Openfront Multitab cheat

Cheat GUI, teaming up bot, control with emoji, auto spawn, spawn overlay, auto factory, auto give troops and gold, auto alliance.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Openfront Multitab cheat
// @namespace    Openfront Multitab cheat
// @version      2.3.2
// @description  Cheat GUI, teaming up bot, control with emoji, auto spawn, spawn overlay, auto factory, auto give troops and gold, auto alliance.
// @author       EcoMaxer
// @match        *://openfront.io/*
// @match        *://*.openfront.io/*
// @allFrames    true
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @run-at       document-body
// @license      MIT
// ==/UserScript==

const storage_key = "multi_cheat";
const UI_KEY = "multi_cheat_ui";

const default_ui = {
  x: 20,
  y: 20,
  collapsed: false
};

const default_setting = {
  targetName: "",
  autoJoinLeave: true,
  autoSpawn: true,
  autoTroops: true,
  autoGold: false,
  autoFactory: true,
  maxFactories: 12
};

let style = `#cheat-panel{--panel:#1c2027;--panel-raised:#23272f;--line:#2e333c;--text:#e8e6df;--text-dim:#8b909c;--amber:#e0a84e;--amber-dim:#6b552c;--green:#6ea87a;--red:#d16b6b;--mono:"IBM Plex Mono","SF Mono",Menlo,Consolas,monospace;--sans:"Inter","Helvetica Neue",Arial,sans-serif;background-color:var(--panel);color:var(--text);font-family:var(--sans);box-sizing:border-box;overflow:hidden;width:320px;position:fixed;top:0;left:0;z-index:999999999999;border:1px solid var(--line);border-radius:10px;display:flex;flex-direction:column}#header{border-bottom:1px solid var(--line);padding:18px 20px;font-size:15px;font-weight:600;letter-spacing:.01em;display:flex;flex-direction:row;align-items:center;justify-content:space-between}#body{flex-grow:1}#footer{padding:14px 20px;display:flex;align-items:center;justify-content:space-between;background:var(--panel-raised)}.row{display:flex;align-items:center;justify-content:space-between;padding:9px 0}.group{padding:18px 20px;border-bottom:1px solid var(--line)}.group-label{font-family:var(--mono);font-size:12px;text-transform:uppercase;letter-spacing:.1em;color:var(--text-dim);margin-bottom:12px}.row-text{display:flex;flex-direction:column;gap:2px;max-width:280px}.row-label{font-size:14px}.row-hint{font-size:12px;color:var(--text-dim)}.text-field{background:var(--panel-raised);border:1px solid var(--line);border-radius:6px;color:var(--text);font-family:var(--mono);font-size:12.5px;padding:6px 10px;width:120px;text-align:right}.text-field:focus{outline:0;border-color:var(--amber)}.switch{position:relative;display:inline-block;width:38px;height:22px}.switch input{opacity:0;width:0;height:0}.slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:var(--panel-raised);box-sizing:content-box;border:1px solid var(--line);transition:.4s;border-radius:34px}.slider::before{position:absolute;content:"";height:16px;width:16px;left:2px;bottom:2px;background-color:var(--text-dim);transition:.4s;border-radius:50%}input:checked+.slider{border-color:var(--amber);background-color:var(--amber-dim)}input:checked+.slider::before{background-color:var(--amber);transform:translateX(16px)}.stepper{display:flex;align-items:center;gap:0;border:1px solid var(--line);border-radius:6px;overflow:hidden;background:var(--panel-raised)}.stepper button{background:0 0;border:none;color:var(--text-dim);width:26px;height:26px;cursor:pointer;font-family:var(--mono);font-size:14px}.stepper input{width:42px;text-align:center;background:0 0;border:none;border-left:1px solid var(--line);border-right:1px solid var(--line);color:var(--text);font-family:var(--mono);font-size:12.5px;height:26px}.stepper input:focus{outline:0}.save-note{font-family:var(--mono);font-size:12px;color:var(--text-dim);transition:opacity .3s ease}.reset-btn{background:0 0;border:1px solid var(--line);color:var(--text-dim);font-family:var(--mono);font-size:12px;padding:6px 12px;border-radius:6px;cursor:pointer;letter-spacing:.03em}.reset-btn:hover{color:var(--red);border-color:#6b3535}.status-light{display:flex;align-items:center;gap:6px;font-family:var(--mono);font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.06em}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}.dot{width:7px;height:7px;border-radius:50%;background:var(--red);box-shadow:0 0 6px var(--red);animation:pulse 2.4s ease-in-out infinite}.dot.green{background:var(--green);box-shadow:0 0 6px var(--green)}`
let panel = `<div id=cheat-panel><div id=header><span class=header-title>Multitab cheat</span><div class=status-light><span class=dot></span><span id=status-text>Disconnected</span></div></div><div id=body><div class=group><div class=group-label>Identity</div><div class=row><div class=row-text><span class=row-label>Target name</span> <span class=row-hint>In game username</span></div><input id=targetName class=text-field></div></div><div class=group><div class=group-label>Bot behaviour</div><div class=row><div class=row-text><span class=row-label>Auto join/leave</span> <span class=row-hint>Join and Leave game automatically</span></div><label class=switch><input id=autoJoinLeave type=checkbox> <span class=slider></span></label></div><div class=row><div class=row-text><span class=row-label>Auto spawn</span> <span class=row-hint>Auto pick spawn near the player</span></div><label class=switch><input id=autoSpawn type=checkbox> <span class=slider></span></label></div><div class=row><div class=row-text><span class=row-label>Auto troops</span> <span class=row-hint>Send reinforcements below threshold</span></div><label class=switch><input id=autoTroops type=checkbox> <span class=slider></span></label></div><div class=row><div class=row-text><span class=row-label>Auto gold</span> <span class=row-hint>Send 40% of it's income on interval</span></div><label class=switch><input id=autoGold type=checkbox> <span class=slider></span></label></div><div class=row><div class=row-text><span class=row-label>Auto factory</span> <span class=row-hint>Build factory automatically</span></div><label class=switch><input id=autoFactory type=checkbox> <span class=slider></span></label></div></div><div class=group><div class=group-label>Limit</div><div class=row><div class=row-text><span class=row-label>Max factory</span> <span class=row-hint>Max number of factory</span></div><div class=stepper><button type=button>–</button> <input id=maxFactories inputMode=numeric> <button type=button>+</button></div></div></div></div><div id=footer><span class=save-note id=save-note>Changes save instantly</span> <button class=reset-btn id=reset-btn>Reset</button></div></div>`

