// ==UserScript== // @name Tribal Wars Auto Builder with UI // @namespace http://tampermonkey.net/ // @version 1.3.1 // @description Automatically builds buildings with configurable settings // @author ricardofauch // @match https://*.die-staemme.de/game.php?village=*&screen=main* // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; // Configuration const CHECK_INTERVAL = 5 * 61 * 1000; // 5 minutes in milliseconds const WAIT_FOR_HIGHER_PRIORITY = 10 * 60; // Wait up to 10 minutes for higher priority building const DEBUG = true; const STORAGE_KEY = 'tribalWarsBuilderConfig'; // Extract available buildings from the table function getAvailableBuildings() { debugLog('Starting getAvailableBuildings function'); const buildings = []; const buildingRows = document.querySelectorAll('#buildings tbody tr[id^="main_buildrow_"]'); debugLog(`Found ${buildingRows.length} building rows`); buildingRows.forEach((row, index) => { debugLog(`Processing row ${index + 1}:`, { rowId: row.id, rowHtml: row.innerHTML.substring(0, 100) + '...' }); const buildingCell = row.querySelector('td:first-child'); if (!buildingCell) { debugLog(`No building cell found in row ${index + 1}`); return; } const buildingId = row.id.replace('main_buildrow_', ''); if (!buildingId) { debugLog(`Could not extract building ID from row ${index + 1}`); return; } // Get the name from the second anchor tag's text const links = buildingCell.querySelectorAll('a'); const nameLink = links[1]; // Second link contains the name if (!nameLink) { debugLog(`No name link found in building cell for row ${index + 1}`); return; } // Skip if the building is fully upgraded const inactiveCell = row.querySelector('td.inactive'); if (inactiveCell && inactiveCell.textContent.includes('vollständig ausgebaut')) { debugLog(`Skipping fully upgraded building: ${buildingId}`); return; } // Get current level const levelSpan = buildingCell.querySelector('span[style="font-size: 0.9em"]'); const currentLevel = levelSpan ? levelSpan.textContent.trim() : 'unknown'; const buildingName = nameLink.textContent.trim(); debugLog(`Found valid building:`, { id: buildingId, name: buildingName, level: currentLevel, element: nameLink.outerHTML }); buildings.push({ id: buildingId, name: buildingName, currentLevel: currentLevel }); }); debugLog('Completed getAvailableBuildings. Found buildings:', buildings); return buildings; } // Load saved configuration function loadConfig() { const defaultConfig = { enabledBuildings: [], useCostReduction: true, buildingPriority: [] }; try { const savedConfig = localStorage.getItem(STORAGE_KEY); return savedConfig ? JSON.parse(savedConfig) : defaultConfig; } catch (error) { debugLog('Error loading config:', error); return defaultConfig; } } // Save configuration function saveConfig(config) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); debugLog('Config saved:', config); } catch (error) { debugLog('Error saving config:', error); } } // Create UI function createUI() { debugLog('Starting UI creation'); const config = loadConfig(); const buildings = getAvailableBuildings(); debugLog('Initial config:', config); debugLog('Available buildings:', buildings); // Main container const uiContainer = document.createElement('div'); uiContainer.style.cssText = 'background: #f4e4bc; padding: 15px; margin: 10px 0; border: 1px solid #603000; font-size: 12px;'; // Title Section const titleSection = document.createElement('div'); titleSection.style.marginBottom = '20px'; const title = document.createElement('h3'); title.textContent = 'Auto Builder Settings'; title.style.cssText = 'margin: 0 0 5px 0; font-size: 14px; font-weight: bold;'; titleSection.appendChild(title); const subtitle = document.createElement('div'); subtitle.textContent = 'Configure building sequence and automation settings'; subtitle.style.cssText = 'color: #666; font-style: italic;'; titleSection.appendChild(subtitle); uiContainer.appendChild(titleSection); // Settings Section const settingsSection = document.createElement('div'); settingsSection.style.cssText = 'background: #fff3d9; padding: 10px; border: 1px solid #c1a264; margin-bottom: 15px;'; // Cost Reduction Setting const costReductionDiv = document.createElement('div'); costReductionDiv.style.marginBottom = '10px'; const costReductionCheckbox = document.createElement('input'); costReductionCheckbox.type = 'checkbox'; costReductionCheckbox.id = 'autoBuildCostReduction'; costReductionCheckbox.checked = config.useCostReduction !== false; const costReductionLabel = document.createElement('label'); costReductionLabel.htmlFor = 'autoBuildCostReduction'; costReductionLabel.textContent = ' Use -20% cost reduction when available'; costReductionLabel.style.cursor = 'pointer'; costReductionDiv.appendChild(costReductionCheckbox); costReductionDiv.appendChild(costReductionLabel); settingsSection.appendChild(costReductionDiv); // Long Build Reduction Settings const longBuildDiv = document.createElement('div'); longBuildDiv.style.marginBottom = '5px'; const longBuildCheckbox = document.createElement('input'); longBuildCheckbox.type = 'checkbox'; longBuildCheckbox.id = 'autoBuildLongReduction'; longBuildCheckbox.checked = config.useLongBuildReduction !== false; const longBuildLabel = document.createElement('label'); longBuildLabel.htmlFor = 'autoBuildLongReduction'; longBuildLabel.textContent = ' Auto-reduce builds longer than '; longBuildLabel.style.cursor = 'pointer'; const longBuildThreshold = document.createElement('input'); longBuildThreshold.type = 'number'; longBuildThreshold.min = '0.5'; longBuildThreshold.step = '0.5'; longBuildThreshold.value = config.longBuildThreshold || 2; longBuildThreshold.style.cssText = 'width: 60px; padding: 2px; margin: 0 5px; background-color: #fff; border: 1px solid #c1a264;'; const hoursLabel = document.createElement('span'); hoursLabel.textContent = ' hours'; longBuildDiv.appendChild(longBuildCheckbox); longBuildDiv.appendChild(longBuildLabel); longBuildDiv.appendChild(longBuildThreshold); longBuildDiv.appendChild(hoursLabel); settingsSection.appendChild(longBuildDiv); uiContainer.appendChild(settingsSection); // Building Sequence Section const sequenceSection = document.createElement('div'); // Sequence Header const sequenceHeader = document.createElement('div'); sequenceHeader.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;'; const sequenceTitle = document.createElement('div'); sequenceTitle.textContent = 'Building Sequence'; sequenceTitle.style.cssText = 'font-weight: bold;'; sequenceHeader.appendChild(sequenceTitle); const clearButton = document.createElement('button'); clearButton.textContent = 'Clear All'; clearButton.className = 'btn btn-default'; clearButton.onclick = () => { sequenceList.innerHTML = ''; const emptyText = document.createElement('div'); emptyText.textContent = 'No buildings in sequence'; emptyText.style.cssText = 'color: #666; font-style: italic; text-align: center;'; sequenceList.appendChild(emptyText); UI.SuccessMessage('Sequence cleared'); }; sequenceHeader.appendChild(clearButton); sequenceSection.appendChild(sequenceHeader); // Current Sequence List const sequenceList = document.createElement('div'); sequenceList.id = 'buildSequenceList'; sequenceList.style.cssText = 'border: 1px solid #c1a264; padding: 10px; margin-bottom: 10px; min-height: 50px; background: #fff3d9;'; // Initialize sequence list function initializeSequenceList() { if (!config.buildSequence || config.buildSequence.length === 0) { debugLog('No existing sequence found, showing empty state'); const emptyText = document.createElement('div'); emptyText.textContent = 'No buildings in sequence'; emptyText.style.cssText = 'color: #666; font-style: italic; text-align: center;'; sequenceList.appendChild(emptyText); } else { debugLog('Loading existing sequence:', config.buildSequence); config.buildSequence.forEach(item => { addSequenceItem(item.building, item.targetLevel); }); } } sequenceSection.appendChild(sequenceList); // Add New Building Controls const addControls = document.createElement('div'); addControls.style.cssText = 'display: flex; gap: 10px; align-items: center; background: #fff3d9; padding: 10px; border: 1px solid #c1a264;'; // Building Selector const buildingSelect = document.createElement('select'); buildingSelect.style.cssText = 'flex: 1; padding: 2px; background-color: #fff; border: 1px solid #c1a264;'; const defaultOption = document.createElement('option'); defaultOption.value = ''; defaultOption.textContent = '-- Select Building --'; defaultOption.disabled = true; defaultOption.selected = true; buildingSelect.appendChild(defaultOption); buildings.forEach(building => { const option = document.createElement('option'); option.value = building.id; option.textContent = `${building.name} (${building.currentLevel})`; buildingSelect.appendChild(option); }); // Target Level Input const untilLevelInput = document.createElement('input'); untilLevelInput.type = 'number'; untilLevelInput.min = '1'; untilLevelInput.style.cssText = 'width: 80px; padding: 2px; background-color: #fff; border: 1px solid #c1a264;'; untilLevelInput.placeholder = 'Target lvl'; // Add Button const addButton = document.createElement('button'); addButton.textContent = 'Add to Sequence'; addButton.className = 'btn'; addButton.onclick = () => { if (!buildingSelect.value) { UI.ErrorMessage('Please select a building'); return; } const buildingId = buildingSelect.value; const building = buildings.find(b => b.id === buildingId); const currentLevel = parseInt(building.currentLevel.replace(/[^\d]/g, '')) || 0; const targetLevel = parseInt(untilLevelInput.value); if (!targetLevel) { UI.ErrorMessage('Please enter a target level'); return; } if (targetLevel <= currentLevel) { UI.ErrorMessage('Target level must be higher than current level'); return; } const emptyText = sequenceList.querySelector('div[style*="text-align: center"]'); if (emptyText) { sequenceList.innerHTML = ''; } addSequenceItem(buildingId, targetLevel); untilLevelInput.value = ''; buildingSelect.value = ''; }; addControls.appendChild(buildingSelect); addControls.appendChild(untilLevelInput); addControls.appendChild(addButton); sequenceSection.appendChild(addControls); function addSequenceItem(buildingId, targetLevel) { debugLog('Adding sequence item:', { buildingId, targetLevel }); const building = buildings.find(b => b.id === buildingId); if (!building) { debugLog('Building not found:', buildingId); return; } const item = document.createElement('div'); item.className = 'sequence-item'; item.style.cssText = 'display: flex; gap: 10px; margin-bottom: 5px; align-items: center; background: #fff; padding: 5px; border: 1px solid #c1a264;'; const text = document.createElement('span'); text.style.flex = '1'; text.textContent = `${building.name} to level ${targetLevel}`; const buttonsDiv = document.createElement('div'); buttonsDiv.style.cssText = 'display: flex; gap: 5px;'; const moveUpBtn = document.createElement('button'); moveUpBtn.innerHTML = '▲'; moveUpBtn.className = 'btn'; moveUpBtn.style.padding = '0 5px'; moveUpBtn.onclick = () => { const prev = item.previousElementSibling; if (prev) sequenceList.insertBefore(item, prev); }; const moveDownBtn = document.createElement('button'); moveDownBtn.innerHTML = '▼'; moveDownBtn.className = 'btn'; moveDownBtn.style.padding = '0 5px'; moveDownBtn.onclick = () => { const next = item.nextElementSibling; if (next) sequenceList.insertBefore(next, item); }; const removeBtn = document.createElement('button'); removeBtn.innerHTML = '✕'; removeBtn.className = 'btn'; removeBtn.style.padding = '0 5px'; removeBtn.style.color = '#ff0000'; removeBtn.onclick = () => { item.remove(); if (sequenceList.children.length === 0) { const emptyText = document.createElement('div'); emptyText.textContent = 'No buildings in sequence'; emptyText.style.cssText = 'color: #666; font-style: italic; text-align: center;'; sequenceList.appendChild(emptyText); } }; buttonsDiv.appendChild(moveUpBtn); buttonsDiv.appendChild(moveDownBtn); buttonsDiv.appendChild(removeBtn); item.appendChild(text); item.appendChild(buttonsDiv); sequenceList.appendChild(item); debugLog('Added sequence item:', text.textContent); } uiContainer.appendChild(sequenceSection); // Save Button const saveButton = document.createElement('button'); saveButton.textContent = 'Save Settings'; saveButton.className = 'btn'; saveButton.style.marginTop = '10px'; saveButton.onclick = () => { debugLog('Save button clicked'); const sequence = []; const items = sequenceList.querySelectorAll('.sequence-item'); debugLog('Found sequence items:', items.length); items.forEach(item => { const text = item.querySelector('span').textContent; debugLog('Processing item:', text); const [buildingName, levelText] = text.split(' to level '); const building = buildings.find(b => b.name === buildingName); if (building) { const targetLevel = parseInt(levelText); sequence.push({ building: building.id, targetLevel: targetLevel }); debugLog('Added to sequence:', { buildingId: building.id, targetLevel }); } }); const newConfig = { useCostReduction: costReductionCheckbox.checked, useLongBuildReduction: longBuildCheckbox.checked, longBuildThreshold: parseFloat(longBuildThreshold.value) || 2, buildSequence: sequence }; debugLog('Saving config:', newConfig); saveConfig(newConfig); UI.SuccessMessage(`Settings saved! Sequence contains ${sequence.length} buildings.`); }; uiContainer.appendChild(saveButton); // Insert UI into page const buildingsTable = document.getElementById('buildings'); if (buildingsTable && buildingsTable.parentElement) { buildingsTable.parentElement.insertBefore(uiContainer, buildingsTable); } // Initialize the sequence list initializeSequenceList(); debugLog('UI creation completed'); } function debugLog(message, data = null) { if (!DEBUG) return; const timestamp = new Date().toLocaleTimeString(); if (data) { console.log(`[${timestamp}] ${message}`, data); } else { console.log(`[${timestamp}] ${message}`); } } function getBuildingLevel(buildingName) { debugLog(`Getting level for ${buildingName}`); try { // Find the row containing the building const row = document.querySelector(`#main_buildrow_${buildingName}`); if (!row) { debugLog(`No row found for ${buildingName}`); return null; } // Find the build button const buildButton = row.querySelector(`a.btn-build[id*="_${buildingName}_"]`); if (!buildButton) { debugLog(`No build button found for ${buildingName}`); return null; } // Get the next level from data attribute and subtract 1 const nextLevel = parseInt(buildButton.getAttribute('data-level-next')); if (isNaN(nextLevel)) { debugLog(`Could not parse next level for ${buildingName}`); return null; } const currentLevel = nextLevel - 1; debugLog(`${buildingName} current level:`, currentLevel); return currentLevel; } catch (error) { debugLog(`Error getting level for ${buildingName}:`, error); return null; } } function getRemainingBuildTime(buildingName) { try { const row = document.querySelector(`#main_buildrow_${buildingName}`); if (!row) { debugLog(`No row found for ${buildingName} when checking time`); return Infinity; } // Check if there's a running timer const timerCell = row.querySelector('td:nth-child(5)'); if (!timerCell) { debugLog(`No timer cell found for ${buildingName}`); return Infinity; } const timeText = timerCell.textContent.trim(); if (!timeText) return 0; const [hours, minutes, seconds] = timeText.split(':').map(Number); const totalSeconds = hours * 3600 + minutes * 60 + seconds; debugLog(`${buildingName} remaining build time: ${timeText} (${totalSeconds} seconds)`); return totalSeconds; } catch (error) { debugLog(`Error getting remaining time for ${buildingName}:`, error); return Infinity; } } function canBuildResource(buildingName) { try { const row = document.querySelector(`#main_buildrow_${buildingName}`); if (!row) { debugLog(`No row found for ${buildingName} when checking buildability`); return false; } // Find the -20% button specifically const buildButton = row.querySelector(`#main_buildlink_${buildingName}_cheap`); if (!buildButton) { debugLog(`No cheap build button found for ${buildingName}`); return false; } // Check if button has the disabled class const isDisabled = buildButton.classList.contains('btn-bcr-disabled'); // Check if there's a valid build link const hasValidHref = buildButton.getAttribute('href') && buildButton.getAttribute('href').includes('cheap') && buildButton.getAttribute('href') !== '#'; // Check if the button is inside a cell with class 'build_options' const inBuildOptions = buildButton.closest('.build_options') !== null; // A button is buildable if it has a valid href, is in the build options cell, and is not disabled const canBuild = hasValidHref && inBuildOptions && !isDisabled; debugLog(`Checking if ${buildingName} can be built with -20%:`, { buttonFound: true, hasValidHref: hasValidHref, inBuildOptions: inBuildOptions, isDisabled: isDisabled, canBuild: canBuild, href: buildButton.getAttribute('href'), buttonText: buildButton.textContent.trim(), buttonClasses: buildButton.className, parentCell: buildButton.closest('td')?.className || 'no parent cell' }); return canBuild; } catch (error) { debugLog(`Error checking if ${buildingName} can be built:`, error); return false; } } function willBeAvailableSoon(buildingName) { const remainingTime = getRemainingBuildTime(buildingName); const willBeSoon = remainingTime > 0 && remainingTime <= WAIT_FOR_HIGHER_PRIORITY; debugLog(`Checking if ${buildingName} will be available soon:`, { remainingTime, threshold: WAIT_FOR_HIGHER_PRIORITY, willBeSoon }); return willBeSoon; } function applyBuildTimeReduction() { try { // Find all build time reduction buttons const reductionButtons = document.querySelectorAll('a.order_feature.btn.btn-btr'); if (!reductionButtons || reductionButtons.length === 0) { debugLog('No build time reduction buttons found'); return false; } // Get the last button (most recently added building) const lastButton = reductionButtons[reductionButtons.length - 1]; // Click the button lastButton.click(); debugLog('Clicked build time reduction button'); return true; } catch (error) { debugLog('Error applying build time reduction:', error); return false; } } function isConstructionInProgress() { // Check for buildorder element const buildorder = document.querySelector('#buildorder_4'); const isBuilding = buildorder !== null; debugLog('Checking for ongoing construction:', { buildorderFound: isBuilding, elementId: isBuilding ? buildorder.id : 'not found' }); return isBuilding; } function reduceLongBuilds() { try { const config = loadConfig(); // Check if feature is enabled if (!config.useLongBuildReduction) { debugLog('Long build reduction is disabled'); return false; } const threshold = config.longBuildThreshold || 2; debugLog(`Checking for builds longer than ${threshold} hours`); // Get all build rows - modified selector to catch all building types const buildRows = document.querySelectorAll('tr.sortable_row[class*="buildorder_"]'); debugLog('Found build rows:', buildRows.length); for (const row of buildRows) { // Get the duration cell const durationCell = row.querySelector('td.nowrap.lit-item'); if (!durationCell) { debugLog('No duration cell found for row:', row.className); continue; } // Get the span containing the time const timeSpan = durationCell.querySelector('span'); if (!timeSpan) { debugLog('No time span found in duration cell'); continue; } // Get the duration text const durationText = timeSpan.textContent.trim(); if (!durationText) { debugLog('Empty duration text'); continue; } // Parse the time const [hours, minutes, seconds] = durationText.split(':').map(Number); const totalHours = hours + minutes/60 + seconds/3600; // Get building info const buildingCell = row.querySelector('td.lit-item'); const buildingName = buildingCell ? buildingCell.textContent.trim().split('\n')[0].trim() : 'Unknown'; debugLog('Checking build duration:', { building: buildingName, duration: durationText, totalHours: totalHours, threshold: threshold, rowClass: row.className }); // If duration is over threshold if (totalHours > threshold) { // Find the reduction button in this row const reductionButton = row.querySelector('a.order_feature.btn.btn-btr:not(.btn-instant)'); if (reductionButton) { debugLog('Found long build, clicking reduction button:', { building: buildingName, duration: durationText, buttonText: reductionButton.textContent.trim() }); reductionButton.click(); return true; } } } debugLog(`No builds over ${threshold} hours found needing reduction`); return false; } catch (error) { debugLog('Error in reduceLongBuilds:', error); return false; } } function buildResource(buildingName) { const config = loadConfig(); debugLog(`Attempting to build ${buildingName}${config.useCostReduction ? ' with -20% discount' : ''}`); try { const row = document.querySelector(`#main_buildrow_${buildingName}`); if (!row) { debugLog(`No row found for ${buildingName} when trying to build`); return false; } // Choose between normal and cheap build based on configuration const buttonSelector = config.useCostReduction ? `#main_buildlink_${buildingName}_cheap` : `a.btn-build[id*="_${buildingName}_"]`; const buildButton = row.querySelector(buttonSelector); if (!buildButton) { debugLog(`No build button found for ${buildingName}`); return false; } const buildUrl = buildButton.getAttribute('href'); if (!buildUrl || (config.useCostReduction && !buildUrl.includes('cheap'))) { debugLog(`No valid build href found for ${buildingName}`); return false; } debugLog(`Clicking build button for ${buildingName} with URL: ${buildUrl}`); // Instead of directly changing location, set up a sequence if (config.useCostReduction) { // For cost reduction, we need to: // 1. Navigate to build URL // 2. Wait for page load // 3. Apply time reduction // 4. Reload to main page window.location.href = buildUrl; // The reduction and reload will be handled by the page load event document.addEventListener('DOMContentLoaded', function() { setTimeout(() => { applyBuildTimeReduction(); setTimeout(() => { window.location.reload(); }, 500); }, 500); }, { once: true }); // Use once: true to ensure it only runs once } else { // For normal build, just build and reload window.location.href = buildUrl; // Add event listener for page load document.addEventListener('DOMContentLoaded', function() { setTimeout(() => { window.location.reload(); }, 500); }, { once: true }); } return true; } catch (error) { debugLog(`Error building ${buildingName}:`, error); return false; } } function checkAndBuild() { debugLog('Starting building check cycle...'); reduceLongBuilds(); try { if (isConstructionInProgress()) { debugLog('Construction already in progress, skipping build check'); return; } const config = loadConfig(); if (!config.buildSequence || config.buildSequence.length === 0) { debugLog('No building sequence configured'); return; } // Get first incomplete sequence item const currentSequenceItem = config.buildSequence[0]; const building = currentSequenceItem.building; const currentLevel = getBuildingLevel(building); debugLog('Checking sequence item:', { building, currentLevel, targetLevel: currentSequenceItem.targetLevel }); if (currentLevel >= currentSequenceItem.targetLevel) { // Remove completed item and save config.buildSequence.shift(); saveConfig(config); debugLog('Building reached target level, removing from sequence'); // Add reload after short delay setTimeout(() => { debugLog('Reloading page after sequence update'); window.location.reload(); }, 2000); return; } if (canBuildResource(building)) { debugLog('Building available for construction'); if (buildResource(building)) { debugLog('Building command sent successfully'); } } else { debugLog('Cannot build current sequence item yet'); } } catch (error) { debugLog('Error in checkAndBuild:', error); } } // Create UI and start the script createUI(); debugLog('Script initialized, performing initial check...'); checkAndBuild(); // Set up periodic page reload debugLog(`Setting up periodic page reload every ${CHECK_INTERVAL/1000} seconds`); setInterval(() => { debugLog('Triggering page reload for next check'); window.location.reload(); }, CHECK_INTERVAL); debugLog('Script setup completed successfully'); })();