feat: new UI
authorxangelo <me@xangelo.ca>
Tue, 15 Aug 2023 18:31:53 +0000 (14:31 -0400)
committerxangelo <me@xangelo.ca>
Tue, 15 Aug 2023 18:31:53 +0000 (14:31 -0400)
This is a huge overhaul of the existing UI away from the temp white
boxes setup to something that embodies the game a bit more. No
functionality has changed, but there's been a ton of CSS updates to
ensure that we keep load times short but still provide a good looking
experience to players.

19 files changed:
public/assets/css/game.css
public/assets/font/BreatheFire.woff [new file with mode: 0644]
public/assets/font/BreatheFire.woff2 [new file with mode: 0644]
public/assets/font/demo.html [new file with mode: 0644]
public/assets/font/stylesheet.css [new file with mode: 0644]
public/index.html
src/server/api.ts
src/server/locations/healer/index.ts
src/server/map.ts
src/server/monster.ts
src/server/views/fight.ts
src/server/views/inventory.ts
src/server/views/map.ts
src/server/views/monster-selector.ts
src/server/views/player-bar.ts
src/server/views/stores.ts
src/server/views/travel.ts
src/shared/map.ts
src/shared/travel.ts

index 8f546dcd22d03ab2c87831a808f6c19536c21f0d..b29a4ec21a5a6af341bc869a0e98061e4402584f 100644 (file)
@@ -1,3 +1,12 @@
+@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;
+}
+
 body {
   margin: 1rem auto 2rem;
   background-color: #eee;
@@ -5,35 +14,48 @@ body {
   max-width: 724px;
   height: 100vh;
 }
-#time-of-day {
+.title-font {
+  font-family: 'Breathe Fire', monospace;
+}
+#title-bar {
   background-color: transparent;
-  color: invert;
+  margin-top: 0.5rem;
+  margin-bottom: 1.5rem;
   border: 0;
-  margin-bottom: 1rem;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+#title-bar a {
+  font-size: 3rem;
+  color: #8e4607;
+  text-decoration: none;
+  letter-spacing: 0.3rem;
+  mix-blend-mode: color-burn;
+  border-bottom: solid 4px;
+  line-height: 25px;
+}
+#time-of-day {
   text-align: right;
 }
 #time-of-day img {
   width: 32px;
   vertical-align: middle;
-}
-.night #time-of-day, .evening #time-of-day {
-  color: #fff;
+  mix-blend-mode: color-burn;
 }
 #view {
   font-size: 14px;
-  background-color: #fff;
   padding: 1rem;
   border: 1px solid #000;
-}
-:disabled {
-  background-color: #aaa;
-  cursor: not-allowed;
+  box-shadow: 2px 3px 20px black, 0 0 60px #8a4d0f inset;
+  background: #fffef0;
+  background-image: url(data:image/png;base64,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);
 }
 b {
   font-weight: bold;
 }
 a {
-  color: blue;
+  color: #a20b00;
 }
 select {
   padding: 0.3rem;
@@ -42,11 +64,37 @@ input {
   border: 1px solid #000;
 }
 button {
-  background-color: #fff;
-  border: 1px solid #000;
-  padding: 0.3rem;
   cursor: pointer;
-  color: #000;
+  color: #fff;
+  background: url(data:image/png;base64,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), linear-gradient(to bottom, #D4AF37 0%, #C5A028 100%);
+  box-shadow: inset 0px 0px 1px 2px rgba(255, 255, 255, 0.3);
+  padding: 0.5rem 1rem;
+  font-weight: bold;
+  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: #0a0;
+}
+button.green:hover {
+  background: #0b0;
+}
+button:active {
+  position: relative;
+  top: 1px;
+}
+button:disabled, button:disabled:hover {
+  background: #aaa;
+  cursor: not-allowed;
+}
+button:focus {
+  outline: none;
 }
 .hidden {
   display: none !important;
@@ -58,11 +106,6 @@ p:last-child {
   margin-bottom: 0;
 }
 
-section {
-  border: 1px solid #000;
-  background-color: #fff;
-}
-
 #announcements, #signup-prompt {
   padding: 1rem;
   line-height: 1.2rem;
@@ -137,6 +180,7 @@ dialog .close-modal {
 }
 #avatar {
   width: 100%;
+  border: solid 1px #6d251c;
 }
 header {
   display: flex;
@@ -158,7 +202,7 @@ header {
 }
 #stat-bars, #defender-stat-bars {
   width: 100%;
-  margin: 5px 5px 0 5px;
+  margin: 0 5px;
 }
 #stat-bars .progress-bar, #defender-stat-bars .progress-bar {
   margin-bottom: 2px;
@@ -184,7 +228,7 @@ header {
 }
 
 .progress-bar {
-  border: solid 1px #000;
+  border: solid 1px #6d251c;
   width: 100%;
   font-size: 0.7rem;
   text-align: center;
@@ -215,16 +259,14 @@ nav a.active {
   text-decoration: underline;
 }
 nav.filter {
-  margin: 0.5rem 0;
+  margin: 0;
   text-align: right;
   border: 0;
   padding: 0;
   position: relative;
-  top: 1px;
+  bottom: 5px;
 }
 nav.filter a {
-  border: solid 1px #ddd;
-  background-color: #ddd;
   border-bottom-width: 0;
   z-index: 1;
   padding: 0.6rem;
@@ -232,13 +274,15 @@ nav.filter a {
 }
 nav.filter a.active {
   background-color: #fff;
-  border-color: #000;
+  border: solid #6d251c;
+  border-width: 1px 1px 0;
   z-index: 4;
 }
 .filter-container .listing {
-  border: solid 1px #000;
+  border: solid 1px #6d251c;
   z-index: 2;
   position: relative;
+  background-color: #fff;
 }
 nav.filter-result {
   display: none;
@@ -316,25 +360,59 @@ nav.filter-result.active {
 }
 #main-nav section {
   min-height: 344px;
-  border: 0;
+  padding: 1rem;
 }
 
 #stat-breakdown th {
   font-weight: bold;
   text-align: right;
-  background-color: #ddd;
+  background-color: #6d251c;
+  color: #fff;
+  background-image: url(data:image/png;base64,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);
 }
 #stat-breakdown th, #stat-breakdown td {
-  padding: 0.3rem 0.5rem;
+  padding: 0.5rem;
 }
 
 #explore {
   text-align: center;
   background-repeat: no-repeat;
-  background-position: bottom right;
   background-size: cover;
-  padding: 3rem 3rem 2rem;
+  padding: 2rem 0rem 2rem !important;
   line-height: 1.3rem;
+  border: solid 1px #6d251c;
+}
+
+.city-title-wrapper {
+  filter: drop-shadow(0 0 10px black);
+  position: relative;
+  z-index: 1;
+}
+.city-title:before {
+  position: absolute;
+  content: ' ';
+  z-index: 1;
+  top: 2px;
+  left: 2px;
+  right: 2px;
+  bottom: 2px;
+  background: transparent;
+  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;
+  letter-spacing: 1rem;
+  display: inline-block;
+  padding: 0.5rem 0.5rem 0.5rem 1.5rem;
+  color: #fff;
+  border: inset 3px rgba(88, 15, 15, 0.4);
+  text-shadow: 1px -1px 0px #522626;
+  background: #bc3915 url(data:image/png;base64,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);
+  position: relative;
+  clip-path: polygon(100% 0, 95% 50%, 100% 98%, 0% 100%, 5% 50%, 0 0);
+  box-shadow: 0 0 4px 4px black;
 }
 
 #fight-container {
@@ -361,10 +439,25 @@ nav.filter-result.active {
   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;
+  margin: 0 auto;
+  width: 80%;
+  background-image: url(data:image/png;base64,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);
+  background-color: #f7f4dd;
+  box-shadow: 0 0 10px black;
+  position: relative;
+  top: -13px;
+  border: solid 1px #6d251c;
+}
+.flex {
   display: flex;
-  justify-content: space-between;
+  justify-content: space-around;
   flex-wrap: wrap;
 }
+.city-details.flex > div {
+  margin: 1rem;
+}
 h1 {
   font-size: 1.5rem;
   font-weight: bold;
@@ -381,6 +474,9 @@ h3 {
   font-size: 1rem;
 }
 
+#travelling {
+  padding-top: 2rem;
+}
 #travelling-actions {
   display: flex;
   justify-content: center;
@@ -389,6 +485,28 @@ h3 {
 }
 
 
+#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 {
+  background-color: #846945;
+  height: 2px;
+  flex: 1;
+  content: ' ';
+  width: 4rem;
+  drop-shadow: 1px 1px 0px rgba(255, 255, 255, 0.7);
+}
 .shop-inventory-listing .listing {
   background-color: #fff;
 }
