commit fb568564a1cd43d3cde050ab544f74298878bed6
Author: @lakelinx
Date: Sat Jul 13 07:55:50 2024 -0700
initial
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..8c6e50cb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+# Thumb DB Files
+*.db
+.idea/
+config.php
+database.json
+.vs/
+.idea
+.vscode/
\ No newline at end of file
diff --git a/autoloader.php b/autoloader.php
new file mode 100644
index 00000000..21a32c9f
--- /dev/null
+++ b/autoloader.php
@@ -0,0 +1,26 @@
+setRoot(__DIR__);
+});
\ No newline at end of file
diff --git a/config/config.json b/config/config.json
new file mode 100644
index 00000000..0975cd9a
--- /dev/null
+++ b/config/config.json
@@ -0,0 +1,53 @@
+{
+ "site": {
+ "title": "Project Aatheria",
+ "logo": "images/logo.png",
+ "root_url": "https://alla.aatheria.com/",
+ "asset_url": "",
+ "spell_image_path": "images/spells/",
+ "spell_image_extension": ".png",
+ "setup": true
+ },
+ "session": {
+ "cookie_httponly": true,
+ "cookie_lifetime": 0,
+ "cookie_use_only_cookies": true,
+ "cookie_secure": true
+ },
+ "display": {
+ "php_debug": true,
+ "items_per_page": 50,
+ "allow_quests_npc": true,
+ "display_named_npcs_info": true,
+ "display_npc_stats": true,
+ "display_spawn_group_info": true,
+ "group_npcs_by_name": true,
+ "show_npc_drop_chances": true,
+ "show_npcs_attack_speed": true,
+ "show_npcs_average_damages": true,
+ "spawngroup_around_range": 100,
+ "trackable_npcs_only": false,
+ "hide_invisible_men": true,
+ "item_add_chance_to_drop": true,
+ "discovered_items_only": false,
+ "item_found_info": true,
+ "display_task_activities": true,
+ "display_task_info": true,
+ "merchants_dont_drop_stuff": true,
+ "sort_zone_level_list": true,
+ "use_spell_globals": false,
+ "legacy_spell_icons": true,
+ "ignore_zones": [
+ "load",
+ "loading",
+ "load2",
+ "nektropos",
+ "arttest",
+ "apprentice",
+ "tutorial"
+ ]
+ },
+ "setup": {
+ "complete": true
+ }
+}
\ No newline at end of file
diff --git a/config/config.json.template b/config/config.json.template
new file mode 100644
index 00000000..c9e464d8
--- /dev/null
+++ b/config/config.json.template
@@ -0,0 +1,50 @@
+{
+ "site": {
+ "title": "",
+ "logo": "images/logo.png",
+ "root_url": ""
+ },
+ "session": {
+ "cookie_httponly": true,
+ "cookie_lifetime": 0,
+ "cookie_use_only_cookies": true,
+ "cookie_secure": true
+ },
+ "display": {
+ "php_debug": false,
+ "items_per_page": 50,
+ "allow_quests_npc": true,
+ "display_named_npcs_info": true,
+ "display_npc_stats": true,
+ "display_spawn_group_info": true,
+ "group_npcs_by_name": true,
+ "show_npc_drop_chances": true,
+ "show_npcs_attack_speed": true,
+ "show_npcs_average_damages": true,
+ "spawngroup_around_range": 100,
+ "trackable_npcs_only": false,
+ "hide_invisible_men": true,
+ "item_add_chance_to_drop": true,
+ "discovered_items_only": false,
+ "item_found_info": true,
+ "display_task_activities": true,
+ "display_task_info": true,
+ "merchants_dont_drop_stuff": true,
+ "server_max_level": 70,
+ "sort_zone_level_list": true,
+ "use_spell_globals": false,
+ "use_legacy": true,
+ "ignore_zones": [
+ "load",
+ "loading",
+ "load2",
+ "nektropos",
+ "arttest",
+ "apprentice",
+ "tutorial"
+ ]
+ },
+ "setup": {
+ "complete": false
+ }
+}
\ No newline at end of file
diff --git a/config/database.json.template b/config/database.json.template
new file mode 100644
index 00000000..93b616d7
--- /dev/null
+++ b/config/database.json.template
@@ -0,0 +1,16 @@
+{
+ "default": {
+ "host": "127.0.0.1",
+ "user": "root",
+ "password": "",
+ "database": "peq",
+ "port": 3306
+ },
+ "content": {
+ "host": "",
+ "user": "",
+ "password": "",
+ "database": "",
+ "port": 3306
+ }
+}
\ No newline at end of file
diff --git a/config/tables.json b/config/tables.json
new file mode 100644
index 00000000..4f5aa50a
--- /dev/null
+++ b/config/tables.json
@@ -0,0 +1,31 @@
+{
+ "accounts": "account",
+ "characters": "character_data",
+ "factions": "faction_list",
+ "forage": "forage",
+ "ground_spawns": "ground_spawns",
+ "items": "items",
+ "loot_drops": "lootdrop",
+ "loot_drop_entries": "lootdrop_entries",
+ "loot_tables": "loottable",
+ "loot_table_entries": "loottable_entries",
+ "merchant_lists": "merchantlist",
+ "tasks": "tasks",
+ "task_activities": "task_activities",
+ "npc_factions": "npc_faction",
+ "npc_faction_entries": "npc_faction_entries",
+ "npc_spell_entries": "npc_spells_entries",
+ "npc_spells": "npc_spells",
+ "npcs": "npc_types",
+ "pets": "pets",
+ "spawns": "spawn2",
+ "spawn_entries": "spawnentry",
+ "spawn_groups": "spawngroup",
+ "recipes": "tradeskill_recipe",
+ "recipe_entries": "tradeskill_recipe_entries",
+ "zones": "zone",
+ "discovered_items": "discovered_items",
+ "spell_globals": "spell_globals",
+ "spells": "spells_new",
+ "zone_connections": "zone_points"
+}
\ No newline at end of file
diff --git a/includes/constants.php b/includes/constants.php
new file mode 100644
index 00000000..7bcb31c7
--- /dev/null
+++ b/includes/constants.php
@@ -0,0 +1,739 @@
+ "Ally",
+ 2 => "Warmly",
+ 3 => "Kindly",
+ 4 => "Amiably",
+ 5 => "Indifferent",
+ 9 => "Apprehensive",
+ 8 => "Dubious",
+ 7 => "Threatenly",
+ 6 => "Ready to attack",
+];
+
+// classes
+$dbclasses_names = [
+ "Warrior",
+ "Cleric",
+ "Paladin",
+ "Ranger",
+ "Shadowknight",
+ "Druid",
+ "Monk",
+ "Bard",
+ "Rogue",
+ "Shaman",
+ "Necromancer",
+ "Wizard",
+ "Magician",
+ "Enchanter",
+ "Beastlord",
+ "Berserker",
+];
+$dbclasses = [];
+$dbclasses[0] = "Warrior";
+$dbclasses[1] = "Warrior";
+$dbclasses[2] = "Cleric";
+$dbclasses[3] = "Paladin";
+$dbclasses[4] = "Ranger";
+$dbclasses[5] = "Shadown Knight";
+$dbclasses[6] = "Druid";
+$dbclasses[7] = "Monk";
+$dbclasses[8] = "Bard";
+$dbclasses[9] = "Rogue";
+$dbclasses[10] = "Shaman";
+$dbclasses[11] = "Necromancer";
+$dbclasses[12] = "Wizard";
+$dbclasses[13] = "Magician";
+$dbclasses[14] = "Enchanter";
+$dbclasses[15] = "Beastlord";
+$dbclasses[16] = "Berserker";
+$dbclasses[17] = "Banker";
+$dbclasses[20] = "GM Warrior";
+$dbclasses[21] = "GM Cleric";
+$dbclasses[22] = "GM Paladin";
+$dbclasses[23] = "GM Ranger";
+$dbclasses[24] = "GM Shadown Knight";
+$dbclasses[25] = "GM Druid";
+$dbclasses[26] = "GM Monk";
+$dbclasses[27] = "GM Bard";
+$dbclasses[28] = "GM Rogue";
+$dbclasses[29] = "GM Shaman";
+$dbclasses[30] = "GM Necromancer";
+$dbclasses[31] = "GM Wizard";
+$dbclasses[32] = "GM Magician";
+$dbclasses[33] = "GM Enchanter";
+$dbclasses[34] = "GM Beastlord";
+$dbclasses[35] = "GM Berserker";
+$dbclasses[40] = "Banker";
+$dbclasses[41] = "Shopkeeper";
+$dbclasses[59] = "Discord Merchant";
+$dbclasses[60] = "Adventure Recruiter";
+$dbclasses[61] = "Adventure Merchant";
+$dbclasses[63] = "Tribute Master";
+$dbclasses[64] = "Guild Tribute Master";
+$dbclasses[66] = "Guild Bank";
+$dbclasses[67] = "Radiant Crystal Merchant";
+$dbclasses[68] = "Ebon Crystal Merchant";
+$dbclasses[69] = "Fellowships";
+$dbclasses[70] = "Alternate Currency Merchant";
+$dbclasses[71] = "Mercenary Merchant";
+
+// Slots
+$dbslots = [];
+$dbslotsid = [];
+$dbslots[4194304] = "Power Source";
+$dbslots[2097152] = "Ammo";
+$dbslots[1048576] = "Waist";
+$dbslots[524288] = "Feet";
+$dbslots[262144] = "Legs";
+$dbslots[131072] = "Chest";
+$dbslots[98304] = "Fingers";
+$dbslots[65536] = "Finger";
+$dbslots[32768] = "Finger";
+$dbslots[16384] = "Secondary";
+$dbslots[8192] = "Primary";
+$dbslots[4096] = "Hands";
+$dbslots[2048] = "Range";
+$dbslots[1536] = "Wrists";
+$dbslots[1024] = "Wrist";
+$dbslots[512] = "Wrist";
+$dbslots[256] = "Back";
+$dbslots[128] = "Arms";
+$dbslots[64] = "Shoulders";
+$dbslots[32] = "Neck";
+$dbslots[18] = "Ears";
+$dbslots[16] = "Ear";
+$dbslots[8] = "Face";
+$dbslots[4] = "Head";
+$dbslots[2] = "Ear";
+$dbslots[1] = "Charm";
+
+// ItemClasses 2^(class-1)
+$dbiclasses = [];
+$dbiclasses[65535] = "All classes";
+$dbiclasses[32768] = "Berserker";
+$dbiclasses[16384] = "Beastlord";
+$dbiclasses[8192] = "Enchanter";
+$dbiclasses[4096] = "Magician";
+$dbiclasses[2048] = "Wizard";
+$dbiclasses[1024] = "Necromancer";
+$dbiclasses[512] = "Shaman";
+$dbiclasses[256] = "Rogue";
+$dbiclasses[128] = "Bard";
+$dbiclasses[64] = "Monk";
+$dbiclasses[32] = "Druid";
+$dbiclasses[16] = "Shadow knight";
+$dbiclasses[8] = "Ranger";
+$dbiclasses[4] = "Paladin";
+$dbiclasses[2] = "Cleric";
+$dbiclasses[1] = "Warrior";
+
+$db_classes_short = [];
+$db_classes_short[65535] = "ALL";
+$db_classes_short[32768] = "BER";
+$db_classes_short[16384] = "BST";
+$db_classes_short[8192] = "ENC";
+$db_classes_short[4096] = "MAG";
+$db_classes_short[2048] = "WIZ";
+$db_classes_short[1024] = "NEC";
+$db_classes_short[512] = "SHM";
+$db_classes_short[256] = "ROG";
+$db_classes_short[128] = "BRD";
+$db_classes_short[64] = "MNK";
+$db_classes_short[32] = "DRU";
+$db_classes_short[16] = "SHD";
+$db_classes_short[8] = "RNG";
+$db_classes_short[4] = "PAL";
+$db_classes_short[2] = "CLR";
+$db_classes_short[1] = "WAR";
+
+// races
+$dbraces = [];
+$dbraces[65535] = "All races";
+$dbraces[32768] = "Drakkin";
+$dbraces[16384] = "Froglok";
+$dbraces[8192] = "Vah Shir";
+$dbraces[4096] = "Iksar";
+$dbraces[2048] = "Gnome";
+$dbraces[1024] = "Halfling";
+$dbraces[512] = "Ogre";
+$dbraces[256] = "Troll";
+$dbraces[128] = "Dwarf";
+$dbraces[64] = "Half Elf";
+$dbraces[32] = "Dark Elf";
+$dbraces[16] = "High Elf";
+$dbraces[8] = "Wood Elf";
+$dbraces[4] = "Erudite";
+$dbraces[2] = "Barbarian";
+$dbraces[1] = "Human";
+
+$db_races_short = [];
+$db_races_short[65535] = "ALL";
+$db_races_short[32768] = "DRK";
+$db_races_short[16384] = "FRG";
+$db_races_short[8192] = "VAH";
+$db_races_short[4096] = "IKS";
+$db_races_short[2048] = "GNM";
+$db_races_short[1024] = "HFL";
+$db_races_short[512] = "OGR";
+$db_races_short[256] = "TRL";
+$db_races_short[128] = "DWF";
+$db_races_short[64] = "HLF";
+$db_races_short[32] = "DKE";
+$db_races_short[16] = "HEF";
+$db_races_short[8] = "WLF";
+$db_races_short[4] = "ERU";
+$db_races_short[2] = "BAR";
+$db_races_short[1] = "HUM";
+
+// skills
+$dbskills = [];
+$dbskills[0] = '1H_BLUNT';
+$dbskills[1] = '1H_SLASHING';
+$dbskills[2] = '2H_BLUNT';
+$dbskills[3] = '2H_SLASHING';
+$dbskills[4] = 'ABJURATION';
+$dbskills[5] = 'ALTERATION';
+$dbskills[6] = 'APPLY_POISON';
+$dbskills[7] = 'ARCHERY';
+$dbskills[8] = 'BACKSTAB';
+$dbskills[9] = 'BIND_WOUND';
+$dbskills[10] = 'BASH';
+$dbskills[11] = 'BLOCKSKILL';
+$dbskills[12] = 'BRASS_INSTRUMENTS';
+$dbskills[13] = 'CHANNELING';
+$dbskills[14] = 'CONJURATION';
+$dbskills[15] = 'DEFENSE';
+$dbskills[16] = 'DISARM';
+$dbskills[17] = 'DISARM_TRAPS';
+$dbskills[18] = 'DIVINATION';
+$dbskills[19] = 'DODGE';
+$dbskills[20] = 'DOUBLE_ATTACK';
+$dbskills[21] = 'DRAGON_PUNCH';
+$dbskills[22] = 'DUEL_WIELD';
+$dbskills[23] = 'EAGLE_STRIKE';
+$dbskills[24] = 'EVOCATION';
+$dbskills[25] = 'FEIGN_DEATH';
+$dbskills[26] = 'FLYING_KICK';
+$dbskills[27] = 'FORAGE';
+$dbskills[28] = 'HAND_TO_HAND';
+$dbskills[29] = 'HIDE';
+$dbskills[30] = 'KICK';
+$dbskills[31] = 'MEDITATE';
+$dbskills[32] = 'MEND';
+$dbskills[33] = 'OFFENSE';
+$dbskills[34] = 'PARRY';
+$dbskills[35] = 'PICK_LOCK';
+$dbskills[36] = 'PIERCING';
+$dbskills[37] = 'RIPOSTE';
+$dbskills[38] = 'ROUND_KICK';
+$dbskills[39] = 'SAFE_FALL';
+$dbskills[40] = 'SENSE_HEADING';
+$dbskills[41] = 'SINGING';
+$dbskills[42] = 'SNEAK';
+$dbskills[43] = 'SPECIALIZE_ABJURE';
+$dbskills[44] = 'SPECIALIZE_ALTERATION';
+$dbskills[45] = 'SPECIALIZE_CONJURATION';
+$dbskills[46] = 'SPECIALIZE_DIVINATION';
+$dbskills[47] = 'SPECIALIZE_EVOCATION';
+$dbskills[48] = 'PICK_POCKETS';
+$dbskills[49] = 'STRINGED_INSTRUMENTS';
+$dbskills[50] = 'SWIMMING';
+$dbskills[51] = 'THROWING';
+$dbskills[52] = 'CLICKY';
+$dbskills[53] = 'TRACKING';
+$dbskills[54] = 'WIND_INSTRUMENTS';
+$dbskills[55] = 'FISHING';
+$dbskills[56] = 'POISON_MAKING';
+$dbskills[57] = 'TINKERING';
+$dbskills[58] = 'RESEARCH';
+$dbskills[59] = 'ALCHEMY';
+$dbskills[60] = 'BAKING';
+$dbskills[61] = 'TAILORING';
+$dbskills[62] = 'SENSE_TRAPS';
+$dbskills[63] = 'BLACKSMITHING';
+$dbskills[64] = 'FLETCHING';
+$dbskills[65] = 'BREWING';
+$dbskills[66] = 'ALCOHOL_TOLERANCE';
+$dbskills[67] = 'BEGGING';
+$dbskills[68] = 'JEWELRY_MAKING';
+$dbskills[69] = 'POTTERY';
+$dbskills[70] = 'PERCUSSION_INSTRUMENTS';
+$dbskills[71] = 'INTIMIDATION';
+$dbskills[72] = 'BERSERKING';
+$dbskills[73] = 'TAUNT';
+
+// spell effects
+$dbspelleffects = [];
+
+$dbspelleffects[28] = 'Invisibility versus Undead';
+$dbspelleffects[29] = 'Invisibility versus Animals';
+$dbspelleffects[30] = 'Frenzy Radius';
+$dbspelleffects[31] = 'Mesmerize';
+$dbspelleffects[32] = 'Summon Item';
+$dbspelleffects[33] = 'Summon Pet:';
+$dbspelleffects[35] = 'Increase Disease Counter';
+$dbspelleffects[36] = 'Increase Poison Counter';
+$dbspelleffects[40] = 'Invunerability';
+$dbspelleffects[41] = 'Destroy Target';
+$dbspelleffects[42] = 'Shadowstep';
+$dbspelleffects[44] = 'Lycanthropy';
+$dbspelleffects[46] = 'Increase Fire Resist';
+$dbspelleffects[47] = 'Increase Cold Resist';
+$dbspelleffects[48] = 'Increase Poison Resist';
+$dbspelleffects[49] = 'Increase Disease Resist';
+$dbspelleffects[50] = 'Increase Magic Resist';
+$dbspelleffects[52] = 'Sense Undead';
+$dbspelleffects[53] = 'Sense Summoned';
+$dbspelleffects[54] = 'Sense Animals';
+$dbspelleffects[55] = 'Increase Absorb Damage';
+$dbspelleffects[56] = 'True North';
+$dbspelleffects[57] = 'Levitate';
+$dbspelleffects[58] = 'Illusion:';
+$dbspelleffects[59] = 'Increase Damage Shield';
+$dbspelleffects[61] = 'Identify';
+$dbspelleffects[63] = 'Memblur';
+$dbspelleffects[64] = 'SpinStun';
+$dbspelleffects[65] = 'Infravision';
+$dbspelleffects[66] = 'Ultravision';
+$dbspelleffects[67] = 'Eye Of Zomm';
+$dbspelleffects[68] = 'Reclaim Energy';
+$dbspelleffects[69] = 'Increase Max Hitpoints';
+$dbspelleffects[71] = 'Summon Skeleton Pet:';
+$dbspelleffects[73] = 'Bind Sight';
+$dbspelleffects[74] = 'Feign Death';
+$dbspelleffects[75] = 'Voice Graft';
+$dbspelleffects[76] = 'Sentinel';
+$dbspelleffects[77] = 'Locate Corpse';
+$dbspelleffects[78] = 'Increase Absorb Magic Damage';
+$dbspelleffects[79] = 'Increase HP when cast';
+$dbspelleffects[81] = 'Resurrect';
+$dbspelleffects[82] = 'Summon PC';
+$dbspelleffects[83] = 'Teleport';
+$dbspelleffects[85] = 'Add Proc:';
+$dbspelleffects[86] = 'Reaction Radius';
+$dbspelleffects[87] = 'Increase Magnification';
+$dbspelleffects[88] = 'Evacuate';
+$dbspelleffects[89] = 'Increase Player Size';
+$dbspelleffects[90] = 'Cloak';
+$dbspelleffects[91] = 'Summon Corpse';
+$dbspelleffects[92] = 'Increase hate';
+$dbspelleffects[93] = 'Stop Rain';
+$dbspelleffects[94] = 'Make Fragile (Delete if combat)';
+$dbspelleffects[95] = 'Sacrifice';
+$dbspelleffects[96] = 'Silence';
+$dbspelleffects[97] = 'Increase Mana Pool';
+$dbspelleffects[98] = 'Increase Haste v2';
+$dbspelleffects[99] = 'Root';
+$dbspelleffects[100] = 'Increase Hitpoints v2';
+$dbspelleffects[101] = 'Complete Heal (with duration)';
+$dbspelleffects[102] = 'Fearless';
+$dbspelleffects[103] = 'Call Pet';
+$dbspelleffects[104] = 'Translocate target to their bind point';
+$dbspelleffects[105] = 'Anti-Gate';
+$dbspelleffects[106] = 'Summon Warder:';
+$dbspelleffects[108] = 'Summon Familiar:';
+$dbspelleffects[109] = 'Summon Item v2';
+$dbspelleffects[111] = 'Increase All Resists';
+$dbspelleffects[112] = 'Increase Effective Casting Level';
+$dbspelleffects[113] = 'Summon Horse:';
+$dbspelleffects[114] = 'Increase Agro Multiplier';
+$dbspelleffects[115] = 'Food/Water';
+$dbspelleffects[116] = 'Decrease Curse Counter';
+$dbspelleffects[117] = 'Make Weapons Magical';
+$dbspelleffects[118] = 'Increase Singing Skill';
+$dbspelleffects[119] = 'Increase Haste v3';
+$dbspelleffects[120] = 'Set Healing Effectiveness';
+$dbspelleffects[121] = 'Reverse Damage Shield';
+$dbspelleffects[123] = 'Screech';
+$dbspelleffects[124] = 'Increase Spell Damage';
+$dbspelleffects[125] = 'Increase Spell Healing';
+$dbspelleffects[127] = 'Increase Spell Haste';
+$dbspelleffects[128] = 'Increase Spell Duration';
+$dbspelleffects[129] = 'Increase Spell Range';
+$dbspelleffects[130] = 'Decrease Spell/Bash Hate';
+$dbspelleffects[131] = 'Decrease Chance of Using Reagent';
+$dbspelleffects[132] = 'Decrease Spell Mana Cost';
+$dbspelleffects[134] = 'Limit: Max Level';
+$dbspelleffects[135] = 'Limit: Resist(Magic allowed)';
+$dbspelleffects[136] = 'Limit: Target';
+$dbspelleffects[137] = 'Limit: Effect(Hitpoints allowed)';
+$dbspelleffects[138] = 'Limit: Spell Type(Detrimental only)';
+$dbspelleffects[139] = 'Limit: Spell';
+$dbspelleffects[140] = 'Limit: Min Duration';
+$dbspelleffects[141] = 'Limit: Instant spells only';
+$dbspelleffects[142] = 'LimitMinLevel';
+$dbspelleffects[143] = 'Limit: Min Casting Time';
+$dbspelleffects[145] = 'Teleport v2';
+$dbspelleffects[147] = 'Increase Hitpoints';
+$dbspelleffects[148] = 'Block new spell';
+$dbspelleffects[149] = 'Stacking: Overwrite existing spell';
+$dbspelleffects[150] = 'Death Save - Restore Full Health';
+$dbspelleffects[151] = 'Suspend Pet - Lose Buffs and Equipment';
+$dbspelleffects[152] = 'Summon Pets:';
+$dbspelleffects[153] = 'Balance Party Health';
+$dbspelleffects[154] = 'Remove Detrimental';
+$dbspelleffects[156] = 'Illusion: Target';
+$dbspelleffects[157] = 'Spell-Damage Shield';
+$dbspelleffects[158] = 'Increase Chance to Reflect Spell';
+$dbspelleffects[159] = 'Decrease Stats';
+$dbspelleffects[167] = 'Pet Power Increase';
+$dbspelleffects[168] = 'Increase Melee Mitigation';
+$dbspelleffects[169] = 'Increase Chance to Critical Hit';
+$dbspelleffects[171] = 'CrippBlowChance';
+$dbspelleffects[172] = 'Increase Chance to Avoid Melee';
+$dbspelleffects[173] = 'Increase Chance to Riposte';
+$dbspelleffects[174] = 'Increase Chance to Dodge';
+$dbspelleffects[175] = 'Increase Chance to Parry';
+$dbspelleffects[176] = 'Increase Chance to Dual Wield';
+$dbspelleffects[177] = 'Increase Chance to Double Attack';
+$dbspelleffects[178] = 'Lifetap from Weapon Damage';
+$dbspelleffects[179] = 'Instrument Modifier';
+$dbspelleffects[180] = 'Increase Chance to Resist Spell';
+$dbspelleffects[181] = 'Increase Chance to Resist Fear Spell';
+$dbspelleffects[182] = 'Hundred Hands Effect';
+$dbspelleffects[183] = 'Increase All Skills Skill Check';
+$dbspelleffects[184] = 'Increase Chance to Hit With all Skills';
+$dbspelleffects[185] = 'Increase All Skills Damage Modifier';
+$dbspelleffects[186] = 'Increase All Skills Minimum Damage Modifier';
+$dbspelleffects[188] = 'Increase Chance to Block';
+$dbspelleffects[192] = 'Increase hate';
+$dbspelleffects[194] = 'Fade';
+$dbspelleffects[195] = 'Stun Resist';
+$dbspelleffects[200] = 'Increase Proc Modifier';
+$dbspelleffects[201] = 'Increase Range Proc Modifier';
+$dbspelleffects[205] = 'Rampage';
+$dbspelleffects[206] = 'Area of Effect Taunt';
+$dbspelleffects[216] = 'Increase Accuracy';
+$dbspelleffects[227] = 'Reduce Skill Timer';
+$dbspelleffects[254] = 'Blank';
+$dbspelleffects[266] = 'Increase Attack Chance';
+$dbspelleffects[273] = 'Increase Critical Dot Chance';
+$dbspelleffects[289] = 'Improved Spell Effect: ';
+$dbspelleffects[294] = 'Increase Critial Spell Chance';
+$dbspelleffects[299] = 'Wake the Dead';
+$dbspelleffects[311] = 'Limit: Combat Skills Not Allowed';
+$dbspelleffects[314] = 'Fixed Duration Invisbility (not documented on Lucy)';
+$dbspelleffects[323] = 'Add Defensive Proc:';
+$dbspelleffects[330] = 'Critical Damage Mob';
+
+
+// spell targets
+$dbspelltargets = [];
+$dbspelltargets[1] = "";
+$dbspelltargets[2] = "Area of effect over the caster";
+$dbspelltargets[3] = "Group teleport";
+$dbspelltargets[4] = "Area of effect around the caster";
+$dbspelltargets[5] = "Single target";
+$dbspelltargets[6] = "Self only";
+$dbspelltargets[8] = "Area of effect around the target";
+$dbspelltargets[9] = "Animal";
+$dbspelltargets[10] = "Undead only";
+$dbspelltargets[11] = "Summoned beings";
+$dbspelltargets[13] = "Tap";
+$dbspelltargets[14] = "Caster's pet";
+$dbspelltargets[15] = "Target's corpse";
+$dbspelltargets[16] = "Plant";
+$dbspelltargets[17] = "Giant";
+$dbspelltargets[18] = "Dragon";
+$dbspelltargets[24] = "Area of effect on undeads";
+$dbspelltargets[36] = "Area - PC Only";
+$dbspelltargets[40] = "Friendly area of effect";
+$dbspelltargets[41] = "Group";
+
+// item skills
+$dbiskills = [];
+$dbiskills[0] = "One Hand Slash";
+$dbiskills[1] = "Two Hands Slash";
+$dbiskills[2] = "Piercing";
+$dbiskills[3] = "One Hand Blunt";
+$dbiskills[4] = "Two Hands Blunt";
+$dbiskills[45] = "Hand to hand";
+
+// item types
+$dbitypes = [];
+$dbitypes[0] = "1HS";
+$dbitypes[1] = "2HS";
+$dbitypes[2] = "Piercing";
+$dbitypes[3] = "1HB";
+$dbitypes[4] = "2HB";
+$dbitypes[5] = "Archery";
+$dbitypes[6] = "Unknown";
+$dbitypes[7] = "Throwing range items";
+$dbitypes[8] = "Shield";
+$dbitypes[9] = "Unknown";
+$dbitypes[10] = "Armor";
+$dbitypes[11] = "Gems";
+$dbitypes[12] = "Lockpicks";
+$dbitypes[13] = "Unknown";
+$dbitypes[14] = "Food";
+$dbitypes[15] = "Drink";
+$dbitypes[16] = "Light";
+$dbitypes[17] = "Combinable";
+$dbitypes[18] = "Bandages";
+$dbitypes[19] = "Throwing";
+$dbitypes[20] = "Scroll";
+$dbitypes[21] = "Potion";
+$dbitypes[22] = "Unknown";
+$dbitypes[23] = "Wind Instrument";
+$dbitypes[24] = "Stringed Instrument";
+$dbitypes[25] = "Brass Instrument";
+$dbitypes[26] = "Percussion Instrument";
+$dbitypes[27] = "Arrow";
+$dbitypes[28] = "Unknown";
+$dbitypes[29] = "Jewelry";
+$dbitypes[30] = "Skull";
+$dbitypes[31] = "Tome";
+$dbitypes[32] = "Note";
+$dbitypes[33] = "Key";
+$dbitypes[34] = "Coin";
+$dbitypes[35] = "2H Piercing";
+$dbitypes[36] = "Fishing Pole";
+$dbitypes[37] = "Fishing Bait";
+$dbitypes[38] = "Alcohol";
+$dbitypes[39] = "Key (bis)";
+$dbitypes[40] = "Compass";
+$dbitypes[41] = "Unknown";
+$dbitypes[42] = "Poison";
+$dbitypes[43] = "Unknown";
+$dbitypes[44] = "Unknown";
+$dbitypes[45] = "Martial";
+$dbitypes[46] = "Unknown";
+$dbitypes[47] = "Unknown";
+$dbitypes[48] = "Unknown";
+$dbitypes[49] = "Unknown";
+$dbitypes[50] = "Unknown";
+$dbitypes[51] = "Unknown";
+$dbitypes[52] = "Charm";
+$dbitypes[53] = "Unknown";
+$dbitypes[54] = "Augmentation";
+
+$dbiaugrestrict[1] = "Armor Only";
+$dbiaugrestrict[2] = "Weapons Only";
+$dbiaugrestrict[3] = "1h Weapons Only";
+$dbiaugrestrict[4] = "2h Weapons Only";
+$dbiaugrestrict[5] = "1h Slash Only";
+$dbiaugrestrict[6] = "1h Blunt Only";
+$dbiaugrestrict[7] = "Piercing Only";
+$dbiaugrestrict[8] = "Hand To Hand Only";
+$dbiaugrestrict[9] = "2h Slash Only";
+$dbiaugrestrict[10] = "2h Blunt Only";
+$dbiaugrestrict[11] = "2h Pierce Only";
+$dbiaugrestrict[12] = "Bows Only";
+
+$dbbardskills[23] = "Wind";
+$dbbardskills[24] = "Strings";
+$dbbardskills[25] = "Brass";
+$dbbardskills[26] = "Percussions";
+$dbbardskills[51] = "All instruments";
+
+$NPCTypeArray = [
+ '###' => 'Boss',
+ '##' => 'Mini-Boss',
+ '#' => 'Named',
+ '~' => 'Quest NPC',
+ '!' => 'Hidden',
+ '_' => 'Event Spawned',
+];
+
+// damage bonuses 2Hands at 65
+//http://lucy.allakhazam.com/dmgbonus.html
+$dam2h = [
+ 0,
+ 14,
+ 14,
+ 14,
+ 14,
+ 14,
+ 14,
+ 14,
+ 14,
+ 14, // 0->9
+ 14,
+ 14,
+ 14,
+ 14,
+ 14,
+ 14,
+ 14,
+ 14,
+ 14,
+ 14, // 10->19
+ 14,
+ 14,
+ 14,
+ 14,
+ 14,
+ 14,
+ 14,
+ 14,
+ 35,
+ 35, // 20->29
+ 36,
+ 36,
+ 37,
+ 37,
+ 38,
+ 38,
+ 39,
+ 39,
+ 40,
+ 40, // 30->39
+ 42,
+ 42,
+ 42,
+ 45,
+ 45,
+ 47,
+ 48,
+ 49,
+ 49,
+ 51, // 40->49
+ 51,
+ 52,
+ 53,
+ 54,
+ 54,
+ 56,
+ 56,
+ 57,
+ 58,
+ 59, // 50->59
+ 59,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0, // 60->69
+ 68,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0, // 70->79
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 80,
+ 0,
+ 0,
+ 0,
+ 0, // 80->89
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 88,
+ 0,
+ 0,
+ 0,
+ 0, // 90->99
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0, // 100->109
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0, // 110->119
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0, // 120->129
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0, // 130->139
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0, // 140->149
+ 132,
+]; // 150
+
+
+
diff --git a/includes/functions.php b/includes/functions.php
new file mode 100644
index 00000000..630cd7d1
--- /dev/null
+++ b/includes/functions.php
@@ -0,0 +1,1243 @@
+";
+ }
+
+ return;
+}
+
+function wrap_content_box($content)
+{
+ $return_buffer = '
+
+
+ |
+ ' . $content . '
+ |
+
+
+ ';
+
+ return $return_buffer;
+}
+
+function display_header($header)
+{
+ return '
+
+ | ' . $header . ' |
+
+ ';
+}
+
+function display_table($content, $width = 500)
+{
+ $return_buffer = '
+
+ ';
+
+ return $return_buffer;
+}
+
+function display_row($left, $right = "")
+{
+ return '
+
+ | ' . $left . ' |
+ ' . $right . ' |
+
+ ';
+}
+
+function search_box($name = "", $value = "", $placeholder = "")
+{
+ return '
+
+ ';
+}
+
+function strip_underscores($string)
+{
+ $string = str_replace("_", " ", $string);
+
+ return $string;
+}
+
+function print_query_results(
+ $mysql_reference_data,
+ $rows_to_return,
+ $anchor_link_callout,
+ $query_description, /* Example: NPCs */
+ $object_description,
+ $href_id_name,
+ $href_name_attribute,
+ $extra_field = "",
+ $extra_field_description = "",
+ $extra_skill = ""
+) {
+ global $dbskills;
+
+ $mysql_rows_returned = mysqli_num_rows($mysql_reference_data);
+ if ($mysql_rows_returned > get_max_query_results_count($rows_to_return)) {
+ $mysql_rows_returned = get_max_query_results_count($rows_to_return);
+ $more_objects_exist = true;
+ } else {
+ $more_objects_exist = false;
+ }
+
+ if ($mysql_rows_returned == 0) {
+ $return_buffer .= "- No " . $query_description . " found.
";
+ } else {
+ $return_buffer .= "" . $mysql_rows_returned . " " . ($mysql_rows_returned == 1 ? $query_description : $object_description) . " displayed";
+ if ($more_objects_exist) {
+ $return_buffer .= " More " . $object_description . " exist but you reached the query limit.";
+ }
+ $return_buffer .= "
";
+ $return_buffer .= "";
+ }
+
+ return wrap_content_box($return_buffer);
+}
+
+function get_max_query_results_count($MaxObjects)
+{
+ if ($MaxObjects == 0) {
+ $Result = 2147483647;
+ } else {
+ $Result = $MaxObjects;
+ }
+
+ return $Result;
+}
+
+function get_npc_name_human_readable($DbName)
+{
+ $Result = str_replace(
+ '-',
+ '`',
+ str_replace(
+ '_',
+ ' ',
+ str_replace('#', '', str_replace('!', '', str_replace('~', '', $DbName)))
+ )
+ );
+ for ($i = 0; $i < 10; $i++) {
+ $Result = str_replace($i, '', $Result);
+ }
+
+ return $Result;
+}
+
+/** Returns the type of NPC based on the name of an NPC from its database-encoded '$DbName'.
+ */
+function NpcTypeFromName($DbName)
+{
+ global $NPCTypeArray;
+ foreach ($NPCTypeArray as $key => $type) {
+ $KeyCount = substr_count($DbName, $key);
+ $StringLength = strlen($DbName);
+ $KeyLength = strlen($key);
+ if ($KeyCount > 0 && substr($DbName, 0, $KeyLength) == $key) {
+ return $type;
+ }
+ }
+
+ return "Normal";
+}
+
+// Converts the first letter of each word in $str to upper case and the rest to lower case.
+function ucfirstwords($str)
+{
+ return ucwords(strtolower($str));
+}
+
+/** Returns the URL in the Wiki to the image illustrating the NPC with ID '$NpcId'
+ * Returns an empty string if the image does not exist in the Wiki
+ */
+function NpcImage($WikiServerUrl, $WikiRootName, $NpcId)
+{
+ $SystemCall = "wget -q \"" . $WikiServerUrl . $WikiRootName . "/index.php/Image:Npc-" . $NpcId . ".jpg\" -O -| grep \"/" . $WikiRootName . "/images\" | head -1 | sed 's;.*\\(/" . $WikiRootName . "/images/[^\"]*\\).*;\\1;'";
+ $Result = `$SystemCall`;
+ if ($Result != "") {
+ $Result = $WikiServerUrl . $Result;
+ }
+
+ return $Result;
+}
+
+/** Returns a human-readable translation of '$sec' seconds (for respawn times)
+ * If '$sec' is '0', returns 'time' (prints 'Spawns all the time' as a result)
+ */
+function translate_time($sec)
+{
+ if ($sec == 0) {
+ $Result = "time";
+ } else {
+ $h = floor($sec / 3600);
+ $m = floor(($sec - $h * 3600) / 60);
+ $s = $sec - $h * 3600 - $m * 60;
+ $Result = ($h > 1 ? "$h hours " : "") . ($h == 1 ? "1 hour " : "") . ($m > 0 ? "$m min " : "") . ($s > 0 ? "$s sec" : "");
+ }
+
+ return $Result;
+}
+
+/** Returns the rest of the euclidian division of '$d' by '$v'
+ * Returns '0' if '$v' equals '0'
+ * Supposes '$d' and '$v' are positive
+ */
+function modulo($d, $v)
+{
+ if ($v == 0) {
+ $Result = 0;
+ } else {
+ $s = floor($d / $v);
+ $Result = $d - $v * $s;
+ }
+}
+
+/** Returns the list of slot names '$val' corresponds to (as a bit field)
+ */
+function get_slots_string($val)
+{
+ global $dbslots;
+ reset($dbslots);
+ do {
+ $key = key($dbslots);
+ if ($key <= $val) {
+ $val -= $key;
+ $Result .= $v . current($dbslots);
+ $v = ", ";
+ }
+ } while (next($dbslots));
+
+ return $Result;
+}
+
+function get_class_usable_string($val)
+{
+ global $db_classes_short;
+ reset($db_classes_short);
+ do {
+ $key = key($db_classes_short);
+ if ($key <= $val) {
+ $val -= $key;
+ $res .= $v . current($db_classes_short);
+ $v = " ";
+ }
+ } while (next($db_classes_short));
+
+ return $res;
+}
+
+function get_race_usable_string($val)
+{
+ global $db_races_short;
+ reset($db_races_short);
+ do {
+ $key = key($db_races_short);
+ if ($key <= $val) {
+ $val -= $key;
+ $res .= $v . current($db_races_short);
+ $v = " ";
+ }
+ } while (next($db_races_short));
+
+ return $res;
+}
+
+function get_size_string($val)
+{
+ switch ($val) {
+ case 0:
+ return "Tiny";
+ break;
+ case 1:
+ return "Small";
+ break;
+ case 2:
+ return "Medium";
+ break;
+ case 3:
+ return "Large";
+ break;
+ case 4:
+ return "Giant";
+ break;
+ default:
+ return "$val?";
+ break;
+ }
+}
+
+function getspell($id)
+{
+ global $spells_table, $spell_globals_table, $use_spell_globals;
+ if ($use_spell_globals == true) {
+ $query = "SELECT " . $spells_table . ".* FROM " . $spells_table . " WHERE " . $spells_table . ".id=" . $id . "
+ AND ISNULL((SELECT " . $spell_globals_table . ".spellid FROM " . $spell_globals_table . "
+ WHERE " . $spell_globals_table . ".spellid = " . $spells_table . ".id))";
+ } else {
+ $query = "SELECT * FROM $spells_table WHERE id=$id";
+ }
+ $result = db_mysql_query($query) or message_die('functions.php', 'getspell', $query, mysqli_error());
+ $s = mysqli_fetch_array($result);
+
+ return $s;
+}
+
+function get_deity_usable_string($val)
+{
+ global $dbideities;
+ reset($dbideities);
+ do {
+ $key = key($dbideities);
+ if ($key <= $val) {
+ $val -= $key;
+ $res .= $v . current($dbideities);
+ $v = ", ";
+ }
+ } while (next($dbideities));
+
+ return $res;
+}
+
+function SelectMobRace($name, $selected)
+{
+ global $dbiracenames;
+ $return_buffer = "";
+
+ return $return_buffer;
+}
+
+function SelectLevel($name, $maxlevel, $selevel)
+{
+ $return_buffer = "";
+
+ return $return_buffer;
+}
+
+function WriteIt($value, $name, $sel)
+{
+ $return_buffer = "
";
+
+
+?>
diff --git a/public/zonenamedscsv.php b/public/zonenamedscsv.php
new file mode 100644
index 00000000..9843ed4f
--- /dev/null
+++ b/public/zonenamedscsv.php
@@ -0,0 +1,88 @@
+ 0) {
+ $cpt = 0;
+ while ($row = mysqli_fetch_array($result)) {
+ $spawns[$cpt] = floor($row["y"]) . " / " . floor($row["x"]) . " / " . floor($row["z"]);
+ $spawns[$cpt] .= " (" . translate_time($row["respawntime"]) . ")";
+ $cpt++;
+ }
+ }
+
+ if (($mymob["loottable_id"] > 0) AND ((!in_array($mymob["class"], $dbmerchants)) OR ($merchants_dont_drop_stuff == FALSE))) {
+ $query = "SELECT $items_table.id,$items_table.Name,$items_table.itemtype,
+ $loot_drop_entries_table.chance,$loot_table_entries.probability,
+ $loot_table_entries.lootdrop_id,$loot_table_entries.multiplier
+ FROM $items_table,$loot_table_entries,$loot_drop_entries_table
+ WHERE $loot_table_entries.loottable_id=" . $mymob["loottable_id"] . "
+ AND $loot_table_entries.lootdrop_id=$loot_drop_entries_table.lootdrop_id
+ AND $loot_drop_entries_table.item_id=$items_table.id";
+ $result = db_mysql_query($query) or message_die('npc.php', 'MYSQL_QUERY', $query, mysqli_error());
+ if (mysqli_num_rows($result) > 0) {
+ $cpt = 0;
+ while ($row = mysqli_fetch_array($result)) {
+ $loots[$cpt] = $row["Name"];
+ $loots[$cpt] .= " (" . $dbitypes[$row["itemtype"]] . ")";
+ $cpt++;
+ }
+ }
+ }
+ $n = max(count($spawns), count($loots));
+ for ($i = 0; $i < $n; $i++) {
+ if ($i == 0) {
+ print $txt;
+ } else {
+ print ",,,,";
+ }
+ if ($i < count($spawns)) {
+ print $spawns[$i];
+ }
+ print ",";
+ if ($i < count($loots)) {
+ print $loots[$i];
+ }
+ print "\n";
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/routes.php b/routes.php
new file mode 100644
index 00000000..2ab94323
--- /dev/null
+++ b/routes.php
@@ -0,0 +1,37 @@
+add('', 'Main#index');
+$router->add('global_search', 'Main#search');
+$router->add('factions', 'Faction#index');
+$router->add('faction', 'Faction#show');
+$router->add('items', 'Item#index');
+$router->add('item', 'Item#show');
+$router->add('login', 'Session#login');
+$router->add('logout', 'Session#logout');
+$router->add('npcs', 'Npc#index');
+$router->add('npc', 'Npc#show');
+$router->add('npc_advanced', 'Npc#advanced');
+$router->add('pets', 'Npc#petIndex');
+$router->add('pet', 'Npc#petShow');
+$router->add('recipes', 'Recipe#index');
+$router->add('recipe', 'Recipe#show');
+$router->add('signup', 'Session#signup');
+$router->add('spells', 'Spell#index');
+$router->add('spell', 'Spell#show');
+$router->add('tasks', 'Task#index');
+$router->add('task', 'Task#show');
+$router->add('zones', 'Zone#index');
+$router->add('zone', 'Zone#show');
+$router->add('zone_named', 'Zone#named');
+$router->add('zonelist', 'Zone#list');
+$router->add('zones_by_level', 'Zone#by_level');
+$router->add('zone_era', function($params) {
+ echo '';
+ echo " ";
+ require_once('pages/zones/zones_by_era/' . $params['era'] . '.php');
+ echo ' |
';
+});
+
+$router->dispatch($_GET['route'] ?? '');
diff --git a/src/Controllers/BaseController.php b/src/Controllers/BaseController.php
new file mode 100644
index 00000000..145fdd05
--- /dev/null
+++ b/src/Controllers/BaseController.php
@@ -0,0 +1,26 @@
+container = $container;
+ $this->config = Config::getInstance();
+ $this->template = new Template();
+ }
+
+ protected function filterSearchParams($params)
+ {
+ $searchParams = [];
+ foreach ($params as $key => $value) {
+ if ($value !== '' && $key !== 'page') {
+ $searchParams[$key] = $value;
+ }
+ }
+ return $searchParams;
+ }
+}
\ No newline at end of file
diff --git a/src/Controllers/FactionController.php b/src/Controllers/FactionController.php
new file mode 100644
index 00000000..11e4664f
--- /dev/null
+++ b/src/Controllers/FactionController.php
@@ -0,0 +1,53 @@
+factionModel = $container->get('FactionModel');
+ $this->factionService = $container->get('FactionService');
+ }
+
+ public function index()
+ {
+ $params = $_GET;
+ $currentPage = isset($params['page']) ? (int) $params['page'] : 1;
+ $limit = isset($params['limit']) ? (int) $params['limit'] : Config::getInstance()->get('display.items_per_page');
+ $offset = ($currentPage - 1) * $limit;
+
+ $searchParams = $this->filterSearchParams($params);
+ $searchQueryString = http_build_query($searchParams);
+
+ $factions = $this->factionModel->getPaginated($searchParams, $offset, $limit);
+ $totalItems = $this->factionModel->getItemCount($searchParams);
+
+ $pagination = new Pagination($totalItems, $currentPage, $limit, 'factions', $searchQueryString);
+
+ $this->template->render('factions/index', false, [
+ 'factions' => $factions,
+ 'searchQueryString' => $searchQueryString,
+ 'paginationData' => $pagination->getPaginationData(),
+ 'template' => $this->template,
+ ]);
+ }
+
+ public function show($id = null, $name = null)
+ {
+ $params = $_GET;
+
+ $id = $params['id'];
+ $faction = $this->factionService->getById($id);
+
+ if ($faction) {
+ $this->template->assign('faction', $faction);
+ $this->template->render('factions/show');
+ } else {
+ $this->template->assign('type', $params['route']);
+ $this->template->render('errors/404');
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Controllers/ItemController.php b/src/Controllers/ItemController.php
new file mode 100644
index 00000000..4956d099
--- /dev/null
+++ b/src/Controllers/ItemController.php
@@ -0,0 +1,54 @@
+itemModel = $container->get('ItemModel');
+ $this->itemService = $container->get('ItemService');
+ }
+
+ public function index()
+ {
+ $params = $_GET;
+ $currentPage = isset($params['page']) ? (int) $params['page'] : 1;
+ $limit = isset($params['pageLimit']) ? (int) $params['pageLimit'] : $this->config->get('display.items_per_page');
+ $offset = ($currentPage - 1) * $limit;
+
+ $searchParams = $this->filterSearchParams($params);
+ $searchQueryString = http_build_query($searchParams);
+
+ $items = $this->itemModel->getPaginated($searchParams, $offset, $limit);
+ $totalItems = $this->itemModel->getItemCount($searchParams);
+
+ $imageService = new ItemImageService($this->config, FileSystem::getInstance());
+ $pagination = new Pagination($totalItems, $currentPage, $limit, 'items', $searchQueryString);
+
+ $this->template->render('items/index', false, [
+ 'items' => $items,
+ 'searchQueryString' => $searchQueryString,
+ 'imageService' => $imageService,
+ 'paginationData' => $pagination->getPaginationData(),
+ 'template' => $this->template,
+ ]);
+ }
+
+ public function show($itemId = null) {
+ $params = $_GET;
+
+ $itemId = $params['id'];
+ $item = $this->itemService->getById($itemId);
+
+ if ($item) {
+ $this->template->assign('item', $item);
+ $this->template->render('items/show');
+ } else {
+ $this->template->assign('type', $params['route']);
+ $this->template->render('errors/404');
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Controllers/MainController.php b/src/Controllers/MainController.php
new file mode 100644
index 00000000..1c205ee6
--- /dev/null
+++ b/src/Controllers/MainController.php
@@ -0,0 +1,16 @@
+template = new Template();
+ }
+
+ public function index()
+ {
+ $this->template->render('index');
+ }
+}
\ No newline at end of file
diff --git a/src/Controllers/NpcController.php b/src/Controllers/NpcController.php
new file mode 100644
index 00000000..bad2bcd3
--- /dev/null
+++ b/src/Controllers/NpcController.php
@@ -0,0 +1,59 @@
+npcModel = $container->get('NpcModel');
+ $this->npcService = $container->get('NpcService');
+ }
+
+ public function index()
+ {
+ $params = $_GET;
+ $currentPage = isset($params['page']) ? (int) $params['page'] : 1;
+ $limit = isset($params['limit']) ? (int) $params['limit'] : $this->config->get('display.items_per_page');
+ $offset = ($currentPage - 1) * $limit;
+
+ $searchParams = $this->filterSearchParams($params);
+ $searchQueryString = http_build_query($searchParams);
+
+ $npcs = $this->npcModel->getPaginated($searchParams, $offset, $limit);
+ $totalItems = $this->npcModel->getItemCount($searchParams);
+
+ $imageService = new NpcImageService($this->config, FileSystem::getInstance());
+ $pagination = new Pagination($totalItems, $currentPage, $limit, 'npcs', $searchQueryString);
+
+ $this->template->render('npcs/index', false, [
+ 'npcs' => $npcs,
+ 'searchQueryString' => $searchQueryString,
+ 'imageService' => $imageService,
+ 'paginationData' => $pagination->getPaginationData(),
+ 'template' => $this->template,
+ ]);
+ }
+
+ public function show($id = null, $name = null)
+ {
+ $params = $_GET;
+
+ $id = $params['id'];
+ $npc = $this->npcService->getById($id);
+
+ if ($npc) {
+ $imageService = new NpcImageService($this->config, FileSystem::getInstance());
+ $this->template->render('npcs/show', false, [
+ 'npc' => $npc,
+ 'imageService' => $imageService
+ ]);
+ } else {
+ $this->template->render('errors/404', [
+ 'type' => $params['route']
+ ]);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Controllers/RecipeController.php b/src/Controllers/RecipeController.php
new file mode 100644
index 00000000..8053f0a9
--- /dev/null
+++ b/src/Controllers/RecipeController.php
@@ -0,0 +1,51 @@
+recipeModel = $container->get('RecipeModel');
+ }
+
+ public function index()
+ {
+ $params = $_GET;
+ $currentPage = isset($params['page']) ? (int) $params['page'] : 1;
+ $limit = isset($params['pageLimit']) ? (int) $params['pageLimit'] : Config::getInstance()->get('display.items_per_page');
+ $offset = ($currentPage - 1) * $limit;
+
+ $searchParams = $this->filterSearchParams($params);
+ $searchQueryString = http_build_query($searchParams);
+
+ $recipes = $this->recipeModel->getPaginated($searchParams, $offset, $limit);
+ $totalItems = $this->recipeModel->getItemCount($searchParams);
+
+ $pagination = new Pagination($totalItems, $currentPage, $limit, 'recipes', $searchQueryString);
+
+ $this->template->render('recipes/index', false, [
+ 'recipes' => $recipes,
+ 'searchQueryString' => $searchQueryString,
+ 'paginationData' => $pagination->getPaginationData(),
+ 'template' => $this->template,
+ ]);
+ }
+
+ public function show($id = null)
+ {
+ $params = $_GET;
+
+ $id = $params['id'];
+ $recipe = $this->recipeModel->getById($id);
+
+ if ($recipe) {
+ $this->template->assign('recipe', $recipe);
+ $this->template->render('recipes/show');
+ } else {
+ $this->template->assign('type', $params['route']);
+ $this->template->render('errors/404');
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Controllers/SessionController.php b/src/Controllers/SessionController.php
new file mode 100644
index 00000000..8de0c0e3
--- /dev/null
+++ b/src/Controllers/SessionController.php
@@ -0,0 +1,57 @@
+model = new SessionModel(Database::getInstance('sessions'));
+ $this->template = new Template();
+ }
+
+ public function login()
+ {
+ if (Request::isGet()) {
+ $this->template->render('sessions/login');
+ } elseif (Request::isPost()) {
+ $username = $_POST['username'] ?? null;
+ $password = $_POST['password'] ?? null;
+ if ($this->authenticate($username, $password)) {
+ Session::startSession();
+ Session::set('user_id', $user->id);
+ Session::set('is_authenticated', true);
+ Router::getInstance()->redirect('?');
+ } else {
+ $this->template->assign('error', 'Invalid credentials');
+ $this->template->render('sessions/login', false, [
+ 'npcs' => $npcs,
+ 'searchQueryString' => $searchQueryString,
+ 'npcImageService' => $npcImageService,
+ 'paginationData' => $pagination->getPaginationData(),
+ 'template' => $this->template,
+ ]);
+ }
+ }
+ }
+
+ public function logout()
+ {
+ Session::destroySession();
+ Router::getInstance()->redirect('?');
+ }
+
+ private function authenticate($username, $password)
+ {
+ // Check credentials against the database or other source
+ // Return true if authenticated, false otherwise
+ }
+
+ public function signup()
+ {
+ $getParams = $_GET;
+ $postParams = $_POST;
+ $this->template->render('sessions/signup');
+ }
+}
\ No newline at end of file
diff --git a/src/Controllers/SetupController.php b/src/Controllers/SetupController.php
new file mode 100644
index 00000000..1b56e127
--- /dev/null
+++ b/src/Controllers/SetupController.php
@@ -0,0 +1,99 @@
+template = new Template();
+ }
+
+ public function index()
+ {
+ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $this->handlePost();
+ } else {
+ $this->showSetupForm();
+ }
+ }
+
+ private function handlePost()
+ {
+ echo json_encode(['status' => 'error', 'message' => 'Invalid setup step']);
+ $step = isset($_POST['step']) ? $_POST['step'] : 'start';
+
+ switch ($step) {
+ case 'database':
+ $this->saveDatabase();
+ break;
+ case 'system_check':
+ $this->performSystemCheck();
+ break;
+ case 'final':
+ $this->finalizeSetup();
+ break;
+ default:
+ if ($this->isAjax()) {
+ echo json_encode(['status' => 'error', 'message' => 'Invalid setup step']);
+ } else {
+ $this->showSetupForm();
+ }
+ break;
+ }
+ }
+
+ private function saveDatabase()
+ {
+ $_SESSION['setup_step'] = 'system_check';
+ if ($this->isAjax()) {
+ if (Config::exists()) {
+ $config = Config::getInstance();
+ $config->set('database.host', $_POST['db_host'], true);
+ $config->set('database.port', $_POST['db_port'], true);
+ $config->set('database.name', $_POST['db_name'], true);
+ $config->set('database.user', $_POST['db_user'], true);
+ $config->set('database.password', $_POST['db_password']);
+ }
+ if (true) {
+ echo json_encode(['status' => 'success', 'next_step' => 'system_check']);
+ } else {
+ echo json_encode(['status' => 'error', 'message' => 'Database configuration failed.']);
+ }
+ } else {
+ $this->template->render('setup/index');
+ }
+ }
+
+ private function performSystemCheck()
+ {
+ $_SESSION['setup_step'] = 'final';
+ if (true) {
+ echo json_encode(['status' => 'success', 'next_step' => 'admin_setup']);
+ } else {
+ echo json_encode(['status' => 'error', 'message' => 'System check failed.']);
+ };
+ }
+
+ private function finalizeSetup()
+ {
+ session_destroy();
+ if (true) {
+ echo json_encode(['status' => 'success', 'message' => 'Setup complete.']);
+ } else {
+ echo json_encode(['status' => 'error', 'message' => 'Admin setup failed.']);
+ }
+ }
+
+ private function showSetupForm()
+ {
+ $currentStep = $_SESSION['setup_step'] ?? 'start';
+ $this->template->assign('setup_step', $currentStep);
+ $this->template->render('setup/index');
+ }
+
+ private function isAjax()
+ {
+ return !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
+ }
+}
\ No newline at end of file
diff --git a/src/Controllers/SpellController.php b/src/Controllers/SpellController.php
new file mode 100644
index 00000000..9d217cff
--- /dev/null
+++ b/src/Controllers/SpellController.php
@@ -0,0 +1,56 @@
+spellModel = $container->get('SpellModel');
+ }
+
+ public function index()
+ {
+ $params = $_GET;
+ $currentPage = $_GET['page'] ?? 1;
+ $limit = isset($params['pageLimit']) ? (int) $params['pageLimit'] : Config::getInstance()->get('display.items_per_page');
+ $offset = ($currentPage - 1) * $limit;
+
+ $searchParams = $this->filterSearchParams($params);
+ $searchQueryString = http_build_query($searchParams);
+
+ $spells = $this->spellModel->getPaginated($searchParams, $offset, $limit);
+ $totalItems = $this->spellModel->getItemCount($searchParams);
+
+ $imageService = new SpellImageService(Config::getInstance(), FileSystem::getInstance());
+ $pagination = new Pagination($totalItems, $currentPage, $limit, 'spells', $searchQueryString);
+
+ $this->template->render('spells/index', false, [
+ 'spells' => $spells,
+ 'searchQueryString' => $searchQueryString,
+ 'imageService' => $imageService,
+ 'paginationData' => $pagination->getPaginationData(),
+ 'template' => $this->template,
+ ]);
+ }
+
+ public function show($spellId = null)
+ {
+ $params = $_GET;
+
+ $spellId = $params['id'];
+ $spell = $this->spellModel->getById($spellId);
+
+ if ($spell) {
+ $iconService = new SpellImageService(Config::getInstance(), FileSystem::getInstance());
+ $this->template->assign('spell', $spell);
+ $this->template->assign('iconService', $iconService);
+
+ $this->template->render('spells/show');
+ } else {
+ $this->template->assign('type', $params['route']);
+ $this->template->render('errors/404');
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Controllers/TaskController.php b/src/Controllers/TaskController.php
new file mode 100644
index 00000000..d02ef350
--- /dev/null
+++ b/src/Controllers/TaskController.php
@@ -0,0 +1,51 @@
+taskModel = $container->get('TaskModel');
+ }
+
+ public function index()
+ {
+ $params = $_GET;
+ $currentPage = isset($params['page']) ? (int) $params['page'] : 1;
+ $limit = isset($params['pageLimit']) ? (int) $params['pageLimit'] : Config::getInstance()->get('display.items_per_page');
+ $offset = ($currentPage - 1) * $limit;
+
+ $searchParams = $this->filterSearchParams($params);
+ $searchQueryString = http_build_query($searchParams);
+
+ $tasks = $this->taskModel->getPaginated($$searchParams, $offset, $limit);
+ $totalItems = $this->taskModel->getItemCount($searchParams);
+
+ $pagination = new Pagination($totalItems, $currentPage, $limit, 'tasks', $searchQueryString);
+
+ $this->template->render('tasks/index', false, [
+ 'tasks' => $tasks,
+ 'searchQueryString' => $searchQueryString,
+ 'paginationData' => $pagination->getPaginationData(),
+ 'template' => $this->template,
+ ]);
+ }
+
+ public function show($taskId)
+ {
+ $params = $_GET;
+
+ $taskId = $params['id'];
+ $task = $this->taskModel->getById($taskId);
+
+ if ($task) {
+ $this->template->assign('task', $task);
+ $this->template->render('tasks/show');
+ } else {
+ $this->template->assign('type', $params['route']);
+ $this->template->render('errors/404');
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Controllers/ZoneController.php b/src/Controllers/ZoneController.php
new file mode 100644
index 00000000..afa61a02
--- /dev/null
+++ b/src/Controllers/ZoneController.php
@@ -0,0 +1,58 @@
+zoneModel = $container->get('ZoneModel');
+ $this->zoneService = $container->get('ZoneService');
+ }
+
+ public function index()
+ {
+ $params = $_GET;
+ $currentPage = isset($params['page']) ? (int) $params['page'] : 1;
+ $limit = isset($params['pageLimit']) ? (int) $params['pageLimit'] : $this->config->get('display.items_per_page');
+ $offset = ($currentPage - 1) * $limit;
+
+ $searchParams = $this->filterSearchParams($params);
+ $searchQueryString = http_build_query($searchParams);
+
+ $zones = $this->zoneModel->getPaginated($params, $offset, $limit);
+ $totalItems = $this->zoneModel->getItemCount($params);
+
+ $imageService = new ZoneImageService($this->config, FileSystem::getInstance());
+ $pagination = new Pagination($totalItems, $currentPage, $limit, 'zones', $searchQueryString);
+
+ $this->template->render('zones/index', false, [
+ 'zones' => $zones,
+ 'searchQueryString' => $searchQueryString,
+ 'imageService' => $imageService,
+ 'paginationData' => $pagination->getPaginationData(),
+ 'template' => $this->template,
+ ]);
+ }
+
+ public function show($shortName = null)
+ {
+ $params = $_GET;
+ $shortName = $params['short_name'];
+ $zone = $this->zoneService->getByShortName($shortName);
+
+ if ($zone) {
+ $imageService = new ZoneImageService($this->config, FileSystem::getInstance());
+
+ $this->template->render('zones/show', false, [
+ 'zone' => $zone,
+ 'mapUris' => $imageService->getImageUris($zone)
+ ]);
+ } else {
+ $this->template->assign('type', $params['route']);
+ $this->template->render('errors/404');
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Definitions/BagTypes.php b/src/Definitions/BagTypes.php
new file mode 100644
index 00000000..598c9e52
--- /dev/null
+++ b/src/Definitions/BagTypes.php
@@ -0,0 +1,55 @@
+ "Unknown",
+ 1 => "Humanoid",
+ 2 => "Lycanthrope",
+ 3 => "Undead",
+ 4 => "Giant",
+ 5 => "Construct",
+ 6 => "Extra planar",
+ 7 => "Magical",
+ 8 => "Summoned undead",
+ 9 => "Unknown",
+ 10 => "Unknown",
+ 11 => "No target",
+ 12 => "Vampire",
+ 13 => "Atenha Ra",
+ 14 => "Greater Akheva",
+ 15 => "Khati Sha",
+ 16 => "Unknown",
+ 17 => "Unknown",
+ 18 => "Unknown",
+ 19 => "Zek",
+ 20 => "Unknown",
+ 21 => "Animal",
+ 22 => "Insect",
+ 23 => "Monster",
+ 24 => "Summoned",
+ 25 => "Plant",
+ 26 => "Dragon",
+ 27 => "Summoned 2",
+ 28 => "Summoned 3",
+ 29 => "Unknown",
+ 30 => "Velious Dragon",
+ 31 => "Unknown",
+ 32 => "Dragon 3",
+ 33 => "Boxes",
+ 34 => "Discord Mob",
+ 60 => "No Target 2",
+ 63 => "Swarm pet",
+ 67 => "Special",
+ ];
+
+ public static function getName($id)
+ {
+ return static::$mapping[$id] ?? 'Unknown';
+ }
+
+ public static function getAll()
+ {
+ return static::$mapping;
+ }
+}
\ No newline at end of file
diff --git a/src/Definitions/BodyTypes.php b/src/Definitions/BodyTypes.php
new file mode 100644
index 00000000..7470dcb9
--- /dev/null
+++ b/src/Definitions/BodyTypes.php
@@ -0,0 +1,32 @@
+ "Alchemy",
+ 10 => "Tinkering",
+ 12 => "Poison Making",
+ 13 => "Special Quests",
+ 14 => "Baking",
+ 15 => "Baking",
+ 16 => "Tailoring",
+ 18 => "Fletching",
+ 20 => "Jewelry",
+ 30 => "Pottery",
+ 24 => "Research",
+ 25 => "Research",
+ 26 => "Research",
+ 27 => "Research",
+ 46 => "Fishing",
+ ];
+
+ public static function getName($id)
+ {
+ return static::$mapping[$id] ?? 'Unknown';
+ }
+
+ public static function getAll()
+ {
+ return static::$mapping;
+ }
+}
\ No newline at end of file
diff --git a/src/Definitions/ClassType.php b/src/Definitions/ClassType.php
new file mode 100644
index 00000000..de371813
--- /dev/null
+++ b/src/Definitions/ClassType.php
@@ -0,0 +1,77 @@
+ "Unknown",
+ 1 => "Warrior",
+ 2 => "Cleric",
+ 3 => "Paladin",
+ 4 => "Ranger",
+ 5 => "Shadow Knight",
+ 6 => "Druid",
+ 7 => "Monk",
+ 8 => "Bard",
+ 9 => "Rogue",
+ 10 => "Shaman",
+ 11 => "Necromancer",
+ 12 => "Wizard",
+ 13 => "Magician",
+ 14 => "Enchanter",
+ 15 => "Beastlord",
+ 16 => "Berserker",
+ 17 => "Banker",
+ 20 => "GM Warrior",
+ 21 => "GM Cleric",
+ 22 => "GM Paladin",
+ 23 => "GM Ranger",
+ 24 => "GM Shadow Knight",
+ 25 => "GM Druid",
+ 26 => "GM Monk",
+ 27 => "GM Bard",
+ 28 => "GM Rogue",
+ 29 => "GM Shaman",
+ 30 => "GM Necromancer",
+ 31 => "GM Wizard",
+ 32 => "GM Magician",
+ 33 => "GM Enchanter",
+ 34 => "GM Beastlord",
+ 35 => "GM Berserker",
+ 40 => "Banker",
+ 41 => "Shopkeeper",
+ 59 => "Discord Merchant",
+ 60 => "Adventure Recruiter",
+ 61 => "Adventure Merchant",
+ 63 => "Tribute Master",
+ 64 => "Guild Tribute Master",
+ 66 => "Guild Bank",
+ 67 => "Radiant Crystal Merchant",
+ 68 => "Ebon Crystal Merchant",
+ 69 => "Fellowships",
+ 70 => "Alternate Currency Merchant",
+ 71 => "Mercenary Merchant"
+ ];
+
+ protected static $merchantIds = [40, 41, 59, 61, 67, 68, 70];
+
+ protected static $playableIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
+
+ public static function getName($id)
+ {
+ return static::$mapping[$id] ?? 'Unknown';
+ }
+
+ public static function getPlayable()
+ {
+ $playableClasses = array_intersect_key(static::$mapping, array_flip(static::$playableIds));
+ asort($playableClasses);
+ return $playableClasses;
+ }
+
+ public static function getAll()
+ {
+ $classes = static::$mapping;
+ asort($classes);
+ return $classes;
+ }
+}
\ No newline at end of file
diff --git a/src/Definitions/Dieties.php b/src/Definitions/Dieties.php
new file mode 100644
index 00000000..51d28eda
--- /dev/null
+++ b/src/Definitions/Dieties.php
@@ -0,0 +1,35 @@
+ "Unknown",
+ 201 => "Bertoxxulous",
+ 202 => "Brell Serilis",
+ 203 => "Cazic Thule",
+ 204 => "Erollisi Marr",
+ 205 => "Bristlebane",
+ 206 => "Innoruuk",
+ 207 => "Karana",
+ 208 => "Mithaniel Marr",
+ 209 => "Prexus",
+ 210 => "Quellious",
+ 211 => "Rallos Zek",
+ 212 => "Rodcet Nife",
+ 213 => "Solusek Ro",
+ 214 => "The Tribunal",
+ 215 => "Tunare",
+ 216 => "Veeshan",
+ 396 => "Agnostic",
+ ];
+
+ public static function getName($id)
+ {
+ return static::$mapping[$id] ?? 'Unknown';
+ }
+
+ public static function getAll()
+ {
+ return static::$mapping;
+ }
+}
\ No newline at end of file
diff --git a/src/Definitions/Elements.php b/src/Definitions/Elements.php
new file mode 100644
index 00000000..26acdda0
--- /dev/null
+++ b/src/Definitions/Elements.php
@@ -0,0 +1,24 @@
+ "Unknown",
+ 1 => "Magic",
+ 2 => "Fire",
+ 3 => "Cold",
+ 4 => "Poison",
+ 5 => "Disease",
+ 6 => "Corruption",
+ ];
+
+ public static function getName($id)
+ {
+ return static::$mapping[$id] ?? 'Unknown';
+ }
+
+ public static function getAll()
+ {
+ return static::$mapping;
+ }
+}
\ No newline at end of file
diff --git a/src/Definitions/Expansions.php b/src/Definitions/Expansions.php
new file mode 100644
index 00000000..a354f7bc
--- /dev/null
+++ b/src/Definitions/Expansions.php
@@ -0,0 +1,56 @@
+ "Unknown",
+ 0 => "EverQuest Classic",
+ 1 => "The Ruins of Kunark",
+ 2 => "The Scars of Velious",
+ 3 => "The Shadows of Luclin",
+ 4 => "The Planes of Power",
+ 5 => "The Legacy of Ykesha",
+ 6 => "Lost Dungeons of Norrath",
+ 7 => "Gates of Discord",
+ 8 => "Omens of War",
+ 9 => "Dragons of Norrath",
+ 10 => "Depths of Darkhollow",
+ 11 => "Prophecy of Ro",
+ 12 => "The Serpent's Spine",
+ 13 => "The Buried Sea",
+ 14 => "Secrets of Faydwer",
+ 15 => "Seeds of Destruction",
+ 16 => "Underfoot",
+ 17 => "House of Thule",
+ 18 => "Veil of Alaris",
+ 19 => "Rain of Fear",
+ 20 => "Call of the Forsaken",
+ 21 => "The Darkened Sea",
+ 22 => "The Broken Mirror",
+ 23 => "Empires of Kunark",
+ 24 => "Ring of Scale",
+ 25 => "The Burning Lands",
+ 26 => "Torment of Velious"
+ ];
+
+ public static function getName($id)
+ {
+ return static::$mapping[$id] ?? 'Unknown';
+ }
+
+ public static function getAll()
+ {
+ return static::$mapping;
+ }
+
+ public static function getExpansionByZoneShortName($zoneShortName)
+ {
+ $zoneExpansionMap = [
+ 'qeynos' => 0
+ ];
+
+ // You will use the short name to find the expansion id
+ $expansionId = $zoneExpansionMap[$zoneShortName] ?? -1;
+ return static::$mapping[$expansionId] ?? 'Unknown';
+ }
+}
\ No newline at end of file
diff --git a/src/Definitions/ItemDieties.php b/src/Definitions/ItemDieties.php
new file mode 100644
index 00000000..0c909658
--- /dev/null
+++ b/src/Definitions/ItemDieties.php
@@ -0,0 +1,33 @@
+ "Veeshan",
+ 32768 => "Tunare",
+ 16384 => "The Tribunal",
+ 8192 => "Solusek Ro",
+ 4096 => "Rodcet Nife",
+ 2048 => "Rallos Zek",
+ 1024 => "Quellious",
+ 512 => "Prexus",
+ 256 => "Mithaniel Marr",
+ 128 => "Karana",
+ 64 => "Innoruuk",
+ 32 => "Bristlebane",
+ 16 => "Erollisi Marr",
+ 8 => "Cazic Thule",
+ 4 => "Brell Serilis",
+ 2 => "Bertoxxulous",
+ ];
+
+ public static function getName($id)
+ {
+ return static::$mapping[$id] ?? 'Unknown';
+ }
+
+ public static function getAll()
+ {
+ return static::$mapping;
+ }
+}
\ No newline at end of file
diff --git a/src/Definitions/Race.php b/src/Definitions/Race.php
new file mode 100644
index 00000000..2db602ff
--- /dev/null
+++ b/src/Definitions/Race.php
@@ -0,0 +1,722 @@
+ "Invalid",
+ 1 => "Human",
+ 2 => "Barbarian",
+ 3 => "Erudite",
+ 4 => "Wood Elf",
+ 5 => "High Elf",
+ 6 => "Dark Elf",
+ 7 => "Half Elf",
+ 8 => "Dwarf",
+ 9 => "Troll",
+ 10 => "Ogre",
+ 11 => "Halfling",
+ 12 => "Gnome",
+ 13 => "Aviak",
+ 14 => "Werewolf",
+ 15 => "Brownie",
+ 16 => "Centaur",
+ 17 => "Golem",
+ 18 => "Giant",
+ 19 => "Trakanon",
+ 20 => "Venril Sathir",
+ 21 => "Evil Eye",
+ 22 => "Beetle",
+ 23 => "Kerran",
+ 24 => "Fish",
+ 25 => "Fairy",
+ 26 => "Froglok",
+ 27 => "Froglok",
+ 28 => "Fungusman",
+ 29 => "Gargoyle",
+ 30 => "Gasbag",
+ 31 => "Gelatinous Cube",
+ 32 => "Ghost",
+ 33 => "Ghoul",
+ 34 => "Bat",
+ 35 => "Eel",
+ 36 => "Rat",
+ 37 => "Snake",
+ 38 => "Spider",
+ 39 => "Gnoll",
+ 40 => "Goblin",
+ 41 => "Gorilla",
+ 42 => "Wolf",
+ 43 => "Bear",
+ 44 => "Guard",
+ 45 => "Demi Lich",
+ 46 => "Imp",
+ 47 => "Griffin",
+ 48 => "Kobold",
+ 49 => "Dragon",
+ 50 => "Lion",
+ 51 => "Lizard Man",
+ 52 => "Mimic",
+ 53 => "Minotaur",
+ 54 => "Orc",
+ 55 => "Beggar",
+ 56 => "Pixie",
+ 57 => "Drachnid",
+ 58 => "Solusek Ro",
+ 59 => "Goblin",
+ 60 => "Skeleton",
+ 61 => "Shark",
+ 62 => "Tunare",
+ 63 => "Tiger",
+ 64 => "Treant",
+ 65 => "Vampire",
+ 66 => "Rallos Zek",
+ 67 => "Human",
+ 68 => "Tentacle Terror",
+ 69 => "Will-O-Wisp",
+ 70 => "Zombie",
+ 71 => "Human",
+ 72 => "Ship",
+ 73 => "Launch",
+ 74 => "Piranha",
+ 75 => "Elemental",
+ 76 => "Puma",
+ 77 => "Dark Elf",
+ 78 => "Erudite",
+ 79 => "Bixie",
+ 80 => "Reanimated Hand",
+ 81 => "Halfling",
+ 82 => "Scarecrow",
+ 83 => "Skunk",
+ 84 => "Snake Elemental",
+ 85 => "Spectre",
+ 86 => "Sphinx",
+ 87 => "Armadillo",
+ 88 => "Clockwork Gnome",
+ 89 => "Drake",
+ 90 => "Barbarian",
+ 91 => "Alligator",
+ 92 => "Troll",
+ 93 => "Ogre",
+ 94 => "Dwarf",
+ 95 => "Cazic Thule",
+ 96 => "Cockatrice",
+ 97 => "Daisy Man",
+ 98 => "Vampire",
+ 99 => "Amygdalan",
+ 100 => "Dervish",
+ 101 => "Efreeti",
+ 102 => "Tadpole",
+ 103 => "Kedge",
+ 104 => "Leech",
+ 105 => "Swordfish",
+ 106 => "Guard",
+ 107 => "Mammoth",
+ 108 => "Eye",
+ 109 => "Wasp",
+ 110 => "Mermaid",
+ 111 => "Harpy",
+ 112 => "Guard",
+ 113 => "Drixie",
+ 114 => "Ghost Ship",
+ 115 => "Clam",
+ 116 => "Seahorse",
+ 117 => "Ghost",
+ 118 => "Ghost",
+ 119 => "Sabertooth",
+ 120 => "Wolf",
+ 121 => "Gorgon",
+ 122 => "Dragon",
+ 123 => "Innoruuk",
+ 124 => "Unicorn",
+ 125 => "Pegasus",
+ 126 => "Djinn",
+ 127 => "Invisible Man",
+ 128 => "Iksar",
+ 129 => "Scorpion",
+ 130 => "Vah Shir",
+ 131 => "Sarnak",
+ 132 => "Draglock",
+ 133 => "Drolvarg",
+ 134 => "Mosquito",
+ 135 => "Rhinoceros",
+ 136 => "Xalgoz",
+ 137 => "Goblin",
+ 138 => "Yeti",
+ 139 => "Iksar",
+ 140 => "Giant",
+ 141 => "Boat",
+ 142 => "Uknown",
+ 143 => "Uknown",
+ 144 => "Burynai",
+ 145 => "Goo",
+ 146 => "Sarnak Spirit",
+ 147 => "Iksar Spirit",
+ 148 => "Fish",
+ 149 => "Scorpion",
+ 150 => "Erollisi",
+ 151 => "Tribunal",
+ 152 => "Bertoxxulous",
+ 153 => "Bristlebane",
+ 154 => "Fay Drake",
+ 155 => "Undead Sarnak",
+ 156 => "Ratman",
+ 157 => "Wyvern",
+ 158 => "Wurm",
+ 159 => "Devourer",
+ 160 => "Iksar Golem",
+ 161 => "Undead Iksar",
+ 162 => "Man-Eating Plant",
+ 163 => "Raptor",
+ 164 => "Sarnak Golem",
+ 165 => "Dragon",
+ 166 => "Animated Hand",
+ 167 => "Succulent",
+ 168 => "Holgresh",
+ 169 => "Brontotherium",
+ 170 => "Snow Dervish",
+ 171 => "Dire Wolf",
+ 172 => "Manticore",
+ 173 => "Totem",
+ 174 => "Ice Spectre",
+ 175 => "Enchanted Armor",
+ 176 => "Snow Rabbit",
+ 177 => "Walrus",
+ 178 => "Geonid",
+ 179 => "Uknown",
+ 180 => "Uknown",
+ 181 => "Yakkar",
+ 182 => "Faun",
+ 183 => "Coldain",
+ 184 => "Dragon",
+ 185 => "Hag",
+ 186 => "Hippogriff",
+ 187 => "Siren",
+ 188 => "Giant",
+ 189 => "Giant",
+ 190 => "Othmir",
+ 191 => "Ulthork",
+ 192 => "Dragon",
+ 193 => "Abhorrent",
+ 194 => "Sea Turtle",
+ 195 => "Dragon",
+ 196 => "Dragon",
+ 197 => "Ronnie Test",
+ 198 => "Dragon",
+ 199 => "Shik\'Nar",
+ 200 => "Rockhopper",
+ 201 => "Underbulk",
+ 202 => "Grimling",
+ 203 => "Worm",
+ 204 => "Evan Test",
+ 205 => "Shadel",
+ 206 => "Owlbear",
+ 207 => "Rhino Beetle",
+ 208 => "Vampire",
+ 209 => "Earth Elemental",
+ 210 => "Air Elemental",
+ 211 => "Water Elemental",
+ 212 => "Fire Elemental",
+ 213 => "Wetfang Minnow",
+ 214 => "Thought Horror",
+ 215 => "Tegi",
+ 216 => "Horse",
+ 217 => "Shissar",
+ 218 => "Fungal Fiend",
+ 219 => "Vampire",
+ 220 => "Stonegrabber",
+ 221 => "Scarlet Cheetah",
+ 222 => "Zelniak",
+ 223 => "Lightcrawler",
+ 224 => "Shade",
+ 225 => "Sunflower",
+ 226 => "Sun Revenant",
+ 227 => "Shrieker",
+ 228 => "Galorian",
+ 229 => "Netherbian",
+ 230 => "Akheva",
+ 231 => "Grieg Veneficus",
+ 232 => "Sonic Wolf",
+ 233 => "Ground Shaker",
+ 234 => "Vah Shir Skeleton",
+ 235 => "Wretch",
+ 236 => "Seru",
+ 237 => "Recuso",
+ 238 => "Vah Shir",
+ 239 => "Guard",
+ 240 => "Teleport Man",
+ 241 => "Werewolf",
+ 242 => "Nymph",
+ 243 => "Dryad",
+ 244 => "Treant",
+ 245 => "Fly",
+ 246 => "Tarew Marr",
+ 247 => "Solusek Ro",
+ 248 => "Clockwork Golem",
+ 249 => "Clockwork Brain",
+ 250 => "Banshee",
+ 251 => "Guard of Justice",
+ 252 => "Mini POM",
+ 253 => "Diseased Fiend",
+ 254 => "Solusek Ro Guard",
+ 255 => "Bertoxxulous",
+ 256 => "The Tribunal",
+ 257 => "Terris Thule",
+ 258 => "Vegerog",
+ 259 => "Crocodile",
+ 260 => "Bat",
+ 261 => "Hraquis",
+ 262 => "Tranquilion",
+ 263 => "Tin Soldier",
+ 264 => "Nightmare Wraith",
+ 265 => "Malarian",
+ 266 => "Knight of Pestilence",
+ 267 => "Lepertoloth",
+ 268 => "Bubonian",
+ 269 => "Bubonian Underling",
+ 270 => "Pusling",
+ 271 => "Water Mephit",
+ 272 => "Stormrider",
+ 273 => "Junk Beast",
+ 274 => "Broken Clockwork",
+ 275 => "Giant Clockwork",
+ 276 => "Clockwork Beetle",
+ 277 => "Nightmare Goblin",
+ 278 => "Karana",
+ 279 => "Blood Raven",
+ 280 => "Nightmare Gargoyle",
+ 281 => "Mouth of Insanity",
+ 282 => "Skeletal Horse",
+ 283 => "Saryrn",
+ 284 => "Fennin Ro",
+ 285 => "Tormentor",
+ 286 => "Soul Devourer",
+ 287 => "Nightmare",
+ 288 => "Rallos Zek",
+ 289 => "Vallon Zek",
+ 290 => "Tallon Zek",
+ 291 => "Air Mephit",
+ 292 => "Earth Mephit",
+ 293 => "Fire Mephit",
+ 294 => "Nightmare Mephit",
+ 295 => "Zebuxoruk",
+ 296 => "Mithaniel Marr",
+ 297 => "Undead Knight",
+ 298 => "The Rathe",
+ 299 => "Xegony",
+ 300 => "Fiend",
+ 301 => "Test Object",
+ 302 => "Crab",
+ 303 => "Phoenix",
+ 304 => "Dragon",
+ 305 => "Bear",
+ 306 => "Giant",
+ 307 => "Giant",
+ 308 => "Giant",
+ 309 => "Giant",
+ 310 => "Giant",
+ 311 => "Giant",
+ 312 => "Giant",
+ 313 => "War Wraith",
+ 314 => "Wrulon",
+ 315 => "Kraken",
+ 316 => "Poison Frog",
+ 317 => "Nilborien",
+ 318 => "Valorian",
+ 319 => "War Boar",
+ 320 => "Efreeti",
+ 321 => "War Boar",
+ 322 => "Valorian",
+ 323 => "Animated Armor",
+ 324 => "Undead Footman",
+ 325 => "Rallos Zek Minion",
+ 326 => "Arachnid",
+ 327 => "Crystal Spider",
+ 328 => "Zebuxoruk\'s Cage",
+ 329 => "BoT Portal",
+ 330 => "Froglok",
+ 331 => "Troll",
+ 332 => "Troll",
+ 333 => "Troll",
+ 334 => "Ghost",
+ 335 => "Pirate",
+ 336 => "Pirate",
+ 337 => "Pirate",
+ 338 => "Pirate",
+ 339 => "Pirate",
+ 340 => "Pirate",
+ 341 => "Pirate",
+ 342 => "Pirate",
+ 343 => "Frog",
+ 344 => "Troll Zombie",
+ 345 => "Luggald",
+ 346 => "Luggald",
+ 347 => "Luggalds",
+ 348 => "Drogmore",
+ 349 => "Froglok Skeleton",
+ 350 => "Undead Froglok",
+ 351 => "Knight of Hate",
+ 352 => "Arcanist of Hate",
+ 353 => "Veksar",
+ 354 => "Veksar",
+ 355 => "Veksar",
+ 356 => "Chokadai",
+ 357 => "Undead Chokadai",
+ 358 => "Undead Veksar",
+ 359 => "Vampire",
+ 360 => "Vampire",
+ 361 => "Rujarkian Orc",
+ 362 => "Bone Golem",
+ 363 => "Synarcana",
+ 364 => "Sand Elf",
+ 365 => "Vampire",
+ 366 => "Rujarkian Orc",
+ 367 => "Skeleton",
+ 368 => "Mummy",
+ 369 => "Goblin",
+ 370 => "Insect",
+ 371 => "Froglok Ghost",
+ 372 => "Dervish",
+ 373 => "Shade",
+ 374 => "Golem",
+ 375 => "Evil Eye",
+ 376 => "Box",
+ 377 => "Barrel",
+ 378 => "Chest",
+ 379 => "Vase",
+ 380 => "Table",
+ 381 => "Weapon Rack",
+ 382 => "Coffin",
+ 383 => "Bones",
+ 384 => "Jokester",
+ 385 => "Nihil",
+ 386 => "Trusik",
+ 387 => "Stone Worker",
+ 388 => "Hynid",
+ 389 => "Turepta",
+ 390 => "Cragbeast",
+ 391 => "Stonemite",
+ 392 => "Ukun",
+ 393 => "Ixt",
+ 394 => "Ikaav",
+ 395 => "Aneuk",
+ 396 => "Kyv",
+ 397 => "Noc",
+ 398 => "Ra`tuk",
+ 399 => "Taneth",
+ 400 => "Huvul",
+ 401 => "Mutna",
+ 402 => "Mastruq",
+ 403 => "Taelosian",
+ 404 => "Discord Ship",
+ 405 => "Stone Worker",
+ 406 => "Mata Muram",
+ 407 => "Lightning Warrior",
+ 408 => "Succubus",
+ 409 => "Bazu",
+ 410 => "Feran",
+ 411 => "Pyrilen",
+ 412 => "Chimera",
+ 413 => "Dragorn",
+ 414 => "Murkglider",
+ 415 => "Rat",
+ 416 => "Bat",
+ 417 => "Gelidran",
+ 418 => "Discordling",
+ 419 => "Girplan",
+ 420 => "Minotaur",
+ 421 => "Dragorn Box",
+ 422 => "Runed Orb",
+ 423 => "Dragon Bones",
+ 424 => "Muramite Armor Pile",
+ 425 => "Crystal Shard",
+ 426 => "Portal",
+ 427 => "Coin Purse",
+ 428 => "Rock Pile",
+ 429 => "Murkglider Egg Sack",
+ 430 => "Drake",
+ 431 => "Dervish",
+ 432 => "Drake",
+ 433 => "Goblin",
+ 434 => "Kirin",
+ 435 => "Dragon",
+ 436 => "Basilisk",
+ 437 => "Dragon",
+ 438 => "Dragon",
+ 439 => "Puma",
+ 440 => "Spider",
+ 441 => "Spider Queen",
+ 442 => "Animated Statue",
+ 443 => "Uknown",
+ 444 => "Uknown",
+ 445 => "Dragon Egg",
+ 446 => "Dragon Statue",
+ 447 => "Lava Rock",
+ 448 => "Animated Statue",
+ 449 => "Spider Egg Sack",
+ 450 => "Lava Spider",
+ 451 => "Lava Spider Queen",
+ 452 => "Dragon",
+ 453 => "Giant",
+ 454 => "Werewolf",
+ 455 => "Kobold",
+ 456 => "Sporali",
+ 457 => "Gnomework",
+ 458 => "Orc",
+ 459 => "Corathus",
+ 460 => "Coral",
+ 461 => "Drachnid",
+ 462 => "Drachnid Cocoon",
+ 463 => "Fungus Patch",
+ 464 => "Gargoyle",
+ 465 => "Witheran",
+ 466 => "Dark Lord",
+ 467 => "Shiliskin",
+ 468 => "Snake",
+ 469 => "Evil Eye",
+ 470 => "Minotaur",
+ 471 => "Zombie",
+ 472 => "Clockwork Boar",
+ 473 => "Fairy",
+ 474 => "Witheran",
+ 475 => "Air Elemental",
+ 476 => "Earth Elemental",
+ 477 => "Fire Elemental",
+ 478 => "Water Elemental",
+ 479 => "Alligator",
+ 480 => "Bear",
+ 481 => "Scaled Wolf",
+ 482 => "Wolf",
+ 483 => "Spirit Wolf",
+ 484 => "Skeleton",
+ 485 => "Spectre",
+ 486 => "Bolvirk",
+ 487 => "Banshee",
+ 488 => "Banshee",
+ 489 => "Elddar",
+ 490 => "Forest Giant",
+ 491 => "Bone Golem",
+ 492 => "Horse",
+ 493 => "Pegasus",
+ 494 => "Shambling Mound",
+ 495 => "Scrykin",
+ 496 => "Treant",
+ 497 => "Vampire",
+ 498 => "Ayonae Ro",
+ 499 => "Sullon Zek",
+ 500 => "Banner",
+ 501 => "Flag",
+ 502 => "Rowboat",
+ 503 => "Bear Trap",
+ 504 => "Clockwork Bomb",
+ 505 => "Dynamite Keg",
+ 506 => "Pressure Plate",
+ 507 => "Puffer Spore",
+ 508 => "Stone Ring",
+ 509 => "Root Tentacle",
+ 510 => "Runic Symbol",
+ 511 => "Saltpetter Bomb",
+ 512 => "Floating Skull",
+ 513 => "Spike Trap",
+ 514 => "Totem",
+ 515 => "Web",
+ 516 => "Wicker Basket",
+ 517 => "Nightmare/Unicorn",
+ 518 => "Horse",
+ 519 => "Nightmare/Unicorn",
+ 520 => "Bixie",
+ 521 => "Centaur",
+ 522 => "Drakkin",
+ 523 => "Giant",
+ 524 => "Gnoll",
+ 525 => "Griffin",
+ 526 => "Giant Shade",
+ 527 => "Harpy",
+ 528 => "Mammoth",
+ 529 => "Satyr",
+ 530 => "Dragon",
+ 531 => "Dragon",
+ 532 => "Dyn\'Leth",
+ 533 => "Boat",
+ 534 => "Weapon Rack",
+ 535 => "Armor Rack",
+ 536 => "Honey Pot",
+ 537 => "Jum Jum Bucket",
+ 538 => "Toolbox",
+ 539 => "Stone Jug",
+ 540 => "Small Plant",
+ 541 => "Medium Plant",
+ 542 => "Tall Plant",
+ 543 => "Wine Cask",
+ 544 => "Elven Boat",
+ 545 => "Gnomish Boat",
+ 546 => "Barrel Barge Ship",
+ 547 => "Goo",
+ 548 => "Goo",
+ 549 => "Goo",
+ 550 => "Merchant Ship",
+ 551 => "Pirate Ship",
+ 552 => "Ghost Ship",
+ 553 => "Banner",
+ 554 => "Banner",
+ 555 => "Banner",
+ 556 => "Banner",
+ 557 => "Banner",
+ 558 => "Aviak",
+ 559 => "Beetle",
+ 560 => "Gorilla",
+ 561 => "Kedge",
+ 562 => "Kerran",
+ 563 => "Shissar",
+ 564 => "Siren",
+ 565 => "Sphinx",
+ 566 => "Human",
+ 567 => "Campfire",
+ 568 => "Brownie",
+ 569 => "Dragon",
+ 570 => "Exoskeleton",
+ 571 => "Ghoul",
+ 572 => "Clockwork Guardian",
+ 573 => "Mantrap",
+ 574 => "Minotaur",
+ 575 => "Scarecrow",
+ 576 => "Shade",
+ 577 => "Rotocopter",
+ 578 => "Tentacle Terror",
+ 579 => "Wereorc",
+ 580 => "Worg",
+ 581 => "Wyvern",
+ 582 => "Chimera",
+ 583 => "Kirin",
+ 584 => "Puma",
+ 585 => "Boulder",
+ 586 => "Banner",
+ 587 => "Elven Ghost",
+ 588 => "Human Ghost",
+ 589 => "Chest",
+ 590 => "Chest",
+ 591 => "Crystal",
+ 592 => "Coffin",
+ 593 => "Guardian CPU",
+ 594 => "Worg",
+ 595 => "Mansion",
+ 596 => "Floating Island",
+ 597 => "Cragslither",
+ 598 => "Wrulon",
+ 599 => "Spell Particle 1",
+ 600 => "Invisible Man of Zomm",
+ 601 => "Robocopter of Zomm",
+ 602 => "Burynai",
+ 603 => "Frog",
+ 604 => "Dracolich",
+ 605 => "Iksar Ghost",
+ 606 => "Iksar Skeleton",
+ 607 => "Mephit",
+ 608 => "Muddite",
+ 609 => "Raptor",
+ 610 => "Sarnak",
+ 611 => "Scorpion",
+ 612 => "Tsetsian",
+ 613 => "Wurm",
+ 614 => "Balrog",
+ 615 => "Hydra Crystal",
+ 616 => "Crystal Sphere",
+ 617 => "Gnoll",
+ 618 => "Sokokar",
+ 619 => "Stone Pylon",
+ 620 => "Demon Vulture",
+ 621 => "Wagon",
+ 622 => "God of Discord",
+ 623 => "Wrulon Mount",
+ 624 => "Ogre NPC - Male",
+ 625 => "Sokokar Mount",
+ 626 => "Giant (Rallosian mats)",
+ 627 => "Sokokar (w saddle)",
+ 628 => "10th Anniversary Banner",
+ 629 => "10th Anniversary Cake",
+ 630 => "Wine Cask",
+ 631 => "Hydra Mount",
+ 632 => "Hydra NPC",
+ 633 => "Wedding Flowers",
+ 634 => "Wedding Arbor",
+ 635 => "Wedding Altar",
+ 636 => "Powder Keg",
+ 637 => "Apexus",
+ 638 => "Bellikos",
+ 639 => "Brell\'s First Creation",
+ 640 => "Brell",
+ 641 => "Crystalskin Ambuloid",
+ 642 => "Cliknar Queen",
+ 643 => "Cliknar Soldier",
+ 644 => "Cliknar Worker",
+ 645 => "Coldain",
+ 646 => "Coldain",
+ 647 => "Crystalskin Sessiloid",
+ 648 => "Genari",
+ 649 => "Gigyn",
+ 650 => "Greken - Young Adult",
+ 651 => "Greken - Young",
+ 652 => "Cliknar Mount",
+ 653 => "Telmira",
+ 654 => "Spider Mount",
+ 655 => "Bear Mount",
+ 656 => "Rat Mount",
+ 657 => "Sessiloid Mount",
+ 658 => "Morell Thule",
+ 659 => "Marionette",
+ 660 => "Book Dervish",
+ 661 => "Topiary Lion",
+ 662 => "Rotdog",
+ 663 => "Amygdalan",
+ 664 => "Sandman",
+ 665 => "Grandfather Clock",
+ 666 => "Gingerbread Man",
+ 667 => "Beefeater",
+ 668 => "Rabbit",
+ 669 => "Blind Dreamer",
+ 670 => "Cazic Thule",
+ 671 => "Topiary Lion Mount",
+ 672 => "Rot Dog Mount",
+ 673 => "Goral Mount",
+ 674 => "Selyran Mount",
+ 675 => "Sclera Mount",
+ 676 => "Braxy Mount",
+ 677 => "Kangon Mount",
+ 678 => "Erudite",
+ 679 => "Wurm Mount",
+ ];
+
+ protected static $playableIds = [40, 41, 59, 61, 67, 68, 70];
+
+ public static function getName($id)
+ {
+ return static::$mapping[$id] ?? 'Unknown';
+ }
+
+ public static function getIDsByName($name)
+ {
+ $name = strtolower($name);
+ $ids = array_keys(array_filter(static::$mapping, function ($val) use ($name) {
+ return strtolower($val) === $name;
+ }));
+ return $ids;
+ }
+
+ public static function getAll()
+ {
+ $races = static::$mapping;
+ asort($races);
+ return $races;
+ }
+
+ public static function getUniqueRaceNames()
+ {
+ $uniqueRaces = [];
+ foreach (self::getAll() as $id => $name) {
+ $lowerName = strtolower($name);
+ if (!array_key_exists($lowerName, $uniqueRaces)) {
+ $uniqueRaces[$lowerName] = $name;
+ }
+ }
+ return $uniqueRaces;
+ }
+}
\ No newline at end of file
diff --git a/src/Definitions/Skills.php b/src/Definitions/Skills.php
new file mode 100644
index 00000000..16131a07
--- /dev/null
+++ b/src/Definitions/Skills.php
@@ -0,0 +1,103 @@
+ 'One Hand Blunt',
+ 1 => 'One Hand Slashing',
+ 2 => 'Two Hand Blunt',
+ 3 => 'Two Hand Slashing',
+ 4 => 'Abjuration',
+ 5 => 'Alteration',
+ 6 => 'Apply Poison',
+ 7 => 'Archery',
+ 8 => 'Backstab',
+ 9 => 'Bind Wound',
+ 10 => 'Bash',
+ 11 => 'Blockskill',
+ 12 => 'Brass Instruments',
+ 13 => 'Channeling',
+ 14 => 'Conjuration',
+ 15 => 'Defense',
+ 16 => 'Disarm',
+ 17 => 'Disarm Traps',
+ 18 => 'Divination',
+ 19 => 'Dodge',
+ 20 => 'Double Attack',
+ 21 => 'Dragon Punch',
+ 22 => 'Duel Wield',
+ 23 => 'Eagle Strike',
+ 24 => 'Evocation',
+ 25 => 'Feign Death',
+ 26 => 'Flying Kick',
+ 27 => 'Forage',
+ 28 => 'Hand To Hand',
+ 29 => 'Hide',
+ 30 => 'Kick',
+ 31 => 'Meditate',
+ 32 => 'Mend',
+ 33 => 'Offense',
+ 34 => 'Parry',
+ 35 => 'Pick Lock',
+ 36 => 'Piercing',
+ 37 => 'Riposte',
+ 38 => 'Round Kick',
+ 39 => 'Safe Fall',
+ 40 => 'Sense Heading',
+ 41 => 'Singing',
+ 42 => 'Sneak',
+ 43 => 'Specialize Abjure',
+ 44 => 'Specialize Alteration',
+ 45 => 'Specialize Conjuration',
+ 46 => 'Specialize Divination',
+ 47 => 'Specialize Evocation',
+ 48 => 'Pick Pockets',
+ 49 => 'Stringed Instruments',
+ 50 => 'Swimming',
+ 51 => 'Throwing',
+ 52 => 'Clicky',
+ 53 => 'Tracking',
+ 54 => 'Wind Instruments',
+ 55 => 'Fishing',
+ 56 => 'Poison Making',
+ 57 => 'Tinkering',
+ 58 => 'Research',
+ 59 => 'Alchemy',
+ 60 => 'Baking',
+ 61 => 'Tailoring',
+ 62 => 'Sense Traps',
+ 63 => 'Blacksmithing',
+ 64 => 'Fletching',
+ 65 => 'Brewing',
+ 66 => 'Alcohol Tolerance',
+ 67 => 'Begging',
+ 68 => 'Jewelry Making',
+ 69 => 'Pottery',
+ 70 => 'Percussion Instruments',
+ 71 => 'Intimidation',
+ 72 => 'Berserking',
+ 73 => 'Taunt',
+ ];
+
+
+ protected static $bardSkillIds = [23, 24, 25, 26, 51];
+
+ protected static $itemSkillIds = [0, 1, 2, 3, 4, 45];
+
+ protected static $tradeSkillIds = [55, 65, 75, 56, 69, 57, 58, 59, 60, 61, 63, 64, 68];
+
+ public static function getName($id)
+ {
+ return static::$mapping[$id] ?? 'Unknown';
+ }
+
+ public static function getTradeskills()
+ {
+ return array_intersect_key(static::$mapping, array_flip(static::$tradeSkillIds));
+ }
+
+ public static function getAll()
+ {
+ return static::$mapping;
+ }
+}
\ No newline at end of file
diff --git a/src/Definitions/SpellResists.php b/src/Definitions/SpellResists.php
new file mode 100644
index 00000000..c54b3eb6
--- /dev/null
+++ b/src/Definitions/SpellResists.php
@@ -0,0 +1,27 @@
+ "None",
+ 1 => "Magic",
+ 2 => "Fire",
+ 3 => "Cold",
+ 4 => "Poison",
+ 5 => "Disease",
+ 6 => "Chromatic",
+ 7 => "Prismatic",
+ 8 => "Physical",
+ 9 => "Corruption",
+ ];
+
+ public static function getName($id)
+ {
+ return static::$mapping[$id] ?? 'Unknown';
+ }
+
+ public static function getAll()
+ {
+ return static::$mapping;
+ }
+}
\ No newline at end of file
diff --git a/src/Definitions/SpellTypes.php b/src/Definitions/SpellTypes.php
new file mode 100644
index 00000000..259f5ba5
--- /dev/null
+++ b/src/Definitions/SpellTypes.php
@@ -0,0 +1,26 @@
+ "Nuke",
+ 2 => "Heal",
+ 4 => "Root",
+ 8 => "Buff",
+ 16 => "Escape",
+ 32 => "Pet",
+ 64 => "Lifetap",
+ 128 => "Snare",
+ 256 => "Dot",
+ ];
+
+ public static function getName($id)
+ {
+ return static::$mapping[$id] ?? 'Unknown';
+ }
+
+ public static function getAll()
+ {
+ return static::$mapping;
+ }
+}
\ No newline at end of file
diff --git a/src/Definitions/Tables.php b/src/Definitions/Tables.php
new file mode 100644
index 00000000..97e46903
--- /dev/null
+++ b/src/Definitions/Tables.php
@@ -0,0 +1,51 @@
+read('/config/tables.json'), true);
+ self::$databaseConfig = json_decode($filesystem->read('/config/database.json'), true);
+ }
+ }
+
+ public static function getTableName($identifier)
+ {
+ self::init();
+ return self::$tableMapping[$identifier] ?? null;
+ }
+
+ public static function getDatabaseConfig($tableName)
+ {
+ self::init();
+ if (in_array($tableName, self::$accountTables)) {
+ return self::$databaseConfig['default'];
+ } elseif (in_array($tableName, self::$contentTables)) {
+ return self::$databaseConfig['content'];
+ } else {
+ return null;
+ }
+ }
+ protected static $accountTables = [
+ "accounts",
+ "characters",
+ "sessions"
+ ];
+
+ protected static $contentTables = [
+ "factions",
+ "items",
+ "task",
+ "activities",
+ "npcs",
+ "recipes",
+ "zones",
+ "spells",
+ "tasks"
+ ];
+}
diff --git a/src/Effect/Descriptions/AdjustAttackSpeedEffectDescription.php b/src/Effect/Descriptions/AdjustAttackSpeedEffectDescription.php
new file mode 100644
index 00000000..b3d9bbc7
--- /dev/null
+++ b/src/Effect/Descriptions/AdjustAttackSpeedEffectDescription.php
@@ -0,0 +1 @@
+effect = $effect;
+ }
+
+ public function getDescription(): string
+ {
+ $description = "Alter Attack Speed";
+ if ($this->effect->getMaxValue() < 100) { // Assuming a decrease in speed
+ $description = "Decrease Attack Speed by " . (100 - $this->effect->getMaxValue()) . "%";
+ } else { // Increase in speed
+ $description = "Increase Attack Speed by " . ($this->effect->getMaxValue() - 100) . "%";
+ }
+ return $description;
+ }
+}
\ No newline at end of file
diff --git a/src/Effect/Descriptions/BindAffinityEffectDescription.php b/src/Effect/Descriptions/BindAffinityEffectDescription.php
new file mode 100644
index 00000000..b3d9bbc7
--- /dev/null
+++ b/src/Effect/Descriptions/BindAffinityEffectDescription.php
@@ -0,0 +1 @@
+effect = $effect;
+ $this->metadata = EffectMetadata::getMetadataFor($effect->getType());
+ }
+
+ public function getDescription(): string
+ {
+ $value = $this->effect->getMaxValue();
+ return sprintf($this->metadata['descriptionPattern'], $value);
+ }
+}
\ No newline at end of file
diff --git a/src/Effect/Descriptions/IncreaseAgilityEffectDescription.php b/src/Effect/Descriptions/IncreaseAgilityEffectDescription.php
new file mode 100644
index 00000000..b3d9bbc7
--- /dev/null
+++ b/src/Effect/Descriptions/IncreaseAgilityEffectDescription.php
@@ -0,0 +1 @@
+effect = $effect;
+ }
+
+ public function getDescription(): string
+ {
+ return "Increase Magnification by up to " . $this->effect->getMaxValue() . "%";
+ }
+}
\ No newline at end of file
diff --git a/src/Effect/Descriptions/IncreaseManaEffectDescription.php b/src/Effect/Descriptions/IncreaseManaEffectDescription.php
new file mode 100644
index 00000000..b3d9bbc7
--- /dev/null
+++ b/src/Effect/Descriptions/IncreaseManaEffectDescription.php
@@ -0,0 +1 @@
+effect = $effect;
+ $this->metadata = EffectMetadata::getMetadataFor($effect->getType());
+ }
+
+ public function getDescription(): string
+ {
+ $value = $this->effect->getMaxValue();
+ return sprintf($this->metadata['descriptionPattern'], $value);
+ }
+}
\ No newline at end of file
diff --git a/src/Effect/Descriptions/PacifyEffectDescriptiony.php b/src/Effect/Descriptions/PacifyEffectDescriptiony.php
new file mode 100644
index 00000000..b3d9bbc7
--- /dev/null
+++ b/src/Effect/Descriptions/PacifyEffectDescriptiony.php
@@ -0,0 +1 @@
+effect = $effect;
+ $this->itemRepository = $itemRepository;
+ }
+
+ public function getDescription(): string
+ {
+ $itemId = $this->effect->getBaseValue();
+ $itemName = $this->itemRepository->getNameById($itemId);
+ return "Summon Item: " . htmlspecialchars($itemName);
+ }
+}
\ No newline at end of file
diff --git a/src/Effect/Descriptions/WaterBreathingEffectDescription.php b/src/Effect/Descriptions/WaterBreathingEffectDescription.php
new file mode 100644
index 00000000..b3d9bbc7
--- /dev/null
+++ b/src/Effect/Descriptions/WaterBreathingEffectDescription.php
@@ -0,0 +1 @@
+id = $id;
+ $this->baseValue = $baseValue;
+ $this->limitValue = $limitValue;
+ $this->maxValue = $maxValue;
+ }
+
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ // Getter for the base value of the effect
+ public function getBaseValue()
+ {
+ return $this->baseValue;
+ }
+
+ // Getter for the limit value of the effect
+ public function getLimitValue()
+ {
+ return $this->limitValue;
+ }
+
+ // Getter for the maximum value of the effect
+ public function getMaxValue()
+ {
+ return $this->maxValue;
+ }
+}
\ No newline at end of file
diff --git a/src/Effect/EffectDescriptionFactory.php b/src/Effect/EffectDescriptionFactory.php
new file mode 100644
index 00000000..9aee4b1d
--- /dev/null
+++ b/src/Effect/EffectDescriptionFactory.php
@@ -0,0 +1,95 @@
+getType(); // Assuming getType() now returns the effect's ID
+
+ switch ($effectId) {
+ case EffectType::INCREASE_MOVEMENT:
+ return new AttackSpeedEffectDescription($effect);
+ case EffectType::SUMMON:
+ return new SummonEffectDescription($effect);
+ case EffectType::ALTER_ATTACK_SPEED:
+ return;
+ case EffectType::STUN:
+ $itemRepository = new ItemRepository(); // Assuming you have this implemented
+ return new SummonEffectDescription($effect, $itemRepository);
+ case EffectType::INCREASE_MAGNIFICATION:
+ return new IncreaseMagnificationEffectDescription($effect);
+ case EffectType::INCREASE_HASTE_V2:
+ return;
+ case EffectType::INCREASE_AGRO_MULTIPLIER:
+ return;
+ case EffectType::INCREASE_HASTE_V3:
+ return;
+ case EffectType::INCREASE_SPELL_DAMAGE:
+ return;
+ case EffectType::INCREASE_HEALING:
+ return;
+ case EffectType::INCREASE_SPELL_HASTE:
+ return;
+ case EffectType::INCREASE_SPELL_DURATION:
+ return;
+ case EffectType::INCREASE_SPELL_RANGE:
+ return;
+ case EffectType::DECREASE_SPELL_BASH_HATE:
+ return;
+ case EffectType::DECREASE_CHANCE_OF_USING_REAGENT:
+ return;
+ case EffectType::DECREASE_SPELL_MANA_COST:
+ return;
+ case EffectType::INCREASE_CHANCE_TO_REFLECT_SPELL:
+ return;
+ case EffectType::INCREASE_MELEE_MITIGATION:
+ return;
+ case EffectType::INCREASE_CRIT_HIT_CHANCE:
+ return;
+ case EffectType::INCREASE_CHANCE_TO_AVOID_MELEE:
+ return;
+ case EffectType::INCREASE_CHANCE_TO_RIPOSTE:
+ return;
+ case EffectType::INCREASE_CHANCE_TO_DODGE:
+ return;
+ case EffectType::INCREASE_CHANCE_TO_PARRY:
+ return;
+ case EffectType::INCREASE_CHANCE_TO_DUAL_WIELD:
+ return;
+ case EffectType::INCREASE_CHANCE_TO_DOUBLE_ATTACK:
+ return;
+ case EffectType::INCREASE_CHANCE_TO_RESIST_SPELL:
+ return;
+ case EffectType::INCREASE_CHANCE_TO_RESIST_FEAR_SPELL:
+ return;
+ case EffectType::INCREASE_ALL_SKILLS_SKILL_CHECK:
+ return;
+ case EffectType::INCREASE_CHANCE_TO_HIT_WITH_ALL_SKILLS:
+ return;
+ case EffectType::INCREASE_ALL_SKILLS_DAMAGE_MODIFIER:
+ return;
+ case EffectType::INCREASE_ALL_SKILLS_MINIMUM_DAMAGE_MODIFIER:
+ return;
+ case EffectType::INCREASE_CHANCE_TO_BLOCK:
+ return;
+ case EffectType::INCREASE_PROC_MODIFIER:
+ return;
+ case EffectType::INCREASE_RANGE_PROC_MODIFIER:
+ return;
+ case EffectType::INCREASE_ACCURACY:
+ return;
+ case EffectType::REDUCE_SKILL_TIMER:
+ return;
+ case EffectType::ADD_ATTACK_CHANCE:
+ return;
+ case EffectType::INCREASE_CRITICAL_DOT_CHANCE:
+ return;
+ case EffectType::INCREASE_CRITICAL_SPELL_CHANCE:
+ return;
+ case EffectType::INCREASE_CRIT_DAMAGE_MOB:
+ return;
+ default:
+ throw new Exception("Unsupported effect ID: " . $effectId);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Effect/EffectDescriptionInterface.php b/src/Effect/EffectDescriptionInterface.php
new file mode 100644
index 00000000..b7a61ed5
--- /dev/null
+++ b/src/Effect/EffectDescriptionInterface.php
@@ -0,0 +1,6 @@
+ ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Movement (% / 0)
+ EffectType::ALTER_ATTACK_SPEED => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Decrease OR Increase AttackSpeed
+ EffectType::STUN => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // stun
+ EffectType::SUMMON_ITEM => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // summonitem
+ EffectType::INCREASE_MAGNIFICATION => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Magnification
+ EffectType::INCREASE_HASTE_V2 => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Haste v2
+ EffectType::INCREASE_AGRO_MULTIPLIER => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Agro Multiplier
+ EffectType::INCREASE_HASTE_V3 => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Haste v3
+ EffectType::INCREASE_SPELL_DAMAGE => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Spell Damage
+ EffectType::INCREASE_HEALING => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Spell Healing
+ EffectType::INCREASE_SPELL_HASTE => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Spell Haste
+ EffectType::INCREASE_SPELL_DURATION => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Spell Duration
+ EffectType::INCREASE_SPELL_RANGE => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Spell Range
+ EffectType::DECREASE_SPELL_BASH_HATE => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Decrease Spell/Bash Hate
+ EffectType::DECREASE_CHANCE_OF_USING_REAGENT => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Decrease Chance of Using Reagent
+ EffectType::DECREASE_SPELL_MANA_COST => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Decrease Spell Mana Cost
+ EffectType::INCREASE_CHANCE_TO_REFLECT_SPELL => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Chance to Reflect Spell
+ EffectType::INCREASE_MELEE_MITIGATION => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Melee Mitigation
+ EffectType::INCREASE_CRIT_HIT_CHANCE => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Chance to Critical Hit
+ EffectType::INCREASE_CHANCE_TO_AVOID_MELEE => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Chance to Avoid Melee
+ EffectType::INCREASE_CHANCE_TO_RIPOSTE => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Chance to Riposte
+ EffectType::INCREASE_CHANCE_TO_DODGE => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Chance to Dodge
+ EffectType::INCREASE_CHANCE_TO_PARRY => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Chance to Parry
+ EffectType::INCREASE_CHANCE_TO_DUAL_WIELD => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Chance to Dual Wield
+ EffectType::INCREASE_CHANCE_TO_DOUBLE_ATTACK => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Chance to Double Attack
+ EffectType::INCREASE_CHANCE_TO_RESIST_SPELL => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Chance to Resist Spell
+ EffectType::INCREASE_CHANCE_TO_RESIST_FEAR_SPELL => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Chance to Resist Fear Spell
+ EffectType::INCREASE_ALL_SKILLS_SKILL_CHECK => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase All Skills Skill Check
+ EffectType::INCREASE_CHANCE_TO_HIT_WITH_ALL_SKILLS => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Chance to Hit With all Skills
+ EffectType::INCREASE_ALL_SKILLS_DAMAGE_MODIFIER => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase All Skills Damage Modifier
+ EffectType::INCREASE_ALL_SKILLS_MINIMUM_DAMAGE_MODIFIER => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase All Skills Minimum Damage Modifier
+ EffectType::INCREASE_CHANCE_TO_BLOCK => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Chance to Block
+ EffectType::INCREASE_PROC_MODIFIER => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Proc Modifier
+ EffectType::INCREASE_RANGE_PROC_MODIFIER => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Range Proc Modifier
+ EffectType::INCREASE_ACCURACY => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Accuracy
+ EffectType::REDUCE_SKILL_TIMER => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Reduce Skill Timer
+ EffectType::ADD_ATTACK_CHANCE => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Add Attack Chance
+ EffectType::INCREASE_CRITICAL_DOT_CHANCE => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Critical Dot Chance
+ EffectType::INCREASE_CRITICAL_SPELL_CHANCE => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true], // Increase Critical Spell Chance
+ EffectType::INCREASE_CRIT_DAMAGE_MOB => ['name' => '', 'descriptionPattern' => '', 'isBeneficial' => true],
+ ];
+
+ public static function getMetadataFor($effectId)
+ {
+ return self::$metadata[$effectId] ?? null;
+ }
+}
\ No newline at end of file
diff --git a/src/Effect/Metadata/EffectType.php b/src/Effect/Metadata/EffectType.php
new file mode 100644
index 00000000..3ee636d9
--- /dev/null
+++ b/src/Effect/Metadata/EffectType.php
@@ -0,0 +1,45 @@
+ 'i',
+ 'name' => 's',
+ 'value' => 'i',
+ ];
+
+ private $id;
+ private $name;
+ private $value;
+
+ public function __construct($data = [])
+ {
+ foreach ($data as $key => $value) {
+ if (property_exists($this, $key)) {
+ $this->$key = $value;
+ }
+ }
+ }
+
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ public static function getValidParams()
+ {
+ return self::VALID_PARAMS;
+ }
+}
\ No newline at end of file
diff --git a/src/Entities/Item.php b/src/Entities/Item.php
new file mode 100644
index 00000000..b73b4893
--- /dev/null
+++ b/src/Entities/Item.php
@@ -0,0 +1,56 @@
+ 'i',
+ 'name' => 's',
+ 'icon' => 's',
+ ];
+
+ private $id;
+ private $name;
+ private $icon;
+ private $itemType;
+ private $description;
+
+ public function __construct($data = [])
+ {
+ foreach ($data as $key => $value) {
+ $key = strtolower($key);
+ if (property_exists($this, $key)) {
+ $this->$key = $value;
+ }
+ }
+ }
+
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getItemType()
+ {
+ return $this->itemType;
+ }
+
+ public function getIcon()
+ {
+ return $this->icon;
+ }
+
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ public static function getValidParams()
+ {
+ return self::VALID_PARAMS;
+ }
+}
\ No newline at end of file
diff --git a/src/Entities/LootDrop.php b/src/Entities/LootDrop.php
new file mode 100644
index 00000000..33068146
--- /dev/null
+++ b/src/Entities/LootDrop.php
@@ -0,0 +1,41 @@
+ $value) {
+ $key = strtolower($key);
+ if (property_exists($this, $key)) {
+ $this->$key = $value;
+ }
+ }
+ $this->item = new Item($data);
+ }
+
+ // Getters
+ public function getItem()
+ {
+ return $this->item;
+ }
+
+ public function getChance()
+ {
+ return $this->chance;
+ }
+
+ public function getProbability()
+ {
+ return $this->probability;
+ }
+
+ public function getMultiplier()
+ {
+ return $this->multiplier;
+ }
+}
\ No newline at end of file
diff --git a/src/Entities/Npc.php b/src/Entities/Npc.php
new file mode 100644
index 00000000..e4e8d147
--- /dev/null
+++ b/src/Entities/Npc.php
@@ -0,0 +1,166 @@
+ 'i',
+ 'name' => 's',
+ 'lastname' => 's',
+ 'level' => 'i',
+ 'race' => 'i',
+ 'class' => 'i',
+ ];
+
+ private $id;
+ private $name;
+ private $lastname;
+ private $level;
+ private $race;
+ private $class;
+ private $attack_speed;
+ private $hp;
+ private $mindmg;
+ private $maxdmg;
+ private $npcspecialattks;
+ private $npc_faction_id;
+ private $texture;
+ private $helmtexture;
+ private $gender;
+ private $spawnGroups = [];
+ private $factions = [];
+ private $primaryFaction;
+ private $lootDrops = [];
+
+ public function __construct($data = [])
+ {
+ foreach ($data as $key => $value) {
+ if (property_exists($this, $key)) {
+ $this->$key = $value;
+ }
+ }
+ }
+
+ public function setSpawnGroups($groups)
+ {
+ $this->spawnGroups = $groups;
+ }
+
+ public function setFactions($factions)
+ {
+ $this->factions = $factions;
+ }
+
+ public function setPrimaryFaction($primaryFaction)
+ {
+ $this->primaryFaction = $primaryFaction;
+ }
+
+ public function setLootDrops($items)
+ {
+ $this->lootDrops = $items;
+ }
+
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getLastname()
+ {
+ return $this->lastname;
+ }
+
+ public function getLevel()
+ {
+ return $this->level;
+ }
+
+ public function getRace()
+ {
+ return $this->race;
+ }
+
+ public function getClass()
+ {
+ return $this->class;
+ }
+
+ public function getAttackSpeed()
+ {
+ return $this->attack_speed;
+ }
+
+ public function getHp()
+ {
+ return $this->hp;
+ }
+
+ public function getMinDmg()
+ {
+ return $this->mindmg;
+ }
+
+ public function getMaxDmg()
+ {
+ return $this->maxdmg;
+ }
+
+ public function getSpecialAttacks()
+ {
+ return $this->npcspecialattks;
+ }
+
+
+ public function getNpcFactionId()
+ {
+ return $this->npc_faction_id;
+ }
+
+
+ public function getTexture()
+ {
+ return $this->texture;
+ }
+
+
+ public function getHelmtexture()
+ {
+ return $this->helmtexture;
+ }
+
+
+ public function getGender()
+ {
+ return $this->gender;
+ }
+
+ public function getFactions()
+ {
+ return $this->factions;
+ }
+
+ public function getPrimaryFaction()
+ {
+ return $this->primaryFaction;
+ }
+
+ public function getSpawnGroups()
+ {
+ return $this->spawnGroups;
+ }
+
+ public function getLootDrops()
+ {
+ return $this->lootDrops;
+ }
+
+ public static function getValidParams()
+ {
+ return self::VALID_PARAMS;
+ }
+}
\ No newline at end of file
diff --git a/src/Entities/Recipe.php b/src/Entities/Recipe.php
new file mode 100644
index 00000000..2cdd1aa9
--- /dev/null
+++ b/src/Entities/Recipe.php
@@ -0,0 +1,44 @@
+ 'i',
+ 'name' => 's',
+ 'tradeskill' => 'i',
+ ];
+
+ private $id;
+ private $name;
+ private $tradeskill;
+
+ public function __construct($data = [])
+ {
+ foreach ($data as $key => $value) {
+ $key = strtolower($key);
+ if (property_exists($this, $key)) {
+ $this->$key = $value;
+ }
+ }
+ }
+
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getTradeskill()
+ {
+ return $this->tradeskill;
+ }
+
+ public static function getValidParams()
+ {
+ return self::VALID_PARAMS;
+ }
+}
\ No newline at end of file
diff --git a/src/Entities/RuleSet.php b/src/Entities/RuleSet.php
new file mode 100644
index 00000000..e5041ca9
--- /dev/null
+++ b/src/Entities/RuleSet.php
@@ -0,0 +1,14 @@
+config = $config;
+ $this->fileSystem = $fileSystem;
+ }
+
+ public function getImageUri(Item $item)
+ {
+ $rootUri = $this->config->root_url;
+ $baseUri = $rootUri . 'images/item_icons/';
+ $imageId = $item->getIcon();
+ $defaultUri = $baseUri . $imageId . '.png';
+
+ return $defaultUri;
+ }
+}
diff --git a/src/Entities/Services/NpcImageService.php b/src/Entities/Services/NpcImageService.php
new file mode 100644
index 00000000..fc65c5ac
--- /dev/null
+++ b/src/Entities/Services/NpcImageService.php
@@ -0,0 +1,26 @@
+config = $config;
+ $this->fileSystem = $fileSystem;
+ }
+
+ public function getImageUri(Npc $npc)
+ {
+ $rootUri = $this->config->root_url;
+ $baseUri = $rootUri . 'images/npc_models/';
+ $raceId = $npc->getRace();
+ $genderId = $npc->getGender();
+ $textureId = $npc->getTexture();
+ $helmTextureId = $npc->getHelmTexture();
+ $defaultUri = $baseUri . 'CTN_' . $raceId . '_' . $genderId . '_' . $textureId . '_' . $helmTextureId . '.png';
+
+ return $defaultUri;
+ }
+}
diff --git a/src/Entities/Services/SpellImageService.php b/src/Entities/Services/SpellImageService.php
new file mode 100644
index 00000000..f6ab0246
--- /dev/null
+++ b/src/Entities/Services/SpellImageService.php
@@ -0,0 +1,35 @@
+config = $config;
+ $this->fileSystem = $fileSystem;
+ $this->setup();
+ }
+
+ public function getImageUri(Spell $spell)
+ {
+ $imageId = $spell->getNewIcon();
+ return $this->baseUri . $imageId . $this->imageExtension;
+ }
+
+ private function setup() {
+ $assetUri = $this->config->get('site.asset_url');
+ $imagePath = $this->config->get('site.spell_image_path');
+ $useLegacy = $this->config->get('display.legacy_spell_icons');
+
+ if ($useLegacy) {
+ $imagePath .= 'legacy/';
+ }
+
+ $this->imageExtension = $this->config->get('site.spell_image_extension');
+ $this->baseUri = $assetUri . $imagePath;
+ }
+}
\ No newline at end of file
diff --git a/src/Entities/Services/ZoneImageService.php b/src/Entities/Services/ZoneImageService.php
new file mode 100644
index 00000000..d4ed691c
--- /dev/null
+++ b/src/Entities/Services/ZoneImageService.php
@@ -0,0 +1,35 @@
+config = $config;
+ $this->fileSystem = $fileSystem;
+ }
+
+ public function getImageUris(Zone $zone)
+ {
+ $rootUri = $this->config->root_url;
+ $baseUri = $rootUri . 'images/maps/';
+
+ $imagesUris = [];
+ $i = 1;
+
+ while (true) {
+ $imageName = $zone->getShortName() . $i . '.png';
+ $imagePath = $baseUri . $imageName;
+ if ($this->fileSystem->exists('/public/' . $imagePath)) {
+ $imagesUris[] = $imagePath;
+ $i++;
+ } else {
+ break;
+ }
+ }
+
+ return $imagesUris;
+ }
+}
diff --git a/src/Entities/SpawnGroup.php b/src/Entities/SpawnGroup.php
new file mode 100644
index 00000000..677ccfaf
--- /dev/null
+++ b/src/Entities/SpawnGroup.php
@@ -0,0 +1,42 @@
+ 's',
+ 'id' => 'i',
+ ];
+
+ private $name;
+ private $id;
+ private $spawnPoints = [];
+
+ public function __construct($data = [])
+ {
+ foreach ($data as $key => $value) {
+ if (property_exists($this, $key)) {
+ $this->$key = $value;
+ }
+ }
+ }
+
+ public function addSpawnPoint(SpawnPoint $spawnPoint)
+ {
+ $this->spawnPoints[] = $spawnPoint;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ public function getSpawnPoints()
+ {
+ return $this->spawnPoints;
+ }
+}
\ No newline at end of file
diff --git a/src/Entities/SpawnPoint.php b/src/Entities/SpawnPoint.php
new file mode 100644
index 00000000..02e25f9b
--- /dev/null
+++ b/src/Entities/SpawnPoint.php
@@ -0,0 +1,56 @@
+ 'i',
+ 'y' => 'i',
+ 'z' => 'i',
+ 'respawntime' => 'i',
+ 'zone' => 's',
+ ];
+
+ private $x;
+ private $y;
+ private $z;
+ private $respawntime;
+ private $zone;
+
+ public function __construct($data = [], $zoneData = null)
+ {
+ foreach ($data as $key => $value) {
+ if (property_exists($this, $key)) {
+ $this->$key = $value;
+ }
+ }
+
+ if ($zoneData) {
+ $this->zone = new Zone($zoneData);
+ }
+ }
+
+ public function getX()
+ {
+ return $this->x;
+ }
+
+ public function getY()
+ {
+ return $this->y;
+ }
+
+ public function getZ()
+ {
+ return $this->z;
+ }
+
+ public function getZone()
+ {
+ return $this->zone;
+ }
+
+ public function getRespawnTime()
+ {
+ return $this->respawntime;
+ }
+}
\ No newline at end of file
diff --git a/src/Entities/Spell.php b/src/Entities/Spell.php
new file mode 100644
index 00000000..4d97fa6e
--- /dev/null
+++ b/src/Entities/Spell.php
@@ -0,0 +1,180 @@
+ 'i',
+ 'name' => 's',
+ 'icon' => 'i',
+ 'memicon' => 'i',
+ 'new_icon' => 'i',
+ ];
+
+ private $id;
+ private $name;
+ private $icon;
+ private $memicon;
+ private $new_icon;
+ private $teleport_zone;
+ private $you_cast;
+ private $other_cast;
+ private $cast_on_you;
+ private $cast_on_other;
+ private $spell_fades;
+ private $range;
+ private $aoerange;
+ private $pushback;
+ private $pushup;
+ private $cast_time;
+ private $recoveryTime;
+ private $recastTime;
+ private $buffDurationFormula;
+ private $mana;
+
+ private $effects = [];
+
+ public function __construct($data = [])
+ {
+ foreach ($data as $key => $value) {
+ if (property_exists($this, $key)) {
+ $this->$key = $value;
+ }
+ }
+ }
+
+ public function addEffect(effect $effect)
+ {
+ $this->effects[] = $effect;
+ }
+
+ public function getEffects()
+ {
+ return $this->effects;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ public function getIcon()
+ {
+ return $this->icon;
+ }
+
+ public function getMemicon()
+ {
+ return $this->memicon;
+ }
+
+ public function getNewIcon()
+ {
+ return $this->new_icon;
+ }
+
+ public function getIconUri()
+ {
+ return $this->new_icon;
+ }
+
+public function getTeleportZone()
+ {
+ return $this->teleport_zone;
+ }
+
+ public function getYouCast()
+ {
+ return $this->you_cast;
+ }
+
+public function getOtherCast()
+ {
+ return $this->other_cast;
+ }
+
+ public function getCastOnYou()
+ {
+ return $this->cast_on_you;
+ }
+
+public function getCastOnOther()
+ {
+ return $this->cast_on_other;
+ }
+
+ public function getSpellFades()
+ {
+ return $this->spell_fades;
+ }
+
+public function getRange()
+ {
+ return $this->range;
+ }
+
+ public function getAoeRange()
+ {
+ return $this->aoerange;
+ }
+
+public function getPushBack()
+ {
+ return $this->pushback;
+ }
+
+ public function getPushUp()
+ {
+ return $this->pushup;
+ }
+
+public function getCastTime()
+ {
+ return $this->cast_time;
+ }
+
+ public function getRecoveryTime()
+ {
+ return $this->recoveryTime;
+ }
+
+public function getRecastTime()
+ {
+ return $this->recastTime;
+ }
+
+ public function getBuffDurationFormula()
+ {
+ return $this->buffDurationFormula;
+ }
+
+public function getMana()
+ {
+ return $this->mana;
+ }
+
+ public function setEffectsFromRow($row)
+ {
+ for ($i = 1; $i <= 12; $i++) {
+ if (isset($row["effectid$i"]) && $row["effectid$i"] != 254) { // Assuming 254 means no effect
+ $this->addEffect(
+ new effect(
+ $row["effectid$i"],
+ $row["effect_base_value$i"],
+ $row["effect_limit_value$i"],
+ $row["max$i"]
+ )
+ );
+ }
+ }
+ }
+
+ public static function getValidParams()
+ {
+ return self::VALID_PARAMS;
+ }
+}
\ No newline at end of file
diff --git a/src/Entities/Task.php b/src/Entities/Task.php
new file mode 100644
index 00000000..7d0379b1
--- /dev/null
+++ b/src/Entities/Task.php
@@ -0,0 +1,36 @@
+ 'i',
+ 'title' => 's',
+ ];
+
+ private $id;
+ private $title;
+
+ public function __construct($data = [])
+ {
+ foreach ($data as $key => $value) {
+ if (property_exists($this, $key)) {
+ $this->$key = $value;
+ }
+ }
+ }
+
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ public static function getValidParams()
+ {
+ return self::VALID_PARAMS;
+ }
+}
\ No newline at end of file
diff --git a/src/Entities/Zone.php b/src/Entities/Zone.php
new file mode 100644
index 00000000..10d04498
--- /dev/null
+++ b/src/Entities/Zone.php
@@ -0,0 +1,103 @@
+ 'i',
+ 'short_name' => 's',
+ 'long_name' => 's',
+ 'expansion' => 'i',
+ ];
+
+ private $zoneidnumber;
+ private $short_name;
+ private $long_name;
+ private $expansion;
+ private $npcs;
+ private $items;
+ private $forageable;
+ private $connectedZones;
+ private $quests;
+ public function __construct($data = [])
+ {
+ foreach ($data as $key => $value) {
+ if (property_exists($this, $key)) {
+ $this->$key = $value;
+ }
+ }
+ }
+
+ public function setNpcs($npcs){
+ $this->npcs = $npcs;
+ }
+
+ public function setItems($items)
+ {
+ $this->items = $items;
+ }
+
+ public function setForageable($forageable)
+ {
+ $this->forageable = $forageable;
+ }
+
+ public function setConnectedZones($connectedZones)
+ {
+ $this->connectedZones = $connectedZones;
+ }
+
+ public function setQuests($quests)
+ {
+ $this->quests = $quests;
+ }
+
+ public function getZoneidnumber()
+ {
+ return $this->zoneidnumber;
+ }
+
+ public function getShortName()
+ {
+ return $this->short_name;
+ }
+
+ public function getLongName()
+ {
+ return $this->long_name;
+ }
+
+ public function getExpansion()
+ {
+ return $this->expansion;
+ }
+
+ public static function getValidParams()
+ {
+ return self::VALID_PARAMS;
+ }
+
+ public function getNpcs()
+ {
+ return $this->npcs;
+ }
+
+ public function getItems()
+ {
+ return $this->items;
+ }
+
+ public function getConnectedZones()
+ {
+ return $this->connectedZones;
+ }
+
+ public function getQuests()
+ {
+ return $this->quests;
+ }
+
+ public function getForageable()
+ {
+ return $this->forageable;
+ }
+}
\ No newline at end of file
diff --git a/src/Models/BaseModel.php b/src/Models/BaseModel.php
new file mode 100644
index 00000000..3de57a3b
--- /dev/null
+++ b/src/Models/BaseModel.php
@@ -0,0 +1,42 @@
+db = $db;
+ if ($tableIdentifier !== null) {
+ $this->tableName = $this->initTableNames()[$tableIdentifier.'_table'];
+ }
+ $this->tables = $this->initTableNames();
+ }
+
+ protected function initTableNames()
+ {
+ $tables = new Tables;
+ return [
+ 'factions_table' => $tables::getTableName('factions'),
+ 'forage_table' => $tables::getTableName('forage'),
+ 'items_table' => $tables::getTableName('items'),
+ 'loot_drop_table' => $tables::getTableName('loot_drops'),
+ 'loot_drop_entries_table' => $tables::getTableName('loot_drop_entries'),
+ 'loot_table_table' => $tables::getTableName('loot_tables'),
+ 'loot_table_entries_table' => $tables::getTableName('loot_table_entries'),
+ 'npcs_table' => $tables::getTableName('npcs'),
+ 'recipes_table' => $tables::getTableName('recipes'),
+ 'recipe_entries_table' => $tables::getTableName('recipe_entries'),
+ 'spawns_table' => $tables::getTableName('spawns'),
+ 'spawn_groups_table' => $tables::getTableName('spawn_groups'),
+ 'spawn_entries_table' => $tables::getTableName('spawn_entries'),
+ 'spells_table' => $tables::getTableName('spells'),
+ 'tasks_table' => $tables::getTableName('tasks'),
+ 'task_activities_table' => $tables::getTableName('task_activities'),
+ 'zone_connections_table' => $tables::getTableName('zone_connections'),
+ 'zones_table' => $tables::getTableName('zones'),
+ ];
+ }
+}
\ No newline at end of file
diff --git a/src/Models/FactionModel.php b/src/Models/FactionModel.php
new file mode 100644
index 00000000..b9eebaee
--- /dev/null
+++ b/src/Models/FactionModel.php
@@ -0,0 +1,136 @@
+tableName}
+ WHERE
+ id = ?";
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("i", $factionId);
+ $stmt->execute();
+
+ $result = $stmt->get_result();
+
+ if ($row = $result->fetch_assoc())
+ {
+ $faction = new Faction($row);
+ return $faction;
+ }
+ return null;
+ }
+
+
+
+ public function getPaginated($params, $offset, $limit)
+ {
+ $validParams = Faction::getValidParams();
+ $paramTypes = '';
+ $paramValues = [];
+
+ $whereClauses = [];
+
+ foreach ($params as $key => $value) {
+ if (isset($validParams[$key])) {
+ if ($key === 'name') {
+ $whereClauses[] = "$key LIKE CONCAT('%', ?, '%')";
+ } else {
+ $whereClauses[] = "$key = ?";
+ }
+ $paramTypes .= $validParams[$key];
+ $paramValues[] = $value;
+ }
+ }
+
+ $whereString = count($whereClauses) > 0 ? 'WHERE ' . implode(' AND ', $whereClauses) : '';
+
+ $query = "
+ SELECT
+ *
+ FROM
+ {$this->tableName}
+ {$whereString}
+ LIMIT ?, ?";
+ $stmt = $this->db->prepare($query);
+
+ $paramTypes .= "ii";
+
+ array_push($paramValues, $offset, $limit);
+
+ $paramReferences = [];
+ foreach ($paramValues as $i => $value) {
+ $paramReferences[$i] = &$paramValues[$i];
+ }
+
+ if (!empty($paramValues)) {
+ $stmt->bind_param($paramTypes, ...$paramReferences);
+ }
+
+ $stmt->execute();
+ $result = $stmt->get_result();
+
+ $factions = [];
+ while ($row = $result->fetch_assoc()) {
+ $factions[] = new Faction($row);
+ }
+ return $factions;
+ }
+ public function getItemCount($params)
+ {
+ $validParams = Faction::getValidParams();
+ $paramTypes = '';
+ $paramValues = [];
+
+ $whereClauses = [];
+
+ foreach ($params as $key => $value) {
+ if (isset($validParams[$key])) {
+ if ($key === 'name') {
+ $whereClauses[] = "$key LIKE CONCAT('%', ?, '%')";
+ } else {
+ $whereClauses[] = "$key = ?";
+ }
+ $paramTypes .= $validParams[$key];
+ $paramValues[] = $value;
+ }
+ }
+
+ $whereString = count($whereClauses) > 0 ? 'WHERE ' . implode(' AND ', $whereClauses) : '';
+
+ $query = "
+ SELECT
+ COUNT(*) as count
+ FROM
+ {$this->tableName}
+ $whereString";
+ $stmt = $this->db->prepare($query);
+
+ $paramReferences = [];
+ foreach ($paramValues as $i => $value) {
+ $paramReferences[$i] = &$paramValues[$i];
+ }
+
+ if (!empty($paramValues)) {
+ $stmt->bind_param($paramTypes, ...$paramReferences);
+ }
+
+ $stmt->execute();
+ $result = $stmt->get_result();
+ $row = $result->fetch_assoc();
+ return $row['count'];
+ }
+}
+
+
+
diff --git a/src/Models/ItemModel.php b/src/Models/ItemModel.php
new file mode 100644
index 00000000..9000cc41
--- /dev/null
+++ b/src/Models/ItemModel.php
@@ -0,0 +1,201 @@
+tableName}
+ WHERE
+ id = ?";
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("i", $itemId);
+ $stmt->execute();
+
+ $result = $stmt->get_result();
+
+ if ($row = $result->fetch_assoc()) {
+ $item = new Item($row);
+ //$spell->setEffectsFromRow($row);
+ return $item;
+ }
+ return null;
+ }
+
+ public function getItemsByNPC($npcId)
+ {
+ $items = [];
+ $query = "
+ SELECT
+ {$this->tableName}.*
+ FROM
+ {$this->tableName}
+ JOIN npc_items ON {$this->tableName}.id = npc_items.item_id
+ WHERE
+ npc_items.npc_id = ?";
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("i", $npcId);
+ $stmt->execute();
+ $result = $stmt->get_result();
+ while ($data = $result->fetch_assoc()) {
+ $items[] = new Item($data);
+ }
+ return $items;
+ }
+
+ // Search for items based on search criteria
+ public function search($params)
+ {
+ $validParams = Item::getValidParams();
+ $paramTypes = '';
+ $paramValues = [];
+
+ $whereClauses = [];
+
+ foreach ($params as $key => $value) {
+ if (isset($validParams[$key])) {
+ if ($key === 'name') {
+ $whereClauses[] = "$key LIKE CONCAT('%', ?, '%')";
+ } else {
+ $whereClauses[] = "$key = ?";
+ }
+ $paramTypes .= $validParams[$key];
+ $paramValues[] = $value;
+ }
+ }
+
+ $whereString = count($whereClauses) > 0 ? 'WHERE ' . implode(' AND ', $whereClauses) : '';
+
+ $query = "
+ SELECT
+ *
+ FROM
+ {$this->tableName}
+ {$whereString}";
+ $stmt = $this->db->prepare($query);
+
+ $paramReferences = [];
+ foreach ($paramValues as $i => $value) {
+ $paramReferences[$i] = &$paramValues[$i];
+ }
+
+ if (!empty($paramValues)) {
+ $stmt->bind_param($paramTypes, ...$paramReferences);
+ }
+
+ $stmt->execute();
+ $result = $stmt->get_result();
+
+ $items = [];
+ while ($row = $result->fetch_assoc()) {
+ $items[] = new Item($row);
+ }
+ return $items;
+ }
+
+ public function getPaginated($params, $offset, $limit)
+ {
+ $validParams = Item::getValidParams();
+ $paramTypes = '';
+ $paramValues = [];
+
+ $whereClauses = [];
+
+ foreach ($params as $key => $value) {
+ if (isset($validParams[$key]) && $validParams[$key] != '') {
+ if ($key === 'name') {
+ $whereClauses[] = "$key LIKE CONCAT('%', ?, '%')";
+ } else {
+ $whereClauses[] = "$key = ?";
+ }
+ $paramTypes .= $validParams[$key];
+ $paramValues[] = $value;
+ }
+ }
+
+ $whereString = count($whereClauses) > 0 ? 'WHERE ' . implode(' AND ', $whereClauses) : '';
+
+ $query = "
+ SELECT
+ *
+ FROM
+ {$this->tableName}
+ {$whereString}
+ LIMIT ?, ?";
+ $stmt = $this->db->prepare($query);
+
+ $paramTypes .= "ii";
+
+ array_push($paramValues, $offset, $limit);
+
+ $paramReferences = [];
+ foreach ($paramValues as $i => $value) {
+ $paramReferences[$i] = &$paramValues[$i];
+ }
+
+ if (!empty($paramValues)) {
+ $stmt->bind_param($paramTypes, ...$paramReferences);
+ }
+ $stmt->execute();
+ $result = $stmt->get_result();
+
+ $items = [];
+ while ($row = $result->fetch_assoc()) {
+ $items[] = new Item($row);
+ }
+ return $items;
+ }
+
+ public function getItemCount($params)
+ {
+ $validParams = Item::getValidParams();
+ $paramTypes = '';
+ $paramValues = [];
+
+ $whereClauses = [];
+
+ foreach ($params as $key => $value) {
+ if (isset($validParams[$key])) {
+ if ($key === 'name') {
+ $whereClauses[] = "$key LIKE CONCAT('%', ?, '%')";
+ } else {
+ $whereClauses[] = "$key = ?";
+ }
+ $paramTypes .= $validParams[$key];
+ $paramValues[] = $value;
+ }
+ }
+
+ $whereString = count($whereClauses) > 0 ? 'WHERE ' . implode(' AND ', $whereClauses) : '';
+
+ $query = "
+ SELECT
+ COUNT(*) as count
+ FROM
+ {$this->tableName}
+ {$whereString}";
+ $stmt = $this->db->prepare($query);
+
+ $paramReferences = [];
+ foreach ($paramValues as $i => $value) {
+ $paramReferences[$i] = &$paramValues[$i];
+ }
+
+ if (!empty($paramValues)) {
+ $stmt->bind_param($paramTypes, ...$paramReferences);
+ }
+
+ $stmt->execute();
+ $result = $stmt->get_result();
+ $row = $result->fetch_assoc();
+ return $row['count'];
+ }
+}
\ No newline at end of file
diff --git a/src/Models/NpcModel.php b/src/Models/NpcModel.php
new file mode 100644
index 00000000..f158f643
--- /dev/null
+++ b/src/Models/NpcModel.php
@@ -0,0 +1,298 @@
+tableName}
+ WHERE
+ id = ?";
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("i", $npcId);
+ $stmt->execute();
+
+ $result = $stmt->get_result();
+
+ if ($row = $result->fetch_assoc()) {
+ return new Npc($row);
+ }
+ return null;
+ }
+
+ public function getByName($npcName)
+ {
+ $query = "
+ SELECT
+ *
+ FROM
+ {$this->tableName}
+ WHERE
+ name = ?";
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("s", $npcName);
+ $stmt->execute();
+
+ $result = $stmt->get_result();
+
+ if ($row = $result->fetch_assoc()) {
+ return new Npc($row);
+ }
+ return null;
+ }
+
+ public function getSpawnGroups($npcId)
+ {
+ // Fetch spawn groups and spawn points for an NPC
+ $spawnGroups = [];
+ $query = "
+ SELECT
+ spawngroup.*
+ FROM
+ spawnentry
+ JOIN spawngroup ON spawnentry.spawngroupid = spawngroup.id
+ WHERE
+ spawnentry.npcid = ?";
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("i", $npcId);
+ $stmt->execute();
+ $results = $stmt->get_result();
+
+ while ($group = $results->fetch_assoc()) {
+ $spawnGroup = new SpawnGroup($group);
+ $spawnPoints = $this->getSpawnPoints($group['id']);
+ foreach ($spawnPoints as $spawnPoint) {
+ $spawnGroup->addSpawnPoint($spawnPoint);
+ }
+ $spawnGroups[] = $spawnGroup;
+ }
+ return $spawnGroups;
+ }
+
+ public function getSpawnPoints($spawngroupId)
+ {
+ $query = "
+ SELECT
+ sp.*,
+ z.short_name,
+ z.long_name
+ FROM
+ spawn2 sp
+ JOIN zone z ON sp.zone = z.short_name
+ WHERE
+ sp.spawngroupID = ?";
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("i", $spawngroupId);
+ $stmt->execute();
+ $results = $stmt->get_result();
+
+ $spawnPoints = [];
+ while ($spawnPoint = $results->fetch_assoc()) {
+ $zoneData = ['short_name' => $spawnPoint['short_name'], 'long_name' => $spawnPoint['long_name']];
+ $spawnPoints[] = new SpawnPoint($spawnPoint, $zoneData);
+ }
+ return $spawnPoints;
+ }
+
+ public function getFactions($npcFactionId)
+ {
+ $query = "
+ SELECT
+ faction_list.name,
+ faction_list.id,
+ npc_faction_entries.value
+ FROM
+ npc_faction_entries
+ JOIN faction_list ON npc_faction_entries.faction_id = faction_list.id
+ WHERE
+ npc_faction_entries.npc_faction_id = ?
+ ORDER BY
+ value DESC";
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("i", $npcFactionId);
+ $stmt->execute();
+
+ $results = $stmt->get_result();
+ $factions = [];
+ while ($row = $results->fetch_assoc()) {
+ $factions[] = new Faction($row);
+ }
+ return $factions;
+ }
+
+ public function getPrimaryFaction($npcFactionId)
+ {
+ $query = "
+ SELECT
+ faction_list.name,
+ faction_list.id
+ FROM
+ faction_list
+ JOIN
+ npc_faction ON npc_faction.primaryfaction = faction_list.id
+ WHERE
+ npc_faction.id = ?";
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("i", $npcFactionId);
+ $stmt->execute();
+
+ $results = $stmt->get_result();
+ $row = $results->fetch_assoc();
+ if ($row) {
+ return new Faction($row);
+ } else {
+ return null;
+ }
+ }
+
+ public function getLootDrops($npcId)
+ {
+ $sql = "
+ SELECT
+ it.*,
+ lde.chance,
+ lte.probability,
+ lte.lootdrop_id,
+ lte.multiplier
+ FROM
+ items it
+ JOIN lootdrop_entries lde ON it.id = lde.item_id
+ JOIN loottable_entries lte ON lde.lootdrop_id = lte.lootdrop_id
+ JOIN npc_types nt ON lte.loottable_id = nt.loottable_id
+ WHERE
+ nt.id = ?";
+
+ $stmt = $this->db->prepare($sql);
+ $stmt->bind_param('i', $npcId);
+ $stmt->execute();
+ $result = $stmt->get_result();
+
+ $lootDrops = [];
+ while ($row = $result->fetch_assoc()) {
+ $lootDrops[] = new LootDrop($row);
+ }
+
+ return $lootDrops;
+ }
+
+ public function getPaginated($params, $offset, $limit)
+ {
+ $validParams = Npc::getValidParams();
+ $paramTypes = '';
+ $paramValues = [];
+
+ $whereClauses = [];
+
+ foreach ($params as $key => $value) {
+ if ($value != '' && isset($validParams[$key])) {
+ if ($key === 'race' && !is_numeric($value)) {
+ // Resolve race name to IDs
+ $raceIds = Race::getIDsByName(strtolower($value)); // Assuming getIdsFromName returns all matching IDs for a race name
+ $raceIdPlaceholders = implode(',', array_fill(0, count($raceIds), '?'));
+ $whereClauses[] = "race IN ($raceIdPlaceholders)";
+ $paramTypes .= str_repeat('i', count($raceIds));
+ $paramValues = array_merge($paramValues, $raceIds);
+ } else {
+ $whereClauses[] = "$key = ?";
+ $paramTypes .= $validParams[$key];
+ $paramValues[] = $value;
+ }
+ }
+ }
+
+ if (Config::getInstance()->get('display.hide_invisible_men')) {
+ $whereClauses[] = "race != 127";
+ $whereClauses[] = "race != 240";
+ }
+
+ $whereString = count($whereClauses) > 0 ? 'WHERE ' . implode(' AND ', $whereClauses) : '';
+
+ $query = "
+ SELECT
+ *
+ FROM
+ {$this->tableName}
+ {$whereString}
+ LIMIT ?, ?";
+ $stmt = $this->db->prepare($query);
+
+ $paramTypes .= "ii";
+
+ array_push($paramValues, $offset, $limit);
+
+ $paramReferences = [];
+ foreach ($paramValues as $i => $value) {
+ $paramReferences[$i] = &$paramValues[$i];
+ }
+
+ if (!empty($paramValues)) {
+ $stmt->bind_param($paramTypes, ...$paramReferences);
+ }
+
+ $stmt->execute();
+ $result = $stmt->get_result();
+
+ $npcs = [];
+ while ($row = $result->fetch_assoc()) {
+ $npcs[] = new Npc($row);
+ }
+ return $npcs;
+ }
+
+ public function getItemCount($params)
+ {
+ $validParams = Npc::getValidParams();
+ $paramTypes = '';
+ $paramValues = [];
+
+ $whereClauses = [];
+
+ foreach ($params as $key => $value) {
+ if ($value != '' && isset($validParams[$key])) {
+ if ($key === 'name') {
+ $whereClauses[] = "$key LIKE CONCAT('%', ?, '%')";
+ } else {
+ $whereClauses[] = "$key = ?";
+ }
+ $paramTypes .= $validParams[$key];
+ $paramValues[] = $value;
+ }
+ }
+ if (Config::getInstance()->get('display.hide_invisible_men')) {
+ $whereClauses[] = "race != 127";
+ $whereClauses[] = "race != 240";
+ }
+
+ $whereString = count($whereClauses) > 0 ? 'WHERE ' . implode(' AND ', $whereClauses) : '';
+
+ $query = "
+ SELECT
+ COUNT(*) as count
+ FROM
+ {$this->tableName}
+ {$whereString}";
+ $stmt = $this->db->prepare($query);
+
+ $paramReferences = [];
+ foreach ($paramValues as $i => $value) {
+ $paramReferences[$i] = &$paramValues[$i];
+ }
+
+ if (!empty($paramValues)) {
+ $stmt->bind_param($paramTypes, ...$paramReferences);
+ }
+
+ $stmt->execute();
+ $result = $stmt->get_result();
+ $row = $result->fetch_assoc();
+ return $row['count'];
+ }
+}
\ No newline at end of file
diff --git a/src/Models/RecipeModel.php b/src/Models/RecipeModel.php
new file mode 100644
index 00000000..dbf3322f
--- /dev/null
+++ b/src/Models/RecipeModel.php
@@ -0,0 +1,231 @@
+tableName}
+ WHERE
+ id = ?";
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("i", $id);
+ $stmt->execute();
+
+ $result = $stmt->get_result();
+
+ if ($row = $result->fetch_assoc()) {
+ $tradeskill = new Recipe($row);
+ return $tradeskill;
+ }
+ return null;
+ }
+
+ public function getContainers($recipeId)
+ {
+ $query = "
+ SELECT
+ {$this->tables['recipe_entries_table']}.*,
+ {$this->tables['items_table']}.*,
+ {$this->tables['items_table']}.id AS item_id
+ FROM
+ {$this->tableName}
+ JOIN {$this->tables['recipe_entries_table']} ON {$this->tableName}.id = {$this->tables['recipe_entries_table']}.recipe_id
+ JOIN {$this->tables['items_table']} ON {$this->tables['recipe_entries_table']}.item_id = {$this->tables['items_table']}.id
+ WHERE
+ {$this->tables['recipe_entries_table']}.recipe_id = ?
+ AND
+ {$this->tables['recipe_entries_table']}.iscontainer = 1";
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("i", $recipeId);
+ $stmt->execute();
+
+ $result = $stmt->get_result();
+
+ if ($row = $result->fetch_assoc()) {
+ $recipe = new Recipe($row);
+ return $recipe;
+ }
+ return null;
+ }
+
+ public function getItemsCreatedOnSuccess($recipeId)
+ {
+ $query = "
+ SELECT
+ {$this->tables['recipe_entries_table']}.*,
+ {$this->tables['items_table']}.*,
+ {$this->tables['items_table']}.id AS item_id
+ FROM
+ {$this->tableName}
+ JOIN {$this->tables['recipe_entries_table']} ON {$this->tableName}.id = {$this->tables['recipe_entries_table']}.recipe_id
+ JOIN {$this->tables['items_table']} ON {$this->tables['recipe_entries_table']}.item_id = {$this->tables['items_table']}.id
+ WHERE
+ {$this->tables['recipe_entries_table']}.recipe_id = ?
+ AND
+ {$this->tables['recipe_entries_table']}.successcount > 0";
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("i", $recipeId);
+ $stmt->execute();
+ $results = $stmt->get_result();
+
+ $recipe_items = [];
+ while ($recipe_item = $results->fetch_assoc()) {
+ $recipe_items[] = new Item($recipe_item);
+ }
+ return $recipe_items;
+ }
+
+ public function getItemsCreatedOnFailure($recipeId)
+ {
+ $query = "
+ SELECT
+ {$this->tables['recipe_entries_table']}.*,
+ {$this->tables['items_table']}.*,
+ {$this->tables['items_table']}.id AS item_id
+ FROM
+ {$this->tableName}
+ JOIN {$this->tables['recipe_entries_table']} ON {$this->tableName}.id = {$this->tables['recipe_entries_table']}.recipe_id
+ JOIN {$this->tables['items_table']} ON {$this->tables['recipe_entries_table']}.item_id = {$this->tables['items_table']}.id
+ WHERE
+ {$this->tables['recipe_entries_table']}.recipe_id = ?
+ AND
+ {$this->tables['recipe_entries_table']}.failcount > 0";
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("i", $recipeId);
+ $stmt->execute();
+ $results = $stmt->get_result();
+
+ $recipe_items = [];
+ while ($recipe_item = $results->fetch_assoc()) {
+ $recipe_items[] = new Item($recipe_item);
+ }
+ return $recipe_items;
+ }
+
+ public function getComponents($recipeId)
+ {
+ $query = "
+ SELECT
+ {$this->tables['recipe_entries_table']}.*,
+ {$this->tables['items_table']}.*,
+ {$this->tables['items_table']}.id AS item_id
+ FROM
+ {$this->tableName}
+ JOIN {$this->tables['recipe_entries_table']} ON {$this->tableName}.id = {$this->tables['recipe_entries_table']}.recipe_id
+ JOIN {$this->tables['items_table']} ON {$this->tables['recipe_entries_table']}.item_id = {$this->tables['items_table']}.id
+ WHERE
+ {$this->tables['recipe_entries_table']}.recipe_id = ?
+ AND
+ {$this->tables['recipe_entries_table']}.iscontainer = 0
+ AND
+ {$this->tables['recipe_entries_table']}.componentcount > 0";
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("i", $recipeId);
+ $stmt->execute();
+ $results = $stmt->get_result();
+
+ $recipe_items = [];
+ while ($recipe_item = $results->fetch_assoc()) {
+ $recipe_items[] = new Item($recipe_item);
+ }
+ return $recipe_items;
+ }
+
+ public function getPaginated($params, $offset, $limit)
+ {
+ $validParams = Recipe::getValidParams();
+ $paramTypes = '';
+ $paramValues = [];
+
+ $whereClauses = [];
+
+ foreach ($params as $key => $value) {
+ if (isset($validParams[$key])) {
+ if ($key === 'name') {
+ $whereClauses[] = "$key LIKE CONCAT('%', ?, '%')";
+ } else {
+ $whereClauses[] = "$key = ?";
+ }
+ $paramTypes .= $validParams[$key];
+ $paramValues[] = $value;
+ }
+ }
+
+ $whereString = count($whereClauses) > 0 ? 'WHERE ' . implode(' AND ', $whereClauses) : '';
+
+ $query = "SELECT * FROM {$this->tableName} {$whereString} LIMIT ?, ?";
+ $stmt = $this->db->prepare($query);
+
+ $paramTypes .= "ii";
+
+ array_push($paramValues, $offset, $limit);
+
+ $paramReferences = [];
+ foreach ($paramValues as $i => $value) {
+ $paramReferences[$i] = &$paramValues[$i];
+ }
+
+ if (!empty($paramValues)) {
+ $stmt->bind_param($paramTypes, ...$paramReferences);
+ }
+
+ $stmt->execute();
+ $result = $stmt->get_result();
+
+ $tradeskills = [];
+ while ($row = $result->fetch_assoc()) {
+ $tradeskills[] = new Recipe($row);
+ }
+ return $tradeskills;
+ }
+
+ public function getItemCount($params)
+ {
+ $validParams = Recipe::getValidParams();
+ $paramTypes = '';
+ $paramValues = [];
+
+ $whereClauses = [];
+
+ foreach ($params as $key => $value) {
+ if (isset($validParams[$key])) {
+ if ($key === 'name') {
+ $whereClauses[] = "$key LIKE CONCAT('%', ?, '%')";
+ } else {
+ $whereClauses[] = "$key = ?";
+ }
+ $paramTypes .= $validParams[$key];
+ $paramValues[] = $value;
+ }
+ }
+
+ $whereString = count($whereClauses) > 0 ? 'WHERE ' . implode(' AND ', $whereClauses) : '';
+
+ $sql = "SELECT COUNT(*) as count FROM {$this->tableName} {$whereString}";
+ $stmt = $this->db->prepare($sql);
+
+ $paramReferences = [];
+ foreach ($paramValues as $i => $value) {
+ $paramReferences[$i] = &$paramValues[$i];
+ }
+
+ if (!empty($paramValues)) {
+ $stmt->bind_param($paramTypes, ...$paramReferences);
+ }
+
+ $stmt->execute();
+ $result = $stmt->get_result();
+ $row = $result->fetch_assoc();
+ return $row['count'];
+ }
+}
\ No newline at end of file
diff --git a/src/Models/SessionModel.php b/src/Models/SessionModel.php
new file mode 100644
index 00000000..cb7d9b2f
--- /dev/null
+++ b/src/Models/SessionModel.php
@@ -0,0 +1,31 @@
+session = $session;
+ }
+
+ public function get($key)
+ {
+ return $this->session[$key] ?? null;
+ }
+
+ public function set($key, $value)
+ {
+ $this->session[$key] = $value;
+ }
+
+ public function delete($key)
+ {
+ unset($this->session[$key]);
+ }
+
+ public function destroy()
+ {
+ $this->session = [];
+ }
+}
\ No newline at end of file
diff --git a/src/Models/SpellModel.php b/src/Models/SpellModel.php
new file mode 100644
index 00000000..e808e788
--- /dev/null
+++ b/src/Models/SpellModel.php
@@ -0,0 +1,199 @@
+tableName}
+ WHERE
+ id = ?";
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("i", $spellId);
+ $stmt->execute();
+
+ $result = $stmt->get_result();
+
+ if ($row = $result->fetch_assoc()) {
+ $spell = new Spell($row);
+ //$spell->setEffectsFromRow($row);
+ return $spell;
+ }
+ return null;
+ }
+
+ public function getPaginated($params, $offset, $limit)
+ {
+ $validParams = Spell::getValidParams();
+ $paramTypes = '';
+ $paramValues = [];
+
+ $whereClauses = [];
+
+ // Name filtering
+ if (!empty($params['name'])) {
+ $whereClauses[] = "name LIKE CONCAT('%', ?, '%')";
+ $paramTypes .= 's';
+ $paramValues[] = $params['name'];
+ }
+
+ // Handling class-specific filtering
+ if (!empty($params['class']) && is_numeric($params['class']) && $params['class'] >= 1 && $params['class'] <= 16) {
+ $classField = 'classes' . $params['class'];
+ $whereClauses[] = "$classField != 255"; // Exclude spells that do not apply to the class
+ }
+
+ if (!empty($params['level']) && is_numeric($params['level'])) {
+ $level = (int) $params['level'];
+ $levelConditions = [];
+
+ for ($i = 1; $i <= 16; $i++) {
+ $classField = 'classes' . $i;
+ switch ($params['opt']) {
+ case 1:
+ $levelConditions[] = "($classField = ? AND $classField != 255)";
+ break;
+ case 2:
+ $levelConditions[] = "($classField >= ? AND $classField != 255)";
+ break;
+ case 3:
+ $levelConditions[] = "($classField <= ? AND $classField > 0 AND $classField != 255)";
+ break;
+ default:
+ $levelConditions[] = "($classField = ? AND $classField != 255)";
+ break;
+ }
+ }
+
+ if (!empty($params['class'])) {
+ // Apply level condition to the specific class only
+ $whereClauses[] = implode(' ', array_slice($levelConditions, $params['class'] - 1, 1));
+ $paramTypes .= 'i';
+ $paramValues[] = $level; // Bind level once for the specific class
+ } else {
+ // Apply level condition to all classes
+ $whereClauses[] = '(' . implode(' OR ', $levelConditions) . ')';
+ for ($i = 1; $i <= 16; $i++) { // Bind the level parameter for each class condition
+ $paramTypes .= 'i';
+ $paramValues[] = $level;
+ }
+ }
+ }
+
+ $whereString = !empty($whereClauses) ? 'WHERE ' . implode(' AND ', $whereClauses) : '';
+
+ $query = "
+ SELECT
+ *
+ FROM
+ {$this->tableName}
+ {$whereString}
+ LIMIT ?, ?";
+ $stmt = $this->db->prepare($query);
+ $paramTypes .= 'ii';
+ $paramValues[] = $offset;
+ $paramValues[] = $limit;
+
+ $stmt->bind_param($paramTypes, ...$paramValues);
+ $stmt->execute();
+ $result = $stmt->get_result();
+
+ $spells = [];
+ while ($row = $result->fetch_assoc()) {
+ $spells[] = new Spell($row);
+ }
+ return $spells;
+ }
+
+ public function getItemCount($params)
+ {
+ $validParams = Spell::getValidParams();
+ $paramTypes = '';
+ $paramValues = [];
+
+ $whereClauses = [];
+
+ // Name filtering
+ if (!empty($params['name'])) {
+ $whereClauses[] = "name LIKE CONCAT('%', ?, '%')";
+ $paramTypes .= 's';
+ $paramValues[] = $params['name'];
+ }
+
+ // Handling class-specific filtering
+ if (!empty($params['class']) && is_numeric($params['class']) && $params['class'] >= 1 && $params['class'] <= 16) {
+ $classField = 'classes' . $params['class'];
+ $whereClauses[] = "$classField != 255"; // Exclude spells that do not apply to the class
+ }
+
+ if (!empty($params['level']) && is_numeric($params['level'])) {
+ $level = (int) $params['level'];
+ $levelConditions = [];
+
+ for ($i = 1; $i <= 16; $i++) {
+ $classField = 'classes' . $i;
+ switch ($params['opt']) {
+ case 1:
+ $levelConditions[] = "($classField = ? AND $classField != 255)";
+ break;
+ case 2:
+ $levelConditions[] = "($classField >= ? AND $classField != 255)";
+ break;
+ case 3:
+ $levelConditions[] = "($classField <= ? AND $classField > 0 AND $classField != 255)";
+ break;
+ default:
+ $levelConditions[] = "($classField = ? AND $classField != 255)";
+ break;
+ }
+ }
+
+ if (!empty($params['class'])) {
+ // Apply level condition to the specific class only
+ $whereClauses[] = implode(' ', array_slice($levelConditions, $params['class'] - 1, 1));
+ $paramTypes .= 'i';
+ $paramValues[] = $level; // Bind level once for the specific class
+ } else {
+ // Apply level condition to all classes
+ $whereClauses[] = '(' . implode(' OR ', $levelConditions) . ')';
+ for ($i = 1; $i <= 16; $i++) { // Bind the level parameter for each class condition
+ $paramTypes .= 'i';
+ $paramValues[] = $level;
+ }
+ }
+ }
+
+ $whereString = !empty($whereClauses) ? 'WHERE ' . implode(' AND ', $whereClauses) : '';
+
+ $query = "
+ SELECT
+ COUNT(*) as count
+ FROM
+ {$this->tableName}
+ {$whereString}";
+ $stmt = $this->db->prepare($query);
+
+ $paramReferences = [];
+ foreach ($paramValues as $i => $value) {
+ $paramReferences[$i] = &$paramValues[$i];
+ }
+
+ if (!empty($paramValues)) {
+ $stmt->bind_param($paramTypes, ...$paramReferences);
+ }
+
+ $stmt->execute();
+ $result = $stmt->get_result();
+ $row = $result->fetch_assoc();
+ return $row['count'];
+ }
+}
\ No newline at end of file
diff --git a/src/Models/TaskModel.php b/src/Models/TaskModel.php
new file mode 100644
index 00000000..a3eb1634
--- /dev/null
+++ b/src/Models/TaskModel.php
@@ -0,0 +1,130 @@
+tableName}
+ WHERE
+ id = ?";
+ $stmt = $this->db->prepare($query);
+ $stmt->execute();
+
+ $result = $stmt->get_result();
+
+ if ($row = $result->fetch_assoc()) {
+ $task = new Task($row);
+ return $task;
+ }
+ return null;
+ }
+
+ public function getPaginated($params, $offset, $limit)
+ {
+ $validParams = Task::getValidParams();
+ $paramTypes = '';
+ $paramValues = [];
+
+ $whereClauses = [];
+
+ foreach ($params as $key => $value) {
+ if ($value != '' && isset($validParams[$key])) {
+ if ($key === 'name') {
+ $whereClauses[] = "$key LIKE CONCAT('%', ?, '%')";
+ } else {
+ $whereClauses[] = "$key = ?";
+ }
+ $paramTypes .= $validParams[$key];
+ $paramValues[] = $value;
+ }
+ }
+
+ $whereString = !empty($whereClauses) ? 'WHERE ' . implode(' AND ', $whereClauses) : '';
+
+ $query = "
+ SELECT
+ *
+ FROM
+ {$this->tableName}
+ {$whereString}
+ LIMIT ?, ?";
+ $stmt = $this->db->prepare($query);
+
+ $paramTypes .= 'ii';
+
+ array_push($paramValues, $offset, $limit);
+
+ $paramReferences = [];
+ foreach ($paramValues as $i => $value) {
+ $paramReferences[$i] = &$paramValues[$i];
+ }
+
+ if (!empty($paramValues)) {
+ $stmt->bind_param($paramTypes, ...$paramReferences);
+ }
+
+ $stmt->execute();
+ $result = $stmt->get_result();
+
+ $tasks = [];
+ while ($row = $result->fetch_assoc()) {
+ $tasks[] = new Task($row);
+ }
+ return $tasks;
+ }
+
+ public function getItemCount($params)
+ {
+ $validParams = Task::getValidParams();
+ $paramTypes = '';
+ $paramValues = [];
+
+ $whereClauses = [];
+
+ foreach ($params as $key => $value) {
+ if ($value != '' && isset($validParams[$key])) {
+ if ($key === 'name') {
+ $whereClauses[] = "$key LIKE CONCAT('%', ?, '%')";
+ } else {
+ $whereClauses[] = "$key = ?";
+ }
+ $paramTypes .= $validParams[$key];
+ $paramValues[] = $value;
+ }
+ }
+
+ $whereString = !empty($whereClauses) ? 'WHERE ' . implode(' AND ', $whereClauses) : '';
+
+ $query = "
+ SELECT
+ COUNT(*) as count
+ FROM
+ {$this->tableName}
+ {$whereString}";
+ $stmt = $this->db->prepare($query);
+
+ $paramReferences = [];
+ foreach ($paramValues as $i => $value) {
+ $paramReferences[$i] = &$paramValues[$i];
+ }
+
+ if (!empty($paramValues)) {
+ $stmt->bind_param($paramTypes, ...$paramReferences);
+ }
+
+ $stmt->execute();
+ $result = $stmt->get_result();
+ $row = $result->fetch_assoc();
+ return $row['count'];
+ }
+}
\ No newline at end of file
diff --git a/src/Models/ZoneModel.php b/src/Models/ZoneModel.php
new file mode 100644
index 00000000..ea50d2ac
--- /dev/null
+++ b/src/Models/ZoneModel.php
@@ -0,0 +1,545 @@
+tableName}
+ WHERE
+ zoneidnumber = ?";
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("i", $zoneId);
+ $stmt->execute();
+
+ $result = $stmt->get_result();
+
+ if ($row = $result->fetch_assoc()) {
+ $zone = new Zone($row);
+ return $zone;
+ }
+ return null;
+ }
+
+ public function getByShortName($shortName)
+ {
+ $query = "
+ SELECT
+ *
+ FROM
+ {$this->tableName}
+ WHERE
+ short_name = ?";
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("s", $shortName);
+ $stmt->execute();
+
+ $result = $stmt->get_result();
+
+ if ($row = $result->fetch_assoc()) {
+ $zone = new Zone($row);
+ return $zone;
+ }
+ return null;
+ }
+
+ public function getNpcs($zoneShortName, $order, $hideInvisibleMen = true, $groupNpcsByName = true)
+ {
+ $query = "
+ SELECT
+ nt.*,
+ MAX(nt.level) as level,
+ MAX(nt.trackable) as trackable,
+ MAX(nt.maxlevel) as maxlevel,
+ MAX(nt.race) as race,
+ MAX(nt.loottable_id) as loottable_id
+ FROM
+ {$this->tables['npcs_table']} nt
+ JOIN {$this->tables['spawn_entries_table']} se ON se.npcID = nt.id
+ JOIN {$this->tables['spawns_table']} sp ON se.spawngroupID = sp.spawngroupID
+ JOIN {$this->tables['spawn_groups_table']} sg ON se.spawngroupID = sg.id
+ WHERE
+ sp.zone = ?";
+ if (Config::getInstance()->get('display.hide_invisible_men')) {
+ $query .= " AND nt.race NOT IN (127, 240)";
+ }
+
+ $query .= "
+ GROUP BY nt.name
+ ORDER BY nt.name";
+
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("s", $zoneShortName);
+ $stmt->execute();
+ $result = $stmt->get_result();
+
+ $npcs = [];
+ while ($row = $result->fetch_assoc()) {
+ $npcs[] = new Npc($row);
+ }
+
+ return $npcs;
+ }
+
+ public function getZoneConnections($currentZone)
+ {
+ $query = "
+ SELECT
+ id,
+ zone,
+ number,
+ x,
+ y,
+ z,
+ target_y,
+ target_x,
+ target_z,
+ target_zone_id
+ FROM
+ {$this->tables['zone_connections_table']}
+ WHERE
+ zone = ?";
+
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("s", $currentZone);
+ $stmt->execute();
+ $result = $stmt->get_result();
+
+ $zoneConnections = [];
+ while ($row = $result->fetch_assoc()) {
+ $connectedZoneId = $row['target_zone_id'];
+ // Get connected zone details from database
+ $connectedZoneDetails = $this->getByZoneidnumber($connectedZoneId);
+
+ // Now, create a ZoneConnection object or associative array including the direction
+ $direction = $this->getDirectionFromCoordinates($row['x'], $row['y']);
+ $zoneConnection = [
+ 'currentZone' => $currentZone,
+ 'connectedZone' => $connectedZoneDetails,
+ 'direction' => $direction,
+ 'x' => $row['x'],
+ 'y' => $row['y'],
+ 'z' => $row['z']
+ // ... any other details you need
+ ];
+ $zoneConnections[] = $zoneConnection;
+ }
+
+ return $zoneConnections;
+ }
+
+ private function getDirectionFromCoordinates($x, $y)
+ {
+ // Invert x-axis
+ $x = -$x;
+
+ $angle = atan2($y, $x);
+
+ // Convert radians to degrees
+ $degrees = rad2deg($angle);
+
+ // Correct the angle to start from the north
+ $degrees = (360 + (90 - $degrees)) % 360;
+
+ // Define the compass rose as an array
+ $compass = [
+ 'N',
+ 'NNE',
+ 'NE',
+ 'ENE',
+ 'E',
+ 'ESE',
+ 'SE',
+ 'SSE',
+ 'S',
+ 'SSW',
+ 'SW',
+ 'WSW',
+ 'W',
+ 'WNW',
+ 'NW',
+ 'NNW',
+ 'N'
+ ];
+
+ // Determine the index of the direction
+ $index = round($degrees / 22.5);
+ if ($index >= count($compass)) {
+ $index -= count($compass);
+ }
+ // Return the direction
+ return $compass[$index];
+ }
+
+ public function getItems($zoneShortName)
+ {
+ $merchantClassesPlaceholder = '';
+ $discoveredItemsJoin = '';
+
+ if (false && !empty($dbmerchants)) {
+ $merchantClassesPlaceholder = implode(',', array_fill(0, count($dbmerchants), '?'));
+ }
+
+ if (false) {
+ $discoveredItemsJoin = 'JOIN discovered_items di ON it.id = di.item_id';
+ }
+
+ $query = "
+ SELECT DISTINCT
+ it.*
+ FROM
+ {$this->tables['items_table']} it
+ JOIN {$this->tables['loot_drop_entries_table']} lde ON it.id = lde.item_id
+ JOIN {$this->tables['loot_table_entries_table']} lte ON lde.lootdrop_id = lte.lootdrop_id
+ JOIN {$this->tables['npcs_table']} nt ON lte.loottable_id = nt.loottable_id
+ JOIN {$this->tables['spawn_entries_table']} se ON nt.id = se.npcID
+ JOIN {$this->tables['spawn_groups_table']} sg ON se.spawngroupID = sg.id
+ JOIN {$this->tables['spawns_table']} sp ON sg.id = sp.spawngroupID
+ {$discoveredItemsJoin}
+ WHERE
+ sp.zone = ?
+ " . ($merchantClassesPlaceholder ? "AND nt.class NOT IN ($merchantClassesPlaceholder)" : '') . "
+ GROUP BY
+ it.id
+ ORDER BY
+ it.name";
+
+ $stmt = $this->db->prepare($query);
+
+ $types = 's';
+ $params = [$zoneShortName];
+
+ if ($merchantClassesPlaceholder) {
+ $types .= str_repeat('i', count($dbmerchants));
+ $params = array_merge($params, $dbmerchants);
+ }
+
+ $stmt->bind_param($types, ...$params);
+ $stmt->execute();
+ $result = $stmt->get_result();
+
+ $items = [];
+ while ($row = $result->fetch_assoc()) {
+ $items[] = new Item($row);
+ }
+
+ return $items;
+ }
+
+ public function getSpawns($zoneShortName)
+ {
+ $query = "
+ SELECT
+ sg.id AS spawngroup_id,
+ sg.name AS spawngroup_name,
+ sp.x,
+ sp.y,
+ sp.z,
+ sp.respawntime,
+ nt.name AS npc_name,
+ nt.level AS npc_level,
+ se.chance
+ FROM
+ {$this->tables['spawns_table']} sp
+ JOIN {$this->tables['spawn_groups_table']} sg ON sp.spawngroupID = sg.id
+ JOIN {$this->tables['spawn_entries_table']} se ON sg.id = se.spawngroupID
+ JOIN {$this->tables['npcs_table']} nt ON se.npcID = nt.id
+ JOIN {$this->tableName} z ON sp.zone = z.short_name
+ WHERE
+ z.short_name = ?
+ ORDER BY
+ sg.name, nt.level";
+
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("s", $zoneShortName);
+ $stmt->execute();
+ $result = $stmt->get_result();
+
+ $spawns = [];
+ while ($row = $result->fetch_assoc()) {
+ $spawns[] = new Npc($row);
+ }
+ return $spawns;
+ }
+ public function getForageable($zoneShortName)
+ {
+ $query = "
+ SELECT
+ it.*
+ FROM
+ {$this->tables['items_table']} it
+ JOIN {$this->tables['forage_table']} f ON it.id = f.itemid
+ JOIN {$this->tableName} z ON f.zoneid = z.zoneidnumber
+ WHERE
+ z.short_name = ?";
+
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("s", $zoneShortName);
+ $stmt->execute();
+ $result = $stmt->get_result();
+
+ $forageables = [];
+ while ($row = $result->fetch_assoc()) {
+ $forageables[] = new Item($row);
+ }
+ return $forageables;
+ }
+
+ public function getTasks($zoneShortName)
+ {
+ $query = "
+ SELECT
+ t.id,
+ t.title,
+ t.description,
+ t.minlevel,
+ t.maxlevel,
+ t.reward,
+ t.rewardid,
+ t.rewardmethod
+ FROM
+ {$this->tables['tasks_table']} t
+ JOIN {$this->tableName} z ON t.startzone = z.zoneidnumber
+ WHERE
+ z.short_name = ?";
+
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("s", $zoneShortName);
+ $stmt->execute();
+ $result = $stmt->get_result();
+
+ $tasks = [];
+ while ($row = $result->fetch_assoc()) {
+ $tasks[] = new Task($row);
+ }
+ return $tasks;
+ }
+
+ public function getAverageLevels($hideInvisibleMen = true, $minimumNpcLevel = 1, $lowlimit = 5)
+ {
+ $zones = array();
+ $query = "SELECT z.short_name, z.long_name,
+ n.level, n.race
+ FROM {$this->tableName} z
+ JOIN {$this->tables['spawns_table']} s ON s.zone = z.short_name
+ JOIN {$this->tables['spawn_entries_table']} se ON se.spawngroupID = s.spawngroupID
+ JOIN {$this->tables['npcs_table']} n ON n.id = se.npcID";
+
+ if ($hideInvisibleMen) {
+ $query .= " WHERE n.race NOT IN (127, 240)";
+ }
+
+ $query .= " AND n.level > ?";
+ $query .= " ORDER BY z.long_name ASC";
+
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("i", $minimumNpcLevel);
+ $stmt->execute();
+ $result = $stmt->get_result();
+
+ while ($row = $result->fetch_assoc()) {
+ $levelIndex = floor($row['level'] / 5);
+ $shortName = $row['short_name'];
+
+ if (!isset($zones[$shortName])) {
+ $zones[$shortName] = [
+ 'long_name' => $row['long_name'],
+ 'levels' => array_fill(0, 20, 0),
+ 'total_npcs' => 0
+ ];
+ }
+
+ $zones[$shortName]['levels'][$levelIndex]++;
+ $zones[$shortName]['total_npcs']++;
+ }
+
+ foreach ($zones as $key => $zone) {
+ $totalLevelSum = 0;
+ $totalLevelCount = 0;
+ foreach ($zone['levels'] as $level => $count) {
+ if ($count > $lowlimit) {
+ $totalLevelSum += ($level * 5 + 2.5) * $count;
+ $totalLevelCount += $count;
+ }
+ }
+
+ $averageLevel = ($totalLevelCount > 0) ? $totalLevelSum / $totalLevelCount : 0;
+ $zones[$key]['average_level'] = $averageLevel;
+ }
+
+ return $zones;
+ }
+
+ public function getPaginated($params, $offset, $limit)
+ {
+ $validParams = Zone::getValidParams();
+ $paramTypes = '';
+ $paramValues = [];
+
+ $whereClauses = [];
+
+ foreach ($params as $key => $value) {
+ if (isset($validParams[$key])) {
+ if ($key === 'long_name') {
+ $whereClauses[] = "$key LIKE CONCAT('%', ?, '%')";
+ $paramTypes .= $validParams['long_name'];
+ $paramValues[] = $value;
+ } else {
+ $whereClauses[] = "$key = ?";
+ $paramTypes .= $validParams[$key];
+ $paramValues[] = $value;
+ }
+ }
+ }
+
+ $whereString = count($whereClauses) > 0 ? 'WHERE ' . implode(' AND ', $whereClauses) : '';
+
+ $query = "
+ SELECT
+ *
+ FROM
+ {$this->tableName}
+ {$whereString}
+ LIMIT ?, ?";
+ $stmt = $this->db->prepare($query);
+
+ $paramTypes .= "ii";
+
+ array_push($paramValues, $offset, $limit);
+
+ $paramReferences = [];
+ foreach ($paramValues as $i => $value) {
+ $paramReferences[$i] = &$paramValues[$i];
+ }
+
+ if (!empty($paramValues)) {
+ $stmt->bind_param($paramTypes, ...$paramReferences);
+ }
+
+ $stmt->execute();
+ $result = $stmt->get_result();
+
+ $zones = [];
+ while ($row = $result->fetch_assoc()) {
+ $zones[] = new Zone($row);
+ }
+ return $zones;
+ }
+
+ public function getItemCount($params)
+ {
+ $validParams = Zone::getValidParams();
+ $paramTypes = '';
+ $paramValues = [];
+
+ $whereClauses = [];
+
+ foreach ($params as $key => $value) {
+ if (isset($validParams[$key])) {
+ if ($key === 'name') {
+ $whereClauses[] = "long_name LIKE CONCAT('%', ?, '%')";
+ $whereClauses[] = "short_name LIKE CONCAT('%', ?, '%')";
+ $paramTypes .= $validParams['long_name'];
+ $paramValues[] = $value;
+ $paramTypes .= $validParams['short_name'];
+ $paramValues[] = $value;
+ } else {
+ $whereClauses[] = "$key = ?";
+ $paramTypes .= $validParams[$key];
+ $paramValues[] = $value;
+ }
+ }
+ }
+
+ $whereString = count($whereClauses) > 0 ? 'WHERE ' . implode(' AND ', $whereClauses) : '';
+
+ $query = "
+ SELECT
+ COUNT(*) as count
+ FROM
+ {$this->tableName}
+ {$whereString}";
+ $stmt = $this->db->prepare($query);
+
+ $paramReferences = [];
+ foreach ($paramValues as $i => $value) {
+ $paramReferences[$i] = &$paramValues[$i];
+ }
+
+ if (!empty($paramValues)) {
+ $stmt->bind_param($paramTypes, ...$paramReferences);
+ }
+
+ $stmt->execute();
+ $result = $stmt->get_result();
+ $row = $result->fetch_assoc();
+ return $row['count'];
+ }
+
+ public function getAverageLevelsForZones($hideInvisibleMen = true, $minimumNpcLevel = 1, $lowlimit = 5) {
+ $zones = array();
+ $query = "
+ SELECT
+ z.short_name,
+ z.long_name,
+ n.level,
+ n.race
+ FROM
+ {$this->tableName} z
+ JOIN {$this->tables['spawns_table']} s ON s.zone = z.short_name
+ JOIN {$this->tables['spawn_entries_table']} se ON se.spawngroupID = s.spawngroupID
+ JOIN {$this->tables['npcs_table']} n ON n.id = se.npcID";
+
+ if ($hideInvisibleMen) {
+ $query .= " WHERE n.race NOT IN (127, 240)"; // Assuming these races are for invisible men
+ }
+
+ $query .= " AND n.level > ?";
+ $query .= " ORDER BY z.long_name ASC";
+
+ $stmt = $this->db->prepare($query);
+ $stmt->bind_param("i", $minimumNpcLevel);
+ $stmt->execute();
+ $result = $stmt->get_result();
+
+ while ($row = $result->fetch_assoc()) {
+ $levelIndex = floor($row['level'] / 5);
+ $shortName = $row['short_name'];
+
+ if (!isset($zones[$shortName])) {
+ $zones[$shortName] = [
+ 'long_name' => $row['long_name'],
+ 'levels' => array_fill(0, 20, 0), // Adjust the number 20 based on your level grouping needs
+ 'total_npcs' => 0
+ ];
+ }
+
+ $zones[$shortName]['levels'][$levelIndex]++;
+ $zones[$shortName]['total_npcs']++;
+ }
+
+ foreach ($zones as $key => $zone) {
+ $totalLevelSum = 0;
+ $totalLevelCount = 0;
+ foreach ($zone['levels'] as $level => $count) {
+ if ($count > $lowlimit) {
+ $totalLevelSum += ($level * 5 + 2.5) * $count; // Level * 5 + 2.5 gives the midpoint of each level range
+ $totalLevelCount += $count;
+ }
+ }
+
+ $averageLevel = ($totalLevelCount > 0) ? $totalLevelSum / $totalLevelCount : 0;
+ $zones[$key]['average_level'] = $averageLevel;
+ }
+
+ return $zones;
+ }
+}
\ No newline at end of file
diff --git a/src/Services/FactionService.php b/src/Services/FactionService.php
new file mode 100644
index 00000000..1c8f6f55
--- /dev/null
+++ b/src/Services/FactionService.php
@@ -0,0 +1,20 @@
+factionModel = $factionModel;
+ }
+
+ public function getById($factionId)
+ {
+ $factions = $this->factionModel->getById($factionId);
+ foreach ($factions as $faction) {
+ //$npc->lootEntries = $this->lootModel->getLootByNpcId($npc->id);
+ }
+ return $factions;
+ }
+}
\ No newline at end of file
diff --git a/src/Services/ItemService.php b/src/Services/ItemService.php
new file mode 100644
index 00000000..900791da
--- /dev/null
+++ b/src/Services/ItemService.php
@@ -0,0 +1,16 @@
+itemModel = $itemModel;
+ }
+
+ public function getItemsByZone($zoneId)
+ {
+ return;
+ }
+}
\ No newline at end of file
diff --git a/src/Services/LuaQuestService.php b/src/Services/LuaQuestService.php
new file mode 100644
index 00000000..46f59392
--- /dev/null
+++ b/src/Services/LuaQuestService.php
@@ -0,0 +1,39 @@
+questsPath = $questsPath;
+ }
+
+ public function getQuestsForNpc($npcName)
+ {
+ $filePath = $this->questsPath . DIRECTORY_SEPARATOR . $npcName . '.lua';
+
+ if (!file_exists($filePath)) {
+ return null;
+ }
+
+ $luaContent = file_get_contents($filePath);
+
+ $patterns = [
+ '/function event_spawn\(.*?\)(.*?)end/s',
+ '/function event_say\(.*?\)(.*?)end/s',
+ '/function event_trade\(.*?\)(.*?)end/s',
+ '/function event_timer\(.*?\)(.*?)end/s',
+ ];
+
+ // Parsing the Lua content
+ foreach ($patterns as $pattern) {
+ preg_match_all($pattern, $luaContent, $matches);
+ foreach ($matches[1] as $match) {
+ echo "Found function block: \n" . trim($match) . "\n\n";
+ }
+ }
+
+ return;
+ }
+}
\ No newline at end of file
diff --git a/src/Services/NpcService.php b/src/Services/NpcService.php
new file mode 100644
index 00000000..345daf24
--- /dev/null
+++ b/src/Services/NpcService.php
@@ -0,0 +1,37 @@
+npcModel = $npcModel;
+ $this->factionModel = $factionService;
+ $this->zoneModel = $zoneService;
+ }
+
+ public function getById($zoneId)
+ {
+ $npc = $this->npcModel->getById($zoneId);
+ $npc->setLootDrops($this->npcModel->getLootDrops($npc->getId()));
+ $npc->setSpawnGroups($this->npcModel->getSpawnGroups($npc->getId()));
+ $npc->setFactions($this->npcModel->getFactions($npc->getNpcFactionId()));
+ $npc->setPrimaryFaction($this->npcModel->getPrimaryFaction($npc->getNpcFactionId()));
+ return $npc;
+ }
+
+ public function getByZone($zoneId)
+ {
+ $npcs = $this->npcModel->getByZone($zoneId);
+ foreach ($npcs as $npc) {
+ $npc->lootDrops = $this->npcModel->getLootDrops($npc->getId);
+ $npc->spawnGroups = $this->npcModel->getSpawnGroups($npc->getId());
+ $npc->factions = $this->npcModel->getFactions($npc->getNpcFactionId());
+ $npc->primaryFaction = $this->npcModel->getPrimaryFaction($npc->getNpcFactionId());
+ }
+ return $npcs;
+ }
+}
\ No newline at end of file
diff --git a/src/Services/RecipeService.php b/src/Services/RecipeService.php
new file mode 100644
index 00000000..03ed6fb7
--- /dev/null
+++ b/src/Services/RecipeService.php
@@ -0,0 +1,20 @@
+recipeModel = $recipeModel;
+ }
+
+ public function getNpcsByZone($zoneId)
+ {
+ $recipes = $this->recipeModel->getNpcsByZone($zoneId);
+ foreach ($recipes as $recipe) {
+ //$npc->lootEntries = $this->lootModel->getLootByNpcId($npc->id);
+ }
+ return $recipes;
+ }
+}
\ No newline at end of file
diff --git a/src/Services/SpellService.php b/src/Services/SpellService.php
new file mode 100644
index 00000000..4e951956
--- /dev/null
+++ b/src/Services/SpellService.php
@@ -0,0 +1,20 @@
+spellModel = $spellModel;
+ }
+
+ public function getSpellsByZone($zoneId)
+ {
+ $spells = $this->spellModel->getSpellsByZone($zoneId);
+ foreach ($spells as $spell) {
+ //$spell->lootEntries = $this->lootModel->getLootBySpellId($spell->id);
+ }
+ return $spells;
+ }
+}
\ No newline at end of file
diff --git a/src/Services/TaskService.php b/src/Services/TaskService.php
new file mode 100644
index 00000000..abf16c9f
--- /dev/null
+++ b/src/Services/TaskService.php
@@ -0,0 +1,16 @@
+taskModel = $taskModel;
+ }
+
+ public function getNpcsByZone($zoneId)
+ {
+ return;
+ }
+}
\ No newline at end of file
diff --git a/src/Services/ZoneService.php b/src/Services/ZoneService.php
new file mode 100644
index 00000000..2fb83a91
--- /dev/null
+++ b/src/Services/ZoneService.php
@@ -0,0 +1,29 @@
+zoneModel = $zoneModel;
+ $this->npcModel = $npcModel;
+ $this->itemModel = $itemModel;
+ $this->taskModel = $taskModel;
+ }
+
+ public function getByShortName($shortName)
+ {
+ $zone = $this->zoneModel->getByShortName($shortName);
+ $zone->setNpcs($this->zoneModel->getNpcs($zone->getShortName(), 'ASC'));
+ $zone->setItems($this->zoneModel->getItems($zone->getShortName()));
+ $zone->setForageable($this->zoneModel->getForageable($zone->getShortName()));
+ $zone->setConnectedZones($this->zoneModel->getZoneConnections($zone->getShortName()));
+ //$zone->setQuests($this->zoneModel->getQuests($zone->getShortName()));
+
+ return $zone;
+ }
+}
\ No newline at end of file
diff --git a/src/Utility/Container.php b/src/Utility/Container.php
new file mode 100644
index 00000000..f01e08d4
--- /dev/null
+++ b/src/Utility/Container.php
@@ -0,0 +1,24 @@
+services[$key] = $callable;
+ }
+
+ public function get($key, ...$params)
+ {
+ if (!isset($this->instances[$key])) {
+ if (isset($this->services[$key])) {
+ $this->instances[$key] = $this->services[$key]($this, ...$params);
+ } else {
+ throw new Exception("Service {$key} is not defined.");
+ }
+ }
+ return $this->instances[$key];
+ }
+}
\ No newline at end of file
diff --git a/src/Utility/Database.php b/src/Utility/Database.php
new file mode 100644
index 00000000..38c3622f
--- /dev/null
+++ b/src/Utility/Database.php
@@ -0,0 +1,19 @@
+conn = new mysqli($config['host'], $config['user'], $config['password'], $config['database'], $config['port']);
+ if ($this->conn->connect_error) {
+ die("Connection failed: " . $this->conn->connect_error);
+ }
+ }
+
+ public function getConnection()
+ {
+ return $this->conn;
+ }
+}
diff --git a/src/Utility/FileSystem.php b/src/Utility/FileSystem.php
new file mode 100644
index 00000000..70810eff
--- /dev/null
+++ b/src/Utility/FileSystem.php
@@ -0,0 +1,42 @@
+rootDir . $filePath);
+ }
+
+ public function setRoot($rootPath) {
+ $this->rootDir = $rootPath;
+ }
+
+ public function read($filePath) {
+ return file_get_contents($this->rootDir . $filePath);
+ }
+
+ public function write($filePath, $data) {
+ return file_put_contents($this->rootDir . $filePath, $data);
+ }
+
+ public function delete($filePath) {
+ return unlink($this->rootDir . $filePath);
+ }
+
+ public function getRootDir()
+ {
+ return self::$rootDir;
+ }
+}
\ No newline at end of file
diff --git a/src/Utility/Pagination.php b/src/Utility/Pagination.php
new file mode 100644
index 00000000..dc101f90
--- /dev/null
+++ b/src/Utility/Pagination.php
@@ -0,0 +1,31 @@
+totalPages = ceil($totalItems / $itemsPerPage);
+ $this->currentPage = $currentPage;
+ $this->route = $route;
+ $this->queryParams = $queryParams;
+ }
+
+ public function getPaginationData()
+ {
+ return [
+ 'totalPages' => $this->totalPages,
+ 'currentPage' => $this->currentPage,
+ 'route' => $this->route,
+ 'queryParams' => $this->queryParams,
+ 'prevPage' => max(1, $this->currentPage - 1),
+ 'nextPage' => min($this->totalPages, $this->currentPage + 1),
+ 'firstPage' => 1,
+ 'lastPage' => $this->totalPages
+ ];
+ }
+}
\ No newline at end of file
diff --git a/src/Utility/Request.php b/src/Utility/Request.php
new file mode 100644
index 00000000..ca6ed423
--- /dev/null
+++ b/src/Utility/Request.php
@@ -0,0 +1,12 @@
+container = $container;
+ }
+ private function __clone() { }
+ protected $routes = [];
+ protected $defaultRoute;
+ protected $container;
+
+ public static function getInstance($container)
+ {
+ if (self::$instance === null) {
+ self::$instance = new self($container);
+ }
+ return self::$instance;
+ }
+
+ public function add($route, $action) {
+
+ $this->routes[$route] = $action;
+ }
+
+ public function dispatch($route)
+ {
+ $params = $_GET;
+
+ if (array_key_exists($route, $this->routes)) {
+ $action = $this->routes[$route];
+
+ if (!Config::exists() || !Config::getInstance()->get('setup.complete')) {
+ $action = 'Setup#index';
+ }
+
+ if (strpos($action, '#') !== false) {
+ list($controllerPrefix, $methodName) = explode('#', $action, 2);
+ $controllerName = $controllerPrefix . 'Controller';
+ $controllerFile = '../src/Controllers/' . $controllerName . '.php';
+
+ if (file_exists($controllerFile)) {
+ require_once ($controllerFile);
+
+ if (class_exists($controllerName)) {
+ $controller = new $controllerName($this->container);
+ if (method_exists($controller, $methodName)) {
+ $refMethod = new ReflectionMethod($controller, $methodName);
+ $pass = [];
+ foreach ($refMethod->getParameters() as $param) {
+ if (isset($params[$param->getName()])) {
+ $pass[] = $params[$param->getName()];
+ } elseif ($param->isDefaultValueAvailable()) {
+ $pass[] = $param->getDefaultValue();
+ }
+ }
+ call_user_func_array([$controller, $methodName], $pass);
+ } else {
+ $template = new Template();
+ $template->assign('errorMessage', 'Critical Error: Method ' . $methodName . ' does not exist in ' . $controllerName . '. Please Reinstall Files');
+ $template->render('errors/5xx', true);
+ http_response_code(500);
+ }
+ } else {
+ $template = new Template();
+ $template->assign('errorMessage', 'Critical Error: Controller class ' . $controllerName . ' not found. Please Reinstall Files');
+ $template->render('errors/5xx', true);
+ http_response_code(500);
+ }
+ } else {
+ $template = new Template();
+ $template->assign('errorMessage', 'Critical Error: Controller file ' . $controllerFile .' not found. Please Reinstall Files');
+ $template->render('errors/5xx', true);
+ http_response_code(500);
+ }
+ } else {
+ if (is_callable($action)) {
+ call_user_func($action, $params);
+ } else {
+ require_once ($action);
+ }
+ }
+ } else {
+ if (isset($this->defaultRoute)) {
+ require_once ($this->defaultRoute);
+ } else {
+ $template = new Template();
+ $template->render('errors/404');
+ http_response_code(404);
+ }
+ }
+ }
+
+ public function redirect($uri) {
+ header('Location: ' . $uri);
+ exit;
+ }
+}
\ No newline at end of file
diff --git a/src/Utility/RuleSet.php b/src/Utility/RuleSet.php
new file mode 100644
index 00000000..c4c71914
--- /dev/null
+++ b/src/Utility/RuleSet.php
@@ -0,0 +1,42 @@
+db = $db;
+ $this->load();
+ }
+
+ private function __clone() { }
+
+ public static function getInstance($db = null)
+ {
+ if (self::$instance === null) {
+ self::$instance = new self($db);
+ }
+ return self::$instance;
+ }
+
+ public function get($key) {
+ if (array_key_exists($key, $this->rules)) {
+ return $this->rules[$key];
+ } else {
+ throw new Exception("Critical rule configuration error: Rule '{$key}' not found in the database.");
+ }
+ }
+
+ private function load()
+ {
+ $ruleset = Config::getInstance()->get('display.ruleset');
+ $stmt = $this->db->query("SELECT rule_name, rule_value FROM rule_sets WHERE ruleset_id = " . isset($ruleset) ? $ruleset : 1);
+ $results = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ foreach ($results as $rule) {
+ $this->rules[$rule['rule_name']] = $rule['rule_value'];
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Utility/Session.php b/src/Utility/Session.php
new file mode 100644
index 00000000..0cd3e5e2
--- /dev/null
+++ b/src/Utility/Session.php
@@ -0,0 +1,41 @@
+startIfNotStarted();
+ }
+
+ function startIfNotStarted()
+ {
+ if (!static::$sessionStarted) {
+ self::startSession();
+ static::$sessionStarted = true;
+ }
+ }
+
+ public static function startSession() {
+ if (session_status() == PHP_SESSION_NONE) {
+ session_start();
+ }
+ }
+
+ public static function set($key, $value) {
+ $_SESSION[$key] = $value;
+ }
+
+ public static function get($key) {
+ return $_SESSION[$key] ?? null;
+ }
+
+ public static function destroySession() {
+ session_unset();
+ session_destroy();
+ }
+
+ public static function regenerateSession() {
+ session_regenerate_id(true);
+ }
+}
\ No newline at end of file
diff --git a/src/Utility/Template.php b/src/Utility/Template.php
new file mode 100644
index 00000000..d0853b92
--- /dev/null
+++ b/src/Utility/Template.php
@@ -0,0 +1,56 @@
+templateDir = $templateDir ?? __DIR__ . '/../Views/';
+ $this->templateExt = $templateExt;
+ }
+
+ public function assign($name, $value)
+ {
+ $this->variables[$name] = $value;
+ }
+
+ public function render($templateName, $minimal = false, array $data = [])
+ {
+ $templateFile = $minimal ? 'common/minimal.php' : 'common/template.php';
+ $templatePath = $this->templateDir . $templateFile;
+ $contentPath = $this->templateDir . $templateName . $this->templateExt;
+ if (file_exists($templatePath)) {
+ if (file_exists($contentPath)) {
+ $data = array_merge($this->variables, $data);
+ extract($data); // Extracts vars to current view scope
+ include ($templatePath);
+ } else {
+ $template = new Template();
+ $template->assign('errorMessage', 'Critical Error: Missing or Corrupted View Files');
+ $template->render('errors/5xx');
+ http_response_code(500);
+ }
+ } else {
+ $template = new Template();
+ $template->assign('errorMessage', 'Critical Error: Missing or Corrupted Template Files');
+ $template->render('errors/5xx');
+ http_response_code(500);
+ }
+ }
+
+ public function renderPartial($partialName, array $data = [])
+ {
+ $partialPath = $this->templateDir . $partialName . $this->templateExt;
+ if (file_exists($partialPath)) {
+ $data = array_merge($this->variables, $data);
+ extract($data); // Extracts vars to current view scope
+ include ($partialPath);
+ } else {
+ $this->assign('errorMessage', 'Critical Error: Missing or Corrupted Partial Files');
+ $this->render('errors/5xx');
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Utility/Utilities.php b/src/Utility/Utilities.php
new file mode 100644
index 00000000..046e7e47
--- /dev/null
+++ b/src/Utility/Utilities.php
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+