feat: extended player profile
This commit is contained in:
parent
0026822768
commit
66136ee097
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<string, Record<string, string>> = {
|
|||
'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<string, Record<string, string>> = {
|
|||
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<string, string> = {
|
||||
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) => {
|
||||
|
|
|
@ -6,6 +6,7 @@ declare global {
|
|||
group_id: number;
|
||||
player: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
village: {
|
||||
id: number;
|
||||
|
|
|
@ -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<InADayPlayer | null> {
|
||||
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',
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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}`);
|
||||
}
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './wait';
|
|
@ -0,0 +1,2 @@
|
|||
export const wait = (timeout: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, timeout));
|
|
@ -6,6 +6,7 @@
|
|||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"skipLibCheck": true,
|
||||
"noImplicitAny": true
|
||||
"noImplicitAny": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
}
|
||||
}
|
||||
|
|
10
yarn.lock
10
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"
|
||||
|
|
Loading…
Reference in New Issue