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

88 lines
2.4 KiB
TypeScript

import React, {useEffect, useState} from 'react';
export interface HostileNpcAnimationConfig {
start: number;
frames: number;
fps?: number;
}
export interface HostileNpcSpriteConfig {
id: string;
name: string;
src: string;
frameWidth: number;
frameHeight: number;
sheetWidth: number;
animations: {
idle: HostileNpcAnimationConfig;
move?: HostileNpcAnimationConfig;
attack?: HostileNpcAnimationConfig;
die?: HostileNpcAnimationConfig;
};
}
interface HostileNpcAnimatorProps {
hostileNpc: HostileNpcSpriteConfig;
animation?: keyof HostileNpcSpriteConfig['animations'];
className?: string;
flip?: boolean;
}
export const HostileNpcAnimator: React.FC<HostileNpcAnimatorProps> = ({
hostileNpc,
animation = 'idle',
className,
flip = false,
}) => {
const [frameOffset, setFrameOffset] = useState(0);
const anim =
hostileNpc.animations[animation] ??
(animation === 'die' ? hostileNpc.animations.attack : undefined) ??
(animation === 'move' ? hostileNpc.animations.attack : undefined) ??
hostileNpc.animations.idle;
const columns = Math.max(1, Math.floor(hostileNpc.sheetWidth / hostileNpc.frameWidth));
const shouldLoop = animation !== 'die' || !hostileNpc.animations.die;
useEffect(() => {
setFrameOffset(0);
if (anim.frames <= 1) {
return;
}
const interval = setInterval(() => {
setFrameOffset(prev => {
if (!shouldLoop) {
return Math.min(prev + 1, anim.frames - 1);
}
return (prev + 1) % anim.frames;
});
}, 1000 / (anim.fps ?? 12));
return () => clearInterval(interval);
}, [anim, shouldLoop]);
const frameIndex = anim.start + frameOffset;
const col = frameIndex % columns;
const row = Math.floor(frameIndex / columns);
return (
<div
className={className}
style={{
width: `${hostileNpc.frameWidth}px`,
height: `${hostileNpc.frameHeight}px`,
backgroundImage: `url("${encodeURI(hostileNpc.src)}")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: `-${col * hostileNpc.frameWidth}px -${row * hostileNpc.frameHeight}px`,
backgroundSize: `${hostileNpc.sheetWidth}px auto`,
imageRendering: 'pixelated',
transform: flip ? 'scaleX(-1)' : undefined,
transformOrigin: 'center',
}}
aria-label={hostileNpc.name}
role="img"
/>
);
};