88 lines
2.4 KiB
TypeScript
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"
|
|
/>
|
|
);
|
|
};
|