let toggled = true;
let isDragging = false;
let setting = null;
let uiState = null;
let hasListeners = false;

let styleElement = document.createElement("style");
styleElement.textContent = style;
document.head.appendChild(styleElement);
document.body.insertAdjacentHTML("afterend", panel);

function saveUIState() {
  const panel = document.getElementById("cheat-panel");
  if (!panel) return;

  uiState = {
    x: parseInt(panel.style.left) || 20,
    y: parseInt(panel.style.top) || 20,
    collapsed: !toggled
  };

  GM_setValue(UI_KEY, uiState);
}

function loadUIState() {
  uiState = GM_getValue(UI_KEY, {...default_ui});

  const panel = document.getElementById("cheat-panel");
  if (!panel) return;

  // Ensure panel is visible and within screen bounds
  let x = uiState.x || 20;
  let y = uiState.y || 20;

  const panelWidth = panel.offsetWidth || 320;
  const headerHeight = 60;

  x = Math.max(10, Math.min(x, window.innerWidth - panelWidth - 10));
  y = Math.max(10, Math.min(y, window.innerHeight - headerHeight - 10));

  panel.style.left = x + "px";
  panel.style.top = y + "px";
  toggled = !uiState.collapsed;

  const body = document.getElementById("body");
  const footer = document.getElementById("footer");

  if (body) body.style.display = toggled ? "block" : "none";
  if (footer) footer.style.display = toggled ? "flex" : "none";

  panel.style.display = "flex";
}

function saveSetting() {
  if (!setting) return;
  if ("autoAlliance" in setting) {
    delete setting.autoAlliance;
  }
  // Read current values from UI
  for (const key in setting) {
    const element = document.getElementById(key);
    if (!element) continue;

    if (element.type === "checkbox") {
      setting[key] = element.checked;
    } else {
      setting[key] = element.inputMode === "numeric"
        ? parseInt(element.value) || 0
        : element.value;
    }
  }

  document.getElementById("save-note").textContent = "Saving...";

  GM_setValue(storage_key, JSON.stringify(setting));

  document.getElementById("save-note").textContent = "Saved!";
}

function loadSetting() {
  const saved = GM_getValue(storage_key);
  setting = {
    ...default_setting,           // start with all defaults
    ...(saved ? JSON.parse(saved) : {})  // override with saved values
  };
  if ("autoAlliance" in setting) {
    delete setting.autoAlliance;
  }
  // Populate UI
  for (const [key, value] of Object.entries(setting)) {
    const element = document.getElementById(key);
    if (!element) continue;

    if (element.type === "checkbox") {
      element.checked = Boolean(value);
      if (!hasListeners) element.addEventListener("change", saveSetting);
    } else {
      element.value = value;
      if (!hasListeners) element.addEventListener("input", saveSetting);
    }
  }

  hasListeners = true;
}

