Skip to content

Instantly share code, notes, and snippets.

@thebergamo
Created May 9, 2026 00:07
Show Gist options
  • Select an option

  • Save thebergamo/c606134e268650e00ee42531e1612385 to your computer and use it in GitHub Desktop.

Select an option

Save thebergamo/c606134e268650e00ee42531e1612385 to your computer and use it in GitHub Desktop.
TW Tribe Troops
/*
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, '&lt;') +
'</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