Compare commits

...

3 Commits

Author SHA1 Message Date
95cee55d0c tooltip, char bar 2024-10-03 16:25:25 -05:00
e32803b7f9 Refactor, update gate methods 2024-10-03 12:36:49 -05:00
a61ac11f60 CSS, char select, gate 2024-10-03 12:02:32 -05:00
32 changed files with 778 additions and 179 deletions

1
.env
View File

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

Binary file not shown.

Binary file not shown.

98
docs/tooltip.md Normal file
View File

@ -0,0 +1,98 @@
# Tooltip
The tooltip library is Isotip, taken and modified by Sharkk! Here is the breakdown of the API.
Configuring a specific tooltip is done via data attributes on an element.
### **`data-tooltip-classname`**
If you'd like to add a classname to the root tooltip element, set it here.
### **`data-tooltip-content`**
This sets the main body content of the tooltip into a `<p>` tag by default with a class of `tooltip-content`. Content is interpreted as plain text by default. To insert html, set the data-tooltip-html attribute to true.
### **`data-tooltip-title`**
This sets the title of the tooltip into a `<p>` tag by default with a class of `tooltip-title`.
### **`data-tooltip-html`**
Setting this to true will force Isotip to try and interpret the content as HTML. If it fails, it will interpret the content as plain text.
### **`data-tooltip-placement`**
This sets the position of the tooltip. Options are `top`, `right`, `bottom`, and `left`. By default, `top` is used for all tooltips.
### **`data-tooltip-container`**
This sets the element that the tooltip will be prepended to. By default, this is the `<body>` element.
Alternatively, programattic creation and destruction of tooltips is available.
### **`data-tooltip-scrollContainer`**
This sets the element that will have a scroll event bound to it. If your tooltip is inside a scrolling element (`overflow:scroll`), you need to add this!.
### **`data-tooltip-autoclose`**
If set to false, the tooltip will *not* close unless you do so programmatically with `isotip.close()`. Normal tooltips will not open until the open one has been closed!
### **`init( config )`**
The init method provides automatic event binding for tooltips. It sets up delegated event listeners for `.tooltip-click`, `.tooltip-hover`, and `.tooltip-focus` for click, mouseover, and focus events respectively. You can pass in an optional config object to overwrite any of the default options.
```javascript
var options = {
html: false, // set to true to always interpret content as HTML
placement: 'top', // default placement of tooltips
container: 'body', // default containing element for the tooltip
scrollContainer: '.scroll-container', // default container for scroll watching
template: '<div class="tooltip" data-tooltip-target="tooltip"></div>', // default template for the tooltip shell
removalDelay: 200, // default number of ms before the tooltip is removed
tooltipOffset: 10, // default number of px the tooltip is offset from the trigger element
windowPadding: { // window bounds for tooltip repositioning
top: 10,
right: 10,
bottom: 10,
left: 10
}
};
Tooltip.init( config );
```
### **`open( trigger, config )`**
The open method will create the tooltip, insert it into the DOM, and position it in relation to it's trigger. The trigger can be an element or a CSS selector. The object to be passed in will serve as a replacement for the data attributes on the trigger.
```javascript
var config = {
className: 'specific-class', // set to add a class to the tooltip
html: false, // set to true to interpret content as HTML
placement: 'top', // where to place the tooltip in relation to the trigger
content: 'Tooltip content', // the content to go into the tooltip,
title: 'Tooltip title', // the text to go in the title, if any
container: document.querySelector('.container'), // the container to append the tooltip to
scrollContainer: document.querySelector('.scroll-container'), // the container to bind the scroll event to
autoClose: false // set to false if you only want to close the tooltip programmatically. Normal tooltips won't open until the open one has been closed!
};
Tooltip.open( '.tooltip', config );
```
### **`close( tooltip )`**
The close method will remove a tooltip from the DOM. The tooltip to remove should be passed in and can be an element or a CSS selector.
```javascript
Tooltip.close( '.tooltip' );
```
### **`positionTooltip( tooltip, trigger, placement )`**
The positionTooltip method will re-evaluate the position of a tooltip in relation to it's trigger element. Only the tooltip and trigger need to be passed in, and placement will default to what's been configured by `init()`. Tooltip and trigger can be either an element or a CSS selector.
```javascript
Tooltip.positionTooltip( '.tooltip', '.tooltip-click', 'left' );
```

