From 5a9ba30ee5095419349801a8e77aa129fcc58946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Mon, 16 Jan 2023 07:32:06 +0100 Subject: [PATCH 01/10] feat: extended player profile --- .env | 1 + .gitignore | 1 - package.json | 2 + src/extendedPlayerProfile.ts | 111 +++++++++++++++++++++++++++++++++++ src/global.d.ts | 10 ++++ src/lib/client.ts | 62 +++++++++++++++++++ yarn.lock | 75 +++++++++++++++++++++++ 7 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 .env create mode 100644 src/lib/client.ts diff --git a/.env b/.env new file mode 100644 index 0000000..6ded5fa --- /dev/null +++ b/.env @@ -0,0 +1 @@ +TWHELP_API_BASE_URL=https://tribalwarshelp.com diff --git a/.gitignore b/.gitignore index 3fbe1a1..3df602f 100644 --- a/.gitignore +++ b/.gitignore @@ -74,7 +74,6 @@ web_modules/ .yarn-integrity # dotenv environment variable files -.env .env.development.local .env.test.local .env.production.local diff --git a/package.json b/package.json index 76c8d7e..6f0c741 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,14 @@ "devDependencies": { "@types/jquery": "^3.5.14", "@types/node": "^18.11.17", + "buffer": "^5.5.0", "npm-run-all": "^4.1.5", "parcel": "^2.8.1", "prettier": "^2.8.1", "typescript": "^4.9.4" }, "dependencies": { + "axios": "^1.2.2", "date-fns": "^2.29.3" } } diff --git a/src/extendedPlayerProfile.ts b/src/extendedPlayerProfile.ts index 3cd9b0e..4cbe21b 100644 --- a/src/extendedPlayerProfile.ts +++ b/src/extendedPlayerProfile.ts @@ -1 +1,112 @@ // Extended player profile + +import { Player, TWHelpClient } from './lib/client'; + +const SCREEN = 'info_player'; + +const translations: Record> = { + pl_PL: {}, +}; + +const t = (s: string) => { + return translations[window.game_data.locale]?.[s] ?? s; +}; + +const getPlayerId = () => { + const str = new URLSearchParams(window.location.search).get('id'); + if (!str) { + return window.game_data.player.id; + } + return parseInt(str); +}; + +class ExtendedPlayerProfile { + constructor(private readonly client: TWHelpClient) {} + + async render() { + const player = await this.client.player( + window.game_data.market, + window.game_data.world, + getPlayerId() + ); + console.log(player); + + this.renderAdditionalInfo(player); + this.renderActions(player); + } + + private renderAdditionalInfo(player: Player) { + const tbody = document.querySelector('#player_info tbody'); + if (!(tbody instanceof HTMLTableSectionElement)) { + return; + } + + tbody.insertAdjacentHTML( + 'beforeend', + ` + + ${t('Joined at')}: + ${new Date(player.createdAt).toLocaleString()} + + + ${t('Last activity at')}: + ${new Date(player.lastActivityAt).toLocaleString()} + + + ${t('Best rank')}: + ${player.bestRank} (${new Date( + player.bestRankAt + ).toLocaleString()}) + + + ${t('Most points')}: + ${player.mostPoints.toLocaleString()} (${new Date( + player.mostPointsAt + ).toLocaleString()}) + + + ${t('Most villages')}: + ${player.mostVillages.toLocaleString()} (${new Date( + player.mostVillagesAt + ).toLocaleString()}) + + ` + ); + } + + private renderActions(player: Player) { + const tbody = document + .querySelector('#content_value a[href*="twstats"]') + ?.closest('tbody'); + if (!(tbody instanceof HTMLTableSectionElement)) { + return; + } + + [ + { name: t('Show tribe changes') }, + { name: t('Show history') }, + { name: t('Show ennoblements') }, + ].forEach(({ name }) => { + const tr = document.createElement('tr'); + const td = document.createElement('td'); + td.colSpan = 2; + const a = document.createElement('a'); + a.innerText = name; + a.href = '#'; + a.setAttribute('data-player-id', player.id.toString()); + td.appendChild(a); + tr.appendChild(td); + tbody.appendChild(tr); + }); + } +} + +(async () => { + if (window.game_data.screen !== SCREEN || window.game_data.mode) { + return; + } + + await new ExtendedPlayerProfile( + new TWHelpClient(process.env.TWHELP_API_BASE_URL ?? '') + ).render(); +})(); diff --git a/src/global.d.ts b/src/global.d.ts index eec5751..071b6f0 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -10,16 +10,26 @@ declare global { village: { id: number; }; + market: string; locale: string; screen: string; mode: string | null; + world: string; }; TribalWars: { redirect: (queryParams: Record) => void; + buildURL: ( + method: string, + screen: string, + params: Record + ) => string; }; UI: { InfoMessage: (s: string) => void; ErrorMessage: (s: string) => void; }; + Dialog: { + show: (id: string, html: string) => void; + }; } } diff --git a/src/lib/client.ts b/src/lib/client.ts new file mode 100644 index 0000000..5ee1654 --- /dev/null +++ b/src/lib/client.ts @@ -0,0 +1,62 @@ +import axios, { AxiosInstance } from 'axios'; + +const DEFAULT_TIMEOUT = 10000; +const X_TOTAL_COUNT_HEADER = 'X-Total-Count'; + +export type Version = { + code: string; + host: string; + name: string; + timezone: string; +}; + +export type Player = { + id: number; + bestRank: number; + bestRankAt: string; + mostPoints: number; + mostPointsAt: string; + mostVillages: number; + mostVillagesAt: string; + lastActivityAt: string; + createdAt: string; +}; + +export type ListResult = { + data: T[]; + total: number; +}; + +export type TWHelpClientOptions = { + timeout?: number; +}; + +export class TWHelpClient { + client: AxiosInstance; + + constructor(url: string, opts?: TWHelpClientOptions) { + this.client = axios.create({ + baseURL: url, + timeout: opts?.timeout ?? DEFAULT_TIMEOUT, + }); + } + + public async versions(): Promise> { + const resp = await this.client.get('/api/v1/versions'); + return { + data: resp.data.data, + total: parseInt(resp.headers[X_TOTAL_COUNT_HEADER] ?? '0'), + }; + } + + public async player( + version: string, + server: string, + id: number + ): Promise { + const resp = await this.client.get( + `/api/v1/versions/${version}/servers/${server}/players/${id}` + ); + return resp.data.data; + } +} diff --git a/yarn.lock b/yarn.lock index 16f09fc..3a94b3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -761,6 +761,20 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.2.tgz#72681724c6e6a43a9fea860fc558127dbe32f9f1" + integrity sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -773,6 +787,11 @@ base-x@^3.0.8: dependencies: safe-buffer "^5.0.1" +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -801,6 +820,14 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -870,6 +897,13 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -951,6 +985,11 @@ define-properties@^1.1.3, define-properties@^1.1.4: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" @@ -1068,6 +1107,20 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -1194,6 +1247,11 @@ htmlparser2@^7.1.1: domutils "^2.8.0" entities "^3.0.1" +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -1427,6 +1485,18 @@ memorystream@^0.3.1: resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + minimatch@^3.0.4: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -1668,6 +1738,11 @@ prettier@^2.8.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.1.tgz#4e1fd11c34e2421bc1da9aea9bd8127cd0a35efc" integrity sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg== +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + react-error-overlay@6.0.9: version "6.0.9" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" -- 2.40.1 From 29ae462b1f928eb61dfc6a219148ddc0bbcd04e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Wed, 18 Jan 2023 07:46:16 +0100 Subject: [PATCH 02/10] feat: extended player profile --- src/extendedPlayerProfile.ts | 229 +++++++++++++++++++++++++++++------ src/lib/client.ts | 60 ++++++++- 2 files changed, 250 insertions(+), 39 deletions(-) diff --git a/src/extendedPlayerProfile.ts b/src/extendedPlayerProfile.ts index 4cbe21b..41d5df1 100644 --- a/src/extendedPlayerProfile.ts +++ b/src/extendedPlayerProfile.ts @@ -1,41 +1,74 @@ // Extended player profile -import { Player, TWHelpClient } from './lib/client'; +import { ListResult, Player, TribeChange, TWHelpClient } from './lib/client'; const SCREEN = 'info_player'; +const MODE = null; const translations: Record> = { - pl_PL: {}, + pl_PL: { + 'Joined at': 'Dołączył o', + 'Last activity at': 'Ostatnio aktywny o', + 'Best rank': 'Najlepszy 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', + 'Fetching data': 'Pobieranie danych', + 'Old tribe': 'Stare plemię', + 'New tribe': 'Nowe plemię', + 'Date/time': 'Data/czas', + }, }; const t = (s: string) => { return translations[window.game_data.locale]?.[s] ?? s; }; -const getPlayerId = () => { - const str = new URLSearchParams(window.location.search).get('id'); - if (!str) { - return window.game_data.player.id; - } - return parseInt(str); -}; +class TWHelpConnector { + constructor( + private readonly client: TWHelpClient, + private readonly version: string, + private readonly server: string, + private readonly id: number + ) {} -class ExtendedPlayerProfile { - constructor(private readonly client: TWHelpClient) {} - - async render() { - const player = await this.client.player( - window.game_data.market, - window.game_data.world, - getPlayerId() - ); - console.log(player); - - this.renderAdditionalInfo(player); - this.renderActions(player); + player() { + return this.client.player(this.version, this.server, this.id); } - private renderAdditionalInfo(player: Player) { + tribeChanges(page: number, limit: number) { + return this.client.tribeChanges(this.version, this.server, this.id, { + offset: (page - 1) * limit, + limit: limit, + sort: ['createdAt:desc', 'id:asc'], + }); + } +} + +interface UIConnector { + tribeChanges(page: number, limit: number): Promise>; +} + +enum DialogId { + TRIBE_CHANGES_LOADING = 'kichiyaki_tribe_changes_loading', + TRIBE_CHANGES = 'kichiyaki_tribe_changes', +} + +class UI { + private static readonly TRIBE_CHANGES_LIMIT = 30; + constructor( + private readonly player: Player, + private readonly connector: UIConnector + ) {} + + public render() { + this.renderAdditionalInfo(); + this.renderActions(); + } + + private renderAdditionalInfo() { const tbody = document.querySelector('#player_info tbody'); if (!(tbody instanceof HTMLTableSectionElement)) { return; @@ -46,35 +79,35 @@ class ExtendedPlayerProfile { ` ${t('Joined at')}: - ${new Date(player.createdAt).toLocaleString()} + ${new Date(this.player.createdAt).toLocaleString()} ${t('Last activity at')}: - ${new Date(player.lastActivityAt).toLocaleString()} + ${new Date(this.player.lastActivityAt).toLocaleString()} ${t('Best rank')}: - ${player.bestRank} (${new Date( - player.bestRankAt + ${this.player.bestRank} (${new Date( + this.player.bestRankAt ).toLocaleString()}) ${t('Most points')}: - ${player.mostPoints.toLocaleString()} (${new Date( - player.mostPointsAt + ${this.player.mostPoints.toLocaleString()} (${new Date( + this.player.mostPointsAt ).toLocaleString()}) ${t('Most villages')}: - ${player.mostVillages.toLocaleString()} (${new Date( - player.mostVillagesAt + ${this.player.mostVillages.toLocaleString()} (${new Date( + this.player.mostVillagesAt ).toLocaleString()}) ` ); } - private renderActions(player: Player) { + private renderActions() { const tbody = document .querySelector('#content_value a[href*="twstats"]') ?.closest('tbody'); @@ -83,26 +116,146 @@ class ExtendedPlayerProfile { } [ - { name: t('Show tribe changes') }, - { name: t('Show history') }, - { name: t('Show ennoblements') }, - ].forEach(({ name }) => { + { + 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.setAttribute('data-player-id', player.id.toString()); + a.addEventListener('click', handler); + a.setAttribute('data-player-id', this.player.id.toString()); td.appendChild(a); tr.appendChild(td); tbody.appendChild(tr); }); } + + private async showTribeChanges(e: Event) { + e.preventDefault(); + + await this._showTribeChanges(1); + } + + private async _showTribeChanges(page: number) { + window.Dialog.show( + DialogId.TRIBE_CHANGES_LOADING, + `

