import { IWT2_BOSS_METADATA, type Iwt2BossId } from '../content/bosses'
import { IWT2_CLASS_METADATA } from '../content/classes'
import type {
  Iwt2ArenaEvent,
  Iwt2ArenaIndicator,
  Iwt2ArenaInput,
  Iwt2ArenaState,
  Iwt2BossEntityState,
  Iwt2EntityId,
  Iwt2GroundHazardState,
  Iwt2HostileAddState,
  Iwt2PartyAiRole,
  Iwt2PartyEntityId,
  Iwt2PartyEntityState,
  Iwt2ProjectileEntityState,
  Iwt2StatusState,
} from './types'
import type { Iwt2PlayerClassId } from '../content/classes'
import { tickBoss } from './bossAi'
import { separateCircles } from './collision'
import { canPartyMemberHitTarget, tickPartyMember } from './partyAi'
import { addFirePuddle, createHazardIndicators, tickGroundHazards } from './hazards'
import { applyPartyDamageInShape } from './mechanics'
import {
  clampVec2ToArena,
  distanceVec2,
  normalizeVec2,
  scaleVec2,
  subtractVec2,
} from './vector'

const DEFAULT_ARENA_WIDTH = 960
const DEFAULT_ARENA_HEIGHT = 540
const MAX_DT = 1 / 15
const MAX_EVENTS = 80
const HEALER_MANA_REGEN_PER_SECOND = 3
const BOSS_PROJECTILE_BOUNCE_COOLDOWN_SECONDS = 0.22
const IWT2_ARENA_BOSS_IDS: Iwt2BossId[] = ['bulldrome', 'yian-kut-ku', 'great-jaggi', 'khezu']

type InitialPartyMember = {
  id: Iwt2PartyEntityId
  classId: Iwt2PlayerClassId
  aiRole: Iwt2PartyAiRole
  x: number
  y: number
  preferredOffset: { x: number; y: number }
  decisionOffset: number
}

const INITIAL_PARTY: InitialPartyMember[] = [
  { id: 'player-healer', classId: 'healer', aiRole: 'player', x: 250, y: 250, preferredOffset: { x: -260, y: 0 }, decisionOffset: 0 },
  { id: 'paladin-tank', classId: 'paladin', aiRole: 'tank', x: 470, y: 250, preferredOffset: { x: -58, y: 6 }, decisionOffset: 0.08 },
  { id: 'ranger', classId: 'ranger', aiRole: 'ranged', x: 280, y: 140, preferredOffset: { x: -230, y: -92 }, decisionOffset: 0.18 },
  { id: 'mage', classId: 'mage', aiRole: 'ranged', x: 300, y: 365, preferredOffset: { x: -190, y: 98 }, decisionOffset: 0.32 },
  { id: 'rogue', classId: 'rogue', aiRole: 'flanker', x: 520, y: 195, preferredOffset: { x: 24, y: -58 }, decisionOffset: 0.12 },
  { id: 'warrior', classId: 'warrior', aiRole: 'melee', x: 515, y: 310, preferredOffset: { x: -18, y: 64 }, decisionOffset: 0.24 },
]

export function createInitialIwt2ArenaState(bossId: Iwt2BossId = 'bulldrome', bossIds?: Iwt2BossId[]): Iwt2ArenaState {
  const initialBossIds = bossIds?.length ? bossIds.slice(0, 2) : chooseInitialBossIds(bossId)
  const bosses = initialBossIds.map((id, index) => createBossEntity(id, index))
  return {
    schemaVersion: 1,
    time: 0,
    bounds: { width: DEFAULT_ARENA_WIDTH, height: DEFAULT_ARENA_HEIGHT },
    party: INITIAL_PARTY.map(createPartyMember),
    projectiles: [],
    hostileAdds: [],
    hazards: [],
    indicators: [],
    boss: bosses[0],
    bosses,
    nextEventId: 1,
    nextProjectileId: 1,
    nextAddId: 1,
    nextHazardId: 1,
    events: [],
  }
}

