fix: migrate existing routers to new router folder
authorxangelo <me@xangelo.ca>
Mon, 18 Dec 2023 06:11:13 +0000 (01:11 -0500)
committerxangelo <me@xangelo.ca>
Mon, 18 Dec 2023 06:11:13 +0000 (01:11 -0500)
There were some existing routers for dungeons, healers, recruiters, and
repair systems that were in their own folder. They've since been folded
into the main router configuration.

13 files changed:
src/server/api.ts
src/server/locations/dungeon.ts [deleted file]
src/server/locations/healer.ts [deleted file]
src/server/locations/recruiter.ts [deleted file]
src/server/locations/repair.ts [deleted file]
src/server/routes/index.ts
src/server/routes/locations/dungeon.ts [new file with mode: 0644]
src/server/routes/locations/healer.ts [new file with mode: 0644]
src/server/routes/locations/recruiter.ts [new file with mode: 0644]
src/server/routes/locations/repair.ts [new file with mode: 0644]
src/server/routes/locations/stores.ts [new file with mode: 0644]
src/server/routes/stores.ts [deleted file]
src/shared/constants.ts

index 75134f5cf9e98c2685490c13fc198d68460da35a..b63d65dd13ed0d08c6034ad59b9c899e1197f3d4 100644 (file)
@@ -25,10 +25,6 @@ import { getPlayerSkills} from './skills';
 
 import { fightRound } from './fight';
 
-import { router as healerRouter } from './locations/healer';
-import { router as professionRouter } from './locations/recruiter';
-import { router as repairRouter } from './locations/repair';
-import { router as dungeonRouter } from './locations/dungeon';
 import * as Routers from './routes';
 
 import * as Alert from './views/alert';
@@ -152,10 +148,6 @@ io.on('connection', async socket => {
 });
 
 
-app.use(healerRouter);
-app.use(professionRouter);
-app.use(repairRouter);
-app.use(dungeonRouter);
 each(Routers, router => {
   app.use(router);
 });
