fix: enhance durability visibility
authorxangelo <me@xangelo.ca>
Wed, 23 Oct 2024 16:25:11 +0000 (12:25 -0400)
committerxangelo <me@xangelo.ca>
Wed, 23 Oct 2024 16:25:11 +0000 (12:25 -0400)
Durability used to be present as a progress bar on the item, but made
it hard to view durability on equipped items.

Durability is now present in the title attribute, and there's a visual
gradient applied to the items to denote how "damaged" it is

migrations/20241021184333_equipment-exp.ts [new file with mode: 0644]
public/assets/css/game.css
src/server/inventory.ts
src/server/views/inventory.ts
src/shared/equipped.ts
src/shared/inventory.ts
src/shared/utils.ts [new file with mode: 0644]

diff --git a/migrations/20241021184333_equipment-exp.ts b/migrations/20241021184333_equipment-exp.ts
new file mode 100644 (file)
index 0000000..cc2da58
--- /dev/null
@@ -0,0 +1,20 @@
+import { Knex } from "knex";
+
+
+export async function up(knex: Knex): Promise<void> {
+  return knex.schema.alterTable('inventory', t => {
+    t.timestamps();
+    t.integer('current_exp').defaultTo(0);
+    t.integer('current_level').defaultTo(1);
+  });
+}
+
+
+export async function down(knex: Knex): Promise<void> {
+  return knex.schema.alterTable('inventory', t => {
+    t.dropTimestamps();
+    t.dropColumn('current_exp');
+    t.dropColumn('current_level');
+  });
+}
+
index 2f32e19858aa1454f8c7a77b3da32033131f5e3b..05fc248349c4e4734a6269e88b8d6a2bbb15ffc7 100644 (file)
@@ -1,11 +1,83 @@
 @font-face {
-    font-family: 'Breathe Fire';
-    src: url('/assets/font/BreatheFire.woff2') format('woff2'),
-        url('/assets/font/BreatheFire.woff') format('woff');
-    font-weight: normal;
-    font-style: normal;
-    font-display: swap;
+  font-family: 'Breathe Fire';
+  src: url('/assets/font/BreatheFire.woff2') format('woff2'),
+    url('/assets/font/BreatheFire.woff') format('woff');
+  font-weight: normal;
+  font-style: normal;
+  font-display: swap;
 }
+
+.equipment-slot {
+  position: relative;
+}
+
+.inventory-icon::after,
+.equipment-slot::after {
+  content: '';
+  position: absolute;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  height: 100%;
+  pointer-events: none;
+}
+
+.equipment-slot.perfect::after,
+.inventory-icon.perfect::after {
+  background: linear-gradient(to top,
+      rgba(0, 255, 0, 0.5) 0%,
+      transparent 30%,
+      transparent 100%);
+}
+
+.equipment-slot.great::after,
+.inventory-icon.great::after {
+  background: linear-gradient(to top,
+      rgba(51, 204, 51, 0.5) 0%,
+      transparent 30%,
+      transparent 100%);
+}
+
+.equipment-slot.good::after,
+.inventory-icon.good::after {
+  background: linear-gradient(to top,
+      rgba(102, 204, 0, 0.5) 0%,
+      transparent 30%,
+      transparent 100%);
+}
+
+.equipment-slot.fair::after,
+.inventory-icon.fair::after {
+  background: linear-gradient(to top,
+      rgba(255, 204, 0, 0.5) 0%,
+      transparent 30%,
+      transparent 100%);
+}
+
+.equipment-slot.poor::after,
+.inventory-icon.poor::after {
+  background: linear-gradient(to top,
+      rgba(255, 153, 0, 0.5) 0%,
+      transparent 30%,
+      transparent 100%);
+}
+
+.equipment-slot.terrible::after,
+.inventory-icon.terrible::after {
+  background: linear-gradient(to top,
+      rgba(255, 102, 0, 0.5) 0%,
+      transparent 30%,
+      transparent 100%);
+}
+
+.equipment-slot.about-to-break::after,
+.inventory-icon.about-to-break::after {
+  background: linear-gradient(to top,
+      rgba(255, 0, 0, 0.5) 0%,
+      transparent 30%,
+      transparent 100%);
+}
+
 html {
   height: 100vh;
 }
@@ -16,9 +88,11 @@ body {
   width: 100%;
   max-width: 724px;
 }
+
 .title-font {
   font-family: 'Breathe Fire', monospace;
 }
+
 #title-bar {
   background-color: transparent;
   margin-top: 0.5rem;
@@ -28,6 +102,7 @@ body {
   justify-content: space-between;
   align-items: center;
 }
+
 #title-bar a {
   font-size: 3rem;
   color: #8e4607;
@@ -37,14 +112,17 @@ body {
   border-bottom: solid 4px;
   line-height: 25px;
 }
+
 #time-of-day {
   text-align: right;
 }
+
 #time-of-day img {
   width: 32px;
   vertical-align: middle;
   mix-blend-mode: color-burn;
 }
+
 #view {
   font-size: 14px;
   padding: 1rem;
@@ -53,18 +131,23 @@ body {
   background: #fffef0;
   background-image: url(data:image/png;base64,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);
 }
+
 b {
   font-weight: bold;
 }
+
 a {
   color: #a20b00;
 }
+
 select {
   padding: 0.3rem;
 }
+
 input {
   border: 1px solid #000;
 }
+
 button {
   cursor: pointer;
   color: #fff;
@@ -75,46 +158,59 @@ button {
   text-shadow: -1px -1px 0px rgba(0, 0, 0, 0.3);
   border: solid 1px #6d251c;
 }
+
 button.red {
   background: #a20b00;
 }
+
 button.red:hover {
   background: #b20b00;
 }
+
 button.green {
   background: url(data:image/png;base64,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), linear-gradient(to bottom, #41d437 0%, #0a9404 100%);
 }
+
 button.green:hover {
   background: url(data:image/png;base64,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), linear-gradient(to bottom, #32e027 0%, #10a209 100%);
 }
+
 button:active {
   position: relative;
   top: 1px;
 }
-button:disabled, button:disabled:hover {
+
+button:disabled,
+button:disabled:hover {
   background: #aaa;
   cursor: not-allowed;
 }
+
 button:focus {
   outline: none;
 }
+
 .hidden {
   display: none !important;
 }
+
 p {
   margin-bottom: 1rem;
 }
+
 p:last-child {
   margin-bottom: 0;
 }
 
-#announcements, #signup-prompt {
+#announcements,
+#signup-prompt {
   padding: 1rem;
   line-height: 1.2rem;
   border: solid 1px #000;
   background-color: #fff;
   margin-bottom: 1rem;
 }
+
 #announcements {
   margin: 1rem 0;
 }
@@ -128,10 +224,12 @@ p:last-child {
   height: 100%;
   backdrop-filter: blur(10px);
 }
+
 dialog::backdrop {
   backdrop-filter: blur(3px);
 
 }
+
 dialog {
   background-color: #fff;
   border: solid 1px #000;
@@ -140,13 +238,16 @@ dialog {
   top: 20%;
   font-size: 0.9rem;
 }
+
 dialog .modal-header {
   font-weight: bold;
   text-align: right;
 }
+
 dialog .actions {
   text-align: right;
 }
+
 dialog .close-modal {
   cursor: pointer;
 }
@@ -155,7 +256,9 @@ dialog .close-modal {
   #signup {
     flex-direction: column;
   }
-  #signup .form-group, #signup button {
+
+  #signup .form-group,
+  #signup button {
     margin-bottom: 1rem;
   }
 }
@@ -165,23 +268,30 @@ dialog .close-modal {
   right: 0;
   bottom: 0;
 }
+
 .alert {
   padding: 0.3rem;
   max-width: 17rem;
   line-height: 1.2rem;
   box-shadow: -3px -3px 4px 0px rgba(0, 0, 0, 0.5);
 }
-.alert.success, button.success {
+
+.alert.success,
+button.success {
   border: solid 1px #0a0;
   background-color: #def7e5;
 }
-.alert.error, button.error {
+
+.alert.error,
+button.error {
   border: solid 1px #a00;
   background-color: #f7dede;
 }
+
 .success {
   color: #0a0;
 }
+
 .error {
   color: #a00;
 }
@@ -190,49 +300,64 @@ dialog .close-modal {
   max-width: 96px;
   min-width: 32px;
 }
+
 #avatar {
   width: 100%;
   border: solid 1px #6d251c;
 }
+
 header {
   display: flex;
   align-items: flex-start;
   border: 0;
   margin-bottom: 1rem;
 }
+
 #player-info {
   width: 100%;
 }
+
 #player-section {
   display: flex;
 }
