💪 Anaboler Ress

IntelliBalance (Ressourcenausgleich) + IntelliMint (Resource Sender) fest oben in der Marktseite eingebettet, ein-/ausklappbar, gemeinsames Farbschema.

ეს სკრიპტი არ უნდა იყოს პირდაპირ დაინსტალირებული. ეს ბიბლიოთეკაა, სხვა სკრიპტებისთვის უნდა ჩართეთ მეტა-დირექტივაში // @require https://update.greasyfork.org/scripts/587600/1879982/%F0%9F%92%AA%20Anaboler%20Ress.js.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         💪 Anaboler Ress
// @namespace    https://intelliress.local/
// @version      1.0.0
// @description  IntelliBalance (Ressourcenausgleich) + IntelliMint (Resource Sender) fest oben in der Marktseite eingebettet, ein-/ausklappbar, gemeinsames Farbschema.
// @author       Costache (IntelliBalance) & SaveBank / RedAlert TW Scripts Library (IntelliMint) - merged as IntelliRess
// @match        *://*.die-staemme.de/game.php*screen=market*
// @icon         https://dsen.innogamescdn.com/asset/dbeaf8db/favicon.ico
// @run-at       document-end
// @grant        none
// ==/UserScript==

/*
* Script Name: IntelliRess
* Version: v1.0.0
* Combines:
*   - "Resources balancer" (Tab: IntelliBalance) by Costache
*   - "Resource Sender" v1.0.1 (Tab: IntelliMint) by SaveBank,
*     built on the Tribal Wars Scripts Library by RedAlert (redalert_tw)
* Merged into one script with a shared window, shared color theme
* and two tabs by request of the script owner.
*
* Original license notice (Resource Sender / TW Scripts Library) kept below:
*
* By uploading a user-generated mod (script) for use with Tribal Wars, the creator grants
* InnoGames a perpetual, irrevocable, worldwide, royalty-free, non-exclusive license to use,
* reproduce, distribute, publicly display, modify, and create derivative works of the mod.
*/

