5b1edc657a5969056141dc3190231d429106e519
[browser-rts.git] / src / api.ts
1 import { HttpServer } from './lib/server';
2 import * as config from './config';
3 import { AccountRepository } from './repository/accounts';
4 import { City, CityRepository } from './repository/city';
5 import { MailRepository } from './repository/mail';
6 import {BadInputError, ERROR_CODE, NotFoundError} from './errors';
7 import { renderKingomOverview } from './render/kingdom-overview';
8 import { renderLandDevelopment } from './render/land-development';
9 import { tick } from './tasks/tick';
10 import { construction } from './tasks/construction';
11 import { unitTraining } from './tasks/unit-training';
12 import { fight } from './tasks/fight';
13 import { renderUnitTraining } from './render/unit-training';
14 import { launchOffensive, listOperations, renderOverworldMap } from './render/map';
15 import { createBullBoard } from '@bull-board/api';
16 import { BullAdapter } from '@bull-board/api/bullAdapter';
17 import _ from 'lodash';
18 import { renderCost } from './render/costs';
19 import {renderMailroom, renderMessage} from './render/mail';
20 import {topbar} from './render/topbar';
21 import {renderPublicChatMessage} from './render/chat-message';
22 import validator from 'validator';
23
24
25 const server = new HttpServer(config.API_PORT);
26 const accountRepo = new AccountRepository();
27 const cityRepo = new CityRepository();
28 const mailRepo = new MailRepository();
29
30 const msgBuffer: string[] = [];
31
32 createBullBoard({
33         queues: [
34                 new BullAdapter(tick.queue),
35                 new BullAdapter(construction.queue),
36                 new BullAdapter(unitTraining.queue),
37                 new BullAdapter(fight.queue)
38         ],
39         serverAdapter: server.bullAdapter,
40 });
41
42 server.post<{
43         body: {username: string, password: string}}, 
44         string
45         >
46         ('/accounts', async (req) => {
47         const { username, password} = req.body;
48         if(!username || !password || username.length < 3 || password.length < 3) {
49                 throw new BadInputError('Invalid username or password', ERROR_CODE.INVALID_USERNAME);
50         }
51         const acct = await accountRepo.create(username, password);
52
53         // lets create the city!
54         await cityRepo.create(acct.id);
55
56         return `<div class="alert success">You are all signed up! You can go ahead and log in</div>`;
57 });
58
59 server.post<{body: {username: string, password: string}}, void>('/login', async (req, raw, res) => {
60         const { username, password} = req.body;
61         if(!username || !password || username.length < 3 || password.length < 3) {
62                 throw new BadInputError('Invalid username or password', ERROR_CODE.INVALID_USERNAME);
63         }
64         const {account, session} = await accountRepo.login(username, password);
65         if(!account) {
66                 throw new NotFoundError('Invalid username or password', ERROR_CODE.USER_NOT_FOUND);
67         }
68
69         res.setHeader('hx-redirect', `/game.html?token=${session.id}&id=${session.account_id}`);
70
71 });
72
73 server.post<{body: {
74         soldiers: number,
75         attackers: number,
76         defenders: number,
77         sp_attackers: number,
78         sp_defenders: number
79   }}, string>('/attack-power', async req => {
80         const power = await cityRepo.power(req.body);
81
82         return power.toLocaleString();
83
84 });
85
86 server.get<{params: { cityId: string }}, string>('/city/:cityId', async req => {
87         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
88         const yourCity = await cityRepo.getUsersCity(account.id);
89         const city = await cityRepo.findById(req.params.cityId);
90         const acct = await accountRepo.FindOne({id: city.owner});
91
92
93         return await launchOffensive(city, acct || {
94                 id: '-',
95                 username: 'Rebels',
96                 password: ''
97         }, yourCity, account);
98 });
99
100 server.get<{}, string>('/poll/overview', async req => {
101         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
102         const city = await cityRepo.getUsersCity(account.id);
103   const unreadMail = await mailRepo.countUnread(account.id);
104
105
106   const usage = {
107     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
108     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
109     energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
110     energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
111   }
112
113         return renderKingomOverview({
114     ...city,
115     ...usage
116   }, account) + topbar({...city, ...usage}, unreadMail);
117 });
118
119 server.get<{}, string>('/poll/construction', async req => {
120         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
121         const city = await cityRepo.getUsersCity(account.id);
122         const buildings = await cityRepo.buildingRepository.list();
123   const unreadMail = await mailRepo.countUnread(account.id);
124
125         const buildQueues = await cityRepo.getBuildQueues(account.id);
126   const usage = {
127     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
128     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
129     energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
130     energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
131   }
132         return renderLandDevelopment(city, buildings, buildQueues) + topbar({...city, ...usage}, unreadMail);
133 });
134
135 server.get<{}, string>('/poll/unit-training', async req => {
136         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
137         const city = await cityRepo.getUsersCity(account.id);
138   const unreadMail = await mailRepo.countUnread(account.id);
139
140         const unitTrainingQueues = await cityRepo.getUnitTrainingQueues(account.id);
141         const units = await cityRepo.unitRepository.list();
142   const usage = {
143     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
144     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
145     energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
146     energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
147   }
148
149         return renderUnitTraining(city, units, unitTrainingQueues) + topbar({
150     ...city,
151     ...usage
152   }, unreadMail);
153 });
154
155 server.post<{body: {sector: string}}, string>('/poll/map', async req => {
156         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
157         const city = await cityRepo.getUsersCity(account.id);
158   const unreadMail = await mailRepo.countUnread(account.id);
159
160   let sector = city.sector_id;
161   if(req.body.sector) {
162     try {
163       sector = parseInt(req.body.sector);
164     }
165     catch(e) {
166       sector = city.sector_id;
167     }
168   }
169
170   const usage = {
171     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
172     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
173     energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
174     energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
175   }
176
177         return renderOverworldMap(await cityRepo.findAllInSector(sector), city, sector) + topbar({
178     ...city,
179     ...usage
180   }, unreadMail);
181 });
182
183 server.get<{}, string>('/poll/mailroom', async req => {
184         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
185         const city = await cityRepo.getUsersCity(account.id);
186   const unreadMail = await mailRepo.countUnread(account.id);
187
188   const usage = {
189     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
190     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
191     energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
192     energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
193   }
194
195         return renderMailroom(await mailRepo.listReceivedMessages(account.id)) + topbar({
196     ...city,
197     ...usage
198   }, unreadMail);
199 });
200
201
202 server.post<{
203         body: {
204                 amount: string,
205                 building_type: string
206         }
207 }, string>('/cost/construction', async req => {
208         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
209         const city = await cityRepo.getUsersCity(account.id);
210
211         const amount = parseInt(req.body.amount.trim(), 10);
212
213         if(isNaN(amount) || amount < 1) {
214                 return '';
215         }
216         const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
217
218         if(!building) {
219                 throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
220         }
221
222   const cost = {
223                 credits: building.credits * amount,
224                 alloys: building.alloys * amount,
225                 energy: building.energy * amount,
226                 land: building.land * amount,
227                 time: building.time
228   };
229
230   return renderCost(cost, city);
231 });
232
233 server.post<{
234         body: {
235                 amount: string;
236                 type: string;
237         }
238 }, string>('/cost/training', async req => {
239         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
240         const city = await cityRepo.getUsersCity(account.id);
241         const amount = parseInt(req.body.amount, 10);
242
243         if(isNaN(amount) || amount < 1) {
244                 return '';
245         }
246
247         const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
248         if(!unit) {
249                 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
250         }
251
252         return renderCost({
253                 population: unit.population * amount,
254                 soldiers: unit.soldiers * amount,
255                 attackers: unit.attackers * amount,
256                 defenders: unit.defenders * amount,
257                 credits: unit.credits * amount,
258                 food: unit.food * amount,
259     time: unit.time * amount
260         }, city);
261 });
262
263 server.post<{
264         body: {
265                 amount: string,
266                 building_type: string,
267         }
268 }, void>('/build', async req => {
269   const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
270   const city = await cityRepo.getUsersCity(account.id);
271
272   const amount = parseInt(req.body.amount, 10);
273   if(amount < 1) {
274     throw new BadInputError('Please specify an amount > 0', ERROR_CODE.INVALID_AMOUNT);
275   }
276   const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
277
278   if(!building) {
279     throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
280   }
281
282   const queueData = await cityRepo.buildBuilding(building, amount, city);
283
284         construction.trigger(queueData, { delay: queueData.due - Date.now() });
285 }, 'reload-construction-queue');
286
287 server.post<{
288                 body: {
289                         amount: string,
290                         type: string
291                 }
292         },
293         void
294         >('/units', async req => {
295         const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
296         const city = await cityRepo.getUsersCity(acct.id);
297
298         const amount  = parseInt(req.body.amount, 10) || 0;
299   console.log('request amount?!', amount);
300   if(amount < 1) {
301     throw new BadInputError('Please specify an amount > 0', ERROR_CODE.INVALID_AMOUNT);
302   }
303         const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
304
305         if(!unit) {
306                 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
307         }
308
309         const queueData = await cityRepo.train(unit, amount, city);
310         unitTraining.trigger(queueData, { delay: queueData.due - Date.now() });
311
312 }, 'reload-unit-training');
313
314 server.post<{
315         body: {
316                 city: string,
317                 soldiers: string,
318                 attackers: string,
319                 defenders: string,
320                 sp_attackers: string,
321                 sp_defenders: string
322         }
323         }, 
324         void
325         >('/attack', async req => {
326                 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
327                 const city = await cityRepo.getUsersCity(acct.id);
328                 const attackedCity = await cityRepo.findById(req.body.city);
329
330                 const army = {
331                         soldiers: parseInt(req.body.soldiers),
332                         attackers: parseInt(req.body.attackers),
333                         defenders: parseInt(req.body.defenders),
334                         sp_attackers: parseInt(req.body.sp_attackers),
335                         sp_defenders: parseInt(req.body.sp_defenders)
336                 };
337
338                 const armyQueue = await cityRepo.attack(city, attackedCity, army);
339
340                 fight.trigger(armyQueue, {
341                         delay: armyQueue.due - Date.now()
342                 });
343         }, 'reload-outgoing-attacks');
344
345 server.get<{params: {id: string}}, string>('/messages/:id', async req => {
346         const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
347   const city = await cityRepo.getUsersCity(acct.id);
348         const msg = await mailRepo.getMessage(req.params.id, acct.id);
349
350         if(!msg) {
351                 throw new NotFoundError('No such message', ERROR_CODE.DUPLICATE_CACHE_KEY);
352         }
353
354   const usage = {
355     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
356     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
357     energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
358     energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
359   }
360
361         await mailRepo.markAsRead(msg.id, msg.to_account);
362   const unreadMail = await mailRepo.countUnread(acct.id);
363
364         return renderMailroom(await mailRepo.listReceivedMessages(acct.id), msg) + topbar({
365     ...city,
366     ...usage
367   }, unreadMail);
368 });
369
370 server.get<void, string>('/attacks/outgoing', async req => {
371         const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
372         const city = await cityRepo.getUsersCity(acct.id);
373         const attacks = await cityRepo.armyRepository.listOutgoing(city.id);
374
375         return listOperations(attacks);
376 });
377
378 server.post<{body: {message: string}}, void>('/chat', async req => {
379   const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
380   const now = Date.now();
381
382   if(!_.isEmpty(req.body.message)) {
383     const msg = renderPublicChatMessage(acct.username, req.body.message);
384     server.ws.emit('/chat-message', msg);
385     msgBuffer.unshift(msg);
386     while(msgBuffer.length > 30) {
387       msgBuffer.pop();
388     }
389   }
390
391   return;
392 });
393
394 server.post<{params: {queueId: string}}, void>('/construction/:queueId/cancel', async req => {
395         const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
396         const city = await cityRepo.getUsersCity(acct.id);
397
398   if(!validator.isUUID(req.params.queueId)) {
399     throw new BadInputError('Invalid queue ID', ERROR_CODE.INVALID_BUILD_QUEUE);
400   }
401
402   // validate that this is an actual queue
403   const queue = await cityRepo.buildQueue.FindOne({owner: city.owner, id: req.params.queueId});
404
405   if(!queue) {
406     throw new NotFoundError('That queue does not exist', ERROR_CODE.INVALID_BUILD_QUEUE);
407   }
408
409   const [, building] = await Promise.all([
410     cityRepo.buildQueue.Delete({
411       owner: city.owner,
412       id: req.params.queueId
413     }),
414     cityRepo.buildingRepository.findBySlug(queue.building_type)
415   ]);
416
417   // now that it's deleted we can give the player back some percentage 
418   // of resources based on how close they are to completion.
419   const diff = (queue.due - Date.now()) / (queue.due - queue.created);
420   // force a 20% loss minimum
421   const finalDiff = diff < 0.2 ? 0.2 : diff;
422
423   const costReturn: Partial<City> = {
424     id: city.id,
425     credits: city.credits + Math.floor(building.credits * queue.amount * diff),
426     alloys: city.alloys + Math.floor(building.alloys * queue.amount * diff),
427     energy: city.energy + Math.floor(building.energy * queue.amount * diff),
428     usedSpace: city.usedSpace - (building.land * queue.amount)
429   };
430
431   console.log('update', costReturn)
432
433   await cityRepo.save(costReturn);
434
435 }, 'reload-construction-queue');
436
437 server.post<{params: {queueId: string}}, void>('/training/:queueId/cancel', async req => {
438         const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
439         const city = await cityRepo.getUsersCity(acct.id);
440
441   if(!validator.isUUID(req.params.queueId)) {
442     throw new BadInputError('Invalid queue ID', ERROR_CODE.INVALID_BUILD_QUEUE);
443   }
444   
445   const queue = await cityRepo.unitTrainigQueue.FindOne({owner: city.owner, id: req.params.queueId});
446   if(!queue) {
447     throw new NotFoundError('That queue does not exist', ERROR_CODE.INVALID_BUILD_QUEUE);
448   }
449
450   const [, unit] = await Promise.all([
451     cityRepo.unitTrainigQueue.Delete({
452       owner: city.owner,
453       id: req.params.queueId
454     }),
455     cityRepo.unitRepository.FindOne({slug: queue.unit_type})
456   ]);
457
458   // now that it's deleted we can give the player back some percentage 
459   // of resources based on how close they are to completion.
460   const diff = (queue.due - Date.now()) / (queue.due - queue.created);
461   // force a 20% loss minimum
462   const finalDiff = diff < 0.2 ? 0.2 : diff;
463
464   const costReturn: Partial<City> = {
465     id: city.id,
466     credits: city.credits + Math.floor(unit.credits * queue.amount * diff),
467     food: city.food + Math.floor(unit.food * queue.amount * diff),
468     population: city.population + Math.floor(unit.population * queue.amount),
469     soldiers: city.soldiers + Math.floor(unit.soldiers * queue.amount),
470     attackers: city.attackers + Math.floor(unit.attackers * queue.amount),
471     defenders: city.defenders + Math.floor(unit.attackers * queue.amount)
472   };
473
474   console.log('update', costReturn)
475
476   await cityRepo.save(costReturn);
477
478 }, 'reload-unit-training');
479
480 server.get<void, string>('/server-stats', async req => {
481   const date = new Date();
482   return `
483   <div class="text-right">
484     <span class="success-text">${(await server.ws.allSockets()).size} Online</span><br>
485     <span>
486     Server Time: ${date.getHours()}:${date.getMinutes()}
487     </span>
488   </div>`;
489 });
490
491 server.ws.on('connection', async socket => {
492   const auth = server.authFromUrl(socket.request.headers['referer']);
493   const acct = await accountRepo.validate(auth.authInfo.accountId, auth.authInfo.token);
494
495   server.ws.emit('/chat-message', msgBuffer.join("\n"));
496
497   server.ws.emit('/chat-message', renderPublicChatMessage('Server', `${acct.username} logged in`));
498
499 });
500
501 server.start();
502
503 tick.trigger({
504   lastTickAt: 0,
505   lastTick: 0
506 }, {
507   repeat: {
508     cron: '0 * * * *'
509   }
510 });