import { useEffect, useState, type CSSProperties } from 'react'; import { DAMAGE_NUMBER_DURATION_MS, DAMAGE_NUMBER_RISE_PX } from '../shared/constants'; import type { FloatingDamageData } from '../game/types'; interface FloatingDamageProps { damages: FloatingDamageData[]; } interface DamageNumberProps { data: FloatingDamageData; onExpire: (id: number) => void; } function DamageNumber({ data, onExpire }: DamageNumberProps) { const [progress, setProgress] = useState(0); useEffect(() => { let rafId: number; const start = data.createdAt; const animate = () => { const elapsed = performance.now() - start; const p = Math.min(1, elapsed / DAMAGE_NUMBER_DURATION_MS); setProgress(p); if (p < 1) { rafId = requestAnimationFrame(animate); } else { onExpire(data.id); } }; rafId = requestAnimationFrame(animate); return () => cancelAnimationFrame(rafId); }, [data.createdAt, data.id, onExpire]); const offsetY = -progress * DAMAGE_NUMBER_RISE_PX; const opacity = 1 - progress * progress; // ease-out fade const scale = data.isCrit && data.kind === 'damage' ? 1.4 - progress * 0.4 : 1; const isOutcomeText = data.kind === 'blocked' || data.kind === 'evaded'; const color = data.kind === 'regen' ? '#44dd66' : isOutcomeText ? (data.target === 'hero' ? '#44dd66' : '#ff5566') : (data.isCrit ? '#ffdd44' : '#ffffff'); const fontSize = isOutcomeText ? 16 : (data.isCrit ? 24 : 18); const style: CSSProperties = { position: 'absolute', left: data.x, top: data.y + offsetY, transform: `translate(-50%, -50%) scale(${scale})`, opacity, color, fontSize, fontWeight: 900, textShadow: '0 2px 4px rgba(0,0,0,0.9), 0 0 8px rgba(0,0,0,0.5)', pointerEvents: 'none', willChange: 'transform, opacity', }; return (