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

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:
2026-03-24 17:57:23 +01:00
parent 8cb5fcd5ba
commit d1609efaae
8 changed files with 222 additions and 29 deletions

View File

@@ -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 */}