feat: zone locking — progression par arcs narratifs + arcs Égouts/Désert
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 33s
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 33s
Zones verrouillées: marais toujours ouvert, égouts après arc Marais, désert après arc Égouts. Filtrage backend sur monstres ET boutique. Arc "Les Égouts de la Cité" (4 quêtes, lv4-7, boss Roi des Rats) Arc "Les Sables Brûlants" (3 quêtes, lv8-12, boss Sphinx) GET /api/monsters/zones — retourne les zones avec statut unlocked. Combat page: monstres groupés par zone, zones lockées avec icône cadenas. Boutique: items filtrés par zones débloquées (potions toujours visibles).
This commit is contained in:
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { combatApi, characterApi } from '../api/endpoints';
|
||||
import type { Monster, CombatResult, CombatLog } from '../api/types';
|
||||
import { Swords, Trophy, Skull, Clock, Zap, Heart } from 'lucide-react';
|
||||
import { Swords, Trophy, Skull, Clock, Zap, Heart, Lock } from 'lucide-react';
|
||||
|
||||
const COMBAT_COST = 5;
|
||||
const REST_COST = 10;
|
||||
@@ -127,6 +127,11 @@ export function CombatPage() {
|
||||
queryFn: combatApi.monsters,
|
||||
});
|
||||
|
||||
const { data: zones } = useQuery({
|
||||
queryKey: ['zones'],
|
||||
queryFn: combatApi.zones,
|
||||
});
|
||||
|
||||
const { data: history } = useQuery({
|
||||
queryKey: ['combatHistory'],
|
||||
queryFn: combatApi.history,
|
||||
@@ -144,35 +149,64 @@ export function CombatPage() {
|
||||
|
||||
if (isLoading) return <div style={{ padding: '2rem', color: '#6b7a99' }}>Chargement des monstres…</div>;
|
||||
|
||||
// Trier : monstres appropriés en haut, trop forts en bas
|
||||
const sorted = [...(monsters ?? [])].sort((a, b) => {
|
||||
const aOk = a.minLevel <= playerLevel + 2 ? 0 : 1;
|
||||
const bOk = b.minLevel <= playerLevel + 2 ? 0 : 1;
|
||||
if (aOk !== bOk) return aOk - bOk;
|
||||
return a.minLevel - b.minLevel;
|
||||
});
|
||||
// Group monsters by zone
|
||||
const monstersByZone = new Map<string, Monster[]>();
|
||||
for (const m of (monsters ?? [])) {
|
||||
const zone = (m as any).zone ?? 'marais';
|
||||
const list = monstersByZone.get(zone) ?? [];
|
||||
list.push(m);
|
||||
monstersByZone.set(zone, list);
|
||||
}
|
||||
|
||||
const ZONE_LABELS: Record<string, { name: string; emoji: string }> = {
|
||||
marais: { name: 'Les Marais', emoji: '🌿' },
|
||||
egouts: { name: 'Les Égouts', emoji: '🕳️' },
|
||||
desert: { name: 'Le Désert', emoji: '🏜️' },
|
||||
};
|
||||
|
||||
// Locked zones (zones not in monsters response = locked)
|
||||
const lockedZones = (zones ?? []).filter((z: any) => !z.unlocked);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ margin: '0 0 1rem', color: '#f4c94e', fontSize: 20 }}>⚔️ Combat</h2>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginBottom: '1rem' }}>
|
||||
{/* Choix monstre */}
|
||||
{/* Choix monstre par zone */}
|
||||
<div>
|
||||
<p style={{ margin: '0 0 0.5rem', fontSize: 12, fontWeight: 700, color: '#6b7a99', textTransform: 'uppercase' }}>
|
||||
Adversaire
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{sorted.map(m => (
|
||||
<MonsterCard
|
||||
key={m.id}
|
||||
m={m}
|
||||
selected={selectedMonster?.id === m.id}
|
||||
onSelect={() => setSelectedMonster(m)}
|
||||
playerLevel={playerLevel}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{Array.from(monstersByZone.entries()).map(([zone, zoneMonsters]) => {
|
||||
const info = ZONE_LABELS[zone] ?? { name: zone, emoji: '📍' };
|
||||
return (
|
||||
<div key={zone} style={{ marginBottom: '1rem' }}>
|
||||
<p style={{ margin: '0 0 0.5rem', fontSize: 12, fontWeight: 700, color: '#9ca3af' }}>
|
||||
{info.emoji} {info.name}
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{zoneMonsters
|
||||
.sort((a, b) => a.minLevel - b.minLevel)
|
||||
.map(m => (
|
||||
<MonsterCard
|
||||
key={m.id}
|
||||
m={m}
|
||||
selected={selectedMonster?.id === m.id}
|
||||
onSelect={() => setSelectedMonster(m)}
|
||||
playerLevel={playerLevel}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Zones verrouillées */}
|
||||
{lockedZones.map((z: any) => (
|
||||
<div key={z.id} className="card" style={{ marginBottom: '0.5rem', opacity: 0.4, textAlign: 'center', padding: '1rem' }}>
|
||||
<Lock size={16} color="#6b7a99" style={{ display: 'inline', marginRight: 6 }} />
|
||||
<span style={{ fontSize: 13, color: '#6b7a99' }}>
|
||||
{z.emoji} {z.name} — Complétez l'arc précédent pour débloquer
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Panneau droite */}
|
||||
|
||||
Reference in New Issue
Block a user