import { HttpServer } from './lib/server';
import * as config from './config';
import { AccountRepository } from './repository/accounts';
-import { CityRepository } from './repository/city';
+import { City, CityRepository } from './repository/city';
import { MailRepository } from './repository/mail';
import {BadInputError, ERROR_CODE, NotFoundError} from './errors';
import { renderKingomOverview } from './render/kingdom-overview';
import { unitTraining } from './tasks/unit-training';
import { fight } from './tasks/fight';
import { renderUnitTraining } from './render/unit-training';
-import { launchOffensive, listOperations, renderOverworldMap } from './render/fight';
+import { launchOffensive, listOperations, renderOverworldMap } from './render/map';
import { createBullBoard } from '@bull-board/api';
import { BullAdapter } from '@bull-board/api/bullAdapter';
import _ from 'lodash';
-import { renderMailroom, renderMessage } from './render/mail';
+import { renderCost } from './render/costs';
+import {renderMailroom, renderMessage} from './render/mail';
+import {topbar} from './render/topbar';
+import {renderPublicChatMessage} from './render/chat-message';
+import validator from 'validator';
+import {Socket} from 'socket.io';
-const server = new HttpServer(config.API_PORT);
+const server = new HttpServer(config.API_PORT);
const accountRepo = new AccountRepository();
const cityRepo = new CityRepository();
const mailRepo = new MailRepository();
+const cache: Record<string, any> = {
+ online_users: []
+};
+
+const msgBuffer: string[] = [];
+
createBullBoard({
queues: [
new BullAdapter(tick.queue),
// lets create the city!
await cityRepo.create(acct.id);
- return `<p>You are all signed up! You can go ahead and log in</p>`;
+ return `<div class="alert success">You are all signed up! You can go ahead and log in</div>`;
});
server.post<{body: {username: string, password: string}}, void>('/login', async (req, raw, res) => {
});
-server.get<{}, string>('/city', async req => {
- const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
- const city = await cityRepo.FindOne({ owner: account.id });
-
-
- const buildQueues = await cityRepo.getBuildQueues(account.id);
- const unitTrainingQueues = await cityRepo.getUnitTrainingQueues(account.id);
-
- const buildings = await cityRepo.buildingRepository.list();
- const units = await cityRepo.unitRepository.list();
-
- let html = `
- <h2>Kingom Overview</h2>
- ${renderKingomOverview(city, account)}
- <hr>
- <h2>Land Development</h2>
- ${renderLandDevelopment(city, buildings, buildQueues)}
- <h2>Unit Training</h2>
- ${renderUnitTraining(city, units, unitTrainingQueues)},
- <h2>Map</h2>
- ${renderOverworldMap(await cityRepo.FindAll(), city)}
- <h2>Mail</h2>
- ${renderMailroom(await mailRepo.listReceivedMessages(account.id))}
- `;
- return html;
-});
-
server.post<{body: {
soldiers: number,
attackers: number,
server.get<{params: { cityId: string }}, string>('/city/:cityId', async req => {
const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
- const yourCity = await cityRepo.FindOne({ owner: account.id });
- const city = await cityRepo.FindOne({ id: req.params.cityId });
+ const yourCity = await cityRepo.getUsersCity(account.id);
+ const city = await cityRepo.findById(req.params.cityId);
const acct = await accountRepo.FindOne({id: city.owner});
server.get<{}, string>('/poll/overview', async req => {
const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
- const city = await cityRepo.FindOne({ owner: account.id });
+ const city = await cityRepo.getUsersCity(account.id);
+ const unreadMail = await mailRepo.countUnread(account.id);
- return renderKingomOverview(city, account);
+
+ const usage = {
+ foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
+ foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
+ energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
+ energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
+ }
+
+ return renderKingomOverview({
+ ...city,
+ ...usage
+ }, account) + topbar({...city, ...usage}, unreadMail);
});
-server.get<{}, string>('/queue/construction', async req => {
+server.get<{}, string>('/poll/construction', async req => {
const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
- const city = await cityRepo.FindOne({ owner: account.id });
+ const city = await cityRepo.getUsersCity(account.id);
const buildings = await cityRepo.buildingRepository.list();
+ const unreadMail = await mailRepo.countUnread(account.id);
const buildQueues = await cityRepo.getBuildQueues(account.id);
- return renderLandDevelopment(city, buildings, buildQueues);
+ const usage = {
+ foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
+ foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
+ energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
+ energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
+ }
+ return renderLandDevelopment(city, buildings, buildQueues) + topbar({...city, ...usage}, unreadMail);
});
-server.get<{}, string>('/queue/units', async req => {
+server.get<{}, string>('/poll/unit-training', async req => {
const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
- const city = await cityRepo.FindOne({ owner: account.id });
+ const city = await cityRepo.getUsersCity(account.id);
+ const unreadMail = await mailRepo.countUnread(account.id);
const unitTrainingQueues = await cityRepo.getUnitTrainingQueues(account.id);
const units = await cityRepo.unitRepository.list();
+ const usage = {
+ foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
+ foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
+ energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
+ energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
+ }
+
+ return renderUnitTraining(city, units, unitTrainingQueues) + topbar({
+ ...city,
+ ...usage
+ }, unreadMail);
+});
- return renderUnitTraining(city, units, unitTrainingQueues);
+server.post<{body: {sector: string}}, string>('/poll/map', async req => {
+ const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
+ const city = await cityRepo.getUsersCity(account.id);
+ const unreadMail = await mailRepo.countUnread(account.id);
+
+ let sector = city.sector_id;
+ if(req.body.sector) {
+ try {
+ sector = parseInt(req.body.sector);
+ }
+ catch(e) {
+ sector = city.sector_id;
+ }
+ }
+
+ const usage = {
+ foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
+ foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
+ energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
+ energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
+ }
+
+ return renderOverworldMap(await cityRepo.findAllInSector(sector), city, sector) + topbar({
+ ...city,
+ ...usage
+ }, unreadMail);
+});
+
+server.get<{}, string>('/poll/mailroom', async req => {
+ const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
+ const city = await cityRepo.getUsersCity(account.id);
+ const unreadMail = await mailRepo.countUnread(account.id);
+
+ const usage = {
+ foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
+ foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
+ energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
+ energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
+ }
+
+ return renderMailroom(await mailRepo.listReceivedMessages(account.id)) + topbar({
+ ...city,
+ ...usage
+ }, unreadMail);
});
building_type: string
}
}, string>('/cost/construction', async req => {
- const amount = parseInt(req.body.amount, 10);
+ const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
+ const city = await cityRepo.getUsersCity(account.id);
+
+ const amount = parseInt(req.body.amount.trim(), 10);
+
+ if(isNaN(amount) || amount < 1) {
+ return '';
+ }
const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
if(!building) {
throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
}
- return JSON.stringify({
- gold: building.gold * amount,
- ore: building.ore * amount,
- logs: building.logs * amount,
+ const cost = {
+ credits: building.credits * amount,
+ alloys: building.alloys * amount,
+ energy: building.energy * amount,
land: building.land * amount,
time: building.time
- });
+ };
+
+ return renderCost(cost, city);
});
server.post<{
type: string;
}
}, string>('/cost/training', async req => {
+ const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
+ const city = await cityRepo.getUsersCity(account.id);
const amount = parseInt(req.body.amount, 10);
- const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
+ if(isNaN(amount) || amount < 1) {
+ return '';
+ }
+
+ const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
if(!unit) {
throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
}
- return JSON.stringify({
+ return renderCost({
population: unit.population * amount,
soldiers: unit.soldiers * amount,
attackers: unit.attackers * amount,
defenders: unit.defenders * amount,
- gold: unit.gold * amount,
- bushels: unit.bushels * amount
- });
+ credits: unit.credits * amount,
+ food: unit.food * amount,
+ time: unit.time * amount
+ }, city);
});
server.post<{
building_type: string,
}
}, void>('/build', async req => {
- const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
- const city = await cityRepo.getUsersCity(account.id);
+ const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
+ const city = await cityRepo.getUsersCity(account.id);
- const amount = parseInt(req.body.amount, 10);
- const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
+ const amount = parseInt(req.body.amount, 10);
+ if(amount < 1) {
+ throw new BadInputError('Please specify an amount > 0', ERROR_CODE.INVALID_AMOUNT);
+ }
+ const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
- if(!building) {
- throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
- }
+ if(!building) {
+ throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
+ }
- const queueData = await cityRepo.buildBuilding(building, amount ,city);
+ const queueData = await cityRepo.buildBuilding(building, amount, city);
- construction.trigger(queueData, { delay: queueData.due });
+ construction.trigger(queueData, { delay: queueData.due - Date.now() });
}, 'reload-construction-queue');
server.post<{
const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
const city = await cityRepo.getUsersCity(acct.id);
- const amount = parseInt(req.body.amount, 10);
+ const amount = parseInt(req.body.amount, 10) || 0;
+ console.log('request amount?!', amount);
+ if(amount < 1) {
+ throw new BadInputError('Please specify an amount > 0', ERROR_CODE.INVALID_AMOUNT);
+ }
const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
if(!unit) {
}
const queueData = await cityRepo.train(unit, amount, city);
- unitTraining.trigger(queueData, { delay: queueData.due });
+ unitTraining.trigger(queueData, { delay: queueData.due - Date.now() });
}, 'reload-unit-training');
>('/attack', async req => {
const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
const city = await cityRepo.getUsersCity(acct.id);
- const attackedCity = await cityRepo.FindOne({id: req.body.city});
+ const attackedCity = await cityRepo.findById(req.body.city);
const army = {
soldiers: parseInt(req.body.soldiers),
});
}, 'reload-outgoing-attacks');
-server.get<void, string>('/messages', async req => {
- const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
- const msgs = await mailRepo.listReceivedMessages(acct.id);
-
- return JSON.stringify(msgs);
-});
-
server.get<{params: {id: string}}, string>('/messages/:id', async req => {
const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
+ const city = await cityRepo.getUsersCity(acct.id);
const msg = await mailRepo.getMessage(req.params.id, acct.id);
if(!msg) {
throw new NotFoundError('No such message', ERROR_CODE.DUPLICATE_CACHE_KEY);
}
+ const usage = {
+ foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
+ foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
+ energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
+ energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
+ }
+
await mailRepo.markAsRead(msg.id, msg.to_account);
+ const unreadMail = await mailRepo.countUnread(acct.id);
- return renderMessage(msg);
+ return renderMailroom(await mailRepo.listReceivedMessages(acct.id), msg) + topbar({
+ ...city,
+ ...usage
+ }, unreadMail);
});
server.get<void, string>('/attacks/outgoing', async req => {
return listOperations(attacks);
});
+server.post<{body: {message: string}}, void>('/chat', async req => {
+ const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
+ const now = Date.now();
+
+ if(!_.isEmpty(req.body.message)) {
+ // is this a command message?!
+ console.log(req.body.message);
+ if(req.body.message[0] === '/') {
+ console.log('Received command message from:', acct.username);
+ // this is a command message, don't record it in chat
+ // history, and only send the response to the single person who sent
+ let socket = server.getSocketFromAuthenticatedUser(req.authInfo);
+ if(!socket) {
+ return;
+ }
+ if(req.body.message === '/online') {
+ socket.emit('/chat-message', renderPublicChatMessage(
+ 'Server',
+ `Online Users: ${cache.online_users.join(', ')}`
+ ));
+ }
+ else if(req.body.message.indexOf('/give') === 0 && acct.username === 'xangelo') {
+ // expects message in format /give [username] [amount] [resource]
+ const [command, username, amount, resource] = req.body.message.split(' ');
+ const user = await accountRepo.FindOne({username: username});
+ const city = await cityRepo.getUsersCity(user.id);
+
+ console.log('giving!', req.body.message.split(' '));
+
+ await cityRepo.save({
+ id: city.id,
+ [resource]: city[resource] + parseInt(amount)
+ });
+
+ socket.emit('/chat-message', renderPublicChatMessage(
+ 'Server',
+ `Gave ${user.username} ${amount} ${resource} ${city[resource]}=>${city[resource] + parseInt(amount)}`
+ ));
+ }
+ else if(req.body.message.indexOf('/assume') === 0 && acct.username === 'xangelo') {
+ // expects /assume [username]
+ const [command, username] = req.body.message.split(' ');
+ const user = await accountRepo.FindOne({username: username});
+ const session = await accountRepo.session.FindOne({account_id: user.id});
+
+ socket.emit('/chat-message', renderPublicChatMessage(
+ 'Server',
+ `Login Link: /game.html?token=${session.id}&id=${session.account_id}`
+ ));
+ }
+ else {
+ console.log(req.body.message.indexOf('/give'), acct.username);
+ socket.emit('/chat-message', renderPublicChatMessage(
+ 'Server',
+ `The command ${req.body.message} is not valid`
+ ));
+ }
+ }
+ else {
+ const msg = renderPublicChatMessage(acct.username, req.body.message);
+ server.ws.emit('/chat-message', msg);
+ msgBuffer.unshift(msg);
+ while(msgBuffer.length > 30) {
+ msgBuffer.pop();
+ }
+ }
+ }
+
+ return;
+});
+
+server.post<{params: {queueId: string}}, void>('/construction/:queueId/cancel', async req => {
+ const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
+ const city = await cityRepo.getUsersCity(acct.id);
+
+ if(!validator.isUUID(req.params.queueId)) {
+ throw new BadInputError('Invalid queue ID', ERROR_CODE.INVALID_BUILD_QUEUE);
+ }
+
+ // validate that this is an actual queue
+ const queue = await cityRepo.buildQueue.FindOne({owner: city.owner, id: req.params.queueId});
+
+ if(!queue) {
+ throw new NotFoundError('That queue does not exist', ERROR_CODE.INVALID_BUILD_QUEUE);
+ }
+
+ const [, building] = await Promise.all([
+ cityRepo.buildQueue.Delete({
+ owner: city.owner,
+ id: req.params.queueId
+ }),
+ cityRepo.buildingRepository.findBySlug(queue.building_type)
+ ]);
+
+ // now that it's deleted we can give the player back some percentage
+ // of resources based on how close they are to completion.
+ const diff = (queue.due - Date.now()) / (queue.due - queue.created);
+ // force a 20% loss minimum
+ const finalDiff = diff < 0.2 ? 0.2 : diff;
+
+ const costReturn: Partial<City> = {
+ id: city.id,
+ credits: city.credits + Math.floor(building.credits * queue.amount * diff),
+ alloys: city.alloys + Math.floor(building.alloys * queue.amount * diff),
+ energy: city.energy + Math.floor(building.energy * queue.amount * diff),
+ usedSpace: city.usedSpace - (building.land * queue.amount)
+ };
+
+ console.log('update', costReturn)
+
+ await cityRepo.save(costReturn);
+
+}, 'reload-construction-queue');
+
+server.post<{params: {queueId: string}}, void>('/training/:queueId/cancel', async req => {
+ const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
+ const city = await cityRepo.getUsersCity(acct.id);
+
+ if(!validator.isUUID(req.params.queueId)) {
+ throw new BadInputError('Invalid queue ID', ERROR_CODE.INVALID_BUILD_QUEUE);
+ }
+
+ const queue = await cityRepo.unitTrainigQueue.FindOne({owner: city.owner, id: req.params.queueId});
+ if(!queue) {
+ throw new NotFoundError('That queue does not exist', ERROR_CODE.INVALID_BUILD_QUEUE);
+ }
+
+ const [, unit] = await Promise.all([
+ cityRepo.unitTrainigQueue.Delete({
+ owner: city.owner,
+ id: req.params.queueId
+ }),
+ cityRepo.unitRepository.FindOne({slug: queue.unit_type})
+ ]);
+
+ // now that it's deleted we can give the player back some percentage
+ // of resources based on how close they are to completion.
+ const diff = (queue.due - Date.now()) / (queue.due - queue.created);
+ // force a 20% loss minimum
+ const finalDiff = diff < 0.2 ? 0.2 : diff;
+
+ const costReturn: Partial<City> = {
+ id: city.id,
+ credits: city.credits + Math.floor(unit.credits * queue.amount * diff),
+ food: city.food + Math.floor(unit.food * queue.amount * diff),
+ population: city.population + Math.floor(unit.population * queue.amount),
+ soldiers: city.soldiers + Math.floor(unit.soldiers * queue.amount),
+ attackers: city.attackers + Math.floor(unit.attackers * queue.amount),
+ defenders: city.defenders + Math.floor(unit.attackers * queue.amount)
+ };
+
+ console.log('update', costReturn)
+
+ await cityRepo.save(costReturn);
+
+}, 'reload-unit-training');
+
+server.get<void, string>('/server-stats', async req => {
+ const date = new Date();
+ const min = date.getMinutes();
+ return `
+ <div class="text-right">
+ <span class="success-text">${server.ws.engine.clientsCount} Online</span><br>
+ <span>
+ Server Time: ${date.getHours()}:${min < 10 ? '0'+min : min}
+ </span>
+ </div>`;
+});
+
+server.ws.on('connection', async socket => {
+ const auth = server.authFromUrl(socket.request.headers['referer']);
+ const acct = await accountRepo.validate(auth.authInfo.accountId, auth.authInfo.token);
+
+ server.ws.emit('/chat-message', msgBuffer.join("\n"));
+
+ server.ws.emit('/chat-message', renderPublicChatMessage('Server', `${acct.username} logged in`));
+
+ cache.online_users.push(acct.username);
+
+ socket.on('disconnect', () => {
+ cache.online_users.splice(cache.online_users.indexOf(acct.username), 1);
+ });
+});
server.start();
-tick.trigger({lastTickAt: 0, lastTick: 0});
\ No newline at end of file
+tick.trigger({
+ lastTickAt: 0,
+ lastTick: 0
+}, {
+ repeat: {
+ cron: '0 * * * *'
+ }
+});