@@ -425,6 +543,7 @@ h3 {
 }
 .store-actions button {
   width: 75px;
+  padding: 0.3rem 0.5rem;
 }
 
 #inventory-page {
@@ -445,7 +564,7 @@ h3 {
   max-height: 64px;
   width: 64px;
   height: 64px;
-  border: solid 1px #000;
+  border: solid 1px #6d251c;
   padding: 0;
   text-align: center;
   vertical-align: bottom;
@@ -505,13 +624,16 @@ h3 {
 }
 
 
+#chat {
+  border: solid 1px #6d251c;
+}
 .chat-message {
   line-height: 1.2rem;
   margin-bottom: 0.3rem;
   padding: 0.3rem;
 }
 .chat-message:nth-child(even) {
-  background-color: #eee;
+  background: linear-gradient(270deg, rgba(0, 0, 0, 0) 0, rgba(196, 177, 149, 0.8) 100%);
 }
 
 .chat-message .from {
@@ -527,8 +649,8 @@ h3 {
   flex-grow: 8;
   padding: 0.3rem;
   outline: none;
-  border-left-width: 0px;
-  border-bottom-width: 0px;
+  border-width: 1px 0 0;
+  background: transparent;
 }
 #chat-form input:focus {
   outline: none;
@@ -538,6 +660,9 @@ h3 {
   border-bottom-width: 0px;
   font-weight: bold;
 }
+#chat-form button:active {
+  top: 0;
+}
 
 #game-footer {
   display: flex;
