chore(release): 0.3.0 v0.3.0
authorxangelo <me@xangelo.ca>
Wed, 30 Aug 2023 13:51:33 +0000 (09:51 -0400)
committerxangelo <me@xangelo.ca>
Wed, 30 Aug 2023 13:51:33 +0000 (09:51 -0400)
29 files changed:
CHANGELOG.md
migrations/20230825165327_vigor.ts [new file with mode: 0644]
package-lock.json
package.json
public/assets/css/game.css
public/index.html
seeds/monsters.ts
seeds/shop_items.ts
src/server/api.ts
src/server/equipment.ts
src/server/fight.ts
src/server/inventory.ts
src/server/locations/healer/index.ts
src/server/locations/recruiter.ts
src/server/locations/repair.ts [new file with mode: 0644]
src/server/monster.ts
src/server/player.ts
src/server/views/components/city.ts [new file with mode: 0644]
src/server/views/components/progress-bar.ts
src/server/views/inventory.ts
src/server/views/monster-selector.ts
src/server/views/player-bar.ts
src/server/views/profile.ts
src/server/views/repair.ts [new file with mode: 0644]
src/server/views/skills.ts
src/server/views/stores.ts
src/shared/inventory.ts
src/shared/monsters.ts
src/shared/player.ts

index a627b745a538392cd858e388e5e1393b14ee4984..1c03855005e14901b38bae8d9841851c90a09a8d 100644 (file)
@@ -2,6 +2,26 @@
 
 All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
 
 
 All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
 
+## [0.3.0](https://git.xangelo.ca/?p=risinglegends.git;a=commitdiff;h=v0.3.0;hp=v0.2.17;ds=sidebyside) (2023-08-30)
+
+
+### ⚠ BREAKING CHANGES
+
+* vigor mortensen
+
+### Features
+
+* display optimal level range for monsters 5878793
+* move alerts to bottom of main section 9575cfb
+* repairing damaged equipment 161b5bf
+* unequip items if they hit 0 ap in battle bc9e05f
+* vigor mortensen f6aba7a
+
+
+### Bug Fixes
+
+* spacing for stat increase button 61e6d07
+
 ### [0.2.17](https://git.xangelo.ca/?p=risinglegends.git;a=commitdiff;h=v0.2.17;hp=v0.2.16;ds=sidebyside) (2023-08-25)
 
 
 ### [0.2.17](https://git.xangelo.ca/?p=risinglegends.git;a=commitdiff;h=v0.2.17;hp=v0.2.16;ds=sidebyside) (2023-08-25)
 
 
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 291569862dc2efb25eabb75a307c109476547a6d..d404a7a38182533a63db77d92b2307d933956c8c 100644 (file)
@@ -1,12 +1,12 @@
 {
   "name": "rising-legends",
 {
   "name": "rising-legends",
-  "version": "0.2.17",
+  "version": "0.3.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "rising-legends",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "rising-legends",
-      "version": "0.2.17",
+      "version": "0.3.0",
       "dependencies": {
         "@honeycombio/opentelemetry-node": "^0.4.0",
         "@opentelemetry/auto-instrumentations-node": "^0.37.0",
       "dependencies": {
         "@honeycombio/opentelemetry-node": "^0.4.0",
         "@opentelemetry/auto-instrumentations-node": "^0.37.0",
index 8f5109fc33d43ce9e6f1df88bc5a1e938a3dc2d9..5fc62422b80e777aeee89297b459d521ef5449fd 100644 (file)
@@ -1,7 +1,7 @@
 {
   "name": "rising-legends",
   "private": true,
 {
   "name": "rising-legends",
   "private": true,
-  "version": "0.2.17",
+  "version": "0.3.0",
   "scripts": {
     "up": "npx prisma migrate dev --name \"init\"",
     "start": "pm2 start dist/server/api.js",
   "scripts": {
     "up": "npx prisma migrate dev --name \"init\"",
     "start": "pm2 start dist/server/api.js",
index 7afe6295627ce058ac4c73e848c825882b788092..b288eefb7ec4f0be9930fbd52370d63416188417 100644 (file)
@@ -113,6 +113,9 @@ p:last-child {
   background-color: #fff;
   margin-bottom: 1rem;
 }
   background-color: #fff;
   margin-bottom: 1rem;
 }
+#announcements {
+  margin: 1rem 0;
+}
 
 #signup {
   display: flex;
 
 #signup {
   display: flex;
@@ -155,9 +158,16 @@ dialog .close-modal {
   }
 }
 
   }
 }
 
+#alerts {
+  position: relative;
+}
 .alert {
   padding: 0.3rem;
 .alert {
   padding: 0.3rem;
-  margin-bottom: 0.3rem;
+  max-width: 17rem;
+  position: absolute;
+  bottom: 1rem;
+  right: 0;
+  line-height: 1.2rem;
 }
 .alert.success, button.success {
   border: solid 1px #0a0;
 }
 .alert.success, button.success {
   border: solid 1px #0a0;
@@ -363,15 +373,23 @@ nav.filter-result.active {
   padding: 1rem;
 }
 
   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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);
 }
   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;
   padding: 0.5rem;
+  min-width: 100px;
+  line-height: 1rem;
+}
+.stat-breakdown tr:nth-child(even) {
+  background-color: #c7b7a1;
+}
+.increase-stat {
+  padding: 1px 6px;
 }
 
 #explore {
 }
 
 #explore {
