proc-gen equipment drops from monsters can be picked up
[sketchy-heroes.git] / src / routes / fight / fight.ts
1 import { server } from '../../lib/server';
2 import { Static, Type } from '@sinclair/typebox';
3 import { prisma } from '../../lib/db';
4 import { ForbiddenError } from '../../lib/http-errors';
5 import {Fight, LootTable, Player, WorldDrop} from '@prisma/client';
6 import {maxExp, maxHp} from '../../formulas';
7 import {weighted} from '../../lib/weighted';
8
9 const FightRoundInput = Type.Object({
10   params: Type.Object({
11     playerId: Type.String(),
12     fightId: Type.String()
13   }),
14   body: Type.Object({
15     action: Type.String()
16   })
17 });
18
19 type FightRoundInputType = Static<typeof FightRoundInput>;
20
21 export type RoundData = {
22   attacker: string;
23   defender: string;
24   damage: number;
25 }
26
27 export type FightRoundOutput = {
28   player: Player,
29   monster: Fight,
30   winner: 'monster' | 'player' | 'flee',
31   roundData: RoundData[],
32   reward: {
33     exp?: number,
34     currency?: number
35     worldDrop?: {
36       id: string,
37       name: string
38     }
39   }
40 }
41
42 function round(player: Player, monster: Fight): FightRoundOutput {
43
44   const roundData: RoundData[] = [];
45   if(player.stamina <= 0 || player.hp <= 0) {
46     return {
47       player: player,
48       monster: monster,
49       winner: 'monster',
50       roundData: roundData,
51       reward: {}
52     };
53   }
54
55   const first = player.woosh >= monster.woosh ? player : monster;
56   const second = player.woosh >= monster.woosh ? monster : player;
57
58
59   while(first.hp > 0 && second.hp > 0) {
60     // eventually these will include weapon and skill damage
61     const firstDamage = first.pow;
62     const secondDamage = second.pow;
63
64     second.hp -= firstDamage;
65     roundData.push({
66       attacker: first.id,
67       defender: second.id,
68       damage: firstDamage
69     });
70
71     if(second.hp > 0) {
72       first.hp -= secondDamage;
73       roundData.push({
74         attacker: second.id,
75         defender: first.id,
76         damage: secondDamage
77       })
78     }
79   }
80
81
82   // now we figure out who won!
83   if(player.hp <= 0) {
84     // the player lost
85     player.hp = 0;
86
87     return {
88       player: player,
89       monster: monster,
90       winner: 'monster',
91       roundData: roundData,
92       reward: {}
93     }
94   }
95   else {
96     return {
97       player: player,
98       monster: monster,
99       winner: 'player',
100       roundData: roundData,
101       reward: {
102         exp: monster.exp,
103         currency: monster.currency
104       }
105     }
106   }
107
108 }
109
110 export const fightRound = server.post<FightRoundInputType, FightRoundOutput>('/v1/accounts/:playerId/fight/:fightId/round', {
111   schema: FightRoundInput,
112   authenicated: true
113 }, async req => {
114
115   const [fight, player] = await prisma.$transaction([
116     prisma.fight.findUnique({
117       where: {
118         id: req.params.fightId
119       }
120     }),
121     prisma.player.findUnique({
122       where: {
123         id: req.params.playerId
124       },
125       include: {
126         zoneBiome: true
127       }
128     })
129   ]);
130
131   if(!player) {
132     throw new ForbiddenError('Not a valid fight');
133   }
134
135   if(!fight) {
136     throw new ForbiddenError('Not a real fight');
137   }
138
139   if(fight.playerId !== req.params.playerId) {
140     throw new ForbiddenError('Not a real fight');
141   }
142
143   if(req.body.action === 'Flee') {
144     await prisma.fight.delete({
145       where: {
146         id: req.params.fightId
147       }
148     });
149
150     return {
151       player: player,
152       monster: fight,
153       winner: 'flee',
154       reward: {},
155       roundData: []
156     }
157   }
158
159   if(req.body.action === 'Fight') {
160       const output = round(player, fight);
161       if(output.winner === 'monster') {
162         await prisma.$transaction([
163           prisma.fight.delete({
164             where: {
165               id: fight.id
166             }
167           }),
168           prisma.player.update({
169             where: {
170               id: player.id
171             },
172             data: {
173               hp: 0
174             }
175           })
176         ]);
177       }
178       else if(output.winner === 'player') {
179         player.currency += fight.currency;
180         player.exp += fight.exp;
181         while(player.exp >= maxExp(player.level)) {
182           player.exp -= maxExp(player.level);
183           player.level++;
184           player.statPoints++;
185           player.hp = maxHp(player.level, player.zest);
186         }
187
188         // we should figure out if you got an item!
189
190         const [monster, ,] = await prisma.$transaction([
191           prisma.monsterBiome.findUnique({
192             where: {
193               monsterId_biome: {
194                 monsterId: fight.monsterId,
195                 biome: player.zoneBiome.biome
196               }
197             },
198             include: {
199               lootTable: {
200                 include: {
201                   item: true
202                 }
203               }
204             }
205           }),
206           prisma.fight.delete({
207             where: {
208               id: fight.id
209             }
210           }),
211           prisma.player.update({
212             where: {
213               id: player.id
214             },
215             data: {
216               hp: player.hp,
217               exp: player.exp,
218               level: player.level,
219               currency: player.currency,
220               stamina: player.stamina--,
221               statPoints: player.statPoints
222             }
223           })
224         ]);
225
226         // since the player won, we can give them an item if they've earned it
227         if(monster && monster.lootTable.length > 0) {
228           const item = weighted(monster.lootTable, (i) => i.dropRate.toNumber());
229
230           if(item) {
231             // we can give the player this item, so lets place it in the "world-drop-table". 
232             // if a player doesn't pick it up, it will remain in the world drop table for 
233             // 24 hours.. after that it's gone forever.
234             // if the player does pick it up then it's moved to their inventory
235             const worldDrop = await prisma.worldDrop.create({
236               data: {
237                 itemId: item.itemId,
238                 droppedById: player.id
239               }
240             });
241
242             output.reward['worldDrop'] = {
243               id: worldDrop.id,
244               name: item.item.name
245             };
246           }
247         }
248
249         // update the player with the new data
250         output.player = player;
251       }
252
253       return output;
254   }
255
256   throw new ForbiddenError('You can not do that in a fight');
257
258 });