Openfront Multitab cheat

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu betiği yüklemek için bir betik yöneticisi eklentisi yüklemeniz gerekecektir.

(Zaten bir betik yöneticim var, hadi yükleyelim!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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

let panelStyle = `:root{--bg:#0d0f13;--panel:#14171c;--panel-raised:#1a1e24;--panel-hover:#1f242b;--line:#2a2f38;--line-bright:#3a3f4a;--text:#e4e6eb;--text-dim:#7a828c;--text-muted:#5a616b;--accent:#e8a838;--accent-dim:#7a5c1e;--accent-glow:rgba(232,168,56,.35);--green:#4ade80;--green-dim:#1a5c2e;--green-glow:rgba(74,222,128,.3);--red:#f87171;--red-dim:#5c1e1e;--red-glow:rgba(248,113,113,.3);--mono:"JetBrains Mono","Fira Code","SF Mono",Menlo,Consolas,monospace;--sans:Inter,system-ui,-apple-system,sans-serif;--radius-sm:4px;--radius-md:6px;--radius-lg:8px;--radius-full:999px;--shadow-sm:0 1px 2px rgba(0,0,0,.3);--shadow-md:0 4px 12px rgba(0,0,0,.4);--shadow-lg:0 8px 24px rgba(0,0,0,.5);--transition-fast:100ms ease;--transition-base:150ms ease;--transition-slow:200ms ease}*,*:before,*:after{box-sizing:border-box}#cheat-panel{background:var(--panel);color:var(--text);font-family:var(--sans);box-sizing:border-box;overflow:hidden;width:320px;position:fixed;top:12px;left:12px;z-index:2147483647;border:1px solid var(--line);border-radius:var(--radius-lg);display:flex;flex-direction:column;box-shadow:var(--shadow-lg);background-image:radial-gradient(ellipse 80% 50% at 50% -20%,rgba(232,168,56,.05),transparent),radial-gradient(circle at 100% 0%,rgba(74,222,128,.03),transparent)}#cheat-panel.collapsed #body,#cheat-panel.collapsed #footer{display:none!important}#cheat-panel.collapsed .collapse-btn svg{transform:rotate(180deg)}#header{border-bottom:1px solid var(--line);padding:10px 12px;font-size:13px;font-weight:600;letter-spacing:.01em;display:flex;align-items:center;justify-content:space-between;gap:10px;cursor:grab;user-select:none}.header-left{display:flex;align-items:center;gap:8px}.header-icon{font-size:13px;line-height:1}.header-title{font-size:13px;font-weight:600}.badge{font-size:9px;font-weight:700;font-family:var(--mono);color:var(--accent);background:rgba(232,168,56,.12);padding:1px 5px;border-radius:var(--radius-full);text-transform:uppercase;letter-spacing:.04em}.header-right{display:flex;align-items:center;gap:8px}.status-light{display:flex;align-items:center;gap:5px;font-family:var(--mono);font-size:9.5px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.06em}.dot{width:6px;height:6px;border-radius:50%;background:var(--red);box-shadow:0 0 5px var(--red);animation:pulse 2.2s ease-in-out infinite}.dot.green{background:var(--green);box-shadow:0 0 5px var(--green)}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}.icon-btn{background:transparent;border:none;padding:4px;color:var(--text-dim);cursor:pointer;border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;transition:color var(--transition-fast),background var(--transition-fast)}.icon-btn:hover{color:var(--text);background:var(--panel-hover)}.icon-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.collapse-btn svg{transition:transform var(--transition-base)}#body{flex-grow:1;overflow-y:auto;padding:4px 0 0}.group{padding:10px 12px 8px;border-bottom:1px solid var(--line)}.group:last-of-type{border-bottom:none}.group-header{display:flex;align-items:center;gap:6px;font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);margin-bottom:8px}.group-icon{font-size:11px;line-height:1}.row{display:flex;align-items:center;justify-content:space-between;padding:6px 0;gap:10px}.row-text{display:flex;flex-direction:column;gap:1px;min-width:0}.row-label{font-size:12.5px;font-weight:500}.row-hint{font-size:10.5px;color:var(--text-dim);font-weight:400}.text-field{background:var(--panel-raised);border:1px solid var(--line);border-radius:var(--radius-sm);color:var(--text);font-family:var(--mono);font-size:11.5px;padding:5px 9px;width:110px;text-align:right;transition:border-color var(--transition-fast),box-shadow var(--transition-fast)}.text-field:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-glow)}.text-field::placeholder{color:var(--text-muted)}.switch{position:relative;display:inline-block;width:40px;height:22px;flex-shrink:0}.switch input{opacity:0;width:0;height:0;position:absolute}.slider{position:absolute;inset:0;cursor:pointer;background:var(--panel-raised);border:1px solid var(--line);border-radius:var(--radius-full);transition:border-color var(--transition-base),background var(--transition-base),box-shadow var(--transition-base)}.slider::before{position:absolute;content:"";height:14px;width:14px;left:3px;bottom:3px;background:var(--text-dim);border-radius:50%;transition:transform var(--transition-base),background var(--transition-base);box-shadow:0 1px 3px rgba(0,0,0,.4)}.switch input:checked+.slider{border-color:var(--accent);background:linear-gradient(180deg,var(--accent-dim),#5a451a);box-shadow:0 0 0 1px var(--accent-glow)}.switch input:checked+.slider::before{background:var(--accent);transform:translateX(18px)}.switch input:focus-visible+.slider{outline:2px solid var(--accent);outline-offset:2px}.stepper{display:flex;align-items:center;gap:0;border:1px solid var(--line);border-radius:var(--radius-sm);overflow:hidden;background:var(--panel-raised)}.stepper-btn{background:transparent;border:none;color:var(--text-dim);width:28px;height:28px;cursor:pointer;font-family:var(--mono);font-size:13px;line-height:1;display:flex;align-items:center;justify-content:center;transition:color var(--transition-fast),background var(--transition-fast)}.stepper-btn:hover{color:var(--text);background:var(--panel-hover)}.stepper-btn:active{background:var(--line)}.stepper-btn:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.stepper-input{width:40px;text-align:center;background:transparent;border:none;border-left:1px solid var(--line);border-right:1px solid var(--line);color:var(--text);font-family:var(--mono);font-size:11.5px;height:28px;line-height:28px;-moz-appearance:textfield}.stepper-input::-webkit-outer-spin-button,.stepper-input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}.stepper-input:focus{outline:none}#footer{padding:8px 12px;display:flex;align-items:center;justify-content:space-between;background:var(--panel-raised);border-top:1px solid var(--line)}.footer-left{display:flex;align-items:center}.save-note{display:flex;align-items:center;gap:5px;font-family:var(--mono);font-size:10.5px;color:var(--text-dim)}.save-indicator{width:5px;height:5px;border-radius:50%;background:var(--green);box-shadow:0 0 4px var(--green);animation:pulse 2s ease-in-out infinite}.reset-btn{display:inline-flex;align-items:center;gap:5px;background:transparent;border:1px solid var(--line);color:var(--text-dim);font-family:var(--mono);font-size:10.5px;padding:5px 10px;border-radius:var(--radius-sm);cursor:pointer;letter-spacing:.02em;transition:color var(--transition-fast),border-color var(--transition-fast),background var(--transition-fast)}.reset-btn:hover{color:var(--red);border-color:#5a2a2a;background:rgba(248,113,113,.08)}.reset-btn:focus-visible{outline:2px solid var(--red);outline-offset:2px}.reset-btn svg{flex-shrink:0}#cheat-panel.collapsed{min-height:0}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;transition-duration:.01ms!important}}@media(prefers-contrast:high){:root{--line:#555;--text-dim:#bbb}.switch input:checked+.slider{border-width:2px}.stepper{border-width:2px}}#body::-webkit-scrollbar{width:6px}#body::-webkit-scrollbar-track{background:transparent}#body::-webkit-scrollbar-thumb{background:var(--line-bright);border-radius:3px}#body::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}`
let cheatPanel = `<div id="cheat-panel"><div id="header"><div class="header-left"><span class="header-icon" aria-hidden="true">⚡</span><span class="header-title">Multitab Cheat</span><span class="badge">v3.4.2</span></div><div class="header-right"><div class="status-light" id="status-indicator"><span class="dot" id="status-dot"></span><span id="status-text">Disconnected</span></div><button class="icon-btn collapse-btn" id="collapse-btn" aria-label="Collapse panel" title="Collapse"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"></polyline></svg></button></div></div><div id="body"><div class="group" id="group-identity"><div class="group-header"><span class="group-icon" aria-hidden="true">👤</span><span class="group-label">Identity</span></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 class="text-field" id="targetName" placeholder="PlayerName" autocomplete="off" spellcheck="false"></div></div><div class="group" id="group-behavior"><div class="group-header"><span class="group-icon" aria-hidden="true">🤖</span><span class="group-label">Bot Behaviour</span></div><div class="row toggle-row" data-setting="autoJoinLeave"><div class="row-text"><span class="row-label">Auto join/leave</span><span class="row-hint">Join and leave games automatically</span></div><label class="switch"><input type="checkbox" id="autoJoinLeave"><span class="slider"></span></label></div><div class="row toggle-row" data-setting="autoSpawn"><div class="row-text"><span class="row-label">Auto spawn</span><span class="row-hint">Auto pick spawn as shown in Overlay</span></div><label class="switch"><input type="checkbox" id="autoSpawn"><span class="slider"></span></label></div><div class="row toggle-row" data-setting="autoTroops"><div class="row-text"><span class="row-label">Auto troops</span><span class="row-hint">Send 40% troops below 60%</span></div><label class="switch"><input type="checkbox" id="autoTroops"><span class="slider"></span></label></div><div class="row toggle-row" data-setting="autoGold"><div class="row-text"><span class="row-label">Auto gold</span><span class="row-hint">Send 40% income / 100% after max factories</span></div><label class="switch"><input type="checkbox" id="autoGold"><span class="slider"></span></label></div><div class="row toggle-row" data-setting="autoFactory"><div class="row-text"><span class="row-label">Auto factory</span><span class="row-hint">Build/upgrade factories automatically</span></div><label class="switch"><input type="checkbox" id="autoFactory"><span class="slider"></span></label></div></div><div class="group" id="group-limits"><div class="group-header"><span class="group-icon" aria-hidden="true">⚙️</span><span class="group-label">Limits</span></div><div class="row"><div class="row-text"><span class="row-label">Max factories</span><span class="row-hint">Maximum factories to build</span></div><div class="stepper" id="factory-stepper"><button type="button" class="stepper-btn minus" aria-label="Decrease">−</button><input inputmode="numeric" id="maxFactories" class="stepper-input" value="12" min="0" max="99" aria-label="Max factories"><button type="button" class="stepper-btn plus" aria-label="Increase">+</button></div></div></div></div><div id="footer"><div class="footer-left"><span class="save-note" id="save-note"><span class="save-indicator" aria-hidden="true"></span>Changes save instantly</span></div><div class="footer-right"><button class="reset-btn" id="reset-btn" type="button"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V12"></path></svg><span>Reset</span></button></div></div></div>`

let setting = null;
const default_setting = {
  targetName: "",
  autoJoinLeave: true,
  autoSpawn: true,
  autoTroops: true,
  autoGold: false,
  autoFactory: true,
  maxFactories: 12
};
const default_ui = {
  x: 12,
  y: 12,
  collapsed: false
};

const SETTING_KEY = "MC-SETTING";
const SPAWN_POSITIONS_KEY = "spawn_positions";

const UI_KEY = "MC-UI";
let uiState = null;
let toggled = true;

// game_assets/assets/index-C_8ts8gt.js 3633:2028
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 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;

  let x = uiState.x || 12;
  let y = uiState.y || 12;

  const panelWidth = panel.offsetWidth || 320;
  const headerHeight = 50;  // updated for compact header

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

  panel.style.left = x + "px";
  panel.style.top = y + "px";
  toggled = !uiState.collapsed;
  panel.classList.toggle("collapsed", !toggled);

  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(data) {
  GM_setValue(SETTING_KEY, JSON.stringify(data));
}

function loadSetting() {
  const raw = GM_getValue(SETTING_KEY, null);
  const saved = raw ? JSON.parse(raw) : null;

  if (!saved) {
    saveSetting(default_setting);
    return { ...default_setting };
  }

  const merged = { ...default_setting };

  // Only copy over keys that actually exist in `saved`, whatever their value.
  for (const key of Object.keys(default_setting)) {
    if (Object.prototype.hasOwnProperty.call(saved, key)) {
      merged[key] = saved[key];
    }
  }

  if (JSON.stringify(merged) !== JSON.stringify(saved)) {
    saveSetting(merged);
  }

  return merged;
}

function refreshUI() {
  setting = loadSetting();

  for (const key of Object.keys(setting)) {
    const element = document.getElementById(key);

    if (!element) continue;

    if (element.type === "checkbox") {
      element.checked = setting[key];
    } else {
      element.value = setting[key];
    }
  }
}

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.closest("input, button, .switch, .icon-btn, .badge")) return;

    e.preventDefault();

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

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

  function elementDrag(e) {
    e.preventDefault();

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

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

    const panelWidth = panelEl.offsetWidth;
    const panelHeight = panelEl.offsetHeight;
    const maxLeft = window.innerWidth - panelWidth - 8;
    const maxTop = window.innerHeight - panelHeight - 8;

    newLeft = Math.max(8, Math.min(newLeft, maxLeft));
    newTop = Math.max(8, 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 renderUI() {
  if (!document.getElementById("MC-STYLE")) {
    const style = document.createElement("style");
    style.id  = "MC-STYLE";
    style.textContent = panelStyle;
    document.head.appendChild(style)
  }

  const isFirstRender = !document.getElementById("cheat-panel");
  if (isFirstRender) {
    document.body.insertAdjacentHTML("afterend", cheatPanel);
  }

  refreshUI();
  
  if (!isFirstRender) return;
  
  for (const key of Object.keys(setting)) {
    const element = document.getElementById(key);
    if (!element) continue;
    const eventName = element.type === "checkbox" ? "change" : "input";

    element.addEventListener(eventName, () => {
      setting[key] = element.type === "checkbox" ? element.checked : element.value;
      saveSetting(setting);
    });
  }

  const stepper = document.querySelector(".stepper");
  const maxFactoriesInput = document.getElementById("maxFactories");
  if (stepper && maxFactoriesInput) {
    const minusBtn = stepper.querySelector(".stepper-btn.minus");
    const plusBtn = stepper.querySelector(".stepper-btn.plus");

    minusBtn?.addEventListener("click", () => {
      setting.maxFactories = Math.max(0, Number(setting.maxFactories) - 1);
      maxFactoriesInput.value = setting.maxFactories;
      saveSetting(setting);
    });

    plusBtn?.addEventListener("click", () => {
      setting.maxFactories = Number(setting.maxFactories) + 1;
      maxFactoriesInput.value = setting.maxFactories;
      saveSetting(setting);
    });
  }

  document.getElementById("reset-btn")?.addEventListener("click", () => {
    saveSetting(default_setting);
    renderUI()
  });

  document.getElementById("collapse-btn")?.addEventListener("click", () => {
    toggled = !toggled;
    const panel = document.getElementById("cheat-panel");
    if (panel) panel.classList.toggle("collapsed", !toggled);
    document.getElementById("body").style.display = toggled ? "block" : "none";
    document.getElementById("footer").style.display = toggled ? "flex" : "none";
    saveUIState();
  });

  loadUIState();
  makeDraggable();
}

const NativeWS = window.WebSocket; // Copy websocket constructor
const NativeSend = NativeWS.prototype.send;
let activeSocket = null;

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

  sock.addEventListener("close", () => {
    if (activeSocket === sock) {
      activeSocket = null;
    }
  });
  return sock;
};
window.WebSocket.prototype = NativeWS.prototype;
Object.getOwnPropertyNames(NativeWS).forEach((k) => {
  try {
    window.WebSocket[k] = NativeWS[k];
  } catch (_) {}
});

NativeWS.prototype.send = function(data) {
  if (typeof data === "string") {
    try {
      const msg = JSON.parse(data);
      if (msg && (
        (msg.type === "intent" && msg.intent) ||
        msg.type === "quick_chat" ||
        msg.type === "ping"
      )) {
        if (activeSocket !== this) {
          activeSocket = this;
        }
      }
    } catch (_) {}
  }
  return NativeSend.call(this, data);
};

function sendPacket(data) {
  if (!activeSocket || activeSocket.readyState !== (NativeWS.OPEN || 1)) return false;

  try {
    NativeSend.call(activeSocket, JSON.stringify({ type: "intent", intent: data }));
    return true;
  } catch (_) {
    return false;
  } 
}

function getOverlay() {
  return document.querySelector("player-info-overlay");
}

function getGameState() {
  const overlay = getOverlay();

  if (!overlay) return;

  return overlay.game ? overlay.game : null;
}

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 spawnOverlay() {
  let sideMode = false;
  let spawnRotation = 0;
  let spawnGap = 0;
  let spawnMouseX = 0;
  let spawnMouseY = 0;
  let spawnCircleCount = 4;
  let lastBotsCheckTime = 0;
  let lastFrameTime = 0;
  const FRAME_INTERVAL = 1000 / 30; // 30 fps

  let isActive = false;
  let rafId = null;
  let canvas = null;
  let ctx = null;

  function getGameScale() {
    try {
      const overlay = getOverlay();
      if (overlay?.transform && typeof overlay.transform.scale === "number") {
        return overlay.transform.scale;
      }
    } catch (_) {}
    return 1;
  }

  function getAngle(i) {
    return sideMode
      ? (spawnRotation - 45 + (i * 90 / Math.max(spawnCircleCount - 1, 1))) * (Math.PI / 180)
      : (spawnRotation + (i * 360 / spawnCircleCount)) * (Math.PI / 180);
  }

  // AI code
  function drawCircle(cx, cy, radius, blockSize, fill, border) {
    const b = radius + blockSize;
    ctx.fillStyle = border;
    for (let y = -b; y <= b; y += blockSize)
      for (let x = -b; x <= b; x += blockSize)
        if ((x + blockSize / 2) ** 2 + (y + blockSize / 2) ** 2 <= b ** 2)
          ctx.fillRect(cx + x, cy + y, blockSize, blockSize);

    ctx.fillStyle = fill;
    for (let y = -radius; y <= radius; y += blockSize)
      for (let x = -radius; x <= radius; x += blockSize)
        if ((x + blockSize / 2) ** 2 + (y + blockSize / 2) ** 2 <= radius ** 2)
          ctx.fillRect(cx + x, cy + y, blockSize, blockSize);
  }

  function updateSpawnCircleCount() {
    const now = Date.now();
    if (now - lastBotsCheckTime < 500) return;
    lastBotsCheckTime = now;
    spawnCircleCount = JSON.parse(GM_getValue("bots") || "[]").length || 4;
  }

  function draw(timestamp) {
    rafId = requestAnimationFrame(draw);
    
    if (timestamp - lastFrameTime < FRAME_INTERVAL) return;
    lastFrameTime = timestamp;

    ctx.clearRect(0, 0, canvas.width, canvas.height);
    const pixelScale = getGameScale(); // The scale is equivelant to 1 game pixel in pixel
    const radius = Math.max(2, Math.round(3 * pixelScale)); // Radius of the circle 3px (outline not included)
    const distance = (8 + spawnGap) * pixelScale;
    const blockSize = Math.max(1, Math.round(pixelScale));

    updateSpawnCircleCount();

    for (let i = 0; i < spawnCircleCount; i++) {
      const angle = getAngle(i);
      drawCircle(
        Math.round(spawnMouseX + Math.cos(angle) * distance),
        Math.round(spawnMouseY + Math.sin(angle) * distance),
        radius, blockSize, "#9ca3af", "#6b7280"
      );
    }

    drawCircle(spawnMouseX, spawnMouseY, radius, blockSize, "#ef4444", "#b91c1c");
  }

  function createOverlay() {
    if (canvas) return;

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

    function resize() {
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
    }
    resize();
    window.addEventListener("resize", resize);

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

    window.addEventListener("mousedown", (e) => {
      if (!isActive || e.button !== 0) return;

      let lastKnownTile = "null";
      let attempts = 0;
      
      const checkInterval = setInterval(() => {
        const game = getGameState();
        if (!game) return;

        const targetPlayer = game.players().find(p => p.name() === setting.targetName);
        if (lastKnownTile === "null") {
          lastKnownTile = targetPlayer.state.spawnTile;
          return;
        }
        
        if (lastKnownTile !== targetPlayer.state.spawnTile) {
          clearInterval(checkInterval);
          const mapWidth = game._map.width_;
          const baseDistance = 8 + spawnGap;
          spawnCircleCount = JSON.parse(GM_getValue("bots") || "[]").length || 4;

          const positions = Array.from({ length: spawnCircleCount }, (_, i) => {
            const angle = getAngle(i);
            return targetPlayer.state.spawnTile
              + (Math.round(Math.sin(angle) * baseDistance) * mapWidth)
              + Math.round(Math.cos(angle) * baseDistance);
          });

          GM_setValue(SPAWN_POSITIONS_KEY, `${JSON.stringify(positions)}:${Date.now()}`);
          return;
        }

        if (attempts >= 12) {
          clearInterval(checkInterval);
        }
      }, 500);
    });

    window.addEventListener("keydown", (e) => {
      if (!isActive) return;
      switch (e.key.toLowerCase()) {
        case "r": spawnRotation = (spawnRotation + 5) % 360; break;
        case "y": spawnGap = Math.min(spawnGap + 1, 50); break;
        case "t": spawnGap = Math.max(spawnGap - 1, 0); break;
        case "u": sideMode = !sideMode; break;
      }
    });
  }

  function activate() {
    if (isActive) return;
    isActive = true;
    createOverlay();
    rafId = requestAnimationFrame(draw);
  }

  function deactivate() {
    if (!isActive) return;
    isActive = false;
    cancelAnimationFrame(rafId);
    rafId = null;
    ctx?.clearRect(0, 0, canvas.width, canvas.height);
  }

  return { activate, deactivate };
}

function register(id) {
  GM_setValue(`bot_${id}`, Date.now());
}

function setGameId(id) {
  let ID = id;
  if (!ID) ID = "null";

  GM_setValue("game-id", `${ID}:${Date.now()}`);
}

let lastJoin = 0;
let lastSpawn = 0;
let targetPlayerSpawnTile = null;

const overlay = spawnOverlay();
let meIsTarget = false;
let lastDonate = 0;
let lastKnownGold = 0;  
let builtFactories = 0;
let factory_id = null;
let registered = false;

function tick() {
  refreshUI();

  const cfg = setting;
  if (!cfg) return;

  const game = getGameState();
  if (game && game.myPlayer && game.myPlayer()) {
    document.querySelector(".dot").classList.add("green");
    document.getElementById("status-text").textContent = "Connected";
  } else {
    document.querySelector(".dot").classList.remove("green");
    document.getElementById("status-text").textContent = "Disconnected";

    if (cfg.autoJoinLeave && !meIsTarget && document.querySelectorAll("username-input input")[1].value === cfg.targetName) {
      setGameId(null);

      document.addEventListener("join-lobby", (e) => {
        const gameID = e.detail.gameID;
        if (gameID && e.detail.source != "singleplayer") {
          setGameId(gameID);
        }
      })
      document.addEventListener("leave-lobby", (e) => {
        setGameId(null);
      })
      meIsTarget = true;
    } else if (cfg.autoJoinLeave && !meIsTarget) {
      const [gameID, timeRaw] = GM_getValue("game-id").split(":");
      const timeStamp = parseInt(timeRaw);
      const now = Date.now();

      if (now - lastJoin > 4000 && now - timeStamp <= 4000) {
        if (gameID != "null") {
          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; // Return if no game exist
  }
  if (game.isCatchingUp() || game.config().isReplay()) return;
  
  // Game config could be changing
  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();
  
  const targetPlayer = game.players().find((p) => p.name() === cfg.targetName) || null;
  if (!targetPlayer || !targetPlayer.isPlayer || !targetPlayer.isPlayer()) return; // Seperated isAlive() because it is false in spawn phase
  
  const myPlayer = game.myPlayer();
  if (!myPlayer) return;
  const mySmallID = myPlayer.smallID()

  if (targetPlayer.id() === myPlayer.id()) {
    if (game.inSpawnPhase() && cfg.autoSpawn && !randomSpawn) {
      overlay.activate();
      
      const bots = [];
      const now = Date.now();
      GM_listValues().forEach((key) => {
        if (key.startsWith("bot_")) {
          const value = GM_getValue(key);

          if (now - parseInt(value) <= 30000) {
            bots.push(key.slice(4));
          } else {
            GM_deleteValue(key);
          }
        }
      })
      GM_setValue("bots", JSON.stringify(bots));
    }
    else {
      overlay.deactivate();
    }
    return; // Return if myPlayer = targetPlayer
  }

  if (!registered && game.inSpawnPhase()) {
    register(myPlayer.id());
    registered = true;
  }

  // Check if the targetPlayer has left the game
  const [id, time] = GM_getValue("game-id").split(":");
  if (id === "null" && Date.now() - parseInt(time) <= 6000) {
    window.location.href = 'https://openfront.io/';
  }
  
  if (!targetPlayer.isAlive || !targetPlayer.isAlive()) return; // Is alive i was talking about
  
  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);
        
        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(myPlayer.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 game and body after spawn phase ended
  const canvasEle = document.querySelector("canvas");
  if (canvasEle && canvasEle.style.display != "none") {
    canvasEle.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() });
      } else if (targetPlayer.state.alliances.find((i) => i.other ? i.other === myPlayer.id() : false)?.hasExtensionRequest) {
        sendPacket({type: "allianceExtension", recipient: targetPlayer.id()});
      }
    }
  }

  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;
        }
      }
    }
  }

  // Get number of factories built
  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) {
      // Upgrade if a factory already exist
      sendPacket({
        type:"upgrade_structure",
        unit: "Factory",
        unitId: factory_id
      })
    } else {
      sendPacket({
        type: "build_unit",
        unit: "Factory",
        tile: myPlayer.state.spawnTile,
      })
    }
  }
}

renderUI();
setInterval(tick, 2000);