chore(release): 0.3.2
[risinglegends.git] / src / server / api.ts
index bd86f977d9eaae3652e87312803ccc74aae6da0f..f167ff61bf0ba9cf5a3b1bb154485e11f29ad784 100644 (file)
@@ -4,29 +4,33 @@ import { config as dotenv } from 'dotenv';
 import { join } from 'path';
 import express, {Request, Response} from 'express';
 import bodyParser from 'body-parser';
+import xss from 'xss';
+import { rateLimit } from 'express-rate-limit';
 
 import http from 'http';
 import { Server, Socket } from 'socket.io';
+import * as CONSTANT from '../shared/constants';
 import { logger } from './lib/logger';
 import { loadPlayer, createPlayer, updatePlayer, movePlayer } from './player';
 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 {maxHp, maxVigor, Player} from '../shared/player';
+import {createFight, getMonsterList, getMonsterLocation, getRandomMonster, loadMonster, loadMonsterFromFight, loadMonsterWithFaction} from './monster';
+import {addInventoryItem, getEquippedItems, getInventory, getInventoryItem} from './inventory';
 import { getItemFromPlayer, getItemFromShop, getPlayersItems, getShopItems, givePlayerItem, updateItemCount } from './items';
-import {FightTrigger, Monster, MonsterForFight, MonsterWithFaction} from '../shared/monsters';
-import {getShopEquipment, getShopItem, listShopItems } from './shopEquipment';
-import {EquippedItemDetails} from '../shared/equipped';
-import {ArmourEquipmentSlot, EquipmentSlot} from '../shared/inventory';
+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 } 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';
+import { getPlayerSkills} from './skills';
 
-import  { router as healerRouter } from './locations/healer';
+import { fightRound, blockPlayerInFight } from './fight';
+
+import { router as healerRouter } from './locations/healer';
+import { router as professionRouter } from './locations/recruiter';
+import { router as repairRouter } from './locations/repair';
 
 import * as Alert from './views/alert';
 import { renderPlayerBar } from './views/player-bar'
@@ -35,15 +39,15 @@ import { renderMap } from './views/map';
 import { renderProfilePage } from './views/profile';
 import { renderSkills } from './views/skills';
 import { renderInventoryPage } from './views/inventory';
-import { renderMonsterSelector } from './views/monster-selector';
-import { renderFight, renderRoundDetails } from './views/fight';
+import { renderMonsterSelector, renderOnlyMonsterSelector } from './views/monster-selector';
+import { renderFight, renderFightPreRound, 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 { HealthPotionSmall } from '../shared/items/health_potion';
@@ -69,6 +73,42 @@ app.use((req, res, next) => {
   next();
 });
 
+const fightRateLimiter = rateLimit({
+  windowMs: parseInt(process.env.RATE_LIMIT_WINDOW || '30000'),
+  max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '20'),
+  standardHeaders: true,
+  legacyHeaders: false,
+  handler: (req, res, next, options) => {
+    logger.log(`Blocked request: [${req.headers['x-authtoken']}: ${req.method} ${req.path}]`);
+    res.status(options.statusCode).send(options.message);
+  }
+});
+
+async function bootstrapSocket(socket: Socket, player: Player) {
+  // ref to get the socket id for a particular 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']}`);
 
@@ -88,321 +128,35 @@ io.on('connection', async socket => {
 
   logger.log(`Socket [${socket.id}] auth token: ${player.id}`);
 
-  // ref to get the socket id for a particular player
-  cache.set(`socket:${player.id}`, socket.id);
-  // ref to get the player object
-  cache.set(`token:${player.id}`, player);
-
-  socket.emit('authToken', player.id);
-
-  socket.emit('chat', broadcastMessage('server', `${player.username} just logged in`));
+  bootstrapSocket(socket, player);
 
   socket.on('disconnect', () => {
     console.log(`Player ${player.username} left`);
-  });
+    cache.delete(`socket-lookup:${socket.id}`);
 
-  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', `${uniqueConnectedUsers().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');
 });
 
