AWBW Custom Army Importer - Beta 1.5

[Client Side] Allows users to fetch custom army sprites and use them in game!

// ==UserScript==
// @name         AWBW Custom Army Importer - Beta 1.5
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  [Client Side] Allows users to fetch custom army sprites and use them in game!
// @author       Vesper
// @match        https://awbw.amarriner.com/prevmaps.php*
// @match        https://awbw.amarriner.com/editmap.php*
// @match        https://awbw.amarriner.com/game.php*
// @icon         https://awbw.amarriner.com/terrain/aw1/bluestar.gif
// @grant        none
// @license MIT
// ==/UserScript==
debugger;

function debounce(func, wait) {
    let timeout;
    return function (...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(this, args), wait);
    };
}

function getKeyByValue(obj, value) {
    const entry = Object.entries(obj).find(([key, val]) => val === value);
    return entry ? entry[0] : null; // Return the key or null if not found
}

function extractCountryAndPath(spritePath) {
    let countryDict = {}
    let countries = []
    for (let c of Object.keys(BaseInfo.countries)) {
        let flatname = getFlatName(c);
        if (flatname == undefined) continue;

        countryDict[flatname] = c;
        countries.push(flatname);
    }

    for (let c of countries) {
        if (spritePath.includes(c)) {
            let path = spritePath.slice(spritePath.indexOf(c))
            path = path.replace(c, "")
            let country = countryDict[c];

            return { country, path };
        }
    }

    let match = spritePath.match(/terrain\/ani\/(gs_)?([a-zA-Z]{2})(.*)/);
    if (match) {
        const country = match[2]; // Extracts the 2-letter country code
        const path = match[3]; // Extracts everything after the country code
        return { country, path, gs: spritePath.includes("/ani/gs_")};
    }

    // Case 2: Check for "aw2/movement" paths with the country in the folder name
    match = spritePath.match(/terrain\/aw2\/movement\/([a-zA-Z]{2})\/(.*)/);
    if (match) {
        const country = match[1]; // Extracts the 2-letter country code
        let path = match[2]; // Extracts everything after the country folder

        // Remove the country code prefix from the path (if it starts with the country code)
        if (path.startsWith(country)) {
            path = path.slice(country.length); // Remove the country code prefix
        }

        return { country, path };
    }


    return null; // Return null if the format doesn't match

}

// Country_Code: [githubURL]
let countryReplacementMap = {};
let countriesDisabled = [];
window.countryReplacementMap = countryReplacementMap;
window.countriesDisabled = countriesDisabled;
function isBaseURL(url) {
    for (let baseURL of Object.values(countryReplacementMap)) {
        return url.startsWith(baseURL);
    }
    return false;
}