diff --git a/public/assets/font/BreatheFire.woff b/public/assets/font/BreatheFire.woff
new file mode 100644 (file)
index 0000000..cf58d3e
Binary files /dev/null and b/public/assets/font/BreatheFire.woff differ
diff --git a/public/assets/font/BreatheFire.woff2 b/public/assets/font/BreatheFire.woff2
new file mode 100644 (file)
index 0000000..c057969
Binary files /dev/null and b/public/assets/font/BreatheFire.woff2 differ
diff --git a/public/assets/font/demo.html b/public/assets/font/demo.html
new file mode 100644 (file)
index 0000000..118cce6
--- /dev/null
@@ -0,0 +1,192 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta name="robots" content="noindex, noarchive">
+    <meta name="format-detection" content="telephone=no">
+    <title>Transfonter demo</title>
+    <link href="stylesheet.css" rel="stylesheet">
+    <style>
+        /*
+        http://meyerweb.com/eric/tools/css/reset/
+        v2.0 | 20110126
+        License: none (public domain)
+        */
+        html, body, div, span, applet, object, iframe,
+        h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+        a, abbr, acronym, address, big, cite, code,
+        del, dfn, em, img, ins, kbd, q, s, samp,
+        small, strike, strong, sub, sup, tt, var,
+        b, u, i, center,
+        dl, dt, dd, ol, ul, li,
+        fieldset, form, label, legend,
+        table, caption, tbody, tfoot, thead, tr, th, td,
+        article, aside, canvas, details, embed,
+        figure, figcaption, footer, header, hgroup,
+        menu, nav, output, ruby, section, summary,
+        time, mark, audio, video {
+            margin: 0;
+            padding: 0;
+            border: 0;
+            font-size: 100%;
+            font: inherit;
+            vertical-align: baseline;
+        }
+        /* HTML5 display-role reset for older browsers */
+        article, aside, details, figcaption, figure,
+        footer, header, hgroup, menu, nav, section {
+            display: block;
+        }
+        body {
+            line-height: 1;
+        }
+        ol, ul {
+            list-style: none;
+        }
+        blockquote, q {
+            quotes: none;
+        }
+        blockquote:before, blockquote:after,
+        q:before, q:after {
+            content: '';
+            content: none;
+        }
+        table {
+            border-collapse: collapse;
+            border-spacing: 0;
+        }
+        /* demo styles */
+        body {
+            background: #f0f0f0;
+            color: #000;
+        }
+        .page {
+            background: #fff;
+            width: 920px;
+            margin: 0 auto;
+            padding: 20px 20px 0 20px;
+            overflow: hidden;
+        }
+        .font-container {
+            overflow-x: auto;
+            overflow-y: hidden;
+            margin-bottom: 40px;
+            line-height: 1.3;
+            white-space: nowrap;
+            padding-bottom: 5px;
+        }
+        h1 {
+            position: relative;
+            background: #444;
+            font-size: 32px;
+            color: #fff;
+            padding: 10px 20px;
+            margin: 0 -20px 12px -20px;
+        }
+        .letters {
+            font-size: 25px;
+            margin-bottom: 20px;
+        }
+        .s10:before {
+            content: '10px';
+        }
+        .s11:before {
+            content: '11px';
+        }
+        .s12:before {
+            content: '12px';
+        }
+        .s14:before {
+            content: '14px';
+        }
+        .s18:before {
+            content: '18px';
+        }
+        .s24:before {
+            content: '24px';
+        }
+        .s30:before {
+            content: '30px';
+        }
+        .s36:before {
+            content: '36px';
+        }
+        .s48:before {
+            content: '48px';
+        }
+        .s60:before {
+            content: '60px';
+        }
+        .s72:before {
+            content: '72px';
+        }
+        .s10:before, .s11:before, .s12:before, .s14:before,
+        .s18:before, .s24:before, .s30:before, .s36:before,
+        .s48:before, .s60:before, .s72:before {
+            font-family: Arial, sans-serif;
+            font-size: 10px;
+            font-weight: normal;
+            font-style: normal;
+            color: #999;
+            padding-right: 6px;
+        }
+        pre {
+            display: block;
+            padding: 9px;
+            margin: 0 0 12px;
+            font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
+            font-size: 13px;
+            line-height: 1.428571429;
+            color: #333;
+            font-weight: normal;
+            font-style: normal;
+            background-color: #f5f5f5;
+            border: 1px solid #ccc;
+            overflow-x: auto;
+            border-radius: 4px;
+        }
+        /* responsive */
+        @media (max-width: 959px) {
+            .page {
+                width: auto;
+                margin: 0;
+            }
+        }
+    </style>
+</head>
+<body>
+<div class="page">
+    <div class="demo">
+        <h1 style="font-family: 'Breathe Fire'; font-weight: normal; font-style: normal;">Breathe Fire</h1>
+        <pre title="Usage">.your-style {
+    font-family: 'Breathe Fire';
+    font-weight: normal;
+    font-style: normal;
+}</pre>
+        <pre title="Preload (optional)">
+&lt;link rel=&quot;preload&quot; href=&quot;BreatheFire.woff2&quot; as=&quot;font&quot; type=&quot;font/woff2&quot; crossorigin&gt;</pre>
+        <div class="font-container" style="font-family: 'Breathe Fire'; font-weight: normal; font-style: normal;">
+            <p class="letters">
+                abcdefghijklmnopqrstuvwxyz<br>
+ABCDEFGHIJKLMNOPQRSTUVWXYZ<br>
+                0123456789.:,;()*!?'@#&lt;&gt;$%&^+-=~
+            </p>
+            <p class="s10" style="font-size: 10px;">The quick brown fox jumps over the lazy dog.</p>
+            <p class="s11" style="font-size: 11px;">The quick brown fox jumps over the lazy dog.</p>
+            <p class="s12" style="font-size: 12px;">The quick brown fox jumps over the lazy dog.</p>
+            <p class="s14" style="font-size: 14px;">The quick brown fox jumps over the lazy dog.</p>
+            <p class="s18" style="font-size: 18px;">The quick brown fox jumps over the lazy dog.</p>
+            <p class="s24" style="font-size: 24px;">The quick brown fox jumps over the lazy dog.</p>
+            <p class="s30" style="font-size: 30px;">The quick brown fox jumps over the lazy dog.</p>
+            <p class="s36" style="font-size: 36px;">The quick brown fox jumps over the lazy dog.</p>
+            <p class="s48" style="font-size: 48px;">The quick brown fox jumps over the lazy dog.</p>
+            <p class="s60" style="font-size: 60px;">The quick brown fox jumps over the lazy dog.</p>
+            <p class="s72" style="font-size: 72px;">The quick brown fox jumps over the lazy dog.</p>
+        </div>
+    </div>
+
+</div>
+</body>
+</html>
diff --git a/public/assets/font/stylesheet.css b/public/assets/font/stylesheet.css
new file mode 100644 (file)
index 0000000..b4c1247
--- /dev/null
@@ -0,0 +1,9 @@
+@font-face {
+    font-family: 'Breathe Fire';
+    src: url('BreatheFire.woff2') format('woff2'),
+        url('BreatheFire.woff') format('woff');
+    font-weight: normal;
+    font-style: normal;
+    font-display: swap;
+}
+
index 5657a720855a436f4d4f0519d5a07c18a7ec9e29..e2efaa05e3ceb66c18fb48d9abe33e50dbd8e2a6 100644 (file)
@@ -4,8 +4,8 @@
     <title>Rising Legends</title>
     <meta charset="utf-8">
     <!--