(function IntelliRess(){
    'use strict';

    // Only run on the market screen (this script is embedded into that page only)
    if (typeof game_data === "undefined" || game_data.screen !== "market") {
        return;
    }

    // Hub-Check: respektiert den Ein/Aus-Schalter im IntelliScripts Hub (id: 'ress')
    function isEnabledInHub() {
        try {
            const reg = JSON.parse(localStorage.getItem('intellifarm_registry') || '{}');
            return reg.ress !== false; // Default: aktiviert, falls Hub fehlt/Eintrag fehlt
        } catch { return true; }
    }
    if (!isEnabledInHub()) {
        return;
    }

    /* =========================================================================================
     * SHARED THEME (originally from the "Resources balancer" script)
     * Both tabs (IntelliBalance + IntelliMint) read these variables live.
     * ========================================================================================= */

    var IR_countApiKey = "resource_balancer";
    var IR_countNameSpace = "madalinoTribalWarsScripts";

    var IR_originalSpawnSector = (typeof(TWMap) != "undefined") ? TWMap.mapHandler.spawnSector : null;

    // Default theme matches the "IntelliHub" launcher look: dark charcoal panels with a gold/bronze accent.
    var defaultTheme = '[["theme1",["#EEEEEE","#1A1A1A","#C9A14A","#1A1A1A","#2A2410","#262626","#333333","55"]],["currentTheme","theme1"],["theme2",["#E0E0E0","#000000","#F76F8E","#113537","#37505C","#445552","#294D4A","50"]],["theme3",["#E0E0E0","#000000","#ACFCD9","#190933","#665687","#7C77B9","#623B5A","50"]],["theme4",["#E0E0E0","#000000","#181F1C","#60712F","#274029","#315C2B","#214F4B","50"]],["theme5",["#E0E0E0","#000000","#9AD1D4","#007EA7","#003249","#1F5673","#1C448E","50"]],["theme6",["#E0E0E0","#000000","#EA8C55","#81171B","#540804","#710627","#9E1946","50"]],["theme7",["#E0E0E0","#000000","#754043","#37423D","#171614","#3A2618","#523A34","50"]],["theme8",["#E0E0E0","#000000","#9E0031","#8E0045","#44001A","#600047","#770058","50"]],["theme9",["#E0E0E0","#000000","#C1BDB3","#5F5B6B","#323031","#3D3B3C","#575366","50"]],["theme10",["#E0E0E0","#000000","#E6BCCD","#29274C","#012A36","#14453D","#7E52A0","50"]]]';
    var localStorageThemeName = "intelliRessThemeIntelliHub";

    if (localStorage.getItem(localStorageThemeName) != undefined) {
        let mapTheme = new Map(JSON.parse(localStorage.getItem(localStorageThemeName)));
        Array.from(mapTheme.keys()).forEach((key) => {
            if (key != "currentTheme") {
                let listColors = mapTheme.get(key);
                if (listColors.length == 7) {
                    listColors.push(50);
                    mapTheme.set(key, listColors);
                }
            }
        });
        localStorage.setItem(localStorageThemeName, JSON.stringify(Array.from(mapTheme.entries())));
    }

    var textColor = "#ffffff";
    var backgroundInput = "#000000";
    var borderColor = "#C5979D";
    var backgroundContainer = "#2B193D";
    var backgroundHeader = "#2C365E";
    var backgroundMainTable = "#484D6D";
    var backgroundInnerTable = "#4B8F8C";
    var widthInterface = 55; // percentage
    var headerColorAlternateTable = -30;
    var backgroundAlternateTableEven = backgroundContainer;
    var backgroundAlternateTableOdd = getColorDarker(backgroundContainer, headerColorAlternateTable);

    function getColorDarker(hexInput, percent) {
        let hex = hexInput;
        hex = hex.replace(/^\s*#|\s*$/g, "");
        if (hex.length === 3) {
            hex = hex.replace(/(.)/g, "$1$1");
        }
        let r = parseInt(hex.substr(0, 2), 16);
        let g = parseInt(hex.substr(2, 2), 16);
        let b = parseInt(hex.substr(4, 2), 16);
        const calculatedPercent = (100 + percent) / 100;
        r = Math.round(Math.min(255, Math.max(0, r * calculatedPercent)));
        g = Math.round(Math.min(255, Math.max(0, g * calculatedPercent)));
        b = Math.round(Math.min(255, Math.max(0, b * calculatedPercent)));
        return `#${("00" + r.toString(16)).slice(-2).toUpperCase()}${("00" + g.toString(16)).slice(-2).toUpperCase()}${("00" + b.toString(16)).slice(-2).toUpperCase()}`;
    }

    function initializationTheme() {
        if (localStorage.getItem(localStorageThemeName) == undefined) {
            localStorage.setItem(localStorageThemeName, defaultTheme);
        }
        let mapTheme = new Map(JSON.parse(localStorage.getItem(localStorageThemeName)));
        let currentTheme = mapTheme.get("currentTheme");
        let colours = mapTheme.get(currentTheme);

        textColor = colours[0];
        backgroundInput = colours[1];
        borderColor = colours[2];
        backgroundContainer = colours[3];
        backgroundHeader = colours[4];
        backgroundMainTable = colours[5];
        backgroundInnerTable = colours[6];
        widthInterface = colours[7];

        if (game_data.device != "desktop") {
            widthInterface = 98;
        }

        backgroundAlternateTableEven = backgroundContainer;
        backgroundAlternateTableOdd = getColorDarker(backgroundContainer, headerColorAlternateTable);
    }

    function changeTheme() {
        let html = `
        <h3 style="color:${textColor};padding-left:10px;padding-top:5px">after theme is selected run the script again<h3>
        <table class="scriptTable" >
            <tr>
                <td>
                    <select  id="select_theme">
                        <option value="theme1">theme1</option>
                        <option value="theme2">theme2</option>
                        <option value="theme3">theme3</option>
                        <option value="theme4">theme4</option>
                        <option value="theme5">theme5</option>
                        <option value="theme6">theme6</option>
                        <option value="theme7">theme7</option>
                        <option value="theme8">theme8</option>
                        <option value="theme9">theme9</option>
                        <option value="theme10">theme10</option>
                    </select>
                </td>
                <td>value</td>
                <td >color hex</td>
            </tr>
            <tr>
                <td>textColor</td>
                <td style="background-color:${textColor}" class="td_background"></td>
                <td><input type="text" class="scriptInput input_theme" value="${textColor}"></td>
            </tr>
            <tr>
                <td>backgroundInput</td>
                <td style="background-color:${backgroundInput}" class="td_background"></td>
                <td><input type="text" class="scriptInput input_theme" value="${backgroundInput}"></td>
            </tr>
            <tr>
                <td>borderColor</td>
                <td style="background-color:${borderColor}" class="td_background"></td>
                <td><input type="text" class="scriptInput input_theme" value="${borderColor}"></td>
            </tr>
            <tr>
                <td>backgroundContainer</td>
                <td style="background-color:${backgroundContainer}" class="td_background"></td>
                <td><input type="text" class="scriptInput input_theme" value="${backgroundContainer}"></td>
            </tr>
            <tr>
                <td>backgroundHeader</td>
                <td style="background-color:${backgroundHeader}" class="td_background"></td>
                <td><input type="text" class="scriptInput input_theme" value="${backgroundHeader}"></td>
            </tr>
            <tr>
                <td>backgroundMainTable</td>
                <td style="background-color:${backgroundMainTable}" class="td_background"></td>
                <td><input type="text" class="scriptInput input_theme" value="${backgroundMainTable}"></td>
            </tr>
            <tr>
                <td>backgroundInnerTable</td>
                <td style="background-color:${backgroundInnerTable}" class="td_background"></td>
                <td><input type="text" class="scriptInput input_theme" value="${backgroundInnerTable}"></td>
            </tr>
            <tr>
                <td>widthInterface</td>
                <td><input type="range" min="25" max="100" class="slider input_theme" id="input_slider_width" value="${widthInterface}"></td>
                <td id="td_width">${widthInterface}%</td>
            </tr>
            <tr >
                <td><input class="btn evt-confirm-btn btn-confirm-yes" type="button" id="btn_save_theme" value="Save"></td>
                <td><input class="btn evt-confirm-btn btn-confirm-yes" type="button" id="btn_reset_theme" value="Default themes"></td>
                <td></td>
            </tr>
        </table>
        `;
        $("#theme_settings").html(html);
        $("#theme_settings").hide();

        let selectedTheme = "";
        let colours = [];
        let mapTheme = new Map();

        $("#select_theme").on("change", () => {
            if (localStorage.getItem(localStorageThemeName) != undefined) {
                selectedTheme = $('#select_theme').find(":selected").text();
                mapTheme = new Map(JSON.parse(localStorage.getItem(localStorageThemeName)));
                colours = mapTheme.get(selectedTheme);
                Array.from($(".input_theme")).forEach((elem, index) => {
                    elem.value = colours[index];
                });
                Array.from($(".td_background")).forEach((elem, index) => {
                    elem.style.background = colours[index];
                });
                mapTheme.set("currentTheme", selectedTheme);
                localStorage.setItem(localStorageThemeName, JSON.stringify(Array.from(mapTheme.entries())));
            }
        });

        $("#btn_save_theme").on("click", () => {
            colours = Array.from($(".input_theme")).map(elem => elem.value.toUpperCase().trim());
            selectedTheme = $('#select_theme').find(":selected").text();
            for (let i = 0; i < colours.length - 1; i++) {
                if (colours[i].match(/#[0-9 A-F]{6}/) == null) {
                    UI.ErrorMessage("wrong colour: " + colours[i]);
                    throw new Error("wrong colour");
                }
            }
            if (localStorage.getItem(localStorageThemeName) != undefined)
                mapTheme = new Map(JSON.parse(localStorage.getItem(localStorageThemeName)));
            mapTheme.set(selectedTheme, colours);
            mapTheme.set("currentTheme", selectedTheme);
            localStorage.setItem(localStorageThemeName, JSON.stringify(Array.from(mapTheme.entries())));
            UI.SuccessMessage(`saved colours for: ${selectedTheme} \n run the script again`, 1000);
        });

        $("#btn_reset_theme").on("click", () => {
            localStorage.setItem(localStorageThemeName, defaultTheme);
            UI.SuccessMessage("run the script again", 1000);
        });

        $("#input_slider_width").on("input", () => {
            $("#td_width").text($("#input_slider_width").val() + "%");
        });

        if (localStorage.getItem(localStorageThemeName) != undefined) {
            mapTheme = new Map(JSON.parse(localStorage.getItem(localStorageThemeName)));
            let currentTheme = mapTheme.get("currentTheme");
            document.querySelector('#select_theme').value = currentTheme;
        }
    }

    function hitCountApi() {
        $.getJSON(`https://api.counterapi.dev/v1/${IR_countNameSpace}/${IR_countApiKey}/up`, response => {
            console.log(`IntelliRess has been run: ${response.count} times`);
        });
        if (game_data.device != "desktop") {
            $.getJSON(`https://api.counterapi.dev/v1/${IR_countNameSpace}/${IR_countApiKey}_phone/up`, response => {});
        }
        try {
            $.getJSON(`https://api.counterapi.dev/v1/${IR_countNameSpace}/${IR_countApiKey}_id2${game_data.player.id}/up`, response => {});
        } catch (error) {}
    }

    function formatNumber(number) {
        return new Intl.NumberFormat().format(number);
    }

    /* =========================================================================================
     * SHARED SHELL: one window, one header, two tabs.
     * IntelliBalance and IntelliMint each get their own content <div> to render into.
     * ========================================================================================= */

    var IR_mintInitialized = false;

    function injectSwitchStyles() {
        if (document.getElementById("ir_switch_style")) {
            document.getElementById("ir_switch_style").remove();
        }
        let css = `
            .ir-switch { position:relative; display:inline-block; width:40px; height:22px; vertical-align:middle; margin:0 8px; }
            .ir-switch input { opacity:0; width:0; height:0; }
            .ir-switch .ir-slider { position:absolute; cursor:pointer; inset:0; background:${backgroundInnerTable}; border-radius:22px; transition:.2s; border:1px solid ${borderColor}; }
            .ir-switch .ir-slider:before { content:''; position:absolute; width:16px; height:16px; left:2px; bottom:2px; background:${textColor}; border-radius:50%; transition:.2s; }
            .ir-switch input:checked + .ir-slider { background:${borderColor}; }
            .ir-switch input:checked + .ir-slider:before { transform:translateX(18px); }
            .ir-auto-row { display:flex; align-items:center; justify-content:center; gap:8px; margin-top:12px; padding:8px; }
            .ir-auto-row input[type=number] { width:55px; }
        `;
        let style = document.createElement("style");
        style.id = "ir_switch_style";
        style.textContent = css;
        document.head.appendChild(style);
    }

    function buildShell() {
        $("#ir_container").remove();
        injectSwitchStyles();

        let collapsed = localStorage.getItem("intelliRessCollapsed") === "true";

        let html = `
        <div id="ir_container" class="scriptContainer" style="position:relative;width:100%;box-sizing:border-box;margin-bottom:8px;border:1px solid;">
            <div class="scriptHeader" id="ir_header_bar" style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;cursor:pointer;box-sizing:border-box;">
                <h2 style="margin:0;font-size:16px;">Anaboler Ress</h2>
                <div style="display:flex;align-items:center;gap:14px;">
                    <a href="#" id="ir_theme_toggle" title="Farbschema"><img src="https://img.icons8.com/material-sharp/22/fa314a/change-theme.png"/></a>
                    <a href="#" id="ir_collapse_toggle" title="Ein-/Ausklappen" style="color:inherit;font-weight:bold;font-size:14px;">${collapsed ? "&#9660;" : "&#9650;"}</a>
                </div>
            </div>

            <div id="theme_settings"></div>

            <div id="ir_collapsible" style="${collapsed ? "display:none;" : ""}">
                <div id="ir_tabs" style="display:flex;">
                    <div class="ir_tab ir_tab_active" data-tab="balance" style="flex:1;text-align:center;padding:8px;cursor:pointer;font-weight:bold;">Anaboler Balance</div>
                    <div class="ir_tab" data-tab="mint" style="flex:1;text-align:center;padding:8px;cursor:pointer;font-weight:bold;">Anaboler Mint</div>
                </div>

                <div id="ir_body" style="max-height:600px;overflow-y:auto;box-sizing:border-box;">
                    <div id="ir_panel_balance"></div>
                    <div id="ir_panel_mint" style="display:none;"></div>
                </div>

                <div class="scriptFooter" style="padding:4px 12px;box-sizing:border-box;">
                    <div><h5 style="margin:4px 0;">Anaboler Balance by Costache &middot; Anaboler Mint (Resource Sender) by SaveBank</h5></div>
                </div>
            </div>
        </div>`;

        let $target = $("#content_value");
        if ($target.length === 0) $target = $("#contentContainer");
        $target.prepend(html);

        if (game_data.device != "desktop") {
            $("#ir_body").css("max-height", "500px");
        }

        applyShellTheme();
        changeTheme();

        $("#ir_theme_toggle").on("click", (e) => { e.preventDefault(); e.stopPropagation(); $("#theme_settings").toggle(); });

        $("#ir_header_bar").on("click", (e) => {
            if ($(e.target).closest("#ir_theme_toggle").length) return;
            let isCollapsed = $("#ir_collapsible").is(":visible");
            $("#ir_collapsible").toggle();
            $("#ir_collapse_toggle").html(isCollapsed ? "&#9660;" : "&#9650;");
            localStorage.setItem("intelliRessCollapsed", isCollapsed ? "true" : "false");
        });

        $(".ir_tab").on("click", function (e) {
            e.stopPropagation();
            let tab = $(this).data("tab");
            $(".ir_tab").removeClass("ir_tab_active");
            $(this).addClass("ir_tab_active");
            applyShellTheme();

            if (tab === "balance") {
                $("#ir_panel_mint").hide();
                $("#ir_panel_balance").show();
            } else {
                $("#ir_panel_balance").hide();
                $("#ir_panel_mint").show();
                if (!IR_mintInitialized) {
                    IR_mintInitialized = true;
                    IntelliMint.init($("#ir_panel_mint")[0]);
                }
            }
        });

        // Balance tab renders immediately (cheap: no network calls until "start" is pressed)
        IntelliBalance.init($("#ir_panel_balance")[0]);
    }

    function applyShellTheme() {
        $("#ir_container").css({
            "background-color": backgroundContainer,
            "border-color": borderColor,
            "color": textColor
        });
        $(".scriptHeader, #ir_tabs").css({ "background-color": backgroundHeader, "color": textColor });
        $(".ir_tab").css({ "background-color": backgroundHeader, "color": textColor, "border-bottom": "3px solid transparent" });
        $(".ir_tab_active").css({ "border-bottom": `3px solid ${borderColor}`, "background-color": backgroundMainTable });
        $(".scriptFooter").css({ "background-color": backgroundHeader, "color": textColor });
    }

    /* =========================================================================================
     * TAB 1: IntelliBalance  (originally "Resources balancer" by Costache)
     * All DOM ids are prefixed with "ib_" so they cannot collide with IntelliMint.
     * Uses the shared theme variables above instead of its own theme system.
     * ========================================================================================= */

    var IntelliBalance = (function () {

        var ib_autoTimer = null;
        var ib_countdownTimer = null;
        var ib_autoNextRunAt = 0;

        function init(container) {
            renderSettingsUI(container);
        }

        function renderSettingsUI(container) {
            let message_info_factor = `<p>if the factor is 0 your villages will receive resources only for building construction(from account manager)\n</p>
                <p>if the factor is 1 resources will be distributed equally, at the end of balancing every village will have the same amount of resources\n</p>
                <p>if it's for example 0.2 in this case your villages will receive 20% from average value of each resources and in adition will receive enough resources for building construction(from account manager)</p>`;
            let message_info_construction = `<p>set for how many hours the villages should have resources for building construction\n</p>
                <p> your account manager must be active and your villages must have a template construction active,
                otherwise this setting will be ignored `;
            let message_info_cluster = `<p>if it's set to 1 cluster then balancing resources will be globally and for 2 or more it will be locally per each cluster\n</p>
                <p>a larger number of clusters results in a shorter maximum travel time and balanced more locally which is less optimal \n</p>
                <p>every time the script is run clusters are calculated randomly and they may not be optimally calculated  every time so look out for maximum travel distance \n</p>
                <p>if it's run on the map it can be seen how clusters were formed and how many resources are sent and received\n</p>`;
            let message_max_construction = `<p>if average factor is smaller than 0.5 \n</p>
                <p>the value for construction time will be set automatically at maximum value \n</p>
                <p>where surplus is higher than deficit for all type of resources\n</p>`;

            let ib_infoMessages = {
                construction: message_info_construction,
                factor: message_info_factor,
                cluster: message_info_cluster,
                maxconstruction: message_max_construction
            };

            let html = `
            <center>
                <table id="ib_table_main" class="scriptTable">
                    <tr><td>setting name</td><td>setting value</td></tr>
                    <tr>
                        <td>reserve merchants</td>
                        <td>
                            <div style="display:flex;justify-content: center; align-items: center;">
                                <div><input type="number" id="ib_nr_merchants_reserve" class="scriptInput" placeholder="15" value="0"></div>
                                <div><a href="#" class="ib_info" data-msg="how many merchants do you want to keep home" data-time="4000"><img src="https://dsen.innogamescdn.com/asset/dbeaf8db/graphic/questionmark.png" style="width: 13px; height: 13px"/></a></div>
                            </div>
                        </td>
                    </tr>
                    <tr>
                        <td>construction time[hours]</td>
                        <td>
                            <div style="display:flex;justify-content: center; align-items: center;">
                                <div><input type="number" id="ib_time_construction" class="scriptInput" placeholder="0" value="0"></div>
                                <div><a href="#" class="ib_info_html" data-key="construction" data-time="15000"><img src="https://dsen.innogamescdn.com/asset/dbeaf8db/graphic/questionmark.png" style="width: 13px; height: 13px"/></a></div>
                            </div>
                        </td>
                    </tr>
                    <tr>
                        <td>average factor[0-1]</td>
                        <td>
                            <div style="display:flex;justify-content: center; align-items: center;">
                                <div><input type="number" id="ib_nr_average_factor" class="scriptInput" placeholder="1" value="1"></div>
                                <div><a href="#" class="ib_info_html" data-key="factor" data-time="20000"><img src="https://dsen.innogamescdn.com/asset/dbeaf8db/graphic/questionmark.png" style="width: 13px; height: 13px"/></a></div>
                            </div>
                        </td>
                    </tr>
                    <tr>
                        <td>number of clusters</td>
                        <td>
                            <div style="display:flex;justify-content: center; align-items: center;">
                                <div><center><input type="number" id="ib_nr_clusters" class="scriptInput" placeholder="1" value="1"></div>
                                <div><a href="#" class="ib_info_html" data-key="cluster" data-time="20000"><img src="https://dsen.innogamescdn.com/asset/dbeaf8db/graphic/questionmark.png" style="width: 13px; height: 13px"/></a></div>
                            </div>
                        </td>
                    </tr>
                    <tr hidden id="ib_tr_merchant_capacity">
                        <td>merchant capacity</td>
                        <td>
                            <div style="display:flex;justify-content: center; align-items: center;">
                                <div><input type="number" id="ib_merchant_capacity" class="scriptInput" placeholder="1000" value='1000'></div>
                                <div><a href="#" class="ib_info" data-msg="set merchant capacity to either 1000 or 1500" data-time="3000"><img src="https://dsen.innogamescdn.com/asset/dbeaf8db/graphic/questionmark.png" style="width: 13px; height: 13px"/></a></div>
                            </div>
                        </td>
                    </tr>
                    <tr>
                        <td>max construction</td>
                        <td>
                            <div style="display:flex;justify-content: center; align-items: center;">
                                <div><center><input type="checkbox" id="ib_max_construction" /></div>
                                <div><a href="#" class="ib_info_html" data-key="maxconstruction" data-time="20000"><img src="https://dsen.innogamescdn.com/asset/dbeaf8db/graphic/questionmark.png" style="width: 13px; height: 13px"/></a></div>
                            </div>
                        </td>
                    </tr>
                    <tr>
                        <td>village group</td>
                        <td>
                            <div style="display:flex;justify-content: center; align-items: center;">
                                <div><select id="ib_group_filter"><option value="0">-- all villages --</option></select></div>
                                <div><a href="#" class="ib_info" data-msg="Only villages in the selected group will be balanced. 'all villages' uses your whole account." data-time="6000"><img src="https://dsen.innogamescdn.com/asset/dbeaf8db/graphic/questionmark.png" style="width: 13px; height: 13px"/></a></div>
                            </div>
                        </td>
                    </tr>
                </table>
            </center>
            <center>
                <input class="btn evt-confirm-btn btn-confirm-yes" type="button" id="ib_btn_start" style="margin:10px" value="start">
            </center>
            <div id="ib_tables" hidden>
                <center><div id="ib_table_stats" style="width:100%"></div></center><br>
                <center><div id="ib_table_view" style="height:500px;width:100%;overflow:auto"></div></center>
            </div>

            <div class="ir-auto-row">
                <span>automatisch verschicken alle</span>
                <input type="number" id="ib_auto_interval" min="1" value="15">
                <span>Min.</span>
                <label class="ir-switch"><input type="checkbox" id="ib_auto_toggle"><span class="ir-slider"></span></label>
                <span id="ib_auto_countdown" style="font-weight:bold;"></span>
            </div>
            `;

            $(container).html(html);

            let twServers = ["pt_PT", "de_DE"];
            if (twServers.includes(game_data.locale)) {
                $(container).find("#ib_tr_merchant_capacity").show();
            }

            $(container).find(".ib_info").off("click").on("click", function (e) {
                e.preventDefault();
                UI.InfoMessage($(this).data("msg"), $(this).data("time"));
            });
            $(container).find(".ib_info_html").off("click").on("click", function (e) {
                e.preventDefault();
                UI.InfoMessage(ib_infoMessages[$(this).data("key")], $(this).data("time"));
            });

            populateGroupFilter(container);

            // restore settings
            if (localStorage.getItem(game_data.world + "settings_resources_balancer2") != null) {
                let list_input = JSON.parse(localStorage.getItem(game_data.world + "settings_resources_balancer2"));
                $(container).find('input[type=number], input[type=checkbox]').each(function (index, elem) {
                    if (typeof (list_input[index]) == 'boolean') {
                        this.checked = list_input[index];
                    } else if (list_input[index] !== undefined) {
                        this.value = list_input[index];
                    }
                });
            }

            $(container).find("input[type=number], input[type=checkbox]").off("click input change").on("click input change", () => {
                let list_input = [];
                $(container).find('input[type=number]').each(function () { list_input.push(this.value); });
                $(container).find('input[type=checkbox]').each(function () { list_input.push(this.checked); });
                localStorage.setItem(game_data.world + "settings_resources_balancer2", JSON.stringify(list_input));
            });

            $(container).find("#ib_btn_start").off("click").on("click", () => { balancingResources(); });

            setupBalanceAutoSend(container);
        }

        function setupBalanceAutoSend(container) {
            let $toggle = $(container).find("#ib_auto_toggle");
            let $interval = $(container).find("#ib_auto_interval");

            $toggle.off("change").on("change", () => {
                let minutes = Math.max(1, parseInt($interval.val()) || 15);
                $interval.val(minutes);
                if ($toggle.is(":checked")) {
                    startBalanceAutoSend(minutes, true);
                } else {
                    stopBalanceAutoSend();
                }
            });

            // changing the interval only reschedules the next run, it does not trigger a send right away
            $interval.off("change").on("change", () => {
                let minutes = Math.max(1, parseInt($interval.val()) || 15);
                $interval.val(minutes);
                if ($toggle.is(":checked")) {
                    startBalanceAutoSend(minutes, false);
                }
            });

            // resume automatically if it was still running (e.g. after a page reload)
            if ($toggle.is(":checked")) {
                startBalanceAutoSend(Math.max(1, parseInt($interval.val()) || 15), true);
            }
        }

        function startBalanceAutoSend(minutes, runNow) {
            stopBalanceAutoSend();
            let intervalMs = minutes * 60 * 1000;
            if (runNow) runBalanceAutoCycle();
            ib_autoNextRunAt = Date.now() + intervalMs;
            ib_autoTimer = setInterval(() => {
                runBalanceAutoCycle();
                ib_autoNextRunAt = Date.now() + intervalMs;
            }, intervalMs);
            startBalanceCountdown();
        }

        function stopBalanceAutoSend() {
            if (ib_autoTimer) {
                clearInterval(ib_autoTimer);
                ib_autoTimer = null;
            }
            stopBalanceCountdown();
        }

        function startBalanceCountdown() {
            if (ib_countdownTimer) clearInterval(ib_countdownTimer);
            updateBalanceCountdownDisplay();
            ib_countdownTimer = setInterval(updateBalanceCountdownDisplay, 1000);
        }

        function stopBalanceCountdown() {
            if (ib_countdownTimer) {
                clearInterval(ib_countdownTimer);
                ib_countdownTimer = null;
            }
            $("#ib_auto_countdown").text("");
        }

        function updateBalanceCountdownDisplay() {
            let remainingMs = Math.max(0, ib_autoNextRunAt - Date.now());
            let totalSeconds = Math.floor(remainingMs / 1000);
            let mm = Math.floor(totalSeconds / 60);
            let ss = totalSeconds % 60;
            $("#ib_auto_countdown").text(`nächster Versand in ${mm}:${ss.toString().padStart(2, "0")}`);
        }

        async function runBalanceAutoCycle() {
            try {
                await balancingResources();
                await new Promise(r => setTimeout(r, 1500));
                await autoClickAllBalanceSends();
            } catch (error) {
                console.error("IntelliBalance auto-send: error during cycle", error);
            }
        }

        async function autoClickAllBalanceSends() {
            let buttons = $("#ib_table_view").find(".ib_btn_send").toArray();
            for (let btn of buttons) {
                if (!document.body.contains(btn)) continue;
                if ($(btn).is(":disabled")) continue;
                $(btn).trigger("click");
                await new Promise(r => setTimeout(r, 500));
            }
        }

        async function fetchGroupsForBalance() {
            let fetchGroupsUrl = '';
            if (game_data.player.sitter > 0) fetchGroupsUrl = game_data.link_base_pure + `groups&mode=overview&ajax=load_group_menu&t=${game_data.player.id}`;
            else fetchGroupsUrl = game_data.link_base_pure + 'groups&mode=overview&ajax=load_group_menu';
            try {
                return await $.get(fetchGroupsUrl);
            } catch (error) {
                console.error("IntelliBalance: could not fetch village groups", error);
                return { result: {} };
            }
        }

        async function populateGroupFilter(container) {
            let groups = await fetchGroupsForBalance();
            let $select = $(container).find("#ib_group_filter");
            if (groups && groups.result) {
                Object.values(groups.result).forEach(group => {
                    if (group.name !== undefined) {
                        $select.append(`<option value="${group.group_id}">${group.name}</option>`);
                    }
                });
            }
            let savedGroupId = localStorage.getItem(game_data.world + "ib_group_filter");
            if (savedGroupId != null && $select.find(`option[value="${savedGroupId}"]`).length > 0) {
                $select.val(savedGroupId);
            }
            $select.off("change").on("change", function () {
                localStorage.setItem(game_data.world + "ib_group_filter", $(this).val());
            });
        }

        async function balancingResources() {
            let time_construction_total = parseFloat($("#ib_time_construction").val());
            let averageFactor = parseFloat($("#ib_nr_average_factor").val());
            let reserveMerchants = parseInt($("#ib_nr_merchants_reserve").val());
            let merchantCapacity = parseInt($("#ib_merchant_capacity").val());
            let groupId = parseInt($("#ib_group_filter").val()) || 0;
            let nrClusters = parseInt($("#ib_nr_clusters").val());
            let maxConstruction = $("#ib_max_construction").is(":checked");

            reserveMerchants = (Number.isNaN(reserveMerchants) == true || reserveMerchants < 0) ? 0 : reserveMerchants;
            nrClusters = (Number.isNaN(nrClusters) == true || nrClusters < 1) ? 1 : nrClusters;
            time_construction_total = (Number.isNaN(time_construction_total) == true || time_construction_total < 0) ? 20 : time_construction_total;
            time_construction_total = (time_construction_total > 50) ? 50 : time_construction_total;
            averageFactor = (Number.isNaN(averageFactor) == true) ? 1 : (averageFactor < 0) ? 0 : (averageFactor > 1) ? 1 : averageFactor;
            merchantCapacity = (Number.isNaN(merchantCapacity) == true) ? 1000 : (merchantCapacity < 1000) ? 1000 : (merchantCapacity > 1500) ? 1500 : merchantCapacity;

            $("#ib_btn_start").prop("disabled", true).val("working...");
            $("#ib_tables").hide();

            let { list_production, map_farm_usage } = await getDataProduction(groupId).catch(err => alert(err));
            let map_incoming = await getDataIncoming(groupId).catch(err => alert(err));
            let map_resources_get_AM_data = await getResourcesForAM(map_farm_usage).catch(err => alert(err));
            let list_production_home = JSON.parse(JSON.stringify(list_production));

            let map_resources_get_AM;
            if (time_construction_total > 0)
                map_resources_get_AM = map_resources_get_AM_data[time_construction_total - 1];
            else
                map_resources_get_AM = new Map();

            let start = new Date().getTime();

            let kmeans_coords = [];
            for (let i = 0; i < list_production.length; i++) {
                kmeans_coords.push([
                    parseInt(list_production[i].coord.split("|")[0]),
                    parseInt(list_production[i].coord.split("|")[1])
                ]);
            }
            let options = { numberOfClusters: nrClusters, maxIterations: 100 };
            let clusters = getClusters(kmeans_coords, options);

            let list_production_cluster = [];
            let list_production_home_cluster = [];
            let map_draw_on_map = new Map();

            for (let i = 0; i < clusters.length; i++) {
                let list_coords = clusters[i].data;
                let list_prod = [], list_prod_home = [];
                for (let j = 0; j < list_coords.length; j++) {
                    let coord = list_coords[j].join("|");
                    for (let k = 0; k < list_production.length; k++) {
                        if (list_production[k].coord == coord) {
                            list_prod.push(list_production[k]);
                            list_prod_home.push(list_production_home[k]);

                            let total_resources_get = 0;
                            if (map_incoming.has(coord)) {
                                total_resources_get = map_incoming.get(coord).wood + map_incoming.get(coord).stone + map_incoming.get(coord).iron;
                            }
                            map_draw_on_map.set(list_production[k].id, {
                                label_cluster: i,
                                villageId: list_production[k].id,
                                total_resources_get: total_resources_get,
                                total_resources_send: 0
                            });
                            break;
                        }
                    }
                }
                list_production_cluster.push(list_prod);
                list_production_home_cluster.push(list_prod_home);
            }

            let total_wood_home = 0, total_stone_home = 0, total_iron_home = 0;
            let avg_wood_total = 0, avg_stone_total = 0, avg_iron_total = 0;

            for (let i = 0; i < list_production.length; i++) {
                let coord = list_production[i].coord;
                if (map_incoming.has(coord)) {
                    list_production[i].wood += map_incoming.get(coord).wood;
                    list_production[i].stone += map_incoming.get(coord).stone;
                    list_production[i].iron += map_incoming.get(coord).iron;

                    list_production[i].wood = Math.min(list_production[i].wood, list_production[i].capacity);
                    list_production[i].stone = Math.min(list_production[i].stone, list_production[i].capacity);
                    list_production[i].iron = Math.min(list_production[i].iron, list_production[i].capacity);
                }
                avg_wood_total += list_production[i].wood / list_production.length;
                avg_stone_total += list_production[i].stone / list_production.length;
                avg_iron_total += list_production[i].iron / list_production.length;

                total_wood_home += list_production[i].wood;
                total_stone_home += list_production[i].stone;
                total_iron_home += list_production[i].iron;
            }

            let list_launches, list_clusters_stats;
            let total_wood_send_stats, total_stone_send_stats, total_iron_send_stats;
            let total_wood_get_stats, total_stone_get_stats, total_iron_get_stats;
            let constructionTimeCalculated = 0;

            if (maxConstruction == false || averageFactor > 0.5) {
                let launchesData = calculateLaunches(list_production_cluster, list_production_home_cluster, map_resources_get_AM, clusters, averageFactor, reserveMerchants, merchantCapacity);
                ({ list_launches, list_clusters_stats, total_wood_send_stats, total_stone_send_stats, total_iron_send_stats, total_wood_get_stats, total_stone_get_stats, total_iron_get_stats } = launchesData);
            } else {
                let map_resources_get_AM_local = map_resources_get_AM_data[0];
                let launchesData = calculateLaunches(list_production_cluster, list_production_home_cluster, map_resources_get_AM_local, clusters, averageFactor, reserveMerchants, merchantCapacity);
                ({ list_launches, list_clusters_stats, total_wood_send_stats, total_stone_send_stats, total_iron_send_stats, total_wood_get_stats, total_stone_get_stats, total_iron_get_stats } = launchesData);

                let count = 1;
                let maxConstructionLimit = 100;
                while (count < maxConstructionLimit) {
                    map_resources_get_AM_local = map_resources_get_AM_data[count];
                    launchesData = calculateLaunches(list_production_cluster, list_production_home_cluster, map_resources_get_AM_local, clusters, averageFactor, reserveMerchants, merchantCapacity);
                    let stats = launchesData.list_clusters_stats;
                    let notEnoughRes = false;
                    for (let i = 0; i < stats.length; i++) {
                        if (stats[i].total_iron_get > stats[i].total_iron_send ||
                            stats[i].total_stone_get > stats[i].total_stone_send ||
                            stats[i].total_wood_get > stats[i].total_wood_send) {
                            notEnoughRes = true;
                            break;
                        }
                    }
                    if (notEnoughRes) {
                        constructionTimeCalculated = count;
                        break;
                    }
                    if (count == maxConstructionLimit - 1) {
                        constructionTimeCalculated = count;
                    }
                    ({ list_launches, list_clusters_stats, total_wood_send_stats, total_stone_send_stats, total_iron_send_stats, total_wood_get_stats, total_stone_get_stats, total_iron_get_stats } = launchesData);
                    count++;
                }
            }

            list_clusters_stats.sort((o1, o2) => (o1.max_distance > o2.max_distance) ? -1 : (o1.max_distance < o2.max_distance) ? 1 : 0);

            let map_nr_merchants = new Map();
            for (let i = 0; i < list_launches.length; i++) {
                let nr_merchants = list_launches[i].wood + list_launches[i].stone + list_launches[i].iron;
                nr_merchants = Math.ceil(nr_merchants / merchantCapacity);
                if (map_nr_merchants.has(list_launches[i].coord_origin)) {
                    let nr_update = map_nr_merchants.get(list_launches[i].coord_origin);
                    map_nr_merchants.set(list_launches[i].coord_origin, nr_merchants + nr_update);
                } else {
                    map_nr_merchants.set(list_launches[i].coord_origin, nr_merchants);
                }
            }
            for (let i = 0; i < list_production.length; i++) {
                let nr_merchants = 0;
                if (map_nr_merchants.get(list_production[i].coord)) nr_merchants = map_nr_merchants.get(list_production[i].coord);
                list_production[i].merchantAvailable = list_production[i].merchants - nr_merchants;
            }

            let obj_stats = {};
            obj_stats.avg_wood = Math.round(avg_wood_total);
            obj_stats.avg_stone = Math.round(avg_stone_total);
            obj_stats.avg_iron = Math.round(avg_iron_total);
            obj_stats.total_wood_send = Math.round(total_wood_send_stats);
            obj_stats.total_stone_send = Math.round(total_stone_send_stats);
            obj_stats.total_iron_send = Math.round(total_iron_send_stats);
            obj_stats.total_wood_get = Math.round(total_wood_get_stats);
            obj_stats.total_stone_get = Math.round(total_stone_get_stats);
            obj_stats.total_iron_get = Math.round(total_iron_get_stats);
            obj_stats.total_wood_home = Math.round(total_wood_home);
            obj_stats.total_stone_home = Math.round(total_stone_home);
            obj_stats.total_iron_home = Math.round(total_iron_home);

            for (let i = 0; i < list_production.length; i++) {
                for (let j = 0; j < list_launches.length; j++) {
                    if (list_production[i].coord == list_launches[j].coord_destination) {
                        list_production[i].wood += list_launches[j].wood;
                        list_production[i].stone += list_launches[j].stone;
                        list_production[i].iron += list_launches[j].iron;
                    } else if (list_production[i].coord == list_launches[j].coord_origin) {
                        list_production[i].wood -= list_launches[j].wood;
                        list_production[i].stone -= list_launches[j].stone;
                        list_production[i].iron -= list_launches[j].iron;
                    }
                    list_production[i].result_wood = list_production[i].wood - Math.round(avg_wood_total);
                    list_production[i].result_stone = list_production[i].stone - Math.round(avg_stone_total);
                    list_production[i].result_iron = list_production[i].iron - Math.round(avg_iron_total);
                    list_production[i].result_total = list_production[i].result_wood + list_production[i].result_stone + list_production[i].result_iron;
                }
            }
            list_production.sort((o1, o2) => (o1.result_total > o2.result_total) ? 1 : (o1.result_total < o2.result_total) ? -1 : 0);

            let map_launches_mass = new Map();
            for (let i = 0; i < list_launches.length; i++) {
                let target_id = list_launches[i].id_destination;
                let origin_id = list_launches[i].id_origin;
                let woodKey = `resource[${origin_id}][wood]`;
                let stoneKey = `resource[${origin_id}][stone]`;
                let ironKey = `resource[${origin_id}][iron]`;
                let send_resources = {};

                if (map_launches_mass.has(target_id)) {
                    let obj_update = map_launches_mass.get(target_id);
                    obj_update.send_resources[woodKey] = list_launches[i].wood;
                    obj_update.send_resources[stoneKey] = list_launches[i].stone;
                    obj_update.send_resources[ironKey] = list_launches[i].iron;
                    obj_update.total_send += list_launches[i].total_send;
                    obj_update.total_wood += list_launches[i].wood;
                    obj_update.total_stone += list_launches[i].stone;
                    obj_update.total_iron += list_launches[i].iron;
                    obj_update.distance = Math.max(obj_update.distance, list_launches[i].distance);
                    map_launches_mass.set(target_id, obj_update);
                } else {
                    send_resources[woodKey] = list_launches[i].wood;
                    send_resources[stoneKey] = list_launches[i].stone;
                    send_resources[ironKey] = list_launches[i].iron;
                    map_launches_mass.set(target_id, {
                        target_id: target_id,
                        coord_destination: list_launches[i].coord_destination,
                        name_destination: list_launches[i].name_destination,
                        send_resources: send_resources,
                        total_send: list_launches[i].total_send,
                        total_wood: list_launches[i].wood,
                        total_stone: list_launches[i].stone,
                        total_iron: list_launches[i].iron,
                        distance: list_launches[i].distance
                    });
                }

                if (map_draw_on_map.has(target_id)) {
                    let obj_update = map_draw_on_map.get(target_id);
                    obj_update.total_resources_get += list_launches[i].wood + list_launches[i].stone + list_launches[i].iron;
                    map_draw_on_map.set(target_id, obj_update);
                }
                if (map_draw_on_map.has(origin_id)) {
                    let obj_update = map_draw_on_map.get(origin_id);
                    obj_update.total_resources_send += list_launches[i].wood + list_launches[i].stone + list_launches[i].iron;
                    map_draw_on_map.set(origin_id, obj_update);
                }
            }

            let list_launches_mass = Array.from(map_launches_mass.entries()).map(e => e[1]);
            list_launches_mass.sort((o1, o2) => (o1.total_send > o2.total_send) ? -1 : (o1.total_send < o2.total_send) ? 1 : 0);

            let stop = new Date().getTime();
            console.log("IntelliBalance time process: " + (stop - start));

            $("#ib_btn_start").prop("disabled", false).val("start");
            $("#ib_tables").show();
            createTable(list_launches_mass, obj_stats, list_production, list_clusters_stats);
            if (constructionTimeCalculated) {
                $("#ib_time_construction").val(constructionTimeCalculated);
            }

            if (typeof (TWMap) != 'undefined') {
                if (document.getElementById("map_container")) document.getElementById("map_container").remove();
                TWMap.mapHandler.spawnSector = IR_originalSpawnSector;

                let random_color = [];
                for (let i = 0; i < clusters.length; i++) {
                    random_color.push(getRandomColor(0.2));
                }
                addInfoOnMap(map_draw_on_map, random_color);
                TWMap.init();
            }
        }

        function calculateLaunches(list_production_cluster, list_production_home_cluster, map_resources_get_AM, clusters, averageFactor, reserveMerchants, merchantCapacity) {
            let list_launches = [];
            let list_clusters_stats = [];
            let total_wood_send_stats = 0, total_stone_send_stats = 0, total_iron_send_stats = 0;
            let total_wood_get_stats = 0, total_stone_get_stats = 0, total_iron_get_stats = 0;

            for (let i = 0; i < list_production_cluster.length; i++) {
                let list_prod = list_production_cluster[i];
                let list_prod_home = list_production_home_cluster[i];

                let avg_wood = 0, avg_stone = 0, avg_iron = 0;
                let avg_wood_factor = 0, avg_stone_factor = 0, avg_iron_factor = 0;
                let total_wood_send = 0, total_stone_send = 0, total_iron_send = 0;
                let total_wood_get = 0, total_stone_get = 0, total_iron_get = 0;
                let list_res_send = [], list_res_get = [];
                let total_wood_cluster = 0, total_stone_cluster = 0, total_iron_cluster = 0;

                for (let j = 0; j < list_prod.length; j++) {
                    avg_wood += list_prod[j].wood / list_prod.length;
                    avg_stone += list_prod[j].stone / list_prod.length;
                    avg_iron += list_prod[j].iron / list_prod.length;
                    total_wood_cluster += list_prod[j].wood;
                    total_stone_cluster += list_prod[j].stone;
                    total_iron_cluster += list_prod[j].iron;
                }

                avg_wood_factor = avg_wood * averageFactor;
                avg_stone_factor = avg_stone * averageFactor;
                avg_iron_factor = avg_iron * averageFactor;

                for (let j = 0; j < list_prod.length; j++) {
                    let coord = list_prod[j].coord;
                    let name = list_prod[j].name;
                    let id = list_prod[j].id;
                    let merchants = list_prod[j].merchants;
                    merchants -= reserveMerchants;

                    let capacity = list_prod[j].capacity * 0.95;
                    let capacity_travel = merchants * merchantCapacity;

                    let avg_wood_res = avg_wood_factor;
                    let avg_stone_res = avg_stone_factor;
                    let avg_iron_res = avg_iron_factor;

                    if (map_resources_get_AM.has(list_prod[j].coord)) {
                        let obj_res_AM = map_resources_get_AM.get(list_prod[j].coord);
                        avg_wood_res += obj_res_AM.total_wood;
                        avg_stone_res += obj_res_AM.total_stone;
                        avg_iron_res += obj_res_AM.total_iron;
                        list_prod[j].time_finished = obj_res_AM.time_finished;
                    } else {
                        list_prod[j].time_finished = 0;
                    }

                    let diff_wood = list_prod[j].wood - Math.round(avg_wood_res);
                    let diff_stone = list_prod[j].stone - Math.round(avg_stone_res);
                    let diff_iron = list_prod[j].iron - Math.round(avg_iron_res);

                    diff_wood = (diff_wood < 0) ? diff_wood : (list_prod_home[j].wood - diff_wood > 0) ? diff_wood : (list_prod_home[j].wood);
                    diff_stone = (diff_stone < 0) ? diff_stone : (list_prod_home[j].stone - diff_stone > 0) ? diff_stone : (list_prod_home[j].stone);
                    diff_iron = (diff_iron < 0) ? diff_iron : (list_prod_home[j].iron - diff_iron > 0) ? diff_iron : (list_prod_home[j].iron);

                    let total_res_available = 0;
                    total_res_available = (diff_wood > 0) ? total_res_available + diff_wood : total_res_available;
                    total_res_available = (diff_stone > 0) ? total_res_available + diff_stone : total_res_available;
                    total_res_available = (diff_iron > 0) ? total_res_available + diff_iron : total_res_available;

                    let norm_factor = (capacity_travel <= total_res_available) ? capacity_travel / total_res_available : 1;
                    let send_wood = 0, send_stone = 0, send_iron = 0;
                    let get_wood = 0, get_stone = 0, get_iron = 0;

                    send_wood = (diff_wood > 0) ? parseInt(diff_wood * norm_factor) : send_wood;
                    send_stone = (diff_stone > 0) ? parseInt(diff_stone * norm_factor) : send_stone;
                    send_iron = (diff_iron > 0) ? parseInt(diff_iron * norm_factor) : send_iron;

                    get_wood = (diff_wood > 0) ? get_wood : (list_prod[j].wood + Math.abs(diff_wood) < capacity) ? Math.abs(diff_wood) : capacity - list_prod[j].wood;
                    get_stone = (diff_stone > 0) ? get_stone : (list_prod[j].stone + Math.abs(diff_stone) < capacity) ? Math.abs(diff_stone) : capacity - list_prod[j].stone;
                    get_iron = (diff_iron > 0) ? get_iron : (list_prod[j].iron + Math.abs(diff_iron) < capacity) ? Math.abs(diff_iron) : capacity - list_prod[j].iron;

                    total_wood_send += send_wood;
                    total_stone_send += send_stone;
                    total_iron_send += send_iron;
                    total_wood_get += get_wood;
                    total_stone_get += get_stone;
                    total_iron_get += get_iron;

                    let obj_send = { coord: coord, id: id, name: name };
                    let obj_get = { coord: coord, id: id, name: name };

                    obj_send.wood = (send_wood > 0) ? send_wood : 0;
                    obj_send.stone = (send_stone > 0) ? send_stone : 0;
                    obj_send.iron = (send_iron > 0) ? send_iron : 0;
                    if (obj_send.wood > 0 || obj_send.stone > 0 || obj_send.iron > 0) list_res_send.push(obj_send);

                    obj_get.wood = (get_wood > 0) ? parseInt(get_wood) : 0;
                    obj_get.stone = (get_stone > 0) ? parseInt(get_stone) : 0;
                    obj_get.iron = (get_iron > 0) ? parseInt(get_iron) : 0;
                    if (obj_get.wood > 0 || obj_get.stone > 0 || obj_get.iron > 0) list_res_get.push(obj_get);
                }

                let norm_wood = (total_wood_get > total_wood_send) ? (total_wood_send / total_wood_get) : 1;
                let norm_stone = (total_stone_get > total_stone_send) ? (total_stone_send / total_stone_get) : 1;
                let norm_iron = (total_iron_get > total_iron_send) ? (total_iron_send / total_iron_get) : 1;

                for (let j = 0; j < list_res_get.length; j++) {
                    list_res_get[j].wood = parseInt(list_res_get[j].wood * norm_wood);
                    list_res_get[j].stone = parseInt(list_res_get[j].stone * norm_stone);
                    list_res_get[j].iron = parseInt(list_res_get[j].iron * norm_iron);
                }

                let list_maxDistance = [];
                for (let j = 0; j < list_res_get.length; j++) {
                    let coord_destination = list_res_get[j].coord;
                    let id_destination = list_res_get[j].id;
                    let name_destination = list_res_get[j].name;

                    let max_distance = 0;
                    for (let k = 0; k < list_res_send.length; k++) {
                        let distance = calcDistance(list_res_get[j].coord, list_res_send[k].coord);
                        list_res_send[k].distance = distance;
                        max_distance = (max_distance > distance) ? max_distance : distance;
                    }
                    list_res_send.sort((o1, o2) => (o1.distance > o2.distance) ? 1 : (o1.distance < o2.distance) ? -1 : 0);

                    let obj_launch = { wood: 0, stone: 0, iron: 0 };

                    for (let k = 0; k < list_res_send.length; k++) {
                        let coord_origin = list_res_send[k].coord;
                        let id_origin = list_res_send[k].id;
                        let name_origin = list_res_send[k].name;

                        let send_wood = (list_res_send[k].wood > 0) ? Math.min(list_res_get[j].wood, list_res_send[k].wood) : 0;
                        let send_stone = (list_res_send[k].stone > 0) ? Math.min(list_res_get[j].stone, list_res_send[k].stone) : 0;
                        let send_iron = (list_res_send[k].iron > 0) ? Math.min(list_res_get[j].iron, list_res_send[k].iron) : 0;

                        obj_launch.wood += send_wood;
                        obj_launch.stone += send_stone;
                        obj_launch.iron += send_iron;

                        list_res_get[j].wood -= send_wood;
                        list_res_get[j].stone -= send_stone;
                        list_res_get[j].iron -= send_iron;

                        list_res_send[k].wood -= send_wood;
                        list_res_send[k].stone -= send_stone;
                        list_res_send[k].iron -= send_iron;

                        let total_send = send_wood + send_stone + send_iron;

                        let restDivision = total_send % merchantCapacity;
                        let minim_resources = (merchantCapacity == 1000) ? 700 : 1200;
                        if (restDivision < minim_resources) {
                            if (send_wood > restDivision) { send_wood -= restDivision; total_send -= restDivision; }
                            else if (send_stone > restDivision) { send_stone -= restDivision; total_send -= restDivision; }
                            else if (send_iron > restDivision) { send_iron -= restDivision; total_send -= restDivision; }
                        }

                        list_maxDistance.push(list_res_send[k].distance);

                        if (total_send >= minim_resources)
                            list_launches.push({
                                total_send: total_send, wood: send_wood, stone: send_stone, iron: send_iron,
                                coord_origin: coord_origin, name_origin: name_origin, id_destination: id_destination,
                                id_origin: id_origin, coord_destination: coord_destination, name_destination: name_destination,
                                distance: list_res_send[k].distance
                            });

                        let total_get = list_res_get[j].wood + list_res_get[j].stone + list_res_get[j].iron;
                        if (total_get < minim_resources) break;
                    }
                }

                total_wood_send_stats += total_wood_send;
                total_stone_send_stats += total_stone_send;
                total_iron_send_stats += total_iron_send;
                total_wood_get_stats += total_wood_get;
                total_stone_get_stats += total_stone_get;
                total_iron_get_stats += total_iron_get;

                let max_distance = 0;
                for (let j = 0; j < list_maxDistance.length; j++) {
                    if (max_distance < list_maxDistance[j]) max_distance = list_maxDistance[j];
                }

                list_clusters_stats.push({
                    nr_coords: clusters[i].data.length,
                    center: parseInt(clusters[i].mean[0]) + "|" + parseInt(clusters[i].mean[1]),
                    max_distance: max_distance,
                    avg_wood: Math.round(avg_wood), avg_stone: Math.round(avg_stone), avg_iron: Math.round(avg_iron),
                    total_wood_send: total_wood_send, total_stone_send: total_stone_send, total_iron_send: total_iron_send,
                    total_wood_get: total_wood_get, total_stone_get: total_stone_get, total_iron_get: total_iron_get,
                    total_wood_cluster: total_wood_cluster, total_stone_cluster: total_stone_cluster, total_iron_cluster: total_iron_cluster
                });
            }

            return {
                list_clusters_stats: list_clusters_stats, list_launches: list_launches,
                total_wood_send_stats: total_wood_send_stats, total_stone_send_stats: total_stone_send_stats, total_iron_send_stats: total_iron_send_stats,
                total_wood_get_stats: total_wood_get_stats, total_stone_get_stats: total_stone_get_stats, total_iron_get_stats: total_iron_get_stats
            };
        }

        function getDataProduction(groupId) {
            return new Promise((resolve, reject) => {
                let link_combined_production = game_data.link_base_pure + "overview_villages&mode=prod";
                if (groupId) link_combined_production += `&group=${groupId}`;
                let dataPage = httpGet(link_combined_production);
                const parser = new DOMParser();
                const htmlDoc = parser.parseFromString(dataPage, 'text/html');
                let list_pages = [];

                if ($(htmlDoc).find(".paged-nav-item").parent().find("select").length > 0) {
                    Array.from($(htmlDoc).find(".paged-nav-item").parent().find("select").find("option")).forEach(function (item) {
                        list_pages.push(item.value);
                    });
                    list_pages.pop();
                } else if (htmlDoc.getElementsByClassName("paged-nav-item").length > 0) {
                    let nr = 0;
                    Array.from(htmlDoc.getElementsByClassName("paged-nav-item")).forEach(function (item) {
                        let current = item.href;
                        current = current.split("page=")[0] + "page=" + nr;
                        nr++;
                        list_pages.push(current);
                    });
                } else {
                    list_pages.push(link_combined_production);
                }
                list_pages = list_pages.reverse();

                let list_production = [];
                let map_farm_usage = new Map();
                function ajaxRequest(urls) {
                    let current_url;
                    if (urls.length > 0) current_url = urls.pop(); else current_url = "stop";
                    let start_ajax = new Date().getTime();
                    if (urls.length >= 0 && current_url != "stop") {
                        $.ajax({
                            url: current_url, method: 'get',
                            success: (data) => {
                                const parser = new DOMParser();
                                const htmlDoc = parser.parseFromString(data, 'text/html');

                                if (game_data.device == "desktop") {
                                    let table_production = Array.from($(htmlDoc).find(".row_a, .row_b"));
                                    for (let i = 0; i < table_production.length; i++) {
                                        let name = table_production[i].getElementsByClassName("quickedit-vn")[0].innerText;
                                        let coord = table_production[i].getElementsByClassName("quickedit-vn")[0].innerText.match(/[0-9]{3}\|[0-9]{3}/)[0];
                                        let id = table_production[i].getElementsByClassName("quickedit-vn")[0].getAttribute("data-id");
                                        let wood = parseInt(table_production[i].getElementsByClassName("wood")[0].innerText.replace(".", ""));
                                        let stone = parseInt(table_production[i].getElementsByClassName("stone")[0].innerText.replace(".", ""));
                                        let iron = parseInt(table_production[i].getElementsByClassName("iron")[0].innerText.replace(".", ""));
                                        let merchants = parseInt(table_production[i].querySelector("a[href*='market']").innerText.split("/")[0]);
                                        let merchants_total = parseInt(table_production[i].querySelector("a[href*='market']").innerText.split("/")[1]);
                                        let capacity = parseInt(table_production[i].children[4].innerText);
                                        let points = parseInt(table_production[i].children[2].innerText.replace(".", ""));
                                        let farm_current_pop = parseInt(table_production[i].children[6].innerText.split("/")[0]);
                                        let farm_total_pop = parseInt(table_production[i].children[6].innerText.split("/")[1]);
                                        let farm_usage = farm_current_pop / farm_total_pop;

                                        list_production.push({ coord, id, wood, stone, iron, name: name.trim(), merchants, merchants_total, capacity, points });
                                        map_farm_usage.set(coord, farm_usage);
                                    }
                                } else {
                                    let table_production = Array.from($(htmlDoc).find(".overview-container").find(".overview-container-item"));
                                    for (let i = 0; i < table_production.length; i++) {
                                        let name = $(table_production[i]).find(".quickedit-label").text().trim();
                                        let coord = name.match(/\d+\|\d+/)[0];
                                        let id = $(table_production[i]).find(".quickedit-vn").attr("data-id");
                                        let wood = parseInt(table_production[i].getElementsByClassName("mwood")[0].innerText.replace(".", ""));
                                        let stone = parseInt(table_production[i].getElementsByClassName("mstone")[0].innerText.replace(".", ""));
                                        let iron = parseInt(table_production[i].getElementsByClassName("miron")[0].innerText.replace(".", ""));
                                        let merchants = parseInt($(table_production[i]).find(".vertical_center").text().trim());
                                        let merchants_total = 500;
                                        let capacity = parseInt(table_production[i].getElementsByClassName("ressources")[0].parentElement.innerText);
                                        let points = parseInt($(table_production[i]).find(".grey").parent().text().replace(".", ""));
                                        let farm_current_pop = parseInt(table_production[i].getElementsByClassName("population")[0].parentElement.innerText.split("/")[0]);
                                        let farm_total_pop = parseInt(table_production[i].getElementsByClassName("population")[0].parentElement.innerText.split("/")[1]);
                                        let farm_usage = farm_current_pop / farm_total_pop;

                                        list_production.push({ coord, id, wood, stone, iron, name, merchants, merchants_total, capacity, points });
                                        map_farm_usage.set(coord, farm_usage);
                                    }
                                }

                                let stop_ajax = new Date().getTime();
                                let diff = stop_ajax - start_ajax;
                                window.setTimeout(function () {
                                    ajaxRequest(list_pages);
                                    UI.SuccessMessage("get production page: " + urls.length);
                                }, 200 - diff);
                            },
                            error: (err) => { reject(err); }
                        });
                    } else {
                        UI.SuccessMessage("done");
                        resolve({ list_production, map_farm_usage });
                    }
                }
                ajaxRequest(list_pages);
            });
        }

        function getDataIncoming(groupId) {
            return new Promise((resolve, reject) => {
                let link_combined_production = game_data.link_base_pure + "overview_villages&mode=trader&type=inc";
                if (groupId) link_combined_production += `&group=${groupId}`;
                let dataPage = httpGet(link_combined_production);
                const parser = new DOMParser();
                const htmlDoc = parser.parseFromString(dataPage, 'text/html');
                let list_pages = [];

                if ($(htmlDoc).find(".paged-nav-item").parent().find("select").length > 0) {
                    Array.from($(htmlDoc).find(".paged-nav-item").parent().find("select").find("option")).forEach(function (item) {
                        list_pages.push(item.value);
                    });
                    list_pages.pop();
                } else if (htmlDoc.getElementsByClassName("paged-nav-item").length > 0) {
                    let nr = 0;
                    Array.from(htmlDoc.getElementsByClassName("paged-nav-item")).forEach(function (item) {
                        let current = item.href;
                        current = current.split("page=")[0] + "page=" + nr;
                        nr++;
                        list_pages.push(current);
                    });
                } else {
                    list_pages.push(link_combined_production);
                }
                list_pages = list_pages.reverse();

                let map_incoming = new Map();
                function ajaxRequest(urls) {
                    let current_url;
                    if (urls.length > 0) current_url = urls.pop(); else current_url = "stop";
                    let start_ajax = new Date().getTime();
                    if (urls.length >= 0 && current_url != "stop") {
                        $.ajax({
                            url: current_url, method: 'get',
                            success: (data) => {
                                const parser = new DOMParser();
                                const htmlDoc = parser.parseFromString(data, 'text/html');
                                let table_incoming = Array.from($(htmlDoc).find(".row_a, .row_b"));

                                for (let i = 0; i < table_incoming.length; i++) {
                                    let coord = "";
                                    if (game_data.device == "desktop") {
                                        coord = table_incoming[i].children[4].innerText.match(/[0-9]{3}\|[0-9]{3}/)[0];
                                    } else {
                                        coord = table_incoming[i].children[3].innerText.match(/[0-9]{3}\|[0-9]{3}/g)[1];
                                    }
                                    let wood = parseInt($(table_incoming[i]).find(".wood").parent().text().replace(".", ""));
                                    let stone = parseInt($(table_incoming[i]).find(".stone").parent().text().replace(".", ""));
                                    let iron = parseInt($(table_incoming[i]).find(".iron").parent().text().replace(".", ""));
                                    wood = (Number.isNaN(wood) == true) ? 0 : wood;
                                    stone = (Number.isNaN(stone) == true) ? 0 : stone;
                                    iron = (Number.isNaN(iron) == true) ? 0 : iron;

                                    if (map_incoming.has(coord)) {
                                        let obj_update = map_incoming.get(coord);
                                        obj_update.wood += wood; obj_update.stone += stone; obj_update.iron += iron;
                                        map_incoming.set(coord, obj_update);
                                    } else {
                                        map_incoming.set(coord, { wood, stone, iron });
                                    }
                                }
                                let stop_ajax = new Date().getTime();
                                let diff = stop_ajax - start_ajax;
                                window.setTimeout(function () {
                                    ajaxRequest(list_pages);
                                    UI.SuccessMessage("get incoming page: " + urls.length);
                                }, 200 - diff);
                            },
                            error: (err) => { reject(err); }
                        });
                    } else {
                        UI.SuccessMessage("done");
                        resolve(map_incoming);
                    }
                }
                ajaxRequest(list_pages);
            });
        }

        function httpGet(theUrl) {
            var xmlHttp = new XMLHttpRequest();
            xmlHttp.open("GET", theUrl, false);
            xmlHttp.send(null);
            return xmlHttp.responseText;
        }

        function calcDistance(coord1, coord2) {
            let x1 = parseInt(coord1.split("|")[0]);
            let y1 = parseInt(coord1.split("|")[1]);
            let x2 = parseInt(coord2.split("|")[0]);
            let y2 = parseInt(coord2.split("|")[1]);
            return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
        }

        function createTable(list_launches, obj_stats, list_production, list_clusters_stats) {
            let html_prod_table = `
                <table class="scriptTableAlternate">
                <tr>
                    <td style="width:3%">nr</td>
                    <td style="width:35%">target</td>
                    <td><a href="#" id="ib_sort_distance"><font color="${textColor}">max distance</font></a></td>
                    <td><a href="#" id="ib_sort_total"><font color="${textColor}">total send</font></a></td>
                    <td class="hide_mobile"><a href="#" id="ib_sort_wood"><img src="https://dsen.innogamescdn.com/asset/c2e59f13/graphic/buildings/wood.png"/></a></td>
                    <td class="hide_mobile"><a href="#" id="ib_sort_stone"><img src="https://dsen.innogamescdn.com/asset/c2e59f13/graphic/buildings/stone.png"/></a></td>
                    <td class="hide_mobile"><a href="#" id="ib_sort_iron"><img src="https://dsen.innogamescdn.com/asset/c2e59f13/graphic/buildings/iron.png"/></a></td>
                    <td>send</td>
                </tr>`;

            for (let i = 0; i < list_launches.length; i++) {
                let target_id = list_launches[i].target_id;
                let wood = list_launches[i].total_wood;
                let stone = list_launches[i].total_stone;
                let iron = list_launches[i].total_iron;
                let data = JSON.stringify(list_launches[i].send_resources);

                html_prod_table += `
                    <tr class="ib_delete_row">
                        <td>${i + 1}</td>
                        <td><a href="${game_data.link_base_pure}info_village&id=${list_launches[i].target_id}"><font color="${textColor}">${list_launches[i].name_destination}</font></a></td>
                        <td>${list_launches[i].distance.toFixed(1)}</td>
                        <td>${formatNumber(list_launches[i].total_send)}</td>
                        <td class="hide_mobile">${formatNumber(wood)}</td>
                        <td class="hide_mobile">${formatNumber(stone)}</td>
                        <td class="hide_mobile">${formatNumber(iron)}</td>
                        <td><input class="btn evt-confirm-btn btn-confirm-yes ib_btn_send" target_id="${target_id}" data='${data}' type="button" value="send"></td>
                    </tr>`;
            }
            html_prod_table += `</table>`;
            document.getElementById("ib_table_view").innerHTML = html_prod_table;

            if (game_data.device != "desktop") $(".hide_mobile").hide();

            $(".ib_btn_send").off("click").on("click", async (event) => {
                if ($(event.target).is(":disabled") == false) {
                    let target_id = $(event.target).attr("target_id");
                    let data = JSON.parse($(event.target).attr("data"));
                    $(".ib_btn_send").attr("disabled", true);
                    let start = new Date().getTime();
                    sendResources(target_id, data);
                    let stop = new Date().getTime();
                    let diff_time = stop - start;
                    window.setTimeout(() => {
                        $(event.target).closest(".ib_delete_row").remove();
                        $(".ib_btn_send").attr("disabled", false);
                    }, 200 - diff_time);
                }
            });

            let html_stats_table = `
                <table id="ib_table_stats_inner" class="scriptTable">
                <tr>
                    <td><input class="btn evt-confirm-btn btn-confirm-yes" id="ib_btn_result" type="button" value="results"></td>
                    <td><input class="btn evt-confirm-btn btn-confirm-yes" id="ib_btn_cluster" type="button" value="clusters"></td>
                    <td><img src="https://dsen.innogamescdn.com/asset/c2e59f13/graphic/buildings/wood.png"/></td>
                    <td><img src="https://dsen.innogamescdn.com/asset/c2e59f13/graphic/buildings/stone.png"/></td>
                    <td><img src="https://dsen.innogamescdn.com/asset/c2e59f13/graphic/buildings/iron.png"/></td>
                </tr>
                <tr><td colspan="2">total</td><td>${formatNumber(obj_stats.total_wood_home)}</td><td>${formatNumber(obj_stats.total_stone_home)}</td><td>${formatNumber(obj_stats.total_iron_home)}</td></tr>
                <tr><td colspan="2">average</td><td>${formatNumber(obj_stats.avg_wood)}</td><td>${formatNumber(obj_stats.avg_stone)}</td><td>${formatNumber(obj_stats.avg_iron)}</td></tr>
                <tr><td colspan="2">surplus</td><td>${formatNumber(obj_stats.total_wood_send)}</td><td>${formatNumber(obj_stats.total_stone_send)}</td><td>${formatNumber(obj_stats.total_iron_send)}</td></tr>
                <tr><td colspan="2">deficit</td><td>${formatNumber(obj_stats.total_wood_get)}</td><td>${formatNumber(obj_stats.total_stone_get)}</td><td>${formatNumber(obj_stats.total_iron_get)}</td></tr>
                </table>`;
            document.getElementById("ib_table_stats").innerHTML = html_stats_table;

            $("#ib_btn_result").off("click").on("click", () => { createTableResults(list_production); });
            $("#ib_btn_cluster").off("click").on("click", () => { createTableClusters(list_clusters_stats); });

            document.getElementById("ib_sort_distance").addEventListener("click", () => {
                list_launches.sort((o1, o2) => (parseFloat(o1.distance) > parseFloat(o2.distance)) ? 1 : (parseFloat(o1.distance) < parseFloat(o2.distance)) ? -1 : 0);
                createTable(list_launches, obj_stats, list_production, list_clusters_stats);
            });
            document.getElementById("ib_sort_total").addEventListener("click", () => {
                list_launches.sort((o1, o2) => (o1.total_send > o2.total_send) ? -1 : (o1.total_send < o2.total_send) ? 1 : 0);
                createTable(list_launches, obj_stats, list_production, list_clusters_stats);
            });
            document.getElementById("ib_sort_wood").addEventListener("click", () => {
                list_launches.sort((o1, o2) => (o1.total_wood > o2.total_wood) ? -1 : (o1.total_wood < o2.total_wood) ? 1 : 0);
                createTable(list_launches, obj_stats, list_production, list_clusters_stats);
            });
            document.getElementById("ib_sort_stone").addEventListener("click", () => {
                list_launches.sort((o1, o2) => (o1.total_stone > o2.total_stone) ? -1 : (o1.total_stone < o2.total_stone) ? 1 : 0);
                createTable(list_launches, obj_stats, list_production, list_clusters_stats);
            });
            document.getElementById("ib_sort_iron").addEventListener("click", () => {
                list_launches.sort((o1, o2) => (o1.total_iron > o2.total_iron) ? -1 : (o1.total_iron < o2.total_iron) ? 1 : 0);
                createTable(list_launches, obj_stats, list_production, list_clusters_stats);
            });

            if (document.getElementsByClassName("ib_btn_send").length > 0) {
                document.getElementsByClassName("ib_btn_send")[0].focus();
            }
        }

        function createTableResults(list_production) {
            let html_end_result = `
            <center><div id="ib_table_results" style="height:800px;width:800px;overflow:auto">
            <table class="scriptTableBalancerResult">
            <tr>
                <td>coord</td>
                <td><a href="#" id="ib_order_points"><font color="${textColor}">points</font></a></td>
                <td style="width:10%"><a href="#" id="ib_order_merchants"><font color="${textColor}">merchants</font></a></td>
                <td><img src="https://dsen.innogamescdn.com/asset/c2e59f13/graphic/buildings/main.png"/><a href="#" id="ib_order_hours"><font color="${textColor}">[hours]</font></a></td>
                <td colspan="2"><a href="#" class="ib_order_deficit"><center style="margin:10px"><img src="https://dsen.innogamescdn.com/asset/c2e59f13/graphic/buildings/wood.png"/></center></a></td>
                <td colspan="2"><a href="#" class="ib_order_deficit"><center style="margin:10px"><img src="https://dsen.innogamescdn.com/asset/c2e59f13/graphic/buildings/stone.png"/></center></a></td>
                <td colspan="2"><a href="#" class="ib_order_deficit"><center style="margin:10px"><img src="https://dsen.innogamescdn.com/asset/c2e59f13/graphic/buildings/iron.png"/></center></a></td>
                <td><a href="#" id="ib_order_wh"><center style="margin:10px"><img src="https://dsen.innogamescdn.com/asset/04d88c84/graphic/buildings/storage.png"/></center></a></td>
            </tr>`;

            for (let i = 0; i < list_production.length; i++) {
                let greenColor = "#013e27", greenColorEven = "#026440";
                let redColor = "#5f0000", redColorEven = "#9a0000";
                let header_status_wood, header_status_stone, header_status_iron;

                if (i % 2 != 0) {
                    header_status_wood = (parseInt(list_production[i].result_wood) >= 0) ? greenColor : redColor;
                    header_status_stone = (parseInt(list_production[i].result_stone) >= 0) ? greenColor : redColor;
                    header_status_iron = (parseInt(list_production[i].result_iron) >= 0) ? greenColor : redColor;
                } else {
                    header_status_wood = (parseInt(list_production[i].result_wood) >= 0) ? greenColorEven : redColorEven;
                    header_status_stone = (parseInt(list_production[i].result_stone) >= 0) ? greenColorEven : redColorEven;
                    header_status_iron = (parseInt(list_production[i].result_iron) >= 0) ? greenColorEven : redColorEven;
                }

                html_end_result += `
                <tr>
                    <td><a href="${game_data.link_base_pure}info_village&id=${list_production[i].id}"><font color="${textColor}">${list_production[i].coord}</font></a></td>
                    <td>${formatNumber(list_production[i].points)}</td>
                    <td><b>${list_production[i].merchantAvailable}</b> / ${list_production[i].merchants_total}</td>
                    <td>${formatNumber(parseInt(list_production[i].time_finished * 10) / 10)}</td>
                    <td>${formatNumber(list_production[i].wood)}</td>
                    <td style="background-color:${header_status_wood}">${formatNumber(list_production[i].result_wood)}</td>
                    <td>${formatNumber(list_production[i].stone)}</td>
                    <td style="background-color:${header_status_stone}">${formatNumber(list_production[i].result_stone)}</td>
                    <td>${formatNumber(list_production[i].iron)}</td>
                    <td style="background-color:${header_status_iron}">${formatNumber(list_production[i].result_iron)}</td>
                    <td>${formatNumber(list_production[i].capacity)}</td>
                </tr>`;
            }
            html_end_result += `</table></div></center>`;
            Dialog.show("content", html_end_result);

            $("#ib_order_points").on("click", () => {
                list_production.sort((o1, o2) => (o1.points > o2.points) ? 1 : (o1.points < o2.points) ? -1 : 0);
                $(".popup_box_close").click(); createTableResults(list_production);
            });
            $("#ib_order_merchants").on("click", () => {
                list_production.sort((o1, o2) => (o1.merchantAvailable > o2.merchantAvailable) ? 1 : (o1.merchantAvailable < o2.merchantAvailable) ? -1 : 0);
                $(".popup_box_close").click(); createTableResults(list_production);
            });
            $("#ib_order_hours").on("click", () => {
                list_production.sort((o1, o2) => (o1.time_finished > o2.time_finished) ? -1 : (o1.time_finished < o2.time_finished) ? 1 : 0);
                $(".popup_box_close").click(); createTableResults(list_production);
            });
            $(".ib_order_deficit").on("click", () => {
                list_production.sort((o1, o2) => (o1.result_total > o2.result_total) ? 1 : (o1.result_total < o2.result_total) ? -1 : 0);
                $(".popup_box_close").click(); createTableResults(list_production);
            });
            $("#ib_order_wh").on("click", () => {
                list_production.sort((o1, o2) => (o1.capacity > o2.capacity) ? 1 : (o1.capacity < o2.capacity) ? -1 : 0);
                $(".popup_box_close").click(); createTableResults(list_production);
            });
        }

        function createTableClusters(list_clusters_stats) {
            let html_end_result = `
            <center><div id="ib_table_results" style="height:800px;width:700px;overflow:auto">
            <table class="scriptTable">
            <tr><td style="width:5%">nr</td><td>coords/\ncluster</td><td>center of cluster</td><td style="width:50%">resources</td><td>max distance</td></tr>`;

            for (let i = 0; i < list_clusters_stats.length; i++) {
                html_end_result += `
                <tr>
                    <td>${i + 1}</td>
                    <td>${formatNumber(list_clusters_stats[i].nr_coords)}</td>
                    <td>${list_clusters_stats[i].center}</td>
                    <td>
                        <table class="scriptTableInner">
                            <tr><td>type</td><td><img src="https://dsen.innogamescdn.com/asset/c2e59f13/graphic/buildings/wood.png"/></td><td><img src="https://dsen.innogamescdn.com/asset/c2e59f13/graphic/buildings/stone.png"/></td><td><img src="https://dsen.innogamescdn.com/asset/c2e59f13/graphic/buildings/iron.png"/></td></tr>
                            <tr><td>total</td><td>${formatNumber(list_clusters_stats[i].total_wood_cluster)}</td><td>${formatNumber(list_clusters_stats[i].total_stone_cluster)}</td><td>${formatNumber(list_clusters_stats[i].total_iron_cluster)}</td></tr>
                            <tr><td>average</td><td>${formatNumber(list_clusters_stats[i].avg_wood)}</td><td>${formatNumber(list_clusters_stats[i].avg_stone)}</td><td>${formatNumber(list_clusters_stats[i].avg_iron)}</td></tr>
                            <tr><td>surplus</td><td>${formatNumber(list_clusters_stats[i].total_wood_send)}</td><td>${formatNumber(list_clusters_stats[i].total_stone_send)}</td><td>${formatNumber(list_clusters_stats[i].total_iron_send)}</td></tr>
                            <tr><td>deficit</td><td>${formatNumber(list_clusters_stats[i].total_wood_get)}</td><td>${formatNumber(list_clusters_stats[i].total_stone_get)}</td><td>${formatNumber(list_clusters_stats[i].total_iron_get)}</td></tr>
                        </table>
                    </td>
                    <td>${list_clusters_stats[i].max_distance.toFixed(1)}</td>
                </tr>`;
            }
            html_end_result += `</table></div></center>`;
            Dialog.show("content", html_end_result);
        }

        function sendResources(target_id, data) {
            let options = { "village": target_id, "ajaxaction": "call", "h": window.csrf_token };
            TribalWars.post("market", options, data, function (response) {
                UI.SuccessMessage(response.success, 1000);
            }, function (error) { console.log(error); });
        }

        async function getResourcesForAM(map_farm_usage) {
            let { map_construction_templates, map_coord_templates, map_priortize_farm } = await getTemplates().catch(e => alert(e));
            let map_buildings_data = await getDataBuildings().catch(e => alert(e));
            let map_constants_buildings = getConstantsTwBuildings();

            let time_construction_total = 100;
            let list_map_resources_get_AM = [];

            return new Promise((resolve, reject) => {
                for (let current_time_construction = 1; current_time_construction <= time_construction_total; current_time_construction++) {
                    let map_resources_get_AM = new Map();
                    let map_buildings = new Map(JSON.parse(JSON.stringify(Array.from(map_buildings_data.entries()))));

                    Array.from(map_buildings.keys()).forEach(key => {
                        if (key.includes("_time_queued")) {
                            map_resources_get_AM.set(key.replace("_time_queued", ""), {
                                total_wood: 0, total_stone: 0, total_iron: 0,
                                time_finished: Math.round(map_buildings.get(key) / 3600)
                            });
                        }
                    });

                    Array.from(map_coord_templates.keys()).forEach(key => {
                        let coord = key;
                        let count_time_construction = map_buildings.get(coord + "_time_queued");
                        let template_name = map_coord_templates.get(coord);
                        let list_template = map_construction_templates.get(template_name);
                        let farmCapacity = map_priortize_farm.get(template_name) / 100;

                        if (map_buildings.get(coord + "_farm") < 30 && map_farm_usage.get(coord) >= farmCapacity) {
                            let lv_building_HQ = map_buildings.get(coord + "_main");
                            let lv_building_current = map_buildings.get(coord + "_farm");
                            let obj_constants_buildings = map_constants_buildings.get("farm");

                            lv_building_current++;
                            let list_info_construction = calculateTimeAndResConstruction(lv_building_HQ, lv_building_current, obj_constants_buildings);
                            let time_construction = list_info_construction[0];
                            let total_wood = list_info_construction[1];
                            let total_stone = list_info_construction[2];
                            let total_iron = list_info_construction[3];
                            count_time_construction += time_construction;

                            map_resources_get_AM.set(coord, {
                                total_wood, total_stone, total_iron,
                                time_finished: count_time_construction / 3600
                            });
                        }

                        for (let i = 0; i < list_template.length; i++) {
                            let name_building = list_template[i].name;
                            let key_building = coord + "_" + name_building;
                            let lv_building_AM = list_template[i].level_absolute;
                            let lv_building_current = map_buildings.get(key_building);

                            if (lv_building_AM > lv_building_current) {
                                let nr_levels = lv_building_AM - lv_building_current;
                                for (let j = 0; j < nr_levels; j++) {
                                    lv_building_current++;
                                    let lv_building_HQ = map_buildings.get(coord + "_main");
                                    let obj_constants_buildings = map_constants_buildings.get(name_building);
                                    let list_info_construction = calculateTimeAndResConstruction(lv_building_HQ, lv_building_current, obj_constants_buildings);
                                    let time_construction = list_info_construction[0];
                                    let total_wood = list_info_construction[1];
                                    let total_stone = list_info_construction[2];
                                    let total_iron = list_info_construction[3];

                                    count_time_construction += time_construction;
                                    if (map_resources_get_AM.has(coord)) {
                                        let obj_update = map_resources_get_AM.get(coord);
                                        obj_update.total_wood += total_wood;
                                        obj_update.total_stone += total_stone;
                                        obj_update.total_iron += total_iron;
                                        obj_update.time_finished = count_time_construction / 3600;
                                        map_resources_get_AM.set(coord, obj_update);
                                    } else {
                                        map_resources_get_AM.set(coord, { total_wood, total_stone, total_iron, time_finished: count_time_construction / 3600 });
                                    }

                                    map_buildings.set(key_building, lv_building_current);
                                    if (count_time_construction > current_time_construction * 3600) break;
                                }
                            }
                            if (count_time_construction > current_time_construction * 3600) break;
                        }
                    });

                    list_map_resources_get_AM.push(map_resources_get_AM);
                }
                resolve(list_map_resources_get_AM);
            });
        }

        function getTemplates() {
            return new Promise((resolve, reject) => {
                if (game_data.features.AccountManager.active == false) {
                    resolve({ map_coord_templates: new Map(), map_construction_templates: new Map(), map_priortize_farm: new Map() });
                    return;
                }

                let link_combined_production = game_data.link_base_pure + "am_village";
                let dataPage = httpGet(link_combined_production);
                const parserMain = new DOMParser();
                const htmlDocMain = parserMain.parseFromString(dataPage, 'text/html');
                let list_pages = [];

                if ($(htmlDocMain).find("#village_table").prev().find("select").length > 0) {
                    Array.from($(htmlDocMain).find("#village_table").prev().find("select").get(0)).forEach(function (item) {
                        list_pages.push(item.value);
                    });
                } else if ($(htmlDocMain).find("#village_table").prev().find(".paged-nav-item").length > 0) {
                    let nr_pages = $(htmlDocMain).find("#village_table").prev().find(".paged-nav-item").length;
                    for (let i = nr_pages - 2; i >= 0; i--) {
                        list_pages.push(game_data.link_base_pure + `am_village&page=${i}`);
                    }
                } else {
                    list_pages.push(link_combined_production);
                }
                list_pages = list_pages.reverse();

                let map_coord_templates = new Map();
                let map_construction_templates = new Map();
                let map_priortize_farm = new Map();

                async function ajaxRequest(urls) {
                    let current_url;
                    if (urls.length > 0) current_url = urls.pop(); else current_url = "stop";
                    let start_ajax = new Date().getTime();
                    if (urls.length >= 0 && current_url != "stop") {
                        $.ajax({
                            url: current_url, method: 'get',
                            success: (data) => {
                                const parser = new DOMParser();
                                const htmlDoc = parser.parseFromString(data, 'text/html');
                                let table_construction = Array.from($(htmlDoc).find(".row_a, .row_b"));
                                for (let i = 0; i < table_construction.length; i++) {
                                    let coord = table_construction[i].children[0].innerText.match(/[0-9]{3}\|[0-9]{3}/)[0];
                                    let template_name = table_construction[i].children[1].innerText.trim();
                                    if (template_name != "") {
                                        map_coord_templates.set(coord, template_name);
                                        map_construction_templates.set(template_name, 0);
                                        map_priortize_farm.set(template_name, 0);
                                    }
                                }
                                let stop_ajax = new Date().getTime();
                                let diff = stop_ajax - start_ajax;
                                window.setTimeout(function () {
                                    ajaxRequest(list_pages);
                                    UI.SuccessMessage("get AM construction page: " + urls.length);
                                }, 200 - diff);
                            },
                            error: (err) => { reject(err); }
                        });
                    } else {
                        let table_name_tamplate = Array.from($(htmlDocMain).find("select[name=template]").eq(0).find("option"));
                        for (let i = 0; i < table_name_tamplate.length; i++) {
                            let link = game_data.link_base_pure + `am_village&mode=queue&template=${table_name_tamplate[i].value}`;
                            let name;
                            if (i < 3) name = table_name_tamplate[i].innerText.replaceAll("\n", "").replaceAll("\t", "").replace(/\(\w+\)/, "");
                            else name = table_name_tamplate[i].innerText.replaceAll("\n", "").replaceAll("\t", "");

                            if (map_construction_templates.has(name)) {
                                let data = await ajaxPromise(link);
                                const parser = new DOMParser();
                                const htmlDoc = parser.parseFromString(data, 'text/html');

                                let template_construction = [];
                                Array.from($(htmlDoc).find(".sortable_row")).forEach(item => {
                                    template_construction.push({
                                        name: item.getAttribute("data-building"),
                                        level_relative: parseInt($(item).find(".level_relative").text()),
                                        level_absolute: parseInt($(item).find(".level_absolute").text().match(/\d+/)[0])
                                    });
                                });
                                map_construction_templates.set(name, template_construction);

                                let farmMaxCapacity = 99;
                                let hasCustomCapacity = $(htmlDoc).find("input[name=farm_upgrade_toggle]").eq(0).is(":checked");
                                if (hasCustomCapacity) {
                                    farmMaxCapacity = 100 - parseInt($(htmlDoc).find("select[name=population_upgrades]").val());
                                }
                                map_priortize_farm.set(name, farmMaxCapacity);
                            }
                        }
                        UI.SuccessMessage("done");
                        resolve({ map_coord_templates, map_construction_templates, map_priortize_farm });
                    }
                }
                ajaxRequest(list_pages);
            });
        }

        function ajaxPromise(link) {
            return new Promise((resolve, reject) => {
                let startAjax = new Date().getTime();
                $.ajax({
                    url: link, method: 'get',
                    success: (data) => {
                        let stopAjax = new Date().getTime();
                        let difAjax = stopAjax - startAjax;
                        window.setTimeout(() => { resolve(data); }, 200 - difAjax);
                    }, error: (data) => { reject(data); }
                });
            });
        }

        function getDataBuildings() {
            return new Promise((resolve, reject) => {
                let link_combined_production = game_data.link_base_pure + "overview_villages&mode=buildings";
                let dataPage = httpGet(link_combined_production);
                const parser = new DOMParser();
                const htmlDoc = parser.parseFromString(dataPage, 'text/html');
                let list_pages = [];

                if ($(htmlDoc).find(".paged-nav-item").parent().find("select").length > 0) {
                    Array.from($(htmlDoc).find(".paged-nav-item").parent().find("select").find("option")).forEach(function (item) {
                        list_pages.push(item.value);
                    });
                    list_pages.pop();
                } else if (htmlDoc.getElementsByClassName("paged-nav-item").length > 0) {
                    let nr = 0;
                    Array.from(htmlDoc.getElementsByClassName("paged-nav-item")).forEach(function (item) {
                        let current = item.href;
                        current = current.split("page=")[0] + "page=" + nr;
                        nr++;
                        list_pages.push(current);
                    });
                } else {
                    list_pages.push(link_combined_production);
                }

                let map_buildings = new Map();
                function ajaxRequest(urls) {
                    let current_url;
                    if (urls.length > 0) current_url = urls.pop(); else current_url = "stop";
                    let start_ajax = new Date().getTime();
                    if (urls.length >= 0 && current_url != "stop") {
                        $.ajax({
                            url: current_url, method: 'get',
                            success: (data) => {
                                const parser = new DOMParser();
                                const htmlDoc = parser.parseFromString(data, 'text/html');

                                if (game_data.device == "desktop") {
                                    let table_buildings = Array.from($(htmlDoc).find(".row_a, .row_b"));
                                    for (let i = 0; i < table_buildings.length; i++) {
                                        let coord = $(table_buildings[i]).find(".nowrap").text().match(/[0-9]{3}\|[0-9]{3}/)[0];
                                        let time_last_construction = $(table_buildings[i]).find(".queue_icon img").last().attr("title");
                                        if (time_last_construction == undefined) time_last_construction = 0;
                                        else { time_last_construction = time_last_construction.split("-")[1]; time_last_construction = getFinishTime(time_last_construction); }
                                        map_buildings.set(coord + "_time_queued", time_last_construction);

                                        let buildings = $(table_buildings[i]).find(".upgrade_building");
                                        for (let j = 0; j < buildings.length; j++) {
                                            let name = buildings[j].classList[1].replace("b_", "");
                                            let level = parseInt(buildings[j].innerText);
                                            map_buildings.set(coord + "_" + name, level);
                                        }

                                        let list_queued = Array.from($(table_buildings[i]).find(".queue_icon img")).map(e => e.src.match(/\w+\.(webp|png)/)[0].replace(/\.(webp|png)/, ""));
                                        for (let j = 0; j < list_queued.length; j++) {
                                            let key = coord + "_" + list_queued[j];
                                            if (map_buildings.has(key)) { let value = map_buildings.get(key); map_buildings.set(key, value + 1); }
                                            else { map_buildings.set(key, 1); }
                                        }
                                    }
                                } else {
                                    let table_buildings = Array.from($(htmlDoc).find(".row_a, .row_b"));
                                    for (let i = 0; i < table_buildings.length; i++) {
                                        let coord = $(table_buildings[i]).find(".nowrap").text().match(/[0-9]{3}\|[0-9]{3}/)[0];
                                        let time_last_construction = $(table_buildings[i].nextElementSibling.nextElementSibling).find("img").last().attr("title");
                                        if (time_last_construction == undefined) time_last_construction = 0;
                                        else { time_last_construction = time_last_construction.split("-")[1]; time_last_construction = getFinishTime(time_last_construction); }
                                        map_buildings.set(coord + "_time_queued", time_last_construction);

                                        let buildingsLevel = $(table_buildings[i].nextElementSibling).find('table').find('td');
                                        let buildingsName = $(table_buildings[i].nextElementSibling).find('table').find('th');
                                        for (let j = 0; j < buildingsLevel.length; j++) {
                                            let name = buildingsName[j].getElementsByTagName("img")[0].src.split("buildings/")[1].replace(".png", "");
                                            let level = parseInt(buildingsLevel[j].innerText);
                                            map_buildings.set(coord + "_" + name, level);
                                        }

                                        let list_queued = Array.from($(table_buildings[i].nextElementSibling.nextElementSibling).find("img")).map(e => e.src.match(/\w+\.(webp|png)/)[0].replace(/\.(webp|png)/, ""));
                                        for (let j = 0; j < list_queued.length; j++) {
                                            let key = coord + "_" + list_queued[j];
                                            if (map_buildings.has(key)) { let value = map_buildings.get(key); map_buildings.set(key, value + 1); }
                                            else { map_buildings.set(key, 1); }
                                        }
                                    }
                                }

                                let stop_ajax = new Date().getTime();
                                let diff = stop_ajax - start_ajax;
                                window.setTimeout(function () {
                                    ajaxRequest(list_pages);
                                    UI.SuccessMessage("get building page: " + urls.length);
                                }, 200 - diff);
                            },
                            error: (err) => { reject(err); }
                        });
                    } else {
                        UI.SuccessMessage("done");
                        resolve(map_buildings);
                    }
                }
                ajaxRequest(list_pages);
            });
        }

        function getFinishTime(time_finished) {
            var date_finished = "";
            let server_date = document.getElementById("serverDate").innerText.split("/");
            if (time_finished.includes(lang["aea2b0aa9ae1534226518faaefffdaad"].replace(" %s", ""))) {
                date_finished = server_date[1] + "/" + server_date[0] + "/" + server_date[2] + " " + time_finished.match(/\d+:\d+/)[0];
            } else if (time_finished.includes(lang["57d28d1b211fddbb7a499ead5bf23079"].replace(" %s", ""))) {
                var tomorrow_date = new Date(server_date[1] + "/" + server_date[0] + "/" + server_date[2]);
                tomorrow_date.setDate(tomorrow_date.getDate() + 1);
                date_finished = ("0" + (tomorrow_date.getMonth() + 1)).slice(-2) + "/" + ("0" + tomorrow_date.getDate()).slice(-2) + "/" + tomorrow_date.getFullYear() + " " + time_finished.match(/\d+:\d+/)[0];
            } else if (time_finished.includes(lang["0cb274c906d622fa8ce524bcfbb7552d"].split(" ")[0])) {
                var on = time_finished.match(/\d+.\d+/)[0].split(".");
                date_finished = on[1] + "/" + on[0] + "/" + server_date[2] + " " + time_finished.match(/\d+:\d+/)[0];
            }
            date_finished = new Date(date_finished);

            let serverTime = document.getElementById("serverTime").innerText;
            let serverDate = document.getElementById("serverDate").innerText.split("/");
            serverDate = serverDate[1] + "/" + serverDate[0] + "/" + serverDate[2];
            let date_current = new Date(serverDate + " " + serverTime);

            let result_seconds = parseInt((date_finished.getTime() - date_current.getTime()) / 1000);
            if (result_seconds < 0) {
                date_finished.setDate(date_finished.getDate() + 1);
                result_seconds = parseInt((date_finished.getTime() - date_current.getTime()) / 1000);
            }
            return result_seconds;
        }

        function getConstantsTwBuildings() {
            if (localStorage.getItem(game_data.world + "constantBuildings") !== null) {
                return new Map(JSON.parse(localStorage.getItem(game_data.world + "constantBuildings")));
            } else {
                let data = httpGet("/interface.php?func=get_building_info");
                const parser = new DOMParser();
                const htmlDoc = parser.parseFromString(data, 'text/html');
                let map_constants_buildings = new Map();
                let list_buildings = htmlDoc.getElementsByTagName("config")[0].children;
                for (let i = 0; i < list_buildings.length; i++) {
                    let name_building = list_buildings[i].tagName.toLowerCase();
                    let wood = Number(list_buildings[i].getElementsByTagName("wood")[0].innerText);
                    let stone = Number(list_buildings[i].getElementsByTagName("stone")[0].innerText);
                    let iron = Number(list_buildings[i].getElementsByTagName("iron")[0].innerText);
                    let wood_factor = Number(list_buildings[i].getElementsByTagName("wood_factor")[0].innerText);
                    let stone_factor = Number(list_buildings[i].getElementsByTagName("stone_factor")[0].innerText);
                    let iron_factor = Number(list_buildings[i].getElementsByTagName("iron_factor")[0].innerText);
                    let build_time = Number(list_buildings[i].getElementsByTagName("build_time")[0].innerText);
                    let build_time_factor = Number(list_buildings[i].getElementsByTagName("build_time_factor")[0].innerText);
                    map_constants_buildings.set(name_building, { wood, stone, iron, wood_factor, stone_factor, iron_factor, build_time, build_time_factor });
                }
                localStorage.setItem(game_data.world + "constantBuildings", JSON.stringify(Array.from(map_constants_buildings.entries())));
                return map_constants_buildings;
            }
        }

        function calculateTimeAndResConstruction(hq, level, obj_data) {
            let constantLvl = {
                1: 1, 2: 1, 3: 0.112292, 4: 0.289555, 5: 0.46113, 6: 0.606372, 7: 0.723059, 8: 0.815935, 9: 0.889947,
                10: 0.948408, 11: 0.994718, 12: 1.031, 13: 1.059231, 14: 1.080939, 15: 1.09729, 16: 1.109156, 17: 1.117308,
                18: 1.122392, 19: 1.124817, 20: 1.124917, 21: 1.123181, 22: 1.119778, 23: 1.114984, 24: 1.109038,
                25: 1.102077, 26: 1.0942, 27: 1.085601, 28: 1.076369, 29: 1.066566, 30: 1.056291
            };
            var buildTime = obj_data.build_time * Math.pow(1.2, (level - 1)) * Math.pow(1.05, -hq) * constantLvl[level];
            let total_wood = Math.round(obj_data.wood * Math.pow(obj_data.wood_factor, level - 1));
            let total_stone = Math.round(obj_data.stone * Math.pow(obj_data.stone_factor, level - 1));
            let total_iron = Math.round(obj_data.iron * Math.pow(obj_data.iron_factor, level - 1));
            return [Math.round(buildTime), total_wood, total_stone, total_iron];
        }

        function getClusters(data, options) {
            let result_cluster = [];
            let maxDistanceGlobal = 999999;
            let repeat = 50;
            for (let rep = 0; rep < repeat; rep++) {
                let result = insideGetCluster(data, options);
                if (maxDistanceGlobal > result.maxDistance) {
                    maxDistanceGlobal = result.maxDistance;
                    result_cluster = result;
                }
            }
            return result_cluster;
        }

        function insideGetCluster(data, options) {
            var numberOfClusters, distanceFunction, vectorFunction, maxIterations;
            if (!options || !options.numberOfClusters) { numberOfClusters = 1; } else { numberOfClusters = options.numberOfClusters; }
            if (!options || !options.distanceFunction) { distanceFunction = getDistance; } else { distanceFunction = options.distanceFunction; }
            if (!options || !options.vectorFunction) { vectorFunction = defaultVectorFunction; } else { vectorFunction = options.vectorFunction; }
            if (!options || !options.maxIterations) { maxIterations = 1000; } else { maxIterations = options.maxIterations; }

            let result_cluster = getClustersWithParams(data, numberOfClusters, distanceFunction, vectorFunction, maxIterations).clusters;

            let maxDistance = 0;
            for (let i = 0; i < result_cluster.length; i++) {
                let list_coord = result_cluster[i].data;
                for (let j = 0; j < list_coord.length; j++) {
                    for (let k = j + 1; k < list_coord.length; k++) {
                        let dist = getDistance(list_coord[j], list_coord[k]);
                        maxDistance = (maxDistance > dist) ? maxDistance : dist;
                    }
                }
            }
            result_cluster.maxDistance = maxDistance;
            return result_cluster;
        }

        function getClustersWithParams(data, numberOfClusters, distanceFunction, vectorFunction, maxIterations) {
            let means = [];
            for (let i = 0; i < numberOfClusters; i++) {
                let random_index = parseInt(Math.random() * Object.keys(data).length);
                means.push(data[random_index]);
            }
            var clusters = createClusters(means);
            var numOfInterations = 0;
            var iterations = [];
            while (numOfInterations < maxIterations) {
                initClustersData(clusters);
                assignDataToClusters(data, clusters, distanceFunction, vectorFunction);
                updateMeans(clusters, vectorFunction);
                numOfInterations++;
            }
            return { clusters: clusters, iterations: iterations };
        }

        function defaultVectorFunction(vector) { return vector; }

        function updateMeans(clusters, vectorFunction) {
            clusters.forEach(function (cluster) { updateMean(cluster, vectorFunction); });
        }

        function updateMean(cluster, vectorFunction) {
            var newMean = [];
            for (var i = 0; i < cluster.mean.length; i++) { newMean.push(getMean(cluster.data, i, vectorFunction)); }
            cluster.mean = newMean;
        }

        function getMean(data, index, vectorFunction) {
            var sum = 0;
            var total = data.length;
            if (total == 0) return 0;
            data.forEach(function (vector) { sum = sum + vectorFunction(vector)[index]; });
            return sum / total;
        }

        function assignDataToClusters(data, clusters, distanceFunction, vectorFunction) {
            data.forEach(function (vector) {
                var cluster = findClosestCluster(vectorFunction(vector), clusters, distanceFunction);
                if (!cluster.data) cluster.data = [];
                cluster.data.push(vector);
            });
        }

        function findClosestCluster(vector, clusters, distanceFunction) {
            var closest = {};
            var minDistance = 9999999;
            clusters.forEach(function (cluster) {
                var distance = distanceFunction(cluster.mean, vector);
                if (distance < minDistance) { minDistance = distance; closest = cluster; }
            });
            return closest;
        }

        function initClustersData(clusters) { clusters.forEach(function (cluster) { cluster.data = []; }); }

        function createClusters(means) {
            var clusters = [];
            means.forEach(function (mean) { clusters.push({ mean: mean, data: [] }); });
            return clusters;
        }

        function getDistance(vector1, vector2) {
            var sum = 0;
            for (var i = 0; i < vector1.length; i++) { sum = sum + Math.pow(vector1[i] - vector2[i], 2); }
            return Math.sqrt(sum);
        }

        function addInfoOnMap(mapInfoResources, random_color) {
            let drawInfo = true;
            TWMap.mapHandler.spawnSector = function (data, sector) {
                IR_originalSpawnSector.call(TWMap.mapHandler, data, sector);
                if (drawInfo == true) {
                    drawInfo = false;
                    window.setTimeout(() => {
                        let visibleSectors = TWMap.map._visibleSectors;
                        Object.keys(visibleSectors).forEach(key => {
                            let elements = visibleSectors[key]._elements;
                            Object.keys(elements).forEach(key => {
                                let villageId = elements[key].id.match(/\d+/);
                                if (villageId != null) {
                                    if (mapInfoResources.has(villageId[0])) {
                                        let obj = mapInfoResources.get(villageId[0]);
                                        createMapInfo(obj, random_color[obj.label_cluster]);
                                    }
                                }
                            });
                        });
                        drawInfo = true;
                    }, 50);
                }
            };
        }

        function createMapInfo(obj, random_color) {
            try {
                if (document.getElementById(`info_extra${obj.villageId}`) == null) {
                    let greenColor = "#026440";
                    let redColor = "#E80000";
                    let villageImg = document.getElementById(`map_village_${obj.villageId}`);
                    let parent = document.getElementById(`map_village_${obj.villageId}`).parentElement;
                    let leftImg = villageImg.style.left;
                    let topImg = villageImg.style.top;

                    while (document.getElementById(`map_icons_${obj.villageId}`) != null) {
                        document.getElementById(`map_icons_${obj.villageId}`).remove();
                    }
                    if (document.getElementById(`map_cmdicons_${obj.villageId}_0`) != null) document.getElementById(`map_cmdicons_${obj.villageId}_0`).remove();
                    if (document.getElementById(`map_cmdicons_${obj.villageId}_1`) != null) document.getElementById(`map_cmdicons_${obj.villageId}_1`).remove();

                    let html_info = `
                        <div class="border_info" id="info_extra${obj.villageId}" style="position:absolute;left:${leftImg};top:${topImg};width:51px;height:36px;z-index:3; background-color:${random_color.colorOpacity};outline:${random_color.color} solid 2px"></div>
                        <center><font color="${textColor}" class="shadow20" style="position:absolute;left:${leftImg};top:${topImg};width:14px;height:14px;z-index:4;margin-left:0px;; font-size: 12px">nr:${obj.label_cluster} </font></center>
                        <center><font color="${greenColor}" class="shadow20" style="position:absolute;left:${leftImg};top:${topImg};width:14px;height:14px;z-index:4;margin-left:0px;margin-top:11px; font-size: 12px">${parseInt(obj.total_resources_get / 1000)}k </font></center>
                        <center><font color="${redColor}" class="shadow20" style="position:absolute;left:${leftImg};top:${topImg};width:14px;height:14px;z-index:4;margin-left:0px;margin-top:23px; font-size: 12px">${parseInt(obj.total_resources_send / 1000)}k </font></center>
                    `;
                    $(html_info).appendTo(parent);
                }
            } catch (error) {}
        }

        function getRandomColor(opacity) {
            let color = 'rgb(';
            let colorOpacity = 'rgba(';
            for (let i = 0; i < 3; i++) {
                let randomNr = Math.floor(Math.random() * 255);
                color += randomNr + ',';
                colorOpacity += randomNr + ',';
            }
            color = color.substr(0, color.length - 1) + ')';
            colorOpacity = colorOpacity + opacity + ')';
            return { color, colorOpacity };
        }

        return { init: init };

    })();

    /* =========================================================================================
     * TAB 2: IntelliMint  (originally "Resource Sender" v1.0.1 by SaveBank,
     * built on the Tribal Wars Scripts Library by RedAlert)
     * Trimmed to the library functions Resource Sender actually uses, restyled with the
     * shared IntelliRess theme instead of the default beige Tribal Wars look.
     * ========================================================================================= */

    var IntelliMint = (function () {

        var MINT_DEBUG = false;
        var im_autoTimer = null;
        var im_countdownTimer = null;
        var im_autoNextRunAt = 0;

        var MintSDK = {
            scriptData: {},
            translations: {},
            allowedMarkets: [],
            allowedScreens: [],
            allowedModes: [],
            enableCountApi: true,
            isDebug: false,
            isMobile: jQuery('#mobileHeader').length > 0,
            market: game_data.market,
            units: game_data.units,
            village: game_data.village,
            sitterId: game_data.player.sitter > 0 ? `&t=${game_data.player.id}` : '',
            coordsRegex: /\d{1,3}\|\d{1,3}/g,
            worldInfoInterface: '/interface.php?func=get_config',
            worldDataVillages: '/map/village.txt',

            _initDebug: function () {
                const scriptInfo = this.scriptInfo();
                console.debug(`${scriptInfo} It works.`);
            },
            addGlobalStyle: function () {
                return `
                    .ra-table-container { overflow-y: auto; overflow-x: hidden; height: auto; max-height: 400px; }
                    .ra-table th { font-size: 14px; }
                    .ra-table th label { margin: 0; padding: 0; }
                    .ra-table th, .ra-table td { padding: 5px; text-align: center; }
                    .ra-table td a { word-break: break-all; }
                    .ra-table tr:nth-of-type(2n) td { background-color: ${backgroundAlternateTableEven}; color: ${textColor}; }
                    .ra-table tr:nth-of-type(2n+1) td { background-color: ${backgroundAlternateTableOdd}; color: ${textColor}; }
                    .ra-table-v2 th, .ra-table-v2 td { text-align: left; }
                    .ra-popup-content { width: 360px; }
                    .ra-popup-content * { box-sizing: border-box; }
                    .ra-popup-content input[type="text"] { padding: 3px; width: 100%; }
                    .ra-popup-content label { display: block; margin-bottom: 5px; font-weight: 600; color: ${textColor}; }
                    .ra-popup-content > div { margin-bottom: 15px; }
                    .ra-popup-content > div:last-child { margin-bottom: 0 !important; }
                    .ra-popup-content textarea { width: 100%; height: 100px; resize: none; background:${backgroundInput}; color:${textColor}; border:1px solid ${borderColor}; }
                    .ra-textarea { width: 100%; height: 80px; resize: none; background:${backgroundInput}; color:${textColor}; border:1px solid ${borderColor}; }
                `;
            },
            calculateDistance: function (from, to) {
                const [x1, y1] = from.split('|');
                const [x2, y2] = to.split('|');
                const deltaX = Math.abs(x1 - x2);
                const deltaY = Math.abs(y1 - y2);
                return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
            },
            cleanString: function (string) {
                try { return decodeURIComponent(string).replace(/\+/g, ' '); }
                catch (error) { console.error(error, string); return string; }
            },
            csvToArray: function (strData, strDelimiter = ',') {
                var objPattern = new RegExp('(\\' + strDelimiter + '|\\r?\\n|\\r|^)' + '(?:"([^"]*(?:""[^"]*)*)"|' + '([^"\\' + strDelimiter + '\\r\\n]*))', 'gi');
                var arrData = [[]];
                var arrMatches = null;
                while ((arrMatches = objPattern.exec(strData))) {
                    var strMatchedDelimiter = arrMatches[1];
                    if (strMatchedDelimiter.length && strMatchedDelimiter !== strDelimiter) { arrData.push([]); }
                    var strMatchedValue;
                    if (arrMatches[2]) { strMatchedValue = arrMatches[2].replace(new RegExp('""', 'g'), '"'); }
                    else { strMatchedValue = arrMatches[3]; }
                    arrData[arrData.length - 1].push(strMatchedValue);
                }
                return arrData;
            },
            xml2json: function ($xml) {
                let data = {};
                const _self = this;
                $.each($xml.children(), function (i) {
                    let $this = $(this);
                    if ($this.children().length > 0) { data[$this.prop('tagName')] = _self.xml2json($this); }
                    else { data[$this.prop('tagName')] = $.trim($this.text()); }
                });
                return data;
            },
            getWorldConfig: async function () {
                const TIME_INTERVAL = 60 * 60 * 1000 * 24 * 7;
                const LAST_UPDATED_TIME = localStorage.getItem('world_config_last_updated') ?? 0;
                let worldConfig = [];
                if (LAST_UPDATED_TIME !== null) {
                    if (Date.parse(new Date()) >= LAST_UPDATED_TIME + TIME_INTERVAL) {
                        const response = await jQuery.ajax({ url: this.worldInfoInterface });
                        worldConfig = this.xml2json(jQuery(response));
                        localStorage.setItem('world_config', JSON.stringify(worldConfig));
                        localStorage.setItem('world_config_last_updated', Date.parse(new Date()));
                    } else {
                        worldConfig = JSON.parse(localStorage.getItem('world_config'));
                    }
                } else {
                    const response = await jQuery.ajax({ url: this.worldInfoInterface });
                    worldConfig = this.xml2json(jQuery(response));
                    localStorage.setItem('world_config', JSON.stringify(worldConfig));
                    localStorage.setItem('world_config_last_updated', Date.parse(new Date()));
                }
                return worldConfig;
            },
            renderBoxWidget: function (body, id, mainClass, customStyle) {
                const globalStyle = this.addGlobalStyle();
                const content = `
                    <div class="${mainClass}" id="${id}">${body}</div>
                    <style>
                        .${mainClass} { position: relative; display: block; width: 100%; }
                        .${mainClass} * { box-sizing: border-box; }
                        .${mainClass} .btn-confirm-yes { padding: 3px; }
                        ${globalStyle}
                        ${customStyle}
                    </style>`;
                $('#ir_panel_mint').html(content);
            },
            renderFixedWidget: function (body, id, mainClass, customStyle, width, customName) {
                const globalStyle = this.addGlobalStyle();
                const content = `
                    <div class="${mainClass} ra-fixed-widget" id="${id}">
                        <div class="${mainClass}-header" style="display:flex;align-items:center;justify-content:space-between;background-color:${backgroundHeader};color:${textColor};padding:6px 10px;">
                            <h3 style="margin:0;">${customName || 'Anaboler Mint'}</h3>
                            <a class="custom-close-button" href="#" style="color:${textColor};font-weight:bold;">&times;</a>
                        </div>
                        <div class="${mainClass}-body" style="padding:10px;">${body}</div>
                    </div>
                    <style>
                        .${mainClass} { position: fixed; top: 10vw; right: 10vw; z-index: 99999; border: 2px solid ${borderColor}; border-radius: 10px; padding: 0; width: ${width ?? '360px'}; overflow-y: auto; background: ${backgroundContainer}; color:${textColor}; }
                        .${mainClass} * { box-sizing: border-box; }
                        ${globalStyle}
                        ${customStyle}
                    </style>`;
                if (jQuery(`#${id}`).length < 1) {
                    jQuery('#contentContainer, #content_value').first().prepend(content);
                    jQuery(`#${id}`).draggable({ cancel: '.ra-table, input, textarea, button, select, option' });
                    jQuery(`#${id} .custom-close-button`).on('click', function (e) { e.preventDefault(); jQuery(`#${id}`).remove(); });
                } else {
                    jQuery(`.${mainClass}-body`).html(body);
                }
            },
            scriptInfo: function (scriptData = this.scriptData) { return `[${scriptData.name} ${scriptData.version}]`; },
            tt: function (string) {
                if (this.translations[game_data.locale] !== undefined) { return this.translations[game_data.locale][string]; }
                else { return this.translations['en_DK'][string]; }
            },
            worldDataAPI: async function (entity) {
                const TIME_INTERVAL = 60 * 60 * 1000;
                const LAST_UPDATED_TIME = localStorage.getItem(`${entity}_last_updated`);
                const allowedEntities = ['village'];
                if (!allowedEntities.includes(entity)) { throw new Error(`Entity ${entity} does not exist!`); }
                const worldData = {};
                const dbConfig = { village: { dbName: 'villagesDb', dbTable: 'villages', key: 'villageId', url: MintSDK.worldDataVillages } };

                const fetchDataAndSave = async () => {
                    const DATA_URL = dbConfig[entity].url;
                    try {
                        const response = await jQuery.ajax(DATA_URL);
                        const data = MintSDK.csvToArray(response);
                        let responseData = data.filter((item) => item[0] != '').map((item) => ({
                            villageId: parseInt(item[0]), villageName: MintSDK.cleanString(item[1]),
                            villageX: item[2], villageY: item[3], playerId: parseInt(item[4]),
                            villagePoints: parseInt(item[5]), villageType: parseInt(item[6])
                        }));
                        saveToIndexedDbStorage(dbConfig[entity].dbName, dbConfig[entity].dbTable, dbConfig[entity].key, responseData);
                        localStorage.setItem(`${entity}_last_updated`, Date.parse(new Date()));
                        return responseData;
                    } catch (error) { throw Error(`Error fetching ${DATA_URL}`); }
                };

                async function saveToIndexedDbStorage(dbName, table, keyId, data) {
                    return new Promise((resolveSave) => {
                        const dbConnect = indexedDB.open(dbName);
                        dbConnect.onupgradeneeded = function () {
                            const db = dbConnect.result;
                            if (keyId.length) { db.createObjectStore(table, { keyPath: keyId }); }
                            else { db.createObjectStore(table, { autoIncrement: true }); }
                        };
                        dbConnect.onsuccess = function () {
                            const db = dbConnect.result;
                            const transaction = db.transaction(table, 'readwrite');
                            const store = transaction.objectStore(table);
                            store.clear();
                            data.forEach((item) => { store.put(item); });
                            transaction.oncomplete = () => resolveSave();
                        };
                    });
                }

                function getAllData(dbName, table) {
                    return new Promise((resolve, reject) => {
                        const dbConnect = indexedDB.open(dbName);
                        dbConnect.onsuccess = () => {
                            const db = dbConnect.result;
                            const dbQuery = db.transaction(table, 'readwrite').objectStore(table).getAll();
                            dbQuery.onsuccess = (event) => { resolve(event.target.result); };
                            dbQuery.onerror = (event) => { reject(event.target.error); };
                        };
                        dbConnect.onerror = (event) => { reject(event.target.error); };
                    });
                }

                function objectToArray(arrayOfObjects) {
                    return arrayOfObjects.map((item) => [item.villageId, item.villageName, item.villageX, item.villageY, item.playerId, item.villagePoints, item.villageType]);
                }

                if (LAST_UPDATED_TIME !== null) {
                    if (Date.parse(new Date()) >= parseInt(LAST_UPDATED_TIME) + TIME_INTERVAL) { worldData[entity] = await fetchDataAndSave(); }
                    else { worldData[entity] = await getAllData(dbConfig[entity].dbName, dbConfig[entity].dbTable); }
                } else { worldData[entity] = await fetchDataAndSave(); }

                worldData[entity] = objectToArray(worldData[entity]);
                return worldData[entity];
            },
            init: async function (scriptConfig) {
                const { scriptData, translations, allowedMarkets, allowedScreens, allowedModes, isDebug, enableCountApi } = scriptConfig;
                this.scriptData = scriptData;
                this.translations = translations;
                this.allowedMarkets = allowedMarkets;
                this.allowedScreens = allowedScreens;
                this.allowedModes = allowedModes;
                this.enableCountApi = enableCountApi;
                this.isDebug = isDebug;
                MintSDK._initDebug();
            }
        };

        var defaultSettings = {
            'sbOriginGroupSelection': true, 'sbOriginCustomSelection': false, 'sbOriginGroupsFilter': 0, 'sbCustomOriginVillages': '',
            'sbTargetGroupSelection': true, 'sbTargetCustomSelection': false, 'sbTargetGroupsFilter': 0, 'sbCustomTargetVillages': '',
            'sbHoldBackMerchants': 0, 'sbMerchantBonus': false, 'sbSendResourcesAbsoluteRadio': true, 'sbSendResourcesRatioRadio': false,
            'sbSendResourcesMintRatioRadio': false, 'sbSendResourcesFillRadio': false, 'sbSendWood': 0, 'sbSendClay': 0, 'sbSendIron': 0,
            'sbSendWoodRatio': 33, 'sbSendClayRatio': 34, 'sbSendIronRatio': 33, 'sbHoldBackResourcesAbsoluteRadio': false,
            'sbHoldBackResourcesPercentageRadio': true, 'sbHoldBackWood': 0, 'sbHoldBackClay': 0, 'sbHoldBackIron': 0,
            'sbHoldBackPercentage': 0, 'sbSendGapToMax': 0, 'sbMaxWood': 0, 'sbMaxClay': 0, 'sbMaxIron': 0, 'sbArrivalTimes': []
        };
        var merchantSpeed = 6;
        var merchantSpeedWithBonus = 4;
        var allIdsRS = [
            'sbOriginGroupSelection', 'sbOriginCustomSelection', 'sbOriginGroupsFilter', 'sbCustomOriginVillages',
            'sbTargetGroupSelection', 'sbTargetCustomSelection', 'sbTargetGroupsFilter', 'sbCustomTargetVillages',
            'sbHoldBackMerchants', 'sbMerchantBonus', 'sbSendResourcesAbsoluteRadio', 'sbSendResourcesRatioRadio',
            'sbSendResourcesMintRatioRadio', 'sbSendResourcesFillRadio', 'sbSendWood', 'sbSendClay', 'sbSendIron',
            'sbSendWoodRatio', 'sbSendClayRatio', 'sbSendIronRatio', 'sbHoldBackResourcesAbsoluteRadio',
            'sbHoldBackResourcesPercentageRadio', 'sbSendGapToMax', 'sbHoldBackWood', 'sbHoldBackClay', 'sbHoldBackIron',
            'sbHoldBackPercentage', 'sbMaxWood', 'sbMaxClay', 'sbMaxIron'
        ];
        var warehouseData = {};
        var totalWoodSent = 0, totalClaySent = 0, totalIronSent = 0;
        var groups, villageIdToCoordMap, playerData, villageData, worldConfig, scriptInfo;

        var scriptConfig = {
            scriptData: { prefix: 'sbRS', name: 'IntelliMint', version: 'v1.0.0', author: 'SaveBank (integrated into IntelliRess)' },
            translations: {
                en_DK: {
                    'Redirecting...': 'Redirecting...', Help: 'Help', 'Resource Sender': 'Anaboler Mint',
                    'Origin villages': 'Origin villages', 'Target villages': 'Target villages', 'Group': 'Group', 'Custom': 'Custom',
                    'Settings': 'Settings', 'Hold back merchants': 'Hold back merchants', 'Merchant bonus active?': 'Merchant bonus active?',
                    'Send resources as': 'Send resources as', 'Absolute numbers': 'Absolute numbers', 'Ratio': 'Ratio', 'Percentage': 'Percentage',
                    'Wood': 'Wood', 'Clay': 'Clay', 'Iron': 'Iron', 'Wood (%)': 'Wood (%)', 'Clay (%)': 'Clay (%)', 'Iron (%)': 'Iron (%)',
                    'Hold back resources as': 'Hold back resources as', 'Max resources per target village': 'Max resources per target village',
                    'Max wood per village': 'Max wood per village', 'Max clay per village': 'Max clay per village', 'Max iron per village': 'Max iron per village',
                    'Paste warehouse data': 'Paste warehouse data', 'Delete all arrival times': 'Delete all arrival times', 'Reset Input': 'Reset Input',
                    'Arrival time': 'Arrival time', 'Reset all inputs': 'Reset all inputs', 'This entry already exists.': 'This entry already exists.',
                    'Invalid entry. Please select valid start and end times.': 'Invalid entry. Please select valid start and end times.',
                    'Invalid entry. Please check the selected times.': 'Invalid entry. Please check the selected times.', 'Mint Ratio': 'Mint Ratio',
                    'From': 'From', 'To': 'To', 'Delete Entry': 'Delete Entry', 'Ratio (has to add up to 100%)': 'Ratio (has to add up to 100%)',
                    'Save': 'Save', 'Warehouse data (will overwrite target villages)': 'Warehouse data (will overwrite target villages)',
                    'Paste warehouse data here...': 'Paste warehouse data here...', 'Gap to warehouse max': 'Gap to warehouse max', 'Fill': 'Fill',
                    'Gap to max (%)': 'Gap to max (%)', 'Calculate & Send': 'Calculate & Send', 'Calculate': 'Calculate',
                    'Please enter origin villages.': 'Please enter origin villages.', 'Please enter target villages.': 'Please enter target villages.',
                    'The sum of the ratios must be 100.': 'The sum of the ratios must be 100.', 'Please select how to send resources.': 'Please select how to send resources.',
                    'Multiple send resource options selected. Please reload andreselect how to send resources.': 'Multiple send resource options selected. Please reload andreselect how to send resources.',
                    'Please paste warehouse data before selecting fill.': 'Please paste warehouse data before selecting fill.',
                    'There was an error while fetching the data!': 'There was an error while fetching the data!', 'Finished sending!': 'Finished sending!',
                    'Origin': 'Origin', 'Target': 'Target', 'Travel Time': 'Travel Time', 'Send': 'Send', 'Total Resources Sent': 'Total Resources Sent',
                    'Fetching player data for more than 1000 villages. This may take a while...': 'Fetching player data for more than 1000 villages. This may take a while...',
                    'Fetching player data...': 'Fetching player data...', 'Absolute send exceeds max resource per target village setting.': 'Absolute send exceeds max resource per target village setting.',
                    'Not enough total origin resources to fulfill absolute target demand.': 'Not enough total origin resources to fulfill absolute target demand.',
                    'Not enough total merchant capacity to fulfill absolute target demand.': 'Not enough total merchant capacity to fulfill absolute target demand.',
                    'Unable to fully satisfy all absolute target demands with current resources/merchants/arrival windows.': 'Unable to fully satisfy all absolute target demands with current resources/merchants/arrival windows.',
                    'There was an error!': 'There was an error!',
                    'Missing warehouse data for one or more selected target villages. Please paste warehouse data again.': 'Missing warehouse data for one or more selected target villages. Please paste warehouse data again.',
                    'Not enough total origin resources to fulfill fill target demand.': 'Not enough total origin resources to fulfill fill target demand.',
                    'Not enough total merchant capacity to fulfill fill target demand.': 'Not enough total merchant capacity to fulfill fill target demand.',
                    'Unable to fully fill all target villages.': 'Unable to fully fill all target villages.', 'Short on: ': 'Short on: ',
                    'Try adjusting hold-back, gap to max, arrival times, or origin villages.': 'Try adjusting hold-back, gap to max, arrival times, or origin villages.'
                },
                de_DE: {
                    'Redirecting...': 'Weiterleiten...', Help: 'Hilfe', 'Resource Sender': 'Anaboler Mint',
                    'Origin villages': 'Herkunftsdörfer', 'Target villages': 'Zieldörfer', 'Group': 'Gruppe', 'Custom': 'Benutzerdefiniert',
                    'Settings': 'Einstellungen', 'Hold back merchants': 'Händler zurückhalten', 'Merchant bonus active?': 'Händlerbonus aktiv?',
                    'Send resources as': 'Ressourcen senden als', 'Absolute numbers': 'Absolute Zahlen', 'Percentage': 'Prozent', 'Ratio': 'Verhältnis',
                    'Wood': 'Holz', 'Clay': 'Lehm', 'Iron': 'Eisen', 'Wood (%)': 'Holz (%)', 'Clay (%)': 'Lehm (%)', 'Iron (%)': 'Eisen (%)',
                    'Hold back resources as': 'Ressourcen zurückhalten als', 'Max resources per target village': 'Max Ressourcen pro Zieldorf',
                    'Max wood per village': 'Max Holz pro Dorf', 'Max clay per village': 'Max Lehm pro Dorf', 'Max iron per village': 'Max Eisen pro Dorf',
                    'Paste warehouse data': 'Speicherdaten einfügen', 'Delete all arrival times': 'Alle Ankunftszeiten löschen', 'Reset Input': 'Eingaben löschen',
                    'Arrival time': 'Ankunftszeit', 'Reset all inputs': 'Alle Eingaben löschen', 'This entry already exists.': 'Dieser Eintrag existiert bereits.',
                    'Invalid entry. Please select valid start and end times.': 'Ungültiger Eintrag. Bitte wählen Sie gültige Start- und Endzeiten.',
                    'Invalid entry. Please check the selected times.': 'Ungültiger Eintrag. Bitte überprüfen Sie die ausgewählten Zeiten.',
                    'Mint Ratio': 'Münzenverhältnis', 'From': 'Von', 'To': 'Bis', 'Delete Entry': 'Eintrag löschen',
                    'Ratio (has to add up to 100%)': 'Verhältnis (muss gesamt 100% sein)', 'Save': 'Speichern',
                    'Warehouse data (will overwrite target villages)': 'Speicherdaten (überschreibt Zieldörfer)', 'Paste warehouse data here...': 'Speicherdaten hier einfügen...',
                    'Gap to warehouse max': 'Abstand zum Speichermaximum', 'Fill': 'Auffüllen', 'Gap to max (%)': 'Abstand zum Maximum (%)',
                    'Calculate & Send': 'Berechnen & Senden', 'Calculate': 'Berechnen', 'Please enter origin villages.': 'Bitte geben Sie Herkunftsdörfer ein.',
                    'Please enter target villages.': 'Bitte geben Sie Zieldörfer ein.', 'The sum of the ratios must be 100.': 'Die Summe der Verhältnisse muss 100 sein.',
                    'Please select how to send resources.': 'Bitte wählen Sie, wie die Ressourcen gesendet werden sollen.',
                    'Multiple send resource options selected. Please reload andreselect how to send resources.': 'Mehrere Optionen zum Senden von Ressourcen ausgewählt. Bitte neu laden und auswählen, wie die Ressourcen gesendet werden sollen.',
                    'Please paste warehouse data before selecting fill.': 'Bitte fügen Sie die Speicherdaten ein, bevor Sie Auffüllen auswählen.',
                    'There was an error while fetching the data!': 'Es ist ein Fehler beim Abrufen der Daten aufgetreten!', 'Finished sending!': 'Fertig mit dem Senden!',
                    'Origin': 'Herkunft', 'Target': 'Ziel', 'Travel Time': 'Reisezeit', 'Send': 'Senden', 'Total Resources Sent': 'Gesendete Gesamtressourcen',
                    'Fetching player data for more than 1000 villages. This may take a while...': 'Spielerdaten für mehr als 1000 Dörfer abrufen. Dies kann einige Zeit dauern...',
                    'Fetching player data...': 'Spielerdaten abrufen...', 'Absolute send exceeds max resource per target village setting.': 'Der absolute Wert überschreitet die Einstellung für maximale Ressourcen pro Zieldorf.',
                    'Not enough total origin resources to fulfill absolute target demand.': 'Nicht genügend Ressourcen in den Herkunftsdörfern, um die absolute Nachfrage der Zieldörfer zu erfüllen.',
                    'Not enough total merchant capacity to fulfill absolute target demand.': 'Nicht genügend Händlerkapazität, um die absolute Nachfrage der Zieldörfer zu erfüllen.',
                    'Unable to fully satisfy all absolute target demands with current resources/merchants/arrival windows.': 'Es ist nicht möglich, alle absoluten Zielanforderungen mit den aktuellen Ressourcen/Händlern/Ankunftszeiten vollständig zu erfüllen.',
                    'There was an error!': 'Es gab einen Fehler!',
                    'Missing warehouse data for one or more selected target villages. Please paste warehouse data again.': 'Fehlende Speicherdaten für ein oder mehrere ausgewählte Zieldörfer. Bitte fügen Sie die Speicherdaten erneut ein.',
                    'Not enough total origin resources to fulfill fill target demand.': 'Nicht genügend Ressourcen in den Herkunftsdörfern, um die Nachfrage der Zieldörfer zu erfüllen.',
                    'Not enough total merchant capacity to fulfill fill target demand.': 'Nicht genügend Händlerkapazität, um die Nachfrage der Zieldörfer zu erfüllen.',
                    'Unable to fully fill all target villages.': 'Nicht alle Zieldörfer konnten vollständig aufgefüllt werden.', 'Short on: ': 'Es fehlt: ',
                    'Try adjusting hold-back, gap to max, arrival times, or origin villages.': 'Versuche zurückgehaltene Resourcen, Abstand zum Maximum, Ankunftszeiten oder Herkunftsdörfer anzupassen.'
                }
            },
            allowedMarkets: [], allowedScreens: [], allowedModes: [], isDebug: MINT_DEBUG, enableCountApi: false
        };

        async function init(container) {
            scriptInfo = MintSDK.scriptInfo(scriptConfig.scriptData);
            await MintSDK.init(scriptConfig);

            groups = await fetchVillageGroups();
            const wd = await fetchWorldConfigData();
            const villages = wd.villages;
            worldConfig = wd.worldConfig;
            villageIdToCoordMap = createVillageMap(villages);
            villageData = villageArrayToDict(villages);
            playerData = await getPlayerData();

            try {
                renderUI();
                addEventHandlers();
                initializeInputFields();
            } catch (error) {
                UI.ErrorMessage(MintSDK.tt('There was an error!'));
                console.error(`${scriptInfo}: Error:`, error);
            }
        }

        function renderUI() {
            const style = generateCSS();
            const groupMenuOrigin = renderGroupsFilter("sbOrigin");
            const groupMenuTarget = renderGroupsFilter("sbTarget");
            const contentArrivalTimeSelector = renderArrivalTimeSelector();

            let content = `
                <div class="sb-grid sb-grid-2 ra-mb10">
                    <fieldset>
                        <legend>${MintSDK.tt('Origin villages')}</legend>
                        <div class="sb-grid sb-grid-2 ra-mb10">
                            <div>
                                <div><input type="radio" id="sbOriginGroupSelection" name="originSelection" value="group" checked><label for="sbOriginGroupSelection">${MintSDK.tt('Group')}</label></div>
                                <div><input type="radio" id="sbOriginCustomSelection" name="originSelection" value="custom"><label for="sbOriginCustomSelection">${MintSDK.tt('Custom')}</label></div>
                            </div>
                            <div id="sbOriginGroupMenuContainer">${groupMenuOrigin}</div>
                        </div>
                        <div id="sbOriginCustomTextAreaContainer" style="display: none;"><textarea id="sbCustomOriginVillages" name="customOriginVillages" rows="4" cols="50"></textarea></div>
                    </fieldset>
                    <fieldset>
                        <legend>${MintSDK.tt('Target villages')}</legend>
                        <div class="sb-grid sb-grid-2 ra-mb10">
                            <div>
                                <div><input type="radio" id="sbTargetGroupSelection" name="targetSelection" value="group" checked><label for="sbTargetGroupSelection">${MintSDK.tt('Group')}</label></div>
                                <div><input type="radio" id="sbTargetCustomSelection" name="targetSelection" value="custom"><label for="sbTargetCustomSelection">${MintSDK.tt('Custom')}</label></div>
                            </div>
                            <div id="sbTargetGroupMenuContainer">${groupMenuTarget}</div>
                        </div>
                        <div id="sbTargetCustomTextAreaContainer" style="display: none;"><textarea id="sbCustomTargetVillages" name="customTargetVillages" rows="4" cols="50"></textarea></div>
                    </fieldset>
                </div>
                <fieldset>
                    <legend>${MintSDK.tt('Settings')}</legend>
                    <div class="sb-grid sb-grid-2 ra-mb10">
                        <div><fieldset><legend>${MintSDK.tt('Hold back merchants')}</legend><input type="number" id="sbHoldBackMerchants" name="holdBackMerchants" min="0"></fieldset></div>
                        <div><fieldset><legend>${MintSDK.tt('Merchant bonus active?')}</legend><input type="checkbox" id="sbMerchantBonus" name="merchantBonus"></fieldset></div>
                    </div>
                    <fieldset class="ra-mb10">
                        <div><fieldset><legend>${MintSDK.tt('Send resources as')}</legend>
                            <div>${MintSDK.tt('Mint Ratio')} (28% ${MintSDK.tt('Wood')} / 30% ${MintSDK.tt('Clay')} / 25% ${MintSDK.tt('Iron')})</div>
                        </fieldset></div>
                    </fieldset>
                    <fieldset class="ra-mb10">
                        <div class="sb-grid sb-grid-2 ra-mb10">
                            <div><fieldset><legend>${MintSDK.tt('Hold back resources as')}</legend>
                                <div>
                                    <div><input type="radio" id="sbHoldBackResourcesAbsoluteRadio" name="holdBackResourcesType" value="absolute" checked><label for="sbHoldBackResourcesAbsoluteRadio">${MintSDK.tt('Absolute numbers')}</label></div>
                                    <div><input type="radio" id="sbHoldBackResourcesPercentageRadio" name="holdBackResourcesType" value="percentage"><label for="sbHoldBackResourcesPercentageRadio">${MintSDK.tt('Percentage')}</label></div>
                                </div>
                            </fieldset></div>
                            <div id="sbHoldBackResourcesAbsolute" style="display: none;"><fieldset class="sb-fieldset"><legend>${MintSDK.tt('Absolute numbers')}</legend>
                                <div class="sb-grid sb-grid-3 ra-mb10">
                                    <div><label for="sbHoldBackWood">${MintSDK.tt('Wood')}</label><input type="number" id="sbHoldBackWood" name="holdBackWood" min="0"></div>
                                    <div><label for="sbHoldBackClay">${MintSDK.tt('Clay')}</label><input type="number" id="sbHoldBackClay" name="holdBackClay" min="0"></div>
                                    <div><label for="sbHoldBackIron">${MintSDK.tt('Iron')}</label><input type="number" id="sbHoldBackIron" name="holdBackIron" min="0"></div>
                                </div>
                            </fieldset></div>
                            <div id="sbHoldBackResourcesPercentage" style="display: none;"><fieldset><legend>${MintSDK.tt('Percentage')}</legend><input type="number" id="sbHoldBackPercentage" name="holdBackPercentage" min="0" max="100"></fieldset></div>
                        </div>
                    </fieldset>
                    <div class="sb-grid sb-grid-75-25 ra-mb10">
                        <fieldset><legend>${MintSDK.tt('Max resources per target village')}</legend>
                            <div class="sb-grid sb-grid-3 ra-mb10">
                                <div><label for="sbMaxWood">${MintSDK.tt('Max wood per village')}</label><input type="number" id="sbMaxWood" name="maxWood" min="0"></div>
                                <div><label for="sbMaxClay">${MintSDK.tt('Max clay per village')}</label><input type="number" id="sbMaxClay" name="maxClay" min="0"></div>
                                <div><label for="sbMaxIron">${MintSDK.tt('Max iron per village')}</label><input type="number" id="sbMaxIron" name="maxIron" min="0"></div>
                            </div>
                        </fieldset>
                        <fieldset><legend>${MintSDK.tt('Reset all inputs')}</legend><div class="ra-tac"><button id="resetInput" class="add-entry-btn sbDeleteAllEntries">${MintSDK.tt('Reset Input')}</button></div></fieldset>
                    </div>
                    <div class="sb-grid ra-mb10">${contentArrivalTimeSelector}</div>
                </fieldset>
                <fieldset>
                    <legend>${MintSDK.tt('Calculate & Send')}</legend>
                    <div><button id="sbResCalculate" class="btn">${MintSDK.tt('Calculate')}</button></div>
                    <div id="sbTransportTableContainer"></div>
                    <div class="ir-auto-row">
                        <span>automatisch verschicken alle</span>
                        <input type="number" id="im_auto_interval" min="1" value="15">
                        <span>Min.</span>
                        <label class="ir-switch"><input type="checkbox" id="im_auto_toggle"><span class="ir-slider"></span></label>
                        <span id="im_auto_countdown" style="font-weight:bold;"></span>
                    </div>
                </fieldset>
            `;
            MintSDK.renderBoxWidget(content, 'IntelliMintWidget', 'im-widget', style);
            setupMintAutoSend();
        }

        function setupMintAutoSend() {
            let $toggle = $("#im_auto_toggle");
            let $interval = $("#im_auto_interval");
            let savedEnabled = localStorage.getItem(game_data.world + "im_auto_enabled") === "true";
            let savedInterval = parseInt(localStorage.getItem(game_data.world + "im_auto_interval")) || 15;

            $toggle.prop("checked", savedEnabled);
            $interval.val(savedInterval);

            $toggle.off("change").on("change", () => {
                let enabled = $toggle.is(":checked");
                let minutes = Math.max(1, parseInt($interval.val()) || 15);
                $interval.val(minutes);
                localStorage.setItem(game_data.world + "im_auto_enabled", enabled);
                localStorage.setItem(game_data.world + "im_auto_interval", minutes);
                if (enabled) {
                    startMintAutoSend(minutes, true);
                } else {
                    stopMintAutoSend();
                }
            });

            // changing the interval only reschedules the next run, it does not trigger a send right away
            $interval.off("change").on("change", () => {
                let minutes = Math.max(1, parseInt($interval.val()) || 15);
                $interval.val(minutes);
                localStorage.setItem(game_data.world + "im_auto_interval", minutes);
                if ($toggle.is(":checked")) {
                    startMintAutoSend(minutes, false);
                }
            });

            if (savedEnabled) {
                startMintAutoSend(savedInterval, true);
            }
        }

        function startMintAutoSend(minutes, runNow) {
            stopMintAutoSend();
            let intervalMs = minutes * 60 * 1000;
            if (runNow) runMintAutoCycle();
            im_autoNextRunAt = Date.now() + intervalMs;
            im_autoTimer = setInterval(() => {
                runMintAutoCycle();
                im_autoNextRunAt = Date.now() + intervalMs;
            }, intervalMs);
            startMintCountdown();
        }

        function stopMintAutoSend() {
            if (im_autoTimer) {
                clearInterval(im_autoTimer);
                im_autoTimer = null;
            }
            stopMintCountdown();
        }

        function startMintCountdown() {
            if (im_countdownTimer) clearInterval(im_countdownTimer);
            updateMintCountdownDisplay();
            im_countdownTimer = setInterval(updateMintCountdownDisplay, 1000);
        }

        function stopMintCountdown() {
            if (im_countdownTimer) {
                clearInterval(im_countdownTimer);
                im_countdownTimer = null;
            }
            $("#im_auto_countdown").text("");
        }

        function updateMintCountdownDisplay() {
            let remainingMs = Math.max(0, im_autoNextRunAt - Date.now());
            let totalSeconds = Math.floor(remainingMs / 1000);
            let mm = Math.floor(totalSeconds / 60);
            let ss = totalSeconds % 60;
            $("#im_auto_countdown").text(`nächster Versand in ${mm}:${ss.toString().padStart(2, "0")}`);
        }

        async function runMintAutoCycle() {
            try {
                await calculate();
                await new Promise(r => setTimeout(r, 1500));
                await autoClickAllMintSends();
            } catch (error) {
                console.error("IntelliMint auto-send: error during cycle", error);
            }
        }

        async function autoClickAllMintSends() {
            let buttons = $("#sbTransportTableContainer").find(".sendResources").toArray();
            for (let btn of buttons) {
                if (!document.body.contains(btn)) continue;
                if ($(btn).is(":disabled")) continue;
                btn.click();
                await new Promise(r => setTimeout(r, 500));
            }
        }

        function addEventHandlers() {
            jQuery('#sbDeleteAllEntries').off('click').on('click', function () {
                jQuery('#sbArrivalEntryTable .entry-row').remove();
                const localStorageObject = getLocalStorage();
                localStorageObject.sbArrivalTimes = [];
                saveLocalStorage(localStorageObject);
                jQuery('#sbArrivalEntryTable').css('display', 'none');
            });

            initializeSavedEntries();

            jQuery('#sbArrivalEntryTable').off('click', '.delete-entry-btn').on('click', '.delete-entry-btn', function () {
                const idParts = this.id.split('-');
                const startTime = Number(idParts[1]);
                const endTime = Number(idParts[2]);
                const updatedLocalStorage = getLocalStorage();
                updatedLocalStorage.sbArrivalTimes = updatedLocalStorage.sbArrivalTimes.filter(timeSpan => !(timeSpan[0] === startTime && timeSpan[1] === endTime));
                saveLocalStorage(updatedLocalStorage);
                jQuery(this).parent().parent().remove();
                if (updatedLocalStorage.sbArrivalTimes.length === 0) jQuery('#sbArrivalEntryTable').css('display', 'none');
            });

            jQuery('#sbAddTimeEntry').off('click').on('click', async function (e) {
                e.preventDefault();
                const startTimeString = jQuery('#startDateTime').val();
                const endTimeString = jQuery('#endDateTime').val();
                if (startTimeString && endTimeString) {
                    const currentTime = Date.now();
                    const startTime = new Date(startTimeString).getTime();
                    const endTime = new Date(endTimeString).getTime();
                    if (!isNaN(startTime) && !isNaN(endTime) && startTime < endTime && currentTime < endTime) {
                        const localStorageObject = getLocalStorage();
                        const isDuplicate = localStorageObject.sbArrivalTimes.some(timeSpan => isEqual(timeSpan, [startTime, endTime]));
                        if (isDuplicate) { UI.ErrorMessage(`${MintSDK.tt('This entry already exists.')}`); return; }

                        localStorageObject.sbArrivalTimes.push([startTime, endTime]);
                        saveLocalStorage(localStorageObject);

                        const newEntryRow = jQuery('<tr class="entry-row"></tr>');
                        newEntryRow.append(`<td class="entry-start">${formatLocalizedTime(new Date(startTime))}</td>`);
                        newEntryRow.append(`<td class="entry-end">${formatLocalizedTime(new Date(endTime))}</td>`);
                        newEntryRow.append(`<td class="ra-tac"><button class="delete-entry-btn" id="btn-${startTime}-${endTime}">X</button></td>`);
                        jQuery('#sbArrivalEntryTable').append(newEntryRow);
                        jQuery('#sbArrivalEntryTable').css('display', 'table');

                        newEntryRow.find('.delete-entry-btn').on('click', function () {
                            newEntryRow.remove();
                            const updatedLocalStorage = getLocalStorage();
                            updatedLocalStorage.sbArrivalTimes = updatedLocalStorage.sbArrivalTimes.filter(timeSpan => !isEqual(timeSpan, [startTime, endTime]));
                            saveLocalStorage(updatedLocalStorage);
                            if (updatedLocalStorage.sbArrivalTimes.length === 0) jQuery('#sbArrivalEntryTable').css('display', 'none');
                        });
                    } else {
                        UI.ErrorMessage(`${MintSDK.tt('Invalid entry. Please select valid start and end times.')}`);
                    }
                } else {
                    UI.ErrorMessage(`${MintSDK.tt('Invalid entry. Please check the selected times.')}`);
                }
            });

            $('#resetInput').off('click').on('click', function () { resetInputFields(); });
            $('#sbResCalculate').off('click').on('click', async function (e) { e.preventDefault(); await calculate(); });

            allIdsRS.forEach(function (id) { $('#' + id).off('change').on('change', handleInputChange); });
        }

        function generateCSS() {
            return `
                .sb-grid-5 { grid-template-columns: repeat(5, 1fr); }
                .sb-grid-4 { grid-template-columns: repeat(4, 1fr); }
                .sb-grid-3 { grid-template-columns: repeat(3, 1fr); }
                .sb-grid-2 { grid-template-columns: repeat(2, 1fr); }
                .sb-grid-75-25 { grid-template-columns: 75% 25%; }
                .sb-grid { display: grid; grid-gap: 10px; }
                #ir_panel_mint, #ir_panel_mint * { color:${textColor}; }
                #ir_panel_mint fieldset { border: 1px solid ${borderColor}; border-radius: 4px; padding: 9px; }
                #ir_panel_mint legend { font-size: 12px; font-weight: bold; color:${textColor}; }
                #ir_panel_mint input[type="number"], #ir_panel_mint input[type="email"], #ir_panel_mint select, #ir_panel_mint input[type="datetime-local"] {
                    padding: 8px; font-size: 14px; border: 1px solid ${borderColor}; border-radius: 3px; background:${backgroundInput}; color:${textColor};
                }
                #ir_panel_mint input[type="number"] { width: 90px; }
                #ir_panel_mint select { width: 165px; }
                #ir_panel_mint input[type="checkbox"] { margin-right: 5px; transform: scale(1.2); }
                #ir_panel_mint .btn-confirm-yes { padding: 3px; margin: 5px; }
                #ir_panel_mint input[type="number"]::-webkit-inner-spin-button, #ir_panel_mint input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
                #ir_panel_mint input[type="number"] { -moz-appearance: textfield; }
                #ir_panel_mint input:focus, #ir_panel_mint select:focus { outline: none; border-color: ${borderColor}; box-shadow: 0 0 5px ${borderColor}; }
                #ir_panel_mint .result-text { width: 100%; height: 150px; border: 1px solid ${borderColor}; border-radius: 4px; padding: 8px; font-size: 14px; margin-top: 5px; resize: none; background:${backgroundInput}; color:${textColor}; }
                #ir_panel_mint #resetInput { padding: 8px; font-size: 12px; color: white; font-weight: bold; background: linear-gradient(to bottom, #af281d 0%,#801006 100%); border: 1px solid; border-color: #600000; border-radius: 3px; cursor: pointer; }
                #ir_panel_mint #resetInput:hover { background: linear-gradient(to bottom, #c92722 0%,#a00d08 100%); }
                #ir_panel_mint .sb-fieldset { border: 1px solid ${borderColor}; border-radius: 4px; padding: 10px; }
                #ir_panel_mint .ra-table th img { display: block; margin: 0 auto; }
                #ir_panel_mint .sb-grid-item-text { font-size: 16px; text-align: center; line-height: 34px; }
                #ir_panel_mint .add-entry-btn { padding: 10px; font-size: 17px; color: white; background: linear-gradient(to bottom, #0bac00 0%,#0e7a1e 100%); border: 1px solid; border-color: #006712; border-radius: 3px; cursor: pointer; }
                #ir_panel_mint .add-entry-btn, #ir_panel_mint .sbDeleteAllEntries { width: 90%; height: 40px; display: inline-block; text-align: center; }
                #ir_panel_mint .sbDeleteAllEntries { padding: 8px; font-size: 11.5px; font-weight: bold; background: linear-gradient(to bottom, #af281d 0%,#801006 100%); }
                #ir_panel_mint .sbDeleteAllEntries:hover { background: linear-gradient(to bottom, #c92722 0%,#a00d08 100%); }
                #ir_panel_mint .sb-mb5 { margin-bottom: 5px !important; }
                #ir_panel_mint #sbAddTimeEntry:hover { background: linear-gradient(to bottom, #13c600 0%,#129e23 100%); }
                #ir_panel_mint .entries-table { width: 100%; border-collapse: collapse; }
                #ir_panel_mint .entries-table th { background-color: ${backgroundHeader}; text-align: left; padding: 10px; }
                #ir_panel_mint .entries-table td { border: 1px solid ${borderColor}; padding: 8px; }
                #ir_panel_mint .entry-row:nth-child(even) { background-color: ${backgroundAlternateTableEven}; }
                #ir_panel_mint .entry-row:nth-child(odd) { background-color: ${backgroundAlternateTableOdd}; }
                #ir_panel_mint .entry-start, #ir_panel_mint .entry-end { text-align: center; }
                #ir_panel_mint .delete-entry-btn { width: 30%; padding: 6px; border: 1px solid black; border-radius: 3px; color: white; cursor: pointer; font-weight: bold; background: linear-gradient(to bottom, #af281d 0%,#801006 100%); }
                #ir_panel_mint .delete-entry-btn:hover { background: linear-gradient(to bottom, #c92722 0%,#a00d08 100%); }
                #ir_panel_mint input[type="radio"] + label, #ir_panel_mint input[type="checkbox"] + label { display: inline-block; margin-left: 5px; }
                #ir_panel_mint #transportTable { width:100%; border-collapse:collapse; }
                #ir_panel_mint #transportTable th, #ir_panel_mint #transportTable td { padding:6px; border:1px solid ${borderColor}; text-align:center; }
                #ir_panel_mint .evenRow { background-color:${backgroundAlternateTableEven}; }
                #ir_panel_mint .oddRow { background-color:${backgroundAlternateTableOdd}; }
                #ir_panel_mint .icon.header.wood, #ir_panel_mint .icon.header.stone, #ir_panel_mint .icon.header.iron { display:inline-block; width:14px; height:14px; margin-left:3px; vertical-align:middle; }
            `;
        }

        async function calculate() {
            totalWoodSent = 0; totalClaySent = 0; totalIronSent = 0;
            const localStorageObject = getLocalStorage();
            if (!verifyInputs(localStorageObject)) return;

            const originVillages = localStorageObject.sbOriginCustomSelection ? localStorageObject.sbCustomOriginVillages.split(' ') : await getVillagesByGroupId(localStorageObject.sbOriginGroupsFilter);
            const targetVillages = localStorageObject.sbTargetCustomSelection ? localStorageObject.sbCustomTargetVillages.split(' ') : await getVillagesByGroupId(localStorageObject.sbTargetGroupsFilter);

            const transportData = calculateMintRatioResources(originVillages, targetVillages);

            createTransportTable(transportData);
        }

        function createTransportTable(transportData) {
            if ($("#sbTransportTable")[0]) $("#sbTransportTable")[0].remove();
            transportData.sort((a, b) => a.travelTime - b.travelTime);

            let htmlString = `
                <div id="sbTransportTable">
                    <table id="transportTable" width="100%">
                        <thead>
                            <tr><th colspan="7" style="text-align:center">${MintSDK.tt('Total Resources Sent')}</th></tr>
                            <tr><th colspan="2"></th><th>${MintSDK.tt('Wood')}</th><th>${MintSDK.tt('Clay')}</th><th>${MintSDK.tt('Iron')}</th><th colspan="2"></th></tr>
                            <tr><th colspan="2"></th><th id="totalWoodSent">0</th><th id="totalClaySent">0</th><th id="totalIronSent">0</th><th colspan="2"></th></tr>
                            <tr>
                                <th>${MintSDK.tt('Origin')}</th><th>${MintSDK.tt('Target')}</th><th>${MintSDK.tt('Travel Time')}</th>
                                <th>${MintSDK.tt('Wood')}</th><th>${MintSDK.tt('Clay')}</th><th>${MintSDK.tt('Iron')}</th><th>${MintSDK.tt('Send')}</th>
                            </tr>
                        </thead>
                        <tbody id="transportTableBody"></tbody>
                    </table>
                </div>`.trim();

            let uiDiv = document.createElement('div');
            uiDiv.innerHTML = htmlString;
            $("#sbTransportTableContainer").prepend(uiDiv.firstChild);

            let tableBody = $("#transportTableBody");
            transportData.forEach((transport, index) => {
                const targetVillageId = villageData[transport.target];
                let rowClass = index % 2 === 0 ? "evenRow" : "oddRow";
                let travelTime = msToTime(transport.travelTime);
                let rowHtml = `
                    <tr id="row-${index}" class="${rowClass}">
                        <td><a href="${transport.url}">${transport.name}</a></td>
                        <td>${transport.target}</td>
                        <td>${travelTime}</td>
                        <td>${Math.floor(transport.wood)}<span class="icon header wood"></span></td>
                        <td>${Math.floor(transport.clay)}<span class="icon header stone"></span></td>
                        <td>${Math.floor(transport.iron)}<span class="icon header iron"></span></td>
                        <td style="text-align:center">
                            <input type="button" class="btn evt-confirm-btn btn-confirm-yes sendResources" value="Send" onclick="imSendResource(${transport.id}, '${targetVillageId}', ${Math.floor(transport.wood)}, ${Math.floor(transport.clay)}, ${Math.floor(transport.iron)}, ${index})">
                        </td>
                    </tr>`;
                tableBody.append(rowHtml);
            });

            $("#transportTableBody input[type='button']").first().focus();
        }

        window.imSendResource = function (originVillageId, targetVillageId, wood, clay, iron, index) {
            $('.sendResources').prop('disabled', true);

            setTimeout(function () {
                $("#row-" + index).remove();
                $('.sendResources').prop('disabled', false);
                const nextButton = $("#transportTableBody input[type='button']").first();
                if (nextButton.length) nextButton.focus();

                if ($("#transportTableBody tr").length <= 0) {
                    alert(MintSDK.tt("Finished sending!"));
                    if ($(".btn-pp").length > 0) $(".btn-pp").remove();
                    throw Error("Done.");
                }
            }, 200);

            var data = { "target_id": targetVillageId, "wood": wood, "stone": clay, "iron": iron };
            TribalWars.post("market", { ajaxaction: "map_send", village: originVillageId }, data, function (response) {
                Dialog.close();
                UI.SuccessMessage(response.message);
                totalWoodSent += Math.floor(wood);
                totalClaySent += Math.floor(clay);
                totalIronSent += Math.floor(iron);
                $("#totalWoodSent").eq(0).text(`${convertToNumberWithDots(totalWoodSent)}`);
                $("#totalClaySent").eq(0).text(`${convertToNumberWithDots(totalClaySent)}`);
                $("#totalIronSent").eq(0).text(`${convertToNumberWithDots(totalIronSent)}`);
            }, false);
        };

        function convertToNumberWithDots(number) { return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, "."); }

        function msToTime(duration) {
            let seconds = Math.floor((duration / 1000) % 60);
            let minutes = Math.floor((duration / (1000 * 60)) % 60);
            let hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
            hours = (hours < 10) ? "0" + hours : hours;
            minutes = (minutes < 10) ? "0" + minutes : minutes;
            seconds = (seconds < 10) ? "0" + seconds : seconds;
            return hours + ":" + minutes + ":" + seconds;
        }

        function calculateAbsoluteResources(originVillages, targetVillages) {
            const localStorageObject = getLocalStorage();
            const merchantBonus = parseBool(localStorageObject.sbMerchantBonus);
            const holdBackMerchants = Number(localStorageObject.sbHoldBackMerchants);
            const holdBackResourcesAbsolute = parseBool(localStorageObject.sbHoldBackResourcesAbsoluteRadio);
            const holdBackResourcesPercentage = parseBool(localStorageObject.sbHoldBackResourcesPercentageRadio);
            const holdBackWood = Number(localStorageObject.sbHoldBackWood);
            const holdBackClay = Number(localStorageObject.sbHoldBackClay);
            const holdBackIron = Number(localStorageObject.sbHoldBackIron);
            const holdBackPercentage = Number(localStorageObject.sbHoldBackPercentage);
            const maxWood = Number(localStorageObject.sbMaxWood);
            const maxClay = Number(localStorageObject.sbMaxClay);
            const maxIron = Number(localStorageObject.sbMaxIron);
            const arrivalTimes = getLocalStorage().sbArrivalTimes;

            const woodToSend = Number(localStorageObject.sbSendWood);
            const clayToSend = Number(localStorageObject.sbSendClay);
            const ironToSend = Number(localStorageObject.sbSendIron);

            if ((maxWood > 0 && woodToSend > maxWood) || (maxClay > 0 && clayToSend > maxClay) || (maxIron > 0 && ironToSend > maxIron)) {
                UI.ErrorMessage(MintSDK.tt('Absolute send exceeds max resource per target village setting.'));
                return [];
            }

            const merchantCapacity = merchantBonus ? 1500 : 1000;

            const playerDataCopy = JSON.parse(JSON.stringify(playerData));
            playerDataCopy.forEach(village => {
                village.availableMerchants = Math.max(0, village.availableMerchants - holdBackMerchants);
                if (holdBackResourcesAbsolute) {
                    village.wood = Math.max(0, village.wood - holdBackWood);
                    village.clay = Math.max(0, village.clay - holdBackClay);
                    village.iron = Math.max(0, village.iron - holdBackIron);
                } else if (holdBackResourcesPercentage) {
                    const holdBackAmount = village.warehouseCapacity * (holdBackPercentage / 100);
                    village.wood = Math.max(0, village.wood - holdBackAmount);
                    village.clay = Math.max(0, village.clay - holdBackAmount);
                    village.iron = Math.max(0, village.iron - holdBackAmount);
                }
            });

            const filteredPlayerData = playerDataCopy.filter(village => originVillages.includes(village.coord));
            const validVillages = filteredPlayerData.filter(village => (village.availableMerchants > 0 && (village.wood > 0 || village.clay > 0 || village.iron > 0)));
            const uniqueTargetVillages = [...new Set(targetVillages)];

            const targetVillageDemand = {};
            uniqueTargetVillages.forEach(targetVillage => { targetVillageDemand[targetVillage] = { wood: woodToSend, clay: clayToSend, iron: ironToSend }; });

            const currentTime = Date.now();
            const originTargetPairs = {};
            validVillages.forEach(originVillage => {
                const possibleTargets = uniqueTargetVillages.map(targetVillage => {
                    if (originVillage.coord === targetVillage) return null;
                    const travelTime = calculateTravelTime(originVillage.coord, targetVillage, merchantBonus);
                    const arrivalTime = currentTime + travelTime;
                    const withinArrivalTime = arrivalTimes.length === 0 || arrivalTimes.some(([start, end]) => arrivalTime >= start && arrivalTime <= end);
                    return withinArrivalTime ? { coord: targetVillage, travelTime } : null;
                }).filter(Boolean);
                if (possibleTargets.length > 0) originTargetPairs[originVillage.coord] = possibleTargets.sort((a, b) => a.travelTime - b.travelTime);
            });

            const totalDemandWood = uniqueTargetVillages.length * woodToSend;
            const totalDemandClay = uniqueTargetVillages.length * clayToSend;
            const totalDemandIron = uniqueTargetVillages.length * ironToSend;
            const totalDemandAll = totalDemandWood + totalDemandClay + totalDemandIron;

            const totalAvailableWood = validVillages.reduce((sum, village) => sum + Number(village.wood), 0);
            const totalAvailableClay = validVillages.reduce((sum, village) => sum + Number(village.clay), 0);
            const totalAvailableIron = validVillages.reduce((sum, village) => sum + Number(village.iron), 0);
            const totalMerchantCapacity = validVillages.reduce((sum, village) => sum + (Number(village.availableMerchants) * merchantCapacity), 0);

            if (totalAvailableWood < totalDemandWood || totalAvailableClay < totalDemandClay || totalAvailableIron < totalDemandIron) {
                UI.ErrorMessage(MintSDK.tt('Not enough total origin resources to fulfill absolute target demand.'));
                return [];
            }
            if (totalMerchantCapacity < totalDemandAll) {
                UI.ErrorMessage(MintSDK.tt('Not enough total merchant capacity to fulfill absolute target demand.'));
                return [];
            }

            const transportData = [];
            uniqueTargetVillages.forEach(targetCoord => {
                const demand = targetVillageDemand[targetCoord];
                while (demand.wood > 0 || demand.clay > 0 || demand.iron > 0) {
                    const eligibleOrigins = validVillages
                        .filter(village => {
                            if (village.availableMerchants <= 0) return false;
                            if (village.wood <= 0 && village.clay <= 0 && village.iron <= 0) return false;
                            const possibleTargets = originTargetPairs[village.coord] || [];
                            return possibleTargets.some(target => target.coord === targetCoord);
                        })
                        .map(village => {
                            const travel = (originTargetPairs[village.coord] || []).find(target => target.coord === targetCoord);
                            return { village, travelTime: travel ? travel.travelTime : Number.MAX_SAFE_INTEGER };
                        })
                        .sort((a, b) => a.travelTime - b.travelTime);

                    if (eligibleOrigins.length === 0) break;

                    const { village: originVillage, travelTime } = eligibleOrigins[0];
                    let remainingCapacity = originVillage.availableMerchants * merchantCapacity;

                    let wood = 0, clay = 0, iron = 0;
                    while (remainingCapacity > 0) {
                        const chunkSize = Math.min(remainingCapacity, merchantCapacity);
                        const availableResources = [
                            demand.wood - wood > 0 && originVillage.wood - wood > 0,
                            demand.clay - clay > 0 && originVillage.clay - clay > 0,
                            demand.iron - iron > 0 && originVillage.iron - iron > 0
                        ];
                        const activeCount = availableResources.filter(Boolean).length;
                        if (activeCount === 0) break;
                        const perResource = Math.floor(chunkSize / activeCount);
                        let spent = 0;
                        if (availableResources[0]) { const add = Math.min(perResource, demand.wood - wood, originVillage.wood - wood); wood += add; spent += add; }
                        if (availableResources[1]) { const add = Math.min(perResource, demand.clay - clay, originVillage.clay - clay); clay += add; spent += add; }
                        if (availableResources[2]) { const add = Math.min(perResource, demand.iron - iron, originVillage.iron - iron); iron += add; spent += add; }
                        if (spent === 0) break;
                        remainingCapacity -= spent;
                    }

                    const totalToSend = wood + clay + iron;
                    if (totalToSend <= 0) {
                        originTargetPairs[originVillage.coord] = (originTargetPairs[originVillage.coord] || []).filter(target => target.coord !== targetCoord);
                        continue;
                    }

                    const merchantsUsed = Math.ceil(totalToSend / merchantCapacity);
                    transportData.push({ origin: originVillage.coord, target: targetCoord, wood, clay, iron, url: originVillage.url, name: originVillage.name, id: originVillage.id, travelTime });

                    originVillage.wood -= wood; originVillage.clay -= clay; originVillage.iron -= iron;
                    originVillage.availableMerchants -= merchantsUsed;
                    demand.wood -= wood; demand.clay -= clay; demand.iron -= iron;
                }
            });

            const remainingTargets = uniqueTargetVillages.filter(targetCoord => {
                const demand = targetVillageDemand[targetCoord];
                return demand.wood > 0 || demand.clay > 0 || demand.iron > 0;
            });
            if (remainingTargets.length > 0) {
                UI.ErrorMessage(MintSDK.tt('Unable to fully satisfy all absolute target demands with current resources/merchants/arrival windows.'));
                return [];
            }
            return transportData;
        }

        function calculateRatioResources(originVillages, targetVillages) {
            const localStorageObject = getLocalStorage();
            const merchantBonus = parseBool(localStorageObject.sbMerchantBonus);
            const holdBackMerchants = Number(localStorageObject.sbHoldBackMerchants);
            const holdBackResourcesAbsolute = parseBool(localStorageObject.sbHoldBackResourcesAbsoluteRadio);
            const holdBackResourcesPercentage = parseBool(localStorageObject.sbHoldBackResourcesPercentageRadio);
            const holdBackWood = Number(localStorageObject.sbHoldBackWood);
            const holdBackClay = Number(localStorageObject.sbHoldBackClay);
            const holdBackIron = Number(localStorageObject.sbHoldBackIron);
            const holdBackPercentage = Number(localStorageObject.sbHoldBackPercentage);
            const maxWood = Number(localStorageObject.sbMaxWood);
            const maxClay = Number(localStorageObject.sbMaxClay);
            const maxIron = Number(localStorageObject.sbMaxIron);
            const arrivalTimes = getLocalStorage().sbArrivalTimes;

            const woodRatio = Number(localStorageObject.sbSendWoodRatio) / 100;
            const clayRatio = Number(localStorageObject.sbSendClayRatio) / 100;
            const ironRatio = Number(localStorageObject.sbSendIronRatio) / 100;
            const merchantCapacity = merchantBonus ? 1500 : 1000;

            const playerDataCopy = JSON.parse(JSON.stringify(playerData));
            playerDataCopy.forEach(village => {
                village.availableMerchants = Math.max(0, village.availableMerchants - holdBackMerchants);
                if (holdBackResourcesAbsolute) {
                    village.wood = Math.max(0, village.wood - holdBackWood);
                    village.clay = Math.max(0, village.clay - holdBackClay);
                    village.iron = Math.max(0, village.iron - holdBackIron);
                } else if (holdBackResourcesPercentage) {
                    const holdBackAmount = village.warehouseCapacity * (holdBackPercentage / 100);
                    village.wood = Math.max(0, village.wood - holdBackAmount);
                    village.clay = Math.max(0, village.clay - holdBackAmount);
                    village.iron = Math.max(0, village.iron - holdBackAmount);
                }
            });

            const filteredPlayerData = playerDataCopy.filter(village => originVillages.includes(village.coord));
            const validVillages = filteredPlayerData.filter(village => (village.wood > 0 || village.clay > 0 || village.iron > 0) && village.availableMerchants > 0);

            const targetVillageResources = {};
            const INFINITE_RESOURCES = Number.MAX_SAFE_INTEGER;
            targetVillages.forEach(targetVillage => {
                targetVillageResources[targetVillage] = {
                    wood: maxWood === 0 ? INFINITE_RESOURCES : maxWood,
                    clay: maxClay === 0 ? INFINITE_RESOURCES : maxClay,
                    iron: maxIron === 0 ? INFINITE_RESOURCES : maxIron
                };
            });

            const currentTime = Date.now();
            const originTargetPairs = {};
            validVillages.forEach(originVillage => {
                const possibleTargets = targetVillages.map(targetVillage => {
                    if (originVillage.coord === targetVillage) return null;
                    const travelTime = calculateTravelTime(originVillage.coord, targetVillage, merchantBonus);
                    const arrivalTime = currentTime + travelTime;
                    const withinArrivalTime = arrivalTimes.length === 0 || arrivalTimes.some(([start, end]) => arrivalTime >= start && arrivalTime <= end);
                    return withinArrivalTime ? { coord: targetVillage, travelTime } : null;
                }).filter(Boolean);
                if (possibleTargets.length > 0) originTargetPairs[originVillage.coord] = possibleTargets.sort((a, b) => a.travelTime - b.travelTime);
            });

            const transportData = [];
            while (Object.keys(originTargetPairs).length > 0) {
                const originCoord = Object.keys(originTargetPairs)[0];
                const originVillage = validVillages.find(village => village.coord === originCoord);
                if (!originVillage || !originTargetPairs[originCoord] || originTargetPairs[originCoord].length === 0) { delete originTargetPairs[originCoord]; continue; }
                const targetVillage = originTargetPairs[originCoord][0];
                const totalMerchantCapacity = originVillage.availableMerchants * merchantCapacity;

                let maxWoodTransport = totalMerchantCapacity * woodRatio;
                let maxClayTransport = totalMerchantCapacity * clayRatio;
                let maxIronTransport = totalMerchantCapacity * ironRatio;

                let adjustmentFactor = 1;
                if (maxWoodTransport > originVillage.wood) { adjustmentFactor = originVillage.wood / maxWoodTransport; maxWoodTransport *= adjustmentFactor; maxClayTransport *= adjustmentFactor; maxIronTransport *= adjustmentFactor; }
                if (maxClayTransport > originVillage.clay) { adjustmentFactor = originVillage.clay / maxClayTransport; maxWoodTransport *= adjustmentFactor; maxClayTransport *= adjustmentFactor; maxIronTransport *= adjustmentFactor; }
                if (maxIronTransport > originVillage.iron) { adjustmentFactor = originVillage.iron / maxIronTransport; maxWoodTransport *= adjustmentFactor; maxClayTransport *= adjustmentFactor; maxIronTransport *= adjustmentFactor; }

                const resourcesToSend = {
                    wood: Math.floor(Math.min(maxWoodTransport, targetVillageResources[targetVillage.coord].wood)),
                    clay: Math.floor(Math.min(maxClayTransport, targetVillageResources[targetVillage.coord].clay)),
                    iron: Math.floor(Math.min(maxIronTransport, targetVillageResources[targetVillage.coord].iron))
                };
                const totalToSend = resourcesToSend.wood + resourcesToSend.clay + resourcesToSend.iron;
                if (totalToSend <= 0) {
                    originTargetPairs[originCoord] = originTargetPairs[originCoord].filter(target => target.coord !== targetVillage.coord);
                    if (originTargetPairs[originCoord].length === 0) delete originTargetPairs[originCoord];
                    continue;
                }

                if (resourcesToSend.wood > 0 || resourcesToSend.clay > 0 || resourcesToSend.iron > 0) {
                    transportData.push({ origin: originVillage.coord, target: targetVillage.coord, wood: resourcesToSend.wood, clay: resourcesToSend.clay, iron: resourcesToSend.iron, url: originVillage.url, name: originVillage.name, id: originVillage.id, travelTime: originTargetPairs[originCoord][0].travelTime });

                    targetVillageResources[targetVillage.coord].wood -= resourcesToSend.wood;
                    targetVillageResources[targetVillage.coord].clay -= resourcesToSend.clay;
                    targetVillageResources[targetVillage.coord].iron -= resourcesToSend.iron;

                    originVillage.wood -= resourcesToSend.wood;
                    originVillage.clay -= resourcesToSend.clay;
                    originVillage.iron -= resourcesToSend.iron;
                    originVillage.availableMerchants -= Math.ceil(totalToSend / merchantCapacity);

                    if (targetVillageResources[targetVillage.coord].wood <= 0 || targetVillageResources[targetVillage.coord].clay <= 0 || targetVillageResources[targetVillage.coord].iron <= 0) {
                        Object.keys(originTargetPairs).forEach(oc => { originTargetPairs[oc] = originTargetPairs[oc].filter(target => target.coord !== targetVillage.coord); });
                    }
                }

                if (originVillage.availableMerchants <= 0 || (woodRatio > 0 && originVillage.wood <= 0) || (clayRatio > 0 && originVillage.clay <= 0) || (ironRatio > 0 && originVillage.iron <= 0) || !originTargetPairs[originCoord] || originTargetPairs[originCoord].length === 0) {
                    delete originTargetPairs[originCoord];
                }
            }
            return transportData;
        }

        function calculateMintRatioResources(originVillages, targetVillages) {
            const woodPercentage = 28000 / 83000;
            const clayPercentage = 30000 / 83000;
            const ironPercentage = 25000 / 83000;

            const localStorageObject = getLocalStorage();
            const merchantBonus = parseBool(localStorageObject.sbMerchantBonus);
            const holdBackMerchants = Number(localStorageObject.sbHoldBackMerchants);
            const holdBackResourcesAbsolute = parseBool(localStorageObject.sbHoldBackResourcesAbsoluteRadio);
            const holdBackResourcesPercentage = parseBool(localStorageObject.sbHoldBackResourcesPercentageRadio);
            const holdBackWood = Number(localStorageObject.sbHoldBackWood);
            const holdBackClay = Number(localStorageObject.sbHoldBackClay);
            const holdBackIron = Number(localStorageObject.sbHoldBackIron);
            const holdBackPercentage = Number(localStorageObject.sbHoldBackPercentage);
            const maxWood = Number(localStorageObject.sbMaxWood);
            const maxClay = Number(localStorageObject.sbMaxClay);
            const maxIron = Number(localStorageObject.sbMaxIron);
            const arrivalTimes = getLocalStorage().sbArrivalTimes;
            const merchantCapacity = merchantBonus ? 1500 : 1000;

            const playerDataCopy = JSON.parse(JSON.stringify(playerData));
            playerDataCopy.forEach(village => {
                village.availableMerchants = Math.max(0, village.availableMerchants - holdBackMerchants);
                if (holdBackResourcesAbsolute) {
                    village.wood = Math.max(0, village.wood - holdBackWood);
                    village.clay = Math.max(0, village.clay - holdBackClay);
                    village.iron = Math.max(0, village.iron - holdBackIron);
                } else if (holdBackResourcesPercentage) {
                    const holdBackAmount = village.warehouseCapacity * (holdBackPercentage / 100);
                    village.wood = Math.max(0, village.wood - holdBackAmount);
                    village.clay = Math.max(0, village.clay - holdBackAmount);
                    village.iron = Math.max(0, village.iron - holdBackAmount);
                }
            });

            const filteredPlayerData = playerDataCopy.filter(village => originVillages.includes(village.coord));
            const validVillages = filteredPlayerData.filter(village => village.wood > 0 && village.clay > 0 && village.iron > 0 && village.availableMerchants > 0);

            const targetVillageResources = {};
            const INFINITE_RESOURCES = Number.MAX_SAFE_INTEGER;
            targetVillages.forEach(targetVillage => {
                targetVillageResources[targetVillage] = {
                    wood: maxWood === 0 ? INFINITE_RESOURCES : maxWood,
                    clay: maxClay === 0 ? INFINITE_RESOURCES : maxClay,
                    iron: maxIron === 0 ? INFINITE_RESOURCES : maxIron
                };
            });

            const currentTime = Date.now();
            const originTargetPairs = {};
            validVillages.forEach(originVillage => {
                const possibleTargets = targetVillages.map(targetVillage => {
                    if (originVillage.coord === targetVillage) return null;
                    const travelTime = calculateTravelTime(originVillage.coord, targetVillage, merchantBonus);
                    const arrivalTime = currentTime + travelTime;
                    const withinArrivalTime = arrivalTimes.length === 0 || arrivalTimes.some(([start, end]) => arrivalTime >= start && arrivalTime <= end);
                    return withinArrivalTime ? { coord: targetVillage, travelTime } : null;
                }).filter(Boolean);
                if (possibleTargets.length > 0) originTargetPairs[originVillage.coord] = possibleTargets.sort((a, b) => a.travelTime - b.travelTime);
            });

            const transportData = [];
            while (Object.keys(originTargetPairs).length > 0) {
                const originCoord = Object.keys(originTargetPairs)[0];
                const originVillage = validVillages.find(village => village.coord === originCoord);
                if (!originVillage || !originTargetPairs[originCoord] || originTargetPairs[originCoord].length === 0) { delete originTargetPairs[originCoord]; continue; }
                const targetVillage = originTargetPairs[originCoord][0];
                const totalMerchantCapacity = originVillage.availableMerchants * merchantCapacity;

                let maxWoodTransport = totalMerchantCapacity * woodPercentage;
                let maxClayTransport = totalMerchantCapacity * clayPercentage;
                let maxIronTransport = totalMerchantCapacity * ironPercentage;

                let adjustmentFactor = 1;
                if (maxWoodTransport > originVillage.wood) { adjustmentFactor = originVillage.wood / maxWoodTransport; maxWoodTransport *= adjustmentFactor; maxClayTransport *= adjustmentFactor; maxIronTransport *= adjustmentFactor; }
                if (maxClayTransport > originVillage.clay) { adjustmentFactor = originVillage.clay / maxClayTransport; maxWoodTransport *= adjustmentFactor; maxClayTransport *= adjustmentFactor; maxIronTransport *= adjustmentFactor; }
                if (maxIronTransport > originVillage.iron) { adjustmentFactor = originVillage.iron / maxIronTransport; maxWoodTransport *= adjustmentFactor; maxClayTransport *= adjustmentFactor; maxIronTransport *= adjustmentFactor; }

                const resourcesToSend = {
                    wood: Math.floor(Math.min(maxWoodTransport, targetVillageResources[targetVillage.coord].wood)),
                    clay: Math.floor(Math.min(maxClayTransport, targetVillageResources[targetVillage.coord].clay)),
                    iron: Math.floor(Math.min(maxIronTransport, targetVillageResources[targetVillage.coord].iron))
                };
                const totalToSend = resourcesToSend.wood + resourcesToSend.clay + resourcesToSend.iron;
                if (totalToSend <= 0) {
                    originTargetPairs[originCoord] = originTargetPairs[originCoord].filter(target => target.coord !== targetVillage.coord);
                    if (originTargetPairs[originCoord].length === 0) delete originTargetPairs[originCoord];
                    continue;
                }

                if (resourcesToSend.wood > 0 || resourcesToSend.clay > 0 || resourcesToSend.iron > 0) {
                    transportData.push({ origin: originVillage.coord, target: targetVillage.coord, wood: resourcesToSend.wood, clay: resourcesToSend.clay, iron: resourcesToSend.iron, url: originVillage.url, name: originVillage.name, id: originVillage.id, travelTime: originTargetPairs[originCoord][0].travelTime });

                    targetVillageResources[targetVillage.coord].wood -= resourcesToSend.wood;
                    targetVillageResources[targetVillage.coord].clay -= resourcesToSend.clay;
                    targetVillageResources[targetVillage.coord].iron -= resourcesToSend.iron;

                    originVillage.wood -= resourcesToSend.wood;
                    originVillage.clay -= resourcesToSend.clay;
                    originVillage.iron -= resourcesToSend.iron;
                    originVillage.availableMerchants -= Math.ceil(totalToSend / merchantCapacity);

                    if (targetVillageResources[targetVillage.coord].wood <= 0 || targetVillageResources[targetVillage.coord].clay <= 0 || targetVillageResources[targetVillage.coord].iron <= 0) {
                        Object.keys(originTargetPairs).forEach(oc => { originTargetPairs[oc] = originTargetPairs[oc].filter(target => target.coord !== targetVillage.coord); });
                    }
                }

                if (originVillage.availableMerchants <= 0 || originVillage.wood <= 0 || originVillage.clay <= 0 || originVillage.iron <= 0 || !originTargetPairs[originCoord] || originTargetPairs[originCoord].length === 0) {
                    delete originTargetPairs[originCoord];
                }
            }
            return transportData;
        }

        function calculateFillResources(originVillages, targetVillages) {
            const localStorageObject = getLocalStorage();
            const merchantBonus = parseBool(localStorageObject.sbMerchantBonus);
            const holdBackMerchants = Number(localStorageObject.sbHoldBackMerchants);
            const holdBackResourcesAbsolute = parseBool(localStorageObject.sbHoldBackResourcesAbsoluteRadio);
            const holdBackResourcesPercentage = parseBool(localStorageObject.sbHoldBackResourcesPercentageRadio);
            const holdBackWood = Number(localStorageObject.sbHoldBackWood);
            const holdBackClay = Number(localStorageObject.sbHoldBackClay);
            const holdBackIron = Number(localStorageObject.sbHoldBackIron);
            const maxWood = Number(localStorageObject.sbMaxWood);
            const maxClay = Number(localStorageObject.sbMaxClay);
            const maxIron = Number(localStorageObject.sbMaxIron);
            const holdBackPercentage = Number(localStorageObject.sbHoldBackPercentage);
            const sendGapToMax = Number(localStorageObject.sbSendGapToMax) / 100;
            const arrivalTimes = getLocalStorage().sbArrivalTimes;
            const merchantCapacity = merchantBonus ? 1500 : 1000;

            const playerDataCopy = JSON.parse(JSON.stringify(playerData));
            playerDataCopy.forEach(village => {
                village.availableMerchants = Math.max(0, village.availableMerchants - holdBackMerchants);
                if (holdBackResourcesAbsolute) {
                    village.wood = Math.max(0, village.wood - holdBackWood);
                    village.clay = Math.max(0, village.clay - holdBackClay);
                    village.iron = Math.max(0, village.iron - holdBackIron);
                } else if (holdBackResourcesPercentage) {
                    const holdBackAmount = village.warehouseCapacity * (holdBackPercentage / 100);
                    village.wood = Math.max(0, village.wood - holdBackAmount);
                    village.clay = Math.max(0, village.clay - holdBackAmount);
                    village.iron = Math.max(0, village.iron - holdBackAmount);
                }
            });

            const filteredPlayerData = playerDataCopy.filter(village => originVillages.includes(village.coord));
            const validOriginVillages = filteredPlayerData.filter(village => village.availableMerchants > 0 && (village.wood > 0 || village.clay > 0 || village.iron > 0));
            const uniqueTargetVillages = [...new Set(targetVillages)];

            const missingWarehouseTargets = uniqueTargetVillages.filter(targetVillage => !warehouseData[targetVillage]);
            if (missingWarehouseTargets.length > 0) {
                UI.ErrorMessage(MintSDK.tt('Missing warehouse data for one or more selected target villages. Please paste warehouse data again.'));
                return [];
            }

            const targetVillageResources = {};
            const INFINITE_RESOURCES = Number.MAX_SAFE_INTEGER;
            uniqueTargetVillages.forEach(targetVillage => {
                const warehouse = warehouseData[targetVillage];
                const maxStorage = warehouse.maxStorage * (1 - sendGapToMax);
                targetVillageResources[targetVillage] = {
                    wood: Math.min(maxWood === 0 ? INFINITE_RESOURCES : maxWood, Math.max(0, maxStorage - warehouse.wood)),
                    clay: Math.min(maxClay === 0 ? INFINITE_RESOURCES : maxClay, Math.max(0, maxStorage - warehouse.clay)),
                    iron: Math.min(maxIron === 0 ? INFINITE_RESOURCES : maxIron, Math.max(0, maxStorage - warehouse.iron))
                };
            });

            const totalDemandWood = Object.values(targetVillageResources).reduce((sum, village) => sum + Number(village.wood), 0);
            const totalDemandClay = Object.values(targetVillageResources).reduce((sum, village) => sum + Number(village.clay), 0);
            const totalDemandIron = Object.values(targetVillageResources).reduce((sum, village) => sum + Number(village.iron), 0);
            const totalDemandAll = totalDemandWood + totalDemandClay + totalDemandIron;

            const totalAvailableWood = validOriginVillages.reduce((sum, village) => sum + Number(village.wood), 0);
            const totalAvailableClay = validOriginVillages.reduce((sum, village) => sum + Number(village.clay), 0);
            const totalAvailableIron = validOriginVillages.reduce((sum, village) => sum + Number(village.iron), 0);
            const totalMerchantCapacity = validOriginVillages.reduce((sum, village) => sum + (Number(village.availableMerchants) * merchantCapacity), 0);

            if (totalAvailableWood < totalDemandWood || totalAvailableClay < totalDemandClay || totalAvailableIron < totalDemandIron) {
                UI.ErrorMessage(MintSDK.tt('Not enough total origin resources to fulfill fill target demand.'));
                return [];
            }
            if (totalMerchantCapacity < totalDemandAll) {
                UI.ErrorMessage(MintSDK.tt('Not enough total merchant capacity to fulfill fill target demand.'));
                return [];
            }

            const currentTime = Date.now();
            const originTargetPairs = {};
            uniqueTargetVillages.forEach(targetVillage => {
                const possibleOrigins = validOriginVillages.map(originVillage => {
                    if (originVillage.coord === targetVillage) return null;
                    const travelTime = calculateTravelTime(originVillage.coord, targetVillage, merchantBonus);
                    const arrivalTime = currentTime + travelTime;
                    const withinArrivalTime = arrivalTimes.length === 0 || arrivalTimes.some(([start, end]) => arrivalTime >= start && arrivalTime <= end);
                    return withinArrivalTime ? { coord: originVillage.coord, travelTime } : null;
                }).filter(Boolean);
                if (possibleOrigins.length > 0) originTargetPairs[targetVillage] = possibleOrigins.sort((a, b) => a.travelTime - b.travelTime);
            });

            const transportData = [];
            Object.keys(targetVillageResources).forEach(targetCoord => {
                const targetVillage = targetVillageResources[targetCoord];
                while (targetVillage.wood > 0 || targetVillage.clay > 0 || targetVillage.iron > 0) {
                    if (!originTargetPairs[targetCoord] || originTargetPairs[targetCoord].length === 0) break;
                    const originCoord = originTargetPairs[targetCoord][0].coord;
                    const originVillage = validOriginVillages.find(village => village.coord === originCoord);
                    if (!originVillage) { originTargetPairs[targetCoord] = originTargetPairs[targetCoord].filter(origin => origin.coord !== originCoord); continue; }
                    const travelTime = originTargetPairs[targetCoord][0].travelTime;

                    if (originVillage.availableMerchants <= 0 || (originVillage.wood <= 0 && originVillage.clay <= 0 && originVillage.iron <= 0)) {
                        originTargetPairs[targetCoord] = originTargetPairs[targetCoord].filter(origin => origin.coord !== originCoord);
                        continue;
                    }

                    let remainingCapacity = originVillage.availableMerchants * merchantCapacity;
                    let woodToSend = 0, clayToSend = 0, ironToSend = 0;
                    while (remainingCapacity > 0) {
                        const chunkSize = Math.min(remainingCapacity, merchantCapacity);
                        const availableResources = [
                            targetVillage.wood - woodToSend > 0 && originVillage.wood - woodToSend > 0,
                            targetVillage.clay - clayToSend > 0 && originVillage.clay - clayToSend > 0,
                            targetVillage.iron - ironToSend > 0 && originVillage.iron - ironToSend > 0
                        ];
                        const activeCount = availableResources.filter(Boolean).length;
                        if (activeCount === 0) break;
                        const perResource = Math.floor(chunkSize / activeCount);
                        let spent = 0;
                        if (availableResources[0]) { const add = Math.min(perResource, targetVillage.wood - woodToSend, originVillage.wood - woodToSend); woodToSend += add; spent += add; }
                        if (availableResources[1]) { const add = Math.min(perResource, targetVillage.clay - clayToSend, originVillage.clay - clayToSend); clayToSend += add; spent += add; }
                        if (availableResources[2]) { const add = Math.min(perResource, targetVillage.iron - ironToSend, originVillage.iron - ironToSend); ironToSend += add; spent += add; }
                        if (spent === 0) break;
                        remainingCapacity -= spent;
                    }

                    const resourcesToSend = { wood: woodToSend, clay: clayToSend, iron: ironToSend };
                    const totalToSend = resourcesToSend.wood + resourcesToSend.clay + resourcesToSend.iron;
                    if (totalToSend <= 0) { originTargetPairs[targetCoord] = originTargetPairs[targetCoord].filter(origin => origin.coord !== originCoord); continue; }

                    if (resourcesToSend.wood > 0 || resourcesToSend.clay > 0 || resourcesToSend.iron > 0) {
                        transportData.push({ origin: originVillage.coord, target: targetCoord, wood: resourcesToSend.wood, clay: resourcesToSend.clay, iron: resourcesToSend.iron, url: originVillage.url, name: originVillage.name, id: originVillage.id, travelTime: travelTime });

                        targetVillage.wood -= resourcesToSend.wood;
                        targetVillage.clay -= resourcesToSend.clay;
                        targetVillage.iron -= resourcesToSend.iron;

                        originVillage.wood -= resourcesToSend.wood;
                        originVillage.clay -= resourcesToSend.clay;
                        originVillage.iron -= resourcesToSend.iron;
                        originVillage.availableMerchants -= Math.ceil(totalToSend / merchantCapacity);
                    }
                    if (originVillage.availableMerchants <= 0 || (originVillage.wood <= 0 && originVillage.clay <= 0 && originVillage.iron <= 0)) {
                        originTargetPairs[targetCoord] = originTargetPairs[targetCoord].filter(origin => origin.coord !== originCoord);
                    }
                }
            });

            const unfulfilledTargets = Object.keys(targetVillageResources).filter(tc => {
                const t = targetVillageResources[tc];
                return t.wood > 0 || t.clay > 0 || t.iron > 0;
            });
            if (unfulfilledTargets.length > 0) {
                const shortOn = [];
                if (unfulfilledTargets.some(tc => targetVillageResources[tc].wood > 0)) shortOn.push(MintSDK.tt('Wood'));
                if (unfulfilledTargets.some(tc => targetVillageResources[tc].clay > 0)) shortOn.push(MintSDK.tt('Clay'));
                if (unfulfilledTargets.some(tc => targetVillageResources[tc].iron > 0)) shortOn.push(MintSDK.tt('Iron'));
                UI.ErrorMessage(MintSDK.tt('Unable to fully fill all target villages.') + ' ' + MintSDK.tt('Short on: ') + shortOn.join(', ') + '. ' + MintSDK.tt('Try adjusting hold-back, gap to max, arrival times, or origin villages.'));
                return [];
            }
            return transportData;
        }

        function calculateTravelTime(origin, target, merchantBonus) {
            const distance = MintSDK.calculateDistance(origin, target);
            const { speed } = worldConfig.config;
            const merchantTravelSpeed = merchantBonus ? merchantSpeedWithBonus : merchantSpeed;
            return Math.round((distance * merchantTravelSpeed * 60 * 1000) / speed);
        }

        function verifyInputs(localStorageObject) {
            const originVillagesCustomSelection = parseBool(localStorageObject.sbOriginCustomSelection);
            const targetVillagesCustomSelection = parseBool(localStorageObject.sbTargetCustomSelection);
            const customOriginVillages = localStorageObject.sbCustomOriginVillages;
            const customTargetVillages = localStorageObject.sbCustomTargetVillages;

            if (originVillagesCustomSelection && !customOriginVillages) { UI.ErrorMessage(`${MintSDK.tt('Please enter origin villages.')}`); return false; }
            if (targetVillagesCustomSelection && !customTargetVillages) { UI.ErrorMessage(`${MintSDK.tt('Please enter target villages.')}`); return false; }
            return true;
        }

        function renderGroupsFilter(idPrefix) {
            let id = idPrefix + 'GroupsFilter';
            const groupId = getLocalStorage().id;
            let groupsFilter = `<select name="group-filter" id="${id}">`;
            for (const [_, group] of Object.entries(groups.result)) {
                const { group_id, name } = group;
                const isSelected = parseInt(group_id) === parseInt(groupId) ? 'selected' : '';
                if (name !== undefined) groupsFilter += `<option value="${group_id}" ${isSelected}>${name}</option>`;
            }
            groupsFilter += `</select>`;
            return groupsFilter;
        }

        async function fetchVillageGroups() {
            let fetchGroups = '';
            if (game_data.player.sitter > 0) fetchGroups = game_data.link_base_pure + `groups&mode=overview&ajax=load_group_menu&t=${game_data.player.id}`;
            else fetchGroups = game_data.link_base_pure + 'groups&mode=overview&ajax=load_group_menu';
            return await jQuery.get(fetchGroups).then((response) => response).catch((error) => {
                UI.ErrorMessage('Error fetching village groups!');
                console.error(`${scriptInfo} Error:`, error);
            });
        }

        function renderArrivalTimeSelector() {
            return `
                <fieldset class="sb-fieldset" id="arrivalTimeFieldset">
                    <legend>${MintSDK.tt("Arrival time")}</legend>
                    <div class="sb-grid sb-grid-4 ra-mb10">
                        <div><input type="datetime-local" id="startDateTime" required></div>
                        <div><input type="datetime-local" id="endDateTime" required></div>
                        <div><button type="button" class="add-entry-btn" id="sbAddTimeEntry">+</button></div>
                        <div><button type="button" class="add-entry-btn sbDeleteAllEntries" id="sbDeleteAllEntries">${MintSDK.tt("Delete all arrival times")}</button></div>
                    </div>
                </fieldset>`;
        }

        function testRegex(line) {
            if (typeof line !== 'string') return false;
            MintSDK.coordsRegex.lastIndex = 0;
            return MintSDK.coordsRegex.test(line);
        }

        function parseBool(input) {
            if (typeof input === 'string') return input.toLowerCase() === 'true';
            else if (typeof input === 'boolean') return input;
            else { console.error(`${scriptInfo}: Invalid input: needs to be a string or boolean.`); return false; }
        }

        function villageArrayToDict(villageArray) {
            let dict = {};
            for (let i = 0; i < villageArray.length; i++) {
                let key = villageArray[i][2] + '|' + villageArray[i][3];
                dict[key] = villageArray[i][0];
            }
            return dict;
        }

        function initializeSavedEntries() {
            const localStorageObject = getLocalStorage();
            const sbArrivalTimes = localStorageObject.sbArrivalTimes;

            const entriesTable = document.createElement('table');
            entriesTable.classList.add('entries-table');
            entriesTable.id = 'sbArrivalEntryTable';

            const headerRow = document.createElement('tr');
            headerRow.innerHTML = `<th class="ra-tac">${MintSDK.tt('From')}</th><th class="ra-tac">${MintSDK.tt('To')}</th><th class="ra-tac">${MintSDK.tt('Delete Entry')}</th>`;
            entriesTable.appendChild(headerRow);

            if (sbArrivalTimes && sbArrivalTimes.length > 0) {
                entriesTable.style.display = 'table';
                sbArrivalTimes.forEach((timeSpan) => {
                    const newEntryRow = document.createElement('tr');
                    newEntryRow.classList.add('entry-row');
                    const startTime = formatLocalizedTime(new Date(timeSpan[0]));
                    const endTime = formatLocalizedTime(new Date(timeSpan[1]));
                    newEntryRow.innerHTML = `<td class="entry-start">${startTime}</td><td class="entry-end">${endTime}</td><td class="ra-tac"><button class="delete-entry-btn" id="btn-${timeSpan[0]}-${timeSpan[1]}">X</button></td>`;
                    entriesTable.appendChild(newEntryRow);
                });
            } else {
                entriesTable.style.display = 'none';
            }
            document.getElementById('arrivalTimeFieldset').appendChild(entriesTable);
        }

        function isEqual(arr1, arr2) { return arr1[0] === arr2[0] && arr1[1] === arr2[1]; }

        function formatLocalizedTime(date) {
            return date.toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', year: 'numeric', hour: 'numeric', minute: 'numeric', hour12: false });
        }

        function initializeInputFields() {
            const settingsObject = getLocalStorage();
            for (let id in settingsObject) {
                if (settingsObject.hasOwnProperty(id)) {
                    const element = document.getElementById(id);
                    if (element) {
                        if (element.type === 'checkbox' || element.type === 'radio') element.checked = settingsObject[id];
                        else element.value = settingsObject[id];

                        switch (id) {
                            case 'sbOriginGroupSelection': if (settingsObject[id]) { $('#sbOriginGroupMenuContainer').show(); $('#sbOriginCustomTextAreaContainer').hide(); } break;
                            case 'sbOriginCustomSelection': if (settingsObject[id]) { $('#sbOriginGroupMenuContainer').hide(); $('#sbOriginCustomTextAreaContainer').show(); } break;
                            case 'sbTargetGroupSelection': if (settingsObject[id]) { $('#sbTargetGroupMenuContainer').show(); $('#sbTargetCustomTextAreaContainer').hide(); } break;
                            case 'sbTargetCustomSelection': if (settingsObject[id]) { $('#sbTargetGroupMenuContainer').hide(); $('#sbTargetCustomTextAreaContainer').show(); } break;
                            case 'sbHoldBackResourcesAbsoluteRadio': if (settingsObject[id]) { $('#sbHoldBackResourcesAbsolute').show(); $('#sbHoldBackResourcesPercentage').hide(); } break;
                            case 'sbHoldBackResourcesPercentageRadio': if (settingsObject[id]) { $('#sbHoldBackResourcesAbsolute').hide(); $('#sbHoldBackResourcesPercentage').show(); } break;
                        }
                    }
                }
            }
        }

        async function getVillagesByGroupId(groupId) {
            const url = game_data.link_base_pure + `overview_villages&mode=combined&group=${groupId}&page=-1`;
            try {
                const response = await fetch(url);
                const pageContent = await response.text();
                const villageCoordinates = [];
                const villageElements = $(pageContent).find("#combined_table tr.nowrap .quickedit-vn");
                villageElements.each(function () {
                    const villageId = parseInt($(this).attr('data-id'));
                    const coord = villageIdToCoordMap.get(villageId);
                    if (coord) villageCoordinates.push(coord);
                });
                return villageCoordinates;
            } catch (error) {
                UI.ErrorMessage(MintSDK.tt('There was an error while fetching the data!'));
                console.error("Error fetching village data:", error);
                return [];
            }
        }

        async function getPlayerData() {
            const baseUrl = game_data.player.sitter > 0
                ? `game.php?t=${game_data.player.id}&screen=overview_villages&mode=prod&group=0&page=`
                : "game.php?&screen=overview_villages&mode=prod&group=0&page=";
            const totalVillages = game_data.player.villages;
            const villagesData = [];

            async function fetchPageData(page) {
                const url = `${baseUrl}${page}`;
                const response = await fetch(url);
                const pageContent = await response.text();

                const woodTotals = [], clayTotals = [], ironTotals = [], warehouseCapacities = [], availableMerchants = [], totalMerchants = [];
                const isMobile = $("#mobileHeader")[0];

                const woodElements = isMobile ? $(pageContent).find(".res.mwood,.warn_90.mwood,.warn.mwood") : $(pageContent).find(".res.wood,.warn_90.wood,.warn.wood");
                const clayElements = isMobile ? $(pageContent).find(".res.mstone,.warn_90.mstone,.warn.mstone") : $(pageContent).find(".res.stone,.warn_90.stone,.warn.stone");
                const ironElements = isMobile ? $(pageContent).find(".res.miron,.warn_90.miron,.warn.miron") : $(pageContent).find(".res.iron,.warn_90.iron,.warn.iron");
                const villageElements = $(pageContent).find(".quickedit-vn");
                const warehouseElements = isMobile ? $(pageContent).find(".mheader.ressources") : $(pageContent).find(".res.iron,.warn_90.iron,.warn.iron").parent().next();
                const merchantElements = isMobile ? $(pageContent).find('a[href*="market"]') : $(pageContent).find(".res.iron,.warn_90.iron,.warn.iron").parent().next().next();

                woodElements.each(function () { woodTotals.push($(this).text().replace(/\./g, '').replace(',', '')); });
                clayElements.each(function () { clayTotals.push($(this).text().replace(/\./g, '').replace(',', '')); });
                ironElements.each(function () { ironTotals.push($(this).text().replace(/\./g, '').replace(',', '')); });
                warehouseElements.each(function () { warehouseCapacities.push($(this).text()); });
                merchantElements.each(function () {
                    const merchants = $(this).text().match(/(\d*)\/(\d*)/);
                    availableMerchants.push(merchants ? merchants[1] : "0");
                    totalMerchants.push(merchants ? merchants[2] : "999");
                });

                villageElements.each(function (index) {
                    const villageId = parseInt($(this).attr('data-id'));
                    const coord = villageIdToCoordMap.get(villageId);
                    if (coord) {
                        villagesData.push({
                            id: villageId, url: $(this).find('a').attr('href'), coord: coord, name: $(this).text().trim(),
                            wood: woodTotals[index], clay: clayTotals[index], iron: ironTotals[index],
                            availableMerchants: availableMerchants[index], totalMerchants: totalMerchants[index],
                            warehouseCapacity: warehouseCapacities[index]
                        });
                    }
                });

                const pageSize = parseInt($(pageContent).find("input[name='page_size']").val(), 10);
                return { processedVillages: villageElements.length, pageSize };
            }

            try {
                if (totalVillages <= 1000) {
                    UI.SuccessMessage(MintSDK.tt('Fetching player data...'));
                    await fetchPageData(-1);
                } else {
                    UI.SuccessMessage(MintSDK.tt('Fetching player data for more than 1000 villages. This may take a while...'));
                    let page = 0, totalProcessedVillages = 0;
                    while (totalProcessedVillages < totalVillages) {
                        const { processedVillages, pageSize } = await fetchPageData(page);
                        totalProcessedVillages += processedVillages;
                        if (processedVillages < pageSize) break;
                        page++;
                        await new Promise(resolve => setTimeout(resolve, 200));
                    }
                }
                return villagesData;
            } catch (error) {
                console.error("Error fetching player data:", error);
                return [];
            }
        }

        function createVillageMap(villageArray) {
            let villageMap = new Map();
            for (let i = 0; i < villageArray.length; i++) {
                villageMap.set(parseInt(villageArray[i][0]), villageArray[i][2] + '|' + villageArray[i][3]);
            }
            return villageMap;
        }

        function handleInputChange() {
            const settingsObject = getLocalStorage();
            const inputId = $(this).attr('id');
            let inputValue;
            let matchesCoordinates;

            switch (inputId) {
                case 'sbOriginGroupSelection':
                    inputValue = $(this).is(':checked');
                    if (inputValue) { $('#sbOriginGroupMenuContainer').show(); $('#sbOriginCustomTextAreaContainer').hide(); }
                    settingsObject.sbOriginCustomSelection = false; break;
                case 'sbOriginCustomSelection':
                    inputValue = $(this).is(':checked');
                    if (inputValue) { $('#sbOriginGroupMenuContainer').hide(); $('#sbOriginCustomTextAreaContainer').show(); }
                    settingsObject.sbOriginGroupSelection = false; break;
                case 'sbOriginGroupsFilter': inputValue = $(this).val(); break;
                case 'sbTargetGroupSelection':
                    inputValue = $(this).is(':checked');
                    if (inputValue) { $('#sbTargetGroupMenuContainer').show(); $('#sbTargetCustomTextAreaContainer').hide(); }
                    settingsObject.sbTargetCustomSelection = false; break;
                case 'sbTargetCustomSelection':
                    inputValue = $(this).is(':checked');
                    if (inputValue) { $('#sbTargetGroupMenuContainer').hide(); $('#sbTargetCustomTextAreaContainer').show(); }
                    settingsObject.sbTargetGroupSelection = false; break;
                case 'sbTargetGroupsFilter': inputValue = $(this).val(); break;
                case 'sbSendResourcesAbsoluteRadio':
                    inputValue = $(this).is(':checked');
                    if (inputValue) { $('#sbSendResourcesAbsolute').show(); $('#sbSendResourcesRatio').hide(); $('#sbSendResourcesFill').hide(); }
                    settingsObject.sbSendResourcesRatioRadio = false; settingsObject.sbSendResourcesMintRatioRadio = false; settingsObject.sbSendResourcesFillRadio = false; break;
                case 'sbSendResourcesRatioRadio':
                    inputValue = $(this).is(':checked');
                    if (inputValue) { $('#sbSendResourcesAbsolute').hide(); $('#sbSendResourcesRatio').show(); $('#sbSendResourcesFill').hide(); }
                    settingsObject.sbSendResourcesAbsoluteRadio = false; settingsObject.sbSendResourcesMintRatioRadio = false; settingsObject.sbSendResourcesFillRadio = false; break;
                case 'sbSendResourcesMintRatioRadio':
                    inputValue = $(this).is(':checked');
                    if (inputValue) { $('#sbSendResourcesAbsolute').hide(); $('#sbSendResourcesRatio').hide(); $('#sbSendResourcesFill').hide(); }
                    settingsObject.sbSendResourcesAbsoluteRadio = false; settingsObject.sbSendResourcesRatioRadio = false; settingsObject.sbSendResourcesFillRadio = false; break;
                case 'sbSendResourcesFillRadio':
                    inputValue = $(this).is(':checked');
                    if (inputValue) { $('#sbSendResourcesAbsolute').hide(); $('#sbSendResourcesRatio').hide(); $('#sbSendResourcesFill').show(); }
                    settingsObject.sbSendResourcesAbsoluteRadio = false; settingsObject.sbSendResourcesRatioRadio = false; settingsObject.sbSendResourcesMintRatioRadio = false; break;
                case 'sbHoldBackResourcesAbsoluteRadio':
                    inputValue = $(this).is(':checked');
                    if (inputValue) { $('#sbHoldBackResourcesAbsolute').show(); $('#sbHoldBackResourcesPercentage').hide(); }
                    settingsObject.sbHoldBackResourcesPercentageRadio = false; break;
                case 'sbHoldBackResourcesPercentageRadio':
                    inputValue = $(this).is(':checked');
                    if (inputValue) { $('#sbHoldBackResourcesAbsolute').hide(); $('#sbHoldBackResourcesPercentage').show(); }
                    settingsObject.sbHoldBackResourcesAbsoluteRadio = false; break;
                case 'sbMerchantBonus': inputValue = $(this).is(':checked'); break;
                case 'sbCustomOriginVillages':
                    inputValue = $(this).val();
                    matchesCoordinates = inputValue.match(MintSDK.coordsRegex) || [];
                    inputValue = matchesCoordinates ? [...new Set(matchesCoordinates)].join(' ') : '';
                    $(this).val(inputValue); break;
                case 'sbCustomTargetVillages':
                    inputValue = $(this).val();
                    matchesCoordinates = inputValue.match(MintSDK.coordsRegex) || [];
                    inputValue = matchesCoordinates ? [...new Set(matchesCoordinates)].join(' ') : '';
                    $(this).val(inputValue); break;
                case 'sbHoldBackMerchants': case 'sbSendWood': case 'sbSendClay': case 'sbSendIron':
                case 'sbHoldBackWood': case 'sbHoldBackClay': case 'sbHoldBackIron': case 'sbMaxWood': case 'sbMaxClay': case 'sbMaxIron': {
                    let v = isNaN(parseInt($(this).val())) ? defaultSettings[inputId] : parseInt($(this).val());
                    if (v < 0) { $(this).val(defaultSettings[inputId]); v = defaultSettings[inputId]; } else { $(this).val(v); }
                    inputValue = v; break;
                }
                case 'sbSendWoodRatio': case 'sbSendClayRatio': case 'sbSendIronRatio':
                case 'sbHoldBackPercentage': case 'sbSendGapToMax': {
                    let v = isNaN(parseInt($(this).val())) ? defaultSettings[inputId] : parseInt($(this).val());
                    if (v < 0) { $(this).val(defaultSettings[inputId]); v = defaultSettings[inputId]; }
                    else if (v > 100) { $(this).val(100); v = 100; }
                    else { $(this).val(v); }
                    inputValue = v; break;
                }
                default:
                    console.error(`${scriptInfo}: Unknown id: ${inputId}`);
                    return;
            }

            settingsObject[inputId] = inputValue;
            saveLocalStorage(settingsObject);
        }

        function resetInputFields() {
            const localStorageSettings = getLocalStorage();
            warehouseData = {};
            for (let key in defaultSettings) {
                if (defaultSettings.hasOwnProperty(key)) {
                    localStorageSettings[key] = defaultSettings[key];
                    const element = document.getElementById(key);
                    if (element) {
                        if (element.type === 'checkbox' || element.type === 'radio') element.checked = defaultSettings[key];
                        else element.value = defaultSettings[key];
                    }
                }
            }
            $('#sbSendResourcesFillRadio').prop('disabled', true);
            $('#sbSendResourcesFill').hide();
            $('#sbPasteWarehouseDataSuccess').hide();
            $('#sbPasteWarehouseDataError').hide();
            saveLocalStorage(localStorageSettings);
            initializeInputFields();
        }

        function getLocalStorage() {
            const localStorageSettings = JSON.parse(localStorage.getItem('imResourceSender'));
            const expectedSettings = Object.keys(defaultSettings);
            let missingSettings = [];
            if (localStorageSettings) missingSettings = expectedSettings.filter(setting => !(setting in localStorageSettings));

            if (localStorageSettings && missingSettings.length === 0) return localStorageSettings;
            else { saveLocalStorage(defaultSettings); return defaultSettings; }
        }

        function saveLocalStorage(settingsObject) {
            localStorage.setItem('imResourceSender', JSON.stringify(settingsObject));
        }

        async function fetchWorldConfigData() {
            try {
                const villages = await MintSDK.worldDataAPI('village');
                const wc = await MintSDK.getWorldConfig();
                return { villages, worldConfig: wc };
            } catch (error) {
                UI.ErrorMessage(MintSDK.tt('There was an error while fetching the data!'));
                console.error(`${scriptInfo} Error:`, error);
                return { villages: [], worldConfig: { config: { speed: 1 } } };
            }
        }

        return { init: init };

    })();

    /* =========================================================================================
     * BOOT
     * ========================================================================================= */
    initializationTheme();
    buildShell();
    hitCountApi();

})();