var BaseInfo = {};
BaseInfo.units = {
    1: {
        name: "Infantry",
        cost: 1e3,
        move_points: 3,
        move_type: "F",
        fuel: 99,
        fuel_per_turn: 0,
        ammo: 0,
        short_range: 0,
        long_range: 0,
        second_weapon: "N"
    },
    2: {
        name: "Mech",
        cost: 3e3,
        move_points: 2,
        move_type: "B",
        fuel: 70,
        fuel_per_turn: 0,
        ammo: 3,
        short_range: 0,
        long_range: 0,
        second_weapon: "Y"
    },
    3: {
        name: "Md.Tank",
        cost: 16e3,
        move_points: 5,
        move_type: "T",
        fuel: 50,
        fuel_per_turn: 0,
        ammo: 8,
        short_range: 0,
        long_range: 0,
        second_weapon: "Y"
    },
    4: {
        name: "Tank",
        cost: 7e3,
        move_points: 6,
        move_type: "T",
        fuel: 70,
        fuel_per_turn: 0,
        ammo: 9,
        short_range: 0,
        long_range: 0,
        second_weapon: "Y"
    },
    5: {
        name: "Recon",
        cost: 4e3,
        move_points: 8,
        move_type: "W",
        fuel: 80,
        fuel_per_turn: 0,
        ammo: 0,
        short_range: 0,
        long_range: 0,
        second_weapon: "N"
    },
    6: {
        name: "APC",
        cost: 5e3,
        move_points: 6,
        move_type: "T",
        fuel: 70,
        fuel_per_turn: 0,
        ammo: 0,
        short_range: 0,
        long_range: 0,
        second_weapon: "N"
    },
    7: {
        name: "Artillery",
        cost: 6e3,
        move_points: 5,
        move_type: "T",
        fuel: 50,
        fuel_per_turn: 0,
        ammo: 9,
        short_range: 2,
        long_range: 3,
        second_weapon: "N"
    },
    8: {
        name: "Rocket",
        cost: 15e3,
        move_points: 5,
        move_type: "W",
        fuel: 50,
        fuel_per_turn: 0,
        ammo: 6,
        short_range: 3,
        long_range: 5,
        second_weapon: "N"
    },
    9: {
        name: "Anti-Air",
        cost: 8e3,
        move_points: 6,
        move_type: "T",
        fuel: 60,
        fuel_per_turn: 0,
        ammo: 9,
        short_range: 0,
        long_range: 0,
        second_weapon: "N"
    },
    10: {
        name: "Missile",
        cost: 12e3,
        move_points: 4,
        move_type: "W",
        fuel: 50,
        fuel_per_turn: 0,
        ammo: 6,
        short_range: 3,
        long_range: 5,
        second_weapon: "N"
    },
    11: {
        name: "Fighter",
        cost: 2e4,
        move_points: 9,
        move_type: "A",
        fuel: 99,
        fuel_per_turn: 5,
        ammo: 9,
        short_range: 0,
        long_range: 0,
        second_weapon: "N"
    },
    12: {
        name: "Bomber",
        cost: 22e3,
        move_points: 7,
        move_type: "A",
        fuel: 99,
        fuel_per_turn: 5,
        ammo: 9,
        short_range: 0,
        long_range: 0,
        second_weapon: "N"
    },
    13: {
        name: "B-Copter",
        cost: 9e3,
        move_points: 6,
        move_type: "A",
        fuel: 99,
        fuel_per_turn: 2,
        ammo: 6,
        short_range: 0,
        long_range: 0,
        second_weapon: "Y"
    },
    14: {
        name: "T-Copter",
        cost: 5e3,
        move_points: 6,
        move_type: "A",
        fuel: 99,
        fuel_per_turn: 2,
        ammo: 0,
        short_range: 0,
        long_range: 0,
        second_weapon: "N"
    },
    15: {
        name: "Battleship",
        cost: 28e3,
        move_points: 5,
        move_type: "S",
        fuel: 99,
        fuel_per_turn: 1,
        ammo: 9,
        short_range: 2,
        long_range: 6,
        second_weapon: "N"
    },
    16: {
        name: "Cruiser",
        cost: 18e3,
        move_points: 6,
        move_type: "S",
        fuel: 99,
        fuel_per_turn: 1,
        ammo: 9,
        short_range: 0,
        long_range: 0,
        second_weapon: "N"
    },
    17: {
        name: "Lander",
        cost: 12e3,
        move_points: 6,
        move_type: "L",
        fuel: 99,
        fuel_per_turn: 1,
        ammo: 0,
        short_range: 0,
        long_range: 0,
        second_weapon: "N"
    },
    18: {
        name: "Sub",
        cost: 2e4,
        move_points: 5,
        move_type: "S",
        fuel: 60,
        fuel_per_turn: 1,
        ammo: 6,
        short_range: 0,
        long_range: 0,
        second_weapon: "N"
    },
    46: {
        name: "Neotank",
        cost: 22e3,
        move_points: 6,
        move_type: "T",
        fuel: 99,
        fuel_per_turn: 1,
        ammo: 9,
        short_range: 0,
        long_range: 0,
        second_weapon: "Y"
    },
    960900: {
        name: "Piperunner",
        cost: 2e4,
        move_points: 9,
        move_type: "P",
        fuel: 99,
        fuel_per_turn: 0,
        ammo: 9,
        short_range: 2,
        long_range: 5,
        second_weapon: "Y"
    },
    968731: {
        name: "Black Bomb",
        cost: 25e3,
        move_points: 9,
        move_type: "A",
        fuel: 45,
        fuel_per_turn: 5,
        ammo: 0,
        short_range: 0,
        long_range: 0,
        second_weapon: "N"
    },
    1141438: {
        name: "Mega Tank",
        cost: 28e3,
        move_points: 4,
        move_type: "T",
        fuel: 50,
        fuel_per_turn: 0,
        ammo: 3,
        short_range: 0,
        long_range: 0,
        second_weapon: "Y"
    },
    28: {
        name: "Black Boat",
        cost: 7500,
        move_points: 7,
        move_type: "L",
        fuel: 60,
        fuel_per_turn: 1,
        ammo: 0,
        short_range: 0,
        long_range: 0,
        second_weapon: "N"
    },
    30: {
        name: "Stealth",
        cost: 24e3,
        move_points: 6,
        move_type: "A",
        fuel: 60,
        fuel_per_turn: 5,
        ammo: 6,
        short_range: 0,
        long_range: 0,
        second_weapon: "N"
    },
    29: {
        name: "Carrier",
        cost: 3e4,
        move_points: 5,
        move_type: "S",
        fuel: 99,
        fuel_per_turn: 1,
        ammo: 9,
        short_range: 3,
        long_range: 8,
        second_weapon: "N"
    }
}
BaseInfo.countries = {
    os: {
        id: 1,
        name: "Orange Star",
        color: "181, 39, 68"
    },
    bm: {
        id: 2,
        name: "Blue Moon",
        color: "70,110,254"
    },
    ge: {
        id: 3,
        name: "Green Earth",
        color: "61, 194, 45"
    },
    yc: {
        id: 4,
        name: "Yellow Comet",
        color: "201, 189, 2"
    },
    bh: {
        id: 5,
        name: "Black Hole",
        color: "116, 89, 138"
    },
    rf: {
        id: 6,
        name: "Red Fire",
        color: "146, 50, 67"
    },
    gs: {
        id: 7,
        name: "Grey Sky",
        color: "114, 114, 114"
    },
    bd: {
        id: 8,
        name: "Brown Desert",
        color: "152, 83, 51"
    },
    ab: {
        id: 9,
        name: "Amber Blaze",
        color: "252, 163, 57"
    },
    js: {
        id: 10,
        name: "Jade Sun",
        color: "166, 182, 153"
    },
    ci: {
        id: 16,
        name: "Cobalt Ice",
        color: "11, 32, 112"
    },
    pc: {
        id: 17,
        name: "Pink Cosmos",
        color: "255, 102, 204"
    },
    tg: {
        id: 19,
        name: "Teal Galaxy",
        color: "60, 205, 193"
    },
    pl: {
        id: 20,
        name: "Purple Lightning",
        color: "111, 26, 155"
    },
    ar: {
        id: 21,
        name: "Acid Rain",
        color: "97, 124, 14"
    },
    wn: {
        id: 22,
        name: "White Nova",
        color: "205, 155, 154"
    },
    aa: {
        id: 23,
        name: "Azure Asteroid",
        color: "130, 220, 232"
    },
    ne: {
        id: 24,
        name: "Noir Eclipse",
        color: "3, 3, 3"
    },
    sc: {
        id: 25,
        name: "Silver Claw",
        color: "137, 168, 188"
    }
}
window.BaseInfo = BaseInfo;
Object.freeze(BaseInfo);