-async function fightRound(player: Player, monster: MonsterWithFaction,  data: {action: 'attack' | 'cast' | 'flee', target: 'head' | 'body' | 'arms' | 'legs'}) {
-  const playerSkills = await getPlayerSkillsAsObject(player.id);
-  const roundData: FightRound = {
-    monster,
-    player,
-    winner: 'in-progress',
-    fightTrigger: monster.fight_trigger,
-    roundDetails: [],
-    rewards: {
-      exp: 0,
-      gold: 0,
-      levelIncrease: false
-    }
-  };
-  const equippedItems = await getEquippedItems(player.id);
-
-  // we only use this if the player successfully defeated the monster 
-  // they were fighting, then we load the other monsters in this area 
-  // so they can "fight again"
-  let potentialMonsters: MonsterForFight[] = [];
-
-  /*
-   * cumulative chance of head/arms/body/legs
-   * 0 -> 0.2 = head
-   * 0.21 -> 0.4 = arms
-   *
-   * we use the factor to decide how many decimal places 
-   * we care about
-   */
-  const factor = 100;
-  const monsterTarget = [0.2, 0.4, 0.9, 1];
-  const targets: ArmourEquipmentSlot[] = ['HEAD', 'CHEST', 'ARMS', 'LEGS'];
-  // calc weighted
-  const rand = Math.ceil(Math.random() * factor);
-  let target: ArmourEquipmentSlot = 'CHEST';
-  monsterTarget.forEach((i, idx) => {
-    if (rand > (i * factor)) {
-      target = targets[idx] as ArmourEquipmentSlot;
-    }
-  });
-
-  const boost = {
-    strength: 0,
-    constitution: 0,
-    dexterity: 0,
-    intelligence: 0,
-    damage: 0,
-    hp: 0,
-  };
-
-  const equipment: Map<EquipmentSlot, EquippedItemDetails> = new Map<EquipmentSlot, EquippedItemDetails>();
-  const weapons: EquippedItemDetails[] = [];
-  let anyDamageSpells: boolean = false;
-  equippedItems.forEach(item => {
-    if(item.type === 'ARMOUR') {
-      equipment.set(item.equipment_slot, item);
-    }
-    else if(item.type === 'WEAPON') {
-      weapons.push(item);
-    }
-    else if(item.type === 'SPELL') {
-      if(item.affectedSkills.includes('destruction_magic')) {
-        anyDamageSpells = true;
-      }
-      weapons.push(item);
-    }
-
-    boost.strength += item.boosts.strength;
-    boost.constitution += item.boosts.constitution;
-    boost.dexterity += item.boosts.dexterity;
-    boost.intelligence += item.boosts.intelligence;
-
-    if(item.type === 'SPELL' && item.affectedSkills.includes('restoration_magic')) {
-      boost.hp += item.boosts.damage;
-    }
-    else {
-      boost.damage += item.boosts.damage;
-    }
-  });
-
-  // if you flee'd, then we want to check your dex vs. the monsters
-  // but we want to give you the item/weapon boosts you need
-  // if not then you're going to get hit.
-  if(data.action === 'flee') {
-    roundData.roundDetails.push(`You managed to escape from the ${monster.name}!`)
-    roundData.winner = 'monster';
-    await clearFight(player.id);
-
-    return { roundData, monsters: [], player };
-  }
-
-  const attackType = data.action === 'attack' ? 'physical' : 'magical';
-  const primaryStat = data.action === 'attack' ? player.strength : player.intelligence;
-  const boostStat = data.action === 'attack' ? boost.strength : boost.intelligence;
-
-  const playerDamage = Math.floor(((primaryStat + boostStat) * 1.3) + boost.damage);
-  const skillsUsed: Record<SkillID | any, number> = {};
-  let hpHealAfterMasteries: number = -1;
-  let playerDamageAfterMasteries: number = 0;
-  // apply masteries!
-  weapons.forEach(item => {
-    item.affectedSkills.forEach(id => {
-      if(id === 'restoration_magic') {
-        if(hpHealAfterMasteries < 0) {
-          hpHealAfterMasteries = 0;
-        }
-        hpHealAfterMasteries += Skills.get(id).effect(playerSkills.get(id));
-      }
-      else {
-        playerDamageAfterMasteries += playerDamage * Skills.get(id).effect(playerSkills.get(id));
-      }
-
-      if(!skillsUsed[id]) {
-        skillsUsed[id] = 0;
-      }
-      skillsUsed[id]++;
-    });
-  });
-
-  await updatePlayerSkills(player.id, skillsUsed);
-
-  const playerFinalDamage = (data.action === 'cast' && !anyDamageSpells) ? 0 : Math.floor(playerDamage + playerDamageAfterMasteries);
-  const playerFinalHeal = Math.floor(boost.hp + hpHealAfterMasteries);
-
-  roundData.roundDetails.push(`You targeted the monsters ${data.target.toUpperCase()} with ${attackType} damage!`);
-  let armourKey: string;
-  switch(data.target) {
-    case 'arms':
-      armourKey = 'armsAp';
-      break;
-    case 'head':
-      armourKey = 'helmAp';
-      break;
-    case 'legs':
-      armourKey = 'legsAp';
-      break;
-    case 'body':
-      armourKey = 'chestAp';
-      break;
-  }
-
-  if(monster[armourKey] && monster[armourKey] > 0) {
-    monster[armourKey] -= playerFinalDamage;
-
-    roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
-    if(monster[armourKey] < 0) {
-      roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
-      roundData.roundDetails.push(`You dealt ${monster[armourKey] * -1} damage to their HP`);
-      monster.hp += monster[armourKey];
-      monster[armourKey] = 0;
-    }
-  }
-  else {
-    roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
-    monster.hp -= playerFinalDamage;
-  }
-
-  if(monster.hp <= 0) {
-    roundData.monster.hp = 0;
-    roundData.winner = 'player';
-
-    roundData.rewards.exp = monster.exp;
-    roundData.rewards.gold = monster.gold;
-
-    player.gold += monster.gold;
-    player.exp += monster.exp;
-
-    if(player.exp >= expToLevel(player.level + 1)) {
-      player.exp -= expToLevel(player.level + 1)
-      player.level++;
-      roundData.rewards.levelIncrease = true;
-      let statPointsGained = 1;
-
-      if(player.profession !== 'Wanderer') {
-        statPointsGained = 2;
-      }
-
-      player.stat_points += statPointsGained;
-
-      roundData.roundDetails.push(`You gained ${statPointsGained} stat points!`);
-
-      player.hp = maxHp(player.constitution, player.level);
-    }
-    // get the monster location if it was an EXPLORED fight
-    if(roundData.fightTrigger === 'explore') {
-      const rawMonster = await loadMonster(monster.ref_id);
-      const monsterList  = await getMonsterList(rawMonster.location_id);
-      potentialMonsters = monsterList.map(monster => {
-        return {
-          id: monster.id,
-          name: monster.name,
-          level: monster.level,
-          hp: monster.hp,
-          maxHp: monster.maxHp,
-          fight_trigger: 'explore'
-        }
-      });
-    }
-
-    await clearFight(player.id);
-    await updatePlayer(player);
-    return { roundData, monsters: potentialMonsters, player };
-  }
-
-  roundData.roundDetails.push(`The ${monster.name} targeted your ${target}!`);
-  if(equipment.has(target)) {
-    const item = equipment.get(target);
-    // apply mitigation!
-    const mitigationPercentage = item.boosts.damage_mitigation || 0;
-    const damageAfterMitigation = Math.floor(monster.strength * ((100-mitigationPercentage)/100));
-
-    item.currentAp -= damageAfterMitigation;
-
-    if(item.currentAp < 0) {
-      roundData.roundDetails.push(`Your ${item.name} amour was destroyed`);
-      roundData.roundDetails.push(`The ${monster.name} hit your HP for ${item.currentAp * -1} damage!`);
-      player.hp += item.currentAp;
-      item.currentAp = 0;
-      await deleteInventoryItem(player.id, item.item_id);
-    }
-    else {
-      roundData.roundDetails.push(`Your ${target} took ${damageAfterMitigation} damage!`);
-      await updateAp(player.id, item.item_id, item.currentAp, item.maxAp);
-    }
-
-  }
-  else {
-    roundData.roundDetails.push(`The ${monster.name} hit you for ${monster.strength} damage`);
-    player.hp -= monster.strength;
-  }
-
-  if(playerFinalHeal > 0) {
-    player.hp += playerFinalHeal;
-    if(player.hp > maxHp(player.constitution, player.level)) {
-      player.hp = maxHp(player.constitution, player.level);
-    }
-    roundData.roundDetails.push(`You healed for ${playerFinalHeal} HP`);
-  }
-
-  // update the players inventory for this item!
-
-  if(player.hp <= 0) {
-    player.hp = 0;
-    roundData.winner = 'monster';
-
-    roundData.roundDetails.push(`You were killed by the ${monster.name}`);
-
-    await clearFight(player.id);
-    await updatePlayer(player);
-    await clearTravelPlan(player.id);
-
-    return { roundData, monsters: [], player};
-  }
-
-  await updatePlayer(player);
-  await saveFightState(player.id, monster);
-
-  return { roundData, monsters: [], player};
-};
 
 app.use(healerRouter);
