import { useState, useRef, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import toast from 'react-hot-toast'; import { combatApi, turnCombatApi, characterApi } from '../api/endpoints'; import type { Monster, TurnResult, TurnBuff } from '../api/types'; import { Swords, Sparkles, PackageOpen, ArrowLeft, Zap, Shield, Skull, Trophy, Users } from 'lucide-react'; import { COMBAT_COST, ATTACK_TYPES, ZONE_INFO } from '../constants'; import { MonsterCard } from '../components/MonsterCard'; type Phase = 'select' | 'combat' | 'result'; export function TurnCombatPage() { const qc = useQueryClient(); const [phase, setPhase] = useState('select'); const [selectedMonster, setSelectedMonster] = useState(null); const [attackType, setAttackType] = useState('melee'); const [combat, setCombat] = useState(null); const [companion, setCompanion] = useState<'mira' | 'vell' | null>(null); const [spellMenuOpen, setSpellMenuOpen] = useState(false); const logRef = useRef(null); const { data: char } = useQuery({ queryKey: ['character'], queryFn: characterApi.me }); const { data: monsters } = useQuery({ queryKey: ['monsters'], queryFn: combatApi.monsters }); const { data: zones } = useQuery({ queryKey: ['zones'], queryFn: combatApi.zones }); const { data: spells } = useQuery({ queryKey: ['turnSpells'], queryFn: turnCombatApi.unlockedSpells }); const { data: daoPaths } = useQuery({ queryKey: ['daoPaths'], queryFn: turnCombatApi.dao }); const hasDaoPath = daoPaths && daoPaths.length > 0 && daoPaths.some((p: any) => p.isPrimary || p.is_primary); const chooseDaoMut = useMutation({ mutationFn: (path: string) => turnCombatApi.chooseDaoPath(path), onSuccess: () => { qc.invalidateQueries({ queryKey: ['daoPaths'] }); qc.invalidateQueries({ queryKey: ['turnSpells'] }); toast.success('Voie du Dao choisie !'); }, onError: (err: Error) => toast.error(err.message), }); const endurance = char?.enduranceCurrent ?? 0; const canFight = endurance >= COMBAT_COST; // Scroll log vers le bas useEffect(() => { if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; }, [combat?.events]); // --- Start combat --- const startMut = useMutation({ mutationFn: () => turnCombatApi.start(selectedMonster!.id, attackType, companion), onSuccess: (result) => { setCombat(result); setPhase('combat'); setSpellMenuOpen(false); }, onError: (err: Error) => toast.error(err.message), }); // --- Submit action --- const actionMut = useMutation({ mutationFn: (params: { type: string; spellId?: string }) => turnCombatApi.action(combat!.sessionId, params.type, params.spellId), onSuccess: (result) => { setCombat(result); setSpellMenuOpen(false); if (result.status === 'finished') { setPhase('result'); qc.invalidateQueries({ queryKey: ['character'] }); qc.invalidateQueries({ queryKey: ['combatHistory'] }); qc.invalidateQueries({ queryKey: ['questsActive'] }); } }, onError: (err: Error) => toast.error(err.message), }); const doAction = (type: string, spellId?: string) => { if (actionMut.isPending) return; actionMut.mutate({ type, spellId }); }; // ========== PHASE: CHOOSE DAO PATH ========== if (!hasDaoPath) { const paths = [ { id: 'ecoute', name: 'Écoute', color: '#88c8e8', icon: '👁️', archetype: 'Le stratège', desc: 'Perception du flux, chant offensif, ancrage mémoriel. Tu deviens ce que Gorn t\'a appris : observer, comprendre.', spell: 'Perception du Flux (révèle faiblesses, +20% dégâts)' }, { id: 'resonance', name: 'Résonance', color: '#f4c94e', icon: '💪', archetype: 'Le protecteur', desc: 'Onde de choc, bouclier, contre-attaque. Tu deviens ce que Vell a appris : la vraie force protège.', spell: 'Onde de Choc (dégâts AoE, Force ×1.5)' }, { id: 'harmonie', name: 'Harmonie', color: '#3ddc84', icon: '🎵', archetype: 'L\'harmoniste', desc: 'Chant apaisant, purge, soin d\'équipe. Tu deviens ce que Mira est : le chant qui guérit.', spell: 'Chant Apaisant (soin Int ×2 + 10% HP max)' }, ]; return (

Le Dao du Courant s'éveille

Gorn est parti. Le Serment est prêté. Le courant coule en toi.
Quelle voie du Dao vas-tu suivre ?

{paths.map(p => ( ))}

Tu pourras explorer les autres voies plus tard — ta voie principale progresse plus vite.

); } // ========== PHASE: SELECT ========== if (phase === 'select') { const monstersByZone = new Map(); 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 lockedZones = (zones ?? []).filter((z: any) => !z.unlocked); return (

