CSS, char select, gate

This commit is contained in:
Sky Johnson 2024-10-03 12:02:32 -05:00
parent 043a57cf86
commit a61ac11f60
24 changed files with 404 additions and 91 deletions

1
.env
View File

@ -1,4 +1,5 @@
debug = true debug = true
version = 0.1.0
open = true open = true
world_size = 250 world_size = 250
exp_modifier = 1 exp_modifier = 1

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,174 @@
@import '/assets/css/forms.css';
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
}
html {
font-size: 16px;
}
body {
background-color: #bcc6cf;
background-image: url('/assets/img/bg.jpg');
background-attachment: fixed;
background-position: center top;
background-repeat: no-repeat;
}
.ui.button {
cursor: pointer;
display: inline-block;
border: none;
font-size: 1rem;
background: #f7f8fa linear-gradient(rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.1));
color: #111111;
margin: 0rem 0.25rem 0rem 0rem;
padding: 0.5rem 1rem 0.5rem;
text-align: center;
border-radius: 3px;
box-shadow: 0 1px 0 1px rgba(255, 255, 255, 0.3) inset, 0 0 0 1px #adb2bb inset;
user-select: none;
text-decoration: none;
transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease;
-webkit-tap-highlight-color: transparent;
&:hover {
background-color: #e0e0e0;
background-image: linear-gradient(rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.1));
box-shadow: 0 1px 0 1px rgba(255, 255, 255, 0.3) inset, 0 0 0 1px #adb2bb inset;
color: rgba(0, 0, 0, 0.8);
}
&.primary {
background-color: #f4cc67;
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
color: #111111;
border: 1px solid;
border-color: #C59F43 #AA8326 #957321;
&:hover {
background-color: #fac847;
border-color: #C59F43 #AA8326 #957321;
}
}
&.secondary {
background-color: #444c55;
color: #ffffff;
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
border: 1px solid;
border-color: #3D444C #2F353B #2C3137;
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
&:hover {
background-color: #4e5964;
border-color: #32373E #24282D #212429;
}
}
}
header {
height: 76px;
color: white;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1rem;
background-image: url('/assets/img/header.jpg');
h1 {
margin: 0;
padding: 0;
}
.right {
display: flex;
align-items: center;
p {
margin-right: 1rem;
}
}
}
main {
padding: 1rem;
}
footer {
display: flex;
justify-content: center;
align-items: center;
margin: 1rem 0;
padding: 1rem;
text-align: center;
color: #666;
& > p:not(:last-child) {
margin-right: 2rem;
}
}
#char-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 1rem;
height: 34px;
color: white;
background-image: url('/assets/img/deco-bar2.jpg');
& > div {
display: flex;
align-items: center;
.icon {
width: 18px;
margin-right: 0.5rem;
}
}
}
span.badge {
font-size: 10px;
background-color: #f7f8fa;
color: #111111;
border-radius: 0.25rem;
padding: 0.1rem 0.25rem;
}
.my-1 { margin-bottom: 0.25rem; margin-top: 0.25rem; }
.my-2 { margin-bottom: 0.5rem; margin-top: 0.5rem; }
.my-3 { margin-bottom: 0.75rem; margin-top: 0.75rem; }
.my-4 { margin-bottom: 1rem; margin-top: 1rem; }
.ml-1 { margin-left: 0.25rem; }
.ml-2 { margin-left: 0.5rem; }
.ml-3 { margin-left: 0.75rem; }
.ml-4 { margin-left: 1rem; }
.mr-1 { margin-right: 0.25rem; }
.mr-2 { margin-right: 0.5rem; }
.mr-3 { margin-right: 0.75rem; }
.mr-4 { margin-right: 1rem; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 0.75rem; }
.mb-4 { margin-bottom: 1rem; }
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-3 { margin-top: 0.75rem; }
.mt-4 { margin-top: 1rem; }
.container-960 {
width: 960px;
margin: 0 auto;
}

View File

@ -0,0 +1,23 @@
.form.control {
appearance: none;
outline: none;
display: block;
width: 100%;
height: 34px;
padding: 6px 12px;
font-size: 14px;
line-height: 1.42857143;
color: #555555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
-webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
}