View File

@ -0,0 +1,260 @@
@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));
box-shadow: 0 1px 0 1px rgba(255, 255, 255, 0.3) inset, 0 0 0 1px #adb2bb inset;
color: #111111;
padding: 0.5rem 1rem 0.5rem;
text-align: center;
border-radius: 3px;
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);
}
&.badge {
font-size: 10px;
padding: 0.1rem 0.25rem;
}
&.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;
display: grid;
grid-template-columns: 1fr 8fr 1fr;
gap: 2rem;
}
aside#left {
.box {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 0.15rem;
&#nav > a {
display: block;
width: 100%;
padding: 0.5rem 1rem;
border-radius: 0.15rem;
text-decoration: none;
color: black;
transition: color, background-color 0.2s ease;
&:hover, &.active {
color: white;
background-color: black;
}
}
}
}
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;
align-items: center;
padding: 0 1rem;
height: 34px;
color: white;
gap: 1rem;
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;
}
.char-meter {
background-color: black;
height: 16px;
min-width: 100px;
border-radius: 0.1rem;
position: relative;
& > div {
height: 100%;
border-radius: 0.1rem;
overflow: hidden;
&.hp {
background-color: #e57373;
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(139, 0, 0, 0.1));
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
border: 1px solid;
border-color: #d32f2f #c62828 #b71c1c;
}
&.mp {
background-color: #5a9bd4;
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(60, 100, 150, 0.1));
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
border: 1px solid;
border-color: #4a8ab0 #3a7a9c #2a6a88;
}
&.tp {
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;
border: 1px solid;
border-color: #C59F43 #AA8326 #957321;
}
}
}
.tooltip {
background-color: black;
color: white;
border: 1px solid #666;
font-size: 14px;
padding: 0.5rem;
box-shadow: 0 0 0.5rem 0.1rem rgba(0, 0, 0, 0.2);
border-radius: 0.1rem;
text-align: center;
}
.tooltip-trigger {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -12,7 +12,8 @@ $r = [];
Home
*/
router_get($r, '/', function () {
echo render('layouts/basic', ['view' => 'pages/home']);
if (user()) must_have_character();
echo render('layouts/basic', ['view' => 'pages/home', 'activeTab' => nav_tabs['home']]);
});
/*
@ -27,8 +28,10 @@ router_post($r, '/auth/logout', 'auth_controller_logout_post');
/*
Characters
*/
router_get($r, '/characters', 'char_controller_select_get');
router_post($r, '/character/create', 'char_controller_create_post');
router_post($r, '/character/select', 'auth_controller_change_character_post');
router_get($r, '/character/create-first', 'char_controller_create_first_get');
/*
Router

View File

@ -3,7 +3,7 @@
/**
* Checks if the given username already exists.
*/
function auth_usernameExists(string $username): bool
function auth_username_exists(string $username): bool
{
return db_exists(db_auth(), 'users', 'username', $username);
}
@ -11,27 +11,11 @@ function auth_usernameExists(string $username): bool
/**
* Checks if the given email already exists.
*/
function auth_emailExists(string $email): bool
function auth_email_exists(string $email): bool
{
return db_exists(db_auth(), 'users', 'email', $email);
}
/**
* Create a long-lived session for the user.
*/
function auth_rememberMe()
{
$token = token();
$expires = strtotime('+30 days');
$result = db_query(db_auth(), "INSERT INTO sessions (token, user_id, expires) VALUES (:t, :u, :e)", [
':t' => $token,
':u' => $_SESSION['user']['id'],
':e' => $expires
]);
if (!$result) router_error(400);
set_cookie('remember_me', $token, $expires);
}
/**
* 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'].
@ -46,6 +30,7 @@ function auth_check(): bool
$user = user_find($session['user_id']);
unset($user['password']);
$_SESSION['user'] = user_find($session['user_id']);
$_SESSION['char'] = char_find($user['char_id']);
return true;
}
}
@ -57,7 +42,7 @@ function auth_check(): bool
* Ensure a user is logged in, or redirect to the login page. This will also check for a remember me cookie and
* populate the $_SESSION['user'] array.
*/
function auth_ensure(): void
function auth_only(): void
{
if (!auth_check()) redirect('/auth/login');
}
@ -65,7 +50,30 @@ function auth_ensure(): void
/**
* If there is a user logged in, redirect to the home page. Used for when we have a guest-only page.
*/
function auth_guest(): void
function guest_only(): void
{
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 must_have_character(): void
{
// If there is a character selected, make sure the session is up to date.
if ($_SESSION['user']['char_id'] !== 0) {
$char = db_query(db_live(), 'SELECT * FROM characters WHERE id = :c', [':c' => $_SESSION['user']['char_id']])->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')])->fetchArray(SQLITE3_ASSOC);
change_user_character($char['id']);
}
}

View File

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

32
src/components.php Normal file
View File

@ -0,0 +1,32 @@
<?php
const nav_tabs = [
'home' => 0,
'chars' => 1,
];
/**
* Render the logout button's form.
*/
function c_logout_button(): string
{
return render('components/logout_button');
}
/**
* Render the character bar. Relies on there being a character in the session. Without one, this will return an empty
* string.
*/
function c_char_bar(): string
{
if (!char()) return '';
return render('components/char_bar', ['char' => char()]);
}
/**
* Render the left sidebar navigation menu. Provide the active tab to highlight it.
*/
function c_left_nav(int $activeTab): string
{
return render('components/left_nav', ['activeTab' => $activeTab]);
}

View File

@ -5,7 +5,7 @@
*/
function auth_controller_register_get(): void
{
auth_guest();
guest_only();
echo render('layouts/basic', ['view' => 'pages/auth/register']);
}
@ -14,7 +14,7 @@ function auth_controller_register_get(): void
*/
function auth_controller_register_post(): void
{
auth_guest();
guest_only();
csrf_ensure();
$errors = [];
@ -63,14 +63,14 @@ function auth_controller_register_post(): void
/*
A username must be unique.
*/
if (auth_usernameExists($u)) {
if (auth_username_exists($u)) {
$errors['u'][] = 'Username is already taken.';
}
/*
An email must be unique.
*/
if (auth_emailExists($e)) {
if (auth_email_exists($e)) {
$errors['e'][] = 'Email is already taken.';
}
@ -84,7 +84,7 @@ function auth_controller_register_post(): void
if ($user === false) router_error(400);
$_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
{
auth_guest();
guest_only();
echo render('layouts/basic', ['view' => 'pages/auth/login']);
}
@ -101,7 +101,7 @@ function auth_controller_login_get(): void
*/
function auth_controller_login_post(): void
{
auth_guest();
guest_only();
csrf_ensure();
$errors = [];
@ -140,7 +140,20 @@ function auth_controller_login_post(): void
}
$_SESSION['user'] = $user;
if ($_POST['remember'] ?? false) auth_rememberMe();
change_user_character($user['char_id']);
if ($_POST['remember'] ?? false) {
$token = token();
$expires = strtotime('+30 days');
$result = db_query(db_auth(), "INSERT INTO sessions (token, user_id, expires) VALUES (:t, :u, :e)", [
':t' => $token,
':u' => $_SESSION['user']['id'],
':e' => $expires
]);
if (!$result) router_error(400);
set_cookie('remember_me', $token, $expires);
}
redirect('/');
}
@ -152,6 +165,7 @@ function auth_controller_logout_post(): void
csrf_ensure();
session_delete($_SESSION['user']['id']);
unset($_SESSION['user']);
unset($_SESSION['char']);
set_cookie('remember_me', '', 1);
redirect('/');
}
@ -161,16 +175,22 @@ function auth_controller_logout_post(): void
*/
function auth_controller_change_character_post(): void
{
auth_check();
auth_only();
must_have_character();
csrf_ensure();
$char_id = (int) ($_POST['char_id'] ?? 0);
if (char_exists($char_id) === false) router_error(400);
$_SESSION['user']['char_id'] = $char_id;
if (db_query(db_auth(), 'UPDATE users SET char_id = :c WHERE id = :u', [
':c' => $char_id,
':u' => $_SESSION['user']['id']
]) === false) router_error(400);
// If the character ID is the current character, do nothing.
if ($char_id === $_SESSION['user']['char_id']) redirect('/');
// Make sure the character ID is valid.
if (char_exists($char_id) === false) throw new Exception('Invalid character ID. (acccp)');
// Make sure the user owns the character.
if (char_belongs_to_user($char_id, $_SESSION['user']['id']) === false) router_error(999);
change_user_character($char_id);
redirect('/');
}

View File

@ -1,11 +1,37 @@
<?php
/**
* Create a player for the currently logged in user.
* Display a list of characters for the currently logged in user.
*/
function char_controller_select_get(): void
{
auth_only();
must_have_character();
$chars = char_list(user('id'));
echo render('layouts/basic', ['view' => 'pages/chars/select', 'chars' => $chars, 'activeTab' => nav_tabs['chars']]);
}
/**
* Form to create your first character.
*/
function char_controller_create_first_get(): void
{
auth_only();
// 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', 'activeTab' => nav_tabs['chars']]);
}
/**
* Create a character for the currently logged in user.
*/
function char_controller_create_post(): void
{
auth_ensure();
auth_only();
csrf_ensure();
$errors = [];
@ -25,9 +51,9 @@ function char_controller_create_post(): void
}
/*
A player's name must be unique.
A character's name must be unique.
*/
if (char_nameExists($name)) $errors['name'][] = 'Name is already taken.';
if (char_name_exists($name)) $errors['name'][] = 'Name is already taken.';
// If there are errors at this point, send them to the page with errors flashed.
if (!empty($errors)) {
@ -35,33 +61,17 @@ function char_controller_create_post(): void
redirect('/');
}
// Create the player
$player = char_create(user('id'), $name);
if ($player === false) router_error(400);
// Create the character
$char = char_create(user('id'), $name);
if ($char === false) router_error(400);
// Create the auxiliary tables
char_location_create($player);
char_wallet_create($player);
char_gear_create($player);
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;
char_location_create($char);
char_wallet_create($char);
char_gear_create($char);
// Set the character as the user's selected character
change_user_character($char);
redirect('/');
}

View File

@ -5,7 +5,7 @@
*/
function env_load(string $filePath): void
{
if (!file_exists($filePath)) throw new Exception("The .env file does not exist.");
if (!file_exists($filePath)) throw new Exception("The .env file does not exist. (el)");
$lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {

View File

@ -122,7 +122,38 @@ function user(string $field = ''): mixed
/**
* 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'];
}
/**
* 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;
}
/**
* Shorthand to update the user's selected character.
*/
function change_user_character(int $char_id): void
{
$_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_find($char_id);
}
/**
* Get a percent between two ints, rounded to the nearest whole number or return 0.
*/
function percent(int $num, int $denom, int $precision = 4): int
{
if ($denom === 0) return 0;
$p = ($num / $denom) * 100;
return $p < 0 ? 0 : round($p, $precision);
}

View File

@ -1,14 +1,14 @@
<?php
/*
Players 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 player interacts with the game world. Separating the player from
the account allows for multiple players to be associated with a single account, and to prevent concurrency issues
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 player, we want to init all of the related data tables; wallets, inventory, bank, etc.
When creating a character, we want to init all of the related data tables; wallets, inventory, bank, etc.
When retrieving a player, we will get the tables as-needed, to prevent allocating more memory than we need.
When retrieving a character, we will get the tables as-needed, to prevent allocating more memory than we need.
*/
const currently = [
@ -20,9 +20,9 @@ const currently = [
];
/**
* Create a player. 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 player's name must be unique, but this function does not check for
* that. Returns the created player's ID.
* 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(int $user_id, string $name, array $overrides = []): int
{
@ -35,19 +35,19 @@ function char_create(int $user_id, string $name, array $overrides = []): int
$f = implode(', ', $k);
$v = implode(', ', array_map(fn($x) => ":$x", $k));
// Create the player!
// 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 player.');
throw new Exception('Failed to create character. (cc)');
}
// Get the player ID
// Get the character ID
return db_live()->lastInsertRowID();
}
/**
* Create a player's location record. A player's location is where they are in the game world. A player can only be
* in one location at a time. Can define a starting location for the player. Default state is 'Exploring'.
* 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(int $char_id, int $x = 0, int $y = 0, int $currently = 0): void
{
@ -57,12 +57,12 @@ function char_location_create(int $char_id, int $x = 0, int $y = 0, int $current
':y' => $y,
':c' => $currently
]) === false) {
throw new Exception('Failed to create player location.');
throw new Exception('Failed to create character location. (clc)');
}
}
/**
* Creates a player's wallet. A player's wallet is where they store their currencies. Can optionally specify the
* Creates a character's wallet. A character's wallet is where they store their currencies. 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.
*/
@ -73,23 +73,23 @@ function char_wallet_create(int $char_id, int $silver = -1, int $starGems = -1):
':s' => $silver === -1 ? env('start_silver', 10) : $silver,
':sg' => $starGems === -1 ? env('start_star_gems', 0) : $starGems
]) === false) {
throw new Exception('Failed to create player wallet.');
throw new Exception('Failed to create character wallet. (cwc)');
}
}
/**
* Create the player's gear table. A player's gear is where they store their equipped items.
* 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(int $char_id, array $initialGear = []): void
{
if (db_query(db_live(), "INSERT INTO char_gear (char_id) VALUES (:p)", [':p' => $char_id]) === false) {
throw new Exception('Failed to create player gear.');
throw new Exception('Failed to create character gear. (cgc)');
}
}
/**
* Create the player's bank account. The bank stores items and currency, with an interest rate based on
* Create the character's bank account. The bank stores items and currency, with an interest rate based on
* the bank account's tier. The bank account has a limited number of slots, which can be increased by upgrading
* the bank account. The bank account starts with 0 silver and 5 slots.
*/
@ -101,95 +101,73 @@ function char_bank_create(int $char_id, int $slots = 5, int $silver = 0, int $ti
':si' => $silver,
':t' => $tier
]) === false) {
throw new Exception('Failed to create player bank.');
throw new Exception('Failed to create character bank. (cbc)');
}
}
/**
* 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
{
// Get the player
$player = db_query(db_live(), "SELECT * FROM characters WHERE id = :id", [':id' => $char_id])->fetchArray(SQLITE3_ASSOC);
if ($player === false) {
throw new Exception('Character not found.');
}
return $player;
$char = 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. (cf)');
return $char;
}
/**
* Count the number of players associated with an account ID.
* Count the number of characters associated with an account ID.
*/
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);
if ($count === false) {
throw new Exception('Failed to count players.');
}
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 players associated with an account ID.
* Get a an array of id => [name, level] for all characters associated with an account ID.
*/
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]);
if ($stmt === false) throw new Exception('Failed to list players.');
if ($stmt === false) throw new Exception('Failed to list characters. (cl)');
$players = [];
$characters = [];
while ($row = $stmt->fetchArray(SQLITE3_ASSOC)) {
$players[$row['id']] = ['name' => $row['name'], 'level' => $row['level']];
$characters[$row['id']] = ['name' => $row['name'], 'level' => $row['level']];
}
return $players;
return $characters;
}
/**
* Get a player's location info by their player ID. Returns the location's data as an associative array.
* Get a character's location info by their character ID. Returns the location's data as an associative array.
*/
function char_get_location(int $char_id): array
{
// 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.');
}
if ($location === false) throw new Exception('Location not found. (cgl)');
return $location;
}
/**
* Get a player's wallet by their player ID. Returns the wallet's data as an associative array.
* Get a character's wallet by their character ID. Returns the wallet's data as an associative 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);
if ($wallet === false) {
throw new Exception('Wallet not found.');
}
if ($wallet === false) throw new Exception('Wallet not found. (cgw)');
return $wallet;
}
/**
* See if a player name exists.
* See if a character name exists.
*/
function char_nameExists(string $name): bool
function char_name_exists(string $name): bool
{
// Check for the name
$exists = db_query(db_live(), "SELECT COUNT(*) FROM characters WHERE name = :n", [':n' => $name])->fetchArray(SQLITE3_NUM);
if ($exists === false) {
throw new Exception('Failed to check for player name.');
}
return (int) $exists[0] > 0;
return db_exists(db_live(), 'characters', 'name', $name);
}
/**
@ -199,3 +177,13 @@ function char_exists(int $char_id): bool
{
return db_exists(db_live(), 'characters', 'id', $char_id);
}
/**
* See if the given character belongs to the given user.
*/
function char_belongs_to_user(int $char_id, int $user_id): bool
{
$char = db_query(db_live(), "SELECT user_id FROM characters WHERE id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC);
if ($char === false) throw new Exception('Character not found. (cbtu)');
return $char['user_id'] === $user_id;
}

View File

@ -124,6 +124,7 @@ function router_error(int $code): void
404 => 'Not Found',
405 => 'Method Not Allowed',
418 => 'I\'m a teapot',
999 => 'Cheating attempt detected',
default => 'Unknown Error',
};
exit;