Combat Tactique

{Array.from(monstersByZone.entries()).map(([zone, zoneMonsters]) => { const info = ZONE_INFO[zone] ?? { name: zone, emoji: '📍' }; return (

{info.emoji} {info.name}

{zoneMonsters.sort((a, b) => a.minLevel - b.minLevel).map(m => ( setSelectedMonster(m)} playerLevel={char?.level ?? 1} /> ))}
); })} {lockedZones.map((z: any) => (
{z.emoji} {z.name} — Verrouillee
))}

Type d'attaque

{ATTACK_TYPES.map(a => (
setAttackType(a.id)} style={{ display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer' }} > {a.emoji}
{a.label}
{a.stat}
))}
{/* Compagnon */}

Compagnon (optionnel)

{[ { id: null as 'mira' | 'vell' | null, label: 'Solo', emoji: '🐸', desc: '' }, { id: 'mira' as const, label: 'Mira', emoji: '🌊', desc: 'Support — heal + buff' }, { id: 'vell' as const, label: 'Vell', emoji: '🪨', desc: 'Tank — taunt + DPS' }, ].map(c => (
setCompanion(c.id)} style={{ flex: 1, cursor: 'pointer', textAlign: 'center', padding: '0.5rem' }} >
{c.emoji}
{c.label}
{c.desc &&
{c.desc}
}
))}
Cout : {COMBAT_COST} endurance — Dispo : {endurance}
); } // ========== PHASE: COMBAT ========== if (phase === 'combat' && combat) { const playerHpPct = Math.round((combat.playerHp / combat.playerHpMax) * 100); const monsterHpPct = Math.round((combat.monsterHp / combat.monsterHpMax) * 100); const manaPct = Math.round((combat.playerMana / combat.playerManaMax) * 100); const isActing = actionMut.isPending; return (

Tour {combat.round}

{/* Combatants */}
{/* Player */}
{combat.playerName}
{/* Companion */} {combat.companion && (
{combat.companion.type === 'mira' ? '🌊' : '🪨'} {combat.companion.name} {combat.companion.hpCurrent <= 0 && ' (KO)'}
)} {/* Monster */}
{combat.monsterName}
{/* Log */}
{combat.events.length === 0 && (

Le combat commence...

)} {combat.events.map((e, i) => (
[T{e.round}] {e.detail}
))}
{/* Actions */} {!spellMenuOpen ? (
) : (
{(spells ?? []).map(spell => { const cd = combat.spellCooldowns[spell.id] ?? 0; const notEnoughMana = combat.playerMana < spell.manaCost; const disabled = isActing || cd > 0 || notEnoughMana; return ( ); })}
)}
); } // ========== PHASE: RESULT ========== if (phase === 'result' && combat) { const won = combat.winner === 'player'; return (
{won ? : }

{won ? 'Victoire !' : 'Defaite...'}

Combat termine en {combat.round} tour{combat.round > 1 ? 's' : ''}

{won && combat.rewards && (
+{combat.rewards.xp} XP   +{combat.rewards.gold} Or
{combat.rewards.levelUp && (
LEVEL UP ! Niveau {combat.rewards.newLevel} (+{combat.rewards.statPointsGained} stat points)
)}
)}
); } return null; } // ========== Sous-composants ========== function BarDisplay({ label, value, max, pct, color }: { label: string; value: number; max: number; pct: number; color: string }) { return (
{label} {value}/{max}
); } function BuffList({ buffs, debuffs }: { buffs: TurnBuff[]; debuffs: TurnBuff[] }) { if (!buffs.length && !debuffs.length) return null; return (
{buffs.map(b => ( {b.name} ({b.remainingTurns}) ))} {debuffs.map(d => ( {d.name} ({d.remainingTurns}) ))}
); } function eventColor(actor: string, combat: TurnResult): string { if (actor === combat.playerName) return '#3ddc84'; if (actor === combat.monsterName) return '#e84040'; if (actor === 'Mira') return '#5ba4f5'; if (actor === 'Vell') return '#f4c94e'; return '#dce4f0'; } function pathColor(path: string): string { switch (path) { case 'ecoute': return '#5ba4f5'; case 'resonance': return '#e84040'; case 'harmonie': return '#3ddc84'; default: return '#dce4f0'; } }