chore(release): 0.3.1
[risinglegends.git] / src / server / views / stores.ts
1 import { ShopEquipment } from "../../shared/inventory";
2 import { ShopItem, Item } from "../../shared/items";
3 import { capitalize, merge } from "lodash";
4 import { Player } from "../../shared/player";
5 import { LocationWithCity } from "shared/map";
6 import { ProgressBar } from "./components/progress-bar";
7 import { BackToTown } from "./components/button";
8
9 type RenderStatOptions = {
10   unsigned: boolean
11 }
12
13 function renderStat(title: string, display: string, val: any, options?: RenderStatOptions): string {
14   const opts = merge({
15     unsigned: false
16   } as RenderStatOptions, options);
17
18   let valSign: string = '';
19   if(typeof val === 'number') {
20     valSign = val > 0 ? '+' : '-';
21   }
22
23   return `<span title="${title}"><span class="requirement-title">${display}</span>: <span class="requirement-value ${opts.unsigned ? '' : (val > 0 ? "success": "error")}">${opts.unsigned ? val : `${valSign}${val}`}</span></span>`;
24 }
25
26 function renderStatBoost(name: string, val: number | string, title?: string): string {
27   let valSign: string = '';
28   if(typeof val === 'number') {
29     valSign = val > 0 ? '+' : '-';
30   }
31   return `<span class="requirement-title" title="${title}">${name}</span>: <span class="requirement-value ${typeof val === 'number' ? (val > 0 ? "success": "error") : ""}">${valSign}${val}</span>`;
32 }
33
34 function renderRequirement(name: string, val: number | string, currentVal?: number): string {
35   let colorIndicator = '';
36   if(currentVal) {
37     colorIndicator = currentVal >= val ? 'success' : 'error';
38   }
39   return `<span class="requirement-title">${name}</span>: <span class="requirement-value ${colorIndicator}">${val.toLocaleString()}</span>`;
40 }
41
42 function renderShopItem(item: (ShopItem & Item), action: (item: (ShopItem & Item)) => string): string {
43     return `<div class="store-list">
44     <div class="store-icon">
45       <img src="/assets/img/icons/items/${item.icon_name}" title="${item.name}"><div class="store-actions">${action(item)}</div>
46     </div>
47     <div class="details">
48       <div class="name">${item.name}</div>
49       <div class="requirements">
50         ${item.description}
51       </div>
52       ${item.hasOwnProperty('id') ? `<div class="store-cost">${item.price_per_unit.toLocaleString()}G</div>` : ''}
53     </div>
54     </div>`;
55 }
56
57 export function renderEquipmentDetails(item: ShopEquipment, player: Player): string {
58   const isSpell = item.type === 'SPELL';
59
60   return `
61     <div class="details">
62       <div class="name">${item.name}${item.equipment_slot === 'TWO_HANDED' ? ' (2H)': ''}</div>
63       <div class="requirements">
64       ${item.requirements.level ? renderRequirement('LVL', item.requirements.level, player.level): ''}
65       ${item.requirements.strength ? renderRequirement('STR', item.requirements.strength, player.strength): ''}
66       ${item.requirements.constitution ? renderRequirement('CON', item.requirements.constitution, player.constitution): ''}
67       ${item.requirements.dexterity ? renderRequirement('DEX', item.requirements.dexterity, player.dexterity): ''}
68       ${item.requirements.intelligence ? renderRequirement('INT', item.requirements.intelligence, player.intelligence): ''}
69       ${renderRequirement('PRF', item.profession)}
70       </div>
71       <div class="stat-mods">
72       ${item.boosts.defence ? renderStatBoost('DEF', item.boosts.defence) : ''}
73       ${item.boosts.strength ? renderStatBoost('STR', item.boosts.strength) : ''}
74       ${item.boosts.constitution ? renderStatBoost('CON', item.boosts.constitution) : ''}
75       ${item.boosts.dexterity ? renderStatBoost('DEX', item.boosts.dexterity) : ''}
76       ${item.boosts.intelligence ? renderStatBoost('INT', item.boosts.intelligence) : ''}
77       ${item.boosts.damage ? renderStatBoost(item.affectedSkills.includes('restoration_magic') ? 'HP' : 'DMG', item.boosts.damage) : ''}
78       ${item.boosts.damage_mitigation ? renderStatBoost('MIT', item.boosts.damage_mitigation.toString())+'%' : ''}
79       ${renderStat(isSpell ? 'Uses': 'Durability', isSpell ? 'Uses': 'DUR', item.maxAp, { unsigned: true })}
80       </div>
81       ${item.hasOwnProperty('id') ? `<div class="store-cost">${item.cost.toLocaleString()}G</div>` : ''}
82     </div>
83 `
84
85 }
86
87 function renderShopEquipment(item: ShopEquipment, action: (item: ShopEquipment) => string, player: Player): string {
88     return `<div class="store-list">
89     <div class="store-icon" style="background-image: url('${item.icon ? `/assets/img/icons/equipment/${item.icon}` : 'https://via.placeholder.com/64x64'}')">
90       <div class="store-actions">${action(item)}</div>
91     </div>
92     ${renderEquipmentDetails(item, player)}
93     </div>`;
94 }
95
96
97
98 export async function renderStore(equipment: ShopEquipment[], items: (ShopItem & Item)[], player: Player, location: LocationWithCity): Promise<string> {
99   const listing: Record<string, string> = {};
100   const listingTypes = new Set<string>();
101
102   equipment.forEach(item => {
103     const filter = item.type === 'ARMOUR' ? item.equipment_slot : item.type;
104
105     listingTypes.add(filter);
106     if(!listing[filter]) {
107       listing[filter] = '';
108     }
109
110     listing[filter] += renderShopEquipment(item, i => {
111       const id = `data-id="${i.id}"`;
112       return `<button type="button" hx-get="/location/${i.location_id}/equipment/${i.id}/overview" hx-target="#modal-wrapper">Buy</button>`
113     }, player);
114
115   });
116
117   if(items && items.length) {
118     listingTypes.add('ITEMS');
119     listing['ITEMS'] = '';
120     items.forEach(item => {
121       listing['ITEMS'] += renderShopItem(item, i => {
122         return `<button type="button" hx-get="/location/${i.location_id}/items/${i.id}/overview" hx-target="#modal-wrapper">Buy</button>`;
123       });
124     });
125   }
126
127   let activeTab: string = listingTypes.keys().next().value;
128
129   const nav: string[] = [];
130   const finalListing: string[] = [];
131
132   listingTypes.forEach(type => {
133     nav.push(`<a href="#" data-filter="${type}" class="${activeTab === type ? 'active': ''}">${capitalize(type)}</a>`);
134     finalListing.push(`<div class="filter-result ${activeTab === type ? 'active': 'hidden'}" data-filter="${type}" id="filter_${type}">${listing[type]}</div>`);
135   });
136
137   let html = `
138 <div class="city-title-wrapper"><div class="city-title">${location.city_name}</div></div>
139 <div class="city-details">
140 <h3 class="location-name"><span>${location.name}</span></h3>
141 <div class="shop-inventory-listing filter-container">
142     <nav class="filter" id="shop-inventory-listing">${nav.join("")}</nav><div class="inventory-listing listing">
143       ${finalListing.join("\n")}
144     </div>
145   </div>
146 ${BackToTown()}
147 </div>`;
148
149   return html;
150 }