OpenFront.IO UI Enhancer

Enhances the OpenFront.IO game interface with troop ratios, visual indicators, and threat icons for better decision making.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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();
    }
})();