-    <a href="https://www.flaticon.com/free-icons/dawn" title="dawn icons">Dawn icons created by Smashicons - Flaticon</a>
-    -->
+<a href="https://www.flaticon.com/free-icons/dawn" title="dawn icons">Dawn icons created by Smashicons - Flaticon</a>
+-->
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <link rel="stylesheet" href="/assets/css/reset.css">
     <link rel="stylesheet" href="/assets/css/game.css">
     <script src="/socket.io/socket.io.js"></script>
   </head>
   <body>
-    <section id="time-of-day"></section>
     <div id="view">
+      <section id="title-bar">
+        <a href="/" class="title-font">Rising Legends</a>
+        <div id="time-of-day"></div>
+      </section>
+
       <header>
         <div class="avatar-container">
           <img id="avatar" src="/assets/img/profile-pics/warrior-1.jpg">
@@ -76,7 +80,6 @@
         </div>
         <div id="server-stats">...Loading</div>
       </section>
-
       <footer>Another project by <a href="https://xangelo.ca/gardens/rising-legends/">xangelo.ca</a>. <span id="version"></span></footer>
     </div>
   </body>
index f93fc3fcf677a2b4969f7a95c144e80d6ecb917f..35ba7e34c03d362b19b6b23b067725b6f3e1045d 100644 (file)
@@ -12,7 +12,7 @@ import { loadPlayer, createPlayer, updatePlayer, movePlayer } from './player';
 import { random, sample } from 'lodash';
 import {broadcastMessage, Message} from '../shared/message';
 import {expToLevel, maxHp, Player} from '../shared/player';
-import {clearFight, createFight, getMonsterList, getRandomMonster, loadMonster, loadMonsterFromFight, loadMonsterWithFaction, saveFightState} from './monster';
+import {clearFight, createFight, getMonsterList, getMonsterLocation, getRandomMonster, loadMonster, loadMonsterFromFight, loadMonsterWithFaction, saveFightState} from './monster';
 import {FightRound} from '../shared/fight';
 import {addInventoryItem, deleteInventoryItem, getEquippedItems, getInventory, getInventoryItem, updateAp} from './inventory';
 import { getItemFromPlayer, getItemFromShop, getPlayersItems, getShopItems, givePlayerItem, updateItemCount } from './items';
@@ -35,8 +35,8 @@ import { renderMap } from './views/map';
 import { renderProfilePage } from './views/profile';
 import { renderSkills } from './views/skills';
 import { renderInventoryPage } from './views/inventory';
-import { renderMonsterSelector } from './views/monster-selector';
-import { renderFight, renderRoundDetails } from './views/fight';
+import { renderMonsterSelector, renderOnlyMonsterSelector } from './views/monster-selector';
+import { renderFight, renderFightPreRound, renderRoundDetails } from './views/fight';
 import { renderTravel, travelButton } from './views/travel';
 import { renderChatMessage } from './views/chat';
 
@@ -547,8 +547,9 @@ app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response)
       level: fight.level,
       fight_trigger: fight.fight_trigger
     };
+    const location = await getMonsterLocation(fight.ref_id);
 
-    res.send(renderPlayerBar(req.player, equippedItems) + renderFight(data));
+    res.send(renderPlayerBar(req.player, equippedItems) + renderFightPreRound(data, true, location));
   }
   else {
     const travelPlan = await getTravelPlan(req.player.id);
@@ -572,7 +573,8 @@ app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response)
         things,
         nextAction,
         closestTown: closest,
