Compare commits

...

2 Commits

19 changed files with 405 additions and 275 deletions

View File

@ -2,6 +2,10 @@
--font-size: 12px; --font-size: 12px;
} }
* {
box-sizing: border-box;
}
html { html {
font-size: var(--font-size); font-size: var(--font-size);
font-family: sans-serif; font-family: sans-serif;
@ -96,6 +100,7 @@ a {
color: #663300; color: #663300;
text-decoration: none; text-decoration: none;
font-weight: bold; font-weight: bold;
cursor: pointer;
} }
a:hover { a:hover {
@ -199,3 +204,26 @@ div.stat-bar > div {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
} }
#babblebox > .messages {
max-height: 200px;
overflow-y: auto;
}
#babblebox > .messages .message {
padding: 0.25rem;
background-color: #eee;
&:nth-child(even) {
background-color: white;
}
}
#babblebox > form {
margin-top: 0 !important;
& > input[type="text"] {
width: 100%;
margin-bottom: 0.5rem;
}
}

View File

@ -8,7 +8,7 @@ html {
} }
body { body {
background-image: url('/img/background.jpg'); background-image: url('/img/backgrounds/background.jpg');
padding: 2rem; padding: 2rem;
} }
table { table {

View File

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -4,11 +4,17 @@
require_once '../src/bootstrap.php'; require_once '../src/bootstrap.php';
// Do an early return with babblebox data if that's what's being requested
if ($uri[0] === 'babblebox' && (isset($uri[1]) && $uri[1] === 'messages')) {
echo babblebox_messages();
exit;
}
$r = new Router; $r = new Router;
$r->get('/', function() { $r->get('/', function() {
if (user()->currentaction === "In Town") { if (user()->currentaction === "In Town") {
$page = dotown(); $page = Towns\town();
$title = "In Town"; $title = "In Town";
} elseif (user()->currentaction === "Exploring") { } elseif (user()->currentaction === "Exploring") {
$page = doexplore(); $page = doexplore();
@ -17,7 +23,7 @@ $r->get('/', function() {
redirect('/fight'); redirect('/fight');
} }
display($page, $title); return is_htmx() ? $page : display($page, $title);
}); });
$r->get('/ninja', function() { $r->get('/ninja', function() {
@ -39,89 +45,21 @@ $r->get('/character', 'show_character_info');
$r->get('/character/:id', 'show_character_info'); $r->get('/character/:id', 'show_character_info');
$r->get('/showmap', 'showmap'); $r->get('/showmap', 'showmap');
$r->form('/babblebox', 'babblebox'); $r->form('/babblebox', 'babblebox');
$r->get('/babblebox/messages', 'babblebox_messages');
// [code, handler, params, middleware] // [code, handler, params, middleware]
$l = $r->lookup($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']); $l = $r->lookup($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
if (is_int($l)) exit("Error: $l"); if (is_int($l)) exit("Error: $l");
if (!empty($l['middleware'])) foreach ($l['middleware'] as $middleware) $middleware(); $content = $l['handler'](...$l['params'] ?? []);
$l['handler'](...$l['params'] ?? []); if (is_htmx() && $uri[0] !== 'babblebox') {
$content .= Render\debug_db_info();
function donothing() if ($GLOBALS['state']['user-state-changed'] ?? false) {
{ $content .= Render\right_nav();
if (user()->currentaction == "In Town") { }
$page = dotown();
$title = "In Town";
} elseif (user()->currentaction == "Exploring") {
$page = doexplore();
$title = "Exploring";
} elseif (user()->currentaction == "Fighting") {
redirect('/fight');
}
display($page, $title);
}
/**
* Spit out the main town page.
*/
function dotown()
{
global $controlrow;
$townrow = get_town_by_xy(user()->longitude, user()->latitude);
if ($townrow === false) display("There is an error with your user account, or with the town data. Please try again.","Error");
$townrow["news"] = "";
$townrow["whosonline"] = "";
$townrow["babblebox"] = "";
// News box. Grab latest news entry and display it. Something a little more graceful coming soon maybe.
if ($controlrow["shownews"] == 1) {
$newsrow = db()->query('SELECT * FROM news ORDER BY id DESC LIMIT 1;')->fetchArray(SQLITE3_ASSOC);
$townrow["news"] = '<div class="title">Latest News</div>';
$townrow["news"] .= "<span class=\"light\">[".pretty_date($newsrow["postdate"])."]</span><br>".nl2br($newsrow["content"]);
}
// Who's Online. Currently just members. Guests maybe later.
if ($controlrow["showonline"] == 1) {
$onlinequery = db()->query(<<<SQL
SELECT id, username
FROM users
WHERE onlinetime >= datetime('now', '-600 seconds')
ORDER BY username;
SQL);
$online_count = 0;
$online_rows = [];
while ($onlinerow = $onlinequery->fetchArray(SQLITE3_ASSOC)) {
$online_count++;
$online_rows[] = "<a href=\"javascript:opencharpopup({$onlinerow['id']})\">".$onlinerow["username"]."</a>";
}
$townrow["whosonline"] = '<div class="title">Who\'s Online</div>';
$townrow["whosonline"] .= "There are <b>$online_count</b> user(s) online within the last 10 minutes: ";
$townrow["whosonline"] .= rtrim(implode(', ', $online_rows), ', ');
}
if ($controlrow["showbabble"] == 1) {
$townrow["babblebox"] = <<<HTML
<div class="title">Babble Box</div>
<iframe src="/babblebox" name="sbox" width="100%" height="250" frameborder="0" id="bbox">
Your browser does not support inline frames! The Babble Box will not be available until you upgrade to
a newer <a href="http://www.mozilla.org" target="_new">browser</a>.
</iframe>
HTML;
}
$u = User::find(1);
$u->gold += 100;
$u->save();
var_dump($u->gold);
return render('towns', ['town' => $townrow]);
} }
echo $content;
exit;
/** /**
* Just spit out a blank exploring page. Exploring without a GET string is normally when they first log in, or when * Just spit out a blank exploring page. Exploring without a GET string is normally when they first log in, or when
@ -130,12 +68,8 @@ function dotown()
function doexplore() function doexplore()
{ {
return <<<HTML return <<<HTML
<table width="100%"> <div class="title"><img src="/img/title_exploring.gif" alt="Exploring"></div>
<tr><td class="title"><img src="/img/title_exploring.gif" alt="Exploring" /></td></tr> You are exploring the map, and nothing has happened. Continue exploring using the direction buttons or the Travel To menus.
<tr><td>
You are exploring the map, and nothing has happened. Continue exploring using the direction buttons or the Travel To menus.
</td></tr>
</table>
HTML; HTML;
} }
@ -167,7 +101,7 @@ function show_character_info(int $id = 0): void
'magic_list' => $magic_list, 'magic_list' => $magic_list,
'controlrow' => $controlrow 'controlrow' => $controlrow
]); ]);
echo render('minimal', ['content' => $showchar, 'title' => $userrow['username'].' Information']); echo render('layouts/minimal', ['content' => $showchar, 'title' => $userrow['username'].' Information']);
} }
function showmap() function showmap()
@ -178,29 +112,43 @@ function showmap()
round(258 - user()->latitude * (500 / 500) - 3) round(258 - user()->latitude * (500 / 500) - 3)
); );
echo render('minimal', [ echo render('layouts/minimal', [
'content' => '<img src="/img/map.gif" alt="Map">'.$pos, 'content' => '<img src="/img/map.gif" alt="Map">'.$pos,
'title' => 'Map' 'title' => 'Map'
]); ]);
} }
/** /**
* Either render the latest 40 chats to the babblebox, or add a chat to it and redirect. This is used * ...
* within an iframe.
*/ */
function babblebox() function babblebox()
{ {
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$safecontent = make_safe($_POST["babble"]); $content = trim($_POST["babble"]);
if (!empty($safecontent)) { if (!empty($content)) {
db()->query('INSERT INTO babble (posttime, author, babble) VALUES (CURRENT_TIMESTAMP, ?, ?);', db()->query('INSERT INTO babble (posttime, author, babble) VALUES (CURRENT_TIMESTAMP, ?, ?);',
[user()->username, $safecontent]); [user()->username, $content]);
} }
redirect('/babblebox'); return babblebox_messages();
} }
}
$query = db()->query('SELECT * FROM babble ORDER BY id DESC LIMIT 40;'); /**
echo render('babblebox', ['messages' => $query]); * Is the handler for the HTMX get request for messages.
*/
function babblebox_messages(): string
{
if (user() === false) return '';
$query = db()->query('SELECT * FROM babble ORDER BY id ASC LIMIT 40;');
$has_chats = false;
$messages = '';
while ($row = $query->fetchArray(SQLITE3_ASSOC)) {
$has_chats = true;
$messages .= '<div class="message">[<b>' . $row['author'] . '</b>] ' . make_safe($row['babble']) . '</div>';
}
if (!$has_chats) $messages = 'There are no messages. :(';
return $messages;
} }
/** /**

1
public/js/htmx.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -495,7 +495,7 @@ function levels()
function display_help(string $content) function display_help(string $content)
{ {
global $controlrow; global $controlrow;
echo render('help', [ echo render('layouts/help', [
'control' => $controlrow, 'control' => $controlrow,
'content' => $content, 'content' => $content,
'version' => VERSION, 'version' => VERSION,

View File

@ -20,40 +20,99 @@ function register_routes(Router $r): Router
return $r; return $r;
} }
/**
* Spit out the main town page.
*/
function town()
{
global $controlrow;
$townrow = get_town_by_xy(user()->longitude, user()->latitude);
if ($townrow === false) display("There is an error with your user account, or with the town data. Please try again.","Error");
$townrow["news"] = "";
$townrow["whosonline"] = "";
$townrow["babblebox"] = "";
// News box. Grab latest news entry and display it. Something a little more graceful coming soon maybe.
if ($controlrow["shownews"] == 1) {
$newsrow = db()->query('SELECT * FROM news ORDER BY id DESC LIMIT 1;')->fetchArray(SQLITE3_ASSOC);
$townrow["news"] = '<div class="title">Latest News</div>';
$townrow["news"] .= "<span class=\"light\">[".pretty_date($newsrow["postdate"])."]</span><br>".nl2br($newsrow["content"]);
}
// Who's Online. Currently just members. Guests maybe later.
if ($controlrow["showonline"] == 1) {
$onlinequery = db()->query(<<<SQL
SELECT id, username
FROM users
WHERE onlinetime >= datetime('now', '-600 seconds')
ORDER BY username;
SQL);
$online_count = 0;
$online_rows = [];
while ($onlinerow = $onlinequery->fetchArray(SQLITE3_ASSOC)) {
$online_count++;
$online_rows[] = "<a href=\"javascript:opencharpopup({$onlinerow['id']})\">".$onlinerow["username"]."</a>";
}
$townrow["whosonline"] = '<div class="title">Who\'s Online</div>';
$townrow["whosonline"] .= "There are <b>$online_count</b> user(s) online within the last 10 minutes: ";
$townrow["whosonline"] .= rtrim(implode(', ', $online_rows), ', ');
}
if ($controlrow["showbabble"] == 1) {
$townrow["babblebox"] = <<<HTML
<div class="title">Babble Box</div>
<iframe src="/babblebox" name="sbox" width="100%" height="250" frameborder="0" id="bbox">
Your browser does not support inline frames! The Babble Box will not be available until you upgrade to
a newer <a href="http://www.mozilla.org" target="_new">browser</a>.
</iframe>
HTML;
}
if (is_htmx()) htmx_update_page_title($townrow['name']);
return render('towns', ['town' => $townrow]);
}
/** /**
* Staying at the inn resets all expendable stats to their max values. * Staying at the inn resets all expendable stats to their max values.
*/ */
function inn() function inn()
{ {
$townrow = get_town_by_xy(user()->longitude, user()->latitude); $town = get_town_by_xy(user()->longitude, user()->latitude);
if ($townrow === false) { display("Cheat attempt detected.<br><br>Get a life, loser.", "Error"); } if ($town === false) { exit('Cheat attempt detected.<br><br>Get a life, loser.'); }
if (user()->gold < $townrow["innprice"]) { $htmx = is_htmx();
display("You do not have enough gold to stay at this Inn tonight.<br><br>You may return to <a href=\"/\">town</a>, or use the direction buttons on the left to start exploring.", "Inn"); if ($htmx) htmx_update_page_title($town['name'] . ' Inn');
}
if (isset($_POST["submit"])) { if (user()->gold < $town['innprice']) {
$newgold = user()->gold - $townrow["innprice"]; $page = <<<HTML
db()->query( You do not have enough gold to stay at this Inn tonight. <br><br>
'UPDATE users SET gold=?, currenthp=?, currentmp=?, currenttp=? WHERE id=?', You may return to <a hx-get="/" hx-target="#middle">town</a>, or use the direction buttons on the left to start exploring.
[$newgold, user()->maxhp, user()->maxmp, user()->maxtp, user()->id HTML;
]); } elseif (isset($_POST['submit'])) {
$title = "Inn"; user()->gold -= $town['innprice'];
$page = "You wake up feeling refreshed and ready for action.<br><br>You may return to <a href=\"/\">town</a>, or use the direction buttons on the left to start exploring."; user()->restore_points()->save();
} elseif (isset($_POST["cancel"])) { $page = <<<HTML
You wake up feeling refreshed and ready for action. <br><br>
You may return to <a hx-get="/" hx-target="#middle">town</a>, or use the direction buttons on the left to start exploring.
HTML;
} elseif (isset($_POST['cancel'])) {
redirect('/'); redirect('/');
} else { } else {
$title = "Inn";
$page = <<<HTML $page = <<<HTML
Resting at the inn will refill your current HP, MP, and TP to their maximum levels.<br><br> Resting at the inn will refill your current HP, MP, and TP to their maximum levels.<br><br>
A night's sleep at this Inn will cost you <b>{$townrow["innprice"]} gold</b>. Is that ok?<br><br> A night's sleep at this Inn will cost you <b>{$town['innprice']} gold</b>. Is that ok?<br><br>
<form action="/inn" method="post"> <form hx-post="/inn" hx-target="#middle">
<input type="submit" name="submit" value="Yes"> <input type="submit" name="cancel" value="No"> <input type="submit" name="submit" value="Yes"> <input type="submit" name="cancel" value="No">
</form> </form>
HTML; HTML;
} }
display($page, $title); return $htmx ? $page : display($page, $town['name'] . ' Inn');
} }
/** /**
@ -61,32 +120,51 @@ function inn()
*/ */
function buy() function buy()
{ {
$townrow = get_town_by_xy(user()->longitude, user()->latitude); $town = get_town_by_xy(user()->longitude, user()->latitude);
if ($townrow === false) display("Cheat attempt detected.<br><br>Get a life, loser.", "Error"); if ($town === false) { exit('Cheat attempt detected.<br><br>Get a life, loser.'); }
$items = db()->query("SELECT * FROM items WHERE id IN ({$townrow["itemslist"]});"); $htmx = is_htmx();
$page = "Buying weapons will increase your Attack Power. Buying armor and shields will increase your Defense Power.<br><br>Click an item name to purchase it.<br><br>The following items are available at this town:<br><br>\n"; if ($htmx) htmx_update_page_title($town['name'] . ' Shop');
$page .= "<table width=\"80%\">\n";
while ($itemsrow = $items->fetchArray(SQLITE3_ASSOC)) { $page = <<<HTML
$attrib = ($itemsrow["type"] == 1) ? "Attack Power:" : "Defense Power:"; Buying weapons will increase your Attack Power. Buying armor and shields will increase your Defense Power.<br><br>
$page .= "<tr><td width=\"4%\">"; Click an item name to purchase it.<br><br>
$page .= match ($itemsrow["type"]) { The following items are available at this town:<br><br>
1 => '<img src="/img/icon_weapon.gif" alt="weapon" /></td>', <table>
2 => '<img src="/img/icon_armor.gif" alt="armor" /></td>', HTML;
3 => '<img src="/img/icon_shield.gif" alt="shield" /></td>'
$items = db()->query('SELECT * FROM items WHERE id IN (' . $town["itemslist"] . ');');
while ($item = $items->fetchArray(SQLITE3_ASSOC)) {
$attrib = ($item["type"] == 1) ? "Attack Power:" : "Defense Power:";
$page .= '<tr><td width="4%">';
$page .= match ($item["type"]) {
1 => '<img src="/img/icon_weapon.gif" alt="weapon">',
2 => '<img src="/img/icon_armor.gif" alt="armor">',
3 => '<img src="/img/icon_shield.gif" alt="shield">'
}; };
if (user()->weaponid == $itemsrow["id"] || user()->armorid == $itemsrow["id"] || user()->shieldid == $itemsrow["id"]) { $page .= '</td>';
$page .= "<td width=\"32%\"><span class=\"light\">".$itemsrow["name"]."</span></td><td width=\"32%\"><span class=\"light\">$attrib ".$itemsrow["attribute"]."</span></td><td width=\"32%\"><span class=\"light\">Already purchased</span></td></tr>\n"; if (user()->weaponid === $item["id"] || user()->armorid === $item["id"] || user()->shieldid === $item["id"]) {
$page .= <<<HTML
<td width="32%"><span class="light">{$item["name"]}</span></td>
<td width="32%"><span class="light">$attrib {$item['attribute']}</span></td>
<td width="32%"><span class="light">Already purchased</span></td>
HTML;
} else { } else {
if ($itemsrow["special"] != "X") { $specialdot = "<span class=\"highlight\">&#42;</span>"; } else { $specialdot = ""; } $specialdot = $item['special'] !== 'X' ? '<span class="highlight">&#42;</span>' : '';
$page .= "<td width=\"32%\"><b><a href=\"/buy2/{$itemsrow["id"]}\">".$itemsrow["name"]."</a>$specialdot</b></td><td width=\"32%\">$attrib <b>".$itemsrow["attribute"]."</b></td><td width=\"32%\">Price: <b>".$itemsrow["buycost"]." gold</b></td></tr>\n"; $page .= <<<HTML
<td width="32%"><b><a href="/buy2/{$item['id']}">{$item['name']}</a>$specialdot</b></td>
<td width="32%">$attrib <b>{$item['attribute']}</b></td>
<td width="32%">Price: <b>{$item['buycost']} gold</b></td>
HTML;
} }
$page .= '</tr>';
} }
$page .= "</table><br>\n"; $page .= <<<HTML
$page .= "If you've changed your mind, you may also return back to <a href=\"/\">town</a>.\n"; </table><br>
$title = "Buy Items"; If you've changed your mind, you may also return back to <a hx-get="/" hx-target="#middle">town</a>.
HTML;
display($page, $title); return $htmx ? $page : display($page, $town['name'] . ' Shop');
} }
/** /**

View File

@ -4,6 +4,7 @@ require_once 'lib.php';
require_once 'router.php'; require_once 'router.php';
require_once 'auth.php'; require_once 'auth.php';
require_once 'mail.php'; require_once 'mail.php';
require_once 'render.php';
require_once 'actions/explore.php'; require_once 'actions/explore.php';
require_once 'actions/heal.php'; require_once 'actions/heal.php';
require_once 'actions/users.php'; require_once 'actions/users.php';
@ -26,7 +27,7 @@ if (!file_exists('../.installed') && $uri[0] !== 'install') {
redirect('/install'); redirect('/install');
} elseif (file_exists(('../.installed')) && $uri[0] === 'install') { } elseif (file_exists(('../.installed')) && $uri[0] === 'install') {
redirect('/'); redirect('/');
} elseif (file_exists(('../.installed')) && $uri[0] !== 'install') { } else {
$controlrow = get_control_row(); $controlrow = get_control_row();
if (!$controlrow["gameopen"]) { if (!$controlrow["gameopen"]) {

View File

@ -19,7 +19,13 @@ function db(): Database
*/ */
function redirect(string $location): void function redirect(string $location): void
{ {
header("Location: $location"); if (is_htmx()) {
header("HX-Redirect: $location");
header("HX-Replace-Url: $location");
} else {
header("Location: $location");
}
exit; exit;
} }
@ -82,7 +88,7 @@ function make_safe(string $content): string
*/ */
function display_admin($content, $title) function display_admin($content, $title)
{ {
echo render('admin', [ echo render('layouts/admin', [
"title" => $title, "title" => $title,
"content" => $content, "content" => $content,
"totaltime" => round(microtime(true) - START, 4), "totaltime" => round(microtime(true) - START, 4),
@ -97,28 +103,17 @@ function display_admin($content, $title)
/** /**
* Finalize page and output to browser. * Finalize page and output to browser.
*/ */
function display($content, $title, bool $topnav = true, bool $leftnav = true, bool $rightnav = true): void function display($content, $title, bool $topnav = true, bool $leftnav = true, bool $rightnav = true): string
{ {
global $controlrow; global $controlrow;
if ($topnav == true) { $game_skin = 0;
if (user() !== false) { // user should be logged in
$topnav = <<<HTML $topnav = $topnav ? Render\header_links() : '';
<a href='/logout'><img src='/img/button_logout.gif' alt='Log Out' title='Log Out'></a>
<a href='/help'><img src='/img/button_help.gif' alt='Help' title='Help'></a>
HTML;
} else {
$topnav = <<<HTML
<a href='/login'><img src='/img/button_login.gif' alt='Log In' title='Log In'></a>
<a href='/register'><img src='/img/button_register.gif' alt='Register' title='Register'></a>
<a href='/help'><img src='/img/button_help.gif' alt='Help' title='Help'></a>
HTML;
}
} else {
$topnav = '';
}
if (user() !== false) { if (user() !== false) {
$game_skin = user()->game_skin;
if (user()->currentaction == 'In Town') { if (user()->currentaction == 'In Town') {
$town = get_town_by_xy(user()->latitude, user()->longitude); $town = get_town_by_xy(user()->latitude, user()->longitude);
$current_town = "Welcome to <b>{$town['name']}</b>.<br><br>"; $current_town = "Welcome to <b>{$town['name']}</b>.<br><br>";
@ -129,26 +124,6 @@ function display($content, $title, bool $topnav = true, bool $leftnav = true, bo
// Format various userrow stuffs... // Format various userrow stuffs...
if (user()->latitude < 0) { user()->latitude = user()->latitude * -1 . "S"; } else { user()->latitude .= "N"; } if (user()->latitude < 0) { user()->latitude = user()->latitude * -1 . "S"; } else { user()->latitude .= "N"; }
if (user()->longitude < 0) { user()->longitude = user()->longitude * -1 . "W"; } else { user()->longitude .= "E"; } if (user()->longitude < 0) { user()->longitude = user()->longitude * -1 . "W"; } else { user()->longitude .= "E"; }
user()->experience = number_format(user()->experience);
user()->gold = number_format(user()->gold);
// Now make numbers stand out if they're low.
if (user()->currenthp <= (user()->maxhp/5)) { user()->currenthp = "<blink><span class=\"highlight\"><b>*".user()->currenthp."*</b></span></blink>"; }
if (user()->currentmp <= (user()->maxmp/5)) { user()->currentmp = "<blink><span class=\"highlight\"><b>*".user()->currentmp."*</b></span></blink>"; }
$user_spells = explode(',', user()->spells);
$spellquery = get_spells_from_list($user_spells);
user()->magiclist = '';
while ($spell = $spellquery->fetchArray(SQLITE3_ASSOC)) {
$spell = false;
foreach($user_spells as $id) {
if ($id === $spell['id'] && $spell['type'] == 1) $spell = true;
}
if ($spell == true) {
user()->magiclist .= "<a href=\"/spell/{$spell['id']}\">".$spell['name']."</a><br>";
}
}
if (user()->magiclist == "") { user()->magiclist = "None"; }
// Travel To list. // Travel To list.
$townslist = explode(",",user()->towns); $townslist = explode(",",user()->towns);
@ -165,22 +140,14 @@ function display($content, $title, bool $topnav = true, bool $leftnav = true, bo
} }
} }
echo render('primary', [ return render('layouts/primary', [
"dkgamename" => $controlrow["gamename"], "dkgamename" => $controlrow["gamename"],
"title" => $title, "title" => $title,
"content" => $content, "content" => $content,
"game_skin" => user()->game_skin ??= '0', "game_skin" => $game_skin,
'rightnav' => $rightnav ? render('rightnav', ['statbars' => create_stat_table(user())]) : '',
"leftnav" => $leftnav ? render('leftnav', ['town_list' => $town_list_html, 'current_town' => $current_town]) : '', "leftnav" => $leftnav ? render('leftnav', ['town_list' => $town_list_html, 'current_town' => $current_town]) : '',
"topnav" => $topnav, "topnav" => $topnav,
"totaltime" => round(microtime(true) - START, 4),
"numqueries" => db()->count,
"version" => VERSION,
"build" => BUILD,
"querylog" => env('debug', false) ? db()->log : []
]); ]);
exit;
} }
function checkcookies() function checkcookies()
@ -575,32 +542,36 @@ function env(string $key, mixed $default = null): mixed
/** /**
* Get the data on spells from a given list of IDs. * Get the data on spells from a given list of IDs.
*/ */
function get_spells_from_list(array|string $spell_ids): SQLite3Result|false function get_spells_from_list(array|string $spell_ids): array|false
{ {
if (is_string($spell_ids)) $spell_ids = explode(',', $spell_ids); if (is_string($spell_ids)) $spell_ids = explode(',', $spell_ids);
$placeholders = implode(',', array_fill(0, count($spell_ids), '?')); $placeholders = implode(',', array_fill(0, count($spell_ids), '?'));
$query = db()->query("SELECT id, name, type FROM spells WHERE id IN($placeholders)", $spell_ids); $query = db()->query("SELECT id, name, type FROM spells WHERE id IN($placeholders)", $spell_ids);
if ($query === false) return false; if ($query === false) return false;
return $query; $rows = [];
while ($row = $query->fetchArray(SQLITE3_ASSOC)) $rows[] = $row;
return !empty($rows) ? $rows : false;
} }
function generate_stat_bar($current, $max) function generate_stat_bar(int $current, int $max): string
{ {
$percent = ($max === 0) ? 0 : ceil($current / $max * 100); $percent = $max > 0 ? round(max(0, $current) / $max * 100, 4) : 0;
$color = $percent >= 66 ? 'green' : ($percent >= 33 ? 'yellow' : 'red'); $color = $percent >= 66 ? 'green' : ($percent >= 33 ? 'yellow' : 'red');
return '<div class="stat-bar" style="width: 15px; height: 100px; border: solid 1px black;">' . return <<<HTML
'<div style="height: ' . $percent . 'px; background-image: url(/img/bars_' . $color . '.gif);"></div>' . <div class="stat-bar" style="width: 15px; height: 100px; border: solid 1px black;">
'</div>'; <div style="height: $percent%; background-image: url(/img/bars_$color.gif);"></div>
</div>
HTML;
} }
function create_stat_table($userrow) function create_stat_table(): string
{ {
$stat_table = '<div class="stat-table">' . $stat_table = '<div class="stat-table">' .
'<div class="stat-row">' . '<div class="stat-row">' .
'<div class="stat-col">' . generate_stat_bar(user()->currenthp, user()->maxhp) . '<div>HP</div></div>' . '<div class="stat-col">' . generate_stat_bar((int)user()->currenthp, (int)user()->maxhp) . '<div>HP</div></div>' .
'<div class="stat-col">' . generate_stat_bar(user()->currentmp, user()->maxmp) . '<div>MP</div></div>' . '<div class="stat-col">' . generate_stat_bar((int)user()->currentmp, (int)user()->maxmp) . '<div>MP</div></div>' .
'<div class="stat-col">' . generate_stat_bar(user()->currenttp, user()->maxtp) . '<div>TP</div></div>' . '<div class="stat-col">' . generate_stat_bar((int)user()->currenttp, (int)user()->maxtp) . '<div>TP</div></div>' .
'</div>' . '</div>' .
'</div>'; '</div>';
@ -612,6 +583,23 @@ function create_stat_table($userrow)
*/ */
function user(): User|false function user(): User|false
{ {
$GLOBALS['state']['user'] ??= ($_SESSION['user_id'] ? User::find($_SESSION['user_id']) : false); $GLOBALS['state']['user'] ??= (isset($_SESSION['user_id']) ? User::find($_SESSION['user_id']) : false);
return $GLOBALS['state']['user']; return $GLOBALS['state']['user'];
} }
/**
* Determine whether a request is from HTMX. If HTMX is trying to restore history, we will say no in order to render
* full pages.
*/
function is_htmx(): bool
{
if (isset($_SERVER['HTTP_HX_HISTORY_RESTORE_REQUEST']) && $_SERVER['HTTP_HX_HISTORY_RESTORE_REQUEST'] === 'true') return false;
return isset($_SERVER['HTTP_HX_REQUEST']) && $_SERVER['HTTP_HX_REQUEST'] === 'true';
}
/**
* Update the page title using HTMX.
*/
function htmx_update_page_title(string $new_title) {
header('HX-Trigger: ' . json_encode(['updateTitle' => ['title' => $new_title]]));
}

View File

@ -19,7 +19,11 @@ class Model
public function __set(string $key, mixed $value): void public function __set(string $key, mixed $value): void
{ {
if (array_key_exists($key, $this->original_data)) $this->changes[$key] = $value; if (array_key_exists($key, $this->original_data)) {
$this->changes[$key] = $value;
} else {
throw new InvalidArgumentException("Attempted to write to $key, which doesn't exist in the data for this model.");
}
} }
public function save(): bool public function save(): bool

View File

@ -18,4 +18,48 @@ class User extends Model
if ($data === false) return false; if ($data === false) return false;
return new User($data); return new User($data);
} }
/**
* Return a list of spells from this user's spell list.
*/
public function spells(): array|false
{
return get_spells_from_list($this->spells);
}
/**
* Restore all HP, MP, and TP values to their max.
*/
public function restore_points(): User
{
$this->currenthp = $this->maxhp;
$this->currentmp = $this->maxmp;
$this->currenttp = $this->maxtp;
return $this;
}
/**
* Save works just as it does on the Model class. In our case, though, user state changing may necessitate
* OOB swaps for parts of the UI that have user data displayed. Left and right nav, for example. In these cases,
* we set a flag in GLOBALS state to signify this.
*/
public function save(): bool
{
if (empty($this->changes)) return true;
$placeholders = [];
$values = [];
foreach ($this->changes as $key => $value) {
$placeholders[] = "$key=?";
$values[] = $value;
}
$values[] = $this->id;
$query = 'UPDATE ' . $this->table_name . ' SET ' . implode(', ', $placeholders) . ' WHERE id = ?;';
$result = db()->query($query, $values);
if ($result === false) return false;
$GLOBALS['state']['user-state-changed'] = true;
return true;
}
} }

45
src/render.php Normal file
View File

@ -0,0 +1,45 @@
<?php
namespace Render;
/*
This file contains functions to render various UI elements. The goal is to begin shifting elements in the game
to HTMX/AJAX for more fluid gameplay.
*/
function header_links(): string
{
if (user() !== false) {
$links = "<a href='/logout'><img src='/img/button_logout.gif' alt='Log Out' title='Log Out'></a>";
} else {
$links = <<<HTML
<a href='/login'><img src='/img/button_login.gif' alt='Log In' title='Log In'></a>
<a href='/register'><img src='/img/button_register.gif' alt='Register' title='Register'></a>
HTML;
}
return $links .= "<a href='/help'><img src='/img/button_help.gif' alt='Help' title='Help'></a>";
}
function debug_db_info(): string {
$total_time = round(microtime(true) - START, 4);
return '<div id="debug-db-info" hx-swap-oob="true">'. $total_time . ' Seconds, ' . db()->count . ' Queries</div>';
}
function right_nav(): string
{
if (user() === false) return '';
// Flashy numbers if they're low
$hp = (user()->currenthp <= (user()->maxhp / 5)) ? "<blink><span class=\"highlight\"><b>*" . user()->currenthp . "*</b></span></blink>" : user()->currenthp;
$mp = (user()->currentmp <= (user()->maxmp / 5)) ? "<blink><span class=\"highlight\"><b>*" . user()->currentmp . "*</b></span></blink>" : user()->currentmp;
$template = render('right_nav', ['hp' => $hp, 'mp' => $mp]);
if (is_htmx()) $template = '<section id="right" hx-swap-oob="true">'.$template."</section>";
return $template;
}
function babblebox(): string
{
return render('babblebox', ['messages' => babblebox_messages()]);
}

View File

@ -1,58 +1,29 @@
<html lang="en"> <div id="babblebox">
<head> <div class="messages" hx-get="/babblebox/messages" hx-trigger="every 5s">
<title>Babblebox</title> <?= $messages ?>
<style type="text/css"> </div>
body {
background-image: url('/img/background.jpg');
color: black;
font: 11px verdana;
margin: 0px;
padding: 0px;
}
div { <form hx-post="/babblebox" hx-target="#babblebox > .messages" style="margin-top: 1rem;">
padding: 2px; <input type="text" name="babble" maxlength="255"><br>
border: solid 1px black;
margin: 2px;
text-align: left;
}
a {
color: #663300;
text-decoration: none;
font-weight: bold;
}
a:hover {
color: #330000;
}
div.message {
background-color: white;
}
div.message:nth-child(even) {
background-color: #eeeeee;
}
</style>
</head>
<body onload="window.scrollTo(0, 99999)">
<?php
$has_chats = false;
while ($row = $messages->fetchArray(SQLITE3_ASSOC)):
$has_chats = true;
?>
<div class="message">[<b><?= $row['author'] ?></b>] <?= $row['babble'] ?></div>
<?php
endwhile;
if (!$has_chats) echo 'There are no messages. :(';
?>
<form action="/babblebox" method="post" style="margin-top: 1rem;">
<input type="text" name="babble" maxlength="255" style="width: 100%;"><br>
<input type="submit" name="submit" value="Babble"> <input type="submit" name="submit" value="Babble">
<input type="reset" name="reset" value="Clear"> <input type="reset" name="reset" value="Clear">
</form> </form>
</body>
</html> <script>
const chatBox = document.querySelector('#babblebox > .messages')
let isUserAtBottom = true
if (chatBox !== null) {
chatBox.scrollTop = chatBox.scrollHeight;
const isAtBottom = () => chatBox.scrollHeight - chatBox.scrollTop === chatBox.clientHeight
const scrollChatToBottom = () => {
if (isUserAtBottom) chatBox.scrollTop = chatBox.scrollHeight;
}
const observer = new MutationObserver(scrollChatToBottom)
observer.observe(chatBox, { childList: true, subtree: true })
chatBox.addEventListener('scroll', () => isUserAtBottom = isAtBottom())
}
</script>
</div>

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $title ?></title> <title><?= $title ?></title>
<link rel="stylesheet" href="/css/dk.css"> <link rel="stylesheet" href="/css/dk.css">
<script src="/js/htmx.js"></script>
<script> <script>
function opencharpopup(id = 0) function opencharpopup(id = 0)
@ -29,20 +30,20 @@
<main> <main>
<section id="left"><?= $leftnav ?></section> <section id="left"><?= $leftnav ?></section>
<section id="middle"><?= $content ?></section> <section id="middle"><?= $content ?></section>
<section id="right"><?= $rightnav ?></section> <section id="right"><?= Render\right_nav() ?></section>
</main> </main>
<footer> <footer>
<div>Powered by <a href="/" target="_new">Dragon Knight</a></div> <div>Powered by <a href="/" target="_new">Dragon Knight</a></div>
<div>&copy; 2024 Sharkk</div> <div>&copy; 2024 Sharkk</div>
<div><?= $totaltime ?> Seconds, <?= $numqueries ?> Queries</div> <?= Render\debug_db_info(); ?>
<div>Version <?= $version ?> <?= $build ?></div> <div>Version <?= VERSION ?> <?= BUILD ?></div>
</footer> </footer>
<?php <?php
if (!empty($querylog)) { if (env('debug', false)) {
echo '<pre>'; echo '<pre>';
foreach ($querylog as $record) { foreach (db()->log as $record) {
$query_string = str_replace(["\r\n", "\n", "\r"], ' ', $record[0]); $query_string = str_replace(["\r\n", "\n", "\r"], ' ', $record[0]);
$error_string = !empty($record[2]) ? '// '.$record[2] : ''; $error_string = !empty($record[2]) ? '// '.$record[2] : '';
echo '<div>['.round($record[1], 2)."s] {$query_string}{$error_string}</div>"; echo '<div>['.round($record[1], 2)."s] {$query_string}{$error_string}</div>";
@ -51,5 +52,15 @@
} }
?> ?>
</div> </div>
<script>
document.addEventListener("updateTitle", (event) => {
const title = event.detail?.title
if (title) {
console.log('New title:', title);
document.title = title;
}
})
</script>
</body> </body>
</html> </html>

View File

@ -2,26 +2,36 @@
<div class="title"><img src="/img/button_character.gif" alt="Character" title="Character"></div> <div class="title"><img src="/img/button_character.gif" alt="Character" title="Character"></div>
<b><?= user()->username ?></b><br> <b><?= user()->username ?></b><br>
Level: <?= user()->level ?><br> Level: <?= user()->level ?><br>
Exp: <?= user()->experience ?><br> Exp: <?= number_format(user()->experience) ?><br>
Gold: <?= user()->gold ?><br> Gold: <?= number_format(user()->gold) ?><br>
HP: <?= user()->currenthp ?><br> HP: <?= $hp ?><br>
MP: <?= user()->currentmp ?><br> MP: <?= $mp ?><br>
TP: <?= user()->currenttp ?><br><br> TP: <?= user()->currenttp ?><br><br>
<?= $statbars ?><br> <?= create_stat_table() ?><br>
<a href="javascript:opencharpopup()">Extended Stats</a> <a href="javascript:opencharpopup()">Extended Stats</a>
</section> </section>
<section> <section>
<div class="title"><img src="/img/button_inventory.gif" alt="Inventory" title="Inventory"></div> <div class="title"><img src="/img/button_inventory.gif" alt="Inventory" title="Inventory"></div>
<img src="/img/icon_weapon.gif" alt="Weapon" title="Weapon"> Weapon: <?= $user['weaponname'] ?><br> <img src="/img/icon_weapon.gif" alt="Weapon" title="Weapon"> Weapon: <?= user()->weaponname ?><br>
<img src="/img/icon_armor.gif" alt="Armor" title="Armor"> Armor: <?= $user['armorname'] ?><br> <img src="/img/icon_armor.gif" alt="Armor" title="Armor"> Armor: <?= user()->armorname ?><br>
<img src="/img/icon_shield.gif" alt="Shield" title="Shield"> Shield: <?= $user['shieldname'] ?><br> <img src="/img/icon_shield.gif" alt="Shield" title="Shield"> Shield: <?= user()->shieldname ?><br>
Slot 1: <?= $user['slot1name'] ?><br> Slot 1: <?= user()->slot1name ?><br>
Slot 2: <?= $user['slot2name'] ?><br> Slot 2: <?= user()->slot2name ?><br>
Slot 3: <?= $user['slot3name'] ?> Slot 3: <?= user()->slot3name ?>
</section> </section>
<section> <section>
<div class="title"><img src="/img/button_fastspells.gif" alt="Fast Spells" title="Fast Spells"></div> <div class="title"><img src="/img/button_fastspells.gif" alt="Fast Spells" title="Fast Spells"></div>
<?= $user['magiclist'] ?> <?php
$user_spells = user()->spells();
if ($user_spells !== false) {
foreach ($user_spells as $spell) {
// list only healing spells for now
if ($spell['type'] === 1) echo "<a href=\"/spell/{$spell['id']}\">".$spell['name']."</a><br>";
}
} else {
echo 'None';
}
?>
</section> </section>

View File

@ -3,8 +3,8 @@
<div class="title"><img src="/img/town_<?= $town['id'] ?>.gif" alt="Welcome to <?= $town['name'] ?>" title="Welcome to <?= $town['name'] ?>"></div> <div class="title"><img src="/img/town_<?= $town['id'] ?>.gif" alt="Welcome to <?= $town['name'] ?>" title="Welcome to <?= $town['name'] ?>"></div>
<b>Town Options:</b><br> <b>Town Options:</b><br>
<ul> <ul>
<li><a href="/inn">Rest at the Inn</a></li> <li><a hx-get="/inn" hx-target="#middle">Rest at the Inn</a></li>
<li><a href="/buy">Buy Weapons/Armor</a></li> <li><a hx-get="/buy" hx-target="#middle">Buy Weapons/Armor</a></li>
<li><a href="/maps">Buy Maps</a></li> <li><a href="/maps">Buy Maps</a></li>
</ul> </ul>
</div> </div>
@ -18,6 +18,7 @@
</div> </div>
<div class="babblebox"> <div class="babblebox">
<?= $town['babblebox'] ?> <div class="title">Babblebox</div>
<?= Render\babblebox() ?>
</div> </div>
</div> </div>