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(
|
||||
|
||||
@@ -54,8 +54,8 @@ export type Iwt2ArenaState = Iwt2CoreArenaState & {
|
||||
telegraphs: Iwt2ArenaTelegraph[]
|
||||
}
|
||||
|
||||
export function createInitialIwt2ArenaState(bossId?: Iwt2BossId): Iwt2ArenaState {
|
||||
return decorateArenaState(createCoreIwt2ArenaState(bossId))
|
||||
export function createInitialIwt2ArenaState(bossId?: Iwt2BossId, bossIds?: Iwt2BossId[]): Iwt2ArenaState {
|
||||
return decorateArenaState(createCoreIwt2ArenaState(bossId, bossIds))
|
||||
}
|
||||
|
||||
export function tickIwt2Arena(state: Iwt2ArenaState, input: Iwt2ArenaInput, dt: number): Iwt2ArenaState {
|
||||
@@ -69,7 +69,7 @@ export function decorateArenaState(state: Iwt2CoreArenaState): Iwt2ArenaState {
|
||||
entities: [
|
||||
...state.party.map(toArenaEntity),
|
||||
...state.hostileAdds.map(toHostileAddArenaEntity),
|
||||
toBossArenaEntity(state.boss),
|
||||
...state.bosses.map(toBossArenaEntity),
|
||||
],
|
||||
telegraphs: createTelegraphs(state.indicators),
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ const CENTER_LEASH_BOSS_IDS = new Set(['great-jaggi', 'khezu'])
|
||||
const CENTER_LEASH_START_DISTANCE = 230
|
||||
const CENTER_LEASH_WALL_MARGIN = 96
|
||||
const CENTER_LEASH_EXTRA_DISTANCE = 36
|
||||
const PARTY_ARRIVAL_RADIUS = 3.5
|
||||
|
||||
export function tickPartyMember(
|
||||
member: Iwt2PartyEntityState,
|
||||
@@ -68,7 +69,7 @@ export function tickPartyMember(
|
||||
const canCast = member.aiRole === 'ranged'
|
||||
&& attackCooldownRemaining <= 0
|
||||
&& !!attackTarget
|
||||
&& canPartyMemberHitTarget({ ...member, attackCooldownRemaining, castSecondsRemaining }, attackTarget.position)
|
||||
&& canPartyMemberHitTarget({ ...member, attackCooldownRemaining, castSecondsRemaining }, attackTarget.position, attackTarget.radius)
|
||||
if (!dangerDestination && member.aiRole === 'ranged' && (member.castSecondsRemaining > 0 || canCast)) {
|
||||
const nextCastSecondsRemaining = member.castSecondsRemaining > 0
|
||||
? castSecondsRemaining
|
||||
@@ -76,7 +77,7 @@ export function tickPartyMember(
|
||||
return {
|
||||
...member,
|
||||
velocity: { x: 0, y: 0 },
|
||||
facing: withFallbackFacing(subtractVec2(attackTarget?.position ?? state.boss.position, member.position), member.facing),
|
||||
facing: withFallbackFacing(subtractVec2(attackTarget?.position ?? getPrimaryBoss(state).position, member.position), member.facing),
|
||||
attackCooldownRemaining,
|
||||
castSecondsRemaining: nextCastSecondsRemaining,
|
||||
attackReady: member.castSecondsRemaining > 0 && nextCastSecondsRemaining <= 0,
|
||||
@@ -85,15 +86,21 @@ export function tickPartyMember(
|
||||
}
|
||||
|
||||
const decisionSecondsRemaining = Math.max(0, member.decisionSecondsRemaining - dt)
|
||||
const desired = dangerDestination ?? getPartyDesiredPosition(member, state, decisionSecondsRemaining)
|
||||
const desired = dangerDestination ?? getPartyDesiredPosition(member, state)
|
||||
const maxDistance = metadata.moveSpeed * dt
|
||||
const position = clampVec2ToArena(moveToward(member.position, desired, maxDistance), member.radius, state.bounds)
|
||||
const velocity = scaleVec2(subtractVec2(position, member.position), dt > 0 ? 1 / dt : 0)
|
||||
const distanceToDesired = distanceVec2(member.position, desired)
|
||||
const movingToDesired = distanceToDesired > PARTY_ARRIVAL_RADIUS
|
||||
const position = movingToDesired
|
||||
? clampVec2ToArena(moveToward(member.position, desired, maxDistance), member.radius, state.bounds)
|
||||
: member.position
|
||||
const velocity = movingToDesired
|
||||
? scaleVec2(subtractVec2(position, member.position), dt > 0 ? 1 / dt : 0)
|
||||
: { x: 0, y: 0 }
|
||||
return {
|
||||
...member,
|
||||
position,
|
||||
velocity,
|
||||
facing: withFallbackFacing(subtractVec2(attackTarget?.position ?? state.boss.position, position), member.facing),
|
||||
facing: withFallbackFacing(subtractVec2(attackTarget?.position ?? getPrimaryBoss(state).position, position), member.facing),
|
||||
attackCooldownRemaining,
|
||||
castSecondsRemaining: 0,
|
||||
attackReady: false,
|
||||
@@ -104,22 +111,21 @@ export function tickPartyMember(
|
||||
}
|
||||
}
|
||||
|
||||
export function canPartyMemberHitTarget(member: Iwt2PartyEntityState, targetPosition: Iwt2Vec2): boolean {
|
||||
export function canPartyMemberHitTarget(member: Iwt2PartyEntityState, targetPosition: Iwt2Vec2, targetRadius = 0): boolean {
|
||||
if (member.health <= 0) return false
|
||||
if (member.status.stunnedSeconds > 0 || member.status.knockedDownSeconds > 0) return false
|
||||
const metadata = IWT2_CLASS_METADATA[member.classId]
|
||||
if (metadata.attackDamage <= 0) return false
|
||||
return distanceVec2(member.position, targetPosition) <= metadata.attackRange + member.radius
|
||||
return distanceVec2(member.position, targetPosition) <= metadata.attackRange + member.radius + targetRadius
|
||||
}
|
||||
|
||||
function getPartyDesiredPosition(
|
||||
member: Iwt2PartyEntityState,
|
||||
state: Iwt2ArenaState,
|
||||
decisionSecondsRemaining: number,
|
||||
): Iwt2Vec2 {
|
||||
const drift = decisionSecondsRemaining <= 0 ? decisionDrift(member, state.time) : { x: 0, y: 0 }
|
||||
const drift = decisionDrift(member, state.time)
|
||||
const attackTarget = getPriorityAttackTarget(member, state)
|
||||
const anchor = attackTarget?.position ?? state.boss.position
|
||||
const anchor = attackTarget?.position ?? getPrimaryBoss(state).position
|
||||
const centerLeash = getTankCenterLeashPosition(member, state)
|
||||
if (centerLeash) return centerLeash
|
||||
return {
|
||||
@@ -132,21 +138,22 @@ function getTankCenterLeashPosition(
|
||||
member: Iwt2PartyEntityState,
|
||||
state: Iwt2ArenaState,
|
||||
): Iwt2Vec2 | null {
|
||||
if (member.aiRole !== 'tank' || !CENTER_LEASH_BOSS_IDS.has(state.boss.bossId)) return null
|
||||
const boss = getPrimaryBoss(state)
|
||||
if (member.aiRole !== 'tank' || !CENTER_LEASH_BOSS_IDS.has(boss.bossId)) return null
|
||||
|
||||
const center = arenaCenter(state)
|
||||
const bossCenterDistance = distanceVec2(state.boss.position, center)
|
||||
const nearWall = isBossNearWall(state)
|
||||
const bossCenterDistance = distanceVec2(boss.position, center)
|
||||
const nearWall = isBossNearWall(boss, state)
|
||||
if (!nearWall && bossCenterDistance < CENTER_LEASH_START_DISTANCE) return null
|
||||
|
||||
const bossMetadata = IWT2_BOSS_METADATA[state.boss.bossId]
|
||||
const towardCenter = withFallbackFacing(subtractVec2(center, state.boss.position), {
|
||||
x: state.boss.position.x < center.x ? 1 : -1,
|
||||
y: state.boss.position.y < center.y ? 0.35 : -0.35,
|
||||
const bossMetadata = IWT2_BOSS_METADATA[boss.bossId]
|
||||
const towardCenter = withFallbackFacing(subtractVec2(center, boss.position), {
|
||||
x: boss.position.x < center.x ? 1 : -1,
|
||||
y: boss.position.y < center.y ? 0.35 : -0.35,
|
||||
})
|
||||
const leashDistance = bossMetadata.meleeRange + state.boss.radius + member.radius + CENTER_LEASH_EXTRA_DISTANCE
|
||||
const leashDistance = bossMetadata.meleeRange + boss.radius + member.radius + CENTER_LEASH_EXTRA_DISTANCE
|
||||
return clampVec2ToArena(
|
||||
addVec2(state.boss.position, scaleVec2(towardCenter, leashDistance)),
|
||||
addVec2(boss.position, scaleVec2(towardCenter, leashDistance)),
|
||||
member.radius,
|
||||
state.bounds,
|
||||
)
|
||||
@@ -159,39 +166,41 @@ function arenaCenter(state: Iwt2ArenaState): Iwt2Vec2 {
|
||||
}
|
||||
}
|
||||
|
||||
function isBossNearWall(state: Iwt2ArenaState): boolean {
|
||||
const margin = state.boss.radius + CENTER_LEASH_WALL_MARGIN
|
||||
function isBossNearWall(boss: Iwt2ArenaState['boss'], state: Iwt2ArenaState): boolean {
|
||||
const margin = boss.radius + CENTER_LEASH_WALL_MARGIN
|
||||
return (
|
||||
state.boss.position.x <= margin
|
||||
|| state.boss.position.x >= state.bounds.width - margin
|
||||
|| state.boss.position.y <= margin
|
||||
|| state.boss.position.y >= state.bounds.height - margin
|
||||
boss.position.x <= margin
|
||||
|| boss.position.x >= state.bounds.width - margin
|
||||
|| boss.position.y <= margin
|
||||
|| boss.position.y >= state.bounds.height - margin
|
||||
)
|
||||
}
|
||||
|
||||
function getDangerAvoidancePosition(member: Iwt2PartyEntityState, state: Iwt2ArenaState): Iwt2Vec2 | null {
|
||||
const boss = state.boss
|
||||
const hazardEscape = getHazardAvoidancePosition(member, state)
|
||||
if (hazardEscape) {
|
||||
return hazardEscape
|
||||
}
|
||||
|
||||
if (boss.bossId === 'bulldrome' && (boss.attackPhase === 'slamWindup' || boss.attackPhase === 'slamRecover')) {
|
||||
const distance = distanceVec2(member.position, boss.position)
|
||||
const dangerRadius = BULLDROME_BOSS_METADATA.slamRadius + member.radius + 34
|
||||
if (distance < dangerRadius) {
|
||||
const away = normalizeVec2(subtractVec2(member.position, boss.position))
|
||||
return clampVec2ToArena(addVec2(member.position, scaleVec2(away, dangerRadius - distance + 40)), member.radius, state.bounds)
|
||||
for (const boss of state.bosses) {
|
||||
if (boss.health <= 0) continue
|
||||
if (boss.bossId === 'bulldrome' && (boss.attackPhase === 'slamWindup' || boss.attackPhase === 'slamRecover')) {
|
||||
const distance = distanceVec2(member.position, boss.position)
|
||||
const dangerRadius = BULLDROME_BOSS_METADATA.slamRadius + member.radius + 34
|
||||
if (distance < dangerRadius) {
|
||||
const away = normalizeVec2(subtractVec2(member.position, boss.position))
|
||||
return clampVec2ToArena(addVec2(member.position, scaleVec2(away, dangerRadius - distance + 40)), member.radius, state.bounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (boss.bossId === 'bulldrome' && (boss.attackPhase === 'chargeWindup' || boss.attackPhase === 'charging')) {
|
||||
const danger = chargeDanger(member.position, state)
|
||||
if (danger.inside || danger.ahead) {
|
||||
const perpendicular = { x: -danger.direction.y, y: danger.direction.x }
|
||||
const side = dotVec2(subtractVec2(member.position, boss.chargeStart), perpendicular) >= 0 ? 1 : -1
|
||||
const escape = addVec2(member.position, scaleVec2(perpendicular, side * (boss.radius * 2.8 + member.radius)))
|
||||
return clampVec2ToArena(escape, member.radius, state.bounds)
|
||||
if (boss.bossId === 'bulldrome' && (boss.attackPhase === 'chargeWindup' || boss.attackPhase === 'charging')) {
|
||||
const danger = chargeDanger(member.position, boss)
|
||||
if (danger.inside || danger.ahead) {
|
||||
const perpendicular = { x: -danger.direction.y, y: danger.direction.x }
|
||||
const side = dotVec2(subtractVec2(member.position, boss.chargeStart), perpendicular) >= 0 ? 1 : -1
|
||||
const escape = addVec2(member.position, scaleVec2(perpendicular, side * (boss.radius * 2.8 + member.radius)))
|
||||
return clampVec2ToArena(escape, member.radius, state.bounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +217,7 @@ function getHazardAvoidancePosition(member: Iwt2PartyEntityState, state: Iwt2Are
|
||||
const dangerRadius = hazard.radius + member.radius + 46
|
||||
if (distance >= dangerRadius) continue
|
||||
|
||||
const fallback = withFallbackFacing(subtractVec2(member.position, state.boss.position), {
|
||||
const fallback = withFallbackFacing(subtractVec2(member.position, getPrimaryBoss(state).position), {
|
||||
x: member.position.x < state.bounds.width * 0.5 ? -1 : 1,
|
||||
y: member.position.y < state.bounds.height * 0.5 ? -0.35 : 0.35,
|
||||
})
|
||||
@@ -242,11 +251,14 @@ function getPriorityAttackTarget(
|
||||
distanceVec2(member.position, add.position) < distanceVec2(member.position, best.position) ? add : best
|
||||
), livingAdds[0])
|
||||
}
|
||||
return state.boss.health > 0 ? state.boss : undefined
|
||||
const livingBosses = state.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 chargeDanger(position: Iwt2Vec2, state: Iwt2ArenaState) {
|
||||
const boss = state.boss
|
||||
function chargeDanger(position: Iwt2Vec2, boss: Iwt2ArenaState['boss']) {
|
||||
const segment = subtractVec2(boss.chargeEnd, boss.chargeStart)
|
||||
const lengthSq = Math.max(1, lengthSqVec2(segment))
|
||||
const direction = normalizeVec2(segment)
|
||||
@@ -262,6 +274,10 @@ function chargeDanger(position: Iwt2Vec2, state: Iwt2ArenaState) {
|
||||
}
|
||||
}
|
||||
|
||||
function getPrimaryBoss(state: Iwt2ArenaState) {
|
||||
return state.bosses.find((boss) => boss.health > 0) ?? state.bosses[0] ?? state.boss
|
||||
}
|
||||
|
||||
function decisionDrift(member: Iwt2PartyEntityState, time: number): Iwt2Vec2 {
|
||||
const seed = member.id.length * 17
|
||||
return {
|
||||
|
||||
@@ -256,6 +256,7 @@ export type Iwt2ArenaState = {
|
||||
hostileAdds: Iwt2HostileAddState[]
|
||||
hazards: Iwt2GroundHazardState[]
|
||||
boss: Iwt2BossEntityState
|
||||
bosses: Iwt2BossEntityState[]
|
||||
indicators: Iwt2ArenaIndicator[]
|
||||
nextEventId: number
|
||||
nextProjectileId: number
|
||||
|
||||
Reference in New Issue
Block a user