const GAME_ID = "active";
const REFRESH_MS = 1500;

let scoreboardThemeApplied = false;
let scoreboardCurrentTheme = "";
let scoreboardServerConfig = null;
const SCOREBOARD_STATIC_ASSET_PHASE = "45D.3";
let scoreboardLeafletMap = null;
let scoreboardLeafletLayerGroup = null;
let scoreboardLeafletTileLayer = null;
let scoreboardLayoutRendered = false;
let scoreboardLastMode = "";
let scoreboardLastStatus = "";
let scoreboardLastZoneKey = "";
let scoreboardLastPlayerKey = "";
let scoreboardLastObjectiveKey = "";
let scoreboardLastGhostKey = "";
let scoreboardKnownEventIds = new Set();
let scoreboardNotificationsPrimed = false;
let scoreboardInaccessibleReportStatuses = new Map();
let scoreboardInaccessibleReportsPrimed = false;
let scoreboardSawRunningRound = false;
let scoreboardRedirectingToAdmin = false;
const SCOREBOARD_ACHIEVEMENT_NOTIFICATION_STORAGE_KEY = "qrtaggame.scoreboard.announcedAchievements.v1";
let scoreboardAnnouncedAchievementKeys = loadScoreboardAnnouncedAchievementKeys();

window.addEventListener("DOMContentLoaded", () => {
  initializeScoreboardControls();
  initializeScoreboardNotifications();
  if (!window.QRTagScoreboardReportsCompanion) {
    initializeInaccessibleReportTray();
  }
  loadScoreboard();
  setInterval(loadScoreboard, REFRESH_MS);
});

async function loadScoreboard() {
  try {
    const response = await fetch(`/api/games/${encodeURIComponent(GAME_ID)}`, { cache: "no-store" });
    const game = await response.json();

    if (!response.ok) {
      throw new Error(game.error || "Could not load game.");
    }

    try {
      const liveMapResponse = await fetch(`/api/games/${encodeURIComponent(GAME_ID)}/live-map`, { cache: "no-store" });
      if (liveMapResponse.ok) {
        game.liveMap = await liveMapResponse.json();
      }
    } catch {
      // The full in-memory game response remains the compatibility fallback.
    }

    // Phase 45D.1.3: the dedicated locked report endpoint is preferred, but the
    // scoreboard also reconstructs report cards from its already-visible Recent
    // Events stream. A report can therefore never disappear merely because one
    // local polling route failed or returned during a mutation window.
    const snapshotReports = Array.isArray(game?.inaccessibleReports)
      ? game.inaccessibleReports.slice()
      : [];
    let dedicatedReports = [];
    try {
      const resolvedGameId = String(game?.gameId || GAME_ID).trim() || GAME_ID;
      const reportResponse = await fetch(
        `/api/games/${encodeURIComponent(resolvedGameId)}/inaccessible-reports?ts=${Date.now()}`,
        {
          cache: "no-store",
          headers: {
            "Cache-Control": "no-cache",
            "Pragma": "no-cache"
          }
        });
      if (reportResponse.ok) {
        const reportPayload = await reportResponse.json();
        if (Array.isArray(reportPayload?.reports)) {
          dedicatedReports = reportPayload.reports;
        }
      }
    } catch {
      // The event-derived fallback below still provides the review card and
      // decision controls when the dedicated poll is unavailable.
    }

    game.inaccessibleReports = mergeInaccessibleReportSources(
      deriveInaccessibleReportsFromEvents(game?.events),
      snapshotReports,
      dedicatedReports
    );

    await loadScoreboardTheme(game);
    await ensureScoreboardServerConfig();
    handleScoreboardRouteForGameStatus(game);
    renderScoreboard(game);
    updateScoreboardControlsState(game);
    if (!window.QRTagScoreboardReportsCompanion) {
      updateInaccessibleReportTray(game);
      processInaccessibleReportNotifications(game);
    }
    processScoreboardEventNotifications(game);
  } catch (error) {
    const content = document.getElementById("scoreboardContent");
    if (content) {
      content.innerHTML = `<div class="scoreboard-error">${escapeHtml(error.message)}</div>`;
    }
  }
}

async function loadScoreboardTheme(game) {
  const themeName = await resolveScoreboardTheme(game);

  if (scoreboardThemeApplied && scoreboardCurrentTheme === (themeName || "default")) {
    return;
  }

  applyScoreboardTheme(themeName);
  scoreboardThemeApplied = true;
}

async function resolveScoreboardTheme(game) {
  // Phase 36D.29: resolve the real selected display theme from every place
  // the Windows Host can save it. D36D.23 accidentally preferred the local
  // server config first, which can be stale when the host setup screen saved
  // the selected theme through game/sync settings. This made Haunted never win
  // on the scoreboard even though the host UI showed Haunted selected.
  const mode = getScoreboardModeKey(game);
  let configTheme = "";
  let settingsTheme = "";

  try {
    const settingsResponse = await fetch("/api/admin/sync-settings", { cache: "no-store" });
    const settings = await settingsResponse.json();

    if (settingsResponse.ok) {
      settingsTheme = normalizeScoreboardThemeName(readThemeValue(settings));
    }
  } catch {
    // Keep going; game/config may still contain the selected theme.
  }

  try {
    const configResponse = await fetch("/api/admin/server-config", { cache: "no-store" });
    const config = await configResponse.json();

    if (configResponse.ok) {
      scoreboardServerConfig = config;
      configTheme = normalizeScoreboardThemeName(readThemeValue(config));
    }
  } catch {
    // Keep going; game/sync settings may still contain the selected theme.
  }

  const gameTheme = normalizeScoreboardThemeName(readThemeValue(game));

  return chooseScoreboardThemeForMode([
    settingsTheme,
    gameTheme,
    configTheme
  ], mode);
}

function readThemeValue(source) {
  if (!source) {
    return "";
  }

  return source.theme ||
    source.Theme ||
    source.selectedTheme ||
    source.SelectedTheme ||
    source.themeId ||
    source.ThemeId ||
    "";
}

function chooseScoreboardThemeForMode(themeCandidates, mode) {
  const normalized = themeCandidates
    .map(normalizeScoreboardThemeName)
    .filter(Boolean);

  if (normalized.length === 0) {
    return "default";
  }

  // A few game modes have a mode-flavored fallback theme. Those should never
  // beat a real selected host theme like Haunted, Alien Invasion, etc.
  const modeFallbackThemes = new Set([
    mode === "zombie_tag" ? "zombie-tag" : "",
    mode === "zombie_extraction" ? "zombie-extraction" : "",
    mode === "ghost_hunt" ? "ghost-hunt" : ""
  ].filter(Boolean));

  const selectedTheme = normalized.find(theme => !modeFallbackThemes.has(theme));
  return selectedTheme || normalized[0] || "default";
}

function normalizeScoreboardThemeName(themeName) {
  const safeTheme = String(themeName || "").trim();
  if (!safeTheme || safeTheme.toLowerCase() === "default") {
    return "";
  }

  const compact = safeTheme.toLowerCase().replace(/[^a-z0-9]/g, "");
  if (compact.includes("haunted")) return "haunted";
  if (compact.includes("alieninvasion")) return "alien-invasion";
  if (compact.includes("candyapocalypse")) return "candy-apocalypse";
  if (compact.includes("cartoonchaos")) return "cartoon-chaos";
  if (compact.includes("lasertagarena")) return "laser-tag-arena";
  if (compact.includes("neon80s")) return "neon-80s";
  if (compact.includes("piratetreasure")) return "pirate-treasure";
  if (compact.includes("retroarcade")) return "retro-arcade";
  if (compact.includes("superherocity")) return "superhero-city";
  if (compact.includes("tacticalcamo")) return "tactical-camo";
  if (compact.includes("toxicslimelab")) return "toxic-slime-lab";
  if (compact.includes("tronscifi")) return "tron-sci-fi";
  if (compact.includes("zombietag")) return "zombie-tag";
  if (compact.includes("zombieextraction")) return "zombie-extraction";
  if (compact.includes("ghosthunt")) return "ghost-hunt";

  return safeTheme
    .toLowerCase()
    .replace(/_/g, "-")
    .replace(/\s+/g, "-");
}

function applyScoreboardTheme(themeName) {
  const safeTheme = themeName || "default";
  const themeLink = document.getElementById("themeStylesheet");

  scoreboardCurrentTheme = safeTheme;
  document.body.dataset.theme = safeTheme;
  document.documentElement.dataset.theme = safeTheme;
  document.body.className = document.body.className
    .split(/\s+/)
    .filter(name => name && !name.startsWith("theme-"))
    .concat(["scoreboard-display-body", `theme-${safeTheme}`])
    .join(" ");
  document.documentElement.className = document.documentElement.className
    .split(/\s+/)
    .filter(name => name && !name.startsWith("theme-"))
    .concat([`theme-${safeTheme}`])
    .join(" ");
  document.body.classList.toggle(
    "theme-bright",
    ["cartoon-chaos", "superhero-city", "candy-apocalypse", "pirate-treasure"].includes(safeTheme)
  );
  document.body.classList.toggle("scoreboard-theme-haunted-active", safeTheme === "haunted");
  document.documentElement.classList.toggle("scoreboard-theme-haunted-active", safeTheme === "haunted");

  if (!themeLink) {
    return;
  }

  if (safeTheme === "default") {
    themeLink.removeAttribute("href");
    return;
  }

  const href = `/themes/${encodeURIComponent(safeTheme)}.css?v=44D49`;

  if (themeLink.getAttribute("href") !== href) {
    themeLink.href = href;
  }
}



function readInaccessibleReportEventId(event) {
  return String(
    getCustomValue(event, "reportId") ||
    event?.reportId ||
    ""
  ).trim();
}

function parseEventBoolean(value) {
  if (typeof value === "boolean") return value;
  return ["true", "1", "yes", "on"].includes(String(value || "").trim().toLowerCase());
}

function deriveInaccessibleReportsFromEvents(events) {
  const orderedEvents = Array.isArray(events)
    ? events.slice().sort((a, b) => new Date(a?.createdAtUtc || 0) - new Date(b?.createdAtUtc || 0))
    : [];
  const reports = new Map();

  orderedEvents.forEach(event => {
    const type = String(event?.type || "").trim().toLowerCase();
    const reportId = readInaccessibleReportEventId(event);
    if (!reportId) return;

    if (type === "inaccessible_report_received") {
      reports.set(reportId, {
        reportId,
        actionId: "",
        createdAtUtc: event?.createdAtUtc || new Date().toISOString(),
        reporterPlayerId: event?.playerId || "",
        reporterHandle: event?.playerHandle || "Player",
        objectiveId: event?.objectiveId || "",
        objectiveName: event?.objectiveName || "Objective",
        objectiveType: getCustomValue(event, "objectiveType") || "",
        status: "pending",
        requiresHostDecision: parseEventBoolean(getCustomValue(event, "requiresHostDecision")),
        resolution: "Pending host review.",
        message: "Waiting for the host to accept or decline this relocation request.",
        customData: { ...(event?.customData || {}) },
        derivedFromScoreboardEvents: true
      });
      return;
    }

    const existing = reports.get(reportId);
    if (!existing) return;

    const outcomes = {
      inaccessible_objective_relocated: "accepted",
      inaccessible_report_declined: "declined",
      inaccessible_report_rejected: "rejected",
      ghost_inaccessible_retargeted: "auto_resolved"
    };
    const status = outcomes[type];
    if (!status) return;

    reports.set(reportId, {
      ...existing,
      status,
      resolvedAtUtc: event?.createdAtUtc || new Date().toISOString(),
      resolution: event?.message || existing.resolution,
      message: event?.message || existing.message,
      customData: {
        ...(existing.customData || {}),
        ...(event?.customData || {})
      }
    });
  });

  return Array.from(reports.values());
}

function mergeInaccessibleReportSources(...sources) {
  const merged = new Map();

  sources.forEach(source => {
    if (!Array.isArray(source)) return;

    source.forEach(report => {
      const key = getInaccessibleReportKey(report);
      if (!key) return;

      const existing = merged.get(key) || {};
      merged.set(key, {
        ...existing,
        ...report,
        customData: {
          ...(existing.customData || {}),
          ...(report?.customData || {})
        }
      });
    });
  });

  return Array.from(merged.values());
}

function initializeInaccessibleReportTray() {
  if (document.getElementById("scoreboardInaccessibleReportTray")) {
    return;
  }

  const tray = document.createElement("aside");
  tray.id = "scoreboardInaccessibleReportTray";
  tray.className = "scoreboard-inaccessible-report-tray hidden";
  tray.setAttribute("aria-live", "polite");
  tray.innerHTML = `
    <div class="scoreboard-inaccessible-report-header">
      <span>Objective Reports</span>
      <span id="scoreboardInaccessibleReportCount" class="scoreboard-inaccessible-report-count">0</span>
    </div>
    <div id="scoreboardInaccessibleReportList" class="scoreboard-inaccessible-report-list"></div>
  `;
  document.body.appendChild(tray);
}

