Class refactor, move char bar

This commit is contained in:
Sky Johnson 2024-10-23 17:39:48 -05:00
parent cee92173a4
commit d3eeaf6ced
31 changed files with 726 additions and 546 deletions

File diff suppressed because one or more lines are too long

View File

@ -10,6 +10,7 @@ body {
background-position: center top; background-position: center top;
background-repeat: no-repeat; background-repeat: no-repeat;
max-width: 1640px; max-width: 1640px;
min-width: 968px;
margin: 0px auto; margin: 0px auto;
font-family: var(--main-font); font-family: var(--main-font);
} }
@ -41,7 +42,6 @@ header#main-header {
main { main {
padding: 1rem; padding: 1rem;
width: 100%; width: 100%;
min-width: 968px;
display: flex; display: flex;
gap: 2rem; gap: 2rem;
@ -208,30 +208,14 @@ footer {
} }
} }
#char-bar { #character {
& > .name {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-around;
padding: 0 1rem;
height: 34px;
color: white;
gap: 1rem;
background-image: url('/assets/img/bar.jpg');
& > div.container {
display: flex;
align-items: center;
gap: 1rem;
& > div {
display: flex;
align-items: center;
.icon {
width: 18px;
margin-right: 0.5rem;
}
} }
& > div:not(:last-child) {
margin-bottom: 0.5rem;
} }
} }
@ -311,10 +295,14 @@ span.badge {
} }
.debug-query-log { .debug-query-log {
padding: 2rem; padding: 1rem;
font-size: 14px; font-size: 14px;
color: #666; color: #666;
font-family: monospace; font-family: monospace;
&:last-child {
padding-top: 0;
}
} }
#center > section { #center > section {

View File

@ -57,6 +57,23 @@ router_get($r, '/settings', 'settings_controller_get');
*/ */
router_get($r, '/auctions', 'auctions_controller_get'); router_get($r, '/auctions', 'auctions_controller_get');
/*
Testing
*/
if (env('debug')) {
router_get($r, '/give_silver/:x', function (int $amt) {
auth_only_and_must_have_character();
wallet()->give(Currency::Silver, $amt);
redirect('/');
});
router_get($r, '/take_silver/:x', function (int $amt) {
auth_only_and_must_have_character();
wallet()->take(Currency::Silver, $amt);
redirect('/');
});
}
/* /*
Router Router
*/ */

View File

@ -4,6 +4,12 @@ session_start();
// SRC is defined as the path to the src/ directory from public/ // SRC is defined as the path to the src/ directory from public/
define('CLASS_MAP', [
'User' => '/models/user.php',
'Character' => '/models/character.php',
'Wallet' => '/models/wallet.php'
]);
// Source libraries // Source libraries
require_once SRC . '/helpers.php'; require_once SRC . '/helpers.php';
require_once SRC . '/util/env.php'; require_once SRC . '/util/env.php';
@ -12,12 +18,11 @@ require_once SRC . '/util/auth.php';
require_once SRC . '/util/router.php'; require_once SRC . '/util/router.php';
require_once SRC . '/util/components.php'; require_once SRC . '/util/components.php';
require_once SRC . '/util/render.php'; require_once SRC . '/util/render.php';
require_once SRC . '/util/enums.php';
// Database models // Database models
require_once SRC . '/model/user.php'; require_once SRC . '/models/session.php';
require_once SRC . '/model/session.php'; require_once SRC . '/models/token.php';
require_once SRC . '/model/token.php';
require_once SRC . '/model/char.php';
// Controllers // Controllers
require_once SRC . '/controller/char.php'; require_once SRC . '/controller/char.php';
@ -27,6 +32,10 @@ require_once SRC . '/controller/settings.php';
require_once SRC . '/controller/auctions.php'; require_once SRC . '/controller/auctions.php';
require_once SRC . '/controller/profile.php'; require_once SRC . '/controller/profile.php';
spl_autoload_register(function (string $class) {
if (array_key_exists($class, CLASS_MAP)) require_once SRC . CLASS_MAP[$class];
});
// Track the start time of the request // Track the start time of the request
define('START_TIME', microtime(true)); define('START_TIME', microtime(true));

View File

@ -53,14 +53,14 @@ function auth_controller_register_post()
/* /*
A username must be unique. A username must be unique.
*/ */
if (auth_username_exists($u)) { if (User::username_exists($u)) {
$errors['u'][] = 'Username is already taken.'; $errors['u'][] = 'Username is already taken.';
} }
/* /*
An email must be unique. An email must be unique.
*/ */
if (auth_email_exists($e)) { if (User::email_exists($e)) {
$errors['e'][] = 'Email is already taken.'; $errors['e'][] = 'Email is already taken.';
} }
@ -71,11 +71,10 @@ function auth_controller_register_post()
exit; exit;
} }
$user = user_create($u, $e, $p); if (User::create($u, $e, $p) === false) router_error(400);
if ($user === false) router_error(400);
$_SESSION['user'] = user_find($u); $_SESSION['user'] = serialize(User::find($u));
wallet_create($_SESSION['user']['id']); Wallet::create(user()->id);
redirect('/character/create-first'); redirect('/character/create-first');
} }
@ -111,31 +110,32 @@ function auth_controller_login_post()
exit; exit;
} }
$user = user_find($u); $user = User::find($u);
if ($user === false || !password_verify($p, $user['password'])) { if ($user === false || !$user->check_password($p)) {
$errors['x'][] = 'Invalid username or password.'; $errors['x'][] = 'Invalid username or password.';
$GLOBALS['form-errors'] = $errors; $GLOBALS['form-errors'] = $errors;
echo render('layouts/basic', ['view' => 'pages/auth/login']); echo render('layouts/basic', ['view' => 'pages/auth/login']);
exit; exit;
} }
$_SESSION['user'] = $user; $_SESSION['user'] = serialize($user);
if ($_POST['remember'] ?? false) { if ($_POST['remember'] ?? false) {
$token = token(); $token = token();
$expires = strtotime('+30 days'); $expires = strtotime('+30 days');
$result = db_query(db_auth(), "INSERT INTO sessions (token, user_id, expires) VALUES (:t, :u, :e)", [ $result = db_query(
':t' => $token, db_auth(),
':u' => $_SESSION['user']['id'], "INSERT INTO sessions (token, user_id, expires) VALUES (:t, :u, :e)",
':e' => $expires [':t' => $token, ':u' => user()->id, ':e' => $expires]
]); );
if (!$result) router_error(400); if (!$result) router_error(400);
set_cookie('remember_me', $token, $expires); set_cookie('remember_me', $token, $expires);
} }
if (char_count($_SESSION['user']['id']) === 0) { if (user()->char_count() === 0) {
redirect('/character/create-first'); redirect('/character/create-first');
} elseif (!change_user_character($_SESSION['user']['char_id'])) { } elseif (!change_user_character(user()->char_id)) {
echo "failed to change user character (aclp)";
router_error(999); router_error(999);
} }
@ -148,9 +148,8 @@ function auth_controller_login_post()
function auth_controller_logout_post() function auth_controller_logout_post()
{ {
csrf_ensure(); csrf_ensure();
session_delete($_SESSION['user']['id']); session_delete(user()->id);
unset($_SESSION['user']); unset($_SESSION['user']);
unset($_SESSION['char']);
set_cookie('remember_me', '', 1); set_cookie('remember_me', '', 1);
redirect('/'); redirect('/');
} }

View File

