Store incoming merchants

This commit is contained in:
Anton Vakhrushev 2020-04-25 12:01:07 +03:00
parent 6bd04ee6e2
commit f005072d9c
13 changed files with 227 additions and 52 deletions

View File

@ -1,4 +1,4 @@
import { elClassId, getNumber, parseLocation, uniqId, waitForLoad } from './utils'; import { parseLocation, uniqId, waitForLoad } from './utils';
import { Scheduler } from './Scheduler'; import { Scheduler } from './Scheduler';
import { BuildingPageController } from './Page/BuildingPageController'; import { BuildingPageController } from './Page/BuildingPageController';
import { UpgradeBuildingTask } from './Task/UpgradeBuildingTask'; import { UpgradeBuildingTask } from './Task/UpgradeBuildingTask';
@ -18,6 +18,7 @@ import { Resources } from './Core/Resources';
import { Village } from './Core/Village'; import { Village } from './Core/Village';
import { calcGatheringTimings } from './Core/GatheringTimings'; import { calcGatheringTimings } from './Core/GatheringTimings';
import { DataStorage } from './DataStorage'; import { DataStorage } from './DataStorage';
import { getBuildingPageAttributes, isBuildingPage } from './Page/PageDetectors';
import { debounce } from 'debounce'; import { debounce } from 'debounce';
interface QuickAction { interface QuickAction {
@ -108,14 +109,8 @@ export class ControlPanel {
showBuildingSlotIds(buildingsInQueue); showBuildingSlotIds(buildingsInQueue);
} }
if (p.pathname === '/build.php') { if (isBuildingPage()) {
const buildPage = new BuildingPageController(this.scheduler, { const buildPage = new BuildingPageController(this.scheduler, getBuildingPageAttributes());
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),
});
buildPage.run(); buildPage.run();
} }
@ -189,6 +184,7 @@ class VillageController {
public readonly warehouse; public readonly warehouse;
public readonly granary; public readonly granary;
public readonly buildRemainingSeconds; public readonly buildRemainingSeconds;
public readonly incomingResources: Resources;
constructor(village: Village, state: VillageState, scheduler: Scheduler) { constructor(village: Village, state: VillageState, scheduler: Scheduler) {
const resources = state.getResources(); const resources = state.getResources();
@ -225,6 +221,7 @@ class VillageController {
this.warehouse = storage.warehouse; this.warehouse = storage.warehouse;
this.granary = storage.granary; this.granary = storage.granary;
this.buildRemainingSeconds = buildQueueInfo.seconds; this.buildRemainingSeconds = buildQueueInfo.seconds;
this.incomingResources = this.calcIncomingResources(state);
} }
timeToRequired() { timeToRequired() {
@ -243,4 +240,8 @@ class VillageController {
return timings.hours * 3600; 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));
}
} }

6
src/Core/Buildings.ts Normal file
View File

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

10
src/Core/Market.ts Normal file
View File

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

View File