function makeDraggable() {
  const panelEl = document.getElementById("cheat-panel");
  const header = document.getElementById("header");

  let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;

  header.addEventListener("mousedown", (e) => {
    if (e.target.tagName === "INPUT" || e.target.tagName === "BUTTON") return;

    isDragging = false;
    e.preventDefault();

    pos3 = e.clientX;
    pos4 = e.clientY;

    document.addEventListener("mousemove", elementDrag);
    document.addEventListener("mouseup", closeDrag);
  });

  function elementDrag(e) {
    e.preventDefault();
    isDragging = true;

    pos1 = pos3 - e.clientX;
    pos2 = pos4 - e.clientY;
    pos3 = e.clientX;
    pos4 = e.clientY;

    let newLeft = panelEl.offsetLeft - pos1;
    let newTop = panelEl.offsetTop - pos2;

    // === Clamp to screen bounds ===
    const panelWidth = panelEl.offsetWidth;
    const panelHeight = panelEl.offsetHeight;
    const maxLeft = window.innerWidth - panelWidth - 10;   // 10px margin
    const maxTop = window.innerHeight - panelHeight - 10;

    newLeft = Math.max(10, Math.min(newLeft, maxLeft));    // keep at least 10px from edges
    newTop = Math.max(10, Math.min(newTop, maxTop));

    panelEl.style.left = newLeft + "px";
    panelEl.style.top = newTop + "px";
  }

  function closeDrag() {
    document.removeEventListener("mousemove", elementDrag);
    document.removeEventListener("mouseup", closeDrag);
    saveUIState();
  }
}

function setupSteppers() {
  const input = document.getElementById("maxFactories");
  if (!input) return;
  const minus = input.previousElementSibling;
  const plus = input.nextElementSibling;

  minus.addEventListener("click", () => {
    let val = parseInt(input.value) || 0;
    input.value = Math.max(0, val - 1);
    saveSetting();
  });

  plus.addEventListener("click", () => {
    let val = parseInt(input.value) || 0;
    input.value = val + 1;
    saveSetting();
  });
}

document.getElementById("header").addEventListener("click", (e) => {
  if (e.target.closest("input, button") || isDragging) {
    isDragging = false;
    return;
  }

  toggled = !toggled;
  document.getElementById("body").style.display = toggled ? "block" : "none";
  document.getElementById("footer").style.display = toggled ? "flex" : "none";

  saveUIState();
});

document.getElementById("reset-btn").addEventListener("click", () => {
  if (confirm("Reset all settings to default?")) {
    setting = {...default_setting};
    GM_setValue(storage_key, JSON.stringify(setting));
    loadSetting();
  }
});

setInterval(loadSetting, 2000);
loadUIState();
makeDraggable();
setupSteppers();

const statusElement = document.querySelector(".dot")
const statusText = document.getElementById("status-text")

const NativeWS = window.WebSocket;
const NativeSend = NativeWS.prototype.send;
const WS_OPEN = NativeWS.OPEN || 1;
let activeSocket = null;
let lastKnownTile = null;
let myUsername = null;
let lastDonate = 0;
let lastKnownGold = 0;
let builtFactories = 0;
let factory_id = null;
let targetPlayerSpawnTile = null;

function getSettings() {
  return {
    targetName: setting.targetName || "",
    autoJoinLeave: setting.autoJoinLeave,
    autoSpawn: setting.autoSpawn,
    autoTroops: setting.autoTroops,
    autoGold: setting.autoGold,
    autoFactory: setting.autoFactory,
    maxFactories: parseInt(setting.maxFactories) || 12
  };
}

let emojiControl = true;

let emoji_table = {
  "😀":0, "😊":1, "🥰":2, "😇":3, "😎":4,
  "😞":5, "🥺":6, "😭":7, "😱":8, "😡":9,
  "😈":10, "🤡":11, "🥱":12, "🫡":13, "🖕":14,
  "👋":15, "👏":16, "✋":17, "🙏":18, "💪":19,
  "👍":20, "👎":21, "🫴":22, "🤌":23, "🤦‍♂️":24,
  "🤝":25, "🆘":26, "🕊️":27, "🏳️":28, "⌛":29,
  "🔥":30, "💥":31, "💀":32, "☢️":33, "⚠️":34,
  "↖️":35, "⬆️":36, "↗️":37, "👑":38, "🥇":39,
  "⬅️":40, "🎯":41, "➡️":42, "🥈":43, "🥉":44,
  "↙️":45, "⬇️":46, "↘️":47, "❤️":48, "💔":49,
  "💰":50, "⚓":51, "⛵":52, "🏡":53, "🛡️":54,
  "🏭":55, "🚂":56, "❓":57, "🐔":58,"🐀":59
}

