Android build v1.1.22
This commit is contained in:
+198
-13
@@ -14,6 +14,7 @@ import type {
|
||||
Iwt2PartyEntityState,
|
||||
Iwt2ProjectileEntityState,
|
||||
Iwt2StatusState,
|
||||
Iwt2Vec2,
|
||||
} from './types'
|
||||
import type { Iwt2PlayerClassId } from '../content/classes'
|
||||
import { tickBoss } from './bossAi'
|
||||
@@ -22,6 +23,7 @@ import { canPartyMemberHitTarget, tickPartyMember } from './partyAi'
|
||||
import { addFirePuddle, createHazardIndicators, tickGroundHazards } from './hazards'
|
||||
import { applyPartyDamageInShape } from './mechanics'
|
||||
import {
|
||||
addVec2,
|
||||
clampVec2ToArena,
|
||||
distanceVec2,
|
||||
normalizeVec2,
|
||||
@@ -35,7 +37,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']
|
||||
const IWT2_ARENA_BOSS_IDS = Object.keys(IWT2_BOSS_METADATA) as Iwt2BossId[]
|
||||
|
||||
type InitialPartyMember = {
|
||||
id: Iwt2PartyEntityId
|
||||
@@ -102,6 +104,10 @@ function createBossEntity(bossId: Iwt2BossId, index: number, healthScale: number
|
||||
radius: bossMetadata.radius,
|
||||
health: maxHealth,
|
||||
maxHealth,
|
||||
armor: bossMetadata.mudArmor ?? 0,
|
||||
maxArmor: bossMetadata.mudArmor ?? 0,
|
||||
mechanicEnergy: 0,
|
||||
mechanicEnergyMax: bossMetadata.staticEnergyMax ?? 0,
|
||||
meleeCooldownRemaining: 0.6 + index * 0.25,
|
||||
chargeCooldownRemaining: initialBossSpecialCooldown(bossId) + index * 0.7,
|
||||
chargeCount: 0,
|
||||
@@ -118,6 +124,8 @@ function createBossEntity(bossId: Iwt2BossId, index: number, healthScale: number
|
||||
birdWaveThresholdsTriggered: [],
|
||||
mechanicLanes: [],
|
||||
mechanicCircles: [],
|
||||
mechanicArcs: [],
|
||||
mechanicLinks: [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,12 +140,31 @@ function initialBossSpecialCooldown(bossId: Iwt2BossId): number {
|
||||
if (bossId === 'bulldrome') return 2
|
||||
if (bossId === 'great-jaggi') return 2.4
|
||||
if (bossId === 'khezu') return 3
|
||||
if (bossId === 'rathian') return 2.2
|
||||
if (bossId === 'barroth') return 2.1
|
||||
if (bossId === 'tobi-kadachi') return 2.6
|
||||
if (bossId === 'rimebastion') return 2.8
|
||||
if (bossId === 'ember-mantis-duelist') return 1.4
|
||||
if (bossId === 'cinderback-ricochet') return 2.5
|
||||
if (bossId === 'obsidian-ram-golem') return 2.2
|
||||
if (bossId === 'stormcoil-wyrm') return 2.6
|
||||
if (bossId === 'venom-orchid-hydra') return 2.4
|
||||
if (bossId === 'sandglass-scorpion') return 2.1
|
||||
if (bossId === 'crystal-bat-matriarch') return 2.3
|
||||
if (bossId === 'hollowcrown-revenant') return 1.8
|
||||
return 0
|
||||
}
|
||||
|
||||
function initialBossSecondaryCooldown(bossId: Iwt2BossId): number {
|
||||
if (bossId === 'yian-kut-ku') return 1.2
|
||||
if (bossId === 'khezu') return 1.6
|
||||
if (bossId === 'rathian') return 3
|
||||
if (bossId === 'tobi-kadachi') return 2.4
|
||||
if (bossId === 'ember-mantis-duelist') return 3
|
||||
if (bossId === 'stormcoil-wyrm') return 3.1
|
||||
if (bossId === 'venom-orchid-hydra') return 2.8
|
||||
if (bossId === 'sandglass-scorpion') return 3.2
|
||||
if (bossId === 'crystal-bat-matriarch') return 2.9
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -157,12 +184,23 @@ export function tickIwt2Arena(state: Iwt2ArenaState, input: Iwt2ArenaInput, dt:
|
||||
}
|
||||
const separatedState = {
|
||||
...baseState,
|
||||
party: separatePartyFromBosses(baseState.party, baseState),
|
||||
party: separatePartyFromBosses(
|
||||
separatePartyFromParty(separatePartyFromBosses(baseState.party, baseState), baseState),
|
||||
baseState,
|
||||
),
|
||||
}
|
||||
const bossResult = tickBosses(separatedState, step)
|
||||
const postBossState = { ...separatedState, bosses: bossResult.bosses }
|
||||
const postBossParty = separatePartyFromBosses(
|
||||
separatePartyFromParty(
|
||||
separatePartyFromBosses(bossResult.party, postBossState),
|
||||
postBossState,
|
||||
),
|
||||
postBossState,
|
||||
)
|
||||
const projectileResult = advanceProjectiles(
|
||||
[...separatedState.projectiles, ...(bossResult.projectiles ?? [])],
|
||||
bossResult.party,
|
||||
postBossParty,
|
||||
bossResult.bosses,
|
||||
bossResult.hostileAdds ?? separatedState.hostileAdds,
|
||||
bossResult.hazards ?? separatedState.hazards,
|
||||
@@ -186,7 +224,15 @@ export function tickIwt2Arena(state: Iwt2ArenaState, input: Iwt2ArenaInput, dt:
|
||||
projectileResult.nextProjectileId,
|
||||
)
|
||||
const hotResult = tickPartyHotEffects(damageResult.party, step, separatedState.time)
|
||||
const finalParty = regeneratePartyMana(hotResult.party, step)
|
||||
const regeneratedParty = regeneratePartyMana(hotResult.party, step)
|
||||
const finalCollisionState = { ...separatedState, bosses: damageResult.bosses }
|
||||
const finalParty = separatePartyFromBosses(
|
||||
separatePartyFromParty(
|
||||
separatePartyFromBosses(regeneratedParty, finalCollisionState),
|
||||
finalCollisionState,
|
||||
),
|
||||
finalCollisionState,
|
||||
)
|
||||
const nextEvents = assignEventIds(
|
||||
[...bossResult.events, ...projectileResult.events, ...hazardResult.events, ...damageResult.events, ...hotResult.events],
|
||||
state.nextEventId,
|
||||
@@ -296,6 +342,7 @@ function createEmptyStatus(): Iwt2StatusState {
|
||||
stunnedSeconds: 0,
|
||||
knockedDownSeconds: 0,
|
||||
invulnerableSeconds: 0,
|
||||
slowedSeconds: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,9 +387,10 @@ function tickBosses(state: Iwt2ArenaState, dt: number) {
|
||||
indicators.push(...result.indicators)
|
||||
}
|
||||
|
||||
const separatedBosses = separateBosses(bosses, state)
|
||||
return {
|
||||
bosses,
|
||||
boss: getPrimaryBoss(bosses),
|
||||
bosses: separatedBosses,
|
||||
boss: getPrimaryBoss(separatedBosses),
|
||||
party: nextParty,
|
||||
hostileAdds: nextHostileAdds,
|
||||
hazards: nextHazards,
|
||||
@@ -355,22 +403,129 @@ function tickBosses(state: Iwt2ArenaState, dt: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function separateBosses(bosses: Iwt2BossEntityState[], state: Iwt2ArenaState): Iwt2BossEntityState[] {
|
||||
const next = bosses.map((boss) => ({ ...boss, position: { ...boss.position } }))
|
||||
for (let pass = 0; pass < 3; pass += 1) {
|
||||
for (let firstIndex = 0; firstIndex < next.length; firstIndex += 1) {
|
||||
const first = next[firstIndex]
|
||||
if (!first || first.health <= 0) continue
|
||||
for (let secondIndex = firstIndex + 1; secondIndex < next.length; secondIndex += 1) {
|
||||
const second = next[secondIndex]
|
||||
if (!second || second.health <= 0) continue
|
||||
|
||||
const minDistance = first.radius + second.radius + 4
|
||||
const distance = distanceVec2(first.position, second.position)
|
||||
if (distance >= minDistance) continue
|
||||
|
||||
const fallback = {
|
||||
x: first.position.x <= second.position.x ? -1 : 1,
|
||||
y: first.position.y <= second.position.y ? -0.4 : 0.4,
|
||||
}
|
||||
const direction = distance <= 0.001 ? normalizeVec2(fallback) : normalizeVec2(subtractVec2(first.position, second.position))
|
||||
const push = (minDistance - distance) * 0.55
|
||||
const firstPosition = clampVec2ToArena(addVec2(first.position, scaleVec2(direction, push)), first.radius, state.bounds)
|
||||
const secondPosition = clampVec2ToArena(addVec2(second.position, scaleVec2(direction, -push)), second.radius, state.bounds)
|
||||
next[firstIndex] = {
|
||||
...first,
|
||||
position: firstPosition,
|
||||
velocity: scaleVec2(subtractVec2(firstPosition, first.position), 30),
|
||||
}
|
||||
next[secondIndex] = {
|
||||
...second,
|
||||
position: secondPosition,
|
||||
velocity: scaleVec2(subtractVec2(secondPosition, second.position), 30),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
function separatePartyFromBosses(party: Iwt2PartyEntityState[], state: Iwt2ArenaState): Iwt2PartyEntityState[] {
|
||||
return party.map((member) => {
|
||||
if (member.health <= 0) return member
|
||||
let position = member.position
|
||||
for (const boss of state.bosses) {
|
||||
if (boss.health <= 0) continue
|
||||
position = separateCircles(
|
||||
const separated = separateCircles(
|
||||
{ position, radius: member.radius },
|
||||
{ position: boss.position, radius: boss.radius },
|
||||
state.bounds,
|
||||
)
|
||||
position = wallClearance(separated, member, state) <= 2
|
||||
|| circleOverlapAmount({ position: separated, radius: member.radius }, boss) > 0.25
|
||||
? separateFromBossTowardCenter(member, boss, state)
|
||||
: separated
|
||||
}
|
||||
return { ...member, position }
|
||||
})
|
||||
}
|
||||
|
||||
function circleOverlapAmount(
|
||||
moving: { position: Iwt2Vec2, radius: number },
|
||||
fixed: { position: Iwt2Vec2, radius: number },
|
||||
): number {
|
||||
return Math.max(0, moving.radius + fixed.radius - distanceVec2(moving.position, fixed.position))
|
||||
}
|
||||
|
||||
function separateFromBossTowardCenter(
|
||||
member: Iwt2PartyEntityState,
|
||||
boss: Iwt2BossEntityState,
|
||||
state: Iwt2ArenaState,
|
||||
): Iwt2Vec2 {
|
||||
const center = { x: state.bounds.width * 0.5, y: state.bounds.height * 0.5 }
|
||||
const direction = normalizeVec2(subtractVec2(center, boss.position))
|
||||
const minDistance = boss.radius + member.radius + 3
|
||||
return clampVec2ToArena(
|
||||
addVec2(boss.position, scaleVec2(direction, minDistance)),
|
||||
member.radius,
|
||||
state.bounds,
|
||||
)
|
||||
}
|
||||
|
||||
function wallClearance(position: Iwt2Vec2, member: Iwt2PartyEntityState, state: Iwt2ArenaState): number {
|
||||
return Math.min(
|
||||
position.x - member.radius,
|
||||
state.bounds.width - member.radius - position.x,
|
||||
position.y - member.radius,
|
||||
state.bounds.height - member.radius - position.y,
|
||||
)
|
||||
}
|
||||
|
||||
function separatePartyFromParty(party: Iwt2PartyEntityState[], state: Iwt2ArenaState): Iwt2PartyEntityState[] {
|
||||
const next = party.map((member) => ({ ...member, position: { ...member.position } }))
|
||||
for (let pass = 0; pass < 2; pass += 1) {
|
||||
for (let firstIndex = 0; firstIndex < next.length; firstIndex += 1) {
|
||||
const first = next[firstIndex]
|
||||
if (!first || first.health <= 0) continue
|
||||
for (let secondIndex = firstIndex + 1; secondIndex < next.length; secondIndex += 1) {
|
||||
const second = next[secondIndex]
|
||||
if (!second || second.health <= 0) continue
|
||||
|
||||
const minDistance = first.radius + second.radius + 2
|
||||
const distance = distanceVec2(first.position, second.position)
|
||||
if (distance >= minDistance) continue
|
||||
|
||||
const fallback = {
|
||||
x: first.position.x <= second.position.x ? -1 : 1,
|
||||
y: first.position.y <= second.position.y ? -0.35 : 0.35,
|
||||
}
|
||||
const direction = distance <= 0.001 ? normalizeVec2(fallback) : normalizeVec2(subtractVec2(first.position, second.position))
|
||||
const push = (minDistance - distance) * 0.5
|
||||
next[firstIndex] = {
|
||||
...first,
|
||||
position: clampVec2ToArena(addVec2(first.position, scaleVec2(direction, push)), first.radius, state.bounds),
|
||||
}
|
||||
next[secondIndex] = {
|
||||
...second,
|
||||
position: clampVec2ToArena(addVec2(second.position, scaleVec2(direction, -push)), second.radius, state.bounds),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
function advanceProjectiles(
|
||||
projectiles: Iwt2ProjectileEntityState[],
|
||||
party: Iwt2PartyEntityState[],
|
||||
@@ -461,9 +616,10 @@ function advanceProjectiles(
|
||||
&& distanceVec2(nextPosition, boss.position) <= boss.radius + projectile.radius
|
||||
))
|
||||
if (hitBoss) {
|
||||
const damage = Math.min(projectile.damage, hitBoss.health)
|
||||
const damageResult = applyDamageToBoss(hitBoss, projectile.damage)
|
||||
const damage = damageResult.healthDamage
|
||||
nextBosses = nextBosses.map((boss) => boss.id === hitBoss.id
|
||||
? { ...boss, health: Math.max(0, boss.health - damage) }
|
||||
? damageResult.boss
|
||||
: boss)
|
||||
nextParty = addDamageDone(nextParty, projectile.sourceId, damage)
|
||||
events.push({
|
||||
@@ -474,7 +630,7 @@ function advanceProjectiles(
|
||||
targetId: hitBoss.id,
|
||||
value: damage,
|
||||
})
|
||||
if (hitBoss.health - damage <= 0) {
|
||||
if (damageResult.boss.health <= 0) {
|
||||
events.push({
|
||||
id: 0,
|
||||
time,
|
||||
@@ -673,10 +829,14 @@ function applyPartyAttacks(
|
||||
}
|
||||
}
|
||||
if (member.attackCooldownRemaining > 0) return member
|
||||
const damage = Math.min(metadata.attackDamage, target.health)
|
||||
let damage = Math.min(metadata.attackDamage, target.health)
|
||||
let defeated = target.health - damage <= 0
|
||||
if (target.kind === 'boss') {
|
||||
const damageResult = applyDamageToBoss(target, metadata.attackDamage)
|
||||
damage = damageResult.healthDamage
|
||||
defeated = damageResult.boss.health <= 0
|
||||
nextBosses = nextBosses.map((boss) => boss.id === target.id
|
||||
? { ...boss, health: Math.max(0, boss.health - damage) }
|
||||
? damageResult.boss
|
||||
: boss)
|
||||
} else {
|
||||
nextHostileAdds = nextHostileAdds.map((add) => add.id === target.id
|
||||
@@ -691,7 +851,7 @@ function applyPartyAttacks(
|
||||
targetId: target.id,
|
||||
value: damage,
|
||||
})
|
||||
if (target.health - damage <= 0) {
|
||||
if (defeated) {
|
||||
events.push({
|
||||
id: 0,
|
||||
time,
|
||||
@@ -754,6 +914,31 @@ function addDamageDone(
|
||||
: member)
|
||||
}
|
||||
|
||||
function applyDamageToBoss(
|
||||
boss: Iwt2BossEntityState,
|
||||
rawDamage: number,
|
||||
): { boss: Iwt2BossEntityState, healthDamage: number } {
|
||||
if (rawDamage <= 0 || boss.health <= 0) return { boss, healthDamage: 0 }
|
||||
if (boss.armor <= 0) {
|
||||
const healthDamage = Math.min(rawDamage, boss.health)
|
||||
return {
|
||||
boss: { ...boss, health: Math.max(0, boss.health - healthDamage) },
|
||||
healthDamage,
|
||||
}
|
||||
}
|
||||
|
||||
const armorDamage = Math.min(boss.armor, rawDamage * 0.75)
|
||||
const healthDamage = Math.min(boss.health, rawDamage - armorDamage * 0.6)
|
||||
return {
|
||||
boss: {
|
||||
...boss,
|
||||
armor: Math.max(0, boss.armor - armorDamage),
|
||||
health: Math.max(0, boss.health - healthDamage),
|
||||
},
|
||||
healthDamage,
|
||||
}
|
||||
}
|
||||
|
||||
function assignEventIds(events: Iwt2ArenaEvent[], firstId: number): Iwt2ArenaEvent[] {
|
||||
return events.map((event, index) => ({ ...event, id: firstId + index }))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user