diff --git a/public/css/admin.css b/public/css/admin.css index 17c7229..41bbb05 100644 --- a/public/css/admin.css +++ b/public/css/admin.css @@ -83,6 +83,15 @@ table { } } +.table-wrapper { + width: 100%; /* Ensure the wrapper takes 100% of the parent's width */ + max-height: 300px; /* Set the desired height limit */ + overflow-x: auto; /* Enable horizontal scrolling if the table overflows */ + overflow-y: auto; /* Enable vertical scrolling if needed */ + display: block; /* Ensure block-level behavior */ + -webkit-overflow-scrolling: touch; /* Smooth scrolling on touch devices */ +} + a { color: #015df7; text-decoration: none; diff --git a/public/index.php b/public/index.php index 8da4764..c70c28f 100644 --- a/public/index.php +++ b/public/index.php @@ -56,7 +56,7 @@ function index(): string redirect('/fight'); } - return Render\content($page); + return $page; } /** diff --git a/src/actions/admin.php b/src/actions/admin.php index bb8bdad..7f846ec 100644 --- a/src/actions/admin.php +++ b/src/actions/admin.php @@ -21,21 +21,21 @@ function register_routes(Router $r): Router $r->form('/admin/drops/:id', 'Admin\edit_drop'); $r->get('/admin/towns', 'Admin\towns'); - $r->form('/admin/towns/:id', 'Admin\edittown'); + $r->form('/admin/towns/:id', 'Admin\edit_town'); $r->get('/admin/monsters', 'Admin\monsters'); - $r->form('/admin/monsters/:id', 'Admin\editmonster'); + $r->form('/admin/monsters/:id', 'Admin\edit_monster'); - $r->form('/admin/level', 'Admin\levels'); - $r->form('/admin/level/:id', 'Admin\editlevel'); + $r->get('/admin/levels', 'Admin\levels'); + $r->post('/admin/levels', 'Admin\edit_level'); $r->get('/admin/spells', 'Admin\spells'); - $r->form('/admin/spells/:id', 'Admin\editspell'); + $r->form('/admin/spells/:id', 'Admin\edit_spell'); $r->get('/admin/users', 'Admin\users'); - $r->form('/admin/users/:id', 'Admin\edituser'); + $r->form('/admin/users/:id', 'Admin\edit_user'); - $r->form('/admin/news', 'Admin\addnews'); + $r->form('/admin/news', 'Admin\add_news'); } return $r; } @@ -45,7 +45,9 @@ function register_routes(Router $r): Router */ function donothing(): string { - $page = <<
@@ -59,9 +61,6 @@ function donothing(): string throughout the admin script, you should definitely pay attention and be careful when editing some fields, because mistakes in the database content may result in script errors or your game breaking completely. HTML; - - page_title('Admin'); - return \Render\content($page, 'layouts/admin'); } /** @@ -104,7 +103,7 @@ function primary(): string } page_title('Admin: Main Settings'); - return \Render\content($page, 'layouts/admin'); + return $page; } /** @@ -113,11 +112,11 @@ function primary(): string function items(): string { $items = db()->query('SELECT * FROM items ORDER BY id;'); - $page = "

Edit Items

Click an item's name or ID to edit it.

\n"; + $page = "

Edit Items

Click an item's name or ID to edit it.

\n"; $page .= build_bulk_table($items, 'name', '/admin/items'); page_title('Admin: Items'); - return \Render\content($page . '
', 'layouts/admin'); + return $page; } /** @@ -128,34 +127,19 @@ function edit_item(int $id): string $item = get_item($id); if (is_post()) { - $form = validate($_POST, [ + $page = handle_edit_form($id, 'items', validate($_POST, [ 'name' => [], 'type' => ['int', 'in:1,2,3'], 'buycost' => ['int', 'min:0'], 'attribute' => ['int', 'min:0'], 'special' => ['default:X'] - ]); - - if ($form['valid']) { - $f = $form['data']; - db()->query('UPDATE items SET name=?, type=?, buycost=?, attribute=?, special=? WHERE id=?;', [ - $f['name'], $f['type'], $f['buycost'], $f['attribute'], $f['special'], $id - ]); - $page = ''.$item['name'].' updated.'; - } else { - $error_list = ul_from_validate_errors($form['errors']); - $page = <<Errors:
-
{$error_list}

- Please go back and try again. - HTML; - } + ])); } else { $page = render('admin/edit_item', ['item' => $item]); } page_title('Admin: Editing '.$item['name']); - return \Render\content($page, 'layouts/admin'); + return $page; } /** @@ -164,11 +148,11 @@ function edit_item(int $id): string function drops() { $drops = db()->query('SELECT * FROM drops ORDER BY id;'); - $page = "

Edit Drops

Click an item's name to edit it.

\n"; + $page = "

Edit Drops

Click an item's name to edit it.

\n"; $page .= build_bulk_table($drops, 'name', '/admin/drops'); page_title('Admin: Drops'); - return \Render\content($page . '
', 'layouts/admin'); + return $page; } /** @@ -179,18 +163,280 @@ function edit_drop(int $id): string $drop = get_drop($id); if (is_post()) { - $form = validate($_POST, [ + $page = handle_edit_form($id, 'drops', validate($_POST, [ 'name' => [], 'mlevel' => ['int', 'min:1'], 'attribute1' => [], 'attribute2' => ['default:X'], + ])); + } else { + $page = render('admin/edit_drop', ['drop' => $drop]); + } + + page_title('Admin: Editing '.$drop['name']); + return $page; +} + +/** + * Generate the list of towns that can be edited. + */ +function towns(): string +{ + $towns = db()->query('SELECT * FROM towns ORDER BY id;'); + $page = "

Edit Towns

Click an town's name or ID to edit it.

\n"; + $page .= build_bulk_table($towns, 'name', '/admin/towns'); + + page_title('Admin: Towns'); + return $page; +} + +/** + * Save any changes to the town made. + */ +function edit_town(int $id): string +{ + $town = get_town_by_id($id); + + if (is_post()) { + $page = handle_edit_form($id, 'towns', validate($_POST, [ + 'name' => [], + 'latitude' => ['int', 'min:0', 'max:'.env('game_size')], + 'longitude' => ['int', 'min:0', 'max:'.env('game_size')], + 'innprice' => ['int', 'min:0'], + 'mapprice' => ['int', 'min:0'], + 'travelpoints' => ['int', 'min:0'], + 'itemslist' => ['optional'] + ])); + } else { + $page = render('admin/edit_town', ['town' => $town]); + } + + page_title('Admin: Editing '.$town['name']); + return $page; +} + +/** + * List the monsters available to edit. + */ +function monsters() +{ + $max_level = db()->query('SELECT level FROM monsters ORDER BY level DESC LIMIT 1;')->fetchArray(SQLITE3_ASSOC)['level']; + $monsters = db()->query('SELECT * FROM monsters ORDER BY id;'); + + $page = "

Edit Monsters

"; + + if ((env('game_size') / 5) !== $max_level) { + $page .= "Note: Your highest monster level does not match with your entered map size. Highest monster level should be ".(env('game_size') / 5).", yours is $max_level. Please fix this before opening the game to the public.
"; + } else { + $page .= "Monster level and map size match. No further actions are required for map compatibility.
"; + } + + $page .= "Click an monster's name or ID to edit it.

\n"; + $page .= build_bulk_table($monsters, 'name', '/admin/monsters'); + + page_title('Admin: Monsters'); + return $page; +} + +/** + * Handle the actual editing of the monster. + */ +function edit_monster(int $id): string +{ + $monster = get_monster($id); + + if (is_post()) { + $page = handle_edit_form($id, 'monsters', validate($_POST, [ + 'name' => [], + 'maxhp' => ['int', 'min:1'], + 'maxdam' => ['int', 'min:0'], + 'armor' => ['int', 'min:0'], + 'level' => ['int', 'min:1'], + 'maxexp' => ['int', 'min:0'], + 'maxgold' => ['int', 'min:0'], + 'immune' => ['in:0,1,2'] + ])); + } else { + $page = render('admin/edit_monster', ['monster' => $monster]); + } + + page_title('Admin: Editing '.$monster['name']); + return $page; +} + +/** + * List all spells available to edit. + */ +function spells(): string +{ + $page = "

Edit Spells

Click an spell's name to edit it.

\n"; + + $spells = db()->query('SELECT * FROM spells ORDER BY id;'); + $page .= build_bulk_table($spells, 'name', '/admin/spells'); + + page_title('Admin: Spells'); + return $page; +} + +/** + * Handle the editing of an individual spell. + */ +function edit_spell(int $id): string +{ + $spell = get_spell($id); + + if (is_post()) { + $page = handle_edit_form($id, 'spells', validate($_POST, [ + 'name' => [], + 'mp' => ['int', 'min:0'], + 'attribute' => ['int', 'min:0'], + 'type' => ['in:1,2,3,4,5'] + ])); + } else { + $page = render('admin/edit_spell', ['spell' => $spell]); + } + + page_title('Admin: Editing '.$spell['name']); + return $page; +} + +/** + * List all editable levels. + */ +function levels(): string +{ + $max_level = db()->query('SELECT id FROM levels ORDER BY id DESC LIMIT 1;')->fetchArray(SQLITE3_ASSOC)['id']; + + $page = <<Edit Levels + Select a level number from the dropdown box to edit it.

+
+ - Latitude:
Positive or negative integer. - Longitude:
Positive or negative integer. - Inn Price: gold - Map Price: gold
How much it costs to buy the map to this town. - Travel Points:
How many TP are consumed when travelling to this town. - Items List:
Comma-separated list of item ID numbers available for purchase at this town. (Example: 1,2,3,6,9,10,13,20) - - -
- HTML; - - display_admin(parse($page, $row), "Edit Towns"); -} - -function monsters() -{ - global $controlrow; - - $max_level = db()->query('SELECT level FROM monsters ORDER BY level DESC LIMIT 1;')->fetchArray(SQLITE3_ASSOC)['level']; - $monsters = db()->query('SELECT id, name FROM monsters ORDER BY id;'); - - $page = "Edit Monsters
"; - - if (($controlrow['gamesize'] / 5) !== $max_level) { - $page .= "Note: Your highest monster level does not match with your entered map size. Highest monster level should be ".($controlrow["gamesize"] / 5).", yours is $max_level. Please fix this before opening the game to the public.
"; - } else { - $page .= "Monster level and map size match. No further actions are required for map compatibility.
"; - } - - $page .= "Click an monster's name to edit it.

\n"; - - $has_monsters = false; - while ($row = $monsters->fetchArray(SQLITE3_ASSOC)) { - $has_monsters = true; - $page .= "\n"; - } - - if (!$has_monsters) { $page .= "\n"; } - - display_admin($page."
".$row["id"]."".$row["name"]."
No monsters found.
", "Edit Monster"); -} - -function editmonster($id) -{ - if (isset($_POST["submit"])) { - $n = trim($_POST['name'] ?? ''); - $mh = (int) trim($_POST['maxhp'] ?? 0); - $md = (int) trim($_POST['maxdam'] ?? 0); - $a = (int) trim($_POST['armor'] ?? 0); - $l = (int) trim($_POST['level'] ?? 0); - $me = (int) trim($_POST['maxexp'] ?? 0); - $mg = (int) trim($_POST['maxgold'] ?? 0); - - $errors = []; - if (empty($n)) $errors[] = "Name is required."; - if ($mh < 1) $errors[] = "Max HP must be a number greater than or equal to 1."; - if ($md < 0) $errors[] = "Max Damage must be a number greater than or equal to 0."; - if ($a < 0) $errors[] = "Armor must be a number greater than or equal to 0."; - if ($l < 1) $errors[] = "Level must be a number greater than or equal to 1."; - if ($me < 0) $errors[] = "Max Exp must be a number greater than or equal to 0."; - if ($mg < 0) $errors[] = "Max Gold must be a number greater than or equal to 0."; - - if (count($errors) === 0) { - db()->query('UPDATE monsters SET name=?, maxhp=?, maxdam=?, armor=?, level=?, maxexp=?, maxgold=?, immune=? WHERE id=?;', [ - $n, $mh, $md, $a, $l, $me, $mg, $_POST['immune'] ?? 0, $id - ]); - display_admin("Monster updated.", "Edit monsters"); - } else { - $errorlist = implode('
', $errors); - display_admin("Errors:
$errorlist

Please go back and try again.", "Edit monsters"); - } - } - - $row = get_monster($id); - - $page = <<Edit Monsters

-
- - - - - - - - - - -
ID:{{id}}
Name:
Max Hit Points:
Max Damage:
Compares to player's attackpower.
Armor:
Compares to player's defensepower.
Monster Level:
Determines spawn location and item drops.
Max Experience:
Max experience gained from defeating monster.
Max Gold:
Max gold gained from defeating monster.
Immunity:
Some monsters may not be hurt by certain spells.
- -
- HTML; - - if ($row["immune"] == 1) { $row["immune1select"] = "selected=\"selected\" "; } else { $row["immune1select"] = ""; } - if ($row["immune"] == 2) { $row["immune2select"] = "selected=\"selected\" "; } else { $row["immune2select"] = ""; } - if ($row["immune"] == 3) { $row["immune3select"] = "selected=\"selected\" "; } else { $row["immune3select"] = ""; } - - display_admin(parse($page, $row), "Edit Monsters"); -} - -function spells() -{ - $page = "Edit Spells
Click an spell's name to edit it.

\n"; - - $spells = db()->query('SELECT id, name FROM spells ORDER BY id;'); - $has_spells = false; - - while ($row = $spells->fetchArray(SQLITE3_ASSOC)) { - $has_spells = true; - $page .= "\n"; - } - - if (!$has_spells) { $page .= "\n"; } - - display_admin($page."
".$row["id"]."".$row["name"]."
No spells found.
", "Edit Spells"); -} - -function editspell($id) -{ - if (isset($_POST["submit"])) { - $n = trim($_POST['name'] ?? ''); - $mp = (int) trim($_POST['mp'] ?? 0); - $a = (int) trim($_POST['attribute'] ?? 0); - - $errors = []; - if (empty($n)) $errors[] = "Name is required."; - if ($mp < 0) $errors[] = "MP must be a number greater than or equal to 0."; - if ($a < 0) $errors[] = "Attribute must be a number greater than or equal to 0."; - - if (count($errors) === 0) { - db()->query('UPDATE spells SET name=?, mp=?, attribute=?, type=? WHERE id=?;', [ - $n, $mp, $a, $_POST['type'] ?? 0, $id - ]); - display_admin("Spell updated.", "Edit Spells"); - } else { - $errorlist = implode('
', $errors); - display_admin("Errors:
$errorlist

Please go back and try again.", "Edit Spells"); - } - } - - $row = get_spell($id); - - $page = <<Edit Spells

-
- - - - - - -
ID:{{id}}
Name:
Magic Points:
MP required to cast spell.
Attribute:
Numeric value of the spell's effect. Ties with type, below.
Type:
- Heal gives player back [attribute] hit points.
- Hurt deals [attribute] damage to monster.
- Sleep keeps monster from attacking ([attribute] is monster's chance out of 15 to stay asleep each turn).
- Uber Attack increases total attack damage by [attribute] percent.
- Uber Defense increases total defense from attack by [attribute] percent.
- -
- HTML; - - if ($row["type"] == 1) { $row["type1select"] = "selected=\"selected\" "; } else { $row["type1select"] = ""; } - if ($row["type"] == 2) { $row["type2select"] = "selected=\"selected\" "; } else { $row["type2select"] = ""; } - if ($row["type"] == 3) { $row["type3select"] = "selected=\"selected\" "; } else { $row["type3select"] = ""; } - if ($row["type"] == 4) { $row["type4select"] = "selected=\"selected\" "; } else { $row["type4select"] = ""; } - if ($row["type"] == 5) { $row["type5select"] = "selected=\"selected\" "; } else { $row["type5select"] = ""; } - - display_admin(parse($page, $row), "Edit Spells"); -} - -function levels() -{ - $max_level = db()->query('SELECT id FROM levels ORDER BY id DESC LIMIT 1;')->fetchArray(SQLITE3_ASSOC)['id']; - - $options = ""; - for ($i = 2; $i < $max_level; $i++) { - $options .= "\n"; - } - - $page = <<Edit Levels
- Select a level number from the dropdown box to edit it.

-
- - -
- HTML; - - display_admin($page, "Edit Levels"); -} - -function editlevel() -{ - if (!isset($_POST["level"])) display_admin("No level to edit.", "Edit Levels"); - $id = $_POST["level"]; - - if (isset($_POST["submit"])) { - unset($_POST['submit']); - $errors = []; - if ($_POST["one_exp"] == "") $errors[] = "Class 1 Experience is required."; - if ($_POST["one_hp"] == "") $errors[] = "Class 1 HP is required."; - if ($_POST["one_mp"] == "") $errors[] = "Class 1 MP is required."; - if ($_POST["one_tp"] == "") $errors[] = "Class 1 TP is required."; - if ($_POST["one_strength"] == "") $errors[] = "Class 1 Strength is required."; - if ($_POST["one_dexterity"] == "") $errors[] = "Class 1 Dexterity is required."; - if ($_POST["one_spells"] == "") $errors[] = "Class 1 Spells is required."; - if (!is_numeric($_POST["one_exp"])) $errors[] = "Class 1 Experience must be a number."; - if (!is_numeric($_POST["one_hp"])) $errors[] = "Class 1 HP must be a number."; - if (!is_numeric($_POST["one_mp"])) $errors[] = "Class 1 MP must be a number."; - if (!is_numeric($_POST["one_tp"])) $errors[] = "Class 1 TP must be a number."; - if (!is_numeric($_POST["one_strength"])) $errors[] = "Class 1 Strength must be a number."; - if (!is_numeric($_POST["one_dexterity"])) $errors[] = "Class 1 Dexterity must be a number."; - if (!is_numeric($_POST["one_spells"])) $errors[] = "Class 1 Spells must be a number."; - - if ($_POST["two_exp"] == "") $errors[] = "Class 2 Experience is required."; - if ($_POST["two_hp"] == "") $errors[] = "Class 2 HP is required."; - if ($_POST["two_mp"] == "") $errors[] = "Class 2 MP is required."; - if ($_POST["two_tp"] == "") $errors[] = "Class 2 TP is required."; - if ($_POST["two_strength"] == "") $errors[] = "Class 2 Strength is required."; - if ($_POST["two_dexterity"] == "") $errors[] = "Class 2 Dexterity is required."; - if ($_POST["two_spells"] == "") $errors[] = "Class 2 Spells is required."; - if (!is_numeric($_POST["two_exp"])) $errors[] = "Class 2 Experience must be a number."; - if (!is_numeric($_POST["two_hp"])) $errors[] = "Class 2 HP must be a number."; - if (!is_numeric($_POST["two_mp"])) $errors[] = "Class 2 MP must be a number."; - if (!is_numeric($_POST["two_tp"])) $errors[] = "Class 2 TP must be a number."; - if (!is_numeric($_POST["two_strength"])) $errors[] = "Class 2 Strength must be a number."; - if (!is_numeric($_POST["two_dexterity"])) $errors[] = "Class 2 Dexterity must be a number."; - if (!is_numeric($_POST["two_spells"])) $errors[] = "Class 2 Spells must be a number."; - - if ($_POST["three_exp"] == "") $errors[] = "Class 3 Experience is required."; - if ($_POST["three_hp"] == "") $errors[] = "Class 3 HP is required."; - if ($_POST["three_mp"] == "") $errors[] = "Class 3 MP is required."; - if ($_POST["three_tp"] == "") $errors[] = "Class 3 TP is required."; - if ($_POST["three_strength"] == "") $errors[] = "Class 3 Strength is required."; - if ($_POST["three_dexterity"] == "") $errors[] = "Class 3 Dexterity is required."; - if ($_POST["three_spells"] == "") $errors[] = "Class 3 Spells is required."; - if (!is_numeric($_POST["three_exp"])) $errors[] = "Class 3 Experience must be a number."; - if (!is_numeric($_POST["three_hp"])) $errors[] = "Class 3 HP must be a number."; - if (!is_numeric($_POST["three_mp"])) $errors[] = "Class 3 MP must be a number."; - if (!is_numeric($_POST["three_tp"])) $errors[] = "Class 3 TP must be a number."; - if (!is_numeric($_POST["three_strength"])) $errors[] = "Class 3 Strength must be a number."; - if (!is_numeric($_POST["three_dexterity"])) $errors[] = "Class 3 Dexterity must be a number."; - if (!is_numeric($_POST["three_spells"])) $errors[] = "Class 3 Spells must be a number."; - - if (count($errors) === 0) { - $updatequery = <<query($updatequery, [ - $one_exp, $one_hp, $one_mp, $one_tp, $one_strength, $one_dexterity, $one_spells, - $two_exp, $two_hp, $two_mp, $two_tp, $two_strength, $two_dexterity, $two_spells, - $three_exp, $three_hp, $three_mp, $three_tp, $three_strength, $three_dexterity, $three_spells, - $id - ]); - display_admin("Level updated.", "Edit Levels"); - } else { - $errorlist = implode('
', $errors); - display_admin("Errors:
$errorlist

Please go back and try again.", "Edit Spells"); - } - } - - - $row = db()->query('SELECT * FROM levels WHERE id=? LIMIT 1;', [$id])->fetchArray(SQLITE3_ASSOC); - - global $controlrow; - - $class1name = $controlrow["class1name"]; - $class2name = $controlrow["class2name"]; - $class3name = $controlrow["class3name"]; - - $page = <<Edit Levels

- Experience values for each level should be the cumulative total amount of experience up to this point. All other values should be only the new amount to add this level.

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ID:{{id}}
 
$class1name Experience:
$class1name HP:
$class1name MP:
$class1name TP:
$class1name Strength:
$class1name Dexterity:
$class1name Spells:
 
$class2name Experience:
$class2name HP:
$class2name MP:
$class2name TP:
$class2name Strength:
$class2name Dexterity:
$class2name Spells:
 
$class3name Experience:
$class3name HP:
$class3name MP:
$class3name TP:
$class3name Strength:
$class3name Dexterity:
$class3name Spells:
- -
- HTML; - - display_admin(parse($page, $row), "Edit Levels"); -} - -function users() -{ - $page = "Edit Users
Click a username to edit the account.

\n"; - - $users = db()->query('SELECT id, username FROM users ORDER BY id;'); - $has_users = false; - - while ($row = $users->fetchArray(SQLITE3_ASSOC)) { - $has_users = true; - $page .= "\n"; - } - - if (!$has_users) { $page .= "\n"; } - - display_admin($page."
".$row["id"]."".$row["username"]."
No spells found.
", "Edit Users"); -} - -function edituser($id) -{ - if (isset($_POST["submit"])) { - extract($_POST); - - $errors = []; - if ($email == "") $errors[] = "Email is required."; - if ($verify == "") $errors[] = "Verify is required."; - if ($authlevel == "") $errors[] = "Auth Level is required."; - if ($latitude == "") $errors[] = "Latitude is required."; - if ($longitude == "") $errors[] = "Longitude is required."; - if ($charclass == "") $errors[] = "Character Class is required."; - if ($currentaction == "") $errors[] = "Current Action is required."; - if ($currentfight == "") $errors[] = "Current Fight is required."; - - if ($currentmonster == "") $errors[] = "Current Monster is required."; - if ($currentmonsterhp == "") $errors[] = "Current Monster HP is required."; - if ($currentmonstersleep == "") $errors[] = "Current Monster Sleep is required."; - if ($currentmonsterimmune == "") $errors[] = "Current Monster Immune is required."; - if ($currentuberdamage == "") $errors[] = "Current Uber Damage is required."; - if ($currentuberdefense == "") $errors[] = "Current Uber Defense is required."; - if ($currenthp == "") $errors[] = "Current HP is required."; - if ($currentmp == "") $errors[] = "Current MP is required."; - if ($currenttp == "") $errors[] = "Current TP is required."; - if ($maxhp == "") $errors[] = "Max HP is required."; - - if ($maxmp == "") $errors[] = "Max MP is required."; - if ($maxtp == "") $errors[] = "Max TP is required."; - if ($level == "") $errors[] = "Level is required."; - if ($gold == "") $errors[] = "Gold is required."; - if ($experience == "") $errors[] = "Experience is required."; - if ($goldbonus == "") $errors[] = "Gold Bonus is required."; - if ($expbonus == "") $errors[] = "Experience Bonus is required."; - if ($strength == "") $errors[] = "Strength is required."; - if ($dexterity == "") $errors[] = "Dexterity is required."; - if ($attackpower == "") $errors[] = "Attack Power is required."; - - if ($defensepower == "") $errors[] = "Defense Power is required."; - if ($weaponid == "") $errors[] = "Weapon ID is required."; - if ($armorid == "") $errors[] = "Armor ID is required."; - if ($shieldid == "") $errors[] = "Shield ID is required."; - if ($slot1id == "") $errors[] = "Slot 1 ID is required."; - if ($slot2id == "") $errors[] = "Slot 2 ID is required."; - if ($slot3id == "") $errors[] = "Slot 3 ID is required."; - if ($weaponname == "") $errors[] = "Weapon Name is required."; - if ($armorname == "") $errors[] = "Armor Name is required."; - if ($shieldname == "") $errors[] = "Shield Name is required."; - - if ($slot1name == "") $errors[] = "Slot 1 Name is required."; - if ($slot2name == "") $errors[] = "Slot 2 Name is required."; - if ($slot3name == "") $errors[] = "Slot 3 Name is required."; - if ($dropcode == "") $errors[] = "Drop Code is required."; - if ($spells == "") $errors[] = "Spells is required."; - if ($towns == "") $errors[] = "Towns is required."; - - if (!is_numeric($authlevel)) $errors[] = "Auth Level must be a number."; - if (!is_numeric($latitude)) $errors[] = "Latitude must be a number."; - if (!is_numeric($longitude)) $errors[] = "Longitude must be a number."; - if (!is_numeric($charclass)) $errors[] = "Character Class must be a number."; - if (!is_numeric($currentfight)) $errors[] = "Current Fight must be a number."; - if (!is_numeric($currentmonster)) $errors[] = "Current Monster must be a number."; - if (!is_numeric($currentmonsterhp)) $errors[] = "Current Monster HP must be a number."; - if (!is_numeric($currentmonstersleep)) $errors[] = "Current Monster Sleep must be a number."; - - if (!is_numeric($currentmonsterimmune)) $errors[] = "Current Monster Immune must be a number."; - if (!is_numeric($currentuberdamage)) $errors[] = "Current Uber Damage must be a number."; - if (!is_numeric($currentuberdefense)) $errors[] = "Current Uber Defense must be a number."; - if (!is_numeric($currenthp)) $errors[] = "Current HP must be a number."; - if (!is_numeric($currentmp)) $errors[] = "Current MP must be a number."; - if (!is_numeric($currenttp)) $errors[] = "Current TP must be a number."; - if (!is_numeric($maxhp)) $errors[] = "Max HP must be a number."; - if (!is_numeric($maxmp)) $errors[] = "Max MP must be a number."; - if (!is_numeric($maxtp)) $errors[] = "Max TP must be a number."; - if (!is_numeric($level)) $errors[] = "Level must be a number."; - - if (!is_numeric($gold)) $errors[] = "Gold must be a number."; - if (!is_numeric($experience)) $errors[] = "Experience must be a number."; - if (!is_numeric($goldbonus)) $errors[] = "Gold Bonus must be a number."; - if (!is_numeric($expbonus)) $errors[] = "Experience Bonus must be a number."; - if (!is_numeric($strength)) $errors[] = "Strength must be a number."; - if (!is_numeric($dexterity)) $errors[] = "Dexterity must be a number."; - if (!is_numeric($attackpower)) $errors[] = "Attack Power must be a number."; - if (!is_numeric($defensepower)) $errors[] = "Defense Power must be a number."; - if (!is_numeric($weaponid)) $errors[] = "Weapon ID must be a number."; - if (!is_numeric($armorid)) $errors[] = "Armor ID must be a number."; - - if (!is_numeric($shieldid)) $errors[] = "Shield ID must be a number."; - if (!is_numeric($slot1id)) $errors[] = "Slot 1 ID must be a number."; - if (!is_numeric($slot2id)) $errors[] = "Slot 2 ID must be a number."; - if (!is_numeric($slot3id)) $errors[] = "Slot 3 ID must be a number."; - if (!is_numeric($dropcode)) $errors[] = "Drop Code must be a number."; - - if (count($errors) === 0) { - $updatequery = <<query($updatequery, [ - $email, $verify, $authlevel, $latitude, $longitude, $charclass, $currentaction, - $currentfight, $currentmonster, $currentmonsterhp, $currentmonstersleep, $currentmonsterimmune, - $currentuberdamage, $currentuberdefense, $currenthp, $currentmp, $currenttp, $maxhp, - $maxmp, $maxtp, $level, $gold, $experience, $goldbonus, $expbonus, $strength, - $dexterity, $attackpower, $defensepower, $weaponid, $armorid, $shieldid, $slot1id, - $slot2id, $slot3id, $weaponname, $armorname, $shieldname, $slot1name, $slot2name, - $slot3name, $dropcode, $spells, $towns, $id - ]); - - display_admin("User updated.", "Edit Users"); - } else { - $errorlist = implode('
', $errors); - display_admin("Errors:
$errorlist

Please go back and try again.", "Edit Users"); - } - } - - $row = db()->query('SELECT * FROM users WHERE id = ? LIMIT 1;', [$id])->fetchArray(SQLITE3_ASSOC); - - global $controlrow; - $class1name = $controlrow["class1name"]; - $class2name = $controlrow["class2name"]; - $class3name = $controlrow["class3name"]; - - $page = <<Edit Users

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ID:{{id}}
Username:{{username}}
Email:
Verify:
Register Date:{{regdate}}
Last Online:{{onlinetime}}
Auth Level:
Set to "Blocked" to temporarily (or permanently) ban a user.
 
Latitude:
Longitude:
Character Class:
 
Current Action:
Current Fight:
Current Monster:
Current Monster HP:
Current Monster Sleep:
Current Monster Immune:
Current Uber Damage:
Current Uber Defense:
 
Current HP:
Current MP:
Current TP:
Max HP:
Max MP:
Max TP:
 
Level:
Gold:
Experience:
Gold Bonus:
Experience Bonus:
Strength:
Dexterity:
Attack Power:
Defense Power:
 
Weapon ID:
Armor ID:
Shield ID:
Slot 1 ID:
Slot 2 ID:
Slot 3 ID:
Weapon Name:
Armor Name:
Shield Name:
Slot 1 Name:
Slot 2 Name:
Slot 3 Name:
 
Drop Code:
Spells:
Towns:
- -
- HTML; - - if ($row["authlevel"] == 0) { $row["auth0select"] = "selected=\"selected\" "; } else { $row["auth0select"] = ""; } - if ($row["authlevel"] == 1) { $row["auth1select"] = "selected=\"selected\" "; } else { $row["auth1select"] = ""; } - if ($row["authlevel"] == 2) { $row["auth2select"] = "selected=\"selected\" "; } else { $row["auth2select"] = ""; } - if ($row["charclass"] == 1) { $row["class1select"] = "selected=\"selected\" "; } else { $row["class1select"] = ""; } - if ($row["charclass"] == 2) { $row["class2select"] = "selected=\"selected\" "; } else { $row["class2select"] = ""; } - if ($row["charclass"] == 3) { $row["class3select"] = "selected=\"selected\" "; } else { $row["class3select"] = ""; } - - display_admin(parse($page, $row), "Edit Users"); -} - -function addnews() -{ - global $userrow; - - if (isset($_POST["submit"])) { + if (is_post()) { $c = trim($_POST['content'] ?? ''); $errors = []; if (empty($c)) $errors[] = "Content is required."; if (count($errors) === 0) { - db()->query('INSERT INTO news (author, content) VALUES (?, ?);', [$userrow['username'], $c]); - display_admin("News post added.", "Add News"); + db()->query('INSERT INTO news (author, content) VALUES (?, ?);', [user()->username, $c]); + $page = 'News post added.'; } else { - $errorlist = implode('
', $errors); - display_admin("Errors:
$errorlist

Please go back and try again.", "Add News"); + $error_list = implode('
', $errors); + $page = "Errors:
$error_list

Please go back and try again."; } - } - - $page = <<Add A News Post

-
+ } else { + $page = <<Add a News Post + Type your post below and then click Submit to add it.

- + +
- HTML; + HTML; + } - display_admin($page, "Add News"); + page_title('Admin: Add News'); + return $page; } /** - * Build an HTML table containing all columns and rows of a given data structure. Takes a SQLiteResult3 of a SELECT * + * Build an HTML table containing all columns and rows of a given data structure. Takes a SQLiteResult3 of a SELECT * query. */ function build_bulk_table(SQLite3Result $query_data, string $edit_column, string $edit_link): string { $data = []; - while ($row = $query_data->fetchArray(SQLITE3_ASSOC)) $data[] = $row; - if (empty($data)) return 'No data.'; + $data_count = 0; + while ($row = $query_data->fetchArray(SQLITE3_ASSOC)) $data[$data_count++] = $row; + if ($data_count === 0) return 'No data.'; + + $columns = array_diff(array_keys($data[0]), ['password']); + + $html_parts = [ + '', + str_repeat('', count($columns)), + '' + ]; - $columns = array_diff(array_keys($data[0]), ['password']); // Filter columns inline - $html = '
'; - foreach ($columns as $_) $html .= ''; - $html .= ''; foreach ($columns as $column) { - if ($column === 'id') $column = 'ID'; - $html .= ''; - } - $html .= ''; + $html_parts[] = ''; + } + $html_parts[] = ''; + + $is_edit_column = array_flip(['id', $edit_column]); foreach ($data as $row) { - $html .= ''; + $html_parts[] = ''; foreach ($columns as $column) { - $name = make_safe($row[$column]); - if (in_array($column, ['id', $edit_column])) { - $html .= <<{$name} - HTML; - } else { - $html .= ""; - } - } - $html .= ''; + $name = make_safe($row[$column]); + $html_parts[] = isset($is_edit_column[$column]) + ? "" + : ""; + } + $html_parts[] = ''; } - return $html . '
' . make_safe(ucfirst($column)) . '
' . + make_safe($column === 'id' ? 'ID' : ucfirst($column)) . + '
$name
{$name}{$name}
'; + $html_parts[] = ''; + return implode('', $html_parts); +} + +/** + * Save a row of data to it's table from the data supplied. + */ +function save_data_row(string $table, array $data, int $id): SQLite3Result|false +{ + $data = array_filter($data, fn($value) => $value !== null && $value !== ''); + if (empty($data)) return false; + + $fields = implode(',', array_map(fn($key) => "`$key`=?", array_keys($data))); + $values = array_values($data); + $values[] = $id; + + return db()->query("UPDATE $table SET $fields WHERE id=?", $values); +} + +/** + * Handle the result of a generic edit form. + */ +function handle_edit_form(int $id, string $table, array $form, string $updated_message = ''): string +{ + if ($form['valid']) { + save_data_row($table, $form['data'], $id); + $page = $updated_message ?: ''.$form['data']['name'].' updated.'; + } else { + $error_list = ul_from_validate_errors($form['errors']); + $page = <<Errors:
+
{$error_list}

+ Please go back and try again. + HTML; + } + + return $page; } diff --git a/src/actions/explore.php b/src/actions/explore.php index 30e2142..470a6fc 100644 --- a/src/actions/explore.php +++ b/src/actions/explore.php @@ -23,10 +23,7 @@ function move() { // Validate direction $form = validate($_POST, ['direction' => ['in:north,west,east,south']]); - if (!$form['valid']) { - $errors = ul_from_validate_errors($form['errors']); - return \Render\content($errors); - } + if (!$form['valid']) return ul_from_validate_errors($form['errors']); // Current game state $game_size = env('game_size'); diff --git a/src/actions/fight.php b/src/actions/fight.php index b5352bf..0d8f494 100644 --- a/src/actions/fight.php +++ b/src/actions/fight.php @@ -120,15 +120,15 @@ function fight() // Spell action if (isset($_POST["spell"])) { $pickedspell = $_POST["userspell"]; - if ($pickedspell == 0) return \Render\content('You must select a spell first. Please go back and try again.'); + if ($pickedspell == 0) return 'You must select a spell first. Please go back and try again.'; $newspellrow = get_spell($pickedspell); $spell = in_array($pickedspell, explode(',', user()->spells)); - if (!$spell) return \Render\content('You have not yet learned this spell. Please go back and try again.'); + if (!$spell) return 'You have not yet learned this spell. Please go back and try again.'; if (user()->currentmp < $newspellrow["mp"]) { - return \Render\content('You do not have enough Magic Points to cast this spell. Please go back and try again.'); + return 'You do not have enough Magic Points to cast this spell. Please go back and try again.'; } // Spell type handling (similar to original function) @@ -177,7 +177,7 @@ function fight() // Finalize page and display it $page = render('fight', ['page' => $page]); - return \Render\content($page); + return $page; } function victory() @@ -251,7 +251,7 @@ function victory() user()->save(); page_title($title); - return \Render\content($page); + return $page; } function drop() @@ -263,10 +263,7 @@ function drop() if (isset($_POST["submit"])) { $slot = $_POST["slot"]; - if ($slot == 0) { - $page = 'Please go back and select an inventory slot to continue.'; - return \Render\content($page); - } + if ($slot === 0) return 'Please go back and select an inventory slot to continue.'; $slotstr = 'slot'.$slot.'id'; if (user()->$slotstr != 0) { @@ -321,8 +318,7 @@ function drop() } user()->save(); - $page = 'The item has been equipped. You can now continue exploring.'; - return \Render\content($page); + return 'The item has been equipped. You can now continue exploring.'; } $attributearray = array("maxhp"=>"Max HP", @@ -352,19 +348,18 @@ function drop() $page .= "
"; $page .= "You may also choose to just continue exploring and give up this item."; - return \Render\content($page); + return $page; } function dead() { - $page = <<You have died.

As a consequence, you've lost half of your gold. However, you have been given back a portion of your hit points to continue your journey.

You may now continue back to town, and we hope you fair better next time. HTML; - return \Render\content($page); } function handleMonsterTurn(&$userrow, $monsterrow) diff --git a/src/actions/forum.php b/src/actions/forum.php index 90c106b..aa50a1f 100644 --- a/src/actions/forum.php +++ b/src/actions/forum.php @@ -56,7 +56,7 @@ function donothing($start = 0) $page .= ''; page_title('Forum'); - return \Render\content($page); + return $page; } function showthread($id, $start) @@ -72,7 +72,7 @@ function showthread($id, $start) $page .= "
Reply To This Thread:

"; page_title('Forum: '.$title['title']); - return \Render\content($page); + return $page; } function reply() @@ -110,7 +110,6 @@ function newthread() redirect('/forum/thread/'.db()->lastInsertRowID().'/0'); } - $page = "
Make A New Post:

Title:


Message:


"; page_title('Form: New Thread'); - return \Render\content($page); + return "
Make A New Post:

Title:


Message:


"; } diff --git a/src/actions/heal.php b/src/actions/heal.php index d3c8c42..371ccc5 100644 --- a/src/actions/heal.php +++ b/src/actions/heal.php @@ -30,5 +30,5 @@ function healspells(int $id): string } page_title('Casting '.$spell['name']); - return \Render\content($page); + return $page; } diff --git a/src/actions/towns.php b/src/actions/towns.php index f78e042..3506b2a 100644 --- a/src/actions/towns.php +++ b/src/actions/towns.php @@ -103,7 +103,7 @@ function inn() } page_title($town['name'] . ' Inn'); - return \Render\content($page); + return $page; } /** @@ -156,7 +156,7 @@ function shop() HTML; page_title($town['name'] . ' Shop'); - return \Render\content($page); + return $page; } /** @@ -186,8 +186,7 @@ function buy(int $id) ]; if (!isset($type_mapping[$item["type"]])) { // should never happen - $page = 'Error! Invalid item type...
'.var_dump($item); - return \Render\content($page, ''); + return 'Error! Invalid item type...
'.var_dump($item); } // Retrieve current equipped item or create a default @@ -271,7 +270,7 @@ function buy(int $id) } page_title('Buying '.$item['name']); - return \Render\content($page); + return $page; } /** @@ -317,7 +316,7 @@ function maps() HTML; page_title('Maps'); - return \Render\content($page); + return $page; } function buy_map(int $id): string @@ -352,7 +351,7 @@ function buy_map(int $id): string } page_title('Buying '.$town['name'].' Map'); - return \Render\content($page); + return $page; } /** @@ -398,5 +397,5 @@ function travelto(int $id, bool $use_points = true): string } page_title('Travelling to '.$town['name']); - return \Render\content($page); + return $page; } diff --git a/src/actions/users.php b/src/actions/users.php index 8f100fb..4b8b4b9 100644 --- a/src/actions/users.php +++ b/src/actions/users.php @@ -38,7 +38,7 @@ function login() } page_title('Login'); - return \Render\content(render('login')); + return render('login'); } /** @@ -98,7 +98,7 @@ function register() } page_title('Register'); - return \Render\content($page); + return $page; } function verify() @@ -113,10 +113,10 @@ function verify() db()->query("UPDATE users SET verify='g2g' WHERE username=?;", [$u]); - return \Render\content("Your account was verified successfully.

You may now continue to the Login Page and start playing the game.

Thanks for playing!"); + return "Your account was verified successfully.

You may now continue to the Login Page and start playing the game.

Thanks for playing!"; } - return \Render\content(render('verify')); + return render('verify'); } function lostpassword() @@ -132,13 +132,13 @@ function lostpassword() db()->query('UPDATE users SET password=? WHERE email=?;', [$hashed, $e]); if (sendpassemail($e, $newpass)) { - return \Render\content("Your new password was emailed to the address you provided.

Once you receive it, you may Log In and continue playing.

Thank you."); + return "Your new password was emailed to the address you provided.

Once you receive it, you may Log In and continue playing.

Thank you."; } else { - return \Render\content("There was an error sending your new password.

Please check with the game administrator for more information.

We apologize for the inconvience."); + return "There was an error sending your new password.

Please check with the game administrator for more information.

We apologize for the inconvience."; } } - return \Render\content(render('lostpassword')); + return render('lostpassword'); } function changepassword() @@ -170,7 +170,7 @@ function changepassword() $auth->logout(); - return \Render\content("Your password was changed successfully.

You have been logged out of the game to avoid errors.

Please log back in to continue playing."); + return "Your password was changed successfully.

You have been logged out of the game to avoid errors.

Please log back in to continue playing."; } } @@ -187,10 +187,10 @@ function settings() user()->save(); $alert = '
Settings updated
'; - return \Render\content($alert . render('settings')); + return $alert . render('settings'); } - return \Render\content(render('settings')); + return render('settings'); } function sendpassemail($emailaddress, $password) diff --git a/src/bootstrap.php b/src/bootstrap.php index b752958..021105c 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -63,6 +63,11 @@ if (!file_exists('../.installed') && $uri[0] !== 'install') { // need to install redirect('/'); } + // Update default page layout based on root endpoint + page_layout('layouts/primary'); + if ($uri[0] === 'admin') page_layout('layouts/admin'); + if ($uri[0] === 'help') page_layout('layouts/help'); + user()->update_online_time(); } else { $auth->logout(); diff --git a/src/lib.php b/src/lib.php index 4825697..b6e6f97 100644 --- a/src/lib.php +++ b/src/lib.php @@ -311,7 +311,7 @@ function validate(array $input_data, array $rules): array break; case 'int': - if (!filter_var($value, FILTER_VALIDATE_INT)) { + if (filter_var($value, FILTER_VALIDATE_INT) === false) { $errors[$field][] = "{$field_name} must be an integer."; } break; @@ -514,22 +514,33 @@ function page_title(string $new_title = ''): string */ function render_response(array $uri, string $content): string { - if (!is_htmx() || $uri[0] === 'babblebox') return $content; + if ($uri[0] === 'babblebox') return $content; - header('HX-Push-Url: '.$_SERVER['REQUEST_URI']); + if (is_htmx()) { + header('HX-Push-Url: '.$_SERVER['REQUEST_URI']); - $content .= ''.page_title().''; + $content .= ''.page_title().''; - $content .= Render\debug_db_info(); + $content .= Render\debug_db_info(); - if (env('debug', false)) { - $content .= Render\debug_query_log(); + if (env('debug', false)) { + $content .= Render\debug_query_log(); + } + + if ($GLOBALS['state']['user-state-changed'] ?? false) { + $content .= Render\right_nav(); + $content .= Render\left_nav(); + } } - if ($GLOBALS['state']['user-state-changed'] ?? false) { - $content .= Render\right_nav(); - $content .= Render\left_nav(); - } - - return $content; + return Render\content($content, page_layout()); +} + +/** + * Get/set page layout through GLOBALS state. + */ +function page_layout(string $layout = ''): string +{ + if ($layout === '') return $GLOBALS['state']['page-layout'] ?? 'layouts/primary'; + return $GLOBALS['state']['page-layout'] = $layout; } diff --git a/templates/admin/edit_level.php b/templates/admin/edit_level.php new file mode 100644 index 0000000..0805ba5 --- /dev/null +++ b/templates/admin/edit_level.php @@ -0,0 +1,33 @@ +

Edit Level

+Experience values for each level should be the cumulative total amount of experience up to this point. All other values should be only the new amount to add this level.

+ +
+ + + + + + + + + + + + + + + + + + + + + + ' : '' ?> + + +
ID:
EXP ">
HP ">
MP ">
TP ">
Strength ">
Dexterity">
Spells ">
+ + + +
diff --git a/templates/admin/edit_monster.php b/templates/admin/edit_monster.php new file mode 100644 index 0000000..7d3e7cc --- /dev/null +++ b/templates/admin/edit_monster.php @@ -0,0 +1,74 @@ +

Edit

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID
Name
Max HP
Max Damage +
+ Compares to player's attack power. +
Armor +
+ Compares to player's defense power. +
Monster Level +
+ Determines spawn location and item drops. +
Max EXP +
+ Max experience gained from defeating monster. +
Max Gold +
+ Max gold gained from defeating monster. +
Immunity + +
+ Some monsters may not be hurt by certain spells. +
+ + + +
diff --git a/templates/admin/edit_spell.php b/templates/admin/edit_spell.php new file mode 100644 index 0000000..cdc75b3 --- /dev/null +++ b/templates/admin/edit_spell.php @@ -0,0 +1,56 @@ +

Edit

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
ID
Name
MP + +
+ MP required to cast spell. +
Attribute + +
+ Numeric value of the spell's effect. Ties with type, below. +
Type + +
+ + - Heal gives player back [attribute] hit points.
+ - Hurt deals [attribute] damage to monster.
+ - Sleep keeps monster from attacking ([attribute] is monster's chance out of 15 to stay asleep each turn).
+ - Uber Attack increases total attack damage by [attribute] percent.
+ - Uber Defense increases total defense from attack by [attribute] percent. +
+
+ + + +
diff --git a/templates/admin/edit_town.php b/templates/admin/edit_town.php new file mode 100644 index 0000000..eb2b864 --- /dev/null +++ b/templates/admin/edit_town.php @@ -0,0 +1,58 @@ +

Edit

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID
Name
Latitude +
+ Positive or negative integer. +
Longitude +
+ Positive or negative integer. +
Inn Price gold
Map Price + gold
+ How much it costs to buy the map to this town. +
Travel Points +
+ How many TP are consumed when travelling to this town. +
Items List +
+ Comma-separated list of item ID numbers available for purchase at this town. + (Example: 1,2,3,6,9,10,13,20) +
+ + + +
diff --git a/templates/admin/edit_user.php b/templates/admin/edit_user.php new file mode 100644 index 0000000..a937b7e --- /dev/null +++ b/templates/admin/edit_user.php @@ -0,0 +1,89 @@ +

Edit

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID
Username
Email
Register Date
Last Online
Auth Level +
+ Set to "Blocked" to temporarily (or permanently) ban a user. +
Latitude
Longitude
Character Class + +
Current Action
Current Fight
Current Monster
Current Monster HP
Current Monster Sleep
Current Monster Immune
Current Uber Damage
Current Uber Defense
Current HP
Current MP
Current TP
Max HP
Max MP
Max TP
Level
Gold
Experience
Gold Bonus
EXP Bonus
Strength
Dexterity
Attack Power
Defense Power
Weapon ID
Armor ID
Shield ID
Slot 1 ID
Slot 2 ID
Slot 3 ID
Weapon Name
Armor Name
Shield Name
Slot 1 Name
Slot 2 Name
Slot 3 Name
Drop Code
Spells
Towns
+ + + +
diff --git a/templates/layouts/admin.php b/templates/layouts/admin.php index 024c19d..e9d19e3 100644 --- a/templates/layouts/admin.php +++ b/templates/layouts/admin.php @@ -28,7 +28,7 @@ Edit Drops
Edit Towns
Edit Monsters
- Edit Levels
+ Edit Levels
Edit Spells