4509bec431ad8d616bc0946d2fa2c0933268b2da
[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
104   const usage = {
105     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
106     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
107     energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
108     energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
109   }
110
111         return renderKingomOverview({
112     ...city,
113     ...usage
114   }, account) + topbar({...city, ...usage});
115 });
116
117 server.get<{}, string>('/poll/construction', async req => {
118         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
119         const city = await cityRepo.getUsersCity(account.id);
120         const buildings = await cityRepo.buildingRepository.list();
121
122         const buildQueues = await cityRepo.getBuildQueues(account.id);
123   const usage = {
124     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
125     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
126     energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
127     energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
128   }
129         return renderLandDevelopment(city, buildings, buildQueues) + topbar({...city, ...usage});
130 });
131
132 server.get<{}, string>('/poll/unit-training', async req => {
133         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
134         const city = await cityRepo.getUsersCity(account.id);
135
136         const unitTrainingQueues = await cityRepo.getUnitTrainingQueues(account.id);
137         const units = await cityRepo.unitRepository.list();
138   const usage = {
139     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
140     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
141     energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
142     energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
143   }
144
145         return renderUnitTraining(city, units, unitTrainingQueues) + topbar({
146     ...city,
147     ...usage
148   });
149 });
150
151 server.post<{body: {sector: string}}, string>('/poll/map', async req => {
152         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
153         const city = await cityRepo.getUsersCity(account.id);
154
155   let sector = city.sector_id;
156   if(req.body.sector) {
157     try {
158       sector = parseInt(req.body.sector);
159     }
160     catch(e) {
161       sector = city.sector_id;
162     }
163   }
164
165   const usage = {
166     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
167     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
168     energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
169     energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
170   }
171
172         return renderOverworldMap(await cityRepo.findAllInSector(sector), city, sector) + topbar({
173     ...city,
174     ...usage
175   });
176 });
177
178 server.get<{}, string>('/poll/mailroom', async req => {
179         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
180         const city = await cityRepo.getUsersCity(account.id);
181
182   const usage = {
183     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
184     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
185     energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
186     energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
187   }
188
189         return renderMailroom(await mailRepo.listReceivedMessages(account.id)) + topbar({
190     ...city,
191     ...usage
192   });
193 });
194
195
196 server.post<{
197         body: {
198                 amount: string,
199                 building_type: string
200         }
201 }, string>('/cost/construction', async req => {
202         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
203         const city = await cityRepo.getUsersCity(account.id);
204
205         const amount = parseInt(req.body.amount.trim(), 10);
206
207         if(isNaN(amount) || amount < 1) {
208                 return '';
209         }
210         const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
211
212         if(!building) {
213                 throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
214         }
215
216   const cost = {
217                 credits: building.credits * amount,
218                 alloys: building.alloys * amount,
219                 energy: building.energy * amount,
220                 land: building.land * amount,
221                 time: building.time
222   };
223
224   return renderCost(cost, city);
225 });
226
227 server.post<{
228         body: {
229                 amount: string;
230                 type: string;
231         }
232 }, string>('/cost/training', async req => {
233         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
234         const city = await cityRepo.getUsersCity(account.id);
235         const amount = parseInt(req.body.amount, 10);
236
237         if(isNaN(amount) || amount < 1) {
238                 return '';
239         }
240
241         const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
242         if(!unit) {
243                 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
244         }
245
246         return renderCost({
247                 population: unit.population * amount,
248                 soldiers: unit.soldiers * amount,
249                 attackers: unit.attackers * amount,
250                 defenders: unit.defenders * amount,
251                 credits: unit.credits * amount,
252                 food: unit.food * amount,
253     time: unit.time * amount
254         }, city);
255 });
256
257 server.post<{
258         body: {
259                 amount: string,
260                 building_type: string,
261         }
262 }, void>('/build', async req => {
263   const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
264   const city = await cityRepo.getUsersCity(account.id);
265
266   const amount = parseInt(req.body.amount, 10);
267   if(amount < 1) {
268     throw new BadInputError('Please specify an amount > 0', ERROR_CODE.INVALID_AMOUNT);
269   }
270   const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
271
272   if(!building) {
273     throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
274   }
275
276   const queueData = await cityRepo.buildBuilding(building, amount, city);
277
278         construction.trigger(queueData, { delay: queueData.due - Date.now() });
279 }, 'reload-construction-queue');
280
281 server.post<{
282                 body: {
283                         amount: string,
284                         type: string
285                 }
286         },
287         void
288         >('/units', async req => {
289         const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
290         const city = await cityRepo.getUsersCity(acct.id);
291
292         const amount  = parseInt(req.body.amount, 10) || 0;
293   console.log('request amount?!', amount);
294   if(amount < 1) {
295     throw new BadInputError('Please specify an amount > 0', ERROR_CODE.INVALID_AMOUNT);
296   }
297         const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
298
299         if(!unit) {
300                 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
301         }
302
303         const queueData = await cityRepo.train(unit, amount, city);
304         unitTraining.trigger(queueData, { delay: queueData.due - Date.now() });
305
306 }, 'reload-unit-training');
307
308 server.post<{
309         body: {
310                 city: string,
311                 soldiers: string,
312                 attackers: string,
313                 defenders: string,
314                 sp_attackers: string,
315                 sp_defenders: string
316         }
317         }, 
318         void
319         >('/attack', async req => {
320                 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
321                 const city = await cityRepo.getUsersCity(acct.id);
322                 const attackedCity = await cityRepo.findById(req.body.city);
323
324                 const army = {
325                         soldiers: parseInt(req.body.soldiers),
326                         attackers: parseInt(req.body.attackers),
327                         defenders: parseInt(req.body.defenders),
328                         sp_attackers: parseInt(req.body.sp_attackers),
329                         sp_defenders: parseInt(req.body.sp_defenders)
330                 };
331
332                 const armyQueue = await cityRepo.attack(city, attackedCity, army);
333
334                 fight.trigger(armyQueue, {
335                         delay: armyQueue.due - Date.now()
336                 });
337         }, 'reload-outgoing-attacks');
338
339 server.get<void, string>('/messages', async req => {
340         const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
341         const msgs = await mailRepo.listReceivedMessages(acct.id);
342
343         return JSON.stringify(msgs);
344 });
345
346 server.get<{params: {id: string}}, string>('/messages/:id', async req => {
347         const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
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         await mailRepo.markAsRead(msg.id, msg.to_account);
355
356         return renderMessage(msg);
357 });
358
359 server.get<void, string>('/attacks/outgoing', async req => {
360         const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
361         const city = await cityRepo.getUsersCity(acct.id);
362         const attacks = await cityRepo.armyRepository.listOutgoing(city.id);
363
364         return listOperations(attacks);
365 });
366
367 server.post<{body: {message: string}}, void>('/chat', async req => {
368   const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
369   const now = Date.now();
370
371   if(!_.isEmpty(req.body.message)) {
372     const msg = renderPublicChatMessage(acct.username, req.body.message);
373     server.ws.emit('/chat-message', msg);
374     msgBuffer.unshift(msg);
375     while(msgBuffer.length > 30) {
376       msgBuffer.pop();
377     }
378   }
379
380   return;
381 });
382
383 server.post<{params: {queueId: string}}, void>('/construction/:queueId/cancel', async req => {
384         const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
385         const city = await cityRepo.getUsersCity(acct.id);
386
387   if(!validator.isUUID(req.params.queueId)) {
388     throw new BadInputError('Invalid queue ID', ERROR_CODE.INVALID_BUILD_QUEUE);
389   }
390
391   // validate that this is an actual queue
392   const queue = await cityRepo.buildQueue.FindOne({owner: city.owner, id: req.params.queueId});
393
394   if(!queue) {
395     throw new NotFoundError('That queue does not exist', ERROR_CODE.INVALID_BUILD_QUEUE);
396   }
397
398   const [, building] = await Promise.all([
399     cityRepo.buildQueue.Delete({
400       owner: city.owner,
401       id: req.params.queueId
402     }),
403     cityRepo.buildingRepository.findBySlug(queue.building_type)
404   ]);
405
406   // now that it's deleted we can give the player back some percentage 
407   // of resources based on how close they are to completion.
408   const diff = (queue.due - Date.now()) / (queue.due - queue.created);
409   // force a 20% loss minimum
410   const finalDiff = diff < 0.2 ? 0.2 : diff;
411
412   const costReturn: Partial<City> = {
413     id: city.id,
414     credits: city.credits + Math.floor(building.credits * queue.amount * diff),
415     alloys: city.alloys + Math.floor(building.alloys * queue.amount * diff),
416     energy: city.energy + Math.floor(building.energy * queue.amount * diff),
417     usedSpace: city.usedSpace - (building.land * queue.amount)
418   };
419
420   console.log('update', costReturn)
421
422   await cityRepo.save(costReturn);
423
424 }, 'reload-construction-queue');
425
426 server.get<void, string>('/server-stats', async req => {
427   const date = new Date();
428   return `
429   <div class="text-right">
430     <span class="success-text">${(await server.ws.allSockets()).size} Online</span><br>
431     <span>
432     Server Time: ${date.getHours()}:${date.getMinutes()}
433     </span>
434   </div>`;
435 });
436
437 server.ws.on('connection', async socket => {
438   const auth = server.authFromUrl(socket.request.headers['referer']);
439   const acct = await accountRepo.validate(auth.authInfo.accountId, auth.authInfo.token);
440
441   server.ws.emit('/chat-message', msgBuffer.join("\n"));
442
443   server.ws.emit('/chat-message', renderPublicChatMessage('Server', `${acct.username} logged in`));
444
445 });
446
447 server.start();
448
449 tick.trigger({
450   lastTickAt: 0,
451   lastTick: 0
452 }, {
453   repeat: {
454     cron: '0 * * * *'
455   }
456 });