@@ -638,21 +656,31 @@ h3 {
 #skill-list {
   width: 100%;
 }
 #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-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 {
 #skill-list .skill-description {
-  padding: 0 0.6rem;
+  padding:  0 0.6rem 0.6rem;
   line-height: 1.2rem;
 }
 #skill-list .skill-exp {
   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 +743,30 @@ footer {
   margin-top: 2rem;
   text-align: center;
 }
   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..144ad914d8fdb1c5db15bc7225aeee486ea6efca 100644 (file)
@@ -44,9 +44,7 @@
       </header>
 
       <div id="signup-prompt" class="hidden"></div>
       </header>
 
       <div id="signup-prompt" class="hidden"></div>
-      <div id="announcements" class="hidden"></div>
 
 
-      <div id="alerts"></div>
       <div id="modal-wrapper"></div>
 
       <div id="main-nav">
       <div id="modal-wrapper"></div>
 
       <div id="main-nav">
@@ -64,6 +62,7 @@
           </div>
         </section>
       </div>
           </div>
         </section>
       </div>
+      <div id="alerts"></div>
 
       <section id="chat">
         <div id="chat-messages" hx-trigger="load delay:1s" hx-get="/chat/history" hx-swap="afterbegin"></div>
 
       <section id="chat">
         <div id="chat-messages" hx-trigger="load delay:1s" hx-get="/chat/history" hx-swap="afterbegin"></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,
           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'
           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'],
             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'],
           },
           currentAp: r.fields['Armour Points'],
           maxAp: r.fields['Armour Points'],
