1 import { HttpServer } from './lib/server';
2 import * as config from './config';
3 import { AccountRepository } from './repository/accounts';
4 import { 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';
24 const server = new HttpServer(config.API_PORT);
25 const accountRepo = new AccountRepository();
26 const cityRepo = new CityRepository();
27 const mailRepo = new MailRepository();
29 const msgBuffer: string[] = [];
33 new BullAdapter(tick.queue),
34 new BullAdapter(construction.queue),
35 new BullAdapter(unitTraining.queue),
36 new BullAdapter(fight.queue)
38 serverAdapter: server.bullAdapter,
42 body: {username: string, password: string}},
45 ('/accounts', async (req) => {
46 const { username, password} = req.body;
47 if(!username || !password || username.length < 3 || password.length < 3) {
48 throw new BadInputError('Invalid username or password', ERROR_CODE.INVALID_USERNAME);
50 const acct = await accountRepo.create(username, password);
52 // lets create the city!
53 await cityRepo.create(acct.id);
55 return `<div class="alert success">You are all signed up! You can go ahead and log in</div>`;
58 server.post<{body: {username: string, password: string}}, void>('/login', async (req, raw, res) => {
59 const { username, password} = req.body;
60 if(!username || !password || username.length < 3 || password.length < 3) {
61 throw new BadInputError('Invalid username or password', ERROR_CODE.INVALID_USERNAME);
63 const {account, session} = await accountRepo.login(username, password);
65 throw new NotFoundError('Invalid username or password', ERROR_CODE.USER_NOT_FOUND);
68 res.setHeader('hx-redirect', `/game.html?token=${session.id}&id=${session.account_id}`);
78 }}, string>('/attack-power', async req => {
79 const power = await cityRepo.power(req.body);
81 return power.toLocaleString();
85 server.get<{params: { cityId: string }}, string>('/city/:cityId', async req => {
86 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
87 const yourCity = await cityRepo.getUsersCity(account.id);
88 const city = await cityRepo.findById(req.params.cityId);
89 const acct = await accountRepo.FindOne({id: city.owner});
92 return await launchOffensive(city, acct || {
96 }, yourCity, account);
99 server.get<{}, string>('/poll/overview', async req => {
100 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
101 const city = await cityRepo.getUsersCity(account.id);
104 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
105 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
106 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
107 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
110 return renderKingomOverview({
113 }, account) + topbar({...city, ...usage});
116 server.get<{}, string>('/poll/construction', async req => {
117 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
118 const city = await cityRepo.getUsersCity(account.id);
119 const buildings = await cityRepo.buildingRepository.list();
121 const buildQueues = await cityRepo.getBuildQueues(account.id);
123 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
124 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
125 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
126 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
128 return renderLandDevelopment(city, buildings, buildQueues) + topbar({...city, ...usage});
131 server.get<{}, string>('/poll/unit-training', async req => {
132 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
133 const city = await cityRepo.getUsersCity(account.id);
135 const unitTrainingQueues = await cityRepo.getUnitTrainingQueues(account.id);
136 const units = await cityRepo.unitRepository.list();
138 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
139 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
140 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
141 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
144 return renderUnitTraining(city, units, unitTrainingQueues) + topbar({
150 server.post<{body: {sector: string}}, string>('/poll/map', async req => {
151 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
152 const city = await cityRepo.getUsersCity(account.id);
154 let sector = city.sector_id;
155 if(req.body.sector) {
157 sector = parseInt(req.body.sector);
160 sector = city.sector_id;
165 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
166 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
167 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
168 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
171 return renderOverworldMap(await cityRepo.findAllInSector(sector), city, sector) + topbar({
177 server.get<{}, string>('/poll/mailroom', async req => {
178 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
179 const city = await cityRepo.getUsersCity(account.id);
182 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
183 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
184 energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
185 energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
188 return renderMailroom(await mailRepo.listReceivedMessages(account.id)) + topbar({
198 building_type: string
200 }, string>('/cost/construction', async req => {
201 const amount = parseInt(req.body.amount.trim(), 10);
203 if(isNaN(amount) || amount < 1) {
206 const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
209 throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
213 credits: building.credits * amount,
214 alloys: building.alloys * amount,
215 energy: building.energy * amount,
216 land: building.land * amount,
220 return renderCost(cost);
228 }, string>('/cost/training', async req => {
229 const amount = parseInt(req.body.amount, 10);
231 if(isNaN(amount) || amount < 1) {
235 const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
237 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
241 population: unit.population * amount,
242 soldiers: unit.soldiers * amount,
243 attackers: unit.attackers * amount,
244 defenders: unit.defenders * amount,
245 credits: unit.credits * amount,
246 food: unit.food * amount,
247 time: unit.time * amount
254 building_type: string,
256 }, void>('/build', async req => {
257 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
258 const city = await cityRepo.getUsersCity(account.id);
260 const amount = parseInt(req.body.amount, 10);
262 throw new BadInputError('Please specify an amount > 0', ERROR_CODE.INVALID_AMOUNT);
264 const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
267 throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
270 const queueData = await cityRepo.buildBuilding(building, amount, city);
272 construction.trigger(queueData, { delay: queueData.due });
273 }, 'reload-construction-queue');
282 >('/units', async req => {
283 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
284 const city = await cityRepo.getUsersCity(acct.id);
286 const amount = parseInt(req.body.amount, 10);
288 throw new BadInputError('Please specify an amount > 0', ERROR_CODE.INVALID_AMOUNT);
290 const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
293 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
296 const queueData = await cityRepo.train(unit, amount, city);
297 unitTraining.trigger(queueData, { delay: queueData.due });
299 }, 'reload-unit-training');
307 sp_attackers: string,
312 >('/attack', async req => {
313 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
314 const city = await cityRepo.getUsersCity(acct.id);
315 const attackedCity = await cityRepo.findById(req.body.city);
318 soldiers: parseInt(req.body.soldiers),
319 attackers: parseInt(req.body.attackers),
320 defenders: parseInt(req.body.defenders),
321 sp_attackers: parseInt(req.body.sp_attackers),
322 sp_defenders: parseInt(req.body.sp_defenders)
325 const armyQueue = await cityRepo.attack(city, attackedCity, army);
327 fight.trigger(armyQueue, {
328 delay: armyQueue.due - Date.now()
330 }, 'reload-outgoing-attacks');
332 server.get<void, string>('/messages', async req => {
333 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
334 const msgs = await mailRepo.listReceivedMessages(acct.id);
336 return JSON.stringify(msgs);
339 server.get<{params: {id: string}}, string>('/messages/:id', async req => {
340 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
341 const msg = await mailRepo.getMessage(req.params.id, acct.id);
344 throw new NotFoundError('No such message', ERROR_CODE.DUPLICATE_CACHE_KEY);
347 await mailRepo.markAsRead(msg.id, msg.to_account);
349 return renderMessage(msg);
352 server.get<void, string>('/attacks/outgoing', async req => {
353 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
354 const city = await cityRepo.getUsersCity(acct.id);
355 const attacks = await cityRepo.armyRepository.listOutgoing(city.id);
357 return listOperations(attacks);
360 server.post<{body: {message: string}}, void>('/chat', async req => {
361 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
362 const now = Date.now();
364 if(!_.isEmpty(req.body.message)) {
365 const msg = renderPublicChatMessage(acct.username, req.body.message);
366 server.ws.emit('/chat-message', msg);
367 msgBuffer.unshift(msg);
368 while(msgBuffer.length > 30) {
376 server.ws.on('connection', async socket => {
377 const auth = server.authFromUrl(socket.request.headers['referer']);
378 const acct = await accountRepo.validate(auth.authInfo.accountId, auth.authInfo.token);
380 server.ws.emit('/chat-message', msgBuffer.join("\n"));
382 server.ws.emit('/chat-message', renderPublicChatMessage('Server', `${acct.username} logged in`));