Android build v1.1.17

This commit is contained in:
Warren H
2026-07-04 18:07:50 -04:00
parent 0d269a6041
commit c2a14bac7d
32 changed files with 598 additions and 6 deletions
+110 -3
View File
@@ -1,6 +1,13 @@
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useGameAction } from '../../input'
import { summarizeChoiceStacks } from '../../combat/roguelikeUpgrades'
import {
useDualScreen,
useDualScreenSetupPublisher,
useDualScreenWorkshopPublisher,
type DualScreenSetupState,
type DualScreenWorkshopState,
} from '../../dualScreen'
import { BossArenaScreen } from './screens/BossArenaScreen'
import {
Iwt2CloudSaveScreen,
@@ -17,8 +24,8 @@ import {
writeIwt2Save,
type Iwt2Save,
} from './save/iwt2Repository'
import type { Iwt2BossId } from './content/bosses'
import { abilitiesForHealer } from './content/healerAbilities'
import { IWT2_BOSS_METADATA, type Iwt2BossId } from './content/bosses'
import { abilitiesForHealer, IWT2_HEALER_METADATA } from './content/healerAbilities'
import {
buildIwt2OpponentDebuffChoices,
buildIwt2SelfBuffChoices,
@@ -113,6 +120,7 @@ const MENU_ITEMS: Array<{
]
export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: () => void }) {
const { enabled: dualScreenEnabled } = useDualScreen()
const [screen, setScreen] = useState<Iwt2Screen>('menu')
const [save, setSave] = useState<Iwt2Save>(loadIwt2Save)
const [selectedIndex, setSelectedIndex] = useState(0)
@@ -126,6 +134,18 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
writeIwt2Save(save)
}, [save])
const setupDualScreenState = useMemo<DualScreenSetupState | null>(
() => buildIwt2SetupDualScreenState(screen, selectedBossId, save),
[save, screen, selectedBossId],
)
const workshopDualScreenState = useMemo<DualScreenWorkshopState | null>(
() => buildIwt2WorkshopDualScreenState(screen, save),
[save, screen],
)
useDualScreenSetupPublisher(setupDualScreenState, dualScreenEnabled)
useDualScreenWorkshopPublisher(workshopDualScreenState, dualScreenEnabled)
useGameAction((action, device) => {
if (screen !== 'menu' || device !== 'controller') return
if (action === 'back') {
@@ -478,3 +498,90 @@ function Iwt2Header({
</header>
)
}
function buildIwt2SetupDualScreenState(
screen: Iwt2Screen,
selectedBossId: Iwt2BossId,
save: Iwt2Save,
): DualScreenSetupState | null {
if (screen !== 'dungeons' && screen !== 'raids') return null
const boss = IWT2_BOSS_METADATA[selectedBossId]
const raid = screen === 'raids'
return {
contentType: raid ? 'raid' : 'dungeon',
description: raid
? `${boss.name} raid assignment. Tank holds aggro while party moves around modular boss mechanics.`
: `${boss.name} arena. Heal the party through melee pressure, telegraphs, hazards, and stun recovery.`,
difficultyName: `IWT2 Level ${save.character.level}`,
experience: 125,
initials: boss.icon,
itemLevel: save.character.level,
stats: {
damage: `${boss.meleeDamage}`,
health: `${boss.maxHealth}`,
loot: 'IWT2',
xp: '125',
},
subtitle: `${raid ? 'Raid' : 'Dungeon'} | 6 Players | ${IWT2_HEALER_METADATA[save.character.healerStyle].name}`,
title: raid ? `${boss.name} Raid` : `${boss.name} Arena`,
}
}
function buildIwt2WorkshopDualScreenState(
screen: Iwt2Screen,
save: Iwt2Save,
): DualScreenWorkshopState | null {
if (screen === 'hunter-profile') {
return {
items: [
{
glyph: 'H',
meta: `${save.character.experience} XP`,
status: `Level ${save.character.level}`,
title: save.character.name,
},
{
glyph: IWT2_HEALER_METADATA[save.character.healerStyle].icon,
meta: IWT2_HEALER_METADATA[save.character.healerStyle].description,
status: 'Class',
title: IWT2_HEALER_METADATA[save.character.healerStyle].name,
},
...(Object.keys(IWT2_BOSS_METADATA) as Iwt2BossId[]).map((bossId) => ({
glyph: IWT2_BOSS_METADATA[bossId].icon,
meta: `${save.collectionLog.bossKills[bossId] ?? 0} kills`,
status: `${save.collectionLog.dropsFound[bossDropIdForAppSummary(bossId)] ?? 0} drops`,
title: IWT2_BOSS_METADATA[bossId].name,
})),
],
mode: 'collection',
subtitle: 'IWT2 inventory and collection log',
summary: `${save.inventory.length} inventory slots`,
title: 'Hunter Profile',
}
}
if (screen === 'customize-character') {
const healer = IWT2_HEALER_METADATA[save.character.healerStyle]
return {
items: abilitiesForHealer(save.character.healerStyle).map((ability) => ({
glyph: ability.icon,
meta: `${ability.manaCost} Mana | ${ability.cooldownSeconds}s cooldown`,
status: `Slot ${ability.slot}`,
title: ability.name,
})),
mode: 'class',
subtitle: healer.description,
summary: healer.name,
title: 'Customize Character',
}
}
return null
}
function bossDropIdForAppSummary(bossId: Iwt2BossId): string {
if (bossId === 'yian-kut-ku') return 'yian-kut-ku-scale'
if (bossId === 'great-jaggi') return 'great-jaggi-hide'
if (bossId === 'khezu') return 'khezu-pearl'
return 'raw-bulldrome-coin'
}
+141
View File
@@ -1,6 +1,12 @@
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react'
import type { MovementVector } from '../../../input'
import { useGameAction, useInput, useMovementVectorRef } from '../../../input'
import {
useDualScreen,
useDualScreenPublisher,
type DualScreenCombatState,
} from '../../../dualScreen'
import type { PartyMember, Role, Spell } from '../../../game'
import {
createInitialIwt2ArenaState,
tickIwt2Arena,
@@ -16,6 +22,7 @@ import {
import { AbilityBar } from '../components/AbilityBar'
import { BossHud } from '../components/BossHud'
import { PartyFrames } from '../components/PartyFrames'
import { IWT2_CLASS_METADATA } from '../content/classes'
import { IWT2_ABILITY_ACTIONS, IWT2_TARGET_ACTIONS } from '../content/controls'
import { abilitiesForHealer, type Iwt2HealerAbility } from '../content/healerAbilities'
import { IWT2_BOSS_METADATA, type Iwt2BossId } from '../content/bosses'
@@ -77,6 +84,7 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
directPartyTargeting,
lastDevice,
} = useInput()
const { enabled: dualScreenEnabled } = useDualScreen()
const activeBindings = bindings[lastDevice]
useEffect(() => {
@@ -292,6 +300,36 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
: roguelikeRun
? 'Leave Roguelike'
: `Leave ${modeLabel ?? 'Arena'}`
const dualScreenState = useMemo(
() => buildIwt2DualScreenCombatState({
abilities,
arenaState,
bindings: activeBindings,
bossMetadata,
cooldowns: abilityCooldowns,
controllerIconStyle,
directPartyTargeting,
modeLabel: modeLabel ?? 'Arena',
playerMana,
selectedPartyId,
status,
}),
[
abilities,
activeBindings,
arenaState,
abilityCooldowns,
bossMetadata,
controllerIconStyle,
directPartyTargeting,
modeLabel,
playerMana,
selectedPartyId,
status,
],
)
useDualScreenPublisher(dualScreenState, dualScreenEnabled)
return (
<main
@@ -507,3 +545,106 @@ function arenaPauseTitle(
}
return roguelikeRun.contentType === 'raid' ? 'Raid Roguelike' : 'Dungeon Roguelike'
}
function buildIwt2DualScreenCombatState({
abilities,
arenaState,
bindings,
bossMetadata,
cooldowns,
controllerIconStyle,
directPartyTargeting,
modeLabel,
playerMana,
selectedPartyId,
status,
}: {
abilities: Iwt2HealerAbility[]
arenaState: Iwt2ArenaState
bindings: DualScreenCombatState['bindings']
bossMetadata: (typeof IWT2_BOSS_METADATA)[Iwt2BossId]
cooldowns: Record<string, number>
controllerIconStyle: DualScreenCombatState['controllerIconStyle']
directPartyTargeting: boolean
modeLabel: string
playerMana: number
selectedPartyId: Iwt2EntityId
status: ArenaStatus
}): DualScreenCombatState {
return {
bindings,
contentName: modeLabel,
controllerIconStyle,
difficultyName: 'IWT2',
directPartyTargeting,
dungeonName: `${bossMetadata.name} Arena`,
encounterCount: 1,
encounterDescription: arenaState.boss.attackPhase,
encounterHealth: arenaState.boss.health,
encounterIndex: 0,
encounterIsBoss: true,
encounterMaxHealth: arenaState.boss.maxHealth,
encounterName: bossMetadata.name,
floatingTexts: [],
maxResource: 100,
party: arenaState.party.map(toDualScreenPartyMember),
partySize: arenaState.party.length,
paused: status === 'paused',
playerIsAlive: (arenaState.party.find((member) => member.id === 'player-healer')?.health ?? 0) > 0,
resource: playerMana,
resourceName: 'Mana',
selectedId: selectedPartyId,
speedMultiplier: 1,
spells: abilities.map((ability, slotIndex) => toDualScreenSpell(ability, slotIndex, cooldowns[ability.id] ?? 0)),
status: status === 'victory' ? 'won' : status === 'defeat' ? 'lost' : 'playing',
targetGroup: 0,
}
}
function toDualScreenPartyMember(member: Iwt2ArenaState['party'][number]): PartyMember {
const metadata = IWT2_CLASS_METADATA[member.classId]
return {
bounceHeals: [],
health: member.health,
hotEffects: member.hotEffects.map((effect) => ({
id: effect.id,
label: effect.label,
power: effect.power,
spellId: effect.id,
ticks: Math.ceil(effect.remainingSeconds),
})),
hotTicks: member.hotEffects.length,
id: member.id,
maxHealth: member.maxHealth,
name: metadata.name,
role: toDualScreenRole(metadata.role),
shield: member.shield,
}
}
function toDualScreenRole(role: (typeof IWT2_CLASS_METADATA)[keyof typeof IWT2_CLASS_METADATA]['role']): Role {
if (role === 'tank') return 'Tank'
if (role === 'healer') return 'Healer'
return 'Damage'
}
function toDualScreenSpell(
ability: Iwt2HealerAbility,
slotIndex: number,
remaining: number,
): Spell & { slotIndex: number; remaining: number } {
return {
cooldown: ability.cooldownSeconds,
cost: ability.manaCost,
description: 'IWT2 arena ability',
effectType: ability.effectType,
glyph: ability.icon,
id: ability.id,
key: String(ability.slot),
kind: ability.kind,
name: ability.name,
power: ability.power,
remaining,
slotIndex,
}
}
+12 -1
View File
@@ -1,4 +1,4 @@
import { useMemo, useState, type ReactNode } from 'react'
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
import { ControllerStylePreview } from '../../../components/ControllerIcons'
import { useGameAction, useInput, type ControllerIconStyle } from '../../../input'
import { getGameMode } from '../../../gameRepository'
@@ -161,6 +161,14 @@ export function Iwt2ScreenShell({
export function Iwt2ActionList({ actions }: Iwt2ActionListProps) {
const { selectedIndex, setSelectedIndex } = useIwt2ActionList(actions)
const rowRefs = useRef<Array<HTMLButtonElement | null>>([])
useEffect(() => {
rowRefs.current[selectedIndex]?.scrollIntoView({
block: 'nearest',
inline: 'nearest',
})
}, [selectedIndex])
return (
<div className="iwt2-action-list">
@@ -174,6 +182,9 @@ export function Iwt2ActionList({ actions }: Iwt2ActionListProps) {
onPointerDown={() => {
if (!action.disabled) setSelectedIndex(index)
}}
ref={(element) => {
rowRefs.current[index] = element
}}
type="button"
>
<span>