local ADDON = ...

local W, H = 760, 620
local PROJECTILE_START_Y, PROJECTILE_DROP = 415, 265
local HEAD_SIZE = 64
local HELD_HEAD_Y = -15
local lanes = { 205, 380, 555 }
local labelOffsets = { 8, 0, -7 }
local labels = { "HUMAN", "ELF", "DWARF" }
local HEAD_TEXTURES = { "HeadHuman", "HeadElf", "HeadDwarf" }
local POT_TEXTURES = { "PotHuman", "PotElf", "PotDwarf" }
local ENGLISH_INTRO_FILE = 641576
local INTRO_FALLBACK_DELAY = 7.2
local ENGLISH_ERROR_FILE = 549632
local FOOTSTEP_FILES = { 1010887, 1010888, 1010889, 1010890, 1010891 }
local CAKE_CATCH_FILE = 567612
local CAKE_POINTS = 50
local NOMI_DEATH_FILE = 895994
local NOMI_DEATH_VOICE_FILE = 632596
local ADDON_PREFIX = "WoWLCDGame"
local min, max, sin, random, pi = math.min, math.max, math.sin, math.random, math.pi
local AXE_IMPACT_ROTATION = pi * 270 / 180
local AXE_IMPACT_Y = -5

local game = { running = false, started = false, score = 0, lives = 5, pandaLane = 2, held = nil, heads = {}, elapsed = 0, spawnIn = 0.8 }
local groupScores = {}

local frame = CreateFrame("Frame", "WoWLCDGameFrame", UIParent)
frame:SetSize(W, H)
frame:SetPoint("CENTER")
frame:SetFrameStrata("DIALOG")
frame:SetMovable(true)
frame:EnableMouse(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", frame.StartMoving)
frame:SetScript("OnDragStop", frame.StopMovingOrSizing)
frame:SetClampedToScreen(true)
frame:SetClipsChildren(false)
frame:Hide()

local bg = frame:CreateTexture(nil, "BACKGROUND")
bg:SetAllPoints()
bg:SetTexture("Interface\\AddOns\\WoWLCDGame\\Textures\\LCDBackground-clean.blp")

local closeButton = CreateFrame("Button", nil, frame, "BackdropTemplate")
closeButton:SetSize(30, 30)
closeButton:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -9, -9)
closeButton:SetFrameLevel(frame:GetFrameLevel() + 30)
closeButton:SetBackdrop({
  bgFile = "Interface\\Buttons\\WHITE8X8",
  edgeFile = "Interface\\Buttons\\WHITE8X8",
  edgeSize = 2,
})
closeButton:SetBackdropColor(0.16, 0.10, 0.06, 0.96)
closeButton:SetBackdropBorderColor(0.48, 0.12, 0.08, 1)
local closeText = closeButton:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
closeText:SetPoint("CENTER", 2, -1)
closeText:SetFont(STANDARD_TEXT_FONT, 18, "OUTLINE")
closeText:SetTextColor(0.82, 0.63, 0.32, 1)
closeText:SetText("X")
closeButton:SetScript("OnClick", function() frame:Hide() end)
closeButton:SetScript("OnEnter", function(self)
  self:SetBackdropColor(0.28, 0.08, 0.05, 1)
  closeText:SetTextColor(1, 0.78, 0.38, 1)
end)
closeButton:SetScript("OnLeave", function(self)
  self:SetBackdropColor(0.16, 0.10, 0.06, 0.96)
  closeText:SetTextColor(0.82, 0.63, 0.32, 1)
end)

local function text(parent, size, x, y, value, anchor)
  local fs = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
  fs:SetFont(STANDARD_TEXT_FONT, size, "OUTLINE")
  fs:SetTextColor(1, 1, 1, 1)
  fs:SetPoint(anchor or "TOPLEFT", x, y)
  fs:SetText(value or "")
  return fs
end

local scoreText = text(frame, 17, 50, -44, "000", "TOPLEFT")
local livesText = text(frame, 17, -50, -44, "HONOR 5/5", "TOPRIGHT")
local titleText = text(frame, 19, 0, -184, "WOW LCD GAME", "TOP")
text(frame, 12, 0, 30, "A/D or LEFT/RIGHT: Move   |   W or UP: Throw   |   SPACE: Start", "BOTTOM")