function chooseInitialBossIds(primaryBossId: Iwt2BossId): Iwt2BossId[] {
  const remaining = IWT2_ARENA_BOSS_IDS.filter((id) => id !== primaryBossId)
  const random = remaining[Math.floor(Math.random() * remaining.length)] ?? primaryBossId
  return [primaryBossId, random]
}

function createBossEntity(bossId: Iwt2BossId, index: number): Iwt2BossEntityState {
  const bossMetadata = IWT2_BOSS_METADATA[bossId]
  const position = initialBossPosition(index)
  return {
    id: bossId,
    kind: 'boss',
    bossId,
    position,
    velocity: { x: 0, y: 0 },
    facing: { x: -1, y: 0 },
    radius: bossMetadata.radius,
    health: bossMetadata.maxHealth,
    maxHealth: bossMetadata.maxHealth,
    meleeCooldownRemaining: 0.6 + index * 0.25,
    chargeCooldownRemaining: initialBossSpecialCooldown(bossId) + index * 0.7,
    chargeCount: 0,
    attackPhase: 'idle',
    phaseSecondsRemaining: 0,
    chargeStart: { ...position },
    chargeEnd: { ...position },
    chargeHitEntityIds: [],
    slamApplied: false,
    wallContactSeconds: 0,
    relocateTarget: { x: DEFAULT_ARENA_WIDTH * 0.58, y: DEFAULT_ARENA_HEIGHT * 0.5 + (index === 0 ? -64 : 64) },
    fireballCooldownRemaining: initialBossSecondaryCooldown(bossId) + index * 0.7,
    fireballTarget: { x: 320, y: 250 },
    birdWaveThresholdsTriggered: [],
    mechanicLanes: [],
    mechanicCircles: [],
  }
}

function initialBossPosition(index: number) {
  return {
    x: index === 0 ? 660 : 760,
    y: index === 0 ? 190 : 345,
  }
}

function initialBossSpecialCooldown(bossId: Iwt2BossId): number {
  if (bossId === 'bulldrome') return 2
  if (bossId === 'great-jaggi') return 2.4
  if (bossId === 'khezu') return 3
  return 0
}

function initialBossSecondaryCooldown(bossId: Iwt2BossId): number {
  if (bossId === 'yian-kut-ku') return 1.2
  if (bossId === 'khezu') return 1.6
  return 0
}

export function tickIwt2Arena(state: Iwt2ArenaState, input: Iwt2ArenaInput, dt: number): Iwt2ArenaState {
  const step = Math.max(0, Math.min(dt, MAX_DT))
  if (step <= 0) return { ...state, events: [...state.events] }

  const inputMove = {
    x: clampInputAxis(input.moveX),
    y: clampInputAxis(input.moveY),
  }
  const baseState = {
    ...state,
    time: state.time + step,
    party: state.party.map((member) => tickPartyMember(member, state, inputMove, step)),
    events: [],
  }
  const separatedState = {
    ...baseState,
    party: separatePartyFromBosses(baseState.party, baseState),
  }
  const bossResult = tickBosses(separatedState, step)
  const projectileResult = advanceProjectiles(
    [...separatedState.projectiles, ...(bossResult.projectiles ?? [])],
    bossResult.party,
    bossResult.bosses,
    bossResult.hostileAdds ?? separatedState.hostileAdds,
    bossResult.hazards ?? separatedState.hazards,
    bossResult.nextHazardId ?? state.nextHazardId,
    bossResult.nextProjectileId ?? state.nextProjectileId,
    separatedState.bounds,
    separatedState.time,
    step,
  )
  const hazardResult = tickGroundHazards({
    dt: step,
    hazards: projectileResult.hazards,
    party: projectileResult.party,
    time: separatedState.time,
  })
  const damageResult = applyPartyAttacks(
    hazardResult.party,
    projectileResult.bosses,
    projectileResult.hostileAdds,
    separatedState.time,
    projectileResult.nextProjectileId,
  )
  const hotResult = tickPartyHotEffects(damageResult.party, step, separatedState.time)
  const finalParty = regeneratePartyMana(hotResult.party, step)
  const nextEvents = assignEventIds(
    [...bossResult.events, ...projectileResult.events, ...hazardResult.events, ...damageResult.events, ...hotResult.events],
    state.nextEventId,
  )
  return {
    ...separatedState,
    boss: getPrimaryBoss(damageResult.bosses),
    bosses: damageResult.bosses,
    indicators: [...bossResult.indicators, ...createHazardIndicators(hazardResult.hazards)],
    party: finalParty,
    hostileAdds: damageResult.hostileAdds,
    hazards: hazardResult.hazards,
    projectiles: [...projectileResult.projectiles, ...damageResult.projectiles],
    nextProjectileId: damageResult.nextProjectileId,
    nextAddId: bossResult.nextAddId ?? state.nextAddId,
    nextHazardId: projectileResult.nextHazardId,
    nextEventId: state.nextEventId + nextEvents.length,
    events: [...state.events, ...nextEvents].slice(-MAX_EVENTS),
  }
}

