import {
  getActionAiAvoidance,
  getActionAttack,
  getEnabledActionAttack,
  type ActionAiAvoidanceConfig,
  type ActionAttackKind,
} from './actionEncounterConfig'
import {
  ACTION_CONTROL_RULES,
  ACTION_MOVEMENT_RULES,
  createSpellCooldowns,
  createSpellResourceState,
  addShield,
  applyHealOverTimeTimers,
  applyShieldedDamage,
  getActiveHealOverTimeFrames,
  getActionSpellDefinition,
  getActionSpellReach,
  getActionSpellTarget,
  getActionTalentAdjustedAmount,
  getHealOverTimeEffectId,
  RANGED_ATTACK_RANGE,
  recordSpellCast,
  ARENA_PRIEST_SPELL_BAR,
  DUNGEON_PRIEST_SPELL_BAR,
  SPELL_GLOBAL_COOLDOWN_SECONDS,
  SPELLS,
  spendActionSpellMana,
  tickHealOverTimes,
  tickSpellCooldowns as tickSharedSpellCooldowns,
  tickSpellResource,
  type CastState,
  type HealOverTimeEffect,
  type PartyRole,
  type SpellDefinition,
  type SpellResourceState,
  type SpellSlot,
  type ActionSpellReachResult,
} from '../actionCombatCore'
import { getArenaAbility, getArenaClassVitals, type ArenaClassId } from '../actionClassKits'
import type { TalentModifiers } from '../claudeCraftTalents'
import { createEmptyTalentModifiers } from '../actionCombatProfile'
import {
  type BossInput,
  type BossPhase,
  type Point,
  type ArcTelegraph,
  type RaidFrame,
  type Telegraph,
} from '../actionCombatTypes'
import {
  getEnemyBodyDamage,
  getEnemyChargeDamage,
  getEnemyChargeSeconds,
  getEnemyChargeSpeed,
  getEnemyRecoverSeconds,
  getEnemyTankFocusSeconds,
  getEnemyTankSwingDamage,
  getEnemyTankSwingSeconds,
  getEnemyTrackSeconds,
  getEnemyWindupSeconds,
} from './actionEnemyRuntime'
import { getActionDifficultyTier, type ActionDifficulty, type ActionDungeonId, type ActionRunMode } from '../actionMode'
import type { PlayerClass } from '../claudeCraftTypes'

export type { ActionDungeonId, ActionRunMode } from '../actionMode'
export { ARENA_PRIEST_SPELL_BAR, DUNGEON_PRIEST_SPELL_BAR, SPELLS }
export type { CastState, PartyRole, SpellDefinition, SpellSlot }
export type EnemyKind =
  | 'bulldrome'
  | 'bullfango'
  | 'yian-kut-ku'
  | 'bird'
  | 'cyber-dragon'
  | 'claudecraft-mob'
  | 'morthen-the-gravecaller'
  | 'vael-the-mistcaller'
  | 'korzul-the-gravewyrm'
export type EncounterStep = 'trash' | 'boss'
type BirdFlightStep = 'ground' | 'toLeft' | 'across' | 'toTank'

export type FighterState = {
  x: number
  y: number
  radius: number
  hp: number
  maxHp: number
}

export type TargetableState = FighterState & {
  id: string
  name: string
  role: PartyRole
  shield: number
  stunTimer: number
  invulnerableTimer: number
  healOverTimes?: HealOverTimeEffect[]
  renewTimer: number
  renewTickTimer: number
  burnTimer: number
  burnTickTimer: number
  bleedTimer: number
  bleedTickTimer: number
}

export type PartyMemberState = TargetableState & {
  aiSlot: number
}

export type EnemyState = FighterState & {
  id: string
  name: string
  kind: EnemyKind
  phase: BossPhase
  phaseTimer: number
  chargeVector: Point
  targetId: string
  chargeCount: number
  tankSwingTimer: number
  flightStep: BirdFlightStep
  flightY: number
  orbitAngle: number
  mechanicTimers: Partial<Record<ActionAttackKind, number>>
  arcTelegraph: ArcTelegraph
  telegraph: Telegraph
  slamTelegraph: {
    active: boolean
    radius: number
  }
}

export type BulldromeState = {
  dungeonId: ActionDungeonId
  difficulty: ActionDifficulty
  runMode: ActionRunMode
  encounterStep: EncounterStep
  arena: {
    width: number
    height: number
    padding: number
  }
  player: TargetableState & {
    classId: PlayerClass
    currentCast: CastState | null
    facingX: number
    facingY: number
    globalCooldown: number
    spellCooldowns: Record<SpellSlot, number>
    talentModifiers: TalentModifiers
  } & SpellResourceState
  party: PartyMemberState[]
  targetId: string
  boss: EnemyState
  adds: EnemyState[]
  fireballs: FireballState[]
  fireSpots: FireSpotState[]
  spinningBlades: SpinningBladeState[]
  nextHazardId: number
  damageDoneByUnit: Record<string, number>
  bossKills: number
  dungeonWaveIndex: number
  dungeonWaveTotal: number
  respawnTimer: number
  healEvents: HealTextEvent[]
  noticeEvents: NoticeTextEvent[]
  elapsed: number
  message: string
  result: 'playing' | 'win' | 'loss'
  lastHit: 'player' | 'party' | 'boss' | 'heal' | null
}

export type FireballState = {
  id: string
  x: number
  y: number
  radius: number
  vx: number
  vy: number
  bouncesRemaining: number
}

export type FireSpotState = {
  id: string
  x: number
  y: number
  radius: number
  remaining: number
}

export type SpinningBladeState = {
  id: string
  sourceKind: EnemyKind
  x: number
  y: number
  radius: number
  speed: number
  remaining: number
  targetId: string
  rotation: number
}

export type EnemyFrame = {
  id: string
  name: string
  kind: EnemyKind
  hp: number
  maxHp: number
  phase: BossPhase
}

export type HealTextEvent = {
  id: string
  targetId: string
  amount: number
}

export type NoticeTextEvent = {
  id: string
  targetId: string
  text: string
}

type CombatSpellTarget = TargetableState | EnemyState

const BOSS_SLAM_CAST_SECONDS = 1.25
const BOSS_SLAM_RADIUS = 142
const DUNGEON_ARENA_HEIGHT = 648
const DUNGEON_ARENA_PADDING = 42
const DUNGEON_ARENA_WIDTH = 1152
const YIAN_FIREBALL_WINDUP_SECONDS = 1
const FIREBALL_SPEED = 190
const FIREBALL_DAMAGE = 16
const FIRE_DOT_SECONDS = 5
const FIRE_DOT_TICK_SECONDS = 1
const FIRE_DOT_DAMAGE = 5
const FIRE_SPOT_SECONDS = 20
const MAX_FIRE_SPOTS = 8
const CYBER_BLADE_SPAWN_SECONDS = 10
const CYBER_BLADE_SECONDS = 15
const CYBER_BLADE_SPEED = 96
const CYBER_BLADE_RADIUS = 24
const CYBER_BLEED_SECONDS = 8
const CYBER_BLEED_TICK_SECONDS = 1
const CYBER_BLEED_DAMAGE = 3
const CYBER_CIRCLE_SECONDS = 1.35
const CYBER_CIRCLE_RADIUS = 124
const CYBER_CHARGE_SECONDS = 0.72
const CYBER_CHARGE_STUN_SECONDS = 1
const AI_MOVE_SPEED = 132
const TANK_DPS = 4
const MELEE_DPS = 10.5
const RANGED_DPS = 8.75

function getInitialArena(dungeonId: ActionDungeonId) {
  void dungeonId
  return {
    width: DUNGEON_ARENA_WIDTH,
    height: DUNGEON_ARENA_HEIGHT,
    padding: DUNGEON_ARENA_PADDING,
  }
}

function getInitialPartyStart(dungeonId: ActionDungeonId) {
  void dungeonId
  const centerX = DUNGEON_ARENA_WIDTH / 2
  const centerY = DUNGEON_ARENA_HEIGHT / 2
  return {
    player: { x: centerX, y: centerY + 134 },
    tank: { x: centerX, y: centerY - 92 },
    melee: { x: centerX, y: centerY - 50 },
    ranged1: { x: centerX - 120, y: centerY + 106 },
    ranged2: { x: centerX + 120, y: centerY + 106 },
  }
}

function toArenaClassId(classId: PlayerClass): ArenaClassId {
  if (classId === 'hunter') return 'ranger'
  if (classId === 'warrior' || classId === 'mage' || classId === 'rogue' || classId === 'paladin' || classId === 'shaman' || classId === 'druid' || classId === 'priest') return classId
  return 'priest'
}

export function createBulldromeState(
  difficulty: ActionDifficulty = 'ilvl-1',
  dungeonId: ActionDungeonId = 'bulldrome',
  runMode: ActionRunMode = 'hunt',
  playerClassId: PlayerClass = 'priest',
  playerTalentModifiers?: TalentModifiers,
): BulldromeState {
  const dungeonWaveTotal = 0
  const arena = getInitialArena(dungeonId)
  const partyStart = getInitialPartyStart(dungeonId)
  const playerVitals = getArenaClassVitals(toArenaClassId(playerClassId))
  const state: BulldromeState = {
    dungeonId,
    difficulty,
    runMode,
    encounterStep: 'boss',
    arena,
    player: {
      id: 'player',
      name: 'You',
      classId: playerClassId,
      role: 'healer',
      x: partyStart.player.x,
      y: partyStart.player.y,
      radius: 15,
      hp: playerVitals.maxHp,
      maxHp: playerVitals.maxHp,
	      shield: 0,
	      stunTimer: 0,
	      invulnerableTimer: 0,
	      healOverTimes: [],
	      renewTimer: 0,
      renewTickTimer: 0,
      burnTimer: 0,
      burnTickTimer: 0,
      bleedTimer: 0,
      bleedTickTimer: 0,
      currentCast: null,
      facingX: 0,
      facingY: -1,
      globalCooldown: 0,
      spellCooldowns: createSpellCooldowns(),
      talentModifiers: playerTalentModifiers ?? createEmptyTalentModifiers(),
      ...createSpellResourceState(playerClassId),
    },
    party: [
      createPartyMember('tank', 'Brakka', 'tank', getArenaClassVitals('warrior_protection').maxHp, partyStart.tank.x, partyStart.tank.y, 0),
      createPartyMember('melee-1', 'Kael', 'melee', getArenaClassVitals('rogue_combat').maxHp, partyStart.melee.x, partyStart.melee.y, 1),
      createPartyMember('ranged-1', 'Sera', 'ranged', getArenaClassVitals('mage_fire').maxHp, partyStart.ranged1.x, partyStart.ranged1.y, 3),
      createPartyMember('ranged-2', 'Nix', 'ranged', getArenaClassVitals('hunter_marksmanship').maxHp, partyStart.ranged2.x, partyStart.ranged2.y, 4),
    ],
    targetId: 'tank',
    boss: createInitialEnemy(dungeonId),
    adds: [],
    fireballs: [],
    fireSpots: [],
    spinningBlades: [],
    nextHazardId: 0,
    damageDoneByUnit: createDamageDoneTracker(),
    bossKills: 0,
    dungeonWaveIndex: 0,
    dungeonWaveTotal,
    respawnTimer: 0,
    healEvents: [],
    noticeEvents: [],
    elapsed: 0,
    message: getDungeonStartMessage(dungeonId),
    result: 'playing',
    lastHit: null,
  }

  applyDifficultyScaling(state)

  return state
}

export function updateBulldromeState(
  state: BulldromeState,
  input: BossInput,
  deltaSeconds: number,
): BulldromeState {
  if (input.reset) return createBulldromeState(state.difficulty, state.dungeonId, state.runMode, state.player.classId, state.player.talentModifiers)

  const next = cloneState(state)
  next.elapsed += deltaSeconds
  next.lastHit = null
  next.healEvents = []
  next.noticeEvents = []

  if (input.targetId) setTarget(next, input.targetId)
  if (input.targetDelta !== 0) cycleTarget(next, input.targetDelta)

  if (next.result !== 'playing') return next

  tickTargetable(next, next.player, deltaSeconds)
  tickPlayerGlobalCooldown(next, deltaSeconds)
  tickSpellCooldowns(next.player.spellCooldowns, deltaSeconds)
  tickSpellResource(next.player, deltaSeconds)
  tickCurrentCast(next, deltaSeconds)
  for (const member of next.party) tickTargetable(next, member, deltaSeconds)

  movePlayer(next, input, deltaSeconds)

  if (next.respawnTimer > 0) {
    next.respawnTimer = Math.max(0, next.respawnTimer - deltaSeconds)
    if (next.respawnTimer <= 0) respawnBossEncounter(next)
    else next.message = `Marathon break. Next boss in ${Math.ceil(next.respawnTimer)}.`
    startPlayerSpell(next, input.castSpell)
    resolveResult(next)
    return next
  }

  updatePartyAi(next, deltaSeconds)
  resolvePartyDamage(next, deltaSeconds)
  updateEnemies(next, deltaSeconds)
  updateFireHazards(next, deltaSeconds)
  updateSpinningBlades(next, deltaSeconds)
  resolveContact(next)
  startPlayerSpell(next, input.castSpell)
  resolveResult(next)

  return next
}

export function getRaidFrames(state: BulldromeState): RaidFrame[] {
  return [state.player, ...state.party].map((unit) => ({
    id: unit.id,
    name: unit.name,
    role: unit.role,
    hp: unit.hp,
    maxHp: unit.maxHp,
    healOverTimes: getActiveHealOverTimeFrames(unit),
    renewTimer: unit.renewTimer,
    burnTimer: unit.burnTimer,
    bleedTimer: unit.bleedTimer,
    shield: unit.shield,
    selected: unit.id === state.targetId,
    damageDone: state.damageDoneByUnit[unit.id] ?? 0,
  }))
}

export function getEnemyFrames(state: BulldromeState): EnemyFrame[] {
  return getLivingEnemies(state).map((enemy) => ({
    id: enemy.id,
    name: enemy.name,
    kind: enemy.kind,
    hp: enemy.hp,
    maxHp: enemy.maxHp,
    phase: enemy.phase,
  }))
}

export function getEncounterTitle(state: BulldromeState) {
  if (state.dungeonId === 'cyber-dragon') return 'Cyber Dragon'
  if (state.dungeonId === 'yian-kut-ku') return 'Yian Kut-Ku'
  return 'Bulldrome'
}

export function getEncounterHp(state: BulldromeState) {
  const enemies = getAllEnemies(state)
  const hp = enemies.reduce((sum, enemy) => sum + Math.max(0, enemy.hp), 0)
  const maxHp = enemies.reduce((sum, enemy) => sum + enemy.maxHp, 0)
  return { hp, maxHp }
}

export function getTargetableUnits(state: BulldromeState): TargetableState[] {
  return [state.player, ...state.party]
}

export function getAllEnemies(state: BulldromeState): EnemyState[] {
  return [state.boss, ...state.adds]
}

export function getLivingEnemies(state: BulldromeState): EnemyState[] {
  return getAllEnemies(state).filter((enemy) => enemy.hp > 0)
}

export function distance(a: Point, b: Point) {
  return Math.hypot(a.x - b.x, a.y - b.y)
}

function createPartyMember(
  id: string,
  name: string,
  role: Exclude<PartyRole, 'healer'>,
  maxHp: number,
  x: number,
  y: number,
  aiSlot: number,
): PartyMemberState {
  return {
    id,
    name,
    role,
    x,
    y,
    radius: role === 'tank' ? 17 : 14,
    hp: maxHp,
    maxHp,
	    shield: 0,
	    stunTimer: 0,
	    invulnerableTimer: 0,
	    healOverTimes: [],
	    renewTimer: 0,
    renewTickTimer: 0,
    burnTimer: 0,
    burnTickTimer: 0,
    bleedTimer: 0,
    bleedTickTimer: 0,
    aiSlot,
  }
}

function createBulldrome(): EnemyState {
  return createEnemy('bulldrome', 'Bulldrome', 'bulldrome', 1320, 34, DUNGEON_ARENA_WIDTH / 2, DUNGEON_ARENA_HEIGHT / 2 - 170, 0)
}

function createYianKutKu(): EnemyState {
  return createEnemy('yian-kut-ku', 'Yian Kut-Ku', 'yian-kut-ku', 1540, 32, DUNGEON_ARENA_WIDTH / 2, DUNGEON_ARENA_HEIGHT / 2 - 170, 0)
}

function createCyberDragon(): EnemyState {
  return createEnemy('cyber-dragon', 'Cyber Dragon', 'cyber-dragon', 1880, 36, DUNGEON_ARENA_WIDTH / 2, DUNGEON_ARENA_HEIGHT / 2 - 170, 0)
}

function createInitialEnemy(dungeonId: ActionDungeonId): EnemyState {
  if (dungeonId === 'cyber-dragon') return createCyberDragon()
  if (dungeonId === 'yian-kut-ku') return createYianKutKu()
  return createBulldrome()
}

function getDungeonStartMessage(dungeonId: ActionDungeonId) {
  if (dungeonId === 'cyber-dragon') return 'Cyber Dragon boots its blade array.'
  if (dungeonId === 'yian-kut-ku') return 'Yian Kut-Ku watches the roost.'
  return 'Bulldrome blocks the hunting grounds.'
}

function createEnemy(
  id: string,
  name: string,
  kind: EnemyKind,
  maxHp: number,
  radius: number,
  x: number,
  y: number,
  timerOffset: number,
): EnemyState {
  const trackSeconds = getTrackSeconds(kind)

  return {
    id,
    name,
    kind,
    x,
    y,
    radius,
    hp: maxHp,
    maxHp,
    phase: 'tracking',
    phaseTimer: trackSeconds + timerOffset,
    chargeVector: { x: 0, y: 1 },
    targetId: 'tank',
    chargeCount: 0,
    tankSwingTimer: getTankSwingSeconds(kind),
    flightStep: 'ground',
    flightY: y,
    orbitAngle: 0,
    mechanicTimers: {
      spinningBlade: getAttackFrequency(kind, 'spinningBlade', CYBER_BLADE_SPAWN_SECONDS),
    },
    arcTelegraph: {
      active: false,
      center: { x, y },
      radius: 0,
      startAngle: 0,
      endAngle: 0,
      width: 0,
    },
    telegraph: {
      active: false,
      start: { x, y },
      end: { x, y: 470 },
      width: kind === 'bulldrome' ? 86 : 48,
    },
    slamTelegraph: {
      active: false,
      radius: BOSS_SLAM_RADIUS,
    },
  }
}

function tickSpellCooldowns(cooldowns: Record<SpellSlot, number>, deltaSeconds: number) {
  tickSharedSpellCooldowns(cooldowns, deltaSeconds)
}

function tickPlayerGlobalCooldown(state: BulldromeState, deltaSeconds: number) {
  state.player.globalCooldown = Math.max(0, state.player.globalCooldown - deltaSeconds)
}

function triggerPlayerGlobalCooldown(state: BulldromeState) {
  state.player.globalCooldown = SPELL_GLOBAL_COOLDOWN_SECONDS
}

function tickCurrentCast(state: BulldromeState, deltaSeconds: number) {
  const cast = state.player.currentCast
  if (!cast) return

  cast.remaining = Math.max(0, cast.remaining - deltaSeconds)
  state.message = `Casting ${getDungeonSpell(state, cast.spell).name} on ${findTarget(state, cast.targetId)?.name ?? 'target'}.`
  if (cast.remaining > 0) return

  const completed = { ...cast }
  state.player.currentCast = null
  completePlayerSpell(state, completed.spell, completed.targetId)
}

function tickTargetable(state: BulldromeState, unit: TargetableState, deltaSeconds: number) {
  unit.stunTimer = Math.max(0, unit.stunTimer - deltaSeconds)
  unit.invulnerableTimer = Math.max(0, unit.invulnerableTimer - deltaSeconds)
  if (unit.burnTimer > 0) {
    unit.burnTimer = Math.max(0, unit.burnTimer - deltaSeconds)
    unit.burnTickTimer -= deltaSeconds
    if (unit.burnTickTimer <= 0) {
      applyDamage(unit, FIRE_DOT_DAMAGE)
      unit.burnTickTimer += FIRE_DOT_TICK_SECONDS
    }
  }
  if (unit.bleedTimer > 0) {
    unit.bleedTimer = Math.max(0, unit.bleedTimer - deltaSeconds)
    unit.bleedTickTimer -= deltaSeconds
    if (unit.bleedTickTimer <= 0) {
      applyDamage(unit, CYBER_BLEED_DAMAGE)
      unit.bleedTickTimer += CYBER_BLEED_TICK_SECONDS
    }
  }
  if (unit.healOverTimes?.length || unit.renewTimer > 0) {
    tickHealOverTimes(unit, deltaSeconds, (effect) => {
      healTarget(state, unit, effect.tickHeal)
      addHealEvent(state, unit, 0)
    })
  }
}

function movePlayer(state: BulldromeState, input: BossInput, deltaSeconds: number) {
  if (state.player.stunTimer > 0) {
    state.message = 'Stunned. Heal after the stars clear.'
    return
  }

  const length = Math.hypot(input.xAxis, input.yAxis)
  if (length <= 0.01) return

  state.player.facingX = input.xAxis / length
  state.player.facingY = input.yAxis / length
  state.player.x += state.player.facingX * ACTION_MOVEMENT_RULES.baseSpeed * deltaSeconds
  state.player.y += state.player.facingY * ACTION_MOVEMENT_RULES.baseSpeed * deltaSeconds
  clampToArena(state, state.player)
}

function updatePartyAi(state: BulldromeState, deltaSeconds: number) {
  for (const member of state.party) {
    if (member.hp <= 0 || member.stunTimer > 0) continue
    const hazardAvoidance = getDamagingEffectAvoidance(state, member)
    if (hazardAvoidance) {
      moveToward(state, member, hazardAvoidance.point, hazardAvoidance.moveSpeed * deltaSeconds)
      continue
    }
    const desired = getAiDesiredPoint(state, member)
    moveToward(state, member, desired, AI_MOVE_SPEED * deltaSeconds)
  }
}

function getAiDesiredPoint(state: BulldromeState, member: PartyMemberState): Point {
  const target = getAiTargetForMember(state, member)

  if (!target) return { x: member.x, y: member.y }

  if (member.role === 'ranged') {
    const enemyDistance = distance(member, target)
    if (enemyDistance > RANGED_ATTACK_RANGE - 18) {
      const dx = member.x - target.x
      const dy = member.y - target.y
      const length = Math.max(1, Math.hypot(dx, dy))
      return clampPointToArena(state, {
        x: target.x + (dx / length) * (RANGED_ATTACK_RANGE - 36),
        y: target.y + (dy / length) * (RANGED_ATTACK_RANGE - 36),
      }, member.radius)
    }

    const home = member.aiSlot === 3
      ? { x: 320, y: 376 }
      : { x: 640, y: 376 }
    return distance(home, target) <= RANGED_ATTACK_RANGE ? home : { x: member.x, y: member.y }
  }

  const bossDistance = distance(member, target)
  if (member.role === 'tank') {
    if (bossDistance >= 46 && bossDistance <= 86) return { x: member.x, y: member.y }
    return offsetFromEnemy(state, target, Math.PI / 2, target.radius + 28)
  }

  if (bossDistance >= 54 && bossDistance <= 98) return { x: member.x, y: member.y }
  const side = member.aiSlot === 1 ? 2.45 : 0.7
  return offsetFromEnemy(state, target, side, target.radius + 42)
}

function offsetFromEnemy(state: BulldromeState, enemy: FighterState, angle: number, range: number): Point {
  const point = {
    x: enemy.x + Math.cos(angle) * range,
    y: enemy.y + Math.sin(angle) * range,
  }
  return clampPointToArena(state, point, 14)
}

function moveToward(state: BulldromeState, unit: TargetableState, point: Point, maxDistance: number) {
  const dx = point.x - unit.x
  const dy = point.y - unit.y
  const length = Math.hypot(dx, dy)
  if (length <= 2) return
  const distanceThisFrame = Math.min(length, maxDistance)
  unit.x += (dx / length) * distanceThisFrame
  unit.y += (dy / length) * distanceThisFrame
  clampToArena(state, unit)
}

function updateEnemies(state: BulldromeState, deltaSeconds: number) {
  for (const enemy of getLivingEnemies(state)) {
    updateEnemy(state, enemy, deltaSeconds)
  }
}

function updateEnemy(state: BulldromeState, enemy: EnemyState, deltaSeconds: number) {
  enemy.phaseTimer -= deltaSeconds

  if (enemy.kind === 'yian-kut-ku') {
    updateYianKutKu(state, enemy, deltaSeconds)
    return
  }

  if (enemy.kind === 'bird') {
    updateBird(state, enemy, deltaSeconds)
    return
  }

  if (enemy.kind === 'cyber-dragon') {
    updateCyberDragon(state, enemy, deltaSeconds)
    return
  }

  if (enemy.phase === 'tracking') {
    enemy.targetId = getTankTargetId(state, enemy)
    const target = findTarget(state, enemy.targetId) ?? state.player
    driftTowardTarget(state, enemy, target, deltaSeconds)
    enemy.telegraph.active = false
    state.message = `${enemy.name} tracks ${target.name}.`
    if (enemy.phaseTimer <= 0) {
      if (getEnabledActionAttack(enemy.kind, 'charge')) {
        enemy.targetId = selectChargeTarget(state, enemy)
        startWindup(state, enemy)
      } else {
        startTankFocus(enemy)
      }
    }
    return
  }

  if (enemy.phase === 'mauling') {
    enemy.telegraph.active = false
    enemy.slamTelegraph.active = false
    enemy.targetId = 'tank'
    const tank = findTarget(state, 'tank') ?? state.player
    driftTowardTarget(state, enemy, tank, deltaSeconds)
    enemy.tankSwingTimer -= deltaSeconds
    state.message = `${enemy.name} attacks ${tank.name}.`
    if (enemy.tankSwingTimer <= 0) {
      applyDamage(tank, scaleDamageForDifficulty(state, getTankSwingDamage(enemy.kind)))
      tank.invulnerableTimer = 0.12
      state.lastHit = tank.id === 'player' ? 'player' : 'party'
      state.message = `${enemy.name} hit ${tank.name}.`
      enemy.tankSwingTimer += getTankSwingSeconds(enemy.kind)
    }
    if (enemy.phaseTimer <= 0) {
      const slam = getEnabledActionAttack(enemy.kind, 'groundSlam')
      const charge = getEnabledActionAttack(enemy.kind, 'charge')
      if (
        slam
        && enemy.chargeCount > 0
        && enemy.chargeCount % Math.max(1, Math.round(slam.everyNthCharge ?? 3)) === 0
      ) {
        startSlam(enemy)
      } else if (charge) {
        enemy.targetId = selectChargeTarget(state, enemy)
        startWindup(state, enemy)
      } else {
        enemy.phaseTimer = getTankFocusSeconds(enemy.kind)
      }
    }
    return
  }

  if (enemy.phase === 'windup') {
    enemy.telegraph.active = true
    const target = findTarget(state, enemy.targetId)
    state.message = `${enemy.name} charges ${target?.name ?? 'the party'}.`
    if (enemy.phaseTimer <= 0) {
      enemy.phase = 'charging'
      enemy.phaseTimer = getChargeSeconds(enemy.kind)
      enemy.telegraph.active = false
      state.message = `${enemy.name} charges.`
    }
    return
  }

  if (enemy.phase === 'charging') {
    enemy.telegraph.active = true
    enemy.x += enemy.chargeVector.x * getChargeSpeed(enemy.kind) * deltaSeconds
    enemy.y += enemy.chargeVector.y * getChargeSpeed(enemy.kind) * deltaSeconds
    clampToArena(state, enemy)
    if (enemy.phaseTimer <= 0 || hitArenaWall(state, enemy)) {
      enemy.phase = 'recovering'
      enemy.phaseTimer = getRecoverSeconds(enemy.kind)
      state.message = `${enemy.name} is recovering.`
    }
    return
  }

  if (enemy.phase === 'slamWindup') {
    enemy.telegraph.active = false
    enemy.slamTelegraph.active = true
    state.message = 'Ground slam. Get away from Bulldrome.'
    if (enemy.phaseTimer <= 0) {
      resolveGroundSlam(state, enemy)
      enemy.slamTelegraph.active = false
      enemy.targetId = selectChargeTarget(state, enemy)
      startWindup(state, enemy)
    }
    return
  }

  if (enemy.phase === 'recovering') {
    enemy.telegraph.active = false
    enemy.slamTelegraph.active = false
    state.message = `${enemy.name} is recovering.`
    if (enemy.phaseTimer <= 0) {
      startTankFocus(enemy)
    }
  }
}

