Android build v1.1.19

This commit is contained in:
Warren H
2026-07-04 20:50:33 -04:00
parent c052b086f8
commit bdae0007a1
16 changed files with 641 additions and 210 deletions
+77 -22
View File
@@ -1,5 +1,7 @@
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useGameAction } from '../../input'
import { getGameMode } from '../../gameRepository'
import { startPvpQueueWithCpuFallback } from '../../pvpQueueLifecycle'
import { summarizeChoiceStacks } from '../../combat/roguelikeUpgrades'
import {
useDualScreen,
@@ -54,6 +56,7 @@ const IWT2_MENU_COLUMNS = 4
const IWT2_ROGUELIKE_CHOICE_COUNT = 3
type Iwt2RoguelikeRunState = {
bossIds: Iwt2BossId[]
buffs: Iwt2RoguelikeSelfBuffId[]
contentType: Iwt2RoguelikeContentType
debuffs: Iwt2RoguelikeOpponentDebuffId[]
@@ -129,11 +132,17 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
const [roguelikeVariant, setRoguelikeVariant] = useState<Iwt2RoguelikeVariant>('pve')
const [roguelikeContentType, setRoguelikeContentType] = useState<Iwt2RoguelikeContentType>('dungeon')
const [roguelikeRun, setRoguelikeRun] = useState<Iwt2RoguelikeRunState | null>(null)
const [pvpQueueMessage, setPvpQueueMessage] = useState('')
const cancelPvpQueueRef = useRef<(() => void) | null>(null)
useEffect(() => {
writeIwt2Save(save)
}, [save])
useEffect(() => () => {
cancelPvpQueueRef.current?.()
}, [])
const setupDualScreenState = useMemo<DualScreenSetupState | null>(
() => buildIwt2SetupDualScreenState(screen, selectedBossId, save),
[save, screen, selectedBossId],
@@ -182,6 +191,7 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
return (
<BossArenaScreen
bossId={selectedBossId}
key={`arena-${selectedBossId}-${arenaModeLabel}`}
modeLabel={arenaModeLabel}
save={save}
onBack={() => setScreen('menu')}
@@ -193,7 +203,9 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
if (screen === 'roguelike-arena' && roguelikeRun) {
return (
<BossArenaScreen
bossIds={roguelikeRun.bossIds}
bossId={selectedBossId}
key={`roguelike-${roguelikeRun.variant}-${roguelikeRun.contentType}-${roguelikeRun.stage}-${roguelikeRun.bossIds.join('-')}`}
roguelikeRun={{
buffs: roguelikeRun.buffs,
contentType: roguelikeRun.contentType,
@@ -233,7 +245,7 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
onChoose={(buffId, debuffId) => {
const nextRun = applyRoguelikeChoice(roguelikeRun, buffId, debuffId, save)
setRoguelikeRun(nextRun)
setSelectedBossId(bossForRoguelike(nextRun.variant, nextRun.contentType, nextRun.stage))
setSelectedBossId(nextRun.bossIds[0] ?? 'bulldrome')
setScreen('roguelike-arena')
}}
/>
@@ -267,17 +279,18 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
onBack={() => setScreen('menu')}
onContentTypeChange={setRoguelikeContentType}
onStart={() => {
const nextRun = createRoguelikeRun(save, roguelikeVariant, roguelikeContentType)
setRoguelikeRun(nextRun)
setSelectedBossId(bossForRoguelike(roguelikeVariant, roguelikeContentType, nextRun.stage))
setScreen('roguelike-arena')
startIwt2RoguelikeRun()
}}
onVariantChange={(nextVariant) => {
cancelPvpQueueRef.current?.()
cancelPvpQueueRef.current = null
setPvpQueueMessage('')
setRoguelikeVariant(nextVariant)
if (nextVariant === 'pve' && roguelikeContentType === 'stadium') {
setRoguelikeContentType('dungeon')
}
}}
queueMessage={pvpQueueMessage}
/>
</main>
)
@@ -359,6 +372,7 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
<button
className={`iwt2-menu-card ${selectedIndex === index ? 'game-selected' : ''}`}
data-controller-nav="skip"
data-game-selected={selectedIndex === index ? 'true' : undefined}
key={`${item.screen}-${item.title}`}
onClick={() => openMenuItem(item)}
onPointerDown={() => setSelectedIndex(index)}
@@ -376,24 +390,58 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
)}
</main>
)
function startIwt2RoguelikeRun() {
cancelPvpQueueRef.current?.()
cancelPvpQueueRef.current = null
setPvpQueueMessage('')
if (roguelikeVariant !== 'pvp') {
beginIwt2RoguelikeArena(roguelikeVariant, roguelikeContentType)
return
}
const startStage = 1
cancelPvpQueueRef.current = startPvpQueueWithCpuFallback<unknown>({
contentType: roguelikeContentType,
startStage,
gameMode: getGameMode(),
liveMatchActive: () => false,
onSearching: setPvpQueueMessage,
onCpuMatch: (_difficulty, message) => {
cancelPvpQueueRef.current = null
setPvpQueueMessage(message)
beginIwt2RoguelikeArena('pvp', roguelikeContentType)
},
onLiveMatch: (...liveMatchArgs) => {
const message = liveMatchArgs[2]
cancelPvpQueueRef.current = null
setPvpQueueMessage(message)
beginIwt2RoguelikeArena('pvp', roguelikeContentType)
},
messages: {
offline: (difficulty) => `Offline mode. CPU ${difficulty} enters IWT2 ${formatRoguelikeContentType(roguelikeContentType)}.`,
searching: `Searching IWT2 ${formatRoguelikeContentType(roguelikeContentType)} queue for 5s.`,
notFound: (difficulty) => `No IWT2 opponent found after 5s. CPU ${difficulty} steps in.`,
unavailable: (difficulty) => `PvP server unavailable. CPU ${difficulty} steps in.`,
liveFound: () => `Opponent found. Starting IWT2 ${formatRoguelikeContentType(roguelikeContentType)} race.`,
},
})
}
function beginIwt2RoguelikeArena(
variant: Iwt2RoguelikeVariant,
contentType: Iwt2RoguelikeContentType,
) {
const nextRun = createRoguelikeRun(save, variant, contentType)
setRoguelikeRun(nextRun)
setSelectedBossId(nextRun.bossIds[0] ?? 'bulldrome')
setScreen('roguelike-arena')
}
}
function bossForRoguelike(
variant: Iwt2RoguelikeVariant,
contentType: Iwt2RoguelikeContentType,
stage = 1,
): Iwt2BossId {
const dungeonBosses: Iwt2BossId[] = variant === 'pvp'
? ['yian-kut-ku', 'great-jaggi', 'bulldrome', 'khezu']
: ['bulldrome', 'yian-kut-ku', 'great-jaggi', 'khezu']
const raidBosses: Iwt2BossId[] = ['khezu', 'great-jaggi', 'yian-kut-ku', 'bulldrome']
const stadiumBosses: Iwt2BossId[] = ['great-jaggi', 'yian-kut-ku', 'khezu', 'bulldrome']
const pool = contentType === 'raid'
? raidBosses
: contentType === 'stadium'
? stadiumBosses
: dungeonBosses
return pool[(stage - 1) % pool.length]
function formatRoguelikeContentType(contentType: Iwt2RoguelikeContentType) {
if (contentType === 'raid') return 'Raid'
if (contentType === 'stadium') return 'Stadium'
return 'Dungeon'
}
function createRoguelikeRun(
@@ -402,6 +450,7 @@ function createRoguelikeRun(
contentType: Iwt2RoguelikeContentType,
): Iwt2RoguelikeRunState {
return {
bossIds: createRandomRoguelikeBossPair(),
buffs: [],
contentType,
debuffs: [],
@@ -462,11 +511,17 @@ function applyRoguelikeChoice(
return {
...run,
...nextBase,
bossIds: createRandomRoguelikeBossPair(),
...buildRoguelikeChoices(save, run.variant),
stage: run.stage + 1,
}
}
function createRandomRoguelikeBossPair(): Iwt2BossId[] {
const pool = Object.keys(IWT2_BOSS_METADATA) as Iwt2BossId[]
return chooseRunChoices(pool, 2)
}
function chooseRunChoices<T>(items: readonly T[], count: number): T[] {
const pool = [...items]
const choices: T[] = []