OpenFront Tactical Assistant

Modular OpenFront advisor toolkit with spawn, threat, expansion, alliance, and optional assist features.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         OpenFront Tactical Assistant
// @namespace    https://github.com/local/openfront-script
// @version      0.8.11
// @description  Modular OpenFront advisor toolkit with spawn, threat, expansion, alliance, and optional assist features.
// @license      MIT
// @match        https://openfront.io/*
// @match        https://beta.openfront.io/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        unsafeWindow
// @run-at       document-start
// @inject-into  auto
// ==/UserScript==
(() => {
  // ofat:virtual:page-bundle
  var PAGE_BUNDLE_SOURCE = `(() => {
  // src/meta.js
  var APP = Object.freeze({
    name: "OpenFront Tactical Assistant",
    shortName: "OF Tactical",
    version: "0.8.11",
    modified: "2026-06-08",
    storageKey: "ofat.settings.v1",
    pagePayloadKey: "__OFAT_PAGE_PAYLOAD__",
    messageSource: "openfront-tactical-assistant"
  });
  var USERSCRIPT_META = Object.freeze({
    name: [APP.name],
    namespace: ["https://github.com/local/openfront-script"],
    version: [APP.version],
    description: ["Modular OpenFront advisor toolkit with spawn, threat, expansion, alliance, and optional assist features."],
    license: ["MIT"],
    match: ["https://openfront.io/*", "https://beta.openfront.io/*"],
    grant: ["GM_setValue", "GM_getValue", "GM_registerMenuCommand", "unsafeWindow"],
    "run-at": ["document-start"],
    "inject-into": ["auto"]
  });
  var DEFAULT_SETTINGS = Object.freeze({
    settingsSchemaVersion: 13,
    showAdvisorPanel: true,
    showHeatmap: true,
    hideAds: true,
    autopilot: false,
    autoSpawn: false,
    autoAlliance: false,
    autoFarm: false,
    autoFarmHumanTargets: true,
    autoEco: false,
    autoDefense: false,
    autoDefenseCounterAttack: true,
    autoDefenseBuildPosts: true,
    autoDefenseBuildSam: true,
    autoDefenseIncomingRatio: 0.35,
    autoBoat: false,
    autoWeapons: false,
    autoWeaponsEarlyNuke: false,
    autoTeamSupport: true,
    autoTeamSupportGold: false,
    autoSosQuickChat: true,
    smartAttack: false,
    showAttackBadges: true,
    showRatioBar: true,
    showTroopRatios: true,
    showWeaknessIndicator: true,
    showDangerIndicator: true,
    showThreatIcons: true,
    showTroopEconomy: true,
    roundLogging: true,
    roundLogAutoDownload: false,
    roundLogSnapshotIntervalMs: 5e3,
    roundLogKeepPlayerNames: true,
    networkLogging: true,
    autoSpawnDelayMs: 2e3,
    autoFarmWindowMs: 24e4,
    autoFarmReserveRatio: 0.55,
    autoFarmCooldownMs: 2500,
    autoFarmDynamicReserve: true,
    autoFarmBaseReserveRatio: 0.3,
    autoFarmMinReserveRatio: 0.2,
    autoFarmMaxReserveRatio: 0.65,
    autoExpand: true
  });

  // src/shared/event-bus.js
  function createEventBus() {
    const listeners = /* @__PURE__ */ new Map();
    return {
      on(type, listener) {
        if (!listeners.has(type)) listeners.set(type, /* @__PURE__ */ new Set());
        listeners.get(type).add(listener);
        return () => listeners.get(type)?.delete(listener);
      },
      emit(type, payload) {
        const set = listeners.get(type);
        if (!set) return;
        set.forEach((listener) => {
          try {
            listener(payload);
          } catch (error) {
            console.error("[OF Tactical] Event listener failed", type, error);
          }
        });
      }
    };
  }

  // src/shared/logger.js
  function createLogger(app) {
    const prefix = \`[\${app.shortName}]\`;
    return {
      info(message, ...args) {
        console.log(\`\${prefix} \${message}\`, ...args);
      },
      warn(message, ...args) {
        console.warn(\`\${prefix} \${message}\`, ...args);
      },
      error(message, ...args) {
        console.error(\`\${prefix} \${message}\`, ...args);
      },
      banner() {
        console.log(\`%c\${prefix} v\${app.version} loaded\`, "color:#4fc3f7;font-weight:bold");
      }
    };
  }

  // src/settings/settings-store.js
  function createSettingsStore(defaults, initialValues = {}) {
    const values = Object.assign({}, defaults, initialValues);
    const listeners = /* @__PURE__ */ new Set();
    function has(key) {
      return Object.prototype.hasOwnProperty.call(defaults, key);
    }
    return {
      values,
      has,
      get(key) {
        return has(key) ? values[key] : void 0;
      },
      set(key, value, meta = {}) {
        if (!has(key)) return false;
        if (values[key] === value) return true;
        values[key] = value;
        listeners.forEach((listener) => listener({ key, value, meta }));
        return true;
      },
      update(nextValues, meta = {}) {
        Object.keys(nextValues || {}).forEach((key) => this.set(key, nextValues[key], meta));
      },
      onChange(listener) {
        listeners.add(listener);
        return () => listeners.delete(listener);
      },
      snapshot() {
        return Object.assign({}, values);
      }
    };
  }

  // src/page/page-settings.js
  function createPageSettingsStore(app, defaults, initialSettings) {
    const store = createSettingsStore(defaults, loadLocalSettings(app, initialSettings));
    store.onChange(({ key, value, meta }) => {
      saveLocalSettings(app, store.snapshot());
      if (!meta.skipUserscriptSync) {
        window.postMessage({ source: app.messageSource, type: "setting-changed", key, value }, "*");
      }
    });
    window.addEventListener("message", (event) => {
      const data = event && event.data;
      if (!data || data.source !== app.messageSource || data.type !== "set-setting") return;
      store.set(data.key, data.value, { skipUserscriptSync: true });
    });
    return store;
  }
  function loadLocalSettings(app, initialSettings) {
    try {
      const raw = localStorage.getItem(app.storageKey);
      if (!raw) return initialSettings;
      const settings = Object.assign({}, initialSettings, JSON.parse(raw));
      return applyLocalSettingsMigrations(app, initialSettings, settings);
    } catch (_) {
      return initialSettings;
    }
  }
  function applyLocalSettingsMigrations(app, initialSettings, settings) {
    const targetSchema = Number(initialSettings.settingsSchemaVersion) || 0;
    const currentSchema = Number(settings.settingsSchemaVersion) || 0;
    if (!targetSchema || currentSchema >= targetSchema) return settings;
    const migrated = Object.assign({}, settings, {
      roundLogAutoDownload: false,
      settingsSchemaVersion: targetSchema
    });
    saveLocalSettings(app, migrated);
    return migrated;
  }
  function saveLocalSettings(app, settings) {
    try {
      localStorage.setItem(app.storageKey, JSON.stringify(settings));
    } catch (_) {
    }
  }

  // src/shared/dom.js
  function appendWhenReady(node, parentSelector = "head") {
    const target = parentSelector === "body" ? document.body : document.head || document.documentElement;
    if (target) {
      target.appendChild(node);
      return;
    }
    document.addEventListener(
      "DOMContentLoaded",
      () => {
        const parent = parentSelector === "body" ? document.body : document.head;
        parent.appendChild(node);
      },
      { once: true }
    );
  }
  function removeElementById(id) {
    const el = document.getElementById(id);
    if (el) el.remove();
  }
  function createStyle(id, css) {
    const existing = document.getElementById(id);
    if (existing) return existing;
    const style = document.createElement("style");
    style.id = id;
    style.textContent = css;
    appendWhenReady(style);
    return style;
  }
  function makeDraggable(el, options = {}) {
    const buttonSelector = options.ignoreSelector || "button";
    let offsetX = 0;
    let offsetY = 0;
    let dragging = false;
    el.addEventListener("mousedown", (event) => {
      if (event.target && event.target.closest && event.target.closest(buttonSelector)) return;
      dragging = true;
      const rect = el.getBoundingClientRect();
      offsetX = event.clientX - rect.left;
      offsetY = event.clientY - rect.top;
    });
    document.addEventListener("mousemove", (event) => {
      if (!dragging) return;
      el.style.left = \`\${Math.max(0, event.clientX - offsetX)}px\`;
      el.style.top = \`\${Math.max(0, event.clientY - offsetY)}px\`;
      el.style.right = "auto";
    });
    document.addEventListener("mouseup", () => {
      dragging = false;
    });
  }

  // src/page/hide-ads.js
  var AD_DOMAINS = [
    "googlesyndication.com",
    "doubleclick.net",
    "doubleverify.com",
    "googleadservices.com",
    "googletag",
    "adservice.google",
    "pagead2.googlesyndication",
    "tpc.googlesyndication",
    "securepubads.g.doubleclick",
    "pubads.g.doubleclick",
    "vpaid.doubleverify",
    "vtrk.dv.tech",
    "innovid.com",
    "ads.pubmatic.com",
    "secure.adnxs.com",
    "ib.adnxs.com",
    "rubiconproject.com",
    "openx.net",
    "advertising.com",
    "amazon-adsystem.com",
    "adsafeprotected.com",
    "moatads.com",
    "chartboost.com",
    "criteo.com",
    "bidswitch.net",
    "playwire.com",
    "intergient.com"
  ];
  var AD_SELECTORS = [
    "#in-game-bottom-left-ad",
    "in-game-promo",
    "[id*='standard_iab']",
    "[class*='bottom_rail']",
    "[class*='bottom-rail']",
    ".pw-oop",
    ".pw-tag",
    "[id^='pw-']",
    "[data-pw-desk]",
    "[data-pw-mobi]",
    "iframe[src*='doubleclick']",
    "iframe[src*='googlesyndication']",
    "iframe[src*='doubleverify']",
    "iframe[src*='googleadservices']",
    "iframe[src*='adsafeprotected']",
    "iframe[src*='amazon-adsystem']",
    "iframe[src*='innovid']",
    "iframe[src*='adnxs']",
    "iframe[src*='rubiconproject']",
    "iframe[src*='openx']",
    "iframe[src*='criteo']",
    "iframe[src*='bidswitch']",
    "iframe[src*='moatads']",
    "iframe[title='Advertisement']",
    "iframe[title='advertisement']",
    "ins.adsbygoogle",
    "[id*='google_ads']",
    "[id*='div-gpt-ad']",
    "[data-ad]"
  ];
  var installed = false;
  function hideAds() {
    if (installed) return;
    installed = true;
    neutralizeRamp();
    installNetworkBlocking();
    installDomBlocking();
  }
  function neutralizeRamp() {
    try {
      window.adsEnabled = false;
      window.ramp = {
        que: [],
        spaAddAds() {
        },
        spaAds() {
        },
        destroyUnits() {
        }
      };
    } catch (_) {
    }
    createStyle(
      "ofat-ad-block-style",
      \`\${AD_SELECTORS.join(",")}{display:none!important;visibility:hidden!important;height:0!important;width:0!important;pointer-events:none!important;}\`
    );
  }
  function isAdUrl(url) {
    if (!url) return false;
    try {
      const str = String(url).toLowerCase();
      return AD_DOMAINS.some((domain) => str.includes(domain));
    } catch (_) {
      return false;
    }
  }
  function installNetworkBlocking() {
    try {
      const origFetch = window.fetch;
      if (typeof origFetch === "function" && !origFetch.__ofatAdBlock) {
        const patched = function patchedFetch(input, init) {
          const url = input && typeof input === "object" ? input.url : input;
          if (isAdUrl(url)) return Promise.reject(new TypeError("blocked ad request"));
          return origFetch.apply(this, arguments);
        };
        patched.__ofatAdBlock = true;
        window.fetch = patched;
      }
    } catch (_) {
    }
    try {
      const origOpen = XMLHttpRequest.prototype.open;
      const origSend = XMLHttpRequest.prototype.send;
      if (origOpen && !origOpen.__ofatAdBlock) {
        const patchedOpen = function patchedXhrOpen(method, url) {
          if (isAdUrl(url)) this.__ofatAdBlocked = true;
          return origOpen.apply(this, arguments);
        };
        patchedOpen.__ofatAdBlock = true;
        XMLHttpRequest.prototype.open = patchedOpen;
        XMLHttpRequest.prototype.send = function patchedXhrSend() {
          if (this.__ofatAdBlocked) return void 0;
          return origSend.apply(this, arguments);
        };
      }
    } catch (_) {
    }
  }
  function installDomBlocking() {
    const nuke = (root) => {
      if (!root || typeof root.querySelectorAll !== "function") return;
      AD_SELECTORS.forEach((selector) => {
        try {
          root.querySelectorAll(selector).forEach((el) => el.remove());
        } catch (_) {
        }
      });
    };
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          if (node.nodeType !== 1) continue;
          const src = String(node.src || node.href || "").toLowerCase();
          if (isAdUrl(src) || node.tagName === "IFRAME" && /advert/i.test(node.title || "")) {
            try {
              node.remove();
            } catch (_) {
            }
            continue;
          }
          nuke(node);
        }
      }
    });
    const start = () => {
      nuke(document);
      try {
        observer.observe(document.documentElement || document.body, { childList: true, subtree: true });
      } catch (_) {
      }
    };
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", start, { once: true });
    } else {
      start();
    }
  }

  // src/page/openfront/map-assets.js
  function installMapAssetHook({ bus, logger }) {
    const origFetch = window.fetch;
    if (typeof origFetch !== "function") return;
    let pendingManifest = null;
    let pendingTerrainBin = null;
    let pendingTerrainIs4x = null;
    let mapLoaded = false;
    window.fetch = function patchedFetch() {
      const args = arguments;
      const url = typeof args[0] === "string" ? args[0] : args[0] && args[0].url || "";
      if (/\\/maps\\//.test(url) && /\\/manifest(\\.[a-f0-9]+)?\\.json(\\?|$)/.test(url)) {
        return origFetch.apply(this, args).then((response) => {
          response.clone().json().then((manifest) => {
            pendingManifest = manifest;
            tryAssembleMap();
          }).catch(() => {
          });
          return response;
        });
      }
      if (/\\/maps\\//.test(url) && /\\/map(4x)?(\\.[a-f0-9]+)?\\.bin(\\?|$)/.test(url)) {
        const is4x = /\\/map4x(\\.[a-f0-9]+)?\\.bin/.test(url);
        return origFetch.apply(this, args).then((response) => {
          response.clone().arrayBuffer().then((buffer) => {
            pendingTerrainBin = new Uint8Array(buffer);
            pendingTerrainIs4x = is4x;
            tryAssembleMap();
          }).catch(() => {
          });
          return response;
        });
      }
      return origFetch.apply(this, args);
    };
    function tryAssembleMap() {
      if (!pendingManifest || !pendingTerrainBin || mapLoaded) return;
      const metaKey = pendingTerrainIs4x ? "map4x" : "map";
      const meta = pendingManifest[metaKey];
      if (!meta) return;
      const width = meta.width;
      const height = meta.height;
      const terrain = pendingTerrainBin;
      if (terrain.length !== width * height) return;
      let nations = pendingManifest.nations || [];
      if (pendingTerrainIs4x) {
        nations = nations.map((nation) => ({
          name: nation.name,
          flag: nation.flag,
          coordinates: [Math.floor(nation.coordinates[0] / 2), Math.floor(nation.coordinates[1] / 2)]
        }));
      }
      mapLoaded = true;
      logger.info(\`Map \${width}x\${height}, \${nations.length} nations\`);
      bus.emit("mapLoaded", { terrain, width, height, nations });
    }
  }

  // src/page/openfront/network-hooks.js
  function installNetworkHooks({ bus, getSmartAttackModifier, logger }) {
    const OrigWS = window.WebSocket;
    const WorkerCtor = window.Worker;
    if (!OrigWS || !OrigWS.prototype) return { OrigWS: null, origWsSend: null };
    const origWsSend = OrigWS.prototype.send;
    installWebSocketHooks(OrigWS, origWsSend, bus, getSmartAttackModifier, logger);
    installWorkerHooks(WorkerCtor, bus);
    return { OrigWS, origWsSend };
  }
  function installWebSocketHooks(OrigWS, origWsSend, bus, getSmartAttackModifier, logger) {
    const origWsAddEL = OrigWS.prototype.addEventListener;
    let wsMessageCount = 0;
    function interceptWsMessage(data) {
      try {
        const message = typeof data === "string" ? JSON.parse(data) : null;
        if (!message || !message.type) return;
        wsMessageCount += 1;
        if (wsMessageCount === 1) logger.info(\`First websocket message: \${message.type}\`);
        bus.emit("wsMessage", message);
      } catch (_) {
      }
    }
    window.WebSocket = function PatchedWebSocket() {
      const args = Array.prototype.slice.call(arguments);
      const ws = new (Function.prototype.bind.apply(OrigWS, [null].concat(args)))();
      const wsUrl = String(args[0] || "");
      origWsAddEL.call(ws, "message", (event) => interceptWsMessage(event.data));
      if (wsUrl.indexOf("/lobbies") === -1 && wsUrl.indexOf("/matchmaking") === -1) {
        bus.emit("socketReady", ws);
        const sendForThisSocket = ws.send.bind(ws);
        ws.send = function patchedSend(data) {
          const modifier = getSmartAttackModifier();
          return sendForThisSocket(modifier ? modifier(data) : data);
        };
        ws.addEventListener("close", () => bus.emit("socketClosed", ws));
      }
      return ws;
    };
    window.WebSocket.prototype = OrigWS.prototype;
    window.WebSocket.CONNECTING = OrigWS.CONNECTING;
    window.WebSocket.OPEN = OrigWS.OPEN;
    window.WebSocket.CLOSING = OrigWS.CLOSING;
    window.WebSocket.CLOSED = OrigWS.CLOSED;
  }
  function installWorkerHooks(WorkerCtor, bus) {
    if (!WorkerCtor || !WorkerCtor.prototype) return;
    const origWorkerAddEL = WorkerCtor.prototype.addEventListener;
    const origWorkerOnmsgDesc = Object.getOwnPropertyDescriptor(WorkerCtor.prototype, "onmessage");
    function interceptWorkerMessage(data) {
      if (!data || data.type !== "game_update_batch" || !data.gameUpdates) return;
      bus.emit("workerGameUpdateBatch", data);
    }
    WorkerCtor.prototype.addEventListener = function patchedWorkerAddEventListener(type, listener) {
      if (type === "message" && typeof listener === "function") {
        const wrapped = function wrappedWorkerMessage(event) {
          interceptWorkerMessage(event.data);
          return listener.apply(this, arguments);
        };
        return origWorkerAddEL.call(this, type, wrapped);
      }
      return origWorkerAddEL.apply(this, arguments);
    };
    if (origWorkerOnmsgDesc && origWorkerOnmsgDesc.set) {
      Object.defineProperty(WorkerCtor.prototype, "onmessage", {
        get: origWorkerOnmsgDesc.get,
        set(fn) {
          if (typeof fn !== "function") {
            origWorkerOnmsgDesc.set.call(this, fn);
            return;
          }
          const wrapped = function wrappedWorkerOnMessage(event) {
            interceptWorkerMessage(event.data);
            return fn.apply(this, arguments);
          };
          origWorkerOnmsgDesc.set.call(this, wrapped);
        },
        configurable: true,
        enumerable: true
      });
    }
  }

  // src/page/openfront/game-state.js
  var GUT_PLAYER = 2;
  function createGameState() {
    const state = {
      myClientID: null,
      myPlayerID: null,
      mySmallID: null,
      currentTick: 0,
      currentTurn: 0,
      gameStarted: false,
      roundStartedAtMs: 0,
      gameSocket: null,
      playerSpawns: /* @__PURE__ */ new Map(),
      playerStates: /* @__PURE__ */ new Map()
    };
    return {
      state,
      handleWsMessage(message, mapData) {
        if (message.type === "start") {
          if (message.myClientID) state.myClientID = message.myClientID;
          state.gameStarted = true;
          state.roundStartedAtMs = performance.now();
          state.currentTurn = 0;
          if (message.turns) message.turns.forEach((turn) => handleTurn(state, turn, mapData));
        } else if (message.type === "turn" && message.turn) {
          handleTurn(state, message.turn, mapData);
        } else if (message.type === "lobby_info" && message.myClientID) {
          state.myClientID = message.myClientID;
        }
      },
      handleWorkerBatch(batch) {
        for (let gi = 0; gi < batch.gameUpdates.length; gi += 1) {
          const gameUpdate = batch.gameUpdates[gi];
          state.currentTick = gameUpdate.tick || state.currentTick;
          if (!gameUpdate.updates) continue;
          const playerUpdates = gameUpdate.updates[GUT_PLAYER];
          if (!playerUpdates) continue;
          for (let pi = 0; pi < playerUpdates.length; pi += 1) {
            const update = playerUpdates[pi];
            if (!update.id) continue;
            const previous = state.playerStates.get(update.id) || { id: update.id };
            const merged = Object.assign({}, previous, update);
            state.playerStates.set(update.id, merged);
            if (merged.clientID && merged.clientID === state.myClientID) {
              state.myPlayerID = merged.id;
              state.mySmallID = merged.smallID;
            }
          }
        }
      },
      resetSocket(socket) {
        if (state.gameSocket !== socket) return;
        state.gameStarted = false;
        state.gameSocket = null;
        state.myPlayerID = null;
        state.mySmallID = null;
        state.currentTick = 0;
        state.currentTurn = 0;
        state.roundStartedAtMs = 0;
        state.playerStates.clear();
      },
      setSocket(socket) {
        state.gameSocket = socket;
      },
      getMyState() {
        return state.myPlayerID ? state.playerStates.get(state.myPlayerID) : null;
      }
    };
  }
  function handleTurn(state, turn, mapData) {
    const turnNumber = Number(turn?.turnNumber ?? turn?.turn ?? turn?.number ?? turn?.id);
    if (Number.isFinite(turnNumber)) state.currentTurn = Math.max(state.currentTurn, turnNumber);
    else state.currentTurn += 1;
    extractSpawnIntents(state, turn, mapData);
  }
  function extractSpawnIntents(state, turn, mapData) {
    if (!turn || !turn.intents || !mapData || !mapData.width) return;
    for (let i = 0; i < turn.intents.length; i += 1) {
      const intent = turn.intents[i];
      if (intent.type !== "spawn" || !intent.clientID || intent.clientID === state.myClientID) continue;
      const x = intent.tile % mapData.width;
      const y = Math.floor(intent.tile / mapData.width);
      state.playerSpawns.set(intent.clientID, { x, y });
    }
  }

  // src/page/openfront/game-view-adapter.js
  var GAME_VIEW_SELECTORS = ["leader-board", "player-info-overlay", "control-panel", "spawn-timer"];
  var GAME_VIEW_PROPS = ["game", "g"];
  var cachedGameView = null;
  var cachedGameViewSource = null;
  var capturedGameView = null;
  var captureInstalled = false;
  function installGameViewCapture() {
    if (captureInstalled) return;
    captureInstalled = true;
    try {
      const HtmlProto = typeof HTMLElement !== "undefined" ? HTMLElement.prototype : null;
      GAME_VIEW_PROPS.forEach((prop) => patchGameProperty(HtmlProto, prop));
      if (window.customElements && typeof window.customElements.define === "function" && !window.customElements.__ofatGameCapture) {
        const nativeDefine = window.customElements.define.bind(window.customElements);
        window.customElements.define = function patchedDefine(name, ctor, options) {
          try {
            if (ctor && ctor.prototype) GAME_VIEW_PROPS.forEach((prop) => patchGameProperty(ctor.prototype, prop));
          } catch (_) {
          }
          return nativeDefine(name, ctor, options);
        };
        Object.defineProperty(window.customElements, "__ofatGameCapture", { configurable: true, value: true });
      }
    } catch (_) {
    }
  }
  function patchGameProperty(proto, prop) {
    const patchedKey = \`__ofat_\${prop}_patched\`;
    if (!proto || Object.prototype.hasOwnProperty.call(proto, patchedKey)) return;
    let desc = null;
    let cursor = proto;
    while (cursor && cursor !== Object.prototype) {
      desc = Object.getOwnPropertyDescriptor(cursor, prop);
      if (desc) break;
      cursor = Object.getPrototypeOf(cursor);
    }
    const storageKey = Symbol(\`ofat_\${prop}\`);
    try {
      Object.defineProperty(proto, prop, {
        configurable: true,
        get() {
          if (desc && typeof desc.get === "function") return desc.get.call(this);
          return this[storageKey];
        },
        set(value) {
          if (looksLikeGameView(value)) capturedGameView = value;
          if (desc && typeof desc.set === "function") desc.set.call(this, value);
          else this[storageKey] = value;
        }
      });
      Object.defineProperty(proto, patchedKey, { configurable: true, value: true });
    } catch (_) {
    }
  }
  function looksLikeGameView(value) {
    return !!value && typeof value === "object" && typeof value.myPlayer === "function" && (typeof value.units === "function" || typeof value.unitStates === "function" || typeof value.players === "function" || typeof value.playerViews === "function");
  }
  function getGameView() {
    if (isValidGameView(cachedGameView)) return cachedGameView;
    cachedGameView = null;
    cachedGameViewSource = null;
    if (isValidGameView(capturedGameView)) {
      cachedGameView = capturedGameView;
      cachedGameViewSource = "captured";
      return capturedGameView;
    }
    try {
      for (let i = 0; i < GAME_VIEW_SELECTORS.length; i += 1) {
        const selector = GAME_VIEW_SELECTORS[i];
        const element = document.querySelector(selector);
        const candidate = element && element.game;
        if (!isValidGameView(candidate)) continue;
        cachedGameView = candidate;
        cachedGameViewSource = selector;
        return candidate;
      }
    } catch (_) {
    }
    return null;
  }
  function getGameViewDiscovery() {
    const gameView = getGameView();
    if (!gameView) return null;
    return {
      source: cachedGameViewSource || "unknown",
      hasConfig: typeof gameView.config === "function",
      hasMyPlayer: typeof gameView.myPlayer === "function",
      hasPlayerViews: typeof gameView.playerViews === "function",
      hasOwner: typeof gameView.owner === "function",
      hasNeighbors: typeof gameView.neighbors === "function"
    };
  }
  function resetGameViewCache() {
    cachedGameView = null;
    cachedGameViewSource = null;
    capturedGameView = null;
  }
  function getMyTroopRatio() {
    try {
      const gameView = getGameView();
      if (!gameView || typeof gameView.myPlayer !== "function" || typeof gameView.config !== "function") return null;
      const me = gameView.myPlayer();
      if (!me || typeof me.troops !== "function") return null;
      const config = gameView.config();
      if (!config || typeof config.maxTroops !== "function") return null;
      const troops = me.troops();
      const maxTroops = config.maxTroops(me);
      if (!Number.isFinite(troops) || !Number.isFinite(maxTroops) || maxTroops <= 0) return null;
      return { troops, maxTroops, ratio: troops / maxTroops };
    } catch (_) {
      return null;
    }
  }
  function isValidGameView(candidate) {
    if (!candidate) return false;
    return typeof candidate.myPlayer === "function" && typeof candidate.config === "function" && (typeof candidate.owner === "function" || typeof candidate.playerViews === "function" || typeof candidate.neighbors === "function");
  }
  function createNeighbourFetcher() {
    let inFlight = null;
    let cachedNeighbourIDs = null;
    let cachedHasOpenFrontier = false;
    return {
      get cachedNeighbourIDs() {
        return cachedNeighbourIDs;
      },
      // true, wenn an meine Grenze unbeanspruchtes Land / TerraNullius ("Wildnis") grenzt.
      get cachedHasOpenFrontier() {
        return cachedHasOpenFrontier;
      },
      refresh() {
        if (inFlight) return inFlight;
        const gameView = getGameView();
        if (!gameView || typeof gameView.myPlayer !== "function") return Promise.resolve(null);
        const me = gameView.myPlayer();
        if (!me || typeof me.borderTiles !== "function") return Promise.resolve(null);
        inFlight = me.borderTiles().then((info) => {
          const neighbourIDs = /* @__PURE__ */ new Set();
          let hasOpenFrontier = false;
          const myID = typeof me.id === "function" ? me.id() : null;
          const borders = info && info.borderTiles;
          if (borders && typeof borders.forEach === "function") {
            borders.forEach((borderTile) => {
              const adjacent = gameView.neighbors ? gameView.neighbors(borderTile) : [];
              for (let i = 0; i < adjacent.length; i += 1) {
                const tile = adjacent[i];
                if (gameView.hasOwner && !gameView.hasOwner(tile)) {
                  hasOpenFrontier = true;
                  continue;
                }
                const owner = gameView.owner ? gameView.owner(tile) : null;
                if (!owner) {
                  hasOpenFrontier = true;
                  continue;
                }
                const ownerIsPlayer = typeof owner.isPlayer === "function" ? owner.isPlayer() : false;
                if (!ownerIsPlayer) {
                  hasOpenFrontier = true;
                  continue;
                }
                const ownerID = typeof owner.id === "function" ? owner.id() : null;
                if (!ownerID || ownerID === myID) continue;
                neighbourIDs.add(ownerID);
              }
            });
          }
          cachedNeighbourIDs = neighbourIDs;
          cachedHasOpenFrontier = hasOpenFrontier;
          return neighbourIDs;
        }).catch(() => null).then((result) => {
          inFlight = null;
          return result;
        });
        return inFlight;
      }
    };
  }

  // src/page/openfront/openfront-mechanics.js
  var TICK_MS = 100;
  var GOLD_RESERVE = 25e3;
  var BASE_STRUCTURE_COST = 125e3;
  var MAX_ECO_STRUCTURE_COST = 1e6;
  var UNIT_FALLBACKS = Object.freeze({
    City: { durationMs: 2e3 },
    Port: { durationMs: 5e3 },
    Factory: { durationMs: 2e3 },
    "Defense Post": { durationMs: 5e3 },
    "Missile Silo": { cost: 1e6, durationMs: 1e4 },
    "SAM Launcher": { durationMs: 3e4 }
  });
  var SPAWN_TURNS_NORMAL = 300;
  var SPAWN_TURNS_RANDOM = 150;
  var SPAWN_TURNS_SINGLEPLAYER = 100;
  function getStructureCost(unit, { gameView = null, player = null, pendingCounts = {}, observedCounts = {} } = {}) {
    const runtime = readRuntimeUnitCost(unit, gameView, player);
    if (runtime != null) return { cost: runtime, source: "runtime", reserveGold: GOLD_RESERVE };
    const cost = fallbackStructureCost(unit, player, pendingCounts, observedCounts);
    return { cost, source: "official_fallback", reserveGold: GOLD_RESERVE };
  }
  function getConstructionDurationMs(unit, { gameView = null } = {}) {
    try {
      const info = readUnitInfo(unit, gameView);
      const raw = info && (info.constructionDuration ?? info.buildTime ?? info.duration);
      const value = toFiniteNumber(typeof raw === "function" ? raw() : raw);
      if (value != null && value > 0) return { durationMs: Math.max(500, value * TICK_MS), source: "runtime" };
    } catch (_) {
    }
    return { durationMs: UNIT_FALLBACKS[unit]?.durationMs || 5e3, source: "official_fallback" };
  }
  function getSpawnPhaseInfo({ gameView = null, state = null, teamMode = false } = {}) {
    const runtimeTurns = readRuntimeSpawnPhaseTurns(gameView);
    const totalTurns = runtimeTurns || fallbackSpawnPhaseTurns(gameView);
    const source = runtimeTurns ? "runtime" : "official_fallback";
    const currentTurn = Number(state?.currentTurn) || 0;
    const elapsedTurns = Math.max(0, currentTurn);
    const remainingTurns = Math.max(0, totalTurns - elapsedTurns);
    const totalMs = totalTurns * TICK_MS;
    const elapsedMs = state?.roundStartedAtMs ? performance.now() - state.roundStartedAtMs : elapsedTurns * TICK_MS;
    const waitCapMs = teamMode ? Math.min(1e4, totalMs * 0.4) : 0;
    return {
      totalTurns,
      currentTurn,
      elapsedTurns,
      remainingTurns,
      totalMs,
      elapsedMs,
      remainingMs: remainingTurns * TICK_MS,
      waitCapMs,
      source
    };
  }
  function estimateOfficialCaptureCost({ target, effectiveTargetTroops = 0, mapData = null, sampleTiles = [] } = {}) {
    const targetTiles = Math.max(1, toFiniteNumber(target?.tilesOwned) || 1);
    const density = Math.max(0, effectiveTargetTroops / targetTiles);
    const terrain = estimateTerrainCost(mapData, sampleTiles);
    const type = String(target?.playerType || "").toUpperCase();
    const botModifier = type === "BOT" ? 0.7 : 1;
    const defensePostMultiplier = estimateDefensePostMultiplier(target);
    const tileCost = Math.ceil((density + terrain.magnitude) * botModifier * defensePostMultiplier);
    const captureCostEstimate = Math.ceil(tileCost * targetTiles);
    const estimatedCaptureTurns = Math.max(1, Math.ceil(targetTiles / Math.max(1, terrain.speed)));
    return {
      source: "official_fallback",
      captureCostEstimate,
      tileCost,
      terrainClass: terrain.kind,
      terrainMagnitude: terrain.magnitude,
      terrainSpeed: terrain.speed,
      defensePostMultiplier,
      estimatedCaptureTurns
    };
  }
  function toFiniteNumber(value) {
    try {
      if (typeof value === "bigint") {
        const number2 = Number(value);
        return Number.isFinite(number2) ? number2 : null;
      }
      const number = Number(value);
      return Number.isFinite(number) ? number : null;
    } catch (_) {
      return null;
    }
  }
  function countPlayerUnits(player, unit) {
    try {
      if (player && typeof player.units === "function") {
        const units = player.units(unit);
        if (Array.isArray(units)) return units.length;
        if (units && typeof units.length === "number") return units.length;
      }
    } catch (_) {
    }
    return 0;
  }
  function readRuntimeUnitCost(unit, gameView, player) {
    try {
      const info = readUnitInfo(unit, gameView);
      if (!info) return null;
      const raw = typeof info.cost === "function" ? tryCallCost(info.cost, gameView, player) : info.cost;
      const value = toFiniteNumber(raw);
      return value != null && value > 0 ? Math.ceil(value) : null;
    } catch (_) {
      return null;
    }
  }
  function tryCallCost(costFn, gameView, player) {
    const candidates = [
      () => costFn(gameView, player),
      () => costFn(player),
      () => costFn(gameView),
      () => costFn()
    ];
    for (let i = 0; i < candidates.length; i += 1) {
      try {
        const value = candidates[i]();
        if (value != null) return value;
      } catch (_) {
      }
    }
    return null;
  }
  function readUnitInfo(unit, gameView) {
    try {
      const config = gameView && typeof gameView.config === "function" ? gameView.config() : null;
      if (!config) return null;
      if (typeof config.unitInfo === "function") return config.unitInfo(unit);
      if (typeof config.structureInfo === "function") return config.structureInfo(unit);
    } catch (_) {
    }
    return null;
  }
  function fallbackStructureCost(unit, player, pendingCounts, observedCounts) {
    const pending = pendingCounts || {};
    const observed = observedCounts || {};
    if (unit === "City") {
      const count = Math.max(countPlayerUnits(player, "City"), observed.City || 0) + (pending.City || 0);
      return Math.min(MAX_ECO_STRUCTURE_COST, Math.pow(2, count) * BASE_STRUCTURE_COST);
    }
    if (unit === "Port" || unit === "Factory") {
      const actualShared = countPlayerUnits(player, "Port") + countPlayerUnits(player, "Factory");
      const observedShared = (observed.Port || 0) + (observed.Factory || 0);
      const count = Math.max(actualShared, observedShared) + (pending.Port || 0) + (pending.Factory || 0);
      return Math.min(MAX_ECO_STRUCTURE_COST, Math.pow(2, count) * BASE_STRUCTURE_COST);
    }
    if (unit === "Defense Post") {
      const count = Math.max(countPlayerUnits(player, "Defense Post"), observed["Defense Post"] || 0) + (pending["Defense Post"] || 0);
      return Math.min(25e4, (count + 1) * 5e4);
    }
    if (unit === "SAM Launcher") {
      const count = Math.max(countPlayerUnits(player, "SAM Launcher"), observed["SAM Launcher"] || 0) + (pending["SAM Launcher"] || 0);
      return Math.min(3e6, (count + 1) * 15e5);
    }
    return UNIT_FALLBACKS[unit]?.cost || BASE_STRUCTURE_COST;
  }
  function readRuntimeSpawnPhaseTurns(gameView) {
    try {
      const config = gameView && typeof gameView.config === "function" ? gameView.config() : null;
      if (config && typeof config.numSpawnPhaseTurns === "function") {
        const value = toFiniteNumber(config.numSpawnPhaseTurns());
        if (value != null && value > 0) return Math.round(value);
      }
    } catch (_) {
    }
    return null;
  }
  function fallbackSpawnPhaseTurns(gameView) {
    try {
      const config = gameView && typeof gameView.config === "function" ? gameView.config() : null;
      const gameConfig = config && typeof config.gameConfig === "function" ? config.gameConfig() : null;
      const mode = String(gameConfig?.gameMode || "").toLowerCase();
      if (mode.includes("single")) return SPAWN_TURNS_SINGLEPLAYER;
      if (gameConfig?.randomSpawn || mode.includes("random")) return SPAWN_TURNS_RANDOM;
    } catch (_) {
    }
    return SPAWN_TURNS_NORMAL;
  }
  function estimateTerrainCost(mapData, sampleTiles) {
    const tiles = Array.isArray(sampleTiles) ? sampleTiles.slice(0, 64) : [];
    let magnitude = 0;
    let count = 0;
    tiles.forEach((tile) => {
      const byte = mapData?.terrain ? mapData.terrain[Number(tile)] : null;
      if (byte == null) return;
      const mag = byte & 31;
      if (mag < 10) magnitude += 80;
      else if (mag < 20) magnitude += 100;
      else magnitude += 120;
      count += 1;
    });
    const avg = count > 0 ? magnitude / count : 100;
    if (avg <= 85) return { kind: "plains", magnitude: 80, speed: 16.5 };
    if (avg <= 105) return { kind: "highland", magnitude: 100, speed: 20 };
    return { kind: "mountain", magnitude: 120, speed: 25 };
  }
  function estimateDefensePostMultiplier(target) {
    try {
      const count = countPlayerUnits(target, "Defense Post");
      return count > 0 ? 1.4 : 1;
    } catch (_) {
      return 1;
    }
  }

  // src/page/openfront/team-detection.js
  function createTeamDetection({ gameState, logger, roundLogger }) {
    let isTeamMode = false;
    let myTeammateIDs = /* @__PURE__ */ new Set();
    let lobby = null;
    let lobbyObserver = null;
    let lobbyCaptured = false;
    let announced = false;
    const api = {
      get isTeamMode() {
        return isTeamMode;
      },
      get myTeamName() {
        return lobby?.myTeamName || (isTeamMode ? "Team" : null);
      },
      get myTeamColor() {
        return lobby?.myTeamColor || null;
      },
      observeLobby() {
        armLobbyObserver();
      },
      syncFromGameView() {
        const gv = getGameView();
        const mode = readGameMode(gv);
        if (mode != null) isTeamMode = mode === "Team";
        else if (lobby) isTeamMode = isTeamMode || lobby.isTeamMode;
        if (!isTeamMode) {
          if (myTeammateIDs.size) myTeammateIDs = /* @__PURE__ */ new Set();
          maybeAnnounce();
          return;
        }
        const next = /* @__PURE__ */ new Set();
        try {
          const me = gv && typeof gv.myPlayer === "function" ? gv.myPlayer() : null;
          const views = gv && typeof gv.playerViews === "function" ? gv.playerViews() : [];
          if (me && Array.isArray(views)) {
            views.forEach((pv) => {
              try {
                if (typeof pv.isOnSameTeam === "function" && pv.isOnSameTeam(me)) {
                  const id = typeof pv.id === "function" ? pv.id() : null;
                  if (id != null) next.add(id);
                }
              } catch (_) {
              }
            });
          }
        } catch (_) {
        }
        myTeammateIDs = next;
        maybeAnnounce();
      },
      isMyTeammate(playerID) {
        if (!isTeamMode || playerID == null) return false;
        if (myTeammateIDs.has(playerID)) return true;
        if (myTeammateIDs.size === 0 && lobby && lobby.myTeamName) {
          const player = gameState.state.playerStates.get(playerID);
          const name = normName(player?.name);
          if (name && lobby.playerNameToTeam[name] === lobby.myTeamName) return true;
        }
        return false;
      },
      getMyTeamMembers() {
        const members = [];
        const seenNames = /* @__PURE__ */ new Set();
        myTeammateIDs.forEach((id) => {
          const player = gameState.state.playerStates.get(id);
          const name = normName(player?.name) || String(id);
          seenNames.add(name);
          members.push({ id, name });
        });
        if (lobby) {
          lobby.myTeamPlayers.forEach((name) => {
            if (!seenNames.has(name)) members.push({ id: null, name });
          });
        }
        return members;
      },
      memberCount() {
        if (myTeammateIDs.size) return myTeammateIDs.size;
        return lobby ? lobby.myTeamPlayers.length : 0;
      },
      // clientIDs meiner Teammitglieder. Br\xFCcke f\xFCr Spawn-Daten (per clientID verschl\xFCsselt),
      // damit das Spawn-Scoring Team- von Gegner-Spawns trennen kann.
      myTeammateClientIDs() {
        const clients = /* @__PURE__ */ new Set();
        if (!isTeamMode) return clients;
        gameState.state.playerStates.forEach((player) => {
          if (!player || player.clientID == null) return;
          if (api.isMyTeammate(player.id)) clients.add(player.clientID);
        });
        return clients;
      },
      teamSummary() {
        return {
          isTeamMode,
          myTeamName: api.myTeamName,
          myTeamColor: api.myTeamColor,
          memberCount: api.memberCount()
        };
      },
      reset() {
        myTeammateIDs = /* @__PURE__ */ new Set();
        lobby = null;
        lobbyCaptured = false;
        isTeamMode = false;
        announced = false;
        if (lobbyObserver) {
          lobbyObserver.disconnect();
          lobbyObserver = null;
        }
        armLobbyObserver();
      }
    };
    return api;
    function maybeAnnounce() {
      if (announced || !isTeamMode) return;
      const count = myTeammateIDs.size || (lobby ? lobby.myTeamPlayers.length : 0);
      if (count <= 0) return;
      announced = true;
      roundLogger?.record("team_detected", {
        source: myTeammateIDs.size ? "gameview" : "lobby",
        myTeamName: lobby?.myTeamName || null,
        myTeamColor: lobby?.myTeamColor || null,
        memberCount: count
      });
    }
    function armLobbyObserver() {
      if (lobbyObserver || lobbyCaptured) return;
      if (tryCaptureLobby()) return;
      try {
        lobbyObserver = new MutationObserver(() => {
          if (tryCaptureLobby() && lobbyObserver) {
            lobbyObserver.disconnect();
            lobbyObserver = null;
          }
        });
        const root = document.body || document.documentElement;
        if (root) lobbyObserver.observe(root, { childList: true, subtree: true });
      } catch (_) {
      }
    }
    function tryCaptureLobby() {
      if (lobbyCaptured) return true;
      let root = null;
      try {
        root = document.querySelector("lobby-player-view");
      } catch (_) {
      }
      if (!root) return false;
      const parsed = parseLobbyTeams(root);
      if (!parsed) return false;
      lobby = parsed;
      lobbyCaptured = true;
      if (parsed.isTeamMode) isTeamMode = true;
      logger?.info?.(\`Team lobby parsed: my team \${parsed.myTeamName || "?"} (\${parsed.myTeamPlayers.length} players)\`);
      return true;
    }
  }
  function readGameMode(gv) {
    try {
      if (!gv || typeof gv.config !== "function") return null;
      const config = gv.config();
      if (!config || typeof config.gameConfig !== "function") return null;
      const gameConfig = config.gameConfig();
      return gameConfig ? gameConfig.gameMode : null;
    } catch (_) {
      return null;
    }
  }
  function parseLobbyTeams(root) {
    const teamNameToPlayers = {};
    const teamColorMap = {};
    const playerNameToTeam = {};
    let myTeamName = null;
    let myTeamColor = null;
    let dots = [];
    try {
      dots = Array.from(root.querySelectorAll("span[style*='--bg']"));
    } catch (_) {
      return null;
    }
    dots.forEach((dot) => {
      const header = dot.parentElement;
      const card = header ? header.parentElement : null;
      if (!header || !card) return;
      const nameSpan = header.querySelector("span.truncate");
      const teamName = nameSpan ? nameSpan.textContent.trim() : "";
      if (!teamName) return;
      const styleAttr = dot.getAttribute("style") || "";
      const colorMatch = styleAttr.match(/--bg:\\s*([^;]+)/);
      const color = colorMatch ? colorMatch[1].trim() : null;
      teamColorMap[teamName] = color;
      let isMine = isHighlighted(card.className);
      const players = [];
      let memberSpans = [];
      try {
        memberSpans = Array.from(card.querySelectorAll("span.truncate.text-white"));
      } catch (_) {
      }
      memberSpans.forEach((span) => {
        const pname = span.textContent.trim();
        if (!pname) return;
        players.push(pname);
        playerNameToTeam[pname] = teamName;
        const row = span.parentElement;
        if (row && isHighlighted(row.className)) isMine = true;
      });
      teamNameToPlayers[teamName] = players;
      if (isMine) {
        myTeamName = teamName;
        myTeamColor = color;
      }
    });
    const teamCount = Object.keys(teamNameToPlayers).length;
    if (teamCount === 0) return null;
    return {
      isTeamMode: teamCount > 0,
      myTeamName,
      myTeamColor,
      myTeamPlayers: myTeamName ? teamNameToPlayers[myTeamName] || [] : [],
      playerNameToTeam,
      teamNameToPlayers,
      teamColorMap
    };
  }
  function isHighlighted(className) {
    const cls = String(className || "");
    return cls.includes("bg-malibu-blue") || cls.includes("border-sky-");
  }
  function normName(name) {
    return String(name == null ? "" : name).trim();
  }

  // src/page/advisor/spawn-scoring.js
  var ANALYSIS_RADIUS = 30;
  var NATION_ATTRACT_RADIUS = 120;
  var PLAYER_REPEL_RADIUS = 150;
  var GRID_STEP = 16;
  var TOP_N = 5;
  var W_LAND = 0.25;
  var W_PLAINS = 0.2;
  var W_NATION = 0.25;
  var W_PLAYER_DIST = 0.25;
  var W_EDGE = 0.05;
  var W_TEAM = 0.2;
  var TEAM_ATTRACT_RADIUS = 110;
  var TEAM_MIN_GAP = ANALYSIS_RADIUS;
  var TEAM_IDEAL_MAX = 90;
  var TEAM_OVERLAP_MAX_SCORE = 0.4;
  var TEAM_NEAR_TAG_THRESHOLD = 0.6;
  var IS_LAND_BIT = 7;
  var MAGNITUDE_MASK = 31;
  function isLandByte(byte) {
    return (byte & 1 << IS_LAND_BIT) !== 0;
  }
  function isPlainsByte(byte) {
    return isLandByte(byte) && (byte & MAGNITUDE_MASK) < 10;
  }
  function precomputeStaticScores(mapData) {
    const { terrain, width, height, nations } = mapData;
    const candidates = [];
    const radius = ANALYSIS_RADIUS;
    const radius2 = radius * radius;
    for (let cy = radius; cy < height - radius; cy += GRID_STEP) {
      for (let cx = radius; cx < width - radius; cx += GRID_STEP) {
        if (!isLandByte(terrain[cy * width + cx])) continue;
        let landCount = 0;
        let plainsCount = 0;
        let totalChecked = 0;
        for (let dy = -radius; dy <= radius; dy += 2) {
          for (let dx = -radius; dx <= radius; dx += 2) {
            if (dx * dx + dy * dy > radius2) continue;
            const nx = cx + dx;
            const ny = cy + dy;
            if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
            totalChecked += 1;
            const byte = terrain[ny * width + nx];
            if (isLandByte(byte)) {
              landCount += 1;
              if (isPlainsByte(byte)) plainsCount += 1;
            }
          }
        }
        const landDensity = totalChecked > 0 ? landCount / totalChecked : 0;
        const plainsRatio = landCount > 0 ? plainsCount / landCount : 0;
        const nationScore = computeNationScore(cx, cy, nations);
        const edgeX = Math.min(cx, width - cx) / (width / 2);
        const edgeY = Math.min(cy, height - cy) / (height / 2);
        const edgeScore = Math.min(edgeX, edgeY);
        const staticScore = landDensity * W_LAND + plainsRatio * W_PLAINS + nationScore * W_NATION + edgeScore * W_EDGE;
        candidates.push({ x: cx, y: cy, staticScore, landDensity, plainsRatio, nationScore, edgeScore });
      }
    }
    candidates.sort((a, b) => b.staticScore - a.staticScore);
    return candidates;
  }
  function rankSpawnCandidates(staticCandidates, playerSpawns, options = {}) {
    const teammateClientIDs = options.teammateClientIDs;
    const teamMode = !!(teammateClientIDs && teammateClientIDs.size > 0);
    const enemySpawns = [];
    const teamSpawns = [];
    playerSpawns.forEach((pos, clientID) => {
      if (teamMode && teammateClientIDs.has(clientID)) teamSpawns.push(pos);
      else enemySpawns.push(pos);
    });
    const repelR2 = PLAYER_REPEL_RADIUS * PLAYER_REPEL_RADIUS;
    const scored = [];
    for (let i = 0; i < staticCandidates.length; i += 1) {
      const candidate = staticCandidates[i];
      let playerDistScore = 1;
      if (enemySpawns.length > 0) {
        const minD2 = nearestDist2(candidate, enemySpawns);
        if (minD2 < repelR2) playerDistScore = minD2 / repelR2;
      }
      let teamAttractScore = 0;
      if (teamSpawns.length > 0) {
        teamAttractScore = teamAttract(Math.sqrt(nearestDist2(candidate, teamSpawns)));
      }
      const score = teamMode ? candidate.staticScore + playerDistScore * W_PLAYER_DIST + teamAttractScore * W_TEAM : candidate.staticScore + playerDistScore * W_PLAYER_DIST;
      scored.push({
        x: candidate.x,
        y: candidate.y,
        score,
        landDensity: candidate.landDensity,
        plainsRatio: candidate.plainsRatio,
        nationScore: candidate.nationScore,
        edgeScore: candidate.edgeScore,
        playerDistScore,
        teamAttractScore
      });
    }
    scored.sort((a, b) => b.score - a.score);
    return { scores: scored, topSpots: pickDiverseTopSpots(scored) };
  }
  function nearestDist2(candidate, spawns) {
    let minD2 = Infinity;
    for (let j = 0; j < spawns.length; j += 1) {
      const dx = candidate.x - spawns[j].x;
      const dy = candidate.y - spawns[j].y;
      const d2 = dx * dx + dy * dy;
      if (d2 < minD2) minD2 = d2;
    }
    return minD2;
  }
  function teamAttract(dist) {
    if (dist >= TEAM_ATTRACT_RADIUS) return 0;
    if (dist <= TEAM_MIN_GAP) return dist / TEAM_MIN_GAP * TEAM_OVERLAP_MAX_SCORE;
    if (dist <= TEAM_IDEAL_MAX) return 1;
    return (TEAM_ATTRACT_RADIUS - dist) / (TEAM_ATTRACT_RADIUS - TEAM_IDEAL_MAX);
  }
  function describeSpawnSpot(spot) {
    const tags = [];
    if (spot.landDensity >= 0.82) tags.push("solid land");
    if (spot.plainsRatio >= 0.65) tags.push("green growth");
    if (spot.nationScore >= 0.65) tags.push("nation farm");
    if (spot.playerDistScore >= 0.85) tags.push("low crowd");
    if (spot.teamAttractScore >= TEAM_NEAR_TAG_THRESHOLD) tags.push("near team");
    if (spot.edgeScore < 0.35) tags.push("edge risk");
    return tags.length ? tags.join(", ") : "balanced";
  }
  function computeNationScore(cx, cy, nations) {
    if (!nations.length) return 0;
    let minD2 = Infinity;
    for (let i = 0; i < nations.length; i += 1) {
      const dx = cx - nations[i].coordinates[0];
      const dy = cy - nations[i].coordinates[1];
      const d2 = dx * dx + dy * dy;
      if (d2 < minD2) minD2 = d2;
    }
    return Math.max(0, 1 - Math.sqrt(minD2) / NATION_ATTRACT_RADIUS);
  }
  function pickDiverseTopSpots(scored) {
    const topSpots = [];
    const minD2 = ANALYSIS_RADIUS * 2 * (ANALYSIS_RADIUS * 2);
    for (let i = 0; i < scored.length && topSpots.length < TOP_N; i += 1) {
      const spot = scored[i];
      let tooClose = false;
      for (let j = 0; j < topSpots.length; j += 1) {
        const dx = topSpots[j].x - spot.x;
        const dy = topSpots[j].y - spot.y;
        if (dx * dx + dy * dy < minD2) {
          tooClose = true;
          break;
        }
      }
      if (!tooClose) topSpots.push(spot);
    }
    return topSpots;
  }

  // src/page/openfront/player-metrics.js
  function troopsInCombat(player) {
    return (player?.outgoingAttacks || []).reduce(
      (sum, attack) => sum + (attack && !attack.retreating ? Number(attack.troops || 0) : 0),
      0
    );
  }
  function incomingAttackTroops(gameState, myState) {
    const mySmallID = myState?.smallID != null ? myState.smallID : gameState.state.mySmallID;
    return incomingAttackTroopsForSmallID(gameState, mySmallID, myState?.id);
  }
  function incomingAttackTroopsForSmallID(gameState, targetSmallID, ownPlayerID = null) {
    if (targetSmallID == null) return 0;
    let sum = 0;
    gameState.state.playerStates.forEach((player) => {
      if (!player || ownPlayerID != null && player.id === ownPlayerID) return;
      (player.outgoingAttacks || []).forEach((attack) => {
        if (attack && !attack.retreating && attack.targetID === targetSmallID) sum += Number(attack.troops || 0);
      });
    });
    return sum;
  }
  function incomingAttackers(gameState, myState) {
    const mySmallID = myState?.smallID != null ? myState.smallID : gameState.state.mySmallID;
    if (mySmallID == null) return [];
    const ownPlayerID = myState?.id;
    const attackers = [];
    gameState.state.playerStates.forEach((player) => {
      if (!player || ownPlayerID != null && player.id === ownPlayerID) return;
      let troops = 0;
      (player.outgoingAttacks || []).forEach((attack) => {
        if (attack && !attack.retreating && attack.targetID === mySmallID) troops += Number(attack.troops || 0);
      });
      if (troops > 0) {
        attackers.push({ id: player.id, smallID: player.smallID, name: player.name || player.id, troops });
      }
    });
    return attackers.sort((a, b) => b.troops - a.troops);
  }

  // src/page/advisor/threat-model.js
  var ATOM_COST = 75e4;
  var HYDROGEN_COST = 5e6;
  var NUKE_SOON_SEC = 120;
  function evaluateThreats(gameState, teamDetection, nukeBuilders = null, goldIntel = null) {
    const myState = gameState.getMyState();
    const threats = /* @__PURE__ */ new Map();
    if (!myState || !myState.troops) return threats;
    gameState.state.playerStates.forEach((player) => {
      if (!player || !player.isAlive || player.id === myState.id) return;
      if (teamDetection?.isMyTeammate(player.id)) return;
      const troopsRatio = (player.troops || 0) / Math.max(1, myState.troops || 0);
      const tilesRatio = (player.tilesOwned || 0) / Math.max(1, myState.tilesOwned || 1);
      const outgoing = troopsInCombat(player);
      const activeAttackPressure = outgoing / Math.max(1, player.troops || 1);
      const nukePotential = player.gold >= HYDROGEN_COST ? 2 : player.gold >= ATOM_COST ? 1 : 0;
      const buildingNuke = !!(nukeBuilders && nukeBuilders.has(player.id));
      const nukeEtaSec = nukePotential === 0 && goldIntel ? goldIntel.timeToAfford(player.id, ATOM_COST, player.gold) : null;
      const nukeSoon = nukeEtaSec != null && nukeEtaSec > 0 && nukeEtaSec <= NUKE_SOON_SEC;
      let score = troopsRatio * 0.45 + tilesRatio * 0.25 + activeAttackPressure * 0.15 + nukePotential * 0.25;
      if (buildingNuke) score += 0.55;
      if (nukeSoon) score += 0.2;
      threats.set(player.id, {
        id: player.id,
        name: player.name || player.id,
        score,
        level: classifyThreat(score),
        troopsRatio,
        tilesRatio,
        nukePotential,
        buildingNuke,
        nukeSoon,
        nukeEtaSec: nukeEtaSec != null ? Math.round(nukeEtaSec) : null
      });
    });
    return threats;
  }
  function classifyThreat(score) {
    if (score >= 1.65) return "Critical";
    if (score >= 1.1) return "Dangerous";
    if (score >= 0.65) return "Neutral";
    return "Weak";
  }

  // src/shared/units.js
  var UNIT = Object.freeze({
    CITY: "City",
    DEFENSE_POST: "Defense Post",
    PORT: "Port",
    SAM_LAUNCHER: "SAM Launcher",
    MISSILE_SILO: "Missile Silo",
    FACTORY: "Factory",
    WARSHIP: "Warship",
    ATOM_BOMB: "Atom Bomb",
    HYDROGEN_BOMB: "Hydrogen Bomb",
    MIRV: "MIRV"
  });
  var WEAPONS_BY_POWER = Object.freeze([UNIT.MIRV, UNIT.HYDROGEN_BOMB, UNIT.ATOM_BOMB]);

  // src/page/openfront/unit-intel.js
  var MISSILE_SILO = UNIT.MISSILE_SILO;
  var SAM_LAUNCHER = UNIT.SAM_LAUNCHER;
  function scanUnitsByType(gameView, unitType, myState, teamDetection) {
    const out = [];
    if (!gameView) return out;
    const seen = /* @__PURE__ */ new Set();
    const myID = myState?.id;
    const mySmallID = myState?.smallID;
    const collect = (unit) => {
      if (!unit) return;
      const id = typeof unit.id === "function" ? unit.id() : null;
      if (id != null && seen.has(id)) return;
      if (id != null) seen.add(id);
      const underConstruction = typeof unit.isUnderConstruction === "function" ? !!unit.isUnderConstruction() : false;
      const owner = typeof unit.owner === "function" ? safeCall(() => unit.owner()) : null;
      const ownerID = owner ? typeof owner.id === "function" ? safeCall(() => owner.id()) : owner.id : null;
      const isMine = ownerID != null && (ownerID === myID || ownerID === mySmallID);
      const isAlly = !isMine && ownerID != null && !!teamDetection?.isMyTeammate?.(ownerID);
      const tile = safeCall(() => typeof unit.tile === "function" ? unit.tile() : null);
      out.push({
        ownerID,
        ownerName: owner ? safeName(owner) : "Unknown",
        isMine,
        isAlly,
        underConstruction,
        tile: tile != null ? tile : null,
        coords: tileCoords(gameView, tile)
      });
    };
    try {
      if (typeof gameView.unitStates === "function" && typeof gameView.unit === "function") {
        for (const state of gameView.unitStates().values()) {
          if (state && state.unitType === unitType) collect(gameView.unit(state.id));
        }
      }
      if (typeof gameView.units === "function") {
        const units = gameView.units(unitType);
        if (units && typeof units.forEach === "function") units.forEach(collect);
      }
    } catch (_) {
    }
    return out;
  }
  function scanMissileSilos(gameView, myState, teamDetection) {
    return scanUnitsByType(gameView, MISSILE_SILO, myState, teamDetection);
  }
  function enemySamSitesUnderConstruction(gameView, myState, teamDetection) {
    return scanUnitsByType(gameView, SAM_LAUNCHER, myState, teamDetection).filter(
      (sam) => !sam.isMine && !sam.isAlly && sam.underConstruction && sam.ownerID != null && sam.tile != null
    );
  }
  function enemyNukeBuilders(siloIntel) {
    const owners = /* @__PURE__ */ new Set();
    (siloIntel || []).forEach((silo) => {
      if (!silo.isMine && !silo.isAlly && silo.underConstruction && silo.ownerID != null) owners.add(silo.ownerID);
    });
    return owners;
  }
  function tileCoords(gameView, tile) {
    try {
      if (tile == null || typeof gameView.x !== "function" || typeof gameView.y !== "function") return null;
      return { x: gameView.x(tile), y: gameView.y(tile) };
    } catch (_) {
      return null;
    }
  }
  function safeName(player) {
    try {
      if (typeof player.displayName === "function") return player.displayName();
      if (typeof player.name === "function") return player.name();
    } catch (_) {
    }
    return "Unknown";
  }
  function safeCall(fn) {
    try {
      return fn();
    } catch (_) {
      return null;
    }
  }

  // src/shared/number.js
  function finiteOrZero(value) {
    const number = Number(value);
    return Number.isFinite(number) ? number : 0;
  }

  // src/page/openfront/gold-intel.js
  var WINDOW_MS = 6e4;
  function createGoldIntel() {
    const history = /* @__PURE__ */ new Map();
    return {
      sample(gameState) {
        const now = Date.now();
        gameState.state.playerStates.forEach((player) => {
          if (!player || player.id == null || !player.isAlive) return;
          const gold = finiteOrZero(typeof player.gold === "bigint" ? Number(player.gold) : player.gold);
          const arr = history.get(player.id) || [];
          arr.push({ t: now, gold });
          while (arr.length > 1 && now - arr[0].t > WINDOW_MS) arr.shift();
          history.set(player.id, arr);
        });
      },
      // Gold pro Sekunde \xFCber das Fenster; null wenn zu wenig Daten.
      mps(id) {
        const arr = history.get(id);
        if (!arr || arr.length < 2) return null;
        const first = arr[0];
        const last = arr[arr.length - 1];
        const dt = Math.max((last.t - first.t) / 1e3, 1e-3);
        return (last.gold - first.gold) / dt;
      },
      // Sekunden, bis der Spieler \`cost\` erreicht; 0 wenn schon leistbar, null wenn nicht absehbar.
      timeToAfford(id, cost, currentGold) {
        const gold = finiteOrZero(currentGold);
        if (gold >= cost) return 0;
        const rate = this.mps(id);
        if (rate == null || rate <= 0) return null;
        return (cost - gold) / rate;
      },
      reset() {
        history.clear();
      }
    };
  }

  // src/page/advisor/expansion-advisor.js
  function evaluateExpansionState(troopInfo, myState, troopEconomy) {
    if (!troopInfo) return { level: "unknown", label: "Expansion: unknown" };
    const outgoingTroops = troopsInCombat(myState || {});
    if (troopInfo.ratio < 0.25 && outgoingTroops > troopInfo.troops * 0.65) {
      return { level: "stop", label: "Expansion: stop" };
    }
    const level = troopEconomy ? levelFromState(troopEconomy.state) : levelFromRatio(troopInfo.ratio);
    return { level, label: \`Expansion: \${level}\` };
  }
  function levelFromState(state) {
    if (state === "CRITICAL") return "recover";
    if (state === "PUSH" || state === "CAP_WASTE") return "aggressive";
    return "normal";
  }
  function levelFromRatio(ratio) {
    if (ratio < 0.18) return "recover";
    if (ratio > 0.65) return "aggressive";
    return "normal";
  }

  // src/page/advisor/attack-advisor.js
  var CLAIM_PATTERN = /\\bclaim\\b/i;
  var HUMAN_TARGET_ENTER = 0.5;
  var HUMAN_TARGET_EXIT = 0.62;
  var HUMAN_FARM_ENTER = 0.28;
  var HUMAN_FARM_EXIT = 0.36;
  var HUMAN_DANGER_ENTER = 1.25;
  var HUMAN_DANGER_EXIT = 1.05;
  var STATUS_MIN_DWELL_TICKS = 25;
  var CAPTURE_TILE_COST = 40;
  var CAPTURE_ARMY_WEIGHT = 1.05;
  var CAPTURE_MARGIN_SAFE = 1.15;
  var CAPTURE_MARGIN_NORMAL = 1.08;
  var TWO_WAVE_MIN_SHARE = 0.58;
  var TARGET_CAPTURE_TURNS = 2;
  var CAPTURE_OVERDRAFT_RESERVE_RATIO = 0.1;
  var FARM_OVERDRAFT_STRENGTH_RATIO = 0.55;
  var HUMAN_OVERDRAFT_STRENGTH_RATIO = 0.28;
  function createAttackAdvisor({ settings, teamDetection }) {
    const statusMemory = /* @__PURE__ */ new Map();
    return {
      reset() {
        statusMemory.clear();
      },
      evaluate({ gameState, neighbourFetcher, troopInfo, expansion, troopEconomy, mapData = null }) {
        const myState = gameState.getMyState();
        const neighbours = neighbourFetcher.cachedNeighbourIDs;
        if (!myState || !myState.isAlive || !neighbours) return [];
        const myTroops = finiteOrZero(troopInfo?.troops || myState.troops);
        const maxTroops = finiteOrZero(troopInfo?.maxTroops);
        if (myTroops <= 0 || maxTroops <= 0) return [];
        const reserveRatio = Number.isFinite(troopEconomy?.recommendedReserve) ? troopEconomy.recommendedReserve : computeReserveRatio({ gameState, neighbours, myState, myTroops, settings });
        const reserveMaxSend = Math.max(0, Math.floor(myTroops - maxTroops * reserveRatio));
        const safePush = !!troopEconomy?.safePush;
        const tick = Number(gameState.state.currentTick) || 0;
        const seen = /* @__PURE__ */ new Set();
        const results = [];
        neighbours.forEach((playerID) => {
          if (playerID === myState.id) return;
          if (teamDetection?.isMyTeammate(playerID)) return;
          const target = gameState.state.playerStates.get(playerID);
          if (!target || !target.isAlive) return;
          seen.add(playerID);
          const targetTroops = finiteOrZero(target.troops);
          const targetTiles = finiteOrZero(target.tilesOwned);
          const effectiveTargetTroops = Math.max(0, targetTroops - troopsInCombat(target));
          const strengthRatio = effectiveTargetTroops / Math.max(1, myTroops);
          const isHuman = isHumanTarget(target);
          const maxSend = computeTargetMaxSend({
            myTroops,
            maxTroops,
            reserveMaxSend,
            safePush,
            strengthRatio,
            isHuman,
            targetTiles,
            expansion
          });
          const sizing = computeAttackSizing({
            myTroops,
            targetTroops: effectiveTargetTroops,
            targetTiles,
            safePush,
            maxSend,
            kind: isHuman ? "human" : "farm"
          });
          const officialSizing = estimateOfficialCaptureCost({
            target,
            effectiveTargetTroops,
            mapData
          });
          const desiredTroops = sizing.desiredTroops;
          const suggestedTroops = Math.min(desiredTroops, maxSend);
          const reserveAfterRatio = (myTroops - suggestedTroops) / maxTroops;
          let status;
          if (isHuman) {
            const committed = statusMemory.get(playerID)?.status || null;
            const desired = classifyHumanTarget({
              committed,
              allied: isAllied(myState, target) || !!teamDetection?.isMyTeammate(target.id),
              autoFarmHumanTargets: !!settings.get("autoFarmHumanTargets"),
              safePush,
              strengthRatio,
              desiredTroops,
              maxSend,
              targetTiles,
              estimatedCaptureTurns: sizing.estimatedCaptureTurns
            });
            status = stabilizeStatus(statusMemory, playerID, committed, desired, tick);
          } else {
            status = classifyFarmTarget({
              expansion,
              strengthRatio,
              desiredTroops,
              maxSend,
              targetTiles,
              target,
              safePush,
              estimatedCaptureTurns: sizing.estimatedCaptureTurns
            });
          }
          const suggestedPercent = myTroops > 0 ? Math.max(1, Math.round(suggestedTroops / myTroops * 100)) : 0;
          results.push({
            id: target.id,
            name: target.name || target.displayName || target.id,
            playerType: target.playerType || "",
            isHuman,
            status,
            label: getLabel(status, suggestedPercent),
            reason: getReason(status, expansion, strengthRatio, desiredTroops, maxSend, sizing, isHuman),
            score: scoreTarget(status, strengthRatio, targetTiles),
            targetTroops,
            targetTiles,
            effectiveTargetTroops,
            strengthRatio,
            suggestedTroops,
            suggestedPercent,
            reserveAfterRatio,
            appliedReserveRatio: reserveRatio,
            reserveMaxSend,
            captureMaxSend: maxSend,
            usedCaptureOverdraft: maxSend > reserveMaxSend,
            captureCostEstimate: sizing.captureCostEstimate,
            captureLandCostEstimate: sizing.landCostEstimate,
            captureTileCost: sizing.tileCost,
            estimatedCaptureTurns: sizing.estimatedCaptureTurns,
            sizingMode: sizing.mode,
            officialCaptureCostEstimate: officialSizing.captureCostEstimate,
            officialCaptureTileCost: officialSizing.tileCost,
            officialCaptureTurns: officialSizing.estimatedCaptureTurns,
            officialCaptureSource: officialSizing.source,
            officialTerrainClass: officialSizing.terrainClass
          });
        });
        statusMemory.forEach((_, id) => {
          if (!seen.has(id)) statusMemory.delete(id);
        });
        return results.sort((a, b) => b.score - a.score).slice(0, 12);
      }
    };
  }
  function computeReserveRatio({ gameState, neighbours, myState, myTroops, settings }) {
    const fixed = clampRatio(Number(settings.get("autoFarmReserveRatio")), 0.55);
    if (!settings.get("autoFarmDynamicReserve")) return fixed;
    const base = clampRatio(Number(settings.get("autoFarmBaseReserveRatio")), 0.3);
    const min = clampRatio(Number(settings.get("autoFarmMinReserveRatio")), 0.2);
    const max = clampRatio(Number(settings.get("autoFarmMaxReserveRatio")), 0.65);
    if (myTroops <= 0) return clamp(min, max, base);
    let reserve = base;
    let humanEffective = 0;
    let strongestAi = 0;
    (neighbours || []).forEach((id) => {
      if (id === myState.id) return;
      const player = gameState.state.playerStates.get(id);
      if (!player || !player.isAlive) return;
      const effective = Math.max(0, finiteOrZero(player.troops) - troopsInCombat(player));
      if (String(player.playerType || "").toUpperCase() === "HUMAN") humanEffective += effective;
      else if (effective > strongestAi) strongestAi = effective;
    });
    reserve += Math.min(0.3, humanEffective / myTroops * 0.3);
    const incoming = incomingAttackTroops(gameState, myState);
    reserve += Math.min(0.25, incoming / myTroops * 0.5);
    if (strongestAi / myTroops > 0.6) reserve += 0.05;
    return clamp(min, max, reserve);
  }
  function stabilizeStatus(statusMemory, id, committed, desired, tick) {
    if (committed === null || desired === "danger" || desired === "hold") {
      statusMemory.set(id, { status: desired, candidate: desired, candidateSince: tick });
      return desired;
    }
    const mem = statusMemory.get(id) || { status: committed, candidate: committed, candidateSince: tick };
    if (desired === committed) {
      statusMemory.set(id, { status: committed, candidate: committed, candidateSince: tick });
      return committed;
    }
    if (mem.candidate !== desired) {
      statusMemory.set(id, { status: committed, candidate: desired, candidateSince: tick });
      return committed;
    }
    if (tick - mem.candidateSince >= STATUS_MIN_DWELL_TICKS) {
      statusMemory.set(id, { status: desired, candidate: desired, candidateSince: tick });
      return desired;
    }
    return committed;
  }
  function isAllowedFarmTarget(player) {
    const type = String(player.playerType || "").toUpperCase();
    const name = \`\${player.name || ""} \${player.displayName || ""}\`;
    if (type === "HUMAN") return false;
    if (type === "BOT" || type === "NATION") return true;
    return CLAIM_PATTERN.test(name);
  }
  function isHumanTarget(player) {
    return String(player.playerType || "").toUpperCase() === "HUMAN";
  }
  function computeTargetMaxSend({ myTroops, maxTroops, reserveMaxSend, safePush, strengthRatio, isHuman, targetTiles, expansion }) {
    if (!safePush || targetTiles <= 0 || expansion && expansion.level === "stop") return reserveMaxSend;
    const threshold = isHuman ? HUMAN_OVERDRAFT_STRENGTH_RATIO : FARM_OVERDRAFT_STRENGTH_RATIO;
    if (strengthRatio > threshold) return reserveMaxSend;
    const overdraftMaxSend = Math.max(0, Math.floor(myTroops - maxTroops * CAPTURE_OVERDRAFT_RESERVE_RATIO));
    return Math.max(reserveMaxSend, overdraftMaxSend);
  }
  function computeAttackSizing({ myTroops, targetTroops, targetTiles, safePush = false, maxSend = 0, kind = "farm" }) {
    const isHuman = kind === "human";
    const legacyMultiplier = isHuman ? 1.35 : 1.25;
    const floorRatio = isHuman ? 0.08 : 0.04;
    const ceilingRatio = isHuman ? safePush ? 0.62 : 0.36 : safePush ? 0.72 : 0.42;
    const targetBased = Math.ceil(Math.max(1, targetTroops) * legacyMultiplier);
    const floor = Math.ceil(myTroops * floorRatio);
    const ceiling = Math.ceil(myTroops * ceilingRatio);
    const tileCost = CAPTURE_TILE_COST;
    const landCostEstimate = Math.ceil(Math.max(0, targetTiles) * tileCost);
    const captureCostEstimate = Math.ceil(Math.max(1, targetTroops) * CAPTURE_ARMY_WEIGHT + landCostEstimate);
    const margin = safePush ? CAPTURE_MARGIN_SAFE : CAPTURE_MARGIN_NORMAL;
    const oneWave = Math.ceil(captureCostEstimate * margin);
    const twoWave = Math.ceil(oneWave * TWO_WAVE_MIN_SHARE);
    let consolidation = oneWave;
    let mode = "one_wave";
    if (oneWave > maxSend) {
      consolidation = maxSend >= twoWave ? maxSend : twoWave;
      mode = maxSend >= twoWave ? "two_wave_push" : "needs_reserve";
    }
    const rawDesired = Math.max(targetBased, floor, consolidation);
    const desiredTroops = Math.max(1, Math.min(rawDesired, ceiling));
    const estimatedCaptureTurns = desiredTroops > 0 ? Math.ceil(oneWave / desiredTroops) : null;
    if (desiredTroops < rawDesired && mode !== "needs_reserve") mode = "ceiling_limited";
    return {
      desiredTroops,
      captureCostEstimate,
      landCostEstimate,
      tileCost,
      oneWave,
      twoWave,
      estimatedCaptureTurns,
      mode
    };
  }
  function classifyFarmTarget({ expansion, strengthRatio, desiredTroops, maxSend, targetTiles, target, safePush, estimatedCaptureTurns }) {
    if (!isAllowedFarmTarget(target)) return "hold";
    if (expansion && (expansion.level === "recover" || expansion.level === "stop")) return "mark";
    if (targetTiles <= 0 || strengthRatio >= 0.75) return "hold";
    const farmThreshold = safePush ? 0.6 : 0.45;
    if (desiredTroops <= maxSend && strengthRatio <= farmThreshold && estimatedCaptureTurns <= TARGET_CAPTURE_TURNS) return "farm";
    if (strengthRatio <= 0.65 || targetTiles >= 250) return "mark";
    return "hold";
  }
  function classifyHumanTarget({
    committed,
    allied,
    autoFarmHumanTargets,
    safePush,
    strengthRatio,
    desiredTroops,
    maxSend,
    targetTiles,
    estimatedCaptureTurns
  }) {
    if (allied) return "hold";
    if (committed === "danger") {
      if (!(targetTiles > 0 && strengthRatio < HUMAN_DANGER_EXIT)) return "danger";
    } else if (targetTiles <= 0 || strengthRatio >= HUMAN_DANGER_ENTER) {
      return "danger";
    }
    const canAutoFarm = autoFarmHumanTargets && safePush && desiredTroops <= maxSend && estimatedCaptureTurns <= TARGET_CAPTURE_TURNS && targetTiles > 0;
    if (committed === "farm") {
      if (canAutoFarm && strengthRatio <= HUMAN_FARM_EXIT) return "farm";
    } else if (canAutoFarm && strengthRatio <= HUMAN_FARM_ENTER) {
      return "farm";
    }
    if (committed === "target") {
      if (strengthRatio <= HUMAN_TARGET_EXIT && desiredTroops <= maxSend) return "target";
      return "wait";
    }
    if (strengthRatio <= HUMAN_TARGET_ENTER && desiredTroops <= maxSend) return "target";
    return "wait";
  }
  function getReason(status, expansion, strengthRatio, desiredTroops, maxSend, sizing, isHuman) {
    if (expansion && (expansion.level === "recover" || expansion.level === "stop")) return "reserve";
    if (status === "farm") return isHuman ? "weak_adjacent_human_capture" : "weak_adjacent_ai_capture";
    if (status === "target") return "weak_adjacent_human";
    if (desiredTroops > maxSend) return "reserve_limit";
    if (sizing?.estimatedCaptureTurns > TARGET_CAPTURE_TURNS) return "capture_too_slow";
    if (strengthRatio >= 0.75 || status === "danger") return "too_strong";
    return "watch";
  }
  function scoreTarget(status, strengthRatio, targetTiles) {
    const statusBonus = status === "farm" ? 2 : status === "target" ? 1.6 : status === "mark" ? 1 : status === "wait" ? 0.4 : 0;
    const tileScore = Math.min(1, targetTiles / 2e3);
    const weaknessScore = Math.max(0, 1 - strengthRatio);
    return statusBonus + tileScore * 0.4 + weaknessScore * 0.6;
  }
  function getLabel(status, suggestedPercent) {
    if (status === "farm") return \`FARM \${suggestedPercent}%\`;
    if (status === "target") return \`TARGET \${suggestedPercent}%\`;
    if (status === "mark") return "MARK";
    if (status === "wait") return "WAIT";
    if (status === "danger") return "DANGER";
    return "HOLD";
  }
  function isAllied(myState, target) {
    const alliances = Array.isArray(myState.alliances) ? myState.alliances : [];
    return alliances.some((alliance) => alliance && alliance.other === target.id);
  }
  function clamp(min, max, value) {
    return Math.max(min, Math.min(max, value));
  }
  function clampRatio(value, fallback) {
    return Number.isFinite(value) && value > 0 && value < 1 ? value : fallback;
  }

  // src/page/advisor/troop-economy.js
  var PEAK_RATIO = 0.42;
  var STATES = [
    { state: "CRITICAL", max: 0.18, floor: 0.42, hint: "kritisch - nur sichere Kleinstaktionen" },
    { state: "RECOVER", max: 0.3, floor: 0.36, hint: "regenerieren - fruehe Expansion moeglich" },
    { state: "GROWTH_PEAK", max: 0.5, floor: 0.34, hint: "Wachstumsband - Druck halten" },
    { state: "READY", max: 0.65, floor: 0.34, hint: "bereit - aktiv expandieren/farmen" },
    { state: "PUSH", max: 0.82, floor: 0.3, hint: "Push-Fenster - Truppen einsetzen" },
    { state: "CAP_WASTE", max: 1.01, floor: 0.25, hint: "zu nah am Cap - Truppen einsetzen" }
  ];
  var PUSH_TARGET_RATIO = 0.45;
  var PUSH_SIM_TICK_CAP = 6e3;
  var SAFE_PUSH_THRESHOLD = 0.78;
  var AGGRESSIVE_RESERVE_DROP = 0.16;
  function evaluateTroopEconomy({ troopInfo, myState, gameState, neighbours, settings, teamDetection, threats }) {
    const maxTroops = finiteOrZero(troopInfo?.maxTroops);
    const currentTroops = finiteOrZero(troopInfo?.troops ?? myState?.troops);
    if (maxTroops <= 0 || currentTroops < 0) return null;
    const currentRatio = clamp01(currentTroops / maxTroops);
    const troopIncreaseRate = growthRate(currentTroops, maxTroops);
    const estimatedPeakIncreaseRate = growthRate(PEAK_RATIO * maxTroops, maxTroops);
    const growthEfficiency = estimatedPeakIncreaseRate > 0 ? clamp01(troopIncreaseRate / estimatedPeakIncreaseRate) : 0;
    const band = STATES.find((entry) => currentRatio < entry.max) || STATES[STATES.length - 1];
    const state = band.state;
    const stateFloor = band.floor;
    const incoming = incomingAttackTroops(gameState, myState);
    let humanNeighbourEffective = 0;
    (neighbours || []).forEach((id) => {
      if (!myState || id === myState.id) return;
      if (teamDetection?.isMyTeammate(id)) return;
      const player = gameState.state.playerStates.get(id);
      if (!player || !player.isAlive) return;
      if (String(player.playerType || "").toUpperCase() !== "HUMAN") return;
      humanNeighbourEffective += Math.max(0, finiteOrZero(player.troops) - troopsInCombat(player));
    });
    const combatSafety = clamp01(1 - (incoming + humanNeighbourEffective) / Math.max(1, maxTroops));
    const safePush = incoming === 0 && combatSafety >= SAFE_PUSH_THRESHOLD;
    const combatReserve = computeReserveRatio({ gameState, neighbours, myState: myState || {}, myTroops: currentTroops, settings });
    let recommendedReserve;
    if (!settings.get("autoFarmDynamicReserve")) {
      recommendedReserve = combatReserve;
    } else {
      const min = clampRatio2(Number(settings.get("autoFarmMinReserveRatio")), 0.2);
      const baseMax = clampRatio2(Number(settings.get("autoFarmMaxReserveRatio")), 0.65);
      const max = teamDetection?.isTeamMode ? Math.max(baseMax, 0.7) : baseMax;
      recommendedReserve = safePush ? clamp2(min, max, Math.min(stateFloor, combatReserve - AGGRESSIVE_RESERVE_DROP)) : clamp2(min, max, Math.max(stateFloor, combatReserve));
    }
    const safeSpendableTroops = Math.max(0, currentTroops - maxTroops * recommendedReserve);
    return {
      currentTroops,
      maxTroops,
      currentRatio,
      troopIncreaseRate,
      growthEfficiency,
      combatSafety,
      safePush,
      recommendedReserve,
      safeSpendableTroops,
      state,
      stateFloor,
      hint: band.hint,
      timeToPushSec: estimateTimeToPushSec(currentTroops, maxTroops),
      hasThreat: Array.isArray(threats) && threats.some((t) => t && (t.level === "Dangerous" || t.level === "Critical"))
    };
  }
  function growthRate(currentTroops, maxTroops) {
    const current = finiteOrZero(currentTroops);
    const max = finiteOrZero(maxTroops);
    if (max <= 0 || current >= max) return 0;
    return (10 + Math.pow(Math.max(0, current), 0.73) / 4) * (1 - current / max);
  }
  function estimateTimeToPushSec(currentTroops, maxTroops) {
    const target = PUSH_TARGET_RATIO * maxTroops;
    if (currentTroops >= target) return null;
    let troops = currentTroops;
    let ticks = 0;
    while (troops < target && ticks < PUSH_SIM_TICK_CAP) {
      const rate = growthRate(troops, maxTroops);
      if (rate <= 0) break;
      troops += rate;
      ticks += 1;
    }
    if (troops < target) return null;
    return Math.round(ticks / 10);
  }
  function clamp2(min, max, value) {
    return Math.max(min, Math.min(max, value));
  }
  function clamp01(value) {
    const number = Number(value);
    if (!Number.isFinite(number)) return 0;
    return Math.max(0, Math.min(1, number));
  }
  function clampRatio2(value, fallback) {
    return Number.isFinite(value) && value > 0 && value < 1 ? value : fallback;
  }

  // src/page/automation/farm-window.js
  function isInsideFarmWindow(state, settings) {
    const startedAt = Number(state.roundStartedAtMs) || 0;
    const windowMs = Number(settings.get("autoFarmWindowMs")) || 24e4;
    return startedAt > 0 && performance.now() - startedAt <= windowMs;
  }

  // src/page/automation/auto-expand.js
  var BACKOFF_AFTER_STAGNANT = 4;
  var BACKOFF_COOLDOWN_MS = 2e4;
  var MIN_SURPLUS_RATIO = 0.01;
  var EARLY_MIN_SURPLUS_RATIO = 2e-3;
  var FAST_EXPAND_COOLDOWN_MS = 900;
  var NORMAL_EXPAND_COOLDOWN_MS = 1500;
  var EARLY_WAVE_RATIO = 0.12;
  var NORMAL_WAVE_RATIO = 0.18;
  var PUSH_WAVE_RATIO = 0.28;
  function createAutoExpand({ gameState, neighbourFetcher, OrigWS, origWsSend, settings, logger, roundLogger, getTroopEconomy }) {
    let lastSentAt = 0;
    let lastSkipAt = 0;
    let lastTilesOwned = -1;
    let stagnantAttempts = 0;
    let status = { state: "idle", reason: "not_started" };
    return {
      reset() {
        lastSentAt = 0;
        lastSkipAt = 0;
        lastTilesOwned = -1;
        stagnantAttempts = 0;
        status = { state: "idle", reason: "reset" };
      },
      getStatus() {
        return status;
      },
      run(troopInfo, options = {}) {
        const state = gameState.state;
        if (!options.force && !settings.get("autoExpand")) return setStatus("idle", "disabled", false);
        if (!OrigWS) return setStatus("idle", "websocket_unavailable", false);
        if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !state.myPlayerID) {
          return setStatus("idle", "not_ready", false);
        }
        if (!origWsSend) return setStatus("idle", "socket_unavailable", false);
        if (!isInsideFarmWindow(state, settings)) return setStatus("idle", "outside_window", false);
        if (!neighbourFetcher.cachedHasOpenFrontier) {
          logSkip("no_frontier");
          return setStatus("blocked", "no_frontier", true);
        }
        const myState = gameState.getMyState();
        if (!myState || !myState.isAlive) return setStatus("idle", "not_alive", false);
        const myTroops = finiteOrZero(troopInfo?.troops || myState.troops);
        const maxTroops = finiteOrZero(troopInfo?.maxTroops);
        if (myTroops <= 0 || maxTroops <= 0) return setStatus("idle", "no_troops", false);
        const economy = getTroopEconomy?.();
        const reserveRatio = Number.isFinite(economy?.recommendedReserve) ? economy.recommendedReserve : computeReserveRatio({
          gameState,
          neighbours: neighbourFetcher.cachedNeighbourIDs,
          myState,
          myTroops,
          settings
        });
        const maxSend = Math.floor(myTroops - maxTroops * reserveRatio);
        const currentRatio = myTroops / maxTroops;
        const minSurplusRatio = currentRatio < 0.25 ? EARLY_MIN_SURPLUS_RATIO : MIN_SURPLUS_RATIO;
        if (maxSend <= Math.floor(maxTroops * minSurplusRatio)) {
          logSkip("reserve_limit", { reserveRatio, maxSend, currentRatio, minSurplusRatio });
          return setStatus("blocked", "reserve_limit", true, { reserveRatio, maxSend });
        }
        const tilesOwned = finiteOrZero(myState.tilesOwned);
        const waveRatio = chooseWaveRatio(economy, currentRatio);
        const cooldownMs = stagnantAttempts >= BACKOFF_AFTER_STAGNANT ? BACKOFF_COOLDOWN_MS : chooseCooldownMs(economy);
        const now = performance.now();
        if (now - lastSentAt < cooldownMs) {
          return setStatus("cooldown", "cooldown", true, { cooldownMs: Math.round(cooldownMs - (now - lastSentAt)) });
        }
        if (lastTilesOwned >= 0) {
          if (tilesOwned > lastTilesOwned) stagnantAttempts = 0;
          else stagnantAttempts += 1;
        }
        const troops = Math.max(1, Math.min(maxSend, Math.floor(maxTroops * waveRatio)));
        origWsSend.call(
          state.gameSocket,
          JSON.stringify({ type: "intent", intent: { type: "attack", targetID: null, troops } })
        );
        lastSentAt = now;
        lastTilesOwned = tilesOwned;
        logger.info(\`Auto-expand wilderness: \${troops} troops\`);
        roundLogger?.record("auto_expand_sent", {
          troops,
          reserveRatio,
          maxSend,
          waveRatio,
          currentRatio,
          minSurplusRatio,
          tilesOwned,
          stagnantAttempts
        });
        setStatus("sent", \`sent_\${troops}\`, true, { troops, reserveRatio, waveRatio });
        return true;
      }
    };
    function logSkip(reason, extra = {}) {
      const now = performance.now();
      if (now - lastSkipAt < 5e3) return;
      lastSkipAt = now;
      roundLogger?.record("auto_expand_skipped", { reason, ...extra });
    }
    function setStatus(state, reason, result, extra = {}) {
      status = { state, reason, ...extra };
      return result;
    }
  }
  function chooseCooldownMs(economy) {
    if (economy?.safePush) return FAST_EXPAND_COOLDOWN_MS;
    return NORMAL_EXPAND_COOLDOWN_MS;
  }
  function chooseWaveRatio(economy, currentRatio) {
    if (economy?.state === "CAP_WASTE" || economy?.state === "PUSH") return PUSH_WAVE_RATIO;
    if (economy?.safePush && currentRatio >= 0.3) return NORMAL_WAVE_RATIO;
    return EARLY_WAVE_RATIO;
  }

  // src/page/advisor/buy-advisor.js
  function recommendBuys({ gameState, troopInfo, threats, farmRecommendations, troopEconomy }) {
    const myState = gameState.getMyState();
    if (!myState || !myState.isAlive) return [];
    const gold = finiteOrZero(myState.gold);
    const dangerousThreat = (threats || []).find((threat) => threat.level === "Dangerous" || threat.level === "Critical");
    const nukeThreat = (threats || []).find((threat) => threat.nukePotential > 0 || threat.buildingNuke || threat.nukeSoon);
    const farmAvailable = (farmRecommendations || []).some((target) => target.status === "farm");
    const ratio = Number(troopInfo?.ratio || 0);
    const ecoState = troopEconomy?.state || null;
    const lowEco = ecoState === "CRITICAL" || ecoState === "RECOVER";
    const recommendations = [];
    const gameView = getGameView();
    const myPlayer = safeMyPlayer(gameView);
    const cityCount = countPlayerUnits(myPlayer, "City");
    const factoryCount = countPlayerUnits(myPlayer, "Factory");
    const cityCost = getStructureCost("City", { gameView, player: myPlayer });
    const portCost = getStructureCost("Port", { gameView, player: myPlayer });
    const factoryCost = getStructureCost("Factory", { gameView, player: myPlayer });
    const defenseCost = getStructureCost("Defense Post", { gameView, player: myPlayer });
    const samCost = getStructureCost("SAM Launcher", { gameView, player: myPlayer });
    const siloCost = getStructureCost("Missile Silo", { gameView, player: myPlayer });
    if (nukeThreat && gold >= samCost.cost) {
      recommendations.push(unverified("SAM Launcher", \`Nuke-Gefahr: \${nukeThreat.name}\`, samCost));
    }
    if (dangerousThreat && gold >= defenseCost.cost) {
      recommendations.push(unverified("Defense Post", \`Frontdruck: \${dangerousThreat.name}\`, defenseCost));
    }
    if (ecoState === "CAP_WASTE" && gold >= cityCost.cost) {
      recommendations.push(verifiedEco("City", "Cap-Waste: Gold jetzt in Eco investieren", cityCost));
    }
    if (gold >= cityCost.cost && ratio >= 0.35 && ecoState !== "CAP_WASTE") {
      recommendations.push(verifiedEco("City", farmAvailable ? "Eco nach Farm-Fenster stabilisieren" : "sicherer Economy-Kauf", cityCost));
    }
    if (cityCount >= 1 && factoryCount < 2 && gold >= factoryCost.cost && ratio >= 0.35 && !lowEco) {
      recommendations.push(verifiedEco("Factory", "sichere Land-Eco ausbauen", factoryCost));
    }
    if (gold >= portCost.cost) {
      recommendations.push(verifiedEco("Port", "nur bei guter Wasserroute", portCost));
    }
    if (gold >= siloCost.cost && !dangerousThreat && !lowEco) {
      recommendations.push(unverified("Missile Silo", "nur bei stabiler Eco und sicherer Flanke", siloCost));
    }
    return recommendations.slice(0, 3);
  }
  function verifiedEco(label, reason, costModel) {
    return {
      label,
      reason,
      actionAvailable: true,
      actionKey: label.toLowerCase().replace(/\\s+/g, "_"),
      estimatedCost: costModel?.cost || null,
      costSource: costModel?.source || null
    };
  }
  function unverified(label, reason, costModel) {
    return {
      label,
      reason,
      actionAvailable: false,
      actionKey: label.toLowerCase().replace(/\\s+/g, "_"),
      estimatedCost: costModel?.cost || null,
      costSource: costModel?.source || null
    };
  }
  function safeMyPlayer(gameView) {
    try {
      return gameView && typeof gameView.myPlayer === "function" ? gameView.myPlayer() : null;
    } catch (_) {
      return null;
    }
  }

  // src/page/automation/auto-spawn.js
  var SPAWN_COMMIT_MARGIN_TURNS = 20;
  var RESEND_COOLDOWN_MS = 1500;
  var SPAWN_IMPROVE_FACTOR = 1.03;
  function createAutoSpawn({ gameState, mapDataRef, OrigWS, origWsSend, settings, logger, roundLogger, teamDetection }) {
    let lastAutoSpawnTile = -1;
    let lastSentScore = 0;
    let lastResendAt = 0;
    let hasSentThisRound = false;
    let lastPhaseLogAt = 0;
    let status = { state: "idle", reason: "not_started" };
    return {
      reset() {
        lastAutoSpawnTile = -1;
        lastSentScore = 0;
        lastResendAt = 0;
        hasSentThisRound = false;
        lastPhaseLogAt = 0;
        status = { state: "idle", reason: "reset" };
      },
      getStatus() {
        return status;
      },
      send(spot) {
        const mapData = mapDataRef.current;
        const state = gameState.state;
        if (!OrigWS) return setStatus("idle", "websocket_unavailable");
        if (!mapData || !state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !origWsSend) {
          return setStatus("idle", "not_ready");
        }
        const delayMs = Number(settings.get("autoSpawnDelayMs")) || 0;
        const waitedMs = state.roundStartedAtMs ? performance.now() - state.roundStartedAtMs : 0;
        if (state.roundStartedAtMs && waitedMs < delayMs) {
          return setStatus("waiting", "delay", { waitMs: Math.round(delayMs - waitedMs) });
        }
        const phase = getSpawnPhaseInfo({ gameView: getGameView(), state, teamMode: !!teamDetection?.isTeamMode });
        logSpawnPhase(phase);
        const tileRef = spot.y * mapData.width + spot.x;
        const score = Number(spot.score) || 0;
        const inSpawnPhase = phase.remainingTurns > SPAWN_COMMIT_MARGIN_TURNS;
        const now = performance.now();
        if (hasSentThisRound) {
          if (!inSpawnPhase) return setStatus("sent", "committed", { tile: lastAutoSpawnTile });
          if (tileRef === lastAutoSpawnTile) return setStatus("sent", "best", { tile: tileRef });
          if (now - lastResendAt < RESEND_COOLDOWN_MS) return setStatus("sent", "resend_cooldown", { tile: lastAutoSpawnTile });
          if (score <= lastSentScore * SPAWN_IMPROVE_FACTOR) return setStatus("sent", "no_improvement", { tile: lastAutoSpawnTile });
        }
        const isResend = hasSentThisRound;
        lastAutoSpawnTile = tileRef;
        lastSentScore = score;
        lastResendAt = now;
        hasSentThisRound = true;
        origWsSend.call(state.gameSocket, JSON.stringify({ type: "intent", intent: { type: "spawn", tile: tileRef } }));
        logger.info(\`Auto-spawn \${isResend ? "re-correct" : "sent"}: (\${spot.x},\${spot.y}) tile=\${tileRef} score=\${score.toFixed(3)}\`);
        roundLogger?.record("auto_spawn_sent", {
          x: spot.x,
          y: spot.y,
          tile: tileRef,
          score,
          resend: isResend,
          waitedMs: Math.round(waitedMs),
          spawnRemainingTurns: phase.remainingTurns,
          spawnPhaseTurns: phase.totalTurns,
          spawnPhaseSource: phase.source
        });
        return setStatus("sent", isResend ? "re_corrected" : "sent", { tile: tileRef });
      }
    };
    function logSpawnPhase(phase) {
      const now = performance.now();
      if (now - lastPhaseLogAt < 5e3) return;
      lastPhaseLogAt = now;
      roundLogger?.record("spawn_phase_state", {
        currentTurn: phase.currentTurn,
        totalTurns: phase.totalTurns,
        remainingTurns: phase.remainingTurns,
        waitCapMs: Math.round(phase.waitCapMs),
        source: phase.source
      });
    }
    function setStatus(state, reason, extra = {}) {
      status = { state, reason, ...extra };
    }
  }

  // src/page/automation/auto-farm.js
  var AGGRESSIVE_FARM_COOLDOWN_MS = 800;
  function createAutoFarm({ gameState, OrigWS, origWsSend, settings, logger, roundLogger, getTroopEconomy }) {
    const targetCooldowns = /* @__PURE__ */ new Map();
    let lastSentAt = 0;
    let lastSkipAt = 0;
    let status = { state: "idle", reason: "not_started" };
    return {
      reset() {
        targetCooldowns.clear();
        lastSentAt = 0;
        lastSkipAt = 0;
        status = { state: "idle", reason: "reset" };
      },
      getStatus() {
        return status;
      },
      run(recommendations, expansion, options = {}) {
        const state = gameState.state;
        if (!options.force && !settings.get("autoFarm")) return setStatus("idle", "disabled");
        if (!OrigWS) return setStatus("idle", "websocket_unavailable");
        if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !state.myPlayerID) {
          return setStatus("idle", "not_ready");
        }
        if (!origWsSend) return setStatus("idle", "socket_unavailable");
        if (!isInsideFarmWindow(state, settings)) return setStatus("idle", "outside_window");
        const now = performance.now();
        const safePush = !!getTroopEconomy?.()?.safePush;
        const target = (recommendations || []).find((candidate) => candidate.status === "farm" && candidate.suggestedTroops > 0);
        if (!target) return setStatus("blocked", "no_target");
        if (expansion && expansion.level === "stop") {
          logSkip("stop", { targetID: target.id, targetName: target.name || target.id });
          return setStatus("blocked", "stop");
        }
        if (expansion && expansion.level === "recover" && !safePush) {
          logSkip("recover", { targetID: target.id, targetName: target.name || target.id });
          return setStatus("blocked", "recover");
        }
        const baseCooldown = Number(settings.get("autoFarmCooldownMs")) || 2500;
        const cooldownMs = safePush ? Math.min(baseCooldown, AGGRESSIVE_FARM_COOLDOWN_MS) : baseCooldown;
        if (now - lastSentAt < cooldownMs) {
          return setStatus("cooldown", "cooldown", { cooldownMs: Math.round(cooldownMs - (now - lastSentAt)) });
        }
        const lastTargetSentAt = targetCooldowns.get(target.id) || 0;
        const perTargetFactor = safePush ? 1.4 : 2.4;
        if (now - lastTargetSentAt < cooldownMs * perTargetFactor) {
          return setStatus("cooldown", "target_cooldown", { targetName: target.name || target.id });
        }
        const troops = Math.max(1, Math.floor(target.suggestedTroops));
        origWsSend.call(
          state.gameSocket,
          JSON.stringify({ type: "intent", intent: { type: "attack", targetID: target.id, troops } })
        );
        lastSentAt = now;
        targetCooldowns.set(target.id, now);
        logger.info(\`Auto-farm attack \${target.name || target.id}: \${troops} troops\`);
        roundLogger?.record("auto_farm_attack_sent", {
          targetID: target.id,
          targetName: target.name || target.id,
          isHuman: !!target.isHuman,
          troops,
          suggestedPercent: target.suggestedPercent,
          reserveAfterRatio: target.reserveAfterRatio,
          appliedReserveRatio: target.appliedReserveRatio,
          reserveMaxSend: target.reserveMaxSend,
          captureMaxSend: target.captureMaxSend,
          usedCaptureOverdraft: !!target.usedCaptureOverdraft,
          targetTroops: target.targetTroops,
          effectiveTargetTroops: target.effectiveTargetTroops,
          targetTiles: target.targetTiles,
          captureCostEstimate: target.captureCostEstimate,
          officialCaptureCostEstimate: target.officialCaptureCostEstimate,
          officialCaptureTurns: target.officialCaptureTurns,
          officialCaptureSource: target.officialCaptureSource,
          estimatedCaptureTurns: target.estimatedCaptureTurns,
          sizingMode: target.sizingMode
        });
        setStatus("sent", \`sent_\${target.name || target.id}\`, { targetName: target.name || target.id, troops });
      }
    };
    function logSkip(reason, extra = {}) {
      const now = performance.now();
      if (now - lastSkipAt <= 5e3) return;
      lastSkipAt = now;
      roundLogger?.record("auto_farm_skipped", { reason, ...extra });
    }
    function setStatus(state, reason, extra = {}) {
      status = { state, reason, ...extra };
    }
  }

  // src/page/automation/auto-eco.js
  var ECO_BUILD_COOLDOWN_MS = 6500;
  var EARLY_ECO_BUILD_COOLDOWN_MS = 1e3;
  var EARLY_ECO_FREE_RESERVE_BUILDS = 10;
  var CITY_UNIT = "City";
  var PORT_UNIT = "Port";
  var FACTORY_UNIT = "Factory";
  var MAX_AUTO_CITIES = 5;
  var MAX_AUTO_PORTS = 2;
  var MAX_AUTO_FACTORIES = 3;
  function createAutoEco({ gameState, mapDataRef, OrigWS, origWsSend, settings, logger, roundLogger, getTroopEconomy }) {
    let lastSentAt = 0;
    let lastSkipAt = 0;
    let lastCostLogAt = 0;
    let lastKnownEcoBuildCount = 0;
    let runtimeProbed = false;
    let inFlight = false;
    let status = { state: "idle", reason: "not_started" };
    const pendingBuilds = /* @__PURE__ */ new Map();
    const observedBuildCounts = /* @__PURE__ */ new Map();
    const blockedTilesByUnit = /* @__PURE__ */ new Map();
    return {
      reset() {
        lastSentAt = 0;
        lastSkipAt = 0;
        lastCostLogAt = 0;
        lastKnownEcoBuildCount = 0;
        runtimeProbed = false;
        inFlight = false;
        pendingBuilds.clear();
        observedBuildCounts.clear();
        blockedTilesByUnit.clear();
        status = { state: "idle", reason: "reset" };
      },
      getStatus() {
        return status;
      },
      observeIntent(intent) {
        if (!intent || intent.type !== "build_unit" || !intent.unit) return;
        observeBuildIntent(intent.unit, intent.tile);
      },
      run({ buyRecommendations = [] } = {}, options = {}) {
        const state = gameState.state;
        if (!options.force && !settings.get("autoEco")) return setStatus("idle", "disabled", false);
        if (!OrigWS) return setStatus("idle", "websocket_unavailable", false);
        if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !state.myPlayerID) {
          return setStatus("idle", "not_ready", false);
        }
        if (!origWsSend) return setStatus("idle", "socket_unavailable", false);
        if (inFlight) return setStatus("cooldown", "tile_scan", true);
        expirePendingBuilds();
        const now = performance.now();
        const cooldownMs = lastKnownEcoBuildCount < EARLY_ECO_FREE_RESERVE_BUILDS ? EARLY_ECO_BUILD_COOLDOWN_MS : ECO_BUILD_COOLDOWN_MS;
        if (now - lastSentAt < cooldownMs) {
          return setStatus("cooldown", "cooldown", true, { cooldownMs: Math.round(cooldownMs - (now - lastSentAt)) });
        }
        const myState = gameState.getMyState();
        if (!myState || !myState.isAlive) return setStatus("idle", "not_alive", false);
        const gold = finiteOrZero(myState.gold);
        if (gold < 5e4) {
          logSkip("gold", { gold, minCost: 5e4 });
          return setStatus("blocked", "gold", true, { gold, minCost: 5e4 });
        }
        const economy = getTroopEconomy?.();
        if (isEcoUnsafe(economy)) {
          logSkip("unsafe", { combatSafety: economy?.combatSafety, currentRatio: economy?.currentRatio });
          return setStatus("blocked", "unsafe", true, { combatSafety: economy?.combatSafety, currentRatio: economy?.currentRatio });
        }
        inFlight = true;
        chooseBuild({ myState, gold, buyRecommendations }).then((choice) => {
          inFlight = false;
          if (!choice) return;
          if (gold < choice.estimatedCost + choice.reserveGold) {
            logSkip("gold_cost_model", { gold, estimatedCost: choice.estimatedCost, reserveGold: choice.reserveGold, unit: choice.unit });
            setStatus("blocked", "gold_cost_model", true, { gold, estimatedCost: choice.estimatedCost, reserveGold: choice.reserveGold, unit: choice.unit });
            return;
          }
          sendBuild(choice);
        }).catch((error) => {
          inFlight = false;
          logSkip("tile_scan_failed", { message: String(error?.message || error || "") });
          setStatus("blocked", "tile_scan_failed");
        });
        return true;
      }
    };
    async function chooseBuild({ myState, gold, buyRecommendations }) {
      const gameView = getGameView();
      if (!gameView || typeof gameView.myPlayer !== "function") {
        logSkip("game_view");
        setStatus("blocked", "game_view");
        return null;
      }
      const myPlayer = gameView.myPlayer();
      maybeLogRuntimeProbe(gameView, myPlayer);
      const observed = observedCountsByUnit();
      const unitCounts = {
        city: Math.max(countPlayerUnits(myPlayer, CITY_UNIT), observed.City || 0),
        port: Math.max(countPlayerUnits(myPlayer, PORT_UNIT), observed.Port || 0),
        factory: Math.max(countPlayerUnits(myPlayer, FACTORY_UNIT), observed.Factory || 0)
      };
      const adjustedCounts = {
        city: unitCounts.city + pendingCount(CITY_UNIT),
        port: unitCounts.port + pendingCount(PORT_UNIT),
        factory: unitCounts.factory + pendingCount(FACTORY_UNIT)
      };
      lastKnownEcoBuildCount = totalEcoBuildCount(adjustedCounts);
      const candidates = await collectOwnedTileCandidates(gameView, myPlayer, myState, mapDataRef.current);
      if (!candidates.length) {
        logSkip("no_owned_tiles");
        setStatus("blocked", "no_owned_tiles");
        return null;
      }
      maybeLogCostModel(gold);
      if (adjustedCounts.city <= 0) {
        const tile = chooseCityTile(candidates, isTileBlockedForUnit);
        if (tile != null) return withCost({ unit: CITY_UNIT, tile, unitCounts, adjustedCounts });
        logPendingBlocked(CITY_UNIT);
      }
      if (adjustedCounts.city >= 1 && adjustedCounts.factory < MAX_AUTO_FACTORIES && adjustedCounts.factory <= adjustedCounts.port) {
        const tile = chooseFactoryTile(candidates, isTileBlockedForUnit);
        if (tile != null) return withCost({ unit: FACTORY_UNIT, tile, unitCounts, adjustedCounts });
        logPendingBlocked(FACTORY_UNIT);
      }
      if (adjustedCounts.port < MAX_AUTO_PORTS) {
        const tile = choosePortTile(candidates, isTileBlockedForUnit);
        if (tile != null) return withCost({ unit: PORT_UNIT, tile, unitCounts, adjustedCounts });
        logPendingBlocked(PORT_UNIT);
      }
      if (adjustedCounts.factory < MAX_AUTO_FACTORIES && adjustedCounts.city >= 1) {
        const tile = chooseFactoryTile(candidates, isTileBlockedForUnit);
        if (tile != null) return withCost({ unit: FACTORY_UNIT, tile, unitCounts, adjustedCounts });
        logPendingBlocked(FACTORY_UNIT);
      }
      if (adjustedCounts.city < MAX_AUTO_CITIES && adjustedCounts.city <= adjustedCounts.port + adjustedCounts.factory + 1) {
        const tile = chooseCityTile(candidates, isTileBlockedForUnit);
        if (tile != null) return withCost({ unit: CITY_UNIT, tile, unitCounts, adjustedCounts });
        logPendingBlocked(CITY_UNIT);
      }
      logSkip("no_recommended_unit", { cityCount: adjustedCounts.city, portCount: adjustedCounts.port, factoryCount: adjustedCounts.factory });
      setStatus("blocked", "no_recommended_unit", false, {
        cityCount: adjustedCounts.city,
        portCount: adjustedCounts.port,
        factoryCount: adjustedCounts.factory
      });
      return null;
    }
    function withCost(choice) {
      const gameView = getGameView();
      const myPlayer = safeMyPlayer2(gameView);
      const model = getStructureCost(choice.unit, {
        gameView,
        player: myPlayer,
        pendingCounts: pendingCountsByUnit(),
        observedCounts: observedCountsByUnit()
      });
      const ecoBuildCount = totalEcoBuildCount(choice.adjustedCounts);
      const reserveGold = ecoBuildCount < EARLY_ECO_FREE_RESERVE_BUILDS ? 0 : model.reserveGold;
      return { ...choice, estimatedCost: model.cost, costSource: model.source, reserveGold, reserveWaived: reserveGold === 0 && model.reserveGold > 0 };
    }
    function sendBuild(choice) {
      const state = gameState.state;
      origWsSend.call(
        state.gameSocket,
        JSON.stringify({ type: "intent", intent: { type: "build_unit", unit: choice.unit, tile: choice.tile } })
      );
      lastSentAt = performance.now();
      addPendingBuild(choice.unit, choice.tile);
      lastKnownEcoBuildCount = totalEcoBuildCount(choice.adjustedCounts) + 1;
      logger.info(\`Auto-eco build \${choice.unit} at \${choice.tile}\`);
      roundLogger?.record("auto_eco_sent", {
        unit: choice.unit,
        tile: choice.tile,
        estimatedCost: choice.estimatedCost,
        costSource: choice.costSource,
        cityCount: choice.unitCounts.city,
        portCount: choice.unitCounts.port,
        factoryCount: choice.unitCounts.factory,
        adjustedCityCount: choice.adjustedCounts.city,
        adjustedPortCount: choice.adjustedCounts.port,
        adjustedFactoryCount: choice.adjustedCounts.factory,
        reserveGold: choice.reserveGold,
        reserveWaived: !!choice.reserveWaived
      });
      setStatus("sent", \`built_\${choice.unit}\`, true, { unit: choice.unit, tile: choice.tile, estimatedCost: choice.estimatedCost, costSource: choice.costSource });
    }
    function observeBuildIntent(unit, tile) {
      const key = buildKey(unit, tile);
      const normalized = normalizeUnitKey(unit);
      if (pendingBuilds.has(key)) pendingBuilds.delete(key);
      observedBuildCounts.set(normalized, (observedBuildCounts.get(normalized) || 0) + 1);
      if (normalized === "city" || normalized === "port" || normalized === "factory") {
        lastKnownEcoBuildCount = Math.max(lastKnownEcoBuildCount, observedEcoBuildCount());
      }
      blockTile(unit, tile);
    }
    function addPendingBuild(unit, tile) {
      const duration = getConstructionDurationMs(unit, { gameView: getGameView() });
      pendingBuilds.set(buildKey(unit, tile), { unit, tile, sentAt: performance.now(), ttlMs: duration.durationMs + 1500 });
      blockTile(unit, tile);
    }
    function expirePendingBuilds() {
      const now = performance.now();
      pendingBuilds.forEach((entry, key) => {
        if (now - entry.sentAt > (entry.ttlMs || 12e3)) pendingBuilds.delete(key);
      });
    }
    function pendingCount(unit) {
      let count = 0;
      const normalized = normalizeUnitKey(unit);
      pendingBuilds.forEach((entry) => {
        if (normalizeUnitKey(entry.unit) === normalized) count += 1;
      });
      return count;
    }
    function maybeLogRuntimeProbe(gameView, myPlayer) {
      if (runtimeProbed) return;
      runtimeProbed = true;
      const probe = { configType: typeof gameView?.config };
      try {
        const config = typeof gameView.config === "function" ? gameView.config() : null;
        probe.hasConfig = !!config;
        if (config) {
          probe.configKeys = listMethods(config).slice(0, 40);
          probe.hasUnitInfo = typeof config.unitInfo === "function";
          probe.hasStructureInfo = typeof config.structureInfo === "function";
          if (typeof config.unitInfo === "function") {
            const info = tryCall(() => config.unitInfo(CITY_UNIT));
            probe.unitInfoCityKeys = info && typeof info === "object" ? Object.keys(info).slice(0, 20) : String(info);
            if (info && info.cost != null) {
              probe.cityCostType = typeof info.cost;
              probe.cityCostCall = String(tryCall(() => typeof info.cost === "function" ? info.cost(myPlayer) : info.cost));
            }
          }
        }
      } catch (error) {
        probe.error = String(error?.message || error || "");
      }
      roundLogger?.record("auto_eco_runtime_probe", probe);
    }
    function maybeLogCostModel(gold) {
      const now = performance.now();
      if (now - lastCostLogAt < 1e4) return;
      lastCostLogAt = now;
      const gameView = getGameView();
      const myPlayer = safeMyPlayer2(gameView);
      const city = getStructureCost(CITY_UNIT, {
        gameView,
        player: myPlayer,
        pendingCounts: pendingCountsByUnit(),
        observedCounts: observedCountsByUnit()
      });
      const port = getStructureCost(PORT_UNIT, {
        gameView,
        player: myPlayer,
        pendingCounts: pendingCountsByUnit(),
        observedCounts: observedCountsByUnit()
      });
      const factory = getStructureCost(FACTORY_UNIT, {
        gameView,
        player: myPlayer,
        pendingCounts: pendingCountsByUnit(),
        observedCounts: observedCountsByUnit()
      });
      roundLogger?.record("auto_eco_cost_model", {
        gold,
        cityCost: city.cost,
        cityCostSource: city.source,
        portCost: port.cost,
        portCostSource: port.source,
        factoryCost: factory.cost,
        factoryCostSource: factory.source,
        pendingCount: pendingBuilds.size,
        observedBuildCounts: Object.fromEntries(observedBuildCounts)
      });
      roundLogger?.record("eco_cost_model", {
        gold,
        cityCost: city.cost,
        cityCostSource: city.source,
        portCost: port.cost,
        portCostSource: port.source,
        factoryCost: factory.cost,
        factoryCostSource: factory.source,
        pendingCounts: pendingCountsByUnit(),
        observedCounts: observedCountsByUnit()
      });
    }
    function logPendingBlocked(unit) {
      const blockedCount = blockedTilesByUnit.get(normalizeUnitKey(unit))?.size || 0;
      if (blockedCount <= 0) return;
      roundLogger?.record("auto_eco_pending_blocked", { unit, blockedCount, pendingCount: pendingCount(unit) });
    }
    function isTileBlockedForUnit(unit, tile) {
      const blocked = blockedTilesByUnit.get(normalizeUnitKey(unit));
      return !!blocked && blocked.has(Number(tile));
    }
    function blockTile(unit, tile) {
      if (!Number.isFinite(Number(tile))) return;
      const normalized = normalizeUnitKey(unit);
      if (!blockedTilesByUnit.has(normalized)) blockedTilesByUnit.set(normalized, /* @__PURE__ */ new Set());
      blockedTilesByUnit.get(normalized).add(Number(tile));
    }
    function logSkip(reason, extra = {}) {
      const now = performance.now();
      if (now - lastSkipAt <= 5e3) return;
      lastSkipAt = now;
      roundLogger?.record("auto_eco_skipped", { reason, ...extra });
    }
    function setStatus(state, reason, result, extra = {}) {
      status = { state, reason, ...extra };
      return result;
    }
    function pendingCountsByUnit() {
      const out = {};
      pendingBuilds.forEach((entry) => {
        out[entry.unit] = (out[entry.unit] || 0) + 1;
      });
      return out;
    }
    function observedCountsByUnit() {
      const out = {};
      observedBuildCounts.forEach((count, key) => {
        if (key === "city") out.City = count;
        else if (key === "port") out.Port = count;
        else if (key === "factory") out.Factory = count;
        else if (key === "defense post") out["Defense Post"] = count;
        else if (key === "sam launcher") out["SAM Launcher"] = count;
        else out[key] = count;
      });
      return out;
    }
    function observedEcoBuildCount() {
      return (observedBuildCounts.get("city") || 0) + (observedBuildCounts.get("port") || 0) + (observedBuildCounts.get("factory") || 0);
    }
  }
  async function collectOwnedTileCandidates(gameView, myPlayer, myState, mapData) {
    if (!myPlayer || typeof myPlayer.borderTiles !== "function") return [];
    const info = await myPlayer.borderTiles();
    const borders = info && info.borderTiles;
    if (!borders || typeof borders.forEach !== "function") return [];
    const seen = /* @__PURE__ */ new Set();
    borders.forEach((tile) => addOwnedCandidate(tile, seen, gameView, myState, mapData));
    Array.from(seen).forEach((tile) => {
      getNeighbors(gameView, tile, mapData).forEach((nearby) => addOwnedCandidate(nearby, seen, gameView, myState, mapData));
    });
    return Array.from(seen).map((tile) => scoreTile(tile, gameView, myState, mapData));
  }
  function addOwnedCandidate(tile, seen, gameView, myState, mapData) {
    if (!Number.isFinite(Number(tile))) return;
    const num = Number(tile);
    if (seen.has(num)) return;
    if (mapData?.terrain && !isLandByte(mapData.terrain[num])) return;
    if (!isOwnedByMe(gameView, num, myState)) return;
    seen.add(num);
  }
  function chooseCityTile(candidates, isBlocked) {
    const best = candidates.filter((candidate) => candidate.isLand && candidate.ownNeighbors >= 3 && !isBlocked(CITY_UNIT, candidate.tile)).sort((a, b) => b.cityScore - a.cityScore)[0];
    return best ? best.tile : null;
  }
  function choosePortTile(candidates, isBlocked) {
    const best = candidates.filter((candidate) => candidate.isLand && candidate.waterNeighbors > 0 && candidate.ownNeighbors >= 2 && !isBlocked(PORT_UNIT, candidate.tile)).sort((a, b) => b.portScore - a.portScore)[0];
    return best ? best.tile : null;
  }
  function chooseFactoryTile(candidates, isBlocked) {
    const best = candidates.filter((candidate) => candidate.isLand && candidate.ownNeighbors >= 3 && !isBlocked(FACTORY_UNIT, candidate.tile)).sort((a, b) => b.factoryScore - a.factoryScore)[0];
    return best ? best.tile : null;
  }
  function scoreTile(tile, gameView, myState, mapData) {
    const neighbors = getNeighbors(gameView, tile, mapData);
    let ownNeighbors = 0;
    let landNeighbors = 0;
    let waterNeighbors = 0;
    neighbors.forEach((nearby) => {
      if (mapData?.terrain && isLandByte(mapData.terrain[nearby])) landNeighbors += 1;
      else waterNeighbors += 1;
      if (isOwnedByMe(gameView, nearby, myState)) ownNeighbors += 1;
    });
    return {
      tile,
      isLand: !mapData?.terrain || isLandByte(mapData.terrain[tile]),
      ownNeighbors,
      landNeighbors,
      waterNeighbors,
      cityScore: ownNeighbors * 8 + landNeighbors * 2 - waterNeighbors * 5,
      portScore: waterNeighbors * 8 + ownNeighbors * 3 + landNeighbors,
      factoryScore: ownNeighbors * 9 + landNeighbors * 3 - waterNeighbors * 3
    };
  }
  function getNeighbors(gameView, tile, mapData) {
    try {
      if (gameView && typeof gameView.neighbors === "function") {
        const neighbours = gameView.neighbors(tile);
        if (Array.isArray(neighbours)) return neighbours.filter((value) => Number.isFinite(Number(value))).map(Number);
      }
    } catch (_) {
    }
    if (!mapData?.width || !mapData?.height) return [];
    const width = mapData.width;
    const height = mapData.height;
    const x = tile % width;
    const y = Math.floor(tile / width);
    const out = [];
    if (x > 0) out.push(tile - 1);
    if (x < width - 1) out.push(tile + 1);
    if (y > 0) out.push(tile - width);
    if (y < height - 1) out.push(tile + width);
    return out;
  }
  function isOwnedByMe(gameView, tile, myState) {
    try {
      if (!gameView || typeof gameView.owner !== "function") return false;
      const owner = gameView.owner(tile);
      if (!owner) return false;
      const ownerID = typeof owner.id === "function" ? owner.id() : owner.id;
      return ownerID === myState.id || ownerID === myState.smallID;
    } catch (_) {
      return false;
    }
  }
  function isEcoUnsafe(economy) {
    const currentRatio = Number(economy?.currentRatio);
    if (economy?.hasThreat && Number(economy.combatSafety) < 0.6 && Number.isFinite(currentRatio) && currentRatio < 0.18) return true;
    return !!(economy?.hasThreat && Number(economy.combatSafety) < 0.25 && Number(economy.currentRatio) < 0.45);
  }
  function safeMyPlayer2(gameView) {
    try {
      return gameView && typeof gameView.myPlayer === "function" ? gameView.myPlayer() : null;
    } catch (_) {
    }
    return null;
  }
  function normalizeUnitKey(unit) {
    return String(unit || "").trim().toLowerCase() || "unknown";
  }
  function listMethods(obj) {
    const out = [];
    try {
      let cursor = obj;
      while (cursor && cursor !== Object.prototype) {
        Object.getOwnPropertyNames(cursor).forEach((name) => {
          if (name !== "constructor" && !out.includes(name)) out.push(name);
        });
        cursor = Object.getPrototypeOf(cursor);
      }
    } catch (_) {
    }
    return out;
  }
  function tryCall(fn) {
    try {
      return fn();
    } catch (_) {
      return null;
    }
  }
  function buildKey(unit, tile) {
    return \`\${normalizeUnitKey(unit)}:\${Number(tile)}\`;
  }
  function totalEcoBuildCount(counts) {
    return Math.max(0, Number(counts?.city) || 0) + Math.max(0, Number(counts?.port) || 0) + Math.max(0, Number(counts?.factory) || 0);
  }

  // src/page/automation/auto-defense.js
  var DEFENSE_POST_UNIT = UNIT.DEFENSE_POST;
  var SAM_UNIT = UNIT.SAM_LAUNCHER;
  var INCOMING_FLOOR = 8e3;
  var COUNTER_FRACTION = 0.6;
  var AUTO_DEFENSE_MIN_COUNTER = 1e3;
  var MIN_COMBAT_SAFETY = 0.25;
  var BUILD_COOLDOWN_MS = 8e3;
  var COUNTER_COOLDOWN_MS = 2500;
  var PER_ATTACKER_COUNTER_FACTOR = 2.4;
  var MAX_AUTO_DEFENSE_POSTS = 4;
  var MAX_AUTO_SAM = 2;
  var MASSIVE_HUMAN_NEIGHBOUR_RATIO = 2;
  var DEFENSE_SETBACK_MIN_STEPS = 4;
  var DEFENSE_SETBACK_MAX_STEPS = 8;
  var DEFENSE_MIN_ENEMY_DISTANCE = 3;
  var DEFENSE_MIN_OWN_NEIGHBORS = 3;
  var PENDING_BUILD_FALLBACK_TTL_MS = 12e3;
  var SKIP_LOG_INTERVAL_MS = 5e3;
  function createAutoDefense({ gameState, mapDataRef, OrigWS, origWsSend, settings, logger, roundLogger, teamDetection, getTroopEconomy }) {
    const attackerCooldowns = /* @__PURE__ */ new Map();
    const pendingBuilds = /* @__PURE__ */ new Map();
    const observedBuildCounts = /* @__PURE__ */ new Map();
    const observedBuildKeys = /* @__PURE__ */ new Set();
    let lastBuildSentAt = 0;
    let lastCounterSentAt = 0;
    let lastSkipAt = 0;
    let buildInFlight = false;
    let status = { state: "idle", reason: "not_started", emergency: false };
    return {
      reset() {
        attackerCooldowns.clear();
        pendingBuilds.clear();
        observedBuildCounts.clear();
        observedBuildKeys.clear();
        lastBuildSentAt = 0;
        lastCounterSentAt = 0;
        lastSkipAt = 0;
        buildInFlight = false;
        status = { state: "idle", reason: "reset", emergency: false };
      },
      getStatus() {
        return status;
      },
      observeIntent(intent) {
        if (!intent || intent.type !== "build_unit" || !isDefenseBuildUnit(intent.unit)) return;
        observeBuildIntent(intent.unit, intent.tile);
      },
      run(options = {}) {
        const state = gameState.state;
        if (!options.force && !settings.get("autoDefense")) return setStatus("idle", "disabled", { emergency: false });
        if (!OrigWS) return setStatus("idle", "websocket_unavailable", { emergency: false });
        if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !state.myPlayerID) {
          return setStatus("idle", "not_ready", { emergency: false });
        }
        if (!origWsSend) return setStatus("idle", "socket_unavailable", { emergency: false });
        const myState = gameState.getMyState();
        if (!myState || !myState.isAlive) return setStatus("idle", "not_alive", { emergency: false });
        const myTroops = finiteOrZero(myState.troops);
        const incoming = incomingAttackTroops(gameState, myState);
        const pressureFloor = Math.max(INCOMING_FLOOR, myTroops * (Number(settings.get("autoDefenseIncomingRatio")) || 0.35));
        const underAttack = incoming > pressureFloor;
        const nukeThreat = !!options.nukeThreat;
        const attackers = incoming > 0 ? incomingAttackers(gameState, myState) : [];
        const seriousThreat = isSeriousThreat({
          myState,
          attackers,
          threats: options.threats,
          neighbourIDs: options.neighbourIDs
        });
        const serious = seriousThreat.serious;
        if (!serious && !nukeThreat) {
          return setStatus("idle", "no_pressure", { emergency: false, incoming });
        }
        const economy = getTroopEconomy?.();
        let lastAction = null;
        if (serious && underAttack && settings.get("autoDefenseCounterAttack")) {
          const counter = maybeCounterAttack({ myState, economy, attackers });
          if (counter) lastAction = counter;
        }
        maybeBuildDefenses({ myState, nukeThreat, buildPost: serious });
        return setStatus(serious ? "emergency" : "alert", serious ? "under_attack" : "nuke_threat", {
          emergency: serious,
          incoming,
          nukeThreat,
          attackerCount: attackers.length,
          topAttacker: attackers[0]?.name || null,
          seriousReason: seriousThreat.reason || null,
          seriousThreat: seriousThreat.name || null,
          effectiveTroopsRatio: seriousThreat.effectiveTroopsRatio || null,
          lastAction: lastAction || status.lastAction || null
        });
      }
    };
    function isSeriousThreat({ myState, attackers, threats, neighbourIDs }) {
      const humanAttacker = (attackers || []).find((attacker) => {
        const player = gameState.state.playerStates.get(attacker.id);
        if (!player || String(player.playerType || "").toUpperCase() !== "HUMAN") return false;
        if (isAllied2(myState, player)) return false;
        if (teamDetection?.isMyTeammate(player.id)) return false;
        return true;
      });
      if (humanAttacker) return { serious: true, reason: "active_human_attacker", id: humanAttacker.id, name: humanAttacker.name || humanAttacker.id };
      const myRecallableTroops = Math.max(1, finiteOrZero(myState.troops) + troopsInCombat(myState));
      const massiveNeighbour = (threats || []).find((threat) => {
        if (!threat || threat.id == null) return false;
        if (!neighbourIDs || !neighbourIDs.has(threat.id)) return false;
        const player = gameState.state.playerStates.get(threat.id);
        if (!isHostileHuman(myState, player, teamDetection)) return false;
        const effectiveEnemyTroops = Math.max(0, finiteOrZero(player.troops) - troopsInCombat(player));
        return effectiveEnemyTroops / myRecallableTroops >= MASSIVE_HUMAN_NEIGHBOUR_RATIO;
      });
      if (massiveNeighbour) {
        const player = gameState.state.playerStates.get(massiveNeighbour.id);
        const effectiveEnemyTroops = Math.max(0, finiteOrZero(player?.troops) - troopsInCombat(player));
        return {
          serious: true,
          reason: "massive_human_neighbour",
          id: massiveNeighbour.id,
          name: massiveNeighbour.name || massiveNeighbour.id,
          effectiveTroopsRatio: effectiveEnemyTroops / myRecallableTroops
        };
      }
      return { serious: false, reason: null };
    }
    function maybeCounterAttack({ myState, economy, attackers }) {
      if (!attackers.length) return null;
      const now = performance.now();
      if (now - lastCounterSentAt < COUNTER_COOLDOWN_MS) return null;
      const combatSafety = Number(economy?.combatSafety);
      if (Number.isFinite(combatSafety) && combatSafety < MIN_COMBAT_SAFETY) {
        logSkip("counter_unsafe", { combatSafety });
        return null;
      }
      const safeSpendable = finiteOrZero(economy?.safeSpendableTroops);
      if (safeSpendable <= AUTO_DEFENSE_MIN_COUNTER) {
        logSkip("counter_no_troops", { safeSpendable });
        return null;
      }
      for (let i = 0; i < attackers.length; i += 1) {
        const attacker = attackers[i];
        const gate = validateCounter(attacker, myState);
        if (!gate.ok) {
          logSkip("counter_blocked", { reason: gate.reason, attacker: attacker.name });
          continue;
        }
        const lastForAttacker = attackerCooldowns.get(attacker.id) || 0;
        if (now - lastForAttacker < COUNTER_COOLDOWN_MS * PER_ATTACKER_COUNTER_FACTOR) continue;
        const troops = Math.max(1, Math.floor(Math.min(safeSpendable * COUNTER_FRACTION, safeSpendable)));
        origWsSend.call(
          gameState.state.gameSocket,
          JSON.stringify({ type: "intent", intent: { type: "attack", targetID: attacker.id, troops } })
        );
        lastCounterSentAt = now;
        attackerCooldowns.set(attacker.id, now);
        logger.info(\`Auto-defense counter \${attacker.name}: \${troops} troops\`);
        roundLogger?.record("auto_defense_counter_sent", {
          targetID: attacker.id,
          targetName: attacker.name,
          attackerTroops: attacker.troops,
          troops,
          safeSpendable: Math.floor(safeSpendable),
          combatSafety
        });
        return \`counter \${attacker.name}\`;
      }
      return null;
    }
    function maybeBuildDefenses({ myState, nukeThreat, buildPost }) {
      if (buildInFlight) return;
      const now = performance.now();
      if (now - lastBuildSentAt < BUILD_COOLDOWN_MS) return;
      expirePendingBuilds();
      const gameView = getGameView();
      if (!gameView || typeof gameView.myPlayer !== "function") {
        logSkip("build_no_game_view");
        return;
      }
      const myPlayer = safeMyPlayer3(gameView);
      if (!myPlayer) {
        logSkip("build_no_player");
        return;
      }
      const wantSam = settings.get("autoDefenseBuildSam") && nukeThreat && canBuild(gameView, myPlayer, myState, SAM_UNIT, MAX_AUTO_SAM);
      const wantPost = buildPost && settings.get("autoDefenseBuildPosts") && canBuild(gameView, myPlayer, myState, DEFENSE_POST_UNIT, MAX_AUTO_DEFENSE_POSTS);
      const plan = wantSam ? { unit: SAM_UNIT, chooseTile: chooseCentralTile } : wantPost ? { unit: DEFENSE_POST_UNIT, chooseTile: chooseFrontierTile } : null;
      if (!plan) return;
      buildInFlight = true;
      plan.chooseTile(gameView, myPlayer, myState, mapDataRef.current).then((tile) => {
        buildInFlight = false;
        if (tile == null) {
          logSkip("build_no_tile", { unit: plan.unit });
          return;
        }
        sendBuild(plan.unit, tile);
      }).catch((error) => {
        buildInFlight = false;
        logSkip("build_tile_scan_failed", { message: String(error?.message || error || "") });
      });
    }
    function canBuild(gameView, myPlayer, myState, unit, cap) {
      const runtimeCount = countPlayerUnits(myPlayer, unit);
      const observedCount = observedCountOf(unit);
      const pendingCount = pendingCountOf(unit);
      const existing = Math.max(runtimeCount, observedCount) + pendingCount;
      if (existing >= cap) {
        logSkip("build_max", { unit, existing, runtimeCount, observedCount, pendingCount });
        return false;
      }
      const cost = getStructureCost(unit, {
        gameView,
        player: myPlayer,
        pendingCounts: pendingCountsByUnit(),
        observedCounts: observedCountsByUnit()
      });
      const gold = finiteOrZero(myState.gold);
      if (gold < cost.cost + cost.reserveGold) {
        logSkip("build_gold", { unit, gold, estimatedCost: cost.cost });
        return false;
      }
      return true;
    }
    function sendBuild(unit, tile) {
      const gameView = getGameView();
      const cost = getStructureCost(unit, {
        gameView,
        player: safeMyPlayer3(gameView),
        pendingCounts: pendingCountsByUnit(),
        observedCounts: observedCountsByUnit()
      });
      origWsSend.call(
        gameState.state.gameSocket,
        JSON.stringify({ type: "intent", intent: { type: "build_unit", unit, tile } })
      );
      lastBuildSentAt = performance.now();
      addPendingBuild(unit, tile);
      logger.info(\`Auto-defense build \${unit} at \${tile}\`);
      roundLogger?.record("auto_defense_build_sent", {
        unit,
        tile,
        estimatedCost: cost.cost,
        costSource: cost.source,
        pendingCounts: pendingCountsByUnit(),
        observedCounts: observedCountsByUnit()
      });
    }
    function validateCounter(attacker, myState) {
      const player = gameState.state.playerStates.get(attacker.id);
      if (!player || !player.isAlive) return { ok: false, reason: "target_not_alive" };
      if (String(player.playerType || "").toUpperCase() !== "HUMAN") return { ok: false, reason: "target_not_human" };
      if (isAllied2(myState, player)) return { ok: false, reason: "target_allied" };
      if (teamDetection?.isMyTeammate(player.id)) return { ok: false, reason: "target_team" };
      return { ok: true };
    }
    function addPendingBuild(unit, tile) {
      const duration = getConstructionDurationMs(unit, { gameView: getGameView() });
      pendingBuilds.set(Number(tile), { unit, sentAt: performance.now(), ttlMs: duration.durationMs + 1500 });
    }
    function observeBuildIntent(unit, tile) {
      const key = buildKey2(unit, tile);
      if (observedBuildKeys.has(key)) return;
      observedBuildKeys.add(key);
      if (Number.isFinite(Number(tile))) pendingBuilds.delete(Number(tile));
      observedBuildCounts.set(unit, observedCountOf(unit) + 1);
    }
    function expirePendingBuilds() {
      const now = performance.now();
      pendingBuilds.forEach((entry, tile) => {
        if (now - entry.sentAt > (entry.ttlMs || PENDING_BUILD_FALLBACK_TTL_MS)) pendingBuilds.delete(tile);
      });
    }
    function pendingCountOf(unit) {
      let count = 0;
      pendingBuilds.forEach((entry) => {
        if (entry.unit === unit) count += 1;
      });
      return count;
    }
    function observedCountOf(unit) {
      return observedBuildCounts.get(unit) || 0;
    }
    function pendingCountsByUnit() {
      const out = {};
      pendingBuilds.forEach((entry) => {
        out[entry.unit] = (out[entry.unit] || 0) + 1;
      });
      return out;
    }
    function observedCountsByUnit() {
      return Object.fromEntries(observedBuildCounts);
    }
    function isPendingTile(tile) {
      return pendingBuilds.has(Number(tile));
    }
    function chooseFrontierTile(gameView, myPlayer, myState, mapData) {
      return scanBorderTiles(
        gameView,
        myPlayer,
        myState,
        mapData,
        (tile) => countEnemyNeighbors(gameView, tile, myState, mapData, teamDetection, gameState),
        1
      ).then((frontier) => frontier == null ? null : chooseSafeDefenseTile(gameView, frontier, myState, mapData));
    }
    function chooseSafeDefenseTile(gameView, frontier, myState, mapData) {
      const candidates = collectSetbackCandidates(gameView, frontier, myState, mapData);
      const preferred = candidates.filter((candidate) => candidate.safe);
      const pool = preferred.length ? preferred : candidates.filter((candidate) => candidate.relaxed);
      const best = pool.sort((a, b) => b.score - a.score)[0];
      return best ? best.tile : null;
    }
    function collectSetbackCandidates(gameView, frontier, myState, mapData) {
      const out = [];
      const visited = /* @__PURE__ */ new Set([Number(frontier)]);
      const queue = [{ tile: Number(frontier), distance: 0 }];
      for (let i = 0; i < queue.length; i += 1) {
        const current = queue[i];
        if (current.distance >= DEFENSE_SETBACK_MAX_STEPS) continue;
        getNeighbors2(gameView, current.tile, mapData).forEach((nearby) => {
          const tile = Number(nearby);
          if (!Number.isFinite(tile) || visited.has(tile)) return;
          visited.add(tile);
          if (mapData?.terrain && !isLandByte(mapData.terrain[tile])) return;
          if (!isOwnedByMe2(gameView, tile, myState)) return;
          const distance = current.distance + 1;
          queue.push({ tile, distance });
          if (isPendingTile(tile)) return;
          const enemyNeighbors = countEnemyNeighbors(gameView, tile, myState, mapData, teamDetection, gameState);
          if (enemyNeighbors > 0 || distance < 3) return;
          const ownNeighbors = countOwnNeighbors(gameView, tile, myState, mapData);
          const enemyDistance = nearestEnemyDistance(gameView, tile, myState, mapData, teamDetection, gameState, DEFENSE_SETBACK_MAX_STEPS);
          const safe = distance >= DEFENSE_SETBACK_MIN_STEPS && ownNeighbors >= DEFENSE_MIN_OWN_NEIGHBORS && enemyDistance >= DEFENSE_MIN_ENEMY_DISTANCE;
          const relaxed = ownNeighbors >= 2 && enemyDistance >= 2;
          if (!safe && !relaxed) return;
          out.push({
            tile,
            distance,
            ownNeighbors,
            enemyDistance,
            safe,
            relaxed,
            score: enemyDistance * 24 + ownNeighbors * 10 - Math.abs(distance - 5) * 6
          });
        });
      }
      return out;
    }
    function chooseCentralTile(gameView, myPlayer, myState, mapData) {
      return scanBorderTiles(gameView, myPlayer, myState, mapData, (tile) => countOwnNeighbors(gameView, tile, myState, mapData), 0);
    }
    function scanBorderTiles(gameView, myPlayer, myState, mapData, scoreFn, minScore) {
      return Promise.resolve().then(() => typeof myPlayer.borderTiles === "function" ? myPlayer.borderTiles() : null).then((info) => {
        const borders = info && info.borderTiles;
        if (!borders || typeof borders.forEach !== "function") return null;
        let best = null;
        let bestScore = minScore - 1;
        borders.forEach((tile) => {
          const num = Number(tile);
          if (!Number.isFinite(num) || isPendingTile(num)) return;
          if (mapData?.terrain && !isLandByte(mapData.terrain[num])) return;
          if (!isOwnedByMe2(gameView, num, myState)) return;
          const value = scoreFn(num);
          if (value > bestScore) {
            bestScore = value;
            best = num;
          }
        });
        return bestScore >= minScore ? best : null;
      });
    }
    function logSkip(reason, extra = {}) {
      const now = performance.now();
      if (now - lastSkipAt < SKIP_LOG_INTERVAL_MS) return;
      lastSkipAt = now;
      roundLogger?.record("auto_defense_skipped", { reason, ...extra });
    }
    function setStatus(state, reason, extra = {}) {
      status = { state, reason, ...extra };
      return status;
    }
  }
  function countEnemyNeighbors(gameView, tile, myState, mapData, teamDetection, gameState) {
    let count = 0;
    getNeighbors2(gameView, tile, mapData).forEach((nearby) => {
      if (mapData?.terrain && !isLandByte(mapData.terrain[nearby])) return;
      if (isEnemyOwned(gameView, nearby, myState, teamDetection, gameState)) count += 1;
    });
    return count;
  }
  function countOwnNeighbors(gameView, tile, myState, mapData) {
    let count = 0;
    getNeighbors2(gameView, tile, mapData).forEach((nearby) => {
      if (mapData?.terrain && !isLandByte(mapData.terrain[nearby])) return;
      if (isOwnedByMe2(gameView, nearby, myState)) count += 1;
    });
    return count;
  }
  function nearestEnemyDistance(gameView, startTile, myState, mapData, teamDetection, gameState, maxDistance) {
    const start = Number(startTile);
    if (!Number.isFinite(start)) return maxDistance + 1;
    const visited = /* @__PURE__ */ new Set([start]);
    const queue = [{ tile: start, distance: 0 }];
    for (let i = 0; i < queue.length; i += 1) {
      const current = queue[i];
      if (current.distance >= maxDistance) continue;
      const nextDistance = current.distance + 1;
      const neighbours = getNeighbors2(gameView, current.tile, mapData);
      for (let n = 0; n < neighbours.length; n += 1) {
        const nearby = Number(neighbours[n]);
        if (!Number.isFinite(nearby) || visited.has(nearby)) continue;
        visited.add(nearby);
        if (mapData?.terrain && !isLandByte(mapData.terrain[nearby])) continue;
        if (isEnemyOwned(gameView, nearby, myState, teamDetection, gameState)) return nextDistance;
        queue.push({ tile: nearby, distance: nextDistance });
      }
    }
    return maxDistance + 1;
  }
  function isEnemyOwned(gameView, tile, myState, teamDetection, gameState) {
    try {
      if (!gameView || typeof gameView.owner !== "function") return false;
      const owner = gameView.owner(tile);
      if (!owner) return false;
      const isPlayer = typeof owner.isPlayer === "function" ? owner.isPlayer() : true;
      if (!isPlayer) return false;
      const ownerID = typeof owner.id === "function" ? owner.id() : owner.id;
      if (ownerID == null) return false;
      if (ownerID === myState.id || ownerID === myState.smallID) return false;
      const player = resolvePlayerByOwnerID(gameState, ownerID);
      return isHostileHuman(myState, player, teamDetection);
    } catch (_) {
      return false;
    }
  }
  function isOwnedByMe2(gameView, tile, myState) {
    try {
      if (!gameView || typeof gameView.owner !== "function") return false;
      const owner = gameView.owner(tile);
      if (!owner) return false;
      const ownerID = typeof owner.id === "function" ? owner.id() : owner.id;
      return ownerID === myState.id || ownerID === myState.smallID;
    } catch (_) {
      return false;
    }
  }
  function getNeighbors2(gameView, tile, mapData) {
    try {
      if (gameView && typeof gameView.neighbors === "function") {
        const neighbours = gameView.neighbors(tile);
        if (Array.isArray(neighbours)) return neighbours.filter((value) => Number.isFinite(Number(value))).map(Number);
      }
    } catch (_) {
    }
    if (!mapData?.width || !mapData?.height) return [];
    const width = mapData.width;
    const height = mapData.height;
    const x = tile % width;
    const y = Math.floor(tile / width);
    const out = [];
    if (x > 0) out.push(tile - 1);
    if (x < width - 1) out.push(tile + 1);
    if (y > 0) out.push(tile - width);
    if (y < height - 1) out.push(tile + width);
    return out;
  }
  function isAllied2(myState, target) {
    const alliances = Array.isArray(myState.alliances) ? myState.alliances : [];
    return alliances.some((alliance) => alliance && alliance.other === target.id);
  }
  function isHostileHuman(myState, player, teamDetection) {
    if (!player || !player.isAlive) return false;
    if (String(player.playerType || "").toUpperCase() !== "HUMAN") return false;
    if (isAllied2(myState, player)) return false;
    if (teamDetection?.isMyTeammate(player.id)) return false;
    return true;
  }
  function resolvePlayerByOwnerID(gameState, ownerID) {
    if (!gameState || ownerID == null) return null;
    const direct = gameState.state.playerStates.get(ownerID);
    if (direct) return direct;
    let found = null;
    gameState.state.playerStates.forEach((player) => {
      if (!found && player && player.smallID === ownerID) found = player;
    });
    return found;
  }
  function safeMyPlayer3(gameView) {
    try {
      return gameView && typeof gameView.myPlayer === "function" ? gameView.myPlayer() : null;
    } catch (_) {
    }
    return null;
  }
  function isDefenseBuildUnit(unit) {
    return unit === DEFENSE_POST_UNIT || unit === SAM_UNIT;
  }
  function buildKey2(unit, tile) {
    return \`\${String(unit || "").trim().toLowerCase()}:\${Number(tile)}\`;
  }

  // src/page/automation/auto-boat.js
  var BOAT_COOLDOWN_MS = 4e3;
  var BOAT_WAVE_RATIO = 0.15;
  var MIN_BOAT_TROOPS = 1;
  var MIN_SURPLUS = 500;
  var BFS_TILE_CAP = 6e3;
  var MAX_BOAT_DISTANCE = 90;
  function createAutoBoat({ gameState, mapDataRef, OrigWS, origWsSend, settings, logger, roundLogger, getTroopEconomy }) {
    let lastSentAt = 0;
    let lastSkipAt = 0;
    let inFlight = false;
    let lastDst = -1;
    let status = { state: "idle", reason: "not_started" };
    return {
      reset() {
        lastSentAt = 0;
        lastSkipAt = 0;
        inFlight = false;
        lastDst = -1;
        status = { state: "idle", reason: "reset" };
      },
      getStatus() {
        return status;
      },
      run(options = {}) {
        const state = gameState.state;
        if (!options.force && !settings.get("autoBoat")) return setStatus("idle", "disabled");
        if (!OrigWS) return setStatus("idle", "websocket_unavailable");
        if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !state.myPlayerID) {
          return setStatus("idle", "not_ready");
        }
        if (!origWsSend) return setStatus("idle", "socket_unavailable");
        if (!isInsideFarmWindow(state, settings)) return setStatus("idle", "outside_window");
        if (inFlight) return setStatus("cooldown", "tile_scan");
        const now = performance.now();
        if (now - lastSentAt < BOAT_COOLDOWN_MS) {
          return setStatus("cooldown", "cooldown", { cooldownMs: Math.round(BOAT_COOLDOWN_MS - (now - lastSentAt)) });
        }
        const myState = gameState.getMyState();
        if (!myState || !myState.isAlive) return setStatus("idle", "not_alive");
        const economy = getTroopEconomy?.();
        const safeSpendable = finiteOrZero(economy?.safeSpendableTroops);
        const maxTroops = finiteOrZero(economy?.maxTroops);
        if (safeSpendable <= MIN_SURPLUS || maxTroops <= 0) {
          logSkip("reserve_limit", { safeSpendable });
          return setStatus("blocked", "reserve_limit", { safeSpendable });
        }
        const troops = Math.max(MIN_BOAT_TROOPS, Math.floor(Math.min(safeSpendable, maxTroops * BOAT_WAVE_RATIO)));
        const gameView = getGameView();
        if (!gameView || typeof gameView.myPlayer !== "function") {
          logSkip("no_game_view");
          return setStatus("blocked", "no_game_view");
        }
        const myPlayer = safeMyPlayer4(gameView);
        if (!myPlayer) {
          logSkip("no_player");
          return setStatus("blocked", "no_player");
        }
        inFlight = true;
        chooseBoatDst(gameView, myPlayer, mapDataRef.current).then((dst) => {
          inFlight = false;
          if (dst == null) {
            logSkip("no_water_target");
            setStatus("blocked", "no_water_target");
            return;
          }
          origWsSend.call(
            gameState.state.gameSocket,
            JSON.stringify({ type: "intent", intent: { type: "boat", troops, dst } })
          );
          lastSentAt = performance.now();
          lastDst = dst;
          logger.info(\`Auto-boat expand: \${troops} troops -> tile \${dst}\`);
          roundLogger?.record("auto_boat_sent", { dst, troops, safeSpendable: Math.floor(safeSpendable) });
          setStatus("sent", \`sent_\${troops}\`, { dst, troops });
        }).catch((error) => {
          inFlight = false;
          logSkip("tile_scan_failed", { message: String(error?.message || error || "") });
          setStatus("blocked", "tile_scan_failed");
        });
        return setStatus("scanning", "scanning");
      }
    };
    function logSkip(reason, extra = {}) {
      const now = performance.now();
      if (now - lastSkipAt < 5e3) return;
      lastSkipAt = now;
      roundLogger?.record("auto_boat_skipped", { reason, ...extra });
    }
    function setStatus(state, reason, extra = {}) {
      status = { state, reason, ...extra };
      return status;
    }
  }
  function chooseBoatDst(gameView, myPlayer, mapData) {
    return Promise.resolve().then(() => typeof myPlayer.borderTiles === "function" ? myPlayer.borderTiles() : null).then((info) => {
      const borders = info && info.borderTiles;
      if (!borders || typeof borders.forEach !== "function") return null;
      const width = mapData?.width;
      const height = mapData?.height;
      const terrain = mapData?.terrain;
      if (!width || !height || !terrain) return null;
      const visited = /* @__PURE__ */ new Set();
      const queue = [];
      borders.forEach((tile) => {
        const num = Number(tile);
        if (!Number.isFinite(num)) return;
        neighborsOf(num, width, height).forEach((nb) => {
          if (!isLandByte(terrain[nb]) && !visited.has(nb)) {
            visited.add(nb);
            queue.push({ tile: nb, dist: 1 });
          }
        });
      });
      let head = 0;
      while (head < queue.length && visited.size < BFS_TILE_CAP) {
        const { tile, dist } = queue[head];
        head += 1;
        if (dist > MAX_BOAT_DISTANCE) continue;
        const nbs = neighborsOf(tile, width, height);
        for (let i = 0; i < nbs.length; i += 1) {
          const nb = nbs[i];
          if (visited.has(nb)) continue;
          if (isLandByte(terrain[nb])) {
            if (isUnclaimedLand(gameView, nb)) return nb;
            visited.add(nb);
          } else {
            visited.add(nb);
            queue.push({ tile: nb, dist: dist + 1 });
          }
        }
      }
      return null;
    });
  }
  function isUnclaimedLand(gameView, tile) {
    try {
      if (typeof gameView.hasOwner === "function") return !gameView.hasOwner(tile);
      const owner = typeof gameView.owner === "function" ? gameView.owner(tile) : null;
      if (!owner) return true;
      return typeof owner.isPlayer === "function" ? !owner.isPlayer() : false;
    } catch (_) {
      return false;
    }
  }
  function neighborsOf(tile, width, height) {
    const x = tile % width;
    const y = Math.floor(tile / width);
    const out = [];
    if (x > 0) out.push(tile - 1);
    if (x < width - 1) out.push(tile + 1);
    if (y > 0) out.push(tile - width);
    if (y < height - 1) out.push(tile + width);
    return out;
  }
  function safeMyPlayer4(gameView) {
    try {
      return gameView && typeof gameView.myPlayer === "function" ? gameView.myPlayer() : null;
    } catch (_) {
    }
    return null;
  }

  // src/page/automation/auto-weapons.js
  var SILO_UNIT = UNIT.MISSILE_SILO;
  var SILO_BUILD_COOLDOWN_MS = 15e3;
  var LAUNCH_COOLDOWN_MS = 12e3;
  var EARLY_NUKE_COOLDOWN_MS = 8e3;
  var MIN_COMBAT_SAFETY_BUILD = 0.6;
  var MIN_COMBAT_SAFETY_LAUNCH = 0.45;
  var MIN_COMBAT_SAFETY_EARLY = 0.3;
  var MAX_AUTO_SILOS = 1;
  var STABLE_ECO_STATES = /* @__PURE__ */ new Set(["GROWTH_PEAK", "READY", "PUSH", "CAP_WASTE"]);
  function createAutoWeapons({ gameState, mapDataRef, OrigWS, origWsSend, settings, logger, roundLogger, teamDetection, getTroopEconomy }) {
    const pendingSilos = /* @__PURE__ */ new Map();
    let lastSiloAt = 0;
    let lastLaunchAt = 0;
    let lastEarlyNukeAt = 0;
    let lastSkipAt = 0;
    let buildInFlight = false;
    let status = { state: "idle", reason: "not_started" };
    return {
      reset() {
        pendingSilos.clear();
        lastSiloAt = 0;
        lastLaunchAt = 0;
        lastEarlyNukeAt = 0;
        lastSkipAt = 0;
        buildInFlight = false;
        status = { state: "idle", reason: "reset" };
      },
      getStatus() {
        return status;
      },
      run({ threats } = {}, options = {}) {
        const state = gameState.state;
        if (!options.force && !settings.get("autoWeapons")) return setStatus("idle", "disabled");
        if (!OrigWS) return setStatus("idle", "websocket_unavailable");
        if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !state.myPlayerID) {
          return setStatus("idle", "not_ready");
        }
        if (!origWsSend) return setStatus("idle", "socket_unavailable");
        if (buildInFlight) return setStatus("cooldown", "tile_scan");
        const myState = gameState.getMyState();
        if (!myState || !myState.isAlive) return setStatus("idle", "not_alive");
        const economy = getTroopEconomy?.();
        const gameView = getGameView();
        if (!gameView || typeof gameView.myPlayer !== "function") return setStatus("blocked", "no_game_view");
        const myPlayer = safeMyPlayer5(gameView);
        if (!myPlayer) return setStatus("blocked", "no_player");
        expirePendingSilos();
        const builtSilos = countPlayerUnits(myPlayer, SILO_UNIT);
        const siloCount = builtSilos + pendingSilos.size;
        if (settings.get("autoWeaponsEarlyNuke") && builtSilos >= 1) {
          const early = maybeEarlyNuke({ myState, economy, gameView, myPlayer, threats });
          if (early) return early;
        }
        if (!economy || !STABLE_ECO_STATES.has(economy.state)) {
          return setStatus("idle", "eco_unstable", { ecoState: economy?.state || null });
        }
        if (siloCount < MAX_AUTO_SILOS) {
          return maybeBuildSilo({ myState, economy, gameView, myPlayer });
        }
        return maybeLaunch({ myState, economy, gameView, myPlayer, threats });
      }
    };
    function maybeEarlyNuke({ myState, economy, gameView, myPlayer, threats }) {
      const now = performance.now();
      if (now - lastEarlyNukeAt < EARLY_NUKE_COOLDOWN_MS) return null;
      if (finiteOrZero(economy?.combatSafety) < MIN_COMBAT_SAFETY_EARLY) return null;
      const sams = enemySamSitesUnderConstruction(gameView, myState, teamDetection);
      if (!sams.length) return null;
      const cost = getStructureCost(UNIT.ATOM_BOMB, { gameView, player: myPlayer });
      if (cost.source !== "runtime") {
        logSkip("early_no_atom_cost");
        return null;
      }
      const gold = finiteOrZero(myState.gold);
      if (gold < cost.cost + cost.reserveGold) {
        logSkip("early_atom_gold", { gold, estimatedCost: cost.cost });
        return null;
      }
      const threatRank = /* @__PURE__ */ new Map();
      (threats || []).forEach((threat, index) => {
        if (threat && threat.id != null) threatRank.set(threat.id, index);
      });
      sams.sort((a, b) => rankOf(threatRank, a.ownerID) - rankOf(threatRank, b.ownerID));
      const target = sams[0];
      origWsSend.call(
        gameState.state.gameSocket,
        JSON.stringify({ type: "intent", intent: { type: "build_unit", unit: UNIT.ATOM_BOMB, tile: target.tile } })
      );
      lastEarlyNukeAt = now;
      lastLaunchAt = now;
      logger.info(\`Auto-weapons EARLY NUKE \${target.ownerName} SAM @\${target.tile}\`);
      roundLogger?.record("auto_weapons_early_nuke_sent", {
        unit: UNIT.ATOM_BOMB,
        tile: target.tile,
        targetName: target.ownerName,
        targetID: target.ownerID,
        estimatedCost: cost.cost
      });
      return setStatus("sent", "early_nuke", { unit: UNIT.ATOM_BOMB, target: target.ownerName });
    }
    function maybeBuildSilo({ myState, economy, gameView, myPlayer }) {
      const now = performance.now();
      if (now - lastSiloAt < SILO_BUILD_COOLDOWN_MS) return setStatus("cooldown", "silo_cooldown");
      if (finiteOrZero(economy.combatSafety) < MIN_COMBAT_SAFETY_BUILD) return setStatus("idle", "unsafe_flank");
      const cost = getStructureCost(SILO_UNIT, { gameView, player: myPlayer, pendingCounts: { [SILO_UNIT]: pendingSilos.size } });
      const gold = finiteOrZero(myState.gold);
      if (gold < cost.cost + cost.reserveGold) {
        logSkip("silo_gold", { gold, estimatedCost: cost.cost });
        return setStatus("blocked", "silo_gold", { gold, estimatedCost: cost.cost });
      }
      buildInFlight = true;
      chooseCentralTile(gameView, myPlayer, myState, mapDataRef.current).then((tile) => {
        buildInFlight = false;
        if (tile == null) {
          logSkip("silo_no_tile");
          return;
        }
        origWsSend.call(
          gameState.state.gameSocket,
          JSON.stringify({ type: "intent", intent: { type: "build_unit", unit: SILO_UNIT, tile } })
        );
        lastSiloAt = performance.now();
        addPendingSilo(tile);
        logger.info(\`Auto-weapons build Missile Silo at \${tile}\`);
        roundLogger?.record("auto_weapons_silo_sent", { tile, estimatedCost: cost.cost, costSource: cost.source });
      }).catch((error) => {
        buildInFlight = false;
        logSkip("silo_tile_scan_failed", { message: String(error?.message || error || "") });
      });
      return setStatus("scanning", "silo_scan");
    }
    function maybeLaunch({ myState, economy, gameView, myPlayer, threats }) {
      const now = performance.now();
      if (now - lastLaunchAt < LAUNCH_COOLDOWN_MS) return setStatus("cooldown", "launch_cooldown");
      if (finiteOrZero(economy.combatSafety) < MIN_COMBAT_SAFETY_LAUNCH) return setStatus("idle", "unsafe_launch");
      const weapon = chooseAffordableWeapon(gameView, myPlayer, finiteOrZero(myState.gold));
      if (!weapon) {
        logSkip("no_affordable_weapon");
        return setStatus("idle", "no_affordable_weapon");
      }
      buildInFlight = true;
      chooseNukeTarget(gameView, myPlayer, myState, threats || []).then((target) => {
        buildInFlight = false;
        if (!target) {
          logSkip("no_target");
          setStatus("idle", "no_target");
          return;
        }
        origWsSend.call(
          gameState.state.gameSocket,
          JSON.stringify({ type: "intent", intent: { type: "build_unit", unit: weapon.unit, tile: target.tile } })
        );
        lastLaunchAt = performance.now();
        logger.info(\`Auto-weapons launch \${weapon.unit} -> \${target.targetName} @\${target.tile}\`);
        roundLogger?.record("auto_weapons_launch_sent", {
          unit: weapon.unit,
          tile: target.tile,
          targetName: target.targetName,
          targetID: target.targetID,
          estimatedCost: weapon.cost
        });
        setStatus("sent", \`launch_\${weapon.unit}\`, { unit: weapon.unit, target: target.targetName });
      }).catch((error) => {
        buildInFlight = false;
        logSkip("target_scan_failed", { message: String(error?.message || error || "") });
      });
      return setStatus("scanning", "launch_scan");
    }
    function chooseAffordableWeapon(gameView, myPlayer, gold) {
      for (let i = 0; i < WEAPONS_BY_POWER.length; i += 1) {
        const unit = WEAPONS_BY_POWER[i];
        const cost = getStructureCost(unit, { gameView, player: myPlayer });
        if (cost.source !== "runtime") continue;
        if (gold >= cost.cost + cost.reserveGold) return { unit, cost: cost.cost };
      }
      return null;
    }
    function chooseNukeTarget(gameView, myPlayer, myState, threats) {
      const threatRank = /* @__PURE__ */ new Map();
      (threats || []).forEach((threat, index) => {
        if (threat && threat.id != null) threatRank.set(threat.id, index);
      });
      return Promise.resolve().then(() => typeof myPlayer.borderTiles === "function" ? myPlayer.borderTiles() : null).then((info) => {
        const borders = info && info.borderTiles;
        if (!borders || typeof borders.forEach !== "function") return null;
        const enemyTile = /* @__PURE__ */ new Map();
        const enemyName = /* @__PURE__ */ new Map();
        borders.forEach((tile2) => {
          const num = Number(tile2);
          if (!Number.isFinite(num)) return;
          neighborsOf2(num, mapDataRef.current).forEach((nb) => {
            const owner = ownerOf(gameView, nb);
            if (!owner || owner.id == null) return;
            if (owner.id === myState.id || owner.id === myState.smallID) return;
            if (teamDetection?.isMyTeammate?.(owner.id)) return;
            if (isAllied3(myState, owner.id)) return;
            if (!enemyTile.has(owner.id)) {
              enemyTile.set(owner.id, nb);
              enemyName.set(owner.id, owner.name);
            }
          });
        });
        if (enemyTile.size === 0) return null;
        let bestID = null;
        let bestRank = Infinity;
        enemyTile.forEach((_tile, id) => {
          const rank = threatRank.has(id) ? threatRank.get(id) : 999;
          if (rank < bestRank) {
            bestRank = rank;
            bestID = id;
          }
        });
        if (bestID == null) return null;
        const boundaryTile = enemyTile.get(bestID);
        const tile = stepIntoTerritory(gameView, boundaryTile, bestID) ?? boundaryTile;
        return { tile, targetID: bestID, targetName: enemyName.get(bestID) || bestID };
      });
    }
    function stepIntoTerritory(gameView, tile, ownerID) {
      const nbs = neighborsOf2(tile, mapDataRef.current);
      for (let i = 0; i < nbs.length; i += 1) {
        const owner = ownerOf(gameView, nbs[i]);
        if (owner && owner.id === ownerID) return nbs[i];
      }
      return null;
    }
    function chooseCentralTile(gameView, myPlayer, myState, mapData) {
      return Promise.resolve().then(() => typeof myPlayer.borderTiles === "function" ? myPlayer.borderTiles() : null).then((info) => {
        const borders = info && info.borderTiles;
        if (!borders || typeof borders.forEach !== "function") return null;
        let best = null;
        let bestOwn = -1;
        borders.forEach((tile) => {
          const num = Number(tile);
          if (!Number.isFinite(num) || pendingSilos.has(num)) return;
          if (mapData?.terrain && !isLandByte(mapData.terrain[num])) return;
          if (!isOwnedByMe3(gameView, num, myState)) return;
          let own = 0;
          neighborsOf2(num, mapData).forEach((nb) => {
            if (isOwnedByMe3(gameView, nb, myState)) own += 1;
          });
          if (own > bestOwn) {
            bestOwn = own;
            best = num;
          }
        });
        return best;
      });
    }
    function addPendingSilo(tile) {
      const duration = getConstructionDurationMs(SILO_UNIT, { gameView: getGameView() });
      pendingSilos.set(Number(tile), { sentAt: performance.now(), ttlMs: duration.durationMs + 1500 });
    }
    function expirePendingSilos() {
      const now = performance.now();
      pendingSilos.forEach((entry, tile) => {
        if (now - entry.sentAt > (entry.ttlMs || 15e3)) pendingSilos.delete(tile);
      });
    }
    function logSkip(reason, extra = {}) {
      const now = performance.now();
      if (now - lastSkipAt < 5e3) return;
      lastSkipAt = now;
      roundLogger?.record("auto_weapons_skipped", { reason, ...extra });
    }
    function setStatus(state, reason, extra = {}) {
      status = { state, reason, ...extra };
      return status;
    }
  }
  function rankOf(rankMap, id) {
    return rankMap.has(id) ? rankMap.get(id) : 999;
  }
  function ownerOf(gameView, tile) {
    try {
      if (!gameView || typeof gameView.owner !== "function") return null;
      const owner = gameView.owner(tile);
      if (!owner) return null;
      const isPlayer = typeof owner.isPlayer === "function" ? owner.isPlayer() : true;
      if (!isPlayer) return null;
      const id = typeof owner.id === "function" ? owner.id() : owner.id;
      const name = typeof owner.displayName === "function" ? owner.displayName() : typeof owner.name === "function" ? owner.name() : id;
      return { id, name };
    } catch (_) {
      return null;
    }
  }
  function isOwnedByMe3(gameView, tile, myState) {
    try {
      if (!gameView || typeof gameView.owner !== "function") return false;
      const owner = gameView.owner(tile);
      if (!owner) return false;
      const ownerID = typeof owner.id === "function" ? owner.id() : owner.id;
      return ownerID === myState.id || ownerID === myState.smallID;
    } catch (_) {
      return false;
    }
  }
  function isAllied3(myState, ownerID) {
    const alliances = Array.isArray(myState.alliances) ? myState.alliances : [];
    return alliances.some((alliance) => alliance && alliance.other === ownerID);
  }
  function neighborsOf2(tile, mapData) {
    if (!mapData?.width || !mapData?.height) return [];
    const width = mapData.width;
    const height = mapData.height;
    const x = tile % width;
    const y = Math.floor(tile / width);
    const out = [];
    if (x > 0) out.push(tile - 1);
    if (x < width - 1) out.push(tile + 1);
    if (y > 0) out.push(tile - width);
    if (y < height - 1) out.push(tile + width);
    return out;
  }
  function safeMyPlayer5(gameView) {
    try {
      return gameView && typeof gameView.myPlayer === "function" ? gameView.myPlayer() : null;
    } catch (_) {
    }
    return null;
  }

  // src/page/automation/auto-team-support.js
  var SUPPORT_COOLDOWN_MS = 1e4;
  var TARGET_COOLDOWN_MS = 1e4;
  var MIN_DONATION_TROOPS = 5e3;
  var MAX_DONATION_OWN_SHARE = 1 / 3;
  var SUPPORT_RESERVE_RATIO = 0.55;
  function createAutoTeamSupport({ gameState, OrigWS, origWsSend, settings, logger, roundLogger, teamDetection, getTroopEconomy }) {
    const targetCooldowns = /* @__PURE__ */ new Map();
    let lastSentAt = 0;
    let lastSkipAt = 0;
    let lastCandidateLogAt = 0;
    let status = { state: "idle", reason: "not_started" };
    return {
      reset() {
        targetCooldowns.clear();
        lastSentAt = 0;
        lastSkipAt = 0;
        lastCandidateLogAt = 0;
        status = { state: "idle", reason: "reset" };
      },
      getStatus() {
        return status;
      },
      recommend() {
        return rankSupportCandidates().slice(0, 3);
      },
      run(options = {}) {
        const state = gameState.state;
        if (!options.force && !settings.get("autoTeamSupport")) return setStatus("idle", "disabled");
        if (!teamDetection?.isTeamMode) return setStatus("idle", "not_team_mode");
        if (!OrigWS) return setStatus("idle", "websocket_unavailable");
        if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !state.myPlayerID) {
          return setStatus("idle", "not_ready");
        }
        if (!origWsSend) return setStatus("idle", "socket_unavailable");
        const now = performance.now();
        if (tryDonateTroops(now, options)) return status;
        if (now - lastSentAt < SUPPORT_COOLDOWN_MS) {
          return setStatus("cooldown", "cooldown", { cooldownMs: Math.round(SUPPORT_COOLDOWN_MS - (now - lastSentAt)) });
        }
        return status;
      }
    };
    function tryDonateTroops(now, options = {}) {
      const state = gameState.state;
      if (now - lastSentAt < SUPPORT_COOLDOWN_MS) return false;
      const gate = supportGate(options);
      if (!gate.ok) {
        logSkip(gate.reason, gate.extra);
        setStatus("blocked", gate.reason, gate.extra);
        return false;
      }
      const candidates = rankSupportCandidates();
      logCandidates(candidates);
      const target = candidates.find((candidate) => {
        const lastTargetSentAt = targetCooldowns.get(candidate.id) || 0;
        return candidate.troops >= MIN_DONATION_TROOPS && now - lastTargetSentAt >= TARGET_COOLDOWN_MS;
      });
      if (!target) {
        logSkip(candidates.length ? "cooldown_or_too_small" : "no_candidate");
        setStatus("idle", candidates.length ? "cooldown_or_too_small" : "no_candidate");
        return false;
      }
      const troops = Math.floor(target.troops);
      origWsSend.call(
        state.gameSocket,
        JSON.stringify({ type: "intent", intent: { type: "donate_troops", recipient: target.id, troops } })
      );
      lastSentAt = now;
      targetCooldowns.set(target.id, now);
      logger.info(\`Auto-team support \${target.name || target.id}: \${troops} troops\`);
      roundLogger?.record("auto_team_support_sent", {
        recipient: target.id,
        name: target.name || target.id,
        troops,
        reason: target.reason,
        incoming: target.incoming,
        ownReserveAfterRatio: target.ownReserveAfterRatio
      });
      setStatus("sent", \`sent_\${target.name || target.id}\`, { targetName: target.name || target.id, troops });
      return true;
    }
    function supportGate(options = {}) {
      const myState = gameState.getMyState();
      if (!myState || !myState.isAlive) return { ok: false, reason: "not_alive" };
      const selfIncoming = incomingAttackTroops(gameState, myState);
      if (selfIncoming > 0) return { ok: false, reason: "self_under_attack", extra: { incoming: selfIncoming } };
      const ownOutgoing = troopsInCombat(myState);
      if (ownOutgoing > 0) return { ok: false, reason: "own_action_active", extra: { outgoingTroops: Math.round(ownOutgoing) } };
      const defense = options.automationStatus?.autoDefense;
      if (defense?.emergency || defense?.state === "alert") {
        return { ok: false, reason: "defense_active", extra: { defenseReason: defense.reason || null } };
      }
      const expand = options.automationStatus?.autoExpand;
      const farm = options.automationStatus?.autoFarm;
      if (expand?.state === "sent") return { ok: false, reason: "expand_active" };
      if (farm?.state === "sent") return { ok: false, reason: "farm_active" };
      const aiNeighbours = countHostileAiNeighbours(options.neighbourIDs);
      if (aiNeighbours > 0) return { ok: false, reason: "early_ai_neighbours", extra: { aiNeighbours } };
      return { ok: true };
    }
    function rankSupportCandidates() {
      const myState = gameState.getMyState();
      const economy = getTroopEconomy?.();
      if (!myState || !myState.isAlive || !teamDetection?.isTeamMode) return [];
      const myTroops = finiteOrZero(myState.troops);
      const maxTroops = finiteOrZero(economy?.maxTroops);
      if (myTroops <= 0 || maxTroops <= 0) return [];
      const reserveRatio = Math.max(SUPPORT_RESERVE_RATIO, finiteOrZero(economy?.recommendedReserve));
      const safeAvailable = Math.max(0, myTroops - maxTroops * reserveRatio);
      if (safeAvailable < MIN_DONATION_TROOPS) return [];
      const isCapWaste = economy?.state === "CAP_WASTE" || finiteOrZero(economy?.currentRatio) >= 0.85;
      const teammates = teamDetection.getMyTeamMembers().map((member) => gameState.state.playerStates.get(member.id)).filter((player) => player && player.id !== myState.id && player.isAlive);
      return teammates.map((player) => makeCandidate({ player, myTroops, maxTroops, safeAvailable, isCapWaste })).filter(Boolean).sort((a, b) => b.score - a.score);
    }
    function makeCandidate({ player, myTroops, maxTroops, safeAvailable, isCapWaste }) {
      const teammateTroops = finiteOrZero(player.troops);
      const incoming = incomingAttackTroopsForSmallID(gameState, player.smallID, player.id);
      const troubleDeficit = incoming > 0 ? Math.max(0, incoming * 1.25 - teammateTroops) : 0;
      const lowTeamDeficit = isCapWaste ? Math.max(0, myTroops * 0.25 - teammateTroops) : 0;
      const capWastePush = isCapWaste && teammateTroops < myTroops * 0.45 ? myTroops * 0.12 : 0;
      const desired = Math.max(troubleDeficit, lowTeamDeficit, capWastePush);
      const maxDonation = Math.min(safeAvailable, myTroops * MAX_DONATION_OWN_SHARE);
      const troops = Math.min(desired, maxDonation);
      if (troops < MIN_DONATION_TROOPS) return null;
      const ownReserveAfterRatio = (myTroops - troops) / maxTroops;
      const reason = troubleDeficit > 0 ? "under_attack" : lowTeamDeficit > 0 ? "low_team_troops" : "cap_waste";
      const score = incoming / Math.max(1, teammateTroops) * 3 + (1 - teammateTroops / Math.max(1, myTroops)) + (reason === "under_attack" ? 2 : 0);
      return {
        id: player.id,
        name: player.name || player.displayName || player.id,
        troops,
        reason,
        incoming,
        teammateTroops,
        ownReserveAfterRatio,
        score
      };
    }
    function logCandidates(candidates) {
      const now = performance.now();
      if (!candidates.length || now - lastCandidateLogAt < 5e3) return;
      lastCandidateLogAt = now;
      roundLogger?.record(
        "auto_team_support_candidate",
        candidates.slice(0, 3).map((candidate) => ({
          id: candidate.id,
          name: candidate.name,
          troops: Math.round(candidate.troops),
          reason: candidate.reason,
          incoming: Math.round(candidate.incoming || 0),
          teammateTroops: Math.round(candidate.teammateTroops || 0),
          ownReserveAfterRatio: candidate.ownReserveAfterRatio
        }))
      );
    }
    function logSkip(reason, extra = {}) {
      const now = performance.now();
      if (now - lastSkipAt < 5e3) return;
      lastSkipAt = now;
      roundLogger?.record("auto_team_support_skipped", { reason, ...extra });
    }
    function setStatus(state, reason, extra = {}) {
      status = { state, reason, ...extra };
    }
    function countHostileAiNeighbours(neighbourIDs) {
      if (!neighbourIDs || typeof neighbourIDs.forEach !== "function") return 0;
      const myState = gameState.getMyState();
      let count = 0;
      neighbourIDs.forEach((id) => {
        const player = gameState.state.playerStates.get(id);
        if (!player || !player.isAlive || player.id === myState?.id) return;
        if (teamDetection?.isMyTeammate(player.id)) return;
        if (isAllied5(myState, player)) return;
        if (String(player.playerType || "").toUpperCase() !== "HUMAN") count += 1;
      });
      return count;
    }
    function isAllied5(myState, target) {
      const alliances = Array.isArray(myState?.alliances) ? myState.alliances : [];
      return alliances.some((alliance) => alliance && alliance.other === target.id);
    }
  }

  // src/page/automation/quick-chat.js
  var SOS_KEY = "help.troops";
  var QUICK_CHAT_COOLDOWN_MS = 1e4;
  var MIN_INCOMING_FOR_SOS = 5e3;
  function createQuickChatAutomation({ gameState, OrigWS, origWsSend, settings, logger, roundLogger, teamDetection }) {
    let lastSentAt = 0;
    let lastSkipAt = 0;
    let status = { state: "idle", reason: "not_started" };
    return {
      reset() {
        lastSentAt = 0;
        lastSkipAt = 0;
        status = { state: "idle", reason: "reset" };
      },
      getStatus() {
        return status;
      },
      observeIntent(intent) {
        if (!intent || intent.type !== "quick_chat") return;
        roundLogger?.record("quick_chat_observed", summarizeQuickChat(intent));
      },
      runAutoSos(context = {}) {
        const state = gameState.state;
        if (!settings.get("autoSosQuickChat")) return setStatus("idle", "disabled");
        if (!teamDetection?.isTeamMode) return setStatus("idle", "not_team_mode");
        if (!OrigWS) return setStatus("idle", "websocket_unavailable");
        if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !state.myPlayerID) {
          return setStatus("idle", "not_ready");
        }
        if (!origWsSend) return setStatus("idle", "socket_unavailable");
        const now = performance.now();
        if (now - lastSentAt < QUICK_CHAT_COOLDOWN_MS) {
          return setStatus("cooldown", "cooldown", { cooldownMs: Math.round(QUICK_CHAT_COOLDOWN_MS - (now - lastSentAt)) });
        }
        const myState = gameState.getMyState();
        if (!myState || !myState.isAlive) return setStatus("idle", "not_alive");
        const incoming = incomingAttackTroops(gameState, myState);
        const threshold = Math.max(MIN_INCOMING_FOR_SOS, finiteOrZero(myState.troops) * 0.2);
        if (incoming < threshold) {
          logSkip("no_pressure", { incoming, threshold });
          return setStatus("idle", "no_pressure", { incoming });
        }
        const recipient = chooseRecipient();
        if (!recipient) {
          logSkip("no_recipient", { incoming });
          return setStatus("blocked", "no_recipient", { incoming });
        }
        const supportState = context.teamSupportStatus;
        if (supportState?.state === "sent" && incoming < threshold * 1.8) {
          logSkip("team_support_sent", { incoming, recipient: recipient.id });
          return setStatus("idle", "team_support_sent", { incoming });
        }
        const intent = { type: "quick_chat", recipient: recipient.id, quickChatKey: SOS_KEY };
        origWsSend.call(state.gameSocket, JSON.stringify({ type: "intent", intent }));
        lastSentAt = now;
        logger.info(\`Auto-SOS quick chat \${SOS_KEY} to \${recipient.name || recipient.id}\`);
        roundLogger?.record("quick_chat_sent", {
          quickChatKey: SOS_KEY,
          recipient: recipient.id,
          recipientName: recipient.name || recipient.id,
          incoming
        });
        return setStatus("sent", "sent_help_troops", { recipientName: recipient.name || recipient.id, incoming });
      }
    };
    function chooseRecipient() {
      const myState = gameState.getMyState();
      return teamDetection.getMyTeamMembers().map((member) => gameState.state.playerStates.get(member.id)).filter((player) => player && player.id !== myState?.id && player.isAlive).sort((a, b) => finiteOrZero(b.troops) - finiteOrZero(a.troops))[0];
    }
    function logSkip(reason, extra = {}) {
      const now = performance.now();
      if (now - lastSkipAt < 5e3) return;
      lastSkipAt = now;
      roundLogger?.record("quick_chat_skipped", { reason, quickChatKey: SOS_KEY, ...extra });
    }
    function setStatus(state, reason, extra = {}) {
      status = { state, reason, ...extra };
    }
  }
  function summarizeQuickChat(intent) {
    return {
      quickChatKey: intent.quickChatKey || null,
      recipient: intent.recipient ?? null,
      target: intent.target ?? null,
      keys: Object.keys(intent)
    };
  }

  // src/page/automation/assist-actions.js
  function createAssistActions({ gameState, OrigWS, origWsSend, settings, logger, roundLogger, teamDetection }) {
    return {
      sendManualAssistAttack(target) {
        const gate = validateAttackTarget({ gameState, OrigWS, origWsSend, settings, target, allowHuman: true, teamDetection });
        if (!gate.ok) {
          roundLogger?.record("autopilot_skip", { action: "manual_assist_attack", reason: gate.reason });
          return { ok: false, reason: gate.reason };
        }
        const troops = Math.max(1, Math.floor(target.suggestedTroops));
        origWsSend.call(
          gameState.state.gameSocket,
          JSON.stringify({ type: "intent", intent: { type: "attack", targetID: target.id, troops } })
        );
        logger.info(\`Manual assist attack \${target.name || target.id}: \${troops} troops\`);
        roundLogger?.record("manual_assist_attack_sent", {
          targetID: target.id,
          targetName: target.name || target.id,
          troops,
          suggestedPercent: target.suggestedPercent,
          reserveAfterRatio: target.reserveAfterRatio
        });
        return { ok: true };
      }
    };
  }
  function validateAttackTarget({ gameState, OrigWS, origWsSend, settings, target, allowHuman = false, teamDetection }) {
    const state = gameState.state;
    const myState = gameState.getMyState();
    if (!target) return { ok: false, reason: "no_target" };
    if (!OrigWS) return { ok: false, reason: "websocket_unavailable" };
    if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !origWsSend) return { ok: false, reason: "socket_closed" };
    if (!state.gameStarted || !state.myPlayerID || !myState || !myState.isAlive) return { ok: false, reason: "not_alive_or_not_started" };
    if (target.isHuman && !allowHuman) return { ok: false, reason: "human_blocked" };
    if (target.status !== "target" && target.status !== "farm") return { ok: false, reason: \`status_\${target.status || "unknown"}\` };
    const player = state.playerStates.get(target.id);
    if (!player || !player.isAlive) return { ok: false, reason: "target_not_alive" };
    if (isAllied4(myState, player)) return { ok: false, reason: "target_allied" };
    if (teamDetection?.isMyTeammate(player.id)) return { ok: false, reason: "target_team" };
    if (!Number.isFinite(Number(target.suggestedTroops)) || Number(target.suggestedTroops) <= 0) {
      return { ok: false, reason: "no_troops" };
    }
    const reserveAfter = Number(target.reserveAfterRatio);
    const reserveLimit = Number.isFinite(Number(target.appliedReserveRatio)) ? Number(target.appliedReserveRatio) : Number(settings.get("autoFarmReserveRatio")) || 0.55;
    if (!Number.isFinite(reserveAfter) || reserveAfter < reserveLimit) return { ok: false, reason: "reserve_limit" };
    return { ok: true };
  }
  function isAllied4(myState, target) {
    const alliances = Array.isArray(myState.alliances) ? myState.alliances : [];
    return alliances.some((alliance) => alliance && alliance.other === target.id);
  }

  // src/page/automation/autopilot-controller.js
  function createAutopilotController({ settings, gameState, autoSpawn, autoFarm, autoExpand, autoAlliance, autoEco, autoDefense, roundLogger }) {
    let lastStateLogAt = 0;
    let lastSkipLogAt = 0;
    return {
      reset() {
        lastStateLogAt = 0;
        lastSkipLogAt = 0;
      },
      run({ topSpot, farmRecommendations, buyRecommendations, expansion, threats, neighbourIDs, troopInfo, nukeThreat }) {
        if (!settings.get("autopilot")) return { active: false };
        const state = gameState.state;
        const myState = gameState.getMyState();
        if (!state.gameStarted || !myState || !myState.isAlive) {
          logSkip("not_ready");
          return { active: true, spawnHandled: false, farmHandled: false, expandHandled: false, allianceHandled: false, ecoHandled: false, defenseHandled: false };
        }
        logState({ expansion, threats });
        let spawnHandled = false;
        if (topSpot) {
          autoSpawn?.send(topSpot);
          spawnHandled = true;
        }
        const defenseStatus = autoDefense?.run({ force: true, nukeThreat, threats, neighbourIDs }) || null;
        if (defenseStatus?.emergency) {
          logSkip("defense_emergency");
          runEco(buyRecommendations);
          return { active: true, spawnHandled, farmHandled: true, expandHandled: true, allianceHandled: false, ecoHandled: true, defenseHandled: true };
        }
        const humanPressure = countHumanPressure(farmRecommendations);
        const strongThreat = isNeighbourThreatStrong(threats, neighbourIDs);
        let expandHandled = false;
        if (autoExpand) {
          autoExpand.run(troopInfo, { force: true });
          expandHandled = true;
        }
        const shouldPauseFarm = humanPressure >= 2 || strongThreat || expansion && expansion.level === "stop";
        if (shouldPauseFarm) {
          logSkip(strongThreat ? "strong_threat" : humanPressure >= 2 ? "human_pressure" : expansion.level);
          runEco(buyRecommendations);
          return { active: true, spawnHandled, farmHandled: true, expandHandled, allianceHandled: false, ecoHandled: true, defenseHandled: true };
        }
        autoFarm?.run(farmRecommendations, expansion, { force: true });
        runEco(buyRecommendations);
        let allianceHandled = false;
        if (settings.get("autoAlliance")) {
          autoAlliance?.run();
          allianceHandled = true;
        }
        return { active: true, spawnHandled, farmHandled: true, expandHandled, allianceHandled, ecoHandled: true, defenseHandled: true };
      }
    };
    function runEco(buyRecommendations) {
      if (settings.get("autoEco")) autoEco?.run({ buyRecommendations }, { force: true });
    }
    function logSkip(reason) {
      const now = performance.now();
      if (now - lastSkipLogAt < 3e3) return;
      lastSkipLogAt = now;
      roundLogger?.record("autopilot_skip", { reason });
    }
    function logState({ expansion, threats }) {
      const now = performance.now();
      if (now - lastStateLogAt < 5e3) return;
      lastStateLogAt = now;
      roundLogger?.record("autopilot_state", {
        expansion: expansion ? expansion.level : "unknown",
        topThreat: threats && threats[0] ? { name: threats[0].name, level: threats[0].level, score: threats[0].score } : null
      });
    }
  }
  function countHumanPressure(recommendations) {
    return (recommendations || []).filter((rec) => rec.isHuman && rec.status === "danger").length;
  }
  function isNeighbourThreatStrong(threats, neighbourIDs) {
    if (!Array.isArray(threats) || !neighbourIDs || neighbourIDs.size === 0) return false;
    return threats.some(
      (threat) => (threat.level === "Dangerous" || threat.level === "Critical") && neighbourIDs.has(threat.id)
    );
  }

  // src/page/automation/smart-attack.js
  function createSmartAttackModifier({ settings, gameState, logger, roundLogger, teamDetection, getTroopEconomy }) {
    return function maybeModifyAttack(data) {
      if (!settings.get("smartAttack") || typeof data !== "string") return data;
      try {
        const message = JSON.parse(data);
        if (!message || message.type !== "intent" || !message.intent) return data;
        const intent = message.intent;
        if (intent.type !== "attack" || !intent.troops || intent.troops <= 0 || !intent.targetID) return data;
        const targetState = resolveTarget(gameState, intent.targetID);
        if (targetState && teamDetection?.isMyTeammate(targetState.id)) return data;
        const economy = getTroopEconomy?.();
        const safe = economy ? economy.safeSpendableTroops : null;
        if (!Number.isFinite(safe)) return data;
        if (safe < 1) return data;
        const originalTroops = intent.troops;
        if (originalTroops <= safe) return data;
        const newTroops = Math.max(1, Math.floor(safe));
        if (newTroops === originalTroops) return data;
        intent.troops = newTroops;
        logger.info(
          \`Smart attack cap \${targetState?.name || intent.targetID}: \${originalTroops} -> \${newTroops} (safe \${Math.floor(safe)})\`
        );
        roundLogger?.record("smart_attack_modified", {
          reason: "growth_cap",
          targetID: intent.targetID,
          targetName: targetState?.name || intent.targetID,
          safeSpendable: Math.floor(safe),
          originalTroops,
          newTroops
        });
        return JSON.stringify(message);
      } catch (_) {
        return data;
      }
    };
  }
  function resolveTarget(gameState, targetID) {
    const direct = gameState.state.playerStates.get(targetID);
    if (direct) return direct;
    let match = null;
    gameState.state.playerStates.forEach((player) => {
      if (!match && player && player.smallID === targetID) match = player;
    });
    return match;
  }

  // src/page/automation/auto-alliance.js
  var ALLIANCE_RENEW_THRESHOLD = 50;
  var ALLIANCE_RETRY_COOLDOWN_TICKS = 300;
  var MAX_ALLIANCE_ATTEMPTS = 2;
  function createAutoAlliance({ gameState, neighbourFetcher, OrigWS, origWsSend, logger, roundLogger, teamDetection }) {
    const allianceRequestCooldowns = /* @__PURE__ */ new Map();
    let lastSkippedNeighbour = null;
    return {
      get lastSkippedNeighbour() {
        return lastSkippedNeighbour;
      },
      reset() {
        allianceRequestCooldowns.clear();
        lastSkippedNeighbour = null;
      },
      run() {
        const state = gameState.state;
        if (!OrigWS) return;
        if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !state.myPlayerID) return;
        const myState = gameState.getMyState();
        if (!myState || !myState.isAlive) return;
        neighbourFetcher.refresh();
        if (!neighbourFetcher.cachedNeighbourIDs) return;
        const attacking = getAttackingPlayerIDs(state, myState);
        const humanNeighbours = [];
        neighbourFetcher.cachedNeighbourIDs.forEach((playerID) => {
          if (playerID === state.myPlayerID || attacking[playerID]) return;
          if (teamDetection?.isMyTeammate(playerID)) return;
          const player = state.playerStates.get(playerID);
          if (!player || !player.isAlive || !isHumanPlayerState(player)) return;
          humanNeighbours.push(player);
        });
        humanNeighbours.sort((a, b) => {
          const tilesA = a.tilesOwned || 0;
          const tilesB = b.tilesOwned || 0;
          if (tilesB !== tilesA) return tilesB - tilesA;
          return (b.troops || 0) - (a.troops || 0);
        });
        const targets = humanNeighbours.length > 1 ? humanNeighbours.slice(0, humanNeighbours.length - 1) : [];
        const skipped = humanNeighbours.length > 1 ? humanNeighbours[humanNeighbours.length - 1] : null;
        lastSkippedNeighbour = skipped ? skipped.name || skipped.id : null;
        const existingAlliances = {};
        (myState.alliances || []).forEach((alliance) => {
          existingAlliances[alliance.other] = alliance;
        });
        const pendingOutgoing = {};
        (myState.outgoingAllianceRequests || []).forEach((playerID) => {
          pendingOutgoing[playerID] = true;
        });
        targets.forEach((target) => {
          const existing = existingAlliances[target.id];
          if (existing) {
            if (existing.expiresAt && existing.expiresAt - state.currentTick < ALLIANCE_RENEW_THRESHOLD && !existing.hasExtensionRequest) {
              sendAllianceIntent(state, OrigWS, origWsSend, "allianceExtension", target.id, target.name, logger, roundLogger);
            }
            return;
          }
          if (pendingOutgoing[target.id]) return;
          const record = allianceRequestCooldowns.get(target.id);
          if (record) {
            if (record.attempts >= MAX_ALLIANCE_ATTEMPTS) return;
            if (state.currentTick - record.lastTick < ALLIANCE_RETRY_COOLDOWN_TICKS) return;
          }
          sendAllianceIntent(state, OrigWS, origWsSend, "allianceRequest", target.id, target.name, logger, roundLogger);
          allianceRequestCooldowns.set(target.id, {
            lastTick: state.currentTick,
            attempts: (record?.attempts || 0) + 1
          });
        });
      }
    };
  }
  function recommendAlliances(gameState, teamDetection) {
    const myState = gameState.getMyState();
    if (!myState) return [];
    const recommendations = [];
    gameState.state.playerStates.forEach((player) => {
      if (!player || !player.isAlive || player.id === myState.id || !isHumanPlayerState(player)) return;
      if (teamDetection?.isMyTeammate(player.id)) return;
      const score = (player.tilesOwned || 0) * 0.55 + (player.troops || 0) * 1e-5;
      recommendations.push({
        id: player.id,
        name: player.name || player.id,
        score,
        label: score > (myState.tilesOwned || 0) * 0.55 ? "Ally candidate" : "Possible target"
      });
    });
    return recommendations.sort((a, b) => b.score - a.score).slice(0, 3);
  }
  function getAttackingPlayerIDs(state, myState) {
    const smallIDtoPlayerID = {};
    state.playerStates.forEach((player) => {
      if (player.smallID != null) smallIDtoPlayerID[player.smallID] = player.id;
    });
    const attacking = {};
    (myState.outgoingAttacks || []).forEach((attack) => {
      if (!attack || attack.retreating) return;
      const targetID = smallIDtoPlayerID[attack.targetID];
      if (targetID) attacking[targetID] = true;
    });
    return attacking;
  }
  function sendAllianceIntent(state, OrigWS, origWsSend, intentType, recipientID, recipientName, logger, roundLogger) {
    if (!OrigWS) return;
    if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !origWsSend) return;
    origWsSend.call(
      state.gameSocket,
      JSON.stringify({ type: "intent", intent: { type: intentType, recipient: recipientID } })
    );
    logger.info(\`\${intentType} sent to \${recipientName || recipientID}\`);
    roundLogger?.record("auto_alliance_sent", { intentType, recipientID, recipientName: recipientName || recipientID });
  }
  function isHumanPlayerState(state) {
    return !!state && String(state.playerType || "").toUpperCase() === "HUMAN";
  }

  // src/shared/html.js
  function escapeHtml(value) {
    return String(value == null ? "" : value).replace(/[&<>"']/g, (char) => {
      switch (char) {
        case "&":
          return "&amp;";
        case "<":
          return "&lt;";
        case ">":
          return "&gt;";
        case '"':
          return "&quot;";
        case "'":
          return "&#39;";
        default:
          return char;
      }
    });
  }

  // src/shared/troops.js
  function formatTroopCount(troops) {
    const num = Number(troops) / 10;
    if (!Number.isFinite(num)) return "0";
    if (num >= 1e7) return \`\${(Math.floor(num / 1e5) / 10).toFixed(1)}M\`;
    if (num >= 1e6) return \`\${(Math.floor(num / 1e4) / 100).toFixed(2)}M\`;
    if (num >= 1e5) return \`\${Math.floor(num / 1e3)}K\`;
    if (num >= 1e4) return \`\${(Math.floor(num / 100) / 10).toFixed(1)}K\`;
    if (num >= 1e3) return \`\${(Math.floor(num / 10) / 100).toFixed(2)}K\`;
    return Math.floor(num).toString();
  }

  // src/page/ui/advisor-panel.js
  function createAdvisorPanel({ app, settings, heatmap, actions = {} }) {
    let panel = null;
    return {
      create() {
        removeElementById("ofat-panel");
        removeElementById("ofat-heatmap");
        installPanelStyles();
        panel = document.createElement("div");
        panel.id = "ofat-panel";
        panel.style.display = settings.get("showAdvisorPanel") ? "block" : "none";
        appendWhenReady(panel, "body");
        makeDraggable(panel);
      },
      setVisible(visible) {
        if (panel) panel.style.display = visible ? "block" : "none";
      },
      attachHeatmap(canvas) {
        appendWhenReady(canvas, "body");
      },
      render(viewModel) {
        if (!panel) return;
        panel.innerHTML = renderPanelHtml(app, settings, viewModel);
        panel.querySelectorAll("button[data-setting]").forEach((button) => {
          button.addEventListener("click", () => settings.set(button.dataset.setting, !settings.get(button.dataset.setting)));
        });
        const closeButton = document.getElementById("ofat-close");
        if (closeButton) closeButton.addEventListener("click", () => settings.set("showAdvisorPanel", false));
        const assistButton = document.getElementById("ofat-assist-attack");
        if (assistButton && actions.onManualAssistAttack) assistButton.addEventListener("click", actions.onManualAssistAttack);
        panel.querySelectorAll("button[data-buy-unavailable]").forEach((button) => {
          button.addEventListener("click", () => actions.onUnavailableBuild?.(button.dataset.buyUnavailable));
        });
        heatmap.render(viewModel.scores, viewModel.topSpots, viewModel.playerSpawns);
      }
    };
  }
  function renderPanelHtml(app, settings, vm) {
    const skipped = vm.lastSkippedNeighbour ? \` | Skip: <b>\${escapeHtml(vm.lastSkippedNeighbour)}</b>\` : "";
    let html = "";
    html += \`<div class="ofat-head">\`;
    html += \`<div class="ofat-title">\${escapeHtml(app.name)}</div>\`;
    html += \`<div class="ofat-muted">v\${escapeHtml(app.version)} | updated \${escapeHtml(app.modified)}</div>\`;
    html += \`</div>\`;
    html += \`<div class="ofat-body">\`;
    html += \`<div class="ofat-muted">Players <b>\${vm.otherPlayerCount}</b> | Nations <b>\${vm.nationCount}</b> | Tick <b>\${vm.tick}</b></div>\`;
    if (vm.expansion) html += \`<div class="ofat-muted">\${escapeHtml(vm.expansion.label)}</div>\`;
    if (vm.troopEconomy && settings.get("showTroopEconomy")) html += renderEconomyHtml(vm.troopEconomy);
    if (vm.topThreats.length) {
      html += \`<div class="ofat-muted">Threats: \${vm.topThreats.map((t) => \`\${escapeHtml(t.name)} <b>\${escapeHtml(t.level)}</b>\`).join(" | ")}</div>\`;
    }
    if (vm.farmRecommendations && vm.farmRecommendations.length) {
      const farms = vm.farmRecommendations.filter((target) => target.status === "farm" || target.status === "mark").slice(0, 3);
      if (farms.length) {
        html += \`<div class="ofat-muted">Farm: \${farms.map((t) => \`\${escapeHtml(t.name)} <b>\${escapeHtml(t.label)}</b>\`).join(" | ")}</div>\`;
      }
      const humanTargets = vm.farmRecommendations.filter((target) => target.isHuman).slice(0, 3);
      if (humanTargets.length) {
        html += \`<div class="ofat-muted">Targets: \${humanTargets.map((t) => \`\${escapeHtml(t.name)} <b>\${escapeHtml(t.label)}</b>\`).join(" | ")}</div>\`;
      }
    }
    if (vm.assistTarget) {
      html += \`<div class="ofat-action-row">\`;
      html += \`<span>Freigabe: <b>\${escapeHtml(vm.assistTarget.name)}</b> \${escapeHtml(vm.assistTarget.label)}</span>\`;
      html += \`<button id="ofat-assist-attack">Ziel angreifen</button>\`;
      html += \`</div>\`;
    }
    if (vm.buyRecommendations && vm.buyRecommendations.length) {
      html += \`<div class="ofat-section-title">Eco / Build</div>\`;
      html += \`<div class="ofat-buy-list">\`;
      vm.buyRecommendations.forEach((rec) => {
        if (rec.actionAvailable) {
          html += \`<span class="ofat-buy-chip" title="\${escapeHtml(rec.reason)}">\${escapeHtml(rec.label)}</span>\`;
        } else {
          html += \`<button data-buy-unavailable="\${escapeHtml(rec.actionKey)}" title="\${escapeHtml(rec.reason)}">\${escapeHtml(rec.label)}: nur Hinweis</button>\`;
        }
      });
      html += \`</div>\`;
    }
    if (vm.topSpots.length) html += \`<div class="ofat-section-title">Spawn Diagnose</div>\`;
    for (let i = 0; i < vm.topSpots.length; i += 1) {
      const spot = vm.topSpots[i];
      const scorePct = Math.max(0, Math.min(100, Math.round(spot.score * 100)));
      html += \`<div class="ofat-row" data-ofat-spot="\${i}">\`;
      html += \`<b>\${i + 1}</b>\`;
      html += \`<div>\`;
      html += \`<div>(\${spot.x},\${spot.y}) <span class="ofat-muted">\${escapeHtml(describeSpawnSpot(spot))}</span></div>\`;
      html += \`<div class="ofat-muted">L:\${Math.floor(spot.landDensity * 100)} G:\${Math.floor(spot.plainsRatio * 100)} N:\${Math.floor(spot.nationScore * 100)} P:\${Math.floor(spot.playerDistScore * 100)}</div>\`;
      html += \`<div class="ofat-scorebar"><span style="width:\${scorePct}%"></span></div>\`;
      html += \`</div>\`;
      html += \`<b>\${scorePct}</b>\`;
      html += \`</div>\`;
    }
    if (vm.allianceRecommendations.length) {
      html += \`<div class="ofat-muted">Alliance: \${vm.allianceRecommendations.map((r) => \`\${escapeHtml(r.name)} (\${escapeHtml(r.label)})\`).join(" | ")}</div>\`;
    }
    html += \`<div class="ofat-muted">Allies: <b>\${vm.allyCount}</b>\${skipped}</div>\`;
    html += \`<div class="ofat-buttons">\`;
    html += buttonHtml(settings, "Auto spawn", "autoSpawn", false);
    html += buttonHtml(settings, "Auto ally", "autoAlliance", false);
    html += buttonHtml(settings, "Map", "showHeatmap", false);
    html += buttonHtml(settings, "Panel", "showAdvisorPanel", false);
    html += \`<button id="ofat-close">Close</button>\`;
    html += \`</div></div>\`;
    return html;
  }
  function renderEconomyHtml(eco) {
    const growth = Math.round((eco.growthEfficiency || 0) * 100);
    const safety = Math.round((eco.combatSafety || 0) * 100);
    const safeSend = formatTroopCount(eco.safeSpendableTroops);
    const push = eco.timeToPushSec != null ? \` | Push in ~\${eco.timeToPushSec}s\` : "";
    let html = \`<div class="ofat-eco ofat-eco-\${economyTone(eco.state)}">\`;
    html += \`<div>State: <b>\${escapeHtml(eco.state)}</b> | Growth: <b>\${growth}%</b>\${push}</div>\`;
    html += \`<div>Combat Safety: <b>\${safety}%</b> | Safe Send: <b>\${escapeHtml(safeSend)}</b></div>\`;
    html += \`<div class="ofat-muted">\${escapeHtml(eco.hint || "")}</div>\`;
    html += \`</div>\`;
    return html;
  }
  function economyTone(state) {
    if (state === "CRITICAL" || state === "RECOVER") return "low";
    if (state === "PUSH" || state === "CAP_WASTE") return "push";
    return "ok";
  }
  function buttonHtml(settings, label, key, warn) {
    const on = settings.get(key);
    return \`<button data-setting="\${escapeHtml(key)}" data-on="\${on ? "true" : "false"}" data-warn="\${warn && on ? "true" : "false"}">\${escapeHtml(label)}: \${on ? "ON" : "OFF"}</button>\`;
  }
  function installPanelStyles() {
    createStyle(
      "ofat-style",
      \`
      #ofat-panel {
        position: fixed;
        top: 10px;
        right: 10px;
        z-index: 99999;
        width: min(460px, calc(100vw - 20px));
        max-height: calc(100vh - 20px);
        overflow: auto;
        background: rgba(8, 10, 13, 0.92);
        color: #f4f7fb;
        border: 1px solid rgba(113, 191, 255, 0.45);
        border-radius: 8px;
        box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
        font: 12px/1.35 ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
        user-select: none;
      }
      #ofat-panel .ofat-head {
        padding: 10px 12px 7px;
        border-bottom: 1px solid rgba(255, 255, 255, 0.1);
        cursor: move;
      }
      #ofat-panel .ofat-title { color: #71c7ff; font-size: 15px; font-weight: 700; }
      #ofat-panel .ofat-muted { color: rgba(244, 247, 251, 0.68); }
      #ofat-panel .ofat-body { padding: 9px 12px 12px; }
      #ofat-panel .ofat-row {
        display: grid;
        grid-template-columns: 26px 1fr auto;
        gap: 8px;
        align-items: center;
        padding: 5px 6px;
        margin: 3px 0;
        border-radius: 6px;
        background: rgba(255, 255, 255, 0.055);
      }
      #ofat-panel .ofat-eco {
        margin: 6px 0;
        padding: 6px 8px;
        border-radius: 6px;
        border-left: 3px solid #71c7ff;
        background: rgba(255, 255, 255, 0.06);
      }
      #ofat-panel .ofat-eco-low { border-left-color: #ef5350; background: rgba(183, 28, 28, 0.16); }
      #ofat-panel .ofat-eco-ok { border-left-color: #66bb6a; background: rgba(46, 125, 50, 0.16); }
      #ofat-panel .ofat-eco-push { border-left-color: #71c7ff; background: rgba(41, 98, 255, 0.16); }
      #ofat-panel .ofat-scorebar {
        height: 4px;
        margin-top: 3px;
        background: rgba(255, 255, 255, 0.12);
        border-radius: 999px;
        overflow: hidden;
      }
      #ofat-panel .ofat-scorebar > span { display: block; height: 100%; background: #71c7ff; }
      #ofat-panel .ofat-section-title {
        margin-top: 9px;
        margin-bottom: 4px;
        color: rgba(244, 247, 251, 0.9);
        font-weight: 700;
      }
      #ofat-panel .ofat-buttons { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; }
      #ofat-panel .ofat-action-row {
        display: grid;
        grid-template-columns: 1fr auto;
        gap: 8px;
        align-items: center;
        margin-top: 7px;
        padding: 6px;
        border-radius: 6px;
        background: rgba(83, 181, 255, 0.12);
      }
      #ofat-panel .ofat-buy-list {
        display: flex;
        flex-wrap: wrap;
        gap: 5px;
        margin-top: 7px;
      }
      #ofat-panel .ofat-buy-chip {
        padding: 4px 7px;
        border-radius: 5px;
        background: rgba(46, 125, 50, 0.5);
        color: #f4f7fb;
      }
      #ofat-panel button {
        color: #f4f7fb;
        border: 1px solid rgba(255, 255, 255, 0.16);
        border-radius: 6px;
        background: rgba(255, 255, 255, 0.1);
        padding: 4px 7px;
        font: inherit;
        cursor: pointer;
      }
      #ofat-panel button[data-on="true"] { background: rgba(46, 125, 50, 0.85); }
      #ofat-panel button[data-warn="true"] { background: rgba(183, 28, 28, 0.85); }
      #ofat-heatmap {
        position: fixed;
        right: 10px;
        bottom: 10px;
        z-index: 99998;
        border: 2px solid #71c7ff;
        border-radius: 6px;
        background: #05070a;
        image-rendering: pixelated;
      }
    \`
    );
  }

  // src/page/ui/heatmap.js
  function createHeatmap({ mapDataRef }) {
    let canvas = null;
    let scale = 1;
    return {
      create(showHeatmap) {
        const mapData = mapDataRef.current;
        canvas = document.createElement("canvas");
        scale = Math.min(320 / mapData.width, 220 / mapData.height);
        canvas.width = Math.max(1, Math.floor(mapData.width * scale));
        canvas.height = Math.max(1, Math.floor(mapData.height * scale));
        canvas.id = "ofat-heatmap";
        canvas.style.display = showHeatmap ? "block" : "none";
        return canvas;
      },
      setVisible(visible) {
        if (canvas) canvas.style.display = visible ? "block" : "none";
      },
      render(scores, topSpots, playerSpawns) {
        if (!canvas) return;
        const mapData = mapDataRef.current;
        if (!mapData) return;
        renderHeatmap(canvas, scale, mapData, scores, topSpots, playerSpawns);
      }
    };
  }
  function renderHeatmap(canvas, scale, mapData, scores, topSpots, playerSpawns) {
    const ctx = canvas.getContext("2d");
    const w = canvas.width;
    const h = canvas.height;
    const scoreGrid = new Float32Array(mapData.width * mapData.height);
    let maxScore = 0;
    const half = 8;
    for (let i = 0; i < scores.length; i += 1) {
      const score = scores[i];
      const x0 = Math.max(0, Math.floor(score.x - half));
      const x1 = Math.min(mapData.width - 1, Math.ceil(score.x + half));
      const y0 = Math.max(0, Math.floor(score.y - half));
      const y1 = Math.min(mapData.height - 1, Math.ceil(score.y + half));
      for (let y = y0; y <= y1; y += 1) {
        const rowOffset = y * mapData.width;
        for (let x = x0; x <= x1; x += 1) scoreGrid[rowOffset + x] = score.score;
      }
      if (score.score > maxScore) maxScore = score.score;
    }
    const imageData = ctx.createImageData(w, h);
    const invMax = maxScore > 0 ? 1 / maxScore : 0;
    for (let y = 0; y < h; y += 1) {
      const my = Math.min(mapData.height - 1, Math.floor(y / scale));
      for (let x = 0; x < w; x += 1) {
        const mx = Math.min(mapData.width - 1, Math.floor(x / scale));
        const value = scoreGrid[my * mapData.width + mx] * invMax;
        const pi = (y * w + x) * 4;
        if (value > 0) {
          imageData.data[pi] = Math.floor((1 - value) * 255);
          imageData.data[pi + 1] = Math.floor(value * 255);
          imageData.data[pi + 2] = 42;
          imageData.data[pi + 3] = Math.floor(value * 180 + 50);
        } else {
          imageData.data[pi] = 8;
          imageData.data[pi + 1] = 10;
          imageData.data[pi + 2] = 14;
          imageData.data[pi + 3] = 150;
        }
      }
    }
    ctx.putImageData(imageData, 0, 0);
    ctx.fillStyle = "#71c7ff";
    mapData.nations.forEach((nation) => {
      ctx.beginPath();
      ctx.arc(Math.floor(nation.coordinates[0] * scale), Math.floor(nation.coordinates[1] * scale), 2.5, 0, Math.PI * 2);
      ctx.fill();
    });
    ctx.strokeStyle = "#ff5757";
    ctx.lineWidth = 2;
    playerSpawns.forEach((spawn) => {
      const x = Math.floor(spawn.x * scale);
      const y = Math.floor(spawn.y * scale);
      ctx.beginPath();
      ctx.moveTo(x - 4, y - 4);
      ctx.lineTo(x + 4, y + 4);
      ctx.moveTo(x + 4, y - 4);
      ctx.lineTo(x - 4, y + 4);
      ctx.stroke();
    });
    topSpots.forEach((spot, index) => {
      const x = Math.floor(spot.x * scale);
      const y = Math.floor(spot.y * scale);
      ctx.strokeStyle = index === 0 ? "#ffd54f" : index < 3 ? "#9ccc65" : "#d8dde6";
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.arc(x, y, 7, 0, Math.PI * 2);
      ctx.stroke();
      ctx.fillStyle = "rgba(0,0,0,0.72)";
      ctx.beginPath();
      ctx.arc(x, y, 6, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = index === 0 ? "#ffd54f" : "#ffffff";
      ctx.font = "bold 9px monospace";
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillText(String(index + 1), x, y);
    });
  }

  // src/page/logging/log-export.js
  function downloadJson(filename, data) {
    const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = filename;
    link.style.display = "none";
    document.documentElement.appendChild(link);
    link.click();
    link.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1e3);
  }
  function createRoundLogFilename(startedAt = /* @__PURE__ */ new Date()) {
    const stamp = [
      startedAt.getFullYear(),
      pad2(startedAt.getMonth() + 1),
      pad2(startedAt.getDate()),
      "-",
      pad2(startedAt.getHours()),
      pad2(startedAt.getMinutes()),
      pad2(startedAt.getSeconds())
    ].join("");
    return \`openfront-round-log-\${stamp}.json\`;
  }
  function pad2(value) {
    return String(value).padStart(2, "0");
  }

  // src/page/logging/log-sanitize.js
  function summarizeMap(mapData) {
    if (!mapData) return null;
    return {
      width: mapData.width,
      height: mapData.height,
      nationCount: mapData.nations ? mapData.nations.length : 0
    };
  }
  function summarizeSettings(settings) {
    return {
      autoSpawn: !!settings.autoSpawn,
      autopilot: !!settings.autopilot,
      autoAlliance: !!settings.autoAlliance,
      autoFarm: !!settings.autoFarm,
      autoFarmHumanTargets: !!settings.autoFarmHumanTargets,
      autoEco: !!settings.autoEco,
      autoTeamSupport: !!settings.autoTeamSupport,
      autoSosQuickChat: !!settings.autoSosQuickChat,
      smartAttack: !!settings.smartAttack,
      showAttackBadges: !!settings.showAttackBadges,
      showAdvisorPanel: !!settings.showAdvisorPanel,
      showHeatmap: !!settings.showHeatmap,
      roundLogging: !!settings.roundLogging,
      roundLogAutoDownload: !!settings.roundLogAutoDownload,
      roundLogSnapshotIntervalMs: settings.roundLogSnapshotIntervalMs,
      networkLogging: !!settings.networkLogging,
      autoSpawnDelayMs: settings.autoSpawnDelayMs,
      autoFarmWindowMs: settings.autoFarmWindowMs,
      autoFarmReserveRatio: settings.autoFarmReserveRatio,
      autoFarmCooldownMs: settings.autoFarmCooldownMs,
      autoFarmDynamicReserve: !!settings.autoFarmDynamicReserve,
      autoFarmBaseReserveRatio: settings.autoFarmBaseReserveRatio,
      autoFarmMinReserveRatio: settings.autoFarmMinReserveRatio,
      autoFarmMaxReserveRatio: settings.autoFarmMaxReserveRatio,
      autoExpand: !!settings.autoExpand
    };
  }
  function summarizePlayers(gameState, keepNames) {
    const players = [];
    gameState.state.playerStates.forEach((player) => {
      if (!player) return;
      players.push({
        id: player.id,
        name: keepNames ? player.name || player.id : player.id,
        troops: finiteOrZero(player.troops),
        tilesOwned: finiteOrZero(player.tilesOwned),
        gold: finiteOrZero(player.gold),
        isAlive: player.isAlive,
        playerType: player.playerType,
        allianceCount: Array.isArray(player.alliances) ? player.alliances.length : 0,
        outgoingAttackCount: Array.isArray(player.outgoingAttacks) ? player.outgoingAttacks.filter((attack) => attack && !attack.retreating).length : 0
      });
    });
    return players.sort((a, b) => {
      if (b.tilesOwned !== a.tilesOwned) return b.tilesOwned - a.tilesOwned;
      return b.troops - a.troops;
    });
  }
  function summarizeMyState(gameState, keepNames) {
    const player = gameState.getMyState();
    if (!player) return null;
    return {
      id: player.id,
      name: keepNames ? player.name || player.id : player.id,
      troops: finiteOrZero(player.troops),
      tilesOwned: finiteOrZero(player.tilesOwned),
      gold: finiteOrZero(player.gold),
      isAlive: player.isAlive,
      allianceCount: Array.isArray(player.alliances) ? player.alliances.length : 0,
      outgoingAttackCount: Array.isArray(player.outgoingAttacks) ? player.outgoingAttacks.filter((attack) => attack && !attack.retreating).length : 0
    };
  }
  function summarizeAdvisorView(viewModel) {
    if (!viewModel) return null;
    return {
      tick: viewModel.tick,
      troopEconomy: viewModel.troopEconomy ? {
        state: viewModel.troopEconomy.state,
        currentRatio: round3(viewModel.troopEconomy.currentRatio),
        growthEfficiency: round3(viewModel.troopEconomy.growthEfficiency),
        combatSafety: round3(viewModel.troopEconomy.combatSafety),
        recommendedReserve: round3(viewModel.troopEconomy.recommendedReserve),
        safeSpendableTroops: Math.round(viewModel.troopEconomy.safeSpendableTroops || 0)
      } : null,
      topSpots: (viewModel.topSpots || []).slice(0, 5).map((spot) => ({
        x: spot.x,
        y: spot.y,
        score: round3(spot.score),
        landDensity: round3(spot.landDensity),
        plainsRatio: round3(spot.plainsRatio),
        nationScore: round3(spot.nationScore),
        playerDistScore: round3(spot.playerDistScore)
      })),
      topThreats: (viewModel.topThreats || []).map((threat) => ({
        id: threat.id,
        name: threat.name,
        level: threat.level,
        score: round3(threat.score),
        troopsRatio: round3(threat.troopsRatio),
        tilesRatio: round3(threat.tilesRatio),
        nukePotential: threat.nukePotential
      })),
      expansion: viewModel.expansion || null,
      farmRecommendations: (viewModel.farmRecommendations || []).slice(0, 8).map((rec) => ({
        id: rec.id,
        name: rec.name,
        status: rec.status,
        label: rec.label,
        reason: rec.reason,
        score: round3(rec.score),
        strengthRatio: round3(rec.strengthRatio),
        suggestedPercent: rec.suggestedPercent,
        reserveAfterRatio: round3(rec.reserveAfterRatio),
        reserveMaxSend: Math.round(rec.reserveMaxSend || 0),
        captureMaxSend: Math.round(rec.captureMaxSend || 0),
        usedCaptureOverdraft: !!rec.usedCaptureOverdraft,
        estimatedCaptureTurns: rec.estimatedCaptureTurns || null,
        captureCostEstimate: Math.round(rec.captureCostEstimate || 0),
        officialCaptureCostEstimate: Math.round(rec.officialCaptureCostEstimate || 0),
        officialCaptureTurns: rec.officialCaptureTurns || null,
        officialCaptureSource: rec.officialCaptureSource || null,
        effectiveTargetTroops: Math.round(rec.effectiveTargetTroops || 0)
      })),
      assistTarget: viewModel.assistTarget ? {
        id: viewModel.assistTarget.id,
        name: viewModel.assistTarget.name,
        label: viewModel.assistTarget.label,
        reason: viewModel.assistTarget.reason,
        suggestedPercent: viewModel.assistTarget.suggestedPercent
      } : null,
      buyRecommendations: (viewModel.buyRecommendations || []).map((rec) => ({
        label: rec.label,
        reason: rec.reason,
        actionKey: rec.actionKey,
        actionAvailable: !!rec.actionAvailable,
        estimatedCost: Math.round(rec.estimatedCost || 0),
        costSource: rec.costSource || null
      })),
      automationStatus: viewModel.automationStatus || null,
      teamSupportRecommendations: (viewModel.teamSupportRecommendations || []).slice(0, 3).map((rec) => ({
        id: rec.id,
        name: rec.name,
        reason: rec.reason,
        troops: Math.round(rec.troops || 0),
        incoming: Math.round(rec.incoming || 0),
        ownReserveAfterRatio: round3(rec.ownReserveAfterRatio)
      })),
      allianceRecommendations: (viewModel.allianceRecommendations || []).map((rec) => ({
        id: rec.id,
        name: rec.name,
        label: rec.label,
        score: round3(rec.score)
      })),
      allyCount: viewModel.allyCount || 0
    };
  }
  function round3(value) {
    const number = Number(value);
    if (!Number.isFinite(number)) return 0;
    return Math.round(number * 1e3) / 1e3;
  }

  // src/page/logging/round-logger.js
  var MAX_TIMELINE_BYTES = 2 * 1024 * 1024;
  var PROTECTED_LOG_TYPES = /* @__PURE__ */ new Set([
    "round_started",
    "round_ended",
    "map_loaded",
    "setting_changed",
    "auto_spawn_sent",
    "auto_alliance_sent",
    "smart_attack_modified",
    "autopilot_action",
    "autopilot_skip",
    "autopilot_state",
    "farm_recommendation",
    "auto_farm_attack_sent",
    "auto_farm_skipped",
    "auto_expand_sent",
    "auto_expand_skipped",
    "auto_eco_sent",
    "auto_eco_skipped",
    "auto_eco_cost_model",
    "auto_eco_pending_blocked",
    "auto_eco_runtime_probe",
    "auto_team_support_sent",
    "auto_team_support_skipped",
    "auto_team_support_candidate",
    "auto_team_support_gold_sent",
    "auto_defense_build_sent",
    "auto_defense_counter_sent",
    "auto_defense_skipped",
    "auto_boat_sent",
    "auto_boat_skipped",
    "auto_weapons_silo_sent",
    "auto_weapons_launch_sent",
    "auto_weapons_early_nuke_sent",
    "auto_weapons_skipped",
    "weapon_intent_observed",
    "retreat_intent_observed",
    "manual_assist_attack_sent",
    "buy_recommendation",
    "build_button_unavailable",
    "intent_observed",
    "boat_intent_observed",
    "team_detected",
    "mechanics_discovered",
    "gameview_discovered",
    "spawn_phase_state",
    "eco_cost_model",
    "quick_chat_observed",
    "quick_chat_sent",
    "quick_chat_skipped",
    "combat_model_estimate"
  ]);
  function createRoundLogger({ app, settings, gameState, mapDataRef, logger }) {
    let session = null;
    let lastSnapshotAt = 0;
    let lastAdvisorView = null;
    let lastExportAt = null;
    let truncated = false;
    let sequence = 0;
    let approxBytes = 0;
    return {
      start(reason = "start") {
        if (!settings.get("roundLogging")) return;
        if (session && !session.endedAt) this.end("restarted", { autoDownload: false });
        const startedAt = /* @__PURE__ */ new Date();
        session = {
          meta: {
            appName: app.name,
            appVersion: app.version,
            createdAt: startedAt.toISOString(),
            endedAt: null,
            durationMs: null,
            reason,
            truncated: false
          },
          settings: summarizeSettings(settings.snapshot()),
          map: summarizeMap(mapDataRef.current),
          timeline: [],
          finalState: null
        };
        truncated = false;
        sequence = 0;
        approxBytes = 0;
        lastExportAt = null;
        lastSnapshotAt = performance.now();
        push("round_started", {});
        this.snapshot("initial");
      },
      end(reason = "socket_closed", options = {}) {
        if (!session || session.endedAt) return;
        this.snapshot("final");
        const endedAt = /* @__PURE__ */ new Date();
        session.meta.endedAt = endedAt.toISOString();
        session.meta.durationMs = Date.parse(session.meta.endedAt) - Date.parse(session.meta.createdAt);
        session.meta.reason = reason;
        session.meta.truncated = truncated;
        session.finalState = {
          tick: gameState.state.currentTick,
          myState: summarizeMyState(gameState, settings.get("roundLogKeepPlayerNames")),
          advisor: summarizeAdvisorView(lastAdvisorView)
        };
        push("round_ended", { reason });
        const shouldDownload = options.autoDownload ?? settings.get("roundLogAutoDownload");
        if (shouldDownload && settings.get("roundLogging")) this.export("auto");
      },
      record(type, data = {}) {
        if (!session || session.endedAt || !settings.get("roundLogging")) return;
        push(type, data);
      },
      recordSettingChange(key, value) {
        if (!session || session.endedAt || !settings.get("roundLogging")) return;
        push("setting_changed", { key, value });
      },
      recordMapLoaded(mapData) {
        if (!settings.get("roundLogging")) return;
        if (session) session.map = summarizeMap(mapData);
        push("map_loaded", summarizeMap(mapData));
      },
      recordAdvisor(viewModel) {
        lastAdvisorView = viewModel;
        if (!session || session.endedAt || !settings.get("roundLogging")) return;
        push("advisor", summarizeAdvisorView(viewModel));
      },
      maybeSnapshot() {
        if (!session || session.endedAt || !settings.get("roundLogging")) return;
        const now = performance.now();
        const interval = Number(settings.get("roundLogSnapshotIntervalMs")) || 5e3;
        if (now - lastSnapshotAt < interval) return;
        lastSnapshotAt = now;
        this.snapshot("interval");
      },
      snapshot(reason = "manual") {
        if (!session || session.endedAt || !settings.get("roundLogging")) return;
        push("snapshot", {
          reason,
          myClientID: gameState.state.myClientID,
          myPlayerID: gameState.state.myPlayerID,
          playerCount: gameState.state.playerStates.size,
          players: summarizePlayers(gameState, settings.get("roundLogKeepPlayerNames"))
        });
      },
      export(reason = "manual") {
        if (!session) {
          logger.warn("No round log session to export");
          return;
        }
        const payload = {
          ...session,
          meta: {
            ...session.meta,
            exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
            exportReason: reason,
            truncated
          }
        };
        downloadJson(createRoundLogFilename(new Date(session.meta.createdAt)), payload);
        lastExportAt = payload.meta.exportedAt;
      },
      clear() {
        session = null;
        lastAdvisorView = null;
        lastSnapshotAt = 0;
        lastExportAt = null;
        truncated = false;
        sequence = 0;
        approxBytes = 0;
        logger.info("Round log cleared");
      },
      hasSession() {
        return !!session;
      },
      getStatus() {
        return {
          active: !!session && !session.endedAt,
          hasSession: !!session,
          eventCount: session ? session.timeline.length : 0,
          truncated,
          lastExportAt,
          endedAt: session?.meta?.endedAt || null
        };
      }
    };
    function entrySize(entry) {
      return JSON.stringify(entry).length + 1;
    }
    function push(type, data) {
      if (!session) return;
      const entry = {
        seq: ++sequence,
        timeMs: Date.now() - Date.parse(session.meta.createdAt),
        tick: gameState.state.currentTick,
        type,
        data
      };
      session.timeline.push(entry);
      approxBytes += entrySize(entry);
      enforceTimelineLimit();
    }
    function enforceTimelineLimit() {
      if (!session || approxBytes <= MAX_TIMELINE_BYTES) return;
      const removableTypes = ["network_request", "advisor", "snapshot", "log_truncated"];
      let removed = 0;
      const dropMatching = (predicate) => {
        for (let i = 0; i < session.timeline.length && approxBytes > MAX_TIMELINE_BYTES; ) {
          if (predicate(session.timeline[i])) {
            approxBytes -= entrySize(session.timeline[i]);
            session.timeline.splice(i, 1);
            removed += 1;
          } else {
            i += 1;
          }
        }
      };
      for (const removableType of removableTypes) {
        if (approxBytes <= MAX_TIMELINE_BYTES) break;
        dropMatching((entry) => entry.type === removableType);
      }
      if (approxBytes > MAX_TIMELINE_BYTES) {
        dropMatching((entry) => !PROTECTED_LOG_TYPES.has(entry.type));
      }
      if (removed === 0) return;
      truncated = true;
      const last = session.timeline[session.timeline.length - 1];
      if (last && last.type === "log_truncated") {
        approxBytes -= entrySize(last);
        last.data.removedEntries += removed;
        last.timeMs = Date.now() - Date.parse(session.meta.createdAt);
        last.tick = gameState.state.currentTick;
        approxBytes += entrySize(last);
      } else {
        const entry = {
          seq: ++sequence,
          timeMs: Date.now() - Date.parse(session.meta.createdAt),
          tick: gameState.state.currentTick,
          type: "log_truncated",
          data: { removedEntries: removed, maxTimelineBytes: MAX_TIMELINE_BYTES }
        };
        session.timeline.push(entry);
        approxBytes += entrySize(entry);
      }
    }
  }

  // src/page/logging/network-logger.js
  function installNetworkMetadataLogger({ settings, roundLogger }) {
    installFetchLogger(settings, roundLogger);
    installXhrLogger(settings, roundLogger);
    installBeaconLogger(settings, roundLogger);
  }
  function installFetchLogger(settings, roundLogger) {
    const origFetch = window.fetch;
    if (typeof origFetch !== "function") return;
    window.fetch = function loggedFetch(input, init) {
      const startedAt = performance.now();
      const requestInfo = getFetchRequestInfo(input, init);
      return origFetch.apply(this, arguments).then(
        (response) => {
          recordNetwork(settings, roundLogger, {
            ...requestInfo,
            status: response.status,
            ok: response.ok,
            durationMs: performance.now() - startedAt,
            responseSize: getResponseSize(response),
            initiator: "fetch"
          });
          return response;
        },
        (error) => {
          recordNetwork(settings, roundLogger, {
            ...requestInfo,
            status: 0,
            ok: false,
            durationMs: performance.now() - startedAt,
            responseSize: 0,
            initiator: "fetch",
            error: error && error.name ? error.name : "FetchError"
          });
          throw error;
        }
      );
    };
  }
  function installXhrLogger(settings, roundLogger) {
    if (!window.XMLHttpRequest || !XMLHttpRequest.prototype) return;
    const origOpen = XMLHttpRequest.prototype.open;
    const origSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.open = function loggedOpen(method, url) {
      this.__ofatNetworkInfo = {
        method: String(method || "GET").toUpperCase(),
        url: sanitizeUrl(url),
        sameOrigin: isSameOrigin(url),
        requestType: classifyUrl(url)
      };
      return origOpen.apply(this, arguments);
    };
    XMLHttpRequest.prototype.send = function loggedSend() {
      const startedAt = performance.now();
      this.addEventListener("loadend", () => {
        const info = this.__ofatNetworkInfo || {};
        recordNetwork(settings, roundLogger, {
          method: info.method || "GET",
          url: info.url || "",
          sameOrigin: !!info.sameOrigin,
          requestType: info.requestType || "other",
          status: this.status || 0,
          ok: this.status >= 200 && this.status < 400,
          durationMs: performance.now() - startedAt,
          responseSize: getXhrResponseSize(this),
          initiator: "xhr"
        });
      });
      return origSend.apply(this, arguments);
    };
  }
  function installBeaconLogger(settings, roundLogger) {
    if (!navigator.sendBeacon) return;
    const origSendBeacon = navigator.sendBeacon.bind(navigator);
    navigator.sendBeacon = function loggedSendBeacon(url, data) {
      const startedAt = performance.now();
      const ok = origSendBeacon(url, data);
      recordNetwork(settings, roundLogger, {
        method: "POST",
        url: sanitizeUrl(url),
        sameOrigin: isSameOrigin(url),
        requestType: classifyUrl(url),
        status: ok ? 204 : 0,
        ok,
        durationMs: performance.now() - startedAt,
        responseSize: 0,
        initiator: "beacon"
      });
      return ok;
    };
  }
  function recordNetwork(settings, roundLogger, event) {
    if (!settings.get("networkLogging")) return;
    roundLogger.record("network_request", {
      method: event.method || "GET",
      url: event.url || "",
      status: event.status || 0,
      ok: !!event.ok,
      durationMs: round1(event.durationMs),
      requestType: event.requestType || "other",
      responseSize: event.responseSize || 0,
      sameOrigin: !!event.sameOrigin,
      initiator: event.initiator || "unknown",
      error: event.error || void 0
    });
  }
  function getFetchRequestInfo(input, init) {
    const url = typeof input === "string" ? input : input && input.url;
    const method = init && init.method ? init.method : input && input.method ? input.method : "GET";
    return {
      method: String(method || "GET").toUpperCase(),
      url: sanitizeUrl(url),
      sameOrigin: isSameOrigin(url),
      requestType: classifyUrl(url)
    };
  }
  function sanitizeUrl(value) {
    try {
      const url = new URL(String(value || ""), location.href);
      return \`\${url.origin === location.origin ? "" : url.origin}\${url.pathname}\${url.search ? "?..." : ""}\`;
    } catch (_) {
      return String(value || "").split("?")[0];
    }
  }
  function isSameOrigin(value) {
    try {
      return new URL(String(value || ""), location.href).origin === location.origin;
    } catch (_) {
      return false;
    }
  }
  function classifyUrl(value) {
    const url = sanitizeUrl(value);
    if (/\\/maps\\//.test(url)) return "map";
    if (/\\/api\\//.test(url)) return "api";
    if (/\\/assets\\/|\\/_assets\\//.test(url)) return "asset";
    if (/\\/analytics|\\/collect|\\/beacon/.test(url)) return "telemetry";
    return "other";
  }
  function getResponseSize(response) {
    const length = response.headers && response.headers.get ? response.headers.get("content-length") : null;
    const parsed = Number(length);
    return Number.isFinite(parsed) ? parsed : 0;
  }
  function getXhrResponseSize(xhr) {
    const length = xhr.getResponseHeader ? Number(xhr.getResponseHeader("content-length")) : 0;
    if (Number.isFinite(length) && length > 0) return length;
    if (typeof xhr.responseText === "string") return xhr.responseText.length;
    if (xhr.response instanceof ArrayBuffer) return xhr.response.byteLength;
    return 0;
  }
  function round1(value) {
    const number = Number(value);
    if (!Number.isFinite(number)) return 0;
    return Math.round(number * 10) / 10;
  }

  // src/page/ui/control-hud.js
  function createControlHud({ app, settings, roundLogger, teamDetection, getTroopEconomy }) {
    let el = null;
    return {
      create() {
        removeElementById("ofat-control-hud");
        installHudStyles();
        el = document.createElement("div");
        el.id = "ofat-control-hud";
        appendWhenReady(el, "body");
        this.render();
      },
      render() {
        if (!el) return;
        const status = roundLogger.getStatus();
        const team = teamDetection?.isTeamMode ? teamDetection.teamSummary() : null;
        const economy = settings.get("showTroopEconomy") ? getTroopEconomy?.() || null : null;
        el.innerHTML = renderHudHtml(app, settings, status, team, economy);
        el.querySelectorAll("button[data-setting]").forEach((button) => {
          button.addEventListener("click", () => settings.set(button.dataset.setting, !settings.get(button.dataset.setting)));
        });
        const exportButton = el.querySelector("[data-action='export']");
        if (exportButton) {
          exportButton.addEventListener("click", () => {
            roundLogger.export("hud");
            this.render();
          });
        }
        const clearButton = el.querySelector("[data-action='clear']");
        if (clearButton) {
          clearButton.addEventListener("click", () => {
            roundLogger.clear();
            this.render();
          });
        }
      }
    };
  }
  function renderHudHtml(app, settings, status, team, economy) {
    const logState = status.active ? "REC" : status.hasSession ? "END" : "IDLE";
    const lastExport = status.lastExportAt ? "exportiert" : "nicht exportiert";
    const truncated = status.truncated ? " gekuerzt" : "";
    const teamLine = team ? \`<div class="ofat-hud-team"><span class="ofat-hud-dot" style="background:\${safeColor(team.myTeamColor)}"></span>Team: \${escapeHtml(team.myTeamName)} (\${team.memberCount})</div>\` : "";
    const ecoLine = economy ? \`<div class="ofat-hud-eco">Eco: <b>\${escapeHtml(economy.state)}</b> | Safe \${escapeHtml(formatTroopCount(economy.safeSpendableTroops))}</div>\` : "";
    return \`
    <div class="ofat-hud-title">\${app.shortName} <span>\${logState}</span></div>
    <div class="ofat-hud-status">\${status.eventCount} Events | \${lastExport}\${truncated}</div>
    \${teamLine}
    \${ecoLine}
    <div class="ofat-hud-grid">
      \${toggle("Rundenlog", "roundLogging", settings.get("roundLogging"))}
      \${toggle("Auto-Download", "roundLogAutoDownload", settings.get("roundLogAutoDownload"))}
      \${toggle("Netzwerk-Log", "networkLogging", settings.get("networkLogging"))}
      \${toggle("Advisor-Panel", "showAdvisorPanel", settings.get("showAdvisorPanel"))}
      \${toggle("Heatmap", "showHeatmap", settings.get("showHeatmap"))}
      \${toggle("Dyn. Reserve", "autoFarmDynamicReserve", settings.get("autoFarmDynamicReserve"))}
      \${toggle("Eco-Panel", "showTroopEconomy", settings.get("showTroopEconomy"))}
      \${toggle("Team-Support", "autoTeamSupport", settings.get("autoTeamSupport"), true)}
      \${toggle("Auto-SOS", "autoSosQuickChat", settings.get("autoSosQuickChat"), true)}
      \${toggle("Auto-Defense", "autoDefense", settings.get("autoDefense"), true)}
      \${toggle("Auto-Boat", "autoBoat", settings.get("autoBoat"), true)}
      \${toggle("Auto-Weapons", "autoWeapons", settings.get("autoWeapons"), true)}
      \${toggle("Attack-Badges", "showAttackBadges", settings.get("showAttackBadges"))}
    </div>
    <div class="ofat-hud-actions">
      <button data-action="export">Log exportieren</button>
      <button data-action="clear">Log leeren</button>
    </div>
  \`;
  }
  function safeColor(value) {
    return /^#[0-9a-fA-F]{3,8}$|^[a-zA-Z]+$|^rgb/.test(String(value || "")) ? value : "#71c7ff";
  }
  function toggle(label, key, enabled, warn = false) {
    return \`<button data-setting="\${key}" data-on="\${enabled ? "true" : "false"}" data-warn="\${warn && enabled ? "true" : "false"}">\${label}</button>\`;
  }
  function installHudStyles() {
    createStyle(
      "ofat-control-hud-style",
      \`
      #ofat-control-hud {
        position: fixed;
        top: 10px;
        left: 10px;
        z-index: 100000;
        width: 330px;
        padding: 10px;
        color: #f4f7fb;
        background: rgba(8, 10, 13, 0.92);
        border: 1px solid rgba(113, 191, 255, 0.45);
        border-radius: 8px;
        box-shadow: 0 10px 26px rgba(0, 0, 0, 0.32);
        font: 12px/1.25 ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
        user-select: none;
      }
      #ofat-control-hud .ofat-hud-title {
        display: flex;
        justify-content: space-between;
        gap: 8px;
        color: #71c7ff;
        font-weight: 700;
        margin-bottom: 4px;
      }
      #ofat-control-hud .ofat-hud-title span {
        color: #f4f7fb;
      }
      #ofat-control-hud .ofat-hud-status {
        color: rgba(244, 247, 251, 0.68);
        margin-bottom: 8px;
      }
      #ofat-control-hud .ofat-hud-team {
        display: flex;
        align-items: center;
        gap: 6px;
        color: rgba(244, 247, 251, 0.85);
        margin-bottom: 8px;
      }
      #ofat-control-hud .ofat-hud-dot {
        display: inline-block;
        width: 10px;
        height: 10px;
        border-radius: 50%;
        border: 1px solid rgba(255, 255, 255, 0.6);
      }
      #ofat-control-hud .ofat-hud-eco {
        color: rgba(244, 247, 251, 0.85);
        margin-bottom: 8px;
      }
      #ofat-control-hud .ofat-hud-grid {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 6px;
      }
      #ofat-control-hud .ofat-hud-actions {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 6px;
        margin-top: 7px;
      }
      #ofat-control-hud button {
        min-width: 0;
        height: 32px;
        padding: 0 8px;
        color: #f4f7fb;
        border: 1px solid rgba(255, 255, 255, 0.14);
        border-radius: 5px;
        background: rgba(255, 255, 255, 0.1);
        font: inherit;
        cursor: pointer;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }
      #ofat-control-hud button[data-on="true"] {
        background: rgba(46, 125, 50, 0.88);
      }
      #ofat-control-hud button[data-warn="true"] {
        background: rgba(183, 28, 28, 0.88);
      }
      #ofat-control-hud button:hover {
        border-color: rgba(113, 191, 255, 0.72);
      }
    \`
    );
  }

  // src/page/ui/action-hud.js
  var ACTION_SETTINGS = [
    { key: "autopilot", label: "Autopilot", warn: true },
    { key: "autoExpand", label: "Expand", warn: true },
    { key: "autoFarm", label: "Farm", warn: true },
    { key: "autoFarmHumanTargets", label: "Human", warn: true },
    { key: "autoEco", label: "Eco", warn: true },
    { key: "autoDefense", label: "Defense", warn: true },
    { key: "autoBoat", label: "Boat", warn: true },
    { key: "autoWeapons", label: "Weapons", warn: true },
    { key: "autoTeamSupport", label: "Team", warn: true },
    { key: "autoSosQuickChat", label: "SOS", warn: true },
    { key: "smartAttack", label: "Smart", warn: true }
  ];
  function createActionHud({ settings, getViewModel, getAutomationStatus }) {
    let el = null;
    return {
      create() {
        removeElementById("ofat-action-hud");
        installActionHudStyles();
        el = document.createElement("div");
        el.id = "ofat-action-hud";
        appendWhenReady(el, "body");
        this.render();
      },
      render() {
        if (!el) return;
        el.innerHTML = renderActionHudHtml(settings, getViewModel?.() || null, getAutomationStatus?.() || {});
        el.querySelectorAll("button[data-setting]").forEach((button) => {
          button.addEventListener("click", () => settings.set(button.dataset.setting, !settings.get(button.dataset.setting)));
        });
      }
    };
  }
  function renderActionHudHtml(settings, vm, status) {
    const economy = vm?.troopEconomy;
    const expansion = vm?.expansion?.level || "unknown";
    const threat = vm?.topThreats?.[0] ? \`\${vm.topThreats[0].name} \${vm.topThreats[0].level}\` : "safe";
    const ecoState = economy ? \`\${economy.state} \${Math.round((economy.currentRatio || 0) * 100)}%\` : "idle";
    const target = (vm?.farmRecommendations || []).find((rec) => rec.status === "farm") || null;
    const support = (vm?.teamSupportRecommendations || [])[0] || null;
    let html = \`<div class="ofat-action-row ofat-action-toggles">\`;
    ACTION_SETTINGS.forEach((item) => {
      html += actionToggle(settings, item);
    });
    html += \`</div>\`;
    html += \`<div class="ofat-action-row ofat-action-state">\`;
    html += actionPill("Spawn", summarizeStatus(status.autoSpawn, "idle"), toneFromStatus(status.autoSpawn));
    html += actionPill("Eco", ecoState, toneFromStatus(status.autoEco));
    html += actionPill("Expand", summarizeStatus(status.autoExpand, expansion), toneFromStatus(status.autoExpand));
    html += actionPill("Farm", target ? target.label : summarizeStatus(status.autoFarm, "none"), target ? "ready" : toneFromStatus(status.autoFarm));
    html += actionPill("Defense", defenseSummary(status.autoDefense), status.autoDefense?.emergency ? "warn" : toneFromStatus(status.autoDefense));
    if (settings.get("autoBoat")) html += actionPill("Boat", summarizeStatus(status.autoBoat, "idle"), toneFromStatus(status.autoBoat));
    if (settings.get("autoWeapons")) html += actionPill("Weapons", weaponsSummary(status.autoWeapons), toneFromStatus(status.autoWeapons));
    html += actionPill("Team", support ? \`\${support.name} \${Math.round(support.troops || 0)}\` : summarizeStatus(status.autoTeamSupport, "idle"), support ? "ready" : toneFromStatus(status.autoTeamSupport));
    html += actionPill("SOS", summarizeStatus(status.quickChat, "idle"), toneFromStatus(status.quickChat));
    html += actionPill("Threat", threat, threat === "safe" ? "ok" : "warn");
    html += \`</div>\`;
    return html;
  }
  function actionToggle(settings, item) {
    const on = !!settings.get(item.key);
    const tone = item.warn && on ? "warn" : on ? "on" : "off";
    return \`<button data-setting="\${escapeHtml(item.key)}" data-tone="\${tone}">\${escapeHtml(item.label)}</button>\`;
  }
  function actionPill(label, value, tone) {
    return \`<span class="ofat-action-pill" data-tone="\${escapeHtml(tone || "ok")}"><b>\${escapeHtml(label)}</b> \${escapeHtml(String(value || ""))}</span>\`;
  }
  function weaponsSummary(status) {
    if (!status) return "idle";
    if (status.unit) return status.target ? \`\${status.unit}->\${status.target}\` : status.unit;
    return status.reason || "idle";
  }
  function defenseSummary(status) {
    if (!status) return "safe";
    if (status.emergency) return status.lastAction || (status.topAttacker ? \`ATK \${status.topAttacker}\` : "under attack");
    return status.reason === "no_pressure" ? "safe" : status.reason || "idle";
  }
  function summarizeStatus(status, fallback) {
    if (!status) return fallback;
    if (status.targetName) return status.targetName;
    if (status.unit) return status.unit;
    if (status.reason) return status.reason;
    return fallback;
  }
  function toneFromStatus(status) {
    if (!status) return "ok";
    if (status.state === "sent") return "ready";
    if (status.state === "blocked") return "warn";
    if (status.state === "cooldown") return "cooldown";
    return "ok";
  }
  function installActionHudStyles() {
    createStyle(
      "ofat-action-hud-style",
      \`
      #ofat-action-hud {
        position: fixed;
        top: 10px;
        left: 50%;
        transform: translateX(-50%);
        z-index: 100001;
        width: min(720px, calc(100vw - 24px));
        padding: 8px;
        color: #f4f7fb;
        background: rgba(8, 10, 13, 0.9);
        border: 1px solid rgba(255, 190, 72, 0.55);
        border-radius: 8px;
        box-shadow: 0 10px 28px rgba(0, 0, 0, 0.32);
        font: 12px/1.25 ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
        user-select: none;
      }
      #ofat-action-hud .ofat-action-row {
        display: flex;
        flex-wrap: wrap;
        gap: 6px;
        align-items: center;
        justify-content: center;
      }
      #ofat-action-hud .ofat-action-state {
        margin-top: 6px;
      }
      #ofat-action-hud button {
        min-width: 72px;
        height: 28px;
        padding: 0 8px;
        color: #f4f7fb;
        border: 1px solid rgba(255, 255, 255, 0.16);
        border-radius: 5px;
        background: rgba(255, 255, 255, 0.1);
        font: inherit;
        cursor: pointer;
      }
      #ofat-action-hud button[data-tone="warn"] { background: rgba(183, 28, 28, 0.88); }
      #ofat-action-hud button[data-tone="on"] { background: rgba(46, 125, 50, 0.88); }
      #ofat-action-hud .ofat-action-pill {
        max-width: 170px;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        padding: 3px 7px;
        border-radius: 5px;
        background: rgba(255, 255, 255, 0.1);
        color: rgba(244, 247, 251, 0.86);
      }
      #ofat-action-hud .ofat-action-pill[data-tone="ready"] { background: rgba(46, 125, 50, 0.68); }
      #ofat-action-hud .ofat-action-pill[data-tone="warn"] { background: rgba(183, 28, 28, 0.72); }
      #ofat-action-hud .ofat-action-pill[data-tone="cooldown"] { background: rgba(255, 190, 72, 0.35); }
    \`
    );
  }

  // src/page/page-main.js
  var RECALC_INTERVAL = 1e3;
  function startPage(payload) {
    if (!payload || !payload.appInfo) return;
    installGameViewCapture();
    const app = payload.appInfo;
    const logger = createLogger(app);
    const bus = createEventBus();
    const settings = createPageSettingsStore(app, DEFAULT_SETTINGS, payload.initialSettings || DEFAULT_SETTINGS);
    const gameState = createGameState();
    const mapDataRef = { current: null };
    const staticCandidatesRef = { current: null };
    const neighbourFetcher = createNeighbourFetcher();
    const goldIntel = createGoldIntel();
    let attackAdvisor = null;
    let teamDetection = null;
    let roundLogger = null;
    let recalcTimer = null;
    let ui = null;
    let heatmap = null;
    let controlHud = null;
    let actionHud = null;
    let autoSpawn = null;
    let autoAlliance = null;
    let autoFarm = null;
    let autoEco = null;
    let autoDefense = null;
    let autoBoat = null;
    let autoWeapons = null;
    let autoTeamSupport = null;
    let quickChat = null;
    let autoExpand = null;
    let assistActions = null;
    let autopilot = null;
    let lastViewModel = null;
    let lastTroopEconomy = null;
    let lastBuyRecommendationLoggedAt = 0;
    let lastFarmRecommendationLoggedAt = 0;
    let lastGameViewDiscoveryKey = null;
    let lastMechanicsLogAt = 0;
    let observedIntentCount = 0;
    const OBSERVED_INTENT_CAP = 400;
    logger.banner();
    if (settings.get("hideAds")) hideAds();
    roundLogger = createRoundLogger({ app, settings, gameState, mapDataRef, logger });
    teamDetection = createTeamDetection({ gameState, logger, roundLogger });
    teamDetection.observeLobby();
    attackAdvisor = createAttackAdvisor({ settings, teamDetection });
    const smartAttackModifier = createSmartAttackModifier({
      settings,
      gameState,
      logger,
      roundLogger,
      teamDetection,
      getTroopEconomy: () => lastTroopEconomy
    });
    const network = installNetworkHooks({
      bus,
      getSmartAttackModifier: () => smartAttackModifier,
      logger
    });
    installMapAssetHook({ bus, logger });
    installNetworkMetadataLogger({ settings, roundLogger });
    controlHud = createControlHud({ app, settings, roundLogger, teamDetection, getTroopEconomy: () => lastTroopEconomy });
    controlHud.create();
    actionHud = createActionHud({
      settings,
      getViewModel: () => lastViewModel,
      getAutomationStatus: () => ({
        autoExpand: autoExpand?.getStatus?.() || null,
        autoFarm: autoFarm?.getStatus?.() || null,
        autoEco: autoEco?.getStatus?.() || null,
        autoDefense: autoDefense?.getStatus?.() || null,
        autoBoat: autoBoat?.getStatus?.() || null,
        autoWeapons: autoWeapons?.getStatus?.() || null,
        autoSpawn: autoSpawn?.getStatus?.() || null,
        autoTeamSupport: autoTeamSupport?.getStatus?.() || null,
        quickChat: quickChat?.getStatus?.() || null
      })
    });
    actionHud.create();
    registerEvents();
    function registerEvents() {
      bus.on("mapLoaded", (mapData) => {
        mapDataRef.current = mapData;
        const startedAt = performance.now();
        staticCandidatesRef.current = precomputeStaticScores(mapData);
        logger.info(
          \`Static spawn analysis: \${staticCandidatesRef.current.length} candidates in \${(performance.now() - startedAt).toFixed(0)}ms\`
        );
        roundLogger.recordMapLoaded(mapData);
        heatmap = createHeatmap({ mapDataRef });
        ui = createAdvisorPanel({
          app,
          settings,
          heatmap,
          actions: {
            onManualAssistAttack: () => handleManualAssistAttack(),
            onUnavailableBuild: (actionKey) => handleUnavailableBuild(actionKey)
          }
        });
        ui.create();
        ui.attachHeatmap(heatmap.create(settings.get("showHeatmap")));
        autoSpawn = createAutoSpawn({
          gameState,
          mapDataRef,
          OrigWS: network.OrigWS,
          origWsSend: network.origWsSend,
          settings,
          logger,
          roundLogger,
          teamDetection
        });
        autoFarm = createAutoFarm({
          gameState,
          OrigWS: network.OrigWS,
          origWsSend: network.origWsSend,
          settings,
          logger,
          roundLogger,
          getTroopEconomy: () => lastTroopEconomy
        });
        autoExpand = createAutoExpand({
          gameState,
          neighbourFetcher,
          OrigWS: network.OrigWS,
          origWsSend: network.origWsSend,
          settings,
          logger,
          roundLogger,
          getTroopEconomy: () => lastTroopEconomy
        });
        autoEco = createAutoEco({
          gameState,
          mapDataRef,
          OrigWS: network.OrigWS,
          origWsSend: network.origWsSend,
          settings,
          logger,
          roundLogger,
          getTroopEconomy: () => lastTroopEconomy
        });
        autoDefense = createAutoDefense({
          gameState,
          mapDataRef,
          OrigWS: network.OrigWS,
          origWsSend: network.origWsSend,
          settings,
          logger,
          roundLogger,
          teamDetection,
          getTroopEconomy: () => lastTroopEconomy
        });
        autoBoat = createAutoBoat({
          gameState,
          mapDataRef,
          OrigWS: network.OrigWS,
          origWsSend: network.origWsSend,
          settings,
          logger,
          roundLogger,
          getTroopEconomy: () => lastTroopEconomy
        });
        autoWeapons = createAutoWeapons({
          gameState,
          mapDataRef,
          OrigWS: network.OrigWS,
          origWsSend: network.origWsSend,
          settings,
          logger,
          roundLogger,
          teamDetection,
          getTroopEconomy: () => lastTroopEconomy
        });
        autoTeamSupport = createAutoTeamSupport({
          gameState,
          OrigWS: network.OrigWS,
          origWsSend: network.origWsSend,
          settings,
          logger,
          roundLogger,
          teamDetection,
          getTroopEconomy: () => lastTroopEconomy
        });
        quickChat = createQuickChatAutomation({
          gameState,
          OrigWS: network.OrigWS,
          origWsSend: network.origWsSend,
          settings,
          logger,
          roundLogger,
          teamDetection
        });
        assistActions = createAssistActions({
          gameState,
          OrigWS: network.OrigWS,
          origWsSend: network.origWsSend,
          settings,
          logger,
          roundLogger,
          teamDetection
        });
        autoAlliance = createAutoAlliance({
          gameState,
          neighbourFetcher,
          OrigWS: network.OrigWS,
          origWsSend: network.origWsSend,
          logger,
          roundLogger,
          teamDetection
        });
        autopilot = createAutopilotController({
          settings,
          gameState,
          autoSpawn,
          autoFarm,
          autoExpand,
          autoAlliance,
          autoEco,
          autoDefense,
          roundLogger
        });
        recalculate();
        startRecalcLoop();
      });
      bus.on("wsMessage", (message) => {
        gameState.handleWsMessage(message, mapDataRef.current);
        if (message.type === "start") {
          roundLogger.start("websocket_start");
          if (Array.isArray(message.turns)) message.turns.forEach((turn) => recordMyIntents(turn));
          controlHud?.render();
        } else if (message.type === "turn") {
          recordMyIntents(message.turn);
          roundLogger.maybeSnapshot();
        }
      });
      bus.on("workerGameUpdateBatch", (batch) => {
        gameState.handleWorkerBatch(batch);
        roundLogger.maybeSnapshot();
      });
      bus.on("socketReady", (socket) => gameState.setSocket(socket));
      bus.on("socketClosed", (socket) => {
        roundLogger.end("socket_closed");
        gameState.resetSocket(socket);
        autoSpawn?.reset();
        autoAlliance?.reset();
        autoFarm?.reset();
        autoEco?.reset();
        autoDefense?.reset();
        autoBoat?.reset();
        autoWeapons?.reset();
        autoTeamSupport?.reset();
        quickChat?.reset();
        autoExpand?.reset();
        autopilot?.reset();
        attackAdvisor.reset();
        teamDetection.reset();
        resetGameViewCache();
        goldIntel.reset();
        lastGameViewDiscoveryKey = null;
        lastTroopEconomy = null;
        controlHud?.render();
        actionHud?.render();
      });
      settings.onChange(({ key, value }) => {
        if (key === "showHeatmap") heatmap?.setVisible(value);
        if (key === "showAdvisorPanel") ui?.setVisible(value);
        if (key === "autoSpawn" && !value) autoSpawn?.reset();
        if (key === "autoFarm" && !value) autoFarm?.reset();
        if (key === "autoEco" && !value) autoEco?.reset();
        if (key === "autoDefense" && !value) autoDefense?.reset();
        if (key === "autoBoat" && !value) autoBoat?.reset();
        if (key === "autoWeapons" && !value) autoWeapons?.reset();
        if (key === "autoTeamSupport" && !value) autoTeamSupport?.reset();
        if (key === "autoSosQuickChat" && !value) quickChat?.reset();
        if (key === "autoExpand" && !value) autoExpand?.reset();
        roundLogger.recordSettingChange(key, value);
        controlHud?.render();
        actionHud?.render();
        recalculate();
      });
      window.addEventListener("message", (event) => {
        const data = event && event.data;
        if (!data || data.source !== app.messageSource) return;
        if (data.type === "export-round-log") roundLogger.export("manual");
        if (data.type === "clear-round-log") roundLogger.clear();
        if (data.type === "export-round-log" || data.type === "clear-round-log") controlHud?.render();
        if (data.type === "export-round-log" || data.type === "clear-round-log") actionHud?.render();
      });
    }
    function startRecalcLoop() {
      if (recalcTimer) return;
      recalcTimer = setInterval(recalculate, RECALC_INTERVAL);
    }
    function recalculate() {
      if (!mapDataRef.current || !staticCandidatesRef.current || !ui) return;
      teamDetection.syncFromGameView();
      recordGameViewDiscovery();
      const teammateClientIDs = teamDetection.myTeammateClientIDs();
      const results = rankSpawnCandidates(staticCandidatesRef.current, gameState.state.playerSpawns, { teammateClientIDs });
      const myState = gameState.getMyState();
      const troopInfo = getMyTroopRatio();
      const siloIntel = scanMissileSilos(getGameView(), myState, teamDetection);
      const nukeBuilders = enemyNukeBuilders(siloIntel);
      goldIntel.sample(gameState);
      const threats = Array.from(evaluateThreats(gameState, teamDetection, nukeBuilders, goldIntel).values()).sort((a, b) => b.score - a.score).slice(0, 3);
      neighbourFetcher.refresh();
      const troopEconomy = evaluateTroopEconomy({
        troopInfo,
        myState,
        gameState,
        neighbours: neighbourFetcher.cachedNeighbourIDs,
        settings,
        teamDetection,
        threats
      });
      lastTroopEconomy = troopEconomy;
      const expansion = evaluateExpansionState(troopInfo, myState, troopEconomy);
      const farmRecommendations = attackAdvisor.evaluate({
        gameState,
        neighbourFetcher,
        troopInfo,
        expansion,
        troopEconomy,
        mapData: mapDataRef.current
      });
      const allianceRecommendations = recommendAlliances(gameState, teamDetection);
      const buyRecommendations = recommendBuys({ gameState, troopInfo, threats, farmRecommendations, troopEconomy });
      const spawnPhase = getSpawnPhaseInfo({ gameView: getGameView(), state: gameState.state, teamMode: !!teamDetection?.isTeamMode });
      recordMechanicsDiscovery(spawnPhase);
      const teamSupportRecommendations = autoTeamSupport?.recommend?.() || [];
      const allyCount = myState && myState.alliances ? myState.alliances.length : 0;
      const assistTarget = chooseAssistTarget(farmRecommendations);
      const nukeThreatActive = nukeBuilders.size > 0 || threats.some((threat) => threat.nukePotential >= 1 || threat.nukeSoon);
      const viewModel = {
        scores: results.scores,
        topSpots: results.topSpots,
        playerSpawns: gameState.state.playerSpawns,
        otherPlayerCount: gameState.state.playerSpawns.size,
        nationCount: mapDataRef.current.nations.length,
        tick: gameState.state.currentTick,
        topThreats: threats,
        expansion,
        troopEconomy,
        farmRecommendations,
        assistTarget,
        buyRecommendations,
        siloIntel,
        nukeThreatActive,
        spawnPhase,
        teamSupportRecommendations,
        allianceRecommendations,
        allyCount,
        lastSkippedNeighbour: autoAlliance?.lastSkippedNeighbour || null,
        automationStatus: {
          autoExpand: autoExpand?.getStatus?.() || null,
          autoFarm: autoFarm?.getStatus?.() || null,
          autoEco: autoEco?.getStatus?.() || null,
          autoDefense: autoDefense?.getStatus?.() || null,
          autoBoat: autoBoat?.getStatus?.() || null,
          autoWeapons: autoWeapons?.getStatus?.() || null,
          autoSpawn: autoSpawn?.getStatus?.() || null,
          autoTeamSupport: autoTeamSupport?.getStatus?.() || null,
          quickChat: quickChat?.getStatus?.() || null
        }
      };
      lastViewModel = viewModel;
      ui.render(viewModel);
      publishFarmRecommendations(farmRecommendations);
      roundLogger.recordAdvisor(viewModel);
      recordFarmRecommendations(farmRecommendations);
      recordBuyRecommendations(buyRecommendations);
      roundLogger.maybeSnapshot();
      controlHud?.render();
      actionHud?.render();
      const autopilotState = autopilot?.run({
        topSpot: results.topSpots[0] || null,
        farmRecommendations,
        buyRecommendations,
        expansion,
        threats,
        neighbourIDs: neighbourFetcher.cachedNeighbourIDs,
        troopInfo,
        nukeThreat: nukeThreatActive
      });
      if (!autopilotState?.defenseHandled && settings.get("autoDefense")) {
        autoDefense?.run({ nukeThreat: nukeThreatActive, threats, neighbourIDs: neighbourFetcher.cachedNeighbourIDs });
      }
      const underAttack = !!autoDefense?.getStatus?.()?.emergency;
      if (!autopilotState?.spawnHandled && settings.get("autoSpawn") && results.topSpots.length > 0) autoSpawn?.send(results.topSpots[0]);
      if (!underAttack && !autopilotState?.expandHandled && settings.get("autoExpand")) autoExpand?.run(troopInfo);
      if (!underAttack && !autopilotState?.farmHandled && settings.get("autoFarm")) autoFarm?.run(farmRecommendations, expansion);
      if (!underAttack && !autopilotState?.ecoHandled && settings.get("autoEco")) autoEco?.run({ buyRecommendations });
      if (!underAttack && settings.get("autoBoat")) autoBoat?.run();
      if (!underAttack && settings.get("autoWeapons")) autoWeapons?.run({ threats });
      if (!autopilotState?.allianceHandled && settings.get("autoAlliance")) autoAlliance?.run();
      if (settings.get("autoTeamSupport")) {
        autoTeamSupport?.run({
          neighbourIDs: neighbourFetcher.cachedNeighbourIDs,
          automationStatus: {
            autoDefense: autoDefense?.getStatus?.() || null,
            autoExpand: autoExpand?.getStatus?.() || null,
            autoFarm: autoFarm?.getStatus?.() || null
          }
        });
      }
      if (settings.get("autoSosQuickChat")) quickChat?.runAutoSos({ teamSupportStatus: autoTeamSupport?.getStatus?.() || null });
      actionHud?.render();
    }
    function handleManualAssistAttack() {
      const target = lastViewModel?.assistTarget;
      const result = assistActions?.sendManualAssistAttack(target);
      if (!result?.ok) logger.warn(\`Manual assist attack skipped: \${result?.reason || "unknown"}\`);
    }
    function handleUnavailableBuild(actionKey) {
      roundLogger.record("build_button_unavailable", { actionKey, reason: "intent_not_verified" });
      logger.warn(\`Build action \${actionKey} is not verified yet\`);
    }
    function chooseAssistTarget(recommendations) {
      return (recommendations || []).find((rec) => rec.isHuman && rec.status === "target") || null;
    }
    function recordGameViewDiscovery() {
      const discovery = getGameViewDiscovery();
      if (!discovery) return;
      const key = JSON.stringify(discovery);
      if (key === lastGameViewDiscoveryKey && roundLogger.hasSession()) return;
      if (!roundLogger.hasSession()) return;
      lastGameViewDiscoveryKey = key;
      roundLogger.record("gameview_discovered", discovery);
    }
    function recordMechanicsDiscovery(spawnPhase) {
      const now = performance.now();
      if (now - lastMechanicsLogAt < 15e3) return;
      lastMechanicsLogAt = now;
      roundLogger.record("mechanics_discovered", {
        spawnPhaseTurns: spawnPhase?.totalTurns || null,
        spawnPhaseSource: spawnPhase?.source || null
      });
    }
    function recordMyIntents(turn) {
      if (!turn || !Array.isArray(turn.intents)) return;
      const myClientID = gameState.state.myClientID;
      if (!myClientID) return;
      for (let i = 0; i < turn.intents.length; i += 1) {
        const intent = turn.intents[i];
        if (!intent || intent.clientID !== myClientID) continue;
        autoEco?.observeIntent?.(intent);
        autoDefense?.observeIntent?.(intent);
        quickChat?.observeIntent?.(intent);
        if (observedIntentCount >= OBSERVED_INTENT_CAP) return;
        observedIntentCount += 1;
        roundLogger.record("intent_observed", summarizeIntent(intent));
        if (intent.type === "boat") roundLogger.record("boat_intent_observed", decodeBoatIntent(intent));
        if (isRetreatLikeIntent(intent)) {
          roundLogger.record("retreat_intent_observed", { type: intent.type, keys: Object.keys(intent), raw: intent });
        }
        if (intent.type === "build_unit" && isWeaponUnit(intent.unit)) {
          roundLogger.record("weapon_intent_observed", decodeWeaponIntent(intent));
        }
      }
    }
    function isWeaponUnit(unit) {
      return unit === UNIT.ATOM_BOMB || unit === UNIT.HYDROGEN_BOMB || unit === UNIT.MIRV || unit === UNIT.WARSHIP;
    }
    function decodeWeaponIntent(intent) {
      const mapData = mapDataRef.current;
      const tile = Number(intent.tile);
      const out = { unit: intent.unit, tile: intent.tile != null ? intent.tile : null, keys: Object.keys(intent), raw: intent };
      if (mapData && mapData.width && Number.isFinite(tile)) {
        out.tileX = tile % mapData.width;
        out.tileY = Math.floor(tile / mapData.width);
        const byte = mapData.terrain ? mapData.terrain[tile] : void 0;
        out.tileIsLand = byte === void 0 ? null : isLandByte(byte);
      }
      return out;
    }
    function isRetreatLikeIntent(intent) {
      if (!intent) return false;
      if (typeof intent.type === "string" && /cancel|retreat|abort/i.test(intent.type)) return true;
      if ("retreating" in intent && intent.retreating) return true;
      if (intent.attackID != null && intent.type !== "attack") return true;
      return false;
    }
    function decodeBoatIntent(intent) {
      const mapData = mapDataRef.current;
      const dst = intent.dst;
      const out = { dst: dst != null ? dst : null, troops: intent.troops != null ? intent.troops : null, keys: Object.keys(intent), raw: intent };
      if (mapData && mapData.width && Number.isFinite(Number(dst))) {
        const tile = Number(dst);
        out.dstX = tile % mapData.width;
        out.dstY = Math.floor(tile / mapData.width);
        const byte = mapData.terrain ? mapData.terrain[tile] : void 0;
        out.dstIsLand = byte === void 0 ? null : isLandByte(byte);
      }
      return out;
    }
    function summarizeIntent(intent) {
      const targetID = intent.targetID;
      let resolved = "none";
      if (targetID != null) {
        let match = gameState.state.playerStates.get(targetID) || null;
        if (!match) {
          gameState.state.playerStates.forEach((player) => {
            if (!match && player && player.smallID === targetID) match = player;
          });
        }
        resolved = match ? \`\${match.playerType || "?"}:\${match.name || match.id}\` : "unresolved";
      }
      return {
        type: intent.type,
        targetID: targetID === void 0 ? "__undefined__" : targetID,
        targetIDType: targetID === null ? "null" : typeof targetID,
        troops: intent.troops != null ? intent.troops : null,
        resolved,
        keys: Object.keys(intent),
        raw: intent
      };
    }
    function publishFarmRecommendations(recommendations) {
      window.postMessage(
        {
          source: app.messageSource,
          type: "farm-recommendations",
          recommendations: recommendations.map((rec) => ({
            id: rec.id,
            name: rec.name,
            status: rec.status,
            label: rec.label,
            suggestedPercent: rec.suggestedPercent,
            reason: rec.reason,
            estimatedCaptureTurns: rec.estimatedCaptureTurns
          }))
        },
        "*"
      );
    }
    function recordFarmRecommendations(recommendations) {
      const now = performance.now();
      if (now - lastFarmRecommendationLoggedAt < 5e3) return;
      const actionable = recommendations.filter((rec) => rec.status === "farm" || rec.status === "mark").slice(0, 5);
      if (!actionable.length) return;
      lastFarmRecommendationLoggedAt = now;
      roundLogger.record(
        "farm_recommendation",
        actionable.map((rec) => ({
          id: rec.id,
          name: rec.name,
          status: rec.status,
          suggestedPercent: rec.suggestedPercent,
          reason: rec.reason,
          score: rec.score,
          targetTroops: rec.targetTroops,
          effectiveTargetTroops: rec.effectiveTargetTroops,
          targetTiles: rec.targetTiles,
          reserveMaxSend: rec.reserveMaxSend,
          captureMaxSend: rec.captureMaxSend,
          usedCaptureOverdraft: !!rec.usedCaptureOverdraft,
          captureCostEstimate: rec.captureCostEstimate,
          officialCaptureCostEstimate: rec.officialCaptureCostEstimate,
          officialCaptureTileCost: rec.officialCaptureTileCost,
          officialCaptureTurns: rec.officialCaptureTurns,
          officialCaptureSource: rec.officialCaptureSource,
          estimatedCaptureTurns: rec.estimatedCaptureTurns,
          sizingMode: rec.sizingMode
        }))
      );
      roundLogger.record(
        "combat_model_estimate",
        actionable.slice(0, 5).map((rec) => ({
          id: rec.id,
          name: rec.name,
          status: rec.status,
          oldCaptureCost: rec.captureCostEstimate,
          newCaptureCost: rec.officialCaptureCostEstimate,
          oldTurns: rec.estimatedCaptureTurns,
          newTurns: rec.officialCaptureTurns,
          source: rec.officialCaptureSource,
          terrain: rec.officialTerrainClass
        }))
      );
    }
    function recordBuyRecommendations(recommendations) {
      const now = performance.now();
      if (!recommendations.length || now - lastBuyRecommendationLoggedAt < 1e4) return;
      lastBuyRecommendationLoggedAt = now;
      roundLogger.record(
        "buy_recommendation",
        recommendations.map((rec) => ({
          label: rec.label,
          reason: rec.reason,
          actionKey: rec.actionKey,
          actionAvailable: !!rec.actionAvailable,
          estimatedCost: rec.estimatedCost || null,
          costSource: rec.costSource || null
        }))
      );
    }
  }

  // src/page-entry.js
  var payloadKey = window.__OFAT_PAGE_PAYLOAD__?.payloadKey || "__OFAT_PAGE_PAYLOAD__";
  startPage(window[payloadKey]);
})();
`;

  // src/meta.js
  var APP = Object.freeze({
    name: "OpenFront Tactical Assistant",
    shortName: "OF Tactical",
    version: "0.8.11",
    modified: "2026-06-08",
    storageKey: "ofat.settings.v1",
    pagePayloadKey: "__OFAT_PAGE_PAYLOAD__",
    messageSource: "openfront-tactical-assistant"
  });
  var USERSCRIPT_META = Object.freeze({
    name: [APP.name],
    namespace: ["https://github.com/local/openfront-script"],
    version: [APP.version],
    description: ["Modular OpenFront advisor toolkit with spawn, threat, expansion, alliance, and optional assist features."],
    license: ["MIT"],
    match: ["https://openfront.io/*", "https://beta.openfront.io/*"],
    grant: ["GM_setValue", "GM_getValue", "GM_registerMenuCommand", "unsafeWindow"],
    "run-at": ["document-start"],
    "inject-into": ["auto"]
  });
  var DEFAULT_SETTINGS = Object.freeze({
    settingsSchemaVersion: 13,
    showAdvisorPanel: true,
    showHeatmap: true,
    hideAds: true,
    autopilot: false,
    autoSpawn: false,
    autoAlliance: false,
    autoFarm: false,
    autoFarmHumanTargets: true,
    autoEco: false,
    autoDefense: false,
    autoDefenseCounterAttack: true,
    autoDefenseBuildPosts: true,
    autoDefenseBuildSam: true,
    autoDefenseIncomingRatio: 0.35,
    autoBoat: false,
    autoWeapons: false,
    autoWeaponsEarlyNuke: false,
    autoTeamSupport: true,
    autoTeamSupportGold: false,
    autoSosQuickChat: true,
    smartAttack: false,
    showAttackBadges: true,
    showRatioBar: true,
    showTroopRatios: true,
    showWeaknessIndicator: true,
    showDangerIndicator: true,
    showThreatIcons: true,
    showTroopEconomy: true,
    roundLogging: true,
    roundLogAutoDownload: false,
    roundLogSnapshotIntervalMs: 5e3,
    roundLogKeepPlayerNames: true,
    networkLogging: true,
    autoSpawnDelayMs: 2e3,
    autoFarmWindowMs: 24e4,
    autoFarmReserveRatio: 0.55,
    autoFarmCooldownMs: 2500,
    autoFarmDynamicReserve: true,
    autoFarmBaseReserveRatio: 0.3,
    autoFarmMinReserveRatio: 0.2,
    autoFarmMaxReserveRatio: 0.65,
    autoExpand: true
  });

  // src/settings/settings-store.js
  function createSettingsStore(defaults, initialValues = {}) {
    const values = Object.assign({}, defaults, initialValues);
    const listeners = /* @__PURE__ */ new Set();
    function has(key) {
      return Object.prototype.hasOwnProperty.call(defaults, key);
    }
    return {
      values,
      has,
      get(key) {
        return has(key) ? values[key] : void 0;
      },
      set(key, value, meta = {}) {
        if (!has(key)) return false;
        if (values[key] === value) return true;
        values[key] = value;
        listeners.forEach((listener) => listener({ key, value, meta }));
        return true;
      },
      update(nextValues, meta = {}) {
        Object.keys(nextValues || {}).forEach((key) => this.set(key, nextValues[key], meta));
      },
      onChange(listener) {
        listeners.add(listener);
        return () => listeners.delete(listener);
      },
      snapshot() {
        return Object.assign({}, values);
      }
    };
  }

  // src/bootstrap/gm-settings.js
  function createGmSettingsStore(defaults) {
    const initial = {};
    Object.keys(defaults).forEach((key) => {
      initial[key] = gmGet(key, defaults[key]);
    });
    applySettingsMigrations(initial, defaults);
    const store = createSettingsStore(defaults, initial);
    store.onChange(({ key, value, meta }) => {
      gmSet(key, value);
      if (!meta.skipPageSync && meta.pageWindow && meta.app) {
        meta.pageWindow.postMessage(
          {
            source: meta.app.messageSource,
            type: "set-setting",
            key,
            value
          },
          "*"
        );
      }
    });
    return store;
  }
  function applySettingsMigrations(initial, defaults) {
    const targetSchema = Number(defaults.settingsSchemaVersion) || 0;
    const currentSchema = Number(gmGet("settingsSchemaVersion", 0)) || 0;
    if (!targetSchema || currentSchema >= targetSchema) return;
    initial.roundLogAutoDownload = false;
    initial.settingsSchemaVersion = targetSchema;
    gmSet("roundLogAutoDownload", false);
    gmSet("settingsSchemaVersion", targetSchema);
  }
  function gmGet(key, fallback) {
    try {
      if (typeof GM_getValue === "function") return GM_getValue(key, fallback);
    } catch (_) {
    }
    return fallback;
  }
  function gmSet(key, value) {
    try {
      if (typeof GM_setValue === "function") GM_setValue(key, value);
    } catch (_) {
    }
  }
  function registerToggleMenu(label, key, store, context) {
    if (typeof GM_registerMenuCommand !== "function") return;
    GM_registerMenuCommand(label, () => {
      store.set(key, !store.get(key), context);
      console.log(`[${context.app.shortName}] ${key}: ${store.get(key) ? "ON" : "OFF"}`);
    });
  }

  // src/bootstrap/page-injection.js
  function injectPageBundle(pageWindow, app, initialSettings, pageBundleSource) {
    const payloadKey = app.pagePayloadKey;
    pageWindow[payloadKey] = {
      appInfo: app,
      initialSettings,
      payloadKey
    };
    try {
      pageWindow.eval(pageBundleSource);
    } finally {
      try {
        delete pageWindow[payloadKey];
      } catch (_) {
        pageWindow[payloadKey] = void 0;
      }
    }
  }

  // src/shared/dom.js
  function appendWhenReady(node, parentSelector = "head") {
    const target = parentSelector === "body" ? document.body : document.head || document.documentElement;
    if (target) {
      target.appendChild(node);
      return;
    }
    document.addEventListener(
      "DOMContentLoaded",
      () => {
        const parent = parentSelector === "body" ? document.body : document.head;
        parent.appendChild(node);
      },
      { once: true }
    );
  }
  function createStyle(id, css) {
    const existing = document.getElementById(id);
    if (existing) return existing;
    const style = document.createElement("style");
    style.id = id;
    style.textContent = css;
    appendWhenReady(style);
    return style;
  }

  // src/shared/troops.js
  function formatTroopCount(troops) {
    const num = Number(troops) / 10;
    if (!Number.isFinite(num)) return "0";
    if (num >= 1e7) return `${(Math.floor(num / 1e5) / 10).toFixed(1)}M`;
    if (num >= 1e6) return `${(Math.floor(num / 1e4) / 100).toFixed(2)}M`;
    if (num >= 1e5) return `${Math.floor(num / 1e3)}K`;
    if (num >= 1e4) return `${(Math.floor(num / 100) / 10).toFixed(1)}K`;
    if (num >= 1e3) return `${(Math.floor(num / 10) / 100).toFixed(2)}K`;
    return Math.floor(num).toString();
  }

  // src/userscript/name-layer-enhancer.js
  var CURRENT_WEAKNESS_THRESHOLD = 0.1;
  var TOTAL_POTENTIAL_THRESHOLD = 0.3;
  var DANGER_THRESHOLD = 1.35;
  var ATOM_COST = 75e4;
  var HYDROGEN_COST = 5e6;
  var ATOM_ICON = "[A]";
  var HYDROGEN_ICON = "[H]";
  function installNameLayerEnhancer({ app, settings, syncContext }) {
    let game = null;
    let nameLayerContainer = null;
    let farmRecommendations = /* @__PURE__ */ new Map();
    installEnhancerStyles();
    if (typeof GM_registerMenuCommand === "function") {
      GM_registerMenuCommand("Refresh OpenFront UI overlays", refreshAllTroopDisplays);
      GM_registerMenuCommand("Disable all automation", () => {
        settings.set("autopilot", false, syncContext);
        settings.set("autoSpawn", false, syncContext);
        settings.set("autoFarm", false, syncContext);
        settings.set("autoAlliance", false, syncContext);
        settings.set("smartAttack", false, syncContext);
      });
    }
    settings.onChange(refreshAllTroopDisplays);
    window.addEventListener("message", (event) => {
      const data = event && event.data;
      if (!data || data.source !== app.messageSource || data.type !== "farm-recommendations") return;
      farmRecommendations = new Map((data.recommendations || []).map((rec) => [rec.id, rec]));
      refreshAllTroopDisplays();
    });
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", waitForGame, { once: true });
    } else {
      waitForGame();
    }
    console.log(`%c[${app.shortName}] UI enhancer loaded`, "color:#4fc3f7;font-weight:bold");
    function waitForGame() {
      try {
        const pageWin = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
        const leaderboard = pageWin.document.querySelector("leader-board");
        if (leaderboard && leaderboard.game) {
          game = leaderboard.game;
          findNameLayerContainer();
        } else {
          setTimeout(waitForGame, 1e3);
        }
      } catch (error) {
        console.error(`[${app.shortName}] waitForGame failed`, error);
      }
    }
    function findNameLayerContainer() {
      try {
        const selector = 'div[style*="position: fixed"][style*="left: 50%"][style*="top: 50%"][style*="pointer-events: none"][style*="z-index: 2"]';
        const containers = document.querySelectorAll(selector);
        if (containers.length > 0) {
          nameLayerContainer = containers[0];
          setupObservers();
        } else {
          setTimeout(findNameLayerContainer, 1e3);
        }
      } catch (error) {
        console.error(`[${app.shortName}] findNameLayerContainer failed`, error);
      }
    }
    function setupObservers() {
      const containerObserver = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
          mutation.addedNodes.forEach((node) => {
            if (node.nodeType !== Node.ELEMENT_NODE) return;
            const troopsDiv = node.querySelector(".player-troops");
            if (troopsDiv) setupTroopObserver(troopsDiv);
          });
        });
      });
      containerObserver.observe(nameLayerContainer, { childList: true, subtree: true });
      nameLayerContainer.querySelectorAll(".player-troops").forEach(setupTroopObserver);
    }
    function setupTroopObserver(troopsDiv) {
      if (troopsDiv._ofatObserver) return;
      const observer = new MutationObserver(() => {
        if (troopsDiv.textContent && troopsDiv.textContent.indexOf("/") === -1) updateTroopDisplay(troopsDiv);
      });
      observer.observe(troopsDiv, { childList: true, characterData: true, subtree: true });
      troopsDiv._ofatObserver = observer;
      updateTroopDisplay(troopsDiv);
    }
    function refreshAllTroopDisplays() {
      if (!nameLayerContainer) return;
      nameLayerContainer.querySelectorAll(".player-troops").forEach(updateTroopDisplay);
    }
    function updateTroopDisplay(troopsDiv) {
      if (!game || !troopsDiv) return;
      const element = troopsDiv.parentElement;
      if (!element) return;
      const nameSpan = element.querySelector(".player-name-span");
      if (!nameSpan) return;
      const playerName = getBasePlayerName(nameSpan);
      const player = findPlayerByName(playerName);
      if (!player) return;
      const observer = troopsDiv._ofatObserver;
      if (observer) observer.disconnect();
      try {
        const currentTroops = Number(player.troops());
        const maxTroops = Number(game.config().maxTroops(player));
        const attackingTroops = getOutgoingAttackTroops(player);
        if (!Number.isFinite(currentTroops) || !Number.isFinite(maxTroops) || !Number.isFinite(attackingTroops)) return;
        renderTroopText(troopsDiv, currentTroops, maxTroops);
        renderRatioBar(element, player, currentTroops, maxTroops, attackingTroops);
        renderRiskState(troopsDiv, player, currentTroops, maxTroops, attackingTroops);
        renderThreatIcon(nameSpan, playerName, player);
        renderFarmBadge(nameSpan, player);
      } catch (error) {
        console.error(`[${app.shortName}] updateTroopDisplay failed`, error);
      } finally {
        if (observer) observer.observe(troopsDiv, { childList: true, characterData: true, subtree: true });
      }
    }
    function cleanPlayerName(text) {
      return String(text || "").replace(ATOM_ICON, "").replace(HYDROGEN_ICON, "").replace(/\b(?:FARM\s+\d+%|TARGET\s+\d+%|MARK|WAIT|DANGER|HOLD)\b/g, "").trim();
    }
    function getBasePlayerName(nameSpan) {
      const textNode = Array.from(nameSpan.childNodes).find((node) => node.nodeType === Node.TEXT_NODE);
      return cleanPlayerName(textNode ? textNode.textContent : nameSpan.textContent);
    }
    function findPlayerByName(playerName) {
      const players = typeof game.playerViews === "function" ? game.playerViews() : [];
      return players.find((player) => {
        if (!player || typeof player.isAlive === "function" && !player.isAlive()) return false;
        const name = typeof player.name === "function" ? player.name() : "";
        const displayName = typeof player.displayName === "function" ? player.displayName() : "";
        return name === playerName || displayName === playerName;
      });
    }
    function getOutgoingAttackTroops(player) {
      if (typeof player.outgoingAttacks !== "function") return 0;
      return player.outgoingAttacks().reduce((sum, attack) => sum + (attack.retreating ? 0 : Number(attack.troops || 0)), 0);
    }
    function renderTroopText(troopsDiv, currentTroops, maxTroops) {
      troopsDiv.textContent = "";
      troopsDiv.appendChild(document.createTextNode(formatTroopCount(currentTroops)));
      if (settings.get("showTroopRatios")) {
        const maxSpan = document.createElement("span");
        maxSpan.className = "ofat-max-troops";
        maxSpan.textContent = `/${formatTroopCount(maxTroops)}`;
        troopsDiv.appendChild(maxSpan);
      }
    }
    function renderRatioBar(element, player, currentTroops, maxTroops, attackingTroops) {
      let ratioBar = element.querySelector(".ofat-troop-ratio-bar");
      if (!ratioBar) {
        ratioBar = document.createElement("div");
        ratioBar.className = "ofat-troop-ratio-bar";
        ratioBar.appendChild(Object.assign(document.createElement("div"), { className: "ofat-ratio-fill" }));
        ratioBar.appendChild(Object.assign(document.createElement("div"), { className: "ofat-ratio-buffer" }));
        element.appendChild(ratioBar);
      }
      const fill = ratioBar.querySelector(".ofat-ratio-fill");
      const buffer = ratioBar.querySelector(".ofat-ratio-buffer");
      const isBot = getPlayerType(player) === "BOT";
      if (!settings.get("showRatioBar") || isBot) {
        ratioBar.style.display = "none";
        return;
      }
      ratioBar.style.display = "";
      const totalPotential = currentTroops + attackingTroops;
      const totalRatio = maxTroops > 0 ? Math.min(totalPotential / maxTroops, 1) : 0;
      const mainRatio = attackingTroops === 0 ? 1 : totalPotential > 0 ? currentTroops / totalPotential : 0;
      const bufferRatio = attackingTroops === 0 ? 0 : totalPotential > 0 ? attackingTroops / totalPotential : 0;
      const mainWidth = mainRatio * totalRatio * 100;
      const bufferWidth = bufferRatio * totalRatio * 100;
      fill.style.width = `${mainWidth}%`;
      buffer.style.width = `${bufferWidth}%`;
      buffer.style.left = `${mainWidth}%`;
    }
    function renderRiskState(troopsDiv, player, currentTroops, maxTroops, attackingTroops) {
      const myPlayer = typeof game.myPlayer === "function" ? game.myPlayer() : null;
      troopsDiv.classList.remove("ofat-flashing-orange");
      troopsDiv.style.color = "";
      if (!myPlayer || player.id() === myPlayer.id()) return;
      const myTroops = Number(myPlayer.troops());
      if (!Number.isFinite(myTroops) || myTroops <= 0) return;
      const dangerRatio = currentTroops / myTroops;
      const sameTeam = isSameTeamMode() && typeof player.isOnSameTeam === "function" && player.isOnSameTeam(myPlayer);
      if (settings.get("showWeaknessIndicator") && currentTroops <= CURRENT_WEAKNESS_THRESHOLD * maxTroops && currentTroops + attackingTroops < TOTAL_POTENTIAL_THRESHOLD * maxTroops) {
        troopsDiv.classList.add("ofat-flashing-orange");
      } else if (settings.get("showDangerIndicator") && dangerRatio >= DANGER_THRESHOLD && !sameTeam) {
        troopsDiv.style.color = "red";
      }
    }
    function renderThreatIcon(nameSpan, playerName, player) {
      nameSpan.textContent = playerName;
      if (!settings.get("showThreatIcons")) return;
      const myPlayer = typeof game.myPlayer === "function" ? game.myPlayer() : null;
      if (myPlayer && player.id() === myPlayer.id()) return;
      const icon = getThreatIcon(player);
      if (!icon) return;
      const span = document.createElement("span");
      span.className = "ofat-threat-icon";
      span.textContent = icon;
      nameSpan.appendChild(span);
    }
    function renderFarmBadge(nameSpan, player) {
      if (!settings.get("showAttackBadges")) return;
      const playerID = typeof player.id === "function" ? player.id() : null;
      const rec = playerID ? farmRecommendations.get(playerID) : null;
      if (!rec || !rec.label) return;
      const span = document.createElement("span");
      span.className = `ofat-farm-badge ofat-farm-badge-${rec.status || "hold"}`;
      span.textContent = rec.label;
      span.title = rec.reason || "";
      nameSpan.appendChild(span);
    }
    function getThreatIcon(player) {
      const hasSilo = typeof player.units === "function" && Array.isArray(player.units("Missile Silo")) && player.units("Missile Silo").length > 0;
      if (!hasSilo) return "";
      const gold = Number(typeof player.gold === "function" ? player.gold() : 0);
      if (gold >= HYDROGEN_COST) return HYDROGEN_ICON;
      if (gold >= ATOM_COST) return ATOM_ICON;
      return "";
    }
    function getPlayerType(player) {
      try {
        return String(typeof player.type === "function" ? player.type() : "").toUpperCase();
      } catch (_) {
        return "";
      }
    }
    function isSameTeamMode() {
      try {
        const config = game.config().gameConfig();
        return config && config.gameMode === "Team";
      } catch (_) {
        return false;
      }
    }
  }
  function installEnhancerStyles() {
    createStyle(
      "ofat-name-layer-style",
      `
      .ofat-flashing-orange { color: #fff !important; animation: ofat-flash 0.2s infinite; }
      @keyframes ofat-flash { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0.68; } }
      .player-name-span, .player-troops {
        color: #000;
        text-shadow: 0 0 0.1em #fff;
        font-weight: 600;
      }
      .ofat-max-troops {
        font-size: 0.68em;
        color: rgba(0,0,0,0.82);
        text-shadow: 0 0 0.1em #fff;
      }
      .ofat-troop-ratio-bar {
        width: 32px;
        height: 4px;
        background: rgba(34,34,34,0.5);
        position: relative;
        border: 1px solid rgba(68,68,68,0.6);
        overflow: hidden;
      }
      .ofat-ratio-fill, .ofat-ratio-buffer { height: 100%; position: absolute; top: 0; }
      .ofat-ratio-fill { background: rgba(0, 210, 82, 0.74); }
      .ofat-ratio-buffer { background: rgba(255, 166, 0, 0.52); }
      .ofat-threat-icon { font-size: 0.72em; opacity: 0.72; margin-left: 2px; }
      .ofat-farm-badge {
        display: inline-block;
        margin-left: 3px;
        padding: 1px 3px;
        color: #05070a;
        border-radius: 3px;
        font-size: 0.62em;
        font-weight: 800;
        text-shadow: none;
        vertical-align: middle;
      }
      .ofat-farm-badge-farm { background: rgba(77, 224, 117, 0.9); }
      .ofat-farm-badge-target { background: rgba(83, 181, 255, 0.92); }
      .ofat-farm-badge-mark { background: rgba(255, 190, 72, 0.92); }
      .ofat-farm-badge-wait { background: rgba(190, 198, 210, 0.86); }
      .ofat-farm-badge-danger { background: rgba(255, 80, 80, 0.92); color: #fff; }
      .ofat-farm-badge-hold { background: rgba(190, 198, 210, 0.86); }
    `
    );
  }

  // src/main.js
  (function bootstrapUserscriptContext() {
    "use strict";
    const pageWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
    const settings = createGmSettingsStore(DEFAULT_SETTINGS);
    const syncContext = { app: APP, pageWindow };
    registerToggleMenu("Toggle hide ads", "hideAds", settings, syncContext);
    registerToggleMenu("Toggle advisor panel", "showAdvisorPanel", settings, syncContext);
    registerToggleMenu("Toggle spawn heatmap", "showHeatmap", settings, syncContext);
    registerToggleMenu("Toggle autopilot", "autopilot", settings, syncContext);
    registerToggleMenu("Toggle auto spawn", "autoSpawn", settings, syncContext);
    registerToggleMenu("Toggle auto farm", "autoFarm", settings, syncContext);
    registerToggleMenu("Toggle human farm", "autoFarmHumanTargets", settings, syncContext);
    registerToggleMenu("Toggle auto eco", "autoEco", settings, syncContext);
    registerToggleMenu("Toggle auto defense", "autoDefense", settings, syncContext);
    registerToggleMenu("Toggle auto boat", "autoBoat", settings, syncContext);
    registerToggleMenu("Toggle auto weapons", "autoWeapons", settings, syncContext);
    registerToggleMenu("Toggle early-nuke (vs SAM build)", "autoWeaponsEarlyNuke", settings, syncContext);
    registerToggleMenu("Toggle auto alliance", "autoAlliance", settings, syncContext);
    registerToggleMenu("Toggle smart attack ratio", "smartAttack", settings, syncContext);
    registerToggleMenu("Toggle attack badges", "showAttackBadges", settings, syncContext);
    registerToggleMenu("Toggle ratio/health bar", "showRatioBar", settings, syncContext);
    registerToggleMenu("Toggle max troops text", "showTroopRatios", settings, syncContext);
    registerToggleMenu("Toggle weakness indicator", "showWeaknessIndicator", settings, syncContext);
    registerToggleMenu("Toggle danger indicator", "showDangerIndicator", settings, syncContext);
    registerToggleMenu("Toggle threat icons", "showThreatIcons", settings, syncContext);
    registerToggleMenu("Toggle troop economy panel", "showTroopEconomy", settings, syncContext);
    registerToggleMenu("Toggle round logging", "roundLogging", settings, syncContext);
    registerToggleMenu("Toggle auto log download", "roundLogAutoDownload", settings, syncContext);
    registerToggleMenu("Toggle network logging", "networkLogging", settings, syncContext);
    if (typeof GM_registerMenuCommand === "function") {
      GM_registerMenuCommand("Export current round log", () => {
        pageWindow.postMessage({ source: APP.messageSource, type: "export-round-log" }, "*");
      });
      GM_registerMenuCommand("Clear current round log", () => {
        pageWindow.postMessage({ source: APP.messageSource, type: "clear-round-log" }, "*");
      });
    }
    window.addEventListener("message", (event) => {
      const data = event && event.data;
      if (!data || data.source !== APP.messageSource || data.type !== "setting-changed") return;
      settings.set(data.key, data.value, { skipPageSync: true });
    });
    injectPageBundle(pageWindow, APP, settings.snapshot(), PAGE_BUNDLE_SOURCE);
    installNameLayerEnhancer({ app: APP, settings, syncContext });
  })();
})();