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 cache: Record<string, any> = {
33 const msgBuffer: string[] = [];
37 new BullAdapter(tick.queue),
38 new BullAdapter(construction.queue),
39 new BullAdapter(unitTraining.queue),
40 new BullAdapter(fight.queue)
42 serverAdapter: server.bullAdapter,
46 body: {username: string, password: string}},
49 ('/accounts', async (req) => {
50 const { username, password} = req.body;
51 if(!username || !password || username.length < 3 || password.length < 3) {
52 throw new BadInputError('Invalid username or password', ERROR_CODE.INVALID_USERNAME);
54 const acct = await accountRepo.create(username, password);
56 // lets create the city!
57 await cityRepo.create(acct.id);
59 return `<div class="alert success">You are all signed up! You can go ahead and log in</div>`;
62 server.post<{body: {username: string, password: string}}, void>('/login', async (req, raw, res) => {
63 const { username, password} = req.body;
64 if(!username || !password || username.length < 3 || password.length < 3) {
65 throw new BadInputError('Invalid username or password', ERROR_CODE.INVALID_USERNAME);
67 const {account, session} = await accountRepo.login(username, password);
69 throw new NotFoundError('Invalid username or password', ERROR_CODE.USER_NOT_FOUND);
72 res.setHeader('hx-redirect', `/game.html?token=${session.id}&id=${session.account_id}`);
82 }}, string>('/attack-power', async req => {
83 const power = await cityRepo.power(req.body);
85 return power.toLocaleString();
89 server.get<{params: { cityId: string }}, string>('/city/:cityId', async req => {
90 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
91 const yourCity = await cityRepo.getUsersCity(account.id);
92 const city = await cityRepo.findById(req.params.cityId);
93 const acct = await accountRepo.FindOne({id: city.owner});
96 return await launchOffensive(city, acct || {
100 }, yourCity, account);
103 server.get<{}, string>('/poll/overview', async req => {
104 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
105 const city = await cityRepo.getUsersCity(account.id);
106 const unreadMail = await mailRepo.countUnread(account.id);
110 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
111 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
112 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
113 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
116 return renderKingomOverview({
119 }, account) + topbar({...city, ...usage}, unreadMail);
122 server.get<{}, string>('/poll/construction', async req => {
123 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
124 const city = await cityRepo.getUsersCity(account.id);
125 const buildings = await cityRepo.buildingRepository.list();
126 const unreadMail = await mailRepo.countUnread(account.id);
128 const buildQueues = await cityRepo.getBuildQueues(account.id);
130 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
131 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
132 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
133 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
135 return renderLandDevelopment(city, buildings, buildQueues) + topbar({...city, ...usage}, unreadMail);
138 server.get<{}, string>('/poll/unit-training', async req => {
139 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
140 const city = await cityRepo.getUsersCity(account.id);
141 const unreadMail = await mailRepo.countUnread(account.id);
143 const unitTrainingQueues = await cityRepo.getUnitTrainingQueues(account.id);
144 const units = await cityRepo.unitRepository.list();
146 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
147 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
148 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
149 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
152 return renderUnitTraining(city, units, unitTrainingQueues) + topbar({
158 server.post<{body: {sector: string}}, string>('/poll/map', async req => {
159 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
160 const city = await cityRepo.getUsersCity(account.id);
161 const unreadMail = await mailRepo.countUnread(account.id);
163 let sector = city.sector_id;
164 if(req.body.sector) {
166 sector = parseInt(req.body.sector);
169 sector = city.sector_id;
174 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
175 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
176 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
177 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
180 return renderOverworldMap(await cityRepo.findAllInSector(sector), city, sector) + topbar({
186 server.get<{}, string>('/poll/mailroom', async req => {
187 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
188 const city = await cityRepo.getUsersCity(account.id);
189 const unreadMail = await mailRepo.countUnread(account.id);
192 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
193 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
194 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
195 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
198 return renderMailroom(await mailRepo.listReceivedMessages(account.id)) + topbar({
208 building_type: string
210 }, string>('/cost/construction', async req => {
211 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
212 const city = await cityRepo.getUsersCity(account.id);
214 const amount = parseInt(req.body.amount.trim(), 10);
216 if(isNaN(amount) || amount < 1) {
219 const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
222 throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
226 credits: building.credits * amount,
227 alloys: building.alloys * amount,
228 energy: building.energy * amount,
229 land: building.land * amount,
233 return renderCost(cost, city);
241 }, string>('/cost/training', async req => {
242 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
243 const city = await cityRepo.getUsersCity(account.id);
244 const amount = parseInt(req.body.amount, 10);
246 if(isNaN(amount) || amount < 1) {
250 const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
252 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
256 population: unit.population * amount,
257 soldiers: unit.soldiers * amount,
258 attackers: unit.attackers * amount,
259 defenders: unit.defenders * amount,
260 credits: unit.credits * amount,
261 food: unit.food * amount,
262 time: unit.time * amount
269 building_type: string,
271 }, void>('/build', async req => {
272 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
273 const city = await cityRepo.getUsersCity(account.id);
275 const amount = parseInt(req.body.amount, 10);
277 throw new BadInputError('Please specify an amount > 0', ERROR_CODE.INVALID_AMOUNT);
279 const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
282 throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
285 const queueData = await cityRepo.buildBuilding(building, amount, city);
287 construction.trigger(queueData, { delay: queueData.due - Date.now() });
288 }, 'reload-construction-queue');
297 >('/units', async req => {
298 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
299 const city = await cityRepo.getUsersCity(acct.id);
301 const amount = parseInt(req.body.amount, 10) || 0;
302 console.log('request amount?!', amount);
304 throw new BadInputError('Please specify an amount > 0', ERROR_CODE.INVALID_AMOUNT);
306 const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
309 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
312 const queueData = await cityRepo.train(unit, amount, city);
313 unitTraining.trigger(queueData, { delay: queueData.due - Date.now() });
315 }, 'reload-unit-training');
323 sp_attackers: string,
328 >('/attack', async req => {
329 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
330 const city = await cityRepo.getUsersCity(acct.id);
331 const attackedCity = await cityRepo.findById(req.body.city);
334 soldiers: parseInt(req.body.soldiers),
335 attackers: parseInt(req.body.attackers),
336 defenders: parseInt(req.body.defenders),
337 sp_attackers: parseInt(req.body.sp_attackers),
338 sp_defenders: parseInt(req.body.sp_defenders)
341 const armyQueue = await cityRepo.attack(city, attackedCity, army);
343 fight.trigger(armyQueue, {
344 delay: armyQueue.due - Date.now()
346 }, 'reload-outgoing-attacks');
348 server.get<{params: {id: string}}, string>('/messages/:id', async req => {
349 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
350 const city = await cityRepo.getUsersCity(acct.id);
351 const msg = await mailRepo.getMessage(req.params.id, acct.id);
354 throw new NotFoundError('No such message', ERROR_CODE.DUPLICATE_CACHE_KEY);
358 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
359 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
360 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
361 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
364 await mailRepo.markAsRead(msg.id, msg.to_account);
365 const unreadMail = await mailRepo.countUnread(acct.id);
367 return renderMailroom(await mailRepo.listReceivedMessages(acct.id), msg) + topbar({
373 server.get<void, string>('/attacks/outgoing', async req => {
374 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
375 const city = await cityRepo.getUsersCity(acct.id);
376 const attacks = await cityRepo.armyRepository.listOutgoing(city.id);
378 return listOperations(attacks);
381 server.post<{body: {message: string}}, void>('/chat', async req => {
382 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
383 const now = Date.now();
385 if(!_.isEmpty(req.body.message)) {
386 const msg = renderPublicChatMessage(acct.username, req.body.message);
387 server.ws.emit('/chat-message', msg);
388 msgBuffer.unshift(msg);
389 while(msgBuffer.length > 30) {
397 server.post<{params: {queueId: string}}, void>('/construction/:queueId/cancel', async req => {
398 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
399 const city = await cityRepo.getUsersCity(acct.id);
401 if(!validator.isUUID(req.params.queueId)) {
402 throw new BadInputError('Invalid queue ID', ERROR_CODE.INVALID_BUILD_QUEUE);
405 // validate that this is an actual queue
406 const queue = await cityRepo.buildQueue.FindOne({owner: city.owner, id: req.params.queueId});
409 throw new NotFoundError('That queue does not exist', ERROR_CODE.INVALID_BUILD_QUEUE);
412 const [, building] = await Promise.all([
413 cityRepo.buildQueue.Delete({
415 id: req.params.queueId
417 cityRepo.buildingRepository.findBySlug(queue.building_type)
420 // now that it's deleted we can give the player back some percentage
421 // of resources based on how close they are to completion.
422 const diff = (queue.due - Date.now()) / (queue.due - queue.created);
423 // force a 20% loss minimum
424 const finalDiff = diff < 0.2 ? 0.2 : diff;
426 const costReturn: Partial<City> = {
428 credits: city.credits + Math.floor(building.credits * queue.amount * diff),
429 alloys: city.alloys + Math.floor(building.alloys * queue.amount * diff),
430 energy: city.energy + Math.floor(building.energy * queue.amount * diff),
431 usedSpace: city.usedSpace - (building.land * queue.amount)
434 console.log('update', costReturn)
436 await cityRepo.save(costReturn);
438 }, 'reload-construction-queue');
440 server.post<{params: {queueId: string}}, void>('/training/:queueId/cancel', async req => {
441 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
442 const city = await cityRepo.getUsersCity(acct.id);
444 if(!validator.isUUID(req.params.queueId)) {
445 throw new BadInputError('Invalid queue ID', ERROR_CODE.INVALID_BUILD_QUEUE);
448 const queue = await cityRepo.unitTrainigQueue.FindOne({owner: city.owner, id: req.params.queueId});
450 throw new NotFoundError('That queue does not exist', ERROR_CODE.INVALID_BUILD_QUEUE);
453 const [, unit] = await Promise.all([
454 cityRepo.unitTrainigQueue.Delete({
456 id: req.params.queueId
458 cityRepo.unitRepository.FindOne({slug: queue.unit_type})
461 // now that it's deleted we can give the player back some percentage
462 // of resources based on how close they are to completion.
463 const diff = (queue.due - Date.now()) / (queue.due - queue.created);
464 // force a 20% loss minimum
465 const finalDiff = diff < 0.2 ? 0.2 : diff;
467 const costReturn: Partial<City> = {
469 credits: city.credits + Math.floor(unit.credits * queue.amount * diff),
470 food: city.food + Math.floor(unit.food * queue.amount * diff),
471 population: city.population + Math.floor(unit.population * queue.amount),
472 soldiers: city.soldiers + Math.floor(unit.soldiers * queue.amount),
473 attackers: city.attackers + Math.floor(unit.attackers * queue.amount),
474 defenders: city.defenders + Math.floor(unit.attackers * queue.amount)
477 console.log('update', costReturn)
479 await cityRepo.save(costReturn);
481 }, 'reload-unit-training');
483 server.get<void, string>('/server-stats', async req => {
484 const date = new Date();
485 const min = date.getMinutes();
487 <div class="text-right">
488 <span class="success-text">${server.ws.engine.clientsCount} Online</span><br>
490 Server Time: ${date.getHours()}:${min < 10 ? '0'+min : min}
495 server.ws.on('connection', async socket => {
496 const auth = server.authFromUrl(socket.request.headers['referer']);
497 const acct = await accountRepo.validate(auth.authInfo.accountId, auth.authInfo.token);
499 server.ws.emit('/chat-message', msgBuffer.join("\n"));
501 server.ws.emit('/chat-message', renderPublicChatMessage('Server', `${acct.username} logged in`));
503 cache.online_users.push(acct.username);
505 socket.on('disconnect', () => {
506 cache.online_users.splice(cache.online_users.indexOf(acct.username), 1);