function updateYianKutKu(state: BulldromeState, enemy: EnemyState, deltaSeconds: number) {
  const fireballAttack = getEnabledActionAttack(enemy.kind, 'fireballVolley')

  if (enemy.phase === 'tracking') {
    enemy.targetId = getTankTargetId(state, enemy)
    const tank = findTarget(state, enemy.targetId) ?? state.player
    driftTowardTarget(state, enemy, tank, deltaSeconds)
    state.message = 'Yian Kut-Ku watches the party.'
    if (enemy.phaseTimer <= 0) {
      if (!fireballAttack) {
        startTankFocus(enemy)
        return
      }
      state.fireballs = []
      enemy.phase = 'windup'
      enemy.phaseTimer = fireballAttack.windupSeconds ?? YIAN_FIREBALL_WINDUP_SECONDS
      state.message = 'Yian Kut-Ku inhales. Fireballs incoming.'
    }
    return
  }

  if (enemy.phase === 'windup') {
    state.message = 'Fireballs: northeast and northwest.'
    if (enemy.phaseTimer <= 0) {
      spawnYianFireballs(state, enemy)
      enemy.phase = 'recovering'
      enemy.phaseTimer = getRecoverSeconds(enemy.kind)
      state.message = 'Yian Kut-Ku spews bouncing fireballs.'
    }
    return
  }

  if (enemy.phase === 'mauling') {
    enemy.targetId = 'tank'
    const tank = findTarget(state, 'tank') ?? state.player
    driftTowardTarget(state, enemy, tank, deltaSeconds)
    enemy.tankSwingTimer -= deltaSeconds
    state.message = `Yian Kut-Ku pecks ${tank.name}.`
    if (enemy.tankSwingTimer <= 0) {
      applyDamage(tank, scaleDamageForDifficulty(state, getTankSwingDamage(enemy.kind)))
      tank.invulnerableTimer = 0.12
      state.lastHit = tank.id === 'player' ? 'player' : 'party'
      enemy.tankSwingTimer += getTankSwingSeconds(enemy.kind)
    }
    if (enemy.phaseTimer <= 0) {
      enemy.phase = 'tracking'
      enemy.phaseTimer = getTrackSeconds(enemy.kind)
    }
    return
  }

  if (enemy.phase === 'recovering') {
    state.message = 'Yian Kut-Ku recovers from the fireball spray.'
    if (enemy.phaseTimer <= 0) {
      enemy.phase = 'mauling'
      enemy.phaseTimer = getTankFocusSeconds(enemy.kind)
      enemy.tankSwingTimer = 0.35
    }
  }
}

function spawnYianFireballs(state: BulldromeState, enemy: EnemyState) {
  const fireballAttack = getEnabledActionAttack(enemy.kind, 'fireballVolley')
  if (!fireballAttack) return

  const directions = [
    { x: 0.72, y: -0.7 },
    { x: -0.72, y: -0.7 },
  ]
  for (const direction of directions) {
    state.fireballs.push({
      id: `fireball-${state.nextHazardId}`,
      x: enemy.x,
      y: enemy.y,
      radius: 13,
      vx: direction.x * (fireballAttack.speed ?? FIREBALL_SPEED),
      vy: direction.y * (fireballAttack.speed ?? FIREBALL_SPEED),
      bouncesRemaining: 9999,
    })
    state.nextHazardId += 1
  }
}

function updateBird(state: BulldromeState, enemy: EnemyState, deltaSeconds: number) {
  enemy.telegraph.active = false
  enemy.slamTelegraph.active = false
  const diveAttack = getEnabledActionAttack(enemy.kind, 'birdDive')

  if (enemy.phase === 'mauling' || enemy.phase === 'tracking') {
    enemy.flightStep = 'ground'
    enemy.targetId = 'tank'
    const tank = findTarget(state, 'tank') ?? state.player
    driftTowardTarget(state, enemy, tank, deltaSeconds)
    enemy.tankSwingTimer -= deltaSeconds
    state.message = `${enemy.name} claws ${tank.name}.`
    if (enemy.tankSwingTimer <= 0) {
      applyDamage(tank, scaleDamageForDifficulty(state, getTankSwingDamage(enemy.kind)))
      tank.invulnerableTimer = 0.12
      state.lastHit = tank.id === 'player' ? 'player' : 'party'
      enemy.tankSwingTimer += getTankSwingSeconds(enemy.kind)
    }
    if (enemy.phaseTimer <= 0) {
      if (diveAttack) startBirdFlight(state, enemy)
      else enemy.phaseTimer = getTankFocusSeconds(enemy.kind)
    }
    return
  }

  if (enemy.phase === 'windup') {
    enemy.flightStep = 'toLeft'
    moveEnemyTowardPoint(state, enemy, {
      x: state.arena.padding + enemy.radius,
      y: enemy.flightY,
    }, getChargeSpeed(enemy.kind) * deltaSeconds)
    state.message = `${enemy.name} lifts off.`
    if (enemy.x <= state.arena.padding + enemy.radius + 3) {
      enemy.phase = 'charging'
      enemy.flightStep = 'across'
      enemy.chargeVector = { x: 1, y: 0 }
      enemy.phaseTimer = 12
      enemy.telegraph = {
        active: true,
        start: { x: state.arena.padding, y: enemy.y },
        end: { x: state.arena.width - state.arena.padding, y: enemy.y },
        width: 42,
      }
    }
    return
  }

  if (enemy.phase === 'charging') {
    enemy.flightStep = 'across'
    enemy.telegraph.active = true
    enemy.x += getChargeSpeed(enemy.kind) * deltaSeconds
    enemy.y = enemy.flightY
    clampToArena(state, enemy)
    state.message = `${enemy.name} dives across the arena.`
    if (enemy.x >= state.arena.width - state.arena.padding - enemy.radius - 3) {
      enemy.phase = 'recovering'
      enemy.flightStep = 'toTank'
      enemy.telegraph.active = false
    }
    return
  }

  if (enemy.phase === 'recovering') {
    enemy.flightStep = 'toTank'
    const tank = findTarget(state, 'tank') ?? state.player
    moveEnemyTowardPoint(state, enemy, tank, getChargeSpeed(enemy.kind) * deltaSeconds)
    state.message = `${enemy.name} returns to ${tank.name}.`
    if (distance(enemy, tank) <= enemy.radius + tank.radius + 18) {
      enemy.phase = 'mauling'
      enemy.flightStep = 'ground'
      enemy.phaseTimer = getTankFocusSeconds(enemy.kind)
      enemy.tankSwingTimer = 0.35
    }
  }
}

function updateCyberDragon(state: BulldromeState, enemy: EnemyState, deltaSeconds: number) {
  updateSpinningBladeSpawner(state, enemy, deltaSeconds)

  const circularCharge = getEnabledActionAttack(enemy.kind, 'circularCharge')
  enemy.slamTelegraph.active = false

  if (enemy.phase === 'tracking' || enemy.phase === 'mauling') {
    enemy.telegraph.active = false
    enemy.arcTelegraph.active = false
    enemy.targetId = getTankTargetId(state, enemy)
    const tank = findTarget(state, enemy.targetId) ?? state.player
    driftTowardTarget(state, enemy, tank, deltaSeconds)
    enemy.tankSwingTimer -= deltaSeconds
    state.message = `Cyber Dragon claws ${tank.name}.`
    if (enemy.tankSwingTimer <= 0) {
      applyDamage(tank, scaleDamageForDifficulty(state, getTankSwingDamage(enemy.kind)))
      tank.invulnerableTimer = 0.12
      state.lastHit = tank.id === 'player' ? 'player' : 'party'
      enemy.tankSwingTimer += getTankSwingSeconds(enemy.kind)
    }
    if (enemy.phaseTimer <= 0) {
      if (circularCharge) startCircularCharge(state, enemy)
      else enemy.phaseTimer = getTankFocusSeconds(enemy.kind)
    }
    return
  }

  if (enemy.phase === 'circling') {
    enemy.telegraph.active = false
    enemy.orbitAngle += Math.PI * 1.7 * deltaSeconds
    const center = getArenaCenter(state)
    const radius = circularCharge?.radius ?? CYBER_CIRCLE_RADIUS
    syncCircularChargeTelegraph(enemy, center, radius)
    enemy.x = center.x + Math.cos(enemy.orbitAngle) * radius
    enemy.y = center.y + Math.sin(enemy.orbitAngle) * radius
    clampToArena(state, enemy)
    state.message = 'Cyber Dragon circles the room.'
    if (enemy.phaseTimer <= 0) startCyberChargeWindup(state, enemy)
    return
  }

  if (enemy.phase === 'windup') {
    enemy.arcTelegraph.active = false
    enemy.telegraph.active = true
    state.message = 'Cyber Dragon locks a charge lane.'
    if (enemy.phaseTimer <= 0) {
      enemy.phase = 'charging'
      enemy.phaseTimer = CYBER_CHARGE_SECONDS
      state.message = 'Cyber Dragon charges.'
    }
    return
  }

  if (enemy.phase === 'charging') {
    enemy.arcTelegraph.active = false
    enemy.telegraph.active = true
    enemy.x += enemy.chargeVector.x * getChargeSpeed(enemy.kind) * deltaSeconds
    enemy.y += enemy.chargeVector.y * getChargeSpeed(enemy.kind) * deltaSeconds
    clampToArena(state, enemy)
    if (enemy.phaseTimer <= 0 || hitArenaWall(state, enemy)) {
      enemy.phase = 'recovering'
      enemy.phaseTimer = getRecoverSeconds(enemy.kind)
      enemy.telegraph.active = false
      state.message = 'Cyber Dragon vents heat.'
    }
    return
  }

  if (enemy.phase === 'recovering') {
    enemy.telegraph.active = false
    enemy.arcTelegraph.active = false
    state.message = 'Cyber Dragon vents heat.'
    if (enemy.phaseTimer <= 0) {
      enemy.phase = 'mauling'
      enemy.phaseTimer = getTankFocusSeconds(enemy.kind)
      enemy.tankSwingTimer = 0.35
    }
  }
}

function updateSpinningBladeSpawner(state: BulldromeState, enemy: EnemyState, deltaSeconds: number) {
  const attack = getEnabledActionAttack(enemy.kind, 'spinningBlade')
  if (!attack || enemy.hp <= 0) return

  const timer = (enemy.mechanicTimers.spinningBlade ?? attack.frequencySeconds) - deltaSeconds
  if (timer > 0) {
    enemy.mechanicTimers.spinningBlade = timer
    return
  }

  spawnSpinningBlade(state, enemy, attack)
  enemy.mechanicTimers.spinningBlade = attack.frequencySeconds
}