+
 #player-section div {
   flex-grow: 1;
 }
+
 #player-section .gold {
   text-align: right;
 }
-#stat-bars, #defender-stat-bars {
+
+#stat-bars,
+#defender-stat-bars {
   width: 100%;
   margin: 0 5px;
 }
-#stat-bars .progress-bar, #defender-stat-bars .progress-bar {
+
+#stat-bars .progress-bar,
+#defender-stat-bars .progress-bar {
   margin-bottom: 2px;
 }
+
 #stat-bars .gold {
   font-size: 0.7rem;
 }
+
 #stat-bars .gold:after {
   content: 'G';
 }
+
 #ap-bar {
   display: flex;
 }
-#ap-bar > div {
+
+#ap-bar>div {
   display: flex;
   flex-grow: 1;
   align-items: center;
 }
+
 #ap-bar .icon {
   flex-basis: 16px;
   width: 16px;
@@ -253,23 +378,29 @@ nav {
   text-align: center;
   padding: 1rem 0;
 }
+
 nav li {
   display: inline-block;
   list-style: none;
 }
+
 nav li:before {
   content: '[';
 }
+
 nav li:after {
   content: ']';
 }
+
 nav a {
   text-decoration: none;
 }
+
 nav a.active {
   font-weight: bold;
   text-decoration: underline;
 }
+
 nav.filter {
   margin: 0;
   text-align: right;
@@ -278,36 +409,44 @@ nav.filter {
   position: relative;
   bottom: 5px;
 }
+
 nav.filter a {
   border-bottom-width: 0;
   z-index: 1;
   padding: 0.6rem;
   position: relative;
 }
+
 nav.filter a.active {
   background-color: #fff;
   border: solid #6d251c;
   border-width: 1px 1px 0;
   z-index: 4;
 }
+
 .filter-container .listing {
   border: solid 1px #6d251c;
   z-index: 2;
   position: relative;
   background-color: #fff;
 }
+
 nav.filter-result {
   display: none;
 }
+
 nav.filter-result.active {
   display: block !important;
 }
+
 #inventory-section {
   width: 100%;
 }
+
 #inventory-section .listing {
   top: 2px;
 }
+
 .inventory-listing {
   min-height: 2rem;
 }