function updateInaccessibleReportTray(game) {
  initializeInaccessibleReportTray();
  const tray = document.getElementById("scoreboardInaccessibleReportTray");
  const list = document.getElementById("scoreboardInaccessibleReportList");
  const count = document.getElementById("scoreboardInaccessibleReportCount");
  if (!tray || !list || !count) return;

  const reports = Array.isArray(game?.inaccessibleReports) ? game.inaccessibleReports.slice() : [];
  reports.sort((a, b) => new Date(b.createdAtUtc || 0) - new Date(a.createdAtUtc || 0));
  const visible = reports.slice(0, 4);
  const pendingCount = reports.filter(report => String(report?.status || "").toLowerCase() === "pending").length;

  if (visible.length === 0) {
    tray.classList.add("hidden");
    list.innerHTML = "";
    count.textContent = "0";
    return;
  }

  tray.classList.remove("hidden");
  count.textContent = pendingCount > 0 ? String(pendingCount) : String(visible.length);
  count.classList.toggle("pending", pendingCount > 0);
  list.innerHTML = visible.map(renderInaccessibleReportCard).join("");
  list.querySelectorAll("[data-report-decision]").forEach(button => {
    button.addEventListener("click", () => {
      const reportId = button.getAttribute("data-report-id") || "";
      const decision = button.getAttribute("data-report-decision") || "";
      submitInaccessibleReportDecision(reportId, decision, button);
    });
  });
}

function getInaccessibleReportKey(report) {
  return String(report?.reportId || report?.actionId || "").trim();
}

function processInaccessibleReportNotifications(game) {
  const reports = Array.isArray(game?.inaccessibleReports) ? game.inaccessibleReports.slice() : [];
  reports.sort((a, b) => new Date(a?.createdAtUtc || 0) - new Date(b?.createdAtUtc || 0));

  if (!scoreboardInaccessibleReportsPrimed) {
    reports.forEach(report => {
      const key = getInaccessibleReportKey(report);
      if (key) scoreboardInaccessibleReportStatuses.set(key, String(report?.status || "pending").toLowerCase());
    });
    scoreboardInaccessibleReportsPrimed = true;

    const pending = reports.filter(report => String(report?.status || "").toLowerCase() === "pending");
    if (pending.length > 0) {
      const latest = pending[pending.length - 1];
      const reporter = latest?.reporterHandle || "A player";
      const objective = latest?.objectiveName || "an objective";
      showScoreboardGameNotification(`${reporter} reported ${objective} inaccessible — host review needed`, "inaccessible_report_received");
    }
    return;
  }

  reports.forEach(report => {
    const key = getInaccessibleReportKey(report);
    if (!key) return;

    const status = String(report?.status || "pending").toLowerCase();
    const previousStatus = scoreboardInaccessibleReportStatuses.get(key);
    scoreboardInaccessibleReportStatuses.set(key, status);

    if (previousStatus === status) return;

    const reporter = report?.reporterHandle || "A player";
    const objective = report?.objectiveName || "an objective";
    if (!previousStatus && status === "pending") {
      showScoreboardGameNotification(`${reporter} reported ${objective} inaccessible — host review needed`, "inaccessible_report_received");
    } else if (previousStatus === "pending" && status === "accepted") {
      showScoreboardGameNotification(`${objective} relocation approved`, "inaccessible_objective_relocated");
    } else if (previousStatus === "pending" && status === "declined") {
      showScoreboardGameNotification(`${objective} relocation declined`, "inaccessible_report_rejected");
    } else if (previousStatus === "pending" && status === "rejected") {
      showScoreboardGameNotification(report?.message || `${objective} report rejected`, "inaccessible_report_rejected");
    }
  });
}

function renderInaccessibleReportCard(report) {
  const status = String(report?.status || "pending").toLowerCase();
  const reporter = report?.reporterHandle || "Player";
  const objective = report?.objectiveName || "Objective";
  const statusLabel = status === "auto_resolved"
    ? "Moved"
    : status === "cooldown_rejected"
      ? "Cooldown"
      : status === "rejected"
        ? "Rejected"
        : status === "accepted"
          ? "Accepted"
          : status === "declined"
            ? "Declined"
            : "Pending";
  const detail = report?.message || report?.resolution || `${objective} was reported inaccessible.`;

  return `
    <article class="scoreboard-inaccessible-report-card status-${escapeHtml(status)}">
      <div class="scoreboard-inaccessible-report-title">
        <strong>${escapeHtml(reporter)}</strong>
        <span>${escapeHtml(statusLabel)}</span>
      </div>
      <div class="scoreboard-inaccessible-report-objective">${escapeHtml(objective)}</div>
      <div class="scoreboard-inaccessible-report-message">${escapeHtml(detail)}</div>
      ${status === "pending" && report?.requiresHostDecision ? `
        <div class="scoreboard-inaccessible-report-actions">
          <button type="button" class="scoreboard-report-accept" data-report-id="${escapeHtml(report.reportId || "")}" data-report-decision="accept">Relocate</button>
          <button type="button" class="scoreboard-report-decline" data-report-id="${escapeHtml(report.reportId || "")}" data-report-decision="decline">Decline</button>
        </div>
      ` : ""}
    </article>
  `;
}

async function submitInaccessibleReportDecision(reportId, decision, button) {
  if (!reportId || !decision || button?.disabled) return;
  const originalText = button.textContent;
  const card = button.closest(".scoreboard-inaccessible-report-card");
  card?.querySelectorAll("button").forEach(candidate => { candidate.disabled = true; });
  button.textContent = decision === "accept" ? "Moving..." : "Declining...";

  try {
    const response = await fetch(`/api/games/${encodeURIComponent(GAME_ID)}/inaccessible-reports/${encodeURIComponent(reportId)}/decision`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ decision })
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || payload.error || "Could not process report.");
    await loadScoreboard();
  } catch (error) {
    card?.querySelectorAll("button").forEach(candidate => { candidate.disabled = false; });
    button.textContent = originalText;
    const message = card?.querySelector(".scoreboard-inaccessible-report-message");
    if (message) message.textContent = error.message || "Could not process report.";
  }
}

function initializeScoreboardNotifications() {
  if (document.getElementById("scoreboardNotificationLayer")) {
    return;
  }

  const layer = document.createElement("div");
  layer.id = "scoreboardNotificationLayer";
  layer.className = "scoreboard-notification-layer";
  layer.setAttribute("aria-live", "polite");
  layer.setAttribute("aria-atomic", "false");

  document.body.appendChild(layer);
}

function processScoreboardEventNotifications(game) {
  const events = Array.isArray(game.events) ? game.events : [];
  const orderedEvents = events
    .slice()
    .sort((a, b) => new Date(a.createdAtUtc || 0) - new Date(b.createdAtUtc || 0));

  if (!scoreboardNotificationsPrimed) {
    orderedEvents.forEach(event => scoreboardKnownEventIds.add(getScoreboardEventKey(event)));
    scoreboardNotificationsPrimed = true;
    return;
  }

  orderedEvents.forEach(event => {
    const eventKey = getScoreboardEventKey(event);

    if (scoreboardKnownEventIds.has(eventKey)) {
      return;
    }

    scoreboardKnownEventIds.add(eventKey);

    const message = formatScoreboardGameNotification(event, game);

    if (message && shouldShowScoreboardGameNotification(event, game)) {
      showScoreboardGameNotification(message, event.type || "");
    }
  });
}

function getScoreboardEventKey(event) {
  return event.eventId ||
    `${event.type || "event"}|${event.createdAtUtc || ""}|${event.playerId || ""}|${event.message || ""}`;
}

function shouldShowScoreboardGameNotification(event, game) {
  const type = (event?.type || "").toLowerCase();

  if (window.QRTagScoreboardReportsCompanion &&
      (type.includes("inaccessible_report") ||
       type === "inaccessible_objective_relocated" ||
       type === "ghost_inaccessible_retargeted")) {
    return false;
  }

  if (type !== "achievement_unlocked") {
    return true;
  }

  const achievementKey = getScoreboardAchievementNotificationKey(event, game);

  if (!achievementKey) {
    return true;
  }

  if (scoreboardAnnouncedAchievementKeys.has(achievementKey)) {
    return false;
  }

  scoreboardAnnouncedAchievementKeys.add(achievementKey);
  saveScoreboardAnnouncedAchievementKeys();
  return true;
}

function getScoreboardAchievementNotificationKey(event, game) {
  const customData = event?.customData || {};
  const rawPlayerKey = event?.playerId ||
    customData.playerId ||
    event?.playerHandle ||
    customData.playerHandle ||
    extractPlayerNameFromAchievementMessage(event?.message) ||
    "";
  const rawAchievementKey = customData.achievementId ||
    customData.achievementTitle ||
    customData.title ||
    customData.achievementName ||
    extractAchievementNameFromMessage(event?.message) ||
    event?.message ||
    "";

  const playerKey = normalizeScoreboardNotificationKeyPart(rawPlayerKey);
  const achievementKey = normalizeScoreboardNotificationKeyPart(rawAchievementKey);

  if (!playerKey || !achievementKey) {
    return "";
  }

  return [
    normalizeScoreboardNotificationKeyPart(game?.serverId || "local"),
    playerKey,
    achievementKey
  ].join("|");
}

function normalizeScoreboardNotificationKeyPart(value) {
  return String(value || "")
    .trim()
    .toLowerCase()
    .replace(/\s+/g, "_");
}

function loadScoreboardAnnouncedAchievementKeys() {
  try {
    const raw = window.localStorage.getItem(SCOREBOARD_ACHIEVEMENT_NOTIFICATION_STORAGE_KEY);
    const values = raw ? JSON.parse(raw) : [];

    if (!Array.isArray(values)) {
      return new Set();
    }

    return new Set(values.filter(value => typeof value === "string" && value.trim()));
  } catch {
    return new Set();
  }
}

function saveScoreboardAnnouncedAchievementKeys() {
  try {
    const values = Array.from(scoreboardAnnouncedAchievementKeys).slice(-500);
    scoreboardAnnouncedAchievementKeys = new Set(values);
    window.localStorage.setItem(SCOREBOARD_ACHIEVEMENT_NOTIFICATION_STORAGE_KEY, JSON.stringify(values));
  } catch {
    // localStorage can be unavailable in locked-down browser modes; notification
    // dedupe is best-effort only.
  }
}

function formatScoreboardGameNotification(event, game) {
  const type = (event.type || "").toLowerCase();
  const customData = event.customData || {};

  if (type === "player_eliminated") {
    const victim = event.playerHandle || customData.targetHandle || "Player";
    const killer = customData.taggedByHandle || customData.scannerHandle || "another player";

    return `${victim} killed by ${killer}`;
  }

  if (type === "player_tagged") {
    // Team Elimination also creates a player_eliminated event for the same tag.
    // Prefer the cleaner "killed by" popup from player_eliminated in that mode.
    if (getScoreboardModeKey(game) === "team_elimination") {
      return "";
    }

    const killer = event.playerHandle || customData.scannerHandle || "Player";
    const victim = customData.targetHandle || event.objectiveName || "another player";

    return `${victim} killed by ${killer}`;
  }

  if (type === "objective_action" || type === "objective_captured" || type === "objective_claimed") {
    const pointsAwarded = Number(event.pointsAwarded || 0);

    // Ignore "already belongs to team" style messages.
    if (pointsAwarded <= 0 && type === "objective_action") {
      return "";
    }

    const player = event.playerHandle || "Player";
    const objective = event.objectiveName || customData.objectiveName || "Objective";

    return `${player} has captured ${objective} Objective`;
  }

  if (type === "inaccessible_report_received") {
    // Phase 45D.1.3: the dedicated report poller owns pending-review popups so
    // the scoreboard does not announce the same report twice.
    return "";
  }

  if (type === "ghost_inaccessible_retargeted") {
    const objective = customData.ghostDisplayName || event.objectiveName || "Ghost";
    return `${objective} moved to a new haunt`;
  }

  if (type === "inaccessible_report_rejected") {
    return event.message || "Objective report rejected";
  }

  if (type === "achievement_unlocked") {
    const player = event.playerHandle || customData.playerHandle || extractPlayerNameFromAchievementMessage(event.message) || "Player";
    const achievement = customData.achievementTitle ||
      customData.title ||
      customData.achievementName ||
      extractAchievementNameFromMessage(event.message) ||
      customData.achievementId ||
      "Achievement";

    return `${player} unlocked ${achievement}`;
  }

  return "";
}

