Cheat GUI, teaming up bot, control with emoji, spawn overlay, auto spawn, auto factory, auto give troops and gold, auto alliance.
// ==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);