@@ -315,6 +454,7 @@ nav.filter-result.active {
 .inventory-ITEMS {
   display: flex;
 }
+
 .player-item {
   position: relative;
   cursor: pointer;
@@ -324,12 +464,15 @@ nav.filter-result.active {
   height: 64px;
   overflow: hidden;
 }
+
 .player-item img {
   filter: grayscale(40%);
 }
+
 .player-item img:hover {
   filter: none;
 }
+
 .player-item .amount {
   font-weight: bold;
   position: absolute;
@@ -340,23 +483,28 @@ nav.filter-result.active {
   padding: 4px;
   border-radius: 3px 0 0 0;
 }
+
 .item-modal-overview {
   display: flex;
 }
+
 .item-modal-overview .icon {
   width: 64px;
   height: 64px;
   margin: 0 1rem 1rem 0;
 }
+
 .item-modal-overview .icon img {
   width: 64px;
   height: 64px;
 }
+
 .item-modal-overview .name {
   margin-top: 0;
   font-weight: bold;
   margin: 0 1rem 0.8rem 0;
 }
+
 .item-modal-overview p {
   margin: 1rem;
 }
@@ -364,13 +512,16 @@ nav.filter-result.active {
 .tab {
   display: none;
 }
+
 .tab.active {
   display: block;
 }
+
 #main-nav {
   margin-bottom: 1rem;
   position: relative;
 }
+
 #main-nav section {
   min-height: 344px;
 }
@@ -382,14 +533,18 @@ nav.filter-result.active {
   color: #fff;
   background-image: url(data:image/png;base64,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);
 }
-.stat-breakdown th, .stat-breakdown td {
+
+.stat-breakdown th,
+.stat-breakdown td {
   padding: 0.5rem;
   min-width: 100px;
   line-height: 1rem;
 }
+
 .stat-breakdown tr:nth-child(even) {
   background-color: #c7b7a1;
 }
+
 .increase-stat {
   padding: 1px 6px;
 }
@@ -408,6 +563,7 @@ nav.filter-result.active {
   position: relative;
   z-index: 1;
 }
+
 .city-title:before {
   position: absolute;
   content: ' ';
@@ -420,6 +576,7 @@ nav.filter-result.active {
   border: solid 2px #ffa500;
   clip-path: polygon(100% 0, 95% 50%, 100% 98%, 0% 100%, 5% 50%, 0 0);
 }
+
 .city-title {
   font-family: 'Breathe Fire', monospace;
   font-size: 1.5rem;
@@ -438,26 +595,33 @@ nav.filter-result.active {
 #fight-container {
   margin: 0 auto;
 }
+
 #defender-info {
   display: flex;
   width: 70%;
   margin: 0 auto 1rem;
 }
+
 .monster-identifier {
   text-align: left;
 }
+
 #defender-name {
   font-weight: bold;
 }
+
 .Elder #defender-name {
   color: #2b2b2b;
 }
+
 .Skittish #defender-name {
   color: #8700ff;
 }
+
 .Brute #defender-name {
   color: #a91313;
 }
+
 #fight-results {
   margin-top: 1rem;
 }
@@ -468,8 +632,9 @@ nav.filter-result.active {
   margin: 0rem auto 1rem;
   padding: 3rem 2rem 2rem;
   line-height: 1.3rem;
-  background: linear-gradient(to bottom, rgba(255,255,255, 0) 0%, rgba(255, 255, 255, 0.5) 30%);
+  background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.5) 30%);
 }