function getFlatName(countryCode) {
    if (BaseInfo.countries[countryCode] == undefined) return undefined
    return BaseInfo.countries[countryCode].name.toLowerCase().replace(" ", "")
}

// MutationObserver to replace sprites
const targetNode = document.querySelector('#gamemap');
if (!targetNode) return;

function initArmyImporter() {
    Vue.component("PrevMapAnalyzer", {
        template:
            `<div id='replay-misc-controls'>
                <div ref='openBtn' class='flex v-center' style='padding: 0px 12px; cursor: pointer; user-select: none;' @click='open = !open'>
                    <img src='terrain/aw1/bluestar.gif'/><b>Custom Armies</b>
                </div>
                <div v-show='open' class='flex col' style='position: absolute; z-index:210; top: 100px; right: 0px;'>
                    <div class='bordertitle flex' style='color: #fff; background: #06c; border: 1px black solid; padding: 4px; justify-content: space-between;'>
                        <div style="font-weight: bold; display: block; float: left;">Custom Army Importer - Beta 1.4</div>
                    <div style="cursor: pointer" @click="open = false">
                    <img width='16' src="terrain/close.png"/>
                </div>
            </div>
            <div style='background: #fff; border: 1px black solid; padding: 4px;'>
                <div class='custom-army-importer-intro' style="background: #7ebeff; border: 1px black solid; padding: 4px; color: #000000; font-size: 12px">
                    <div style="display: flex; flex-direction: column; max-width: 312px; align-items: left; justify-content: left; margin: 0 auto;">
                        <label style='text-align: center; padding-bottom: 8px'><strong>Welcome To The Custom Army Importer!</strong></label>
                        <label style='text-align: left; padding-bottom: 4px'>Here you can set specific countries to any custom army you want.</label>
                        <label style='text-align: left; padding-bottom: 2px'>Each country available on the site can be pointed to a custom github repo or CDN.</label>
                        <label style='text-align: left; padding-bottom: 8px'>Enter the <strong>Folder</strong> that the link points to under any country then press <strong>Enter</strong>.</label>
                        <strong style='text-align: left; padding-bottom: 2px'>Example URL:</strong>
                        <label style='text-align: left; padding-bottom: 2px'>https://raw.githubusercontent.com/ShinyDeagle/My-Custom-Army/refs/heads/main/ab-rework/</label>
                        <label style='text-align: left; padding-bottom: 2px'>Afterwards, you need to press Reload Sprites to import the new sprites.</label>
                        <label style='text-align: left; padding-bottom: 2px'>You'll know if your sprites loaded if you see the custom Infantry and Mech below the url.</label>
                        <strong style='text-align: left; padding-bottom: 2px'>WE ALSO SUPPORT CUSTOM BUILDINGS NOW!</strong>
                        <strong style='text-align: left; padding-bottom: 12px'>Check the Github Readme Plis.</strong>
                        <label style='text-align: left; padding-bottom: 2px'>If the animations seem to <strong>Flicker</strong>...</label>
                        <label style='text-align: left; padding-bottom: 2px'>Reload the page with <strong>CTRL + SHIFT + R</strong> to refresh the browser cache.</label>
                        <label style='text-align: left; padding-bottom: 12px'>Refreshing the page can solve a lot of problems :p</label>
                        <label style='text-align: left; padding-bottom: 2px'>If you want to make your <strong>Own Custom Army</strong>, follow these steps at my <strong>Readme</strong> on this repo.</label>
                        <strong style='text-align: left; padding-bottom: 12px'>https://github.com/ShinyDeagle/My-Custom-Army</strong>
                        <strong style='text-align: left; padding-bottom: 2px'>Have Fun!</strong>
                    </div>
                </div>
                <hr style='width: 312px;'>
                <div v-for='cc in countries' style="display: flex; flex-direction: column; gap: 4px; width: 312px">
                    <div style='width: 100%; display: flex; flex-direction: row; align-items: center; gap: 4px;'>
                        <img
                            style='width: 24px; margin: 1px;'
                            :src='"terrain/aw1/"+ cc +"logo.gif"'
                        >
                        <strong
                            style="font-size: 16px"
                        >
                            {{BaseInfo.countries[cc].name}}
                        </strong>
                        <img
                            style='width: 24px; margin: 1px;'
                            :src='"terrain/ani/"+ cc +"infantry.gif"'
                        >
                        <img
                            style='width: 24px; margin: 1px;'
                            :src='"terrain/ani/"+ cc +"mech.gif"'
                        >
                    </div>
                    <div style='width: 100%; height: 100%;'>
                        <div
                            style="text-align: left; font-size: 10px; margin-left: 12px; margin-right: 32px; border: 1px black solid; padding: 4px"
                            contenteditable="true"
                            @keydown.enter.prevent="updateCountryURL($event, cc)"
                        >
                            {{ countryReplacementMap[cc] || "Enter Github Folder URL" }}
                        </div>
                    </div>
                    <div v-if='countryReplacementMap[cc]'>
                        <div style="display: flex; flex-direction: column">
                            <div style="display: flex; flex-direction: row; margin-top: 8px; align-items: center; justify-content: center; color: #ffffff; gap:16px; font-size: 12px">
                                <div @click='preloadSprites(cc)' style="font-weight: bold; padding: 8px; background: #7ebeff;">
                                    Reload Sprites
                                </div>
                                <div v-show='isDisabled(cc)' @click='disableCountry(cc)' style="font-weight: bold; padding: 8px; background: #ff972b; border: 2px solid black;">
                                    Enable Country
                                </div>
                                <div v-show='!isDisabled(cc)' @click='disableCountry(cc)' style="font-weight: bold; padding: 8px; background: #7ebeff;">
                                    Disable Country
                                </div>
                            </div>
                            <div style='margin-top: 4px; margin-bottom: 0px'>
                                <img
                                    style='width: 24px; margin: 1px;'
                                    :src='countryReplacementMap[cc] + "xxinfantry.gif"'
                                >
                                <img
                                    style='width: 24px; margin: 1px;'
                                    :src='countryReplacementMap[cc] + "xxmech.gif"'
                                >
                                <img
                                    style='width: 24px; margin: 1px;'
                                    :src='countryReplacementMap[cc] + "xxcity.gif"'
                                >
                                <img
                                    style='width: 24px; margin: 1px;'
                                    :src='countryReplacementMap[cc] + "xxhq.gif"'
                                >
                                <img
                                    style='width: 24px; margin: 1px;'
                                    :src='countryReplacementMap[cc] + "xxlab.gif"'
                                >
                            </div>
                        </div>
                    </div>
                    <hr style='width: 312px;'>
                </div>
            </div>
        </div>`,
        props: {
            countries: Array,
        },
        data: function() {
            return {
                open: !1,
                countriesDisabled: [],
                countryReplacementMap: {},
            }
        },
        created() {
            this.BaseInfo = BaseInfo;
            this.countryReplacementMap = countryReplacementMap;
            this.countriesDisabled = countriesDisabled;
            // Load settings for countryReplacementMap from localStorage
            let importerSettings = localStorage.importerSettings;
            if (importerSettings) {
                let data = JSON.parse(importerSettings);
                this.countryReplacementMap = data;
                window.countryReplacementMap = this.countryReplacementMap;
                preloadSprites();
            }

            // Load settings for countriesDisabled from localStorage
            let disabledCountries = localStorage.disabledCountries;
            if (disabledCountries) {
                let data = JSON.parse(disabledCountries);
                this.countriesDisabled = data;
                window.countriesDisabled = this.countriesDisabled;
            }

            // Save settings with debounce to avoid excessive updates
            this.saveSettings = debounce(() => {
                // Save countryReplacementMap to localStorage
                localStorage.importerSettings = JSON.stringify(this.countryReplacementMap);

                // Save countriesDisabled to localStorage
                localStorage.disabledCountries = JSON.stringify(this.countriesDisabled);
            }, 1500);
        },
        methods: {
            preloadSprites(country) {
                window.preloadCountrySprites(country, this.countryReplacementMap);
            },
            isDisabled(country) {
                return this.countriesDisabled.includes(country);
            },
            disableCountry(country) {
                if (this.countriesDisabled.includes(country)) {
                    let index = this.countriesDisabled.indexOf(country);
                    if (index != -1) this.countriesDisabled.splice(index, 1);
                } else {
                    this.countriesDisabled.push(country);
                }
                window.countriesDisabled = this.countriesDisabled;
                this.saveSettings();
            },
            updateCountryURL(event, cc) {
                const newContent = event.target.innerText.trim(); // Get text content
                if (newContent) {
                    // Update the map if the input is not empty
                    this.$set(this.countryReplacementMap, cc, newContent);
                    console.log(`Updated country ${cc}: ${newContent}`);
                } else {
                    if (!this.countriesDisabled.includes(cc)) {
                        this.countriesDisabled.push(cc);
                    }
                    window.checkSrcChanges();

                    // Remove the entry if the input is empty
                    this.$delete(this.countryReplacementMap, cc);

                    // Force Vue to detect reactivity changes
                    this.countryReplacementMap = { ...this.countryReplacementMap };
                    window.countryReplacementMap = this.countryReplacementMap;

                    let index = this.countriesDisabled.indexOf(cc);
                    if (index != -1) this.countriesDisabled.splice(index, 1);

                    console.log(`Deleted country ${cc}`);

                    for (let spriteName of window.unitSpriteMap) {
                        let sprite = spriteName.replace("xx", cc);
                        window.replacementSprites.delete(sprite);
                    }

                    event.target.innerText = "";
                }

                this.saveSettings();
                // Optionally clear the focus after pressing Enter
                event.target.blur();
                window.forceUpdate();
            },
        }
    });

    let gameContainer = document.querySelector("#gamecontainer");
    if (gameContainer == undefined) return;

    let extensionPanel = document.querySelector("#vesper-extensions");
    if (extensionPanel == undefined) {
        extensionPanel = document.createElement("div");
        extensionPanel.id = "vesper-extensions";

        extensionPanel.style.background = '#98a0b8';
        extensionPanel.style.border = '2px solid #768a96';
        extensionPanel.style.display = 'flex';
        extensionPanel.style.flexDirection = 'row';
        extensionPanel.style.padding = '4px';
        extensionPanel.style.margin = '0px';
        extensionPanel.style.marginLeft = '-4px';
        extensionPanel.style.marginBottom = '8px';

        gameContainer.children[1].after(extensionPanel);
    }

    let e = document.createElement("div");
    e.id = "custom-army-importer";
    extensionPanel.append(e);

    let o = Object.keys(BaseInfo.countries).map((e => e)).sort(((e, t) => BaseInfo.countries[e].id - BaseInfo.countries[t].id));
    window.armyImporter = new Vue({
        el: "#custom-army-importer",
        template: '<PrevMapAnalyzer :countries="countries" :countriesDisabled="countriesDisabled" :countryReplacementMap="countryReplacementMap"/>',
        data: function() {
            return {
                countries: o,
                countriesDisabled: [],
                countryReplacementMap: {},
            }
        }
    })
}

