diff --git a/IWantToHeal-Thor-v1.0.25.apk b/IWantToHeal-Thor-v1.0.25.apk new file mode 100644 index 0000000..907dfb2 Binary files /dev/null and b/IWantToHeal-Thor-v1.0.25.apk differ diff --git a/android/app/build.gradle b/android/app/build.gradle index 720a83e..7b98a08 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.warren.iwanttoheal" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 41 - versionName "1.0.24" + versionCode 42 + versionName "1.0.25" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/src/App.tsx b/src/App.tsx index aba9ce6..48b8f44 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -131,6 +131,13 @@ function App() { }) }, [screen]) + useEffect(() => { + if (!authChecked || !account || !profile || screen === 'combat') return + window.requestAnimationFrame(() => { + focusFirstControl() + }) + }, [account, authChecked, profile, screen]) + useEffect(() => { window.localStorage.setItem(LAST_DIFFICULTY_KEY, String(selectedDifficultyId)) }, [selectedDifficultyId]) @@ -148,6 +155,9 @@ function App() { setScreen('menu') setError('') setServerMessage('') + window.requestAnimationFrame(() => { + focusFirstControl() + }) } async function signOut() { diff --git a/src/components/CombatScreen.tsx b/src/components/CombatScreen.tsx index 66fcea2..68490f6 100644 --- a/src/components/CombatScreen.tsx +++ b/src/components/CombatScreen.tsx @@ -374,6 +374,7 @@ export function CombatScreen({ const partyRef = useRef(partyTemplate) const enemyHealthRef = useRef(encounters[initialEncounterIndex].maxHealth) const elapsedTicksRef = useRef(0) + const selectedIdRef = useRef(partyTemplate[0].id) const encounter = encounters[encounterIndex] const currentPart = getCurrentPart(encounterIndex) const firstEncounterIndex = (startPart - 1) * 3 @@ -415,6 +416,11 @@ export function CombatScreen({ }) }, [paused]) + const setSelectedTargetId = useCallback((id: string) => { + selectedIdRef.current = id + setSelectedId(id) + }, []) + const addLog = useCallback((text: string, tone: CombatLogEntry['tone']) => { const entry = { id: nextLogId.current++, text, tone } setLog((current) => [entry, ...current].slice(0, 60)) @@ -467,7 +473,7 @@ export function CombatScreen({ if (roguelikeMode) setRoguelikeEncounters(nextRoguelikeEncounters) setRoguelikeStage(1) setParty(freshParty) - setSelectedId(partyTemplate[0].id) + setSelectedTargetId(partyTemplate[0].id) setResource(maxResource) setEncounterIndex(initialEncounterIndex) setEnemyHealth(nextEncounters[initialEncounterIndex].maxHealth) @@ -494,7 +500,7 @@ export function CombatScreen({ runStartedAtRef.current = Date.now() partStartTimesRef.current = { [startPart]: runStartedAtRef.current } setLog([{ id: nextLogId.current++, text: 'A new run begins.', tone: 'system' }]) - }, [difficulty, initialEncounterIndex, maxResource, partyTemplate, roguelikeMode, roguelikePool, startPart, staticEncounters]) + }, [difficulty, initialEncounterIndex, maxResource, partyTemplate, roguelikeMode, roguelikePool, setSelectedTargetId, startPart, staticEncounters]) const castSpell = useCallback( (spell: Spell) => { @@ -502,26 +508,27 @@ export function CombatScreen({ if (status !== 'playing' || cooldowns[spell.id] > 0 || resource < effectiveCost) return const healer = partyRef.current.find((member) => member.id === 'mira') if (!healer || healer.health <= 0) return - const selected = partyRef.current.find((member) => member.id === selectedId) + const targetId = selectedIdRef.current + const selected = partyRef.current.find((member) => member.id === targetId) if (!selected || selected.health <= 0) return const extraTarget = (blockedIds: string[]) => partyRef.current .filter((member) => member.health > 0 && !blockedIds.includes(member.id)) .sort((left, right) => (left.health / left.maxHealth) - (right.health / right.maxHealth))[0] - const directTargets = new Set([selectedId]) + const directTargets = new Set([targetId]) const hotTargets = new Set() const shieldTargets = new Set() - if (spell.kind === 'hot') hotTargets.add(selectedId) - if (spell.kind === 'shield') shieldTargets.add(selectedId) + if (spell.kind === 'hot') hotTargets.add(targetId) + if (spell.kind === 'shield') shieldTargets.add(targetId) if (spell.name === 'Mend' && activeSetEffects.has('mend_extra_target')) { - const extra = extraTarget([selectedId]) + const extra = extraTarget([targetId]) if (extra) directTargets.add(extra.id) } if (spell.name === 'Renew' && activeSetEffects.has('renew_extra_target')) { - const extra = extraTarget([selectedId]) + const extra = extraTarget([targetId]) if (extra) hotTargets.add(extra.id) } if (spell.name === 'Mend' && activeSetEffects.has('mend_applies_renew')) { - hotTargets.add(selectedId) + hotTargets.add(targetId) } const extraTargets = upgradeStackCount(roguelikeUpgrades, `slot${spell.key as SlotKey}-extra-target` as RoguelikeUpgradeId) for (let index = 0; index < extraTargets; index += 1) { @@ -599,7 +606,7 @@ export function CombatScreen({ setParty(nextParty) addLog(`${spell.name} cast on ${spell.kind === 'group' ? 'the party' : selected.name}${effectiveCost === 0 ? ' for free' : ''}.`, 'heal') }, - [activeSetEffects, addFloatingHeal, addLog, cooldowns, freeCastReady, resource, roguelikeUpgrades, selectedId, status], + [activeSetEffects, addFloatingHeal, addLog, cooldowns, freeCastReady, resource, roguelikeUpgrades, status], ) const finishRun = useCallback( @@ -664,18 +671,18 @@ export function CombatScreen({ const selectRelativeTarget = useCallback((direction: -1 | 1) => { const living = partyRef.current.filter((member) => member.health > 0) if (living.length === 0) return - const currentIndex = living.findIndex((member) => member.id === selectedId) + const currentIndex = living.findIndex((member) => member.id === selectedIdRef.current) const nextIndex = currentIndex < 0 ? 0 : (currentIndex + direction + living.length) % living.length - setSelectedId(living[nextIndex].id) - }, [selectedId]) + setSelectedTargetId(living[nextIndex].id) + }, [setSelectedTargetId]) const selectDirectionalTarget = useCallback((action: InputAction) => { const columns = dungeon.partySize >= 10 ? 6 : 3 - const currentIndex = partyRef.current.findIndex((member) => member.id === selectedId) + const currentIndex = partyRef.current.findIndex((member) => member.id === selectedIdRef.current) if (currentIndex < 0) { - setSelectedId(partyRef.current[0].id) + setSelectedTargetId(partyRef.current[0].id) return } const currentRow = Math.floor(currentIndex / columns) @@ -709,14 +716,14 @@ export function CombatScreen({ : Math.abs(b.column - currentColumn) return aPrimary - bPrimary || aSecondary - bSecondary }) - if (candidates[0]) setSelectedId(candidates[0].member.id) - }, [dungeon.partySize, selectedId]) + if (candidates[0]) setSelectedTargetId(candidates[0].member.id) + }, [dungeon.partySize, setSelectedTargetId]) const selectDirectTarget = useCallback((slot: number) => { const index = slot + (dungeon.partySize > 6 ? targetGroup * 6 : 0) const member = partyRef.current[index] - if (member) setSelectedId(member.id) - }, [dungeon.partySize, targetGroup]) + if (member) setSelectedTargetId(member.id) + }, [dungeon.partySize, setSelectedTargetId, targetGroup]) const chooseRoguelikeUpgrade = useCallback((upgrade: RoguelikeUpgrade) => { if (!roguelikeMode) return @@ -778,9 +785,9 @@ export function CombatScreen({ setTargetGroup((current) => { const groupCount = Math.max(1, Math.ceil(partyRef.current.length / 6)) const next = ((current + 1) % groupCount) as 0 | 1 | 2 - const selectedIndex = partyRef.current.findIndex((member) => member.id === selectedId) + const selectedIndex = partyRef.current.findIndex((member) => member.id === selectedIdRef.current) const nextMember = partyRef.current[(selectedIndex < 0 ? 0 : selectedIndex % 6) + next * 6] - if (nextMember) setSelectedId(nextMember.id) + if (nextMember) setSelectedTargetId(nextMember.id) return next }) return @@ -1134,7 +1141,7 @@ export function CombatScreen({