diff --git a/.terserrc.js b/.terserrc.js index fc9a2c9..eb02c3d 100644 --- a/.terserrc.js +++ b/.terserrc.js @@ -2,11 +2,11 @@ const preambles = { 'extended-player-profile': `// ==UserScript== // @name Extended player profile // @version 1.0.0 -// @description Adds additional info to player profile. +// @description Adds additional info and actions to a player's profile. // @author Dawid Wysokiński - Kichiyaki - contact@dwysokinski.me // @match https://*/game.php?*screen=info_player* -// @downloadURL ${process.env.PUBLIC_URL}/extendedPlayerProfile.js -// @updateURL ${process.env.PUBLIC_URL}/extendedPlayerProfile.js +// @downloadURL ${process.env.PUBLIC_URL}/userscripts/extendedPlayerProfile.js +// @updateURL ${process.env.PUBLIC_URL}/userscripts/extendedPlayerProfile.js // @icon https://www.google.com/s2/favicons?domain=plemiona.pl // @grant none // @run-at document-end diff --git a/package.json b/package.json index 3e2a4b3..8745269 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,12 @@ "version": "0.1.0", "license": "MIT", "scripts": { - "build-single": "PUBLIC_URL=https://scripts.tribalwarshelp.com parcel build --dist-dir dist", - "build:extended-player-profile": "PREAMBLE=extended-player-profile yarn build-single src/extendedPlayerProfile.ts", + "build-userscript": "PUBLIC_URL=https://scripts.tribalwarshelp.com parcel build --dist-dir dist/userscripts", + "build-quickbar": "PUBLIC_URL=https://scripts.tribalwarshelp.com parcel build --dist-dir dist/quickbar", + "build:userscript:extended-player-profile": "PREAMBLE=extended-player-profile yarn build-userscript src/extendedPlayerProfile.ts", + "build:quickbar:extended-player-profile": "yarn build-quickbar src/extendedPlayerProfile.ts", + "build:userscript": "npm-run-all build:userscript:*", + "build:quickbar": "npm-run-all build:quickbar:*", "build": "npm-run-all build:*" }, "author": { diff --git a/src/common/DialogTable.ts b/src/common/DialogTable.ts index acc069b..c962f8a 100644 --- a/src/common/DialogTable.ts +++ b/src/common/DialogTable.ts @@ -1,6 +1,6 @@ -import { ListResult } from '../lib/twhelp'; +import { createTranslationFunc } from '../utils'; -const translations: Record> = { +const t = createTranslationFunc({ pl_PL: { Loading: 'Wczytywanie', 'Previous page': 'Poprzednia strona', @@ -8,17 +8,18 @@ const translations: Record> = { 'Something went wrong while loading the data': 'Coś poszło nie tak podczas wczytywania danych', }, -}; - -const t = (s: string) => { - return translations[window.game_data.locale]?.[s] ?? s; -}; +}); export type DialogTableColumn = { header: string; accessor: (row: T, index: number, rows: T[]) => string; }; +export type LoadDataResult = { + data: T[]; + total: number; +}; + export class DialogTable { prevPageId: string; selectId: string; @@ -30,7 +31,7 @@ export class DialogTable { private readonly loadData: ( page: number, limit: number - ) => Promise> + ) => Promise> ) { this.prevPageId = `${this.id}_page_prev`; this.selectId = `${this.id}_page_select`; diff --git a/src/extendedPlayerProfile.ts b/src/extendedPlayerProfile.ts index 9776965..564ddfa 100644 --- a/src/extendedPlayerProfile.ts +++ b/src/extendedPlayerProfile.ts @@ -1,13 +1,11 @@ // Extended player profile -import { Player, TWHelpClient } from './lib/twhelp'; +import { Player, PlayerSnapshot, TWHelpClient } from './lib/twhelp'; import { DialogTable } from './common/DialogTable'; -import { InADayClient, InADayPlayerScore } from './lib/tw'; +import { InADayClient } from './lib/tw'; +import { createTranslationFunc } from './utils'; -const SCREEN = 'info_player'; -const MODE = null; - -const translations: Record> = { +const t = createTranslationFunc({ pl_PL: { 'Joined at': 'Dołączył o', 'Last activity at': 'Ostatnio aktywny o', @@ -24,6 +22,7 @@ const translations: Record> = { Village: 'Wioska', 'Old owner': 'Stary właściciel', 'New owner': 'Nowy właściciel', + Rank: 'Ranking', Points: 'Punkty', Barbarian: 'Barbarzyńska', Unknown: 'Nieznany', @@ -34,10 +33,14 @@ const translations: Record> = { Date: 'Data', Tribe: 'Plemię', Villages: 'Wioski', - OD: 'Pokonani ogólny', + 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', @@ -47,12 +50,9 @@ const translations: Record> = { 'Villages plundered': 'Splądrowane wioski', 'Resources gathered': 'Zebrane surowce', 'Villages conquered': 'Przejęte wioski', + Changes: 'Zmiany', }, -}; - -const t = (s: string) => { - return translations[window.game_data.locale]?.[s] ?? s; -}; +}); class TWHelpConnector { constructor( @@ -66,10 +66,15 @@ class TWHelpConnector { 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: limit, + limit, sort: ['createdAt:desc', 'id:asc'], }); } @@ -77,7 +82,7 @@ class TWHelpConnector { playerEnnoblements(page: number, limit: number) { return this.client.playerEnnoblements(this.version, this.server, this.id, { offset: (page - 1) * limit, - limit: limit, + limit, sort: ['createdAt:desc'], }); } @@ -85,7 +90,7 @@ class TWHelpConnector { playerHistory(page: number, limit: number) { return this.client.playerHistory(this.version, this.server, this.id, { offset: (page - 1) * limit, - limit: limit, + limit, sort: ['date:desc'], }); } @@ -112,12 +117,14 @@ enum DialogId { 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(); } @@ -160,6 +167,106 @@ class UI { ); } + 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"]') @@ -396,6 +503,7 @@ class UI { class ExtendedPlayerProfile { private readonly twhelpConnector: TWHelpConnector; private readonly inADayConnector: InADayConnector; + constructor(twhelpClient: TWHelpClient, inADayClient: InADayClient) { this.twhelpConnector = new TWHelpConnector( twhelpClient, @@ -412,7 +520,14 @@ class ExtendedPlayerProfile { async render() { const player = await this.twhelpConnector.player(); - new UI(player, this.twhelpConnector, this.inADayConnector).render(); + const latestSnapshot = await this.twhelpConnector.latestSnapshot(); + + new UI( + player, + latestSnapshot, + this.twhelpConnector, + this.inADayConnector + ).render(); } private getPlayerId() { @@ -433,7 +548,10 @@ class ExtendedPlayerProfile { } (async () => { - if (window.game_data.screen !== SCREEN || window.game_data.mode !== MODE) { + if ( + window.game_data.screen !== 'info_player' || + window.game_data.mode !== null + ) { return; } diff --git a/src/lib/tw.ts b/src/lib/tw.ts index 6eceedd..d70d627 100644 --- a/src/lib/tw.ts +++ b/src/lib/tw.ts @@ -95,10 +95,6 @@ class InADayParser { } } -export type InADayClientOptions = { - timeout?: number; -}; - type InADayParams = { name?: string; page?: number; @@ -131,9 +127,9 @@ export type InADayPlayer = { export class InADayClient { client: AxiosInstance; - constructor(opts?: InADayClientOptions) { + constructor(timeout?: number) { this.client = axios.create({ - timeout: opts?.timeout ?? DEFAULT_TIMEOUT, + timeout: timeout ?? DEFAULT_TIMEOUT, }); } @@ -190,7 +186,9 @@ export class InADayClient { }); } - await wait(random(200, 400)); + if (key !== urlsAndKeys[urlsAndKeys.length - 1].key) { + await wait(random(200, 400)); + } } if (scores.length === 0) { diff --git a/src/lib/twhelp.ts b/src/lib/twhelp.ts index 43fe05e..db41dd8 100644 --- a/src/lib/twhelp.ts +++ b/src/lib/twhelp.ts @@ -12,6 +12,17 @@ export type Version = { export type Player = { id: number; + points: number; + rank: number; + numVillages: number; + scoreAtt: number; + rankAtt: number; + scoreDef: number; + rankDef: number; + scoreSup: number; + rankSup: number; + scoreTotal: number; + rankTotal: number; bestRank: number; bestRankAt: string; mostPoints: number; @@ -101,17 +112,13 @@ export type ListPlayerSnapshotsParams = { sort?: string[]; }; -export type TWHelpClientOptions = { - timeout?: number; -}; - export class TWHelpClient { client: AxiosInstance; - constructor(url: string, opts?: TWHelpClientOptions) { + constructor(url: string, timeout?: number) { this.client = axios.create({ baseURL: url, - timeout: opts?.timeout ?? DEFAULT_TIMEOUT, + timeout: timeout ?? DEFAULT_TIMEOUT, }); } diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts new file mode 100644 index 0000000..0a6cd9b --- /dev/null +++ b/src/utils/i18n.ts @@ -0,0 +1,7 @@ +export const createTranslationFunc = ( + translations: Record> +) => { + return (s: string) => { + return translations[window.game_data.locale]?.[s] ?? s; + }; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 8c5f7e7..a737461 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1 +1,2 @@ +export * from './i18n'; export * from './wait';