feat!: vigor mortensen
authorxangelo <me@xangelo.ca>
Fri, 25 Aug 2023 20:13:04 +0000 (16:13 -0400)
committerxangelo <me@xangelo.ca>
Fri, 25 Aug 2023 20:13:04 +0000 (16:13 -0400)
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.

21 files changed:
migrations/20230825165327_vigor.ts [new file with mode: 0644]
public/assets/css/game.css
public/index.html
seeds/monsters.ts
seeds/shop_items.ts
src/server/api.ts
src/server/fight.ts
src/server/inventory.ts
src/server/locations/healer/index.ts
src/server/locations/recruiter.ts
src/server/monster.ts
src/server/player.ts
src/server/views/components/progress-bar.ts
src/server/views/inventory.ts
src/server/views/player-bar.ts
src/server/views/profile.ts
src/server/views/skills.ts
src/server/views/stores.ts
src/shared/inventory.ts
src/shared/monsters.ts
src/shared/player.ts

diff --git a/migrations/20230825165327_vigor.ts b/migrations/20230825165327_vigor.ts
new file mode 100644 (file)
index 0000000..ad29194
--- /dev/null
@@ -0,0 +1,38 @@
+import { Knex } from "knex";
+
+
+const monsterColumns = ['helmAp', 'chestAp', 'armsAp', 'legsAp'];
+
+export async function up(knex: Knex): Promise<void> {
+  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<void> {
+  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');
+    });
+}
+
index 7afe6295627ce058ac4c73e848c825882b788092..c1ab83b7c60f785e1a8d2a719b06b11c5eb305ce 100644 (file)
@@ -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;
+  }
+}
index 777ee8db40e68a56b66d1b2345cad42268ee8134..5e809aa851aa6f3ae1d7d6ab190455d4bbb5ea4f 100644 (file)
@@ -44,7 +44,6 @@
       </header>
 
       <div id="signup-prompt" class="hidden"></div>
-      <div id="announcements" class="hidden"></div>
 
       <div id="alerts"></div>
       <div id="modal-wrapper"></div>
index c9ca8b536fd91a0218abfe9b8ad432f7888e9a24..44afa4f723d8542914e86542187e6b9b015d1fa1 100644 (file)
@@ -59,10 +59,7 @@ export async function createMonsters(): Promise<void> {
           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'
index 55e558de4871d219eceb41198e94e648b25ee28d..6fca12aee5f59ce5d329e6ecf4c6824e6d7d4d7e 100644 (file)
@@ -35,7 +35,8 @@ export async function createShopEquipment(): Promise<void> {
             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'],
index 60c2b5c6b019ab40bc99c1b172e05bd1bcb3922a..61e7f5cb9c38a8bd4ef9ad15fb2f69c8a737e0a2 100644 (file)
@@ -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));
 
   }
 
index 3ceb706031e1cf4520a18177b0fd0bcca53e9a46..4f18e41eb5789efb6c22f2ff72ceb2f803407fa5 100644 (file)
@@ -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<SkillID | any, number> = {};
@@ -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);
 
index 88bfc87da5a19d1b4da990f2f9c6a06abe33a1b0..33c496fac1f8bebfd0d085bdabd75ec12836ed19 100644 (file)
@@ -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<EquippedItemD
     });
 }
 
