chore(release): 0.4.0
[risinglegends.git] / src / server / fight.ts
1 import {FightRound} from '../shared/fight';
2 import { clearFight, loadMonster, getMonsterList, saveFightState, loadMonsterFromFight } from './monster';
3 import { Player, expToLevel, maxHp, totalDefence, maxVigor } from '../shared/player';
4 import { clearTravelPlan } from './map';
5 import { updatePlayer } from './player';
6 import { getEquippedItems, updateAp } from './inventory';
7 import { EquippedItemDetails } from '../shared/equipped';
8 import { EquipmentSlot } from '../shared/inventory';
9 import { MonsterWithFaction, MonsterForFight, Fight } from '../shared/monsters';
10 import { getPlayerSkillsAsObject, updatePlayerSkills } from './skills';
11 import { SkillID, Skills } from '../shared/skills';
12 import { Request, Response } from 'express';
13 import * as Alert from './views/alert';
14 import { addEvent } from './events';
15
16 export async function blockPlayerInFight(req: Request, res: Response, next: any) {
17   const fight = await loadMonsterFromFight(req.player.id);
18   if(!fight) {
19     next();
20     return;
21   }
22
23   res.send(Alert.ErrorAlert(`You are currently in a fight with a ${fight.name}`));
24 }
25
26 function exponentialExp(exp: number, monsterLevel: number, playerLevel: number): number {
27   let finalExp = exp;
28
29   if((monsterLevel+3) < playerLevel) {
30     finalExp = Math.floor(exp * Math.pow(Math.E, ((monsterLevel + 3) - playerLevel)/5));
31   }
32   else if(monsterLevel > (playerLevel + 3)) {
33     finalExp = Math.floor(exp * Math.pow(Math.E, ((playerLevel + 3) - monsterLevel)/5));
34   }
35
36   return Math.floor(finalExp);
37 }
38
39 export async function fightRound(player: Player, monster: Fight,  data: {action: 'attack' | 'cast' | 'flee'}) {
40   const playerSkills = await getPlayerSkillsAsObject(player.id);
41   const roundData: FightRound = {
42     monster,
43     player,
44     winner: 'in-progress',
45     fightTrigger: monster.fight_trigger,
46     roundDetails: [],
47     rewards: {
48       exp: 0,
49       gold: 0,
50       levelIncrease: false
51     }
52   };
53   const equippedItems = await getEquippedItems(player.id);
54
55   // we only use this if the player successfully defeated the monster 
56   // they were fighting, then we load the other monsters in this area 
57   // so they can "fight again"
58   let potentialMonsters: MonsterForFight[] = [];
59
60   const boost = {
61     defence: totalDefence(equippedItems, player),
62     strength: 0,
63     constitution: 0,
64     dexterity: 0,
65     intelligence: 0,
66     damage: 0,
67     hp: 0,
68   };
69
70   const equipment: Map<EquipmentSlot, EquippedItemDetails> = new Map<EquipmentSlot, EquippedItemDetails>();
71   const weapons: EquippedItemDetails[] = [];
72   let anyDamageSpells: boolean = false;
73   equippedItems.forEach(item => {
74     if(item.type === 'ARMOUR') {
75       equipment.set(item.equipment_slot, item);
76     }
77     else if(item.type === 'WEAPON') {
78       weapons.push(item);
79     }
80     else if(item.type === 'SPELL') {
81       if(item.affectedSkills.includes('destruction_magic')) {
82         anyDamageSpells = true;
83       }
84       weapons.push(item);
85     }
86
87     boost.strength += item.boosts.strength;
88     boost.constitution += item.boosts.constitution;
89     boost.dexterity += item.boosts.dexterity;
90     boost.intelligence += item.boosts.intelligence;
91
92     if(item.type === 'SPELL' && item.affectedSkills.includes('restoration_magic')) {
93       boost.hp += item.boosts.damage;
94     }
95     else {
96       boost.damage += item.boosts.damage;
97     }
98   });
99
100   // @TODO implement flee based on dex + vigor
101   if(data.action === 'flee') {
102     roundData.roundDetails.push(`You managed to escape from the ${monster.name}!`)
103     roundData.winner = 'monster';
104     await clearFight(player.id);
105
106     return { roundData, monsters: [], player };
107   }
108
109   const attackType = data.action === 'attack' ? 'physical' : 'magical';
110   const primaryStat = attackType === 'physical' ? player.strength : player.intelligence;
111   const boostStat = attackType === 'physical' ? boost.strength : boost.intelligence;
112
113   const playerDamage = Math.floor(((primaryStat + boostStat) * 1.3) + boost.damage);
114   const skillsUsed: Record<SkillID | any, number> = {};
115   let hpHealAfterMasteries: number = -1;
116   let playerDamageAfterMasteries: number = 0;
117   // apply masteries!
118   weapons.forEach(item => {
119     item.affectedSkills.forEach(id => {
120       if(id === 'restoration_magic') {
121         if(hpHealAfterMasteries < 0) {
122           hpHealAfterMasteries = 0;
123         }
124         const skill = Skills.get(id);
125         if(skill) {
126           const playerSkill = playerSkills.get(id);
127           if(playerSkill) {
128             hpHealAfterMasteries += skill.effect(playerSkill);
129           }
130         }
131       }
132       else {
133         const skill = Skills.get(id);
134         if(skill) {
135           const playerSkill = playerSkills.get(id);
136           if(playerSkill) {
137             playerDamageAfterMasteries += playerDamage * skill.effect(playerSkill);
138           }
139         }
140       }
141
142       if(!skillsUsed[id]) {
143         skillsUsed[id] = 0;
144       }
145       skillsUsed[id]++;
146     });
147   });
148
149   await updatePlayerSkills(player.id, playerSkills, skillsUsed);
150
151   const playerFinalDamage = (data.action === 'cast' && !anyDamageSpells) ? 0 : Math.floor(playerDamage + playerDamageAfterMasteries);
152   const playerFinalHeal = Math.floor(boost.hp + hpHealAfterMasteries);
153
154   let monsterTakesDamage = playerFinalDamage - monster.defence;
155   if(monsterTakesDamage < 0) {
156     monsterTakesDamage = 0;
157   }
158   roundData.roundDetails.push(`You dealt ${monsterTakesDamage} damage to the ${monster.name}!`);
159
160   monster.hp -= monsterTakesDamage;
161
162   if(monster.hp <= 0) {
163     roundData.monster.hp = 0;
164     roundData.winner = 'player';
165
166     addEvent('MONSTER_KILLED', player.id, {
167       monster_id: roundData.monster.ref_id,
168       monster_name: roundData.monster.name,
169       level: roundData.monster.level,
170       fightTrigger: roundData.monster.fight_trigger
171     });
172
173     const expGained = exponentialExp(monster.exp, monster.level, player.level);
174
175     roundData.rewards.exp = expGained;
176     roundData.rewards.gold = monster.gold;
177
178     player.gold += monster.gold;
179     player.exp += expGained;
180
181     if(player.exp >= expToLevel(player.level + 1)) {
182       player.exp -= expToLevel(player.level + 1)
183       player.level++;
184       addEvent('LEVEL_UP', player.id, {
185         from_level: player.level-1,
186         to_level: player.level
187       });
188       roundData.rewards.levelIncrease = true;
189       let statPointsGained = 1;
190
191       if(player.profession !== 'Wanderer') {
192         statPointsGained = 2;
193       }
194
195       player.stat_points += statPointsGained;
196
197       roundData.roundDetails.push(`You gained ${statPointsGained} stat points!`);
198
199       player.hp = maxHp(player.constitution, player.level);
200       player.vigor = maxVigor(player.constitution, player.level);
201     }
202     // get the monster location if it was an EXPLORED fight
203     if(roundData.fightTrigger === 'explore') {
204       const rawMonster = await loadMonster(monster.ref_id);
205       const monsterList  = await getMonsterList(rawMonster.location_id);
206       potentialMonsters = monsterList.map(monster => {
207         return {
208           id: monster.id,
209           name: monster.name,
210           minLevel: monster.minLevel,
211           maxLevel: monster.maxLevel,
212           hp: monster.hp,
213           maxHp: monster.maxHp,
214           fight_trigger: 'explore'
215         }
216       });
217     }
218
219     player.vigor -= 1;
220     if(player.vigor < 0) {
221       player.vigor = 0;
222     }
223
224     const unequippedItems = await updateAp(player.id, 1, equippedItems.map(i => i.item_id));
225     await clearFight(player.id);
226     await updatePlayer(player);
227
228     if(unequippedItems.length) {
229       unequippedItems.forEach(i => {
230         roundData.roundDetails.push(`Your ${i.name} was too damaged and was unequipped!`);
231       });
232     }
233
234     return { roundData, monsters: potentialMonsters, player };
235   }
236
237   let monsterDamage = (monster.strength*2) - boost.defence;
238   if(monsterDamage < 0) {
239     monsterDamage = 0;
240   }
241
242   roundData.roundDetails.push(`The ${monster.name} hit you for ${monsterDamage} damage`);
243   player.hp -= monsterDamage;
244
245   if(playerFinalHeal > 0) {
246     player.hp += playerFinalHeal;
247     if(player.hp > maxHp(player.constitution, player.level)) {
248       player.hp = maxHp(player.constitution, player.level);
249     }
250     roundData.roundDetails.push(`You healed for ${playerFinalHeal} HP`);
251   }
252
253   if(player.hp <= 0) {
254     player.hp = 0;
255     player.vigor = 0;
256     roundData.winner = 'monster';
257
258     roundData.roundDetails.push(`You were killed by the ${monster.name}`);
259
260     await clearFight(player.id);
261     const unequippedItems = await updateAp(player.id, 5, equippedItems.map(i => i.item_id));
262     await updatePlayer(player);
263     await clearTravelPlan(player.id);
264
265     if(unequippedItems.length) {
266       unequippedItems.forEach(i => {
267         roundData.roundDetails.push(`Your ${i.name} was too damaged and was unequipped!`);
268       });
269     }
270
271     return { roundData, monsters: [], player};
272   }
273
274   await updatePlayer(player);
275   await saveFightState(player.id, monster);
276
277   return { roundData, monsters: [], player};
278 };