127 lines
3.7 KiB
TypeScript
127 lines
3.7 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
|
|
}
|
|
|
|
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 cleanupTimerRef = useRef<number | null>(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<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))
|
|
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<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,
|
|
}
|
|
}
|