+
 .city-details {
   position: relative;
   padding: 1rem 1px 2rem;
@@ -482,27 +647,33 @@ nav.filter-result.active {
   top: -13px;
   border: solid 1px #6d251c;
 }
+
 .flex {
   display: flex;
   justify-content: space-around;
   flex-wrap: wrap;
 }
-.city-details.flex > div {
+
+.city-details.flex>div {
   margin: 1rem;
 }
+
 .service-in-town {
   padding: 0 1rem;
 }
+
 h1 {
   font-size: 1.5rem;
   font-weight: bold;
   margin-bottom: 1rem;
 }
+
 h2 {
   font-weight: bold;
   margin: 1rem;
   font-size: 1.3rem;
 }
+
 h3 {
   font-weight: bold;
   margin: 1rem;
@@ -512,32 +683,38 @@ h3 {
 #travelling {
   padding: 2rem;
 }
+
 #travelling-actions {
   display: flex;
   justify-content: center;
   gap: 1rem;
   margin-bottom: 1rem;
 }
+
 .travel-distance {
   margin-bottom: 1rem;
 }
 
 
-#explore .shop-inventory-listing  {
+#explore .shop-inventory-listing {
   margin: 2rem auto 1rem;
   width: 90%;
 }
+
 .location-name {
   display: flex;
   align-items: center;
   justify-content: center;
 }
+
 .location-name span {
   color: #846945;
   text-shadow: 1px 1px 0px rgba(255, 255, 255, 0.7);
   margin: 0 1rem;
 }
-.location-name:before, .location-name:after {
+
+.location-name:before,
+.location-name:after {
   background-color: #846945;
   height: 2px;
   flex: 1;
@@ -545,36 +722,45 @@ h3 {
   width: 4rem;
   drop-shadow: 1px 1px 0px rgba(255, 255, 255, 0.7);
 }
+
 .shop-inventory-listing .listing {
   background-color: #fff;
 }
+
 .store-list {
   display: flex;
   text-align: left;
   padding: 0.5rem;
 }
+
 .store-list:nth-child(even) {
   background-color: #f2f0ec;
 }
+
 .store-list .details {
   padding: 0 0.4rem;
   line-height: 1rem;
   flex-grow: 2;
 }
+
 .store-list .name {
   font-weight: bold;
 }
+
 .requirements {
   margin-top: 0.5rem;
   line-height: 1.3rem;
 }
+
 .requirement-title {
   font-weight: bold;
   text-transform: capitalize;
 }
+
 .store-cost {
   margin-top: 0.5rem;
 }
+
 .store-icon {
   width: 64px;
   height: calc(64px + 27px);
@@ -584,27 +770,33 @@ h3 {
   position: relative;
   margin-right: 0.5rem;
 }
+
 .inventory-icon {
   width: 64px;
-  height: 64px;;
+  height: 64px;
+  ;
   padding: 0;
   background-repeat: no-repeat;
   background-size: contain;
   position: relative;
   margin-right: 0.5rem;
 }
+
 .store-actions {
   width: 100%;
   position: absolute;
   bottom: 0;
 }
+
 .store-actions button {
   width: 100%;
   padding: 0.3rem 0.5rem;
 }
+
 .inventory-actions {
   width: 74px;
 }
+
 .inventory-actions button {
   width: 100%;
   padding: 0.3rem 0.5rem;
@@ -616,10 +808,12 @@ h3 {
   justify-content: space-between;
   gap: 1rem;
 }
+
 #character-equipment-placement {
   border-spacing: 0;
   width: 192px;
 }
+
 #character-equipment-placement td {
   display: table-cell;
   min-width: 64px;
@@ -637,30 +831,37 @@ h3 {
   overflow: hidden;
   background-size: contain;
 }
+
 #extra-inventory-info {
   margin-top: 1rem;
 }
+
 .filter-container .listing {
   max-height: 400px;
   width: 100%;
   overflow: auto;
 }
+
 @media(max-width: 650px) {
   #time-of-day {
     padding: 0 1rem;
   }
+
   #inventory-page {
     flex-direction: column;
   }
+
   #character-summary {
     width: 100%;
     display: flex;
     justify-content: space-between;
     align-items: flex-start;
   }
+
   #extra-inventory-info {
     margin-top: 0rem;
   }
+
   #inventory-section {
     margin-left: 0;
     margin-top: 2rem;
@@ -670,29 +871,35 @@ h3 {
 #skill-list {
   width: 100%;
 }
+
 #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 0.6rem;
+  padding: 0 0.6rem 0.6rem;
   line-height: 1.2rem;
 }
+
 #skill-list .skill-exp {
   text-align: right;
   padding-right: 0.6rem;
 }
+
 #skill-list tr:nth-child(even) .skill-details {
   background-color: #c7b7a1;
 }
@@ -701,14 +908,17 @@ h3 {
 #chat {
   border: solid 1px #6d251c;
 }
+
 #chat-messages {
   max-height: 250px;
   overflow: auto;
 }
+
 .chat-message {
   line-height: 1.2rem;
   padding: 0.2rem 0.3rem 0.3rem;
 }
+
 .chat-message:nth-child(even) {
   background: linear-gradient(270deg, rgba(0, 0, 0, 0) 0, rgba(196, 177, 149, 0.8) 100%);
 }
@@ -716,12 +926,15 @@ h3 {
 .chat-message .from {
   font-weight: bold;
 }
+
 .chat-message .from::after {
   content: ':';
 }
+
 #chat-form {
   display: flex;
 }
+
 #chat-form input {
   flex-grow: 8;
   padding: 0.3rem;
@@ -729,14 +942,17 @@ h3 {
   border-width: 1px 0 0;
   background: transparent;
 }
