// ==UserScript==
// @name Tribal Wars Battle Report Production Calculator
// @namespace http://tampermonkey.net/
// @version 1.4
// @description Calculates total resource production from battle report spy information
// @author ricardofauch
// @match https://*.die-staemme.de/game.php?village=*&screen=report&*
// @match https://*.die-staemme.de/game.php?village=*&screen=report*
// @match https://*.die-staemme.de/game.php?screen=report&*
// @match https://*.die-staemme.de/game.php?village=*&screen=place*
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Debug configuration
const DEBUG = true;
// Debug logger function
function log(message, data = null) {
if (!DEBUG) return;
const prefix = '[Production Calculator]';
if (data) {
console.log(prefix, message, data);
} else {
console.log(prefix, message);
}
}
let SettingsHelper = {
configConf: null,
loadSettings() {
log('Loading settings...');
var win = typeof unsafeWindow != 'undefined' ? unsafeWindow : window;
var path = "config_settings_" + win.game_data.world;
log('Settings path:', path);
if (win.localStorage.getItem(path) == null) {
log('Settings not found in localStorage, fetching from server...');
var oRequest = new XMLHttpRequest();
var sURL = 'https://' + window.location.hostname + '/interface.php?func=get_config';
log('Fetching config from URL:', sURL);
oRequest.open('GET', sURL, 0);
oRequest.send(null);
if (oRequest.status != 200) {
log('Error fetching config! Status:', oRequest.status);
throw "Error executing XMLHttpRequest call to get Config! " + oRequest.status;
}
const config = this.xmlToJson(oRequest.responseXML).config;
log('Received config from server:', config);
win.localStorage.setItem(path, JSON.stringify(config));
}
const settings = JSON.parse(win.localStorage.getItem(path));
log('Loaded settings:', settings);
return settings;
},
xmlToJson(xml) {
log('Converting XML to JSON...');
var obj = {};
if (xml.nodeType == 1) {
if (xml.attributes.length > 0) {
obj["@attributes"] = {};
for (var j = 0; j < xml.attributes.length; j++) {
var attribute = xml.attributes.item(j);
obj["@attributes"][attribute.nodeName] = isNaN(parseFloat(attribute.nodeValue)) ? attribute.nodeValue : parseFloat(attribute.nodeValue);
}
}
} else if (xml.nodeType == 3) {
obj = xml.nodeValue;
}
if (xml.hasChildNodes() && xml.childNodes.length === [].slice.call(xml.childNodes).filter(function(node) {
return node.nodeType === 3;
}).length) {
obj = [].slice.call(xml.childNodes).reduce(function(text, node) {
return text + node.nodeValue;
}, "");
} else if (xml.hasChildNodes()) {
for (var i = 0; i < xml.childNodes.length; i++) {
var item = xml.childNodes.item(i);
var nodeName = item.nodeName;
if (typeof obj[nodeName] == "undefined") {
obj[nodeName] = this.xmlToJson(item);
} else {
if (typeof obj[nodeName].push == "undefined") {
var old = obj[nodeName];
obj[nodeName] = [];
obj[nodeName].push(old);
}
obj[nodeName].push(this.xmlToJson(item));
}
}
}
return obj;
},
getConf() {
log('Getting configuration...');
if (!this.configConf) {
log('Config not cached, loading from localStorage...');
this.configConf = JSON.parse(window.localStorage.getItem('config_settings_' + game_data.world));
log('Loaded config:', this.configConf);
}
return this.configConf;
},
checkConfigs() {
log('Checking configurations...');
const configConf = this.getConf();
if (configConf == null) {
log('No config found, loading settings...');
SettingsHelper.loadSettings();
}
}
};
// Initialize based on current screen
if (window.location.href.includes('screen=place')) {
handleRallyPoint();
} else {
// Normal report page handling with 2 second delay
window.addEventListener('load', function() {
log('Page loaded, waiting 2 seconds before processing...');
setTimeout(() => {
log('Checking for attack results after delay...');
if (document.getElementById('attack_results')) {
log('Attack results found, calculating production...');
calculateProduction();
} else {
log('No attack results found on this page');
}
}, 200); // 2000ms = 2 seconds
});
}
function calculateTimeDifferenceHours() {
const now = new Date();
const fightTimeText = document.evaluate(
"//td[contains(text(), 'Kampfzeit')]/following-sibling::td",
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue.textContent.trim();
log('Fight time text:', fightTimeText);
// Parse the fight time
const [datePart, timePart] = fightTimeText.split(' ');
const [day, month, year] = datePart.split('.');
const [hours, minutes, seconds] = timePart.split(':');
const fightTime = new Date(2000 + parseInt(year), parseInt(month) - 1, parseInt(day),
parseInt(hours), parseInt(minutes), parseInt(seconds));
log('Parsed fight time:', fightTime);
log('Current time:', now);
// Calculate difference in hours
const diffHours = (now - fightTime) / (1000 * 60 * 60);
log('Time difference in hours:', diffHours);
return diffHours;
}
function extractSpiedResources() {
const resourcesRow = document.querySelector('#attack_spy_resources td');
if (!resourcesRow) {
log('No spied resources found');
return null;
}
const resources = {
wood: 0,
stone: 0,
iron: 0
};
const amounts = resourcesRow.textContent.match(/\d+(?:\.\d+)?/g);
if (amounts && amounts.length >= 3) {
resources.wood = parseInt(amounts[0].replace('.', ''));
resources.stone = parseInt(amounts[1].replace('.', ''));
resources.iron = parseInt(amounts[2].replace('.', ''));
}
log('Extracted spied resources:', resources);
return resources;
}
function extractMyVillageCoords() {
const coordCell = document.querySelector('td.box-item b.nowrap');
if (!coordCell) {
log('Could not find village coordinates cell');
return null;
}
const coordMatch = coordCell.textContent.match(/\((\d+)\|(\d+)\)/);
if (!coordMatch) {
log('Could not extract coordinates from:', coordCell.textContent);
return null;
}
return {
x: parseInt(coordMatch[1]),
y: parseInt(coordMatch[2])
};
}
function calculateDistance(source, target) {
return Math.sqrt(
Math.pow(source.x - target.x, 2) +
Math.pow(source.y - target.y, 2)
);
}
function calculateTravelTimeHours(distance) {
const MINUTES_PER_FIELD = 10;
return (distance * MINUTES_PER_FIELD) / 60; // Convert to hours
}
function calculateExpectedResources(hourlyProduction, spiedResources, hoursSinceSpy, travelTimeHours) {
const totalHours = hoursSinceSpy + travelTimeHours;
if (DEBUG) {
console.log('[Production Calculator] Hours since spy:', hoursSinceSpy);
console.log('[Production Calculator] Travel time hours:', travelTimeHours);
console.log('[Production Calculator] Total hours for calculation:', totalHours);
}
const expected = {
wood: Math.floor(spiedResources.wood + (hourlyProduction.wood * totalHours)),
stone: Math.floor(spiedResources.stone + (hourlyProduction.stone * totalHours)),
iron: Math.floor(spiedResources.iron + (hourlyProduction.iron * totalHours))
};
if (DEBUG) {
console.log('[Production Calculator] Expected resources:', expected);
}
return expected;
}
function calculateNeededLightCavalry(totalResources) {
const LIGHT_CAVALRY_CAPACITY = 80;
const TARGET_PERCENTAGE = 0.9; // 100%
const targetResources = Math.floor(totalResources * TARGET_PERCENTAGE);
if (DEBUG) {
console.log('[Production Calculator] Total resources:', totalResources);
console.log('[Production Calculator] Target resources (90%):', targetResources);
console.log('[Production Calculator] LC needed:', Math.ceil(targetResources / LIGHT_CAVALRY_CAPACITY));
}
return Math.ceil(targetResources / LIGHT_CAVALRY_CAPACITY);
}
function extractTargetCoordinates() {
// Find the target village link
const targetCell = document.evaluate(
"//td[contains(text(), 'Ziel:')]/following-sibling::td//a[contains(@href, 'screen=info_village')]",
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
if (!targetCell) {
log('Target cell not found');
return null;
}
// Get the village ID for the place screen
const villageIdMatch = targetCell.href.match(/id=(\d+)/);
const villageId = villageIdMatch ? villageIdMatch[1] : null;
// Extract coordinates
const coordMatch = targetCell.textContent.match(/\((\d+)\|(\d+)\)/);
if (!coordMatch) {
log('No coordinates found in target cell');
return null;
}
const coordinates = {
x: parseInt(coordMatch[1]),
y: parseInt(coordMatch[2]),
id: villageId
};
log('Extracted coordinates and village ID:', coordinates);
return coordinates;
}
function handleRallyPoint() {
const pendingAttack = localStorage.getItem('pendingAttack');
if (pendingAttack) {
const attack = JSON.parse(pendingAttack);
const scriptContent = `
(function() {
// Define settings globally
window.settings = [...${JSON.stringify(attack.troops)}, ${attack.coordinates.x}, ${attack.coordinates.y}];
$(document).ready(function() {
try {
// Register script
if (window.ScriptAPI) {
window.ScriptAPI.register('Scriptgenerator - Truppen im Versammlungsplatz einfügen', true, 'tomabrafix', 'support-nur-im-forum@die-staemme.de');
}
var scriptgenerator = {
replace_all: function(unit) {
var all = $('#unit_input_'+unit).next().text().match(/\\d+/);
return all;
},
main: function() {
var units = ['spear', 'sword', 'axe', 'archer', 'spy', 'light', 'marcher', 'heavy', 'ram', 'catapult', 'knight', 'snob'];
for(var i = 0; i <= units.length; i++) {
if($('#unit_input_'+units[i]).length == 0) {
continue;
}
var anzahl = this.replace_all(units[i]);
if(window.settings[i] < 0) {
var dif = Number(anzahl) + Number(window.settings[i]);
anzahl = dif < 0 ? 0 : dif;
} else if (window.settings[i] > 0) {
anzahl = window.settings[i];
}
if (window.settings[i] !== 0) {
$('#unit_input_'+units[i]).val(anzahl);
}
}
if(window.settings[12] != 'none') {
$('#inputx').val(window.settings[12]);
$('#inputy').val(window.settings[13]);
}
}
};
// Execute main and set up auto-confirmation
scriptgenerator.main();
// Schedule the first attack button click
setTimeout(() => {
const attackButton = document.getElementById('target_attack');
if (attackButton) {
attackButton.click();
console.log('Clicked first attack button');
// After clicking the first button, we need to wait for the new page and confirm button
// Store flag in localStorage to indicate we need to click confirm
localStorage.setItem('needsConfirm', 'true');
} else {
console.log('Attack button not found');
}
}, 331);
} catch(e) {
console.error('Script execution error:', e);
}
});
})();
`;
// Create and inject the script
const script = document.createElement('script');
script.textContent = scriptContent;
document.head.appendChild(script);
// Clear the pending attack
localStorage.removeItem('pendingAttack');
}
// Check if we need to click the confirm button (on the confirmation page)
const needsConfirm = localStorage.getItem('needsConfirm');
if (needsConfirm === 'true') {
const confirmScript = `
(function() {
$(document).ready(function() {
setTimeout(() => {
const confirmButton = document.getElementById('troop_confirm_submit');
if (confirmButton) {
confirmButton.click();
console.log('Clicked confirm button');
} else {
console.log('Confirm button not found');
}
localStorage.removeItem('needsConfirm');
}, 212);
});
})();
`;
const confirmScriptElement = document.createElement('script');
confirmScriptElement.textContent = confirmScript;
document.head.appendChild(confirmScriptElement);
}
}
// Function to create the attack button with auto-confirm flag
function createAttackButton(coordinates, lcAmount) {
const button = document.createElement('button');
button.className = 'btn';
button.style.marginTop = '5px';
button.textContent = `Send ${lcAmount} LC to (${coordinates.x}|${coordinates.y})`;
button.onclick = function() {
if (!confirm(`Are you sure you want to send ${lcAmount} LC to (${coordinates.x}|${coordinates.y})?`)) {
return;
}
const targetParam = coordinates.id ?
`target=${coordinates.id}` :
`x=${coordinates.x}&y=${coordinates.y}`;
const url = `/game.php?village=${game_data.village.id}&screen=place&${targetParam}`;
localStorage.setItem('pendingAttack', JSON.stringify({
troops: [0, 0, 0, 0, 1, lcAmount, 0, 0, 0, 0, 0, 0],
coordinates: {x: coordinates.x, y: coordinates.y}
}));
window.location.href = url;
};
return button;
}
function calculateProduction() {
log('Starting production calculation...');
// Initialize settings
SettingsHelper.checkConfigs();
const worldConfig = SettingsHelper.getConf();
const worldspeed = worldConfig.speed;
const base_production = worldConfig.game.base_production;
log('World configuration:', { worldspeed, base_production });
// Get building levels from spy data
const buildingDataElement = document.getElementById('attack_spy_building_data');
if (!buildingDataElement) {
log('Error: Building data element not found!');
return;
}
const buildingData = JSON.parse(buildingDataElement.value);
log('Building data:', buildingData);
// Find resource building levels
let woodLevel = 0;
let stoneLevel = 0;
let ironLevel = 0;
buildingData.forEach(building => {
switch(building.id) {
case 'wood':
woodLevel = parseInt(building.level);
break;
case 'stone':
stoneLevel = parseInt(building.level);
break;
case 'iron':
ironLevel = parseInt(building.level);
break;
}
});
log('Resource building levels:', { woodLevel, stoneLevel, ironLevel });
// Calculate base production for each resource
const woodProduction = Math.round(Math.pow(1.163118, woodLevel - 1) * worldspeed * base_production);
const stoneProduction = Math.round(Math.pow(1.163118, stoneLevel - 1) * worldspeed * base_production);
const ironProduction = Math.round(Math.pow(1.163118, ironLevel - 1) * worldspeed * base_production);
log('Base production calculations:', {
woodProduction,
stoneProduction,
ironProduction
});
// Check for resource bonus (bonus village)
const bonusIcons = document.querySelectorAll('.bonus_icon_1, .bonus_icon_2, .bonus_icon_3, .bonus_icon_8');
log('Found bonus icons:', bonusIcons.length);
let finalWoodProduction = woodProduction;
let finalStoneProduction = stoneProduction;
let finalIronProduction = ironProduction;
// Track applied bonuses for debugging
const appliedBonuses = [];
bonusIcons.forEach(icon => {
if (icon.classList.contains('bonus_icon_1')) {
finalWoodProduction *= 2;
appliedBonuses.push('2x wood bonus');
}
if (icon.classList.contains('bonus_icon_2')) {
finalStoneProduction *= 2;
appliedBonuses.push('2x stone bonus');
}
if (icon.classList.contains('bonus_icon_3')) {
finalIronProduction *= 2;
appliedBonuses.push('2x iron bonus');
}
if (icon.classList.contains('bonus_icon_8')) {
finalWoodProduction = Math.round(finalWoodProduction * 1.3);
finalStoneProduction = Math.round(finalStoneProduction * 1.3);
finalIronProduction = Math.round(finalIronProduction * 1.3);
appliedBonuses.push('30% all resources bonus');
}
});
log('Applied bonuses:', appliedBonuses);
log('Final production values:', {
finalWoodProduction,
finalStoneProduction,
finalIronProduction
});
// Create display element
const productionDiv = document.createElement('div');
productionDiv.style.marginBottom = '10px';
productionDiv.style.padding = '5px';
productionDiv.style.border = '1px solid #DED3B9';
productionDiv.innerHTML = `
<b>Theoretical Resource Production:</b><br>
<span class="icon header wood"></span> ${formatNumber(finalWoodProduction)} per hour | ${formatNumber(finalWoodProduction * 24)} per day<br>
<span class="icon header stone"></span> ${formatNumber(finalStoneProduction)} per hour | ${formatNumber(finalStoneProduction * 24)} per day<br>
<span class="icon header iron"></span> ${formatNumber(finalIronProduction)} per hour | ${formatNumber(finalIronProduction * 24)} per day<br>
Total: ${formatNumber(finalWoodProduction + finalStoneProduction + finalIronProduction)} per hour | ${formatNumber((finalWoodProduction + finalStoneProduction + finalIronProduction) * 24)} per day
`;
log('Created production display element');
// Insert before attack results table
const attackResults = document.getElementById('attack_results');
attackResults.parentNode.insertBefore(productionDiv, attackResults);
log('Inserted production display into page');
const hourlyProduction = {
wood: finalWoodProduction,
stone: finalStoneProduction,
iron: finalIronProduction
};
// When calculating expected resources:
const sourceCoords = extractMyVillageCoords();
const targetCoords = extractTargetCoordinates();
// Calculate expected resources
const spiedResources = extractSpiedResources();
const hoursSinceFight = calculateTimeDifferenceHours();
if (spiedResources && hoursSinceFight > 0) {
const sourceCoords = extractMyVillageCoords();
const targetCoords = extractTargetCoordinates();
if (sourceCoords && targetCoords) {
const distance = calculateDistance(sourceCoords, targetCoords);
const travelTime = calculateTravelTimeHours(distance);
if (DEBUG) {
console.log('[Production Calculator] Source village:', sourceCoords);
console.log('[Production Calculator] Target village:', targetCoords);
console.log('[Production Calculator] Distance:', distance.toFixed(2), 'fields');
console.log('[Production Calculator] Travel time:', (travelTime * 60).toFixed(2), 'minutes');
}
const expectedResources = calculateExpectedResources(
hourlyProduction,
spiedResources,
hoursSinceFight,
travelTime
);
const totalExpectedResources = expectedResources.wood + expectedResources.stone + expectedResources.iron;
const neededLC = calculateNeededLightCavalry(totalExpectedResources);
// Update the display
const estimationDiv = document.createElement('div');
estimationDiv.style.marginBottom = '10px';
estimationDiv.style.padding = '5px';
estimationDiv.style.border = '1px solid #DED3B9';
estimationDiv.innerHTML = `
<b>Resource Estimation:</b><br>
Time since spy: ${formatNumber(hoursSinceFight.toFixed(2))} hours<br>
Travel time: ${formatNumber((travelTime * 60).toFixed(1))} minutes<br>
Total time for calculation: ${formatNumber((hoursSinceFight + travelTime).toFixed(2))} hours<br>
Distance: ${formatNumber(distance.toFixed(1))} fields<br>
Expected resources on arrival:<br>
<span class="icon header wood"></span> ${formatNumber(expectedResources.wood)}<br>
<span class="icon header stone"></span> ${formatNumber(expectedResources.stone)}<br>
<span class="icon header iron"></span> ${formatNumber(expectedResources.iron)}<br>
Total: ${formatNumber(expectedResources.wood + expectedResources.stone + expectedResources.iron)}<br>
Required light cavalry to farm 90%: ${formatNumber(neededLC)}
(${formatNumber(neededLC * 80)} carry capacity)
`;
// Add attack button if coordinates were found
if (targetCoords) {
const attackButton = createAttackButton(targetCoords, neededLC);
estimationDiv.appendChild(attackButton);
}
// Insert after production div
productionDiv.parentNode.insertBefore(estimationDiv, productionDiv.nextSibling);
}
}
}
function formatNumber(number) {
return new Intl.NumberFormat().format(Math.round(number));
}
})();