diff --git a/package-lock.json b/package-lock.json index 1c4f02c..b206150 100644 --- a/package-lock.json +++ b/package-lock.json @@ -336,6 +336,12 @@ "integrity": "sha512-KlPPdikagvL6ELjWsljbyDIPzNCeliYkqRpI+zea99vBBbCIA5JNshZAwQKTON139c87y9qvTFVgkFd14rtS4g==", "dev": true }, + "@types/debounce": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.0.tgz", + "integrity": "sha512-bWG5wapaWgbss9E238T0R6bfo5Fh3OkeoSt245CM7JJwVwpw6MEBCbIxLq5z8KzsE3uJhzcIuQkyiZmzV3M/Dw==", + "dev": true + }, "@types/jquery": { "version": "3.3.34", "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.34.tgz", @@ -2082,6 +2088,12 @@ "integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=", "dev": true }, + "debounce": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz", + "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==", + "dev": true + }, "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", diff --git a/package.json b/package.json index 5e13699..ef35424 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "devDependencies": { "@types/chai": "^4.2.11", "@types/dateformat": "^3.0.1", + "@types/debounce": "^1.2.0", "@types/jquery": "^3.3.34", "@types/mocha": "^7.0.2", "@types/node": "^13.9.4", @@ -30,6 +31,7 @@ "chai": "^4.2.0", "css-loader": "^3.5.2", "dateformat": "^3.0.3", + "debounce": "^1.2.0", "jquery": "^3.4.1", "mocha": "^7.1.1", "mocha-junit-reporter": "^1.23.3", diff --git a/src/Action/BalanceHeroResourcesAction.ts b/src/Action/BalanceHeroResourcesAction.ts index dfec63b..53e3b5d 100644 --- a/src/Action/BalanceHeroResourcesAction.ts +++ b/src/Action/BalanceHeroResourcesAction.ts @@ -1,7 +1,7 @@ import { ActionController, registerAction } from './ActionController'; import { Args } from '../Command'; import { Task } from '../Queue/TaskQueue'; -import { grabResources, grabResourceStorage } from '../Page/ResourcesBlock'; +import { grabVillageResources, grabVillageResourceStorage } from '../Page/ResourcesBlock'; import { changeHeroResource, grabCurrentHeroResource } from '../Page/HeroPage'; import { grabActiveVillageId } from '../Page/VillageBlock'; import { HeroState } from '../State/HeroState'; @@ -19,10 +19,10 @@ export class BalanceHeroResourcesAction extends ActionController { return; } - const resources = grabResources(); + const resources = grabVillageResources(); const requiredResources = this.scheduler.getVillageRequiredResources(heroVillageId); const totalRequiredResources = this.scheduler.getTotalVillageRequiredResources(heroVillageId); - const storage = grabResourceStorage(); + const storage = grabVillageResourceStorage(); const currentType = grabCurrentHeroResource(); const heroType = calcHeroResource(resources, requiredResources, totalRequiredResources, storage); diff --git a/src/Action/SendResourcesAction.ts b/src/Action/SendResourcesAction.ts new file mode 100644 index 0000000..4820e68 --- /dev/null +++ b/src/Action/SendResourcesAction.ts @@ -0,0 +1,88 @@ +import { ActionController, registerAction } from './ActionController'; +import { Args } from '../Command'; +import { AbortTaskError, ActionError } from '../Errors'; +import { Task } from '../Queue/TaskQueue'; +import { Resources } from '../Core/Resources'; +import { Coordinates } from '../Core/Village'; +import { clickSendButton, fillSendResourcesForm, grabMerchantsInfo } from '../Page/BuildingPage'; +import { grabVillageResources } from '../Page/ResourcesBlock'; +import { grabVillageList } from '../Page/VillageBlock'; +import { SendResourcesTask } from '../Task/SendResourcesTask'; +import { timestamp } from '../utils'; +import { VillageState } from '../State/VillageState'; + +function err(msg): never { + throw new ActionError(msg); +} + +@registerAction +export class SendResourcesAction extends ActionController { + async run(args: Args, task: Task): Promise { + const resources = Resources.fromObject(args.resources || err('No resources')); + const coordinates = Coordinates.fromObject(args.coordinates || err('No coordinates')); + + const merchants = grabMerchantsInfo(); + const villageList = grabVillageList(); + + const village = villageList.find(v => v.crd.eq(coordinates)); + + if (!village) { + throw new AbortTaskError('No village'); + } + + const capacity = merchants.available * merchants.carry; + + if (!capacity) { + throw new AbortTaskError('No merchants'); + } + + console.log('Send', resources, 'to', coordinates); + console.log('Merchants', merchants, capacity); + + const villageResources = grabVillageResources(); + + const targetVillageState = new VillageState(village.id); + const targetVillageResources = targetVillageState.getResources(); + const targetVillageRequirements = this.scheduler + .getVillageRequiredResources(village.id) + .sub(targetVillageResources) + .max(Resources.zero()); + + if (targetVillageRequirements.eq(Resources.zero())) { + throw new AbortTaskError('No requirements'); + } + + let sendResources = targetVillageRequirements.min(villageResources).min(resources); + + const reqSum = sendResources.lumber + sendResources.clay + sendResources.iron + sendResources.crop; + + let coeff = reqSum > capacity ? capacity / reqSum : 1; + + const normSendResources = sendResources.scale(coeff); + + const remainingResources = resources.sub(normSendResources); + + console.log('planned res', resources); + console.log('current village res', villageResources); + console.log('target village req', targetVillageRequirements); + console.log('send res', sendResources); + console.log('coeff', coeff); + console.log('norm send res', normSendResources); + console.log('remaining res', remainingResources); + + if (remainingResources.gt(Resources.zero())) { + console.log('schedule next', remainingResources); + this.scheduler.scheduleTask( + SendResourcesTask.name, + { + ...args, + resources: remainingResources, + }, + timestamp() + 10 * 60 + ); + } + + fillSendResourcesForm(normSendResources, coordinates); + clickSendButton(); + } +} diff --git a/src/Command.ts b/src/Command.ts index f28dfbd..55ec20e 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -1,5 +1,6 @@ import { TaskId } from './Queue/TaskQueue'; import { ResourcesInterface } from './Core/Resources'; +import { CoordinatesInterface } from './Core/Village'; export interface Args { taskId?: TaskId; @@ -7,11 +8,13 @@ export interface Args { villageId?: number; buildId?: number; categoryId?: number; + sheetId?: number; tabId?: number; buildTypeId?: number; troopId?: number; trainCount?: number; resources?: ResourcesInterface; + coordinates?: CoordinatesInterface; [name: string]: any; } diff --git a/src/ControlPanel.ts b/src/ControlPanel.ts index 104f4e8..eb72922 100644 --- a/src/ControlPanel.ts +++ b/src/ControlPanel.ts @@ -16,6 +16,9 @@ import { ConsoleLogger, Logger } from './Logger'; import { VillageState } from './State/VillageState'; import { Resources } from './Core/Resources'; import { Village } from './Core/Village'; +import { calcGatheringTimings } from './Core/GatheringTimings'; +import { DataStorage } from './DataStorage'; +import { debounce } from 'debounce'; interface QuickAction { label: string; @@ -76,10 +79,19 @@ export class ControlPanel { }; state.refreshTasks(); - setInterval(() => state.refreshTasks(), 2000); - state.refreshVillages(); - setInterval(() => state.refreshVillages(), 5000); + + setInterval(() => { + state.refreshTasks(); + state.refreshVillages(); + }, 3000); + + DataStorage.onChange(() => { + debounce(() => { + setInterval(() => state.refreshTasks(), 2000); + setInterval(() => state.refreshVillages(), 5000); + }, 500); + }); const tasks = this.scheduler.getTaskItems(); const buildingsInQueue = tasks @@ -101,7 +113,8 @@ export class ControlPanel { buildId: getNumber(p.query.id), buildTypeId: getNumber(elClassId(jQuery('#build').attr('class'), 'gid')), categoryId: getNumber(p.query.category, 1), - tabId: getNumber(p.query.s, 0), + sheetId: getNumber(p.query.s, 0), + tabId: getNumber(p.query.t, 0), }); buildPage.run(); } @@ -223,29 +236,11 @@ class VillageController { } private timeToResources(resources: Resources): number { - const time_to_lumber = this.timeToRes(this.resources.lumber, resources.lumber, this.performance.lumber); - - const time_to_clay = this.timeToRes(this.resources.clay, resources.clay, this.performance.clay); - const time_to_iron = this.timeToRes(this.resources.iron, resources.iron, this.performance.iron); - const time_to_crop = this.timeToRes(this.resources.crop, resources.crop, this.performance.crop); - - const min = Math.max(time_to_lumber, time_to_clay, time_to_iron, time_to_crop); - - if (min === -1) { + const timings = calcGatheringTimings(this.resources, resources, this.performance); + if (timings.never) { return -1; } - return Math.max(time_to_lumber, time_to_clay, time_to_iron, time_to_crop); - } - - private timeToRes(current: number, desired: number, speed: number) { - if (current >= desired) { - return 0; - } - if (current < desired && speed <= 0) { - return -1; - } - const diff = desired - current; - return (diff / speed) * 3600; + return timings.hours * 3600; } } diff --git a/src/Core/GatheringTimings.ts b/src/Core/GatheringTimings.ts new file mode 100644 index 0000000..e16fddf --- /dev/null +++ b/src/Core/GatheringTimings.ts @@ -0,0 +1,56 @@ +import { Resources } from './Resources'; + +type GatheringNever = 'never'; +type GatheringTime = number | GatheringNever; + +export class GatheringTimings { + readonly lumber: GatheringTime; + readonly clay: GatheringTime; + readonly iron: GatheringTime; + readonly crop: GatheringTime; + + constructor(lumber: GatheringTime, clay: GatheringTime, iron: GatheringTime, crop: GatheringTime) { + this.lumber = lumber; + this.clay = clay; + this.iron = iron; + this.crop = crop; + } + + get common(): GatheringTime { + const xs = [this.lumber, this.clay, this.iron, this.crop]; + return xs.reduce((m, t) => (m === 'never' || t === 'never' ? 'never' : Math.max(m, t)), 0); + } + + get hours(): number { + const common = this.common; + if (common === 'never') { + throw Error('Never'); + } + return common; + } + + get never(): boolean { + return this.common === 'never'; + } +} + +function calcGatheringTime(val: number, desired: number, speed: number): GatheringTime { + const diff = desired - val; + if (diff > 0 && speed <= 0) { + return 'never'; + } + if (diff <= 0) { + return 0; + } + + return diff / speed; +} + +export function calcGatheringTimings(resources: Resources, desired: Resources, speed: Resources): GatheringTimings { + return new GatheringTimings( + calcGatheringTime(resources.lumber, desired.lumber, speed.lumber), + calcGatheringTime(resources.clay, desired.clay, speed.clay), + calcGatheringTime(resources.iron, desired.iron, speed.iron), + calcGatheringTime(resources.crop, desired.crop, speed.crop) + ); +} diff --git a/src/Core/Resources.ts b/src/Core/Resources.ts index 5b31a6b..1f1d3b8 100644 --- a/src/Core/Resources.ts +++ b/src/Core/Resources.ts @@ -29,6 +29,10 @@ export class Resources implements ResourcesInterface { return new Resources(storage.warehouse, storage.warehouse, storage.warehouse, storage.granary); } + static zero(): Resources { + return new Resources(0, 0, 0, 0); + } + getByType(type: ResourceType): number { switch (type) { case ResourceType.Lumber: @@ -72,19 +76,46 @@ export class Resources implements ResourcesInterface { ); } - lt(other: Resources): boolean { + eq(other: ResourcesInterface): boolean { + return ( + this.lumber === other.lumber && + this.clay === other.clay && + this.iron === other.iron && + this.crop === other.crop + ); + } + + lt(other: ResourcesInterface): boolean { return this.lumber < other.lumber && this.clay < other.clay && this.iron < other.iron && this.crop < other.crop; } - gt(other: Resources): boolean { + gt(other: ResourcesInterface): boolean { return this.lumber > other.lumber && this.clay > other.clay && this.iron > other.iron && this.crop > other.crop; } - lte(other: Resources): boolean { + lte(other: ResourcesInterface): boolean { return !this.gt(other); } - gte(other: Resources): boolean { + gte(other: ResourcesInterface): boolean { return !this.lt(other); } + + min(other: ResourcesInterface): Resources { + return new Resources( + Math.min(this.lumber, other.lumber), + Math.min(this.clay, other.clay), + Math.min(this.iron, other.iron), + Math.min(this.crop, other.crop) + ); + } + + max(other: ResourcesInterface): Resources { + return new Resources( + Math.max(this.lumber, other.lumber), + Math.max(this.clay, other.clay), + Math.max(this.iron, other.iron), + Math.max(this.crop, other.crop) + ); + } } diff --git a/src/Core/Village.ts b/src/Core/Village.ts index 201da19..b4e5033 100644 --- a/src/Core/Village.ts +++ b/src/Core/Village.ts @@ -1,11 +1,24 @@ -export class Coordinates { +export interface CoordinatesInterface { + x: number; + y: number; +} + +export class Coordinates implements CoordinatesInterface { readonly x: number; readonly y: number; + static fromObject(crd: CoordinatesInterface) { + return new Coordinates(crd.x, crd.y); + } + constructor(x: number, y: number) { this.x = x; this.y = y; } + + eq(other: CoordinatesInterface): boolean { + return this.x === other.x && this.y === other.y; + } } export class Village { diff --git a/src/DataStorage.ts b/src/DataStorage.ts index e47bf28..068346d 100644 --- a/src/DataStorage.ts +++ b/src/DataStorage.ts @@ -2,6 +2,8 @@ import { ConsoleLogger, Logger, NullLogger } from './Logger'; const NAMESPACE = 'travian:v1'; +const storage = localStorage; + function join(...parts: Array) { return parts.map(p => p.replace(/[:]+$/g, '').replace(/^[:]+/g, '')).join(':'); } @@ -16,10 +18,18 @@ export class DataStorage { this.logger = new NullLogger(); } + static onChange(handler: (key: string) => void) { + window.addEventListener('storage', ({ key, storageArea }) => { + if (storageArea === storage) { + handler(key || ''); + } + }); + } + get(key: string): any { const fullKey = join(NAMESPACE, this.name, key); try { - const serialized = localStorage.getItem(fullKey); + const serialized = storage.getItem(fullKey); this.logger.log('GET', fullKey, serialized); return JSON.parse(serialized || '"null"'); } catch (e) { @@ -32,13 +42,13 @@ export class DataStorage { has(key: string): boolean { const fullKey = join(NAMESPACE, this.name, key); - return localStorage.getItem(fullKey) !== null; + return storage.getItem(fullKey) !== null; } set(key: string, value: any) { const fullKey = join(NAMESPACE, this.name, key); let serialized = JSON.stringify(value); this.logger.log('SET', fullKey, serialized); - localStorage.setItem(fullKey, serialized); + storage.setItem(fullKey, serialized); } } diff --git a/src/Executor.ts b/src/Executor.ts index 082cccb..98517f1 100644 --- a/src/Executor.ts +++ b/src/Executor.ts @@ -6,19 +6,19 @@ import { TaskQueueRenderer } from './TaskQueueRenderer'; import { createAction } from './Action/ActionController'; import { createTask } from './Task/TaskController'; import { ConsoleLogger, Logger } from './Logger'; -import { StateGrabberManager } from './Grabber/StateGrabberManager'; +import { GrabberManager } from './Grabber/GrabberManager'; import { Scheduler } from './Scheduler'; export class Executor { private readonly version: string; private readonly scheduler: Scheduler; - private grabbers: StateGrabberManager; + private grabbers: GrabberManager; private logger: Logger; constructor(version: string, scheduler: Scheduler) { this.version = version; this.scheduler = scheduler; - this.grabbers = new StateGrabberManager(); + this.grabbers = new GrabberManager(); this.logger = new ConsoleLogger(this.constructor.name); } diff --git a/src/Grabber/Grabber.ts b/src/Grabber/Grabber.ts new file mode 100644 index 0000000..8c3b9ec --- /dev/null +++ b/src/Grabber/Grabber.ts @@ -0,0 +1,3 @@ +export abstract class Grabber { + abstract grab(): void; +} diff --git a/src/Grabber/StateGrabberManager.ts b/src/Grabber/GrabberManager.ts similarity index 62% rename from src/Grabber/StateGrabberManager.ts rename to src/Grabber/GrabberManager.ts index 5616aca..e51061a 100644 --- a/src/Grabber/StateGrabberManager.ts +++ b/src/Grabber/GrabberManager.ts @@ -1,14 +1,14 @@ -import { StateGrabber } from './StateGrabber'; -import { ResourceGrabber } from './ResourceGrabber'; +import { Grabber } from './Grabber'; +import { VillageResourceGrabber } from './VillageResourceGrabber'; import { VillageOverviewPageGrabber } from './VillageOverviewPageGrabber'; import { HeroPageGrabber } from './HeroPageGrabber'; -export class StateGrabberManager { - private readonly grabbers: Array = []; +export class GrabberManager { + private readonly grabbers: Array = []; constructor() { this.grabbers = []; - this.grabbers.push(new ResourceGrabber()); + this.grabbers.push(new VillageResourceGrabber()); this.grabbers.push(new VillageOverviewPageGrabber()); this.grabbers.push(new HeroPageGrabber()); } diff --git a/src/Grabber/HeroPageGrabber.ts b/src/Grabber/HeroPageGrabber.ts index 0f8315e..fc056f9 100644 --- a/src/Grabber/HeroPageGrabber.ts +++ b/src/Grabber/HeroPageGrabber.ts @@ -1,4 +1,4 @@ -import { StateGrabber } from './StateGrabber'; +import { Grabber } from './Grabber'; import { grabActiveVillageId, grabBuildingQueueInfo, @@ -12,7 +12,7 @@ import { BuildingQueueInfo } from '../Game'; import { HeroState } from '../State/HeroState'; import { grabHeroAttributes, grabHeroVillage } from '../Page/HeroPage'; -export class HeroPageGrabber extends StateGrabber { +export class HeroPageGrabber extends Grabber { grab(): void { const p = parseLocation(); if (p.pathname !== '/hero.php') { diff --git a/src/Grabber/ResourceGrabber.ts b/src/Grabber/ResourceGrabber.ts deleted file mode 100644 index fe7a428..0000000 --- a/src/Grabber/ResourceGrabber.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { StateGrabber } from './StateGrabber'; -import { grabActiveVillageId } from '../Page/VillageBlock'; -import { grabResources, grabResourceStorage } from '../Page/ResourcesBlock'; -import { VillageState } from '../State/VillageState'; - -export class ResourceGrabber extends StateGrabber { - grab(): void { - const villageId = grabActiveVillageId(); - const state = new VillageState(villageId); - state.storeResources(grabResources()); - state.storeResourceStorage(grabResourceStorage()); - } -} diff --git a/src/Grabber/StateGrabber.ts b/src/Grabber/StateGrabber.ts deleted file mode 100644 index 8be407a..0000000 --- a/src/Grabber/StateGrabber.ts +++ /dev/null @@ -1,3 +0,0 @@ -export abstract class StateGrabber { - abstract grab(): void; -} diff --git a/src/Grabber/VillageOverviewPageGrabber.ts b/src/Grabber/VillageOverviewPageGrabber.ts index 54c4ae9..8771995 100644 --- a/src/Grabber/VillageOverviewPageGrabber.ts +++ b/src/Grabber/VillageOverviewPageGrabber.ts @@ -1,11 +1,11 @@ -import { StateGrabber } from './StateGrabber'; +import { Grabber } from './Grabber'; import { grabActiveVillageId, grabBuildingQueueInfo, grabResourcesPerformance } from '../Page/VillageBlock'; import { VillageState } from '../State/VillageState'; import { parseLocation } from '../utils'; import { GrabError } from '../Errors'; import { BuildingQueueInfo } from '../Game'; -export class VillageOverviewPageGrabber extends StateGrabber { +export class VillageOverviewPageGrabber extends Grabber { grab(): void { const p = parseLocation(); if (p.pathname !== '/dorf1.php') { diff --git a/src/Grabber/VillageResourceGrabber.ts b/src/Grabber/VillageResourceGrabber.ts new file mode 100644 index 0000000..5e12bf4 --- /dev/null +++ b/src/Grabber/VillageResourceGrabber.ts @@ -0,0 +1,13 @@ +import { Grabber } from './Grabber'; +import { grabActiveVillageId } from '../Page/VillageBlock'; +import { grabVillageResources, grabVillageResourceStorage } from '../Page/ResourcesBlock'; +import { VillageState } from '../State/VillageState'; + +export class VillageResourceGrabber extends Grabber { + grab(): void { + const villageId = grabActiveVillageId(); + const state = new VillageState(villageId); + state.storeResources(grabVillageResources()); + state.storeResourceStorage(grabVillageResourceStorage()); + } +} diff --git a/src/Page/BuildingPage.ts b/src/Page/BuildingPage.ts index bfb4ab3..62271f1 100644 --- a/src/Page/BuildingPage.ts +++ b/src/Page/BuildingPage.ts @@ -1,6 +1,7 @@ import { GrabError } from '../Errors'; import { elClassId, getNumber, trimPrefix, uniqId } from '../utils'; import { Resources } from '../Core/Resources'; +import { Coordinates } from '../Core/Village'; export function clickBuildButton(typeId: number) { const section = jQuery(`#contract_building${typeId}`); @@ -102,3 +103,40 @@ export function createTrainTroopButtons( }); }); } + +export function createSendResourcesButton(onClickHandler: (resources: Resources, crd: Coordinates) => void) { + const id = uniqId(); + jQuery('#button').before(`
Отправить
`); + jQuery(`#${id}`).on('click', evt => { + evt.preventDefault(); + const sendSelect = jQuery('#send_select'); + const resources = new Resources( + getNumber(sendSelect.find('#r1').val()), + getNumber(sendSelect.find('#r2').val()), + getNumber(sendSelect.find('#r3').val()), + getNumber(sendSelect.find('#r4').val()) + ); + const crd = new Coordinates(getNumber(jQuery('#xCoordInput').val()), getNumber(jQuery('#yCoordInput').val())); + onClickHandler(resources, crd); + }); +} + +export function grabMerchantsInfo() { + const available = getNumber(jQuery('.merchantsAvailable').text()); + const carry = getNumber(jQuery('.carry b').text()); + return { available, carry }; +} + +export function fillSendResourcesForm(resources: Resources, crd: Coordinates) { + const sendSelect = jQuery('#send_select'); + sendSelect.find('#r1').val(resources.lumber); + sendSelect.find('#r2').val(resources.clay); + sendSelect.find('#r3').val(resources.iron); + sendSelect.find('#r4').val(resources.crop); + jQuery('#xCoordInput').val(crd.x); + jQuery('#yCoordInput').val(crd.y); +} + +export function clickSendButton() { + jQuery('#enabledButton').trigger('click'); +} diff --git a/src/Page/BuildingPageController.ts b/src/Page/BuildingPageController.ts index 916a7cb..76cec3f 100644 --- a/src/Page/BuildingPageController.ts +++ b/src/Page/BuildingPageController.ts @@ -4,10 +4,18 @@ import { Scheduler } from '../Scheduler'; import { TrainTroopTask } from '../Task/TrainTroopTask'; import { grabActiveVillageId } from './VillageBlock'; import { ConsoleLogger } from '../Logger'; -import { createBuildButton, createTrainTroopButtons, createUpgradeButton } from './BuildingPage'; +import { + createBuildButton, + createSendResourcesButton, + createTrainTroopButtons, + createUpgradeButton, +} 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; @@ -16,6 +24,7 @@ export interface BuildingPageAttributes { buildId: number; buildTypeId: number; categoryId: number; + sheetId: number; tabId: number; } @@ -31,8 +40,8 @@ export class BuildingPageController { } run() { - const { buildTypeId } = this.attributes; - this.logger.log('BUILD PAGE DETECTED', 'ID', this.attributes.buildId, 'TYPE', buildTypeId); + const { buildTypeId, sheetId, tabId } = this.attributes; + this.logger.log('BUILD PAGE DETECTED', 'ID', this.attributes.buildId, this.attributes); if (buildTypeId) { createUpgradeButton(res => this.onScheduleUpgradeBuilding(res)); @@ -48,9 +57,13 @@ export class BuildingPageController { createTrainTroopButtons((troopId, res, count) => this.onScheduleTrainTroopers(troopId, res, count)); } - if (buildTypeId === EMBASSY_ID && this.attributes.tabId === 1) { + if (buildTypeId === EMBASSY_ID && sheetId === 1) { createTrainTroopButtons((troopId, res, count) => this.onScheduleTrainTroopers(troopId, res, count)); } + + if (buildTypeId === MARKET_ID && tabId === 5) { + createSendResourcesButton((res, crd) => this.onSendResources(res, crd)); + } } private onScheduleBuildBuilding(buildTypeId: number, resources: Resources) { @@ -69,13 +82,12 @@ export class BuildingPageController { } private onScheduleTrainTroopers(troopId: number, resources: Resources, count: number) { - const tabId = this.attributes.tabId; for (let chunk of split(count)) { const args = { villageId: grabActiveVillageId(), buildId: this.attributes.buildId, buildTypeId: this.attributes.buildTypeId, - tabId, + sheetId: this.attributes.sheetId, troopId, resources: resources.scale(chunk), trainCount: chunk, @@ -85,4 +97,17 @@ export class BuildingPageController { } notify(`Training ${count} troopers scheduled`); } + + private onSendResources(resources: Resources, coordinates: Coordinates) { + const villageId = grabActiveVillageId(); + this.scheduler.scheduleTask(SendResourcesTask.name, { + villageId: villageId, + buildTypeId: this.attributes.buildTypeId, + buildId: this.attributes.buildId, + tabId: this.attributes.tabId, + resources, + coordinates, + }); + notify(`Send resources ${JSON.stringify(resources)} from ${villageId} to ${JSON.stringify(coordinates)}`); + } } diff --git a/src/Page/ResourcesBlock.ts b/src/Page/ResourcesBlock.ts index f51bf92..7a0eef1 100644 --- a/src/Page/ResourcesBlock.ts +++ b/src/Page/ResourcesBlock.ts @@ -4,7 +4,7 @@ import { Resources } from '../Core/Resources'; import { ResourceType } from '../Core/ResourceType'; import { ResourceStorage } from '../Core/ResourceStorage'; -export function grabResources(): Resources { +export function grabVillageResources(): Resources { const lumber = grabResource(ResourceType.Lumber); const clay = grabResource(ResourceType.Clay); const iron = grabResource(ResourceType.Iron); @@ -13,7 +13,7 @@ export function grabResources(): Resources { return new Resources(lumber, clay, iron, crop); } -export function grabResourceStorage() { +export function grabVillageResourceStorage() { const warehouse = grabCapacity('warehouse'); const granary = grabCapacity('granary'); return new ResourceStorage(warehouse, granary); diff --git a/src/Task/SendResourcesTask.ts b/src/Task/SendResourcesTask.ts new file mode 100644 index 0000000..1eec42a --- /dev/null +++ b/src/Task/SendResourcesTask.ts @@ -0,0 +1,31 @@ +import { Args, Command } from '../Command'; +import { Task } from '../Queue/TaskQueue'; +import { TaskController, registerTask } from './TaskController'; +import { GoToPageAction } from '../Action/GoToPageAction'; +import { CompleteTaskAction } from '../Action/CompleteTaskAction'; +import { path } from '../utils'; +import { SendResourcesAction } from '../Action/SendResourcesAction'; +import { ClickButtonAction } from '../Action/ClickButtonAction'; + +@registerTask +export class SendResourcesTask extends TaskController { + async run(task: Task) { + const args: Args = { ...task.args, taskId: task.id }; + + const pathArgs = { + newdid: args.villageId, + gid: args.buildTypeId || undefined, + id: args.buildId || undefined, + t: args.tabId, + }; + + const pagePath = path('/build.php', pathArgs); + + this.scheduler.scheduleActions([ + new Command(GoToPageAction.name, { ...args, path: pagePath }), + new Command(SendResourcesAction.name, args), + new Command(ClickButtonAction.name, { ...args, selector: '#enabledButton.green.sendRessources' }), + new Command(CompleteTaskAction.name, args), + ]); + } +} diff --git a/src/Task/TrainTroopTask.ts b/src/Task/TrainTroopTask.ts index 2a7d9bf..25cb926 100644 --- a/src/Task/TrainTroopTask.ts +++ b/src/Task/TrainTroopTask.ts @@ -15,7 +15,7 @@ export class TrainTroopTask extends TaskController { newdid: args.villageId, gid: args.buildTypeId || undefined, id: args.buildId || undefined, - s: args.tabId, + s: args.sheetId, }; const pagePath = path('/build.php', pathArgs); diff --git a/tests/Core/GatheringTimingsTest.ts b/tests/Core/GatheringTimingsTest.ts new file mode 100644 index 0000000..7c59991 --- /dev/null +++ b/tests/Core/GatheringTimingsTest.ts @@ -0,0 +1,59 @@ +import { it, describe } from 'mocha'; +import { expect } from 'chai'; + +import { Resources } from '../../src/Core/Resources'; +import { ResourceType } from '../../src/Core/ResourceType'; +import { ResourceStorage } from '../../src/Core/ResourceStorage'; +import { calcGatheringTimings, GatheringTimings } from '../../src/Core/GatheringTimings'; + +describe('Gathering timings', function() { + it('Can calc common from numbers', function() { + const timings = new GatheringTimings(10, 20, 15, 5); + expect(20).to.equals(timings.common); + }); + + it('Can calc common with never', function() { + const timings = new GatheringTimings(10, 20, 'never', 5); + expect('never').to.equals(timings.common); + expect(true).to.equals(timings.never); + }); + + it('Can calc common with all never', function() { + const timings = new GatheringTimings('never', 'never', 'never', 'never'); + expect('never').to.equals(timings.common); + expect(true).to.equals(timings.never); + }); + + it('Can calc timings', function() { + const resources = new Resources(10, 10, 10, 10); + const desired = new Resources(60, 60, 60, 60); + const speed = new Resources(5, 5, 5, 5); + const timings = calcGatheringTimings(resources, desired, speed); + expect(10).to.equals(timings.common); + }); + + it('Can calc timings with different speed', function() { + const resources = new Resources(10, 10, 10, 10); + const desired = new Resources(60, 60, 60, 60); + const speed = new Resources(5, 10, 25, 5); + const timings = calcGatheringTimings(resources, desired, speed); + expect(10).to.equals(timings.lumber); + expect(5).to.equals(timings.clay); + expect(2).to.equals(timings.iron); + expect(10).to.equals(timings.crop); + expect(10).to.equals(timings.common); + }); + + it('Can calc timings with negative speed', function() { + const resources = new Resources(10, 10, 10, 10); + const desired = new Resources(60, 60, 60, 60); + const speed = new Resources(5, 10, 25, -5); + const timings = calcGatheringTimings(resources, desired, speed); + expect(10).to.equals(timings.lumber); + expect(5).to.equals(timings.clay); + expect(2).to.equals(timings.iron); + expect('never').to.equals(timings.crop); + expect('never').to.equals(timings.common); + expect(true).to.equals(timings.never); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 56c874e..1982446 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,7 @@ "strictNullChecks": true, "strictPropertyInitialization": true, "target": "es2018", - "types": ["node", "url-parse", "jquery", "mocha", "chai"] + "types": ["node", "url-parse", "jquery", "mocha", "chai", "debounce"] }, "include": [ "./src/**/*"