+app.use(professionRouter);
+app.use(repairRouter);
 
 
-app.get('/chat/history', 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: 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('/chat', authEndpoint, async (req: AuthRequest, res: Response) => {
   const msg = req.body.message.trim();
 
   if(!msg || !msg.length) {
@@ -422,6 +176,7 @@ app.post('/chat', authEndpoint, async (req: Request, res: Response) => {
     }
     else if(msg === '/server lmnop refresh-shops') {
       await createShopItems();
+      await createShopEquipment();
       message = broadcastMessage('server', 'Refresh shop items');
     }
     else {
@@ -430,80 +185,74 @@ app.post('/chat', authEndpoint, async (req: Request, res: Response) => {
         message = broadcastMessage('server', str);
       }
     }
-
-
+  }
+  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(player.username, msg);
+    message = broadcastMessage(req.player.username, xss(msg, {
+      whiteList: {}
+    }));
     chatHistory.push(message);
     chatHistory.slice(-10);
-
-    io.emit('chat', message);
-    res.sendStatus(204);
   }
 
   if(message) {
-    io.emit('chat', message);
+    io.emit('chat', renderChatMessage(message));
+    res.sendStatus(204);
   }
-
 });
 
-app.get('/player', 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 equipment = await getEquippedItems(req.player.id);
+  res.send(renderPlayerBar(req.player) + renderProfilePage(req.player, equipment));
+});
 
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
+app.post('/player/stat/:stat', authEndpoint, async (req: AuthRequest, res: Response) => {
+  const equipment = await getEquippedItems(req.player.id);
+  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 inventory = await getEquippedItems(player.id);
+  if(req.player.stat_points <= 0) {
+    res.send(Alert.ErrorAlert(`Sorry, you don't have enough stat points`));
+    return;
+  }
 
-  res.send(renderPlayerBar(player, inventory) + (await renderProfilePage(player)));
-});
+  req.player.stat_points -= 1;
+  req.player[stat]++;
 
-app.get('/player/skills', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
+  req.player.hp = maxHp(req.player.constitution, req.player.level);
+  req.player.vigor = maxVigor(req.player.constitution, req.player.level);
+  updatePlayer(req.player);
 
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
-  }
+  res.send(renderPlayerBar(req.player) + renderProfilePage(req.player, equipment));
+});
 
