diff --git a/Distribution/Configuration/modernuo.json b/Distribution/Configuration/modernuo.json index 55f494480..75728c72d 100644 --- a/Distribution/Configuration/modernuo.json +++ b/Distribution/Configuration/modernuo.json @@ -45,17 +45,6 @@ "crashGuard.restartServer": "True", "crashGuard.saveBackup": "True", "ethics.enable": "False", - "exileBestiary.damage.categoryMilestoneBonus": "0.20", - "exileBestiary.damage.markDuration": "00:00:10", - "exileBestiary.damage.markTickPercent": "0.01", - "exileBestiary.damage.markTickSeconds": "00:00:02", - "exileBestiary.damage.mode": "Mark", - "exileBestiary.damage.perCreatureBonus": "0.10", - "exileBestiary.damage.totalCap": "0.25", - "exileBestiary.debug": "True", - "exileBestiary.drop.chance": "-1.0", - "exileBestiary.drop.pityThreshold": "40", - "exileBestiary.enabled": "True", "houseDecay.enable": "True", "movement.delay.runFoot": "200", "movement.delay.runMount": "100", @@ -116,4 +105,4 @@ "world.savePath": "Saves", "world.useMultithreadedSaves": "True" } -} \ No newline at end of file +} diff --git a/Projects/UOContent/Engines/ExileHunterBestiary/CHANGES_v2.md b/Projects/UOContent/Engines/ExileHunterBestiary/CHANGES_v2.md deleted file mode 100644 index e6085a1a5..000000000 --- a/Projects/UOContent/Engines/ExileHunterBestiary/CHANGES_v2.md +++ /dev/null @@ -1,69 +0,0 @@ -# Exile Hunter's Bestiary - changelog & audit response - -## Final stable state -This build is the final working configuration for the current ModernUO project state: -- No BaseCreature patch required. -- Death handling is done through generated death events. -- Drop result is resolved from combat tracker + killer resolution. -- Treatise is dropped into the corpse when available; otherwise, it is retried for a short window. -- Legacy partial-file patching is not used. - -## Critical fixes applied -1. Treatise drop now lands in the corpse when the corpse exists. -2. Death processing no longer fails because the mob is already deleted by the time the event fires. -3. Auto-retry is used when corpse is not yet available immediately after death. -4. Duplicate processing of the same death is blocked by Serial-based guard. -5. Kill credit is resolved from LastKiller or combat tracker, not from dead/invalid objects. -6. Dead code and noisy debug logging were removed. -7. Registry validation is preserved. -8. Profile persistence remains compatible with XML save/load. - -## Architecture -- Death hook is registered via generated event: `BaseCreature.CreatureDeathEvent`. -- Combat tracker records the last player damage event and the creature location/map. -- Bestiary drop logic is separated into: - - Registry - - Profile - - Store - - Engine - - Combat tracker - - Death hook - - UI gump - - Treatise item - -## Balance -- Per-creature mastery bonus: +10% -- Category milestone bonus: up to +20% -- Total hard cap: +25% -- Pity timer: guaranteed treatise every 40 kills per creature -- Damage mode: Mark is default and patch-free - -## Important note -The system does NOT require modifying `BaseCreature.cs` in the final version. -The working approach is: -- use combat tracker from `AggressiveAction` -- use generated death event -- resolve actual killer -- drop treatise into corpse or retry for a short time - -## v3.2 - final stable build -- Removed old patch-based death handling and fallback partial overrides. -- Removed excessive debug logging from runtime combat flow. -- Death event is now the only actual drop trigger. -- Combat tracker stores last player hit and location before the mob is deleted. -- Treatise drop request waits a little if corpse is not immediately available. -- Final implementation is leaner and safer than patch-based variations. - -## v3.3 - docs cleanup -- Removed outdated instructions about BaseCreature edits. -- Removed legacy mentions of "Variant A/B/C/D" patch hooks. -- Clarified that `HunterCreatureDeathEvent.cs` is the correct active death hook. -- Corrected troubleshooting around corpse timing in ModernUO. - -## Current supported behavior -- Treatise drops only for registered creature types. -- Only players can receive credit. -- Summoned and bonded creatures are ignored. -- Global chance override: `exileBestiary.drop.chance` -- Default chance values are still taken from the registry if the global key is unset or set to -1. -- `exileBestiary.debug` is available for optional diagnostics, but production logs should stay clean. \ No newline at end of file diff --git a/Projects/UOContent/Engines/ExileHunterBestiary/Core/BestiaryCategory.cs b/Projects/UOContent/Engines/ExileHunterBestiary/Core/BestiaryCategory.cs deleted file mode 100644 index bb71cd5f5..000000000 --- a/Projects/UOContent/Engines/ExileHunterBestiary/Core/BestiaryCategory.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Server.Custom.ExileHunterBestiary; - -public enum BestiaryCategory -{ - Undead, - Daemons, - Reptiles, - Elementals, - Humanoids, - Arachnids, - Beasts, - Bosses -} diff --git a/Projects/UOContent/Engines/ExileHunterBestiary/Core/BestiaryData.cs b/Projects/UOContent/Engines/ExileHunterBestiary/Core/BestiaryData.cs deleted file mode 100644 index b301b249a..000000000 --- a/Projects/UOContent/Engines/ExileHunterBestiary/Core/BestiaryData.cs +++ /dev/null @@ -1,214 +0,0 @@ -namespace Server.Custom.ExileHunterBestiary; - -internal static class BestiaryData -{ - public static void RegisterAll() - { - - BestiaryRegistry.Register("Skeleton", "Skeleton", "Skeleton", BestiaryCategory.Undead, 2101, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("SkeletalMage", "Skeletal Mage", "SkeletalMage", BestiaryCategory.Undead, 2101, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("SkeletalKnight", "Skeletal Knight", "SkeletalKnight", BestiaryCategory.Undead, 2101, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("PatchworkSkeleton", "Patchwork Skeleton", "PatchworkSkeleton", BestiaryCategory.Undead, 2101, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Zombie", "Zombie", "Zombie", BestiaryCategory.Undead, 1175, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Ghoul", "Ghoul", "Ghoul", BestiaryCategory.Undead, 1175, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Shade", "Shade", "Shade", BestiaryCategory.Undead, 1150, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Spectre", "Spectre", "Spectre", BestiaryCategory.Undead, 1150, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Wraith", "Wraith", "Wraith", BestiaryCategory.Undead, 1150, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Mummy", "Mummy", "Mummy", BestiaryCategory.Undead, 2305, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("PestilentBandage", "Pestilent Bandage", "PestilentBandage", BestiaryCategory.Undead, 1150, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("BoneKnight", "Bone Knight", "BoneKnight", BestiaryCategory.Undead, 2101, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("BoneMagi", "Bone Magi", "BoneMagi", BestiaryCategory.Undead, 2101, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("RottingCorpse", "Rotting Corpse", "RottingCorpse", BestiaryCategory.Undead, 1175, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Lich", "Lich", "Lich", BestiaryCategory.Undead, 1150, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("LichLord", "Lich Lord", "LichLord", BestiaryCategory.Undead, 1153, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("AncientLich", "Ancient Lich", "AncientLich", BestiaryCategory.Undead, 1175, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("SkeletalDragon", "Skeletal Dragon", "SkeletalDragon", BestiaryCategory.Undead, 1175, 0.03); - BestiaryRegistry.Register("BoneDemon", "Bone Demon", "BoneDemon", BestiaryCategory.Undead, 2101, 0.04); - BestiaryRegistry.Register("UndeadGargoyle", "Undead Gargoyle", "UndeadGargoyle", BestiaryCategory.Undead, 1175, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("PutridUndeadGargoyle", "Putrid Undead Gargoyle", "PutridUndeadGargoyle", BestiaryCategory.Undead, 1175, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("WailingBanshee", "Wailing Banshee", "WailingBanshee", BestiaryCategory.Undead, 1153, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("GoreFiend", "Gore Fiend", "GoreFiend", BestiaryCategory.Undead, 1175, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("FleshGolem", "Flesh Golem", "FleshGolem", BestiaryCategory.Undead, 1175, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Gibberling", "Gibberling", "Gibberling", BestiaryCategory.Undead, 1175, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Rotworm", "Rotworm", "Rotworm", BestiaryCategory.Undead, 2215, HunterBestiaryEngine.DefaultDropChance); - - - BestiaryRegistry.Register("Daemon", "Daemon", "Daemon", BestiaryCategory.Daemons, 1258, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("FireDaemon", "Fire Daemon", "FireDaemon", BestiaryCategory.Daemons, 1260, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("IceFiend", "Ice Fiend", "IceFiend", BestiaryCategory.Daemons, 1152, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Succubus", "Succubus", "Succubus", BestiaryCategory.Daemons, 1166, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Balron", "Balron", "Balron", BestiaryCategory.Daemons, 1255, 0.03); - BestiaryRegistry.Register("ArcaneDaemon", "Arcane Daemon", "ArcaneDaemon", BestiaryCategory.Daemons, 1165, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("ChaosDaemon", "Chaos Daemon", "ChaosDaemon", BestiaryCategory.Daemons, 1167, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Moloch", "Moloch", "Moloch", BestiaryCategory.Daemons, 1257, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Ravager", "Ravager", "Ravager", BestiaryCategory.Daemons, 1258, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Devourer", "Devourer of Souls", "Devourer", BestiaryCategory.Daemons, 1153, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Imp", "Imp", "Imp", BestiaryCategory.Daemons, 1255, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Gargoyle", "Gargoyle", "Gargoyle", BestiaryCategory.Daemons, 2419, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("StoneGargoyle", "Stone Gargoyle", "StoneGargoyle", BestiaryCategory.Daemons, 2413, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("FireGargoyle", "Fire Gargoyle", "FireGargoyle", BestiaryCategory.Daemons, 1260, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("GargoyleEnforcer", "Gargoyle Enforcer", "GargoyleEnforcer", BestiaryCategory.Daemons, 2419, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("GargoyleDestroyer", "Gargoyle Destroyer", "GargoyleDestroyer", BestiaryCategory.Daemons, 2419, HunterBestiaryEngine.DefaultDropChance); - - - BestiaryRegistry.Register("Dragon", "Dragon", "Dragon", BestiaryCategory.Reptiles, 1365, 0.03); - BestiaryRegistry.Register("Drake", "Drake", "Drake", BestiaryCategory.Reptiles, 1362, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("WhiteWyrm", "White Wyrm", "WhiteWyrm", BestiaryCategory.Reptiles, 1150, 0.03); - BestiaryRegistry.Register("ShadowWyrm", "Shadow Wyrm", "ShadowWyrm", BestiaryCategory.Reptiles, 1109, 0.03); - BestiaryRegistry.Register("AncientWyrm", "Ancient Wyrm", "AncientWyrm", BestiaryCategory.Reptiles, 1370, 0.04); - BestiaryRegistry.Register("GreaterDragon", "Greater Dragon", "GreaterDragon", BestiaryCategory.Reptiles, 1360, 0.04); - BestiaryRegistry.Register("SwampDragon", "Swamp Dragon", "SwampDragon", BestiaryCategory.Reptiles, 2212, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Hydra", "Hydra", "Hydra", BestiaryCategory.Reptiles, 1368, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("SerpentineDragon", "Serpentine Dragon", "SerpentineDragon", BestiaryCategory.Reptiles, 1160, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Reptalon", "Reptalon", "Reptalon", BestiaryCategory.Reptiles, 1372, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Lizardman", "Lizardman", "Lizardman", BestiaryCategory.Reptiles, 2210, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("OphidianWarrior", "Ophidian Warrior", "OphidianWarrior", BestiaryCategory.Reptiles, 2001, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("OphidianMage", "Ophidian Mage", "OphidianMage", BestiaryCategory.Reptiles, 2002, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("OphidianMatriarch", "Ophidian Matriarch", "OphidianMatriarch", BestiaryCategory.Reptiles, 2003, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Slith", "Slith", "Slith", BestiaryCategory.Reptiles, 2212, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("ToxicSlith", "Toxic Slith", "ToxicSlith", BestiaryCategory.Reptiles, 1267, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("StoneSlith", "Stone Slith", "StoneSlith", BestiaryCategory.Reptiles, 2413, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("LavaLizard", "Lava Lizard", "LavaLizard", BestiaryCategory.Reptiles, 1260, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("LavaSerpent", "Lava Serpent", "LavaSerpent", BestiaryCategory.Reptiles, 1260, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("LavaSnake", "Lava Snake", "LavaSnake", BestiaryCategory.Reptiles, 1260, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("IceSerpent", "Ice Serpent", "IceSerpent", BestiaryCategory.Reptiles, 1152, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("IceSnake", "Ice Snake", "IceSnake", BestiaryCategory.Reptiles, 1152, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("SilverSerpent", "Silver Serpent", "SilverSerpent", BestiaryCategory.Reptiles, 2301, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("GiantSerpent", "Giant Serpent", "GiantSerpent", BestiaryCategory.Reptiles, 2210, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Alligator", "Alligator", "Alligator", BestiaryCategory.Reptiles, 2210, HunterBestiaryEngine.DefaultDropChance); - - - BestiaryRegistry.Register("EarthElemental", "Earth Elemental", "EarthElemental", BestiaryCategory.Elementals, 2419, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("AirElemental", "Air Elemental", "AirElemental", BestiaryCategory.Elementals, 1153, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("FireElemental", "Fire Elemental", "FireElemental", BestiaryCategory.Elementals, 1260, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("WaterElemental", "Water Elemental", "WaterElemental", BestiaryCategory.Elementals, 1165, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("PoisonElemental", "Poison Elemental", "PoisonElemental", BestiaryCategory.Elementals, 1267, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("BloodElemental", "Blood Elemental", "BloodElemental", BestiaryCategory.Elementals, 1258, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("SnowElemental", "Snow Elemental", "SnowElemental", BestiaryCategory.Elementals, 1150, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("IceElemental", "Ice Elemental", "IceElemental", BestiaryCategory.Elementals, 1152, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Efreet", "Efreet", "Efreet", BestiaryCategory.Elementals, 1260, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("LavaElemental", "Lava Elemental", "LavaElemental", BestiaryCategory.Elementals, 1260, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("SandVortex", "Sand Vortex", "SandVortex", BestiaryCategory.Elementals, 2419, HunterBestiaryEngine.DefaultDropChance); - - - BestiaryRegistry.Register("Ogre", "Ogre", "Ogre", BestiaryCategory.Humanoids, 2215, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("OgreLord", "Ogre Lord", "OgreLord", BestiaryCategory.Humanoids, 2216, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("ArcticOgreLord", "Arctic Ogre Lord", "ArcticOgreLord", BestiaryCategory.Humanoids, 1150, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Ettin", "Ettin", "Ettin", BestiaryCategory.Humanoids, 2214, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Troll", "Troll", "Troll", BestiaryCategory.Humanoids, 2213, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Titan", "Titan", "Titan", BestiaryCategory.Humanoids, 1153, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Cyclops", "Cyclops", "Cyclops", BestiaryCategory.Humanoids, 2217, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Orc", "Orc", "Orc", BestiaryCategory.Humanoids, 2208, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("OrcishLord", "Orcish Lord", "OrcishLord", BestiaryCategory.Humanoids, 2209, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("OrcCaptain", "Orc Captain", "OrcCaptain", BestiaryCategory.Humanoids, 2210, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("OrcishMage", "Orcish Mage", "OrcishMage", BestiaryCategory.Humanoids, 2208, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("OrcBrute", "Orc Brute", "OrcBrute", BestiaryCategory.Humanoids, 2211, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Ratman", "Ratman", "Ratman", BestiaryCategory.Humanoids, 2307, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("RatmanArcher", "Ratman Archer", "RatmanArcher", BestiaryCategory.Humanoids, 2307, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("RatmanMage", "Ratman Mage", "RatmanMage", BestiaryCategory.Humanoids, 2308, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Minotaur", "Minotaur", "Minotaur", BestiaryCategory.Humanoids, 2418, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Centaur", "Centaur", "Centaur", BestiaryCategory.Humanoids, 2215, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Satyr", "Satyr", "Satyr", BestiaryCategory.Humanoids, 2215, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("InsaneDryad", "Insane Dryad", "InsaneDryad", BestiaryCategory.Humanoids, 2210, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Pixie", "Pixie", "Pixie", BestiaryCategory.Humanoids, 1160, HunterBestiaryEngine.DefaultDropChance); - - - BestiaryRegistry.Register("GiantSpider", "Giant Spider", "GiantSpider", BestiaryCategory.Arachnids, 2110, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("DreadSpider", "Dread Spider", "DreadSpider", BestiaryCategory.Arachnids, 1175, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("FrostSpider", "Frost Spider", "FrostSpider", BestiaryCategory.Arachnids, 1152, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("GiantBlackWidow", "Giant Black Widow", "GiantBlackWidow", BestiaryCategory.Arachnids, 1109, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("TerathanDrone", "Terathan Drone", "TerathanDrone", BestiaryCategory.Arachnids, 2115, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("TerathanWarrior", "Terathan Warrior", "TerathanWarrior", BestiaryCategory.Arachnids, 2116, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("TerathanMatriarch", "Terathan Matriarch", "TerathanMatriarch", BestiaryCategory.Arachnids, 2117, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Scorpion", "Scorpion", "Scorpion", BestiaryCategory.Arachnids, 2206, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("RuneBeetle", "Rune Beetle", "RuneBeetle", BestiaryCategory.Arachnids, 1165, 0.03); - BestiaryRegistry.Register("FireBeetle", "Fire Beetle", "FireBeetle", BestiaryCategory.Arachnids, 1260, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Beetle", "Giant Beetle", "Beetle", BestiaryCategory.Arachnids, 2210, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("IronBeetle", "Iron Beetle", "IronBeetle", BestiaryCategory.Arachnids, 2413, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("DeathwatchBeetle", "Deathwatch Beetle", "DeathwatchBeetle", BestiaryCategory.Arachnids, 2215, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("AntLion", "Ant Lion", "AntLion", BestiaryCategory.Arachnids, 2419, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("FireAnt", "Fire Ant", "FireAnt", BestiaryCategory.Arachnids, 1260, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("SkitteringHopper", "Skittering Hopper", "SkitteringHopper", BestiaryCategory.Arachnids, 2210, HunterBestiaryEngine.DefaultDropChance); - - - BestiaryRegistry.Register("Gazer", "Gazer", "Gazer", BestiaryCategory.Beasts, 1166, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("ElderGazer", "Elder Gazer", "ElderGazer", BestiaryCategory.Beasts, 1167, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("GazerLarva", "Gazer Larva", "GazerLarva", BestiaryCategory.Beasts, 1166, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("HellHound", "Hell Hound", "HellHound", BestiaryCategory.Beasts, 1258, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("HellCat", "Hell Cat", "HellCat", BestiaryCategory.Beasts, 1258, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("PredatorHellCat", "Predator Hell Cat", "PredatorHellCat", BestiaryCategory.Beasts, 1258, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Nightmare", "Nightmare", "Nightmare", BestiaryCategory.Beasts, 1109, 0.03); - BestiaryRegistry.Register("CuSidhe", "Cu Sidhe", "CuSidhe", BestiaryCategory.Beasts, 2212, 0.03); - BestiaryRegistry.Register("Kirin", "Ki-Rin", "Kirin", BestiaryCategory.Beasts, 1160, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Unicorn", "Unicorn", "Unicorn", BestiaryCategory.Beasts, 1150, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Hiryu", "Hiryu", "Hiryu", BestiaryCategory.Beasts, 1365, 0.03); - BestiaryRegistry.Register("Phoenix", "Phoenix", "Phoenix", BestiaryCategory.Beasts, 1260, 0.04); - BestiaryRegistry.Register("TimberWolf", "Timber Wolf", "TimberWolf", BestiaryCategory.Beasts, 2215, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("GreyWolf", "Grey Wolf", "GreyWolf", BestiaryCategory.Beasts, 2215, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("WhiteWolf", "White Wolf", "WhiteWolf", BestiaryCategory.Beasts, 1150, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("DireWolf", "Dire Wolf", "DireWolf", BestiaryCategory.Beasts, 2305, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Cougar", "Cougar", "Cougar", BestiaryCategory.Beasts, 2215, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Panther", "Panther", "Panther", BestiaryCategory.Beasts, 1109, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("SnowLeopard", "Snow Leopard", "SnowLeopard", BestiaryCategory.Beasts, 1150, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("GrizzlyBear", "Grizzly Bear", "GrizzlyBear", BestiaryCategory.Beasts, 2215, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("PolarBear", "Polar Bear", "PolarBear", BestiaryCategory.Beasts, 1150, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("BlackBear", "Black Bear", "BlackBear", BestiaryCategory.Beasts, 1109, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Gorilla", "Gorilla", "Gorilla", BestiaryCategory.Beasts, 2210, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Walrus", "Walrus", "Walrus", BestiaryCategory.Beasts, 2419, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("GiantToad", "Giant Toad", "GiantToad", BestiaryCategory.Beasts, 2210, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Bullfrog", "Bullfrog", "BullFrog", BestiaryCategory.Beasts, 2210, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("VampireBat", "Vampire Bat", "VampireBat", BestiaryCategory.Beasts, 1109, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Mongbat", "Mongbat", "Mongbat", BestiaryCategory.Beasts, 2215, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("DarkWisp", "Dark Wisp", "DarkWisp", BestiaryCategory.Beasts, 1109, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("ShadowWisp", "Shadow Wisp", "ShadowWisp", BestiaryCategory.Beasts, 1109, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("GiantIceWorm", "Giant Ice Worm", "GiantIceWorm", BestiaryCategory.Beasts, 1152, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Raptor", "Raptor", "Raptor", BestiaryCategory.Beasts, 2210, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Kepetch", "Kepetch", "Kepetch", BestiaryCategory.Beasts, 2215, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("KepetchAmbusher", "Kepetch Ambusher", "KepetchAmbusher", BestiaryCategory.Beasts, 2215, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("BloodWorm", "Blood Worm", "BloodWorm", BestiaryCategory.Beasts, 1258, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("OsseinRam", "Ossein Ram", "OsseinRam", BestiaryCategory.Beasts, 2101, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Harpy", "Harpy", "Harpy", BestiaryCategory.Beasts, 2215, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("StoneHarpy", "Stone Harpy", "StoneHarpy", BestiaryCategory.Beasts, 2413, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Corpser", "Corpser", "Corpser", BestiaryCategory.Beasts, 2215, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Reaper", "Reaper", "Reaper", BestiaryCategory.Beasts, 2215, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("BogThing", "Bog Thing", "BogThing", BestiaryCategory.Beasts, 2212, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Bogling", "Bogling", "Bogling", BestiaryCategory.Beasts, 2212, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("PlagueBeast", "Plague Beast", "PlagueBeast", BestiaryCategory.Beasts, 1267, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("PlagueBeastLord", "Plague Beast Lord", "PlagueBeastLord", BestiaryCategory.Beasts, 1267, 0.03); - BestiaryRegistry.Register("Quagmire", "Quagmire", "Quagmire", BestiaryCategory.Beasts, 2212, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Slime", "Slime", "Slime", BestiaryCategory.Beasts, 2210, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("FrostOoze", "Frost Ooze", "FrostOoze", BestiaryCategory.Beasts, 1152, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("SeaSerpent", "Sea Serpent", "SeaSerpent", BestiaryCategory.Beasts, 1165, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("DeepSeaSerpent", "Deep Sea Serpent", "DeepSeaSerpent", BestiaryCategory.Beasts, 1165, HunterBestiaryEngine.DefaultDropChance); - BestiaryRegistry.Register("Kraken", "Kraken", "Kraken", BestiaryCategory.Beasts, 1165, 0.03); - BestiaryRegistry.Register("Leviathan", "Leviathan", "Leviathan", BestiaryCategory.Beasts, 1165, 0.05); - - - BestiaryRegistry.Register("DemonKnight", "Demon Knight (Dark Father)", "DemonKnight", BestiaryCategory.Bosses, 1161, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("DarknightCreeper", "Darknight Creeper", "DarknightCreeper", BestiaryCategory.Bosses, 1161, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("FleshRenderer", "Flesh Renderer", "FleshRenderer", BestiaryCategory.Bosses, 1161, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("Impaler", "Impaler", "Impaler", BestiaryCategory.Bosses, 1161, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("ShadowKnight", "Shadow Knight", "ShadowKnight", BestiaryCategory.Bosses, 1161, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("AbysmalHorror", "Abysmal Horror", "AbysmalHorror", BestiaryCategory.Bosses, 1161, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("Harrower", "The Harrower", "Harrower", BestiaryCategory.Bosses, 1175, 0.10); - BestiaryRegistry.Register("SlasherOfVeils", "Slasher of Veils", "SlasherOfVeils", BestiaryCategory.Bosses, 1170, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("StygianDragon", "Stygian Dragon", "StygianDragon", BestiaryCategory.Bosses, 1172, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("Medusa", "Medusa", "Medusa", BestiaryCategory.Bosses, 1168, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("PrimevalLich", "Primeval Lich", "PrimevalLich", BestiaryCategory.Bosses, 1153, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("AbyssalInfernal", "Abyssal Infernal", "AbyssalInfernal", BestiaryCategory.Bosses, 1255, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("Rikktor", "Rikktor", "Rikktor", BestiaryCategory.Bosses, 1365, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("Mephitis", "Mephitis", "Mephitis", BestiaryCategory.Bosses, 1175, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("Semidar", "Semidar", "Semidar", BestiaryCategory.Bosses, 1258, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("Barracoon", "Barracoon the Piper", "Barracoon", BestiaryCategory.Bosses, 2307, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("Neira", "Neira the Necromancer", "Neira", BestiaryCategory.Bosses, 1175, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("LordOaks", "Lord Oaks", "LordOaks", BestiaryCategory.Bosses, 2215, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("Silvani", "Silvani", "Silvani", BestiaryCategory.Bosses, 1160, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("Ilhenir", "Ilhenir the Stained", "Ilhenir", BestiaryCategory.Bosses, 1267, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("Serado", "Serado the Scourge", "Serado", BestiaryCategory.Bosses, 1267, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("ChiefParoxysmus", "Chief Paroxysmus", "ChiefParoxysmus", BestiaryCategory.Bosses, 1267, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("DreadHorn", "Dread Horn", "DreadHorn", BestiaryCategory.Bosses, 1160, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("Travesty", "Travesty", "Travesty", BestiaryCategory.Bosses, 1153, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("ShimmeringEffusion", "Shimmering Effusion", "ShimmeringEffusion", BestiaryCategory.Bosses, 1152, HunterBestiaryEngine.BossDropChance); - BestiaryRegistry.Register("MonstrousInterredGrizzle", "Monstrous Interred Grizzle", "MonstrousInterredGrizzle", BestiaryCategory.Bosses, 1175, HunterBestiaryEngine.BossDropChance); - } -} diff --git a/Projects/UOContent/Engines/ExileHunterBestiary/Core/BestiaryRegistry.cs b/Projects/UOContent/Engines/ExileHunterBestiary/Core/BestiaryRegistry.cs deleted file mode 100644 index bb909d1b0..000000000 --- a/Projects/UOContent/Engines/ExileHunterBestiary/Core/BestiaryRegistry.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Collections.Generic; -using Server.Logging; - -namespace Server.Custom.ExileHunterBestiary; - -public static class BestiaryRegistry -{ - private static readonly ILogger _logger = LogFactory.GetLogger(typeof(BestiaryRegistry)); - - private static Dictionary _byId = new(StringComparer.OrdinalIgnoreCase); - private static Dictionary _byType = []; - private static Dictionary> _byCategory = []; - - - - private static readonly Dictionary _resolutionCache = []; - - public static int TotalCount => _byId.Count; - - public static IReadOnlyDictionary> ByCategory => _byCategory; - - public static void Initialize() - { - _byId = new Dictionary(StringComparer.OrdinalIgnoreCase); - _byType = new Dictionary(); - _byCategory = new Dictionary>(); - _resolutionCache.Clear(); - - foreach (var category in Enum.GetValues()) - { - _byCategory[category] = []; - } - - BestiaryData.RegisterAll(); - - _logger.Information( - "Initialized with {Count} creatures across {Categories} categories.", - _byId.Count, _byCategory.Count - ); - } - - public static void Register( - string id, string displayName, string typeName, - BestiaryCategory category, int bookHue = 0, - double dropChance = HunterBestiaryEngine.DefaultDropChance) - { - var type = AssemblyHandler.FindTypeByName(typeName); - - if (type == null) - { - _logger.Warning("Type '{TypeName}' not found - bestiary entry '{Id}' skipped.", typeName, id); - return; - } - - if (_byType.ContainsKey(type)) - { - _logger.Warning("Duplicate type '{TypeName}' - bestiary entry '{Id}' skipped.", typeName, id); - return; - } - - var entry = new HunterCreatureEntry(id, displayName, type, category, bookHue, dropChance); - _byId[id] = entry; - _byType[type] = entry; - _byCategory[category].Add(entry); - } - - public static bool TryGetById(string id, out HunterCreatureEntry entry) => - _byId.TryGetValue(id ?? "", out entry); - - - - - - public static HunterCreatureEntry FindEntry(Type creatureType) - { - if (creatureType == null) - { - return null; - } - - if (_resolutionCache.TryGetValue(creatureType, out var cached)) - { - return cached; - } - - HunterCreatureEntry found = null; - for (var current = creatureType; current != null && current != typeof(object); current = current.BaseType) - { - if (_byType.TryGetValue(current, out found)) - { - break; - } - } - - _resolutionCache[creatureType] = found; - return found; - } -} diff --git a/Projects/UOContent/Engines/ExileHunterBestiary/Core/DamageMode.cs b/Projects/UOContent/Engines/ExileHunterBestiary/Core/DamageMode.cs deleted file mode 100644 index 8458b63d2..000000000 --- a/Projects/UOContent/Engines/ExileHunterBestiary/Core/DamageMode.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Server.Custom.ExileHunterBestiary; - -public enum DamageMode -{ - Mark, - Multiplier -} diff --git a/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterBestiaryEngine.cs b/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterBestiaryEngine.cs deleted file mode 100644 index 2569af31a..000000000 --- a/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterBestiaryEngine.cs +++ /dev/null @@ -1,567 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text.Json; -using Server.Commands; -using Server.Custom.ExileHunterBestiary.Gumps; -using Server.Custom.ExileHunterBestiary.Hooks; -using Server.Logging; -using Server.Mobiles; - -namespace Server.Custom.ExileHunterBestiary; - -public static class HunterBestiaryEngine -{ - private static readonly ILogger _logger = - LogFactory.GetLogger(typeof(HunterBestiaryEngine)); - - private static readonly Dictionary _profiles = []; - - public const double DefaultDropChance = 0.02; - public const double BossDropChance = 0.05; - - public static bool Enabled { get; private set; } = true; - - public static double PerCreatureBonus { get; private set; } = 0.10; - - public static double CategoryMilestoneBonus { get; private set; } = 0.20; - - public static double TotalBonusCap { get; private set; } = 0.25; - - public static int PityThreshold { get; private set; } = 40; - - public static DamageMode DamageMode { get; private set; } = DamageMode.Mark; - - public static double GlobalDropChance { get; private set; } = -1.0; - - public static bool Debug { get; private set; } = true; - - public static void Configure() - { - Enabled = GetBool("exileBestiary.enabled", true); - PerCreatureBonus = GetDouble( - "exileBestiary.damage.perCreatureBonus", - 0.10 - ); - CategoryMilestoneBonus = GetDouble( - "exileBestiary.damage.categoryMilestoneBonus", - 0.20 - ); - TotalBonusCap = GetDouble( - "exileBestiary.damage.totalCap", - 0.25 - ); - PityThreshold = GetInt( - "exileBestiary.drop.pityThreshold", - 40 - ); - DamageMode = GetEnum( - "exileBestiary.damage.mode", - DamageMode.Mark - ); - GlobalDropChance = GetDouble( - "exileBestiary.drop.chance", - -1.0 - ); - Debug = GetBool( - "exileBestiary.debug", - true - ); - - _logger.Information( - "Config ({Source}): enabled={Enabled} mode={Mode} dropChance={DropChance} perCreature={PerCreature} milestone={Milestone} cap={Cap} pity={Pity} debug={Debug}", - _configSource, - Enabled, - DamageMode, - GlobalDropChance, - PerCreatureBonus, - CategoryMilestoneBonus, - TotalBonusCap, - PityThreshold, - Debug - ); - - BestiaryRegistry.Initialize(); - - if (!Enabled) - { - _logger.Warning( - "System disabled via configuration (exileBestiary.enabled = false)." - ); - - return; - } - - EventSink.WorldSave += OnWorldSave; - EventSink.WorldLoad += OnWorldLoad; - - EventSink.AggressiveAction += static args => - HunterCombatTracker.Note(args.Aggressor, args.Aggressed); - - HunterCombatTracker.StartSweeper(); - - ReloadProfiles(); - - CommandSystem.Register( - "Bestiary", - AccessLevel.Player, - OnBestiaryCommand - ); - - CommandSystem.Register( - "Bestiario", - AccessLevel.Player, - OnBestiaryCommand - ); - - CommandSystem.Register( - "BestiaryTest", - AccessLevel.GameMaster, - OnBestiaryTestCommand - ); - - CommandSystem.Register( - "BestiaryDiag", - AccessLevel.GameMaster, - OnBestiaryDiagCommand - ); - - if (DamageMode == DamageMode.Mark) - { - HunterMarkHook.Configure(); - } - } - - private static void ReloadProfiles() - { - _profiles.Clear(); - - foreach (var (serial, profile) in HunterProfileStore.Load()) - { - _profiles[serial] = profile; - } - } - - public static HunterProfile GetProfile(Mobile m) - { - if (m == null) - { - return null; - } - - if (!_profiles.TryGetValue(m.Serial, out var profile)) - { - profile = new HunterProfile(m.Serial); - _profiles[m.Serial] = profile; - } - - return profile; - } - - public static bool TryGetProfile( - Mobile m, - out HunterProfile profile - ) - { - if (m == null) - { - profile = null; - return false; - } - - return _profiles.TryGetValue(m.Serial, out profile); - } - - public static bool TryGetEffectiveBonus( - Mobile attacker, - BaseCreature victim, - out double bonus - ) - { - bonus = 0; - - if (!Enabled || attacker == null || victim == null) - { - return false; - } - - var entry = BestiaryRegistry.FindEntry(victim.GetType()); - - if (entry == null || - !_profiles.TryGetValue(attacker.Serial, out var profile)) - { - return false; - } - - if (profile.HasLearned(entry.Id)) - { - bonus += PerCreatureBonus; - } - - if (BestiaryRegistry.ByCategory.TryGetValue( - entry.Category, - out var categoryEntries - ) && - categoryEntries.Count > 0) - { - bonus += CategoryMilestoneBonus * - profile.GetCategoryMasteredCount(entry.Category) / - categoryEntries.Count; - } - - if (bonus > TotalBonusCap) - { - bonus = TotalBonusCap; - } - - return bonus > 0; - } - - [Usage("Bestiary")] - [Description("Opens the Hunter's Bestiary.")] - private static void OnBestiaryCommand(CommandEventArgs e) - { - if (e.Mobile is { Deleted: false } from) - { - HunterBestiaryGump.DisplayTo(from); - } - } - - private static void OnWorldSave() - { - HunterProfileStore.Save(_profiles); - } - - private static void OnWorldLoad() - { - ReloadProfiles(); - HunterCombatTracker.Clear(); - HunterDeathHook.ClearProcessedDeaths(); - } - - private static string _configSource = - "modernuo.json NOT FOUND - defaults in use"; - - private static readonly Dictionary _rawSettings = - LoadRawSettings(); - - private static Dictionary LoadRawSettings() - { - var dict = new Dictionary( - StringComparer.OrdinalIgnoreCase - ); - - var searched = new List(); - - try - { - string path = null; - - foreach (var candidate in ConfigCandidates(searched)) - { - if (File.Exists(candidate)) - { - path = candidate; - break; - } - } - - if (path == null) - { - _configSource = - "modernuo.json NOT FOUND; searched: " + - string.Join(" | ", searched); - - return dict; - } - - _configSource = path; - - using var doc = JsonDocument.Parse( - File.ReadAllText(path) - ); - - if (!doc.RootElement.TryGetProperty( - "settings", - out var settings - )) - { - return dict; - } - - foreach (var prop in settings.EnumerateObject()) - { - dict[prop.Name] = - prop.Value.ValueKind == JsonValueKind.String - ? prop.Value.GetString() - : prop.Value.ToString(); - } - } - catch (Exception ex) - { - _configSource = - "modernuo.json read error: " + ex.Message; - } - - return dict; - } - - private static IEnumerable ConfigCandidates( - List searched - ) - { - var roots = new List(); - - foreach (var root in new[] - { - Core.BaseDirectory, - Directory.GetCurrentDirectory(), - AppContext.BaseDirectory - }) - { - try - { - if (string.IsNullOrEmpty(root)) - { - continue; - } - - var full = Path.GetFullPath(root); - - if (!roots.Contains(full)) - { - roots.Add(full); - } - } - catch - { - // Ignore invalid path. - } - } - - var seenDirs = new HashSet( - StringComparer.OrdinalIgnoreCase - ); - - foreach (var root in roots) - { - var dir = root; - - while (!string.IsNullOrEmpty(dir) && - seenDirs.Add(dir)) - { - searched.Add(dir); - - foreach (var sub in new[] - { - "", - "Distribution", - "Configuration", - "Config", - "Configs", - "Server", - "Run" - }) - { - yield return Path.Combine( - dir, - sub, - "modernuo.json" - ); - } - - dir = Directory.GetParent(dir)?.FullName; - } - } - } - - public static bool GetBool( - string key, - bool defaultValue - ) => - _rawSettings.TryGetValue(key, out var s) && - bool.TryParse(s, out var value) - ? value - : defaultValue; - - public static int GetInt( - string key, - int defaultValue - ) => - _rawSettings.TryGetValue(key, out var s) && - int.TryParse( - s, - NumberStyles.Integer, - CultureInfo.InvariantCulture, - out var value - ) - ? value - : defaultValue; - - public static double GetDouble( - string key, - double defaultValue - ) - { - if (_rawSettings.TryGetValue(key, out var s)) - { - if (double.TryParse( - s, - NumberStyles.Float, - CultureInfo.InvariantCulture, - out var value - ) || - double.TryParse( - s, - NumberStyles.Float, - CultureInfo.CurrentCulture, - out value - )) - { - return value; - } - } - - return defaultValue; - } - - public static string GetString( - string key, - string defaultValue - ) => - _rawSettings.TryGetValue(key, out var value) - ? value - : defaultValue; - - public static TimeSpan GetTimeSpan( - string key, - TimeSpan defaultValue - ) => - _rawSettings.TryGetValue(key, out var s) && - TimeSpan.TryParse( - s, - CultureInfo.InvariantCulture, - out var value - ) - ? value - : defaultValue; - - public static TEnum GetEnum( - string key, - TEnum defaultValue - ) - where TEnum : struct => - _rawSettings.TryGetValue(key, out var s) && - Enum.TryParse( - s, - true, - out var value - ) - ? value - : defaultValue; - - public static void DebugLog(string message) - { - if (Debug) - { - _logger.Information("{Msg}", message); - } - } - - [Usage("BestiaryDiag")] - [Description("Shows combat-event counters for bestiary hook diagnostics.")] - private static void OnBestiaryDiagCommand(CommandEventArgs e) - { - if (e.Mobile is not { } from) - { - return; - } - - from.SendMessage( - $"AggressiveAction events seen: " + - $"{HunterCombatTracker.HitCount}; " + - $"tracked creatures: " + - $"{HunterCombatTracker.TrackedCount}." - ); - - from.SendMessage( - "Hit a mob 2-3 times in combat, then run this again. " + - "If the counter does not grow, no AggressiveAction event is received." - ); - } - - [Usage("BestiaryTest")] - [Description("Runs the treatise drop pipeline on the nearest registered creature.")] - private static void OnBestiaryTestCommand(CommandEventArgs e) - { - if (e.Mobile is not PlayerMobile from) - { - return; - } - - BaseCreature target = null; - - foreach (var bc in from.GetMobilesInRange(10)) - { - if (!bc.Deleted && - !bc.Summoned && - !bc.IsBonded && - bc.Hits > 0 && - BestiaryRegistry.FindEntry(bc.GetType()) != null) - { - target = bc; - break; - } - } - - if (target == null) - { - from.SendMessage( - "No registered creature within 10 tiles." - ); - - return; - } - - var entry = BestiaryRegistry.FindEntry(target.GetType()); - - var effectiveChance = GlobalDropChance >= 0 - ? GlobalDropChance - : entry?.DropChance ?? 0; - - from.SendMessage( - $"Running drop pipeline on {target.GetType().Name} " + - $"(chance {effectiveChance:P0})..." - ); - - var result = HunterDeathHook.OnCreatureDeath( - target, - from - ); - - HunterDeathHook.ClearProcessedDeath(target.Serial); - HunterCombatTracker.Forget(target.Serial); - - if (result == null) - { - from.SendMessage( - 0x44, - $"DROP EXECUTED at {effectiveChance:P0} chance. " + - "Check the corpse or the ground." - ); - } - else - { - from.SendMessage( - 0x22, - "NO DROP: " + result - ); - } - - DebugLog( - $"[Bestiary] TEST on {target.GetType().Name}: " + - $"{result ?? "DROP executed"}" - ); - } -} \ No newline at end of file diff --git a/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterCombatTracker.cs b/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterCombatTracker.cs deleted file mode 100644 index 4087ca9ea..000000000 --- a/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterCombatTracker.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System; -using System.Collections.Generic; -using Server.Custom.ExileHunterBestiary.Hooks; -using Server.Mobiles; - -namespace Server.Custom.ExileHunterBestiary; - -public static class HunterCombatTracker -{ - private static readonly TimeSpan Expiry = TimeSpan.FromSeconds(60); - - private sealed class Hit - { - public BaseCreature Creature; - public Mobile Master; - public Point3D Location; - public Map Map; - public DateTime When; - } - - private static readonly Dictionary _hits = []; - - private static bool _sweeping; - - public static int HitCount { get; private set; } - - public static int TrackedCount => _hits.Count; - - public static void Note(Mobile aggressor, Mobile aggressed) - { - if (aggressor == null || aggressed is not BaseCreature creature) - { - return; - } - - if (creature.Deleted || creature.Summoned || creature.IsBonded) - { - return; - } - - var master = aggressor is BaseCreature - { - Controlled: true, - ControlMaster: { } controlMaster - } - ? controlMaster - : aggressor; - - if (master is not PlayerMobile) - { - return; - } - - if (!_hits.TryGetValue(creature.Serial, out var hit)) - { - hit = new Hit(); - _hits[creature.Serial] = hit; - } - - hit.Creature = creature; - hit.Master = master; - hit.Location = creature.Location; - hit.Map = creature.Map; - hit.When = DateTime.UtcNow; - - HitCount++; - } - - public static Mobile GetLastHitter(BaseCreature creature) - { - if (creature == null) - { - return null; - } - - if (_hits.TryGetValue(creature.Serial, out var hit) && - DateTime.UtcNow - hit.When <= Expiry && - hit.Master is { Deleted: false }) - { - return hit.Master; - } - - return null; - } - - public static bool TryGetDeathLocation( - BaseCreature creature, - out Point3D location, - out Map map - ) - { - location = Point3D.Zero; - map = null; - - if (creature == null) - { - return false; - } - - if (_hits.TryGetValue(creature.Serial, out var hit) && - DateTime.UtcNow - hit.When <= Expiry && - hit.Map != null && - hit.Map != Map.Internal) - { - location = hit.Location; - map = hit.Map; - - return true; - } - - return false; - } - - public static void Forget(Serial serial) - { - if (!_sweeping) - { - _hits.Remove(serial); - } - } - - public static void Clear() - { - _hits.Clear(); - HitCount = 0; - } - - public static void StartSweeper() - { - Timer.DelayCall( - TimeSpan.FromSeconds(1), - TimeSpan.FromSeconds(1), - Sweep - ); - } - - private static void Sweep() - { - if (_hits.Count == 0 || _sweeping) - { - return; - } - - _sweeping = true; - - try - { - var now = DateTime.UtcNow; - List remove = null; - - foreach (var (serial, hit) in _hits) - { - var creature = hit.Creature; - - if (creature == null || - creature.Deleted || - now - hit.When > Expiry) - { - (remove ??= []).Add(serial); - continue; - } - - if (creature.Hits > 0) - { - continue; - } - - HunterDeathHook.OnCreatureDeath( - creature, - hit.Master - ); - - (remove ??= []).Add(serial); - } - - if (remove == null) - { - return; - } - - foreach (var serial in remove) - { - _hits.Remove(serial); - } - } - finally - { - _sweeping = false; - } - } -} \ No newline at end of file diff --git a/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterCreatureDeathEvent.cs b/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterCreatureDeathEvent.cs deleted file mode 100644 index 71f52ff8a..000000000 --- a/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterCreatureDeathEvent.cs +++ /dev/null @@ -1,22 +0,0 @@ -using ModernUO.CodeGeneratedEvents; -using Server.Custom.ExileHunterBestiary.Hooks; -using Server.Mobiles; - -namespace Server.Custom.ExileHunterBestiary; - -public static class HunterCreatureDeathEvent -{ - [OnEvent(nameof(CreatureEvents.CreatureDeathEvent))] - public static void OnCreatureDeath(BaseCreature creature) - { - if (creature == null) - { - return; - } - - HunterDeathHook.OnCreatureDeath( - creature, - creature.LastKiller - ); - } -} diff --git a/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterCreatureEntry.cs b/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterCreatureEntry.cs deleted file mode 100644 index c1e163b44..000000000 --- a/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterCreatureEntry.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Server.Custom.ExileHunterBestiary; - -public sealed record HunterCreatureEntry( - string Id, - string DisplayName, - Type CreatureType, - BestiaryCategory Category, - int BookHue = 0, - double DropChance = HunterBestiaryEngine.DefaultDropChance); diff --git a/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterProfile.cs b/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterProfile.cs deleted file mode 100644 index 9e551a6e5..000000000 --- a/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterProfile.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Server.Custom.ExileHunterBestiary; - -public sealed class HunterProfile -{ - private readonly Dictionary _killCounters = new(StringComparer.OrdinalIgnoreCase); - - public Serial MobileSerial { get; } - - public HashSet LearnedCreatures { get; } = new(StringComparer.OrdinalIgnoreCase); - - public int TotalMastered => LearnedCreatures.Count; - - public HunterProfile(Serial serial) => MobileSerial = serial; - - public bool HasLearned(string creatureId) => - !string.IsNullOrEmpty(creatureId) && LearnedCreatures.Contains(creatureId); - - - public bool Learn(string creatureId) - { - if (string.IsNullOrEmpty(creatureId) || !BestiaryRegistry.TryGetById(creatureId, out _)) - { - return false; - } - - return LearnedCreatures.Add(creatureId); - } - - public int GetCategoryMasteredCount(BestiaryCategory category) - { - var count = 0; - if (BestiaryRegistry.ByCategory.TryGetValue(category, out var list)) - { - foreach (var entry in list) - { - if (LearnedCreatures.Contains(entry.Id)) - { - count++; - } - } - } - - return count; - } - - - - - - public int GetKillCount(string creatureId) => - _killCounters.TryGetValue(creatureId ?? "", out var kills) ? kills : 0; - - public void SetKillCount(string creatureId, int kills) - { - if (!string.IsNullOrEmpty(creatureId) && kills > 0) - { - _killCounters[creatureId] = kills; - } - } - - - public bool RegisterKill(string creatureId, int pityThreshold) - { - if (string.IsNullOrEmpty(creatureId) || pityThreshold <= 0) - { - return false; - } - - var kills = GetKillCount(creatureId) + 1; - if (kills < pityThreshold) - { - _killCounters[creatureId] = kills; - return false; - } - - _killCounters.Remove(creatureId); - return true; - } - - public IEnumerable> KillCounters => _killCounters; -} diff --git a/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterProfileStore.cs b/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterProfileStore.cs deleted file mode 100644 index 612c6a1c6..000000000 --- a/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterProfileStore.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Xml; -using Server.Logging; - -namespace Server.Custom.ExileHunterBestiary; - -public static class HunterProfileStore -{ - private static readonly ILogger _logger = LogFactory.GetLogger(typeof(HunterProfileStore)); - - private static string SavePath => Path.Combine(Core.BaseDirectory, "Data", "ExileHunterProfiles.xml"); - - public static void Save(IReadOnlyDictionary profiles) - { - try - { - var dir = Path.GetDirectoryName(SavePath); - if (!string.IsNullOrEmpty(dir)) - { - Directory.CreateDirectory(dir); - } - - var doc = new XmlDocument(); - var root = doc.CreateElement("HunterProfiles"); - doc.AppendChild(root); - - foreach (var (serial, profile) in profiles) - { - var hasCounters = false; - foreach (var (_, kills) in profile.KillCounters) - { - if (kills > 0) - { - hasCounters = true; - break; - } - } - - if (profile.TotalMastered == 0 && !hasCounters) - { - continue; - } - - var pElem = doc.CreateElement("Profile"); - pElem.SetAttribute("serial", serial.Value.ToString()); - - foreach (var id in profile.LearnedCreatures) - { - var cElem = doc.CreateElement("Creature"); - cElem.SetAttribute("id", id); - - var kills = profile.GetKillCount(id); - if (kills > 0) - { - cElem.SetAttribute("kills", kills.ToString()); - } - - pElem.AppendChild(cElem); - } - - foreach (var (id, kills) in profile.KillCounters) - { - if (kills <= 0 || profile.HasLearned(id)) - { - continue; - } - - var cElem = doc.CreateElement("Creature"); - cElem.SetAttribute("id", id); - cElem.SetAttribute("kills", kills.ToString()); - pElem.AppendChild(cElem); - } - - root.AppendChild(pElem); - } - - doc.Save(SavePath); - } - catch (Exception ex) - { - _logger.Error(ex, "Error saving bestiary profiles."); - } - } - - public static Dictionary Load() - { - var profiles = new Dictionary(); - - try - { - if (!File.Exists(SavePath)) - { - return profiles; - } - - var doc = new XmlDocument(); - doc.Load(SavePath); - - var list = doc.SelectNodes("//HunterProfiles/Profile"); - if (list == null) - { - return profiles; - } - - foreach (XmlElement pElem in list) - { - if (!int.TryParse(pElem.GetAttribute("serial"), out var sVal)) - { - continue; - } - - var profile = new HunterProfile((Serial)(uint)sVal); - var hasData = false; - - var cList = pElem.SelectNodes("Creature"); - if (cList != null) - { - foreach (XmlElement cElem in cList) - { - var id = cElem.GetAttribute("id"); - - - if (!BestiaryRegistry.TryGetById(id, out _)) - { - continue; - } - - profile.LearnedCreatures.Add(id); - - if (int.TryParse(cElem.GetAttribute("kills"), out var kills) && kills > 0) - { - profile.SetKillCount(id, kills); - } - - hasData = true; - } - } - - if (hasData) - { - profiles[profile.MobileSerial] = profile; - } - } - - _logger.Information("Loaded {Count} bestiary profiles.", profiles.Count); - } - catch (Exception ex) - { - _logger.Error(ex, "Error loading bestiary profiles."); - } - - return profiles; - } -} diff --git a/Projects/UOContent/Engines/ExileHunterBestiary/Gumps/HunterBestiaryGump.cs b/Projects/UOContent/Engines/ExileHunterBestiary/Gumps/HunterBestiaryGump.cs deleted file mode 100644 index 4119e79d6..000000000 --- a/Projects/UOContent/Engines/ExileHunterBestiary/Gumps/HunterBestiaryGump.cs +++ /dev/null @@ -1,238 +0,0 @@ -using System; -using System.Collections.Generic; -using Server.Gumps; -using Server.Network; - -namespace Server.Custom.ExileHunterBestiary.Gumps; - -public sealed class HunterBestiaryGump : DynamicGump -{ - private const int EntriesPerPage = 10; - - private const int ButtonClose = 0; - private const int ButtonNext = 10; - private const int ButtonPrev = 11; - private const int ButtonToggleFilter = 12; - private const int ButtonSearch = 13; - private const int TextSearch = 1; - private const int ButtonCategoryBase = 100; - private const int ButtonCategoryLimit = 150; - - private readonly Mobile _player; - private readonly BestiaryCategory _category; - private readonly int _page; - private readonly string _search; - private readonly bool _hideMastered; - - public override bool Singleton => true; - - private HunterBestiaryGump(Mobile player, BestiaryCategory category, int page, string search, bool hideMastered) - : base(100, 40) - { - _player = player; - _category = category; - _page = page; - _search = search ?? ""; - _hideMastered = hideMastered; - } - - public static void DisplayTo( - Mobile from, BestiaryCategory category = BestiaryCategory.Undead, - int page = 0, string search = "", bool hideMastered = false) - { - if (from is not { Deleted: false } || !HunterBestiaryEngine.Enabled) - { - return; - } - - from.SendGump(new HunterBestiaryGump(from, category, page, search, hideMastered)); - } - - protected override void BuildLayout(ref DynamicGumpBuilder builder) - { - var profile = HunterBestiaryEngine.TryGetProfile(_player, out var p) ? p : null; - var totalMastered = profile?.TotalMastered ?? 0; - var grandTotal = BestiaryRegistry.TotalCount; - var grandPct = grandTotal > 0 ? totalMastered * 100.0 / grandTotal : 0; - - builder.AddPage(); - builder.AddBackground(0, 0, 660, 560, 9270); - builder.AddAlphaRegion(10, 10, 640, 540); - builder.AddBackground(14, 14, 632, 532, 9200); - builder.AddAlphaRegion(18, 18, 624, 524); - - - builder.AddItem(25, 20, 0x2252, 1153); - builder.AddHtml(85, 20, 310, 24, "The Grand Hunter's Bestiary"); - builder.AddHtml(85, 46, 310, 20, "Studied Treatises & Anatomical Lore"); - builder.AddHtml( - 400, 24, 240, 22, - $"Overall: {totalMastered}/{grandTotal} ({grandPct:F0}%)" - ); - builder.AddHtml( - 400, 46, 240, 20, - $"Cap +{HunterBestiaryEngine.TotalBonusCap:P0}" - + $" - {(HunterBestiaryEngine.DamageMode == DamageMode.Mark ? "Hunter's Mark" : "Multiplier")}" - ); - - builder.AddImageTiled(20, 72, 620, 2, 9354); - builder.AddImageTiled(195, 78, 2, 428, 9354); - - - var catY = 82; - foreach (var cat in Enum.GetValues()) - { - var selected = cat == _category; - var catTotal = BestiaryRegistry.ByCategory[cat].Count; - var catMastered = profile?.GetCategoryMasteredCount(cat) ?? 0; - - builder.AddButton(24, catY + 4, selected ? 4006 : 4005, 4007, ButtonCategoryBase + (int)cat); - - var catColor = selected ? "#00FFCC" : catMastered == catTotal && catTotal > 0 ? "#69F0AE" : "#ECEFF1"; - builder.AddHtml( - 60, catY + 5, 130, 20, - $"{cat} ({catMastered}/{catTotal})" - ); - - catY += 34; - } - - - builder.AddButton(24, catY + 8, _hideMastered ? 4006 : 4005, 4007, ButtonToggleFilter); - builder.AddHtml( - 60, catY + 9, 130, 20, - $"Hide mastered" - ); - - - var catList = BestiaryRegistry.ByCategory[_category]; - var catLearned = profile?.GetCategoryMasteredCount(_category) ?? 0; - var catPct = catList.Count > 0 ? catLearned * 100.0 / catList.Count : 0; - - builder.AddHtml(205, 82, 430, 24, $"{_category} Studies"); - builder.AddHtml( - 205, 106, 430, 20, - $"Mastery: {catLearned} / {catList.Count} ({catPct:F0}%)" - + $" - milestone bonus up to +{HunterBestiaryEngine.CategoryMilestoneBonus:P0}" - ); - - - builder.AddTextEntry(205, 132, 300, 22, 0x480, TextSearch, _search); - builder.AddButton(512, 132, 4005, 4007, ButtonSearch); - builder.AddHtml(548, 134, 80, 20, "Search"); - - builder.AddImageTiled(200, 160, 435, 2, 9354); - - - var filtered = new List(catList.Count); - foreach (var entry in catList) - { - var learned = profile != null && profile.HasLearned(entry.Id); - if (_hideMastered && learned) - { - continue; - } - - if (_search.Length > 0 && - entry.DisplayName.Contains(_search, StringComparison.OrdinalIgnoreCase) == false) - { - continue; - } - - filtered.Add(entry); - } - - filtered.Sort(static (a, b) => string.CompareOrdinal(a.DisplayName, b.DisplayName)); - - var maxPages = Math.Max(1, (filtered.Count + EntriesPerPage - 1) / EntriesPerPage); - var page = Math.Clamp(_page, 0, maxPages - 1); - var start = page * EntriesPerPage; - var end = Math.Min(start + EntriesPerPage, filtered.Count); - - var categoryFraction = catList.Count > 0 ? (double)catLearned / catList.Count : 0; - - var entryY = 170; - for (var i = start; i < end; i++) - { - var creature = filtered[i]; - var learned = profile != null && profile.HasLearned(creature.Id); - - - var bonus = (learned ? HunterBestiaryEngine.PerCreatureBonus : 0.0) - + HunterBestiaryEngine.CategoryMilestoneBonus * categoryFraction; - bonus = Math.Min(bonus, HunterBestiaryEngine.TotalBonusCap); - - builder.AddBackground(200, entryY, 435, 32, learned ? 9200 : 9300); - builder.AddAlphaRegion(202, entryY + 2, 431, 28); - builder.AddImage(207, entryY + 5, learned ? 4017 : 4020); - - var nameColor = learned ? "#FFFFFF" : "#B0BEC5"; - builder.AddHtml(250, entryY + 6, 185, 20, $"{creature.DisplayName}"); - builder.AddHtml( - 440, entryY + 6, 190, 20, - learned - ? $"Mastered - +{HunterBestiaryEngine.PerCreatureBonus:P0} (+{bonus:P0} total)" - : $"Pending Treatise - +{bonus:P0} milestone" - ); - - entryY += 36; - } - - - builder.AddImageTiled(20, 520, 620, 2, 9354); - - builder.AddButton(30, 528, 4017, 4019, ButtonClose); - builder.AddHtml(65, 530, 50, 20, "Close"); - - if (page > 0) - { - builder.AddButton(200, 528, 4014, 4016, ButtonPrev); - builder.AddHtml(236, 530, 60, 20, "Previous"); - } - - builder.AddHtml(360, 530, 120, 20, $"Page {page + 1} of {maxPages}"); - - if (page < maxPages - 1) - { - builder.AddButton(560, 528, 4005, 4007, ButtonNext); - builder.AddHtml(590, 530, 40, 20, "Next"); - } - } - - public override void OnResponse(NetState sender, in RelayInfo info) - { - if (sender.Mobile is not { } from) - { - return; - } - - var search = info.GetTextEntry(TextSearch)?.Trim() ?? _search; - - switch (info.ButtonID) - { - case ButtonClose: - return; - case ButtonNext: - DisplayTo(from, _category, _page + 1, search, _hideMastered); - return; - case ButtonPrev: - DisplayTo(from, _category, Math.Max(0, _page - 1), search, _hideMastered); - return; - case ButtonToggleFilter: - DisplayTo(from, _category, 0, search, !_hideMastered); - return; - case ButtonSearch: - DisplayTo(from, _category, 0, search, _hideMastered); - return; - default: - { - if (info.ButtonID is >= ButtonCategoryBase and < ButtonCategoryLimit) - { - DisplayTo(from, (BestiaryCategory)(info.ButtonID - ButtonCategoryBase), 0, search, _hideMastered); - } - - return; - } - } - } -} diff --git a/Projects/UOContent/Engines/ExileHunterBestiary/Hooks/HunterDamageHook.cs b/Projects/UOContent/Engines/ExileHunterBestiary/Hooks/HunterDamageHook.cs deleted file mode 100644 index 0c016ce92..000000000 --- a/Projects/UOContent/Engines/ExileHunterBestiary/Hooks/HunterDamageHook.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using Server.Mobiles; - -namespace Server.Custom.ExileHunterBestiary.Hooks; - -public static class HunterDamageHook -{ - public static long ApplyCount { get; private set; } - - public static void Apply(BaseCreature victim, Mobile attacker, ref int damage) - { - ApplyCount++; - - if (victim == null || attacker == null || damage <= 0) - { - return; - } - - var master = attacker is BaseCreature { Controlled: true, ControlMaster: { } controlMaster } - ? controlMaster - : attacker; - - if (master is PlayerMobile && - HunterBestiaryEngine.TryGetEffectiveBonus(master, victim, out var bonus)) - { - var scaled = (int)Math.Round(damage * (1.0 + bonus)); - damage = Math.Max(damage + 1, scaled); - } - - if (victim.Hits > 0 && victim.Hits <= damage) - { - var creature = victim; - var killer = attacker; - - Timer.DelayCall(TimeSpan.FromMilliseconds(100), () => - { - if (creature.Deleted || creature.Hits > 0) - { - return; - } - - HunterDeathHook.OnCreatureDeath(creature, killer); - }); - } - } -} \ No newline at end of file diff --git a/Projects/UOContent/Engines/ExileHunterBestiary/Hooks/HunterDeathHook.cs b/Projects/UOContent/Engines/ExileHunterBestiary/Hooks/HunterDeathHook.cs deleted file mode 100644 index 35f861ed0..000000000 --- a/Projects/UOContent/Engines/ExileHunterBestiary/Hooks/HunterDeathHook.cs +++ /dev/null @@ -1,245 +0,0 @@ -using System; -using System.Collections.Generic; -using Server.Custom.ExileHunterBestiary.Items; -using Server.Items; -using Server.Mobiles; - -namespace Server.Custom.ExileHunterBestiary.Hooks; - -public static class HunterDeathHook -{ - private static readonly HashSet _processedDeaths = []; - - public static string OnCreatureDeath(BaseCreature creature, Mobile killer) - { - if (!HunterBestiaryEngine.Enabled) - { - return "system disabled"; - } - - if (creature == null) - { - return "creature is null"; - } - - if (creature.Summoned || creature.IsBonded) - { - return "creature filtered (summoned/bonded)"; - } - - var entry = BestiaryRegistry.FindEntry(creature.GetType()); - - if (entry == null) - { - return $"NOT REGISTERED: {creature.GetType().FullName}"; - } - - if (killer == null || killer.Deleted) - { - killer = creature.LastKiller; - } - - if (killer == null || killer.Deleted) - { - killer = HunterCombatTracker.GetLastHitter(creature); - } - - if (killer == null || killer.Deleted) - { - return $"{entry.Id}: no killer resolved"; - } - - var master = killer; - - if (killer is BaseCreature killerCreature) - { - master = killerCreature.GetMaster() ?? killer; - } - - if (master is not PlayerMobile player) - { - HunterCombatTracker.Forget(creature.Serial); - return $"{entry.Id}: killer is not a player"; - } - - if (!_processedDeaths.Add(creature.Serial)) - { - return $"{entry.Id}: already processed"; - } - - var location = creature.Location; - var map = creature.Map; - - if (map == null || map == Map.Internal) - { - HunterCombatTracker.TryGetDeathLocation( - creature, - out location, - out map - ); - } - - HunterCombatTracker.Forget(creature.Serial); - - var profile = HunterBestiaryEngine.GetProfile(player); - - if (profile == null) - { - _processedDeaths.Remove(creature.Serial); - return $"{entry.Id}: profile is null"; - } - - if (profile.HasLearned(entry.Id)) - { - return $"{entry.Id}: already learned by this player"; - } - - var chance = HunterBestiaryEngine.GlobalDropChance >= 0 - ? HunterBestiaryEngine.GlobalDropChance - : entry.DropChance; - - chance = Math.Clamp(chance, 0.0, 1.0); - - var pityDrop = profile.RegisterKill( - entry.Id, - HunterBestiaryEngine.PityThreshold - ); - - if (!pityDrop && Utility.RandomDouble() >= chance) - { - return $"{entry.Id}: rolled above drop chance ({chance:P0})"; - } - - if (map == null || map == Map.Internal) - { - _processedDeaths.Remove(creature.Serial); - return $"{entry.Id}: death location unavailable"; - } - - var treatise = new HunterTreatise(entry.Id); - - var corpse = FindCorpseAt(location, map, creature); - - if (corpse is { Deleted: false }) - { - corpse.DropItem(treatise); - return null; - } - - DropIntoCorpseLater( - treatise, - entry.Id, - creature.Serial, - location, - map, - 0 - ); - - return null; - } - - private static void DropIntoCorpseLater( - HunterTreatise treatise, - string creatureId, - Serial creatureSerial, - Point3D location, - Map map, - int attempt - ) - { - Timer.DelayCall( - TimeSpan.FromMilliseconds(100), - () => - { - if (treatise.Deleted) - { - return; - } - - var corpse = FindCorpseAt(location, map, null); - - if (corpse is { Deleted: false }) - { - corpse.DropItem(treatise); - return; - } - - if (attempt < 10) - { - DropIntoCorpseLater( - treatise, - creatureId, - creatureSerial, - location, - map, - attempt + 1 - ); - - return; - } - - treatise.Delete(); - _processedDeaths.Remove(creatureSerial); - } - ); - } - - private static Corpse FindCorpseAt( - Point3D location, - Map map, - BaseCreature owner - ) - { - if (map == null || map == Map.Internal) - { - return null; - } - - Corpse nearest = null; - var nearestDistance = int.MaxValue; - - foreach (var item in World.Items.Values) - { - if (item is not Corpse corpse || - corpse.Deleted || - corpse.Map != map) - { - continue; - } - - var corpseLocation = corpse.Location; - var distance = Math.Max( - Math.Abs(corpseLocation.X - location.X), - Math.Abs(corpseLocation.Y - location.Y) - ); - - if (distance > 3) - { - continue; - } - - if (owner != null && corpse.Owner == owner) - { - return corpse; - } - - if (distance < nearestDistance) - { - nearest = corpse; - nearestDistance = distance; - } - } - - return nearest; - } - - public static void ClearProcessedDeath(Serial serial) - { - _processedDeaths.Remove(serial); - } - - public static void ClearProcessedDeaths() - { - _processedDeaths.Clear(); - } -} \ No newline at end of file diff --git a/Projects/UOContent/Engines/ExileHunterBestiary/Hooks/HunterMarkHook.cs b/Projects/UOContent/Engines/ExileHunterBestiary/Hooks/HunterMarkHook.cs deleted file mode 100644 index 6cd570852..000000000 --- a/Projects/UOContent/Engines/ExileHunterBestiary/Hooks/HunterMarkHook.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using System.Collections.Generic; -using Server.Logging; -using Server.Mobiles; - -namespace Server.Custom.ExileHunterBestiary.Hooks; - -public static class HunterMarkHook -{ - private static readonly ILogger _logger = LogFactory.GetLogger(typeof(HunterMarkHook)); - - private sealed class MarkState - { - public BaseCreature Creature; - public Mobile Master; - public DateTime ExpiresAt; - public DateTime NextTickAt; - } - - private static readonly Dictionary _marks = []; - - - - private static bool _applyingMark; - - public static TimeSpan MarkDuration { get; private set; } = TimeSpan.FromSeconds(10); - public static TimeSpan TickInterval { get; private set; } = TimeSpan.FromSeconds(2); - public static double TickPercentOfMaxHits { get; private set; } = 0.01; - - public static void Configure() - { - MarkDuration = HunterBestiaryEngine.GetTimeSpan("exileBestiary.damage.markDuration", TimeSpan.FromSeconds(10)); - TickInterval = HunterBestiaryEngine.GetTimeSpan("exileBestiary.damage.markTickSeconds", TimeSpan.FromSeconds(2)); - TickPercentOfMaxHits = HunterBestiaryEngine.GetDouble("exileBestiary.damage.markTickPercent", 0.01); - - EventSink.AggressiveAction += OnAggressiveAction; - Timer.DelayCall(TickInterval, TickInterval, Tick); - - _logger.Information( - "Hunter's Mark enabled: {Duration}s mark, {Interval}s ticks, {Percent:P0} of max hits per tick.", - MarkDuration, TickInterval, TickPercentOfMaxHits - ); - } - - private static void OnAggressiveAction(AggressiveActionEventArgs args) - { - if (_applyingMark || - !HunterBestiaryEngine.Enabled || - args.Aggressed is not BaseCreature creature || - creature is { Deleted: true } or { Summoned: true } or { IsBonded: true }) - { - return; - } - - var entry = BestiaryRegistry.FindEntry(creature.GetType()); - if (entry == null) - { - return; - } - - var master = args.Aggressor is BaseCreature { Controlled: true, ControlMaster: { } controlMaster } - ? controlMaster - : args.Aggressor; - - if (master is not PlayerMobile || - !HunterBestiaryEngine.TryGetEffectiveBonus(master, creature, out var bonus)) - { - return; - } - - var now = DateTime.UtcNow; - if (!_marks.TryGetValue(creature.Serial, out var state)) - { - state = new MarkState { Creature = creature }; - _marks[creature.Serial] = state; - } - - state.Master = master; - state.ExpiresAt = now + MarkDuration; - if (state.NextTickAt < now) - { - state.NextTickAt = now + TickInterval; - } - } - - private static void Tick() - { - if (_marks.Count == 0) - { - return; - } - - var now = DateTime.UtcNow; - List expired = null; - - foreach (var (serial, state) in _marks) - { - var creature = state.Creature; - var master = state.Master; - - if (creature.Deleted || creature.Hits <= 0 || - master is not { Deleted: false } || - now >= state.ExpiresAt || - !master.InRange(creature.Location, 16)) - { - (expired ??= []).Add(serial); - continue; - } - - if (now < state.NextTickAt) - { - continue; - } - - state.NextTickAt = now + TickInterval; - - if (!HunterBestiaryEngine.TryGetEffectiveBonus(master, creature, out var bonus)) - { - (expired ??= []).Add(serial); - continue; - } - - - - var intensity = HunterBestiaryEngine.TotalBonusCap > 0 - ? bonus / HunterBestiaryEngine.TotalBonusCap - : 0; - - var amount = (int)Math.Max(1, Math.Round(creature.HitsMax * TickPercentOfMaxHits * intensity)); - - _applyingMark = true; - try - { - creature.Damage(amount, master); - } - finally - { - _applyingMark = false; - } - } - - if (expired != null) - { - foreach (var serial in expired) - { - _marks.Remove(serial); - } - } - } -} diff --git a/Projects/UOContent/Engines/ExileHunterBestiary/INSTALLATION_GUIDE.txt b/Projects/UOContent/Engines/ExileHunterBestiary/INSTALLATION_GUIDE.txt deleted file mode 100644 index 79fc96d96..000000000 --- a/Projects/UOContent/Engines/ExileHunterBestiary/INSTALLATION_GUIDE.txt +++ /dev/null @@ -1,278 +0,0 @@ -=============================================== - EXILE HUNTER'S BESTIARY - ModernUO EDITION - Final stable version - Author: Imagine - Refactored: Expa -=============================================== - -INSTALL - - 1. Copy the ExileHunterBestiary folder to: - - Projects/UOContent/Custom/ExileHunterBestiary/ - - 2. Make sure the generated death-event file exists: - - HunterCreatureDeathEvent.cs - - 3. Build the project: - - dotnet build - - 4. Start the server. - - IMPORTANT: - - The final version does not require any changes to BaseCreature.cs. - - Do not add: - - EventSink.CreatureDeath - - a custom Kill() override - - a custom OnDeath() override - - a BaseCreature damage patch - - Do not keep this legacy file: - - BaseCreature.ExileHunterBestiary.cs - - Do not keep old patch files such as: - - PATCH_BaseCreature.txt - - -ACTIVE DEATH HANDLER - - Creature deaths are handled by the generated ModernUO event: - - BaseCreature.CreatureDeathEvent - - The handler is located in: - - HunterCreatureDeathEvent.cs - - This is not an EventSink event. Do not use: - - EventSink.CreatureDeath - - -COMBAT TRACKING - - HunterCombatTracker receives player combat activity through: - - EventSink.AggressiveAction - - The tracker stores: - - - the last player attacker; - - the player's pet owner when applicable; - - the creature serial; - - the creature location; - - the creature map; - - the time of the last hit. - - The stored location is used because ModernUO can remove the creature - before the generated death event is handled. - - -CORPSE DROPS - - The treatise is placed into the creature's corpse. - - If the corpse is not available immediately, the system retries for a - short period. - - The system does not intentionally place the treatise on the ground. - - If no corpse can be found after the retry period, the drop is canceled. - - -CONFIGURATION - - Add these values inside the "settings" object in modernuo.json: - - "exileBestiary.enabled": "True", - "exileBestiary.debug": "False", - "exileBestiary.damage.mode": "Mark", - "exileBestiary.damage.perCreatureBonus": "0.10", - "exileBestiary.damage.categoryMilestoneBonus": "0.20", - "exileBestiary.damage.totalCap": "0.25", - "exileBestiary.damage.markDuration": "00:00:10", - "exileBestiary.damage.markTickSeconds": "00:00:02", - "exileBestiary.damage.markTickPercent": "0.01", - "exileBestiary.drop.chance": "-1.0", - "exileBestiary.drop.pityThreshold": "40" - - Drop chance values: - - -1.0 Use the creature's registered chance - 1.0 100% chance - 0.5 50% chance - 0.02 2% chance - 0.0 No random drops - - Example for guaranteed drops: - - "exileBestiary.drop.chance": "1.0" - - The configuration must be inside: - - { - "settings": { - ... - } - } - - -DAMAGE MODE - - The recommended mode is: - - "exileBestiary.damage.mode": "Mark" - - Mark mode is patch-free and does not require BaseCreature.cs changes. - - The mark system uses: - - - EventSink.AggressiveAction - - a timer - - public ModernUO APIs - - It applies periodic bonus damage while the hunter remains near - the marked creature. - - -COMMANDS - - [Bestiary - - Opens the Hunter's Bestiary. - - [Bestiario - - Alias for [Bestiary. - - [BestiaryTest - - Game Master command that runs the treatise pipeline against the nearest - registered creature. - - The command uses the configured chance and is intended for testing. - - [BestiaryDiag - - Game Master diagnostic command that displays combat-tracker counters. - - -STARTUP CHECK - - The server should report a bestiary initialization message similar to: - - Initialized with N creatures across 8 categories. - - When Mark mode is enabled, it should also report: - - Hunter's Mark enabled ... - - -TROUBLESHOOTING - - No treatise appears: - - 1. Confirm the creature exists in BestiaryData.cs. - 2. Confirm the creature type was registered at startup. - 3. Confirm exileBestiary.enabled is True. - 4. Confirm the player actually damaged the creature. - 5. Confirm EventSink.AggressiveAction is receiving events. - 6. Confirm the generated event file exists. - 7. Confirm no old BaseCreature patch file remains. - 8. Set exileBestiary.drop.chance to 1.0. - 9. Temporarily set exileBestiary.debug to True. - - No combat events are tracked: - - Run: - - [BestiaryDiag - - The AggressiveAction counter should increase after attacking a creature. - - Log says "killer not resolved": - - The system did not receive LastKiller and did not find a valid player - in HunterCombatTracker. - - Log says "killer is not a player": - - The kill was credited to an NPC, an uncontrolled creature, or another - non-player source. - - Log says "creature not registered": - - The creature type is not present in BestiaryData.cs or could not be - resolved by AssemblyHandler.FindTypeByName. - - Treatise is already learned: - - A player receives only one treatise for a creature that has already - been mastered. Additional drops are blocked for that player. - - Treatise is not visible: - - Open the creature corpse and check its contents. The treatise is not - automatically placed into the player's backpack. - - -SAVE DATA - - Profiles are stored at: - - Data/ExileHunterProfiles.xml - - Saved data includes: - - - learned creature IDs; - - pity counters; - - player serials. - - Invalid creature IDs are ignored during loading. - - -FILES TO KEEP - - BestiaryCategory.cs - BestiaryData.cs - BestiaryRegistry.cs - DamageMode.cs - HunterBestiaryEngine.cs - HunterCombatTracker.cs - HunterCreatureDeathEvent.cs - HunterCreatureEntry.cs - HunterProfile.cs - HunterProfileStore.cs - HunterBestiaryGump.cs - HunterDamageHook.cs - HunterDeathHook.cs - HunterMarkHook.cs - HunterBestiary.cs - HunterTreatise.cs - - -LEGACY FILES TO REMOVE - - BaseCreature.ExileHunterBestiary.cs - PATCH_BaseCreature.txt - - Also remove any legacy bestiary file that overrides: - - BaseCreature.Kill() - BaseCreature.OnBeforeDeath() - BaseCreature.OnDeath() - - -PRODUCTION SETTINGS - - After confirming the system works, use: - - "exileBestiary.debug": "False" - - Keep debug enabled only while diagnosing the drop pipeline. \ No newline at end of file diff --git a/Projects/UOContent/Engines/ExileHunterBestiary/Items/HunterBestiary.cs b/Projects/UOContent/Engines/ExileHunterBestiary/Items/HunterBestiary.cs deleted file mode 100644 index 7c796393e..000000000 --- a/Projects/UOContent/Engines/ExileHunterBestiary/Items/HunterBestiary.cs +++ /dev/null @@ -1,66 +0,0 @@ -using ModernUO.Serialization; -using Server.Custom.ExileHunterBestiary.Gumps; -using Server.Network; - -namespace Server.Custom.ExileHunterBestiary.Items; - -[SerializationGenerator(0)] -public partial class HunterBestiary : Item -{ - public const int DefaultBestiaryHue = 1153; - - [Constructible] - public HunterBestiary() : base(0x2252) - { - Name = "The Grand Hunter's Bestiary"; - Hue = DefaultBestiaryHue; - Weight = 2.0; - LootType = LootType.Blessed; - } - - public HunterBestiary(Serial serial) : base(serial) - { - } - - public override void OnDoubleClick(Mobile from) - { - if (!IsChildOf(from.Backpack)) - { - from.SendLocalizedMessage(1042001); - return; - } - - HunterBestiaryGump.DisplayTo(from); - } - - public override void GetProperties(IPropertyList list) - { - base.GetProperties(list); - - list.Add(1070722, $"{"Grand Grimoire of Monster Lore"}"); - list.Add(1070722, $"{"Tracks mastered treatises and combat bonuses"}"); - list.Add(1070722, $"{"Double-click to open Bestiary"}"); - } - - public override void SendPropertiesTo(NetState ns) - { - if (ns?.Mobile is not { } from) - { - return; - } - - var list = new ObjectPropertyList(this); - GetProperties(list); - - var mastered = HunterBestiaryEngine.TryGetProfile(from, out var profile) ? profile.TotalMastered : 0; - var total = BestiaryRegistry.TotalCount; - - list.Add( - 1070722, - $"{$"Mastery Progress: {mastered} / {total} Creatures"}" - ); - - list.Terminate(); - ns.Send(list.Buffer); - } -} diff --git a/Projects/UOContent/Engines/ExileHunterBestiary/Items/HunterTreatise.cs b/Projects/UOContent/Engines/ExileHunterBestiary/Items/HunterTreatise.cs deleted file mode 100644 index 38a49d06b..000000000 --- a/Projects/UOContent/Engines/ExileHunterBestiary/Items/HunterTreatise.cs +++ /dev/null @@ -1,130 +0,0 @@ -using ModernUO.Serialization; -using Server.Network; - -namespace Server.Custom.ExileHunterBestiary.Items; - -[SerializationGenerator(0)] -public partial class HunterTreatise : Item -{ - [SerializableField(0)] - [InvalidateProperties] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private string _creatureId; - - public HunterCreatureEntry Entry => - BestiaryRegistry.TryGetById(_creatureId, out var entry) ? entry : null; - - [Constructible] - public HunterTreatise(string creatureId) : base(0x1F5F) - { - _creatureId = creatureId; - Weight = 1.0; - LootType = LootType.Regular; - UpdateAppearance(false); - } - - [Constructible] - public HunterTreatise() : this("Lich") - { - } - - public HunterTreatise(Serial serial) : base(serial) - { - } - - [AfterDeserialization] - private void AfterDeserialization() => UpdateAppearance(false); - - private void UpdateAppearance(bool invalidate) - { - ItemID = 0x1F5F; - - if (Entry is { } entry) - { - Name = $"Hunter's Treatise: {entry.DisplayName}"; - Hue = entry.BookHue != 0 ? entry.BookHue : 2101; - } - else - { - Name = $"Hunter's Treatise: {_creatureId}"; - Hue = 2101; - } - - if (invalidate) - { - InvalidateProperties(); - } - } - - public override void OnDoubleClick(Mobile from) - { - if (!IsChildOf(from.Backpack)) - { - from.SendLocalizedMessage(1042001); - return; - } - - var profile = HunterBestiaryEngine.GetProfile(from); - if (profile == null) - { - return; - } - - var displayName = Entry?.DisplayName ?? _creatureId ?? "Unknown"; - - if (profile.HasLearned(_creatureId)) - { - from.SendMessage(0x22, $"You have already mastered the study of {displayName}. Keep this treatise to trade with other hunters!"); - from.PlaySound(0x5C); - return; - } - - if (profile.Learn(_creatureId)) - { - from.SendMessage( - 0x3F, - $"You thoroughly study the treatise and permanently unlock +{HunterBestiaryEngine.PerCreatureBonus:P0} damage against {displayName}!" - ); - from.FixedParticles(0x373A, 10, 15, 5012, EffectLayer.Waist); - from.PlaySound(0x249); - Consume(); - } - } - - public override void GetProperties(IPropertyList list) - { - base.GetProperties(list); - - var creatureName = Entry?.DisplayName ?? _creatureId ?? "Unknown"; - var category = Entry?.Category.ToString() ?? "Unknown"; - - list.Add( - 1070722, - $"{$"Hunter's Study: +{HunterBestiaryEngine.PerCreatureBonus:P0} vs {creatureName}"}" - ); - list.Add(1070722, $"{$"Category: {category}"}"); - list.Add(1070722, $"{"Double-click to permanently master"}"); - } - - public override void SendPropertiesTo(NetState ns) - { - if (ns?.Mobile is not { } from) - { - return; - } - - var list = new ObjectPropertyList(this); - GetProperties(list); - - var learned = HunterBestiaryEngine.TryGetProfile(from, out var profile) && profile.HasLearned(_creatureId); - list.Add( - 1070722, - learned - ? $"{"[Already Learned]"}" - : $"{"[Not Learned Yet]"}" - ); - - list.Terminate(); - ns.Send(list.Buffer); - } -}