Auto build warehouse, granary and crop fields

This commit is contained in:
Anton Vakhrushev 2020-07-01 18:06:11 +03:00
parent 7653c7b6e7
commit effc1b1626
14 changed files with 252 additions and 54 deletions

View File

@ -4,6 +4,7 @@ import { BuildingPageController } from './Page/BuildingPageController';
import { UpgradeBuildingTask } from './Task/UpgradeBuildingTask'; import { UpgradeBuildingTask } from './Task/UpgradeBuildingTask';
import { grabActiveVillageId } from './Page/VillageBlock'; import { grabActiveVillageId } from './Page/VillageBlock';
import { import {
grabBuildingSlots,
grabResourceSlots, grabResourceSlots,
onBuildingSlotCtrlClick, onBuildingSlotCtrlClick,
onResourceSlotCtrlClick, onResourceSlotCtrlClick,
@ -126,6 +127,7 @@ export class ControlPanel {
.map(t => t.args.buildId || 0); .map(t => t.args.buildId || 0);
if (p.pathname === '/dorf1.php') { if (p.pathname === '/dorf1.php') {
console.log('RSLOTS', grabResourceSlots());
showResourceSlotIds(getBuildingsInQueue()); showResourceSlotIds(getBuildingsInQueue());
state.quickActions.push(...this.createDepositsQuickActions(villageId)); state.quickActions.push(...this.createDepositsQuickActions(villageId));
onResourceSlotCtrlClick(buildId => { onResourceSlotCtrlClick(buildId => {
@ -135,6 +137,7 @@ export class ControlPanel {
} }
if (p.pathname === '/dorf2.php') { if (p.pathname === '/dorf2.php') {
console.log('BSLOTS', grabBuildingSlots());
showBuildingSlotIds(getBuildingsInQueue()); showBuildingSlotIds(getBuildingsInQueue());
onBuildingSlotCtrlClick(buildId => { onBuildingSlotCtrlClick(buildId => {
this.onSlotCtrlClick(villageId, buildId); this.onSlotCtrlClick(villageId, buildId);

View File

@ -1,11 +1,7 @@
<template> <template>
<table class="task-list"> <table class="task-list">
<tr v-for="task in tasks"> <tr v-for="task in tasks">
<td <td class="col-name" v-text="taskLabel(task)" :title="task.name + ', ' + task.id"></td>
class="col-name"
v-text="(task.canBeBuilt ? '' : '(!) ') + task.name"
:title="task.name + ', ' + task.id"
></td>
<td class="col-actions"> <td class="col-actions">
<a href="#" class="action" @click.prevent="upTask(task.id)" title="Поднять задачу">up</a> <a href="#" class="action" @click.prevent="upTask(task.id)" title="Поднять задачу">up</a>
<a href="#" class="action" @click.prevent="downTask(task.id)" title="Опустить задачу">dn</a> <a href="#" class="action" @click.prevent="downTask(task.id)" title="Опустить задачу">dn</a>
@ -30,6 +26,19 @@ export default {
props: ['villageId', 'tasks'], props: ['villageId', 'tasks'],
computed: {}, computed: {},
methods: { methods: {
taskLabel(task) {
let taskStatus = '';
if (!task.isEnoughWarehouseCapacity) {
taskStatus += 'w!';
}
if (!task.isEnoughGranaryCapacity) {
taskStatus += 'g!';
}
if (taskStatus) {
taskStatus = '(' + taskStatus + ') ';
}
return taskStatus + task.name;
},
upTask(taskId) { upTask(taskId) {
this.$store.dispatch(Actions.UpVillageTask, { villageId: this.villageId, taskId }); this.$store.dispatch(Actions.UpVillageTask, { villageId: this.villageId, taskId });
}, },

View File

@ -7,18 +7,34 @@ export class BuildingQueueInfo {
} }
} }
export interface ResourceSlot { export interface Slot {
readonly buildId: number; readonly buildId: number;
readonly type: ResourceType;
readonly level: number; readonly level: number;
readonly isReady: boolean; readonly isReady: boolean;
readonly isUnderConstruction: boolean; readonly isUnderConstruction: boolean;
readonly isMaxLevel: boolean; readonly isMaxLevel: boolean;
} }
export interface ResourceSlot extends Slot {
readonly type: ResourceType;
}
export const ResourceSlotDefaults: ResourceSlot = { export const ResourceSlotDefaults: ResourceSlot = {
buildId: 0,
type: ResourceType.Lumber, type: ResourceType.Lumber,
buildId: 0,
level: 0,
isReady: false,
isUnderConstruction: false,
isMaxLevel: false,
};
export interface BuildingSlot extends Slot {
readonly buildTypeId: number;
}
export const BuildingSlotDefaults: BuildingSlot = {
buildTypeId: 0,
buildId: 0,
level: 0, level: 0,
isReady: false, isReady: false,
isUnderConstruction: false, isUnderConstruction: false,

View File

@ -7,6 +7,7 @@ import { BuildingContractGrabber } from './BuildingContractGrabber';
import { ForgePageGrabber } from './ForgePageGrabber'; import { ForgePageGrabber } from './ForgePageGrabber';
import { GuildHallPageGrabber } from './GuildHallPageGrabber'; import { GuildHallPageGrabber } from './GuildHallPageGrabber';
import { VillageFactory } from '../VillageFactory'; import { VillageFactory } from '../VillageFactory';
import { VillageBuildingsPageGrabber } from './VillageBuildingsPageGrabber';
export class GrabberManager { export class GrabberManager {
private factory: VillageFactory; private factory: VillageFactory;
@ -28,6 +29,7 @@ export class GrabberManager {
const grabbers: Array<Grabber> = []; const grabbers: Array<Grabber> = [];
grabbers.push(new VillageResourceGrabber(taskCollection, storage)); grabbers.push(new VillageResourceGrabber(taskCollection, storage));
grabbers.push(new VillageOverviewPageGrabber(taskCollection, storage)); grabbers.push(new VillageOverviewPageGrabber(taskCollection, storage));
grabbers.push(new VillageBuildingsPageGrabber(taskCollection, storage));
grabbers.push(new HeroPageGrabber(taskCollection, storage)); grabbers.push(new HeroPageGrabber(taskCollection, storage));
grabbers.push(new MarketPageGrabber(taskCollection, storage)); grabbers.push(new MarketPageGrabber(taskCollection, storage));
grabbers.push(new BuildingContractGrabber(taskCollection, storage)); grabbers.push(new BuildingContractGrabber(taskCollection, storage));

View File

@ -0,0 +1,14 @@
import { Grabber } from './Grabber';
import { parseLocation } from '../utils';
import { grabBuildingSlots } from '../Page/SlotBlock';
export class VillageBuildingsPageGrabber extends Grabber {
grab(): void {
const p = parseLocation();
if (p.pathname !== '/dorf2.php') {
return;
}
this.storage.storeBuildingSlots(grabBuildingSlots());
}
}

View File

@ -1,5 +1,5 @@
import { elClassId, getNumber } from '../utils'; import { elClassId, getNumber } from '../utils';
import { ResourceSlot } from '../Game'; import { BuildingSlot, ResourceSlot } from '../Game';
import { numberToResourceType } from '../Core/ResourceType'; import { numberToResourceType } from '../Core/ResourceType';
interface SlotElement { interface SlotElement {
@ -90,3 +90,22 @@ function makeResourceSlot(slot: SlotElement): ResourceSlot {
export function grabResourceSlots(): Array<ResourceSlot> { export function grabResourceSlots(): Array<ResourceSlot> {
return slotElements('buildingSlot').map(makeResourceSlot); return slotElements('buildingSlot').map(makeResourceSlot);
} }
function makeBuildingSlot(slot: SlotElement): BuildingSlot {
const $el = jQuery(slot.el);
const classes = $el.attr('class');
const $parent = $el.closest('.buildingSlot');
const parentClasses = $parent.attr('class');
return {
buildId: getNumber(elClassId(classes, 'aid')),
buildTypeId: getNumber(elClassId(parentClasses, 'g')),
level: getNumber(elClassId(classes, 'level')),
isReady: !$el.hasClass('notNow'),
isUnderConstruction: $el.hasClass('underConstruction'),
isMaxLevel: $el.hasClass('maxLevel'),
};
}
export function grabBuildingSlots(): Array<BuildingSlot> {
return slotElements('aid').map(makeBuildingSlot);
}

View File

@ -20,7 +20,12 @@ export function uniqTaskId(): TaskId {
return 'tid.' + ts + '.' + String(idSequence).padStart(4, '0') + '.' + uniqId(''); return 'tid.' + ts + '.' + String(idSequence).padStart(4, '0') + '.' + uniqId('');
} }
export class Task { export interface TaskCore {
readonly name: string;
readonly args: Args;
}
export class Task implements TaskCore {
readonly id: TaskId; readonly id: TaskId;
readonly ts: number; readonly ts: number;
readonly name: string; readonly name: string;
@ -51,7 +56,26 @@ export interface TaskTransformer {
} }
export function isInQueue(queue: ProductionQueue): TaskMatcher { export function isInQueue(queue: ProductionQueue): TaskMatcher {
return (task: Task) => getProductionQueue(task.name) === queue; return (task: TaskCore) => getProductionQueue(task.name) === queue;
}
export function isBuildingPlanned(
name: string,
buildId: number | undefined,
buildTypeId: number | undefined
) {
return (task: TaskCore) => {
if (name !== task.name) {
return false;
}
if (buildId && task.args.buildId) {
return buildId === task.args.buildId;
}
if (buildTypeId && task.args.buildTypeId) {
return buildTypeId === task.args.buildTypeId;
}
return false;
};
} }
export function withTime(ts: number): TaskTransformer { export function withTime(ts: number): TaskTransformer {

View File

@ -6,7 +6,7 @@ import { VillageSettings, VillageSettingsDefaults } from '../Core/Village';
import { ProductionQueue } from '../Core/ProductionQueue'; import { ProductionQueue } from '../Core/ProductionQueue';
import { getNumber } from '../utils'; import { getNumber } from '../utils';
import { Task, uniqTaskId } from '../Queue/TaskProvider'; import { Task, uniqTaskId } from '../Queue/TaskProvider';
import { ResourceSlot, ResourceSlotDefaults } from '../Game'; import { BuildingSlot, BuildingSlotDefaults, ResourceSlot, ResourceSlotDefaults } from '../Game';
const RESOURCES_KEY = 'resources'; const RESOURCES_KEY = 'resources';
const CAPACITY_KEY = 'capacity'; const CAPACITY_KEY = 'capacity';
@ -96,6 +96,16 @@ export class VillageStorage {
this.storage.set(RESOURCE_SLOTS_KEY, slots); this.storage.set(RESOURCE_SLOTS_KEY, slots);
} }
getBuildingSlots(): ReadonlyArray<BuildingSlot> {
return this.storage.getTypedList<BuildingSlot>(BUILDING_SLOTS_KEY, {
factory: () => Object.assign({}, BuildingSlotDefaults),
});
}
storeBuildingSlots(slots: ReadonlyArray<BuildingSlot>): void {
this.storage.set(BUILDING_SLOTS_KEY, slots);
}
getSettings(): VillageSettings { getSettings(): VillageSettings {
return this.storage.getTyped<VillageSettings>(SETTINGS_KEY, { return this.storage.getTyped<VillageSettings>(SETTINGS_KEY, {
factory: () => Object.assign({}, VillageSettingsDefaults), factory: () => Object.assign({}, VillageSettingsDefaults),

View File

@ -13,6 +13,15 @@ export function goToResourceViewPage(villageId: number): ActionDefinition {
}; };
} }
export function goToBuildingsViewPage(villageId: number): ActionDefinition {
return {
name: GoToPageAction.name,
args: {
path: path('/dorf2.php', { newdid: villageId }),
},
};
}
export function goToMarketSendResourcesPage(villageId: number): ActionDefinition { export function goToMarketSendResourcesPage(villageId: number): ActionDefinition {
return { return {
name: GoToPageAction.name, name: GoToPageAction.name,
@ -44,6 +53,7 @@ export function scanAllVillagesBundle(villages: Array<Village>): Array<ActionDef
const actions: Array<ActionDefinition> = []; const actions: Array<ActionDefinition> = [];
for (let village of villages) { for (let village of villages) {
actions.push(goToResourceViewPage(village.id)); actions.push(goToResourceViewPage(village.id));
actions.push(goToBuildingsViewPage(village.id));
actions.push(goToMarketSendResourcesPage(village.id)); actions.push(goToMarketSendResourcesPage(village.id));
actions.push(goToForgePage(village.id)); actions.push(goToForgePage(village.id));
actions.push(goToGuildHallPage(village.id)); actions.push(goToGuildHallPage(village.id));

View File

@ -1,5 +1,5 @@
import { VillageTaskCollection } from './VillageTaskCollection'; import { VillageTaskCollection } from './VillageTaskCollection';
import { TaskId } from './Queue/TaskProvider'; import { isBuildingPlanned, TaskId } from './Queue/TaskProvider';
import { Args } from './Queue/Args'; import { Args } from './Queue/Args';
import { TaskState, VillageState } from './VillageState'; import { TaskState, VillageState } from './VillageState';
import { Resources } from './Core/Resources'; import { Resources } from './Core/Resources';
@ -9,6 +9,7 @@ import { ReceiveResourcesMode } from './Core/Village';
import { ResourceType } from './Core/ResourceType'; import { ResourceType } from './Core/ResourceType';
import { UpgradeBuildingTask } from './Task/UpgradeBuildingTask'; import { UpgradeBuildingTask } from './Task/UpgradeBuildingTask';
import * as _ from 'underscore'; import * as _ from 'underscore';
import { GARNER_ID, WAREHOUSE_ID } from './Core/Buildings';
export class VillageController { export class VillageController {
private readonly villageId: number; private readonly villageId: number;
@ -152,39 +153,49 @@ export class VillageController {
} }
planTasks(): void { planTasks(): void {
const performance = this.state.performance; if (this.state.tasks.length >= 100) {
return;
if (performance.crop < 100) {
this.planCropBuilding();
} }
this.planCropBuilding();
this.planWarehouseBuilding();
this.planGranaryBuilding();
} }
private planCropBuilding() { private planCropBuilding() {
const performance = this.state.performance;
if (performance.crop >= 100) {
return;
}
const resourceSlots = this.storage.getResourceSlots(); const resourceSlots = this.storage.getResourceSlots();
const tasks = this.taskCollection.getTasks(); const tasks = this.taskCollection.getTasks();
const cropSlots = resourceSlots.filter(s => s.type === ResourceType.Crop); const cropSlots = resourceSlots.filter(s => s.type === ResourceType.Crop && !s.isMaxLevel);
if (cropSlots.length === 0) {
return;
}
// Check, if crop field is building now // Check, if crop field is building now
const isCropBuilding = cropSlots.filter(s => s.isUnderConstruction); const underContraction = cropSlots.find(s => s.isUnderConstruction);
if (isCropBuilding) { if (underContraction !== undefined) {
return; return;
} }
// Check, if we already have crop task in queue // Check, if we already have crop task in queue
const cropBuildIds = cropSlots.map(s => s.buildId); const cropBuildIds = cropSlots.map(s => s.buildId);
const cropBuildingTaskInQueue = tasks.find( for (let buildId of cropBuildIds) {
t => t.args.buildId && cropBuildIds.includes(t.args.buildId) const upgradeTask = tasks.find(
); isBuildingPlanned(UpgradeBuildingTask.name, buildId, undefined)
if (cropBuildingTaskInQueue !== undefined) { );
return; if (upgradeTask !== undefined) {
return;
}
} }
// Find ready for building slots and sort them by level // Find ready for building slots and sort them by level
const readyCropSlots = cropSlots.filter(s => !s.isMaxLevel); cropSlots.sort((s1, s2) => s1.level - s2.level);
readyCropSlots.sort((s1, s2) => s1.level - s2.level);
const targetCropBuildId = _.first(readyCropSlots)?.buildId; const targetCropBuildId = _.first(cropSlots)?.buildId;
if (!targetCropBuildId) { if (!targetCropBuildId) {
return; return;
} }
@ -193,4 +204,55 @@ export class VillageController {
buildId: targetCropBuildId, buildId: targetCropBuildId,
}); });
} }
private planWarehouseBuilding(): void {
this.planStorageBuilding(WAREHOUSE_ID, t => !t.isEnoughWarehouseCapacity);
}
private planGranaryBuilding(): void {
this.planStorageBuilding(GARNER_ID, t => !t.isEnoughGranaryCapacity);
}
private planStorageBuilding(
buildTypeId: number,
checkNeedEnlargeFunc: (task: TaskState) => boolean
): void {
const buildingSlots = this.storage.getBuildingSlots();
const storageSlots = buildingSlots.filter(
s => s.buildTypeId === buildTypeId && !s.isMaxLevel
);
if (storageSlots.length === 0) {
return;
}
// Check, if storage is building now
const underConstruction = storageSlots.find(s => s.isUnderConstruction);
if (underConstruction !== undefined) {
return;
}
const tasks = this.state.tasks;
// Check, if we have storage is in building queue
const storageBuildIds = storageSlots.map(s => s.buildId);
for (let buildId of storageBuildIds) {
const upgradeTask = tasks.find(
isBuildingPlanned(UpgradeBuildingTask.name, buildId, buildTypeId)
);
if (upgradeTask !== undefined) {
return;
}
}
const needStorageEnlargeTasks = tasks.filter(checkNeedEnlargeFunc);
if (needStorageEnlargeTasks.length === 0) {
return;
}
const firstSlot = _.first(storageSlots);
if (firstSlot) {
this.addTask(UpgradeBuildingTask.name, { buildId: firstSlot.buildId, buildTypeId });
}
}
} }

View File

@ -15,6 +15,8 @@ export interface TaskState {
id: TaskId; id: TaskId;
name: string; name: string;
args: Args; args: Args;
isEnoughWarehouseCapacity: boolean;
isEnoughGranaryCapacity: boolean;
canBeBuilt: boolean; canBeBuilt: boolean;
} }
@ -222,11 +224,29 @@ function getTaskResources(task: Task | TaskState | undefined): Resources {
} }
function makeTaskState(task: Task, maxResourcesForTask: Resources): TaskState { function makeTaskState(task: Task, maxResourcesForTask: Resources): TaskState {
const taskResources = getTaskResources(task);
const taskWarehouseRequirements = new Resources(
taskResources.lumber,
taskResources.clay,
taskResources.iron,
0
);
const isEnoughWarehouseCapacity = maxResourcesForTask.allGreaterOrEqual(
taskWarehouseRequirements
);
const taskGranaryRequirements = new Resources(0, 0, 0, taskResources.crop);
const isEnoughGranaryCapacity = maxResourcesForTask.allGreaterOrEqual(taskGranaryRequirements);
const canBeBuilt = isEnoughWarehouseCapacity && isEnoughGranaryCapacity;
return { return {
id: task.id, id: task.id,
args: task.args, args: task.args,
name: task.name, name: task.name,
canBeBuilt: maxResourcesForTask.allGreaterOrEqual(getTaskResources(task)), isEnoughWarehouseCapacity,
isEnoughGranaryCapacity,
canBeBuilt,
}; };
} }

View File

@ -56,13 +56,7 @@ export class VillageTaskCollection {
throw new Error(`Task "${name}" is not production task`); throw new Error(`Task "${name}" is not production task`);
} }
if (args.villageId !== this.villageId) { return new Task(uniqTaskId(), 0, name, { ...args, villageId: this.villageId });
throw new Error(
`Task village id (${args.villageId}) not equal controller village id (${this.villageId}`
);
}
return new Task(uniqTaskId(), 0, name, { villageId: this.villageId, ...args });
} }
removeTask(taskId: TaskId) { removeTask(taskId: TaskId) {

View File

@ -59,8 +59,9 @@ export function elClassId(classes: string | undefined, prefix: string): number |
} }
let result: number | undefined = undefined; let result: number | undefined = undefined;
classes.split(/\s/).forEach(part => { classes.split(/\s/).forEach(part => {
if (part.startsWith(prefix)) { const match = part.match(new RegExp(prefix + '(\\d+)'));
result = toNumber(part.substr(prefix.length)); if (match) {
result = toNumber(match[1]);
} }
}); });
return result; return result;

View File

@ -1,25 +1,39 @@
import { it, describe } from 'mocha'; import { it, describe } from 'mocha';
import { expect } from 'chai'; import { expect } from 'chai';
import { getNumber } from '../src/utils'; import { elClassId, getNumber } from '../src/utils';
describe('Utils', function() { describe('Utils', function() {
it('Can parse positive number', function() { describe('getNumber', function() {
const text = '123'; it('Can parse positive number', function() {
expect(getNumber(text)).to.be.equals(123); const text = '123';
expect(getNumber(text)).to.be.equals(123);
});
it('Can parse positive number with noise', function() {
const text = ' 123 ';
expect(getNumber(text)).to.be.equals(123);
});
it('Can parse negative number', function() {
const text = '-146';
expect(getNumber(text)).to.be.equals(-146);
});
it('Can parse negative number with minus sign', function() {
const text = '\u2212132';
expect(getNumber(text)).to.be.equals(-132);
});
}); });
it('Can parse positive number with noise', function() { describe('elClassId', function() {
const text = ' 123 '; it('Can parse number with prefix', function() {
expect(getNumber(text)).to.be.equals(123); const text = 'foo bar12 baz';
}); expect(elClassId(text, 'bar')).to.be.equals(12);
});
it('Can parse negative number', function() { it('Can parse number from parts with same prefix', function() {
const text = '-146'; const text = 'foo12 foobar';
expect(getNumber(text)).to.be.equals(-146); expect(elClassId(text, 'foo')).to.be.equals(12);
}); });
it('Can parse negative number with minus sign', function() {
const text = '\u2212132';
expect(getNumber(text)).to.be.equals(-132);
}); });
}); });