BIN
public/assets/img/bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -12,6 +12,7 @@ $r = [];
Home Home
*/ */
router_get($r, '/', function () { router_get($r, '/', function () {
if (user()) auth_char_ensure();
echo render('layouts/basic', ['view' => 'pages/home']); echo render('layouts/basic', ['view' => 'pages/home']);
}); });
@ -29,6 +30,7 @@ router_post($r, '/auth/logout', 'auth_controller_logout_post');
*/ */
router_post($r, '/character/create', 'char_controller_create_post'); router_post($r, '/character/create', 'char_controller_create_post');
router_post($r, '/character/select', 'auth_controller_change_character_post'); router_post($r, '/character/select', 'auth_controller_change_character_post');
router_get($r, '/character/create-first', 'char_controller_create_first_get');
/* /*
Router Router

View File

@ -46,6 +46,7 @@ function auth_check(): bool
$user = user_find($session['user_id']); $user = user_find($session['user_id']);
unset($user['password']); unset($user['password']);
$_SESSION['user'] = user_find($session['user_id']); $_SESSION['user'] = user_find($session['user_id']);
$_SESSION['char'] = char_find($user['char_id']);
return true; return true;
} }
} }
@ -69,3 +70,33 @@ function auth_guest(): void
{ {
if (auth_check()) redirect('/'); if (auth_check()) redirect('/');
} }
/**
* Ensure the user has a character selected. If they have no character, redirect to the character creation page. Otherwise,
* select the first character attached to the user.
*/
function auth_char_ensure(): void
{
// If there is a character selected, and the data exists, return early.
if ($_SESSION['user']['char_id'] !== 0 && !empty($_SESSION['char'])) return;
// If there is a character selected, make sure the session is up to date.
if ($_SESSION['user']['char_id'] !== 0 && empty($_SESSION['char'])) {
$char = db_query(db_live(), 'SELECT * FROM characters WHERE id = :c', [':c' => $_SESSION['user']['char_id']]);
$char = $char = $char->fetchArray(SQLITE3_ASSOC);
$_SESSION['char'] = $char;
return;
}
// if no characters, redirect to create first
if (char_count(user('id')) === 0) redirect('/character/create-first');
// if no character selected, select the first one
if ($_SESSION['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')]);
$char = $char->fetchArray(SQLITE3_ASSOC);
$_SESSION['user']['char_id'] = $char['id'];
db_query(db_auth(), 'UPDATE users SET char_id = :c WHERE id = :u', [':c' => $char['id'], ':u' => user('id')]);
$_SESSION['char'] = $char;
}
}

View File

@ -10,6 +10,7 @@ require_once SRC . '/env.php';
require_once SRC . '/database.php'; require_once SRC . '/database.php';
require_once SRC . '/auth.php'; require_once SRC . '/auth.php';
require_once SRC . '/router.php'; require_once SRC . '/router.php';
require_once SRC . '/components.php';
// Database models // Database models
require_once SRC . '/models/user.php'; require_once SRC . '/models/user.php';

19
src/components.php Normal file
View File

@ -0,0 +1,19 @@
<?php
/**
* Render the logout button's form.
*/
function c_logout_button(): string
{
return render('components/logout_button');
}
/**
* Render the character bar; pass either the character data as an array or the character ID as an int.
*/
function c_char_bar(array|int $char): string
{
if (is_int($char)) $char = char_find($char);
if ($char === false) throw new Exception('Character not found for char bar.');
return render('components/char_bar', ['char' => $char]);
}

View File

@ -5,7 +5,7 @@
*/ */
function auth_controller_register_get(): void function auth_controller_register_get(): void
{ {
auth_guest(); gate(false, false);
echo render('layouts/basic', ['view' => 'pages/auth/register']); echo render('layouts/basic', ['view' => 'pages/auth/register']);
} }
@ -14,7 +14,7 @@ function auth_controller_register_get(): void
*/ */
function auth_controller_register_post(): void function auth_controller_register_post(): void
{ {
auth_guest(); gate(false, false);
csrf_ensure(); csrf_ensure();
$errors = []; $errors = [];
@ -84,7 +84,7 @@ function auth_controller_register_post(): void
if ($user === false) router_error(400); if ($user === false) router_error(400);
$_SESSION['user'] = user_find($u); $_SESSION['user'] = user_find($u);
redirect('/'); redirect('/character/create-first');
} }
/** /**
@ -92,7 +92,7 @@ function auth_controller_register_post(): void
*/ */
function auth_controller_login_get(): void function auth_controller_login_get(): void
{ {
auth_guest(); gate(false, false);
echo render('layouts/basic', ['view' => 'pages/auth/login']); echo render('layouts/basic', ['view' => 'pages/auth/login']);
} }
@ -101,7 +101,7 @@ function auth_controller_login_get(): void
*/ */
function auth_controller_login_post(): void function auth_controller_login_post(): void
{ {
auth_guest(); gate(false, false);
csrf_ensure(); csrf_ensure();
$errors = []; $errors = [];
@ -140,6 +140,7 @@ function auth_controller_login_post(): void
} }
$_SESSION['user'] = $user; $_SESSION['user'] = $user;
$_SESSION['char'] = char_find($user['char_id']);
if ($_POST['remember'] ?? false) auth_rememberMe(); if ($_POST['remember'] ?? false) auth_rememberMe();
redirect('/'); redirect('/');
} }
@ -152,6 +153,7 @@ function auth_controller_logout_post(): void
csrf_ensure(); csrf_ensure();
session_delete($_SESSION['user']['id']); session_delete($_SESSION['user']['id']);
unset($_SESSION['user']); unset($_SESSION['user']);
unset($_SESSION['char']);
set_cookie('remember_me', '', 1); set_cookie('remember_me', '', 1);
redirect('/'); redirect('/');
} }
@ -161,7 +163,7 @@ function auth_controller_logout_post(): void
*/ */
function auth_controller_change_character_post(): void function auth_controller_change_character_post(): void
{ {
auth_check(); gate(true);
csrf_ensure(); csrf_ensure();
$char_id = (int) ($_POST['char_id'] ?? 0); $char_id = (int) ($_POST['char_id'] ?? 0);
@ -172,5 +174,8 @@ function auth_controller_change_character_post(): void
':c' => $char_id, ':c' => $char_id,
':u' => $_SESSION['user']['id'] ':u' => $_SESSION['user']['id']
]) === false) router_error(400); ]) === false) router_error(400);
$_SESSION['char'] = char_find($char_id);
redirect('/'); redirect('/');
} }

View File

@ -1,11 +1,24 @@
<?php <?php
/**
* Form to create your first character.
*/
function char_controller_create_first_get(): void
{
gate(false);
// If the user already has a character, redirect them to the main page.
if (char_count(user('id')) > 0) redirect('/');
echo render('layouts/basic', ['view' => 'pages/chars/first']);
}
/** /**
* Create a player for the currently logged in user. * Create a player for the currently logged in user.
*/ */
function char_controller_create_post(): void function char_controller_create_post(): void
{ {
auth_ensure(); gate(false);
csrf_ensure(); csrf_ensure();
$errors = []; $errors = [];
@ -46,22 +59,3 @@ function char_controller_create_post(): void
redirect('/'); redirect('/');
} }
/**
* Change the user's selected character.
*/
function char_controller_select_post(): void
{
auth_ensure();
csrf_ensure();
$char_id = (int) $_POST['char_id'] ?? 0;
// Ensure the character exists and belongs to the user
if (!char_exists($char_id)) router_error(400);
// Update the user's selected character
$_SESSION['user']['char_id'] = $char_id;
redirect('/');
}

View File

@ -122,7 +122,28 @@ function user(string $field = ''): mixed
/** /**
* Check whether the user has selected a character. If so, return the character's ID. * Check whether the user has selected a character. If so, return the character's ID.
*/ */
function char_selected(): int function user_selected_char(): int
{ {
return (int) $_SESSION['user']['char_id']; return (int) $_SESSION['user']['char_id'];
} }
/**
* If the current user has a selected char and the data is in the session, retrieve either the full array of data
* or a specific field.
*/
function char(string $field = ''): mixed
{
if (empty($_SESSION['char'])) return false;
if ($field === '') return $_SESSION['char'];
return $_SESSION['char'][$field] ?? false;
}
/**
* Perform an authentication and optionally a character check. Failing user auth will redirect to the login page. Failing
* the character check will redirect to the character creation page.
*/
function gate(bool $char = false, bool $user = true): void
{
if ($user && !auth_check()) redirect('/auth/login');
if ($char) auth_char_ensure();
}

View File

