import { Player, Tribe, TribeSnapshot, TWHelpClient } from './lib/twhelp'; import { createTranslationFunc } from './utils'; import { DialogTable } from './common/dialog-table'; import { buildURL } from './lib/twstats'; const t = createTranslationFunc({ pl_PL: { 'Created at': 'Data/czas utworzenia', Dominance: 'Dominacja', 'Best rank': 'Najwyższy ranking', 'Most points': 'Najwięcej punktów', 'Most villages': 'Najwięcej wiosek', 'Show tribe changes': 'Pokaż zmiany plemion', 'Show history': 'Pokaż historię', 'Show ennoblements': 'Pokaż przejęcia', Changes: 'Zmiany', Members: 'Członkowie', Points: 'Punkty', Rank: 'Ranking', Villages: 'Wioski', Village: 'Wioska', OD: 'Pokonani ogólnie', 'OD - rank': 'Pokonani ogólnie - ranking', ODA: 'Pokonani agresor', 'ODA - rank': 'Pokonani agresor - ranking', ODD: 'Pokonani obrońca', 'ODD - rank': 'Pokonani obrońca - ranking', ODS: 'Pokonani wspierający', 'ODS - rank': 'Pokonani wspierający - ranking', Player: 'Gracz', 'New owner': 'Nowy właściciel', 'New tribe': 'Nowe plemię', 'Old owner': 'Stary własciciel', 'Old tribe': 'Stare plemię', 'Date/time': 'Data/czas', Action: 'Akcja', Joined: 'Dołączył', Left: 'Opuścił', Barbarian: 'Barbarzyńska', Unknown: 'Nieznany', 'Last activity at': 'Ostatnia aktywność', Actions: 'Akcje', }, }); class TWHelpConnector { constructor( private readonly client: TWHelpClient, private readonly version: string, private readonly server: string, private readonly id: number ) {} tribe() { return this.client.tribe(this.version, this.server, this.id); } tribeMembers() { return this.client.tribeMembers(this.version, this.server, this.id); } async latestSnapshot() { const history = await this.tribeHistory(1, 1); return history.data.length > 0 ? history.data[0] : null; } tribeHistory(page: number, limit: number) { return this.client.tribeHistory(this.version, this.server, this.id, { offset: (page - 1) * limit, limit, sort: ['date:desc'], }); } tribeTribeChanges(page: number, limit: number) { return this.client.tribeTribeChanges(this.version, this.server, this.id, { offset: (page - 1) * limit, limit, sort: ['createdAt:desc'], }); } tribeEnnoblements(page: number, limit: number) { return this.client.tribeEnnoblements(this.version, this.server, this.id, { offset: (page - 1) * limit, limit, sort: ['createdAt:desc'], }); } } enum DialogId { HISTORY = 'extended_tribe_profile_history', TRIBE_CHANGES = 'extended_tribe_profile_tribe_changes', ENNOBLEMENTS = 'extended_tribe_profile_ennoblements', } class UI { constructor( private readonly serverKey: string, private readonly tribe: Tribe, private readonly latestSnapshot: TribeSnapshot | null, private readonly members: Player[], private readonly twhelpConnector: TWHelpConnector ) {} public render() { this.renderAdditionalInfo(); this.renderStats(); this.renderActions(); this.extendMemberTable(); } private renderAdditionalInfo() { const tbody = document .querySelector('#content_value a[href*="twstats"]') ?.closest('tbody'); if (!(tbody instanceof HTMLTableSectionElement)) { return; } tbody.insertAdjacentHTML( 'beforeend', ` ${t('Created at')}: ${new Date(this.tribe.createdAt).toLocaleString()} ${t('Dominance')}: ${this.tribe.dominance.toFixed(2)}% ${t('Best rank')}: ${this.tribe.bestRank} (${new Date( this.tribe.bestRankAt ).toLocaleString()}) ${t('Most points')}: ${this.tribe.mostPoints.toLocaleString()} (${new Date( this.tribe.mostPointsAt ).toLocaleString()}) ${t('Most villages')}: ${this.tribe.mostVillages.toLocaleString()} (${new Date( this.tribe.mostVillagesAt ).toLocaleString()}) ` ); } private renderStats() { const td = document.querySelector( '#content_value td[valign="top"]:nth-child(2)' ); if (!(td instanceof HTMLTableCellElement)) { return; } const rows = [ { header: t('Points'), value: this.tribe.allPoints - (this.latestSnapshot?.allPoints ?? 0), }, { header: t('Dominance'), value: this.tribe.dominance - (this.latestSnapshot?.dominance ?? 0), customFormat: (v: number) => `${v.toFixed(4)}%`, }, { header: t('Members'), value: this.tribe.numMembers - (this.latestSnapshot?.numMembers ?? 0), }, { header: t('Rank'), value: this.tribe.rank - (this.latestSnapshot?.rank ?? 0), rank: true, }, { header: t('Villages'), value: this.tribe.numVillages - (this.latestSnapshot?.numVillages ?? 0), }, { header: t('ODA'), value: this.tribe.scoreAtt - (this.latestSnapshot?.scoreAtt ?? 0), }, { header: t('ODA - rank'), value: this.tribe.rankAtt - (this.latestSnapshot?.rankAtt ?? 0), rank: true, }, { header: t('ODD'), value: this.tribe.scoreDef - (this.latestSnapshot?.scoreDef ?? 0), }, { header: t('ODD - rank'), value: this.tribe.rankDef - (this.latestSnapshot?.rankDef ?? 0), rank: true, }, { header: t('OD'), value: this.tribe.scoreTotal - (this.latestSnapshot?.scoreTotal ?? 0), }, { header: t('OD - rank'), value: this.tribe.rankTotal - (this.latestSnapshot?.rankTotal ?? 0), rank: true, }, ]; td.insertAdjacentHTML( 'afterbegin', ` ${rows .map( (r) => ` ` ) .join('')}
${t('Changes')}
${r.header} ${ r.customFormat ? r.customFormat(r.value) : Math.abs(r.value).toLocaleString() }
` ); } private getStatBgColor(val: number, rank?: boolean): string { if ((val > 0 && !rank) || (val < 0 && rank)) { return '#0f0'; } if ((val < 0 && !rank) || (val > 0 && rank)) { return '#f00'; } return '#808080'; } private renderActions() { const tbody = document .querySelector('#content_value a[href*="twstats"]') ?.closest('tbody'); if (!(tbody instanceof HTMLTableSectionElement)) { return; } [ { name: t('Show tribe changes'), handler: this.showTribeChanges.bind(this), }, { name: t('Show history'), handler: this.showHistory.bind(this) }, { name: t('Show ennoblements'), handler: this.showEnnoblements.bind(this), }, ].forEach(({ name, handler }) => { const tr = document.createElement('tr'); const td = document.createElement('td'); td.colSpan = 2; const a = document.createElement('a'); a.innerText = name; a.href = '#'; a.addEventListener('click', handler); td.appendChild(a); tr.appendChild(td); tbody.appendChild(tr); }); } private async showTribeChanges(e: Event) { e.preventDefault(); await new DialogTable( DialogId.TRIBE_CHANGES, [ { header: t('Player'), accessor: (tc) => `${tc.player.name}`, }, { header: t('Action'), accessor: (tc) => t(tc.newTribe?.id !== this.tribe.id ? 'Left' : 'Joined'), }, { header: t('Old tribe'), accessor: (tc) => tc.player.tribe ? `${tc.player.tribe.tag}` : '-', }, { header: t('New tribe'), accessor: (tc) => tc.newTribe ? `${tc.newTribe.tag}` : '-', }, { header: t('Date/time'), accessor: (tc) => new Date(tc.createdAt).toLocaleString(), }, ], 30, (page: number, limit: number) => { return this.twhelpConnector.tribeTribeChanges(page, limit); } ).render(); } private async showHistory(e: Event) { e.preventDefault(); await new DialogTable( DialogId.HISTORY, [ { header: t('Date'), accessor: (s) => s.date, }, { header: t('Points'), accessor: (s) => `${s.allPoints.toLocaleString()} (${s.rank})`, }, { header: t('Villages'), accessor: (s) => `${s.numVillages.toLocaleString()}`, }, { header: t('Members'), accessor: (s) => `${s.numMembers.toLocaleString()}`, }, { header: t('Dominance'), accessor: (s) => `${s.dominance.toFixed(2)}%`, }, { header: t('OD'), accessor: (s) => `${s.scoreTotal.toLocaleString()} (${ s.rankTotal })`, }, { header: t('ODA'), accessor: (s) => `${s.scoreAtt.toLocaleString()} (${s.rankAtt})`, }, { header: t('ODD'), accessor: (s) => `${s.scoreDef.toLocaleString()} (${s.rankDef})`, }, ], 30, (page: number, limit: number) => { return this.twhelpConnector.tribeHistory(page, limit); } ).render(); } private async showEnnoblements(e: Event) { e.preventDefault(); await new DialogTable( DialogId.ENNOBLEMENTS, [ { header: t('Village'), accessor: (e) => `${e.village.fullName}`, }, { header: t('Points'), accessor: (e) => e.points.toLocaleString(), }, { header: t('Old owner'), accessor: ({ village: { player } }) => { if (!player) { return t('Barbarian'); } return `${player.name}${ player.tribe ? ` (${player.tribe.tag})` : '' }`; }, }, { header: t('New owner'), accessor: ({ newOwner }) => { if (!newOwner) { return t('Unknown'); } return `${newOwner.name}${ newOwner.tribe ? ` (${newOwner.tribe.tag})` : '' }`; }, }, { header: t('Date/time'), accessor: (e) => new Date(e.createdAt).toLocaleString(), }, ], 30, (page: number, limit: number) => { return this.twhelpConnector.tribeEnnoblements(page, limit); } ).render(); } private extendMemberTable() { const table = document.querySelector('#content_value .vis:nth-child(3)'); if ( !(table instanceof HTMLTableElement) || !table.querySelector('a[href*="info_player"]') ) { return; } if (!(table.previousElementSibling instanceof HTMLHeadingElement)) { return; } const div = document.createElement('div'); if (table.previousElementSibling instanceof HTMLHeadingElement) { div.appendChild(table.previousElementSibling); } div.appendChild(table); document.querySelector('#content_value')?.appendChild(div); for (const el of table.querySelectorAll('tr')) { const a = el.querySelector('a[href*="info_player"]'); if (!(a instanceof HTMLAnchorElement)) { [ t('ODA'), t('ODD'), t('ODS'), t('OD'), t('Last activity at'), t('Actions'), ].forEach((s) => { const th = document.createElement('th'); th.innerHTML = s; el.appendChild(th); }); continue; } const id = parseInt(new URL(a.href).searchParams.get('id') ?? ''); const member = this.members.find((m) => m.id === id); [ (member?.rankAtt ?? 0).toString(), (member?.rankDef ?? 0).toString(), (member?.rankSup ?? 0).toString(), (member?.rankTotal ?? 0).toString(), member?.lastActivityAt ? new Date(member.lastActivityAt).toLocaleString() : '-', `TWStats`, ].forEach((v) => { const td = document.createElement('td'); td.innerHTML = v; el.appendChild(td); }); } } } class ExtendedTribeProfile { private readonly twhelpConnector: TWHelpConnector; constructor(twhelpClient: TWHelpClient) { this.twhelpConnector = new TWHelpConnector( twhelpClient, window.game_data.market, window.game_data.world, this.getTribeId() ); } async run() { const [tribe, latestSnapshot, { data: members }] = await Promise.all([ this.twhelpConnector.tribe(), this.twhelpConnector.latestSnapshot(), this.twhelpConnector.tribeMembers(), ]); await new UI( window.game_data.world, tribe, latestSnapshot, members, this.twhelpConnector ).render(); } private getTribeId() { const str = new URLSearchParams(window.location.search).get('id'); if (!str) { throw new Error(`couldn't extract tribe id`); } return parseInt(str); } } (async () => { if ( window.game_data.screen !== 'info_ally' || window.game_data.mode !== null ) { return; } await new ExtendedTribeProfile( new TWHelpClient(process.env.TWHELP_API_BASE_URL ?? '') ) .run() .catch((err) => { console.log(err); }); })();