Rebuild village state calculation

This commit is contained in:
Anton Vakhrushev 2020-05-01 16:13:12 +03:00
parent 3d54294043
commit 129f107881
7 changed files with 226 additions and 145 deletions

View File

@ -1,4 +1,4 @@
import { parseLocation, timestamp, uniqId, waitForLoad } from './utils';
import { notify, parseLocation, timestamp, uniqId, waitForLoad } from './utils';
import { Scheduler } from './Scheduler';
import { BuildingPageController } from './Page/BuildingPageController';
import { UpgradeBuildingTask } from './Task/UpgradeBuildingTask';
@ -13,21 +13,35 @@ import Vue from 'vue';
import DashboardApp from './DashboardView/Dashboard.vue';
import { ResourcesToLevel } from './Task/ResourcesToLevel';
import { ConsoleLogger, Logger } from './Logger';
import { VillageStorage } from './Storage/VillageStorage';
import { Resources } from './Core/Resources';
import { Coordinates, Village } from './Core/Village';
import { calcGatheringTimings } from './Core/GatheringTimings';
import { DataStorage } from './DataStorage';
import { getBuildingPageAttributes, isBuildingPage } from './Page/PageDetectors';
import { debounce } from 'debounce';
import { ExecutionStorage } from './Storage/ExecutionStorage';
import { ResourceStorage } from './Core/ResourceStorage';
import { createVillageStates, VillageState } from './VillageState';
import { Task } from './Queue/TaskProvider';
import { Action } from './Queue/ActionQueue';
interface QuickAction {
label: string;
cb: () => void;
}
interface GameState {
name: string;
version: string;
activeVillageState: VillageState | undefined;
villageStates: ReadonlyArray<VillageState>;
taskList: ReadonlyArray<Task>;
actionList: ReadonlyArray<Action>;
quickActions: Array<QuickAction>;
pauseSeconds: number;
refresh(): void;
removeTask(taskId: string): void;
refreshVillages(): void;
pause(): void;
}
export class ControlPanel {
private readonly version: string;
private readonly scheduler: Scheduler;
@ -48,18 +62,17 @@ export class ControlPanel {
const villageId = grabActiveVillageId();
const scheduler = this.scheduler;
const quickActions: QuickAction[] = [];
const executionState = new ExecutionStorage();
const state: any = {
name: 'Dashboard',
const state: GameState = {
name: 'Control',
version: this.version,
activeVillage: {},
villages: [],
activeVillageState: undefined,
villageStates: [],
taskList: [],
actionList: [],
quickActions: quickActions,
quickActions: [],
pauseSeconds: 0,
refresh() {
@ -76,12 +89,10 @@ export class ControlPanel {
},
refreshVillages() {
this.villages = grabVillageList().map(village => {
return new VillageController(village, new VillageStorage(village.id), scheduler);
});
for (let village of this.villages) {
if (village.active) {
this.activeVillage = village;
this.villageStates = createVillageStates(grabVillageList(), scheduler);
for (let state of this.villageStates) {
if (state.village.active) {
this.activeVillageState = state;
}
}
},
@ -111,7 +122,7 @@ export class ControlPanel {
if (p.pathname === '/dorf1.php') {
showResourceSlotIds(buildingsInQueue);
onResourceSlotCtrlClick(buildId => this.onResourceSlotCtrlClick(villageId, buildId));
quickActions.push(...this.createDepositsQuickActions(villageId));
state.quickActions.push(...this.createDepositsQuickActions(villageId));
}
if (p.pathname === '/dorf2.php') {
@ -126,12 +137,12 @@ export class ControlPanel {
this.createControlPanel(state);
}
private createControlPanel(state: any) {
private createControlPanel(gameState: GameState) {
const appId = `app-${uniqId()}`;
jQuery('body').prepend(`<div id="${appId}"></div>`);
new Vue({
el: `#${appId}`,
data: state,
data: gameState,
render: h => h(DashboardApp),
});
}
@ -157,78 +168,6 @@ export class ControlPanel {
private onResourceSlotCtrlClick(villageId: number, buildId: number) {
this.scheduler.scheduleTask(UpgradeBuildingTask.name, { villageId, buildId });
const n = new Notification(`Building ${buildId} scheduled`);
setTimeout(() => n && n.close(), 4000);
}
}
class VillageController {
public readonly id: number;
public readonly name: string;
public readonly crd: Coordinates;
public readonly active: boolean;
public readonly lumber: number;
public readonly clay: number;
public readonly iron: number;
public readonly crop: number;
public readonly resources: Resources;
public readonly performance: Resources;
public readonly requiredResources: Resources;
public readonly requiredBalance: Resources;
public readonly totalRequiredResources: Resources;
public readonly totalRequiredBalance: Resources;
public readonly incomingResources: Resources;
public readonly storage: ResourceStorage;
public readonly warehouse: number;
public readonly granary: number;
public readonly buildRemainingSeconds: number;
constructor(village: Village, state: VillageStorage, scheduler: Scheduler) {
const resources = state.getResources();
const storage = state.getResourceStorage();
const performance = state.getResourcesPerformance();
const buildQueueInfo = state.getBuildingQueueInfo();
const requiredResources = scheduler.getVillageRequiredResources(village.id);
const totalRequiredResources = scheduler.getTotalVillageRequiredResources(village.id);
this.id = village.id;
this.name = village.name;
this.crd = village.crd;
this.active = village.active;
this.lumber = resources.lumber;
this.clay = resources.clay;
this.iron = resources.iron;
this.crop = resources.crop;
this.resources = resources;
this.performance = performance;
this.requiredResources = requiredResources;
this.requiredBalance = resources.sub(requiredResources);
this.totalRequiredResources = totalRequiredResources;
this.totalRequiredBalance = resources.sub(totalRequiredResources);
this.storage = storage;
this.warehouse = storage.warehouse;
this.granary = storage.granary;
this.buildRemainingSeconds = buildQueueInfo.seconds;
this.incomingResources = this.calcIncomingResources(state);
}
timeToRequired() {
return this.timeToResources(this.requiredResources);
}
timeToTotalRequired() {
return this.timeToResources(this.totalRequiredResources);
}
private timeToResources(resources: Resources): number {
const timings = calcGatheringTimings(this.resources, resources, this.performance);
if (timings.never) {
return -1;
}
return timings.hours * 3600;
}
private calcIncomingResources(state: VillageStorage): Resources {
return state.getIncomingMerchants().reduce((m, i) => m.add(i.resources), new Resources(0, 0, 0, 0));
notify(`Building ${buildId} scheduled`);
}
}

View File

@ -19,8 +19,8 @@ export default {
},
computed: {
villageName() {
let village = this.shared.activeVillage;
return village ? village.name : 'Unknown';
let state = this.shared.activeVillageState;
return state ? state.village.name : 'Unknown';
},
},
methods: {

View File

@ -25,13 +25,12 @@
<script>
import * as dateFormat from 'dateformat';
import { timestamp } from '../utils';
export default {
data() {
return {
shared: this.$root.$data,
activeVillage: this.$root.$data.activeVillage,
activeVillageState: this.$root.$data.activeVillageState,
};
},
computed: {
@ -49,7 +48,7 @@ export default {
},
isThisVillageTask(task) {
const taskVillageId = (task.args || {}).villageId;
const currentVillageId = this.activeVillage.id;
const currentVillageId = this.activeVillageState.id;
return taskVillageId !== undefined && taskVillageId === currentVillageId;
},
onRemove(taskId) {

View File

@ -13,39 +13,57 @@
</tr>
</thead>
<tbody>
<template v-for="village in shared.villages">
<template v-for="villageState in shared.villageStates">
<tr class="normal-line top-line">
<td :class="{ active: village.active }" :title="village.id">{{ village.name }}</td>
<td class="right">
<filling :value="village.lumber" :max="village.warehouse" :speed="village.performance.lumber"></filling>
<td :class="{ active: villageState.village.active }" :title="villageState.id">
{{ villageState.village.name }}
</td>
<td class="right">
<filling :value="village.clay" :max="village.warehouse" :speed="village.performance.clay"></filling>
<filling
:value="villageState.resources.lumber"
:max="villageState.storage.lumber"
:speed="villageState.performance.lumber"
></filling>
</td>
<td class="right">
<filling :value="village.iron" :max="village.warehouse" :speed="village.performance.iron"></filling>
<filling
:value="villageState.resources.clay"
:max="villageState.storage.clay"
:speed="villageState.performance.clay"
></filling>
</td>
<td class="right">
<filling :value="village.crop" :max="village.granary" :speed="village.performance.crop"></filling>
<filling
:value="villageState.resources.iron"
:max="villageState.storage.iron"
:speed="villageState.performance.iron"
></filling>
</td>
<td class="right">
<a :href="warehousePath(village)" v-text="village.warehouse"></a>
<filling
:value="villageState.resources.crop"
:max="villageState.storage.crop"
:speed="villageState.performance.crop"
></filling>
</td>
<td class="right" v-text="village.granary"></td>
<td class="right">
<a :href="warehousePath(villageState.village)" v-text="villageState.storage.lumber"></a>
</td>
<td class="right" v-text="villageState.storage.crop"></td>
</tr>
<tr class="performance-line">
<td class="right">Прирост:</td>
<td class="right">
<resource :value="village.performance.lumber"></resource>
<resource :value="villageState.performance.lumber"></resource>
</td>
<td class="right">
<resource :value="village.performance.clay"></resource>
<resource :value="villageState.performance.clay"></resource>
</td>
<td class="right">
<resource :value="village.performance.iron"></resource>
<resource :value="villageState.performance.iron"></resource>
</td>
<td class="right">
<resource :value="village.performance.crop"></resource>
<resource :value="villageState.performance.crop"></resource>
</td>
<td></td>
<td></td>
@ -54,7 +72,7 @@
<td class="right">След:</td>
<td class="right">
<resource
:value="village.requiredResources.lumber"
:value="villageState.required.resources.lumber"
:hide-zero="true"
:color="false"
:sign="false"
@ -62,7 +80,7 @@
</td>
<td class="right">
<resource
:value="village.requiredResources.clay"
:value="villageState.required.resources.clay"
:hide-zero="true"
:color="false"
:sign="false"
@ -70,7 +88,7 @@
</td>
<td class="right">
<resource
:value="village.requiredResources.iron"
:value="villageState.required.resources.iron"
:hide-zero="true"
:color="false"
:sign="false"
@ -78,62 +96,79 @@
</td>
<td class="right">
<resource
:value="village.requiredResources.crop"
:value="villageState.required.resources.crop"
:hide-zero="true"
:color="false"
:sign="false"
></resource>
</td>
<td class="right" v-text="secondsToTime(village.buildRemainingSeconds)"></td>
<td class="right" v-text="secondsToRequiredTime(villageState.buildRemainingSeconds)"></td>
<td></td>
</tr>
<tr class="required-line">
<td class="right">Баланс:</td>
<td class="right">
<resource :value="village.requiredBalance.lumber"></resource>
<resource :value="villageState.required.balance.lumber"></resource>
</td>
<td class="right">
<resource :value="village.requiredBalance.clay"></resource>
<resource :value="villageState.required.balance.clay"></resource>
</td>
<td class="right">
<resource :value="village.requiredBalance.iron"></resource>
<resource :value="villageState.required.balance.iron"></resource>
</td>
<td class="right">
<resource :value="village.requiredBalance.crop"></resource>
<resource :value="villageState.required.balance.crop"></resource>
</td>
<td class="right" v-text="timeToRequired(village)"></td>
<td class="right" v-text="secondsToRequiredTime(villageState.required.time)"></td>
<td></td>
</tr>
<tr class="required-line">
<td class="right">Баланс очереди:</td>
<td class="right">
<resource :value="village.totalRequiredBalance.lumber"></resource>
<resource :value="villageState.totalRequired.balance.lumber"></resource>
</td>
<td class="right">
<resource :value="village.totalRequiredBalance.clay"></resource>
<resource :value="villageState.totalRequired.balance.clay"></resource>
</td>
<td class="right">
<resource :value="village.totalRequiredBalance.iron"></resource>
<resource :value="villageState.totalRequired.balance.iron"></resource>
</td>
<td class="right">
<resource :value="village.totalRequiredBalance.crop"></resource>
<resource :value="villageState.totalRequired.balance.crop"></resource>
</td>
<td class="right" v-text="timeToTotalRequired(village)"></td>
<td class="right" v-text="secondsToRequiredTime(villageState.totalRequired.time)"></td>
<td></td>
</tr>
<tr class="commitments-line">
<td class="right">Обязательства:</td>
<td class="right">
<resource :value="villageState.commitments.lumber" :hide-zero="true"></resource>
</td>
<td class="right">
<resource :value="villageState.commitments.clay" :hide-zero="true"></resource>
</td>
<td class="right">
<resource :value="villageState.commitments.iron" :hide-zero="true"></resource>
</td>
<td class="right">
<resource :value="villageState.commitments.crop" :hide-zero="true"></resource>
</td>
<td></td>
<td></td>
</tr>
<tr class="incoming-line">
<td class="right">Торговцы:</td>
<td class="right">
<resource :value="village.incomingResources.lumber" :hide-zero="true"></resource>
<resource :value="villageState.incomingResources.lumber" :hide-zero="true"></resource>
</td>
<td class="right">
<resource :value="village.incomingResources.clay" :hide-zero="true"></resource>
<resource :value="villageState.incomingResources.clay" :hide-zero="true"></resource>
</td>
<td class="right">
<resource :value="village.incomingResources.iron" :hide-zero="true"></resource>
<resource :value="villageState.incomingResources.iron" :hide-zero="true"></resource>
</td>
<td class="right">
<resource :value="village.incomingResources.crop" :hide-zero="true"></resource>
<resource :value="villageState.incomingResources.crop" :hide-zero="true"></resource>
</td>
<td></td>
<td></td>
@ -143,14 +178,14 @@
<td class="right" colspan="6">
<a
class="village-quick-link"
v-for="v in shared.villages"
v-if="v.id !== village.id"
:href="marketPath(village, v)"
:title="'Отправить ресурсы из ' + village.name + ' в ' + v.name"
>->{{ v.name }}</a
v-for="s in shared.villageStates"
v-if="s.id !== villageState.id"
:href="marketPath(villageState.village, s.village)"
:title="'Отправить ресурсы из ' + villageState.village.name + ' в ' + s.village.name"
>->{{ s.village.name }}</a
>
<a class="village-quick-link" :href="quartersPath(village)">Казармы</a>
<a class="village-quick-link" :href="horseStablePath(village)">Конюшни</a>
<a class="village-quick-link" :href="quartersPath(villageState.village)">Казармы</a>
<a class="village-quick-link" :href="horseStablePath(villageState.village)">Конюшни</a>
</td>
</tr>
</template>
@ -173,7 +208,7 @@ export default {
data() {
return {
shared: this.$root.$data,
activeVillage: this.$root.$data.activeVillage,
activeVillageState: this.$root.$data.activeVillageState,
};
},
methods: {
@ -213,12 +248,6 @@ export default {
}
return this.secondsToTime(value);
},
timeToRequired(village) {
return this.secondsToRequiredTime(village.timeToRequired());
},
timeToTotalRequired(village) {
return this.secondsToRequiredTime(village.timeToTotalRequired());
},
},
};
</script>
@ -244,6 +273,7 @@ export default {
.performance-line td,
.required-line td,
.commitments-line td,
.incoming-line td {
padding: 0 4px 4px;
font-size: 90%;

View File

@ -6,6 +6,7 @@ export interface Args {
taskId?: TaskId;
targetTaskId?: TaskId;
villageId?: number;
targetVillageId?: number;
buildId?: number;
categoryId?: number;
sheetId?: number;

View File

@ -133,6 +133,18 @@ export class Scheduler {
return tasks.reduce((acc, t) => acc.add(t.args.resources!), new Resources(0, 0, 0, 0));
}
getResourceCommitments(villageId: number): Array<number> {
const tasks = this.taskQueue
.seeItems()
.filter(
t =>
t.name === SendResourcesTask.name &&
t.args.villageId === villageId &&
t.args.targetVillageId !== undefined
);
return tasks.map(t => t.args.targetVillageId!);
}
private reorderVillageTasks(villageId: number) {
const tasks = this.taskQueue.seeItems();
const trainPred = (t: Task) => isTrainTroopTask(t.name) && sameVillage(villageId, t.args);

100
src/VillageState.ts Normal file
View File

@ -0,0 +1,100 @@
import { Village } from './Core/Village';
import { Scheduler } from './Scheduler';
import { Resources } from './Core/Resources';
import { VillageStorage } from './Storage/VillageStorage';
import { calcGatheringTimings } from './Core/GatheringTimings';
interface RequiredResources {
resources: Resources;
balance: Resources;
time: number;
}
interface VillageOwnState {
id: number;
village: Village;
resources: Resources;
performance: Resources;
required: RequiredResources;
totalRequired: RequiredResources;
incomingResources: Resources;
storage: Resources;
buildRemainingSeconds: number;
}
interface VillageOwnStateDictionary {
[id: number]: VillageOwnState;
}
export interface VillageState extends VillageOwnState {
commitments: Resources;
}
function calcResourceBalance(resources: Resources, current: Resources, performance: Resources): RequiredResources {
return {
resources: resources,
balance: current.sub(resources),
time: timeToResources(current, resources, performance),
};
}
function timeToResources(current: Resources, desired: Resources, performance: Resources): number {
const timings = calcGatheringTimings(current, desired, performance);
if (timings.never) {
return -1;
}
return timings.hours * 3600;
}
function calcIncomingResources(storage: VillageStorage): Resources {
return storage.getIncomingMerchants().reduce((m, i) => m.add(i.resources), Resources.zero());
}
function createVillageOwnState(village: Village, scheduler: Scheduler): VillageOwnState {
const storage = new VillageStorage(village.id);
const resources = storage.getResources();
const resourceStorage = storage.getResourceStorage();
const performance = storage.getResourcesPerformance();
const buildQueueInfo = storage.getBuildingQueueInfo();
const requiredResources = scheduler.getVillageRequiredResources(village.id);
const totalRequiredResources = scheduler.getTotalVillageRequiredResources(village.id);
return {
id: village.id,
village,
resources,
performance,
required: calcResourceBalance(requiredResources, resources, performance),
totalRequired: calcResourceBalance(totalRequiredResources, resources, performance),
storage: Resources.fromStorage(resourceStorage),
buildRemainingSeconds: buildQueueInfo.seconds,
incomingResources: calcIncomingResources(storage),
};
}
function createVillageOwnStates(villages: Array<Village>, scheduler: Scheduler): VillageOwnStateDictionary {
const result: VillageOwnStateDictionary = {};
for (let village of villages) {
result[village.id] = createVillageOwnState(village, scheduler);
}
return result;
}
function createVillageState(
state: VillageOwnState,
ownStates: VillageOwnStateDictionary,
scheduler: Scheduler
): VillageState {
const villageIds = scheduler.getResourceCommitments(state.id);
const commitments = villageIds.reduce(
(res, villageId) => res.add(ownStates[villageId].required.balance),
Resources.zero()
);
return { ...state, commitments };
}
export function createVillageStates(villages: Array<Village>, scheduler: Scheduler): Array<VillageState> {
const ownStates = createVillageOwnStates(villages, scheduler);
return villages.map(village => createVillageState(ownStates[village.id], ownStates, scheduler));
}