chore(release): 0.2.7
[risinglegends.git] / src / server / api.ts
index 46d4204ebc4de43b39310b43db733ab317b752e2..f7bccdd7316deb947f62498b658b9b35e3f85335 100644 (file)
@@ -6,22 +6,22 @@ import express, {Request, Response} from 'express';
 import bodyParser from 'body-parser';
 
 import http from 'http';
-import { Server, Socket } from 'socket.io';
+import { Server } from 'socket.io';
 import { logger } from './lib/logger';
 import { loadPlayer, createPlayer, updatePlayer, movePlayer } from './player';
-import * as _ from 'lodash';
+import { random, sample } from 'lodash';
 import {broadcastMessage, Message} from '../shared/message';
 import {expToLevel, maxHp, Player} from '../shared/player';
 import {clearFight, createFight, getMonsterList, getRandomMonster, loadMonster, loadMonsterFromFight, loadMonsterWithFaction, saveFightState} from './monster';
 import {FightRound} from '../shared/fight';
 import {addInventoryItem, deleteInventoryItem, getEquippedItems, getInventory, getInventoryItem, updateAp} from './inventory';
-import { getItemFromPlayer, getItemFromShop, getPlayersItems, getShopItems, givePlayerItem } from './items';
+import { getItemFromPlayer, getItemFromShop, getPlayersItems, getShopItems, givePlayerItem, updateItemCount } from './items';
 import {FightTrigger, Monster, MonsterForFight, MonsterWithFaction} from '../shared/monsters';
-import {getShopEquipment, getShopItem, listShopItems } from './shopEquipment';
+import {getShopEquipment, listShopItems } from './shopEquipment';
 import {EquippedItemDetails} from '../shared/equipped';
 import {ArmourEquipmentSlot, EquipmentSlot} from '../shared/inventory';
 import { clearTravelPlan, completeTravel, getAllPaths, getAllServices, getCityDetails, getService, getTravelPlan, stepForward, travel } from './map';
-import { signup, login, authEndpoint } from './auth';
+import { signup, login, authEndpoint, AuthRequest } from './auth';
 import {db} from './lib/db';
 import { getPlayerSkills, getPlayerSkillsAsObject, updatePlayerSkills } from './skills';
 import {SkillID, Skills} from '../shared/skills';
@@ -38,14 +38,15 @@ import { renderInventoryPage } from './views/inventory';
 import { renderMonsterSelector } from './views/monster-selector';
 import { renderFight, renderRoundDetails } from './views/fight';
 import { renderTravel, travelButton } from './views/travel';
+import { renderChatMessage } from './views/chat';
 
 // TEMP!
 import { createMonsters } from '../../seeds/monsters';
 import { createAllCitiesAndLocations } from '../../seeds/cities';
-import { createShopItems } from '../../seeds/shop_items';
+import { createShopItems, createShopEquipment } from '../../seeds/shop_items';
 import { Item, PlayerItem, ShopItem } from 'shared/items';
 import { equip, unequip } from './equipment';
-import { random, sample } from 'lodash';
+import { HealthPotionSmall } from '../shared/items/health_potion';
 
 dotenv();
 