local highScorePanel = CreateFrame("Frame", nil, frame, "BackdropTemplate")
highScorePanel:SetSize(390, 330)
highScorePanel:SetPoint("CENTER", UIParent, "CENTER", 0, -10)
highScorePanel:SetFrameLevel(frame:GetFrameLevel() + 20)
highScorePanel:SetMovable(true)
highScorePanel:EnableMouse(true)
highScorePanel:RegisterForDrag("LeftButton")
highScorePanel:SetClampedToScreen(true)
highScorePanel:SetScript("OnDragStart", highScorePanel.StartMoving)
highScorePanel:SetScript("OnDragStop", function(self)
  self:StopMovingOrSizing()
  local point, _, relativePoint, x, y = self:GetPoint(1)
  WoWLCDGameDB = WoWLCDGameDB or { scores = {} }
  WoWLCDGameDB.highScorePosition = {
    point = point,
    relativePoint = relativePoint,
    x = x,
    y = y,
  }
end)
highScorePanel:SetBackdrop({
  bgFile = "Interface\\Buttons\\WHITE8X8",
  edgeFile = "Interface\\Buttons\\WHITE8X8",
  edgeSize = 2,
})
highScorePanel:SetBackdropColor(0.05, 0.06, 0.05, 0.94)
highScorePanel:SetBackdropBorderColor(0.75, 0.82, 0.68, 1)
local highScoreTitle = text(highScorePanel, 22, 0, -18, "HIGH SCORES", "TOP")
local highScoreRows = {}
for i = 1, 10 do
  highScoreRows[i] = {
    name = text(highScorePanel, 14, 22, -48 - (i - 1) * 25, "", "TOPLEFT"),
    score = text(highScorePanel, 14, -24, -48 - (i - 1) * 25, "", "TOPRIGHT"),
  }
end
text(highScorePanel, 12, 0, 14, "SPACE TO PLAY AGAIN", "BOTTOM")
highScorePanel:Hide()

local potLabels = {}
for i = 1, 3 do
  local l = text(frame, 13, lanes[i] - W / 2 + labelOffsets[i], 163, labels[i], "BOTTOM")
  l:SetAlpha(0.72)
  potLabels[i] = l
end

local pixelContainer = CreateFrame("Frame", nil, frame)
pixelContainer:SetAllPoints()

local function imageSprite(name, size, parent)
  local holder = CreateFrame("Frame", nil, parent or pixelContainer)
  holder:SetSize(size, size)
  local texture = holder:CreateTexture(nil, "ARTWORK")
  texture:SetAllPoints()
  texture:SetTexture("Interface\\AddOns\\WoWLCDGame\\Textures\\" .. name .. ".tga")
  holder.texture = texture
  return holder
end

-- Three distinct pot silhouettes: crown, leaf, and smithing hammers.
for i = 1, 3 do
  local pot = imageSprite(POT_TEXTURES[i], 112, pixelContainer)
  pot:SetPoint("BOTTOM", frame, "BOTTOMLEFT", lanes[i], 175)
end

local splashes = {}
for lane = 1, 3 do
  local splash = imageSprite("SplashLCD", 100, pixelContainer)
  local inwardOffset = lane == 1 and 7 or (lane == 3 and -8 or 0)
  splash:SetPoint("BOTTOM", frame, "BOTTOMLEFT", lanes[lane] + inwardOffset, 237)
  splash:Hide()
  splashes[lane] = splash
end

local function startSplash(lane)
  local splash = splashes[lane]
  splash.token = (splash.token or 0) + 1
  local token = splash.token
  splash:Show()
  PlaySound(39658, "SFX")
  C_Timer.After(0.32, function()
    if splash.token == token then splash:Hide() end
  end)
end

local garrosh = pixelContainer:CreateTexture(nil, "ARTWORK")
garrosh:SetTexture("Interface\\AddOns\\WoWLCDGame\\Textures\\GarroshLCD-v2.tga")
garrosh:SetSize(140, 140)
garrosh:SetPoint("TOP", frame, "TOPLEFT", lanes[2], -34)