function extractPlayerNameFromAchievementMessage(message) {
  if (!message) {
    return "";
  }

  const match = String(message).match(/^(.+?)\s+unlocked achievement:/i);
  return match ? match[1].trim() : "";
}

function extractAchievementNameFromMessage(message) {
  if (!message) {
    return "";
  }

  const match = String(message).match(/unlocked achievement:\s*(.+)$/i);
  return match ? match[1].trim() : "";
}

function showScoreboardGameNotification(message, type = "") {
  initializeScoreboardNotifications();

  const layer = document.getElementById("scoreboardNotificationLayer");

  if (!layer) {
    return;
  }

  const notification = document.createElement("div");
  notification.className = `scoreboard-game-notification ${getScoreboardNotificationClass(type)}`;
  notification.innerHTML = `
    <div class="scoreboard-game-notification-kicker">Game Event</div>
    <div class="scoreboard-game-notification-message">${escapeHtml(message)}</div>
  `;

  layer.appendChild(notification);

  window.setTimeout(() => {
    notification.classList.add("leaving");

    window.setTimeout(() => {
      notification.remove();
    }, 450);
  }, 10000);
}

function getScoreboardNotificationClass(type) {
  const safeType = (type || "").toLowerCase();

  if (safeType.includes("achievement")) {
    return "achievement";
  }

  if (safeType.includes("objective") || safeType.includes("inaccessible") || safeType.includes("retargeted")) {
    return "objective";
  }

  if (safeType.includes("tag") || safeType.includes("eliminated")) {
    return "combat";
  }

  return "";
}


function initializeScoreboardControls() {
  if (document.getElementById("scoreboardControlsButton")) {
    return;
  }

  const controls = document.createElement("div");
  controls.className = "scoreboard-controls-layer";
  controls.innerHTML = `
    <button id="scoreboardControlsButton" class="scoreboard-controls-gear" type="button" title="Game controls" aria-label="Open game controls">⚙</button>

    <div id="scoreboardControlsModal" class="scoreboard-controls-modal hidden" role="dialog" aria-modal="true" aria-labelledby="scoreboardControlsTitle">
      <div class="scoreboard-controls-backdrop" data-close-controls="true"></div>
      <section class="scoreboard-controls-panel">
        <div class="scoreboard-controls-header">
          <div>
            <div id="scoreboardControlsTitle" class="scoreboard-controls-title">Game Controls</div>
            <div class="scoreboard-controls-subtitle">Basic host controls for the active game.</div>
          </div>
          <button id="scoreboardControlsClose" class="scoreboard-controls-close" type="button" aria-label="Close game controls">×</button>
        </div>

        <div id="scoreboardControlsMessage" class="scoreboard-controls-message hidden"></div>

        <div class="scoreboard-controls-actions">
          <button id="scoreboardPauseGameButton" class="scoreboard-control-button pause" type="button">Pause Game</button>
          <button id="scoreboardRematchGameButton" class="scoreboard-control-button rematch" type="button">Rematch / Return to Lobby</button>
          <button id="scoreboardStopGameButton" class="scoreboard-control-button stop" type="button">Stop Game & Close Server</button>
        </div>
      </section>
    </div>
  `;

  document.body.appendChild(controls);

  document.getElementById("scoreboardControlsButton")?.addEventListener("click", openScoreboardControls);
  document.getElementById("scoreboardControlsClose")?.addEventListener("click", closeScoreboardControls);
  document.querySelector("[data-close-controls='true']")?.addEventListener("click", closeScoreboardControls);
  document.getElementById("scoreboardPauseGameButton")?.addEventListener("click", pauseGameFromScoreboard);
  document.getElementById("scoreboardRematchGameButton")?.addEventListener("click", rematchGameFromScoreboard);
  document.getElementById("scoreboardStopGameButton")?.addEventListener("click", stopGameFromScoreboard);

  document.addEventListener("keydown", event => {
    if (event.key === "Escape") {
      closeScoreboardControls();
    }
  });
}

function openScoreboardControls() {
  document.getElementById("scoreboardControlsModal")?.classList.remove("hidden");
  setScoreboardControlsMessage("");
}

function closeScoreboardControls() {
  document.getElementById("scoreboardControlsModal")?.classList.add("hidden");
}

function setScoreboardControlsMessage(message, type = "") {
  const el = document.getElementById("scoreboardControlsMessage");
  if (!el) return;

  if (!message) {
    el.textContent = "";
    el.className = "scoreboard-controls-message hidden";
    return;
  }

  el.textContent = message;
  el.className = `scoreboard-controls-message ${type}`;
}

function updateScoreboardControlsState(game) {
  const button = document.getElementById("scoreboardPauseGameButton");
  const rematchButton = document.getElementById("scoreboardRematchGameButton");
  const status = String(game?.status || "").toLowerCase();
  const paused = isGamePaused(game);
  const canPause = status === "active" || status === "paused";
  const canRematch = ["active", "paused", "finished", "ended", "complete", "completed", "ready"].includes(status);

  if (button) {
    button.textContent = paused ? "Resume Game" : "Pause Game";
    button.dataset.controlAction = paused ? "resume" : "pause";
    button.classList.toggle("resume", paused);
    button.classList.toggle("pause", !paused);
    button.disabled = !canPause;
  }

  if (rematchButton) {
    rematchButton.disabled = !canRematch;
    rematchButton.title = canRematch
      ? "Keep joined players and return everyone to the Ready lobby."
      : "Rematch is available once a lobby or game is running.";
  }
}

