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 = `