import { RotateCcw } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { getCharacterAnimationDurationMs, getSkillCasterAnimation, getSkillDelivery } from '../data/characterCombat'; import { ROLE_TEMPLATE_CHARACTERS } from '../data/characterPresets'; import { createSceneHostileNpcsFromIds } from '../data/hostileNpcs'; import { buildInitialNpcState, createNpcBattleMonster } from '../data/npcInteractions'; import { getScenePreset } from '../data/scenePresets'; import { buildSkillEffects } from '../hooks/useCombatFlow'; import { AnimationState, Character, CharacterSkillDefinition, CombatActionMode, CombatVisualEffect, Encounter, SceneHostileNpc, WorldType, } from '../types'; import { GameCanvas } from './GameCanvas'; export interface SkillEffectPreviewProps { mode: 'player' | 'npc'; worldType: WorldType; character: Character; skill: CharacterSkillDefinition | null; targetMonsterId?: string | null; npcEncounter?: Encounter | null; targetCharacter?: Character | null; } const PLAYER_X = 0; function getSkillReleaseDelayMs(character: Character, skill: CharacterSkillDefinition) { if (typeof skill.releaseDelayMs === 'number') return skill.releaseDelayMs; const animationDuration = getCharacterAnimationDurationMs(character, getSkillCasterAnimation(skill)); return Math.min(260, Math.max(120, Math.round(animationDuration * 0.45))); } function buildPreviewTargetMonster(worldType: WorldType, targetMonsterId?: string | null) { const previewMonster = createSceneHostileNpcsFromIds( worldType, targetMonsterId ? [targetMonsterId] : [], PLAYER_X, )[0]; return previewMonster ? { ...previewMonster, xMeters: 3.2, animation: 'idle' as const, action: `${previewMonster.name}站稳架势,等待受击`, } : null; } function resetNpcPreviewMonster(monster: SceneHostileNpc) { return { ...monster, animation: 'idle' as const, action: `${monster.name}准备出招`, characterAnimation: undefined, combatMode: undefined, }; } export function SkillEffectPreview({ mode, worldType, character, skill, targetMonsterId, npcEncounter, targetCharacter, }: SkillEffectPreviewProps) { const scenePreset = useMemo(() => getScenePreset(worldType, 0), [worldType]); const fallbackTargetCharacter = useMemo( () => targetCharacter ?? ROLE_TEMPLATE_CHARACTERS.find(candidate => candidate.id !== character.id) ?? ROLE_TEMPLATE_CHARACTERS[0] ?? character, [character, targetCharacter], ); const initialMonsters = useMemo(() => { if (mode === 'player') { const monster = buildPreviewTargetMonster(worldType, targetMonsterId); return monster ? [monster] : []; } if (!npcEncounter) return []; return [ createNpcBattleMonster( npcEncounter, buildInitialNpcState(npcEncounter, worldType), 'fight', { worldType, }, ), ]; }, [mode, npcEncounter, targetMonsterId, worldType]); const [playerAnimation, setPlayerAnimation] = useState(AnimationState.IDLE); const [playerActionMode, setPlayerActionMode] = useState('idle'); const [sceneHostileNpcs, setSceneMonsters] = useState(initialMonsters); const [activeCombatEffects, setActiveCombatEffects] = useState([]); const [replayTick, setReplayTick] = useState(0); const [isPlaying, setIsPlaying] = useState(false); useEffect(() => { setSceneMonsters(initialMonsters); setPlayerAnimation(AnimationState.IDLE); setPlayerActionMode('idle'); setActiveCombatEffects([]); setIsPlaying(false); }, [initialMonsters, skill?.id]); useEffect(() => { if (!skill || !scenePreset) return; let active = true; const timers: number[] = []; const casterAnimation = getSkillCasterAnimation(skill); const delivery = getSkillDelivery(skill); const attackerFacing = mode === 'player' ? 'right' : 'left'; const primaryMonster = initialMonsters[0] ?? null; if (mode === 'player') { setPlayerAnimation(casterAnimation); setPlayerActionMode(delivery); setSceneMonsters(initialMonsters.map(monster => ({ ...monster, action: `${monster.name}正面承受${skill.name}的预览`, }))); } else { setPlayerAnimation(AnimationState.IDLE); setPlayerActionMode('idle'); setSceneMonsters(initialMonsters.map(monster => ({ ...resetNpcPreviewMonster(monster), action: `${monster.name}施展${skill.name}`, characterAnimation: casterAnimation, combatMode: delivery, }))); } setIsPlaying(true); const phases = primaryMonster ? buildSkillEffects( { character, xMeters: mode === 'player' ? PLAYER_X : primaryMonster.xMeters, origin: mode === 'player' ? 'player' : 'monster', facing: attackerFacing, monsterId: mode === 'player' ? undefined : primaryMonster.id, }, { xMeters: mode === 'player' ? primaryMonster.xMeters : PLAYER_X, origin: mode === 'player' ? 'monster' : 'player', monsterId: mode === 'player' ? primaryMonster.id : undefined, }, skill, ) : { cast: [] as CombatVisualEffect[], travel: [] as CombatVisualEffect[], impact: [] as CombatVisualEffect[], castDurationMs: 0, travelDurationMs: 0, impactDurationMs: 0, }; const releaseDelay = (skill.effects?.length ?? 0) > 0 ? getSkillReleaseDelayMs(character, skill) : getCharacterAnimationDurationMs(character, casterAnimation); let delay = releaseDelay; const schedule = (taskDelay: number, task: () => void) => { timers.push(window.setTimeout(() => { if (!active) return; task(); }, taskDelay)); }; if (phases.cast.length > 0) { schedule(delay, () => setActiveCombatEffects(phases.cast)); delay += phases.castDurationMs; } if (phases.travel.length > 0) { schedule(delay, () => setActiveCombatEffects(phases.travel)); delay += phases.travelDurationMs; } if (phases.impact.length > 0) { schedule(delay, () => { setActiveCombatEffects(phases.impact); if (mode === 'player') { setSceneMonsters(current => current.map(monster => ({ ...monster, action: `${monster.name}被${skill.name}命中`, }))); } }); delay += phases.impactDurationMs; } schedule(delay, () => { setActiveCombatEffects([]); setPlayerAnimation(AnimationState.IDLE); setPlayerActionMode('idle'); setSceneMonsters(initialMonsters.map(monster => resetNpcPreviewMonster(monster))); setIsPlaying(false); }); return () => { active = false; timers.forEach(timerId => window.clearTimeout(timerId)); }; }, [character, initialMonsters, mode, replayTick, scenePreset, skill]); return (
{skill?.name ?? '未选择技能'}
{mode === 'player' ? `受击对象:${sceneHostileNpcs[0]?.name ?? '无目标'}` : `受击对象:${fallbackTargetCharacter.name}`}
); }