@ -106,17 +106,13 @@ function char_bank_create(int $char_id, int $slots = 5, int $silver = 0, int $ti
} }
/** /**
* Get a player by their account ID. Returns the player's data as an associative array. * Get a charcter by their ID. Returns the character's data as an associative array.
*/ */
function char_find(int $char_id): array function char_find(int $char_id): array
{ {
// Get the player $char = db_query(db_live(), "SELECT * FROM characters WHERE id = :id", [':id' => $char_id])->fetchArray(SQLITE3_ASSOC);
$player = db_query(db_live(), "SELECT * FROM characters WHERE id = :id", [':id' => $char_id])->fetchArray(SQLITE3_ASSOC); if ($char === false) throw new Exception('Character not found.');
if ($player === false) { return $char;
throw new Exception('Character not found.');
}
return $player;
} }
/** /**
@ -124,12 +120,8 @@ function char_find(int $char_id): array
*/ */
function char_count(int $user_id): int function char_count(int $user_id): int
{ {
// Get the count
$count = db_query(db_live(), "SELECT COUNT(*) FROM characters WHERE user_id = :u", [':u' => $user_id])->fetchArray(SQLITE3_NUM); $count = db_query(db_live(), "SELECT COUNT(*) FROM characters WHERE user_id = :u", [':u' => $user_id])->fetchArray(SQLITE3_NUM);
if ($count === false) { if ($count === false) throw new Exception('Failed to count players.');
throw new Exception('Failed to count players.');
}
return (int) $count[0]; return (int) $count[0];
} }
@ -138,7 +130,6 @@ function char_count(int $user_id): int
*/ */
function char_list(int $user_id): array function char_list(int $user_id): array
{ {
// Get the players
$stmt = db_query(db_live(), "SELECT id, name, level FROM characters WHERE user_id = :u", [':u' => $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 players.'); if ($stmt === false) throw new Exception('Failed to list players.');
@ -157,10 +148,7 @@ function char_get_location(int $char_id): array
{ {
// Get the location // Get the location
$location = db_query(db_live(), "SELECT * FROM char_locations WHERE char_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC); $location = db_query(db_live(), "SELECT * FROM char_locations WHERE char_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC);
if ($location === false) { if ($location === false) throw new Exception('Location not found.');
throw new Exception('Location not found.');
}
return $location; return $location;
} }
@ -169,12 +157,8 @@ function char_get_location(int $char_id): array
*/ */
function char_get_wallet(int $char_id): array function char_get_wallet(int $char_id): array
{ {
// Get the wallet
$wallet = db_query(db_live(), "SELECT * FROM char_wallets WHERE char_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC); $wallet = db_query(db_live(), "SELECT * FROM char_wallets WHERE char_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC);
if ($wallet === false) { if ($wallet === false) throw new Exception('Wallet not found.');
throw new Exception('Wallet not found.');
}
return $wallet; return $wallet;
} }
@ -183,12 +167,8 @@ function char_get_wallet(int $char_id): array
*/ */
function char_nameExists(string $name): bool function char_nameExists(string $name): bool
{ {
// Check for the name
$exists = db_query(db_live(), "SELECT COUNT(*) FROM characters WHERE name = :n", [':n' => $name])->fetchArray(SQLITE3_NUM); $exists = db_query(db_live(), "SELECT COUNT(*) FROM characters WHERE name = :n", [':n' => $name])->fetchArray(SQLITE3_NUM);
if ($exists === false) { if ($exists === false) throw new Exception('Failed to check for player name.');
throw new Exception('Failed to check for player name.');
}
return (int) $exists[0] > 0; return (int) $exists[0] > 0;
} }

View File

@ -0,0 +1,6 @@
<div id="char-bar">
<div>
<img class="icon" src="/assets/img/icons/user1.png" alt="User">
<?= $char['name'] ?> <span class="badge ml-2"><?= $char['level'] ?></span>
</div>
</div>

View File

@ -0,0 +1,4 @@
<form action="/auth/logout" method="post">
<input type="hidden" name="csrf" value="<?= csrf() ?>">
<input type="submit" value="Logout" class="ui button secondary">
</form>

View File

@ -4,20 +4,37 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dragon Knight</title> <title>Dragon Knight</title>
<link rel="stylesheet" href="/assets/css/dragon.css">
</head> </head>
<body> <body>
<div id="dk">
<header> <header>
<div class="left">
<h1>Dragon Knight</h1> <h1>Dragon Knight</h1>
</div>
<div class="right">
<?php if (user()): ?>
<p>Welcome, <?= user('username') ?></p>
<?= c_logout_button() ?>
<?php else: ?>
<a class="ui button primary" href="/auth/login">Login</a>
<a class="ui button secondary" href="/auth/register">Register</a>
<?php endif ?>
</div>
</header> </header>
<?php if (char()) {
echo c_char_bar(user('char_id'));
} ?>
<main> <main>
<?= render($view, $data) ?> <?= render($view, $data) ?>
</main> </main>
<footer> <footer>
<p>&copy; 2024 Dragon Knight</p> <p>&copy; <?= date('Y') ?> Dragon Knight</p>
<p>q<?= $GLOBALS['queries'] ?></p>
<p>v<?= env('version') ?></p>
</footer> </footer>
</div>
</body> </body>
</html> </html>

View File

@ -9,11 +9,21 @@
} }
?> ?>
<div class="container-960">
<h1 class="my-4">Login</h1>
<form action="/auth/login" method="post"> <form action="/auth/login" method="post">
<?= csrf_field() ?> <?= csrf_field() ?>
<input type="text" name="username" placeholder="Username"> <input class="form control mb-1" type="text" name="username" placeholder="Username">
<input type="password" name="password" placeholder="Password"> <input class="form control mb-4" type="password" name="password" placeholder="Password">
<div class="mb-4">
<input type="checkbox" name="remember" id="remember"> <label for="remember">remember me</label> <input type="checkbox" name="remember" id="remember"> <label for="remember">remember me</label>
<input type="submit" value="Login"> </div>
<button class="ui button primary" type="submit">Login</button>
<a href="/auth/register" class="ui button secondary">Register</a>
</form> </form>
</div>

View File

@ -9,11 +9,17 @@
} }
?> ?>
<div class="container-960">
<h1 class="my-4">Register</h1>
<form action="/auth/register" method="post"> <form action="/auth/register" method="post">
<?= csrf_field() ?> <?= csrf_field() ?>
<input type="text" name="username" placeholder="Username"> <input type="text" name="username" placeholder="Username" class="form control mb-1">
<input type="text" name="email" placeholder="Email"> <input type="text" name="email" placeholder="Email" class="form control mb-1">
<input type="password" name="password" placeholder="Password"> <input type="password" name="password" placeholder="Password" class="form control mb-4">
<input type="submit" value="Register">
<button type="submit" class="ui button primary">Register</button>
<a href="/auth/login" class="ui button secondary">Login</a>
</form> </form>
</div>