function setLastKnownTile(tile) {
  lastKnownTile = tile;
  try {
    if (window.top) window.top.__blonLastKnownTile = tile;
  } catch (e) {}
}

window.WebSocket = function(url, protocols) {
  const sock = protocols !== void 0 ? new NativeWS(url, protocols) : new NativeWS(url);

  sock.addEventListener("close", () => {
    if (activeSocket === sock) {
      activeSocket = null;
      try {
        if (window.top && window.top.__blonActiveSocket === sock) {
          window.top.__blonActiveSocket = null;
        }
      } catch (e) {}
    }
  });
  return sock;
};
window.WebSocket.prototype = NativeWS.prototype;
Object.getOwnPropertyNames(NativeWS).forEach((k) => {
  try {
    window.WebSocket[k] = NativeWS[k];
  } catch (_) {
  }
});
NativeWS.prototype.send = function(data) {
  let payload = data;
  if (typeof data === "string") {
    try {
      const msg = JSON.parse(data);
      if (msg) {
        const isGameMsg = msg.type === "intent" && msg.intent || msg.type === "quick_chat" || msg.type === "ping";
        if (isGameMsg && activeSocket !== this) {
          activeSocket = this;
          try {
            if (window.top) window.top.__blonActiveSocket = this;
          } catch (e) {}
        }
        const intent = msg.intent || msg;
        if (typeof intent.tile === "number") setLastKnownTile(intent.tile);
        if (typeof intent.dst === "number") setLastKnownTile(intent.dst);
        if (typeof intent.src === "number") setLastKnownTile(intent.src);
        if (intent && (intent.type === "attack" || intent.type === "boat" || intent.type === "donate_troops") && typeof intent.troops === "number") {
          try {
            const state = getGameState();
            if (state && state.myPlayer) {
              const rawMyTroops = typeof state.myPlayer.troops === "function" ? state.myPlayer.troops() : 0;
              const myTroops = typeof rawMyTroops === "bigint" ? Number(rawMyTroops) : Number(rawMyTroops || 0);
              window.__blonPendingExpansions = window.__blonPendingExpansions || [];
              const isDuplicate = window.__blonPendingExpansions.some(item => Math.abs(Date.now() - item.time) < 10 && item.amount === intent.troops);
              if (!isDuplicate) {
                window.__blonPendingExpansions.push({
                  time: Date.now(),
                  amount: intent.troops,
                  initialTroops: myTroops
                });
              }
            }
          } catch (e) {}
        }
        if (msg.type === "join") {
          myUsername = msg.username;
        }
      }
    } catch (e) {}
  }
  return NativeSend.call(this, payload);
};

function sendPacket(intentObj) {
  let sock = activeSocket;
  try {
    if (!sock && window.top && window.top.__blonActiveSocket) {
      sock = window.top.__blonActiveSocket;
    }
  } catch (e) {}
  if (!sock || sock.readyState !== WS_OPEN) return false;
  try {
    if (intentObj && (intentObj.type === "attack" || intentObj.type === "boat" || intentObj.type === "donate_troops") && typeof intentObj.troops === "number") {
      try {
        const state = getGameState();
        if (state && state.myPlayer) {
          const rawMyTroops = typeof state.myPlayer.troops === "function" ? state.myPlayer.troops() : 0;
          const myTroops = typeof rawMyTroops === "bigint" ? Number(rawMyTroops) : Number(rawMyTroops || 0);
          window.__blonPendingExpansions = window.__blonPendingExpansions || [];
          const isDuplicate = window.__blonPendingExpansions.some(item => Math.abs(Date.now() - item.time) < 10 && item.amount === intentObj.troops);
          if (!isDuplicate) {
            window.__blonPendingExpansions.push({
              time: Date.now(),
              amount: intentObj.troops,
              initialTroops: myTroops
            });
          }
        }
      } catch (e) {}
    }
    NativeSend.call(sock, JSON.stringify({ type: "intent", intent: intentObj }));
    return true;
  } catch (e) {
    return false;
  }
}

function getAccessibleDocuments() {
  const docs = [document];
  try {
    for (const frame of document.querySelectorAll("iframe")) {
      if (frame.contentDocument) docs.push(frame.contentDocument);
    }
  } catch (e) {}
  return docs;
}

function getGameState() {
  try {
    let overlay = null;
    for (const doc of getAccessibleDocuments()) {
      overlay = doc.querySelector("player-info-overlay");
      if (overlay) break;
    }

    let game = overlay && overlay.game ? overlay.game : null;
    if (!game) return;

    let myPlayer = game.myPlayer ? game.myPlayer() : null;
    if (!myPlayer) return;

    return { game, myPlayer };
  } catch (e) {}
}

