chore: separate fight round into its own file
authorxangelo <me@xangelo.ca>
Thu, 24 Aug 2023 17:53:54 +0000 (13:53 -0400)
committerxangelo <me@xangelo.ca>
Thu, 24 Aug 2023 17:53:54 +0000 (13:53 -0400)
src/server/api.ts
src/server/fight.ts [new file with mode: 0644]

index d9f67abc902cc611118d2871b29b19937306a4dc..4a176dd3d2d4004d0284c8a754fecb096f24913e 100644 (file)
@@ -13,20 +13,19 @@ import { logger } from './lib/logger';
 import { loadPlayer, createPlayer, updatePlayer, movePlayer } from './player';
 import { random, sample } from 'lodash';
 import {broadcastMessage, Message} from '../shared/message';
-import {expToLevel, maxHp, Player} from '../shared/player';
-import {clearFight, createFight, getMonsterList, getMonsterLocation, getRandomMonster, loadMonster, loadMonsterFromFight, loadMonsterWithFaction, saveFightState} from './monster';
-import {FightRound} from '../shared/fight';
-import {addInventoryItem, deleteInventoryItem, getEquippedItems, getInventory, getInventoryItem, updateAp} from './inventory';
+import {maxHp, Player} from '../shared/player';
+import {createFight, getMonsterList, getMonsterLocation, getRandomMonster, loadMonster, loadMonsterFromFight, loadMonsterWithFaction} from './monster';
+import {addInventoryItem, getEquippedItems, getInventory, getInventoryItem} from './inventory';
 import { getItemFromPlayer, getItemFromShop, getPlayersItems, getShopItems, givePlayerItem, updateItemCount } from './items';
-import {FightTrigger, Monster, MonsterForFight, MonsterWithFaction} from '../shared/monsters';
+import {FightTrigger, Monster, MonsterForFight} from '../shared/monsters';
 import {getShopEquipment, listShopItems } from './shopEquipment';
-import {EquippedItemDetails} from '../shared/equipped';
-import {ArmourEquipmentSlot, EquipmentSlot} from '../shared/inventory';
+import {EquipmentSlot} from '../shared/inventory';
 import { clearTravelPlan, completeTravel, getAllPaths, getAllServices, getCityDetails, getService, getTravelPlan, stepForward, travel } from './map';
 import { signup, login, authEndpoint, AuthRequest } from './auth';
 import {db} from './lib/db';
-import { getPlayerSkills, getPlayerSkillsAsObject, updatePlayerSkills } from './skills';
-import {SkillID, Skills} from '../shared/skills';
+import { getPlayerSkills} from './skills';
+
+import { fightRound, blockPlayerInFight } from './fight';
 
 import  { router as healerRouter } from './locations/healer';
 import { router as professionRouter } from './locations/recruiter';
@@ -116,265 +115,6 @@ io.on('connection', async socket => {
   socket.emit('ready');
 });
 
