Files
Genarrative/src/data/characterCombat.ts
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

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');
}