Android build v1.1.22

This commit is contained in:
Warren H
2026-07-05 12:08:11 -04:00
parent 547044892d
commit 956c9e32f9
69 changed files with 9259 additions and 196 deletions
+209 -12
View File
@@ -1,6 +1,7 @@
import { IWT2_CLASS_METADATA } from '../content/classes'
import { BULLDROME_BOSS_METADATA, IWT2_BOSS_METADATA } from '../content/bosses'
import type { Iwt2ArenaState, Iwt2HostileAddState, Iwt2PartyEntityState, Iwt2Vec2 } from './types'
import { partyMemberIntersectsShape, type Iwt2HitShape } from './mechanics'
import {
addVec2,
clampVec2ToArena,
@@ -20,6 +21,12 @@ const CENTER_LEASH_WALL_MARGIN = 96
const CENTER_LEASH_EXTRA_DISTANCE = 36
const PARTY_ARRIVAL_RADIUS = 3.5
const PARTY_SAFE_DESTINATION_PADDING = 34
const PARTY_STEER_ANGLES = [0, 0.42, -0.42, 0.82, -0.82, 1.22, -1.22, 1.7, -1.7, 2.25, -2.25, Math.PI]
const HAZARD_ROUTE_BUFFER = 54
const INDICATOR_ROUTE_PENALTY = 24
const BOSS_ROUTE_BUFFER = 22
const PARTY_ROUTE_BUFFER = 8
const DANGER_CENTER_WEIGHT = 0.32
export function tickPartyMember(
member: Iwt2PartyEntityState,
@@ -32,6 +39,7 @@ export function tickPartyMember(
stunnedSeconds: Math.max(0, member.status.stunnedSeconds - dt),
knockedDownSeconds: Math.max(0, member.status.knockedDownSeconds - dt),
invulnerableSeconds: Math.max(0, member.status.invulnerableSeconds - dt),
slowedSeconds: Math.max(0, member.status.slowedSeconds - dt),
}
const attackCooldownRemaining = Math.max(0, member.attackCooldownRemaining - dt)
const castSecondsRemaining = Math.max(0, member.castSecondsRemaining - dt)
@@ -44,7 +52,7 @@ export function tickPartyMember(
if (member.aiRole === 'player') {
const normalizedInput = lengthSqVec2(inputMove) > 1 ? normalizeVec2(inputMove) : inputMove
const velocity = scaleVec2(normalizedInput, metadata.moveSpeed)
const velocity = scaleVec2(normalizedInput, metadata.moveSpeed * movementSpeedMultiplier(status.slowedSeconds))
const position = clampVec2ToArena(
{
x: member.position.x + velocity.x * dt,
@@ -88,11 +96,11 @@ export function tickPartyMember(
const decisionSecondsRemaining = Math.max(0, member.decisionSecondsRemaining - dt)
const desired = dangerDestination ?? getPartyDesiredPosition(member, state)
const maxDistance = metadata.moveSpeed * dt
const maxDistance = metadata.moveSpeed * movementSpeedMultiplier(status.slowedSeconds) * dt
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)
? choosePartyStepPosition(member, state, desired, maxDistance)
: member.position
const velocity = movingToDesired
? scaleVec2(subtractVec2(position, member.position), dt > 0 ? 1 / dt : 0)
@@ -112,6 +120,112 @@ export function tickPartyMember(
}
}
function choosePartyStepPosition(
member: Iwt2PartyEntityState,
state: Iwt2ArenaState,
desired: Iwt2Vec2,
maxDistance: number,
): Iwt2Vec2 {
if (maxDistance <= 0) return member.position
const direct = withFallbackFacing(subtractVec2(desired, member.position), member.facing)
const currentDanger = routeDangerPenalty(member.position, member, state)
let best = clampVec2ToArena(moveToward(member.position, desired, maxDistance), member.radius, state.bounds)
let bestScore = scoreRoutePosition(best, member, state, desired, currentDanger)
for (const angle of PARTY_STEER_ANGLES) {
const direction = rotateVec2(direct, angle)
const candidate = clampVec2ToArena(addVec2(member.position, scaleVec2(direction, maxDistance)), member.radius, state.bounds)
const score = scoreRoutePosition(candidate, member, state, desired, currentDanger)
+ (angle === 0 ? 0 : Math.abs(angle) * 1.8)
if (score < bestScore) {
best = candidate
bestScore = score
}
}
return best
}
function scoreRoutePosition(
position: Iwt2Vec2,
member: Iwt2PartyEntityState,
state: Iwt2ArenaState,
desired: Iwt2Vec2,
currentDanger: number,
): number {
const progress = distanceVec2(position, desired)
const centerPull = currentDanger > 0 ? distanceVec2(position, arenaCenter(state)) * DANGER_CENTER_WEIGHT : 0
const progressWeight = currentDanger > 0 ? 0.28 : 1
const danger = routeDangerPenalty(position, member, state)
const crowd = routeCrowdPenalty(position, member, state)
const wall = routeWallPenalty(position, member, state)
const stayingInDanger = currentDanger > 0 && distanceVec2(position, member.position) <= 0.75 ? 90 : 0
return progress * progressWeight + centerPull + danger * 100 + crowd * 42 + wall + stayingInDanger
}
function routeDangerPenalty(position: Iwt2Vec2, member: Iwt2PartyEntityState, state: Iwt2ArenaState): number {
let penalty = 0
for (const hazard of state.hazards) {
const distance = distanceVec2(position, hazard.position)
const damageRadius = hazard.radius + member.radius
const avoidRadius = damageRadius + HAZARD_ROUTE_BUFFER
if (distance <= damageRadius) {
penalty += 18 + (damageRadius - distance) * 0.7
} else if (distance < avoidRadius) {
const ratio = (avoidRadius - distance) / HAZARD_ROUTE_BUFFER
penalty += ratio * ratio * 8
}
}
for (const indicator of state.indicators) {
if (indicator.phase === 'recover') continue
const shape = indicatorAvoidanceShape(indicator)
if (!partyMemberIntersectsShape({ ...member, position }, shape)) continue
penalty += indicator.phase === 'active' ? INDICATOR_ROUTE_PENALTY * 1.4 : INDICATOR_ROUTE_PENALTY
}
return penalty
}
function routeCrowdPenalty(position: Iwt2Vec2, member: Iwt2PartyEntityState, state: Iwt2ArenaState): number {
let penalty = 0
for (const boss of state.bosses) {
if (boss.health <= 0) continue
const distance = distanceVec2(position, boss.position)
const avoidRadius = boss.radius + member.radius + BOSS_ROUTE_BUFFER
if (distance < avoidRadius) penalty += (avoidRadius - distance) * 1.8
}
for (const other of state.party) {
if (other.id === member.id || other.health <= 0) continue
const distance = distanceVec2(position, other.position)
const avoidRadius = other.radius + member.radius + PARTY_ROUTE_BUFFER
if (distance < avoidRadius) penalty += avoidRadius - distance
}
return penalty
}
function routeWallPenalty(position: Iwt2Vec2, member: Iwt2PartyEntityState, state: Iwt2ArenaState): number {
const clearance = Math.min(
position.x - member.radius,
state.bounds.width - member.radius - position.x,
position.y - member.radius,
state.bounds.height - member.radius - position.y,
)
if (clearance >= PARTY_SAFE_DESTINATION_PADDING) return 0
return (PARTY_SAFE_DESTINATION_PADDING - clearance) * (clearance <= 4 ? 12 : 4.8)
}
function rotateVec2(vector: Iwt2Vec2, angle: number): Iwt2Vec2 {
const cos = Math.cos(angle)
const sin = Math.sin(angle)
return {
x: vector.x * cos - vector.y * sin,
y: vector.x * sin + vector.y * cos,
}
}
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
@@ -158,10 +272,10 @@ function getTankCenterLeashPosition(
y: boss.position.y < center.y ? 0.35 : -0.35,
})
const leashDistance = bossMetadata.meleeRange + boss.radius + member.radius + CENTER_LEASH_EXTRA_DISTANCE
return clampVec2ToArena(
return clampPartyDestination(
addVec2(boss.position, scaleVec2(towardCenter, leashDistance)),
member.radius,
state.bounds,
member,
state,
)
}
@@ -198,6 +312,11 @@ function isBossNearWall(boss: Iwt2ArenaState['boss'], state: Iwt2ArenaState): bo
}
function getDangerAvoidancePosition(member: Iwt2PartyEntityState, state: Iwt2ArenaState): Iwt2Vec2 | null {
const indicatorEscape = getIndicatorAvoidancePosition(member, state)
if (indicatorEscape) {
return indicatorEscape
}
const hazardEscape = getHazardAvoidancePosition(member, state)
if (hazardEscape) {
return hazardEscape
@@ -210,7 +329,7 @@ function getDangerAvoidancePosition(member: Iwt2PartyEntityState, state: Iwt2Are
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)
return clampPartyDestination(addVec2(member.position, scaleVec2(away, dangerRadius - distance + 40)), member, state)
}
}
@@ -220,7 +339,7 @@ function getDangerAvoidancePosition(member: Iwt2PartyEntityState, state: Iwt2Are
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)
return clampPartyDestination(escape, member, state)
}
}
}
@@ -228,6 +347,65 @@ function getDangerAvoidancePosition(member: Iwt2PartyEntityState, state: Iwt2Are
return null
}
function getIndicatorAvoidancePosition(member: Iwt2PartyEntityState, state: Iwt2ArenaState): Iwt2Vec2 | null {
let escape = { x: 0, y: 0 }
let pressure = 0
for (const indicator of state.indicators) {
if (indicator.phase === 'recover') continue
const shape = indicatorAvoidanceShape(indicator)
if (!partyMemberIntersectsShape(member, shape)) continue
const origin = indicator.kind === 'lane'
? closestPointOnSegment(member.position, indicator.start, indicator.end)
: indicator.kind === 'cone'
? indicator.origin
: indicator.position
const away = withFallbackFacing(subtractVec2(member.position, origin), {
x: member.position.x < state.bounds.width * 0.5 ? -1 : 1,
y: member.position.y < state.bounds.height * 0.5 ? -0.35 : 0.35,
})
escape = addVec2(escape, scaleVec2(away, indicator.phase === 'active' ? 96 : 72))
pressure += indicator.phase === 'active' ? 1.35 : 1
}
if (pressure <= 0) return null
return clampPartyDestination(addVec2(member.position, escape), member, state)
}
function indicatorAvoidanceShape(indicator: Iwt2ArenaState['indicators'][number]): Iwt2HitShape {
if (indicator.kind === 'lane') {
return { kind: 'lane', start: indicator.start, end: indicator.end, width: indicator.width + 22 }
}
if (indicator.kind === 'circle') {
return { kind: 'circle', position: indicator.position, radius: indicator.radius + 18 }
}
if (indicator.kind === 'donut') {
return {
kind: 'donut',
innerRadius: indicator.innerRadius,
outerRadius: indicator.outerRadius + 18,
position: indicator.position,
}
}
if (indicator.kind === 'arc') {
return {
kind: 'arc',
angleRadians: indicator.angleRadians,
direction: indicator.direction,
innerRadius: Math.max(0, indicator.innerRadius - 8),
outerRadius: indicator.outerRadius + 24,
position: indicator.position,
}
}
return {
kind: 'cone',
angleRadians: indicator.angleRadians,
direction: indicator.direction,
origin: indicator.origin,
range: indicator.range + 24,
}
}
function getHazardAvoidancePosition(member: Iwt2PartyEntityState, state: Iwt2ArenaState): Iwt2Vec2 | null {
let escape = { x: 0, y: 0 }
let maxNeededDistance = 0
@@ -254,10 +432,18 @@ function getHazardAvoidancePosition(member: Iwt2PartyEntityState, state: Iwt2Are
x: member.position.x < state.bounds.width * 0.5 ? -1 : 1,
y: 0,
})
return clampVec2ToArena(
addVec2(member.position, scaleVec2(direction, maxNeededDistance + 72)),
member.radius,
state.bounds,
const destination = clampPartyDestination(
addVec2(member.position, scaleVec2(direction, maxNeededDistance + 96)),
member,
state,
)
if (distanceVec2(destination, member.position) > PARTY_ARRIVAL_RADIUS) return destination
const centerDirection = withFallbackFacing(subtractVec2(arenaCenter(state), member.position), direction)
return clampPartyDestination(
addVec2(member.position, scaleVec2(centerDirection, maxNeededDistance + 120)),
member,
state,
)
}
@@ -295,6 +481,17 @@ function chargeDanger(position: Iwt2Vec2, boss: Iwt2ArenaState['boss']) {
}
}
function closestPointOnSegment(position: Iwt2Vec2, start: Iwt2Vec2, end: Iwt2Vec2): Iwt2Vec2 {
const segment = subtractVec2(end, start)
const lengthSq = Math.max(1, lengthSqVec2(segment))
const t = Math.min(1, Math.max(0, dotVec2(subtractVec2(position, start), segment) / lengthSq))
return addVec2(start, scaleVec2(segment, t))
}
function movementSpeedMultiplier(slowedSeconds: number): number {
return slowedSeconds > 0 ? 0.58 : 1
}
function getPrimaryBoss(state: Iwt2ArenaState) {
return state.bosses.find((boss) => boss.health > 0) ?? state.bosses[0] ?? state.boss
}