Compare commits

..

2 Commits

24 changed files with 712 additions and 135 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -28,7 +28,7 @@ if (!isset($argv[1])) {
}
// make sure it's a valid database
if (!in_array($argv[1], [AUTH, LIVE, FIGHTS, BPS])) {
if (!in_array($argv[1], [AUTH, LIVE, FIGHTS, BPS, 'reset'])) {
eln(red('Invalid database: ') . $argv[1]);
exit(1);
}
@ -47,8 +47,8 @@ $drop = isset($argv[2]) && $argv[2] === '-d';
The Auth database is used to store user information - not player info, but user info.
Usernames, passwords, email, session tokens, etc.
*/
if ($database === AUTH) {
if ($drop) {
if ($database === AUTH || $database === 'reset') {
if ($drop || $database === 'reset') {
unlink(__DIR__ . '/../' . AUTH);
eln(red('Dropped database: ') . 'auth.db');
}
@ -63,6 +63,7 @@ if ($database === AUTH) {
password TEXT NOT NULL,
auth INT NOT NULL DEFAULT 0,
char_id INTEGER NOT NULL DEFAULT 0,
char_slots INTEGER NOT NULL DEFAULT 3,
created DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME DEFAULT CURRENT_TIMESTAMP
)');
@ -76,6 +77,7 @@ if ($database === AUTH) {
token TEXT NOT NULL UNIQUE,
expires INTEGER NOT NULL
)');
$db->exec('CREATE INDEX idx_sessions_user_id ON sessions (user_id)');
created_or_error($r, 'sessions');
@ -86,20 +88,21 @@ if ($database === AUTH) {
token TEXT NOT NULL UNIQUE,
created INTEGER NOT NULL
)');
$db->exec('CREATE INDEX idx_tokens_user_id ON tokens (user_id)');
created_or_error($r, 'tokens');
eln(green('Created database: ') . 'auth.db');
exit(0);
if ($database !== 'reset') exit(0);
}
/*
The Fights database is used to store information about fights.
A fight is a battle between two characters; players or NPCs.
*/
if ($database === FIGHTS) {
if ($drop) {
if ($database === FIGHTS || $database === 'reset') {
if ($drop || $database === 'reset') {
unlink(__DIR__ . '/../' . FIGHTS);
eln(red('Dropped database: ') . 'fights.db');
}
@ -148,6 +151,8 @@ if ($database === FIGHTS) {
created DATETIME DEFAULT CURRENT_TIMESTAMP,
updated DATETIME DEFAULT CURRENT_TIMESTAMP
)');
// create an index for char_id
$db->exec('CREATE INDEX idx_pve_char_id ON pve (char_id)');
created_or_error($r, 'pve');
@ -191,6 +196,10 @@ if ($database === FIGHTS) {
created DATETIME DEFAULT CURRENT_TIMESTAMP,
updated DATETIME DEFAULT CURRENT_TIMESTAMP
)');
// Create an index for char1_id
$db->exec('CREATE INDEX idx_pvp_char1_id ON pvp (char1_id)');
// Create an index for char2_id
$db->exec('CREATE INDEX idx_pvp_char2_id ON pvp (char2_id)');
created_or_error($r, 'pvp');
@ -201,6 +210,8 @@ if ($database === FIGHTS) {
fight_id INTEGER NOT NULL,
info TEXT NOT NULL
)');
// Create an index for fight_id
$db->exec('CREATE INDEX idx_pve_logs_fight_id ON pve_logs (fight_id)');
created_or_error($r, 'pve_logs');
@ -211,19 +222,21 @@ if ($database === FIGHTS) {
fight_id INTEGER NOT NULL,
info TEXT NOT NULL
)');
// Create an index for fight_id
$db->exec('CREATE INDEX idx_pvp_logs_fight_id ON pvp_logs (fight_id)');
created_or_error($r, 'pvp_logs');
eln(green('Created database: ') . 'fights.db');
exit(0);
if ($database !== 'reset') exit(0);
}
/*
The Blueprints database is used to store information about items, weapons, armor, etc.
*/
if ($database === BPS) {
if ($drop) {
if ($database === BPS || $database === 'reset') {
if ($drop || $database === 'reset') {
unlink(__DIR__ . '/../' . BPS);
eln(red('Dropped database: ') . 'blueprints.db');
}
@ -294,20 +307,38 @@ if ($database === BPS) {
eln(green('Created database: ') . 'blueprints.db');
exit(0);
if ($database !== 'reset') exit(0);
}
/*
The Live database is used to store information about players, NPCs, guilds, etc.
*/
if ($database === LIVE) {
if ($drop) {
if ($database === LIVE || $database === 'reset') {
if ($drop || $database === 'reset') {
unlink(__DIR__ . '/../' . LIVE);
eln(red('Dropped database: ') . 'live.db');
}
$db = new SQLite3(__DIR__ . '/../' . LIVE);
// Blog posts
$db->exec('DROP TABLE IF EXISTS blog');
$r = $db->exec('CREATE TABLE blog (
id INTEGER PRIMARY KEY AUTOINCREMENT,
author_id INTEGER NOT NULL,
title TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
content TEXT NOT NULL,
created DATETIME DEFAULT CURRENT_TIMESTAMP,
updated DATETIME DEFAULT CURRENT_TIMESTAMP
)');
// Create an index for author_id
$db->exec('CREATE INDEX idx_blog_author_id ON blog (author_id)');
// Create an index for the slug
$db->exec('CREATE INDEX idx_blog_slug ON blog (slug)');
created_or_error($r, 'blog');
// Characters
$db->exec('DROP TABLE IF EXISTS characters');
$r = $db->exec('CREATE TABLE characters (
@ -322,8 +353,8 @@ if ($database === LIVE) {
max_hp INTEGER NOT NULL DEFAULT 20,
current_mp INTEGER NOT NULL DEFAULT 10,
max_mp INTEGER NOT NULL DEFAULT 10,
current_tp INTEGER NOT NULL DEFAULT 0,
max_tp INTEGER NOT NULL DEFAULT 0,
current_tp INTEGER NOT NULL DEFAULT 1,
max_tp INTEGER NOT NULL DEFAULT 1,
power INTEGER NOT NULL DEFAULT 0,
accuracy INTEGER NOT NULL DEFAULT 0,
penetration INTEGER NOT NULL DEFAULT 0,
@ -337,6 +368,8 @@ if ($database === LIVE) {
inv_slots INTEGER NOT NULL DEFAULT 10,
attrib_points INTEGER NOT NULL DEFAULT 0
)');
// Create an index for user_id
$db->exec('CREATE INDEX idx_characters_user_id ON characters (user_id)');
created_or_error($r, 'characters');
@ -367,31 +400,37 @@ if ($database === LIVE) {
max_hp INTEGER NOT NULL DEFAULT 0,
max_mp INTEGER NOT NULL DEFAULT 0
)');
// Create an index for char_id
$db->exec('CREATE INDEX idx_char_gear_char_id ON char_gear (char_id)');
created_or_error($r, 'char_gear');
// Player inventory
$db->exec('DROP TABLE IF EXISTS char_inventory');
$r = $db->exec('CREATE TABLE inventory (
$r = $db->exec('CREATE TABLE char_inventory (
char_id INTEGER NOT NULL,
item_id INTEGER NOT NULL
)');
// Create an index for char_id
$db->exec('CREATE INDEX idx_inventory_char_id ON char_inventory (char_id)');
created_or_error($r, 'inventory');
created_or_error($r, 'char_inventory');
// Player wallet
$db->exec('DROP TABLE IF EXISTS char_wallets');
$r = $db->exec('CREATE TABLE char_wallets (
char_id INTEGER NOT NULL,
$db->exec('DROP TABLE IF EXISTS wallets');
$r = $db->exec('CREATE TABLE wallets (
user_id INTEGER NOT NULL,
silver INTEGER NOT NULL DEFAULT 10,
stargem INTEGER NOT NULL DEFAULT 0
)');
// Create an index for user_id
$db->exec('CREATE INDEX idx_wallets_user_id ON wallets (user_id)');
created_or_error($r, 'char_wallets');
created_or_error($r, 'wallets');
// Player bank
$db->exec('DROP TABLE IF EXISTS char_bank');
$r = $db->exec('CREATE TABLE bank (
$r = $db->exec('CREATE TABLE char_bank (
char_id INTEGER NOT NULL,
slots INTEGER NOT NULL DEFAULT 5,
silver INTEGER NOT NULL DEFAULT 0,
@ -399,17 +438,23 @@ if ($database === LIVE) {
can_collect INTEGER NOT NULL DEFAULT 1,
last_collected DATETIME DEFAULT CURRENT_TIMESTAMP
)');
// Create an index for char_id
$db->exec('CREATE INDEX idx_bank_char_id ON char_bank (char_id)');
created_or_error($r, 'bank');
created_or_error($r, 'char_bank');
// Banked items
$db->exec('DROP TABLE IF EXISTS char_banked_items');
$r = $db->exec('CREATE TABLE banked_items (
$r = $db->exec('CREATE TABLE char_banked_items (
char_id INTEGER NOT NULL,
item_id INTEGER NOT NULL
)');
// Create an index for char_id
$db->exec('CREATE INDEX idx_banked_items_char_id ON char_banked_items (char_id)');
// Create an index for item_id
$db->exec('CREATE INDEX idx_banked_items_item_id ON char_banked_items (item_id)');
created_or_error($r, 'banked_items');
created_or_error($r, 'char_banked_items');
// Towns
$db->exec('DROP TABLE IF EXISTS towns');
@ -423,6 +468,8 @@ if ($database === LIVE) {
created DATETIME DEFAULT CURRENT_TIMESTAMP,
updated DATETIME DEFAULT CURRENT_TIMESTAMP
)');
// Create an index for the x, y location
$db->exec('CREATE INDEX idx_towns_location ON towns (x, y)');
created_or_error($r, 'towns');
@ -443,6 +490,8 @@ if ($database === LIVE) {
created DATETIME DEFAULT CURRENT_TIMESTAMP,
updated DATETIME DEFAULT CURRENT_TIMESTAMP
)');
// Create an index for the x, y location
$db->exec('CREATE INDEX idx_shops_location ON shops (x, y)');
created_or_error($r, 'shops');
@ -459,6 +508,8 @@ if ($database === LIVE) {
created DATETIME DEFAULT CURRENT_TIMESTAMP,
updated DATETIME DEFAULT CURRENT_TIMESTAMP
)');
// Create an index for the x, y location
$db->exec('CREATE INDEX idx_inns_location ON inns (x, y)');
created_or_error($r, 'inns');
@ -474,6 +525,8 @@ if ($database === LIVE) {
created DATETIME DEFAULT CURRENT_TIMESTAMP,
updated DATETIME DEFAULT CURRENT_TIMESTAMP
)');
// Create an index for leader_id
$db->exec('CREATE INDEX idx_guilds_leader_id ON guilds (leader_id)');
created_or_error($r, 'guilds');
@ -485,6 +538,8 @@ if ($database === LIVE) {
name TEXT NOT NULL,
permissions TEXT NOT NULL
)');
// Create an index for guild_id
$db->exec('CREATE INDEX idx_guild_ranks_guild_id ON guild_ranks (guild_id)');
created_or_error($r, 'guild_ranks');
@ -498,6 +553,10 @@ if ($database === LIVE) {
donated INTEGER NOT NULL DEFAULT 0,
joined DATETIME DEFAULT CURRENT_TIMESTAMP
)');
// Create an index for guild_id
$db->exec('CREATE INDEX idx_guild_members_guild_id ON guild_members (guild_id)');
// Create an index for char_id
$db->exec('CREATE INDEX idx_guild_members_char_id ON guild_members (char_id)');
created_or_error($r, 'guild_members');
@ -514,6 +573,8 @@ if ($database === LIVE) {
created DATETIME DEFAULT CURRENT_TIMESTAMP,
updated DATETIME DEFAULT CURRENT_TIMESTAMP
)');
// Create an index for the x, y location
$db->exec('CREATE INDEX idx_npcs_location ON npcs (x, y)');
created_or_error($r, 'npcs');
@ -524,6 +585,8 @@ if ($database === LIVE) {
town_id INTEGER NOT NULL,
rep INTEGER NOT NULL DEFAULT 0
)');
// Create an index for char_id
$db->exec('CREATE INDEX idx_char_town_rep_char_id ON char_town_rep (char_id)');
created_or_error($r, 'char_town_rep');
@ -568,6 +631,8 @@ if ($database === LIVE) {
trait_id INTEGER NOT NULL,
value INTEGER NOT NULL DEFAULT 0
)');
// Create an index for char_id
$db->exec('CREATE INDEX idx_char_traits_char_id ON char_traits (char_id)');
created_or_error($r, 'char_traits');
@ -579,12 +644,16 @@ if ($database === LIVE) {
y INTEGER NOT NULL,
currently INTEGER NOT NULL DEFAULT 0
)');
// Create an index for char_id
$db->exec('CREATE INDEX idx_char_locations_char_id ON char_locations (char_id)');
// Create an index for x, y location
$db->exec('CREATE INDEX idx_char_locations_location ON char_locations (x, y)');
created_or_error($r, 'char_locations');
eln(green('Created database: ') . 'live.db');
exit(0);
if ($database !== 'reset') exit(0);
}
function created_or_error(bool $result, string $table): void

View File

@ -74,6 +74,19 @@ body {
border-color: #32373E #24282D #212429;
}
}
&.danger {
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;
&:hover {
background-color: #d95c5c;
border-color: #b71c1c #a52727 #8e1f1f;
}
}
}
header {
@ -150,7 +163,7 @@ footer {
height: 34px;
color: white;
gap: 1rem;
background-image: url('/assets/img/deco-bar2.jpg');
background-image: url('/assets/img/bar.jpg');
& > div {
display: flex;
@ -169,6 +182,11 @@ span.badge {
color: #111111;
border-radius: 0.25rem;
padding: 0.1rem 0.25rem;
&.dark {
background-color: #444c55;
color: white;
}
}
.my-1 { margin-bottom: 0.25rem; margin-top: 0.25rem; }
@ -258,3 +276,70 @@ span.badge {
top: 0;
left: 0;
}
#debug-query-log {
padding: 2rem;
font-size: 14px;
color: #666;
font-family: monospace;
}
#center section {
&:not(:last-child) {
padding-bottom: 1rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
margin-bottom: 1rem;
}
}
h1:has(.badge), h2:has(.badge), h3:has(.badge), h4:has(.badge), h5:has(.badge), h6:has(.badge) {
display: flex;
align-items: center;
& > .badge {
margin-left: 0.5rem;
}
}
.alert {
position: relative;
min-height: 1rem;
margin: 1rem 0;
background: #f8f8f9;
padding: 0.5rem 1rem;
line-height: 1.4285rem;
color: rgba(0, 0, 0, .87);
transition: opacity .1s ease, color .1s ease, background .1s ease, box-shadow .1s ease;
border-radius: .28571429rem;
box-shadow: 0 0 0 1px rgba(34, 36, 38, .22) inset, 0 0 0 0 transparent;
&.success {
background-color: #f0f9eb;
color: #2c662d;
border-color: #b3dc9d;
}
&.danger {
background-color: #f9e9eb;
color: #9f3a38;
border-color: #e0b4b4;
}
&.warning {
background-color: #fff8e1;
color: #573a08;
border-color: #f9e79f;
}
&.info {
background-color: #f0f9fb;
color: #2c7fba;
border-color: #b3d7f9;
}
&.dark {
background-color: #f0f0f0;
color: #2c2c2c;
border-color: #b3b3b3;
}
}

View File

@ -21,3 +21,124 @@
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;
}
/*
.radio-block {
& > input[type="radio"] {
display: none;
}
& > label {
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 {
background-color: #444c55;
color: white;
}
}
&.active > label {
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;
& > .badge {
background-color: #f7f8fa;
color: #111111;
}
}
/* When the radio button is checked, change the background color of the label
& > input[type="radio"]:checked + label {
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;
}
/* When the radio button is disabled, show a normal cursor
& > input[type="radio"]:disabled + label {
cursor: default;
}
}
*/
.radio-block-2 {
& > input[type="radio"] {
display: none;
}
& > label {
cursor: pointer;
display: block;
}
}
.character-select > .radio-block {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 0.15rem;
& > input[type="radio"] {
display: none;
}
& > label {
display: flex;
align-items: center;
width: 100%;
border-radius: 0.15rem;
cursor: pointer;
transition: color, background-color 0.2s ease;
padding: 0.5rem;
&:hover {
background-color: black;
color: white;
}
& > .badge {
margin-left: 0.25rem;
}
}
&.active > label {
background-color: black;
color: white;
}
/* When the radio button is checked, change the background color of the label */
& > input[type="radio"]:checked + label {
background-color: #f4cc67;
color: #111111;
}
/* When the radio button is disabled, show a normal cursor */
& > input[type="radio"]:disabled + label {
cursor: default;
}
}

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@ -28,10 +28,11 @@ 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, '/characters', 'char_controller_list_get');
router_post($r, '/characters', 'char_controller_list_post');
router_get($r, '/character/create-first', 'char_controller_create_first_get');
router_post($r, '/character/create', 'char_controller_create_post');
router_post($r, '/character/delete', 'char_controller_delete_post');
/*
Router

View File

@ -63,8 +63,7 @@ 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;
char();
return;
}

View File

@ -19,7 +19,7 @@ function c_logout_button(): string
*/
function c_char_bar(): string
{
if (!char()) return '';
if (char() === false) return '';
return render('components/char_bar', ['char' => char()]);
}
@ -30,3 +30,35 @@ function c_left_nav(int $activeTab): string
{
return render('components/left_nav', ['activeTab' => $activeTab]);
}
/**
* Render the debug query log.
*/
function c_debug_query_log(): string
{
return render('components/debug_query_log');
}
/**
* Render the character select radio buttons.
*/
function c_char_select_box(int $id, array $char): string
{
return render('components/char_select_box', ['id' => $id, 'char' => $char]);
}
/**
* Render an alert with a given type and message.
*/
function c_alert(string $t, string $m): string
{
return "<div class=\"alert $t\">$m</div>";
}
/**
* Generate a form field.
*/
function c_form_field(string $type, string $name, string $id, string $placeholder, bool $required = false): string
{
return render('components/form_field', ['type' => $type, 'name' => $name, 'id' => $id, 'placeholder' => $placeholder, 'required' => $required]);
}

View File

@ -56,7 +56,7 @@ function auth_controller_register_post(): void
// If there are errors at this point, send them to the page with errors flashed.
if (!empty($errors)) {
flash('errors', $errors);
flash('alert-registration', ['errors', $errors]);
redirect('/auth/register');
}
@ -76,7 +76,7 @@ function auth_controller_register_post(): void
// If there are errors at this point, send them to the page with errors flashed.
if (!empty($errors)) {
flash('errors', $errors);
flash('alert-registration', ['errors', $errors]);
redirect('/auth/register');
}
@ -84,6 +84,7 @@ function auth_controller_register_post(): void
if ($user === false) router_error(400);
$_SESSION['user'] = user_find($u);
wallet_create($_SESSION['user']['id']);
redirect('/character/create-first');
}
@ -169,28 +170,3 @@ function auth_controller_logout_post(): void
set_cookie('remember_me', '', 1);
redirect('/');
}
/**
* Changes the user's currently selected character.
*/
function auth_controller_change_character_post(): void
{
auth_only();
must_have_character();
csrf_ensure();
$char_id = (int) ($_POST['char_id'] ?? 0);
// 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

@ -3,14 +3,110 @@
/**
* Display a list of characters for the currently logged in user.
*/
function char_controller_select_get(): void
function char_controller_list_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']]);
echo render('layouts/basic', ['view' => 'pages/chars/list', 'chars' => $chars, 'activeTab' => nav_tabs['chars']]);
}
/**
* Handle an action from the character list page.
*/
function char_controller_list_post(): void
{
auth_only();
must_have_character();
csrf_ensure();
$char_id = (int) ($_POST['char_id'] ?? 0);
$action = $_POST['action'] ?? '';
// 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 the action is not one of the allowed actions, return a 400.
if (!in_array($action, ['select', 'delete'])) router_error(400);
// If the action is to select a character, change the user's selected character.
if ($action === 'select') {
// If the character ID is the current character, do nothing.
if ($char_id === $_SESSION['user']['char_id'] || $char_id === 0) {
flash('info', 'You are already using that character.');
redirect('/characters');
}
// 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'])) router_error(999);
change_user_character($char_id);
flash('success', 'Switched to character ' . char('name') . '!');
}
// If the action is to delete a character, move to the confirmation page.
if ($action === 'delete') {
// Make sure the character ID is valid.
if (char_exists($char_id) === false) throw new Exception('Invalid character ID. (accdp)');
// Make sure the user owns the character.
if (!char_belongs_to_user($char_id, $_SESSION['user']['id'])) router_error(999);
$char = char_find($char_id);
echo render('layouts/basic', ['view' => 'pages/chars/delete', 'char' => $char, 'activeTab' => nav_tabs['chars']]);
return;
}
redirect('/characters');
}
/**
* Delete a character for the currently logged in user.
*/
function char_controller_delete_post(): void
{
auth_only();
must_have_character();
csrf_ensure();
$char_id = (int) ($_POST['char_id'] ?? 0);
$name = $_POST['name'] ?? '';
// If the character ID is not a number, return a 400.
if (!is_numeric($char_id)) router_error(400);
// Make sure the character ID is valid.
if (char_exists($char_id) === false) throw new Exception('Invalid character ID. (acddp)');
// Make sure the user owns the character.
if (!char_belongs_to_user($char_id, $_SESSION['user']['id'])) router_error(999);
$char = char_find($char_id);
// Confirm the name matches the name of the character.
if ($char['name'] !== $_POST['name']) {
flash('error', 'Failed to delete character. Name confirmation did not match.');
redirect('/characters');
}
// Delete the character
char_delete($char_id);
// If the character being deleted is the currently selected character, select the first character.
if ($_SESSION['user']['char_id'] === $char_id) {
$chars = char_list(user('id'));
if (count($chars) > 0) change_user_character($chars[0]['id']);
}
flash('error', 'Character ' . $char['name'] . ' deleted.');
redirect('/characters');
}
/**
@ -57,8 +153,8 @@ function char_controller_create_post(): void
// If there are errors at this point, send them to the page with errors flashed.
if (!empty($errors)) {
flash('errors', $errors);
redirect('/');
flash('alert-cl2', ['errors', $errors]);
redirect('/characters');
}
// Create the character
@ -67,11 +163,12 @@ function char_controller_create_post(): void
// Create the auxiliary tables
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('/');
flash('alert_character_list_1', ['success', 'Character ' . $name . ' created!']);
redirect('/characters');
}

View File

@ -1,11 +1,28 @@
<?php
/**
* Open a connection to a database.
*/
function db_open(string $path): SQLite3
{
$db = new SQLite3($path);
// Increase cache size to 32MB
$db->exec('PRAGMA cache_size = 32000');
// Enable WAL mode
$db->exec('PRAGMA journal_mode = WAL');
// Move temp store to memory
$db->exec('PRAGMA temp_store = MEMORY');
return $db;
}
/**
* Return a connection to the auth database.
*/
function db_auth(): SQLite3
{
return $GLOBALS['db_auth'] ??= new SQLite3(__DIR__ . '/../database/auth.db');
return $GLOBALS['db_auth'] ??= db_open(__DIR__ . '/../database/auth.db');
}
/**
@ -13,7 +30,7 @@ function db_auth(): SQLite3
*/
function db_live(): SQLite3
{
return $GLOBALS['db_live'] ??= new SQLite3(__DIR__ . '/../database/live.db');
return $GLOBALS['db_live'] ??= db_open(__DIR__ . '/../database/live.db');
}
@ -22,7 +39,7 @@ function db_live(): SQLite3
*/
function db_fights(): SQLite3
{
return $GLOBALS['db_fights'] ??= new SQLite3(__DIR__ . '/../database/fights.db');
return $GLOBALS['db_fights'] ??= db_open(__DIR__ . '/../database/fights.db');
}
@ -31,7 +48,7 @@ function db_fights(): SQLite3
*/
function db_blueprints(): SQLite3
{
return $GLOBALS['db_blueprints'] ??= new SQLite3(__DIR__ . '/../database/blueprints.db');
return $GLOBALS['db_blueprints'] ??= db_open(__DIR__ . '/../database/blueprints.db');
}
/**
@ -43,6 +60,7 @@ function db_query(SQLite3 $db, string $query, array $params = []): SQLite3Result
$stmt = $db->prepare($query);
if (!empty($params)) foreach ($params as $key => $value) $stmt->bindValue($key, $value, getSQLiteType($value));
$GLOBALS['queries']++;
db_log($query);
return $stmt->execute();
}
@ -52,6 +70,7 @@ function db_query(SQLite3 $db, string $query, array $params = []): SQLite3Result
function db_exec(SQLite3 $db, string $query): bool
{
$GLOBALS['queries']++;
db_log($query);
return $db->exec($query);
}
@ -61,7 +80,9 @@ function db_exec(SQLite3 $db, string $query): bool
*/
function db_exists(SQLite3 $db, string $table, string $column, mixed $value): bool
{
$result = db_query($db, "SELECT 1 FROM $table WHERE $column = :v LIMIT 1", [':v' => $value]);
$query = "SELECT 1 FROM $table WHERE $column = :v LIMIT 1";
$result = db_query($db, $query, [':v' => $value]);
db_log($query);
return $result->fetchArray(SQLITE3_NUM) !== false;
}
@ -77,3 +98,11 @@ function getSQLiteType(mixed $value): int
default => SQLITE3_TEXT
};
}
/**
* Log the given query string to the db debug log.
*/
function db_log(string $query): void
{
if (env('debug', false)) $GLOBALS['query_log'][] = $query;
}

View File

@ -37,7 +37,7 @@ function redirect(string $location): void
}
/**
* Flash a message to the session, or retrieve an existing flash value.
* Flash data to the session, or retrieve an existing flash value. Returns false if the key does not exist.
*/
function flash(string $key, mixed $value = ''): mixed
{
@ -129,13 +129,23 @@ function user_selected_char(): int
/**
* 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.
* or a specific field. If there is no character data, populate it.
*/
function char(string $field = ''): mixed
{
if (empty($_SESSION['char'])) return false;
if ($field === '') return $_SESSION['char'];
return $_SESSION['char'][$field] ?? false;
// If there is no user, return false
if (empty($_SESSION['user'])) return false;
if (empty($GLOBALS['char'])) {
$GLOBALS['char'] = db_query(
db_live(),
"SELECT * FROM characters WHERE id = :c",
[':c' => user_selected_char()]
)->fetchArray(SQLITE3_ASSOC);
}
if ($field === '') return $GLOBALS['char'];
return $GLOBALS['char'][$field] ?? false;
}
/**
@ -145,7 +155,7 @@ 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);
$GLOBALS['char'] = char_find($char_id);
}
/**
@ -157,3 +167,32 @@ function percent(int $num, int $denom, int $precision = 4): int
$p = ($num / $denom) * 100;
return $p < 0 ? 0 : round($p, $precision);
}
/**
* Access the account wallet. On first execution it will populate $GLOBALS['wallet'] with the wallet data. This way
* the data is up to date with every request without having to query the database every use within, for example, a
* template. Will return false if the field does not exist, or the entire wallet array if no field is specified.
*/
function wallet(string $field = ''): array|int|false
{
if (empty($GLOBALS['wallet'])) {
$GLOBALS['wallet'] = db_query(
db_live(),
"SELECT * FROM wallets WHERE user_id = :u",
[':u' => user('id')]
)->fetchArray(SQLITE3_ASSOC);
}
if ($field === '') return $GLOBALS['wallet'];
return $GLOBALS['wallet'][$field] ?? false;
}
/**
* Format an array of strings to a ul element.
*/
function array_to_ul(array $array): string
{
$html = '';
foreach ($array as $item) $html .= "<li>$item</li>";
return "<ul>$html</ul>";
}

View File

@ -61,22 +61,6 @@ function char_location_create(int $char_id, int $x = 0, int $y = 0, int $current
}
}
/**
* 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.
*/
function char_wallet_create(int $char_id, int $silver = -1, int $starGems = -1): void
{
if (db_query(db_live(), "INSERT INTO char_wallets (char_id, silver, stargem) VALUES (:p, :s, :sg)", [
':p' => $char_id,
':s' => $silver === -1 ? env('start_silver', 10) : $silver,
':sg' => $starGems === -1 ? env('start_star_gems', 0) : $starGems
]) === false) {
throw new Exception('Failed to create character wallet. (cwc)');
}
}
/**
* Create the character's gear table. A character's gear is where they store their equipped items.
* @TODO: implement initial gear
@ -88,23 +72,6 @@ function char_gear_create(int $char_id, array $initialGear = []): void
}
}
/**
* 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.
*/
function char_bank_create(int $char_id, int $slots = 5, int $silver = 0, int $tier = 0): void
{
if (db_query(db_live(), "INSERT INTO char_banks (char_id, slots, silver, tier) VALUES (:p, :s, :si, :t)", [
':p' => $char_id,
':s' => $slots,
':si' => $silver,
':t' => $tier
]) === false) {
throw new Exception('Failed to create character bank. (cbc)');
}
}
/**
* Get a charcter by their ID. Returns the character's data as an associative array.
*/
@ -152,16 +119,6 @@ function char_get_location(int $char_id): array
return $location;
}
/**
* 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
{
$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. (cgw)');
return $wallet;
}
/**
* See if a character name exists.
*/
@ -187,3 +144,89 @@ function char_belongs_to_user(int $char_id, int $user_id): bool
if ($char === false) throw new Exception('Character not found. (cbtu)');
return $char['user_id'] === $user_id;
}
/**
* Delete a character by their ID. This will delete all associated data tables as well.
*/
function char_delete(int $char_id): void
{
// Delete the character
if (db_query(db_live(), "DELETE FROM characters WHERE id = :p", [':p' => $char_id]) === false) {
throw new Exception('Failed to delete character. (cd)');
}
// Get item IDs from the character's inventory
$items = db_query(db_live(), "SELECT item_id FROM char_inventory WHERE char_id = :p", [':p' => $char_id]);
// delete the character's inventory and items
while ($row = $items->fetchArray(SQLITE3_ASSOC)) {
if (db_query(db_live(), "DELETE FROM char_inventory WHERE char_id = :c", [':c' => $char_id]) === false) {
throw new Exception('Failed to delete character inventory. (cd)');
}
if (db_query(db_live(), "DELETE FROM items WHERE id = :p", [':p' => $row['id']]) === false) {
throw new Exception('Failed to delete character item slots. (cd)');
}
}
// Delete the character's location
if (db_query(db_live(), "DELETE FROM char_locations WHERE char_id = :p", [':p' => $char_id]) === false) {
throw new Exception('Failed to delete character location. (cd)');
}
// Delete the character's gear
if (db_query(db_live(), "DELETE FROM char_gear WHERE char_id = :p", [':p' => $char_id]) === false) {
throw new Exception('Failed to delete character gear. (cd)');
}
// Delete the character's bank
if (db_query(db_live(), "DELETE FROM char_bank WHERE char_id = :p", [':p' => $char_id]) === false) {
throw new Exception('Failed to delete character bank. (cd)');
}
// Delete character's banked items
if (db_query(db_live(), "DELETE FROM char_banked_items WHERE char_id = :p", [':p' => $char_id]) === false) {
throw new Exception('Failed to delete character bank items. (cd)');
}
// Delete the user's guild membership
if (db_query(db_live(), "DELETE FROM guild_members WHERE char_id = :p", [':p' => $char_id]) === false) {
throw new Exception('Failed to delete character guild membership. (cd)');
}
// if the character was a guild leader, hand leadership to the next highest ranking member
$guild = db_query(db_live(), "SELECT id FROM guilds WHERE leader_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC);
if ($guild !== false) {
$members = db_query(db_live(), "SELECT char_id FROM guild_members WHERE guild_id = :p ORDER BY rank DESC", [':p' => $guild['id']]);
$newLeader = $members->fetchArray(SQLITE3_ASSOC);
if ($newLeader !== false) {
db_query(db_live(), "UPDATE guilds SET leader_id = :p WHERE id = :g", [':p' => $newLeader['char_id'], ':g' => $guild['id']]);
}
}
// Get a list of all pve fight IDs.
$pve = db_query(db_fights(), "SELECT id FROM pve WHERE char_id = :p", [':p' => $char_id]);
// Get a list of all pvp fight IDs.
$pvp = db_query(db_fights(), "SELECT id FROM pvp WHERE char1_id = :p OR char2_id = :p", [':p' => $char_id]);
// Delete all pve fights
while ($row = $pve->fetchArray(SQLITE3_ASSOC)) {
if (db_query(db_fights(), "DELETE FROM pve WHERE id = :p", [':p' => $row['id']]) === false) {
throw new Exception('Failed to delete pve fight. (cd)');
}
if (db_query(db_fights(), "DELETE FROM pve_logs WHERE fight_id = :p", [':p' => $row['id']]) === false) {
throw new Exception('Failed to delete pve fight logs. (cd)');
}
}
// Delete all pvp fights
while ($row = $pvp->fetchArray(SQLITE3_ASSOC)) {
if (db_query(db_fights(), "DELETE FROM pvp WHERE id = :p", [':p' => $row['id']]) === false) {
throw new Exception('Failed to delete pvp fight. (cd)');
}
if (db_query(db_fights(), "DELETE FROM pvp_logs WHERE fight_id = :p", [':p' => $row['id']]) === false) {
throw new Exception('Failed to delete pvp fight logs. (cd)');
}
}
}

View File

@ -35,3 +35,18 @@ function user_delete(string|int $user): SQLite3Result|false
{
return db_query(db_auth(), "DELETE FROM users WHERE username = :u OR email = :u OR id = :u", [':u' => $user]);
}
/**
* Creates an account wallet. Can optionally specify the starting balances of the wallet. Returns the created wallet's
* ID. If a currency is set to -1, the starting_silver or starting_star_gems fields from the env will be used.
*/
function wallet_create(int $user_id, int $silver = -1, int $starGems = -1): void
{
if (db_query(db_live(), "INSERT INTO wallets (user_id, silver, stargem) VALUES (:u, :s, :sg)", [
':u' => $user_id,
':s' => $silver === -1 ? env('start_silver', 10) : $silver,
':sg' => $starGems === -1 ? env('start_star_gems', 0) : $starGems
]) === false) {
throw new Exception('Failed to create wallet. (wc)');
}
}

View File

@ -27,4 +27,8 @@
<div class="tooltip-trigger tooltip-hover" data-tooltip-content="Travel Points<br><?= $char['current_tp'] ?> / <?= $char['max_tp'] ?>"></div>
</div>
</div>
<div>
<?= wallet('silver') ?> Silver
</div>
</div>

View File

@ -0,0 +1,7 @@
<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' : '' ?>>
<label for="char_<?= $id ?>">
<?= $char['name'] ?>
<span class="badge"><?= $char['level'] ?></span>
</label>
</div>

View File

@ -0,0 +1,5 @@
<div id="debug-query-log">
<h3>Query Log</h3>
<p class="mb-2"><?= $GLOBALS['queries'] ?> queries were executed.</p>
<?php if (!empty($GLOBALS['query_log'])) foreach ($GLOBALS['query_log'] as $query) echo "<p>$query</p>"; ?>
</div>

View File

@ -49,6 +49,8 @@
<p>v<?= env('version') ?></p>
</footer>
<?php if (env('debug', false)) echo c_debug_query_log(); ?>
<script type="module">
import Tooltip from '/assets/scripts/tooltip.js';
Tooltip.init();

View File

@ -0,0 +1,24 @@
<div class="container-960">
<h1 class="my-4">Delete Character</h1>
<p class="mb-2">
<b>Warning!</b> This action is irreversible. Once you delete a character, it is gone forever. All items, banked items and currency,
fight records, and other data associated with the character will be lost.
</p>
<p class="mb-4">
Are you <b>absolutely sure</b> you want to <b>delete this character</b>? If so, type the character's name below to confirm.
Otherwise, click the back button to cancel.
</p>
<form action="/character/delete" method="post">
<?= csrf_field() ?>
<input type="hidden" name="char_id" value="<?= $char['id'] ?>">
<label for="name">Type <b><?= $char['name'] ?></b> below.</label>
<input id="name" class="form control mb-2" type="text" name="name" placeholder="Character Name">
<button class="ui button danger" type="submit">Delete</button>
<a class="ui button" href="/characters">Back</a>
</form>
</div>

View File

@ -0,0 +1,45 @@
<section>
<?php
if (($f = flash('alert_character_list_1')) !== false) echo c_alert($f[0], $f[1]);
?>
<h1>Characters <span class="badge"><?= count($chars) . '/' . user('char_slots') ?></span></h1>
<?php
if (count($chars) > 0): ?>
<form action="/characters" method="post">
<input type="hidden" name="csrf" value="<?= csrf() ?>">
<div class="my-4 character-select">
<?php foreach ($chars as $id => $char) echo c_char_select_box($id, $char); ?>
</div>
<button type="submit" class="ui button primary" name="action" value="select">Select</button>
<button type="submit" class="ui button danger" name="action" value="delete">Delete</button>
</form>
<?php else: ?>
<!-- Should never see this particular message. If you have, there's a bug. -->
<p>You have no characters.</p>
<?php endif; ?>
</section>
<section>
<?php
if (($f = flash('alert_character_list_2')) !== false) echo c_alert($f[0], $f[1]);
if (($f = flash('errors_create_character')) !== false) echo c_alert('danger', array_to_ul($f));
?>
<?php if (user('char_slots') > count($chars)): ?>
<h2>Create a new character</h2>
<?php $num_slots_left = user('char_slots') - count($chars); ?>
<p>You have <b><?= $num_slots_left ?> <?= $num_slots_left === 1 ? 'slot' : 'slots' ?></b> left.</p>
<form action="/character/create" method="post">
<input type="hidden" name="csrf" value="<?= csrf() ?>">
<div class="my-4">
<input class="form control" type="text" name="name" id="name" placeholder="Character name" required>
</div>
<button type="submit" class="ui button secondary">Create</button>
</form>
<?php endif; ?>
</section>

View File

@ -1,16 +0,0 @@
<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; ?>