-  const skills = await getPlayerSkills(player.id);
+app.get('/player/skills', authEndpoint, async (req: AuthRequest, res: Response) => {
+  const skills = await getPlayerSkills(req.player.id);
 
   res.send(renderSkills(skills));
 });
 
-app.get('/player/inventory', 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('/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, blockPlayerInFight, 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;
 
@@ -533,9 +282,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}`
@@ -546,50 +295,37 @@ 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));
 });
 
-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, blockPlayerInFight, 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));
 });
 
-app.get('/player/explore', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
+app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response) => {
+  const fight = await loadMonsterFromFight(req.player.id);
+  const travelPlan = await getTravelPlan(req.player.id);
+  let closestTown = req.player.city_id;
 
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
+  if(travelPlan) {
+      closestTown = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
   }
 
-
-  const fight = await loadMonsterFromFight(player.id);
-  let closestTown = player.city_id;
-
   if(fight) {
-    // ok lets display the fight screen!
     const data: MonsterForFight = {
       id: fight.id,
       hp: fight.hp,
@@ -598,57 +334,48 @@ app.get('/player/explore', authEndpoint, async (req: Request, res: Response) =>
       level: fight.level,
       fight_trigger: fight.fight_trigger
     };
+    const location = await getMonsterLocation(fight.ref_id);
+
 
-    res.send(renderFight(data));
+    res.send(renderPlayerBar(req.player) + renderFightPreRound(data, true, location, closestTown));
   }
   else {
-    const travelPlan = await getTravelPlan(player.id);
     if(travelPlan) {
       // traveling!
-      const travelPlan = await getTravelPlan(player.id);
-
-      const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
-
       const chanceToSeeMonster = random(0, 100);
       const things: any[] = [];
       if(chanceToSeeMonster <= 30) {
-        const monster = await getRandomMonster([closest]);
+        const monster = await getRandomMonster([closestTown]);
         things.push(monster);
       }
 
       // STEP_DELAY
-      const nextAction = cache[`step:${player.id}`] || 0;
+      const nextAction = cache[`step:${req.player.id}`] || 0;
 
-      res.send(renderTravel({
+      res.send(renderPlayerBar(req.player) + renderTravel({
         things,
         nextAction,
-        closestTown: closest,
-        walkingText: ''
+        closestTown: closestTown,
+        walkingText: '',
+        travelPlan
       }));
     }
     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));
+      res.send(renderPlayerBar(req.player) + await renderMap({city, locations, paths}, closestTown));
     }
 
   }
 });
 
 // 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) {
@@ -656,30 +383,21 @@ 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);
-
-  res.send(renderPlayerBar(player, equippedItems) + Alert.SuccessAlert(`You purchased ${item.name}`));
+  res.send(renderPlayerBar(req.player) + 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) {
@@ -687,32 +405,23 @@ 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;
-
-  await updatePlayer(player);
-  await givePlayerItem(player.id, item.id, 1);
+  req.player.gold -= item.price_per_unit;
 
-  const equippedItems = await getEquippedItems(player.id);
+  await updatePlayer(req.player);
+  await givePlayerItem(req.player.id, item.id, 1);
 
-  res.send(renderPlayerBar(player, equippedItems) + Alert.SuccessAlert(`You purchased a ${item.name}`));
+  res.send(renderPlayerBar(req.player) + 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) {
@@ -724,14 +433,14 @@ 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">
-    <button hx-put="/location/${equipment.location_id}/equipment/${equipment.id}" formmethod="dialog" value="cancel">Buy</button>
+    <button hx-put="/location/${equipment.location_id}/equipment/${equipment.id}" formmethod="dialog" value="cancel" class="green">Buy</button>
     <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
   </div>
 </dialog>
@@ -743,14 +452,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) {
@@ -770,7 +472,7 @@ app.get('/location/:location_id/items/:item_id/overview', authEndpoint, async (r
     </div>
   </div>
   <div class="actions">
-    <button hx-put="/location/${item.location_id}/items/${item.id}" formmethod="dialog" value="cancel">Buy</button>
+    <button hx-put="/location/${item.location_id}/items/${item.id}" formmethod="dialog" value="cancel" class="red">Buy</button>
     <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
   </div>
 </dialog>
@@ -779,15 +481,8 @@ app.get('/location/:location_id/items/:item_id/overview', authEndpoint, async (r
   res.send(html);
 });
 
-app.put('/item/: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);
-  }
-
-  const item: PlayerItem = await getItemFromPlayer(player.id, parseInt(req.params.item_id));
+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}]`);
@@ -803,26 +498,25 @@ app.put('/item/:item_id', authEndpoint, async (req: Request, res: Response) => {
 
   switch(item.effect_name) {
     case 'heal_small':
-      const hpGain = HealthPotionSmall.effect(player);
+      const hpGain = HealthPotionSmall.effect(req.player);
 
-      player.hp += hpGain;
+      req.player.hp += hpGain;
 
-      if(player.hp > maxHp(player.constitution, player.level)) {
-        player.hp = maxHp(player.constitution, player.level);
+      if(req.player.hp > maxHp(req.player.constitution, req.player.level)) {
+        req.player.hp = maxHp(req.player.constitution, req.player.level);
       }
     break;
   }
 
-  await updateItemCount(player.id, item.item_id, -1);
-  await updatePlayer(player);
+  await updateItemCount(req.player.id, item.item_id, -1);
+  await updatePlayer(req.player);
 
-  const inventory = await getInventory(player.id);
-  const equippedItems = inventory.filter(i => i.is_equipped);
-  const items = await getPlayersItems(player.id);
+  const inventory = await getInventory(req.player.id);
+  const items = await getPlayersItems(req.player.id);
 
   res.send(
     [
-      renderPlayerBar(player, equippedItems),
+      renderPlayerBar(req.player),
       renderInventoryPage(inventory, items, 'ITEMS'),
       Alert.SuccessAlert(`You used the ${item.name}`)
     ].join("")
@@ -830,15 +524,8 @@ app.put('/item/:item_id', authEndpoint, async (req: Request, res: Response) => {
 
 });
 
-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);
-  }
-
-  const item: PlayerItem = await getItemFromPlayer(player.id, parseInt(req.params.item_id));
+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}]`);
@@ -857,7 +544,7 @@ app.get('/modal/items/:item_id', authEndpoint, async (req: Request, res: Respons
     </div>
   </div>
   <div class="actions">
-    <button hx-put="/item/${item.item_id}" formmethod="dialog" value="cancel" hx-target="#inventory">Use</button>
+    <button hx-put="/item/${item.item_id}" formmethod="dialog" value="cancel" hx-target="#inventory" class="red">Use</button>
     <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
   </div>
 </dialog>
@@ -866,96 +553,82 @@ app.get('/modal/items/:item_id', authEndpoint, async (req: Request, res: Respons
   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);
   }
   const [shopEquipment, shopItems] = await Promise.all([
     listShopItems({location_id: location.id}),
-    getShopItems(location.id)
+    getShopItems(location.id),
   ]);
 
-  const html = await renderStore(shopEquipment, shopItems, player);
+  const html = await renderStore(shopEquipment, shopItems, req.player, location);
 
   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);
   }
 
   const monsters: Monster[] = await getMonsterList(location.id);
-  res.send(renderMonsterSelector(monsters));
+  res.send(renderOnlyMonsterSelector(monsters, 0, location));
 });
 
-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)
+app.post('/fight/turn', authEndpoint, async (req: AuthRequest, res: Response) => {
+  const fightBlockKey = `fightturn:${req.player.id}`;
+
+  if(cache[fightBlockKey] && cache[fightBlockKey] > Date.now()) {
+    res.status(429).send(Alert.ErrorAlert('Hmm, you are fight too quickly'));
+    return;
 
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
   }
 
-  const monster = await loadMonsterWithFaction(player.id);
+  cache[fightBlockKey] = Date.now() + CONSTANT.FIGHT_ATTACK_DELAY;
+  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
   });
 
+
   let html = renderFight(
     monster,
     renderRoundDetails(fightData.roundData),
-    fightData.roundData.winner === 'in-progress'
+    fightData.roundData.winner === 'in-progress',
+    cache[fightBlockKey]
   );
 
+  if(fightData.roundData.winner !== 'in-progress') {
+    delete cache[fightBlockKey];
+  }
+
   if(fightData.monsters.length && monster.fight_trigger === 'explore') {
     html += renderMonsterSelector(fightData.monsters, monster.ref_id);
   }
@@ -963,27 +636,18 @@ app.post('/fight/turn', authEndpoint, async (req: Request, res: Response) => {
   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(fightData.player, equippedItems);
+  const playerBar = renderPlayerBar(fightData.player);
 
   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', fightRateLimiter, authEndpoint, async (req: AuthRequest, res: Response) => {
+  if(req.player.hp <= 0) {
     logger.log(`Player didn\'t have enough hp`);
     return res.sendStatus(400);
   }
