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';
25 const server = new HttpServer(config.API_PORT);
26 const accountRepo = new AccountRepository();
27 const cityRepo = new CityRepository();
28 const mailRepo = new MailRepository();
30 const msgBuffer: string[] = [];
34 new BullAdapter(tick.queue),
35 new BullAdapter(construction.queue),
36 new BullAdapter(unitTraining.queue),
37 new BullAdapter(fight.queue)
39 serverAdapter: server.bullAdapter,
43 body: {username: string, password: string}},
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);
51 const acct = await accountRepo.create(username, password);
53 // lets create the city!
54 await cityRepo.create(acct.id);
56 return `<div class="alert success">You are all signed up! You can go ahead and log in</div>`;
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);
64 const {account, session} = await accountRepo.login(username, password);
66 throw new NotFoundError('Invalid username or password', ERROR_CODE.USER_NOT_FOUND);
69 res.setHeader('hx-redirect', `/game.html?token=${session.id}&id=${session.account_id}`);
79 }}, string>('/attack-power', async req => {
80 const power = await cityRepo.power(req.body);
82 return power.toLocaleString();
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});
93 return await launchOffensive(city, acct || {
97 }, yourCity, account);
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);
107 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
108 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
109 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
110 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
113 return renderKingomOverview({
116 }, account) + topbar({...city, ...usage}, unreadMail);
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);
125 const buildQueues = await cityRepo.getBuildQueues(account.id);
127 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
128 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
129 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
130 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
132 return renderLandDevelopment(city, buildings, buildQueues) + topbar({...city, ...usage}, unreadMail);
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);
140 const unitTrainingQueues = await cityRepo.getUnitTrainingQueues(account.id);
141 const units = await cityRepo.unitRepository.list();
143 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
144 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
145 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
146 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
149 return renderUnitTraining(city, units, unitTrainingQueues) + topbar({
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);
160 let sector = city.sector_id;
161 if(req.body.sector) {
163 sector = parseInt(req.body.sector);
166 sector = city.sector_id;
171 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
172 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
173 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
174 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
177 return renderOverworldMap(await cityRepo.findAllInSector(sector), city, sector) + topbar({
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);
189 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
190 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
191 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
192 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
195 return renderMailroom(await mailRepo.listReceivedMessages(account.id)) + topbar({
205 building_type: string
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);
211 const amount = parseInt(req.body.amount.trim(), 10);
213 if(isNaN(amount) || amount < 1) {
216 const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
219 throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
223 credits: building.credits * amount,
224 alloys: building.alloys * amount,
225 energy: building.energy * amount,
226 land: building.land * amount,
230 return renderCost(cost, city);
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);
243 if(isNaN(amount) || amount < 1) {
247 const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
249 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
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
266 building_type: string,
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);
272 const amount = parseInt(req.body.amount, 10);
274 throw new BadInputError('Please specify an amount > 0', ERROR_CODE.INVALID_AMOUNT);
276 const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
279 throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
282 const queueData = await cityRepo.buildBuilding(building, amount, city);
284 construction.trigger(queueData, { delay: queueData.due - Date.now() });
285 }, 'reload-construction-queue');
294 >('/units', async req => {
295 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
296 const city = await cityRepo.getUsersCity(acct.id);
298 const amount = parseInt(req.body.amount, 10) || 0;
299 console.log('request amount?!', amount);
301 throw new BadInputError('Please specify an amount > 0', ERROR_CODE.INVALID_AMOUNT);
303 const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
306 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
309 const queueData = await cityRepo.train(unit, amount, city);
310 unitTraining.trigger(queueData, { delay: queueData.due - Date.now() });
312 }, 'reload-unit-training');
320 sp_attackers: string,
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);
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)
338 const armyQueue = await cityRepo.attack(city, attackedCity, army);
340 fight.trigger(armyQueue, {
341 delay: armyQueue.due - Date.now()
343 }, 'reload-outgoing-attacks');
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);
351 throw new NotFoundError('No such message', ERROR_CODE.DUPLICATE_CACHE_KEY);
355 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
356 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
357 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
358 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
361 await mailRepo.markAsRead(msg.id, msg.to_account);
362 const unreadMail = await mailRepo.countUnread(acct.id);
364 return renderMailroom(await mailRepo.listReceivedMessages(acct.id), msg) + topbar({
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);
375 return listOperations(attacks);
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();
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) {
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);
398 if(!validator.isUUID(req.params.queueId)) {
399 throw new BadInputError('Invalid queue ID', ERROR_CODE.INVALID_BUILD_QUEUE);
402 // validate that this is an actual queue
403 const queue = await cityRepo.buildQueue.FindOne({owner: city.owner, id: req.params.queueId});
406 throw new NotFoundError('That queue does not exist', ERROR_CODE.INVALID_BUILD_QUEUE);
409 const [, building] = await Promise.all([
410 cityRepo.buildQueue.Delete({
412 id: req.params.queueId
414 cityRepo.buildingRepository.findBySlug(queue.building_type)
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;
423 const costReturn: Partial<City> = {
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)
431 console.log('update', costReturn)
433 await cityRepo.save(costReturn);
435 }, 'reload-construction-queue');
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);
441 if(!validator.isUUID(req.params.queueId)) {
442 throw new BadInputError('Invalid queue ID', ERROR_CODE.INVALID_BUILD_QUEUE);
445 const queue = await cityRepo.unitTrainigQueue.FindOne({owner: city.owner, id: req.params.queueId});
447 throw new NotFoundError('That queue does not exist', ERROR_CODE.INVALID_BUILD_QUEUE);
450 const [, unit] = await Promise.all([
451 cityRepo.unitTrainigQueue.Delete({
453 id: req.params.queueId
455 cityRepo.unitRepository.FindOne({slug: queue.unit_type})
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;
464 const costReturn: Partial<City> = {
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)
474 console.log('update', costReturn)
476 await cityRepo.save(costReturn);
478 }, 'reload-unit-training');
480 server.get<void, string>('/server-stats', async req => {
481 const date = new Date();
483 <div class="text-right">
484 <span class="success-text">${(await server.ws.allSockets()).size} Online</span><br>
486 Server Time: ${date.getHours()}:${date.getMinutes()}
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);
495 server.ws.emit('/chat-message', msgBuffer.join("\n"));
497 server.ws.emit('/chat-message', renderPublicChatMessage('Server', `${acct.username} logged in`));