// Extended player profile import { Player, PlayerSnapshot, TWHelpClient } from './lib/twhelp'; import { DialogTable } from './common/dialog-table'; import { InADayClient } from './lib/tw'; import { createTranslationFunc } from './utils'; import { buildURL } from './lib/twstats'; const t = createTranslationFunc({ pl_PL: { 'Joined at': 'Dołączył o', 'Last activity at': 'Ostatnio aktywny o', 'Best rank': 'Najwyższy ranking', 'Most points': 'Najwięcej punktów', 'Most villages': 'Najwięcej wiosek', 'Show other servers': 'Pokaż inne serwery', 'Show in a day ranks': 'Pokaż dzienne rankingi', 'Show tribe changes': 'Pokaż zmiany plemion', 'Show history': 'Pokaż historię', 'Show ennoblements': 'Pokaż przejęcia', 'Old tribe': 'Stare plemię', 'New tribe': 'Nowe plemię', 'Date/time': 'Data/czas', Village: 'Wioska', 'Old owner': 'Stary właściciel', 'New owner': 'Nowy właściciel', Rank: 'Ranking', Points: 'Punkty', Barbarian: 'Barbarzyńska', Unknown: 'Nieznany', Server: 'Serwer', Deleted: 'Usunięty', Yes: 'Tak', No: 'Nie', Date: 'Data', Tribe: 'Plemię', Villages: 'Wioski', 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', Type: 'Typ', Score: 'Wynik', 'Units defeated while attacking': 'Jednostki pokonane w ataku', 'Units defeated while defending': 'Jednostki pokonane w obronie', 'Units defeated while supporting': 'Jednostki pokonane jako wspierający', 'Resources plundered': 'Zrabowane surowce', 'Villages plundered': 'Splądrowane wioski', 'Resources gathered': 'Zebrane surowce', 'Villages conquered': 'Przejęte wioski', Changes: 'Zmiany', Open: 'Otwarty', Closed: 'Zamknięty', }, }); class TWHelpConnector { constructor( private readonly client: TWHelpClient, private readonly version: string, private readonly server: string, private readonly id: number ) {} player() { return this.client.player(this.version, this.server, this.id); } async latestSnapshot() { const history = await this.playerHistory(1, 1); return history.data.length > 0 ? history.data[0] : null; } playerTribeChanges(page: number, limit: number) { return this.client.playerTribeChanges(this.version, this.server, this.id, { offset: (page - 1) * limit, limit, sort: ['createdAt:desc', 'id:asc'], }); } playerEnnoblements(page: number, limit: number) { return this.client.playerEnnoblements(this.version, this.server, this.id, { offset: (page - 1) * limit, limit, sort: ['createdAt:desc'], }); } playerHistory(page: number, limit: number) { return this.client.playerHistory(this.version, this.server, this.id, { offset: (page - 1) * limit, limit, sort: ['date:desc'], }); } playerOtherServers(page: number, limit: number) { return this.client.playerOtherServers(this.version, this.server, this.id, { offset: (page - 1) * limit, limit, }); } } class InADayConnector { constructor( private readonly client: InADayClient, private readonly name: string ) {} player() { return this.client.player(this.name); } } enum DialogId { OTHER_SERVERS = 'extended_player_profile_other_servers', IN_A_DAY_RANKS = 'extended_player_profile_in_a_day_ranks', TRIBE_CHANGES = 'extended_player_profile_tribe_changes', ENNOBLEMENTS = 'extended_player_profile_ennoblements', HISTORY = 'extended_player_profile_history', } class UI { constructor( private readonly player: Player, private readonly latestSnapshot: PlayerSnapshot | null, private readonly twhelpConnector: TWHelpConnector, private readonly inADayConnector: InADayConnector ) {} public render() { this.renderAdditionalInfo(); this.renderStats(); this.renderActions(); } private renderAdditionalInfo() { const tbody = document.querySelector('#player_info tbody'); if (!(tbody instanceof HTMLTableSectionElement)) { return; } tbody.insertAdjacentHTML( 'beforeend', ` ${t('Joined at')}: ${new Date(this.player.createdAt).toLocaleString()} ${t('Last activity at')}: ${new Date(this.player.lastActivityAt).toLocaleString()} ${t('Best rank')}: ${this.player.bestRank} (${new Date( this.player.bestRankAt ).toLocaleString()}) ${t('Most points')}: ${this.player.mostPoints.toLocaleString()} (${new Date( this.player.mostPointsAt ).toLocaleString()}) ${t('Most villages')}: ${this.player.mostVillages.toLocaleString()} (${new Date( this.player.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.player.points - (this.latestSnapshot?.points ?? 0), }, { header: t('Rank'), value: this.player.rank - (this.latestSnapshot?.rank ?? 0), rank: true, }, { header: t('Villages'), value: this.player.numVillages - (this.latestSnapshot?.numVillages ?? 0), }, { header: t('ODA'), value: this.player.scoreAtt - (this.latestSnapshot?.scoreAtt ?? 0), }, { header: t('ODA - rank'), value: this.player.rankAtt - (this.latestSnapshot?.rankAtt ?? 0), rank: true, }, { header: t('ODD'), value: this.player.scoreDef - (this.latestSnapshot?.scoreDef ?? 0), }, { header: t('ODD - rank'), value: this.player.rankDef - (this.latestSnapshot?.rankDef ?? 0), rank: true, }, { header: t('ODS'), value: this.player.scoreSup - (this.latestSnapshot?.scoreSup ?? 0), }, { header: t('ODS - rank'), value: this.player.rankSup - (this.latestSnapshot?.rankSup ?? 0), rank: true, }, { header: t('OD'), value: this.player.scoreTotal - (this.latestSnapshot?.scoreTotal ?? 0), }, { header: t('OD - rank'), value: this.player.rankTotal - (this.latestSnapshot?.rankTotal ?? 0), rank: true, }, ]; td.insertAdjacentHTML( 'afterbegin', ` ${rows .map( (r) => ` ` ) .join('')}
${t('Changes')}
${r.header} ${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 other servers'), handler: this.showOtherServers.bind(this), }, { name: t('Show in a day ranks'), handler: this.showInADayRanks.bind(this), }, { 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 showOtherServers(e: Event) { e.preventDefault(); await new DialogTable( DialogId.OTHER_SERVERS, [ { header: t('Server'), accessor: (p) => `${p.server.key} (${ p.server.open ? t('Open') : t('Closed') })`, }, { header: t('Tribe'), accessor: (p) => { if (!p.tribe) { return '-'; } const url = buildURL({ server: p.server.key, id: p.tribe.id, entity: 'tribe', }); return `${p.tribe.tag}`; }, }, { header: t('Best rank'), accessor: (p) => p.bestRank.toString(), }, { header: t('Most points'), accessor: (p) => p.mostPoints.toLocaleString(), }, { header: t('Most villages'), accessor: (p) => p.mostVillages.toLocaleString(), }, ], 30, (page: number, limit: number) => { return this.twhelpConnector.playerOtherServers(page, limit); } ).render(); } private async showInADayRanks(e: Event) { e.preventDefault(); const translationKeyByType: Record = { killAtt: 'Units defeated while attacking', killDef: 'Units defeated while defending', killSup: 'Units defeated while supporting', lootRes: 'Resources plundered', lootVil: 'Villages plundered', scavenge: 'Resources gathered', conquer: 'Villages conquered', }; await new DialogTable( DialogId.IN_A_DAY_RANKS, [ { header: t('Type'), accessor: (s) => s.type in translationKeyByType ? t(translationKeyByType[s.type]) : s.type, }, { header: t('Rank'), accessor: (s) => (s.rank ? s.rank.toString() : '0'), }, { header: t('Score'), accessor: (s) => (s.score ? s.score.toLocaleString() : '0'), }, { header: t('Date'), accessor: (s) => (s.date ? s.date : '-'), }, ], 30, async () => { const player = await this.inADayConnector.player(); if (!player) { return { data: [], total: 0, }; } return { data: Object.entries(player?.scores) .map(([type, sc]) => ({ type, ...(sc ? sc : {}), })) .sort((a, b) => (a.type > b.type ? 1 : -1)), total: Object.entries(player?.scores).length, }; } ).render(); } private async showTribeChanges(e: Event) { e.preventDefault(); await new DialogTable( DialogId.TRIBE_CHANGES, [ { 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.playerTribeChanges(page, limit); } ).render(); } private async showHistory(e: Event) { e.preventDefault(); await new DialogTable( DialogId.HISTORY, [ { header: t('Date'), accessor: (s) => s.date, }, { header: t('Tribe'), accessor: (s) => s.tribe ? `${s.tribe.tag}` : '-', }, { header: t('Points'), accessor: (s) => `${s.points.toLocaleString()} (${s.rank})`, }, { header: t('Villages'), accessor: (s) => `${s.numVillages.toLocaleString()}`, }, { 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})`, }, { header: t('ODS'), accessor: (s) => `${s.scoreSup.toLocaleString()} (${s.rankSup})`, }, ], 30, (page: number, limit: number) => { return this.twhelpConnector.playerHistory(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.playerEnnoblements(page, limit); } ).render(); } } class ExtendedPlayerProfile { private readonly twhelpConnector: TWHelpConnector; private readonly inADayConnector: InADayConnector; constructor(twhelpClient: TWHelpClient, inADayClient: InADayClient) { this.twhelpConnector = new TWHelpConnector( twhelpClient, window.game_data.market, window.game_data.world, this.getPlayerId() ); this.inADayConnector = new InADayConnector( inADayClient, this.getPlayerName() ); } async run() { const [player, latestSnapshot] = await Promise.all([ this.twhelpConnector.player(), this.twhelpConnector.latestSnapshot(), ]); new UI( player, latestSnapshot, this.twhelpConnector, this.inADayConnector ).render(); } private getPlayerId() { const str = new URLSearchParams(window.location.search).get('id'); if (!str) { return window.game_data.player.id; } return parseInt(str); } private getPlayerName() { const header = document.querySelector('#map_toggler')?.closest('th'); if (!(header instanceof HTMLTableCellElement)) { return window.game_data.player.name; } return header.innerText.trim(); } } (async () => { if ( window.game_data.screen !== 'info_player' || window.game_data.mode !== null ) { return; } await new ExtendedPlayerProfile( new TWHelpClient(process.env.TWHELP_API_BASE_URL ?? ''), new InADayClient() ) .run() .catch((err) => { console.log(err); }); })();