${t('Fetching data')}...

` + ); + + const tcs = await this.connector.tribeChanges(page, UI.TRIBE_CHANGES_LIMIT); + + const maxPage = Math.ceil(tcs.total / UI.TRIBE_CHANGES_LIMIT); + const pageOpts = []; + for (let i = 1; i <= (page > maxPage ? page : maxPage); i++) { + pageOpts.push( + `${i}` + ); + } + + const selectId = `${DialogId.TRIBE_CHANGES}_page`; + + window.Dialog.show( + DialogId.TRIBE_CHANGES, + ` +
+ +
+ + + + + + + + ${tcs.data + .map( + (tc) => ` + + + + + + ` + ) + .join('')} + +
${t('Old tribe')}${t('New tribe')}${t('Date/time')}
${ + tc.player.tribe + ? `${tc.player.tribe.tag}` + : '-' + }${ + tc.newTribe + ? `${tc.newTribe.tag}` + : '-' + }${new Date(tc.createdAt).toLocaleString()}
+ ` + ); + + document + .querySelector('#' + selectId) + ?.addEventListener('change', (e: Event) => { + console.log(e.currentTarget); + if (!(e.currentTarget instanceof HTMLSelectElement)) { + return; + } + + this._showTribeChanges(parseInt(e.currentTarget.value)); + }); + } + + private showHistory(e: Event) { + e.preventDefault(); + } + + private showEnnoblements(e: Event) { + e.preventDefault(); + } +} + +class ExtendedPlayerProfile { + connector: TWHelpConnector; + constructor(client: TWHelpClient) { + this.connector = new TWHelpConnector( + client, + window.game_data.market, + window.game_data.world, + this.getPlayerId() + ); + } + + async render() { + const player = await this.connector.player(); + new UI(player, this.connector).render(); + } + + private getPlayerId() { + const str = new URLSearchParams(window.location.search).get('id'); + if (!str) { + return window.game_data.player.id; + } + return parseInt(str); + } } (async () => { - if (window.game_data.screen !== SCREEN || window.game_data.mode) { + if (window.game_data.screen !== SCREEN || window.game_data.mode !== MODE) { return; } diff --git a/src/lib/client.ts b/src/lib/client.ts index 5ee1654..571bd32 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -1,7 +1,7 @@ import axios, { AxiosInstance } from 'axios'; const DEFAULT_TIMEOUT = 10000; -const X_TOTAL_COUNT_HEADER = 'X-Total-Count'; +const X_TOTAL_COUNT_HEADER = 'x-total-count'; export type Version = { code: string; @@ -22,11 +22,38 @@ export type Player = { createdAt: string; }; +export type TribeMeta = { + id: number; + name: string; + tag: string; + profileUrl: string; +}; + +export type PlayerMeta = { + id: number; + name: string; + profileUrl: string; + tribe: TribeMeta | null; +}; + +export type TribeChange = { + id: number; + newTribe: TribeMeta | null; + player: PlayerMeta; + createdAt: string; +}; + export type ListResult = { data: T[]; total: number; }; +export type ListTribeChangesQueryParams = { + offset?: number; + limit?: number; + sort?: string[]; +}; + export type TWHelpClientOptions = { timeout?: number; }; @@ -59,4 +86,35 @@ export class TWHelpClient { ); return resp.data.data; } + + public async tribeChanges( + version: string, + server: string, + id: number, + queryParams?: ListTribeChangesQueryParams + ): Promise> { + const params = new URLSearchParams(); + + if (queryParams?.limit) { + params.set('limit', queryParams.limit.toString()); + } + + if (queryParams?.offset) { + params.set('offset', queryParams.offset.toString()); + } + + if (Array.isArray(queryParams?.sort)) { + queryParams?.sort.forEach((s) => { + params.append('sort', s); + }); + } + + const resp = await this.client.get( + `/api/v1/versions/${version}/servers/${server}/players/${id}/tribe-changes?${params.toString()}` + ); + return { + data: resp.data.data, + total: parseInt(resp.headers[X_TOTAL_COUNT_HEADER] ?? '0'), + }; + } } -- 2.40.1 From 84c5c3fc9a0a6328e95eb1e53af464682db36e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Wed, 18 Jan 2023 16:40:54 +0100 Subject: [PATCH 03/10] feat: extended player profile --- src/common/DialogTable.ts | 113 +++++++++++++++++++++++++++++++++++ src/extendedPlayerProfile.ts | 111 ++++++++++------------------------ 2 files changed, 144 insertions(+), 80 deletions(-) create mode 100644 src/common/DialogTable.ts diff --git a/src/common/DialogTable.ts b/src/common/DialogTable.ts new file mode 100644 index 0000000..a5b3b08 --- /dev/null +++ b/src/common/DialogTable.ts @@ -0,0 +1,113 @@ +import { ListResult } from '../lib/client'; + +const translations: Record> = { + pl_PL: { + Loading: 'Wczytywanie', + 'Previous page': 'Poprzednia strona', + 'Next page': 'Następna strona', + }, +}; + +const t = (s: string) => { + return translations[window.game_data.locale]?.[s] ?? s; +}; + +export type DialogTableColumn = { + header: string; + accessor: (row: T) => string; +}; + +export class DialogTable { + constructor( + private readonly id: string, + private readonly columns: DialogTableColumn[], + private readonly limit: number, + private readonly loadData: ( + page: number, + limit: number + ) => Promise> + ) {} + + public async render() { + await this.renderPage(1); + } + + private async renderPage(page: number) { + window.Dialog.show(`${this.id}_loading`, `

