Android build v1.1.2
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
type DeadlineTimerOptions = {
|
||||
initialSeconds: number
|
||||
active: boolean
|
||||
intervalMs?: number
|
||||
onExpire: () => void
|
||||
}
|
||||
|
||||
type RoundCountdownOptions = {
|
||||
seconds: number
|
||||
active: boolean
|
||||
intervalMs?: number
|
||||
onComplete: () => void
|
||||
}
|
||||
|
||||
export function useDeadlineTimer({
|
||||
initialSeconds,
|
||||
active,
|
||||
intervalMs = 100,
|
||||
onExpire,
|
||||
}: DeadlineTimerOptions) {
|
||||
const [timeLeft, setTimeLeft] = useState(initialSeconds)
|
||||
const deadlineRef = useRef(0)
|
||||
const timerRef = useRef<number | null>(null)
|
||||
const onExpireRef = useRef(onExpire)
|
||||
|
||||
useEffect(() => {
|
||||
onExpireRef.current = onExpire
|
||||
}, [onExpire])
|
||||
|
||||
const clearTimer = useCallback(() => {
|
||||
if (timerRef.current === null) return
|
||||
window.clearInterval(timerRef.current)
|
||||
timerRef.current = null
|
||||
}, [])
|
||||
|
||||
const tick = useCallback(() => {
|
||||
if (deadlineRef.current <= 0) return
|
||||
const remaining = Math.max(0, (deadlineRef.current - Date.now()) / 1000)
|
||||
setTimeLeft(remaining)
|
||||
if (remaining > 0) return
|
||||
deadlineRef.current = 0
|
||||
clearTimer()
|
||||
onExpireRef.current()
|
||||
}, [clearTimer])
|
||||
|
||||
const start = useCallback((seconds = initialSeconds) => {
|
||||
clearTimer()
|
||||
deadlineRef.current = Date.now() + seconds * 1000
|
||||
setTimeLeft(seconds)
|
||||
timerRef.current = window.setInterval(tick, intervalMs)
|
||||
}, [clearTimer, initialSeconds, intervalMs, tick])
|
||||
|
||||
const clear = useCallback(() => {
|
||||
deadlineRef.current = 0
|
||||
clearTimer()
|
||||
}, [clearTimer])
|
||||
|
||||
const reset = useCallback((seconds = initialSeconds) => {
|
||||
clear()
|
||||
setTimeLeft(seconds)
|
||||
}, [clear, initialSeconds])
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) clearTimer()
|
||||
}, [active, clearTimer])
|
||||
|
||||
useEffect(() => () => clearTimer(), [clearTimer])
|
||||
|
||||
return {
|
||||
timeLeft,
|
||||
start,
|
||||
clear,
|
||||
reset,
|
||||
}
|
||||
}
|
||||
|
||||
export function useRoundCountdown({
|
||||
seconds,
|
||||
active,
|
||||
intervalMs = 100,
|
||||
onComplete,
|
||||
}: RoundCountdownOptions) {
|
||||
return useDeadlineTimer({
|
||||
initialSeconds: seconds,
|
||||
active,
|
||||
intervalMs,
|
||||
onExpire: onComplete,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useCallback, type RefObject } from 'react'
|
||||
import type { PartyMember } from '../game'
|
||||
import {
|
||||
selectDirectionalPartyTarget,
|
||||
selectDirectPartyTarget,
|
||||
selectRelativePartyTarget,
|
||||
type NavigateAction,
|
||||
} from '../combat/targeting'
|
||||
import type { InputAction } from '../input'
|
||||
|
||||
type PartyTargetingOptions = {
|
||||
getParty: () => PartyMember[]
|
||||
selectedIdRef: RefObject<string>
|
||||
setSelectedTargetId: (id: string) => void
|
||||
columns: number
|
||||
livingOnly?: boolean
|
||||
relativeLivingOnly?: boolean
|
||||
directionalLivingOnly?: boolean
|
||||
directLivingOnly?: boolean
|
||||
directTargetGroup?: number
|
||||
}
|
||||
|
||||
export function usePartyTargeting({
|
||||
getParty,
|
||||
selectedIdRef,
|
||||
setSelectedTargetId,
|
||||
columns,
|
||||
livingOnly = true,
|
||||
relativeLivingOnly = livingOnly,
|
||||
directionalLivingOnly = livingOnly,
|
||||
directLivingOnly = livingOnly,
|
||||
directTargetGroup = 0,
|
||||
}: PartyTargetingOptions) {
|
||||
const selectRelativeTarget = useCallback((direction: -1 | 1) => {
|
||||
const targetId = selectRelativePartyTarget(getParty(), selectedIdRef.current, direction, {
|
||||
livingOnly: relativeLivingOnly,
|
||||
})
|
||||
if (targetId) setSelectedTargetId(targetId)
|
||||
}, [getParty, relativeLivingOnly, selectedIdRef, setSelectedTargetId])
|
||||
|
||||
const selectDirectionalTarget = useCallback((action: InputAction) => {
|
||||
const targetId = selectDirectionalPartyTarget(
|
||||
getParty(),
|
||||
selectedIdRef.current,
|
||||
action as NavigateAction,
|
||||
columns,
|
||||
{ livingOnly: directionalLivingOnly },
|
||||
)
|
||||
if (targetId) setSelectedTargetId(targetId)
|
||||
}, [columns, directionalLivingOnly, getParty, selectedIdRef, setSelectedTargetId])
|
||||
|
||||
const selectDirectTarget = useCallback((slot: number) => {
|
||||
const targetId = selectDirectPartyTarget(getParty(), slot, {
|
||||
livingOnly: directLivingOnly,
|
||||
targetGroup: directTargetGroup,
|
||||
})
|
||||
if (targetId) setSelectedTargetId(targetId)
|
||||
}, [directLivingOnly, directTargetGroup, getParty, setSelectedTargetId])
|
||||
|
||||
return {
|
||||
selectRelativeTarget,
|
||||
selectDirectionalTarget,
|
||||
selectDirectTarget,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user