Openfront Multitab cheat

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Openfront Multitab cheat
// @namespace    Openfront Multitab cheat
// @version      1.2.2
// @description  Cheat GUI, teaming up bot, control with emoji, auto spawn, 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
// @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: "",
  autoAlliance: 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 alliance</span> <span class=row-hint>Accept alliance only from you</span></div><label class=switch><input id=autoAlliance 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) {
    panel.style.left = uiState.x + "px";
    panel.style.top = uiState.y + "px";
    toggled = !uiState.collapsed;

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

function saveSetting() {
  if (!setting) return;

  document.getElementById("save-note").textContent = "Saving...";
  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;
    }
  }
  GM_setValue(storage_key, JSON.stringify(setting));
  document.getElementById("save-note").textContent = "Saved!";
}

function loadSetting() {
  const saved = GM_getValue(storage_key);
  setting = saved ? JSON.parse(saved) : {...default_setting};

  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;

    panelEl.style.top = (panelEl.offsetTop - pos2) + "px";
    panelEl.style.left = (panelEl.offsetLeft - pos1) + "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 ? "block" : "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 || "",
    autoAlliance: setting.autoAlliance,
    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) {
        console.log(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) {
            console.log(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) {
            console.log(e)
          }
        }
        if (msg.type === "join") {
          myUsername = msg.username;
        }
      }
    } catch (e) {
      console.log(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) {
    console.log(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) {
        console.log(e)
      }
    }
    NativeSend.call(sock, JSON.stringify({ type: "intent", intent: intentObj }));
    return true;
  } catch (e) {
    console.log(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) {
    console.log(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) {
    console.log(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 randomPointOnCircleInt(position) {
  const angle = Math.random() * Math.PI * 2;
  const radius = getRandomInt(12, 24)
  const x = Math.round(position[0] + radius * Math.cos(angle));
  const y = Math.round(position[1] + radius * Math.sin(angle));

  return Number(`${x}${y}`)
}

function sliceIntegerInHalf(num) {
  const numStr = num.toString();
  const mid = Math.ceil(numStr.length / 2);
  const firstHalf = numStr.slice(0, mid);
  const secondHalf = numStr.slice(mid);

  return [Number(firstHalf), Number(secondHalf)];
}

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";
    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()) return;
  if (!targetPlayer || !targetPlayer.isPlayer || !targetPlayer.isPlayer() || !targetPlayer.isAlive()) return;

  const mySmallID = myPlayer.smallID()

  if (game.inSpawnPhase()) {
    if (!randomSpawn && targetPlayer.state.spawnTile && targetPlayer.state.spawnTile != targetPlayerSpawnTile) {
      targetPlayerSpawnTile = targetPlayer.state.spawnTile
      console.log("Spawn tile: ", targetPlayerSpawnTile)

      const randomTile = randomPointOnCircleInt(sliceIntegerInHalf(targetPlayerSpawnTile));
      console.log("Random tile: ", randomTile)

      sendPacket({
        type: "spawn",
        tile: randomTile
      })
    }
    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 && cfg.autoAlliance) {
    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) {
    console.log("Building factory");

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