87
src/components/HostileNpcAnimator.tsx
Normal file
87
src/components/HostileNpcAnimator.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user