function clamp(value, min, max) {
  return Math.max(min, Math.min(max, value));
}

function getPercentAmount(value, percent) {
  const n = typeof value === "bigint" ? Number(value) : Number(value || 0);
  if (!Number.isFinite(n) || n <= 0) return 0;
  const pct = clamp(Number(percent || 0), 1, 100);
  return Math.max(1, Math.floor(n * pct / 100));
}

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

function getRandomPointOnCircle(centerIndex, stride) {
  const radius = getRandomInt(12, 24);
  const angle = Math.random() * Math.PI * 2;

  const dx = Math.round(radius * Math.cos(angle));
  const dy = Math.round(radius * Math.sin(angle));

  const newIndex = centerIndex + (dy * stride) + dx;
  return newIndex;
}

// === Spawn Phase Overlay ===
let spawnOverlayCanvas = null;
let spawnOverlayCtx = null;
let spawnMouseX = 0;
let spawnMouseY = 0;
let spawnGap = 1;
let spawnRotation = 0;
let isInSpawnPhase = false;
let overlayLoopStarted = false;
let sideMode = false;
let spawnCircleCount = 4;

const GAME_PIXEL_SIZE = 1;
const SPAWN_POSITIONS_KEY = "spawn_circle_positions";

function getGameScale() {
  try {
    const overlay = document.querySelector("player-info-overlay");
    if (overlay && overlay.transform && typeof overlay.transform.scale === "number") {
      return Math.max(0.2, Math.min(20, overlay.transform.scale));
    }
  } catch (e) {}
  return 1;
}

function createSpawnOverlay() {
  if (spawnOverlayCanvas) return;

  spawnOverlayCanvas = document.createElement("canvas");
  spawnOverlayCanvas.id = "spawn-phase-overlay";
  spawnOverlayCanvas.style.cssText = `
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    pointer-events: none;
    z-index: 999999999998;
    image-rendering: pixelated;
  `;
  document.body.appendChild(spawnOverlayCanvas);

  spawnOverlayCtx = spawnOverlayCanvas.getContext("2d");
  spawnOverlayCtx.imageSmoothingEnabled = false;

  function resize() {
    if (!spawnOverlayCanvas) return;
    spawnOverlayCanvas.width = window.innerWidth;
    spawnOverlayCanvas.height = window.innerHeight;
  }
  resize();
  window.addEventListener("resize", resize);

  window.addEventListener("mousemove", (e) => {
    spawnMouseX = e.clientX;
    spawnMouseY = e.clientY;
  });

  // ===== Save 1D tile positions on left click =====
  window.addEventListener("mousedown", (e) => {
  if (!isInSpawnPhase || e.button !== 0) return;

  // Wait a bit so the game has time to set spawnTile
  setTimeout(() => {
    const state = getGameState();
    if (!state || !state.game || !state.myPlayer) return;

    const game = state.game;
    const cfg = getSettings();
    const targetPlayer = game.players().find(p => p.name() === cfg.targetName);

    if (!targetPlayer?.state?.spawnTile) {
      console.warn("spawnTile still not available after delay");
      return;
    }

    const centerTile = targetPlayer.state.spawnTile;
    const mapWidth = game._map.width_;
    const baseDistance = 7 + spawnGap;

    spawnCircleCount = JSON.parse(GM_getValue("bots") || "[]").length || 4;

    const positions = [];

    for (let i = 0; i < spawnCircleCount; i++) {
      let angle;

      if (sideMode) {
        const spread = 90;
        angle = (spawnRotation - spread / 2 + (i * spread / Math.max(spawnCircleCount - 1, 1))) * (Math.PI / 180);
      } else {
        angle = (spawnRotation + (i * 360 / spawnCircleCount)) * (Math.PI / 180);
      }

      const dx = Math.round(Math.cos(angle) * baseDistance);
      const dy = Math.round(Math.sin(angle) * baseDistance);

      const tileIndex = centerTile + (dy * mapWidth) + dx;
      positions.push(tileIndex);
    }

    GM_setValue(SPAWN_POSITIONS_KEY, `${JSON.stringify(positions)}:${Date.now()}`);
    console.log("Saved 1D spawn positions:", positions);
  }, 500);
});

  window.addEventListener("keydown", (e) => {
    if (!isInSpawnPhase) return;

    if (e.key === "r" || e.key === "R") {
      spawnRotation = (spawnRotation + 5) % 360;
    }
    if (e.key === "y" || e.key === "Y") {
      spawnGap = Math.min(spawnGap + 1, 50);
    }
    if (e.key === "t" || e.key === "T") {
      spawnGap = Math.max(spawnGap - 1, 1);
    }
    if (e.key === "u" || e.key === "U") {
      sideMode = !sideMode;
    }
  });

  if (!overlayLoopStarted) {
    overlayLoopStarted = true;
    requestAnimationFrame(spawnOverlayLoop);
  }
}

