functions.php

This commit is contained in:
Valithor Obsidion 2025-08-14 15:47:25 -04:00
parent feba005335
commit 4d5c345528
27 changed files with 1232 additions and 1191 deletions

View File

@ -13,6 +13,10 @@
"team-reflex/discord-php": "^10.18.45"
},
"autoload": {
"files": [
"src/DragonKnight/functions.php",
"src/DragonKnight/DragonKnight.php"
],
"psr-4": {
"DragonKnight\\": "src/DragonKnight"
}

View File

@ -21,20 +21,20 @@ use DragonKnight\Admin\Admin;
// 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 Lib::babblebox_messages();
echo babblebox_messages();
exit;
}
$r = new Router;
$r->get('/', 'DragonKnight\Lib::index');
$r->get('/', 'DragonKnight\index');
$r->post('/move', 'DragonKnight\Actions\Explore::move');
$r->get('/spell/:id', 'DragonKnight\Actions\Heal::healspells');
$r->get('/character', 'DragonKnight\Lib::show_character_info');
$r->get('/character/:id', 'DragonKnight\Lib::show_character_info');
$r->get('/showmap', 'DragonKnight\Lib::show_map');
$r->form('/babblebox', 'DragonKnight\Lib::babblebox');
$r->get('/babblebox/messages', 'DragonKnight\Lib::babblebox_messages');
$r->get('/character', 'DragonKnight\show_character_info');
$r->get('/character/:id', 'DragonKnight\show_character_info');
$r->get('/showmap', 'DragonKnight\show_map');
$r->form('/babblebox', 'DragonKnight\babblebox');
$r->get('/babblebox/messages', 'DragonKnight\babblebox_messages');
Towns::register_routes($r);
Fight::register_routes($r);
@ -57,4 +57,4 @@ $l = $r->lookup($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
if (is_int($l)) {
exit("Error: $l");
}
echo Lib::render_response($uri, $l['handler'](...$l['params'] ?? []));
echo render_response($uri, $l['handler'](...$l['params'] ?? []));

View File

@ -13,15 +13,29 @@ declare(strict_types=1);
namespace DragonKnight\Admin;
use DragonKnight\Lib;
use DragonKnight\Router;
use SQLite3Result;
use function DragonKnight\db;
use function DragonKnight\env;
use function DragonKnight\get_drop;
use function DragonKnight\get_item;
use function DragonKnight\get_monster;
use function DragonKnight\get_spell;
use function DragonKnight\get_town_by_id;
use function DragonKnight\is_post;
use function DragonKnight\make_safe;
use function DragonKnight\page_title;
use function DragonKnight\render;
use function DragonKnight\ul_from_validate_errors;
use function DragonKnight\user;
use function DragonKnight\validate;
class Admin
{
public static function register_routes(Router $r): Router
{
if (Lib::user() !== false && Lib::user()->authlevel === 1) {
if (user() !== false && user()->authlevel === 1) {
$r->get('/admin', 'Admin\donothing');
$r->form('/admin/main', 'Admin\primary');
@ -58,7 +72,7 @@ class Admin
*/
public static function donothing(): string
{
Lib::page_title('Admin');
page_title('Admin');
return <<<HTML
Welcome to the administration section. Use the links on the left bar to control and edit various
@ -81,8 +95,8 @@ class Admin
*/
public static function primary(): string
{
if (Lib::is_post()) {
$form = Lib::validate($_POST, [
if (is_post()) {
$form = validate($_POST, [
'gamename' => ['alphanum-spaces'],
'gamesize' => ['int', 'min:5'],
'class1name' => ['alpha-spaces'],
@ -106,7 +120,7 @@ class Admin
$page = 'Main settings updated.';
} else {
$error_list = Lib::ul_from_validate_errors($form['errors']);
$error_list = ul_from_validate_errors($form['errors']);
$page = <<<HTML
<b>Errors:</b><br>
<div style="color: red;">{$error_list}</div><br>
@ -114,10 +128,10 @@ class Admin
HTML;
}
} else {
$page = Lib::render('admin/main_settings');
$page = render('admin/main_settings');
}
Lib::page_title('Admin: Main Settings');
page_title('Admin: Main Settings');
return $page;
}
@ -127,11 +141,11 @@ class Admin
*/
public static function items(): string
{
$items = Lib::db()->query('SELECT * FROM items ORDER BY id;');
$items = db()->query('SELECT * FROM items ORDER BY id;');
$page = "<h2>Edit Items</h2>Click an item's name or ID to edit it.<br><br>\n";
$page .= self::build_bulk_table($items, 'name', '/admin/items');
Lib::page_title('Admin: Items');
page_title('Admin: Items');
return $page;
}
@ -141,19 +155,19 @@ class Admin
*/
public static function edit_item(int $id): string
{
$item = Lib::get_item($id);
$item = get_item($id);
$page = Lib::is_post()
? self::handle_edit_form($id, 'items', Lib::validate($_POST, [
$page = is_post()
? self::handle_edit_form($id, 'items', validate($_POST, [
'name' => [],
'type' => ['int', 'in:1,2,3'],
'buycost' => ['int', 'min:0'],
'attribute' => ['int', 'min:0'],
'special' => ['default:X'],
]))
: Lib::render('admin/edit_item', ['item' => $item]);
: render('admin/edit_item', ['item' => $item]);
Lib::page_title('Admin: Editing '.$item['name']);
page_title('Admin: Editing '.$item['name']);
return $page;
}
@ -163,11 +177,11 @@ class Admin
*/
public static function drops()
{
$drops = Lib::db()->query('SELECT * FROM drops ORDER BY id;');
$drops = db()->query('SELECT * FROM drops ORDER BY id;');
$page = "<h2>Edit Drops</h2>Click an item's name to edit it.<br><br>\n";
$page .= self::build_bulk_table($drops, 'name', '/admin/drops');
Lib::page_title('Admin: Drops');
page_title('Admin: Drops');
return $page;
}
@ -177,20 +191,20 @@ class Admin
*/
public static function edit_drop(int $id): string
{
$drop = Lib::get_drop($id);
$drop = get_drop($id);
if (Lib::is_post()) {
$page = self::handle_edit_form($id, 'drops', Lib::validate($_POST, [
if (is_post()) {
$page = self::handle_edit_form($id, 'drops', validate($_POST, [
'name' => [],
'mlevel' => ['int', 'min:1'],
'attribute1' => [],
'attribute2' => ['default:X'],
]));
} else {
$page = Lib::render('admin/edit_drop', ['drop' => $drop]);
$page = render('admin/edit_drop', ['drop' => $drop]);
}
Lib::page_title('Admin: Editing '.$drop['name']);
page_title('Admin: Editing '.$drop['name']);
return $page;
}
@ -200,11 +214,11 @@ class Admin
*/
public static function towns(): string
{
$towns = Lib::db()->query('SELECT * FROM towns ORDER BY id;');
$towns = db()->query('SELECT * FROM towns ORDER BY id;');
$page = "<h2>Edit Towns</h2>Click an town's name or ID to edit it.<br><br>\n";
$page .= self::build_bulk_table($towns, 'name', '/admin/towns');
Lib::page_title('Admin: Towns');
page_title('Admin: Towns');
return $page;
}
@ -214,23 +228,23 @@ class Admin
*/
public static function edit_town(int $id): string
{
$town = Lib::get_town_by_id($id);
$town = get_town_by_id($id);
if (Lib::is_post()) {
$page = self::handle_edit_form($id, 'towns', Lib::validate($_POST, [
if (is_post()) {
$page = self::handle_edit_form($id, 'towns', validate($_POST, [
'name' => [],
'latitude' => ['int', 'min:0', 'max:'.Lib::env('game_size')],
'longitude' => ['int', 'min:0', 'max:'.Lib::env('game_size')],
'latitude' => ['int', 'min:0', 'max:'.env('game_size')],
'longitude' => ['int', 'min:0', 'max:'.env('game_size')],
'innprice' => ['int', 'min:0'],
'mapprice' => ['int', 'min:0'],
'travelpoints' => ['int', 'min:0'],
'itemslist' => ['optional'],
]));
} else {
$page = Lib::render('admin/edit_town', ['town' => $town]);
$page = render('admin/edit_town', ['town' => $town]);
}
Lib::page_title('Admin: Editing '.$town['name']);
page_title('Admin: Editing '.$town['name']);
return $page;
}
@ -240,19 +254,19 @@ class Admin
*/
public static function monsters()
{
$max_level = Lib::db()->query('SELECT level FROM monsters ORDER BY level DESC LIMIT 1;')->fetchArray(SQLITE3_ASSOC)['level'];
$monsters = Lib::db()->query('SELECT * FROM monsters ORDER BY id;');
$max_level = db()->query('SELECT level FROM monsters ORDER BY level DESC LIMIT 1;')->fetchArray(SQLITE3_ASSOC)['level'];
$monsters = db()->query('SELECT * FROM monsters ORDER BY id;');
$page = '<h2>Edit Monsters</h2>';
$page .= ((Lib::env('game_size') / 5) !== $max_level)
? '<span class="highlight">Note:</span> Your highest monster level does not match with your entered map size. Highest monster level should be '.(Lib::env('game_size') / 5).", yours is $max_level. Please fix this before opening the game to the public.<br>"
$page .= ((env('game_size') / 5) !== $max_level)
? '<span class="highlight">Note:</span> Your highest monster level does not match with your entered map size. Highest monster level should be '.(env('game_size') / 5).", yours is $max_level. Please fix this before opening the game to the public.<br>"
: 'Monster level and map size match. No further actions are required for map compatibility.<br>';
$page .= "Click an monster's name or ID to edit it.<br><br>\n";
$page .= self::build_bulk_table($monsters, 'name', '/admin/monsters');
Lib::page_title('Admin: Monsters');
page_title('Admin: Monsters');
return $page;
}
@ -262,10 +276,10 @@ class Admin
*/
public static function edit_monster(int $id): string
{
$monster = Lib::get_monster($id);
$monster = get_monster($id);
$page = (Lib::is_post())
? self::handle_edit_form($id, 'monsters', Lib::validate($_POST, [
$page = (is_post())
? self::handle_edit_form($id, 'monsters', validate($_POST, [
'name' => [],
'maxhp' => ['int', 'min:1'],
'maxdam' => ['int', 'min:0'],
@ -275,9 +289,9 @@ class Admin
'maxgold' => ['int', 'min:0'],
'immune' => ['in:0,1,2'],
]))
: Lib::render('admin/edit_monster', ['monster' => $monster]);
: render('admin/edit_monster', ['monster' => $monster]);
Lib::page_title('Admin: Editing '.$monster['name']);
page_title('Admin: Editing '.$monster['name']);
return $page;
}
@ -289,10 +303,10 @@ class Admin
{
$page = "<h2>Edit Spells</h2>Click an spell's name to edit it.<br><br>\n";
$spells = Lib::db()->query('SELECT * FROM spells ORDER BY id;');
$spells = db()->query('SELECT * FROM spells ORDER BY id;');
$page .= self::build_bulk_table($spells, 'name', '/admin/spells');
Lib::page_title('Admin: Spells');
page_title('Admin: Spells');
return $page;
}
@ -302,18 +316,18 @@ class Admin
*/
public static function edit_spell(int $id): string
{
$spell = Lib::get_spell($id);
$spell = get_spell($id);
$page = (Lib::is_post())
? self::handle_edit_form($id, 'spells', Lib::validate($_POST, [
$page = (is_post())
? self::handle_edit_form($id, 'spells', validate($_POST, [
'name' => [],
'mp' => ['int', 'min:0'],
'attribute' => ['int', 'min:0'],
'type' => ['in:1,2,3,4,5'],
]))
: Lib::render('admin/edit_spell', ['spell' => $spell]);
: render('admin/edit_spell', ['spell' => $spell]);
Lib::page_title('Admin: Editing '.$spell['name']);
page_title('Admin: Editing '.$spell['name']);
return $page;
}
@ -323,7 +337,7 @@ class Admin
*/
public static function levels(): string
{
$max_level = Lib::db()->query('SELECT id FROM levels ORDER BY id DESC LIMIT 1;')->fetchArray(SQLITE3_ASSOC)['id'];
$max_level = db()->query('SELECT id FROM levels ORDER BY id DESC LIMIT 1;')->fetchArray(SQLITE3_ASSOC)['id'];
$page = <<<HTML
<h2>Edit Levels</h2>
@ -340,7 +354,7 @@ class Admin
</form>
HTML;
Lib::page_title('Admin: Levels');
page_title('Admin: Levels');
return $page;
}
@ -354,13 +368,13 @@ class Admin
return 'No level to edit.';
}
$id = $_POST['level'];
$level = Lib::db()->query('SELECT * FROM levels WHERE id=? LIMIT 1;', [$id])->fetchArray(SQLITE3_ASSOC);
$level = db()->query('SELECT * FROM levels WHERE id=? LIMIT 1;', [$id])->fetchArray(SQLITE3_ASSOC);
if (Lib::is_post() && isset($_POST['save'])) {
if (is_post() && isset($_POST['save'])) {
unset($_POST['save']);
unset($_POST['level']);
$page = self::handle_edit_form($id, 'levels', Lib::validate($_POST, [
$page = self::handle_edit_form($id, 'levels', validate($_POST, [
'1_exp' => ['int', 'min:0'],
'1_hp' => ['int', 'min:0'],
'1_mp' => ['int', 'min:0'],
@ -384,21 +398,21 @@ class Admin
'3_spells' => ['int', 'min:0'],
]), 'Level <b>'.$id.'</b> updated.');
} else {
$page = Lib::render('admin/edit_level', ['level' => $level]);
$page = render('admin/edit_level', ['level' => $level]);
}
Lib::page_title('Admin: Editing Level '.$id);
page_title('Admin: Editing Level '.$id);
return $page;
}
public static function users()
{
$users = Lib::db()->query('SELECT * FROM users ORDER BY id;');
$users = db()->query('SELECT * FROM users ORDER BY id;');
$page = '<h2>Edit Users</h2>Click a username or ID to edit the account.<br><br><div class="table-wrapper">';
$page .= self::build_bulk_table($users, 'username', '/admin/users');
Lib::page_title('Admin: Users');
page_title('Admin: Users');
return $page.'</div>';
}
@ -408,17 +422,17 @@ class Admin
*/
public static function edit_user(int $id): string
{
$user = Lib::db()->query('SELECT * FROM users WHERE id = ? LIMIT 1;', [$id])->fetchArray(SQLITE3_ASSOC);
$user = db()->query('SELECT * FROM users WHERE id = ? LIMIT 1;', [$id])->fetchArray(SQLITE3_ASSOC);
if (Lib::is_post()) {
$form = Lib::validate($_POST, [
if (is_post()) {
$form = validate($_POST, [
'username' => ['length:3-18', 'alpha-spaces', 'unique:users,username'],
'verify' => [],
'authlevel' => ['int'],
'email' => ['email', 'unique:users,email'],
'charclass' => ['in:1,2,3'],
'latitude' => ['int', 'min:0', 'max:'.Lib::env('game_size')],
'longitude' => ['int', 'min:0', 'max:'.Lib::env('game_size')],
'latitude' => ['int', 'min:0', 'max:'.env('game_size')],
'longitude' => ['int', 'min:0', 'max:'.env('game_size')],
'currentaction' => [],
'currentfight' => ['int'],
'currentmonster' => ['int'],
@ -463,7 +477,7 @@ class Admin
self::save_data_row('users', $form['data'], $id);
$page = 'User <b>'.$user['username'].'</b> updated.';
} else {
$error_list = Lib::ul_from_validate_errors($form['errors']);
$error_list = ul_from_validate_errors($form['errors']);
$page = <<<HTML
<b>Errors:</b><br>
<div style="color: red;">{$error_list}</div><br>
@ -471,10 +485,10 @@ class Admin
HTML;
}
} else {
$page = Lib::render('admin/edit_user', ['user' => $user]);
$page = render('admin/edit_user', ['user' => $user]);
}
Lib::page_title('Admin: Editing '.$user['username']);
page_title('Admin: Editing '.$user['username']);
return $page;
}
@ -484,7 +498,7 @@ class Admin
*/
public static function add_news()
{
if (Lib::is_post()) {
if (is_post()) {
$c = trim($_POST['content'] ?? '');
$errors = [];
@ -493,7 +507,7 @@ class Admin
}
if (count($errors) === 0) {
Lib::db()->query('INSERT INTO news (author, content) VALUES (?, ?);', [Lib::user()->username, $c]);
db()->query('INSERT INTO news (author, content) VALUES (?, ?);', [user()->username, $c]);
$page = 'News post added.';
} else {
$error_list = implode('<br>', $errors);
@ -511,7 +525,7 @@ class Admin
HTML;
}
Lib::page_title('Admin: Add News');
page_title('Admin: Add News');
return $page;
}
@ -541,7 +555,7 @@ class Admin
foreach ($columns as $column) {
$html_parts[] = '<th>'.
Lib::make_safe($column === 'id' ? 'ID' : ucfirst($column)).
make_safe($column === 'id' ? 'ID' : ucfirst($column)).
'</th>';
}
$html_parts[] = '</tr></thead><tbody>';
@ -551,7 +565,7 @@ class Admin
foreach ($data as $row) {
$html_parts[] = '<tr>';
foreach ($columns as $column) {
$name = Lib::make_safe($row[$column]);
$name = make_safe($row[$column]);
$html_parts[] = isset($is_edit_column[$column])
? "<td><a href=\"{$edit_link}/{$row['id']}\" hx-get=\"{$edit_link}/{$row['id']}\" hx-target=\"#main\">{$name}</a></td>"
: "<td>{$name}</td>";
@ -578,7 +592,7 @@ class Admin
$values = array_values($data);
$values[] = $id;
return Lib::db()->query("UPDATE $table SET $fields WHERE id=?", $values);
return db()->query("UPDATE $table SET $fields WHERE id=?", $values);
}
/**
@ -590,7 +604,7 @@ class Admin
self::save_data_row($table, $form['data'], $id);
$page = $updated_message ?: '<b>'.$form['data']['name'].'</b> updated.';
} else {
$error_list = Lib::ul_from_validate_errors($form['errors']);
$error_list = ul_from_validate_errors($form['errors']);
$page = <<<HTML
<b>Errors:</b><br>
<div style="color: red;">{$error_list}</div><br>

View File

@ -13,7 +13,14 @@ declare(strict_types=1);
namespace DragonKnight\Actions;
use DragonKnight\Lib;
use function DragonKnight\env;
use function DragonKnight\get_town_by_xy;
use function DragonKnight\index;
use function DragonKnight\page_title;
use function DragonKnight\redirect;
use function DragonKnight\ul_from_validate_errors;
use function DragonKnight\user;
use function DragonKnight\validate;
class Explore
{
@ -23,7 +30,7 @@ class Explore
*/
public static function explore()
{
Lib::page_title('Exploring');
page_title('Exploring');
return <<<HTML
<div class="title"><img src="/img/title_exploring.gif" alt="Exploring"></div>
@ -34,20 +41,20 @@ class Explore
public static function move()
{
// Early exit if fighting
if (Lib::user()->currentaction == 'Fighting') {
Lib::redirect('/fight');
if (user()->currentaction == 'Fighting') {
redirect('/fight');
}
// Validate direction
$form = Lib::validate($_POST, ['direction' => ['in:north,west,east,south']]);
$form = validate($_POST, ['direction' => ['in:north,west,east,south']]);
if (! $form['valid']) {
return Lib::ul_from_validate_errors($form['errors']);
return ul_from_validate_errors($form['errors']);
}
// Current game state
$game_size = Lib::env('game_size');
$latitude = Lib::user()->latitude;
$longitude = Lib::user()->longitude;
$game_size = env('game_size');
$latitude = user()->latitude;
$longitude = user()->longitude;
$direction = $form['data']['direction'];
// Calculate new coordinates with boundary checks
@ -67,23 +74,23 @@ class Explore
}
// Check for town
$town = Lib::get_town_by_xy($longitude, $latitude);
$town = get_town_by_xy($longitude, $latitude);
if ($town !== false) {
return Towns::travelto($town['id'], false);
}
// Determine action (1 in 5 chance of fighting)
if (rand(1, 5) === 1) {
Lib::user()->currentaction = 'Fighting';
Lib::user()->currentfight = 1;
user()->currentaction = 'Fighting';
user()->currentfight = 1;
} else {
Lib::user()->currentaction = 'Exploring';
user()->currentaction = 'Exploring';
}
Lib::user()->latitude = $latitude;
Lib::user()->longitude = $longitude;
Lib::user()->save();
user()->latitude = $latitude;
user()->longitude = $longitude;
user()->save();
return Lib::index();
return index();
}
}

View File

@ -13,7 +13,6 @@ declare(strict_types=1);
namespace DragonKnight\Actions;
use DragonKnight\Lib;
use DragonKnight\Router;
class Fight
@ -33,7 +32,7 @@ class Fight
*/
public static function fight()
{
if (Lib::user()->currentaction !== 'Fighting') {
if (user()->currentaction !== 'Fighting') {
exit('Cheat attempt detected.<br><br>Get a life, loser.');
}
@ -41,7 +40,7 @@ class Fight
$playerisdead = 0;
// Generate spell list
$user_spells = Lib::user()->spells();
$user_spells = user()->spells();
if (! empty($user_spells)) {
$page['magiclist'] = '<select name="userspell">';
foreach ($user_spells as $spell) {
@ -51,52 +50,52 @@ class Fight
}
// Determine initial combat parameters
$chancetoswingfirst = rand(1, 10) + (int) ceil(sqrt(Lib::user()->dexterity));
if (Lib::user()->currentfight === 1) {
$maxlevel = (int) floor(max(abs(Lib::user()->latitude) + 5, abs(Lib::user()->longitude) + 5) / 5);
$chancetoswingfirst = rand(1, 10) + (int) ceil(sqrt(user()->dexterity));
if (user()->currentfight === 1) {
$maxlevel = (int) floor(max(abs(user()->latitude) + 5, abs(user()->longitude) + 5) / 5);
$minlevel = max(1, $maxlevel - 2);
$monster = Lib::db()->query('SELECT * FROM monsters WHERE level >= ? AND level <= ? ORDER BY RANDOM() LIMIT 1;', [
$monster = db()->query('SELECT * FROM monsters WHERE level >= ? AND level <= ? ORDER BY RANDOM() LIMIT 1;', [
$minlevel, $maxlevel,
])->fetchArray(SQLITE3_ASSOC);
Lib::user()->currentmonster = $monster['id'];
Lib::user()->currentmonsterhp = rand((int) (($monster['maxhp'] / 5) * 4), $monster['maxhp']);
Lib::user()->currentmonstersleep = 0;
Lib::user()->currentmonsterimmune = $monster['immune'];
user()->currentmonster = $monster['id'];
user()->currentmonsterhp = rand((int) (($monster['maxhp'] / 5) * 4), $monster['maxhp']);
user()->currentmonstersleep = 0;
user()->currentmonsterimmune = $monster['immune'];
$chancetoswingfirst = ($chancetoswingfirst > (rand(1, 7) + (int) ceil(sqrt($monster['maxdam'])))) ? 1 : 0;
}
// Get monster statistics
$monster = Lib::get_monster(Lib::user()->currentmonster);
$monster = get_monster(user()->currentmonster);
$page['monstername'] = $monster['name'];
// Run action
if (isset($_POST['run'])) {
$chancetorun = rand(4, 10) + (int) ceil(sqrt(Lib::user()->dexterity));
$chancetorun = rand(4, 10) + (int) ceil(sqrt(user()->dexterity));
if ($chancetorun <= (rand(1, 5) + (int) ceil(sqrt($monster['maxdam'])))) {
$page['yourturn'] = 'You tried to run away, but were blocked in front!<br><br>';
$page['monsterhp'] = "Monster's HP: ".Lib::user()->currentmonsterhp.'<br><br>';
$page['monsterhp'] = "Monster's HP: ".user()->currentmonsterhp.'<br><br>';
// Monster turn logic (similar to original public static function)
$page['monsterturn'] = self::handleMonsterTurn($userrow, $monster);
Lib::user()->currentaction = 'Exploring';
Lib::user()->save();
Lib::redirect('/');
user()->currentaction = 'Exploring';
user()->save();
redirect('/');
}
}
// Fight action
if (isset($_POST['fight'])) {
// Player's attack
$min = (int) (Lib::user()->attackpower * 0.75);
$max = (int) (Lib::user()->attackpower / 3);
$min = (int) (user()->attackpower * 0.75);
$max = (int) (user()->attackpower / 3);
$tohit = (int) ceil(mt_rand(min($min, $max), max($min, $max)));
$toexcellent = rand(1, 150);
if ($toexcellent <= sqrt(Lib::user()->strength)) {
if ($toexcellent <= sqrt(user()->strength)) {
$tohit *= 2;
$page['yourturn'] .= 'Excellent hit!<br>';
}
@ -113,19 +112,19 @@ class Fight
$page['yourturn'] .= 'The monster is dodging. No damage has been scored.<br>';
}
if (Lib::user()->currentuberdamage != 0) {
$monsterdamage += (int) ceil($monsterdamage * (Lib::user()->currentuberdamage / 100));
if (user()->currentuberdamage != 0) {
$monsterdamage += (int) ceil($monsterdamage * (user()->currentuberdamage / 100));
}
Lib::user()->currentmonsterhp -= $monsterdamage;
user()->currentmonsterhp -= $monsterdamage;
$page['yourturn'] .= "You attack the monster for $monsterdamage damage.<br><br>";
$page['monsterhp'] = "Monster's HP: ".Lib::user()->currentmonsterhp.'<br><br>';
$page['monsterhp'] = "Monster's HP: ".user()->currentmonsterhp.'<br><br>';
// Check for monster defeat
if (Lib::user()->currentmonsterhp <= 0) {
Lib::user()->currentmonsterhp = 0;
Lib::user()->save();
Lib::redirect('/victory');
if (user()->currentmonsterhp <= 0) {
user()->currentmonsterhp = 0;
user()->save();
redirect('/victory');
}
// Monster's turn
@ -139,26 +138,26 @@ class Fight
return 'You must select a spell first. Please go back and try again.';
}
$newspellrow = Lib::get_spell($pickedspell);
$spell = in_array($pickedspell, explode(',', Lib::user()->spells));
$newspellrow = get_spell($pickedspell);
$spell = in_array($pickedspell, explode(',', user()->spells));
if (! $spell) {
return 'You have not yet learned this spell. Please go back and try again.';
}
if (Lib::user()->currentmp < $newspellrow['mp']) {
if (user()->currentmp < $newspellrow['mp']) {
return 'You do not have enough Magic Points to cast this spell. Please go back and try again.';
}
// Spell type handling (similar to original public static function)
$page['yourturn'] = self::handleSpellCast($userrow, $newspellrow);
$page['monsterhp'] = "Monster's HP: ".Lib::user()->currentmonsterhp.'<br><br>';
$page['monsterhp'] = "Monster's HP: ".user()->currentmonsterhp.'<br><br>';
// Check for monster defeat
if (Lib::user()->currentmonsterhp <= 0) {
Lib::user()->currentmonsterhp = 0;
Lib::user()->save();
Lib::redirect('/victory');
if (user()->currentmonsterhp <= 0) {
user()->currentmonsterhp = 0;
user()->save();
redirect('/victory');
}
// Monster's turn
@ -168,7 +167,7 @@ class Fight
// Monster's turn if player lost first swing
if (! isset($_POST['run']) && ! isset($_POST['fight']) && ! isset($_POST['spell']) && $chancetoswingfirst == 0) {
$page['yourturn'] = 'The monster attacks before you are ready!<br><br>';
$page['monsterhp'] = "Monster's HP: ".Lib::user()->currentmonsterhp.'<br><br>';
$page['monsterhp'] = "Monster's HP: ".user()->currentmonsterhp.'<br><br>';
$page['monsterturn'] = self::handleMonsterTurn($userrow, $monster);
}
@ -183,7 +182,7 @@ class Fight
</form>
HTML;
Lib::user()->currentfight += 1;
user()->currentfight += 1;
} else {
$page['command'] = <<<HTML
<b>You have died.</b><br><br>
@ -192,24 +191,24 @@ class Fight
HTML;
}
Lib::user()->save();
user()->save();
// Finalize page and display it
$page = Lib::render('fight', ['page' => $page]);
$page = render('fight', ['page' => $page]);
return $page;
}
public static function victory()
{
if (Lib::user()->currentmonsterhp != 0) {
Lib::redirect('/fight');
if (user()->currentmonsterhp != 0) {
redirect('/fight');
}
if (Lib::user()->currentfight == 0) {
Lib::redirect('/');
if (user()->currentfight == 0) {
redirect('/');
}
$monsterrow = Lib::get_monster(Lib::user()->currentmonster);
$monsterrow = get_monster(user()->currentmonster);
$min = (int) (($monsterrow['maxexp'] / 6) * 5);
$max = (int) $monsterrow['maxexp'];
@ -218,8 +217,8 @@ class Fight
$exp = 1;
}
if (Lib::user()->expbonus != 0) {
$exp += ceil((Lib::user()->expbonus / 100) * $exp);
if (user()->expbonus != 0) {
$exp += ceil((user()->expbonus / 100) * $exp);
}
$min = (int) (($monsterrow['maxgold'] / 6) * 5);
@ -230,56 +229,56 @@ class Fight
$gold = 1;
}
if (Lib::user()->goldbonus != 0) {
$gold += ceil((Lib::user()->goldbonus / 100) * $exp);
if (user()->goldbonus != 0) {
$gold += ceil((user()->goldbonus / 100) * $exp);
}
if (Lib::user()->experience + $exp < 16777215) {
$newexp = Lib::user()->experience += $exp;
if (user()->experience + $exp < 16777215) {
$newexp = user()->experience += $exp;
$warnexp = '';
} else {
$newexp = Lib::user()->experience;
$newexp = user()->experience;
$exp = 0;
$warnexp = 'You have maxed out your experience points.';
}
if (Lib::user()->gold + $gold < 16777215) {
$newgold = Lib::user()->gold += $gold;
if (user()->gold + $gold < 16777215) {
$newgold = user()->gold += $gold;
$warngold = '';
} else {
$newgold = Lib::user()->gold;
$newgold = user()->gold;
$gold = 0;
$warngold = 'You have maxed out your gold.';
}
$levelrow = Lib::db()->query('SELECT * FROM levels WHERE id=? LIMIT 1;', [Lib::user()->level + 1])->fetchArray(SQLITE3_ASSOC);
$levelrow = db()->query('SELECT * FROM levels WHERE id=? LIMIT 1;', [user()->level + 1])->fetchArray(SQLITE3_ASSOC);
if (Lib::user()->level < 100) {
if ($newexp >= $levelrow[Lib::user()->charclass.'_exp']) {
Lib::user()->maxhp += $levelrow[Lib::user()->charclass.'_hp'];
Lib::user()->maxmp += $levelrow[Lib::user()->charclass.'_mp'];
Lib::user()->maxtp += $levelrow[Lib::user()->charclass.'_tp'];
Lib::user()->strength += $levelrow[Lib::user()->charclass.'_strength'];
Lib::user()->dexterity += $levelrow[Lib::user()->charclass.'_dexterity'];
Lib::user()->attackpower += $levelrow[Lib::user()->charclass.'_strength'];
Lib::user()->defensepower += $levelrow[Lib::user()->charclass.'_dexterity'];
Lib::user()->level += 1;
if (user()->level < 100) {
if ($newexp >= $levelrow[user()->charclass.'_exp']) {
user()->maxhp += $levelrow[user()->charclass.'_hp'];
user()->maxmp += $levelrow[user()->charclass.'_mp'];
user()->maxtp += $levelrow[user()->charclass.'_tp'];
user()->strength += $levelrow[user()->charclass.'_strength'];
user()->dexterity += $levelrow[user()->charclass.'_dexterity'];
user()->attackpower += $levelrow[user()->charclass.'_strength'];
user()->defensepower += $levelrow[user()->charclass.'_dexterity'];
user()->level += 1;
$newlevel = $levelrow['id'];
if ($levelrow[Lib::user()->charclass.'_spells'] != 0) {
Lib::user()->spells .= ','.$levelrow[Lib::user()->charclass.'_spells'];
if ($levelrow[user()->charclass.'_spells'] != 0) {
user()->spells .= ','.$levelrow[user()->charclass.'_spells'];
$spelltext = 'You have learned a new spell.<br>';
} else {
$spelltext = '';
$newspell = '';
}
$page = 'Congratulations. You have defeated the '.$monsterrow['name'].".<br>You gain $exp experience. $warnexp <br>You gain $gold gold. $warngold <br><br><b>You have gained a level!</b><br><br>You gain ".$levelrow[Lib::user()->charclass.'_hp'].' hit points.<br>You gain '.$levelrow[Lib::user()->charclass.'_mp'].' magic points.<br>You gain '.$levelrow[Lib::user()->charclass.'_tp'].' travel points.<br>You gain '.$levelrow[Lib::user()->charclass.'_strength'].' strength.<br>You gain '.$levelrow[Lib::user()->charclass.'_dexterity']." dexterity.<br>$spelltext<br>You can now continue <a href=\"/\" hx-get=\"/\" hx-target=\"#middle\">exploring</a>.";
$page = 'Congratulations. You have defeated the '.$monsterrow['name'].".<br>You gain $exp experience. $warnexp <br>You gain $gold gold. $warngold <br><br><b>You have gained a level!</b><br><br>You gain ".$levelrow[user()->charclass.'_hp'].' hit points.<br>You gain '.$levelrow[user()->charclass.'_mp'].' magic points.<br>You gain '.$levelrow[user()->charclass.'_tp'].' travel points.<br>You gain '.$levelrow[user()->charclass.'_strength'].' strength.<br>You gain '.$levelrow[user()->charclass.'_dexterity']." dexterity.<br>$spelltext<br>You can now continue <a href=\"/\" hx-get=\"/\" hx-target=\"#middle\">exploring</a>.";
$title = 'Courage and Wit have served thee well!';
$dropcode = '';
} else {
$page = 'Congratulations. You have defeated the '.$monsterrow['name'].".<br>You gain $exp experience. $warnexp <br>You gain $gold gold. $warngold <br><br>";
if (rand(1, 30) === 1) {
$droprow = Lib::db()->query('SELECT * FROM drops WHERE mlevel <= ? ORDER BY RANDOM() LIMIT 1;', [$monsterrow['level']])->fetchArray(SQLITE3_ASSOC);
$droprow = db()->query('SELECT * FROM drops WHERE mlevel <= ? ORDER BY RANDOM() LIMIT 1;', [$monsterrow['level']])->fetchArray(SQLITE3_ASSOC);
$dropcode = "dropcode='".$droprow['id']."',";
$page .= 'This monster has dropped an item. <a href="/drop" hx-get="/drop" hx-target="#middle">Click here</a> to reveal and equip the item, or you may also move on and continue <a href="/" hx-get="/" hx-target="#middle">exploring</a>.';
} else {
@ -291,26 +290,26 @@ class Fight
}
}
Lib::user()->currentaction = 'Exploring';
Lib::user()->currentfight = 0;
Lib::user()->currentuberdamage = 0;
Lib::user()->currentuberdefense = 0;
Lib::user()->currentmonstersleep = 0;
Lib::user()->currentmonsterimmune = 0;
Lib::user()->save();
user()->currentaction = 'Exploring';
user()->currentfight = 0;
user()->currentuberdamage = 0;
user()->currentuberdefense = 0;
user()->currentmonstersleep = 0;
user()->currentmonsterimmune = 0;
user()->save();
Lib::page_title($title);
page_title($title);
return $page;
}
public static function drop()
{
if (Lib::user()->dropcode == 0) {
Lib::redirect('/');
if (user()->dropcode == 0) {
redirect('/');
}
$droprow = Lib::get_drop(Lib::user()->dropcode);
$droprow = get_drop(user()->dropcode);
if (isset($_POST['submit'])) {
$slot = $_POST['slot'];
@ -320,8 +319,8 @@ class Fight
}
$slotstr = 'slot'.$slot.'id';
if (Lib::user()->$slotstr != 0) {
$slotrow = Lib::get_drop(Lib::user()->$slotstr);
if (user()->$slotstr != 0) {
$slotrow = get_drop(user()->$slotstr);
$old1 = explode(',', $slotrow['attribute1']);
if ($slotrow['attribute2'] != 'X') {
@ -336,52 +335,52 @@ class Fight
$new2 = [0 => 'maxhp',1 => 0];
}
Lib::user()->$old1[0] -= $old1[1];
Lib::user()->$old2[0] -= $old2[1];
user()->$old1[0] -= $old1[1];
user()->$old2[0] -= $old2[1];
if ($old1[0] == 'strength') {
Lib::user()->attackpower -= $old1[1];
user()->attackpower -= $old1[1];
}
if ($old1[0] == 'dexterity') {
Lib::user()->defensepower -= $old1[1];
user()->defensepower -= $old1[1];
}
if ($old2[0] == 'strength') {
Lib::user()->attackpower -= $old2[1];
user()->attackpower -= $old2[1];
}
if ($old2[0] == 'dexterity') {
Lib::user()->defensepower -= $old2[1];
user()->defensepower -= $old2[1];
}
Lib::user()->$new1[0] += $new1[1];
Lib::user()->$new2[0] += $new2[1];
user()->$new1[0] += $new1[1];
user()->$new2[0] += $new2[1];
if ($new1[0] == 'strength') {
Lib::user()->attackpower += $new1[1];
user()->attackpower += $new1[1];
}
if ($new1[0] == 'dexterity') {
Lib::user()->defensepower += $new1[1];
user()->defensepower += $new1[1];
}
if ($new2[0] == 'strength') {
Lib::user()->attackpower += $new2[1];
user()->attackpower += $new2[1];
}
if ($new2[0] == 'dexterity') {
Lib::user()->defensepower += $new2[1];
user()->defensepower += $new2[1];
}
if (Lib::user()->currenthp > Lib::user()->maxhp) {
Lib::user()->currenthp = Lib::user()->maxhp;
if (user()->currenthp > user()->maxhp) {
user()->currenthp = user()->maxhp;
}
if (Lib::user()->currentmp > Lib::user()->maxmp) {
Lib::user()->currentmp = Lib::user()->maxmp;
if (user()->currentmp > user()->maxmp) {
user()->currentmp = user()->maxmp;
}
if (Lib::user()->currenttp > Lib::user()->maxtp) {
Lib::user()->currenttp = Lib::user()->maxtp;
if (user()->currenttp > user()->maxtp) {
user()->currenttp = user()->maxtp;
}
$slot_s = 'slot'.$_POST['slot'];
$slot_name = "{$slot_s}name";
$slot_id = "{$slot_s}id";
Lib::user()->$slot_name = $droprow['name'];
Lib::user()->$slot_id = $droprow['id'];
user()->$slot_name = $droprow['name'];
user()->$slot_id = $droprow['id'];
} else {
$new1 = explode(',', $droprow['attribute1']);
if ($droprow['attribute2'] != 'X') {
@ -390,30 +389,30 @@ class Fight
$new2 = [0 => 'maxhp',1 => 0];
}
Lib::user()->$new1[0] += $new1[1];
Lib::user()->$new2[0] += $new2[1];
user()->$new1[0] += $new1[1];
user()->$new2[0] += $new2[1];
if ($new1[0] == 'strength') {
Lib::user()->attackpower += $new1[1];
user()->attackpower += $new1[1];
}
if ($new1[0] == 'dexterity') {
Lib::user()->defensepower += $new1[1];
user()->defensepower += $new1[1];
}
if ($new2[0] == 'strength') {
Lib::user()->attackpower += $new2[1];
user()->attackpower += $new2[1];
}
if ($new2[0] == 'dexterity') {
Lib::user()->defensepower += $new2[1];
user()->defensepower += $new2[1];
}
$slot_s = 'slot'.$_POST['slot'];
$slot_name = "{$slot_s}name";
$slot_id = "{$slot_s}id";
Lib::user()->$slot_name = $droprow['name'];
Lib::user()->$slot_id = $droprow['id'];
user()->$slot_name = $droprow['name'];
user()->$slot_id = $droprow['id'];
}
Lib::user()->save();
user()->save();
return 'The item has been equipped. You can now continue <a href="/" hx-get="/" hx-target="#middle">exploring</a>.';
}
@ -450,7 +449,7 @@ class Fight
}
$page .= '<br>Select an inventory slot from the list below to equip this item. If the inventory slot is already full, the old item will be discarded.';
$page .= '<form action="/drop" method="post"><select name="slot"><option value="0">Choose One</option><option value="1">Slot 1: '.Lib::user()->slot1name.'</option><option value="2">Slot 2: '.Lib::user()->slot2name.'</option><option value="3">Slot 3: '.Lib::user()->slot3name.'</option></select> <input type="submit" name="submit" value="Submit" /></form>';
$page .= '<form action="/drop" method="post"><select name="slot"><option value="0">Choose One</option><option value="1">Slot 1: '.user()->slot1name.'</option><option value="2">Slot 2: '.user()->slot2name.'</option><option value="3">Slot 3: '.user()->slot3name.'</option></select> <input type="submit" name="submit" value="Submit" /></form>';
$page .= 'You may also choose to just continue <a href="/" hx-get="/" hx-target="#middle">exploring</a> and give up this item.';
return $page;
@ -469,40 +468,40 @@ class Fight
public static function handleMonsterTurn(&$userrow, $monsterrow)
{
$pagearray = '';
if (Lib::user()->currentmonstersleep != 0) {
if (user()->currentmonstersleep != 0) {
$chancetowake = rand(1, 15);
if ($chancetowake > Lib::user()->currentmonstersleep) {
Lib::user()->currentmonstersleep = 0;
if ($chancetowake > user()->currentmonstersleep) {
user()->currentmonstersleep = 0;
$pagearray .= 'The monster has woken up.<br>';
} else {
$pagearray .= 'The monster is still asleep.<br>';
}
}
if (Lib::user()->currentmonstersleep == 0) {
if (user()->currentmonstersleep == 0) {
$tohit = (int) ceil(mt_rand((int) ($monsterrow['maxdam'] * 0.5), (int) $monsterrow['maxdam']));
$toblock = (int) ceil(mt_rand((int) (Lib::user()->defensepower * 0.75), (int) Lib::user()->defensepower) / 4);
$toblock = (int) ceil(mt_rand((int) (user()->defensepower * 0.75), (int) user()->defensepower) / 4);
$tododge = rand(1, 150);
if ($tododge <= sqrt(Lib::user()->dexterity)) {
if ($tododge <= sqrt(user()->dexterity)) {
$tohit = 0;
$pagearray .= "You dodge the monster's attack. No damage has been scored.<br>";
$persondamage = 0;
} else {
$persondamage = max(1, $tohit - $toblock);
if (Lib::user()->currentuberdefense != 0) {
$persondamage -= (int) ceil($persondamage * (Lib::user()->currentuberdefense / 100));
if (user()->currentuberdefense != 0) {
$persondamage -= (int) ceil($persondamage * (user()->currentuberdefense / 100));
}
$persondamage = max(1, $persondamage);
}
$pagearray .= "The monster attacks you for $persondamage damage.<br><br>";
Lib::user()->currenthp -= $persondamage;
user()->currenthp -= $persondamage;
if (Lib::user()->currenthp <= 0) {
$newgold = (int) ceil(Lib::user()->gold / 2);
$newhp = (int) ceil(Lib::user()->maxhp / 4);
Lib::db()->query("UPDATE users SET currenthp=?, currentaction='In Town', currentmonster=0, currentmonsterhp=0, currentmonstersleep=0, currentmonsterimmune=0, currentfight=0, latitude=0, longitude=0, gold=? WHERE id=?;", [
if (user()->currenthp <= 0) {
$newgold = (int) ceil(user()->gold / 2);
$newhp = (int) ceil(user()->maxhp / 4);
db()->query("UPDATE users SET currenthp=?, currentaction='In Town', currentmonster=0, currentmonsterhp=0, currentmonstersleep=0, currentmonsterimmune=0, currentfight=0, latitude=0, longitude=0, gold=? WHERE id=?;", [
$newhp, $newgold, $userrow['id'],
]);
self::dead();
@ -517,38 +516,38 @@ class Fight
$pagearray = '';
switch ($newspellrow['type']) {
case 1: // Heal spell
$newhp = min(Lib::user()->currenthp + $newspellrow['attribute'], Lib::user()->maxhp);
Lib::user()->currenthp = $newhp;
Lib::user()->currentmp -= $newspellrow['mp'];
$newhp = min(user()->currenthp + $newspellrow['attribute'], user()->maxhp);
user()->currenthp = $newhp;
user()->currentmp -= $newspellrow['mp'];
$pagearray = "You have cast the {$newspellrow['name']} spell, and gained {$newspellrow['attribute']} Hit Points.<br><br>";
break;
case 2: // Hurt spell
if (Lib::user()->currentmonsterimmune == 0) {
if (user()->currentmonsterimmune == 0) {
$monsterdamage = mt_rand((int) (($newspellrow['attribute'] / 6) * 5), $newspellrow['attribute']);
Lib::user()->currentmonsterhp -= $monsterdamage;
user()->currentmonsterhp -= $monsterdamage;
$pagearray = "You have cast the {$newspellrow['name']} spell for $monsterdamage damage.<br><br>";
} else {
$pagearray = "You have cast the {$newspellrow['name']} spell, but the monster is immune to it.<br><br>";
}
Lib::user()->currentmp -= $newspellrow['mp'];
user()->currentmp -= $newspellrow['mp'];
break;
case 3: // Sleep spell
if (Lib::user()->currentmonsterimmune != 2) {
Lib::user()->currentmonstersleep = $newspellrow['attribute'];
if (user()->currentmonsterimmune != 2) {
user()->currentmonstersleep = $newspellrow['attribute'];
$pagearray = "You have cast the {$newspellrow['name']} spell. The monster is asleep.<br><br>";
} else {
$pagearray = "You have cast the {$newspellrow['name']} spell, but the monster is immune to it.<br><br>";
}
Lib::user()->currentmp -= $newspellrow['mp'];
user()->currentmp -= $newspellrow['mp'];
break;
case 4: // +Damage spell
Lib::user()->currentuberdamage = $newspellrow['attribute'];
Lib::user()->currentmp -= $newspellrow['mp'];
user()->currentuberdamage = $newspellrow['attribute'];
user()->currentmp -= $newspellrow['mp'];
$pagearray = "You have cast the {$newspellrow['name']} spell, and will gain {$newspellrow['attribute']}% damage until the end of this fight.<br><br>";
break;
case 5: // +Defense spell
Lib::user()->currentuberdefense = $newspellrow['attribute'];
Lib::user()->currentmp -= $newspellrow['mp'];
user()->currentuberdefense = $newspellrow['attribute'];
user()->currentmp -= $newspellrow['mp'];
$pagearray = "You have cast the {$newspellrow['name']} spell, and will gain {$newspellrow['attribute']}% defense until the end of this fight.<br><br>";
break;
}

View File

@ -13,7 +13,6 @@ declare(strict_types=1);
namespace DragonKnight\Actions;
use DragonKnight\Lib;
use DragonKnight\Router;
class Forum
@ -31,7 +30,7 @@ class Forum
public static function donothing($start = 0)
{
$query = Lib::db()->query('SELECT * FROM forum WHERE parent=0 ORDER BY newpostdate DESC LIMIT 20 OFFSET ?;', [20 * $start]);
$query = db()->query('SELECT * FROM forum WHERE parent=0 ORDER BY newpostdate DESC LIMIT 20 OFFSET ?;', [20 * $start]);
$page = <<<HTML
<table width="100%">
<tr>
@ -68,45 +67,45 @@ class Forum
$page .= '</table></td></tr></table>';
Lib::page_title('Forum');
page_title('Forum');
return $page;
}
public static function showthread($id, $start)
{
$posts = Lib::db()->query('SELECT * FROM forum WHERE id=? OR parent=? ORDER BY id LIMIT 15 OFFSET ?;', [$id, $id, $start * 15]);
$title = Lib::db()->query('SELECT title FROM forum WHERE id=? LIMIT 1;', [$id])->fetchArray(SQLITE3_ASSOC);
$posts = db()->query('SELECT * FROM forum WHERE id=? OR parent=? ORDER BY id LIMIT 15 OFFSET ?;', [$id, $id, $start * 15]);
$title = db()->query('SELECT title FROM forum WHERE id=? LIMIT 1;', [$id])->fetchArray(SQLITE3_ASSOC);
$page = '<table width="100%"><tr><td style="padding:1px; background-color:black;"><table width="100%" style="margins:0px;" cellspacing="1" cellpadding="3"><tr><td colspan="2" style="background-color:#dddddd;"><b><a href="/forum" hx-get="/forum" hx-target="#middle">Forum</a> :: '.$title['title']."</b></td></tr>\n";
while ($row = $posts->fetchArray(SQLITE3_ASSOC)) {
$page .= '<tr><td width="25%" style="background-color:#ffffff; vertical-align:top;"><span class="small"><b>'.$row['author'].'</b><br><br>'.Lib::pretty_date($row['postdate']).'</td><td style="background-color:#ffffff; vertical-align:top;">'.nl2br($row['content'])."</td></tr>\n";
$page .= '<tr><td width="25%" style="background-color:#ffffff; vertical-align:top;"><span class="small"><b>'.$row['author'].'</b><br><br>'.pretty_date($row['postdate']).'</td><td style="background-color:#ffffff; vertical-align:top;">'.nl2br($row['content'])."</td></tr>\n";
}
$page .= '</table></td></tr></table><br>';
$page .= "<table width=\"100%\"><tr><td><b>Reply To This Thread:</b><br><form action=\"/forum/reply\" method=\"post\" hx-post=\"/forum/reply\" hx-target=\"#middle\"><input type=\"hidden\" name=\"parent\" value=\"$id\" /><input type=\"hidden\" name=\"title\" value=\"Re: ".$title['title'].'" /><textarea name="content" rows="7" cols="40"></textarea><br><input type="submit" name="submit" value="Submit" /> <input type="reset" name="reset" value="Reset" /></form></td></tr></table>';
Lib::page_title('Forum: '.$title['title']);
page_title('Forum: '.$title['title']);
return $page;
}
public static function reply()
{
$form = Lib::validate($_POST, [
$form = validate($_POST, [
'title' => [],
'content' => [],
]);
if (! $form['valid']) {
exit(Lib::ul_from_validate_errors($form['errors']));
exit(ul_from_validate_errors($form['errors']));
}
$form = $form['data'];
Lib::db()->query('INSERT INTO forum (author, title, content, parent) VALUES (?, ?, ?, ?);', [
Lib::user()->username, $form['title'], $form['content'], $form['parent'],
db()->query('INSERT INTO forum (author, title, content, parent) VALUES (?, ?, ?, ?);', [
user()->username, $form['title'], $form['content'], $form['parent'],
]);
Lib::db()->query('UPDATE forum SET newpostdate=CURRENT_TIMESTAMP, replies=replies + 1 WHERE id=?;', [$form['parent']]);
db()->query('UPDATE forum SET newpostdate=CURRENT_TIMESTAMP, replies=replies + 1 WHERE id=?;', [$form['parent']]);
return self::showthread($form['parent'], 0);
}
@ -114,23 +113,23 @@ class Forum
public static function newthread()
{
if (isset($_POST['submit'])) {
$form = Lib::validate($_POST, [
$form = validate($_POST, [
'title' => ['length:2-30'],
'content' => [],
]);
if (! $form['valid']) {
exit(Lib::ul_from_validate_errors($form['errors']));
exit(ul_from_validate_errors($form['errors']));
}
$form = $form['data'];
Lib::db()->query('INSERT INTO forum (author, title, content) VALUES (?, ?, ?);', [
Lib::user()->username, $form['title'], $form['content'],
db()->query('INSERT INTO forum (author, title, content) VALUES (?, ?, ?);', [
user()->username, $form['title'], $form['content'],
]);
Lib::redirect('/forum/thread/'.Lib::db()->lastInsertRowID().'/0');
redirect('/forum/thread/'.db()->lastInsertRowID().'/0');
}
Lib::page_title('Form: New Thread');
page_title('Form: New Thread');
return '<table width="100%"><tr><td><b>Make A New Post:</b><br><br/ ><form action="/forum/new" method="post" hx-post="/forum/new" hx-target="#middle">Title:<br><input type="text" name="title" size="50" maxlength="50" /><br><br>Message:<br><textarea name="content" rows="7" cols="40"></textarea><br><br><input type="submit" name="submit" value="Submit" /> <input type="reset" name="reset" value="Reset" /></form></td></tr></table>';
}

View File

@ -13,14 +13,12 @@ declare(strict_types=1);
namespace DragonKnight\Actions;
use DragonKnight\Lib;
class Heal
{
public static function healspells(int $id): string
{
$user_spells = Lib::user()->spells();
$spell = Lib::get_spell($id);
$user_spells = user()->spells();
$spell = get_spell($id);
$has_spell = false;
foreach ($user_spells as $us) {
if ($us['id'] === $id) {
@ -32,23 +30,23 @@ class Heal
$page = 'You have not yet learned this spell. Please go back and try again.';
} elseif ($spell['type'] !== 1) {
$page = 'This is not a healing spell. Please go back and try again.';
} elseif (Lib::user()->currentmp < $spell['mp']) {
} elseif (user()->currentmp < $spell['mp']) {
$page = 'You do not have enough Magic Points to cast this spell. Please go back and try again.';
} elseif (Lib::user()->currentaction === 'Fighting') {
} elseif (user()->currentaction === 'Fighting') {
$page = 'You cannot use the Quick Spells list during a fight. Please go back and select the Healing Spell you wish to use from the Spells box on the main fighting screen to continue.';
} elseif (Lib::user()->currenthp == Lib::user()->maxhp) {
} elseif (user()->currenthp == user()->maxhp) {
$page = 'Your HP is already full. You don\'t need to use a Healing spell now.';
} else {
$restored = Lib::user()->restore_hp($spell['attribute']);
Lib::user()->currentmp -= $spell['mp'];
Lib::user()->save();
$restored = user()->restore_hp($spell['attribute']);
user()->currentmp -= $spell['mp'];
user()->save();
$page = <<<HTML
You have cast the {$spell['name']} spell, and gained {$restored} HP. You can now continue <a href="/" hx-get="/" hx-target="#middle">exploring</a>.
HTML;
}
Lib::page_title('Casting '.$spell['name']);
page_title('Casting '.$spell['name']);
return $page;
}

View File

@ -13,7 +13,6 @@ declare(strict_types=1);
namespace DragonKnight\Actions;
use DragonKnight\Lib;
use DragonKnight\Router;
class Help
@ -270,7 +269,7 @@ class Help
<tr><td><b>Type</b></td><td><b>Name</b></td><td><b>Cost</b></td><td><b>Attribute</b></td><td><b>Special</b></td></tr>
HTML;
$items = Lib::db()->query('SELECT * FROM items ORDER BY id;');
$items = db()->query('SELECT * FROM items ORDER BY id;');
$item_types = [1 => ['weapon', 'Attack'], 2 => ['armor', 'Defense'], 3 => ['shield', 'Defense']];
while ($item = $items->fetchArray(SQLITE3_ASSOC)) {
@ -279,7 +278,7 @@ class Help
if ($item['special'] !== 'X') {
$special = explode(',', $item['special']);
$attr = Lib::special_to_string($special[0]);
$attr = special_to_string($special[0]);
$stat = (($special[1] > 0) ? '+' : '').$special[1];
$bigspecial = "$attr $stat";
} else {
@ -299,12 +298,12 @@ class Help
<tr><td><b>Name</b></td><td><b>Monster Level</b></td><td><b>Attribute 1</b></td><td><b>Attribute 2</b></td></tr>
HTML;
$drops = Lib::db()->query('SELECT * FROM drops ORDER BY id;');
$drops = db()->query('SELECT * FROM drops ORDER BY id;');
while ($drop = $drops->fetchArray(SQLITE3_ASSOC)) {
if ($drop['attribute1'] !== 'X') {
$special = explode(',', $drop['attribute1']);
$attr = Lib::special_to_string($special[0]);
$attr = special_to_string($special[0]);
$stat = (($special[1] > 0) ? '+' : '').$special[1];
$bigspecial1 = "$attr $stat";
} else {
@ -313,7 +312,7 @@ class Help
if ($drop['attribute2'] !== 'X') {
$special = explode(',', $drop['attribute2']);
$attr = Lib::special_to_string($special[0]);
$attr = special_to_string($special[0]);
$stat = (($special[1] > 0) ? '+' : '').$special[1];
$bigspecial2 = "$attr $stat";
} else {
@ -335,7 +334,7 @@ class Help
<tr><td><b>Name</b></td><td><b>Cost</b></td><td><b>Type</b></td><td><b>Attribute</b></td></tr>
HTML;
$spells = Lib::db()->query('SELECT * FROM spells ORDER BY id;');
$spells = db()->query('SELECT * FROM spells ORDER BY id;');
$spell_types = ['None', 'Heal', 'Hurt', 'Sleep', '+Damage (%)', '+Defense (%)'];
while ($spell = $spells->fetchArray(SQLITE3_ASSOC)) {
$page .= <<<HTML
@ -368,7 +367,7 @@ class Help
<tr><td><b>Name</b></td><td><b>Max HP</b></td><td><b>Max Damage</b></td><td><b>Armor</b></td><td><b>Level</b></td><td><b>Max Exp.</b></td><td><b>Max Gold</b></td><td><b>Immunity</b></td></tr>
HTML;
$monsters = Lib::db()->query('SELECT * FROM monsters ORDER BY id;');
$monsters = db()->query('SELECT * FROM monsters ORDER BY id;');
$immunities = ['<span class="light">None</span>', 'Hurt', 'Hurt & Sleep'];
while ($m = $monsters->fetchArray(SQLITE3_ASSOC)) {
@ -383,7 +382,7 @@ class Help
{
$rows = [];
$levels = Lib::db()->query('SELECT * FROM levels ORDER BY id;');
$levels = db()->query('SELECT * FROM levels ORDER BY id;');
while ($level = $levels->fetchArray(SQLITE3_ASSOC)) {
$class_data = [1 => [], 2 => [], 3 => []];
@ -405,7 +404,7 @@ class Help
}
$spells = [];
$spells_query = Lib::db()->query('SELECT * FROM spells ORDER BY id;');
$spells_query = db()->query('SELECT * FROM spells ORDER BY id;');
while ($spell = $spells_query->fetchArray(SQLITE3_ASSOC)) {
$spells[$spell['id']] = $spell;
}
@ -508,7 +507,7 @@ class Help
public static function display_help(string $content)
{
return Lib::render('layouts/help', [
return render('layouts/help', [
'content' => $content,
'version' => VERSION,
'build' => BUILD,

View File

@ -13,9 +13,13 @@ declare(strict_types=1);
namespace DragonKnight\Actions;
use DragonKnight\Lib;
use DragonKnight\Router;
use function DragonKnight\db;
use function DragonKnight\ul_from_validate_errors;
use function DragonKnight\user;
use function DragonKnight\validate;
class Install
{
public static function register_routes(Router $r): Router
@ -56,20 +60,20 @@ class Install
*/
public static function second()
{
if (! is_dir($path = getcwd().'/db')) {
if (! is_dir($path = getcwd().'/db')) {
if (mkdir($path, 0777, true) === false) {
throw new \Exception('Failed to create database directory at '.$path.'. Please check permissions.');
}
throw new \Exception('Failed to create database directory at '.$path.'. Please check permissions.');
}
}
if (file_exists($path = getcwd().'/db/database.db')) {
if (unlink($path) === false) {
throw new \Exception('Failed to delete existing database file at '.$path.'. Please check permissions.');
}
throw new \Exception('Failed to delete existing database file at '.$path.'. Please check permissions.');
}
}
$page = '<html><head><title>Dragon Knight Installation</title></head><body><b>Dragon Knight Installation: Page Two</b><br><br>';
$query = Lib::db()->exec(<<<SQL
$query = db()->exec(<<<SQL
CREATE TABLE babble (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`posttime` TEXT NOT NULL DEFAULT '00:00:00',
@ -80,7 +84,7 @@ class Install
$page .= self::table_status_msg($query === true, 'Babble', 'create');
$query = Lib::db()->exec(<<<SQL
$query = db()->exec(<<<SQL
CREATE TABLE drops (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`name` TEXT NOT NULL DEFAULT '',
@ -93,7 +97,7 @@ class Install
$page .= self::table_status_msg($query === true, 'Drops', 'create');
$query = Lib::db()->exec(<<<SQL
$query = db()->exec(<<<SQL
INSERT INTO drops VALUES
(1, 'Life Pebble', 1, 1, 'maxhp,10', 'X'),
(2, 'Life Stone', 10, 1, 'maxhp,25', 'X'),
@ -131,7 +135,7 @@ class Install
$page .= self::table_status_msg($query === true, 'Drops', 'populate');
$query = Lib::db()->exec(<<<SQL
$query = db()->exec(<<<SQL
CREATE TABLE forum (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`postdate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
@ -146,7 +150,7 @@ class Install
$page .= self::table_status_msg($query === true, 'Forum', 'create');
$query = Lib::db()->exec(<<<SQL
$query = db()->exec(<<<SQL
CREATE TABLE items (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`type` INTEGER NOT NULL DEFAULT 0,
@ -159,7 +163,7 @@ class Install
$page .= self::table_status_msg($query === true, 'Items', 'create');
$query = Lib::db()->exec(<<<SQL
$query = db()->exec(<<<SQL
INSERT INTO items VALUES
(1, 1, 'Stick', 10, 2, 'X'),
(2, 1, 'Branch', 30, 4, 'X'),
@ -198,7 +202,7 @@ class Install
$page .= self::table_status_msg($query === true, 'Drops', 'populate');
$query = Lib::db()->exec(<<<SQL
$query = db()->exec(<<<SQL
CREATE TABLE classes (
'id' INTEGER PRIMARY KEY AUTOINCREMENT,
'name' TEXT NOT NULL,
@ -217,7 +221,7 @@ class Install
$page .= self::table_status_msg($query === true, 'Classes', 'create');
$query = Lib::db()->exec(<<<SQL
$query = db()->exec(<<<SQL
INSERT INTO classes VALUES
(0, 'Adventurer', '', 3, 15, 10, 4, 4, 2, 2, 2, 2),
(1, 'Mage', '', 1, 10, 15, 1, 7, 1, 3, 1, 2),
@ -227,7 +231,7 @@ class Install
$page .= self::table_status_msg($query === true, 'Classes', 'populate');
$query = Lib::db()->exec(<<<SQL
$query = db()->exec(<<<SQL
CREATE TABLE levels (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`1_exp` INTEGER NOT NULL DEFAULT 0,
@ -256,7 +260,7 @@ class Install
$page .= self::table_status_msg($query === true, 'Levels', 'create');
$query = Lib::db()->exec(<<<SQL
$query = db()->exec(<<<SQL
INSERT INTO levels VALUES
(1, 0, 15, 0, 5, 5, 5, 0, 0, 15, 0, 5, 5, 5, 0, 0, 15, 0, 5, 5, 5, 0),
(2, 15, 2, 5, 1, 0, 1, 1, 18, 2, 4, 1, 2, 1, 1, 20, 2, 5, 1, 0, 2, 1),
@ -362,7 +366,7 @@ class Install
$page .= self::table_status_msg($query === true, 'Levels', 'populate');
$query = Lib::db()->exec(<<<SQL
$query = db()->exec(<<<SQL
CREATE TABLE monsters (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`name` TEXT NOT NULL DEFAULT '',
@ -378,7 +382,7 @@ class Install
$page .= self::table_status_msg($query === true, 'Monsters', 'create');
$query = Lib::db()->exec(<<<SQL
$query = db()->exec(<<<SQL
INSERT INTO monsters VALUES
(1, 'Blue Slime', 4, 3, 1, 1, 1, 1, 0),
(2, 'Red Slime', 6, 5, 1, 1, 2, 1, 0),
@ -535,7 +539,7 @@ class Install
$page .= self::table_status_msg($query === true, 'Monsters', 'populate');
$query = Lib::db()->exec(<<<SQL
$query = db()->exec(<<<SQL
CREATE TABLE news (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`author` TEXT NOT NULL DEFAULT 'Guild Master',
@ -546,11 +550,11 @@ class Install
$page .= self::table_status_msg($query === true, 'News', 'create');
$query = Lib::db()->exec("INSERT INTO news (content) VALUES ('This is the first news post. Please use the admin control panel to add another one and make this one go away.');");
$query = db()->exec("INSERT INTO news (content) VALUES ('This is the first news post. Please use the admin control panel to add another one and make this one go away.');");
$page .= self::table_status_msg($query === true, 'News', 'populate');
$query = Lib::db()->exec(<<<SQL
$query = db()->exec(<<<SQL
CREATE TABLE spells (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`name` TEXT NOT NULL,
@ -562,7 +566,7 @@ class Install
$page .= self::table_status_msg($query === true, 'Spells', 'create');
$query = Lib::db()->exec(<<<SQL
$query = db()->exec(<<<SQL
INSERT INTO spells VALUES
(1, 'Heal', 5, 10, 1),
(2, 'Revive', 10, 25, 1),
@ -587,7 +591,7 @@ class Install
$page .= self::table_status_msg($query === true, 'Spells', 'populate');
$query = Lib::db()->exec(<<<SQL
$query = db()->exec(<<<SQL
CREATE TABLE towns (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`name` TEXT NOT NULL,
@ -602,7 +606,7 @@ class Install
$page .= self::table_status_msg($query === true, 'Towns', 'create');
$query = Lib::db()->exec(<<<SQL
$query = db()->exec(<<<SQL
INSERT INTO towns VALUES
(1, 'Midworld', 0, 0, 5, 0, 0, '1,2,3,17,18,19,28,29'),
(2, 'Roma', 30, 30, 10, 25, 5, '2,3,4,18,19,29'),
@ -616,7 +620,7 @@ class Install
$page .= self::table_status_msg($query === true, 'Towns', 'populate');
$query = Lib::db()->exec(<<<SQL
$query = db()->exec(<<<SQL
CREATE TABLE users (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`username` TEXT NOT NULL,
@ -714,7 +718,7 @@ class Install
public static function fourth(?array $post = null)
{
$post ??= $_POST;
$form = Lib::validate($post, [
$form = validate($post, [
'username' => ['length:3-18', 'alpha-spaces'],
'email' => ['email'],
'confirm_email' => ['confirm'],
@ -723,11 +727,11 @@ class Install
]);
if (! $form['valid']) {
exit(Lib::ul_from_validate_errors($form['errors']));
exit(ul_from_validate_errors($form['errors']));
}
$form = $form['data'];
if (Lib::db()->query(
if (db()->query(
"INSERT INTO users (username, password, email, verify, charclass, authlevel) VALUES (?, ?, ?, 'g2g', ?, 1)",
[$form['username'], password_hash($form['password'], PASSWORD_ARGON2ID), $form['email'], $form['charclass'] ?? null]
) === false) {
@ -788,7 +792,7 @@ class Install
};
if ($condition === false) {
return "Error {$verb[1]} $table_name table. (".Lib::db()->lastErrorMsg().')<br>';
return "Error {$verb[1]} $table_name table. (".db()->lastErrorMsg().')<br>';
}
return "$table_name table {$verb[0]}.<br>";

View File

@ -13,9 +13,19 @@ declare(strict_types=1);
namespace DragonKnight\Actions;
use DragonKnight\Lib;
use DragonKnight\Router;
use function DragonKnight\db;
use function DragonKnight\env;
use function DragonKnight\get_town_by_xy;
use function DragonKnight\get_item;
use function DragonKnight\get_town_by_id;
use function DragonKnight\is_post;
use function DragonKnight\page_title;
use function DragonKnight\redirect;
use function DragonKnight\render;
use function DragonKnight\user;
class Towns
{
public static function register_routes(Router $r): Router
@ -36,7 +46,7 @@ class Towns
*/
public static function town()
{
$town = Lib::get_town_by_xy(Lib::user()->longitude, Lib::user()->latitude);
$town = get_town_by_xy(user()->longitude, user()->latitude);
if ($town === false) {
exit('There is an error with your user account, or with the town data. Please try again.');
}
@ -44,9 +54,9 @@ class Towns
$page = ['news' => '', 'whos_online' => ''];
// News box. Grab latest news entry and display it. Something a little more graceful coming soon maybe.
if (Lib::env('show_news')) {
$news = Lib::db()->query('SELECT * FROM news ORDER BY id DESC LIMIT 1;')->fetchArray(SQLITE3_ASSOC);
$news_date = Lib::pretty_date($news['postdate']);
if (env('show_news')) {
$news = db()->query('SELECT * FROM news ORDER BY id DESC LIMIT 1;')->fetchArray(SQLITE3_ASSOC);
$news_date = pretty_date($news['postdate']);
$news_content = nl2br($news['content']);
$page['news'] = <<<HTML
<div class="title">Latest News</div>
@ -56,8 +66,8 @@ class Towns
}
// Who's Online. Currently just members. Guests maybe later.
if (Lib::env('show_online')) {
$onlinequery = Lib::db()->query(<<<SQL
if (env('show_online')) {
$onlinequery = db()->query(<<<SQL
SELECT id, username
FROM users
WHERE onlinetime >= datetime('now', '-600 seconds')
@ -75,13 +85,13 @@ class Towns
$online_rows = implode(', ', $online_rows);
$page['whos_online'] = <<<HTML
<div class="title">Who's Online</div>
There are <b>$online_count</b> Lib::user(s) online within the last 10 minutes: $online_rows
There are <b>$online_count</b> user(s) online within the last 10 minutes: $online_rows
HTML;
}
Lib::page_title($town['name']);
page_title($town['name']);
return Lib::render('towns', ['town' => $town, 'news' => $page['news'], 'whos_online' => $page['whos_online']]);
return render('towns', ['town' => $town, 'news' => $page['news'], 'whos_online' => $page['whos_online']]);
}
/**
@ -90,25 +100,25 @@ class Towns
*/
public static function inn()
{
$town = Lib::get_town_by_xy(Lib::user()->longitude, Lib::user()->latitude);
$town = get_town_by_xy(user()->longitude, user()->latitude);
if ($town === false) {
exit('Cheat attempt detected.<br><br>Get a life, loser.');
}
if (Lib::user()->gold < $town['innprice']) {
if (user()->gold < $town['innprice']) {
$page = <<<HTML
You do not have enough gold to stay at this Inn tonight. <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 (Lib::is_post() && $_POST['rest']) {
Lib::user()->gold -= $town['innprice'];
Lib::user()->restore_points()->save();
} elseif (is_post() && $_POST['rest']) {
user()->gold -= $town['innprice'];
user()->restore_points()->save();
$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 (Lib::is_post() && ! $_POST['rest']) {
Lib::redirect('/');
} elseif (is_post() && ! $_POST['rest']) {
redirect('/');
} else {
$page = <<<HTML
Resting at the inn will refill your current HP, MP, and TP to their maximum levels.<br><br>
@ -120,7 +130,7 @@ class Towns
HTML;
}
Lib::page_title($town['name'].' Inn');
page_title($town['name'].' Inn');
return $page;
}
@ -132,7 +142,7 @@ class Towns
*/
public static function shop()
{
$town = Lib::get_town_by_xy(Lib::user()->longitude, Lib::user()->latitude);
$town = get_town_by_xy(user()->longitude, user()->latitude);
if ($town === false) {
exit('Cheat attempt detected.<br><br>Get a life, loser.');
}
@ -144,7 +154,7 @@ class Towns
<table>
HTML;
$items = Lib::db()->query('SELECT * FROM items WHERE id IN ('.$town['itemslist'].');');
$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%">';
@ -154,7 +164,7 @@ class Towns
3 => '<img src="/img/icon_shield.gif" alt="shield">'
};
$page .= '</td>';
if (Lib::user()->weaponid === $item['id'] || Lib::user()->armorid === $item['id'] || Lib::user()->shieldid === $item['id']) {
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>
@ -175,7 +185,7 @@ class Towns
If you've changed your mind, you may also return back to <a hx-get="/" hx-target="#middle">town</a>.
HTML;
Lib::page_title($town['name'].' Shop');
page_title($town['name'].' Shop');
return $page;
}
@ -185,15 +195,15 @@ class Towns
*/
public static function buy(int $id)
{
$town = Lib::get_town_by_xy(Lib::user()->longitude, Lib::user()->latitude);
$town = get_town_by_xy(user()->longitude, user()->latitude);
if ($town === false) {
Lib::redirect('/');
redirect('/');
}
if (! in_array($id, explode(',', $town['itemslist']))) {
Lib::redirect('/shop');
redirect('/shop');
}
$item = Lib::get_item($id);
$can_afford = Lib::user()->gold >= $item['buycost'];
$item = get_item($id);
$can_afford = user()->gold >= $item['buycost'];
if (! $can_afford) {
$page = <<<HTML
@ -201,9 +211,9 @@ class Towns
You may return to <a hx-get="/" hx-target="#middle">town</a>, <a hx-get="/shop" hx-target="#middle">shop</a>,
or use the direction buttons on the left to start exploring.
HTML;
} elseif (Lib::is_post() && ! $_POST['buy']) {
Lib::redirect('/shop');
} elseif (Lib::is_post() && $_POST['buy']) {
} elseif (is_post() && ! $_POST['buy']) {
redirect('/shop');
} elseif (is_post() && $_POST['buy']) {
$type_mapping = [
1 => ['id' => 'weaponid', 'name' => 'weaponname', 'power' => 'attackpower'],
2 => ['id' => 'armorid', 'name' => 'armorname', 'power' => 'defensepower'],
@ -215,9 +225,9 @@ class Towns
}
// Retrieve current equipped item or create a default
$current_equip_id = Lib::user()->{$type_mapping[$item['type']]['id']};
$current_equip_id = user()->{$type_mapping[$item['type']]['id']};
if ($current_equip_id != 0) {
$item2 = Lib::get_item($current_equip_id);
$item2 = get_item($current_equip_id);
} else {
$item2 = ['attribute' => 0, 'buycost' => 0, 'special' => 'X'];
}
@ -233,9 +243,9 @@ class Towns
$toChange = $special[0];
$changeAmount = $index === 0 ? $special[1] : -$special[1];
Lib::user()->$toChange += $changeAmount;
user()->$toChange += $changeAmount;
$specialFields[] = "$toChange = ?";
$specialValues[] = Lib::user()->$toChange;
$specialValues[] = user()->$toChange;
// Adjust attack or defense power
if ($toChange == 'strength' || $toChange == 'dexterity') {
@ -248,21 +258,21 @@ class Towns
// Determine power and type-specific updates
$currentType = $type_mapping[$item['type']];
$powerField = $currentType['power'];
Lib::user()->$powerField += $item['attribute'] - $item2['attribute'];
user()->$powerField += $item['attribute'] - $item2['attribute'];
// Calculate new gold with trade-in value
Lib::user()->gold += ceil($item2['buycost'] / 2) - $item['buycost'];
user()->gold += ceil($item2['buycost'] / 2) - $item['buycost'];
// Ensure current HP/MP/TP don't exceed max values
Lib::user()->currenthp = min(Lib::user()->currenthp, Lib::user()->maxhp);
Lib::user()->currentmp = min(Lib::user()->currentmp, Lib::user()->maxmp);
Lib::user()->currenttp = min(Lib::user()->currenttp, Lib::user()->maxtp);
user()->currenthp = min(user()->currenthp, user()->maxhp);
user()->currentmp = min(user()->currentmp, user()->maxmp);
user()->currenttp = min(user()->currenttp, user()->maxtp);
// Update item info in user
Lib::user()->{$type_mapping[$item['type']]['id']} = $item['id'];
Lib::user()->{$type_mapping[$item['type']]['name']} = $item['name'];
user()->{$type_mapping[$item['type']]['id']} = $item['id'];
user()->{$type_mapping[$item['type']]['name']} = $item['name'];
Lib::user()->save();
user()->save();
$page = <<<HTML
Thank you for purchasing <b>{$item['name']}</b>.<br><br>
@ -271,10 +281,10 @@ class Towns
HTML;
} else {
$type_to_row_mapping = [1 => 'weaponid', 2 => 'armorid', 3 => 'shieldid'];
$current_equipped_id = Lib::user()->{$type_to_row_mapping[$item['type']]} ?? 0;
$current_equipped_id = user()->{$type_to_row_mapping[$item['type']]} ?? 0;
if ($current_equipped_id != 0) {
$item2 = Lib::get_item($current_equipped_id);
$item2 = get_item($current_equipped_id);
$sell_price = ceil($item2['buycost'] / 2);
$page = <<<HTML
If you are buying the {$item['name']}, then I will buy your {$item2['name']} for $sell_price gold. Is that ok?<br><br>
@ -294,7 +304,7 @@ class Towns
}
}
Lib::page_title('Buying '.$item['name']);
page_title('Buying '.$item['name']);
return $page;
}
@ -310,8 +320,8 @@ class Towns
<table>
HTML;
$mapped = explode(',', Lib::user()->towns);
$towns = Lib::db()->query('SELECT * FROM towns ORDER BY id;');
$mapped = explode(',', user()->towns);
$towns = db()->query('SELECT * FROM towns ORDER BY id;');
while ($town = $towns->fetchArray(SQLITE3_ASSOC)) {
$latitude = ($town['latitude'] >= 0) ? $town['latitude'].'N,' : ($town['latitude'] * -1).'S,';
$longitude = ($town['longitude'] >= 0) ? $town['longitude'].'E' : ($town['longitude'] * -1).'W';
@ -341,34 +351,34 @@ class Towns
If you've changed your mind, you may also return back to <a hx-get="/" hx-target="#middle">town</a>.
HTML;
Lib::page_title('Maps');
page_title('Maps');
return $page;
}
public static function buy_map(int $id): string
{
$town = Lib::get_town_by_id($id);
$town = get_town_by_id($id);
if ($town === false) {
Lib::redirect('/maps');
redirect('/maps');
}
if (Lib::user()->gold < $town['mapprice']) {
if (user()->gold < $town['mapprice']) {
$page = <<<HTML
You do not have enough gold to buy this map.<br><br>
You may return to <a hx-get="/" hx-target="#middle">town</a>, <a hx-get="/maps" hx-target="#middle">store</a>, or use the direction buttons on the left to start exploring.
HTML;
} elseif (Lib::is_post() && $_POST['buy']) {
Lib::user()->towns .= ",$id";
Lib::user()->gold -= $town['mapprice'];
Lib::user()->save();
} elseif (is_post() && $_POST['buy']) {
user()->towns .= ",$id";
user()->gold -= $town['mapprice'];
user()->save();
$page = <<<HTML
Thank you for purchasing this map.<br><br>
You may return to <a hx-get="/" hx-target="#middle">town</a>, <a hx-get="/maps" hx-target="#middle">map shop</a>, or use the direction buttons on the left to start exploring.
HTML;
} elseif (Lib::is_post() && ! $_POST['buy']) {
Lib::redirect('/maps');
} elseif (is_post() && ! $_POST['buy']) {
redirect('/maps');
} else {
$page = <<<HTML
You are buying the <b>{$town['name']}</b> map for {$town['mapprice']} gold. Is that ok?<br><br>
@ -379,7 +389,7 @@ class Towns
HTML;
}
Lib::page_title('Buying '.$town['name'].' Map');
page_title('Buying '.$town['name'].' Map');
return $page;
}
@ -389,24 +399,24 @@ class Towns
*/
public static function travelto(int $id, bool $use_points = true): string
{
if (Lib::user()->currentaction == 'Fighting') {
Lib::redirect('/fight');
if (user()->currentaction == 'Fighting') {
redirect('/fight');
}
$town = Lib::get_town_by_id($id);
$town = get_town_by_id($id);
$cost = $use_points ? $town['travelpoints'] : 0;
$mapped = explode(',', Lib::user()->towns);
$mapped = explode(',', user()->towns);
$travelled = false;
if ($use_points && ! in_array($id, $mapped)) {
// trying to teleport to this town when it is not mapped
Lib::redirect('/');
} elseif (Lib::user()->currenttp < $cost) {
redirect('/');
} elseif (user()->currenttp < $cost) {
$page = 'You do not have enough TP to travel here. Please <a href="/" hx-get="/" hx-target="#middle">go back</a> and try again when you get more TP.';
} elseif ((Lib::user()->latitude == $town['latitude']) && (Lib::user()->longitude == $town['longitude'])) {
} elseif ((user()->latitude == $town['latitude']) && (user()->longitude == $town['longitude'])) {
if (! in_array($id, $mapped)) {
// add town to user's mapped if they travelled here
Lib::user()->towns .= ",$id";
user()->towns .= ",$id";
$travelled = true;
$page = <<<HTML
You have discovered <b>{$town['name']}</b>! It has been added to your mapped towns.<br><br>
@ -416,19 +426,19 @@ class Towns
$page = 'You are already in this town. <a href="/" hx-get="/" hx-target="#middle">Click here</a> to return.';
}
} else {
Lib::user()->latitude = $town['latitude'];
Lib::user()->longitude = $town['longitude'];
Lib::user()->currenttp -= $cost;
user()->latitude = $town['latitude'];
user()->longitude = $town['longitude'];
user()->currenttp -= $cost;
$travelled = true;
$page = 'You have travelled to <b>'.$town['name'].'</b>. You may now <a href="/" hx-get="/" hx-target="#middle">enter this town</a>.';
}
if ($travelled) {
Lib::user()->currentaction = 'In Town';
Lib::user()->save();
user()->currentaction = 'In Town';
user()->save();
}
Lib::page_title('Travelling to '.$town['name']);
page_title('Travelling to '.$town['name']);
return $page;
}

View File

@ -66,7 +66,7 @@ class Auth
public function login(string $username, string $password): bool
{
$user = Lib::get_user($username);
$user = get_user($username);
if ($user === false) {
return false;
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
/*
* This file is a part of the Dragon-Knight project.
*
* Copyright (c) 2024-present Sharkk
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE.md file.
*/
namespace DragonKnight;
define('VERSION', '1.2.5');
define('BUILD', 'Reawaken');
define('START', microtime(true));

View File

@ -1,731 +0,0 @@
<?php
declare(strict_types=1);
/*
* This file is a part of the Dragon-Knight project.
*
* Copyright (c) 2024-present Sharkk
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE.md file.
*/
namespace DragonKnight;
use DragonKnight\Actions\Explore;
use DragonKnight\Actions\Towns;
use DragonKnight\Models\User;
define('VERSION', '1.2.5');
define('BUILD', 'Reawaken');
define('START', microtime(true));
class Lib
{
/**
* Return a page for a couple generic actions.
*/
public static function index(): string
{
if (self::user()->currentaction === 'In Town') {
$page = Towns::town();
} elseif (self::user()->currentaction === 'Exploring') {
$page = Explore::explore();
} elseif (self::user()->currentaction === 'Fighting') {
self::redirect('/fight');
}
return $page;
}
/**
* Show the user their position on the current world map. Only works with a game size of 250 and the default towns 😅.
*/
public static function show_map()
{
$pos = sprintf(
'<div style="position: absolute; width: 5px; height: 5px; border-radius: 1000px; border: solid 1px black; background-color: red; left: %dpx; top: %dpx;"></div>',
round(258 + Lib::user()->longitude * (500 / 500) - 3),
round(258 - Lib::user()->latitude * (500 / 500) - 3)
);
echo Lib::render('layouts/minimal', [
'content' => '<img src="/img/map.gif" alt="Map">'.$pos,
'title' => 'Map',
]);
}
/**
* Show a character's info. Defaults to the currently logged in user.
*/
public static function show_character_info(int $id = 0): string
{
$user = $id !== 0 ? User::find($id) : Lib::user();
if ($user === false) {
exit('Failed to show info for user ID '.$id);
}
$level = Lib::db()->query("SELECT `{$user->charclass}_exp` FROM levels WHERE id=? LIMIT 1;", [$user->level + 1])->fetchArray(SQLITE3_ASSOC);
$spells = $user->spells();
$magic_list = 'None';
if (! empty($spells)) {
$magic_list = '';
foreach ($spells as $spell) {
$magic_list .= $spell['name'].'<br>';
}
}
$showchar = Lib::render('show_char', [
'char' => $user,
'level' => $level,
'magic_list' => $magic_list,
]);
return Lib::render('layouts/minimal', ['content' => $showchar, 'title' => $user->username.' Information']);
}
/**
* Handle a POST request to send a new babblebox message.
*/
public static function babblebox()
{
if (Lib::is_post()) {
$content = trim($_POST['babble']);
if (! empty($content)) {
Lib::db()->query(
'INSERT INTO babble (posttime, author, babble) VALUES (CURRENT_TIMESTAMP, ?, ?);',
[Lib::user()->username, $content]
);
}
return self::babblebox_messages();
}
}
/**
* The handler that is polled by HTMX for new babblebox messages.
*/
public static function babblebox_messages(): string
{
if (Lib::user() === false) {
return '';
}
$query = Lib::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>] '.Lib::make_safe($row['babble']).'</div>';
}
if (! $has_chats) {
$messages = 'There are no messages. :(';
}
return $messages;
}
/**
* Open or get SQLite database connection.
*/
public static function db(): Database
{
if (! is_dir($path = getcwd().'/db')) {
error_log('Database folder not found at '.$path.'. Please run the installer first.');
exit();
}
return $GLOBALS['database'] ??= new Database(getcwd().'/db/database.db');
}
/**
* Redirect to a different URL, exit.
*/
public static function redirect(string $location): void
{
if (self::is_htmx()) {
$target = isset($_SERVER['HTTP_HX_TARGET']) ? '#'.$_SERVER['HTTP_HX_TARGET'] : '#middle';
$json = json_encode(['path' => $location, 'target' => $target]);
header("HX-Location: $json");
} else {
header("Location: $location");
}
exit;
}
/**
* Render a view with the given data. Can be used redundantly within the template.
*/
public static function render(string $path_to_base_view, array $data = []): string|false
{
ob_start();
extract($data);
require __DIR__."/../templates/$path_to_base_view.php";
return ob_get_clean();
}
/**
* Replace tags with given content.
*/
public static function parse(string $template, array $array): string
{
return strtr($template, array_combine(
array_map(fn ($key) => "{{{$key}}}", array_keys($array)),
array_values($array)
));
}
/**
* Change the SQLite3 datetime format (YYYY-MM-DD HH:MM:SS) into something friendlier.
*/
public static function pretty_date(string $uglydate): string
{
return date('l, F j, Y', mktime(
0,
0,
0,
(int) substr($uglydate, 5, 2), // Month
(int) substr($uglydate, 8, 2), // Day
(int) substr($uglydate, 0, 4) // Year
));
}
/**
* Use htmlentities with UTF-8 encoding to ensure we're only outputting healthy, safe and effective HTML.
*/
public static function make_safe(string $content): string
{
return htmlentities($content, ENT_QUOTES, 'UTF-8');
}
/**
* Finalize admin page and output to browser.
*/
public static function display_admin($content, $title)
{
echo self::render('layouts/admin', [
'title' => $title,
'content' => $content,
]);
exit;
}
/**
* Determine what game skin to use. If a user is logged in then it uses their setting, otherwise defaults to 0 (retro).
*/
public static function game_skin(): int
{
return self::user() !== false ? self::user()->game_skin : 0;
}
/**
* Get a town's data by it's coordinates.
*/
public static function get_town_by_xy(int $x, int $y): array|false
{
$cache_tag = "town-$x-$y";
if (! isset($GLOBALS['cache'][$cache_tag])) {
$query = self::db()->query('SELECT * FROM towns WHERE longitude = ? AND latitude = ? LIMIT 1;', [$x, $y]);
if ($query === false) {
return false;
}
$GLOBALS['cache'][$cache_tag] = $query->fetchArray(SQLITE3_ASSOC);
}
return $GLOBALS['cache'][$cache_tag];
}
/**
* Get a town's data by it's ID.
*/
public static function get_town_by_id(int $id): array|false
{
$query = self::db()->query('SELECT * FROM towns WHERE id = ? LIMIT 1;', [$id]);
if ($query === false) {
return false;
}
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Get a user's data by their ID, username or email.
*/
public static function get_user(int|string $id, string $data = '*'): array|false
{
$query = self::db()->query(
"SELECT $data FROM users WHERE id=? OR username=? COLLATE NOCASE OR email=? COLLATE NOCASE LIMIT 1;",
[$id, $id, $id]
);
if ($query === false) {
return false;
}
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Get an item by it's ID.
*/
public static function get_item(int $id): array|false
{
$query = self::db()->query('SELECT * FROM items WHERE id=? LIMIT 1;', [$id]);
if ($query === false) {
return false;
}
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Get a drop by it's ID.
*/
public static function get_drop(int $id): array|false
{
$query = self::db()->query('SELECT * FROM drops WHERE id=? LIMIT 1;', [$id]);
if ($query === false) {
return false;
}
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Get a spell by it's ID.
*/
public static function get_spell(int $id): array|false
{
$query = self::db()->query('SELECT * FROM spells WHERE id=? LIMIT 1;', [$id]);
if ($query === false) {
return false;
}
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Get a monster by it's ID.
*/
public static function get_monster(int $id): array|false
{
$query = self::db()->query('SELECT * FROM monsters WHERE id=? LIMIT 1;', [$id]);
if ($query === false) {
return false;
}
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Translate a Specials keyword to it's string.
*/
public static function special_to_string(string $special): string
{
return match ($special) {
'maxhp' => 'Max HP',
'maxmp' => 'Max MP',
'maxtp' => 'Max TP',
'goldbonus' => 'Gold Bonus (%)',
'expbonus' => 'Experience Bonus (%)',
'strength' => 'Strength',
'dexterity' => 'Dexterity',
'attackpower' => 'Attack Power',
'defensepower' => 'Defense Power',
default => $special
};
}
/**
* Generate a pretty dope token.
*/
public static function token($length = 32): string
{
return bin2hex(random_bytes($length));
}
/**
* Validate any given array of data against rules. Returns [valid, data, error]. Data contains the trimmed
* values from the input array. Note: all fields with rules are assumed to be required, unless the optional
* rule is used.
*
* Example: ['required', 'no-trim', 'length:5-20', 'alphanum-spaces']
*/
public static function validate(array $input_data, array $rules): array
{
$data = [];
$errors = [];
foreach ($rules as $field => $field_rules) {
$value = $input_data[$field] ?? null;
$field_name = ucfirst(str_replace('_', ' ', $field));
$is_required = true;
$default_value = null;
if (in_array('optional', $field_rules)) {
$is_required = false;
}
foreach ($field_rules as $rule) {
if (strpos($rule, 'default:') === 0) {
$default_value = substr($rule, 8);
break;
}
}
if (($value === null || $value === '') && $default_value !== null) {
$value = $default_value;
}
if (($value === null || $value === '') && ! $is_required) {
continue;
}
if ($is_required && ($value === null || $value === '')) {
$errors[$field][] = "{$field_name} is required.";
continue;
}
if (! in_array('no-trim', $field_rules)) {
$value = trim($value);
}
$data[$field] = $value;
foreach ($field_rules as $rule) {
// Parse rule and arguments
if (strpos($rule, ':') !== false) {
list($rule_name, $rule_args) = explode(':', $rule, 2);
} else {
$rule_name = $rule;
$rule_args = null;
}
if ($rule_name === 'optional') {
continue;
}
switch ($rule_name) {
case 'bool':
if (! isset($input_data[$field]) || empty($value)) {
$value = false;
} else {
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if ($value === null) {
$errors[$field][] = "{$field_name} must be a valid boolean value.";
}
}
break;
case 'length':
list($min, $max) = explode('-', $rule_args);
$len = strlen((string) $value);
if ($len < $min || $len > $max) {
$errors[$field][] = "{$field_name} must be between {$min} and {$max} characters.";
}
break;
case 'alphanum':
if (! preg_match('/^[a-zA-Z0-9]+$/', $value)) {
$errors[$field][] = "{$field_name} must contain only letters and numbers.";
}
break;
case 'alpha':
if (! preg_match('/^[a-zA-Z]+$/', $value)) {
$errors[$field][] = "{$field_name} must contain only letters and numbers.";
}
break;
case 'alphanum-spaces':
if (! preg_match('/^[a-zA-Z0-9\s_]+$/', $value)) {
$errors[$field][] = "{$field_name} must contain only letters, numbers, spaces, and underscores.";
}
break;
case 'alpha-spaces':
if (! preg_match('/^[a-zA-Z\s_]+$/', $value)) {
$errors[$field][] = "{$field_name} must contain only letters, numbers, spaces, and underscores.";
}
break;
case 'email':
if (! filter_var($value, FILTER_VALIDATE_EMAIL)) {
$errors[$field][] = "{$field_name} must be a valid email address.";
}
break;
case 'int':
if (filter_var($value, FILTER_VALIDATE_INT) === false) {
$errors[$field][] = "{$field_name} must be an integer.";
}
break;
case 'min':
if ($value < $rule_args) {
$errors[$field][] = "{$field_name} must be at least {$rule_args}.";
}
break;
case 'max':
if ($value > $rule_args) {
$errors[$field][] = "{$field_name} must be no more than {$rule_args}.";
}
break;
case 'regex':
if (! preg_match($rule_args, $value)) {
$errors[$field][] = "{$field_name} does not match the required pattern.";
}
break;
case 'in':
$options = explode(',', $rule_args);
if (! in_array($value, $options)) {
$errors[$field][] = "{$field_name} must be one of: ".implode(', ', $options);
}
break;
case 'confirm':
$field_to_confirm = substr($field, 8);
$confirm_value = $data[$field_to_confirm] ?? '';
$confirm_field_name = ucfirst(str_replace('_', ' ', $field_to_confirm));
if ($value !== $confirm_value) {
$errors[$field][] = "{$field_name} must match {$confirm_field_name}.";
}
break;
case 'unique':
list($table, $column) = explode(',', $rule_args, 2);
if (self::db()->exists($table, $column, $value)) {
$errors[$field][] = "{$field_name} must be unique.";
}
break;
}
}
}
foreach ($input_data as $field => $value) {
if (! isset($data[$field])) {
$data[$field] = trim($value);
}
}
return [
'valid' => empty($errors),
'data' => $data,
'errors' => $errors,
];
}
/**
* Generates a ul list from `validate()`'s errors.
*/
public static function ul_from_validate_errors(array $errors): string
{
$string = '<ul>';
foreach ($errors as $field => $errors) {
$string .= '<li>';
foreach ($errors as $error) {
$string .= $error;
}
$string .= '</li>';
}
return $string.'</ul>';
}
/**
* Load the environment variables from the .env file.
*/
public static function env_load(string $filePath): void
{
if (! file_exists($filePath)) {
throw new \Exception('The .env file does not exist. (el)');
}
$lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$line = trim($line);
// Skip lines that are empty after trimming or are comments
if ($line === '' || str_starts_with($line, '#')) {
continue;
}
// Skip lines without an '=' character
if (strpos($line, '=') === false) {
continue;
}
[$name, $value] = explode('=', $line, 2);
$name = trim($name);
$value = trim($value, " \t\n\r\0\x0B\"'"); // Trim whitespace and quotes
if (! array_key_exists($name, $_SERVER) && ! array_key_exists($name, $_ENV)) {
putenv("$name=$value");
$_ENV[$name] = $value;
$_SERVER[$name] = $value;
}
}
}
/**
* Retrieve an environment variable.
*/
public static function env(string $key, mixed $default = null): mixed
{
$v = $_ENV[$key] ?? $_SERVER[$key] ?? (getenv($key) ?: $default);
return match (true) {
$v === 'true' => true,
$v === 'false' => false,
is_numeric($v) => (int) $v,
is_float($v) => (float) $v,
default => $v
};
}
/**
* Get the data on spells from a given list of IDs.
*/
public static function get_spells_from_list(array|string $spell_ids): array|false
{
if (is_string($spell_ids)) {
$spell_ids = explode(',', $spell_ids);
}
$placeholders = implode(',', array_fill(0, count($spell_ids), '?'));
$query = self::db()->query("SELECT id, name, type FROM spells WHERE id IN($placeholders)", $spell_ids);
if ($query === false) {
return false;
}
$rows = [];
while ($row = $query->fetchArray(SQLITE3_ASSOC)) {
$rows[] = $row;
}
return ! empty($rows) ? $rows : false;
}
public static function generate_stat_bar(int $current, int $max): string
{
$percent = $max > 0 ? round(max(0, $current) / $max * 100, 4) : 0;
if ($percent < 0) {
$percent = 0;
}
if ($percent > 100) {
$percent = 100;
}
$color = $percent >= 66 ? 'green' : ($percent >= 33 ? 'yellow' : 'red');
return <<<HTML
<div class="stat-bar" style="width: 15px; height: 100px; border: solid 1px black;">
<div style="height: $percent%; background-image: url(/img/bars_$color.gif);"></div>
</div>
HTML;
}
public static function create_stat_table(): string
{
$stat_table = '<div class="stat-table">'.
'<div class="stat-row">'.
'<div class="stat-col">'.self::generate_stat_bar((int) self::user()->currenthp, (int) self::user()->maxhp).'<div>HP</div></div>'.
'<div class="stat-col">'.self::generate_stat_bar((int) self::user()->currentmp, (int) self::user()->maxmp).'<div>MP</div></div>'.
'<div class="stat-col">'.self::generate_stat_bar((int) self::user()->currenttp, (int) self::user()->maxtp).'<div>TP</div></div>'.
'</div>'.
'</div>';
return $stat_table;
}
/**
* Returns the user in the GLOBALS state, if there is one. If not, populates it if there is a SESSION user_id.
*/
public static function user(): User|false
{
$GLOBALS['state']['user'] ??= (isset($_SESSION['user_id']) ? User::find($_SESSION['user_id']) : false);
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.
*/
public static 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';
}
/**
* Return whether the request is POST.
*/
public static function is_post(): bool
{
return $_SERVER['REQUEST_METHOD'] === 'POST';
}
/**
* Get the current page title per updates. Optionally set a new title.
*/
public static function page_title(string $new_title = ''): string
{
if ($new_title) {
return $GLOBALS['state']['new-page-title'] = $new_title;
}
return $GLOBALS['state']['new-page-title'] ?? self::env('game_name');
}
/**
* Render the response for the browser based on the request context. The main point is to seperate the handling
* of HTMX responses from normal responses.
*/
public static function render_response(array $uri, string $content): string
{
if ($uri[0] === 'babblebox') {
return $content;
}
if (self::is_htmx()) {
header('HX-Push-Url: '.$_SERVER['REQUEST_URI']);
$content .= '<title>'.self::page_title().'</title>';
$content .= Render::debug_db_info();
if (self::env('debug', false)) {
$content .= Render::debug_query_log();
}
if ($GLOBALS['state']['user-state-changed'] ?? false) {
$content .= Render::right_nav();
$content .= Render::left_nav();
}
}
return Render::content($content, self::page_layout());
}
/**
* Get/set page layout through GLOBALS state.
*/
public static function page_layout(string $layout = ''): string
{
if ($layout === '') {
return $GLOBALS['state']['page-layout'] ?? 'layouts/primary';
}
return $GLOBALS['state']['page-layout'] = $layout;
}
}

View File

@ -13,8 +13,6 @@ declare(strict_types=1);
namespace DragonKnight\Models;
use DragonKnight\Lib;
class Model
{
protected string $table_name = '';
@ -59,7 +57,7 @@ class Model
$values[] = $this->id;
$query = 'UPDATE '.$this->table_name.' SET '.implode(', ', $placeholders).' WHERE id = ?;';
$result = Lib::db()->query($query, $values);
$result = db()->query($query, $values);
return $result === false ? false : true;
}

View File

@ -13,8 +13,6 @@ declare(strict_types=1);
namespace DragonKnight\Models;
use DragonKnight\Lib;
class User extends Model
{
protected string $table_name = 'users';
@ -24,7 +22,7 @@ class User extends Model
*/
public static function find(int|string $id): User|false
{
$query = Lib::db()->query(
$query = db()->query(
'SELECT * FROM users WHERE id=? OR username=? COLLATE NOCASE OR email=? COLLATE NOCASE LIMIT 1;',
[$id, $id, $id]
);
@ -44,7 +42,7 @@ class User extends Model
*/
public function spells(): array|false
{
return Lib::get_spells_from_list($this->spells);
return get_spells_from_list($this->spells);
}
/**
@ -67,7 +65,7 @@ class User extends Model
if ($this->onlinetime && strtotime($this->onlinetime) > strtotime('-9 minutes')) {
return;
}
Lib::db()->query('UPDATE users SET onlinetime=CURRENT_TIMESTAMP WHERE id=?;', [$this->id]);
db()->query('UPDATE users SET onlinetime=CURRENT_TIMESTAMP WHERE id=?;', [$this->id]);
}
/**
@ -105,7 +103,7 @@ class User extends Model
$values[] = $this->id;
$query = 'UPDATE '.$this->table_name.' SET '.implode(', ', $placeholders).' WHERE id = ?;';
$result = Lib::db()->query($query, $values);
$result = db()->query($query, $values);
if ($result === false) {
return false;
}

View File

@ -21,37 +21,37 @@ class Render
{
/**
* Prepare content for final render. If the request is HTMX-based, will return just the content passed to it. Otherwise
* it will Lib::render() onto $layout with some additional bits.
* it will render() onto $layout with some additional bits.
*/
public static function content(string $content, string $layout = 'layouts/primary'): string
{
if (Lib::is_htmx()) {
if (is_htmx()) {
return $content;
}
return Lib::render($layout, ['content' => $content]);
return render($layout, ['content' => $content]);
}
public static function debug_db_info(): string
{
$total_time = round(microtime(true) - START, 4);
$htmx = Lib::is_htmx() ? ' (htmx)' : '';
$htmx = is_htmx() ? ' (htmx)' : '';
return '<div id="debug-db-info" hx-swap-oob="true">'.$total_time.' Seconds, '.Lib::db()->count.' Queries'.$htmx.'</div>';
return '<div id="debug-db-info" hx-swap-oob="true">'.$total_time.' Seconds, '.db()->count.' Queries'.$htmx.'</div>';
}
public static function right_nav(): string
{
if (Lib::user() === false) {
if (user() === false) {
return '';
}
// Flashy numbers if they're low
$hp = (Lib::user()->currenthp <= (Lib::user()->maxhp / 5)) ? '<blink><span class="highlight"><b>*'.Lib::user()->currenthp.'*</b></span></blink>' : Lib::user()->currenthp;
$mp = (Lib::user()->currentmp <= (Lib::user()->maxmp / 5)) ? '<blink><span class="highlight"><b>*'.Lib::user()->currentmp.'*</b></span></blink>' : Lib::user()->currentmp;
$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 = Lib::render('right_nav', ['hp' => $hp, 'mp' => $mp]);
if (Lib::is_htmx()) {
$template = render('right_nav', ['hp' => $hp, 'mp' => $mp]);
if (is_htmx()) {
$template = '<section id="right" hx-swap-oob="true">'.$template.'</section>';
}
@ -60,12 +60,12 @@ class Render
public static function left_nav(): string
{
if (Lib::user() === false) {
if (user() === false) {
return '';
}
$template = Lib::render('left_nav');
if (Lib::is_htmx()) {
$template = render('left_nav');
if (is_htmx()) {
$template = '<section id="left" hx-swap-oob="true">'.$template.'</section>';
}
@ -74,13 +74,13 @@ class Render
public static function babblebox(): string
{
return Lib::render('babblebox', ['messages' => babblebox_messages()]);
return render('babblebox', ['messages' => babblebox_messages()]);
}
public static function debug_query_log(): string
{
$html = '<pre id="debug-query-log" hx-swap-oob="true">';
foreach (Lib::db()->log as $record) {
foreach (db()->log as $record) {
$query_string = str_replace(["\r\n", "\n", "\r"], ' ', $record[0]);
$error_string = ! empty($record[2]) ? '// '.$record[2] : '';
$html .= '<div>['.round($record[1], 2)."s] {$query_string}{$error_string}</div>";

View File

@ -0,0 +1,724 @@
<?php
declare(strict_types=1);
/*
* This file is a part of the Dragon-Knight project.
*
* Copyright (c) 2024-present Sharkk
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE.md file.
*/
namespace DragonKnight;
use DragonKnight\Actions\Explore;
use DragonKnight\Actions\Towns;
use DragonKnight\Models\User;
function index(): string|false
{
$page = false;
if (user()->currentaction === 'In Town') {
$page = Towns::town();
} elseif (user()->currentaction === 'Exploring') {
$page = Explore::explore();
} elseif (user()->currentaction === 'Fighting') {
redirect('/fight');
}
return $page;
}
/**
* Show the user their position on the current world map. Only works with a game size of 250 and the default towns 😅.
*/
function show_map()
{
$pos = sprintf(
'<div style="position: absolute; width: 5px; height: 5px; border-radius: 1000px; border: solid 1px black; background-color: red; left: %dpx; top: %dpx;"></div>',
round(258 + user()->longitude * (500 / 500) - 3),
round(258 - user()->latitude * (500 / 500) - 3)
);
echo render('layouts/minimal', [
'content' => '<img src="/img/map.gif" alt="Map">'.$pos,
'title' => 'Map',
]);
}
/**
* Show a character's info. Defaults to the currently logged in user.
*/
function show_character_info(int $id = 0): string
{
$user = $id !== 0 ? User::find($id) : user();
if ($user === false) {
exit('Failed to show info for user ID '.$id);
}
$level = db()->query("SELECT `{$user->charclass}_exp` FROM levels WHERE id=? LIMIT 1;", [$user->level + 1])->fetchArray(SQLITE3_ASSOC);
$spells = $user->spells();
$magic_list = 'None';
if (! empty($spells)) {
$magic_list = '';
foreach ($spells as $spell) {
$magic_list .= $spell['name'].'<br>';
}
}
$showchar = render('show_char', [
'char' => $user,
'level' => $level,
'magic_list' => $magic_list,
]);
return render('layouts/minimal', ['content' => $showchar, 'title' => $user->username.' Information']);
}
/**
* Handle a POST request to send a new babblebox message.
*/
function babblebox()
{
if (is_post()) {
$content = trim($_POST['babble']);
if (! empty($content)) {
db()->query(
'INSERT INTO babble (posttime, author, babble) VALUES (CURRENT_TIMESTAMP, ?, ?);',
[user()->username, $content]
);
}
return babblebox_messages();
}
}
/**
* The handler that is polled by HTMX for new babblebox 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;
}
/**
* Open or get SQLite database connection.
*/
function db(): Database
{
if (! is_dir($path = getcwd().'/db')) {
error_log('Database folder not found at '.$path.'. Please run the installer first.');
exit();
}
return $GLOBALS['database'] ??= new Database(getcwd().'/db/database.db');
}
/**
* Redirect to a different URL, exit.
*/
function redirect(string $location): void
{
if (is_htmx()) {
$target = isset($_SERVER['HTTP_HX_TARGET']) ? '#'.$_SERVER['HTTP_HX_TARGET'] : '#middle';
$json = json_encode(['path' => $location, 'target' => $target]);
header("HX-Location: $json");
} else {
header("Location: $location");
}
exit;
}
/**
* Render a view with the given data. Can be used redundantly within the template.
*/
function render(string $path_to_base_view, array $data = []): string|false
{
ob_start();
extract($data);
require __DIR__."/../templates/$path_to_base_view.php";
return ob_get_clean();
}
/**
* Replace tags with given content.
*/
function parse(string $template, array $array): string
{
return strtr($template, array_combine(
array_map(fn ($key) => "{{{$key}}}", array_keys($array)),
array_values($array)
));
}
/**
* Change the SQLite3 datetime format (YYYY-MM-DD HH:MM:SS) into something friendlier.
*/
function pretty_date(string $uglydate): string
{
return date('l, F j, Y', mktime(
0,
0,
0,
(int) substr($uglydate, 5, 2), // Month
(int) substr($uglydate, 8, 2), // Day
(int) substr($uglydate, 0, 4) // Year
));
}
/**
* Use htmlentities with UTF-8 encoding to ensure we're only outputting healthy, safe and effective HTML.
*/
function make_safe(string $content): string
{
return htmlentities($content, ENT_QUOTES, 'UTF-8');
}
/**
* Finalize admin page and output to browser.
*/
function display_admin($content, $title)
{
echo render('layouts/admin', [
'title' => $title,
'content' => $content,
]);
exit;
}
/**
* Determine what game skin to use. If a user is logged in then it uses their setting, otherwise defaults to 0 (retro).
*/
function game_skin(): int
{
return user() !== false ? user()->game_skin : 0;
}
/**
* Get a town's data by it's coordinates.
*/
function get_town_by_xy(int $x, int $y): array|false
{
$cache_tag = "town-$x-$y";
if (! isset($GLOBALS['cache'][$cache_tag])) {
$query = db()->query('SELECT * FROM towns WHERE longitude = ? AND latitude = ? LIMIT 1;', [$x, $y]);
if ($query === false) {
return false;
}
$GLOBALS['cache'][$cache_tag] = $query->fetchArray(SQLITE3_ASSOC);
}
return $GLOBALS['cache'][$cache_tag];
}
/**
* Get a town's data by it's ID.
*/
function get_town_by_id(int $id): array|false
{
$query = db()->query('SELECT * FROM towns WHERE id = ? LIMIT 1;', [$id]);
if ($query === false) {
return false;
}
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Get a user's data by their ID, username or email.
*/
function get_user(int|string $id, string $data = '*'): array|false
{
$query = db()->query(
"SELECT $data FROM users WHERE id=? OR username=? COLLATE NOCASE OR email=? COLLATE NOCASE LIMIT 1;",
[$id, $id, $id]
);
if ($query === false) {
return false;
}
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Get an item by it's ID.
*/
function get_item(int $id): array|false
{
$query = db()->query('SELECT * FROM items WHERE id=? LIMIT 1;', [$id]);
if ($query === false) {
return false;
}
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Get a drop by it's ID.
*/
function get_drop(int $id): array|false
{
$query = db()->query('SELECT * FROM drops WHERE id=? LIMIT 1;', [$id]);
if ($query === false) {
return false;
}
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Get a spell by it's ID.
*/
function get_spell(int $id): array|false
{
$query = db()->query('SELECT * FROM spells WHERE id=? LIMIT 1;', [$id]);
if ($query === false) {
return false;
}
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Get a monster by it's ID.
*/
function get_monster(int $id): array|false
{
$query = db()->query('SELECT * FROM monsters WHERE id=? LIMIT 1;', [$id]);
if ($query === false) {
return false;
}
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Translate a Specials keyword to it's string.
*/
function special_to_string(string $special): string
{
return match ($special) {
'maxhp' => 'Max HP',
'maxmp' => 'Max MP',
'maxtp' => 'Max TP',
'goldbonus' => 'Gold Bonus (%)',
'expbonus' => 'Experience Bonus (%)',
'strength' => 'Strength',
'dexterity' => 'Dexterity',
'attackpower' => 'Attack Power',
'defensepower' => 'Defense Power',
default => $special
};
}
/**
* Generate a pretty dope token.
*/
function token($length = 32): string
{
return bin2hex(random_bytes($length));
}
/**
* Validate any given array of data against rules. Returns [valid, data, error]. Data contains the trimmed
* values from the input array. Note: all fields with rules are assumed to be required, unless the optional
* rule is used.
*
* Example: ['required', 'no-trim', 'length:5-20', 'alphanum-spaces']
*/
function validate(array $input_data, array $rules): array
{
$data = [];
$errors = [];
foreach ($rules as $field => $field_rules) {
$value = $input_data[$field] ?? null;
$field_name = ucfirst(str_replace('_', ' ', $field));
$is_required = true;
$default_value = null;
if (in_array('optional', $field_rules)) {
$is_required = false;
}
foreach ($field_rules as $rule) {
if (strpos($rule, 'default:') === 0) {
$default_value = substr($rule, 8);
break;
}
}
if (($value === null || $value === '') && $default_value !== null) {
$value = $default_value;
}
if (($value === null || $value === '') && ! $is_required) {
continue;
}
if ($is_required && ($value === null || $value === '')) {
$errors[$field][] = "{$field_name} is required.";
continue;
}
if (! in_array('no-trim', $field_rules)) {
$value = trim($value);
}
$data[$field] = $value;
foreach ($field_rules as $rule) {
// Parse rule and arguments
if (strpos($rule, ':') !== false) {
list($rule_name, $rule_args) = explode(':', $rule, 2);
} else {
$rule_name = $rule;
$rule_args = null;
}
if ($rule_name === 'optional') {
continue;
}
switch ($rule_name) {
case 'bool':
if (! isset($input_data[$field]) || empty($value)) {
$value = false;
} else {
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if ($value === null) {
$errors[$field][] = "{$field_name} must be a valid boolean value.";
}
}
break;
case 'length':
list($min, $max) = explode('-', $rule_args);
$len = strlen((string) $value);
if ($len < $min || $len > $max) {
$errors[$field][] = "{$field_name} must be between {$min} and {$max} characters.";
}
break;
case 'alphanum':
if (! preg_match('/^[a-zA-Z0-9]+$/', $value)) {
$errors[$field][] = "{$field_name} must contain only letters and numbers.";
}
break;
case 'alpha':
if (! preg_match('/^[a-zA-Z]+$/', $value)) {
$errors[$field][] = "{$field_name} must contain only letters and numbers.";
}
break;
case 'alphanum-spaces':
if (! preg_match('/^[a-zA-Z0-9\s_]+$/', $value)) {
$errors[$field][] = "{$field_name} must contain only letters, numbers, spaces, and underscores.";
}
break;
case 'alpha-spaces':
if (! preg_match('/^[a-zA-Z\s_]+$/', $value)) {
$errors[$field][] = "{$field_name} must contain only letters, numbers, spaces, and underscores.";
}
break;
case 'email':
if (! filter_var($value, FILTER_VALIDATE_EMAIL)) {
$errors[$field][] = "{$field_name} must be a valid email address.";
}
break;
case 'int':
if (filter_var($value, FILTER_VALIDATE_INT) === false) {
$errors[$field][] = "{$field_name} must be an integer.";
}
break;
case 'min':
if ($value < $rule_args) {
$errors[$field][] = "{$field_name} must be at least {$rule_args}.";
}
break;
case 'max':
if ($value > $rule_args) {
$errors[$field][] = "{$field_name} must be no more than {$rule_args}.";
}
break;
case 'regex':
if (! preg_match($rule_args, $value)) {
$errors[$field][] = "{$field_name} does not match the required pattern.";
}
break;
case 'in':
$options = explode(',', $rule_args);
if (! in_array($value, $options)) {
$errors[$field][] = "{$field_name} must be one of: ".implode(', ', $options);
}
break;
case 'confirm':
$field_to_confirm = substr($field, 8);
$confirm_value = $data[$field_to_confirm] ?? '';
$confirm_field_name = ucfirst(str_replace('_', ' ', $field_to_confirm));
if ($value !== $confirm_value) {
$errors[$field][] = "{$field_name} must match {$confirm_field_name}.";
}
break;
case 'unique':
list($table, $column) = explode(',', $rule_args, 2);
if (db()->exists($table, $column, $value)) {
$errors[$field][] = "{$field_name} must be unique.";
}
break;
}
}
}
foreach ($input_data as $field => $value) {
if (! isset($data[$field])) {
$data[$field] = trim($value);
}
}
return [
'valid' => empty($errors),
'data' => $data,
'errors' => $errors,
];
}
/**
* Generates a ul list from `validate()`'s errors.
*/
function ul_from_validate_errors(array $errors): string
{
$string = '<ul>';
foreach ($errors as $field => $errors) {
$string .= '<li>';
foreach ($errors as $error) {
$string .= $error;
}
$string .= '</li>';
}
return $string.'</ul>';
}
/**
* Load the environment variables from the .env file.
*/
function env_load(string $filePath): void
{
if (! file_exists($filePath)) {
throw new \Exception('The .env file does not exist. (el)');
}
$lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$line = trim($line);
// Skip lines that are empty after trimming or are comments
if ($line === '' || str_starts_with($line, '#')) {
continue;
}
// Skip lines without an '=' character
if (strpos($line, '=') === false) {
continue;
}
[$name, $value] = explode('=', $line, 2);
$name = trim($name);
$value = trim($value, " \t\n\r\0\x0B\"'"); // Trim whitespace and quotes
if (! array_key_exists($name, $_SERVER) && ! array_key_exists($name, $_ENV)) {
putenv("$name=$value");
$_ENV[$name] = $value;
$_SERVER[$name] = $value;
}
}
}
/**
* Retrieve an environment variable.
*/
function env(string $key, mixed $default = null): mixed
{
$v = $_ENV[$key] ?? $_SERVER[$key] ?? (getenv($key) ?: $default);
return match (true) {
$v === 'true' => true,
$v === 'false' => false,
is_numeric($v) => (int) $v,
is_float($v) => (float) $v,
default => $v
};
}
/**
* Get the data on spells from a given list of IDs.
*/
function get_spells_from_list(array|string $spell_ids): array|false
{
if (is_string($spell_ids)) {
$spell_ids = explode(',', $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);
if ($query === false) {
return false;
}
$rows = [];
while ($row = $query->fetchArray(SQLITE3_ASSOC)) {
$rows[] = $row;
}
return ! empty($rows) ? $rows : false;
}
function generate_stat_bar(int $current, int $max): string
{
$percent = $max > 0 ? round(max(0, $current) / $max * 100, 4) : 0;
if ($percent < 0) {
$percent = 0;
}
if ($percent > 100) {
$percent = 100;
}
$color = $percent >= 66 ? 'green' : ($percent >= 33 ? 'yellow' : 'red');
return <<<HTML
<div class="stat-bar" style="width: 15px; height: 100px; border: solid 1px black;">
<div style="height: $percent%; background-image: url(/img/bars_$color.gif);"></div>
</div>
HTML;
}
function create_stat_table(): string
{
$stat_table = '<div class="stat-table">'.
'<div class="stat-row">'.
'<div class="stat-col">'.generate_stat_bar((int) user()->currenthp, (int) user()->maxhp).'<div>HP</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((int) user()->currenttp, (int) user()->maxtp).'<div>TP</div></div>'.
'</div>'.
'</div>';
return $stat_table;
}
/**
* Returns the user in the GLOBALS state, if there is one. If not, populates it if there is a SESSION user_id.
*/
function user(): User|false
{
$GLOBALS['state']['user'] ??= (isset($_SESSION['user_id']) ? User::find($_SESSION['user_id']) : false);
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';
}
/**
* Return whether the request is POST.
*/
function is_post(): bool
{
return $_SERVER['REQUEST_METHOD'] === 'POST';
}
/**
* Get the current page title per updates. Optionally set a new title.
*/
function page_title(string $new_title = ''): string
{
if ($new_title) {
return $GLOBALS['state']['new-page-title'] = $new_title;
}
return $GLOBALS['state']['new-page-title'] ?? env('game_name');
}
/**
* Render the response for the browser based on the request context. The main point is to seperate the handling
* of HTMX responses from normal responses.
*/
function render_response(array $uri, string $content): string
{
if ($uri[0] === 'babblebox') {
return $content;
}
if (is_htmx()) {
header('HX-Push-Url: '.$_SERVER['REQUEST_URI']);
$content .= '<title>'.page_title().'</title>';
$content .= Render::debug_db_info();
if (env('debug', false)) {
$content .= Render::debug_query_log();
}
if ($GLOBALS['state']['user-state-changed'] ?? false) {
$content .= Render::right_nav();
$content .= Render::left_nav();
}
}
return Render::content($content, page_layout());
}
/**
* Get/set page layout through GLOBALS state.
*/
function page_layout(string $layout = ''): string
{
if ($layout === '') {
return $GLOBALS['state']['page-layout'] ?? 'layouts/primary';
}
return $GLOBALS['state']['page-layout'] = $layout;
}

View File

@ -15,7 +15,7 @@ Experience values for each level should be the cumulative total amount of experi
<tr><td colspan="2"></td></tr>
<?php foreach ([1, 2, 3] as $n): ?>
<?php $class_name = DragonKnight\Lib::env("class_{$n}_name"); ?>
<?php $class_name = DragonKnight\env("class_{$n}_name"); ?>
<tr><td><?php echo $class_name ?> EXP</td> <td><input type="number" name="<?php echo $n ?>_exp" value="<?php echo $level["{$n}_exp"] ?>"></td></tr>
<tr><td><?php echo $class_name ?> HP</td> <td><input type="number" name="<?php echo $n ?>_hp" value="<?php echo $level["{$n}_hp"] ?>"></td></tr>
<tr><td><?php echo $class_name ?> MP</td> <td><input type="number" name="<?php echo $n ?>_mp" value="<?php echo $level["{$n}_mp"] ?>"></td></tr>

View File

@ -23,9 +23,9 @@
<tr><td>Longitude</td><td><input type="number" name="longitude" value="<?php echo $user['longitude'] ?>" /></td></tr>
<tr><td>Character Class</td><td>
<select name="charclass">
<option value="1" <?php echo $user['charclass'] == 1 ? 'selected' : '' ?>><?php echo DragonKnight\Lib::env('class_1_name') ?></option>
<option value="2" <?php echo $user['charclass'] == 2 ? 'selected' : '' ?>><?php echo DragonKnight\Lib::env('class_2_name') ?></option>
<option value="3" <?php echo $user['charclass'] == 3 ? 'selected' : '' ?>><?php echo DragonKnight\Lib::env('class_3_name') ?></option>
<option value="1" <?php echo $user['charclass'] == 1 ? 'selected' : '' ?>><?php echo DragonKnight\env('class_1_name') ?></option>
<option value="2" <?php echo $user['charclass'] == 2 ? 'selected' : '' ?>><?php echo DragonKnight\env('class_2_name') ?></option>
<option value="3" <?php echo $user['charclass'] == 3 ? 'selected' : '' ?>><?php echo DragonKnight\env('class_3_name') ?></option>
</select>
</td></tr>

View File

@ -8,8 +8,8 @@
<td width="20%"><span class="highlight">Game Open:</span></td>
<td>
<select name="gameopen">
<option value="1" <?php echo DragonKnight\Lib::env('game_open') ? 'selected' : '' ?>>Open</option>
<option value="0" <?php echo ! DragonKnight\Lib::env('game_open') ? 'selected' : '' ?>>Closed</option>
<option value="1" <?php echo DragonKnight\env('game_open') ? 'selected' : '' ?>>Open</option>
<option value="0" <?php echo ! DragonKnight\env('game_open') ? 'selected' : '' ?>>Closed</option>
</select><br>
<span class="small">Close the game if you are upgrading or working on settings and don't want to
cause odd errors for end-users. Closing the game will completely halt all activity.</span>
@ -18,14 +18,14 @@
<tr>
<td width="20%">Game Name:</td>
<td>
<input type="text" name="gamename" value="<?php echo DragonKnight\Lib::env('game_name') ?>"><br>
<input type="text" name="gamename" value="<?php echo DragonKnight\env('game_name') ?>"><br>
<span class="small">Change this if you want to change to call your game something different.</span>
</td>
</tr>
<tr>
<td width="20%">Game URL:</td>
<td>
<input type="text" name="gameurl" value="<?php echo DragonKnight\Lib::env('game_url') ?>"><br>
<input type="text" name="gameurl" value="<?php echo DragonKnight\env('game_url') ?>"><br>
<span class="small">Please specify the full URL to your game installation
("https://www.dragonknight.com/"). This gets used in the registration email sent to users. If
you leave this field blank or incorrect, users may not be able to register correctly.</span>
@ -34,7 +34,7 @@
<tr>
<td width="20%">Admin Email:</td>
<td>
<input type="text" name="adminemail" value="<?php echo DragonKnight\Lib::env('admin_email') ?>"><br>
<input type="text" name="adminemail" value="<?php echo DragonKnight\env('admin_email') ?>"><br>
<span class="small">Please specify your email address. This gets used when the game has to send an
email to users.</span>
</td>
@ -42,7 +42,7 @@
<tr>
<td width="20%">Map Size:</td>
<td>
<input type="number" name="gamesize" value="<?php echo DragonKnight\Lib::env('game_size') ?>"><br>
<input type="number" name="gamesize" value="<?php echo DragonKnight\env('game_size') ?>"><br>
<span class="small">
Default is 250. This is the size of each map quadrant. Note that monster
levels increase every 5 spaces, so you should ensure that you have at least (map size / 5)
@ -55,8 +55,8 @@
<td width="20%">Email Verification:</td>
<td>
<select name="verifyemail">
<option value="0" <?php echo ! DragonKnight\Lib::env('verify_email') ? 'selected' : '' ?>>Disabled</option>
<option value="1" <?php echo DragonKnight\Lib::env('verify_email') ? 'selected' : '' ?>>Enabled</option>
<option value="0" <?php echo ! DragonKnight\env('verify_email') ? 'selected' : '' ?>>Disabled</option>
<option value="1" <?php echo DragonKnight\env('verify_email') ? 'selected' : '' ?>>Enabled</option>
</select><br>
<span class="small">Make users verify their email address for added security.</span>
</td>
@ -65,8 +65,8 @@
<td width="20%">Show News:</td>
<td>
<select name="shownews">
<option value="0" <?php echo ! DragonKnight\Lib::env('show_news') ? 'selected' : '' ?>>No</option>
<option value="1" <?php echo DragonKnight\Lib::env('show_news') ? 'selected' : '' ?>>Yes</option>
<option value="0" <?php echo ! DragonKnight\env('show_news') ? 'selected' : '' ?>>No</option>
<option value="1" <?php echo DragonKnight\env('show_news') ? 'selected' : '' ?>>Yes</option>
</select><br>
<span class="small">Toggle display of the Latest News box in towns.
</td>
@ -75,8 +75,8 @@
<td width="20%">Show Who's Online:</td>
<td>
<select name="showonline">
<option value="0" <?php echo ! DragonKnight\Lib::env('show_online') ? 'selected' : '' ?>>No</option>
<option value="1" <?php echo DragonKnight\Lib::env('show_online') ? 'selected' : '' ?>>Yes</option>
<option value="0" <?php echo ! DragonKnight\env('show_online') ? 'selected' : '' ?>>No</option>
<option value="1" <?php echo DragonKnight\env('show_online') ? 'selected' : '' ?>>Yes</option>
</select><br>
<span class="small">Toggle display of the Who's Online box in towns.</span>
</td>
@ -85,23 +85,23 @@
<td width="20%">Show Babblebox:</td>
<td>
<select name="showbabble">
<option value="0" <?php echo ! DragonKnight\Lib::env('show_babble') ? 'selected' : '' ?>>No</option>
<option value="1" <?php echo DragonKnight\Lib::env('show_babble') ? 'selected' : '' ?>>Yes</option>
<option value="0" <?php echo ! DragonKnight\env('show_babble') ? 'selected' : '' ?>>No</option>
<option value="1" <?php echo DragonKnight\env('show_babble') ? 'selected' : '' ?>>Yes</option>
</select><br>
<span class="small">Toggle display of the Babble Box in towns.</span>
</td>
</tr>
<tr>
<td width="20%">Class 1 Name:</td>
<td><input type="text" name="class1name" value="<?php echo DragonKnight\Lib::env('class_1_name') ?>"><br></td>
<td><input type="text" name="class1name" value="<?php echo DragonKnight\env('class_1_name') ?>"><br></td>
</tr>
<tr>
<td width="20%">Class 2 Name:</td>
<td><input type="text" name="class2name" value="<?php echo DragonKnight\Lib::env('class_2_name') ?>"><br></td>
<td><input type="text" name="class2name" value="<?php echo DragonKnight\env('class_2_name') ?>"><br></td>
</tr>
<tr>
<td width="20%">Class 3 Name:</td>
<td><input type="text" name="class3name" value="<?php echo DragonKnight\Lib::env('class_3_name') ?>"><br></td>
<td><input type="text" name="class3name" value="<?php echo DragonKnight\env('class_3_name') ?>"><br></td>
</tr>
</table>

View File

@ -3,14 +3,14 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo DragonKnight\Lib::page_title() ?></title>
<title><?php echo DragonKnight\page_title() ?></title>
<link rel="stylesheet" href="/css/admin.css">
<script src="/js/htmx.js"></script>
</head>
<body>
<div id="admin-container">
<header>
<h1><?php echo DragonKnight\Lib::env('game_name') ?></h1>
<h1><?php echo DragonKnight\env('game_name') ?></h1>
<h3>Admin</h3>
</header>
<main>
@ -44,7 +44,7 @@
<div>Version <?php echo VERSION ?> <?php echo BUILD ?></div>
</footer>
<?php if (DragonKnight\Lib::env('debug', false)) {
<?php if (DragonKnight\env('debug', false)) {
echo DragonKnight\Render::debug_query_log();
} ?>
</div>

View File

@ -3,12 +3,12 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo DragonKnight\Lib::env('game_name', 'Dragon Knight') ?> Help</title>
<title><?php echo DragonKnight\env('game_name', 'Dragon Knight') ?> Help</title>
<link rel="stylesheet" href="/css/help.css">
</head>
<body>
<a name="top"></a>
<h1><?php echo DragonKnight\Lib::env('game_name', 'Dragon Knight') ?> Help</h1>
<h1><?php echo DragonKnight\env('game_name', 'Dragon Knight') ?> Help</h1>
[ <a href="/help">Back to Help</a> ]<br>
[ <a href="/">Return to Game</a> ]

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo DragonKnight\Lib::page_title() ?></title>
<title><?php echo DragonKnight\page_title() ?></title>
<link rel="stylesheet" href="/css/dk.css">
<script src="/js/htmx.js"></script>
@ -20,12 +20,12 @@
}
</script>
</head>
<body class="skin-<?php echo DragonKnight\Lib::game_skin() ?>">
<body class="skin-<?php echo DragonKnight\game_skin() ?>">
<div id="game-container">
<header>
<a href="/"><img id="logo" src="/img/logo.gif" alt="<?php echo DragonKnight\Lib::env('game_name', 'Dragon Knight') ?>" title="<?php echo DragonKnight\Lib::env('game_name', 'Dragon Knight') ?>"></a>
<a href="/"><img id="logo" src="/img/logo.gif" alt="<?php echo DragonKnight\env('game_name', 'Dragon Knight') ?>" title="<?php echo DragonKnight\env('game_name', 'Dragon Knight') ?>"></a>
<nav>
<?php if (DragonKnight\Lib::user() !== false): ?>
<?php if (DragonKnight\user() !== false): ?>
<a href='/logout'><img src='/img/button_logout.gif' alt='Log Out' title='Log Out'></a>
<?php else: ?>
<a href='/login'><img src='/img/button_login.gif' alt='Log In' title='Log In'></a>
@ -48,7 +48,7 @@
<div>Version <?php echo VERSION ?> <?php echo BUILD ?></div>
</footer>
<?php if (DragonKnight\Lib::env('debug', false)) {
<?php if (DragonKnight\env('debug', false)) {
echo DragonKnight\Render::debug_query_log();
} ?>
</div>

View File

@ -1,9 +1,9 @@
<section>
<div class="title"><img src="/img/button_location.gif" alt="Location" title="Location"></div>
Currently: <?php echo DragonKnight\Lib::user()->currentaction ?><br>
Currently: <?php echo DragonKnight\user()->currentaction ?><br>
<?php
$lat = DragonKnight\Lib::user()->latitude;
$lon = DragonKnight\Lib::user()->longitude;
$lat = DragonKnight\user()->latitude;
$lon = DragonKnight\user()->longitude;
if ($lat < 0) {
$lat = ($lat * -1).'S';
} else {
@ -31,15 +31,15 @@
<section>
<div class="title"><img src="/img/button_towns.gif" alt="Towns" title="Towns"></div>
<?php
if (DragonKnight\Lib::user()->currentaction == 'In Town') {
$town = DragonKnight\Lib::get_town_by_xy((int) DragonKnight\Lib::user()->latitude, (int) DragonKnight\Lib::user()->longitude);
if (DragonKnight\user()->currentaction == 'In Town') {
$town = DragonKnight\get_town_by_xy((int) DragonKnight\user()->latitude, (int) DragonKnight\user()->longitude);
echo "Welcome to <b>{$town['name']}</b>.<br><br>";
}
?>
Travel To:<br>
<?php
$town_list = explode(',', DragonKnight\Lib::user()->towns);
$towns = DragonKnight\Lib::db()->query('SELECT * FROM towns ORDER BY id;');
$town_list = explode(',', DragonKnight\user()->towns);
$towns = DragonKnight\db()->query('SELECT * FROM towns ORDER BY id;');
$mapped = false;
while ($row = $towns->fetchArray(SQLITE3_ASSOC)) {
$mapped = true;
@ -60,7 +60,7 @@
<a href="/" hx-get="/" hx-target="#middle">Home</a><br>
<a href="/forum" hx-get="/forum" hx-target="#middle">Forum</a><br>
<a href="/settings">Settings</a><br>
<?php if (DragonKnight\Lib::user()->authlevel === 1): ?>
<?php if (DragonKnight\user()->authlevel === 1): ?>
<a href="/admin">Admin</a><br>
<?php endif; ?>
<a href="/help">Help</a><br>

View File

@ -9,9 +9,9 @@
<td>Character Class:</td>
<td>
<select name="charclass">
<option value="1"><?php echo DragonKnight\Lib::env('class_1_name') ?></option>
<option value="2"><?php echo DragonKnight\Lib::env('class_2_name') ?></option>
<option value="3"><?php echo DragonKnight\Lib::env('class_3_name') ?></option>
<option value="1"><?php echo DragonKnight\env('class_1_name') ?></option>
<option value="2"><?php echo DragonKnight\env('class_2_name') ?></option>
<option value="3"><?php echo DragonKnight\env('class_3_name') ?></option>
</select>
</td>
</tr>

View File

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

View File

@ -2,9 +2,9 @@
<b><?php echo $char->username ?></b><br><br>
Class: <?php echo match ($char->charclass) {
1 => DragonKnight\Lib::env('class_1_name'),
2 => DragonKnight\Lib::env('class_2_name'),
3 => DragonKnight\Lib::env('class_3_name')
1 => DragonKnight\env('class_1_name'),
2 => DragonKnight\env('class_2_name'),
3 => DragonKnight\env('class_3_name')
}; ?><br><br>
Level: <?php echo $char->level ?><br>