function spawnSpinningBlade(state: BulldromeState, enemy: EnemyState, attack: NonNullable<ReturnType<typeof getEnabledActionAttack>>) {
  const target = chooseRandomLivingTarget(state)
  if (!target) return

  state.spinningBlades.push({
    id: `spinning-blade-${state.nextHazardId}`,
    sourceKind: enemy.kind,
    x: enemy.x,
    y: enemy.y,
    radius: attack.radius ?? CYBER_BLADE_RADIUS,
    speed: attack.speed ?? CYBER_BLADE_SPEED,
    remaining: attack.recoverSeconds ?? attack.durationSeconds ?? CYBER_BLADE_SECONDS,
    targetId: target.id,
    rotation: 0,
  })
  state.nextHazardId += 1
  state.message = `Spinning blade targets ${target.name}.`
}

function updateSpinningBlades(state: BulldromeState, deltaSeconds: number) {
  const nextBlades: SpinningBladeState[] = []
  for (const blade of state.spinningBlades) {
    const attack = getEnabledActionAttack(blade.sourceKind, 'spinningBlade')
    if (!attack) continue

    const next = {
      ...blade,
      remaining: blade.remaining - deltaSeconds,
      rotation: blade.rotation + deltaSeconds * 9,
    }
    if (next.remaining <= 0) continue

    let target = findTarget(state, next.targetId)
    if (!target || target.hp <= 0) {
      target = chooseRandomLivingTarget(state)
      if (target) next.targetId = target.id
    }

    if (target) {
      const dx = target.x - next.x
      const dy = target.y - next.y
      const length = Math.hypot(dx, dy)
      if (length > 1) {
        const move = Math.min(length, next.speed * deltaSeconds)
        next.x += (dx / length) * move
        next.y += (dy / length) * move
      }
    }

    for (const unit of getTargetableUnits(state)) {
      if (unit.hp <= 0 || distance(unit, next) > unit.radius + next.radius) continue
      applyDamage(unit, scaleDamageForDifficulty(state, attack.damage) * deltaSeconds)
      applyBleed(unit)
      state.lastHit = unit.id === 'player' ? 'player' : 'party'
      state.message = `${unit.name} is clipped by a spinning blade.`
    }

    nextBlades.push(next)
  }
  state.spinningBlades = nextBlades
}

function applyBleed(unit: TargetableState) {
  unit.bleedTimer = CYBER_BLEED_SECONDS
  unit.bleedTickTimer = Math.max(0.15, Math.min(unit.bleedTickTimer || CYBER_BLEED_TICK_SECONDS, CYBER_BLEED_TICK_SECONDS))
}

function chooseRandomLivingTarget(state: BulldromeState) {
  const living = getTargetableUnits(state).filter((unit) => unit.hp > 0)
  if (living.length <= 0) return undefined
  return living[Math.floor(Math.random() * living.length)]
}

function startCircularCharge(state: BulldromeState, enemy: EnemyState) {
  const attack = getEnabledActionAttack(enemy.kind, 'circularCharge')
  const center = getArenaCenter(state)
  enemy.phase = 'circling'
  enemy.phaseTimer = attack?.windupSeconds ?? CYBER_CIRCLE_SECONDS
  enemy.orbitAngle = Math.atan2(enemy.y - center.y, enemy.x - center.x)
  enemy.telegraph.active = false
  syncCircularChargeTelegraph(enemy, center, attack?.radius ?? CYBER_CIRCLE_RADIUS)
  state.message = 'Cyber Dragon starts a circular charge.'
}

function syncCircularChargeTelegraph(enemy: EnemyState, center: Point, radius: number) {
  enemy.arcTelegraph = {
    active: true,
    center,
    radius,
    startAngle: enemy.orbitAngle,
    endAngle: enemy.orbitAngle + Math.PI,
    width: 72,
  }
}

function startCyberChargeWindup(state: BulldromeState, enemy: EnemyState) {
  const target = selectNearestTarget(state, enemy) ?? state.player
  const dx = target.x - enemy.x
  const dy = target.y - enemy.y
  const length = Math.max(1, Math.hypot(dx, dy))
  const chargeVector = { x: dx / length, y: dy / length }
  const end = projectToArenaWall(state, enemy, chargeVector)

  enemy.phase = 'windup'
  enemy.phaseTimer = 0.42
  enemy.targetId = target.id
  enemy.chargeVector = chargeVector
  enemy.telegraph = {
    active: true,
    start: { x: enemy.x, y: enemy.y },
    end,
    width: 72,
  }
  enemy.chargeCount += 1
}

function selectNearestTarget(state: BulldromeState, enemy: EnemyState) {
  return getTargetableUnits(state)
    .filter((unit) => unit.hp > 0)
    .sort((a, b) => distance(a, enemy) - distance(b, enemy))[0] ?? null
}

function getArenaCenter(state: BulldromeState): Point {
  return { x: state.arena.width / 2, y: state.arena.height / 2 }
}

function startBirdFlight(state: BulldromeState, enemy: EnemyState) {
  enemy.phase = 'windup'
  enemy.flightStep = 'toLeft'
  enemy.telegraph.active = true
  enemy.telegraph = {
    active: true,
    start: { x: state.arena.padding, y: enemy.flightY },
    end: { x: state.arena.width - state.arena.padding, y: enemy.flightY },
    width: 42,
  }
  state.message = `${enemy.name} starts a dive path.`
}

function startTankFocus(enemy: EnemyState) {
  enemy.phase = 'mauling'
  enemy.phaseTimer = getTankFocusSeconds(enemy.kind)
  enemy.targetId = 'tank'
  enemy.tankSwingTimer = 0.35
  enemy.telegraph.active = false
  enemy.slamTelegraph.active = false
}

function startSlam(enemy: EnemyState) {
  const slam = getEnabledActionAttack(enemy.kind, 'groundSlam')
  enemy.phase = 'slamWindup'
  enemy.phaseTimer = slam?.windupSeconds ?? BOSS_SLAM_CAST_SECONDS
  enemy.slamTelegraph.radius = slam?.radius ?? BOSS_SLAM_RADIUS
  enemy.slamTelegraph.active = true
  enemy.telegraph.active = false
}

function resolveGroundSlam(state: BulldromeState, enemy: EnemyState) {
  const slam = getEnabledActionAttack(enemy.kind, 'groundSlam')
  if (!slam) return

  let hitCount = 0
  for (const unit of getTargetableUnits(state)) {
    if (unit.hp <= 0 || distance(unit, enemy) > (slam.radius ?? BOSS_SLAM_RADIUS)) continue
    applyDamage(unit, scaleDamageForDifficulty(state, slam.damage))
    unit.stunTimer = 0.35
    unit.invulnerableTimer = ACTION_CONTROL_RULES.invulnerableSeconds
    hitCount += 1
  }
  state.lastHit = hitCount > 0 ? 'party' : null
  state.message = hitCount > 0 ? `Ground slam hit ${hitCount}.` : 'Ground slam missed.'
}

function resolvePartyDamage(state: BulldromeState, deltaSeconds: number) {
  if (state.result !== 'playing') return

  for (const member of state.party) {
    if (member.hp <= 0 || member.stunTimer > 0) continue
    const target = getAiTargetForMember(state, member)
    if (!target) continue

    const enemyDistance = distance(member, target)
    let damage = 0
    if (member.role === 'tank' && enemyDistance <= target.radius + 58) damage = TANK_DPS * deltaSeconds
    if (member.role === 'melee' && enemyDistance <= target.radius + 72) damage = MELEE_DPS * deltaSeconds
    if (member.role === 'ranged' && enemyDistance <= RANGED_ATTACK_RANGE) damage = RANGED_DPS * deltaSeconds
    if (damage <= 0) continue

    damageEnemy(state, target, damage, member.id)
    if (target.hp <= 0) {
      target.phase = 'defeated'
      target.telegraph.active = false
      target.slamTelegraph.active = false
      state.message = `${target.name} defeated.`
    }
  }
}

function updateFireHazards(state: BulldromeState, deltaSeconds: number) {
  state.fireSpots = state.fireSpots
    .map((spot) => ({ ...spot, remaining: spot.remaining - deltaSeconds }))
    .filter((spot) => spot.remaining > 0)

  const nextFireballs: FireballState[] = []
  for (const fireball of state.fireballs) {
    const next = { ...fireball }
    next.x += next.vx * deltaSeconds
    next.y += next.vy * deltaSeconds

    let bounced = false
    const minX = state.arena.padding + next.radius
    const maxX = state.arena.width - state.arena.padding - next.radius
    const minY = state.arena.padding + next.radius
    const maxY = state.arena.height - state.arena.padding - next.radius

    if (next.x <= minX || next.x >= maxX) {
      next.x = Math.max(minX, Math.min(maxX, next.x))
      next.vx *= -1
      bounced = true
    }
    if (next.y <= minY || next.y >= maxY) {
      next.y = Math.max(minY, Math.min(maxY, next.y))
      next.vy *= -1
      bounced = true
    }

    if (bounced) {
      addFireSpot(state, next.x, next.y)
    }

    for (const unit of getTargetableUnits(state)) {
      if (unit.hp <= 0 || unit.invulnerableTimer > 0 || distance(unit, next) > unit.radius + next.radius) continue
      applyDamage(unit, scaleDamageForDifficulty(state, getAttackDamage('yian-kut-ku', 'fireballVolley', FIREBALL_DAMAGE)))
      igniteUnit(unit)
      addFireSpot(state, next.x, next.y)
      bounceFireballOffUnit(next, unit)
      unit.invulnerableTimer = ACTION_CONTROL_RULES.invulnerableSeconds
      state.lastHit = unit.id === 'player' ? 'player' : 'party'
      state.message = `${unit.name} was hit by a fireball.`
    }
    nextFireballs.push(next)
  }
  state.fireballs = nextFireballs

  for (const spot of state.fireSpots) {
    for (const unit of getTargetableUnits(state)) {
      if (unit.hp <= 0 || unit.burnTimer > 0 || distance(unit, spot) > unit.radius + spot.radius) continue
      igniteUnit(unit)
      state.lastHit = unit.id === 'player' ? 'player' : 'party'
      state.message = `${unit.name} is standing in fire.`
    }
  }
}