@ -85,6 +85,23 @@
<td class="right" v-text="timeToTotalRequired(village)"></td> <td class="right" v-text="timeToTotalRequired(village)"></td>
<td></td> <td></td>
</tr> </tr>
<tr class="incoming-line">
<td class="right">Торговцы:</td>
<td class="right">
<resource :value="village.incomingResources.lumber"></resource>
</td>
<td class="right">
<resource :value="village.incomingResources.clay"></resource>
</td>
<td class="right">
<resource :value="village.incomingResources.iron"></resource>
</td>
<td class="right">
<resource :value="village.incomingResources.crop"></resource>
</td>
<td></td>
<td></td>
</tr>
<tr class="normal-line"> <tr class="normal-line">
<td></td> <td></td>
<td class="right" colspan="6"> <td class="right" colspan="6">
@ -97,6 +114,7 @@
>->{{ v.name }}</a >->{{ v.name }}</a
> >
<a class="village-quick-link" :href="quartersPath(village)">Казармы</a> <a class="village-quick-link" :href="quartersPath(village)">Казармы</a>
<a class="village-quick-link" :href="horseStablePath(village)">Конюшни</a>
</td> </td>
</tr> </tr>
</template> </template>
@ -109,6 +127,7 @@
import { path } from '../utils'; import { path } from '../utils';
import ResourceBalance from './ResourceBalance'; import ResourceBalance from './ResourceBalance';
import VillageResource from './VillageResource'; import VillageResource from './VillageResource';
import { HORSE_STABLE_ID, MARKET_ID, QUARTERS_ID, WAREHOUSE_ID } from '../Core/Buildings';
export default { export default {
components: { components: {
@ -126,13 +145,22 @@ export default {
return path(name, args); return path(name, args);
}, },
marketPath(fromVillage, toVillage) { 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) { warehousePath(village) {
return path('/build.php', { newdid: village.id, gid: 10 }); return path('/build.php', { newdid: village.id, gid: WAREHOUSE_ID });
}, },
quartersPath(village) { 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) { secondsToTime(value) {
if (value === 0) { if (value === 0) {
@ -178,12 +206,9 @@ export default {
border: 1px solid #ddd; border: 1px solid #ddd;
} }
.performance-line td { .performance-line td,
padding: 0 4px 4px; .required-line td,
font-size: 90%; .incoming-line td {
}
.required-line td {
padding: 0 4px 4px; padding: 0 4px 4px;
font-size: 90%; font-size: 90%;
} }

View File

@ -1,4 +1,5 @@
import { ConsoleLogger, Logger, NullLogger } from './Logger'; import { ConsoleLogger, Logger, NullLogger } from './Logger';
import { Resources } from './Core/Resources';
const NAMESPACE = 'travian:v1'; const NAMESPACE = 'travian:v1';
@ -8,6 +9,36 @@ function join(...parts: Array<string>) {
return parts.map(p => p.replace(/[:]+$/g, '').replace(/^[:]+/g, '')).join(':'); return parts.map(p => p.replace(/[:]+$/g, '').replace(/^[:]+/g, '')).join(':');
} }
interface EmptyObjectFactory<T> {
(): T;
}
interface ObjectMapper<T> {
(item: any): T;
}
interface ObjectMapperOptions<T> {
factory?: EmptyObjectFactory<T>;
mapper?: ObjectMapper<T>;
}
function createMapper<T>(options: ObjectMapperOptions<T>): ObjectMapper<T> {
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 { export class DataStorage {
private readonly logger: Logger; private readonly logger: Logger;
private readonly name: string; private readonly name: string;
@ -40,6 +71,21 @@ export class DataStorage {
} }
} }
getTyped<T>(key: string, options: ObjectMapperOptions<T> = {}): T {
let plain = this.get(key);
const mapper = createMapper(options);
return mapper(plain);
}
getTypedList<T>(key: string, options: ObjectMapperOptions<T> = {}): Array<T> {
let plain = this.get(key);
if (!Array.isArray(plain)) {
return [];
}
const mapper = createMapper(options);
return (plain as Array<any>).map(mapper);
}
has(key: string): boolean { has(key: string): boolean {
const fullKey = join(NAMESPACE, this.name, key); const fullKey = join(NAMESPACE, this.name, key);
return storage.getItem(fullKey) !== null; return storage.getItem(fullKey) !== null;

View File

@ -2,6 +2,7 @@ import { Grabber } from './Grabber';
import { VillageResourceGrabber } from './VillageResourceGrabber'; import { VillageResourceGrabber } from './VillageResourceGrabber';
import { VillageOverviewPageGrabber } from './VillageOverviewPageGrabber'; import { VillageOverviewPageGrabber } from './VillageOverviewPageGrabber';
import { HeroPageGrabber } from './HeroPageGrabber'; import { HeroPageGrabber } from './HeroPageGrabber';
import { MarketPageGrabber } from './MarketPageGrabber';
export class GrabberManager { export class GrabberManager {
private readonly grabbers: Array<Grabber> = []; private readonly grabbers: Array<Grabber> = [];
@ -11,6 +12,7 @@ export class GrabberManager {
this.grabbers.push(new VillageResourceGrabber()); this.grabbers.push(new VillageResourceGrabber());
this.grabbers.push(new VillageOverviewPageGrabber()); this.grabbers.push(new VillageOverviewPageGrabber());
this.grabbers.push(new HeroPageGrabber()); this.grabbers.push(new HeroPageGrabber());
this.grabbers.push(new MarketPageGrabber());
} }
grab() { grab() {

View File

@ -1,21 +1,12 @@
import { Grabber } from './Grabber'; import { Grabber } from './Grabber';
import { import { grabVillageList } from '../Page/VillageBlock';
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 { HeroState } from '../State/HeroState'; import { HeroState } from '../State/HeroState';
import { grabHeroAttributes, grabHeroVillage } from '../Page/HeroPage'; import { grabHeroAttributes, grabHeroVillage } from '../Page/HeroPage';
import { isHeroPage } from '../Page/PageDetectors';
export class HeroPageGrabber extends Grabber { export class HeroPageGrabber extends Grabber {
grab(): void { grab(): void {
const p = parseLocation(); if (!isHeroPage()) {
if (p.pathname !== '/hero.php') {
return; return;
} }

View File

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

View File

@ -2,6 +2,7 @@ import { GrabError } from '../Errors';
import { elClassId, getNumber, trimPrefix, uniqId } from '../utils'; import { elClassId, getNumber, trimPrefix, uniqId } from '../utils';
import { Resources } from '../Core/Resources'; import { Resources } from '../Core/Resources';
import { Coordinates } from '../Core/Village'; import { Coordinates } from '../Core/Village';
import { IncomingMerchant } from '../Core/Market';
export function clickBuildButton(typeId: number) { export function clickBuildButton(typeId: number) {
const section = jQuery(`#contract_building${typeId}`); const section = jQuery(`#contract_building${typeId}`);
@ -140,3 +141,18 @@ export function fillSendResourcesForm(resources: Resources, crd: Coordinates) {
export function clickSendButton() { export function clickSendButton() {
jQuery('#enabledButton').trigger('click'); jQuery('#enabledButton').trigger('click');
} }
export function grabIncomingMerchants(): ReadonlyArray<IncomingMerchant> {
const result: Array<IncomingMerchant> = [];
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;
}

View File

@ -9,24 +9,14 @@ import {
createSendResourcesButton, createSendResourcesButton,
createTrainTroopButtons, createTrainTroopButtons,
createUpgradeButton, createUpgradeButton,
grabIncomingMerchants,
} from './BuildingPage'; } from './BuildingPage';
import { BuildBuildingTask } from '../Task/BuildBuildingTask'; import { BuildBuildingTask } from '../Task/BuildBuildingTask';
import { Resources } from '../Core/Resources'; import { Resources } from '../Core/Resources';
import { Coordinates } from '../Core/Village'; import { Coordinates } from '../Core/Village';
import { SendResourcesTask } from '../Task/SendResourcesTask'; import { SendResourcesTask } from '../Task/SendResourcesTask';
import { EMBASSY_ID, HORSE_STABLE_ID, QUARTERS_ID } from '../Core/Buildings';
const MARKET_ID = 17; import { BuildingPageAttributes, isMarketSendResourcesPage } from './PageDetectors';
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;
}
export class BuildingPageController { export class BuildingPageController {
private scheduler: Scheduler; private scheduler: Scheduler;
@ -40,7 +30,7 @@ export class BuildingPageController {
} }
run() { run() {
const { buildTypeId, sheetId, tabId } = this.attributes; const { buildTypeId, sheetId } = this.attributes;
this.logger.log('BUILD PAGE DETECTED', 'ID', this.attributes.buildId, this.attributes); this.logger.log('BUILD PAGE DETECTED', 'ID', this.attributes.buildId, this.attributes);
if (buildTypeId) { if (buildTypeId) {
@ -61,7 +51,8 @@ export class BuildingPageController {
createTrainTroopButtons((troopId, res, count) => this.onScheduleTrainTroopers(troopId, res, count)); 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)); createSendResourcesButton((res, crd) => this.onSendResources(res, crd));
} }
} }

42
src/Page/PageDetectors.ts Normal file
View File

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

View File

@ -1,12 +1,18 @@
import { DataStorage } from '../DataStorage'; import { DataStorage } from '../DataStorage';
import { BuildingQueueInfo } from '../Game'; import { BuildingQueueInfo } from '../Game';
import { Resources } from '../Core/Resources'; import { Resources, ResourcesInterface } from '../Core/Resources';
import { ResourceStorage } from '../Core/ResourceStorage'; import { ResourceStorage } from '../Core/ResourceStorage';
import { IncomingMerchant } from '../Core/Market';
const RESOURCES_KEY = 'res'; const RESOURCES_KEY = 'res';
const CAPACITY_KEY = 'cap'; const CAPACITY_KEY = 'cap';
const PERFORMANCE_KEY = 'perf'; const PERFORMANCE_KEY = 'perf';
const BUILDING_QUEUE_KEY = 'bq'; const BUILDING_QUEUE_KEY = 'bq';
const INCOMING_MERCHANTS_KEY = 'im';
const ResourceOptions = {
factory: () => new Resources(0, 0, 0, 0),
};
export class VillageState { export class VillageState {
private storage: DataStorage; private storage: DataStorage;
@ -19,9 +25,7 @@ export class VillageState {
} }
getResources(): Resources { getResources(): Resources {
let plain = this.storage.get(RESOURCES_KEY); return this.storage.getTyped(RESOURCES_KEY, ResourceOptions);
let res = new Resources(0, 0, 0, 0);
return Object.assign(res, plain) as Resources;
} }
storeResourceStorage(storage: ResourceStorage) { storeResourceStorage(storage: ResourceStorage) {
@ -39,9 +43,7 @@ export class VillageState {
} }
getResourcesPerformance(): Resources { getResourcesPerformance(): Resources {
let plain = this.storage.get(PERFORMANCE_KEY); return this.storage.getTyped(PERFORMANCE_KEY, ResourceOptions);
let res = new Resources(0, 0, 0, 0);
return Object.assign(res, plain) as Resources;
} }
storeBuildingQueueInfo(info: BuildingQueueInfo): void { storeBuildingQueueInfo(info: BuildingQueueInfo): void {
@ -53,4 +55,23 @@ export class VillageState {
let res = new BuildingQueueInfo(0); let res = new BuildingQueueInfo(0);
return Object.assign(res, plain) as BuildingQueueInfo; return Object.assign(res, plain) as BuildingQueueInfo;
} }
storeIncomingMerchants(merchants: ReadonlyArray<IncomingMerchant>): void {
this.storage.set(
INCOMING_MERCHANTS_KEY,
merchants.map(m => ({ ...m.resources, ts: m.ts }))
);
}
getIncomingMerchants(): ReadonlyArray<IncomingMerchant> {
const objects = this.storage.getTypedList<object>(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));
});
}
} }

View File

@ -5,6 +5,7 @@ import { path } from '../utils';
import { Task } from '../Queue/TaskQueue'; import { Task } from '../Queue/TaskQueue';
import { TaskController, registerTask } from './TaskController'; import { TaskController, registerTask } from './TaskController';
import { grabVillageList } from '../Page/VillageBlock'; import { grabVillageList } from '../Page/VillageBlock';
import { MARKET_ID } from '../Core/Buildings';
@registerTask @registerTask
export class GrabVillageState extends TaskController { export class GrabVillageState extends TaskController {
@ -21,6 +22,12 @@ export class GrabVillageState extends TaskController {
path: path('/dorf1.php', { newdid: village.id }), 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)); actions.push(new Command(CompleteTaskAction.name, args));