exploring/fighting is functional
[sketchy-heroes.git] / src / routes / move.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 { random, sample } from 'lodash';
6 import {Biome, Player, Fight} from '@prisma/client';
7 import {now} from '../lib/time';
8 import {generateMonster} from '../services/generate-monster';
9
10 type RawMonsterSelection = {
11   monsterId: string;
12   biome: Biome,
13   time: {min: number, max:number}[]
14 }
15
16 const MoveInput = Type.Object({
17   params: Type.Object({
18     playerId: Type.String()
19   })
20 });
21
22 type MoveInputType = Static<typeof MoveInput>;
23
24 export type MoveOutputType = {
25   player: Player,
26   biome: {
27     id: string;
28     biome: Biome
29   },
30   displayText: string,
31   generatedMonster: Fight | null
32 }
33
34 const moveStatements = [
35   "You walk forward",
36   "You take a few steps",
37   "You pause for a second before hurrying along",
38   "You stumble forward..."
39 ];
40
41 export const move = server.post<MoveInputType, MoveOutputType>('/v1/accounts/:playerId/move', {
42   schema: MoveInput,
43   authenicated: true
44 }, async req => {
45
46   let generatedMonster: Fight | null = null;
47
48   const player = await prisma.player.findUnique({
49     where: {
50       id: req.params.playerId
51     },
52     include: {
53       zoneBiome: true
54     }
55   });
56
57   if(!player) {
58     throw new ForbiddenError();
59   }
60
61   if(player.stamina <= 0) {
62     throw new ForbiddenError('Insufficient Stamina');
63   }
64
65
66   player.stamina--;
67   player.steps++;
68
69   if(player.steps > player.zoneBiome.maxSteps) {
70     // ok, move them to a new zone!
71     const chosenBiome = sample(Object.values(Biome)) as Biome;
72     const biome = await prisma.zoneBiomes.create({
73       data: {
74         biome: chosenBiome,
75         maxSteps: random(500, 1500)
76       }
77     });
78
79     await prisma.zoneBiomes.delete({
80       where: {
81         id: player.zoneBiomeId
82       }
83     });
84
85     player.zoneBiome = biome;
86     player.steps = 0;
87   }
88
89
90   await prisma.player.update({
91     where: {
92       id: player.id
93     },
94     data: {
95       steps: player.steps,
96       stamina: player.stamina
97     }
98   });
99
100
101   // now that you have moved, you have 40% chance of fighting a monster
102   if(random(0, 100) < 20) {
103     // ok it's battle time!
104     // retrieve all monsters from the database that are in this biome
105     // that are within the timeframe specified!
106     const monsters = await prisma.$queryRaw`
107     WITH p AS ( -- probability
108                SELECT *,
109                weight::NUMERIC / sum(weight) OVER () AS probability
110                FROM "MonsterBiome"
111                WHERE biome = ${player.zoneBiome.biome}
112               ),
113               cp AS ( -- cumulative probability
114                      SELECT *,
115                      sum(p.probability) OVER (
116                        ORDER BY probability DESC
117                        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
118                      ) AS cumprobability
119                      FROM p
120                     ),
121               fp AS (
122                   SELECT
123         cp.*,
124         cp.cumprobability - cp.probability AS startprobability,
125         cp.cumprobability AS endprobability
126     FROM cp
127               )
128
129                     SELECT "monsterId", biome, time FROM fp
130 WHERE random() BETWEEN startprobability AND endprobability;
131     ` as RawMonsterSelection[];
132
133     // now lets filter out the timezones!
134     const moment = now();
135     const availableMonsters = monsters.filter(monster => {
136       return monster.time.filter(time => {
137         return time.min <= moment && time.max >= moment;
138       });
139     })
140
141     if(availableMonsters) {
142       const selectedMonster = sample(availableMonsters);
143       if(selectedMonster) {
144         const monster = await prisma.monster.findUnique({
145           where: {
146             id: selectedMonster.monsterId
147           }
148         });
149
150         if(monster) {
151           generatedMonster = await prisma.fight.create({
152             data: generateMonster(player, monster)
153           });
154         }
155       }
156     }
157
158   }
159
160
161   return {
162     player,
163     biome: {
164       id: player.zoneBiomeId,
165       biome: player.zoneBiome.biome
166     },
167     generatedMonster,
168     displayText: sample(moveStatements) || moveStatements[0]
169   };
170 });