function bounceFireballOffUnit(fireball: FireballState, unit: TargetableState) {
  const dx = fireball.x - unit.x
  const dy = fireball.y - unit.y
  const length = Math.max(1, Math.hypot(dx, dy))
  const nx = dx / length
  const ny = dy / length
  const dot = fireball.vx * nx + fireball.vy * ny
  fireball.vx -= 2 * dot * nx
  fireball.vy -= 2 * dot * ny
  fireball.x = unit.x + nx * (unit.radius + fireball.radius + 2)
  fireball.y = unit.y + ny * (unit.radius + fireball.radius + 2)
}

function addFireSpot(state: BulldromeState, x: number, y: number) {
  state.fireSpots.push({
    id: `firespot-${state.nextHazardId}`,
    x,
    y,
    radius: 34,
    remaining: FIRE_SPOT_SECONDS,
  })
  state.nextHazardId += 1
  if (state.fireSpots.length > MAX_FIRE_SPOTS) {
    state.fireSpots = state.fireSpots.slice(-MAX_FIRE_SPOTS)
  }
}

function igniteUnit(unit: TargetableState) {
  unit.burnTimer = FIRE_DOT_SECONDS
  unit.burnTickTimer = FIRE_DOT_TICK_SECONDS
}

function getAiTargetForMember(state: BulldromeState, member: PartyMemberState): EnemyState | null {
  const enemies = getLivingEnemies(state)
  if (enemies.length === 0) return null

  if (member.role === 'tank') return getPrimaryTankEnemy(state)

  void member
  return state.boss.hp > 0 ? state.boss : enemies[0]
}

function getPrimaryTankEnemy(state: BulldromeState): EnemyState | null {
  const enemies = getLivingEnemies(state)
  return state.boss.hp > 0 ? state.boss : enemies[0] ?? null
}

function getTankTargetId(state: BulldromeState, enemy: EnemyState) {
  const tank = findTarget(state, 'tank')
  return tank && tank.hp > 0 ? tank.id : selectChargeTarget(state, enemy)
}

function driftTowardTarget(
  state: BulldromeState,
  enemy: EnemyState,
  target: TargetableState,
  deltaSeconds: number,
) {
  const dx = target.x - enemy.x
  const dy = target.y - enemy.y
  const length = Math.hypot(dx, dy)
  if (length <= 1) return

  enemy.chargeVector = { x: dx / length, y: dy / length }
  const driftSpeed = (enemy.kind === 'bulldrome' ? 82 : enemy.kind === 'cyber-dragon' ? 74 : enemy.kind === 'yian-kut-ku' ? 72 : 62) * deltaSeconds
  enemy.x += (dx / length) * driftSpeed
  enemy.y += (dy / length) * driftSpeed
  clampToArena(state, enemy)
}

function moveEnemyTowardPoint(state: BulldromeState, enemy: EnemyState, point: Point, maxDistance: number) {
  const dx = point.x - enemy.x
  const dy = point.y - enemy.y
  const length = Math.hypot(dx, dy)
  if (length <= 2) return
  const distanceThisFrame = Math.min(length, maxDistance)
  enemy.x += (dx / length) * distanceThisFrame
  enemy.y += (dy / length) * distanceThisFrame
  clampToArena(state, enemy)
}

function startWindup(state: BulldromeState, enemy: EnemyState) {
  const target = findTarget(state, enemy.targetId) ?? state.player
  const dx = target.x - enemy.x
  const dy = target.y - enemy.y
  const length = Math.max(1, Math.hypot(dx, dy))
  const chargeVector = { x: dx / length, y: dy / length }
  const end = projectToArenaWall(state, enemy, chargeVector)

  enemy.phase = 'windup'
  enemy.phaseTimer = getWindupSeconds(enemy.kind)
  enemy.chargeVector = chargeVector
  enemy.telegraph = {
    active: true,
    start: { x: enemy.x, y: enemy.y },
    end,
    width: enemy.kind === 'bulldrome' ? 86 : 48,
  }
  enemy.chargeCount += 1
}

function selectChargeTarget(state: BulldromeState, enemy: EnemyState) {
  const living = getTargetableUnits(state).filter((unit) => unit.hp > 0)
  const preferred = enemy.kind === 'bulldrome' || enemy.kind === 'cyber-dragon'
    ? ['player', 'tank', 'melee-1', 'ranged-1', 'ranged-2']
    : ['tank', 'melee-1', 'player', 'ranged-1', 'ranged-2']
  const offset = enemy.chargeCount % preferred.length
  const ordered = preferred.slice(offset).concat(preferred.slice(0, offset))
  return ordered.find((id) => living.some((unit) => unit.id === id)) ?? 'player'
}

function resolveContact(state: BulldromeState) {
  for (const enemy of getLivingEnemies(state)) {
    for (const unit of getTargetableUnits(state)) {
      const overlap = unit.hp > 0 && distance(unit, enemy) < unit.radius + enemy.radius
      if (!overlap || unit.invulnerableTimer > 0) continue

      const isCharge = enemy.phase === 'charging' || enemy.phase === 'circling'
      applyDamage(unit, scaleDamageForDifficulty(state, isCharge ? getChargeDamage(enemy.kind) : getBodyDamage(enemy.kind)))
      unit.stunTimer = isCharge && enemy.kind === 'cyber-dragon' ? CYBER_CHARGE_STUN_SECONDS : isCharge ? ACTION_CONTROL_RULES.stunSeconds : 0.25
      unit.invulnerableTimer = ACTION_CONTROL_RULES.invulnerableSeconds
      state.lastHit = unit.id === 'player' ? 'player' : 'party'
      state.message = isCharge
        ? `${unit.name} ate ${enemy.name}'s charge.`
        : `${unit.name} was trampled.`

      const dx = unit.x - enemy.x
      const dy = unit.y - enemy.y
      const length = Math.max(1, Math.hypot(dx, dy))
      unit.x += (dx / length) * 34
      unit.y += (dy / length) * 34
      clampToArena(state, unit)
    }
  }
}

function getDungeonSpell(state: BulldromeState, spell: SpellSlot): SpellDefinition {
  return getActionSpellDefinition(state.player.classId, spell, state.player.talentModifiers)
}

function startPlayerSpell(state: BulldromeState, spell: SpellSlot | null) {
  if (!spell || state.player.currentCast || state.player.globalCooldown > 0 || state.player.spellCooldowns[spell] > 0 || state.player.stunTimer > 0) return
  const target = findCombatTarget(state, state.targetId)
  if (!target || target.hp <= 0) {
    state.message = 'No living target selected.'
    return
  }

  const damageSpell = isDungeonDamageSpell(state, spell)
  const enemyTarget = isEnemyTarget(state, target)
  if (damageSpell && !enemyTarget) {
    showNotice(state, state.player.id, 'Enemy target')
    state.message = `${getDungeonSpell(state, spell).name} requires an enemy target.`
    return
  }
  if (!damageSpell && enemyTarget) {
    showNotice(state, state.player.id, 'Ally target')
    state.message = `${getDungeonSpell(state, spell).name} requires a friendly target.`
    return
  }
  const reach = getDungeonSpellReach(state, target, spell, { includeTargetRadius: damageSpell })
  if (!reach.ok) {
    setDungeonSpellReachFailure(state, reach)
    return
  }
  const spend = spendActionSpellMana(state.player, state.player.classId, spell, { talentMods: state.player.talentModifiers })
  if (!spend.ok) {
    showNotice(state, state.player.id, 'Mana')
    state.message = `Need ${spend.cost} mana.`
    return
  }

  const definition = getDungeonSpell(state, spell)
  triggerPlayerGlobalCooldown(state)
  if (definition.castTime > 0) {
    state.player.currentCast = {
      spell,
      targetId: target.id,
      remaining: definition.castTime,
      total: definition.castTime,
    }
    state.message = `Casting ${definition.name} on ${target.name}.`
    return
  }

  completePlayerSpell(state, spell, target.id)
}

function completePlayerSpell(state: BulldromeState, spell: SpellSlot, targetId: string) {
  const target = findCombatTarget(state, targetId)
  if (!target || target.hp <= 0) {
    state.message = 'Spell fizzled. Target is down.'
    return
  }

  const damageSpell = isDungeonDamageSpell(state, spell)
  const enemyTarget = isEnemyTarget(state, target)
  if (damageSpell && !enemyTarget) {
    state.message = `${getDungeonSpell(state, spell).name} requires an enemy target.`
    return
  }
  if (!damageSpell && enemyTarget) {
    state.message = `${getDungeonSpell(state, spell).name} requires a friendly target.`
    return
  }
  const reach = getDungeonSpellReach(state, target, spell, { includeTargetRadius: damageSpell })
  if (!reach.ok) {
    setDungeonSpellReachFailure(state, reach)
    return
  }

  state.player.spellCooldowns[spell] = getDungeonSpell(state, spell).cooldown
  recordSpellCast(state.player)

  if (damageSpell) {
    completePlayerDamageSpell(state, spell, target as EnemyState)
    return
  }

  const friendlyTarget = target as TargetableState
  const ability = getArenaAbility(getDungeonSpell(state, spell).abilityId)
  if (ability.shield || ability.kind === 'buff') {
    const shield = getActionTalentAdjustedAmount(state.player.classId, spell, ability.shield ?? (ability.id === 'blessing_of_might' ? 80 : 90), 'shield', state.player.talentModifiers)
    addShield(friendlyTarget, shield, { cap: Math.max(shield, friendlyTarget.maxHp) })
    addHealEvent(state, friendlyTarget, 0)
    state.lastHit = 'heal'
    state.message = `${ability.name} protected ${friendlyTarget.name}.`
    return
  }
  const healOverTimeId = getHealOverTimeEffectId(ability.id)
  if (healOverTimeId) {
    applyHealOverTimeTimers(friendlyTarget, healOverTimeId)
    healTarget(state, friendlyTarget, getActionTalentAdjustedAmount(state.player.classId, spell, ability.heal ?? 28, 'heal', state.player.talentModifiers))
    state.lastHit = 'heal'
    state.message = `${ability.name} on ${friendlyTarget.name}.`
    return
  }

  healTarget(state, friendlyTarget, getActionTalentAdjustedAmount(state.player.classId, spell, ability.heal ?? 120, 'heal', state.player.talentModifiers))
  addHealEvent(state, friendlyTarget, 0)
  state.lastHit = 'heal'
  state.message = `${ability.name} healed ${friendlyTarget.name}.`
}

