import {
  type BossInput,
  type BossPhase,
  type Point,
  type RaidFrame,
  type Telegraph,
} from './actionCombatTypes'
import {
  ACTION_CONTROL_RULES,
  ACTION_MOVEMENT_RULES,
  createSpellCooldowns,
  addShield,
  applyHealOverTimeTimers,
  applyRenewTimers,
  applyShieldedDamage,
  dampenHealing,
  getActiveHealOverTimeFrames,
  getActionSpellDefinition,
  getActionSpellManaCost,
  getActionSpellReach,
  getActionSpellTarget,
  getActionTalentAdjustedAmount,
  getHealOverTimeEffectId,
  HEALING_SPELL_RULES,
  recordSpellCast,
  SPELL_GLOBAL_COOLDOWN_SECONDS,
  spendActionSpellMana,
  tickHealOverTimes,
  tickSpellCooldowns,
  tickSpellResource,
  type CastState,
  type HealOverTimeEffect,
  type PartyRole,
  type SpellResourceState,
  type SpellSlot,
} from './actionCombatCore'
import type { TalentModifiers } from './claudeCraftTalents'
import { createEmptyTalentModifiers } from './actionCombatProfile'
import {
  getArenaSpellCooldownMultiplier,
  getArenaSpellCostMultiplier,
  type ArenaUpgradeId,
} from './actionArena'
import {
  ARENA_PVP_DEFAULT_TALENT_POINTS,
  ARENA_PVP_EFFECTIVE_LEVEL,
  createArenaAbilityCooldowns,
  getArenaAbility,
  getArenaClassVitals,
  getArenaClassKit,
  type ArenaAbilityId,
  type ArenaAbilitySchool,
  type ArenaClassId,
} from './actionClassKits'

export type ArenaBattleTeamId = 'player' | 'opponent'
export type ArenaBattleResult = 'playing' | 'player' | 'opponent'
export type ArenaBossKind =
  | 'deacon-varric'
  | 'vael-the-mistcaller'
  | 'ysolei'
  | 'korgath-the-bound'
  | 'grand-necromancer-velkhar'
  | 'korzul-the-gravewyrm'

export type ArenaBattleUnit = {
  abilityCooldowns: Record<string, number>
  activeAbility: ArenaAbilityId | null
  activeTargetId: string | null
  activeAbilityTimer: number
  currentAbilityCast: {
    abilityId: ArenaAbilityId
    remaining: number
    targetId: string
    total: number
  } | null
  damageDone: number
  globalCooldown: number
  classId: ArenaClassId
  id: string
  name: string
  role: PartyRole
  teamId: ArenaBattleTeamId
  x: number
  y: number
  radius: number
  hp: number
  maxHp: number
  shield: number
  stunTimer: number
  invulnerableTimer: number
  healOverTimes?: HealOverTimeEffect[]
  renewTimer: number
  renewTickTimer: number
  attackTimer: number
  aiSlot: number
}

export type ArenaBossState = {
  activeAction: 'idle' | 'melee' | 'run' | 'spell' | 'stomp'
  activeActionTimer: number
  id: string
  name: string
  teamId: ArenaBattleTeamId
  kind: ArenaBossKind
  x: number
  y: number
  radius: number
  hp: number
  maxHp: number
  phase: BossPhase
  phaseTimer: number
  chargeVector: Point
  targetId: string
  chargeCount: number
  tankSwingTimer: number
  telegraph: Telegraph
  slamTelegraph: {
    active: boolean
    radius: number
  }
  mechanics: {
    addWaves: Array<{ attackTimer: number, count: number, name: string, remaining: number }>
    enraged: boolean
    firedSummons: number
    pulseTimer: number
    stompCasting: boolean
    stompCastTimer: number
    stompTimer: number
  }
  spawnCount: number
}

export type ArenaWallHazard = {
  id: string
  x: number
  y: number
  width: number
  height: number
  remaining: number
  warning: number
}

export type ArenaBirdHazard = {
  id: string
  x: number
  y: number
  vx: number
  radius: number
  remaining: number
  warning: number
}

export type ArenaBattleState = {
  round: number
  arena: {
    width: number
    height: number
    padding: number
  }
  player: ArenaBattleUnit & {
    currentCast: CastState | null
    facingX: number
    facingY: number
    globalCooldown: number
    spellCooldowns: Record<SpellSlot, number>
    talentModifiers: TalentModifiers
  } & SpellResourceState
  playerParty: ArenaBattleUnit[]
  opponents: ArenaBattleUnit[]
  opponentHealerResource: SpellResourceState
  pvpEffectiveLevel: number
  pvpDefaultTalentPoints: number
  playerBoss: ArenaBossState
  opponentBoss: ArenaBossState
  playerBossKills: number
  opponentBossKills: number
  dampeningPercent: number
  targetId: string
  playerUpgrades: ArenaUpgradeId[]
  opponentUpgrades: ArenaUpgradeId[]
  walls: ArenaWallHazard[]
  birds: ArenaBirdHazard[]
  nextHazardId: number
  wallTimer: number
  birdTimer: number
  healEvents: Array<{ id: string, targetId: string, amount: number }>
  damageEvents: Array<{ id: string, targetId: string, amount: number, teamId: ArenaBattleTeamId }>
  abilityEvents: Array<{
    id: string
    abilityId: ArenaAbilityId
    casterId: string
    school: ArenaAbilitySchool
    targetId: string
    teamId: ArenaBattleTeamId
  }>
  noticeEvents: Array<{ id: string, targetId: string, text: string }>
  elapsed: number
  message: string
  result: ArenaBattleResult
  lastHit: 'player' | 'party' | 'opponent' | 'hazard' | 'heal' | null
}

export type ArenaBattleSnapshot = {
  playerFrames: RaidFrame[]
  opponentFrames: RaidFrame[]
}

const ARENA_WIDTH = 1680
const ARENA_HEIGHT = 945
const ARENA_SIZE_MULTIPLIER = 1.75
const ARENA_SIDE_GAP = 102
const ARENA_TANK_CLASS_POOL: ArenaClassId[] = ['warrior_protection', 'paladin_protection', 'druid_feral_tank']
const ARENA_HEALER_CLASS_POOL: ArenaClassId[] = ['priest_holy', 'priest_discipline', 'paladin_holy', 'druid_restoration', 'shaman_restoration']
const ARENA_DPS_CLASS_POOL: ArenaClassId[] = [
  'priest_shadow',
  'warrior_arms',
  'warrior_fury',
  'paladin_retribution',
  'druid_feral_cat',
  'druid_balance',
  'mage_arcane',
  'mage_fire',
  'mage_frost',
  'rogue_assassination',
  'rogue_combat',
  'rogue_subtlety',
  'hunter_beast_mastery',
  'hunter_marksmanship',
  'hunter_survival',
  'shaman_elemental',
  'shaman_enhancement',
  'warlock_affliction',
  'warlock_demonology',
  'warlock_destruction',
]
const BOSS_STOMP_CAST_SECONDS = 1.15
const AI_GLOBAL_COOLDOWN_SECONDS = SPELL_GLOBAL_COOLDOWN_SECONDS
const AI_ABILITY_MANA_COSTS: Partial<Record<ArenaAbilityId, number>> = {
  barkskin: 90,
  blessing_of_might: 70,
  flash_heal: 75,
  flash_of_light: 75,
  healing_touch: 130,
  healing_wave: 125,
  heal: 130,
  holy_light: 130,
  lay_on_hands: 220,
  lesser_heal: 65,
  lightning_shield: 90,
  power_word_fortitude: 80,
  power_word_shield: 100,
  regrowth: 95,
  rejuvenation: 75,
  renew: 75,
}
const BOSS_SLAM_RADIUS = 142
const BOSS_DRIFT_SPEED = 82
const CLAUDECRAFT_ELITE_HP_MULTIPLIER = 2.3
const CLAUDECRAFT_ELITE_DAMAGE_MULTIPLIER = 1.5
const ARENA_BOSS_SEQUENCE: Array<{
  attackSpeed: number
  dmgBase: number
  dmgPerLevel: number
  hpBase: number
  hpPerLevel: number
  kind: ArenaBossKind
  level: number
  name: string
  radius: number
  mechanics: ArenaBossMechanicTemplate
}> = [
  {
    attackSpeed: 2.2,
    dmgBase: 8,
    dmgPerLevel: 2.4,
    hpBase: 100,
    hpPerLevel: 22,
    kind: 'deacon-varric',
    level: 9,
    name: 'Deacon Varric',
    radius: 36,
    mechanics: {
      stomp: { radius: 170, every: 12, duration: 0.8, min: 14, max: 22, name: 'Bell Toll' },
      summonAdds: { count: 2, name: 'Funeral Ringers', atHpPct: [0.6, 0.3] },
      enrage: { belowHpPct: 0.2, dmgMult: 1.4, hasteMult: 1.2 },
    },
  },
  {
    attackSpeed: 2.4,
    dmgBase: 12,
    dmgPerLevel: 2.6,
    hpBase: 240,
    hpPerLevel: 34,
    kind: 'vael-the-mistcaller',
    level: 13,
    name: 'Vael the Mistcaller',
    radius: 34,
    mechanics: {
      aoePulse: { min: 16, max: 24, radius: 210, every: 10, name: 'Mist Surge', school: 'frost' },
      summonAdds: { count: 2, name: 'Drowned Thralls', atHpPct: [0.6, 0.3] },
    },
  },
  {
    attackSpeed: 2.5,
    dmgBase: 14,
    dmgPerLevel: 2.9,
    hpBase: 300,
    hpPerLevel: 38,
    kind: 'ysolei',
    level: 18,
    name: 'Ysolei, Avatar of the Drowned Moon',
    radius: 42,
    mechanics: {
      aoePulse: { min: 22, max: 32, radius: 230, every: 9, name: 'Lunar Tide', school: 'arcane' },
      summonAdds: { count: 2, name: 'Moonspawn', atHpPct: [0.6, 0.3] },
      enrage: { belowHpPct: 0.3, dmgMult: 1.4, hasteMult: 1.3 },
    },
  },
  {
    attackSpeed: 2.8,
    dmgBase: 14,
    dmgPerLevel: 2.9,
    hpBase: 260,
    hpPerLevel: 36,
    kind: 'korgath-the-bound',
    level: 20,
    name: 'Korgath the Bound',
    radius: 40,
    mechanics: {
      stomp: { radius: 190, every: 12, duration: 1.5, min: 20, max: 30, name: 'War Stomp' },
      enrage: { belowHpPct: 0.3, dmgMult: 1.5, hasteMult: 1.3 },
    },
  },
  {
    attackSpeed: 2,
    dmgBase: 13,
    dmgPerLevel: 2.8,
    hpBase: 230,
    hpPerLevel: 33,
    kind: 'grand-necromancer-velkhar',
    level: 20,
    name: 'Grand Necromancer Velkhar',
    radius: 34,
    mechanics: {
      summonAdds: { count: 3, name: 'Raised Bonewalkers', atHpPct: [0.66, 0.33] },
    },
  },
  {
    attackSpeed: 2.6,
    dmgBase: 15,
    dmgPerLevel: 3,
    hpBase: 420,
    hpPerLevel: 48,
    kind: 'korzul-the-gravewyrm',
    level: 20,
    name: 'Korzul the Gravewyrm',
    radius: 48,
    mechanics: {
      aoePulse: { min: 30, max: 42, radius: 250, every: 8, name: 'Necrotic Shockwave', school: 'shadow' },
      enrage: { belowHpPct: 0.3, dmgMult: 1.5, hasteMult: 1.3 },
    },
  },
]

type ArenaBossMechanicTemplate = {
  aoePulse?: {
    every: number
    max: number
    min: number
    name: string
    radius: number
    school: ArenaAbilitySchool
  }
  enrage?: { belowHpPct: number, dmgMult: number, hasteMult: number }
  stomp?: {
    duration: number
    every: number
    max: number
    min: number
    name: string
    radius: number
  }
  summonAdds?: { atHpPct: number[], count: number, name: string }
}

export function createArenaBattleState(
  round: number,
  playerUpgrades: ArenaUpgradeId[],
  opponentUpgrades: ArenaUpgradeId[],
  playerClassId: ArenaClassId = 'priest',
  playerTalentModifiers?: TalentModifiers,
): ArenaBattleState {
  const playerParty = createArenaTeamUnits(round, 'player')
  const opponents = createArenaTeamUnits(round, 'opponent')
  const opponentHealer = opponents.find((unit) => unit.role === 'healer') ?? opponents[0]

  return {
    round,
    arena: {
      width: ARENA_WIDTH,
      height: ARENA_HEIGHT,
      padding: 34,
    },
    player: {
      ...createUnit('player', 'You', playerClassId, 'player', 100, scaleArenaX(360), scaleArenaY(302), 0),
      currentCast: null,
      facingX: 1,
      facingY: 0,
      globalCooldown: 0,
      spellCooldowns: createSpellCooldowns(),
      talentModifiers: playerTalentModifiers ?? createEmptyTalentModifiers(),
      ...createArenaSpellResourceState(playerClassId),
    },
    playerParty: [
      ...playerParty,
    ],
    opponents,
    opponentHealerResource: createArenaSpellResourceState(opponentHealer.classId),
    pvpEffectiveLevel: ARENA_PVP_EFFECTIVE_LEVEL,
    pvpDefaultTalentPoints: ARENA_PVP_DEFAULT_TALENT_POINTS,
    playerBoss: createBoss('player', 1),
    opponentBoss: createBoss('opponent', 1),
    playerBossKills: 0,
    opponentBossKills: 0,
    dampeningPercent: 0,
    targetId: 'player-boss-1',
    playerUpgrades,
    opponentUpgrades,
    walls: [],
    birds: [],
    nextHazardId: 1,
    wallTimer: 3.2,
    birdTimer: 2.1,
    healEvents: [],
    damageEvents: [],
    abilityEvents: [],
    noticeEvents: [],
    elapsed: 0,
    message: `PvP Roguelike round ${round}. Kill bosses until one party falls.`,
    result: 'playing',
    lastHit: null,
  }
}

function createArenaTeamUnits(round: number, teamId: ArenaBattleTeamId): ArenaBattleUnit[] {
  const { dpsClasses, tankClass } = createArenaSharedLineup(round)

  if (teamId === 'player') {
    return [
      createUnit('player-tank', getArenaUnitName(tankClass), tankClass, 'player', 142, scaleArenaX(294), scaleArenaY(270), 0),
      createUnit('player-melee-1', getArenaUnitName(dpsClasses[0]), dpsClasses[0], 'player', 98, scaleArenaX(314), scaleArenaY(326), 1),
      createUnit('player-ranged-1', getArenaUnitName(dpsClasses[1]), dpsClasses[1], 'player', 92, scaleArenaX(376), scaleArenaY(226), 2),
      createUnit('player-ranged-2', getArenaUnitName(dpsClasses[2]), dpsClasses[2], 'player', 100, scaleArenaX(376), scaleArenaY(374), 3),
    ]
  }

  const healerClass = pickRandom(ARENA_HEALER_CLASS_POOL, createSeededRng(`roguelike-${round}-opponent-healer`))
  return [
    createUnit('opponent-healer', `Rival ${getArenaUnitName(healerClass)}`, healerClass, 'opponent', 100, scaleArenaX(600), scaleArenaY(302), 0),
    createUnit('opponent-tank', `Rival ${getArenaUnitName(tankClass)}`, tankClass, 'opponent', 142, scaleArenaX(666), scaleArenaY(270), 1),
    createUnit('opponent-melee-1', `Rival ${getArenaUnitName(dpsClasses[0])}`, dpsClasses[0], 'opponent', 98, scaleArenaX(646), scaleArenaY(326), 2),
    createUnit('opponent-ranged-1', `Rival ${getArenaUnitName(dpsClasses[1])}`, dpsClasses[1], 'opponent', 92, scaleArenaX(584), scaleArenaY(226), 3),
    createUnit('opponent-ranged-2', `Rival ${getArenaUnitName(dpsClasses[2])}`, dpsClasses[2], 'opponent', 100, scaleArenaX(584), scaleArenaY(374), 4),
  ]
}

function createArenaSharedLineup(round: number) {
  const rng = createSeededRng(`roguelike-${round}-shared-lineup`)
  return {
    tankClass: pickRandom(ARENA_TANK_CLASS_POOL, rng),
    dpsClasses: [
      pickRandom(ARENA_DPS_CLASS_POOL, rng),
      pickRandom(ARENA_DPS_CLASS_POOL, rng),
      pickRandom(ARENA_DPS_CLASS_POOL, rng),
    ] as [ArenaClassId, ArenaClassId, ArenaClassId],
  }
}

function getArenaUnitName(classId: ArenaClassId) {
  const kit = getArenaClassKit(classId)
  return `${kit.displayName} (${kit.defaultPvpSpec})`
}

function pickRandom<T>(items: T[], rng: () => number) {
  return items[Math.floor(rng() * items.length) % items.length]
}

function createSeededRng(seed: string) {
  let state = 2166136261
  for (let i = 0; i < seed.length; i += 1) {
    state ^= seed.charCodeAt(i)
    state = Math.imul(state, 16777619)
  }
  return () => {
    state += 0x6d2b79f5
    let next = state
    next = Math.imul(next ^ (next >>> 15), next | 1)
    next ^= next + Math.imul(next ^ (next >>> 7), next | 61)
    return ((next ^ (next >>> 14)) >>> 0) / 4294967296
  }
}