@@ -1008,7 +672,8 @@ 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 location = await getService(monster.location_id);
 
 
   const data: MonsterForFight = {
@@ -1020,20 +685,13 @@ app.post('/fight', authEndpoint, async (req: Request, res: Response) => {
     fight_trigger: fight.fight_trigger
   };
 
-  res.send(renderFight(data));
+  res.send(renderFightPreRound(data, true, location, location.city_id));
 });
 
-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}`;
-
-  if(!player) {
-    logger.log(`Couldnt find player with id ${authToken}`);
-    return res.sendStatus(400);
-  }
+app.post('/travel/step', authEndpoint, async (req: AuthRequest, res: Response) => {
+  const stepTimerKey = `step:${req.player.id}`;
 
-  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;
@@ -1049,10 +707,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),
@@ -1061,7 +719,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[] = [
@@ -1070,7 +728,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;
 
@@ -1082,7 +740,7 @@ app.post('/travel/step', authEndpoint, async (req: Request, res: Response) => {
     }
 
     // STEP_DELAY
-    const nextAction = Date.now() + 3000;
+    const nextAction = Date.now() + CONSTANT.STEP_DELAY;
 
     cache[stepTimerKey] = nextAction;
 
@@ -1090,22 +748,49 @@ app.post('/travel/step', authEndpoint, async (req: Request, res: Response) => {
       things,
       nextAction,
       closestTown: closest,
-      walkingText: sample(walkingText)
+      walkingText: sample(walkingText),
+      travelPlan
     }));
 
   }
 });
 
-app.post('/travel/:destination_id', authEndpoint, async (req: Request, res: Response) => {
-  const authToken = req.headers['x-authtoken'].toString();
-  const player: Player = await loadPlayer(authToken)
+app.post('/travel/return-to-source', authEndpoint, async (req: AuthRequest, res: Response) => {
+  // puts the player back in their starting town
+  // doesn't matter if they don't have one
+  // redirect them!
+  await clearTravelPlan(req.player.id);
+
+  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));
+  }
+  else {
+    const [city, locations, paths] = await Promise.all([
+      getCityDetails(req.player.city_id),
+      getAllServices(req.player.city_id),
+      getAllPaths(req.player.city_id)
+    ]);
+
+    res.send(renderPlayerBar(req.player) + await renderMap({city, locations, paths}, req.player.city_id));
 
-  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;
@@ -1118,27 +803,63 @@ app.post('/travel/:destination_id', authEndpoint, async (req: Request, res: Resp
     return;
   }
 
-  await travel(player, destination.id);
+  const travelPlan = await travel(req.player, destination.id);
 
   res.send(renderTravel({
     things: [],
     nextAction: 0,
     walkingText: '',
-    closestTown: player.city_id
+    closestTown: req.player.city_id,
+    travelPlan
   }));
 });
 
+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('/auth', async (req: Request, res: Response) => {
+  if(req.body.authType === 'login') {
+    loginHandler(req, res);
+  }
+  else if(req.body.authType === 'signup') { 
+    signupHandler(req, res);
+  }
+  else {
+    logger.log(`Invalid auth type [${req.body.authType}]`);
+    res.sendStatus(400);
+  }
+});
+
 
-app.post('/signup', async (req: Request, res: Response) => {
+async function signupHandler(req: Request, res: Response) {
   const {username, password} = req.body;
   const authToken = req.headers['x-authtoken'];
 
   if(!username || !password || !authToken) {
-    res.sendStatus(400);
+    res.send(Alert.ErrorAlert('Invalid username/password'));
     return;
   }
 
-
   try {
     const player = await loadPlayer(authToken.toString());
     logger.log(`Attempted claim for ${player.username}`);
@@ -1150,49 +871,49 @@ app.post('/signup', async (req: Request, res: Response) => {
       username: username
     });
 
-    logger.log(`Player claimed ${player.username} => ${username}`);
+    logger.log(`${username} claimed ${player.username}`);
 
     io.emit('chat', broadcastMessage('server', `${player.username} is now ${username}`));
 
-    player.username = username;
-    player.account_type = 'auth';
-
-    res.json({
-      player
-    });
+    res.setHeader('hx-refresh', 'true');
+    res.sendStatus(200);
   }
   catch(e) {
     logger.log(e);
     if(e?.constraint === 'players_username_unique') {
-      res.send({
-        error: 'That username is already taken.'
-      }).status(500);
+      res.send(Alert.ErrorAlert('That username is already taken'));
     }
     else {
-      res.send({error: 'Please try again'}).status(500);
+      res.send(Alert.ErrorAlert('Please try again'));
     }
   }
-});
+}
 
-app.post('/login', async (req: Request, res: Response) => {
+async function loginHandler (req: Request, res: Response) {
   const {username, password} = req.body;
+  if(!username || !username.length) {
+    res.send(Alert.ErrorAlert('Invalid username'));
+    return;
+  }
+  if(!password || !password.length) {
+    res.send(Alert.ErrorAlert('Invalid password'));
+    return;
+  }
+
   try {
     const player = await login(username, password);
-    res.json({player});
+    io.sockets.sockets.forEach(socket => {
+      if(socket.handshake.headers['x-authtoken'] === req.headers['x-authtoken']) {
+        bootstrapSocket(socket, player);
+      }
+    });
+    res.sendStatus(204);
   }
   catch(e) {
     console.log(e);
-    res.json({error: 'That user doesnt exist'}).status(500);
+    res.send(Alert.ErrorAlert('That user doesn\'t exist'));
   }
-});
-
-app.get('/status', async (req: Request, res: Response) => {
-  res.send(`
-  <div id="server-stats" hx-trigger="load delay:15s" hx-get="/status" hx-swap="outerHTML">
-    ${io.sockets.sockets.size} Online (v${version})
-  </div>
-`);
-})
+}
 
 server.listen(process.env.API_PORT, () => {
   logger.log(`Listening on port ${process.env.API_PORT}`);