async function postScoreboardGameControl(action) {
  const response = await fetch(`/api/games/${encodeURIComponent(GAME_ID)}/${action}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    }
  });

  const result = await response.json().catch(() => ({}));

  if (!response.ok) {
    throw new Error(result.error || result.message || `Could not ${action} game.`);
  }

  return result;
}

async function pauseGameFromScoreboard() {
  const button = document.getElementById("scoreboardPauseGameButton");
  const action = button?.dataset?.controlAction === "resume" ? "resume" : "pause";

  try {
    if (button) button.disabled = true;

    const result = await postScoreboardGameControl(action);
    setScoreboardControlsMessage(result.message || (action === "resume" ? "Game resumed." : "Game paused."), "success");
    await loadScoreboard();
  } catch (error) {
    setScoreboardControlsMessage(error.message, "error");
  } finally {
    if (button) button.disabled = false;
  }
}

function showScoreboardConfirm({ title, message, confirmText = "Confirm", cancelText = "Cancel", danger = false } = {}) {
  return new Promise(resolve => {
    const existing = document.getElementById("scoreboardConfirmModal");
    existing?.remove();

    const modal = document.createElement("div");
    modal.id = "scoreboardConfirmModal";
    modal.className = "scoreboard-confirm-modal";
    modal.innerHTML = `
      <div class="scoreboard-confirm-backdrop" data-scoreboard-confirm-cancel="true"></div>
      <section class="scoreboard-confirm-panel" role="dialog" aria-modal="true" aria-labelledby="scoreboardConfirmTitle">
        <div class="scoreboard-confirm-kicker">Host Control</div>
        <h2 id="scoreboardConfirmTitle">${escapeHtml(title || "Are you sure?")}</h2>
        <p>${escapeHtml(message || "Confirm this action.")}</p>
        <div class="scoreboard-confirm-actions">
          <button class="scoreboard-control-button secondary" type="button" data-scoreboard-confirm-cancel="true">${escapeHtml(cancelText)}</button>
          <button class="scoreboard-control-button ${danger ? "stop" : "rematch"}" type="button" data-scoreboard-confirm-ok="true">${escapeHtml(confirmText)}</button>
        </div>
      </section>
    `;

    const finish = value => {
      modal.remove();
      document.removeEventListener("keydown", onKeyDown);
      resolve(value);
    };

    const onKeyDown = event => {
      if (event.key === "Escape") {
        finish(false);
      }
    };

    modal.addEventListener("click", event => {
      if (event.target?.closest?.("[data-scoreboard-confirm-ok]")) {
        finish(true);
        return;
      }

      if (event.target?.closest?.("[data-scoreboard-confirm-cancel]")) {
        finish(false);
      }
    });

    document.addEventListener("keydown", onKeyDown);
    document.body.appendChild(modal);
    modal.querySelector("[data-scoreboard-confirm-ok]")?.focus();
  });
}

async function rematchGameFromScoreboard() {
  const confirmed = await showScoreboardConfirm({
    title: "Start a rematch lobby?",
    message: "Joined players stay connected. The current round is cleared and everyone returns to the Ready lobby.",
    confirmText: "Rematch Lobby",
    cancelText: "Keep Scoreboard"
  });

  if (!confirmed) {
    return;
  }

  const button = document.getElementById("scoreboardRematchGameButton");

  try {
    if (button) button.disabled = true;

    const result = await postScoreboardGameControl("rematch");
    setScoreboardControlsMessage(result.message || "Players are ready for a rematch.", "success");
    scoreboardRedirectingToAdmin = true;
    window.location.href = "/admin";
  } catch (error) {
    setScoreboardControlsMessage(error.message, "error");
    if (button) button.disabled = false;
  }
}

async function stopGameFromScoreboard() {
  const confirmed = await showScoreboardConfirm({
    title: "Close this server?",
    message: "This stops the game, removes joined players, and sends clients back home. Use Rematch if you want players to stay connected.",
    confirmText: "Close Server",
    cancelText: "Keep Game",
    danger: true
  });

  if (!confirmed) {
    return;
  }

  const button = document.getElementById("scoreboardStopGameButton");

  try {
    if (button) button.disabled = true;

    await postScoreboardGameControl("finish");
    window.location.href = "/admin";
  } catch (error) {
    setScoreboardControlsMessage(error.message, "error");
    if (button) button.disabled = false;
  }
}


async function ensureScoreboardServerConfig() {
  if (scoreboardServerConfig) {
    return;
  }

  try {
    const configResponse = await fetch("/api/admin/server-config");
    const config = await configResponse.json();

    if (configResponse.ok) {
      scoreboardServerConfig = config;
    }
  } catch {
    // Keep fallback names.
  }
}

function getServerDisplayName(game) {
  return (
    game.serverName ||
    game.localServerName ||
    scoreboardServerConfig?.serverName ||
    scoreboardServerConfig?.name ||
    game.name ||
    "Oops, You're Dead"
  );
}

function handleScoreboardRouteForGameStatus(game) {
  if (scoreboardRedirectingToAdmin) {
    return;
  }

  const status = String(game?.status || "").toLowerCase();
  const playerCount = Array.isArray(game?.players) ? game.players.length : Number(game?.playerCount || 0);

  if (["active", "started", "running", "inprogress", "playing", "paused", "finished", "ended", "complete", "completed"].includes(status)) {
    scoreboardSawRunningRound = true;
    return;
  }

  if (status === "ready" && (scoreboardSawRunningRound || playerCount > 0)) {
    scoreboardRedirectingToAdmin = true;
    window.location.href = "/admin";
  }
}

function canonicalGameModeKey(value) {
  const raw = String(value ?? "").trim();
  if (!raw) {
    return "";
  }

  const compact = raw.toLowerCase().replace(/[^a-z0-9]/g, "");

  if (compact.includes("zombietag")) return "zombie_tag";
  if (compact.includes("zombieextraction")) return "zombie_extraction";
  if (compact.includes("ghosthunt")) return "ghost_hunt";
  if (compact.includes("capturetheflag") || compact === "ctf") return "capture_the_flag";
  if (compact.includes("teamelimination")) return "team_elimination";
  if (compact.includes("objectivecapture")) return "objective_capture";
  if (compact.includes("hotspotrush")) return "hot_spot_rush";

  return raw.toLowerCase().replace(/[\s-]+/g, "_");
}

function getScoreboardModeKey(game) {
  const candidates = [
    game?.gameMode,
    game?.gameModeId,
    game?.modeId,
    game?.liveMap?.gameMode,
    game?.liveMap?.gameModeId,
    game?.customFields?.gameMode,
    game?.customFields?.modeId,
    game?.rulesFile
  ];

  const knownModes = new Set([
    "zombie_tag",
    "zombie_extraction",
    "ghost_hunt",
    "capture_the_flag",
    "team_elimination",
    "objective_capture",
    "hot_spot_rush"
  ]);

  for (const candidate of candidates) {
    const key = canonicalGameModeKey(candidate);
    if (knownModes.has(key)) {
      return key;
    }
  }

  // Compatibility guard for older or Android-originated game documents where
  // the mode label was stored in a non-canonical field or spelling.
  const customFields = game?.customFields || {};
  const liveZombies = game?.liveMap?.zombies ?? game?.liveMap?.Zombies;
  const hasLiveZombies = Array.isArray(liveZombies)
    ? liveZombies.length > 0
    : Boolean(liveZombies);

  if (
    customFields.zombieExtraction ||
    customFields.zombieExtractionRoundId ||
    customFields.zombieExtractionFoundationOnly ||
    hasLiveZombies
  ) {
    return "zombie_extraction";
  }

  const objectives = [
    ...(Array.isArray(game?.objectives) ? game.objectives : []),
    ...(Array.isArray(game?.liveMap?.objectives) ? game.liveMap.objectives : [])
  ];

  if (objectives.some(objective => {
    const type = String(objective?.objectiveType || objective?.ObjectiveType || "").toLowerCase();
    const role = String(getCustomValue(objective, "zombieExtraction.role") || "").toLowerCase();
    return type === "zombie_beacon_part" || type === "zombie_supply_crate" || type === "zombie" ||
      role === "beacon_part" || role === "supply_crate" || role === "zombie";
  })) {
    return "zombie_extraction";
  }

  return canonicalGameModeKey(candidates.find(candidate => String(candidate ?? "").trim()) || "");
}

function isGamePaused(game) {
  return String(game?.status || "").toLowerCase() === "paused";
}

function getPausedRemainingSeconds(game) {
  const raw = game?.pausedRemainingSeconds ?? game?.customFields?.pausedRemainingSeconds ?? game?.customFields?.["pausedRemainingSeconds"];
  const value = Number(raw);
  return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
}

function formatClockSeconds(totalSeconds) {
  const safeSeconds = Math.max(0, Math.floor(Number(totalSeconds) || 0));
  const minutes = Math.floor(safeSeconds / 60);
  const seconds = safeSeconds % 60;
  return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
}

function renderPausedNotice(game) {
  return isGamePaused(game)
    ? `<div class="round-pause-banner" role="status" aria-live="polite"><strong>PAUSED</strong><span>Don't move until the host unpauses.</span></div>`
    : `<div class="round-pause-banner hidden" role="status" aria-live="polite"><strong>PAUSED</strong><span>Don't move until the host unpauses.</span></div>`;
}

function renderScoreboard(game) {
  const content = document.getElementById("scoreboardContent");
  if (!content) return;

  const mode = getScoreboardModeKey(game);
  document.body.dataset.scoreboardMode = mode || "default";
  document.documentElement.dataset.scoreboardMode = mode || "default";
  const status = game.status || "";
  const zoneKey = getZoneKey(game);
  const playerKey = getPlayerKey(game);
  const objectiveKey = getObjectiveKey(game);
  const ghostKey = getGhostKey(game);
  const shouldRebuildLayout =
    !scoreboardLayoutRendered ||
    mode !== scoreboardLastMode ||
    status !== scoreboardLastStatus ||
    zoneKey !== scoreboardLastZoneKey ||
    playerKey !== scoreboardLastPlayerKey ||
    objectiveKey !== scoreboardLastObjectiveKey ||
    ghostKey !== scoreboardLastGhostKey;

  scoreboardLastMode = mode;
  scoreboardLastStatus = status;
  scoreboardLastZoneKey = zoneKey;
  scoreboardLastPlayerKey = playerKey;
  scoreboardLastObjectiveKey = objectiveKey;
  scoreboardLastGhostKey = ghostKey;

  if (mode === "zombie_extraction") {
    if (shouldRebuildLayout) {
      content.innerHTML = renderZombieExtractionScoreboard(game);
      scoreboardLayoutRendered = true;
    } else {
      updateZombieExtractionScoreboardText(game);
    }

    requestAnimationFrame(() => renderLeafletPlayZoneMap(game, shouldRebuildLayout));
    return;
  }

  if (mode === "ghost_hunt") {
    if (shouldRebuildLayout) {
      content.innerHTML = renderGhostHuntScoreboard(game);
      scoreboardLayoutRendered = true;
    } else {
      updateGhostHuntScoreboardText(game);
    }

    requestAnimationFrame(() => renderLeafletPlayZoneMap(game, shouldRebuildLayout));
    return;
  }

  if (mode === "zombie_tag" || mode === "team_elimination" || mode === "capture_the_flag") {
    if (shouldRebuildLayout) {
      content.innerHTML = renderTeamElimination(game);
      scoreboardLayoutRendered = true;
    } else {
      updateTeamEliminationText(game);
    }

    requestAnimationFrame(() => renderLeafletPlayZoneMap(game, shouldRebuildLayout));
    return;
  }

  content.innerHTML = renderGenericScoreboard(game);
  scoreboardLayoutRendered = true;
}

function updateTeamEliminationText(game) {
  const teams = normalizeTeams(game);
  const isZombieTag = getScoreboardModeKey(game) === "zombie_tag";
  const red = teams[0] || emptyTeam(isZombieTag ? "survivors" : "red", isZombieTag ? "Survivors" : "Red Team");
  const blue = teams[1] || emptyTeam(isZombieTag ? "zombies" : "blue", isZombieTag ? "Zombies" : "Blue Team");
  const winningTeamIds = getWinningTeamIds(game);

  const timer = document.querySelector(".round-timer-box");
  if (timer) timer.textContent = getClockText(game);

  const pauseBanner = document.querySelector(".round-pause-banner");
  if (pauseBanner) {
    pauseBanner.classList.toggle("hidden", !isGamePaused(game));
  }

  updateScoreBox("red", red, isZombieTag ? "Survivors" : "Red", winningTeamIds.includes(red.teamId || ""), game);
  updateScoreBox("blue", blue, isZombieTag ? "Zombies" : "Blue", winningTeamIds.includes(blue.teamId || ""), game);
  updateRosterPanel("red", red, game);
  updateRosterPanel("blue", blue, game);
}

function getWinningTeamIds(game) {
  const status = String(game?.status || "").toLowerCase();
  const isFinished =
    status === "finished" ||
    status === "ended" ||
    status === "complete" ||
    status === "completed";

  if (!isFinished) {
    return [];
  }

  const mode = getScoreboardModeKey(game);
  const explicitWinner = (
    getCustomValue(game, "winnerTeamId") ||
    game?.winnerTeamId ||
    getCustomValue(game, "winningTeamId") ||
    game?.winningTeamId ||
    ""
  ).toString().trim().toLowerCase();

  if (explicitWinner) {
    return [explicitWinner];
  }

  if (mode === "zombie_tag") {
    const result = (getCustomValue(game, "zombieTagResult") || game?.zombieTagResult || "").toString().toLowerCase();
    if (["zombies_win", "zombie_win", "zombies", "infected", "horde"].some(token => result.includes(token))) {
      return ["zombies"];
    }
    if (["survivors_win", "survivor_win", "survivors", "survived", "time_up"].some(token => result.includes(token))) {
      return ["survivors"];
    }
  }

  const teams = normalizeTeams(game)
    .map(team => ({
      teamId: String(team.teamId || "").toLowerCase(),
      alive: team.players.filter(player => isAlive(player)).length,
      score: Number(team.score || 0)
    }));

  if (teams.length === 0) {
    return [];
  }

  const maxAlive = Math.max(...teams.map(team => team.alive));
  const aliveLeaders = teams.filter(team => team.alive === maxAlive);

  if (maxAlive > 0 && aliveLeaders.length === 1) {
    return [aliveLeaders[0].teamId];
  }

  const maxScore = Math.max(...teams.map(team => team.score));
  const scoreLeaders = teams.filter(team => team.score === maxScore);

  if (scoreLeaders.length === 1) {
    return [scoreLeaders[0].teamId];
  }

  return [];
}

function updateScoreBox(teamId, team, fallbackName, isWinner = false, game = null) {
  const panel = document.querySelector(`.score-box-${teamId}`);
  if (!panel) return;

  panel.classList.toggle("score-box-winner", !!isWinner);
  panel.innerHTML = renderScoreBox(team, fallbackName, game);
}

function updateRosterPanel(teamId, team, game = null) {
  const title = document.querySelector(`.team-roster-${teamId} .team-roster-title`);
  if (title) title.textContent = toTitleCase(team.name || team.teamId || "Team");

  const grid = document.querySelector(`.team-roster-${teamId} .team-roster-grid`);
  if (!grid) return;

  const shouldTwoColumn = team.players.length > 6;
  grid.classList.toggle("two-columns", shouldTwoColumn);

  grid.innerHTML = team.players.length === 0
    ? `<div class="team-roster-empty">No players joined.</div>`
    : team.players.map(player => renderRosterPlayerCard(player, game)).join("");
}

function getMapZones(game) {
  const liveZones = game?.liveMap?.zones ?? game?.liveMap?.Zones;
  return Array.isArray(liveZones) ? liveZones : (game?.zones || game?.Zones || []);
}

function getMapPlayers(game) {
  const livePlayers = game?.liveMap?.players ?? game?.liveMap?.Players;
  return Array.isArray(livePlayers) ? livePlayers : (game?.players || game?.Players || []);
}

function getMapObjectives(game) {
  const liveObjectives = game?.liveMap?.objectives ?? game?.liveMap?.Objectives;
  return Array.isArray(liveObjectives) ? liveObjectives : (game?.objectives || game?.Objectives || []);
}

function getMapZombies(game) {
  const liveZombies = game?.liveMap?.zombies ?? game?.liveMap?.Zombies;
  return Array.isArray(liveZombies) ? liveZombies : [];
}

function getMapPlayerLocation(player) {
  return player?.location ?? player?.Location ?? null;
}

function getMapBoundaryPoints(zone) {
  const points = zone?.boundaryPoints ?? zone?.BoundaryPoints;
  return Array.isArray(points) ? points : [];
}

function getZoneKey(game) {
  const zone = getMapZones(game)[0];
  const live = getZombieTagLiveState(game);
  return JSON.stringify({
    radius: Number(zone?.radiusMeters ?? zone?.RadiusMeters ?? live?.extractionRadiusMeters ?? live?.ExtractionRadiusMeters ?? 0).toFixed(2),
    percent: getZombieTagArenaPercent(game).toFixed(2),
    points: getMapBoundaryPoints(zone).map(point => [
      Number(point.latitude ?? point.Latitude).toFixed(6),
      Number(point.longitude ?? point.Longitude).toFixed(6)
    ])
  });
}

function getPlayerKey(game) {
  // Phase 44B.5 / 44F.8: keep this as a layout key only.
  // Live GPS coordinates/timestamps must not force a full scoreboard DOM/map
  // rebuild, or the Leaflet map flashes every location refresh. Marker movement
  // is handled below by renderLeafletPlayZoneMap without recreating the map.
  // Phase 44F.8 also bumps scoreboard.html cache-busters so browsers cannot
  // keep an older scoreboard.js that still rebuilt the map on every GPS tick.
  return JSON.stringify(getMapPlayers(game).map(player => [
    player.playerId || player.PlayerId || "",
    player.handle || player.Handle || "",
    player.teamId || player.TeamId || "",
    (player.isAlive ?? player.IsAlive) === false ? "dead" : "alive",
    String(getCustomValue(player, "zombieTag.role") || getCustomValue(player, "zombieExtraction.role") || "")
  ]));
}
function getObjectiveKey(game) {
  // Phase 44B.5 / 44F.8: keep moving objective coordinates out of the layout key.
  // Moving ghosts/zombies/flags should move markers, not recreate the Leaflet map.
  return JSON.stringify(getMapObjectives(game).map(objective => [
    objective.objectiveId || objective.ObjectiveId || "",
    objective.objectiveType || objective.ObjectiveType || "",
    getCustomValue(objective, "zombieTag.role") || getCustomValue(objective, "zombieExtraction.role") || "",
    (objective.active ?? objective.Active) === false ? "inactive" : "active",
    getObjectiveCustom(objective, "ctf.carrierPlayerId") || ""
  ]));
}


function getGhostKey(game) {
  return JSON.stringify(getGhostObjectives(game).map(ghost => [
    ghost.objectiveId || "",
    getGhostType(ghost),
    getGhostState(ghost),
    ghost.active === false ? "inactive" : "active"
  ]));
}

function getCustomValue(owner, key) {
  if (!owner || !key) {
    return undefined;
  }

  const candidates = [owner.customFields, owner.customData];
  const lower = key.toLowerCase();

  for (const bag of candidates) {
    if (!bag || typeof bag !== "object") {
      continue;
    }

    if (Object.prototype.hasOwnProperty.call(bag, key)) {
      return bag[key];
    }

    const matchingKey = Object.keys(bag).find(existing => existing.toLowerCase() === lower);
    if (matchingKey) {
      return bag[matchingKey];
    }
  }

  return undefined;
}

function getNumberCustomValue(owner, key, fallback = 0) {
  const value = Number(getCustomValue(owner, key));
  return Number.isFinite(value) ? value : fallback;
}

function getGhostObjectives(game) {
  return (game.objectives || []).filter(objective => {
    const type = String(objective.objectiveType || "").toLowerCase();
    const role = String(getCustomValue(objective, "ghost.role") || "").toLowerCase();
    return type === "ghost" || role === "ghost";
  });
}

function getActiveGhostObjectives(game) {
  return getGhostObjectives(game).filter(ghost => !isGhostCaptured(ghost));
}

function isGhostCaptured(ghost) {
  return ghost?.active === false || getGhostState(ghost) === "captured";
}

function getGhostState(ghost) {
  return String(getCustomValue(ghost, "ghost.state") || "wandering").toLowerCase();
}

function getGhostType(ghost) {
  return String(getCustomValue(ghost, "ghost.type") || "normal").toLowerCase();
}

function getGhostDisplayName(ghostOrType) {
  if (typeof ghostOrType === "string") {
    switch (ghostOrType.toLowerCase()) {
      case "shy":
        return "Whisper";
      case "aggressive":
        return "Wraith";
      case "trickster":
        return "Flicker";
      case "normal":
      default:
        return "Drifter";
    }
  }

  return String(getCustomValue(ghostOrType, "ghost.displayName") || getGhostDisplayName(getGhostType(ghostOrType)));
}

function getGhostStats(game) {
  const ghosts = getGhostObjectives(game);
  const captured = ghosts.filter(isGhostCaptured).length;
  const configuredTotal = getNumberCustomValue(game, "ghostHuntTotalGhosts", ghosts.length);
  const total = Math.max(configuredTotal, ghosts.length);

  return {
    ghosts,
    active: ghosts.filter(ghost => !isGhostCaptured(ghost)),
    captured,
    total,
    remaining: Math.max(0, total - captured)
  };
}

function getHunterStats(game) {
  const players = game.players || [];
  const alive = players.filter(player => isAlive(player)).length;
  return {
    players,
    alive,
    dead: Math.max(0, players.length - alive),
    total: players.length
  };
}

function getObjectiveCustom(objective, key) {
  const customData = objective?.customData || {};
  return customData[key] || customData[key.toLowerCase()] || "";
}

function getObjectiveTeamId(objective) {
  return getObjectiveCustom(objective, "ctf.flagTeamId") ||
    getObjectiveCustom(objective, "ctf.baseTeamId") ||
    objective.currentOwnerTeamId ||
    "";
}

function getObjectiveTeamName(objective) {
  return getObjectiveCustom(objective, "ctf.flagTeamName") ||
    getObjectiveCustom(objective, "ctf.baseTeamName") ||
    getObjectiveTeamId(objective) ||
    "Objective";
}


function renderGhostHuntStyleScoreboardShell(game, options) {
  const {
    boardClasses,
    leftScoreClasses,
    leftScoreContent,
    rightScoreClasses,
    rightScoreContent,
    rightScoreHidden = false,
    leftRosterClasses,
    leftRosterContent,
    leftRosterTwoColumns = false,
    mapClasses,
    mapPrelude = "",
    rightRosterClasses,
    rightRosterContent,
    rightRosterGridClasses = "",
    rightRosterHidden = false
  } = options;

  return `
    <section class="team-elimination-board ghost-hunt-board ${escapeHtml(boardClasses || "")}">
      <section class="score-box-card score-box-red ${escapeHtml(leftScoreClasses || "")}">
        ${leftScoreContent}
      </section>

      <section class="round-status-shell">
        <div class="round-timer-box">${escapeHtml(getClockText(game))}</div>
        ${renderPausedNotice(game)}
        <div class="round-status-card">
          <div class="round-status-primary">${escapeHtml(getServerDisplayName(game))}</div>
          <div class="round-status-secondary">${escapeHtml(modeDisplayName(getScoreboardModeKey(game)))}</div>
        </div>
      </section>

      <section class="score-box-card score-box-blue ${escapeHtml(rightScoreClasses || "")}"${rightScoreHidden ? ' aria-hidden="true"' : ""}>
        ${rightScoreContent || ""}
      </section>

      <section class="team-roster-card team-roster-red ${escapeHtml(leftRosterClasses || "")}">
        <div class="team-roster-grid ${leftRosterTwoColumns ? "two-columns" : ""}">
          ${leftRosterContent}
        </div>
      </section>

      <section class="scoreboard-map-card scoreboard-map-center ${escapeHtml(mapClasses || "")}">
        ${mapPrelude || ""}
        ${renderPlayZoneMap(game)}
      </section>

      <section class="team-roster-card team-roster-blue ${escapeHtml(rightRosterClasses || "")}"${rightRosterHidden ? ' aria-hidden="true"' : ""}>
        <div class="team-roster-grid ${escapeHtml(rightRosterGridClasses || "")}">
          ${rightRosterContent || ""}
        </div>
      </section>

      <section class="scoreboard-event-strip">
        <div class="scoreboard-event-strip-label">Recent Events</div>
        <div class="scoreboard-event-strip-content">
          ${renderRecentEventsInline(game)}
        </div>
      </section>
    </section>
  `;
}

function renderGhostHuntScoreboard(game) {
  const hunterStats = getHunterStats(game);
  const ghostStats = getGhostStats(game);

  return renderGhostHuntStyleScoreboardShell(game, {
    boardClasses: "",
    leftScoreClasses: "ghost-hunt-hunters-card",
    leftScoreContent: renderGhostHuntHuntersScoreBox(hunterStats),
    rightScoreClasses: "ghost-hunt-ghosts-card",
    rightScoreContent: renderGhostHuntGhostsScoreBox(ghostStats),
    leftRosterClasses: "ghost-hunt-hunters-list",
    leftRosterContent: renderGhostHuntHunterCards(game),
    leftRosterTwoColumns: hunterStats.players.length > 6,
    mapClasses: "ghost-hunt-map-card",
    rightRosterClasses: "ghost-journal-card",
    rightRosterContent: renderGhostJournalCards(game),
    rightRosterGridClasses: "ghost-journal-grid"
  });
}

function updateGhostHuntScoreboardText(game) {
  const hunterStats = getHunterStats(game);
  const ghostStats = getGhostStats(game);

  const timer = document.querySelector(".round-timer-box");
  if (timer) timer.textContent = getClockText(game);

  const pauseBanner = document.querySelector(".round-pause-banner");
  if (pauseBanner) {
    pauseBanner.classList.toggle("hidden", !isGamePaused(game));
  }

  const hunterCard = document.querySelector(".ghost-hunt-hunters-card");
  if (hunterCard) hunterCard.innerHTML = renderGhostHuntHuntersScoreBox(hunterStats);

  const ghostCard = document.querySelector(".ghost-hunt-ghosts-card");
  if (ghostCard) ghostCard.innerHTML = renderGhostHuntGhostsScoreBox(ghostStats);

  const hunterGrid = document.querySelector(".ghost-hunt-hunters-list .team-roster-grid");
  if (hunterGrid) {
    hunterGrid.classList.toggle("two-columns", hunterStats.players.length > 6);
    hunterGrid.innerHTML = renderGhostHuntHunterCards(game);
  }

  const ghostGrid = document.querySelector(".ghost-journal-card .ghost-journal-grid");
  if (ghostGrid) ghostGrid.innerHTML = renderGhostJournalCards(game);

  const events = document.querySelector(".scoreboard-event-strip-content");
  if (events) events.innerHTML = renderRecentEventsInline(game);
}

function renderGhostHuntHuntersScoreBox(hunterStats) {
  return `
    <div class="score-box-label">Hunters</div>
    <div class="score-box-value">${hunterStats.alive}/${hunterStats.total}</div>
    <div class="score-box-meta">${hunterStats.alive} alive • ${hunterStats.dead} dead</div>
  `;
}

function renderGhostHuntGhostsScoreBox(ghostStats) {
  return `
    <div class="score-box-label">Ghosts Captured</div>
    <div class="score-box-value">${ghostStats.captured}/${ghostStats.total}</div>
    <div class="score-box-meta">${ghostStats.remaining} still haunting</div>
  `;
}

function renderGhostHuntHunterCards(game) {
  const players = game.players || [];
  if (players.length === 0) {
    return `<div class="team-roster-empty">No hunters joined.</div>`;
  }

  return players.map(player => renderGhostHuntHunterCard(player)).join("");
}

function renderGhostHuntHunterCard(player) {
  const alive = isAlive(player);
  const score = Number(player.stats?.score ?? player.score ?? 0);
  const captures = getNumberCustomValue(player.stats, "ghostCapturesThisGame", score);
  const hearts = Math.max(0, getNumberCustomValue(player, "ghostHuntHearts", alive ? 0 : 0));
  const maxHearts = Math.max(1, getNumberCustomValue(player, "ghostHuntMaxHearts", hearts || 3));
  const heartText = alive ? `${"♥".repeat(Math.min(hearts, 5))}${"♡".repeat(Math.max(0, Math.min(maxHearts, 5) - hearts))}` : "OUT";

  return `
    <article class="roster-player-card ghost-hunter-card ${alive ? "alive" : "dead"}">
      <div class="roster-player-name">${escapeHtml(player.handle || player.playerId || "Hunter")}</div>
      <div class="roster-player-stats">${captures} captured • ${escapeHtml(heartText)}</div>
      <div class="roster-player-status">${alive ? "HUNTING" : "DEAD"}</div>
    </article>
  `;
}

function getGhostTypeNote(type) {
  switch (String(type || "").toLowerCase()) {
    case "shy":
      return "flees";
    case "aggressive":
      return "hunts";
    case "trickster":
      return "blinks";
    case "normal":
    default:
      return "wanders";
  }
}

function getGhostStatusLabel(ghost) {
  if (isGhostCaptured(ghost)) {
    return "CAPTURED";
  }

  const state = getGhostState(ghost);
  switch (state) {
    case "hunting":
      return "HUNTING";
    case "fleeing":
      return "FLEEING";
    case "hidden":
      return "HIDDEN";
    case "wandering":
    default:
      return "ACTIVE";
  }
}

function renderGhostJournalCards(game) {
  const ghosts = getGhostObjectives(game);
  const ghostStats = getGhostStats(game);

  if (ghostStats.total <= 0) {
    return `<div class="team-roster-empty">No ghosts spawned.</div>`;
  }

  const rows = ghosts.map((ghost, index) => {
    const type = getGhostType(ghost);
    const displayName = getGhostDisplayName(ghost);
    const note = getGhostTypeNote(type);
    const captured = isGhostCaptured(ghost);
    const detail = captured ? "captured" : "still haunting";

    return `
      <article class="roster-player-card ghost-journal-row ghost-type-${escapeHtml(type)} ${captured ? "captured" : "active"}">
        <div class="roster-player-name">${escapeHtml(displayName)} #${index + 1}</div>
        <div class="roster-player-stats">${escapeHtml(detail)} • ${escapeHtml(note)}</div>
        <div class="roster-player-status">${escapeHtml(getGhostStatusLabel(ghost))}</div>
      </article>
    `;
  });

  const missingGhostDetails = Math.max(0, ghostStats.total - ghosts.length);
  for (let i = 0; i < missingGhostDetails; i += 1) {
    rows.push(`
      <article class="roster-player-card ghost-journal-row ghost-type-unknown active">
        <div class="roster-player-name">Unknown Ghost #${ghosts.length + i + 1}</div>
        <div class="roster-player-stats">waiting for ghost details</div>
        <div class="roster-player-status">ACTIVE</div>
      </article>
    `);
  }

  return rows.join("");
}

function getZombieExtractionPlayerStats(game) {
  const players = getMapPlayers(game);
  const alive = players.filter(player => isAlive(player)).length;
  return {
    players,
    alive,
    dead: Math.max(0, players.length - alive),
    total: players.length
  };
}

function renderZombieExtractionSurvivorsScoreBox(stats, game) {
  const partsCollected = getNumberCustomValue(game, "zombieExtractionBeaconPartsCollected", 0);
  const partsRequired = getNumberCustomValue(game, "zombieExtractionBeaconPartCount", 0);
  const horde = getNumberCustomValue(game, "zombieExtractionActiveZombieCount",
    Array.isArray(game?.liveMap?.zombies) ? game.liveMap.zombies.filter(zombie => (zombie.active ?? zombie.Active) !== false).length : 0);
  return `
    <div class="score-box-label">Survivors</div>
    <div class="score-box-value">${stats.alive}/${stats.total}</div>
    <div class="score-box-meta">Parts ${partsCollected}/${partsRequired} • Horde ${horde}</div>
  `;
}

function renderZombieExtractionPlayerCards(game) {
  const players = getMapPlayers(game);
  if (players.length === 0) {
    return `<div class="team-roster-empty">No survivors joined.</div>`;
  }

  return players.map(player => {
    const alive = isAlive(player);
    const health = Number(player.zombieExtractionHealth ?? player.ZombieExtractionHealth ?? 0);
    const maxHealth = Number(player.zombieExtractionMaxHealth ?? player.ZombieExtractionMaxHealth ?? 0);
    const parts = Number(player.zombieBeaconPartsThisGame ?? player.ZombieBeaconPartsThisGame ?? 0);
    const crates = Number(player.zombieCratesThisGame ?? player.ZombieCratesThisGame ?? 0);
    const state = String(player.playerState ?? player.PlayerState ?? (alive ? "alive" : "eliminated"));
    return `
      <article class="roster-player-card zombie-survivor-card ${alive ? "alive" : "dead"}">
        <div class="roster-player-name">${escapeHtml(player.handle || player.playerId || "Survivor")}</div>
        <div class="roster-player-stats">Health ${health}/${maxHealth} • Parts ${parts} • Crates ${crates}</div>
        <div class="roster-player-status">${escapeHtml(state.toUpperCase())}</div>
      </article>
    `;
  }).join("");
}


function getZombieExtractionHorde(game) {
  const liveZombies = game?.liveMap?.zombies ?? game?.liveMap?.Zombies;
  const snapshotZombies = getCustomValue(game, "zombieExtractionInitialZombies");
  const source = Array.isArray(liveZombies)
    ? liveZombies
    : (Array.isArray(snapshotZombies) ? snapshotZombies : []);

  return source
    .filter(zombie => (zombie?.active ?? zombie?.Active) !== false)
    .slice()
    .sort((a, b) => {
      const aNumber = Number(a?.spawnNumber ?? a?.SpawnNumber ?? Number.MAX_SAFE_INTEGER);
      const bNumber = Number(b?.spawnNumber ?? b?.SpawnNumber ?? Number.MAX_SAFE_INTEGER);
      if (aNumber !== bNumber) return aNumber - bNumber;
      return String(getZombieHordeName(a)).localeCompare(String(getZombieHordeName(b)));
    });
}

function getZombieHordeName(zombie) {
  return String(
    zombie?.displayName ?? zombie?.DisplayName ??
    zombie?.name ?? zombie?.Name ??
    "Unnamed Zombie"
  ).trim() || "Unnamed Zombie";
}

function getZombieHordeState(zombie) {
  const raw = String(zombie?.state ?? zombie?.State ?? zombie?.zombieState ?? zombie?.ZombieState ?? "hunting")
    .trim()
    .toLowerCase();
  switch (raw) {
    case "repelled": return "REPELLED";
    case "stunned": return "STUNNED";
    case "wandering": return "WANDERING";
    case "retreating": return "RETREATING";
    case "hunting":
    default:
      return "HUNTING";
  }
}

function renderZombieExtractionHordeScoreBox(game) {
  const horde = getZombieExtractionHorde(game);
  return `
    <div class="score-box-label">Meet the Horde</div>
    <div class="score-box-value">${horde.length}</div>
    <div class="score-box-meta">Every monster has a name</div>
  `;
}

function renderZombieExtractionHordeCards(game) {
  const horde = getZombieExtractionHorde(game);
  if (horde.length === 0) {
    return `<div class="team-roster-empty">The horde is gathering...</div>`;
  }

  return horde.map((zombie, index) => {
    const spawnNumber = Number(zombie?.spawnNumber ?? zombie?.SpawnNumber ?? index + 1);
    return `
      <article class="roster-player-card zombie-horde-row">
        <div class="roster-player-name">${escapeHtml(getZombieHordeName(zombie))}</div>
        <div class="roster-player-stats">Horde member #${Number.isFinite(spawnNumber) ? spawnNumber : index + 1}</div>
        <div class="roster-player-status">${escapeHtml(getZombieHordeState(zombie))}</div>
      </article>
    `;
  }).join("");
}

function renderZombieExtractionLiveStatus(game) {
  const players = getMapPlayers(game);
  const locatedPlayers = players.filter(player => Boolean(getMapPlayerLocation(player))).length;
  const liveMapLoaded = Boolean(game?.liveMap);

  if (!liveMapLoaded) {
    return `<div class="zombie-live-map-status warning">Waiting for local live-map data</div>`;
  }

  if (players.length > 0 && locatedPlayers === 0) {
    return `<div class="zombie-live-map-status warning">Waiting for SignalR player GPS</div>`;
  }

  return `<div class="zombie-live-map-status">Live map • ${locatedPlayers}/${players.length} located</div>`;
}

function renderZombieExtractionScoreboard(game) {
  const survivorStats = getZombieExtractionPlayerStats(game);

  // Phase 45D.3: restore the Ghost Hunt shell's right column as a cosmetic
  // live roll call for every active named zombie in the horde.
  return renderGhostHuntStyleScoreboardShell(game, {
    boardClasses: "zombie-extraction-board",
    leftScoreClasses: "zombie-extraction-survivors-card",
    leftScoreContent: renderZombieExtractionSurvivorsScoreBox(survivorStats, game),
    rightScoreClasses: "zombie-extraction-horde-score",
    rightScoreContent: renderZombieExtractionHordeScoreBox(game),
    leftRosterClasses: "zombie-extraction-survivors-list",
    leftRosterContent: renderZombieExtractionPlayerCards(game),
    leftRosterTwoColumns: survivorStats.players.length > 6,
    mapClasses: "ghost-hunt-map-card zombie-extraction-map-card",
    mapPrelude: renderZombieExtractionLiveStatus(game),
    rightRosterClasses: "zombie-extraction-horde-column",
    rightRosterContent: renderZombieExtractionHordeCards(game),
    rightRosterGridClasses: "zombie-horde-grid"
  });
}

function updateZombieExtractionScoreboardText(game) {
  const survivorStats = getZombieExtractionPlayerStats(game);

  const timer = document.querySelector(".round-timer-box");
  if (timer) timer.textContent = getClockText(game);

  const pauseBanner = document.querySelector(".round-pause-banner");
  if (pauseBanner) pauseBanner.classList.toggle("hidden", !isGamePaused(game));

  const survivorCard = document.querySelector(".zombie-extraction-survivors-card");
  if (survivorCard) survivorCard.innerHTML = renderZombieExtractionSurvivorsScoreBox(survivorStats, game);

  const hordeCard = document.querySelector(".zombie-extraction-horde-score");
  if (hordeCard) hordeCard.innerHTML = renderZombieExtractionHordeScoreBox(game);

  const survivorGrid = document.querySelector(".zombie-extraction-survivors-list .team-roster-grid");
  if (survivorGrid) {
    survivorGrid.classList.toggle("two-columns", survivorStats.players.length > 6);
    survivorGrid.innerHTML = renderZombieExtractionPlayerCards(game);
  }

  const hordeGrid = document.querySelector(".zombie-extraction-horde-column .zombie-horde-grid");
  if (hordeGrid) hordeGrid.innerHTML = renderZombieExtractionHordeCards(game);

  const liveStatus = document.querySelector(".zombie-live-map-status");
  if (liveStatus) {
    const replacement = document.createElement("div");
    replacement.innerHTML = renderZombieExtractionLiveStatus(game);
    liveStatus.replaceWith(replacement.firstElementChild);
  }

  const events = document.querySelector(".scoreboard-event-strip-content");
  if (events) events.innerHTML = renderRecentEventsInline(game);
}

function getZombieTagLiveState(game) {
  return game?.liveMap?.zombieTag || game?.liveMap?.ZombieTag ||
    game?.liveMap?.zombieExtraction || game?.liveMap?.ZombieExtraction ||
    game?.zombieTag || game?.ZombieTag || null;
}

function getZombieTagArenaPercent(game) {
  const live = getZombieTagLiveState(game);
  const radius = Number(live?.extractionRadiusMeters ?? live?.ExtractionRadiusMeters ?? 0);
  const zone = getMapZones(game)[0];
  const original = Number(getCustomValue(game, "zombieTagOriginalArenaRadiusMeters") || zone?.originalRadiusMeters || zone?.OriginalRadiusMeters || 0);
  if (Number.isFinite(radius) && radius > 0 && Number.isFinite(original) && original > 0) {
    return Math.max(0, Math.min(100, (radius / original) * 100));
  }
  const rawPercent = Number(getCustomValue(game, "zombieTagCurrentArenaPercent") || live?.currentArenaPercent || live?.CurrentArenaPercent || 100);
  return Number.isFinite(rawPercent) ? rawPercent : 100;
}

function renderZombieTagLiveStatus(game) {
  if (getScoreboardModeKey(game) !== "zombie_tag") return "";
  const live = getZombieTagLiveState(game);
  const crateCount = Array.isArray(live?.objectives) ? live.objectives.length : getMapObjectives(game).filter(objective => {
    const type = String(objective.objectiveType || objective.ObjectiveType || "").toLowerCase();
    const role = String(getCustomValue(objective, "zombieTag.role") || "").toLowerCase();
    return (objective.active ?? objective.Active) !== false && (type === "zombie_tag_crate" || role === "zombie_tag_crate");
  }).length;
  const percent = getZombieTagArenaPercent(game);
  const radius = Number(live?.extractionRadiusMeters ?? live?.ExtractionRadiusMeters ?? getMapZones(game)[0]?.radiusMeters ?? 0);
  const radiusText = Number.isFinite(radius) && radius > 0 ? ` • ${Math.round(radius)}m safe zone` : "";
  return `<div class="zombie-live-map-status">Zombie Tag arena • ${Math.round(percent)}% remaining${radiusText} • ${crateCount} crate${crateCount === 1 ? "" : "s"}</div>`;
}

function renderTeamElimination(game) {
  const teams = normalizeTeams(game);
  const isZombieTag = getScoreboardModeKey(game) === "zombie_tag";
  const red = teams[0] || emptyTeam(isZombieTag ? "survivors" : "red", isZombieTag ? "Survivors" : "Red Team");
  const blue = teams[1] || emptyTeam(isZombieTag ? "zombies" : "blue", isZombieTag ? "Zombies" : "Blue Team");
  const winningTeamIds = getWinningTeamIds(game);

  return `
    <section class="team-elimination-board ${isZombieTag ? "zombie-tag-board" : ""}">
      <section class="score-box-card score-box-red ${isZombieTag ? "score-box-zombie-survivors" : ""} ${winningTeamIds.includes(red.teamId || "") ? "score-box-winner" : ""}">
        ${renderScoreBox(red, isZombieTag ? "Survivors" : "Red", game)}
      </section>

      <section class="round-status-shell">
        <div class="round-timer-box">${escapeHtml(getClockText(game))}</div>
        ${renderPausedNotice(game)}
        <div class="round-status-card">
          <div class="round-status-primary">${escapeHtml(getServerDisplayName(game))}</div>
          <div class="round-status-secondary">${escapeHtml(modeDisplayName(game.gameMode || ""))}</div>
        </div>
      </section>

      <section class="score-box-card score-box-blue ${isZombieTag ? "score-box-zombie-zombies" : ""} ${winningTeamIds.includes(blue.teamId || "") ? "score-box-winner" : ""}">
        ${renderScoreBox(blue, isZombieTag ? "Zombies" : "Blue", game)}
      </section>

      <section class="team-roster-card team-roster-red ${isZombieTag ? "team-roster-zombie-survivors" : ""}">
        <div class="team-roster-grid ${red.players.length > 6 ? "two-columns" : ""}">
          ${red.players.length === 0
            ? `<div class="team-roster-empty">No players joined.</div>`
            : red.players.map(player => renderRosterPlayerCard(player, game)).join("")}
        </div>
      </section>

      <section class="scoreboard-map-card scoreboard-map-center">
        ${isZombieTag ? renderZombieTagLiveStatus(game) : ""}
        ${renderPlayZoneMap(game)}
      </section>

      <section class="team-roster-card team-roster-blue ${isZombieTag ? "team-roster-zombie-zombies" : ""}">
        <div class="team-roster-grid ${blue.players.length > 6 ? "two-columns" : ""}">
          ${blue.players.length === 0
            ? `<div class="team-roster-empty">No players joined.</div>`
            : blue.players.map(player => renderRosterPlayerCard(player, game)).join("")}
        </div>
      </section>

      <section class="scoreboard-event-strip">
        <div class="scoreboard-event-strip-label">Recent Events</div>
        <div class="scoreboard-event-strip-content">
          ${renderRecentEventsInline(game)}
        </div>
      </section>
    </section>
  `;
}

function renderScoreBox(team, fallbackName, game = null) {
  const isZombieTag = getScoreboardModeKey(game) === "zombie_tag";
  const teamId = String(team.teamId || "").toLowerCase();
  const label = String(team.name || fallbackName || "");
  const alive = team.players.filter(player => isAlive(player)).length;
  const dead = team.players.length - alive;
  let score = Number(team.score || 0);
  let meta = `${alive} alive • ${dead} dead`;

  if (isZombieTag) {
    const playerCount = Array.isArray(game?.players) ? game.players.length : 0;
    if (teamId === "zombies") {
      score = getZombieTagScoreCount(game, team, "zombies", playerCount);
      if (score <= 0 && !isGameActive(game) && !isGamePaused(game) && !isGameFinished(game)) {
        score = getPlannedZombieCount(game, playerCount);
        meta = score > 0 ? `planned at start` : `waiting for players`;
      } else {
        meta = `${score} infected`;
      }
    } else if (teamId === "survivors") {
      score = getZombieTagScoreCount(game, team, "survivors", playerCount);
      if (score <= 0 && !isGameActive(game) && !isGamePaused(game) && !isGameFinished(game)) {
        score = playerCount;
        meta = playerCount > 0 ? `random roles at start` : `waiting for players`;
      } else {
        meta = `${score} still breathing`;
      }
    }
  }

  return `
    <div class="score-box-label">${escapeHtml(toTitleCase(label))}</div>
    <div class="score-box-value">${score}</div>
    <div class="score-box-meta">${escapeHtml(meta)}</div>
  `;
}

function getZombieTagScoreCount(game, team, role, playerCount) {
  const keys = role === "zombies"
    ? ["zombieTagFinalZombieCount", "zombieTagZombieCount"]
    : ["zombieTagFinalSurvivorCount", "zombieTagSurvivorCount"];

  for (const key of keys) {
    const value = getNumberCustomValue(game, key, Number.NaN);
    if (Number.isFinite(value)) {
      return Math.max(0, value);
    }
  }

  const teamFieldKeys = role === "zombies"
    ? ["finalZombieCount", "zombieCount", "playerCount", "alivePlayers"]
    : ["finalSurvivorCount", "survivorCount", "playerCount", "alivePlayers"];

  for (const key of teamFieldKeys) {
    const value = getNumberCustomValue(team, key, Number.NaN);
    if (Number.isFinite(value)) {
      return Math.max(0, value);
    }
  }

  const teamScore = Number(team?.score ?? Number.NaN);
  if (Number.isFinite(teamScore) && teamScore > 0) {
    return Math.max(0, teamScore);
  }

  if (role === "zombies") {
    const playersByRole = (game?.players || []).filter(player => isZombieTagZombiePlayer(player)).length;
    return Math.max(playersByRole, team.players.length);
  }

  if (role === "survivors") {
    const playersByRole = (game?.players || []).filter(player => isZombieTagSurvivorPlayer(player)).length;
    return Math.max(playersByRole, team.players.length);
  }

  return team.players.length;
}

function isZombieTagZombiePlayer(player) {
  const state = `${player?.teamId || ""} ${player?.playerState || ""} ${getCustomValue(player, "zombieTagRole") || ""} ${getCustomValue(player, "playerState") || ""}`.toLowerCase();
  return state.includes("zombie") || state.includes("infected") || state.includes("horde");
}

function isZombieTagSurvivorPlayer(player) {
  const state = `${player?.teamId || ""} ${player?.playerState || ""} ${getCustomValue(player, "zombieTagRole") || ""} ${getCustomValue(player, "playerState") || ""}`.toLowerCase();
  return state.includes("survivor") || state.includes("human");
}

function isGameFinished(game) {
  const status = String(game?.status || "").toLowerCase();
  return ["finished", "ended", "complete", "completed"].includes(status);
}

function getPlannedZombieCount(game, playerCount) {
  if (!playerCount || playerCount <= 0) return 0;
  const every = Math.max(1, getNumberCustomValue(game, "startingZombieEveryPlayers", 5));
  return Math.max(1, Math.floor(playerCount / every));
}

function isGameActive(game) {
  const status = String(game?.status || "").toLowerCase();
  return ["active", "started", "running", "inprogress", "playing"].includes(status);
}

function renderRosterPlayerCard(player, game = null) {
  const alive = isAlive(player);
  const score = Number(player.stats?.score ?? player.score ?? 0);
  const meters = Number(player.stats?.distanceMetersThisGame ?? 0);
  const playerTeamId = player.teamId || player.TeamId || "";
  const playerColor = getTeamColor(game, playerTeamId);
  const teamClass = getScoreboardModeKey(game) === "zombie_tag"
    ? getZombieTagScoreboardTeamColor(playerTeamId) === "#ff4c4c"
      ? " zombie-player"
      : getZombieTagScoreboardTeamColor(playerTeamId) === "#54d979"
        ? " survivor-player"
        : ""
    : "";

  return `
    <article class="roster-player-card ${alive ? "alive" : "dead"} ${playerColor ? "team-colored" : ""}${teamClass}"${teamColorStyle(playerColor)}>
      <div class="roster-player-name">${escapeHtml(player.handle || player.playerId || "Player")}</div>
      <div class="roster-player-stats">${score} pts • ${formatMeters(meters)}</div>
      <div class="roster-player-status">${alive ? "ALIVE" : "DEAD"}</div>
    </article>
  `;
}

function renderPlayZoneMap(game) {
  const zone = getMapZones(game)[0];

  const boundaryPoints = getMapBoundaryPoints(zone);
  if (!zone || boundaryPoints.length < 3) {
    return `<div class="scoreboard-map-empty">No play zone defined.</div>`;
  }

  return `
    <div id="scoreboardLeafletMap" class="scoreboard-map-wrap scoreboard-leaflet-map"></div>
  `;
}

function normalizeHexColor(value) {
  const raw = (value || "").toString().trim();
  const match = raw.match(/^#?([0-9a-fA-F]{6})$/);
  return match ? `#${match[1].toLowerCase()}` : "";
}

function getZombieTagScoreboardTeamColor(teamIdOrName) {
  const key = (teamIdOrName || "").toString().trim().toLowerCase();
  if (key.includes("zombie") || key.includes("infected") || key.includes("horde")) return "#ff4c4c";
  if (key.includes("survivor") || key.includes("human")) return "#54d979";
  return "";
}

function getDefaultTeamColor(team, index = 0) {
  const key = `${team?.teamId || ""} ${team?.name || ""}`.toLowerCase();
  const zombieTagColor = getZombieTagScoreboardTeamColor(key);
  if (zombieTagColor) return zombieTagColor;
  if (key.includes("blue")) return "#45a3ff";
  if (key.includes("red")) return "#ff4c4c";
  return index === 1 ? "#45a3ff" : "#ff4c4c";
}

function getTeamColor(game, teamId) {
  const wanted = (teamId || "").toString().trim().toLowerCase();
  if (getScoreboardModeKey(game) === "zombie_tag") {
    const forcedZombieTagColor = getZombieTagScoreboardTeamColor(wanted);
    if (forcedZombieTagColor) return forcedZombieTagColor;
  }
  const teams = game?.teams || [];
  const index = teams.findIndex(team => (team?.teamId || "").toString().trim().toLowerCase() === wanted);
  const team = index >= 0 ? teams[index] : null;

  if (!team) {
    return "";
  }

  if (getScoreboardModeKey(game) === "zombie_tag") {
    const forcedZombieTagColor = getZombieTagScoreboardTeamColor(`${team?.teamId || ""} ${team?.name || team?.teamName || ""}`);
    if (forcedZombieTagColor) return forcedZombieTagColor;
  }

  return normalizeHexColor(team.colorHex || team.color || team.teamColorHex || team.customData?.colorHex || team.customFields?.colorHex) ||
    getDefaultTeamColor(team, index);
}

function getObjectiveTeamColor(game, objective) {
  const data = objective?.customData || {};
  const teamColor = getTeamColor(game, getObjectiveTeamId(objective));
  if (teamColor) {
    return teamColor;
  }

  return normalizeHexColor(data["ctf.teamColor"] || data["ctf.flagTeamColor"] || data["ctf.baseTeamColor"]);
}

function teamColorStyle(color) {
  const safeColor = normalizeHexColor(color);
  return safeColor ? ` style="--team-color:${escapeHtml(safeColor)};"` : "";
}

function renderLeafletPlayZoneMap(game, forceFit = false) {
  const mapEl = document.getElementById("scoreboardLeafletMap");
  const zone = getMapZones(game)[0];

  const boundaryPoints = getMapBoundaryPoints(zone);
  if (!mapEl || !zone || boundaryPoints.length < 3) {
    return;
  }

  if (!window.L) {
    mapEl.innerHTML = `<div class="scoreboard-map-empty">Map engine did not load.</div>`;
    return;
  }

  const boundary = boundaryPoints
    .map(point => [Number(point.latitude ?? point.Latitude), Number(point.longitude ?? point.Longitude)])
    .filter(point => Number.isFinite(point[0]) && Number.isFinite(point[1]));

  if (boundary.length < 3) {
    mapEl.innerHTML = `<div class="scoreboard-map-empty">Play zone boundary is invalid.</div>`;
    return;
  }

  if (!scoreboardLeafletMap || forceFit) {
    if (scoreboardLeafletMap) {
      scoreboardLeafletMap.remove();
      scoreboardLeafletMap = null;
      scoreboardLeafletLayerGroup = null;
      scoreboardLeafletTileLayer = null;
      mapEl.innerHTML = "";
    }

    scoreboardLeafletMap = L.map(mapEl, {
      zoomControl: false,
      attributionControl: false,
      dragging: false,
      scrollWheelZoom: false,
      doubleClickZoom: false,
      boxZoom: false,
      keyboard: false,
      tap: false,
      touchZoom: false
    });

    scoreboardLeafletTileLayer = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
      maxZoom: 20,
      crossOrigin: true,
      keepBuffer: 4,
      updateWhenIdle: true,
      updateWhenZooming: false
    }).addTo(scoreboardLeafletMap);

    scoreboardLeafletLayerGroup = L.layerGroup().addTo(scoreboardLeafletMap);
  }

  if (!scoreboardLeafletLayerGroup) {
    scoreboardLeafletLayerGroup = L.layerGroup().addTo(scoreboardLeafletMap);
  }

  scoreboardLeafletLayerGroup.clearLayers();

  const accent = getComputedStyle(document.body).getPropertyValue("--score-accent").trim() || "#ffbd62";

  L.polygon(boundary, {
    color: accent,
    weight: 4,
    fillColor: accent,
    fillOpacity: 0.16,
    interactive: false
  }).addTo(scoreboardLeafletLayerGroup);

  const playerLatLngs = [];
  const objectiveLatLngs = [];

  const currentMode = getScoreboardModeKey(game);
  const mapObjectives = getMapObjectives(game).slice();

  if (currentMode === "zombie_extraction") {
    getMapZombies(game).forEach(zombie => {
      mapObjectives.push({
        objectiveId: zombie.zombieId || zombie.ZombieId || zombie.entityId || zombie.EntityId || "zombie",
        objectiveType: "zombie",
        name: "Zombie",
        latitude: zombie.latitude ?? zombie.Latitude,
        longitude: zombie.longitude ?? zombie.Longitude,
        active: (zombie.active ?? zombie.Active) !== false,
        customData: { "zombieExtraction.role": "zombie" }
      });
    });
  }

  mapObjectives
    .filter(objective => (objective.active ?? objective.Active) !== false)
    .forEach(objective => {
      const lat = Number(objective.latitude ?? objective.Latitude);
      const lng = Number(objective.longitude ?? objective.Longitude);

      if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
        return;
      }

      const objectiveType = (objective.objectiveType || objective.ObjectiveType || "").toLowerCase();
      const objectiveId = (objective.objectiveId || objective.ObjectiveId || "").toLowerCase();
      const zombieExtractionRole = String(getCustomValue(objective, "zombieExtraction.role") || "").toLowerCase();
      const zombieTagRole = String(getCustomValue(objective, "zombieTag.role") || "").toLowerCase();
      const zombieRole = zombieExtractionRole || zombieTagRole;
      const isGhost = objectiveType === "ghost" || String(getCustomValue(objective, "ghost.role") || "").toLowerCase() === "ghost";
      const isFlag = objectiveType === "ctf_flag" || objectiveId.startsWith("ctf_flag_");
      const isBase = objectiveType === "ctf_base" || objectiveId.startsWith("ctf_base_");
      const isBeacon = objectiveType === "zombie_beacon_part" || zombieRole === "beacon_part";
      const isCrate = objectiveType === "zombie_supply_crate" || objectiveType === "zombie_tag_crate" || zombieRole === "supply_crate" || zombieRole === "zombie_tag_crate";
      const isZombie = objectiveType === "zombie" || zombieRole === "zombie";
      const isExtraction = objectiveType === "zombie_extraction_zone" || zombieRole === "extraction_zone";

      const visibleForMode =
        currentMode === "zombie_tag" ? isCrate :
        currentMode === "zombie_extraction" ? (isBeacon || isCrate || isZombie || isExtraction) :
        currentMode === "ghost_hunt" ? isGhost :
        currentMode === "capture_the_flag" ? (isFlag || isBase) :
        false;

      if (!visibleForMode) {
        return;
      }

      if (isGhost && isGhostCaptured(objective)) {
        return;
      }

      objectiveLatLngs.push([lat, lng]);

      const teamId = getObjectiveTeamId(objective).toLowerCase();
      const carrierPlayerId = getObjectiveCustom(objective, "ctf.carrierPlayerId");
      const objectiveColor = getObjectiveTeamColor(game, objective);
      const ghostType = isGhost ? getGhostType(objective) : "";
      const markerClass = isGhost
        ? `leaflet-objective-marker ghost ghost-${ghostType}`
        : isBeacon
          ? "leaflet-objective-marker zombie-beacon"
          : isCrate
            ? "leaflet-objective-marker zombie-crate zombie-tag-crate"
            : isZombie
              ? "leaflet-objective-marker zombie-enemy"
              : isExtraction
                ? "leaflet-objective-marker zombie-extraction-zone"
              : `leaflet-objective-marker ${isFlag ? "flag" : "base"} team-colored ${teamId === "red" ? "red" : teamId === "blue" ? "blue" : ""} ${carrierPlayerId ? "carried" : "home"}`;
      const iconText = isGhost ? "👻" : isBeacon ? "◆" : isCrate ? "▣" : isZombie ? "Z" : isExtraction ? "EXIT" : isFlag ? "⚑" : "⌂";
      const labelText = isGhost
        ? `${getGhostDisplayName(objective)} ${String(objective.name || "").replace(/^.*?#/, "#")}`.trim()
        : isBeacon
          ? (objective.name || objective.Name || "Beacon Part")
          : isCrate
            ? (currentMode === "zombie_tag" ? "Survivor Crate" : (objective.name || objective.Name || "Supply Crate"))
            : isZombie
              ? "Zombie"
              : isExtraction
                ? "Extraction"
                : `${getObjectiveTeamName(objective)} ${isFlag ? "Flag" : "Base"}`;

      const marker = L.divIcon({
        className: markerClass,
        html: `<span${isGhost ? "" : teamColorStyle(objectiveColor)}>${iconText}</span><b>${escapeHtml(labelText)}</b>`,
        iconSize: [1, 1],
        iconAnchor: [0, 0]
      });

      L.marker([lat, lng], {
        icon: marker,
        interactive: false
      }).addTo(scoreboardLeafletLayerGroup);
    });

  getMapPlayers(game).forEach(player => {
    const location = getMapPlayerLocation(player);
    if (!location) {
      return;
    }

    const lat = Number(location.latitude ?? location.Latitude);
    const lng = Number(location.longitude ?? location.Longitude);

    if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
      return;
    }

    playerLatLngs.push([lat, lng]);

    const playerTeamId = player.teamId || player.TeamId || "";
    const teamId = playerTeamId.toLowerCase();
    const alive = isAlive(player);
    const playerColor = getTeamColor(game, playerTeamId);
    const zombieTagMarkerClass = currentMode === "zombie_tag"
      ? getZombieTagScoreboardTeamColor(teamId) === "#ff4c4c"
        ? "zombie-player"
        : getZombieTagScoreboardTeamColor(teamId) === "#54d979"
          ? "survivor-player"
          : ""
      : "";
    const markerClass = `leaflet-player-marker team-colored ${teamId === "red" ? "red" : teamId === "blue" ? "blue" : ""} ${zombieTagMarkerClass} ${alive ? "alive" : "dead"}`;

    const marker = L.divIcon({
      className: markerClass,
      html: `<span${teamColorStyle(playerColor)}></span><b>${escapeHtml((player.handle || player.Handle || "?").slice(0, 10))}</b>`,
      iconSize: [1, 1],
      iconAnchor: [0, 0]
    });

    L.marker([lat, lng], {
      icon: marker,
      interactive: false
    }).addTo(scoreboardLeafletLayerGroup);
  });

  scoreboardLeafletMap.invalidateSize(false);

  if (forceFit || !scoreboardLeafletMap.__scoreboardBoundsFit) {
    const bounds = L.latLngBounds(boundary.concat(playerLatLngs).concat(objectiveLatLngs));

    scoreboardLeafletMap.fitBounds(bounds, {
      padding: [24, 24],
      maxZoom: 18,
      animate: false
    });

    scoreboardLeafletMap.__scoreboardBoundsFit = true;

    setTimeout(() => {
      scoreboardLeafletMap?.invalidateSize(false);
      scoreboardLeafletMap?.fitBounds(bounds, {
        padding: [24, 24],
        maxZoom: 18,
        animate: false
      });
    }, 150);

    setTimeout(() => {
      scoreboardLeafletMap?.invalidateSize(false);
    }, 500);
  }
}