function destroySpawnOverlay() {
  if (spawnOverlayCanvas) {
    spawnOverlayCanvas.remove();
    spawnOverlayCanvas = null;
    spawnOverlayCtx = null;
  }
  isInSpawnPhase = false;
}

function drawSpawnOverlay() {
  if (!spawnOverlayCtx || !isInSpawnPhase) return;

  const ctx = spawnOverlayCtx;
  ctx.clearRect(0, 0, spawnOverlayCanvas.width, spawnOverlayCanvas.height);

  const scale = getGameScale();
  const pixelScale = scale * GAME_PIXEL_SIZE;

  const baseRadius = 3;
  const baseDistance = 7 + spawnGap;

  const radius = Math.max(2, Math.round(baseRadius * pixelScale));
  const distance = baseDistance * pixelScale;
  const blockSize = Math.max(1, Math.round(pixelScale));

  // Keep circle count in sync with number of bots
  spawnCircleCount = JSON.parse(GM_getValue("bots") || "[]").length || 4;

  // ========== Draw the gray circles ==========
  for (let i = 0; i < spawnCircleCount; i++) {
    let angle;

    if (sideMode) {
      const spread = 90;
      angle = (spawnRotation - spread / 2 + (i * spread / Math.max(spawnCircleCount - 1, 1))) * (Math.PI / 180);
    } else {
      angle = (spawnRotation + (i * 360 / spawnCircleCount)) * (Math.PI / 180);
    }

    const cx = Math.round(spawnMouseX + Math.cos(angle) * distance);
    const cy = Math.round(spawnMouseY + Math.sin(angle) * distance);

    // Border
    const borderRadius = radius + blockSize;
    ctx.fillStyle = "#6b7280";
    for (let y = -borderRadius; y <= borderRadius; y += blockSize) {
      for (let x = -borderRadius; x <= borderRadius; x += blockSize) {
        const distSq = (x + blockSize / 2) ** 2 + (y + blockSize / 2) ** 2;
        if (distSq <= borderRadius * borderRadius) {
          ctx.fillRect(cx + x, cy + y, blockSize, blockSize);
        }
      }
    }

    // Fill
    ctx.fillStyle = "#9ca3af";
    for (let y = -radius; y <= radius; y += blockSize) {
      for (let x = -radius; x <= radius; x += blockSize) {
        const distSq = (x + blockSize / 2) ** 2 + (y + blockSize / 2) ** 2;
        if (distSq <= radius * radius) {
          ctx.fillRect(cx + x, cy + y, blockSize, blockSize);
        }
      }
    }
  }

  // ========== Center red circle ==========
  const centerRadius = radius;
  const centerBorderRadius = centerRadius + blockSize;

  ctx.fillStyle = "#b91c1c";
  for (let y = -centerBorderRadius; y <= centerBorderRadius; y += blockSize) {
    for (let x = -centerBorderRadius; x <= centerBorderRadius; x += blockSize) {
      const distSq = (x + blockSize / 2) ** 2 + (y + blockSize / 2) ** 2;
      if (distSq <= centerBorderRadius * centerBorderRadius) {
        ctx.fillRect(spawnMouseX + x, spawnMouseY + y, blockSize, blockSize);
      }
    }
  }

  ctx.fillStyle = "#ef4444";
  for (let y = -centerRadius; y <= centerRadius; y += blockSize) {
    for (let x = -centerRadius; x <= centerRadius; x += blockSize) {
      const distSq = (x + blockSize / 2) ** 2 + (y + blockSize / 2) ** 2;
      if (distSq <= centerRadius * centerRadius) {
        ctx.fillRect(spawnMouseX + x, spawnMouseY + y, blockSize, blockSize);
      }
    }
  }
}

function spawnOverlayLoop() {
  drawSpawnOverlay();
  requestAnimationFrame(spawnOverlayLoop);
}

const BOT_ID = Array.from({ length: 4 }, () =>
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"[Math.floor(Math.random() * 52)]
).join('');

async function register(attempt = 0) {
  const bots = JSON.parse(GM_getValue("bots") || "[]");
  if (bots.includes(BOT_ID)) return true;
  if (attempt > 15) {
    return false;
  }
  bots.push(BOT_ID);
  GM_setValue("bots", JSON.stringify(bots));
  await new Promise(r => setTimeout(r, 100)); // delay (ms) before next attempt
  return register(attempt + 1);
}