function tickPartyHotEffects(
  party: Iwt2PartyEntityState[],
  dt: number,
  time: number,
): { party: Iwt2PartyEntityState[], events: Iwt2ArenaEvent[] } {
  const events: Iwt2ArenaEvent[] = []
  const nextParty = party.map((member) => {
    if (member.hotEffects.length === 0) return member

    let health = member.health
    const hotEffects = member.hotEffects.flatMap((effect) => {
      const remainingSeconds = effect.remainingSeconds - dt
      if (remainingSeconds <= 0) return []
      let nextTickInSeconds = effect.nextTickInSeconds - dt
      if (member.health > 0 && nextTickInSeconds <= 0) {
        const before = health
        health = Math.min(member.maxHealth, health + effect.power)
        const healed = health - before
        if (healed > 0) {
          events.push({
            id: 0,
            time,
            type: 'partyHealed',
            sourceId: 'player-healer',
            targetId: member.id,
            value: healed,
          })
        }
        nextTickInSeconds += effect.tickIntervalSeconds
      }
      return [{
        ...effect,
        nextTickInSeconds,
        remainingSeconds,
      }]
    })

    return {
      ...member,
      health,
      hotEffects,
    }
  })
  return { party: nextParty, events }
}

function regeneratePartyMana(party: Iwt2PartyEntityState[], dt: number): Iwt2PartyEntityState[] {
  return party.map((member) => {
    if (member.maxMana <= 0 || member.health <= 0 || member.mana >= member.maxMana) return member
    return {
      ...member,
      mana: Math.min(member.maxMana, member.mana + HEALER_MANA_REGEN_PER_SECOND * dt),
    }
  })
}

function createPartyMember(initial: InitialPartyMember): Iwt2PartyEntityState {
  const metadata = IWT2_CLASS_METADATA[initial.classId]
  return {
    id: initial.id,
    kind: 'party',
    classId: initial.classId,
    aiRole: initial.aiRole,
    position: { x: initial.x, y: initial.y },
    velocity: { x: 0, y: 0 },
    facing: { x: 1, y: 0 },
    radius: metadata.radius,
    health: metadata.maxHealth,
    maxHealth: metadata.maxHealth,
    shield: 0,
    mana: initial.classId === 'healer' ? 100 : 0,
    maxMana: initial.classId === 'healer' ? 100 : 0,
    damageDone: 0,
    attackCooldownRemaining: initial.classId === 'healer' ? 0 : metadata.attackCooldown * 0.35,
    castSecondsRemaining: 0,
    attackReady: false,
    status: createEmptyStatus(),
    hotEffects: [],
    preferredOffset: { ...initial.preferredOffset },
    decisionSecondsRemaining: initial.decisionOffset,
  }
}

function createEmptyStatus(): Iwt2StatusState {
  return {
    stunnedSeconds: 0,
    knockedDownSeconds: 0,
    invulnerableSeconds: 0,
  }
}

function clampInputAxis(value: number): number {
  if (!Number.isFinite(value)) return 0
  return Math.min(1, Math.max(-1, value))
}

function tickBosses(state: Iwt2ArenaState, dt: number) {
  let nextParty = state.party
  let nextHostileAdds = state.hostileAdds
  let nextHazards = state.hazards
  let nextProjectileId = state.nextProjectileId
  let nextAddId = state.nextAddId
  let nextHazardId = state.nextHazardId
  const bosses: Iwt2BossEntityState[] = []
  const projectiles: Iwt2ProjectileEntityState[] = []
  const events: Iwt2ArenaEvent[] = []
  const indicators: Iwt2ArenaIndicator[] = []

  for (const boss of state.bosses) {
    const bossState = {
      ...state,
      boss,
      party: nextParty,
      hostileAdds: nextHostileAdds,
      hazards: nextHazards,
      nextProjectileId,
      nextAddId,
      nextHazardId,
    }
    const result = tickBoss(bossState, dt)
    bosses.push(result.boss)
    nextParty = result.party
    nextHostileAdds = result.hostileAdds ?? nextHostileAdds
    nextHazards = result.hazards ?? nextHazards
    nextProjectileId = result.nextProjectileId ?? nextProjectileId
    nextAddId = result.nextAddId ?? nextAddId
    nextHazardId = result.nextHazardId ?? nextHazardId
    projectiles.push(...(result.projectiles ?? []))
    events.push(...result.events)
    indicators.push(...result.indicators)
  }

  return {
    bosses,
    boss: getPrimaryBoss(bosses),
    party: nextParty,
    hostileAdds: nextHostileAdds,
    hazards: nextHazards,
    projectiles,
    events,
    indicators,
    nextAddId,
    nextHazardId,
    nextProjectileId,
  }
}

function separatePartyFromBosses(party: Iwt2PartyEntityState[], state: Iwt2ArenaState): Iwt2PartyEntityState[] {
  return party.map((member) => {
    if (member.health <= 0) return member
    let position = member.position
    for (const boss of state.bosses) {
      if (boss.health <= 0) continue
      position = separateCircles(
        { position, radius: member.radius },
        { position: boss.position, radius: boss.radius },
        state.bounds,
      )
    }
    return { ...member, position }
  })
}

function advanceProjectiles(
  projectiles: Iwt2ProjectileEntityState[],
  party: Iwt2PartyEntityState[],
  bosses: Iwt2BossEntityState[],
  hostileAdds: Iwt2HostileAddState[],
  hazards: Iwt2GroundHazardState[],
  nextHazardId: number,
  nextProjectileId: number,
  bounds: Iwt2ArenaState['bounds'],
  time: number,
  dt: number,
): {
  bosses: Iwt2BossEntityState[]
  party: Iwt2PartyEntityState[]
  hostileAdds: Iwt2HostileAddState[]
  hazards: Iwt2GroundHazardState[]
  nextHazardId: number
  nextProjectileId: number
  projectiles: Iwt2ProjectileEntityState[]
  events: Iwt2ArenaEvent[]
} {
  const events: Iwt2ArenaEvent[] = []
  let nextBosses = bosses
  let nextParty = party
  let nextHostileAdds = hostileAdds
  let nextHazards = hazards
  let nextHazardIdValue = nextHazardId
  const nextProjectiles: Iwt2ProjectileEntityState[] = []

  for (const projectile of projectiles) {
    if (projectile.owner === 'boss') {
      const result = advanceBossProjectile({
        bounds,
        dt,
        hazards: nextHazards,
        nextHazardId: nextHazardIdValue,
        party: nextParty,
        projectile,
        time,
      })
      nextParty = result.party
      nextHazards = result.hazards
      nextHazardIdValue = result.nextHazardId
      events.push(...result.events)
      if (result.projectile) nextProjectiles.push(result.projectile)
      continue
    }

    if (nextBosses.every((boss) => boss.health <= 0) && nextHostileAdds.every((add) => add.health <= 0)) continue
    const nextPosition = {
      x: projectile.position.x + projectile.velocity.x * dt,
      y: projectile.position.y + projectile.velocity.y * dt,
    }
    const remainingSeconds = projectile.remainingSeconds - dt
    const hitAdd = nextHostileAdds.find((add) => (
      add.health > 0
      && distanceVec2(nextPosition, add.position) <= add.radius + projectile.radius
    ))
    if (remainingSeconds <= 0) continue
    if (hitAdd) {
      const damage = Math.min(projectile.damage, hitAdd.health)
      nextHostileAdds = nextHostileAdds.map((add) => add.id === hitAdd.id
        ? { ...add, health: Math.max(0, add.health - damage) }
        : add)
      nextParty = addDamageDone(nextParty, projectile.sourceId, damage)
      events.push({
        id: 0,
        time,
        type: 'bossDamaged',
        sourceId: projectile.sourceId,
        targetId: hitAdd.id,
        value: damage,
      })
      if (hitAdd.health - damage <= 0) {
        events.push({
          id: 0,
          time,
          type: 'entityDefeated',
          sourceId: projectile.sourceId,
          targetId: hitAdd.id,
        })
      }
      continue
    }

    const hitBoss = nextBosses.find((boss) => (
      boss.health > 0
      && distanceVec2(nextPosition, boss.position) <= boss.radius + projectile.radius
    ))
    if (hitBoss) {
      const damage = Math.min(projectile.damage, hitBoss.health)
      nextBosses = nextBosses.map((boss) => boss.id === hitBoss.id
        ? { ...boss, health: Math.max(0, boss.health - damage) }
        : boss)
      nextParty = addDamageDone(nextParty, projectile.sourceId, damage)
      events.push({
        id: 0,
        time,
        type: 'bossDamaged',
        sourceId: projectile.sourceId,
        targetId: hitBoss.id,
        value: damage,
      })
      if (hitBoss.health - damage <= 0) {
        events.push({
          id: 0,
          time,
          type: 'entityDefeated',
          sourceId: projectile.sourceId,
          targetId: hitBoss.id,
        })
      }
      continue
    }
    nextProjectiles.push({
      ...projectile,
      position: nextPosition,
      remainingSeconds,
    })
  }

  return {
    bosses: nextBosses,
    party: nextParty,
    hostileAdds: nextHostileAdds,
    hazards: nextHazards,
    nextHazardId: nextHazardIdValue,
    nextProjectileId,
    projectiles: nextProjectiles,
    events,
  }
}

function advanceBossProjectile({
  bounds,
  dt,
  hazards,
  nextHazardId,
  party,
  projectile,
  time,
}: {
  bounds: Iwt2ArenaState['bounds']
  dt: number
  hazards: Iwt2GroundHazardState[]
  nextHazardId: number
  party: Iwt2PartyEntityState[]
  projectile: Iwt2ProjectileEntityState
  time: number
}): {
  party: Iwt2PartyEntityState[]
  hazards: Iwt2GroundHazardState[]
  nextHazardId: number
  projectile?: Iwt2ProjectileEntityState
  events: Iwt2ArenaEvent[]
} {
  const events: Iwt2ArenaEvent[] = []
  const remainingSeconds = projectile.remainingSeconds - dt
  if (remainingSeconds <= 0) return { party, hazards, nextHazardId, events }

  let nextParty = party
  let nextHazards = hazards
  let nextHazardIdValue = nextHazardId
  let bounced = false
  const bounceCooldownSeconds = Math.max(0, (projectile.bounceCooldownSeconds ?? 0) - dt)
  const canBounce = bounceCooldownSeconds <= 0
  let velocity = { ...projectile.velocity }
  let position = {
    x: projectile.position.x + projectile.velocity.x * dt,
    y: projectile.position.y + projectile.velocity.y * dt,
  }

  if (canBounce && (position.x - projectile.radius <= 0 || position.x + projectile.radius >= bounds.width)) {
    velocity = { ...velocity, x: -velocity.x }
    position = clampVec2ToArena(position, projectile.radius, bounds)
    bounced = true
  }
  if (canBounce && (position.y - projectile.radius <= 0 || position.y + projectile.radius >= bounds.height)) {
    velocity = { ...velocity, y: -velocity.y }
    position = clampVec2ToArena(position, projectile.radius, bounds)
    bounced = true
  }

  const hitMember = canBounce
    ? nextParty.find((member) => (
      member.health > 0
      && distanceVec2(position, member.position) <= projectile.radius + member.radius
    ))
    : undefined
  if (hitMember) {
    const result = applyPartyDamageInShape(nextParty, {
      kind: 'circle',
      position: hitMember.position,
      radius: hitMember.radius + projectile.radius,
    }, {
      damage: projectile.damage,
      sourceId: projectile.sourceId,
      time,
    })
    nextParty = result.party
    events.push(...result.events)
    const away = normalizeVec2(subtractVec2(position, hitMember.position))
    const fallback = normalizeVec2(scaleVec2(projectile.velocity, -1))
    const direction = away.x === 0 && away.y === 0 ? fallback : away
    const speed = Math.max(1, Math.hypot(projectile.velocity.x, projectile.velocity.y))
    velocity = scaleVec2(direction, speed)
    position = clampVec2ToArena({
      x: hitMember.position.x + direction.x * (hitMember.radius + projectile.radius + 2),
      y: hitMember.position.y + direction.y * (hitMember.radius + projectile.radius + 2),
    }, projectile.radius, bounds)
    bounced = true
  }

  if (bounced) {
    const puddle = addFirePuddle({
      damage: IWT2_BOSS_METADATA['yian-kut-ku'].firePuddleDamage!,
      duration: IWT2_BOSS_METADATA['yian-kut-ku'].firePuddleSeconds!,
      hazards: nextHazards,
      nextHazardId: nextHazardIdValue,
      position: hitMember?.position ?? position,
      radius: IWT2_BOSS_METADATA['yian-kut-ku'].firePuddleRadius!,
      sourceId: projectile.sourceId,
      time,
    })
    nextHazards = puddle.hazards
    nextHazardIdValue = puddle.nextHazardId
  }

  const bouncesRemaining = bounced
    ? (projectile.bouncesRemaining ?? 0) - 1
    : projectile.bouncesRemaining
  if (bouncesRemaining !== undefined && bouncesRemaining < 0) {
    return { party: nextParty, hazards: nextHazards, nextHazardId: nextHazardIdValue, events }
  }

  return {
    events,
    hazards: nextHazards,
    nextHazardId: nextHazardIdValue,
    party: nextParty,
    projectile: {
      ...projectile,
      bouncesRemaining,
      bounceCooldownSeconds: bounced ? BOSS_PROJECTILE_BOUNCE_COOLDOWN_SECONDS : bounceCooldownSeconds,
      position,
      remainingSeconds,
      velocity,
    },
  }
}

