import { Village, VillageSettings } from './Core/Village'; import { Resources } from './Core/Resources'; import { VillageStorage } from './Storage/VillageStorage'; import { calcGatheringTimings, GatheringTime } from './Core/GatheringTimings'; import { VillageRepositoryInterface } from './VillageRepository'; import { VillageNotFound } from './Errors'; import { ProductionQueue } from './Core/ProductionQueue'; import { Task, TaskId } from './Queue/TaskProvider'; import { timestamp } from './utils'; import { QueueTasks, VillageTaskCollection } from './VillageTaskCollection'; import { TrainTroopTask } from './Task/TrainTroopTask'; import { Args } from './Queue/Args'; export interface TaskState { id: TaskId; name: string; args: Args; canBeBuilt: boolean; } interface VillageStorageState { resources: Resources; capacity: Resources; balance: Resources; performance: Resources; timeToZero: GatheringTime; timeToFull: GatheringTime; optimumFullness: Resources; criticalFullness: Resources; isOverflowing: boolean; } /** * State of one or more tasks, which required some resources. */ interface ResourceLineState { /** * Required resources (always positive) */ resources: Resources; /** * Balance resources (current - required, may be negative) */ balance: Resources; /** * Time to gather all type of resources (slowest time) */ time: GatheringTime; /** * Is active - resources not equals to zero. */ active: boolean; } interface VillageProductionQueueState { queue: ProductionQueue; tasks: Array; isActive: boolean; isWaiting: boolean; currentTaskFinishTimestamp: number; currentTaskFinishSeconds: number; firstTask: ResourceLineState; allTasks: ResourceLineState; taskCount: number; } interface VillageOwnState { /** * Village id */ id: number; /** * Village object */ village: Village; /** * Current village resources */ resources: Resources; performance: Resources; storage: VillageStorageState; queues: Array; tasks: Array; firstReadyTask: TaskState | undefined; /** * Required resources for nearest task */ required: ResourceLineState; incomingResources: Resources; settings: VillageSettings; } interface VillageOwnStateDictionary { [id: number]: VillageOwnState; } export interface VillageState extends VillageOwnState { /** * Resource commitments of this village to other (may be negative) */ commitments: Resources; } function makeResourceState( resources: Resources, current: Resources, performance: Resources ): ResourceLineState { return { resources, balance: current.sub(resources), time: timeToAllResources(current, resources, performance), active: !resources.empty(), }; } function makeStorageState( resources: Resources, storage: Resources, performance: Resources ): VillageStorageState { const optimumFullness = storage.sub(performance.scale(3)); const criticalFullness = storage.sub(performance.scale(1)); return { resources, capacity: storage, performance, balance: storage.sub(resources), timeToZero: timeToFastestResource(resources, Resources.zero(), performance), timeToFull: timeToFastestResource(resources, storage, performance), optimumFullness: optimumFullness, criticalFullness: criticalFullness, isOverflowing: criticalFullness.anyLower(resources), }; } function timeToAllResources( current: Resources, desired: Resources, performance: Resources ): GatheringTime { const timings = calcGatheringTimings(current, desired, performance); return timings.slowest; } function timeToFastestResource( current: Resources, desired: Resources, performance: Resources ): GatheringTime { const timings = calcGatheringTimings(current, desired, performance); return timings.fastest; } function calcIncomingResources(storage: VillageStorage): Resources { return storage.getIncomingMerchants().reduce((m, i) => m.add(i.resources), Resources.zero()); } function taskResourceReducer(resources: Resources, task: Task) { return task.args.resources ? resources.add(Resources.fromObject(task.args.resources)) : resources; } function createProductionQueueState( taskQueueInfo: QueueTasks, storage: VillageStorageState ): VillageProductionQueueState { const queue = taskQueueInfo.queue; const tasks = taskQueueInfo.tasks; const taskEndingTimestamp = taskQueueInfo.finishTs; const resources = storage.resources; const performance = storage.performance; const firstTaskResources = tasks.slice(0, 1).reduce(taskResourceReducer, Resources.zero()); const allTaskResources = tasks.reduce(taskResourceReducer, Resources.zero()); const currentTimestamp = timestamp(); return { queue, tasks: tasks.map(t => makeTaskState(t, storage.optimumFullness)), isActive: tasks.length !== 0 || taskEndingTimestamp > currentTimestamp, isWaiting: tasks.length !== 0 && taskEndingTimestamp < currentTimestamp, currentTaskFinishTimestamp: taskEndingTimestamp, currentTaskFinishSeconds: Math.max( taskEndingTimestamp ? taskEndingTimestamp - currentTimestamp : 0, 0 ), firstTask: makeResourceState(firstTaskResources, resources, performance), allTasks: makeResourceState(allTaskResources, resources, performance), taskCount: tasks.length, }; } function createAllProductionQueueStates( storage: VillageStorageState, taskCollection: VillageTaskCollection ) { let result: Array = []; for (let taskQueueInfo of taskCollection.getGroupedByQueueTasks()) { result.push(createProductionQueueState(taskQueueInfo, storage)); } return result; } function getReadyForProductionTask( queues: ReadonlyArray ): TaskState | undefined { const firstReadyQueue = queues.find(queue => queue.isWaiting); if (!firstReadyQueue) { return undefined; } return firstReadyQueue.tasks.find(task => task.name === TrainTroopTask.name || task.canBeBuilt); } function getTaskResources(task: Task | TaskState | undefined): Resources { if (task && task.args.resources) { return Resources.fromObject(task.args.resources); } return Resources.zero(); } function makeTaskState(task: Task, maxResourcesForTask: Resources): TaskState { return { id: task.id, args: task.args, name: task.name, canBeBuilt: maxResourcesForTask.allGreaterOrEqual(getTaskResources(task)), }; } function createVillageOwnState( village: Village, storage: VillageStorage, taskCollection: VillageTaskCollection ): VillageOwnState { const settings = storage.getSettings(); const resources = storage.getResources(); const storageResources = Resources.fromStorage(storage.getResourceStorage()); const performance = storage.getResourcesPerformance(); const storageState = makeStorageState(resources, storageResources, performance); const queues = createAllProductionQueueStates(storageState, taskCollection); const firstReadyTask = getReadyForProductionTask(queues); const requiredResources = getTaskResources(firstReadyTask); return { id: village.id, village, resources, performance, storage: storageState, required: makeResourceState(requiredResources, resources, performance), queues, tasks: taskCollection.getTasks().map(t => makeTaskState(t, storageState.optimumFullness)), firstReadyTask, incomingResources: calcIncomingResources(storage), settings, }; } function createVillageOwnStates( villages: Array, storageFactory: VillageStorageFactory, taskCollectionFactory: VillageTaskCollectionFactory ): VillageOwnStateDictionary { const result: VillageOwnStateDictionary = {}; for (let village of villages) { result[village.id] = createVillageOwnState( village, storageFactory(village.id), taskCollectionFactory(village.id) ); } return result; } function createVillageState( state: VillageOwnState, ownStates: VillageOwnStateDictionary ): VillageState { const villageIds = Object.keys(ownStates).map(k => +k); const commitments = villageIds.reduce((memo, shipmentVillageId) => { const shipmentVillageState = ownStates[shipmentVillageId]; const shipmentVillageRequired = shipmentVillageState.required; const shipmentVillageIncoming = shipmentVillageState.incomingResources; const targetVillageMissing = shipmentVillageRequired.balance .add(shipmentVillageIncoming) .min(Resources.zero()); return memo.add(targetVillageMissing); }, Resources.zero()); return { ...state, commitments }; } function getVillageStates( villages: Array, storageFactory: VillageStorageFactory, taskCollectionFactory: VillageTaskCollectionFactory ): Array { const ownStates = createVillageOwnStates(villages, storageFactory, taskCollectionFactory); return villages.map(village => createVillageState(ownStates[village.id], ownStates)); } interface VillageStorageFactory { (villageId: number): VillageStorage; } interface VillageTaskCollectionFactory { (villageId: number): VillageTaskCollection; } export class VillageStateFactory { private readonly villageRepository: VillageRepositoryInterface; private readonly storageFactory: VillageStorageFactory; private readonly taskCollectionFactory: VillageTaskCollectionFactory; constructor( villageRepository: VillageRepositoryInterface, storageFactory: VillageStorageFactory, taskCollectionFactory: VillageTaskCollectionFactory ) { this.villageRepository = villageRepository; this.storageFactory = storageFactory; this.taskCollectionFactory = taskCollectionFactory; } getAllVillageStates(): Array { return getVillageStates( this.villageRepository.all(), this.storageFactory, this.taskCollectionFactory ); } getVillageState(villageId: number): VillageState { const states = getVillageStates( this.villageRepository.all(), this.storageFactory, this.taskCollectionFactory ); const needle = states.find(s => s.id === villageId); if (!needle) { throw new VillageNotFound(`Village ${villageId} not found`); } return needle; } }