diff --git a/.env b/.env index 9ec955f..2a885ec 100644 --- a/.env +++ b/.env @@ -6,4 +6,4 @@ silver_modifier = 1 allow_pvp = true allow_registration = true start_silver = 100 -sp_per_level = 5 +atts_per_level = 5 diff --git a/database/auth.db b/database/auth.db index 3831452..c7f1b9e 100644 Binary files a/database/auth.db and b/database/auth.db differ diff --git a/database/live.db b/database/live.db index 1296611..f2c5079 100644 Binary files a/database/live.db and b/database/live.db differ diff --git a/database/scripts/create.php b/database/scripts/create.php index 7f5076f..89b0a88 100644 --- a/database/scripts/create.php +++ b/database/scripts/create.php @@ -56,36 +56,38 @@ if ($database === AUTH) { // Users table $db->exec('DROP TABLE IF EXISTS users'); - $db->exec('CREATE TABLE users ( + $r = $db->exec('CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, auth INT NOT NULL DEFAULT 0, + char_id INTEGER NOT NULL DEFAULT 0, created DATETIME DEFAULT CURRENT_TIMESTAMP, last_login DATETIME DEFAULT CURRENT_TIMESTAMP )'); - eln(yellow('Created table: ') . 'users'); + created_or_error($r, 'users'); // Sessions table $db->exec('DROP TABLE IF EXISTS sessions'); - $db->exec('CREATE TABLE sessions ( + $r = $db->exec('CREATE TABLE sessions ( user_id INTEGER NOT NULL, token TEXT NOT NULL UNIQUE, expires INTEGER NOT NULL )'); - eln(yellow('Created table: ') . 'sessions'); + created_or_error($r, 'sessions'); // Verification tokens $db->exec('DROP TABLE IF EXISTS tokens'); - $db->exec('CREATE TABLE tokens ( + $r = $db->exec('CREATE TABLE tokens ( user_id INTEGER NOT NULL, token TEXT NOT NULL UNIQUE, created INTEGER NOT NULL )'); - eln(yellow('Created table: ') . 'tokens'); + created_or_error($r, 'tokens'); eln(green('Created database: ') . 'auth.db'); @@ -105,20 +107,23 @@ if ($database === FIGHTS) { // PvE fights $db->exec('DROP TABLE IF EXISTS pve'); - $db->exec('CREATE TABLE pve ( + $r = $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, + char_id INTEGER NOT NULL, + char_hp INTEGER NOT NULL, + char_max_hp INTEGER NOT NULL, + char_mp INTEGER NOT NULL, + char_max_mp INTEGER NOT NULL, + char_power INTEGER NOT NULL, + char_accuracy INTEGER NOT NULL, + char_penetration INTEGER NOT NULL DEFAULT 0, + char_focus INTEGER NOT NULL DEFAULT 0, + char_toughness INTEGER NOT NULL, + char_armor INTEGER NOT NULL, + char_resist INTEGER NOT NULL, + char_crit INTEGER NOT NULL, + char_precision INTEGER NOT NULL, + char_ferocity INTEGER NOT NULL, mob_id INTEGER NOT NULL, mob_level INTEGER NOT NULL, mob_rank INTEGER NOT NULL, @@ -130,6 +135,8 @@ if ($database === FIGHTS) { mob_toughness INTEGER NOT NULL, mob_armor INTEGER NOT NULL, mob_precision INTEGER NOT NULL, + mob_penetration INTEGER NOT NULL DEFAULT 0, + mob_focus INTEGER NOT NULL DEFAULT 0, mob_crit INTEGER NOT NULL, mob_ferocity INTEGER NOT NULL, mob_vitality INTEGER NOT NULL, @@ -142,36 +149,42 @@ if ($database === FIGHTS) { updated DATETIME DEFAULT CURRENT_TIMESTAMP )'); - eln(yellow('Created table: ') . 'pve'); + created_or_error($r, 'pve'); // PvP fights $db->exec('DROP TABLE IF EXISTS pvp'); - $db->exec('CREATE TABLE pvp ( + $r = $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, + char1_id INTEGER NOT NULL, + char1_hp INTEGER NOT NULL, + char1_max_hp INTEGER NOT NULL, + char1_mp INTEGER NOT NULL, + char1_max_mp INTEGER NOT NULL, + char1_power INTEGER NOT NULL, + char1_accuracy INTEGER NOT NULL, + char1_penetration INTEGER NOT NULL DEFAULT 0, + char1_focus INTEGER NOT NULL DEFAULT 0, + char1_toughness INTEGER NOT NULL, + char1_armor INTEGER NOT NULL, + char1_resist INTEGER NOT NULL, + char1_crit INTEGER NOT NULL, + char1_precision INTEGER NOT NULL, + char1_ferocity INTEGER NOT NULL, + char2_id INTEGER NOT NULL, + char2_hp INTEGER NOT NULL, + char2_max_hp INTEGER NOT NULL, + char2_mp INTEGER NOT NULL, + char2_max_mp INTEGER NOT NULL, + char2_power INTEGER NOT NULL, + char2_accuracy INTEGER NOT NULL, + char2_penetration INTEGER NOT NULL DEFAULT 0, + char2_focus INTEGER NOT NULL DEFAULT 0, + char2_toughness INTEGER NOT NULL, + char2_armor INTEGER NOT NULL, + char2_resist INTEGER NOT NULL, + char2_crit INTEGER NOT NULL, + char2_precision INTEGER NOT NULL, + char2_ferocity INTEGER NOT NULL, first_turn INTEGER NOT NULL, turn INTEGER NOT NULL default 1, winner INTEGER NOT NULL default 0, @@ -179,27 +192,27 @@ if ($database === FIGHTS) { updated DATETIME DEFAULT CURRENT_TIMESTAMP )'); - eln(yellow('Created table: ') . 'pvp'); + created_or_error($r, 'pvp'); // PvE fight logs $db->exec('DROP TABLE IF EXISTS pve_logs'); - $db->exec('CREATE TABLE pve_logs ( + $r = $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'); + created_or_error($r, 'pve_logs'); // PvP fight logs $db->exec('DROP TABLE IF EXISTS pvp_logs'); - $db->exec('CREATE TABLE pvp_logs ( + $r = $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'); + created_or_error($r, 'pvp_logs'); eln(green('Created database: ') . 'fights.db'); @@ -219,7 +232,7 @@ if ($database === BPS) { // Items $db->exec('DROP TABLE IF EXISTS items'); - $db->exec('CREATE TABLE items ( + $r = $db->exec('CREATE TABLE items ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type INTEGER NOT NULL DEFAULT 0, @@ -231,12 +244,16 @@ if ($database === BPS) { duration INTEGER NOT NULL DEFAULT 0, durability INTEGER NOT NULL DEFAULT 0, power INTEGER NOT NULL DEFAULT 0, + accuracy INTEGER NOT NULL DEFAULT 0, + penetration INTEGER NOT NULL DEFAULT 0, + focus INTEGER NOT NULL DEFAULT 0, toughness INTEGER NOT NULL DEFAULT 0, armor INTEGER NOT NULL DEFAULT 0, - precision INTEGER NOT NULL DEFAULT 0, + resist INTEGER NOT NULL DEFAULT 0, crit INTEGER NOT NULL DEFAULT 0, + precision INTEGER NOT NULL DEFAULT 0, ferocity INTEGER NOT NULL DEFAULT 0, - vitality INTEGER NOT NULL DEFAULT 0, + luck INTEGER NOT NULL DEFAULT 0, reqs TEXT NOT NULL DEFAULT "", traits TEXT NOT NULL DEFAULT "", lore TEXT NOT NULL DEFAULT "", @@ -244,11 +261,11 @@ if ($database === BPS) { updated DATETIME DEFAULT CURRENT_TIMESTAMP )'); - eln(yellow('Created table: ') . 'items'); + created_or_error($r, 'items'); // Mobs $db->exec('DROP TABLE IF EXISTS mobs'); - $db->exec('CREATE TABLE mobs ( + $r = $db->exec('CREATE TABLE mobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type INTEGER NOT NULL, @@ -273,7 +290,7 @@ if ($database === BPS) { updated DATETIME DEFAULT CURRENT_TIMESTAMP )'); - eln(yellow('Created table: ') . 'mobs'); + created_or_error($r, 'mobs'); eln(green('Created database: ') . 'blueprints.db'); @@ -291,13 +308,13 @@ if ($database === LIVE) { $db = new SQLite3(__DIR__ . '/../' . LIVE); - // Players - $db->exec('DROP TABLE IF EXISTS players'); - $db->exec('CREATE TABLE players ( + // Characters + $db->exec('DROP TABLE IF EXISTS characters'); + $r = $db->exec('CREATE TABLE characters ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, name TEXT NOT NULL UNIQUE, - title_id INTEGER NOT NULL DEFAULT, + title_id INTEGER NOT NULL DEFAULT 0, level INTEGER NOT NULL DEFAULT 1, xp INTEGER NOT NULL DEFAULT 0, xp_to_level INTEGER NOT NULL DEFAULT 100, @@ -308,21 +325,25 @@ if ($database === LIVE) { current_tp INTEGER NOT NULL DEFAULT 0, max_tp INTEGER NOT NULL DEFAULT 0, power INTEGER NOT NULL DEFAULT 0, + accuracy INTEGER NOT NULL DEFAULT 0, + penetration INTEGER NOT NULL DEFAULT 0, + focus INTEGER NOT NULL DEFAULT 0, toughness INTEGER NOT NULL DEFAULT 0, armor INTEGER NOT NULL DEFAULT 0, + resist 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 + luck INTEGER NOT NULL DEFAULT 0, + inv_slots INTEGER NOT NULL DEFAULT 10, + attrib_points INTEGER NOT NULL DEFAULT 0 )'); - eln(yellow('Created table: ') . 'players'); + created_or_error($r, 'characters'); // Player gear - $db->exec('DROP TABLE IF EXISTS player_gear'); - $db->exec('CREATE TABLE player_gear ( - player_id INTEGER NOT NULL, + $db->exec('DROP TABLE IF EXISTS char_gear'); + $r = $db->exec('CREATE TABLE char_gear ( + char_id INTEGER NOT NULL, head INTEGER NOT NULL DEFAULT 0, chest INTEGER NOT NULL DEFAULT 0, boots INTEGER NOT NULL DEFAULT 0, @@ -333,62 +354,66 @@ if ($database === LIVE) { ring INTEGER NOT NULL DEFAULT 0, amulet INTEGER NOT NULL DEFAULT 0, power INTEGER NOT NULL DEFAULT 0, + accuracy INTEGER NOT NULL DEFAULT 0, + penetration INTEGER NOT NULL DEFAULT 0, + focus INTEGER NOT NULL DEFAULT 0, toughness INTEGER NOT NULL DEFAULT 0, armor INTEGER NOT NULL DEFAULT 0, - precision INTEGER NOT NULL DEFAULT 0, + resist INTEGER NOT NULL DEFAULT 0, crit INTEGER NOT NULL DEFAULT 0, + precision INTEGER NOT NULL DEFAULT 0, ferocity INTEGER NOT NULL DEFAULT 0, - vitality INTEGER NOT NULL DEFAULT 0, + luck INTEGER NOT NULL DEFAULT 0, max_hp INTEGER NOT NULL DEFAULT 0, - max_mp INTEGER NOT NULL DEFAULT 0, - traits TEXT NOT NULL DEFAULT "" + max_mp INTEGER NOT NULL DEFAULT 0 )'); - eln(yellow('Created table: ') . 'player_gear'); + created_or_error($r, 'char_gear'); // Player inventory - $db->exec('DROP TABLE IF EXISTS player_inventory'); - $db->exec('CREATE TABLE inventory ( - player_id INTEGER NOT NULL, + $db->exec('DROP TABLE IF EXISTS char_inventory'); + $r = $db->exec('CREATE TABLE inventory ( + char_id INTEGER NOT NULL, item_id INTEGER NOT NULL )'); - eln(yellow('Created table: ') . 'inventory'); + created_or_error($r, 'inventory'); // Player wallet - $db->exec('DROP TABLE IF EXISTS player_wallet'); - $db->exec('CREATE TABLE wallet ( - player_id INTEGER NOT NULL, + $db->exec('DROP TABLE IF EXISTS char_wallets'); + $r = $db->exec('CREATE TABLE char_wallets ( + char_id INTEGER NOT NULL, silver INTEGER NOT NULL DEFAULT 10, stargem INTEGER NOT NULL DEFAULT 0 )'); - eln(yellow('Created table: ') . 'wallet'); + created_or_error($r, 'char_wallets'); // Player bank - $db->exec('DROP TABLE IF EXISTS player_bank'); - $db->exec('CREATE TABLE bank ( - player_id INTEGER NOT NULL, + $db->exec('DROP TABLE IF EXISTS char_bank'); + $r = $db->exec('CREATE TABLE bank ( + char_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 + tier INTEGER NOT NULL DEFAULT 0, + can_collect INTEGER NOT NULL DEFAULT 1, + last_collected DATETIME DEFAULT CURRENT_TIMESTAMP )'); - eln(yellow('Created table: ') . 'bank'); + created_or_error($r, 'bank'); // Banked items - $db->exec('DROP TABLE IF EXISTS player_banked_items'); - $db->exec('CREATE TABLE banked_items ( - player_id INTEGER NOT NULL, + $db->exec('DROP TABLE IF EXISTS char_banked_items'); + $r = $db->exec('CREATE TABLE banked_items ( + char_id INTEGER NOT NULL, item_id INTEGER NOT NULL )'); - eln(yellow('Created table: ') . 'banked_items'); + created_or_error($r, 'banked_items'); // Towns $db->exec('DROP TABLE IF EXISTS towns'); - $db->exec('CREATE TABLE towns ( + $r = $db->exec('CREATE TABLE towns ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, x INTEGER NOT NULL, @@ -399,11 +424,11 @@ if ($database === LIVE) { updated DATETIME DEFAULT CURRENT_TIMESTAMP )'); - eln(yellow('Created table: ') . 'towns'); + created_or_error($r, 'towns'); // Shops $db->exec('DROP TABLE IF EXISTS shops'); - $db->exec('CREATE TABLE shops ( + $r = $db->exec('CREATE TABLE shops ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type INTEGER NOT NULL, @@ -419,11 +444,11 @@ if ($database === LIVE) { updated DATETIME DEFAULT CURRENT_TIMESTAMP )'); - eln(yellow('Created table: ') . 'shops'); + created_or_error($r, 'shops'); // Inns $db->exec('DROP TABLE IF EXISTS inns'); - $db->exec('CREATE TABLE inns ( + $r = $db->exec('CREATE TABLE inns ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type INTEGER NOT NULL, @@ -435,11 +460,11 @@ if ($database === LIVE) { updated DATETIME DEFAULT CURRENT_TIMESTAMP )'); - eln(yellow('Created table: ') . 'inns'); + created_or_error($r, 'inns'); // Guilds $db->exec('DROP TABLE IF EXISTS guilds'); - $db->exec('CREATE TABLE guilds ( + $r = $db->exec('CREATE TABLE guilds ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, lore TEXT NOT NULL DEFAULT "", @@ -450,35 +475,35 @@ if ($database === LIVE) { updated DATETIME DEFAULT CURRENT_TIMESTAMP )'); - eln(yellow('Created table: ') . 'guilds'); + created_or_error($r, 'guilds'); // Guild ranks $db->exec('DROP TABLE IF EXISTS guild_ranks'); - $db->exec('CREATE TABLE guild_ranks ( + $r = $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'); + created_or_error($r, 'guild_ranks'); // Guild members $db->exec('DROP TABLE IF EXISTS guild_members'); - $db->exec('CREATE TABLE guild_members ( + $r = $db->exec('CREATE TABLE guild_members ( guild_id INTEGER NOT NULL, - player_id INTEGER NOT NULL, + char_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'); + created_or_error($r, 'guild_members'); // NPCs $db->exec('DROP TABLE IF EXISTS npcs'); - $db->exec('CREATE TABLE npcs ( + $r = $db->exec('CREATE TABLE npcs ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type INTEGER NOT NULL, @@ -490,22 +515,21 @@ if ($database === LIVE) { updated DATETIME DEFAULT CURRENT_TIMESTAMP )'); - eln(yellow('Created table: ') . 'npcs'); + created_or_error($r, 'npcs'); // Town reputation - $db->exec('DROP TABLE IF EXISTS player_town_rep'); - $db->exec('CREATE TABLE town_rep ( - player_id INTEGER NOT NULL, + $db->exec('DROP TABLE IF EXISTS char_town_rep'); + $r = $db->exec('CREATE TABLE char_town_rep ( + char_id INTEGER NOT NULL, town_id INTEGER NOT NULL, rep INTEGER NOT NULL DEFAULT 0 )'); - eln(yellow('Created table: ') . 'town_rep'); + created_or_error($r, 'char_town_rep'); - // Items // Items $db->exec('DROP TABLE IF EXISTS items'); - $db->exec('CREATE TABLE items ( + $r = $db->exec('CREATE TABLE items ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type TEXT NOT NULL DEFAULT 0, @@ -518,12 +542,16 @@ if ($database === LIVE) { durability INTEGER NOT NULL DEFAULT 0, max_durability INTEGER NOT NULL DEFAULT 0, power INTEGER NOT NULL DEFAULT 0, + accuracy INTEGER NOT NULL DEFAULT 0, + penetration INTEGER NOT NULL DEFAULT 0, + focus INTEGER NOT NULL DEFAULT 0, toughness INTEGER NOT NULL DEFAULT 0, armor INTEGER NOT NULL DEFAULT 0, - precision INTEGER NOT NULL DEFAULT 0, + resist INTEGER NOT NULL DEFAULT 0, crit INTEGER NOT NULL DEFAULT 0, + precision INTEGER NOT NULL DEFAULT 0, ferocity INTEGER NOT NULL DEFAULT 0, - vitality INTEGER NOT NULL DEFAULT 0, + luck INTEGER NOT NULL DEFAULT 0, reqs TEXT NOT NULL DEFAULT "", traits TEXT NOT NULL DEFAULT "", lore TEXT NOT NULL DEFAULT "", @@ -531,10 +559,39 @@ if ($database === LIVE) { updated DATETIME DEFAULT CURRENT_TIMESTAMP )'); - eln(yellow('Created table: ') . 'items'); + created_or_error($r, 'items'); + + // Player traits + $db->exec('DROP TABLE IF EXISTS char_traits'); + $r = $db->exec('CREATE TABLE char_traits ( + char_id INTEGER NOT NULL, + trait_id INTEGER NOT NULL, + value INTEGER NOT NULL DEFAULT 0 + )'); + + created_or_error($r, 'char_traits'); + + // Character location + $db->exec('DROP TABLE IF EXISTS char_locations'); + $r = $db->exec('CREATE TABLE char_locations ( + char_id INTEGER NOT NULL, + x INTEGER NOT NULL, + y INTEGER NOT NULL, + currently INTEGER NOT NULL DEFAULT 0 + )'); + + created_or_error($r, 'char_locations'); eln(green('Created database: ') . 'live.db'); exit(0); } +function created_or_error(bool $result, string $table): void +{ + if ($result === false) { + eln(red('Failed to create table: ') . $table); + exit(1); + } + eln(yellow('Created table: ') . $table); +} diff --git a/docs/attributes.md b/docs/attributes.md new file mode 100644 index 0000000..fb71374 --- /dev/null +++ b/docs/attributes.md @@ -0,0 +1,34 @@ +# Attributes + +## Power +Power is the attribute that determines overall strike damage. Damage from weapon attacks, spells, or other strikes and specific skills will be directly influenced by Power. Power is a linear measure, with 1 Power = 1 damage in most cases. + +## Accuracy +Accuracy determines the chance your attack, spell, or other skill will actually land on your opponent. + +## Penetration +Penetration as a value determines your ability to make it through physical defenses; i.e. the enemy's Toughness and Armor. + +## Focus +Focus is the same as Penetration but for causing magical damage, and helps go through magical defenses; i.e. the enemy's Resist. + +## Evasion +Evasion is the attribute responsible for determining your chance to dodge any kind of attack. When calculating the chance, at base every 2 Evasion is 0.1% chance. The enemy's Accuracy negatively impacts this stat. The chance to dodge is capped at 85%. + +## Toughness +Toughness affects your health pool and baseline defensive capability to physical attacks, such as weapons, projectiles, and physical skills. + +## Armor +Armor is an additional layer of physical protection over Toughness. All armor grants some level of Armor, and it's effect is linear; 1 Armor = 1 point of damage negated. + +## Resist +Resist is your defensive capacity against magic such as spells, magic skills, and magic weapons. Like Armor, this is a linear stat. + +## Precision +Precision determines your Uber (Critical) Hit chance. In general, 2 points of Precision = 0.1% chance. No matter how high this value, your Uber Hit chance caps at 90%. Some equipment grants straight Uber Hit chance. + +## Ferocity +Ferocity is a linear modifier of your Uber Hit damage; 1 Ferocity = 1% Uber Hit damage. Uber Hit damage increases cap at 300%. + +## Luck +Luck is a modifier for gained XP/Silver. Like Precision, 2 points of Luck = 0.1% increase of these gains. diff --git a/public/index.php b/public/index.php index 918bc31..4c85bd9 100644 --- a/public/index.php +++ b/public/index.php @@ -1,23 +1,44 @@ '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'); +/* + Auth +*/ +router_get($r, '/auth/register', 'auth_controller_register_get'); +router_post($r, '/auth/register', 'auth_controller_register_post'); +router_get($r, '/auth/login', 'auth_controller_login_get'); +router_post($r, '/auth/login', 'auth_controller_login_post'); +router_post($r, '/auth/logout', 'auth_controller_logout_post'); +/* + Characters +*/ +router_post($r, '/character/create', 'char_controller_create_post'); +router_post($r, '/character/select', 'auth_controller_change_character_post'); + +/* + Router +*/ // [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'] ?? []); + +/* + Cleanup +*/ clear_flashes(); diff --git a/src/auth.php b/src/auth.php index 5b16d1f..1452aef 100644 --- a/src/auth.php +++ b/src/auth.php @@ -16,158 +16,6 @@ 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. */ @@ -204,3 +52,20 @@ function auth_check(): bool return false; } + +/** + * Ensure a user is logged in, or redirect to the login page. This will also check for a remember me cookie and + * populate the $_SESSION['user'] array. + */ +function auth_ensure(): void +{ + if (!auth_check()) redirect('/auth/login'); +} + +/** + * If there is a user logged in, redirect to the home page. Used for when we have a guest-only page. + */ +function auth_guest(): void +{ + if (auth_check()) redirect('/'); +} diff --git a/src/bootstrap.php b/src/bootstrap.php index bf942dd..b2c9d43 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -15,6 +15,11 @@ require_once SRC . '/router.php'; require_once SRC . '/models/user.php'; require_once SRC . '/models/session.php'; require_once SRC . '/models/token.php'; +require_once SRC . '/models/char.php'; + +// Controllers +require_once SRC . '/controllers/char.php'; +require_once SRC . '/controllers/auth.php'; /* Load env, set error reporting, etc. @@ -32,3 +37,6 @@ csrf(); // Have a global counter for queries $GLOBALS['queries'] = 0; + +// Run auth_check to see if we're logged in, since it populates the user data in SESSION +auth_check(); diff --git a/src/controllers/auth.php b/src/controllers/auth.php new file mode 100644 index 0000000..e36d670 --- /dev/null +++ b/src/controllers/auth.php @@ -0,0 +1,176 @@ + 'pages/auth/register']); +} + +/** + * Handles the registration form submission. + */ +function auth_controller_register_post(): void +{ + auth_guest(); + 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 18 characters long. + A username must contain only alphanumeric characters and spaces. + */ + if (empty($u) || strlen($u) < 3 || strlen($u) > 18 || !ctype_alnum(str_replace(' ', '', $u))) { + $errors['u'][] = 'Username is required and must be between 3 and 18 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_controller_login_get(): void +{ + auth_guest(); + echo render('layouts/basic', ['view' => 'pages/auth/login']); +} + +/** + * Handles the login form submission. + */ +function auth_controller_login_post(): void +{ + auth_guest(); + 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_controller_logout_post(): void +{ + csrf_ensure(); + session_delete($_SESSION['user']['id']); + unset($_SESSION['user']); + set_cookie('remember_me', '', 1); + redirect('/'); +} + +/** + * Changes the user's currently selected character. + */ +function auth_controller_change_character_post(): void +{ + auth_check(); + csrf_ensure(); + + $char_id = (int) ($_POST['char_id'] ?? 0); + if (char_exists($char_id) === false) router_error(400); + + $_SESSION['user']['char_id'] = $char_id; + if (db_query(db_auth(), 'UPDATE users SET char_id = :c WHERE id = :u', [ + ':c' => $char_id, + ':u' => $_SESSION['user']['id'] + ]) === false) router_error(400); + redirect('/'); +} diff --git a/src/controllers/char.php b/src/controllers/char.php new file mode 100644 index 0000000..c190d99 --- /dev/null +++ b/src/controllers/char.php @@ -0,0 +1,67 @@ + 18 || !ctype_alnum(str_replace(' ', '', $name))) { + $errors['name'][] = 'Name is required and must be between 3 and 18 characters long and contain only alphanumeric characters and spaces.'; + } + + /* + A player's name must be unique. + */ + if (char_nameExists($name)) $errors['name'][] = 'Name is already taken.'; + + // If there are errors at this point, send them to the page with errors flashed. + if (!empty($errors)) { + flash('errors', $errors); + redirect('/'); + } + + // Create the player + $player = char_create(user('id'), $name); + if ($player === false) router_error(400); + + // Create the auxiliary tables + char_location_create($player); + char_wallet_create($player); + char_gear_create($player); + + redirect('/'); +} + +/** + * Change the user's selected character. + */ +function char_controller_select_post(): void +{ + auth_ensure(); + csrf_ensure(); + + $char_id = (int) $_POST['char_id'] ?? 0; + + // Ensure the character exists and belongs to the user + if (!char_exists($char_id)) router_error(400); + + // Update the user's selected character + $_SESSION['user']['char_id'] = $char_id; + + redirect('/'); +} diff --git a/src/env.php b/src/env.php index 06239f2..e941417 100644 --- a/src/env.php +++ b/src/env.php @@ -36,5 +36,5 @@ function env_load(string $filePath): void */ function env(string $key, mixed $default = null): mixed { - return $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key) ?? $default; + return $_ENV[$key] ?? $_SERVER[$key] ?? (getenv($key) ?: $default); } diff --git a/src/helpers.php b/src/helpers.php index 0ffd95e..d9212fd 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -108,3 +108,21 @@ function set_cookie(string $name, string $value, int $expires): void 'samesite' => 'Strict' // Enforce SameSite=Strict ]); } + +/** + * Get the current user's array from SESSION if it exists. Specify a key to get a specific value. + */ +function user(string $field = ''): mixed +{ + if (empty($_SESSION['user'])) return false; + if ($field === '') return $_SESSION['user']; + return $_SESSION['user'][$field] ?? false; +} + +/** + * Check whether the user has selected a character. If so, return the character's ID. + */ +function char_selected(): int +{ + return (int) $_SESSION['user']['char_id']; +} diff --git a/src/models/char.php b/src/models/char.php new file mode 100644 index 0000000..c89e97f --- /dev/null +++ b/src/models/char.php @@ -0,0 +1,201 @@ + 'Exploring', + 1 => 'In Town', + 2 => 'In Combat', + 4 => 'In Shop', + 5 => 'In Inn' +]; + +/** + * Create a player. Only a user ID and a name are required. All other fields are optional. Pass a key-value array + * of overrides to set additional fields. A player's name must be unique, but this function does not check for + * that. Returns the created player's ID. + */ +function char_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(', ', $k); + $v = implode(', ', array_map(fn($x) => ":$x", $k)); + + // Create the player! + if (db_query(db_live(), "INSERT INTO characters ($f) VALUES ($v)", $data) === false) { + // @TODO: Log this error + throw new Exception('Failed to create player.'); + } + + // Get the player ID + return db_live()->lastInsertRowID(); +} + +/** + * Create a player's location record. A player's location is where they are in the game world. A player can only be + * in one location at a time. Can define a starting location for the player. Default state is 'Exploring'. + */ +function char_location_create(int $char_id, int $x = 0, int $y = 0, int $currently = 0): void +{ + if (db_query(db_live(), "INSERT INTO char_locations (char_id, x, y, currently) VALUES (:p, :x, :y, :c)", [ + ':p' => $char_id, + ':x' => $x, + ':y' => $y, + ':c' => $currently + ]) === false) { + throw new Exception('Failed to create player location.'); + } +} + +/** + * Creates a player's wallet. A player's wallet is where they store their currencies. Can optionally specify the + * starting balances of the wallet. Returns the created wallet's ID. If a currency is set to -1, the starting_silver + * or starting_star_gems fields from the env will be used. + */ +function char_wallet_create(int $char_id, int $silver = -1, int $starGems = -1): void +{ + if (db_query(db_live(), "INSERT INTO char_wallets (char_id, silver, stargem) VALUES (:p, :s, :sg)", [ + ':p' => $char_id, + ':s' => $silver === -1 ? env('start_silver', 10) : $silver, + ':sg' => $starGems === -1 ? env('start_star_gems', 0) : $starGems + ]) === false) { + throw new Exception('Failed to create player wallet.'); + } +} + +/** + * Create the player's gear table. A player's gear is where they store their equipped items. + * @TODO: implement initial gear + */ +function char_gear_create(int $char_id, array $initialGear = []): void +{ + if (db_query(db_live(), "INSERT INTO char_gear (char_id) VALUES (:p)", [':p' => $char_id]) === false) { + throw new Exception('Failed to create player gear.'); + } +} + +/** + * Create the player's bank account. The bank stores items and currency, with an interest rate based on + * the bank account's tier. The bank account has a limited number of slots, which can be increased by upgrading + * the bank account. The bank account starts with 0 silver and 5 slots. + */ +function char_bank_create(int $char_id, int $slots = 5, int $silver = 0, int $tier = 0): void +{ + if (db_query(db_live(), "INSERT INTO char_banks (char_id, slots, silver, tier) VALUES (:p, :s, :si, :t)", [ + ':p' => $char_id, + ':s' => $slots, + ':si' => $silver, + ':t' => $tier + ]) === false) { + throw new Exception('Failed to create player bank.'); + } +} + +/** + * Get a player by their account ID. Returns the player's data as an associative array. + */ +function char_find(int $char_id): array +{ + // Get the player + $player = db_query(db_live(), "SELECT * FROM characters WHERE id = :id", [':id' => $char_id])->fetchArray(SQLITE3_ASSOC); + if ($player === false) { + throw new Exception('Character not found.'); + } + + return $player; +} + +/** + * Count the number of players associated with an account ID. + */ +function char_count(int $user_id): int +{ + // Get the count + $count = db_query(db_live(), "SELECT COUNT(*) FROM characters WHERE user_id = :u", [':u' => $user_id])->fetchArray(SQLITE3_NUM); + if ($count === false) { + throw new Exception('Failed to count players.'); + } + + return (int) $count[0]; +} + +/** + * Get a an array of id => [name, level] for all players associated with an account ID. + */ +function char_list(int $user_id): array +{ + // Get the players + $stmt = db_query(db_live(), "SELECT id, name, level FROM characters WHERE user_id = :u", [':u' => $user_id]); + if ($stmt === false) throw new Exception('Failed to list players.'); + + $players = []; + while ($row = $stmt->fetchArray(SQLITE3_ASSOC)) { + $players[$row['id']] = ['name' => $row['name'], 'level' => $row['level']]; + } + + return $players; +} + +/** + * Get a player's location info by their player ID. Returns the location's data as an associative array. + */ +function char_get_location(int $char_id): array +{ + // Get the location + $location = db_query(db_live(), "SELECT * FROM char_locations WHERE char_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC); + if ($location === false) { + throw new Exception('Location not found.'); + } + + return $location; +} + +/** + * Get a player's wallet by their player ID. Returns the wallet's data as an associative array. + */ +function char_get_wallet(int $char_id): array +{ + // Get the wallet + $wallet = db_query(db_live(), "SELECT * FROM char_wallets WHERE char_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC); + if ($wallet === false) { + throw new Exception('Wallet not found.'); + } + + return $wallet; +} + +/** + * See if a player name exists. + */ +function char_nameExists(string $name): bool +{ + // Check for the name + $exists = db_query(db_live(), "SELECT COUNT(*) FROM characters WHERE name = :n", [':n' => $name])->fetchArray(SQLITE3_NUM); + if ($exists === false) { + throw new Exception('Failed to check for player name.'); + } + + return (int) $exists[0] > 0; +} + +/** + * Checks whether a character exists at a certain ID. + */ +function char_exists(int $char_id): bool +{ + return db_exists(db_live(), 'characters', 'id', $char_id); +} diff --git a/src/models/player.php b/src/models/player.php deleted file mode 100644 index d707706..0000000 --- a/src/models/player.php +++ /dev/null @@ -1,37 +0,0 @@ - $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/templates/pages/home.php b/templates/pages/home.php index 8a1d0b6..dce70dd 100644 --- a/templates/pages/home.php +++ b/templates/pages/home.php @@ -1,11 +1,32 @@ - - Hello, oppai! + +

Welcome!

Register Login - Hello, ! +

Hello, !

+ +

Playing as

+
+ + 0): ?> +

Characters

+
+ + $char): ?> + +
+ + +
+ + +
+ + + +