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