From f005072d9c719517d673c44b621e60f92a34b56c Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Sat, 25 Apr 2020 12:01:07 +0300 Subject: [PATCH] Store incoming merchants --- src/ControlPanel.ts | 19 ++++++----- src/Core/Buildings.ts | 6 ++++ src/Core/Market.ts | 10 ++++++ src/DashboardView/VillageStateList.vue | 43 +++++++++++++++++++----- src/DataStorage.ts | 46 ++++++++++++++++++++++++++ src/Grabber/GrabberManager.ts | 2 ++ src/Grabber/HeroPageGrabber.ts | 15 ++------- src/Grabber/MarketPageGrabber.ts | 17 ++++++++++ src/Page/BuildingPage.ts | 16 +++++++++ src/Page/BuildingPageController.ts | 21 ++++-------- src/Page/PageDetectors.ts | 42 +++++++++++++++++++++++ src/State/VillageState.ts | 35 ++++++++++++++++---- src/Task/GrabVillageState.ts | 7 ++++ 13 files changed, 227 insertions(+), 52 deletions(-) create mode 100644 src/Core/Buildings.ts create mode 100644 src/Core/Market.ts create mode 100644 src/Grabber/MarketPageGrabber.ts create mode 100644 src/Page/PageDetectors.ts diff --git a/src/ControlPanel.ts b/src/ControlPanel.ts index eb72922..8ed02fe 100644 --- a/src/ControlPanel.ts +++ b/src/ControlPanel.ts @@ -1,4 +1,4 @@ -import { elClassId, getNumber, parseLocation, uniqId, waitForLoad } from './utils'; +import { parseLocation, uniqId, waitForLoad } from './utils'; import { Scheduler } from './Scheduler'; import { BuildingPageController } from './Page/BuildingPageController'; import { UpgradeBuildingTask } from './Task/UpgradeBuildingTask'; @@ -18,6 +18,7 @@ import { Resources } from './Core/Resources'; import { Village } from './Core/Village'; import { calcGatheringTimings } from './Core/GatheringTimings'; import { DataStorage } from './DataStorage'; +import { getBuildingPageAttributes, isBuildingPage } from './Page/PageDetectors'; import { debounce } from 'debounce'; interface QuickAction { @@ -108,14 +109,8 @@ export class ControlPanel { showBuildingSlotIds(buildingsInQueue); } - if (p.pathname === '/build.php') { - const buildPage = new BuildingPageController(this.scheduler, { - buildId: getNumber(p.query.id), - buildTypeId: getNumber(elClassId(jQuery('#build').attr('class'), 'gid')), - categoryId: getNumber(p.query.category, 1), - sheetId: getNumber(p.query.s, 0), - tabId: getNumber(p.query.t, 0), - }); + if (isBuildingPage()) { + const buildPage = new BuildingPageController(this.scheduler, getBuildingPageAttributes()); buildPage.run(); } @@ -189,6 +184,7 @@ class VillageController { public readonly warehouse; public readonly granary; public readonly buildRemainingSeconds; + public readonly incomingResources: Resources; constructor(village: Village, state: VillageState, scheduler: Scheduler) { const resources = state.getResources(); @@ -225,6 +221,7 @@ class VillageController { this.warehouse = storage.warehouse; this.granary = storage.granary; this.buildRemainingSeconds = buildQueueInfo.seconds; + this.incomingResources = this.calcIncomingResources(state); } timeToRequired() { @@ -243,4 +240,8 @@ class VillageController { return timings.hours * 3600; } + + private calcIncomingResources(state: VillageState): Resources { + return state.getIncomingMerchants().reduce((m, i) => m.add(i.resources), new Resources(0, 0, 0, 0)); + } } diff --git a/src/Core/Buildings.ts b/src/Core/Buildings.ts new file mode 100644 index 0000000..64e08b4 --- /dev/null +++ b/src/Core/Buildings.ts @@ -0,0 +1,6 @@ +export const WAREHOUSE_ID = 10; +export const GARNER_ID = 11; +export const MARKET_ID = 17; +export const QUARTERS_ID = 19; +export const HORSE_STABLE_ID = 20; +export const EMBASSY_ID = 25; diff --git a/src/Core/Market.ts b/src/Core/Market.ts new file mode 100644 index 0000000..bf80868 --- /dev/null +++ b/src/Core/Market.ts @@ -0,0 +1,10 @@ +import { Resources } from './Resources'; + +export class IncomingMerchant { + readonly resources: Resources; + readonly ts: number; + constructor(resources: Resources, ts: number) { + this.resources = resources; + this.ts = ts; + } +} diff --git a/src/DashboardView/VillageStateList.vue b/src/DashboardView/VillageStateList.vue index 2f627fb..2811865 100644 --- a/src/DashboardView/VillageStateList.vue +++ b/src/DashboardView/VillageStateList.vue @@ -85,6 +85,23 @@ + + Торговцы: + + + + + + + + + + + + + + + @@ -97,6 +114,7 @@ >->{{ v.name }} Казармы + Конюшни @@ -109,6 +127,7 @@ import { path } from '../utils'; import ResourceBalance from './ResourceBalance'; import VillageResource from './VillageResource'; +import { HORSE_STABLE_ID, MARKET_ID, QUARTERS_ID, WAREHOUSE_ID } from '../Core/Buildings'; export default { components: { @@ -126,13 +145,22 @@ export default { return path(name, args); }, marketPath(fromVillage, toVillage) { - return path('/build.php', { newdid: fromVillage.id, gid: 17, t: 5, x: toVillage.crd.x, y: toVillage.crd.y }); + return path('/build.php', { + newdid: fromVillage.id, + gid: MARKET_ID, + t: 5, + x: toVillage.crd.x, + y: toVillage.crd.y, + }); }, warehousePath(village) { - return path('/build.php', { newdid: village.id, gid: 10 }); + return path('/build.php', { newdid: village.id, gid: WAREHOUSE_ID }); }, quartersPath(village) { - return path('/build.php', { newdid: village.id, gid: 19 }); + return path('/build.php', { newdid: village.id, gid: QUARTERS_ID }); + }, + horseStablePath(village) { + return path('/build.php', { newdid: village.id, gid: HORSE_STABLE_ID }); }, secondsToTime(value) { if (value === 0) { @@ -178,12 +206,9 @@ export default { border: 1px solid #ddd; } -.performance-line td { - padding: 0 4px 4px; - font-size: 90%; -} - -.required-line td { +.performance-line td, +.required-line td, +.incoming-line td { padding: 0 4px 4px; font-size: 90%; } diff --git a/src/DataStorage.ts b/src/DataStorage.ts index 068346d..180e448 100644 --- a/src/DataStorage.ts +++ b/src/DataStorage.ts @@ -1,4 +1,5 @@ import { ConsoleLogger, Logger, NullLogger } from './Logger'; +import { Resources } from './Core/Resources'; const NAMESPACE = 'travian:v1'; @@ -8,6 +9,36 @@ function join(...parts: Array) { return parts.map(p => p.replace(/[:]+$/g, '').replace(/^[:]+/g, '')).join(':'); } +interface EmptyObjectFactory { + (): T; +} + +interface ObjectMapper { + (item: any): T; +} + +interface ObjectMapperOptions { + factory?: EmptyObjectFactory; + mapper?: ObjectMapper; +} + +function createMapper(options: ObjectMapperOptions): ObjectMapper { + const { mapper, factory } = options; + + if (mapper) { + return mapper; + } + + if (factory) { + return plain => { + let item = factory(); + return Object.assign(item, plain) as T; + }; + } + + throw new Error('Factory or mapper must be specified'); +} + export class DataStorage { private readonly logger: Logger; private readonly name: string; @@ -40,6 +71,21 @@ export class DataStorage { } } + getTyped(key: string, options: ObjectMapperOptions = {}): T { + let plain = this.get(key); + const mapper = createMapper(options); + return mapper(plain); + } + + getTypedList(key: string, options: ObjectMapperOptions = {}): Array { + let plain = this.get(key); + if (!Array.isArray(plain)) { + return []; + } + const mapper = createMapper(options); + return (plain as Array).map(mapper); + } + has(key: string): boolean { const fullKey = join(NAMESPACE, this.name, key); return storage.getItem(fullKey) !== null; diff --git a/src/Grabber/GrabberManager.ts b/src/Grabber/GrabberManager.ts index e51061a..dbaceb3 100644 --- a/src/Grabber/GrabberManager.ts +++ b/src/Grabber/GrabberManager.ts @@ -2,6 +2,7 @@ import { Grabber } from './Grabber'; import { VillageResourceGrabber } from './VillageResourceGrabber'; import { VillageOverviewPageGrabber } from './VillageOverviewPageGrabber'; import { HeroPageGrabber } from './HeroPageGrabber'; +import { MarketPageGrabber } from './MarketPageGrabber'; export class GrabberManager { private readonly grabbers: Array = []; @@ -11,6 +12,7 @@ export class GrabberManager { this.grabbers.push(new VillageResourceGrabber()); this.grabbers.push(new VillageOverviewPageGrabber()); this.grabbers.push(new HeroPageGrabber()); + this.grabbers.push(new MarketPageGrabber()); } grab() { diff --git a/src/Grabber/HeroPageGrabber.ts b/src/Grabber/HeroPageGrabber.ts index fc056f9..b7f9f9d 100644 --- a/src/Grabber/HeroPageGrabber.ts +++ b/src/Grabber/HeroPageGrabber.ts @@ -1,21 +1,12 @@ import { Grabber } from './Grabber'; -import { - grabActiveVillageId, - grabBuildingQueueInfo, - grabResourcesPerformance, - grabVillageList, -} from '../Page/VillageBlock'; -import { VillageState } from '../State/VillageState'; -import { parseLocation } from '../utils'; -import { GrabError } from '../Errors'; -import { BuildingQueueInfo } from '../Game'; +import { grabVillageList } from '../Page/VillageBlock'; import { HeroState } from '../State/HeroState'; import { grabHeroAttributes, grabHeroVillage } from '../Page/HeroPage'; +import { isHeroPage } from '../Page/PageDetectors'; export class HeroPageGrabber extends Grabber { grab(): void { - const p = parseLocation(); - if (p.pathname !== '/hero.php') { + if (!isHeroPage()) { return; } diff --git a/src/Grabber/MarketPageGrabber.ts b/src/Grabber/MarketPageGrabber.ts new file mode 100644 index 0000000..4eeaa87 --- /dev/null +++ b/src/Grabber/MarketPageGrabber.ts @@ -0,0 +1,17 @@ +import { Grabber } from './Grabber'; +import { grabActiveVillageId } from '../Page/VillageBlock'; +import { VillageState } from '../State/VillageState'; +import { grabIncomingMerchants } from '../Page/BuildingPage'; +import { isMarketSendResourcesPage } from '../Page/PageDetectors'; + +export class MarketPageGrabber extends Grabber { + grab(): void { + if (!isMarketSendResourcesPage()) { + return; + } + + const villageId = grabActiveVillageId(); + const state = new VillageState(villageId); + state.storeIncomingMerchants(grabIncomingMerchants()); + } +} diff --git a/src/Page/BuildingPage.ts b/src/Page/BuildingPage.ts index 62271f1..ccb106d 100644 --- a/src/Page/BuildingPage.ts +++ b/src/Page/BuildingPage.ts @@ -2,6 +2,7 @@ import { GrabError } from '../Errors'; import { elClassId, getNumber, trimPrefix, uniqId } from '../utils'; import { Resources } from '../Core/Resources'; import { Coordinates } from '../Core/Village'; +import { IncomingMerchant } from '../Core/Market'; export function clickBuildButton(typeId: number) { const section = jQuery(`#contract_building${typeId}`); @@ -140,3 +141,18 @@ export function fillSendResourcesForm(resources: Resources, crd: Coordinates) { export function clickSendButton() { jQuery('#enabledButton').trigger('click'); } + +export function grabIncomingMerchants(): ReadonlyArray { + const result: Array = []; + const root = jQuery('#merchantsOnTheWay .ownMerchants'); + root.find('.traders').each((idx, el) => { + const $el = jQuery(el); + result.push( + new IncomingMerchant( + grabResourcesFromList($el.find('.resourceWrapper .resources')), + getNumber($el.find('.timer').attr('value')) + ) + ); + }); + return result; +} diff --git a/src/Page/BuildingPageController.ts b/src/Page/BuildingPageController.ts index 76cec3f..2e23288 100644 --- a/src/Page/BuildingPageController.ts +++ b/src/Page/BuildingPageController.ts @@ -9,24 +9,14 @@ import { createSendResourcesButton, createTrainTroopButtons, createUpgradeButton, + grabIncomingMerchants, } from './BuildingPage'; import { BuildBuildingTask } from '../Task/BuildBuildingTask'; import { Resources } from '../Core/Resources'; import { Coordinates } from '../Core/Village'; import { SendResourcesTask } from '../Task/SendResourcesTask'; - -const MARKET_ID = 17; -const QUARTERS_ID = 19; -const HORSE_STABLE_ID = 20; -const EMBASSY_ID = 25; - -export interface BuildingPageAttributes { - buildId: number; - buildTypeId: number; - categoryId: number; - sheetId: number; - tabId: number; -} +import { EMBASSY_ID, HORSE_STABLE_ID, QUARTERS_ID } from '../Core/Buildings'; +import { BuildingPageAttributes, isMarketSendResourcesPage } from './PageDetectors'; export class BuildingPageController { private scheduler: Scheduler; @@ -40,7 +30,7 @@ export class BuildingPageController { } run() { - const { buildTypeId, sheetId, tabId } = this.attributes; + const { buildTypeId, sheetId } = this.attributes; this.logger.log('BUILD PAGE DETECTED', 'ID', this.attributes.buildId, this.attributes); if (buildTypeId) { @@ -61,7 +51,8 @@ export class BuildingPageController { createTrainTroopButtons((troopId, res, count) => this.onScheduleTrainTroopers(troopId, res, count)); } - if (buildTypeId === MARKET_ID && tabId === 5) { + if (isMarketSendResourcesPage()) { + console.log('MERCH', grabIncomingMerchants()); createSendResourcesButton((res, crd) => this.onSendResources(res, crd)); } } diff --git a/src/Page/PageDetectors.ts b/src/Page/PageDetectors.ts new file mode 100644 index 0000000..cbb7120 --- /dev/null +++ b/src/Page/PageDetectors.ts @@ -0,0 +1,42 @@ +import { elClassId, getNumber, parseLocation } from '../utils'; +import { MARKET_ID } from '../Core/Buildings'; + +export interface BuildingPageAttributes { + buildId: number; + buildTypeId: number; + categoryId: number; + sheetId: number; + tabId: number; +} + +export function isBuildingPage() { + const p = parseLocation(); + return p.pathname === '/build.php'; +} + +export function isHeroPage() { + const p = parseLocation(); + return p.pathname === '/hero.php'; +} + +export function getBuildingPageAttributes(): BuildingPageAttributes { + if (!isBuildingPage()) { + throw Error('Not building page'); + } + const p = parseLocation(); + return { + buildId: getNumber(p.query.id), + buildTypeId: getNumber(elClassId(jQuery('#build').attr('class'), 'gid')), + categoryId: getNumber(p.query.category, 1), + sheetId: getNumber(p.query.s, 0), + tabId: getNumber(p.query.t, 0), + }; +} + +export function isMarketSendResourcesPage(): boolean { + if (!isBuildingPage()) { + return false; + } + const { buildTypeId, tabId } = getBuildingPageAttributes(); + return buildTypeId === MARKET_ID && tabId === 5; +} diff --git a/src/State/VillageState.ts b/src/State/VillageState.ts index 6338c59..04ae939 100644 --- a/src/State/VillageState.ts +++ b/src/State/VillageState.ts @@ -1,12 +1,18 @@ import { DataStorage } from '../DataStorage'; import { BuildingQueueInfo } from '../Game'; -import { Resources } from '../Core/Resources'; +import { Resources, ResourcesInterface } from '../Core/Resources'; import { ResourceStorage } from '../Core/ResourceStorage'; +import { IncomingMerchant } from '../Core/Market'; const RESOURCES_KEY = 'res'; const CAPACITY_KEY = 'cap'; const PERFORMANCE_KEY = 'perf'; const BUILDING_QUEUE_KEY = 'bq'; +const INCOMING_MERCHANTS_KEY = 'im'; + +const ResourceOptions = { + factory: () => new Resources(0, 0, 0, 0), +}; export class VillageState { private storage: DataStorage; @@ -19,9 +25,7 @@ export class VillageState { } getResources(): Resources { - let plain = this.storage.get(RESOURCES_KEY); - let res = new Resources(0, 0, 0, 0); - return Object.assign(res, plain) as Resources; + return this.storage.getTyped(RESOURCES_KEY, ResourceOptions); } storeResourceStorage(storage: ResourceStorage) { @@ -39,9 +43,7 @@ export class VillageState { } getResourcesPerformance(): Resources { - let plain = this.storage.get(PERFORMANCE_KEY); - let res = new Resources(0, 0, 0, 0); - return Object.assign(res, plain) as Resources; + return this.storage.getTyped(PERFORMANCE_KEY, ResourceOptions); } storeBuildingQueueInfo(info: BuildingQueueInfo): void { @@ -53,4 +55,23 @@ export class VillageState { let res = new BuildingQueueInfo(0); return Object.assign(res, plain) as BuildingQueueInfo; } + + storeIncomingMerchants(merchants: ReadonlyArray): void { + this.storage.set( + INCOMING_MERCHANTS_KEY, + merchants.map(m => ({ ...m.resources, ts: m.ts })) + ); + } + + getIncomingMerchants(): ReadonlyArray { + const objects = this.storage.getTypedList(INCOMING_MERCHANTS_KEY, { factory: () => ({}) }); + return objects.map((plain: object) => { + const m = new IncomingMerchant(new Resources(0, 0, 0, 0), 0); + if (typeof plain !== 'object') { + return m; + } + const norm = plain as ResourcesInterface & { ts: number }; + return new IncomingMerchant(Resources.fromObject(norm), Number(norm.ts || 0)); + }); + } } diff --git a/src/Task/GrabVillageState.ts b/src/Task/GrabVillageState.ts index 8804963..7870a2e 100644 --- a/src/Task/GrabVillageState.ts +++ b/src/Task/GrabVillageState.ts @@ -5,6 +5,7 @@ import { path } from '../utils'; import { Task } from '../Queue/TaskQueue'; import { TaskController, registerTask } from './TaskController'; import { grabVillageList } from '../Page/VillageBlock'; +import { MARKET_ID } from '../Core/Buildings'; @registerTask export class GrabVillageState extends TaskController { @@ -21,6 +22,12 @@ export class GrabVillageState extends TaskController { path: path('/dorf1.php', { newdid: village.id }), }) ); + actions.push( + new Command(GoToPageAction.name, { + ...args, + path: path('/build.php', { newdid: village.id, gid: MARKET_ID, t: 5 }), + }) + ); } actions.push(new Command(CompleteTaskAction.name, args));