Greasy Fork is available in English.
Cheat GUI, teaming up bot, control with emoji, auto spawn, auto factory, auto give troops and gold, auto alliance.
// ==UserScript==
// @name Openfront Multitab cheat
// @namespace Openfront Multitab cheat
// @version 1.2.1
// @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==
let style = `
body {
background-color: black;
padding: 10px;
}
#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: 0.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: 0.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: none;
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: 0.4s;
border-radius: 34px;
}
.slider::before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 2px;
bottom: 2px;
background-color: var(--text-dim);
transition: 0.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: transparent;
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: transparent;
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: none;
}
.save-note {
font-family: var(--mono);
font-size: 12px;
color: var(--text-dim);
transition: opacity 0.3s ease;
}
.reset-btn {
background: transparent;
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: 0.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: 0.06em;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.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 class="text-field" id="targetName">
</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 type="checkbox" id="autoAlliance">
<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 type="checkbox" id="autoTroops">
<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 type="checkbox" id="autoGold">
<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 type="checkbox" id="autoFactory">
<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 inputmode="numeric" id="maxFactories">
<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 styleElement = document.createElement("style");
styleElement.textContent = style;
document.head.appendChild(styleElement);
document.body.insertAdjacentHTML("beforeend", panel);
let toggled = true;
let isDragging = false; // ← New flag
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 setting = null;
let uiState = null;
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() {
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.type === "number" ?
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);
element.addEventListener("change", saveSetting);
} else {
element.value = value;
element.addEventListener("input", saveSetting);
}
}
}
// Draggable Panel - Improved
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(); // Save position after drag
}
}
// Stepper
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();
});
}
// Header Click (Toggle) - Now respects dragging
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(); // Save collapsed state
});
// Reset Button
document.getElementById("reset-btn").addEventListener("click", () => {
if (confirm("Reset all settings to default?")) {
setting = {...default_setting};
GM_setValue(storage_key, JSON.stringify(setting));
loadSetting();
}
});
// Initialize
setInterval(loadSetting, 2000);
loadUIState();
makeDraggable();
setupSteppers();
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)];
}
const statusElement = document.querySelector(".dot")
const statusText = document.getElementById("status-text")
function tick() {
const cfg = getSettings(); // ← Get latest settings
if (!cfg.targetName) return;
const state = getGameState();
if (state && state.game && state.myPlayer) {
statusElement.classList.add("green");
statusText.textContent = "Connected";
} else {
statusElement.classList.remove("green");
statusText.textContent = "Disconnected";
}
if (!state || !state.game || !state.myPlayer) return;
const game = state.game;
const myPlayer = state.myPlayer;
if (game.isCatchingUp()) return;
const targetPlayer = game.players().find((p) => p.name() === cfg.targetName);
if (targetPlayer.id() === myPlayer.id()) return;
if (!targetPlayer || !targetPlayer.isPlayer || !targetPlayer.isPlayer() || !targetPlayer.isAlive()) return;
const mySmallID = game.players().indexOf(myPlayer) + 1
let donateTroopsEnable = game._config._gameConfig.donateTroops;
let donateGoldEnable = game._config._gameConfig.donateGold;
if (game.inSpawnPhase() && 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;
}
if (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;
}
}
}
}
const buildCost =
builtFactories === 0 ? 125000 :
builtFactories === 1 ? 250000 :
builtFactories === 2 ? 500000 :
builtFactories >= 3 ? 1000000 :
0;
if (cfg.autoFactory && builtFactories < cfg.maxFactories && myPlayer.gold() >= buildCost) {
console.log("Building factory");
if (builtFactories >= 1) {
if (!factory_id) {
for (const [key, value] of state.game.unitStates()) {
if (value.ownerID == mySmallID && value.unitType == "Factory") {
factory_id = value.id;
break;
};
}
}
if (sendPacket({
type:"upgrade_structure",
unit: "Factory",
unitId: factory_id
})) {
builtFactories += 1;
}
} else {
if (sendPacket({
type: "build_unit",
unit: "Factory",
tile: myPlayer.state.spawnTile,
})) {
builtFactories += 1;
}
}
}
}
setInterval(tick, 2000);