Android build v1.1.2

This commit is contained in:
Warren H
2026-06-29 22:35:49 -04:00
parent cbe42b6164
commit c6251167a5
52 changed files with 5554 additions and 3117 deletions
+91
View File
@@ -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,
})
}
+104
View File
@@ -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,
}
}
+65
View File
@@ -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,
}
}