// Path to your GitHub sprite folder
const githubBaseUrl = "https://raw.githubusercontent.com/ShinyDeagle/custom-army-testing/refs/heads/main/ab-rework/";
// List of possible types sprites that can be replaced.
const spriteMap = [
    "xxoo.gif",
    "gs_xxoo.gif",
    "xxoo_mside.gif",
    "xxoo_mup.gif",
    "xxoo_mdown.gif",
];
const buildingMap = [
    "xxcity.gif",
    "xxport.gif",
    "xxbase.gif",
    "xxlab.gif",
    "xxcomtower.gif",
    "xxairport.gif",
    "xxhq.gif",
];
const buildingNames = [
    "city.gif",
    "port.gif",
    "base.gif",
    "lab.gif",
    "comtower.gif",
    "airport.gif",
    "hq.gif",
];
// List of all possible sprites that can be replaced.
const unitSpriteMap = [];
for (let id of Object.keys(BaseInfo.units)) {
    let name = BaseInfo.units[id].name;
    let cleanedName = name.toLowerCase().replace(" ", "");
    for (let spriteKey of spriteMap) {
        let sprite = spriteKey.replace("oo", cleanedName);
        unitSpriteMap.push(sprite);
    }
}

// Accounts for the loss of the `.` in the md.tank when using animations on the site.
unitSpriteMap.push("xxmdtank_mside.gif")
unitSpriteMap.push("xxmdtank_mup.gif")
unitSpriteMap.push("xxmdtank_mdown.gif")

