ava borders, controllers, item boxes

This commit is contained in:
Sky Johnson 2024-10-24 18:23:55 -05:00
parent 69e057a96e
commit 0e0a4c8e38
49 changed files with 313 additions and 203 deletions

File diff suppressed because one or more lines are too long

View File

@ -276,6 +276,7 @@ span.badge {
} }
.tooltip { .tooltip {
position: absolute;
background-color: black; background-color: black;
color: white; color: white;
border: 1px solid #666; border: 1px solid #666;

View File

@ -1,15 +1,14 @@
section.profile { section.profile {
header { header {
text-align: center; text-align: center;
margin-bottom: 2rem; margin-bottom: 3rem;
h3 { h3 {
color: rgba(0, 0, 0, 0.3);
text-transform: uppercase;
font-size: 1rem; font-size: 1rem;
} }
h4 { h5 {
color: rgba(0, 0, 0, 0.5);
font-size: 0.75rem; font-size: 0.75rem;
} }
} }
@ -22,7 +21,7 @@ section.profile {
width: 50%; width: 50%;
& > div:not(:last-child) { & > div:not(:last-child) {
margin-bottom: 1rem; margin-bottom: 2rem;
} }
} }
} }
@ -31,8 +30,17 @@ section.profile {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding-bottom: 1rem;
img { img {
max-width: 250px; height: 185px;
width: 185px;
}
.border {
width: 250px;
height: 250px;
position: absolute;
} }
} }
@ -67,4 +75,58 @@ section.profile {
} }
} }
} }
#equipped-gear {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
div.item {
display: flex;
justify-content: center;
align-items: center;
&.i-1x1 {
width: 30px;
height: 30px;
background-image: url('/assets/img/ui/1x1.png');
}
&.i-2x2 {
width: 60px;
height: 60px;
background-image: url('/assets/img/ui/2x2.png');
}
&.i-2x3 {
width: 60px;
height: 90px;
background-image: url('/assets/img/ui/2x3.png');
}
}
& > div {
display: flex;
gap: 0.5rem;
& > div {
display: flex;
align-items: center;
justify-content: center;
width: 60px;
&.top, &.bot {
width: 60px;
height: 60px;
}
&.mid {
width: 60px;
height: 90px;
}
}
}
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -6,12 +6,12 @@
define('SRC', __DIR__ . '/../src'); define('SRC', __DIR__ . '/../src');
require_once SRC . '/bootstrap.php'; require_once SRC . '/bootstrap.php';
$r = []; $r = new Router;
/* /*
Home Home
*/ */
router_get($r, '/', function () { $r->get('/', function () {
if (user()) must_have_character(); if (user()) must_have_character();
$GLOBALS['active_nav_tab'] = 'home'; $GLOBALS['active_nav_tab'] = 'home';
echo render('layouts/basic', ['view' => 'pages/home']); echo render('layouts/basic', ['view' => 'pages/home']);
@ -20,54 +20,54 @@ router_get($r, '/', function () {
/* /*
Auth Auth
*/ */
router_get($r, '/auth/register', 'auth_controller_register_get'); $r->get('/auth/register', 'auth_controller_register_get');
router_post($r, '/auth/register', 'auth_controller_register_post'); $r->post('/auth/register', 'auth_controller_register_post');
router_get($r, '/auth/login', 'auth_controller_login_get'); $r->get('/auth/login', 'auth_controller_login_get');
router_post($r, '/auth/login', 'auth_controller_login_post'); $r->post('/auth/login', 'auth_controller_login_post');
router_post($r, '/auth/logout', 'auth_controller_logout_post'); $r->post('/auth/logout', 'auth_controller_logout_post');
/* /*
Characters Characters
*/ */
router_get($r, '/characters', 'char_controller_list_get'); $r->get('/characters', 'char_controller_list_get');
router_post($r, '/characters', 'char_controller_list_post'); $r->post('/characters', 'char_controller_list_post');
router_get($r, '/character/create-first', 'char_controller_create_first_get'); $r->get('/character/create-first', 'char_controller_create_first_get');
router_post($r, '/character/create', 'char_controller_create_post'); $r->post('/character/create', 'char_controller_create_post');
router_post($r, '/character/delete', 'char_controller_delete_post'); $r->post('/character/delete', 'char_controller_delete_post');
/* /*
World World
*/ */
router_get($r, '/world', 'world_controller_get'); $r->get('/world', 'world_controller_get');
router_post($r, '/move', 'world_controller_move_post'); $r->post('/move', 'world_controller_move_post');
/* /*
Profile Profile
*/ */
router_get($r, '/profile', 'profile_controller_get'); $r->get('/profile', 'profile_controller_get');
router_get($r, '/profile/:id', 'profile_controller_show_get'); $r->get('/profile/:id', 'profile_controller_show_get');
/* /*
Settings Settings
*/ */
router_get($r, '/settings', 'settings_controller_get'); $r->get('/settings', 'settings_controller_get');
/* /*
Auctions Auctions
*/ */
router_get($r, '/auctions', 'auctions_controller_get'); $r->get('/auctions', 'auctions_controller_get');
/* /*
Testing Testing
*/ */
if (env('debug')) { if (env('debug')) {
router_get($r, '/give_silver/:x', function (int $amt) { $r->get('/give_silver/:x', function (int $amt) {
auth_only_and_must_have_character(); auth_only_and_must_have_character();
wallet()->give(Currency::Silver, $amt); wallet()->give(Currency::Silver, $amt);
redirect('/'); redirect('/');
}); });
router_get($r, '/take_silver/:x', function (int $amt) { $r->get('/take_silver/:x', function (int $amt) {
auth_only_and_must_have_character(); auth_only_and_must_have_character();
wallet()->take(Currency::Silver, $amt); wallet()->take(Currency::Silver, $amt);
redirect('/'); redirect('/');
@ -79,11 +79,11 @@ if (env('debug')) {
*/ */
// [code, handler, params] // [code, handler, params]
stopwatch_start('router'); stopwatch_start('router');
$l = router_lookup($r, $_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']); $l = $r->lookup($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
stopwatch_stop('router'); stopwatch_stop('router');
stopwatch_start('handler'); stopwatch_start('handler');
if ($l['code'] !== 200) router_error($l['code']); if ($l['code'] !== 200) error_response($l['code']);
$l['handler'](...$l['params'] ?? []); $l['handler'](...$l['params'] ?? []);
stopwatch_stop('handler'); stopwatch_stop('handler');

View File

@ -25,12 +25,12 @@ require_once SRC . '/models/session.php';
require_once SRC . '/models/token.php'; require_once SRC . '/models/token.php';
// Controllers // Controllers
require_once SRC . '/controller/char.php'; require_once SRC . '/controllers/char.php';
require_once SRC . '/controller/auth.php'; require_once SRC . '/controllers/auth.php';
require_once SRC . '/controller/world.php'; require_once SRC . '/controllers/world.php';
require_once SRC . '/controller/settings.php'; require_once SRC . '/controllers/settings.php';
require_once SRC . '/controller/auctions.php'; require_once SRC . '/controllers/auctions.php';
require_once SRC . '/controller/profile.php'; require_once SRC . '/controllers/profile.php';
spl_autoload_register(function (string $class) { spl_autoload_register(function (string $class) {
if (array_key_exists($class, CLASS_MAP)) require_once SRC . CLASS_MAP[$class]; if (array_key_exists($class, CLASS_MAP)) require_once SRC . CLASS_MAP[$class];

View File

@ -71,7 +71,7 @@ function auth_controller_register_post()
exit; exit;
} }
if (User::create($u, $e, $p) === false) router_error(400); if (User::create($u, $e, $p) === false) error_response(400);
$_SESSION['user'] = serialize(User::find($u)); $_SESSION['user'] = serialize(User::find($u));
Wallet::create(user()->id); Wallet::create(user()->id);
@ -128,7 +128,7 @@ function auth_controller_login_post()
"INSERT INTO sessions (token, user_id, expires) VALUES (:t, :u, :e)", "INSERT INTO sessions (token, user_id, expires) VALUES (:t, :u, :e)",
[':t' => $token, ':u' => user()->id, ':e' => $expires] [':t' => $token, ':u' => user()->id, ':e' => $expires]
); );
if (!$result) router_error(400); if (!$result) error_response(400);
set_cookie('remember_me', $token, $expires); set_cookie('remember_me', $token, $expires);
} }
@ -136,7 +136,7 @@ function auth_controller_login_post()
redirect('/character/create-first'); redirect('/character/create-first');
} elseif (!change_user_character(user()->char_id)) { } elseif (!change_user_character(user()->char_id)) {
echo "failed to change user character (aclp)"; echo "failed to change user character (aclp)";
router_error(999); error_response(999);
} }
redirect('/'); redirect('/');

View File

@ -24,7 +24,7 @@ function char_controller_list_post()
$action = $_POST['action'] ?? ''; $action = $_POST['action'] ?? '';
// If the character ID is not a number, or the action is not a string, return a 400. // If the character ID is not a number, or the action is not a string, return a 400.
if (!is_numeric($char_id) || !is_string($action)) router_error(400); if (!is_numeric($char_id) || !is_string($action)) error_response(400);
// If the character ID is 0, return to the list. // If the character ID is 0, return to the list.
if ($char_id === 0) { if ($char_id === 0) {
@ -33,7 +33,7 @@ function char_controller_list_post()
} }
// If the action is not one of the allowed actions, return a 400. // If the action is not one of the allowed actions, return a 400.
if (!in_array($action, ['select', 'delete'])) router_error(400); if (!in_array($action, ['select', 'delete'])) error_response(400);
// 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') {
@ -43,7 +43,7 @@ function char_controller_list_post()
redirect('/characters'); redirect('/characters');
} }
if (!Character::belongs_to($char_id, user()->id)) router_error(999); if (!Character::belongs_to($char_id, user()->id)) error_response(999);
change_user_character($char_id); change_user_character($char_id);
@ -52,7 +52,7 @@ function char_controller_list_post()
// 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') {
if (!Character::belongs_to($char_id, user()->id)) router_error(999); if (!Character::belongs_to($char_id, user()->id)) error_response(999);
echo page('chars/delete', ['char' => Character::find($char_id)]); echo page('chars/delete', ['char' => Character::find($char_id)]);
exit; exit;
@ -71,10 +71,10 @@ function char_controller_delete_post()
$char_id = (int) ($_POST['char_id'] ?? 0); $char_id = (int) ($_POST['char_id'] ?? 0);
// If the character ID is not a number, return a 400. // If the character ID is not a number, return a 400.
if (!is_numeric($char_id)) router_error(400); if (!is_numeric($char_id)) error_response(400);
// Ensure the character ID is valid and belongs to the user. // Ensure the character ID is valid and belongs to the user.
if (!Character::belongs_to($char_id, user()->id)) router_error(999); if (!Character::belongs_to($char_id, user()->id)) error_response(999);
$char = Character::find($char_id); $char = Character::find($char_id);
@ -154,7 +154,7 @@ function char_controller_create_post()
} }
} }
if (($char = Character::create(user()->id, $name)) === false) router_error(400); if (($char = Character::create(user()->id, $name)) === false) error_response(400);
// Create the auxiliary tables // Create the auxiliary tables
$char->create_location(); $char->create_location();

View File

@ -18,7 +18,7 @@ function profile_controller_show_get($id)
{ {
auth_only_and_must_have_character(); auth_only_and_must_have_character();
if (($char = Character::find($id)) == false) router_error(999); if (($char = Character::find($id)) == false) error_response(999);
if (user()->char_id == $id) redirect('/profile'); if (user()->char_id == $id) redirect('/profile');
echo page('profile/show', ['c' => $char]); echo page('profile/show', ['c' => $char]);
} }

View File

@ -45,7 +45,7 @@ function world_controller_move_post()
$x += directions[$d][0]; $x += directions[$d][0];
$y += directions[$d][1]; $y += directions[$d][1];
} else { } else {
router_error(999); error_response(999);
} }
$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', [

View File

@ -75,7 +75,7 @@ function csrf_field()
*/ */
function csrf_ensure() function csrf_ensure()
{ {
if (!csrf_verify($_POST['csrf'] ?? '')) router_error(418); if (!csrf_verify($_POST['csrf'] ?? '')) error_response(418);
} }
/** /**
@ -121,8 +121,8 @@ function modify_user_session(string $field, mixed $value): bool
function char(): Character|false function char(): Character|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'])) $GLOBALS['char'] = user()->current_char();
return unserialize($GLOBALS['char']); return $GLOBALS['char'];
} }
/** /**
@ -133,7 +133,7 @@ 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 = Character::find($char_id)) === false) return false; if (($char = Character::find($char_id)) === false) return false;
$GLOBALS['char'] = serialize($char); $GLOBALS['char'] = $char;
// If the character ID is different, update the session and database // If the character ID is different, update the session and database
if (user()->char_id !== $char_id) { if (user()->char_id !== $char_id) {
@ -218,17 +218,17 @@ function stopwatch_stop($key)
/** /**
* Get the stopwatch value and format it to within 10 digits. * Get the stopwatch value and format it to within 10 digits.
*/ */
function stopwatch_get($key) function stopwatch_get(string $key): string
{ {
if (!env('debug', false)) return; if (!env('debug', false)) return '';
if (empty($GLOBALS['stopwatch'][$key])) return 0; if (empty($GLOBALS['stopwatch'][$key])) return '';
return number_format($GLOBALS['stopwatch'][$key], 10); return number_format($GLOBALS['stopwatch'][$key], 10);
} }
/** /**
* Conditional Echo; if the condition is true, echo the value. If the condition is false, echo the $or value. * Conditional Echo; if the condition is true, echo the value. If the condition is false, echo the $or value.
*/ */
function ce($condition, $value, $or = '') function ce(bool $condition, mixed $value, mixed $or = ''): void
{ {
echo $condition ? $value : $or; echo $condition ? $value : $or;
} }
@ -236,7 +236,7 @@ function ce($condition, $value, $or = '')
/** /**
* Get whether the request is an HTMX request. * Get whether the request is an HTMX request.
*/ */
function is_htmx() function is_htmx(): bool
{ {
return isset($_SERVER['HTTP_HX_REQUEST']); return isset($_SERVER['HTTP_HX_REQUEST']);
} }
@ -244,7 +244,7 @@ function is_htmx()
/** /**
* Get whether the request is an AJAX (fetch) request. * Get whether the request is an AJAX (fetch) request.
*/ */
function is_ajax() function is_ajax(): bool
{ {
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'; return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
} }
@ -252,33 +252,50 @@ function is_ajax()
/** /**
* Limit a request to AJAX only. * Limit a request to AJAX only.
*/ */
function ajax_only() function ajax_only(): void
{ {
if (!is_ajax()) router_error(418); if (!is_ajax()) error_response(418);
} }
/** /**
* Return a JSON response with the given data. * Return a JSON response with the given data.
*/ */
function json_response($data) function json_response(mixed $data): void
{ {
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
echo json_encode($data); echo json_encode($data);
exit; exit;
} }
/**
* Handle an error by setting the response code and echoing an error message
*/
function error_response(int $code): void
{
http_response_code($code);
echo match ($code) {
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
418 => 'I\'m a teapot',
999 => 'Cheating attempt detected',
default => 'Unknown Error',
};
exit;
}
/** /**
* Return a title's [name, lore]. * Return a title's [name, lore].
*/ */
function title($title_id) function title(int $title_id): array|false
{ {
return db_query(db_live(), 'SELECT * FROM titles WHERE id = :i', [':i' => $title_id])->fetchArray(); return db_query(db_live(), 'SELECT * FROM titles WHERE id = ?', [$title_id])->fetchArray();
} }
/** /**
* Abbreviate a number in text notation. * Abbreviate a number in text notation.
*/ */
function abb_num($num) function abb_num(int $num): string
{ {
$d = $num % 100 === 0 ? 0 : 1; $d = $num % 100 === 0 ? 0 : 1;
return match(true) { return match(true) {

View File

@ -36,7 +36,15 @@ class Character
public int $pre; // Precision public int $pre; // Precision
public int $fer; // Ferocity public int $fer; // Ferocity
public int $luck; // Luck public int $luck; // Luck
/**
* Max number of slots this Character has in their inventory.
*/
public int $inv_slots; public int $inv_slots;
/**
* Number of points available to spend on attributes.
*/
public int $att_points; public int $att_points;
public string $bio; public string $bio;
@ -45,6 +53,11 @@ class Character
*/ */
private User $user; private User $user;
/**
* The current title of the Character.
*/
private array $title;
public function __construct(array $data) public function __construct(array $data)
{ {
foreach ($data as $k => $v) { foreach ($data as $k => $v) {
@ -52,7 +65,10 @@ class Character
} }
} }
public static function find(int $id): Character|false /**
* Find a Character by their name or ID.
*/
public static function find(int|string $id): Character|false
{ {
$q = db_query( $q = db_query(
db_live(), db_live(),
@ -63,6 +79,10 @@ class Character
return ($c = $q->fetchArray(SQLITE3_ASSOC)) === false ? false : new Character($c); return ($c = $q->fetchArray(SQLITE3_ASSOC)) === false ? false : new Character($c);
} }
/**
* Create a Character in the database using a User ID and a name. Pass optional overrides to adjust the
* defaults for other fields.
*/
public static function create(int $user_id, string $name, array $overrides = []): Character|false public static function create(int $user_id, string $name, array $overrides = []): Character|false
{ {
// Prep the data and merge in any overrides // Prep the data and merge in any overrides
@ -254,12 +274,14 @@ class Character
*/ */
public function title(): array|false public function title(): array|false
{ {
if (isset($this->title)) return $this->title;
$t = title($this->title_id); $t = title($this->title_id);
$q = db_query( $q = db_query(
db_live(), db_live(),
'SELECT awarded FROM owned_titles WHERE char_id = :c AND title_id = :t LIMIT 1', 'SELECT awarded FROM owned_titles WHERE char_id = ? AND title_id = ? LIMIT 1',
[':c' => $this->id, ':t' => $this->title_id] [$this->id, $this->title_id]
); );
if ($q === false) throw new Exception('Failed to query title. (C::t)'); if ($q === false) throw new Exception('Failed to query title. (C::t)');
@ -267,6 +289,6 @@ class Character
if ($a === false) return false; if ($a === false) return false;
$t['awarded'] = $a['awarded']; // add the awarded date to the title info $t['awarded'] = $a['awarded']; // add the awarded date to the title info
return $t; return $this->title = $t;
} }
} }

View File

@ -3,7 +3,7 @@
/** /**
* Render the logout button's form. * Render the logout button's form.
*/ */
function c_logout_button() function c_logout_button(): string
{ {
return render('components/logout_button'); return render('components/logout_button');
} }
@ -12,7 +12,7 @@ function c_logout_button()
* Render the character bar. Relies on there being a character in the session. Without one, this will return an empty * Render the character bar. Relies on there being a character in the session. Without one, this will return an empty
* string. * string.
*/ */
function c_char_bar() function c_char_bar(): string
{ {
if (char() === false) return ''; if (char() === false) return '';
return render('components/char_bar', ['char' => char()]); return render('components/char_bar', ['char' => char()]);
@ -22,7 +22,7 @@ function c_char_bar()
* Render the left sidebar navigation menu. Provide the active tab to highlight it. Will retrieve the current * Render the left sidebar navigation menu. Provide the active tab to highlight it. Will retrieve the current
* tab from the GLOBALS. * tab from the GLOBALS.
*/ */
function c_left_nav() function c_left_nav(): string
{ {
return render('components/left_nav'); return render('components/left_nav');
} }
@ -30,7 +30,7 @@ function c_left_nav()
/** /**
* Render the right sidebar menu. * Render the right sidebar menu.
*/ */
function c_right_nav() function c_right_nav(): string
{ {
return render('components/right_nav', ['c' => char()]); return render('components/right_nav', ['c' => char()]);
} }
@ -38,7 +38,7 @@ function c_right_nav()
/** /**
* Render the debug query log. * Render the debug query log.
*/ */
function c_debug_query_log() function c_debug_query_log(): string
{ {
return render('components/debug_query_log'); return render('components/debug_query_log');
} }
@ -46,15 +46,15 @@ function c_debug_query_log()
/** /**
* Render the character select radio buttons. * Render the character select radio buttons.
*/ */
function c_char_select_box($id, $char) function c_char_select_box(int $id, array $info): string
{ {
return render('components/char_select_box', ['id' => $id, 'char' => $char]); return render('components/char_select_box', ['id' => $id, 'c' => $info]);
} }
/** /**
* Render an alert with a given type and message. * Render an alert with a given type and message.
*/ */
function c_alert($type, $message) function c_alert(string $type, string $message): string
{ {
$a = $type !== 'danger' ? ' auto-close="5000"' : ''; $a = $type !== 'danger' ? ' auto-close="5000"' : '';
return "<div class=\"alert $type\"$a><div>$message</div> <a alert-close>&times;</a></div>"; return "<div class=\"alert $type\"$a><div>$message</div> <a alert-close>&times;</a></div>";
@ -63,7 +63,7 @@ function c_alert($type, $message)
/** /**
* Renders a danger alert with form errors, if there are any. Add an optional placement id. * Renders a danger alert with form errors, if there are any. Add an optional placement id.
*/ */
function c_form_errors($placement = '') function c_form_errors(string $placement = ''): string
{ {
$errors = $GLOBALS[($placement !== '' ? "form-errors-$placement" : 'form-errors')] ?? false; $errors = $GLOBALS[($placement !== '' ? "form-errors-$placement" : 'form-errors')] ?? false;
if ($errors === false) return ''; if ($errors === false) return '';
@ -78,8 +78,14 @@ function c_form_errors($placement = '')
* Generate a text form field. This component will automatically add the error class if there are errors for this field, * Generate a text form field. This component will automatically add the error class if there are errors for this field,
* depending on the form-errors GLOBAL. Pass an optional form ID to target a specific form GLOBAL. * depending on the form-errors GLOBAL. Pass an optional form ID to target a specific form GLOBAL.
*/ */
function c_form_field($type, $name, $placeholder, $required = false, $autocomplete = "off", $formId = '') function c_form_field(
{ string $type,
string $name,
string $placeholder,
bool $required = false,
string $autocomplete = "off",
string $formId = ''
) {
$errors = $GLOBALS[($formId !== '' ? "form-errors-$formId" : 'form-errors')] ?? false; $errors = $GLOBALS[($formId !== '' ? "form-errors-$formId" : 'form-errors')] ?? false;
$html = "<input type=\"$type\" name=\"$name\" id=\"$name\" placeholder=\"$placeholder\""; $html = "<input type=\"$type\" name=\"$name\" id=\"$name\" placeholder=\"$placeholder\"";
if ($required) $html .= ' required'; if ($required) $html .= ' required';
@ -99,7 +105,15 @@ function c_debug_stopwatch()
/** /**
* Render a profile's stat grid using a character array. * Render a profile's stat grid using a character array.
*/ */
function c_profile_stats($char) function c_profile_stats(Character $char): string
{ {
return render('components/profile_stats', ['char' => $char]); return render('components/profile_stats', ['char' => $char]);
} }
/**
* Render a character's equipped gear for their profile.
*/
function c_equipped_gear(Character $char): string
{
return render('components/equipped_gear', ['char' => $char]);
}

View File

@ -1,13 +1,17 @@
<?php <?php
class Router
{
private array $routes = [];
/** /**
* Add a route to the route tree. The route must be a URI path, and contain dynamic segments * Add a route to the route tree. The route must be a URI path, and contain dynamic segments
* using a colon prefix. (:id, :slug, etc) * using a colon prefix. (:id, :slug, etc)
* *
* Example: * Example:
* `router_add($routes, 'GET', '/posts/:id', function($id) { echo "Viewing post $id"; });` * `$r->add($routes, 'GET', '/posts/:id', function($id) { echo "Viewing post $id"; });`
*/ */
function router_add(&$routes, $method, $route, $handler) public function add(string $method, string $route, callable $handler): Router
{ {
// Expand the route into segments and make dynamic segments into a common placeholder // Expand the route into segments and make dynamic segments into a common placeholder
$segments = array_map(function($segment) { $segments = array_map(function($segment) {
@ -15,7 +19,7 @@ function router_add(&$routes, $method, $route, $handler)
}, explode('/', trim($route, '/'))); }, explode('/', trim($route, '/')));
// Push each segment into the routes array as a node, except if this is the root node // Push each segment into the routes array as a node, except if this is the root node
$node = &$routes; $node = &$this->routes;
foreach ($segments as $segment) { foreach ($segments as $segment) {
// skip an empty segment, which allows us to register handlers for the root node // skip an empty segment, which allows us to register handlers for the root node
if ($segment === '') continue; if ($segment === '') continue;
@ -24,6 +28,8 @@ function router_add(&$routes, $method, $route, $handler)
// Add the handler to the last node // Add the handler to the last node
$node[$method] = $handler; $node[$method] = $handler;
return $this;
} }
/** /**
@ -33,10 +39,10 @@ function router_add(&$routes, $method, $route, $handler)
* *
* @return array ['code', 'handler', 'params'] * @return array ['code', 'handler', 'params']
*/ */
function router_lookup($routes, $method, $uri) public function lookup(string $method, string $uri): array
{ {
// node is a reference to our current location in the node tree // node is a reference to our current location in the node tree
$node = $routes; $node = $this->routes;
// params will hold any dynamic segments we find // params will hold any dynamic segments we find
$params = []; $params = [];
@ -73,59 +79,13 @@ function router_lookup($routes, $method, $uri)
: ['code' => 405, 'handler' => null, 'params' => []]; : ['code' => 405, 'handler' => null, 'params' => []];
} }
/** public function get(string $route, callable $handler): Router
* Register a GET route
*/
function router_get(array &$routes, $route, callable $handler)
{ {
router_add($routes, 'GET', $route, $handler); return $this->add('GET', $route, $handler);
} }
/** public function post(string $route, callable $handler): Router
* Register a POST route
*/
function router_post(array &$routes, $route, callable $handler)
{ {
router_add($routes, 'POST', $route, $handler); return $this->add('POST', $route, $handler);
} }
/**
* Register a PUT route
*/
function router_put(array &$routes, $route, callable $handler)
{
router_add($routes, 'PUT', $route, $handler);
}
/**
* Register a DELETE route
*/
function router_delete(array &$routes, $route, callable $handler)
{
router_add($routes, 'DELETE', $route, $handler);
}
/**
* Register a PATCH route
*/
function router_patch(array &$routes, $route, callable $handler)
{
router_add($routes, 'PATCH', $route, $handler);
}
/**
* Handle a router error by setting the response code and echoing an error message
*/
function router_error($code)
{
http_response_code($code);
echo match ($code) {
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
418 => 'I\'m a teapot',
999 => 'Cheating attempt detected',
default => 'Unknown Error',
};
exit;
} }

View File

@ -1,8 +1,8 @@
<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'] ?> <?= $c['name'] ?>
<span class="badge"><?= $char['level'] ?></span> <span class="badge"><?= $c['level'] ?></span>
<span class="badge green selected">Active</span> <span class="badge green selected">Active</span>
</label> </label>
</div> </div>

View File

@ -0,0 +1,22 @@
<div id="equipped-gear">
<!-- Top tow; gloves, head, amulet -->
<div class="top">
<div><div class="item i-2x2"></div></div>
<div><div class="item i-2x2"></div></div>
<div><div class="item i-1x1"></div></div>
</div>
<!-- Mid row; main hand, chest, off hand -->
<div class="mid">
<div><div class="item i-2x3"></div></div>
<div><div class="item i-2x3"></div></div>
<div><div class="item i-2x3"></div></div>
</div>
<!-- Bot row; ring, boots, relic -->
<div class="bot">
<div><div class="item i-1x1"></div></div>
<div><div class="item i-2x2"></div></div>
<div><div class="item i-1x1"></div></div>
</div>
</div>

View File

@ -54,7 +54,9 @@
<script type="module"> <script type="module">
import Tooltip from '/assets/scripts/tooltip.js'; import Tooltip from '/assets/scripts/tooltip.js';
Tooltip.init(); Tooltip.init({
removalDelay: 0
});
</script> </script>
<script> <script>

View File

@ -9,7 +9,7 @@
<input type="hidden" name="csrf" value="<?= csrf() ?>"> <input type="hidden" name="csrf" value="<?= csrf() ?>">
<div class="my-2 character-select"> <div class="my-2 character-select">
<?php foreach ($chars as $id => $char) echo c_char_select_box($id, $char); ?> <?php foreach ($chars as $i => $c) echo c_char_select_box($i, $c); ?>
<div class="mt-4 buttons"> <div class="mt-4 buttons">
<button type="submit" class="ui button primary" name="action" value="select">Select</button> <button type="submit" class="ui button primary" name="action" value="select">Select</button>

View File

@ -9,6 +9,7 @@
<section class="left"> <section class="left">
<div class="avatar"> <div class="avatar">
<img src="/assets/img/rathalos.webp"> <img src="/assets/img/rathalos.webp">
<img class="border" src="/assets/img/ui/borders/water.webp">
</div> </div>
<?= c_profile_stats(char()) ?> <?= c_profile_stats(char()) ?>
@ -32,12 +33,16 @@
<div class="gear"> <div class="gear">
<h4>Equipped Gear</h4> <h4>Equipped Gear</h4>
@TODO <?= c_equipped_gear(char()) ?>
</div> </div>
<div class="inv"> <div class="inv">
<h4>Inventory</h4> <h4>Inventory</h4>
@TODO <?php
for ($i = 0; $i < char()->inv_slots; $i++) {
echo '<img class="mr-1" src="/assets/img/ui/2x3.png">';
}
?>
</div> </div>
</section> </section>
</div> </div>

View File

@ -9,6 +9,7 @@
<section class="left"> <section class="left">
<div class="avatar"> <div class="avatar">
<img src="/assets/img/rathalos.webp"> <img src="/assets/img/rathalos.webp">
<img class="border" src="/assets/img/ui/borders/water.webp">
</div> </div>
<?= c_profile_stats($c) ?> <?= c_profile_stats($c) ?>
@ -32,12 +33,16 @@
<div class="gear"> <div class="gear">
<h4>Equipped Gear</h4> <h4>Equipped Gear</h4>
@TODO <?= c_equipped_gear($c) ?>
</div> </div>
<div class="inv"> <div class="inv">
<h4>Inventory</h4> <h4>Inventory</h4>
@TODO <?php
for ($i = 0; $i < $c->inv_slots; $i++) {
echo '<img class="mr-1" src="/assets/img/ui/2x3.png">';
}
?>
</div> </div>
</section> </section>
</div> </div>