let meIsTarget = false;
let unloadingEventSet = false;
let lastJoin = 0;
let registered = false;
let lastSpawn = 0;

async function tick() {
  const cfg = getSettings();
  if (!cfg.targetName) return;

  const state = getGameState();

  if (state && state.game && state.myPlayer) {
    statusElement.classList.add("green");
    statusText.textContent = "Connected";
  } else {
    statusElement.classList.remove("green");
    statusText.textContent = "Disconnected";

    if (cfg.autoJoinLeave && !meIsTarget && document.querySelectorAll("username-input input")[1].value === cfg.targetName) {
      GM_setValue("game-id", `null:${Date.now()}`)

      document.addEventListener("join-lobby", (e) => {
        const gameID = e.detail.gameID;
        if (gameID && e.detail.source != "singleplayer") {
          GM_setValue("game-id", `${gameID}:${Date.now()}`)
        }
      })
      document.addEventListener("leave-lobby", (e) => {
        GM_setValue("game-id", `null:${Date.now()}`);
      })
      meIsTarget = true;
      GM_setValue("bots", "[]")
    } else if (cfg.autoJoinLeave && !meIsTarget) {
      const gameInfo = GM_getValue("game-id").split(":");
      const gameID = gameInfo[0]
      const time = parseInt(gameInfo[1])
      const now = Date.now();

      if (now - lastJoin > 4000 && now - time <= 4000) {
        if (gameID != "null") {
          if (typeof window.showPage === "function") window.showPage("page-join-lobby");
          const joinEvent = new CustomEvent("join-lobby", {
            detail: { gameID: gameID, source: "public" },
            bubbles: true,
            composed: true
          });
          document.dispatchEvent(joinEvent);
        } else {
          const closeBTN = document.querySelector("button[aria-label='Close']")
          if (closeBTN) {
            closeBTN.click()
          } else {
            window.location.href = 'https://openfront.io/';
          }
        }
        if (document.querySelector(".bottom-0 .flex span.text-sm.font-bold.text-white")) {
          lastJoin = now;
        }
      }
    }
    return;
  }

  const game = state.game;
  const myPlayer = state.myPlayer;

  const gameConfig = game._config._gameConfig;
  let donateTroopsEnable = gameConfig.donateTroops;
  let donateGoldEnable = gameConfig.donateGold;
  let randomSpawn = gameConfig.randomSpawn;
  let disabledUnits = gameConfig.disabledUnits;
  let factoryDisabled = disabledUnits.includes("Factory");
  let allianceDisabled = game._config.disableAlliances();

  if (game.isCatchingUp() || game.config().isReplay()) return;

  const targetPlayer = game.players().find((p) => p.name() === cfg.targetName) || null;

  if (targetPlayer.id() === myPlayer.id() && game.inSpawnPhase() ) {
    isInSpawnPhase = true;
    createSpawnOverlay();
  } else {
    if (isInSpawnPhase) {
      destroySpawnOverlay();
    }
  }

  if (targetPlayer.id() === myPlayer.id()) {
    return
  };

  if (!registered) {
    const result = await register()
    registered = true;
  }

  const [id, time] = GM_getValue("game-id").split(":");
  if (id === "null" && Date.now() - parseInt(time) <= 6000) {
    window.location.href = 'https://openfront.io/';
  }
  if (!targetPlayer || !targetPlayer.isPlayer || !targetPlayer.isPlayer() || !targetPlayer.isAlive()) return;

  const mySmallID = myPlayer.smallID()

  if (game.inSpawnPhase()) {
    if (cfg.autoSpawn && !randomSpawn && targetPlayer.state.spawnTile && targetPlayer.state.spawnTile != targetPlayerSpawnTile) {
      targetPlayerSpawnTile = targetPlayer.state.spawnTile;
      const raw = GM_getValue(SPAWN_POSITIONS_KEY);
      if (raw) {
        const [positionsStr, timestampStr] = raw.split(":");
        const timestamp = parseInt(timestampStr);

        // Only use positions that were saved in the last 4 seconds
        if (Date.now() - lastSpawn > 8000 && Date.now() - timestamp <= 8000) {
          try {
            const positions = JSON.parse(positionsStr);
            const bots = JSON.parse(GM_getValue("bots") || "[]");
            const myIndex = bots.indexOf(BOT_ID);

            if (myIndex !== -1 && positions[myIndex] != null) {
              sendPacket({
                type: "spawn",
                tile: positions[myIndex]
              });
            }
            lastSpawn = Date.now();
          } catch (e) {
            console.error("Failed to parse spawn positions", e);
          }
        }
      }
    }
    return;
  }

  // Hide canvas after spawn phase
  document.querySelectorAll("canvas").forEach((e) => {
    if (e.style.display != "none") {
      e.style.display = "none";
      document.body.style.display = "none";
    }
  })

  if (!allianceDisabled) {
    if (!myPlayer.isOnSameTeam(targetPlayer)) {
      if (targetPlayer.state.outgoingAllianceRequests.includes(myPlayer.id()) && !myPlayer.isAlliedWith(targetPlayer)) {
        sendPacket({ type: "allianceRequest", recipient: targetPlayer.id() });
      }
    }
  }

  if (emojiControl) {
    const emojis = targetPlayer.state.outgoingEmojis;

    for (const emoji of emojis) {
      if (emoji && emoji.recipientID != "AllPlayers") {
        const recipientPlayer = game.players()[parseInt(emoji.recipientID) - 1]

        if (emoji.message === "💔" && recipientPlayer.id() === myPlayer.id()) {
          sendPacket({
            type: "breakAlliance",
            recipient: targetPlayer.id()
          })
        }
        if (emoji.message === "🆘") {
          if (!myPlayer.isOnSameTeam(targetPlayer) && myPlayer.isAlliedWith(targetPlayer) || myPlayer.isOnSameTeam(targetPlayer)) {
            if (donateGoldEnable && Date.now() - lastDonate > 10000 && recipientPlayer.id() === myPlayer.id()) {
              if (!myPlayer.state.outgoingEmojis.some((e) => e.message === "👍")) {
                sendPacket({type: "emoji", recipient: targetPlayer.id(), emoji: emoji_table["👍"]})
              }
              if (donateGoldEnable && sendPacket({ type: "donate_gold", recipient: targetPlayer.id(), gold: getPercentAmount(myPlayer.gold(), 100) })) {
                lastDonate = Date.now();
                lastKnownGold = 0;
              }
            } else if (donateGoldEnable && Date.now() - lastDonate > 10000 && recipientPlayer?.state?.outgoingEmojis?.some((e) => e.message === "👍")) {
              if (sendPacket({ type: "donate_gold", recipient: targetPlayer.id(), gold: getPercentAmount(myPlayer.gold(), 100) })) {
                lastDonate = Date.now();
                lastKnownGold = 0;
              }
            }
          }
        }
      }
    }
  }

  if (!myPlayer.isOnSameTeam(targetPlayer) && myPlayer.isAlliedWith(targetPlayer) || myPlayer.isOnSameTeam(targetPlayer)) {
    if (donateTroopsEnable && cfg.autoTroops && Date.now() - lastDonate > 10000 && targetPlayer.troops() / game.config().maxTroops(targetPlayer) <= 0.6) {
      if (sendPacket({ type: "donate_troops", recipient: targetPlayer.id(), troops: getPercentAmount(myPlayer.troops(), 40) })) lastDonate = Date.now();
    }

    if (donateGoldEnable && cfg.autoGold && Date.now() - lastDonate > 30000) {
      if (Number(myPlayer.gold()) - lastKnownGold < 0 || lastKnownGold < 0) lastKnownGold = 0;
      const goldDifference = Number(myPlayer.gold()) - lastKnownGold;

      if (builtFactories < cfg.maxFactories) {
        if (sendPacket({ type: "donate_gold", recipient: targetPlayer.id(), gold: getPercentAmount(goldDifference, 40) })) {
          lastDonate = Date.now();
          lastKnownGold = Number(myPlayer.gold());
        }
      } else {
        if (sendPacket({ type: "donate_gold", recipient: targetPlayer.id(), gold: getPercentAmount(myPlayer.gold(), 100) })) {
          lastDonate = Date.now();
          lastKnownGold = 0;
        }
      }
    }
  }

  for (const [key, value] of game.unitStates()) {
    if (value.ownerID == mySmallID && value.unitType == "Factory") {
      if (!factory_id) factory_id = value.id;
      builtFactories = value.level;
      break;
    }
  }

  const buildCost =
    builtFactories === 0 ? 125000 :
    builtFactories === 1 ? 250000 :
    builtFactories === 2 ? 500000 :
    builtFactories >= 3 ? 1000000 :
    null;

  if (!factoryDisabled && buildCost && cfg.autoFactory && builtFactories < cfg.maxFactories && myPlayer.gold() >= buildCost) {
    if (builtFactories >= 1) {
      sendPacket({
        type:"upgrade_structure",
        unit: "Factory",
        unitId: factory_id
      })
    } else {
      sendPacket({
        type: "build_unit",
        unit: "Factory",
        tile: myPlayer.state.spawnTile,
      })
    }
  }
}
setInterval(tick, 2000);