+@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;
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;
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;
margin-bottom: 0;
}
-section {
- border: 1px solid #000;
- background-color: #fff;
-}
-
#announcements, #signup-prompt {
padding: 1rem;
line-height: 1.2rem;
}
#avatar {
width: 100%;
+ border: solid 1px #6d251c;
}
header {
display: flex;
}
#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;
}
.progress-bar {
- border: solid 1px #000;
+ border: solid 1px #6d251c;
width: 100%;
font-size: 0.7rem;
text-align: center;
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;
}
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;
}
#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 {
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;
font-size: 1rem;
}
+#travelling {
+ padding-top: 2rem;
+}
#travelling-actions {
display: flex;
justify-content: center;
}
+#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;
}
}
.store-actions button {
width: 75px;
+ padding: 0.3rem 0.5rem;
}
#inventory-page {
max-height: 64px;
width: 64px;
height: 64px;
- border: solid 1px #000;
+ border: solid 1px #6d251c;
padding: 0;
text-align: center;
vertical-align: bottom;
}
+#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 {
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;
border-bottom-width: 0px;
font-weight: bold;
}
+#chat-form button:active {
+ top: 0;
+}
#game-footer {
display: flex;
--- /dev/null
+<!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)">
+<link rel="preload" href="BreatheFire.woff2" as="font" type="font/woff2" crossorigin></pre>
+ <div class="font-container" style="font-family: 'Breathe Fire'; font-weight: normal; font-style: normal;">
+ <p class="letters">
+ abcdefghijklmnopqrstuvwxyz<br>
+ABCDEFGHIJKLMNOPQRSTUVWXYZ<br>
+ 0123456789.:,;()*!?'@#<>$%&^+-=~
+ </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>
--- /dev/null
+@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;
+}
+
<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">
</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>
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';
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';
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);
things,
nextAction,
closestTown: closest,
- walkingText: ''
+ walkingText: '',
+ travelPlan
}));
}
else {
</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>
</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>
}
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);
});
}
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) => {
}
const fight = await createFight(req.player.id, monster, fightTrigger);
+ const location = await getService(monster.location_id);
const data: MonsterForFight = {
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) => {
things,
nextAction,
closestTown: closest,
- walkingText: sample(walkingText)
+ walkingText: sample(walkingText),
+ travelPlan
}));
}
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
}));
});
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();
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>`);
}
}
- 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>`);
});
}
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) : ''}
+`);
});
-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';
.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[]> {
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,
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> {
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();
}
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();
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
import { FightRound } from "shared/fight";
+import { LocationWithCity } from "shared/map";
import { MonsterForFight } from "../../shared/monsters";
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':
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">
<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;
}
-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;
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>
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') {
});
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>`
+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;
}
}
-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 {
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>`;
}
<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() : ''}
`;
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 = '';
-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>();
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;
}
}
*/
- 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) {
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>`;
}
+import { TravelWithNames } from "./travel";
+
export type City = {
id: number;
name: string;
event_name: string;
}
+export type LocationWithCity = Location & {
+ city_name: string;
+}
+
export type Path = {
starting_city: number;
ending_city: number;
nextAction: number,
walkingText: string,
closestTown: number;
+ travelPlan: TravelWithNames
}
export const STEP_DELAY = 3000;
total_distance: number;
current_position: number;
}
+
+export type TravelWithNames = Travel & {
+ destination_city_name: string;
+ source_city_name: string;
+}