const buildingSpriteMap = buildingMap.slice();
window.unitSpriteMap = unitSpriteMap;
window.buildingSpriteMap = unitSpriteMap;

// Cache for available sprites
const spriteTable = new Map();
let replacementSprites = new Map();
window.replacementSprites = replacementSprites;

// Function to check if a sprite exists on GitHub
async function checkSpritesExist(baseURLs, spriteNames, isBuilding) {
    const allChecks = [];
    for (let URL of baseURLs) {
        let baseURL = URL;
        let country = getKeyByValue(this.countryReplacementMap, baseURL);
        if (isBuilding) country = getFlatName(country);
        if (country == undefined) continue;

        const checks = spriteNames.map(spriteName => {
            const url = baseURL + spriteName;
            return fetch(url, { method: "HEAD" })
                .then(response => ({ spriteName, baseURL: baseURL, country: country, exists: response.ok }))
                .catch(() => ({ spriteName, exists: false })); // Handle fetch errors
        });
        for (let check of checks) allChecks.push(check);
    }

    // Wait for all checks to complete
    const results = await Promise.all(allChecks);
    return results;
}

// Preload all sprites
async function preloadSprites() {
    countryReplacementMap = window.countryReplacementMap;
    const spriteArray = unitSpriteMap; // Convert unitSpriteMap to an array
    const baseURLs = Object.values(countryReplacementMap);
    let results = await checkSpritesExist(baseURLs, spriteArray); // Check all sprites at once

    // Process the results
    results.forEach(({ spriteName, baseURL, country, exists }) => {
        if (exists) {
            replacementSprites.set(spriteName.replace("xx", country), baseURL + spriteName);
            console.log(`Cached Unit: ${spriteName}`);
        } else {
            // console.log(`Not found: ${spriteName}`);
        }
    });

    const buildingArray = buildingSpriteMap;
    results = await checkSpritesExist(baseURLs, buildingArray, true);

    // Process the results
    results.forEach(({ spriteName, baseURL, country, exists }) => {
        if (exists) {
            replacementSprites.set(spriteName.replace("xx", country), baseURL + spriteName);
            console.log(`Cached Building: ${spriteName}`);
        } else {
            // console.log(`Not found: ${spriteName}`);
        }
    });

    console.log("All sprites preloaded!");
}