@ -8,7 +8,7 @@ function char_controller_list_get()
auth_only_and_must_have_character(); auth_only_and_must_have_character();
$GLOBALS['active_nav_tab'] = 'chars'; $GLOBALS['active_nav_tab'] = 'chars';
echo page('chars/list', ['chars' => char_list(user('id'))]); echo page('chars/list', ['chars' => user()->char_list()]);
} }
/** /**
@ -38,25 +38,23 @@ function char_controller_list_post()
// If the action is to select a character, change the user's selected character. // If the action is to select a character, change the user's selected character.
if ($action === 'select') { if ($action === 'select') {
// If the character ID is the current character, do nothing. // If the character ID is the current character, do nothing.
if ($char_id === $_SESSION['user']['char_id'] || $char_id === 0) { if ($char_id === user()->char_id || $char_id === 0) {
flash('alert_character_list_1', ['info', 'You are already using <b>' . char('name') . '</b>.']); flash('alert_character_list_1', ['info', 'You are already using <b>' . char()->name . '</b>.']);
redirect('/characters'); redirect('/characters');
} }
// Ensure the character ID is valid and belongs to the user. if (!Character::belongs_to($char_id, user()->id)) router_error(999);
if (!char_belongs_to_user($char_id, $_SESSION['user']['id'])) router_error(999);
change_user_character($char_id); change_user_character($char_id);
flash('alert_character_list_1', ['success', 'Switched to <b>' . char('name') . '</b>!']); flash('alert_character_list_1', ['success', 'Switched to <b>' . char()->name . '</b>!']);
} }
// If the action is to delete a character, move to the confirmation page. // If the action is to delete a character, move to the confirmation page.
if ($action === 'delete') { if ($action === 'delete') {
// Ensure the character ID is valid and belongs to the user. if (!Character::belongs_to($char_id, user()->id)) router_error(999);
if (!char_belongs_to_user($char_id, $_SESSION['user']['id'])) router_error(999);
echo page('chars/delete', ['char' => char_find($char_id)]); echo page('chars/delete', ['char' => Character::find($char_id)]);
exit; exit;
} }
@ -76,9 +74,9 @@ function char_controller_delete_post()
if (!is_numeric($char_id)) router_error(400); if (!is_numeric($char_id)) router_error(400);
// Ensure the character ID is valid and belongs to the user. // Ensure the character ID is valid and belongs to the user.
if (!char_belongs_to_user($char_id, $_SESSION['user']['id'])) router_error(999); if (!Character::belongs_to($char_id, user()->id)) router_error(999);
$char = char_find($char_id); $char = Character::find($char_id);
// Confirm the name matches the name of the character. CASE SENSITIVE. // Confirm the name matches the name of the character. CASE SENSITIVE.
if ($char['name'] !== trim($_POST['n'] ?? '')) { if ($char['name'] !== trim($_POST['n'] ?? '')) {
@ -87,11 +85,11 @@ function char_controller_delete_post()
} }
// Delete the character // Delete the character
char_delete($char_id); Character::delete($char_id);
// If the character being deleted is the currently selected character, select the first character. // If the character being deleted is the currently selected character, select the first character.
if ($_SESSION['user']['char_id'] === $char_id) { if (user()->char_id === $char_id) {
$chars = char_list(user('id')); $chars = user()->char_list();
if (count($chars) > 0) change_user_character($chars[0]['id']); if (count($chars) > 0) change_user_character($chars[0]['id']);
} }
@ -109,7 +107,7 @@ function char_controller_create_first_get()
$GLOBALS['active_nav_tab'] = 'chars'; $GLOBALS['active_nav_tab'] = 'chars';
// If the user already has a character, redirect them to the main page. // If the user already has a character, redirect them to the main page.
if (char_count(user('id')) > 0) redirect('/'); if (user()->char_count() > 0) redirect('/');
echo page('chars/first'); echo page('chars/first');
} }
@ -139,7 +137,7 @@ function char_controller_create_post()
/* /*
A character's name must be unique. A character's name must be unique.
*/ */
if (char_name_exists($name)) $errors['n'][] = 'Name is already taken.'; if (Character::name_exists($name)) $errors['n'][] = 'Name is already taken.';
// If there are errors at this point, send them to the page with errors flashed. // If there are errors at this point, send them to the page with errors flashed.
if (!empty($errors)) { if (!empty($errors)) {
@ -151,24 +149,22 @@ function char_controller_create_post()
exit; exit;
} else { } else {
// If this is not the first character, return to the character list page. // If this is not the first character, return to the character list page.
echo page('chars/list', ['chars' => char_list(user('id'))]); echo page('chars/list', ['chars' => user()->char_list()]);
exit; exit;
} }
} }
// Create the character if (($char = Character::create(user()->id, $name)) === false) router_error(400);
$char = char_create(user('id'), $name);
if ($char === false) router_error(400);
// Create the auxiliary tables // Create the auxiliary tables
char_location_create($char); $char->create_location();
char_gear_create($char); $char->create_gear();
// Award the Adventurer title. // Award the Adventurer title.
char_award_title(1, $char); $char->award_title(1);
// Set the character as the user's selected character // Set the character as the user's selected character
change_user_character($char); change_user_character($char->id);
flash('alert_character_list_1', ['success', 'Character <b>' . $name . '</b> created!']); flash('alert_character_list_1', ['success', 'Character <b>' . $name . '</b> created!']);
redirect('/characters'); redirect('/characters');

View File

@ -19,6 +19,6 @@ function profile_controller_show_get($id)
auth_only_and_must_have_character(); auth_only_and_must_have_character();
if (($char = char_find($id)) == false) router_error(999); if (($char = char_find($id)) == false) router_error(999);
if (user('char_id') == $id) redirect('/profile'); if (user()->char_id == $id) redirect('/profile');
echo page('profile/show', ['char' => $char]); echo page('profile/show', ['char' => $char]);
} }

View File

@ -51,7 +51,7 @@ function world_controller_move_post()
$r = db_query(db_live(), 'UPDATE char_locations SET x = :x, y = :y WHERE char_id = :c', [ $r = db_query(db_live(), 'UPDATE char_locations SET x = :x, y = :y WHERE char_id = :c', [
':x' => $x, ':x' => $x,
':y' => $y, ':y' => $y,
':c' => user('char_id') ':c' => user()->char_id
]); ]);
if ($r === false) throw new Exception('Failed to move character. (wcmp)'); if ($r === false) throw new Exception('Failed to move character. (wcmp)');

View File