-async function fightRound(player: Player, monster: MonsterWithFaction,  data: {action: 'attack' | 'cast' | 'flee', target: 'head' | 'body' | 'arms' | 'legs'}) {
-  const playerSkills = await getPlayerSkillsAsObject(player.id);
-  const roundData: FightRound = {
-    monster,
-    player,
-    winner: 'in-progress',
-    fightTrigger: monster.fight_trigger,
-    roundDetails: [],
-    rewards: {
-      exp: 0,
-      gold: 0,
-      levelIncrease: false
-    }
-  };
-  const equippedItems = await getEquippedItems(player.id);
-
-  // we only use this if the player successfully defeated the monster 
-  // they were fighting, then we load the other monsters in this area 
-  // so they can "fight again"
-  let potentialMonsters: MonsterForFight[] = [];
-
-  /*
-   * cumulative chance of head/arms/body/legs
-   * 0 -> 0.2 = head
-   * 0.21 -> 0.4 = arms
-   *
-   * we use the factor to decide how many decimal places 
-   * we care about
-   */
-  const factor = 100;
-  const monsterTarget = [0.2, 0.4, 0.9, 1];
-  const targets: ArmourEquipmentSlot[] = ['HEAD', 'CHEST', 'ARMS', 'LEGS'];
-  // calc weighted
-  const rand = Math.ceil(Math.random() * factor);
-  let target: ArmourEquipmentSlot = 'CHEST';
-  monsterTarget.forEach((i, idx) => {
-    if (rand > (i * factor)) {
-      target = targets[idx] as ArmourEquipmentSlot;
-    }
-  });
-
-  const boost = {
-    strength: 0,
-    constitution: 0,
-    dexterity: 0,
-    intelligence: 0,
-    damage: 0,
-    hp: 0,
-  };
-
-  const equipment: Map<EquipmentSlot, EquippedItemDetails> = new Map<EquipmentSlot, EquippedItemDetails>();
-  const weapons: EquippedItemDetails[] = [];
-  let anyDamageSpells: boolean = false;
-  equippedItems.forEach(item => {
-    if(item.type === 'ARMOUR') {
-      equipment.set(item.equipment_slot, item);
-    }
-    else if(item.type === 'WEAPON') {
-      weapons.push(item);
-    }
-    else if(item.type === 'SPELL') {
-      if(item.affectedSkills.includes('destruction_magic')) {
-        anyDamageSpells = true;
-      }
-      weapons.push(item);
-    }
-
-    boost.strength += item.boosts.strength;
-    boost.constitution += item.boosts.constitution;
-    boost.dexterity += item.boosts.dexterity;
-    boost.intelligence += item.boosts.intelligence;
-
-    if(item.type === 'SPELL' && item.affectedSkills.includes('restoration_magic')) {
-      boost.hp += item.boosts.damage;
-    }
-    else {
-      boost.damage += item.boosts.damage;
-    }
-  });
-
-  // if you flee'd, then we want to check your dex vs. the monsters
-  // but we want to give you the item/weapon boosts you need
-  // if not then you're going to get hit.
-  if(data.action === 'flee') {
-    roundData.roundDetails.push(`You managed to escape from the ${monster.name}!`)
-    roundData.winner = 'monster';
-    await clearFight(player.id);
-
-    return { roundData, monsters: [], player };
-  }
-
-  const attackType = data.action === 'attack' ? 'physical' : 'magical';
-  const primaryStat = data.action === 'attack' ? player.strength : player.intelligence;
-  const boostStat = data.action === 'attack' ? boost.strength : boost.intelligence;
-
-  const playerDamage = Math.floor(((primaryStat + boostStat) * 1.3) + boost.damage);
-  const skillsUsed: Record<SkillID | any, number> = {};
-  let hpHealAfterMasteries: number = -1;
-  let playerDamageAfterMasteries: number = 0;
-  // apply masteries!
-  weapons.forEach(item => {
-    item.affectedSkills.forEach(id => {
-      if(id === 'restoration_magic') {
-        if(hpHealAfterMasteries < 0) {
-          hpHealAfterMasteries = 0;
-        }
-        hpHealAfterMasteries += Skills.get(id).effect(playerSkills.get(id));
-      }
-      else {
-        playerDamageAfterMasteries += playerDamage * Skills.get(id).effect(playerSkills.get(id));
-      }
-
-      if(!skillsUsed[id]) {
-        skillsUsed[id] = 0;
-      }
-      skillsUsed[id]++;
-    });
-  });
-
-  await updatePlayerSkills(player.id, skillsUsed);
-
-  const playerFinalDamage = (data.action === 'cast' && !anyDamageSpells) ? 0 : Math.floor(playerDamage + playerDamageAfterMasteries);
-  const playerFinalHeal = Math.floor(boost.hp + hpHealAfterMasteries);
-
-  roundData.roundDetails.push(`You targeted the monsters ${data.target.toUpperCase()} with ${attackType} damage!`);
-  let armourKey: string;
-  switch(data.target) {
-    case 'arms':
-      armourKey = 'armsAp';
-      break;
-    case 'head':
-      armourKey = 'helmAp';
-      break;
-    case 'legs':
-      armourKey = 'legsAp';
-      break;
-    case 'body':
-      armourKey = 'chestAp';
-      break;
-  }
-
-  if(monster[armourKey] && monster[armourKey] > 0) {
-    monster[armourKey] -= playerFinalDamage;
-
-    roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
-    if(monster[armourKey] < 0) {
-      roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
-      roundData.roundDetails.push(`You dealt ${monster[armourKey] * -1} damage to their HP`);
-      monster.hp += monster[armourKey];
-      monster[armourKey] = 0;
-    }
-  }
-  else {
-    roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
-    monster.hp -= playerFinalDamage;
-  }
-
-  if(monster.hp <= 0) {
-    roundData.monster.hp = 0;
-    roundData.winner = 'player';
-
-    roundData.rewards.exp = monster.exp;
-    roundData.rewards.gold = monster.gold;
-
-    player.gold += monster.gold;
-    player.exp += monster.exp;
-
-    if(player.exp >= expToLevel(player.level + 1)) {
-      player.exp -= expToLevel(player.level + 1)
-      player.level++;
-      roundData.rewards.levelIncrease = true;
-      let statPointsGained = 1;
-
-      if(player.profession !== 'Wanderer') {
-        statPointsGained = 2;
-      }
-
-      player.stat_points += statPointsGained;
-
-      roundData.roundDetails.push(`You gained ${statPointsGained} stat points!`);
-
-      player.hp = maxHp(player.constitution, player.level);
-    }
-    // get the monster location if it was an EXPLORED fight
-    if(roundData.fightTrigger === 'explore') {
-      const rawMonster = await loadMonster(monster.ref_id);
-      const monsterList  = await getMonsterList(rawMonster.location_id);
-      potentialMonsters = monsterList.map(monster => {
-        return {
-          id: monster.id,
-          name: monster.name,
-          level: monster.level,
-          hp: monster.hp,
-          maxHp: monster.maxHp,
-          fight_trigger: 'explore'
-        }
-      });
-    }
-
-    await clearFight(player.id);
-    await updatePlayer(player);
-    return { roundData, monsters: potentialMonsters, player };
-  }
-
-  roundData.roundDetails.push(`The ${monster.name} targeted your ${target}!`);
-  if(equipment.has(target)) {
-    const item = equipment.get(target);
-    // apply mitigation!
-    const mitigationPercentage = item.boosts.damage_mitigation || 0;
-    const damageAfterMitigation = Math.floor(monster.strength * ((100-mitigationPercentage)/100));
-
-    item.currentAp -= damageAfterMitigation;
-
-    if(item.currentAp < 0) {
-      roundData.roundDetails.push(`Your ${item.name} amour was destroyed`);
-      roundData.roundDetails.push(`The ${monster.name} hit your HP for ${item.currentAp * -1} damage!`);
-      player.hp += item.currentAp;
-      item.currentAp = 0;
-      await deleteInventoryItem(player.id, item.item_id);
-    }
-    else {
-      roundData.roundDetails.push(`Your ${target} took ${damageAfterMitigation} damage!`);
-      await updateAp(player.id, item.item_id, item.currentAp, item.maxAp);
-    }
-
-  }
-  else {
-    roundData.roundDetails.push(`The ${monster.name} hit you for ${monster.strength} damage`);
-    player.hp -= monster.strength;
-  }
-
-  if(playerFinalHeal > 0) {
-    player.hp += playerFinalHeal;
-    if(player.hp > maxHp(player.constitution, player.level)) {
-      player.hp = maxHp(player.constitution, player.level);
-    }
-    roundData.roundDetails.push(`You healed for ${playerFinalHeal} HP`);
-  }
-
-  // update the players inventory for this item!
-
-  if(player.hp <= 0) {
-    player.hp = 0;
-    roundData.winner = 'monster';
-
-    roundData.roundDetails.push(`You were killed by the ${monster.name}`);
-
-    await clearFight(player.id);
-    await updatePlayer(player);
-    await clearTravelPlan(player.id);
-
-    return { roundData, monsters: [], player};
-  }
-
-  await updatePlayer(player);
-  await saveFightState(player.id, monster);
-
-  return { roundData, monsters: [], player};
-};
 
 app.use(healerRouter);
 app.use(professionRouter);
