chore(release): 0.2.10
[risinglegends.git] / src / server / views / inventory.ts
1 import { EquipmentSlot } from "shared/inventory";
2 import { EquippedItemDetails } from "../../shared/equipped";
3 import { PlayerItem } from "../../shared/items";
4 import { capitalize } from "lodash";
5
6 function icon(icon_name?: string): string {
7   const placeholder = 'https://placehold.co/64x64/af936c/6d5f4d';
8   const icon = icon_name ? `/assets/img/icons/equipment/${icon_name}` : placeholder;
9
10   return icon;
11 }
12
13 function renderEquipmentPlacementGrid(items: EquippedItemDetails[]) {
14   // @ts-ignore
15   const map: Record<EquipmentSlot, EquippedItemDetails> = items.filter(item => item.is_equipped).reduce((acc, item) => {
16     acc[item.equipment_slot] = item;
17     return acc;
18   }, {});
19
20   const html = `
21 <table id="character-equipment-placement">
22 <tr>
23 <td>
24 </td>
25 <td style="background-image: url('${icon(map.HEAD?.icon)}');" title="${map.HEAD ? map.HEAD.name : 'Empty'}">
26 ${map.HEAD ? (map.HEAD.icon ? '' : map.HEAD.name) : 'HEAD'}
27 </td>
28 <td style="background-image: url('${icon(map.ARMS?.icon)}');" title="${map.ARMS ? map.ARMS.name : 'Empty'}">
29 ${map.ARMS ? (map.ARMS.icon ? '' : map.ARMS.name) : 'ARMS'}
30 </td>
31 </tr>
32 <tr>
33 <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 : '')}">
34 ${map.LEFT_HAND ? (map.LEFT_HAND.icon ? '' : map.LEFT_HAND.name) : (map.TWO_HANDED ? (map.TWO_HANDED.icon ? '' : map.TWO_HANDED.name) : 'L_HAND')}
35 </td>
36 <td style="background-image: url('${icon(map.CHEST?.icon)}');" title="${map.CHEST ? map.CHEST.name : ''}">
37 ${map.CHEST ? (map.CHEST.icon ? '' : map.CHEST.name) : 'CHEST'}
38 </td>
39 <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 : '')}">
40 ${map.RIGHT_HAND ? (map.RIGHT_HAND.icon ? '' : map.RIGHT_HAND.name) : (map.TWO_HANDED ? (map.TWO_HANDED.icon ? '' : map.TWO_HANDED.name) : 'R_HAND')}
41 </td>
42 </tr>
43 <tr>
44 <td>
45 </td>
46 <td style="background-image: url('${icon(map.LEGS?.icon)}');" title="${map.LEGS ? map.LEGS.name : ''}">
47 ${map.LEGS ? (map.LEGS.icon ? '' : map.LEGS.name) : 'LEGS'}
48 </td>
49 <td>
50 </td>
51 </tr>
52 </table>
53 `;
54
55   return html;
56 }
57
58 function renderInventoryItems(items: PlayerItem[]): string {
59   return items.map(item => {
60     return `
61 <div class="player-item" hx-get="/modal/items/${item.item_id}" hx-target="#modal-wrapper">
62 <img src="/assets/img/icons/items/${item.icon_name}" title="${item.name}">
63 <span class="amount">${item.amount.toLocaleString()}</span>
64 </div>`;
65   }).join("<br>");
66 }
67
68
69 function renderRequirement(name: string, val: number | string, currentVal?: number): string {
70   let colorIndicator = '';
71   if(currentVal) {
72     colorIndicator = currentVal >= val ? 'success' : 'error';
73   }
74   return `<span class="requirement-title">${name}</span>: <span class="requirement-value ${colorIndicator}">${val.toLocaleString()}</span>`;
75 }
76
77 function renderStatBoost(name: string, val: number | string): string {
78   let valSign: string = '';
79   if(typeof val === 'number') {
80     valSign = val > 0 ? '+' : '-';
81   }
82   return `<span class="requirement-title">${name}</span>: <span class="requirement-value ${typeof val === 'number' ? (val > 0 ? "success": "error") : ""}">${valSign}${val}</span>`;
83 }
84
85 function generateProgressBar(current: number, max: number, color: string, displayPercent: boolean = true): string {
86   let percent = 0;
87   if(max > 0) {
88     percent = Math.floor((current / max) * 100);
89   }
90   const display = `${displayPercent? `${percent}% - `: ''}`;
91   return `<div class="progress-bar" style="background: linear-gradient(to right, ${color}, ${color} ${percent}%, transparent ${percent}%, transparent)" title="${display}${current}/${max}">${display}${current}/${max}</div>`;
92 }
93
94 function renderInventoryItem(item: EquippedItemDetails , action: (item: EquippedItemDetails) => string): string {
95   return `<div class="store-list">
96     <div class="inventory-icon" style="background-image: url('${icon(item.icon)}')">
97     </div>
98     <div class="details">
99       <div class="name">${item.name}</div>
100       <div class="requirements">
101         ${item.requirements.level ? renderRequirement('LVL', item.requirements.level): ''}
102         ${item.requirements.strength ? renderRequirement('STR', item.requirements.strength): ''}
103         ${item.requirements.constitution ? renderRequirement('CON', item.requirements.constitution): ''}
104         ${item.requirements.dexterity ? renderRequirement('DEX', item.requirements.dexterity): ''}
105         ${item.requirements.intelligence ? renderRequirement('INT', item.requirements.intelligence): ''}
106         ${renderRequirement('PRF', item.profession)}
107       </div>
108       <div class="stat-mods">
109         ${item.boosts.strength ? renderStatBoost('STR', item.boosts.strength) : ''}
110         ${item.boosts.constitution ? renderStatBoost('CON', item.boosts.constitution) : ''}
111         ${item.boosts.dexterity ? renderStatBoost('DEX', item.boosts.dexterity) : ''}
112         ${item.boosts.intelligence ? renderStatBoost('INT', item.boosts.intelligence) : ''}
113         ${item.boosts.damage ? renderStatBoost('DMG', item.boosts.damage) : ''}
114         ${item.boosts.damage_mitigation ? renderStatBoost('MIT', item.boosts.damage_mitigation.toString())+'%' : ''}
115         ${['WEAPON','SPELL'].includes(item.type) ? '': generateProgressBar(item.currentAp, item.maxAp, '#7be67b')}
116       </div>
117       ${item.hasOwnProperty('id') ? `<div>${item.cost.toLocaleString()}G</div>` : ''}
118       </div>
119       <div class="inventory-actions">
120         ${action(item)}
121       </div>
122     </div>`;
123 }
124
125 function renderInventorySection(inventory: EquippedItemDetails[]): string {
126   return inventory.map(item => {
127     return renderInventoryItem(item, item => {
128       if(item.is_equipped) {
129         return `<button type="button" class="unequip-item red" hx-post="/player/unequip/${item.item_id}">Unequip</button>`;
130       }
131       else {
132         if(item.equipment_slot === 'ANY_HAND') {
133           return `<button type="button" class="equip-item" hx-post="/player/equip/${item.item_id}/LEFT_HAND">Equip L</button>
134 <button type="button" class="equip-item" hx-post="/player/equip/${item.item_id}/RIGHT_HAND">Equip R</button>`;
135         }
136         else if(item.equipment_slot === 'LEFT_HAND') {
137           return `<button type="button" class="equip-item" hx-post="/player/equip/${item.item_id}/${item.equipment_slot}">Equip Left</button>`;
138         }
139         else if(item.equipment_slot === 'RIGHT_HAND') {
140           return `<button type="button" class="equip-item" hx-post="/player/equip/${item.item_id}/${item.equipment_slot}">Equip Right</button>`;
141         }
142         else {
143           return `<button type="button" class="equip-item" hx-post="/player/equip/${item.item_id}/${item.equipment_slot}">Equip</button>`;
144         }
145       }
146     })
147   }).join("\n");
148 }
149
150
151 export function renderInventoryPage(inventory: EquippedItemDetails[], items: PlayerItem[], requestedSlot: string = 'ARMOUR') {
152   const sectionedInventory: {ARMOUR: EquippedItemDetails[], WEAPON: EquippedItemDetails[], SPELL: EquippedItemDetails[], ITEMS: PlayerItem[]} = {
153     ARMOUR: [],
154     WEAPON: [],
155     SPELL: [],
156     ITEMS: []
157   };
158
159   inventory.forEach(item => {
160     sectionedInventory[item.type].push(item);
161   });
162   items.forEach(item => {
163     sectionedInventory.ITEMS.push(item);
164   });
165
166   const html = `
167   <div id="inventory-page" hx-target="section#inventory" hx-swap="true">
168     <div id="character-summary">
169       ${renderEquipmentPlacementGrid(inventory)}
170     </div>
171     <div id="inventory-section" class="filter-container">
172       <nav class="filter" id="inventory-section">
173         ${Object.keys(sectionedInventory).map((type) => {
174           return `<a href="#" data-filter="${type}" class="${type === requestedSlot ? 'active': ''}">${capitalize(type)}</a>`;
175         }).join("")}
176       </nav>
177
178       <div class="inventory-listing listing">
179         ${Object.keys(sectionedInventory).map((type) => {
180           return `<div class="filter-result inventory-${type} ${type === requestedSlot ? 'active': 'hidden'}" data-filter="${type}" id="filter_${type}">
181           ${type === 'ITEMS' ? renderInventoryItems(sectionedInventory[type]) : renderInventorySection(sectionedInventory[type])}
182           </div>`;
183         }).join("")}
184       </div>
185     </div>
186   </div>
187   `;
188
189   return html;
190
191 }