1 import * as otel from './tracing';
2 import { version } from "../../package.json";
3 import { config as dotenv } from 'dotenv';
4 import { join } from 'path';
5 import express, {Request, Response} from 'express';
6 import bodyParser from 'body-parser';
8 import { rateLimit } from 'express-rate-limit';
10 import http from 'http';
11 import { Server, Socket } from 'socket.io';
12 import * as CONSTANT from '../shared/constants';
13 import { logger } from './lib/logger';
14 import { loadPlayer, createPlayer, updatePlayer, movePlayer } from './player';
15 import { random, sample } from 'lodash';
16 import {broadcastMessage, Message} from '../shared/message';
17 import {maxHp, maxVigor, Player} from '../shared/player';
18 import {createFight, getMonsterList, getMonsterLocation, getRandomMonster, loadMonster, loadMonsterFromFight, loadMonsterWithFaction} from './monster';
19 import {addInventoryItem, getEquippedItems, getInventory, getInventoryItem} from './inventory';
20 import { getItemFromPlayer, getItemFromShop, getPlayersItems, getShopItems, givePlayerItem, updateItemCount } from './items';
21 import {FightTrigger, Monster} from '../shared/monsters';
22 import {getShopEquipment, listShopItems } from './shopEquipment';
23 import {EquipmentSlot} from '../shared/inventory';
24 import { clearTravelPlan, completeTravel, getAllPaths, getAllServices, getCityDetails, getService, getTravelPlan, stepForward, travel, getDungeon } from './map';
25 import { signup, login, authEndpoint } from './auth';
26 import {db} from './lib/db';
27 import { getPlayerSkills} from './skills';
28 import { handleChatCommands } from './chat-commands';
30 import { fightRound, blockPlayerInFight } from './fight';
32 import { router as healerRouter } from './locations/healer';
33 import { router as professionRouter } from './locations/recruiter';
34 import { router as repairRouter } from './locations/repair';
35 import { router as dungeonRouter } from './locations/dungeon';
37 import * as Alert from './views/alert';
38 import { ExplorePane } from './views/components/explore-pane';
39 import { renderPlayerBar } from './views/player-bar'
40 import { renderEquipmentDetails, renderStore } from './views/stores';
41 import { renderMap } from './views/map';
42 import { renderProfilePage } from './views/profile';
43 import { renderSkills } from './views/skills';
44 import { renderInventoryPage } from './views/inventory';
45 import { renderMonsterSelector, renderOnlyMonsterSelector } from './views/monster-selector';
46 import { renderFight, renderFightPreRound, renderRoundDetails } from './views/fight';
47 import { renderTravel, travelButton } from './views/travel';
48 import { renderChatMessage } from './views/chat';
51 import { Item, PlayerItem, ShopItem } from 'shared/items';
52 import { equip, unequip } from './equipment';
53 import { HealthPotionSmall } from '../shared/items/health_potion';
54 import { completeDungeonFight, getActiveDungeon, getRoomVists, loadRoom, blockPlayerInDungeon } from './dungeon';
55 import { renderDungeon, renderDungeonRoom } from './views/dungeons/room';
56 import { flushBuffer, addEvent } from './events';
63 const app = express();
64 const server = http.createServer(app);
66 app.use(express.static(join(__dirname, '..', '..', 'public')));
67 app.use(bodyParser.urlencoded({ extended: true }));
68 app.use(express.json());
70 const io = new Server(server);
72 const cache = new Map<string, any>();
73 const chatHistory: Message[] = [];
75 app.use((req, res, next) => {
76 console.log(req.method, req.url);
80 const fightRateLimiter = rateLimit({
81 windowMs: parseInt(process.env.RATE_LIMIT_WINDOW || '30000'),
82 max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '20'),
83 standardHeaders: true,
85 handler: (req, res, next, options) => {
86 logger.log(`Blocked request: [${req.headers['x-authtoken']}: ${req.method} ${req.path}]`);
87 res.status(options.statusCode).send(options.message);
91 async function bootstrapSocket(socket: Socket, player: Player) {
92 // ref to get the socket id for a particular player
93 cache.set(`socket:${player.id}`, socket.id);
94 // ref to get the player object
95 cache.set(`token:${player.id}`, player);
96 cache.set(`socket-lookup:${socket.id}`, {
98 username: player.username
101 socket.emit('authToken', player.id);
103 socket.emit('chat', renderChatMessage(broadcastMessage('server', `${player.username} just logged in`)));
106 function uniqueConnectedUsers(): Set<string> {
107 const users = new Set<string>();
109 io.sockets.sockets.forEach((socket) => {
110 users.add(cache.get(`socket-lookup:${socket.id}`).username);
116 io.on('connection', async socket => {
117 logger.log(`socket ${socket.id} connected, authToken: ${socket.handshake.headers['x-authtoken']}`);
119 let authToken = socket.handshake.headers['x-authtoken'].toString() === 'null' ? null : socket.handshake.headers['x-authtoken'].toString();
123 logger.log(`Attempting to load player with id ${authToken}`);
124 player = await loadPlayer(authToken);
127 logger.log(`Creating player`);
128 player = await createPlayer();
129 authToken = player.id;
130 socket.handshake.headers['x-authtoken'] = authToken;
133 logger.log(`Socket [${socket.id}] auth token: ${player.id}`);
135 bootstrapSocket(socket, player);
137 socket.on('disconnect', () => {
138 console.log(`Player ${player.username} left`);
139 cache.delete(`socket-lookup:${socket.id}`);
141 io.emit('status', `${uniqueConnectedUsers().size} Online (v${version})`);
145 io.emit('status', `${uniqueConnectedUsers().size} Online (v${version})`);
146 // this is a special event to let the client know it can start
148 socket.emit('ready');
150 addEvent('LOGIN', player.id);
154 app.use(healerRouter);
155 app.use(professionRouter);
156 app.use(repairRouter);
157 app.use(dungeonRouter);
160 app.get('/chat/history', authEndpoint, async (req: Request, res: Response) => {
161 let html = chatHistory.map(renderChatMessage);
163 res.send(html.join("\n"));
166 app.post('/chat', authEndpoint, async (req: Request, res: Response) => {
167 const msg = req.body.message.trim();
169 if(!msg || !msg.length) {
174 if(msg.startsWith('/server') && req.player.permissions.includes('admin')) {
175 const sender = io.sockets.sockets.get(cache.get(`socket:${req.player.id}`));
177 await handleChatCommands(msg, req.player, io, sender);
180 sender.emit('chat', renderChatMessage(broadcastMessage('server', e.message)));
183 else if(msg === '/online') {
184 const users = Array.from(uniqueConnectedUsers().values());
185 // send to specific user
186 const message = broadcastMessage('server', `Online Players: [${users.join(", ")}]`);
187 io.to(cache.get(`socket:${req.player.id}`)).emit('chat', renderChatMessage(message));
191 const message = broadcastMessage(req.player.username, xss(msg, {
194 chatHistory.push(message);
195 chatHistory.slice(-10);
196 io.emit('chat', renderChatMessage(message));
202 app.get('/player', authEndpoint, async (req: Request, res: Response) => {
203 const equipment = await getEquippedItems(req.player.id);
204 res.send(renderPlayerBar(req.player) + renderProfilePage(req.player, equipment));
207 app.post('/player/stat/:stat', authEndpoint, async (req: Request, res: Response) => {
208 const equipment = await getEquippedItems(req.player.id);
209 const stat = req.params.stat;
210 if(!['strength', 'constitution', 'dexterity', 'intelligence'].includes(stat)) {
211 res.send(Alert.ErrorAlert(`Sorry, that's not a valid stat to increase`));
215 if(req.player.stat_points <= 0) {
216 res.send(Alert.ErrorAlert(`Sorry, you don't have enough stat points`));
220 req.player.stat_points -= 1;
223 req.player.hp = maxHp(req.player.constitution, req.player.level);
224 req.player.vigor = maxVigor(req.player.constitution, req.player.level);
225 updatePlayer(req.player);
227 res.send(renderPlayerBar(req.player) + renderProfilePage(req.player, equipment));
230 app.get('/player/skills', authEndpoint, async (req: Request, res: Response) => {
231 const skills = await getPlayerSkills(req.player.id);
233 res.send(renderSkills(skills));
236 app.get('/player/inventory', authEndpoint, async (req: Request, res: Response) => {
237 const [inventory, items] = await Promise.all([
238 getInventory(req.player.id),
239 getPlayersItems(req.player.id)
242 res.send(renderInventoryPage(inventory, items));
245 app.post('/player/equip/:item_id/:slot', authEndpoint, blockPlayerInFight, blockPlayerInDungeon, async (req: Request, res: Response) => {
246 const inventoryItem = await getInventoryItem(req.player.id, req.params.item_id);
247 const equippedItems = await getEquippedItems(req.player.id);
248 const requestedSlot = req.params.slot;
249 let desiredSlot: EquipmentSlot = inventoryItem.equipment_slot;
252 // handes the situation where you're trying to equip an item
253 // that can be equipped to any hand
254 if(inventoryItem.equipment_slot === 'ANY_HAND') {
255 if(requestedSlot === 'LEFT_HAND' || requestedSlot === 'RIGHT_HAND') {
256 // get the players current equipment in that slot!
257 if(equippedItems.some(v => {
258 return v.equipment_slot === requestedSlot || v.equipment_slot === 'TWO_HANDED';
263 desiredSlot = requestedSlot;
268 if(requestedSlot === 'TWO_HANDED') {
269 if(equippedItems.some(v => {
270 return v.equipment_slot === 'LEFT_HAND' || v.equipment_slot === 'RIGHT_HAND';
277 await equip(req.player.id, inventoryItem, desiredSlot);
278 const socketId = cache.get(`socket:${req.player.id}`).toString();
279 io.to(socketId).emit('updatePlayer', req.player);
280 io.to(socketId).emit('alert', {
282 text: `You equipped your ${inventoryItem.name}`
289 const [inventory, items] = await Promise.all([
290 getInventory(req.player.id),
291 getPlayersItems(req.player.id)
294 res.send(renderInventoryPage(inventory, items, inventoryItem.type) + renderPlayerBar(req.player));
297 app.post('/player/unequip/:item_id', authEndpoint, blockPlayerInFight, blockPlayerInDungeon, async (req: Request, res: Response) => {
298 const [item, ] = await Promise.all([
299 getInventoryItem(req.player.id, req.params.item_id),
300 unequip(req.player.id, req.params.item_id)
303 const [inventory, items] = await Promise.all([
304 getInventory(req.player.id),
305 getPlayersItems(req.player.id)
308 res.send(renderInventoryPage(inventory, items, item.type) + renderPlayerBar(req.player));
311 app.get('/player/explore', authEndpoint, async (req: Request, res: Response) => {
312 const fight = await loadMonsterFromFight(req.player.id);
313 const travelPlan = await getTravelPlan(req.player.id);
314 let closestTown = req.player.city_id;
317 closestTown = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
320 if(fight && req.player.hp > 0) {
321 const location = await getMonsterLocation(fight.ref_id);
323 res.send(renderPlayerBar(req.player) + renderFightPreRound(fight, true, location, closestTown));
327 const dungeon = await getActiveDungeon(req.player.id);
328 if(dungeon && req.player.hp > 0) {
329 const service = await getDungeon(dungeon.dungeon_id);
330 const room = await loadRoom(dungeon.current_room_id);
331 const visits = await getRoomVists(req.player.id, service.event_name);
333 res.send(ExplorePane(service.city_id, renderDungeon(service.city_name, service.name, room, visits)));
337 if(travelPlan && req.player.hp > 0) {
339 const chanceToSeeMonster = random(0, 100);
340 const things: any[] = [];
341 if(chanceToSeeMonster <= 30) {
342 const monster = await getRandomMonster([closestTown]);
343 things.push(monster);
347 const nextAction = cache[`step:${req.player.id}`] || 0;
349 res.send(renderPlayerBar(req.player) + renderTravel({
352 closestTown: closestTown,
359 // display the default explore view
360 const [city, locations, paths] = await Promise.all([
361 getCityDetails(req.player.city_id),
362 getAllServices(req.player.city_id, req.player.level),
363 getAllPaths(req.player.city_id)
366 res.send(renderPlayerBar(req.player) + await renderMap({city, locations, paths}, closestTown));
369 // used to purchase equipment from a particular shop
370 app.put('/location/:location_id/equipment/:item_id', authEndpoint, async (req: Request, res: Response) => {
371 const item = await getShopEquipment(parseInt(req.params.item_id), parseInt(req.params.location_id));
374 logger.log(`Invalid item [${req.params.item_id}]`);
375 return res.sendStatus(400);
378 if(req.player.gold < item.cost) {
379 res.send(Alert.ErrorAlert(`Sorry, you need at least ${item.cost.toLocaleString()}G to purchase this.`));
383 req.player.gold -= item.cost;
385 await updatePlayer(req.player);
386 await addInventoryItem(req.player.id, item);
388 res.send(renderPlayerBar(req.player) + Alert.SuccessAlert(`You purchased ${item.name}`));
391 // used to purchase items from a particular shop
392 app.put('/location/:location_id/items/:item_id', authEndpoint, async (req: Request, res: Response) => {
393 const item: (ShopItem & Item) = await getItemFromShop(parseInt(req.params.item_id), parseInt(req.params.location_id));
396 logger.log(`Invalid item [${req.params.item_id}]`);
397 return res.sendStatus(400);
400 if(req.player.gold < item.price_per_unit) {
401 res.send(Alert.ErrorAlert(`Sorry, you need at least ${item.price_per_unit.toLocaleString()}G to purchase this.`));
405 req.player.gold -= item.price_per_unit;
407 await updatePlayer(req.player);
408 await givePlayerItem(req.player.id, item.id, 1);
410 res.send(renderPlayerBar(req.player) + Alert.SuccessAlert(`You purchased a ${item.name}`));
413 // used to display equipment modals in a store, validates that
414 // the equipment is actually in this store before displaying
416 app.get('/location/:location_id/equipment/:item_id/overview', authEndpoint, async (req: Request, res: Response) => {
417 const equipment = await getShopEquipment(parseInt(req.params.item_id), parseInt(req.params.location_id));
420 logger.log(`Invalid equipment [${req.params.item_id}]`);
421 return res.sendStatus(400);
426 <div class="item-modal-overview">
428 <img src="${equipment.icon ? `/assets/img/icons/equipment/${equipment.icon}` : 'https://via.placeholder.com/64x64'}" title="${equipment.name}" alt="${equipment.name}">
431 ${renderEquipmentDetails(equipment, req.player)}
434 <div class="actions">
435 <button hx-put="/location/${equipment.location_id}/equipment/${equipment.id}" formmethod="dialog" value="cancel" class="green">Buy</button>
436 <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
444 // used to display item modals in a store, validates that
445 // the item is actually in this store before displaying
447 app.get('/location/:location_id/items/:item_id/overview', authEndpoint, async (req: Request, res: Response) => {
448 const item: (ShopItem & Item) = await getItemFromShop(parseInt(req.params.item_id), parseInt(req.params.location_id));
451 logger.log(`Invalid item [${req.params.item_id}]`);
452 return res.sendStatus(400);
457 <div class="item-modal-overview">
459 <img src="/assets/img/icons/items/${item.icon_name}" title="${item.name}" alt="${item.name}">
462 <h4>${item.name}</h4>
463 <p>${item.description}</p>
466 <div class="actions">
467 <button hx-put="/location/${item.location_id}/items/${item.id}" formmethod="dialog" value="cancel" class="red">Buy</button>
468 <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
476 app.put('/item/:item_id', authEndpoint, async (req: Request, res: Response) => {
477 const item: PlayerItem = await getItemFromPlayer(req.player.id, parseInt(req.params.item_id));
480 console.log(`Can't find item [${req.params.item_id}]`);
484 if(item.amount < 1) {
485 res.send(Alert.ErrorAlert(`You dont have enough ${item.name}`));
491 switch(item.effect_name) {
493 const hpGain = HealthPotionSmall.effect(req.player);
495 req.player.hp += hpGain;
497 if(req.player.hp > maxHp(req.player.constitution, req.player.level)) {
498 req.player.hp = maxHp(req.player.constitution, req.player.level);
503 await updateItemCount(req.player.id, item.item_id, -1);
504 await updatePlayer(req.player);
506 const inventory = await getInventory(req.player.id);
507 const items = await getPlayersItems(req.player.id);
511 renderPlayerBar(req.player),
512 renderInventoryPage(inventory, items, 'ITEMS'),
513 Alert.SuccessAlert(`You used the ${item.name}`)
519 app.get('/modal/items/:item_id', authEndpoint, async (req: Request, res: Response) => {
520 const item: PlayerItem = await getItemFromPlayer(req.player.id, parseInt(req.params.item_id));
523 logger.log(`Invalid item [${req.params.item_id}]`);
524 return res.sendStatus(400);
529 <div class="item-modal-overview">
531 <img src="/assets/img/icons/items/${item.icon_name}" title="${item.name}" alt="${item.name}">
534 <h4>${item.name}</h4>
535 <p>${item.description}</p>
538 <div class="actions">
539 <button hx-put="/item/${item.item_id}" formmethod="dialog" value="cancel" hx-target="#inventory" class="red">Use</button>
540 <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
548 app.get('/city/stores/city:stores/:location_id', authEndpoint, async (req: Request, res: Response) => {
549 const location = await getService(parseInt(req.params.location_id));
551 if(!location || location.city_id !== req.player.city_id) {
552 logger.log(`Invalid location: [${req.params.location_id}]`);
555 const [shopEquipment, shopItems] = await Promise.all([
556 listShopItems({location_id: location.id}),
557 getShopItems(location.id),
560 const html = await renderStore(shopEquipment, shopItems, req.player, location);
565 app.get('/city/explore/city:explore/:location_id', authEndpoint, async (req: Request, res: Response) => {
566 const location = await getService(parseInt(req.params.location_id));
567 if(!location || location.city_id !== req.player.city_id) {
569 logger.log(`Invalid location: [${req.params.location_id}]`);
573 const monsters: Monster[] = await getMonsterList(location.id);
574 res.send(renderOnlyMonsterSelector(monsters, 0, location));
577 app.post('/travel', authEndpoint, async (req: Request, res: Response) => {
578 const destination_id = parseInt(req.body.destination_id);
580 if(!destination_id || isNaN(destination_id)) {
581 logger.log(`Invalid destination_id [${req.body.destination_id}]`);
582 return res.sendStatus(400);
585 const travelPlan = travel(req.player, req.body.destination_id);
587 res.json(travelPlan);
590 app.post('/fight/turn', authEndpoint, async (req: Request, res: Response) => {
591 const fightBlockKey = `fightturn:${req.player.id}`;
593 if(cache[fightBlockKey] && cache[fightBlockKey] > Date.now()) {
594 res.status(429).send(Alert.ErrorAlert('Hmm, you are fight too quickly'));
599 cache[fightBlockKey] = Date.now() + CONSTANT.FIGHT_ATTACK_DELAY;
600 const monster = await loadMonsterFromFight(req.player.id);
603 res.send(Alert.ErrorAlert('Not in a fight'));
607 const fightData = await fightRound(req.player, monster, {
608 action: req.body.action
612 if(fightData.roundData.winner !== 'in-progress') {
613 delete cache[fightBlockKey];
616 if(fightData.roundData.winner === 'player') {
617 //@TODO: Add equipped weapons
618 addEvent('MONSTER_KILLED', req.player.id, {
619 monster_id: monster.ref_id,
620 monster_name: monster.name,
621 monster_level: monster.level,
622 fight_trigger: monster.fight_trigger,
625 else if(fightData.roundData.winner === 'monster') {
626 addEvent('PLAYER_KILLED', req.player.id, {
627 monster_id: monster.ref_id,
628 monster_name: monster.name,
629 monster_level: monster.level,
630 fight_trigger: monster.fight_trigger
635 if(monster.fight_trigger === 'dungeon-forced' && fightData.roundData.winner === 'player') {
636 // ok the player was in a dungeon, lets make sure
637 // that they complete whatever dungeon room they are in
638 const dungeonState = await completeDungeonFight(req.player.id, monster);
639 const room = await loadRoom(dungeonState.current_room_id);
640 const visits = await getRoomVists(req.player.id, room.dungeon_id);
642 res.send(renderDungeonRoom(room, visits));
646 let html = renderFight(
648 renderRoundDetails(fightData.roundData),
649 fightData.roundData.winner === 'in-progress',
653 if(fightData.monsters.length && monster.fight_trigger === 'explore') {
654 html += renderMonsterSelector(fightData.monsters, monster.ref_id);
657 let travelSection = '';
658 if(monster.fight_trigger === 'travel' && fightData.roundData.winner === 'player') {
659 // you're travellinga dn you won.. display the keep walking!
660 const travelPlan = await getTravelPlan(req.player.id);
661 const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
662 travelSection = travelButton(0);
665 const playerBar = renderPlayerBar(fightData.player);
667 res.send(html + travelSection + playerBar);
670 app.post('/fight', fightRateLimiter, authEndpoint, async (req: Request, res: Response) => {
671 if(req.player.hp <= 0) {
672 logger.log(`Player didn\'t have enough hp`);
673 return res.sendStatus(400);
676 const monsterId: number = req.body.monsterId;
677 const fightTrigger: FightTrigger = req.body.fightTrigger ?? 'travel';
680 logger.log(`Missing monster Id ${monsterId}`);
681 return res.sendStatus(400);
684 if(!fightTrigger || !['travel', 'explore'].includes(fightTrigger)) {
685 logger.log(`Invalid fight trigger [${fightTrigger}]`);
686 return res.sendStatus(400);
689 const monster = await loadMonster(monsterId);
692 logger.log(`Couldnt find monster for ${monsterId}`);
693 return res.sendStatus(400);
696 const fight = await createFight(req.player.id, monster, fightTrigger);
697 const location = await getService(monster.location_id);
699 res.send(renderFightPreRound(fight, true, location, location.city_id));
702 app.post('/travel/step', authEndpoint, async (req: Request, res: Response) => {
703 const stepTimerKey = `step:${req.player.id}`;
705 const travelPlan = await getTravelPlan(req.player.id);
707 res.send(Alert.ErrorAlert('You don\'t have a travel plan'));
711 if(cache[stepTimerKey]) {
712 if(cache[stepTimerKey] > Date.now()) {
713 res.send(Alert.ErrorAlert('Hmm.. travelling too quickly'));
718 travelPlan.current_position++;
720 if(travelPlan.current_position >= travelPlan.total_distance) {
721 const travel = await completeTravel(req.player.id);
723 req.player.city_id = travel.destination_id;
724 await movePlayer(travel.destination_id, req.player.id);
726 const [city, locations, paths] = await Promise.all([
727 getCityDetails(travel.destination_id),
728 getAllServices(travel.destination_id, req.player.level),
729 getAllPaths(travel.destination_id)
732 delete cache[stepTimerKey];
733 res.send(await renderMap({city, locations, paths}, req.player.city_id));
736 const walkingText: string[] = [
737 'You take a step forward',
740 // update existing plan..
741 // decide if they will run into anything
742 const travelPlan = await stepForward(req.player.id);
744 const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
746 const chanceToSeeMonster = random(0, 100);
747 const things: any[] = [];
748 if(chanceToSeeMonster <= 30) {
749 const monster = await getRandomMonster([closest]);
750 things.push(monster);
754 const nextAction = Date.now() + CONSTANT.STEP_DELAY;
756 cache[stepTimerKey] = nextAction;
758 res.send(renderTravel({
761 closestTown: closest,
762 walkingText: sample(walkingText),
769 app.post('/travel/return-to-source', authEndpoint, async (req: Request, res: Response) => {
770 // puts the player back in their starting town
771 // doesn't matter if they don't have one
773 await clearTravelPlan(req.player.id);
775 const fight = await loadMonsterFromFight(req.player.id);
777 // go to the fight screen
778 const location = await getMonsterLocation(fight.ref_id);
780 res.send(renderPlayerBar(req.player) + renderFightPreRound(fight, true, location, req.player.city_id));
783 const [city, locations, paths] = await Promise.all([
784 getCityDetails(req.player.city_id),
785 getAllServices(req.player.city_id, req.player.level),
786 getAllPaths(req.player.city_id)
789 res.send(renderPlayerBar(req.player) + await renderMap({city, locations, paths}, req.player.city_id));
795 app.post('/travel/:destination_id', authEndpoint, async (req: Request, res: Response) => {
796 if(req.player.hp <= 0) {
797 logger.log(`Player didn\'t have enough hp`);
798 res.send(Alert.ErrorAlert('Sorry, you need some HP to start travelling.'));
802 const destination = await getCityDetails(parseInt(req.params.destination_id));
805 res.send(Alert.ErrorAlert(`Thats not a valid desination`));
809 const travelPlan = await travel(req.player, destination.id);
811 res.send(renderTravel({
815 closestTown: req.player.city_id,
820 app.get('/settings', authEndpoint, async (req: Request, res: Response) => {
823 if(req.player.account_type === 'session') {
824 warning += `<div class="alert error">If you log out without signing up first, this account is lost forever.</div>`;
827 html += '<a href="#" hx-post="/logout">Logout</a>';
828 res.send(warning + html);
831 app.post('/logout', authEndpoint, async (req: Request, res: Response) => {
832 // ref to get the socket id for a particular player
833 cache.delete(`socket:${req.player.id}`);
834 // ref to get the player object
835 cache.delete(`token:${req.player.id}`);
837 logger.log(`${req.player.username} logged out`);
843 app.post('/auth', async (req: Request, res: Response) => {
844 if(req.body.authType === 'login') {
845 loginHandler(req, res);
847 else if(req.body.authType === 'signup') {
848 signupHandler(req, res);
851 logger.log(`Invalid auth type [${req.body.authType}]`);
857 async function signupHandler(req: Request, res: Response) {
858 const {username, password} = req.body;
859 const authToken = req.headers['x-authtoken'];
861 if(!username || !password || !authToken) {
862 res.send(Alert.ErrorAlert('Invalid username/password'));
867 const player = await loadPlayer(authToken.toString());
868 logger.log(`Attempted claim for ${player.username}`);
870 await signup(authToken.toString(), username, password);
872 await db('players').where({id: player.id}).update({
873 account_type: 'auth',
877 logger.log(`${username} claimed ${player.username}`);
879 io.emit('chat', broadcastMessage('server', `${player.username} is now ${username}`));
881 res.setHeader('hx-refresh', 'true');
886 if(e?.constraint === 'players_username_unique') {
887 res.send(Alert.ErrorAlert('That username is already taken'));
890 res.send(Alert.ErrorAlert('Please try again'));
895 async function loginHandler (req: Request, res: Response) {
896 const {username, password} = req.body;
897 if(!username || !username.length) {
898 res.send(Alert.ErrorAlert('Invalid username'));
901 if(!password || !password.length) {
902 res.send(Alert.ErrorAlert('Invalid password'));
907 const player = await login(username, password);
908 io.sockets.sockets.forEach(socket => {
909 if(socket.handshake.headers['x-authtoken'] === req.headers['x-authtoken']) {
910 bootstrapSocket(socket, player);
917 res.send(Alert.ErrorAlert('That user doesn\'t exist'));
921 server.listen(process.env.API_PORT, () => {
922 logger.log(`Listening on port ${process.env.API_PORT}`);