@@ -538,16 +278,6 @@ app.post('/player/unequip/:item_id', authEndpoint, blockPlayerInFight, async (re
   res.send(renderInventoryPage(inventory, items, item.type) + renderPlayerBar(req.player, inventory));
 });
 
-async function blockPlayerInFight(req: AuthRequest, res: Response, next) {
-  const fight = await loadMonsterFromFight(req.player.id);
-  if(!fight) {
-    next();
-    return;
-  }
-
-  res.send(Alert.ErrorAlert(`You are currently in a fight with a ${fight.name}`));
-}
-
 app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response) => {
   const fight = await loadMonsterFromFight(req.player.id);
   const travelPlan = await getTravelPlan(req.player.id);
diff --git a/src/server/fight.ts b/src/server/fight.ts
new file mode 100644 (file)
index 0000000..9b377e6
--- /dev/null
@@ -0,0 +1,296 @@
+import {FightRound} from '../shared/fight';
+import { clearFight, loadMonster, getMonsterList, saveFightState, loadMonsterFromFight } from './monster';
+import { Player, expToLevel, maxHp } from '../shared/player';
+import { clearTravelPlan } from './map';
+import { updatePlayer } from './player';
+import { getEquippedItems, updateAp, deleteInventoryItem } from './inventory';
+import { EquippedItemDetails } from '../shared/equipped';
+import { ArmourEquipmentSlot, EquipmentSlot } from '../shared/inventory';
+import { MonsterWithFaction, MonsterForFight } from '../shared/monsters';
+import { getPlayerSkillsAsObject, updatePlayerSkills } from './skills';
+import { SkillID, Skills } from '../shared/skills';
+import { AuthRequest } from './auth';
+import { Response } from 'express';
+import * as Alert from './views/alert';
+
+export async function blockPlayerInFight(req: AuthRequest, res: Response, next: any) {
+  const fight = await loadMonsterFromFight(req.player.id);
+  if(!fight) {
+    next();
+    return;
+  }
+
+  res.send(Alert.ErrorAlert(`You are currently in a fight with a ${fight.name}`));
+}
+
+export async function fightRound(player: Player, monster: MonsterWithFaction,  data: {action: 'attack' | 'cast' | 'flee', target: 'head' | 'body' | 'arms' | 'legs'}) {
+  const playerSkills = await getPlayerSkillsAsObject(player.id);
+  const roundData: FightRound = {
+    monster,
+    player,
+    winner: 'in-progress',
+    fightTrigger: monster.fight_trigger,
+    roundDetails: [],
+    rewards: {
+      exp: 0,
+      gold: 0,
+      levelIncrease: false
+    }
+  };
+  const equippedItems = await getEquippedItems(player.id);
+
+  // we only use this if the player successfully defeated the monster 
+  // they were fighting, then we load the other monsters in this area 
+  // so they can "fight again"
+  let potentialMonsters: MonsterForFight[] = [];
+
+  /*
+   * cumulative chance of head/arms/body/legs
+   * 0 -> 0.2 = head
+   * 0.21 -> 0.4 = arms
+   *
+   * we use the factor to decide how many decimal places 
+   * we care about
+   */
+  const factor = 100;
+  const monsterTarget = [0.2, 0.4, 0.9, 1];
+  const targets: ArmourEquipmentSlot[] = ['HEAD', 'CHEST', 'ARMS', 'LEGS'];
+  // calc weighted
+  const rand = Math.ceil(Math.random() * factor);
+  let target: ArmourEquipmentSlot = 'CHEST';
+  monsterTarget.forEach((i, idx) => {
+    if (rand > (i * factor)) {
+      target = targets[idx] as ArmourEquipmentSlot;
+    }
+  });
+
+  const boost = {
+    strength: 0,
+    constitution: 0,
+    dexterity: 0,
+    intelligence: 0,
+    damage: 0,
+    hp: 0,
+  };
+
+  const equipment: Map<EquipmentSlot, EquippedItemDetails> = new Map<EquipmentSlot, EquippedItemDetails>();
+  const weapons: EquippedItemDetails[] = [];
+  let anyDamageSpells: boolean = false;
+  equippedItems.forEach(item => {
+    if(item.type === 'ARMOUR') {
+      equipment.set(item.equipment_slot, item);
+    }
+    else if(item.type === 'WEAPON') {
+      weapons.push(item);
+    }
+    else if(item.type === 'SPELL') {
+      if(item.affectedSkills.includes('destruction_magic')) {
+        anyDamageSpells = true;
+      }
+      weapons.push(item);
+    }
+
+    boost.strength += item.boosts.strength;
+    boost.constitution += item.boosts.constitution;
+    boost.dexterity += item.boosts.dexterity;
+    boost.intelligence += item.boosts.intelligence;
+
+    if(item.type === 'SPELL' && item.affectedSkills.includes('restoration_magic')) {
+      boost.hp += item.boosts.damage;
+    }
+    else {
+      boost.damage += item.boosts.damage;
+    }
+  });
+
+  // if you flee'd, then we want to check your dex vs. the monsters
+  // but we want to give you the item/weapon boosts you need
+  // if not then you're going to get hit.
+  if(data.action === 'flee') {
+    roundData.roundDetails.push(`You managed to escape from the ${monster.name}!`)
+    roundData.winner = 'monster';
+    await clearFight(player.id);
+
+    return { roundData, monsters: [], player };
+  }
+
+  const attackType = data.action === 'attack' ? 'physical' : 'magical';
+  const primaryStat = data.action === 'attack' ? player.strength : player.intelligence;
+  const boostStat = data.action === 'attack' ? boost.strength : boost.intelligence;
+
+  const playerDamage = Math.floor(((primaryStat + boostStat) * 1.3) + boost.damage);
+  const skillsUsed: Record<SkillID | any, number> = {};
+  let hpHealAfterMasteries: number = -1;
+  let playerDamageAfterMasteries: number = 0;
+  // apply masteries!
+  weapons.forEach(item => {
+    item.affectedSkills.forEach(id => {
+      if(id === 'restoration_magic') {
+        if(hpHealAfterMasteries < 0) {
+          hpHealAfterMasteries = 0;
+        }
+        const skill = Skills.get(id);
+        if(skill) {
+          const playerSkill = playerSkills.get(id);
+          if(playerSkill) {
+            hpHealAfterMasteries += skill.effect(playerSkill);
+          }
+        }
+      }
+      else {
+        const skill = Skills.get(id);
+        if(skill) {
+          const playerSkill = playerSkills.get(id);
+          if(playerSkill) {
+            playerDamageAfterMasteries += playerDamage * skill.effect(playerSkill);
+          }
+        }
+      }
+
+      if(!skillsUsed[id]) {
+        skillsUsed[id] = 0;
+      }
+      skillsUsed[id]++;
+    });
+  });
+
+  await updatePlayerSkills(player.id, skillsUsed);
+
+  const playerFinalDamage = (data.action === 'cast' && !anyDamageSpells) ? 0 : Math.floor(playerDamage + playerDamageAfterMasteries);
+  const playerFinalHeal = Math.floor(boost.hp + hpHealAfterMasteries);
+
+  roundData.roundDetails.push(`You targeted the monsters ${data.target.toUpperCase()} with ${attackType} damage!`);
+  let armourKey: string;
+  switch(data.target) {
+    case 'arms':
+      armourKey = 'armsAp';
+      break;
+    case 'head':
+      armourKey = 'helmAp';
+      break;
+    case 'legs':
+      armourKey = 'legsAp';
+      break;
+    case 'body':
+      armourKey = 'chestAp';
+      break;
+  }
+
+  if(monster[armourKey] && monster[armourKey] > 0) {
+    monster[armourKey] -= playerFinalDamage;
+
+    roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
+    if(monster[armourKey] < 0) {
+      roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
+      roundData.roundDetails.push(`You dealt ${monster[armourKey] * -1} damage to their HP`);
+      monster.hp += monster[armourKey];
+      monster[armourKey] = 0;
+    }
+  }
+  else {
+    roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
+    monster.hp -= playerFinalDamage;
+  }
+
+  if(monster.hp <= 0) {
+    roundData.monster.hp = 0;
+    roundData.winner = 'player';
+
+    roundData.rewards.exp = monster.exp;
+    roundData.rewards.gold = monster.gold;
+
+    player.gold += monster.gold;
+    player.exp += monster.exp;
+
+    if(player.exp >= expToLevel(player.level + 1)) {
+      player.exp -= expToLevel(player.level + 1)
+      player.level++;
+      roundData.rewards.levelIncrease = true;
+      let statPointsGained = 1;
+
+      if(player.profession !== 'Wanderer') {
+        statPointsGained = 2;
+      }
+
+      player.stat_points += statPointsGained;
+
+      roundData.roundDetails.push(`You gained ${statPointsGained} stat points!`);
+
+      player.hp = maxHp(player.constitution, player.level);
+    }
+    // get the monster location if it was an EXPLORED fight
+    if(roundData.fightTrigger === 'explore') {
+      const rawMonster = await loadMonster(monster.ref_id);
+      const monsterList  = await getMonsterList(rawMonster.location_id);
+      potentialMonsters = monsterList.map(monster => {
+        return {
+          id: monster.id,
+          name: monster.name,
+          level: monster.level,
+          hp: monster.hp,
+          maxHp: monster.maxHp,
+          fight_trigger: 'explore'
+        }
+      });
+    }
+
+    await clearFight(player.id);
+    await updatePlayer(player);
+    return { roundData, monsters: potentialMonsters, player };
+  }
+
+  roundData.roundDetails.push(`The ${monster.name} targeted your ${target}!`);
+  const item = equipment.get(target);
+  if(item) {
+    // apply mitigation!
+    const mitigationPercentage = item.boosts.damage_mitigation || 0;
+    const damageAfterMitigation = Math.floor(monster.strength * ((100-mitigationPercentage)/100));
+
+    item.currentAp -= damageAfterMitigation;
+
+    if(item.currentAp < 0) {
+      roundData.roundDetails.push(`Your ${item.name} amour was destroyed`);
+      roundData.roundDetails.push(`The ${monster.name} hit your HP for ${item.currentAp * -1} damage!`);
+      player.hp += item.currentAp;
+      item.currentAp = 0;
+      await deleteInventoryItem(player.id, item.item_id);
+    }
+    else {
+      roundData.roundDetails.push(`Your ${target} took ${damageAfterMitigation} damage!`);
+      await updateAp(player.id, item.item_id, item.currentAp, item.maxAp);
+    }
+
+  }
+  else {
+    roundData.roundDetails.push(`The ${monster.name} hit you for ${monster.strength} damage`);
+    player.hp -= monster.strength;
+  }
+
+  if(playerFinalHeal > 0) {
+    player.hp += playerFinalHeal;
+    if(player.hp > maxHp(player.constitution, player.level)) {
+      player.hp = maxHp(player.constitution, player.level);
+    }
+    roundData.roundDetails.push(`You healed for ${playerFinalHeal} HP`);
+  }
+
+  // update the players inventory for this item!
+
+  if(player.hp <= 0) {
+    player.hp = 0;
+    roundData.winner = 'monster';
+
+    roundData.roundDetails.push(`You were killed by the ${monster.name}`);
+
+    await clearFight(player.id);
+    await updatePlayer(player);
+    await clearTravelPlan(player.id);
+
+    return { roundData, monsters: [], player};
+  }
+
+  await updatePlayer(player);
+  await saveFightState(player.id, monster);
+
+  return { roundData, monsters: [], player};
+};