-        walkingText: ''
+        walkingText: '',
+        travelPlan
       }));
     }
     else {
@@ -691,7 +693,7 @@ app.get('/location/:location_id/items/:item_id/overview', authEndpoint, async (r
     </div>
   </div>
   <div class="actions">
-    <button hx-put="/location/${item.location_id}/items/${item.id}" formmethod="dialog" value="cancel">Buy</button>
+    <button hx-put="/location/${item.location_id}/items/${item.id}" formmethod="dialog" value="cancel" class="red">Buy</button>
     <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
   </div>
 </dialog>
@@ -764,7 +766,7 @@ app.get('/modal/items/:item_id', authEndpoint, async (req: AuthRequest, res: Res
     </div>
   </div>
   <div class="actions">
-    <button hx-put="/item/${item.item_id}" formmethod="dialog" value="cancel" hx-target="#inventory">Use</button>
+    <button hx-put="/item/${item.item_id}" formmethod="dialog" value="cancel" hx-target="#inventory" class="red">Use</button>
     <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
   </div>
 </dialog>
@@ -782,10 +784,10 @@ app.get('/city/stores/city:stores/:location_id', authEndpoint, async (req: AuthR
   }
   const [shopEquipment, shopItems] = await Promise.all([
     listShopItems({location_id: location.id}),
-    getShopItems(location.id)
+    getShopItems(location.id),
   ]);
 
-  const html = await renderStore(shopEquipment, shopItems, req.player);
+  const html = await renderStore(shopEquipment, shopItems, req.player, location);
 
   res.send(html);
 });
@@ -799,7 +801,7 @@ app.get('/city/explore/city:explore/:location_id', authEndpoint, async (req: Aut
   }
 
   const monsters: Monster[] = await getMonsterList(location.id);
-  res.send(renderMonsterSelector(monsters));
+  res.send(renderOnlyMonsterSelector(monsters, 0, location));
 });
 
 app.post('/travel', authEndpoint, async (req: AuthRequest, res: Response) => {
@@ -879,6 +881,7 @@ app.post('/fight', authEndpoint, async (req: AuthRequest, res: Response) => {
   }
 
   const fight = await createFight(req.player.id, monster, fightTrigger);
+  const location = await getService(monster.location_id);
 
 
   const data: MonsterForFight = {
@@ -890,7 +893,7 @@ app.post('/fight', authEndpoint, async (req: AuthRequest, res: Response) => {
     fight_trigger: fight.fight_trigger
   };
 
-  res.send(renderFight(data));
+  res.send(renderFightPreRound(data, true, location));
 });
 
 app.post('/travel/step', authEndpoint, async (req: AuthRequest, res: Response) => {
@@ -953,7 +956,8 @@ app.post('/travel/step', authEndpoint, async (req: AuthRequest, res: Response) =
       things,
       nextAction,
       closestTown: closest,
-      walkingText: sample(walkingText)
+      walkingText: sample(walkingText),
+      travelPlan
     }));
 
   }
@@ -973,13 +977,14 @@ app.post('/travel/:destination_id', authEndpoint, async (req: AuthRequest, res:
     return;
   }
 
-  await travel(req.player, destination.id);
+  const travelPlan = await travel(req.player, destination.id);
 
   res.send(renderTravel({
     things: [],
     nextAction: 0,
     walkingText: '',
-    closestTown: req.player.city_id
+    closestTown: req.player.city_id,
+    travelPlan
   }));
 });
 
index 46877b24e621f8ff003887225b8a63844814dc0b..3504d2aab194e81869050f202ca2a1f2f26389c2 100644 (file)
@@ -8,6 +8,7 @@ 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();
 
@@ -102,10 +103,8 @@ router.get('/city/services/city:services:healer/:location_id', authEndpoint, asy
 
   const text: string[] = [];
 
-  text.push(`<p><b>${service.name}</b></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>`);
   }
@@ -120,7 +119,17 @@ router.get('/city/services/city:services:healer/:location_id', authEndpoint, asy
 
   }
 
-  res.send(`<div>${text.join("\n")}</div>`);
+  res.send(`
+<div class="city-title-wrapper"><div class="city-title">${service.city_name}</div></div>
+<div class="city-details">
+<h3 class="location-name"><span>${service.name}</span></h3>
+<div class="service-in-town">
+${text.join("\n")}
+</div>
+</div>
+  `);
+
+  //res.send(`<div class="service-in-town">${text.join("\n")}</div>`);
 });
 
 
@@ -135,23 +144,31 @@ router.post('/city/services/city:services:healer:heal/:location_id', authEndpoin
   }
 
   const text: string[] = [];
-  text.push(`<p><b>${service.name}</b></p>`);
-
   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>`)
-    res.send(`<div>${text.join("\n")}</div>`);
   }
   else {
     req.player.hp = maxHp(req.player.constitution, req.player.level);
     req.player.gold -= cost;
 
     await updatePlayer(req.player);
-    const inventory = await getEquippedItems(req.player.id);
+    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>');
-    res.send(`<div>${text.join("\n")}</div>` + renderPlayerBar(req.player, inventory));
   }
+
+  res.send(`
+<div class="city-title-wrapper"><div class="city-title">${service.city_name}</div></div>
+<div class="city-details">
+<h3 class="location-name"><span>${service.name}</span></h3>
+<div class="service-in-town">
+${text.join("\n")}
+</div>
+</div>
+${inventory ? renderPlayerBar(req.player, inventory) : ''}
+`);
 });
index 54f0de8a6becc57f82f27f1f758f318e5ed6b857..df70a3882f63eb12f3d110b0964fd01004348be6 100644 (file)
@@ -1,6 +1,6 @@
-import { City, Location, Path } from "../shared/map";
+import { City, Location, LocationWithCity, Path } from "../shared/map";
 import type { Player } from '../shared/player';
-import type { Travel } from '../shared/travel';
+import type { Travel, TravelWithNames } from '../shared/travel';
 import { db } from './lib/db';
 import { random } from 'lodash';
 
@@ -12,10 +12,11 @@ export async function getAllServices(city_id: number): Promise<Location[]> {
             .orderBy('display_order');
 }
 
-export async function getService(location_id: number): Promise<Location> {
-  return db.select('*').first().from<Location>('locations').where({
-    id: location_id
-  });
+export async function getService(location_id: number): Promise<LocationWithCity> {
+  return db.select(['locations.*', 'cities.name as city_name']).
+          from<Location>('locations').join('cities', 'locations.city_id', '=', 'cities.id').where({
+    'locations.id': location_id
+  }).first();
 }
 
 export async function getAllPaths(city_id: number): Promise<Path[]> {
@@ -42,7 +43,7 @@ export async function getCityDetails(city_id: number): Promise<City> {
   return db.first().select('*').from<City>('cities').where({id: city_id});
 }
 
-export async function travel(player: Player, dest_id: number): Promise<Travel> {
+export async function travel(player: Player, dest_id: number): Promise<TravelWithNames> {
   const city = await getCityDetails(dest_id);
   const path = await db.first().select('*').from('paths').where({
     starting_city: player.city_id,
@@ -63,25 +64,15 @@ export async function travel(player: Player, dest_id: number): Promise<Travel> {
     source_id: player.city_id,
     destination_id: dest_id,
     total_distance: steps
-  }).returning('*');
-
-  if(rows.length !== 1) {
-    console.log(rows);
-    throw new Error('Unexpected response when creating travel');
-  }
-
-  return rows[0] as Travel;
+  });
+  
+  return getTravelPlan(player.id);
 }
 
-export async function stepForward(player_id: string): Promise<Travel> {
-  const rows = await db('travel').increment('current_position').returning('*');
+export async function stepForward(player_id: string): Promise<TravelWithNames> {
+  await db('travel').increment('current_position');
 
-  if(rows.length !== 1) {
-    console.log(rows);
-    throw new Error('Unexpected response when moving');
-  }
-
-  return rows[0] as Travel;
+  return getTravelPlan(player_id);
 }
 
 export async function clearTravelPlan(player_id: string): Promise<Travel> {
@@ -98,8 +89,15 @@ export async function completeTravel(player_id: string): Promise<Travel> {
   return rows[0] as Travel;
 }
 
-export async function getTravelPlan(player_id: string): Promise<Travel> {
-  return db.select('*').first().from<Travel>('travel').where({
-    player_id
-  });
+export async function getTravelPlan(player_id: string): Promise<TravelWithNames> {
+  return db.select([
+    'travel.*',
+    'source.name as source_city_name',
+    'destination.name as destination_city_name'
+  ]).from<Travel>('travel')
+    .join('cities as source', 'travel.source_id', '=', 'source.id')
+    .join('cities as destination', 'travel.destination_id', '=', 'destination.id')
+    .where({
+      'travel.player_id': player_id
+  }).first();
 }
index 7565146436f687578c619fdae62d0498bf99b9fa..e58c814bf6beacec0c898b0bf04c97606f73a6e9 100644 (file)
@@ -1,6 +1,7 @@
 import { db } from './lib/db';
 import { Fight, Monster, MonsterWithFaction, MonsterForList, FightTrigger } from '../shared/monsters';
 import { TimePeriod, TimeManager } from '../shared/time';
+import { LocationWithCity } from 'shared/map';
 
 const time = new TimeManager();
 
@@ -84,6 +85,16 @@ export async function createFight(playerId: string, monster: Monster, fightTrigg
   return res.pop();
 }
 
+export async function getMonsterLocation(monsterId: number): Promise<LocationWithCity> {
+return db.select(['locations.*', 'cities.name as city_name'])
+          .from<Monster>('monsters')
+          .join('locations', 'monsters.location_id', '=', 'locations.id')
+          .join('cities', 'cities.id', '=', 'locations.city_id')
+          .where({
+            'monsters.id': monsterId
+          }).first();
+}
+
 /**
  * Given a list of cities, it will return a monster that 
  * exists in any of the exploration zones with every monster 
index b6ed6dcf276ca62095cf94a22da6a9e3927645ad..1995d9a326ccea8a247610354577cf411ecb8d6b 100644 (file)
@@ -1,4 +1,5 @@
 import { FightRound } from "shared/fight";
+import { LocationWithCity } from "shared/map";
 import { MonsterForFight } from "../../shared/monsters";
 
 export function renderRoundDetails(roundData: FightRound): string {
@@ -8,13 +9,13 @@ export function renderRoundDetails(roundData: FightRound): string {
     case 'player':
       html.push(`<div>You defeated the ${roundData.monster.name}!</div>`);
       if(roundData.rewards.gold) {
-        html.push(`<div>You gained ${roundData.rewards.gold} gold`);
+        html.push(`<div>You gained ${roundData.rewards.gold} gold</div>`);
       }
       if(roundData.rewards.exp) {
-        html.push(`<div>You gained ${roundData.rewards.exp} exp`);
+        html.push(`<div>You gained ${roundData.rewards.exp} exp</div>`);
       }
       if(roundData.rewards.levelIncrease) {
-        html.push(`<div>You gained a level! ${roundData.player.level}`);
+        html.push(`<div>You gained a level! ${roundData.player.level}</div>`);
       }
     break;
     case 'monster':
@@ -34,7 +35,8 @@ export function renderRoundDetails(roundData: FightRound): string {
 export function renderFight(monster: MonsterForFight, results: string = '', displayFightActions: boolean = true) {
   const hpPercent = Math.floor((monster.hp / monster.maxHp) * 100);
 
-  let html = `<div id="fight-container">
+  let html = `
+  <div id="fight-container">
     <div id="defender-info">
       <div class="avatar-container">
         <img id="avatar" src="https://via.placeholder.com/64x64">
@@ -53,15 +55,57 @@ export function renderFight(monster: MonsterForFight, results: string = '', disp
           <option value="arms">Arms</option>
           <option value="legs">Legs</option>
         </select>
-        <button type="submit" class="fight-action" name="action" value="attack">Attack</button>
-        <button type="submit" class="fight-action" name="action" value="cast">Cast</button>
+        <button type="submit" class="fight-action red" name="action" value="attack">Attack</button>
+        <button type="submit" class="fight-action red" name="action" value="cast">Cast</button>
         <button type="submit" class="fight-action" name="action" value="flee">Flee</button>
       </form>
       `: ''}
       </div>
-  </form>
     <div id="fight-results">${results}</div>
-  </div>`;
+  </div>
+</div>`;
+
+  return html;
+}
+
+export function renderFightPreRound(monster: MonsterForFight,  displayFightActions: boolean = true, location: LocationWithCity) {
+  const hpPercent = Math.floor((monster.hp / monster.maxHp) * 100);
+
+  let html = `
+  <div class="city-title-wrapper">
+    <div class="city-title">${location.city_name}</div>
+  </div>
+  <div class="city-details">
+    <h3 class="location-name"><span>${location.name}</span></h3>
+
+  <div id="fight-container">
+    <div id="defender-info">
+      <div class="avatar-container">
+        <img id="avatar" src="https://via.placeholder.com/64x64">
+      </div>
+      <div id="defender-stat-bars">
+        <div id="defender-name">${monster.name}</div>
+        <div class="progress-bar" id="defender-hp-bar" style="background: linear-gradient(to right, red, red ${hpPercent}%, transparent ${hpPercent}%, transparent)" title="${hpPercent}% - ${monster.hp}/${monster.maxHp}">${hpPercent}% - ${monster.hp} / ${monster.maxHp}</div>
+      </div>
+    </div>
+    <div id="fight-actions">
+      ${displayFightActions ? `
+      <form hx-post="/fight/turn" hx-target="#fight-container">
+        <select id="fight-target" name="fightTarget">
+          <option value="head">Head</option>
+          <option value="body">Body</option>
+          <option value="arms">Arms</option>
+          <option value="legs">Legs</option>
+        </select>
+        <button type="submit" class="fight-action red" name="action" value="attack">Attack</button>
+        <button type="submit" class="fight-action red" name="action" value="cast">Cast</button>
+        <button type="submit" class="fight-action" name="action" value="flee">Flee</button>
+      </form>
+      `: ''}
+      </div>
+    <div id="fight-results"></div>
+  </div>
+</div>`;
 
   return html;
 }