${t('Loading')}...

`); + + const { data, total } = await this.loadData(page, this.limit); + + const maxPage = Math.ceil(total / this.limit); + const pageOpts = []; + for (let i = 1; i <= (page > maxPage ? page : maxPage); i++) { + pageOpts.push( + `${i}` + ); + } + + const prevPageId = `${this.id}_page_prev`; + const selectId = `${this.id}_page_select`; + const nextPageId = `${this.id}_page_next`; + + window.Dialog.show( + this.id, + ` +
+ + + +
+ + + + ${this.columns.map((col) => ``).join('')} + + ${data + .map( + (r) => ` + + ${this.columns + .map((col) => ``) + .join('')} + + ` + ) + .join('')} + +
${col.header}
${col.accessor(r)}
+ ` + ); + + document + .querySelector('#' + prevPageId) + ?.addEventListener('click', (e: Event) => { + this.renderPage(page - 1); + }); + + document + .querySelector('#' + selectId) + ?.addEventListener('change', (e: Event) => { + if (!(e.currentTarget instanceof HTMLSelectElement)) { + return; + } + + this.renderPage(parseInt(e.currentTarget.value)); + }); + + document + .querySelector('#' + nextPageId) + ?.addEventListener('click', (e: Event) => { + this.renderPage(page + 1); + }); + } +} diff --git a/src/extendedPlayerProfile.ts b/src/extendedPlayerProfile.ts index 41d5df1..6001833 100644 --- a/src/extendedPlayerProfile.ts +++ b/src/extendedPlayerProfile.ts @@ -1,6 +1,7 @@ // Extended player profile import { ListResult, Player, TribeChange, TWHelpClient } from './lib/client'; +import { DialogTable } from './common/DialogTable'; const SCREEN = 'info_player'; const MODE = null; @@ -15,7 +16,6 @@ const translations: Record> = { 'Show tribe changes': 'Pokaż zmiany plemion', 'Show history': 'Pokaż historię', 'Show ennoblements': 'Pokaż przejęcia', - 'Fetching data': 'Pobieranie danych', 'Old tribe': 'Stare plemię', 'New tribe': 'Nowe plemię', 'Date/time': 'Data/czas', @@ -47,13 +47,12 @@ class TWHelpConnector { } } -interface UIConnector { - tribeChanges(page: number, limit: number): Promise>; +enum DialogId { + TRIBE_CHANGES = 'kichiyaki_tribe_changes', } -enum DialogId { - TRIBE_CHANGES_LOADING = 'kichiyaki_tribe_changes_loading', - TRIBE_CHANGES = 'kichiyaki_tribe_changes', +interface UIConnector { + tribeChanges(page: number, limit: number): Promise>; } class UI { @@ -143,81 +142,33 @@ class UI { private async showTribeChanges(e: Event) { e.preventDefault(); - await this._showTribeChanges(1); - } - - private async _showTribeChanges(page: number) { - window.Dialog.show( - DialogId.TRIBE_CHANGES_LOADING, - `

${t('Fetching data')}...

