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(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 = ` ${t('Can send a nobleman')}: ${dist <= this.config.snob.maxDist ? t('Yes') : t('No')} `; 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 = ` ${title}: ${value} `; }); } } 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); }); })();