From b0637405479619ffbb132b415b7a3228660e8848 Mon Sep 17 00:00:00 2001
From: Sky Johnson
Date: Fri, 27 Sep 2024 18:45:33 -0500
Subject: [PATCH] initial commit
---
.env | 9 +
color.php | 15 +
database/auth.db | Bin 0 -> 36864 bytes
database/blueprints.db | Bin 0 -> 16384 bytes
database/fights.db | Bin 0 -> 24576 bytes
database/live.db | Bin 0 -> 77824 bytes
database/scripts/create.php | 540 ++++++++++++++++++++++++++++++
docs/TODO.md | 4 +
docs/items.md | 3 +
public/index.php | 23 ++
src/auth.php | 206 ++++++++++++
src/bootstrap.php | 34 ++
src/database.php | 79 +++++
src/env.php | 40 +++
src/helpers.php | 110 ++++++
src/models/fights.php | 3 +
src/models/items.php | 66 ++++
src/models/player.php | 37 ++
src/models/session.php | 50 +++
src/models/token.php | 50 +++
src/models/user.php | 37 ++
src/router.php | 130 +++++++
templates/layouts/basic.php | 23 ++
templates/pages/auth/login.php | 19 ++
templates/pages/auth/register.php | 19 ++
templates/pages/home.php | 11 +
26 files changed, 1508 insertions(+)
create mode 100644 .env
create mode 100644 color.php
create mode 100644 database/auth.db
create mode 100644 database/blueprints.db
create mode 100644 database/fights.db
create mode 100644 database/live.db
create mode 100644 database/scripts/create.php
create mode 100644 docs/TODO.md
create mode 100644 docs/items.md
create mode 100644 public/index.php
create mode 100644 src/auth.php
create mode 100644 src/bootstrap.php
create mode 100644 src/database.php
create mode 100644 src/env.php
create mode 100644 src/helpers.php
create mode 100644 src/models/fights.php
create mode 100644 src/models/items.php
create mode 100644 src/models/player.php
create mode 100644 src/models/session.php
create mode 100644 src/models/token.php
create mode 100644 src/models/user.php
create mode 100644 src/router.php
create mode 100644 templates/layouts/basic.php
create mode 100644 templates/pages/auth/login.php
create mode 100644 templates/pages/auth/register.php
create mode 100644 templates/pages/home.php
diff --git a/.env b/.env
new file mode 100644
index 0000000..9ec955f
--- /dev/null
+++ b/.env
@@ -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
diff --git a/color.php b/color.php
new file mode 100644
index 0000000..ea3c0f3
--- /dev/null
+++ b/color.php
@@ -0,0 +1,15 @@
+FJH;e~
zBhy|WTYXGW9o=SpPt6_^Cfq?>D|r
zTFFoAACmH-PN;_f1Rwwb2tWV=5P$##{`UgoSUk>dZqnn{z^Zqhf!A?df08DSE4pUt
zoT>eAs&kV%ZhMVku6?IpYulVrH1!|#3Rf(dT(Nq3x<`(M%{WtkO{(syxoXiktLnkg
zM&Gdp&gVVXn4R53j6K?SwPjS>wy>U*CuDL36q=|udl}0tu1=o38VHM
z-*0=aKh0xvkxy&CFm~tms@-?|&qhsRKZwQHFaPG!F-Cdm@%Y55y*XlWYr0LGjdvu&-S^5_>{LEmA{4j~k^$)8;h?)5-SB!#I
zx!_*v7o1i#OGc6ODds2eq!(YP;OS9mtIBL}P58
zT{L5%QcVoAm7U1LO@**&u$E5*-zZ7)j%>WAQWcV5g8&2|009U<00Izz00bZa0SJ6G
zf$@HXp&pM*M+r&tJ0me|^;@1Rw(ayyUdW|;c_ovPl)XV--n+^RX`^R2-B*`o_twsu
zo#8?6tXmyvX_3Dib#IJ9r!I}2`^M$4Jvh+};r?{3PB7!tJ-7+hzWu
z-g{PP%G$YGZ{3P-&eCta*A=Dp(k+z><*eZqMlWu&)dTf{zk0>1Z%$0@dT95Rl6_?l
z7lu0%t#T-fhk`O2?OoLxnb%0_6P5ahB-kJT0SG_<0uX=z1Rwwb2tWV=5LgO4bntRyTj1|e6`Q%cJijNRw8-UxHj%ynQn1=UejQ`<@;IctAFFYTY&
zPn(>zVM^p~hwZTN3KU~~?kV{D-ycNBT9?y|
zk;wlNk-?dsqaY|()y8O4jQ
zmpPNt8l@uR_Pn`ZaUv6*Tk~R(C~IKG1dkJSV+}47MboO_wf$r=NtYEzOF38DrAxW6
z6f_o$DmEpjx>cfH-zTU32X7QaJ2lw`l%WDtEIoy?1lx_
zbY7P<1l85vuBa(m(zMvNjHzL>8H<%PYqpjg9F!l6c`l2rH278vt(8Cj60@4i^_k0|5{K0T2KI5C8!X009sH0T2Lz2LibNqZL2^
Z1V8`;KmY_l00ck)1V8`;Kw$R?`~V`sq`UwC
literal 0
HcmV?d00001
diff --git a/database/fights.db b/database/fights.db
new file mode 100644
index 0000000000000000000000000000000000000000..a6c2a1726084637bccca9617b8f4c95d6c244d92
GIT binary patch
literal 24576
zcmeI#%Wm306b4{B38a7`CCVbJNV80osA^oP>ypH+P=Qclq@pfHjv0d`upQgvvSgQS
zA0e;P=japk2|B~YB7nzkrAQU}Q6gXFcx1kFd>CJUtvV4SwigV{h`d*J6;)L}5~3)4
zS5fl(fA$f_TeAbcRp<7Tc6XJxmbz2=spQmWrSy>dtv(lj6&p$6IKL$DR4;5F?C+~T
zHY4+<&-^=||K%^5dfCv4Q9iHggr6Ws#X`a9kV@UqKkH4>XjW?FW}AG`+oaqwt}1nY
zMNO|8r##N@n~y9wqjT}~D}&Tq)$06k&yS~DH9RmMXj(}63zC~XWP;D2ZecAv4O
z-<7y;jGGG!Ez{?HW3~>M{cy)ouiafnd!_Az!$Wm6oGn*q-|L2ai89{i7W&NO>|Zq-
z+v)bAq~dm5+areljjv{=D;nPMMtjAV_r#|6G}&$~+|%UjE8kONBY$vmqPE9M;VoYc
zFdDMkA#*LZaJc#6{uTz0Tywxq`IX0fasFXx^@tM?fB*y%1$M-Uc(ca`0uX=z1Rwwb
z2tWV=5P$##AOL~26~O!d+NKs?3<3~<00bZa0SG_<0uX=z1RxL#;Qc?Qzzzf;009U<
z00Izz00bZa0SG`~Z3XcDzqYBx7lQxJE2W~Pw54UV8{Gz@u&nh)Xp&~Bv#pe26z4cmCrgeb
z=fe7u^a8uU{-8bWasNTt{)0X5X?vN@k>gphEx8a4F}?vOvUGCv)8}05qn{pqzF-By
zsJmXn42<^*GX+g6d~6tnLSZ_5&4#bTANhXt@J;wv>)IZ)IbC?Crk#|9r~j5^-irPV
zD$afc5I_I{1Q0*~0R#|0009ILIDUa?eNt9BbNn?^BLWB@fB*srAb!A1X%y)r!9yC
z0R#|0009ILKmY**5I_KdtOQv9XC+I$2q1s}0tg_000IagfB*srXS!axaz)$!C@|M-
zv1U5oi1n%!h=$+2J$`ScJXa|jmAOwA%0~AQpiC%;xxYP27XTf#{KfAbE^v#Si;gE?KCNR45$i%O}Q4H*RQ;Rj2m1!c&W?
zFSc63sfiiO
zjcUrL^6gvU@FD--`8vi10tg_000IagfB*srAbIS
z#(&kn8~Zcd#Sa1qqC~7x#2U@dg;OiZGT4%oWR|2s-9>b{yh^LK8JgUNPw!MU1^~198Oq&
zLiY-TI=DNf((y08RzW$;O&vnVY
zy%T||DZ55Jn3~i}SFdW%pD4eLmSwM38=|omMjm@J6%&meQnb;Y0nOOXhz~gGh0jm)
z)ktoADYNc6sskTwl}@);Q_xe_a)ufcvX@^wt@8Z`r7jQ|VVgs3PWHi22hu49UTpu5
zkEZY%mM>G$MX`70FPq?^mgycJ=O529?IXieVz0IKsttGDswY346+I&03)>#P
zbe2Ql(zt5J7nfB#%1>us{VK-VQSCa0c6^g=t-ebio$94@@4S?8XD9!k-vu6#A%Fk^
s2q1s}0tg_000Iaga9jec|Bp+OdJsSW0R#|0009ILKmY**5Xi5
+*/
+
+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);
+}
+
diff --git a/docs/TODO.md b/docs/TODO.md
new file mode 100644
index 0000000..d045ad8
--- /dev/null
+++ b/docs/TODO.md
@@ -0,0 +1,4 @@
+# TODO
+Currently, everything needs implemented.
+
+First task is to finish building up the database structures.
diff --git a/docs/items.md b/docs/items.md
new file mode 100644
index 0000000..d250712
--- /dev/null
+++ b/docs/items.md
@@ -0,0 +1,3 @@
+# Items
+
+Items consists of all item types in the game; useless flavor items, gear, consumables, etc.
diff --git a/public/index.php b/public/index.php
new file mode 100644
index 0000000..918bc31
--- /dev/null
+++ b/public/index.php
@@ -0,0 +1,23 @@
+ '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();
diff --git a/src/auth.php b/src/auth.php
new file mode 100644
index 0000000..5b16d1f
--- /dev/null
+++ b/src/auth.php
@@ -0,0 +1,206 @@
+ '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;
+}
diff --git a/src/bootstrap.php b/src/bootstrap.php
new file mode 100644
index 0000000..bf942dd
--- /dev/null
+++ b/src/bootstrap.php
@@ -0,0 +1,34 @@
+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
+ };
+}
diff --git a/src/env.php b/src/env.php
new file mode 100644
index 0000000..06239f2
--- /dev/null
+++ b/src/env.php
@@ -0,0 +1,40 @@
+ $_) {
+ 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 '';
+}
+
+/**
+ * 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
+ ]);
+}
diff --git a/src/models/fights.php b/src/models/fights.php
new file mode 100644
index 0000000..e8e80b9
--- /dev/null
+++ b/src/models/fights.php
@@ -0,0 +1,3 @@
+ '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) {
+
+}
diff --git a/src/models/player.php b/src/models/player.php
new file mode 100644
index 0000000..d707706
--- /dev/null
+++ b/src/models/player.php
@@ -0,0 +1,37 @@
+ $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();
+}
diff --git a/src/models/session.php b/src/models/session.php
new file mode 100644
index 0000000..ced6361
--- /dev/null
+++ b/src/models/session.php
@@ -0,0 +1,50 @@
+ $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;
+}
diff --git a/src/models/token.php b/src/models/token.php
new file mode 100644
index 0000000..e19375c
--- /dev/null
+++ b/src/models/token.php
@@ -0,0 +1,50 @@
+ $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;
+}
+
diff --git a/src/models/user.php b/src/models/user.php
new file mode 100644
index 0000000..0245406
--- /dev/null
+++ b/src/models/user.php
@@ -0,0 +1,37 @@
+ $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]);
+}
diff --git a/src/router.php b/src/router.php
new file mode 100644
index 0000000..5af9985
--- /dev/null
+++ b/src/router.php
@@ -0,0 +1,130 @@
+ 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;
+}
diff --git a/templates/layouts/basic.php b/templates/layouts/basic.php
new file mode 100644
index 0000000..132bc37
--- /dev/null
+++ b/templates/layouts/basic.php
@@ -0,0 +1,23 @@
+
+
+
+
+
+ Dragon Knight
+
+
+
+
+
+
+ = render($view, $data) ?>
+
+
+
+
+
+
diff --git a/templates/pages/auth/login.php b/templates/pages/auth/login.php
new file mode 100644
index 0000000..9040f0e
--- /dev/null
+++ b/templates/pages/auth/login.php
@@ -0,0 +1,19 @@
+$message
";
+ }
+ }
+ }
+?>
+
+
diff --git a/templates/pages/auth/register.php b/templates/pages/auth/register.php
new file mode 100644
index 0000000..bcaa1c3
--- /dev/null
+++ b/templates/pages/auth/register.php
@@ -0,0 +1,19 @@
+$message";
+ }
+ }
+ }
+?>
+
+
diff --git a/templates/pages/home.php b/templates/pages/home.php
new file mode 100644
index 0000000..8a1d0b6
--- /dev/null
+++ b/templates/pages/home.php
@@ -0,0 +1,11 @@
+
+ Hello, oppai!
+ Register
+ Login
+
+ Hello, = $_SESSION['user']['username'] ?>!
+
+