Android build v1.0.60 stadium updated

This commit is contained in:
Warren H
2026-06-23 13:13:57 -04:00
parent c2888c287b
commit cbe42b6164
6 changed files with 184 additions and 26 deletions
Binary file not shown.
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "com.warren.iwanttoheal" applicationId "com.warren.iwanttoheal"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 78 versionCode 79
versionName "1.0.59" versionName "1.0.60"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -4,6 +4,7 @@ import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import com.getcapacitor.BridgeActivity; import com.getcapacitor.BridgeActivity;
import java.io.File; import java.io.File;
@@ -37,6 +38,18 @@ public abstract class ControllerBridgeActivity extends BridgeActivity {
if (hasFocus) enableImmersiveMode(); if (hasFocus) enableImmersiveMode();
} }
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (
event.getActionMasked() == MotionEvent.ACTION_DOWN
&& bridge != null
&& bridge.getWebView() != null
) {
bridge.getWebView().requestFocus();
}
return super.dispatchTouchEvent(event);
}
private void loadIntentUrl() { private void loadIntentUrl() {
if (bridge == null || getIntent() == null) return; if (bridge == null || getIntent() == null) return;
String initialUrl = getIntent().getStringExtra(EXTRA_INITIAL_URL); String initialUrl = getIntent().getStringExtra(EXTRA_INITIAL_URL);
@@ -86,7 +99,10 @@ public abstract class ControllerBridgeActivity extends BridgeActivity {
"window.dispatchEvent(new CustomEvent('ashen-halls-native-controller'," "window.dispatchEvent(new CustomEvent('ashen-halls-native-controller',"
+ "{detail:{token:'" + token + "',repeat:" + repeat + "}}));"; + "{detail:{token:'" + token + "',repeat:" + repeat + "}}));";
bridge.getWebView().post( bridge.getWebView().post(
() -> bridge.getWebView().evaluateJavascript(script, null) () -> {
bridge.getWebView().requestFocus();
bridge.getWebView().evaluateJavascript(script, null);
}
); );
} }
return true; return true;
+13
View File
@@ -2782,6 +2782,19 @@ function updatePvpMatchState(session, matchId, payload) {
alive: Boolean(payload.alive), alive: Boolean(payload.alive),
elapsedTicks: Math.max(0, Math.floor(Number(payload.elapsedTicks) || 0)), elapsedTicks: Math.max(0, Math.floor(Number(payload.elapsedTicks) || 0)),
} }
const currentProgress = match.progress[side]
if (
currentProgress
&& (
progress.stage < currentProgress.stage
|| (
progress.stage === currentProgress.stage
&& progress.encounterIndex < currentProgress.encounterIndex
)
)
) {
return pvpSnapshot(match)
}
match.states[side] = payload.state ?? null match.states[side] = payload.state ?? null
match.statuses[side] = status match.statuses[side] = status
match.progress[side] = progress match.progress[side] = progress
+92 -6
View File
@@ -453,6 +453,7 @@ export function PvpStadiumScreen({
const { enabled: dualScreenEnabled } = useDualScreen() const { enabled: dualScreenEnabled } = useDualScreen()
const opponentLabel = liveMatch ? liveMatch.opponentName : `CPU ${cpuDifficulty ?? 1}` const opponentLabel = liveMatch ? liveMatch.opponentName : `CPU ${cpuDifficulty ?? 1}`
const playerAlive = playerSide.party.some((member) => member.health > 0) const playerAlive = playerSide.party.some((member) => member.health > 0)
const partyColumns = 3
const setSelectedTargetId = useCallback((id: string) => { const setSelectedTargetId = useCallback((id: string) => {
selectedIdRef.current = id selectedIdRef.current = id
@@ -841,6 +842,55 @@ export function PvpStadiumScreen({
if (succeeded) addLog(`${spell.name} cast on ${playerRef.current.party.find((member) => member.id === targetId)?.name ?? 'target'}.`, 'heal') if (succeeded) addLog(`${spell.name} cast on ${playerRef.current.party.find((member) => member.id === targetId)?.name ?? 'target'}.`, 'heal')
}, [addLog, applySpell, playerAlive, status]) }, [addLog, applySpell, playerAlive, status])
const selectRelativeTarget = useCallback((direction: -1 | 1) => {
const living = playerRef.current.party.filter((member) => member.health > 0)
if (living.length === 0) return
const currentIndex = living.findIndex((member) => member.id === selectedIdRef.current)
const nextIndex = currentIndex < 0
? 0
: (currentIndex + direction + living.length) % living.length
setSelectedTargetId(living[nextIndex].id)
}, [setSelectedTargetId])
const selectDirectionalTarget = useCallback((action: InputAction) => {
const currentIndex = playerRef.current.party.findIndex((member) => member.id === selectedIdRef.current)
if (currentIndex < 0) {
const firstLiving = playerRef.current.party.find((member) => member.health > 0)
if (firstLiving) setSelectedTargetId(firstLiving.id)
return
}
const currentRow = Math.floor(currentIndex / partyColumns)
const currentColumn = currentIndex % partyColumns
const candidates = playerRef.current.party
.map((member, index) => ({
member,
index,
row: Math.floor(index / partyColumns),
column: index % partyColumns,
}))
.filter(({ member, index, row, column }) => {
if (member.health <= 0 || index === currentIndex) return false
if (action === 'navigateLeft') return row === currentRow && column < currentColumn
if (action === 'navigateRight') return row === currentRow && column > currentColumn
if (action === 'navigateUp') return row < currentRow
return row > currentRow
})
.sort((a, b) => {
const horizontal = action === 'navigateLeft' || action === 'navigateRight'
const aPrimary = horizontal ? Math.abs(a.column - currentColumn) : Math.abs(a.row - currentRow)
const bPrimary = horizontal ? Math.abs(b.column - currentColumn) : Math.abs(b.row - currentRow)
const aSecondary = horizontal ? 0 : Math.abs(a.column - currentColumn)
const bSecondary = horizontal ? 0 : Math.abs(b.column - currentColumn)
return aPrimary - bPrimary || aSecondary - bSecondary
})
if (candidates[0]) setSelectedTargetId(candidates[0].member.id)
}, [partyColumns, setSelectedTargetId])
const selectDirectTarget = useCallback((slot: number) => {
const member = playerRef.current.party[slot]
if (member?.health > 0) setSelectedTargetId(member.id)
}, [setSelectedTargetId])
const cpuTakeTurn = useCallback(() => { const cpuTakeTurn = useCallback(() => {
if (!cpuDifficulty || status !== 'playing') return if (!cpuDifficulty || status !== 'playing') return
const behavior = CPU_BEHAVIOR[cpuDifficulty] const behavior = CPU_BEHAVIOR[cpuDifficulty]
@@ -1032,6 +1082,18 @@ export function PvpStadiumScreen({
setShopReady(false) setShopReady(false)
setShopPoints(0) setShopPoints(0)
addLog(`Round ${nextRound} starts. HP and mana restored.`, 'system') addLog(`Round ${nextRound} starts. HP and mana restored.`, 'system')
if (liveMatchRef.current) {
publishPvpMatchState<StadiumSideState>(liveMatchRef.current.id, {
state: nextPlayer,
status: 'playing',
stage: nextRound,
encounterIndex: nextRound,
encountersCleared: roundWins.player,
enemyHealth: 0,
alive: true,
elapsedTicks: 0,
}).catch(() => undefined)
}
beginRoundCountdown() beginRoundCountdown()
}, [addLog, beginRoundCountdown, cpuPartyTemplate, partyTemplate, roundIndex, roundWins.opponent, roundWins.player, setSelectedTargetId]) }, [addLog, beginRoundCountdown, cpuPartyTemplate, partyTemplate, roundIndex, roundWins.opponent, roundWins.player, setSelectedTargetId])
@@ -1086,7 +1148,7 @@ export function PvpStadiumScreen({
.then((snapshot) => { .then((snapshot) => {
if (stopped) return if (stopped) return
const opponentState = snapshot.states[liveMatch.opponentSide] const opponentState = snapshot.states[liveMatch.opponentSide]
if (opponentState) { if (opponentState && opponentState.roundIndex >= roundIndex) {
cpuRef.current = opponentState cpuRef.current = opponentState
setCpuSide(opponentState) setCpuSide(opponentState)
} }
@@ -1094,6 +1156,7 @@ export function PvpStadiumScreen({
if (opponentStatus === 'won' && status !== 'won' && status !== 'lost') setStatus('lost') if (opponentStatus === 'won' && status !== 'won' && status !== 'lost') setStatus('lost')
if (opponentStatus === 'lost' && status !== 'won' && status !== 'lost') setStatus('won') if (opponentStatus === 'lost' && status !== 'won' && status !== 'lost') setStatus('won')
if (!opponentState) return if (!opponentState) return
if (opponentState.roundIndex < roundIndex) return
if (status === 'playing' && opponentState.roundIndex === roundIndex && opponentState.roundStatus === 'shop' && opponentState.lastRoundOutcome) { if (status === 'playing' && opponentState.roundIndex === roundIndex && opponentState.roundStatus === 'shop' && opponentState.lastRoundOutcome) {
const key = `${roundIndex}-${opponentState.lastRoundOutcome}` const key = `${roundIndex}-${opponentState.lastRoundOutcome}`
if (loggedOpponentRoundRef.current === key) return if (loggedOpponentRoundRef.current === key) return
@@ -1102,7 +1165,14 @@ export function PvpStadiumScreen({
else if (opponentState.lastRoundOutcome === 'win') finishRound('loss') else if (opponentState.lastRoundOutcome === 'win') finishRound('loss')
else finishRound('tie') else finishRound('tie')
} }
if (status === 'shop' && shopReady && opponentState.roundIndex === roundIndex && opponentState.shopReady) { if (
status === 'shop'
&& shopReady
&& (
(opponentState.roundIndex === roundIndex && opponentState.shopReady)
|| opponentState.roundIndex > roundIndex
)
) {
startNextRound() startNextRound()
} }
}) })
@@ -1178,15 +1248,31 @@ export function PvpStadiumScreen({
window.requestAnimationFrame(() => focusFirstControl()) window.requestAnimationFrame(() => focusFirstControl())
}, [status]) }, [status])
useEffect(() => {
if (!paused) return
window.requestAnimationFrame(() => focusFirstControl())
}, [paused])
useGameAction((action) => { useGameAction((action) => {
if (action === 'pause') { if (action === 'pause' || action === 'back') {
if (status === 'playing') setPaused((value) => !value) if (status === 'playing') setPaused((value) => !value)
return return
} }
if (paused || status !== 'playing') return
if (action.startsWith('navigate')) {
selectDirectionalTarget(action)
return
}
if (action === 'previousTarget') {
selectRelativeTarget(-1)
return
}
if (action === 'nextTarget') {
selectRelativeTarget(1)
return
}
if (action.startsWith('targetParty')) { if (action.startsWith('targetParty')) {
const index = Number(action.slice('targetParty'.length)) - 1 selectDirectTarget(Number(action.slice('targetParty'.length)) - 1)
const member = playerRef.current.party[index]
if (member?.health > 0) setSelectedTargetId(member.id)
return return
} }
if (action.startsWith('ability')) { if (action.startsWith('ability')) {
+60 -17
View File
@@ -125,6 +125,9 @@ const STORAGE_KEY = 'ashen-halls-input-bindings-v1'
const PREFERENCES_STORAGE_KEY = 'ashen-halls-input-preferences-v1' const PREFERENCES_STORAGE_KEY = 'ashen-halls-input-preferences-v1'
const GAME_ACTION_EVENT = 'ashen-halls-game-action' const GAME_ACTION_EVENT = 'ashen-halls-game-action'
const NATIVE_CONTROLLER_EVENT = 'ashen-halls-native-controller' const NATIVE_CONTROLLER_EVENT = 'ashen-halls-native-controller'
const FOCUSABLE_SELECTOR = 'button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])'
let lastControllerFocus: HTMLElement | null = null
type CaptureState = { type CaptureState = {
device: InputDevice device: InputDevice
@@ -234,25 +237,41 @@ function focusableElements() {
).find(isVisible) ).find(isVisible)
const scope: ParentNode = keyboard ?? pauseMenu ?? dialog ?? document const scope: ParentNode = keyboard ?? pauseMenu ?? dialog ?? document
return Array.from( return Array.from(
scope.querySelectorAll<HTMLElement>( scope.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR),
'button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])',
),
).filter(isVisible) ).filter(isVisible)
} }
function rememberFocusableControl(element: HTMLElement) {
lastControllerFocus = element
}
function focusControl(element: HTMLElement) {
rememberFocusableControl(element)
element.focus({ preventScroll: true })
}
function currentFocusableControl(candidates = focusableElements()) {
const active = document.activeElement
if (active instanceof HTMLElement && candidates.includes(active)) {
rememberFocusableControl(active)
return active
}
if (lastControllerFocus && candidates.includes(lastControllerFocus) && isVisible(lastControllerFocus)) {
return lastControllerFocus
}
return null
}
export function focusFirstControl() { export function focusFirstControl() {
const first = focusableElements()[0] const first = focusableElements()[0]
first?.focus({ preventScroll: true }) if (first) focusControl(first)
return first return first
} }
function moveFocus(action: InputAction) { function moveFocus(action: InputAction) {
const candidates = focusableElements() const candidates = focusableElements()
if (candidates.length === 0) return if (candidates.length === 0) return
const current = document.activeElement instanceof HTMLElement const current = currentFocusableControl(candidates)
&& candidates.includes(document.activeElement)
? document.activeElement
: null
if (!current) { if (!current) {
focusFirstControl() focusFirstControl()
return return
@@ -279,7 +298,7 @@ function moveFocus(action: InputAction) {
const next = ranked[0]?.candidate const next = ranked[0]?.candidate
if (!next) return if (!next) return
next.focus({ preventScroll: true }) focusControl(next)
} }
function hasUiOverlay() { function hasUiOverlay() {
@@ -456,12 +475,12 @@ export function InputProvider({ children }: { children: ReactNode }) {
if (action.startsWith('navigate')) { if (action.startsWith('navigate')) {
if (uiOverlay || !combatActive) moveFocus(action) if (uiOverlay || !combatActive) moveFocus(action)
} else if (action === 'confirm') { } else if (action === 'confirm') {
const active = document.activeElement const active = currentFocusableControl()
if (isTextInput(active)) { if (isTextInput(active)) {
setKeyboardInput(active) setKeyboardInput(active)
window.requestAnimationFrame(() => focusFirstControl()) window.requestAnimationFrame(() => focusFirstControl())
} else if ( } else if (
active instanceof HTMLElement active
&& active.matches('button:not(:disabled), [role="button"]') && active.matches('button:not(:disabled), [role="button"]')
&& isVisible(active) && isVisible(active)
) { ) {
@@ -581,6 +600,29 @@ export function InputProvider({ children }: { children: ReactNode }) {
return () => window.removeEventListener('keydown', onKeyDown) return () => window.removeEventListener('keydown', onKeyDown)
}, [assignBinding, dispatchAction]) }, [assignBinding, dispatchAction])
useEffect(() => {
const onFocusIn = (event: FocusEvent) => {
const target = event.target
if (!(target instanceof HTMLElement)) return
if (!target.matches(FOCUSABLE_SELECTOR) || !isVisible(target)) return
rememberFocusableControl(target)
}
const onPointerDown = (event: PointerEvent) => {
document.documentElement.dataset.inputDevice = 'pc'
const target = event.target
if (!(target instanceof Element)) return
const control = target.closest<HTMLElement>(FOCUSABLE_SELECTOR)
if (!control || !isVisible(control)) return
rememberFocusableControl(control)
}
document.addEventListener('focusin', onFocusIn)
document.addEventListener('pointerdown', onPointerDown, { capture: true })
return () => {
document.removeEventListener('focusin', onFocusIn)
document.removeEventListener('pointerdown', onPointerDown, { capture: true })
}
}, [])
useEffect(() => { useEffect(() => {
const listener = (event: Event) => { const listener = (event: Event) => {
const detail = (event as CustomEvent<{ token: string; repeat?: boolean }>).detail const detail = (event as CustomEvent<{ token: string; repeat?: boolean }>).detail
@@ -595,16 +637,17 @@ export function InputProvider({ children }: { children: ReactNode }) {
const combatActive = document.querySelector('[data-combat-active="true"]') const combatActive = document.querySelector('[data-combat-active="true"]')
if (combatActive) return if (combatActive) return
const candidates = focusableElements() const candidates = focusableElements()
const active = document.activeElement const activeControl = currentFocusableControl(candidates)
const activeIsUsable = active instanceof HTMLElement
&& candidates.includes(active)
&& isVisible(active)
if ( if (
(!activeIsUsable || document.activeElement === document.body) (!activeControl || document.activeElement === document.body)
&& !keyboardInputRef.current && !keyboardInputRef.current
&& !captureRef.current && !captureRef.current
) { ) {
focusFirstControl() if (activeControl) {
focusControl(activeControl)
} else {
focusFirstControl()
}
} }
} }
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {