Android build v1.1.16

This commit is contained in:
Warren H
2026-07-04 17:41:45 -04:00
parent 99e97e5208
commit 0d269a6041
20 changed files with 2304 additions and 307 deletions
+256 -19
View File
@@ -1,12 +1,15 @@
import { useEffect, useState } from 'react'
import { useGameAction } from '../../input'
import { summarizeChoiceStacks } from '../../combat/roguelikeUpgrades'
import { BossArenaScreen } from './screens/BossArenaScreen'
import {
Iwt2CloudSaveScreen,
Iwt2CustomizeCharacterScreen,
Iwt2DungeonsScreen,
Iwt2HunterProfileScreen,
Iwt2ModePlaceholderScreen,
Iwt2ModeScreen,
Iwt2RoguelikeScreen,
Iwt2RoguelikeUpgradeScreen,
Iwt2SettingsScreen,
} from './screens/Iwt2ShellScreens'
import {
@@ -15,6 +18,17 @@ import {
type Iwt2Save,
} from './save/iwt2Repository'
import type { Iwt2BossId } from './content/bosses'
import { abilitiesForHealer } from './content/healerAbilities'
import {
buildIwt2OpponentDebuffChoices,
buildIwt2SelfBuffChoices,
IWT2_REVIVE_PARTY_CHOICE,
type Iwt2RoguelikeChoice,
type Iwt2RoguelikeContentType,
type Iwt2RoguelikeOpponentDebuffId,
type Iwt2RoguelikeSelfBuffId,
type Iwt2RoguelikeVariant,
} from './content/roguelike'
type Iwt2Screen =
| 'menu'
@@ -23,12 +37,24 @@ type Iwt2Screen =
| 'dungeons'
| 'raids'
| 'roguelike'
| 'pvp'
| 'roguelike-arena'
| 'roguelike-upgrade'
| 'hunter-profile'
| 'customize-character'
| 'settings'
const IWT2_MENU_COLUMNS = 4
const IWT2_ROGUELIKE_CHOICE_COUNT = 3
type Iwt2RoguelikeRunState = {
buffs: Iwt2RoguelikeSelfBuffId[]
contentType: Iwt2RoguelikeContentType
debuffs: Iwt2RoguelikeOpponentDebuffId[]
debuffChoices: Array<Iwt2RoguelikeChoice<Iwt2RoguelikeOpponentDebuffId>>
selfChoices: Array<Iwt2RoguelikeChoice<Iwt2RoguelikeSelfBuffId>>
stage: number
variant: Iwt2RoguelikeVariant
}
const MENU_ITEMS: Array<{
screen: Iwt2Screen
@@ -38,32 +64,32 @@ const MENU_ITEMS: Array<{
}> = [
{
screen: 'cloud-save',
title: 'Cloud Save',
description: 'Account sync shell for IWT2 progression and local save status.',
title: 'Backup Slot',
description: 'Save or restore the isolated IWT2 progress slot.',
glyph: 'C',
},
{
screen: 'dungeons',
title: 'Dungeons',
description: 'Queue into IWT2 boss arenas. Bulldrome and Yian Kut Ku are playable slices.',
description: 'Queue into Bulldrome, Yian Kut Ku, Great Jaggi, and Khezu boss arenas.',
glyph: 'D',
},
{
screen: 'raids',
title: 'Raids',
description: 'Large-party encounter shell for future multi-group boss fights.',
description: 'Open raid assignments built from active IWT2 boss mechanics.',
glyph: 'R',
},
{
screen: 'roguelike',
title: 'Roguelike',
description: 'Run-based IWT2 progression shell for room chains and reward drafts.',
glyph: 'G',
description: 'Draft upgrades through escalating random encounters.',
glyph: 'L',
},
{
screen: 'pvp',
screen: 'roguelike',
title: 'PvP',
description: 'Competitive healing shell with controller targeting preserved.',
description: 'Race another healer through roguelike encounters with buffs and sabotage.',
glyph: 'P',
},
{
@@ -75,7 +101,7 @@ const MENU_ITEMS: Array<{
{
screen: 'customize-character',
title: 'Customize Character',
description: 'IWT2-only character identity and cosmetic setup shell.',
description: 'Choose healer kit, armor palette, and IWT2 hunter callsign.',
glyph: 'K',
},
{
@@ -91,6 +117,10 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
const [save, setSave] = useState<Iwt2Save>(loadIwt2Save)
const [selectedIndex, setSelectedIndex] = useState(0)
const [selectedBossId, setSelectedBossId] = useState<Iwt2BossId>('bulldrome')
const [arenaModeLabel, setArenaModeLabel] = useState('Dungeon')
const [roguelikeVariant, setRoguelikeVariant] = useState<Iwt2RoguelikeVariant>('pve')
const [roguelikeContentType, setRoguelikeContentType] = useState<Iwt2RoguelikeContentType>('dungeon')
const [roguelikeRun, setRoguelikeRun] = useState<Iwt2RoguelikeRunState | null>(null)
useEffect(() => {
writeIwt2Save(save)
@@ -103,7 +133,7 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
return
}
if (action === 'confirm') {
setScreen(MENU_ITEMS[selectedIndex].screen)
openMenuItem(MENU_ITEMS[selectedIndex])
return
}
if (action === 'navigateUp' || action === 'navigateLeft') {
@@ -115,10 +145,24 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
}
})
function openMenuItem(item: (typeof MENU_ITEMS)[number]) {
if (item.title === 'PvP') {
setRoguelikeVariant('pvp')
setScreen('roguelike')
return
}
if (item.title === 'Roguelike') {
setRoguelikeVariant('pve')
setRoguelikeContentType((current) => current === 'stadium' ? 'dungeon' : current)
}
setScreen(item.screen)
}
if (screen === 'arena') {
return (
<BossArenaScreen
bossId={selectedBossId}
modeLabel={arenaModeLabel}
save={save}
onBack={() => setScreen('menu')}
onSaveUpdated={setSave}
@@ -126,6 +170,57 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
)
}
if (screen === 'roguelike-arena' && roguelikeRun) {
return (
<BossArenaScreen
bossId={selectedBossId}
roguelikeRun={{
buffs: roguelikeRun.buffs,
contentType: roguelikeRun.contentType,
debuffs: roguelikeRun.debuffs,
onVictory: () => {
setRoguelikeRun((current) => current
? {
...current,
...buildRoguelikeChoices(save, current.variant),
}
: current)
setScreen('roguelike-upgrade')
},
stage: roguelikeRun.stage,
variant: roguelikeRun.variant,
}}
save={save}
onBack={() => setScreen('roguelike')}
onSaveUpdated={setSave}
/>
)
}
if (screen === 'roguelike-upgrade' && roguelikeRun) {
return (
<main className="game-shell iwt2-shell">
<Iwt2Header save={save} onBackToGameSelect={onBackToGameSelect} />
<Iwt2RoguelikeUpgradeScreen
activeBuffSummary={summarizeIwt2Buffs(save, roguelikeRun.buffs)}
activeDebuffSummary={summarizeIwt2Debuffs(save, roguelikeRun.debuffs)}
contentType={roguelikeRun.contentType}
debuffChoices={roguelikeRun.debuffChoices}
selfChoices={roguelikeRun.selfChoices}
stage={roguelikeRun.stage}
variant={roguelikeRun.variant}
onBack={() => setScreen('roguelike')}
onChoose={(buffId, debuffId) => {
const nextRun = applyRoguelikeChoice(roguelikeRun, buffId, debuffId, save)
setRoguelikeRun(nextRun)
setSelectedBossId(bossForRoguelike(nextRun.variant, nextRun.contentType, nextRun.stage))
setScreen('roguelike-arena')
}}
/>
</main>
)
}
if (screen === 'dungeons') {
return (
<main className="game-shell iwt2-shell">
@@ -133,6 +228,7 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
<Iwt2DungeonsScreen
onBack={() => setScreen('menu')}
onOpenBoss={(bossId) => {
setArenaModeLabel('Dungeon')
setSelectedBossId(bossId)
setScreen('arena')
}}
@@ -141,12 +237,45 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
)
}
if (screen === 'raids' || screen === 'roguelike' || screen === 'pvp') {
const modeName = screen === 'raids' ? 'Raids' : screen === 'roguelike' ? 'Roguelike' : 'PvP'
if (screen === 'roguelike') {
return (
<main className="game-shell iwt2-shell">
<Iwt2Header save={save} onBackToGameSelect={onBackToGameSelect} />
<Iwt2ModePlaceholderScreen mode={modeName} onBack={() => setScreen('menu')} />
<Iwt2RoguelikeScreen
contentType={roguelikeContentType}
variant={roguelikeVariant}
onBack={() => setScreen('menu')}
onContentTypeChange={setRoguelikeContentType}
onStart={() => {
const nextRun = createRoguelikeRun(save, roguelikeVariant, roguelikeContentType)
setRoguelikeRun(nextRun)
setSelectedBossId(bossForRoguelike(roguelikeVariant, roguelikeContentType, nextRun.stage))
setScreen('roguelike-arena')
}}
onVariantChange={(nextVariant) => {
setRoguelikeVariant(nextVariant)
if (nextVariant === 'pve' && roguelikeContentType === 'stadium') {
setRoguelikeContentType('dungeon')
}
}}
/>
</main>
)
}
if (screen === 'raids') {
return (
<main className="game-shell iwt2-shell">
<Iwt2Header save={save} onBackToGameSelect={onBackToGameSelect} />
<Iwt2ModeScreen
mode="Raids"
onBack={() => setScreen('menu')}
onOpenBoss={(bossId) => {
setArenaModeLabel('Raid')
setSelectedBossId(bossId)
setScreen('arena')
}}
/>
</main>
)
}
@@ -164,7 +293,11 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
return (
<main className="game-shell iwt2-shell">
<Iwt2Header save={save} onBackToGameSelect={onBackToGameSelect} />
<Iwt2CustomizeCharacterScreen save={save} onBack={() => setScreen('menu')} />
<Iwt2CustomizeCharacterScreen
save={save}
onBack={() => setScreen('menu')}
onSaveUpdated={setSave}
/>
</main>
)
}
@@ -173,7 +306,11 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
return (
<main className="game-shell iwt2-shell">
<Iwt2Header save={save} onBackToGameSelect={onBackToGameSelect} />
<Iwt2CloudSaveScreen save={save} onBack={() => setScreen('menu')} />
<Iwt2CloudSaveScreen
save={save}
onBack={() => setScreen('menu')}
onSaveUpdated={setSave}
/>
</main>
)
}
@@ -202,8 +339,8 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
<button
className={`iwt2-menu-card ${selectedIndex === index ? 'game-selected' : ''}`}
data-controller-nav="skip"
key={item.screen}
onClick={() => setScreen(item.screen)}
key={`${item.screen}-${item.title}`}
onClick={() => openMenuItem(item)}
onPointerDown={() => setSelectedIndex(index)}
type="button"
>
@@ -221,6 +358,106 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
)
}
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 createRoguelikeRun(
save: Iwt2Save,
variant: Iwt2RoguelikeVariant,
contentType: Iwt2RoguelikeContentType,
): Iwt2RoguelikeRunState {
return {
buffs: [],
contentType,
debuffs: [],
...buildRoguelikeChoices(save, variant),
stage: 1,
variant,
}
}
function buildRoguelikeChoices(save: Iwt2Save, variant: Iwt2RoguelikeVariant) {
const abilities = abilitiesForHealer(save.character.healerStyle)
const selfCatalog = [IWT2_REVIVE_PARTY_CHOICE, ...buildIwt2SelfBuffChoices(abilities)]
const debuffCatalog = buildIwt2OpponentDebuffChoices(abilities)
return {
selfChoices: chooseRunChoices(selfCatalog, IWT2_ROGUELIKE_CHOICE_COUNT),
debuffChoices: variant === 'pvp'
? chooseRunChoices(debuffCatalog, IWT2_ROGUELIKE_CHOICE_COUNT)
: [],
}
}
function summarizeIwt2Buffs(save: Iwt2Save, buffs: Iwt2RoguelikeSelfBuffId[]) {
if (buffs.length === 0) return ''
const abilities = abilitiesForHealer(save.character.healerStyle)
return summarizeChoiceStacks(
buffs,
[IWT2_REVIVE_PARTY_CHOICE, ...buildIwt2SelfBuffChoices(abilities)],
'None',
)
}
function summarizeIwt2Debuffs(save: Iwt2Save, debuffs: Iwt2RoguelikeOpponentDebuffId[]) {
if (debuffs.length === 0) return ''
const abilities = abilitiesForHealer(save.character.healerStyle)
return summarizeChoiceStacks(
debuffs,
buildIwt2OpponentDebuffChoices(abilities),
'None',
)
}
function applyRoguelikeChoice(
run: Iwt2RoguelikeRunState,
buffId: Iwt2RoguelikeSelfBuffId,
debuffId: Iwt2RoguelikeOpponentDebuffId | undefined,
save: Iwt2Save,
): Iwt2RoguelikeRunState {
const nextDebuffs = debuffId ? [...run.debuffs, debuffId] : run.debuffs
const nextBase = buffId === IWT2_REVIVE_PARTY_CHOICE.id
? {
buffs: run.buffs,
debuffs: nextDebuffs.slice(1),
}
: {
buffs: [...run.buffs, buffId],
debuffs: nextDebuffs,
}
return {
...run,
...nextBase,
...buildRoguelikeChoices(save, run.variant),
stage: run.stage + 1,
}
}
function chooseRunChoices<T>(items: readonly T[], count: number): T[] {
const pool = [...items]
const choices: T[] = []
while (pool.length > 0 && choices.length < count) {
const index = Math.floor(Math.random() * pool.length)
const [choice] = pool.splice(index, 1)
if (choice) choices.push(choice)
}
return choices
}
function Iwt2Header({
onBackToGameSelect,
save,