feat: extended player profile

This commit is contained in:
Dawid Wysokiński 2023-01-26 07:28:54 +01:00
parent 66136ee097
commit 0c9539dca9
Signed by: Kichiyaki
GPG Key ID: B5445E357FB8B892
8 changed files with 179 additions and 43 deletions

View File

@ -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

View File

@ -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": {

View File

@ -1,6 +1,6 @@
import { ListResult } from '../lib/twhelp';
import { createTranslationFunc } from '../utils';
const translations: Record<string, Record<string, string>> = {
const t = createTranslationFunc({
pl_PL: {
Loading: 'Wczytywanie',
'Previous page': 'Poprzednia strona',
@ -8,17 +8,18 @@ const translations: Record<string, Record<string, string>> = {
'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<T> = {
header: string;
accessor: (row: T, index: number, rows: T[]) => string;
};
export type LoadDataResult<T> = {
data: T[];
total: number;
};
export class DialogTable<T> {
prevPageId: string;
selectId: string;
@ -30,7 +31,7 @@ export class DialogTable<T> {
private readonly loadData: (
page: number,
limit: number
) => Promise<ListResult<T>>
) => Promise<LoadDataResult<T>>
) {
this.prevPageId = `${this.id}_page_prev`;
this.selectId = `${this.id}_page_select`;

View File

@ -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<string, Record<string, string>> = {
const t = createTranslationFunc({
pl_PL: {
'Joined at': 'Dołączył o',
'Last activity at': 'Ostatnio aktywny o',
@ -24,6 +22,7 @@ const translations: Record<string, Record<string, string>> = {
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<string, Record<string, string>> = {
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<string, Record<string, string>> = {
'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',
`
<table style="width: 100%" class="vis">
<tbody>
<tr>
<th colspan="2">
${t('Changes')}
</th>
</tr>
${rows
.map(
(r) => `
<tr>
<td>${r.header}</td>
<td style="color: #000; background-color: ${this.getStatBgColor(
r.value,
r.rank
)}">${Math.abs(r.value).toLocaleString()}</td>
</tr>
`
)
.join('')}
</tbody>
</table>
`
);
}
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;
}

View File

@ -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) {

View File

@ -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,
});
}

7
src/utils/i18n.ts Normal file
View File

@ -0,0 +1,7 @@
export const createTranslationFunc = (
translations: Record<string, Record<string, string>>
) => {
return (s: string) => {
return translations[window.game_data.locale]?.[s] ?? s;
};
};

View File

@ -1 +1,2 @@
export * from './i18n';
export * from './wait';