feat: equipment exp
authorxangelo <me@xangelo.ca>
Tue, 12 Nov 2024 18:21:05 +0000 (13:21 -0500)
committerxangelo <me@xangelo.ca>
Tue, 12 Nov 2024 18:21:05 +0000 (13:21 -0500)
All equipment now gains experience when used in combat. The levels are
calculated using a logarithmic progression so it isn't stored anywhere.

This also requires some UI updates to a players inventory to include
the new experience bar.

migrations/20241021184333_equipment-exp.ts
public/assets/css/game.css
src/server/fight.ts
src/server/inventory.ts
src/server/views/inventory.ts
src/shared/inventory.ts

index cc2da58c954458287d80fcf46bc04c570c95fc72..21180de1a0aa280618b83b566b43513a107a75fd 100644 (file)
@@ -5,7 +5,6 @@ export async function up(knex: Knex): Promise<void> {
   return knex.schema.alterTable('inventory', t => {
     t.timestamps();
     t.integer('current_exp').defaultTo(0);
-    t.integer('current_level').defaultTo(1);
   });
 }
 
@@ -14,7 +13,6 @@ export async function down(knex: Knex): Promise<void> {
   return knex.schema.alterTable('inventory', t => {
     t.dropTimestamps();
     t.dropColumn('current_exp');
-    t.dropColumn('current_level');
   });
 }
 
index 05fc248349c4e4734a6269e88b8d6a2bbb15ffc7..aa6064b2c84e1c5c5782256ae695846ad36bceb2 100644 (file)
@@ -743,13 +743,15 @@ h3 {
   flex-grow: 2;
 }
 
