chore(release): 0.4.0
[risinglegends.git] / src / server / locations / dungeon.ts
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";
15
16 export const router = Router();
17
18 router.get('/city/dungeon/:dungeon_id/:location_id', authEndpoint, async (req, res) => {
19   let dungeon: Dungeon;
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));
25
26   if(service.type !== 'DUNGEON') {
27     logger.log(`Attempting to enter a non-dungeon`);
28     res.sendStatus(400);
29     return;
30   }
31
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}]`);
34     res.sendStatus(400);
35     return;
36   }
37
38   if(!activeDungeon) {
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);
43   }
44
45   const room = await loadRoom(activeDungeon.current_room_id);
46   const visits = await getRoomVists(req.player.id, service.event_name);
47
48   res.send(renderDungeon(service.city_name, service.name, room, visits));
49 });
50
51 router.post('/city/dungeon/step/:target_room_id', authEndpoint, async (req, res) => {
52   const activeDungeon = await getActiveDungeon(req.player.id);
53   if(!activeDungeon) {
54     logger.log(`Not in a dungeon`);
55     res.sendStatus(400);
56     return;
57   }
58
59   const dungeon = await loadDungeon(activeDungeon.dungeon_id);
60   const service = await getDungeon(dungeon.id);
61
62   const targetRoomId = req.params.target_room_id.toLowerCase().trim();
63   const currentRoom = await loadRoom(activeDungeon.current_room_id);
64
65   const targetExit = currentRoom.exits.filter(exit => { 
66     return exit.target_room_id === targetRoomId 
67   });
68
69   if(!targetExit.length) {
70     logger.log(`Invalid exit: [${targetRoomId}]`);
71     res.sendStatus(400);
72     return;
73   }
74
75   const nextRoom = await loadRoom(targetRoomId);
76
77   if(!nextRoom) {
78     logger.error(`Dang.. no valid room`, targetRoomId, currentRoom);
79     res.send(Alert.ErrorAlert(`${req.params.direction} is not a valid direction`)).status(400);
80     return;
81   }
82
83
84   // if the room contiains a fight and 
85   // its a one time fight the user hasn't finished
86   // OR
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);
91     if(
92       (
93         nextRoom.settings.fight.one_time &&
94         (
95           has(fights, [nextRoom.id, nextRoom.settings.fight.monster_id])
96             ? 
97             fights[nextRoom.id][nextRoom.settings.fight.monster_id]
98             :
99             0
100         ) === 0
101       )
102       || 
103       !nextRoom.settings.fight.one_time
104     ){
105         const monster = await loadMonster(nextRoom.settings.fight.monster_id);
106         const fight = await createFight(req.player.id, monster, 'dungeon-forced');
107
108         // ensure that we know what room the player was attempting to go 
109         // to
110         await updatePlayerDungeonState(req.player.id, currentRoom.dungeon_id, {
111           current_room_id: currentRoom.id,
112           target_room_id:  nextRoom.id
113         });
114
115         // ok render the fight view instead!
116         res.send(renderFightPreRoundDungeon(service.city_name, service.name, fight));
117         return;
118     }
119   }
120
121   await movePlayerToRoomInDungeon(req.player.id, nextRoom.dungeon_id, nextRoom.id);
122   const visits = await getRoomVists(req.player.id, service.event_name);
123
124   res.send(renderDungeon(service.city_name, service.name, nextRoom, visits));
125 });
126
127 router.post('/city/dungeon/:dungeon_id/complete', authEndpoint, async (req, res) => {
128   const activeDungeon = await getActiveDungeon(req.player.id);
129   if(!activeDungeon) {
130     logger.log(`Not in a dungeon`);
131     res.sendStatus(400);
132     return;
133   }
134
135   const dungeon = await loadDungeon(activeDungeon.dungeon_id);
136   const currentRoom = await loadRoom(activeDungeon.current_room_id);
137
138   if(!currentRoom.settings.end) {
139     logger.log(`Not the end of the dungeon: [${currentRoom.id}]`);
140     res.sendStatus(400);
141     return;
142   }
143
144   const stats = await getUniqueFights(req.player.id, dungeon.id);
145
146   const rewards: DungeonRewards = {
147     exp: max([currentRoom.settings.rewards?.base.exp, 0]),
148     gold: max([currentRoom.settings.rewards?.base.gold, 0])
149   };
150
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)
156         }
157         if(currentRoom.settings.rewards.per_kill_bonus.gold) {
158           rewards.gold += (count * currentRoom.settings.rewards.per_kill_bonus.gold)
159         }
160       });
161     });
162   }
163
164
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;
168
169   // give the user these rewards!
170   req.player.gold += Math.ceil(rewards.gold * factor);
171   req.player.exp += Math.ceil(rewards.exp * factor);
172
173   while(req.player.exp >=  expToLevel(req.player.level + 1)) {
174     req.player.exp -= expToLevel(req.player.level + 1);
175     req.player.level++;
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);
179   }
180
181   // delete the tracking for this dungeon-run
182   await completeDungeon(req.player.id);
183   await updatePlayer(req.player);
184
185   res.send(dungeonRewards(dungeon, rewards, completionsToday.length));
186 });