Created
May 9, 2026 00:07
-
-
Save thebergamo/c606134e268650e00ee42531e1612385 to your computer and use it in GitHub Desktop.
TW Tribe Troops
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| Tribal Wars - Tribe Troops Ratio Export | |
| Purpose: | |
| - Read tribe troops table (ally troops screen) | |
| - Aggregate troop population per player | |
| - Merge with tribe members points | |
| - Export BBCode table ready for forum post | |
| Usage: | |
| 1) Open tribe troops screen as tribe admin. | |
| 2) Run this script in browser console. | |
| 3) Copy BBCode output from the popup/console. | |
| */ | |
| (function () { | |
| 'use strict'; | |
| var DEBUG_PREFIX = '[Tribe Troops Ratio Export]'; | |
| var SORT_BY = 'ratio'; | |
| var SORT_ORDER = 'desc'; | |
| // TW population costs (classic worlds with optional archer/marcher/militia support) | |
| var UNIT_POP = { | |
| spear: 1, | |
| sword: 1, | |
| axe: 1, | |
| archer: 1, | |
| spy: 2, | |
| light: 4, | |
| marcher: 5, | |
| heavy: 6, | |
| ram: 5, | |
| catapult: 8, | |
| knight: 10, | |
| snob: 100, | |
| militia: 0 | |
| }; | |
| var POINTS_HEADERS = [ | |
| 'points', | |
| 'pontos', | |
| 'punkte', | |
| 'punti', | |
| 'punte' | |
| ]; | |
| function log() { | |
| var args = Array.prototype.slice.call(arguments); | |
| args.unshift(DEBUG_PREFIX); | |
| console.log.apply(console, args); | |
| } | |
| function getSearchParam(name) { | |
| try { | |
| return new URL(window.location.href).searchParams.get(name); | |
| } catch (err) { | |
| return null; | |
| } | |
| } | |
| function normalizeText(text) { | |
| return String(text || '') | |
| .replace(/\s+/g, ' ') | |
| .trim(); | |
| } | |
| function parseIntLoose(text) { | |
| var digits = String(text || '').replace(/[^0-9]/g, ''); | |
| return digits ? parseInt(digits, 10) : 0; | |
| } | |
| function formatInt(value) { | |
| return Number(value || 0).toLocaleString('pt-BR'); | |
| } | |
| function formatRatio(value) { | |
| if (!isFinite(value)) return '0.00'; | |
| return Number(value).toFixed(2); | |
| } | |
| function comparePlayers(a, b) { | |
| var dir = SORT_ORDER === 'asc' ? 1 : -1; | |
| if (SORT_BY === 'ratio') { | |
| if (a.ratio !== b.ratio) return (a.ratio - b.ratio) * dir; | |
| if (a.troopPop !== b.troopPop) return (a.troopPop - b.troopPop) * dir; | |
| if (a.points !== b.points) return (a.points - b.points) * dir; | |
| return a.player.localeCompare(b.player); | |
| } | |
| return a.player.localeCompare(b.player); | |
| } | |
| function isTroopsScreenLikely() { | |
| var screen = getSearchParam('screen'); | |
| var mode = getSearchParam('mode'); | |
| var byUrl = screen === 'ally' && mode && mode.indexOf('troop') !== -1; | |
| var byDom = document.querySelectorAll('img[src*="/graphic/unit/unit_"]').length > 0; | |
| return Boolean(byUrl || byDom); | |
| } | |
| function findTroopsTable() { | |
| var tables = Array.prototype.slice.call(document.querySelectorAll('table')); | |
| var best = null; | |
| var bestScore = -1; | |
| tables.forEach(function (table) { | |
| var unitIcons = table.querySelectorAll('img[src*="/graphic/unit/unit_"]').length; | |
| var playerLinks = table.querySelectorAll('a[href*="screen=info_player"]').length; | |
| var rows = table.querySelectorAll('tr').length; | |
| // Heuristic: prefer tables with unit icons + player links + many rows | |
| var score = unitIcons * 10 + playerLinks * 4 + rows; | |
| if (score > bestScore && unitIcons > 0 && playerLinks > 0) { | |
| best = table; | |
| bestScore = score; | |
| } | |
| }); | |
| return best; | |
| } | |
| function buildHeaderMap(table) { | |
| var headerRows = table.querySelectorAll('tr'); | |
| var bestHeaderRow = null; | |
| var bestHeaderScore = -1; | |
| Array.prototype.forEach.call(headerRows, function (row) { | |
| var cells = row.querySelectorAll('th,td'); | |
| var rowScore = 0; | |
| Array.prototype.forEach.call(cells, function (cell) { | |
| if (cell.querySelector('img[src*="/graphic/unit/unit_"]')) rowScore += 3; | |
| if (normalizeText(cell.textContent)) rowScore += 1; | |
| }); | |
| if (rowScore > bestHeaderScore) { | |
| bestHeaderScore = rowScore; | |
| bestHeaderRow = row; | |
| } | |
| }); | |
| if (!bestHeaderRow) return null; | |
| var map = { | |
| unitByCol: {}, | |
| playerCol: -1, | |
| pointsCol: -1 | |
| }; | |
| var cells = bestHeaderRow.querySelectorAll('th,td'); | |
| Array.prototype.forEach.call(cells, function (cell, idx) { | |
| var unitImg = cell.querySelector('img[src*="/graphic/unit/unit_"]'); | |
| if (unitImg) { | |
| var match = unitImg.src.match(/unit_([a-z_]+)\.png/i); | |
| if (match && match[1]) { | |
| map.unitByCol[idx] = match[1].toLowerCase(); | |
| } | |
| } | |
| var txt = normalizeText(cell.textContent).toLowerCase(); | |
| if (map.playerCol === -1 && (txt.indexOf('player') !== -1 || txt.indexOf('jogador') !== -1 || txt.indexOf('spieler') !== -1)) { | |
| map.playerCol = idx; | |
| } | |
| if (map.pointsCol === -1) { | |
| for (var i = 0; i < POINTS_HEADERS.length; i++) { | |
| if (txt.indexOf(POINTS_HEADERS[i]) !== -1) { | |
| map.pointsCol = idx; | |
| break; | |
| } | |
| } | |
| } | |
| }); | |
| return map; | |
| } | |
| function parseTroopsByPlayer(table) { | |
| var headerMap = buildHeaderMap(table); | |
| var result = {}; | |
| if (!headerMap) return result; | |
| var rows = table.querySelectorAll('tr'); | |
| Array.prototype.forEach.call(rows, function (row) { | |
| var playerLink = row.querySelector('a[href*="screen=info_player"]'); | |
| if (!playerLink) return; | |
| var playerName = normalizeText(playerLink.textContent); | |
| if (!playerName) return; | |
| if (!result[playerName]) { | |
| result[playerName] = { | |
| player: playerName, | |
| troopPop: 0, | |
| rowPoints: null, | |
| shared: true | |
| }; | |
| } | |
| var cells = row.querySelectorAll('td,th'); | |
| Array.prototype.forEach.call(cells, function (cell, idx) { | |
| var unit = headerMap.unitByCol[idx]; | |
| if (!unit || typeof UNIT_POP[unit] === 'undefined') return; | |
| var unitCount = parseIntLoose(cell.textContent); | |
| result[playerName].troopPop += unitCount * UNIT_POP[unit]; | |
| }); | |
| if (headerMap.pointsCol >= 0 && cells[headerMap.pointsCol]) { | |
| var parsedPoints = parseIntLoose(cells[headerMap.pointsCol].textContent); | |
| if (parsedPoints > 0) { | |
| result[playerName].rowPoints = parsedPoints; | |
| } | |
| } | |
| }); | |
| return result; | |
| } | |
| function guessTableWithPlayers(doc) { | |
| var candidateTables = Array.prototype.slice.call(doc.querySelectorAll('table.vis, table#ally_content, table')); | |
| var best = null; | |
| var bestScore = -1; | |
| candidateTables.forEach(function (table) { | |
| var links = table.querySelectorAll('a[href*="screen=info_player"]').length; | |
| var rows = table.querySelectorAll('tr').length; | |
| var text = table.textContent.toLowerCase(); | |
| var hasMemberHint = text.indexOf('member') !== -1 || text.indexOf('membro') !== -1 || text.indexOf('mitglied') !== -1; | |
| var score = links * 10 + rows + (hasMemberHint ? 15 : 0); | |
| if (score > bestScore && links > 0) { | |
| best = table; | |
| bestScore = score; | |
| } | |
| }); | |
| return best; | |
| } | |
| function extractPointsFromMembersDoc(doc) { | |
| var map = {}; | |
| var table = guessTableWithPlayers(doc); | |
| if (!table) return map; | |
| var rows = table.querySelectorAll('tr'); | |
| Array.prototype.forEach.call(rows, function (row) { | |
| var playerLink = row.querySelector('a[href*="screen=info_player"]'); | |
| if (!playerLink) return; | |
| var playerName = normalizeText(playerLink.textContent); | |
| if (!playerName) return; | |
| var cells = row.querySelectorAll('td,th'); | |
| var points = 0; | |
| // Prefer right-most large numeric cell as points fallback. | |
| Array.prototype.forEach.call(cells, function (cell) { | |
| var value = parseIntLoose(cell.textContent); | |
| if (value > points) points = value; | |
| }); | |
| if (points > 0) { | |
| map[playerName] = points; | |
| } | |
| }); | |
| return map; | |
| } | |
| function fetchMembersPoints() { | |
| var membersUrl = '/game.php?screen=ally&mode=members'; | |
| return fetch(membersUrl, { credentials: 'same-origin' }) | |
| .then(function (res) { | |
| if (!res.ok) throw new Error('Could not load members page: HTTP ' + res.status); | |
| return res.text(); | |
| }) | |
| .then(function (html) { | |
| var parser = new DOMParser(); | |
| var doc = parser.parseFromString(html, 'text/html'); | |
| return extractPointsFromMembersDoc(doc); | |
| }) | |
| .catch(function (err) { | |
| console.warn(DEBUG_PREFIX, 'Failed to fetch members points.', err); | |
| return {}; | |
| }); | |
| } | |
| function mergePlayers(troopsByPlayer, pointsByPlayer) { | |
| var merged = {}; | |
| Object.keys(pointsByPlayer).forEach(function (name) { | |
| merged[name] = { | |
| player: name, | |
| troopPop: 0, | |
| points: pointsByPlayer[name] || 0, | |
| shared: false | |
| }; | |
| }); | |
| Object.keys(troopsByPlayer).forEach(function (name) { | |
| if (!merged[name]) { | |
| merged[name] = { | |
| player: name, | |
| troopPop: 0, | |
| points: 0, | |
| shared: true | |
| }; | |
| } | |
| merged[name].troopPop = troopsByPlayer[name].troopPop; | |
| merged[name].shared = true; | |
| if (typeof troopsByPlayer[name].rowPoints === 'number' && troopsByPlayer[name].rowPoints > 0) { | |
| merged[name].points = troopsByPlayer[name].rowPoints; | |
| } | |
| }); | |
| return Object.keys(merged) | |
| .map(function (k) { | |
| var item = merged[k]; | |
| var ratio = item.points > 0 ? item.troopPop / item.points : 0; | |
| return { | |
| player: item.player, | |
| shared: item.shared, | |
| troopPop: item.troopPop, | |
| points: item.points, | |
| ratio: ratio | |
| }; | |
| }) | |
| .sort(comparePlayers); | |
| } | |
| function buildForumTable(rows) { | |
| var lines = []; | |
| var generatedAt = new Date(); | |
| lines.push('[table]'); | |
| lines.push('[**]Jogador[||]Compartilhado?[||]Tropas(pop)[||]Pontos[||]Ratio[/**]'); | |
| rows.forEach(function (row) { | |
| lines.push( | |
| '[*]' + row.player + | |
| '[|]' + (row.shared ? ':white_check_mark:' : ':x:') + | |
| '[|]' + formatInt(row.troopPop) + | |
| '[|]' + formatInt(row.points) + | |
| '[|]' + formatRatio(row.ratio) | |
| ); | |
| }); | |
| lines.push('[/table]'); | |
| lines.push(''); | |
| lines.push('[i]Atualizado em: ' + generatedAt.toLocaleString('pt-BR') + '[/i]'); | |
| return lines.join('\n'); | |
| } | |
| function showExport(text) { | |
| if (typeof Dialog !== 'undefined' && Dialog.show) { | |
| var html = '' + | |
| '<div style="max-width:900px;">' + | |
| '<h3 style="margin:0 0 8px 0;">Tribe Ratio Export (BBCode)</h3>' + | |
| '<textarea id="tribe_ratio_export" style="width:100%;height:320px;box-sizing:border-box;">' + | |
| text.replace(/</g, '<') + | |
| '</textarea>' + | |
| '<p style="margin:8px 0 0 0;font-size:12px;">Copie e cole no fórum.</p>' + | |
| '</div>'; | |
| Dialog.show('content', html); | |
| var box = document.getElementById('tribe_ratio_export'); | |
| if (box) { | |
| box.focus(); | |
| box.select(); | |
| } | |
| return; | |
| } | |
| window.prompt('Copie o BBCode abaixo:', text); | |
| } | |
| Promise.resolve() | |
| .then(function () { | |
| if (!isTroopsScreenLikely()) { | |
| throw new Error('Abra a tela de tropas da tribo antes de executar.'); | |
| } | |
| var troopsTable = findTroopsTable(); | |
| if (!troopsTable) { | |
| throw new Error('Nao foi possivel localizar a tabela de tropas da tribo.'); | |
| } | |
| var troopsByPlayer = parseTroopsByPlayer(troopsTable); | |
| if (!Object.keys(troopsByPlayer).length) { | |
| throw new Error('Nenhum jogador encontrado na tabela de tropas.'); | |
| } | |
| return fetchMembersPoints().then(function (pointsByPlayer) { | |
| var mergedRows = mergePlayers(troopsByPlayer, pointsByPlayer); | |
| var bbcode = buildForumTable(mergedRows); | |
| showExport(bbcode); | |
| log('Export generated for', mergedRows.length, 'members.'); | |
| console.log(bbcode); | |
| }); | |
| }) | |
| .catch(function (err) { | |
| console.error(DEBUG_PREFIX, err.message || err); | |
| if (typeof UI !== 'undefined' && UI.ErrorMessage) { | |
| UI.ErrorMessage(err.message || String(err), 6000); | |
| } else { | |
| alert(err.message || String(err)); | |
| } | |
| }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment