import {motion} from 'motion/react'; import React, {useEffect, useLayoutEffect, useRef, useState} from 'react'; import type {Character, CombatVisualEffect, SceneHostileNpc} from '../../types'; import {getEntityEffectBottom} from './GameCanvasShared'; interface GameCanvasEffectLayerProps { activeCombatEffects: CombatVisualEffect[]; getPlayerEffectLeft: (effectX: number, offsetPx?: number) => string; getHostileNpcEffectLeft: (effectX: number, hostileNpcId?: string, offsetPx?: number) => string; sceneCombatants: SceneHostileNpc[]; playerCharacter: Character | null; groundBottom: string; stageLiftPx: number; playerOffsetY: number; stageRef: React.RefObject; } function useCombatEffectFrames(effect: CombatVisualEffect) { const [frameIndex, setFrameIndex] = useState(0); useEffect(() => { setFrameIndex(0); if (effect.frames.length <= 1) return; const interval = window.setInterval(() => { setFrameIndex(prev => Math.min(prev + 1, effect.frames.length - 1)); }, Math.max(50, Math.round(1000 / effect.fps))); return () => window.clearInterval(interval); }, [effect.fps, effect.frames, effect.id]); return Math.min(frameIndex, Math.max(0, effect.frames.length - 1)); } function TravelingSpriteCombatEffect({ effect, startLeft, endLeft, startBottom, endBottom, stageRef, }: { effect: CombatVisualEffect; startLeft: string; endLeft: string; startBottom: string; endBottom: string; stageRef: React.RefObject; }) { const frameIndex = useCombatEffectFrames(effect); const startMarkerRef = useRef(null); const endMarkerRef = useRef(null); const [vector, setVector] = useState({x: 0, y: 0}); const [measured, setMeasured] = useState(false); useLayoutEffect(() => { setMeasured(false); let cancelled = false; const measure = () => { const stage = stageRef.current; const startEl = startMarkerRef.current; const endEl = endMarkerRef.current; if (cancelled) return; if (!stage || !startEl || !endEl) { setVector({x: 0, y: 0}); setMeasured(true); return; } const stageRect = stage.getBoundingClientRect(); const startRect = startEl.getBoundingClientRect(); const endRect = endEl.getBoundingClientRect(); const startX = startRect.left + startRect.width / 2 - stageRect.left; const startY = startRect.top + startRect.height / 2 - stageRect.top; const endX = endRect.left + endRect.width / 2 - stageRect.left; const endY = endRect.top + endRect.height / 2 - stageRect.top; setVector({x: endX - startX, y: endY - startY}); setMeasured(true); }; const frameId = requestAnimationFrame(() => { requestAnimationFrame(measure); }); return () => { cancelled = true; cancelAnimationFrame(frameId); }; }, [effect.id, endBottom, endLeft, stageRef, startBottom, startLeft]); const half = effect.sizePx / 2; const markerBox: React.CSSProperties = { position: 'absolute', width: effect.sizePx, height: effect.sizePx, marginLeft: -half, pointerEvents: 'none', visibility: 'hidden', zIndex: 0, }; return ( <>
{measured && ( )} ); } function SpriteCombatEffect({ effect, startLeft, endLeft, startBottom, endBottom, }: { effect: CombatVisualEffect; startLeft: string; endLeft?: string; startBottom: string; endBottom?: string; }) { const frameIndex = useCombatEffectFrames(effect); return ( ); } export function GameCanvasEffectLayer({ activeCombatEffects, getPlayerEffectLeft, getHostileNpcEffectLeft, sceneCombatants, playerCharacter, groundBottom, stageLiftPx, playerOffsetY, stageRef, }: GameCanvasEffectLayerProps) { return ( <> {activeCombatEffects.map(effect => { const startLeft = effect.startOrigin === 'player' ? getPlayerEffectLeft(effect.startX, effect.startOffsetX ?? 0) : getHostileNpcEffectLeft(effect.startX, effect.startHostileNpcId ?? effect.startMonsterId, effect.startOffsetX ?? 0); const endLeft = effect.endOrigin === 'player' ? getPlayerEffectLeft(effect.endX ?? effect.startX, effect.endOffsetX ?? effect.startOffsetX ?? 0) : effect.endOrigin === 'hostile_npc' || effect.endOrigin === 'monster' ? getHostileNpcEffectLeft(effect.endX ?? effect.startX, effect.endHostileNpcId ?? effect.endMonsterId, effect.endOffsetX ?? effect.startOffsetX ?? 0) : undefined; const startBottom = `calc(${getEntityEffectBottom({ origin: effect.startOrigin, hostileNpcId: effect.startHostileNpcId ?? effect.startMonsterId, sceneCombatants, playerCharacter, groundBottom, stageLiftPx, playerOffsetY, anchorOffsetY: effect.startAnchorOffsetY ?? 0, })} + ${effect.startYOffset}px)`; const endBottom = `calc(${getEntityEffectBottom({ origin: effect.endOrigin ?? effect.startOrigin, hostileNpcId: effect.endHostileNpcId ?? effect.endMonsterId ?? effect.startHostileNpcId ?? effect.startMonsterId, sceneCombatants, playerCharacter, groundBottom, stageLiftPx, playerOffsetY, anchorOffsetY: effect.endAnchorOffsetY ?? effect.startAnchorOffsetY ?? 0, })} + ${(effect.endYOffset ?? effect.startYOffset)}px)`; const useTravelingPath = Boolean( effect.traveling && endLeft && endBottom && (startLeft !== endLeft || startBottom !== endBottom), ); if (useTravelingPath && endLeft && endBottom) { return ( ); } return ( ); })} ); }