105 lines
2.9 KiB
TypeScript
105 lines
2.9 KiB
TypeScript
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
|
|
}
|
|
|
|
const FLOATING_TEXT_CLEANUP_INTERVAL_MS = 100
|
|
|
|
export function useFloatingCombatText<TText extends BasicFloatingCombatText>({
|
|
durationMs = 900,
|
|
}: FloatingCombatTextOptions = {}) {
|
|
const [floatingTexts, setFloatingTexts] = useState<TText[]>([])
|
|
const nextId = useRef(1)
|
|
const expirationsRef = useRef(new Map<number, number>())
|
|
|
|
const clearFloatingTexts = useCallback(() => {
|
|
expirationsRef.current.clear()
|
|
setFloatingTexts([])
|
|
}, [])
|
|
|
|
const addFloatingText = useCallback((entry: Omit<TText, 'id'>) => {
|
|
if (entry.value <= 0) return
|
|
const id = nextId.current++
|
|
expirationsRef.current.set(id, performance.now() + durationMs)
|
|
setFloatingTexts((current) => appendFloatingText(current, { ...entry, id } as TText))
|
|
}, [durationMs])
|
|
|
|
useEffect(() => {
|
|
const timer = window.setInterval(() => {
|
|
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) return
|
|
setFloatingTexts((current) => current.filter((text) => expirationsRef.current.has(text.id)))
|
|
}, FLOATING_TEXT_CLEANUP_INTERVAL_MS)
|
|
return () => window.clearInterval(timer)
|
|
}, [])
|
|
|
|
const floatingTextsByMember = useMemo(
|
|
() => groupFloatingTextsByMember(floatingTexts),
|
|
[floatingTexts],
|
|
)
|
|
|
|
return {
|
|
floatingTexts,
|
|
floatingTextsByMember,
|
|
addFloatingText,
|
|
clearFloatingTexts,
|
|
}
|
|
}
|
|
|
|
export function useSidedFloatingCombatText(options?: FloatingCombatTextOptions) {
|
|
const {
|
|
floatingTexts,
|
|
addFloatingText,
|
|
clearFloatingTexts,
|
|
} = useFloatingCombatText<SideFloatingCombatText>(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,
|
|
}
|
|
}
|