async function preloadCountrySprites(country, map) {
    countryReplacementMap = map;
    window.countryReplacementMap = map;
    const spriteArray = unitSpriteMap;
    const baseURL = countryReplacementMap[country];
    if (baseURL == undefined) return;

    let results = await checkSpritesExist([baseURL], spriteArray);
    // Process the results
    results.forEach(({ spriteName, baseURL, country, exists }) => {
        if (exists) {
            replacementSprites.set(spriteName.replace("xx", country), baseURL + spriteName);
            console.log(`Cached: ${spriteName}`);
        } else {
            // console.log(`Not found: ${spriteName}`);
        }
    });

    const buildingArray = buildingSpriteMap;
    results = await checkSpritesExist([baseURL], buildingArray, true);

    // Process the results
    results.forEach(({ spriteName, baseURL, country, exists }) => {
        if (exists) {
            replacementSprites.set(spriteName.replace("xx", country), baseURL + spriteName);
            console.log(`Cached Building: ${spriteName}`);
        } else {
            // console.log(`Not found: ${spriteName}`);
        }
    });
    console.log(`Sprites for ${country} have been preloaded!`);
}
window.preloadCountrySprites = preloadCountrySprites;

function forceUpdate() {
    countryReplacementMap = window.countryReplacementMap;
    countriesDisabled = window.countriesDisabled;
    replacementSprites = window.replacementSprites;
}

