initial commit
This commit is contained in:
commit
b063740547
9
.env
Normal file
9
.env
Normal file
|
@ -0,0 +1,9 @@
|
|||
debug = true
|
||||
open = true
|
||||
world_size = 250
|
||||
exp_modifier = 1
|
||||
silver_modifier = 1
|
||||
allow_pvp = true
|
||||
allow_registration = true
|
||||
start_silver = 100
|
||||
sp_per_level = 5
|
15
color.php
Normal file
15
color.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* A collection of functions to colorize the output of the terminal.
|
||||
*/
|
||||
|
||||
function c(string $c, string $s): string { return $c . $s . "\033[0m"; }
|
||||
function black(string $s): string { return c("\033[30m", $s); }
|
||||
function red(string $s): string { return c("\033[31m", $s); }
|
||||
function green(string $s): string { return c("\033[32m", $s); }
|
||||
function yellow(string $s): string { return c("\033[33m", $s); }
|
||||
function blue(string $s): string { return c("\033[34m", $s); }
|
||||
function magenta(string $s): string { return c("\033[35m", $s); }
|
||||
function cyan(string $s): string { return c("\033[36m", $s); }
|
||||
function white(string $s): string { return c("\033[37m", $s); }
|
BIN
database/auth.db
Normal file
BIN
database/auth.db
Normal file
Binary file not shown.
BIN
database/blueprints.db
Normal file
BIN
database/blueprints.db
Normal file
Binary file not shown.
BIN
database/fights.db
Normal file
BIN
database/fights.db
Normal file
Binary file not shown.
BIN
database/live.db
Normal file
BIN
database/live.db
Normal file
Binary file not shown.
540
database/scripts/create.php
Normal file
540
database/scripts/create.php
Normal file
|
@ -0,0 +1,540 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
This script is used to create the database and the tables for said database.
|
||||
php create.php <database>
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../color.php';
|
||||
|
||||
const AUTH = 'auth.db';
|
||||
const LIVE = 'live.db';
|
||||
const FIGHTS = 'fights.db';
|
||||
const BPS = 'blueprints.db';
|
||||
|
||||
/**
|
||||
* Echo a string with a newline.
|
||||
*/
|
||||
function eln(string $string): void
|
||||
{
|
||||
echo $string . PHP_EOL;
|
||||
}
|
||||
|
||||
// pick the database to create
|
||||
if (!isset($argv[1])) {
|
||||
eln(red('Missing database name.'));
|
||||
eln(blue('Usage: ') . 'php create.php auth.db|live.db|fight.db|blueprints.db [-d]');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// make sure it's a valid database
|
||||
if (!in_array($argv[1], [AUTH, LIVE, FIGHTS, BPS])) {
|
||||
eln(red('Invalid database: ') . $argv[1]);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$database = $argv[1];
|
||||
// whether the -d flag is set
|
||||
$drop = isset($argv[2]) && $argv[2] === '-d';
|
||||
|
||||
/*
|
||||
================================================================================
|
||||
Databases
|
||||
================================================================================
|
||||
*/
|
||||
|
||||
/*
|
||||
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) {
|
||||
unlink(__DIR__ . '/../' . AUTH);
|
||||
eln(red('Dropped database: ') . 'auth.db');
|
||||
}
|
||||
$db = new SQLite3(__DIR__ . '/../' . AUTH);
|
||||
|
||||
// Users table
|
||||
$db->exec('DROP TABLE IF EXISTS users');
|
||||
$db->exec('CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL,
|
||||
auth INT NOT NULL DEFAULT 0,
|
||||
created DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'users');
|
||||
|
||||
// Sessions table
|
||||
$db->exec('DROP TABLE IF EXISTS sessions');
|
||||
$db->exec('CREATE TABLE sessions (
|
||||
user_id INTEGER NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
expires INTEGER NOT NULL
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'sessions');
|
||||
|
||||
// Verification tokens
|
||||
$db->exec('DROP TABLE IF EXISTS tokens');
|
||||
$db->exec('CREATE TABLE tokens (
|
||||
user_id INTEGER NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
created INTEGER NOT NULL
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'tokens');
|
||||
|
||||
eln(green('Created database: ') . 'auth.db');
|
||||
|
||||
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) {
|
||||
unlink(__DIR__ . '/../' . FIGHTS);
|
||||
eln(red('Dropped database: ') . 'fights.db');
|
||||
}
|
||||
$db = new SQLite3(__DIR__ . '/../' . FIGHTS);
|
||||
|
||||
// PvE fights
|
||||
$db->exec('DROP TABLE IF EXISTS pve');
|
||||
$db->exec('CREATE TABLE pve (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
player_id INTEGER NOT NULL,
|
||||
player_hp INTEGER NOT NULL,
|
||||
player_max_hp INTEGER NOT NULL,
|
||||
player_mp INTEGER NOT NULL,
|
||||
player_max_mp INTEGER NOT NULL,
|
||||
player_power INTEGER NOT NULL,
|
||||
player_toughness INTEGER NOT NULL,
|
||||
player_armor INTEGER NOT NULL,
|
||||
player_precision INTEGER NOT NULL,
|
||||
player_crit INTEGER NOT NULL,
|
||||
player_ferocity INTEGER NOT NULL,
|
||||
player_vitality INTEGER NOT NULL,
|
||||
mob_id INTEGER NOT NULL,
|
||||
mob_level INTEGER NOT NULL,
|
||||
mob_rank INTEGER NOT NULL,
|
||||
mob_hp INTEGER NOT NULL,
|
||||
mob_max_hp INTEGER NOT NULL,
|
||||
mob_mp INTEGER NOT NULL,
|
||||
mob_max_mp INTEGER NOT NULL,
|
||||
mob_power INTEGER NOT NULL,
|
||||
mob_toughness INTEGER NOT NULL,
|
||||
mob_armor INTEGER NOT NULL,
|
||||
mob_precision INTEGER NOT NULL,
|
||||
mob_crit INTEGER NOT NULL,
|
||||
mob_ferocity INTEGER NOT NULL,
|
||||
mob_vitality INTEGER NOT NULL,
|
||||
first_turn INTEGER NOT NULL,
|
||||
turn INTEGER NOT NULL default 1,
|
||||
winner INTEGER NOT NULL default 0,
|
||||
flee INTEGER NOT NULL default 1,
|
||||
escaped INTEGER NOT NULL default 0,
|
||||
created DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'pve');
|
||||
|
||||
// PvP fights
|
||||
$db->exec('DROP TABLE IF EXISTS pvp');
|
||||
$db->exec('CREATE TABLE pvp (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
player1_id INTEGER NOT NULL,
|
||||
player1_hp INTEGER NOT NULL,
|
||||
player1_max_hp INTEGER NOT NULL,
|
||||
player1_mp INTEGER NOT NULL,
|
||||
player1_max_mp INTEGER NOT NULL,
|
||||
player1_power INTEGER NOT NULL,
|
||||
player1_toughness INTEGER NOT NULL,
|
||||
player1_armor INTEGER NOT NULL,
|
||||
player1_precision INTEGER NOT NULL,
|
||||
player1_crit INTEGER NOT NULL,
|
||||
player1_ferocity INTEGER NOT NULL,
|
||||
player1_vitality INTEGER NOT NULL,
|
||||
player2_id INTEGER NOT NULL,
|
||||
player2_hp INTEGER NOT NULL,
|
||||
player2_max_hp INTEGER NOT NULL,
|
||||
player2_mp INTEGER NOT NULL,
|
||||
player2_max_mp INTEGER NOT NULL,
|
||||
player2_power INTEGER NOT NULL,
|
||||
player2_toughness INTEGER NOT NULL,
|
||||
player2_armor INTEGER NOT NULL,
|
||||
player2_precision INTEGER NOT NULL,
|
||||
player2_crit INTEGER NOT NULL,
|
||||
player2_ferocity INTEGER NOT NULL,
|
||||
player2_vitality INTEGER NOT NULL,
|
||||
first_turn INTEGER NOT NULL,
|
||||
turn INTEGER NOT NULL default 1,
|
||||
winner INTEGER NOT NULL default 0,
|
||||
created DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'pvp');
|
||||
|
||||
// PvE fight logs
|
||||
$db->exec('DROP TABLE IF EXISTS pve_logs');
|
||||
$db->exec('CREATE TABLE pve_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
fight_id INTEGER NOT NULL,
|
||||
info TEXT NOT NULL
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'pve_logs');
|
||||
|
||||
// PvP fight logs
|
||||
$db->exec('DROP TABLE IF EXISTS pvp_logs');
|
||||
$db->exec('CREATE TABLE pvp_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
fight_id INTEGER NOT NULL,
|
||||
info TEXT NOT NULL
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'pvp_logs');
|
||||
|
||||
eln(green('Created database: ') . 'fights.db');
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/*
|
||||
The Blueprints database is used to store information about items, weapons, armor, etc.
|
||||
*/
|
||||
if ($database === BPS) {
|
||||
if ($drop) {
|
||||
unlink(__DIR__ . '/../' . BPS);
|
||||
eln(red('Dropped database: ') . 'blueprints.db');
|
||||
}
|
||||
|
||||
$db = new SQLite3(__DIR__ . '/../' . BPS);
|
||||
|
||||
// Items
|
||||
$db->exec('DROP TABLE IF EXISTS items');
|
||||
$db->exec('CREATE TABLE items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
type INTEGER NOT NULL DEFAULT 0,
|
||||
subtype INTEGER NOT NULL DEFAULT 0,
|
||||
slot INTEGER NOT NULL DEFAULT 0,
|
||||
rarity INTEGER NOT NULL DEFAULT 0,
|
||||
value INTEGER NOT NULL DEFAULT 0,
|
||||
consumable INTEGER NOT NULL DEFAULT 0,
|
||||
duration INTEGER NOT NULL DEFAULT 0,
|
||||
durability INTEGER NOT NULL DEFAULT 0,
|
||||
power INTEGER NOT NULL DEFAULT 0,
|
||||
toughness INTEGER NOT NULL DEFAULT 0,
|
||||
armor INTEGER NOT NULL DEFAULT 0,
|
||||
precision INTEGER NOT NULL DEFAULT 0,
|
||||
crit INTEGER NOT NULL DEFAULT 0,
|
||||
ferocity INTEGER NOT NULL DEFAULT 0,
|
||||
vitality INTEGER NOT NULL DEFAULT 0,
|
||||
reqs TEXT NOT NULL DEFAULT "",
|
||||
traits TEXT NOT NULL DEFAULT "",
|
||||
lore TEXT NOT NULL DEFAULT "",
|
||||
created DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'items');
|
||||
|
||||
// Mobs
|
||||
$db->exec('DROP TABLE IF EXISTS mobs');
|
||||
$db->exec('CREATE TABLE mobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
type INTEGER NOT NULL,
|
||||
rank INTEGER NOT NULL,
|
||||
level INTEGER NOT NULL,
|
||||
hp INTEGER NOT NULL,
|
||||
max_hp INTEGER NOT NULL,
|
||||
mp INTEGER NOT NULL,
|
||||
max_mp INTEGER NOT NULL,
|
||||
power INTEGER NOT NULL,
|
||||
toughness INTEGER NOT NULL,
|
||||
armor INTEGER NOT NULL,
|
||||
precision INTEGER NOT NULL,
|
||||
crit INTEGER NOT NULL,
|
||||
ferocity INTEGER NOT NULL,
|
||||
vitality INTEGER NOT NULL,
|
||||
xp INTEGER NOT NULL,
|
||||
silver INTEGER NOT NULL,
|
||||
loot TEXT NOT NULL,
|
||||
lore TEXT NOT NULL,
|
||||
created DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'mobs');
|
||||
|
||||
eln(green('Created database: ') . 'blueprints.db');
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/*
|
||||
The Live database is used to store information about players, NPCs, guilds, etc.
|
||||
*/
|
||||
if ($database === LIVE) {
|
||||
if ($drop) {
|
||||
unlink(__DIR__ . '/../' . LIVE);
|
||||
eln(red('Dropped database: ') . 'live.db');
|
||||
}
|
||||
|
||||
$db = new SQLite3(__DIR__ . '/../' . LIVE);
|
||||
|
||||
// Players
|
||||
$db->exec('DROP TABLE IF EXISTS players');
|
||||
$db->exec('CREATE TABLE players (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
title_id INTEGER NOT NULL DEFAULT,
|
||||
level INTEGER NOT NULL DEFAULT 1,
|
||||
xp INTEGER NOT NULL DEFAULT 0,
|
||||
xp_to_level INTEGER NOT NULL DEFAULT 100,
|
||||
current_hp INTEGER NOT NULL DEFAULT 20,
|
||||
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,
|
||||
power INTEGER NOT NULL DEFAULT 0,
|
||||
toughness INTEGER NOT NULL DEFAULT 0,
|
||||
armor INTEGER NOT NULL DEFAULT 0,
|
||||
precision INTEGER NOT NULL DEFAULT 0,
|
||||
crit INTEGER NOT NULL DEFAULT 0,
|
||||
ferocity INTEGER NOT NULL DEFAULT 0,
|
||||
vitality INTEGER NOT NULL DEFAULT 0,
|
||||
inv_slots INTEGER NOT NULL DEFAULT 10
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'players');
|
||||
|
||||
// Player gear
|
||||
$db->exec('DROP TABLE IF EXISTS player_gear');
|
||||
$db->exec('CREATE TABLE player_gear (
|
||||
player_id INTEGER NOT NULL,
|
||||
head INTEGER NOT NULL DEFAULT 0,
|
||||
chest INTEGER NOT NULL DEFAULT 0,
|
||||
boots INTEGER NOT NULL DEFAULT 0,
|
||||
hands INTEGER NOT NULL DEFAULT 0,
|
||||
main_hand INTEGER NOT NULL DEFAULT 0,
|
||||
off_hand INTEGER NOT NULL DEFAULT 0,
|
||||
rune INTEGER NOT NULL DEFAULT 0,
|
||||
ring INTEGER NOT NULL DEFAULT 0,
|
||||
amulet INTEGER NOT NULL DEFAULT 0,
|
||||
power INTEGER NOT NULL DEFAULT 0,
|
||||
toughness INTEGER NOT NULL DEFAULT 0,
|
||||
armor INTEGER NOT NULL DEFAULT 0,
|
||||
precision INTEGER NOT NULL DEFAULT 0,
|
||||
crit INTEGER NOT NULL DEFAULT 0,
|
||||
ferocity INTEGER NOT NULL DEFAULT 0,
|
||||
vitality INTEGER NOT NULL DEFAULT 0,
|
||||
max_hp INTEGER NOT NULL DEFAULT 0,
|
||||
max_mp INTEGER NOT NULL DEFAULT 0,
|
||||
traits TEXT NOT NULL DEFAULT ""
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'player_gear');
|
||||
|
||||
// Player inventory
|
||||
$db->exec('DROP TABLE IF EXISTS player_inventory');
|
||||
$db->exec('CREATE TABLE inventory (
|
||||
player_id INTEGER NOT NULL,
|
||||
item_id INTEGER NOT NULL
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'inventory');
|
||||
|
||||
// Player wallet
|
||||
$db->exec('DROP TABLE IF EXISTS player_wallet');
|
||||
$db->exec('CREATE TABLE wallet (
|
||||
player_id INTEGER NOT NULL,
|
||||
silver INTEGER NOT NULL DEFAULT 10,
|
||||
stargem INTEGER NOT NULL DEFAULT 0
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'wallet');
|
||||
|
||||
// Player bank
|
||||
$db->exec('DROP TABLE IF EXISTS player_bank');
|
||||
$db->exec('CREATE TABLE bank (
|
||||
player_id INTEGER NOT NULL,
|
||||
slots INTEGER NOT NULL DEFAULT 5,
|
||||
silver INTEGER NOT NULL DEFAULT 0,
|
||||
tier INTEGER NOT NULL DEFAULT 1,
|
||||
interest INTEGER NOT NULL DEFAULT 0
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'bank');
|
||||
|
||||
// Banked items
|
||||
$db->exec('DROP TABLE IF EXISTS player_banked_items');
|
||||
$db->exec('CREATE TABLE banked_items (
|
||||
player_id INTEGER NOT NULL,
|
||||
item_id INTEGER NOT NULL
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'banked_items');
|
||||
|
||||
// Towns
|
||||
$db->exec('DROP TABLE IF EXISTS towns');
|
||||
$db->exec('CREATE TABLE towns (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
x INTEGER NOT NULL,
|
||||
y INTEGER NOT NULL,
|
||||
type INTEGER NOT NULL,
|
||||
lore TEXT NOT NULL,
|
||||
created DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'towns');
|
||||
|
||||
// Shops
|
||||
$db->exec('DROP TABLE IF EXISTS shops');
|
||||
$db->exec('CREATE TABLE shops (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
type INTEGER NOT NULL,
|
||||
lore TEXT NOT NULL,
|
||||
x INTEGER NOT NULL,
|
||||
y INTEGER NOT NULL,
|
||||
items TEXT NOT NULL,
|
||||
gear TEXT NOT NULL,
|
||||
materials TEXT NOT NULL,
|
||||
buy_modifier INTEGER NOT NULL DEFAULT 100,
|
||||
sell_modifier INTEGER NOT NULL DEFAULT 100,
|
||||
created DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'shops');
|
||||
|
||||
// Inns
|
||||
$db->exec('DROP TABLE IF EXISTS inns');
|
||||
$db->exec('CREATE TABLE inns (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
type INTEGER NOT NULL,
|
||||
lore TEXT NOT NULL,
|
||||
x INTEGER NOT NULL,
|
||||
y INTEGER NOT NULL,
|
||||
cost INTEGER NOT NULL,
|
||||
created DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'inns');
|
||||
|
||||
// Guilds
|
||||
$db->exec('DROP TABLE IF EXISTS guilds');
|
||||
$db->exec('CREATE TABLE guilds (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
lore TEXT NOT NULL DEFAULT "",
|
||||
leader_id INTEGER NOT NULL,
|
||||
silver INTEGER NOT NULL DEFAULT 0,
|
||||
rep INTEGER NOT NULL DEFAULT 0,
|
||||
created DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'guilds');
|
||||
|
||||
// Guild ranks
|
||||
$db->exec('DROP TABLE IF EXISTS guild_ranks');
|
||||
$db->exec('CREATE TABLE guild_ranks (
|
||||
guild_id INTEGER NOT NULL,
|
||||
rank INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
permissions TEXT NOT NULL
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'guild_ranks');
|
||||
|
||||
// Guild members
|
||||
$db->exec('DROP TABLE IF EXISTS guild_members');
|
||||
$db->exec('CREATE TABLE guild_members (
|
||||
guild_id INTEGER NOT NULL,
|
||||
player_id INTEGER NOT NULL,
|
||||
rank INTEGER NOT NULL,
|
||||
rep INTEGER NOT NULL DEFAULT 0,
|
||||
donated INTEGER NOT NULL DEFAULT 0,
|
||||
joined DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'guild_members');
|
||||
|
||||
// NPCs
|
||||
$db->exec('DROP TABLE IF EXISTS npcs');
|
||||
$db->exec('CREATE TABLE npcs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
type INTEGER NOT NULL,
|
||||
lore TEXT NOT NULL,
|
||||
conversation TEXT NOT NULL,
|
||||
x INTEGER NOT NULL,
|
||||
y INTEGER NOT NULL,
|
||||
created DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'npcs');
|
||||
|
||||
// Town reputation
|
||||
$db->exec('DROP TABLE IF EXISTS player_town_rep');
|
||||
$db->exec('CREATE TABLE town_rep (
|
||||
player_id INTEGER NOT NULL,
|
||||
town_id INTEGER NOT NULL,
|
||||
rep INTEGER NOT NULL DEFAULT 0
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'town_rep');
|
||||
|
||||
// Items
|
||||
// Items
|
||||
$db->exec('DROP TABLE IF EXISTS items');
|
||||
$db->exec('CREATE TABLE items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 0,
|
||||
rarity INTEGER NOT NULL DEFAULT 0,
|
||||
forged INTEGER NOT NULL DEFAULT 0,
|
||||
quality INTEGER NOT NULL DEFAULT 0,
|
||||
value INTEGER NOT NULL DEFAULT 0,
|
||||
consumable INTEGER NOT NULL DEFAULT 0,
|
||||
duration INTEGER NOT NULL DEFAULT 0,
|
||||
durability INTEGER NOT NULL DEFAULT 0,
|
||||
max_durability INTEGER NOT NULL DEFAULT 0,
|
||||
power INTEGER NOT NULL DEFAULT 0,
|
||||
toughness INTEGER NOT NULL DEFAULT 0,
|
||||
armor INTEGER NOT NULL DEFAULT 0,
|
||||
precision INTEGER NOT NULL DEFAULT 0,
|
||||
crit INTEGER NOT NULL DEFAULT 0,
|
||||
ferocity INTEGER NOT NULL DEFAULT 0,
|
||||
vitality INTEGER NOT NULL DEFAULT 0,
|
||||
reqs TEXT NOT NULL DEFAULT "",
|
||||
traits TEXT NOT NULL DEFAULT "",
|
||||
lore TEXT NOT NULL DEFAULT "",
|
||||
created DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)');
|
||||
|
||||
eln(yellow('Created table: ') . 'items');
|
||||
|
||||
eln(green('Created database: ') . 'live.db');
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
4
docs/TODO.md
Normal file
4
docs/TODO.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
# TODO
|
||||
Currently, everything needs implemented.
|
||||
|
||||
First task is to finish building up the database structures.
|
3
docs/items.md
Normal file
3
docs/items.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Items
|
||||
|
||||
Items consists of all item types in the game; useless flavor items, gear, consumables, etc.
|
23
public/index.php
Normal file
23
public/index.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
define('SRC', __DIR__ . '/../src');
|
||||
require_once SRC . '/bootstrap.php';
|
||||
|
||||
$r = [];
|
||||
|
||||
router_get($r, '/', function () {
|
||||
echo render('layouts/basic', ['view' => 'pages/home']);
|
||||
});
|
||||
|
||||
router_get($r, '/auth/register', 'auth_register_get');
|
||||
router_post($r, '/auth/register', 'auth_register_post');
|
||||
router_get($r, '/auth/login', 'auth_login_get');
|
||||
router_post($r, '/auth/login', 'auth_login_post');
|
||||
router_post($r, '/auth/logout', 'auth_logout');
|
||||
|
||||
// [code, handler, params]
|
||||
$l = router_lookup($r, $_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
|
||||
|
||||
if ($l['code'] !== 200) router_error($l['code']);
|
||||
$l['handler'](...$l['params'] ?? []);
|
||||
clear_flashes();
|
206
src/auth.php
Normal file
206
src/auth.php
Normal file
|
@ -0,0 +1,206 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Checks if the given username already exists.
|
||||
*/
|
||||
function auth_usernameExists(string $username): bool
|
||||
{
|
||||
return db_exists(db_auth(), 'users', 'username', $username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given email already exists.
|
||||
*/
|
||||
function auth_emailExists(string $email): bool
|
||||
{
|
||||
return db_exists(db_auth(), 'users', 'email', $email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the registration page.
|
||||
*/
|
||||
function auth_register_get(): void
|
||||
{
|
||||
echo render('layouts/basic', ['view' => 'pages/auth/register']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the registration form submission.
|
||||
*/
|
||||
function auth_register_post(): void
|
||||
{
|
||||
csrf_ensure();
|
||||
|
||||
$errors = [];
|
||||
|
||||
$u = $_POST['username'] ?? '';
|
||||
$e = $_POST['email'] ?? '';
|
||||
$p = $_POST['password'] ?? '';
|
||||
|
||||
// Trim the input.
|
||||
$u = trim($u);
|
||||
$e = trim($e);
|
||||
|
||||
/*
|
||||
A username is required.
|
||||
A username must be at least 3 characters long and at most 25 characters long.
|
||||
A username must contain only alphanumeric characters and spaces.
|
||||
*/
|
||||
if (empty($u) || strlen($u) < 3 || strlen($u) > 25 || !ctype_alnum(str_replace(' ', '', $u))) {
|
||||
$errors['u'][] = 'Username is required and must be between 3 and 25 characters long and contain only
|
||||
alphanumeric characters and spaces.';
|
||||
}
|
||||
|
||||
/*
|
||||
An email is required.
|
||||
An email must be at most 255 characters long.
|
||||
An email must be a valid email address.
|
||||
*/
|
||||
if (empty($e) || strlen($e) > 255 || !filter_var($e, FILTER_VALIDATE_EMAIL)) {
|
||||
$errors['e'][] = 'Email is required must be a valid email address.';
|
||||
}
|
||||
|
||||
/*
|
||||
A password is required.
|
||||
A password must be at least 6 characters long.
|
||||
*/
|
||||
if (empty($p) || strlen($p) < 6) {
|
||||
$errors['p'][] = 'Password is required and must be at least 6 characters long.';
|
||||
}
|
||||
|
||||
// If there are errors at this point, send them to the page with errors flashed.
|
||||
if (!empty($errors)) {
|
||||
flash('errors', $errors);
|
||||
redirect('/auth/register');
|
||||
}
|
||||
|
||||
/*
|
||||
A username must be unique.
|
||||
*/
|
||||
if (auth_usernameExists($u)) {
|
||||
$errors['u'][] = 'Username is already taken.';
|
||||
}
|
||||
|
||||
/*
|
||||
An email must be unique.
|
||||
*/
|
||||
if (auth_emailExists($e)) {
|
||||
$errors['e'][] = 'Email is already taken.';
|
||||
}
|
||||
|
||||
// If there are errors at this point, send them to the page with errors flashed.
|
||||
if (!empty($errors)) {
|
||||
flash('errors', $errors);
|
||||
redirect('/auth/register');
|
||||
}
|
||||
|
||||
$user = user_create($u, $e, $p);
|
||||
if ($user === false) router_error(400);
|
||||
|
||||
$_SESSION['user'] = user_find($u);
|
||||
redirect('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the login page.
|
||||
*/
|
||||
function auth_login_get(): void
|
||||
{
|
||||
echo render('layouts/basic', ['view' => 'pages/auth/login']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the login form submission.
|
||||
*/
|
||||
function auth_login_post(): void
|
||||
{
|
||||
csrf_ensure();
|
||||
|
||||
$errors = [];
|
||||
|
||||
$u = $_POST['username'] ?? '';
|
||||
$p = $_POST['password'] ?? '';
|
||||
|
||||
// Trim the input.
|
||||
$u = trim($u);
|
||||
|
||||
/*
|
||||
A username is required.
|
||||
*/
|
||||
if (empty($u)) {
|
||||
$errors['u'][] = 'Username is required.';
|
||||
}
|
||||
|
||||
/*
|
||||
A password is required.
|
||||
*/
|
||||
if (empty($p)) {
|
||||
$errors['p'][] = 'Password is required.';
|
||||
}
|
||||
|
||||
// If there are errors at this point, send them to the page with errors flashed.
|
||||
if (!empty($errors)) {
|
||||
flash('errors', $errors);
|
||||
redirect('/auth/login');
|
||||
}
|
||||
|
||||
$user = user_find($u);
|
||||
if ($user === false || !password_verify($p, $user['password'])) {
|
||||
$errors['u'][] = 'Invalid username or password.';
|
||||
flash('errors', $errors);
|
||||
redirect('/auth/login');
|
||||
}
|
||||
|
||||
$_SESSION['user'] = $user;
|
||||
if ($_POST['remember'] ?? false) auth_rememberMe();
|
||||
redirect('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the user out.
|
||||
*/
|
||||
function auth_logout(): void
|
||||
{
|
||||
csrf_ensure();
|
||||
session_delete($_SESSION['user']['id']);
|
||||
unset($_SESSION['user']);
|
||||
set_cookie('remember_me', '', 1);
|
||||
redirect('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'].
|
||||
*/
|
||||
function auth_check(): bool
|
||||
{
|
||||
if (isset($_SESSION['user'])) return true;
|
||||
|
||||
if (isset($_COOKIE['remember_me'])) {
|
||||
$session = session_validate($_COOKIE['remember_me']);
|
||||
if ($session === true) {
|
||||
$user = user_find($session['user_id']);
|
||||
unset($user['password']);
|
||||
$_SESSION['user'] = user_find($session['user_id']);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
34
src/bootstrap.php
Normal file
34
src/bootstrap.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
session_start();
|
||||
|
||||
// SRC is defined as the path to the src/ directory from public/
|
||||
|
||||
// Source libraries
|
||||
require_once SRC . '/helpers.php';
|
||||
require_once SRC . '/env.php';
|
||||
require_once SRC . '/database.php';
|
||||
require_once SRC . '/auth.php';
|
||||
require_once SRC . '/router.php';
|
||||
|
||||
// Database models
|
||||
require_once SRC . '/models/user.php';
|
||||
require_once SRC . '/models/session.php';
|
||||
require_once SRC . '/models/token.php';
|
||||
|
||||
/*
|
||||
Load env, set error reporting, etc.
|
||||
*/
|
||||
env_load(SRC . '/../.env');
|
||||
|
||||
if (env('debug') === 'true') {
|
||||
ini_set('display_errors', '1');
|
||||
ini_set('display_startup_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
}
|
||||
|
||||
// Generate a new CSRF token. (if one doesn't exist, that is)
|
||||
csrf();
|
||||
|
||||
// Have a global counter for queries
|
||||
$GLOBALS['queries'] = 0;
|
79
src/database.php
Normal file
79
src/database.php
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Return a connection to the auth database.
|
||||
*/
|
||||
function db_auth(): SQLite3
|
||||
{
|
||||
return $GLOBALS['db_auth'] ??= new SQLite3(__DIR__ . '/../database/auth.db');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a connection to the live database.
|
||||
*/
|
||||
function db_live(): SQLite3
|
||||
{
|
||||
return $GLOBALS['db_live'] ??= new SQLite3(__DIR__ . '/../database/live.db');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a connection to the fights database.
|
||||
*/
|
||||
function db_fights(): SQLite3
|
||||
{
|
||||
return $GLOBALS['db_fights'] ??= new SQLite3(__DIR__ . '/../database/fights.db');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a connection to the blueprints database.
|
||||
*/
|
||||
function db_blueprints(): SQLite3
|
||||
{
|
||||
return $GLOBALS['db_blueprints'] ??= new SQLite3(__DIR__ . '/../database/blueprints.db');
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a SQLite3 database connection, a query string, and an array of parameters. Prepare the query and
|
||||
* bind the parameters with proper type casting. Then execute the query and return the result.
|
||||
*/
|
||||
function db_query(SQLite3 $db, string $query, array $params = []): SQLite3Result|false
|
||||
{
|
||||
$stmt = $db->prepare($query);
|
||||
if (!empty($params)) foreach ($params as $key => $value) $stmt->bindValue($key, $value, getSQLiteType($value));
|
||||
$GLOBALS['queries']++;
|
||||
return $stmt->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a SQLite3 database connection and a query string. Execute the query and return the result.
|
||||
*/
|
||||
function db_exec(SQLite3 $db, string $query): bool
|
||||
{
|
||||
$GLOBALS['queries']++;
|
||||
return $db->exec($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a SQLite3 database connection, a column name, and a value. Execute a COUNT query to see if the value
|
||||
* exists in the column. Return true if the value exists, false otherwise.
|
||||
*/
|
||||
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]);
|
||||
return $result->fetchArray(SQLITE3_NUM) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the appropriate SQLite type casting for the value.
|
||||
*/
|
||||
function getSQLiteType(mixed $value): int
|
||||
{
|
||||
return match (true) {
|
||||
is_int($value) => SQLITE3_INTEGER,
|
||||
is_float($value) => SQLITE3_FLOAT,
|
||||
is_null($value) => SQLITE3_NULL,
|
||||
default => SQLITE3_TEXT
|
||||
};
|
||||
}
|
40
src/env.php
Normal file
40
src/env.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Load the environment variables from the .env file.
|
||||
*/
|
||||
function env_load(string $filePath): void
|
||||
{
|
||||
if (!file_exists($filePath)) throw new Exception("The .env file does not exist.");
|
||||
|
||||
$lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
|
||||
// Skip lines that are empty after trimming or are comments
|
||||
if ($line === '' || str_starts_with($line, '#')) continue;
|
||||
|
||||
// Skip lines without an '=' character
|
||||
if (strpos($line, '=') === false) continue;
|
||||
|
||||
[$name, $value] = explode('=', $line, 2);
|
||||
|
||||
$name = trim($name);
|
||||
$value = trim($value, " \t\n\r\0\x0B\"'"); // Trim whitespace and quotes
|
||||
|
||||
if (!array_key_exists($name, $_SERVER) && !array_key_exists($name, $_ENV)) {
|
||||
putenv("$name=$value");
|
||||
$_ENV[$name] = $value;
|
||||
$_SERVER[$name] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve an environment variable.
|
||||
*/
|
||||
function env(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key) ?? $default;
|
||||
}
|
110
src/helpers.php
Normal file
110
src/helpers.php
Normal file
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Return the path to a view file.
|
||||
*/
|
||||
function template(string $name): string
|
||||
{
|
||||
return __DIR__ . "/../templates/$name.php";
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a view with the given data. Looks for `$view` through `template()`.
|
||||
*/
|
||||
function render(string $pathToBaseView, array $data = []): string|false
|
||||
{
|
||||
ob_start();
|
||||
extract($data);
|
||||
require template($pathToBaseView);
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a pretty dope token.
|
||||
*/
|
||||
function token(int $length = 32): string
|
||||
{
|
||||
return bin2hex(random_bytes($length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to a new location.
|
||||
*/
|
||||
function redirect(string $location): void
|
||||
{
|
||||
header("Location: $location");
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flash a message to the session, or retrieve an existing flash value.
|
||||
*/
|
||||
function flash(string $key, mixed $value = ''): mixed
|
||||
{
|
||||
if ($value === '') return $_SESSION["flash_$key"] ?? false;
|
||||
$_SESSION["flash_$key"] = $value;
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all flash messages.
|
||||
*/
|
||||
function clear_flashes(): void
|
||||
{
|
||||
foreach ($_SESSION as $key => $_) {
|
||||
if (str_starts_with($key, 'flash_')) unset($_SESSION[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a CSRF token.
|
||||
*/
|
||||
function csrf(): string
|
||||
{
|
||||
if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = token();
|
||||
return $_SESSION['csrf'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a CSRF token.
|
||||
*/
|
||||
function csrf_verify(string $token): bool
|
||||
{
|
||||
if (hash_equals($_SESSION['csrf'] ?? '', $token)) {
|
||||
$_SESSION['csrf'] = token();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a hidden input field for CSRF tokens.
|
||||
*/
|
||||
function csrf_field(): string
|
||||
{
|
||||
return '<input type="hidden" name="csrf" value="' . csrf() . '">';
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill the current request with a 418 error, if $_POST['csrf'] is invalid.
|
||||
*/
|
||||
function csrf_ensure(): void
|
||||
{
|
||||
if (!csrf_verify($_POST['csrf'] ?? '')) router_error(418);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a cookie with secure and HTTP-only flags.
|
||||
*/
|
||||
function set_cookie(string $name, string $value, int $expires): void
|
||||
{
|
||||
setcookie($name, $value, [
|
||||
'expires' => $expires,
|
||||
'path' => '/',
|
||||
'domain' => '', // Defaults to the current domain
|
||||
'secure' => true, // Ensure the cookie is only sent over HTTPS
|
||||
'httponly' => true, // Prevent access to cookie via JavaScript
|
||||
'samesite' => 'Strict' // Enforce SameSite=Strict
|
||||
]);
|
||||
}
|
3
src/models/fights.php
Normal file
3
src/models/fights.php
Normal file
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
// @TODO: everything
|
66
src/models/items.php
Normal file
66
src/models/items.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
const item_types = [
|
||||
0 => 'Item',
|
||||
1 => 'Weapon', // Can be one-handed or two-handed, see slot
|
||||
5 => 'Off-Hand',
|
||||
6 => 'Armor',
|
||||
5 => 'Shield',
|
||||
10 => 'Jewelry',
|
||||
11 => 'Rune',
|
||||
12 => 'Potion',
|
||||
13 => 'Food',
|
||||
14 => 'Crafting Material',
|
||||
15 => 'Quest Item',
|
||||
];
|
||||
|
||||
const item_subtypes = [
|
||||
0 => 'None',
|
||||
1 => 'Axe',
|
||||
2 => 'Bow',
|
||||
3 => 'Dagger',
|
||||
4 => 'Mace',
|
||||
5 => 'Polearm',
|
||||
6 => 'Sword',
|
||||
7 => 'Warglaive',
|
||||
8 => 'Staff',
|
||||
9 => 'Fist Weapon',
|
||||
10 => 'Miscellaneous',
|
||||
11 => 'Gun',
|
||||
12 => 'Crossbow',
|
||||
13 => 'Wand',
|
||||
14 => 'Fishing Pole',
|
||||
15 => 'Thrown',
|
||||
16 => 'Shield',
|
||||
17 => 'Miscellaneous',
|
||||
];
|
||||
|
||||
const item_rarities = [
|
||||
0 => 'Common',
|
||||
1 => 'Uncommon',
|
||||
2 => 'Rare',
|
||||
3 => 'Unique',
|
||||
4 => 'Super Elite',
|
||||
5 => 'Crystalline',
|
||||
6 => 'Epic',
|
||||
7 => 'Artifact',
|
||||
8 => 'Heirloom',
|
||||
9 => 'Legendary'
|
||||
];
|
||||
|
||||
const item_qualities = [
|
||||
0 => 'Very Poor',
|
||||
1 => 'Poor',
|
||||
2 => 'Average',
|
||||
3 => 'Good',
|
||||
4 => 'Very Good',
|
||||
5 => 'Excellent',
|
||||
6 => 'Masterwork',
|
||||
];
|
||||
|
||||
/**
|
||||
* Create an item
|
||||
*/
|
||||
function create_item(string $name, array $type, array $opts) {
|
||||
|
||||
}
|
37
src/models/player.php
Normal file
37
src/models/player.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?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
|
||||
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 retrieving a player, we will get the tables as-needed, to prevent allocating more memory than we need.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function player_create(int $user_id, string $name, array $overrides = []): int
|
||||
{
|
||||
// Prep the data and merge in any overrides
|
||||
$data = ['user_id' => $user_id, 'name' => $name];
|
||||
if (!empty($overrides)) $data = array_merge($data, $overrides);
|
||||
|
||||
// Prep the fields for the query
|
||||
$k = array_keys($data);
|
||||
$f = implode(', ', array_keys($k));
|
||||
$v = implode(', ', array_map(fn($x) => ":$x", $k));
|
||||
|
||||
// Create the player!
|
||||
if (db_query(db_live(), "INSERT INTO players ($f) VALUES ($v)", $data) === false) {
|
||||
// @TODO: Log this error
|
||||
throw new Exception('Failed to create player.');
|
||||
}
|
||||
|
||||
// Get the player ID
|
||||
return db_live()->lastInsertRowID();
|
||||
}
|
50
src/models/session.php
Normal file
50
src/models/session.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Create a session for a user with a token and expiration date. Returns the token on success, or false on failure.
|
||||
*/
|
||||
function session_create(int $userId, int $expires): string|false
|
||||
{
|
||||
$token = token();
|
||||
$result = db_query(db_auth(), "INSERT INTO sessions (token, user_id, expires) VALUES (:t, :u, :e)", [
|
||||
':t' => $token,
|
||||
':u' => $userId,
|
||||
':e' => $expires
|
||||
]);
|
||||
if (!$result) return false;
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a session by token.
|
||||
*/
|
||||
function session_find(string $token): array|false
|
||||
{
|
||||
$result = db_query(db_auth(), "SELECT * FROM sessions WHERE token = :t", [':t' => $token]);
|
||||
$session = $result->fetchArray(SQLITE3_ASSOC);
|
||||
if (!$session) return false;
|
||||
$result->finalize();
|
||||
return $session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete sessions by user id.
|
||||
*/
|
||||
function session_delete(int $userId): SQLite3Result|false
|
||||
{
|
||||
return db_query(db_auth(), "DELETE FROM sessions WHERE user_id = :u", [':u' => $userId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a session by token and expiration date. If expired, the session is deleted and false is returned.
|
||||
*/
|
||||
function session_validate(string $token): bool
|
||||
{
|
||||
$session = session_find($token);
|
||||
if (!$session) return false;
|
||||
if ($session['expires'] < time()) {
|
||||
session_delete($session['user_id']);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
50
src/models/token.php
Normal file
50
src/models/token.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Create a token for a user. Returns the token on success, or false on failure.
|
||||
*/
|
||||
function token_create(int $userId): string|false
|
||||
{
|
||||
$token = token();
|
||||
$result = db_query(db_auth(), "INSERT INTO tokens (token, user_id) VALUES (:t, :u)", [
|
||||
':t' => $token,
|
||||
':u' => $userId
|
||||
]);
|
||||
if (!$result) return false;
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a token by token.
|
||||
*/
|
||||
function token_find(string $token): array|false
|
||||
{
|
||||
$result = db_query(db_auth(), "SELECT * FROM tokens WHERE token = :t", [':t' => $token]);
|
||||
$token = $result->fetchArray(SQLITE3_ASSOC);
|
||||
if (!$token) return false;
|
||||
$result->finalize();
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a token by token.
|
||||
*/
|
||||
function token_delete(string $token): SQLite3Result|false
|
||||
{
|
||||
return db_query(db_auth(), "DELETE FROM tokens WHERE token = :t", [':t' => $token]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a token by token and created date. Tokens are invalid if older than 7 days.
|
||||
*/
|
||||
function token_validate(string $token): bool
|
||||
{
|
||||
$token = token_find($token);
|
||||
if (!$token) return false;
|
||||
if (strtotime('+7 days') < time()) {
|
||||
token_delete($token['token']);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
37
src/models/user.php
Normal file
37
src/models/user.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Find a user by username, email, or id.
|
||||
*/
|
||||
function user_find(string|int $user): array|false
|
||||
{
|
||||
$result = db_query(db_auth(), "SELECT * FROM users WHERE username = :u OR email = :u OR id = :u", [':u' => $user]);
|
||||
$user = $result->fetchArray(SQLITE3_ASSOC);
|
||||
if (!$user) return false;
|
||||
$result->finalize();
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a user with a username, email, and password. Optionally pass an auth level. This function will not check
|
||||
* if the username or email already exists. It is up to the caller to check this before calling this function. It is
|
||||
* also up to the caller to validate password strength. This function will hash the password with the PASSWORD_ARGON2ID
|
||||
* algorithm.
|
||||
*/
|
||||
function user_create(string $username, string $email, string $password, int $auth = 0): SQLite3Result|false
|
||||
{
|
||||
return db_query(db_auth(), "INSERT INTO users (username, email, password, auth) VALUES (:u, :e, :p, :a)", [
|
||||
':u' => $username,
|
||||
':e' => $email,
|
||||
':p' => password_hash($password, PASSWORD_ARGON2ID),
|
||||
':a' => $auth
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a user by username, email, or id.
|
||||
*/
|
||||
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]);
|
||||
}
|
130
src/router.php
Normal file
130
src/router.php
Normal file
|
@ -0,0 +1,130 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Add a route to the route tree. The route must be a URI path, and contain dynamic segments
|
||||
* using a colon prefix. (:id, :slug, etc)
|
||||
*
|
||||
* Example:
|
||||
* `router_add($routes, 'GET', '/posts/:id', function($id) { echo "Viewing post $id"; });`
|
||||
*/
|
||||
function router_add(array &$routes, string $method, string $route, callable $handler): void
|
||||
{
|
||||
// Expand the route into segments and make dynamic segments into a common placeholder
|
||||
$segments = array_map(function($segment) {
|
||||
return str_starts_with($segment, ':') ? ':x' : $segment;
|
||||
}, explode('/', trim($route, '/')));
|
||||
|
||||
// Push each segment into the routes array as a node, except if this is the root node
|
||||
$node = &$routes;
|
||||
foreach ($segments as $segment) {
|
||||
// skip an empty segment, which allows us to register handlers for the root node
|
||||
if ($segment === '') continue;
|
||||
$node = &$node[$segment]; // build the node tree as we go
|
||||
}
|
||||
|
||||
// Add the handler to the last node
|
||||
$node[$method] = $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a lookup in the route tree for a given method and URI. Returns an array with a result code,
|
||||
* a handler if found, and any dynamic parameters. Codes are 200 for success, 404 for not found, and
|
||||
* 405 for method not allowed.
|
||||
*
|
||||
* @return array ['code', 'handler', 'params']
|
||||
*/
|
||||
function router_lookup(array $routes, string $method, string $uri): array
|
||||
{
|
||||
// node is a reference to our current location in the node tree
|
||||
$node = $routes;
|
||||
|
||||
// params will hold any dynamic segments we find
|
||||
$params = [];
|
||||
|
||||
// if the URI is just a slash, we can return the handler for the root node
|
||||
if ($uri === '/') {
|
||||
return isset($node[$method])
|
||||
? ['code' => 200, 'handler' => $node[$method], 'params' => null]
|
||||
: ['code' => 405, 'handler' => null, 'params' => null];
|
||||
}
|
||||
|
||||
// We'll split up the URI into segments and traverse the node tree
|
||||
foreach (explode('/', trim($uri, '/')) as $segment) {
|
||||
// if there is a node for this segment, move to it
|
||||
if (isset($node[$segment])) {
|
||||
$node = $node[$segment];
|
||||
continue;
|
||||
}
|
||||
|
||||
// if there is a dynamic segment, move to it and store the value
|
||||
if (isset($node[':x'])) {
|
||||
$params[] = $segment;
|
||||
$node = $node[':x'];
|
||||
continue;
|
||||
}
|
||||
|
||||
// if we can't find a node for this segment, return 404
|
||||
return ['code' => 404, 'handler' => null, 'params' => []];
|
||||
}
|
||||
|
||||
// if we found a handler for the method, return it and any params. if not, return a 405
|
||||
return isset($node[$method])
|
||||
? ['code' => 200, 'handler' => $node[$method], 'params' => $params ?? []]
|
||||
: ['code' => 405, 'handler' => null, 'params' => []];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a GET route
|
||||
*/
|
||||
function router_get(array &$routes, string $route, callable $handler): void
|
||||
{
|
||||
router_add($routes, 'GET', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a POST route
|
||||
*/
|
||||
function router_post(array &$routes, string $route, callable $handler): void
|
||||
{
|
||||
router_add($routes, 'POST', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a PUT route
|
||||
*/
|
||||
function router_put(array &$routes, string $route, callable $handler): void
|
||||
{
|
||||
router_add($routes, 'PUT', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a DELETE route
|
||||
*/
|
||||
function router_delete(array &$routes, string $route, callable $handler): void
|
||||
{
|
||||
router_add($routes, 'DELETE', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a PATCH route
|
||||
*/
|
||||
function router_patch(array &$routes, string $route, callable $handler): void
|
||||
{
|
||||
router_add($routes, 'PATCH', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a router error by setting the response code and echoing an error message
|
||||
*/
|
||||
function router_error(int $code): void
|
||||
{
|
||||
http_response_code($code);
|
||||
echo match ($code) {
|
||||
403 => 'Forbidden',
|
||||
404 => 'Not Found',
|
||||
405 => 'Method Not Allowed',
|
||||
418 => 'I\'m a teapot',
|
||||
default => 'Unknown Error',
|
||||
};
|
||||
exit;
|
||||
}
|
23
templates/layouts/basic.php
Normal file
23
templates/layouts/basic.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dragon Knight</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="dk">
|
||||
<header>
|
||||
<h1>Dragon Knight</h1>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<?= render($view, $data) ?>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© 2024 Dragon Knight</p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
19
templates/pages/auth/login.php
Normal file
19
templates/pages/auth/login.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
$errors = flash('errors');
|
||||
if ($errors !== false) {
|
||||
foreach ($errors as $error) {
|
||||
foreach ($error as $message) {
|
||||
echo "<p>$message</p>";
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<form action="/auth/login" method="post">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<input type="text" name="username" placeholder="Username">
|
||||
<input type="password" name="password" placeholder="Password">
|
||||
<input type="checkbox" name="remember" id="remember"> <label for="remember">remember me</label>
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
19
templates/pages/auth/register.php
Normal file
19
templates/pages/auth/register.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
$errors = flash('errors');
|
||||
if ($errors !== false) {
|
||||
foreach ($errors as $error) {
|
||||
foreach ($error as $message) {
|
||||
echo "<p>$message</p>";
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<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>
|
11
templates/pages/home.php
Normal file
11
templates/pages/home.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php if (!auth_check()): ?>
|
||||
Hello, oppai!
|
||||
<a href="/auth/register">Register</a>
|
||||
<a href="/auth/login">Login</a>
|
||||
<?php else: ?>
|
||||
Hello, <?= $_SESSION['user']['username'] ?>!
|
||||
<form action="/auth/logout" method="post">
|
||||
<input type="hidden" name="csrf" value="<?= csrf() ?>">
|
||||
<input type="submit" value="Logout">
|
||||
</form>
|
||||
<?php endif; ?>
|
Loading…
Reference in New Issue
Block a user