function renderRecentEventsInline(game) {
  const events = (game.events || [])
    .slice()
    .sort((a, b) => new Date(b.createdAtUtc || 0) - new Date(a.createdAtUtc || 0))
    .slice(0, 5);

  if (events.length === 0) {
    return `<span class="scoreboard-event-chip">No events yet.</span>`;
  }

  return events.map(event => `
    <span class="scoreboard-event-chip">
      <strong>${escapeHtml(event.type || "event")}:</strong>
      ${escapeHtml(event.message || "")}
    </span>
  `).join("");
}

function renderGenericScoreboard(game) {
  const teams = normalizeTeams(game);

  return `
    <section class="generic-scoreboard-layout">
      <header class="generic-scoreboard-header">
        <div>
          <h1>${escapeHtml(game.name || "Oops, You're Dead")}</h1>
          <p>${escapeHtml(modeDisplayName(game.gameMode || ""))}</p>
        </div>
        <div>
          <div class="generic-scoreboard-clock">${escapeHtml(getClockText(game))}</div>
          ${renderPausedNotice(game)}
        </div>
      </header>

      <section class="generic-team-grid">
        ${teams.map(team => `
          <article class="generic-team-card">
            <div class="generic-team-name">${escapeHtml(team.name)}</div>
            <div class="generic-team-score">${Number(team.score || 0)}</div>
            <div class="generic-team-players">
              ${team.players.map(player => renderRosterPlayerCard(player, game)).join("")}
            </div>
          </article>
        `).join("")}
      </section>
    </section>
  `;
}

