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 + + +
+
+

Dragon Knight

+
+ +
+ +
+ +
+

© 2024 Dragon Knight

+
+
+ + 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, ! +
+ + +
+