chore(release): 0.3.6
[risinglegends.git] / src / server / api.ts
index 77d54b28c942efb5ae375e78686ae144d9a7e84e..0aa2689c95a6e594999ccffc22309771648ee358 100644 (file)
@@ -22,9 +22,10 @@ import {FightTrigger, Monster, MonsterForFight} from '../shared/monsters';
 import {getShopEquipment, listShopItems } from './shopEquipment';
 import {EquipmentSlot} from '../shared/inventory';
 import { clearTravelPlan, completeTravel, getAllPaths, getAllServices, getCityDetails, getService, getTravelPlan, stepForward, travel } from './map';
-import { signup, login, authEndpoint, AuthRequest } from './auth';
+import { signup, login, authEndpoint } from './auth';
 import {db} from './lib/db';
 import { getPlayerSkills} from './skills';
+import { handleChatCommands } from './chat-commands';
 
 import { fightRound, blockPlayerInFight } from './fight';
 
@@ -45,9 +46,6 @@ import { renderTravel, travelButton } from './views/travel';
 import { renderChatMessage } from './views/chat';
 
 // TEMP!
-import { createMonsters } from '../../seeds/monsters';
-import { createAllCitiesAndLocations } from '../../seeds/cities';
-import { createShopItems, createShopEquipment } from '../../seeds/shop_items';
 import { Item, PlayerItem, ShopItem } from 'shared/items';
 import { equip, unequip } from './equipment';
 import { HealthPotionSmall } from '../shared/items/health_potion';
@@ -89,12 +87,26 @@ async function bootstrapSocket(socket: Socket, player: Player) {
   cache.set(`socket:${player.id}`, socket.id);
   // ref to get the player object
   cache.set(`token:${player.id}`, player);
+  cache.set(`socket-lookup:${socket.id}`, {
+    id: player.id,
+    username: player.username
+  });
 
   socket.emit('authToken', player.id);
 
   socket.emit('chat', renderChatMessage(broadcastMessage('server', `${player.username} just logged in`)));
 }
 
