Enhances the OpenFront.IO game interface with troop ratios, visual indicators, and threat icons for better decision making.
// ==UserScript==
// @name OpenFront.IO UI Enhancer
// @version 1.0.2
// @description Enhances the OpenFront.IO game interface with troop ratios, visual indicators, and threat icons for better decision making.
// @author Speedrunner
// @license MIT
// @namespace https://greasyfork.org/
// @match *://*.openfront.io/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @icon https://images4.imagebam.com/94/4a/ff/ME190M0L_o.png
// ==/UserScript==
(function() {
'use strict';
const ATOM_COST = 750000;
const HYDROGEN_COST = 5000000;
const ATOM_ICON = "🚀";
const HYDROGEN_ICON = "💥";
const CURRENT_WEAKNESS_THRESHOLD = 0.1;
const TOTAL_POTENTIAL_THRESHOLD = 0.3;
const DANGER_TRESHOLD = 1.35;
const style = document.createElement('style');
style.textContent = `
.flashing-orange {
color: white !important;
animation: flash 0.2s infinite;
}
@keyframes flash {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.7; }
}
.player-name-span {
color: #000000;
text-shadow: 0 0 0.1em #ffffff;
font-weight: 600;
}
.player-troops {
color: #000000;
text-shadow: 0 0 0.1em #ffffff;
font-weight: 600;
}
.max-troops {
font-size: 0.68em;
color: #000000d0;
text-shadow: 0 0 0.1em #ffffff;
}
.troop-ratio-bar {
width: 30px;
height: 4px;
background-color: rgba(34,34,34,0.5);
position: relative;
margin-top: 0px;
border: 1px solid rgba(68, 68, 68, 0.6);
}
.ratio-fill-bar, .ratio-buffer-bar {
height: 100%;
position: absolute;
}
.ratio-buffer-bar {
background-color: rgba(255, 166, 0, 0.48);
}
`;
document.head.appendChild(style);
let showRatioBar = GM_getValue('showRatioBar', true);
let showTroopRatios = GM_getValue('showTroopRatios', true);
let showWeaknessIndicator = GM_getValue('showWeaknessIndicator', true);
let showDangerIndicator = GM_getValue('showDangerIndicator', true);
let showThreatIcons = GM_getValue('showThreatIcons', true);
GM_registerMenuCommand('Toggle Ratio/Health Bar', () => {
showRatioBar = !showRatioBar;
GM_setValue('showRatioBar', showRatioBar);
const bars = document.querySelectorAll('.troop-ratio-bar');
bars.forEach(bar => {
bar.style.display = showRatioBar ? '' : 'none';
});
});
GM_registerMenuCommand('Toggle Max Troops', () => {
showTroopRatios = !showTroopRatios;
GM_setValue('showTroopRatios', showTroopRatios);
const troops = nameLayerContainer.querySelectorAll('.player-troops');
troops.forEach(troopDiv => updateTroopDisplay(troopDiv));
});
GM_registerMenuCommand('Toggle Weakness Indicator (Flashing Orange)', () => {
showWeaknessIndicator = !showWeaknessIndicator;
GM_setValue('showWeaknessIndicator', showWeaknessIndicator);
const troops = nameLayerContainer.querySelectorAll('.player-troops');
troops.forEach(troopDiv => updateTroopDisplay(troopDiv));
});
GM_registerMenuCommand('Toggle Danger Indicator (Red Troop Count)', () => {
showDangerIndicator = !showDangerIndicator;
GM_setValue('showDangerIndicator', showDangerIndicator);
const troops = nameLayerContainer.querySelectorAll('.player-troops');
troops.forEach(troopDiv => updateTroopDisplay(troopDiv));
});
GM_registerMenuCommand('Toggle Threat Icons', () => {
showThreatIcons = !showThreatIcons;
GM_setValue('showThreatIcons', showThreatIcons);
const troops = nameLayerContainer.querySelectorAll('.player-troops');
troops.forEach(troopDiv => updateTroopDisplay(troopDiv));
});
GM_registerMenuCommand('Show Current Settings', () => {
console.log('Troop Script Settings:');
console.log('Ratio/Health Bar:', showRatioBar ? 'ON' : 'OFF');
console.log('Troop Ratios:', showTroopRatios ? 'ON' : 'OFF');
console.log('Weakness Indicator:', showWeaknessIndicator ? 'ON' : 'OFF');
console.log('Danger Indicator:', showDangerIndicator ? 'ON' : 'OFF');
console.log('Threat Icons:', showThreatIcons ? 'ON' : 'OFF');
alert('Check the browser console (F12) for current settings.');
});
let game = null;
let nameLayerContainer = null;
function waitForGame() {
try {
const leaderboard = document.querySelector('leader-board');
if (leaderboard && leaderboard.game) {
game = leaderboard.game;
findNameLayerContainer();
} else {
setTimeout(waitForGame, 1000);
}
} catch (e) {
console.error('Error in waitForGame:', e);
}
}
function findNameLayerContainer() {
try {
const containers = document.querySelectorAll('div[style*="position: fixed"][style*="left: 50%"][style*="top: 50%"][style*="pointer-events: none"][style*="z-index: 2"]'); // Fragile; update if site layout changes
if (containers.length > 0) {
nameLayerContainer = containers[0];
setupObservers();
} else {
setTimeout(findNameLayerContainer, 1000);
}
} catch (e) {
console.error('Error in findNameLayerContainer:', e);
}
}
function setupObservers() {
try {
const containerObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const troopsDiv = node.querySelector('.player-troops');
if (troopsDiv) {
setupTroopObserver(troopsDiv);
}
}
});
});
});
containerObserver.observe(nameLayerContainer, { childList: true, subtree: true });
const existingTroops = nameLayerContainer.querySelectorAll('.player-troops');
existingTroops.forEach(setupTroopObserver);
} catch (e) {
console.error('Error in setupObservers:', e);
}
}
function setupTroopObserver(troopsDiv) {
const observer = new MutationObserver((mutations) => {
if (troopsDiv.textContent && !troopsDiv.textContent.includes('/')) {
updateTroopDisplay(troopsDiv);
}
});
observer.observe(troopsDiv, { childList: true, characterData: true, subtree: true });
troopsDiv._observer = observer;
}
function updateTroopDisplay(troopsDiv) {
if (!game) return;
const myPlayer = game.myPlayer();
const element = troopsDiv.parentElement;
if (!element) return;
const nameSpan = element.querySelector('.player-name-span');
if (!nameSpan) return;
const playerName = nameSpan.innerHTML.replace(new RegExp(`${ATOM_ICON}|${HYDROGEN_ICON}`, 'g'), "");
const players = game.playerViews();
const player = players.find(p => p.isAlive() && (p.name() === playerName || p.displayName() === playerName));
if (!player) return;
const observer = troopsDiv._observer;
if (observer) observer.disconnect(); // Prevent triggering on our own DOM updates
try {
const currentTroops = player.troops();
const maxTroops = game.config().maxTroops(player);
const attackingTroops = player.outgoingAttacks().reduce((sum, attack) => sum + (attack.retreating ? 0 : attack.troops), 0);
if (isNaN(currentTroops) || isNaN(maxTroops) || isNaN(attackingTroops)) return;
// Troop ratio display
const formattedCurrent = formatTroopCount(currentTroops);
const formattedMax = formatTroopCount(maxTroops);
troopsDiv.innerHTML = showTroopRatios ? `${formattedCurrent}<span class="max-troops">/${formattedMax}</span>` : formattedCurrent;
// Ratio bar
let ratioBar = element.querySelector('.troop-ratio-bar');
let fill, bufferFill;
if (!ratioBar) {
ratioBar = document.createElement('div');
ratioBar.className = 'troop-ratio-bar';
fill = document.createElement('div');
fill.className = 'ratio-fill-bar';
bufferFill = document.createElement('div');
bufferFill.className = 'ratio-buffer-bar';
ratioBar.appendChild(fill);
ratioBar.appendChild(bufferFill);
element.appendChild(ratioBar);
} else {
fill = ratioBar.firstElementChild;
bufferFill = ratioBar.lastElementChild;
}
const isBot = player.type() === 'BOT';
if (!showRatioBar || isBot) {
ratioBar.style.display = 'none';
} else {
ratioBar.style.display = '';
const totalPotential = currentTroops + attackingTroops;
const totalRatio = maxTroops > 0 ? Math.min(totalPotential / maxTroops, 1) : 0;
const mainRatio = attackingTroops === 0 ? 1 : (totalPotential > 0 ? currentTroops / totalPotential : 0);
const bufferRatio = attackingTroops === 0 ? 0 : (totalPotential > 0 ? attackingTroops / totalPotential : 0);
const mainWidth = mainRatio * totalRatio * 100;
const bufferWidth = bufferRatio * totalRatio * 100;
fill.style.width = mainWidth + '%';
fill.style.backgroundColor = 'rgba(0,255,0,0.7)';
bufferFill.style.width = bufferWidth + '%';
bufferFill.style.left = mainWidth + '%';
}
const isTeamMode = game.config().gameConfig().gameMode === 'Team';
// Dynamic danger coloring
if (myPlayer && player.id() !== myPlayer.id()) {
troopsDiv.style.color = 'black';
const myTroops = myPlayer.troops();
if (!isNaN(myTroops) && myTroops > 0) {
const dangerRatio = currentTroops / myTroops; // Does not take into account attackingTroops atm (not sure if neccessary as it might get confusing)
if (showWeaknessIndicator && currentTroops <= CURRENT_WEAKNESS_THRESHOLD * maxTroops && (currentTroops + attackingTroops) < TOTAL_POTENTIAL_THRESHOLD * maxTroops) {
troopsDiv.classList.add('flashing-orange');
} else if (showDangerIndicator && dangerRatio >= DANGER_TRESHOLD) {
troopsDiv.classList.remove('flashing-orange');
if (!isTeamMode || !player.isOnSameTeam(myPlayer)) {
troopsDiv.style.color = 'red';
}
} else {
troopsDiv.classList.remove('flashing-orange');
}
}
} else {
troopsDiv.classList.remove('flashing-orange');
troopsDiv.style.color = '';
}
// Threat indicator for nuke capability
let icon = "";
if (showThreatIcons && (!myPlayer || myPlayer.id() !== player.id())) {
const hasSilo = player.units("Missile Silo").length > 0;
const playerGold = isNaN(Number(player.gold())) ? 0 : Number(player.gold());
if (hasSilo) {
if (playerGold >= HYDROGEN_COST) {
icon = HYDROGEN_ICON;
} else if (playerGold >= ATOM_COST) {
icon = ATOM_ICON;
}
}
}
nameSpan.innerHTML = playerName + (icon ? `<span style="font-size: smaller; opacity: 0.6;">${icon}</span>` : "");
} catch (e) {
console.error('Error in updateTroopDisplay:', e);
}
if (observer) observer.observe(troopsDiv, { childList: true, characterData: true, subtree: true });
}
function formatTroopCount(troops) {
const num = troops / 10;
if (num >= 10000000) return (Math.floor(num / 100000) / 10).toFixed(1) + "M";
if (num >= 1000000) return (Math.floor(num / 10000) / 100).toFixed(2) + "M";
if (num >= 100000) return Math.floor(num / 1000) + "K";
if (num >= 10000) return (Math.floor(num / 100) / 10).toFixed(1) + "K";
if (num >= 1000) return (Math.floor(num / 10) / 100).toFixed(2) + "K";
return Math.floor(num).toString();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', waitForGame);
} else {
waitForGame();
}
})();