Add quick actions and mass resources to level upgrade

This commit is contained in:
Anton Vakhrushev 2020-04-12 16:41:23 +03:00
parent 9d37697b13
commit ed66b0a308
14 changed files with 250 additions and 36 deletions

View File

@ -0,0 +1,51 @@
import { ActionController, registerAction } from './ActionController';
import { Args } from '../Common';
import { ActionError, GrabError, TryLaterError } from '../Errors';
import { Task } from '../Storage/TaskQueue';
import { clickUpgradeButton } from '../Page/BuildingPage';
import { grabResourceDeposits } from '../Page/SlotBlock';
import { UpgradeBuildingTask } from '../Task/UpgradeBuildingTask';
@registerAction
export class UpgradeResourceToLevel extends ActionController {
async run(args: Args, task: Task): Promise<any> {
const deposits = grabResourceDeposits();
if (deposits.length === 0) {
throw new ActionError(task.id, 'No deposits');
}
const villageId = args.villageId;
const requiredLevel = args.level;
const tasks = this.scheduler.getTaskItems();
const allUpgraded = deposits.reduce((memo, dep) => memo && dep.level >= requiredLevel, true);
if (allUpgraded) {
this.scheduler.completeTask(task.id);
return;
}
const available = deposits
.sort((x, y) => x.level - y.level)
.filter(dep => dep.ready)
.filter(
dep =>
tasks.find(
t =>
t.name === UpgradeBuildingTask.name &&
t.args.villageId === villageId &&
t.args.buildId === dep.buildId
) === undefined
);
if (available.length === 0) {
throw new TryLaterError(task.id, 10 * 60, 'No available deposits');
}
const targetDep = available[0];
this.scheduler.scheduleTask(UpgradeBuildingTask.name, { villageId, buildId: targetDep.buildId });
throw new TryLaterError(task.id, 20 * 60, 'Sleep for next round');
}
}

View File

@ -1,4 +1,6 @@
export interface Args {
villageId?: number;
buildId?: number;
[name: string]: any;
}

View File

@ -3,6 +3,8 @@
<div id="dashboard-inner">
<hdr></hdr>
<task-list></task-list>
<hr class="separator" />
<quick-actions></quick-actions>
</div>
</main>
</template>
@ -10,10 +12,12 @@
<script>
import Header from './Header';
import TaskList from './TaskList';
import QuickActions from './QuickActions';
export default {
components: {
hdr: Header,
'task-list': TaskList,
'quick-actions': QuickActions,
},
data() {
return {};
@ -38,5 +42,9 @@ export default {
#dashboard-inner {
background-color: white;
margin: 4px;
padding-bottom: 10px;
}
.separator {
margin: 10px auto;
}
</style>

View File

@ -0,0 +1,34 @@
<template>
<ul class="actions">
<li v-for="action in actions">
<a href="#" v-on:click.prevent="onAction(action.cb)">{{ action.label }}</a>
</li>
</ul>
</template>
<script>
export default {
data() {
return {
shared: this.$root.$data,
};
},
computed: {
actions() {
return this.shared.quickActions;
},
},
methods: {
onAction(cb) {
cb();
},
},
};
</script>
<style scoped>
.actions {
margin: 10px auto;
padding-inline-start: 20px;
}
</style>

View File

@ -3,9 +3,12 @@
<p class="summary">Task count: {{ shared.taskList.length }}</p>
<div class="container">
<table class="task-table">
<tr class="task-item" v-for="task in shared.taskList">
<tr v-for="task in shared.taskList" class="task-item" :class="{ 'this-village': isThisVillageTask(task) }">
<td :title="formatDate(task.ts)">{{ formatDate(task.ts) }}</td>
<td :title="task.id">{{ task.id }}</td>
<td>
<a href="#" title="Remove task" class="remove-action" v-on:click.prevent="onRemove(task.id)">&times;</a>
</td>
<td :title="task.name">{{ task.name }}</td>
<td :title="JSON.stringify(task.args)">{{ JSON.stringify(task.args) }}</td>
</tr>
@ -23,11 +26,21 @@ export default {
shared: this.$root.$data,
};
},
computed: {},
methods: {
formatDate(ts) {
const d = new Date(ts * 1000);
return dateFormat(d, 'HH:MM:ss');
},
isThisVillageTask(task) {
const taskVillageId = (task.args || {}).villageId;
const currentVillageId = (this.shared.village || {}).id;
return taskVillageId !== undefined && taskVillageId === currentVillageId;
},
onRemove(taskId) {
console.log('ON REMOVE TASK', taskId);
this.shared.removeTask(taskId);
},
},
};
</script>
@ -54,4 +67,11 @@ export default {
padding: 2px 4px;
max-width: 25%;
}
.this-village {
color: blue;
}
.remove-action {
font-weight: bold;
color: red;
}
</style>

View File