diff --git a/src/server/locations/dungeon.ts b/src/server/locations/dungeon.ts
deleted file mode 100644 (file)
index 2b45096..0000000
+++ /dev/null
@@ -1,186 +0,0 @@
-import { Router } from "express";
-import { authEndpoint } from '../auth';
-import { logger } from "../lib/logger";
-import { getDungeon, getService } from '../map';
-import { completeDungeon, getActiveDungeon, getRoomVists, getUniqueFights, loadDungeon, loadRoom, movePlayerToRoomInDungeon, putPlayerInDungeon, updatePlayerDungeonState } from '../dungeon';
-import { Dungeon, DungeonRewards } from "../../shared/dungeon";
-import { dungeonRewards, renderDungeon } from '../views/dungeons/room';
-import * as Alert from '../views/alert';
-import { createFight, loadMonster } from "../monster";
-import { renderFightPreRoundDungeon } from "../views/fight";
-import { has, max, each } from 'lodash';
-import { expToLevel, maxHp, maxVigor } from "../../shared/player";
-import { updatePlayer } from "../player";
-import { getEventHistoryToday } from "../events";
-
-export const router = Router();
-
-router.get('/city/dungeon/:dungeon_id/:location_id', authEndpoint, async (req, res) => {
-  let dungeon: Dungeon;
-  let activeDungeon = await getActiveDungeon(req.player.id);
-  // because of how we treat locations + dungeons, the "event_name" of a location 
-  // is actually the uuid of the dungeon. How fancy
-  // in this case service.event_name === dungeon.id
-  const service = await getService(parseInt(req.params.location_id));
-
-  if(service.type !== 'DUNGEON') {
-    logger.log(`Attempting to enter a non-dungeon`);
-    res.sendStatus(400);
-    return;
-  }
-
-  if(service.city_id !== req.player.city_id) {
-    logger.log(`Player is not in the same place as the dungeon: [${req.params.location_id}]`);
-    res.sendStatus(400);
-    return;
-  }
-
-  if(!activeDungeon) {
-    // for a dungeon the "event_name" serves as a mapping between the 
-    // airtable integer id that is used for the location and the ifid (interactive fiction id) 
-    // that is generated by twine
-    activeDungeon = await putPlayerInDungeon(req.player.id, service.event_name);
-  }
-
-  const room = await loadRoom(activeDungeon.current_room_id);
-  const visits = await getRoomVists(req.player.id, service.event_name);
-
-  res.send(renderDungeon(service.city_name, service.name, room, visits));
-});
-
-router.post('/city/dungeon/step/:target_room_id', authEndpoint, async (req, res) => {
-  const activeDungeon = await getActiveDungeon(req.player.id);
-  if(!activeDungeon) {
-    logger.log(`Not in a dungeon`);
-    res.sendStatus(400);
-    return;
-  }
-
-  const dungeon = await loadDungeon(activeDungeon.dungeon_id);
-  const service = await getDungeon(dungeon.id);
-
-  const targetRoomId = req.params.target_room_id.toLowerCase().trim();
-  const currentRoom = await loadRoom(activeDungeon.current_room_id);
-
-  const targetExit = currentRoom.exits.filter(exit => { 
-    return exit.target_room_id === targetRoomId 
-  });
-
-  if(!targetExit.length) {
-    logger.log(`Invalid exit: [${targetRoomId}]`);
-    res.sendStatus(400);
-    return;
-  }
-
-  const nextRoom = await loadRoom(targetRoomId);
-
-  if(!nextRoom) {
-    logger.error(`Dang.. no valid room`, targetRoomId, currentRoom);
-    res.send(Alert.ErrorAlert(`${req.params.direction} is not a valid direction`)).status(400);
-    return;
-  }
-
-
-  // if the room contiains a fight and 
-  // its a one time fight the user hasn't finished
-  // OR
-  // its not a one time fight
-  // render the fight screen
-  if(nextRoom.settings.fight) {
-    const fights = await getUniqueFights(req.player.id, nextRoom.dungeon_id);
-    if(
-      (
-        nextRoom.settings.fight.one_time &&
-        (
-          has(fights, [nextRoom.id, nextRoom.settings.fight.monster_id])
-            ? 
-            fights[nextRoom.id][nextRoom.settings.fight.monster_id]
-            :
-            0
-        ) === 0
-      )
-      || 
-      !nextRoom.settings.fight.one_time
-    ){
-        const monster = await loadMonster(nextRoom.settings.fight.monster_id);
-        const fight = await createFight(req.player.id, monster, 'dungeon-forced');
-
-        // ensure that we know what room the player was attempting to go 
-        // to
-        await updatePlayerDungeonState(req.player.id, currentRoom.dungeon_id, {
-          current_room_id: currentRoom.id,
-          target_room_id:  nextRoom.id
-        });
-
-        // ok render the fight view instead!
-        res.send(renderFightPreRoundDungeon(service.city_name, service.name, fight));
-        return;
-    }
-  }
-
-  await movePlayerToRoomInDungeon(req.player.id, nextRoom.dungeon_id, nextRoom.id);
-  const visits = await getRoomVists(req.player.id, service.event_name);
-
-  res.send(renderDungeon(service.city_name, service.name, nextRoom, visits));
-});
-
-router.post('/city/dungeon/:dungeon_id/complete', authEndpoint, async (req, res) => {
-  const activeDungeon = await getActiveDungeon(req.player.id);
-  if(!activeDungeon) {
-    logger.log(`Not in a dungeon`);
-    res.sendStatus(400);
-    return;
-  }
-
-  const dungeon = await loadDungeon(activeDungeon.dungeon_id);
-  const currentRoom = await loadRoom(activeDungeon.current_room_id);
-
-  if(!currentRoom.settings.end) {
-    logger.log(`Not the end of the dungeon: [${currentRoom.id}]`);
-    res.sendStatus(400);
-    return;
-  }
-
-  const stats = await getUniqueFights(req.player.id, dungeon.id);
-
-  const rewards: DungeonRewards = {
-    exp: max([currentRoom.settings.rewards?.base.exp, 0]),
-    gold: max([currentRoom.settings.rewards?.base.gold, 0])
-  };
-
-  if(currentRoom.settings.rewards.per_kill_bonus) {
-    each(stats, (room) => {
-      each(room, (count) => {
-        if(currentRoom.settings.rewards.per_kill_bonus.exp) {
-          rewards.exp += (count * currentRoom.settings.rewards.per_kill_bonus.exp)
-        }
-        if(currentRoom.settings.rewards.per_kill_bonus.gold) {
-          rewards.gold += (count * currentRoom.settings.rewards.per_kill_bonus.gold)
-        }
-      });
-    });
-  }
-
-  await completeDungeon(req.player.id);
-
-  // if this is not the first completion, lets give them diminishing returns
-  const completionsToday = await getEventHistoryToday(req.player.id, 'DUNGEON_COMPLETE');
-  let factor = completionsToday.length <= 5 ? 1 : 0.2;
-
-  // give the user these rewards!
-  req.player.gold += Math.ceil(rewards.gold * factor);
-  req.player.exp += Math.ceil(rewards.exp * factor);
-
-  while(req.player.exp >=  expToLevel(req.player.level + 1)) {
-    req.player.exp -= expToLevel(req.player.level + 1);
-    req.player.level++;
-    req.player.stat_points += req.player.profession === 'Wanderer' ? 1 : 2;
-    req.player.hp = maxHp(req.player.constitution, req.player.level);
-    req.player.vigor = maxVigor(req.player.constitution, req.player.level);
-  }
-
-  // delete the tracking for this dungeon-run
-  await updatePlayer(req.player);
-
-  res.send(dungeonRewards(dungeon, rewards, completionsToday.length));
-});
diff --git a/src/server/locations/healer.ts b/src/server/locations/healer.ts
deleted file mode 100644 (file)
index 3d7374b..0000000
+++ /dev/null
@@ -1,173 +0,0 @@
-import { Request, Response, Router } from "express";
-import { maxHp, maxVigor } from "../../shared/player";
-import { authEndpoint } from '../auth';
-import { logger } from "../lib/logger";
-import { updatePlayer } from "../player";
-import { getCityDetails, getService } from '../map';
-import { sample } from 'lodash';
-import { City, Location } from "../../shared/map";
-import { renderPlayerBar } from "../views/player-bar";
-import { BackToTown } from "../views/components/button";
-
-export const router = Router();
-
-type TextSegment = 'intro' | 'insufficient_money' | 'heal_successful';
-
-type HealText = Record<TextSegment, string[]>;
-
-const healCost = 10;
-
-const defaultTexts: HealText = {
-  intro: [
-    `Welcome traveller, I am {{NAME}}, Healer of {{CITY_NAME}}`,
-    "Please come in traveller, I am {{NAME}}, Healer of {{CITY_NAME}}",
-  ],
-  insufficient_money: [
-    "Sorry friend, you don't have enough money..",
-    "Sorry, that won't be enough..",
-    "Healing is hard work.. I'm afraid that won't cover it.."
-  ],
-  heal_successful: [
-    "I hope you feel better now",
-    "Good luck on your travels!",
-    "Glad to be of service..."
-  ]
-};
-
-// overrides for specific areas
-const playerTexts: Record<number, HealText> = {
-  [8]: {
-    intro: [
-      'Welcome to Midfield traveller, I am Casim - healer in these parts',
-      'I am Casim the Healer here... how are you enjoying your stay at Midfield?'
-    ],
-    insufficient_money: [
-      'Sorry friend, you don\'t have enough money',
-      'Look.. I\'m sorry.. that won\'t be enough...'
-    ],
-    heal_successful: [
-      'Glad to help!'
-    ]
-  },
-  [16]: {
-    intro: [
-      'Ah, welcome to Wildegard, one of the few safehavens in the Akari Woods. I am Adovras, healer in these parts.',
-      'Welcome traveller, I am Adovras - healer in these parts'
-    ],
-    insufficient_money: [
-      `Sorry friend, you don't have enough money...`
-    ],
-    heal_successful: [
-      "Hope this small healing will be helpful on your journeys"
-    ]
-
-  },
-  [11]: {
-    intro: [
-      'Ah, welcome traveler - I am Uthar, healer of Davelfell',
-      'Hello, I am Uthar, healer of Davelfell',
-      'Sorry I\'m a bit busy today, I am Uthar, healer of Davelfell'
-    ],
-    insufficient_money: [
-      "Bah, don't bother me if you don't have the money",
-      "Look, I'm very busy - come back when you have the money"
-    ],
-    heal_successful: [
-      "*Fizz* *POOF* YOU'RE HEALED!"
-    ]
-  }
-}
-
-function getText(type: TextSegment, location: Location, city: City): string {
-  let selected = sample(defaultTexts[type]);
-
-  if(playerTexts[location.id]) {
-    if(playerTexts[location.id][type].length) {
-      selected = sample(playerTexts[location.id][type]);
-    }
-  }
-
-  return selected.replace("{{NAME}}", location.name).replace("{{CITY_NAME}}", city.name);
-
-}
-
-router.get('/city/services/healer/:location_id', authEndpoint, async (req: Request, res: Response) => {
-  const service = await getService(parseInt(req.params.location_id));
-  const city = await getCityDetails(service.city_id);
-
-  if(!service || service.city_id !== req.player.city_id) {
-    logger.log(`Invalid location: [${req.params.location_id}]`);
-    res.sendStatus(400);
-  }
-
-  const text: string[] = [];
-
-  text.push(`<p>"${getText('intro', service, city)}"</p>`);
-
-  if(req.player.hp === maxHp(req.player.constitution, req.player.level) && req.player.vigor === maxVigor(req.player.constitution, req.player.level)) {
-    text.push(`<p>You're already in peak condition!</p>`);
-  }
-  else {
-    if(req.player.gold <= (healCost * 2)) {
-      text.push(`<p>You don't seem to have too much money... I guess I can do it for free this time...</p>`);
-      text.push(`<p><button type="button" hx-post="/city/services/healer/heal/${service.id}" hx-target="#explore">Heal for Free!</button></p>`);
-    }
-    else {
-      text.push(`<p><button type="button" hx-post="/city/services/healer/heal/${service.id}" hx-target="#explore">Heal for ${healCost}g!</button></p>`);
-    }
-
-  }
-
-  res.send(`
-<div class="city-title-wrapper"><div class="city-title">${service.city_name}</div></div>
-<div class="city-details">
-<h3 class="location-name"><span>${service.name}</span></h3>
-<div class="service-in-town">
-${text.join("\n")}
-${BackToTown()}
-</div>
-</div>
-  `);
-
-  //res.send(`<div class="service-in-town">${text.join("\n")}</div>`);
-});
-
-
-
-router.post('/city/services/healer/heal/:location_id', authEndpoint, async (req: Request, res: Response) => {
-  const service = await getService(parseInt(req.params.location_id));
-  const city = await getCityDetails(service.city_id);
-
-  if(!service || service.city_id !== req.player.city_id) {
-    logger.log(`Invalid location: [${req.params.location_id}]`);
-    res.sendStatus(400);
-  }
-
-  const text: string[] = [];
-  const cost = req.player.gold <= (healCost * 2) ? 0 : healCost;
-
-  if(req.player.gold < cost) {
-    text.push(`<p>${getText('insufficient_money', service, city)}</p>`)
-  }
-  else {
-    req.player.hp = maxHp(req.player.constitution, req.player.level);
-    req.player.vigor = maxVigor(req.player.constitution, req.player.level);
-    req.player.gold -= cost;
-
-    await updatePlayer(req.player);
-
-    text.push(`<p>${getText('heal_successful', service, city)}</p>`);
-  }
-
-  res.send(`
-<div class="city-title-wrapper"><div class="city-title">${service.city_name}</div></div>
-<div class="city-details">
-<h3 class="location-name"><span>${service.name}</span></h3>
-<div class="service-in-town">
-${text.join("\n")}
-${BackToTown()}
-</div>
-</div>
-${renderPlayerBar(req.player)}
-`);
-});
diff --git a/src/server/locations/recruiter.ts b/src/server/locations/recruiter.ts
deleted file mode 100644 (file)
index fa46860..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-import { Request, Response, Router } from "express";
-import { getService } from "../map";
-import { authEndpoint } from '../auth';
-import { logger } from "../lib/logger";
-import * as Alert from "../views/alert";
-import { changeProfession } from "../player";
-import { renderPlayerBar } from "../views/player-bar";
-import { BackToTown } from "../views/components/button";
-
-function p(str: string) {
-  return `<p>${str}</p>`;
-}
-
-export const router = Router();
-
-const MIN_LEVEL = 25;
-
-router.get('/city/services/profession_recruitor/:location_id', authEndpoint, async(req: Request, res: Response) => {
-  const service = await getService(parseInt(req.params.location_id));
-
-  if(!service || service.city_id !== req.player.city_id) {
-    logger.log(`Invalid location: [${req.params.location_id}]`);
-    res.sendStatus(400);
-  }
-
-  let html: string[] = [];
-  if(req.player.profession === 'Wanderer') {
-    html.push(`<p>Our duty is to help Wanderers such as yourself become more than they are. By helping you achieve new levels in service of the King, we can ensure that the Kingdom of Khatis continues to grow!</p>`);
-    html.push(`<p>You have 3 choices laid before you.</p>`);
-    html.push(`<p>You could become a great and mighty <b>Warrior</b>! Wielding powerful swords and maces.</p>`);
-    html.push(`<p>You could become a powerful <b>Mage</b>! Casting spells to rain fire upon our enemies.</p>`);
-    html.push(`<p>You could become a lithe <b>Rogue</b>! Attacking our enemies swiftly when they least expect!</p>`);
-
-    if(req.player.level < MIN_LEVEL) {
-      html.push(p(`Unfortunately you have to be at least level ${MIN_LEVEL} to take part in our training...`));
-    }
-    else {
-      html.push(p(`<b>Be Careful!</b> Once you change your profession, you'll never be a Wanderer again...`));
-      html.push(`
-          <div>
-          <form hx-post="/city/services/profession_change/${service.id}">
-          <button type="submit" value="warrior" name="profession">Become a Warrior</button>
-          <button type="submit" value="mage" name="profession">Become a Mage</button>
-          <button type="submit" value="rogue" name="profession">Become a Rogue</button>
-          </form>
-          </div>
-        `);
-    }
-  }
-  else {
-    let town = 'UNSET';
-    let place = 'UNSETPLACE';
-    switch(req.player.profession) {
-      case 'Warrior':
-        town = 'Stether';
-        place = 'Highbreaker Inn'
-        break;
-      case 'Mage':
-        town = 'Davelfell';
-        place = 'Mages Tower';
-        break;
-      case 'Rogue':
-        town = 'Ferbalt Gap';
-        place = 'Keepers Tavern';
-        break;
-    }
-
-    html.push(p(`Welcome <b>${req.player.profession}</b>!`));
-    html.push(`<p>Unfortunately I won't be of much help to you now that you are no longer a wanderer...</p>`);
-    html.push(`<p>However, you should visit the ${place} in ${town} that can probably provide some guidance!</p>`);
-  }
-
-  html.push(BackToTown());
-  res.send(`
-    <div class="city-title-wrapper"><div class="city-title">${service.city_name}</div></div>
-    <div class="city-details">
-      <h3 class="location-name"><span>${service.name}</span></h3>
-      <div class="service-in-town" id="recruiter-target">${html.join("\n")}</div>
-    </div>
-  `);
-});
-
-router.post('/city/services/profession_change/:location_id', authEndpoint, async(req: Request, res: Response) => {
-  const service = await getService(parseInt(req.params.location_id));
-
-  if(!service || service.city_id !== req.player.city_id) {
-    logger.log(`Invalid location: [${req.params.location_id}]`);
-    res.sendStatus(400);
-  }
-
-  let update: {level: number, exp: number};
-
-  switch(req.body.profession.toLowerCase()) {
-    case 'warrior':
-      update = await changeProfession(req.player.id, 'Warrior');
-      req.player.profession = 'Warrior';
-    break;
-    case 'mage':
-      update = await changeProfession(req.player.id, 'Mage');
-      req.player.profession = 'Mage';
-    break;
-    case 'rogue':
-      update = await changeProfession(req.player.id, 'Rogue');
-      req.player.profession = 'Rogue';
-    break;
-    default:
-      res.send(Alert.ErrorAlert(`Invalid profession`));
-    break;
-  }
-
-  if(update) {
-    req.player.level = update.level;
-    req.player.exp = update.exp;
-    res.send(renderPlayerBar(req.player) + `<div id="recruiter-target" class="service-in-town" hx-swap-oob="true">Congrats! You are now a ${req.player.profession}</div>`);
-  }
-
-});
diff --git a/src/server/locations/repair.ts b/src/server/locations/repair.ts
deleted file mode 100644 (file)
index 19d382c..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-import { Request, Response, Router } from "express";
-import { authEndpoint } from '../auth';
-import { logger } from "../lib/logger";
-import { getService } from "../map";
-import { getInventory, getInventoryItem, repair } from '../inventory';
-import { renderRepairService } from '../views/repair';
-import { repairCost } from "../../shared/inventory";
-import * as Alert from "../views/alert";
-import { updatePlayer } from "../player";
-import { renderPlayerBar } from "../views/player-bar";
-
-export const router = Router();
-
-router.get('/city/services/repair/:location_id', authEndpoint, async(req: Request, res: Response) => {
-  const service = await getService(parseInt(req.params.location_id));
-
-  if(!service || service.city_id !== req.player.city_id) {
-    logger.log(`Invalid location: [${req.params.location_id}]`);
-    res.sendStatus(400);
-  }
-
-  const equippedItems = await getInventory(req.player.id);
-
-  const damaged = equippedItems.filter(i => {
-    return i.currentAp < i.maxAp
-  });
-
-  res.send(renderRepairService(damaged, req.player, service));
-});
-
-router.post('/city/services/:location_id/repair/:item_id', authEndpoint, async (req: Request, res: Response) => {
-  const service = await getService(parseInt(req.params.location_id));
-
-  if(!service || service.city_id !== req.player.city_id) {
-    logger.log(`Invalid location: [${req.params.location_id}]`);
-    res.sendStatus(400);
-  }
-
-  const item = await getInventoryItem(req.player.id, req.params.item_id);
-
-  if(!item) {
-    logger.log(`Invalid item [${req.params.item_id}]`);
-    res.sendStatus(400);
-  }
-
-  const cost = repairCost(item);
-
-  if(req.player.gold < cost) {
-    res.status(400).send(Alert.ErrorAlert(`You need at least ${cost}G to repair your ${item.name}`));
-    return;
-  }
-
-  req.player.gold -= cost;
-  item.currentAp = item.maxAp;
-
-  await updatePlayer(req.player);
-  await repair(req.player.id, item.item_id);
-
-  const equippedItems = await getInventory(req.player.id);
-
-  const damaged = equippedItems.filter(i => {
-    return i.currentAp < i.maxAp
-  });
-
-  res.send(
-    renderRepairService(damaged, req.player, service) 
-      + renderPlayerBar(req.player)
-    + Alert.SuccessAlert(`You repaired your ${item.name} for ${cost}G`)
-  );
-});
index b08846f1719370991a9b5217644933a1b1008601..07fe33dc780be395c372a23cea75f567a9775392 100644 (file)
@@ -2,4 +2,8 @@ export { chatRouter } from './chat';
 export { inventoryRouter } from './inventory';
 export { profileRouter } from './profile';
 export { travelRouter } from './travel';