+function uniqueConnectedUsers(): Set<string> {
+  const users = new Set<string>();
+
+  io.sockets.sockets.forEach((socket) => {
+    users.add(cache.get(`socket-lookup:${socket.id}`).username);
+  });
+
+  return users;
+}
+
 io.on('connection', async socket => {
   logger.log(`socket ${socket.id} connected, authToken: ${socket.handshake.headers['x-authtoken']}`);
 
@@ -118,11 +130,13 @@ io.on('connection', async socket => {
 
   socket.on('disconnect', () => {
     console.log(`Player ${player.username} left`);
-    io.emit('status', `${io.sockets.sockets.size} Online (v${version})`);
+    cache.delete(`socket-lookup:${socket.id}`);
+
+    io.emit('status', `${uniqueConnectedUsers().size} Online (v${version})`);
   });
 
 
-  io.emit('status', `${io.sockets.sockets.size} Online (v${version})`);
+  io.emit('status', `${uniqueConnectedUsers().size} Online (v${version})`);
   // this is a special event to let the client know it can start 
   // requesting data
   socket.emit('ready');
@@ -134,13 +148,13 @@ app.use(professionRouter);
 app.use(repairRouter);
 
 
-app.get('/chat/history', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.get('/chat/history', authEndpoint, async (req: Request, res: Response) => {
   let html = chatHistory.map(renderChatMessage);
 
   res.send(html.join("\n"));
 });
 
-app.post('/chat', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.post('/chat', authEndpoint, async (req: Request, res: Response) => {
   const msg = req.body.message.trim();
 
   if(!msg || !msg.length) {
@@ -148,48 +162,42 @@ app.post('/chat', authEndpoint, async (req: AuthRequest, res: Response) => {
     return;
   }
 
-  let message: Message;
-  if(msg.startsWith('/server lmnop')) {
-    if(msg === '/server lmnop refresh-monsters') {
-      await createMonsters();
-      message = broadcastMessage('server', 'Monster refresh!');
-    }
-    else if(msg === '/server lmnop refresh-cities') {
-      await createAllCitiesAndLocations();
-      message = broadcastMessage('server', 'Cities, Locations, and Paths refreshed!');
-    }
-    else if(msg === '/server lmnop refresh-shops') {
-      await createShopItems();
-      await createShopEquipment();
-      message = broadcastMessage('server', 'Refresh shop items');
-    }
-    else {
-      const str = msg.split('/server lmnop ')[1];
-      if(str) {
-        message = broadcastMessage('server', str);
+  if(msg.startsWith('/server') && req.player.permissions.includes('admin')) {
+    try {
+      const output = await handleChatCommands(msg, req.player, io);
+      if(output) {
+        io.to(cache.get(`socket:${req.player.id}`)).emit('chat', renderChatMessage(output));
       }
     }
+    catch(e) {
+      io.to(cache.get(`socket:${req.player.id}`)).emit('chat', renderChatMessage(broadcastMessage('server', e.message)));
+    }
+  }
+  else if(msg === '/online') {
+    const users = Array.from(uniqueConnectedUsers().values());
+    // send to specific user
+    const message = broadcastMessage('server', `Online Players: [${users.join(", ")}]`);
+    io.to(cache.get(`socket:${req.player.id}`)).emit('chat', renderChatMessage(message));
+    res.sendStatus(204);
   }
   else {
-    message = broadcastMessage(req.player.username, xss(msg, {
+    const message = broadcastMessage(req.player.username, xss(msg, {
       whiteList: {}
     }));
     chatHistory.push(message);
     chatHistory.slice(-10);
-  }
-
-  if(message) {
     io.emit('chat', renderChatMessage(message));
-    res.sendStatus(204);
   }
+
+  res.sendStatus(204);
 });
 
-app.get('/player', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.get('/player', authEndpoint, async (req: Request, res: Response) => {
   const equipment = await getEquippedItems(req.player.id);
   res.send(renderPlayerBar(req.player) + renderProfilePage(req.player, equipment));
 });
 
-app.post('/player/stat/:stat', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.post('/player/stat/:stat', authEndpoint, async (req: Request, res: Response) => {
   const equipment = await getEquippedItems(req.player.id);
   const stat = req.params.stat;
   if(!['strength', 'constitution', 'dexterity', 'intelligence'].includes(stat)) {
@@ -212,13 +220,13 @@ app.post('/player/stat/:stat', authEndpoint, async (req: AuthRequest, res: Respo
   res.send(renderPlayerBar(req.player) + renderProfilePage(req.player, equipment));
 });
 
-app.get('/player/skills', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.get('/player/skills', authEndpoint, async (req: Request, res: Response) => {
   const skills = await getPlayerSkills(req.player.id);
 
   res.send(renderSkills(skills));
 });
 
-app.get('/player/inventory', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.get('/player/inventory', authEndpoint, async (req: Request, res: Response) => {
   const [inventory, items] = await Promise.all([
     getInventory(req.player.id),
     getPlayersItems(req.player.id)
@@ -227,7 +235,7 @@ app.get('/player/inventory', authEndpoint, async (req: AuthRequest, res: Respons
   res.send(renderInventoryPage(inventory, items));
 });
 
-app.post('/player/equip/:item_id/:slot', authEndpoint, blockPlayerInFight, async (req: AuthRequest, res: Response) => {
+app.post('/player/equip/:item_id/:slot', authEndpoint, blockPlayerInFight, async (req: Request, res: Response) => {
   const inventoryItem = await getInventoryItem(req.player.id, req.params.item_id);
   const equippedItems = await getEquippedItems(req.player.id);
   const requestedSlot = req.params.slot;
@@ -279,7 +287,7 @@ app.post('/player/equip/:item_id/:slot', authEndpoint, blockPlayerInFight, async
   res.send(renderInventoryPage(inventory, items, inventoryItem.type) + renderPlayerBar(req.player));
 });
 
-app.post('/player/unequip/:item_id', authEndpoint, blockPlayerInFight, async (req: AuthRequest, res: Response) => {
+app.post('/player/unequip/:item_id', authEndpoint, blockPlayerInFight, async (req: Request, res: Response) => {
   const [item, ] = await Promise.all([
     getInventoryItem(req.player.id, req.params.item_id),
     unequip(req.player.id, req.params.item_id)
@@ -293,7 +301,7 @@ app.post('/player/unequip/:item_id', authEndpoint, blockPlayerInFight, async (re
   res.send(renderInventoryPage(inventory, items, item.type) + renderPlayerBar(req.player));
 });
 
-app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.get('/player/explore', authEndpoint, async (req: Request, res: Response) => {
   const fight = await loadMonsterFromFight(req.player.id);
   const travelPlan = await getTravelPlan(req.player.id);
   let closestTown = req.player.city_id;
@@ -303,18 +311,9 @@ app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response)
   }
 
   if(fight) {
-    const data: MonsterForFight = {
-      id: fight.id,
-      hp: fight.hp,
-      maxHp: fight.maxHp,
-      name: fight.name,
-      level: fight.level,
-      fight_trigger: fight.fight_trigger
-    };
     const location = await getMonsterLocation(fight.ref_id);
 
-
-    res.send(renderPlayerBar(req.player) + renderFightPreRound(data, true, location, closestTown));
+    res.send(renderPlayerBar(req.player) + renderFightPreRound(fight, true, location, closestTown));
   }
   else {
     if(travelPlan) {
@@ -352,7 +351,7 @@ app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response)
 });
 
 // used to purchase equipment from a particular shop
-app.put('/location/:location_id/equipment/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.put('/location/:location_id/equipment/:item_id', authEndpoint, async (req: Request, res: Response) => {
   const item = await getShopEquipment(parseInt(req.params.item_id), parseInt(req.params.location_id));
 
   if(!item) {
@@ -374,7 +373,7 @@ app.put('/location/:location_id/equipment/:item_id', authEndpoint, async (req: A
 });
 
 // used to purchase items from a particular shop
-app.put('/location/:location_id/items/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.put('/location/:location_id/items/:item_id', authEndpoint, async (req: Request, res: Response) => {
   const item: (ShopItem & Item) = await getItemFromShop(parseInt(req.params.item_id), parseInt(req.params.location_id));
 
   if(!item) {
@@ -398,7 +397,7 @@ app.put('/location/:location_id/items/:item_id', authEndpoint, async (req: AuthR
 // used to display equipment modals in a store, validates that 
 // the equipment is actually in this store before displaying 
 // the modal
-app.get('/location/:location_id/equipment/:item_id/overview', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.get('/location/:location_id/equipment/:item_id/overview', authEndpoint, async (req: Request, res: Response) => {
   const equipment = await getShopEquipment(parseInt(req.params.item_id), parseInt(req.params.location_id));
 
   if(!equipment) {
@@ -429,7 +428,7 @@ app.get('/location/:location_id/equipment/:item_id/overview', authEndpoint, asyn
 // used to display item modals in a store, validates that 
 // the item is actually in this store before displaying 
 // the modal
-app.get('/location/:location_id/items/:item_id/overview', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.get('/location/:location_id/items/:item_id/overview', authEndpoint, async (req: Request, res: Response) => {
   const item: (ShopItem & Item) = await getItemFromShop(parseInt(req.params.item_id), parseInt(req.params.location_id));
 
   if(!item) {
@@ -458,7 +457,7 @@ app.get('/location/:location_id/items/:item_id/overview', authEndpoint, async (r
   res.send(html);
 });
 
-app.put('/item/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.put('/item/:item_id', authEndpoint, async (req: Request, res: Response) => {
   const item: PlayerItem = await getItemFromPlayer(req.player.id, parseInt(req.params.item_id));
 
   if(!item) {
@@ -501,7 +500,7 @@ app.put('/item/:item_id', authEndpoint, async (req: AuthRequest, res: Response)
 
 });
 
-app.get('/modal/items/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.get('/modal/items/:item_id', authEndpoint, async (req: Request, res: Response) => {
   const item: PlayerItem = await getItemFromPlayer(req.player.id, parseInt(req.params.item_id));
 
   if(!item) {
@@ -530,7 +529,7 @@ app.get('/modal/items/:item_id', authEndpoint, async (req: AuthRequest, res: Res
   res.send(html);
 });
 
-app.get('/city/stores/city:stores/:location_id', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.get('/city/stores/city:stores/:location_id', authEndpoint, async (req: Request, res: Response) => {
   const location = await getService(parseInt(req.params.location_id));
 
   if(!location || location.city_id !== req.player.city_id) {
@@ -547,7 +546,7 @@ app.get('/city/stores/city:stores/:location_id', authEndpoint, async (req: AuthR
   res.send(html);
 });
 
-app.get('/city/explore/city:explore/:location_id', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.get('/city/explore/city:explore/:location_id', authEndpoint, async (req: Request, res: Response) => {
   const location = await getService(parseInt(req.params.location_id));
   if(!location || location.city_id !== req.player.city_id) {
 
@@ -559,7 +558,7 @@ app.get('/city/explore/city:explore/:location_id', authEndpoint, async (req: Aut
   res.send(renderOnlyMonsterSelector(monsters, 0, location));
 });
 
-app.post('/travel', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.post('/travel', authEndpoint, async (req: Request, res: Response) => {
   const destination_id = parseInt(req.body.destination_id);
 
   if(!destination_id || isNaN(destination_id)) {
@@ -572,7 +571,7 @@ app.post('/travel', authEndpoint, async (req: AuthRequest, res: Response) => {
   res.json(travelPlan);
 });
 
-app.post('/fight/turn', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.post('/fight/turn', authEndpoint, async (req: Request, res: Response) => {
   const fightBlockKey = `fightturn:${req.player.id}`;
 
   if(cache[fightBlockKey] && cache[fightBlockKey] > Date.now()) {
@@ -582,7 +581,7 @@ app.post('/fight/turn', authEndpoint, async (req: AuthRequest, res: Response) =>
   }
 
   cache[fightBlockKey] = Date.now() + CONSTANT.FIGHT_ATTACK_DELAY;
-  const monster = await loadMonsterWithFaction(req.player.id);
+  const monster = await loadMonsterFromFight(req.player.id);
 
   if(!monster) {
     res.send(Alert.ErrorAlert('Not in a fight'));
@@ -590,8 +589,7 @@ app.post('/fight/turn', authEndpoint, async (req: AuthRequest, res: Response) =>
   }
 
   const fightData  = await fightRound(req.player, monster, {
-    action: req.body.action,
-    target: req.body.fightTarget
+    action: req.body.action
   });
 
 
@@ -623,7 +621,7 @@ app.post('/fight/turn', authEndpoint, async (req: AuthRequest, res: Response) =>
   res.send(html + travelSection + playerBar);
 });
 
-app.post('/fight', fightRateLimiter, authEndpoint, async (req: AuthRequest, res: Response) => {
+app.post('/fight', fightRateLimiter, authEndpoint, async (req: Request, res: Response) => {
   if(req.player.hp <= 0) {
     logger.log(`Player didn\'t have enough hp`);
     return res.sendStatus(400);
@@ -652,20 +650,10 @@ app.post('/fight', fightRateLimiter, authEndpoint, async (req: AuthRequest, res:
   const fight = await createFight(req.player.id, monster, fightTrigger);
   const location = await getService(monster.location_id);
 
-
-  const data: MonsterForFight = {
-    id: fight.id,
-    hp: fight.hp,
-    maxHp: fight.maxHp,
-    name: fight.name,
-    level: fight.level,
-    fight_trigger: fight.fight_trigger
-  };
-
-  res.send(renderFightPreRound(data, true, location, location.city_id));
+  res.send(renderFightPreRound(fight, true, location, location.city_id));
 });
 
-app.post('/travel/step', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.post('/travel/step', authEndpoint, async (req: Request, res: Response) => {
   const stepTimerKey = `step:${req.player.id}`;
 
   const travelPlan = await getTravelPlan(req.player.id);
@@ -732,7 +720,7 @@ app.post('/travel/step', authEndpoint, async (req: AuthRequest, res: Response) =
   }
 });
 
-app.post('/travel/return-to-source', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.post('/travel/return-to-source', authEndpoint, async (req: Request, res: Response) => {
   // puts the player back in their starting town
   // doesn't matter if they don't have one
   // redirect them!
@@ -741,17 +729,9 @@ app.post('/travel/return-to-source', authEndpoint, async (req: AuthRequest, res:
   const fight = await loadMonsterFromFight(req.player.id);
   if(fight) {
     // go to the fight screen
-    const data: MonsterForFight = {
-      id: fight.id,
-      hp: fight.hp,
-      maxHp: fight.maxHp,
-      name: fight.name,
-      level: fight.level,
-      fight_trigger: fight.fight_trigger
-    };
     const location = await getMonsterLocation(fight.ref_id);
 
-    res.send(renderPlayerBar(req.player) + renderFightPreRound(data, true, location, req.player.city_id));
+    res.send(renderPlayerBar(req.player) + renderFightPreRound(fight, true, location, req.player.city_id));
   }
   else {
     const [city, locations, paths] = await Promise.all([
@@ -766,7 +746,7 @@ app.post('/travel/return-to-source', authEndpoint, async (req: AuthRequest, res:
 
 });
 
-app.post('/travel/:destination_id', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.post('/travel/:destination_id', authEndpoint, async (req: Request, res: Response) => {
 if(req.player.hp <= 0) {
     logger.log(`Player didn\'t have enough hp`);
     res.send(Alert.ErrorAlert('Sorry, you need some HP to start travelling.'));
@@ -791,7 +771,7 @@ if(req.player.hp <= 0) {
   }));
 });
 
-app.get('/settings', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.get('/settings', authEndpoint, async (req: Request, res: Response) => {
   let warning = '';
   let html = '';
   if(req.player.account_type === 'session') {
@@ -802,7 +782,7 @@ app.get('/settings', authEndpoint, async (req: AuthRequest, res: Response) => {
   res.send(warning + html);
 });
 
-app.post('/logout', authEndpoint, async (req: AuthRequest, res: Response) => {
+app.post('/logout', authEndpoint, async (req: Request, res: Response) => {
   // ref to get the socket id for a particular player
   cache.delete(`socket:${req.player.id}`);
   // ref to get the player object