From f6aba7a783ae455663723e7bfad39be6a5396c87 Mon Sep 17 00:00:00 2001 From: xangelo Date: Fri, 25 Aug 2023 16:13:04 -0400 Subject: [PATCH] feat!: vigor mortensen This introduces the new vigor system which replaces the previous armour point system. The new Vigor system introduces a new stat based on constitution that comprises your "vigor". At 100% vigor your defence + damage are at their highest possible base values. As you fight your vigor drops (until you visit a healer). As it drops it starts affecting your defence + damage negatively. Armour Points still exist, but have been converted into a "durability" system which goes down much slower per fight and a bit more drastically if you die. However, nowhere near the same rate as before. As such, mitigation no longer has any effect. --- migrations/20230825165327_vigor.ts | 38 +++++++ public/assets/css/game.css | 59 +++++++++-- public/index.html | 1 - seeds/monsters.ts | 5 +- seeds/shop_items.ts | 3 +- src/server/api.ts | 43 ++++---- src/server/fight.ts | 108 +++++--------------- src/server/inventory.ts | 15 ++- src/server/locations/healer/index.ts | 17 ++- src/server/locations/recruiter.ts | 4 +- src/server/monster.ts | 5 +- src/server/player.ts | 1 + src/server/views/components/progress-bar.ts | 16 ++- src/server/views/inventory.ts | 1 + src/server/views/player-bar.ts | 68 ++---------- src/server/views/profile.ts | 41 ++++++-- src/server/views/skills.ts | 22 ++-- src/server/views/stores.ts | 1 + src/shared/inventory.ts | 1 + src/shared/monsters.ts | 5 +- src/shared/player.ts | 35 ++++++- 21 files changed, 257 insertions(+), 232 deletions(-) create mode 100644 migrations/20230825165327_vigor.ts 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/public/assets/css/game.css b/public/assets/css/game.css index 7afe629..c1ab83b 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; @@ -363,15 +366,20 @@ 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; } #explore { @@ -638,21 +646,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.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 +733,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..5e809aa 100644 --- a/public/index.html +++ b/public/index.html @@ -44,7 +44,6 @@ -
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..61e7f5c 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'; @@ -183,12 +183,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 +203,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 +274,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 +288,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 +300,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 +312,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 +327,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 +343,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 +368,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 +390,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 +487,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 +616,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 +735,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 +749,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 +758,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/fight.ts b/src/server/fight.ts index 3ceb706..4f18e41 100644 --- a/src/server/fight.ts +++ b/src/server/fight.ts @@ -1,6 +1,6 @@ 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'; @@ -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,25 @@ export async function fightRound(player: Player, monster: MonsterWithFaction, d }); } + player.vigor -= 1; + if(player.vigor < 0) { + player.vigor = 0; + } + + 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); - } - - } - 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,15 +231,15 @@ 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); + await updateAp(player.id, 5, equippedItems.map(i => i.item_id)); await updatePlayer(player); await clearTravelPlan(player.id); diff --git a/src/server/inventory.ts b/src/server/inventory.ts index 88bfc87..33c496f 100644 --- a/src/server/inventory.ts +++ b/src/server/inventory.ts @@ -29,6 +29,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 +77,12 @@ export async function getEquippedItems(player_id: string): Promise"${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/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/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..bef40c3 100644 --- a/src/server/views/inventory.ts +++ b/src/server/views/inventory.ts @@ -106,6 +106,7 @@ 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) : ''} 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/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..75e1a30 100644 --- a/src/server/views/stores.ts +++ b/src/server/views/stores.ts @@ -48,6 +48,7 @@ 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) : ''} diff --git a/src/shared/inventory.ts b/src/shared/inventory.ts index cb42200..c0b0859 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; 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' }); -- 2.25.1