-.store-list .name {
-  font-weight: bold;
+.store-list .details>div {
+  margin-bottom: 4px;
 }
 
-.requirements {
-  margin-top: 0.5rem;
-  line-height: 1.3rem;
+.store-list .name {
+  font-weight: bold;
+  background: linear-gradient(90deg, #ecd4a8 0%, transparent 40%);
+  border-bottom: 2px solid #6d251c;
+  padding: 4px;
 }
 
 .requirement-title {
@@ -771,15 +773,39 @@ h3 {
   margin-right: 0.5rem;
 }
 
+
+.inventory-icon-wrapper {
+  position: relative;
+  margin-right: 0.5rem;
+}
+
 .inventory-icon {
   width: 64px;
   height: 64px;
-  ;
   padding: 0;
   background-repeat: no-repeat;
   background-size: contain;
   position: relative;
-  margin-right: 0.5rem;
+  border: solid 1px #6d251c;
+  box-sizing: border-box;
+}
+
+#inventory-section .store-list .name {
+  font-size: 0.9rem;
+}
+
+#inventory-section .store-list .requirements {
+  font-size: 0.8rem;
+}
+
+#inventory-section .store-list .requirements::before {
+  content: 'REQ: ';
+  font-weight: bold;
+  text-transform: uppercase;
+}
+
+#inventory-section .store-list .stat-mods {
+  font-size: 0.8rem;
 }
 
 .store-actions {
@@ -794,11 +820,13 @@ h3 {
 }
 
 .inventory-actions {
-  width: 74px;
+  width: 64px;
+  margin-top: 0.2rem;
 }
 
 .inventory-actions button {
   width: 100%;
+  font-size: 0.7rem;
   padding: 0.3rem 0.5rem;
 }
 
index 3a97a9df6ca7761ae134b1542f4061440df65727..b5a04b0949fe88f1be0d90d4960d25b24e983163 100644 (file)
-import {FightRound} from '../shared/fight';
-import { clearFight, loadMonster, getMonsterList, saveFightState, loadMonsterFromFight } from './monster';
-import { Player, expToLevel, maxHp, totalDefence, maxVigor, baseDamage } from '../shared/player';
-import { clearTravelPlan } from './map';
-import { updatePlayer } from './player';
-import { getEquippedItems, updateAp } from './inventory';
-import { EquippedItemDetails } from '../shared/equipped';
-import { EquipmentSlot } from '../shared/inventory';
-import { MonsterWithFaction, MonsterForFight, Fight } from '../shared/monsters';
-import { getPlayerSkillsAsObject, updatePlayerSkills } from './skills';
-import { SkillID, Skills } from '../shared/skills';
-import { Request, Response } from 'express';
-import * as Alert from './views/alert';
-import { addEvent } from './events';
-import { Professions } from '../shared/profession';
-
-export async function blockPlayerInFight(req: Request, res: Response, next: any) {
+import { FightRound } from "../shared/fight";
+import {
+  clearFight,
+  loadMonster,
+  getMonsterList,
+  saveFightState,
+  loadMonsterFromFight,
+} from "./monster";
+import {
+  Player,
+  expToLevel,
+  maxHp,
+  totalDefence,
+  maxVigor,
+  baseDamage,
+} from "../shared/player";
+import { clearTravelPlan } from "./map";
+import { updatePlayer } from "./player";
+import {
+  DbReturnInventoryExp,
+  getEquippedItems,
+  getInventoryBulk,
+  increaseExp,
+  updateAp,
+} from "./inventory";
+import { EquippedItemDetails } from "../shared/equipped";
+import { EquipmentSlot, levelFromExp } from "../shared/inventory";
+import { MonsterWithFaction, MonsterForFight, Fight } from "../shared/monsters";
+import { getPlayerSkillsAsObject, updatePlayerSkills } from "./skills";
+import { SkillID, Skills } from "../shared/skills";
+import { Request, Response } from "express";
+import * as Alert from "./views/alert";
+import { addEvent } from "./events";
+import { Professions } from "../shared/profession";
+
+export async function blockPlayerInFight(
+  req: Request,
+  res: Response,
+  next: any
+) {
   const fight = await loadMonsterFromFight(req.player.id);
-  if(!fight) {
+  if (!fight) {
     next();
     return;
   }
 
-  res.send(Alert.ErrorAlert(`You are currently in a fight with a ${fight.name}`));
+  res.send(
+    Alert.ErrorAlert(`You are currently in a fight with a ${fight.name}`)
+  );
 }
 
-function exponentialExp(exp: number, monsterLevel: number, playerLevel: number): number {
+function exponentialExp(
+  exp: number,
+  monsterLevel: number,
+  playerLevel: number
+): number {
   let finalExp = exp;
 
-  if((monsterLevel+3) < playerLevel) {
-    finalExp = Math.floor(exp * Math.pow(Math.E, ((monsterLevel + 3) - playerLevel)/5));
-  }
-  else if(monsterLevel > (playerLevel + 3)) {
-    finalExp = Math.floor(exp * Math.pow(Math.E, ((playerLevel + 3) - monsterLevel)/5));
+  if (monsterLevel + 3 < playerLevel) {
+    finalExp = Math.floor(
+      exp * Math.pow(Math.E, (monsterLevel + 3 - playerLevel) / 5)
+    );
+  } else if (monsterLevel > playerLevel + 1) {
+    finalExp = Math.floor(
+      exp * Math.pow(Math.E, (playerLevel + 3 - monsterLevel) / 5)
+    );
   }
 
   return Math.floor(finalExp);
 }
 
-export async function fightRound(player: Player, monster: Fight,  data: {action: 'attack' | 'cast' | 'flee'}) {
+async function increaseEquipmentLevel(
+  player_id: string,
+  items: DbReturnInventoryExp[]
+) {
+  if (items.length < 1) {
+    return;
+  }
+
+  const inventory = await getInventoryBulk(
+    player_id,
+    items.map((i) => i.item_id)
+  );
+
+  const updatedInventory = inventory.map((item, i) => {
+    const newLevel = levelFromExp(items[i].new_exp);
+    const updatedBoosts = {
+      strength: Math.ceil(item.boosts.strength * 1.1),
+      constitution: Math.ceil(item.boosts.constitution * 1.1),
+      dexterity: Math.ceil(item.boosts.dexterity * 1.1),
+      intelligence: Math.ceil(item.boosts.intelligence * 1.1),
+      damage: Math.ceil(item.boosts.damage * 1.1),
+      damage_mitigation: Math.ceil(item.boosts.damage_mitigation * 1.1),
+      defence: Math.ceil(item.boosts.defence * 1.1),
+    };
+    return { ...item, boosts: updatedBoosts, level: newLevel };
+  });
+
+  console.log(updatedInventory);
+}
+
+export async function fightRound(
+  player: Player,
+  monster: Fight,
+  data: { action: "attack" | "cast" | "flee" }
+) {
   const playerSkills = await getPlayerSkillsAsObject(player.id);
   const roundData: FightRound = {
     monster,
     player,
-    winner: 'in-progress',
+    winner: "in-progress",
     fightTrigger: monster.fight_trigger,
     roundDetails: [],
     rewards: {
       exp: 0,
       gold: 0,
-      levelIncrease: false
-    }
+      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 
+  // 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[] = [];
 
@@ -68,18 +134,21 @@ export async function fightRound(player: Player, monster: Fight,  data: {action:
     hp: 0,
   };
 
-  const equipment: Map<EquipmentSlot, EquippedItemDetails> = new Map<EquipmentSlot, EquippedItemDetails>();
+  const equipment: Map<EquipmentSlot, EquippedItemDetails> = new Map<
+    EquipmentSlot,
+    EquippedItemDetails
+  >();
   const weapons: EquippedItemDetails[] = [];
+  const increaseExpToEquipment: Record<string, number> = {};
   let anyDamageSpells: boolean = false;
-  equippedItems.forEach(item => {
-    if(item.type === 'ARMOUR') {
+  equippedItems.forEach((item) => {
+    increaseExpToEquipment[item.item_id] = 1;
+    if (item.type === "ARMOUR") {
       equipment.set(item.equipment_slot, item);
-    }
-    else if(item.type === 'WEAPON') {
+    } else if (item.type === "WEAPON") {
       weapons.push(item);
-    }
-    else if(item.type === 'SPELL') {
-      if(item.affectedSkills.includes('destruction_magic')) {
+    } else if (item.type === "SPELL") {
+      if (item.affectedSkills.includes("destruction_magic")) {
         anyDamageSpells = true;
       }
       weapons.push(item);
@@ -90,57 +159,74 @@ export async function fightRound(player: Player, monster: Fight,  data: {action:
     boost.dexterity += item.boosts.dexterity;
     boost.intelligence += item.boosts.intelligence;
 
-    if(item.type === 'SPELL' && item.affectedSkills.includes('restoration_magic')) {
+    if (
+      item.type === "SPELL" &&
+      item.affectedSkills.includes("restoration_magic")
+    ) {
       boost.hp += item.boosts.damage;
-    }
-    else {
+    } else {
       boost.damage += item.boosts.damage;
     }
   });
 
+  const equipmentExpGains = (
+    await increaseExp(player.id, increaseExpToEquipment)
+  ).filter((obj) => {
+    return levelFromExp(obj.previous_exp) < levelFromExp(obj.new_exp);
+  });
+  // check if any of the equipment levelled up
+  if (equipmentExpGains.length) {
+    // increment random stats
+    await increaseEquipmentLevel(player.id, equipmentExpGains);
+  }
+
   // @TODO implement flee based on dex + vigor
-  if(data.action === 'flee') {
-    roundData.roundDetails.push(`You managed to escape from the ${monster.name}!`)
-    roundData.winner = 'monster';
+  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 = attackType === 'physical' ? player.strength : player.intelligence;
-  const boostStat = attackType === 'physical' ? boost.strength : boost.intelligence;
+  const attackType = data.action === "attack" ? "physical" : "magical";
+  const primaryStat =
+    attackType === "physical" ? player.strength : player.intelligence;
+  const boostStat =
+    attackType === "physical" ? boost.strength : boost.intelligence;
 
   const playerDamage = baseDamage(primaryStat, boostStat, 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) {
+  weapons.forEach((item) => {
+    item.affectedSkills.forEach((id) => {
+      if (id === "restoration_magic") {
+        if (hpHealAfterMasteries < 0) {
           hpHealAfterMasteries = 0;
         }
         const skill = Skills.get(id);
-        if(skill) {
+        if (skill) {
           const playerSkill = playerSkills.get(id);
-          if(playerSkill) {
+          if (playerSkill) {
             hpHealAfterMasteries += skill.effect(playerSkill);
           }
         }
-      }
-      else {
+      } else {
         const skill = Skills.get(id);
-        if(skill) {
+        if (skill) {
           const playerSkill = playerSkills.get(id);
-          if(playerSkill) {
-            playerDamageAfterMasteries += playerDamage * skill.effect(playerSkill);
+          if (playerSkill) {
+            playerDamageAfterMasteries +=
+              playerDamage * skill.effect(playerSkill);
           }
         }
       }
 
-      if(!skillsUsed[id]) {
+      if (!skillsUsed[id]) {
         skillsUsed[id] = 0;
       }
       skillsUsed[id]++;
@@ -148,27 +234,31 @@ export async function fightRound(player: Player, monster: Fight,  data: {action:
   });
 
   await updatePlayerSkills(player.id, playerSkills, skillsUsed);
-
-  const playerFinalDamage = (data.action === 'cast' && !anyDamageSpells) ? 0 : Math.floor(playerDamage + playerDamageAfterMasteries);
+  const playerFinalDamage =
+    data.action === "cast" && !anyDamageSpells
+      ? 0
+      : Math.floor(playerDamage + playerDamageAfterMasteries);
   const playerFinalHeal = Math.floor(boost.hp + hpHealAfterMasteries);
 
   let monsterTakesDamage = playerFinalDamage - monster.defence;
-  if(monsterTakesDamage < 0) {
+  if (monsterTakesDamage < 0) {
     monsterTakesDamage = 0;
   }
-  roundData.roundDetails.push(`You dealt ${monsterTakesDamage} damage to the ${monster.name}!`);
+  roundData.roundDetails.push(
+    `You dealt ${monsterTakesDamage} damage to the ${monster.name}!`
+  );
 
   monster.hp -= monsterTakesDamage;
 
-  if(monster.hp <= 0) {
+  if (monster.hp <= 0) {
     roundData.monster.hp = 0;
-    roundData.winner = 'player';
+    roundData.winner = "player";
 
-    addEvent('MONSTER_KILLED', player.id, {
+    addEvent("MONSTER_KILLED", player.id, {
       monster_id: roundData.monster.ref_id,
       monster_name: roundData.monster.name,
       level: roundData.monster.level,
-      fightTrigger: roundData.monster.fight_trigger
+      fightTrigger: roundData.monster.fight_trigger,
     });
 
     const expGained = exponentialExp(monster.exp, monster.level, player.level);
@@ -179,46 +269,51 @@ export async function fightRound(player: Player, monster: Fight,  data: {action:
     player.gold += monster.gold;
     player.exp += expGained;
 
-    if(player.exp >= expToLevel(player.level + 1)) {
-      player.exp -= expToLevel(player.level + 1)
+    if (player.exp >= expToLevel(player.level + 1)) {
+      player.exp -= expToLevel(player.level + 1);
       player.level++;
-      addEvent('LEVEL_UP', player.id, {
-        from_level: player.level-1,
-        to_level: player.level
+      addEvent("LEVEL_UP", player.id, {
+        from_level: player.level - 1,
+        to_level: player.level,
       });
       roundData.rewards.levelIncrease = true;
       const statPointsGained = 1;
       const prof = Professions.get(player.profession);
 
-      if(player.level%2) {
-        if(prof.levelUpStatIncrease.odd) {
-          for(let stat in prof.levelUpStatIncrease.odd) {
+      if (player.level % 2) {
+        if (prof.levelUpStatIncrease.odd) {
+          for (let stat in prof.levelUpStatIncrease.odd) {
             player[stat] += prof.levelUpStatIncrease.odd[stat];
-            roundData.roundDetails.push(`You gained +${prof.levelUpStatIncrease.odd[stat]} ${stat}`);
+            roundData.roundDetails.push(
+              `You gained +${prof.levelUpStatIncrease.odd[stat]} ${stat}`
+            );
           }
         }
-      }
-      else {
-        if(prof.levelUpStatIncrease.even) {
-          for(let stat in prof.levelUpStatIncrease.even) {
+      } else {
+        if (prof.levelUpStatIncrease.even) {
+          for (let stat in prof.levelUpStatIncrease.even) {
             player[stat] += prof.levelUpStatIncrease.even[stat];
-            roundData.roundDetails.push(`You gained +${prof.levelUpStatIncrease.even[stat]} ${stat}`);
+            roundData.roundDetails.push(
+              `You gained +${prof.levelUpStatIncrease.even[stat]} ${stat}`
+            );
           }
         }
       }
 
       player.stat_points += statPointsGained;
 
-      roundData.roundDetails.push(`You gained ${statPointsGained} stat points!`);
+      roundData.roundDetails.push(
+        `You gained ${statPointsGained} stat points!`
+      );
 
       player.hp = maxHp(player.constitution, player.level);
       player.vigor = maxVigor(player.constitution, player.level);
     }
     // get the monster location if it was an EXPLORED fight
-    if(roundData.fightTrigger === 'explore') {
+    if (roundData.fightTrigger === "explore") {
       const rawMonster = await loadMonster(monster.ref_id);
-      const monsterList  = await getMonsterList(rawMonster.location_id);
-      potentialMonsters = monsterList.map(monster => {
+      const monsterList = await getMonsterList(rawMonster.location_id);
+      potentialMonsters = monsterList.map((monster) => {
         return {
           id: monster.id,
           name: monster.name,
@@ -226,68 +321,82 @@ export async function fightRound(player: Player, monster: Fight,  data: {action:
           maxLevel: monster.maxLevel,
           hp: monster.hp,
           maxHp: monster.maxHp,
-          fight_trigger: 'explore'
-        }
+          fight_trigger: "explore",
+        };
       });
     }
 
     player.vigor -= 1;
-    if(player.vigor < 0) {
+    if (player.vigor < 0) {
       player.vigor = 0;
     }
 
-    const unequippedItems = await updateAp(player.id, 1, equippedItems.map(i => i.item_id));
+    const unequippedItems = await updateAp(
+      player.id,
+      1,
+      equippedItems.map((i) => i.item_id)
+    );
     await clearFight(player.id);
     await updatePlayer(player);
 
-    if(unequippedItems.length) {
-      unequippedItems.forEach(i => {
-        roundData.roundDetails.push(`Your ${i.name} was too damaged and was unequipped!`);
+    if (unequippedItems.length) {
+      unequippedItems.forEach((i) => {
+        roundData.roundDetails.push(
+          `Your ${i.name} was too damaged and was unequipped!`
+        );
       });
     }
 
     return { roundData, monsters: potentialMonsters, player };
   }
 
-  let monsterDamage = (monster.strength*2) - boost.defence;
-  if(monsterDamage < 0) {
+  let monsterDamage = monster.strength * 2 - boost.defence;
+  if (monsterDamage < 0) {
     monsterDamage = 0;
   }
 
-  roundData.roundDetails.push(`The ${monster.name} hit you for ${monsterDamage} damage`);
+  roundData.roundDetails.push(
+    `The ${monster.name} hit you for ${monsterDamage} damage`
+  );
   player.hp -= monsterDamage;
 
-  if(playerFinalHeal > 0) {
+  if (playerFinalHeal > 0) {
     player.hp += playerFinalHeal;
-    if(player.hp > maxHp(player.constitution, player.level)) {
+    if (player.hp > maxHp(player.constitution, player.level)) {
       player.hp = maxHp(player.constitution, player.level);
     }
     roundData.roundDetails.push(`You healed for ${playerFinalHeal} HP`);
   }
 
-  if(player.hp <= 0) {
+  if (player.hp <= 0) {
     player.hp = 0;
     player.vigor = 0;
-    roundData.winner = 'monster';
+    roundData.winner = "monster";
 
     roundData.roundDetails.push(`You were killed by the ${monster.name}`);
 
     await clearFight(player.id);
-    const unequippedItems = await updateAp(player.id, 5, equippedItems.map(i => i.item_id));
+    const unequippedItems = await updateAp(
+      player.id,
+      5,
+      equippedItems.map((i) => i.item_id)
+    );
     await updatePlayer(player);
     await clearTravelPlan(player.id);
 
-    if(unequippedItems.length) {
-      unequippedItems.forEach(i => {
-        roundData.roundDetails.push(`Your ${i.name} was too damaged and was unequipped!`);
+    if (unequippedItems.length) {
+      unequippedItems.forEach((i) => {
+        roundData.roundDetails.push(
+          `Your ${i.name} was too damaged and was unequipped!`
+        );
       });
     }
 
-    return { roundData, monsters: [], player};
+    return { roundData, monsters: [], player };
   }
 
   await updatePlayer(player);
   await saveFightState(player.id, monster);
 
-  return { roundData, monsters: [], player};
-};
+  return { roundData, monsters: [], player };
+}
index 1ef723650b37605526157c54c17cb6d0717f6ffb..6c8d44acdaaaa6cdb37b3f1fe32930ebfa2278fb 100644 (file)
@@ -1,9 +1,15 @@
-import {InventoryItem, ShopEquipment} from "../shared/inventory";
+import {expToLevel, InventoryItem, levelFromExp, ShopEquipment} from "../shared/inventory";
 import { v4 as uuid } from 'uuid';
 import { db} from './lib/db';
 import {EquippedItemDetails} from "../shared/equipped";
 import { unequipItems } from "./equipment";
 
+export type DbReturnInventoryExp = {
+  item_id: string;
+  previous_exp: number;
+  new_exp: number;
+}
+
 
 export async function addInventoryItem(playerId: string, item: ShopEquipment) {
   const inventoryItem: InventoryItem = {
@@ -17,7 +23,6 @@ export async function addInventoryItem(playerId: string, item: ShopEquipment) {
     profession:  item.profession,
     icon: item.icon,
     current_exp: 0,
-    current_level: 1,
     requirements: {
       level: item.requirements.level,
       strength: item.requirements.strength,
@@ -59,6 +64,13 @@ export async function getInventory(player_id: string): Promise<EquippedItemDetai
   });
 }
 
+export async function getInventoryBulk(player_id: string, item_ids: string[]): Promise<InventoryItem[]> {
+  return db.select('*')
+    .from<InventoryItem>('inventory')
+    .where({ player_id })
+    .whereIn('item_id', item_ids);
+}
+
 export async function getInventoryItem(player_id: string, item_id: string): Promise<InventoryItem> {
   return db.select('*').from<InventoryItem>('inventory').where({
     player_id,
@@ -96,6 +108,55 @@ export async function updateAp(player_id: string, apDamage: number, itemIds: str
   return itemsToUnequip;
 }
 
+// the Payload is in the form itemId => amountOfExp
+export async function increaseExp(player_id: string, payload: Record<string, number>): Promise<DbReturnInventoryExp[]> {
+  const itemIds = Object.keys(payload);
+  
+  if (itemIds.length === 0) {
+    return;
+  }
+
+  const updates = await db('inventory')
+    .whereIn('item_id', itemIds)
+    .andWhere('player_id', player_id)
+    .update({
+      current_exp: db.raw('current_exp + CASE item_id ' +
+        itemIds.map(id => `WHEN '${id}' THEN ${payload[id]} `).join('') +
+        'ELSE 0 END')
+    })
+    .returning<DbReturnInventoryExp[]>(['item_id', 'current_exp as new_exp', db.raw('current_exp - CASE item_id ' +
+      itemIds.map(id => `WHEN '${id}' THEN ${payload[id]} `).join('') +
+      'ELSE 0 END as previous_exp')]);
+
+  const itemsToUpdate = updates.filter(item => {
+    const previousLevel = expToLevel(item.previous_exp);
+    const newLevel = levelFromExp(item.new_exp);
+
+    return previousLevel !== newLevel;
+  });
+
+  if(itemsToUpdate.length) {
+    const inventory = await getInventoryBulk(player_id, itemsToUpdate.map(i => i.item_id));
+
+    inventory.map(async item => {
+      const boosts = {...item.boosts};
+
+      Object.keys(item.boosts).forEach(boost => {
+        if(item.boosts[boost] > 0) {
+          boosts[boost] = Math.max(item.boosts[boost] + 1, Math.ceil(item.boosts[boost] * 1.2));
+        }
+      });
+
+      await db('inventory').where({
+        item_id: item.item_id
+      }).update({
+        boosts
+      });
+    });
+  }
+  return updates;
+}
+
 export async function repair(player_id: string, item_id: string) {
   return db('inventory').where({
     player_id,
index f3ffb89f7bf529cd21a7ae9ee7ab062a60e71c17..cb2208b98b670f6d58f13f61f5eef83ea3035c34 100644 (file)
@@ -5,7 +5,7 @@ import { capitalize } from "lodash";
 import { ProgressBar } from "./components/progress-bar";
 import { Player } from "../../shared/player";
 import { renderStatBoostWithPlayerIncrease } from "./components/stats";
-import { getDurabilityApproximation, expToLevel} from "../../shared/inventory";
+import { getDurabilityApproximation, expToLevel, levelFromExp } from "../../shared/inventory";
 import { slugify } from "../../shared/utils";
 
 function icon(icon_name?: string): string {
@@ -76,7 +76,7 @@ function renderInventoryItems(items: PlayerItem[]): string {
 
 function renderRequirement(name: string, val: number | string, currentVal?: number): string {
   let colorIndicator = '';
-  if(currentVal) {
+  if(currentVal && typeof val === 'number') {
     colorIndicator = currentVal >= val ? 'success' : 'error';
   }
   return `<span class="requirement-title">${name}</span>: <span class="requirement-value ${colorIndicator}">${val.toLocaleString()}</span>`;
@@ -91,19 +91,17 @@ function renderStatBoost(name: string, val: number | string): string {
 }
 
 function renderInventoryItem(player: Player, item: EquippedItemDetails , action: (item: EquippedItemDetails) => string): string {
+  const itemLevel = levelFromExp(item.current_exp);
   return `<div class="store-list">
-    <div class="inventory-icon ${slugify(getDurabilityApproximation(item))}" style="background-image: url('${icon(item.icon)}')" title="Durability: ${item.currentAp}/${item.maxAp} (${Math.round((item.currentAp / item.maxAp) * 100)}%)">
+    <div class="inventory-icon-wrapper">
+      <div class="inventory-icon ${slugify(getDurabilityApproximation(item))}" style="background-image: url('${icon(item.icon)}')" title="Durability: ${item.currentAp}/${item.maxAp} (${Math.round((item.currentAp / item.maxAp) * 100)}%)">
+      </div>
+      <div class="inventory-actions">
+        ${action(item)}
+      </div>
     </div>
     <div class="details">
       <div class="name">${item.name}</div>
-      <div class="requirements">
-        ${item.requirements.level ? renderRequirement('LVL', item.requirements.level): ''}
-        ${item.requirements.strength ? renderRequirement('STR', item.requirements.strength): ''}
-        ${item.requirements.constitution ? renderRequirement('CON', item.requirements.constitution): ''}
-        ${item.requirements.dexterity ? renderRequirement('DEX', item.requirements.dexterity): ''}
-        ${item.requirements.intelligence ? renderRequirement('INT', item.requirements.intelligence): ''}
-        ${renderRequirement('PRF', item.profession)}
-      </div>
       <div class="stat-mods">
         ${item.boosts.defence ? renderStatBoost('DEF', item.boosts.defence) : ''}
         ${item.boosts.strength ? renderStatBoost('STR', item.boosts.strength) : ''}
@@ -113,10 +111,21 @@ function renderInventoryItem(player: Player, item: EquippedItemDetails , action:
         ${item.boosts.damage ? renderStatBoostWithPlayerIncrease(player, item.affectedSkills.includes('restoration_magic') ? 'HP' : 'DMG', item) : ''}
         ${item.boosts.damage_mitigation ? renderStatBoost('MIT', item.boosts.damage_mitigation.toString())+'%' : ''}
       </div>
-      ${item.hasOwnProperty('id') ? `<div>${item.cost.toLocaleString()}G</div>` : ''}
+      <div class="requirements" title="Requirements">
+        ${item.requirements.level ? renderRequirement('LVL', item.requirements.level): ''}
+        ${item.requirements.strength ? renderRequirement('STR', item.requirements.strength): ''}
+        ${item.requirements.constitution ? renderRequirement('CON', item.requirements.constitution): ''}
+        ${item.requirements.dexterity ? renderRequirement('DEX', item.requirements.dexterity): ''}
+        ${item.requirements.intelligence ? renderRequirement('INT', item.requirements.intelligence): ''}
+        ${renderRequirement('PRF', item.profession)}
       </div>
-      <div class="inventory-actions">
-        ${action(item)}
+      ${ProgressBar(item.current_exp - expToLevel(itemLevel), expToLevel(itemLevel + 1) - expToLevel(itemLevel), `exp-${item.item_id}`, {
+        startingColor: '#7be67b',
+        endingColor: '#7be67b',
+        displayPercent: true,
+        title: `<b>Level ${itemLevel}:</b> `
+      })}
+      ${item.hasOwnProperty('id') ? `<div>${item.cost.toLocaleString()}G</div>` : ''}
       </div>
     </div>`;
 }
index d7eb879909290190f05fe1286698ebbd4636f7a9..2cb1da4417830fd49985795fe5d6f509607f3731 100644 (file)
@@ -32,7 +32,6 @@ export type InventoryItem = {
   count: number;
   icon: string;
   current_exp: number;
-  current_level: number;
   requirements: {
     level: number,
     strength: number,
@@ -69,7 +68,6 @@ export function repairCost(item: InventoryItem): number {
   return max([Math.floor(totalCost * damageRatio), 1]);
 }
 
-
 export function expToLevel(level: number): number {
   if (level <= 1) return 0;
   if (level <= 5) {
@@ -81,7 +79,19 @@ export function expToLevel(level: number): number {
     const growthFactor = 1.2;
     return Math.floor(baseExp * Math.pow(growthFactor, level - 5));
   }
+}
 
+export function levelFromExp(exp: number): number {
+  if (exp < 100) return 1;
+  if (exp < 400) {
+    // Linear growth for levels 1-5
+    return Math.floor(exp / 100) + 1;
+  } else {
+    // Exponential growth starting from level 5
+    const baseExp = 400;
+    const growthFactor = 1.2;
+    return Math.floor(Math.log(exp / baseExp) / Math.log(growthFactor)) + 5;
+  }
 }
 
 export function getDurabilityApproximation(item?: InventoryItem): DurabilityApproximation | "" {