import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { appendFloatingText, groupFloatingTextsByMember, stripFloatingTextSide, type BasicFloatingCombatText, } from '../combat/combatPresentation' type FloatingSide = 'player' | 'cpu' type SideFloatingCombatText = BasicFloatingCombatText & { side: FloatingSide } type FloatingCombatTextOptions = { durationMs?: number } export function useFloatingCombatText({ durationMs = 900, }: FloatingCombatTextOptions = {}) { const [floatingTexts, setFloatingTexts] = useState([]) const nextId = useRef(1) const expirationsRef = useRef(new Map()) const cleanupTimerRef = useRef(null) const clearCleanupTimer = useCallback(() => { if (cleanupTimerRef.current === null) return window.clearTimeout(cleanupTimerRef.current) cleanupTimerRef.current = null }, []) const scheduleCleanup = useCallback(() => { function scheduleNext() { clearCleanupTimer() let nextExpiry = Infinity for (const expiresAt of expirationsRef.current.values()) { nextExpiry = Math.min(nextExpiry, expiresAt) } if (!Number.isFinite(nextExpiry)) return cleanupTimerRef.current = window.setTimeout(() => { cleanupTimerRef.current = null const now = performance.now() let removed = false for (const [id, expiresAt] of expirationsRef.current) { if (expiresAt > now) continue expirationsRef.current.delete(id) removed = true } if (removed) { setFloatingTexts((current) => current.filter((text) => expirationsRef.current.has(text.id))) } scheduleNext() }, Math.max(0, nextExpiry - performance.now())) } scheduleNext() }, [clearCleanupTimer]) const clearFloatingTexts = useCallback(() => { clearCleanupTimer() expirationsRef.current.clear() setFloatingTexts([]) }, [clearCleanupTimer]) const addFloatingText = useCallback((entry: Omit) => { if (entry.value <= 0) return const id = nextId.current++ expirationsRef.current.set(id, performance.now() + durationMs) setFloatingTexts((current) => appendFloatingText(current, { ...entry, id } as TText)) scheduleCleanup() }, [durationMs, scheduleCleanup]) useEffect(() => () => clearCleanupTimer(), [clearCleanupTimer]) const floatingTextsByMember = useMemo( () => groupFloatingTextsByMember(floatingTexts), [floatingTexts], ) return { floatingTexts, floatingTextsByMember, addFloatingText, clearFloatingTexts, } } export function useSidedFloatingCombatText(options?: FloatingCombatTextOptions) { const { floatingTexts, addFloatingText, clearFloatingTexts, } = useFloatingCombatText(options) const playerFloatingTexts = useMemo( () => floatingTexts.filter((entry) => entry.side === 'player'), [floatingTexts], ) const cpuFloatingTexts = useMemo( () => floatingTexts.filter((entry) => entry.side === 'cpu'), [floatingTexts], ) const playerFloatingTextsByMember = useMemo( () => groupFloatingTextsByMember(playerFloatingTexts), [playerFloatingTexts], ) const cpuFloatingTextsByMember = useMemo( () => groupFloatingTextsByMember(cpuFloatingTexts), [cpuFloatingTexts], ) const dualScreenFloatingTexts = useMemo( () => stripFloatingTextSide(playerFloatingTexts), [playerFloatingTexts], ) return { floatingTexts, playerFloatingTexts, cpuFloatingTexts, playerFloatingTextsByMember, cpuFloatingTextsByMember, dualScreenFloatingTexts, addFloatingText, clearFloatingTexts, } }