window.forceUpdate = forceUpdate;

// Run preloading process
preloadSprites();

const preloadedSprites = new Map();

function addToPreloads() {
    // Preload available sprites to ensure seamless animation
    replacementSprites.values().forEach((sprite) => {
        const img = new Image();
        img.src = sprite;
        preloadedSprites.set(sprite, img);
    });
}

addToPreloads();

function doSpriteReplacement(img) {
    countriesDisabled = window.countriesDisabled;
    const spriteName = img.src;
    const result = extractCountryAndPath(spriteName)
    if (!result) return;

    const country = result.country;
    if (countriesDisabled.includes(country)) return;
    const path = result.path;
    let unit = country + path;

    let isBuilding = false;
    for (let bName of buildingNames) {
        if (bName == unit.replace(country, "")) {
            isBuilding = true;
            break;
        }
    }

    if (isBuilding) unit = getFlatName(country) + path;

    const replacer = "xx" + path;
    const desiredSrc = countryReplacementMap[country] ? countryReplacementMap[country] + replacer : null;
    if (!desiredSrc) return;

    // Replace the src only if it differs and the sprite is available
    if (img.src !== desiredSrc && replacementSprites.has((result.gs ? "gs_" : "") + unit)) {
        img.src = replacementSprites.get((result.gs ? "gs_" : "") + unit);
        //console.log(`Replaced ${spriteName} with ${img.src}`);
    }
}

