import type { Iwt2BossTickResult } from './bossAi' import type { Iwt2ArenaEvent, Iwt2ArenaIndicator, Iwt2ArenaState, Iwt2BossEntityState, Iwt2EntityId, Iwt2GroundHazardState, Iwt2MechanicCircleState, Iwt2MechanicLaneState, Iwt2PartyEntityState, Iwt2Vec2, } from './types' import { addPoisonPuddle } from './hazards' import { applyPartyDamageInShape, createCircleIndicator, createConeIndicator, createLaneIndicator, indicatorPhaseFromAttack, } from './mechanics' import { addVec2, clampVec2ToArena, distanceVec2, moveToward, normalizeVec2, scaleVec2, subtractVec2, withFallbackFacing, } from './vector' const VENOM_ORCHID_HYDRA_PHASE = { bloomPodRecover: 'venomOrchidHydraBloomPodRecover', bloomPodWindup: 'venomOrchidHydraBloomPodWindup', coneRecover: 'venomOrchidHydraConeRecover', coneWindup: 'venomOrchidHydraConeWindup', rootSnareRecover: 'venomOrchidHydraRootSnareRecover', rootSnareWindup: 'venomOrchidHydraRootSnareWindup', } as const type VenomOrchidHydraPhase = typeof VENOM_ORCHID_HYDRA_PHASE[keyof typeof VENOM_ORCHID_HYDRA_PHASE] const VENOM_ORCHID_HYDRA = { bloomBurstDamage: 18, bloomCooldown: 8.6, bloomPodCount: 3, bloomPodDamage: 10, bloomPodRadius: 30, bloomPodSeconds: 1.4, bloomRecover: 0.5, bloomWindup: 0.9, coneAngleRadians: Math.PI * 0.34, coneCooldown: 5.7, coneDamage: 28, coneRange: 178, coneRecover: 0.62, coneStaggerWindup: 0.48, coneStunSeconds: 0.18, headOffset: 22, meleeCooldown: 1.35, meleeDamage: 10, meleeRange: 58, moveSpeed: 58, relocateSpeed: 114, relocateSeconds: 1.45, rootLaneDamage: 22, rootLaneLengthInset: 36, rootLaneWidth: 28, rootRecover: 0.58, rootSnareSeconds: 0.85, rootWindup: 0.82, wallDisengageSeconds: 0.72, wallMargin: 70, } const VENOM_COLOR = '#b7ff4f' const ROOT_COLOR = '#d68b49' const BLOOM_COLOR = '#ff70d7' type PoisonConePlan = { direction: Iwt2Vec2 end: Iwt2Vec2 origin: Iwt2Vec2 } export function tickVenomOrchidHydra(state: Iwt2ArenaState, dt: number): Iwt2BossTickResult { const events: Iwt2ArenaEvent[] = [] let hazards = state.hazards let nextHazardId = state.nextHazardId let party = state.party 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 }, } const phase = venomOrchidHydraPhase(boss) boss = { ...boss, wallContactSeconds: phase === 'relocating' ? 0 : isBossNearWall(boss, state) ? boss.wallContactSeconds + dt : Math.max(0, boss.wallContactSeconds - dt * 2), } const target = getBossTarget(party) if (boss.health <= 0 || !target) { return withVenomOrchidHydraIndicators({ boss, party, hazards, events, nextHazardId }) } if (phase === 'relocating') { boss = moveBossTowardRelocationTarget(boss, state, target, dt) if (boss.phaseSecondsRemaining <= 0 || distanceVec2(boss.position, boss.relocateTarget) <= 8) { boss = { ...boss, attackPhase: 'idle', mechanicCircles: [], mechanicLanes: [], phaseSecondsRemaining: 0, velocity: { x: 0, y: 0 }, } } return withVenomOrchidHydraIndicators({ boss, party, hazards, events, nextHazardId }) } if (phase === VENOM_ORCHID_HYDRA_PHASE.coneWindup) { if (boss.phaseSecondsRemaining <= 0) { const result = applyPoisonCone(party, boss, state.time + dt) party = result.party events.push(...result.events) const nextHeadIndex = Math.floor(boss.mechanicEnergy) + 1 if (nextHeadIndex < 3) { const cone = createPoisonConePlan(boss, party, state, nextHeadIndex) boss = { ...boss, chargeEnd: cone.end, chargeHitEntityIds: result.hitEntityIds, chargeStart: cone.origin, facing: cone.direction, mechanicEnergy: nextHeadIndex, phaseSecondsRemaining: VENOM_ORCHID_HYDRA.coneStaggerWindup, } } else { boss = { ...boss, attackPhase: asBossPhase(VENOM_ORCHID_HYDRA_PHASE.coneRecover), chargeHitEntityIds: [], mechanicEnergy: 0, phaseSecondsRemaining: VENOM_ORCHID_HYDRA.coneRecover, } } } return withVenomOrchidHydraIndicators({ boss, party, hazards, events, nextHazardId }) } if (phase === VENOM_ORCHID_HYDRA_PHASE.coneRecover) { if (boss.phaseSecondsRemaining <= 0) { boss = { ...boss, attackPhase: 'idle', phaseSecondsRemaining: 0 } } return withVenomOrchidHydraIndicators({ boss, party, hazards, events, nextHazardId }) } if (phase === VENOM_ORCHID_HYDRA_PHASE.rootSnareWindup) { if (boss.phaseSecondsRemaining <= 0) { const result = applyRootSnareLanes(party, boss, state.time + dt) party = result.party events.push(...result.events) boss = { ...boss, attackPhase: asBossPhase(VENOM_ORCHID_HYDRA_PHASE.rootSnareRecover), phaseSecondsRemaining: VENOM_ORCHID_HYDRA.rootRecover, } } return withVenomOrchidHydraIndicators({ boss, party, hazards, events, nextHazardId }) } if (phase === VENOM_ORCHID_HYDRA_PHASE.rootSnareRecover) { if (boss.phaseSecondsRemaining <= 0) { boss = { ...boss, attackPhase: 'idle', mechanicLanes: [], phaseSecondsRemaining: 0, } } return withVenomOrchidHydraIndicators({ boss, party, hazards, events, nextHazardId }) } if (phase === VENOM_ORCHID_HYDRA_PHASE.bloomPodWindup) { if (boss.phaseSecondsRemaining <= 0) { const burst = applyBloomBursts(party, boss, state.time + dt) const pods = addBloomPoisonPods({ boss, hazards, nextHazardId, time: state.time + dt, }) party = burst.party hazards = pods.hazards nextHazardId = pods.nextHazardId events.push(...burst.events) boss = { ...boss, attackPhase: asBossPhase(VENOM_ORCHID_HYDRA_PHASE.bloomPodRecover), phaseSecondsRemaining: VENOM_ORCHID_HYDRA.bloomRecover, } } return withVenomOrchidHydraIndicators({ boss, party, hazards, events, nextHazardId }) } if (phase === VENOM_ORCHID_HYDRA_PHASE.bloomPodRecover) { if (boss.phaseSecondsRemaining <= 0) { boss = { ...boss, attackPhase: 'idle', mechanicCircles: [], phaseSecondsRemaining: 0, } } return withVenomOrchidHydraIndicators({ boss, party, hazards, events, nextHazardId }) } if (boss.wallContactSeconds >= VENOM_ORCHID_HYDRA.wallDisengageSeconds) { boss = { ...boss, attackPhase: 'relocating', mechanicCircles: [], mechanicLanes: [], phaseSecondsRemaining: VENOM_ORCHID_HYDRA.relocateSeconds, relocateTarget: relocationTarget(boss, state), velocity: { x: 0, y: 0 }, } return withVenomOrchidHydraIndicators({ boss, party, hazards, events, nextHazardId }) } if (boss.fireballCooldownRemaining <= 0) { boss = beginBloomPods(boss, state, party) return withVenomOrchidHydraIndicators({ boss, party, hazards, events, nextHazardId }) } if (boss.chargeCooldownRemaining <= 0) { boss = boss.chargeCount % 2 === 0 ? beginStaggeredPoisonCones(boss, state, party) : beginRootSnareLanes(boss, state, party) return withVenomOrchidHydraIndicators({ 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 withVenomOrchidHydraIndicators({ boss, party, hazards, events, nextHazardId }) boss = moveHydraTowardEngagePoint(boss, state, target, dt) return withVenomOrchidHydraIndicators({ boss, party, hazards, events, nextHazardId }) } function beginStaggeredPoisonCones( boss: Iwt2BossEntityState, state: Iwt2ArenaState, party: Iwt2PartyEntityState[], ): Iwt2BossEntityState { const cone = createPoisonConePlan(boss, party, state, 0) return { ...boss, attackPhase: asBossPhase(VENOM_ORCHID_HYDRA_PHASE.coneWindup), chargeCooldownRemaining: VENOM_ORCHID_HYDRA.coneCooldown, chargeCount: boss.chargeCount + 1, chargeEnd: cone.end, chargeHitEntityIds: [], chargeStart: cone.origin, facing: cone.direction, mechanicCircles: [], mechanicEnergy: 0, mechanicLanes: [], phaseSecondsRemaining: VENOM_ORCHID_HYDRA.coneStaggerWindup, velocity: { x: 0, y: 0 }, } } function beginRootSnareLanes( boss: Iwt2BossEntityState, state: Iwt2ArenaState, party: Iwt2PartyEntityState[], ): Iwt2BossEntityState { return { ...boss, attackPhase: asBossPhase(VENOM_ORCHID_HYDRA_PHASE.rootSnareWindup), chargeCooldownRemaining: VENOM_ORCHID_HYDRA.coneCooldown, chargeCount: boss.chargeCount + 1, mechanicCircles: [], mechanicLanes: createRootSnarePattern(state, party), phaseSecondsRemaining: VENOM_ORCHID_HYDRA.rootWindup, velocity: { x: 0, y: 0 }, } } function beginBloomPods( boss: Iwt2BossEntityState, state: Iwt2ArenaState, party: Iwt2PartyEntityState[], ): Iwt2BossEntityState { return { ...boss, attackPhase: asBossPhase(VENOM_ORCHID_HYDRA_PHASE.bloomPodWindup), fireballCooldownRemaining: VENOM_ORCHID_HYDRA.bloomCooldown, mechanicCircles: createBloomPodPattern(state, party, boss), mechanicLanes: [], phaseSecondsRemaining: VENOM_ORCHID_HYDRA.bloomWindup, velocity: { x: 0, y: 0 }, } } function createPoisonConePlan( boss: Iwt2BossEntityState, party: Iwt2PartyEntityState[], state: Iwt2ArenaState, headIndex: number, ): PoisonConePlan { const living = livingParty(party) const fallbackTarget = getBossTarget(party)?.position ?? arenaCenter(state) const target = living[(headIndex + boss.chargeCount) % Math.max(1, living.length)]?.position ?? fallbackTarget const headOffset = (headIndex - 1) * VENOM_ORCHID_HYDRA.headOffset const baseDirection = withFallbackFacing(subtractVec2(target, boss.position), boss.facing) const direction = rotateVec2(baseDirection, (headIndex - 1) * 0.28) const perpendicular = { x: -baseDirection.y, y: baseDirection.x } const origin = clampVec2ToArena( addVec2(boss.position, scaleVec2(perpendicular, headOffset)), 8, state.bounds, ) return { direction, end: addVec2(origin, scaleVec2(direction, VENOM_ORCHID_HYDRA.coneRange)), origin, } } function createRootSnarePattern( state: Iwt2ArenaState, party: Iwt2PartyEntityState[], ): Iwt2MechanicLaneState[] { const living = livingParty(party) const center = partyCenter(living, arenaCenter(state)) const selected = [ getPriorityMember(living, 'player')?.position ?? center, getPriorityMember(living, 'ranged')?.position ?? center, getPriorityMember(living, 'flanker')?.position ?? center, ] const directions = [ { x: 1, y: 0 }, { x: 0, y: 1 }, normalizeVec2({ x: state.bounds.width >= state.bounds.height ? 1 : 0.8, y: 0.72 }), ] return selected.map((position, index) => { const lane = createArenaLaneThroughPoint(position, directions[index], state) return { ...lane, id: `venom-orchid-root-snare-${index}`, width: VENOM_ORCHID_HYDRA.rootLaneWidth, } }) } function createBloomPodPattern( state: Iwt2ArenaState, party: Iwt2PartyEntityState[], boss: Iwt2BossEntityState, ): Iwt2MechanicCircleState[] { const living = livingParty(party) const center = partyCenter(living, arenaCenter(state)) const chosen = [ getPriorityMember(living, 'ranged')?.position, getPriorityMember(living, 'player')?.position, getPriorityMember(living, 'flanker')?.position, addVec2(center, scaleVec2(normalizeVec2(subtractVec2(center, boss.position)), 54)), ].filter((position): position is Iwt2Vec2 => Boolean(position)) return chosen.slice(0, VENOM_ORCHID_HYDRA.bloomPodCount).map((position, index) => ({ id: `venom-orchid-bloom-pod-${index}`, position: clampVec2ToArena(position, VENOM_ORCHID_HYDRA.bloomPodRadius, state.bounds), radius: VENOM_ORCHID_HYDRA.bloomPodRadius, })) } function applyPoisonCone( party: Iwt2PartyEntityState[], boss: Iwt2BossEntityState, time: number, ): { party: Iwt2PartyEntityState[], hitEntityIds: Iwt2EntityId[], events: Iwt2ArenaEvent[] } { const result = applyPartyDamageInShape(party, { angleRadians: VENOM_ORCHID_HYDRA.coneAngleRadians, direction: boss.facing, kind: 'cone', origin: boss.chargeStart, range: VENOM_ORCHID_HYDRA.coneRange, }, { damage: VENOM_ORCHID_HYDRA.coneDamage, excludedEntityIds: boss.chargeHitEntityIds, knockdownSeconds: 0, sourceId: boss.id, stunSeconds: VENOM_ORCHID_HYDRA.coneStunSeconds, time, }) return { ...result, hitEntityIds: [...boss.chargeHitEntityIds, ...result.hitEntityIds], } } function applyRootSnareLanes( 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: VENOM_ORCHID_HYDRA.rootLaneDamage, excludedEntityIds: hitEntityIds, knockdownSeconds: 0, sourceId: boss.id, stunSeconds: VENOM_ORCHID_HYDRA.rootSnareSeconds, time, }) nextParty = result.party hitEntityIds.push(...result.hitEntityIds) events.push(...result.events) } return { party: nextParty, events } } function applyBloomBursts( 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: VENOM_ORCHID_HYDRA.bloomBurstDamage, excludedEntityIds: hitEntityIds, knockdownSeconds: 0, sourceId: boss.id, stunSeconds: 0, time, }) nextParty = result.party hitEntityIds.push(...result.hitEntityIds) events.push(...result.events) } return { party: nextParty, events } } function addBloomPoisonPods({ boss, hazards, nextHazardId, time, }: { boss: Iwt2BossEntityState hazards: Iwt2GroundHazardState[] nextHazardId: number time: number }): { hazards: Iwt2GroundHazardState[], nextHazardId: number } { let nextHazards = hazards let nextId = nextHazardId for (const circle of boss.mechanicCircles) { const result = addPoisonPuddle({ damage: VENOM_ORCHID_HYDRA.bloomPodDamage, duration: VENOM_ORCHID_HYDRA.bloomPodSeconds, hazards: nextHazards, nextHazardId: nextId, position: circle.position, radius: circle.radius, sourceId: boss.id, time, }) nextHazards = result.hazards nextId = result.nextHazardId } return { hazards: nextHazards, nextHazardId: nextId } } 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) > VENOM_ORCHID_HYDRA.meleeRange + target.radius) { return { boss, party, events: [] } } const result = applyPartyDamageInShape(party, { kind: 'circle', position: boss.position, radius: VENOM_ORCHID_HYDRA.meleeRange, }, { damage: VENOM_ORCHID_HYDRA.meleeDamage, sourceId: boss.id, time, }) return { boss: { ...boss, meleeCooldownRemaining: VENOM_ORCHID_HYDRA.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, VENOM_ORCHID_HYDRA.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 moveHydraTowardEngagePoint( boss: Iwt2BossEntityState, state: Iwt2ArenaState, target: Iwt2PartyEntityState, dt: number, ): Iwt2BossEntityState { const center = arenaCenter(state) const targetNearWall = isPositionNearWall(target.position, target.radius + VENOM_ORCHID_HYDRA.wallMargin, state) const desired = targetNearWall ? addVec2(target.position, scaleVec2(normalizeVec2(subtractVec2(center, target.position)), 106)) : target.position const nextPosition = clampVec2ToArena( moveToward(boss.position, desired, VENOM_ORCHID_HYDRA.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 relocationTarget(boss: Iwt2BossEntityState, state: Iwt2ArenaState): Iwt2Vec2 { const center = arenaCenter(state) const awayFromWall = normalizeVec2({ x: boss.position.x < state.bounds.width * 0.5 ? 1 : -1, y: boss.position.y < state.bounds.height * 0.5 ? 1 : -1, }) return clampVec2ToArena(addVec2(center, scaleVec2(awayFromWall, 66)), boss.radius, state.bounds) } function createArenaLaneThroughPoint( point: Iwt2Vec2, direction: Iwt2Vec2, state: Iwt2ArenaState, ): { start: Iwt2Vec2, end: Iwt2Vec2 } { const dir = withFallbackFacing(direction, { x: 1, y: 0 }) const inset = VENOM_ORCHID_HYDRA.rootLaneLengthInset const minX = inset const maxX = state.bounds.width - inset const minY = inset const maxY = state.bounds.height - inset const candidates: number[] = [] if (Math.abs(dir.x) > 0.0001) { candidates.push((minX - point.x) / dir.x, (maxX - point.x) / dir.x) } if (Math.abs(dir.y) > 0.0001) { candidates.push((minY - point.y) / dir.y, (maxY - point.y) / dir.y) } const valid = candidates.filter((t) => { const position = addVec2(point, scaleVec2(dir, t)) return position.x >= minX - 0.1 && position.x <= maxX + 0.1 && position.y >= minY - 0.1 && position.y <= maxY + 0.1 }) const startT = Math.min(...valid) const endT = Math.max(...valid) return { end: addVec2(point, scaleVec2(dir, endT)), start: addVec2(point, scaleVec2(dir, startT)), } } function rotateVec2(vector: Iwt2Vec2, radians: number): Iwt2Vec2 { const sin = Math.sin(radians) const cos = Math.cos(radians) return normalizeVec2({ x: vector.x * cos - vector.y * sin, y: vector.x * sin + vector.y * cos, }) } function livingParty(party: Iwt2PartyEntityState[]): Iwt2PartyEntityState[] { return party.filter((member) => member.health > 0) } function getPriorityMember( party: Iwt2PartyEntityState[], role: Iwt2PartyEntityState['aiRole'], ): Iwt2PartyEntityState | undefined { return party.find((member) => member.aiRole === role) } 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 partyCenter(party: Iwt2PartyEntityState[], fallback: Iwt2Vec2): Iwt2Vec2 { if (party.length <= 0) return fallback const total = party.reduce((sum, member) => addVec2(sum, member.position), { x: 0, y: 0 }) return scaleVec2(total, 1 / party.length) } 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 + VENOM_ORCHID_HYDRA.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 withVenomOrchidHydraIndicators(result: Omit): Iwt2BossTickResult { return { ...result, indicators: createVenomOrchidHydraIndicators(result.boss), } } function createVenomOrchidHydraIndicators(boss: Iwt2BossEntityState): Iwt2ArenaIndicator[] { const indicators: Iwt2ArenaIndicator[] = [] const phase = venomOrchidHydraPhase(boss) if (phase === VENOM_ORCHID_HYDRA_PHASE.coneWindup || phase === VENOM_ORCHID_HYDRA_PHASE.coneRecover) { indicators.push(createConeIndicator({ angleRadians: VENOM_ORCHID_HYDRA.coneAngleRadians, color: VENOM_COLOR, direction: boss.facing, id: `${boss.id}:venom-orchid-hydra-cone-${Math.floor(boss.mechanicEnergy)}`, mechanicId: 'venom-orchid-hydra-poison-cone', origin: boss.chargeStart, phase: indicatorPhaseFromAttack( phase === VENOM_ORCHID_HYDRA_PHASE.coneWindup, phase === VENOM_ORCHID_HYDRA_PHASE.coneRecover, ), range: VENOM_ORCHID_HYDRA.coneRange, sourceId: boss.id, })) } if (phase === VENOM_ORCHID_HYDRA_PHASE.rootSnareWindup || phase === VENOM_ORCHID_HYDRA_PHASE.rootSnareRecover) { for (const lane of boss.mechanicLanes) { indicators.push(createLaneIndicator({ color: ROOT_COLOR, end: lane.end, id: `${boss.id}:${lane.id}`, mechanicId: 'venom-orchid-hydra-root-snare', phase: indicatorPhaseFromAttack( phase === VENOM_ORCHID_HYDRA_PHASE.rootSnareWindup, phase === VENOM_ORCHID_HYDRA_PHASE.rootSnareRecover, ), sourceId: boss.id, start: lane.start, width: lane.width, })) } } if (phase === VENOM_ORCHID_HYDRA_PHASE.bloomPodWindup || phase === VENOM_ORCHID_HYDRA_PHASE.bloomPodRecover) { for (const circle of boss.mechanicCircles) { indicators.push(createCircleIndicator({ color: BLOOM_COLOR, id: `${boss.id}:${circle.id}`, mechanicId: 'venom-orchid-hydra-bloom-pod', phase: indicatorPhaseFromAttack( phase === VENOM_ORCHID_HYDRA_PHASE.bloomPodWindup, phase === VENOM_ORCHID_HYDRA_PHASE.bloomPodRecover, ), position: circle.position, radius: circle.radius, sourceId: boss.id, })) } } return indicators } function venomOrchidHydraPhase(boss: Iwt2BossEntityState): string { return boss.attackPhase as string } function asBossPhase(phase: VenomOrchidHydraPhase): Iwt2BossEntityState['attackPhase'] { return phase as unknown as Iwt2BossEntityState['attackPhase'] }