Openfront Multitab cheat

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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);