import { AnimationState, Character, CharacterAnimationConfig, CharacterSkillDefinition, CombatDelivery, SpriteSequenceDefinition, } from '../types'; const DEFAULT_FRAME_MS = 100; const DEFAULT_FPS = 10; function getCharacterRoot(character: Character) { return `/character/${encodeURIComponent(character.assetFolder)}/${encodeURIComponent(character.assetVariant)}`; } function buildFramesFromConfig( character: Character, config: CharacterAnimationConfig, folderPrefix = 'Hero', ) { const normalizedBasePath = config.basePath?.replace(/\/+$/u, ''); const extension = config.extension ?? 'png'; if (normalizedBasePath) { if (config.file) { return [`${normalizedBasePath}/${encodeURIComponent(config.file)}`]; } const frames: string[] = []; const startFrame = config.startFrame ?? 1; for (let index = 0; index < config.frames; index += 1) { const frameNumber = (startFrame + index).toString().padStart(2, '0'); frames.push(`${normalizedBasePath}/${config.prefix}${frameNumber}.${extension}`); } return frames; } const root = getCharacterRoot(character); const folder = encodeURIComponent(config.folder); if (config.file) { return [`${root}/${folderPrefix}/${folder}/${encodeURIComponent(config.file)}`]; } const frames: string[] = []; const startFrame = config.startFrame ?? 1; for (let index = 0; index < config.frames; index += 1) { const frameNumber = (startFrame + index).toString().padStart(2, '0'); frames.push(`${root}/${folderPrefix}/${folder}/${config.prefix}${frameNumber}.${extension}`); } return frames; } function buildFramesFromAsset( character: Character, sequence: Extract, ) { const root = getCharacterRoot(character); const folder = sequence.folder .split('/') .map(segment => encodeURIComponent(segment)) .join('/'); if (sequence.file) { return [`${root}/${folder}/${encodeURIComponent(sequence.file)}`]; } const frames: string[] = []; const totalFrames = Math.max(1, sequence.frames ?? 1); const startFrame = sequence.startFrame ?? 1; const extension = sequence.extension ?? 'png'; for (let index = 0; index < totalFrames; index += 1) { const frameNumber = (startFrame + index).toString().padStart(2, '0'); frames.push(`${root}/${folder}/${sequence.prefix ?? ''}${frameNumber}.${extension}`); } return frames; } export function getCharacterAnimationConfig( character: Character, animation: AnimationState, ) { return character.animationMap?.[animation] ?? null; } export function getCharacterAnimationDurationMs( character: Character, animation: AnimationState, ) { const config = getCharacterAnimationConfig(character, animation); if (!config) return DEFAULT_FRAME_MS; return Math.max(DEFAULT_FRAME_MS, config.frames * DEFAULT_FRAME_MS); } export function getSequenceFps(sequence: SpriteSequenceDefinition) { return sequence.fps ?? DEFAULT_FPS; } export function getSequenceDurationMs(sequence: SpriteSequenceDefinition, frameCount: number) { const fps = getSequenceFps(sequence); return Math.max(DEFAULT_FRAME_MS, Math.ceil((Math.max(1, frameCount) * 1000) / fps)); } export function resolveSequenceFrames( character: Character, sequence: SpriteSequenceDefinition, ) { if (sequence.source === 'animation') { const config = getCharacterAnimationConfig(character, sequence.animation); return config ? buildFramesFromConfig(character, config) : []; } return buildFramesFromAsset(character, sequence); } export function getSkillCasterAnimation(skill: CharacterSkillDefinition) { return skill.casterAnimation ?? skill.animation; } export function getSkillDelivery(skill: CharacterSkillDefinition): CombatDelivery { return skill.delivery ?? (skill.style === 'projectile' ? 'ranged' : 'melee'); }