` - ); - - const tcs = await this.connector.tribeChanges(page, UI.TRIBE_CHANGES_LIMIT); - - const maxPage = Math.ceil(tcs.total / UI.TRIBE_CHANGES_LIMIT); - const pageOpts = []; - for (let i = 1; i <= (page > maxPage ? page : maxPage); i++) { - pageOpts.push( - `${i}` - ); - } - - const selectId = `${DialogId.TRIBE_CHANGES}_page`; - - window.Dialog.show( + await new DialogTable( DialogId.TRIBE_CHANGES, - ` -
- -
- - - - - - - - ${tcs.data - .map( - (tc) => ` - - - - - - ` - ) - .join('')} - -
${t('Old tribe')}${t('New tribe')}${t('Date/time')}
${ - tc.player.tribe - ? `${tc.player.tribe.tag}` - : '-' - }${ - tc.newTribe - ? `${tc.newTribe.tag}` - : '-' - }${new Date(tc.createdAt).toLocaleString()}
- ` - ); - - document - .querySelector('#' + selectId) - ?.addEventListener('change', (e: Event) => { - console.log(e.currentTarget); - if (!(e.currentTarget instanceof HTMLSelectElement)) { - return; - } - - this._showTribeChanges(parseInt(e.currentTarget.value)); - }); + [ + { + 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(), + }, + ], + 2, + (page: number, limit: number) => { + return this.connector.tribeChanges(page, limit); + } + ).render(); } private showHistory(e: Event) { -- 2.40.1 From 6f1a7e5852ce97b1b6a6ff9e21c29f72dc6b8f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Wed, 18 Jan 2023 16:41:07 +0100 Subject: [PATCH 04/10] feat: extended player profile --- src/extendedPlayerProfile.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/extendedPlayerProfile.ts b/src/extendedPlayerProfile.ts index 6001833..2a05c1d 100644 --- a/src/extendedPlayerProfile.ts +++ b/src/extendedPlayerProfile.ts @@ -56,7 +56,6 @@ interface UIConnector { } class UI { - private static readonly TRIBE_CHANGES_LIMIT = 30; constructor( private readonly player: Player, private readonly connector: UIConnector @@ -164,7 +163,7 @@ class UI { accessor: (tc) => new Date(tc.createdAt).toLocaleString(), }, ], - 2, + 30, (page: number, limit: number) => { return this.connector.tribeChanges(page, limit); } -- 2.40.1 From 7102eece1fa36b6d09f858a63e39896902b76950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Wed, 18 Jan 2023 17:02:13 +0100 Subject: [PATCH 05/10] feat: extended player profile --- src/extendedPlayerProfile.ts | 85 +++++++++++++++++++++++++++++++++--- src/lib/client.ts | 57 +++++++++++++++++++++++- 2 files changed, 135 insertions(+), 7 deletions(-) diff --git a/src/extendedPlayerProfile.ts b/src/extendedPlayerProfile.ts index 2a05c1d..9da1168 100644 --- a/src/extendedPlayerProfile.ts +++ b/src/extendedPlayerProfile.ts @@ -1,6 +1,12 @@ // Extended player profile -import { ListResult, Player, TribeChange, TWHelpClient } from './lib/client'; +import { + Ennoblement, + ListResult, + Player, + TribeChange, + TWHelpClient, +} from './lib/client'; import { DialogTable } from './common/DialogTable'; const SCREEN = 'info_player'; @@ -19,6 +25,12 @@ const translations: Record> = { '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', + Points: 'Punkty', + Barbarian: 'Barbarzyńska', + Unknown: 'Nieznany', }, }; @@ -38,21 +50,37 @@ class TWHelpConnector { return this.client.player(this.version, this.server, this.id); } - tribeChanges(page: number, limit: number) { - return this.client.tribeChanges(this.version, this.server, this.id, { + playerTribeChanges(page: number, limit: number) { + return this.client.playerTribeChanges(this.version, this.server, this.id, { offset: (page - 1) * limit, 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: limit, + sort: ['createdAt:desc'], + }); + } } enum DialogId { TRIBE_CHANGES = 'kichiyaki_tribe_changes', + ENNOBLEMENTS = 'kichiyaki_ennoblements', } interface UIConnector { - tribeChanges(page: number, limit: number): Promise>; + playerTribeChanges( + page: number, + limit: number + ): Promise>; + playerEnnoblements( + page: number, + limit: number + ): Promise>; } class UI { @@ -165,7 +193,7 @@ class UI { ], 30, (page: number, limit: number) => { - return this.connector.tribeChanges(page, limit); + return this.connector.playerTribeChanges(page, limit); } ).render(); } @@ -174,8 +202,53 @@ class UI { e.preventDefault(); } - private showEnnoblements(e: Event) { + 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: (e) => + e.oldOwner + ? `${e.oldOwner.name}${ + e.oldOwner.tribe + ? ` (${e.oldOwner.tribe.tag})` + : '' + }` + : t('Barbarian'), + }, + { + header: t('New owner'), + accessor: (e) => + e.newOwner + ? `${e.newOwner.name}${ + e.newOwner.tribe + ? ` (${e.newOwner.tribe.tag})` + : '' + }` + : t('Unknown'), + }, + { + header: t('Date/time'), + accessor: (e) => new Date(e.createdAt).toLocaleString(), + }, + ], + 30, + (page: number, limit: number) => { + return this.connector.playerEnnoblements(page, limit); + } + ).render(); } } diff --git a/src/lib/client.ts b/src/lib/client.ts index 571bd32..520f00a 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -36,6 +36,15 @@ export type PlayerMeta = { tribe: TribeMeta | null; }; +export type VillageMeta = { + id: number; + fullName: string; + profileUrl: string; + x: number; + y: number; + continent: string; +}; + export type TribeChange = { id: number; newTribe: TribeMeta | null; @@ -43,6 +52,15 @@ export type TribeChange = { createdAt: string; }; +export type Ennoblement = { + id: number; + points: number; + newOwner: PlayerMeta | null; + oldOwner: PlayerMeta | null; + village: VillageMeta; + createdAt: string; +}; + export type ListResult = { data: T[]; total: number; @@ -54,6 +72,12 @@ export type ListTribeChangesQueryParams = { sort?: string[]; }; +export type ListEnnoblementsParams = { + offset?: number; + limit?: number; + sort?: string[]; +}; + export type TWHelpClientOptions = { timeout?: number; }; @@ -87,7 +111,7 @@ export class TWHelpClient { return resp.data.data; } - public async tribeChanges( + public async playerTribeChanges( version: string, server: string, id: number, @@ -117,4 +141,35 @@ export class TWHelpClient { total: parseInt(resp.headers[X_TOTAL_COUNT_HEADER] ?? '0'), }; } + + public async playerEnnoblements( + version: string, + server: string, + id: number, + queryParams?: ListEnnoblementsParams + ): Promise> { + const params = new URLSearchParams(); + + if (queryParams?.limit) { + params.set('limit', queryParams.limit.toString()); + } + + if (queryParams?.offset) { + params.set('offset', queryParams.offset.toString()); + } + + if (Array.isArray(queryParams?.sort)) { + queryParams?.sort.forEach((s) => { + params.append('sort', s); + }); + } + + const resp = await this.client.get( + `/api/v1/versions/${version}/servers/${server}/players/${id}/ennoblements?${params.toString()}` + ); + return { + data: resp.data.data, + total: parseInt(resp.headers[X_TOTAL_COUNT_HEADER] ?? '0'), + }; + } } -- 2.40.1 From 4b0dbabf021320c1bb4d8fc603808b342c90bf98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Wed, 18 Jan 2023 17:28:33 +0100 Subject: [PATCH 06/10] feat: extended player profile --- src/common/DialogTable.ts | 2 +- src/extendedPlayerProfile.ts | 44 ++++++++++++++++++++++++++++++++++++ src/lib/client.ts | 6 +++++ src/lib/twstats.ts | 14 ++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 src/lib/twstats.ts diff --git a/src/common/DialogTable.ts b/src/common/DialogTable.ts index a5b3b08..4a255ba 100644 --- a/src/common/DialogTable.ts +++ b/src/common/DialogTable.ts @@ -67,7 +67,7 @@ export class DialogTable { page >= maxPage ? ' disabled' : '' }>> - +
${this.columns.map((col) => ``).join('')} diff --git a/src/extendedPlayerProfile.ts b/src/extendedPlayerProfile.ts index 9da1168..8267bb0 100644 --- a/src/extendedPlayerProfile.ts +++ b/src/extendedPlayerProfile.ts @@ -8,6 +8,7 @@ import { TWHelpClient, } from './lib/client'; import { DialogTable } from './common/DialogTable'; +import { buildURL } from './lib/twstats'; const SCREEN = 'info_player'; const MODE = null; @@ -19,6 +20,7 @@ const translations: Record> = { 'Best rank': 'Najlepszy ranking', 'Most points': 'Najwięcej punktów', 'Most villages': 'Najwięcej wiosek', + 'Show other servers': 'Pokaż inne światy', 'Show tribe changes': 'Pokaż zmiany plemion', 'Show history': 'Pokaż historię', 'Show ennoblements': 'Pokaż przejęcia', @@ -31,6 +33,10 @@ const translations: Record> = { Points: 'Punkty', Barbarian: 'Barbarzyńska', Unknown: 'Nieznany', + Server: 'Serwer', + Deleted: 'Usunięty', + Yes: 'Tak', + No: 'Nie', }, }; @@ -68,6 +74,7 @@ class TWHelpConnector { } enum DialogId { + OTHER_SERVERS = 'kichiyaki_other_servers', TRIBE_CHANGES = 'kichiyaki_tribe_changes', ENNOBLEMENTS = 'kichiyaki_ennoblements', } @@ -142,6 +149,10 @@ class UI { } [ + { + name: t('Show other servers'), + handler: this.showOtherServers.bind(this), + }, { name: t('Show tribe changes'), handler: this.showTribeChanges.bind(this), @@ -166,6 +177,39 @@ class UI { }); } + private async showOtherServers(e: Event) { + e.preventDefault(); + + await new DialogTable( + DialogId.OTHER_SERVERS, + [ + { + header: t('Server'), + accessor: (s) => + `${s.key}`, + }, + { + header: t('Deleted'), + accessor: (s) => + s.deletedAt + ? `${t('Yes')} (${new Date(s.deletedAt).toLocaleString()})` + : t('No'), + }, + ], + this.player.otherServers.length, + () => { + return Promise.resolve({ + data: this.player.otherServers, + total: this.player.otherServers.length, + }); + } + ).render(); + } + private async showTribeChanges(e: Event) { e.preventDefault(); diff --git a/src/lib/client.ts b/src/lib/client.ts index 520f00a..5807e99 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -10,6 +10,11 @@ export type Version = { timezone: string; }; +export type PlayerServer = { + key: string; + deletedAt: string | null; +}; + export type Player = { id: number; bestRank: number; @@ -20,6 +25,7 @@ export type Player = { mostVillagesAt: string; lastActivityAt: string; createdAt: string; + otherServers: PlayerServer[]; }; export type TribeMeta = { diff --git a/src/lib/twstats.ts b/src/lib/twstats.ts new file mode 100644 index 0000000..998c437 --- /dev/null +++ b/src/lib/twstats.ts @@ -0,0 +1,14 @@ +export type BuildURLParams = { + entity: 'player'; + id: number; + server: string; +}; + +export const buildURL = (params: BuildURLParams) => { + switch (params.entity) { + case 'player': + return `https://www.twstats.com/in/${params.server}/player/${params.id}`; + default: + throw new Error(`Unknown entity: ${params.entity}`); + } +}; -- 2.40.1 From 89f71f319dbaf97dc7a8a79b821bc291fc61595d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Thu, 19 Jan 2023 13:57:11 +0100 Subject: [PATCH 07/10] feat: extended player profile --- src/common/DialogTable.ts | 8 +-- src/extendedPlayerProfile.ts | 91 +++++++++++++++++++++++++------- src/lib/{client.ts => twhelp.ts} | 54 +++++++++++++++++++ 3 files changed, 129 insertions(+), 24 deletions(-) rename src/lib/{client.ts => twhelp.ts} (75%) diff --git a/src/common/DialogTable.ts b/src/common/DialogTable.ts index 4a255ba..9d79527 100644 --- a/src/common/DialogTable.ts +++ b/src/common/DialogTable.ts @@ -1,4 +1,4 @@ -import { ListResult } from '../lib/client'; +import { ListResult } from '../lib/twhelp'; const translations: Record> = { pl_PL: { @@ -14,7 +14,7 @@ const t = (s: string) => { export type DialogTableColumn = { header: string; - accessor: (row: T) => string; + accessor: (row: T, index: number, rows: T[]) => string; }; export class DialogTable { @@ -74,10 +74,10 @@ export class DialogTable { ${data .map( - (r) => ` + (r, index) => ` ${this.columns - .map((col) => ``) + .map((col) => ``) .join('')} ` diff --git a/src/extendedPlayerProfile.ts b/src/extendedPlayerProfile.ts index 8267bb0..7b4f29a 100644 --- a/src/extendedPlayerProfile.ts +++ b/src/extendedPlayerProfile.ts @@ -1,12 +1,6 @@ // Extended player profile -import { - Ennoblement, - ListResult, - Player, - TribeChange, - TWHelpClient, -} from './lib/client'; +import { Player, TWHelpClient } from './lib/twhelp'; import { DialogTable } from './common/DialogTable'; import { buildURL } from './lib/twstats'; @@ -37,6 +31,13 @@ const translations: Record> = { Deleted: 'Usunięty', Yes: 'Tak', No: 'Nie', + Date: 'Data', + Tribe: 'Plemię', + Villages: 'Wioski', + OD: 'Pokonani ogólny', + ODA: 'Pokonani agresor', + ODD: 'Pokonani obrońca', + ODS: 'Pokonani wspierający', }, }; @@ -71,29 +72,27 @@ class TWHelpConnector { sort: ['createdAt:desc'], }); } + + playerHistory(page: number, limit: number) { + return this.client.playerHistory(this.version, this.server, this.id, { + offset: (page - 1) * limit, + limit: limit, + sort: ['date:desc'], + }); + } } enum DialogId { OTHER_SERVERS = 'kichiyaki_other_servers', TRIBE_CHANGES = 'kichiyaki_tribe_changes', ENNOBLEMENTS = 'kichiyaki_ennoblements', -} - -interface UIConnector { - playerTribeChanges( - page: number, - limit: number - ): Promise>; - playerEnnoblements( - page: number, - limit: number - ): Promise>; + HISTORY = 'kichiyaki_history', } class UI { constructor( private readonly player: Player, - private readonly connector: UIConnector + private readonly connector: TWHelpConnector ) {} public render() { @@ -242,8 +241,60 @@ class UI { ).render(); } - private showHistory(e: Event) { + 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.connector.playerHistory(page, limit); + } + ).render(); } private async showEnnoblements(e: Event) { diff --git a/src/lib/client.ts b/src/lib/twhelp.ts similarity index 75% rename from src/lib/client.ts rename to src/lib/twhelp.ts index 5807e99..3cad5b7 100644 --- a/src/lib/client.ts +++ b/src/lib/twhelp.ts @@ -67,6 +67,23 @@ export type Ennoblement = { createdAt: string; }; +export type PlayerSnapshot = { + id: number; + tribe: TribeMeta | null; + points: number; + numVillages: number; + rank: number; + rankAtt: number; + rankDef: number; + rankSup: number; + rankTotal: number; + scoreAtt: number; + scoreDef: number; + scoreSup: number; + scoreTotal: number; + date: string; +}; + export type ListResult = { data: T[]; total: number; @@ -84,6 +101,12 @@ export type ListEnnoblementsParams = { sort?: string[]; }; +export type ListPlayerSnapshotsParams = { + offset?: number; + limit?: number; + sort?: string[]; +}; + export type TWHelpClientOptions = { timeout?: number; }; @@ -178,4 +201,35 @@ export class TWHelpClient { total: parseInt(resp.headers[X_TOTAL_COUNT_HEADER] ?? '0'), }; } + + public async playerHistory( + version: string, + server: string, + id: number, + queryParams?: ListPlayerSnapshotsParams + ): Promise> { + const params = new URLSearchParams(); + + if (queryParams?.limit) { + params.set('limit', queryParams.limit.toString()); + } + + if (queryParams?.offset) { + params.set('offset', queryParams.offset.toString()); + } + + if (Array.isArray(queryParams?.sort)) { + queryParams?.sort.forEach((s) => { + params.append('sort', s); + }); + } + + const resp = await this.client.get( + `/api/v1/versions/${version}/servers/${server}/players/${id}/history?${params.toString()}` + ); + return { + data: resp.data.data, + total: parseInt(resp.headers[X_TOTAL_COUNT_HEADER] ?? '0'), + }; + } } -- 2.40.1 From 0026822768ae1dfd891ec65e17139929f44efc6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Sun, 22 Jan 2023 11:43:50 +0100 Subject: [PATCH 08/10] feat: extended player profile --- src/common/DialogTable.ts | 78 +++++++++++++++++++++++----------- src/extendedPlayerProfile.ts | 82 +++++++++++------------------------- src/global.d.ts | 1 + src/lib/twhelp.ts | 8 +--- 4 files changed, 80 insertions(+), 89 deletions(-) diff --git a/src/common/DialogTable.ts b/src/common/DialogTable.ts index 9d79527..acc069b 100644 --- a/src/common/DialogTable.ts +++ b/src/common/DialogTable.ts @@ -5,6 +5,8 @@ const translations: Record> = { Loading: 'Wczytywanie', 'Previous page': 'Poprzednia strona', 'Next page': 'Następna strona', + 'Something went wrong while loading the data': + 'Coś poszło nie tak podczas wczytywania danych', }, }; @@ -18,6 +20,9 @@ export type DialogTableColumn = { }; export class DialogTable { + prevPageId: string; + selectId: string; + nextPageId: string; constructor( private readonly id: string, private readonly columns: DialogTableColumn[], @@ -26,7 +31,11 @@ export class DialogTable { page: number, limit: number ) => Promise> - ) {} + ) { + this.prevPageId = `${this.id}_page_prev`; + this.selectId = `${this.id}_page_select`; + this.nextPageId = `${this.id}_page_next`; + } public async render() { await this.renderPage(1); @@ -35,9 +44,28 @@ export class DialogTable { private async renderPage(page: number) { window.Dialog.show(`${this.id}_loading`, `