index 60c2b5c6b019ab40bc99c1b172e05bd1bcb3922a..77d54b28c942efb5ae375e78686ae144d9a7e84e 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 { 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';
 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';
@@ -28,8 +28,9 @@ import { getPlayerSkills} from './skills';
 
 import { fightRound, blockPlayerInFight } from './fight';
 
 
 import { fightRound, blockPlayerInFight } from './fight';
 
-import  { router as healerRouter } from './locations/healer';
+import { router as healerRouter } from './locations/healer';
 import { router as professionRouter } from './locations/recruiter';
 import { router as professionRouter } from './locations/recruiter';
+import { router as repairRouter } from './locations/repair';
 
 import * as Alert from './views/alert';
 import { renderPlayerBar } from './views/player-bar'
 
 import * as Alert from './views/alert';
 import { renderPlayerBar } from './views/player-bar'
@@ -130,6 +131,7 @@ io.on('connection', async socket => {
 
 app.use(healerRouter);
 app.use(professionRouter);
 
 app.use(healerRouter);
 app.use(professionRouter);
+app.use(repairRouter);
 
 
 app.get('/chat/history', authEndpoint, async (req: AuthRequest, res: Response) => {
 
 
 app.get('/chat/history', authEndpoint, async (req: AuthRequest, res: Response) => {
@@ -183,12 +185,12 @@ app.post('/chat', authEndpoint, async (req: AuthRequest, res: Response) => {
 });
 
 app.get('/player', 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) => {
 });
 
 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`));
   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 +205,11 @@ app.post('/player/stat/:stat', authEndpoint, async (req: AuthRequest, res: Respo
   req.player.stat_points -= 1;
   req.player[stat]++;
 
   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);
 
   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) => {
 });
 
 app.get('/player/skills', authEndpoint, async (req: AuthRequest, res: Response) => {
@@ -273,7 +276,7 @@ app.post('/player/equip/:item_id/:slot', authEndpoint, blockPlayerInFight, async
     getPlayersItems(req.player.id)
   ]);
 
     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) => {
 });
 
 app.post('/player/unequip/:item_id', authEndpoint, blockPlayerInFight, async (req: AuthRequest, res: Response) => {
@@ -287,7 +290,7 @@ app.post('/player/unequip/:item_id', authEndpoint, blockPlayerInFight, async (re
     getPlayersItems(req.player.id)
   ]);
 
     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) => {
 });
 
 app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response) => {
@@ -299,7 +302,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;
   }
 
       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,
   if(fight) {
     const data: MonsterForFight = {
       id: fight.id,
@@ -312,7 +314,7 @@ app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response)
     const location = await getMonsterLocation(fight.ref_id);
 
 
     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) {
   }
   else {
     if(travelPlan) {
@@ -327,7 +329,7 @@ app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response)
       // STEP_DELAY
       const nextAction = cache[`step:${req.player.id}`] || 0;
 
       // 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,
         things,
         nextAction,
         closestTown: closestTown,
@@ -343,7 +345,7 @@ app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response)
         getAllPaths(req.player.city_id)
       ]);
 
         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 +370,7 @@ app.put('/location/:location_id/equipment/:item_id', authEndpoint, async (req: A
   await updatePlayer(req.player);
   await addInventoryItem(req.player.id, item);
 
   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
 });
 
 // used to purchase items from a particular shop
@@ -392,9 +392,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);
 
   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 
 });
 
 // used to display equipment modals in a store, validates that 
@@ -491,12 +489,11 @@ app.put('/item/:item_id', authEndpoint, async (req: AuthRequest, res: Response)
   await updatePlayer(req.player);
 
   const inventory = await getInventory(req.player.id);
   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(
     [
   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("")
       renderInventoryPage(inventory, items, 'ITEMS'),
       Alert.SuccessAlert(`You used the ${item.name}`)
     ].join("")
@@ -621,8 +618,7 @@ app.post('/fight/turn', authEndpoint, async (req: AuthRequest, res: Response) =>
     travelSection = travelButton(0);
   }
 
     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);
 });
 
   res.send(html + travelSection + playerBar);
 });
@@ -741,7 +737,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);
   // 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) {
 
   const fight = await loadMonsterFromFight(req.player.id);
   if(fight) {
@@ -756,7 +751,7 @@ app.post('/travel/return-to-source', authEndpoint, async (req: AuthRequest, res:
     };
     const location = await getMonsterLocation(fight.ref_id);
 
     };
     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([
   }
   else {
     const [city, locations, paths] = await Promise.all([
@@ -765,7 +760,7 @@ app.post('/travel/return-to-source', authEndpoint, async (req: AuthRequest, res:
       getAllPaths(req.player.city_id)
     ]);
 
       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 9ebac7d80318be815dc91c6a212c51723c032247..d1a73bad9e06a9f640eaaecda037e19907bf96b4 100644 (file)
@@ -40,3 +40,9 @@ export async function unequip(player_id: string, item_id: string) {
     item_id
   }).delete();
 }
     item_id
   }).delete();
 }
+
+export async function unequipItems(player_id: string, item_ids: string[]) {
+  return db('equipped').where({
+    player_id
+  }).whereIn('item_id', item_ids).delete();
+}
index 3ceb706031e1cf4520a18177b0fd0bcca53e9a46..2c9383d19b361f22d2c36fbd40420f151c64a85c 100644 (file)
@@ -1,11 +1,11 @@
 import {FightRound} from '../shared/fight';
 import { clearFight, loadMonster, getMonsterList, saveFightState, loadMonsterFromFight } from './monster';
 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 { clearTravelPlan } from './map';
 import { updatePlayer } from './player';
-import { getEquippedItems, updateAp, deleteInventoryItem } from './inventory';
+import { getEquippedItems, updateAp } from './inventory';
 import { EquippedItemDetails } from '../shared/equipped';
 import { EquippedItemDetails } from '../shared/equipped';
-import { ArmourEquipmentSlot, EquipmentSlot } from '../shared/inventory';
+import { EquipmentSlot } from '../shared/inventory';
 import { MonsterWithFaction, MonsterForFight } from '../shared/monsters';
 import { getPlayerSkillsAsObject, updatePlayerSkills } from './skills';
 import { SkillID, Skills } from '../shared/skills';
 import { MonsterWithFaction, MonsterForFight } from '../shared/monsters';
 import { getPlayerSkillsAsObject, updatePlayerSkills } from './skills';
 import { SkillID, Skills } from '../shared/skills';
@@ -57,27 +57,8 @@ export async function fightRound(player: Player, monster: MonsterWithFaction,  d
   // so they can "fight again"
   let potentialMonsters: MonsterForFight[] = [];
 
   // 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 = {
   const boost = {
+    defence: totalDefence(equippedItems, player),
     strength: 0,
     constitution: 0,
     dexterity: 0,
     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';
   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 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> = {};
 
   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);
 
   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;
 
   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);
       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') {
     }
     // get the monster location if it was an EXPLORED fight
     if(roundData.fightTrigger === 'explore') {
@@ -249,38 +204,32 @@ export async function fightRound(player: Player, monster: MonsterWithFaction,  d
       });
     }
 
       });
     }
 
+    player.vigor -= 1;
+    if(player.vigor < 0) {
+      player.vigor = 0;
+    }
+
+    const unequippedItems = await updateAp(player.id, 1, equippedItems.map(i => i.item_id));
     await clearFight(player.id);
     await updatePlayer(player);
     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);
+    if(unequippedItems.length) {
+      unequippedItems.forEach(i => {
+        roundData.roundDetails.push(`Your ${i.name} was too damaged and was unequipped!`);
+      });
     }
 
     }
 
+    return { roundData, monsters: potentialMonsters, player };
   }
   }
-  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)) {
   if(playerFinalHeal > 0) {
     player.hp += playerFinalHeal;
     if(player.hp > maxHp(player.constitution, player.level)) {
@@ -289,18 +238,24 @@ export async function fightRound(player: Player, monster: MonsterWithFaction,  d
     roundData.roundDetails.push(`You healed for ${playerFinalHeal} HP`);
   }
 
     roundData.roundDetails.push(`You healed for ${playerFinalHeal} HP`);
   }
 
-  // update the players inventory for this item!
-
   if(player.hp <= 0) {
     player.hp = 0;
   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);
     roundData.winner = 'monster';
 
     roundData.roundDetails.push(`You were killed by the ${monster.name}`);
 
     await clearFight(player.id);
+    const unequippedItems = await updateAp(player.id, 5, equippedItems.map(i => i.item_id));
     await updatePlayer(player);
     await clearTravelPlan(player.id);
 
     await updatePlayer(player);
     await clearTravelPlan(player.id);
 
+    if(unequippedItems.length) {
+      unequippedItems.forEach(i => {
+        roundData.roundDetails.push(`Your ${i.name} was too damaged and was unequipped!`);
+      });
+    }
+
     return { roundData, monsters: [], player};
   }
 
     return { roundData, monsters: [], player};
   }
 
index 88bfc87da5a19d1b4da990f2f9c6a06abe33a1b0..3074644501ca3b708bacefd05938b1e515b1ca96 100644 (file)
@@ -2,6 +2,7 @@ import {InventoryItem, ShopEquipment} from "../shared/inventory";
 import { v4 as uuid } from 'uuid';
 import { db} from './lib/db';
 import {EquippedItemDetails} from "../shared/equipped";
 import { v4 as uuid } from 'uuid';
 import { db} from './lib/db';
 import {EquippedItemDetails} from "../shared/equipped";
+import { unequipItems } from "./equipment";
 
 
 export async function addInventoryItem(playerId: string, item: ShopEquipment) {
 
 
 export async function addInventoryItem(playerId: string, item: ShopEquipment) {
@@ -29,6 +30,7 @@ export async function addInventoryItem(playerId: string, item: ShopEquipment) {
       intelligence: item.boosts.intelligence,
       damage: item.boosts.damage,
       damage_mitigation: item.boosts.damage_mitigation,
       intelligence: item.boosts.intelligence,
       damage: item.boosts.damage,
       damage_mitigation: item.boosts.damage_mitigation,
+      defence: item.boosts.defence
     },
     maxAp: item.maxAp,
     currentAp: item.currentAp,
     },
     maxAp: item.maxAp,
     currentAp: item.currentAp,
@@ -76,14 +78,29 @@ 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({
+export async function updateAp(player_id: string, apDamage: number, itemIds: string[]) {
+  const res: {item_id: string, currentAp: number, name: string}[] = await db('inventory').where({
+    player_id
+  }).whereIn('item_id', itemIds).update({
+      'currentAp': db.raw(`GREATEST("currentAp" - ${apDamage}, 0)`)
+    }).returning(['item_id', 'name', 'currentAp']);
+  
+  const itemsToUnequip = res.filter(i => i.currentAp <= 0);
+
+  if(itemsToUnequip.length) {
+    await unequipItems(player_id, itemsToUnequip.map(i => i.item_id));
+  }
+
+  return itemsToUnequip;
+}
+
+export async function repair(player_id: string, item_id: string) {
+  return db('inventory').where({
     player_id,
     item_id
     player_id,
     item_id
-  })
+  }).update({
+      'currentAp': db.raw('"maxAp"')
+    });
 }
 
 export async function deleteInventoryItem(player_id: string, item_id: string) {
 }
 
 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 { 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 { 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();
 
 
 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>`);
 
 
   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)) {
   }
   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;
 
   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);
 
   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);
     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>');
 
     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>
 ${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 * 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>`;
 
 function p(str: string) {
   return `<p>${str}</p>`;
@@ -108,10 +107,9 @@ router.post('/city/services/profession_change/:location_id', authEndpoint, async
   }
 
   if(update) {
   }
 
   if(update) {
-    const equipped = await getEquippedItems(req.player.id);
     req.player.level = update.level;
     req.player.exp = update.exp;
     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>`);
   }
 
 });
   }
 
 });
