make players into characters, allow character selection

This commit is contained in:
Sky Johnson 2024-09-28 18:33:17 -05:00
parent b063740547
commit 043a57cf86
15 changed files with 745 additions and 314 deletions

2
.env
View File

@ -6,4 +6,4 @@ silver_modifier = 1
allow_pvp = true
allow_registration = true
start_silver = 100
sp_per_level = 5
atts_per_level = 5

Binary file not shown.

Binary file not shown.

View File

@ -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);
}

34
docs/attributes.md Normal file
View File

@ -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.

View File

@ -1,23 +1,44 @@
<?php
/*
Setup
*/
define('SRC', __DIR__ . '/../src');
require_once SRC . '/bootstrap.php';
$r = [];
/*
Home
*/
router_get($r, '/', function () {
echo render('layouts/basic', ['view' => 'pages/home']);
});
router_get($r, '/auth/register', 'auth_register_get');
router_post($r, '/auth/register', 'auth_register_post');
router_get($r, '/auth/login', 'auth_login_get');
router_post($r, '/auth/login', 'auth_login_post');
router_post($r, '/auth/logout', 'auth_logout');
/*
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();

View File

@ -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('/');
}

View File

@ -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();

176
src/controllers/auth.php Normal file
View File

@ -0,0 +1,176 @@
<?php
/**
* Displays the registration page.
*/
function auth_controller_register_get(): void
{
auth_guest();
echo render('layouts/basic', ['view' => '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('/');
}

67
src/controllers/char.php Normal file
View File

@ -0,0 +1,67 @@
<?php
/**
* Create a player for the currently logged in user.
*/
function char_controller_create_post(): void
{
auth_ensure();
csrf_ensure();
$errors = [];
$name = $_POST['name'] ?? '';
// Trim the input.
$name = trim($name);
/*
A name is required.
A name must be between 3 and 18 characters.
A name must contain only alphanumeric characters and spaces.
*/
if (empty($name) || strlen($name) < 3 || strlen($name) > 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('/');
}

View File

@ -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);
}

View File

@ -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'];
}

201
src/models/char.php Normal file
View File

@ -0,0 +1,201 @@
<?php
/*
Players are the living, breathing entities that interact with the game world. They are inextricably linked to their
accounts, and are the primary means by which the player interacts with the game world. Separating the player from
the account allows for multiple players to be associated with a single account, and to prevent concurrency issues
when performing auth checks on the database.
When creating a player, we want to init all of the related data tables; wallets, inventory, bank, etc.
When retrieving a player, we will get the tables as-needed, to prevent allocating more memory than we need.
*/
const currently = [
0 => '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);
}

View File

@ -1,37 +0,0 @@
<?php
/*
Players are the living, breathing entities that interact with the game world. They are inextricably linked to their
accounts, and are the primary means by which the player interacts with the game world. Separating the player from
the account allows for multiple players to be associated with a single account, and to prevent concurrency issues
when performing auth checks on the database.
When creating a player, we want to init all of the related data tables; wallets, inventory, bank, etc.
When retrieving a player, we will get the tables as-needed, to prevent allocating more memory than we need.
*/
/**
* Create a player. Only a user ID and a name are required. All other fields are optional. Pass a key-value array
* of overrides to set additional fields. A player's name must be unique, but this function does not check for that.
*/
function player_create(int $user_id, string $name, array $overrides = []): int
{
// Prep the data and merge in any overrides
$data = ['user_id' => $user_id, 'name' => $name];
if (!empty($overrides)) $data = array_merge($data, $overrides);
// Prep the fields for the query
$k = array_keys($data);
$f = implode(', ', array_keys($k));
$v = implode(', ', array_map(fn($x) => ":$x", $k));
// Create the player!
if (db_query(db_live(), "INSERT INTO players ($f) VALUES ($v)", $data) === false) {
// @TODO: Log this error
throw new Exception('Failed to create player.');
}
// Get the player ID
return db_live()->lastInsertRowID();
}

View File

@ -1,11 +1,32 @@
<?php if (!auth_check()): ?>
Hello, oppai!
<?php if (!user()): ?>
<h2>Welcome!</h2>
<a href="/auth/register">Register</a>
<a href="/auth/login">Login</a>
<?php else: ?>
Hello, <?= $_SESSION['user']['username'] ?>!
<h2>Hello, <?= user('username') ?>!</h2>
<?php if (user('char_id') !== 0): ?>
<h3>Playing as <?= char_find(user('char_id'))['name'] ?></h3>
<?php endif; ?>
<form action="/auth/logout" method="post">
<input type="hidden" name="csrf" value="<?= csrf() ?>">
<input type="submit" value="Logout">
</form>
<?php if (char_count(user('id')) > 0): ?>
<h3>Characters</h3>
<form action="character/select" method="post">
<input type="hidden" name="csrf" value="<?= csrf() ?>">
<?php foreach (char_list(user('id')) as $id => $char): ?>
<input type="radio" name="char_id" value="<?= $id ?>" id="char_<?= $id ?>">
<label for="char_<?= $id ?>"><?= $char['name'] ?> (Level <?= $char['level'] ?>)</label><br>
<?php endforeach; ?>
<input type="submit" value="Select Character">
</form>
<?php endif; ?>
<form action="/character/create" method="post">
<input type="hidden" name="csrf" value="<?= csrf() ?>">
<input type="text" name="name" placeholder="Character Name">
<input type="submit" value="Create Character">
</form>
<?php endif; ?>