1 import { Router } from "express";
2 import { authEndpoint } from '../auth';
3 import { logger } from "../lib/logger";
4 import { getDungeon, getService } from '../map';
5 import { completeDungeon, getActiveDungeon, getRoomVists, getUniqueFights, loadDungeon, loadRoom, movePlayerToRoomInDungeon, putPlayerInDungeon, updatePlayerDungeonState } from '../dungeon';
6 import { Dungeon, DungeonRewards } from "../../shared/dungeon";
7 import { dungeonRewards, renderDungeon } from '../views/dungeons/room';
8 import * as Alert from '../views/alert';
9 import { createFight, loadMonster } from "../monster";
10 import { renderFightPreRoundDungeon } from "../views/fight";
11 import { has, max, each } from 'lodash';
12 import { expToLevel, maxHp, maxVigor } from "../../shared/player";
13 import { updatePlayer } from "../player";
14 import { getEventHistoryToday } from "../events";
16 export const router = Router();
18 router.get('/city/dungeon/:dungeon_id/:location_id', authEndpoint, async (req, res) => {
20 let activeDungeon = await getActiveDungeon(req.player.id);
21 // because of how we treat locations + dungeons, the "event_name" of a location
22 // is actually the uuid of the dungeon. How fancy
23 // in this case service.event_name === dungeon.id
24 const service = await getService(parseInt(req.params.location_id));
26 if(service.type !== 'DUNGEON') {
27 logger.log(`Attempting to enter a non-dungeon`);
32 if(service.city_id !== req.player.city_id) {
33 logger.log(`Player is not in the same place as the dungeon: [${req.params.location_id}]`);
39 // for a dungeon the "event_name" serves as a mapping between the
40 // airtable integer id that is used for the location and the ifid (interactive fiction id)
41 // that is generated by twine
42 activeDungeon = await putPlayerInDungeon(req.player.id, service.event_name);
45 const room = await loadRoom(activeDungeon.current_room_id);
46 const visits = await getRoomVists(req.player.id, service.event_name);
48 res.send(renderDungeon(service.city_name, service.name, room, visits));
51 router.post('/city/dungeon/step/:target_room_id', authEndpoint, async (req, res) => {
52 const activeDungeon = await getActiveDungeon(req.player.id);
54 logger.log(`Not in a dungeon`);
59 const dungeon = await loadDungeon(activeDungeon.dungeon_id);
60 const service = await getDungeon(dungeon.id);
62 const targetRoomId = req.params.target_room_id.toLowerCase().trim();
63 const currentRoom = await loadRoom(activeDungeon.current_room_id);
65 const targetExit = currentRoom.exits.filter(exit => {
66 return exit.target_room_id === targetRoomId
69 if(!targetExit.length) {
70 logger.log(`Invalid exit: [${targetRoomId}]`);
75 const nextRoom = await loadRoom(targetRoomId);
78 logger.error(`Dang.. no valid room`, targetRoomId, currentRoom);
79 res.send(Alert.ErrorAlert(`${req.params.direction} is not a valid direction`)).status(400);
84 // if the room contiains a fight and
85 // its a one time fight the user hasn't finished
87 // its not a one time fight
88 // render the fight screen
89 if(nextRoom.settings.fight) {
90 const fights = await getUniqueFights(req.player.id, nextRoom.dungeon_id);
93 nextRoom.settings.fight.one_time &&
95 has(fights, [nextRoom.id, nextRoom.settings.fight.monster_id])
97 fights[nextRoom.id][nextRoom.settings.fight.monster_id]
103 !nextRoom.settings.fight.one_time
105 const monster = await loadMonster(nextRoom.settings.fight.monster_id);
106 const fight = await createFight(req.player.id, monster, 'dungeon-forced');
108 // ensure that we know what room the player was attempting to go
110 await updatePlayerDungeonState(req.player.id, currentRoom.dungeon_id, {
111 current_room_id: currentRoom.id,
112 target_room_id: nextRoom.id
115 // ok render the fight view instead!
116 res.send(renderFightPreRoundDungeon(service.city_name, service.name, fight));
121 await movePlayerToRoomInDungeon(req.player.id, nextRoom.dungeon_id, nextRoom.id);
122 const visits = await getRoomVists(req.player.id, service.event_name);
124 res.send(renderDungeon(service.city_name, service.name, nextRoom, visits));
127 router.post('/city/dungeon/:dungeon_id/complete', authEndpoint, async (req, res) => {
128 const activeDungeon = await getActiveDungeon(req.player.id);
130 logger.log(`Not in a dungeon`);
135 const dungeon = await loadDungeon(activeDungeon.dungeon_id);
136 const currentRoom = await loadRoom(activeDungeon.current_room_id);
138 if(!currentRoom.settings.end) {
139 logger.log(`Not the end of the dungeon: [${currentRoom.id}]`);
144 const stats = await getUniqueFights(req.player.id, dungeon.id);
146 const rewards: DungeonRewards = {
147 exp: max([currentRoom.settings.rewards?.base.exp, 0]),
148 gold: max([currentRoom.settings.rewards?.base.gold, 0])
151 if(currentRoom.settings.rewards.per_kill_bonus) {
152 each(stats, (room) => {
153 each(room, (count) => {
154 if(currentRoom.settings.rewards.per_kill_bonus.exp) {
155 rewards.exp += (count * currentRoom.settings.rewards.per_kill_bonus.exp)
157 if(currentRoom.settings.rewards.per_kill_bonus.gold) {
158 rewards.gold += (count * currentRoom.settings.rewards.per_kill_bonus.gold)
165 // if this is not the first completion, lets give them diminishing returns
166 const completionsToday = await getEventHistoryToday(req.player.id, 'DUNGEON_COMPLETE');
167 let factor = completionsToday.length <= 5 ? 1 : 0.2;
169 // give the user these rewards!
170 req.player.gold += Math.ceil(rewards.gold * factor);
171 req.player.exp += Math.ceil(rewards.exp * factor);
173 while(req.player.exp >= expToLevel(req.player.level + 1)) {
174 req.player.exp -= expToLevel(req.player.level + 1);
176 req.player.stat_points += req.player.profession === 'Wanderer' ? 1 : 2;
177 req.player.hp = maxHp(req.player.constitution, req.player.level);
178 req.player.vigor = maxVigor(req.player.constitution, req.player.level);
181 // delete the tracking for this dungeon-run
182 await completeDungeon(req.player.id);
183 await updatePlayer(req.player);
185 res.send(dungeonRewards(dungeon, rewards, completionsToday.length));