View File

@ -0,0 +1,30 @@
<?php
$errors = flash('errors');
if ($errors !== false) {
foreach ($errors as $error) {
foreach ($error as $message) {
echo "<p>$message</p>";
}
}
}
?>
<div class="container-960">
<h1 class="my-4">Create Your First Character</h1>
<p class="mb-2">Welcome to Dragon Knight!</p>
<p class="mb-4">
Before you can begin your adventure, you need to make your first character. Pick a name below. You
can create multiple characters later, and there are no classes; feel free to experiment!
</p>
<form action="/character/create" method="post">
<?= csrf_field() ?>
<input class="form control mb-2" type="text" name="name" placeholder="Character Name">
<button class="ui button primary" type="submit">Create</button>
</form>
</div>

View File

@ -1,20 +1,9 @@
<?php if (!user()): ?> <?php if (!user()): ?>
<h2>Welcome!</h2> <h2>Welcome!</h2>
<a href="/auth/register">Register</a>
<a href="/auth/login">Login</a>
<?php else: ?> <?php else: ?>
<h2>Hello, <?= user('username') ?>!</h2>
<?php if (user('char_id') !== 0): ?>
<h3>Playing as <?= char_find(user('char_id'))['name'] ?></h3>
<?php endif; ?>
<form action="/auth/logout" method="post">
<input type="hidden" name="csrf" value="<?= csrf() ?>">
<input type="submit" value="Logout">
</form>
<?php if (char_count(user('id')) > 0): ?> <?php if (char_count(user('id')) > 0): ?>
<h3>Characters</h3> <h3>Characters</h3>
<form action="character/select" method="post"> <form action="/character/select" method="post">
<input type="hidden" name="csrf" value="<?= csrf() ?>"> <input type="hidden" name="csrf" value="<?= csrf() ?>">
<?php foreach (char_list(user('id')) as $id => $char): ?> <?php foreach (char_list(user('id')) as $id => $char): ?>
<input type="radio" name="char_id" value="<?= $id ?>" id="char_<?= $id ?>"> <input type="radio" name="char_id" value="<?= $id ?>" id="char_<?= $id ?>">