Android build v1.1.22
This commit is contained in:
@@ -0,0 +1,605 @@
|
||||
import type { Iwt2BossTickResult } from './bossAi'
|
||||
import type {
|
||||
Iwt2ArenaEvent,
|
||||
Iwt2ArenaIndicator,
|
||||
Iwt2ArenaState,
|
||||
Iwt2BossEntityState,
|
||||
Iwt2EntityId,
|
||||
Iwt2GroundHazardState,
|
||||
Iwt2PartyEntityState,
|
||||
Iwt2Vec2,
|
||||
} from './types'
|
||||
import { addFirePuddle } from './hazards'
|
||||
import {
|
||||
applyPartyDamageInShape,
|
||||
createArenaEvent,
|
||||
createCircleIndicator,
|
||||
createLaneIndicator,
|
||||
indicatorPhaseFromAttack,
|
||||
} from './mechanics'
|
||||
import {
|
||||
addVec2,
|
||||
clampVec2ToArena,
|
||||
distanceVec2,
|
||||
normalizeVec2,
|
||||
scaleVec2,
|
||||
subtractVec2,
|
||||
withFallbackFacing,
|
||||
} from './vector'
|
||||
|
||||
const CINDERBACK_PHASE = {
|
||||
armorSlamRecover: 'cinderbackArmorSlamRecover',
|
||||
armorSlamWindup: 'cinderbackArmorSlamWindup',
|
||||
ricochetWindup: 'cinderbackRicochetWindup',
|
||||
ricocheting: 'cinderbackRicocheting',
|
||||
} as const
|
||||
|
||||
type CinderbackPhase = typeof CINDERBACK_PHASE[keyof typeof CINDERBACK_PHASE]
|
||||
|
||||
const CINDERBACK_TUNING = {
|
||||
armorSlamDamage: 20,
|
||||
armorSlamRadius: 118,
|
||||
armorSlamRecover: 1.05,
|
||||
armorSlamStunSeconds: 0.7,
|
||||
armorSlamWindup: 0.62,
|
||||
centerDisengageSpeed: 152,
|
||||
firePuddleDamage: 5,
|
||||
firePuddleRadius: 34,
|
||||
firePuddleSeconds: 6.4,
|
||||
lavaTrailInterval: 0.26,
|
||||
meleeCooldown: 1.12,
|
||||
meleeDamage: 5,
|
||||
meleeRange: 58,
|
||||
moveSpeed: 104,
|
||||
ricochetCooldown: 7.2,
|
||||
ricochetDamage: 18,
|
||||
ricochetDuration: 3.1,
|
||||
ricochetMaxBounces: 4,
|
||||
ricochetSpeed: 470,
|
||||
ricochetStunSeconds: 0.42,
|
||||
ricochetWidth: 46,
|
||||
ricochetWindup: 0.72,
|
||||
wallContactLimit: 0.48,
|
||||
wallMargin: 64,
|
||||
}
|
||||
|
||||
export function tickCinderbackRicochet(state: Iwt2ArenaState, dt: number): Iwt2BossTickResult {
|
||||
const events: Iwt2ArenaEvent[] = []
|
||||
let hazards = state.hazards
|
||||
let nextHazardId = state.nextHazardId
|
||||
let party = state.party
|
||||
const incomingPhase = cinderbackPhase(state.boss)
|
||||
let boss = {
|
||||
...state.boss,
|
||||
chargeCooldownRemaining: Math.max(0, state.boss.chargeCooldownRemaining - dt),
|
||||
fireballCooldownRemaining: Math.max(0, state.boss.fireballCooldownRemaining - dt),
|
||||
meleeCooldownRemaining: Math.max(0, state.boss.meleeCooldownRemaining - dt),
|
||||
phaseSecondsRemaining: Math.max(0, state.boss.phaseSecondsRemaining - dt),
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
boss = {
|
||||
...boss,
|
||||
wallContactSeconds: incomingPhase === CINDERBACK_PHASE.ricocheting
|
||||
|| incomingPhase === CINDERBACK_PHASE.armorSlamWindup
|
||||
|| incomingPhase === CINDERBACK_PHASE.armorSlamRecover
|
||||
? 0
|
||||
: isBossNearWall(boss, state)
|
||||
? boss.wallContactSeconds + dt
|
||||
: Math.max(0, boss.wallContactSeconds - dt * 2),
|
||||
}
|
||||
|
||||
const target = getBossTarget(party)
|
||||
if (boss.health <= 0 || !target) {
|
||||
return withCinderbackIndicators({ boss, party, hazards, events, nextHazardId })
|
||||
}
|
||||
|
||||
const phase = cinderbackPhase(boss)
|
||||
if (phase === CINDERBACK_PHASE.ricochetWindup) {
|
||||
boss = {
|
||||
...boss,
|
||||
facing: withFallbackFacing(subtractVec2(boss.chargeEnd, boss.position), boss.facing),
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
if (boss.phaseSecondsRemaining <= 0) {
|
||||
const facing = wallSafeRollDirection(boss, target.position, state)
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: asBossPhase(CINDERBACK_PHASE.ricocheting),
|
||||
chargeEnd: nextWallImpact(boss.position, facing, boss.radius, state),
|
||||
chargeHitEntityIds: [],
|
||||
chargeStart: { ...boss.position },
|
||||
facing,
|
||||
fireballCooldownRemaining: 0,
|
||||
mechanicEnergy: 0,
|
||||
phaseSecondsRemaining: CINDERBACK_TUNING.ricochetDuration,
|
||||
}
|
||||
}
|
||||
return withCinderbackIndicators({ boss, party, hazards, events, nextHazardId })
|
||||
}
|
||||
|
||||
if (phase === CINDERBACK_PHASE.ricocheting) {
|
||||
const roll = moveRicochet(boss, state, dt)
|
||||
const hitResult = applyRicochetHits(party, boss, boss.position, roll.position, state.time + dt)
|
||||
party = hitResult.party
|
||||
events.push(...hitResult.events)
|
||||
|
||||
const trailResult = maybeAddLavaTrail({
|
||||
boss,
|
||||
hazards,
|
||||
nextHazardId,
|
||||
position: roll.position,
|
||||
time: state.time + dt,
|
||||
})
|
||||
hazards = trailResult.hazards
|
||||
nextHazardId = trailResult.nextHazardId
|
||||
|
||||
const bounceCount = boss.mechanicEnergy + roll.bounces
|
||||
const wallContactSeconds = roll.bounces > 0 || roll.cornerBounce
|
||||
? 0
|
||||
: isBossNearWall({ ...boss, position: roll.position }, state)
|
||||
? boss.wallContactSeconds + dt
|
||||
: Math.max(0, boss.wallContactSeconds - dt * 2)
|
||||
boss = {
|
||||
...boss,
|
||||
chargeEnd: nextWallImpact(roll.position, roll.facing, boss.radius, state),
|
||||
chargeHitEntityIds: hitResult.hitEntityIds,
|
||||
chargeStart: { ...roll.position },
|
||||
facing: roll.facing,
|
||||
fireballCooldownRemaining: trailResult.nextTrailInSeconds,
|
||||
mechanicEnergy: bounceCount,
|
||||
position: roll.position,
|
||||
velocity: roll.velocity,
|
||||
wallContactSeconds,
|
||||
}
|
||||
|
||||
if (
|
||||
boss.phaseSecondsRemaining <= 0
|
||||
|| bounceCount >= CINDERBACK_TUNING.ricochetMaxBounces
|
||||
|| wallContactSeconds >= CINDERBACK_TUNING.wallContactLimit
|
||||
|| roll.cornerBounce
|
||||
) {
|
||||
boss = beginArmorSlam(boss, state)
|
||||
}
|
||||
return withCinderbackIndicators({ boss, party, hazards, events, nextHazardId })
|
||||
}
|
||||
|
||||
if (phase === CINDERBACK_PHASE.armorSlamWindup) {
|
||||
boss = { ...boss, velocity: { x: 0, y: 0 } }
|
||||
if (boss.phaseSecondsRemaining <= 0) {
|
||||
const slamResult = applyArmorSlam(party, boss, state.time + dt)
|
||||
const puddleResult = addArmorSlamPuddles({
|
||||
boss,
|
||||
hazards,
|
||||
nextHazardId,
|
||||
state,
|
||||
time: state.time + dt,
|
||||
})
|
||||
party = slamResult.party
|
||||
hazards = puddleResult.hazards
|
||||
nextHazardId = puddleResult.nextHazardId
|
||||
events.push(...slamResult.events)
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: asBossPhase(CINDERBACK_PHASE.armorSlamRecover),
|
||||
mechanicEnergy: 0,
|
||||
phaseSecondsRemaining: CINDERBACK_TUNING.armorSlamRecover,
|
||||
}
|
||||
}
|
||||
return withCinderbackIndicators({ boss, party, hazards, events, nextHazardId })
|
||||
}
|
||||
|
||||
if (phase === CINDERBACK_PHASE.armorSlamRecover) {
|
||||
boss = { ...boss, velocity: { x: 0, y: 0 } }
|
||||
if (boss.phaseSecondsRemaining <= 0) {
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: 'idle',
|
||||
chargeCooldownRemaining: Math.max(boss.chargeCooldownRemaining, 1.4),
|
||||
phaseSecondsRemaining: 0,
|
||||
}
|
||||
}
|
||||
return withCinderbackIndicators({ boss, party, hazards, events, nextHazardId })
|
||||
}
|
||||
|
||||
if (boss.wallContactSeconds >= 0.68) {
|
||||
boss = moveBossTowardCenter(boss, state, CINDERBACK_TUNING.centerDisengageSpeed, dt)
|
||||
return withCinderbackIndicators({ boss, party, hazards, events, nextHazardId })
|
||||
}
|
||||
|
||||
if (boss.chargeCooldownRemaining <= 0) {
|
||||
const facing = wallSafeRollDirection(boss, target.position, state)
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: asBossPhase(CINDERBACK_PHASE.ricochetWindup),
|
||||
chargeCooldownRemaining: CINDERBACK_TUNING.ricochetCooldown,
|
||||
chargeEnd: nextWallImpact(boss.position, facing, boss.radius, state),
|
||||
chargeHitEntityIds: [],
|
||||
chargeStart: { ...boss.position },
|
||||
facing,
|
||||
phaseSecondsRemaining: CINDERBACK_TUNING.ricochetWindup,
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
events.push(createArenaEvent(0, state.time + dt, 'bossChargeStart', boss.id, target.id))
|
||||
return withCinderbackIndicators({ boss, party, hazards, events, nextHazardId })
|
||||
}
|
||||
|
||||
const meleeResult = maybeApplyMelee(party, boss, target, state.time + dt)
|
||||
party = meleeResult.party
|
||||
events.push(...meleeResult.events)
|
||||
boss = meleeResult.boss
|
||||
if (events.length > 0) return withCinderbackIndicators({ boss, party, hazards, events, nextHazardId })
|
||||
|
||||
const nextPosition = clampVec2ToArena(
|
||||
moveTowardKeepingDistance(boss.position, target.position, CINDERBACK_TUNING.moveSpeed * dt),
|
||||
boss.radius,
|
||||
state.bounds,
|
||||
)
|
||||
boss = {
|
||||
...boss,
|
||||
facing: withFallbackFacing(subtractVec2(target.position, nextPosition), boss.facing),
|
||||
position: nextPosition,
|
||||
velocity: scaleVec2(subtractVec2(nextPosition, boss.position), dt > 0 ? 1 / dt : 0),
|
||||
}
|
||||
return withCinderbackIndicators({ boss, party, hazards, events, nextHazardId })
|
||||
}
|
||||
|
||||
function beginArmorSlam(boss: Iwt2BossEntityState, state: Iwt2ArenaState): Iwt2BossEntityState {
|
||||
const position = clampVec2ToArena(disengageFromWall(boss.position, boss.radius, state), boss.radius, state.bounds)
|
||||
return {
|
||||
...boss,
|
||||
attackPhase: asBossPhase(CINDERBACK_PHASE.armorSlamWindup),
|
||||
chargeEnd: { ...position },
|
||||
chargeStart: { ...position },
|
||||
mechanicEnergy: 0,
|
||||
phaseSecondsRemaining: CINDERBACK_TUNING.armorSlamWindup,
|
||||
position,
|
||||
velocity: { x: 0, y: 0 },
|
||||
wallContactSeconds: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function moveRicochet(
|
||||
boss: Iwt2BossEntityState,
|
||||
state: Iwt2ArenaState,
|
||||
dt: number,
|
||||
): { bounces: number, cornerBounce: boolean, facing: Iwt2Vec2, position: Iwt2Vec2, velocity: Iwt2Vec2 } {
|
||||
const minX = boss.radius
|
||||
const maxX = state.bounds.width - boss.radius
|
||||
const minY = boss.radius
|
||||
const maxY = state.bounds.height - boss.radius
|
||||
let facing = withFallbackFacing(boss.facing, { x: 1, y: 0 })
|
||||
let position = addVec2(boss.position, scaleVec2(facing, CINDERBACK_TUNING.ricochetSpeed * dt))
|
||||
let xBounce = false
|
||||
let yBounce = false
|
||||
|
||||
if (position.x < minX) {
|
||||
position = { ...position, x: minX + (minX - position.x) }
|
||||
facing = { ...facing, x: Math.abs(facing.x) }
|
||||
xBounce = true
|
||||
} else if (position.x > maxX) {
|
||||
position = { ...position, x: maxX - (position.x - maxX) }
|
||||
facing = { ...facing, x: -Math.abs(facing.x) }
|
||||
xBounce = true
|
||||
}
|
||||
|
||||
if (position.y < minY) {
|
||||
position = { ...position, y: minY + (minY - position.y) }
|
||||
facing = { ...facing, y: Math.abs(facing.y) }
|
||||
yBounce = true
|
||||
} else if (position.y > maxY) {
|
||||
position = { ...position, y: maxY - (position.y - maxY) }
|
||||
facing = { ...facing, y: -Math.abs(facing.y) }
|
||||
yBounce = true
|
||||
}
|
||||
|
||||
position = clampVec2ToArena(position, boss.radius, state.bounds)
|
||||
const cornerBounce = xBounce && yBounce
|
||||
if (cornerBounce) {
|
||||
facing = withFallbackFacing(subtractVec2(arenaCenter(state), position), facing)
|
||||
position = disengageFromWall(position, boss.radius, state)
|
||||
}
|
||||
|
||||
facing = normalizeVec2(facing)
|
||||
return {
|
||||
bounces: Number(xBounce) + Number(yBounce),
|
||||
cornerBounce,
|
||||
facing,
|
||||
position,
|
||||
velocity: scaleVec2(subtractVec2(position, boss.position), dt > 0 ? 1 / dt : 0),
|
||||
}
|
||||
}
|
||||
|
||||
function maybeAddLavaTrail({
|
||||
boss,
|
||||
hazards,
|
||||
nextHazardId,
|
||||
position,
|
||||
time,
|
||||
}: {
|
||||
boss: Iwt2BossEntityState
|
||||
hazards: Iwt2GroundHazardState[]
|
||||
nextHazardId: number
|
||||
position: Iwt2Vec2
|
||||
time: number
|
||||
}): { hazards: Iwt2GroundHazardState[], nextHazardId: number, nextTrailInSeconds: number } {
|
||||
if (boss.fireballCooldownRemaining > 0) {
|
||||
return { hazards, nextHazardId, nextTrailInSeconds: boss.fireballCooldownRemaining }
|
||||
}
|
||||
const result = addFirePuddle({
|
||||
damage: CINDERBACK_TUNING.firePuddleDamage,
|
||||
duration: CINDERBACK_TUNING.firePuddleSeconds,
|
||||
hazards,
|
||||
nextHazardId,
|
||||
position,
|
||||
radius: CINDERBACK_TUNING.firePuddleRadius,
|
||||
sourceId: boss.id,
|
||||
time,
|
||||
})
|
||||
return {
|
||||
hazards: result.hazards,
|
||||
nextHazardId: result.nextHazardId,
|
||||
nextTrailInSeconds: CINDERBACK_TUNING.lavaTrailInterval,
|
||||
}
|
||||
}
|
||||
|
||||
function addArmorSlamPuddles({
|
||||
boss,
|
||||
hazards,
|
||||
nextHazardId,
|
||||
state,
|
||||
time,
|
||||
}: {
|
||||
boss: Iwt2BossEntityState
|
||||
hazards: Iwt2GroundHazardState[]
|
||||
nextHazardId: number
|
||||
state: Iwt2ArenaState
|
||||
time: number
|
||||
}): { hazards: Iwt2GroundHazardState[], nextHazardId: number } {
|
||||
let nextHazards = hazards
|
||||
let nextId = nextHazardId
|
||||
const angles = [0, (Math.PI * 2) / 3, (Math.PI * 4) / 3]
|
||||
for (const angle of angles) {
|
||||
const position = clampVec2ToArena({
|
||||
x: boss.position.x + Math.cos(angle) * CINDERBACK_TUNING.armorSlamRadius * 0.72,
|
||||
y: boss.position.y + Math.sin(angle) * CINDERBACK_TUNING.armorSlamRadius * 0.72,
|
||||
}, CINDERBACK_TUNING.firePuddleRadius, state.bounds)
|
||||
const result = addFirePuddle({
|
||||
damage: CINDERBACK_TUNING.firePuddleDamage,
|
||||
duration: CINDERBACK_TUNING.firePuddleSeconds,
|
||||
hazards: nextHazards,
|
||||
nextHazardId: nextId,
|
||||
position,
|
||||
radius: CINDERBACK_TUNING.firePuddleRadius,
|
||||
sourceId: boss.id,
|
||||
time,
|
||||
})
|
||||
nextHazards = result.hazards
|
||||
nextId = result.nextHazardId
|
||||
}
|
||||
return { hazards: nextHazards, nextHazardId: nextId }
|
||||
}
|
||||
|
||||
function applyRicochetHits(
|
||||
party: Iwt2PartyEntityState[],
|
||||
boss: Iwt2BossEntityState,
|
||||
start: Iwt2Vec2,
|
||||
end: Iwt2Vec2,
|
||||
time: number,
|
||||
): { party: Iwt2PartyEntityState[], hitEntityIds: Iwt2EntityId[], events: Iwt2ArenaEvent[] } {
|
||||
const result = applyPartyDamageInShape(party, {
|
||||
end,
|
||||
kind: 'lane',
|
||||
start,
|
||||
width: CINDERBACK_TUNING.ricochetWidth,
|
||||
}, {
|
||||
damage: CINDERBACK_TUNING.ricochetDamage,
|
||||
damageEventType: 'bossChargeHit',
|
||||
excludedEntityIds: boss.chargeHitEntityIds,
|
||||
knockdownSeconds: CINDERBACK_TUNING.ricochetStunSeconds,
|
||||
sourceId: boss.id,
|
||||
stunSeconds: CINDERBACK_TUNING.ricochetStunSeconds,
|
||||
time,
|
||||
})
|
||||
return {
|
||||
...result,
|
||||
hitEntityIds: [...boss.chargeHitEntityIds, ...result.hitEntityIds],
|
||||
}
|
||||
}
|
||||
|
||||
function applyArmorSlam(
|
||||
party: Iwt2PartyEntityState[],
|
||||
boss: Iwt2BossEntityState,
|
||||
time: number,
|
||||
): { party: Iwt2PartyEntityState[], events: Iwt2ArenaEvent[] } {
|
||||
const result = applyPartyDamageInShape(party, {
|
||||
kind: 'circle',
|
||||
position: boss.position,
|
||||
radius: CINDERBACK_TUNING.armorSlamRadius,
|
||||
}, {
|
||||
damage: CINDERBACK_TUNING.armorSlamDamage,
|
||||
knockdownSeconds: CINDERBACK_TUNING.armorSlamStunSeconds,
|
||||
sourceId: boss.id,
|
||||
stunSeconds: CINDERBACK_TUNING.armorSlamStunSeconds,
|
||||
time,
|
||||
})
|
||||
return {
|
||||
party: result.party,
|
||||
events: [
|
||||
createArenaEvent(0, time, 'bossSlam', boss.id, undefined, CINDERBACK_TUNING.armorSlamRadius),
|
||||
...result.events,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function maybeApplyMelee(
|
||||
party: Iwt2PartyEntityState[],
|
||||
boss: Iwt2BossEntityState,
|
||||
target: Iwt2PartyEntityState,
|
||||
time: number,
|
||||
): { boss: Iwt2BossEntityState, party: Iwt2PartyEntityState[], events: Iwt2ArenaEvent[] } {
|
||||
if (boss.meleeCooldownRemaining > 0) return { boss, party, events: [] }
|
||||
if (distanceVec2(boss.position, target.position) > CINDERBACK_TUNING.meleeRange + target.radius) {
|
||||
return { boss, party, events: [] }
|
||||
}
|
||||
const result = applyPartyDamageInShape(party, {
|
||||
kind: 'circle',
|
||||
position: boss.position,
|
||||
radius: CINDERBACK_TUNING.meleeRange,
|
||||
}, {
|
||||
damage: CINDERBACK_TUNING.meleeDamage,
|
||||
sourceId: boss.id,
|
||||
time,
|
||||
})
|
||||
return {
|
||||
boss: { ...boss, meleeCooldownRemaining: CINDERBACK_TUNING.meleeCooldown, velocity: { x: 0, y: 0 } },
|
||||
events: result.events,
|
||||
party: result.party,
|
||||
}
|
||||
}
|
||||
|
||||
function moveBossTowardCenter(
|
||||
boss: Iwt2BossEntityState,
|
||||
state: Iwt2ArenaState,
|
||||
speed: number,
|
||||
dt: number,
|
||||
): Iwt2BossEntityState {
|
||||
const center = arenaCenter(state)
|
||||
const nextPosition = clampVec2ToArena(
|
||||
moveTowardKeepingDistance(boss.position, center, speed * dt),
|
||||
boss.radius,
|
||||
state.bounds,
|
||||
)
|
||||
return {
|
||||
...boss,
|
||||
facing: withFallbackFacing(subtractVec2(center, nextPosition), boss.facing),
|
||||
position: nextPosition,
|
||||
velocity: scaleVec2(subtractVec2(nextPosition, boss.position), dt > 0 ? 1 / dt : 0),
|
||||
wallContactSeconds: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function moveTowardKeepingDistance(current: Iwt2Vec2, target: Iwt2Vec2, maxDistance: number): Iwt2Vec2 {
|
||||
const offset = subtractVec2(target, current)
|
||||
const distance = Math.hypot(offset.x, offset.y)
|
||||
if (distance <= maxDistance || distance <= 0.0001) return { ...target }
|
||||
return addVec2(current, scaleVec2(offset, maxDistance / distance))
|
||||
}
|
||||
|
||||
function nextWallImpact(
|
||||
position: Iwt2Vec2,
|
||||
direction: Iwt2Vec2,
|
||||
radius: number,
|
||||
state: Iwt2ArenaState,
|
||||
): Iwt2Vec2 {
|
||||
const facing = withFallbackFacing(direction, { x: 1, y: 0 })
|
||||
const times: number[] = []
|
||||
if (Math.abs(facing.x) > 0.0001) {
|
||||
times.push(((facing.x > 0 ? state.bounds.width - radius : radius) - position.x) / facing.x)
|
||||
}
|
||||
if (Math.abs(facing.y) > 0.0001) {
|
||||
times.push(((facing.y > 0 ? state.bounds.height - radius : radius) - position.y) / facing.y)
|
||||
}
|
||||
const impactDistance = Math.max(0, Math.min(...times.filter((value) => value > 0)))
|
||||
return clampVec2ToArena(addVec2(position, scaleVec2(facing, impactDistance)), radius, state.bounds)
|
||||
}
|
||||
|
||||
function wallSafeRollDirection(
|
||||
boss: Iwt2BossEntityState,
|
||||
targetPosition: Iwt2Vec2,
|
||||
state: Iwt2ArenaState,
|
||||
): Iwt2Vec2 {
|
||||
if (!isBossNearWall(boss, state)) return withFallbackFacing(subtractVec2(targetPosition, boss.position), boss.facing)
|
||||
const centerBias = normalizeVec2(subtractVec2(arenaCenter(state), boss.position))
|
||||
const targetBias = normalizeVec2(subtractVec2(targetPosition, boss.position))
|
||||
return withFallbackFacing(addVec2(scaleVec2(centerBias, 1.25), scaleVec2(targetBias, 0.55)), centerBias)
|
||||
}
|
||||
|
||||
function isBossNearWall(boss: Iwt2BossEntityState, state: Iwt2ArenaState): boolean {
|
||||
const margin = boss.radius + CINDERBACK_TUNING.wallMargin
|
||||
return (
|
||||
boss.position.x <= margin
|
||||
|| boss.position.x >= state.bounds.width - margin
|
||||
|| boss.position.y <= margin
|
||||
|| boss.position.y >= state.bounds.height - margin
|
||||
)
|
||||
}
|
||||
|
||||
function disengageFromWall(position: Iwt2Vec2, radius: number, state: Iwt2ArenaState): Iwt2Vec2 {
|
||||
const center = arenaCenter(state)
|
||||
const margin = radius + CINDERBACK_TUNING.wallMargin * 0.55
|
||||
const clamped = {
|
||||
x: Math.min(state.bounds.width - margin, Math.max(margin, position.x)),
|
||||
y: Math.min(state.bounds.height - margin, Math.max(margin, position.y)),
|
||||
}
|
||||
if (clamped.x !== position.x || clamped.y !== position.y) return clamped
|
||||
return addVec2(position, scaleVec2(normalizeVec2(subtractVec2(center, position)), 18))
|
||||
}
|
||||
|
||||
function arenaCenter(state: Iwt2ArenaState): Iwt2Vec2 {
|
||||
return {
|
||||
x: state.bounds.width * 0.5,
|
||||
y: state.bounds.height * 0.5,
|
||||
}
|
||||
}
|
||||
|
||||
function getBossTarget(party: Iwt2PartyEntityState[]): Iwt2PartyEntityState | undefined {
|
||||
const livingTank = party.find((member) => member.classId === 'paladin' && member.health > 0)
|
||||
if (livingTank) return livingTank
|
||||
return party.find((member) => member.health > 0)
|
||||
}
|
||||
|
||||
function withCinderbackIndicators(result: Omit<Iwt2BossTickResult, 'indicators'>): Iwt2BossTickResult {
|
||||
return {
|
||||
...result,
|
||||
indicators: createCinderbackIndicators(result.boss),
|
||||
}
|
||||
}
|
||||
|
||||
function createCinderbackIndicators(boss: Iwt2BossEntityState): Iwt2ArenaIndicator[] {
|
||||
const indicators: Iwt2ArenaIndicator[] = []
|
||||
const phase = cinderbackPhase(boss)
|
||||
if (
|
||||
phase === CINDERBACK_PHASE.ricochetWindup
|
||||
|| phase === CINDERBACK_PHASE.ricocheting
|
||||
) {
|
||||
indicators.push(createLaneIndicator({
|
||||
color: phase === CINDERBACK_PHASE.ricocheting ? '#ff6a2a' : '#ffd166',
|
||||
end: boss.chargeEnd,
|
||||
id: `${boss.id}:cinderback-ricochet-lane`,
|
||||
mechanicId: 'cinderback-ricochet',
|
||||
phase: indicatorPhaseFromAttack(
|
||||
phase === CINDERBACK_PHASE.ricochetWindup,
|
||||
phase === CINDERBACK_PHASE.ricocheting,
|
||||
),
|
||||
sourceId: boss.id,
|
||||
start: boss.chargeStart,
|
||||
width: CINDERBACK_TUNING.ricochetWidth,
|
||||
}))
|
||||
}
|
||||
if (
|
||||
phase === CINDERBACK_PHASE.armorSlamWindup
|
||||
|| phase === CINDERBACK_PHASE.armorSlamRecover
|
||||
) {
|
||||
indicators.push(createCircleIndicator({
|
||||
color: '#ff7a2f',
|
||||
id: `${boss.id}:cinderback-armor-slam`,
|
||||
mechanicId: 'cinderback-armor-slam',
|
||||
phase: indicatorPhaseFromAttack(
|
||||
phase === CINDERBACK_PHASE.armorSlamWindup,
|
||||
phase === CINDERBACK_PHASE.armorSlamRecover,
|
||||
),
|
||||
position: boss.position,
|
||||
radius: CINDERBACK_TUNING.armorSlamRadius,
|
||||
sourceId: boss.id,
|
||||
}))
|
||||
}
|
||||
return indicators
|
||||
}
|
||||
|
||||
function cinderbackPhase(boss: Iwt2BossEntityState): string {
|
||||
return boss.attackPhase as string
|
||||
}
|
||||
|
||||
function asBossPhase(phase: CinderbackPhase): Iwt2BossEntityState['attackPhase'] {
|
||||
return phase as unknown as Iwt2BossEntityState['attackPhase']
|
||||
}
|
||||
Reference in New Issue
Block a user