@@ -92,76 +93,16 @@ io.on('connection', async socket => {
   // ref to get the player object
   cache.set(`token:${player.id}`, player);
 
-  socket.emit('init', {
-    version
-  });
-
   socket.emit('authToken', player.id);
 
-  io.emit('chathistory', chatHistory);
-
-  socket.emit('chat', broadcastMessage('server', `${player.username} just logged in`));
+  socket.emit('chat', renderChatMessage(broadcastMessage('server', `${player.username} just logged in`)));
 
   socket.on('disconnect', () => {
     console.log(`Player ${player.username} left`);
+    io.emit('status', `${io.sockets.sockets.size} Online (v${version})`);
   });
 
-  socket.on('chat', async (msg: string) => {
-    if(msg.length > 0) {
-
-      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();
-          message = broadcastMessage('server', 'Refresh shop items');
-        }
-        else {
-          const str = msg.split('/server lmnop ')[1];
-          if(str) {
-            message = broadcastMessage('server', str);
-          }
-        }
-      }
-      else {
-        const authToken = socket.handshake.headers['x-authtoken'];
-        if(cache.has(`token:${authToken}`)) {
-          const player = cache.get(`token:${authToken}`);
-          message = broadcastMessage(player.username, msg);
-        }
-        else {
-          logger.log(`Missing cache for [token:${authToken}]`);
-        }
-      }
-
-      if(message) {
-        chatHistory.push(message);
-        chatHistory.slice(-10);
-        io.emit('chat', message);
-      }
-      else {
-        logger.log(`Unset message`);
-      }
-    }
-  });
-
-  socket.on('logout', async () => {
-    // clear this player from the cache!
-    const player = cache.get(`token:${socket.handshake.headers['x-authtoken']}`);
-    if(player) {
-      logger.log(`Player ${player.username} logged out`);
-    }
-    else {
-      logger.log(`Invalid user logout`);
-    }
-  });
+  io.emit('status', `${io.sockets.sockets.size} Online (v${version})`);
 
   // this is a special event to let the client know it can start 
   // requesting data
@@ -256,7 +197,7 @@ async function fightRound(player: Player, monster: MonsterWithFaction,  data: {a
     roundData.winner = 'monster';
     await clearFight(player.id);
 
-    return { roundData, monsters: [] };
+    return { roundData, monsters: [], player };
   }
 
   const attackType = data.action === 'attack' ? 'physical' : 'magical';
@@ -369,7 +310,7 @@ async function fightRound(player: Player, monster: MonsterWithFaction,  data: {a
 
     await clearFight(player.id);
     await updatePlayer(player);
-    return { roundData, monsters: potentialMonsters };
+    return { roundData, monsters: potentialMonsters, player };
   }
 
   roundData.roundDetails.push(`The ${monster.name} targeted your ${target}!`);
@@ -419,73 +360,111 @@ async function fightRound(player: Player, monster: MonsterWithFaction,  data: {a
     await updatePlayer(player);
     await clearTravelPlan(player.id);
 
-    return { roundData, monsters: []};
+    return { roundData, monsters: [], player};
   }
 
   await updatePlayer(player);
   await saveFightState(player.id, monster);
 
-  return { roundData, monsters: []};
+  return { roundData, monsters: [], player};
 };
 
 app.use(healerRouter);
 
-app.get('/player', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
 
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
+app.get('/chat/history', authEndpoint, async (req: AuthRequest, res: Response) => {
+  let html = chatHistory.map(renderChatMessage);
+
+  res.send(html.join("\n"));
+});
+
+app.post('/chat', authEndpoint, async (req: AuthRequest, res: Response) => {
+  const msg = req.body.message.trim();
+
+  if(!msg || !msg.length) {
+    res.sendStatus(204);
+    return;
   }
 
-  const inventory = await getEquippedItems(player.id);
+  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);
+      }
+    }
+  }
+  else {
+    message = broadcastMessage(req.player.username, msg);
+    chatHistory.push(message);
+    chatHistory.slice(-10);
+  }
 
-  res.send(renderPlayerBar(player, inventory) + (await renderProfilePage(player)));
+  if(message) {
+    io.emit('chat', renderChatMessage(message));
+    res.sendStatus(204);
+  }
 });
 
