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