From: xangelo Date: Wed, 30 Aug 2023 13:51:33 +0000 (-0400) Subject: chore(release): 0.3.0 X-Git-Tag: v0.3.0 X-Git-Url: https://git.xangelo.ca/?p=risinglegends.git;a=commitdiff_plain;h=v0.3.0;hp=v0.2.17;ds=sidebyside chore(release): 0.3.0 --- diff --git a/CHANGELOG.md b/CHANGELOG.md index a627b74..1c03855 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [0.3.0](https://git.xangelo.ca/?p=risinglegends.git;a=commitdiff;h=v0.3.0;hp=v0.2.17;ds=sidebyside) (2023-08-30) + + +### ⚠ BREAKING CHANGES + +* vigor mortensen + +### Features + +* display optimal level range for monsters 5878793 +* move alerts to bottom of main section 9575cfb +* repairing damaged equipment 161b5bf +* unequip items if they hit 0 ap in battle bc9e05f +* vigor mortensen f6aba7a + + +### Bug Fixes + +* spacing for stat increase button 61e6d07 + ### [0.2.17](https://git.xangelo.ca/?p=risinglegends.git;a=commitdiff;h=v0.2.17;hp=v0.2.16;ds=sidebyside) (2023-08-25) diff --git a/migrations/20230825165327_vigor.ts b/migrations/20230825165327_vigor.ts new file mode 100644 index 0000000..ad29194 --- /dev/null +++ b/migrations/20230825165327_vigor.ts @@ -0,0 +1,38 @@ +import { Knex } from "knex"; + + +const monsterColumns = ['helmAp', 'chestAp', 'armsAp', 'legsAp']; + +export async function up(knex: Knex): Promise { + return knex.schema.alterTable('players', function(table) { + table.integer('vigor').defaultTo(0); + }).alterTable('monsters', function(table) { + monsterColumns.forEach(col => { + table.dropColumn(col); + }); + table.integer('defence').notNullable().defaultTo(0); + }).alterTable('fight', function(table) { + monsterColumns.forEach(col => { + table.dropColumn(col); + }); + table.integer('defence').notNullable().defaultTo(0); + }); +} + + +export async function down(knex: Knex): Promise { + return knex.schema.alterTable('players', function(table) { + table.dropColumn('vigor'); + }).alterTable('monsters', function(table) { + monsterColumns.forEach(col => { + table.integer(col).defaultTo(0) + }); + table.dropColumn('defence'); + }).alterTable('fight', function(table) { + monsterColumns.forEach(col => { + table.integer(col).defaultTo(0) + }); + table.dropColumn('defence'); + }); +} + diff --git a/package-lock.json b/package-lock.json index 2915698..d404a7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rising-legends", - "version": "0.2.17", + "version": "0.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "rising-legends", - "version": "0.2.17", + "version": "0.3.0", "dependencies": { "@honeycombio/opentelemetry-node": "^0.4.0", "@opentelemetry/auto-instrumentations-node": "^0.37.0", diff --git a/package.json b/package.json index 8f5109f..5fc6242 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "rising-legends", "private": true, - "version": "0.2.17", + "version": "0.3.0", "scripts": { "up": "npx prisma migrate dev --name \"init\"", "start": "pm2 start dist/server/api.js", diff --git a/public/assets/css/game.css b/public/assets/css/game.css index 7afe629..b288eef 100644 --- a/public/assets/css/game.css +++ b/public/assets/css/game.css @@ -113,6 +113,9 @@ p:last-child { background-color: #fff; margin-bottom: 1rem; } +#announcements { + margin: 1rem 0; +} #signup { display: flex; @@ -155,9 +158,16 @@ dialog .close-modal { } } +#alerts { + position: relative; +} .alert { padding: 0.3rem; - margin-bottom: 0.3rem; + max-width: 17rem; + position: absolute; + bottom: 1rem; + right: 0; + line-height: 1.2rem; } .alert.success, button.success { border: solid 1px #0a0; @@ -363,15 +373,23 @@ nav.filter-result.active { padding: 1rem; } -#stat-breakdown th { +.stat-breakdown th { font-weight: bold; text-align: right; background-color: #6d251c; color: #fff; background-image: url(data:image/png;base64,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); } -#stat-breakdown th, #stat-breakdown td { +.stat-breakdown th, .stat-breakdown td { padding: 0.5rem; + min-width: 100px; + line-height: 1rem; +} +.stat-breakdown tr:nth-child(even) { + background-color: #c7b7a1; +} +.increase-stat { + padding: 1px 6px; } #explore { @@ -638,21 +656,31 @@ h3 { #skill-list { width: 100%; } -#skill-list tr:nth-child(even) { - background-color: #eee; -} #skill-list .skill-level { font-size: 2rem; vertical-align: middle; text-align: center; border: solid 1px #000; } +#skill-list .skill-details table { + width: 100%; +} +#skill-list .skill-title { + text-align: left; + padding: 0.6rem 0.6rem 0 0.6rem; + line-height: 1.2rem; + font-weight: bold; +} #skill-list .skill-description { - padding: 0 0.6rem; + padding: 0 0.6rem 0.6rem; line-height: 1.2rem; } #skill-list .skill-exp { - float: right; + text-align: right; + padding-right: 0.6rem; +} +#skill-list tr:nth-child(even) .skill-details { + background-color: #c7b7a1; } @@ -715,3 +743,30 @@ footer { margin-top: 2rem; text-align: center; } + +/* tooltip styling */ +@media(pointer: coarse), (hover: none) { + [title] { + position: realtive; + display: flex; + justify-content: center; + } + [title]:focus::after { + content: attr(title); + background-color: #fff; + color: #222; + font-size: 14px; + padding: 8px 12px; + max-height: 100px; + height: fit-content; + width: fit-content; + position: absolute; + text-align: center; + left: 50%; + transform: translate(-100%, 0%) scale(1); + transform-origin: top; + display: block; + box-shadow: 0 4px 14px 0 rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.05); + overflow: auto; + } +} diff --git a/public/index.html b/public/index.html index 777ee8d..144ad91 100644 --- a/public/index.html +++ b/public/index.html @@ -44,9 +44,7 @@ - -
+
diff --git a/seeds/monsters.ts b/seeds/monsters.ts index c9ca8b5..44afa4f 100644 --- a/seeds/monsters.ts +++ b/seeds/monsters.ts @@ -59,10 +59,7 @@ export async function createMonsters(): Promise { gold: r.fields.GOLD, hp: r.fields.HP, maxHp: r.fields.HP, - helmAp: r.fields.helmAp, - chestAp: r.fields.chestAp, - legsAp: r.fields.legsAp, - armsAp: r.fields.armsAm, + defence: Math.floor(parseInt(r.fields.Defence.toString() || '0')), location_id: r.fields.location_id[0], faction_id: factionId, time_period: r.fields.time_period ? r.fields.time_period : 'any' diff --git a/seeds/shop_items.ts b/seeds/shop_items.ts index 55e558d..6fca12a 100644 --- a/seeds/shop_items.ts +++ b/seeds/shop_items.ts @@ -35,7 +35,8 @@ export async function createShopEquipment(): Promise { dexterity: r.fields['Boost DEX'], intelligence: r.fields['Boost INT'], damage: r.fields['Boost DMG'], - damage_mitigation: r.fields['Damage Mitigation'] + damage_mitigation: r.fields['Damage Mitigation'], + defence: r.fields['Defence'], }, currentAp: r.fields['Armour Points'], maxAp: r.fields['Armour Points'], diff --git a/src/server/api.ts b/src/server/api.ts index 60c2b5c..77d54b2 100644 --- a/src/server/api.ts +++ b/src/server/api.ts @@ -14,7 +14,7 @@ import { logger } from './lib/logger'; import { loadPlayer, createPlayer, updatePlayer, movePlayer } from './player'; import { random, sample } from 'lodash'; import {broadcastMessage, Message} from '../shared/message'; -import {maxHp, Player} from '../shared/player'; +import {maxHp, maxVigor, 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'; @@ -28,8 +28,9 @@ import { getPlayerSkills} from './skills'; import { fightRound, blockPlayerInFight } from './fight'; -import { router as healerRouter } from './locations/healer'; +import { router as healerRouter } from './locations/healer'; import { router as professionRouter } from './locations/recruiter'; +import { router as repairRouter } from './locations/repair'; import * as Alert from './views/alert'; import { renderPlayerBar } from './views/player-bar' @@ -130,6 +131,7 @@ io.on('connection', async socket => { app.use(healerRouter); app.use(professionRouter); +app.use(repairRouter); app.get('/chat/history', authEndpoint, async (req: AuthRequest, res: Response) => { @@ -183,12 +185,12 @@ app.post('/chat', authEndpoint, async (req: AuthRequest, res: Response) => { }); app.get('/player', authEndpoint, async (req: AuthRequest, res: Response) => { - const inventory = await getEquippedItems(req.player.id); - - res.send(renderPlayerBar(req.player, inventory) + renderProfilePage(req.player)); + const equipment = await getEquippedItems(req.player.id); + res.send(renderPlayerBar(req.player) + renderProfilePage(req.player, equipment)); }); app.post('/player/stat/:stat', authEndpoint, async (req: AuthRequest, res: Response) => { + const equipment = await getEquippedItems(req.player.id); const stat = req.params.stat; if(!['strength', 'constitution', 'dexterity', 'intelligence'].includes(stat)) { res.send(Alert.ErrorAlert(`Sorry, that's not a valid stat to increase`)); @@ -203,10 +205,11 @@ app.post('/player/stat/:stat', authEndpoint, async (req: AuthRequest, res: Respo req.player.stat_points -= 1; req.player[stat]++; + req.player.hp = maxHp(req.player.constitution, req.player.level); + req.player.vigor = maxVigor(req.player.constitution, req.player.level); updatePlayer(req.player); - const equippedItems = await getEquippedItems(req.player.id); - res.send(renderPlayerBar(req.player, equippedItems) + renderProfilePage(req.player)); + res.send(renderPlayerBar(req.player) + renderProfilePage(req.player, equipment)); }); app.get('/player/skills', authEndpoint, async (req: AuthRequest, res: Response) => { @@ -273,7 +276,7 @@ app.post('/player/equip/:item_id/:slot', authEndpoint, blockPlayerInFight, async getPlayersItems(req.player.id) ]); - res.send(renderInventoryPage(inventory, items, inventoryItem.type) + renderPlayerBar(req.player, inventory)); + res.send(renderInventoryPage(inventory, items, inventoryItem.type) + renderPlayerBar(req.player)); }); app.post('/player/unequip/:item_id', authEndpoint, blockPlayerInFight, async (req: AuthRequest, res: Response) => { @@ -287,7 +290,7 @@ app.post('/player/unequip/:item_id', authEndpoint, blockPlayerInFight, async (re getPlayersItems(req.player.id) ]); - res.send(renderInventoryPage(inventory, items, item.type) + renderPlayerBar(req.player, inventory)); + res.send(renderInventoryPage(inventory, items, item.type) + renderPlayerBar(req.player)); }); app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response) => { @@ -299,7 +302,6 @@ app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response) closestTown = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id; } - const equippedItems = await getEquippedItems(req.player.id); if(fight) { const data: MonsterForFight = { id: fight.id, @@ -312,7 +314,7 @@ app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response) const location = await getMonsterLocation(fight.ref_id); - res.send(renderPlayerBar(req.player, equippedItems) + renderFightPreRound(data, true, location, closestTown)); + res.send(renderPlayerBar(req.player) + renderFightPreRound(data, true, location, closestTown)); } else { if(travelPlan) { @@ -327,7 +329,7 @@ app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response) // STEP_DELAY const nextAction = cache[`step:${req.player.id}`] || 0; - res.send(renderPlayerBar(req.player, equippedItems) + renderTravel({ + res.send(renderPlayerBar(req.player) + renderTravel({ things, nextAction, closestTown: closestTown, @@ -343,7 +345,7 @@ app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response) getAllPaths(req.player.city_id) ]); - res.send(renderPlayerBar(req.player, equippedItems) + await renderMap({city, locations, paths}, closestTown)); + res.send(renderPlayerBar(req.player) + await renderMap({city, locations, paths}, closestTown)); } } @@ -368,9 +370,7 @@ app.put('/location/:location_id/equipment/:item_id', authEndpoint, async (req: A await updatePlayer(req.player); await addInventoryItem(req.player.id, item); - const equippedItems = await getEquippedItems(req.player.id); - - res.send(renderPlayerBar(req.player, equippedItems) + Alert.SuccessAlert(`You purchased ${item.name}`)); + res.send(renderPlayerBar(req.player) + Alert.SuccessAlert(`You purchased ${item.name}`)); }); // used to purchase items from a particular shop @@ -392,9 +392,7 @@ app.put('/location/:location_id/items/:item_id', authEndpoint, async (req: AuthR await updatePlayer(req.player); await givePlayerItem(req.player.id, item.id, 1); - const equippedItems = await getEquippedItems(req.player.id); - - res.send(renderPlayerBar(req.player, equippedItems) + Alert.SuccessAlert(`You purchased a ${item.name}`)); + res.send(renderPlayerBar(req.player) + Alert.SuccessAlert(`You purchased a ${item.name}`)); }); // used to display equipment modals in a store, validates that @@ -491,12 +489,11 @@ app.put('/item/:item_id', authEndpoint, async (req: AuthRequest, res: Response) await updatePlayer(req.player); const inventory = await getInventory(req.player.id); - const equippedItems = inventory.filter(i => i.is_equipped); const items = await getPlayersItems(req.player.id); res.send( [ - renderPlayerBar(req.player, equippedItems), + renderPlayerBar(req.player), renderInventoryPage(inventory, items, 'ITEMS'), Alert.SuccessAlert(`You used the ${item.name}`) ].join("") @@ -621,8 +618,7 @@ app.post('/fight/turn', authEndpoint, async (req: AuthRequest, res: Response) => travelSection = travelButton(0); } - const equippedItems = await getEquippedItems(req.player.id); - const playerBar = renderPlayerBar(fightData.player, equippedItems); + const playerBar = renderPlayerBar(fightData.player); res.send(html + travelSection + playerBar); }); @@ -741,7 +737,6 @@ app.post('/travel/return-to-source', authEndpoint, async (req: AuthRequest, res: // doesn't matter if they don't have one // redirect them! await clearTravelPlan(req.player.id); - const equippedItems = await getEquippedItems(req.player.id); const fight = await loadMonsterFromFight(req.player.id); if(fight) { @@ -756,7 +751,7 @@ app.post('/travel/return-to-source', authEndpoint, async (req: AuthRequest, res: }; const location = await getMonsterLocation(fight.ref_id); - res.send(renderPlayerBar(req.player, equippedItems) + renderFightPreRound(data, true, location, req.player.city_id)); + res.send(renderPlayerBar(req.player) + renderFightPreRound(data, true, location, req.player.city_id)); } else { const [city, locations, paths] = await Promise.all([ @@ -765,7 +760,7 @@ app.post('/travel/return-to-source', authEndpoint, async (req: AuthRequest, res: getAllPaths(req.player.city_id) ]); - res.send(renderPlayerBar(req.player, equippedItems) + await renderMap({city, locations, paths}, req.player.city_id)); + res.send(renderPlayerBar(req.player) + await renderMap({city, locations, paths}, req.player.city_id)); } diff --git a/src/server/equipment.ts b/src/server/equipment.ts index 9ebac7d..d1a73ba 100644 --- a/src/server/equipment.ts +++ b/src/server/equipment.ts @@ -40,3 +40,9 @@ export async function unequip(player_id: string, item_id: string) { item_id }).delete(); } + +export async function unequipItems(player_id: string, item_ids: string[]) { + return db('equipped').where({ + player_id + }).whereIn('item_id', item_ids).delete(); +} diff --git a/src/server/fight.ts b/src/server/fight.ts index 3ceb706..2c9383d 100644 --- a/src/server/fight.ts +++ b/src/server/fight.ts @@ -1,11 +1,11 @@ import {FightRound} from '../shared/fight'; import { clearFight, loadMonster, getMonsterList, saveFightState, loadMonsterFromFight } from './monster'; -import { Player, expToLevel, maxHp } from '../shared/player'; +import { Player, expToLevel, maxHp, totalDefence, maxVigor } from '../shared/player'; import { clearTravelPlan } from './map'; import { updatePlayer } from './player'; -import { getEquippedItems, updateAp, deleteInventoryItem } from './inventory'; +import { getEquippedItems, updateAp } from './inventory'; import { EquippedItemDetails } from '../shared/equipped'; -import { ArmourEquipmentSlot, EquipmentSlot } from '../shared/inventory'; +import { EquipmentSlot } from '../shared/inventory'; import { MonsterWithFaction, MonsterForFight } from '../shared/monsters'; import { getPlayerSkillsAsObject, updatePlayerSkills } from './skills'; import { SkillID, Skills } from '../shared/skills'; @@ -57,27 +57,8 @@ export async function fightRound(player: Player, monster: MonsterWithFaction, d // 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 = { + defence: totalDefence(equippedItems, player), strength: 0, constitution: 0, dexterity: 0, @@ -116,9 +97,7 @@ export async function fightRound(player: Player, monster: MonsterWithFaction, d } }); - // 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. + // @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'; @@ -128,8 +107,8 @@ export async function fightRound(player: Player, monster: MonsterWithFaction, d } 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 primaryStat = attackType === 'physical' ? player.strength : player.intelligence; + const boostStat = attackType === 'physical' ? boost.strength : boost.intelligence; const playerDamage = Math.floor(((primaryStat + boostStat) * 1.3) + boost.damage); const skillsUsed: Record = {}; @@ -172,38 +151,13 @@ export async function fightRound(player: Player, monster: MonsterWithFaction, d 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; + let monsterTakesDamage = playerFinalDamage - monster.defence; + if(monsterTakesDamage < 0) { + monsterTakesDamage = 0; } + roundData.roundDetails.push(`You dealt ${monsterTakesDamage} damage to the ${monster.name}!`); - 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; - } + monster.hp -= monsterTakesDamage; if(monster.hp <= 0) { roundData.monster.hp = 0; @@ -232,6 +186,7 @@ export async function fightRound(player: Player, monster: MonsterWithFaction, d 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') { @@ -249,38 +204,32 @@ export async function fightRound(player: Player, monster: MonsterWithFaction, d }); } + player.vigor -= 1; + if(player.vigor < 0) { + player.vigor = 0; + } + + const unequippedItems = await updateAp(player.id, 1, equippedItems.map(i => i.item_id)); 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); + if(unequippedItems.length) { + unequippedItems.forEach(i => { + roundData.roundDetails.push(`Your ${i.name} was too damaged and was unequipped!`); + }); } + return { roundData, monsters: potentialMonsters, player }; } - else { - roundData.roundDetails.push(`The ${monster.name} hit you for ${monster.strength} damage`); - player.hp -= monster.strength; + + let monsterDamage = (monster.strength*2) - boost.defence; + if(monsterDamage < 0) { + monsterDamage = 0; } + roundData.roundDetails.push(`The ${monster.name} hit you for ${monsterDamage} damage`); + player.hp -= monsterDamage; + if(playerFinalHeal > 0) { player.hp += playerFinalHeal; if(player.hp > maxHp(player.constitution, player.level)) { @@ -289,18 +238,24 @@ export async function fightRound(player: Player, monster: MonsterWithFaction, d roundData.roundDetails.push(`You healed for ${playerFinalHeal} HP`); } - // update the players inventory for this item! - if(player.hp <= 0) { player.hp = 0; + player.vigor = 0; 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)); 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!`); + }); + } + return { roundData, monsters: [], player}; } diff --git a/src/server/inventory.ts b/src/server/inventory.ts index 88bfc87..3074644 100644 --- a/src/server/inventory.ts +++ b/src/server/inventory.ts @@ -2,6 +2,7 @@ import {InventoryItem, 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 async function addInventoryItem(playerId: string, item: ShopEquipment) { @@ -29,6 +30,7 @@ export async function addInventoryItem(playerId: string, item: ShopEquipment) { intelligence: item.boosts.intelligence, damage: item.boosts.damage, damage_mitigation: item.boosts.damage_mitigation, + defence: item.boosts.defence }, maxAp: item.maxAp, currentAp: item.currentAp, @@ -76,14 +78,29 @@ export async function getEquippedItems(player_id: string): Promise i.currentAp <= 0); + + if(itemsToUnequip.length) { + await unequipItems(player_id, itemsToUnequip.map(i => i.item_id)); + } + + return itemsToUnequip; +} + +export async function repair(player_id: string, item_id: string) { + return db('inventory').where({ player_id, item_id - }) + }).update({ + 'currentAp': db.raw('"maxAp"') + }); } export async function deleteInventoryItem(player_id: string, item_id: string) { diff --git a/src/server/locations/healer/index.ts b/src/server/locations/healer/index.ts index 3e658fe..16274a2 100644 --- a/src/server/locations/healer/index.ts +++ b/src/server/locations/healer/index.ts @@ -1,14 +1,12 @@ -import { Request, Response, Router } from "express"; -import { maxHp, Player } from "../../../shared/player"; +import { Response, Router } from "express"; +import { maxHp, maxVigor } from "../../../shared/player"; import { authEndpoint, AuthRequest } from '../../auth'; import { logger } from "../../lib/logger"; -import { loadPlayer, updatePlayer } from "../../player"; +import { updatePlayer } from "../../player"; import { getCityDetails, getService } from '../../map'; import { sample } from 'lodash'; import { City, Location } from "../../../shared/map"; import { renderPlayerBar } from "../../views/player-bar"; -import { getEquippedItems } from "../../inventory"; -import { EquippedItemDetails } from "../../../shared/equipped"; export const router = Router(); @@ -105,8 +103,8 @@ router.get('/city/services/healer/:location_id', authEndpoint, async (req: AuthR text.push(`

"${getText('intro', service, city)}"

`); - if(req.player.hp === maxHp(req.player.constitution, req.player.level)) { - text.push(`

You're already at full health?

`); + if(req.player.hp === maxHp(req.player.constitution, req.player.level) && req.player.vigor === maxVigor(req.player.constitution, req.player.level)) { + text.push(`

You're already in peak condition!

`); } else { if(req.player.gold <= (healCost * 2)) { @@ -145,17 +143,16 @@ router.post('/city/services/healer/heal/:location_id', authEndpoint, async (req: const text: string[] = []; const cost = req.player.gold <= (healCost * 2) ? 0 : healCost; - let inventory: EquippedItemDetails[]; if(req.player.gold < cost) { text.push(`

${getText('insufficient_money', service, city)}

`) } else { req.player.hp = maxHp(req.player.constitution, req.player.level); + req.player.vigor = maxVigor(req.player.constitution, req.player.level); req.player.gold -= cost; await updatePlayer(req.player); - inventory = await getEquippedItems(req.player.id); text.push(`

${getText('heal_successful', service, city)}

`); text.push('

'); @@ -169,6 +166,6 @@ router.post('/city/services/healer/heal/:location_id', authEndpoint, async (req: ${text.join("\n")} -${inventory ? renderPlayerBar(req.player, inventory) : ''} +${renderPlayerBar(req.player)} `); }); diff --git a/src/server/locations/recruiter.ts b/src/server/locations/recruiter.ts index 10623d8..4ce985c 100644 --- a/src/server/locations/recruiter.ts +++ b/src/server/locations/recruiter.ts @@ -5,7 +5,6 @@ import { logger } from "../lib/logger"; import * as Alert from "../views/alert"; import { changeProfession } from "../player"; import { renderPlayerBar } from "../views/player-bar"; -import { getEquippedItems } from "../inventory"; function p(str: string) { return `

${str}

`; @@ -108,10 +107,9 @@ router.post('/city/services/profession_change/:location_id', authEndpoint, async } if(update) { - const equipped = await getEquippedItems(req.player.id); req.player.level = update.level; req.player.exp = update.exp; - res.send(renderPlayerBar(req.player, equipped) + `
Congrats! You are now a ${req.player.profession}
`); + res.send(renderPlayerBar(req.player) + `
Congrats! You are now a ${req.player.profession}
`); } }); diff --git a/src/server/locations/repair.ts b/src/server/locations/repair.ts new file mode 100644 index 0000000..6bb7423 --- /dev/null +++ b/src/server/locations/repair.ts @@ -0,0 +1,70 @@ +import { Response, Router } from "express"; +import { authEndpoint, AuthRequest } from '../auth'; +import { logger } from "../lib/logger"; +import { getService } from "../map"; +import { getInventory, getInventoryItem, repair } from '../inventory'; +import { renderRepairService } from '../views/repair'; +import { repairCost } from "../../shared/inventory"; +import * as Alert from "../views/alert"; +import { updatePlayer } from "../player"; +import { renderPlayerBar } from "../views/player-bar"; + +export const router = Router(); + +router.get('/city/services/repair/:location_id', authEndpoint, async(req: AuthRequest, res: Response) => { + const service = await getService(parseInt(req.params.location_id)); + + if(!service || service.city_id !== req.player.city_id) { + logger.log(`Invalid location: [${req.params.location_id}]`); + res.sendStatus(400); + } + + const equippedItems = await getInventory(req.player.id); + + const damaged = equippedItems.filter(i => { + return i.currentAp < i.maxAp + }); + + res.send(renderRepairService(damaged, req.player, service)); +}); + +router.post('/city/services/:location_id/repair/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => { + const service = await getService(parseInt(req.params.location_id)); + + if(!service || service.city_id !== req.player.city_id) { + logger.log(`Invalid location: [${req.params.location_id}]`); + res.sendStatus(400); + } + + const item = await getInventoryItem(req.player.id, req.params.item_id); + + if(!item) { + logger.log(`Invalid item [${req.params.item_id}]`); + res.sendStatus(400); + } + + const cost = repairCost(item); + + if(req.player.gold < cost) { + res.status(400).send(Alert.ErrorAlert(`You need at least ${cost}G to repair your ${item.name}`)); + return; + } + + req.player.gold -= cost; + item.currentAp = item.maxAp; + + await updatePlayer(req.player); + await repair(req.player.id, item.item_id); + + const equippedItems = await getInventory(req.player.id); + + const damaged = equippedItems.filter(i => { + return i.currentAp < i.maxAp + }); + + res.send( + renderRepairService(damaged, req.player, service) + + renderPlayerBar(req.player) + + Alert.SuccessAlert(`You repaired your ${item.name} for ${cost}G`) + ); +}); diff --git a/src/server/monster.ts b/src/server/monster.ts index e58c814..943a868 100644 --- a/src/server/monster.ts +++ b/src/server/monster.ts @@ -73,10 +73,7 @@ export async function createFight(playerId: string, monster: Monster, fightTrigg level: monster.level, gold: monster.gold, hp: monster.hp, - helmAp: monster.helmAp, - chestAp: monster.chestAp, - legsAp: monster.legsAp, - armsAp: monster.armsAp, + defence: monster.defence, maxHp: monster.maxHp, ref_id: monster.id, fight_trigger: fightTrigger diff --git a/src/server/player.ts b/src/server/player.ts index ba4d570..527069b 100644 --- a/src/server/player.ts +++ b/src/server/player.ts @@ -72,6 +72,7 @@ export async function updatePlayer(player: Player) { id: player.id }).update({ hp: player.hp, + vigor: player.vigor, strength: player.strength, constitution: player.constitution, dexterity: player.dexterity, diff --git a/src/server/views/components/city.ts b/src/server/views/components/city.ts new file mode 100644 index 0000000..18eb3e2 --- /dev/null +++ b/src/server/views/components/city.ts @@ -0,0 +1,12 @@ +export function Title(str: string): string { + return `
${str}
`; +} + +export function Details(name: string, content: string): string { + return ` +
+

${name}

+ ${content} +
+ `; +} diff --git a/src/server/views/components/progress-bar.ts b/src/server/views/components/progress-bar.ts index 7d7677d..9d9a290 100644 --- a/src/server/views/components/progress-bar.ts +++ b/src/server/views/components/progress-bar.ts @@ -1,14 +1,24 @@ export interface ProgressBarOptions { startingColor: string; - endingColor: string; + endingColor?: string; + title?: string + displayPercent?: boolean; } export function ProgressBar(current: number, max: number, id: string, opts: ProgressBarOptions) { + const endingColor = opts.endingColor ?? opts.startingColor; + const title = opts.title ?? ''; + const display = [`${current}/${max}`]; let percent = 0; + if(max > 0) { percent = Math.floor((current / max) * 100); } - return `
${current}/${max} - ${percent}%
`; + if(opts.displayPercent) { + display.push(`${percent}%`); + } + + return `
${title} ${display.join(" - ")}
`; } diff --git a/src/server/views/inventory.ts b/src/server/views/inventory.ts index adf4105..e21d1fd 100644 --- a/src/server/views/inventory.ts +++ b/src/server/views/inventory.ts @@ -106,13 +106,14 @@ function renderInventoryItem(item: EquippedItemDetails , action: (item: Equipped ${renderRequirement('PRF', item.profession)}
+ ${item.boosts.defence ? renderStatBoost('DEF', item.boosts.defence) : ''} ${item.boosts.strength ? renderStatBoost('STR', item.boosts.strength) : ''} ${item.boosts.constitution ? renderStatBoost('CON', item.boosts.constitution) : ''} ${item.boosts.dexterity ? renderStatBoost('DEX', item.boosts.dexterity) : ''} ${item.boosts.intelligence ? renderStatBoost('INT', item.boosts.intelligence) : ''} ${item.boosts.damage ? renderStatBoost('DMG', item.boosts.damage) : ''} ${item.boosts.damage_mitigation ? renderStatBoost('MIT', item.boosts.damage_mitigation.toString())+'%' : ''} - ${['WEAPON','SPELL'].includes(item.type) ? '': generateProgressBar(item.currentAp, item.maxAp, '#7be67b')} + ${['SPELL'].includes(item.type) ? '': generateProgressBar(item.currentAp, item.maxAp, '#7be67b')}
${item.hasOwnProperty('id') ? `
${item.cost.toLocaleString()}G
` : ''} diff --git a/src/server/views/monster-selector.ts b/src/server/views/monster-selector.ts index 24140c6..58c8665 100644 --- a/src/server/views/monster-selector.ts +++ b/src/server/views/monster-selector.ts @@ -1,3 +1,4 @@ +import { max } from "lodash"; import { LocationWithCity } from "../../shared/map"; import { Monster, MonsterForFight } from "../../shared/monsters"; @@ -21,7 +22,8 @@ export function renderMonsterSelector(monsters: Monster[] | MonsterForFight[], a `; diff --git a/src/server/views/player-bar.ts b/src/server/views/player-bar.ts index 9092eee..560a71b 100644 --- a/src/server/views/player-bar.ts +++ b/src/server/views/player-bar.ts @@ -1,6 +1,5 @@ -import { EquippedItemDetails } from "shared/equipped"; -import { EquipmentSlot } from "shared/inventory"; -import { expToLevel, maxHp, Player } from "../../shared/player"; +import { expToLevel, maxHp, maxVigor, Player } from "../../shared/player"; +import { ProgressBar } from "./components/progress-bar"; function displayLoginSignupForm(): string { return ` @@ -25,71 +24,16 @@ function displayLoginSignupForm(): string { } -function generateProgressBar(current: number, max: number, opts: ProgressBarOptions): string { - let percent = 0; - if(max > 0) { - percent = Math.floor((current / max) * 100); - } - const display = `${percent}% - `; - return `
${display}${current}/${max}
`; -} - -function calcAp(inventoryItem: EquippedItemDetails[]): string { - const ap: Record = {}; - inventoryItem.forEach(item => { - if(item.is_equipped && item.type === 'ARMOUR') { - ap[item.equipment_slot] = { - currentAp: item.currentAp, - maxAp: item.maxAp - }; - } - }); - - return ` -
- - ${generateProgressBar(ap.HEAD?.currentAp || 0, ap.HEAD?.maxAp || 0, { startingColor: '#5ebb5e', endingColor: '#7be67b'})} -
-
- - ${generateProgressBar(ap.ARMS?.currentAp || 0, ap.ARMS?.maxAp || 0, { startingColor: '#5ebb5e', endingColor: '#7be67b'})} -
-
- - ${generateProgressBar(ap.CHEST?.currentAp || 0, ap.CHEST?.maxAp || 0, { startingColor: '#5ebb5e', endingColor: '#7be67b'})} -
-
- - ${generateProgressBar(ap.LEGS?.currentAp || 0, ap.LEGS?.maxAp || 0, { startingColor: '#5ebb5e', endingColor: '#7be67b'})} -
-`; -} - -interface ProgressBarOptions { - startingColor: string; - endingColor: string; -} - -function progressBar(current: number, max: number, id: string, opts: ProgressBarOptions) { - let percent = 0; - if(max > 0) { - percent = Math.floor((current / max) * 100); - } - - return `
${current}/${max} - ${percent}%
`; -} - -export function renderPlayerBar(player: Player, inventory: EquippedItemDetails[]): string { +export function renderPlayerBar(player: Player): string { return `
${player.username}, level ${player.level} ${player.profession}
${player.gold.toLocaleString()}
-
${calcAp(inventory)}
- ${progressBar(player.hp, maxHp(player.constitution, player.level), 'hp-bar', { endingColor: '#ff7070', startingColor: '#d62f2f' })} - ${progressBar(player.exp, expToLevel(player.level + 1), 'exp-bar', { endingColor: '#5997f9', startingColor: '#1d64d4'})} + ${ProgressBar(player.hp, maxHp(player.constitution, player.level), 'hp-bar', { endingColor: '#ff7070', startingColor: '#d62f2f', title: 'HP', displayPercent: true })} + ${ProgressBar(player.vigor, maxVigor(player.constitution, player.level), 'vigor-bar', { endingColor: '#5ebb5e', startingColor: '#7be67b', title: 'Vigor', displayPercent: true})} + ${ProgressBar(player.exp, expToLevel(player.level + 1), 'exp-bar', { endingColor: '#5997f9', startingColor: '#1d64d4', title: 'EXP', displayPercent: true})}
${player.account_type === 'session' ? displayLoginSignupForm() : ''} `; diff --git a/src/server/views/profile.ts b/src/server/views/profile.ts index 93a9bf7..547cdef 100644 --- a/src/server/views/profile.ts +++ b/src/server/views/profile.ts @@ -1,27 +1,54 @@ -import { Player, StatDef, StatDisplay } from "../../shared/player"; +import { EquippedItemDetails } from "../../shared/equipped"; +import { expToLevel, maxHp, maxVigor, Player, StatDef, StatDisplay, totalDefence } from "../../shared/player"; function statPointIncreaser(stat: StatDisplay) { return ``; } -export function renderProfilePage(player: Player): string { + +export function renderProfilePage(player: Player, equipment: EquippedItemDetails[]): string { + let statBreakdown = ''; StatDef.forEach(stat => { statBreakdown += ` - ${stat.display} + ${stat.display} - ${player[stat.id]} + ${player[stat.id].toLocaleString()} ${player.stat_points ? statPointIncreaser(stat) : ''} `; }); const html = `
- +
+ + + + + + + + + + + + + + + + + + + + ${statBreakdown} -
HP${player.hp.toLocaleString()}/${maxHp(player.constitution, player.level).toLocaleString()}
Vigor${player.vigor.toLocaleString()}/${maxVigor(player.constitution, player.level).toLocaleString()}
EXP${player.exp.toLocaleString()}/${expToLevel(player.level + 1).toLocaleString()}
Defence${totalDefence(equipment, player, false).toLocaleString()} (${totalDefence(equipment, player).toLocaleString()})
Stat Points${player.stat_points}
Stat Points${player.stat_points}
-
`; + +
+

Hi, thanks for checking out this VERY early build of Rising Legends.

+

If you have any questions or run into any bugs, feel free to drop an email on our mailing list: ~xangelo/rising-legends-discuss@lists.sr.ht +

+`; return html; } diff --git a/src/server/views/repair.ts b/src/server/views/repair.ts new file mode 100644 index 0000000..447dc84 --- /dev/null +++ b/src/server/views/repair.ts @@ -0,0 +1,115 @@ +import { capitalize } from "lodash"; +import { Player } from "../../shared/player"; +import { repairCost } from "../../shared/inventory"; +import { LocationWithCity } from "../../shared/map"; +import { EquippedItemDetails } from "../../shared/equipped"; +import { ProgressBar } from "./components/progress-bar"; +import * as City from './components/city'; + +function renderStatBoost(name: string, val: number | string): string { + let valSign: string = ''; + if(typeof val === 'number') { + valSign = val > 0 ? '+' : '-'; + } + return `${name}: ${valSign}${val}`; +} + +function renderRequirement(name: string, val: number | string, currentVal?: number): string { + let colorIndicator = ''; + if(currentVal) { + colorIndicator = currentVal >= val ? 'success' : 'error'; + } + return `${name}: ${val.toLocaleString()}`; +} + +export function renderEquipmentDetails(item: EquippedItemDetails, player: Player): string { + return ` +
+
${item.name}${item.equipment_slot === 'TWO_HANDED' ? ' (2H)': ''}
+
+ ${item.requirements.level ? renderRequirement('LVL', item.requirements.level, player.level): ''} + ${item.requirements.strength ? renderRequirement('STR', item.requirements.strength, player.strength): ''} + ${item.requirements.constitution ? renderRequirement('CON', item.requirements.constitution, player.constitution): ''} + ${item.requirements.dexterity ? renderRequirement('DEX', item.requirements.dexterity, player.dexterity): ''} + ${item.requirements.intelligence ? renderRequirement('INT', item.requirements.intelligence, player.intelligence): ''} + ${renderRequirement('PRF', item.profession)} +
+
+ ${item.boosts.defence ? renderStatBoost('DEF', item.boosts.defence) : ''} + ${item.boosts.strength ? renderStatBoost('STR', item.boosts.strength) : ''} + ${item.boosts.constitution ? renderStatBoost('CON', item.boosts.constitution) : ''} + ${item.boosts.dexterity ? renderStatBoost('DEX', item.boosts.dexterity) : ''} + ${item.boosts.intelligence ? renderStatBoost('INT', item.boosts.intelligence) : ''} + ${item.boosts.damage ? renderStatBoost(item.affectedSkills.includes('restoration_magic') ? 'HP' : 'DMG', item.boosts.damage) : ''} + ${item.boosts.damage_mitigation ? renderStatBoost('MIT', item.boosts.damage_mitigation.toString())+'%' : ''} + ${['WEAPON','SPELL'].includes(item.type) ? '' : ProgressBar(item.currentAp, item.maxAp, `${item.item_id}-ap`, { +displayPercent: false, +title: 'Durability', +startingColor: '#7be67b', +endingColor: '#7be67b' +})} +
+
${repairCost(item).toLocaleString()}G to Repair
+
+` + +} + +function renderEquipmentToRepair(item: EquippedItemDetails, action: (item: EquippedItemDetails) => string, player: Player): string { + return `
+
+
${action(item)}
+
+ ${renderEquipmentDetails(item, player)} +
`; +} + + + +export function renderRepairService(equipment: EquippedItemDetails[], player: Player, location: LocationWithCity): string { + const listing: Record = {}; + const listingTypes = new Set(); + + if(equipment.length === 0) { + return ` + ${City.Title(location.city_name)} + ${City.Details(location.name, `You don't have any equipment that needs repairing.`)} + `; + } + + equipment.forEach(item => { + const filter = item.type === 'ARMOUR' ? item.equipment_slot : item.type; + + listingTypes.add(filter); + if(!listing[filter]) { + listing[filter] = ''; + } + + listing[filter] += renderEquipmentToRepair(item, i => { + return `` + }, player); + + }); + + let activeTab: string = listingTypes.keys().next().value; + + const nav: string[] = []; + const finalListing: string[] = []; + + listingTypes.forEach(type => { + nav.push(`${capitalize(type)}`); + finalListing.push(`
${listing[type]}
`); + }); + + let html = ` + ${City.Title(location.city_name)} + ${City.Details(location.name, ` +
+
+ ${finalListing.join("\n")} +
+
+`)}`; + + return html; +} diff --git a/src/server/views/skills.ts b/src/server/views/skills.ts index 0c11815..8b585f2 100644 --- a/src/server/views/skills.ts +++ b/src/server/views/skills.ts @@ -7,12 +7,22 @@ export function renderSkills(skills: Skill[]): string { const percent = skill.exp / definition.expToLevel(skill.level + 1); return ` - ${skill.level.toLocaleString()} - - ${(percent * 100).toPrecision(2)}% to next level - ${definition.display} -

${definition.description}

- + ${skill.level.toLocaleString()} + + + + + + + + + +
+ ${definition.display} + + ${(percent * 100).toPrecision(2)}% to next level +
${definition.description}
+ `; }).join("\n")} diff --git a/src/server/views/stores.ts b/src/server/views/stores.ts index 81b2273..be20cf0 100644 --- a/src/server/views/stores.ts +++ b/src/server/views/stores.ts @@ -1,15 +1,33 @@ import { ShopEquipment } from "../../shared/inventory"; import { ShopItem, Item } from "../../shared/items"; -import { capitalize } from "lodash"; +import { capitalize, merge } from "lodash"; import { Player } from "../../shared/player"; import { LocationWithCity } from "shared/map"; +import { ProgressBar } from "./components/progress-bar"; -function renderStatBoost(name: string, val: number | string): string { +type RenderStatOptions = { + unsigned: boolean +} + +function renderStat(title: string, display: string, val: any, options?: RenderStatOptions): string { + const opts = merge({ + unsigned: false + } as RenderStatOptions, options); + + let valSign: string = ''; + if(typeof val === 'number') { + valSign = val > 0 ? '+' : '-'; + } + + return `${display}: ${opts.unsigned ? val : `${valSign}${val}`}`; +} + +function renderStatBoost(name: string, val: number | string, title?: string): string { let valSign: string = ''; if(typeof val === 'number') { valSign = val > 0 ? '+' : '-'; } - return `${name}: ${valSign}${val}`; + return `${name}: ${valSign}${val}`; } function renderRequirement(name: string, val: number | string, currentVal?: number): string { @@ -48,13 +66,14 @@ export function renderEquipmentDetails(item: ShopEquipment, player: Player): str ${renderRequirement('PRF', item.profession)}
+ ${item.boosts.defence ? renderStatBoost('DEF', item.boosts.defence) : ''} ${item.boosts.strength ? renderStatBoost('STR', item.boosts.strength) : ''} ${item.boosts.constitution ? renderStatBoost('CON', item.boosts.constitution) : ''} ${item.boosts.dexterity ? renderStatBoost('DEX', item.boosts.dexterity) : ''} ${item.boosts.intelligence ? renderStatBoost('INT', item.boosts.intelligence) : ''} ${item.boosts.damage ? renderStatBoost(item.affectedSkills.includes('restoration_magic') ? 'HP' : 'DMG', item.boosts.damage) : ''} ${item.boosts.damage_mitigation ? renderStatBoost('MIT', item.boosts.damage_mitigation.toString())+'%' : ''} - ${['WEAPON','SPELL'].includes(item.type) ? '' : renderStatBoost('AP', item.maxAp.toString())} + ${['SPELL'].includes(item.type) ? '' : renderStat('Durability', 'DUR', item.maxAp, { unsigned: true })}
${item.hasOwnProperty('id') ? `
${item.cost.toLocaleString()}G
` : ''} diff --git a/src/shared/inventory.ts b/src/shared/inventory.ts index cb42200..0ed26dd 100644 --- a/src/shared/inventory.ts +++ b/src/shared/inventory.ts @@ -33,6 +33,7 @@ export type InventoryItem = { intelligence: number; damage: number; damage_mitigation: number; + defence: number; } currentAp: number; maxAp: number; @@ -45,3 +46,11 @@ export type ShopEquipment = Omit & { id: number; location_id: number; }; + +export function repairCost(item: InventoryItem): number { + const totalCost = item.cost * 0.7; + + const damageRatio = 1 - (item.currentAp / item.maxAp); + + return Math.floor(totalCost * damageRatio); +} diff --git a/src/shared/monsters.ts b/src/shared/monsters.ts index c2a5fce..c29a477 100644 --- a/src/shared/monsters.ts +++ b/src/shared/monsters.ts @@ -11,10 +11,7 @@ export type Monster = { gold: number; exp: number; hp: number; - helmAp: number; - chestAp: number; - armsAp: number; - legsAp: number; + defence: number; maxHp: number; location_id: number; faction_id: number; diff --git a/src/shared/player.ts b/src/shared/player.ts index ca94496..b7c172a 100644 --- a/src/shared/player.ts +++ b/src/shared/player.ts @@ -1,6 +1,7 @@ import { Profession } from './profession'; import { Stat } from './stats'; import { SkillDefinition, Skill } from './skills'; +import { EquippedItemDetails } from './equipped'; export type Player = { id: string, @@ -19,6 +20,7 @@ export type Player = { hp: number; city_id: number; stat_points: number; + vigor: number; } export type PlayerWithSkills = Player & { @@ -29,6 +31,10 @@ export function maxHp(constitution: number, playerLevel: number): number { return Math.ceil((constitution * 1.7) + (playerLevel * 1.3)); } +export function maxVigor(constitution: number, playerLevel: number): number { + return Math.ceil((constitution * 3.8) + (playerLevel * 1.5)); +} + export function expToLevel(level: number): number { if(level < 10) { return level * 10 - 10; @@ -38,11 +44,28 @@ export function expToLevel(level: number): number { } } +export function totalDefence(equippedItems: EquippedItemDetails[], player: Player, accountForVigor: boolean = true): number { + const vigorPercent = player.vigor / maxVigor(player.constitution, player.level); + + const totalDefence = equippedItems.reduce((acc, curr) => { + let defence = curr.boosts.defence ?? 0; + return acc += defence; + }, 0); + + if(accountForVigor) { + return Math.ceil(totalDefence * vigorPercent); + } + else { + return totalDefence; + } +} + export type StatDisplay = { id: Stat, display: string; abbrv: string; + description: string; } export const StatDef: Map = new Map(); @@ -50,23 +73,27 @@ export const StatDef: Map = new Map(); StatDef.set(Stat.strength, { id: Stat.strength, display: 'Strength', - abbrv: 'STR' + abbrv: 'STR', + description: 'Affects your melee damage' }); StatDef.set(Stat.constitution, { id: Stat.constitution, display: 'Constitution', - abbrv: 'CON' + abbrv: 'CON', + description: 'Affects your max HP and Vigor' }); StatDef.set(Stat.dexterity, { id: Stat.dexterity, display: 'Dexterity', - abbrv: 'DEX' + abbrv: 'DEX', + description: 'Affects you ability to dodge attacks double-hit' }); StatDef.set(Stat.intelligence, { id: Stat.intelligence, display: 'Intelligence', - abbrv: 'INT' + abbrv: 'INT', + description: 'Affects your magical damage' });