@ -1,14 +1,18 @@
import * as URLParse from 'url-parse';
import { markPage, uniqId, waitForLoad } from '../utils';
import { uniqId, waitForLoad } from '../utils';
import { Scheduler } from '../Scheduler';
import { TaskQueueRenderer } from '../TaskQueueRenderer';
import { BuildPage } from '../Page/BuildPage';
import { UpgradeBuildingTask } from '../Task/UpgradeBuildingTask';
import { grabResources } from '../Page/ResourcesBlock';
import { grabActiveVillage, grabActiveVillageId, grabVillageList } from '../Page/VillageBlock';
import { onResourceSlotCtrlClick, showBuildingSlotIds, showResourceSlotIds } from '../Page/SlotBlock';
import { grabActiveVillage, grabActiveVillageId } from '../Page/VillageBlock';
import {
grabResourceDeposits,
onResourceSlotCtrlClick,
showBuildingSlotIds,
showResourceSlotIds,
} from '../Page/SlotBlock';
import Vue from 'vue';
import DashboardApp from './Components/DashboardApp.vue';
import { ResourcesToLevel } from '../Task/ResourcesToLevel';
export class Dashboard {
private readonly version: string;
@ -25,45 +29,54 @@ export class Dashboard {
const p = new URLParse(window.location.href, true);
this.log('PARSED LOCATION', p);
const res = grabResources();
this.log('RES', res);
const villages = grabVillageList();
this.log('VILL', villages);
const villageId = grabActiveVillageId();
const scheduler = this.scheduler;
const quickActions: any[] = [];
const state = {
name: 'Dashboard',
village: grabActiveVillage(),
version: this.version,
taskList: this.scheduler.getTaskItems(),
quickActions: quickActions,
refreshTasks() {
this.taskList = scheduler.getTaskItems();
},
removeTask(taskId: string) {
scheduler.removeTask(taskId);
this.taskList = scheduler.getTaskItems();
},
};
const appId = `app-${uniqId()}`;
jQuery('body').prepend(`<div id="${appId}"></div>`);
new Vue({
el: `#${appId}`,
data: state,
render: h => h(DashboardApp),
});
setInterval(() => state.refreshTasks(), 1000);
// markPage('Dashboard', this.version);
// this.renderTaskQueue();
// setInterval(() => this.renderTaskQueue(), 5000);
const deposits = grabResourceDeposits();
if (deposits.length) {
const sorted = deposits.sort((x, y) => x.level - y.level);
const minLevel = sorted[0].level;
for (let i = minLevel + 1; i < minLevel + 4; ++i) {
quickActions.push({
label: `Ресурсы до уровня ${i}`,
cb: () => {
scheduler.scheduleTask(ResourcesToLevel.name, { villageId, level: i });
state.refreshTasks();
},
});
}
}
const tasks = this.scheduler.getTaskItems();
const buildingsInQueue = tasks
.filter(t => t.name === UpgradeBuildingTask.name && t.args.villageId === villageId)
.map(t => t.args.buildId);
.map(t => t.args.buildId || 0);
if (p.pathname === '/dorf1.php') {
showResourceSlotIds(buildingsInQueue);
onResourceSlotCtrlClick(buildId => {
this.scheduler.scheduleTask(UpgradeBuildingTask.name, { villageId, buildId });
const n = new Notification(`Building ${buildId} scheduled`);
setTimeout(() => n && n.close(), 4000);
});
onResourceSlotCtrlClick(buildId => this.onResourceSlotCtrlClick(villageId, buildId, state));
console.log(grabResourceDeposits());
}
if (p.pathname === '/dorf2.php') {
@ -73,11 +86,25 @@ export class Dashboard {
if (p.pathname === '/build.php') {
new BuildPage(this.scheduler, Number(p.query.id)).run();
}
this.createControlPanel(state);
}
private renderTaskQueue() {
this.log('RENDER TASK QUEUE');
new TaskQueueRenderer().render(this.scheduler.getTaskItems());
private createControlPanel(state) {
const appId = `app-${uniqId()}`;
jQuery('body').prepend(`<div id="${appId}"></div>`);
new Vue({
el: `#${appId}`,
data: state,
render: h => h(DashboardApp),
});
}
private onResourceSlotCtrlClick(villageId: number, buildId: number, state) {
this.scheduler.scheduleTask(UpgradeBuildingTask.name, { villageId, buildId });
state.refreshTasks();
const n = new Notification(`Building ${buildId} scheduled`);
setTimeout(() => n && n.close(), 4000);
}
private log(...args) {

View File

@ -12,6 +12,15 @@ export const ResourceMapping: ReadonlyArray<{ num: number; type: ResourceType }>
{ num: 4, type: ResourceType.Crop },
];
export function numberToResourceType(typeAsNumber: number): ResourceType {
for (let mp of ResourceMapping) {
if (typeAsNumber === mp.num) {
return mp.type;
}
}
throw new Error(`Type ${typeAsNumber} in not valid`);
}
export type ResourceList = Array<{ num: number; type: ResourceType; value: number }>;
export class Resources {
@ -80,3 +89,16 @@ export type HeroAllResourcesType = 'all';
export const HeroAllResources: HeroAllResourcesType = 'all';
export type HeroResourceType = ResourceType | HeroAllResourcesType;
export class ResourceDeposit {
readonly buildId: number;
readonly type: ResourceType;
readonly level: number;
readonly ready: boolean;
constructor(buildId: number, type: ResourceType, level: number, ready: boolean) {
this.buildId = buildId;
this.type = type;
this.level = level;
this.ready = ready;
}
}

View File

@ -59,7 +59,7 @@ export class BuildPage {
});
}
private onTrainTroopClick(buildId: Number, troopId: Number, el: HTMLElement) {
private onTrainTroopClick(buildId: number, troopId: number, el: HTMLElement) {
console.log('TRAIN TROOPERS', 'TROOP ID', troopId, 'BUILDING ID', buildId);
const villageId = grabActiveVillageId();
const input = jQuery(el).find(`input[name="t${troopId}"]`);

View File

@ -1,4 +1,5 @@
import { elClassId, getNumber } from '../utils';
import { numberToResourceType, ResourceDeposit } from '../Game';
interface Slot {
el: HTMLElement;
@ -14,7 +15,7 @@ function slotElements(prefix: string): Array<Slot> {
return result;
}
function showSlotIds(prefix: string, buildingIds: number[]): void {
function showSlotIds(prefix: string, buildingIds: Array<number>): void {
const slots = slotElements(prefix);
slots.forEach(slot => {
const oldLabel = jQuery(slot.el)
@ -54,3 +55,17 @@ export function onResourceSlotCtrlClick(cb: (buildId: number) => void): void {
});
});
}
function slotToDepositMapper(slot: Slot): ResourceDeposit {
const el = slot.el;
const classes = jQuery(el).attr('class') || '';
const buildId = getNumber(elClassId(classes, 'buildingSlot'));
const level = getNumber(elClassId(classes, 'level'));
const type = getNumber(elClassId(classes, 'gid'));
const ready = !jQuery(el).hasClass('notNow');
return new ResourceDeposit(buildId, numberToResourceType(type), level, ready);
}
export function grabResourceDeposits(): Array<ResourceDeposit> {
return slotElements('buildingSlot').map(slotToDepositMapper);
}

View File

@ -145,6 +145,7 @@ export class Scheduler {
completeTask(id: TaskId) {
this.taskQueue.complete(id);
this.actionQueue.clear();
}
scheduleTask(name: string, args: Args): void {
@ -152,6 +153,11 @@ export class Scheduler {
this.taskQueue.push(name, args, timestamp());
}
removeTask(id: TaskId) {
this.taskQueue.remove(id);
this.actionQueue.clear();
}
scheduleActions(actions: Array<Command>): void {
this.actionQueue.assign(actions);
}

View File

@ -76,6 +76,11 @@ export class TaskQueue {
this.flushItems(items);
}
remove(id: TaskId) {
const [_, items] = this.shiftTask(id);
this.flushItems(items);
}
seeItems(): TaskList {
return this.getItems();
}

View File

@ -0,0 +1,22 @@
import { Args, Command } from '../Common';
import { Task } from '../Storage/TaskQueue';
import { TaskController, registerTask } from './TaskController';
import { GoToPageAction } from '../Action/GoToPageAction';
import { CompleteTaskAction } from '../Action/CompleteTaskAction';
import { path } from '../utils';
import { UpgradeResourceToLevel } from '../Action/UpgradeResourceToLevel';
@registerTask
export class ResourcesToLevel extends TaskController {
async run(task: Task) {
const args: Args = { ...task.args, taskId: task.id };
this.scheduler.scheduleActions([
new Command(GoToPageAction.name, {
...args,
path: path('/dorf1.php', { newdid: args.villageId }),
}),
new Command(UpgradeResourceToLevel.name, args),
new Command(CompleteTaskAction.name, args),
]);
}
}

View File

@ -85,11 +85,13 @@ export function getNumber(value: any, def: number = 0): number {
return converted === undefined ? def : converted;
}
export function path(p: string, query: { [key: string]: string | number } = {}) {
export function path(p: string, query: { [key: string]: string | number | undefined } = {}) {
let parts: string[] = [];
for (let k in query) {
if (query[k] !== undefined) {
parts.push(`${k}=${query[k]}`);
}
}
return p + (parts.length ? '?' + parts.join('&') : '');
}

View File

@ -7,7 +7,7 @@
"outDir": "./dist",
"strictNullChecks": true,
"strictPropertyInitialization": true,
"target": "es2015",
"target": "es2018",
"types": ["node", "url-parse", "jquery", "mocha", "chai"]
},
"include": [