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';
});
-app.use(healerRouter);
-app.use(professionRouter);
-app.use(repairRouter);
-app.use(dungeonRouter);
each(Routers, router => {
app.use(router);
});
+++ /dev/null
-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));
-});
+++ /dev/null
-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)}
-`);
-});
+++ /dev/null
-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>`);
- }
-
-});
+++ /dev/null
-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`)
- );
-});
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';
--- /dev/null
+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));
+});
--- /dev/null
+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)}
+`);
+});
--- /dev/null
+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>`);
+ }
+
+});
--- /dev/null
+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`)
+ );
+});
--- /dev/null
+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);
+});
+++ /dev/null
-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);
-});
export const EVENT_FLUSH_INTERVAL = 10000;
export const EVENT_SECOND_BUCKET = 2;
+
+export const MIN_LEVEL_TO_CHANGE_PROFESSIONS = 25;