export function updateArenaBattleState(
  state: ArenaBattleState,
  input: BossInput,
  deltaSeconds: number,
): ArenaBattleState {
  if (input.reset) return createArenaBattleState(state.round, state.playerUpgrades, state.opponentUpgrades, state.player.classId, state.player.talentModifiers)

  const next = cloneArenaBattleState(state)
  next.elapsed += deltaSeconds
  next.dampeningPercent = Math.floor(next.elapsed / 5)
  next.lastHit = null
  next.healEvents = []
  next.damageEvents = []
  next.abilityEvents = []
  next.noticeEvents = []

  if (input.targetId) setTarget(next, input.targetId)
  if (input.targetDelta !== 0) cycleTarget(next, input.targetDelta)
  if (next.result !== 'playing') return next

  tickUnit(next, next.player, deltaSeconds)
  tickPlayerGlobalCooldown(next, deltaSeconds)
  tickCooldowns(next.player.spellCooldowns, deltaSeconds)
  tickPlayerMana(next, deltaSeconds)
  tickSpellResource(next.opponentHealerResource, deltaSeconds)
  tickCurrentCast(next, deltaSeconds)
  for (const unit of [...next.playerParty, ...next.opponents]) tickUnit(next, unit, deltaSeconds)

  movePlayer(next, input, deltaSeconds)
  updateTeamAi(next, 'player', deltaSeconds)
  updateTeamAi(next, 'opponent', deltaSeconds)
  updateBoss(next, next.playerBoss, [next.player, ...next.playerParty], deltaSeconds)
  updateBoss(next, next.opponentBoss, next.opponents, deltaSeconds)
  resolveBossContact(next, next.playerBoss, [next.player, ...next.playerParty])
  resolveBossContact(next, next.opponentBoss, next.opponents)
  resolveBossRespawns(next)
  startPlayerSpell(next, input.castSpell)
  resolveResult(next)

  return next
}

export function getArenaBattleFrames(state: ArenaBattleState): ArenaBattleSnapshot {
  return {
    playerFrames: [state.player, ...state.playerParty].map((unit) => unitToFrame(state, unit)),
    opponentFrames: state.opponents.map((unit) => unitToFrame(state, unit)),
  }
}

export function getArenaBattleUnits(state: ArenaBattleState): ArenaBattleUnit[] {
  return [state.player, ...state.playerParty, ...state.opponents]
}

export function getArenaBattleBosses(state: ArenaBattleState): ArenaBossState[] {
  return [state.playerBoss, state.opponentBoss]
}

export function getLivingArenaUnits(state: ArenaBattleState, teamId: ArenaBattleTeamId) {
  return getArenaBattleUnits(state).filter((unit) => unit.teamId === teamId && unit.hp > 0)
}

export function getArenaBattleSpellManaCost(upgrades: ArenaUpgradeId[], spell: SpellSlot, storedMomentumReady = false, classId: ArenaClassId = 'priest', talentModifiers?: TalentModifiers) {
  return getActionSpellManaCost(classId, spell, {
    costMultiplier: getArenaSpellCostMultiplier(upgrades, spell),
    freeCast: storedMomentumReady,
    talentMods: talentModifiers,
  })
}

function tickPlayerMana(state: ArenaBattleState, deltaSeconds: number) {
  tickSpellResource(state.player, deltaSeconds)
}

function spendPlayerMana(state: ArenaBattleState, spell: SpellSlot) {
  const spend = spendActionSpellMana(state.player, state.player.classId, spell, {
    costMultiplier: getArenaSpellCostMultiplier(state.playerUpgrades, spell),
    talentMods: state.player.talentModifiers,
  })
  if (!spend.ok) {
    state.message = `Need ${spend.cost} mana.`
    showNotice(state, state.player.id, 'Mana')
    return false
  }

  return true
}

function recordPlayerSpellCast(state: ArenaBattleState) {
  recordSpellCast(state.player, { storedMomentum: state.playerUpgrades.includes('stored-momentum') })
}

function movePlayer(state: ArenaBattleState, input: BossInput, deltaSeconds: number) {
  if (state.player.stunTimer > 0) 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
  moveUnitWithCollision(
    state,
    state.player,
    state.player.facingX * ACTION_MOVEMENT_RULES.baseSpeed * deltaSeconds,
    state.player.facingY * ACTION_MOVEMENT_RULES.baseSpeed * deltaSeconds,
  )
  state.player.x = Math.min(getPlayerSideMaxX(state) - state.player.radius, state.player.x)
}

function updateTeamAi(state: ArenaBattleState, teamId: ArenaBattleTeamId, deltaSeconds: number) {
  const allies = teamId === 'player' ? state.playerParty : state.opponents
  const boss = teamId === 'player' ? state.playerBoss : state.opponentBoss
  for (const unit of allies) {
    if (unit.hp <= 0 || unit.stunTimer > 0) continue
    if (unit.currentAbilityCast) continue
    const kit = getArenaClassKit(unit.classId)
    const targetPoint = boss.hp > 0 ? boss : findLowestFriendlyTarget(state, teamId, unit)
    if (!targetPoint) continue

    if (boss.hp <= 0) {
      if (unit.role === 'healer') castAiPriestHeal(state, teamId, unit)
      continue
    }
    const dx = targetPoint.x - unit.x
    const dy = targetPoint.y - unit.y
    const length = Math.max(1, Math.hypot(dx, dy))
    const attackRange = getArenaUnitAttackRange(unit)
    if (length > attackRange || !hasLineOfSight(state, unit, boss)) {
      const speed = unit.role === 'tank' ? 116 : 132
      moveUnitWithCollision(state, unit, (dx / length) * speed * deltaSeconds, (dy / length) * speed * deltaSeconds)
    }
    constrainToSide(unit, teamId)

    unit.attackTimer -= deltaSeconds
    if (unit.attackTimer > 0) continue
    if (unit.globalCooldown > 0) continue
    unit.attackTimer += kit.attackSeconds

    if (unit.role === 'healer') {
      if (castAiPriestHeal(state, teamId, unit)) continue
    }

    const abilityId = chooseAiDamageAbility(unit)
    if (!abilityId) continue
    const ability = getArenaAbility(abilityId)
    if (boss.hp > 0 && distance(unit, boss) <= ability.range + boss.radius && hasLineOfSight(state, unit, boss)) {
      applyAiAbilityToBoss(state, teamId, unit, abilityId)
      state.lastHit = teamId === 'player' ? 'opponent' : 'party'
      if (boss.hp <= 0) state.message = `${unit.name} defeated ${boss.name}.`
    }
  }
}

function updateBoss(state: ArenaBattleState, boss: ArenaBossState, party: ArenaBattleUnit[], deltaSeconds: number) {
  if (boss.hp <= 0) return
  tickBossAction(boss, deltaSeconds)
  updateClaudeCraftBossMechanics(state, boss, party, deltaSeconds)
  if (boss.phase === 'slamWindup') return

  boss.phaseTimer -= deltaSeconds

  if (boss.phase === 'tracking') {
    boss.targetId = getBossTankTargetId(boss, party)
    const target = findBossTarget(party, boss.targetId)
    if (target) driftBossTowardTarget(state, boss, target, deltaSeconds)
    boss.telegraph.active = false
    boss.slamTelegraph.active = false
    state.message = target ? `${boss.name} tracks ${target.name}.` : `${boss.name} tracks.`
    if (boss.phaseTimer <= 0) startBossTankFocus(boss)
    return
  }

  if (boss.phase === 'mauling') {
    boss.telegraph.active = false
    boss.slamTelegraph.active = false
    boss.targetId = getBossTankTargetId(boss, party)
    const target = findBossTarget(party, boss.targetId)
    if (target) {
      driftBossTowardTarget(state, boss, target, deltaSeconds)
      boss.tankSwingTimer -= deltaSeconds
      state.message = `${boss.name} attacks ${target.name}.`
      if (boss.tankSwingTimer <= 0) {
        applyDamage(target, getBossTankSwingDamage(boss))
        playBossAction(boss, 'melee', 0.75)
        state.abilityEvents.push(createBossAbilityEvent(state, boss, target.id, 'physical'))
        target.invulnerableTimer = 0.12
        state.lastHit = boss.teamId === 'player' ? (target.id === 'player' ? 'player' : 'party') : 'opponent'
        state.message = `${boss.name} hit ${target.name}.`
        boss.tankSwingTimer += getBossTankSwingSeconds(boss)
      }
    }
    if (boss.phaseTimer <= 0) boss.phaseTimer = getBossTankFocusSeconds(boss)
    return
  }

  startBossTankFocus(boss)
}