index 3ad3e025eb8ae4ccd2a89ccf7525ba797548cc30..c027dd01d199196ac95251b483ae02b64124c8f0 100644 (file)
@@ -1,16 +1,16 @@
-import { EquipmentSlot, InventoryType } from "shared/inventory";
+import { EquipmentSlot } from "shared/inventory";
 import { EquippedItemDetails } from "../../shared/equipped";
 import { PlayerItem } from "../../shared/items";
 import { capitalize } from "lodash";
 
 function icon(icon_name?: string): string {
-  const icon = icon_name ? `/assets/img/icons/equipment/${icon_name}` : 'https://via.placeholder.com/64x64';
+  const placeholder = 'https://placehold.co/64x64/af936c/6d5f4d';
+  const icon = icon_name ? `/assets/img/icons/equipment/${icon_name}` : placeholder;
 
   return icon;
 }
 
 function renderEquipmentPlacementGrid(items: EquippedItemDetails[]) {
-  const placeholder = 'https://via.placeholder.com/64x64';
   // @ts-ignore
   const map: Record<EquipmentSlot, EquippedItemDetails> = items.filter(item => item.is_equipped).reduce((acc, item) => {
     acc[item.equipment_slot] = item;
@@ -94,7 +94,7 @@ function generateProgressBar(current: number, max: number, color: string, displa
 function renderInventoryItem(item: EquippedItemDetails , action: (item: EquippedItemDetails) => string): string {
   return `<div class="store-list">
     <div>
-      <img src="${item.icon ? `/assets/img/icons/equipment/${item.icon}` : 'https://via.placeholder.com/64x64'}">
+      <img src="${icon(item.icon)}">
     </div>
     <div class="details">
       <div class="name">${item.name}</div>
@@ -127,7 +127,7 @@ function renderInventorySection(inventory: EquippedItemDetails[]): string {
   return inventory.map(item => {
     return renderInventoryItem(item, item => {
       if(item.is_equipped) {
-        return `<button type="button" class="unequip-item error" hx-post="/player/unequip/${item.item_id}">Unequip</button>`;
+        return `<button type="button" class="unequip-item red" hx-post="/player/unequip/${item.item_id}">Unequip</button>`;
       }
       else {
         if(item.equipment_slot === 'ANY_HAND') {
index 6f53fea3cf0aeddd234bfefaa8c10b9075f7e41b..76a546b9914dbf38b0d26c86f2f2faa92a11f644 100644 (file)
@@ -17,9 +17,9 @@ export async function renderMap(data: { city: City, locations: Location[], paths
   });
 
   let html = `
-<section id="explore" class="tab active" style="background-image: linear-gradient(to left top, rgba(255,255,255,0) 0%,rgb(255,255,255) 100%), linear-gradient(to left, rgba(255, 255, 255, 0) 0%, rgb(255, 255, 255) 100%), url('/assets/img/map/${closestTown}.jpeg')" hx-swap-oob="true">
-<h1>${data.city.name}</h1>
-  <div class="city-details">`;
+<section id="explore" class="tab active" style="background-image: url('/assets/img/map/${closestTown}.jpeg')" hx-swap-oob="true">
+<div class="city-title-wrapper"><div class="city-title">${data.city.name}</div></div>
+  <div class="city-details flex">`;
 
   if(servicesParsed.SERVICES.length) {
     html += `<div><h3>Services</h3>${servicesParsed.SERVICES.join("<br>")}</div>`
index 8acb82f549cde7305111949298f2a22132da3c61..24140c6c9b75de1c7dd06568ee8cbc32746e7821 100644 (file)
@@ -1,13 +1,30 @@
+import { LocationWithCity } from "../../shared/map";
 import { Monster, MonsterForFight } from "../../shared/monsters";
 
-export function renderMonsterSelector(monsters: Monster[] | MonsterForFight[], activeMonsterId: number = 0): string {
-  let html = `<form id="fight-selector" hx-post="/fight" hx-target="#explore">
+export function renderOnlyMonsterSelector(monsters: Monster[] | MonsterForFight[], activeMonsterId: number = 0, location?: LocationWithCity): string {
+  let html = `
+<div class="city-title-wrapper">
+  <div class="city-title">${location?.city_name}</div>
+</div>
+<div class="city-details">
+  <h3 class="location-name"><span>${location?.name}</span></h3>
+  ${renderMonsterSelector(monsters, activeMonsterId, location)}
+</div>
+`;
+
+  return html;
+}
+
+export function renderMonsterSelector(monsters: Monster[] | MonsterForFight[], activeMonsterId: number = 0, location?: LocationWithCity): string {
+  let html = `
+<div class="service-in-town"><form id="fight-selector" hx-post="/fight" hx-target="#explore">
   <input type="hidden" name="fightTrigger" value="explore">
   <select id="monsterId" name="monsterId">
-  ${monsters.map((monster) => {
+  ${monsters.map((monster: (Monster | MonsterForFight)) => {
       return `<option value="${monster.id}" ${monster.id === activeMonsterId ? 'selected': ''}>${monster.name}</option>`;
   }).join("\n")}
-  </select> <button type="submit">Fight</button></form>`;
+  </select> <button type="submit" class="red">Fight</button></form></div>
+`;
 
   return html;
 }
index 9ca454b69ed62156059a59fc2c1d9af7901ec179..3b57789a6649e95bb983c5e0fdbbdea2f3f496be 100644 (file)
@@ -23,13 +23,13 @@ function displayLoginSignupForm(): string {
 
 }
 
-function generateProgressBar(current: number, max: number, color: string, displayPercent: boolean = true): string {
+function generateProgressBar(current: number, max: number, opts: ProgressBarOptions): string {
   let percent = 0;
   if(max > 0) {
     percent = Math.floor((current / max) * 100);
   }
-  const display = `${displayPercent? `${percent}% - `: ''}`;
-  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>`;
+  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 {
@@ -46,30 +46,35 @@ function calcAp(inventoryItem: EquippedItemDetails[]): string {
   return `
   <div>
     <img src="/assets/img/helm.png" class="icon">
-    ${generateProgressBar(ap.HEAD?.currentAp || 0, ap.HEAD?.maxAp || 0, '#7be67b')}
+    ${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, '#7be67b')}
+    ${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, '#7be67b')}
+    ${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, '#7be67b')}
+    ${generateProgressBar(ap.LEGS?.currentAp || 0, ap.LEGS?.maxAp || 0, { startingColor: '#5ebb5e', endingColor: '#7be67b'})}
   </div>
 `;
 }
 
-function progressBar(current: number, max: number, id: string, color: string) {
+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, ${color}, ${color} ${percent}%, transparent ${percent}%, transparent)"
+  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>`;
 }
 
@@ -81,8 +86,8 @@ export function renderPlayerBar(player: Player, inventory: EquippedItemDetails[]
         <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', '#ff7070')}
-      ${progressBar(player.exp, expToLevel(player.level + 1), 'exp-bar', '#5997f9')}
+      ${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'})}
     </div>
     ${player.account_type === 'session' ? displayLoginSignupForm() : ''}
   `;
index 6a24e89b1138c2602b7e12690dd852d8f5fe3b7c..08745f70b24d74e93c825762093055a2d7075f04 100644 (file)
@@ -2,6 +2,7 @@ import { ShopEquipment } from "../../shared/inventory";
 import { ShopItem, Item } from "../../shared/items";
 import { capitalize } from "lodash";
 import { Player } from "../../shared/player";
+import { LocationWithCity } from "shared/map";
 
 function renderStatBoost(name: string, val: number | string): string {
   let valSign: string = '';
@@ -78,7 +79,7 @@ function renderShopEquipment(item: ShopEquipment, action: (item: ShopEquipment)
 
 
 
-export async function renderStore(equipment: ShopEquipment[], items: (ShopItem & Item)[], player: Player): Promise<string> {
+export async function renderStore(equipment: ShopEquipment[], items: (ShopItem & Item)[], player: Player, location: LocationWithCity): Promise<string> {
   const listing: Record<string, string> = {};
   const listingTypes = new Set<string>();
 
@@ -117,11 +118,16 @@ export async function renderStore(equipment: ShopEquipment[], items: (ShopItem &
     finalListing.push(`<div class="filter-result ${activeTab === type ? 'active': 'hidden'}" data-filter="${type}" id="filter_${type}">${listing[type]}</div>`);
   });
 
-  let html = `<div class="shop-inventory-listing filter-container">
+  let html = `
+<div class="city-title-wrapper"><div class="city-title">${location.city_name}</div></div>
+<div class="city-details">
+<h3 class="location-name"><span>${location.name}</span></h3>
+<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>`;
+  </div>
+</div>`;
 
   return html;
 }
index fc70860509903a8d994de14ce235f6dbdebf121b..cf2d7dbcc12cd71bd2f582fbd5bf910e577a10e6 100644 (file)
@@ -15,8 +15,9 @@ export function renderTravel(data: TravelDTO): string {
   }
   */
 
-  let html = `<section id="explore" class="tab active" hx-swap-oob="true" style="background-image: linear-gradient(to left top, rgba(255,255,255,0) 0%,rgb(255,255,255) 100%), linear-gradient(to left, rgba(255, 255, 255, 0) 0%, rgb(255, 255, 255) 100%), url('/assets/img/map/${data.closestTown}.jpeg')">
-<div id="travelling">`;
+  let html = `<section id="explore" class="tab active" hx-swap-oob="true" style="background-image: url('/assets/img/map/${data.closestTown}.jpeg')">
+
+<div id="travelling" class="city-details">`;
   html += '<div id="travelling-actions">';
   html += travelButton(blockTime);
   if(data.things.length) {
@@ -25,7 +26,7 @@ export function renderTravel(data: TravelDTO): string {
     promptText = `You see a ${data.things[0].name}`;
     html += `<form hx-post="/fight" hx-target="#explore">
 <input type="hidden" name="monsterId" value="${data.things[0].id}">
-<button type="submit">Fight</button>
+<button type="submit" class="red">Fight</button>
 </form>`;
   }
 
index 5fe8b405548f5ef3417539dfec449782afd39a1a..c05ae8fe01adef5c8c9c8b2234faba223c25e9f1 100644 (file)
@@ -1,3 +1,5 @@
+import { TravelWithNames } from "./travel";
+
 export type City = {
   id: number;
   name: string;
@@ -14,6 +16,10 @@ export type Location = {
   event_name: string;
 }
 
+export type LocationWithCity  = Location & {
+  city_name: string;
+}
+
 export type Path = {
   starting_city: number;
   ending_city: number;
@@ -27,6 +33,7 @@ export type TravelDTO = {
   nextAction: number,
   walkingText: string,
   closestTown: number;
+  travelPlan: TravelWithNames
 }
 
 export const STEP_DELAY = 3000;
index 2c5911fad98a33fa2cbb57a24e2585a53f280119..babafa062e0a029e94b191415350ca7bcaa75ffe 100644 (file)
@@ -5,3 +5,8 @@ export type Travel = {
   total_distance: number;
   current_position: number;
 }
+
+export type TravelWithNames = Travel & {
+  destination_city_name: string;
+  source_city_name: string;
+}