fix(inventory): clean up requirement display
authorxangelo <me@xangelo.ca>
Fri, 15 Nov 2024 20:22:33 +0000 (15:22 -0500)
committerxangelo <me@xangelo.ca>
Fri, 15 Nov 2024 20:59:20 +0000 (15:59 -0500)
Between requirements and boosts the items were getting a bit messy. This
makes sure that the content is the boost, but the requirements are easily
accessible if they matter.

public/assets/css/base.css
public/assets/css/game.css
public/assets/css/inventory.css
public/assets/css/store.css
public/assets/css/tabs.css
src/server/views/components/progress-bar.ts
src/server/views/inventory.ts
src/server/views/stores.ts
src/shared/inventory.ts

index 39e515a46408c30b123fb5ccb5b83961550a4482..92a43f9aea30d01769f6c31b13c1f87bcef94303 100644 (file)
@@ -47,6 +47,10 @@ p:last-child {
     display: none !important;
 }
 
+.right {
+    float: right;
+}
+
 .flex {
     display: flex;
     justify-content: space-around;
index 80edc64fc8b520a7acb07e8d167e8bad208c2660..f7cc76ae9573280a4f903214e39d3e9bf82aa315 100644 (file)
@@ -255,6 +255,11 @@ button.error {
   line-height: 110%;
 }
 
+.progress-bar.vertical {
+  height: 100%;
+  width: 10px;
+}
+
 #map {
   width: 75%;
   margin: 0rem auto 1rem;
index bb56a94ec5aad2e14a8109d04b2c9a0c5f8a968e..50d348fffea6eaa5a944ddb8fb4e7f431184ba0d 100644 (file)
     p {
         margin: 1rem;
     }
+}
+
+
+.inventory-listing {
+
+    .requirements {
+        display: none !important;
+    }
+
+    .requirement-hover {
+        cursor: help;
+        position: relative;
+        font-size: 0.8rem;
+        font-weight: normal;
+    }
+
+    .requirement-hover:hover+.requirements,
+    .requirements:hover {
+        padding: 0.5rem;
+        border-radius: 4px;
+        font-size: 0.8rem;
+        width: max-content;
+        max-width: 200px;
+        position: absolute;
+        background: #2d2d2d;
+        color: #fff;
+        display: grid !important;
+        top: 100%;
+        right: 0;
+        transition: opacity 0.2s;
+        z-index: 100;
+    }
+
+}
+
+.icon-durability-container {
+    position: relative;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    height: 64px;
+}
+
+.icon-durability-container .icon {
+    margin-right: 3px;
 }
\ No newline at end of file
index 9473b8bfbf698b3bed10f89cdec9e8a573e2f213..9a546e410c53697bb90e7b54dd18afb3c3933283 100644 (file)
@@ -1,4 +1,28 @@
 #explore .shop-inventory-listing {
     margin: 2rem auto 1rem;
     width: 90%;
+}
+
+.shop-inventory-listing {
+
+    .list-item:nth-child(even) .requirements {
+        background-color: #f5e6d3;
+    }
+
+    .requirements {
+        background-color: #faf3ea;
+
+        border-radius: 6px;
+        padding: 0.5rem;
+        margin-top: 0.5rem;
+
+        &:before {
+            content: 'REQUIREMENTS';
+            font-weight: bold;
+            display: block;
+            margin-bottom: 0.3rem;
+            color: #6d251c;
+            grid-column: 1 / -1;
+        }
+    }
 }