-export async function updateAp(player_id: string, item_id: string, currentAp: number, maxAp: number) {
-  return db('inventory').update({
-    currentAp,
-    maxAp
-  }).where({
-    player_id,
-    item_id
-  })
+export async function updateAp(player_id: string, apDamage: number, itemIds: string[]) {
+  return db('inventory').where({
+    player_id
+  }).whereIn('item_id', itemIds).update({
+      'currentAp': db.raw(`GREATEST("currentAp" - ${apDamage}, 0)`)
+    });
 }
 
 export async function deleteInventoryItem(player_id: string, item_id: string) {
index 3e658fe5c11020442b82f2fe6c084e5d5d01f7d9..16274a2f7b23cd47bfcb464318221dd0596372ec 100644 (file)
@@ -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(`<p>"${getText('intro', service, city)}"</p>`);
 
-  if(req.player.hp === maxHp(req.player.constitution, req.player.level)) {
-    text.push(`<p>You're already at full health?</p>`);
+  if(req.player.hp === maxHp(req.player.constitution, req.player.level) && req.player.vigor === maxVigor(req.player.constitution, req.player.level)) {
+    text.push(`<p>You're already in peak condition!</p>`);
   }
   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(`<p>${getText('insufficient_money', service, city)}</p>`)
   }
   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(`<p>${getText('heal_successful', service, city)}</p>`);
     text.push('<p><button hx-get="/player/explore" hx-target="#explore">Back to Town</button></p>');
@@ -169,6 +166,6 @@ router.post('/city/services/healer/heal/:location_id', authEndpoint, async (req:
 ${text.join("\n")}
 </div>
 </div>
-${inventory ? renderPlayerBar(req.player, inventory) : ''}
+${renderPlayerBar(req.player)}
 `);
 });
index 10623d8fadfe4eb9a2887f0aab41627c55b737b4..4ce985c2eb819a88620ef9cdedd497add1aeb538 100644 (file)
@@ -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 `<p>${str}</p>`;
@@ -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) + `<div id="recruiter-target" class="service-in-town" hx-swap-oob="true">Congrats! You are now a ${req.player.profession}</div>`);
+    res.send(renderPlayerBar(req.player) + `<div id="recruiter-target" class="service-in-town" hx-swap-oob="true">Congrats! You are now a ${req.player.profession}</div>`);
   }
 
 });
index e58c814bf6beacec0c898b0bf04c97606f73a6e9..943a868205bedd89cee65471efd21111ab7c0435 100644 (file)
@@ -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
index ba4d5709fcc3f249712050d7547bfbc42b26a8e0..527069b6693acfd1a2e92142bc59787b6d65abba 100644 (file)
@@ -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,
index 7d7677d9d3383dcfd12728d82795aba0df3fa2be..9d9a290ff47b4bcc2fc96b8a42cef276928695ce 100644 (file)
@@ -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 `<div class="progress-bar" id="${id}" style="background: linear-gradient(to right, ${opts.startingColor}, ${opts.endingColor} ${percent}%, transparent ${percent}%, transparent)"
-title="${percent}% - ${current}/${max}">${current}/${max} - ${percent}%</div>`;
+  if(opts.displayPercent) {
+    display.push(`${percent}%`);
+  }
+
+  return `<div class="progress-bar" id="${id}" style="background: linear-gradient(to right, ${opts.startingColor}, ${endingColor} ${percent}%, transparent ${percent}%, transparent)"
+title="${title} ${display.join(" - ")}">${title} ${display.join(" - ")}</div>`;
 }
index adf4105c11ebff58565b82d9cb18beefac5f7907..bef40c33e58e76e44566d80aa2238554a1e45e8e 100644 (file)
@@ -106,6 +106,7 @@ function renderInventoryItem(item: EquippedItemDetails , action: (item: Equipped
         ${renderRequirement('PRF', item.profession)}
       </div>
       <div class="stat-mods">
+        ${item.boosts.defence ? renderStatBoost('DEF', item.boosts.defence) : ''}
         ${item.boosts.strength ? renderStatBoost('STR', item.boosts.strength) : ''}
         ${item.boosts.constitution ? renderStatBoost('CON', item.boosts.constitution) : ''}
         ${item.boosts.dexterity ? renderStatBoost('DEX', item.boosts.dexterity) : ''}
index 9092eeeb61e0f825fbf43c9c122907ab8235f421..560a71b47612c4e54d87823de14fec6205d6ba47 100644 (file)
@@ -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 `<div class="progress-bar" style="background: linear-gradient(to right, ${opts.startingColor}, ${opts.endingColor} ${percent}%, transparent ${percent}%, transparent)" title="${display}${current}/${max}">${display}${current}/${max}</div>`;
-}
-
-function calcAp(inventoryItem: EquippedItemDetails[]): string {
-  const ap: Record<any | EquipmentSlot, {currentAp: number, maxAp: number}> = {};
-  inventoryItem.forEach(item => {
-    if(item.is_equipped && item.type === 'ARMOUR') {
-      ap[item.equipment_slot] = {
-        currentAp: item.currentAp,
-        maxAp: item.maxAp
-      };
-    }
-  });
-
-  return `
-  <div>
-    <img src="/assets/img/helm.png" class="icon">
-    ${generateProgressBar(ap.HEAD?.currentAp || 0, ap.HEAD?.maxAp || 0, { startingColor: '#5ebb5e', endingColor: '#7be67b'})}
-  </div>
-  <div>
-    <img src="/assets/img/arms.png" class="icon">
-    ${generateProgressBar(ap.ARMS?.currentAp || 0, ap.ARMS?.maxAp || 0, { startingColor: '#5ebb5e', endingColor: '#7be67b'})}
-  </div>
-  <div>
-    <img src="/assets/img/chest.png" class="icon">
-    ${generateProgressBar(ap.CHEST?.currentAp || 0, ap.CHEST?.maxAp || 0, { startingColor: '#5ebb5e', endingColor: '#7be67b'})}
-  </div>
-  <div>
-    <img src="/assets/img/legs.png" class="icon">
-    ${generateProgressBar(ap.LEGS?.currentAp || 0, ap.LEGS?.maxAp || 0, { startingColor: '#5ebb5e', endingColor: '#7be67b'})}
-  </div>
-`;
-}
-
-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 `<div class="progress-bar" id="${id}" style="background: linear-gradient(to right, ${opts.startingColor}, ${opts.endingColor} ${percent}%, transparent ${percent}%, transparent)"
-title="${percent}% - ${current}/${max}">${current}/${max} - ${percent}%</div>`;
-}
-
-export function renderPlayerBar(player: Player, inventory: EquippedItemDetails[]): string {
+export function renderPlayerBar(player: Player): string {
   return `
     <div id="stat-bars" hx-swap-oob="true">
       <div id="player-section">
         <div id="username">${player.username}, level ${player.level} ${player.profession}</div>
         <div class="gold">${player.gold.toLocaleString()}</div>
       </div>
-      <div id="ap-bar">${calcAp(inventory)}</div>
-      ${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})}
     </div>
     ${player.account_type === 'session' ? displayLoginSignupForm() : ''}
   `;
index 93a9bf739801b94abed608803d12184ba9290730..547cdefebe097df44623791138de66cad149742a 100644 (file)
@@ -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 `<button class="increase-stat" hx-post="/player/stat/${stat.id}" hx-target="#profile">+</button>`;
 }
-export function renderProfilePage(player: Player): string {
+
+export function renderProfilePage(player: Player, equipment: EquippedItemDetails[]): string {
+
   let statBreakdown = '';
 
   StatDef.forEach(stat => {
     statBreakdown += `<tr>
-      <th>${stat.display}</th>
+      <th title="${stat.description}" tabindex="0">${stat.display}</th>
       <td class="${stat.id}">
-        ${player[stat.id]}
+        ${player[stat.id].toLocaleString()}
         ${player.stat_points ? statPointIncreaser(stat) : ''}
       </td>
     </tr>`;
   });
 
   const html = `<div id="extra-inventory-info">
-  <table id="stat-breakdown">
+  <table class="stat-breakdown">
+    <tr>
+      <th title="The total amount of damage you can take before you pass out" tabindex="0">HP</th>
+      <td>${player.hp.toLocaleString()}/${maxHp(player.constitution, player.level).toLocaleString()}</td>
+    </tr>
+    <tr>
+      <th title="Your energy level. Low vigor will cause your overall defence and damage to drop." tabindex="0">Vigor</th>
+      <td>${player.vigor.toLocaleString()}/${maxVigor(player.constitution, player.level).toLocaleString()}</td>
+    </tr>
+    <tr>
+      <th title="How many experience points you need to get to your next level" tabindex="0">EXP</th>
+      <td>${player.exp.toLocaleString()}/${expToLevel(player.level + 1).toLocaleString()}</td>
+    </tr>
+    <tr>
+      <th title="The max defence you can have (and your true defence affected by your vigor)" tabindex="0">Defence</th>
+      <td>${totalDefence(equipment, player, false).toLocaleString()} (${totalDefence(equipment, player).toLocaleString()})</td>
+    </tr>
+    <tr>
+      <th title="You can use these to increase the base stats below" tabindex="0">Stat Points</th>
+      <td class="stat_points">${player.stat_points}</td>
+    </tr>
     ${statBreakdown}
-    <tr><th>Stat Points</th><td class="stat_points">${player.stat_points}</td></tr>
   </table>
-  </div>`;
+  </div>
+  <div id="announcements">
+<p>Hi, thanks for checking out this VERY early build of Rising Legends.</p>
+<p>If you have any questions or run into any bugs, feel free to drop an email on our mailing list: <a href="mailto:~xangelo/rising-legends-discuss@lists.sr.ht">~xangelo/rising-legends-discuss@lists.sr.ht</a>
+  </div>
+`;
 
   return html;
 }
index 0c11815c04ff27ef7e442eadf37508fb3a724832..8b585f221e0d6994a64d3225002f6b3c5394ff96 100644 (file)
@@ -7,12 +7,22 @@ export function renderSkills(skills: Skill[]): string {
     const percent = skill.exp / definition.expToLevel(skill.level + 1);
     return `
     <tr>
-    <td class="skill-level">${skill.level.toLocaleString()}</td>
-    <td class="skill-description" title="Total Exp: ${skill.exp.toLocaleString()}/${definition.expToLevel(skill.level + 1).toLocaleString()}">
-      <span class="skill-exp">${(percent * 100).toPrecision(2)}% to next level</span>
-      <b>${definition.display}</b>
-      <p>${definition.description}</p>
-    </td>
+      <td class="skill-level">${skill.level.toLocaleString()}</td>
+      <td class="skill-details">
+        <table>
+          <tr>
+            <th class="skill-title" title="Total Exp: ${skill.exp.toLocaleString()}/${definition.expToLevel(skill.level + 1).toLocaleString()}">
+              ${definition.display}
+            </th>
+            <td class="skill-exp">
+              ${(percent * 100).toPrecision(2)}% to next level
+            </td>
+          </tr>
+          <tr>
+            <td colspan="2" class="skill-description">${definition.description}</td>
+          </tr>
+        </table>
+      </td>
     </tr>
     `;
   }).join("\n")}
