import type { Iwt2BossTickResult } from './bossAi' import type { Iwt2ArenaEvent, Iwt2ArenaIndicator, Iwt2ArenaState, Iwt2BossEntityState, Iwt2EntityId, Iwt2GroundHazardState, Iwt2MechanicCircleState, Iwt2MechanicLaneState, Iwt2PartyEntityState, Iwt2Vec2, } from './types' import { addMudPuddle } from './hazards' import { applyPartyDamageInShape, createArenaEvent, createCircleIndicator, createLaneIndicator, indicatorPhaseFromAttack, } from './mechanics' import { addVec2, clamp, clampVec2ToArena, distanceVec2, moveToward, normalizeVec2, scaleVec2, subtractVec2, withFallbackFacing, } from './vector' const OBSIDIAN_RAM_PHASE = { armorShatterRecover: 'obsidianRamArmorShatterRecover', armorShatterWindup: 'obsidianRamArmorShatterWindup', fractureQuakeRecover: 'obsidianRamFractureQuakeRecover', fractureQuakeWindup: 'obsidianRamFractureQuakeWindup', plateChargeRecover: 'obsidianRamPlateChargeRecover', plateChargeWindup: 'obsidianRamPlateChargeWindup', plateCharging: 'obsidianRamPlateCharging', } as const type ObsidianRamPhase = typeof OBSIDIAN_RAM_PHASE[keyof typeof OBSIDIAN_RAM_PHASE] const OBSIDIAN_RAM = { armorCracksBeforeShatter: 3, centerDisengageSpeed: 118, chargeCooldown: 4.4, chargeDamage: 24, chargeLength: 340, chargeRecover: 0.5, chargeSpeed: 390, chargeStunSeconds: 0.55, chargeWidth: 54, chargeWindup: 0.68, fractureLaneCount: 5, fractureLaneDamage: 20, fractureLaneLength: 300, fractureLaneWidth: 30, meleeCooldown: 1.3, meleeDamage: 8, meleeRange: 62, moveSpeed: 82, quakeCooldown: 7.4, quakeRadius: 126, quakeRecover: 0.48, quakeStunSeconds: 0.35, quakeWindup: 0.78, slabDamage: 22, slabDuration: 5.8, slabRadius: 42, slabSlowRadius: 46, shatterRecover: 0.8, shatterWindup: 0.9, wallContactLimit: 0.72, wallMargin: 72, } as const export function tickObsidianRamGolem(state: Iwt2ArenaState, dt: number): Iwt2BossTickResult { const events: Iwt2ArenaEvent[] = [] let hazards = state.hazards let nextHazardId = state.nextHazardId let party = state.party const incomingPhase = obsidianRamPhase(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 }, wallContactSeconds: isLockedPhase(incomingPhase) ? 0 : 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 withObsidianRamIndicators({ boss, party, hazards, events, nextHazardId }) } const phase = obsidianRamPhase(boss) 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), mechanicCircles: [], mechanicLanes: [], phaseSecondsRemaining: 0, velocity: { x: 0, y: 0 }, } } return withObsidianRamIndicators({ boss, party, hazards, events, nextHazardId }) } if (phase === OBSIDIAN_RAM_PHASE.plateChargeWindup) { boss = { ...boss, facing: withFallbackFacing(subtractVec2(boss.chargeEnd, boss.position), boss.facing), velocity: { x: 0, y: 0 }, } if (boss.phaseSecondsRemaining <= 0) { boss = { ...boss, attackPhase: asBossPhase(OBSIDIAN_RAM_PHASE.plateCharging), chargeHitEntityIds: [], phaseSecondsRemaining: 1.25, } } return withObsidianRamIndicators({ boss, party, hazards, events, nextHazardId }) } if (phase === OBSIDIAN_RAM_PHASE.plateCharging) { const nextPosition = clampVec2ToArena( moveToward(boss.position, boss.chargeEnd, OBSIDIAN_RAM.chargeSpeed * dt), boss.radius, state.bounds, ) const hitResult = applyChargeHits(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), } if (distanceVec2(nextPosition, boss.chargeEnd) <= 2 || boss.phaseSecondsRemaining <= 0) { const crackedArmor = clamp(boss.mechanicEnergy + 1, 0, OBSIDIAN_RAM.armorCracksBeforeShatter) boss = { ...boss, attackPhase: asBossPhase(OBSIDIAN_RAM_PHASE.plateChargeRecover), mechanicEnergy: crackedArmor, phaseSecondsRemaining: OBSIDIAN_RAM.chargeRecover, velocity: { x: 0, y: 0 }, wallContactSeconds: 0, } } return withObsidianRamIndicators({ boss, party, hazards, events, nextHazardId }) } if (phase === OBSIDIAN_RAM_PHASE.plateChargeRecover) { if (boss.phaseSecondsRemaining <= 0) { boss = boss.mechanicEnergy >= OBSIDIAN_RAM.armorCracksBeforeShatter ? beginArmorShatter(boss, state, party) : { ...boss, attackPhase: 'idle', phaseSecondsRemaining: 0, } } return withObsidianRamIndicators({ boss, party, hazards, events, nextHazardId }) } if (phase === OBSIDIAN_RAM_PHASE.fractureQuakeWindup) { boss = { ...boss, velocity: { x: 0, y: 0 } } if (boss.phaseSecondsRemaining <= 0) { const result = applyFractureQuake(party, boss, state.time + dt) party = result.party events.push(...result.events) boss = { ...boss, attackPhase: asBossPhase(OBSIDIAN_RAM_PHASE.fractureQuakeRecover), phaseSecondsRemaining: OBSIDIAN_RAM.quakeRecover, } } return withObsidianRamIndicators({ boss, party, hazards, events, nextHazardId }) } if (phase === OBSIDIAN_RAM_PHASE.fractureQuakeRecover) { if (boss.phaseSecondsRemaining <= 0) { boss = { ...boss, attackPhase: 'idle', mechanicCircles: [], mechanicLanes: [], phaseSecondsRemaining: 0, } } return withObsidianRamIndicators({ boss, party, hazards, events, nextHazardId }) } if (phase === OBSIDIAN_RAM_PHASE.armorShatterWindup) { boss = { ...boss, velocity: { x: 0, y: 0 } } if (boss.phaseSecondsRemaining <= 0) { const slabResult = dropObsidianSlabs({ boss, hazards, nextHazardId, party, state, time: state.time + dt, }) party = slabResult.party hazards = slabResult.hazards nextHazardId = slabResult.nextHazardId events.push(...slabResult.events) boss = { ...boss, attackPhase: asBossPhase(OBSIDIAN_RAM_PHASE.armorShatterRecover), mechanicEnergy: 0, phaseSecondsRemaining: OBSIDIAN_RAM.shatterRecover, } } return withObsidianRamIndicators({ boss, party, hazards, events, nextHazardId }) } if (phase === OBSIDIAN_RAM_PHASE.armorShatterRecover) { if (boss.phaseSecondsRemaining <= 0) { boss = { ...boss, attackPhase: 'idle', mechanicCircles: [], phaseSecondsRemaining: 0, } } return withObsidianRamIndicators({ boss, party, hazards, events, nextHazardId }) } if (boss.wallContactSeconds >= OBSIDIAN_RAM.wallContactLimit) { boss = { ...boss, attackPhase: 'relocating', mechanicCircles: [], mechanicLanes: [], phaseSecondsRemaining: 1.3, relocateTarget: relocationTarget(boss, state), velocity: { x: 0, y: 0 }, } return withObsidianRamIndicators({ boss, party, hazards, events, nextHazardId }) } if (boss.mechanicEnergy >= OBSIDIAN_RAM.armorCracksBeforeShatter) { boss = beginArmorShatter(boss, state, party) return withObsidianRamIndicators({ boss, party, hazards, events, nextHazardId }) } if (boss.fireballCooldownRemaining <= 0) { boss = { ...boss, attackPhase: asBossPhase(OBSIDIAN_RAM_PHASE.fractureQuakeWindup), fireballCooldownRemaining: OBSIDIAN_RAM.quakeCooldown, mechanicCircles: [{ id: 'obsidian-ram-quake-core', position: { ...boss.position }, radius: OBSIDIAN_RAM.quakeRadius }], mechanicLanes: createFractureWaveLanes(state, boss, target.position), phaseSecondsRemaining: OBSIDIAN_RAM.quakeWindup, velocity: { x: 0, y: 0 }, } return withObsidianRamIndicators({ boss, party, hazards, events, nextHazardId }) } if (boss.chargeCooldownRemaining <= 0) { boss = beginPlateCharge(boss, state, target) events.push(createArenaEvent(0, state.time + dt, 'bossChargeStart', boss.id, target.id)) return withObsidianRamIndicators({ 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 withObsidianRamIndicators({ boss, party, hazards, events, nextHazardId }) boss = moveBossTowardEngagePoint(boss, state, target, dt) return withObsidianRamIndicators({ boss, party, hazards, events, nextHazardId }) } function beginPlateCharge( boss: Iwt2BossEntityState, state: Iwt2ArenaState, target: Iwt2PartyEntityState, ): Iwt2BossEntityState { const direction = wallSafeChargeDirection(boss, state, target.position) const chargeEnd = clampVec2ToArena( addVec2(boss.position, scaleVec2(direction, OBSIDIAN_RAM.chargeLength)), boss.radius, state.bounds, ) return { ...boss, attackPhase: asBossPhase(OBSIDIAN_RAM_PHASE.plateChargeWindup), chargeCooldownRemaining: OBSIDIAN_RAM.chargeCooldown, chargeEnd, chargeHitEntityIds: [], chargeStart: { ...boss.position }, facing: direction, mechanicLanes: [{ end: chargeEnd, id: 'obsidian-ram-plate-charge', start: { ...boss.position }, width: OBSIDIAN_RAM.chargeWidth, }], phaseSecondsRemaining: OBSIDIAN_RAM.chargeWindup, velocity: { x: 0, y: 0 }, } } function beginArmorShatter( boss: Iwt2BossEntityState, state: Iwt2ArenaState, party: Iwt2PartyEntityState[], ): Iwt2BossEntityState { const slabCircles = createObsidianSlabCircles(state, boss, party) return { ...boss, attackPhase: asBossPhase(OBSIDIAN_RAM_PHASE.armorShatterWindup), mechanicCircles: slabCircles, mechanicLanes: [], phaseSecondsRemaining: OBSIDIAN_RAM.shatterWindup, velocity: { x: 0, y: 0 }, } } function applyChargeHits( 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: OBSIDIAN_RAM.chargeWidth, }, { damage: OBSIDIAN_RAM.chargeDamage, damageEventType: 'bossChargeHit', excludedEntityIds: boss.chargeHitEntityIds, knockdownSeconds: OBSIDIAN_RAM.chargeStunSeconds, sourceId: boss.id, stunSeconds: OBSIDIAN_RAM.chargeStunSeconds, time, }) return { ...result, hitEntityIds: [...boss.chargeHitEntityIds, ...result.hitEntityIds], } } function applyFractureQuake( party: Iwt2PartyEntityState[], boss: Iwt2BossEntityState, time: number, ): { party: Iwt2PartyEntityState[], events: Iwt2ArenaEvent[] } { let nextParty = party const events: Iwt2ArenaEvent[] = [ createArenaEvent(0, time, 'bossSlam', boss.id, undefined, OBSIDIAN_RAM.quakeRadius), ] const quakeResult = applyPartyDamageInShape(nextParty, { kind: 'circle', position: boss.position, radius: OBSIDIAN_RAM.quakeRadius, }, { damage: OBSIDIAN_RAM.fractureLaneDamage, knockdownSeconds: OBSIDIAN_RAM.quakeStunSeconds, sourceId: boss.id, stunSeconds: OBSIDIAN_RAM.quakeStunSeconds, time, }) nextParty = quakeResult.party events.push(...quakeResult.events) for (const lane of boss.mechanicLanes) { const laneResult = applyPartyDamageInShape(nextParty, { end: lane.end, kind: 'lane', start: lane.start, width: lane.width, }, { damage: OBSIDIAN_RAM.fractureLaneDamage, knockdownSeconds: OBSIDIAN_RAM.quakeStunSeconds, sourceId: boss.id, stunSeconds: OBSIDIAN_RAM.quakeStunSeconds, time, }) nextParty = laneResult.party events.push(...laneResult.events) } return { events, party: nextParty } } function dropObsidianSlabs({ boss, hazards, nextHazardId, party, state, time, }: { boss: Iwt2BossEntityState hazards: Iwt2GroundHazardState[] nextHazardId: number party: Iwt2PartyEntityState[] state: Iwt2ArenaState time: number }): { events: Iwt2ArenaEvent[] hazards: Iwt2GroundHazardState[] nextHazardId: number party: Iwt2PartyEntityState[] } { let nextParty = party let nextHazards = hazards let nextId = nextHazardId const events: Iwt2ArenaEvent[] = [] for (const slab of boss.mechanicCircles) { const hitResult = applyPartyDamageInShape(nextParty, { kind: 'circle', position: slab.position, radius: slab.radius, }, { damage: OBSIDIAN_RAM.slabDamage, knockdownSeconds: 0.22, sourceId: boss.id, stunSeconds: 0.22, time, }) nextParty = hitResult.party events.push(...hitResult.events) const hazardResult = addMudPuddle({ duration: OBSIDIAN_RAM.slabDuration, hazards: nextHazards, nextHazardId: nextId, position: clampVec2ToArena(slab.position, OBSIDIAN_RAM.slabSlowRadius, state.bounds), radius: OBSIDIAN_RAM.slabSlowRadius, sourceId: boss.id, time, }) nextHazards = hazardResult.hazards nextId = hazardResult.nextHazardId } return { events, hazards: nextHazards, nextHazardId: nextId, party: nextParty } } 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) > OBSIDIAN_RAM.meleeRange + target.radius) { return { boss, party, events: [] } } const result = applyPartyDamageInShape(party, { kind: 'circle', position: boss.position, radius: OBSIDIAN_RAM.meleeRange, }, { damage: OBSIDIAN_RAM.meleeDamage, sourceId: boss.id, time, }) return { boss: { ...boss, meleeCooldownRemaining: OBSIDIAN_RAM.meleeCooldown, velocity: { x: 0, y: 0 } }, events: result.events, party: result.party, } } function createFractureWaveLanes( state: Iwt2ArenaState, boss: Iwt2BossEntityState, targetPosition: Iwt2Vec2, ): Iwt2MechanicLaneState[] { const baseDirection = withFallbackFacing(subtractVec2(targetPosition, boss.position), boss.facing) const baseAngle = Math.atan2(baseDirection.y, baseDirection.x) const spread = Math.PI * 0.82 const lanes: Iwt2MechanicLaneState[] = [] const laneCount: number = OBSIDIAN_RAM.fractureLaneCount for (let index = 0; index < laneCount; index += 1) { const ratio = laneCount === 1 ? 0.5 : index / (laneCount - 1) const angle = baseAngle - spread * 0.5 + spread * ratio const direction = { x: Math.cos(angle), y: Math.sin(angle) } lanes.push({ end: clampVec2ToArena( addVec2(boss.position, scaleVec2(direction, OBSIDIAN_RAM.fractureLaneLength)), OBSIDIAN_RAM.fractureLaneWidth, state.bounds, ), id: `obsidian-ram-fracture-${index}`, start: { ...boss.position }, width: OBSIDIAN_RAM.fractureLaneWidth, }) } return lanes } function createObsidianSlabCircles( state: Iwt2ArenaState, boss: Iwt2BossEntityState, party: Iwt2PartyEntityState[], ): Iwt2MechanicCircleState[] { const living = party.filter((member) => member.health > 0) const targetPositions = living .filter((member) => member.aiRole === 'ranged' || member.aiRole === 'flanker' || member.aiRole === 'player') .slice(0, 3) .map((member) => member.position) const fallbackAngles = [0, (Math.PI * 2) / 3, (Math.PI * 4) / 3] while (targetPositions.length < 3) { const angle = fallbackAngles[targetPositions.length] targetPositions.push(addVec2(boss.position, { x: Math.cos(angle) * OBSIDIAN_RAM.quakeRadius * 1.05, y: Math.sin(angle) * OBSIDIAN_RAM.quakeRadius * 1.05, })) } return targetPositions.map((position, index) => ({ id: `obsidian-ram-slab-${index}`, position: clampVec2ToArena(position, OBSIDIAN_RAM.slabRadius, state.bounds), radius: OBSIDIAN_RAM.slabRadius, })) } function moveBossTowardEngagePoint( boss: Iwt2BossEntityState, state: Iwt2ArenaState, target: Iwt2PartyEntityState, dt: number, ): Iwt2BossEntityState { 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.8)), targetBias) const desiredPosition = addVec2(boss.position, scaleVec2(direction, OBSIDIAN_RAM.moveSpeed * dt)) const nextPosition = clampVec2ToArena(desiredPosition, 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 moveBossTowardRelocationTarget( boss: Iwt2BossEntityState, state: Iwt2ArenaState, target: Iwt2PartyEntityState, dt: number, ): Iwt2BossEntityState { const nextPosition = clampVec2ToArena( moveToward(boss.position, boss.relocateTarget, OBSIDIAN_RAM.centerDisengageSpeed * 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 wallSafeChargeDirection( boss: Iwt2BossEntityState, state: Iwt2ArenaState, targetPosition: Iwt2Vec2, ): Iwt2Vec2 { const targetBias = normalizeVec2(subtractVec2(targetPosition, boss.position)) if (!isBossNearWall(boss, state)) return withFallbackFacing(targetBias, boss.facing) const centerBias = normalizeVec2(subtractVec2(arenaCenter(state), boss.position)) return withFallbackFacing(addVec2(scaleVec2(centerBias, 1.35), scaleVec2(targetBias, 0.55)), centerBias) } function relocationTarget(boss: Iwt2BossEntityState, state: Iwt2ArenaState): Iwt2Vec2 { const center = arenaCenter(state) const inward = normalizeVec2(subtractVec2(center, boss.position)) const offset = addVec2(scaleVec2(inward, 138), { x: boss.position.y < center.y ? 42 : -42, y: boss.position.x < center.x ? -34 : 34, }) return clampVec2ToArena(addVec2(boss.position, offset), boss.radius, state.bounds) } function isBossNearWall(boss: Iwt2BossEntityState, state: Iwt2ArenaState): boolean { const margin = boss.radius + OBSIDIAN_RAM.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 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 withObsidianRamIndicators(result: Omit): Iwt2BossTickResult { return { ...result, indicators: createObsidianRamIndicators(result.boss), } } function createObsidianRamIndicators(boss: Iwt2BossEntityState): Iwt2ArenaIndicator[] { const indicators: Iwt2ArenaIndicator[] = [] const phase = obsidianRamPhase(boss) if (phase === OBSIDIAN_RAM_PHASE.plateChargeWindup || phase === OBSIDIAN_RAM_PHASE.plateCharging) { indicators.push(createLaneIndicator({ color: phase === OBSIDIAN_RAM_PHASE.plateCharging ? '#ff7a2f' : '#f0b35a', end: boss.chargeEnd, id: `${boss.id}:obsidian-ram-plate-charge`, mechanicId: 'obsidian-ram-plate-charge', phase: indicatorPhaseFromAttack( phase === OBSIDIAN_RAM_PHASE.plateChargeWindup, phase === OBSIDIAN_RAM_PHASE.plateCharging, ), sourceId: boss.id, start: boss.chargeStart, width: OBSIDIAN_RAM.chargeWidth, })) } if (phase === OBSIDIAN_RAM_PHASE.fractureQuakeWindup || phase === OBSIDIAN_RAM_PHASE.fractureQuakeRecover) { const indicatorPhase = indicatorPhaseFromAttack( phase === OBSIDIAN_RAM_PHASE.fractureQuakeWindup, phase === OBSIDIAN_RAM_PHASE.fractureQuakeRecover, ) for (const circle of boss.mechanicCircles) { indicators.push(createCircleIndicator({ color: '#ff9f1c', id: `${boss.id}:${circle.id}`, mechanicId: 'obsidian-ram-quake-core', phase: indicatorPhase, position: circle.position, radius: circle.radius, sourceId: boss.id, })) } for (const lane of boss.mechanicLanes) { indicators.push(createLaneIndicator({ color: '#ffbf69', end: lane.end, id: `${boss.id}:${lane.id}`, mechanicId: 'obsidian-ram-fracture-wave', phase: indicatorPhase, sourceId: boss.id, start: lane.start, width: lane.width, })) } } if (phase === OBSIDIAN_RAM_PHASE.armorShatterWindup || phase === OBSIDIAN_RAM_PHASE.armorShatterRecover) { for (const circle of boss.mechanicCircles) { indicators.push(createCircleIndicator({ color: '#b8b6c7', id: `${boss.id}:${circle.id}`, mechanicId: 'obsidian-ram-broken-slab', phase: indicatorPhaseFromAttack( phase === OBSIDIAN_RAM_PHASE.armorShatterWindup, phase === OBSIDIAN_RAM_PHASE.armorShatterRecover, ), position: circle.position, radius: circle.radius, sourceId: boss.id, })) } } return indicators } function isLockedPhase(phase: string): boolean { return phase === OBSIDIAN_RAM_PHASE.plateChargeWindup || phase === OBSIDIAN_RAM_PHASE.plateCharging || phase === OBSIDIAN_RAM_PHASE.fractureQuakeWindup || phase === OBSIDIAN_RAM_PHASE.fractureQuakeRecover || phase === OBSIDIAN_RAM_PHASE.armorShatterWindup || phase === OBSIDIAN_RAM_PHASE.armorShatterRecover } function obsidianRamPhase(boss: Iwt2BossEntityState): string { return boss.attackPhase as string } function asBossPhase(phase: ObsidianRamPhase): Iwt2BossEntityState['attackPhase'] { return phase as unknown as Iwt2BossEntityState['attackPhase'] }