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 import {Socket} from 'socket.io';
26 const server = new HttpServer(config.API_PORT);
27 const accountRepo = new AccountRepository();
28 const cityRepo = new CityRepository();
29 const mailRepo = new MailRepository();
31 const cache: Record<string, any> = {
35 const msgBuffer: string[] = [];
39 new BullAdapter(tick.queue),
40 new BullAdapter(construction.queue),
41 new BullAdapter(unitTraining.queue),
42 new BullAdapter(fight.queue)
44 serverAdapter: server.bullAdapter,
48 body: {username: string, password: string}},
51 ('/accounts', async (req) => {
52 const { username, password} = req.body;
53 if(!username || !password || username.length < 3 || password.length < 3) {
54 throw new BadInputError('Invalid username or password', ERROR_CODE.INVALID_USERNAME);
56 const acct = await accountRepo.create(username, password);
58 // lets create the city!
59 await cityRepo.create(acct.id);
61 return `<div class="alert success">You are all signed up! You can go ahead and log in</div>`;
64 server.post<{body: {username: string, password: string}}, void>('/login', async (req, raw, res) => {
65 const { username, password} = req.body;
66 if(!username || !password || username.length < 3 || password.length < 3) {
67 throw new BadInputError('Invalid username or password', ERROR_CODE.INVALID_USERNAME);
69 const {account, session} = await accountRepo.login(username, password);
71 throw new NotFoundError('Invalid username or password', ERROR_CODE.USER_NOT_FOUND);
74 res.setHeader('hx-redirect', `/game.html?token=${session.id}&id=${session.account_id}`);
84 }}, string>('/attack-power', async req => {
85 const power = await cityRepo.power(req.body);
87 return power.toLocaleString();
91 server.get<{params: { cityId: string }}, string>('/city/:cityId', async req => {
92 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
93 const yourCity = await cityRepo.getUsersCity(account.id);
94 const city = await cityRepo.findById(req.params.cityId);
95 const acct = await accountRepo.FindOne({id: city.owner});
98 return await launchOffensive(city, acct || {
102 }, yourCity, account);
105 server.get<{}, string>('/poll/overview', async req => {
106 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
107 const city = await cityRepo.getUsersCity(account.id);
108 const unreadMail = await mailRepo.countUnread(account.id);
112 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
113 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
114 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
115 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
118 return renderKingomOverview({
121 }, account) + topbar({...city, ...usage}, unreadMail);
124 server.get<{}, string>('/poll/construction', async req => {
125 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
126 const city = await cityRepo.getUsersCity(account.id);
127 const buildings = await cityRepo.buildingRepository.list();
128 const unreadMail = await mailRepo.countUnread(account.id);
130 const buildQueues = await cityRepo.getBuildQueues(account.id);
132 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
133 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
134 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
135 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
137 return renderLandDevelopment(city, buildings, buildQueues) + topbar({...city, ...usage}, unreadMail);
140 server.get<{}, string>('/poll/unit-training', async req => {
141 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
142 const city = await cityRepo.getUsersCity(account.id);
143 const unreadMail = await mailRepo.countUnread(account.id);
145 const unitTrainingQueues = await cityRepo.getUnitTrainingQueues(account.id);
146 const units = await cityRepo.unitRepository.list();
148 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
149 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
150 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
151 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
154 return renderUnitTraining(city, units, unitTrainingQueues) + topbar({
160 server.post<{body: {sector: string}}, string>('/poll/map', async req => {
161 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
162 const city = await cityRepo.getUsersCity(account.id);
163 const unreadMail = await mailRepo.countUnread(account.id);
165 let sector = city.sector_id;
166 if(req.body.sector) {
168 sector = parseInt(req.body.sector);
171 sector = city.sector_id;
176 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
177 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
178 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
179 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
182 return renderOverworldMap(await cityRepo.findAllInSector(sector), city, sector) + topbar({
188 server.get<{}, string>('/poll/mailroom', async req => {
189 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
190 const city = await cityRepo.getUsersCity(account.id);
191 const unreadMail = await mailRepo.countUnread(account.id);
194 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
195 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
196 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
197 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
200 return renderMailroom(await mailRepo.listReceivedMessages(account.id)) + topbar({
210 building_type: string
212 }, string>('/cost/construction', async req => {
213 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
214 const city = await cityRepo.getUsersCity(account.id);
216 const amount = parseInt(req.body.amount.trim(), 10);
218 if(isNaN(amount) || amount < 1) {
221 const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
224 throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
228 credits: building.credits * amount,
229 alloys: building.alloys * amount,
230 energy: building.energy * amount,
231 land: building.land * amount,
235 return renderCost(cost, city);
243 }, string>('/cost/training', async req => {
244 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
245 const city = await cityRepo.getUsersCity(account.id);
246 const amount = parseInt(req.body.amount, 10);
248 if(isNaN(amount) || amount < 1) {
252 const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
254 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
258 population: unit.population * amount,
259 soldiers: unit.soldiers * amount,
260 attackers: unit.attackers * amount,
261 defenders: unit.defenders * amount,
262 credits: unit.credits * amount,
263 food: unit.food * amount,
264 time: unit.time * amount
271 building_type: string,
273 }, void>('/build', async req => {
274 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
275 const city = await cityRepo.getUsersCity(account.id);
277 const amount = parseInt(req.body.amount, 10);
279 throw new BadInputError('Please specify an amount > 0', ERROR_CODE.INVALID_AMOUNT);
281 const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
284 throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
287 const queueData = await cityRepo.buildBuilding(building, amount, city);
289 construction.trigger(queueData, { delay: queueData.due - Date.now() });
290 }, 'reload-construction-queue');
299 >('/units', async req => {
300 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
301 const city = await cityRepo.getUsersCity(acct.id);
303 const amount = parseInt(req.body.amount, 10) || 0;
304 console.log('request amount?!', amount);
306 throw new BadInputError('Please specify an amount > 0', ERROR_CODE.INVALID_AMOUNT);
308 const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
311 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
314 const queueData = await cityRepo.train(unit, amount, city);
315 unitTraining.trigger(queueData, { delay: queueData.due - Date.now() });
317 }, 'reload-unit-training');
325 sp_attackers: string,
330 >('/attack', async req => {
331 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
332 const city = await cityRepo.getUsersCity(acct.id);
333 const attackedCity = await cityRepo.findById(req.body.city);
336 soldiers: parseInt(req.body.soldiers),
337 attackers: parseInt(req.body.attackers),
338 defenders: parseInt(req.body.defenders),
339 sp_attackers: parseInt(req.body.sp_attackers),
340 sp_defenders: parseInt(req.body.sp_defenders)
343 const armyQueue = await cityRepo.attack(city, attackedCity, army);
345 fight.trigger(armyQueue, {
346 delay: armyQueue.due - Date.now()
348 }, 'reload-outgoing-attacks');
350 server.get<{params: {id: string}}, string>('/messages/:id', async req => {
351 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
352 const city = await cityRepo.getUsersCity(acct.id);
353 const msg = await mailRepo.getMessage(req.params.id, acct.id);
356 throw new NotFoundError('No such message', ERROR_CODE.DUPLICATE_CACHE_KEY);
360 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
361 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
362 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
363 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
366 await mailRepo.markAsRead(msg.id, msg.to_account);
367 const unreadMail = await mailRepo.countUnread(acct.id);
369 return renderMailroom(await mailRepo.listReceivedMessages(acct.id), msg) + topbar({
375 server.get<void, string>('/attacks/outgoing', async req => {
376 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
377 const city = await cityRepo.getUsersCity(acct.id);
378 const attacks = await cityRepo.armyRepository.listOutgoing(city.id);
380 return listOperations(attacks);
383 server.post<{body: {message: string}}, void>('/chat', async req => {
384 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
385 const now = Date.now();
387 if(!_.isEmpty(req.body.message)) {
388 // is this a command message?!
389 console.log(req.body.message);
390 if(req.body.message[0] === '/') {
391 console.log('Received command message from:', acct.username);
392 // this is a command message, don't record it in chat
393 // history, and only send the response to the single person who sent
394 let socket = server.getSocketFromAuthenticatedUser(req.authInfo);
398 if(req.body.message === '/online') {
399 socket.emit('/chat-message', renderPublicChatMessage(
401 `Online Users: ${cache.online_users.join(', ')}`
404 else if(req.body.message.indexOf('/give') === 0 && acct.username === 'xangelo') {
405 // expects message in format /give [username] [amount] [resource]
406 const [command, username, amount, resource] = req.body.message.split(' ');
407 const user = await accountRepo.FindOne({username: username});
408 const city = await cityRepo.getUsersCity(user.id);
410 console.log('giving!', req.body.message.split(' '));
412 await cityRepo.save({
414 [resource]: city[resource] + parseInt(amount)
417 socket.emit('/chat-message', renderPublicChatMessage(
419 `Gave ${user.username} ${amount} ${resource} ${city[resource]}=>${city[resource] + parseInt(amount)}`
422 else if(req.body.message.indexOf('/assume') === 0 && acct.username === 'xangelo') {
423 // expects /assume [username]
424 const [command, username] = req.body.message.split(' ');
425 const user = await accountRepo.FindOne({username: username});
426 const session = await accountRepo.session.FindOne({account_id: user.id});
428 socket.emit('/chat-message', renderPublicChatMessage(
430 `Login Link: /game.html?token=${session.id}&id=${session.account_id}`
434 console.log(req.body.message.indexOf('/give'), acct.username);
435 socket.emit('/chat-message', renderPublicChatMessage(
437 `The command ${req.body.message} is not valid`
442 const msg = renderPublicChatMessage(acct.username, req.body.message);
443 server.ws.emit('/chat-message', msg);
444 msgBuffer.unshift(msg);
445 while(msgBuffer.length > 30) {
454 server.post<{params: {queueId: string}}, void>('/construction/:queueId/cancel', async req => {
455 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
456 const city = await cityRepo.getUsersCity(acct.id);
458 if(!validator.isUUID(req.params.queueId)) {
459 throw new BadInputError('Invalid queue ID', ERROR_CODE.INVALID_BUILD_QUEUE);
462 // validate that this is an actual queue
463 const queue = await cityRepo.buildQueue.FindOne({owner: city.owner, id: req.params.queueId});
466 throw new NotFoundError('That queue does not exist', ERROR_CODE.INVALID_BUILD_QUEUE);
469 const [, building] = await Promise.all([
470 cityRepo.buildQueue.Delete({
472 id: req.params.queueId
474 cityRepo.buildingRepository.findBySlug(queue.building_type)
477 // now that it's deleted we can give the player back some percentage
478 // of resources based on how close they are to completion.
479 const diff = (queue.due - Date.now()) / (queue.due - queue.created);
480 // force a 20% loss minimum
481 const finalDiff = diff < 0.2 ? 0.2 : diff;
483 const costReturn: Partial<City> = {
485 credits: city.credits + Math.floor(building.credits * queue.amount * diff),
486 alloys: city.alloys + Math.floor(building.alloys * queue.amount * diff),
487 energy: city.energy + Math.floor(building.energy * queue.amount * diff),
488 usedSpace: city.usedSpace - (building.land * queue.amount)
491 console.log('update', costReturn)
493 await cityRepo.save(costReturn);
495 }, 'reload-construction-queue');
497 server.post<{params: {queueId: string}}, void>('/training/:queueId/cancel', async req => {
498 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
499 const city = await cityRepo.getUsersCity(acct.id);
501 if(!validator.isUUID(req.params.queueId)) {
502 throw new BadInputError('Invalid queue ID', ERROR_CODE.INVALID_BUILD_QUEUE);
505 const queue = await cityRepo.unitTrainigQueue.FindOne({owner: city.owner, id: req.params.queueId});
507 throw new NotFoundError('That queue does not exist', ERROR_CODE.INVALID_BUILD_QUEUE);
510 const [, unit] = await Promise.all([
511 cityRepo.unitTrainigQueue.Delete({
513 id: req.params.queueId
515 cityRepo.unitRepository.FindOne({slug: queue.unit_type})
518 // now that it's deleted we can give the player back some percentage
519 // of resources based on how close they are to completion.
520 const diff = (queue.due - Date.now()) / (queue.due - queue.created);
521 // force a 20% loss minimum
522 const finalDiff = diff < 0.2 ? 0.2 : diff;
524 const costReturn: Partial<City> = {
526 credits: city.credits + Math.floor(unit.credits * queue.amount * diff),
527 food: city.food + Math.floor(unit.food * queue.amount * diff),
528 population: city.population + Math.floor(unit.population * queue.amount),
529 soldiers: city.soldiers + Math.floor(unit.soldiers * queue.amount),
530 attackers: city.attackers + Math.floor(unit.attackers * queue.amount),
531 defenders: city.defenders + Math.floor(unit.attackers * queue.amount)
534 console.log('update', costReturn)
536 await cityRepo.save(costReturn);
538 }, 'reload-unit-training');
540 server.get<void, string>('/server-stats', async req => {
541 const date = new Date();
542 const min = date.getMinutes();
544 <div class="text-right">
545 <span class="success-text">${server.ws.engine.clientsCount} Online</span><br>
547 Server Time: ${date.getHours()}:${min < 10 ? '0'+min : min}
552 server.ws.on('connection', async socket => {
553 const auth = server.authFromUrl(socket.request.headers['referer']);
554 const acct = await accountRepo.validate(auth.authInfo.accountId, auth.authInfo.token);
556 server.ws.emit('/chat-message', msgBuffer.join("\n"));
558 server.ws.emit('/chat-message', renderPublicChatMessage('Server', `${acct.username} logged in`));
560 cache.online_users.push(acct.username);
562 socket.on('disconnect', () => {
563 cache.online_users.splice(cache.online_users.indexOf(acct.username), 1);