-export { storeRouter } from './stores';
+export { storeRouter } from './locations/stores';
+export { dungeonRouter } from './locations/dungeon';
+export { healerRouter } from './locations/healer';
+export { recruiterRouter } from './locations/recruiter';
+export { repairRouter } from './locations/repair';
diff --git a/src/server/routes/locations/dungeon.ts b/src/server/routes/locations/dungeon.ts
new file mode 100644 (file)
index 0000000..9355656
--- /dev/null
@@ -0,0 +1,186 @@
+import { Router } from "express";
+import { authEndpoint } from '../../auth';
+import { logger } from "../../lib/logger";
+import { getDungeon, getService } from '../../map';
+import { completeDungeon, getActiveDungeon, getRoomVists, getUniqueFights, loadDungeon, loadRoom, movePlayerToRoomInDungeon, putPlayerInDungeon, updatePlayerDungeonState } from '../../dungeon';
+import { Dungeon, DungeonRewards } from "../../../shared/dungeon";
+import { dungeonRewards, renderDungeon } from '../../views/dungeons/room';
+import * as Alert from '../../views/alert';
+import { createFight, loadMonster } from "../../monster";
+import { renderFightPreRoundDungeon } from "../../views/fight";
+import { has, max, each } from 'lodash';
+import { expToLevel, maxHp, maxVigor } from "../../../shared/player";
+import { updatePlayer } from "../../player";
+import { getEventHistoryToday } from "../../events";
+
+export const dungeonRouter = Router();
+
+dungeonRouter.get('/city/dungeon/:dungeon_id/:location_id', authEndpoint, async (req, res) => {
+  let dungeon: Dungeon;
+  let activeDungeon = await getActiveDungeon(req.player.id);
+  // because of how we treat locations + dungeons, the "event_name" of a location 
+  // is actually the uuid of the dungeon. How fancy
+  // in this case service.event_name === dungeon.id
+  const service = await getService(parseInt(req.params.location_id));
+
+  if(service.type !== 'DUNGEON') {
+    logger.log(`Attempting to enter a non-dungeon`);
+    res.sendStatus(400);
+    return;
+  }
+
+  if(service.city_id !== req.player.city_id) {
+    logger.log(`Player is not in the same place as the dungeon: [${req.params.location_id}]`);
+    res.sendStatus(400);
+    return;
+  }
+
+  if(!activeDungeon) {
+    // for a dungeon the "event_name" serves as a mapping between the 
+    // airtable integer id that is used for the location and the ifid (interactive fiction id) 
+    // that is generated by twine
+    activeDungeon = await putPlayerInDungeon(req.player.id, service.event_name);
+  }
+
+  const room = await loadRoom(activeDungeon.current_room_id);
+  const visits = await getRoomVists(req.player.id, service.event_name);
+
+  res.send(renderDungeon(service.city_name, service.name, room, visits));
+});
+
+dungeonRouter.post('/city/dungeon/step/:target_room_id', authEndpoint, async (req, res) => {
+  const activeDungeon = await getActiveDungeon(req.player.id);
+  if(!activeDungeon) {
+    logger.log(`Not in a dungeon`);
+    res.sendStatus(400);
+    return;
+  }
+
+  const dungeon = await loadDungeon(activeDungeon.dungeon_id);
+  const service = await getDungeon(dungeon.id);
+
+  const targetRoomId = req.params.target_room_id.toLowerCase().trim();
+  const currentRoom = await loadRoom(activeDungeon.current_room_id);
+
+  const targetExit = currentRoom.exits.filter(exit => { 
+    return exit.target_room_id === targetRoomId 
+  });
+
+  if(!targetExit.length) {
+    logger.log(`Invalid exit: [${targetRoomId}]`);
+    res.sendStatus(400);
+    return;
+  }
+
+  const nextRoom = await loadRoom(targetRoomId);
+
+  if(!nextRoom) {
+    logger.error(`Dang.. no valid room`, targetRoomId, currentRoom);
+    res.send(Alert.ErrorAlert(`${req.params.direction} is not a valid direction`)).status(400);
+    return;
+  }
+
+
+  // if the room contiains a fight and 
+  // its a one time fight the user hasn't finished
+  // OR
+  // its not a one time fight
+  // render the fight screen
+  if(nextRoom.settings.fight) {
+    const fights = await getUniqueFights(req.player.id, nextRoom.dungeon_id);
+    if(
+      (
+        nextRoom.settings.fight.one_time &&
+        (
+          has(fights, [nextRoom.id, nextRoom.settings.fight.monster_id])
+            ? 
+            fights[nextRoom.id][nextRoom.settings.fight.monster_id]
+            :
+            0
+        ) === 0
+      )
+      || 
+      !nextRoom.settings.fight.one_time
+    ){
+        const monster = await loadMonster(nextRoom.settings.fight.monster_id);
+        const fight = await createFight(req.player.id, monster, 'dungeon-forced');
+
+        // ensure that we know what room the player was attempting to go 
+        // to
+        await updatePlayerDungeonState(req.player.id, currentRoom.dungeon_id, {
+          current_room_id: currentRoom.id,
+          target_room_id:  nextRoom.id
+        });
+
+        // ok render the fight view instead!
+        res.send(renderFightPreRoundDungeon(service.city_name, service.name, fight));
+        return;
+    }
+  }
+
+  await movePlayerToRoomInDungeon(req.player.id, nextRoom.dungeon_id, nextRoom.id);
+  const visits = await getRoomVists(req.player.id, service.event_name);
+
+  res.send(renderDungeon(service.city_name, service.name, nextRoom, visits));
+});
+
+dungeonRouter.post('/city/dungeon/:dungeon_id/complete', authEndpoint, async (req, res) => {
+  const activeDungeon = await getActiveDungeon(req.player.id);
+  if(!activeDungeon) {
+    logger.log(`Not in a dungeon`);
+    res.sendStatus(400);
+    return;
+  }
+
+  const dungeon = await loadDungeon(activeDungeon.dungeon_id);
+  const currentRoom = await loadRoom(activeDungeon.current_room_id);
+
+  if(!currentRoom.settings.end) {
+    logger.log(`Not the end of the dungeon: [${currentRoom.id}]`);
+    res.sendStatus(400);
+    return;
+  }
+
+  const stats = await getUniqueFights(req.player.id, dungeon.id);
+
+  const rewards: DungeonRewards = {
+    exp: max([currentRoom.settings.rewards?.base.exp, 0]),
+    gold: max([currentRoom.settings.rewards?.base.gold, 0])
+  };
+
+  if(currentRoom.settings.rewards.per_kill_bonus) {
+    each(stats, (room) => {
+      each(room, (count) => {
+        if(currentRoom.settings.rewards.per_kill_bonus.exp) {
+          rewards.exp += (count * currentRoom.settings.rewards.per_kill_bonus.exp)
+        }
+        if(currentRoom.settings.rewards.per_kill_bonus.gold) {
+          rewards.gold += (count * currentRoom.settings.rewards.per_kill_bonus.gold)
+        }
+      });
+    });
+  }
+
+  await completeDungeon(req.player.id);
+
+  // if this is not the first completion, lets give them diminishing returns
+  const completionsToday = await getEventHistoryToday(req.player.id, 'DUNGEON_COMPLETE');
+  let factor = completionsToday.length <= 5 ? 1 : 0.2;
+
+  // give the user these rewards!
+  req.player.gold += Math.ceil(rewards.gold * factor);
+  req.player.exp += Math.ceil(rewards.exp * factor);
+
+  while(req.player.exp >=  expToLevel(req.player.level + 1)) {
+    req.player.exp -= expToLevel(req.player.level + 1);
+    req.player.level++;
+    req.player.stat_points += req.player.profession === 'Wanderer' ? 1 : 2;
+    req.player.hp = maxHp(req.player.constitution, req.player.level);
+    req.player.vigor = maxVigor(req.player.constitution, req.player.level);
+  }
+
+  // delete the tracking for this dungeon-run
+  await updatePlayer(req.player);
+
+  res.send(dungeonRewards(dungeon, rewards, completionsToday.length));
+});
diff --git a/src/server/routes/locations/healer.ts b/src/server/routes/locations/healer.ts
new file mode 100644 (file)
index 0000000..7c20c6e
--- /dev/null
@@ -0,0 +1,173 @@
+import { Request, Response, Router } from "express";
+import { maxHp, maxVigor } from "../../../shared/player";
+import { authEndpoint } from '../../auth';
+import { logger } from "../../lib/logger";
+import { updatePlayer } from "../../player";
+import { getCityDetails, getService } from '../../map';
+import { sample } from 'lodash';
+import { City, Location } from "../../../shared/map";
+import { renderPlayerBar } from "../../views/player-bar";
+import { BackToTown } from "../../views/components/button";
+
+export const healerRouter = Router();
+
+type TextSegment = 'intro' | 'insufficient_money' | 'heal_successful';
+
+type HealText = Record<TextSegment, string[]>;
+
+const healCost = 10;
+
+const defaultTexts: HealText = {
+  intro: [
+    `Welcome traveller, I am {{NAME}}, Healer of {{CITY_NAME}}`,
+    "Please come in traveller, I am {{NAME}}, Healer of {{CITY_NAME}}",
+  ],
+  insufficient_money: [
+    "Sorry friend, you don't have enough money..",
+    "Sorry, that won't be enough..",
+    "Healing is hard work.. I'm afraid that won't cover it.."
+  ],
+  heal_successful: [
+    "I hope you feel better now",
+    "Good luck on your travels!",
+    "Glad to be of service..."
+  ]
+};
+
+// overrides for specific areas
+const playerTexts: Record<number, HealText> = {
+  [8]: {
+    intro: [
+      'Welcome to Midfield traveller, I am Casim - healer in these parts',
+      'I am Casim the Healer here... how are you enjoying your stay at Midfield?'
+    ],
+    insufficient_money: [
+      'Sorry friend, you don\'t have enough money',
+      'Look.. I\'m sorry.. that won\'t be enough...'
+    ],
+    heal_successful: [
+      'Glad to help!'
+    ]
+  },
+  [16]: {
+    intro: [
+      'Ah, welcome to Wildegard, one of the few safehavens in the Akari Woods. I am Adovras, healer in these parts.',
+      'Welcome traveller, I am Adovras - healer in these parts'
+    ],
+    insufficient_money: [
+      `Sorry friend, you don't have enough money...`
+    ],
+    heal_successful: [
+      "Hope this small healing will be helpful on your journeys"
+    ]
+
+  },
+  [11]: {
+    intro: [
+      'Ah, welcome traveler - I am Uthar, healer of Davelfell',
+      'Hello, I am Uthar, healer of Davelfell',
+      'Sorry I\'m a bit busy today, I am Uthar, healer of Davelfell'
+    ],
+    insufficient_money: [
+      "Bah, don't bother me if you don't have the money",
+      "Look, I'm very busy - come back when you have the money"
+    ],
+    heal_successful: [
+      "*Fizz* *POOF* YOU'RE HEALED!"
+    ]
+  }
+}
+
+function getText(type: TextSegment, location: Location, city: City): string {
+  let selected = sample(defaultTexts[type]);
+
+  if(playerTexts[location.id]) {
+    if(playerTexts[location.id][type].length) {
+      selected = sample(playerTexts[location.id][type]);
+    }
+  }
+
+  return selected.replace("{{NAME}}", location.name).replace("{{CITY_NAME}}", city.name);
+
+}
+
+healerRouter.get('/city/services/healer/:location_id', authEndpoint, async (req: Request, res: Response) => {
+  const service = await getService(parseInt(req.params.location_id));
+  const city = await getCityDetails(service.city_id);
+
+  if(!service || service.city_id !== req.player.city_id) {
+    logger.log(`Invalid location: [${req.params.location_id}]`);
+    res.sendStatus(400);
+  }
+
+  const text: string[] = [];
+
+  text.push(`<p>"${getText('intro', service, city)}"</p>`);
+
+  if(req.player.hp === maxHp(req.player.constitution, req.player.level) && req.player.vigor === maxVigor(req.player.constitution, req.player.level)) {
+    text.push(`<p>You're already in peak condition!</p>`);
+  }
+  else {
+    if(req.player.gold <= (healCost * 2)) {
+      text.push(`<p>You don't seem to have too much money... I guess I can do it for free this time...</p>`);
+      text.push(`<p><button type="button" hx-post="/city/services/healer/heal/${service.id}" hx-target="#explore">Heal for Free!</button></p>`);
+    }
+    else {
+      text.push(`<p><button type="button" hx-post="/city/services/healer/heal/${service.id}" hx-target="#explore">Heal for ${healCost}g!</button></p>`);
+    }
+
+  }
+
+  res.send(`
+<div class="city-title-wrapper"><div class="city-title">${service.city_name}</div></div>
+<div class="city-details">
+<h3 class="location-name"><span>${service.name}</span></h3>
+<div class="service-in-town">
+${text.join("\n")}
+${BackToTown()}
+</div>
+</div>
+  `);
+
+  //res.send(`<div class="service-in-town">${text.join("\n")}</div>`);
+});
+
+
+
+healerRouter.post('/city/services/healer/heal/:location_id', authEndpoint, async (req: Request, res: Response) => {
+  const service = await getService(parseInt(req.params.location_id));
+  const city = await getCityDetails(service.city_id);
+
+  if(!service || service.city_id !== req.player.city_id) {
+    logger.log(`Invalid location: [${req.params.location_id}]`);
+    res.sendStatus(400);
+  }
+
+  const text: string[] = [];
+  const cost = req.player.gold <= (healCost * 2) ? 0 : healCost;
+
+  if(req.player.gold < cost) {
+    text.push(`<p>${getText('insufficient_money', service, city)}</p>`)
+  }
+  else {
+    req.player.hp = maxHp(req.player.constitution, req.player.level);
+    req.player.vigor = maxVigor(req.player.constitution, req.player.level);
+    req.player.gold -= cost;
+
+    await updatePlayer(req.player);
+
+    text.push(`<p>${getText('heal_successful', service, city)}</p>`);
+  }
+
+  res.send(`
+<div class="city-title-wrapper"><div class="city-title">${service.city_name}</div></div>
+<div class="city-details">
+<h3 class="location-name"><span>${service.name}</span></h3>
+<div class="service-in-town">
+${text.join("\n")}
+${BackToTown()}
+</div>
+</div>
+${renderPlayerBar(req.player)}
+`);
+});
diff --git a/src/server/routes/locations/recruiter.ts b/src/server/routes/locations/recruiter.ts
new file mode 100644 (file)
index 0000000..73a3c1a
--- /dev/null
@@ -0,0 +1,116 @@
+import { Request, Response, Router } from "express";
+import { getService } from "../../map";
+import { authEndpoint } from '../../auth';
+import { logger } from "../../lib/logger";
+import * as Alert from "../../views/alert";
+import { changeProfession } from "../../player";
+import { renderPlayerBar } from "../../views/player-bar";
+import { BackToTown } from "../../views/components/button";
+import { MIN_LEVEL_TO_CHANGE_PROFESSIONS } from "../../../shared/constants";
+
+function p(str: string) {
+  return `<p>${str}</p>`;
+}
+
+export const recruiterRouter = Router();
+
+recruiterRouter.get('/city/services/profession_recruitor/:location_id', authEndpoint, async(req: Request, res: Response) => {
+  const service = await getService(parseInt(req.params.location_id));
+
+  if(!service || service.city_id !== req.player.city_id) {
+    logger.log(`Invalid location: [${req.params.location_id}]`);
+    res.sendStatus(400);
+  }
+
+  let html: string[] = [];
+  if(req.player.profession === 'Wanderer') {
+    html.push(`<p>Our duty is to help Wanderers such as yourself become more than they are. By helping you achieve new levels in service of the King, we can ensure that the Kingdom of Khatis continues to grow!</p>`);
+    html.push(`<p>You have 3 choices laid before you.</p>`);
+    html.push(`<p>You could become a great and mighty <b>Warrior</b>! Wielding powerful swords and maces.</p>`);
+    html.push(`<p>You could become a powerful <b>Mage</b>! Casting spells to rain fire upon our enemies.</p>`);
+    html.push(`<p>You could become a lithe <b>Rogue</b>! Attacking our enemies swiftly when they least expect!</p>`);
+
+    if(req.player.level < MIN_LEVEL_TO_CHANGE_PROFESSIONS) {
+      html.push(p(`Unfortunately you have to be at least level ${MIN_LEVEL_TO_CHANGE_PROFESSIONS} to take part in our training...`));
+    }
+    else {
+      html.push(p(`<b>Be Careful!</b> Once you change your profession, you'll never be a Wanderer again...`));
+      html.push(`
+          <div>
+          <form hx-post="/city/services/profession_change/${service.id}">
+          <button type="submit" value="warrior" name="profession">Become a Warrior</button>
+          <button type="submit" value="mage" name="profession">Become a Mage</button>
+          <button type="submit" value="rogue" name="profession">Become a Rogue</button>
+          </form>
+          </div>
+        `);
+    }
+  }
+  else {
+    let town = 'UNSET';
+    let place = 'UNSETPLACE';
+    switch(req.player.profession) {
+      case 'Warrior':
+        town = 'Stether';
+        place = 'Highbreaker Inn'
+        break;
+      case 'Mage':
+        town = 'Davelfell';
+        place = 'Mages Tower';
+        break;
+      case 'Rogue':
+        town = 'Ferbalt Gap';
+        place = 'Keepers Tavern';
+        break;
+    }
+
+    html.push(p(`Welcome <b>${req.player.profession}</b>!`));
+    html.push(`<p>Unfortunately I won't be of much help to you now that you are no longer a wanderer...</p>`);
+    html.push(`<p>However, you should visit the ${place} in ${town} that can probably provide some guidance!</p>`);
+  }
+
+  html.push(BackToTown());
+  res.send(`
+    <div class="city-title-wrapper"><div class="city-title">${service.city_name}</div></div>
+    <div class="city-details">
+      <h3 class="location-name"><span>${service.name}</span></h3>
+      <div class="service-in-town" id="recruiter-target">${html.join("\n")}</div>
+    </div>
+  `);
+});
+
+recruiterRouter.post('/city/services/profession_change/:location_id', authEndpoint, async(req: Request, res: Response) => {
+  const service = await getService(parseInt(req.params.location_id));
+
+  if(!service || service.city_id !== req.player.city_id) {
+    logger.log(`Invalid location: [${req.params.location_id}]`);
+    res.sendStatus(400);
+  }
+
+  let update: {level: number, exp: number};
+
+  switch(req.body.profession.toLowerCase()) {
+    case 'warrior':
+      update = await changeProfession(req.player.id, 'Warrior');
+      req.player.profession = 'Warrior';
+    break;
+    case 'mage':
+      update = await changeProfession(req.player.id, 'Mage');
+      req.player.profession = 'Mage';
+    break;
+    case 'rogue':
+      update = await changeProfession(req.player.id, 'Rogue');
+      req.player.profession = 'Rogue';
+    break;
+    default:
+      res.send(Alert.ErrorAlert(`Invalid profession`));
+    break;
+  }
+
+  if(update) {
+    req.player.level = update.level;
+    req.player.exp = update.exp;
+    res.send(renderPlayerBar(req.player) + `<div id="recruiter-target" class="service-in-town" hx-swap-oob="true">Congrats! You are now a ${req.player.profession}</div>`);
+  }
+
+});
diff --git a/src/server/routes/locations/repair.ts b/src/server/routes/locations/repair.ts
new file mode 100644 (file)
index 0000000..d457f5f
--- /dev/null
@@ -0,0 +1,70 @@
+import { Request, Response, Router } from "express";
+import { authEndpoint } from '../../auth';
+import { logger } from "../../lib/logger";
+import { getService } from "../../map";
+import { getInventory, getInventoryItem, repair } from '../../inventory';
+import { renderRepairService } from '../../views/repair';
+import { repairCost } from "../../../shared/inventory";
+import * as Alert from "../../views/alert";
+import { updatePlayer } from "../../player";
+import { renderPlayerBar } from "../../views/player-bar";
+
+export const repairRouter = Router();
+
+repairRouter.get('/city/services/repair/:location_id', authEndpoint, async(req: Request, res: Response) => {
+  const service = await getService(parseInt(req.params.location_id));
+
+  if(!service || service.city_id !== req.player.city_id) {
+    logger.log(`Invalid location: [${req.params.location_id}]`);
+    res.sendStatus(400);
+  }
+
+  const equippedItems = await getInventory(req.player.id);
+
+  const damaged = equippedItems.filter(i => {
+    return i.currentAp < i.maxAp
+  });
+
+  res.send(renderRepairService(damaged, req.player, service));
+});
+
+repairRouter.post('/city/services/:location_id/repair/:item_id', authEndpoint, async (req: Request, res: Response) => {
+  const service = await getService(parseInt(req.params.location_id));
+
+  if(!service || service.city_id !== req.player.city_id) {
+    logger.log(`Invalid location: [${req.params.location_id}]`);
+    res.sendStatus(400);
+  }
+
+  const item = await getInventoryItem(req.player.id, req.params.item_id);
+
+  if(!item) {
+    logger.log(`Invalid item [${req.params.item_id}]`);
+    res.sendStatus(400);
+  }
+
+  const cost = repairCost(item);
+
+  if(req.player.gold < cost) {
+    res.status(400).send(Alert.ErrorAlert(`You need at least ${cost}G to repair your ${item.name}`));
+    return;
+  }
+
+  req.player.gold -= cost;
+  item.currentAp = item.maxAp;
+
+  await updatePlayer(req.player);
+  await repair(req.player.id, item.item_id);
+
+  const equippedItems = await getInventory(req.player.id);
+
+  const damaged = equippedItems.filter(i => {
+    return i.currentAp < i.maxAp
+  });
+
+  res.send(
+    renderRepairService(damaged, req.player, service) 
+      + renderPlayerBar(req.player)
+    + Alert.SuccessAlert(`You repaired your ${item.name} for ${cost}G`)
+  );
+});
diff --git a/src/server/routes/locations/stores.ts b/src/server/routes/locations/stores.ts
new file mode 100644 (file)
index 0000000..9dd1a80
--- /dev/null
@@ -0,0 +1,114 @@
+import { Request, Response, Router } from 'express';
+import { logger } from '../../lib/logger';
+import { authEndpoint } from '../../auth';
+import { getShopEquipment, listShopItems } from '../../shopEquipment';
+import { updatePlayer } from '../../player';
+import { getService } from '../../map';
+import { getItemFromShop, getShopItems, givePlayerItem } from '../../items';
+import { Item, ShopItem } from '../../../shared/items';
+import { renderPlayerBar } from '../../views/player-bar';
+import * as Alert from '../../views/alert';
+import { renderEquipmentDetails, renderStore } from '../../views/stores';
+
+export const storeRouter = Router();
+// used to purchase items from a particular shop
+storeRouter.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) {
+    logger.log(`Invalid item [${req.params.item_id}]`);
+    return res.sendStatus(400);
+  }
+
+  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;
+  }
+
+  req.player.gold -= item.price_per_unit;
+
+  await updatePlayer(req.player);
+  await givePlayerItem(req.player.id, item.id, 1);
+
+  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
+storeRouter.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) {
+    logger.log(`Invalid equipment [${req.params.item_id}]`);
+    return res.sendStatus(400);
+  }
+
+  let html = `
+<dialog>
+  <div class="item-modal-overview">
+    <div class="icon">
+      <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, req.player)}
+    </div>
+  </div>
+  <div class="actions">
+    <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>
+`;
+
+  res.send(html);
+});
+
+// used to display item modals in a store, validates that 
+// the item is actually in this store before displaying 
+// the modal
+storeRouter.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) {
+    logger.log(`Invalid item [${req.params.item_id}]`);
+    return res.sendStatus(400);
+  }
+
+  let html = `
+<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="/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>
+`;
+
+  res.send(html);
+});
+
+storeRouter.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) {
+    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),
+  ]);
+
+  const html = await renderStore(shopEquipment, shopItems, req.player, location);
+
+  res.send(html);
+});
diff --git a/src/server/routes/stores.ts b/src/server/routes/stores.ts
deleted file mode 100644 (file)
index b126fa8..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-import { Request, Response, Router } from 'express';
-import { logger } from '../lib/logger';
-import { authEndpoint } from '../auth';
-import { getShopEquipment, listShopItems } from '../shopEquipment';
-import { updatePlayer } from '../player';
-import { getService } from '../map';
-import { getItemFromShop, getShopItems, givePlayerItem } from '../items';
-import { Item, ShopItem } from '../../shared/items';
-import { renderPlayerBar } from '../views/player-bar';
-import * as Alert from '../views/alert';
-import { renderEquipmentDetails, renderStore } from '../views/stores';
-
-export const storeRouter = Router();
-// used to purchase items from a particular shop
-storeRouter.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) {
-    logger.log(`Invalid item [${req.params.item_id}]`);
-    return res.sendStatus(400);
-  }
-
-  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;
-  }
-
-  req.player.gold -= item.price_per_unit;
-
-  await updatePlayer(req.player);
-  await givePlayerItem(req.player.id, item.id, 1);
-
-  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
-storeRouter.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) {
-    logger.log(`Invalid equipment [${req.params.item_id}]`);
-    return res.sendStatus(400);
-  }
-
-  let html = `
-<dialog>
-  <div class="item-modal-overview">
-    <div class="icon">
-      <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, req.player)}
-    </div>
-  </div>
-  <div class="actions">
-    <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>
-`;
-
-  res.send(html);
-});
-
-// used to display item modals in a store, validates that 
-// the item is actually in this store before displaying 
-// the modal
-storeRouter.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) {
-    logger.log(`Invalid item [${req.params.item_id}]`);
-    return res.sendStatus(400);
-  }
-
-  let html = `
-<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="/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>
-`;
-
-  res.send(html);
-});
-
-storeRouter.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) {
-    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),
-  ]);
-
-  const html = await renderStore(shopEquipment, shopItems, req.player, location);
-
-  res.send(html);
-});
index 2ca98e8f6fce9318daef9ecc877c264d24bfe587..352c019eff7383f806efff3e6a2d5fcce9d51e4b 100644 (file)
@@ -8,3 +8,5 @@ export const DUNGEON_TRAVEL_BLOCK = 3000;
 
 export const EVENT_FLUSH_INTERVAL = 10000;
 export const EVENT_SECOND_BUCKET = 2;
+
+export const MIN_LEVEL_TO_CHANGE_PROFESSIONS = 25;