1 | var express = require('express'),
|
2 | router = express.Router(),
|
3 | q = require('q'),
|
4 | _ = require('lodash'),
|
5 | jsonResponse = require('q-json-response'),
|
6 | utils = require('./utils'),
|
7 | common = require('@screeps/common'),
|
8 | config = common.configManager.config,
|
9 | db = common.storage.db,
|
10 | env = common.storage.env,
|
11 | C = config.common.constants;
|
12 |
|
13 | config.cronjobs = {
|
14 | sendNotifications: [60, sendNotifications],
|
15 | roomsForceUpdate: [20, roomsForceUpdate],
|
16 | genPowerBanks: [5*60, genPowerBanks],
|
17 | genInvaders: [5*60, genInvaders],
|
18 | purgeTransactions: [60*60, purgeTransactions],
|
19 | recreateNpcOrders: [5*60, recreateNpcOrders],
|
20 | calcMarketStats: [60*60, calcMarketStats],
|
21 | deletePowerCreeps: [10*60, deletePowerCreeps]
|
22 | };
|
23 |
|
24 | module.exports.run = function() {
|
25 | _.forEach(config.cronjobs, (i,key) => {
|
26 | if(Date.now() - (i[2]||0) > i[0]*1000) {
|
27 | console.log(`Running cronjob '${key}'`);
|
28 | i[2] = Date.now();
|
29 | i[1]();
|
30 | }
|
31 | });
|
32 | };
|
33 |
|
34 | function isBus(coord) {
|
35 | return coord < 0 && (coord+1) % 10 == 0 || coord > 0 && (coord) % 10 == 0 || coord == 0;
|
36 | }
|
37 |
|
38 | function isCenter(x,y) {
|
39 | return (x < 0 && Math.abs(x+1)%10 >= 4 && Math.abs(x+1)%10 <= 6 || x >= 0 && Math.abs(x)%10 >= 4 && Math.abs(x)%10 <= 6) &&
|
40 | (y < 0 && Math.abs(y+1)%10 >= 4 && Math.abs(y+1)%10 <= 6 || y >= 0 && Math.abs(y)%10 >= 4 && Math.abs(y)%10 <= 6);
|
41 | }
|
42 |
|
43 | function recreateNpcOrders() {
|
44 | var gameTime;
|
45 |
|
46 | var sellMinerals = ['X','Z','K','L','U','O','O','H','H','Z','K','L','U','O','O','H','H'];
|
47 | var buyMinerals = ['X','Z','K','L','U','O','O','H','H','Z','K','L','U','O','O','H','H'];
|
48 | var sellPrice = {
|
49 | H: 3000,
|
50 | O: 3000,
|
51 | Z: 6000,
|
52 | K: 6000,
|
53 | U: 6000,
|
54 | L: 6000,
|
55 | X: 18000
|
56 | };
|
57 | var buyPrice = 1000;
|
58 |
|
59 | var sellMineralAmount = 40, sellEnergyAmount = 40, buyMineralAmount = 20, period = 5000;
|
60 |
|
61 | var cnt = 0;
|
62 |
|
63 | return common.getGametime()
|
64 | .then(data => gameTime = data)
|
65 | .then(() => db['rooms.objects'].find({$and: [{type: 'terminal'}, {user: {$eq: null}}]}))
|
66 | .then(terminals => common.qSequence(terminals, terminal => {
|
67 | return db.rooms.findOne({_id: terminal.room})
|
68 | .then(room => {
|
69 | if(room.status != 'normal') {
|
70 | return;
|
71 | }
|
72 | if(room.nextNpcMarketOrder && room.nextNpcMarketOrder > gameTime) {
|
73 | return;
|
74 | }
|
75 | const nowTimestamp = new Date().getTime();
|
76 | var sellMineral = sellMinerals[Math.floor(Math.random()*sellMinerals.length)];
|
77 | var buyMineral = buyMinerals[Math.floor(Math.random()*buyMinerals.length)];
|
78 | var orders = [];
|
79 |
|
80 | orders.push({
|
81 | created: gameTime,
|
82 | createdTimestamp: nowTimestamp,
|
83 | active: true,
|
84 | type: 'sell',
|
85 | amount: period*sellMineralAmount,
|
86 | remainingAmount: period*sellMineralAmount,
|
87 | totalAmount: period*sellMineralAmount,
|
88 | resourceType: sellMineral,
|
89 | price: sellPrice[sellMineral],
|
90 | roomName: terminal.room
|
91 | });
|
92 |
|
93 | if(Math.random() < 0.5) {
|
94 | orders.push({
|
95 | created: gameTime,
|
96 | createdTimestamp: nowTimestamp,
|
97 | active: true,
|
98 | type: 'sell',
|
99 | amount: period*sellEnergyAmount,
|
100 | remainingAmount: period*sellEnergyAmount,
|
101 | totalAmount: period*sellEnergyAmount,
|
102 | resourceType: 'energy',
|
103 | price: 1000,
|
104 | roomName: terminal.room
|
105 | });
|
106 | }
|
107 | if(Math.random() < 0.25) {
|
108 | orders.push({
|
109 | created: gameTime,
|
110 | createdTimestamp: nowTimestamp,
|
111 | active: true,
|
112 | type: 'buy',
|
113 | amount: period*buyMineralAmount,
|
114 | remainingAmount: period*buyMineralAmount,
|
115 | totalAmount: period*buyMineralAmount,
|
116 | resourceType: buyMineral,
|
117 | price: buyPrice,
|
118 | roomName: terminal.room
|
119 | });
|
120 | }
|
121 | cnt++;
|
122 | return db['market.orders'].removeWhere({roomName: room._id})
|
123 | .then(() => db['market.orders'].insert(orders))
|
124 | .then(() => db.rooms.update({_id: room._id}, {$set: {nextNpcMarketOrder: gameTime + Math.round(period*(0.8 + 0.4*Math.random()))}}));
|
125 | })
|
126 | }))
|
127 | }
|
128 |
|
129 | function sendNotifications() {
|
130 |
|
131 | var notifications, userIds;
|
132 | var filterDate = new Date();
|
133 | return db['users.notifications'].find({date: {$lt: filterDate.getTime()}})
|
134 | .then((data) => {
|
135 | notifications = data;
|
136 | userIds = _(notifications).pluck('user').uniq(false, (i) => i.toString()).value();
|
137 | })
|
138 | .then(() => db.users.find({_id: {$in: userIds}}))
|
139 | .then((users) => {
|
140 | var promise = q.when();
|
141 | users.forEach((user) => {
|
142 |
|
143 | var notificationIdsToRemove = [];
|
144 |
|
145 | promise = promise.then(() => {
|
146 |
|
147 | var userNotifications = _.filter(notifications, (i) => i.user == user._id);
|
148 |
|
149 | if (user.notifyPrefs && (user.notifyPrefs.disabled || !user.email)) {
|
150 | userNotifications.forEach((notification) => {
|
151 | notificationIdsToRemove.push(notification._id);
|
152 | });
|
153 | return;
|
154 | }
|
155 |
|
156 | var interval = 5;
|
157 | if (user.notifyPrefs && user.notifyPrefs.interval > 5) {
|
158 | interval = user.notifyPrefs.interval;
|
159 | }
|
160 | interval *= 60 * 1000;
|
161 |
|
162 | if (user.lastNotifyDate && (user.lastNotifyDate + interval > Date.now())) {
|
163 | return;
|
164 | }
|
165 |
|
166 | userNotifications.forEach((notification) => {
|
167 | notificationIdsToRemove.push(notification._id);
|
168 | });
|
169 |
|
170 | config.backend.emit('sendUserNotifications',user,
|
171 | userNotifications.map(i => _.pick(i, ['message','date','count','type'])));
|
172 | })
|
173 | .catch((e) => console.log(`Error sending a message to ${user.username}: ${e}`))
|
174 | .then(() => notificationIdsToRemove.length > 0 && q.all([
|
175 | db['users.notifications'].removeWhere({_id: {$in: notificationIdsToRemove}}),
|
176 | db.users.update({_id: user._id}, {$set: {lastNotifyDate: Date.now()}})
|
177 | ]))
|
178 | });
|
179 | return promise;
|
180 | })
|
181 | }
|
182 |
|
183 | function roomsForceUpdate() {
|
184 | return common.getGametime()
|
185 | .then(gameTime => {
|
186 | return db.rooms.find({$and: [{status: {$ne: 'out of borders'}}, {active: false}]})
|
187 | .then(rooms => common.qSequence(rooms, room => {
|
188 | if (!room.nextForceUpdateTime || gameTime >= room.nextForceUpdateTime) {
|
189 | return db.rooms.update({_id: room._id}, {
|
190 | $set: {
|
191 | active: true,
|
192 | nextForceUpdateTime: gameTime + 90 + Math.floor(Math.random() * 20)
|
193 | }
|
194 | });
|
195 | }
|
196 | }))
|
197 | });
|
198 | }
|
199 |
|
200 | function genPowerBanks() {
|
201 |
|
202 |
|
203 |
|
204 | |
205 |
|
206 |
|
207 |
|
208 |
|
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 |
|
228 |
|
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 |
|
236 |
|
237 |
|
238 |
|
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
256 |
|
257 | }
|
258 |
|
259 | function genInvaders() {
|
260 |
|
261 | function checkExit(roomName, exit) {
|
262 | var [x,y] = utils.roomNameToXY(roomName);
|
263 | if(exit == 'top') y--;
|
264 | if(exit == 'right') x++;
|
265 | if(exit == 'bottom') y++;
|
266 | if(exit == 'left') x--;
|
267 | var newRoomName = utils.roomNameFromXY(x,y);
|
268 | return db['rooms.objects'].findOne({$and: [{room: newRoomName}, {type: 'controller'}]})
|
269 | .then(controller => {
|
270 | if(controller && (controller.user || controller.reservation)) {
|
271 | return q.reject();
|
272 | }
|
273 | })
|
274 | }
|
275 |
|
276 | function createCreep(type, room, square, boosted) {
|
277 |
|
278 | var [x,y] = utils.roomNameToXY(room);
|
279 |
|
280 | var body = {
|
281 | bigHealer: ['move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','heal','heal','heal','heal','heal','heal','heal','heal','heal','heal','heal','heal','heal','heal','heal','heal','heal','heal','heal','heal','heal','heal','heal','heal','heal','move'],
|
282 | bigRanged: ['tough','tough','tough','tough','tough','tough','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','ranged_attack','ranged_attack','ranged_attack','ranged_attack','ranged_attack','ranged_attack','ranged_attack','ranged_attack','ranged_attack','ranged_attack','ranged_attack','ranged_attack','ranged_attack','ranged_attack','ranged_attack','ranged_attack','ranged_attack','ranged_attack','work','move'],
|
283 | bigMelee: ['tough','tough','tough','tough','tough','tough','tough','tough','tough','tough','tough','tough','tough','tough','tough','tough','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','move','ranged_attack','ranged_attack','ranged_attack','work','work','work','work','attack','attack','move'],
|
284 | smallHealer: ['move','move','move','move','heal','heal','heal','heal','heal','move'],
|
285 | smallRanged: ['tough','tough','move','move','move','move','ranged_attack','ranged_attack','ranged_attack','move'],
|
286 | smallMelee: ['tough','tough','move','move','move','move','ranged_attack','work','attack','move']
|
287 | };
|
288 |
|
289 | var creep = {
|
290 | type: 'creep',
|
291 | user: '2',
|
292 | body: _.map(body[type], type => ({type, hits: 100})),
|
293 | hits: body[type].length * 100,
|
294 | hitsMax: body[type].length * 100,
|
295 | ticksToLive: 1500,
|
296 | x: square[0],
|
297 | y: square[1],
|
298 | room,
|
299 | fatigue: 0,
|
300 | energy: 0,
|
301 | energyCapacity: 0,
|
302 | name: `invader_${room}_${Math.floor(Math.random()*1000)}`
|
303 | };
|
304 |
|
305 | if(boosted) {
|
306 | creep.body.forEach(i => {
|
307 | if(i.type == 'heal') {
|
308 | i.boost = isCenter(x,y) ? 'XLHO2' : 'LO';
|
309 | }
|
310 | if(i.type == 'ranged_attack') {
|
311 | i.boost = isCenter(x,y) ? 'XKHO2' : 'KO';
|
312 | }
|
313 | if(i.type == 'work') {
|
314 | i.boost = isCenter(x,y) ? 'XZH2O' : 'ZH';
|
315 | }
|
316 | if(i.type == 'attack') {
|
317 | i.boost = isCenter(x,y) ? 'XUH2O' : 'UH';
|
318 | }
|
319 | if(i.type == 'tough') {
|
320 | i.boost = isCenter(x,y) ? 'XGHO2' : 'GO';
|
321 | }
|
322 | })
|
323 | }
|
324 |
|
325 | return db['rooms.objects'].insert(creep);
|
326 | }
|
327 |
|
328 | function findClosestExit(square, exits) {
|
329 | var sortedExits = _.sortBy(exits, i => Math.max(Math.abs(i[0] - square[0]), Math.abs(i[1] - square[1])));
|
330 | return sortedExits[0];
|
331 | }
|
332 |
|
333 |
|
334 | function createRaid(controllerLevel, room, exits) {
|
335 |
|
336 | var type = controllerLevel && controllerLevel >= 4 ? 'big' : 'small';
|
337 |
|
338 | var max = 1, count = 1, boostChance = 0.5;
|
339 |
|
340 | var [x,y] = utils.roomNameToXY(room);
|
341 |
|
342 | if(Math.random() > 0.9 || isCenter(x,y)) {
|
343 | max = 2;
|
344 | boostChance = type == 'big' ? 0 : 0.5;
|
345 | if (Math.random() > 0.8 || isCenter(x,y)) {
|
346 | max = 5;
|
347 | if (type == 'big') {
|
348 | if (controllerLevel < 5) {
|
349 | max = 2;
|
350 | }
|
351 | else if (controllerLevel < 6) {
|
352 | max = 2;
|
353 | }
|
354 | else if (controllerLevel < 7) {
|
355 | max = 3;
|
356 | }
|
357 | else if (controllerLevel < 8) {
|
358 | boostChance = 0.4;
|
359 | max = 3;
|
360 | }
|
361 | else {
|
362 | boostChance = 0.4;
|
363 | max = 5;
|
364 | }
|
365 | }
|
366 | }
|
367 |
|
368 | count = Math.floor(Math.random()*(max-1)) + 2;
|
369 | count = Math.min(count, exits.length);
|
370 | }
|
371 |
|
372 | var promises = [], lastSquare;
|
373 |
|
374 | for(var i=0; i<count; i++) {
|
375 | var subtype = i == 0 && !isCenter(x,y) ? 'Melee' :
|
376 | i == 0 || (i == 1 || i == 2 && count == 5) && Math.random() > 0.5 ? 'Ranged' :
|
377 | 'Healer';
|
378 |
|
379 | var square = lastSquare ? findClosestExit(lastSquare, exits) : exits[Math.floor(Math.random() * exits.length)];
|
380 | if(!square) {
|
381 | break;
|
382 | }
|
383 | promises.push(createCreep(type + subtype, room, square, Math.random() < boostChance));
|
384 | _.pull(exits, square);
|
385 | lastSquare = square;
|
386 | }
|
387 |
|
388 | return q.all(promises);
|
389 | }
|
390 |
|
391 | return db.rooms.find({$and: [{status: 'normal'}, {active: true}]})
|
392 | .then(rooms => q.all(rooms.map(room => {
|
393 |
|
394 | return db['rooms.objects'].find({$and: [{room: room._id}, {type: {$in: ['source','controller','creep']}}]})
|
395 | .then(objects => {
|
396 | var sources = _.filter(objects, {type: 'source'});
|
397 | var creeps = _.filter(objects, {type: 'creep', user: '2'});
|
398 | if(creeps.length) {
|
399 | return;
|
400 | }
|
401 | var invaderHarvested = _.sum(sources, 'invaderHarvested');
|
402 | var goal = room.invaderGoal || C.INVADERS_ENERGY_GOAL;
|
403 | if(goal != 1 && invaderHarvested < goal) {
|
404 | return;
|
405 | }
|
406 | return db['rooms.terrain'].findOne({room: room._id})
|
407 | .then(terrain => {
|
408 | var exits = {}, exitSquares = {top: [], left: [], right: [], bottom: []};
|
409 | for(var i=0; i<49; i++) {
|
410 | if(!common.checkTerrain(terrain.terrain, i, 0, C.TERRAIN_MASK_WALL)) {
|
411 | exits.top = true;
|
412 | exitSquares.top.push([i,0]);
|
413 | }
|
414 | if(!common.checkTerrain(terrain.terrain, i, 49, C.TERRAIN_MASK_WALL)) {
|
415 | exits.bottom = true;
|
416 | exitSquares.bottom.push([i,49]);
|
417 | }
|
418 | if(!common.checkTerrain(terrain.terrain, 0, i, C.TERRAIN_MASK_WALL)) {
|
419 | exits.left = true;
|
420 | exitSquares.left.push([0,i]);
|
421 | }
|
422 | if(!common.checkTerrain(terrain.terrain, 49, i, C.TERRAIN_MASK_WALL)) {
|
423 | exits.right = true;
|
424 | exitSquares.right.push([49,i]);
|
425 | }
|
426 | }
|
427 | exits = _.keys(exits);
|
428 | return q.all(_.map(exits, exit => {
|
429 | return checkExit(room._id, exit).catch(() => _.pull(exits, exit));
|
430 | }))
|
431 | .then(() => {
|
432 | if(!exits.length) {
|
433 | return;
|
434 | }
|
435 | var exit = exits[Math.floor(Math.random()*exits.length)];
|
436 | var controller = _.find(objects, {type: 'controller'});
|
437 | return createRaid(controller && controller.user && controller.level, room._id, exitSquares[exit]);
|
438 | })
|
439 | })
|
440 | .then(() => {
|
441 | var invaderGoal = Math.floor(C.INVADERS_ENERGY_GOAL * (Math.random()*0.6 + 0.7));
|
442 | if(Math.random() < 0.1) {
|
443 | invaderGoal *= Math.floor( Math.random() > 0.5 ? 2 : 0.5 );
|
444 | }
|
445 | return db.rooms.update({_id: room._id}, {$set: {invaderGoal}})
|
446 | })
|
447 | .then(() => db['rooms.objects'].update({$and: [{room: room._id}, {type: 'source'}]}, {$set: {invaderHarvested: 0}}));
|
448 | })
|
449 | })));
|
450 | }
|
451 |
|
452 | function purgeTransactions() {
|
453 | return db.transactions.find()
|
454 | .then(data => {
|
455 | data = _.sortBy(data, i => -parseInt(i.time));
|
456 |
|
457 | var senders = {}, recipients = {}, forDelete = [];
|
458 | data.forEach(i => {
|
459 | var flag1 = true, flag2 = true;
|
460 | senders[i.sender] = senders[i.sender] || [];
|
461 | if(senders[i.sender].length < 100) {
|
462 | senders[i.sender].push(i);
|
463 | flag1 = false;
|
464 | }
|
465 | recipients[i.recipient] = recipients[i.recipient] || [];
|
466 | if(recipients[i.recipient].length < 100) {
|
467 | recipients[i.recipient].push(i);
|
468 | flag2 = false;
|
469 | }
|
470 | if(flag1 && flag2) {
|
471 | forDelete.push(i._id);
|
472 | }
|
473 | });
|
474 |
|
475 | if(forDelete.length > 0) {
|
476 | return db.transactions.removeWhere({_id: {$in: forDelete}});
|
477 | }
|
478 | })
|
479 | }
|
480 |
|
481 | function calcMarketStats() {
|
482 |
|
483 | };
|
484 |
|
485 | function calcPowerLevelBase(level) {
|
486 | return Math.pow(level, C.POWER_LEVEL_POW) * C.POWER_LEVEL_MULTIPLY;
|
487 | }
|
488 |
|
489 | function calcPowerLevel(power) {
|
490 | return Math.floor( Math.pow( (power || 0) / C.POWER_LEVEL_MULTIPLY, 1 / C.POWER_LEVEL_POW ) );
|
491 | }
|
492 |
|
493 | function deletePowerCreeps() {
|
494 | return db['users.power_creeps'].find({deleteTime: {$lt: Date.now()}})
|
495 | .then((data) => _.reduce(data, (promise, creep) => {
|
496 | if(!creep.deleteTime) {
|
497 | return promise;
|
498 | }
|
499 | return promise
|
500 | .then(() => db['users'].findOne({_id: creep.user}))
|
501 | .then(user => {
|
502 | var level = calcPowerLevel(user.power);
|
503 | var basePrev = calcPowerLevelBase(level-1);
|
504 | var baseCurrent = calcPowerLevelBase(level);
|
505 | var baseNext = calcPowerLevelBase(level+1);
|
506 | var change = Math.round(user.power - basePrev -
|
507 | (user.power - baseCurrent) * (baseCurrent - basePrev) / (baseNext - baseCurrent));
|
508 | return q.all([
|
509 | db['users'].update({_id: user._id}, {$inc: {power: -change}}),
|
510 | db['users.power_creeps'].removeWhere({_id: creep._id})
|
511 | ]);
|
512 | })
|
513 | }, q.when()));
|
514 | }
|