function completePlayerDamageSpell(state: BulldromeState, spell: SpellSlot, target: EnemyState) {
  const damage = getDungeonDamageSpellAmount(state, spell)
  damageEnemy(state, target, damage, state.player.id)
  state.lastHit = 'boss'
  const spellDef = getDungeonSpell(state, spell)
  state.message = `${spellDef.name} hit ${target.name}.`
  if (spellDef.abilityId !== 'smite') return

  const ally = getTargetableUnits(state)
    .filter((unit) => unit.hp > 0 && unit.hp < unit.maxHp && isInHealRange(state, unit, spell))
    .sort((a, b) => (a.hp / a.maxHp) - (b.hp / b.maxHp))[0]
  if (!ally) return

  healTarget(state, ally, damage * 0.5)
  addHealEvent(state, ally, 0)
  state.lastHit = 'heal'
  state.message = `Smite hit ${target.name} and healed ${ally.name}.`
}

function getDungeonDamageSpellAmount(state: BulldromeState, spell: SpellSlot) {
  const ability = getArenaAbility(getDungeonSpell(state, spell).abilityId)
  const dotPulse = ability.dot ? ability.dot.damage / Math.max(1, ability.dot.seconds / 3) : 0
  return getActionTalentAdjustedAmount(state.player.classId, spell, (ability.damage ?? 0) + dotPulse, 'damage', state.player.talentModifiers)
}

function isDungeonDamageSpell(state: BulldromeState, spell: SpellSlot) {
  return getActionSpellTarget(state.player.classId, spell) === 'Enemy target'
}

function healTarget(state: BulldromeState, target: TargetableState, amount: number) {
  const before = target.hp
  target.hp = Math.min(target.maxHp, target.hp + amount)
  const healed = target.hp - before
  if (healed <= 0) return

  addHealEvent(state, target, healed)
}

function addHealEvent(state: BulldromeState, target: TargetableState, amount: number) {
  if (state.healEvents.some((event) => event.targetId === target.id)) return

  state.healEvents.push({
    id: `${state.elapsed.toFixed(3)}-${target.id}-${state.healEvents.length}`,
    targetId: target.id,
    amount,
  })
}

function isInHealRange(state: BulldromeState, target: TargetableState, spell: SpellSlot) {
  return getDungeonSpellReach(state, target, spell).ok
}

function getDungeonSpellReach(
  state: BulldromeState,
  target: CombatSpellTarget,
  spell: SpellSlot,
  options?: { includeTargetRadius?: boolean },
) {
  return getActionSpellReach({
    caster: state.player,
    target,
    classId: state.player.classId,
    spell,
    talentMods: state.player.talentModifiers,
    includeTargetRadius: options?.includeTargetRadius,
    hasLineOfSight: () => hasDungeonLineOfSight(),
  })
}

function hasDungeonLineOfSight() {
  return true
}

function setDungeonSpellReachFailure(state: BulldromeState, reach: ActionSpellReachResult) {
  if (reach.ok) return
  const blocked = reach.reason === 'lineOfSight'
  showNotice(state, state.player.id, blocked ? 'Blocked' : 'Range')
  state.message = blocked ? 'Line of sight blocked.' : 'Not in range.'
}

function showNotice(state: BulldromeState, targetId: string, text: string) {
  state.noticeEvents.push({
    id: `${state.elapsed.toFixed(3)}-${targetId}-${state.noticeEvents.length}-${text}`,
    targetId,
    text,
  })
}

function createDamageDoneTracker(): Record<string, number> {
  return {
    player: 0,
    tank: 0,
    'melee-1': 0,
    'ranged-1': 0,
    'ranged-2': 0,
  }
}

function damageEnemy(state: BulldromeState, target: EnemyState, amount: number, sourceId: string) {
  const before = target.hp
  target.hp = Math.max(0, target.hp - amount)
  const dealt = Math.max(0, before - target.hp)
  if (dealt > 0) state.damageDoneByUnit[sourceId] = (state.damageDoneByUnit[sourceId] ?? 0) + dealt
}

function applyDamage(unit: TargetableState, amount: number) {
  applyShieldedDamage(unit, amount)
}

function getDamagingEffectAvoidance(state: BulldromeState, member: PartyMemberState): { point: Point, moveSpeed: number } | null {
  let escapeX = 0
  let escapeY = 0
  let moveSpeed = AI_MOVE_SPEED
  let avoidDistance = 1

  const fireAreaAvoidance = getActionAiAvoidance('yian-kut-ku', 'fireballVolley', 'persistentArea')
  for (const spot of state.fireSpots) {
    if (!fireAreaAvoidance) continue
    const avoidRadius = member.radius + spot.radius + fireAreaAvoidance.buffer
    addRepulsionFromPoint(member, spot, fireAreaAvoidance, (avoidRadius - distance(member, spot)) / avoidRadius)
  }

  const fireballAvoidance = getActionAiAvoidance('yian-kut-ku', 'fireballVolley', 'projectile')
  for (const fireball of state.fireballs) {
    if (!fireballAvoidance) continue
    const avoidRadius = member.radius + fireball.radius + fireballAvoidance.buffer
    addRepulsionFromPoint(member, fireball, fireballAvoidance, (avoidRadius - distance(member, fireball)) / avoidRadius)
  }

  for (const blade of state.spinningBlades) {
    const bladeAvoidance = getActionAiAvoidance(blade.sourceKind, 'spinningBlade', 'persistentArea')
    if (!bladeAvoidance) continue
    const avoidRadius = member.radius + blade.radius + bladeAvoidance.buffer
    addRepulsionFromPoint(member, blade, bladeAvoidance, (avoidRadius - distance(member, blade)) / avoidRadius)
  }

  for (const enemy of getLivingEnemies(state)) {
    const slamAvoidance = getActionAiAvoidance(enemy.kind, 'groundSlam', 'circleTelegraph')
    if (enemy.slamTelegraph.active) {
      if (slamAvoidance) {
        const avoidRadius = enemy.slamTelegraph.radius + member.radius + slamAvoidance.buffer
        addRepulsionFromPoint(member, enemy, slamAvoidance, (avoidRadius - distance(member, enemy)) / avoidRadius)
      }
    }

    const lineAttackKind: ActionAttackKind = enemy.kind === 'bird'
      ? 'birdDive'
      : enemy.kind === 'cyber-dragon'
        ? 'circularCharge'
        : 'charge'
    const lineAvoidance = getActionAiAvoidance(enemy.kind, lineAttackKind, 'lineTelegraph')
    if (
      lineAvoidance
      && enemy.telegraph.active
      && enemy.phase === 'windup'
      && isPointNearSegment(
        member,
        enemy.telegraph.start,
        enemy.telegraph.end,
        enemy.telegraph.width * (lineAvoidance.lineWidthMultiplier ?? 0.42) + lineAvoidance.buffer,
      )
    ) {
      const perpendicular = { x: -enemy.chargeVector.y, y: enemy.chargeVector.x }
      const side = member.aiSlot % 2 === 0 ? 1 : -1
      escapeX += perpendicular.x * side
      escapeY += perpendicular.y * side
      moveSpeed = Math.max(moveSpeed, lineAvoidance.moveSpeed)
      avoidDistance = Math.max(avoidDistance, lineAvoidance.avoidDistance)
    }
  }

  const escapeLength = Math.hypot(escapeX, escapeY)
  if (escapeLength <= 0.01) return null

  return {
    point: clampPointToArena(state, {
      x: member.x + (escapeX / escapeLength) * avoidDistance,
      y: member.y + (escapeY / escapeLength) * avoidDistance,
    }, member.radius),
    moveSpeed,
  }

  function addRepulsionFromPoint(
    unit: TargetableState,
    hazard: Point,
    avoidance: ActionAiAvoidanceConfig,
    weight: number,
  ) {
    if (weight <= 0) return
    moveSpeed = Math.max(moveSpeed, avoidance.moveSpeed)
    avoidDistance = Math.max(avoidDistance, avoidance.avoidDistance)
    const dx = unit.x - hazard.x
    const dy = unit.y - hazard.y
    const length = Math.hypot(dx, dy)
    if (length <= 0.01) {
      const angle = member.aiSlot * 1.7
      escapeX += Math.cos(angle) * weight
      escapeY += Math.sin(angle) * weight
      return
    }
    escapeX += (dx / length) * weight
    escapeY += (dy / length) * weight
  }
}

