From edb355e76e09afa7cbb327fe8a663875cfde9201 Mon Sep 17 00:00:00 2001 From: xangelo Date: Tue, 12 Nov 2024 13:21:05 -0500 Subject: [PATCH] feat: equipment exp 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 | 2 - public/assets/css/game.css | 44 ++- src/server/fight.ts | 323 ++++++++++++++------- src/server/inventory.ts | 65 ++++- src/server/views/inventory.ts | 37 ++- src/shared/inventory.ts | 14 +- 6 files changed, 350 insertions(+), 135 deletions(-) diff --git a/migrations/20241021184333_equipment-exp.ts b/migrations/20241021184333_equipment-exp.ts index cc2da58..21180de 100644 --- a/migrations/20241021184333_equipment-exp.ts +++ b/migrations/20241021184333_equipment-exp.ts @@ -5,7 +5,6 @@ export async function up(knex: Knex): Promise { 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 { return knex.schema.alterTable('inventory', t => { t.dropTimestamps(); t.dropColumn('current_exp'); - t.dropColumn('current_level'); }); } diff --git a/public/assets/css/game.css b/public/assets/css/game.css index 05fc248..aa6064b 100644 --- a/public/assets/css/game.css +++ b/public/assets/css/game.css @@ -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; } diff --git a/src/server/fight.ts b/src/server/fight.ts index 3a97a9d..b5a04b0 100644 --- a/src/server/fight.ts +++ b/src/server/fight.ts @@ -1,60 +1,126 @@ -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 = new Map(); + const equipment: Map = new Map< + EquipmentSlot, + EquippedItemDetails + >(); const weapons: EquippedItemDetails[] = []; + const increaseExpToEquipment: Record = {}; 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 = {}; 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 }; +} diff --git a/src/server/inventory.ts b/src/server/inventory.ts index 1ef7236..6c8d44a 100644 --- a/src/server/inventory.ts +++ b/src/server/inventory.ts @@ -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 { + return db.select('*') + .from('inventory') + .where({ player_id }) + .whereIn('item_id', item_ids); +} + export async function getInventoryItem(player_id: string, item_id: string): Promise { return db.select('*').from('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): Promise { + 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(['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, diff --git a/src/server/views/inventory.ts b/src/server/views/inventory.ts index f3ffb89..cb2208b 100644 --- a/src/server/views/inventory.ts +++ b/src/server/views/inventory.ts @@ -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 `${name}: ${val.toLocaleString()}`; @@ -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 `
-
+
+
+
+
+ ${action(item)} +
${item.name}
-
- ${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)} -
${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())+'%' : ''}
- ${item.hasOwnProperty('id') ? `
${item.cost.toLocaleString()}G
` : ''} +
+ ${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)}
-
- ${action(item)} + ${ProgressBar(item.current_exp - expToLevel(itemLevel), expToLevel(itemLevel + 1) - expToLevel(itemLevel), `exp-${item.item_id}`, { + startingColor: '#7be67b', + endingColor: '#7be67b', + displayPercent: true, + title: `Level ${itemLevel}: ` + })} + ${item.hasOwnProperty('id') ? `
${item.cost.toLocaleString()}G
` : ''}
`; } diff --git a/src/shared/inventory.ts b/src/shared/inventory.ts index d7eb879..2cb1da4 100644 --- a/src/shared/inventory.ts +++ b/src/shared/inventory.ts @@ -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 | "" { -- 2.25.1