Android build v1.1.18

This commit is contained in:
Warren H
2026-07-04 19:31:58 -04:00
parent c2a14bac7d
commit c052b086f8
5 changed files with 137 additions and 13 deletions
+94 -6
View File
@@ -63,12 +63,17 @@ type BossArenaScreenProps = {
export function BossArenaScreen({ bossId, 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 [opponentArenaState, setOpponentArenaState] = useState<Iwt2ArenaState | null>(() => (
pvpRoguelike ? createInitialIwt2ArenaState(bossId) : null
))
const [abilityCooldowns, setAbilityCooldowns] = useState<Record<string, number>>({})
const [status, setStatus] = useState<ArenaStatus>('playing')
const [selectedOverlayAction, setSelectedOverlayAction] = useState<OverlayAction>('primary')
const [selectedPartyId, setSelectedPartyId] = useState<Iwt2EntityId>('player-healer')
const stateRef = useRef(arenaState)
const opponentStateRef = useRef<Iwt2ArenaState | null>(opponentArenaState)
const statusRef = useRef(status)
const selectedOverlayActionRef = useRef<OverlayAction>(selectedOverlayAction)
const selectedPartyIdRef = useRef<Iwt2EntityId>(selectedPartyId)
@@ -91,6 +96,16 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
stateRef.current = arenaState
}, [arenaState])
useEffect(() => {
opponentStateRef.current = opponentArenaState
}, [opponentArenaState])
useEffect(() => {
const nextOpponentState = pvpRoguelike ? createInitialIwt2ArenaState(bossId) : null
opponentStateRef.current = nextOpponentState
setOpponentArenaState(nextOpponentState)
}, [bossId, pvpRoguelike])
useEffect(() => {
statusRef.current = status
}, [status])
@@ -109,15 +124,18 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
const resetArena = useCallback(() => {
const next = createInitialIwt2ArenaState(bossId)
const nextOpponentState = pvpRoguelike ? createInitialIwt2ArenaState(bossId) : null
killRecordedRef.current = false
abilityCooldownsRef.current = {}
lastHudSignatureRef.current = arenaHudSignature(next)
stateRef.current = next
opponentStateRef.current = nextOpponentState
setArenaState(next)
setOpponentArenaState(nextOpponentState)
setAbilityCooldowns({})
setSelectedOverlayAction('primary')
setStatus('playing')
}, [bossId])
}, [bossId, pvpRoguelike])
const showOverlay = useCallback((nextStatus: Exclude<ArenaStatus, 'playing'>) => {
setSelectedOverlayAction('primary')
@@ -240,6 +258,19 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
moveY: movement.y,
}, dtSeconds)
stateRef.current = next
let nextOpponentState = opponentStateRef.current
if (pvpRoguelike && nextOpponentState) {
const opponentMovement = iwt2OpponentMovement(nextOpponentState)
nextOpponentState = tickIwt2Arena(
nextOpponentState,
{
moveX: opponentMovement.x,
moveY: opponentMovement.y,
},
dtSeconds,
)
opponentStateRef.current = nextOpponentState
}
if (next.boss.health <= 0 && !killRecordedRef.current) {
killRecordedRef.current = true
@@ -260,10 +291,11 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
lastHudSignatureRef.current = hudSignature
lastPublishTimeRef.current = next.time
setArenaState(next)
if (pvpRoguelike) setOpponentArenaState(nextOpponentState)
setAbilityCooldowns(abilityCooldownsRef.current)
}
return next
}, [bossId, onSaveUpdated, showOverlay])
}, [bossId, onSaveUpdated, pvpRoguelike, showOverlay])
const targetBindings = directPartyTargeting
? IWT2_TARGET_ACTIONS.map((action) => activeBindings[action] ?? null)
@@ -310,7 +342,9 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
controllerIconStyle,
directPartyTargeting,
modeLabel: modeLabel ?? 'Arena',
opponentArenaState,
playerMana,
roguelikeRun,
selectedPartyId,
status,
}),
@@ -323,7 +357,9 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
controllerIconStyle,
directPartyTargeting,
modeLabel,
opponentArenaState,
playerMana,
roguelikeRun,
selectedPartyId,
status,
],
@@ -555,7 +591,9 @@ function buildIwt2DualScreenCombatState({
controllerIconStyle,
directPartyTargeting,
modeLabel,
opponentArenaState,
playerMana,
roguelikeRun,
selectedPartyId,
status,
}: {
@@ -567,11 +605,28 @@ function buildIwt2DualScreenCombatState({
controllerIconStyle: DualScreenCombatState['controllerIconStyle']
directPartyTargeting: boolean
modeLabel: string
opponentArenaState: Iwt2ArenaState | null
playerMana: number
roguelikeRun: BossArenaScreenProps['roguelikeRun'] | undefined
selectedPartyId: Iwt2EntityId
status: ArenaStatus
}): DualScreenCombatState {
const opponentHealer = opponentArenaState?.party.find((member) => member.id === 'player-healer')
const pvpOpponentState = roguelikeRun?.variant === 'pvp' && opponentArenaState
? {
opponentBuffSummary: `Stage ${roguelikeRun.stage}`,
opponentClassName: `CPU Healer | ${formatRoguelikeContentType(roguelikeRun.contentType)}`,
opponentDebuffSummary: formatIwt2DebuffSummary(roguelikeRun.debuffs),
opponentEnemyHealth: opponentArenaState.boss.health,
opponentMaxResource: opponentHealer?.maxMana ?? 100,
opponentName: 'CPU Rival',
opponentParty: opponentArenaState.party.map((member) => toDualScreenPartyMember(member, true)),
opponentResource: opponentHealer?.mana ?? 100,
opponentResourceName: 'Mana',
}
: {}
return {
...pvpOpponentState,
bindings,
contentName: modeLabel,
controllerIconStyle,
@@ -587,7 +642,7 @@ function buildIwt2DualScreenCombatState({
encounterName: bossMetadata.name,
floatingTexts: [],
maxResource: 100,
party: arenaState.party.map(toDualScreenPartyMember),
party: arenaState.party.map((member) => toDualScreenPartyMember(member)),
partySize: arenaState.party.length,
paused: status === 'paused',
playerIsAlive: (arenaState.party.find((member) => member.id === 'player-healer')?.health ?? 0) > 0,
@@ -601,7 +656,7 @@ function buildIwt2DualScreenCombatState({
}
}
function toDualScreenPartyMember(member: Iwt2ArenaState['party'][number]): PartyMember {
function toDualScreenPartyMember(member: Iwt2ArenaState['party'][number], opponent = false): PartyMember {
const metadata = IWT2_CLASS_METADATA[member.classId]
return {
bounceHeals: [],
@@ -614,14 +669,19 @@ function toDualScreenPartyMember(member: Iwt2ArenaState['party'][number]): Party
ticks: Math.ceil(effect.remainingSeconds),
})),
hotTicks: member.hotEffects.length,
id: member.id,
id: opponent ? `opponent-${member.id}` : member.id,
maxHealth: member.maxHealth,
name: metadata.name,
name: opponent ? opponentPartyName(metadata.name) : metadata.name,
role: toDualScreenRole(metadata.role),
shield: member.shield,
}
}
function opponentPartyName(name: string): string {
if (name === 'Player Healer') return 'CPU Healer'
return `CPU ${name.replace(' Tank', '')}`
}
function toDualScreenRole(role: (typeof IWT2_CLASS_METADATA)[keyof typeof IWT2_CLASS_METADATA]['role']): Role {
if (role === 'tank') return 'Tank'
if (role === 'healer') return 'Healer'
@@ -648,3 +708,31 @@ function toDualScreenSpell(
slotIndex,
}
}
function iwt2OpponentMovement(state: Iwt2ArenaState): MovementVector {
const healer = state.party.find((member) => member.id === 'player-healer')
if (!healer || healer.health <= 0) return { x: 0, y: 0 }
const orbitSeconds = Math.max(1, state.time)
return normalizedArenaMovement({
x: Math.cos(orbitSeconds * 0.75),
y: Math.sin(orbitSeconds * 0.75) * 0.55,
})
}
function normalizedArenaMovement(vector: MovementVector): MovementVector {
const magnitude = Math.hypot(vector.x, vector.y)
if (magnitude <= 1) return vector
return {
x: vector.x / magnitude,
y: vector.y / magnitude,
}
}
function formatIwt2DebuffSummary(debuffs: readonly string[]): string {
if (debuffs.length === 0) return 'none'
return debuffs
.map((debuff) => debuff
.replace('opp-', '')
.replaceAll('-', ' '))
.join(', ')
}