chore(release): 0.2.16
[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 } from '../shared/player';
4 import { clearTravelPlan } from './map';
5 import { updatePlayer } from './player';
6 import { getEquippedItems, updateAp, deleteInventoryItem } from './inventory';
7 import { EquippedItemDetails } from '../shared/equipped';
8 import { ArmourEquipmentSlot, EquipmentSlot } from '../shared/inventory';
9 import { MonsterWithFaction, MonsterForFight } from '../shared/monsters';
10 import { getPlayerSkillsAsObject, updatePlayerSkills } from './skills';
11 import { SkillID, Skills } from '../shared/skills';
12 import { AuthRequest } from './auth';
13 import { Response } from 'express';
14 import * as Alert from './views/alert';
15
16 export async function blockPlayerInFight(req: AuthRequest, 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: MonsterWithFaction,  data: {action: 'attack' | 'cast' | 'flee', target: 'head' | 'body' | 'arms' | 'legs'}) {
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   /*
61    * cumulative chance of head/arms/body/legs
62    * 0 -> 0.2 = head
63    * 0.21 -> 0.4 = arms
64    *
65    * we use the factor to decide how many decimal places 
66    * we care about
67    */
68   const factor = 100;
69   const monsterTarget = [0.2, 0.4, 0.9, 1];
70   const targets: ArmourEquipmentSlot[] = ['HEAD', 'CHEST', 'ARMS', 'LEGS'];
71   // calc weighted
72   const rand = Math.ceil(Math.random() * factor);
73   let target: ArmourEquipmentSlot = 'CHEST';
74   monsterTarget.forEach((i, idx) => {
75     if (rand > (i * factor)) {
76       target = targets[idx] as ArmourEquipmentSlot;
77     }
78   });
79
80   const boost = {
81     strength: 0,
82     constitution: 0,
83     dexterity: 0,
84     intelligence: 0,
85     damage: 0,
86     hp: 0,
87   };
88
89   const equipment: Map<EquipmentSlot, EquippedItemDetails> = new Map<EquipmentSlot, EquippedItemDetails>();
90   const weapons: EquippedItemDetails[] = [];
91   let anyDamageSpells: boolean = false;
92   equippedItems.forEach(item => {
93     if(item.type === 'ARMOUR') {
94       equipment.set(item.equipment_slot, item);
95     }
96     else if(item.type === 'WEAPON') {
97       weapons.push(item);
98     }
99     else if(item.type === 'SPELL') {
100       if(item.affectedSkills.includes('destruction_magic')) {
101         anyDamageSpells = true;
102       }
103       weapons.push(item);
104     }
105
106     boost.strength += item.boosts.strength;
107     boost.constitution += item.boosts.constitution;
108     boost.dexterity += item.boosts.dexterity;
109     boost.intelligence += item.boosts.intelligence;
110
111     if(item.type === 'SPELL' && item.affectedSkills.includes('restoration_magic')) {
112       boost.hp += item.boosts.damage;
113     }
114     else {
115       boost.damage += item.boosts.damage;
116     }
117   });
118
119   // if you flee'd, then we want to check your dex vs. the monsters
120   // but we want to give you the item/weapon boosts you need
121   // if not then you're going to get hit.
122   if(data.action === 'flee') {
123     roundData.roundDetails.push(`You managed to escape from the ${monster.name}!`)
124     roundData.winner = 'monster';
125     await clearFight(player.id);
126
127     return { roundData, monsters: [], player };
128   }
129
130   const attackType = data.action === 'attack' ? 'physical' : 'magical';
131   const primaryStat = data.action === 'attack' ? player.strength : player.intelligence;
132   const boostStat = data.action === 'attack' ? boost.strength : boost.intelligence;
133
134   const playerDamage = Math.floor(((primaryStat + boostStat) * 1.3) + boost.damage);
135   const skillsUsed: Record<SkillID | any, number> = {};
136   let hpHealAfterMasteries: number = -1;
137   let playerDamageAfterMasteries: number = 0;
138   // apply masteries!
139   weapons.forEach(item => {
140     item.affectedSkills.forEach(id => {
141       if(id === 'restoration_magic') {
142         if(hpHealAfterMasteries < 0) {
143           hpHealAfterMasteries = 0;
144         }
145         const skill = Skills.get(id);
146         if(skill) {
147           const playerSkill = playerSkills.get(id);
148           if(playerSkill) {
149             hpHealAfterMasteries += skill.effect(playerSkill);
150           }
151         }
152       }
153       else {
154         const skill = Skills.get(id);
155         if(skill) {
156           const playerSkill = playerSkills.get(id);
157           if(playerSkill) {
158             playerDamageAfterMasteries += playerDamage * skill.effect(playerSkill);
159           }
160         }
161       }
162
163       if(!skillsUsed[id]) {
164         skillsUsed[id] = 0;
165       }
166       skillsUsed[id]++;
167     });
168   });
169
170   await updatePlayerSkills(player.id, playerSkills, skillsUsed);
171
172   const playerFinalDamage = (data.action === 'cast' && !anyDamageSpells) ? 0 : Math.floor(playerDamage + playerDamageAfterMasteries);
173   const playerFinalHeal = Math.floor(boost.hp + hpHealAfterMasteries);
174
175   roundData.roundDetails.push(`You targeted the monsters ${data.target.toUpperCase()} with ${attackType} damage!`);
176   let armourKey: string;
177   switch(data.target) {
178     case 'arms':
179       armourKey = 'armsAp';
180       break;
181     case 'head':
182       armourKey = 'helmAp';
183       break;
184     case 'legs':
185       armourKey = 'legsAp';
186       break;
187     case 'body':
188       armourKey = 'chestAp';
189       break;
190   }
191
192   if(monster[armourKey] && monster[armourKey] > 0) {
193     monster[armourKey] -= playerFinalDamage;
194
195     roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
196     if(monster[armourKey] < 0) {
197       roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
198       roundData.roundDetails.push(`You dealt ${monster[armourKey] * -1} damage to their HP`);
199       monster.hp += monster[armourKey];
200       monster[armourKey] = 0;
201     }
202   }
203   else {
204     roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
205     monster.hp -= playerFinalDamage;
206   }
207
208   if(monster.hp <= 0) {
209     roundData.monster.hp = 0;
210     roundData.winner = 'player';
211
212     const expGained = exponentialExp(monster.exp, monster.level, player.level);
213
214     roundData.rewards.exp = expGained;
215     roundData.rewards.gold = monster.gold;
216
217     player.gold += monster.gold;
218     player.exp += expGained;
219
220     if(player.exp >= expToLevel(player.level + 1)) {
221       player.exp -= expToLevel(player.level + 1)
222       player.level++;
223       roundData.rewards.levelIncrease = true;
224       let statPointsGained = 1;
225
226       if(player.profession !== 'Wanderer') {
227         statPointsGained = 2;
228       }
229
230       player.stat_points += statPointsGained;
231
232       roundData.roundDetails.push(`You gained ${statPointsGained} stat points!`);
233
234       player.hp = maxHp(player.constitution, player.level);
235     }
236     // get the monster location if it was an EXPLORED fight
237     if(roundData.fightTrigger === 'explore') {
238       const rawMonster = await loadMonster(monster.ref_id);
239       const monsterList  = await getMonsterList(rawMonster.location_id);
240       potentialMonsters = monsterList.map(monster => {
241         return {
242           id: monster.id,
243           name: monster.name,
244           level: monster.level,
245           hp: monster.hp,
246           maxHp: monster.maxHp,
247           fight_trigger: 'explore'
248         }
249       });
250     }
251
252     await clearFight(player.id);
253     await updatePlayer(player);
254     return { roundData, monsters: potentialMonsters, player };
255   }
256
257   roundData.roundDetails.push(`The ${monster.name} targeted your ${target}!`);
258   const item = equipment.get(target);
259   if(item) {
260     // apply mitigation!
261     const mitigationPercentage = item.boosts.damage_mitigation || 0;
262     const damageAfterMitigation = Math.floor(monster.strength * ((100-mitigationPercentage)/100));
263
264     item.currentAp -= damageAfterMitigation;
265
266     if(item.currentAp < 0) {
267       roundData.roundDetails.push(`Your ${item.name} amour was destroyed`);
268       roundData.roundDetails.push(`The ${monster.name} hit your HP for ${item.currentAp * -1} damage!`);
269       player.hp += item.currentAp;
270       item.currentAp = 0;
271       await deleteInventoryItem(player.id, item.item_id);
272     }
273     else {
274       roundData.roundDetails.push(`Your ${target} took ${damageAfterMitigation} damage!`);
275       await updateAp(player.id, item.item_id, item.currentAp, item.maxAp);
276     }
277
278   }
279   else {
280     roundData.roundDetails.push(`The ${monster.name} hit you for ${monster.strength} damage`);
281     player.hp -= monster.strength;
282   }
283
284   if(playerFinalHeal > 0) {
285     player.hp += playerFinalHeal;
286     if(player.hp > maxHp(player.constitution, player.level)) {
287       player.hp = maxHp(player.constitution, player.level);
288     }
289     roundData.roundDetails.push(`You healed for ${playerFinalHeal} HP`);
290   }
291
292   // update the players inventory for this item!
293
294   if(player.hp <= 0) {
295     player.hp = 0;
296     roundData.winner = 'monster';
297
298     roundData.roundDetails.push(`You were killed by the ${monster.name}`);
299
300     await clearFight(player.id);
301     await updatePlayer(player);
302     await clearTravelPlan(player.id);
303
304     return { roundData, monsters: [], player};
305   }
306
307   await updatePlayer(player);
308   await saveFightState(player.id, monster);
309
310   return { roundData, monsters: [], player};
311 };