${t('Loading')}...

`); - const { data, total } = await this.loadData(page, this.limit); + try { + const { data, total } = await this.loadData(page, this.limit); + window.Dialog.show( + this.id, + ` + ${this.buildPagination(page, total)} + ${this.buildTable(data)} + ` + ); + + this.addEventListeners(page); + } catch (err) { + console.error(err); + window.Dialog.close(); + window.UI.ErrorMessage(t('Something went wrong while loading the data')); + } + } + + private buildPagination(page: number, total: number): string { const maxPage = Math.ceil(total / this.limit); + const pageOpts = []; for (let i = 1; i <= (page > maxPage ? page : maxPage); i++) { pageOpts.push( @@ -47,26 +75,25 @@ export class DialogTable { ); } - const prevPageId = `${this.id}_page_prev`; - const selectId = `${this.id}_page_select`; - const nextPageId = `${this.id}_page_next`; - - window.Dialog.show( - this.id, - ` + return `
- ${pageOpts.join('')} - -
+ + + `; + } + + private buildTable(data: T[]) { + return `
${col.header}
${col.accessor(r)}${col.accessor(r, index, data)}
@@ -84,18 +111,19 @@ export class DialogTable { ) .join('')} -
- ` - ); + + `; + } + private addEventListeners(page: number) { document - .querySelector('#' + prevPageId) - ?.addEventListener('click', (e: Event) => { + .querySelector('#' + this.prevPageId) + ?.addEventListener('click', () => { this.renderPage(page - 1); }); document - .querySelector('#' + selectId) + .querySelector('#' + this.selectId) ?.addEventListener('change', (e: Event) => { if (!(e.currentTarget instanceof HTMLSelectElement)) { return; @@ -105,8 +133,8 @@ export class DialogTable { }); document - .querySelector('#' + nextPageId) - ?.addEventListener('click', (e: Event) => { + .querySelector('#' + this.nextPageId) + ?.addEventListener('click', () => { this.renderPage(page + 1); }); } diff --git a/src/extendedPlayerProfile.ts b/src/extendedPlayerProfile.ts index 7b4f29a..31af4be 100644 --- a/src/extendedPlayerProfile.ts +++ b/src/extendedPlayerProfile.ts @@ -2,7 +2,6 @@ import { Player, TWHelpClient } from './lib/twhelp'; import { DialogTable } from './common/DialogTable'; -import { buildURL } from './lib/twstats'; const SCREEN = 'info_player'; const MODE = null; @@ -14,7 +13,6 @@ const translations: Record> = { 'Best rank': 'Najlepszy ranking', 'Most points': 'Najwięcej punktów', 'Most villages': 'Najwięcej wiosek', - 'Show other servers': 'Pokaż inne światy', 'Show tribe changes': 'Pokaż zmiany plemion', 'Show history': 'Pokaż historię', 'Show ennoblements': 'Pokaż przejęcia', @@ -83,7 +81,6 @@ class TWHelpConnector { } enum DialogId { - OTHER_SERVERS = 'kichiyaki_other_servers', TRIBE_CHANGES = 'kichiyaki_tribe_changes', ENNOBLEMENTS = 'kichiyaki_ennoblements', HISTORY = 'kichiyaki_history', @@ -148,10 +145,6 @@ class UI { } [ - { - name: t('Show other servers'), - handler: this.showOtherServers.bind(this), - }, { name: t('Show tribe changes'), handler: this.showTribeChanges.bind(this), @@ -176,39 +169,6 @@ class UI { }); } - private async showOtherServers(e: Event) { - e.preventDefault(); - - await new DialogTable( - DialogId.OTHER_SERVERS, - [ - { - header: t('Server'), - accessor: (s) => - `${s.key}`, - }, - { - header: t('Deleted'), - accessor: (s) => - s.deletedAt - ? `${t('Yes')} (${new Date(s.deletedAt).toLocaleString()})` - : t('No'), - }, - ], - this.player.otherServers.length, - () => { - return Promise.resolve({ - data: this.player.otherServers, - total: this.player.otherServers.length, - }); - } - ).render(); - } - private async showTribeChanges(e: Event) { e.preventDefault(); @@ -314,25 +274,29 @@ class UI { }, { header: t('Old owner'), - accessor: (e) => - e.oldOwner - ? `${e.oldOwner.name}${ - e.oldOwner.tribe - ? ` (${e.oldOwner.tribe.tag})` - : '' - }` - : t('Barbarian'), + accessor: ({ village: { player } }) => { + if (!player) { + return t('Barbarian'); + } + return `${player.name}${ + player.tribe + ? ` (${player.tribe.tag})` + : '' + }`; + }, }, { header: t('New owner'), - accessor: (e) => - e.newOwner - ? `${e.newOwner.name}${ - e.newOwner.tribe - ? ` (${e.newOwner.tribe.tag})` - : '' - }` - : t('Unknown'), + accessor: ({ newOwner }) => { + if (!newOwner) { + return t('Unknown'); + } + return `${newOwner.name}${ + newOwner.tribe + ? ` (${newOwner.tribe.tag})` + : '' + }`; + }, }, { header: t('Date/time'), @@ -379,5 +343,9 @@ class ExtendedPlayerProfile { await new ExtendedPlayerProfile( new TWHelpClient(process.env.TWHELP_API_BASE_URL ?? '') - ).render(); + ) + .render() + .catch((err) => { + console.log(err); + }); })(); diff --git a/src/global.d.ts b/src/global.d.ts index 071b6f0..bb37dbf 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -30,6 +30,7 @@ declare global { }; Dialog: { show: (id: string, html: string) => void; + close: () => void; }; } } diff --git a/src/lib/twhelp.ts b/src/lib/twhelp.ts index 3cad5b7..43fe05e 100644 --- a/src/lib/twhelp.ts +++ b/src/lib/twhelp.ts @@ -10,11 +10,6 @@ export type Version = { timezone: string; }; -export type PlayerServer = { - key: string; - deletedAt: string | null; -}; - export type Player = { id: number; bestRank: number; @@ -25,7 +20,6 @@ export type Player = { mostVillagesAt: string; lastActivityAt: string; createdAt: string; - otherServers: PlayerServer[]; }; export type TribeMeta = { @@ -49,6 +43,7 @@ export type VillageMeta = { x: number; y: number; continent: string; + player: PlayerMeta | null; }; export type TribeChange = { @@ -62,7 +57,6 @@ export type Ennoblement = { id: number; points: number; newOwner: PlayerMeta | null; - oldOwner: PlayerMeta | null; village: VillageMeta; createdAt: string; }; -- 2.40.1 From 66136ee097556c94941782944a474fd450857a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Tue, 24 Jan 2023 07:57:24 +0100 Subject: [PATCH 09/10] feat: extended player profile --- package.json | 4 +- src/extendedPlayerProfile.ts | 119 ++++++++++++++-- src/global.d.ts | 1 + src/lib/tw.ts | 256 +++++++++++++++++++++++++++++++++++ src/lib/twstats.ts | 14 -- src/utils/index.ts | 1 + src/utils/wait.ts | 2 + tsconfig.json | 3 +- yarn.lock | 10 ++ 9 files changed, 383 insertions(+), 27 deletions(-) create mode 100644 src/lib/tw.ts delete mode 100644 src/lib/twstats.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/wait.ts diff --git a/package.json b/package.json index 6f0c741..3e2a4b3 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ ], "devDependencies": { "@types/jquery": "^3.5.14", + "@types/lodash": "^4.14.191", "@types/node": "^18.11.17", "buffer": "^5.5.0", "npm-run-all": "^4.1.5", @@ -26,6 +27,7 @@ }, "dependencies": { "axios": "^1.2.2", - "date-fns": "^2.29.3" + "date-fns": "^2.29.3", + "lodash": "^4.17.21" } } diff --git a/src/extendedPlayerProfile.ts b/src/extendedPlayerProfile.ts index 31af4be..9776965 100644 --- a/src/extendedPlayerProfile.ts +++ b/src/extendedPlayerProfile.ts @@ -2,6 +2,7 @@ import { Player, TWHelpClient } from './lib/twhelp'; import { DialogTable } from './common/DialogTable'; +import { InADayClient, InADayPlayerScore } from './lib/tw'; const SCREEN = 'info_player'; const MODE = null; @@ -13,6 +14,7 @@ const translations: Record> = { 'Best rank': 'Najlepszy ranking', 'Most points': 'Najwięcej punktów', 'Most villages': 'Najwięcej wiosek', + 'Show in a day ranks': 'Pokaż dzienne rankingi', 'Show tribe changes': 'Pokaż zmiany plemion', 'Show history': 'Pokaż historię', 'Show ennoblements': 'Pokaż przejęcia', @@ -36,6 +38,15 @@ const translations: Record> = { ODA: 'Pokonani agresor', ODD: 'Pokonani obrońca', ODS: 'Pokonani wspierający', + 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', }, }; @@ -80,7 +91,19 @@ class TWHelpConnector { } } +class InADayConnector { + constructor( + private readonly client: InADayClient, + private readonly name: string + ) {} + + player() { + return this.client.player(this.name); + } +} + enum DialogId { + IN_A_DAY_RANKS = 'kichiyaki_in_a_day_ranks', TRIBE_CHANGES = 'kichiyaki_tribe_changes', ENNOBLEMENTS = 'kichiyaki_ennoblements', HISTORY = 'kichiyaki_history', @@ -89,7 +112,8 @@ enum DialogId { class UI { constructor( private readonly player: Player, - private readonly connector: TWHelpConnector + private readonly twhelpConnector: TWHelpConnector, + private readonly inADayConnector: InADayConnector ) {} public render() { @@ -145,6 +169,10 @@ class UI { } [ + { + name: t('Show in a day ranks'), + handler: this.showInADayRanks.bind(this), + }, { name: t('Show tribe changes'), handler: this.showTribeChanges.bind(this), @@ -169,6 +197,60 @@ class UI { }); } + 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('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(); @@ -196,7 +278,7 @@ class UI { ], 30, (page: number, limit: number) => { - return this.connector.playerTribeChanges(page, limit); + return this.twhelpConnector.playerTribeChanges(page, limit); } ).render(); } @@ -252,7 +334,7 @@ class UI { ], 30, (page: number, limit: number) => { - return this.connector.playerHistory(page, limit); + return this.twhelpConnector.playerHistory(page, limit); } ).render(); } @@ -305,26 +387,32 @@ class UI { ], 30, (page: number, limit: number) => { - return this.connector.playerEnnoblements(page, limit); + return this.twhelpConnector.playerEnnoblements(page, limit); } ).render(); } } class ExtendedPlayerProfile { - connector: TWHelpConnector; - constructor(client: TWHelpClient) { - this.connector = new TWHelpConnector( - client, + 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 render() { - const player = await this.connector.player(); - new UI(player, this.connector).render(); + const player = await this.twhelpConnector.player(); + new UI(player, this.twhelpConnector, this.inADayConnector).render(); } private getPlayerId() { @@ -334,6 +422,14 @@ class ExtendedPlayerProfile { } 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 () => { @@ -342,7 +438,8 @@ class ExtendedPlayerProfile { } await new ExtendedPlayerProfile( - new TWHelpClient(process.env.TWHELP_API_BASE_URL ?? '') + new TWHelpClient(process.env.TWHELP_API_BASE_URL ?? ''), + new InADayClient() ) .render() .catch((err) => { diff --git a/src/global.d.ts b/src/global.d.ts index bb37dbf..813f2ab 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -6,6 +6,7 @@ declare global { group_id: number; player: { id: number; + name: string; }; village: { id: number; diff --git a/src/lib/tw.ts b/src/lib/tw.ts new file mode 100644 index 0000000..6eceedd --- /dev/null +++ b/src/lib/tw.ts @@ -0,0 +1,256 @@ +import axios, { AxiosInstance } from 'axios'; +import random from 'lodash/random'; +import { wait } from '../utils'; + +const DEFAULT_TIMEOUT = 10000; + +type InADayScore = { + player: { + id: number; + name: string; + profileUrl: string; + }; + tribe: { + id: number; + tag: string; + profileUrl: string; + } | null; + rank: number; + score: number; + date: string; +}; + +class InADayParser { + private readonly doc: Document; + constructor(html: string) { + this.doc = new DOMParser().parseFromString(html, 'text/html'); + } + + parse() { + const results: InADayScore[] = []; + + for (const row of this.doc.querySelectorAll( + '#in_a_day_ranking_table tbody tr' + )) { + if (!(row instanceof HTMLTableRowElement)) { + continue; + } + + const res = this.parseRow(row); + if (!res) { + continue; + } + + results.push(res); + } + + return results; + } + + private parseRow(row: HTMLTableRowElement): InADayScore | null { + const cells = [...row.children].filter( + (cell): cell is HTMLTableCellElement => + cell instanceof HTMLTableCellElement + ); + if (cells.length !== 5) { + return null; + } + + const playerProfileUrl = cells[1].querySelector('a')?.href; + if (!playerProfileUrl) { + return null; + } + + const tribeProfileUrl = cells[2].querySelector('a')?.href; + + return { + player: { + id: this.getIdFromUrl(playerProfileUrl), + name: cells[1].innerText.trim(), + profileUrl: playerProfileUrl, + }, + ...(tribeProfileUrl + ? { + tribe: { + id: this.getIdFromUrl(tribeProfileUrl), + tag: cells[2].innerText.trim(), + profileUrl: tribeProfileUrl, + }, + } + : { + tribe: null, + }), + rank: parseInt(cells[0].innerText.trim()), + score: this.parseScore(cells[3].innerText.trim()), + date: cells[4].innerText.trim(), + }; + } + + private getIdFromUrl(u: string) { + return parseInt(new URL(u).searchParams.get('id') ?? ''); + } + + private parseScore(s: string): number { + return parseInt(s.trim().replace(/\./g, '')); + } +} + +export type InADayClientOptions = { + timeout?: number; +}; + +type InADayParams = { + name?: string; + page?: number; +}; + +export type InADayPlayerScore = { + score: number; + date: string; +}; + +export type InADayPlayer = { + id: number; + name: string; + profileUrl: string; + tribe: { + id: number; + tag: string; + profileUrl: string; + } | null; + scores: { + killAtt: InADayPlayerScore | null; + killDef: InADayPlayerScore | null; + killSup: InADayPlayerScore | null; + lootRes: InADayPlayerScore | null; + lootVil: InADayPlayerScore | null; + scavenge: InADayPlayerScore | null; + conquer: InADayPlayerScore | null; + }; +}; + +export class InADayClient { + client: AxiosInstance; + constructor(opts?: InADayClientOptions) { + this.client = axios.create({ + timeout: opts?.timeout ?? DEFAULT_TIMEOUT, + }); + } + + async player(name: string): Promise { + const params: InADayParams = { + name, + }; + + const urlsAndKeys: { + url: string; + key: keyof InADayPlayer['scores']; + }[] = [ + { + url: this.buildUrl('kill_att', params), + key: 'killAtt', + }, + { + url: this.buildUrl('kill_def', params), + key: 'killDef', + }, + { + url: this.buildUrl('kill_sup', params), + key: 'killSup', + }, + { + url: this.buildUrl('loot_res', params), + key: 'lootRes', + }, + { + url: this.buildUrl('loot_vil', params), + key: 'lootVil', + }, + { + url: this.buildUrl('scavenge', params), + key: 'scavenge', + }, + { + url: this.buildUrl('conquer', params), + key: 'conquer', + }, + ]; + const scores: { + key: keyof InADayPlayer['scores']; + score: InADayScore; + }[] = []; + for (const { url, key } of urlsAndKeys) { + const resp = await this.client.get(url); + + const score = new InADayParser(resp.data).parse()[0]; + if (score && score.player.name === name) { + scores.push({ + key, + score, + }); + } + + await wait(random(200, 400)); + } + + if (scores.length === 0) { + return null; + } + + const player: InADayPlayer = { + id: 0, + name: '', + profileUrl: '', + tribe: null, + scores: { + conquer: null, + killAtt: null, + killDef: null, + killSup: null, + lootRes: null, + lootVil: null, + scavenge: null, + }, + }; + scores.forEach(({ score, key }) => { + if (!player.id) { + player.id = score.player.id; + } + if (!player.name) { + player.name = score.player.name; + } + if (!player.profileUrl) { + player.profileUrl = score.player.profileUrl; + } + if (!player.tribe && score.tribe) { + player.tribe = score.tribe; + } + + player.scores[key] = { + score: score.score, + date: score.date, + }; + }); + + return player; + } + + private buildUrl(type: string, params?: InADayParams) { + return window.TribalWars.buildURL('GET', 'ranking', { + mode: 'in_a_day', + type, + ...(params?.name + ? { + name: params.name, + } + : {}), + ...(params?.page + ? { + offset: ((params.page - 1) * 25).toString(), + } + : { + offset: '0', + }), + }); + } +} diff --git a/src/lib/twstats.ts b/src/lib/twstats.ts deleted file mode 100644 index 998c437..0000000 --- a/src/lib/twstats.ts +++ /dev/null @@ -1,14 +0,0 @@ -export type BuildURLParams = { - entity: 'player'; - id: number; - server: string; -}; - -export const buildURL = (params: BuildURLParams) => { - switch (params.entity) { - case 'player': - return `https://www.twstats.com/in/${params.server}/player/${params.id}`; - default: - throw new Error(`Unknown entity: ${params.entity}`); - } -}; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..8c5f7e7 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1 @@ +export * from './wait'; diff --git a/src/utils/wait.ts b/src/utils/wait.ts new file mode 100644 index 0000000..c7a6d15 --- /dev/null +++ b/src/utils/wait.ts @@ -0,0 +1,2 @@ +export const wait = (timeout: number) => + new Promise((resolve) => setTimeout(resolve, timeout)); diff --git a/tsconfig.json b/tsconfig.json index e1339e0..7178200 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "strictNullChecks": true, "strictFunctionTypes": true, "skipLibCheck": true, - "noImplicitAny": true + "noImplicitAny": true, + "allowSyntheticDefaultImports": true } } diff --git a/yarn.lock b/yarn.lock index 3a94b3d..f07a8c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -722,6 +722,11 @@ dependencies: "@types/sizzle" "*" +"@types/lodash@^4.14.191": + version "4.14.191" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" + integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== + "@types/node@^18.11.17": version "18.11.17" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.17.tgz#5c009e1d9c38f4a2a9d45c0b0c493fe6cdb4bcb5" @@ -1475,6 +1480,11 @@ load-json-file@^4.0.0: pify "^3.0.0" strip-bom "^3.0.0" +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + mdn-data@2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" -- 2.40.1 From 0c9539dca9aa8b4eeeadca5eaf830bfd86a7776c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Thu, 26 Jan 2023 07:28:54 +0100 Subject: [PATCH 10/10] feat: extended player profile --- .terserrc.js | 6 +- package.json | 8 +- src/common/DialogTable.ts | 17 ++-- src/extendedPlayerProfile.ts | 152 +++++++++++++++++++++++++++++++---- src/lib/tw.ts | 12 ++- src/lib/twhelp.ts | 19 +++-- src/utils/i18n.ts | 7 ++ src/utils/index.ts | 1 + 8 files changed, 179 insertions(+), 43 deletions(-) create mode 100644 src/utils/i18n.ts 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'; -- 2.40.1