-app.get('/player/skills', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
+app.get('/player', authEndpoint, async (req: AuthRequest, res: Response) => {
+  const inventory = await getEquippedItems(req.player.id);
 
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
+  res.send(renderPlayerBar(req.player, inventory) + renderProfilePage(req.player));
+});
+
+app.post('/player/stat/:stat', authEndpoint, async (req: AuthRequest, res: Response) => {
+  const stat = req.params.stat;
+  if(!['strength', 'constitution', 'dexterity', 'intelligence'].includes(stat)) {
+    res.send(Alert.ErrorAlert(`Sorry, that's not a valid stat to increase`));
+    return;
   }
 
-  const skills = await getPlayerSkills(player.id);
+  if(req.player.stat_points <= 0) {
+    res.send(Alert.ErrorAlert(`Sorry, you don't have enough stat points`));
+    return;
+  }
 
-  res.send(renderSkills(skills));
+  req.player.stat_points -= 1;
+  req.player[stat]++;
+
+  updatePlayer(req.player);
+
+  const equippedItems = await getEquippedItems(req.player.id);
+  res.send(renderPlayerBar(req.player, equippedItems) + renderProfilePage(req.player));
 });
 
-app.get('/player/inventory', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
+app.get('/player/skills', authEndpoint, async (req: AuthRequest, res: Response) => {
+  const skills = await getPlayerSkills(req.player.id);
 
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
-  }
+  res.send(renderSkills(skills));
+});
 
+app.get('/player/inventory', authEndpoint, async (req: AuthRequest, res: Response) => {
   const [inventory, items] = await Promise.all([
-    getInventory(player.id),
-    getPlayersItems(player.id)
+    getInventory(req.player.id),
+    getPlayersItems(req.player.id)
   ]);
 
   res.send(renderInventoryPage(inventory, items));
 });
 
-app.post('/player/equip/:item_id/:slot', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
-
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
-  }
-
-  const inventoryItem = await getInventoryItem(player.id, req.params.item_id);
-  const equippedItems = await getEquippedItems(player.id);
+app.post('/player/equip/:item_id/:slot', authEndpoint, async (req: AuthRequest, 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;
   let desiredSlot: EquipmentSlot = inventoryItem.equipment_slot;
 
@@ -515,9 +494,9 @@ app.post('/player/equip/:item_id/:slot', authEndpoint, async (req: Request, res:
     }
 
 
-    await equip(player.id, inventoryItem, desiredSlot);
-    const socketId = cache.get(`socket:${player.id}`).toString();
-    io.to(socketId).emit('updatePlayer', player);
+    await equip(req.player.id, inventoryItem, desiredSlot);
+    const socketId = cache.get(`socket:${req.player.id}`).toString();
+    io.to(socketId).emit('updatePlayer', req.player);
     io.to(socketId).emit('alert', {
       type: 'success',
       text: `You equipped your ${inventoryItem.name}`
@@ -528,47 +507,30 @@ app.post('/player/equip/:item_id/:slot', authEndpoint, async (req: Request, res:
   }
 
   const [inventory, items] = await Promise.all([
-    getInventory(player.id),
-    getPlayersItems(player.id)
+    getInventory(req.player.id),
+    getPlayersItems(req.player.id)
   ]);
 
-  res.send(renderInventoryPage(inventory, items, inventoryItem.type) + renderPlayerBar(player, inventory));
+  res.send(renderInventoryPage(inventory, items, inventoryItem.type) + renderPlayerBar(req.player, inventory));
 });
 
-app.post('/player/unequip/:item_id', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
-
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
-  }
-
+app.post('/player/unequip/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
   const [item, ] = await Promise.all([
-    getInventoryItem(player.id, req.params.item_id),
-    unequip(player.id, req.params.item_id)
+    getInventoryItem(req.player.id, req.params.item_id),
+    unequip(req.player.id, req.params.item_id)
   ]);
 
   const [inventory, items] = await Promise.all([
-    getInventory(player.id),
-    getPlayersItems(player.id)
+    getInventory(req.player.id),
+    getPlayersItems(req.player.id)
   ]);
 
-  res.send(renderInventoryPage(inventory, items, item.type) + renderPlayerBar(player, inventory));
+  res.send(renderInventoryPage(inventory, items, item.type) + renderPlayerBar(req.player, inventory));
 });
 
-app.get('/player/explore', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
-
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
-  }
-
-
-  const fight = await loadMonsterFromFight(player.id);
-  let closestTown = player.city_id;
+app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response) => {
+  const fight = await loadMonsterFromFight(req.player.id);
+  let closestTown = req.player.city_id;
 
   if(fight) {
     // ok lets display the fight screen!
@@ -584,10 +546,10 @@ app.get('/player/explore', authEndpoint, async (req: Request, res: Response) =>
     res.send(renderFight(data));
   }
   else {
-    const travelPlan = await getTravelPlan(player.id);
+    const travelPlan = await getTravelPlan(req.player.id);
     if(travelPlan) {
       // traveling!
-      const travelPlan = await getTravelPlan(player.id);
+      const travelPlan = await getTravelPlan(req.player.id);
 
       const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
 
@@ -599,7 +561,7 @@ app.get('/player/explore', authEndpoint, async (req: Request, res: Response) =>
       }
 
       // STEP_DELAY
-      const nextAction = cache[`step:${player.id}`] || 0;
+      const nextAction = cache[`step:${req.player.id}`] || 0;
 
       res.send(renderTravel({
         things,
@@ -611,9 +573,9 @@ app.get('/player/explore', authEndpoint, async (req: Request, res: Response) =>
     else {
       // display the city info!
       const [city, locations, paths] = await Promise.all([
-        getCityDetails(player.city_id),
-        getAllServices(player.city_id),
-        getAllPaths(player.city_id)
+        getCityDetails(req.player.city_id),
+        getAllServices(req.player.city_id),
+        getAllPaths(req.player.city_id)
       ]);
 
       res.send(await renderMap({city, locations, paths}, closestTown));
@@ -623,14 +585,7 @@ app.get('/player/explore', authEndpoint, async (req: Request, res: Response) =>
 });
 
 // used to purchase equipment from a particular shop
-app.put('/location/:location_id/equipment/:item_id', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
-  }
-
+app.put('/location/:location_id/equipment/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
   const item = await getShopEquipment(parseInt(req.params.item_id), parseInt(req.params.location_id));
 
   if(!item) {
@@ -638,30 +593,23 @@ app.put('/location/:location_id/equipment/:item_id', authEndpoint, async (req: R
     return res.sendStatus(400);
   }
 
-  if(player.gold < item.cost) {
+  if(req.player.gold < item.cost) {
     res.send(Alert.ErrorAlert(`Sorry, you need at least ${item.cost.toLocaleString()}G to purchase this.`));
     return;
   }
 
-  player.gold -= item.cost;
+  req.player.gold -= item.cost;
 
-  await updatePlayer(player);
-  await addInventoryItem(player.id, item);
+  await updatePlayer(req.player);
+  await addInventoryItem(req.player.id, item);
 
-  const equippedItems = await getEquippedItems(player.id);
+  const equippedItems = await getEquippedItems(req.player.id);
 
-  res.send(renderPlayerBar(player, equippedItems) + Alert.SuccessAlert(`You purchased ${item.name}`));
+  res.send(renderPlayerBar(req.player, equippedItems) + Alert.SuccessAlert(`You purchased ${item.name}`));
 });
 
 // used to purchase items from a particular shop
-app.put('/location/:location_id/items/:item_id', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
-  }
-
+app.put('/location/:location_id/items/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
   const item: (ShopItem & Item) = await getItemFromShop(parseInt(req.params.item_id), parseInt(req.params.location_id));
 
   if(!item) {
@@ -669,32 +617,25 @@ app.put('/location/:location_id/items/:item_id', authEndpoint, async (req: Reque
     return res.sendStatus(400);
   }
 
-  if(player.gold < item.price_per_unit) {
+  if(req.player.gold < item.price_per_unit) {
     res.send(Alert.ErrorAlert(`Sorry, you need at least ${item.price_per_unit.toLocaleString()}G to purchase this.`));
     return;
   }
 
-  player.gold -= item.price_per_unit;
+  req.player.gold -= item.price_per_unit;
 
-  await updatePlayer(player);
-  await givePlayerItem(player.id, item.id, 1);
+  await updatePlayer(req.player);
+  await givePlayerItem(req.player.id, item.id, 1);
 
-  const equippedItems = await getEquippedItems(player.id);
+  const equippedItems = await getEquippedItems(req.player.id);
 
-  res.send(renderPlayerBar(player, equippedItems) + Alert.SuccessAlert(`You purchased a ${item.name}`));
+  res.send(renderPlayerBar(req.player, equippedItems) + Alert.SuccessAlert(`You purchased a ${item.name}`));
 });
 
 // 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: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
-  }
-
+app.get('/location/:location_id/equipment/:item_id/overview', authEndpoint, async (req: AuthRequest, res: Response) => {
   const equipment = await getShopEquipment(parseInt(req.params.item_id), parseInt(req.params.location_id));
 
   if(!equipment) {
@@ -706,10 +647,10 @@ app.get('/location/:location_id/equipment/:item_id/overview', authEndpoint, asyn
 <dialog>
   <div class="item-modal-overview">
     <div class="icon">
-      <img src="https://via.placeholder.com/64x64" title="${equipment.name}" alt="${equipment.name}"> 
+      <img src="${equipment.icon ? `/assets/img/icons/equipment/${equipment.icon}` : 'https://via.placeholder.com/64x64'}" title="${equipment.name}" alt="${equipment.name}"> 
     </div>
     <div>
-      ${renderEquipmentDetails(equipment, player)}
+      ${renderEquipmentDetails(equipment, req.player)}
     </div>
   </div>
   <div class="actions">
@@ -725,14 +666,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: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
-  }
-
+app.get('/location/:location_id/items/:item_id/overview', authEndpoint, async (req: AuthRequest, res: Response) => {
   const item: (ShopItem & Item) = await getItemFromShop(parseInt(req.params.item_id), parseInt(req.params.location_id));
 
   if(!item) {
@@ -761,15 +695,52 @@ app.get('/location/:location_id/items/:item_id/overview', authEndpoint, async (r
   res.send(html);
 });
 
-app.get('/modal/items/:item_id', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
+app.put('/item/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
+  const item: PlayerItem = await getItemFromPlayer(req.player.id, parseInt(req.params.item_id));
+
+  if(!item) {
+    console.log(`Can't find item [${req.params.item_id}]`);
+    return;
+  }
+
+  if(item.amount < 1) {
+    res.send(Alert.ErrorAlert(`You dont have enough ${item.name}`));
+    return;
   }
 
-  const item: PlayerItem = await getItemFromPlayer(player.id, parseInt(req.params.item_id));
+  item.amount -= 1;
+
+  switch(item.effect_name) {
+    case 'heal_small':
+      const hpGain = HealthPotionSmall.effect(req.player);
+
+      req.player.hp += hpGain;
+
+      if(req.player.hp > maxHp(req.player.constitution, req.player.level)) {
+        req.player.hp = maxHp(req.player.constitution, req.player.level);
+      }
+    break;
+  }
+
+  await updateItemCount(req.player.id, item.item_id, -1);
+  await updatePlayer(req.player);
+
+  const inventory = await getInventory(req.player.id);
+  const equippedItems = inventory.filter(i => i.is_equipped);
+  const items = await getPlayersItems(req.player.id);
+
+  res.send(
+    [
+      renderPlayerBar(req.player, equippedItems),
+      renderInventoryPage(inventory, items, 'ITEMS'),
+      Alert.SuccessAlert(`You used the ${item.name}`)
+    ].join("")
+  );
+
+});
+
+app.get('/modal/items/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
+  const item: PlayerItem = await getItemFromPlayer(req.player.id, parseInt(req.params.item_id));
 
   if(!item) {
     logger.log(`Invalid item [${req.params.item_id}]`);
@@ -777,34 +748,30 @@ app.get('/modal/items/:item_id', authEndpoint, async (req: Request, res: Respons
   }
 
   let html = `
-<div class="item-modal-overview">
-<div class="icon">
-<img src="/assets/img/icons/items/${item.icon_name}" title="${item.name}" alt="${item.name}"> 
-</div>
-<div>
-<h4>${item.name}</h4>
-<p>${item.description}</p>
-</div>
-</div>
-<div class="actions">
-<button class="emit-event close-modal" data-event="item:use:${item.effect_name}" data-args="${item.item_id}">Use</button>
-</div>
+<dialog>
+  <div class="item-modal-overview">
+    <div class="icon">
+      <img src="/assets/img/icons/items/${item.icon_name}" title="${item.name}" alt="${item.name}"> 
+    </div>
+    <div>
+      <h4>${item.name}</h4>
+      <p>${item.description}</p>
+    </div>
+  </div>
+  <div class="actions">
+    <button hx-put="/item/${item.item_id}" formmethod="dialog" value="cancel" hx-target="#inventory">Use</button>
+    <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
+  </div>
+</dialog>
 `;
 
-  return res.json({ description: html });
+  res.send(html);
 });
 
-app.get('/city/stores/city:stores/:location_id', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
-  }
-
+app.get('/city/stores/city:stores/:location_id', authEndpoint, async (req: AuthRequest, res: Response) => {
   const location = await getService(parseInt(req.params.location_id));
 
-  if(!location || location.city_id !== player.city_id) {
+  if(!location || location.city_id !== req.player.city_id) {
     logger.log(`Invalid location: [${req.params.location_id}]`);
     res.sendStatus(400);
   }
@@ -813,21 +780,14 @@ app.get('/city/stores/city:stores/:location_id', authEndpoint, async (req: Reque
     getShopItems(location.id)
   ]);
 
-  const html = await renderStore(shopEquipment, shopItems, player);
+  const html = await renderStore(shopEquipment, shopItems, req.player);
 
   res.send(html);
 });
 
-app.get('/city/explore/city:explore/:location_id', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
-  }
-
+app.get('/city/explore/city:explore/:location_id', authEndpoint, async (req: AuthRequest, res: Response) => {
   const location = await getService(parseInt(req.params.location_id));
-  if(!location || location.city_id !== player.city_id) {
+  if(!location || location.city_id !== req.player.city_id) {
 
     logger.log(`Invalid location: [${req.params.location_id}]`);
     res.sendStatus(400);
@@ -837,43 +797,28 @@ app.get('/city/explore/city:explore/:location_id', authEndpoint, async (req: Req
   res.send(renderMonsterSelector(monsters));
 });
 
-app.post('/travel', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
+app.post('/travel', authEndpoint, async (req: AuthRequest, res: Response) => {
   const destination_id = parseInt(req.body.destination_id);
 
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
-  }
-
   if(!destination_id || isNaN(destination_id)) {
     logger.log(`Invalid destination_id [${req.body.destination_id}]`);
     return res.sendStatus(400);
   }
 
-  const travelPlan = travel(player, req.body.destination_id);
+  const travelPlan = travel(req.player, req.body.destination_id);
 
   res.json(travelPlan);
 });
 
-app.post('/fight/turn', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
-
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
-  }
-
-  const monster = await loadMonsterWithFaction(player.id);
+app.post('/fight/turn', authEndpoint, async (req: AuthRequest, res: Response) => {
+  const monster = await loadMonsterWithFaction(req.player.id);
 
   if(!monster) {
     res.send(Alert.ErrorAlert('Not in a fight'));
     return;
   }
 
-  const fightData  = await fightRound(player, monster, {
+  const fightData  = await fightRound(req.player, monster, {
     action: req.body.action,
     target: req.body.fightTarget
   });
@@ -885,33 +830,25 @@ app.post('/fight/turn', authEndpoint, async (req: Request, res: Response) => {
   );
 
   if(fightData.monsters.length && monster.fight_trigger === 'explore') {
-    html += renderMonsterSelector(fightData.monsters);
+    html += renderMonsterSelector(fightData.monsters, monster.ref_id);
   }
 
   let travelSection = '';
   if(monster.fight_trigger === 'travel' && fightData.roundData.winner === 'player') {
     // you're travellinga dn you won.. display the keep walking!
-    const travelPlan = await getTravelPlan(player.id);
+    const travelPlan = await getTravelPlan(req.player.id);
     const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
     travelSection = travelButton(0);
   }
 
-  const equippedItems = await getEquippedItems(player.id);
-  const playerBar = renderPlayerBar(player, equippedItems);
+  const equippedItems = await getEquippedItems(req.player.id);
+  const playerBar = renderPlayerBar(fightData.player, equippedItems);
 
   res.send(html + travelSection + playerBar);
 });
 
-app.post('/fight', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
-
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
-  }
-
-  if(player.hp <= 0) {
+app.post('/fight', authEndpoint, async (req: AuthRequest, res: Response) => {
+  if(req.player.hp <= 0) {
     logger.log(`Player didn\'t have enough hp`);
     return res.sendStatus(400);
   }
@@ -936,7 +873,7 @@ app.post('/fight', authEndpoint, async (req: Request, res: Response) => {
     return res.sendStatus(400);
   }
 
-  const fight = await createFight(player.id, monster, fightTrigger);
+  const fight = await createFight(req.player.id, monster, fightTrigger);
 
 
   const data: MonsterForFight = {
@@ -951,17 +888,10 @@ app.post('/fight', authEndpoint, async (req: Request, res: Response) => {
   res.send(renderFight(data));
 });
 
-app.post('/travel/step', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
-  const stepTimerKey = `step:${player.id}`;
+app.post('/travel/step', authEndpoint, async (req: AuthRequest, res: Response) => {
+  const stepTimerKey = `step:${req.player.id}`;
 
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
-  }
-
-  const travelPlan = await getTravelPlan(player.id);
+  const travelPlan = await getTravelPlan(req.player.id);
   if(!travelPlan) {
     res.send(Alert.ErrorAlert('You don\'t have a travel plan'));
     return;
@@ -977,10 +907,10 @@ app.post('/travel/step', authEndpoint, async (req: Request, res: Response) => {
   travelPlan.current_position++;
 
   if(travelPlan.current_position >= travelPlan.total_distance) {
-    const travel = await completeTravel(player.id);
+    const travel = await completeTravel(req.player.id);
 
-    player.city_id = travel.destination_id;
-    await movePlayer(travel.destination_id, player.id);
+    req.player.city_id = travel.destination_id;
+    await movePlayer(travel.destination_id, req.player.id);
 
     const [city, locations, paths] = await Promise.all([
       getCityDetails(travel.destination_id),
@@ -989,7 +919,7 @@ app.post('/travel/step', authEndpoint, async (req: Request, res: Response) => {
     ]);
 
     delete cache[stepTimerKey];
-    res.send(await renderMap({city, locations, paths}, player.city_id));
+    res.send(await renderMap({city, locations, paths}, req.player.city_id));
   }
   else {
     const walkingText: string[] = [
@@ -998,7 +928,7 @@ app.post('/travel/step', authEndpoint, async (req: Request, res: Response) => {
     ];
     // update existing plan..
     // decide if they will run into anything
-    const travelPlan = await stepForward(player.id);
+    const travelPlan = await stepForward(req.player.id);
 
     const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
 
@@ -1024,16 +954,8 @@ app.post('/travel/step', authEndpoint, async (req: Request, res: Response) => {
   }
 });
 
-app.post('/travel/:destination_id', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
-
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
-  }
-
-  if(player.hp <= 0) {
+app.post('/travel/:destination_id', authEndpoint, async (req: AuthRequest, 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.'));
     return;
@@ -1046,16 +968,38 @@ app.post('/travel/:destination_id', authEndpoint, async (req: Request, res: Resp
     return;
   }
 
-  await travel(player, destination.id);
+  await travel(req.player, destination.id);
 
   res.send(renderTravel({
     things: [],
     nextAction: 0,
     walkingText: '',
-    closestTown: player.city_id
+    closestTown: req.player.city_id
   }));
 });
 
+app.get('/settings', authEndpoint, async (req: AuthRequest, res: Response) => {
+  let warning = '';
+  let html = '';
+  if(req.player.account_type === 'session') {
+    warning += `<div class="alert error">If you log out without signing up first, this account is lost forever.</div>`;
+  }
+
+  html += '<a href="#" hx-post="/logout">Logout</a>';
+  res.send(warning + html);
+});
+
+app.post('/logout', authEndpoint, async (req: AuthRequest, res: Response) => {
+  // ref to get the socket id for a particular player
+  cache.delete(`socket:${req.player.id}`);
+  // ref to get the player object
+  cache.delete(`token:${req.player.id}`);
+
+  logger.log(`${req.player.username} logged out`);
+
+  res.send('logout');
+});
+
 
 app.post('/signup', async (req: Request, res: Response) => {
   const {username, password} = req.body;
@@ -1114,12 +1058,6 @@ app.post('/login', async (req: Request, res: Response) => {
   }
 });
 
-app.get('/status', async (req: Request, res: Response) => {
-  res.json({
-    onlinePlayers: io.sockets.sockets.size
-  });
-})
-
 server.listen(process.env.API_PORT, () => {
   logger.log(`Listening on port ${process.env.API_PORT}`);
 });