scripts/src/extended-map-popup.user.ts

284 lines
7.1 KiB
TypeScript

import addSeconds from 'date-fns/addSeconds';
import {
Ennoblement,
ServerConfig,
TWHelpClient,
UnitInfo,
} from './lib/twhelp';
import { Cache, InMemoryStorage } from './lib/cache';
import { calcDistance, calcLoyalty } from './lib/tw';
import { createTranslationFunc } from './utils';
declare global {
interface Window {
TWMap: {
popup: {
loadVillage: (villageId: string) => void;
_loadVillage: (villageId: string) => void;
displayForVillage: (
village: { id: string },
x: number,
y: number
) => void;
_displayForVillage: (
village: { id: string },
x: number,
y: number
) => void;
};
};
}
}
const t = createTranslationFunc({
pl_PL: {
'Ennobled at': 'Ostatnio przejęta',
Loyalty: 'Poparcie',
'Can send a nobleman': 'Można wysłać szlachica',
Yes: 'Tak',
No: 'Nie',
Never: 'Nigdy',
},
});
class TWHelpConnector {
private static readonly SERVER_CONFIG_CACHE_KEY =
'extended_map_popup_server_config';
private static readonly UNIT_INFO_CACHE_KEY = 'extended_map_popup_unit_info';
private static readonly VILLAGE_CACHE_KEY_PREFIX =
'extended_map_popup_village_';
private readonly localStorageCache = new Cache(localStorage);
private readonly inMemoryCache = new Cache(new InMemoryStorage());
constructor(
private readonly client: TWHelpClient,
private readonly version: string,
private readonly server: string
) {}
serverConfig() {
return this.localStorageCache.load(
TWHelpConnector.SERVER_CONFIG_CACHE_KEY,
3600,
() => {
return this.client.serverConfig(this.version, this.server);
}
);
}
unitInfo() {
return this.localStorageCache.load(
TWHelpConnector.UNIT_INFO_CACHE_KEY,
3600,
() => {
return this.client.unitInfo(this.version, this.server);
}
);
}
async latestEnnoblement(id: number, cacheOnly?: boolean) {
const key = TWHelpConnector.VILLAGE_CACHE_KEY_PREFIX + id.toString();
if (cacheOnly) {
return this.inMemoryCache.get<Ennoblement | null>(key);
}
return this.inMemoryCache.load(key, 86400, async () => {
const ennoblements = await this.client.villageEnnoblements(
this.version,
this.server,
id,
{
limit: 1,
sort: ['createdAt:DESC'],
}
);
return ennoblements.data.length > 0 ? ennoblements.data[0] : null;
});
}
}
class Popup {
private static readonly ID_ENNOBLED_AT = 'extended_map_popup_ennobled_at';
private static readonly ID_LOYALTY = 'extended_map_popup_loyalty';
constructor(
private readonly config: ServerConfig,
private readonly unitInfo: UnitInfo,
private readonly currentVillage: Window['game_data']['village'],
private readonly connector: TWHelpConnector
) {}
addHandlers() {
window.TWMap.popup._loadVillage = window.TWMap.popup.loadVillage;
window.TWMap.popup.loadVillage = this.loadVillage.bind(this);
window.TWMap.popup._displayForVillage =
window.TWMap.popup.displayForVillage;
window.TWMap.popup.displayForVillage = this.display.bind(this);
}
private async loadVillage(villageId: string) {
window.TWMap.popup._loadVillage(villageId);
await this.displayLatestEnnoblementAndLoyalty(parseInt(villageId), false);
}
private async display(village: { id: string }, x: number, y: number) {
window.TWMap.popup._displayForVillage(village, x, y);
this.displayArrivalTimes(x, y);
this.displayCanSendNobleman(x, y);
await this.displayLatestEnnoblementAndLoyalty(
parseInt(village.id),
window.game_data.features.Premium.active
);
}
private displayArrivalTimes(x: number, y: number) {
const dist = calcDistance({ x, y }, this.currentVillage);
if (dist <= 0) {
return;
}
const imgs = document.querySelectorAll(
'#map_popup #info_content tbody img[src*="unit/unit_"]'
);
if (imgs.length === 0) {
return;
}
const tbody = imgs[0].closest('tbody');
if (!(tbody instanceof HTMLTableSectionElement)) {
return;
}
const tr = document.createElement('tr');
tr.classList.add('center');
imgs.forEach((img, idx) => {
if (!(img instanceof HTMLImageElement)) {
return;
}
for (const [unit, config] of Object.entries(this.unitInfo)) {
if (!img.src.includes(unit)) {
continue;
}
const td = document.createElement('td');
td.style.padding = '2px';
td.style.backgroundColor = idx % 2 === 0 ? '#F8F4E8' : '#DED3B9';
td.style.maxWidth = '70px';
td.innerText = addSeconds(
window.Timing.getCurrentServerTime(),
Math.round(dist * config.speed * 60)
).toLocaleString();
tr.appendChild(td);
break;
}
});
tbody.appendChild(tr);
}
private displayCanSendNobleman(x: number, y: number) {
const dist = calcDistance({ x, y }, this.currentVillage);
if (dist <= 0) {
return;
}
const tbody = document.querySelector('#map_popup #info_content tbody');
if (!(tbody instanceof HTMLTableSectionElement)) {
return;
}
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${t('Can send a nobleman')}:</td>
<td>${dist <= this.config.snob.maxDist ? t('Yes') : t('No')}</td>
`;
tbody.appendChild(tr);
}
private async displayLatestEnnoblementAndLoyalty(
id: number,
cacheOnly: boolean
) {
const ennoblement = await this.connector.latestEnnoblement(id, cacheOnly);
const tbody = document.querySelector('#map_popup #info_content tbody');
if (!(tbody instanceof HTMLTableSectionElement)) {
return;
}
[
{
id: Popup.ID_ENNOBLED_AT,
title: t('Ennobled at'),
value: ennoblement
? new Date(ennoblement.createdAt).toLocaleString()
: t('Never'),
},
{
id: Popup.ID_LOYALTY,
title: t('Loyalty'),
value: calcLoyalty(ennoblement?.createdAt ?? 0, this.config.speed),
},
].forEach(({ id, title, value }) => {
let tr = tbody.querySelector('#' + id);
if (!tr) {
tr = document.createElement('tr');
tr.id = id;
tbody.appendChild(tr);
}
tr.innerHTML = `
<td>${title}:</td>
<td>${value}</td>
`;
});
}
}
class ExtendedMapPopup {
connector: TWHelpConnector;
constructor(client: TWHelpClient) {
this.connector = new TWHelpConnector(
client,
window.game_data.market,
window.game_data.world
);
}
async run() {
const [config, unitInfo] = await Promise.all([
this.connector.serverConfig(),
this.connector.unitInfo(),
]);
new Popup(
config,
unitInfo,
window.game_data.village,
this.connector
).addHandlers();
}
}
(async () => {
if (window.game_data.screen !== 'map' || window.game_data.mode !== null) {
return;
}
await new ExtendedMapPopup(
new TWHelpClient(process.env.TWHELP_API_BASE_URL ?? '')
)
.run()
.catch((err) => {
console.log(err);
});
})();