From 646d9d777d67e71dbb9e266e3c38b18d7e33487e Mon Sep 17 00:00:00 2001 From: WarrentyExpired Date: Wed, 19 Aug 2026 20:00:44 -0400 Subject: [PATCH] #W# Added random dungeon traps. Added DungeonTrapRegion. --- Data/Config/Regions.xml | 2 +- Data/Spawns/chests.map | 18 ++ Data/Spawns/dungeons.map | 32 +++ Data/Spawns/traps.map | 33 +++ Scripts/Engines/DungeonTraps.cs | 301 +++++++++++++++++++++++++++ Scripts/Regions/DungeonTrapRegion.cs | 66 ++++++ Scripts/Settings.cs | 2 +- 7 files changed, 452 insertions(+), 2 deletions(-) create mode 100644 Scripts/Engines/DungeonTraps.cs create mode 100644 Scripts/Regions/DungeonTrapRegion.cs diff --git a/Data/Config/Regions.xml b/Data/Config/Regions.xml index d8c6d07..d91cfb0 100644 --- a/Data/Config/Regions.xml +++ b/Data/Config/Regions.xml @@ -158,7 +158,7 @@ - + diff --git a/Data/Spawns/chests.map b/Data/Spawns/chests.map index a8c9b56..75f3279 100644 --- a/Data/Spawns/chests.map +++ b/Data/Spawns/chests.map @@ -162,3 +162,21 @@ *|treasurechestlevel1||||||3523|90|30|1|5|10|-1|4|1|1|0|0|0|0|0 *|treasurechestlevel1||||||3528|124|10|1|5|10|-1|4|1|1|0|0|0|0|0 # Level 2 +*|treasurechestlevel3||||||3642|135|0|1|5|10|-1|0|1|1|0|0|0|0|0 +*|treasurechestlevel2||||||3659|140|0|1|5|10|-1|0|1|1|0|0|0|0|0 +*|treasurechestlevel2||||||3671|165|-5|1|5|10|-1|0|1|1|0|0|0|0|0 +*|treasurechestlevel2||||||3741|88|4|1|5|10|-1|0|1|1|0|0|0|0|0 +*|treasurechestlevel2||||||3737|161|1|1|5|10|-1|0|1|1|0|0|0|0|0 +*|treasurechestlevel3||||||3692|163|3|1|5|10|-1|0|1|1|0|0|0|0|0 +*|treasurechestlevel2||||||3683|106|0|1|5|10|-1|0|1|1|0|0|0|0|0 +*|treasurechestlevel3||||||3667|158|3|1|5|10|-1|0|1|1|0|0|0|0|0 +*|treasurechestlevel2||||||3649|120|0|1|5|10|-1|0|1|1|0|0|0|0|0 +*|treasurechestlevel2||||||3642|94|0|1|5|10|-1|0|1|1|0|0|0|0|0 +*|treasurechestlevel2||||||3740|70|10|1|5|10|-1|4|1|1|0|0|0|0|0 +*|treasurechestlevel2||||||3726|76|10|1|5|10|-1|4|1|1|0|0|0|0|0 +*|treasurechestlevel1:treasurechestlevel2|treasurechestlevel2:treasurechestlevel3|||||3728|56|9|1|5|10|-1|2|1|1|1|0|0|0|0 +*|treasurechestlevel2||||||3694|114|0|1|5|10|-1|0|1|1|0|0|0|0|0 +*|treasurechestlevel2||||||42|114|0|1|5|10|-1|0|1|1|0|0|0|0|0 +*|treasurechestlevel2||||||3643|93|0|1|5|10|-1|0|1|1|0|0|0|0|0 +*|treasurechestlevel2||||||3640|108|1|1|5|10|-1|0|1|1|0|0|0|0|0 +# Level 3 diff --git a/Data/Spawns/dungeons.map b/Data/Spawns/dungeons.map index c1f4ebd..76e202a 100644 --- a/Data/Spawns/dungeons.map +++ b/Data/Spawns/dungeons.map @@ -289,3 +289,35 @@ *|harpy||||||3528|107|-20|1|5|10|-1|4|1|1|0|0|0|0|0 *|harpy||||||3522|106|-20|1|5|10|-1|4|1|1|0|0|0|0|0 # Level 2 +*|seaserpent||||||3637|85|-5|1|5|10|-1|6|1|1|0|0|0|0|0 +*|troll:earthelemental||||||3694|64|0|1|5|10|-1|8|1|2|0|0|0|0|0 +*|troll:earthelemental:dullcopperelemental:shadowIronElemental||||||3705|100|0|1|5|10|-1|8|1|2|0|0|0|0|0 +*|waterelemental||||||3687|112|0|1|5|10|-1|4|1|1|0|0|0|0|0 +*|waterelemental||||||3654|119|0|1|5|10|-1|4|1|1|0|0|0|0|0 +*|troll:earthelemental:dullcopperelemental:shadowIronElemental||||||3670|108|0|1|5|10|-1|8|1|1|0|0|0|0|0 +*|airelemental:earthelemental:dullcopperelemental:shadowIronElemental||||||119|81|0|1|5|10|-1|8|1|2|0|0|0|0|0 +*|troll:earthelemental||||||3709|76|0|1|5|10|-1|8|1|2|0|0|0|0|0 +*|earthelemental||||||3728|58|9|1|5|10|-1|4|1|1|0|0|0|0|0 +*|earthelemental||||||3726|74|10|1|5|10|-1|4|1|1|0|0|0|0|0 +*|gazerlarva||||||3728|65|10|1|5|10|-1|4|1|1|0|0|0|0|0 +*|dullcopperelemental||||||3740|69|10|1|5|10|-1|4|1|1|0|0|0|0|0 +*|troll:earthelemental||||||3690|82|0|1|5|10|-1|8|1|2|0|0|0|0|0 +*|troll:earthelemental||||||3669|74|0|1|5|10|-1|8|1|1|0|0|0|0|0 +*|waterelemental||||||3645|93|0|1|5|10|-1|4|1|1|0|0|0|0|0 +*|earthelemental||||||3662|88|0|1|5|10|-1|4|1|1|0|0|0|0|0 +*|troll||||||3645|104|0|1|5|10|-1|4|1|1|0|0|0|0|0 +*|troll:earthelemental:dullcopperelemental:shadowIronElemental||||||3714|113|0|1|5|10|-1|8|1|1|0|0|0|0|0 +*|airelemental:waterelemental||||||3738|90|0|1|5|10|-1|4|1|1|0|0|0|0|0 +*|kraken||||||3716|127|-5|1|5|10|-1|4|1|1|0|0|0|0|0 +*|airelemental||||||3739|136|2|1|5|10|-1|4|1|1|0|0|0|0|0 +*|airelemental:earthelemental:dullcopperelemental:shadowIronElemental||||||3740|163|0|1|5|10|-1|8|1|2|0|0|0|0|0 +*|airelemental:earthelemental:dullcopperelemental:shadowIronElemental||||||3720|163|0|1|5|10|-1|8|1|2|0|0|0|0|0 +*|waterelemental||||||3715|135|0|1|5|10|-1|4|1|1|0|0|0|0|0 +*|fireelemental||||||3691|162|3|1|5|10|-1|4|1|1|0|0|0|0|0 +*|airelemental:earthelemental:dullcopperelemental:shadowIronElemental||||||3682|147|3|1|5|10|-1|8|1|2|0|0|0|0|0 +*|waterelemental||||||3677|130|0|1|5|10|-1|4|1|1|0|0|0|0|0 +*|waterelemental||||||3655|136|0|1|5|10|-1|4|1|1|0|0|0|0|0 +*|seaserpent||||||3663|126|-5|1|5|10|-1|4|1|1|0|0|0|0|0 +*|airelemental:earthelemental:dullcopperelemental:shadowIronElemental||||||3643|152|0|1|5|10|-1|8|1|2|0|0|0|0|0 +*|fireelemental||||||3666|162|2|1|5|10|-1|4|1|1|0|0|0|0|0 +# Level 3 diff --git a/Data/Spawns/traps.map b/Data/Spawns/traps.map index ab45461..1b217d8 100644 --- a/Data/Spawns/traps.map +++ b/Data/Spawns/traps.map @@ -326,3 +326,36 @@ *|mushroomtrap||||||3614|66|-30|1|1|1|-1|8|1|2|0|0|0|0|0 *|mushroomtrap||||||3518|158|0|1|1|1|-1|8|1|2|0|0|0|0|0 # Level 2 +*|mushroomtrap||||||3708|146|2|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3715|162|1|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3740|163|0|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3740|150|1|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3740|133|2|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3725|94|0|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3715|112|0|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3703|1|0|1|1|1|-1|8|99|2|0|0|0|0|0 +*|mushroomtrap||||||3685|113|0|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3676|109|0|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3666|111|0|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3647|94|0|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3662|88|0|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3686|83|0|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3706|83|0|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3737|66|10|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3726|72|10|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3708|69|0|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3698|66|1|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3671|70|0|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3686|60|0|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||49|68|0|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3640|63|0|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3717|138|1|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3668|163|1|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3664|161|3|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3644|152|0|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3655|139|0|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3677|133|1|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3683|152|3|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3691|162|3|1|1|1|-1|8|1|2|0|0|0|0|0 +*|mushroomtrap||||||3728|62|9|1|1|1|-1|8|1|2|0|0|0|0|0 +# Level 3 diff --git a/Scripts/Engines/DungeonTraps.cs b/Scripts/Engines/DungeonTraps.cs new file mode 100644 index 0000000..ad85e5e --- /dev/null +++ b/Scripts/Engines/DungeonTraps.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using Server; +using Server.Mobiles; +using Server.Items; + +namespace Server.Regions +{ + public static class CustomDungeonTraps + { + private static Dictionary m_KarmaTimers = new Dictionary(); + + public static bool SavingThrow(PlayerMobile pm, string saveType) + { + int saveThrow = 0; + string area = ""; + + if (saveType == "Magic") + { + area = "magical resistance"; + saveThrow = (int)((pm.Int + pm.Skills[SkillName.MagicResist].Value + pm.EnergyResistance) / 4); + } + else if (saveType == "Physical") + { + area = "physical endurance"; + saveThrow = (int)((pm.Str + pm.PhysicalResistance) / 3); + } + else if (saveType == "Agility") + { + area = "quick reflexes"; + saveThrow = pm.Dex; + } + else if (saveType == "Poison") + { + area = "poison resistance"; + saveThrow = (int)((pm.Str + pm.Skills[SkillName.Poisoning].Value + pm.PoisonResistance) / 4); + } + + // Cap the maximum possible save chance at 60% + if (saveThrow > 60) + { + saveThrow = 60; + } + + // If their save score is higher than a random 1-100 roll, they succeed! + if (saveThrow >= Utility.RandomMinMax(1, 100)) + { + pm.LocalOverheadMessage(Server.Network.MessageType.Emote, 0x3B2, false, "You triggered a trap, but with your " + area + "...you avoid the brunt of it."); + pm.PlaySound(pm.Female ? 778 : 1049); + return true; + } + + return false; + } + + public static void TriggerRandomTrap(PlayerMobile pm) + { + // Increase this number as you add more cases below! + int roll = Utility.Random(6); + + switch (roll) + { + case 0: ApplyPotionBreakTrap(pm); break; + case 1: ApplySpikeTrap(pm); break; + case 2: ApplyGasTrap(pm); break; + case 3: ApplyTripWireTrap(pm); break; + case 4: ApplyTransmutationTrap(pm); break; + case 5: ApplyKarmaTrap(pm); break; + } + } + + public static void ApplyPotionBreakTrap(PlayerMobile pm) + { + if (SavingThrow(pm, "Agility")) + return; + + pm.PlaySound(0x38D); + + if (pm.Backpack != null) + { + List potions = pm.Backpack.FindItemsByType(); + + if (potions.Count > 0) + { + BasePotion targetPotion = potions[Utility.Random(potions.Count)]; + targetPotion.Consume(1); + + pm.FixedParticles(0x374A, 10, 15, 5021, EffectLayer.Waist); + pm.SendMessage(33, "A concealed tripwire snaps! A flask in your backpack breaks!"); + return; + } + } + + pm.Damage(Utility.RandomMinMax(10, 20)); + pm.SendMessage(33, "You trip over a hidden wire and take a nasty spill!"); + } + + public static void ApplySpikeTrap(PlayerMobile pm) + { + if (SavingThrow(pm, "Physical")) + return; + pm.PlaySound(0x22C); + pm.FixedParticles(0x3728, 10, 10, 5021, EffectLayer.Waist); + pm.Damage(Utility.RandomMinMax(15, 30)); + pm.SendMessage(33, "Spikes spring from the floor!"); + } + + public static void ApplyGasTrap(PlayerMobile pm) + { + if (SavingThrow(pm, "Poison")) + return; + pm.PlaySound(0x231); + pm.FixedParticles(0x3709, 10, 30, 5052, EffectLayer.Waist); + pm.ApplyPoison(pm, Poison.Regular); + pm.SendMessage(33, "You trigger a noxious gas vent!"); + } + public static void ApplyTripWireTrap(PlayerMobile pm) + { + if (SavingThrow(pm, "Agility")) + return; + + pm.PlaySound(pm.Female ? 812 : 1086); + + Item dropped = GetRandomEquippedItem(pm); + + if (dropped != null && dropped.LootType != LootType.Blessed) + { + // Drops it right at their feet + dropped.MoveToWorld(pm.Location, pm.Map); + pm.LocalOverheadMessage(Server.Network.MessageType.Emote, 33, true, "You tripped over a wire and dropped one of your equipped items!"); + } + else + { + // Fallback if they are completely naked or only have blessed gear + pm.Damage(Utility.RandomMinMax(5, 10)); + pm.SendMessage(33, "You trip over a hidden wire and take a nasty spill!"); + } + } + + private static Item GetRandomEquippedItem(PlayerMobile pm) + { + List wornItems = new List(); + + Layer[] validLayers = { Layer.OneHanded, Layer.TwoHanded, Layer.Helm, Layer.Neck, Layer.Arms, Layer.Gloves, Layer.Ring, Layer.Bracelet, Layer.Shoes }; + + foreach (Layer layer in validLayers) + { + Item item = pm.FindItemOnLayer(layer); + + if (item != null && item.LootType != LootType.Blessed) + { + wornItems.Add(item); + } + } + + if (wornItems.Count == 0) + { + return null; + } + return wornItems[Utility.Random(wornItems.Count)]; + } + + public static void ApplyTransmutationTrap(PlayerMobile pm) + { + if (SavingThrow(pm, "Magic")) + return; + + if (pm.Backpack != null) + { + int goldAmount = pm.Backpack.GetAmount(typeof(Gold)); + + if (goldAmount > 0) + { + // Calculate a 10% to 25% tax on their gold + int goldToDestroy = (int)(goldAmount * Utility.RandomMinMax(10, 25) / 100.0); + + if (goldToDestroy > 0 && pm.Backpack.ConsumeTotal(typeof(Gold), goldToDestroy)) + { + pm.PlaySound(0x1E1); + pm.FixedParticles(0x374A, 10, 15, 5028, EffectLayer.Waist); + pm.LocalOverheadMessage(Server.Network.MessageType.Emote, 33, true, "A magical trap triggers, turning a chunk of your gold into useless dust!"); + return; + } + } + } + + // Fallback if they have zero gold + pm.PlaySound(0x1DF); + pm.Mana -= 20; + pm.SendMessage(33, "A magical trap drains your mana!"); + } + + public static void ApplyKarmaTrap(PlayerMobile pm) + { + if (SavingThrow(pm, "Magic")) + return; + + if (pm.Karma == 0) + { + pm.PlaySound(0x1DF); + pm.Mana -= 20; + pm.SendMessage(33, "A dark energy washes over you, draining your mana!"); + return; + } + + KarmaCurseToken token = pm.Backpack.FindItemByType(); + + if (token == null) + { + pm.Karma *= -1; + pm.PlaySound(0x1E1); + pm.FixedParticles(0x374A, 10, 15, 5028, EffectLayer.Waist); + pm.LocalOverheadMessage(Server.Network.MessageType.Emote, 33, true, "A mind-warping trap triggers, completely inverting your morality!"); + + token = new KarmaCurseToken(pm, TimeSpan.FromMinutes(3)); + pm.Backpack.DropItem(token); + } + else + { + token.ExtendCurse(TimeSpan.FromMinutes(5)); + pm.SendMessage(33, "The mind-warping curse intensifies, extending its duration!"); + } + } + } + public class KarmaCurseToken : Item + { + private Mobile m_Owner; + private DateTime m_Expires; + private Timer m_Timer; + + public KarmaCurseToken(Mobile owner, TimeSpan duration) : base(0x1EA7) + { + Movable = false; + Visible = false; + Weight = 0.0; + m_Owner = owner; + m_Expires = DateTime.UtcNow + duration; + + StartTimer(); + } + + public KarmaCurseToken(Serial serial) : base(serial) + { + } + + private void StartTimer() + { + if (m_Timer != null) + m_Timer.Stop(); + + TimeSpan delay = m_Expires - DateTime.UtcNow; + + if (delay <= TimeSpan.Zero) + { + RemoveCurse(); + } + else + { + m_Timer = Timer.DelayCall(delay, RemoveCurse); + } + } + + public void ExtendCurse(TimeSpan duration) + { + m_Expires = DateTime.UtcNow + duration; + StartTimer(); + } + + private void RemoveCurse() + { + if (m_Owner != null && !m_Owner.Deleted) + { + m_Owner.Karma *= -1; + m_Owner.SendMessage(0x3B2, "The morality-warping curse fades, and your true nature returns."); + } + + if (m_Timer != null) + m_Timer.Stop(); + + Delete(); + } + + public override void Serialize(GenericWriter writer) + { + base.Serialize(writer); + writer.Write((int)0); + writer.Write(m_Owner); + writer.Write(m_Expires); + } + + public override void Deserialize(GenericReader reader) + { + base.Deserialize(reader); + int version = reader.ReadInt(); + m_Owner = reader.ReadMobile(); + m_Expires = reader.ReadDateTime(); + + StartTimer(); + } + } +} diff --git a/Scripts/Regions/DungeonTrapRegion.cs b/Scripts/Regions/DungeonTrapRegion.cs new file mode 100644 index 0000000..e6ce63e --- /dev/null +++ b/Scripts/Regions/DungeonTrapRegion.cs @@ -0,0 +1,66 @@ +using System; +using Server; +using Server.Mobiles; +using System.Xml; + +namespace Server.Regions +{ + public class DungeonTrapRegion : DungeonRegion + { + public DungeonTrapRegion(XmlElement xml, Map map, Region parent) : base(xml, map, parent) + { + } + + public override void OnLocationChanged(Mobile m, Point3D oldLocation) + { + base.OnLocationChanged(m, oldLocation); + + if (m is PlayerMobile pm && pm.Alive && pm.AccessLevel == AccessLevel.Player && m.Location != oldLocation) + { + // We are using 2% (0.02) base chance per step. + if (Utility.RandomDouble() < 0.02) + { + EvaluateTrapSequence(pm); + } + } + } + + private void EvaluateTrapSequence(PlayerMobile pm) + { + // --- TIER 1: PASSIVE SEARCHING (Detect Hidden) --- + if (pm.Skills[SkillName.DetectHidden].Value >= 5.0) + { + if (pm.CheckSkill(SkillName.DetectHidden, 0.0, 125.0)) + { + pm.PlaySound(pm.Female ? 778 : 1049); // Plays a female/male gasp or sigh + //pm.LocalOverheadMessage(Server.Network.MessageType.Emote, 0x3B2, false, "You notice a hidden floor mechanism and carefully step around it."); + return; + } + } + + // --- TIER 2: TRAP AVOIDANCE (Remove Trap) --- + if (pm.Skills[SkillName.RemoveTrap].Value >= 5.0) + { + if (pm.CheckSkill(SkillName.RemoveTrap, 0.0, 125.0)) + { + pm.PlaySound(0x241); + //pm.LocalOverheadMessage(Server.Network.MessageType.Emote, 0x3B2, false, "Your experience with traps allows you to safely bypass a hidden trigger."); + return; + } + } + + // --- TIER 3: LUCK --- + // A simple scale: 1000 Luck = 10% chance to avoid. 2000 Luck = 20% chance. + double luckChance = pm.Luck / 10000.0; + if (Utility.RandomDouble() < luckChance) + { + pm.PlaySound(0x241); + //pm.LocalOverheadMessage(Server.Network.MessageType.Emote, 0x3B2, false, "With luck on your side, the trap mechanism fails to trigger."); + return; + } + + // If they failed all three avoidance checks the trap is sprung. + CustomDungeonTraps.TriggerRandomTrap(pm); + } + } +} diff --git a/Scripts/Settings.cs b/Scripts/Settings.cs index 126eadf..3bc6659 100644 --- a/Scripts/Settings.cs +++ b/Scripts/Settings.cs @@ -19,7 +19,7 @@ namespace Server public static int S_MaxGold = 1000; // maximum is 10,000 public static bool S_CanStealWhileHoldingThings = true; public static bool S_MonstersSurprise = true; - public static bool S_DungeonTraps = true; + public static bool S_DungeonTraps = false; public static double S_FarmSpawnTimer = 15.0; public static int S_HarvestRange = 1; }