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: 42, antlerLaneLength: 440, antlerLaneOffset: 44, antlerLineWidth: 30, antlerRecover: 0.52, antlerStunSeconds: 0.48, antlerWindup: 0.82, dashCooldown: 5.2, dashDamage: 34, dashLength: 330, dashRecover: 0.42, dashSpeed: 430, dashStunSeconds: 0.32, dashTrailDamage: 20, dashTrailSeconds: 0.5, dashTrailWidth: 40, hoofCooldown: 6.2, hoofDamage: 34, hoofRadius: 46, hoofRecover: 0.46, hoofStunSeconds: 0.34, hoofWindup: 0.74, meleeCooldown: 1.25, meleeDamage: 11, 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, 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 }