Android build v1.1.19
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user