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';
22 const server = new HttpServer(config.API_PORT);
24 const accountRepo = new AccountRepository();
25 const cityRepo = new CityRepository();
26 const mailRepo = new MailRepository();
30 new BullAdapter(tick.queue),
31 new BullAdapter(construction.queue),
32 new BullAdapter(unitTraining.queue),
33 new BullAdapter(fight.queue)
35 serverAdapter: server.bullAdapter,
39 body: {username: string, password: string}},
42 ('/accounts', async (req) => {
43 const { username, password} = req.body;
44 if(!username || !password || username.length < 3 || password.length < 3) {
45 throw new BadInputError('Invalid username or password', ERROR_CODE.INVALID_USERNAME);
47 const acct = await accountRepo.create(username, password);
49 // lets create the city!
50 await cityRepo.create(acct.id);
52 return `<div class="alert success">You are all signed up! You can go ahead and log in</div>`;
55 server.post<{body: {username: string, password: string}}, void>('/login', async (req, raw, res) => {
56 const { username, password} = req.body;
57 if(!username || !password || username.length < 3 || password.length < 3) {
58 throw new BadInputError('Invalid username or password', ERROR_CODE.INVALID_USERNAME);
60 const {account, session} = await accountRepo.login(username, password);
62 throw new NotFoundError('Invalid username or password', ERROR_CODE.USER_NOT_FOUND);
65 res.setHeader('hx-redirect', `/game.html?token=${session.id}&id=${session.account_id}`);
75 }}, string>('/attack-power', async req => {
76 const power = await cityRepo.power(req.body);
78 return power.toLocaleString();
82 server.get<{params: { cityId: string }}, string>('/city/:cityId', async req => {
83 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
84 const yourCity = await cityRepo.getUsersCity(account.id);
85 const city = await cityRepo.findById(req.params.cityId);
86 const acct = await accountRepo.FindOne({id: city.owner});
89 return await launchOffensive(city, acct || {
93 }, yourCity, account);
96 server.get<{}, string>('/poll/overview', async req => {
97 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
98 const city = await cityRepo.getUsersCity(account.id);
101 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
102 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
103 energyUsagePerTick: 0,
104 energyProductionPerTick: 0
107 return renderKingomOverview({
110 }, account) + topbar({...city, ...usage});
113 server.get<{}, string>('/poll/construction', async req => {
114 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
115 const city = await cityRepo.getUsersCity(account.id);
116 const buildings = await cityRepo.buildingRepository.list();
118 const buildQueues = await cityRepo.getBuildQueues(account.id);
120 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
121 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
122 energyUsagePerTick: 0,
123 energyProductionPerTick: 0
125 return renderLandDevelopment(city, buildings, buildQueues) + topbar({...city, ...usage});
128 server.get<{}, string>('/poll/unit-training', async req => {
129 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
130 const city = await cityRepo.getUsersCity(account.id);
132 const unitTrainingQueues = await cityRepo.getUnitTrainingQueues(account.id);
133 const units = await cityRepo.unitRepository.list();
135 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
136 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
137 energyUsagePerTick: 0,
138 energyProductionPerTick: 0
141 return renderUnitTraining(city, units, unitTrainingQueues) + topbar({
147 server.post<{body: {sector: string}}, string>('/poll/map', async req => {
148 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
149 const city = await cityRepo.getUsersCity(account.id);
151 let sector = city.sector_id;
152 if(req.body.sector) {
154 sector = parseInt(req.body.sector);
157 sector = city.sector_id;
162 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
163 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
164 energyUsagePerTick: 0,
165 energyProductionPerTick: 0
168 return renderOverworldMap(await cityRepo.findAllInSector(sector), city, sector) + topbar({
174 server.get<{}, string>('/poll/mailroom', async req => {
175 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
176 const city = await cityRepo.getUsersCity(account.id);
179 foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
180 foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
181 energyUsagePerTick: 0,
182 energyProductionPerTick: 0
185 return renderMailroom(await mailRepo.listReceivedMessages(account.id)) + topbar({
195 building_type: string
197 }, string>('/cost/construction', async req => {
198 const amount = parseInt(req.body.amount.trim(), 10);
200 if(isNaN(amount) || amount < 1) {
203 const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
206 throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
210 credits: building.credits * amount,
211 alloys: building.alloys * amount,
212 energy: building.energy * amount,
213 land: building.land * amount,
217 return renderCost(cost);
225 }, string>('/cost/training', async req => {
226 const amount = parseInt(req.body.amount, 10);
228 if(isNaN(amount) || amount < 1) {
232 const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
234 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
238 population: unit.population * amount,
239 soldiers: unit.soldiers * amount,
240 attackers: unit.attackers * amount,
241 defenders: unit.defenders * amount,
242 credits: unit.credits * amount,
243 food: unit.food * amount,
244 time: unit.time * amount
251 building_type: string,
253 }, void>('/build', async req => {
254 const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
255 const city = await cityRepo.getUsersCity(account.id);
257 const amount = parseInt(req.body.amount, 10);
258 const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
261 throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
264 const queueData = await cityRepo.buildBuilding(building, amount, city);
266 construction.trigger(queueData, { delay: queueData.due });
267 }, 'reload-construction-queue');
276 >('/units', async req => {
277 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
278 const city = await cityRepo.getUsersCity(acct.id);
280 const amount = parseInt(req.body.amount, 10);
281 const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
284 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
287 const queueData = await cityRepo.train(unit, amount, city);
288 unitTraining.trigger(queueData, { delay: queueData.due });
290 }, 'reload-unit-training');
298 sp_attackers: string,
303 >('/attack', async req => {
304 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
305 const city = await cityRepo.getUsersCity(acct.id);
306 const attackedCity = await cityRepo.findById(req.body.city);
309 soldiers: parseInt(req.body.soldiers),
310 attackers: parseInt(req.body.attackers),
311 defenders: parseInt(req.body.defenders),
312 sp_attackers: parseInt(req.body.sp_attackers),
313 sp_defenders: parseInt(req.body.sp_defenders)
316 const armyQueue = await cityRepo.attack(city, attackedCity, army);
318 fight.trigger(armyQueue, {
319 delay: armyQueue.due - Date.now()
321 }, 'reload-outgoing-attacks');
323 server.get<void, string>('/messages', async req => {
324 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
325 const msgs = await mailRepo.listReceivedMessages(acct.id);
327 return JSON.stringify(msgs);
330 server.get<{params: {id: string}}, string>('/messages/:id', async req => {
331 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
332 const msg = await mailRepo.getMessage(req.params.id, acct.id);
335 throw new NotFoundError('No such message', ERROR_CODE.DUPLICATE_CACHE_KEY);
338 await mailRepo.markAsRead(msg.id, msg.to_account);
340 return renderMessage(msg);
343 server.get<void, string>('/attacks/outgoing', async req => {
344 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
345 const city = await cityRepo.getUsersCity(acct.id);
346 const attacks = await cityRepo.armyRepository.listOutgoing(city.id);
348 return listOperations(attacks);