+
 #chat-form input:focus {
   outline: none;
 }
+
 #chat-form button {
   border-right-width: 0px;
   border-bottom-width: 0px;
   font-weight: bold;
 }
+
 #chat-form button:active {
   top: 0;
 }
@@ -747,10 +963,12 @@ h3 {
   margin-top: 1rem;
   border: 0;
 }
+
 #game-footer nav {
   margin: 0;
   padding: 0;
 }
+
 #game-footer img {
   width: 1rem;
   height: 1rem;
@@ -768,6 +986,7 @@ footer {
     display: flex;
     justify-content: center;
   }
+
   .tooltip[title]:focus::after {
     content: attr(title);
     background-color: #fff;
@@ -790,4 +1009,4 @@ footer {
 
 .dungeon-room-description {
   padding: 1rem;
-}
+}
\ No newline at end of file
index 3074644501ca3b708bacefd05938b1e515b1ca96..1ef723650b37605526157c54c17cb6d0717f6ffb 100644 (file)
@@ -16,6 +16,8 @@ export async function addInventoryItem(playerId: string, item: ShopEquipment) {
     count: item.count,
     profession:  item.profession,
     icon: item.icon,
+    current_exp: 0,
+    current_level: 1,
     requirements: {
       level: item.requirements.level,
       strength: item.requirements.strength,
index d9b8c435de68f7f19e9d13a3f28b3f20d251b468..f3ffb89f7bf529cd21a7ae9ee7ab062a60e71c17 100644 (file)
@@ -5,6 +5,8 @@ import { capitalize } from "lodash";
 import { ProgressBar } from "./components/progress-bar";
 import { Player } from "../../shared/player";
 import { renderStatBoostWithPlayerIncrease } from "./components/stats";
+import { getDurabilityApproximation, expToLevel} from "../../shared/inventory";
+import { slugify } from "../../shared/utils";
 
 function icon(icon_name?: string): string {
   const placeholder = 'https://placehold.co/64x64/af936c/6d5f4d';
@@ -18,35 +20,38 @@ function renderEquipmentPlacementGrid(items: EquippedItemDetails[]) {
   const map: Record<EquipmentSlot, EquippedItemDetails> = items.filter(item => item.is_equipped).reduce((acc, item) => {
     acc[item.equipment_slot] = item;
     return acc;
-  }, {});
+  }, {}); 
+
+  const leftHand = map.LEFT_HAND ? map.LEFT_HAND : (map.TWO_HANDED ? map.TWO_HANDED : null);
+  const rightHand = map.RIGHT_HAND ? map.RIGHT_HAND : (map.TWO_HANDED ? map.TWO_HANDED : null);
 
   const html = `
 <table id="character-equipment-placement">
 <tr>
 <td>
 </td>
-<td style="background-image: url('${icon(map.HEAD?.icon)}');" title="${map.HEAD ? map.HEAD.name : 'Empty'}">
+<td style="background-image: url('${icon(map.HEAD?.icon)}');" title="${map.HEAD ? `${map.HEAD.name} - Durability: ${map.HEAD.currentAp}/${map.HEAD.maxAp} (${Math.round((map.HEAD.currentAp / map.HEAD.maxAp) * 100)}%)` : 'Empty'}" class="equipment-slot ${slugify(getDurabilityApproximation(map.HEAD))}">
 ${map.HEAD ? (map.HEAD.icon ? '' : map.HEAD.name) : 'HEAD'}
 </td>
-<td style="background-image: url('${icon(map.ARMS?.icon)}');" title="${map.ARMS ? map.ARMS.name : 'Empty'}">
+<td style="background-image: url('${icon(map.ARMS?.icon)}');" title="${map.ARMS ? `${map.ARMS.name} - Durability: ${map.ARMS.currentAp}/${map.ARMS.maxAp} (${Math.round((map.ARMS.currentAp / map.ARMS.maxAp) * 100)}%)` : 'Empty'}" class="equipment-slot ${slugify(getDurabilityApproximation(map.ARMS))}">
 ${map.ARMS ? (map.ARMS.icon ? '' : map.ARMS.name) : 'ARMS'}
 </td>
 </tr>
 <tr>
-<td style="background-image: url('${icon(map.LEFT_HAND ? map.LEFT_HAND.icon : map.TWO_HANDED?.icon)}');" title="${map.LEFT_HAND ? map.LEFT_HAND.name : (map.TWO_HANDED ? map.TWO_HANDED.name : '')}">
+<td style="background-image: url('${icon(leftHand?.icon)}');" title="${leftHand ? `${leftHand.name} - Durability: ${leftHand.currentAp}/${leftHand.maxAp} (${Math.round((leftHand.currentAp / leftHand.maxAp) * 100)}%)` : 'Empty'}" class="equipment-slot ${slugify(getDurabilityApproximation(leftHand))}">
 ${map.LEFT_HAND ? (map.LEFT_HAND.icon ? '' : map.LEFT_HAND.name) : (map.TWO_HANDED ? (map.TWO_HANDED.icon ? '' : map.TWO_HANDED.name) : 'L_HAND')}
 </td>
-<td style="background-image: url('${icon(map.CHEST?.icon)}');" title="${map.CHEST ? map.CHEST.name : ''}">
+<td style="background-image: url('${icon(map.CHEST?.icon)}');" title="${map.CHEST ? `${map.CHEST.name} - Durability: ${map.CHEST.currentAp}/${map.CHEST.maxAp} (${Math.round((map.CHEST.currentAp / map.CHEST.maxAp) * 100)}%)` : 'Empty'}" class="equipment-slot ${slugify(getDurabilityApproximation(map.CHEST))}">
 ${map.CHEST ? (map.CHEST.icon ? '' : map.CHEST.name) : 'CHEST'}
 </td>
-<td style="background-image: url('${icon(map.RIGHT_HAND ? map.RIGHT_HAND.icon : map.TWO_HANDED?.icon)}');" title="${map.RIGHT_HAND ? map.RIGHT_HAND.name : (map.TWO_HANDED ? map.TWO_HANDED.name : '')}">
+<td style="background-image: url('${icon(rightHand?.icon)}');" title="${rightHand ? `${rightHand.name} - Durability: ${rightHand.currentAp}/${rightHand.maxAp} (${Math.round((rightHand.currentAp / rightHand.maxAp) * 100)}%)` : 'Empty'}" class="equipment-slot ${slugify(getDurabilityApproximation(rightHand))}">
 ${map.RIGHT_HAND ? (map.RIGHT_HAND.icon ? '' : map.RIGHT_HAND.name) : (map.TWO_HANDED ? (map.TWO_HANDED.icon ? '' : map.TWO_HANDED.name) : 'R_HAND')}
 </td>
 </tr>
 <tr>
 <td>
 </td>
-<td style="background-image: url('${icon(map.LEGS?.icon)}');" title="${map.LEGS ? map.LEGS.name : ''}">
+<td style="background-image: url('${icon(map.LEGS?.icon)}');" title="${map.LEGS ? `${map.LEGS.name} - Durability: ${map.LEGS.currentAp}/${map.LEGS.maxAp} (${Math.round((map.LEGS.currentAp / map.LEGS.maxAp) * 100)}%)` : 'Empty'}" class="equipment-slot ${slugify(getDurabilityApproximation(map.LEGS))}">
 ${map.LEGS ? (map.LEGS.icon ? '' : map.LEGS.name) : 'LEGS'}
 </td>
 <td>
@@ -87,7 +92,7 @@ function renderStatBoost(name: string, val: number | string): string {
 
 function renderInventoryItem(player: Player, item: EquippedItemDetails , action: (item: EquippedItemDetails) => string): string {
   return `<div class="store-list">
-    <div class="inventory-icon" style="background-image: url('${icon(item.icon)}')">
+    <div class="inventory-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>
     <div class="details">
       <div class="name">${item.name}</div>
@@ -107,12 +112,6 @@ function renderInventoryItem(player: Player, item: EquippedItemDetails , action:
         ${item.boosts.intelligence ? renderStatBoost('INT', item.boosts.intelligence) : ''}
         ${item.boosts.damage ? renderStatBoostWithPlayerIncrease(player, item.affectedSkills.includes('restoration_magic') ? 'HP' : 'DMG', item) : ''}
         ${item.boosts.damage_mitigation ? renderStatBoost('MIT', item.boosts.damage_mitigation.toString())+'%' : ''}
-        ${ProgressBar(item.currentAp, item.maxAp, `dur-${item.item_id}`, {
-          startingColor: '#7be67b',
-          endingColor: '#7be67b',
-          displayPercent: false,
-          title: item.type === 'SPELL' ? 'Uses' : 'Durability'
-        })}
       </div>
       ${item.hasOwnProperty('id') ? `<div>${item.cost.toLocaleString()}G</div>` : ''}
       </div>
index 2dc33a793e9e48650421454db91285720e349c0f..3e87998352bd772648c54c84a6f733df1679a219 100644 (file)
@@ -1,12 +1,5 @@
 import {EquipmentSlot, InventoryItem, InventoryType} from "./inventory";
 
-export type EquippedItem = {
-  item_id: number;
-  player_id: string;
-  type: InventoryType;
-  equipment_slot: EquipmentSlot
-}
-
 export type EquippedItemDetails = InventoryItem & {
   is_equipped: boolean
 };
index f78d65b6e30984273451fccb17f10e9a1d13b15b..d7eb879909290190f05fe1286698ebbd4636f7a9 100644 (file)
@@ -2,6 +2,8 @@ import {Profession} from "./profession";
 import {SkillID} from "./skills";
 import { max } from 'lodash';
 
+export type DurabilityApproximation = 'Perfect' | 'Great' | 'Good' | 'Fair' | 'Poor' | 'Terrible' | 'About to Break';
+
 export type InventoryType = 'ARMOUR' | 'WEAPON' | 'SPELL';
 
 export type ArmourEquipmentSlot = 'HEAD' | 'LEGS' | 'ARMS' | 'CHEST';
@@ -29,6 +31,8 @@ export type InventoryItem = {
   cost: number;
   count: number;
   icon: string;
+  current_exp: number;
+  current_level: number;
   requirements: {
     level: number,
     strength: number,
@@ -64,3 +68,41 @@ export function repairCost(item: InventoryItem): number {
 
   return max([Math.floor(totalCost * damageRatio), 1]);
 }
+
+
+export function expToLevel(level: number): number {
+  if (level <= 1) return 0;
+  if (level <= 5) {
+    // Linear growth for levels 1-5
+    return 100 * (level - 1);
+  } else {
+    // Exponential growth starting from level 5
+    const baseExp = 400; // Exp required for level 5
+    const growthFactor = 1.2;
+    return Math.floor(baseExp * Math.pow(growthFactor, level - 5));
+  }
+
+}
+
+export function getDurabilityApproximation(item?: InventoryItem): DurabilityApproximation | "" {
+  if (!item) {
+    return "";
+  }
+
+  const durability = item.currentAp / item.maxAp;
+  if (durability === 1) {
+    return "Perfect";
+  } else if (durability >= 0.90) {
+    return "Great";
+  } else if (durability >= 0.75) {
+    return "Good";
+  } else if (durability >= 0.50) {
+    return "Fair";
+  } else if (durability >= 0.25) {
+    return "Poor";
+  } else if (durability >= 0.10) {
+    return "Terrible";
+  } else {
+    return "About to Break";
+  }
+}
diff --git a/src/shared/utils.ts b/src/shared/utils.ts
new file mode 100644 (file)
index 0000000..29b3f11
--- /dev/null
@@ -0,0 +1,9 @@
+export function slugify(str: string): string {
+  return str
+    .toLowerCase()
+    .trim()
+    .replace(/\s+/g, '-')
+    .replace(/[^\w\-]+/g, '')
+    .replace(/\-\-+/g, '-');
+    
+}
\ No newline at end of file