function isPointNearSegment(point: Point, start: Point, end: Point, width: number) {
  const dx = end.x - start.x
  const dy = end.y - start.y
  const lengthSquared = dx * dx + dy * dy
  if (lengthSquared <= 0) return distance(point, start) <= width
  const t = Math.max(0, Math.min(1, ((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSquared))
  const projected = {
    x: start.x + dx * t,
    y: start.y + dy * t,
  }
  return distance(point, projected) <= width
}

function resolveResult(state: BulldromeState) {
  if (state.respawnTimer > 0) return

  const livingEnemies = getLivingEnemies(state)
  if (livingEnemies.length <= 0) {
    if (state.runMode === 'marathon') {
      state.bossKills += 1
      state.respawnTimer = 5
      state.fireballs = []
      state.fireSpots = []
      state.spinningBlades = []
      state.damageDoneByUnit = createDamageDoneTracker()
      state.message = `Boss ${state.bossKills} down. Next boss in 5.`
      return
    }

    state.result = 'win'
    state.message = state.dungeonId === 'cyber-dragon'
      ? 'Cyber Dragon disabled. Hunt complete.'
      : state.dungeonId === 'yian-kut-ku'
        ? 'Yian Kut-Ku defeated. Hunt complete.'
        : 'Bulldrome broken. Hunt complete.'
  }

  if (state.player.hp <= 0) {
    for (const enemy of getLivingEnemies(state)) {
      enemy.phase = 'victory'
      enemy.telegraph.active = false
      enemy.slamTelegraph.active = false
    }
    state.result = 'loss'
    state.message = 'You were carted. Reset and keep yourself alive.'
  }
}

function respawnBossEncounter(state: BulldromeState) {
  state.encounterStep = 'boss'
  if (state.dungeonId === 'cyber-dragon') {
    state.boss = createCyberDragon()
    state.adds = []
    scaleEnemyForDifficulty(state.difficulty, state.boss)
    state.message = 'Marathon continues. Cyber Dragon reboots.'
  } else if (state.dungeonId === 'yian-kut-ku') {
    state.boss = createYianKutKu()
    state.adds = []
    scaleEnemyForDifficulty(state.difficulty, state.boss)
    state.message = 'Marathon continues. Yian Kut-Ku returns.'
  } else {
    state.boss = createBulldrome()
    state.adds = []
    scaleEnemyForDifficulty(state.difficulty, state.boss)
    state.message = 'Marathon continues. Bulldrome returns.'
  }
  state.fireballs = []
  state.fireSpots = []
  state.spinningBlades = []
  state.respawnTimer = 0
}

function findTarget(state: BulldromeState, id: string) {
  return getTargetableUnits(state).find((unit) => unit.id === id)
}

function findEnemyTarget(state: BulldromeState, id: string) {
  return getAllEnemies(state).find((enemy) => enemy.id === id)
}

function findCombatTarget(state: BulldromeState, id: string): CombatSpellTarget | undefined {
  return findTarget(state, id) ?? findEnemyTarget(state, id)
}

function isEnemyTarget(state: BulldromeState, target: CombatSpellTarget): target is EnemyState {
  return getAllEnemies(state).some((enemy) => enemy.id === target.id)
}

function setTarget(state: BulldromeState, id: string) {
  const target = findCombatTarget(state, id)
  if (target) state.targetId = target.id
}

function cycleTarget(state: BulldromeState, delta: -1 | 1) {
  const frames = getRaidFrames(state)
  const currentIndex = Math.max(0, frames.findIndex((frame) => frame.id === state.targetId))
  const nextIndex = (currentIndex + delta + frames.length) % frames.length
  state.targetId = frames[nextIndex].id
}

function clampToArena(state: BulldromeState, fighter: FighterState) {
  fighter.x = Math.max(
    state.arena.padding + fighter.radius,
    Math.min(state.arena.width - state.arena.padding - fighter.radius, fighter.x),
  )
  fighter.y = Math.max(
    state.arena.padding + fighter.radius,
    Math.min(state.arena.height - state.arena.padding - fighter.radius, fighter.y),
  )
}

function clampPointToArena(state: BulldromeState, point: Point, radius: number): Point {
  const clamped = {
    x: Math.max(
      state.arena.padding + radius,
      Math.min(state.arena.width - state.arena.padding - radius, point.x),
    ),
    y: Math.max(
      state.arena.padding + radius,
      Math.min(state.arena.height - state.arena.padding - radius, point.y),
    ),
  }
  return clamped
}

function hitArenaWall(state: BulldromeState, fighter: FighterState) {
  return (
    fighter.x <= state.arena.padding + fighter.radius + 1
    || fighter.x >= state.arena.width - state.arena.padding - fighter.radius - 1
    || fighter.y <= state.arena.padding + fighter.radius + 1
    || fighter.y >= state.arena.height - state.arena.padding - fighter.radius - 1
  )
}

function projectToArenaWall(state: BulldromeState, start: Point, vector: Point): Point {
  const minX = state.arena.padding
  const maxX = state.arena.width - state.arena.padding
  const minY = state.arena.padding
  const maxY = state.arena.height - state.arena.padding
  const candidates: number[] = []

  if (vector.x > 0) candidates.push((maxX - start.x) / vector.x)
  if (vector.x < 0) candidates.push((minX - start.x) / vector.x)
  if (vector.y > 0) candidates.push((maxY - start.y) / vector.y)
  if (vector.y < 0) candidates.push((minY - start.y) / vector.y)

  const distanceToWall = Math.min(...candidates.filter((candidate) => candidate > 0))
  return {
    x: start.x + vector.x * distanceToWall,
    y: start.y + vector.y * distanceToWall,
  }
}

function getWindupSeconds(kind: EnemyKind) {
  return getEnemyWindupSeconds(kind)
}

function getChargeSeconds(kind: EnemyKind) {
  return getEnemyChargeSeconds(kind)
}

function getChargeSpeed(kind: EnemyKind) {
  return getEnemyChargeSpeed(kind)
}

function getRecoverSeconds(kind: EnemyKind) {
  return getEnemyRecoverSeconds(kind)
}

function getTankFocusSeconds(kind: EnemyKind) {
  return getEnemyTankFocusSeconds(kind)
}

function getTankSwingSeconds(kind: EnemyKind) {
  return getEnemyTankSwingSeconds(kind)
}

function getTankSwingDamage(kind: EnemyKind) {
  return getEnemyTankSwingDamage(kind)
}

function getChargeDamage(kind: EnemyKind) {
  return getEnemyChargeDamage(kind)
}

function getBodyDamage(kind: EnemyKind) {
  return getEnemyBodyDamage(kind)
}

function getTrackSeconds(kind: EnemyKind) {
  return getEnemyTrackSeconds(kind)
}

function getAttackFrequency(kind: EnemyKind, attackKind: ActionAttackKind, fallback: number) {
  return getActionAttack(kind, attackKind)?.frequencySeconds ?? fallback
}

function getAttackDamage(kind: EnemyKind, attackKind: ActionAttackKind, fallback: number) {
  const attack = getActionAttack(kind, attackKind)
  if (!attack) return fallback
  return attack.enabled ? attack.damage : 0
}

function applyDifficultyScaling(state: BulldromeState) {
  scaleCurrentEnemiesForDifficulty(state)
}

function scaleCurrentEnemiesForDifficulty(state: BulldromeState) {
  for (const enemy of getAllEnemies(state)) {
    scaleEnemyForDifficulty(state.difficulty, enemy)
  }
}

function scaleEnemyForDifficulty(difficulty: ActionDifficulty, enemy: EnemyState) {
  const multiplier = getActionDifficultyTier(difficulty).healthMultiplier
  enemy.maxHp = Math.ceil(enemy.maxHp * multiplier)
  enemy.hp = enemy.maxHp
}

function scaleDamageForDifficulty(state: BulldromeState, damage: number) {
  return Math.ceil(damage * getActionDifficultyTier(state.difficulty).damageMultiplier)
}

function cloneEnemy(enemy: EnemyState): EnemyState {
  return {
    ...enemy,
    chargeVector: { ...enemy.chargeVector },
    mechanicTimers: { ...enemy.mechanicTimers },
    arcTelegraph: {
      active: enemy.arcTelegraph.active,
      center: { ...enemy.arcTelegraph.center },
      radius: enemy.arcTelegraph.radius,
      startAngle: enemy.arcTelegraph.startAngle,
      endAngle: enemy.arcTelegraph.endAngle,
      width: enemy.arcTelegraph.width,
    },
    slamTelegraph: { ...enemy.slamTelegraph },
    telegraph: {
      active: enemy.telegraph.active,
      start: { ...enemy.telegraph.start },
      end: { ...enemy.telegraph.end },
      width: enemy.telegraph.width,
    },
  }
}

function cloneState(state: BulldromeState): BulldromeState {
  return {
    dungeonId: state.dungeonId,
    difficulty: state.difficulty,
    runMode: state.runMode,
    encounterStep: state.encounterStep,
    arena: { ...state.arena },
	    player: {
	      ...state.player,
	      currentCast: state.player.currentCast ? { ...state.player.currentCast } : null,
	      healOverTimes: state.player.healOverTimes?.map((effect) => ({ ...effect })),
	      spellCooldowns: { ...state.player.spellCooldowns },
	    },
	    party: state.party.map((member) => ({
	      ...member,
	      healOverTimes: member.healOverTimes?.map((effect) => ({ ...effect })),
	    })),
    targetId: state.targetId,
    boss: cloneEnemy(state.boss),
    adds: state.adds.map(cloneEnemy),
    fireballs: state.fireballs.map((fireball) => ({ ...fireball })),
    fireSpots: state.fireSpots.map((spot) => ({ ...spot })),
    spinningBlades: state.spinningBlades.map((blade) => ({ ...blade })),
    nextHazardId: state.nextHazardId,
    damageDoneByUnit: { ...state.damageDoneByUnit },
    bossKills: state.bossKills,
    dungeonWaveIndex: state.dungeonWaveIndex,
    dungeonWaveTotal: state.dungeonWaveTotal,
    respawnTimer: state.respawnTimer,
    healEvents: state.healEvents.map((event) => ({ ...event })),
    noticeEvents: state.noticeEvents.map((event) => ({ ...event })),
    elapsed: state.elapsed,
    message: state.message,
    result: state.result,
    lastHit: state.lastHit,
  }
}