function updateClaudeCraftBossMechanics(
  state: ArenaBattleState,
  boss: ArenaBossState,
  party: ArenaBattleUnit[],
  deltaSeconds: number,
) {
  const template = getArenaBossTemplateByKind(boss.kind)
  const mechanics = template.mechanics
  const hpFrac = boss.hp / Math.max(1, boss.maxHp)

  if (mechanics.enrage && !boss.mechanics.enraged && hpFrac <= mechanics.enrage.belowHpPct) {
    boss.mechanics.enraged = true
    playBossAction(boss, 'spell', 0.9)
    state.abilityEvents.push(createBossAbilityEvent(state, boss, boss.id, 'fire'))
    state.message = `${boss.name} becomes enraged.`
  }

  if (mechanics.summonAdds) {
    const thresholds = mechanics.summonAdds.atHpPct
    while (boss.mechanics.firedSummons < thresholds.length && hpFrac <= thresholds[boss.mechanics.firedSummons]) {
      boss.mechanics.firedSummons += 1
      boss.mechanics.addWaves.push({
        attackTimer: 0.6,
        count: mechanics.summonAdds.count,
        name: mechanics.summonAdds.name,
        remaining: 10,
      })
      playBossAction(boss, 'spell', 0.9)
      state.abilityEvents.push(createBossAbilityEvent(state, boss, boss.id, 'shadow'))
      state.message = `${boss.name} summons ${mechanics.summonAdds.name}.`
    }
  }

  for (const wave of boss.mechanics.addWaves) {
    wave.remaining = Math.max(0, wave.remaining - deltaSeconds)
    wave.attackTimer -= deltaSeconds
    if (wave.attackTimer > 0) continue
    wave.attackTimer += 1.45
    const target = selectBossMechanicTarget(party, boss, false)
    if (!target) continue
    applyDamage(target, wave.count * 5 * getBossLevelScale(boss) * getBossDamageMultiplier(boss))
    playBossAction(boss, 'spell', 0.55)
    target.invulnerableTimer = Math.max(target.invulnerableTimer, 0.12)
    state.lastHit = boss.teamId === 'player' ? (target.id === 'player' ? 'player' : 'party') : 'opponent'
    state.abilityEvents.push(createBossAbilityEvent(state, boss, target.id, 'shadow'))
    state.message = `${wave.name} hit ${target.name}.`
  }
  boss.mechanics.addWaves = boss.mechanics.addWaves.filter((wave) => wave.remaining > 0)

  if (mechanics.aoePulse) {
    boss.mechanics.pulseTimer -= deltaSeconds
    if (boss.mechanics.pulseTimer <= 0) {
      boss.mechanics.pulseTimer = mechanics.aoePulse.every / getBossHasteMultiplier(boss)
      resolveBossAoePulse(state, boss, party, mechanics.aoePulse)
    }
  }

  if (!mechanics.stomp) return
  if (boss.mechanics.stompCasting) {
    boss.mechanics.stompCastTimer -= deltaSeconds
    boss.slamTelegraph.active = true
    boss.slamTelegraph.radius = mechanics.stomp.radius
    boss.phase = 'slamWindup'
    boss.phaseTimer = boss.mechanics.stompCastTimer
    if (boss.mechanics.stompCastTimer > 0) return
    boss.mechanics.stompCasting = false
    boss.mechanics.stompTimer = mechanics.stomp.every / getBossHasteMultiplier(boss)
    resolveBossStomp(state, boss, party)
    boss.slamTelegraph.active = false
    startBossTankFocus(boss)
    return
  }

  boss.mechanics.stompTimer -= deltaSeconds
  if (boss.mechanics.stompTimer <= 0) {
    boss.mechanics.stompCasting = true
    boss.mechanics.stompCastTimer = BOSS_STOMP_CAST_SECONDS
    playBossAction(boss, 'stomp', BOSS_STOMP_CAST_SECONDS + 0.35)
    boss.phase = 'slamWindup'
    boss.phaseTimer = BOSS_STOMP_CAST_SECONDS
    boss.slamTelegraph.active = true
    boss.slamTelegraph.radius = mechanics.stomp.radius
    state.message = `${boss.name} prepares ${mechanics.stomp.name}.`
  }
}

function resolveBossRespawns(state: ArenaBattleState) {
  const playerDown = state.playerBoss.hp <= 0
  const opponentDown = state.opponentBoss.hp <= 0
  if (!playerDown && !opponentDown) return

  if (playerDown) markBossDefeated(state.playerBoss)
  if (opponentDown) markBossDefeated(state.opponentBoss)

  if (playerDown && opponentDown) {
    const wasTargetingPlayerBoss = state.targetId === state.playerBoss.id
    state.playerBossKills += 1
    state.opponentBossKills += 1
    state.playerBoss = createBoss('player', state.playerBossKills + 1)
    state.opponentBoss = createBoss('opponent', state.opponentBossKills + 1)
    if (wasTargetingPlayerBoss) state.targetId = state.playerBoss.id
    state.message = `Both bosses down. Boss ${state.playerBossKills + 1} spawned.`
  } else if (playerDown) {
    state.message = `Your boss is down. Waiting for rival side.`
  } else {
    state.message = `Rival boss is down. Finish your boss.`
  }
}

function markBossDefeated(boss: ArenaBossState) {
  boss.phase = 'defeated'
  boss.telegraph.active = false
  boss.slamTelegraph.active = false
}

function getBossTankTargetId(boss: ArenaBossState, party: ArenaBattleUnit[]) {
  const tank = party.find((unit) => unit.role === 'tank' && unit.hp > 0)
  return tank?.id ?? selectBossChargeTarget(boss, party)
}

function findBossTarget(party: ArenaBattleUnit[], id: string) {
  return party.find((unit) => unit.id === id && unit.hp > 0) ?? null
}

function driftBossTowardTarget(state: ArenaBattleState, boss: ArenaBossState, target: ArenaBattleUnit, deltaSeconds: number) {
  const dx = target.x - boss.x
  const dy = target.y - boss.y
  const length = Math.hypot(dx, dy)
  if (length <= 1) return

  boss.chargeVector = { x: dx / length, y: dy / length }
  boss.x += (dx / length) * BOSS_DRIFT_SPEED * deltaSeconds
  boss.y += (dy / length) * BOSS_DRIFT_SPEED * deltaSeconds
  if (boss.activeActionTimer <= 0) boss.activeAction = 'run'
  constrainBossToSide(state, boss)
}

function tickBossAction(boss: ArenaBossState, deltaSeconds: number) {
  boss.activeActionTimer = Math.max(0, boss.activeActionTimer - deltaSeconds)
  if (boss.activeActionTimer <= 0) boss.activeAction = 'idle'
}

function playBossAction(boss: ArenaBossState, action: ArenaBossState['activeAction'], seconds: number) {
  boss.activeAction = action
  boss.activeActionTimer = Math.max(boss.activeActionTimer, seconds)
}

function startBossTankFocus(boss: ArenaBossState) {
  boss.phase = 'mauling'
  boss.phaseTimer = getBossTankFocusSeconds(boss)
  boss.tankSwingTimer = getBossTankSwingSeconds(boss)
  boss.telegraph.active = false
  boss.slamTelegraph.active = false
}

function selectBossChargeTarget(boss: ArenaBossState, party: ArenaBattleUnit[]) {
  const living = party.filter((unit) => unit.hp > 0)
  const preferred = boss.teamId === 'player'
    ? ['player', 'player-tank', 'player-melee-1', 'player-ranged-1', 'player-ranged-2']
    : ['opponent-healer', 'opponent-tank', 'opponent-melee-1', 'opponent-ranged-1', 'opponent-ranged-2']
  const offset = boss.chargeCount % preferred.length
  const ordered = preferred.slice(offset).concat(preferred.slice(0, offset))
  return ordered.find((id) => living.some((unit) => unit.id === id)) ?? living[0]?.id ?? ''
}

function resolveBossStomp(state: ArenaBattleState, boss: ArenaBossState, party: ArenaBattleUnit[]) {
  const slam = getArenaBossTemplateByKind(boss.kind).mechanics.stomp
  if (!slam) return

  let hitCount = 0
  for (const unit of party) {
    if (unit.hp <= 0 || distance(unit, boss) > slam.radius) continue
    applyDamage(unit, randomRange(slam.min, slam.max) * getBossLevelScale(boss) * getBossDamageMultiplier(boss))
    unit.stunTimer = slam.duration
    unit.invulnerableTimer = ACTION_CONTROL_RULES.invulnerableSeconds
    hitCount += 1
  }
  state.lastHit = hitCount > 0 ? (boss.teamId === 'player' ? 'party' : 'opponent') : null
  state.abilityEvents.push(createBossAbilityEvent(state, boss, boss.id, 'physical'))
  state.message = hitCount > 0 ? `${slam.name} hit ${hitCount}.` : `${slam.name} missed.`
}