index 81b227373b4aaff559de06b1e2f4c378a785eae2..75e1a30230ff77fd38790dcaa62cb702e544ae14 100644 (file)
@@ -48,6 +48,7 @@ export function renderEquipmentDetails(item: ShopEquipment, player: Player): str
       ${renderRequirement('PRF', item.profession)}
       </div>
       <div class="stat-mods">
+      ${item.boosts.defence ? renderStatBoost('DEF', item.boosts.defence) : ''}
       ${item.boosts.strength ? renderStatBoost('STR', item.boosts.strength) : ''}
       ${item.boosts.constitution ? renderStatBoost('CON', item.boosts.constitution) : ''}
       ${item.boosts.dexterity ? renderStatBoost('DEX', item.boosts.dexterity) : ''}
index cb4220061ae4cbcac68988c025271476a842e113..c0b08596eee6208f9fce91f78833579f1c19eb9a 100644 (file)
@@ -33,6 +33,7 @@ export type InventoryItem = {
     intelligence: number;
     damage: number;
     damage_mitigation: number;
+    defence: number;
   }
   currentAp: number;
   maxAp: number;
index c2a5fce70ae561dbb82fdd86d08de9b7d51e30ee..c29a4777800047d5e4cc3acfb408940352410aa3 100644 (file)
@@ -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;
index ca94496dc7216f55e32c3bf2bac58ba2d7be4f8f..b7c172ab978d421e8fa3ee6cb2f8a70a09c8501e 100644 (file)
@@ -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<Stat, StatDisplay> = new Map<Stat, StatDisplay>();
@@ -50,23 +73,27 @@ export const StatDef: Map<Stat, StatDisplay> = new Map<Stat, StatDisplay>();
 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'
 });