function applyPartyAttacks(
  party: Iwt2PartyEntityState[],
  bosses: Iwt2BossEntityState[],
  hostileAdds: Iwt2HostileAddState[],
  time: number,
  nextProjectileId: number,
): {
  bosses: Iwt2BossEntityState[]
  party: Iwt2PartyEntityState[]
  hostileAdds: Iwt2HostileAddState[]
  projectiles: Iwt2ProjectileEntityState[]
  nextProjectileId: number
  events: Iwt2ArenaEvent[]
} {
  const events: Iwt2ArenaEvent[] = []
  const projectiles: Iwt2ProjectileEntityState[] = []
  let nextBosses = bosses
  let nextHostileAdds = hostileAdds
  let projectileId = nextProjectileId
  const nextParty = party.map((member) => {
    const target = getPriorityAttackTarget(member, nextBosses, nextHostileAdds)
    if (!target) return member
    if (!canPartyMemberHitTarget(member, target.position, target.radius)) return member
    const metadata = IWT2_CLASS_METADATA[member.classId]
    if (metadata.projectileSpeed > 0) {
      if (!member.attackReady) return member
      const direction = normalizeVec2(subtractVec2(target.position, member.position))
      projectiles.push({
        id: `projectile-${projectileId}`,
        sourceId: member.id,
        owner: 'party',
        classId: member.classId,
        projectileKind: member.classId === 'mage' ? 'magic' : 'arrow',
        color: metadata.accentColor,
        position: {
          x: member.position.x + direction.x * (member.radius + 4),
          y: member.position.y + direction.y * (member.radius + 4),
        },
        velocity: scaleVec2(direction, metadata.projectileSpeed),
        radius: member.classId === 'mage' ? 7 : 4,
        damage: metadata.attackDamage,
        remainingSeconds: 1.2,
      })
      projectileId += 1
      return {
        ...member,
        attackCooldownRemaining: metadata.attackCooldown,
        attackReady: false,
      }
    }
    if (member.attackCooldownRemaining > 0) return member
    const damage = Math.min(metadata.attackDamage, target.health)
    if (target.kind === 'boss') {
      nextBosses = nextBosses.map((boss) => boss.id === target.id
        ? { ...boss, health: Math.max(0, boss.health - damage) }
        : boss)
    } else {
      nextHostileAdds = nextHostileAdds.map((add) => add.id === target.id
        ? { ...add, health: Math.max(0, add.health - damage) }
        : add)
    }
    events.push({
      id: 0,
      time,
      type: 'bossDamaged',
      sourceId: member.id,
      targetId: target.id,
      value: damage,
    })
    if (target.health - damage <= 0) {
      events.push({
        id: 0,
        time,
        type: 'entityDefeated',
        sourceId: member.id,
        targetId: target.id,
      })
      return {
        ...member,
        damageDone: member.damageDone + damage,
        attackCooldownRemaining: metadata.attackCooldown,
      }
    }
    return {
      ...member,
      damageDone: member.damageDone + damage,
      attackCooldownRemaining: metadata.attackCooldown,
    }
  })
  return {
    bosses: nextBosses,
    party: nextParty,
    hostileAdds: nextHostileAdds.filter((add) => add.health > 0),
    projectiles,
    nextProjectileId: projectileId,
    events,
  }
}

function getPriorityAttackTarget(
  member: Iwt2PartyEntityState,
  bosses: Iwt2BossEntityState[],
  hostileAdds: Iwt2HostileAddState[],
): (Iwt2BossEntityState | Iwt2HostileAddState) | undefined {
  if (member.health <= 0) return undefined
  const livingAdds = hostileAdds.filter((add) => add.health > 0)
  if (livingAdds.length > 0) {
    return livingAdds.reduce((best, add) => (
      distanceVec2(member.position, add.position) < distanceVec2(member.position, best.position) ? add : best
    ), livingAdds[0])
  }
  const livingBosses = bosses.filter((boss) => boss.health > 0)
  if (livingBosses.length === 0) return undefined
  return livingBosses.reduce((best, boss) => (
    distanceVec2(member.position, boss.position) < distanceVec2(member.position, best.position) ? boss : best
  ), livingBosses[0])
}

function getPrimaryBoss(bosses: Iwt2BossEntityState[]): Iwt2BossEntityState {
  return bosses.find((boss) => boss.health > 0) ?? bosses[0]
}

function addDamageDone(
  party: Iwt2PartyEntityState[],
  sourceId: Iwt2EntityId,
  damage: number,
): Iwt2PartyEntityState[] {
  return party.map((member) => member.id === sourceId
    ? { ...member, damageDone: member.damageDone + damage }
    : member)
}

function assignEventIds(events: Iwt2ArenaEvent[], firstId: number): Iwt2ArenaEvent[] {
  return events.map((event, index) => ({ ...event, id: firstId + index }))
}