function normalizeTeams(game) {
  const players = game.players || [];
  const serverTeams = Array.isArray(game.teams) ? game.teams : [];
  const isZombieTag = getScoreboardModeKey(game) === "zombie_tag";

  const teams = serverTeams.map(team => {
    const teamId = team.teamId || "";
    const teamName = team.name || team.teamName || team.teamId || "Team";
    const forcedZombieTagColor = isZombieTag ? getZombieTagScoreboardTeamColor(`${teamId} ${teamName}`) : "";
    return {
      teamId,
      name: teamName,
      colorHex: forcedZombieTagColor || normalizeHexColor(team.colorHex || team.color || team.teamColorHex || team.customData?.colorHex || team.customFields?.colorHex) || getDefaultTeamColor(team, serverTeams.indexOf(team)),
      score: Number(team.score || 0),
      players: players.filter(player => (player.teamId || "") === teamId)
    };
  });

  const knownTeamIds = new Set(teams.map(team => (team.teamId || "").toLowerCase()));
  const playersWithUnknownTeams = players.filter(player =>
    player.teamId && !knownTeamIds.has((player.teamId || "").toLowerCase())
  );

  const unknownTeamIds = [...new Set(playersWithUnknownTeams.map(player => player.teamId))];

  unknownTeamIds.forEach(teamId => {
    const teamPlayers = playersWithUnknownTeams.filter(player => player.teamId === teamId);
    const teamName = teamPlayers.find(player => player.teamName)?.teamName || teamId;
    const forcedZombieTagColor = isZombieTag ? getZombieTagScoreboardTeamColor(`${teamId} ${teamName}`) : "";
    teams.push({
      teamId,
      name: teamName,
      colorHex: forcedZombieTagColor || normalizeHexColor(teamPlayers.find(player => player.teamColorHex)?.teamColorHex) || "",
      score: teamPlayers.reduce((total, player) => total + Number(player.stats?.score ?? player.score ?? 0), 0),
      players: teamPlayers
    });
  });

  const unassigned = players.filter(player => !player.teamId);

  if (unassigned.length > 0) {
    teams.push({
      teamId: "lobby",
      name: "Lobby",
      score: 0,
      players: unassigned
    });
  }

  if (isZombieTag) {
    const rankZombieTagTeam = team => {
      const key = `${team.teamId || ""} ${team.name || ""}`.toLowerCase();
      if (key.includes("survivor") || key.includes("human")) return 0;
      if (key.includes("zombie") || key.includes("infected") || key.includes("horde")) return 1;
      if (key === "lobby") return 3;
      return 2;
    };
    teams.sort((a, b) => rankZombieTagTeam(a) - rankZombieTagTeam(b));
  }

  return teams;
}