\ No newline at end of file
index 88d871f85fa3f032db99e216223435e97bcf58ef..0789a224bcb8c87cea1137d4d8db4a31843547f5 100644 (file)
@@ -91,6 +91,7 @@ nav {
     display: flex;
     text-align: left;
     padding: 0.5rem;
+    overflow: hidden;
 
     &:nth-child(even) {
         background-color: #f2f0ec;
@@ -99,6 +100,7 @@ nav {
     .icon-wrapper {
         position: relative;
         margin-right: 0.5rem;
+        width: 77px;
 
         .icon {
             width: 64px;
@@ -112,7 +114,6 @@ nav {
         }
 
         .actions {
-            width: 64px;
             margin-top: 0.2rem;
 
             button {
@@ -129,6 +130,7 @@ nav {
         padding: 0 0.4rem;
         line-height: 1rem;
         flex-grow: 2;
+        width: 100%;
 
         &>div {
             margin-bottom: 4px;
@@ -144,16 +146,16 @@ nav {
 
         .stat-mods {
             font-size: 0.8rem;
+            display: grid;
+            grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
+            gap: 0.2rem;
         }
 
         .requirements {
             font-size: 0.8rem;
-
-            &::before {
-                content: 'REQ: ';
-                font-weight: bold;
-                text-transform: uppercase;
-            }
+            display: grid;
+            grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
+            gap: 0.2rem;
 
             .requirement-title {
                 font-weight: bold;
index 9d9a290ff47b4bcc2fc96b8a42cef276928695ce..8ec7149ef0fbd9f1d1d62342db1f00fd9a8d10c2 100644 (file)
@@ -3,6 +3,8 @@ export interface ProgressBarOptions {
   endingColor?: string;
   title?: string
   displayPercent?: boolean;
+  vertical?: boolean;
+  noContent?: boolean;
 }
 
 export function ProgressBar(current: number, max: number, id: string, opts: ProgressBarOptions) {
@@ -19,6 +21,6 @@ export function ProgressBar(current: number, max: number, id: string, opts: Prog
     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>`;
+  return `<div class="progress-bar ${opts.vertical ? 'vertical' : ''}" id="${id}" style="background: linear-gradient(${opts.vertical ? 'to top' : 'to right'}, ${opts.startingColor}, ${endingColor} ${percent}%, transparent ${percent}%, transparent)"
+title="${title} ${display.join(" - ")}">${opts.noContent ? '' : `${title} ${display.join(" - ")}`}</div>`;
 }
index ca9cd470437a4ec3cc2803e89e29de36b96b10ce..1cd533f66c1c979ca65d51f44ba8467a67523b56 100644 (file)
@@ -1,6 +1,6 @@
-import { EquipmentSlot, meetsRequirements } from "@shared/inventory";
+import { EquipmentSlot, InventoryItem, meetsRequirements, requirementMap } from "@shared/inventory";
 import { EquippedItemDetails } from "@shared/equipped";
-import { PlayerItem } from "@shared/items";
+import { Item, PlayerItem } from "@shared/items";
 import { capitalize } from "lodash";
 import { ProgressBar } from "./components/progress-bar";
 import { Player } from "@shared/player";
@@ -73,6 +73,15 @@ function renderInventoryItems(items: PlayerItem[]): string {
   }).join("<br>");
 }
 
+function renderRequirements(requirements: InventoryItem['requirements'], player: Player): string {
+  return `
+  <div class="requirements">
+  ${Object.entries(requirements).filter(([key, val]) => val !== 0).map(([key, val]) => {
+    return renderRequirement(key, val, player[key]);
+  }).join('')}
+  </div>
+  `;
+}
 
 function renderRequirement(name: string, val: number | string, currentVal?: number): string {
   let colorIndicator = '';
@@ -92,17 +101,26 @@ function renderStatBoost(name: string, val: number | string, percent: boolean =
 
 function renderInventoryItem(player: Player, item: EquippedItemDetails , action: (item: EquippedItemDetails) => string): string {
   const itemLevel = levelFromExp(item.current_exp);
-  const requirements = meetsRequirements(item, player);
+  const meetsAllRequirements = meetsRequirements(item, player);
   return `<div class="list-item">
     <div class="icon-wrapper">
-      <div class="icon ${slugify(getDurabilityApproximation(item))}" style="background-image: url('${icon(item.icon)}')" title="Durability: ${item.currentAp}/${item.maxAp} (${Math.round((item.currentAp / item.maxAp) * 100)}%)">
+      <div class="icon-durability-container">
+        <div class="icon ${slugify(getDurabilityApproximation(item))}" style="background-image: url('${icon(item.icon)}')" title="Durability: ${item.currentAp}/${item.maxAp} (${Math.round((item.currentAp / item.maxAp) * 100)}%)">
+        </div>
+        ${ProgressBar(item.currentAp, item.maxAp, `durability-${item.item_id}`, {
+          startingColor: '#7be67b',
+          endingColor: '#7be67b',
+          title: 'Durability',
+          vertical: true,
+          noContent: true
+        })}
       </div>
       <div class="actions">
         ${action(item)}
       </div>
     </div>
     <div class="details">
-      <div class="name">${item.name} <span class="right">${item.profession} (<span class="requirement-hover">REQ</span>)</span></div>
+      <div class="name">${item.name} <span class="right" style="position: relative;">${item.profession} <span class="requirement-hover${meetsAllRequirements ? '' : ' error'}">(REQ)</span>${renderRequirements(item.requirements, player)}</span></div>
       <div class="stat-mods">
         ${item.boosts.defence ? renderStatBoost('DEF', item.boosts.defence) : ''}
         ${item.boosts.strength ? renderStatBoost('STR', item.boosts.strength) : ''}
@@ -112,13 +130,6 @@ function renderInventoryItem(player: Player, item: EquippedItemDetails , action:
         ${item.boosts.damage ? renderStatBoostWithPlayerIncrease(player, item.affectedSkills.includes('restoration_magic') ? 'HP' : 'DMG', item) : ''}
         ${item.boosts.damage_mitigation ? renderStatBoost('MIT', item.boosts.damage_mitigation.toString(), true) : ''}
       </div>
-      <div class="requirements" title="Requirements">
-        ${item.requirements.level ? renderRequirement('LVL', item.requirements.level): ''}
-        ${item.requirements.strength ? renderRequirement('STR', item.requirements.strength): ''}
-        ${item.requirements.constitution ? renderRequirement('CON', item.requirements.constitution): ''}
-        ${item.requirements.dexterity ? renderRequirement('DEX', item.requirements.dexterity): ''}
-        ${item.requirements.intelligence ? renderRequirement('INT', item.requirements.intelligence): ''}
-      </div>
       ${ProgressBar(item.current_exp - expToLevel(itemLevel), expToLevel(itemLevel + 1) - expToLevel(itemLevel), `exp-${item.item_id}`, {
         startingColor: '#7be67b',
         endingColor: '#7be67b',
@@ -135,10 +146,16 @@ function renderInventorySection(player: Player, inventory: EquippedItemDetails[]
 
   return inventory.map(item => {
     return renderInventoryItem(player, item, item => {
+
+      const meetsAllRequirements = meetsRequirements(item, player);
       if(item.is_equipped) {
         return `<button type="button" class="unequip-item red" hx-post="/player/unequip/${item.item_id}">Unequip</button>`;
       }
       else {
+        if(!meetsAllRequirements) {
+          return `<button type="button" disabled="disabled" class="equip-item disabled">Locked</button>`;
+        }
+
         if(['ANY_HAND', 'LEFT_HAND', 'RIGHT_HAND'].includes(item.equipment_slot)) {
           const str: string[] = [
             `<button type="button" class="equip-item" hx-post="/player/equip/${item.item_id}/LEFT_HAND">Equip L</button>`,
index 29c9709d65ed4276f1d9512a37eaffd44a413367..33413ffebc372cdbae87dea50f0cc2d15bbf1f8e 100644 (file)
@@ -62,14 +62,6 @@ export function renderEquipmentDetails(item: ShopEquipment, player: Player): str
   return `
     <div class="details">
       <div class="name">${item.name}${item.equipment_slot === 'TWO_HANDED' ? ' (2H)': ''}</div>
-      <div class="requirements">
-      ${item.requirements.level ? renderRequirement('LVL', item.requirements.level, player.level): ''}
-      ${item.requirements.strength ? renderRequirement('STR', item.requirements.strength, player.strength): ''}
-      ${item.requirements.constitution ? renderRequirement('CON', item.requirements.constitution, player.constitution): ''}
-      ${item.requirements.dexterity ? renderRequirement('DEX', item.requirements.dexterity, player.dexterity): ''}
-      ${item.requirements.intelligence ? renderRequirement('INT', item.requirements.intelligence, player.intelligence): ''}
-      ${renderRequirement('PRF', item.profession)}
-      </div>
       <div class="stat-mods">
       ${item.boosts.defence ? renderStatBoost('DEF', item.boosts.defence) : ''}
       ${item.boosts.strength ? renderStatBoost('STR', item.boosts.strength) : ''}
@@ -81,6 +73,14 @@ export function renderEquipmentDetails(item: ShopEquipment, player: Player): str
       ${renderStat(isSpell ? 'Uses': 'Durability', isSpell ? 'Uses': 'DUR', item.maxAp, { unsigned: true })}
       </div>
       ${item.hasOwnProperty('id') ? `<div class="store-cost">${item.cost.toLocaleString()}G</div>` : ''}
+      <div class="requirements">
+      ${item.requirements.level ? renderRequirement('LVL', item.requirements.level, player.level): ''}
+      ${item.requirements.strength ? renderRequirement('STR', item.requirements.strength, player.strength): ''}
+      ${item.requirements.constitution ? renderRequirement('CON', item.requirements.constitution, player.constitution): ''}
+      ${item.requirements.dexterity ? renderRequirement('DEX', item.requirements.dexterity, player.dexterity): ''}
+      ${item.requirements.intelligence ? renderRequirement('INT', item.requirements.intelligence, player.intelligence): ''}
+      ${renderRequirement('PRF', item.profession)}
+      </div>
     </div>
 `
 
index 2cb1da4417830fd49985795fe5d6f509607f3731..16d5852015dd040a699268d3fc4fc5b9fe989429 100644 (file)
@@ -1,3 +1,4 @@
+import { Player } from "./player";
 import {Profession} from "./profession";
 import {SkillID} from "./skills";
 import { max } from 'lodash';
@@ -116,3 +117,16 @@ export function getDurabilityApproximation(item?: InventoryItem): DurabilityAppr
     return "About to Break";
   }
 }
+
+export function requirementMap(item: InventoryItem, player: Player): Map<string, boolean> {
+  const requirements = new Map<string, boolean>();
+  Object.entries(item.requirements).forEach(([key, value]) => {
+    requirements.set(key, player[key] >= value);
+  });
+  return requirements;
+}
+
+export function meetsRequirements(item: InventoryItem, player: Player): boolean {
+  const requirements = requirementMap(item, player);
+  return Array.from(requirements.values()).every(val => val);
+}
\ No newline at end of file