Files
Genarrative/src/components/CharacterAnimator.tsx
高物 c49c64896a
Some checks failed
CI / verify (push) Has been cancelled
初始仓库迁移
2026-04-04 23:57:06 +08:00

101 lines
4.4 KiB
TypeScript

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, CharacterAnimationConfig> = {
[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<CharacterAnimatorProps> = ({
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 (
<div className={`relative ${className ?? ''}`} style={style}>
<img
src={imagePath}
alt={`${character.name} ${state} animation`}
className={resolvedImageClassName}
style={{ imageRendering: 'pixelated' }}
onError={(e) => {
const target = e.target as HTMLImageElement;
target.src = character.portrait;
target.className = resolvedImageClassName;
}}
/>
</div>
);
};