IntelliBalance (Ressourcenausgleich) + IntelliMint (Resource Sender) fest oben in der Marktseite eingebettet, ein-/ausklappbar, gemeinsames Farbschema.
Ce script ne doit pas être installé directement. C'est une librairie destinée à être incluse dans d'autres scripts avec la méta-directive // @require https://update.greasyfork.org/scripts/587600/1879982/%F0%9F%92%AA%20Anaboler%20Ress.js
// ==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 ? "▼" : "▲"}</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 · 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 ? "▼" : "▲");
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;">×</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();
})();