View File

@ -0,0 +1,30 @@
<div id="char-bar">
<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['attrib_points'] > 0): ?>
<span class="ui button primary badge ml-2 tooltip-hover" data-tooltip-content="Attribute Points"><?= $char['attrib_points'] ?></span>
<?php endif; ?>
</div>
<div>
<div class="char-meter">
<div class="hp" style="width: <?= percent($char['current_hp'], $char['max_hp']) ?>%"></div>
<div class="tooltip-trigger tooltip-hover" data-tooltip-content="Health<br><?= $char['current_hp'] ?> / <?= $char['max_hp'] ?>"></div>
</div>
</div>
<div>
<div class="char-meter">
<div class="mp" style="width: <?= percent($char['current_mp'], $char['max_mp']) ?>%"></div>
<div class="tooltip-trigger tooltip-hover" data-tooltip-content="Mana<br><?= $char['current_mp'] ?> / <?= $char['max_mp'] ?>"></div>
</div>
</div>
<div>
<div class="char-meter">
<div class="tp" style="width: <?= percent($char['current_tp'], $char['max_tp']) ?>%"></div>
<div class="tooltip-trigger tooltip-hover" data-tooltip-content="Travel Points<br><?= $char['current_tp'] ?> / <?= $char['max_tp'] ?>"></div>
</div>
</div>
</div>