local panda = imageSprite("NomiLCD", 104, pixelContainer)
local axe = imageSprite("AxeLCD", 76, pixelContainer)
axe:Hide()
local axe2 = imageSprite("AxeLCD", 76, pixelContainer)
axe2:Hide()
local cake = imageSprite("CakeLCD", 62, pixelContainer)
cake:Hide()
local heldSprite
local headPool = { {}, {}, {} }

local function acquireHead(kind, size)
  local pool = headPool[kind]
  local head = table.remove(pool)
  if not head then head = imageSprite(HEAD_TEXTURES[kind], size, pixelContainer) end
  head:SetSize(size, size)
  head:Show()
  return head
end

local function releaseHead(head, kind)
  if not head or not kind then return end
  head:Hide()
  head:ClearAllPoints()
  local pool = headPool[kind]
  pool[#pool + 1] = head
end

local function clearFallingHeads()
  for i = 1, #game.heads do
    local head = game.heads[i]
    releaseHead(head.sprite, head.kind)
  end
  wipe(game.heads)
end

local function setPandaLane(lane)
  if game.death and game.death.done then return false end
  local newLane = max(1, min(3, lane))
  local changed = newLane ~= game.pandaLane
  if not changed and game.pandaPositioned then return false end
  game.pandaLane = newLane
  panda:ClearAllPoints()
  panda:SetPoint("BOTTOM", frame, "BOTTOMLEFT", lanes[game.pandaLane], 50)
  if heldSprite then
    heldSprite:ClearAllPoints()
    heldSprite:SetPoint("BOTTOM", panda, "TOP", 0, HELD_HEAD_Y)
  end
  game.pandaPositioned = true
  return changed
end

local function setGarroshLane(lane)
  game.garroshLane = lane
  garrosh:ClearAllPoints()
  garrosh:SetPoint("TOP", frame, "TOPLEFT", lanes[lane], -34)
end

local function updateHUD()
  scoreText:SetText(string.format("%03d", game.score))
  livesText:SetText(string.format("HONOR %d/5", game.lives))
end

local function playFootstep()
  game.footstepIndex = (game.footstepIndex or 0) % #FOOTSTEP_FILES + 1
  PlaySoundFile(FOOTSTEP_FILES[game.footstepIndex], "SFX")
end

local function sortScores(a, b)
  if a.score == b.score then return a.name < b.name end
  return a.score > b.score
end

local function addHighScore(name, score)
  WoWLCDGameDB = WoWLCDGameDB or { scores = {} }
  WoWLCDGameDB.scores = WoWLCDGameDB.scores or {}
  name = (name or "Unknown"):sub(1, 28)
  local scores = WoWLCDGameDB.scores
  score = max(0, tonumber(score) or 0)
  scores[#scores + 1] = { name = name, score = score }
  table.sort(scores, sortScores)
  while #scores > 10 do table.remove(scores) end
end

local function scoreName(name, realm)
  if realm and realm ~= "" then name = name .. "-" .. realm end
  return (name or "Unknown"):sub(1, 28)
end

local function addGroupScore(name, score)
  name = scoreName(name)
  score = max(0, tonumber(score) or 0)
  if not groupScores[name] or score > groupScores[name] then
    groupScores[name] = score
  end
end

local function pruneGroupScores()
  if not IsInGroup() then wipe(groupScores); return end
  local present = {}
  local playerName, playerRealm = UnitFullName("player")
  present[scoreName(playerName, playerRealm)] = true
  local prefix = IsInRaid() and "raid" or "party"
  local count = IsInRaid() and GetNumGroupMembers() or GetNumSubgroupMembers()
  for i = 1, count do
    local name, realm = UnitFullName(prefix .. i)
    if name then present[scoreName(name, realm)] = true end
  end
  for name in pairs(groupScores) do
    if not present[name] then groupScores[name] = nil end
  end
end

local function recordHighScore()
  if game.scoreRecorded then return end
  game.scoreRecorded = true
  local name, realm = UnitFullName("player")
  local personalName = scoreName(name, realm)
  addHighScore(personalName, game.score)
  local channel = IsInRaid() and "RAID" or (IsInGroup() and "PARTY")
  if channel then
    addGroupScore(personalName, game.score)
    C_ChatInfo.SendAddonMessage(ADDON_PREFIX, tostring(game.score), channel)
  end
end

local function showHighScores()
  local scores
  if IsInGroup() then
    scores = {}
    for name, score in pairs(groupScores) do
      scores[#scores + 1] = { name = name, score = score }
    end
    table.sort(scores, sortScores)
    highScoreTitle:SetText("GROUP SCORES")
  else
    scores = WoWLCDGameDB and WoWLCDGameDB.scores or {}
    highScoreTitle:SetText("HIGH SCORES")
  end
  for i = 1, 10 do
    local entry = scores[i]
    highScoreRows[i].name:SetText(entry and string.format("%2d.  %s", i, entry.name) or "")
    highScoreRows[i].score:SetText(entry and string.format("%04d", entry.score) or "")
  end
  highScorePanel:Show()
end

local function clearHeld()
  releaseHead(heldSprite, game.held)
  heldSprite = nil
  game.held = nil
end

local function showHeld(kind)
  clearHeld()
  heldSprite = acquireHead(kind, HEAD_SIZE)
  heldSprite:SetPoint("BOTTOM", panda, "TOP", 0, HELD_HEAD_Y)
  game.held = kind
  PlaySound(1115, "SFX")
end

local function showNomiDead(impactAxe, message)
  panda.texture:SetRotation(pi / 2)
  panda.texture:SetAlpha(1)
  impactAxe:AdjustPointsOffset(0, AXE_IMPACT_Y)
  impactAxe.texture:SetRotation(AXE_IMPACT_ROTATION)
  titleText:SetText(message)
  PlaySoundFile(NOMI_DEATH_FILE, "Dialog")
  PlaySoundFile(NOMI_DEATH_VOICE_FILE, "Dialog")
end

local function startDeathAnimation()
  clearFallingHeads()
  clearHeld()
  game.hazard = nil
  game.blockedTargets = nil
  axe2:Hide()
  game.cake = nil
  cake:Hide()
  game.death = {
    age = 0,
    startX = lanes[game.garroshLane or 2],
  }
  axe:ClearAllPoints()
  axe:SetPoint("CENTER", frame, "BOTTOMLEFT", game.death.startX, 500)
  axe.texture:SetRotation(0)
  axe:Show()
  titleText:SetText("GARROSH STRIKES!")
end

local function updateDeathAnimation(dt)
  local death = game.death
  death.age = death.age + dt
  local t = min(1, death.age / 1.05)
  -- The final axe homes in on Nomi's live position until the impact frame.
  local targetX = lanes[game.pandaLane]
  local x = death.startX + (targetX - death.startX) * t
  local y = 500 - 390 * t + 70 * sin(pi * t)
  axe:ClearAllPoints()
  axe:SetPoint("CENTER", frame, "BOTTOMLEFT", x, y)
  axe.texture:SetRotation(t * pi * 5)
  if t >= 1 then
    death.done = true
    showNomiDead(axe, "NOMI IS DOWN - GAME OVER")
    showHighScores()
  end
end

local function loseLife()
  game.lives = max(0, game.lives - 1)
  updateHUD()
  if game.lives <= 0 then
    game.running = false
    recordHighScore()
    startDeathAnimation()
    return true
  end
  PlaySoundFile(ENGLISH_ERROR_FILE, "Dialog")
  return false
end

local function throwIntoPot()
  if not game.running or not game.held then return end
  if game.held == game.pandaLane then
    game.score = game.score + 10
    titleText:SetText("SCORE +10")
    startSplash(game.pandaLane)
    updateHUD()
  else
    if not loseLife() then titleText:SetText("WRONG POT!") end
  end
  clearHeld()
end

local function chooseGarroshLane()
  local currentLane = game.garroshLane or 2
  if currentLane == 1 then return random(1, 2) end
  if currentLane == 3 then return random(2, 3) end
  return random(1, 3)
end

local function chooseTarget(blockedTargets)
  if not blockedTargets then return random(1, 3) end
  local choice, count
  for lane = 1, 3 do
    if not blockedTargets[lane] then
      count = (count or 0) + 1
      if random(1, count) == 1 then choice = lane end
    end
  end
  return choice or random(1, 3)
end

local function chooseDifferentTarget(excludedTarget)
  if not excludedTarget then return random(1, 3) end
  local target = random(1, 2)
  return target >= excludedTarget and target + 1 or target
end

local function spawnHead(blockedTargets)
  local kind = random(1, 3)
  local s = acquireHead(kind, HEAD_SIZE)
  local target = chooseTarget(blockedTargets)
  local throwLane = chooseGarroshLane()
  local startX = lanes[throwLane]
  setGarroshLane(throwLane)
  s:SetPoint("CENTER", frame, "BOTTOMLEFT", startX, PROJECTILE_START_Y)
  game.heads[#game.heads + 1] = {
    sprite = s,
    kind = kind,
    target = target,
    startX = startX,
    endX = lanes[target],
    age = 0,
  }
  return target
end

local function spawnAxeHazard(throwLane)
  throwLane = throwLane or chooseGarroshLane()
  local target = random(1, 3)
  setGarroshLane(throwLane)
  game.hazard = {{
    sprite = axe,
    age = -0.18,
    startX = lanes[throwLane],
    endX = lanes[target],
    target = target,
  }}
  game.blockedTargets = { [target] = true }
  axe.texture:SetRotation(0)
  axe:ClearAllPoints()
  axe:SetPoint("CENTER", frame, "BOTTOMLEFT", lanes[throwLane], PROJECTILE_START_Y)
  axe:Show()
  if random(1, 100) <= 32 then
    local secondTarget = chooseDifferentTarget(target)
    game.hazard[2] = {
      sprite = axe2,
      age = -0.36,
      startX = lanes[throwLane],
      endX = lanes[secondTarget],
      target = secondTarget,
    }
    game.blockedTargets[secondTarget] = true
    axe2.texture:SetRotation(0)
    axe2:ClearAllPoints()
    axe2:SetPoint("CENTER", frame, "BOTTOMLEFT", lanes[throwLane], PROJECTILE_START_Y)
    axe2:Show()
    titleText:SetText("DOUBLE AXE - DODGE!")
  else
    axe2:Hide()
    titleText:SetText("DODGE THE AXE!")
  end
end

local function updateAxeHazard(dt, duration)
  local hazards = game.hazard
  local remaining = 0
  for _, hazard in ipairs(hazards) do
    if not hazard.done then
      hazard.age = hazard.age + dt
      local t = max(0, min(1, hazard.age / (duration * 0.85)))
      local x = hazard.startX + (hazard.endX - hazard.startX) * t
      local y = PROJECTILE_START_Y - PROJECTILE_DROP * t + 55 * sin(pi * t)
      hazard.sprite:ClearAllPoints()
      hazard.sprite:SetPoint("CENTER", frame, "BOTTOMLEFT", x, y)
      hazard.sprite.texture:SetRotation(t * pi * 6)
      if t >= 1 then
        hazard.done = true
        game.blockedTargets[hazard.target] = nil
        if hazard.target == game.pandaLane then
          game.hazard = nil
          game.blockedTargets = nil
          game.lives = 0
          game.running = false
          game.death = { done = true }
          recordHighScore()
          updateHUD()
          clearHeld()
          for _, other in ipairs(hazards) do
            if other.sprite ~= hazard.sprite then other.sprite:Hide() end
          end
          showNomiDead(hazard.sprite, "AXE HIT - INSTANT GAME OVER")
          showHighScores()
          return
        end
        hazard.sprite:Hide()
      else
        remaining = remaining + 1
      end
    end
  end
  if remaining == 0 then
    game.hazard = nil
    game.blockedTargets = nil
    titleText:SetText(#hazards > 1 and "DOUBLE AXE DODGED!" or "AXE DODGED!")
  end
end

local function spawnCake(throwLane, excludedTarget)
  throwLane = throwLane or chooseGarroshLane()
  local target = chooseDifferentTarget(excludedTarget)
  setGarroshLane(throwLane)
  game.cake = {
    age = -0.30,
    startX = lanes[throwLane],
    endX = lanes[target],
    target = target,
    visible = false,
  }
  game.blockedTargets = { [target] = true }
  cake:ClearAllPoints()
  cake:SetPoint("CENTER", frame, "BOTTOMLEFT", lanes[throwLane], PROJECTILE_START_Y)
  cake:Hide()
  titleText:SetText("BONUS CAKE!")
end

local function updateCake(dt, duration)
  local bonus = game.cake
  bonus.age = bonus.age + dt
  if bonus.age < 0 then return end
  if not bonus.visible then
    bonus.visible = true
    cake:Show()
  end
  local t = max(0, min(1, bonus.age / duration))
  local x = bonus.startX + (bonus.endX - bonus.startX) * t
  local y = PROJECTILE_START_Y - PROJECTILE_DROP * t + 50 * sin(pi * t)
  cake:ClearAllPoints()
  cake:SetPoint("CENTER", frame, "BOTTOMLEFT", x, y)
  if t >= 1 then
    game.cake = nil
    game.blockedTargets = nil
    cake:Hide()
    if bonus.target == game.pandaLane then
      game.score = game.score + CAKE_POINTS
      updateHUD()
      PlaySoundFile(CAKE_CATCH_FILE, "SFX")
      titleText:SetText("CAKE CAUGHT +" .. CAKE_POINTS .. "!")
    else
      titleText:SetText("CAKE MISSED")
    end
  end
end

local function fallDuration()
  -- Starts slowly and reaches maximum speed after roughly three minutes.
  return max(1.15, 3.8 - game.elapsed * 0.015)
end

local function spawnDelay()
  -- Spawn frequency ramps up at the same reduced rate.
  return max(0.60, 1.85 - game.elapsed * 0.007)
end

local function beginRound()
  if not game.intro then return end
  game.intro = false
  game.introHandle = nil
  game.running = true
  for _, label in ipairs(potLabels) do label:Hide() end
  titleText:SetText("CATCH AND SORT!")
end

local function resetGame()
  clearFallingHeads()
  clearHeld()
  game.score, game.lives, game.elapsed, game.spawnIn = 0, 5, 0, 1.25
  game.running = false
  game.started = true
  game.intro = true
  game.scoreRecorded = false
  game.introToken = (game.introToken or 0) + 1
  local introToken = game.introToken
  game.death = nil
  game.hazard = nil
  game.blockedTargets = nil
  game.cake = nil
  for _, label in ipairs(potLabels) do label:Show() end
  for _, splash in ipairs(splashes) do splash.token = (splash.token or 0) + 1; splash:Hide() end
  highScorePanel:Hide()
  axe:Hide()
  axe2:Hide()
  axe:ClearAllPoints()
  axe2:ClearAllPoints()
  cake:Hide()
  cake:ClearAllPoints()
  panda.texture:SetRotation(0)
  panda.texture:SetAlpha(1)
  setGarroshLane(2)
  titleText:SetText("GET READY...")
  setPandaLane(2)
  updateHUD()
  local willPlay, handle = PlaySoundFile(ENGLISH_INTRO_FILE, "Dialog", true)
  if willPlay then
    game.introHandle = handle
    C_Timer.After(INTRO_FALLBACK_DELAY, function()
      if game.intro and game.introToken == introToken then beginRound() end
    end)
  else
    beginRound()
  end
end

local function startGame()
  if game.intro then return end
  if not game.started then resetGame() return end
  if game.lives <= 0 then resetGame() return end
end

local keyFrame = CreateFrame("Frame", nil, frame)
keyFrame:SetAllPoints()
keyFrame:EnableKeyboard(true)
keyFrame:SetPropagateKeyboardInput(true)
keyFrame:SetScript("OnKeyDown", function(self, key)
  local handled = true
  if key == "LEFT" or key == "A" then
    if setPandaLane(game.pandaLane - 1) then playFootstep() end
  elseif key == "RIGHT" or key == "D" then
    if setPandaLane(game.pandaLane + 1) then playFootstep() end
  elseif key == "UP" or key == "W" then throwIntoPot()
  elseif key == "DOWN" or key == "S" then
    -- Consume backwards movement while the game has keyboard focus.
  elseif key == "SPACE" then startGame()
  elseif key == "ESCAPE" then frame:Hide()
  else handled = false end

  self:SetPropagateKeyboardInput(not handled)
end)

frame:SetScript("OnUpdate", function(_, dt)
  if game.death then
    if not game.death.done then updateDeathAnimation(dt) end
    return
  end
  if not game.running then return end
  game.elapsed = game.elapsed + dt
  game.spawnIn = game.spawnIn - dt
  local duration = fallDuration()
  if game.spawnIn <= 0 then
    if game.hazard or game.cake then
      spawnHead(game.blockedTargets)
      game.spawnIn = spawnDelay()
    else
      local roll = random(1, 100)
      if roll <= 18 then
        spawnAxeHazard(game.garroshLane)
        game.spawnIn = max(0.75, duration * 0.28)
      elseif roll <= 30 then
        local headTarget = spawnHead()
        spawnCake(game.garroshLane, headTarget)
        game.spawnIn = spawnDelay()
      else
        spawnHead()
        game.spawnIn = spawnDelay()
      end
    end
  end
  if game.hazard then updateAxeHazard(dt, duration) end
  if game.cake then updateCake(dt, duration) end
  if not game.running then return end
  for i = #game.heads, 1, -1 do
    local h = game.heads[i]
    h.age = h.age + dt
    local t = min(1, h.age / duration)
    local x = h.startX + (h.endX - h.startX) * t
    local y = PROJECTILE_START_Y - PROJECTILE_DROP * t + 45 * sin(pi * t)
    h.sprite:ClearAllPoints()
    h.sprite:SetPoint("CENTER", frame, "BOTTOMLEFT", x, y)
    if t >= 1 then
      releaseHead(h.sprite, h.kind)
      table.remove(game.heads, i)
      if h.target == game.pandaLane and not game.held then
        showHeld(h.kind)
        titleText:SetText(labels[h.kind] .. " CAUGHT")
      else
        if loseLife() then break end
        titleText:SetText("MISSED!")
      end
    end
  end
end)

SLASH_WOWLCDGAME1 = "/wowlcd"
SlashCmdList.WOWLCDGAME = function(message)
  if message and message:match("%S") then return end
  highScorePanel:Hide()
  frame:SetShown(not frame:IsShown())
end

SLASH_WOWLCDSCORES1 = "/wowlcdscores"
SlashCmdList.WOWLCDSCORES = function()
  if highScorePanel:IsShown() then
    highScorePanel:Hide()
    return
  end
  frame:Show()
  showHighScores()
end

frame:RegisterEvent("ADDON_LOADED")
frame:RegisterEvent("SOUNDKIT_FINISHED")
frame:RegisterEvent("CHAT_MSG_ADDON")
frame:RegisterEvent("GROUP_ROSTER_UPDATE")
frame:SetScript("OnEvent", function(_, event, ...)
  if event == "SOUNDKIT_FINISHED" then
    local arg1 = ...
    if game.intro and arg1 == game.introHandle then beginRound() end
    return
  end
  if event == "CHAT_MSG_ADDON" then
    local prefix, message, channel, sender = ...
    if prefix ~= ADDON_PREFIX or (channel ~= "RAID" and channel ~= "PARTY") then return end
    if Ambiguate(sender, "none") == UnitName("player") then return end
    local score = tonumber(message)
    if not score or score < 0 then return end
    addGroupScore(sender, score)
    if highScorePanel:IsShown() then showHighScores() end
    return
  end
  if event == "GROUP_ROSTER_UPDATE" then
    pruneGroupScores()
    if highScorePanel:IsShown() then showHighScores() end
    return
  end
  local name = ...
  if name ~= ADDON then return end
  WoWLCDGameDB = WoWLCDGameDB or { scores = {} }
  local savedPosition = WoWLCDGameDB.highScorePosition
  if savedPosition then
    highScorePanel:ClearAllPoints()
    highScorePanel:SetPoint(
      savedPosition.point or "CENTER",
      UIParent,
      savedPosition.relativePoint or savedPosition.point or "CENTER",
      savedPosition.x or 0,
      savedPosition.y or 0
    )
  end
  local previousScores = WoWLCDGameDB.scores or {}
  WoWLCDGameDB.scores = {}
  local playerName, playerRealm = UnitFullName("player")
  local personalName = scoreName(playerName, playerRealm)
  for _, entry in ipairs(previousScores) do
    if scoreName(entry.name) == personalName or Ambiguate(entry.name or "", "none") == UnitName("player") then
      addHighScore(personalName, entry.score)
    end
  end
  C_ChatInfo.RegisterAddonMessagePrefix(ADDON_PREFIX)
  updateHUD()
  setPandaLane(2)
end)
