392 lines
12 KiB
TypeScript
392 lines
12 KiB
TypeScript
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 { OrderedProductionQueues, ProductionQueue } from '../Core/ProductionQueue';
|
|
import { VillageTaskCollection } from './VillageTaskCollection';
|
|
import { TrainTroopTask } from '../Handler/Task/TrainTroopTask';
|
|
import { Args } from '../Queue/Args';
|
|
import { timestamp } from '../Helpers/Time';
|
|
import { isInQueue, TaskCore } from '../Queue/Task';
|
|
import { TaskId } from '../Queue/TaskId';
|
|
|
|
export interface TaskState {
|
|
id: TaskId;
|
|
name: string;
|
|
args: Args;
|
|
isEnoughWarehouseCapacity: boolean;
|
|
isEnoughGranaryCapacity: boolean;
|
|
canBeBuilt: boolean;
|
|
}
|
|
|
|
export interface TaskQueueState {
|
|
queue: ProductionQueue;
|
|
tasks: ReadonlyArray<TaskState>;
|
|
finishTs: number;
|
|
}
|
|
|
|
interface VillageProductionQueueState {
|
|
queue: ProductionQueue;
|
|
tasks: ReadonlyArray<TaskState>;
|
|
isActive: boolean;
|
|
isWaiting: boolean;
|
|
currentTaskFinishTimestamp: number;
|
|
currentTaskFinishSeconds: number;
|
|
firstTask: ResourceLineState;
|
|
allTasks: ResourceLineState;
|
|
taskCount: number;
|
|
}
|
|
|
|
interface VillageWarehouseState {
|
|
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 VillageOwnState {
|
|
/**
|
|
* Village id
|
|
*/
|
|
id: number;
|
|
/**
|
|
* Village object
|
|
*/
|
|
village: Village;
|
|
/**
|
|
* Current village resources
|
|
*/
|
|
resources: Resources;
|
|
performance: Resources;
|
|
storage: VillageWarehouseState;
|
|
queues: Array<VillageProductionQueueState>;
|
|
tasks: Array<TaskState>;
|
|
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
|
|
): VillageWarehouseState {
|
|
// @fixme Если у героя большая добыча ресурсов, а склад маленький, то значения получаются тож маленькими
|
|
// @fixme с одной деревней это не прокатывает, и даже не построить склад
|
|
// const optimumFullness = storage.sub(performance.scale(3));
|
|
// const criticalFullness = storage.sub(performance.scale(1));
|
|
const optimumFullness = storage.scale(0.9);
|
|
const criticalFullness = storage.scale(0.98);
|
|
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 createProductionQueueState(
|
|
taskQueueState: TaskQueueState,
|
|
storage: VillageWarehouseState
|
|
): VillageProductionQueueState {
|
|
const queue = taskQueueState.queue;
|
|
const tasks = taskQueueState.tasks;
|
|
const taskEndingTimestamp = taskQueueState.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,
|
|
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 getGroupedByQueueTasks(
|
|
tasks: ReadonlyArray<TaskState>,
|
|
storage: VillageStorage
|
|
): Array<TaskQueueState> {
|
|
const result: Array<TaskQueueState> = [];
|
|
for (let queue of OrderedProductionQueues) {
|
|
result.push({
|
|
queue,
|
|
tasks: tasks.filter(isInQueue(queue)),
|
|
finishTs: storage.getQueueTaskEnding(queue),
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function createTaskQueueStates(
|
|
warehouse: VillageWarehouseState,
|
|
tasks: ReadonlyArray<TaskState>,
|
|
storage: VillageStorage
|
|
) {
|
|
let result: Array<VillageProductionQueueState> = [];
|
|
const possibleTasks = tasks.filter(task => task.canBeBuilt);
|
|
for (let taskQueueInfo of getGroupedByQueueTasks(possibleTasks, storage)) {
|
|
result.push(createProductionQueueState(taskQueueInfo, warehouse));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function getReadyForProductionTask(
|
|
queues: ReadonlyArray<VillageProductionQueueState>
|
|
): 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: TaskCore | undefined): Resources {
|
|
if (task && task.args.resources) {
|
|
return Resources.fromObject(task.args.resources);
|
|
}
|
|
return Resources.zero();
|
|
}
|
|
|
|
function taskResourceReducer(resources: Resources, task: TaskCore) {
|
|
return task.args.resources
|
|
? resources.add(Resources.fromObject(task.args.resources))
|
|
: resources;
|
|
}
|
|
|
|
function makeTaskState(task: TaskCore, maxResourcesForTask: Resources): TaskState {
|
|
const taskResources = getTaskResources(task);
|
|
const isEnoughWarehouseCapacity = maxResourcesForTask.allGreaterOrEqual(
|
|
new Resources(taskResources.lumber, taskResources.clay, taskResources.iron, 0)
|
|
);
|
|
const isEnoughGranaryCapacity = maxResourcesForTask.allGreaterOrEqual(
|
|
new Resources(0, 0, 0, taskResources.crop)
|
|
);
|
|
|
|
const canBeBuilt = isEnoughWarehouseCapacity && isEnoughGranaryCapacity;
|
|
|
|
return {
|
|
id: task.id,
|
|
args: task.args,
|
|
name: task.name,
|
|
isEnoughWarehouseCapacity,
|
|
isEnoughGranaryCapacity,
|
|
canBeBuilt,
|
|
};
|
|
}
|
|
|
|
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 tasks = taskCollection
|
|
.getTasks()
|
|
.map(t => makeTaskState(t, storageState.optimumFullness));
|
|
const queues = createTaskQueueStates(storageState, tasks, storage);
|
|
const firstReadyTask = getReadyForProductionTask(queues);
|
|
const requiredResources = getTaskResources(firstReadyTask);
|
|
return {
|
|
id: village.id,
|
|
village,
|
|
resources,
|
|
performance,
|
|
storage: storageState,
|
|
required: makeResourceState(requiredResources, resources, performance),
|
|
tasks,
|
|
queues,
|
|
firstReadyTask,
|
|
incomingResources: calcIncomingResources(storage),
|
|
settings,
|
|
};
|
|
}
|
|
|
|
function createVillageOwnStates(
|
|
villages: Array<Village>,
|
|
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<Village>,
|
|
storageFactory: VillageStorageFactory,
|
|
taskCollectionFactory: VillageTaskCollectionFactory
|
|
): Array<VillageState> {
|
|
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<VillageState> {
|
|
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;
|
|
}
|
|
}
|