Android build v1.1.5
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { cooldownNow, hasActiveCooldowns } from '../combat/spellCasting'
|
||||
|
||||
export function useCooldownClock(
|
||||
cooldowns: Record<string, number>,
|
||||
active: boolean,
|
||||
intervalMs = 100,
|
||||
) {
|
||||
const [now, setNow] = useState(cooldownNow)
|
||||
|
||||
useEffect(() => {
|
||||
if (!active || !hasActiveCooldowns(cooldowns)) return undefined
|
||||
|
||||
let timer = 0
|
||||
let startTimer = 0
|
||||
const tick = () => {
|
||||
const nextNow = cooldownNow()
|
||||
setNow(nextNow)
|
||||
if (timer && !hasActiveCooldowns(cooldowns, nextNow)) {
|
||||
window.clearInterval(timer)
|
||||
timer = 0
|
||||
}
|
||||
}
|
||||
|
||||
startTimer = window.setTimeout(() => {
|
||||
tick()
|
||||
timer = window.setInterval(tick, intervalMs)
|
||||
}, 0)
|
||||
return () => {
|
||||
window.clearTimeout(startTimer)
|
||||
if (timer) window.clearInterval(timer)
|
||||
}
|
||||
}, [active, cooldowns, intervalMs])
|
||||
|
||||
return now
|
||||
}
|
||||
@@ -15,41 +15,63 @@ 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 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))
|
||||
}, [durationMs])
|
||||
scheduleCleanup()
|
||||
}, [durationMs, scheduleCleanup])
|
||||
|
||||
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)
|
||||
}, [])
|
||||
useEffect(() => () => clearCleanupTimer(), [clearCleanupTimer])
|
||||
|
||||
const floatingTextsByMember = useMemo(
|
||||
() => groupFloatingTextsByMember(floatingTexts),
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { Spell } from '../game'
|
||||
import type { SpellSlot } from '../components/SpellBars'
|
||||
import { cooldownRemaining } from '../combat/spellCasting'
|
||||
import { useCooldownClock } from './useCooldownClock'
|
||||
|
||||
type UseSpellSlotsOptions = {
|
||||
spells: readonly Spell[]
|
||||
cooldowns: Record<string, number>
|
||||
active: boolean
|
||||
cost: (spell: Spell) => number
|
||||
abilitySlots?: readonly (number | null)[]
|
||||
}
|
||||
|
||||
export function useSpellSlots({
|
||||
spells,
|
||||
cooldowns,
|
||||
active,
|
||||
cost,
|
||||
abilitySlots,
|
||||
}: UseSpellSlotsOptions) {
|
||||
const now = useCooldownClock(cooldowns, active)
|
||||
|
||||
return useMemo<SpellSlot[]>(() => {
|
||||
const slotCount = abilitySlots?.length ?? spells.length
|
||||
return Array.from({ length: slotCount }, (_, slotIndex) => {
|
||||
if (abilitySlots && !abilitySlots[slotIndex]) return null
|
||||
const spell = spells[slotIndex]
|
||||
if (!spell) return null
|
||||
return {
|
||||
...spell,
|
||||
cost: cost(spell),
|
||||
slotIndex,
|
||||
remaining: cooldownRemaining(cooldowns, spell.id, now),
|
||||
}
|
||||
})
|
||||
}, [abilitySlots, cooldowns, cost, now, spells])
|
||||
}
|
||||
Reference in New Issue
Block a user