Greasy Fork is available in English.
CHEAT GUI! Macros + Lobby + Adblock + Embargo + Combat + Diplomacy control panel for openfront.io
// ==UserScript==
// @name Project Blon Openfront Cheats
// @namespace http://tampermonkey.net/
// @version 22.8
// @description CHEAT GUI! Macros + Lobby + Adblock + Embargo + Combat + Diplomacy control panel for openfront.io
// @author blon
// @match *://openfront.io/*
// @match *://*.openfront.io/*
// @grant none
// @run-at document-start
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const BLON_GAME_CACHE_KEY = '__blonGameView';
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 rememberGameView(value) {
if (!looksLikeGameView(value)) return false;
try {
window[BLON_GAME_CACHE_KEY] = value;
} catch (e) {}
return true;
}
function readCachedGameView() {
try {
const game = window[BLON_GAME_CACHE_KEY];
if (looksLikeGameView(game)) return game;
for (const frame of document.querySelectorAll('iframe')) {
try {
const framedGame = frame.contentWindow && frame.contentWindow[BLON_GAME_CACHE_KEY];
if (looksLikeGameView(framedGame)) return framedGame;
} catch (e) {}
}
return null;
} catch (e) {
return null;
}
}
function getAccessibleDocuments() {
const docs = [document];
try {
for (const frame of document.querySelectorAll('iframe')) {
try {
if (frame.contentDocument) docs.push(frame.contentDocument);
} catch (e) {}
}
} catch (e) {}
return docs;
}
function patchGameProperty(proto, prop) {
const patchedKey = `__blon_${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(`blon_${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) {
rememberGameView(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 (e) {}
}
// this took longer than im proud of also they store the variable as 10x its normal value and that fooled me lmao
function patchLeaderboardCtor(ctor) {
if (!ctor || !ctor.prototype || ctor.prototype.__blonLeaderboardPatched) return;
ctor.prototype.__blonLeaderboardPatched = true;
const origUpdated = ctor.prototype.updated;
ctor.prototype.updated = function(changedProperties) {
if (origUpdated) {
try {
origUpdated.call(this, changedProperties);
} catch(e) {
console.error('[Blon] Error in original updated:', e);
}
}
try {
patchLeaderboardDOM(this);
} catch(e) {
console.error('[Blon] Error patching leaderboard in updated:', e);
}
};
}
let leaderboardIntervalId = null;
function startLeaderboardLoop() {
if (leaderboardIntervalId) {
clearInterval(leaderboardIntervalId);
}
leaderboardIntervalId = setInterval(() => {
try {
patchLeaderboardDOM(document.querySelector('leader-board'));
} catch(e) {}
}, cfg.troopsCheckIntervalMs || 500);
}
function patchLeaderboardDOM(lb) {
if (!lb) return;
const grid = lb.querySelector('.grid');
if (!grid) return;
if (!cfg.showTroopsOverlay) {
removeLeaderboardTroops(grid);
return;
}
const targetStyle = "minmax(24px, 30px) minmax(60px, 100px) minmax(45px, 70px) minmax(40px, 55px) minmax(55px, 105px) minmax(55px, 105px)";
if (grid.style.gridTemplateColumns !== targetStyle) {
grid.style.gridTemplateColumns = targetStyle;
}
const headerRow = grid.querySelector('.contents.font-bold');
if (headerRow) {
let troopsHeader = headerRow.querySelector('.blon-troops-header');
if (!troopsHeader) {
troopsHeader = document.createElement('div');
troopsHeader.className = 'py-1 md:py-2 text-center border-b border-slate-500 truncate blon-troops-header';
troopsHeader.textContent = 'Troops';
headerRow.appendChild(troopsHeader);
}
}
const rows = Array.from(grid.children).slice(1);
const players = lb.players || [];
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const playerEntry = players[i];
if (!playerEntry) continue;
if (!row.classList.contains('contents')) continue;
let troopsCell = row.querySelector('.blon-troops-cell');
if (!troopsCell) {
troopsCell = document.createElement('div');
troopsCell.className = 'py-1 md:py-2 text-center blon-troops-cell';
row.appendChild(troopsCell);
}
const siblingCell = row.firstElementChild;
if (siblingCell) {
if (siblingCell.classList.contains('border-b')) {
troopsCell.classList.add('border-b', 'border-slate-500');
} else {
troopsCell.classList.remove('border-b', 'border-slate-500');
}
}
const player = playerEntry.player;
let val = '0';
if (player && typeof player.troops === 'function') {
const tr = player.troops();
const numericTroops = typeof tr === 'bigint' ? Number(tr) : tr;
val = fmtNum(numericTroops / 10);
}
if (troopsCell.textContent !== val) {
troopsCell.textContent = val;
}
}
}
function removeLeaderboardTroops(grid) {
if (!grid) return;
const origStyle = "minmax(24px, 30px) minmax(60px, 100px) minmax(45px, 70px) minmax(40px, 55px) minmax(55px, 105px)";
if (grid.style.gridTemplateColumns === "minmax(24px, 30px) minmax(60px, 100px) minmax(45px, 70px) minmax(40px, 55px) minmax(55px, 105px) minmax(55px, 105px)") {
grid.style.gridTemplateColumns = origStyle;
}
const header = grid.querySelector('.blon-troops-header');
if (header) header.remove();
const cells = grid.querySelectorAll('.blon-troops-cell');
cells.forEach(c => c.remove());
}
function installGameViewCapture() {
try {
patchGameProperty(window.HTMLElement && window.HTMLElement.prototype, 'game');
patchGameProperty(window.HTMLElement && window.HTMLElement.prototype, 'g');
if (window.customElements) {
const lbCtor = window.customElements.get('leader-board');
if (lbCtor && lbCtor.prototype && !lbCtor.prototype.__blonLeaderboardPatched) {
patchLeaderboardCtor(lbCtor);
}
}
if (!window.customElements || window.customElements.__blonGameCapturePatched) return;
const nativeDefine = window.customElements.define.bind(window.customElements);
window.customElements.define = function(name, ctor, options) {
try {
if (ctor && ctor.prototype) {
patchGameProperty(ctor.prototype, 'game');
patchGameProperty(ctor.prototype, 'g');
if (name === 'leader-board') {
patchLeaderboardCtor(ctor);
}
}
} catch (e) {}
return nativeDefine(name, ctor, options);
};
Object.defineProperty(window.customElements, '__blonGameCapturePatched', {
configurable: true,
value: true,
});
} catch (e) {}
}
installGameViewCapture();
// ill probably move this method too
// the bottom later its just i recently worked on it so i kept it at the top
(function blonAdBlock() {
const AD_DOMAINS = [
'googlesyndication.com',
'doubleclick.net',
'doubleverify.com',
'googleadservices.com',
'googletag',
'adservice.google',
'pagead2.googlesyndication',
'tpc.googlesyndication',
'vpaid.doubleverify',
'vtrk.dv.tech',
'innovid.com',
'ads.pubmatic.com',
'secure.adnxs.com',
'ib.adnxs.com',
'rubiconproject.com',
'openx.net',
'advertising.com',
'ad.doubleclick',
'amazon-adsystem.com',
'adsafeprotected.com',
'moatads.com',
'chartboost.com',
'criteo.com',
'bidswitch.net',
'pubads.g.doubleclick',
'securepubads.g.doubleclick',
];
function isAdUrl(url) {
if (!url) return false;
try {
const str = String(url).toLowerCase();
return AD_DOMAINS.some(d => str.includes(d));
} catch(e) { return false; }
}
const _fetch = window.fetch;
window.fetch = function(input, init) {
const url = (input && typeof input === 'object') ? input.url : input;
if (isAdUrl(url)) {
console.log('[Blon] blocked fetch:', url);
return Promise.reject(new TypeError('hah'));
}
return _fetch.apply(this, arguments);
};
const _XHROpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
if (isAdUrl(url)) {
console.log('[Blon] Blocked XHR:', url);
this._blonBlocked = true;
return;
}
return _XHROpen.apply(this, arguments);
};
const _XHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function() {
if (this._blonBlocked) return;
return _XHRSend.apply(this, arguments);
};
const AD_SELECTORS = [
'iframe[src*="googlesyndication"]',
'iframe[src*="doubleclick"]',
'iframe[src*="doubleverify"]',
'iframe[src*="googleadservices"]',
'iframe[src*="innovid"]',
'iframe[src*="adnxs"]',
'iframe[src*="rubiconproject"]',
'iframe[src*="openx"]',
'iframe[src*="amazon-adsystem"]',
'iframe[src*="criteo"]',
'iframe[src*="bidswitch"]',
'iframe[src*="moatads"]',
'iframe[src*="adsafeprotected"]',
'iframe[src*="pubads"]',
'iframe[src*="vtrk.dv"]',
'iframe[title="Advertisement"]',
'iframe[title="advertisement"]',
'script[src*="googlesyndication"]',
'script[src*="doubleclick"]',
'script[src*="doubleverify"]',
'script[src*="googleadservices"]',
'script[src*="adservice.google"]',
'ins.adsbygoogle',
'div[id*="google_ads"]',
'div[class*="google_ads"]',
'div[id*="ad-container"]',
'div[data-ad]',
];
function nukeAdElements(root) {
AD_SELECTORS.forEach(sel => {
try {
root.querySelectorAll(sel).forEach(el => {
console.log('[Blon] Removed element:', el.tagName, el.src || el.id || '');
el.remove();
});
} catch(e) {}
});
}
if (document.body) nukeAdElements(document);
const adObserver = new MutationObserver(mutations => {
for (const m of mutations) {
for (const node of m.addedNodes) {
if (node.nodeType !== 1) continue; // elements only
const src = (node.src || node.href || '').toLowerCase();
if (isAdUrl(src)) {
console.log('[Blon] Removed injected node:', node.tagName, src);
node.remove();
continue;
}
// title=Advertisement
if (node.tagName === 'IFRAME' && /advert/i.test(node.title || '')) {
console.log('[Blon] Removed ad iframe by title:', node.title);
node.remove();
continue;
}
if (node.querySelectorAll) nukeAdElements(node);
}
}
});
function startAdObserver() {
adObserver.observe(document.documentElement || document.body, {
childList: true,
subtree: true
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
nukeAdElements(document);
startAdObserver();
});
} else {
nukeAdElements(document);
startAdObserver();
}
})();
// USED JLL Beautifier THX
// btw i think i fixed header idk tho
//cfg
const STORAGE_KEY = 'blon_v10_cfg';
let cfg = {
useChargeTime: true,
blockPassThrough: true,
holdDelay: 3000,
spamInterval: 25,
selectedUnit: 'City',
toggleMode: false,
spamMethod: 'websocket',
hotkeyMap: {
'q': '1', 'w': '2', 'e': '3', 'r': '4', 't': '5',
'y': '6', 'u': '7', 'i': '8', 'o': '9', 'p': '0',
'z': 'atk_1', 'x': 'atk_2', 'c': 'atk_3', 'v': 'atk_4'
},
combatPercentages: {
'atk_1': 10,
'atk_2': 25,
'atk_3': 50,
'atk_4': 100
},
tradePercentages: {
'donate_troops_1': 10,
'donate_troops_2': 25,
'donate_troops_3': 50,
'donate_troops_4': 100,
'donate_gold_1': 10,
'donate_gold_2': 25,
'donate_gold_3': 50,
'donate_gold_4': 100
},
combatHotkeysPriority: false,
combatSiloIndicator: false,
combatSiloPanel: false,
combatSiloOnlyAllies: false,
combatSiloKeepAllPlaced: false,
showGoldOverlay: false,
showTroopsOverlay: false,
troopsCheckIntervalMs: 500,
goldRateWindowSeconds: 240,
goldTopCount: 10,
actionHotkeyMap: {
'embargo_fire': { mod: 'alt', key: 'x' },
'embargo_lift': { mod: 'alt', key: 'z' },
'donate_troops_1': { mod: 'alt', key: 't' },
'donate_troops_2': { mod: '', key: '' },
'donate_troops_3': { mod: '', key: '' },
'donate_troops_4': { mod: '', key: '' },
'donate_gold_1': { mod: 'alt', key: 'g' },
'donate_gold_2': { mod: '', key: '' },
'donate_gold_3': { mod: '', key: '' },
'donate_gold_4': { mod: '', key: '' }
},
guiOpacity: 1.0,
overlayOpacity: 1.0,
guiColor: '#00ff66',
guiColorHue: 150,
overlayColor: '#ffcc00',
// Feature toggles (do not delete any hotkey/config; just disable behavior)
features: {
spamHotkeys: true,
combatHotkeys: true,
actionHotkeys: true,
quickChat: true,
embargo: true,
overlays: true,
missilePredictor: true
}
};
function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); }
function hexToRgb(hex) {
const cleaned = String(hex || '').trim().replace(/^#/, '');
if (!/^[0-9a-f]{3,6}$/i.test(cleaned)) return null;
const normalized = cleaned.length === 3
? cleaned.split('').map(ch => ch + ch).join('')
: cleaned;
const int = parseInt(normalized, 16);
return {
r: (int >> 16) & 255,
g: (int >> 8) & 255,
b: int & 255
};
}
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h = 0, s = 0, l = (max + min) / 2;
if (max !== min) {
const diff = max - min;
s = l > 0.5 ? diff / (2 - max - min) : diff / (max + min);
switch (max) {
case r: h = ((g - b) / diff + (g < b ? 6 : 0)); break;
case g: h = ((b - r) / diff + 2); break;
case b: h = ((r - g) / diff + 4); break;
}
h *= 60;
}
return { h, s, l };
}
function hslToHex(h, s, l) {
h = ((h % 360) + 360) % 360;
s = clamp(s, 0, 1);
l = clamp(l, 0, 1);
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
const m = l - c / 2;
let r = 0, g = 0, b = 0;
if (h < 60) [r, g, b] = [c, x, 0];
else if (h < 120) [r, g, b] = [x, c, 0];
else if (h < 180) [r, g, b] = [0, c, x];
else if (h < 240) [r, g, b] = [0, x, c];
else if (h < 300) [r, g, b] = [x, 0, c];
else [r, g, b] = [c, 0, x];
const toHex = v => Math.round((v + m) * 255).toString(16).padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
function normalizeHexColor(value) {
const rgb = hexToRgb(value);
if (!rgb) return null;
return `#${((1 << 24) + (rgb.r << 16) + (rgb.g << 8) + rgb.b).toString(16).slice(1)}`;
}
function syncGuiColorFromHex(value) {
const normalized = normalizeHexColor(value);
if (!normalized) return null;
const rgb = hexToRgb(normalized);
if (!rgb) return null;
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
cfg.guiColor = normalized;
cfg.guiColorHue = Math.round(hsl.h);
return normalized;
}
function syncGuiColorFromHue(hue) {
cfg.guiColorHue = clamp(Math.round(hue), 0, 360);
cfg.guiColor = hslToHex(cfg.guiColorHue, 1, 0.55);
return cfg.guiColor;
}
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const p = JSON.parse(stored);
if (p && typeof p === 'object') {
if (typeof p.useChargeTime === 'boolean') cfg.useChargeTime = p.useChargeTime;
if (typeof p.blockPassThrough === 'boolean') cfg.blockPassThrough = p.blockPassThrough;
if (!isNaN(parseInt(p.holdDelay))) cfg.holdDelay = parseInt(p.holdDelay);
if (!isNaN(parseInt(p.spamInterval))) cfg.spamInterval = parseInt(p.spamInterval);
if (typeof p.selectedUnit === 'string') cfg.selectedUnit = p.selectedUnit;
if (typeof p.toggleMode === 'boolean') cfg.toggleMode = p.toggleMode;
if (typeof p.spamMethod === 'string' && ['websocket','click'].includes(p.spamMethod)) cfg.spamMethod = p.spamMethod;
if (p.hotkeyMap && typeof p.hotkeyMap === 'object') {
cfg.hotkeyMap = {};
for (const [k, v] of Object.entries(p.hotkeyMap)) {
if (typeof k === 'string' && typeof v === 'string') {
cfg.hotkeyMap[k.toLowerCase()] = v;
}
}
}
if (p.actionHotkeyMap && typeof p.actionHotkeyMap === 'object') {
Object.assign(cfg.actionHotkeyMap, p.actionHotkeyMap);
}
if (p.combatPercentages && typeof p.combatPercentages === 'object') {
Object.assign(cfg.combatPercentages, p.combatPercentages);
}
if (p.tradePercentages && typeof p.tradePercentages === 'object') {
Object.assign(cfg.tradePercentages, p.tradePercentages);
}
if (typeof p.combatHotkeysPriority === 'boolean') {
cfg.combatHotkeysPriority = p.combatHotkeysPriority;
}
if (typeof p.combatSiloIndicator === 'boolean') {
cfg.combatSiloIndicator = p.combatSiloIndicator;
}
if (typeof p.combatSiloPanel === 'boolean') {
cfg.combatSiloPanel = p.combatSiloPanel;
}
if (typeof p.combatSiloOnlyAllies === 'boolean') {
cfg.combatSiloOnlyAllies = p.combatSiloOnlyAllies;
}
if (typeof p.combatSiloKeepAllPlaced === 'boolean') {
cfg.combatSiloKeepAllPlaced = p.combatSiloKeepAllPlaced;
}
if (typeof p.showGoldOverlay === 'boolean') {
cfg.showGoldOverlay = p.showGoldOverlay;
}
if (typeof p.showTroopsOverlay === 'boolean') {
cfg.showTroopsOverlay = p.showTroopsOverlay;
}
if (!isNaN(parseInt(p.troopsCheckIntervalMs))) {
cfg.troopsCheckIntervalMs = Math.max(1, Math.min(1000, parseInt(p.troopsCheckIntervalMs)));
}
if (p.features && typeof p.features === 'object') {
const defaultFeatureState = {
spamHotkeys: true,
combatHotkeys: true,
actionHotkeys: true,
quickChat: true,
embargo: true,
overlays: true,
missilePredictor: true,
};
cfg.features = Object.assign({}, defaultFeatureState, p.features);
for (const [key, defaultValue] of Object.entries(defaultFeatureState)) {
if (typeof cfg.features[key] !== 'boolean') {
cfg.features[key] = defaultValue;
}
}
}
if (!isNaN(parseInt(p.goldRateWindowSeconds))) {
cfg.goldRateWindowSeconds = Math.max(5, Math.min(240, parseInt(p.goldRateWindowSeconds)));
}
if (!isNaN(parseInt(p.goldTopCount))) {
cfg.goldTopCount = Math.max(1, Math.min(20, parseInt(p.goldTopCount)));
}
if (!isNaN(parseFloat(p.guiOpacity))) {
cfg.guiOpacity = Math.max(0.1, Math.min(1, parseFloat(p.guiOpacity)));
}
if (!isNaN(parseFloat(p.overlayOpacity))) {
cfg.overlayOpacity = Math.max(0.1, Math.min(1, parseFloat(p.overlayOpacity)));
}
if (typeof p.guiColor === 'string' && p.guiColor.trim()) {
const normalized = normalizeHexColor(p.guiColor.trim());
if (normalized) cfg.guiColor = normalized;
}
if (!isNaN(parseInt(p.guiColorHue, 10))) {
cfg.guiColorHue = clamp(parseInt(p.guiColorHue, 10), 0, 360);
}
if (typeof p.overlayColor === 'string' && p.overlayColor.trim()) {
const normalized = normalizeHexColor(p.overlayColor.trim());
if (normalized) cfg.overlayColor = normalized;
}
}
}
} catch(e) {}
const initialGuiColorHsl = rgbToHsl(...Object.values(hexToRgb(cfg.guiColor) || { r: 0, g: 255, b: 102 }));
if (!Number.isFinite(cfg.guiColorHue) || cfg.guiColorHue < 0 || cfg.guiColorHue > 360) {
cfg.guiColorHue = Math.round(initialGuiColorHsl.h || 150);
}
for (const key in cfg.hotkeyMap) {
if (cfg.hotkeyMap[key] === 'atk_10') cfg.hotkeyMap[key] = 'atk_1';
else if (cfg.hotkeyMap[key] === 'atk_25') cfg.hotkeyMap[key] = 'atk_2';
else if (cfg.hotkeyMap[key] === 'atk_50') cfg.hotkeyMap[key] = 'atk_3';
else if (cfg.hotkeyMap[key] === 'atk_100') cfg.hotkeyMap[key] = 'atk_4';
}
// maybe add more hotkeys later if useful?
const defaultCombatKeys = { 'atk_1': 'z', 'atk_2': 'x', 'atk_3': 'c', 'atk_4': 'v', 'boat_1': 'b' };
for (const [atkId, defaultKey] of Object.entries(defaultCombatKeys)) {
const alreadyBound = Object.values(cfg.hotkeyMap).includes(atkId);
if (!alreadyBound) {
delete cfg.hotkeyMap[defaultKey];
cfg.hotkeyMap[defaultKey] = atkId;
}
}
function saveCfg() {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(cfg)); } catch(e) {}
}
// websocket stuff
const NativeWS = window.WebSocket;
const NativeSend = NativeWS.prototype.send;
const WS_OPEN = NativeWS.OPEN || 1; // use native constant never undefined
let activeSocket = null;
// the game creates many sockets like lobby, analytics, CDN that are not the game socket
window.WebSocket = function(url, protocols) {
const sock = protocols !== undefined ? new NativeWS(url, protocols) : new NativeWS(url);
sock.addEventListener('close', () => {
if (activeSocket === sock) {
activeSocket = null;
setLinkStatus(false);
}
});
return sock;
};
window.WebSocket.prototype = NativeWS.prototype;
Object.getOwnPropertyNames(NativeWS).forEach(k => {
try { window.WebSocket[k] = NativeWS[k]; } catch(_) {}
});
NativeWS.prototype.send = function(data) {
if (typeof data === 'string') {
try {
const msg = JSON.parse(data);
if (msg) {
// only see this socket as the game socket when it sends a game intent
// or a known game message type... prevents lobby sockets
// from stealing activeSocket and causing disconnect
// i hate websockets D:
const isGameMsg = (msg.type === 'intent' && msg.intent) ||
msg.type === 'quick_chat' ||
msg.type === 'ping';
if (isGameMsg && activeSocket !== this) {
activeSocket = this;
setLinkStatus(true);
}
const intent = msg.intent || msg;
if (typeof intent.tile === 'number') lastKnownTile = intent.tile;
if (typeof intent.dst === 'number') lastKnownTile = intent.dst;
if (typeof intent.src === 'number') lastKnownTile = intent.src;
}
} catch(e) {}
}
return NativeSend.apply(this, arguments);
};
function sendPacket(intentObj) {
if (!activeSocket || activeSocket.readyState !== WS_OPEN) return false;
try {
NativeSend.call(activeSocket, JSON.stringify({ type: 'intent', intent: intentObj }));
return true;
} catch(e) {
console.error('[Blon] Packet send failed:', e);
return false;
}
}
function setLinkStatus(connected) {
const el = document.getElementById('blon-link');
if (!el) return;
el.textContent = connected ? 'CONNECTED' : 'DISCONNECTED';
el.style.color = connected ? '#00ff66' : '#ff4444';
}
// mouse pos
let mouseX = 0, mouseY = 0;
window.addEventListener('mousemove', e => { mouseX = e.clientX; mouseY = e.clientY; });
let lastKnownTile = null;
function hookCanvasTileSniff() {
const sniff = (clientX, clientY) => {
try {
const overlay = document.querySelector('player-info-overlay');
if (overlay && overlay.game && overlay.transform) {
const worldCoord = overlay.transform.screenToWorldCoordinates(clientX, clientY);
if (worldCoord && typeof overlay.game.isValidCoord === 'function' && overlay.game.isValidCoord(worldCoord.x, worldCoord.y)) {
const tile = overlay.game.ref(worldCoord.x, worldCoord.y);
if (tile !== null && tile !== undefined) {
lastKnownTile = tile;
}
}
}
} catch(e) {}
};
window.addEventListener('mousemove', (e) => {
sniff(e.clientX, e.clientY);
}, { passive: true });
window.addEventListener('pointerdown', (e) => {
sniff(e.clientX, e.clientY);
}, { passive: true });
}
hookCanvasTileSniff();
const UNIT_TYPES = {
'City': 'City',
'Defense Post': 'Defense Post',
'Port': 'Port',
'SAM Launcher': 'SAM Launcher',
'Missile Silo': 'Missile Silo',
'Factory': 'Factory',
'Warship': 'Warship',
'Atom Bomb': 'Atom Bomb',
'H-Bomb': 'Hydrogen Bomb',
'MIRV': 'MIRV',
};
let spamInterval = null;
let holdTimeout = null;
let countdownInterval = null;
let currentSpamKey = null;
let isKeyDown = false;
let toggleSpamActive = false;
let internalRebindSlot = null;
let internalRebindAction = null; // for hotkeys
function getGameState() {
try {
const overlay = document.querySelector('player-info-overlay');
let game = overlay && overlay.game ? overlay.game : null;
if (!game) game = readCachedGameView();
if (!game) return null;
const myPlayer = game.myPlayer();
if (!myPlayer) return null;
return { game, myPlayer };
} catch(e) { return null; }
}
function updateCombatStatus(text, color) {
const el = document.getElementById('blon-combat-status');
if (el) { el.textContent = text; el.style.color = color || '#00ff66'; }
}
function getSiloConstructionInfo(game) {
try {
if (typeof game.unitStates === 'function' && typeof game.unit === 'function') {
for (const state of game.unitStates().values()) {
if (
state.unitType === 'Missile Silo' &&
state.underConstruction === true
) {
const unit = game.unit(state.id);
if (unit) return unit;
}
}
}
const silos = (typeof game.units === 'function' ? game.units('Missile Silo') : []) || [];
return silos.filter(
(u) => typeof u.isUnderConstruction === 'function' && u.isUnderConstruction(),
)[0] || null;
} catch (e) {
return null;
}
}
function getSiloTrackerUnits(game) {
const units = [];
const seen = new Set();
const includeUnit = (unit) => {
if (!unit) return;
const id = typeof unit.id === 'function' ? unit.id() : null;
if (id !== null && seen.has(id)) return;
if (!cfg.combatSiloKeepAllPlaced && typeof unit.isUnderConstruction === 'function' && !unit.isUnderConstruction()) return;
if (id !== null) seen.add(id);
units.push(unit);
};
try {
if (typeof game.unitStates === 'function' && typeof game.unit === 'function') {
for (const state of game.unitStates().values()) {
if (state.unitType !== 'Missile Silo') continue;
if (state.isActive !== true) continue;
if (state.markedForDeletion !== false) continue;
if (!cfg.combatSiloKeepAllPlaced && state.underConstruction !== true) continue;
const unit = game.unit(state.id);
includeUnit(unit);
}
}
if (typeof game.units === 'function') {
const silos = game.units('Missile Silo');
silos.forEach(includeUnit);
}
} catch (e) {
return [];
}
return units;
}
function isAllySilo(unit, myPlayer) {
if (!myPlayer || !unit || typeof unit.owner !== 'function') return false;
try {
const owner = unit.owner();
if (!owner) return false;
return isFriendlyPlayer(myPlayer, owner);
} catch (e) {
return false;
}
}
function getSiloTileCoordinates(game, unit) {
try {
const tile = typeof unit.tile === 'function' ? unit.tile() : null;
if (!tile) return null;
if (typeof game.x !== 'function' || typeof game.y !== 'function') return null;
return { x: game.x(tile), y: game.y(tile) };
} catch (e) {
return null;
}
}
function updateSiloPanel() {
if (!cfg.combatSiloIndicator || !cfg.combatSiloPanel) {
removePopoutPanel('blon-popout-silos');
return;
}
const panel = document.getElementById('blon-popout-silos') || createDraggablePopoutPanel('blon-popout-silos', 'Silo Tracker', () => {
cfg.combatSiloPanel = false;
saveCfg();
const toggle = document.getElementById('cfg-combat-silo-panel');
if (toggle) toggle.checked = false;
});
const content = document.getElementById('blon-popout-silos-content');
if (!content) return;
const state = getGameState();
if (!state) {
content.innerHTML = '<div style="color:#777;">No game state available.</div>';
return;
}
const allSilos = getSiloTrackerUnits(state.game).filter((unit) => {
if (!cfg.combatSiloOnlyAllies) return true;
if (!state.myPlayer) return false;
return !isAllySilo(unit, state.myPlayer);
});
if (allSilos.length === 0) {
content.innerHTML = '<div style="color:#777;">No matching missile silos found.</div>';
return;
}
const items = allSilos
.sort((a, b) => {
const aUnder = typeof a.isUnderConstruction === 'function' && a.isUnderConstruction() ? 0 : 1;
const bUnder = typeof b.isUnderConstruction === 'function' && b.isUnderConstruction() ? 0 : 1;
return aUnder - bUnder;
})
.map((unit) => {
const owner = typeof unit.owner === 'function' ? unit.owner() : null;
const ownerName = owner ? formatPlayerLabel(owner, state.myPlayer) : 'Unknown';
const coords = getSiloTileCoordinates(state.game, unit);
const status = typeof unit.isUnderConstruction === 'function' && unit.isUnderConstruction() ? 'Building' : 'Placed';
const posLabel = coords ? `@${coords.x},${coords.y}` : '@unknown';
return { unit, ownerName, status, posLabel, coords };
});
content.innerHTML = '';
items.forEach((item) => {
const row = document.createElement('div');
row.style.cssText = 'padding:6px 6px;border-bottom:1px solid rgba(255,255,255,0.08);cursor:pointer;';
row.innerHTML = `
<div style="display:flex;justify-content:space-between;gap:8px;align-items:center;">
<strong style="color:#fff;font-size:11px;">${item.ownerName}</strong>
<span style="color:${item.status === 'Building' ? '#00ccff' : '#ffcc66'};font-size:10px;">${item.status}</span>
</div>
<div style="display:flex;justify-content:space-between;gap:8px;align-items:center;margin-top:2px;font-size:10px;color:#aaa;">
<span>${item.posLabel}</span>
${item.coords ? '<span style="color:#ccc;">center</span>' : ''}
</div>
`;
row.addEventListener('click', () => {
if (item.coords) centerCameraOnTile(item.coords.x, item.coords.y);
});
content.appendChild(row);
});
}
function setSiloSubtoggleVisibility() {
const subSection = document.getElementById('blon-silo-subtoggles');
if (!subSection) return;
subSection.style.display = cfg.combatSiloIndicator ? 'block' : 'none';
}
function formatPlayerLabel(player, myPlayer) {
try {
if (player && typeof player.smallID === 'function' && myPlayer && typeof myPlayer.smallID === 'function' && player.smallID() === myPlayer.smallID()) {
return 'You';
}
if (player && typeof player.displayName === 'function') return player.displayName();
if (player && typeof player.name === 'function') return player.name();
} catch (e) {}
return 'Unknown';
}
function centerCameraOnTile(x, y) {
try {
const overlay = document.querySelector('player-info-overlay');
if (!overlay) return;
const transform = overlay.transform;
if (transform && typeof transform.onGoToPosition === 'function') {
transform.onGoToPosition({ x, y });
}
} catch (e) {
console.warn('Failed to center camera on silo', e);
}
}
function updateSiloNotification() {
const el = document.getElementById('blon-silo-notification');
if (!el) return;
if (!cfg.combatSiloIndicator) {
el.style.display = 'none';
return;
}
const state = getGameState();
if (!state) {
el.style.display = 'none';
return;
}
const silo = getSiloConstructionInfo(state.game);
if (!silo) {
el.style.display = 'none';
updateSiloPanel();
return;
}
const owner = typeof silo.owner === 'function' ? silo.owner() : null;
const ownerLabel = owner ? formatPlayerLabel(owner, state.myPlayer) : 'Unknown';
const message = ownerLabel === 'You'
? 'You are placing a silo'
: `${ownerLabel} is placing a silo`;
el.textContent = message;
el.style.display = 'block';
}
function onSiloNotificationClick() {
const state = getGameState();
if (!state) return;
const silo = getSiloConstructionInfo(state.game);
if (!silo) return;
const tile = typeof silo.tile === 'function' ? silo.tile() : null;
if (!tile) return;
const x = typeof state.game.x === 'function' ? state.game.x(tile) : null;
const y = typeof state.game.y === 'function' ? state.game.y(tile) : null;
if (x === null || y === null) return;
centerCameraOnTile(x, y);
}
const QUICK_CHAT_KEYS = [
'help.troops','help.troops_frontlines','help.gold','help.no_attack','help.sorry_attack',
'help.alliance','help.trade_partners',
'attack.build_warships',
'defend.build_posts',
'greet.hello','greet.good_job','greet.good_luck','greet.have_fun','greet.gg',
'greet.nice_to_meet','greet.well_played','greet.hi_again','greet.bye',
'greet.thanks','greet.oops','greet.trust_me','greet.trust_broken',
'greet.ruining_games','greet.dont_do_that','greet.same_team',
'misc.go','misc.strategy','misc.fun','misc.pr','misc.build_closer','misc.coastline',
'warnings.number1_warning','warnings.stalemate','warnings.stop_trading_all'
];
function samePlayer(a, b) {
if (!a || !b) return false;
try {
if (typeof a.smallID === 'function' && typeof b.smallID === 'function') return a.smallID() === b.smallID();
if (typeof a.id === 'function' && typeof b.id === 'function') return a.id() === b.id();
} catch (e) {}
return false;
}
function isFriendlyPlayer(myPlayer, otherPlayer) {
if (!myPlayer || !otherPlayer) return false;
try {
if (samePlayer(myPlayer, otherPlayer)) return true;
if (typeof myPlayer.isFriendly === 'function' && myPlayer.isFriendly(otherPlayer)) return true;
if (typeof myPlayer.isAlliedWith === 'function' && myPlayer.isAlliedWith(otherPlayer)) return true;
if (typeof myPlayer.isOnSameTeam === 'function' && myPlayer.isOnSameTeam(otherPlayer)) return true;
if (typeof otherPlayer.isFriendly === 'function' && otherPlayer.isFriendly(myPlayer)) return true;
if (typeof otherPlayer.isAlliedWith === 'function' && otherPlayer.isAlliedWith(myPlayer)) return true;
if (typeof otherPlayer.isOnSameTeam === 'function' && otherPlayer.isOnSameTeam(myPlayer)) return true;
if (typeof myPlayer.team === 'function' && typeof otherPlayer.team === 'function') {
const myTeam = myPlayer.team();
const otherTeam = otherPlayer.team();
if (myTeam != null && otherTeam != null && myTeam === otherTeam) return true;
}
} catch (e) {
return false;
}
return false;
}
function getGamePlayers(game) {
try {
if (game && typeof game.players === 'function') return game.players() || [];
if (game && typeof game.playerViews === 'function') return game.playerViews() || [];
} catch (e) {}
return [];
}
function sendQuickChat(key, targetMode) {
const state = getGameState();
if (!state) {
updateDiploStatus('Game state not found', '#ff4444'); return;
}
if (!activeSocket || activeSocket.readyState !== WS_OPEN) {
updateDiploStatus('Not connected', '#ff4444'); return;
}
try {
const allPlayers = getGamePlayers(state.game);
const otherPlayers = (allPlayers || []).filter(p => {
try {
return p && p.isPlayer && p.isPlayer() && !samePlayer(p, state.myPlayer) && p.isAlive();
} catch(e) { return false; }
});
if (otherPlayers.length === 0) {
updateDiploStatus('No other players found', '#ffaa00');
return;
}
const mode = ['everyone', 'allies', 'enemies'].includes(targetMode) ? targetMode : 'enemies';
const targetPlayers = otherPlayers.filter(p => {
const friendly = isFriendlyPlayer(state.myPlayer, p);
if (mode === 'allies') return friendly;
if (mode === 'enemies') return !friendly;
return true;
});
if (targetPlayers.length === 0) {
const label = mode === 'allies' ? 'allies' : mode === 'enemies' ? 'enemies' : 'players';
updateDiploStatus(`No ${label} found`, '#ffaa00');
return;
}
let sent = 0;
targetPlayers.forEach(p => {
const ok = sendPacket({
type: 'quick_chat',
recipient: p.id(),
quickChatKey: key
});
if (ok) sent++;
});
const targetLabel = mode === 'allies' ? 'allies' : mode === 'enemies' ? 'enemies' : 'everyone';
updateDiploStatus(`Sent to ${sent} ${targetLabel}`, '#00ff66');
} catch(e) {
updateDiploStatus('Send failed: ' + e.message, '#ff4444');
}
setTimeout(() => updateDiploStatus('Ready', '#00ff66'), 2000);
}
let autoAcceptTimer = null;
function setAutoAccept(enabled) {
if (enabled) {
autoAcceptTimer = setInterval(() => {
const state = getGameState();
if (!state) return;
try {
const allPlayers = state.game.players ? state.game.players() : [];
(allPlayers || []).forEach(p => {
try {
if (!p || !p.isPlayer || !p.isPlayer() || !p.isAlive()) return;
if (p.id() === state.myPlayer.id()) return;
// Check if they have an active alliance request sent to us
const outgoing = p.state.outgoingAllianceRequests || [];
const hasRequestedMe = outgoing.includes(state.myPlayer.id());
// Check if already allied
const isAllied = state.myPlayer.isAlliedWith && state.myPlayer.isAlliedWith(p);
if (hasRequestedMe && !isAllied) {
sendPacket({ type: 'allianceRequest', recipient: p.id() });
}
} catch(e) {}
});
} catch(e) {}
}, 2000);
} else {
clearInterval(autoAcceptTimer);
autoAcceptTimer = null;
}
}
function updateDiploStatus(text, color) {
const el = document.getElementById('blon-diplo-status');
if (el) { el.textContent = text; el.style.color = color || '#00ff66'; }
}
let lastGoldAmount = null;
let lastGoldTime = null;
let lastGoldRate = 0;
let lastPlayerGoldSnapshot = new Map();
const playerGoldHistory = new Map();
function getGoldRateWindowMs() {
return Math.max(5000, Math.min(240000, (cfg.goldRateWindowSeconds || 12) * 1000));
}
function recordPlayerGoldSample(id, gold) {
if (id == null) return null;
const now = Date.now();
const numericGold = typeof gold === 'bigint' ? Number(gold) : Number(gold || 0);
const windowMs = getGoldRateWindowMs();
const history = playerGoldHistory.get(id) || [];
history.push({ time: now, gold: numericGold });
while (history.length > 1 && now - history[0].time > windowMs) {
history.shift();
}
playerGoldHistory.set(id, history);
return history;
}
function getPlayerMpsFromHistory(id) {
const history = playerGoldHistory.get(id);
if (!history || history.length < 2) return null;
const oldest = history[0];
const newest = history[history.length - 1];
const deltaSeconds = Math.max((newest.time - oldest.time) / 1000, 0.001);
return (newest.gold - oldest.gold) / deltaSeconds;
}
function getPlayerFocusCoordinates(game, player) {
if (!game || !player || typeof player.smallID !== 'function') return null;
const playerSmallId = player.smallID();
if (typeof game.units !== 'function' || typeof game.x !== 'function' || typeof game.y !== 'function') {
return null;
}
try {
const units = game.units();
for (const unit of units) {
if (!unit || typeof unit.owner !== 'function' || typeof unit.tile !== 'function') continue;
const owner = unit.owner();
if (!owner || typeof owner.smallID !== 'function' || owner.smallID() !== playerSmallId) continue;
const tile = unit.tile();
if (!tile) continue;
return { x: game.x(tile), y: game.y(tile) };
}
} catch (e) {
return null;
}
return null;
}
function centerCameraOnPlayer(game, player) {
const coords = getPlayerFocusCoordinates(game, player);
if (!coords) return;
centerCameraOnTile(coords.x, coords.y);
}
function getPlayerUniqueId(player) {
if (!player) return null;
if (player.id && typeof player.id === 'function') return player.id();
if (player.smallID && typeof player.smallID === 'function') return player.smallID();
return null;
}
function getPlayerLabel(player) {
if (!player) return 'Unknown';
if (player.name && typeof player.name === 'function') return player.name();
const id = getPlayerUniqueId(player);
return id != null ? `Player ${id}` : 'Unknown';
}
function getPlayerGoldValue(player) {
const value = player.gold ? player.gold() : 0;
return typeof value === 'bigint' ? Number(value) : Number(value || 0);
}
function getPlayerTeamKey(player) {
if (!player) return 'No Team';
let rawTeam = null;
if (player.team && typeof player.team === 'function') rawTeam = player.team();
else if (player.static && player.static.team != null) rawTeam = player.static.team;
else if (player.team != null) rawTeam = player.team;
if (rawTeam == null || rawTeam === 'No Team' || rawTeam === '') return 'No Team';
return `Team ${rawTeam}`;
}
function fmtNum(n) {
if (n == null) return '?';
if (n >= 1e9) return (n/1e9).toFixed(2) + 'B';
if (n >= 1e6) return (n/1e6).toFixed(2) + 'M';
if (n >= 1e3) return (n/1e3).toFixed(1) + 'K';
return String(Math.floor(n));
}
function createDraggablePopoutPanel(panelId, title, onClose) {
const existing = document.getElementById(panelId);
if (existing) return existing;
const panel = document.createElement('div');
panel.id = panelId;
panel.style.cssText = `
position:fixed; left:50%; top:50%; transform:translate(-50%, -50%); width:250px; min-width:200px; min-height:80px;
background:rgba(0,0,0,0.95); color:#fff; border:1px solid #333;
font-family:monospace; font-size:11px; border-radius:5px; z-index:999999;
box-shadow:0 8px 30px rgba(0,0,0,0.7); overflow:hidden; cursor:default;
`;
panel.innerHTML = `
<div class="blon-popout-drag" style="display:flex;justify-content:space-between;align-items:center;padding:8px 10px;background:#111;border-bottom:1px solid #222;cursor:move;user-select:none;">
<span style="font-size:10px;color:#00ff66;font-weight:bold;">${title}</span>
<div style="display:flex;gap:6px;align-items:center;">
<button id="${panelId}-minimize" style="background:transparent;border:none;color:#888;cursor:pointer;font-size:12px;line-height:1;">▾</button>
<button id="${panelId}-close" style="background:transparent;border:none;color:#888;cursor:pointer;font-size:12px;">×</button>
</div>
</div>
<div id="${panelId}-content" style="padding:10px;line-height:1.4;color:#ddd;"></div>
<div class="blon-popout-resize" style="position:absolute;right:4px;bottom:4px;width:12px;height:12px;cursor:nwse-resize;border-right:2px solid #444;border-bottom:2px solid #444;">
</div>
`;
document.body.appendChild(panel);
const dragHandle = panel.querySelector('.blon-popout-drag');
let dragging = false;
let offsetX = 0;
let offsetY = 0;
const contentEl = panel.querySelector(`#${panelId}-content`);
const minimizeBtn = panel.querySelector(`#${panelId}-minimize`);
const closeBtn = panel.querySelector(`#${panelId}-close`);
const resizeHandle = panel.querySelector('.blon-popout-resize');
let resizing = false;
let startX = 0;
let startY = 0;
let startWidth = 0;
let startHeight = 0;
if (contentEl) {
contentEl.style.fontSize = '11px';
contentEl.style.overflowY = 'auto';
contentEl.style.maxHeight = '300px';
contentEl.style.boxSizing = 'border-box';
}
panel.dataset.minimized = 'false';
panel.dataset.savedHeight = '';
const setMinimizedState = minimized => {
if (!contentEl) return;
if (minimized) {
panel.dataset.savedHeight = panel.style.height || `${panel.getBoundingClientRect().height}px`;
contentEl.style.display = 'none';
panel.style.height = `${dragHandle.getBoundingClientRect().height}px`;
panel.style.minHeight = '0px';
resizeHandle.style.display = 'none';
minimizeBtn.textContent = '▴';
panel.dataset.minimized = 'true';
} else {
contentEl.style.display = 'block';
panel.style.height = panel.dataset.savedHeight || '';
panel.style.minHeight = '80px';
resizeHandle.style.display = 'block';
minimizeBtn.textContent = '▾';
panel.dataset.minimized = 'false';
}
};
dragHandle.addEventListener('mousedown', e => {
if (e.target === minimizeBtn || e.target === closeBtn) return;
dragging = true;
const rect = panel.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
panel.style.transform = 'none';
panel.style.left = `${rect.left}px`;
panel.style.top = `${rect.top}px`;
dragHandle.style.color = '#00ffcc';
e.preventDefault();
});
window.addEventListener('mousemove', e => {
if (dragging) {
panel.style.left = `${e.clientX - offsetX}px`;
panel.style.top = `${e.clientY - offsetY}px`;
panel.style.right = 'auto';
}
if (resizing) {
const newWidth = Math.max(200, startWidth + e.clientX - startX);
const newHeight = Math.max(80, startHeight + e.clientY - startY);
panel.style.width = `${newWidth}px`;
panel.style.height = `${newHeight}px`;
panel.dataset.savedHeight = panel.style.height;
if (contentEl) {
const fontSize = Math.min(16, Math.max(10, 11 * (newWidth / 250)));
contentEl.style.fontSize = `${fontSize.toFixed(1)}px`;
}
}
});
window.addEventListener('mouseup', () => {
if (dragging) {
dragging = false;
dragHandle.style.color = '#00ff66';
}
resizing = false;
});
resizeHandle.addEventListener('mousedown', e => {
resizing = true;
const rect = panel.getBoundingClientRect();
startX = e.clientX;
startY = e.clientY;
startWidth = rect.width;
startHeight = rect.height;
e.preventDefault();
e.stopPropagation();
});
minimizeBtn.addEventListener('mousedown', e => e.stopPropagation());
closeBtn.addEventListener('mousedown', e => e.stopPropagation());
minimizeBtn.addEventListener('click', () => {
const minimized = panel.dataset.minimized === 'true';
setMinimizedState(!minimized);
});
closeBtn.addEventListener('click', () => {
panel.remove();
if (typeof onClose === 'function') onClose();
updateMainGoldOverlayVisibility();
});
return panel;
}
function removePopoutPanel(panelId) {
const panel = document.getElementById(panelId);
if (panel) panel.remove();
updateMainGoldOverlayVisibility();
}
function updateMainGoldOverlayVisibility() {
const goldOverlay = document.getElementById('blon-gold-overlay');
if (!goldOverlay) return;
const popoutGold = !!document.getElementById('blon-popout-gold');
const popoutMps = !!document.getElementById('blon-popout-mps');
goldOverlay.style.display = (cfg.showGoldOverlay && !popoutGold && !popoutMps) ? 'block' : 'none';
}
function renderGoldOverlay(state) {
const populateTop10 = (top10El, groupEl) => {
if (!top10El || !groupEl) return;
const allPlayers = state.game.players ? state.game.players() : [];
const players = (allPlayers || []).filter(p => p && p.gold && typeof p.gold === 'function');
const playerRows = players.map(p => {
const value = getPlayerGoldValue(p);
return { player: p, gold: value };
}).sort((a, b) => b.gold - a.gold).slice(0, cfg.goldTopCount || 10);
const myId = state.myPlayer.id ? state.myPlayer.id() : null;
top10El.innerHTML = '';
if (playerRows.length === 0) {
top10El.innerHTML = '<div style="color:#777;grid-column:1/-1;">No player gold data available.</div>';
} else {
playerRows.forEach((row, index) => {
const isMe = myId !== null && row.player.id && row.player.id() === myId;
const rank = document.createElement('div');
rank.textContent = `${index + 1}.`;
rank.style.color = isMe ? '#fff' : '#aaa';
const name = document.createElement('div');
name.textContent = getPlayerLabel(row.player);
name.style.color = isMe ? '#00ff66' : '#ddd';
name.style.whiteSpace = 'nowrap';
name.style.cursor = 'pointer';
name.title = 'Click to center on player';
name.addEventListener('click', () => centerCameraOnPlayer(state.game, row.player));
const value = document.createElement('div');
value.textContent = fmtNum(row.gold);
value.style.color = '#ffcc00';
value.style.textAlign = 'right';
top10El.appendChild(rank);
top10El.appendChild(name);
top10El.appendChild(value);
});
}
const grouped = new Map();
let teamMode = false;
players.forEach(p => {
const teamKey = getPlayerTeamKey(p);
if (teamKey !== 'No Team') teamMode = true;
grouped.set(teamKey, (grouped.get(teamKey) || 0) + getPlayerGoldValue(p));
});
groupEl.innerHTML = '';
if (!teamMode) {
groupEl.innerHTML = '<div style="color:#777;grid-column:1/-1;">Team totals unavailable in this game mode.</div>';
} else {
Array.from(grouped.entries()).sort((a, b) => b[1] - a[1]).forEach(([team, gold]) => {
const teamName = document.createElement('div');
teamName.textContent = team;
teamName.style.color = '#ddd';
const teamValue = document.createElement('div');
teamValue.textContent = fmtNum(gold);
teamValue.style.color = '#ffcc00';
teamValue.style.textAlign = 'right';
groupEl.appendChild(teamName);
groupEl.appendChild(teamValue);
});
}
};
const populateTop10Mps = (mpsEl) => {
if (!mpsEl) return;
const allPlayers = state.game.players ? state.game.players() : [];
const players = (allPlayers || []).filter(p => p && p.gold && typeof p.gold === 'function');
if (!players.length) {
mpsEl.innerHTML = '<div style="color:#777;grid-column:1/-1;">Income/sec data unavailable yet.</div>';
return;
}
const rows = players.map(p => {
const id = getPlayerUniqueId(p);
const mps = id != null ? getPlayerMpsFromHistory(id) : null;
return {
player: p,
mps
};
}).filter(row => row.mps !== null).sort((a, b) => b.mps - a.mps).slice(0, cfg.goldTopCount || 10);
mpsEl.innerHTML = '';
if (rows.length === 0) {
mpsEl.innerHTML = '<div style="color:#777;grid-column:1/-1;">Income/sec data unavailable yet.</div>';
return;
}
const myId = state.myPlayer.id ? state.myPlayer.id() : null;
rows.forEach((row, index) => {
const isMe = myId !== null && row.player.id && row.player.id() === myId;
const rank = document.createElement('div');
rank.textContent = `${index + 1}.`;
rank.style.color = isMe ? '#fff' : '#aaa';
const name = document.createElement('div');
name.textContent = getPlayerLabel(row.player);
name.style.color = isMe ? '#00ff66' : '#ddd';
name.style.whiteSpace = 'nowrap';
name.style.cursor = 'pointer';
name.title = 'Click to center on player';
name.addEventListener('click', () => centerCameraOnPlayer(state.game, row.player));
const value = document.createElement('div');
value.textContent = `${fmtNum(row.mps)}/s`;
value.style.color = '#ffcc00';
value.style.textAlign = 'right';
mpsEl.appendChild(rank);
mpsEl.appendChild(name);
mpsEl.appendChild(value);
});
};
const allPlayers = state.game.players ? state.game.players() : [];
const currentSnapshot = new Map();
(allPlayers || []).forEach(p => {
const id = getPlayerUniqueId(p);
if (id != null) {
const gold = getPlayerGoldValue(p);
currentSnapshot.set(id, gold);
recordPlayerGoldSample(id, gold);
}
});
const top10El = document.getElementById('blon-gold-top10');
const teamGoldEl = document.getElementById('blon-team-gold');
populateTop10(top10El, teamGoldEl);
const mpsTop10El = document.getElementById('blon-gold-mps-top10');
populateTop10Mps(mpsTop10El);
const popoutTop10El = document.getElementById('blon-popout-gold-top10');
const popoutTeamGoldEl = document.getElementById('blon-popout-team-gold');
if (popoutTop10El || popoutTeamGoldEl) {
populateTop10(popoutTop10El, popoutTeamGoldEl);
}
const popoutMpsEl = document.getElementById('blon-popout-mps-top10');
if (popoutMpsEl) {
populateTop10Mps(popoutMpsEl);
}
updateMainGoldOverlayVisibility();
lastPlayerGoldSnapshot = currentSnapshot;
}
function renderGoldRate(state) {
const currentGold = state.myPlayer.gold ? state.myPlayer.gold() : 0;
const numericGold = typeof currentGold === 'bigint' ? Number(currentGold) : currentGold;
const myId = getPlayerUniqueId(state.myPlayer);
const selfHistory = recordPlayerGoldSample(myId, numericGold);
if (selfHistory && selfHistory.length > 1) {
const oldest = selfHistory[0];
const deltaGold = numericGold - oldest.gold;
const deltaSeconds = Math.max((Date.now() - oldest.time) / 1000, 0.001);
lastGoldRate = deltaGold / deltaSeconds;
}
lastGoldAmount = numericGold;
lastGoldTime = Date.now();
const rateEl = document.getElementById('blon-stat-gold-rate');
if (rateEl) rateEl.textContent = `${fmtNum(lastGoldRate)}/s`;
const popoutRateEl = document.getElementById('blon-popout-gold-rate');
if (popoutRateEl) popoutRateEl.textContent = `${fmtNum(lastGoldRate)}/s`;
const popoutValueEl = document.getElementById('blon-popout-gold-value');
if (popoutValueEl) popoutValueEl.textContent = fmtNum(numericGold);
}
function updateStatsHUD() {
const tEl = document.getElementById('blon-stat-troops');
const gEl = document.getElementById('blon-stat-gold');
const tiEl = document.getElementById('blon-stat-tiles');
const aEl = document.getElementById('blon-stat-attacks');
if (!tEl) return;
const state = getGameState();
if (!state) {
[tEl,gEl,tiEl,aEl].forEach(el => { if(el) el.textContent = 'N/A'; });
const rateEl = document.getElementById('blon-stat-gold-rate');
if (rateEl) rateEl.textContent = 'N/A';
const popoutRateEl = document.getElementById('blon-popout-gold-rate');
if (popoutRateEl) popoutRateEl.textContent = 'N/A';
const popoutValueEl = document.getElementById('blon-popout-gold-value');
if (popoutValueEl) popoutValueEl.textContent = 'N/A';
const top10El = document.getElementById('blon-gold-top10');
const teamGoldEl = document.getElementById('blon-team-gold');
if (top10El) top10El.innerHTML = '<div style="color:#777;grid-column:1/-1;">No game state available.</div>';
if (teamGoldEl) teamGoldEl.innerHTML = '<div style="color:#777;grid-column:1/-1;">No game state available.</div>';
const popoutTop10El = document.getElementById('blon-popout-gold-top10');
const popoutTeamGoldEl = document.getElementById('blon-popout-team-gold');
if (popoutTop10El) popoutTop10El.innerHTML = '<div style="color:#777;grid-column:1/-1;">No game state available.</div>';
if (popoutTeamGoldEl) popoutTeamGoldEl.innerHTML = '<div style="color:#777;grid-column:1/-1;">No game state available.</div>';
updateMainGoldOverlayVisibility();
return;
}
try {
if (tEl) {
const tr = state.myPlayer.troops ? state.myPlayer.troops() : 0;
const numericTroops = typeof tr === 'bigint' ? Number(tr) : tr;
tEl.textContent = fmtNum(numericTroops / 10);
}
if (gEl) {
const g = state.myPlayer.gold ? state.myPlayer.gold() : 0;
gEl.textContent = fmtNum(typeof g === 'bigint' ? Number(g) : g);
}
if (tiEl) tiEl.textContent = fmtNum(state.myPlayer.numTilesOwned ? state.myPlayer.numTilesOwned() : 0);
const atks = state.myPlayer.outgoingAttacks ? state.myPlayer.outgoingAttacks() : [];
if (aEl) aEl.textContent = atks.length;
renderGoldRate(state);
if (cfg.features && cfg.features.overlays === false) {
const overlay = document.getElementById('blon-gold-overlay');
if (overlay) overlay.style.display = 'none';
const popTop10 = document.getElementById('blon-popout-gold-top10');
const popTeam = document.getElementById('blon-popout-team-gold');
if (popTop10) popTop10.innerHTML = '';
if (popTeam) popTeam.innerHTML = '';
} else if (cfg.showGoldOverlay || document.getElementById('blon-popout-gold-top10') || document.getElementById('blon-popout-team-gold')) {
renderGoldOverlay(state);
}
updateSiloNotification();
updateSiloPanel();
} catch(e) {}
}
let lobbyWS = null;
let lobbyReconnectTimeout = null;
let lobbyShouldReconnect = true;
let lobbyConnected = false;
let lobbyLatestPayload = null;
function getNumWorkers() {
const bc = window.BOOTSTRAP_CONFIG;
return bc && Number.isInteger(bc.numWorkers) && bc.numWorkers > 0 ? bc.numWorkers : 8;
}
function simpleHashForWorkerPath(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash);
}
function getCurrentWorkerPath() {
const pathParts = window.location.pathname.split("/").filter(Boolean);
const candidate = pathParts[0];
return /^w\d+$/.test(candidate) ? candidate : null;
}
function getWorkerPath(gameID) {
const currentPath = getCurrentWorkerPath();
if (currentPath) {
return currentPath;
}
return `w${simpleHashForWorkerPath(gameID) % getNumWorkers()}`;
}
function getLobbyUrl(gameID) {
const path = getWorkerPath(gameID);
return `${window.location.origin}/${path}/game/${encodeURIComponent(gameID)}`;
}
function formatDuration(ms) {
if (ms <= 0) return 'now';
const seconds = Math.ceil(ms / 1000);
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m`;
return `${Math.floor(minutes / 60)}h`;
}
function getLobbyModeLabel(game) {
const cfg = game.gameConfig;
const mode = cfg && typeof cfg.gameMode === 'string' ? cfg.gameMode : null;
const totalPlayers = cfg?.maxPlayers ?? game.numClients ?? undefined;
if (mode === 'Free For All') {
return 'FFA';
}
if (mode === 'Team') {
if (cfg?.playerTeams === 'Humans Vs Nations') {
return totalPlayers
? `Humans vs Nations (${totalPlayers} players)`
: 'Humans vs Nations';
}
const namedTeamSizes = {
Duos: 2,
Trios: 3,
Quads: 4,
};
if (typeof cfg?.playerTeams === 'string' && namedTeamSizes[cfg.playerTeams]) {
const playersPerTeam = namedTeamSizes[cfg.playerTeams];
if (totalPlayers) {
const teamCount = Math.floor(totalPlayers / playersPerTeam);
return `${cfg.playerTeams} (${teamCount} teams of ${playersPerTeam})`;
}
return cfg.playerTeams;
}
if (typeof cfg?.playerTeams === 'number' && cfg.playerTeams > 0) {
const teamCount = cfg.playerTeams;
if (totalPlayers) {
const playersPerTeam = Math.floor(totalPlayers / teamCount);
return `${teamCount} teams of ${playersPerTeam}`;
}
return `${teamCount} teams`;
}
return 'Team';
}
if (game.publicGameType) {
return String(game.publicGameType).toUpperCase();
}
return 'UNKNOWN';
}
function getLobbyMapDisplay(game) {
try {
if (!game || typeof game !== 'object') return 'Unknown';
const resolveVal = (val) => {
if (val == null) return null;
if (typeof val === 'string' && val.trim()) return val.trim();
if (typeof val === 'number') return String(val);
if (typeof val === 'object') {
if (typeof val.name === 'string' && val.name.trim()) return val.name.trim();
if (typeof val.mapName === 'string' && val.mapName.trim()) return val.mapName.trim();
if (typeof val.gameMap === 'string' && val.gameMap.trim()) return val.gameMap.trim();
if (typeof val.id === 'string' && val.id.trim()) return val.id.trim();
if (typeof val.id === 'number') return String(val.id);
}
return null;
};
// try them im too lazy to check which ones are actually used across servers so might as well be thorough
const topCandidates = [
'gameMap', 'game_map', 'map', 'mapName', 'map_name', 'terrain', 'terrainName', 'terrain_name', 'mapId', 'map_id'
];
for (const k of topCandidates) {
if (Object.prototype.hasOwnProperty.call(game, k)) {
const v = resolveVal(game[k]);
if (v) return v;
}
}
if (game.gameConfig && typeof game.gameConfig === 'object') {
for (const k of topCandidates) {
if (Object.prototype.hasOwnProperty.call(game.gameConfig, k)) {
const v = resolveVal(game.gameConfig[k]);
if (v) return v;
}
}
if (Object.prototype.hasOwnProperty.call(game.gameConfig, 'gameMap')) {
const v = resolveVal(game.gameConfig.gameMap);
if (v) return v;
}
}
for (const key of Object.keys(game)) {
if (/map|terrain/i.test(key)) {
const v = resolveVal(game[key]);
if (v) return v;
}
}
} catch (e) {}
return 'Unknown';
}
function setMatchFeedStatus(text, color) {
const statusEl = document.getElementById('blon-matches-status');
if (statusEl) { statusEl.textContent = text; statusEl.style.color = color || '#ffcc00'; }
}
function updateMatchFeedButton() {
const btn = document.getElementById('blon-matches-connect-btn');
if (!btn) return;
btn.textContent = lobbyWS ? 'Disconnect Feed' : 'Connect Feed';
}
function renderLobbyMatches() {
const listEl = document.getElementById('blon-matches-list');
if (!listEl) return;
if (!lobbyLatestPayload || !lobbyLatestPayload.games) {
listEl.innerHTML = '<div style="color:#777;grid-column:1/-1;">No lobby feed data yet.</div>';
return;
}
const serverTime = typeof lobbyLatestPayload.serverTime === 'number' ? lobbyLatestPayload.serverTime : Date.now();
const games = Object.values(lobbyLatestPayload.games).flat();
if (!games.length) {
listEl.innerHTML = '<div style="color:#777;grid-column:1/-1;">No upcoming matches available.</div>';
return;
}
const sortedGames = games.slice().sort((a, b) => {
const aStart = typeof a.startsAt === 'number' ? a.startsAt : serverTime;
const bStart = typeof b.startsAt === 'number' ? b.startsAt : serverTime;
if (aStart !== bStart) return aStart - bStart;
return (a.numClients || 0) - (b.numClients || 0);
}).slice(0, 18);
listEl.innerHTML = '';
sortedGames.forEach(game => {
const startAt = typeof game.startsAt === 'number' ? game.startsAt : serverTime;
const timeDelta = startAt - serverTime;
const isLive = typeof game.startsAt !== 'number' || timeDelta <= 0;
const status = isLive ? 'Open lobby' : `Starts in ${formatDuration(timeDelta)}`;
const type = getLobbyModeLabel(game);
const row = document.createElement('div');
row.style.cssText = 'display:grid;grid-template-columns:1.7fr 1fr auto;gap:6px;align-items:center;padding:8px 0;border-bottom:1px solid #222;';
row.innerHTML = `
<div style="display:flex;flex-direction:column;gap:3px;">
<span style="color:#fff;font-size:12px;font-weight:600;">${type}</span>
<span style="color:#aaa;font-size:10px;">${status}</span>
<span style="color:#777;font-size:10px;">Map: ${getLobbyMapDisplay(game)}</span>
</div>
<div style="display:flex;flex-direction:column;gap:3px;text-align:right;">
<span style="color:#ddd;font-size:12px;">${game.numClients} players</span>
<span style="color:#777;font-size:10px;">ID ${game.gameID}</span>
</div>
<button type="button" style="padding:5px 8px;background:#111;border:1px solid #333;color:#aaa;border-radius:3px;font-size:10px;cursor:pointer;">Join</button>
`;
const button = row.querySelector('button');
if (button) {
button.addEventListener('click', () => {
const lobbyId = game.gameID;
try {
if (typeof window.showPage === 'function') {
window.showPage('page-join-lobby');
}
const joinEvent = new CustomEvent('join-lobby', {
detail: { gameID: lobbyId, source: 'public' },
bubbles: true,
composed: true,
});
document.dispatchEvent(joinEvent);
return;
} catch (e) {
console.warn('Join via event failed falling back to URL', e);
}
const joinModal = document.querySelector('join-lobby-modal');
if (joinModal && typeof joinModal.open === 'function') {
try { joinModal.open({ lobbyId }); return; } catch (e) { }
}
const url = getLobbyUrl(lobbyId);
window.location.href = url;
});
}
listEl.appendChild(row);
});
}
function scheduleLobbyReconnect() {
if (!lobbyShouldReconnect || lobbyReconnectTimeout) return;
lobbyReconnectTimeout = window.setTimeout(() => {
lobbyReconnectTimeout = null;
if (lobbyShouldReconnect) connectLobbyFeed();
}, 3000);
}
function handleLobbySocketMessage(event) {
try {
const payload = JSON.parse(event.data);
if (payload && typeof payload.serverTime === 'number' && payload.games) {
lobbyLatestPayload = payload;
renderLobbyMatches();
}
} catch(e) {
console.error('[Blon] Lobby feed parse failed', e);
}
}
function connectLobbyFeed() {
if (!NativeWS) return;
if (lobbyWS) return;
lobbyShouldReconnect = true;
const workerPath = `/w${Math.floor(Math.random() * getNumWorkers())}`;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const url = `${protocol}//${window.location.host}${workerPath}/lobbies`;
try {
lobbyWS = new NativeWS(url);
setMatchFeedStatus('Connecting…', '#ffaa00');
updateMatchFeedButton();
lobbyWS.addEventListener('open', () => {
lobbyConnected = true;
setMatchFeedStatus('Connected', '#00ff66');
if (lobbyReconnectTimeout) { clearTimeout(lobbyReconnectTimeout); lobbyReconnectTimeout = null; }
updateMatchFeedButton();
renderLobbyMatches();
});
lobbyWS.addEventListener('message', handleLobbySocketMessage);
lobbyWS.addEventListener('close', () => {
lobbyConnected = false;
lobbyWS = null;
setMatchFeedStatus('Disconnected', '#ff4444');
updateMatchFeedButton();
scheduleLobbyReconnect();
});
lobbyWS.addEventListener('error', () => {
lobbyConnected = false;
setMatchFeedStatus('Error', '#ff4444');
updateMatchFeedButton();
scheduleLobbyReconnect();
});
} catch (e) {
lobbyWS = null;
setMatchFeedStatus('Connect failed', '#ff4444');
updateMatchFeedButton();
scheduleLobbyReconnect();
}
}
function disconnectLobbyFeed() {
lobbyShouldReconnect = false;
if (lobbyReconnectTimeout) { clearTimeout(lobbyReconnectTimeout); lobbyReconnectTimeout = null; }
if (lobbyWS) { lobbyWS.close(); lobbyWS = null; }
lobbyConnected = false;
setMatchFeedStatus('Disconnected', '#ff4444');
updateMatchFeedButton();
}
let slotUnitMap = {
'1': 'City', '2': 'Defense Post', '3': 'Port', '4': 'SAM Launcher',
'5': 'Missile Silo', '6': 'Factory', '7': 'Warship',
'8': 'Atom Bomb', '9': 'Hydrogen Bomb', '0': 'MIRV'
};
try {
const storedSlots = localStorage.getItem('blon_v8_slots');
if (storedSlots) slotUnitMap = Object.assign(slotUnitMap, JSON.parse(storedSlots));
} catch(e) {}
function saveSlots() {
try { localStorage.setItem('blon_v8_slots', JSON.stringify(slotUnitMap)); } catch(e) {}
}
function spamBuildPacket(unitType) {
if (!unitType) return; // undefined unit
if (cfg.spamMethod === 'click') {
fallbackClick();
return;
}
if (lastKnownTile === null) {
fallbackClick();
return;
}
sendPacket({ type: 'build_unit', unit: unitType, tile: lastKnownTile });
}
function fallbackClick() {
const target = document.elementFromPoint(mouseX, mouseY) || document.querySelector('canvas') || document.body;
const props = { view: window, bubbles: true, cancelable: true, clientX: mouseX, clientY: mouseY, button: 0, buttons: 1 };
target.dispatchEvent(new PointerEvent('pointerdown', props));
target.dispatchEvent(new MouseEvent('mousedown', props));
target.dispatchEvent(new PointerEvent('pointerup', props));
target.dispatchEvent(new MouseEvent('mouseup', props));
target.dispatchEvent(new MouseEvent('click', props));
}
function startSpam(slot) {
const unitType = slotUnitMap[slot];
if (!unitType) return;
if (spamInterval !== null) { clearInterval(spamInterval); spamInterval = null; }
const statusEl = document.getElementById('blon-spam-status');
if (statusEl) { statusEl.textContent = `SPAMMING: ${unitType}`; statusEl.style.color = '#ff3333'; }
spamBuildPacket(unitType);
spamInterval = setInterval(() => {
spamBuildPacket(unitType);
}, cfg.spamInterval);
}
function stopSpam() {
clearTimeout(holdTimeout);
clearInterval(countdownInterval);
clearInterval(spamInterval);
holdTimeout = null; countdownInterval = null; spamInterval = null; currentSpamKey = null;
toggleSpamActive = false;
isKeyDown = false;
const statusEl = document.getElementById('blon-spam-status');
if (statusEl) { statusEl.textContent = 'IDLE'; statusEl.style.color = '#888'; }
}
let embargoOnCooldown = false;
let embargoCooldownTimer = null;
let embargoAutoRepeat = false;
const EMBARGO_COOLDOWN_MS = 30000;
function fireEmbargoAll() {
if (embargoOnCooldown) { return; }
if (!activeSocket || activeSocket.readyState !== WS_OPEN) {
updateEmbargoStatus('No socket join a game first', '#ff4444');
return;
}
const ok = sendPacket({ type: 'embargo_all', action: 'start' });
if (ok) {
startEmbargoCooldown();
}
}
function liftEmbargoAll() {
if (!activeSocket || activeSocket.readyState !== WS_OPEN) {
updateEmbargoStatus('No socket join a game first', '#ff4444');
return;
}
sendPacket({ type: 'embargo_all', action: 'stop' });
updateEmbargoStatus('All embargoes lifted', '#00ff66');
setTimeout(() => updateEmbargoStatus('Ready', '#00ff66'), 2000);
}
function getOnePercentAmount(value) {
return getPercentAmount(value, 1);
}
function getPercentAmount(value, percent) {
const n = typeof value === 'bigint' ? Number(value) : Number(value || 0);
if (!Number.isFinite(n) || n <= 0) return 0;
const pct = clamp(Number(percent || 0), 1, 100);
return Math.max(1, Math.floor(n * pct / 100));
}
function getPlayerAtTile(game, tile) {
if (!game || tile === null || tile === undefined) return null;
try {
if (typeof game.hasOwner === 'function' && !game.hasOwner(tile)) return null;
const owner = typeof game.owner === 'function' ? game.owner(tile) : null;
if (!owner || (typeof owner.isPlayer === 'function' && !owner.isPlayer())) return null;
return owner;
} catch (e) {
return null;
}
}
function getDonationTarget() {
const state = getGameState();
if (!state) return null;
const hovered = getPlayerAtTile(state.game, lastKnownTile);
if (
hovered &&
!samePlayer(hovered, state.myPlayer) &&
isFriendlyPlayer(state.myPlayer, hovered) &&
(!hovered.isAlive || hovered.isAlive())
) {
return { state, target: hovered };
}
return { state, target: null };
}
function sendOnePercentBoat() {
const state = getGameState();
if (!state) {
updateCombatStatus('Game state not found', '#ff4444');
return;
}
if (!activeSocket || activeSocket.readyState !== WS_OPEN) {
updateCombatStatus('No socket join a game first', '#ff4444');
return;
}
if (lastKnownTile === null) {
updateCombatStatus('Hover a target tile first', '#ffaa00');
return;
}
const troops = getOnePercentAmount(state.myPlayer.troops ? state.myPlayer.troops() : 0);
if (troops <= 0) {
updateCombatStatus('No troops available', '#ffaa00');
return;
}
const ok = sendPacket({ type: 'boat', troops, dst: lastKnownTile });
updateCombatStatus(ok ? `Sent 1% boat (${fmtNum(troops / 10)})` : 'Boat send failed', ok ? '#00ff66' : '#ff4444');
if (ok) setTimeout(() => updateCombatStatus('Ready', '#00ff66'), 2000);
}
function sendDonationToAlly(kind, percent) {
if (!activeSocket || activeSocket.readyState !== WS_OPEN) {
updateEmbargoStatus('No socket join a game first', '#ff4444');
return;
}
const result = getDonationTarget();
if (!result || !result.state) {
updateEmbargoStatus('Game state not found', '#ff4444');
return;
}
if (!result.target) {
updateEmbargoStatus('Hover an ally first', '#ffaa00');
return;
}
const targetId = result.target.id && result.target.id();
if (!targetId) {
updateEmbargoStatus('Ally id not found', '#ff4444');
return;
}
const amount = kind === 'gold'
? getPercentAmount(result.state.myPlayer.gold ? result.state.myPlayer.gold() : 0, percent)
: getPercentAmount(result.state.myPlayer.troops ? result.state.myPlayer.troops() : 0, percent);
if (amount <= 0) {
updateEmbargoStatus(kind === 'gold' ? 'No gold available' : 'No troops available', '#ffaa00');
return;
}
const intent = kind === 'gold'
? { type: 'donate_gold', recipient: targetId, gold: amount }
: { type: 'donate_troops', recipient: targetId, troops: amount };
const ok = sendPacket(intent);
const label = kind === 'gold' ? 'gold' : 'troops';
const targetName = getPlayerLabel(result.target);
const formattedAmount = kind === 'gold' ? fmtNum(amount) : fmtNum(amount / 10);
updateEmbargoStatus(ok ? `Sent ${percent}% ${label} (${formattedAmount}) to ${targetName}` : `Send ${label} failed`, ok ? '#00ff66' : '#ff4444');
if (ok) setTimeout(() => updateEmbargoStatus('Ready', '#00ff66'), 2000);
}
function startEmbargoCooldown() {
embargoOnCooldown = true;
updateEmbargoStatus('Sent! Cooling down...', '#ffaa00');
const fireBtn = document.getElementById('embargo-fire-btn');
if (fireBtn) {
fireBtn.disabled = true;
fireBtn.style.opacity = '0.4';
fireBtn.style.cursor = 'not-allowed';
}
let remaining = EMBARGO_COOLDOWN_MS / 1000;
const tick = () => {
remaining--;
updateEmbargoStatus(`Cooldown: ${remaining}s`, '#888');
if (remaining <= 0) {
clearInterval(embargoCooldownTimer);
embargoOnCooldown = false;
updateEmbargoStatus('Ready', '#00ff66');
if (fireBtn) { fireBtn.disabled = false; fireBtn.style.opacity = '1'; fireBtn.style.cursor = 'pointer'; }
if (embargoAutoRepeat) { fireEmbargoAll(); }
}
};
embargoCooldownTimer = setInterval(tick, 1000);
}
function updateEmbargoStatus(text, color) {
const el = document.getElementById('embargo-status-text');
if (el) { el.textContent = text; el.style.color = color; }
}
function fmtActionKey(actionId) {
const binding = cfg.actionHotkeyMap[actionId];
if (!binding || !binding.key) return 'NONE';
const mod = binding.mod ? binding.mod.toUpperCase() + '+' : '';
return mod + binding.key.toUpperCase();
}
const ui = document.createElement('div');
ui.id = 'blon-root';
ui.style.cssText = `
position:fixed; top:15px; right:15px; width:320px; min-width:260px; max-width:560px;
background:rgba(0,0,0,${cfg.guiOpacity}); color:#fff;
font-family:monospace; font-size:11px;
border-radius:5px; border:1px solid #333;
z-index:999999; user-select:none;
box-shadow:0 6px 24px rgba(0,0,0,0.7);
resize:both; overflow:hidden;
display:flex; flex-direction:column;
min-height:120px; max-height:85vh;
opacity:1;
`;
function applyUiStyle() {
ui.style.backgroundColor = `rgba(0,0,0,${cfg.guiOpacity})`;
const drag = document.getElementById('blon-drag');
if (drag) drag.style.color = cfg.guiColor;
const link = document.getElementById('blon-link');
if (link) link.style.color = cfg.guiColor;
}
function applyOverlayAppearance() {
const overlay = document.getElementById('blon-gold-overlay');
if (overlay) {
overlay.style.opacity = String(cfg.overlayOpacity);
overlay.style.setProperty('opacity', String(cfg.overlayOpacity), 'important');
}
const popTop10Overlay = document.getElementById('blon-popout-gold');
const popMpsOverlay = document.getElementById('blon-popout-mps');
[popTop10Overlay, popMpsOverlay].forEach(el => {
if (!el) return;
el.style.opacity = String(cfg.overlayOpacity);
el.style.setProperty('opacity', String(cfg.overlayOpacity), 'important');
});
}
ui.innerHTML = `
<div id="blon-drag" style="display:flex;align-items:center;justify-content:space-between;padding:7px 12px;background:#111;color:${cfg.guiColor};font-weight:bold;letter-spacing:1px;cursor:move;border-radius:5px 5px 0 0;border-bottom:1px solid #222;flex-shrink:0;">
<div style="display:flex;align-items:center;gap:7px;">
<a href="https://github.com/blontd6/" target="_blank" id="blon-github" style="color:#00ff66;text-decoration:none;" title="GitHub">
<svg height="14" width="14" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
</svg>
</a>
<span>PROJECT BLON v22</span>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<span id="blon-link" style="font-size:9px;color:#ff4444;font-weight:normal;">DISCONNECTED</span>
<button id="blon-minimize-btn" title="Minimize Blon UI" style="background:transparent;border:none;color:#888;cursor:pointer;font-size:12px;line-height:1;padding:0 4px;">▾</button>
</div>
</div>
<div style="display:flex;background:#161616;border-bottom:1px solid #222;font-size:10px;flex-shrink:0;">
<div id="tab-main2-btn" class="blon-tab" style="flex:1;text-align:center;padding:6px 0;cursor:pointer;border-right:1px solid #222;background:#000;font-weight:bold;color:#fff;">Main</div>
<div id="tab-main-btn" class="blon-tab" style="flex:1;text-align:center;padding:6px 0;cursor:pointer;color:#888;border-right:1px solid #222;">Spam</div>
<div id="tab-stats-btn" class="blon-tab" style="flex:1;text-align:center;padding:6px 0;cursor:pointer;color:#888;border-right:1px solid #222;">Stats</div>
<div id="tab-matches-btn" class="blon-tab" style="flex:1;text-align:center;padding:6px 0;cursor:pointer;color:#888;border-right:1px solid #222;">Lob</div>
<div id="tab-combat-btn" class="blon-tab" style="flex:1;text-align:center;padding:6px 0;cursor:pointer;color:#888;border-right:1px solid #222;">Cmbt</div>
<div id="tab-diplo-btn" class="blon-tab" style="flex:1;text-align:center;padding:6px 0;cursor:pointer;color:#888;border-right:1px solid #222;">Diplo</div>
<div id="tab-embargo-btn" class="blon-tab" style="flex:1;text-align:center;padding:6px 0;cursor:pointer;color:#888;border-right:1px solid #222;">Trade</div>
<div id="tab-cfg-btn" class="blon-tab" style="flex:1;text-align:center;padding:6px 0;cursor:pointer;color:#888;">Cfg</div>
</div>
<div id="tab-main2-panel" style="padding:10px 12px;overflow-y:auto;flex:1;">
<div style="margin-bottom:10px;border-top:1px solid #222;padding-top:8px;">
<div style="color:#aaa;font-size:10px;margin-bottom:6px;">Disable Features</div>
<label style="display:flex;align-items:center;gap:7px;cursor:pointer;color:#aaa;margin:0 0 4px;">
<input type="checkbox" id="blon-feat-spam-hotkeys" ${cfg.features?.spamHotkeys ? 'checked' : ''} style="cursor:pointer;margin:0;"> Spam hotkeys
</label>
<label style="display:flex;align-items:center;gap:7px;cursor:pointer;color:#aaa;margin:0 0 4px;">
<input type="checkbox" id="blon-feat-combat-hotkeys" ${cfg.features?.combatHotkeys ? 'checked' : ''} style="cursor:pointer;margin:0;"> Combat hotkeys
</label>
<label style="display:flex;align-items:center;gap:7px;cursor:pointer;color:#aaa;margin:0 0 4px;">
<input type="checkbox" id="blon-feat-action-hotkeys" ${cfg.features?.actionHotkeys ? 'checked' : ''} style="cursor:pointer;margin:0;"> Action hotkeys (trade/embargo)
</label>
<label style="display:flex;align-items:center;gap:7px;cursor:pointer;color:#aaa;margin:0 0 4px;">
<input type="checkbox" id="blon-feat-quick-chat" ${cfg.features?.quickChat ? 'checked' : ''} style="cursor:pointer;margin:0;"> Quick chat
</label>
<label style="display:flex;align-items:center;gap:7px;cursor:pointer;color:#aaa;margin:0 0 4px;">
<input type="checkbox" id="blon-feat-embargo" ${cfg.features?.embargo ? 'checked' : ''} style="cursor:pointer;margin:0;"> Embargo controls
</label>
<label style="display:flex;align-items:center;gap:7px;cursor:pointer;color:#aaa;margin:0 0 0;">
<input type="checkbox" id="blon-feat-overlays" ${cfg.features?.overlays ? 'checked' : ''} style="cursor:pointer;margin:0;"> Overlays
</label>
</div>
</div>
<div id="tab-main-panel" style="padding:10px 12px;display:none;overflow-y:auto;flex:1;">
<div style="color:#888;margin-bottom:7px;font-size:10px;" id="blon-mode-hint">Hold hotkey over map to spam build packets.</div>
<label style="display:flex;align-items:center;gap:6px;margin-bottom:5px;cursor:pointer;color:#aaa;">
<input type="checkbox" id="blon-toggle-mode" ${cfg.toggleMode ? 'checked' : ''} style="cursor:pointer;margin:0;"> Toggle Mode (press once to start/stop)
</label>
<div style="margin-bottom:8px;">
<div style="color:#aaa;font-size:10px;margin-bottom:4px;">Spam Method:</div>
<div style="display:flex;gap:4px;">
<button id="blon-method-ws" style="flex:1;padding:4px 0;border-radius:3px;border:1px solid #444;font-family:monospace;font-size:10px;cursor:pointer;background:${cfg.spamMethod==='websocket'?'#00ff66':'#111'};color:${cfg.spamMethod==='websocket'?'#000':'#aaa'};font-weight:${cfg.spamMethod==='websocket'?'bold':'normal'};">WebSocket</button>
<button id="blon-method-click" style="flex:1;padding:4px 0;border-radius:3px;border:1px solid #444;font-family:monospace;font-size:10px;cursor:pointer;background:${cfg.spamMethod==='click'?'#ff9900':'#111'};color:${cfg.spamMethod==='click'?'#000':'#aaa'};font-weight:${cfg.spamMethod==='click'?'bold':'normal'};">Click</button>
</div>
</div>
<label style="display:flex;align-items:center;gap:6px;margin-bottom:5px;cursor:pointer;color:#aaa;">
<input type="checkbox" id="blon-toggle-charge" ${cfg.useChargeTime ? 'checked' : ''} style="cursor:pointer;margin:0;"> Enable Charge Delay
</label>
<label style="display:flex;align-items:center;gap:6px;margin-bottom:8px;cursor:pointer;color:#aaa;">
<input type="checkbox" id="blon-toggle-passthrough" ${cfg.blockPassThrough ? 'checked' : ''} style="cursor:pointer;margin:0;"> Block Key Pass-Through
</label>
<div style="font-size:10px;color:#555;border-top:1px solid #222;padding-top:6px;">
Spam Status: <span id="blon-spam-status" style="color:#888;">IDLE</span>
</div>
<div style="font-size:10px;color:#555;margin-top:3px;">
Last Tile: <span id="blon-tile-display" style="color:#888;">none</span>
</div>
</div>
<div id="tab-stats-panel" style="padding:10px 12px;display:none;overflow-y:auto;flex:1;">
<div style="display:grid;grid-template-columns:auto 1fr;gap:4px 10px;font-size:11px;">
<span style="color:#888;">Troops</span> <span id="blon-stat-troops" style="color:#00ff66;font-weight:bold;">N/A</span>
<span style="color:#888;">Gold</span> <span id="blon-stat-gold" style="color:#ffcc00;font-weight:bold;">N/A</span>
<span style="color:#888;">Tiles</span> <span id="blon-stat-tiles" style="color:#66aaff;font-weight:bold;">N/A</span>
<span style="color:#888;">Attacks</span> <span id="blon-stat-attacks" style="color:#ff9900;font-weight:bold;">N/A</span>
<span style="color:#888;">Money/sec</span> <span id="blon-stat-gold-rate" style="color:#ffcc00;font-weight:bold;">N/A</span>
</div>
<div style="display:grid;grid-template-columns:auto 1fr;gap:6px 10px;align-items:center;font-size:11px;margin-top:6px;">
<span style="color:#888;">Smoothing window</span>
<div style="display:flex;align-items:center;gap:8px;">
<input id="blon-gold-rate-window" type="range" min="5" max="240" step="1" value="${cfg.goldRateWindowSeconds}" style="flex:1 1 100px;min-width:60px;max-width:100px;">
<span id="blon-gold-rate-window-value" style="color:#ffcc00;font-size:10px;min-width:40px;text-align:right;">${cfg.goldRateWindowSeconds}s</span>
</div>
</div>
<div style="display:flex;justify-content:space-between;gap:8px;margin:12px 0 8px;flex-wrap:wrap;align-items:center;">
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;color:#aaa;margin:0;">
<input type="checkbox" id="blon-toggle-gold-overlay" ${cfg.showGoldOverlay ? 'checked' : ''} style="cursor:pointer;margin:0;"> Show top gold leaderboard and team totals
</label>
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;color:#aaa;margin:0;">
<input type="checkbox" id="blon-toggle-troops-overlay" ${cfg.showTroopsOverlay ? 'checked' : ''} style="cursor:pointer;margin:0;"> Show Troops in Leaderboard
</label>
<div style="display:flex;align-items:center;gap:8px;color:#aaa;font-size:11px;">
<span>Leaderboard size</span>
<input id="blon-gold-top-count" type="range" min="1" max="20" step="1" value="${cfg.goldTopCount || 10}" style="flex:1 1 120px;min-width:80px;max-width:180px;">
<span id="blon-gold-top-count-value" style="color:#ffcc00;font-size:10px;min-width:20px;text-align:right;">${cfg.goldTopCount || 10}</span>
</div>
<div style="display:flex;align-items:center;gap:8px;color:#aaa;font-size:11px;">
<span>Troops Check</span>
<input id="blon-troops-check-interval" type="range" min="1" max="1000" step="1" value="${cfg.troopsCheckIntervalMs || 500}" style="flex:1 1 120px;min-width:80px;max-width:180px;">
<span id="blon-troops-check-interval-value" style="color:#00ff66;font-size:10px;min-width:40px;text-align:right;">${cfg.troopsCheckIntervalMs || 500}ms</span>
</div>
</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;">
<button id="blon-popout-gold-btn" style="padding:4px 8px;background:#111;border:1px solid #333;color:#aaa;font-size:10px;cursor:pointer;border-radius:3px;">Pop out Gold Leaderboard</button>
<button id="blon-popout-mps-btn" style="padding:4px 8px;background:#111;border:1px solid #333;color:#aaa;font-size:10px;cursor:pointer;border-radius:3px;">Pop out Income/sec</button>
</div>
</div>
<div id="blon-gold-overlay" style="display:${cfg.showGoldOverlay ? 'block' : 'none'};font-size:11px;line-height:1.4;color:#ddd;opacity:${cfg.overlayOpacity};">
<div style="color:#aaa;font-size:10px;margin-bottom:4px;">Top ${cfg.goldTopCount || 10} players by gold</div>
<div id="blon-gold-top10" style="display:grid;grid-template-columns:auto auto 1fr;gap:3px 8px;margin-bottom:10px;"></div>
<div style="color:#aaa;font-size:10px;margin:10px 0 4px;">Top ${cfg.goldTopCount || 10} players by income/sec</div>
<div id="blon-gold-mps-top10" style="display:grid;grid-template-columns:auto auto 1fr;gap:3px 8px;margin-bottom:10px;"></div>
<div style="color:#aaa;font-size:10px;margin-bottom:4px;">Team gold totals</div>
<div id="blon-team-gold" style="display:grid;grid-template-columns:auto 1fr;gap:3px 8px;"></div>
</div>
</div>
<div id="tab-matches-panel" style="padding:10px 12px;display:none;overflow-y:auto;flex:1;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<span style="color:#aaa;font-size:10px;">Future Match Detector</span>
<span id="blon-matches-status" style="color:#ffcc00;font-weight:bold;font-size:10px;">Disconnected</span>
</div>
<div style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap;">
<button id="blon-matches-connect-btn" style="flex:1;min-width:120px;padding:6px;background:#111;border:1px solid #333;color:#aaa;font-size:10px;cursor:pointer;border-radius:3px;">Connect Feed</button>
<button id="blon-matches-refresh-btn" style="flex:1;min-width:120px;padding:6px;background:#111;border:1px solid #333;color:#aaa;font-size:10px;cursor:pointer;border-radius:3px;">Refresh List</button>
</div>
<div id="blon-matches-list" style="display:flex;flex-direction:column;gap:8px;font-size:11px;min-height:120px;color:#ddd;">
<div style="color:#777;font-size:10px;">Waiting for lobby feed...</div>
</div>
</div>
<div id="tab-combat-panel" style="padding:10px 12px;display:none;overflow-y:auto;flex:1;">
<div style="margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;">
<span style="color:#aaa;font-size:10px;">Status:</span>
<span id="blon-combat-status" style="color:#00ff66;font-weight:bold;font-size:10px;">Ready</span>
</div>
<div id="blon-silo-notification" style="color:#00ff66;font-size:10px;min-height:18px;cursor:pointer;text-decoration:underline;display:none;user-select:none;margin-bottom:8px;">Silo indicator disabled</div>
<label style="display:flex;align-items:center;gap:6px;margin-bottom:8px;cursor:pointer;color:#aaa;">
<input type="checkbox" id="cfg-combat-silo-indicator" ${cfg.combatSiloIndicator ? 'checked' : ''} style="cursor:pointer;margin:0;"> Silo build notifications
</label>
<div id="blon-silo-subtoggles" style="display:${cfg.combatSiloIndicator ? 'block' : 'none'};margin-left:14px;margin-bottom:8px;">
<label style="display:flex;align-items:center;gap:6px;margin-bottom:6px;cursor:pointer;color:#aaa;">
<input type="checkbox" id="cfg-combat-silo-panel" ${cfg.combatSiloPanel ? 'checked' : ''} style="cursor:pointer;margin:0;"> Pop out silo tracker UI
</label>
<label style="display:flex;align-items:center;gap:6px;margin-bottom:6px;cursor:pointer;color:#aaa;">
<input type="checkbox" id="cfg-combat-silo-allies" ${cfg.combatSiloOnlyAllies ? 'checked' : ''} style="cursor:pointer;margin:0;"> Filter ally silos only
</label>
<label style="display:flex;align-items:center;gap:6px;margin-bottom:6px;cursor:pointer;color:#aaa;">
<input type="checkbox" id="cfg-combat-silo-keep-all" ${cfg.combatSiloKeepAllPlaced ? 'checked' : ''} style="cursor:pointer;margin:0;"> Keep every placed silo in the UI
</label>
</div>
<label style="display:flex;align-items:center;gap:6px;margin-bottom:8px;cursor:pointer;color:#aaa;">
<input type="checkbox" id="cfg-combat-priority" ${cfg.combatHotkeysPriority ? 'checked' : ''} style="cursor:pointer;margin:0;"> Combat hotkeys override game hotkeys
</label>
<label style="display:flex;align-items:center;gap:6px;margin-bottom:8px;cursor:pointer;color:#aaa;">
<input type="checkbox" id="cfg-missile-predictor" ${cfg.features?.missilePredictor ? 'checked' : ''} style="cursor:pointer;margin:0;"> Missile predictor overlay
</label>
<div style="border-top:1px solid #222;padding-top:8px;margin-top:4px;">
<div style="color:#aaa;font-size:10px;margin-bottom:4px;">Combat hotkeys (sets attack %):</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:3px;font-size:10px;">
<div id="combat-atk1-label" style="color:#888;"></div><div id="combat-atk2-label" style="color:#888;"></div>
<div id="combat-atk3-label" style="color:#888;"></div><div id="combat-atk4-label" style="color:#888;"></div>
<div id="combat-boat1-label" style="color:#888;grid-column:1/-1;"></div>
</div>
</div>
</div>
<div id="tab-diplo-panel" style="padding:10px 12px;display:none;overflow-y:auto;flex:1;">
<div style="margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;">
<span style="color:#aaa;font-size:10px;">Status:</span>
<span id="blon-diplo-status" style="color:#00ff66;font-weight:bold;font-size:10px;">Ready</span>
</div>
<label style="display:flex;align-items:center;gap:7px;cursor:pointer;color:#aaa;margin-bottom:8px;">
<input type="checkbox" id="blon-auto-accept" style="cursor:pointer;margin:0;">
<span style="font-size:11px;">Auto-Accept Incoming Alliances</span>
</label>
<div style="border-top:1px solid #222;padding-top:8px;margin-top:8px;">
<div style="color:#aaa;font-size:10px;margin-bottom:4px;">Quick Chat:</div>
<select id="blon-qchat-target" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 4px;font-family:monospace;font-size:10px;border-radius:2px;margin-bottom:5px;box-sizing:border-box;">
<option value="enemies" selected>Send to enemies only</option>
<option value="allies">Send to allies only</option>
<option value="everyone">Send to everyone</option>
</select>
<select id="blon-qchat-select" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 4px;font-family:monospace;font-size:10px;border-radius:2px;margin-bottom:5px;box-sizing:border-box;"></select>
<button id="blon-qchat-btn" style="width:100%;background:#111;color:#aaa;border:1px solid #444;padding:6px;border-radius:4px;font-weight:bold;font-size:11px;cursor:pointer;font-family:monospace;transition:background 0.15s;box-sizing:border-box;">
SEND QUICK CHAT
</button>
</div>
</div>
<div id="tab-embargo-panel" style="padding:12px;display:none;overflow-y:auto;flex:1;">
<div style="margin-bottom:10px;display:flex;justify-content:space-between;align-items:center;">
<span style="color:#aaa;">Status:</span>
<span id="embargo-status-text" style="color:#00ff66;font-weight:bold;">Ready</span>
</div>
<button id="embargo-fire-btn" style="width:100%;background:#111;color:#aaa;border:1px solid #444;padding:6px;border-radius:4px;font-weight:bold;font-size:11px;cursor:pointer;margin-bottom:6px;font-family:monospace;transition:background 0.15s;box-sizing:border-box;">
EMBARGO ALL [<span id="label-embargo_fire">${fmtActionKey('embargo_fire')}</span>]
</button>
<button id="embargo-lift-btn" style="width:100%;background:#111;color:#aaa;border:1px solid #444;padding:6px;border-radius:4px;font-weight:bold;font-size:11px;cursor:pointer;margin-bottom:10px;font-family:monospace;transition:background 0.15s;box-sizing:border-box;">
LIFT ALL EMBARGOES [<span id="label-embargo_lift">${fmtActionKey('embargo_lift')}</span>]
</button>
<div style="border-top:1px solid #222;padding-top:8px;margin-top:8px;">
<div style="color:#aaa;font-size:10px;margin-bottom:4px;">Donation hotkeys target the hovered ally:</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:3px;font-size:10px;margin-bottom:8px;">
<div id="trade-troops1-label" style="color:#888;"></div><div id="trade-gold1-label" style="color:#888;"></div>
<div id="trade-troops2-label" style="color:#888;"></div><div id="trade-gold2-label" style="color:#888;"></div>
<div id="trade-troops3-label" style="color:#888;"></div><div id="trade-gold3-label" style="color:#888;"></div>
</div>
</div>
<div style="border-top:1px solid #222;padding-top:8px;">
<label style="display:flex;align-items:center;gap:7px;cursor:pointer;color:#aaa;">
<input type="checkbox" id="embargo-auto-repeat" style="cursor:pointer;margin:0;">
<span>Auto-Repeat after cooldown</span>
</label>
</div>
</div>
<div id="tab-cfg-panel" style="padding:10px 12px;display:none;overflow-y:auto;flex:1;">
<div style="margin-bottom:8px;">
<div style="color:#aaa;margin-bottom:3px;">Spam Interval (ms):</div>
<input type="number" id="cfg-spam-rate" value="${cfg.spamInterval}" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
</div>
<div style="margin-bottom:10px;">
<div style="color:#aaa;margin-bottom:3px;">Charge Time (ms):</div>
<input type="number" id="cfg-charge-delay" value="${cfg.holdDelay}" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
</div>
<div style="color:#00ff66;font-weight:bold;margin:10px 0 6px;border-top:1px solid #222;padding-top:7px;">Combat Hotkey Percentages</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px;">
<div>
<span style="color:#aaa;">Hotkey 1 (%):</span>
<input type="number" id="cfg-combat-pct-1" value="${cfg.combatPercentages.atk_1}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
</div>
<div>
<span style="color:#aaa;">Hotkey 2 (%):</span>
<input type="number" id="cfg-combat-pct-2" value="${cfg.combatPercentages.atk_2}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
</div>
<div>
<span style="color:#aaa;">Hotkey 3 (%):</span>
<input type="number" id="cfg-combat-pct-3" value="${cfg.combatPercentages.atk_3}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
</div>
<div>
<span style="color:#aaa;">Hotkey 4 (%):</span>
<input type="number" id="cfg-combat-pct-4" value="${cfg.combatPercentages.atk_4}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
</div>
</div>
<div style="color:#00ff66;font-weight:bold;margin-bottom:6px;border-top:1px solid #222;padding-top:7px;">Key Bindings (click to rebind)</div>
<div id="blon-bind-matrix" style="display:grid;grid-template-columns:1fr 1fr;gap:4px;"></div>
<div style="color:#00ff66;font-weight:bold;margin:10px 0 6px;border-top:1px solid #222;padding-top:7px;">Trade Hotkey Percentages</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px;">
<div>
<span style="color:#aaa;">Troops 1 (%):</span>
<input type="number" id="cfg-trade-pct-troops-1" value="${cfg.tradePercentages.donate_troops_1}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
</div>
<div>
<span style="color:#aaa;">Money 1 (%):</span>
<input type="number" id="cfg-trade-pct-gold-1" value="${cfg.tradePercentages.donate_gold_1}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
</div>
<div>
<span style="color:#aaa;">Troops 2 (%):</span>
<input type="number" id="cfg-trade-pct-troops-2" value="${cfg.tradePercentages.donate_troops_2}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
</div>
<div>
<span style="color:#aaa;">Money 2 (%):</span>
<input type="number" id="cfg-trade-pct-gold-2" value="${cfg.tradePercentages.donate_gold_2}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
</div>
<div>
<span style="color:#aaa;">Troops 3 (%):</span>
<input type="number" id="cfg-trade-pct-troops-3" value="${cfg.tradePercentages.donate_troops_3}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
</div>
<div>
<span style="color:#aaa;">Money 3 (%):</span>
<input type="number" id="cfg-trade-pct-gold-3" value="${cfg.tradePercentages.donate_gold_3}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
</div>
<div>
<span style="color:#aaa;">Troops 4 (%):</span>
<input type="number" id="cfg-trade-pct-troops-4" value="${cfg.tradePercentages.donate_troops_4}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
</div>
<div>
<span style="color:#aaa;">Money 4 (%):</span>
<input type="number" id="cfg-trade-pct-gold-4" value="${cfg.tradePercentages.donate_gold_4}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
</div>
</div>
<div style="color:#00ff66;font-weight:bold;margin:10px 0 6px;border-top:1px solid #222;padding-top:7px;">Action Hotkeys (click to rebind)</div>
<div style="font-size:10px;color:#555;margin-bottom:6px;">Mod keys: Alt, Ctrl, Shift</div>
<div id="blon-action-bind-matrix" style="display:grid;grid-template-columns:1fr 1fr;gap:4px;"></div>
<div style="color:#00ff66;font-weight:bold;margin:10px 0 6px;border-top:1px solid #222;padding-top:7px;">Slot to Unit Mapping (websocket spam only)</div>
<div id="blon-slot-matrix" style="display:flex;flex-direction:column;gap:3px;"></div>
<div style="color:#00ff66;font-weight:bold;margin:14px 0 6px;border-top:1px solid #222;padding-top:10px;">GUI & Overlay Appearance</div>
<div style="display:grid;grid-template-columns:1fr;gap:8px;margin-top:6px;">
<div>
<div style="color:#aaa;margin-bottom:3px;font-size:10px;">GUI background opacity (0.1 - 1):</div>
<input type="range" id="cfg-gui-opacity" min="0.1" max="1" step="0.01" value="${cfg.guiOpacity}" style="width:100%;">
<div style="display:flex;justify-content:space-between;gap:10px;font-size:10px;color:#888;">
<span>0.10</span><span id="cfg-gui-opacity-val" style="color:#ffcc00;font-weight:bold;">${cfg.guiOpacity.toFixed(2)}</span><span>1.00</span>
</div>
</div>
<div style="display:none;">
<div style="color:#aaa;margin-bottom:3px;font-size:10px;">GUI accent color:</div>
<div style="display:flex;gap:6px;align-items:center;">
<input type="color" id="cfg-gui-color-picker" value="${cfg.guiColor}" style="width:34px;height:28px;border:none;padding:0;background:#111;border-radius:4px;cursor:pointer;">
<input type="text" id="cfg-gui-color" value="${cfg.guiColor}" style="flex:1;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
</div>
<div style="color:#aaa;margin:6px 0 3px;font-size:10px;">Accent hue:</div>
<input type="range" id="cfg-gui-color-hue" min="0" max="360" step="1" value="${cfg.guiColorHue}" style="width:100%;">
<div style="display:flex;justify-content:space-between;gap:10px;font-size:10px;color:#888;">
<span>0</span><span id="cfg-gui-color-hue-val" style="color:#ffcc00;font-weight:bold;">${cfg.guiColorHue}</span><span>360</span>
</div>
</div>
<div>
<div style="color:#aaa;margin-bottom:3px;font-size:10px;">Overlay opacity (0.1 - 1):</div>
<input type="range" id="cfg-overlay-opacity" min="0.1" max="1" step="0.01" value="${cfg.overlayOpacity}" style="width:100%;">
<div style="display:flex;justify-content:space-between;gap:10px;font-size:10px;color:#888;">
<span>0.10</span><span id="cfg-overlay-opacity-val" style="color:#ffcc00;font-weight:bold;">${cfg.overlayOpacity.toFixed(2)}</span><span>1.00</span>
</div>
</div>
<div style="display:none;">
<div style="color:#aaa;margin-bottom:3px;font-size:10px;">Overlay color:</div>
<div style="display:flex;gap:6px;align-items:center;">
<input type="color" id="cfg-overlay-color-picker" value="${cfg.overlayColor}" style="width:34px;height:28px;border:none;padding:0;background:#111;border-radius:4px;cursor:pointer;">
<input type="text" id="cfg-overlay-color" value="${cfg.overlayColor}" style="flex:1;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
</div>
</div>
</div>
</div>
`;
function mountUI() {
document.body.appendChild(ui);
bindUIEvents();
setSiloSubtoggleVisibility();
updateSiloPanel();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', mountUI);
} else {
mountUI();
}
const ACTION_DEFS = [
{ id: 'embargo_fire', label: 'Embargo All' },
{ id: 'embargo_lift', label: 'Lift Embargo' },
{ id: 'donate_troops_1', label: 'Donate Troops 1' },
{ id: 'donate_troops_2', label: 'Donate Troops 2' },
{ id: 'donate_troops_3', label: 'Donate Troops 3' },
{ id: 'donate_troops_4', label: 'Donate Troops 4' },
{ id: 'donate_gold_1', label: 'Donate Money 1' },
{ id: 'donate_gold_2', label: 'Donate Money 2' },
{ id: 'donate_gold_3', label: 'Donate Money 3' },
{ id: 'donate_gold_4', label: 'Donate Money 4' }
];
function refreshActionLabels() {
ACTION_DEFS.forEach(def => {
const label = fmtActionKey(def.id);
['label-' + def.id, 'label-' + def.id + '2'].forEach(elId => {
const el = document.getElementById(elId);
if (el) el.textContent = label;
});
});
// refresh combat atk labels
refreshAtkLabels();
refreshTradeLabels();
}
function refreshTradeLabels() {
const slots = [
{ id: 'donate_troops_1', elId: 'trade-troops1-label', label: 'Troops 1' },
{ id: 'donate_troops_2', elId: 'trade-troops2-label', label: 'Troops 2' },
{ id: 'donate_troops_3', elId: 'trade-troops3-label', label: 'Troops 3' },
{ id: 'donate_troops_4', elId: 'trade-troops4-label', label: 'Troops 4' },
{ id: 'donate_gold_1', elId: 'trade-gold1-label', label: 'Money 1' },
{ id: 'donate_gold_2', elId: 'trade-gold2-label', label: 'Money 2' },
{ id: 'donate_gold_3', elId: 'trade-gold3-label', label: 'Money 3' },
{ id: 'donate_gold_4', elId: 'trade-gold4-label', label: 'Money 4' },
];
slots.forEach(s => {
const el = document.getElementById(s.elId);
if (!el) return;
const keyLabel = fmtActionKey(s.id);
const pct = cfg.tradePercentages[s.id] || 0;
el.textContent = `${s.label}: ${keyLabel} = ${pct}%`;
});
}
function refreshAtkLabels() {
const slots = [
{ id: 'atk_1', elId: 'combat-atk1-label' },
{ id: 'atk_2', elId: 'combat-atk2-label' },
{ id: 'atk_3', elId: 'combat-atk3-label' },
{ id: 'atk_4', elId: 'combat-atk4-label' },
{ id: 'boat_1', elId: 'combat-boat1-label' },
];
slots.forEach(s => {
const boundKey = Object.keys(cfg.hotkeyMap).find(k => cfg.hotkeyMap[k] === s.id) || 'NONE';
const el = document.getElementById(s.elId);
const pct = cfg.combatPercentages[s.id] || 0;
if (el) el.textContent = s.id === 'boat_1'
? boundKey.toUpperCase() + ' = 1% boat'
: boundKey.toUpperCase() + ' = ' + pct + '%';
});
}
function buildActionBindMatrix() {
const container = document.getElementById('blon-action-bind-matrix');
if (!container) return;
container.innerHTML = '';
ACTION_DEFS.forEach(def => {
const binding = cfg.actionHotkeyMap[def.id] || { mod: '', key: '' };
const box = document.createElement('div');
box.style.cssText = 'background:#111;border:1px solid #333;padding:4px 6px;border-radius:2px;cursor:pointer;text-align:center;';
const keyLabel = fmtActionKey(def.id);
const pctLabel = cfg.tradePercentages && cfg.tradePercentages[def.id]
? ` (${cfg.tradePercentages[def.id]}%)`
: '';
box.innerHTML = `<span style="color:#888">${def.label}${pctLabel}:</span> <strong id="action-bind-${def.id}" style="color:#fff">${keyLabel}</strong>`;
box.addEventListener('click', () => {
if (internalRebindAction === def.id) { internalRebindAction = null; buildActionBindMatrix(); return; }
internalRebindAction = def.id;
internalRebindSlot = null;
const strong = document.getElementById(`action-bind-${def.id}`);
if (strong) { strong.textContent = 'PRESS KEY'; strong.style.color = '#ffaa00'; }
});
container.appendChild(box);
});
}
function bindUIEvents() {
const ghLink = document.getElementById('blon-github');
ghLink.addEventListener('mousedown', e => e.stopPropagation());
ghLink.addEventListener('mouseenter', () => ghLink.style.color = '#fff');
ghLink.addEventListener('mouseleave', () => ghLink.style.color = '#00ff66');
const allTabBtns = ['tab-main2-btn','tab-main-btn','tab-stats-btn','tab-matches-btn','tab-combat-btn','tab-diplo-btn','tab-embargo-btn','tab-cfg-btn'];
const allTabPanels = ['tab-main2-panel','tab-main-panel','tab-stats-panel','tab-matches-panel','tab-combat-panel','tab-diplo-panel','tab-embargo-panel','tab-cfg-panel'];
const blonRoot = document.getElementById('blon-root');
const blonContentElements = Array.from(document.querySelectorAll('#blon-root > div:not(#blon-drag)'));
const minimizeBtn = document.getElementById('blon-minimize-btn');
if (minimizeBtn && blonRoot) {
minimizeBtn.dataset.minimized = 'false';
minimizeBtn.dataset.savedHeight = blonRoot.style.height || '';
minimizeBtn.dataset.savedMinHeight = blonRoot.style.minHeight || '';
minimizeBtn.addEventListener('mousedown', e => e.stopPropagation());
minimizeBtn.addEventListener('click', () => {
const isMinimized = minimizeBtn.dataset.minimized === 'true';
if (isMinimized) {
blonRoot.style.minHeight = minimizeBtn.dataset.savedMinHeight || '';
blonRoot.style.height = minimizeBtn.dataset.savedHeight || '';
blonContentElements.forEach(el => {
el.style.display = el.dataset.origDisplay || 'flex';
});
minimizeBtn.textContent = '▾';
minimizeBtn.dataset.minimized = 'false';
} else {
minimizeBtn.dataset.savedHeight = blonRoot.style.height || '';
minimizeBtn.dataset.savedMinHeight = blonRoot.style.minHeight || '';
const drag = document.getElementById('blon-drag');
const headerHeight = drag ? `${Math.ceil(drag.getBoundingClientRect().height)}px` : '36px';
blonRoot.style.minHeight = '0px';
blonRoot.style.height = headerHeight;
blonContentElements.forEach(el => {
el.dataset.origDisplay = el.style.display || window.getComputedStyle(el).display;
el.style.display = 'none';
});
minimizeBtn.textContent = '▴';
minimizeBtn.dataset.minimized = 'true';
}
});
}
const tabMap = {
'tab-main2-btn': 'tab-main2-panel',
'tab-main-btn': 'tab-main-panel',
'tab-stats-btn': 'tab-stats-panel',
'tab-matches-btn': 'tab-matches-panel',
'tab-combat-btn': 'tab-combat-panel',
'tab-diplo-btn': 'tab-diplo-panel',
'tab-embargo-btn': 'tab-embargo-panel',
'tab-cfg-btn': 'tab-cfg-panel',
};
allTabBtns.forEach(btnId => {
document.getElementById(btnId).addEventListener('click', () => {
const showPanel = tabMap[btnId];
allTabPanels.forEach(p => {
const el = document.getElementById(p);
el.style.display = (p === showPanel) ? 'flex' : 'none';
el.style.flexDirection = 'column';
});
allTabBtns.forEach(id => {
const btn = document.getElementById(id);
if (id === btnId) {
btn.style.background = '#000'; btn.style.color = '#fff'; btn.style.fontWeight = 'bold';
} else {
btn.style.background = 'transparent'; btn.style.color = '#888'; btn.style.fontWeight = 'normal';
}
});
if (btnId === 'tab-cfg-btn') { buildBindMatrix(); buildSlotMatrix(); buildActionBindMatrix(); }
if (btnId === 'tab-combat-btn') { refreshAtkLabels(); }
if (btnId === 'tab-embargo-btn') { refreshTradeLabels(); }
if (btnId === 'tab-main2-btn') { }
if (btnId === 'tab-matches-btn') { renderLobbyMatches(); updateMatchFeedButton(); }
if (btnId !== 'tab-cfg-btn') { internalRebindSlot = null; internalRebindAction = null; }
});
});
// diplomacy tab
document.getElementById('blon-auto-accept').addEventListener('change', e => {
setAutoAccept(e.target.checked);
});
// quick chat select finally got this trash FULLY working
const qchatTargetSel = document.getElementById('blon-qchat-target');
const qchatSel = document.getElementById('blon-qchat-select');
QUICK_CHAT_KEYS.forEach(k => {
const opt = document.createElement('option');
opt.value = k;
opt.textContent = k.replace('.', ': ').replace(/_/g, ' ');
qchatSel.appendChild(opt);
});
const qchatBtn = document.getElementById('blon-qchat-btn');
qchatBtn.addEventListener('mouseenter', () => qchatBtn.style.background = '#1a1a1a');
qchatBtn.addEventListener('mouseleave', () => qchatBtn.style.background = '#111');
qchatBtn.addEventListener('click', () => {
if (cfg.features && cfg.features.quickChat === false) return;
sendQuickChat(qchatSel.value, qchatTargetSel.value);
});
document.getElementById('blon-toggle-mode').addEventListener('change', e => {
cfg.toggleMode = e.target.checked;
saveCfg();
const hint = document.getElementById('blon-mode-hint');
if (hint) hint.textContent = cfg.toggleMode
? 'Press hotkey once to start spamming, press again to stop.'
: 'Hold hotkey over map to spam build packets.';
});
const goldOverlayToggle = document.getElementById('blon-toggle-gold-overlay');
if (goldOverlayToggle) {
goldOverlayToggle.addEventListener('change', e => {
cfg.showGoldOverlay = e.target.checked;
saveCfg();
updateMainGoldOverlayVisibility();
updateStatsHUD();
});
}
const troopsOverlayToggle = document.getElementById('blon-toggle-troops-overlay');
if (troopsOverlayToggle) {
troopsOverlayToggle.addEventListener('change', e => {
cfg.showTroopsOverlay = e.target.checked;
saveCfg();
patchLeaderboardDOM(document.querySelector('leader-board'));
});
}
const goldRateSlider = document.getElementById('blon-gold-rate-window');
const goldRateValue = document.getElementById('blon-gold-rate-window-value');
const goldTopCountSlider = document.getElementById('blon-gold-top-count');
const goldTopCountValue = document.getElementById('blon-gold-top-count-value');
if (goldRateSlider) {
goldRateSlider.addEventListener('input', e => {
const sec = parseInt(e.target.value);
if (!isNaN(sec)) {
cfg.goldRateWindowSeconds = Math.max(5, Math.min(240, sec));
if (goldRateValue) goldRateValue.textContent = `${cfg.goldRateWindowSeconds}s`;
saveCfg();
}
});
}
if (goldTopCountSlider) {
goldTopCountSlider.addEventListener('input', e => {
const count = parseInt(e.target.value);
if (!isNaN(count)) {
cfg.goldTopCount = Math.max(1, Math.min(20, count));
if (goldTopCountValue) goldTopCountValue.textContent = `${cfg.goldTopCount}`;
saveCfg();
updateStatsHUD();
}
});
}
const troopsIntervalSlider = document.getElementById('blon-troops-check-interval');
const troopsIntervalValue = document.getElementById('blon-troops-check-interval-value');
if (troopsIntervalSlider) {
troopsIntervalSlider.addEventListener('input', e => {
const ms = parseInt(e.target.value);
if (!isNaN(ms)) {
cfg.troopsCheckIntervalMs = Math.max(1, Math.min(1000, ms));
if (troopsIntervalValue) troopsIntervalValue.textContent = `${cfg.troopsCheckIntervalMs}ms`;
saveCfg();
startLeaderboardLoop();
}
});
}
const popoutGoldBtn = document.getElementById('blon-popout-gold-btn');
if (popoutGoldBtn) {
popoutGoldBtn.addEventListener('click', () => {
const panel = createDraggablePopoutPanel('blon-popout-gold', 'Gold Leaderboard');
const content = document.getElementById('blon-popout-gold-content');
if (content) {
content.innerHTML = `
<div style="color:#aaa;font-size:10px;margin-bottom:6px;">Top ${cfg.goldTopCount || 10} players by gold</div>
<div id="blon-popout-gold-top10" style="display:grid;grid-template-columns:auto auto 1fr;gap:3px 8px;margin-bottom:10px;"></div>
<div style="color:#aaa;font-size:10px;margin-bottom:6px;">Team gold totals</div>
<div id="blon-popout-team-gold" style="display:grid;grid-template-columns:auto 1fr;gap:3px 8px;"></div>
`;
updateStatsHUD();
updateMainGoldOverlayVisibility();
}
});
}
const popoutMpsBtn = document.getElementById('blon-popout-mps-btn');
if (popoutMpsBtn) {
popoutMpsBtn.addEventListener('click', () => {
const panel = createDraggablePopoutPanel('blon-popout-mps', 'Gold Income/sec');
const content = document.getElementById('blon-popout-mps-content');
if (content) {
content.innerHTML = `
<div style="display:grid;grid-template-columns:auto 1fr;gap:4px 8px;font-size:11px;margin-bottom:10px;">
<div style="color:#888;">Gold</div><div id="blon-popout-gold-value" style="color:#ffcc00;font-weight:bold;">N/A</div>
<div style="color:#888;">Income/sec</div><div id="blon-popout-gold-rate" style="color:#ffcc00;font-weight:bold;">N/A</div>
</div>
<div style="color:#aaa;font-size:10px;margin-bottom:4px;">Top ${cfg.goldTopCount || 10} players by income/sec</div>
<div id="blon-popout-mps-top10" style="display:grid;grid-template-columns:auto auto 1fr;gap:3px 8px;"></div>
`;
updateStatsHUD();
updateMainGoldOverlayVisibility();
}
});
}
const matchesConnectBtn = document.getElementById('blon-matches-connect-btn');
if (matchesConnectBtn) {
matchesConnectBtn.addEventListener('click', () => {
if (lobbyWS) {
disconnectLobbyFeed();
} else {
connectLobbyFeed();
}
});
}
const matchesRefreshBtn = document.getElementById('blon-matches-refresh-btn');
if (matchesRefreshBtn) {
matchesRefreshBtn.addEventListener('click', renderLobbyMatches);
}
updateMatchFeedButton();
connectLobbyFeed();
function updateMethodButtons() {
const wsBtn = document.getElementById('blon-method-ws');
const clBtn = document.getElementById('blon-method-click');
if (!wsBtn || !clBtn) return;
wsBtn.style.background = cfg.spamMethod === 'websocket' ? '#00ff66' : '#111';
wsBtn.style.color = cfg.spamMethod === 'websocket' ? '#000' : '#aaa';
wsBtn.style.fontWeight = cfg.spamMethod === 'websocket' ? 'bold' : 'normal';
clBtn.style.background = cfg.spamMethod === 'click' ? '#ff9900' : '#111';
clBtn.style.color = cfg.spamMethod === 'click' ? '#000' : '#aaa';
clBtn.style.fontWeight = cfg.spamMethod === 'click' ? 'bold' : 'normal';
}
document.getElementById('blon-method-ws').addEventListener('click', () => {
cfg.spamMethod = 'websocket'; saveCfg(); updateMethodButtons();
});
document.getElementById('blon-method-click').addEventListener('click', () => {
cfg.spamMethod = 'click'; saveCfg(); updateMethodButtons();
});
document.getElementById('blon-toggle-charge').addEventListener('change', e => {
cfg.useChargeTime = e.target.checked; saveCfg();
});
document.getElementById('blon-toggle-passthrough').addEventListener('change', e => {
cfg.blockPassThrough = e.target.checked; saveCfg();
});
const featureIds = [
['blon-feat-spam-hotkeys', 'spamHotkeys', true],
['blon-feat-combat-hotkeys', 'combatHotkeys', true],
['blon-feat-action-hotkeys', 'actionHotkeys', true],
['blon-feat-quick-chat', 'quickChat', true],
['blon-feat-embargo', 'embargo', true],
['blon-feat-overlays', 'overlays', true],
['cfg-missile-predictor', 'missilePredictor', true],
];
featureIds.forEach(([domId, featKey, defaultVal]) => {
const el = document.getElementById(domId);
if (!el) return;
if (typeof cfg.features?.[featKey] !== 'boolean') cfg.features = cfg.features || {};
el.checked = cfg.features?.[featKey] ?? defaultVal;
el.addEventListener('change', ev => {
cfg.features[featKey] = ev.target.checked;
saveCfg();
if (!cfg.features.spamHotkeys) stopSpam();
if (!cfg.features.overlays) updateMainGoldOverlayVisibility();
});
});
document.getElementById('cfg-combat-silo-indicator').addEventListener('change', e => {
cfg.combatSiloIndicator = e.target.checked; saveCfg(); updateSiloNotification(); setSiloSubtoggleVisibility(); updateSiloPanel();
});
document.getElementById('cfg-combat-silo-panel').addEventListener('change', e => {
cfg.combatSiloPanel = e.target.checked; saveCfg(); updateSiloPanel();
});
document.getElementById('cfg-combat-silo-allies').addEventListener('change', e => {
cfg.combatSiloOnlyAllies = e.target.checked; saveCfg(); updateSiloPanel();
});
document.getElementById('cfg-combat-silo-keep-all').addEventListener('change', e => {
cfg.combatSiloKeepAllPlaced = e.target.checked; saveCfg(); updateSiloPanel();
});
document.getElementById('cfg-combat-priority').addEventListener('change', e => {
cfg.combatHotkeysPriority = e.target.checked; saveCfg();
});
const siloNotificationEl = document.getElementById('blon-silo-notification');
if (siloNotificationEl) {
siloNotificationEl.addEventListener('click', onSiloNotificationClick);
}
document.getElementById('cfg-spam-rate').addEventListener('input', e => {
const v = parseInt(e.target.value);
if (!isNaN(v) && v >= 5) { cfg.spamInterval = v; saveCfg(); }
});
document.getElementById('cfg-charge-delay').addEventListener('input', e => {
const v = parseInt(e.target.value);
if (!isNaN(v) && v >= 0) { cfg.holdDelay = v; saveCfg(); }
});
document.getElementById('cfg-combat-pct-1').addEventListener('input', e => {
const v = parseInt(e.target.value);
if (!isNaN(v) && v >= 1 && v <= 100) { cfg.combatPercentages.atk_1 = v; saveCfg(); refreshAtkLabels(); buildBindMatrix(); }
});
document.getElementById('cfg-combat-pct-2').addEventListener('input', e => {
const v = parseInt(e.target.value);
if (!isNaN(v) && v >= 1 && v <= 100) { cfg.combatPercentages.atk_2 = v; saveCfg(); refreshAtkLabels(); buildBindMatrix(); }
});
document.getElementById('cfg-combat-pct-3').addEventListener('input', e => {
const v = parseInt(e.target.value);
if (!isNaN(v) && v >= 1 && v <= 100) { cfg.combatPercentages.atk_3 = v; saveCfg(); refreshAtkLabels(); buildBindMatrix(); }
});
document.getElementById('cfg-combat-pct-4').addEventListener('input', e => {
const v = parseInt(e.target.value);
if (!isNaN(v) && v >= 1 && v <= 100) { cfg.combatPercentages.atk_4 = v; saveCfg(); refreshAtkLabels(); buildBindMatrix(); }
});
[
['cfg-trade-pct-troops-1', 'donate_troops_1'],
['cfg-trade-pct-troops-2', 'donate_troops_2'],
['cfg-trade-pct-troops-3', 'donate_troops_3'],
['cfg-trade-pct-troops-4', 'donate_troops_4'],
['cfg-trade-pct-gold-1', 'donate_gold_1'],
['cfg-trade-pct-gold-2', 'donate_gold_2'],
['cfg-trade-pct-gold-3', 'donate_gold_3'],
['cfg-trade-pct-gold-4', 'donate_gold_4'],
].forEach(([domId, pctId]) => {
const input = document.getElementById(domId);
if (!input) return;
input.addEventListener('input', e => {
const v = parseInt(e.target.value);
if (!isNaN(v) && v >= 1 && v <= 100) {
cfg.tradePercentages[pctId] = v;
saveCfg();
refreshTradeLabels();
buildActionBindMatrix();
}
});
});
const fireBtn = document.getElementById('embargo-fire-btn');
const liftBtn = document.getElementById('embargo-lift-btn');
fireBtn.addEventListener('mouseenter', () => { if (!embargoOnCooldown) fireBtn.style.background = '#1a1a1a'; });
fireBtn.addEventListener('mouseleave', () => { if (!embargoOnCooldown) fireBtn.style.background = '#111'; });
fireBtn.addEventListener('click', () => {
if (cfg.features && cfg.features.embargo === false) return;
fireEmbargoAll();
});
liftBtn.addEventListener('mouseenter', () => liftBtn.style.background = '#1a1a1a');
liftBtn.addEventListener('mouseleave', () => liftBtn.style.background = '#111');
liftBtn.addEventListener('click', liftEmbargoAll);
document.getElementById('embargo-auto-repeat').addEventListener('change', e => {
embargoAutoRepeat = e.target.checked;
});
const dragHandle = document.getElementById('blon-drag');
let dragging = false, ox, oy;
dragHandle.addEventListener('mousedown', e => {
dragging = true;
ox = e.clientX - ui.getBoundingClientRect().left;
oy = e.clientY - ui.getBoundingClientRect().top;
dragHandle.style.color = '#00ffcc';
});
window.addEventListener('mousemove', e => {
if (!dragging) return;
ui.style.left = (e.clientX - ox) + 'px';
ui.style.top = (e.clientY - oy) + 'px';
ui.style.right = 'auto';
});
window.addEventListener('mouseup', () => {
dragging = false;
dragHandle.style.color = '#00ff66';
});
// lbl refresh
refreshAtkLabels();
refreshTradeLabels();
const guiOpacity = document.getElementById('cfg-gui-opacity');
const guiOpacityVal = document.getElementById('cfg-gui-opacity-val');
if (guiOpacity) {
guiOpacity.addEventListener('input', e => {
const v = parseFloat(e.target.value);
if (!isNaN(v)) {
cfg.guiOpacity = Math.max(0.1, Math.min(1, v));
if (guiOpacityVal) guiOpacityVal.textContent = cfg.guiOpacity.toFixed(2);
saveCfg();
applyUiStyle();
}
});
}
const guiColor = document.getElementById('cfg-gui-color');
const guiColorPicker = document.getElementById('cfg-gui-color-picker');
const guiColorHue = document.getElementById('cfg-gui-color-hue');
const guiColorHueVal = document.getElementById('cfg-gui-color-hue-val');
if (guiColorPicker) {
guiColorPicker.addEventListener('input', e => {
const v = String(e.target.value || '').trim();
const normalized = syncGuiColorFromHex(v);
if (normalized) {
if (guiColor) guiColor.value = normalized;
if (guiColorHue) guiColorHue.value = String(cfg.guiColorHue);
if (guiColorHueVal) guiColorHueVal.textContent = String(cfg.guiColorHue);
saveCfg();
applyUiStyle();
}
});
}
if (guiColor) {
guiColor.addEventListener('input', e => {
const v = String(e.target.value || '').trim();
const normalized = syncGuiColorFromHex(v);
if (normalized) {
if (guiColorPicker) guiColorPicker.value = normalized;
if (guiColorHue) guiColorHue.value = String(cfg.guiColorHue);
if (guiColorHueVal) guiColorHueVal.textContent = String(cfg.guiColorHue);
saveCfg();
applyUiStyle();
}
});
}
if (guiColorHue) {
guiColorHue.addEventListener('input', e => {
const v = parseInt(e.target.value, 10);
if (!isNaN(v)) {
const normalized = syncGuiColorFromHue(v);
if (guiColor) guiColor.value = normalized;
if (guiColorPicker) guiColorPicker.value = normalized;
if (guiColorHueVal) guiColorHueVal.textContent = String(cfg.guiColorHue);
saveCfg();
applyUiStyle();
}
});
}
// disabled for now because i am not rewriting more stuff
const overlayColor = document.getElementById('cfg-overlay-color');
const overlayColorPicker = document.getElementById('cfg-overlay-color-picker');
if (overlayColor) overlayColor.disabled = true;
if (overlayColorPicker) overlayColorPicker.disabled = true;
const overlayOpacity = document.getElementById('cfg-overlay-opacity');
const overlayOpacityVal = document.getElementById('cfg-overlay-opacity-val');
if (overlayOpacity) {
overlayOpacity.addEventListener('input', e => {
const v = parseFloat(e.target.value);
if (!isNaN(v)) {
cfg.overlayOpacity = Math.max(0.1, Math.min(1, v));
if (overlayOpacityVal) overlayOpacityVal.textContent = cfg.overlayOpacity.toFixed(2);
saveCfg();
applyOverlayAppearance();
}
});
}
applyOverlayAppearance();
}
const BINDABLE_ACTIONS = [
{ id: '1', label: 'Slot 1' },
{ id: '2', label: 'Slot 2' },
{ id: '3', label: 'Slot 3' },
{ id: '4', label: 'Slot 4' },
{ id: '5', label: 'Slot 5' },
{ id: '6', label: 'Slot 6' },
{ id: '7', label: 'Slot 7' },
{ id: '8', label: 'Slot 8' },
{ id: '9', label: 'Slot 9' },
{ id: '0', label: 'Slot 0' },
{ id: 'atk_1', label: 'Combat 1' },
{ id: 'atk_2', label: 'Combat 2' },
{ id: 'atk_3', label: 'Combat 3' },
{ id: 'atk_4', label: 'Combat 4' },
{ id: 'boat_1', label: 'Boat 1%' }
];
function buildBindMatrix() {
const container = document.getElementById('blon-bind-matrix');
if (!container) return;
container.innerHTML = '';
BINDABLE_ACTIONS.forEach(action => {
const boundKey = Object.keys(cfg.hotkeyMap).find(k => cfg.hotkeyMap[k] === action.id) || 'NONE';
const box = document.createElement('div');
box.style.cssText = 'background:#111;border:1px solid #333;padding:4px 6px;border-radius:2px;cursor:pointer;text-align:center;';
let displayLabel = action.label;
if (action.id.startsWith('atk_')) {
const pct = cfg.combatPercentages[action.id] || 0;
displayLabel = `Combat ${action.id.replace('atk_', '')} (${pct}%)`;
} else if (action.id === 'boat_1') {
displayLabel = 'Boat (1%)';
}
box.innerHTML = `<span style="color:#888">${displayLabel}:</span> <strong id="bind-slot-${action.id}" style="color:#fff">${boundKey.toUpperCase()}</strong>`;
box.addEventListener('click', () => {
if (internalRebindSlot === action.id) { internalRebindSlot = null; buildBindMatrix(); return; }
internalRebindSlot = action.id;
internalRebindAction = null;
document.getElementById(`bind-slot-${action.id}`).textContent = 'PRESS KEY';
document.getElementById(`bind-slot-${action.id}`).style.color = '#ffaa00';
});
container.appendChild(box);
});
}
function buildSlotMatrix() {
const container = document.getElementById('blon-slot-matrix');
if (!container) return;
container.innerHTML = '';
['1','2','3','4','5','6','7','8','9','0'].forEach(slot => {
const row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:6px;';
row.innerHTML = `
<span style="color:#888;min-width:44px;">Slot ${slot}:</span>
<select id="slot-unit-${slot}" style="flex:1;background:#111;border:1px solid #444;color:#fff;padding:2px 4px;font-family:monospace;font-size:10px;border-radius:2px;">
${Object.entries(UNIT_TYPES).map(([label, val]) =>
`<option value="${val}" ${slotUnitMap[slot] === val ? 'selected' : ''}>${label}</option>`
).join('')}
</select>
`;
const sel = row.querySelector(`#slot-unit-${slot}`);
sel.addEventListener('change', e => {
slotUnitMap[slot] = e.target.value;
saveSlots();
});
container.appendChild(row);
});
}
window.addEventListener('keydown', e => {
if (!internalRebindSlot && !internalRebindAction) return;
e.preventDefault(); e.stopPropagation();
const key = e.key.toLowerCase();
if (key === 'escape') {
if (internalRebindSlot) {
for (const k in cfg.hotkeyMap) {
if (cfg.hotkeyMap[k] === internalRebindSlot) delete cfg.hotkeyMap[k];
}
} else if (internalRebindAction) {
cfg.actionHotkeyMap[internalRebindAction] = { mod: '', key: '' };
}
internalRebindSlot = null; internalRebindAction = null;
saveCfg(); buildBindMatrix(); buildActionBindMatrix(); refreshAtkLabels(); refreshActionLabels();
return;
}
if (['tab','enter'].includes(key)) {
internalRebindSlot = null; internalRebindAction = null;
buildBindMatrix(); buildActionBindMatrix();
return;
}
if (['shift','control','alt','meta'].includes(key)) {
// ignore standalone modifier presses while waiting for a combo key
return;
}
if (internalRebindSlot) {
for (const k in cfg.hotkeyMap) {
if (cfg.hotkeyMap[k] === internalRebindSlot || k === key) delete cfg.hotkeyMap[k];
}
cfg.hotkeyMap[key] = internalRebindSlot;
internalRebindSlot = null;
saveCfg(); buildBindMatrix(); refreshAtkLabels();
} else if (internalRebindAction) {
const mod = e.altKey ? 'alt' : e.ctrlKey ? 'ctrl' : e.shiftKey ? 'shift' : '';
cfg.actionHotkeyMap[internalRebindAction] = { mod, key };
internalRebindAction = null;
saveCfg(); buildActionBindMatrix(); refreshActionLabels();
}
}, true);
window.addEventListener('keydown', e => {
if (internalRebindSlot || internalRebindAction) return;
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
const key = e.key.toLowerCase();
const action = cfg.hotkeyMap[key];
if (typeof action === 'string' && action.startsWith('atk_')) {
if (!cfg.features || !cfg.features.combatHotkeys) return;
// always stop propagation so the game never steals atk_ keys
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
if (e.repeat) return;
const pct = cfg.combatPercentages[action] || 10;
const sliders = document.querySelectorAll('control-panel input[type="range"]');
if (sliders.length > 0) {
sliders.forEach(slider => {
slider.value = String(pct);
slider.valueAsNumber = pct;
slider.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
slider.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
});
updateCombatStatus(`Set attack ratio to ${pct}%`, '#00ff66');
} else {
const panels = document.querySelectorAll('control-panel');
let panelUpdated = false;
panels.forEach(panel => {
try {
const slider = panel.querySelector && panel.querySelector('input[type="range"]');
if (slider) {
slider.value = String(pct);
slider.valueAsNumber = pct;
slider.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
slider.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
panelUpdated = true;
return;
}
if (typeof panel.onAttackRatioChange === 'function') {
panel.onAttackRatioChange(pct / 100);
panelUpdated = true;
return;
}
if (typeof panel.attackRatio !== 'undefined') {
panel.attackRatio = pct / 100;
if (typeof panel.requestUpdate === 'function') panel.requestUpdate();
panelUpdated = true;
}
} catch (err) {
// ignore if component API is different
}
});
if (panelUpdated) {
updateCombatStatus(`Set attack ratio to ${pct}%`, '#00ff66');
} else {
updateCombatStatus('Control panel sliders not found', '#ff4444');
}
}
return;
}
if (action === 'boat_1') {
if (!cfg.features || !cfg.features.combatHotkeys) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
if (e.repeat) return;
sendOnePercentBoat();
return;
}
if (!e.repeat) {
if (!cfg.features || !cfg.features.actionHotkeys) return;
for (const actionId in cfg.actionHotkeyMap) {
const binding = cfg.actionHotkeyMap[actionId];
if (!binding || !binding.key) continue;
const modMatch = (binding.mod === 'alt' && e.altKey) ||
(binding.mod === 'ctrl' && e.ctrlKey) ||
(binding.mod === 'shift' && e.shiftKey) ||
(!binding.mod && !e.altKey && !e.ctrlKey && !e.shiftKey);
if (modMatch && e.key.toLowerCase() === binding.key.toLowerCase()) {
e.preventDefault();
switch(actionId) {
case 'embargo_fire': fireEmbargoAll(); break;
case 'embargo_lift': liftEmbargoAll(); break;
case 'donate_troops_1':
case 'donate_troops_2':
case 'donate_troops_3':
case 'donate_troops_4':
sendDonationToAlly('troops', cfg.tradePercentages[actionId] || 1);
break;
case 'donate_gold_1':
case 'donate_gold_2':
case 'donate_gold_3':
case 'donate_gold_4':
sendDonationToAlly('gold', cfg.tradePercentages[actionId] || 1);
break;
}
return;
}
}
}
if (!action) return;
if (!cfg.features || !cfg.features.spamHotkeys) return;
if (cfg.toggleMode) {
if (e.repeat) return;
if (toggleSpamActive && e.key === currentSpamKey) {
stopSpam();
return;
}
stopSpam();
currentSpamKey = e.key;
toggleSpamActive = true;
const slot = cfg.hotkeyMap[key];
const unitType = slotUnitMap[slot];
if (!unitType) return; // invalid slot, bail out
const statusEl = document.getElementById('blon-spam-status');
if (cfg.useChargeTime && cfg.holdDelay > 0) {
let timeLeft = cfg.holdDelay / 1000;
if (statusEl) { statusEl.textContent = `CHARGING ${unitType} (${timeLeft.toFixed(1)}s)`; statusEl.style.color = '#ffaa00'; }
countdownInterval = setInterval(() => {
timeLeft -= 0.1;
if (statusEl && timeLeft > 0) statusEl.textContent = `CHARGING ${unitType} (${Math.max(0,timeLeft).toFixed(1)}s)`;
if (timeLeft <= 0) clearInterval(countdownInterval);
}, 100);
holdTimeout = setTimeout(() => startSpam(slot), cfg.holdDelay);
} else {
// immediate
startSpam(slot);
}
return;
}
if (isKeyDown && e.key === currentSpamKey) return;
stopSpam();
isKeyDown = true;
currentSpamKey = e.key;
const slot = cfg.hotkeyMap[key];
const unitType = slotUnitMap[slot];
if (!unitType) return; // invalid slot bail
const statusEl = document.getElementById('blon-spam-status');
if (cfg.useChargeTime && cfg.holdDelay > 0) {
let timeLeft = cfg.holdDelay / 1000;
if (statusEl) { statusEl.textContent = `CHARGING ${unitType} (${timeLeft.toFixed(1)}s)`; statusEl.style.color = '#ffaa00'; }
countdownInterval = setInterval(() => {
timeLeft -= 0.1;
if (statusEl && timeLeft > 0) statusEl.textContent = `CHARGING ${unitType} (${Math.max(0,timeLeft).toFixed(1)}s)`;
if (timeLeft <= 0) clearInterval(countdownInterval);
}, 100);
// fires the first packet itself once the charge completes (fix)
holdTimeout = setTimeout(() => startSpam(slot), cfg.holdDelay);
} else {
startSpam(slot);
}
}, true);
window.addEventListener('keyup', e => {
if (cfg.toggleMode) return;
if (!cfg.features || !cfg.features.spamHotkeys) return;
if (e.key === currentSpamKey) {
if (cfg.blockPassThrough) { e.preventDefault(); e.stopPropagation(); }
isKeyDown = false;
stopSpam();
}
}, true);
setInterval(() => {
const el = document.getElementById('blon-tile-display');
if (el) el.textContent = lastKnownTile !== null ? `#${lastKnownTile}` : 'none';
}, 500);
// interval for updating stats maybe lower it prolly wont lag even at like 20ms
setInterval(updateStatsHUD, 500);
startLeaderboardLoop();
// MISSILE PREDICTOR finally working!
(function blonMissilePredictor() {
const NUKE_DATA = {
'Atom Bomb': { inner: 12, outer: 30, color: '#ff9900', label: 'ATOM' },
'Hydrogen Bomb': { inner: 80, outer: 100, color: '#ff0000', label: 'H-BOMB' },
'MIRV Warhead': { inner: 12, outer: 18, color: '#ffff00', label: 'MIRV' },
};
const MISSILE_SPEED = 80; // tiles per second
const MISSILE_TYPES = ['Atom Bomb', 'Hydrogen Bomb', 'MIRV Warhead'];
const trackedMissiles = new Map();
let overlayCanvas = null;
let overlayCtx = null;
let animFrameId = null;
let scanIntervalId = null;
function ensureCanvas() {
if (overlayCanvas) return;
overlayCanvas = document.createElement('canvas');
overlayCanvas.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;pointer-events:none;z-index:999998;';
document.body.appendChild(overlayCanvas);
overlayCtx = overlayCanvas.getContext('2d');
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
}
function resizeCanvas() {
if (!overlayCanvas) return;
overlayCanvas.width = window.innerWidth;
overlayCanvas.height = window.innerHeight;
}
function getOverlay() {
try {
const overlay = document.querySelector('player-info-overlay');
if (overlay && overlay.game && overlay.transform) return overlay;
} catch(e) {}
return null;
}
function scanMissiles() {
const overlay = getOverlay();
if (!overlay) return;
const game = overlay.game;
const now = performance.now();
const seen = new Set();
for (const type of MISSILE_TYPES) {
let units;
try { units = game.units(type); } catch(e) { continue; }
if (!units) continue;
for (const missile of units) {
try {
if (!missile.isActive()) continue;
const id = missile.id();
seen.add(id);
if (!trackedMissiles.has(id)) {
const targetTile = missile.targetTile();
if (!targetTile) continue;
trackedMissiles.set(id, {
id: id,
type: missile.type(),
currentTile: missile.tile(),
targetTile: targetTile,
firstSeen: now,
lastSeen: now,
});
} else {
const record = trackedMissiles.get(id);
record.currentTile = missile.tile();
record.lastSeen = now;
}
} catch(e) {}
}
}
// delete missiles not seen for 3s... (Maybe shorten cuz ofc sams and i dont want hanging overlays??)
for (const [id, record] of trackedMissiles) {
if (!seen.has(id) && now - record.lastSeen > 3000) {
trackedMissiles.delete(id);
}
}
}
function worldToScreen(game, transform, worldX, worldY) {
try {
return transform.worldToScreenCoordinates({ x: worldX, y: worldY });
} catch(e) { return null; }
}
function calculateRadiusPx(transform, worldX, worldY, radius) {
try {
const p1 = transform.worldToScreenCoordinates({ x: worldX, y: worldY });
const p2 = transform.worldToScreenCoordinates({ x: worldX + radius, y: worldY });
return Math.abs(p2.x - p1.x);
} catch(e) {
return radius * (transform.scale || 1);
}
}
function render() {
animFrameId = requestAnimationFrame(render);
if (!cfg.features || !cfg.features.missilePredictor) return;
if (!overlayCtx || !overlayCanvas) return;
const overlay = getOverlay();
if (!overlay) return;
const game = overlay.game;
const transform = overlay.transform;
const ctx = overlayCtx;
const w = overlayCanvas.width;
const h = overlayCanvas.height;
ctx.clearRect(0, 0, w, h);
for (const [id, record] of trackedMissiles) {
const data = NUKE_DATA[record.type];
if (!data) continue;
try {
const targetWorldX = game.x(record.targetTile);
const targetWorldY = game.y(record.targetTile);
const currentWorldX = game.x(record.currentTile);
const currentWorldY = game.y(record.currentTile);
const screen = worldToScreen(game, transform, targetWorldX, targetWorldY);
if (!screen) continue;
const outerRadiusPx = calculateRadiusPx(transform, targetWorldX, targetWorldY, data.outer);
const innerRadiusPx = (data.inner !== data.outer)
? calculateRadiusPx(transform, targetWorldX, targetWorldY, data.inner)
: outerRadiusPx;
// ETA
const dx = targetWorldX - currentWorldX;
const dy = targetWorldY - currentWorldY;
const distance = Math.hypot(dx, dy);
const eta = Math.max(0, distance / MISSILE_SPEED).toFixed(1);
// Trajectory line
const currentScreen = worldToScreen(game, transform, currentWorldX, currentWorldY);
if (currentScreen) {
ctx.beginPath();
ctx.moveTo(currentScreen.x, currentScreen.y);
ctx.lineTo(screen.x, screen.y);
ctx.strokeStyle = data.color;
ctx.globalAlpha = 0.25;
ctx.lineWidth = 2;
ctx.setLineDash([8, 5]);
ctx.stroke();
ctx.setLineDash([]);
ctx.globalAlpha = 1;
// Draw current missile position dot
ctx.beginPath();
ctx.arc(currentScreen.x, currentScreen.y, 4, 0, Math.PI * 2);
ctx.fillStyle = data.color;
ctx.globalAlpha = 0.9;
ctx.fill();
ctx.globalAlpha = 1;
}
const margin = outerRadiusPx + 30;
const onScreen = (
screen.x + margin > 0 && screen.x - margin < w &&
screen.y + margin > 0 && screen.y - margin < h
);
if (onScreen) {
// Outer blast radius
ctx.beginPath();
ctx.arc(screen.x, screen.y, outerRadiusPx, 0, Math.PI * 2);
ctx.fillStyle = data.color;
ctx.globalAlpha = 0.1;
ctx.fill();
ctx.globalAlpha = 0.6;
ctx.strokeStyle = data.color;
ctx.lineWidth = 2;
ctx.stroke();
ctx.globalAlpha = 1;
// Inner blast radius
if (data.inner !== data.outer) {
ctx.beginPath();
ctx.arc(screen.x, screen.y, innerRadiusPx, 0, Math.PI * 2);
ctx.fillStyle = data.color;
ctx.globalAlpha = 0.18;
ctx.fill();
ctx.globalAlpha = 0.8;
ctx.strokeStyle = data.color;
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 3]);
ctx.stroke();
ctx.setLineDash([]);
ctx.globalAlpha = 1;
}
// plus sign thingy
const crossSize = Math.min(outerRadiusPx * 0.3, 12);
ctx.beginPath();
ctx.moveTo(screen.x - crossSize, screen.y);
ctx.lineTo(screen.x + crossSize, screen.y);
ctx.moveTo(screen.x, screen.y - crossSize);
ctx.lineTo(screen.x, screen.y + crossSize);
ctx.strokeStyle = data.color;
ctx.globalAlpha = 0.7;
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.globalAlpha = 1;
// ETA label
ctx.font = 'bold 12px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const labelText = data.label + ' ' + eta + 's';
const textWidth = ctx.measureText(labelText).width;
ctx.fillStyle = 'rgba(0,0,0,0.75)';
const labelY = screen.y - outerRadiusPx - 14;
ctx.fillRect(screen.x - textWidth/2 - 5, labelY - 8, textWidth + 10, 16);
ctx.fillStyle = data.color;
ctx.fillText(labelText, screen.x, labelY);
}
} catch(e) {}
}
}
function start() {
ensureCanvas();
if (!animFrameId) animFrameId = requestAnimationFrame(render);
if (!scanIntervalId) scanIntervalId = setInterval(scanMissiles, 100);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start);
} else {
start();
}
})();
})();