let debounceTimers = new Map();
const observer = new MutationObserver((mutationsList) => {
    for (const mutation of mutationsList) {
        const img = mutation.target;
        if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
           // Clear any existing timeout for this specific image
            if (debounceTimers.has(img)) {
                clearTimeout(debounceTimers.get(img));
            }

            // Create a new debounce timer for this image
            const timer = setTimeout(() => {
                doSpriteReplacement(img);

                // Clean up the timer once the operation is complete
                debounceTimers.delete(img);
            }, 10); // Debounce interval

            // Store the timer for this image
            debounceTimers.set(img, timer);
        }
        if (mutation.type === 'childList') {
            mutation.addedNodes.forEach((node) => {
                // Check if the node is an element (ignores text nodes)
                if (node.nodeType === Node.ELEMENT_NODE) {
                    // Look for img elements inside the node
                    const images = node.querySelectorAll('img');
                    images.forEach((img) => {
                        if (isBaseURL(img.src)) return;
                        doSpriteReplacement(img);
                    });
                }
            });
        }
    }
});

observer.observe(targetNode, {
    childList: true,  // Detects added or removed child nodes
    subtree: true,    // Detects nodes anywhere within the subtree
    attributes: true, // Detects changes to attributes (like `src` of images)
});
const replaceAllSprites = () => {
    let images = document.querySelectorAll('#gamemap .game-unit img, #gamemap .game-building img, #calculator .unit-menu img, #calculator .selected-unit.border img, #gamemap-container .unit-info-sprite img, #gamemap-container .terrain-info-sprite img'); // Assuming all the images are within #game-map
    images.forEach((img) => {
        doSpriteReplacement(img);
    });
};

// Call replaceAllSprites to replace images on page load (or wherever appropriate)
replaceAllSprites();

const checkInterval = 500; // Check every 500ms

function checkSrcChanges() {
    let images = document.querySelectorAll('#gamemap .game-unit img, #gamemap .game-building img, #calculator .unit-menu img, #calculator .selected-unit.border img, #gamemap-container .unit-info-sprite img, #gamemap-container .terrain-info-sprite img'); // Assuming all the images are within #game-map
    // console.log("Periodic Src Check!");
    // console.log(images.length);

    const terrainAniPath = 'terrain/ani/'; // The part of the URL we care about
    images.forEach(img => {
        if (img.src.includes("hq.gif")) {
            let a = 0;
        }
        if (isBaseURL(img.src)) {
            let fixed = img.src;
            for (let country of Object.keys(countryReplacementMap)) {
                let baseURL = countryReplacementMap[country];
                const urlIndex = fixed.indexOf(baseURL);
                if (urlIndex !== -1) {
                    // Cut everything before "terrain/ani/" and keep everything after it
                    const spritePath = fixed.replace(baseURL, ""); // Get the part of the URL after "terrain/ani/"
                    let spriteName = spritePath.split('/').pop().replace("xx", country); // Get the sprite filename

                    let isBuilding = false;
                    for (let bName of buildingNames) {
                        if (bName == spriteName.replace(country, "")) {
                            isBuilding = true;
                            break;
                        }
                    }

                    if (isBuilding) spriteName = spriteName.replace(country, getFlatName(country));

                    if (!replacementSprites.has(spriteName) || countriesDisabled.includes(country)) {
                        fixed = terrainAniPath + spriteName; // Set the new sprite path with "terrain/ani/"

                        img.src = fixed;
                        // console.log(`Overridden: ${fixed}`);
                        return;
                    }
                }
            }
        }

        doSpriteReplacement(img);
    });
}
window.checkSrcChanges = checkSrcChanges;

// Call `checkSrcChanges` periodically
setInterval(checkSrcChanges, checkInterval);

async function loadThisShit() {
    try {
        if (typeof Vue === "undefined") {
            await loadScript("js/vue.js");
        }

        initArmyImporter();
    } catch (error) {
        console.error("Failed to load scripts or initialize:", error);
    }
}
window.gameMap = document.getElementById("gamemap")
loadThisShit();

console.log("Sprite replacement script with caching is running...");

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址