function resolveBossContact(state: ArenaBattleState, boss: ArenaBossState, party: ArenaBattleUnit[]) {
  if (boss.hp <= 0) return

  for (const unit of party) {
    const overlap = unit.hp > 0 && distance(unit, boss) < unit.radius + boss.radius
    if (!overlap || unit.invulnerableTimer > 0) continue

    const isCharge = boss.phase === 'charging'
    applyDamage(unit, isCharge ? getBossChargeDamage(boss) : getBossBodyDamage(boss))
    unit.stunTimer = isCharge ? ACTION_CONTROL_RULES.stunSeconds : 0.25
    unit.invulnerableTimer = ACTION_CONTROL_RULES.invulnerableSeconds
    state.lastHit = boss.teamId === 'player' ? (unit.id === 'player' ? 'player' : 'party') : 'opponent'
    state.message = isCharge
      ? `${unit.name} ate ${boss.name}'s charge.`
      : `${unit.name} was trampled.`

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

function getBossTankFocusSeconds(boss: ArenaBossState) {
  return 2.6 / getBossHasteMultiplier(boss)
}

function getBossTankSwingSeconds(boss: ArenaBossState) {
  return getArenaBossTemplateByKind(boss.kind).attackSpeed / getBossHasteMultiplier(boss)
}

function getBossTankSwingDamage(boss: ArenaBossState) {
  return getBossBaseDamage(boss) * getBossDamageMultiplier(boss)
}

function getBossBodyDamage(boss: ArenaBossState) {
  return Math.round(getBossBaseDamage(boss) * 0.65 * getBossDamageMultiplier(boss))
}

function getBossChargeDamage(boss: ArenaBossState) {
  return Math.round(getBossBaseDamage(boss) * 1.2 * getBossDamageMultiplier(boss))
}

function getArenaBossTemplate(spawnCount: number) {
  return ARENA_BOSS_SEQUENCE[(spawnCount - 1) % ARENA_BOSS_SEQUENCE.length]
}

function getArenaBossTemplateByKind(kind: ArenaBossKind) {
  return ARENA_BOSS_SEQUENCE.find((template) => template.kind === kind) ?? ARENA_BOSS_SEQUENCE[0]
}

function getBossBaseDamage(boss: ArenaBossState) {
  const template = getArenaBossTemplateByKind(boss.kind)
  return Math.round(
    (template.dmgBase + template.dmgPerLevel * (ARENA_PVP_EFFECTIVE_LEVEL - 1))
      * CLAUDECRAFT_ELITE_DAMAGE_MULTIPLIER,
  )
}

function getBossDamageMultiplier(boss: ArenaBossState) {
  const enrage = getArenaBossTemplateByKind(boss.kind).mechanics.enrage
  return boss.mechanics.enraged ? enrage?.dmgMult ?? 1 : 1
}

function getBossHasteMultiplier(boss: ArenaBossState) {
  const enrage = getArenaBossTemplateByKind(boss.kind).mechanics.enrage
  return boss.mechanics.enraged ? enrage?.hasteMult ?? 1 : 1
}

function resolveBossAoePulse(
  state: ArenaBattleState,
  boss: ArenaBossState,
  party: ArenaBattleUnit[],
  pulse: NonNullable<ArenaBossMechanicTemplate['aoePulse']>,
) {
  let hitCount = 0
  for (const unit of party) {
    if (unit.hp <= 0 || distance(unit, boss) > pulse.radius) continue
    applyDamage(unit, randomRange(pulse.min, pulse.max) * getBossLevelScale(boss) * getBossDamageMultiplier(boss))
    unit.invulnerableTimer = Math.max(unit.invulnerableTimer, 0.12)
    hitCount += 1
  }
  state.abilityEvents.push(createBossAbilityEvent(state, boss, boss.id, pulse.school))
  playBossAction(boss, 'spell', 0.9)
  state.lastHit = hitCount > 0 ? (boss.teamId === 'player' ? 'party' : 'opponent') : null
  state.message = hitCount > 0 ? `${boss.name} used ${pulse.name}.` : `${pulse.name} missed.`
}

function selectBossMechanicTarget(party: ArenaBattleUnit[], boss: ArenaBossState, preferTank: boolean) {
  const living = party.filter((unit) => unit.hp > 0)
  if (living.length === 0) return null
  if (preferTank) return findBossTarget(party, boss.targetId) ?? living[0]
  return living[(boss.chargeCount + boss.mechanics.firedSummons + living.length) % living.length]
}

function createBossAbilityEvent(
  state: ArenaBattleState,
  boss: ArenaBossState,
  targetId: string,
  school: ArenaAbilitySchool,
) {
  return {
    abilityId: 'smite' as ArenaAbilityId,
    casterId: boss.id,
    id: `${state.elapsed.toFixed(3)}-${boss.id}-${school}-${state.abilityEvents.length}`,
    school,
    targetId,
    teamId: boss.teamId,
  }
}

function randomRange(min: number, max: number) {
  return Math.round(min + (max - min) * 0.5)
}

function getBossLevelScale(boss: ArenaBossState) {
  return ARENA_PVP_EFFECTIVE_LEVEL / Math.max(1, getArenaBossTemplateByKind(boss.kind).level)
}

function createArenaSpellResourceState(classId: ArenaClassId): SpellResourceState {
  const vitals = getArenaClassVitals(classId)
  return {
    mana: vitals.maxMana,
    maxMana: vitals.maxMana,
    storedMomentumCasts: 0,
    storedMomentumReady: false,
  }
}

function castAiPriestHeal(state: ArenaBattleState, teamId: ArenaBattleTeamId, healer: ArenaBattleUnit) {
  if (healer.currentAbilityCast || healer.globalCooldown > 0) return false
  const target = findLowestFriendlyTarget(state, teamId, healer)
  if (!target) return false
  const healthRatio = target.hp / target.maxHp
  const kit = getArenaClassKit(healer.classId)
  const shieldId = kit.actionBar.find((id) => getArenaAbility(id).kind === 'shield' && getArenaAbility(id).range > 0)
  if (healthRatio < 0.48 && shieldId && canUnitUseAbility(healer, shieldId) && canAffordAiAbility(state, healer, shieldId) && distance(healer, target) <= getArenaAbility(shieldId).range) {
    return startAiAbility(state, healer, target.id, shieldId)
  }

  const healIds = kit.actionBar.filter((id) => getArenaAbility(id).kind === 'heal' && canAffordAiAbility(state, healer, id))
  const instantHeal = healIds.find((id) => getArenaAbility(id).castTime === 0)
  const fastHeal = healIds.find((id) => getArenaAbility(id).castTime <= 1.5)
  const bigHeal = healIds.find((id) => getArenaAbility(id).heal && (getArenaAbility(id).heal ?? 0) >= 200)
  const healId = healthRatio < 0.42 ? fastHeal ?? bigHeal ?? healIds[0] : healthRatio < 0.62 ? bigHeal ?? healIds[0] : instantHeal ?? healIds[0]
  if (!healId) return false
  const ability = getArenaAbility(healId)
  if (!canUnitUseAbility(healer, healId) || distance(healer, target) > ability.range || !hasLineOfSight(state, healer, target)) return false

  return startAiAbility(state, healer, target.id, healId)
}

function findLowestFriendlyTarget(state: ArenaBattleState, teamId: ArenaBattleTeamId, source: ArenaBattleUnit) {
  const allies = teamId === 'player' ? [state.player, ...state.playerParty] : state.opponents
  return allies
    .filter((unit) => unit.hp > 0 && unit.hp < unit.maxHp && hasLineOfSight(state, source, unit))
    .sort((a, b) => (a.hp / a.maxHp) - (b.hp / b.maxHp))[0] ?? null
}

function chooseAiDamageAbility(unit: ArenaBattleUnit): ArenaAbilityId | null {
  const kit = getArenaClassKit(unit.classId)
  const ready = (id: ArenaAbilityId) => canUnitUseAbility(unit, id)
  for (const id of kit.actionBar) {
    const ability = getArenaAbility(id)
    if ((ability.kind === 'damage' || ability.kind === 'attack') && ready(id)) return id
  }
  return ready(kit.basicAttack) ? kit.basicAttack : null
}

function applyAiAbilityToBoss(state: ArenaBattleState, teamId: ArenaBattleTeamId, unit: ArenaBattleUnit, abilityId: ArenaAbilityId) {
  const boss = teamId === 'player' ? state.playerBoss : state.opponentBoss
  startAiAbility(state, unit, boss.id, abilityId)
}

function getAiAbilityManaCost(abilityId: ArenaAbilityId) {
  if (AI_ABILITY_MANA_COSTS[abilityId] !== undefined) return AI_ABILITY_MANA_COSTS[abilityId]
  const ability = getArenaAbility(abilityId)
  if (ability.kind === 'heal') return Math.max(50, Math.round((ability.heal ?? 120) * 0.52))
  if (ability.kind === 'shield') return Math.max(60, Math.round((ability.shield ?? 120) * 0.72))
  if (ability.animation === 'spell') return Math.max(45, Math.round((ability.damage ?? 70) * 0.7))
  return 0
}

function getAiAbilityResource(state: ArenaBattleState, unit: ArenaBattleUnit) {
  return unit.id === 'opponent-healer' ? state.opponentHealerResource : null
}

function canAffordAiAbility(state: ArenaBattleState, unit: ArenaBattleUnit, abilityId: ArenaAbilityId) {
  const resource = getAiAbilityResource(state, unit)
  if (!resource) return true
  return resource.mana >= getAiAbilityManaCost(abilityId)
}

function spendAiAbilityMana(state: ArenaBattleState, unit: ArenaBattleUnit, abilityId: ArenaAbilityId) {
  const resource = getAiAbilityResource(state, unit)
  if (!resource) return true
  const cost = getAiAbilityManaCost(abilityId)
  if (resource.mana < cost) return false
  resource.mana = Math.max(0, resource.mana - cost)
  return true
}

function startAiAbility(state: ArenaBattleState, unit: ArenaBattleUnit, targetId: string, abilityId: ArenaAbilityId) {
  if (!spendAiAbilityMana(state, unit, abilityId)) return false
  const ability = getArenaAbility(abilityId)
  unit.abilityCooldowns[abilityId] = ability.cooldown
  unit.globalCooldown = AI_GLOBAL_COOLDOWN_SECONDS
  unit.activeAbility = abilityId
  unit.activeTargetId = targetId
  unit.activeAbilityTimer = ability.castTime > 0
    ? ability.castTime
    : Math.max(0.24, Math.min(0.9, unit.attackTimer || 0.35))
  if (ability.castTime > 0) {
    unit.currentAbilityCast = {
      abilityId,
      remaining: ability.castTime,
      targetId,
      total: ability.castTime,
    }
    state.message = `${unit.name} is casting ${ability.name}.`
    return true
  }
  resolveAiAbility(state, unit, targetId, abilityId)
  return true
}

function emitAiAbilityEvent(state: ArenaBattleState, unit: ArenaBattleUnit, targetId: string, abilityId: ArenaAbilityId) {
  const ability = getArenaAbility(abilityId)
  state.abilityEvents.push({
    abilityId,
    casterId: unit.id,
    id: `${state.elapsed.toFixed(3)}-${unit.id}-${abilityId}-${state.abilityEvents.length}`,
    school: ability.school,
    targetId,
    teamId: unit.teamId,
  })
}

function playAbility(state: ArenaBattleState, unit: ArenaBattleUnit, targetId: string, abilityId: ArenaAbilityId) {
  const ability = getArenaAbility(abilityId)
  unit.activeAbility = abilityId
  unit.activeTargetId = targetId
  unit.activeAbilityTimer = Math.max(0.24, Math.min(0.9, ability.castTime || unit.attackTimer || 0.35))
  emitAiAbilityEvent(state, unit, targetId, abilityId)
}

function resolveAiAbility(state: ArenaBattleState, unit: ArenaBattleUnit, targetId: string, abilityId: ArenaAbilityId) {
  const ability = getArenaAbility(abilityId)
  emitAiAbilityEvent(state, unit, targetId, abilityId)
  if (ability.kind === 'heal' || ability.kind === 'shield') {
    const target = findArenaUnitById(state, targetId)
    if (!target || target.hp <= 0) return
    if (ability.kind === 'shield') addShield(target, ability.shield ?? 0, { capRatio: HEALING_SPELL_RULES.arenaShieldCapRatio })
    const healOverTimeId = getHealOverTimeEffectId(abilityId)
    if (healOverTimeId) applyHealOverTimeTimers(target, healOverTimeId, { durationSeconds: HEALING_SPELL_RULES.renewSeconds })
    if (ability.heal) healTarget(state, target, ability.heal)
    state.lastHit = 'heal'
    state.message = `${unit.name} used ${ability.name}.`
    return
  }

  const boss = findArenaBossById(state, targetId)
  if (!boss || boss.hp <= 0 || abilityId === 'rapid_fire' || abilityId === 'taunt') return
  const dotPulse = ability.dot ? ability.dot.damage / Math.max(1, ability.dot.seconds / 3) : 0
  const damage = (ability.damage ?? 0) + dotPulse
  applyBossDamageFromUnit(unit, boss, damage)
  showDamage(state, boss.id, damage, unit.teamId)
  state.message = `${unit.name} used ${ability.name}.`
}

function findArenaUnitById(state: ArenaBattleState, id: string) {
  return [state.player, ...state.playerParty, ...state.opponents].find((unit) => unit.id === id) ?? null
}

function findArenaBossById(state: ArenaBattleState, id: string) {
  if (state.playerBoss.id === id) return state.playerBoss
  if (state.opponentBoss.id === id) return state.opponentBoss
  return null
}

function canUnitUseAbility(unit: ArenaBattleUnit, abilityId: ArenaAbilityId) {
  return (unit.abilityCooldowns[abilityId] ?? 0) <= 0
}

function getArenaUnitAttackRange(unit: ArenaBattleUnit) {
  if (unit.role === 'tank' || unit.role === 'melee') return 58
  return 250
}

function startPlayerSpell(state: ArenaBattleState, spell: SpellSlot | null) {
  if (!spell || state.player.currentCast || state.player.globalCooldown > 0 || state.player.spellCooldowns[spell] > 0 || state.player.stunTimer > 0) return
  const spellDef = getActionSpellDefinition(state.player.classId, spell, state.player.talentModifiers)
  if (isArenaDamageSpell(state, spell)) {
    if (state.playerBoss.hp <= 0) {
      state.message = 'No boss target.'
      return
    }
    if (state.targetId !== state.playerBoss.id) {
      showNotice(state, state.player.id, 'Target')
      state.message = `${spellDef.name} requires a boss target.`
      return
    }
    const reach = getPlayerSpellReach(state, state.playerBoss, spell, { includeTargetRadius: true })
    if (!reach.ok) {
      setPlayerSpellReachFailure(state, state.player.id, reach, 'Boss not in range.')
      return
    }
    if (!spendPlayerMana(state, spell)) return
    triggerPlayerGlobalCooldown(state)
    const definition = spellDef
    state.player.currentCast = {
      spell,
      targetId: state.playerBoss.id,
      remaining: definition.castTime,
      total: definition.castTime,
    }
    state.message = `Casting ${definition.name} on ${state.playerBoss.name}.`
    return
  }
  const target = findFriendlyTarget(state, state.targetId)
  if (!target || target.hp <= 0) {
    state.message = 'No living ally selected.'
    return
  }
  if (!canHealTarget(state, target, spell)) return
  if (!spendPlayerMana(state, spell)) return
  triggerPlayerGlobalCooldown(state)

  const definition = spellDef
  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 tickCurrentCast(state: ArenaBattleState, deltaSeconds: number) {
  const cast = state.player.currentCast
  if (!cast) return

  cast.remaining = Math.max(0, cast.remaining - deltaSeconds)
  const target = isArenaDamageSpell(state, cast.spell) ? null : findFriendlyTarget(state, cast.targetId)
  state.message = `Casting ${getActionSpellDefinition(state.player.classId, cast.spell, state.player.talentModifiers).name} on ${target?.name ?? 'target'}.`
  if (cast.remaining > 0) return

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

function completePlayerSpell(state: ArenaBattleState, spell: SpellSlot, targetId: string) {
  if (isArenaDamageSpell(state, spell)) {
    completePlayerDamageSpell(state, spell)
    return
  }
  const target = findFriendlyTarget(state, targetId)
  if (!target || target.hp <= 0 || !canHealTarget(state, target, spell)) return

  state.player.spellCooldowns[spell] = getSpellCooldown(state, spell)
  recordPlayerSpellCast(state)
  const targets = getSpellTargets(state, spell, target)
  const ability = getArenaAbility(getActionSpellDefinition(state.player.classId, spell, state.player.talentModifiers).abilityId)
  const healOverTimeId = getHealOverTimeEffectId(ability.id)
  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)
    for (const unit of targets) {
      shieldTarget(state.playerUpgrades, unit, shield)
      applyLinkedEffects(state.playerUpgrades, unit, spell)
    }
    state.message = `${ability.name} protected ${targets.length}.`
  } else if (healOverTimeId) {
    const amount = getActionTalentAdjustedAmount(state.player.classId, spell, ability.heal ?? 28, 'heal', state.player.talentModifiers)
    for (const unit of targets) {
      applyHealOverTimeTimers(unit, healOverTimeId, {
        durationSeconds: spell === 6 && state.playerUpgrades.includes('slot-2-duration') ? HEALING_SPELL_RULES.renewSeconds * 2 : HEALING_SPELL_RULES.renewSeconds,
      })
      healTarget(state, unit, amount)
      applyLinkedEffects(state.playerUpgrades, unit, spell)
    }
    state.message = `${ability.name} on ${targets.length}.`
  } else {
    const amount = getActionTalentAdjustedAmount(state.player.classId, spell, ability.heal ?? 120, 'heal', state.player.talentModifiers) * (state.playerUpgrades.includes('wide-radiance') ? 1.25 : 1)
    for (const unit of targets) {
      healTarget(state, unit, amount)
      unit.stunTimer = 0
      applyLinkedEffects(state.playerUpgrades, unit, spell)
    }
    state.message = `${ability.name} healed ${targets.length}.`
  }
  state.lastHit = 'heal'
}

function completePlayerDamageSpell(state: ArenaBattleState, spell: SpellSlot) {
  if (state.playerBoss.hp <= 0) return
  const abilityId = getPlayerAbility(state, spell)
  if (!abilityId) return
  const ability = getArenaAbility(abilityId)
  state.player.spellCooldowns[spell] = getSpellCooldown(state, spell)
  recordPlayerSpellCast(state)
  const dotPulse = ability.dot ? ability.dot.damage / Math.max(1, ability.dot.seconds / 3) : 0
  const damage = getActionTalentAdjustedAmount(state.player.classId, spell, (ability.damage ?? 0) + dotPulse, 'damage', state.player.talentModifiers)
  applyBossDamageFromUnit(state.player, state.playerBoss, damage)
  showDamage(state, state.playerBoss.id, damage, 'player')
  playAbility(state, state.player, state.playerBoss.id, abilityId)
  const ally = canTriggerAtonement(state, abilityId) ? findAtonementTarget(state, spell) : undefined
  if (ally) {
    healTarget(state, ally, damage)
    applyLinkedEffects(state.playerUpgrades, ally, spell)
  }
  state.lastHit = 'opponent'
  state.message = ally
    ? `${ability.name} hit ${state.playerBoss.name} and Atonement healed ${ally.name}.`
    : `${ability.name} hit ${state.playerBoss.name}.`
}

function canTriggerAtonement(state: ArenaBattleState, abilityId: ArenaAbilityId) {
  return state.playerUpgrades.includes('atonement') && (abilityId === 'smite' || abilityId === 'shadow_word_pain')
}

function findAtonementTarget(state: ArenaBattleState, spell: SpellSlot) {
  return [state.player, ...state.playerParty]
    .filter((unit) => unit.hp > 0 && unit.hp < unit.maxHp && getPlayerSpellReach(state, unit, spell).ok)
    .sort((a, b) => (a.hp / a.maxHp) - (b.hp / b.maxHp))[0]
}

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

function getPlayerAbility(state: ArenaBattleState, spell: SpellSlot): ArenaAbilityId | null {
  return getActionSpellDefinition(state.player.classId, spell, state.player.talentModifiers).abilityId
}

function canHealTarget(state: ArenaBattleState, target: ArenaBattleUnit, spell: SpellSlot) {
  const reach = getPlayerSpellReach(state, target, spell)
  if (!reach.ok) {
    setPlayerSpellReachFailure(state, target.id, reach)
    return false
  }
  return true
}

function getSpellTargets(state: ArenaBattleState, _spell: SpellSlot, primary: ArenaBattleUnit) {
  const base = 1
  const count = base + (state.playerUpgrades.includes('all-target') ? 1 : 0)
  const allies = [state.player, ...state.playerParty]
    .filter((unit) => unit.hp > 0 && getPlayerSpellReach(state, unit, _spell).ok)
    .sort((a, b) => (a.hp / a.maxHp) - (b.hp / b.maxHp))
  const targets = [primary]
  for (const ally of allies) {
    if (targets.length >= count) break
    if (!targets.some((target) => target.id === ally.id)) targets.push(ally)
  }
  return targets
}

function getPlayerSpellReach(
  state: ArenaBattleState,
  target: ArenaBattleUnit | ArenaBossState,
  spell: SpellSlot,
  options?: { includeTargetRadius?: boolean },
) {
  return getActionSpellReach({
    caster: state.player,
    target,
    classId: state.player.classId,
    spell,
    talentMods: state.player.talentModifiers,
    includeTargetRadius: options?.includeTargetRadius,
    hasLineOfSight: (from, to) => hasLineOfSight(state, from, to),
  })
}

function setPlayerSpellReachFailure(
  state: ArenaBattleState,
  targetId: string,
  reach: ReturnType<typeof getPlayerSpellReach>,
  rangeMessage = 'Not in range.',
) {
  if (reach.ok) return
  const blocked = reach.reason === 'lineOfSight'
  showNotice(state, targetId, blocked ? 'Blocked' : 'Range')
  state.message = blocked ? 'Line of sight blocked.' : rangeMessage
}

function applyLinkedEffects(upgrades: ArenaUpgradeId[], unit: ArenaBattleUnit, spell: SpellSlot) {
  if ((spell === 1 && upgrades.includes('slot-1-renew'))
    || (spell === 7 && upgrades.includes('slot-3-renew'))
    || (spell === 4 && upgrades.includes('slot-4-renew'))
    || (spell === 9 && upgrades.includes('slot-5-renew'))) {
    applyRenew(upgrades, unit, spell)
  }
  if ((spell === 1 && upgrades.includes('slot-1-shield'))
    || (spell === 2 && upgrades.includes('slot-2-shield'))
    || (spell === 9 && upgrades.includes('slot-5-shield'))) {
    shieldTarget(upgrades, unit, 24)
  }
  if (spell === 7 && upgrades.includes('slot-3-half-shield')) shieldTarget(upgrades, unit, HEALING_SPELL_RULES.sunWardShield * 0.5)
}

function applyRenew(upgrades: ArenaUpgradeId[], unit: ArenaBattleUnit, spell: SpellSlot) {
  applyRenewTimers(unit, spell === 6 && upgrades.includes('slot-2-duration') ? HEALING_SPELL_RULES.renewSeconds * 2 : HEALING_SPELL_RULES.renewSeconds)
}

function getSpellCooldown(state: ArenaBattleState, spell: SpellSlot) {
  return (getActionSpellDefinition(state.player.classId, spell, state.player.talentModifiers)?.cooldown ?? 0.5) * getArenaSpellCooldownMultiplier(state.playerUpgrades, spell)
}

function tickUnit(state: ArenaBattleState, unit: ArenaBattleUnit, deltaSeconds: number) {
  unit.stunTimer = Math.max(0, unit.stunTimer - deltaSeconds)
  unit.invulnerableTimer = Math.max(0, unit.invulnerableTimer - deltaSeconds)
  unit.globalCooldown = Math.max(0, unit.globalCooldown - deltaSeconds)
  if (unit.currentAbilityCast) {
    unit.currentAbilityCast.remaining = Math.max(0, unit.currentAbilityCast.remaining - deltaSeconds)
    unit.activeAbilityTimer = unit.currentAbilityCast.remaining
    if (unit.currentAbilityCast.remaining <= 0) {
      const cast = unit.currentAbilityCast
      unit.currentAbilityCast = null
      resolveAiAbility(state, unit, cast.targetId, cast.abilityId)
      unit.activeAbility = null
      unit.activeTargetId = null
      unit.activeAbilityTimer = 0
    }
  } else {
    unit.activeAbilityTimer = Math.max(0, unit.activeAbilityTimer - deltaSeconds)
  }
  if (!unit.currentAbilityCast && unit.activeAbilityTimer <= 0) {
    unit.activeAbility = null
    unit.activeTargetId = null
  }
  for (const abilityId of Object.keys(unit.abilityCooldowns)) {
    unit.abilityCooldowns[abilityId] = Math.max(0, unit.abilityCooldowns[abilityId] - deltaSeconds)
  }
  if (unit.renewTimer > 0 && unit.hp > 0) {
    tickHealOverTimes(unit, deltaSeconds, (effect) => healTarget(state, unit, effect.tickHeal))
  } else if (unit.healOverTimes?.length && unit.hp > 0) {
    tickHealOverTimes(unit, deltaSeconds, (effect) => healTarget(state, unit, effect.tickHeal))
  }
}

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

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

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

function resolveResult(state: ArenaBattleState) {
  const playerAlive = getLivingArenaUnits(state, 'player').length > 0
  const opponentAlive = getLivingArenaUnits(state, 'opponent').length > 0
  if (playerAlive && opponentAlive) return
  state.result = playerAlive ? 'player' : 'opponent'
  state.message = state.result === 'player'
    ? `Round won. You killed ${state.playerBossKills} bosses.`
    : `Round lost. Rival killed ${state.opponentBossKills} bosses.`
}

function healTarget(state: ArenaBattleState, target: ArenaBattleUnit, amount: number) {
  const before = target.hp
  const dampenedAmount = dampenHealing(amount, state.dampeningPercent)
  target.hp = Math.min(target.maxHp, target.hp + dampenedAmount)
  const healed = target.hp - before
  if (healed <= 0) return
  state.healEvents.push({
    id: `${state.elapsed.toFixed(3)}-${target.id}-${state.healEvents.length}`,
    targetId: target.id,
    amount: healed,
  })
}

function showDamage(state: ArenaBattleState, targetId: string, amount: number, teamId: ArenaBattleTeamId) {
  const rounded = Math.round(amount)
  if (rounded <= 0) return
  state.damageEvents.push({
    id: `${state.elapsed.toFixed(3)}-${targetId}-damage-${state.damageEvents.length}`,
    targetId,
    amount: rounded,
    teamId,
  })
}

function shieldTarget(upgrades: ArenaUpgradeId[], target: ArenaBattleUnit, amount: number) {
  const value = amount * (upgrades.includes('dense-shields') ? 1.25 : 1)
  addShield(target, value, { capRatio: HEALING_SPELL_RULES.arenaShieldCapRatio })
}

function applyDamage(target: ArenaBattleUnit, amount: number) {
  applyShieldedDamage(target, amount)
}

function applyBossDamage(target: ArenaBossState, amount: number) {
  target.hp = Math.max(0, target.hp - amount)
}

function applyBossDamageFromUnit(source: ArenaBattleUnit, target: ArenaBossState, amount: number) {
  const before = target.hp
  applyBossDamage(target, amount)
  source.damageDone += Math.max(0, before - target.hp)
}

function moveUnitWithCollision(state: ArenaBattleState, unit: ArenaBattleUnit, dx: number, dy: number) {
  unit.x += dx
  clampToArena(state, unit)
  for (const wall of state.walls) resolveWallCollision(unit, wall)
  unit.y += dy
  clampToArena(state, unit)
  for (const wall of state.walls) resolveWallCollision(unit, wall)
  constrainToSide(unit, unit.teamId)
}

function constrainToSide(unit: ArenaBattleUnit, teamId: ArenaBattleTeamId) {
  if (teamId === 'player') unit.x = Math.min(getPlayerSideMaxXForWidth(ARENA_WIDTH) - unit.radius, unit.x)
  else unit.x = Math.max(getOpponentSideMinXForWidth(ARENA_WIDTH) + unit.radius, unit.x)
}

function constrainBossToSide(state: ArenaBattleState, boss: ArenaBossState) {
  if (boss.teamId === 'player') {
    boss.x = Math.max(state.arena.padding + boss.radius, Math.min(getPlayerSideMaxX(state) - boss.radius, boss.x))
  } else {
    boss.x = Math.max(getOpponentSideMinX(state) + boss.radius, Math.min(state.arena.width - state.arena.padding - boss.radius, boss.x))
  }
  boss.y = Math.max(state.arena.padding + boss.radius, Math.min(state.arena.height - state.arena.padding - boss.radius, boss.y))
}

function getPlayerSideMaxX(state: ArenaBattleState) {
  return getPlayerSideMaxXForWidth(state.arena.width)
}

function getOpponentSideMinX(state: ArenaBattleState) {
  return getOpponentSideMinXForWidth(state.arena.width)
}

function getPlayerSideMaxXForWidth(width: number) {
  return width / 2 - ARENA_SIDE_GAP / 2
}

function getOpponentSideMinXForWidth(width: number) {
  return width / 2 + ARENA_SIDE_GAP / 2
}

function scaleArenaX(value: number) {
  return Math.round(value * ARENA_SIZE_MULTIPLIER)
}

function scaleArenaY(value: number) {
  return Math.round(value * ARENA_SIZE_MULTIPLIER)
}

function resolveWallCollision(unit: ArenaBattleUnit, wall: ArenaWallHazard) {
  if (wall.warning > 0) return
  const left = wall.x - wall.width / 2
  const right = wall.x + wall.width / 2
  const top = wall.y - wall.height / 2
  const bottom = wall.y + wall.height / 2
  const nearestX = Math.max(left, Math.min(right, unit.x))
  const nearestY = Math.max(top, Math.min(bottom, unit.y))
  const dx = unit.x - nearestX
  const dy = unit.y - nearestY
  const overlap = unit.radius - Math.hypot(dx, dy)
  if (overlap <= 0) return

  if (Math.abs(dx) > Math.abs(dy)) unit.x += Math.sign(dx || 1) * overlap
  else unit.y += Math.sign(dy || 1) * overlap
}

function hasLineOfSight(state: ArenaBattleState, from: { x: number, y: number }, to: { x: number, y: number }) {
  return !state.walls.some((wall) => wall.warning <= 0 && segmentIntersectsRect(from, to, wall))
}

function segmentIntersectsRect(from: { x: number, y: number }, to: { x: number, y: number }, wall: ArenaWallHazard) {
  const left = wall.x - wall.width / 2
  const right = wall.x + wall.width / 2
  const top = wall.y - wall.height / 2
  const bottom = wall.y + wall.height / 2
  for (let i = 0; i <= 12; i += 1) {
    const t = i / 12
    const x = from.x + (to.x - from.x) * t
    const y = from.y + (to.y - from.y) * t
    if (x >= left && x <= right && y >= top && y <= bottom) return true
  }
  return false
}

function findFriendlyTarget(state: ArenaBattleState, id: string) {
  return [state.player, ...state.playerParty].find((unit) => unit.id === id)
}

function setTarget(state: ArenaBattleState, id: string) {
  if (findFriendlyTarget(state, id) || id === state.playerBoss.id) state.targetId = id
}

function cycleTarget(state: ArenaBattleState, delta: -1 | 1) {
  const units = [state.playerBoss, state.player, ...state.playerParty]
  const currentIndex = Math.max(0, units.findIndex((unit) => unit.id === state.targetId))
  state.targetId = units[(currentIndex + delta + units.length) % units.length].id
}

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

function unitToFrame(state: ArenaBattleState, unit: ArenaBattleUnit): RaidFrame {
  return {
    id: unit.id,
    name: unit.name,
    role: unit.role,
    specLabel: getArenaClassKit(unit.classId).defaultPvpSpec,
    damageDone: unit.damageDone,
    hp: unit.hp,
    maxHp: unit.maxHp,
    healOverTimes: getActiveHealOverTimeFrames(unit),
    renewTimer: unit.renewTimer,
    burnTimer: 0,
    bleedTimer: 0,
    selected: unit.id === state.targetId,
    shield: unit.shield,
  }
}

function createUnit(
  id: string,
  name: string,
  classId: ArenaClassId,
  teamId: ArenaBattleTeamId,
  _maxHp: number,
  x: number,
  y: number,
  aiSlot: number,
): ArenaBattleUnit {
  const kit = getArenaClassKit(classId)
  const vitals = getArenaClassVitals(classId)
  const maxHp = vitals.maxHp
  return {
    abilityCooldowns: createArenaAbilityCooldowns(classId),
    activeAbility: null,
    activeTargetId: null,
    activeAbilityTimer: 0,
    currentAbilityCast: null,
    damageDone: 0,
    globalCooldown: 0,
    classId,
    id,
    name,
    role: kit.role,
    teamId,
    x,
    y,
    radius: kit.role === 'tank' ? 17 : 14,
    hp: maxHp,
    maxHp,
    shield: 0,
    stunTimer: 0,
    invulnerableTimer: 0,
    healOverTimes: [],
    renewTimer: 0,
    renewTickTimer: 0,
    attackTimer: kit.attackSeconds + aiSlot * 0.13,
    aiSlot,
  }
}

function createBoss(teamId: ArenaBattleTeamId, spawnCount: number): ArenaBossState {
  const template = getArenaBossTemplate(spawnCount)
  const cycle = Math.floor((spawnCount - 1) / ARENA_BOSS_SEQUENCE.length)
  const cycleMultiplier = 1 + cycle * 0.12
  const maxHp = Math.round(
    (template.hpBase + template.hpPerLevel * (ARENA_PVP_EFFECTIVE_LEVEL - 1))
      * CLAUDECRAFT_ELITE_HP_MULTIPLIER
      * cycleMultiplier,
  )
  const x = teamId === 'player' ? scaleArenaX(118) : scaleArenaX(842)
  const y = scaleArenaY(270)
  const name = cycle > 0 ? `${template.name} ${cycle + 1}` : template.name
  return {
    activeAction: 'idle',
    activeActionTimer: 0,
    id: `${teamId}-boss-${spawnCount}`,
    name,
    teamId,
    kind: template.kind,
    x,
    y,
    radius: template.radius,
    hp: maxHp,
    maxHp,
    phase: 'tracking',
    phaseTimer: 1.15,
    chargeVector: { x: teamId === 'player' ? 1 : -1, y: 0 },
    targetId: teamId === 'player' ? 'player-tank' : 'opponent-tank',
    chargeCount: 0,
    tankSwingTimer: 1.15,
    telegraph: {
      active: false,
      start: { x, y },
      end: { x: teamId === 'player' ? getPlayerSideMaxXForWidth(ARENA_WIDTH) : getOpponentSideMinXForWidth(ARENA_WIDTH), y },
      width: template.radius * 2.4,
    },
    slamTelegraph: {
      active: false,
      radius: BOSS_SLAM_RADIUS,
    },
    mechanics: {
      addWaves: [],
      enraged: false,
      firedSummons: 0,
      pulseTimer: Math.min(3, template.mechanics.aoePulse?.every ?? 0),
      stompCasting: false,
      stompCastTimer: 0,
      stompTimer: Math.min(3.5, template.mechanics.stomp?.every ?? 0),
    },
    spawnCount,
  }
}

function clampToArena(state: ArenaBattleState, unit: ArenaBattleUnit) {
  unit.x = Math.max(state.arena.padding + unit.radius, Math.min(state.arena.width - state.arena.padding - unit.radius, unit.x))
  unit.y = Math.max(state.arena.padding + unit.radius, Math.min(state.arena.height - state.arena.padding - unit.radius, unit.y))
}

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

function cloneBoss(boss: ArenaBossState): ArenaBossState {
  return {
    ...boss,
    chargeVector: { ...boss.chargeVector },
    telegraph: {
      active: boss.telegraph.active,
      start: { ...boss.telegraph.start },
      end: { ...boss.telegraph.end },
      width: boss.telegraph.width,
    },
    slamTelegraph: { ...boss.slamTelegraph },
    mechanics: {
      ...boss.mechanics,
      addWaves: boss.mechanics.addWaves.map((wave) => ({ ...wave })),
    },
  }
}

function cloneArenaUnit<T extends ArenaBattleUnit>(unit: T): T {
  return {
    ...unit,
    abilityCooldowns: { ...unit.abilityCooldowns },
    currentAbilityCast: unit.currentAbilityCast ? { ...unit.currentAbilityCast } : null,
    healOverTimes: unit.healOverTimes?.map((effect) => ({ ...effect })),
  }
}

function cloneArenaBattleState(state: ArenaBattleState): ArenaBattleState {
  return {
    ...state,
    arena: { ...state.arena },
    player: {
      ...cloneArenaUnit(state.player),
      spellCooldowns: { ...state.player.spellCooldowns },
      currentCast: state.player.currentCast ? { ...state.player.currentCast } : null,
    },
    playerParty: state.playerParty.map((unit) => cloneArenaUnit(unit)),
    opponents: state.opponents.map((unit) => cloneArenaUnit(unit)),
    opponentHealerResource: { ...state.opponentHealerResource },
    playerBoss: cloneBoss(state.playerBoss),
    opponentBoss: cloneBoss(state.opponentBoss),
    playerUpgrades: [...state.playerUpgrades],
    opponentUpgrades: [...state.opponentUpgrades],
    walls: state.walls.map((wall) => ({ ...wall })),
    birds: state.birds.map((bird) => ({ ...bird })),
    healEvents: state.healEvents.map((event) => ({ ...event })),
    damageEvents: state.damageEvents.map((event) => ({ ...event })),
    abilityEvents: state.abilityEvents.map((event) => ({ ...event })),
    noticeEvents: state.noticeEvents.map((event) => ({ ...event })),
  }
}
