Greasy Fork is available in English.
Modular OpenFront advisor toolkit with spawn, threat, expansion, alliance, and optional assist features.
// ==UserScript==
// @name OpenFront Tactical Assistant
// @namespace https://github.com/local/openfront-script
// @version 0.8.11
// @description Modular OpenFront advisor toolkit with spawn, threat, expansion, alliance, and optional assist features.
// @license MIT
// @match https://openfront.io/*
// @match https://beta.openfront.io/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant unsafeWindow
// @run-at document-start
// @inject-into auto
// ==/UserScript==
(() => {
// ofat:virtual:page-bundle
var PAGE_BUNDLE_SOURCE = `(() => {
// src/meta.js
var APP = Object.freeze({
name: "OpenFront Tactical Assistant",
shortName: "OF Tactical",
version: "0.8.11",
modified: "2026-06-08",
storageKey: "ofat.settings.v1",
pagePayloadKey: "__OFAT_PAGE_PAYLOAD__",
messageSource: "openfront-tactical-assistant"
});
var USERSCRIPT_META = Object.freeze({
name: [APP.name],
namespace: ["https://github.com/local/openfront-script"],
version: [APP.version],
description: ["Modular OpenFront advisor toolkit with spawn, threat, expansion, alliance, and optional assist features."],
license: ["MIT"],
match: ["https://openfront.io/*", "https://beta.openfront.io/*"],
grant: ["GM_setValue", "GM_getValue", "GM_registerMenuCommand", "unsafeWindow"],
"run-at": ["document-start"],
"inject-into": ["auto"]
});
var DEFAULT_SETTINGS = Object.freeze({
settingsSchemaVersion: 13,
showAdvisorPanel: true,
showHeatmap: true,
hideAds: true,
autopilot: false,
autoSpawn: false,
autoAlliance: false,
autoFarm: false,
autoFarmHumanTargets: true,
autoEco: false,
autoDefense: false,
autoDefenseCounterAttack: true,
autoDefenseBuildPosts: true,
autoDefenseBuildSam: true,
autoDefenseIncomingRatio: 0.35,
autoBoat: false,
autoWeapons: false,
autoWeaponsEarlyNuke: false,
autoTeamSupport: true,
autoTeamSupportGold: false,
autoSosQuickChat: true,
smartAttack: false,
showAttackBadges: true,
showRatioBar: true,
showTroopRatios: true,
showWeaknessIndicator: true,
showDangerIndicator: true,
showThreatIcons: true,
showTroopEconomy: true,
roundLogging: true,
roundLogAutoDownload: false,
roundLogSnapshotIntervalMs: 5e3,
roundLogKeepPlayerNames: true,
networkLogging: true,
autoSpawnDelayMs: 2e3,
autoFarmWindowMs: 24e4,
autoFarmReserveRatio: 0.55,
autoFarmCooldownMs: 2500,
autoFarmDynamicReserve: true,
autoFarmBaseReserveRatio: 0.3,
autoFarmMinReserveRatio: 0.2,
autoFarmMaxReserveRatio: 0.65,
autoExpand: true
});
// src/shared/event-bus.js
function createEventBus() {
const listeners = /* @__PURE__ */ new Map();
return {
on(type, listener) {
if (!listeners.has(type)) listeners.set(type, /* @__PURE__ */ new Set());
listeners.get(type).add(listener);
return () => listeners.get(type)?.delete(listener);
},
emit(type, payload) {
const set = listeners.get(type);
if (!set) return;
set.forEach((listener) => {
try {
listener(payload);
} catch (error) {
console.error("[OF Tactical] Event listener failed", type, error);
}
});
}
};
}
// src/shared/logger.js
function createLogger(app) {
const prefix = \`[\${app.shortName}]\`;
return {
info(message, ...args) {
console.log(\`\${prefix} \${message}\`, ...args);
},
warn(message, ...args) {
console.warn(\`\${prefix} \${message}\`, ...args);
},
error(message, ...args) {
console.error(\`\${prefix} \${message}\`, ...args);
},
banner() {
console.log(\`%c\${prefix} v\${app.version} loaded\`, "color:#4fc3f7;font-weight:bold");
}
};
}
// src/settings/settings-store.js
function createSettingsStore(defaults, initialValues = {}) {
const values = Object.assign({}, defaults, initialValues);
const listeners = /* @__PURE__ */ new Set();
function has(key) {
return Object.prototype.hasOwnProperty.call(defaults, key);
}
return {
values,
has,
get(key) {
return has(key) ? values[key] : void 0;
},
set(key, value, meta = {}) {
if (!has(key)) return false;
if (values[key] === value) return true;
values[key] = value;
listeners.forEach((listener) => listener({ key, value, meta }));
return true;
},
update(nextValues, meta = {}) {
Object.keys(nextValues || {}).forEach((key) => this.set(key, nextValues[key], meta));
},
onChange(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
snapshot() {
return Object.assign({}, values);
}
};
}
// src/page/page-settings.js
function createPageSettingsStore(app, defaults, initialSettings) {
const store = createSettingsStore(defaults, loadLocalSettings(app, initialSettings));
store.onChange(({ key, value, meta }) => {
saveLocalSettings(app, store.snapshot());
if (!meta.skipUserscriptSync) {
window.postMessage({ source: app.messageSource, type: "setting-changed", key, value }, "*");
}
});
window.addEventListener("message", (event) => {
const data = event && event.data;
if (!data || data.source !== app.messageSource || data.type !== "set-setting") return;
store.set(data.key, data.value, { skipUserscriptSync: true });
});
return store;
}
function loadLocalSettings(app, initialSettings) {
try {
const raw = localStorage.getItem(app.storageKey);
if (!raw) return initialSettings;
const settings = Object.assign({}, initialSettings, JSON.parse(raw));
return applyLocalSettingsMigrations(app, initialSettings, settings);
} catch (_) {
return initialSettings;
}
}
function applyLocalSettingsMigrations(app, initialSettings, settings) {
const targetSchema = Number(initialSettings.settingsSchemaVersion) || 0;
const currentSchema = Number(settings.settingsSchemaVersion) || 0;
if (!targetSchema || currentSchema >= targetSchema) return settings;
const migrated = Object.assign({}, settings, {
roundLogAutoDownload: false,
settingsSchemaVersion: targetSchema
});
saveLocalSettings(app, migrated);
return migrated;
}
function saveLocalSettings(app, settings) {
try {
localStorage.setItem(app.storageKey, JSON.stringify(settings));
} catch (_) {
}
}
// src/shared/dom.js
function appendWhenReady(node, parentSelector = "head") {
const target = parentSelector === "body" ? document.body : document.head || document.documentElement;
if (target) {
target.appendChild(node);
return;
}
document.addEventListener(
"DOMContentLoaded",
() => {
const parent = parentSelector === "body" ? document.body : document.head;
parent.appendChild(node);
},
{ once: true }
);
}
function removeElementById(id) {
const el = document.getElementById(id);
if (el) el.remove();
}
function createStyle(id, css) {
const existing = document.getElementById(id);
if (existing) return existing;
const style = document.createElement("style");
style.id = id;
style.textContent = css;
appendWhenReady(style);
return style;
}
function makeDraggable(el, options = {}) {
const buttonSelector = options.ignoreSelector || "button";
let offsetX = 0;
let offsetY = 0;
let dragging = false;
el.addEventListener("mousedown", (event) => {
if (event.target && event.target.closest && event.target.closest(buttonSelector)) return;
dragging = true;
const rect = el.getBoundingClientRect();
offsetX = event.clientX - rect.left;
offsetY = event.clientY - rect.top;
});
document.addEventListener("mousemove", (event) => {
if (!dragging) return;
el.style.left = \`\${Math.max(0, event.clientX - offsetX)}px\`;
el.style.top = \`\${Math.max(0, event.clientY - offsetY)}px\`;
el.style.right = "auto";
});
document.addEventListener("mouseup", () => {
dragging = false;
});
}
// src/page/hide-ads.js
var AD_DOMAINS = [
"googlesyndication.com",
"doubleclick.net",
"doubleverify.com",
"googleadservices.com",
"googletag",
"adservice.google",
"pagead2.googlesyndication",
"tpc.googlesyndication",
"securepubads.g.doubleclick",
"pubads.g.doubleclick",
"vpaid.doubleverify",
"vtrk.dv.tech",
"innovid.com",
"ads.pubmatic.com",
"secure.adnxs.com",
"ib.adnxs.com",
"rubiconproject.com",
"openx.net",
"advertising.com",
"amazon-adsystem.com",
"adsafeprotected.com",
"moatads.com",
"chartboost.com",
"criteo.com",
"bidswitch.net",
"playwire.com",
"intergient.com"
];
var AD_SELECTORS = [
"#in-game-bottom-left-ad",
"in-game-promo",
"[id*='standard_iab']",
"[class*='bottom_rail']",
"[class*='bottom-rail']",
".pw-oop",
".pw-tag",
"[id^='pw-']",
"[data-pw-desk]",
"[data-pw-mobi]",
"iframe[src*='doubleclick']",
"iframe[src*='googlesyndication']",
"iframe[src*='doubleverify']",
"iframe[src*='googleadservices']",
"iframe[src*='adsafeprotected']",
"iframe[src*='amazon-adsystem']",
"iframe[src*='innovid']",
"iframe[src*='adnxs']",
"iframe[src*='rubiconproject']",
"iframe[src*='openx']",
"iframe[src*='criteo']",
"iframe[src*='bidswitch']",
"iframe[src*='moatads']",
"iframe[title='Advertisement']",
"iframe[title='advertisement']",
"ins.adsbygoogle",
"[id*='google_ads']",
"[id*='div-gpt-ad']",
"[data-ad]"
];
var installed = false;
function hideAds() {
if (installed) return;
installed = true;
neutralizeRamp();
installNetworkBlocking();
installDomBlocking();
}
function neutralizeRamp() {
try {
window.adsEnabled = false;
window.ramp = {
que: [],
spaAddAds() {
},
spaAds() {
},
destroyUnits() {
}
};
} catch (_) {
}
createStyle(
"ofat-ad-block-style",
\`\${AD_SELECTORS.join(",")}{display:none!important;visibility:hidden!important;height:0!important;width:0!important;pointer-events:none!important;}\`
);
}
function isAdUrl(url) {
if (!url) return false;
try {
const str = String(url).toLowerCase();
return AD_DOMAINS.some((domain) => str.includes(domain));
} catch (_) {
return false;
}
}
function installNetworkBlocking() {
try {
const origFetch = window.fetch;
if (typeof origFetch === "function" && !origFetch.__ofatAdBlock) {
const patched = function patchedFetch(input, init) {
const url = input && typeof input === "object" ? input.url : input;
if (isAdUrl(url)) return Promise.reject(new TypeError("blocked ad request"));
return origFetch.apply(this, arguments);
};
patched.__ofatAdBlock = true;
window.fetch = patched;
}
} catch (_) {
}
try {
const origOpen = XMLHttpRequest.prototype.open;
const origSend = XMLHttpRequest.prototype.send;
if (origOpen && !origOpen.__ofatAdBlock) {
const patchedOpen = function patchedXhrOpen(method, url) {
if (isAdUrl(url)) this.__ofatAdBlocked = true;
return origOpen.apply(this, arguments);
};
patchedOpen.__ofatAdBlock = true;
XMLHttpRequest.prototype.open = patchedOpen;
XMLHttpRequest.prototype.send = function patchedXhrSend() {
if (this.__ofatAdBlocked) return void 0;
return origSend.apply(this, arguments);
};
}
} catch (_) {
}
}
function installDomBlocking() {
const nuke = (root) => {
if (!root || typeof root.querySelectorAll !== "function") return;
AD_SELECTORS.forEach((selector) => {
try {
root.querySelectorAll(selector).forEach((el) => el.remove());
} catch (_) {
}
});
};
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== 1) continue;
const src = String(node.src || node.href || "").toLowerCase();
if (isAdUrl(src) || node.tagName === "IFRAME" && /advert/i.test(node.title || "")) {
try {
node.remove();
} catch (_) {
}
continue;
}
nuke(node);
}
}
});
const start = () => {
nuke(document);
try {
observer.observe(document.documentElement || document.body, { childList: true, subtree: true });
} catch (_) {
}
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", start, { once: true });
} else {
start();
}
}
// src/page/openfront/map-assets.js
function installMapAssetHook({ bus, logger }) {
const origFetch = window.fetch;
if (typeof origFetch !== "function") return;
let pendingManifest = null;
let pendingTerrainBin = null;
let pendingTerrainIs4x = null;
let mapLoaded = false;
window.fetch = function patchedFetch() {
const args = arguments;
const url = typeof args[0] === "string" ? args[0] : args[0] && args[0].url || "";
if (/\\/maps\\//.test(url) && /\\/manifest(\\.[a-f0-9]+)?\\.json(\\?|$)/.test(url)) {
return origFetch.apply(this, args).then((response) => {
response.clone().json().then((manifest) => {
pendingManifest = manifest;
tryAssembleMap();
}).catch(() => {
});
return response;
});
}
if (/\\/maps\\//.test(url) && /\\/map(4x)?(\\.[a-f0-9]+)?\\.bin(\\?|$)/.test(url)) {
const is4x = /\\/map4x(\\.[a-f0-9]+)?\\.bin/.test(url);
return origFetch.apply(this, args).then((response) => {
response.clone().arrayBuffer().then((buffer) => {
pendingTerrainBin = new Uint8Array(buffer);
pendingTerrainIs4x = is4x;
tryAssembleMap();
}).catch(() => {
});
return response;
});
}
return origFetch.apply(this, args);
};
function tryAssembleMap() {
if (!pendingManifest || !pendingTerrainBin || mapLoaded) return;
const metaKey = pendingTerrainIs4x ? "map4x" : "map";
const meta = pendingManifest[metaKey];
if (!meta) return;
const width = meta.width;
const height = meta.height;
const terrain = pendingTerrainBin;
if (terrain.length !== width * height) return;
let nations = pendingManifest.nations || [];
if (pendingTerrainIs4x) {
nations = nations.map((nation) => ({
name: nation.name,
flag: nation.flag,
coordinates: [Math.floor(nation.coordinates[0] / 2), Math.floor(nation.coordinates[1] / 2)]
}));
}
mapLoaded = true;
logger.info(\`Map \${width}x\${height}, \${nations.length} nations\`);
bus.emit("mapLoaded", { terrain, width, height, nations });
}
}
// src/page/openfront/network-hooks.js
function installNetworkHooks({ bus, getSmartAttackModifier, logger }) {
const OrigWS = window.WebSocket;
const WorkerCtor = window.Worker;
if (!OrigWS || !OrigWS.prototype) return { OrigWS: null, origWsSend: null };
const origWsSend = OrigWS.prototype.send;
installWebSocketHooks(OrigWS, origWsSend, bus, getSmartAttackModifier, logger);
installWorkerHooks(WorkerCtor, bus);
return { OrigWS, origWsSend };
}
function installWebSocketHooks(OrigWS, origWsSend, bus, getSmartAttackModifier, logger) {
const origWsAddEL = OrigWS.prototype.addEventListener;
let wsMessageCount = 0;
function interceptWsMessage(data) {
try {
const message = typeof data === "string" ? JSON.parse(data) : null;
if (!message || !message.type) return;
wsMessageCount += 1;
if (wsMessageCount === 1) logger.info(\`First websocket message: \${message.type}\`);
bus.emit("wsMessage", message);
} catch (_) {
}
}
window.WebSocket = function PatchedWebSocket() {
const args = Array.prototype.slice.call(arguments);
const ws = new (Function.prototype.bind.apply(OrigWS, [null].concat(args)))();
const wsUrl = String(args[0] || "");
origWsAddEL.call(ws, "message", (event) => interceptWsMessage(event.data));
if (wsUrl.indexOf("/lobbies") === -1 && wsUrl.indexOf("/matchmaking") === -1) {
bus.emit("socketReady", ws);
const sendForThisSocket = ws.send.bind(ws);
ws.send = function patchedSend(data) {
const modifier = getSmartAttackModifier();
return sendForThisSocket(modifier ? modifier(data) : data);
};
ws.addEventListener("close", () => bus.emit("socketClosed", ws));
}
return ws;
};
window.WebSocket.prototype = OrigWS.prototype;
window.WebSocket.CONNECTING = OrigWS.CONNECTING;
window.WebSocket.OPEN = OrigWS.OPEN;
window.WebSocket.CLOSING = OrigWS.CLOSING;
window.WebSocket.CLOSED = OrigWS.CLOSED;
}
function installWorkerHooks(WorkerCtor, bus) {
if (!WorkerCtor || !WorkerCtor.prototype) return;
const origWorkerAddEL = WorkerCtor.prototype.addEventListener;
const origWorkerOnmsgDesc = Object.getOwnPropertyDescriptor(WorkerCtor.prototype, "onmessage");
function interceptWorkerMessage(data) {
if (!data || data.type !== "game_update_batch" || !data.gameUpdates) return;
bus.emit("workerGameUpdateBatch", data);
}
WorkerCtor.prototype.addEventListener = function patchedWorkerAddEventListener(type, listener) {
if (type === "message" && typeof listener === "function") {
const wrapped = function wrappedWorkerMessage(event) {
interceptWorkerMessage(event.data);
return listener.apply(this, arguments);
};
return origWorkerAddEL.call(this, type, wrapped);
}
return origWorkerAddEL.apply(this, arguments);
};
if (origWorkerOnmsgDesc && origWorkerOnmsgDesc.set) {
Object.defineProperty(WorkerCtor.prototype, "onmessage", {
get: origWorkerOnmsgDesc.get,
set(fn) {
if (typeof fn !== "function") {
origWorkerOnmsgDesc.set.call(this, fn);
return;
}
const wrapped = function wrappedWorkerOnMessage(event) {
interceptWorkerMessage(event.data);
return fn.apply(this, arguments);
};
origWorkerOnmsgDesc.set.call(this, wrapped);
},
configurable: true,
enumerable: true
});
}
}
// src/page/openfront/game-state.js
var GUT_PLAYER = 2;
function createGameState() {
const state = {
myClientID: null,
myPlayerID: null,
mySmallID: null,
currentTick: 0,
currentTurn: 0,
gameStarted: false,
roundStartedAtMs: 0,
gameSocket: null,
playerSpawns: /* @__PURE__ */ new Map(),
playerStates: /* @__PURE__ */ new Map()
};
return {
state,
handleWsMessage(message, mapData) {
if (message.type === "start") {
if (message.myClientID) state.myClientID = message.myClientID;
state.gameStarted = true;
state.roundStartedAtMs = performance.now();
state.currentTurn = 0;
if (message.turns) message.turns.forEach((turn) => handleTurn(state, turn, mapData));
} else if (message.type === "turn" && message.turn) {
handleTurn(state, message.turn, mapData);
} else if (message.type === "lobby_info" && message.myClientID) {
state.myClientID = message.myClientID;
}
},
handleWorkerBatch(batch) {
for (let gi = 0; gi < batch.gameUpdates.length; gi += 1) {
const gameUpdate = batch.gameUpdates[gi];
state.currentTick = gameUpdate.tick || state.currentTick;
if (!gameUpdate.updates) continue;
const playerUpdates = gameUpdate.updates[GUT_PLAYER];
if (!playerUpdates) continue;
for (let pi = 0; pi < playerUpdates.length; pi += 1) {
const update = playerUpdates[pi];
if (!update.id) continue;
const previous = state.playerStates.get(update.id) || { id: update.id };
const merged = Object.assign({}, previous, update);
state.playerStates.set(update.id, merged);
if (merged.clientID && merged.clientID === state.myClientID) {
state.myPlayerID = merged.id;
state.mySmallID = merged.smallID;
}
}
}
},
resetSocket(socket) {
if (state.gameSocket !== socket) return;
state.gameStarted = false;
state.gameSocket = null;
state.myPlayerID = null;
state.mySmallID = null;
state.currentTick = 0;
state.currentTurn = 0;
state.roundStartedAtMs = 0;
state.playerStates.clear();
},
setSocket(socket) {
state.gameSocket = socket;
},
getMyState() {
return state.myPlayerID ? state.playerStates.get(state.myPlayerID) : null;
}
};
}
function handleTurn(state, turn, mapData) {
const turnNumber = Number(turn?.turnNumber ?? turn?.turn ?? turn?.number ?? turn?.id);
if (Number.isFinite(turnNumber)) state.currentTurn = Math.max(state.currentTurn, turnNumber);
else state.currentTurn += 1;
extractSpawnIntents(state, turn, mapData);
}
function extractSpawnIntents(state, turn, mapData) {
if (!turn || !turn.intents || !mapData || !mapData.width) return;
for (let i = 0; i < turn.intents.length; i += 1) {
const intent = turn.intents[i];
if (intent.type !== "spawn" || !intent.clientID || intent.clientID === state.myClientID) continue;
const x = intent.tile % mapData.width;
const y = Math.floor(intent.tile / mapData.width);
state.playerSpawns.set(intent.clientID, { x, y });
}
}
// src/page/openfront/game-view-adapter.js
var GAME_VIEW_SELECTORS = ["leader-board", "player-info-overlay", "control-panel", "spawn-timer"];
var GAME_VIEW_PROPS = ["game", "g"];
var cachedGameView = null;
var cachedGameViewSource = null;
var capturedGameView = null;
var captureInstalled = false;
function installGameViewCapture() {
if (captureInstalled) return;
captureInstalled = true;
try {
const HtmlProto = typeof HTMLElement !== "undefined" ? HTMLElement.prototype : null;
GAME_VIEW_PROPS.forEach((prop) => patchGameProperty(HtmlProto, prop));
if (window.customElements && typeof window.customElements.define === "function" && !window.customElements.__ofatGameCapture) {
const nativeDefine = window.customElements.define.bind(window.customElements);
window.customElements.define = function patchedDefine(name, ctor, options) {
try {
if (ctor && ctor.prototype) GAME_VIEW_PROPS.forEach((prop) => patchGameProperty(ctor.prototype, prop));
} catch (_) {
}
return nativeDefine(name, ctor, options);
};
Object.defineProperty(window.customElements, "__ofatGameCapture", { configurable: true, value: true });
}
} catch (_) {
}
}
function patchGameProperty(proto, prop) {
const patchedKey = \`__ofat_\${prop}_patched\`;
if (!proto || Object.prototype.hasOwnProperty.call(proto, patchedKey)) return;
let desc = null;
let cursor = proto;
while (cursor && cursor !== Object.prototype) {
desc = Object.getOwnPropertyDescriptor(cursor, prop);
if (desc) break;
cursor = Object.getPrototypeOf(cursor);
}
const storageKey = Symbol(\`ofat_\${prop}\`);
try {
Object.defineProperty(proto, prop, {
configurable: true,
get() {
if (desc && typeof desc.get === "function") return desc.get.call(this);
return this[storageKey];
},
set(value) {
if (looksLikeGameView(value)) capturedGameView = value;
if (desc && typeof desc.set === "function") desc.set.call(this, value);
else this[storageKey] = value;
}
});
Object.defineProperty(proto, patchedKey, { configurable: true, value: true });
} catch (_) {
}
}
function looksLikeGameView(value) {
return !!value && typeof value === "object" && typeof value.myPlayer === "function" && (typeof value.units === "function" || typeof value.unitStates === "function" || typeof value.players === "function" || typeof value.playerViews === "function");
}
function getGameView() {
if (isValidGameView(cachedGameView)) return cachedGameView;
cachedGameView = null;
cachedGameViewSource = null;
if (isValidGameView(capturedGameView)) {
cachedGameView = capturedGameView;
cachedGameViewSource = "captured";
return capturedGameView;
}
try {
for (let i = 0; i < GAME_VIEW_SELECTORS.length; i += 1) {
const selector = GAME_VIEW_SELECTORS[i];
const element = document.querySelector(selector);
const candidate = element && element.game;
if (!isValidGameView(candidate)) continue;
cachedGameView = candidate;
cachedGameViewSource = selector;
return candidate;
}
} catch (_) {
}
return null;
}
function getGameViewDiscovery() {
const gameView = getGameView();
if (!gameView) return null;
return {
source: cachedGameViewSource || "unknown",
hasConfig: typeof gameView.config === "function",
hasMyPlayer: typeof gameView.myPlayer === "function",
hasPlayerViews: typeof gameView.playerViews === "function",
hasOwner: typeof gameView.owner === "function",
hasNeighbors: typeof gameView.neighbors === "function"
};
}
function resetGameViewCache() {
cachedGameView = null;
cachedGameViewSource = null;
capturedGameView = null;
}
function getMyTroopRatio() {
try {
const gameView = getGameView();
if (!gameView || typeof gameView.myPlayer !== "function" || typeof gameView.config !== "function") return null;
const me = gameView.myPlayer();
if (!me || typeof me.troops !== "function") return null;
const config = gameView.config();
if (!config || typeof config.maxTroops !== "function") return null;
const troops = me.troops();
const maxTroops = config.maxTroops(me);
if (!Number.isFinite(troops) || !Number.isFinite(maxTroops) || maxTroops <= 0) return null;
return { troops, maxTroops, ratio: troops / maxTroops };
} catch (_) {
return null;
}
}
function isValidGameView(candidate) {
if (!candidate) return false;
return typeof candidate.myPlayer === "function" && typeof candidate.config === "function" && (typeof candidate.owner === "function" || typeof candidate.playerViews === "function" || typeof candidate.neighbors === "function");
}
function createNeighbourFetcher() {
let inFlight = null;
let cachedNeighbourIDs = null;
let cachedHasOpenFrontier = false;
return {
get cachedNeighbourIDs() {
return cachedNeighbourIDs;
},
// true, wenn an meine Grenze unbeanspruchtes Land / TerraNullius ("Wildnis") grenzt.
get cachedHasOpenFrontier() {
return cachedHasOpenFrontier;
},
refresh() {
if (inFlight) return inFlight;
const gameView = getGameView();
if (!gameView || typeof gameView.myPlayer !== "function") return Promise.resolve(null);
const me = gameView.myPlayer();
if (!me || typeof me.borderTiles !== "function") return Promise.resolve(null);
inFlight = me.borderTiles().then((info) => {
const neighbourIDs = /* @__PURE__ */ new Set();
let hasOpenFrontier = false;
const myID = typeof me.id === "function" ? me.id() : null;
const borders = info && info.borderTiles;
if (borders && typeof borders.forEach === "function") {
borders.forEach((borderTile) => {
const adjacent = gameView.neighbors ? gameView.neighbors(borderTile) : [];
for (let i = 0; i < adjacent.length; i += 1) {
const tile = adjacent[i];
if (gameView.hasOwner && !gameView.hasOwner(tile)) {
hasOpenFrontier = true;
continue;
}
const owner = gameView.owner ? gameView.owner(tile) : null;
if (!owner) {
hasOpenFrontier = true;
continue;
}
const ownerIsPlayer = typeof owner.isPlayer === "function" ? owner.isPlayer() : false;
if (!ownerIsPlayer) {
hasOpenFrontier = true;
continue;
}
const ownerID = typeof owner.id === "function" ? owner.id() : null;
if (!ownerID || ownerID === myID) continue;
neighbourIDs.add(ownerID);
}
});
}
cachedNeighbourIDs = neighbourIDs;
cachedHasOpenFrontier = hasOpenFrontier;
return neighbourIDs;
}).catch(() => null).then((result) => {
inFlight = null;
return result;
});
return inFlight;
}
};
}
// src/page/openfront/openfront-mechanics.js
var TICK_MS = 100;
var GOLD_RESERVE = 25e3;
var BASE_STRUCTURE_COST = 125e3;
var MAX_ECO_STRUCTURE_COST = 1e6;
var UNIT_FALLBACKS = Object.freeze({
City: { durationMs: 2e3 },
Port: { durationMs: 5e3 },
Factory: { durationMs: 2e3 },
"Defense Post": { durationMs: 5e3 },
"Missile Silo": { cost: 1e6, durationMs: 1e4 },
"SAM Launcher": { durationMs: 3e4 }
});
var SPAWN_TURNS_NORMAL = 300;
var SPAWN_TURNS_RANDOM = 150;
var SPAWN_TURNS_SINGLEPLAYER = 100;
function getStructureCost(unit, { gameView = null, player = null, pendingCounts = {}, observedCounts = {} } = {}) {
const runtime = readRuntimeUnitCost(unit, gameView, player);
if (runtime != null) return { cost: runtime, source: "runtime", reserveGold: GOLD_RESERVE };
const cost = fallbackStructureCost(unit, player, pendingCounts, observedCounts);
return { cost, source: "official_fallback", reserveGold: GOLD_RESERVE };
}
function getConstructionDurationMs(unit, { gameView = null } = {}) {
try {
const info = readUnitInfo(unit, gameView);
const raw = info && (info.constructionDuration ?? info.buildTime ?? info.duration);
const value = toFiniteNumber(typeof raw === "function" ? raw() : raw);
if (value != null && value > 0) return { durationMs: Math.max(500, value * TICK_MS), source: "runtime" };
} catch (_) {
}
return { durationMs: UNIT_FALLBACKS[unit]?.durationMs || 5e3, source: "official_fallback" };
}
function getSpawnPhaseInfo({ gameView = null, state = null, teamMode = false } = {}) {
const runtimeTurns = readRuntimeSpawnPhaseTurns(gameView);
const totalTurns = runtimeTurns || fallbackSpawnPhaseTurns(gameView);
const source = runtimeTurns ? "runtime" : "official_fallback";
const currentTurn = Number(state?.currentTurn) || 0;
const elapsedTurns = Math.max(0, currentTurn);
const remainingTurns = Math.max(0, totalTurns - elapsedTurns);
const totalMs = totalTurns * TICK_MS;
const elapsedMs = state?.roundStartedAtMs ? performance.now() - state.roundStartedAtMs : elapsedTurns * TICK_MS;
const waitCapMs = teamMode ? Math.min(1e4, totalMs * 0.4) : 0;
return {
totalTurns,
currentTurn,
elapsedTurns,
remainingTurns,
totalMs,
elapsedMs,
remainingMs: remainingTurns * TICK_MS,
waitCapMs,
source
};
}
function estimateOfficialCaptureCost({ target, effectiveTargetTroops = 0, mapData = null, sampleTiles = [] } = {}) {
const targetTiles = Math.max(1, toFiniteNumber(target?.tilesOwned) || 1);
const density = Math.max(0, effectiveTargetTroops / targetTiles);
const terrain = estimateTerrainCost(mapData, sampleTiles);
const type = String(target?.playerType || "").toUpperCase();
const botModifier = type === "BOT" ? 0.7 : 1;
const defensePostMultiplier = estimateDefensePostMultiplier(target);
const tileCost = Math.ceil((density + terrain.magnitude) * botModifier * defensePostMultiplier);
const captureCostEstimate = Math.ceil(tileCost * targetTiles);
const estimatedCaptureTurns = Math.max(1, Math.ceil(targetTiles / Math.max(1, terrain.speed)));
return {
source: "official_fallback",
captureCostEstimate,
tileCost,
terrainClass: terrain.kind,
terrainMagnitude: terrain.magnitude,
terrainSpeed: terrain.speed,
defensePostMultiplier,
estimatedCaptureTurns
};
}
function toFiniteNumber(value) {
try {
if (typeof value === "bigint") {
const number2 = Number(value);
return Number.isFinite(number2) ? number2 : null;
}
const number = Number(value);
return Number.isFinite(number) ? number : null;
} catch (_) {
return null;
}
}
function countPlayerUnits(player, unit) {
try {
if (player && typeof player.units === "function") {
const units = player.units(unit);
if (Array.isArray(units)) return units.length;
if (units && typeof units.length === "number") return units.length;
}
} catch (_) {
}
return 0;
}
function readRuntimeUnitCost(unit, gameView, player) {
try {
const info = readUnitInfo(unit, gameView);
if (!info) return null;
const raw = typeof info.cost === "function" ? tryCallCost(info.cost, gameView, player) : info.cost;
const value = toFiniteNumber(raw);
return value != null && value > 0 ? Math.ceil(value) : null;
} catch (_) {
return null;
}
}
function tryCallCost(costFn, gameView, player) {
const candidates = [
() => costFn(gameView, player),
() => costFn(player),
() => costFn(gameView),
() => costFn()
];
for (let i = 0; i < candidates.length; i += 1) {
try {
const value = candidates[i]();
if (value != null) return value;
} catch (_) {
}
}
return null;
}
function readUnitInfo(unit, gameView) {
try {
const config = gameView && typeof gameView.config === "function" ? gameView.config() : null;
if (!config) return null;
if (typeof config.unitInfo === "function") return config.unitInfo(unit);
if (typeof config.structureInfo === "function") return config.structureInfo(unit);
} catch (_) {
}
return null;
}
function fallbackStructureCost(unit, player, pendingCounts, observedCounts) {
const pending = pendingCounts || {};
const observed = observedCounts || {};
if (unit === "City") {
const count = Math.max(countPlayerUnits(player, "City"), observed.City || 0) + (pending.City || 0);
return Math.min(MAX_ECO_STRUCTURE_COST, Math.pow(2, count) * BASE_STRUCTURE_COST);
}
if (unit === "Port" || unit === "Factory") {
const actualShared = countPlayerUnits(player, "Port") + countPlayerUnits(player, "Factory");
const observedShared = (observed.Port || 0) + (observed.Factory || 0);
const count = Math.max(actualShared, observedShared) + (pending.Port || 0) + (pending.Factory || 0);
return Math.min(MAX_ECO_STRUCTURE_COST, Math.pow(2, count) * BASE_STRUCTURE_COST);
}
if (unit === "Defense Post") {
const count = Math.max(countPlayerUnits(player, "Defense Post"), observed["Defense Post"] || 0) + (pending["Defense Post"] || 0);
return Math.min(25e4, (count + 1) * 5e4);
}
if (unit === "SAM Launcher") {
const count = Math.max(countPlayerUnits(player, "SAM Launcher"), observed["SAM Launcher"] || 0) + (pending["SAM Launcher"] || 0);
return Math.min(3e6, (count + 1) * 15e5);
}
return UNIT_FALLBACKS[unit]?.cost || BASE_STRUCTURE_COST;
}
function readRuntimeSpawnPhaseTurns(gameView) {
try {
const config = gameView && typeof gameView.config === "function" ? gameView.config() : null;
if (config && typeof config.numSpawnPhaseTurns === "function") {
const value = toFiniteNumber(config.numSpawnPhaseTurns());
if (value != null && value > 0) return Math.round(value);
}
} catch (_) {
}
return null;
}
function fallbackSpawnPhaseTurns(gameView) {
try {
const config = gameView && typeof gameView.config === "function" ? gameView.config() : null;
const gameConfig = config && typeof config.gameConfig === "function" ? config.gameConfig() : null;
const mode = String(gameConfig?.gameMode || "").toLowerCase();
if (mode.includes("single")) return SPAWN_TURNS_SINGLEPLAYER;
if (gameConfig?.randomSpawn || mode.includes("random")) return SPAWN_TURNS_RANDOM;
} catch (_) {
}
return SPAWN_TURNS_NORMAL;
}
function estimateTerrainCost(mapData, sampleTiles) {
const tiles = Array.isArray(sampleTiles) ? sampleTiles.slice(0, 64) : [];
let magnitude = 0;
let count = 0;
tiles.forEach((tile) => {
const byte = mapData?.terrain ? mapData.terrain[Number(tile)] : null;
if (byte == null) return;
const mag = byte & 31;
if (mag < 10) magnitude += 80;
else if (mag < 20) magnitude += 100;
else magnitude += 120;
count += 1;
});
const avg = count > 0 ? magnitude / count : 100;
if (avg <= 85) return { kind: "plains", magnitude: 80, speed: 16.5 };
if (avg <= 105) return { kind: "highland", magnitude: 100, speed: 20 };
return { kind: "mountain", magnitude: 120, speed: 25 };
}
function estimateDefensePostMultiplier(target) {
try {
const count = countPlayerUnits(target, "Defense Post");
return count > 0 ? 1.4 : 1;
} catch (_) {
return 1;
}
}
// src/page/openfront/team-detection.js
function createTeamDetection({ gameState, logger, roundLogger }) {
let isTeamMode = false;
let myTeammateIDs = /* @__PURE__ */ new Set();
let lobby = null;
let lobbyObserver = null;
let lobbyCaptured = false;
let announced = false;
const api = {
get isTeamMode() {
return isTeamMode;
},
get myTeamName() {
return lobby?.myTeamName || (isTeamMode ? "Team" : null);
},
get myTeamColor() {
return lobby?.myTeamColor || null;
},
observeLobby() {
armLobbyObserver();
},
syncFromGameView() {
const gv = getGameView();
const mode = readGameMode(gv);
if (mode != null) isTeamMode = mode === "Team";
else if (lobby) isTeamMode = isTeamMode || lobby.isTeamMode;
if (!isTeamMode) {
if (myTeammateIDs.size) myTeammateIDs = /* @__PURE__ */ new Set();
maybeAnnounce();
return;
}
const next = /* @__PURE__ */ new Set();
try {
const me = gv && typeof gv.myPlayer === "function" ? gv.myPlayer() : null;
const views = gv && typeof gv.playerViews === "function" ? gv.playerViews() : [];
if (me && Array.isArray(views)) {
views.forEach((pv) => {
try {
if (typeof pv.isOnSameTeam === "function" && pv.isOnSameTeam(me)) {
const id = typeof pv.id === "function" ? pv.id() : null;
if (id != null) next.add(id);
}
} catch (_) {
}
});
}
} catch (_) {
}
myTeammateIDs = next;
maybeAnnounce();
},
isMyTeammate(playerID) {
if (!isTeamMode || playerID == null) return false;
if (myTeammateIDs.has(playerID)) return true;
if (myTeammateIDs.size === 0 && lobby && lobby.myTeamName) {
const player = gameState.state.playerStates.get(playerID);
const name = normName(player?.name);
if (name && lobby.playerNameToTeam[name] === lobby.myTeamName) return true;
}
return false;
},
getMyTeamMembers() {
const members = [];
const seenNames = /* @__PURE__ */ new Set();
myTeammateIDs.forEach((id) => {
const player = gameState.state.playerStates.get(id);
const name = normName(player?.name) || String(id);
seenNames.add(name);
members.push({ id, name });
});
if (lobby) {
lobby.myTeamPlayers.forEach((name) => {
if (!seenNames.has(name)) members.push({ id: null, name });
});
}
return members;
},
memberCount() {
if (myTeammateIDs.size) return myTeammateIDs.size;
return lobby ? lobby.myTeamPlayers.length : 0;
},
// clientIDs meiner Teammitglieder. Br\xFCcke f\xFCr Spawn-Daten (per clientID verschl\xFCsselt),
// damit das Spawn-Scoring Team- von Gegner-Spawns trennen kann.
myTeammateClientIDs() {
const clients = /* @__PURE__ */ new Set();
if (!isTeamMode) return clients;
gameState.state.playerStates.forEach((player) => {
if (!player || player.clientID == null) return;
if (api.isMyTeammate(player.id)) clients.add(player.clientID);
});
return clients;
},
teamSummary() {
return {
isTeamMode,
myTeamName: api.myTeamName,
myTeamColor: api.myTeamColor,
memberCount: api.memberCount()
};
},
reset() {
myTeammateIDs = /* @__PURE__ */ new Set();
lobby = null;
lobbyCaptured = false;
isTeamMode = false;
announced = false;
if (lobbyObserver) {
lobbyObserver.disconnect();
lobbyObserver = null;
}
armLobbyObserver();
}
};
return api;
function maybeAnnounce() {
if (announced || !isTeamMode) return;
const count = myTeammateIDs.size || (lobby ? lobby.myTeamPlayers.length : 0);
if (count <= 0) return;
announced = true;
roundLogger?.record("team_detected", {
source: myTeammateIDs.size ? "gameview" : "lobby",
myTeamName: lobby?.myTeamName || null,
myTeamColor: lobby?.myTeamColor || null,
memberCount: count
});
}
function armLobbyObserver() {
if (lobbyObserver || lobbyCaptured) return;
if (tryCaptureLobby()) return;
try {
lobbyObserver = new MutationObserver(() => {
if (tryCaptureLobby() && lobbyObserver) {
lobbyObserver.disconnect();
lobbyObserver = null;
}
});
const root = document.body || document.documentElement;
if (root) lobbyObserver.observe(root, { childList: true, subtree: true });
} catch (_) {
}
}
function tryCaptureLobby() {
if (lobbyCaptured) return true;
let root = null;
try {
root = document.querySelector("lobby-player-view");
} catch (_) {
}
if (!root) return false;
const parsed = parseLobbyTeams(root);
if (!parsed) return false;
lobby = parsed;
lobbyCaptured = true;
if (parsed.isTeamMode) isTeamMode = true;
logger?.info?.(\`Team lobby parsed: my team \${parsed.myTeamName || "?"} (\${parsed.myTeamPlayers.length} players)\`);
return true;
}
}
function readGameMode(gv) {
try {
if (!gv || typeof gv.config !== "function") return null;
const config = gv.config();
if (!config || typeof config.gameConfig !== "function") return null;
const gameConfig = config.gameConfig();
return gameConfig ? gameConfig.gameMode : null;
} catch (_) {
return null;
}
}
function parseLobbyTeams(root) {
const teamNameToPlayers = {};
const teamColorMap = {};
const playerNameToTeam = {};
let myTeamName = null;
let myTeamColor = null;
let dots = [];
try {
dots = Array.from(root.querySelectorAll("span[style*='--bg']"));
} catch (_) {
return null;
}
dots.forEach((dot) => {
const header = dot.parentElement;
const card = header ? header.parentElement : null;
if (!header || !card) return;
const nameSpan = header.querySelector("span.truncate");
const teamName = nameSpan ? nameSpan.textContent.trim() : "";
if (!teamName) return;
const styleAttr = dot.getAttribute("style") || "";
const colorMatch = styleAttr.match(/--bg:\\s*([^;]+)/);
const color = colorMatch ? colorMatch[1].trim() : null;
teamColorMap[teamName] = color;
let isMine = isHighlighted(card.className);
const players = [];
let memberSpans = [];
try {
memberSpans = Array.from(card.querySelectorAll("span.truncate.text-white"));
} catch (_) {
}
memberSpans.forEach((span) => {
const pname = span.textContent.trim();
if (!pname) return;
players.push(pname);
playerNameToTeam[pname] = teamName;
const row = span.parentElement;
if (row && isHighlighted(row.className)) isMine = true;
});
teamNameToPlayers[teamName] = players;
if (isMine) {
myTeamName = teamName;
myTeamColor = color;
}
});
const teamCount = Object.keys(teamNameToPlayers).length;
if (teamCount === 0) return null;
return {
isTeamMode: teamCount > 0,
myTeamName,
myTeamColor,
myTeamPlayers: myTeamName ? teamNameToPlayers[myTeamName] || [] : [],
playerNameToTeam,
teamNameToPlayers,
teamColorMap
};
}
function isHighlighted(className) {
const cls = String(className || "");
return cls.includes("bg-malibu-blue") || cls.includes("border-sky-");
}
function normName(name) {
return String(name == null ? "" : name).trim();
}
// src/page/advisor/spawn-scoring.js
var ANALYSIS_RADIUS = 30;
var NATION_ATTRACT_RADIUS = 120;
var PLAYER_REPEL_RADIUS = 150;
var GRID_STEP = 16;
var TOP_N = 5;
var W_LAND = 0.25;
var W_PLAINS = 0.2;
var W_NATION = 0.25;
var W_PLAYER_DIST = 0.25;
var W_EDGE = 0.05;
var W_TEAM = 0.2;
var TEAM_ATTRACT_RADIUS = 110;
var TEAM_MIN_GAP = ANALYSIS_RADIUS;
var TEAM_IDEAL_MAX = 90;
var TEAM_OVERLAP_MAX_SCORE = 0.4;
var TEAM_NEAR_TAG_THRESHOLD = 0.6;
var IS_LAND_BIT = 7;
var MAGNITUDE_MASK = 31;
function isLandByte(byte) {
return (byte & 1 << IS_LAND_BIT) !== 0;
}
function isPlainsByte(byte) {
return isLandByte(byte) && (byte & MAGNITUDE_MASK) < 10;
}
function precomputeStaticScores(mapData) {
const { terrain, width, height, nations } = mapData;
const candidates = [];
const radius = ANALYSIS_RADIUS;
const radius2 = radius * radius;
for (let cy = radius; cy < height - radius; cy += GRID_STEP) {
for (let cx = radius; cx < width - radius; cx += GRID_STEP) {
if (!isLandByte(terrain[cy * width + cx])) continue;
let landCount = 0;
let plainsCount = 0;
let totalChecked = 0;
for (let dy = -radius; dy <= radius; dy += 2) {
for (let dx = -radius; dx <= radius; dx += 2) {
if (dx * dx + dy * dy > radius2) continue;
const nx = cx + dx;
const ny = cy + dy;
if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
totalChecked += 1;
const byte = terrain[ny * width + nx];
if (isLandByte(byte)) {
landCount += 1;
if (isPlainsByte(byte)) plainsCount += 1;
}
}
}
const landDensity = totalChecked > 0 ? landCount / totalChecked : 0;
const plainsRatio = landCount > 0 ? plainsCount / landCount : 0;
const nationScore = computeNationScore(cx, cy, nations);
const edgeX = Math.min(cx, width - cx) / (width / 2);
const edgeY = Math.min(cy, height - cy) / (height / 2);
const edgeScore = Math.min(edgeX, edgeY);
const staticScore = landDensity * W_LAND + plainsRatio * W_PLAINS + nationScore * W_NATION + edgeScore * W_EDGE;
candidates.push({ x: cx, y: cy, staticScore, landDensity, plainsRatio, nationScore, edgeScore });
}
}
candidates.sort((a, b) => b.staticScore - a.staticScore);
return candidates;
}
function rankSpawnCandidates(staticCandidates, playerSpawns, options = {}) {
const teammateClientIDs = options.teammateClientIDs;
const teamMode = !!(teammateClientIDs && teammateClientIDs.size > 0);
const enemySpawns = [];
const teamSpawns = [];
playerSpawns.forEach((pos, clientID) => {
if (teamMode && teammateClientIDs.has(clientID)) teamSpawns.push(pos);
else enemySpawns.push(pos);
});
const repelR2 = PLAYER_REPEL_RADIUS * PLAYER_REPEL_RADIUS;
const scored = [];
for (let i = 0; i < staticCandidates.length; i += 1) {
const candidate = staticCandidates[i];
let playerDistScore = 1;
if (enemySpawns.length > 0) {
const minD2 = nearestDist2(candidate, enemySpawns);
if (minD2 < repelR2) playerDistScore = minD2 / repelR2;
}
let teamAttractScore = 0;
if (teamSpawns.length > 0) {
teamAttractScore = teamAttract(Math.sqrt(nearestDist2(candidate, teamSpawns)));
}
const score = teamMode ? candidate.staticScore + playerDistScore * W_PLAYER_DIST + teamAttractScore * W_TEAM : candidate.staticScore + playerDistScore * W_PLAYER_DIST;
scored.push({
x: candidate.x,
y: candidate.y,
score,
landDensity: candidate.landDensity,
plainsRatio: candidate.plainsRatio,
nationScore: candidate.nationScore,
edgeScore: candidate.edgeScore,
playerDistScore,
teamAttractScore
});
}
scored.sort((a, b) => b.score - a.score);
return { scores: scored, topSpots: pickDiverseTopSpots(scored) };
}
function nearestDist2(candidate, spawns) {
let minD2 = Infinity;
for (let j = 0; j < spawns.length; j += 1) {
const dx = candidate.x - spawns[j].x;
const dy = candidate.y - spawns[j].y;
const d2 = dx * dx + dy * dy;
if (d2 < minD2) minD2 = d2;
}
return minD2;
}
function teamAttract(dist) {
if (dist >= TEAM_ATTRACT_RADIUS) return 0;
if (dist <= TEAM_MIN_GAP) return dist / TEAM_MIN_GAP * TEAM_OVERLAP_MAX_SCORE;
if (dist <= TEAM_IDEAL_MAX) return 1;
return (TEAM_ATTRACT_RADIUS - dist) / (TEAM_ATTRACT_RADIUS - TEAM_IDEAL_MAX);
}
function describeSpawnSpot(spot) {
const tags = [];
if (spot.landDensity >= 0.82) tags.push("solid land");
if (spot.plainsRatio >= 0.65) tags.push("green growth");
if (spot.nationScore >= 0.65) tags.push("nation farm");
if (spot.playerDistScore >= 0.85) tags.push("low crowd");
if (spot.teamAttractScore >= TEAM_NEAR_TAG_THRESHOLD) tags.push("near team");
if (spot.edgeScore < 0.35) tags.push("edge risk");
return tags.length ? tags.join(", ") : "balanced";
}
function computeNationScore(cx, cy, nations) {
if (!nations.length) return 0;
let minD2 = Infinity;
for (let i = 0; i < nations.length; i += 1) {
const dx = cx - nations[i].coordinates[0];
const dy = cy - nations[i].coordinates[1];
const d2 = dx * dx + dy * dy;
if (d2 < minD2) minD2 = d2;
}
return Math.max(0, 1 - Math.sqrt(minD2) / NATION_ATTRACT_RADIUS);
}
function pickDiverseTopSpots(scored) {
const topSpots = [];
const minD2 = ANALYSIS_RADIUS * 2 * (ANALYSIS_RADIUS * 2);
for (let i = 0; i < scored.length && topSpots.length < TOP_N; i += 1) {
const spot = scored[i];
let tooClose = false;
for (let j = 0; j < topSpots.length; j += 1) {
const dx = topSpots[j].x - spot.x;
const dy = topSpots[j].y - spot.y;
if (dx * dx + dy * dy < minD2) {
tooClose = true;
break;
}
}
if (!tooClose) topSpots.push(spot);
}
return topSpots;
}
// src/page/openfront/player-metrics.js
function troopsInCombat(player) {
return (player?.outgoingAttacks || []).reduce(
(sum, attack) => sum + (attack && !attack.retreating ? Number(attack.troops || 0) : 0),
0
);
}
function incomingAttackTroops(gameState, myState) {
const mySmallID = myState?.smallID != null ? myState.smallID : gameState.state.mySmallID;
return incomingAttackTroopsForSmallID(gameState, mySmallID, myState?.id);
}
function incomingAttackTroopsForSmallID(gameState, targetSmallID, ownPlayerID = null) {
if (targetSmallID == null) return 0;
let sum = 0;
gameState.state.playerStates.forEach((player) => {
if (!player || ownPlayerID != null && player.id === ownPlayerID) return;
(player.outgoingAttacks || []).forEach((attack) => {
if (attack && !attack.retreating && attack.targetID === targetSmallID) sum += Number(attack.troops || 0);
});
});
return sum;
}
function incomingAttackers(gameState, myState) {
const mySmallID = myState?.smallID != null ? myState.smallID : gameState.state.mySmallID;
if (mySmallID == null) return [];
const ownPlayerID = myState?.id;
const attackers = [];
gameState.state.playerStates.forEach((player) => {
if (!player || ownPlayerID != null && player.id === ownPlayerID) return;
let troops = 0;
(player.outgoingAttacks || []).forEach((attack) => {
if (attack && !attack.retreating && attack.targetID === mySmallID) troops += Number(attack.troops || 0);
});
if (troops > 0) {
attackers.push({ id: player.id, smallID: player.smallID, name: player.name || player.id, troops });
}
});
return attackers.sort((a, b) => b.troops - a.troops);
}
// src/page/advisor/threat-model.js
var ATOM_COST = 75e4;
var HYDROGEN_COST = 5e6;
var NUKE_SOON_SEC = 120;
function evaluateThreats(gameState, teamDetection, nukeBuilders = null, goldIntel = null) {
const myState = gameState.getMyState();
const threats = /* @__PURE__ */ new Map();
if (!myState || !myState.troops) return threats;
gameState.state.playerStates.forEach((player) => {
if (!player || !player.isAlive || player.id === myState.id) return;
if (teamDetection?.isMyTeammate(player.id)) return;
const troopsRatio = (player.troops || 0) / Math.max(1, myState.troops || 0);
const tilesRatio = (player.tilesOwned || 0) / Math.max(1, myState.tilesOwned || 1);
const outgoing = troopsInCombat(player);
const activeAttackPressure = outgoing / Math.max(1, player.troops || 1);
const nukePotential = player.gold >= HYDROGEN_COST ? 2 : player.gold >= ATOM_COST ? 1 : 0;
const buildingNuke = !!(nukeBuilders && nukeBuilders.has(player.id));
const nukeEtaSec = nukePotential === 0 && goldIntel ? goldIntel.timeToAfford(player.id, ATOM_COST, player.gold) : null;
const nukeSoon = nukeEtaSec != null && nukeEtaSec > 0 && nukeEtaSec <= NUKE_SOON_SEC;
let score = troopsRatio * 0.45 + tilesRatio * 0.25 + activeAttackPressure * 0.15 + nukePotential * 0.25;
if (buildingNuke) score += 0.55;
if (nukeSoon) score += 0.2;
threats.set(player.id, {
id: player.id,
name: player.name || player.id,
score,
level: classifyThreat(score),
troopsRatio,
tilesRatio,
nukePotential,
buildingNuke,
nukeSoon,
nukeEtaSec: nukeEtaSec != null ? Math.round(nukeEtaSec) : null
});
});
return threats;
}
function classifyThreat(score) {
if (score >= 1.65) return "Critical";
if (score >= 1.1) return "Dangerous";
if (score >= 0.65) return "Neutral";
return "Weak";
}
// src/shared/units.js
var UNIT = Object.freeze({
CITY: "City",
DEFENSE_POST: "Defense Post",
PORT: "Port",
SAM_LAUNCHER: "SAM Launcher",
MISSILE_SILO: "Missile Silo",
FACTORY: "Factory",
WARSHIP: "Warship",
ATOM_BOMB: "Atom Bomb",
HYDROGEN_BOMB: "Hydrogen Bomb",
MIRV: "MIRV"
});
var WEAPONS_BY_POWER = Object.freeze([UNIT.MIRV, UNIT.HYDROGEN_BOMB, UNIT.ATOM_BOMB]);
// src/page/openfront/unit-intel.js
var MISSILE_SILO = UNIT.MISSILE_SILO;
var SAM_LAUNCHER = UNIT.SAM_LAUNCHER;
function scanUnitsByType(gameView, unitType, myState, teamDetection) {
const out = [];
if (!gameView) return out;
const seen = /* @__PURE__ */ new Set();
const myID = myState?.id;
const mySmallID = myState?.smallID;
const collect = (unit) => {
if (!unit) return;
const id = typeof unit.id === "function" ? unit.id() : null;
if (id != null && seen.has(id)) return;
if (id != null) seen.add(id);
const underConstruction = typeof unit.isUnderConstruction === "function" ? !!unit.isUnderConstruction() : false;
const owner = typeof unit.owner === "function" ? safeCall(() => unit.owner()) : null;
const ownerID = owner ? typeof owner.id === "function" ? safeCall(() => owner.id()) : owner.id : null;
const isMine = ownerID != null && (ownerID === myID || ownerID === mySmallID);
const isAlly = !isMine && ownerID != null && !!teamDetection?.isMyTeammate?.(ownerID);
const tile = safeCall(() => typeof unit.tile === "function" ? unit.tile() : null);
out.push({
ownerID,
ownerName: owner ? safeName(owner) : "Unknown",
isMine,
isAlly,
underConstruction,
tile: tile != null ? tile : null,
coords: tileCoords(gameView, tile)
});
};
try {
if (typeof gameView.unitStates === "function" && typeof gameView.unit === "function") {
for (const state of gameView.unitStates().values()) {
if (state && state.unitType === unitType) collect(gameView.unit(state.id));
}
}
if (typeof gameView.units === "function") {
const units = gameView.units(unitType);
if (units && typeof units.forEach === "function") units.forEach(collect);
}
} catch (_) {
}
return out;
}
function scanMissileSilos(gameView, myState, teamDetection) {
return scanUnitsByType(gameView, MISSILE_SILO, myState, teamDetection);
}
function enemySamSitesUnderConstruction(gameView, myState, teamDetection) {
return scanUnitsByType(gameView, SAM_LAUNCHER, myState, teamDetection).filter(
(sam) => !sam.isMine && !sam.isAlly && sam.underConstruction && sam.ownerID != null && sam.tile != null
);
}
function enemyNukeBuilders(siloIntel) {
const owners = /* @__PURE__ */ new Set();
(siloIntel || []).forEach((silo) => {
if (!silo.isMine && !silo.isAlly && silo.underConstruction && silo.ownerID != null) owners.add(silo.ownerID);
});
return owners;
}
function tileCoords(gameView, tile) {
try {
if (tile == null || typeof gameView.x !== "function" || typeof gameView.y !== "function") return null;
return { x: gameView.x(tile), y: gameView.y(tile) };
} catch (_) {
return null;
}
}
function safeName(player) {
try {
if (typeof player.displayName === "function") return player.displayName();
if (typeof player.name === "function") return player.name();
} catch (_) {
}
return "Unknown";
}
function safeCall(fn) {
try {
return fn();
} catch (_) {
return null;
}
}
// src/shared/number.js
function finiteOrZero(value) {
const number = Number(value);
return Number.isFinite(number) ? number : 0;
}
// src/page/openfront/gold-intel.js
var WINDOW_MS = 6e4;
function createGoldIntel() {
const history = /* @__PURE__ */ new Map();
return {
sample(gameState) {
const now = Date.now();
gameState.state.playerStates.forEach((player) => {
if (!player || player.id == null || !player.isAlive) return;
const gold = finiteOrZero(typeof player.gold === "bigint" ? Number(player.gold) : player.gold);
const arr = history.get(player.id) || [];
arr.push({ t: now, gold });
while (arr.length > 1 && now - arr[0].t > WINDOW_MS) arr.shift();
history.set(player.id, arr);
});
},
// Gold pro Sekunde \xFCber das Fenster; null wenn zu wenig Daten.
mps(id) {
const arr = history.get(id);
if (!arr || arr.length < 2) return null;
const first = arr[0];
const last = arr[arr.length - 1];
const dt = Math.max((last.t - first.t) / 1e3, 1e-3);
return (last.gold - first.gold) / dt;
},
// Sekunden, bis der Spieler \`cost\` erreicht; 0 wenn schon leistbar, null wenn nicht absehbar.
timeToAfford(id, cost, currentGold) {
const gold = finiteOrZero(currentGold);
if (gold >= cost) return 0;
const rate = this.mps(id);
if (rate == null || rate <= 0) return null;
return (cost - gold) / rate;
},
reset() {
history.clear();
}
};
}
// src/page/advisor/expansion-advisor.js
function evaluateExpansionState(troopInfo, myState, troopEconomy) {
if (!troopInfo) return { level: "unknown", label: "Expansion: unknown" };
const outgoingTroops = troopsInCombat(myState || {});
if (troopInfo.ratio < 0.25 && outgoingTroops > troopInfo.troops * 0.65) {
return { level: "stop", label: "Expansion: stop" };
}
const level = troopEconomy ? levelFromState(troopEconomy.state) : levelFromRatio(troopInfo.ratio);
return { level, label: \`Expansion: \${level}\` };
}
function levelFromState(state) {
if (state === "CRITICAL") return "recover";
if (state === "PUSH" || state === "CAP_WASTE") return "aggressive";
return "normal";
}
function levelFromRatio(ratio) {
if (ratio < 0.18) return "recover";
if (ratio > 0.65) return "aggressive";
return "normal";
}
// src/page/advisor/attack-advisor.js
var CLAIM_PATTERN = /\\bclaim\\b/i;
var HUMAN_TARGET_ENTER = 0.5;
var HUMAN_TARGET_EXIT = 0.62;
var HUMAN_FARM_ENTER = 0.28;
var HUMAN_FARM_EXIT = 0.36;
var HUMAN_DANGER_ENTER = 1.25;
var HUMAN_DANGER_EXIT = 1.05;
var STATUS_MIN_DWELL_TICKS = 25;
var CAPTURE_TILE_COST = 40;
var CAPTURE_ARMY_WEIGHT = 1.05;
var CAPTURE_MARGIN_SAFE = 1.15;
var CAPTURE_MARGIN_NORMAL = 1.08;
var TWO_WAVE_MIN_SHARE = 0.58;
var TARGET_CAPTURE_TURNS = 2;
var CAPTURE_OVERDRAFT_RESERVE_RATIO = 0.1;
var FARM_OVERDRAFT_STRENGTH_RATIO = 0.55;
var HUMAN_OVERDRAFT_STRENGTH_RATIO = 0.28;
function createAttackAdvisor({ settings, teamDetection }) {
const statusMemory = /* @__PURE__ */ new Map();
return {
reset() {
statusMemory.clear();
},
evaluate({ gameState, neighbourFetcher, troopInfo, expansion, troopEconomy, mapData = null }) {
const myState = gameState.getMyState();
const neighbours = neighbourFetcher.cachedNeighbourIDs;
if (!myState || !myState.isAlive || !neighbours) return [];
const myTroops = finiteOrZero(troopInfo?.troops || myState.troops);
const maxTroops = finiteOrZero(troopInfo?.maxTroops);
if (myTroops <= 0 || maxTroops <= 0) return [];
const reserveRatio = Number.isFinite(troopEconomy?.recommendedReserve) ? troopEconomy.recommendedReserve : computeReserveRatio({ gameState, neighbours, myState, myTroops, settings });
const reserveMaxSend = Math.max(0, Math.floor(myTroops - maxTroops * reserveRatio));
const safePush = !!troopEconomy?.safePush;
const tick = Number(gameState.state.currentTick) || 0;
const seen = /* @__PURE__ */ new Set();
const results = [];
neighbours.forEach((playerID) => {
if (playerID === myState.id) return;
if (teamDetection?.isMyTeammate(playerID)) return;
const target = gameState.state.playerStates.get(playerID);
if (!target || !target.isAlive) return;
seen.add(playerID);
const targetTroops = finiteOrZero(target.troops);
const targetTiles = finiteOrZero(target.tilesOwned);
const effectiveTargetTroops = Math.max(0, targetTroops - troopsInCombat(target));
const strengthRatio = effectiveTargetTroops / Math.max(1, myTroops);
const isHuman = isHumanTarget(target);
const maxSend = computeTargetMaxSend({
myTroops,
maxTroops,
reserveMaxSend,
safePush,
strengthRatio,
isHuman,
targetTiles,
expansion
});
const sizing = computeAttackSizing({
myTroops,
targetTroops: effectiveTargetTroops,
targetTiles,
safePush,
maxSend,
kind: isHuman ? "human" : "farm"
});
const officialSizing = estimateOfficialCaptureCost({
target,
effectiveTargetTroops,
mapData
});
const desiredTroops = sizing.desiredTroops;
const suggestedTroops = Math.min(desiredTroops, maxSend);
const reserveAfterRatio = (myTroops - suggestedTroops) / maxTroops;
let status;
if (isHuman) {
const committed = statusMemory.get(playerID)?.status || null;
const desired = classifyHumanTarget({
committed,
allied: isAllied(myState, target) || !!teamDetection?.isMyTeammate(target.id),
autoFarmHumanTargets: !!settings.get("autoFarmHumanTargets"),
safePush,
strengthRatio,
desiredTroops,
maxSend,
targetTiles,
estimatedCaptureTurns: sizing.estimatedCaptureTurns
});
status = stabilizeStatus(statusMemory, playerID, committed, desired, tick);
} else {
status = classifyFarmTarget({
expansion,
strengthRatio,
desiredTroops,
maxSend,
targetTiles,
target,
safePush,
estimatedCaptureTurns: sizing.estimatedCaptureTurns
});
}
const suggestedPercent = myTroops > 0 ? Math.max(1, Math.round(suggestedTroops / myTroops * 100)) : 0;
results.push({
id: target.id,
name: target.name || target.displayName || target.id,
playerType: target.playerType || "",
isHuman,
status,
label: getLabel(status, suggestedPercent),
reason: getReason(status, expansion, strengthRatio, desiredTroops, maxSend, sizing, isHuman),
score: scoreTarget(status, strengthRatio, targetTiles),
targetTroops,
targetTiles,
effectiveTargetTroops,
strengthRatio,
suggestedTroops,
suggestedPercent,
reserveAfterRatio,
appliedReserveRatio: reserveRatio,
reserveMaxSend,
captureMaxSend: maxSend,
usedCaptureOverdraft: maxSend > reserveMaxSend,
captureCostEstimate: sizing.captureCostEstimate,
captureLandCostEstimate: sizing.landCostEstimate,
captureTileCost: sizing.tileCost,
estimatedCaptureTurns: sizing.estimatedCaptureTurns,
sizingMode: sizing.mode,
officialCaptureCostEstimate: officialSizing.captureCostEstimate,
officialCaptureTileCost: officialSizing.tileCost,
officialCaptureTurns: officialSizing.estimatedCaptureTurns,
officialCaptureSource: officialSizing.source,
officialTerrainClass: officialSizing.terrainClass
});
});
statusMemory.forEach((_, id) => {
if (!seen.has(id)) statusMemory.delete(id);
});
return results.sort((a, b) => b.score - a.score).slice(0, 12);
}
};
}
function computeReserveRatio({ gameState, neighbours, myState, myTroops, settings }) {
const fixed = clampRatio(Number(settings.get("autoFarmReserveRatio")), 0.55);
if (!settings.get("autoFarmDynamicReserve")) return fixed;
const base = clampRatio(Number(settings.get("autoFarmBaseReserveRatio")), 0.3);
const min = clampRatio(Number(settings.get("autoFarmMinReserveRatio")), 0.2);
const max = clampRatio(Number(settings.get("autoFarmMaxReserveRatio")), 0.65);
if (myTroops <= 0) return clamp(min, max, base);
let reserve = base;
let humanEffective = 0;
let strongestAi = 0;
(neighbours || []).forEach((id) => {
if (id === myState.id) return;
const player = gameState.state.playerStates.get(id);
if (!player || !player.isAlive) return;
const effective = Math.max(0, finiteOrZero(player.troops) - troopsInCombat(player));
if (String(player.playerType || "").toUpperCase() === "HUMAN") humanEffective += effective;
else if (effective > strongestAi) strongestAi = effective;
});
reserve += Math.min(0.3, humanEffective / myTroops * 0.3);
const incoming = incomingAttackTroops(gameState, myState);
reserve += Math.min(0.25, incoming / myTroops * 0.5);
if (strongestAi / myTroops > 0.6) reserve += 0.05;
return clamp(min, max, reserve);
}
function stabilizeStatus(statusMemory, id, committed, desired, tick) {
if (committed === null || desired === "danger" || desired === "hold") {
statusMemory.set(id, { status: desired, candidate: desired, candidateSince: tick });
return desired;
}
const mem = statusMemory.get(id) || { status: committed, candidate: committed, candidateSince: tick };
if (desired === committed) {
statusMemory.set(id, { status: committed, candidate: committed, candidateSince: tick });
return committed;
}
if (mem.candidate !== desired) {
statusMemory.set(id, { status: committed, candidate: desired, candidateSince: tick });
return committed;
}
if (tick - mem.candidateSince >= STATUS_MIN_DWELL_TICKS) {
statusMemory.set(id, { status: desired, candidate: desired, candidateSince: tick });
return desired;
}
return committed;
}
function isAllowedFarmTarget(player) {
const type = String(player.playerType || "").toUpperCase();
const name = \`\${player.name || ""} \${player.displayName || ""}\`;
if (type === "HUMAN") return false;
if (type === "BOT" || type === "NATION") return true;
return CLAIM_PATTERN.test(name);
}
function isHumanTarget(player) {
return String(player.playerType || "").toUpperCase() === "HUMAN";
}
function computeTargetMaxSend({ myTroops, maxTroops, reserveMaxSend, safePush, strengthRatio, isHuman, targetTiles, expansion }) {
if (!safePush || targetTiles <= 0 || expansion && expansion.level === "stop") return reserveMaxSend;
const threshold = isHuman ? HUMAN_OVERDRAFT_STRENGTH_RATIO : FARM_OVERDRAFT_STRENGTH_RATIO;
if (strengthRatio > threshold) return reserveMaxSend;
const overdraftMaxSend = Math.max(0, Math.floor(myTroops - maxTroops * CAPTURE_OVERDRAFT_RESERVE_RATIO));
return Math.max(reserveMaxSend, overdraftMaxSend);
}
function computeAttackSizing({ myTroops, targetTroops, targetTiles, safePush = false, maxSend = 0, kind = "farm" }) {
const isHuman = kind === "human";
const legacyMultiplier = isHuman ? 1.35 : 1.25;
const floorRatio = isHuman ? 0.08 : 0.04;
const ceilingRatio = isHuman ? safePush ? 0.62 : 0.36 : safePush ? 0.72 : 0.42;
const targetBased = Math.ceil(Math.max(1, targetTroops) * legacyMultiplier);
const floor = Math.ceil(myTroops * floorRatio);
const ceiling = Math.ceil(myTroops * ceilingRatio);
const tileCost = CAPTURE_TILE_COST;
const landCostEstimate = Math.ceil(Math.max(0, targetTiles) * tileCost);
const captureCostEstimate = Math.ceil(Math.max(1, targetTroops) * CAPTURE_ARMY_WEIGHT + landCostEstimate);
const margin = safePush ? CAPTURE_MARGIN_SAFE : CAPTURE_MARGIN_NORMAL;
const oneWave = Math.ceil(captureCostEstimate * margin);
const twoWave = Math.ceil(oneWave * TWO_WAVE_MIN_SHARE);
let consolidation = oneWave;
let mode = "one_wave";
if (oneWave > maxSend) {
consolidation = maxSend >= twoWave ? maxSend : twoWave;
mode = maxSend >= twoWave ? "two_wave_push" : "needs_reserve";
}
const rawDesired = Math.max(targetBased, floor, consolidation);
const desiredTroops = Math.max(1, Math.min(rawDesired, ceiling));
const estimatedCaptureTurns = desiredTroops > 0 ? Math.ceil(oneWave / desiredTroops) : null;
if (desiredTroops < rawDesired && mode !== "needs_reserve") mode = "ceiling_limited";
return {
desiredTroops,
captureCostEstimate,
landCostEstimate,
tileCost,
oneWave,
twoWave,
estimatedCaptureTurns,
mode
};
}
function classifyFarmTarget({ expansion, strengthRatio, desiredTroops, maxSend, targetTiles, target, safePush, estimatedCaptureTurns }) {
if (!isAllowedFarmTarget(target)) return "hold";
if (expansion && (expansion.level === "recover" || expansion.level === "stop")) return "mark";
if (targetTiles <= 0 || strengthRatio >= 0.75) return "hold";
const farmThreshold = safePush ? 0.6 : 0.45;
if (desiredTroops <= maxSend && strengthRatio <= farmThreshold && estimatedCaptureTurns <= TARGET_CAPTURE_TURNS) return "farm";
if (strengthRatio <= 0.65 || targetTiles >= 250) return "mark";
return "hold";
}
function classifyHumanTarget({
committed,
allied,
autoFarmHumanTargets,
safePush,
strengthRatio,
desiredTroops,
maxSend,
targetTiles,
estimatedCaptureTurns
}) {
if (allied) return "hold";
if (committed === "danger") {
if (!(targetTiles > 0 && strengthRatio < HUMAN_DANGER_EXIT)) return "danger";
} else if (targetTiles <= 0 || strengthRatio >= HUMAN_DANGER_ENTER) {
return "danger";
}
const canAutoFarm = autoFarmHumanTargets && safePush && desiredTroops <= maxSend && estimatedCaptureTurns <= TARGET_CAPTURE_TURNS && targetTiles > 0;
if (committed === "farm") {
if (canAutoFarm && strengthRatio <= HUMAN_FARM_EXIT) return "farm";
} else if (canAutoFarm && strengthRatio <= HUMAN_FARM_ENTER) {
return "farm";
}
if (committed === "target") {
if (strengthRatio <= HUMAN_TARGET_EXIT && desiredTroops <= maxSend) return "target";
return "wait";
}
if (strengthRatio <= HUMAN_TARGET_ENTER && desiredTroops <= maxSend) return "target";
return "wait";
}
function getReason(status, expansion, strengthRatio, desiredTroops, maxSend, sizing, isHuman) {
if (expansion && (expansion.level === "recover" || expansion.level === "stop")) return "reserve";
if (status === "farm") return isHuman ? "weak_adjacent_human_capture" : "weak_adjacent_ai_capture";
if (status === "target") return "weak_adjacent_human";
if (desiredTroops > maxSend) return "reserve_limit";
if (sizing?.estimatedCaptureTurns > TARGET_CAPTURE_TURNS) return "capture_too_slow";
if (strengthRatio >= 0.75 || status === "danger") return "too_strong";
return "watch";
}
function scoreTarget(status, strengthRatio, targetTiles) {
const statusBonus = status === "farm" ? 2 : status === "target" ? 1.6 : status === "mark" ? 1 : status === "wait" ? 0.4 : 0;
const tileScore = Math.min(1, targetTiles / 2e3);
const weaknessScore = Math.max(0, 1 - strengthRatio);
return statusBonus + tileScore * 0.4 + weaknessScore * 0.6;
}
function getLabel(status, suggestedPercent) {
if (status === "farm") return \`FARM \${suggestedPercent}%\`;
if (status === "target") return \`TARGET \${suggestedPercent}%\`;
if (status === "mark") return "MARK";
if (status === "wait") return "WAIT";
if (status === "danger") return "DANGER";
return "HOLD";
}
function isAllied(myState, target) {
const alliances = Array.isArray(myState.alliances) ? myState.alliances : [];
return alliances.some((alliance) => alliance && alliance.other === target.id);
}
function clamp(min, max, value) {
return Math.max(min, Math.min(max, value));
}
function clampRatio(value, fallback) {
return Number.isFinite(value) && value > 0 && value < 1 ? value : fallback;
}
// src/page/advisor/troop-economy.js
var PEAK_RATIO = 0.42;
var STATES = [
{ state: "CRITICAL", max: 0.18, floor: 0.42, hint: "kritisch - nur sichere Kleinstaktionen" },
{ state: "RECOVER", max: 0.3, floor: 0.36, hint: "regenerieren - fruehe Expansion moeglich" },
{ state: "GROWTH_PEAK", max: 0.5, floor: 0.34, hint: "Wachstumsband - Druck halten" },
{ state: "READY", max: 0.65, floor: 0.34, hint: "bereit - aktiv expandieren/farmen" },
{ state: "PUSH", max: 0.82, floor: 0.3, hint: "Push-Fenster - Truppen einsetzen" },
{ state: "CAP_WASTE", max: 1.01, floor: 0.25, hint: "zu nah am Cap - Truppen einsetzen" }
];
var PUSH_TARGET_RATIO = 0.45;
var PUSH_SIM_TICK_CAP = 6e3;
var SAFE_PUSH_THRESHOLD = 0.78;
var AGGRESSIVE_RESERVE_DROP = 0.16;
function evaluateTroopEconomy({ troopInfo, myState, gameState, neighbours, settings, teamDetection, threats }) {
const maxTroops = finiteOrZero(troopInfo?.maxTroops);
const currentTroops = finiteOrZero(troopInfo?.troops ?? myState?.troops);
if (maxTroops <= 0 || currentTroops < 0) return null;
const currentRatio = clamp01(currentTroops / maxTroops);
const troopIncreaseRate = growthRate(currentTroops, maxTroops);
const estimatedPeakIncreaseRate = growthRate(PEAK_RATIO * maxTroops, maxTroops);
const growthEfficiency = estimatedPeakIncreaseRate > 0 ? clamp01(troopIncreaseRate / estimatedPeakIncreaseRate) : 0;
const band = STATES.find((entry) => currentRatio < entry.max) || STATES[STATES.length - 1];
const state = band.state;
const stateFloor = band.floor;
const incoming = incomingAttackTroops(gameState, myState);
let humanNeighbourEffective = 0;
(neighbours || []).forEach((id) => {
if (!myState || id === myState.id) return;
if (teamDetection?.isMyTeammate(id)) return;
const player = gameState.state.playerStates.get(id);
if (!player || !player.isAlive) return;
if (String(player.playerType || "").toUpperCase() !== "HUMAN") return;
humanNeighbourEffective += Math.max(0, finiteOrZero(player.troops) - troopsInCombat(player));
});
const combatSafety = clamp01(1 - (incoming + humanNeighbourEffective) / Math.max(1, maxTroops));
const safePush = incoming === 0 && combatSafety >= SAFE_PUSH_THRESHOLD;
const combatReserve = computeReserveRatio({ gameState, neighbours, myState: myState || {}, myTroops: currentTroops, settings });
let recommendedReserve;
if (!settings.get("autoFarmDynamicReserve")) {
recommendedReserve = combatReserve;
} else {
const min = clampRatio2(Number(settings.get("autoFarmMinReserveRatio")), 0.2);
const baseMax = clampRatio2(Number(settings.get("autoFarmMaxReserveRatio")), 0.65);
const max = teamDetection?.isTeamMode ? Math.max(baseMax, 0.7) : baseMax;
recommendedReserve = safePush ? clamp2(min, max, Math.min(stateFloor, combatReserve - AGGRESSIVE_RESERVE_DROP)) : clamp2(min, max, Math.max(stateFloor, combatReserve));
}
const safeSpendableTroops = Math.max(0, currentTroops - maxTroops * recommendedReserve);
return {
currentTroops,
maxTroops,
currentRatio,
troopIncreaseRate,
growthEfficiency,
combatSafety,
safePush,
recommendedReserve,
safeSpendableTroops,
state,
stateFloor,
hint: band.hint,
timeToPushSec: estimateTimeToPushSec(currentTroops, maxTroops),
hasThreat: Array.isArray(threats) && threats.some((t) => t && (t.level === "Dangerous" || t.level === "Critical"))
};
}
function growthRate(currentTroops, maxTroops) {
const current = finiteOrZero(currentTroops);
const max = finiteOrZero(maxTroops);
if (max <= 0 || current >= max) return 0;
return (10 + Math.pow(Math.max(0, current), 0.73) / 4) * (1 - current / max);
}
function estimateTimeToPushSec(currentTroops, maxTroops) {
const target = PUSH_TARGET_RATIO * maxTroops;
if (currentTroops >= target) return null;
let troops = currentTroops;
let ticks = 0;
while (troops < target && ticks < PUSH_SIM_TICK_CAP) {
const rate = growthRate(troops, maxTroops);
if (rate <= 0) break;
troops += rate;
ticks += 1;
}
if (troops < target) return null;
return Math.round(ticks / 10);
}
function clamp2(min, max, value) {
return Math.max(min, Math.min(max, value));
}
function clamp01(value) {
const number = Number(value);
if (!Number.isFinite(number)) return 0;
return Math.max(0, Math.min(1, number));
}
function clampRatio2(value, fallback) {
return Number.isFinite(value) && value > 0 && value < 1 ? value : fallback;
}
// src/page/automation/farm-window.js
function isInsideFarmWindow(state, settings) {
const startedAt = Number(state.roundStartedAtMs) || 0;
const windowMs = Number(settings.get("autoFarmWindowMs")) || 24e4;
return startedAt > 0 && performance.now() - startedAt <= windowMs;
}
// src/page/automation/auto-expand.js
var BACKOFF_AFTER_STAGNANT = 4;
var BACKOFF_COOLDOWN_MS = 2e4;
var MIN_SURPLUS_RATIO = 0.01;
var EARLY_MIN_SURPLUS_RATIO = 2e-3;
var FAST_EXPAND_COOLDOWN_MS = 900;
var NORMAL_EXPAND_COOLDOWN_MS = 1500;
var EARLY_WAVE_RATIO = 0.12;
var NORMAL_WAVE_RATIO = 0.18;
var PUSH_WAVE_RATIO = 0.28;
function createAutoExpand({ gameState, neighbourFetcher, OrigWS, origWsSend, settings, logger, roundLogger, getTroopEconomy }) {
let lastSentAt = 0;
let lastSkipAt = 0;
let lastTilesOwned = -1;
let stagnantAttempts = 0;
let status = { state: "idle", reason: "not_started" };
return {
reset() {
lastSentAt = 0;
lastSkipAt = 0;
lastTilesOwned = -1;
stagnantAttempts = 0;
status = { state: "idle", reason: "reset" };
},
getStatus() {
return status;
},
run(troopInfo, options = {}) {
const state = gameState.state;
if (!options.force && !settings.get("autoExpand")) return setStatus("idle", "disabled", false);
if (!OrigWS) return setStatus("idle", "websocket_unavailable", false);
if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !state.myPlayerID) {
return setStatus("idle", "not_ready", false);
}
if (!origWsSend) return setStatus("idle", "socket_unavailable", false);
if (!isInsideFarmWindow(state, settings)) return setStatus("idle", "outside_window", false);
if (!neighbourFetcher.cachedHasOpenFrontier) {
logSkip("no_frontier");
return setStatus("blocked", "no_frontier", true);
}
const myState = gameState.getMyState();
if (!myState || !myState.isAlive) return setStatus("idle", "not_alive", false);
const myTroops = finiteOrZero(troopInfo?.troops || myState.troops);
const maxTroops = finiteOrZero(troopInfo?.maxTroops);
if (myTroops <= 0 || maxTroops <= 0) return setStatus("idle", "no_troops", false);
const economy = getTroopEconomy?.();
const reserveRatio = Number.isFinite(economy?.recommendedReserve) ? economy.recommendedReserve : computeReserveRatio({
gameState,
neighbours: neighbourFetcher.cachedNeighbourIDs,
myState,
myTroops,
settings
});
const maxSend = Math.floor(myTroops - maxTroops * reserveRatio);
const currentRatio = myTroops / maxTroops;
const minSurplusRatio = currentRatio < 0.25 ? EARLY_MIN_SURPLUS_RATIO : MIN_SURPLUS_RATIO;
if (maxSend <= Math.floor(maxTroops * minSurplusRatio)) {
logSkip("reserve_limit", { reserveRatio, maxSend, currentRatio, minSurplusRatio });
return setStatus("blocked", "reserve_limit", true, { reserveRatio, maxSend });
}
const tilesOwned = finiteOrZero(myState.tilesOwned);
const waveRatio = chooseWaveRatio(economy, currentRatio);
const cooldownMs = stagnantAttempts >= BACKOFF_AFTER_STAGNANT ? BACKOFF_COOLDOWN_MS : chooseCooldownMs(economy);
const now = performance.now();
if (now - lastSentAt < cooldownMs) {
return setStatus("cooldown", "cooldown", true, { cooldownMs: Math.round(cooldownMs - (now - lastSentAt)) });
}
if (lastTilesOwned >= 0) {
if (tilesOwned > lastTilesOwned) stagnantAttempts = 0;
else stagnantAttempts += 1;
}
const troops = Math.max(1, Math.min(maxSend, Math.floor(maxTroops * waveRatio)));
origWsSend.call(
state.gameSocket,
JSON.stringify({ type: "intent", intent: { type: "attack", targetID: null, troops } })
);
lastSentAt = now;
lastTilesOwned = tilesOwned;
logger.info(\`Auto-expand wilderness: \${troops} troops\`);
roundLogger?.record("auto_expand_sent", {
troops,
reserveRatio,
maxSend,
waveRatio,
currentRatio,
minSurplusRatio,
tilesOwned,
stagnantAttempts
});
setStatus("sent", \`sent_\${troops}\`, true, { troops, reserveRatio, waveRatio });
return true;
}
};
function logSkip(reason, extra = {}) {
const now = performance.now();
if (now - lastSkipAt < 5e3) return;
lastSkipAt = now;
roundLogger?.record("auto_expand_skipped", { reason, ...extra });
}
function setStatus(state, reason, result, extra = {}) {
status = { state, reason, ...extra };
return result;
}
}
function chooseCooldownMs(economy) {
if (economy?.safePush) return FAST_EXPAND_COOLDOWN_MS;
return NORMAL_EXPAND_COOLDOWN_MS;
}
function chooseWaveRatio(economy, currentRatio) {
if (economy?.state === "CAP_WASTE" || economy?.state === "PUSH") return PUSH_WAVE_RATIO;
if (economy?.safePush && currentRatio >= 0.3) return NORMAL_WAVE_RATIO;
return EARLY_WAVE_RATIO;
}
// src/page/advisor/buy-advisor.js
function recommendBuys({ gameState, troopInfo, threats, farmRecommendations, troopEconomy }) {
const myState = gameState.getMyState();
if (!myState || !myState.isAlive) return [];
const gold = finiteOrZero(myState.gold);
const dangerousThreat = (threats || []).find((threat) => threat.level === "Dangerous" || threat.level === "Critical");
const nukeThreat = (threats || []).find((threat) => threat.nukePotential > 0 || threat.buildingNuke || threat.nukeSoon);
const farmAvailable = (farmRecommendations || []).some((target) => target.status === "farm");
const ratio = Number(troopInfo?.ratio || 0);
const ecoState = troopEconomy?.state || null;
const lowEco = ecoState === "CRITICAL" || ecoState === "RECOVER";
const recommendations = [];
const gameView = getGameView();
const myPlayer = safeMyPlayer(gameView);
const cityCount = countPlayerUnits(myPlayer, "City");
const factoryCount = countPlayerUnits(myPlayer, "Factory");
const cityCost = getStructureCost("City", { gameView, player: myPlayer });
const portCost = getStructureCost("Port", { gameView, player: myPlayer });
const factoryCost = getStructureCost("Factory", { gameView, player: myPlayer });
const defenseCost = getStructureCost("Defense Post", { gameView, player: myPlayer });
const samCost = getStructureCost("SAM Launcher", { gameView, player: myPlayer });
const siloCost = getStructureCost("Missile Silo", { gameView, player: myPlayer });
if (nukeThreat && gold >= samCost.cost) {
recommendations.push(unverified("SAM Launcher", \`Nuke-Gefahr: \${nukeThreat.name}\`, samCost));
}
if (dangerousThreat && gold >= defenseCost.cost) {
recommendations.push(unverified("Defense Post", \`Frontdruck: \${dangerousThreat.name}\`, defenseCost));
}
if (ecoState === "CAP_WASTE" && gold >= cityCost.cost) {
recommendations.push(verifiedEco("City", "Cap-Waste: Gold jetzt in Eco investieren", cityCost));
}
if (gold >= cityCost.cost && ratio >= 0.35 && ecoState !== "CAP_WASTE") {
recommendations.push(verifiedEco("City", farmAvailable ? "Eco nach Farm-Fenster stabilisieren" : "sicherer Economy-Kauf", cityCost));
}
if (cityCount >= 1 && factoryCount < 2 && gold >= factoryCost.cost && ratio >= 0.35 && !lowEco) {
recommendations.push(verifiedEco("Factory", "sichere Land-Eco ausbauen", factoryCost));
}
if (gold >= portCost.cost) {
recommendations.push(verifiedEco("Port", "nur bei guter Wasserroute", portCost));
}
if (gold >= siloCost.cost && !dangerousThreat && !lowEco) {
recommendations.push(unverified("Missile Silo", "nur bei stabiler Eco und sicherer Flanke", siloCost));
}
return recommendations.slice(0, 3);
}
function verifiedEco(label, reason, costModel) {
return {
label,
reason,
actionAvailable: true,
actionKey: label.toLowerCase().replace(/\\s+/g, "_"),
estimatedCost: costModel?.cost || null,
costSource: costModel?.source || null
};
}
function unverified(label, reason, costModel) {
return {
label,
reason,
actionAvailable: false,
actionKey: label.toLowerCase().replace(/\\s+/g, "_"),
estimatedCost: costModel?.cost || null,
costSource: costModel?.source || null
};
}
function safeMyPlayer(gameView) {
try {
return gameView && typeof gameView.myPlayer === "function" ? gameView.myPlayer() : null;
} catch (_) {
return null;
}
}
// src/page/automation/auto-spawn.js
var SPAWN_COMMIT_MARGIN_TURNS = 20;
var RESEND_COOLDOWN_MS = 1500;
var SPAWN_IMPROVE_FACTOR = 1.03;
function createAutoSpawn({ gameState, mapDataRef, OrigWS, origWsSend, settings, logger, roundLogger, teamDetection }) {
let lastAutoSpawnTile = -1;
let lastSentScore = 0;
let lastResendAt = 0;
let hasSentThisRound = false;
let lastPhaseLogAt = 0;
let status = { state: "idle", reason: "not_started" };
return {
reset() {
lastAutoSpawnTile = -1;
lastSentScore = 0;
lastResendAt = 0;
hasSentThisRound = false;
lastPhaseLogAt = 0;
status = { state: "idle", reason: "reset" };
},
getStatus() {
return status;
},
send(spot) {
const mapData = mapDataRef.current;
const state = gameState.state;
if (!OrigWS) return setStatus("idle", "websocket_unavailable");
if (!mapData || !state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !origWsSend) {
return setStatus("idle", "not_ready");
}
const delayMs = Number(settings.get("autoSpawnDelayMs")) || 0;
const waitedMs = state.roundStartedAtMs ? performance.now() - state.roundStartedAtMs : 0;
if (state.roundStartedAtMs && waitedMs < delayMs) {
return setStatus("waiting", "delay", { waitMs: Math.round(delayMs - waitedMs) });
}
const phase = getSpawnPhaseInfo({ gameView: getGameView(), state, teamMode: !!teamDetection?.isTeamMode });
logSpawnPhase(phase);
const tileRef = spot.y * mapData.width + spot.x;
const score = Number(spot.score) || 0;
const inSpawnPhase = phase.remainingTurns > SPAWN_COMMIT_MARGIN_TURNS;
const now = performance.now();
if (hasSentThisRound) {
if (!inSpawnPhase) return setStatus("sent", "committed", { tile: lastAutoSpawnTile });
if (tileRef === lastAutoSpawnTile) return setStatus("sent", "best", { tile: tileRef });
if (now - lastResendAt < RESEND_COOLDOWN_MS) return setStatus("sent", "resend_cooldown", { tile: lastAutoSpawnTile });
if (score <= lastSentScore * SPAWN_IMPROVE_FACTOR) return setStatus("sent", "no_improvement", { tile: lastAutoSpawnTile });
}
const isResend = hasSentThisRound;
lastAutoSpawnTile = tileRef;
lastSentScore = score;
lastResendAt = now;
hasSentThisRound = true;
origWsSend.call(state.gameSocket, JSON.stringify({ type: "intent", intent: { type: "spawn", tile: tileRef } }));
logger.info(\`Auto-spawn \${isResend ? "re-correct" : "sent"}: (\${spot.x},\${spot.y}) tile=\${tileRef} score=\${score.toFixed(3)}\`);
roundLogger?.record("auto_spawn_sent", {
x: spot.x,
y: spot.y,
tile: tileRef,
score,
resend: isResend,
waitedMs: Math.round(waitedMs),
spawnRemainingTurns: phase.remainingTurns,
spawnPhaseTurns: phase.totalTurns,
spawnPhaseSource: phase.source
});
return setStatus("sent", isResend ? "re_corrected" : "sent", { tile: tileRef });
}
};
function logSpawnPhase(phase) {
const now = performance.now();
if (now - lastPhaseLogAt < 5e3) return;
lastPhaseLogAt = now;
roundLogger?.record("spawn_phase_state", {
currentTurn: phase.currentTurn,
totalTurns: phase.totalTurns,
remainingTurns: phase.remainingTurns,
waitCapMs: Math.round(phase.waitCapMs),
source: phase.source
});
}
function setStatus(state, reason, extra = {}) {
status = { state, reason, ...extra };
}
}
// src/page/automation/auto-farm.js
var AGGRESSIVE_FARM_COOLDOWN_MS = 800;
function createAutoFarm({ gameState, OrigWS, origWsSend, settings, logger, roundLogger, getTroopEconomy }) {
const targetCooldowns = /* @__PURE__ */ new Map();
let lastSentAt = 0;
let lastSkipAt = 0;
let status = { state: "idle", reason: "not_started" };
return {
reset() {
targetCooldowns.clear();
lastSentAt = 0;
lastSkipAt = 0;
status = { state: "idle", reason: "reset" };
},
getStatus() {
return status;
},
run(recommendations, expansion, options = {}) {
const state = gameState.state;
if (!options.force && !settings.get("autoFarm")) return setStatus("idle", "disabled");
if (!OrigWS) return setStatus("idle", "websocket_unavailable");
if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !state.myPlayerID) {
return setStatus("idle", "not_ready");
}
if (!origWsSend) return setStatus("idle", "socket_unavailable");
if (!isInsideFarmWindow(state, settings)) return setStatus("idle", "outside_window");
const now = performance.now();
const safePush = !!getTroopEconomy?.()?.safePush;
const target = (recommendations || []).find((candidate) => candidate.status === "farm" && candidate.suggestedTroops > 0);
if (!target) return setStatus("blocked", "no_target");
if (expansion && expansion.level === "stop") {
logSkip("stop", { targetID: target.id, targetName: target.name || target.id });
return setStatus("blocked", "stop");
}
if (expansion && expansion.level === "recover" && !safePush) {
logSkip("recover", { targetID: target.id, targetName: target.name || target.id });
return setStatus("blocked", "recover");
}
const baseCooldown = Number(settings.get("autoFarmCooldownMs")) || 2500;
const cooldownMs = safePush ? Math.min(baseCooldown, AGGRESSIVE_FARM_COOLDOWN_MS) : baseCooldown;
if (now - lastSentAt < cooldownMs) {
return setStatus("cooldown", "cooldown", { cooldownMs: Math.round(cooldownMs - (now - lastSentAt)) });
}
const lastTargetSentAt = targetCooldowns.get(target.id) || 0;
const perTargetFactor = safePush ? 1.4 : 2.4;
if (now - lastTargetSentAt < cooldownMs * perTargetFactor) {
return setStatus("cooldown", "target_cooldown", { targetName: target.name || target.id });
}
const troops = Math.max(1, Math.floor(target.suggestedTroops));
origWsSend.call(
state.gameSocket,
JSON.stringify({ type: "intent", intent: { type: "attack", targetID: target.id, troops } })
);
lastSentAt = now;
targetCooldowns.set(target.id, now);
logger.info(\`Auto-farm attack \${target.name || target.id}: \${troops} troops\`);
roundLogger?.record("auto_farm_attack_sent", {
targetID: target.id,
targetName: target.name || target.id,
isHuman: !!target.isHuman,
troops,
suggestedPercent: target.suggestedPercent,
reserveAfterRatio: target.reserveAfterRatio,
appliedReserveRatio: target.appliedReserveRatio,
reserveMaxSend: target.reserveMaxSend,
captureMaxSend: target.captureMaxSend,
usedCaptureOverdraft: !!target.usedCaptureOverdraft,
targetTroops: target.targetTroops,
effectiveTargetTroops: target.effectiveTargetTroops,
targetTiles: target.targetTiles,
captureCostEstimate: target.captureCostEstimate,
officialCaptureCostEstimate: target.officialCaptureCostEstimate,
officialCaptureTurns: target.officialCaptureTurns,
officialCaptureSource: target.officialCaptureSource,
estimatedCaptureTurns: target.estimatedCaptureTurns,
sizingMode: target.sizingMode
});
setStatus("sent", \`sent_\${target.name || target.id}\`, { targetName: target.name || target.id, troops });
}
};
function logSkip(reason, extra = {}) {
const now = performance.now();
if (now - lastSkipAt <= 5e3) return;
lastSkipAt = now;
roundLogger?.record("auto_farm_skipped", { reason, ...extra });
}
function setStatus(state, reason, extra = {}) {
status = { state, reason, ...extra };
}
}
// src/page/automation/auto-eco.js
var ECO_BUILD_COOLDOWN_MS = 6500;
var EARLY_ECO_BUILD_COOLDOWN_MS = 1e3;
var EARLY_ECO_FREE_RESERVE_BUILDS = 10;
var CITY_UNIT = "City";
var PORT_UNIT = "Port";
var FACTORY_UNIT = "Factory";
var MAX_AUTO_CITIES = 5;
var MAX_AUTO_PORTS = 2;
var MAX_AUTO_FACTORIES = 3;
function createAutoEco({ gameState, mapDataRef, OrigWS, origWsSend, settings, logger, roundLogger, getTroopEconomy }) {
let lastSentAt = 0;
let lastSkipAt = 0;
let lastCostLogAt = 0;
let lastKnownEcoBuildCount = 0;
let runtimeProbed = false;
let inFlight = false;
let status = { state: "idle", reason: "not_started" };
const pendingBuilds = /* @__PURE__ */ new Map();
const observedBuildCounts = /* @__PURE__ */ new Map();
const blockedTilesByUnit = /* @__PURE__ */ new Map();
return {
reset() {
lastSentAt = 0;
lastSkipAt = 0;
lastCostLogAt = 0;
lastKnownEcoBuildCount = 0;
runtimeProbed = false;
inFlight = false;
pendingBuilds.clear();
observedBuildCounts.clear();
blockedTilesByUnit.clear();
status = { state: "idle", reason: "reset" };
},
getStatus() {
return status;
},
observeIntent(intent) {
if (!intent || intent.type !== "build_unit" || !intent.unit) return;
observeBuildIntent(intent.unit, intent.tile);
},
run({ buyRecommendations = [] } = {}, options = {}) {
const state = gameState.state;
if (!options.force && !settings.get("autoEco")) return setStatus("idle", "disabled", false);
if (!OrigWS) return setStatus("idle", "websocket_unavailable", false);
if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !state.myPlayerID) {
return setStatus("idle", "not_ready", false);
}
if (!origWsSend) return setStatus("idle", "socket_unavailable", false);
if (inFlight) return setStatus("cooldown", "tile_scan", true);
expirePendingBuilds();
const now = performance.now();
const cooldownMs = lastKnownEcoBuildCount < EARLY_ECO_FREE_RESERVE_BUILDS ? EARLY_ECO_BUILD_COOLDOWN_MS : ECO_BUILD_COOLDOWN_MS;
if (now - lastSentAt < cooldownMs) {
return setStatus("cooldown", "cooldown", true, { cooldownMs: Math.round(cooldownMs - (now - lastSentAt)) });
}
const myState = gameState.getMyState();
if (!myState || !myState.isAlive) return setStatus("idle", "not_alive", false);
const gold = finiteOrZero(myState.gold);
if (gold < 5e4) {
logSkip("gold", { gold, minCost: 5e4 });
return setStatus("blocked", "gold", true, { gold, minCost: 5e4 });
}
const economy = getTroopEconomy?.();
if (isEcoUnsafe(economy)) {
logSkip("unsafe", { combatSafety: economy?.combatSafety, currentRatio: economy?.currentRatio });
return setStatus("blocked", "unsafe", true, { combatSafety: economy?.combatSafety, currentRatio: economy?.currentRatio });
}
inFlight = true;
chooseBuild({ myState, gold, buyRecommendations }).then((choice) => {
inFlight = false;
if (!choice) return;
if (gold < choice.estimatedCost + choice.reserveGold) {
logSkip("gold_cost_model", { gold, estimatedCost: choice.estimatedCost, reserveGold: choice.reserveGold, unit: choice.unit });
setStatus("blocked", "gold_cost_model", true, { gold, estimatedCost: choice.estimatedCost, reserveGold: choice.reserveGold, unit: choice.unit });
return;
}
sendBuild(choice);
}).catch((error) => {
inFlight = false;
logSkip("tile_scan_failed", { message: String(error?.message || error || "") });
setStatus("blocked", "tile_scan_failed");
});
return true;
}
};
async function chooseBuild({ myState, gold, buyRecommendations }) {
const gameView = getGameView();
if (!gameView || typeof gameView.myPlayer !== "function") {
logSkip("game_view");
setStatus("blocked", "game_view");
return null;
}
const myPlayer = gameView.myPlayer();
maybeLogRuntimeProbe(gameView, myPlayer);
const observed = observedCountsByUnit();
const unitCounts = {
city: Math.max(countPlayerUnits(myPlayer, CITY_UNIT), observed.City || 0),
port: Math.max(countPlayerUnits(myPlayer, PORT_UNIT), observed.Port || 0),
factory: Math.max(countPlayerUnits(myPlayer, FACTORY_UNIT), observed.Factory || 0)
};
const adjustedCounts = {
city: unitCounts.city + pendingCount(CITY_UNIT),
port: unitCounts.port + pendingCount(PORT_UNIT),
factory: unitCounts.factory + pendingCount(FACTORY_UNIT)
};
lastKnownEcoBuildCount = totalEcoBuildCount(adjustedCounts);
const candidates = await collectOwnedTileCandidates(gameView, myPlayer, myState, mapDataRef.current);
if (!candidates.length) {
logSkip("no_owned_tiles");
setStatus("blocked", "no_owned_tiles");
return null;
}
maybeLogCostModel(gold);
if (adjustedCounts.city <= 0) {
const tile = chooseCityTile(candidates, isTileBlockedForUnit);
if (tile != null) return withCost({ unit: CITY_UNIT, tile, unitCounts, adjustedCounts });
logPendingBlocked(CITY_UNIT);
}
if (adjustedCounts.city >= 1 && adjustedCounts.factory < MAX_AUTO_FACTORIES && adjustedCounts.factory <= adjustedCounts.port) {
const tile = chooseFactoryTile(candidates, isTileBlockedForUnit);
if (tile != null) return withCost({ unit: FACTORY_UNIT, tile, unitCounts, adjustedCounts });
logPendingBlocked(FACTORY_UNIT);
}
if (adjustedCounts.port < MAX_AUTO_PORTS) {
const tile = choosePortTile(candidates, isTileBlockedForUnit);
if (tile != null) return withCost({ unit: PORT_UNIT, tile, unitCounts, adjustedCounts });
logPendingBlocked(PORT_UNIT);
}
if (adjustedCounts.factory < MAX_AUTO_FACTORIES && adjustedCounts.city >= 1) {
const tile = chooseFactoryTile(candidates, isTileBlockedForUnit);
if (tile != null) return withCost({ unit: FACTORY_UNIT, tile, unitCounts, adjustedCounts });
logPendingBlocked(FACTORY_UNIT);
}
if (adjustedCounts.city < MAX_AUTO_CITIES && adjustedCounts.city <= adjustedCounts.port + adjustedCounts.factory + 1) {
const tile = chooseCityTile(candidates, isTileBlockedForUnit);
if (tile != null) return withCost({ unit: CITY_UNIT, tile, unitCounts, adjustedCounts });
logPendingBlocked(CITY_UNIT);
}
logSkip("no_recommended_unit", { cityCount: adjustedCounts.city, portCount: adjustedCounts.port, factoryCount: adjustedCounts.factory });
setStatus("blocked", "no_recommended_unit", false, {
cityCount: adjustedCounts.city,
portCount: adjustedCounts.port,
factoryCount: adjustedCounts.factory
});
return null;
}
function withCost(choice) {
const gameView = getGameView();
const myPlayer = safeMyPlayer2(gameView);
const model = getStructureCost(choice.unit, {
gameView,
player: myPlayer,
pendingCounts: pendingCountsByUnit(),
observedCounts: observedCountsByUnit()
});
const ecoBuildCount = totalEcoBuildCount(choice.adjustedCounts);
const reserveGold = ecoBuildCount < EARLY_ECO_FREE_RESERVE_BUILDS ? 0 : model.reserveGold;
return { ...choice, estimatedCost: model.cost, costSource: model.source, reserveGold, reserveWaived: reserveGold === 0 && model.reserveGold > 0 };
}
function sendBuild(choice) {
const state = gameState.state;
origWsSend.call(
state.gameSocket,
JSON.stringify({ type: "intent", intent: { type: "build_unit", unit: choice.unit, tile: choice.tile } })
);
lastSentAt = performance.now();
addPendingBuild(choice.unit, choice.tile);
lastKnownEcoBuildCount = totalEcoBuildCount(choice.adjustedCounts) + 1;
logger.info(\`Auto-eco build \${choice.unit} at \${choice.tile}\`);
roundLogger?.record("auto_eco_sent", {
unit: choice.unit,
tile: choice.tile,
estimatedCost: choice.estimatedCost,
costSource: choice.costSource,
cityCount: choice.unitCounts.city,
portCount: choice.unitCounts.port,
factoryCount: choice.unitCounts.factory,
adjustedCityCount: choice.adjustedCounts.city,
adjustedPortCount: choice.adjustedCounts.port,
adjustedFactoryCount: choice.adjustedCounts.factory,
reserveGold: choice.reserveGold,
reserveWaived: !!choice.reserveWaived
});
setStatus("sent", \`built_\${choice.unit}\`, true, { unit: choice.unit, tile: choice.tile, estimatedCost: choice.estimatedCost, costSource: choice.costSource });
}
function observeBuildIntent(unit, tile) {
const key = buildKey(unit, tile);
const normalized = normalizeUnitKey(unit);
if (pendingBuilds.has(key)) pendingBuilds.delete(key);
observedBuildCounts.set(normalized, (observedBuildCounts.get(normalized) || 0) + 1);
if (normalized === "city" || normalized === "port" || normalized === "factory") {
lastKnownEcoBuildCount = Math.max(lastKnownEcoBuildCount, observedEcoBuildCount());
}
blockTile(unit, tile);
}
function addPendingBuild(unit, tile) {
const duration = getConstructionDurationMs(unit, { gameView: getGameView() });
pendingBuilds.set(buildKey(unit, tile), { unit, tile, sentAt: performance.now(), ttlMs: duration.durationMs + 1500 });
blockTile(unit, tile);
}
function expirePendingBuilds() {
const now = performance.now();
pendingBuilds.forEach((entry, key) => {
if (now - entry.sentAt > (entry.ttlMs || 12e3)) pendingBuilds.delete(key);
});
}
function pendingCount(unit) {
let count = 0;
const normalized = normalizeUnitKey(unit);
pendingBuilds.forEach((entry) => {
if (normalizeUnitKey(entry.unit) === normalized) count += 1;
});
return count;
}
function maybeLogRuntimeProbe(gameView, myPlayer) {
if (runtimeProbed) return;
runtimeProbed = true;
const probe = { configType: typeof gameView?.config };
try {
const config = typeof gameView.config === "function" ? gameView.config() : null;
probe.hasConfig = !!config;
if (config) {
probe.configKeys = listMethods(config).slice(0, 40);
probe.hasUnitInfo = typeof config.unitInfo === "function";
probe.hasStructureInfo = typeof config.structureInfo === "function";
if (typeof config.unitInfo === "function") {
const info = tryCall(() => config.unitInfo(CITY_UNIT));
probe.unitInfoCityKeys = info && typeof info === "object" ? Object.keys(info).slice(0, 20) : String(info);
if (info && info.cost != null) {
probe.cityCostType = typeof info.cost;
probe.cityCostCall = String(tryCall(() => typeof info.cost === "function" ? info.cost(myPlayer) : info.cost));
}
}
}
} catch (error) {
probe.error = String(error?.message || error || "");
}
roundLogger?.record("auto_eco_runtime_probe", probe);
}
function maybeLogCostModel(gold) {
const now = performance.now();
if (now - lastCostLogAt < 1e4) return;
lastCostLogAt = now;
const gameView = getGameView();
const myPlayer = safeMyPlayer2(gameView);
const city = getStructureCost(CITY_UNIT, {
gameView,
player: myPlayer,
pendingCounts: pendingCountsByUnit(),
observedCounts: observedCountsByUnit()
});
const port = getStructureCost(PORT_UNIT, {
gameView,
player: myPlayer,
pendingCounts: pendingCountsByUnit(),
observedCounts: observedCountsByUnit()
});
const factory = getStructureCost(FACTORY_UNIT, {
gameView,
player: myPlayer,
pendingCounts: pendingCountsByUnit(),
observedCounts: observedCountsByUnit()
});
roundLogger?.record("auto_eco_cost_model", {
gold,
cityCost: city.cost,
cityCostSource: city.source,
portCost: port.cost,
portCostSource: port.source,
factoryCost: factory.cost,
factoryCostSource: factory.source,
pendingCount: pendingBuilds.size,
observedBuildCounts: Object.fromEntries(observedBuildCounts)
});
roundLogger?.record("eco_cost_model", {
gold,
cityCost: city.cost,
cityCostSource: city.source,
portCost: port.cost,
portCostSource: port.source,
factoryCost: factory.cost,
factoryCostSource: factory.source,
pendingCounts: pendingCountsByUnit(),
observedCounts: observedCountsByUnit()
});
}
function logPendingBlocked(unit) {
const blockedCount = blockedTilesByUnit.get(normalizeUnitKey(unit))?.size || 0;
if (blockedCount <= 0) return;
roundLogger?.record("auto_eco_pending_blocked", { unit, blockedCount, pendingCount: pendingCount(unit) });
}
function isTileBlockedForUnit(unit, tile) {
const blocked = blockedTilesByUnit.get(normalizeUnitKey(unit));
return !!blocked && blocked.has(Number(tile));
}
function blockTile(unit, tile) {
if (!Number.isFinite(Number(tile))) return;
const normalized = normalizeUnitKey(unit);
if (!blockedTilesByUnit.has(normalized)) blockedTilesByUnit.set(normalized, /* @__PURE__ */ new Set());
blockedTilesByUnit.get(normalized).add(Number(tile));
}
function logSkip(reason, extra = {}) {
const now = performance.now();
if (now - lastSkipAt <= 5e3) return;
lastSkipAt = now;
roundLogger?.record("auto_eco_skipped", { reason, ...extra });
}
function setStatus(state, reason, result, extra = {}) {
status = { state, reason, ...extra };
return result;
}
function pendingCountsByUnit() {
const out = {};
pendingBuilds.forEach((entry) => {
out[entry.unit] = (out[entry.unit] || 0) + 1;
});
return out;
}
function observedCountsByUnit() {
const out = {};
observedBuildCounts.forEach((count, key) => {
if (key === "city") out.City = count;
else if (key === "port") out.Port = count;
else if (key === "factory") out.Factory = count;
else if (key === "defense post") out["Defense Post"] = count;
else if (key === "sam launcher") out["SAM Launcher"] = count;
else out[key] = count;
});
return out;
}
function observedEcoBuildCount() {
return (observedBuildCounts.get("city") || 0) + (observedBuildCounts.get("port") || 0) + (observedBuildCounts.get("factory") || 0);
}
}
async function collectOwnedTileCandidates(gameView, myPlayer, myState, mapData) {
if (!myPlayer || typeof myPlayer.borderTiles !== "function") return [];
const info = await myPlayer.borderTiles();
const borders = info && info.borderTiles;
if (!borders || typeof borders.forEach !== "function") return [];
const seen = /* @__PURE__ */ new Set();
borders.forEach((tile) => addOwnedCandidate(tile, seen, gameView, myState, mapData));
Array.from(seen).forEach((tile) => {
getNeighbors(gameView, tile, mapData).forEach((nearby) => addOwnedCandidate(nearby, seen, gameView, myState, mapData));
});
return Array.from(seen).map((tile) => scoreTile(tile, gameView, myState, mapData));
}
function addOwnedCandidate(tile, seen, gameView, myState, mapData) {
if (!Number.isFinite(Number(tile))) return;
const num = Number(tile);
if (seen.has(num)) return;
if (mapData?.terrain && !isLandByte(mapData.terrain[num])) return;
if (!isOwnedByMe(gameView, num, myState)) return;
seen.add(num);
}
function chooseCityTile(candidates, isBlocked) {
const best = candidates.filter((candidate) => candidate.isLand && candidate.ownNeighbors >= 3 && !isBlocked(CITY_UNIT, candidate.tile)).sort((a, b) => b.cityScore - a.cityScore)[0];
return best ? best.tile : null;
}
function choosePortTile(candidates, isBlocked) {
const best = candidates.filter((candidate) => candidate.isLand && candidate.waterNeighbors > 0 && candidate.ownNeighbors >= 2 && !isBlocked(PORT_UNIT, candidate.tile)).sort((a, b) => b.portScore - a.portScore)[0];
return best ? best.tile : null;
}
function chooseFactoryTile(candidates, isBlocked) {
const best = candidates.filter((candidate) => candidate.isLand && candidate.ownNeighbors >= 3 && !isBlocked(FACTORY_UNIT, candidate.tile)).sort((a, b) => b.factoryScore - a.factoryScore)[0];
return best ? best.tile : null;
}
function scoreTile(tile, gameView, myState, mapData) {
const neighbors = getNeighbors(gameView, tile, mapData);
let ownNeighbors = 0;
let landNeighbors = 0;
let waterNeighbors = 0;
neighbors.forEach((nearby) => {
if (mapData?.terrain && isLandByte(mapData.terrain[nearby])) landNeighbors += 1;
else waterNeighbors += 1;
if (isOwnedByMe(gameView, nearby, myState)) ownNeighbors += 1;
});
return {
tile,
isLand: !mapData?.terrain || isLandByte(mapData.terrain[tile]),
ownNeighbors,
landNeighbors,
waterNeighbors,
cityScore: ownNeighbors * 8 + landNeighbors * 2 - waterNeighbors * 5,
portScore: waterNeighbors * 8 + ownNeighbors * 3 + landNeighbors,
factoryScore: ownNeighbors * 9 + landNeighbors * 3 - waterNeighbors * 3
};
}
function getNeighbors(gameView, tile, mapData) {
try {
if (gameView && typeof gameView.neighbors === "function") {
const neighbours = gameView.neighbors(tile);
if (Array.isArray(neighbours)) return neighbours.filter((value) => Number.isFinite(Number(value))).map(Number);
}
} catch (_) {
}
if (!mapData?.width || !mapData?.height) return [];
const width = mapData.width;
const height = mapData.height;
const x = tile % width;
const y = Math.floor(tile / width);
const out = [];
if (x > 0) out.push(tile - 1);
if (x < width - 1) out.push(tile + 1);
if (y > 0) out.push(tile - width);
if (y < height - 1) out.push(tile + width);
return out;
}
function isOwnedByMe(gameView, tile, myState) {
try {
if (!gameView || typeof gameView.owner !== "function") return false;
const owner = gameView.owner(tile);
if (!owner) return false;
const ownerID = typeof owner.id === "function" ? owner.id() : owner.id;
return ownerID === myState.id || ownerID === myState.smallID;
} catch (_) {
return false;
}
}
function isEcoUnsafe(economy) {
const currentRatio = Number(economy?.currentRatio);
if (economy?.hasThreat && Number(economy.combatSafety) < 0.6 && Number.isFinite(currentRatio) && currentRatio < 0.18) return true;
return !!(economy?.hasThreat && Number(economy.combatSafety) < 0.25 && Number(economy.currentRatio) < 0.45);
}
function safeMyPlayer2(gameView) {
try {
return gameView && typeof gameView.myPlayer === "function" ? gameView.myPlayer() : null;
} catch (_) {
}
return null;
}
function normalizeUnitKey(unit) {
return String(unit || "").trim().toLowerCase() || "unknown";
}
function listMethods(obj) {
const out = [];
try {
let cursor = obj;
while (cursor && cursor !== Object.prototype) {
Object.getOwnPropertyNames(cursor).forEach((name) => {
if (name !== "constructor" && !out.includes(name)) out.push(name);
});
cursor = Object.getPrototypeOf(cursor);
}
} catch (_) {
}
return out;
}
function tryCall(fn) {
try {
return fn();
} catch (_) {
return null;
}
}
function buildKey(unit, tile) {
return \`\${normalizeUnitKey(unit)}:\${Number(tile)}\`;
}
function totalEcoBuildCount(counts) {
return Math.max(0, Number(counts?.city) || 0) + Math.max(0, Number(counts?.port) || 0) + Math.max(0, Number(counts?.factory) || 0);
}
// src/page/automation/auto-defense.js
var DEFENSE_POST_UNIT = UNIT.DEFENSE_POST;
var SAM_UNIT = UNIT.SAM_LAUNCHER;
var INCOMING_FLOOR = 8e3;
var COUNTER_FRACTION = 0.6;
var AUTO_DEFENSE_MIN_COUNTER = 1e3;
var MIN_COMBAT_SAFETY = 0.25;
var BUILD_COOLDOWN_MS = 8e3;
var COUNTER_COOLDOWN_MS = 2500;
var PER_ATTACKER_COUNTER_FACTOR = 2.4;
var MAX_AUTO_DEFENSE_POSTS = 4;
var MAX_AUTO_SAM = 2;
var MASSIVE_HUMAN_NEIGHBOUR_RATIO = 2;
var DEFENSE_SETBACK_MIN_STEPS = 4;
var DEFENSE_SETBACK_MAX_STEPS = 8;
var DEFENSE_MIN_ENEMY_DISTANCE = 3;
var DEFENSE_MIN_OWN_NEIGHBORS = 3;
var PENDING_BUILD_FALLBACK_TTL_MS = 12e3;
var SKIP_LOG_INTERVAL_MS = 5e3;
function createAutoDefense({ gameState, mapDataRef, OrigWS, origWsSend, settings, logger, roundLogger, teamDetection, getTroopEconomy }) {
const attackerCooldowns = /* @__PURE__ */ new Map();
const pendingBuilds = /* @__PURE__ */ new Map();
const observedBuildCounts = /* @__PURE__ */ new Map();
const observedBuildKeys = /* @__PURE__ */ new Set();
let lastBuildSentAt = 0;
let lastCounterSentAt = 0;
let lastSkipAt = 0;
let buildInFlight = false;
let status = { state: "idle", reason: "not_started", emergency: false };
return {
reset() {
attackerCooldowns.clear();
pendingBuilds.clear();
observedBuildCounts.clear();
observedBuildKeys.clear();
lastBuildSentAt = 0;
lastCounterSentAt = 0;
lastSkipAt = 0;
buildInFlight = false;
status = { state: "idle", reason: "reset", emergency: false };
},
getStatus() {
return status;
},
observeIntent(intent) {
if (!intent || intent.type !== "build_unit" || !isDefenseBuildUnit(intent.unit)) return;
observeBuildIntent(intent.unit, intent.tile);
},
run(options = {}) {
const state = gameState.state;
if (!options.force && !settings.get("autoDefense")) return setStatus("idle", "disabled", { emergency: false });
if (!OrigWS) return setStatus("idle", "websocket_unavailable", { emergency: false });
if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !state.myPlayerID) {
return setStatus("idle", "not_ready", { emergency: false });
}
if (!origWsSend) return setStatus("idle", "socket_unavailable", { emergency: false });
const myState = gameState.getMyState();
if (!myState || !myState.isAlive) return setStatus("idle", "not_alive", { emergency: false });
const myTroops = finiteOrZero(myState.troops);
const incoming = incomingAttackTroops(gameState, myState);
const pressureFloor = Math.max(INCOMING_FLOOR, myTroops * (Number(settings.get("autoDefenseIncomingRatio")) || 0.35));
const underAttack = incoming > pressureFloor;
const nukeThreat = !!options.nukeThreat;
const attackers = incoming > 0 ? incomingAttackers(gameState, myState) : [];
const seriousThreat = isSeriousThreat({
myState,
attackers,
threats: options.threats,
neighbourIDs: options.neighbourIDs
});
const serious = seriousThreat.serious;
if (!serious && !nukeThreat) {
return setStatus("idle", "no_pressure", { emergency: false, incoming });
}
const economy = getTroopEconomy?.();
let lastAction = null;
if (serious && underAttack && settings.get("autoDefenseCounterAttack")) {
const counter = maybeCounterAttack({ myState, economy, attackers });
if (counter) lastAction = counter;
}
maybeBuildDefenses({ myState, nukeThreat, buildPost: serious });
return setStatus(serious ? "emergency" : "alert", serious ? "under_attack" : "nuke_threat", {
emergency: serious,
incoming,
nukeThreat,
attackerCount: attackers.length,
topAttacker: attackers[0]?.name || null,
seriousReason: seriousThreat.reason || null,
seriousThreat: seriousThreat.name || null,
effectiveTroopsRatio: seriousThreat.effectiveTroopsRatio || null,
lastAction: lastAction || status.lastAction || null
});
}
};
function isSeriousThreat({ myState, attackers, threats, neighbourIDs }) {
const humanAttacker = (attackers || []).find((attacker) => {
const player = gameState.state.playerStates.get(attacker.id);
if (!player || String(player.playerType || "").toUpperCase() !== "HUMAN") return false;
if (isAllied2(myState, player)) return false;
if (teamDetection?.isMyTeammate(player.id)) return false;
return true;
});
if (humanAttacker) return { serious: true, reason: "active_human_attacker", id: humanAttacker.id, name: humanAttacker.name || humanAttacker.id };
const myRecallableTroops = Math.max(1, finiteOrZero(myState.troops) + troopsInCombat(myState));
const massiveNeighbour = (threats || []).find((threat) => {
if (!threat || threat.id == null) return false;
if (!neighbourIDs || !neighbourIDs.has(threat.id)) return false;
const player = gameState.state.playerStates.get(threat.id);
if (!isHostileHuman(myState, player, teamDetection)) return false;
const effectiveEnemyTroops = Math.max(0, finiteOrZero(player.troops) - troopsInCombat(player));
return effectiveEnemyTroops / myRecallableTroops >= MASSIVE_HUMAN_NEIGHBOUR_RATIO;
});
if (massiveNeighbour) {
const player = gameState.state.playerStates.get(massiveNeighbour.id);
const effectiveEnemyTroops = Math.max(0, finiteOrZero(player?.troops) - troopsInCombat(player));
return {
serious: true,
reason: "massive_human_neighbour",
id: massiveNeighbour.id,
name: massiveNeighbour.name || massiveNeighbour.id,
effectiveTroopsRatio: effectiveEnemyTroops / myRecallableTroops
};
}
return { serious: false, reason: null };
}
function maybeCounterAttack({ myState, economy, attackers }) {
if (!attackers.length) return null;
const now = performance.now();
if (now - lastCounterSentAt < COUNTER_COOLDOWN_MS) return null;
const combatSafety = Number(economy?.combatSafety);
if (Number.isFinite(combatSafety) && combatSafety < MIN_COMBAT_SAFETY) {
logSkip("counter_unsafe", { combatSafety });
return null;
}
const safeSpendable = finiteOrZero(economy?.safeSpendableTroops);
if (safeSpendable <= AUTO_DEFENSE_MIN_COUNTER) {
logSkip("counter_no_troops", { safeSpendable });
return null;
}
for (let i = 0; i < attackers.length; i += 1) {
const attacker = attackers[i];
const gate = validateCounter(attacker, myState);
if (!gate.ok) {
logSkip("counter_blocked", { reason: gate.reason, attacker: attacker.name });
continue;
}
const lastForAttacker = attackerCooldowns.get(attacker.id) || 0;
if (now - lastForAttacker < COUNTER_COOLDOWN_MS * PER_ATTACKER_COUNTER_FACTOR) continue;
const troops = Math.max(1, Math.floor(Math.min(safeSpendable * COUNTER_FRACTION, safeSpendable)));
origWsSend.call(
gameState.state.gameSocket,
JSON.stringify({ type: "intent", intent: { type: "attack", targetID: attacker.id, troops } })
);
lastCounterSentAt = now;
attackerCooldowns.set(attacker.id, now);
logger.info(\`Auto-defense counter \${attacker.name}: \${troops} troops\`);
roundLogger?.record("auto_defense_counter_sent", {
targetID: attacker.id,
targetName: attacker.name,
attackerTroops: attacker.troops,
troops,
safeSpendable: Math.floor(safeSpendable),
combatSafety
});
return \`counter \${attacker.name}\`;
}
return null;
}
function maybeBuildDefenses({ myState, nukeThreat, buildPost }) {
if (buildInFlight) return;
const now = performance.now();
if (now - lastBuildSentAt < BUILD_COOLDOWN_MS) return;
expirePendingBuilds();
const gameView = getGameView();
if (!gameView || typeof gameView.myPlayer !== "function") {
logSkip("build_no_game_view");
return;
}
const myPlayer = safeMyPlayer3(gameView);
if (!myPlayer) {
logSkip("build_no_player");
return;
}
const wantSam = settings.get("autoDefenseBuildSam") && nukeThreat && canBuild(gameView, myPlayer, myState, SAM_UNIT, MAX_AUTO_SAM);
const wantPost = buildPost && settings.get("autoDefenseBuildPosts") && canBuild(gameView, myPlayer, myState, DEFENSE_POST_UNIT, MAX_AUTO_DEFENSE_POSTS);
const plan = wantSam ? { unit: SAM_UNIT, chooseTile: chooseCentralTile } : wantPost ? { unit: DEFENSE_POST_UNIT, chooseTile: chooseFrontierTile } : null;
if (!plan) return;
buildInFlight = true;
plan.chooseTile(gameView, myPlayer, myState, mapDataRef.current).then((tile) => {
buildInFlight = false;
if (tile == null) {
logSkip("build_no_tile", { unit: plan.unit });
return;
}
sendBuild(plan.unit, tile);
}).catch((error) => {
buildInFlight = false;
logSkip("build_tile_scan_failed", { message: String(error?.message || error || "") });
});
}
function canBuild(gameView, myPlayer, myState, unit, cap) {
const runtimeCount = countPlayerUnits(myPlayer, unit);
const observedCount = observedCountOf(unit);
const pendingCount = pendingCountOf(unit);
const existing = Math.max(runtimeCount, observedCount) + pendingCount;
if (existing >= cap) {
logSkip("build_max", { unit, existing, runtimeCount, observedCount, pendingCount });
return false;
}
const cost = getStructureCost(unit, {
gameView,
player: myPlayer,
pendingCounts: pendingCountsByUnit(),
observedCounts: observedCountsByUnit()
});
const gold = finiteOrZero(myState.gold);
if (gold < cost.cost + cost.reserveGold) {
logSkip("build_gold", { unit, gold, estimatedCost: cost.cost });
return false;
}
return true;
}
function sendBuild(unit, tile) {
const gameView = getGameView();
const cost = getStructureCost(unit, {
gameView,
player: safeMyPlayer3(gameView),
pendingCounts: pendingCountsByUnit(),
observedCounts: observedCountsByUnit()
});
origWsSend.call(
gameState.state.gameSocket,
JSON.stringify({ type: "intent", intent: { type: "build_unit", unit, tile } })
);
lastBuildSentAt = performance.now();
addPendingBuild(unit, tile);
logger.info(\`Auto-defense build \${unit} at \${tile}\`);
roundLogger?.record("auto_defense_build_sent", {
unit,
tile,
estimatedCost: cost.cost,
costSource: cost.source,
pendingCounts: pendingCountsByUnit(),
observedCounts: observedCountsByUnit()
});
}
function validateCounter(attacker, myState) {
const player = gameState.state.playerStates.get(attacker.id);
if (!player || !player.isAlive) return { ok: false, reason: "target_not_alive" };
if (String(player.playerType || "").toUpperCase() !== "HUMAN") return { ok: false, reason: "target_not_human" };
if (isAllied2(myState, player)) return { ok: false, reason: "target_allied" };
if (teamDetection?.isMyTeammate(player.id)) return { ok: false, reason: "target_team" };
return { ok: true };
}
function addPendingBuild(unit, tile) {
const duration = getConstructionDurationMs(unit, { gameView: getGameView() });
pendingBuilds.set(Number(tile), { unit, sentAt: performance.now(), ttlMs: duration.durationMs + 1500 });
}
function observeBuildIntent(unit, tile) {
const key = buildKey2(unit, tile);
if (observedBuildKeys.has(key)) return;
observedBuildKeys.add(key);
if (Number.isFinite(Number(tile))) pendingBuilds.delete(Number(tile));
observedBuildCounts.set(unit, observedCountOf(unit) + 1);
}
function expirePendingBuilds() {
const now = performance.now();
pendingBuilds.forEach((entry, tile) => {
if (now - entry.sentAt > (entry.ttlMs || PENDING_BUILD_FALLBACK_TTL_MS)) pendingBuilds.delete(tile);
});
}
function pendingCountOf(unit) {
let count = 0;
pendingBuilds.forEach((entry) => {
if (entry.unit === unit) count += 1;
});
return count;
}
function observedCountOf(unit) {
return observedBuildCounts.get(unit) || 0;
}
function pendingCountsByUnit() {
const out = {};
pendingBuilds.forEach((entry) => {
out[entry.unit] = (out[entry.unit] || 0) + 1;
});
return out;
}
function observedCountsByUnit() {
return Object.fromEntries(observedBuildCounts);
}
function isPendingTile(tile) {
return pendingBuilds.has(Number(tile));
}
function chooseFrontierTile(gameView, myPlayer, myState, mapData) {
return scanBorderTiles(
gameView,
myPlayer,
myState,
mapData,
(tile) => countEnemyNeighbors(gameView, tile, myState, mapData, teamDetection, gameState),
1
).then((frontier) => frontier == null ? null : chooseSafeDefenseTile(gameView, frontier, myState, mapData));
}
function chooseSafeDefenseTile(gameView, frontier, myState, mapData) {
const candidates = collectSetbackCandidates(gameView, frontier, myState, mapData);
const preferred = candidates.filter((candidate) => candidate.safe);
const pool = preferred.length ? preferred : candidates.filter((candidate) => candidate.relaxed);
const best = pool.sort((a, b) => b.score - a.score)[0];
return best ? best.tile : null;
}
function collectSetbackCandidates(gameView, frontier, myState, mapData) {
const out = [];
const visited = /* @__PURE__ */ new Set([Number(frontier)]);
const queue = [{ tile: Number(frontier), distance: 0 }];
for (let i = 0; i < queue.length; i += 1) {
const current = queue[i];
if (current.distance >= DEFENSE_SETBACK_MAX_STEPS) continue;
getNeighbors2(gameView, current.tile, mapData).forEach((nearby) => {
const tile = Number(nearby);
if (!Number.isFinite(tile) || visited.has(tile)) return;
visited.add(tile);
if (mapData?.terrain && !isLandByte(mapData.terrain[tile])) return;
if (!isOwnedByMe2(gameView, tile, myState)) return;
const distance = current.distance + 1;
queue.push({ tile, distance });
if (isPendingTile(tile)) return;
const enemyNeighbors = countEnemyNeighbors(gameView, tile, myState, mapData, teamDetection, gameState);
if (enemyNeighbors > 0 || distance < 3) return;
const ownNeighbors = countOwnNeighbors(gameView, tile, myState, mapData);
const enemyDistance = nearestEnemyDistance(gameView, tile, myState, mapData, teamDetection, gameState, DEFENSE_SETBACK_MAX_STEPS);
const safe = distance >= DEFENSE_SETBACK_MIN_STEPS && ownNeighbors >= DEFENSE_MIN_OWN_NEIGHBORS && enemyDistance >= DEFENSE_MIN_ENEMY_DISTANCE;
const relaxed = ownNeighbors >= 2 && enemyDistance >= 2;
if (!safe && !relaxed) return;
out.push({
tile,
distance,
ownNeighbors,
enemyDistance,
safe,
relaxed,
score: enemyDistance * 24 + ownNeighbors * 10 - Math.abs(distance - 5) * 6
});
});
}
return out;
}
function chooseCentralTile(gameView, myPlayer, myState, mapData) {
return scanBorderTiles(gameView, myPlayer, myState, mapData, (tile) => countOwnNeighbors(gameView, tile, myState, mapData), 0);
}
function scanBorderTiles(gameView, myPlayer, myState, mapData, scoreFn, minScore) {
return Promise.resolve().then(() => typeof myPlayer.borderTiles === "function" ? myPlayer.borderTiles() : null).then((info) => {
const borders = info && info.borderTiles;
if (!borders || typeof borders.forEach !== "function") return null;
let best = null;
let bestScore = minScore - 1;
borders.forEach((tile) => {
const num = Number(tile);
if (!Number.isFinite(num) || isPendingTile(num)) return;
if (mapData?.terrain && !isLandByte(mapData.terrain[num])) return;
if (!isOwnedByMe2(gameView, num, myState)) return;
const value = scoreFn(num);
if (value > bestScore) {
bestScore = value;
best = num;
}
});
return bestScore >= minScore ? best : null;
});
}
function logSkip(reason, extra = {}) {
const now = performance.now();
if (now - lastSkipAt < SKIP_LOG_INTERVAL_MS) return;
lastSkipAt = now;
roundLogger?.record("auto_defense_skipped", { reason, ...extra });
}
function setStatus(state, reason, extra = {}) {
status = { state, reason, ...extra };
return status;
}
}
function countEnemyNeighbors(gameView, tile, myState, mapData, teamDetection, gameState) {
let count = 0;
getNeighbors2(gameView, tile, mapData).forEach((nearby) => {
if (mapData?.terrain && !isLandByte(mapData.terrain[nearby])) return;
if (isEnemyOwned(gameView, nearby, myState, teamDetection, gameState)) count += 1;
});
return count;
}
function countOwnNeighbors(gameView, tile, myState, mapData) {
let count = 0;
getNeighbors2(gameView, tile, mapData).forEach((nearby) => {
if (mapData?.terrain && !isLandByte(mapData.terrain[nearby])) return;
if (isOwnedByMe2(gameView, nearby, myState)) count += 1;
});
return count;
}
function nearestEnemyDistance(gameView, startTile, myState, mapData, teamDetection, gameState, maxDistance) {
const start = Number(startTile);
if (!Number.isFinite(start)) return maxDistance + 1;
const visited = /* @__PURE__ */ new Set([start]);
const queue = [{ tile: start, distance: 0 }];
for (let i = 0; i < queue.length; i += 1) {
const current = queue[i];
if (current.distance >= maxDistance) continue;
const nextDistance = current.distance + 1;
const neighbours = getNeighbors2(gameView, current.tile, mapData);
for (let n = 0; n < neighbours.length; n += 1) {
const nearby = Number(neighbours[n]);
if (!Number.isFinite(nearby) || visited.has(nearby)) continue;
visited.add(nearby);
if (mapData?.terrain && !isLandByte(mapData.terrain[nearby])) continue;
if (isEnemyOwned(gameView, nearby, myState, teamDetection, gameState)) return nextDistance;
queue.push({ tile: nearby, distance: nextDistance });
}
}
return maxDistance + 1;
}
function isEnemyOwned(gameView, tile, myState, teamDetection, gameState) {
try {
if (!gameView || typeof gameView.owner !== "function") return false;
const owner = gameView.owner(tile);
if (!owner) return false;
const isPlayer = typeof owner.isPlayer === "function" ? owner.isPlayer() : true;
if (!isPlayer) return false;
const ownerID = typeof owner.id === "function" ? owner.id() : owner.id;
if (ownerID == null) return false;
if (ownerID === myState.id || ownerID === myState.smallID) return false;
const player = resolvePlayerByOwnerID(gameState, ownerID);
return isHostileHuman(myState, player, teamDetection);
} catch (_) {
return false;
}
}
function isOwnedByMe2(gameView, tile, myState) {
try {
if (!gameView || typeof gameView.owner !== "function") return false;
const owner = gameView.owner(tile);
if (!owner) return false;
const ownerID = typeof owner.id === "function" ? owner.id() : owner.id;
return ownerID === myState.id || ownerID === myState.smallID;
} catch (_) {
return false;
}
}
function getNeighbors2(gameView, tile, mapData) {
try {
if (gameView && typeof gameView.neighbors === "function") {
const neighbours = gameView.neighbors(tile);
if (Array.isArray(neighbours)) return neighbours.filter((value) => Number.isFinite(Number(value))).map(Number);
}
} catch (_) {
}
if (!mapData?.width || !mapData?.height) return [];
const width = mapData.width;
const height = mapData.height;
const x = tile % width;
const y = Math.floor(tile / width);
const out = [];
if (x > 0) out.push(tile - 1);
if (x < width - 1) out.push(tile + 1);
if (y > 0) out.push(tile - width);
if (y < height - 1) out.push(tile + width);
return out;
}
function isAllied2(myState, target) {
const alliances = Array.isArray(myState.alliances) ? myState.alliances : [];
return alliances.some((alliance) => alliance && alliance.other === target.id);
}
function isHostileHuman(myState, player, teamDetection) {
if (!player || !player.isAlive) return false;
if (String(player.playerType || "").toUpperCase() !== "HUMAN") return false;
if (isAllied2(myState, player)) return false;
if (teamDetection?.isMyTeammate(player.id)) return false;
return true;
}
function resolvePlayerByOwnerID(gameState, ownerID) {
if (!gameState || ownerID == null) return null;
const direct = gameState.state.playerStates.get(ownerID);
if (direct) return direct;
let found = null;
gameState.state.playerStates.forEach((player) => {
if (!found && player && player.smallID === ownerID) found = player;
});
return found;
}
function safeMyPlayer3(gameView) {
try {
return gameView && typeof gameView.myPlayer === "function" ? gameView.myPlayer() : null;
} catch (_) {
}
return null;
}
function isDefenseBuildUnit(unit) {
return unit === DEFENSE_POST_UNIT || unit === SAM_UNIT;
}
function buildKey2(unit, tile) {
return \`\${String(unit || "").trim().toLowerCase()}:\${Number(tile)}\`;
}
// src/page/automation/auto-boat.js
var BOAT_COOLDOWN_MS = 4e3;
var BOAT_WAVE_RATIO = 0.15;
var MIN_BOAT_TROOPS = 1;
var MIN_SURPLUS = 500;
var BFS_TILE_CAP = 6e3;
var MAX_BOAT_DISTANCE = 90;
function createAutoBoat({ gameState, mapDataRef, OrigWS, origWsSend, settings, logger, roundLogger, getTroopEconomy }) {
let lastSentAt = 0;
let lastSkipAt = 0;
let inFlight = false;
let lastDst = -1;
let status = { state: "idle", reason: "not_started" };
return {
reset() {
lastSentAt = 0;
lastSkipAt = 0;
inFlight = false;
lastDst = -1;
status = { state: "idle", reason: "reset" };
},
getStatus() {
return status;
},
run(options = {}) {
const state = gameState.state;
if (!options.force && !settings.get("autoBoat")) return setStatus("idle", "disabled");
if (!OrigWS) return setStatus("idle", "websocket_unavailable");
if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !state.myPlayerID) {
return setStatus("idle", "not_ready");
}
if (!origWsSend) return setStatus("idle", "socket_unavailable");
if (!isInsideFarmWindow(state, settings)) return setStatus("idle", "outside_window");
if (inFlight) return setStatus("cooldown", "tile_scan");
const now = performance.now();
if (now - lastSentAt < BOAT_COOLDOWN_MS) {
return setStatus("cooldown", "cooldown", { cooldownMs: Math.round(BOAT_COOLDOWN_MS - (now - lastSentAt)) });
}
const myState = gameState.getMyState();
if (!myState || !myState.isAlive) return setStatus("idle", "not_alive");
const economy = getTroopEconomy?.();
const safeSpendable = finiteOrZero(economy?.safeSpendableTroops);
const maxTroops = finiteOrZero(economy?.maxTroops);
if (safeSpendable <= MIN_SURPLUS || maxTroops <= 0) {
logSkip("reserve_limit", { safeSpendable });
return setStatus("blocked", "reserve_limit", { safeSpendable });
}
const troops = Math.max(MIN_BOAT_TROOPS, Math.floor(Math.min(safeSpendable, maxTroops * BOAT_WAVE_RATIO)));
const gameView = getGameView();
if (!gameView || typeof gameView.myPlayer !== "function") {
logSkip("no_game_view");
return setStatus("blocked", "no_game_view");
}
const myPlayer = safeMyPlayer4(gameView);
if (!myPlayer) {
logSkip("no_player");
return setStatus("blocked", "no_player");
}
inFlight = true;
chooseBoatDst(gameView, myPlayer, mapDataRef.current).then((dst) => {
inFlight = false;
if (dst == null) {
logSkip("no_water_target");
setStatus("blocked", "no_water_target");
return;
}
origWsSend.call(
gameState.state.gameSocket,
JSON.stringify({ type: "intent", intent: { type: "boat", troops, dst } })
);
lastSentAt = performance.now();
lastDst = dst;
logger.info(\`Auto-boat expand: \${troops} troops -> tile \${dst}\`);
roundLogger?.record("auto_boat_sent", { dst, troops, safeSpendable: Math.floor(safeSpendable) });
setStatus("sent", \`sent_\${troops}\`, { dst, troops });
}).catch((error) => {
inFlight = false;
logSkip("tile_scan_failed", { message: String(error?.message || error || "") });
setStatus("blocked", "tile_scan_failed");
});
return setStatus("scanning", "scanning");
}
};
function logSkip(reason, extra = {}) {
const now = performance.now();
if (now - lastSkipAt < 5e3) return;
lastSkipAt = now;
roundLogger?.record("auto_boat_skipped", { reason, ...extra });
}
function setStatus(state, reason, extra = {}) {
status = { state, reason, ...extra };
return status;
}
}
function chooseBoatDst(gameView, myPlayer, mapData) {
return Promise.resolve().then(() => typeof myPlayer.borderTiles === "function" ? myPlayer.borderTiles() : null).then((info) => {
const borders = info && info.borderTiles;
if (!borders || typeof borders.forEach !== "function") return null;
const width = mapData?.width;
const height = mapData?.height;
const terrain = mapData?.terrain;
if (!width || !height || !terrain) return null;
const visited = /* @__PURE__ */ new Set();
const queue = [];
borders.forEach((tile) => {
const num = Number(tile);
if (!Number.isFinite(num)) return;
neighborsOf(num, width, height).forEach((nb) => {
if (!isLandByte(terrain[nb]) && !visited.has(nb)) {
visited.add(nb);
queue.push({ tile: nb, dist: 1 });
}
});
});
let head = 0;
while (head < queue.length && visited.size < BFS_TILE_CAP) {
const { tile, dist } = queue[head];
head += 1;
if (dist > MAX_BOAT_DISTANCE) continue;
const nbs = neighborsOf(tile, width, height);
for (let i = 0; i < nbs.length; i += 1) {
const nb = nbs[i];
if (visited.has(nb)) continue;
if (isLandByte(terrain[nb])) {
if (isUnclaimedLand(gameView, nb)) return nb;
visited.add(nb);
} else {
visited.add(nb);
queue.push({ tile: nb, dist: dist + 1 });
}
}
}
return null;
});
}
function isUnclaimedLand(gameView, tile) {
try {
if (typeof gameView.hasOwner === "function") return !gameView.hasOwner(tile);
const owner = typeof gameView.owner === "function" ? gameView.owner(tile) : null;
if (!owner) return true;
return typeof owner.isPlayer === "function" ? !owner.isPlayer() : false;
} catch (_) {
return false;
}
}
function neighborsOf(tile, width, height) {
const x = tile % width;
const y = Math.floor(tile / width);
const out = [];
if (x > 0) out.push(tile - 1);
if (x < width - 1) out.push(tile + 1);
if (y > 0) out.push(tile - width);
if (y < height - 1) out.push(tile + width);
return out;
}
function safeMyPlayer4(gameView) {
try {
return gameView && typeof gameView.myPlayer === "function" ? gameView.myPlayer() : null;
} catch (_) {
}
return null;
}
// src/page/automation/auto-weapons.js
var SILO_UNIT = UNIT.MISSILE_SILO;
var SILO_BUILD_COOLDOWN_MS = 15e3;
var LAUNCH_COOLDOWN_MS = 12e3;
var EARLY_NUKE_COOLDOWN_MS = 8e3;
var MIN_COMBAT_SAFETY_BUILD = 0.6;
var MIN_COMBAT_SAFETY_LAUNCH = 0.45;
var MIN_COMBAT_SAFETY_EARLY = 0.3;
var MAX_AUTO_SILOS = 1;
var STABLE_ECO_STATES = /* @__PURE__ */ new Set(["GROWTH_PEAK", "READY", "PUSH", "CAP_WASTE"]);
function createAutoWeapons({ gameState, mapDataRef, OrigWS, origWsSend, settings, logger, roundLogger, teamDetection, getTroopEconomy }) {
const pendingSilos = /* @__PURE__ */ new Map();
let lastSiloAt = 0;
let lastLaunchAt = 0;
let lastEarlyNukeAt = 0;
let lastSkipAt = 0;
let buildInFlight = false;
let status = { state: "idle", reason: "not_started" };
return {
reset() {
pendingSilos.clear();
lastSiloAt = 0;
lastLaunchAt = 0;
lastEarlyNukeAt = 0;
lastSkipAt = 0;
buildInFlight = false;
status = { state: "idle", reason: "reset" };
},
getStatus() {
return status;
},
run({ threats } = {}, options = {}) {
const state = gameState.state;
if (!options.force && !settings.get("autoWeapons")) return setStatus("idle", "disabled");
if (!OrigWS) return setStatus("idle", "websocket_unavailable");
if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !state.myPlayerID) {
return setStatus("idle", "not_ready");
}
if (!origWsSend) return setStatus("idle", "socket_unavailable");
if (buildInFlight) return setStatus("cooldown", "tile_scan");
const myState = gameState.getMyState();
if (!myState || !myState.isAlive) return setStatus("idle", "not_alive");
const economy = getTroopEconomy?.();
const gameView = getGameView();
if (!gameView || typeof gameView.myPlayer !== "function") return setStatus("blocked", "no_game_view");
const myPlayer = safeMyPlayer5(gameView);
if (!myPlayer) return setStatus("blocked", "no_player");
expirePendingSilos();
const builtSilos = countPlayerUnits(myPlayer, SILO_UNIT);
const siloCount = builtSilos + pendingSilos.size;
if (settings.get("autoWeaponsEarlyNuke") && builtSilos >= 1) {
const early = maybeEarlyNuke({ myState, economy, gameView, myPlayer, threats });
if (early) return early;
}
if (!economy || !STABLE_ECO_STATES.has(economy.state)) {
return setStatus("idle", "eco_unstable", { ecoState: economy?.state || null });
}
if (siloCount < MAX_AUTO_SILOS) {
return maybeBuildSilo({ myState, economy, gameView, myPlayer });
}
return maybeLaunch({ myState, economy, gameView, myPlayer, threats });
}
};
function maybeEarlyNuke({ myState, economy, gameView, myPlayer, threats }) {
const now = performance.now();
if (now - lastEarlyNukeAt < EARLY_NUKE_COOLDOWN_MS) return null;
if (finiteOrZero(economy?.combatSafety) < MIN_COMBAT_SAFETY_EARLY) return null;
const sams = enemySamSitesUnderConstruction(gameView, myState, teamDetection);
if (!sams.length) return null;
const cost = getStructureCost(UNIT.ATOM_BOMB, { gameView, player: myPlayer });
if (cost.source !== "runtime") {
logSkip("early_no_atom_cost");
return null;
}
const gold = finiteOrZero(myState.gold);
if (gold < cost.cost + cost.reserveGold) {
logSkip("early_atom_gold", { gold, estimatedCost: cost.cost });
return null;
}
const threatRank = /* @__PURE__ */ new Map();
(threats || []).forEach((threat, index) => {
if (threat && threat.id != null) threatRank.set(threat.id, index);
});
sams.sort((a, b) => rankOf(threatRank, a.ownerID) - rankOf(threatRank, b.ownerID));
const target = sams[0];
origWsSend.call(
gameState.state.gameSocket,
JSON.stringify({ type: "intent", intent: { type: "build_unit", unit: UNIT.ATOM_BOMB, tile: target.tile } })
);
lastEarlyNukeAt = now;
lastLaunchAt = now;
logger.info(\`Auto-weapons EARLY NUKE \${target.ownerName} SAM @\${target.tile}\`);
roundLogger?.record("auto_weapons_early_nuke_sent", {
unit: UNIT.ATOM_BOMB,
tile: target.tile,
targetName: target.ownerName,
targetID: target.ownerID,
estimatedCost: cost.cost
});
return setStatus("sent", "early_nuke", { unit: UNIT.ATOM_BOMB, target: target.ownerName });
}
function maybeBuildSilo({ myState, economy, gameView, myPlayer }) {
const now = performance.now();
if (now - lastSiloAt < SILO_BUILD_COOLDOWN_MS) return setStatus("cooldown", "silo_cooldown");
if (finiteOrZero(economy.combatSafety) < MIN_COMBAT_SAFETY_BUILD) return setStatus("idle", "unsafe_flank");
const cost = getStructureCost(SILO_UNIT, { gameView, player: myPlayer, pendingCounts: { [SILO_UNIT]: pendingSilos.size } });
const gold = finiteOrZero(myState.gold);
if (gold < cost.cost + cost.reserveGold) {
logSkip("silo_gold", { gold, estimatedCost: cost.cost });
return setStatus("blocked", "silo_gold", { gold, estimatedCost: cost.cost });
}
buildInFlight = true;
chooseCentralTile(gameView, myPlayer, myState, mapDataRef.current).then((tile) => {
buildInFlight = false;
if (tile == null) {
logSkip("silo_no_tile");
return;
}
origWsSend.call(
gameState.state.gameSocket,
JSON.stringify({ type: "intent", intent: { type: "build_unit", unit: SILO_UNIT, tile } })
);
lastSiloAt = performance.now();
addPendingSilo(tile);
logger.info(\`Auto-weapons build Missile Silo at \${tile}\`);
roundLogger?.record("auto_weapons_silo_sent", { tile, estimatedCost: cost.cost, costSource: cost.source });
}).catch((error) => {
buildInFlight = false;
logSkip("silo_tile_scan_failed", { message: String(error?.message || error || "") });
});
return setStatus("scanning", "silo_scan");
}
function maybeLaunch({ myState, economy, gameView, myPlayer, threats }) {
const now = performance.now();
if (now - lastLaunchAt < LAUNCH_COOLDOWN_MS) return setStatus("cooldown", "launch_cooldown");
if (finiteOrZero(economy.combatSafety) < MIN_COMBAT_SAFETY_LAUNCH) return setStatus("idle", "unsafe_launch");
const weapon = chooseAffordableWeapon(gameView, myPlayer, finiteOrZero(myState.gold));
if (!weapon) {
logSkip("no_affordable_weapon");
return setStatus("idle", "no_affordable_weapon");
}
buildInFlight = true;
chooseNukeTarget(gameView, myPlayer, myState, threats || []).then((target) => {
buildInFlight = false;
if (!target) {
logSkip("no_target");
setStatus("idle", "no_target");
return;
}
origWsSend.call(
gameState.state.gameSocket,
JSON.stringify({ type: "intent", intent: { type: "build_unit", unit: weapon.unit, tile: target.tile } })
);
lastLaunchAt = performance.now();
logger.info(\`Auto-weapons launch \${weapon.unit} -> \${target.targetName} @\${target.tile}\`);
roundLogger?.record("auto_weapons_launch_sent", {
unit: weapon.unit,
tile: target.tile,
targetName: target.targetName,
targetID: target.targetID,
estimatedCost: weapon.cost
});
setStatus("sent", \`launch_\${weapon.unit}\`, { unit: weapon.unit, target: target.targetName });
}).catch((error) => {
buildInFlight = false;
logSkip("target_scan_failed", { message: String(error?.message || error || "") });
});
return setStatus("scanning", "launch_scan");
}
function chooseAffordableWeapon(gameView, myPlayer, gold) {
for (let i = 0; i < WEAPONS_BY_POWER.length; i += 1) {
const unit = WEAPONS_BY_POWER[i];
const cost = getStructureCost(unit, { gameView, player: myPlayer });
if (cost.source !== "runtime") continue;
if (gold >= cost.cost + cost.reserveGold) return { unit, cost: cost.cost };
}
return null;
}
function chooseNukeTarget(gameView, myPlayer, myState, threats) {
const threatRank = /* @__PURE__ */ new Map();
(threats || []).forEach((threat, index) => {
if (threat && threat.id != null) threatRank.set(threat.id, index);
});
return Promise.resolve().then(() => typeof myPlayer.borderTiles === "function" ? myPlayer.borderTiles() : null).then((info) => {
const borders = info && info.borderTiles;
if (!borders || typeof borders.forEach !== "function") return null;
const enemyTile = /* @__PURE__ */ new Map();
const enemyName = /* @__PURE__ */ new Map();
borders.forEach((tile2) => {
const num = Number(tile2);
if (!Number.isFinite(num)) return;
neighborsOf2(num, mapDataRef.current).forEach((nb) => {
const owner = ownerOf(gameView, nb);
if (!owner || owner.id == null) return;
if (owner.id === myState.id || owner.id === myState.smallID) return;
if (teamDetection?.isMyTeammate?.(owner.id)) return;
if (isAllied3(myState, owner.id)) return;
if (!enemyTile.has(owner.id)) {
enemyTile.set(owner.id, nb);
enemyName.set(owner.id, owner.name);
}
});
});
if (enemyTile.size === 0) return null;
let bestID = null;
let bestRank = Infinity;
enemyTile.forEach((_tile, id) => {
const rank = threatRank.has(id) ? threatRank.get(id) : 999;
if (rank < bestRank) {
bestRank = rank;
bestID = id;
}
});
if (bestID == null) return null;
const boundaryTile = enemyTile.get(bestID);
const tile = stepIntoTerritory(gameView, boundaryTile, bestID) ?? boundaryTile;
return { tile, targetID: bestID, targetName: enemyName.get(bestID) || bestID };
});
}
function stepIntoTerritory(gameView, tile, ownerID) {
const nbs = neighborsOf2(tile, mapDataRef.current);
for (let i = 0; i < nbs.length; i += 1) {
const owner = ownerOf(gameView, nbs[i]);
if (owner && owner.id === ownerID) return nbs[i];
}
return null;
}
function chooseCentralTile(gameView, myPlayer, myState, mapData) {
return Promise.resolve().then(() => typeof myPlayer.borderTiles === "function" ? myPlayer.borderTiles() : null).then((info) => {
const borders = info && info.borderTiles;
if (!borders || typeof borders.forEach !== "function") return null;
let best = null;
let bestOwn = -1;
borders.forEach((tile) => {
const num = Number(tile);
if (!Number.isFinite(num) || pendingSilos.has(num)) return;
if (mapData?.terrain && !isLandByte(mapData.terrain[num])) return;
if (!isOwnedByMe3(gameView, num, myState)) return;
let own = 0;
neighborsOf2(num, mapData).forEach((nb) => {
if (isOwnedByMe3(gameView, nb, myState)) own += 1;
});
if (own > bestOwn) {
bestOwn = own;
best = num;
}
});
return best;
});
}
function addPendingSilo(tile) {
const duration = getConstructionDurationMs(SILO_UNIT, { gameView: getGameView() });
pendingSilos.set(Number(tile), { sentAt: performance.now(), ttlMs: duration.durationMs + 1500 });
}
function expirePendingSilos() {
const now = performance.now();
pendingSilos.forEach((entry, tile) => {
if (now - entry.sentAt > (entry.ttlMs || 15e3)) pendingSilos.delete(tile);
});
}
function logSkip(reason, extra = {}) {
const now = performance.now();
if (now - lastSkipAt < 5e3) return;
lastSkipAt = now;
roundLogger?.record("auto_weapons_skipped", { reason, ...extra });
}
function setStatus(state, reason, extra = {}) {
status = { state, reason, ...extra };
return status;
}
}
function rankOf(rankMap, id) {
return rankMap.has(id) ? rankMap.get(id) : 999;
}
function ownerOf(gameView, tile) {
try {
if (!gameView || typeof gameView.owner !== "function") return null;
const owner = gameView.owner(tile);
if (!owner) return null;
const isPlayer = typeof owner.isPlayer === "function" ? owner.isPlayer() : true;
if (!isPlayer) return null;
const id = typeof owner.id === "function" ? owner.id() : owner.id;
const name = typeof owner.displayName === "function" ? owner.displayName() : typeof owner.name === "function" ? owner.name() : id;
return { id, name };
} catch (_) {
return null;
}
}
function isOwnedByMe3(gameView, tile, myState) {
try {
if (!gameView || typeof gameView.owner !== "function") return false;
const owner = gameView.owner(tile);
if (!owner) return false;
const ownerID = typeof owner.id === "function" ? owner.id() : owner.id;
return ownerID === myState.id || ownerID === myState.smallID;
} catch (_) {
return false;
}
}
function isAllied3(myState, ownerID) {
const alliances = Array.isArray(myState.alliances) ? myState.alliances : [];
return alliances.some((alliance) => alliance && alliance.other === ownerID);
}
function neighborsOf2(tile, mapData) {
if (!mapData?.width || !mapData?.height) return [];
const width = mapData.width;
const height = mapData.height;
const x = tile % width;
const y = Math.floor(tile / width);
const out = [];
if (x > 0) out.push(tile - 1);
if (x < width - 1) out.push(tile + 1);
if (y > 0) out.push(tile - width);
if (y < height - 1) out.push(tile + width);
return out;
}
function safeMyPlayer5(gameView) {
try {
return gameView && typeof gameView.myPlayer === "function" ? gameView.myPlayer() : null;
} catch (_) {
}
return null;
}
// src/page/automation/auto-team-support.js
var SUPPORT_COOLDOWN_MS = 1e4;
var TARGET_COOLDOWN_MS = 1e4;
var MIN_DONATION_TROOPS = 5e3;
var MAX_DONATION_OWN_SHARE = 1 / 3;
var SUPPORT_RESERVE_RATIO = 0.55;
function createAutoTeamSupport({ gameState, OrigWS, origWsSend, settings, logger, roundLogger, teamDetection, getTroopEconomy }) {
const targetCooldowns = /* @__PURE__ */ new Map();
let lastSentAt = 0;
let lastSkipAt = 0;
let lastCandidateLogAt = 0;
let status = { state: "idle", reason: "not_started" };
return {
reset() {
targetCooldowns.clear();
lastSentAt = 0;
lastSkipAt = 0;
lastCandidateLogAt = 0;
status = { state: "idle", reason: "reset" };
},
getStatus() {
return status;
},
recommend() {
return rankSupportCandidates().slice(0, 3);
},
run(options = {}) {
const state = gameState.state;
if (!options.force && !settings.get("autoTeamSupport")) return setStatus("idle", "disabled");
if (!teamDetection?.isTeamMode) return setStatus("idle", "not_team_mode");
if (!OrigWS) return setStatus("idle", "websocket_unavailable");
if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !state.myPlayerID) {
return setStatus("idle", "not_ready");
}
if (!origWsSend) return setStatus("idle", "socket_unavailable");
const now = performance.now();
if (tryDonateTroops(now, options)) return status;
if (now - lastSentAt < SUPPORT_COOLDOWN_MS) {
return setStatus("cooldown", "cooldown", { cooldownMs: Math.round(SUPPORT_COOLDOWN_MS - (now - lastSentAt)) });
}
return status;
}
};
function tryDonateTroops(now, options = {}) {
const state = gameState.state;
if (now - lastSentAt < SUPPORT_COOLDOWN_MS) return false;
const gate = supportGate(options);
if (!gate.ok) {
logSkip(gate.reason, gate.extra);
setStatus("blocked", gate.reason, gate.extra);
return false;
}
const candidates = rankSupportCandidates();
logCandidates(candidates);
const target = candidates.find((candidate) => {
const lastTargetSentAt = targetCooldowns.get(candidate.id) || 0;
return candidate.troops >= MIN_DONATION_TROOPS && now - lastTargetSentAt >= TARGET_COOLDOWN_MS;
});
if (!target) {
logSkip(candidates.length ? "cooldown_or_too_small" : "no_candidate");
setStatus("idle", candidates.length ? "cooldown_or_too_small" : "no_candidate");
return false;
}
const troops = Math.floor(target.troops);
origWsSend.call(
state.gameSocket,
JSON.stringify({ type: "intent", intent: { type: "donate_troops", recipient: target.id, troops } })
);
lastSentAt = now;
targetCooldowns.set(target.id, now);
logger.info(\`Auto-team support \${target.name || target.id}: \${troops} troops\`);
roundLogger?.record("auto_team_support_sent", {
recipient: target.id,
name: target.name || target.id,
troops,
reason: target.reason,
incoming: target.incoming,
ownReserveAfterRatio: target.ownReserveAfterRatio
});
setStatus("sent", \`sent_\${target.name || target.id}\`, { targetName: target.name || target.id, troops });
return true;
}
function supportGate(options = {}) {
const myState = gameState.getMyState();
if (!myState || !myState.isAlive) return { ok: false, reason: "not_alive" };
const selfIncoming = incomingAttackTroops(gameState, myState);
if (selfIncoming > 0) return { ok: false, reason: "self_under_attack", extra: { incoming: selfIncoming } };
const ownOutgoing = troopsInCombat(myState);
if (ownOutgoing > 0) return { ok: false, reason: "own_action_active", extra: { outgoingTroops: Math.round(ownOutgoing) } };
const defense = options.automationStatus?.autoDefense;
if (defense?.emergency || defense?.state === "alert") {
return { ok: false, reason: "defense_active", extra: { defenseReason: defense.reason || null } };
}
const expand = options.automationStatus?.autoExpand;
const farm = options.automationStatus?.autoFarm;
if (expand?.state === "sent") return { ok: false, reason: "expand_active" };
if (farm?.state === "sent") return { ok: false, reason: "farm_active" };
const aiNeighbours = countHostileAiNeighbours(options.neighbourIDs);
if (aiNeighbours > 0) return { ok: false, reason: "early_ai_neighbours", extra: { aiNeighbours } };
return { ok: true };
}
function rankSupportCandidates() {
const myState = gameState.getMyState();
const economy = getTroopEconomy?.();
if (!myState || !myState.isAlive || !teamDetection?.isTeamMode) return [];
const myTroops = finiteOrZero(myState.troops);
const maxTroops = finiteOrZero(economy?.maxTroops);
if (myTroops <= 0 || maxTroops <= 0) return [];
const reserveRatio = Math.max(SUPPORT_RESERVE_RATIO, finiteOrZero(economy?.recommendedReserve));
const safeAvailable = Math.max(0, myTroops - maxTroops * reserveRatio);
if (safeAvailable < MIN_DONATION_TROOPS) return [];
const isCapWaste = economy?.state === "CAP_WASTE" || finiteOrZero(economy?.currentRatio) >= 0.85;
const teammates = teamDetection.getMyTeamMembers().map((member) => gameState.state.playerStates.get(member.id)).filter((player) => player && player.id !== myState.id && player.isAlive);
return teammates.map((player) => makeCandidate({ player, myTroops, maxTroops, safeAvailable, isCapWaste })).filter(Boolean).sort((a, b) => b.score - a.score);
}
function makeCandidate({ player, myTroops, maxTroops, safeAvailable, isCapWaste }) {
const teammateTroops = finiteOrZero(player.troops);
const incoming = incomingAttackTroopsForSmallID(gameState, player.smallID, player.id);
const troubleDeficit = incoming > 0 ? Math.max(0, incoming * 1.25 - teammateTroops) : 0;
const lowTeamDeficit = isCapWaste ? Math.max(0, myTroops * 0.25 - teammateTroops) : 0;
const capWastePush = isCapWaste && teammateTroops < myTroops * 0.45 ? myTroops * 0.12 : 0;
const desired = Math.max(troubleDeficit, lowTeamDeficit, capWastePush);
const maxDonation = Math.min(safeAvailable, myTroops * MAX_DONATION_OWN_SHARE);
const troops = Math.min(desired, maxDonation);
if (troops < MIN_DONATION_TROOPS) return null;
const ownReserveAfterRatio = (myTroops - troops) / maxTroops;
const reason = troubleDeficit > 0 ? "under_attack" : lowTeamDeficit > 0 ? "low_team_troops" : "cap_waste";
const score = incoming / Math.max(1, teammateTroops) * 3 + (1 - teammateTroops / Math.max(1, myTroops)) + (reason === "under_attack" ? 2 : 0);
return {
id: player.id,
name: player.name || player.displayName || player.id,
troops,
reason,
incoming,
teammateTroops,
ownReserveAfterRatio,
score
};
}
function logCandidates(candidates) {
const now = performance.now();
if (!candidates.length || now - lastCandidateLogAt < 5e3) return;
lastCandidateLogAt = now;
roundLogger?.record(
"auto_team_support_candidate",
candidates.slice(0, 3).map((candidate) => ({
id: candidate.id,
name: candidate.name,
troops: Math.round(candidate.troops),
reason: candidate.reason,
incoming: Math.round(candidate.incoming || 0),
teammateTroops: Math.round(candidate.teammateTroops || 0),
ownReserveAfterRatio: candidate.ownReserveAfterRatio
}))
);
}
function logSkip(reason, extra = {}) {
const now = performance.now();
if (now - lastSkipAt < 5e3) return;
lastSkipAt = now;
roundLogger?.record("auto_team_support_skipped", { reason, ...extra });
}
function setStatus(state, reason, extra = {}) {
status = { state, reason, ...extra };
}
function countHostileAiNeighbours(neighbourIDs) {
if (!neighbourIDs || typeof neighbourIDs.forEach !== "function") return 0;
const myState = gameState.getMyState();
let count = 0;
neighbourIDs.forEach((id) => {
const player = gameState.state.playerStates.get(id);
if (!player || !player.isAlive || player.id === myState?.id) return;
if (teamDetection?.isMyTeammate(player.id)) return;
if (isAllied5(myState, player)) return;
if (String(player.playerType || "").toUpperCase() !== "HUMAN") count += 1;
});
return count;
}
function isAllied5(myState, target) {
const alliances = Array.isArray(myState?.alliances) ? myState.alliances : [];
return alliances.some((alliance) => alliance && alliance.other === target.id);
}
}
// src/page/automation/quick-chat.js
var SOS_KEY = "help.troops";
var QUICK_CHAT_COOLDOWN_MS = 1e4;
var MIN_INCOMING_FOR_SOS = 5e3;
function createQuickChatAutomation({ gameState, OrigWS, origWsSend, settings, logger, roundLogger, teamDetection }) {
let lastSentAt = 0;
let lastSkipAt = 0;
let status = { state: "idle", reason: "not_started" };
return {
reset() {
lastSentAt = 0;
lastSkipAt = 0;
status = { state: "idle", reason: "reset" };
},
getStatus() {
return status;
},
observeIntent(intent) {
if (!intent || intent.type !== "quick_chat") return;
roundLogger?.record("quick_chat_observed", summarizeQuickChat(intent));
},
runAutoSos(context = {}) {
const state = gameState.state;
if (!settings.get("autoSosQuickChat")) return setStatus("idle", "disabled");
if (!teamDetection?.isTeamMode) return setStatus("idle", "not_team_mode");
if (!OrigWS) return setStatus("idle", "websocket_unavailable");
if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !state.myPlayerID) {
return setStatus("idle", "not_ready");
}
if (!origWsSend) return setStatus("idle", "socket_unavailable");
const now = performance.now();
if (now - lastSentAt < QUICK_CHAT_COOLDOWN_MS) {
return setStatus("cooldown", "cooldown", { cooldownMs: Math.round(QUICK_CHAT_COOLDOWN_MS - (now - lastSentAt)) });
}
const myState = gameState.getMyState();
if (!myState || !myState.isAlive) return setStatus("idle", "not_alive");
const incoming = incomingAttackTroops(gameState, myState);
const threshold = Math.max(MIN_INCOMING_FOR_SOS, finiteOrZero(myState.troops) * 0.2);
if (incoming < threshold) {
logSkip("no_pressure", { incoming, threshold });
return setStatus("idle", "no_pressure", { incoming });
}
const recipient = chooseRecipient();
if (!recipient) {
logSkip("no_recipient", { incoming });
return setStatus("blocked", "no_recipient", { incoming });
}
const supportState = context.teamSupportStatus;
if (supportState?.state === "sent" && incoming < threshold * 1.8) {
logSkip("team_support_sent", { incoming, recipient: recipient.id });
return setStatus("idle", "team_support_sent", { incoming });
}
const intent = { type: "quick_chat", recipient: recipient.id, quickChatKey: SOS_KEY };
origWsSend.call(state.gameSocket, JSON.stringify({ type: "intent", intent }));
lastSentAt = now;
logger.info(\`Auto-SOS quick chat \${SOS_KEY} to \${recipient.name || recipient.id}\`);
roundLogger?.record("quick_chat_sent", {
quickChatKey: SOS_KEY,
recipient: recipient.id,
recipientName: recipient.name || recipient.id,
incoming
});
return setStatus("sent", "sent_help_troops", { recipientName: recipient.name || recipient.id, incoming });
}
};
function chooseRecipient() {
const myState = gameState.getMyState();
return teamDetection.getMyTeamMembers().map((member) => gameState.state.playerStates.get(member.id)).filter((player) => player && player.id !== myState?.id && player.isAlive).sort((a, b) => finiteOrZero(b.troops) - finiteOrZero(a.troops))[0];
}
function logSkip(reason, extra = {}) {
const now = performance.now();
if (now - lastSkipAt < 5e3) return;
lastSkipAt = now;
roundLogger?.record("quick_chat_skipped", { reason, quickChatKey: SOS_KEY, ...extra });
}
function setStatus(state, reason, extra = {}) {
status = { state, reason, ...extra };
}
}
function summarizeQuickChat(intent) {
return {
quickChatKey: intent.quickChatKey || null,
recipient: intent.recipient ?? null,
target: intent.target ?? null,
keys: Object.keys(intent)
};
}
// src/page/automation/assist-actions.js
function createAssistActions({ gameState, OrigWS, origWsSend, settings, logger, roundLogger, teamDetection }) {
return {
sendManualAssistAttack(target) {
const gate = validateAttackTarget({ gameState, OrigWS, origWsSend, settings, target, allowHuman: true, teamDetection });
if (!gate.ok) {
roundLogger?.record("autopilot_skip", { action: "manual_assist_attack", reason: gate.reason });
return { ok: false, reason: gate.reason };
}
const troops = Math.max(1, Math.floor(target.suggestedTroops));
origWsSend.call(
gameState.state.gameSocket,
JSON.stringify({ type: "intent", intent: { type: "attack", targetID: target.id, troops } })
);
logger.info(\`Manual assist attack \${target.name || target.id}: \${troops} troops\`);
roundLogger?.record("manual_assist_attack_sent", {
targetID: target.id,
targetName: target.name || target.id,
troops,
suggestedPercent: target.suggestedPercent,
reserveAfterRatio: target.reserveAfterRatio
});
return { ok: true };
}
};
}
function validateAttackTarget({ gameState, OrigWS, origWsSend, settings, target, allowHuman = false, teamDetection }) {
const state = gameState.state;
const myState = gameState.getMyState();
if (!target) return { ok: false, reason: "no_target" };
if (!OrigWS) return { ok: false, reason: "websocket_unavailable" };
if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !origWsSend) return { ok: false, reason: "socket_closed" };
if (!state.gameStarted || !state.myPlayerID || !myState || !myState.isAlive) return { ok: false, reason: "not_alive_or_not_started" };
if (target.isHuman && !allowHuman) return { ok: false, reason: "human_blocked" };
if (target.status !== "target" && target.status !== "farm") return { ok: false, reason: \`status_\${target.status || "unknown"}\` };
const player = state.playerStates.get(target.id);
if (!player || !player.isAlive) return { ok: false, reason: "target_not_alive" };
if (isAllied4(myState, player)) return { ok: false, reason: "target_allied" };
if (teamDetection?.isMyTeammate(player.id)) return { ok: false, reason: "target_team" };
if (!Number.isFinite(Number(target.suggestedTroops)) || Number(target.suggestedTroops) <= 0) {
return { ok: false, reason: "no_troops" };
}
const reserveAfter = Number(target.reserveAfterRatio);
const reserveLimit = Number.isFinite(Number(target.appliedReserveRatio)) ? Number(target.appliedReserveRatio) : Number(settings.get("autoFarmReserveRatio")) || 0.55;
if (!Number.isFinite(reserveAfter) || reserveAfter < reserveLimit) return { ok: false, reason: "reserve_limit" };
return { ok: true };
}
function isAllied4(myState, target) {
const alliances = Array.isArray(myState.alliances) ? myState.alliances : [];
return alliances.some((alliance) => alliance && alliance.other === target.id);
}
// src/page/automation/autopilot-controller.js
function createAutopilotController({ settings, gameState, autoSpawn, autoFarm, autoExpand, autoAlliance, autoEco, autoDefense, roundLogger }) {
let lastStateLogAt = 0;
let lastSkipLogAt = 0;
return {
reset() {
lastStateLogAt = 0;
lastSkipLogAt = 0;
},
run({ topSpot, farmRecommendations, buyRecommendations, expansion, threats, neighbourIDs, troopInfo, nukeThreat }) {
if (!settings.get("autopilot")) return { active: false };
const state = gameState.state;
const myState = gameState.getMyState();
if (!state.gameStarted || !myState || !myState.isAlive) {
logSkip("not_ready");
return { active: true, spawnHandled: false, farmHandled: false, expandHandled: false, allianceHandled: false, ecoHandled: false, defenseHandled: false };
}
logState({ expansion, threats });
let spawnHandled = false;
if (topSpot) {
autoSpawn?.send(topSpot);
spawnHandled = true;
}
const defenseStatus = autoDefense?.run({ force: true, nukeThreat, threats, neighbourIDs }) || null;
if (defenseStatus?.emergency) {
logSkip("defense_emergency");
runEco(buyRecommendations);
return { active: true, spawnHandled, farmHandled: true, expandHandled: true, allianceHandled: false, ecoHandled: true, defenseHandled: true };
}
const humanPressure = countHumanPressure(farmRecommendations);
const strongThreat = isNeighbourThreatStrong(threats, neighbourIDs);
let expandHandled = false;
if (autoExpand) {
autoExpand.run(troopInfo, { force: true });
expandHandled = true;
}
const shouldPauseFarm = humanPressure >= 2 || strongThreat || expansion && expansion.level === "stop";
if (shouldPauseFarm) {
logSkip(strongThreat ? "strong_threat" : humanPressure >= 2 ? "human_pressure" : expansion.level);
runEco(buyRecommendations);
return { active: true, spawnHandled, farmHandled: true, expandHandled, allianceHandled: false, ecoHandled: true, defenseHandled: true };
}
autoFarm?.run(farmRecommendations, expansion, { force: true });
runEco(buyRecommendations);
let allianceHandled = false;
if (settings.get("autoAlliance")) {
autoAlliance?.run();
allianceHandled = true;
}
return { active: true, spawnHandled, farmHandled: true, expandHandled, allianceHandled, ecoHandled: true, defenseHandled: true };
}
};
function runEco(buyRecommendations) {
if (settings.get("autoEco")) autoEco?.run({ buyRecommendations }, { force: true });
}
function logSkip(reason) {
const now = performance.now();
if (now - lastSkipLogAt < 3e3) return;
lastSkipLogAt = now;
roundLogger?.record("autopilot_skip", { reason });
}
function logState({ expansion, threats }) {
const now = performance.now();
if (now - lastStateLogAt < 5e3) return;
lastStateLogAt = now;
roundLogger?.record("autopilot_state", {
expansion: expansion ? expansion.level : "unknown",
topThreat: threats && threats[0] ? { name: threats[0].name, level: threats[0].level, score: threats[0].score } : null
});
}
}
function countHumanPressure(recommendations) {
return (recommendations || []).filter((rec) => rec.isHuman && rec.status === "danger").length;
}
function isNeighbourThreatStrong(threats, neighbourIDs) {
if (!Array.isArray(threats) || !neighbourIDs || neighbourIDs.size === 0) return false;
return threats.some(
(threat) => (threat.level === "Dangerous" || threat.level === "Critical") && neighbourIDs.has(threat.id)
);
}
// src/page/automation/smart-attack.js
function createSmartAttackModifier({ settings, gameState, logger, roundLogger, teamDetection, getTroopEconomy }) {
return function maybeModifyAttack(data) {
if (!settings.get("smartAttack") || typeof data !== "string") return data;
try {
const message = JSON.parse(data);
if (!message || message.type !== "intent" || !message.intent) return data;
const intent = message.intent;
if (intent.type !== "attack" || !intent.troops || intent.troops <= 0 || !intent.targetID) return data;
const targetState = resolveTarget(gameState, intent.targetID);
if (targetState && teamDetection?.isMyTeammate(targetState.id)) return data;
const economy = getTroopEconomy?.();
const safe = economy ? economy.safeSpendableTroops : null;
if (!Number.isFinite(safe)) return data;
if (safe < 1) return data;
const originalTroops = intent.troops;
if (originalTroops <= safe) return data;
const newTroops = Math.max(1, Math.floor(safe));
if (newTroops === originalTroops) return data;
intent.troops = newTroops;
logger.info(
\`Smart attack cap \${targetState?.name || intent.targetID}: \${originalTroops} -> \${newTroops} (safe \${Math.floor(safe)})\`
);
roundLogger?.record("smart_attack_modified", {
reason: "growth_cap",
targetID: intent.targetID,
targetName: targetState?.name || intent.targetID,
safeSpendable: Math.floor(safe),
originalTroops,
newTroops
});
return JSON.stringify(message);
} catch (_) {
return data;
}
};
}
function resolveTarget(gameState, targetID) {
const direct = gameState.state.playerStates.get(targetID);
if (direct) return direct;
let match = null;
gameState.state.playerStates.forEach((player) => {
if (!match && player && player.smallID === targetID) match = player;
});
return match;
}
// src/page/automation/auto-alliance.js
var ALLIANCE_RENEW_THRESHOLD = 50;
var ALLIANCE_RETRY_COOLDOWN_TICKS = 300;
var MAX_ALLIANCE_ATTEMPTS = 2;
function createAutoAlliance({ gameState, neighbourFetcher, OrigWS, origWsSend, logger, roundLogger, teamDetection }) {
const allianceRequestCooldowns = /* @__PURE__ */ new Map();
let lastSkippedNeighbour = null;
return {
get lastSkippedNeighbour() {
return lastSkippedNeighbour;
},
reset() {
allianceRequestCooldowns.clear();
lastSkippedNeighbour = null;
},
run() {
const state = gameState.state;
if (!OrigWS) return;
if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !state.gameStarted || !state.myPlayerID) return;
const myState = gameState.getMyState();
if (!myState || !myState.isAlive) return;
neighbourFetcher.refresh();
if (!neighbourFetcher.cachedNeighbourIDs) return;
const attacking = getAttackingPlayerIDs(state, myState);
const humanNeighbours = [];
neighbourFetcher.cachedNeighbourIDs.forEach((playerID) => {
if (playerID === state.myPlayerID || attacking[playerID]) return;
if (teamDetection?.isMyTeammate(playerID)) return;
const player = state.playerStates.get(playerID);
if (!player || !player.isAlive || !isHumanPlayerState(player)) return;
humanNeighbours.push(player);
});
humanNeighbours.sort((a, b) => {
const tilesA = a.tilesOwned || 0;
const tilesB = b.tilesOwned || 0;
if (tilesB !== tilesA) return tilesB - tilesA;
return (b.troops || 0) - (a.troops || 0);
});
const targets = humanNeighbours.length > 1 ? humanNeighbours.slice(0, humanNeighbours.length - 1) : [];
const skipped = humanNeighbours.length > 1 ? humanNeighbours[humanNeighbours.length - 1] : null;
lastSkippedNeighbour = skipped ? skipped.name || skipped.id : null;
const existingAlliances = {};
(myState.alliances || []).forEach((alliance) => {
existingAlliances[alliance.other] = alliance;
});
const pendingOutgoing = {};
(myState.outgoingAllianceRequests || []).forEach((playerID) => {
pendingOutgoing[playerID] = true;
});
targets.forEach((target) => {
const existing = existingAlliances[target.id];
if (existing) {
if (existing.expiresAt && existing.expiresAt - state.currentTick < ALLIANCE_RENEW_THRESHOLD && !existing.hasExtensionRequest) {
sendAllianceIntent(state, OrigWS, origWsSend, "allianceExtension", target.id, target.name, logger, roundLogger);
}
return;
}
if (pendingOutgoing[target.id]) return;
const record = allianceRequestCooldowns.get(target.id);
if (record) {
if (record.attempts >= MAX_ALLIANCE_ATTEMPTS) return;
if (state.currentTick - record.lastTick < ALLIANCE_RETRY_COOLDOWN_TICKS) return;
}
sendAllianceIntent(state, OrigWS, origWsSend, "allianceRequest", target.id, target.name, logger, roundLogger);
allianceRequestCooldowns.set(target.id, {
lastTick: state.currentTick,
attempts: (record?.attempts || 0) + 1
});
});
}
};
}
function recommendAlliances(gameState, teamDetection) {
const myState = gameState.getMyState();
if (!myState) return [];
const recommendations = [];
gameState.state.playerStates.forEach((player) => {
if (!player || !player.isAlive || player.id === myState.id || !isHumanPlayerState(player)) return;
if (teamDetection?.isMyTeammate(player.id)) return;
const score = (player.tilesOwned || 0) * 0.55 + (player.troops || 0) * 1e-5;
recommendations.push({
id: player.id,
name: player.name || player.id,
score,
label: score > (myState.tilesOwned || 0) * 0.55 ? "Ally candidate" : "Possible target"
});
});
return recommendations.sort((a, b) => b.score - a.score).slice(0, 3);
}
function getAttackingPlayerIDs(state, myState) {
const smallIDtoPlayerID = {};
state.playerStates.forEach((player) => {
if (player.smallID != null) smallIDtoPlayerID[player.smallID] = player.id;
});
const attacking = {};
(myState.outgoingAttacks || []).forEach((attack) => {
if (!attack || attack.retreating) return;
const targetID = smallIDtoPlayerID[attack.targetID];
if (targetID) attacking[targetID] = true;
});
return attacking;
}
function sendAllianceIntent(state, OrigWS, origWsSend, intentType, recipientID, recipientName, logger, roundLogger) {
if (!OrigWS) return;
if (!state.gameSocket || state.gameSocket.readyState !== OrigWS.OPEN || !origWsSend) return;
origWsSend.call(
state.gameSocket,
JSON.stringify({ type: "intent", intent: { type: intentType, recipient: recipientID } })
);
logger.info(\`\${intentType} sent to \${recipientName || recipientID}\`);
roundLogger?.record("auto_alliance_sent", { intentType, recipientID, recipientName: recipientName || recipientID });
}
function isHumanPlayerState(state) {
return !!state && String(state.playerType || "").toUpperCase() === "HUMAN";
}
// src/shared/html.js
function escapeHtml(value) {
return String(value == null ? "" : value).replace(/[&<>"']/g, (char) => {
switch (char) {
case "&":
return "&";
case "<":
return "<";
case ">":
return ">";
case '"':
return """;
case "'":
return "'";
default:
return char;
}
});
}
// src/shared/troops.js
function formatTroopCount(troops) {
const num = Number(troops) / 10;
if (!Number.isFinite(num)) return "0";
if (num >= 1e7) return \`\${(Math.floor(num / 1e5) / 10).toFixed(1)}M\`;
if (num >= 1e6) return \`\${(Math.floor(num / 1e4) / 100).toFixed(2)}M\`;
if (num >= 1e5) return \`\${Math.floor(num / 1e3)}K\`;
if (num >= 1e4) return \`\${(Math.floor(num / 100) / 10).toFixed(1)}K\`;
if (num >= 1e3) return \`\${(Math.floor(num / 10) / 100).toFixed(2)}K\`;
return Math.floor(num).toString();
}
// src/page/ui/advisor-panel.js
function createAdvisorPanel({ app, settings, heatmap, actions = {} }) {
let panel = null;
return {
create() {
removeElementById("ofat-panel");
removeElementById("ofat-heatmap");
installPanelStyles();
panel = document.createElement("div");
panel.id = "ofat-panel";
panel.style.display = settings.get("showAdvisorPanel") ? "block" : "none";
appendWhenReady(panel, "body");
makeDraggable(panel);
},
setVisible(visible) {
if (panel) panel.style.display = visible ? "block" : "none";
},
attachHeatmap(canvas) {
appendWhenReady(canvas, "body");
},
render(viewModel) {
if (!panel) return;
panel.innerHTML = renderPanelHtml(app, settings, viewModel);
panel.querySelectorAll("button[data-setting]").forEach((button) => {
button.addEventListener("click", () => settings.set(button.dataset.setting, !settings.get(button.dataset.setting)));
});
const closeButton = document.getElementById("ofat-close");
if (closeButton) closeButton.addEventListener("click", () => settings.set("showAdvisorPanel", false));
const assistButton = document.getElementById("ofat-assist-attack");
if (assistButton && actions.onManualAssistAttack) assistButton.addEventListener("click", actions.onManualAssistAttack);
panel.querySelectorAll("button[data-buy-unavailable]").forEach((button) => {
button.addEventListener("click", () => actions.onUnavailableBuild?.(button.dataset.buyUnavailable));
});
heatmap.render(viewModel.scores, viewModel.topSpots, viewModel.playerSpawns);
}
};
}
function renderPanelHtml(app, settings, vm) {
const skipped = vm.lastSkippedNeighbour ? \` | Skip: <b>\${escapeHtml(vm.lastSkippedNeighbour)}</b>\` : "";
let html = "";
html += \`<div class="ofat-head">\`;
html += \`<div class="ofat-title">\${escapeHtml(app.name)}</div>\`;
html += \`<div class="ofat-muted">v\${escapeHtml(app.version)} | updated \${escapeHtml(app.modified)}</div>\`;
html += \`</div>\`;
html += \`<div class="ofat-body">\`;
html += \`<div class="ofat-muted">Players <b>\${vm.otherPlayerCount}</b> | Nations <b>\${vm.nationCount}</b> | Tick <b>\${vm.tick}</b></div>\`;
if (vm.expansion) html += \`<div class="ofat-muted">\${escapeHtml(vm.expansion.label)}</div>\`;
if (vm.troopEconomy && settings.get("showTroopEconomy")) html += renderEconomyHtml(vm.troopEconomy);
if (vm.topThreats.length) {
html += \`<div class="ofat-muted">Threats: \${vm.topThreats.map((t) => \`\${escapeHtml(t.name)} <b>\${escapeHtml(t.level)}</b>\`).join(" | ")}</div>\`;
}
if (vm.farmRecommendations && vm.farmRecommendations.length) {
const farms = vm.farmRecommendations.filter((target) => target.status === "farm" || target.status === "mark").slice(0, 3);
if (farms.length) {
html += \`<div class="ofat-muted">Farm: \${farms.map((t) => \`\${escapeHtml(t.name)} <b>\${escapeHtml(t.label)}</b>\`).join(" | ")}</div>\`;
}
const humanTargets = vm.farmRecommendations.filter((target) => target.isHuman).slice(0, 3);
if (humanTargets.length) {
html += \`<div class="ofat-muted">Targets: \${humanTargets.map((t) => \`\${escapeHtml(t.name)} <b>\${escapeHtml(t.label)}</b>\`).join(" | ")}</div>\`;
}
}
if (vm.assistTarget) {
html += \`<div class="ofat-action-row">\`;
html += \`<span>Freigabe: <b>\${escapeHtml(vm.assistTarget.name)}</b> \${escapeHtml(vm.assistTarget.label)}</span>\`;
html += \`<button id="ofat-assist-attack">Ziel angreifen</button>\`;
html += \`</div>\`;
}
if (vm.buyRecommendations && vm.buyRecommendations.length) {
html += \`<div class="ofat-section-title">Eco / Build</div>\`;
html += \`<div class="ofat-buy-list">\`;
vm.buyRecommendations.forEach((rec) => {
if (rec.actionAvailable) {
html += \`<span class="ofat-buy-chip" title="\${escapeHtml(rec.reason)}">\${escapeHtml(rec.label)}</span>\`;
} else {
html += \`<button data-buy-unavailable="\${escapeHtml(rec.actionKey)}" title="\${escapeHtml(rec.reason)}">\${escapeHtml(rec.label)}: nur Hinweis</button>\`;
}
});
html += \`</div>\`;
}
if (vm.topSpots.length) html += \`<div class="ofat-section-title">Spawn Diagnose</div>\`;
for (let i = 0; i < vm.topSpots.length; i += 1) {
const spot = vm.topSpots[i];
const scorePct = Math.max(0, Math.min(100, Math.round(spot.score * 100)));
html += \`<div class="ofat-row" data-ofat-spot="\${i}">\`;
html += \`<b>\${i + 1}</b>\`;
html += \`<div>\`;
html += \`<div>(\${spot.x},\${spot.y}) <span class="ofat-muted">\${escapeHtml(describeSpawnSpot(spot))}</span></div>\`;
html += \`<div class="ofat-muted">L:\${Math.floor(spot.landDensity * 100)} G:\${Math.floor(spot.plainsRatio * 100)} N:\${Math.floor(spot.nationScore * 100)} P:\${Math.floor(spot.playerDistScore * 100)}</div>\`;
html += \`<div class="ofat-scorebar"><span style="width:\${scorePct}%"></span></div>\`;
html += \`</div>\`;
html += \`<b>\${scorePct}</b>\`;
html += \`</div>\`;
}
if (vm.allianceRecommendations.length) {
html += \`<div class="ofat-muted">Alliance: \${vm.allianceRecommendations.map((r) => \`\${escapeHtml(r.name)} (\${escapeHtml(r.label)})\`).join(" | ")}</div>\`;
}
html += \`<div class="ofat-muted">Allies: <b>\${vm.allyCount}</b>\${skipped}</div>\`;
html += \`<div class="ofat-buttons">\`;
html += buttonHtml(settings, "Auto spawn", "autoSpawn", false);
html += buttonHtml(settings, "Auto ally", "autoAlliance", false);
html += buttonHtml(settings, "Map", "showHeatmap", false);
html += buttonHtml(settings, "Panel", "showAdvisorPanel", false);
html += \`<button id="ofat-close">Close</button>\`;
html += \`</div></div>\`;
return html;
}
function renderEconomyHtml(eco) {
const growth = Math.round((eco.growthEfficiency || 0) * 100);
const safety = Math.round((eco.combatSafety || 0) * 100);
const safeSend = formatTroopCount(eco.safeSpendableTroops);
const push = eco.timeToPushSec != null ? \` | Push in ~\${eco.timeToPushSec}s\` : "";
let html = \`<div class="ofat-eco ofat-eco-\${economyTone(eco.state)}">\`;
html += \`<div>State: <b>\${escapeHtml(eco.state)}</b> | Growth: <b>\${growth}%</b>\${push}</div>\`;
html += \`<div>Combat Safety: <b>\${safety}%</b> | Safe Send: <b>\${escapeHtml(safeSend)}</b></div>\`;
html += \`<div class="ofat-muted">\${escapeHtml(eco.hint || "")}</div>\`;
html += \`</div>\`;
return html;
}
function economyTone(state) {
if (state === "CRITICAL" || state === "RECOVER") return "low";
if (state === "PUSH" || state === "CAP_WASTE") return "push";
return "ok";
}
function buttonHtml(settings, label, key, warn) {
const on = settings.get(key);
return \`<button data-setting="\${escapeHtml(key)}" data-on="\${on ? "true" : "false"}" data-warn="\${warn && on ? "true" : "false"}">\${escapeHtml(label)}: \${on ? "ON" : "OFF"}</button>\`;
}
function installPanelStyles() {
createStyle(
"ofat-style",
\`
#ofat-panel {
position: fixed;
top: 10px;
right: 10px;
z-index: 99999;
width: min(460px, calc(100vw - 20px));
max-height: calc(100vh - 20px);
overflow: auto;
background: rgba(8, 10, 13, 0.92);
color: #f4f7fb;
border: 1px solid rgba(113, 191, 255, 0.45);
border-radius: 8px;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
font: 12px/1.35 ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
user-select: none;
}
#ofat-panel .ofat-head {
padding: 10px 12px 7px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
cursor: move;
}
#ofat-panel .ofat-title { color: #71c7ff; font-size: 15px; font-weight: 700; }
#ofat-panel .ofat-muted { color: rgba(244, 247, 251, 0.68); }
#ofat-panel .ofat-body { padding: 9px 12px 12px; }
#ofat-panel .ofat-row {
display: grid;
grid-template-columns: 26px 1fr auto;
gap: 8px;
align-items: center;
padding: 5px 6px;
margin: 3px 0;
border-radius: 6px;
background: rgba(255, 255, 255, 0.055);
}
#ofat-panel .ofat-eco {
margin: 6px 0;
padding: 6px 8px;
border-radius: 6px;
border-left: 3px solid #71c7ff;
background: rgba(255, 255, 255, 0.06);
}
#ofat-panel .ofat-eco-low { border-left-color: #ef5350; background: rgba(183, 28, 28, 0.16); }
#ofat-panel .ofat-eco-ok { border-left-color: #66bb6a; background: rgba(46, 125, 50, 0.16); }
#ofat-panel .ofat-eco-push { border-left-color: #71c7ff; background: rgba(41, 98, 255, 0.16); }
#ofat-panel .ofat-scorebar {
height: 4px;
margin-top: 3px;
background: rgba(255, 255, 255, 0.12);
border-radius: 999px;
overflow: hidden;
}
#ofat-panel .ofat-scorebar > span { display: block; height: 100%; background: #71c7ff; }
#ofat-panel .ofat-section-title {
margin-top: 9px;
margin-bottom: 4px;
color: rgba(244, 247, 251, 0.9);
font-weight: 700;
}
#ofat-panel .ofat-buttons { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; }
#ofat-panel .ofat-action-row {
display: grid;
grid-template-columns: 1fr auto;
gap: 8px;
align-items: center;
margin-top: 7px;
padding: 6px;
border-radius: 6px;
background: rgba(83, 181, 255, 0.12);
}
#ofat-panel .ofat-buy-list {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 7px;
}
#ofat-panel .ofat-buy-chip {
padding: 4px 7px;
border-radius: 5px;
background: rgba(46, 125, 50, 0.5);
color: #f4f7fb;
}
#ofat-panel button {
color: #f4f7fb;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 6px;
background: rgba(255, 255, 255, 0.1);
padding: 4px 7px;
font: inherit;
cursor: pointer;
}
#ofat-panel button[data-on="true"] { background: rgba(46, 125, 50, 0.85); }
#ofat-panel button[data-warn="true"] { background: rgba(183, 28, 28, 0.85); }
#ofat-heatmap {
position: fixed;
right: 10px;
bottom: 10px;
z-index: 99998;
border: 2px solid #71c7ff;
border-radius: 6px;
background: #05070a;
image-rendering: pixelated;
}
\`
);
}
// src/page/ui/heatmap.js
function createHeatmap({ mapDataRef }) {
let canvas = null;
let scale = 1;
return {
create(showHeatmap) {
const mapData = mapDataRef.current;
canvas = document.createElement("canvas");
scale = Math.min(320 / mapData.width, 220 / mapData.height);
canvas.width = Math.max(1, Math.floor(mapData.width * scale));
canvas.height = Math.max(1, Math.floor(mapData.height * scale));
canvas.id = "ofat-heatmap";
canvas.style.display = showHeatmap ? "block" : "none";
return canvas;
},
setVisible(visible) {
if (canvas) canvas.style.display = visible ? "block" : "none";
},
render(scores, topSpots, playerSpawns) {
if (!canvas) return;
const mapData = mapDataRef.current;
if (!mapData) return;
renderHeatmap(canvas, scale, mapData, scores, topSpots, playerSpawns);
}
};
}
function renderHeatmap(canvas, scale, mapData, scores, topSpots, playerSpawns) {
const ctx = canvas.getContext("2d");
const w = canvas.width;
const h = canvas.height;
const scoreGrid = new Float32Array(mapData.width * mapData.height);
let maxScore = 0;
const half = 8;
for (let i = 0; i < scores.length; i += 1) {
const score = scores[i];
const x0 = Math.max(0, Math.floor(score.x - half));
const x1 = Math.min(mapData.width - 1, Math.ceil(score.x + half));
const y0 = Math.max(0, Math.floor(score.y - half));
const y1 = Math.min(mapData.height - 1, Math.ceil(score.y + half));
for (let y = y0; y <= y1; y += 1) {
const rowOffset = y * mapData.width;
for (let x = x0; x <= x1; x += 1) scoreGrid[rowOffset + x] = score.score;
}
if (score.score > maxScore) maxScore = score.score;
}
const imageData = ctx.createImageData(w, h);
const invMax = maxScore > 0 ? 1 / maxScore : 0;
for (let y = 0; y < h; y += 1) {
const my = Math.min(mapData.height - 1, Math.floor(y / scale));
for (let x = 0; x < w; x += 1) {
const mx = Math.min(mapData.width - 1, Math.floor(x / scale));
const value = scoreGrid[my * mapData.width + mx] * invMax;
const pi = (y * w + x) * 4;
if (value > 0) {
imageData.data[pi] = Math.floor((1 - value) * 255);
imageData.data[pi + 1] = Math.floor(value * 255);
imageData.data[pi + 2] = 42;
imageData.data[pi + 3] = Math.floor(value * 180 + 50);
} else {
imageData.data[pi] = 8;
imageData.data[pi + 1] = 10;
imageData.data[pi + 2] = 14;
imageData.data[pi + 3] = 150;
}
}
}
ctx.putImageData(imageData, 0, 0);
ctx.fillStyle = "#71c7ff";
mapData.nations.forEach((nation) => {
ctx.beginPath();
ctx.arc(Math.floor(nation.coordinates[0] * scale), Math.floor(nation.coordinates[1] * scale), 2.5, 0, Math.PI * 2);
ctx.fill();
});
ctx.strokeStyle = "#ff5757";
ctx.lineWidth = 2;
playerSpawns.forEach((spawn) => {
const x = Math.floor(spawn.x * scale);
const y = Math.floor(spawn.y * scale);
ctx.beginPath();
ctx.moveTo(x - 4, y - 4);
ctx.lineTo(x + 4, y + 4);
ctx.moveTo(x + 4, y - 4);
ctx.lineTo(x - 4, y + 4);
ctx.stroke();
});
topSpots.forEach((spot, index) => {
const x = Math.floor(spot.x * scale);
const y = Math.floor(spot.y * scale);
ctx.strokeStyle = index === 0 ? "#ffd54f" : index < 3 ? "#9ccc65" : "#d8dde6";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(x, y, 7, 0, Math.PI * 2);
ctx.stroke();
ctx.fillStyle = "rgba(0,0,0,0.72)";
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = index === 0 ? "#ffd54f" : "#ffffff";
ctx.font = "bold 9px monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(String(index + 1), x, y);
});
}
// src/page/logging/log-export.js
function downloadJson(filename, data) {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.style.display = "none";
document.documentElement.appendChild(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(url), 1e3);
}
function createRoundLogFilename(startedAt = /* @__PURE__ */ new Date()) {
const stamp = [
startedAt.getFullYear(),
pad2(startedAt.getMonth() + 1),
pad2(startedAt.getDate()),
"-",
pad2(startedAt.getHours()),
pad2(startedAt.getMinutes()),
pad2(startedAt.getSeconds())
].join("");
return \`openfront-round-log-\${stamp}.json\`;
}
function pad2(value) {
return String(value).padStart(2, "0");
}
// src/page/logging/log-sanitize.js
function summarizeMap(mapData) {
if (!mapData) return null;
return {
width: mapData.width,
height: mapData.height,
nationCount: mapData.nations ? mapData.nations.length : 0
};
}
function summarizeSettings(settings) {
return {
autoSpawn: !!settings.autoSpawn,
autopilot: !!settings.autopilot,
autoAlliance: !!settings.autoAlliance,
autoFarm: !!settings.autoFarm,
autoFarmHumanTargets: !!settings.autoFarmHumanTargets,
autoEco: !!settings.autoEco,
autoTeamSupport: !!settings.autoTeamSupport,
autoSosQuickChat: !!settings.autoSosQuickChat,
smartAttack: !!settings.smartAttack,
showAttackBadges: !!settings.showAttackBadges,
showAdvisorPanel: !!settings.showAdvisorPanel,
showHeatmap: !!settings.showHeatmap,
roundLogging: !!settings.roundLogging,
roundLogAutoDownload: !!settings.roundLogAutoDownload,
roundLogSnapshotIntervalMs: settings.roundLogSnapshotIntervalMs,
networkLogging: !!settings.networkLogging,
autoSpawnDelayMs: settings.autoSpawnDelayMs,
autoFarmWindowMs: settings.autoFarmWindowMs,
autoFarmReserveRatio: settings.autoFarmReserveRatio,
autoFarmCooldownMs: settings.autoFarmCooldownMs,
autoFarmDynamicReserve: !!settings.autoFarmDynamicReserve,
autoFarmBaseReserveRatio: settings.autoFarmBaseReserveRatio,
autoFarmMinReserveRatio: settings.autoFarmMinReserveRatio,
autoFarmMaxReserveRatio: settings.autoFarmMaxReserveRatio,
autoExpand: !!settings.autoExpand
};
}
function summarizePlayers(gameState, keepNames) {
const players = [];
gameState.state.playerStates.forEach((player) => {
if (!player) return;
players.push({
id: player.id,
name: keepNames ? player.name || player.id : player.id,
troops: finiteOrZero(player.troops),
tilesOwned: finiteOrZero(player.tilesOwned),
gold: finiteOrZero(player.gold),
isAlive: player.isAlive,
playerType: player.playerType,
allianceCount: Array.isArray(player.alliances) ? player.alliances.length : 0,
outgoingAttackCount: Array.isArray(player.outgoingAttacks) ? player.outgoingAttacks.filter((attack) => attack && !attack.retreating).length : 0
});
});
return players.sort((a, b) => {
if (b.tilesOwned !== a.tilesOwned) return b.tilesOwned - a.tilesOwned;
return b.troops - a.troops;
});
}
function summarizeMyState(gameState, keepNames) {
const player = gameState.getMyState();
if (!player) return null;
return {
id: player.id,
name: keepNames ? player.name || player.id : player.id,
troops: finiteOrZero(player.troops),
tilesOwned: finiteOrZero(player.tilesOwned),
gold: finiteOrZero(player.gold),
isAlive: player.isAlive,
allianceCount: Array.isArray(player.alliances) ? player.alliances.length : 0,
outgoingAttackCount: Array.isArray(player.outgoingAttacks) ? player.outgoingAttacks.filter((attack) => attack && !attack.retreating).length : 0
};
}
function summarizeAdvisorView(viewModel) {
if (!viewModel) return null;
return {
tick: viewModel.tick,
troopEconomy: viewModel.troopEconomy ? {
state: viewModel.troopEconomy.state,
currentRatio: round3(viewModel.troopEconomy.currentRatio),
growthEfficiency: round3(viewModel.troopEconomy.growthEfficiency),
combatSafety: round3(viewModel.troopEconomy.combatSafety),
recommendedReserve: round3(viewModel.troopEconomy.recommendedReserve),
safeSpendableTroops: Math.round(viewModel.troopEconomy.safeSpendableTroops || 0)
} : null,
topSpots: (viewModel.topSpots || []).slice(0, 5).map((spot) => ({
x: spot.x,
y: spot.y,
score: round3(spot.score),
landDensity: round3(spot.landDensity),
plainsRatio: round3(spot.plainsRatio),
nationScore: round3(spot.nationScore),
playerDistScore: round3(spot.playerDistScore)
})),
topThreats: (viewModel.topThreats || []).map((threat) => ({
id: threat.id,
name: threat.name,
level: threat.level,
score: round3(threat.score),
troopsRatio: round3(threat.troopsRatio),
tilesRatio: round3(threat.tilesRatio),
nukePotential: threat.nukePotential
})),
expansion: viewModel.expansion || null,
farmRecommendations: (viewModel.farmRecommendations || []).slice(0, 8).map((rec) => ({
id: rec.id,
name: rec.name,
status: rec.status,
label: rec.label,
reason: rec.reason,
score: round3(rec.score),
strengthRatio: round3(rec.strengthRatio),
suggestedPercent: rec.suggestedPercent,
reserveAfterRatio: round3(rec.reserveAfterRatio),
reserveMaxSend: Math.round(rec.reserveMaxSend || 0),
captureMaxSend: Math.round(rec.captureMaxSend || 0),
usedCaptureOverdraft: !!rec.usedCaptureOverdraft,
estimatedCaptureTurns: rec.estimatedCaptureTurns || null,
captureCostEstimate: Math.round(rec.captureCostEstimate || 0),
officialCaptureCostEstimate: Math.round(rec.officialCaptureCostEstimate || 0),
officialCaptureTurns: rec.officialCaptureTurns || null,
officialCaptureSource: rec.officialCaptureSource || null,
effectiveTargetTroops: Math.round(rec.effectiveTargetTroops || 0)
})),
assistTarget: viewModel.assistTarget ? {
id: viewModel.assistTarget.id,
name: viewModel.assistTarget.name,
label: viewModel.assistTarget.label,
reason: viewModel.assistTarget.reason,
suggestedPercent: viewModel.assistTarget.suggestedPercent
} : null,
buyRecommendations: (viewModel.buyRecommendations || []).map((rec) => ({
label: rec.label,
reason: rec.reason,
actionKey: rec.actionKey,
actionAvailable: !!rec.actionAvailable,
estimatedCost: Math.round(rec.estimatedCost || 0),
costSource: rec.costSource || null
})),
automationStatus: viewModel.automationStatus || null,
teamSupportRecommendations: (viewModel.teamSupportRecommendations || []).slice(0, 3).map((rec) => ({
id: rec.id,
name: rec.name,
reason: rec.reason,
troops: Math.round(rec.troops || 0),
incoming: Math.round(rec.incoming || 0),
ownReserveAfterRatio: round3(rec.ownReserveAfterRatio)
})),
allianceRecommendations: (viewModel.allianceRecommendations || []).map((rec) => ({
id: rec.id,
name: rec.name,
label: rec.label,
score: round3(rec.score)
})),
allyCount: viewModel.allyCount || 0
};
}
function round3(value) {
const number = Number(value);
if (!Number.isFinite(number)) return 0;
return Math.round(number * 1e3) / 1e3;
}
// src/page/logging/round-logger.js
var MAX_TIMELINE_BYTES = 2 * 1024 * 1024;
var PROTECTED_LOG_TYPES = /* @__PURE__ */ new Set([
"round_started",
"round_ended",
"map_loaded",
"setting_changed",
"auto_spawn_sent",
"auto_alliance_sent",
"smart_attack_modified",
"autopilot_action",
"autopilot_skip",
"autopilot_state",
"farm_recommendation",
"auto_farm_attack_sent",
"auto_farm_skipped",
"auto_expand_sent",
"auto_expand_skipped",
"auto_eco_sent",
"auto_eco_skipped",
"auto_eco_cost_model",
"auto_eco_pending_blocked",
"auto_eco_runtime_probe",
"auto_team_support_sent",
"auto_team_support_skipped",
"auto_team_support_candidate",
"auto_team_support_gold_sent",
"auto_defense_build_sent",
"auto_defense_counter_sent",
"auto_defense_skipped",
"auto_boat_sent",
"auto_boat_skipped",
"auto_weapons_silo_sent",
"auto_weapons_launch_sent",
"auto_weapons_early_nuke_sent",
"auto_weapons_skipped",
"weapon_intent_observed",
"retreat_intent_observed",
"manual_assist_attack_sent",
"buy_recommendation",
"build_button_unavailable",
"intent_observed",
"boat_intent_observed",
"team_detected",
"mechanics_discovered",
"gameview_discovered",
"spawn_phase_state",
"eco_cost_model",
"quick_chat_observed",
"quick_chat_sent",
"quick_chat_skipped",
"combat_model_estimate"
]);
function createRoundLogger({ app, settings, gameState, mapDataRef, logger }) {
let session = null;
let lastSnapshotAt = 0;
let lastAdvisorView = null;
let lastExportAt = null;
let truncated = false;
let sequence = 0;
let approxBytes = 0;
return {
start(reason = "start") {
if (!settings.get("roundLogging")) return;
if (session && !session.endedAt) this.end("restarted", { autoDownload: false });
const startedAt = /* @__PURE__ */ new Date();
session = {
meta: {
appName: app.name,
appVersion: app.version,
createdAt: startedAt.toISOString(),
endedAt: null,
durationMs: null,
reason,
truncated: false
},
settings: summarizeSettings(settings.snapshot()),
map: summarizeMap(mapDataRef.current),
timeline: [],
finalState: null
};
truncated = false;
sequence = 0;
approxBytes = 0;
lastExportAt = null;
lastSnapshotAt = performance.now();
push("round_started", {});
this.snapshot("initial");
},
end(reason = "socket_closed", options = {}) {
if (!session || session.endedAt) return;
this.snapshot("final");
const endedAt = /* @__PURE__ */ new Date();
session.meta.endedAt = endedAt.toISOString();
session.meta.durationMs = Date.parse(session.meta.endedAt) - Date.parse(session.meta.createdAt);
session.meta.reason = reason;
session.meta.truncated = truncated;
session.finalState = {
tick: gameState.state.currentTick,
myState: summarizeMyState(gameState, settings.get("roundLogKeepPlayerNames")),
advisor: summarizeAdvisorView(lastAdvisorView)
};
push("round_ended", { reason });
const shouldDownload = options.autoDownload ?? settings.get("roundLogAutoDownload");
if (shouldDownload && settings.get("roundLogging")) this.export("auto");
},
record(type, data = {}) {
if (!session || session.endedAt || !settings.get("roundLogging")) return;
push(type, data);
},
recordSettingChange(key, value) {
if (!session || session.endedAt || !settings.get("roundLogging")) return;
push("setting_changed", { key, value });
},
recordMapLoaded(mapData) {
if (!settings.get("roundLogging")) return;
if (session) session.map = summarizeMap(mapData);
push("map_loaded", summarizeMap(mapData));
},
recordAdvisor(viewModel) {
lastAdvisorView = viewModel;
if (!session || session.endedAt || !settings.get("roundLogging")) return;
push("advisor", summarizeAdvisorView(viewModel));
},
maybeSnapshot() {
if (!session || session.endedAt || !settings.get("roundLogging")) return;
const now = performance.now();
const interval = Number(settings.get("roundLogSnapshotIntervalMs")) || 5e3;
if (now - lastSnapshotAt < interval) return;
lastSnapshotAt = now;
this.snapshot("interval");
},
snapshot(reason = "manual") {
if (!session || session.endedAt || !settings.get("roundLogging")) return;
push("snapshot", {
reason,
myClientID: gameState.state.myClientID,
myPlayerID: gameState.state.myPlayerID,
playerCount: gameState.state.playerStates.size,
players: summarizePlayers(gameState, settings.get("roundLogKeepPlayerNames"))
});
},
export(reason = "manual") {
if (!session) {
logger.warn("No round log session to export");
return;
}
const payload = {
...session,
meta: {
...session.meta,
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
exportReason: reason,
truncated
}
};
downloadJson(createRoundLogFilename(new Date(session.meta.createdAt)), payload);
lastExportAt = payload.meta.exportedAt;
},
clear() {
session = null;
lastAdvisorView = null;
lastSnapshotAt = 0;
lastExportAt = null;
truncated = false;
sequence = 0;
approxBytes = 0;
logger.info("Round log cleared");
},
hasSession() {
return !!session;
},
getStatus() {
return {
active: !!session && !session.endedAt,
hasSession: !!session,
eventCount: session ? session.timeline.length : 0,
truncated,
lastExportAt,
endedAt: session?.meta?.endedAt || null
};
}
};
function entrySize(entry) {
return JSON.stringify(entry).length + 1;
}
function push(type, data) {
if (!session) return;
const entry = {
seq: ++sequence,
timeMs: Date.now() - Date.parse(session.meta.createdAt),
tick: gameState.state.currentTick,
type,
data
};
session.timeline.push(entry);
approxBytes += entrySize(entry);
enforceTimelineLimit();
}
function enforceTimelineLimit() {
if (!session || approxBytes <= MAX_TIMELINE_BYTES) return;
const removableTypes = ["network_request", "advisor", "snapshot", "log_truncated"];
let removed = 0;
const dropMatching = (predicate) => {
for (let i = 0; i < session.timeline.length && approxBytes > MAX_TIMELINE_BYTES; ) {
if (predicate(session.timeline[i])) {
approxBytes -= entrySize(session.timeline[i]);
session.timeline.splice(i, 1);
removed += 1;
} else {
i += 1;
}
}
};
for (const removableType of removableTypes) {
if (approxBytes <= MAX_TIMELINE_BYTES) break;
dropMatching((entry) => entry.type === removableType);
}
if (approxBytes > MAX_TIMELINE_BYTES) {
dropMatching((entry) => !PROTECTED_LOG_TYPES.has(entry.type));
}
if (removed === 0) return;
truncated = true;
const last = session.timeline[session.timeline.length - 1];
if (last && last.type === "log_truncated") {
approxBytes -= entrySize(last);
last.data.removedEntries += removed;
last.timeMs = Date.now() - Date.parse(session.meta.createdAt);
last.tick = gameState.state.currentTick;
approxBytes += entrySize(last);
} else {
const entry = {
seq: ++sequence,
timeMs: Date.now() - Date.parse(session.meta.createdAt),
tick: gameState.state.currentTick,
type: "log_truncated",
data: { removedEntries: removed, maxTimelineBytes: MAX_TIMELINE_BYTES }
};
session.timeline.push(entry);
approxBytes += entrySize(entry);
}
}
}
// src/page/logging/network-logger.js
function installNetworkMetadataLogger({ settings, roundLogger }) {
installFetchLogger(settings, roundLogger);
installXhrLogger(settings, roundLogger);
installBeaconLogger(settings, roundLogger);
}
function installFetchLogger(settings, roundLogger) {
const origFetch = window.fetch;
if (typeof origFetch !== "function") return;
window.fetch = function loggedFetch(input, init) {
const startedAt = performance.now();
const requestInfo = getFetchRequestInfo(input, init);
return origFetch.apply(this, arguments).then(
(response) => {
recordNetwork(settings, roundLogger, {
...requestInfo,
status: response.status,
ok: response.ok,
durationMs: performance.now() - startedAt,
responseSize: getResponseSize(response),
initiator: "fetch"
});
return response;
},
(error) => {
recordNetwork(settings, roundLogger, {
...requestInfo,
status: 0,
ok: false,
durationMs: performance.now() - startedAt,
responseSize: 0,
initiator: "fetch",
error: error && error.name ? error.name : "FetchError"
});
throw error;
}
);
};
}
function installXhrLogger(settings, roundLogger) {
if (!window.XMLHttpRequest || !XMLHttpRequest.prototype) return;
const origOpen = XMLHttpRequest.prototype.open;
const origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function loggedOpen(method, url) {
this.__ofatNetworkInfo = {
method: String(method || "GET").toUpperCase(),
url: sanitizeUrl(url),
sameOrigin: isSameOrigin(url),
requestType: classifyUrl(url)
};
return origOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function loggedSend() {
const startedAt = performance.now();
this.addEventListener("loadend", () => {
const info = this.__ofatNetworkInfo || {};
recordNetwork(settings, roundLogger, {
method: info.method || "GET",
url: info.url || "",
sameOrigin: !!info.sameOrigin,
requestType: info.requestType || "other",
status: this.status || 0,
ok: this.status >= 200 && this.status < 400,
durationMs: performance.now() - startedAt,
responseSize: getXhrResponseSize(this),
initiator: "xhr"
});
});
return origSend.apply(this, arguments);
};
}
function installBeaconLogger(settings, roundLogger) {
if (!navigator.sendBeacon) return;
const origSendBeacon = navigator.sendBeacon.bind(navigator);
navigator.sendBeacon = function loggedSendBeacon(url, data) {
const startedAt = performance.now();
const ok = origSendBeacon(url, data);
recordNetwork(settings, roundLogger, {
method: "POST",
url: sanitizeUrl(url),
sameOrigin: isSameOrigin(url),
requestType: classifyUrl(url),
status: ok ? 204 : 0,
ok,
durationMs: performance.now() - startedAt,
responseSize: 0,
initiator: "beacon"
});
return ok;
};
}
function recordNetwork(settings, roundLogger, event) {
if (!settings.get("networkLogging")) return;
roundLogger.record("network_request", {
method: event.method || "GET",
url: event.url || "",
status: event.status || 0,
ok: !!event.ok,
durationMs: round1(event.durationMs),
requestType: event.requestType || "other",
responseSize: event.responseSize || 0,
sameOrigin: !!event.sameOrigin,
initiator: event.initiator || "unknown",
error: event.error || void 0
});
}
function getFetchRequestInfo(input, init) {
const url = typeof input === "string" ? input : input && input.url;
const method = init && init.method ? init.method : input && input.method ? input.method : "GET";
return {
method: String(method || "GET").toUpperCase(),
url: sanitizeUrl(url),
sameOrigin: isSameOrigin(url),
requestType: classifyUrl(url)
};
}
function sanitizeUrl(value) {
try {
const url = new URL(String(value || ""), location.href);
return \`\${url.origin === location.origin ? "" : url.origin}\${url.pathname}\${url.search ? "?..." : ""}\`;
} catch (_) {
return String(value || "").split("?")[0];
}
}
function isSameOrigin(value) {
try {
return new URL(String(value || ""), location.href).origin === location.origin;
} catch (_) {
return false;
}
}
function classifyUrl(value) {
const url = sanitizeUrl(value);
if (/\\/maps\\//.test(url)) return "map";
if (/\\/api\\//.test(url)) return "api";
if (/\\/assets\\/|\\/_assets\\//.test(url)) return "asset";
if (/\\/analytics|\\/collect|\\/beacon/.test(url)) return "telemetry";
return "other";
}
function getResponseSize(response) {
const length = response.headers && response.headers.get ? response.headers.get("content-length") : null;
const parsed = Number(length);
return Number.isFinite(parsed) ? parsed : 0;
}
function getXhrResponseSize(xhr) {
const length = xhr.getResponseHeader ? Number(xhr.getResponseHeader("content-length")) : 0;
if (Number.isFinite(length) && length > 0) return length;
if (typeof xhr.responseText === "string") return xhr.responseText.length;
if (xhr.response instanceof ArrayBuffer) return xhr.response.byteLength;
return 0;
}
function round1(value) {
const number = Number(value);
if (!Number.isFinite(number)) return 0;
return Math.round(number * 10) / 10;
}
// src/page/ui/control-hud.js
function createControlHud({ app, settings, roundLogger, teamDetection, getTroopEconomy }) {
let el = null;
return {
create() {
removeElementById("ofat-control-hud");
installHudStyles();
el = document.createElement("div");
el.id = "ofat-control-hud";
appendWhenReady(el, "body");
this.render();
},
render() {
if (!el) return;
const status = roundLogger.getStatus();
const team = teamDetection?.isTeamMode ? teamDetection.teamSummary() : null;
const economy = settings.get("showTroopEconomy") ? getTroopEconomy?.() || null : null;
el.innerHTML = renderHudHtml(app, settings, status, team, economy);
el.querySelectorAll("button[data-setting]").forEach((button) => {
button.addEventListener("click", () => settings.set(button.dataset.setting, !settings.get(button.dataset.setting)));
});
const exportButton = el.querySelector("[data-action='export']");
if (exportButton) {
exportButton.addEventListener("click", () => {
roundLogger.export("hud");
this.render();
});
}
const clearButton = el.querySelector("[data-action='clear']");
if (clearButton) {
clearButton.addEventListener("click", () => {
roundLogger.clear();
this.render();
});
}
}
};
}
function renderHudHtml(app, settings, status, team, economy) {
const logState = status.active ? "REC" : status.hasSession ? "END" : "IDLE";
const lastExport = status.lastExportAt ? "exportiert" : "nicht exportiert";
const truncated = status.truncated ? " gekuerzt" : "";
const teamLine = team ? \`<div class="ofat-hud-team"><span class="ofat-hud-dot" style="background:\${safeColor(team.myTeamColor)}"></span>Team: \${escapeHtml(team.myTeamName)} (\${team.memberCount})</div>\` : "";
const ecoLine = economy ? \`<div class="ofat-hud-eco">Eco: <b>\${escapeHtml(economy.state)}</b> | Safe \${escapeHtml(formatTroopCount(economy.safeSpendableTroops))}</div>\` : "";
return \`
<div class="ofat-hud-title">\${app.shortName} <span>\${logState}</span></div>
<div class="ofat-hud-status">\${status.eventCount} Events | \${lastExport}\${truncated}</div>
\${teamLine}
\${ecoLine}
<div class="ofat-hud-grid">
\${toggle("Rundenlog", "roundLogging", settings.get("roundLogging"))}
\${toggle("Auto-Download", "roundLogAutoDownload", settings.get("roundLogAutoDownload"))}
\${toggle("Netzwerk-Log", "networkLogging", settings.get("networkLogging"))}
\${toggle("Advisor-Panel", "showAdvisorPanel", settings.get("showAdvisorPanel"))}
\${toggle("Heatmap", "showHeatmap", settings.get("showHeatmap"))}
\${toggle("Dyn. Reserve", "autoFarmDynamicReserve", settings.get("autoFarmDynamicReserve"))}
\${toggle("Eco-Panel", "showTroopEconomy", settings.get("showTroopEconomy"))}
\${toggle("Team-Support", "autoTeamSupport", settings.get("autoTeamSupport"), true)}
\${toggle("Auto-SOS", "autoSosQuickChat", settings.get("autoSosQuickChat"), true)}
\${toggle("Auto-Defense", "autoDefense", settings.get("autoDefense"), true)}
\${toggle("Auto-Boat", "autoBoat", settings.get("autoBoat"), true)}
\${toggle("Auto-Weapons", "autoWeapons", settings.get("autoWeapons"), true)}
\${toggle("Attack-Badges", "showAttackBadges", settings.get("showAttackBadges"))}
</div>
<div class="ofat-hud-actions">
<button data-action="export">Log exportieren</button>
<button data-action="clear">Log leeren</button>
</div>
\`;
}
function safeColor(value) {
return /^#[0-9a-fA-F]{3,8}$|^[a-zA-Z]+$|^rgb/.test(String(value || "")) ? value : "#71c7ff";
}
function toggle(label, key, enabled, warn = false) {
return \`<button data-setting="\${key}" data-on="\${enabled ? "true" : "false"}" data-warn="\${warn && enabled ? "true" : "false"}">\${label}</button>\`;
}
function installHudStyles() {
createStyle(
"ofat-control-hud-style",
\`
#ofat-control-hud {
position: fixed;
top: 10px;
left: 10px;
z-index: 100000;
width: 330px;
padding: 10px;
color: #f4f7fb;
background: rgba(8, 10, 13, 0.92);
border: 1px solid rgba(113, 191, 255, 0.45);
border-radius: 8px;
box-shadow: 0 10px 26px rgba(0, 0, 0, 0.32);
font: 12px/1.25 ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
user-select: none;
}
#ofat-control-hud .ofat-hud-title {
display: flex;
justify-content: space-between;
gap: 8px;
color: #71c7ff;
font-weight: 700;
margin-bottom: 4px;
}
#ofat-control-hud .ofat-hud-title span {
color: #f4f7fb;
}
#ofat-control-hud .ofat-hud-status {
color: rgba(244, 247, 251, 0.68);
margin-bottom: 8px;
}
#ofat-control-hud .ofat-hud-team {
display: flex;
align-items: center;
gap: 6px;
color: rgba(244, 247, 251, 0.85);
margin-bottom: 8px;
}
#ofat-control-hud .ofat-hud-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.6);
}
#ofat-control-hud .ofat-hud-eco {
color: rgba(244, 247, 251, 0.85);
margin-bottom: 8px;
}
#ofat-control-hud .ofat-hud-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
}
#ofat-control-hud .ofat-hud-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
margin-top: 7px;
}
#ofat-control-hud button {
min-width: 0;
height: 32px;
padding: 0 8px;
color: #f4f7fb;
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 5px;
background: rgba(255, 255, 255, 0.1);
font: inherit;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#ofat-control-hud button[data-on="true"] {
background: rgba(46, 125, 50, 0.88);
}
#ofat-control-hud button[data-warn="true"] {
background: rgba(183, 28, 28, 0.88);
}
#ofat-control-hud button:hover {
border-color: rgba(113, 191, 255, 0.72);
}
\`
);
}
// src/page/ui/action-hud.js
var ACTION_SETTINGS = [
{ key: "autopilot", label: "Autopilot", warn: true },
{ key: "autoExpand", label: "Expand", warn: true },
{ key: "autoFarm", label: "Farm", warn: true },
{ key: "autoFarmHumanTargets", label: "Human", warn: true },
{ key: "autoEco", label: "Eco", warn: true },
{ key: "autoDefense", label: "Defense", warn: true },
{ key: "autoBoat", label: "Boat", warn: true },
{ key: "autoWeapons", label: "Weapons", warn: true },
{ key: "autoTeamSupport", label: "Team", warn: true },
{ key: "autoSosQuickChat", label: "SOS", warn: true },
{ key: "smartAttack", label: "Smart", warn: true }
];
function createActionHud({ settings, getViewModel, getAutomationStatus }) {
let el = null;
return {
create() {
removeElementById("ofat-action-hud");
installActionHudStyles();
el = document.createElement("div");
el.id = "ofat-action-hud";
appendWhenReady(el, "body");
this.render();
},
render() {
if (!el) return;
el.innerHTML = renderActionHudHtml(settings, getViewModel?.() || null, getAutomationStatus?.() || {});
el.querySelectorAll("button[data-setting]").forEach((button) => {
button.addEventListener("click", () => settings.set(button.dataset.setting, !settings.get(button.dataset.setting)));
});
}
};
}
function renderActionHudHtml(settings, vm, status) {
const economy = vm?.troopEconomy;
const expansion = vm?.expansion?.level || "unknown";
const threat = vm?.topThreats?.[0] ? \`\${vm.topThreats[0].name} \${vm.topThreats[0].level}\` : "safe";
const ecoState = economy ? \`\${economy.state} \${Math.round((economy.currentRatio || 0) * 100)}%\` : "idle";
const target = (vm?.farmRecommendations || []).find((rec) => rec.status === "farm") || null;
const support = (vm?.teamSupportRecommendations || [])[0] || null;
let html = \`<div class="ofat-action-row ofat-action-toggles">\`;
ACTION_SETTINGS.forEach((item) => {
html += actionToggle(settings, item);
});
html += \`</div>\`;
html += \`<div class="ofat-action-row ofat-action-state">\`;
html += actionPill("Spawn", summarizeStatus(status.autoSpawn, "idle"), toneFromStatus(status.autoSpawn));
html += actionPill("Eco", ecoState, toneFromStatus(status.autoEco));
html += actionPill("Expand", summarizeStatus(status.autoExpand, expansion), toneFromStatus(status.autoExpand));
html += actionPill("Farm", target ? target.label : summarizeStatus(status.autoFarm, "none"), target ? "ready" : toneFromStatus(status.autoFarm));
html += actionPill("Defense", defenseSummary(status.autoDefense), status.autoDefense?.emergency ? "warn" : toneFromStatus(status.autoDefense));
if (settings.get("autoBoat")) html += actionPill("Boat", summarizeStatus(status.autoBoat, "idle"), toneFromStatus(status.autoBoat));
if (settings.get("autoWeapons")) html += actionPill("Weapons", weaponsSummary(status.autoWeapons), toneFromStatus(status.autoWeapons));
html += actionPill("Team", support ? \`\${support.name} \${Math.round(support.troops || 0)}\` : summarizeStatus(status.autoTeamSupport, "idle"), support ? "ready" : toneFromStatus(status.autoTeamSupport));
html += actionPill("SOS", summarizeStatus(status.quickChat, "idle"), toneFromStatus(status.quickChat));
html += actionPill("Threat", threat, threat === "safe" ? "ok" : "warn");
html += \`</div>\`;
return html;
}
function actionToggle(settings, item) {
const on = !!settings.get(item.key);
const tone = item.warn && on ? "warn" : on ? "on" : "off";
return \`<button data-setting="\${escapeHtml(item.key)}" data-tone="\${tone}">\${escapeHtml(item.label)}</button>\`;
}
function actionPill(label, value, tone) {
return \`<span class="ofat-action-pill" data-tone="\${escapeHtml(tone || "ok")}"><b>\${escapeHtml(label)}</b> \${escapeHtml(String(value || ""))}</span>\`;
}
function weaponsSummary(status) {
if (!status) return "idle";
if (status.unit) return status.target ? \`\${status.unit}->\${status.target}\` : status.unit;
return status.reason || "idle";
}
function defenseSummary(status) {
if (!status) return "safe";
if (status.emergency) return status.lastAction || (status.topAttacker ? \`ATK \${status.topAttacker}\` : "under attack");
return status.reason === "no_pressure" ? "safe" : status.reason || "idle";
}
function summarizeStatus(status, fallback) {
if (!status) return fallback;
if (status.targetName) return status.targetName;
if (status.unit) return status.unit;
if (status.reason) return status.reason;
return fallback;
}
function toneFromStatus(status) {
if (!status) return "ok";
if (status.state === "sent") return "ready";
if (status.state === "blocked") return "warn";
if (status.state === "cooldown") return "cooldown";
return "ok";
}
function installActionHudStyles() {
createStyle(
"ofat-action-hud-style",
\`
#ofat-action-hud {
position: fixed;
top: 10px;
left: 50%;
transform: translateX(-50%);
z-index: 100001;
width: min(720px, calc(100vw - 24px));
padding: 8px;
color: #f4f7fb;
background: rgba(8, 10, 13, 0.9);
border: 1px solid rgba(255, 190, 72, 0.55);
border-radius: 8px;
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.32);
font: 12px/1.25 ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
user-select: none;
}
#ofat-action-hud .ofat-action-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
justify-content: center;
}
#ofat-action-hud .ofat-action-state {
margin-top: 6px;
}
#ofat-action-hud button {
min-width: 72px;
height: 28px;
padding: 0 8px;
color: #f4f7fb;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 5px;
background: rgba(255, 255, 255, 0.1);
font: inherit;
cursor: pointer;
}
#ofat-action-hud button[data-tone="warn"] { background: rgba(183, 28, 28, 0.88); }
#ofat-action-hud button[data-tone="on"] { background: rgba(46, 125, 50, 0.88); }
#ofat-action-hud .ofat-action-pill {
max-width: 170px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 3px 7px;
border-radius: 5px;
background: rgba(255, 255, 255, 0.1);
color: rgba(244, 247, 251, 0.86);
}
#ofat-action-hud .ofat-action-pill[data-tone="ready"] { background: rgba(46, 125, 50, 0.68); }
#ofat-action-hud .ofat-action-pill[data-tone="warn"] { background: rgba(183, 28, 28, 0.72); }
#ofat-action-hud .ofat-action-pill[data-tone="cooldown"] { background: rgba(255, 190, 72, 0.35); }
\`
);
}
// src/page/page-main.js
var RECALC_INTERVAL = 1e3;
function startPage(payload) {
if (!payload || !payload.appInfo) return;
installGameViewCapture();
const app = payload.appInfo;
const logger = createLogger(app);
const bus = createEventBus();
const settings = createPageSettingsStore(app, DEFAULT_SETTINGS, payload.initialSettings || DEFAULT_SETTINGS);
const gameState = createGameState();
const mapDataRef = { current: null };
const staticCandidatesRef = { current: null };
const neighbourFetcher = createNeighbourFetcher();
const goldIntel = createGoldIntel();
let attackAdvisor = null;
let teamDetection = null;
let roundLogger = null;
let recalcTimer = null;
let ui = null;
let heatmap = null;
let controlHud = null;
let actionHud = null;
let autoSpawn = null;
let autoAlliance = null;
let autoFarm = null;
let autoEco = null;
let autoDefense = null;
let autoBoat = null;
let autoWeapons = null;
let autoTeamSupport = null;
let quickChat = null;
let autoExpand = null;
let assistActions = null;
let autopilot = null;
let lastViewModel = null;
let lastTroopEconomy = null;
let lastBuyRecommendationLoggedAt = 0;
let lastFarmRecommendationLoggedAt = 0;
let lastGameViewDiscoveryKey = null;
let lastMechanicsLogAt = 0;
let observedIntentCount = 0;
const OBSERVED_INTENT_CAP = 400;
logger.banner();
if (settings.get("hideAds")) hideAds();
roundLogger = createRoundLogger({ app, settings, gameState, mapDataRef, logger });
teamDetection = createTeamDetection({ gameState, logger, roundLogger });
teamDetection.observeLobby();
attackAdvisor = createAttackAdvisor({ settings, teamDetection });
const smartAttackModifier = createSmartAttackModifier({
settings,
gameState,
logger,
roundLogger,
teamDetection,
getTroopEconomy: () => lastTroopEconomy
});
const network = installNetworkHooks({
bus,
getSmartAttackModifier: () => smartAttackModifier,
logger
});
installMapAssetHook({ bus, logger });
installNetworkMetadataLogger({ settings, roundLogger });
controlHud = createControlHud({ app, settings, roundLogger, teamDetection, getTroopEconomy: () => lastTroopEconomy });
controlHud.create();
actionHud = createActionHud({
settings,
getViewModel: () => lastViewModel,
getAutomationStatus: () => ({
autoExpand: autoExpand?.getStatus?.() || null,
autoFarm: autoFarm?.getStatus?.() || null,
autoEco: autoEco?.getStatus?.() || null,
autoDefense: autoDefense?.getStatus?.() || null,
autoBoat: autoBoat?.getStatus?.() || null,
autoWeapons: autoWeapons?.getStatus?.() || null,
autoSpawn: autoSpawn?.getStatus?.() || null,
autoTeamSupport: autoTeamSupport?.getStatus?.() || null,
quickChat: quickChat?.getStatus?.() || null
})
});
actionHud.create();
registerEvents();
function registerEvents() {
bus.on("mapLoaded", (mapData) => {
mapDataRef.current = mapData;
const startedAt = performance.now();
staticCandidatesRef.current = precomputeStaticScores(mapData);
logger.info(
\`Static spawn analysis: \${staticCandidatesRef.current.length} candidates in \${(performance.now() - startedAt).toFixed(0)}ms\`
);
roundLogger.recordMapLoaded(mapData);
heatmap = createHeatmap({ mapDataRef });
ui = createAdvisorPanel({
app,
settings,
heatmap,
actions: {
onManualAssistAttack: () => handleManualAssistAttack(),
onUnavailableBuild: (actionKey) => handleUnavailableBuild(actionKey)
}
});
ui.create();
ui.attachHeatmap(heatmap.create(settings.get("showHeatmap")));
autoSpawn = createAutoSpawn({
gameState,
mapDataRef,
OrigWS: network.OrigWS,
origWsSend: network.origWsSend,
settings,
logger,
roundLogger,
teamDetection
});
autoFarm = createAutoFarm({
gameState,
OrigWS: network.OrigWS,
origWsSend: network.origWsSend,
settings,
logger,
roundLogger,
getTroopEconomy: () => lastTroopEconomy
});
autoExpand = createAutoExpand({
gameState,
neighbourFetcher,
OrigWS: network.OrigWS,
origWsSend: network.origWsSend,
settings,
logger,
roundLogger,
getTroopEconomy: () => lastTroopEconomy
});
autoEco = createAutoEco({
gameState,
mapDataRef,
OrigWS: network.OrigWS,
origWsSend: network.origWsSend,
settings,
logger,
roundLogger,
getTroopEconomy: () => lastTroopEconomy
});
autoDefense = createAutoDefense({
gameState,
mapDataRef,
OrigWS: network.OrigWS,
origWsSend: network.origWsSend,
settings,
logger,
roundLogger,
teamDetection,
getTroopEconomy: () => lastTroopEconomy
});
autoBoat = createAutoBoat({
gameState,
mapDataRef,
OrigWS: network.OrigWS,
origWsSend: network.origWsSend,
settings,
logger,
roundLogger,
getTroopEconomy: () => lastTroopEconomy
});
autoWeapons = createAutoWeapons({
gameState,
mapDataRef,
OrigWS: network.OrigWS,
origWsSend: network.origWsSend,
settings,
logger,
roundLogger,
teamDetection,
getTroopEconomy: () => lastTroopEconomy
});
autoTeamSupport = createAutoTeamSupport({
gameState,
OrigWS: network.OrigWS,
origWsSend: network.origWsSend,
settings,
logger,
roundLogger,
teamDetection,
getTroopEconomy: () => lastTroopEconomy
});
quickChat = createQuickChatAutomation({
gameState,
OrigWS: network.OrigWS,
origWsSend: network.origWsSend,
settings,
logger,
roundLogger,
teamDetection
});
assistActions = createAssistActions({
gameState,
OrigWS: network.OrigWS,
origWsSend: network.origWsSend,
settings,
logger,
roundLogger,
teamDetection
});
autoAlliance = createAutoAlliance({
gameState,
neighbourFetcher,
OrigWS: network.OrigWS,
origWsSend: network.origWsSend,
logger,
roundLogger,
teamDetection
});
autopilot = createAutopilotController({
settings,
gameState,
autoSpawn,
autoFarm,
autoExpand,
autoAlliance,
autoEco,
autoDefense,
roundLogger
});
recalculate();
startRecalcLoop();
});
bus.on("wsMessage", (message) => {
gameState.handleWsMessage(message, mapDataRef.current);
if (message.type === "start") {
roundLogger.start("websocket_start");
if (Array.isArray(message.turns)) message.turns.forEach((turn) => recordMyIntents(turn));
controlHud?.render();
} else if (message.type === "turn") {
recordMyIntents(message.turn);
roundLogger.maybeSnapshot();
}
});
bus.on("workerGameUpdateBatch", (batch) => {
gameState.handleWorkerBatch(batch);
roundLogger.maybeSnapshot();
});
bus.on("socketReady", (socket) => gameState.setSocket(socket));
bus.on("socketClosed", (socket) => {
roundLogger.end("socket_closed");
gameState.resetSocket(socket);
autoSpawn?.reset();
autoAlliance?.reset();
autoFarm?.reset();
autoEco?.reset();
autoDefense?.reset();
autoBoat?.reset();
autoWeapons?.reset();
autoTeamSupport?.reset();
quickChat?.reset();
autoExpand?.reset();
autopilot?.reset();
attackAdvisor.reset();
teamDetection.reset();
resetGameViewCache();
goldIntel.reset();
lastGameViewDiscoveryKey = null;
lastTroopEconomy = null;
controlHud?.render();
actionHud?.render();
});
settings.onChange(({ key, value }) => {
if (key === "showHeatmap") heatmap?.setVisible(value);
if (key === "showAdvisorPanel") ui?.setVisible(value);
if (key === "autoSpawn" && !value) autoSpawn?.reset();
if (key === "autoFarm" && !value) autoFarm?.reset();
if (key === "autoEco" && !value) autoEco?.reset();
if (key === "autoDefense" && !value) autoDefense?.reset();
if (key === "autoBoat" && !value) autoBoat?.reset();
if (key === "autoWeapons" && !value) autoWeapons?.reset();
if (key === "autoTeamSupport" && !value) autoTeamSupport?.reset();
if (key === "autoSosQuickChat" && !value) quickChat?.reset();
if (key === "autoExpand" && !value) autoExpand?.reset();
roundLogger.recordSettingChange(key, value);
controlHud?.render();
actionHud?.render();
recalculate();
});
window.addEventListener("message", (event) => {
const data = event && event.data;
if (!data || data.source !== app.messageSource) return;
if (data.type === "export-round-log") roundLogger.export("manual");
if (data.type === "clear-round-log") roundLogger.clear();
if (data.type === "export-round-log" || data.type === "clear-round-log") controlHud?.render();
if (data.type === "export-round-log" || data.type === "clear-round-log") actionHud?.render();
});
}
function startRecalcLoop() {
if (recalcTimer) return;
recalcTimer = setInterval(recalculate, RECALC_INTERVAL);
}
function recalculate() {
if (!mapDataRef.current || !staticCandidatesRef.current || !ui) return;
teamDetection.syncFromGameView();
recordGameViewDiscovery();
const teammateClientIDs = teamDetection.myTeammateClientIDs();
const results = rankSpawnCandidates(staticCandidatesRef.current, gameState.state.playerSpawns, { teammateClientIDs });
const myState = gameState.getMyState();
const troopInfo = getMyTroopRatio();
const siloIntel = scanMissileSilos(getGameView(), myState, teamDetection);
const nukeBuilders = enemyNukeBuilders(siloIntel);
goldIntel.sample(gameState);
const threats = Array.from(evaluateThreats(gameState, teamDetection, nukeBuilders, goldIntel).values()).sort((a, b) => b.score - a.score).slice(0, 3);
neighbourFetcher.refresh();
const troopEconomy = evaluateTroopEconomy({
troopInfo,
myState,
gameState,
neighbours: neighbourFetcher.cachedNeighbourIDs,
settings,
teamDetection,
threats
});
lastTroopEconomy = troopEconomy;
const expansion = evaluateExpansionState(troopInfo, myState, troopEconomy);
const farmRecommendations = attackAdvisor.evaluate({
gameState,
neighbourFetcher,
troopInfo,
expansion,
troopEconomy,
mapData: mapDataRef.current
});
const allianceRecommendations = recommendAlliances(gameState, teamDetection);
const buyRecommendations = recommendBuys({ gameState, troopInfo, threats, farmRecommendations, troopEconomy });
const spawnPhase = getSpawnPhaseInfo({ gameView: getGameView(), state: gameState.state, teamMode: !!teamDetection?.isTeamMode });
recordMechanicsDiscovery(spawnPhase);
const teamSupportRecommendations = autoTeamSupport?.recommend?.() || [];
const allyCount = myState && myState.alliances ? myState.alliances.length : 0;
const assistTarget = chooseAssistTarget(farmRecommendations);
const nukeThreatActive = nukeBuilders.size > 0 || threats.some((threat) => threat.nukePotential >= 1 || threat.nukeSoon);
const viewModel = {
scores: results.scores,
topSpots: results.topSpots,
playerSpawns: gameState.state.playerSpawns,
otherPlayerCount: gameState.state.playerSpawns.size,
nationCount: mapDataRef.current.nations.length,
tick: gameState.state.currentTick,
topThreats: threats,
expansion,
troopEconomy,
farmRecommendations,
assistTarget,
buyRecommendations,
siloIntel,
nukeThreatActive,
spawnPhase,
teamSupportRecommendations,
allianceRecommendations,
allyCount,
lastSkippedNeighbour: autoAlliance?.lastSkippedNeighbour || null,
automationStatus: {
autoExpand: autoExpand?.getStatus?.() || null,
autoFarm: autoFarm?.getStatus?.() || null,
autoEco: autoEco?.getStatus?.() || null,
autoDefense: autoDefense?.getStatus?.() || null,
autoBoat: autoBoat?.getStatus?.() || null,
autoWeapons: autoWeapons?.getStatus?.() || null,
autoSpawn: autoSpawn?.getStatus?.() || null,
autoTeamSupport: autoTeamSupport?.getStatus?.() || null,
quickChat: quickChat?.getStatus?.() || null
}
};
lastViewModel = viewModel;
ui.render(viewModel);
publishFarmRecommendations(farmRecommendations);
roundLogger.recordAdvisor(viewModel);
recordFarmRecommendations(farmRecommendations);
recordBuyRecommendations(buyRecommendations);
roundLogger.maybeSnapshot();
controlHud?.render();
actionHud?.render();
const autopilotState = autopilot?.run({
topSpot: results.topSpots[0] || null,
farmRecommendations,
buyRecommendations,
expansion,
threats,
neighbourIDs: neighbourFetcher.cachedNeighbourIDs,
troopInfo,
nukeThreat: nukeThreatActive
});
if (!autopilotState?.defenseHandled && settings.get("autoDefense")) {
autoDefense?.run({ nukeThreat: nukeThreatActive, threats, neighbourIDs: neighbourFetcher.cachedNeighbourIDs });
}
const underAttack = !!autoDefense?.getStatus?.()?.emergency;
if (!autopilotState?.spawnHandled && settings.get("autoSpawn") && results.topSpots.length > 0) autoSpawn?.send(results.topSpots[0]);
if (!underAttack && !autopilotState?.expandHandled && settings.get("autoExpand")) autoExpand?.run(troopInfo);
if (!underAttack && !autopilotState?.farmHandled && settings.get("autoFarm")) autoFarm?.run(farmRecommendations, expansion);
if (!underAttack && !autopilotState?.ecoHandled && settings.get("autoEco")) autoEco?.run({ buyRecommendations });
if (!underAttack && settings.get("autoBoat")) autoBoat?.run();
if (!underAttack && settings.get("autoWeapons")) autoWeapons?.run({ threats });
if (!autopilotState?.allianceHandled && settings.get("autoAlliance")) autoAlliance?.run();
if (settings.get("autoTeamSupport")) {
autoTeamSupport?.run({
neighbourIDs: neighbourFetcher.cachedNeighbourIDs,
automationStatus: {
autoDefense: autoDefense?.getStatus?.() || null,
autoExpand: autoExpand?.getStatus?.() || null,
autoFarm: autoFarm?.getStatus?.() || null
}
});
}
if (settings.get("autoSosQuickChat")) quickChat?.runAutoSos({ teamSupportStatus: autoTeamSupport?.getStatus?.() || null });
actionHud?.render();
}
function handleManualAssistAttack() {
const target = lastViewModel?.assistTarget;
const result = assistActions?.sendManualAssistAttack(target);
if (!result?.ok) logger.warn(\`Manual assist attack skipped: \${result?.reason || "unknown"}\`);
}
function handleUnavailableBuild(actionKey) {
roundLogger.record("build_button_unavailable", { actionKey, reason: "intent_not_verified" });
logger.warn(\`Build action \${actionKey} is not verified yet\`);
}
function chooseAssistTarget(recommendations) {
return (recommendations || []).find((rec) => rec.isHuman && rec.status === "target") || null;
}
function recordGameViewDiscovery() {
const discovery = getGameViewDiscovery();
if (!discovery) return;
const key = JSON.stringify(discovery);
if (key === lastGameViewDiscoveryKey && roundLogger.hasSession()) return;
if (!roundLogger.hasSession()) return;
lastGameViewDiscoveryKey = key;
roundLogger.record("gameview_discovered", discovery);
}
function recordMechanicsDiscovery(spawnPhase) {
const now = performance.now();
if (now - lastMechanicsLogAt < 15e3) return;
lastMechanicsLogAt = now;
roundLogger.record("mechanics_discovered", {
spawnPhaseTurns: spawnPhase?.totalTurns || null,
spawnPhaseSource: spawnPhase?.source || null
});
}
function recordMyIntents(turn) {
if (!turn || !Array.isArray(turn.intents)) return;
const myClientID = gameState.state.myClientID;
if (!myClientID) return;
for (let i = 0; i < turn.intents.length; i += 1) {
const intent = turn.intents[i];
if (!intent || intent.clientID !== myClientID) continue;
autoEco?.observeIntent?.(intent);
autoDefense?.observeIntent?.(intent);
quickChat?.observeIntent?.(intent);
if (observedIntentCount >= OBSERVED_INTENT_CAP) return;
observedIntentCount += 1;
roundLogger.record("intent_observed", summarizeIntent(intent));
if (intent.type === "boat") roundLogger.record("boat_intent_observed", decodeBoatIntent(intent));
if (isRetreatLikeIntent(intent)) {
roundLogger.record("retreat_intent_observed", { type: intent.type, keys: Object.keys(intent), raw: intent });
}
if (intent.type === "build_unit" && isWeaponUnit(intent.unit)) {
roundLogger.record("weapon_intent_observed", decodeWeaponIntent(intent));
}
}
}
function isWeaponUnit(unit) {
return unit === UNIT.ATOM_BOMB || unit === UNIT.HYDROGEN_BOMB || unit === UNIT.MIRV || unit === UNIT.WARSHIP;
}
function decodeWeaponIntent(intent) {
const mapData = mapDataRef.current;
const tile = Number(intent.tile);
const out = { unit: intent.unit, tile: intent.tile != null ? intent.tile : null, keys: Object.keys(intent), raw: intent };
if (mapData && mapData.width && Number.isFinite(tile)) {
out.tileX = tile % mapData.width;
out.tileY = Math.floor(tile / mapData.width);
const byte = mapData.terrain ? mapData.terrain[tile] : void 0;
out.tileIsLand = byte === void 0 ? null : isLandByte(byte);
}
return out;
}
function isRetreatLikeIntent(intent) {
if (!intent) return false;
if (typeof intent.type === "string" && /cancel|retreat|abort/i.test(intent.type)) return true;
if ("retreating" in intent && intent.retreating) return true;
if (intent.attackID != null && intent.type !== "attack") return true;
return false;
}
function decodeBoatIntent(intent) {
const mapData = mapDataRef.current;
const dst = intent.dst;
const out = { dst: dst != null ? dst : null, troops: intent.troops != null ? intent.troops : null, keys: Object.keys(intent), raw: intent };
if (mapData && mapData.width && Number.isFinite(Number(dst))) {
const tile = Number(dst);
out.dstX = tile % mapData.width;
out.dstY = Math.floor(tile / mapData.width);
const byte = mapData.terrain ? mapData.terrain[tile] : void 0;
out.dstIsLand = byte === void 0 ? null : isLandByte(byte);
}
return out;
}
function summarizeIntent(intent) {
const targetID = intent.targetID;
let resolved = "none";
if (targetID != null) {
let match = gameState.state.playerStates.get(targetID) || null;
if (!match) {
gameState.state.playerStates.forEach((player) => {
if (!match && player && player.smallID === targetID) match = player;
});
}
resolved = match ? \`\${match.playerType || "?"}:\${match.name || match.id}\` : "unresolved";
}
return {
type: intent.type,
targetID: targetID === void 0 ? "__undefined__" : targetID,
targetIDType: targetID === null ? "null" : typeof targetID,
troops: intent.troops != null ? intent.troops : null,
resolved,
keys: Object.keys(intent),
raw: intent
};
}
function publishFarmRecommendations(recommendations) {
window.postMessage(
{
source: app.messageSource,
type: "farm-recommendations",
recommendations: recommendations.map((rec) => ({
id: rec.id,
name: rec.name,
status: rec.status,
label: rec.label,
suggestedPercent: rec.suggestedPercent,
reason: rec.reason,
estimatedCaptureTurns: rec.estimatedCaptureTurns
}))
},
"*"
);
}
function recordFarmRecommendations(recommendations) {
const now = performance.now();
if (now - lastFarmRecommendationLoggedAt < 5e3) return;
const actionable = recommendations.filter((rec) => rec.status === "farm" || rec.status === "mark").slice(0, 5);
if (!actionable.length) return;
lastFarmRecommendationLoggedAt = now;
roundLogger.record(
"farm_recommendation",
actionable.map((rec) => ({
id: rec.id,
name: rec.name,
status: rec.status,
suggestedPercent: rec.suggestedPercent,
reason: rec.reason,
score: rec.score,
targetTroops: rec.targetTroops,
effectiveTargetTroops: rec.effectiveTargetTroops,
targetTiles: rec.targetTiles,
reserveMaxSend: rec.reserveMaxSend,
captureMaxSend: rec.captureMaxSend,
usedCaptureOverdraft: !!rec.usedCaptureOverdraft,
captureCostEstimate: rec.captureCostEstimate,
officialCaptureCostEstimate: rec.officialCaptureCostEstimate,
officialCaptureTileCost: rec.officialCaptureTileCost,
officialCaptureTurns: rec.officialCaptureTurns,
officialCaptureSource: rec.officialCaptureSource,
estimatedCaptureTurns: rec.estimatedCaptureTurns,
sizingMode: rec.sizingMode
}))
);
roundLogger.record(
"combat_model_estimate",
actionable.slice(0, 5).map((rec) => ({
id: rec.id,
name: rec.name,
status: rec.status,
oldCaptureCost: rec.captureCostEstimate,
newCaptureCost: rec.officialCaptureCostEstimate,
oldTurns: rec.estimatedCaptureTurns,
newTurns: rec.officialCaptureTurns,
source: rec.officialCaptureSource,
terrain: rec.officialTerrainClass
}))
);
}
function recordBuyRecommendations(recommendations) {
const now = performance.now();
if (!recommendations.length || now - lastBuyRecommendationLoggedAt < 1e4) return;
lastBuyRecommendationLoggedAt = now;
roundLogger.record(
"buy_recommendation",
recommendations.map((rec) => ({
label: rec.label,
reason: rec.reason,
actionKey: rec.actionKey,
actionAvailable: !!rec.actionAvailable,
estimatedCost: rec.estimatedCost || null,
costSource: rec.costSource || null
}))
);
}
}
// src/page-entry.js
var payloadKey = window.__OFAT_PAGE_PAYLOAD__?.payloadKey || "__OFAT_PAGE_PAYLOAD__";
startPage(window[payloadKey]);
})();
`;
// src/meta.js
var APP = Object.freeze({
name: "OpenFront Tactical Assistant",
shortName: "OF Tactical",
version: "0.8.11",
modified: "2026-06-08",
storageKey: "ofat.settings.v1",
pagePayloadKey: "__OFAT_PAGE_PAYLOAD__",
messageSource: "openfront-tactical-assistant"
});
var USERSCRIPT_META = Object.freeze({
name: [APP.name],
namespace: ["https://github.com/local/openfront-script"],
version: [APP.version],
description: ["Modular OpenFront advisor toolkit with spawn, threat, expansion, alliance, and optional assist features."],
license: ["MIT"],
match: ["https://openfront.io/*", "https://beta.openfront.io/*"],
grant: ["GM_setValue", "GM_getValue", "GM_registerMenuCommand", "unsafeWindow"],
"run-at": ["document-start"],
"inject-into": ["auto"]
});
var DEFAULT_SETTINGS = Object.freeze({
settingsSchemaVersion: 13,
showAdvisorPanel: true,
showHeatmap: true,
hideAds: true,
autopilot: false,
autoSpawn: false,
autoAlliance: false,
autoFarm: false,
autoFarmHumanTargets: true,
autoEco: false,
autoDefense: false,
autoDefenseCounterAttack: true,
autoDefenseBuildPosts: true,
autoDefenseBuildSam: true,
autoDefenseIncomingRatio: 0.35,
autoBoat: false,
autoWeapons: false,
autoWeaponsEarlyNuke: false,
autoTeamSupport: true,
autoTeamSupportGold: false,
autoSosQuickChat: true,
smartAttack: false,
showAttackBadges: true,
showRatioBar: true,
showTroopRatios: true,
showWeaknessIndicator: true,
showDangerIndicator: true,
showThreatIcons: true,
showTroopEconomy: true,
roundLogging: true,
roundLogAutoDownload: false,
roundLogSnapshotIntervalMs: 5e3,
roundLogKeepPlayerNames: true,
networkLogging: true,
autoSpawnDelayMs: 2e3,
autoFarmWindowMs: 24e4,
autoFarmReserveRatio: 0.55,
autoFarmCooldownMs: 2500,
autoFarmDynamicReserve: true,
autoFarmBaseReserveRatio: 0.3,
autoFarmMinReserveRatio: 0.2,
autoFarmMaxReserveRatio: 0.65,
autoExpand: true
});
// src/settings/settings-store.js
function createSettingsStore(defaults, initialValues = {}) {
const values = Object.assign({}, defaults, initialValues);
const listeners = /* @__PURE__ */ new Set();
function has(key) {
return Object.prototype.hasOwnProperty.call(defaults, key);
}
return {
values,
has,
get(key) {
return has(key) ? values[key] : void 0;
},
set(key, value, meta = {}) {
if (!has(key)) return false;
if (values[key] === value) return true;
values[key] = value;
listeners.forEach((listener) => listener({ key, value, meta }));
return true;
},
update(nextValues, meta = {}) {
Object.keys(nextValues || {}).forEach((key) => this.set(key, nextValues[key], meta));
},
onChange(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
snapshot() {
return Object.assign({}, values);
}
};
}
// src/bootstrap/gm-settings.js
function createGmSettingsStore(defaults) {
const initial = {};
Object.keys(defaults).forEach((key) => {
initial[key] = gmGet(key, defaults[key]);
});
applySettingsMigrations(initial, defaults);
const store = createSettingsStore(defaults, initial);
store.onChange(({ key, value, meta }) => {
gmSet(key, value);
if (!meta.skipPageSync && meta.pageWindow && meta.app) {
meta.pageWindow.postMessage(
{
source: meta.app.messageSource,
type: "set-setting",
key,
value
},
"*"
);
}
});
return store;
}
function applySettingsMigrations(initial, defaults) {
const targetSchema = Number(defaults.settingsSchemaVersion) || 0;
const currentSchema = Number(gmGet("settingsSchemaVersion", 0)) || 0;
if (!targetSchema || currentSchema >= targetSchema) return;
initial.roundLogAutoDownload = false;
initial.settingsSchemaVersion = targetSchema;
gmSet("roundLogAutoDownload", false);
gmSet("settingsSchemaVersion", targetSchema);
}
function gmGet(key, fallback) {
try {
if (typeof GM_getValue === "function") return GM_getValue(key, fallback);
} catch (_) {
}
return fallback;
}
function gmSet(key, value) {
try {
if (typeof GM_setValue === "function") GM_setValue(key, value);
} catch (_) {
}
}
function registerToggleMenu(label, key, store, context) {
if (typeof GM_registerMenuCommand !== "function") return;
GM_registerMenuCommand(label, () => {
store.set(key, !store.get(key), context);
console.log(`[${context.app.shortName}] ${key}: ${store.get(key) ? "ON" : "OFF"}`);
});
}
// src/bootstrap/page-injection.js
function injectPageBundle(pageWindow, app, initialSettings, pageBundleSource) {
const payloadKey = app.pagePayloadKey;
pageWindow[payloadKey] = {
appInfo: app,
initialSettings,
payloadKey
};
try {
pageWindow.eval(pageBundleSource);
} finally {
try {
delete pageWindow[payloadKey];
} catch (_) {
pageWindow[payloadKey] = void 0;
}
}
}
// src/shared/dom.js
function appendWhenReady(node, parentSelector = "head") {
const target = parentSelector === "body" ? document.body : document.head || document.documentElement;
if (target) {
target.appendChild(node);
return;
}
document.addEventListener(
"DOMContentLoaded",
() => {
const parent = parentSelector === "body" ? document.body : document.head;
parent.appendChild(node);
},
{ once: true }
);
}
function createStyle(id, css) {
const existing = document.getElementById(id);
if (existing) return existing;
const style = document.createElement("style");
style.id = id;
style.textContent = css;
appendWhenReady(style);
return style;
}
// src/shared/troops.js
function formatTroopCount(troops) {
const num = Number(troops) / 10;
if (!Number.isFinite(num)) return "0";
if (num >= 1e7) return `${(Math.floor(num / 1e5) / 10).toFixed(1)}M`;
if (num >= 1e6) return `${(Math.floor(num / 1e4) / 100).toFixed(2)}M`;
if (num >= 1e5) return `${Math.floor(num / 1e3)}K`;
if (num >= 1e4) return `${(Math.floor(num / 100) / 10).toFixed(1)}K`;
if (num >= 1e3) return `${(Math.floor(num / 10) / 100).toFixed(2)}K`;
return Math.floor(num).toString();
}
// src/userscript/name-layer-enhancer.js
var CURRENT_WEAKNESS_THRESHOLD = 0.1;
var TOTAL_POTENTIAL_THRESHOLD = 0.3;
var DANGER_THRESHOLD = 1.35;
var ATOM_COST = 75e4;
var HYDROGEN_COST = 5e6;
var ATOM_ICON = "[A]";
var HYDROGEN_ICON = "[H]";
function installNameLayerEnhancer({ app, settings, syncContext }) {
let game = null;
let nameLayerContainer = null;
let farmRecommendations = /* @__PURE__ */ new Map();
installEnhancerStyles();
if (typeof GM_registerMenuCommand === "function") {
GM_registerMenuCommand("Refresh OpenFront UI overlays", refreshAllTroopDisplays);
GM_registerMenuCommand("Disable all automation", () => {
settings.set("autopilot", false, syncContext);
settings.set("autoSpawn", false, syncContext);
settings.set("autoFarm", false, syncContext);
settings.set("autoAlliance", false, syncContext);
settings.set("smartAttack", false, syncContext);
});
}
settings.onChange(refreshAllTroopDisplays);
window.addEventListener("message", (event) => {
const data = event && event.data;
if (!data || data.source !== app.messageSource || data.type !== "farm-recommendations") return;
farmRecommendations = new Map((data.recommendations || []).map((rec) => [rec.id, rec]));
refreshAllTroopDisplays();
});
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", waitForGame, { once: true });
} else {
waitForGame();
}
console.log(`%c[${app.shortName}] UI enhancer loaded`, "color:#4fc3f7;font-weight:bold");
function waitForGame() {
try {
const pageWin = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
const leaderboard = pageWin.document.querySelector("leader-board");
if (leaderboard && leaderboard.game) {
game = leaderboard.game;
findNameLayerContainer();
} else {
setTimeout(waitForGame, 1e3);
}
} catch (error) {
console.error(`[${app.shortName}] waitForGame failed`, error);
}
}
function findNameLayerContainer() {
try {
const selector = 'div[style*="position: fixed"][style*="left: 50%"][style*="top: 50%"][style*="pointer-events: none"][style*="z-index: 2"]';
const containers = document.querySelectorAll(selector);
if (containers.length > 0) {
nameLayerContainer = containers[0];
setupObservers();
} else {
setTimeout(findNameLayerContainer, 1e3);
}
} catch (error) {
console.error(`[${app.shortName}] findNameLayerContainer failed`, error);
}
}
function setupObservers() {
const containerObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
const troopsDiv = node.querySelector(".player-troops");
if (troopsDiv) setupTroopObserver(troopsDiv);
});
});
});
containerObserver.observe(nameLayerContainer, { childList: true, subtree: true });
nameLayerContainer.querySelectorAll(".player-troops").forEach(setupTroopObserver);
}
function setupTroopObserver(troopsDiv) {
if (troopsDiv._ofatObserver) return;
const observer = new MutationObserver(() => {
if (troopsDiv.textContent && troopsDiv.textContent.indexOf("/") === -1) updateTroopDisplay(troopsDiv);
});
observer.observe(troopsDiv, { childList: true, characterData: true, subtree: true });
troopsDiv._ofatObserver = observer;
updateTroopDisplay(troopsDiv);
}
function refreshAllTroopDisplays() {
if (!nameLayerContainer) return;
nameLayerContainer.querySelectorAll(".player-troops").forEach(updateTroopDisplay);
}
function updateTroopDisplay(troopsDiv) {
if (!game || !troopsDiv) return;
const element = troopsDiv.parentElement;
if (!element) return;
const nameSpan = element.querySelector(".player-name-span");
if (!nameSpan) return;
const playerName = getBasePlayerName(nameSpan);
const player = findPlayerByName(playerName);
if (!player) return;
const observer = troopsDiv._ofatObserver;
if (observer) observer.disconnect();
try {
const currentTroops = Number(player.troops());
const maxTroops = Number(game.config().maxTroops(player));
const attackingTroops = getOutgoingAttackTroops(player);
if (!Number.isFinite(currentTroops) || !Number.isFinite(maxTroops) || !Number.isFinite(attackingTroops)) return;
renderTroopText(troopsDiv, currentTroops, maxTroops);
renderRatioBar(element, player, currentTroops, maxTroops, attackingTroops);
renderRiskState(troopsDiv, player, currentTroops, maxTroops, attackingTroops);
renderThreatIcon(nameSpan, playerName, player);
renderFarmBadge(nameSpan, player);
} catch (error) {
console.error(`[${app.shortName}] updateTroopDisplay failed`, error);
} finally {
if (observer) observer.observe(troopsDiv, { childList: true, characterData: true, subtree: true });
}
}
function cleanPlayerName(text) {
return String(text || "").replace(ATOM_ICON, "").replace(HYDROGEN_ICON, "").replace(/\b(?:FARM\s+\d+%|TARGET\s+\d+%|MARK|WAIT|DANGER|HOLD)\b/g, "").trim();
}
function getBasePlayerName(nameSpan) {
const textNode = Array.from(nameSpan.childNodes).find((node) => node.nodeType === Node.TEXT_NODE);
return cleanPlayerName(textNode ? textNode.textContent : nameSpan.textContent);
}
function findPlayerByName(playerName) {
const players = typeof game.playerViews === "function" ? game.playerViews() : [];
return players.find((player) => {
if (!player || typeof player.isAlive === "function" && !player.isAlive()) return false;
const name = typeof player.name === "function" ? player.name() : "";
const displayName = typeof player.displayName === "function" ? player.displayName() : "";
return name === playerName || displayName === playerName;
});
}
function getOutgoingAttackTroops(player) {
if (typeof player.outgoingAttacks !== "function") return 0;
return player.outgoingAttacks().reduce((sum, attack) => sum + (attack.retreating ? 0 : Number(attack.troops || 0)), 0);
}
function renderTroopText(troopsDiv, currentTroops, maxTroops) {
troopsDiv.textContent = "";
troopsDiv.appendChild(document.createTextNode(formatTroopCount(currentTroops)));
if (settings.get("showTroopRatios")) {
const maxSpan = document.createElement("span");
maxSpan.className = "ofat-max-troops";
maxSpan.textContent = `/${formatTroopCount(maxTroops)}`;
troopsDiv.appendChild(maxSpan);
}
}
function renderRatioBar(element, player, currentTroops, maxTroops, attackingTroops) {
let ratioBar = element.querySelector(".ofat-troop-ratio-bar");
if (!ratioBar) {
ratioBar = document.createElement("div");
ratioBar.className = "ofat-troop-ratio-bar";
ratioBar.appendChild(Object.assign(document.createElement("div"), { className: "ofat-ratio-fill" }));
ratioBar.appendChild(Object.assign(document.createElement("div"), { className: "ofat-ratio-buffer" }));
element.appendChild(ratioBar);
}
const fill = ratioBar.querySelector(".ofat-ratio-fill");
const buffer = ratioBar.querySelector(".ofat-ratio-buffer");
const isBot = getPlayerType(player) === "BOT";
if (!settings.get("showRatioBar") || isBot) {
ratioBar.style.display = "none";
return;
}
ratioBar.style.display = "";
const totalPotential = currentTroops + attackingTroops;
const totalRatio = maxTroops > 0 ? Math.min(totalPotential / maxTroops, 1) : 0;
const mainRatio = attackingTroops === 0 ? 1 : totalPotential > 0 ? currentTroops / totalPotential : 0;
const bufferRatio = attackingTroops === 0 ? 0 : totalPotential > 0 ? attackingTroops / totalPotential : 0;
const mainWidth = mainRatio * totalRatio * 100;
const bufferWidth = bufferRatio * totalRatio * 100;
fill.style.width = `${mainWidth}%`;
buffer.style.width = `${bufferWidth}%`;
buffer.style.left = `${mainWidth}%`;
}
function renderRiskState(troopsDiv, player, currentTroops, maxTroops, attackingTroops) {
const myPlayer = typeof game.myPlayer === "function" ? game.myPlayer() : null;
troopsDiv.classList.remove("ofat-flashing-orange");
troopsDiv.style.color = "";
if (!myPlayer || player.id() === myPlayer.id()) return;
const myTroops = Number(myPlayer.troops());
if (!Number.isFinite(myTroops) || myTroops <= 0) return;
const dangerRatio = currentTroops / myTroops;
const sameTeam = isSameTeamMode() && typeof player.isOnSameTeam === "function" && player.isOnSameTeam(myPlayer);
if (settings.get("showWeaknessIndicator") && currentTroops <= CURRENT_WEAKNESS_THRESHOLD * maxTroops && currentTroops + attackingTroops < TOTAL_POTENTIAL_THRESHOLD * maxTroops) {
troopsDiv.classList.add("ofat-flashing-orange");
} else if (settings.get("showDangerIndicator") && dangerRatio >= DANGER_THRESHOLD && !sameTeam) {
troopsDiv.style.color = "red";
}
}
function renderThreatIcon(nameSpan, playerName, player) {
nameSpan.textContent = playerName;
if (!settings.get("showThreatIcons")) return;
const myPlayer = typeof game.myPlayer === "function" ? game.myPlayer() : null;
if (myPlayer && player.id() === myPlayer.id()) return;
const icon = getThreatIcon(player);
if (!icon) return;
const span = document.createElement("span");
span.className = "ofat-threat-icon";
span.textContent = icon;
nameSpan.appendChild(span);
}
function renderFarmBadge(nameSpan, player) {
if (!settings.get("showAttackBadges")) return;
const playerID = typeof player.id === "function" ? player.id() : null;
const rec = playerID ? farmRecommendations.get(playerID) : null;
if (!rec || !rec.label) return;
const span = document.createElement("span");
span.className = `ofat-farm-badge ofat-farm-badge-${rec.status || "hold"}`;
span.textContent = rec.label;
span.title = rec.reason || "";
nameSpan.appendChild(span);
}
function getThreatIcon(player) {
const hasSilo = typeof player.units === "function" && Array.isArray(player.units("Missile Silo")) && player.units("Missile Silo").length > 0;
if (!hasSilo) return "";
const gold = Number(typeof player.gold === "function" ? player.gold() : 0);
if (gold >= HYDROGEN_COST) return HYDROGEN_ICON;
if (gold >= ATOM_COST) return ATOM_ICON;
return "";
}
function getPlayerType(player) {
try {
return String(typeof player.type === "function" ? player.type() : "").toUpperCase();
} catch (_) {
return "";
}
}
function isSameTeamMode() {
try {
const config = game.config().gameConfig();
return config && config.gameMode === "Team";
} catch (_) {
return false;
}
}
}
function installEnhancerStyles() {
createStyle(
"ofat-name-layer-style",
`
.ofat-flashing-orange { color: #fff !important; animation: ofat-flash 0.2s infinite; }
@keyframes ofat-flash { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0.68; } }
.player-name-span, .player-troops {
color: #000;
text-shadow: 0 0 0.1em #fff;
font-weight: 600;
}
.ofat-max-troops {
font-size: 0.68em;
color: rgba(0,0,0,0.82);
text-shadow: 0 0 0.1em #fff;
}
.ofat-troop-ratio-bar {
width: 32px;
height: 4px;
background: rgba(34,34,34,0.5);
position: relative;
border: 1px solid rgba(68,68,68,0.6);
overflow: hidden;
}
.ofat-ratio-fill, .ofat-ratio-buffer { height: 100%; position: absolute; top: 0; }
.ofat-ratio-fill { background: rgba(0, 210, 82, 0.74); }
.ofat-ratio-buffer { background: rgba(255, 166, 0, 0.52); }
.ofat-threat-icon { font-size: 0.72em; opacity: 0.72; margin-left: 2px; }
.ofat-farm-badge {
display: inline-block;
margin-left: 3px;
padding: 1px 3px;
color: #05070a;
border-radius: 3px;
font-size: 0.62em;
font-weight: 800;
text-shadow: none;
vertical-align: middle;
}
.ofat-farm-badge-farm { background: rgba(77, 224, 117, 0.9); }
.ofat-farm-badge-target { background: rgba(83, 181, 255, 0.92); }
.ofat-farm-badge-mark { background: rgba(255, 190, 72, 0.92); }
.ofat-farm-badge-wait { background: rgba(190, 198, 210, 0.86); }
.ofat-farm-badge-danger { background: rgba(255, 80, 80, 0.92); color: #fff; }
.ofat-farm-badge-hold { background: rgba(190, 198, 210, 0.86); }
`
);
}
// src/main.js
(function bootstrapUserscriptContext() {
"use strict";
const pageWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
const settings = createGmSettingsStore(DEFAULT_SETTINGS);
const syncContext = { app: APP, pageWindow };
registerToggleMenu("Toggle hide ads", "hideAds", settings, syncContext);
registerToggleMenu("Toggle advisor panel", "showAdvisorPanel", settings, syncContext);
registerToggleMenu("Toggle spawn heatmap", "showHeatmap", settings, syncContext);
registerToggleMenu("Toggle autopilot", "autopilot", settings, syncContext);
registerToggleMenu("Toggle auto spawn", "autoSpawn", settings, syncContext);
registerToggleMenu("Toggle auto farm", "autoFarm", settings, syncContext);
registerToggleMenu("Toggle human farm", "autoFarmHumanTargets", settings, syncContext);
registerToggleMenu("Toggle auto eco", "autoEco", settings, syncContext);
registerToggleMenu("Toggle auto defense", "autoDefense", settings, syncContext);
registerToggleMenu("Toggle auto boat", "autoBoat", settings, syncContext);
registerToggleMenu("Toggle auto weapons", "autoWeapons", settings, syncContext);
registerToggleMenu("Toggle early-nuke (vs SAM build)", "autoWeaponsEarlyNuke", settings, syncContext);
registerToggleMenu("Toggle auto alliance", "autoAlliance", settings, syncContext);
registerToggleMenu("Toggle smart attack ratio", "smartAttack", settings, syncContext);
registerToggleMenu("Toggle attack badges", "showAttackBadges", settings, syncContext);
registerToggleMenu("Toggle ratio/health bar", "showRatioBar", settings, syncContext);
registerToggleMenu("Toggle max troops text", "showTroopRatios", settings, syncContext);
registerToggleMenu("Toggle weakness indicator", "showWeaknessIndicator", settings, syncContext);
registerToggleMenu("Toggle danger indicator", "showDangerIndicator", settings, syncContext);
registerToggleMenu("Toggle threat icons", "showThreatIcons", settings, syncContext);
registerToggleMenu("Toggle troop economy panel", "showTroopEconomy", settings, syncContext);
registerToggleMenu("Toggle round logging", "roundLogging", settings, syncContext);
registerToggleMenu("Toggle auto log download", "roundLogAutoDownload", settings, syncContext);
registerToggleMenu("Toggle network logging", "networkLogging", settings, syncContext);
if (typeof GM_registerMenuCommand === "function") {
GM_registerMenuCommand("Export current round log", () => {
pageWindow.postMessage({ source: APP.messageSource, type: "export-round-log" }, "*");
});
GM_registerMenuCommand("Clear current round log", () => {
pageWindow.postMessage({ source: APP.messageSource, type: "clear-round-log" }, "*");
});
}
window.addEventListener("message", (event) => {
const data = event && event.data;
if (!data || data.source !== APP.messageSource || data.type !== "setting-changed") return;
settings.set(data.key, data.value, { skipPageSync: true });
});
injectPageBundle(pageWindow, APP, settings.snapshot(), PAGE_BUNDLE_SOURCE);
installNameLayerEnhancer({ app: APP, settings, syncContext });
})();
})();