545fd1b7f6921255006c4886f03f8af47a2b99a8
[sketchy-heroes.git] / src / public / app / api.ts
1 import {Player} from '@prisma/client';
2 import axios from 'axios';
3 import { Events } from './events';
4 import { LoginOutputType, AccountCreateType, MoveOutputType, FightStartType, FightRoundOutput } from 'src/routes';
5 import {actionLog} from './components';
6 import {disableFightButton, disablePickItemButton, enableFightButton, enablePickItemButton} from './dom';
7 import {pickWorldDropOutput} from 'src/routes/inventory';
8
9 type ApiResponse<T> = {
10   status: 'ok' | 'error',
11   statusCode: number
12   meta: {
13     gameTime: number;
14     id: string;
15     processingTime: number
16   },
17   payload: T
18 }
19
20 export class Api extends Events {
21   base: string;
22   headers: Record<string, string>;
23   player: Player | null;
24
25   constructor(root: string) {
26     super();
27     this.base = root;
28
29     // we use the headers for auth data
30     this.headers = {};
31     this.player = null;
32   }
33
34   setPlayer(player: Player) {
35     this.player = player;
36     this.emit('player', player);
37   }
38
39
40   async get<T>(endpoint: string, params: Record<string, string> = {}): Promise<ApiResponse<T>> {
41     const res = await axios({
42       method: 'get',
43       url: endpoint,
44       baseURL: this.base,
45       params: params,
46       headers: this.headers
47     });
48
49     if(res.data.status !== 'ok') {
50       throw new Error(res.data.payload.toString());
51     }
52
53     return res.data;
54   }
55
56   async post<T>(endpoint: string, data: any = {}): Promise<ApiResponse<T>> {
57     const res = await axios({
58       method: 'post',
59       url: endpoint,
60       baseURL: this.base,
61       data: data,
62       headers: this.headers
63     });
64
65     if(res.data.status !== 'ok') {
66       throw new Error(res.data.payload.toString());
67     }
68
69     return res.data;
70   }
71
72   async signup(username: string, password: string, confirmation: string): Promise<AccountCreateType> {
73     const res = await this.post<AccountCreateType>('/v1/accounts', {
74       username: username,
75       password: password,
76       confirmation: confirmation
77     });
78
79     return res.payload;
80   }
81
82   async setGameTime(gameTime: number) {
83     const str = gameTime < 10 ? `0${gameTime}` : gameTime.toString();
84
85     $('body').removeClass().addClass(`sky-gradient-${str}`);
86   }
87
88   async syncPlayerInfo() {
89     if(!this.player) {
90       throw new Error('Not authenticated');
91     }
92     const res = await this.get<Player>(`/v1/accounts/${this.player.id}`);
93
94     this.setPlayer(res.payload);
95
96     // this also sets the background based on the time!
97     this.setGameTime(res.meta.gameTime);
98
99     setTimeout(this.syncPlayerInfo.bind(this), 10000);
100   }
101
102   async login(username: string, password: string): Promise<LoginOutputType> {
103     const res = await this.post<LoginOutputType>('/v1/accounts/auth', { 
104       username: username,
105       password: password
106     });
107
108     this.headers['x-auth-token'] = res.payload.token;
109
110     this.setPlayer(res.payload.player);
111
112     this.setGameTime(res.meta.gameTime);
113     
114
115     this.syncPlayerInfo();
116
117     return res.payload;
118   }
119
120   async move(): Promise<MoveOutputType> {
121     if(this.player === null) {
122       throw new Error('Not authenicated');
123     }
124     const res = await this.post<MoveOutputType>(`/v1/accounts/${this.player.id}/move`);
125
126     this.setPlayer(res.payload.player);
127
128     if(res.payload.generatedMonster !== null) {
129       actionLog(`You see a ${res.payload.generatedMonster.name}`);
130       if(this.player.hp > 0 && this.player.stamina > 0) {
131         enableFightButton({fightId: res.payload.generatedMonster.id});
132       }
133     }
134     else {
135       actionLog(res.payload.displayText, true);
136       disableFightButton();
137     }
138
139     return res.payload;
140   }
141
142   async increaseStat(stat: string): Promise<Player> {
143     if(this.player === null) {
144       throw new Error('Not authenticated');
145     }
146     const res = await this.post<Player>(`/v1/accounts/${this.player.id}/stat`, {
147       stat: stat
148     });
149
150     this.setPlayer(res.payload);
151     return res.payload;
152   }
153
154   async startFight(fightId: string): Promise<FightStartType> {
155     if(this.player === null) {
156       throw new Error('Not authenticated');
157     }
158
159     const res = await this.post<FightStartType>(`/v1/accounts/${this.player.id}/fight/${fightId}`);
160
161     if(!res.payload.id) {
162       throw new Error('Invalid fight!');
163     }
164
165     return res.payload;
166   }
167
168   async pickItem(dropId: string): Promise<void> {
169     if(this.player === null) {
170       throw new Error('Not authenticated');
171     }
172
173     disablePickItemButton();
174
175     const res = await this.post<pickWorldDropOutput>(`/v1/accounts/${this.player.id}/pick/${dropId}`);
176
177     actionLog(`You picked up the item!`, false);
178   }
179
180   async fight(fightId: string): Promise<FightRoundOutput> {
181     if(this.player === null) {
182       throw new Error('Not authenticated');
183     }
184
185     await this.startFight(fightId);
186
187     const res = await this.post<FightRoundOutput>(`/v1/accounts/${this.player.id}/fight/${fightId}/round`, {
188       action: 'Fight'
189     });
190
191     const output = res.payload;
192
193     this.setPlayer(output.player);
194
195     console.log(output);
196
197     const participants = {
198       [output.player.id]: output.player.username,
199       [output.monster.id]: output.monster.name
200     }
201
202     output.roundData.map(round => {
203       const p1 = participants[round.attacker];
204       const p2 = participants[round.defender];
205
206       const css = round.attacker === output.player.id ? 'text-secondary' : 'text-info';
207       let str = `<span class="${css}">${p1}</span> dealt <span class="text-danger">${round.damage} damage</span> to ${p2}`;
208
209       return str;
210     }).forEach(a => actionLog(a, false));
211
212     if(output.winner === 'player') {
213       actionLog(`<span class="text-success">You defeated the ${output.monster.name}</span>`, false);
214     }
215     else {
216       actionLog(`<span class="text-danger">You were defeated by the ${output.monster.name}</span>`, false);
217     }
218
219     if(output.reward.exp) {
220       actionLog(`You gained <span class="text-info">${output.reward.exp}</span> Exp`, false);
221     }
222
223     if(output.reward.currency) {
224       actionLog(`You gained <span class="text-warning">${output.reward.currency}</span> Steel`, false);
225     }
226
227     if(output.reward.worldDrop) {
228       enablePickItemButton({itemId: output.reward.worldDrop.id}, output.reward.worldDrop.name);
229     }
230
231     disableFightButton();
232
233     return output;
234   }
235 }