@ -94,58 +94,51 @@ function set_cookie($name, $value, $expires)
} }
/** /**
* Get the current user's array from SESSION if it exists. Specify a key to get a specific value. * Get the current user's object from SESSION if it exists.
*/ */
function user($field = '') function user(): User|false
{ {
if (empty($_SESSION['user'])) return false; if (empty($_SESSION['user'])) return false;
if ($field === '') return $_SESSION['user']; return unserialize($_SESSION['user']);
return $_SESSION['user'][$field] ?? false;
} }
/** /**
* Check whether the user has selected a character. * Modify a field in the user session object. Returns true on success and false on failure.
*/ */
function user_selected_char() function modify_user_session(string $field, mixed $value): bool
{ {
return user('char_id') > 0 ? true : false; $user = user();
if ($user === false || !property_exists($user, $field)) return false;
$user->{$field} = $value;
$_SESSION['user'] = serialize($user);
return true;
} }
/** /**
* If the current user has a selected char and the data is in the session, retrieve either the full array of data * If the current user has a selected char and the data is in the session, retrieve the character object. If there
* or a specific field. If there is no character data, populate it. * is no character data, populate it.
*/ */
function char($field = '') function char(): Character|false
{ {
// If there is no user, return false
if (empty($_SESSION['user'])) return false; if (empty($_SESSION['user'])) return false;
if (empty($GLOBALS['char'])) $GLOBALS['char'] = serialize(user()->current_char());
if (empty($GLOBALS['char'])) { return unserialize($GLOBALS['char']);
$GLOBALS['char'] = db_query(
db_live(),
"SELECT * FROM characters WHERE id = :c",
[':c' => user('char_id')]
)->fetchArray(SQLITE3_ASSOC);
}
if ($field === '') return $GLOBALS['char'];
return $GLOBALS['char'][$field] ?? false;
} }
/** /**
* Shorthand to update the user's selected character. Returns true on success, false on failure. Database * Update the user's selected character. Returns true on success, false on failure. Database
* is updated if the character ID is different from the current session. * is updated if the character ID is different from the current session.
*/ */
function change_user_character($char_id) function change_user_character(int $char_id): bool
{ {
// If the character does not exist, return false // If the character does not exist, return false
if (($char = char_find($char_id)) === false) return false; if (($char = Character::find($char_id)) === false) return false;
$GLOBALS['char'] = $char; $GLOBALS['char'] = serialize($char);
// If the character ID is different, update the session and database // If the character ID is different, update the session and database
if ($_SESSION['user']['char_id'] !== $char_id) { if (user()->char_id !== $char_id) {
$_SESSION['user']['char_id'] = $char_id; modify_user_session('char_id', $char_id);
db_query(db_auth(), "UPDATE users SET char_id = :c WHERE id = :u", [':c' => $char_id, ':u' => user('id')]); db_query(db_auth(), "UPDATE users SET char_id = :c WHERE id = :u", [':c' => $char_id, ':u' => user()->id]);
} }
return true; return true;
@ -154,7 +147,7 @@ function change_user_character($char_id)
/** /**
* Get a percent between two ints, rounded to the nearest whole number or return 0. * Get a percent between two ints, rounded to the nearest whole number or return 0.
*/ */
function percent($num, $denom, $precision = 4): int function percent(float $num, float $denom, int $precision = 4): float
{ {
if ($denom === 0) return 0; if ($denom === 0) return 0;
$p = ($num / $denom) * 100; $p = ($num / $denom) * 100;
@ -164,20 +157,14 @@ function percent($num, $denom, $precision = 4): int
/** /**
* Access the account wallet. On first execution it will populate $GLOBALS['wallet'] with the wallet data. This way * Access the account wallet. On first execution it will populate $GLOBALS['wallet'] with the wallet data. This way
* the data is up to date with every request without having to query the database every use within, for example, a * the data is up to date with every request without having to query the database every use within, for example, a
* template. Will return false if the field does not exist, or the entire wallet array if no field is specified. * template. Will return false if the user or wallet does not exist.
*/ */
function wallet($field = '') function wallet(): Wallet|false
{ {
if (empty($GLOBALS['wallet'])) { if (user() === false) return false;
$GLOBALS['wallet'] = db_query( if (empty($GLOBALS['wallet'])) $w = user()->wallet();
db_live(), if ($w === false) return false;
"SELECT * FROM wallets WHERE user_id = :u", return $GLOBALS['wallet'] = $w;
[':u' => user('id')]
)->fetchArray(SQLITE3_ASSOC);
}
if ($field === '') return $GLOBALS['wallet'];
return $GLOBALS['wallet'][$field] ?? false;
} }
/** /**
@ -191,7 +178,7 @@ function location($field = '')
$GLOBALS['location'] = db_query( $GLOBALS['location'] = db_query(
db_live(), db_live(),
"SELECT * FROM char_locations WHERE char_id = :c", "SELECT * FROM char_locations WHERE char_id = :c",
[':c' => user('char_id')] [':c' => user()->char_id]
)->fetchArray(SQLITE3_ASSOC); )->fetchArray(SQLITE3_ASSOC);
} }
@ -300,3 +287,12 @@ function abb_num($num)
default => number_format($num, 0) default => number_format($num, 0)
}; };
} }
/**
* Check if all keys of an array are numeric.
*/
function all_keys_numeric(array $a): bool
{
foreach (array_keys($a) as $k) if (!is_int($k)) return false;
return true;
}

View File

@ -1,284 +0,0 @@
<?php
/*
characters are the living, breathing entities that interact with the game world. They are inextricably linked to their
accounts, and are the primary means by which the character interacts with the game world. Separating the character from
the account allows for multiple characters to be associated with a single account, and to prevent concurrency issues
when performing auth checks on the database.
When creating a character, we want to init all of the related data tables; wallets, inventory, bank, etc.
When retrieving a character, we will get the tables as-needed, to prevent allocating more memory than we need.
*/
const currently = [
0 => 'Exploring',
1 => 'In Town',
2 => 'In Combat',
4 => 'In Shop',
5 => 'In Inn'
];
/**
* Create a character. Only a user ID and a name are required. All other fields are optional. Pass a key-value array
* of overrides to set additional fields. A character's name must be unique, but this function does not check for
* that. Returns the created character's ID.
*/
function char_create($user_id, $name, $overrides = []): int
{
// Prep the data and merge in any overrides
$data = ['user_id' => $user_id, 'name' => $name];
if (!empty($overrides)) $data = array_merge($data, $overrides);
// Prep the fields for the query
$k = array_keys($data);
$f = implode(', ', $k);
$v = implode(', ', array_map(fn($x) => ":$x", $k));
// Create the character!
if (db_query(db_live(), "INSERT INTO characters ($f) VALUES ($v)", $data) === false) {
// @TODO: Log this error
throw new Exception('Failed to create character. (cc)');
}
// Get the character ID
return db_live()->lastInsertRowID();
}
/**
* Create a character's location record. A character's location is where they are in the game world. A character can only be
* in one location at a time. Can define a starting location for the character. Default state is 'Exploring'.
*/
function char_location_create($char_id, $x = 0, $y = 0, $currently = 0)
{
if (db_query(db_live(), "INSERT INTO char_locations (char_id, x, y, currently) VALUES (:p, :x, :y, :c)", [
':p' => $char_id,
':x' => $x,
':y' => $y,
':c' => $currently
]) === false) {
throw new Exception('Failed to create character location. (clc)');
}
}
/**
* Create the character's gear table. A character's gear is where they store their equipped items.
* @TODO: implement initial gear
*/
function char_gear_create($char_id, $initialGear = [])
{
if (db_query(db_live(), "INSERT INTO char_gear (char_id) VALUES (:p)", [':p' => $char_id]) === false) {
throw new Exception('Failed to create character gear. (cgc)');
}
}
/**
* Get a charcter by their ID or name. Returns the character's data as an associative array, or false if not found.
*/
function char_find($char_id)
{
$char = db_query(db_live(), "SELECT * FROM characters WHERE id = :id OR name = :id", [':id' => $char_id])->fetchArray(SQLITE3_ASSOC);
return $char === false ? false : $char;
}
/**
* Count the number of characters associated with an account ID.
*/
function char_count($user_id): int
{
$count = db_query(db_live(), "SELECT COUNT(*) FROM characters WHERE user_id = :u", [':u' => $user_id])->fetchArray(SQLITE3_NUM);
if ($count === false) throw new Exception('Failed to count characters. (cc)');
return (int) $count[0];
}
/**
* Get a an array of id => [name, level] for all characters associated with an account ID.
*/
function char_list($user_id)
{
$stmt = db_query(db_live(), "SELECT id, name, level FROM characters WHERE user_id = :u", [':u' => $user_id]);
if ($stmt === false) throw new Exception('Failed to list characters. (cl)');
$characters = [];
while ($row = $stmt->fetchArray(SQLITE3_ASSOC)) {
$characters[$row['id']] = ['name' => $row['name'], 'level' => $row['level']];
}
return $characters;
}
/**
* Get a character's location info by their character ID. Returns the location's data as an associative array.
*/
function char_get_location($char_id)
{
// Get the location
$location = db_query(db_live(), "SELECT * FROM char_locations WHERE char_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC);
if ($location === false) throw new Exception('Location not found. (cgl)');
return $location;
}
/**
* See if a character name exists.
*/
function char_name_exists($name)
{
return db_exists(db_live(), 'characters', 'name', $name);
}
/**
* Checks whether a character exists at a certain ID.
*/
function char_exists($char_id)
{
return db_exists(db_live(), 'characters', 'id', $char_id);
}
/**
* See if the given character belongs to the given user. Returns false if the character does not belong to the user,
* or if the character does not exist. Returns true if the character belongs to the user. Generally this function
* shouldn't return false, as it should be called after the character's existence is confirmed. If it does return false,
* it is likely due to user interference.
*/
function char_belongs_to_user($char_id, $user_id)
{
$char = db_query(
db_live(),
"SELECT 1 FROM characters WHERE id = :p AND user_id = :u",
[':p' => $char_id, ':u' => $user_id]
)->fetchArray(SQLITE3_ASSOC);
return $char !== false;
}
/**
* Delete a character by their ID. This will delete all associated data tables as well.
*/
function char_delete($char_id)
{
// Delete the character
if (db_query(db_live(), "DELETE FROM characters WHERE id = :p", [':p' => $char_id]) === false) {
throw new Exception('Failed to delete character. (cd)');
}
// Get item IDs from the character's inventory
$items = db_query(db_live(), "SELECT item_id FROM char_inventory WHERE char_id = :p", [':p' => $char_id]);
// delete the character's inventory and items
while ($row = $items->fetchArray(SQLITE3_ASSOC)) {
if (db_query(db_live(), "DELETE FROM char_inventory WHERE char_id = :c", [':c' => $char_id]) === false) {
throw new Exception('Failed to delete character inventory. (cd)');
}
if (db_query(db_live(), "DELETE FROM items WHERE id = :p", [':p' => $row['id']]) === false) {
throw new Exception('Failed to delete character item slots. (cd)');
}
}
// Delete the character's location
if (db_query(db_live(), "DELETE FROM char_locations WHERE char_id = :p", [':p' => $char_id]) === false) {
throw new Exception('Failed to delete character location. (cd)');
}
// Delete the character's gear
if (db_query(db_live(), "DELETE FROM char_gear WHERE char_id = :p", [':p' => $char_id]) === false) {
throw new Exception('Failed to delete character gear. (cd)');
}
// Delete the character's bank
if (db_query(db_live(), "DELETE FROM char_bank WHERE char_id = :p", [':p' => $char_id]) === false) {
throw new Exception('Failed to delete character bank. (cd)');
}
// Delete character's banked items
if (db_query(db_live(), "DELETE FROM char_banked_items WHERE char_id = :p", [':p' => $char_id]) === false) {
throw new Exception('Failed to delete character bank items. (cd)');
}
// Delete the user's guild membership
if (db_query(db_live(), "DELETE FROM guild_members WHERE char_id = :p", [':p' => $char_id]) === false) {
throw new Exception('Failed to delete character guild membership. (cd)');
}
// if the character was a guild leader, hand leadership to the next highest ranking member
$guild = db_query(db_live(), "SELECT id FROM guilds WHERE leader_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC);
if ($guild !== false) {
$members = db_query(db_live(), "SELECT char_id FROM guild_members WHERE guild_id = :p ORDER BY rank DESC", [':p' => $guild['id']]);
$newLeader = $members->fetchArray(SQLITE3_ASSOC);
if ($newLeader !== false) {
db_query(db_live(), "UPDATE guilds SET leader_id = :p WHERE id = :g", [':p' => $newLeader['char_id'], ':g' => $guild['id']]);
}
}
// Get a list of all pve fight IDs.
$pve = db_query(db_fights(), "SELECT id FROM pve WHERE char_id = :p", [':p' => $char_id]);
// Get a list of all pvp fight IDs.
$pvp = db_query(db_fights(), "SELECT id FROM pvp WHERE char1_id = :p OR char2_id = :p", [':p' => $char_id]);
// Delete all pve fights
while ($row = $pve->fetchArray(SQLITE3_ASSOC)) {
if (db_query(db_fights(), "DELETE FROM pve WHERE id = :p", [':p' => $row['id']]) === false) {
throw new Exception('Failed to delete pve fight. (cd)');
}
if (db_query(db_fights(), "DELETE FROM pve_logs WHERE fight_id = :p", [':p' => $row['id']]) === false) {
throw new Exception('Failed to delete pve fight logs. (cd)');
}
}
// Delete all pvp fights
while ($row = $pvp->fetchArray(SQLITE3_ASSOC)) {
if (db_query(db_fights(), "DELETE FROM pvp WHERE id = :p", [':p' => $row['id']]) === false) {
throw new Exception('Failed to delete pvp fight. (cd)');
}
if (db_query(db_fights(), "DELETE FROM pvp_logs WHERE fight_id = :p", [':p' => $row['id']]) === false) {
throw new Exception('Failed to delete pvp fight logs. (cd)');
}
}
}
/**
* Award a character a title.
*/
function char_award_title($title_id, $char_id)
{
$r = db_query(
db_live(),
'INSERT INTO owned_titles (`title_id`, `char_id`) VALUES (:t, :c)',
[':t' => $title_id, ':c' => $char_id]
);
if ($r === false) throw new Exception("Failed to award $char_id the title $title_id. (cat)");
}
/**
* Get the character's title's info and award date. Will use the currently logged in character's ID if $char_id is 0.
*/
function char_get_title($char_id = 0)
{
$char = $char_id === 0 ? char() : char_find($char_id);
$title = title($char['title_id']);
$stmt = db_query(
db_live(),
'SELECT awarded FROM owned_titles WHERE char_id = :c AND title_id = :t LIMIT 1',
[':c' => $char['id'], ':t' => $char['title_id']]
);
// If the query failed, send back an array with only an error.
if ($stmt === false) return ['error' => "owned titles query failed {$char['id']} (cat) (1)"];
$award = $stmt->fetchArray(SQLITE3_ASSOC);
// If no title, send back an empty array
if (!$award) return [];
$title['awarded'] = $award['awarded'];
return $title;
}
/**
* Get the character's user data.
*/
function char_get_user($char)
{
return user_find($char['user_id']);
}

View File

@ -1,52 +0,0 @@
<?php
/**
* Find a user by username, email, or id.
*/
function user_find($user)
{
$result = db_query(db_auth(), "SELECT * FROM users WHERE username = :u OR email = :u OR id = :u", [':u' => $user]);
$user = $result->fetchArray(SQLITE3_ASSOC);
if (!$user) return false;
$result->finalize();
return $user;
}
/**
* Create a user with a username, email, and password. Optionally pass an auth level. This function will not check
* if the username or email already exists. It is up to the caller to check this before calling this function. It is
* also up to the caller to validate password strength. This function will hash the password with the PASSWORD_ARGON2ID
* algorithm.
*/
function user_create($username, $email, $password, $auth = 0)
{
return db_query(db_auth(), "INSERT INTO users (username, email, password, auth) VALUES (:u, :e, :p, :a)", [
':u' => $username,
':e' => $email,
':p' => password_hash($password, PASSWORD_ARGON2ID),
':a' => $auth
]);
}
/**
* Delete a user by username, email, or id.
*/
function user_delete($user)
{
return db_query(db_auth(), "DELETE FROM users WHERE username = :u OR email = :u OR id = :u", [':u' => $user]);
}
/**
* Creates an account wallet. Can optionally specify the starting balances of the wallet. Returns the created wallet's
* ID. If a currency is set to -1, the starting_silver or starting_star_gems fields from the env will be used.
*/
function wallet_create($user_id, $silver = -1, $starGems = -1)
{
if (db_query(db_live(), "INSERT INTO wallets (user_id, silver, stargem) VALUES (:u, :s, :sg)", [
':u' => $user_id,
':s' => $silver === -1 ? env('start_silver', 10) : $silver,
':sg' => $starGems === -1 ? env('start_star_gems', 0) : $starGems
]) === false) {
throw new Exception('Failed to create wallet. (wc)');
}
}

262
src/models/character.php Normal file
View File

@ -0,0 +1,262 @@
<?php
/*
Characters are the living, breathing entities that interact with the game world. They are inextricably linked to their
accounts, and are the primary means by which the character interacts with the game world. Separating the character from
the account allows for multiple characters to be associated with a single account, and to prevent concurrency issues
when performing auth checks on the database.
When creating a character, we want to init all of the related data tables; wallets, inventory, bank, etc.
When retrieving a character, we will get the tables as-needed, to prevent allocating more memory than we need.
*/
class Character
{
public int $id;
public int $user_id;
public string $name;
public int $title_id;
public int $level;
public int $xp;
public int $xp_to_level;
public int $hp; // Health
public int $m_hp;
public int $mp; // Mana
public int $m_mp;
public int $tp; // Travel
public int $m_tp;
public int $pow; // Power
public int $acc; // Accuracy
public int $pen; // Penetration
public int $foc; // Focus
public int $tou; // Toughness
public int $arm; // Armor
public int $res; // Resist
public int $pre; // Precision
public int $fer; // Ferocity
public int $luck; // Luck
public int $inv_slots;
public int $att_points;
public string $bio;
public function __construct(array $data)
{
foreach ($data as $k => $v) {
if (property_exists($this, $k)) $this->$k = $v;
}
}
public static function find(int $id): Character|false
{
$q = db_query(
db_live(),
"SELECT * FROM characters WHERE id = :id OR name = :id COLLATE NOCASE",
[':id' => $id]
);
if ($q === false) throw new Exception('Failed to query character. (C::f)'); // badly formed query
return ($c = $q->fetchArray(SQLITE3_ASSOC)) === false ? false : new Character($c);
}
public static function create(int $user_id, string $name, array $overrides = []): Character|false
{
// Prep the data and merge in any overrides
$data = ['user_id' => $user_id, 'name' => $name];
if (!empty($overrides)) $data = array_merge($data, $overrides);
// Prep the fields for the query
$k = array_keys($data);
$f = implode(', ', $k);
$v = implode(', ', array_map(fn($x) => ":$x", $k));
// Create the character!
if (db_query(db_live(), "INSERT INTO characters ($f) VALUES ($v)", $data) === false) {
// @TODO: Log this error
throw new Exception('Failed to create character. (cc)');
}
// Get the character ID
return Character::find(db_live()->lastInsertRowID());
}
/**
* Create an associated location row for this character.
*/
public function create_location(int $x = 0, int $y = 0, int $currently = 0): bool
{
$l = db_query(
db_live(),
"INSERT INTO char_locations (char_id, x, y, currently) VALUES (:i, :x, :y, :c)",
[':i' => $this->id, ':x' => $x, ':y' => $y, ':c' => $currently]
);
return $l !== false;
}
/**
* Create an associated gear row for this character.
*/
public function create_gear(array $initialGear = []): bool
{
// @TODO implement initial gear
$g = db_query(db_live(), "INSERT INTO char_gear (char_id) VALUES (:i)", [':i' => $this->id]);
return $g !== false;
}
/**
* Return whether a given character name exists already.
*/
public static function name_exists(string $name): bool
{
return db_exists(db_live(), 'characters', 'name', $name);
}
/**
* Return whether a character exists with the given ID.
*/
public static function exists(int $id): bool
{
return db_exists(db_live(), 'characters', 'id', $id);
}
/**
* Return whether a character belongs to the given user ID.
*/
public static function belongs_to(int $id, int $user_id): bool
{
$q = db_query(
db_live(),
"SELECT 1 FROM characters WHERE id = :i AND user_id = :u LIMIT 1",
[':i' => $id, ':u' => $user_id]
);
if ($q === false) throw new Exception('Failed to query char ownership. (C::bt)');
return $q->fetchArray(SQLITE3_ASSOC) !== false;
}
/**
* Get the character's user.
*/
public function user(): User|false
{
return User::find($this->user_id);
}
/**
* Award this character a title.
*/
public function award_title(int $title_id): bool
{
$r = db_query(
db_live(),
'INSERT INTO owned_titles (`title_id`, `char_id`) VALUES (:t, :i)',
[':t' => $title_id, ':i' => $this->id]
);
return $r !== false;
}
/**
* Delete a character by the ID. Deletes many rows of data from many tables; items, bank, PvE and PvP w/ logs, etc
*/
public static function delete(int $id)
{
// Delete the character
if (db_query(db_live(), "DELETE FROM characters WHERE id = :p", [':p' => $id]) === false) {
throw new Exception('Failed to delete character. (C::d)');
}
// Get item IDs from the character's inventory
$items = db_query(db_live(), "SELECT item_id FROM char_inventory WHERE char_id = :p", [':p' => $id]);
// delete the character's inventory and items
while ($row = $items->fetchArray(SQLITE3_ASSOC)) {
if (db_query(db_live(), "DELETE FROM char_inventory WHERE char_id = :c", [':c' => $id]) === false) {
throw new Exception('Failed to delete character inventory. (C::d)');
}
if (db_query(db_live(), "DELETE FROM items WHERE id = :p", [':p' => $row['id']]) === false) {
throw new Exception('Failed to delete character item slots. (C::d)');
}
}
// Delete the character's location
if (db_query(db_live(), "DELETE FROM char_locations WHERE char_id = :p", [':p' => $id]) === false) {
throw new Exception('Failed to delete character location. (C::d)');
}
// Delete the character's gear
if (db_query(db_live(), "DELETE FROM char_gear WHERE char_id = :p", [':p' => $id]) === false) {
throw new Exception('Failed to delete character gear. (C::d)');
}
// Delete the character's bank
if (db_query(db_live(), "DELETE FROM char_bank WHERE char_id = :p", [':p' => $id]) === false) {
throw new Exception('Failed to delete character bank. (C::d)');
}
// Delete character's banked items
if (db_query(db_live(), "DELETE FROM char_banked_items WHERE char_id = :p", [':p' => $id]) === false) {
throw new Exception('Failed to delete character bank items. (C::d)');
}
// Delete the user's guild membership
if (db_query(db_live(), "DELETE FROM guild_members WHERE char_id = :p", [':p' => $id]) === false) {
throw new Exception('Failed to delete character guild membership. (C::d)');
}
// if the character was a guild leader, hand leadership to the next highest ranking member
$guild = db_query(db_live(), "SELECT id FROM guilds WHERE leader_id = :p", [':p' => $id])->fetchArray(SQLITE3_ASSOC);
if ($guild !== false) {
$members = db_query(db_live(), "SELECT char_id FROM guild_members WHERE guild_id = :p ORDER BY rank DESC", [':p' => $guild['id']]);
$newLeader = $members->fetchArray(SQLITE3_ASSOC);
if ($newLeader !== false) {
db_query(db_live(), "UPDATE guilds SET leader_id = :p WHERE id = :g", [':p' => $newLeader['char_id'], ':g' => $guild['id']]);
}
}
// Get a list of all pve fight IDs.
$pve = db_query(db_fights(), "SELECT id FROM pve WHERE char_id = :p", [':p' => $id]);
// Get a list of all pvp fight IDs.
$pvp = db_query(db_fights(), "SELECT id FROM pvp WHERE char1_id = :p OR char2_id = :p", [':p' => $id]);
// Delete all pve fights
while ($row = $pve->fetchArray(SQLITE3_ASSOC)) {
if (db_query(db_fights(), "DELETE FROM pve WHERE id = :p", [':p' => $row['id']]) === false) {
throw new Exception('Failed to delete pve fight. (C::d)');
}
if (db_query(db_fights(), "DELETE FROM pve_logs WHERE fight_id = :p", [':p' => $row['id']]) === false) {
throw new Exception('Failed to delete pve fight logs. (C::d)');
}
}
// Delete all pvp fights
while ($row = $pvp->fetchArray(SQLITE3_ASSOC)) {
if (db_query(db_fights(), "DELETE FROM pvp WHERE id = :p", [':p' => $row['id']]) === false) {
throw new Exception('Failed to delete pvp fight. (C::d)');
}
if (db_query(db_fights(), "DELETE FROM pvp_logs WHERE fight_id = :p", [':p' => $row['id']]) === false) {
throw new Exception('Failed to delete pvp fight logs. (C::d)');
}
}
}
/**
* Get the character's current title.
*/
public function title(): array|false
{
$t = title($this->title_id);
$q = db_query(
db_live(),
'SELECT awarded FROM owned_titles WHERE char_id = :c AND title_id = :t LIMIT 1',
[':c' => $this->id, ':t' => $this->title_id]
);
if ($q === false) throw new Exception('Failed to query title. (C::t)');
$a = $q->fetchArray(SQLITE3_ASSOC);
if ($a === false) return false;
$t['awarded'] = $a['awarded']; // add the awarded date to the title info
return $t;
}
}

180
src/models/user.php Normal file
View File

@ -0,0 +1,180 @@
<?php
/**
* Representation of a User from the auth database. Contains auth-related info and handles meta-level state.
*/
class User
{
/**
* Auto-incrementing unique ID.
*/
public int $id;
/**
* A presumably unique username.
*/
public string $username;
/**
* A presumably unique email address.
*/
public string $email;
/**
* The user's hashed password; private to protect it from inspection.
*/
private string $password;
/**
* Auth level; see Auth to check how permissions work.
*/
public int $auth;
/**
* Currently selected character's ID.
*/
public int $char_id;
/**
* How many characters the user can have.
*/
public int $char_slots;
/**
* When this account was created (registered date).
*/
public DateTime $created;
/**
* When the account was last logged in to.
*/
public DateTime $last_login;
/**
* Populate a User object with data; assumes you are passing the associatve array from SQLite directly.
*/
public function __construct(array $data)
{
foreach ($data as $k => $v) {
if (property_exists($this, $k)) {
$this->$k = in_array($k, ['created', 'last_login']) ? new DateTime($v) : $v;
}
}
}
/**
* Find a user by their username, email or ID. Case-nonsensitive. Throws an Exception when the query is
* badly formed. Returns false if no user is found. Returns a User on success.
*/
public static function find(string|int $identifier): User|false
{
$r = db_query(
db_auth(),
"SELECT * FROM users WHERE username = :i COLLATE NOCASE OR email = :i COLLATE NOCASE OR id = :i LIMIT 1",
[':i' => $identifier]
);
if ($r === false) throw new Exception("Failed to query user. (U::f)"); // badly formed query
$u = $r->fetchArray(SQLITE3_ASSOC);
if ($u === false) return false; // no user found
return new User($u);
}
/**
* Create a new user row in the database. This function does not check for the unique-ness or validity
* of the username or password passed to it; that is the responsibility of the caller. Returns false on
* failure.
*/
public static function create(string $username, string $email, string $password, int $auth = 0): SQLite3Result|false
{
return db_query(db_auth(), "INSERT INTO users (username, email, password, auth) VALUES (:u, :e, :p, :a)", [
':u' => $username,
':e' => $email,
':p' => password_hash($password, PASSWORD_ARGON2ID),
':a' => $auth
]);
}
/**
* Check $ref against the user's password.
*/
public function check_password(string $ref): bool
{
return password_verify($ref, $this->password);
}
/**
* Check if the given username exists.
*/
public static function username_exists(string $username): bool
{
return db_exists(db_auth(), 'users', 'username', $username);
}
/**
* Check if the given email exists.
*/
public static function email_exists(string $email): bool
{
return db_exists(db_auth(), 'users', 'email', $email);
}
/**
* Delete a user by their username, email, or id.
*/
public static function delete(string|int $identifier): SQLite3Result|false
{
return db_query(
db_auth(),
"DELETE FROM users WHERE username = :i OR email = :i OR id = :i",
[':i' => $identifier]
);
}
/**
* Return a count of how many characters this user has.
*/
public function char_count(): int
{
$c = db_query(
db_live(),
"SELECT COUNT(*) FROM characters WHERE user_id = :u",
[':u' => $this->id]
)->fetchArray(SQLITE3_NUM);
if ($c === false) throw new Exception('Failed to count characters. (U::cc)');
return (int) $c[0];
}
/**
* Get a an array of id => [name, level] for all characters associated with this user ID. Returns false
* if there are no characters.
*/
public function char_list(): array|false
{
$q = db_query(db_live(), "SELECT id, name, level FROM characters WHERE user_id = ?", [$this->id]);
if ($q === false) throw new Exception('Failed to list characters. (U->cl)');
$c = [];
while ($row = $q->fetchArray(SQLITE3_ASSOC)) {
$c[$row['id']] = ['name' => $row['name'], 'level' => $row['level']];
}
// return false if no characters
return empty($c) ? false : $c;
}
/**
* Get the user's current Character.
*/
public function current_char(): Character|false
{
return Character::find($this->char_id);
}
/**
* Get the user's wallet.
*/
public function wallet(): Wallet|false
{
return Wallet::find($this->id);
}
}

52
src/models/wallet.php Normal file
View File

@ -0,0 +1,52 @@
<?php
class Wallet
{
public function __construct(
public int $user_id,
public int $silver,
public int $stargem
) {}
public static function find(int $user_id): Wallet|false
{
$r = db_query(db_live(), 'SELECT * FROM wallets WHERE user_id = ?', [$user_id]);
if ($r === false) throw new Exception('Failed to query wallet. (W::f)'); // badly formed query
$w = $r->fetchArray(SQLITE3_ASSOC);
if ($w === false) return false; // no wallet found
return new Wallet($user_id, $w['silver'], $w['stargem']);
}
public static function create(int $user_id, int $silver = -1, int $starGems = -1): SQLite3Result|false
{
return db_query(
db_live(),
"INSERT INTO wallets (user_id, silver, stargem) VALUES (:u, :s, :sg)",
[
':u' => $user_id,
':s' => $silver === -1 ? env('start_silver', 10) : $silver,
':sg' => $starGems === -1 ? env('start_star_gems', 0) : $starGems
]
);
}
/**
* Add a certain amount of currency to the user's wallet.
*/
public function give(Currency $c, int $amt): SQLite3Result|false
{
$cs = $c->string(true);
$new = $this->{$cs} + $amt;
return db_query(db_live(), "UPDATE wallets SET $cs = ? WHERE user_id = ?", [$new, $this->user_id]);
}
/**
* Remove a certain amount of currency from the user's wallet.
*/
public function take(Currency $c, int $amt): SQLite3Result|false
{
$cs = $c->string(true);
$new = $this->{$cs} - $amt;
return db_query(db_live(), "UPDATE wallets SET $cs = ? WHERE user_id = ?", [$new < 0 ? 0 : $new, $this->user_id]);
}
}

View File

@ -1,21 +1,5 @@
<?php <?php
/**
* Checks if the given username already exists.
*/
function auth_username_exists($username)
{
return db_exists(db_auth(), 'users', 'username', $username);
}
/**
* Checks if the given email already exists.
*/
function auth_email_exists($email)
{
return db_exists(db_auth(), 'users', 'email', $email);
}
/** /**
* Check for a user session. If $_SESSION['user'] already exists, return early. If not, check for a remember me * Check for a user session. If $_SESSION['user'] already exists, return early. If not, check for a remember me
* cookie. If a remember me cookie exists, validate the session and set $_SESSION['user']. * cookie. If a remember me cookie exists, validate the session and set $_SESSION['user'].
@ -27,10 +11,8 @@ function auth_check()
if (isset($_COOKIE['remember_me'])) { if (isset($_COOKIE['remember_me'])) {
$session = session_validate($_COOKIE['remember_me']); $session = session_validate($_COOKIE['remember_me']);
if ($session === true) { if ($session === true) {
$user = user_find($session['user_id']); $user = User::find($session['user_id']);
unset($user['password']); $_SESSION['user'] = serialize($user);
$_SESSION['user'] = user_find($session['user_id']);
$_SESSION['char'] = char_find($user['char_id']);
return true; return true;
} }
} }
@ -62,17 +44,21 @@ function guest_only()
function must_have_character() function must_have_character()
{ {
// If there is a character selected, make sure the session is up to date. // If there is a character selected, make sure the session is up to date.
if ($_SESSION['user']['char_id'] !== 0) { if (user()->char_id !== 0) {
char(); char();
return; return;
} }
// if no characters, redirect to create first // if no characters, redirect to create first
if (char_count(user('id')) === 0) redirect('/character/create-first'); if (user()->char_count() === 0) redirect('/character/create-first');
// if no character selected, select the first one // if no character selected, select the first one
if ($_SESSION['user']['char_id'] === 0) { if (user()->char_id === 0) {
$char = db_query(db_live(), 'SELECT * FROM characters WHERE user_id = :u ORDER BY id ASC LIMIT 1', [':u' => user('id')])->fetchArray(SQLITE3_ASSOC); $char = db_query(
db_live(),
'SELECT * FROM characters WHERE user_id = :u ORDER BY id ASC LIMIT 1',
[':u' => user()->id]
)->fetchArray(SQLITE3_ASSOC);
change_user_character($char['id']); change_user_character($char['id']);
} }
} }

View File

@ -27,6 +27,14 @@ function c_left_nav()
return render('components/left_nav'); return render('components/left_nav');
} }
/**
* Render the right sidebar menu.
*/
function c_right_nav()
{
return render('components/right_nav', ['c' => char()]);
}
/** /**
* Render the debug query log. * Render the debug query log.
*/ */

View File

@ -57,10 +57,13 @@ function db_blueprints()
* Take a SQLite3 database connection, a query string, and an array of parameters. Prepare the query and * Take a SQLite3 database connection, a query string, and an array of parameters. Prepare the query and
* bind the parameters with proper type casting. Then execute the query and return the result. * bind the parameters with proper type casting. Then execute the query and return the result.
*/ */
function db_query($db, $query, $params = []) function db_query(SQLite3 $db, string $query, array $params = []): SQLite3Result|false
{ {
$p = strpos($query, '?') !== false; // are generic placeholders?
$stmt = $db->prepare($query); $stmt = $db->prepare($query);
if (!empty($params)) foreach ($params as $key => $value) $stmt->bindValue($key, $value, getSQLiteType($value)); if (!empty($params)) {
foreach ($params as $k => $v) $stmt->bindValue($p ? $k + 1 : $k, $v, getSQLiteType($v));
}
$start = microtime(true); $start = microtime(true);
$r = $stmt->execute(); $r = $stmt->execute();
db_log($query, microtime(true) - $start); db_log($query, microtime(true) - $start);
@ -79,10 +82,10 @@ function db_exec($db, $query)
} }
/** /**
* Take a SQLite3 database connection, a column name, and a value. Execute a COUNT query to see if the value * Take a SQLite3 database connection, a column name, and a value. Execute a SELECT query to see if the value
* exists in the column. Return true if the value exists, false otherwise. * exists in the column. Return true if the value exists, false otherwise.
*/ */
function db_exists($db, $table, $column, $value, $caseInsensitive = true) function db_exists(SQLite3 $db, string $table, string $column, mixed $value, bool $caseInsensitive = true): bool
{ {
if ($caseInsensitive) { if ($caseInsensitive) {
$query = "SELECT 1 FROM $table WHERE $column = :v COLLATE NOCASE LIMIT 1"; $query = "SELECT 1 FROM $table WHERE $column = :v COLLATE NOCASE LIMIT 1";

25
src/util/enums.php Normal file
View File

@ -0,0 +1,25 @@
<?php
enum Currently {
case Exploring;
case InTown;
case InCombat;
case InShop;
case InInn;
}
enum Currency {
case Silver;
case StarGem;
/**
* Convert currency to it's human readable string or DB column name.
*/
public function string(bool $db = false): string
{
return match($this) {
Currency::Silver => $db ? 'silver' : 'Silver',
Currency::StarGem => $db ? 'stargem' : 'Star Gem'
};
}
}

View File

@ -1,36 +0,0 @@
<div id="char-bar">
<div class="container">
<div>
<img class="icon" src="/assets/img/icons/user1.png" alt="User">
<?= $char['name'] ?> <span class="badge ml-2 tooltip-hover" data-tooltip-content="Level"><?= $char['level'] ?></span>
<?php if ($char['att_points'] > 0): ?>
<span class="ui button primary badge ml-2 tooltip-hover" data-tooltip-content="Attribute Points"><?= $char['att_points'] ?></span>
<?php endif; ?>
</div>
<div>
<div class="char-meter">
<div class="hp" style="width: <?= percent($char['hp'], $char['m_hp']) ?>%"></div>
<div class="tooltip-trigger tooltip-hover" data-tooltip-content="Health<br><?= $char['hp'] ?> / <?= $char['m_hp'] ?>"></div>
</div>
</div>
<div>
<div class="char-meter">
<div class="mp" style="width: <?= percent($char['mp'], $char['m_mp']) ?>%"></div>
<div class="tooltip-trigger tooltip-hover" data-tooltip-content="Mana<br><?= $char['mp'] ?> / <?= $char['m_mp'] ?>"></div>
</div>
</div>
<div>
<div class="char-meter">
<div class="tp" style="width: <?= percent($char['tp'], $char['m_tp']) ?>%"></div>
<div class="tooltip-trigger tooltip-hover" data-tooltip-content="Travel Points<br><?= $char['tp'] ?> / <?= $char['m_tp'] ?>"></div>
</div>
</div>
<div>
<?= wallet('silver') ?> Silver
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
<div class="radio-block <?= $id === user('char_id') ? 'active' : '' ?>"> <div class="radio-block <?= $id === user()->char_id ? 'active' : '' ?>">
<input type="radio" name="char_id" value="<?= $id ?>" id="char_<?= $id ?>"<?= $id === user('char_id') ? 'disabled' : '' ?>> <input type="radio" name="char_id" value="<?= $id ?>" id="char_<?= $id ?>"<?= $id === user()->char_id ? 'disabled' : '' ?>>
<label for="char_<?= $id ?>"> <label for="char_<?= $id ?>">
<?= $char['name'] ?> <?= $char['name'] ?>
<span class="badge"><?= $char['level'] ?></span> <span class="badge"><?= $char['level'] ?></span>

View File

@ -1,17 +1,17 @@
<div class="stats"> <div class="stats">
<h4>Stats</h4> <h4>Stats</h4>
<div class="grid"> <div class="grid">
<div class="cell"><span class="label">Max HP</span> <?= abb_num($char['m_hp']) ?></div> <div class="cell"><span class="label">Max HP</span> <?= abb_num($char->m_hp) ?></div>
<div class="cell"><span class="label">Max MP</span> <?= abb_num($char['m_mp']) ?></div> <div class="cell"><span class="label">Max MP</span> <?= abb_num($char->m_mp) ?></div>
<div class="cell"><span class="label">Power</span> <?= abb_num($char['pow']) ?></div> <div class="cell"><span class="label">Power</span> <?= abb_num($char->pow) ?></div>
<div class="cell"><span class="label">Accuracy</span> <?= abb_num($char['acc']) ?></div> <div class="cell"><span class="label">Accuracy</span> <?= abb_num($char->acc) ?></div>
<div class="cell"><span class="label">Penetration</span> <?= abb_num($char['pen']) ?></div> <div class="cell"><span class="label">Penetration</span> <?= abb_num($char->pen) ?></div>
<div class="cell"><span class="label">Focus</span> <?= abb_num($char['foc']) ?></div> <div class="cell"><span class="label">Focus</span> <?= abb_num($char->foc) ?></div>
<div class="cell"><span class="label">Toughness</span> <?= abb_num($char['tou']) ?></div> <div class="cell"><span class="label">Toughness</span> <?= abb_num($char->tou) ?></div>
<div class="cell"><span class="label">Armor</span> <?= abb_num($char['arm']) ?></div> <div class="cell"><span class="label">Armor</span> <?= abb_num($char->arm) ?></div>
<div class="cell"><span class="label">Resist</span> <?= abb_num($char['res']) ?></div> <div class="cell"><span class="label">Resist</span> <?= abb_num($char->res) ?></div>
<div class="cell"><span class="label">Precision</span> <?= abb_num($char['pre']) ?></div> <div class="cell"><span class="label">Precision</span> <?= abb_num($char->pre) ?></div>
<div class="cell"><span class="label">Ferocity</span> <?= abb_num($char['fer']) ?></div> <div class="cell"><span class="label">Ferocity</span> <?= abb_num($char->fer) ?></div>
<div class="cell"><span class="label">Luck</span> <?= abb_num($char['luck']) ?></div> <div class="cell"><span class="label">Luck</span> <?= abb_num($char->luck) ?></div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,37 @@
<div id="character" class="box">
<div class="name">
<?= $c->name ?>
<?php if ($c->att_points > 0): ?>
<span class="ui button primary badge ml-2 tooltip-hover" data-tooltip-content="Attribute Points"><?= $c->att_points ?></span>
<?php endif; ?>
</div>
<div>
L<?= $c->level ?> <?= $c->title()['name'] ?>
</div>
<div>
<?= wallet()->silver ?> s
</div>
<div>
<div class="char-meter">
<div class="hp" style="width: <?= percent($c->hp, $c->m_hp) ?>%"></div>
<div class="tooltip-trigger tooltip-hover" data-tooltip-content="Health<br><?= $c->hp ?> / <?= $c->m_hp ?>"></div>
</div>
</div>
<div>
<div class="char-meter">
<div class="mp" style="width: <?= percent($c->mp, $c->m_mp) ?>%"></div>
<div class="tooltip-trigger tooltip-hover" data-tooltip-content="Mana<br><?= $c->mp ?> / <?= $c->m_mp ?>"></div>
</div>
</div>
<div>
<div class="char-meter">
<div class="tp" style="width: <?= percent($c->tp, $c->m_tp) ?>%"></div>
<div class="tooltip-trigger tooltip-hover" data-tooltip-content="Travel Points<br><?= $c->tp ?> / <?= $c->m_tp ?>"></div>
</div>
</div>
</div>

View File

@ -15,7 +15,7 @@
<div class="right"> <div class="right">
<?php if (user()): ?> <?php if (user()): ?>
<p>Welcome, <?= user('username') ?></p> <p>Welcome, <?= user()->username ?></p>
<?= c_logout_button() ?> <?= c_logout_button() ?>
<?php else: ?> <?php else: ?>
<a class="ui button primary" href="/auth/login">Login</a> <a class="ui button primary" href="/auth/login">Login</a>
@ -24,11 +24,9 @@
</div> </div>
</header> </header>
<?= c_char_bar(user('char_id')) ?>
<main> <main>
<aside id="left"> <aside id="left">
<?php if (user() && user_selected_char()) echo c_left_nav($activeTab ?? 0); ?> <?php if (user() && user()->char_id > 0) echo c_left_nav($activeTab ?? 0); ?>
</aside> </aside>
<div id="center"> <div id="center">
@ -36,11 +34,7 @@
</div> </div>
<aside id="right"> <aside id="right">
<?php if (user() && user_selected_char()): ?> <?php if (user() && user()->char_id > 0) echo c_right_nav(); ?>
<div class="box">
@TODO
</div>
<?php endif; ?>
</aside> </aside>
</main> </main>

View File

@ -1,5 +1,5 @@
<section> <section>
<h1>Characters <span class="badge"><?= count($chars) . '/' . user('char_slots') ?></span></h1> <h1>Characters <span class="badge"><?= count($chars) . '/' . user()->char_slots ?></span></h1>
<?php <?php
if (($f = flash('alert_character_list_1')) !== false) echo c_alert($f[0], $f[1]); if (($f = flash('alert_character_list_1')) !== false) echo c_alert($f[0], $f[1]);
@ -23,10 +23,10 @@
<?php endif; ?> <?php endif; ?>
</section> </section>
<?php if (user('char_slots') > count($chars)): ?> <?php if (user()->char_slots > count($chars)): ?>
<section> <section>
<h2>Create a new character</h2> <h2>Create a new character</h2>
<?php $num_slots_left = user('char_slots') - count($chars); ?> <?php $num_slots_left = user()->char_slots - count($chars); ?>
<p>You have <b><?= $num_slots_left ?> <?= $num_slots_left === 1 ? 'slot' : 'slots' ?></b> left.</p> <p>You have <b><?= $num_slots_left ?> <?= $num_slots_left === 1 ? 'slot' : 'slots' ?></b> left.</p>
<form action="/character/create" method="post"> <form action="/character/create" method="post">
<input type="hidden" name="csrf" value="<?= csrf() ?>"> <input type="hidden" name="csrf" value="<?= csrf() ?>">

View File

@ -4,5 +4,5 @@
<a href="/auth/register" class="ui button secondary">Register</a> <a href="/auth/register" class="ui button secondary">Register</a>
<?php else: ?> <?php else: ?>
<h1>Home</h1> <h1>Home</h1>
<p>Welcome, <?= user('username') ?>!</p> <p>Welcome, <?= user()->username ?>!</p>
<?php endif; ?> <?php endif; ?>

View File

@ -1,7 +1,7 @@
<section class="profile"> <section class="profile">
<header> <header>
<h1><?= char('name') ?></h1> <h1><?= char()->name ?></h1>
<h3>Level <?= char('level') ?> <?= char_get_title()['name'] ?></h3> <h3>Level <?= char()->level ?> <?= char()->title()['name'] ?></h3>
<h4>You</h4> <h4>You</h4>
</header> </header>
@ -27,7 +27,7 @@
<section class="right"> <section class="right">
<div class="bio"> <div class="bio">
<h4>Biography</h4> <h4>Biography</h4>
<?= char('bio') ?> <?= char()->bio ?>
</div> </div>
<div class="gear"> <div class="gear">

View File

@ -2,7 +2,7 @@
<header> <header>
<h1><?= $char['name'] ?></h1> <h1><?= $char['name'] ?></h1>
<h3>Level <?= $char['level'] ?> <?= char_get_title($char['id'])['name'] ?></h3> <h3>Level <?= $char['level'] ?> <?= char_get_title($char['id'])['name'] ?></h3>
<h4>owned by <?= $char['user_id'] === user('id') ? 'you' : char_get_user($char)['username'] ?></h4> <h4>owned by <?= $char['user_id'] === user()->id ? 'you' : char_get_user($char)['username'] ?></h4>
</header> </header>
<div class="grid"> <div class="grid">