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);
105 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
106 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
107 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
108 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
111 return renderKingomOverview({
114 }, account) + topbar({...city, ...usage});
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();
122 const buildQueues = await cityRepo.getBuildQueues(account.id);
124 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
125 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
126 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
127 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
129 return renderLandDevelopment(city, buildings, buildQueues) + topbar({...city, ...usage});
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);
136 const unitTrainingQueues = await cityRepo.getUnitTrainingQueues(account.id);
137 const units = await cityRepo.unitRepository.list();
139 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
140 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
141 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
142 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
145 return renderUnitTraining(city, units, unitTrainingQueues) + topbar({
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);
155 let sector = city.sector_id;
156 if(req.body.sector) {
158 sector = parseInt(req.body.sector);
161 sector = city.sector_id;
166 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
167 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
168 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
169 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
172 return renderOverworldMap(await cityRepo.findAllInSector(sector), city, sector) + topbar({
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);
183 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
184 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
185 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
186 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
189 return renderMailroom(await mailRepo.listReceivedMessages(account.id)) + topbar({
199 building_type: string
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);
205 const amount = parseInt(req.body.amount.trim(), 10);
207 if(isNaN(amount) || amount < 1) {
210 const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
213 throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
217 credits: building.credits * amount,
218 alloys: building.alloys * amount,
219 energy: building.energy * amount,
220 land: building.land * amount,
224 return renderCost(cost, city);
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);
237 if(isNaN(amount) || amount < 1) {
241 const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
243 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
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
260 building_type: string,
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);
266 const amount = parseInt(req.body.amount, 10);
268 throw new BadInputError('Please specify an amount > 0', ERROR_CODE.INVALID_AMOUNT);
270 const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
273 throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
276 const queueData = await cityRepo.buildBuilding(building, amount, city);
278 construction.trigger(queueData, { delay: queueData.due - Date.now() });
279 }, 'reload-construction-queue');
288 >('/units', async req => {
289 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
290 const city = await cityRepo.getUsersCity(acct.id);
292 const amount = parseInt(req.body.amount, 10) || 0;
293 console.log('request amount?!', amount);
295 throw new BadInputError('Please specify an amount > 0', ERROR_CODE.INVALID_AMOUNT);
297 const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
300 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
303 const queueData = await cityRepo.train(unit, amount, city);
304 unitTraining.trigger(queueData, { delay: queueData.due - Date.now() });
306 }, 'reload-unit-training');
314 sp_attackers: string,
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);
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)
332 const armyQueue = await cityRepo.attack(city, attackedCity, army);
334 fight.trigger(armyQueue, {
335 delay: armyQueue.due - Date.now()
337 }, 'reload-outgoing-attacks');
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);
343 return JSON.stringify(msgs);
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);
351 throw new NotFoundError('No such message', ERROR_CODE.DUPLICATE_CACHE_KEY);
354 await mailRepo.markAsRead(msg.id, msg.to_account);
356 return renderMessage(msg);
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);
364 return listOperations(attacks);
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();
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) {
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);
387 if(!validator.isUUID(req.params.queueId)) {
388 throw new BadInputError('Invalid queue ID', ERROR_CODE.INVALID_BUILD_QUEUE);
391 // validate that this is an actual queue
392 const queue = await cityRepo.buildQueue.FindOne({owner: city.owner, id: req.params.queueId});
395 throw new NotFoundError('That queue does not exist', ERROR_CODE.INVALID_BUILD_QUEUE);
398 const [, building] = await Promise.all([
399 cityRepo.buildQueue.Delete({
401 id: req.params.queueId
403 cityRepo.buildingRepository.findBySlug(queue.building_type)
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;
412 const costReturn: Partial<City> = {
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)
420 console.log('update', costReturn)
422 await cityRepo.save(costReturn);
424 }, 'reload-construction-queue');
426 server.get<void, string>('/server-stats', async req => {
427 const date = new Date();
429 <div class="text-right">
430 <span class="success-text">${(await server.ws.allSockets()).size} Online</span><br>
432 Server Time: ${date.getHours()}:${date.getMinutes()}
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);
441 server.ws.emit('/chat-message', msgBuffer.join("\n"));
443 server.ws.emit('/chat-message', renderPublicChatMessage('Server', `${acct.username} logged in`));