Store incoming merchants
This commit is contained in:
		| @@ -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)); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										6
									
								
								src/Core/Buildings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/Core/Buildings.ts
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										10
									
								
								src/Core/Market.ts
									
									
									
									
									
										Normal 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; | ||||
|     } | ||||
| } | ||||
| @@ -85,6 +85,23 @@ | ||||
|             <td class="right" v-text="timeToTotalRequired(village)"></td> | ||||
|             <td></td> | ||||
|           </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"> | ||||
|             <td></td> | ||||
|             <td class="right" colspan="6"> | ||||
| @@ -97,6 +114,7 @@ | ||||
|                 >->{{ v.name }}</a | ||||
|               > | ||||
|               <a class="village-quick-link" :href="quartersPath(village)">Казармы</a> | ||||
|               <a class="village-quick-link" :href="horseStablePath(village)">Конюшни</a> | ||||
|             </td> | ||||
|           </tr> | ||||
|         </template> | ||||
| @@ -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%; | ||||
| } | ||||
|   | ||||
| @@ -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<string>) { | ||||
|     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 { | ||||
|     private readonly logger: Logger; | ||||
|     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 { | ||||
|         const fullKey = join(NAMESPACE, this.name, key); | ||||
|         return storage.getItem(fullKey) !== null; | ||||
|   | ||||
| @@ -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<Grabber> = []; | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -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; | ||||
|         } | ||||
|  | ||||
|   | ||||
							
								
								
									
										17
									
								
								src/Grabber/MarketPageGrabber.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/Grabber/MarketPageGrabber.ts
									
									
									
									
									
										Normal 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()); | ||||
|     } | ||||
| } | ||||
| @@ -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<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; | ||||
| } | ||||
|   | ||||
| @@ -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)); | ||||
|         } | ||||
|     } | ||||
|   | ||||
							
								
								
									
										42
									
								
								src/Page/PageDetectors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/Page/PageDetectors.ts
									
									
									
									
									
										Normal 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; | ||||
| } | ||||
| @@ -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<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)); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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)); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user