Android build v1.1.22
This commit is contained in:
@@ -0,0 +1,712 @@
|
||||
import type { Iwt2BossTickResult } from './bossAi'
|
||||
import type {
|
||||
Iwt2ArenaEvent,
|
||||
Iwt2ArenaIndicator,
|
||||
Iwt2ArenaIndicatorPhase,
|
||||
Iwt2ArenaState,
|
||||
Iwt2BossEntityState,
|
||||
Iwt2EntityId,
|
||||
Iwt2MechanicCircleState,
|
||||
Iwt2MechanicLaneState,
|
||||
Iwt2PartyEntityState,
|
||||
Iwt2Vec2,
|
||||
} from './types'
|
||||
import {
|
||||
applyPartyDamageInShape,
|
||||
createArenaEvent,
|
||||
createCircleIndicator,
|
||||
createLaneIndicator,
|
||||
indicatorPhaseFromAttack,
|
||||
} from './mechanics'
|
||||
import {
|
||||
addVec2,
|
||||
clampVec2ToArena,
|
||||
distanceVec2,
|
||||
moveToward,
|
||||
normalizeVec2,
|
||||
scaleVec2,
|
||||
subtractVec2,
|
||||
withFallbackFacing,
|
||||
} from './vector'
|
||||
|
||||
const HOLLOWCROWN_PHASE = {
|
||||
antlerChargeRecover: 'hollowcrownAntlerChargeRecover',
|
||||
antlerChargeWindup: 'hollowcrownAntlerChargeWindup',
|
||||
hoofCurseRecover: 'hollowcrownHoofCurseRecover',
|
||||
hoofCurseWindup: 'hollowcrownHoofCurseWindup',
|
||||
trailDashing: 'hollowcrownTrailDashing',
|
||||
trailDashWindup: 'hollowcrownTrailDashWindup',
|
||||
trailFade: 'hollowcrownTrailFade',
|
||||
} as const
|
||||
|
||||
type HollowcrownSpecialPhase = typeof HOLLOWCROWN_PHASE[keyof typeof HOLLOWCROWN_PHASE]
|
||||
type HollowcrownAttackPhase = Iwt2BossEntityState['attackPhase'] | HollowcrownSpecialPhase
|
||||
|
||||
const HOLLOWCROWN_TUNING = {
|
||||
antlerCooldown: 7.4,
|
||||
antlerDamage: 17,
|
||||
antlerLaneLength: 440,
|
||||
antlerLaneOffset: 44,
|
||||
antlerLineWidth: 30,
|
||||
antlerRecover: 0.52,
|
||||
antlerStunSeconds: 0.48,
|
||||
antlerWindup: 0.82,
|
||||
dashCooldown: 5.2,
|
||||
dashDamage: 13,
|
||||
dashLength: 330,
|
||||
dashRecover: 0.42,
|
||||
dashSpeed: 430,
|
||||
dashStunSeconds: 0.32,
|
||||
dashTrailDamage: 9,
|
||||
dashTrailSeconds: 0.5,
|
||||
dashTrailWidth: 40,
|
||||
hoofCooldown: 6.2,
|
||||
hoofDamage: 12,
|
||||
hoofRadius: 46,
|
||||
hoofRecover: 0.46,
|
||||
hoofStunSeconds: 0.34,
|
||||
hoofWindup: 0.74,
|
||||
meleeCooldown: 1.25,
|
||||
meleeDamage: 5,
|
||||
meleeRange: 58,
|
||||
moveSpeed: 116,
|
||||
relocateSeconds: 1.25,
|
||||
relocateSpeed: 168,
|
||||
wallContactLimit: 0.68,
|
||||
wallMargin: 76,
|
||||
} as const
|
||||
|
||||
const CURSE_COLOR = '#9b74ff'
|
||||
const TRAIL_COLOR = '#7ee7ff'
|
||||
const ANTLER_COLOR = '#e8dcff'
|
||||
|
||||
export function tickHollowcrownRevenant(state: Iwt2ArenaState, dt: number): Iwt2BossTickResult {
|
||||
const events: Iwt2ArenaEvent[] = []
|
||||
let party = state.party
|
||||
const incomingPhase = state.boss.attackPhase as HollowcrownAttackPhase
|
||||
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 },
|
||||
wallContactSeconds: incomingPhase === 'idle' && isBossNearWall(state.boss, state)
|
||||
? state.boss.wallContactSeconds + dt
|
||||
: Math.max(0, state.boss.wallContactSeconds - dt * 2),
|
||||
}
|
||||
const target = getBossTarget(party)
|
||||
|
||||
if (boss.health <= 0 || !target) return withHollowcrownIndicators({ boss, party, events, state })
|
||||
|
||||
const phase = boss.attackPhase as HollowcrownAttackPhase
|
||||
|
||||
if (phase === 'relocating') {
|
||||
boss = moveBossTowardRelocationTarget(boss, state, target, dt)
|
||||
if (boss.phaseSecondsRemaining <= 0 || distanceVec2(boss.position, boss.relocateTarget) <= 8) {
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: 'idle',
|
||||
chargeCooldownRemaining: Math.max(boss.chargeCooldownRemaining, 1.1),
|
||||
fireballCooldownRemaining: Math.max(boss.fireballCooldownRemaining, 1.0),
|
||||
mechanicCircles: [],
|
||||
mechanicLanes: [],
|
||||
phaseSecondsRemaining: 0,
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
}
|
||||
return withHollowcrownIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
if (phase === HOLLOWCROWN_PHASE.hoofCurseWindup) {
|
||||
boss = faceTarget(boss, target)
|
||||
if (boss.phaseSecondsRemaining <= 0) {
|
||||
const result = applyHoofCurse(party, boss, state.time + dt)
|
||||
party = result.party
|
||||
events.push(...result.events)
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: asBossPhase(HOLLOWCROWN_PHASE.hoofCurseRecover),
|
||||
phaseSecondsRemaining: HOLLOWCROWN_TUNING.hoofRecover,
|
||||
}
|
||||
}
|
||||
return withHollowcrownIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
if (phase === HOLLOWCROWN_PHASE.hoofCurseRecover) {
|
||||
if (boss.phaseSecondsRemaining <= 0) {
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: 'idle',
|
||||
mechanicCircles: [],
|
||||
phaseSecondsRemaining: 0,
|
||||
}
|
||||
}
|
||||
return withHollowcrownIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
if (phase === HOLLOWCROWN_PHASE.trailDashWindup) {
|
||||
boss = {
|
||||
...boss,
|
||||
facing: withFallbackFacing(subtractVec2(boss.chargeEnd, boss.position), boss.facing),
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
if (boss.phaseSecondsRemaining <= 0) {
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: asBossPhase(HOLLOWCROWN_PHASE.trailDashing),
|
||||
chargeHitEntityIds: [],
|
||||
phaseSecondsRemaining: 0,
|
||||
}
|
||||
}
|
||||
return withHollowcrownIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
if (phase === HOLLOWCROWN_PHASE.trailDashing) {
|
||||
const nextPosition = clampVec2ToArena(
|
||||
moveToward(boss.position, boss.chargeEnd, HOLLOWCROWN_TUNING.dashSpeed * dt),
|
||||
boss.radius,
|
||||
state.bounds,
|
||||
)
|
||||
const hitResult = applyTrailDashHits(party, boss, boss.position, nextPosition, state.time + dt)
|
||||
party = hitResult.party
|
||||
events.push(...hitResult.events)
|
||||
boss = {
|
||||
...boss,
|
||||
chargeHitEntityIds: hitResult.hitEntityIds,
|
||||
facing: withFallbackFacing(subtractVec2(nextPosition, boss.position), boss.facing),
|
||||
position: nextPosition,
|
||||
velocity: scaleVec2(subtractVec2(nextPosition, boss.position), dt > 0 ? 1 / dt : 0),
|
||||
wallContactSeconds: isBossNearWall({ ...boss, position: nextPosition }, state) ? boss.wallContactSeconds + dt : 0,
|
||||
}
|
||||
if (distanceVec2(nextPosition, boss.chargeEnd) <= 2 || boss.wallContactSeconds >= HOLLOWCROWN_TUNING.wallContactLimit) {
|
||||
const trailResult = applyFadingTrail(party, boss, state.time + dt)
|
||||
party = trailResult.party
|
||||
events.push(...trailResult.events)
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: asBossPhase(HOLLOWCROWN_PHASE.trailFade),
|
||||
phaseSecondsRemaining: HOLLOWCROWN_TUNING.dashTrailSeconds,
|
||||
velocity: { x: 0, y: 0 },
|
||||
wallContactSeconds: 0,
|
||||
}
|
||||
}
|
||||
return withHollowcrownIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
if (phase === HOLLOWCROWN_PHASE.trailFade) {
|
||||
if (boss.phaseSecondsRemaining <= 0) {
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: 'idle',
|
||||
mechanicLanes: [],
|
||||
phaseSecondsRemaining: 0,
|
||||
}
|
||||
}
|
||||
return withHollowcrownIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
if (phase === HOLLOWCROWN_PHASE.antlerChargeWindup) {
|
||||
boss = faceLaneTarget(boss)
|
||||
if (boss.phaseSecondsRemaining <= 0) {
|
||||
const result = applyAntlerLines(party, boss, state.time + dt)
|
||||
party = result.party
|
||||
events.push(...result.events)
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: asBossPhase(HOLLOWCROWN_PHASE.antlerChargeRecover),
|
||||
phaseSecondsRemaining: HOLLOWCROWN_TUNING.antlerRecover,
|
||||
}
|
||||
}
|
||||
return withHollowcrownIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
if (phase === HOLLOWCROWN_PHASE.antlerChargeRecover) {
|
||||
if (boss.phaseSecondsRemaining <= 0) {
|
||||
boss = {
|
||||
...boss,
|
||||
attackPhase: 'idle',
|
||||
mechanicLanes: [],
|
||||
phaseSecondsRemaining: 0,
|
||||
}
|
||||
}
|
||||
return withHollowcrownIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
if (boss.wallContactSeconds >= HOLLOWCROWN_TUNING.wallContactLimit) {
|
||||
boss = beginRelocation(boss, state)
|
||||
return withHollowcrownIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
if (boss.fireballCooldownRemaining <= 0) {
|
||||
boss = beginRangedPattern(boss, state, party, target)
|
||||
return withHollowcrownIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
if (boss.chargeCooldownRemaining <= 0) {
|
||||
boss = beginTrailDash(boss, state, target)
|
||||
events.push(createArenaEvent(0, state.time + dt, 'bossChargeStart', boss.id, target.id))
|
||||
return withHollowcrownIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
const meleeResult = maybeApplyMelee(party, boss, target, state.time + dt)
|
||||
party = meleeResult.party
|
||||
events.push(...meleeResult.events)
|
||||
boss = meleeResult.boss
|
||||
if (events.length > 0) return withHollowcrownIndicators({ boss, party, events, state })
|
||||
|
||||
boss = moveBossTowardEngagePoint(boss, state, target, dt)
|
||||
return withHollowcrownIndicators({ boss, party, events, state })
|
||||
}
|
||||
|
||||
function asBossPhase(phase: HollowcrownAttackPhase): Iwt2BossEntityState['attackPhase'] {
|
||||
return phase as Iwt2BossEntityState['attackPhase']
|
||||
}
|
||||
|
||||
function beginRangedPattern(
|
||||
boss: Iwt2BossEntityState,
|
||||
state: Iwt2ArenaState,
|
||||
party: Iwt2PartyEntityState[],
|
||||
target: Iwt2PartyEntityState,
|
||||
): Iwt2BossEntityState {
|
||||
const useAntlers = boss.chargeCount % 2 === 1
|
||||
if (useAntlers) {
|
||||
return {
|
||||
...boss,
|
||||
attackPhase: asBossPhase(HOLLOWCROWN_PHASE.antlerChargeWindup),
|
||||
chargeCount: boss.chargeCount + 1,
|
||||
fireballCooldownRemaining: HOLLOWCROWN_TUNING.antlerCooldown,
|
||||
mechanicCircles: [],
|
||||
mechanicLanes: createAntlerLines(state, boss, target),
|
||||
phaseSecondsRemaining: HOLLOWCROWN_TUNING.antlerWindup,
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
}
|
||||
return {
|
||||
...boss,
|
||||
attackPhase: asBossPhase(HOLLOWCROWN_PHASE.hoofCurseWindup),
|
||||
chargeCount: boss.chargeCount + 1,
|
||||
fireballCooldownRemaining: HOLLOWCROWN_TUNING.hoofCooldown,
|
||||
mechanicCircles: createHoofZones(state, party, boss),
|
||||
mechanicLanes: [],
|
||||
phaseSecondsRemaining: HOLLOWCROWN_TUNING.hoofWindup,
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
function beginTrailDash(
|
||||
boss: Iwt2BossEntityState,
|
||||
state: Iwt2ArenaState,
|
||||
target: Iwt2PartyEntityState,
|
||||
): Iwt2BossEntityState {
|
||||
const lane = createDashTrailLane(state, boss, target)
|
||||
return {
|
||||
...boss,
|
||||
attackPhase: asBossPhase(HOLLOWCROWN_PHASE.trailDashWindup),
|
||||
chargeCooldownRemaining: HOLLOWCROWN_TUNING.dashCooldown,
|
||||
chargeEnd: lane.end,
|
||||
chargeHitEntityIds: [],
|
||||
chargeStart: { ...boss.position },
|
||||
facing: withFallbackFacing(subtractVec2(lane.end, boss.position), boss.facing),
|
||||
mechanicCircles: [],
|
||||
mechanicLanes: [lane],
|
||||
phaseSecondsRemaining: 0.58,
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
function beginRelocation(boss: Iwt2BossEntityState, state: Iwt2ArenaState): Iwt2BossEntityState {
|
||||
return {
|
||||
...boss,
|
||||
attackPhase: 'relocating',
|
||||
mechanicCircles: [],
|
||||
mechanicLanes: [],
|
||||
phaseSecondsRemaining: HOLLOWCROWN_TUNING.relocateSeconds,
|
||||
relocateTarget: relocationTarget(boss, state),
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
function createHoofZones(
|
||||
state: Iwt2ArenaState,
|
||||
party: Iwt2PartyEntityState[],
|
||||
boss: Iwt2BossEntityState,
|
||||
): Iwt2MechanicCircleState[] {
|
||||
const living = party.filter((member) => member.health > 0)
|
||||
const priority = [
|
||||
...living.filter((member) => member.aiRole === 'ranged' || member.aiRole === 'flanker' || member.aiRole === 'player'),
|
||||
...living.filter((member) => member.aiRole !== 'ranged' && member.aiRole !== 'flanker' && member.aiRole !== 'player'),
|
||||
]
|
||||
return priority.slice(0, 4).map((member, index) => {
|
||||
const drift = normalizeVec2(addVec2(
|
||||
subtractVec2(member.position, boss.position),
|
||||
scaleVec2(subtractVec2(arenaCenter(state), member.position), 0.35),
|
||||
))
|
||||
return {
|
||||
id: `hollowcrown-hoof-zone-${index}`,
|
||||
position: clampVec2ToArena(
|
||||
addVec2(member.position, scaleVec2(drift, 18 + index * 4)),
|
||||
HOLLOWCROWN_TUNING.hoofRadius,
|
||||
state.bounds,
|
||||
),
|
||||
radius: HOLLOWCROWN_TUNING.hoofRadius,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function createDashTrailLane(
|
||||
state: Iwt2ArenaState,
|
||||
boss: Iwt2BossEntityState,
|
||||
target: Iwt2PartyEntityState,
|
||||
): Iwt2MechanicLaneState {
|
||||
const wallBias = isBossNearWall(boss, state)
|
||||
? normalizeVec2(subtractVec2(arenaCenter(state), boss.position))
|
||||
: { x: 0, y: 0 }
|
||||
const targetBias = normalizeVec2(subtractVec2(target.position, boss.position))
|
||||
const direction = withFallbackFacing(addVec2(targetBias, scaleVec2(wallBias, 0.9)), boss.facing)
|
||||
const rawEnd = addVec2(boss.position, scaleVec2(direction, HOLLOWCROWN_TUNING.dashLength))
|
||||
return {
|
||||
end: clampVec2ToArena(rawEnd, boss.radius + 8, state.bounds),
|
||||
id: 'hollowcrown-dash-trail',
|
||||
start: { ...boss.position },
|
||||
width: HOLLOWCROWN_TUNING.dashTrailWidth,
|
||||
}
|
||||
}
|
||||
|
||||
function createAntlerLines(
|
||||
state: Iwt2ArenaState,
|
||||
boss: Iwt2BossEntityState,
|
||||
target: Iwt2PartyEntityState,
|
||||
): Iwt2MechanicLaneState[] {
|
||||
const direction = withFallbackFacing(subtractVec2(target.position, boss.position), boss.facing)
|
||||
const perpendicular = { x: -direction.y, y: direction.x }
|
||||
return [-1, 0, 1].map((index) => {
|
||||
const offset = scaleVec2(perpendicular, index * HOLLOWCROWN_TUNING.antlerLaneOffset)
|
||||
const start = clampVec2ToArena(addVec2(boss.position, offset), 4, state.bounds)
|
||||
const end = clampVec2ToArena(
|
||||
addVec2(start, scaleVec2(direction, HOLLOWCROWN_TUNING.antlerLaneLength)),
|
||||
4,
|
||||
state.bounds,
|
||||
)
|
||||
return {
|
||||
end,
|
||||
id: `hollowcrown-antler-line-${index + 1}`,
|
||||
start,
|
||||
width: index === 0 ? HOLLOWCROWN_TUNING.antlerLineWidth + 8 : HOLLOWCROWN_TUNING.antlerLineWidth,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function applyHoofCurse(
|
||||
party: Iwt2PartyEntityState[],
|
||||
boss: Iwt2BossEntityState,
|
||||
time: number,
|
||||
): { party: Iwt2PartyEntityState[], events: Iwt2ArenaEvent[] } {
|
||||
let nextParty = party
|
||||
const events: Iwt2ArenaEvent[] = []
|
||||
const hitEntityIds: Iwt2EntityId[] = []
|
||||
for (const circle of boss.mechanicCircles) {
|
||||
const result = applyPartyDamageInShape(nextParty, {
|
||||
kind: 'circle',
|
||||
position: circle.position,
|
||||
radius: circle.radius,
|
||||
}, {
|
||||
damage: HOLLOWCROWN_TUNING.hoofDamage,
|
||||
excludedEntityIds: hitEntityIds,
|
||||
knockdownSeconds: 0,
|
||||
sourceId: boss.id,
|
||||
stunSeconds: HOLLOWCROWN_TUNING.hoofStunSeconds,
|
||||
time,
|
||||
})
|
||||
nextParty = result.party
|
||||
hitEntityIds.push(...result.hitEntityIds)
|
||||
events.push(...result.events)
|
||||
}
|
||||
return { party: nextParty, events }
|
||||
}
|
||||
|
||||
function applyTrailDashHits(
|
||||
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: HOLLOWCROWN_TUNING.dashTrailWidth,
|
||||
}, {
|
||||
damage: HOLLOWCROWN_TUNING.dashDamage,
|
||||
damageEventType: 'bossChargeHit',
|
||||
excludedEntityIds: boss.chargeHitEntityIds,
|
||||
knockdownSeconds: HOLLOWCROWN_TUNING.dashStunSeconds,
|
||||
sourceId: boss.id,
|
||||
stunSeconds: HOLLOWCROWN_TUNING.dashStunSeconds,
|
||||
time,
|
||||
})
|
||||
return {
|
||||
...result,
|
||||
hitEntityIds: [...boss.chargeHitEntityIds, ...result.hitEntityIds],
|
||||
}
|
||||
}
|
||||
|
||||
function applyFadingTrail(
|
||||
party: Iwt2PartyEntityState[],
|
||||
boss: Iwt2BossEntityState,
|
||||
time: number,
|
||||
): { party: Iwt2PartyEntityState[], events: Iwt2ArenaEvent[] } {
|
||||
const lane = boss.mechanicLanes[0]
|
||||
if (!lane) return { party, events: [] }
|
||||
const result = applyPartyDamageInShape(party, {
|
||||
end: lane.end,
|
||||
kind: 'lane',
|
||||
start: lane.start,
|
||||
width: lane.width * 1.15,
|
||||
}, {
|
||||
damage: HOLLOWCROWN_TUNING.dashTrailDamage,
|
||||
knockdownSeconds: 0,
|
||||
sourceId: boss.id,
|
||||
stunSeconds: HOLLOWCROWN_TUNING.dashStunSeconds * 0.65,
|
||||
time,
|
||||
})
|
||||
return { party: result.party, events: result.events }
|
||||
}
|
||||
|
||||
function applyAntlerLines(
|
||||
party: Iwt2PartyEntityState[],
|
||||
boss: Iwt2BossEntityState,
|
||||
time: number,
|
||||
): { party: Iwt2PartyEntityState[], events: Iwt2ArenaEvent[] } {
|
||||
let nextParty = party
|
||||
const events: Iwt2ArenaEvent[] = []
|
||||
const hitEntityIds: Iwt2EntityId[] = []
|
||||
for (const lane of boss.mechanicLanes) {
|
||||
const result = applyPartyDamageInShape(nextParty, {
|
||||
end: lane.end,
|
||||
kind: 'lane',
|
||||
start: lane.start,
|
||||
width: lane.width,
|
||||
}, {
|
||||
damage: HOLLOWCROWN_TUNING.antlerDamage,
|
||||
excludedEntityIds: hitEntityIds,
|
||||
knockdownSeconds: HOLLOWCROWN_TUNING.antlerStunSeconds,
|
||||
sourceId: boss.id,
|
||||
stunSeconds: HOLLOWCROWN_TUNING.antlerStunSeconds,
|
||||
time,
|
||||
})
|
||||
nextParty = result.party
|
||||
hitEntityIds.push(...result.hitEntityIds)
|
||||
events.push(...result.events)
|
||||
}
|
||||
return { party: nextParty, 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) > HOLLOWCROWN_TUNING.meleeRange + target.radius) {
|
||||
return { boss, party, events: [] }
|
||||
}
|
||||
const result = applyPartyDamageInShape(party, {
|
||||
kind: 'circle',
|
||||
position: boss.position,
|
||||
radius: HOLLOWCROWN_TUNING.meleeRange,
|
||||
}, {
|
||||
damage: HOLLOWCROWN_TUNING.meleeDamage,
|
||||
sourceId: boss.id,
|
||||
time,
|
||||
})
|
||||
return {
|
||||
boss: { ...boss, meleeCooldownRemaining: HOLLOWCROWN_TUNING.meleeCooldown, velocity: { x: 0, y: 0 } },
|
||||
events: result.events,
|
||||
party: result.party,
|
||||
}
|
||||
}
|
||||
|
||||
function moveBossTowardRelocationTarget(
|
||||
boss: Iwt2BossEntityState,
|
||||
state: Iwt2ArenaState,
|
||||
target: Iwt2PartyEntityState,
|
||||
dt: number,
|
||||
): Iwt2BossEntityState {
|
||||
const nextPosition = clampVec2ToArena(
|
||||
moveToward(boss.position, boss.relocateTarget, HOLLOWCROWN_TUNING.relocateSpeed * dt),
|
||||
boss.radius,
|
||||
state.bounds,
|
||||
)
|
||||
return {
|
||||
...boss,
|
||||
facing: withFallbackFacing(subtractVec2(target.position, nextPosition), boss.facing),
|
||||
position: nextPosition,
|
||||
velocity: scaleVec2(subtractVec2(nextPosition, boss.position), dt > 0 ? 1 / dt : 0),
|
||||
wallContactSeconds: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function moveBossTowardEngagePoint(
|
||||
boss: Iwt2BossEntityState,
|
||||
state: Iwt2ArenaState,
|
||||
target: Iwt2PartyEntityState,
|
||||
dt: number,
|
||||
): Iwt2BossEntityState {
|
||||
const center = arenaCenter(state)
|
||||
const targetNearWall = isPositionNearWall(target.position, target.radius + HOLLOWCROWN_TUNING.wallMargin, state)
|
||||
const desired = targetNearWall
|
||||
? addVec2(target.position, scaleVec2(normalizeVec2(subtractVec2(center, target.position)), HOLLOWCROWN_TUNING.meleeRange))
|
||||
: target.position
|
||||
const nextPosition = clampVec2ToArena(
|
||||
moveToward(boss.position, desired, HOLLOWCROWN_TUNING.moveSpeed * dt),
|
||||
boss.radius,
|
||||
state.bounds,
|
||||
)
|
||||
return {
|
||||
...boss,
|
||||
facing: withFallbackFacing(subtractVec2(target.position, nextPosition), boss.facing),
|
||||
position: nextPosition,
|
||||
velocity: scaleVec2(subtractVec2(nextPosition, boss.position), dt > 0 ? 1 / dt : 0),
|
||||
}
|
||||
}
|
||||
|
||||
function faceTarget(boss: Iwt2BossEntityState, target: Iwt2PartyEntityState): Iwt2BossEntityState {
|
||||
return {
|
||||
...boss,
|
||||
facing: withFallbackFacing(subtractVec2(target.position, boss.position), boss.facing),
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
function faceLaneTarget(boss: Iwt2BossEntityState): Iwt2BossEntityState {
|
||||
const lane = boss.mechanicLanes[0]
|
||||
if (!lane) return { ...boss, velocity: { x: 0, y: 0 } }
|
||||
return {
|
||||
...boss,
|
||||
facing: withFallbackFacing(subtractVec2(lane.end, lane.start), boss.facing),
|
||||
velocity: { x: 0, y: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
function relocationTarget(boss: Iwt2BossEntityState, state: Iwt2ArenaState): Iwt2Vec2 {
|
||||
const center = arenaCenter(state)
|
||||
const awayFromWall = normalizeVec2(addVec2(
|
||||
subtractVec2(center, boss.position),
|
||||
{
|
||||
x: boss.position.x < center.x ? 1 : -1,
|
||||
y: boss.position.y < center.y ? 1 : -1,
|
||||
},
|
||||
))
|
||||
return clampVec2ToArena(addVec2(center, scaleVec2(awayFromWall, 72)), boss.radius, state.bounds)
|
||||
}
|
||||
|
||||
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 arenaCenter(state: Iwt2ArenaState): Iwt2Vec2 {
|
||||
return {
|
||||
x: state.bounds.width * 0.5,
|
||||
y: state.bounds.height * 0.5,
|
||||
}
|
||||
}
|
||||
|
||||
function isBossNearWall(boss: Pick<Iwt2BossEntityState, 'position' | 'radius'>, state: Iwt2ArenaState): boolean {
|
||||
return isPositionNearWall(boss.position, boss.radius + HOLLOWCROWN_TUNING.wallMargin, state)
|
||||
}
|
||||
|
||||
function isPositionNearWall(position: Iwt2Vec2, margin: number, state: Iwt2ArenaState): boolean {
|
||||
return (
|
||||
position.x <= margin
|
||||
|| position.x >= state.bounds.width - margin
|
||||
|| position.y <= margin
|
||||
|| position.y >= state.bounds.height - margin
|
||||
)
|
||||
}
|
||||
|
||||
function withHollowcrownIndicators(result: {
|
||||
boss: Iwt2BossEntityState
|
||||
party: Iwt2PartyEntityState[]
|
||||
events: Iwt2ArenaEvent[]
|
||||
state: Iwt2ArenaState
|
||||
}): Iwt2BossTickResult {
|
||||
return {
|
||||
boss: result.boss,
|
||||
events: result.events,
|
||||
indicators: createHollowcrownIndicators(result.boss),
|
||||
party: result.party,
|
||||
}
|
||||
}
|
||||
|
||||
function createHollowcrownIndicators(boss: Iwt2BossEntityState): Iwt2ArenaIndicator[] {
|
||||
const indicators: Iwt2ArenaIndicator[] = []
|
||||
const phase = boss.attackPhase as HollowcrownAttackPhase
|
||||
|
||||
if (phase === HOLLOWCROWN_PHASE.hoofCurseWindup || phase === HOLLOWCROWN_PHASE.hoofCurseRecover) {
|
||||
const indicatorPhase = indicatorPhaseFromAttack(
|
||||
phase === HOLLOWCROWN_PHASE.hoofCurseWindup,
|
||||
phase === HOLLOWCROWN_PHASE.hoofCurseRecover,
|
||||
)
|
||||
for (const circle of boss.mechanicCircles) {
|
||||
indicators.push(createCircleIndicator({
|
||||
color: CURSE_COLOR,
|
||||
id: `${boss.id}:${circle.id}`,
|
||||
mechanicId: 'hollowcrown-cursed-hoof-zone',
|
||||
phase: indicatorPhase,
|
||||
position: circle.position,
|
||||
radius: circle.radius,
|
||||
sourceId: boss.id,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
phase === HOLLOWCROWN_PHASE.trailDashWindup
|
||||
|| phase === HOLLOWCROWN_PHASE.trailDashing
|
||||
|| phase === HOLLOWCROWN_PHASE.trailFade
|
||||
) {
|
||||
const indicatorPhase: Iwt2ArenaIndicatorPhase = phase === HOLLOWCROWN_PHASE.trailDashWindup
|
||||
? 'windup'
|
||||
: phase === HOLLOWCROWN_PHASE.trailDashing
|
||||
? 'active'
|
||||
: 'recover'
|
||||
for (const lane of boss.mechanicLanes) {
|
||||
indicators.push(createLaneIndicator({
|
||||
color: TRAIL_COLOR,
|
||||
end: lane.end,
|
||||
id: `${boss.id}:${lane.id}`,
|
||||
mechanicId: 'hollowcrown-fading-dash-trail',
|
||||
phase: indicatorPhase,
|
||||
sourceId: boss.id,
|
||||
start: phase === HOLLOWCROWN_PHASE.trailDashing ? boss.position : lane.start,
|
||||
width: phase === HOLLOWCROWN_PHASE.trailFade ? lane.width * 1.15 : lane.width,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
if (phase === HOLLOWCROWN_PHASE.antlerChargeWindup || phase === HOLLOWCROWN_PHASE.antlerChargeRecover) {
|
||||
const indicatorPhase = indicatorPhaseFromAttack(
|
||||
phase === HOLLOWCROWN_PHASE.antlerChargeWindup,
|
||||
phase === HOLLOWCROWN_PHASE.antlerChargeRecover,
|
||||
)
|
||||
for (const lane of boss.mechanicLanes) {
|
||||
indicators.push(createLaneIndicator({
|
||||
color: ANTLER_COLOR,
|
||||
end: lane.end,
|
||||
id: `${boss.id}:${lane.id}`,
|
||||
mechanicId: 'hollowcrown-ghost-antler-line-charge',
|
||||
phase: indicatorPhase,
|
||||
sourceId: boss.id,
|
||||
start: lane.start,
|
||||
width: lane.width,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return indicators
|
||||
}
|
||||
Reference in New Issue
Block a user