128 lines
3.8 KiB
TypeScript
128 lines
3.8 KiB
TypeScript
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<SpriteSequenceDefinition, { source: 'asset' }>,
|
|
) {
|
|
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');
|
|
}
|