function emptyTeam(teamId, name) {
  return {
    teamId,
    name,
    colorHex: getDefaultTeamColor({ teamId, name }, teamId === "blue" ? 1 : 0),
    score: 0,
    players: []
  };
}

function getBounds(points) {
  const lats = points.map(point => point.lat);
  const lngs = points.map(point => point.lng);

  return {
    minLat: Math.min(...lats),
    maxLat: Math.max(...lats),
    minLng: Math.min(...lngs),
    maxLng: Math.max(...lngs)
  };
}

function makeProjector(bounds, width, height, padding) {
  const latSpan = Math.max(0.000001, bounds.maxLat - bounds.minLat);
  const lngSpan = Math.max(0.000001, bounds.maxLng - bounds.minLng);

  return (lat, lng) => {
    const x = padding + ((lng - bounds.minLng) / lngSpan) * (width - padding * 2);
    const y = padding + ((bounds.maxLat - lat) / latSpan) * (height - padding * 2);

    return { x, y };
  };
}

function getClockText(game) {
  const status = (game.status || "").toLowerCase();

  if (["finished", "ended", "complete", "completed"].includes(status)) {
    return "GAME OVER";
  }

  if (status === "paused") {
    const pausedRemaining = getPausedRemainingSeconds(game);
    if (pausedRemaining > 0) {
      return formatClockSeconds(pausedRemaining);
    }
  }

  const end = parseDate(
    game.endTimeUtc ||
    game.endsAtUtc ||
    game.endUtc ||
    game.endedAtUtc ||
    getCustomValue(game, "endTimeUtc") ||
    getCustomValue(game, "zombieTagEndsAtUtc")
  );

  if (end) {
    const diffSeconds = Math.max(0, Math.floor((end.getTime() - Date.now()) / 1000));
    return formatClockSeconds(diffSeconds);
  }

  const endMillis = Number(
    game.endsAtUnixMillis ||
    game.endTimeUnixMillis ||
    getCustomValue(game, "zombieTagEndsAtUnixMillis") ||
    getCustomValue(game, "endsAtUnixMillis") ||
    0
  );

  if (Number.isFinite(endMillis) && endMillis > 0) {
    const diffSeconds = Math.max(0, Math.floor((endMillis - Date.now()) / 1000));
    return formatClockSeconds(diffSeconds);
  }

  const started = parseDate(game.startTimeUtc || game.startedAtUtc || getCustomValue(game, "startTimeUtc"));
  const durationMinutes = Number(
    getCustomValue(game, "zombieTag.timeLimitMinutes") ||
    getCustomValue(game, "zombieTagTimeLimitMinutes") ||
    getCustomValue(game, "gameDurationMinutes") ||
    0
  );

  if (started && Number.isFinite(durationMinutes) && durationMinutes > 0) {
    const fallbackEndMillis = started.getTime() + (durationMinutes * 60 * 1000);
    const diffSeconds = Math.max(0, Math.floor((fallbackEndMillis - Date.now()) / 1000));
    return formatClockSeconds(diffSeconds);
  }

  return "--:--";
}

function parseDate(value) {
  if (!value) return null;

  const date = new Date(value);
  return Number.isNaN(date.getTime()) ? null : date;
}

function isAlive(player) {
  if (typeof player.isAlive === "boolean") {
    return player.isAlive;
  }

  return (player.playerState || "").toLowerCase() !== "dead";
}

function toTitleCase(value) {
  return String(value || "")
    .replace(/[_-]+/g, " ")
    .trim()
    .split(/\s+/)
    .filter(Boolean)
    .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
    .join(" ");
}

function modeDisplayName(mode) {
  switch (canonicalGameModeKey(mode)) {
    case "team_elimination":
      return "Team Elimination";
    case "capture_the_flag":
      return "Capture the Flag";
    case "ghost_hunt":
      return "Ghost Hunt";
    case "zombie_extraction":
      return "Zombie Extraction";
    case "zombie_tag":
      return "Zombie Tag";
    default:
      return toTitleCase(mode) || "No Mode";
  }
}

function formatMeters(value) {
  if (!Number.isFinite(value)) {
    return "0 m";
  }

  if (value >= 1000) {
    return `${(value / 1000).toFixed(2)} km`;
  }

  return `${Math.round(value)} m`;
}

function escapeHtml(value) {
  return String(value ?? "")
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}
