From: xangelo Date: Mon, 18 Dec 2023 06:11:13 +0000 (-0500) Subject: fix: migrate existing routers to new router folder X-Git-Url: https://git.xangelo.ca/?a=commitdiff_plain;h=b5185d916fff5422880ddaaf817ec4068c22879d;p=risinglegends.git fix: migrate existing routers to new router folder 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. --- diff --git a/src/server/api.ts b/src/server/api.ts index 75134f5..b63d65d 100644 --- a/src/server/api.ts +++ b/src/server/api.ts @@ -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 index 2b45096..0000000 --- a/src/server/locations/dungeon.ts +++ /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 index 3d7374b..0000000 --- a/src/server/locations/healer.ts +++ /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; - -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 = { - [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(`

"${getText('intro', service, city)}"

`); - - if(req.player.hp === maxHp(req.player.constitution, req.player.level) && req.player.vigor === maxVigor(req.player.constitution, req.player.level)) { - text.push(`

You're already in peak condition!

`); - } - else { - if(req.player.gold <= (healCost * 2)) { - text.push(`

You don't seem to have too much money... I guess I can do it for free this time...

`); - text.push(`

`); - } - else { - text.push(`

`); - } - - } - - res.send(` -
${service.city_name}
-
-

${service.name}

-
-${text.join("\n")} -${BackToTown()} -
-
- `); - - //res.send(`
${text.join("\n")}
`); -}); - - - -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(`

${getText('insufficient_money', service, city)}

`) - } - 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(`

${getText('heal_successful', service, city)}

`); - } - - res.send(` -
${service.city_name}
-
-

${service.name}

-
-${text.join("\n")} -${BackToTown()} -
-
-${renderPlayerBar(req.player)} -`); -}); diff --git a/src/server/locations/recruiter.ts b/src/server/locations/recruiter.ts deleted file mode 100644 index fa46860..0000000 --- a/src/server/locations/recruiter.ts +++ /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 `

${str}

`; -} - -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(`

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!

`); - html.push(`

You have 3 choices laid before you.

`); - html.push(`

You could become a great and mighty Warrior! Wielding powerful swords and maces.

`); - html.push(`

You could become a powerful Mage! Casting spells to rain fire upon our enemies.

`); - html.push(`

You could become a lithe Rogue! Attacking our enemies swiftly when they least expect!

`); - - 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(`Be Careful! Once you change your profession, you'll never be a Wanderer again...`)); - html.push(` -
-
- - - -
-
- `); - } - } - 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 ${req.player.profession}!`)); - html.push(`

Unfortunately I won't be of much help to you now that you are no longer a wanderer...

`); - html.push(`

However, you should visit the ${place} in ${town} that can probably provide some guidance!

`); - } - - html.push(BackToTown()); - res.send(` -
${service.city_name}
-
-

${service.name}

-
${html.join("\n")}
-
- `); -}); - -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) + `
Congrats! You are now a ${req.player.profession}
`); - } - -}); diff --git a/src/server/locations/repair.ts b/src/server/locations/repair.ts deleted file mode 100644 index 19d382c..0000000 --- a/src/server/locations/repair.ts +++ /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`) - ); -}); diff --git a/src/server/routes/index.ts b/src/server/routes/index.ts index b08846f..07fe33d 100644 --- a/src/server/routes/index.ts +++ b/src/server/routes/index.ts @@ -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 index 0000000..9355656 --- /dev/null +++ b/src/server/routes/locations/dungeon.ts @@ -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 index 0000000..7c20c6e --- /dev/null +++ b/src/server/routes/locations/healer.ts @@ -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; + +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 = { + [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(`

"${getText('intro', service, city)}"

`); + + if(req.player.hp === maxHp(req.player.constitution, req.player.level) && req.player.vigor === maxVigor(req.player.constitution, req.player.level)) { + text.push(`

You're already in peak condition!

`); + } + else { + if(req.player.gold <= (healCost * 2)) { + text.push(`

You don't seem to have too much money... I guess I can do it for free this time...

`); + text.push(`

`); + } + else { + text.push(`

`); + } + + } + + res.send(` +
${service.city_name}
+
+

${service.name}

+
+${text.join("\n")} +${BackToTown()} +
+
+ `); + + //res.send(`
${text.join("\n")}
`); +}); + + + +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(`

${getText('insufficient_money', service, city)}

`) + } + 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(`

${getText('heal_successful', service, city)}

`); + } + + res.send(` +
${service.city_name}
+
+

${service.name}

+
+${text.join("\n")} +${BackToTown()} +
+
+${renderPlayerBar(req.player)} +`); +}); diff --git a/src/server/routes/locations/recruiter.ts b/src/server/routes/locations/recruiter.ts new file mode 100644 index 0000000..73a3c1a --- /dev/null +++ b/src/server/routes/locations/recruiter.ts @@ -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 `

${str}

`; +} + +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(`

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!

`); + html.push(`

You have 3 choices laid before you.

`); + html.push(`

You could become a great and mighty Warrior! Wielding powerful swords and maces.

`); + html.push(`

You could become a powerful Mage! Casting spells to rain fire upon our enemies.

`); + html.push(`

You could become a lithe Rogue! Attacking our enemies swiftly when they least expect!

`); + + 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(`Be Careful! Once you change your profession, you'll never be a Wanderer again...`)); + html.push(` +
+
+ + + +
+
+ `); + } + } + 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 ${req.player.profession}!`)); + html.push(`

Unfortunately I won't be of much help to you now that you are no longer a wanderer...

`); + html.push(`

However, you should visit the ${place} in ${town} that can probably provide some guidance!

`); + } + + html.push(BackToTown()); + res.send(` +
${service.city_name}
+
+

${service.name}

+
${html.join("\n")}
+
+ `); +}); + +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) + `
Congrats! You are now a ${req.player.profession}
`); + } + +}); diff --git a/src/server/routes/locations/repair.ts b/src/server/routes/locations/repair.ts new file mode 100644 index 0000000..d457f5f --- /dev/null +++ b/src/server/routes/locations/repair.ts @@ -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 index 0000000..9dd1a80 --- /dev/null +++ b/src/server/routes/locations/stores.ts @@ -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 = ` + +
+
+ ${equipment.name} +
+
+ ${renderEquipmentDetails(equipment, req.player)} +
+
+
+ + +
+
+`; + + 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 = ` + +
+
+ ${item.name} +
+
+

${item.name}

+

${item.description}

+
+
+
+ + +
+
+`; + + 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 index b126fa8..0000000 --- a/src/server/routes/stores.ts +++ /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 = ` - -
-
- ${equipment.name} -
-
- ${renderEquipmentDetails(equipment, req.player)} -
-
-
- - -
-
-`; - - 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 = ` - -
-
- ${item.name} -
-
-

${item.name}

-

${item.description}

-
-
-
- - -
-
-`; - - 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/shared/constants.ts b/src/shared/constants.ts index 2ca98e8..352c019 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -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;