diff --git a/src/server/locations/repair.ts b/src/server/locations/repair.ts
new file mode 100644 (file)
index 0000000..6bb7423
--- /dev/null
@@ -0,0 +1,70 @@
+import { Response, Router } from "express";
+import { authEndpoint, AuthRequest } from '../auth';
+import { logger } from "../lib/logger";
+import { getService } from "../map";
+import { getInventory, getInventoryItem, repair } from '../inventory';
+import { renderRepairService } from '../views/repair';
+import { repairCost } from "../../shared/inventory";
+import * as Alert from "../views/alert";
+import { updatePlayer } from "../player";
+import { renderPlayerBar } from "../views/player-bar";
+
+export const router = Router();
+
+router.get('/city/services/repair/:location_id', authEndpoint, async(req: AuthRequest, res: Response) => {
+  const service = await getService(parseInt(req.params.location_id));
+
+  if(!service || service.city_id !== req.player.city_id) {
+    logger.log(`Invalid location: [${req.params.location_id}]`);
+    res.sendStatus(400);
+  }
+
+  const equippedItems = await getInventory(req.player.id);
+
+  const damaged = equippedItems.filter(i => {
+    return i.currentAp < i.maxAp
+  });
+
+  res.send(renderRepairService(damaged, req.player, service));
+});
+
+router.post('/city/services/:location_id/repair/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
+  const service = await getService(parseInt(req.params.location_id));
+
+  if(!service || service.city_id !== req.player.city_id) {
+    logger.log(`Invalid location: [${req.params.location_id}]`);
+    res.sendStatus(400);
+  }
+
+  const item = await getInventoryItem(req.player.id, req.params.item_id);
+
+  if(!item) {
+    logger.log(`Invalid item [${req.params.item_id}]`);
+    res.sendStatus(400);
+  }
+
+  const cost = repairCost(item);
+
+  if(req.player.gold < cost) {
+    res.status(400).send(Alert.ErrorAlert(`You need at least ${cost}G to repair your ${item.name}`));
+    return;
+  }
+
+  req.player.gold -= cost;
+  item.currentAp = item.maxAp;
+
+  await updatePlayer(req.player);
+  await repair(req.player.id, item.item_id);
+
+  const equippedItems = await getInventory(req.player.id);
+
+  const damaged = equippedItems.filter(i => {
+    return i.currentAp < i.maxAp
+  });
+
+  res.send(
+    renderRepairService(damaged, req.player, service) 
+      + renderPlayerBar(req.player)
+    + Alert.SuccessAlert(`You repaired your ${item.name} for ${cost}G`)
+  );
+});
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,
     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
     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,
       id: player.id
     }).update({
       hp: player.hp,
+      vigor: player.vigor,
       strength: player.strength,
       constitution: player.constitution,
       dexterity: player.dexterity,
       strength: player.strength,
       constitution: player.constitution,
       dexterity: player.dexterity,
diff --git a/src/server/views/components/city.ts b/src/server/views/components/city.ts
new file mode 100644 (file)
index 0000000..18eb3e2
--- /dev/null
@@ -0,0 +1,12 @@
+export function Title(str: string): string {
+  return `<div class="city-title-wrapper"><div class="city-title">${str}</div></div>`;
+}
+
+export function Details(name: string, content: string): string {
+  return `
+  <div class="city-details">
+  <h3 class="location-name"><span>${name}</span></h3>
+    ${content}
+  </div>
+  `;
+}
index 7d7677d9d3383dcfd12728d82795aba0df3fa2be..9d9a290ff47b4bcc2fc96b8a42cef276928695ce 100644 (file)
@@ -1,14 +1,24 @@
 export interface ProgressBarOptions {
   startingColor: string;
 export interface ProgressBarOptions {
   startingColor: string;
-  endingColor: string;
+  endingColor?: string;
+  title?: string
+  displayPercent?: boolean;
 }
 
 export function ProgressBar(current: number, max: number, id: string, opts: ProgressBarOptions) {
 }
 
 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;
   let percent = 0;
+
   if(max > 0) {
     percent = Math.floor((current / max) * 100);
   }
 
   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..e21d1fd2e74bbcedc94459ffb968dc20e22102dc 100644 (file)
@@ -106,13 +106,14 @@ function renderInventoryItem(item: EquippedItemDetails , action: (item: Equipped
         ${renderRequirement('PRF', item.profession)}
       </div>
       <div class="stat-mods">
         ${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) : ''}
         ${item.boosts.intelligence ? renderStatBoost('INT', item.boosts.intelligence) : ''}
         ${item.boosts.damage ? renderStatBoost('DMG', item.boosts.damage) : ''}
         ${item.boosts.damage_mitigation ? renderStatBoost('MIT', item.boosts.damage_mitigation.toString())+'%' : ''}
         ${item.boosts.strength ? renderStatBoost('STR', item.boosts.strength) : ''}
         ${item.boosts.constitution ? renderStatBoost('CON', item.boosts.constitution) : ''}
         ${item.boosts.dexterity ? renderStatBoost('DEX', item.boosts.dexterity) : ''}
         ${item.boosts.intelligence ? renderStatBoost('INT', item.boosts.intelligence) : ''}
         ${item.boosts.damage ? renderStatBoost('DMG', item.boosts.damage) : ''}
         ${item.boosts.damage_mitigation ? renderStatBoost('MIT', item.boosts.damage_mitigation.toString())+'%' : ''}
-        ${['WEAPON','SPELL'].includes(item.type) ? '': generateProgressBar(item.currentAp, item.maxAp, '#7be67b')}
+        ${['SPELL'].includes(item.type) ? '': generateProgressBar(item.currentAp, item.maxAp, '#7be67b')}
       </div>
       ${item.hasOwnProperty('id') ? `<div>${item.cost.toLocaleString()}G</div>` : ''}
       </div>
       </div>
       ${item.hasOwnProperty('id') ? `<div>${item.cost.toLocaleString()}G</div>` : ''}
       </div>
index 24140c6c9b75de1c7dd06568ee8cbc32746e7821..58c8665f480da4a72dd6c6338fc92da04b698434 100644 (file)
@@ -1,3 +1,4 @@
+import { max } from "lodash";
 import { LocationWithCity } from "../../shared/map";
 import { Monster, MonsterForFight } from "../../shared/monsters";
 
 import { LocationWithCity } from "../../shared/map";
 import { Monster, MonsterForFight } from "../../shared/monsters";
 
@@ -21,7 +22,8 @@ export function renderMonsterSelector(monsters: Monster[] | MonsterForFight[], a
   <input type="hidden" name="fightTrigger" value="explore">
   <select id="monsterId" name="monsterId">
   ${monsters.map((monster: (Monster | MonsterForFight)) => {
   <input type="hidden" name="fightTrigger" value="explore">
   <select id="monsterId" name="monsterId">
   ${monsters.map((monster: (Monster | MonsterForFight)) => {
-      return `<option value="${monster.id}" ${monster.id === activeMonsterId ? 'selected': ''}>${monster.name}</option>`;
+      const range = [monster.level, monster.level + 3];
+      return `<option value="${monster.id}" ${monster.id === activeMonsterId ? 'selected': ''}>${monster.name} (${range[0]} - ${range[1]})</option>`;
   }).join("\n")}
   </select> <button type="submit" class="red">Fight</button></form></div>
 `;
   }).join("\n")}
   </select> <button type="submit" class="red">Fight</button></form></div>
 `;
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 `
 
 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>
   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() : ''}
   `;
     </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>`;
 }
 
 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>
   let statBreakdown = '';
 
   StatDef.forEach(stat => {
     statBreakdown += `<tr>
-      <th>${stat.display}</th>
+      <th title="${stat.description}" tabindex="0">${stat.display}</th>
       <td class="${stat.id}">
       <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">
         ${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}
     ${statBreakdown}
-    <tr><th>Stat Points</th><td class="stat_points">${player.stat_points}</td></tr>
   </table>
   </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;
 }
 
   return html;
 }
diff --git a/src/server/views/repair.ts b/src/server/views/repair.ts
new file mode 100644 (file)
index 0000000..447dc84
--- /dev/null
@@ -0,0 +1,115 @@
+import { capitalize } from "lodash";
+import { Player } from "../../shared/player";
+import { repairCost } from "../../shared/inventory";
+import { LocationWithCity } from "../../shared/map";
+import { EquippedItemDetails } from "../../shared/equipped";
+import { ProgressBar } from "./components/progress-bar";
+import * as City from './components/city';
+
+function renderStatBoost(name: string, val: number | string): string {
+  let valSign: string = '';
+  if(typeof val === 'number') {
+    valSign = val > 0 ? '+' : '-';
+  }
+  return `<span class="requirement-title">${name}</span>: <span class="requirement-value ${typeof val === 'number' ? (val > 0 ? "success": "error") : ""}">${valSign}${val}</span>`;
+}
+
+function renderRequirement(name: string, val: number | string, currentVal?: number): string {
+  let colorIndicator = '';
+  if(currentVal) {
+    colorIndicator = currentVal >= val ? 'success' : 'error';
+  }
+  return `<span class="requirement-title">${name}</span>: <span class="requirement-value ${colorIndicator}">${val.toLocaleString()}</span>`;
+}
+
+export function renderEquipmentDetails(item: EquippedItemDetails, player: Player): string {
+  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) : ''}
+      ${item.boosts.constitution ? renderStatBoost('CON', item.boosts.constitution) : ''}
+      ${item.boosts.dexterity ? renderStatBoost('DEX', item.boosts.dexterity) : ''}
+      ${item.boosts.intelligence ? renderStatBoost('INT', item.boosts.intelligence) : ''}
+      ${item.boosts.damage ? renderStatBoost(item.affectedSkills.includes('restoration_magic') ? 'HP' : 'DMG', item.boosts.damage) : ''}
+      ${item.boosts.damage_mitigation ? renderStatBoost('MIT', item.boosts.damage_mitigation.toString())+'%' : ''}
+      ${['WEAPON','SPELL'].includes(item.type) ? '' :  ProgressBar(item.currentAp, item.maxAp, `${item.item_id}-ap`, {
+displayPercent: false,
+title: 'Durability',
+startingColor: '#7be67b',
+endingColor: '#7be67b'
+})}
+      </div>
+      <div class="store-cost">${repairCost(item).toLocaleString()}G to Repair</div>
+    </div>
+`
+
+}
+
+function renderEquipmentToRepair(item: EquippedItemDetails, action: (item: EquippedItemDetails) => string, player: Player): string {
+    return `<div class="store-list">
+    <div class="store-icon" style="background-image: url('${item.icon ? `/assets/img/icons/equipment/${item.icon}` : 'https://via.placeholder.com/64x64'}')">
+      <div class="store-actions">${action(item)}</div>
+    </div>
+    ${renderEquipmentDetails(item, player)}
+    </div>`;
+}
+
+
+
+export function renderRepairService(equipment: EquippedItemDetails[], player: Player, location: LocationWithCity): string {
+  const listing: Record<string, string> = {};
+  const listingTypes = new Set<string>();
+
+  if(equipment.length === 0) {
+    return `
+      ${City.Title(location.city_name)}
+      ${City.Details(location.name, `You don't have any equipment that needs repairing.`)}
+    `;
+  }
+
+  equipment.forEach(item => {
+    const filter = item.type === 'ARMOUR' ? item.equipment_slot : item.type;
+
+    listingTypes.add(filter);
+    if(!listing[filter]) {
+      listing[filter] = '';
+    }
+
+    listing[filter] += renderEquipmentToRepair(item, i => {
+      return `<button type="button" hx-post="/city/services/${location.id}/repair/${i.item_id}" hx-target="#explore">Repair</button>`
+    }, player);
+
+  });
+
+  let activeTab: string = listingTypes.keys().next().value;
+
+  const nav: string[] = [];
+  const finalListing: string[] = [];
+
+  listingTypes.forEach(type => {
+    nav.push(`<a href="#" data-filter="${type}" class="${activeTab === type ? 'active': ''}">${capitalize(type)}</a>`);
+    finalListing.push(`<div class="filter-result ${activeTab === type ? 'active': 'hidden'}" data-filter="${type}" id="filter_${type}">${listing[type]}</div>`);
+  });
+
+  let html = `
+    ${City.Title(location.city_name)}
+    ${City.Details(location.name, `
+<div class="shop-inventory-listing filter-container">
+    <nav class="filter" id="shop-inventory-listing">${nav.join("")}</nav><div class="inventory-listing listing">
+      ${finalListing.join("\n")}
+    </div>
+  </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>
     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")}
     </tr>
     `;
   }).join("\n")}
index 81b227373b4aaff559de06b1e2f4c378a785eae2..be20cf07aece0951e8b30995835ff756b6f91703 100644 (file)
@@ -1,15 +1,33 @@
 import { ShopEquipment } from "../../shared/inventory";
 import { ShopItem, Item } from "../../shared/items";
 import { ShopEquipment } from "../../shared/inventory";
 import { ShopItem, Item } from "../../shared/items";
-import { capitalize } from "lodash";
+import { capitalize, merge } from "lodash";
 import { Player } from "../../shared/player";
 import { LocationWithCity } from "shared/map";
 import { Player } from "../../shared/player";
 import { LocationWithCity } from "shared/map";
+import { ProgressBar } from "./components/progress-bar";
 
 
-function renderStatBoost(name: string, val: number | string): string {
+type RenderStatOptions = {
+  unsigned: boolean
+}
+
+function renderStat(title: string, display: string, val: any, options?: RenderStatOptions): string {
+  const opts = merge({
+    unsigned: false
+  } as RenderStatOptions, options);
+
+  let valSign: string = '';
+  if(typeof val === 'number') {
+    valSign = val > 0 ? '+' : '-';
+  }
+
+  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>`;
+}
+
+function renderStatBoost(name: string, val: number | string, title?: string): string {
   let valSign: string = '';
   if(typeof val === 'number') {
     valSign = val > 0 ? '+' : '-';
   }
   let valSign: string = '';
   if(typeof val === 'number') {
     valSign = val > 0 ? '+' : '-';
   }
-  return `<span class="requirement-title">${name}</span>: <span class="requirement-value ${typeof val === 'number' ? (val > 0 ? "success": "error") : ""}">${valSign}${val}</span>`;
+  return `<span class="requirement-title" title="${title}">${name}</span>: <span class="requirement-value ${typeof val === 'number' ? (val > 0 ? "success": "error") : ""}">${valSign}${val}</span>`;
 }
 
 function renderRequirement(name: string, val: number | string, currentVal?: number): string {
 }
 
 function renderRequirement(name: string, val: number | string, currentVal?: number): string {
@@ -48,13 +66,14 @@ export function renderEquipmentDetails(item: ShopEquipment, player: Player): str
       ${renderRequirement('PRF', item.profession)}
       </div>
       <div class="stat-mods">
       ${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) : ''}
       ${item.boosts.intelligence ? renderStatBoost('INT', item.boosts.intelligence) : ''}
       ${item.boosts.damage ? renderStatBoost(item.affectedSkills.includes('restoration_magic') ? 'HP' : 'DMG', item.boosts.damage) : ''}
       ${item.boosts.damage_mitigation ? renderStatBoost('MIT', item.boosts.damage_mitigation.toString())+'%' : ''}
       ${item.boosts.strength ? renderStatBoost('STR', item.boosts.strength) : ''}
       ${item.boosts.constitution ? renderStatBoost('CON', item.boosts.constitution) : ''}
       ${item.boosts.dexterity ? renderStatBoost('DEX', item.boosts.dexterity) : ''}
       ${item.boosts.intelligence ? renderStatBoost('INT', item.boosts.intelligence) : ''}
       ${item.boosts.damage ? renderStatBoost(item.affectedSkills.includes('restoration_magic') ? 'HP' : 'DMG', item.boosts.damage) : ''}
       ${item.boosts.damage_mitigation ? renderStatBoost('MIT', item.boosts.damage_mitigation.toString())+'%' : ''}
-      ${['WEAPON','SPELL'].includes(item.type) ? '' : renderStatBoost('AP', item.maxAp.toString())}
+      ${['SPELL'].includes(item.type) ? '' : renderStat('Durability', 'DUR', item.maxAp, { unsigned: true })}
       </div>
       ${item.hasOwnProperty('id') ? `<div class="store-cost">${item.cost.toLocaleString()}G</div>` : ''}
     </div>
       </div>
       ${item.hasOwnProperty('id') ? `<div class="store-cost">${item.cost.toLocaleString()}G</div>` : ''}
     </div>
index cb4220061ae4cbcac68988c025271476a842e113..0ed26ddef78e988274df8da97b3832ac253911d3 100644 (file)
@@ -33,6 +33,7 @@ export type InventoryItem = {
     intelligence: number;
     damage: number;
     damage_mitigation: number;
     intelligence: number;
     damage: number;
     damage_mitigation: number;
+    defence: number;
   }
   currentAp: number;
   maxAp: number;
   }
   currentAp: number;
   maxAp: number;
@@ -45,3 +46,11 @@ export type ShopEquipment = Omit<InventoryItem, 'id' | 'player_id'> & {
   id: number;
   location_id: number;
 };
   id: number;
   location_id: number;
 };
+
+export function repairCost(item: InventoryItem): number {
+  const totalCost = item.cost * 0.7;
+
+  const damageRatio = 1 - (item.currentAp / item.maxAp);
+
+  return Math.floor(totalCost * damageRatio);
+}
index c2a5fce70ae561dbb82fdd86d08de9b7d51e30ee..c29a4777800047d5e4cc3acfb408940352410aa3 100644 (file)
@@ -11,10 +11,7 @@ export type Monster = {
   gold: number;
   exp: number;
   hp: number;
   gold: number;
   exp: number;
   hp: number;
-  helmAp: number;
-  chestAp: number;
-  armsAp: number;
-  legsAp: number;
+  defence: number;
   maxHp: number;
   location_id: number;
   faction_id: 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 { Profession } from './profession'; 
 import { Stat } from './stats';
 import { SkillDefinition, Skill } from './skills';
+import { EquippedItemDetails } from './equipped';
 
 export type Player = {
   id: string,
 
 export type Player = {
   id: string,
@@ -19,6 +20,7 @@ export type Player = {
   hp: number;
   city_id: number;
   stat_points: number;
   hp: number;
   city_id: number;
   stat_points: number;
+  vigor: number;
 }
 
 export type PlayerWithSkills = Player & {
 }
 
 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));
 }
 
   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;
 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;
 
 export type StatDisplay = {
   id: Stat,
   display: string;
   abbrv: string;
+  description: string;
 }
 
 export const StatDef: Map<Stat, StatDisplay> = new Map<Stat, StatDisplay>();
 }
 
 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',
 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',
 });
 
 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',
 });
 
 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',
 });
 
 StatDef.set(Stat.intelligence, {
   id: Stat.intelligence,
   display: 'Intelligence',
-  abbrv: 'INT'
+  abbrv: 'INT',
+  description: 'Affects your magical damage'
 });
 });