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