View File

@ -0,0 +1,4 @@
<div id="nav" class="box">
<a href="/" class="<?= $activeTab === 0 ? 'active' : '' ?>">Home</a>
<a href="/characters" class="<?= $activeTab === 1 ? 'active' : '' ?>">Characters</a>
</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

View File

@ -4,20 +4,54 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dragon Knight</title>
<link rel="stylesheet" href="/assets/css/dragon.css">
</head>
<body>
<div id="dk">
<header>
<div class="left">
<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>
<?= c_char_bar(user('char_id')) ?>
<main>
<aside id="left">
<?php if (user()): ?>
<?= c_left_nav($activeTab ?? 0) ?>
<?php endif; ?>
</aside>
<div id="center">
<?= render($view, $data) ?>
</div>
<aside id="right">
<?php if (user()): ?>
// right nav
<?php endif; ?>
</aside>
</main>
<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>
</div>
<script type="module">
import Tooltip from '/assets/scripts/tooltip.js';
Tooltip.init();
</script>
</body>
</html>

View File

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

View File

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

@ -0,0 +1,16 @@
<h1>Characters</h1>
<?php
$list = char_list(user('id'));
if (count($list) > 0): ?>
<form action="/character/select" method="post">
<input type="hidden" name="csrf" value="<?= csrf() ?>">
<?php foreach ($list as $id => $char): ?>
<input type="radio" name="char_id" value="<?= $id ?>" id="char_<?= $id ?>">
<label for="char_<?= $id ?>"><?= $char['name'] ?> (Level <?= $char['level'] ?>)</label><br>
<?php endforeach; ?>
<input type="submit" value="Select Character">
</form>
<?php else: ?>
<!-- Should never see this particular message. If you have, there's a bug. -->
<p>You have no characters.</p>
<?php endif; ?>

View File

@ -1,32 +1,8 @@
<?php if (!user()): ?>
<h2>Welcome!</h2>
<a href="/auth/register">Register</a>
<a href="/auth/login">Login</a>
<h1>Welcome!</h1>
<a href="/auth/login" class="ui button primary">Login</a>
<a href="/auth/register" class="ui button secondary">Register</a>
<?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): ?>
<h3>Characters</h3>
<form action="character/select" method="post">
<input type="hidden" name="csrf" value="<?= csrf() ?>">
<?php foreach (char_list(user('id')) as $id => $char): ?>
<input type="radio" name="char_id" value="<?= $id ?>" id="char_<?= $id ?>">
<label for="char_<?= $id ?>"><?= $char['name'] ?> (Level <?= $char['level'] ?>)</label><br>
<?php endforeach; ?>
<input type="submit" value="Select Character">
</form>
<?php endif; ?>
<form action="/character/create" method="post">
<input type="hidden" name="csrf" value="<?= csrf() ?>">
<input type="text" name="name" placeholder="Character Name">
<input type="submit" value="Create Character">
</form>
<h1 class="tooltip-click" data-tooltip-content="Hover-based tooltip">Home</h1>
<?= print_r(char()) ?>
<?php endif; ?>