import React, { useEffect, useState } from 'react'; import { AnimationState, Character, CharacterAnimationConfig } from '../types'; interface CharacterAnimatorProps { state: AnimationState; character: Character; className?: string; style?: React.CSSProperties; imageClassName?: string; } const DEFAULT_ANIMATIONS: Record = { [AnimationState.ACQUIRE]: { frames: 1, prefix: 'acquire', folder: 'acquire' }, [AnimationState.ATTACK]: { frames: 1, prefix: 'Attack', folder: 'attack' }, [AnimationState.RUN]: { frames: 1, prefix: 'Run', folder: 'run' }, [AnimationState.DOUBLE_JUMP]: { frames: 1, prefix: 'double jump', folder: 'double jump' }, [AnimationState.JUMP_ATTACK]: { frames: 1, prefix: 'jump attack', folder: 'jump attack' }, [AnimationState.DASH]: { frames: 1, prefix: 'dash', folder: 'dash' }, [AnimationState.HURT]: { frames: 1, prefix: 'hurt', folder: 'hurt' }, [AnimationState.DIE]: { frames: 1, prefix: 'die', folder: 'die' }, [AnimationState.CLIMB]: { frames: 1, prefix: 'Climb', folder: 'climb' }, [AnimationState.SKILL1]: { frames: 1, prefix: 'skill1', folder: 'skill1' }, [AnimationState.SKILL1_JUMP]: { frames: 1, prefix: 'skill1 jump', folder: 'skill1 jump' }, [AnimationState.SKILL1_BULLET]: { frames: 1, prefix: 'skill1 bullet', folder: 'skill1 bullet' }, [AnimationState.SKILL1_BULLET_FX]: { frames: 1, prefix: 'skill1 bullet FX', folder: 'skill1 bullet FX' }, [AnimationState.SKILL2]: { frames: 1, prefix: 'skill2', folder: 'skill2' }, [AnimationState.SKILL2_JUMP]: { frames: 1, prefix: 'skill2 jump', folder: 'skill2 jump' }, [AnimationState.SKILL3]: { frames: 1, prefix: 'skill3', folder: 'skill3' }, [AnimationState.SKILL3_JUMP]: { frames: 1, prefix: 'skill3 jump', folder: 'skill3 jump' }, [AnimationState.SKILL3_BULLET]: { frames: 1, prefix: 'skill3 bullet', folder: 'skill3 bullet' }, [AnimationState.SKILL3_BULLET_FX]: { frames: 1, prefix: 'skill3 bullet FX', folder: 'skill3 bullet FX' }, [AnimationState.SKILL4]: { frames: 1, prefix: 'skill4', folder: 'skill4' }, [AnimationState.WALL_SLIDE]: { frames: 1, prefix: 'Wall Slide', folder: 'Wall Slide' }, [AnimationState.IDLE]: { frames: 1, prefix: 'Idle', folder: 'idle' }, [AnimationState.JUMP]: { frames: 1, prefix: 'Jump', folder: 'jump' }, }; export const CharacterAnimator: React.FC = ({ state, character, className, style, imageClassName, }) => { const [frameIndex, setFrameIndex] = useState(1); const config = character.animationMap?.[state] ?? DEFAULT_ANIMATIONS[state] ?? character.animationMap?.[AnimationState.IDLE] ?? DEFAULT_ANIMATIONS[AnimationState.IDLE]; useEffect(() => { setFrameIndex(config.startFrame || 1); if (config.frames <= 1) return; const interval = setInterval(() => { setFrameIndex(prev => { const start = config.startFrame || 1; const end = start + config.frames - 1; return prev >= end ? start : prev + 1; }); }, 100); return () => clearInterval(interval); }, [config]); const frameNumber = frameIndex.toString().padStart(2, '0'); const normalizedBasePath = config.basePath?.replace(/\/+$/u, ''); const imagePath = normalizedBasePath ? config.file ? `${normalizedBasePath}/${encodeURIComponent(config.file)}` : `${normalizedBasePath}/${config.prefix}${frameNumber}.${config.extension ?? 'png'}` : (() => { const folder = encodeURIComponent(character.assetFolder); const variant = encodeURIComponent(character.assetVariant); const animationFolder = encodeURIComponent(config.folder); return config.file ? `/character/${folder}/${variant}/Hero/${animationFolder}/${encodeURIComponent(config.file)}` : `/character/${folder}/${variant}/Hero/${animationFolder}/${config.prefix}${frameNumber}.${config.extension ?? 'png'}`; })(); const resolvedImageClassName = `h-full w-full object-contain pixelated ${imageClassName ?? ''}`.trim(); return (
{`${character.name} { const target = e.target as HTMLImageElement; target.src = character.portrait; target.className = resolvedImageClassName; }} />
); };