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
+98 -37
View File
@@ -47,6 +47,7 @@ const OVERLAY_NAV_ENTRIES: OverlayNavEntry[] = [
type BossArenaScreenProps = {
bossId: Iwt2BossId
bossIds?: Iwt2BossId[]
modeLabel?: string
save: Iwt2Save
onBack: () => void
@@ -61,12 +62,12 @@ type BossArenaScreenProps = {
}
}
export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated, roguelikeRun }: BossArenaScreenProps) {
export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSaveUpdated, roguelikeRun }: BossArenaScreenProps) {
const bossMetadata = IWT2_BOSS_METADATA[bossId]
const pvpRoguelike = roguelikeRun?.variant === 'pvp'
const [arenaState, setArenaState] = useState<Iwt2ArenaState>(() => createInitialIwt2ArenaState(bossId))
const [arenaState, setArenaState] = useState<Iwt2ArenaState>(() => createInitialIwt2ArenaState(bossId, bossIds))
const [opponentArenaState, setOpponentArenaState] = useState<Iwt2ArenaState | null>(() => (
pvpRoguelike ? createInitialIwt2ArenaState(bossId) : null
pvpRoguelike ? createInitialIwt2ArenaState(bossId, bossIds) : null
))
const [abilityCooldowns, setAbilityCooldowns] = useState<Record<string, number>>({})
const [status, setStatus] = useState<ArenaStatus>('playing')
@@ -78,7 +79,7 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
const selectedOverlayActionRef = useRef<OverlayAction>(selectedOverlayAction)
const selectedPartyIdRef = useRef<Iwt2EntityId>(selectedPartyId)
const saveRef = useRef(save)
const killRecordedRef = useRef(false)
const recordedKillIdsRef = useRef<Set<Iwt2BossId>>(new Set())
const lastPublishTimeRef = useRef(0)
const lastHudSignatureRef = useRef('')
const abilityCooldownsRef = useRef<Record<string, number>>({})
@@ -100,12 +101,6 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
opponentStateRef.current = opponentArenaState
}, [opponentArenaState])
useEffect(() => {
const nextOpponentState = pvpRoguelike ? createInitialIwt2ArenaState(bossId) : null
opponentStateRef.current = nextOpponentState
setOpponentArenaState(nextOpponentState)
}, [bossId, pvpRoguelike])
useEffect(() => {
statusRef.current = status
}, [status])
@@ -123,9 +118,9 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
}, [save])
const resetArena = useCallback(() => {
const next = createInitialIwt2ArenaState(bossId)
const nextOpponentState = pvpRoguelike ? createInitialIwt2ArenaState(bossId) : null
killRecordedRef.current = false
const next = createInitialIwt2ArenaState(bossId, bossIds)
const nextOpponentState = pvpRoguelike ? createInitialIwt2ArenaState(bossId, bossIds) : null
recordedKillIdsRef.current = new Set()
abilityCooldownsRef.current = {}
lastHudSignatureRef.current = arenaHudSignature(next)
stateRef.current = next
@@ -135,7 +130,7 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
setAbilityCooldowns({})
setSelectedOverlayAction('primary')
setStatus('playing')
}, [bossId, pvpRoguelike])
}, [bossId, bossIds, pvpRoguelike])
const showOverlay = useCallback((nextStatus: Exclude<ArenaStatus, 'playing'>) => {
setSelectedOverlayAction('primary')
@@ -272,11 +267,20 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
opponentStateRef.current = nextOpponentState
}
if (next.boss.health <= 0 && !killRecordedRef.current) {
killRecordedRef.current = true
const updatedSave = recordIwt2BossKill(saveRef.current, bossId)
const newlyDefeatedBosses = next.bosses.filter((boss) => boss.health <= 0 && !recordedKillIdsRef.current.has(boss.bossId))
if (newlyDefeatedBosses.length > 0) {
const nextRecordedIds = new Set(recordedKillIdsRef.current)
let updatedSave = saveRef.current
for (const defeatedBoss of newlyDefeatedBosses) {
nextRecordedIds.add(defeatedBoss.bossId)
updatedSave = recordIwt2BossKill(updatedSave, defeatedBoss.bossId)
}
recordedKillIdsRef.current = nextRecordedIds
saveRef.current = updatedSave
onSaveUpdated(updatedSave)
}
if (next.bosses.every((boss) => boss.health <= 0)) {
showOverlay('victory')
} else if (next.party.every((member) => member.health <= 0)) {
showOverlay('defeat')
@@ -286,7 +290,7 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
if (
hudSignature !== lastHudSignatureRef.current
|| next.time - lastPublishTimeRef.current >= 0.08
|| next.boss.health <= 0
|| next.bosses.some((boss) => boss.health <= 0)
) {
lastHudSignatureRef.current = hudSignature
lastPublishTimeRef.current = next.time
@@ -295,7 +299,7 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
setAbilityCooldowns(abilityCooldownsRef.current)
}
return next
}, [bossId, onSaveUpdated, pvpRoguelike, showOverlay])
}, [onSaveUpdated, pvpRoguelike, showOverlay])
const targetBindings = directPartyTargeting
? IWT2_TARGET_ACTIONS.map((action) => activeBindings[action] ?? null)
@@ -303,13 +307,15 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
const playerMana = arenaState.party.find((member) => member.id === 'player-healer')?.mana ?? 0
const alivePartyCount = arenaState.party.filter((member) => member.health > 0).length
const totalPartyDamage = arenaState.party.reduce((total, member) => total + member.damageDone, 0)
const defeatedBossCount = arenaState.bosses.filter((boss) => boss.health <= 0).length
const bossTitle = formatBossEncounterTitle(arenaState.bosses)
const overlayPrimaryLabel = status === 'paused'
? 'Resume'
: status === 'victory' && roguelikeRun
? 'Choose Upgrade'
: 'Restart'
const overlayTitle = status === 'victory'
? `${bossMetadata.name} Down`
? `${bossTitle} Down`
: status === 'defeat'
? 'Party Defeated'
: 'Arena Paused'
@@ -337,7 +343,6 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
abilities,
arenaState,
bindings: activeBindings,
bossMetadata,
cooldowns: abilityCooldowns,
controllerIconStyle,
directPartyTargeting,
@@ -353,7 +358,6 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
activeBindings,
arenaState,
abilityCooldowns,
bossMetadata,
controllerIconStyle,
directPartyTargeting,
modeLabel,
@@ -375,7 +379,7 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
>
<section className="iwt2-arena-layout">
<div className="iwt2-arena-stage">
<BossHud boss={arenaState.boss} />
<BossHud bosses={arenaState.bosses} />
<PartyFrames
controllerIconStyle={controllerIconStyle}
onTarget={setSelectedPartyId}
@@ -399,6 +403,7 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
<button
className={selectedOverlayAction === 'primary' ? 'game-selected' : ''}
data-controller-nav="skip"
data-game-selected={selectedOverlayAction === 'primary' ? 'true' : undefined}
onClick={() => activateOverlayAction('primary')}
onPointerDown={() => setSelectedOverlayAction('primary')}
type="button"
@@ -408,6 +413,7 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
<button
className={`secondary-result-button ${selectedOverlayAction === 'menu' ? 'game-selected' : ''}`}
data-controller-nav="skip"
data-game-selected={selectedOverlayAction === 'menu' ? 'true' : undefined}
onClick={() => activateOverlayAction('menu')}
onPointerDown={() => setSelectedOverlayAction('menu')}
type="button"
@@ -422,7 +428,7 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
<div className={`pause-screen iwt2-arena-overlay ${overlayTone}`} data-game-nav-active="true">
<div className="iwt2-result-panel">
<div className="iwt2-result-crest" style={{ '--boss-color': bossMetadata.color, '--boss-accent': bossMetadata.accentColor } as CSSProperties}>
<span>{bossMetadata.icon}</span>
<span>{arenaState.bosses.map((boss) => IWT2_BOSS_METADATA[boss.bossId].icon).join('')}</span>
</div>
<p className="eyebrow">{overlayEyebrow}</p>
<h1>{overlayTitle}</h1>
@@ -440,6 +446,10 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
<strong>{alivePartyCount}/{arenaState.party.length}</strong>
Standing
</span>
<span>
<strong>{defeatedBossCount}/{arenaState.bosses.length}</strong>
Bosses
</span>
<span>
<strong>{Math.round(totalPartyDamage)}</strong>
Damage
@@ -447,9 +457,9 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
</div>
{status === 'victory' && (
<div className="iwt2-result-reward">
<span>+125 XP</span>
<span>Common carve</span>
<span>Log +1</span>
<span>+{arenaState.bosses.length * 125} XP</span>
<span>{arenaState.bosses.length} carves</span>
<span>Log +{arenaState.bosses.length}</span>
</div>
)}
<div className="iwt2-overlay-actions">
@@ -498,9 +508,11 @@ function formatArenaTime(seconds: number): string {
function arenaHudSignature(state: Iwt2ArenaState): string {
return [
state.boss.id,
Math.ceil(state.boss.health),
state.boss.attackPhase,
...state.bosses.map((boss) => [
boss.id,
Math.ceil(boss.health),
boss.attackPhase,
].join(':')),
state.hostileAdds.length,
state.hazards.length,
...state.party.map((member) => [
@@ -514,6 +526,12 @@ function arenaHudSignature(state: Iwt2ArenaState): string {
].join('|')
}
function formatBossEncounterTitle(bosses: Iwt2ArenaState['bosses']): string {
return bosses
.map((boss) => IWT2_BOSS_METADATA[boss.bossId].name)
.join(' + ')
}
function nextTargetId(state: Iwt2ArenaState, currentId: Iwt2EntityId, direction: -1 | 1): Iwt2EntityId {
const living = state.party.filter((member) => member.health > 0)
const targets = living.length > 0 ? living : state.party
@@ -586,7 +604,6 @@ function buildIwt2DualScreenCombatState({
abilities,
arenaState,
bindings,
bossMetadata,
cooldowns,
controllerIconStyle,
directPartyTargeting,
@@ -600,7 +617,6 @@ function buildIwt2DualScreenCombatState({
abilities: Iwt2HealerAbility[]
arenaState: Iwt2ArenaState
bindings: DualScreenCombatState['bindings']
bossMetadata: (typeof IWT2_BOSS_METADATA)[Iwt2BossId]
cooldowns: Record<string, number>
controllerIconStyle: DualScreenCombatState['controllerIconStyle']
directPartyTargeting: boolean
@@ -612,12 +628,15 @@ function buildIwt2DualScreenCombatState({
status: ArenaStatus
}): DualScreenCombatState {
const opponentHealer = opponentArenaState?.party.find((member) => member.id === 'player-healer')
const arenaBossHealth = totalBossHealth(arenaState)
const arenaBossMaxHealth = totalBossMaxHealth(arenaState)
const pvpOpponentState = roguelikeRun?.variant === 'pvp' && opponentArenaState
? {
opponentBuffSummary: `Stage ${roguelikeRun.stage}`,
opponentClassName: `CPU Healer | ${formatRoguelikeContentType(roguelikeRun.contentType)}`,
opponentDebuffSummary: formatIwt2DebuffSummary(roguelikeRun.debuffs),
opponentEnemyHealth: opponentArenaState.boss.health,
opponentArena: toDualScreenOpponentArena(opponentArenaState),
opponentEnemyHealth: totalBossHealth(opponentArenaState),
opponentMaxResource: opponentHealer?.maxMana ?? 100,
opponentName: 'CPU Rival',
opponentParty: opponentArenaState.party.map((member) => toDualScreenPartyMember(member, true)),
@@ -632,14 +651,14 @@ function buildIwt2DualScreenCombatState({
controllerIconStyle,
difficultyName: 'IWT2',
directPartyTargeting,
dungeonName: `${bossMetadata.name} Arena`,
dungeonName: `${formatBossEncounterTitle(arenaState.bosses)} Arena`,
encounterCount: 1,
encounterDescription: arenaState.boss.attackPhase,
encounterHealth: arenaState.boss.health,
encounterHealth: arenaBossHealth,
encounterIndex: 0,
encounterIsBoss: true,
encounterMaxHealth: arenaState.boss.maxHealth,
encounterName: bossMetadata.name,
encounterMaxHealth: arenaBossMaxHealth,
encounterName: formatBossEncounterTitle(arenaState.bosses),
floatingTexts: [],
maxResource: 100,
party: arenaState.party.map((member) => toDualScreenPartyMember(member)),
@@ -656,6 +675,48 @@ function buildIwt2DualScreenCombatState({
}
}
function totalBossHealth(state: Iwt2ArenaState): number {
return state.bosses.reduce((total, boss) => total + Math.max(0, boss.health), 0)
}
function totalBossMaxHealth(state: Iwt2ArenaState): number {
return state.bosses.reduce((total, boss) => total + boss.maxHealth, 0)
}
function toDualScreenOpponentArena(state: Iwt2ArenaState): NonNullable<DualScreenCombatState['opponentArena']> {
return {
bounds: state.bounds,
bosses: state.bosses.map((boss) => {
const metadata = IWT2_BOSS_METADATA[boss.bossId]
return {
id: boss.id,
name: metadata.name,
icon: metadata.icon,
color: metadata.color,
x: boss.position.x,
y: boss.position.y,
radius: boss.radius,
health: boss.health,
maxHealth: boss.maxHealth,
}
}),
party: state.party.map((member) => {
const metadata = IWT2_CLASS_METADATA[member.classId]
return {
id: member.id,
icon: metadata.icon,
color: metadata.color,
x: member.position.x,
y: member.position.y,
radius: member.radius,
health: member.health,
maxHealth: member.maxHealth,
isHealer: member.id === 'player-healer',
}
}),
}
}
function toDualScreenPartyMember(member: Iwt2ArenaState['party'][number], opponent = false): PartyMember {
const metadata = IWT2_CLASS_METADATA[member.classId]
return {