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
+157 -64
View File
@@ -2,8 +2,10 @@ import { IWT2_BOSS_METADATA, type Iwt2BossId } from '../content/bosses'
import { IWT2_CLASS_METADATA } from '../content/classes'
import type {
Iwt2ArenaEvent,
Iwt2ArenaIndicator,
Iwt2ArenaInput,
Iwt2ArenaState,
Iwt2BossEntityState,
Iwt2EntityId,
Iwt2GroundHazardState,
Iwt2HostileAddState,
@@ -33,6 +35,7 @@ const MAX_DT = 1 / 15
const MAX_EVENTS = 80
const HEALER_MANA_REGEN_PER_SECOND = 3
const BOSS_PROJECTILE_BOUNCE_COOLDOWN_SECONDS = 0.22
const IWT2_ARENA_BOSS_IDS: Iwt2BossId[] = ['bulldrome', 'yian-kut-ku', 'great-jaggi', 'khezu']
type InitialPartyMember = {
id: Iwt2PartyEntityId
@@ -53,8 +56,9 @@ const INITIAL_PARTY: InitialPartyMember[] = [
{ id: 'warrior', classId: 'warrior', aiRole: 'melee', x: 515, y: 310, preferredOffset: { x: -18, y: 64 }, decisionOffset: 0.24 },
]
export function createInitialIwt2ArenaState(bossId: Iwt2BossId = 'bulldrome'): Iwt2ArenaState {
const bossMetadata = IWT2_BOSS_METADATA[bossId]
export function createInitialIwt2ArenaState(bossId: Iwt2BossId = 'bulldrome', bossIds?: Iwt2BossId[]): Iwt2ArenaState {
const initialBossIds = bossIds?.length ? bossIds.slice(0, 2) : chooseInitialBossIds(bossId)
const bosses = initialBossIds.map((id, index) => createBossEntity(id, index))
return {
schemaVersion: 1,
time: 0,
@@ -64,33 +68,8 @@ export function createInitialIwt2ArenaState(bossId: Iwt2BossId = 'bulldrome'): I
hostileAdds: [],
hazards: [],
indicators: [],
boss: {
id: bossId,
kind: 'boss',
bossId,
position: { x: 640, y: 250 },
velocity: { x: 0, y: 0 },
facing: { x: -1, y: 0 },
radius: bossMetadata.radius,
health: bossMetadata.maxHealth,
maxHealth: bossMetadata.maxHealth,
meleeCooldownRemaining: 0.6,
chargeCooldownRemaining: initialBossSpecialCooldown(bossId),
chargeCount: 0,
attackPhase: 'idle',
phaseSecondsRemaining: 0,
chargeStart: { x: 640, y: 250 },
chargeEnd: { x: 640, y: 250 },
chargeHitEntityIds: [],
slamApplied: false,
wallContactSeconds: 0,
relocateTarget: { x: DEFAULT_ARENA_WIDTH * 0.58, y: DEFAULT_ARENA_HEIGHT * 0.5 },
fireballCooldownRemaining: initialBossSecondaryCooldown(bossId),
fireballTarget: { x: 320, y: 250 },
birdWaveThresholdsTriggered: [],
mechanicLanes: [],
mechanicCircles: [],
},
boss: bosses[0],
bosses,
nextEventId: 1,
nextProjectileId: 1,
nextAddId: 1,
@@ -99,6 +78,51 @@ export function createInitialIwt2ArenaState(bossId: Iwt2BossId = 'bulldrome'): I
}
}
function chooseInitialBossIds(primaryBossId: Iwt2BossId): Iwt2BossId[] {
const remaining = IWT2_ARENA_BOSS_IDS.filter((id) => id !== primaryBossId)
const random = remaining[Math.floor(Math.random() * remaining.length)] ?? primaryBossId
return [primaryBossId, random]
}
function createBossEntity(bossId: Iwt2BossId, index: number): Iwt2BossEntityState {
const bossMetadata = IWT2_BOSS_METADATA[bossId]
const position = initialBossPosition(index)
return {
id: bossId,
kind: 'boss',
bossId,
position,
velocity: { x: 0, y: 0 },
facing: { x: -1, y: 0 },
radius: bossMetadata.radius,
health: bossMetadata.maxHealth,
maxHealth: bossMetadata.maxHealth,
meleeCooldownRemaining: 0.6 + index * 0.25,
chargeCooldownRemaining: initialBossSpecialCooldown(bossId) + index * 0.7,
chargeCount: 0,
attackPhase: 'idle',
phaseSecondsRemaining: 0,
chargeStart: { ...position },
chargeEnd: { ...position },
chargeHitEntityIds: [],
slamApplied: false,
wallContactSeconds: 0,
relocateTarget: { x: DEFAULT_ARENA_WIDTH * 0.58, y: DEFAULT_ARENA_HEIGHT * 0.5 + (index === 0 ? -64 : 64) },
fireballCooldownRemaining: initialBossSecondaryCooldown(bossId) + index * 0.7,
fireballTarget: { x: 320, y: 250 },
birdWaveThresholdsTriggered: [],
mechanicLanes: [],
mechanicCircles: [],
}
}
function initialBossPosition(index: number) {
return {
x: index === 0 ? 660 : 760,
y: index === 0 ? 190 : 345,
}
}
function initialBossSpecialCooldown(bossId: Iwt2BossId): number {
if (bossId === 'bulldrome') return 2
if (bossId === 'great-jaggi') return 2.4
@@ -128,13 +152,13 @@ export function tickIwt2Arena(state: Iwt2ArenaState, input: Iwt2ArenaInput, dt:
}
const separatedState = {
...baseState,
party: separatePartyFromBoss(baseState.party, baseState),
party: separatePartyFromBosses(baseState.party, baseState),
}
const bossResult = tickBoss(separatedState, step)
const bossResult = tickBosses(separatedState, step)
const projectileResult = advanceProjectiles(
[...separatedState.projectiles, ...(bossResult.projectiles ?? [])],
bossResult.party,
bossResult.boss,
bossResult.bosses,
bossResult.hostileAdds ?? separatedState.hostileAdds,
bossResult.hazards ?? separatedState.hazards,
bossResult.nextHazardId ?? state.nextHazardId,
@@ -151,7 +175,7 @@ export function tickIwt2Arena(state: Iwt2ArenaState, input: Iwt2ArenaInput, dt:
})
const damageResult = applyPartyAttacks(
hazardResult.party,
projectileResult.boss,
projectileResult.bosses,
projectileResult.hostileAdds,
separatedState.time,
projectileResult.nextProjectileId,
@@ -164,7 +188,8 @@ export function tickIwt2Arena(state: Iwt2ArenaState, input: Iwt2ArenaInput, dt:
)
return {
...separatedState,
boss: damageResult.boss,
boss: getPrimaryBoss(damageResult.bosses),
bosses: damageResult.bosses,
indicators: [...bossResult.indicators, ...createHazardIndicators(hazardResult.hazards)],
party: finalParty,
hostileAdds: damageResult.hostileAdds,
@@ -274,14 +299,69 @@ function clampInputAxis(value: number): number {
return Math.min(1, Math.max(-1, value))
}
function separatePartyFromBoss(party: Iwt2PartyEntityState[], state: Iwt2ArenaState): Iwt2PartyEntityState[] {
function tickBosses(state: Iwt2ArenaState, dt: number) {
let nextParty = state.party
let nextHostileAdds = state.hostileAdds
let nextHazards = state.hazards
let nextProjectileId = state.nextProjectileId
let nextAddId = state.nextAddId
let nextHazardId = state.nextHazardId
const bosses: Iwt2BossEntityState[] = []
const projectiles: Iwt2ProjectileEntityState[] = []
const events: Iwt2ArenaEvent[] = []
const indicators: Iwt2ArenaIndicator[] = []
for (const boss of state.bosses) {
const bossState = {
...state,
boss,
party: nextParty,
hostileAdds: nextHostileAdds,
hazards: nextHazards,
nextProjectileId,
nextAddId,
nextHazardId,
}
const result = tickBoss(bossState, dt)
bosses.push(result.boss)
nextParty = result.party
nextHostileAdds = result.hostileAdds ?? nextHostileAdds
nextHazards = result.hazards ?? nextHazards
nextProjectileId = result.nextProjectileId ?? nextProjectileId
nextAddId = result.nextAddId ?? nextAddId
nextHazardId = result.nextHazardId ?? nextHazardId
projectiles.push(...(result.projectiles ?? []))
events.push(...result.events)
indicators.push(...result.indicators)
}
return {
bosses,
boss: getPrimaryBoss(bosses),
party: nextParty,
hostileAdds: nextHostileAdds,
hazards: nextHazards,
projectiles,
events,
indicators,
nextAddId,
nextHazardId,
nextProjectileId,
}
}
function separatePartyFromBosses(party: Iwt2PartyEntityState[], state: Iwt2ArenaState): Iwt2PartyEntityState[] {
return party.map((member) => {
if (member.health <= 0) return member
const position = separateCircles(
{ position: member.position, radius: member.radius },
{ position: state.boss.position, radius: state.boss.radius },
state.bounds,
)
let position = member.position
for (const boss of state.bosses) {
if (boss.health <= 0) continue
position = separateCircles(
{ position, radius: member.radius },
{ position: boss.position, radius: boss.radius },
state.bounds,
)
}
return { ...member, position }
})
}
@@ -289,7 +369,7 @@ function separatePartyFromBoss(party: Iwt2PartyEntityState[], state: Iwt2ArenaSt
function advanceProjectiles(
projectiles: Iwt2ProjectileEntityState[],
party: Iwt2PartyEntityState[],
boss: Iwt2ArenaState['boss'],
bosses: Iwt2BossEntityState[],
hostileAdds: Iwt2HostileAddState[],
hazards: Iwt2GroundHazardState[],
nextHazardId: number,
@@ -298,7 +378,7 @@ function advanceProjectiles(
time: number,
dt: number,
): {
boss: Iwt2ArenaState['boss']
bosses: Iwt2BossEntityState[]
party: Iwt2PartyEntityState[]
hostileAdds: Iwt2HostileAddState[]
hazards: Iwt2GroundHazardState[]
@@ -308,7 +388,7 @@ function advanceProjectiles(
events: Iwt2ArenaEvent[]
} {
const events: Iwt2ArenaEvent[] = []
let nextBoss = boss
let nextBosses = bosses
let nextParty = party
let nextHostileAdds = hostileAdds
let nextHazards = hazards
@@ -334,7 +414,7 @@ function advanceProjectiles(
continue
}
if (nextBoss.health <= 0 && nextHostileAdds.every((add) => add.health <= 0)) continue
if (nextBosses.every((boss) => boss.health <= 0) && nextHostileAdds.every((add) => add.health <= 0)) continue
const nextPosition = {
x: projectile.position.x + projectile.velocity.x * dt,
y: projectile.position.y + projectile.velocity.y * dt,
@@ -371,28 +451,31 @@ function advanceProjectiles(
continue
}
if (distanceVec2(nextPosition, nextBoss.position) <= nextBoss.radius + projectile.radius) {
const damage = Math.min(projectile.damage, nextBoss.health)
nextBoss = {
...nextBoss,
health: Math.max(0, nextBoss.health - damage),
}
const hitBoss = nextBosses.find((boss) => (
boss.health > 0
&& distanceVec2(nextPosition, boss.position) <= boss.radius + projectile.radius
))
if (hitBoss) {
const damage = Math.min(projectile.damage, hitBoss.health)
nextBosses = nextBosses.map((boss) => boss.id === hitBoss.id
? { ...boss, health: Math.max(0, boss.health - damage) }
: boss)
nextParty = addDamageDone(nextParty, projectile.sourceId, damage)
events.push({
id: 0,
time,
type: 'bossDamaged',
sourceId: projectile.sourceId,
targetId: nextBoss.id,
targetId: hitBoss.id,
value: damage,
})
if (nextBoss.health <= 0) {
if (hitBoss.health - damage <= 0) {
events.push({
id: 0,
time,
type: 'entityDefeated',
sourceId: projectile.sourceId,
targetId: nextBoss.id,
targetId: hitBoss.id,
})
}
continue
@@ -405,7 +488,7 @@ function advanceProjectiles(
}
return {
boss: nextBoss,
bosses: nextBosses,
party: nextParty,
hostileAdds: nextHostileAdds,
hazards: nextHazards,
@@ -536,12 +619,12 @@ function advanceBossProjectile({
function applyPartyAttacks(
party: Iwt2PartyEntityState[],
boss: Iwt2ArenaState['boss'],
bosses: Iwt2BossEntityState[],
hostileAdds: Iwt2HostileAddState[],
time: number,
nextProjectileId: number,
): {
boss: Iwt2ArenaState['boss']
bosses: Iwt2BossEntityState[]
party: Iwt2PartyEntityState[]
hostileAdds: Iwt2HostileAddState[]
projectiles: Iwt2ProjectileEntityState[]
@@ -550,13 +633,13 @@ function applyPartyAttacks(
} {
const events: Iwt2ArenaEvent[] = []
const projectiles: Iwt2ProjectileEntityState[] = []
let nextBoss = boss
let nextBosses = bosses
let nextHostileAdds = hostileAdds
let projectileId = nextProjectileId
const nextParty = party.map((member) => {
const target = getPriorityAttackTarget(member, nextBoss, nextHostileAdds)
const target = getPriorityAttackTarget(member, nextBosses, nextHostileAdds)
if (!target) return member
if (!canPartyMemberHitTarget(member, target.position)) return member
if (!canPartyMemberHitTarget(member, target.position, target.radius)) return member
const metadata = IWT2_CLASS_METADATA[member.classId]
if (metadata.projectileSpeed > 0) {
if (!member.attackReady) return member
@@ -587,7 +670,9 @@ function applyPartyAttacks(
if (member.attackCooldownRemaining > 0) return member
const damage = Math.min(metadata.attackDamage, target.health)
if (target.kind === 'boss') {
nextBoss = { ...nextBoss, health: Math.max(0, nextBoss.health - damage) }
nextBosses = nextBosses.map((boss) => boss.id === target.id
? { ...boss, health: Math.max(0, boss.health - damage) }
: boss)
} else {
nextHostileAdds = nextHostileAdds.map((add) => add.id === target.id
? { ...add, health: Math.max(0, add.health - damage) }
@@ -622,7 +707,7 @@ function applyPartyAttacks(
}
})
return {
boss: nextBoss,
bosses: nextBosses,
party: nextParty,
hostileAdds: nextHostileAdds.filter((add) => add.health > 0),
projectiles,
@@ -633,9 +718,9 @@ function applyPartyAttacks(
function getPriorityAttackTarget(
member: Iwt2PartyEntityState,
boss: Iwt2ArenaState['boss'],
bosses: Iwt2BossEntityState[],
hostileAdds: Iwt2HostileAddState[],
): (Iwt2ArenaState['boss'] | Iwt2HostileAddState) | undefined {
): (Iwt2BossEntityState | Iwt2HostileAddState) | undefined {
if (member.health <= 0) return undefined
const livingAdds = hostileAdds.filter((add) => add.health > 0)
if (livingAdds.length > 0) {
@@ -643,7 +728,15 @@ function getPriorityAttackTarget(
distanceVec2(member.position, add.position) < distanceVec2(member.position, best.position) ? add : best
), livingAdds[0])
}
return boss.health > 0 ? boss : undefined
const livingBosses = bosses.filter((boss) => boss.health > 0)
if (livingBosses.length === 0) return undefined
return livingBosses.reduce((best, boss) => (
distanceVec2(member.position, boss.position) < distanceVec2(member.position, best.position) ? boss : best
), livingBosses[0])
}
function getPrimaryBoss(bosses: Iwt2BossEntityState[]): Iwt2BossEntityState {
return bosses.find((boss) => boss.health > 0) ?? bosses[0]
}
function addDamageDone(