Files
Genarrative/src/data/characterCombat.ts
T
kdletters 071faa482c 统一 Rust 与 TypeScript 格式化门禁
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口

完成项目 TypeScript/Prettier 与 Rust 全量格式化

修复 Pingora expected executable 门禁的空白敏感误报

同步开发运维文档与 AGC skill pack 格式化忽略规则
2026-09-01 16:28:34 +08:00

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