From c43f54361163c6f93b7def21a2bbfe26761c64bc Mon Sep 17 00:00:00 2001 From: WarrentyExpired Date: Wed, 14 Oct 2026 15:19:54 -0400 Subject: [PATCH] #W# Traps: Added Dungeon traps that are step based but can be avoided with skill/armor/stats. New settings in Settings.cs --- .gitignore | 3 +- .../UOContent/Engines/RegionDungeonTraps.cs | 193 ++++++++++++++++++ Projects/UOContent/Gumps/BuildWorld.cs | 10 +- Projects/UOContent/Misc/ValidSettings.cs | 9 + .../Regions/Custom/GreetingDungeonRegion.cs | 82 +++++++- Projects/UOContent/Settings.cs | 3 + 6 files changed, 294 insertions(+), 6 deletions(-) create mode 100644 Projects/UOContent/Engines/RegionDungeonTraps.cs diff --git a/.gitignore b/.gitignore index ab40f2c9d..107e1a905 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,8 @@ /Distribution/Logs /Distribution/Archives /Distribution/Backups -/Distribution/Saves +/Distribution/Saves/* +/Distribution/Saves.* /Distribution/docs /Distribution/Client /Distribution/Data/Spawns/Exported/* diff --git a/Projects/UOContent/Engines/RegionDungeonTraps.cs b/Projects/UOContent/Engines/RegionDungeonTraps.cs new file mode 100644 index 000000000..04433c960 --- /dev/null +++ b/Projects/UOContent/Engines/RegionDungeonTraps.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using Server; +using Server.Items; +using Server.Mobiles; +using Server.Spells; + +namespace Server.Engines.DungeonTraps +{ + public enum TrapDamageType + { + Physical, + Magical + } + + public abstract class BaseRegionTrap + { + public abstract string Name { get; } + public abstract TrapDamageType DamageType { get; } + public abstract int MinDamage { get; } + public abstract int MaxDamage { get; } + + // The master method called when a region step roll succeeds + public virtual void Trigger(PlayerMobile pm) + { + // 1. Remove Trap Check (Absolute Disarm) + double removeTrap = pm.Skills[SkillName.RemoveTrap].Value; + if (removeTrap > 0 && removeTrap >= Utility.RandomDouble() * 100.0) + { + pm.SendMessage(0x35, $"You hear a click and instinctively jam the {Name} mechanism before it fires."); + pm.PlaySound(0x3A4); // Click sound + return; + } + + // 2. Evasion Check: (Dex + Int) * 0.25 + double evasionChance = (pm.Dex + pm.Int) * 0.25; + if (evasionChance >= Utility.RandomDouble() * 100.0) + { + pm.SendMessage(0x59, $"A {Name} triggers, but your quick reflexes allow you to dodge it!"); + PlayTrapEffect(pm); // Trap goes off, but misses + return; + } + + // 3. Mitigation Check (AR for Physical, Magic Resist for Magical) + PlayTrapEffect(pm); + bool mitigated = false; + + if (DamageType == TrapDamageType.Physical) + { + // Using PhysicalResistance (or VirtualArmor for pre-AoS) + if (pm.PhysicalResistance >= Utility.RandomDouble() * 100.0) + { + mitigated = true; + } + } + else // Magical + { + if (pm.Skills[SkillName.MagicResist].Value >= Utility.RandomDouble() * 100.0) + { + mitigated = true; + } + } + + // 4. Resolve Impact + if (mitigated) + { + pm.SendMessage(0x22, $"Your defenses absorb the {Name}, but your equipment takes a beating!"); + DamageRandomArmor(pm); + } + else + { + pm.SendMessage(0x22, $"You are struck by a {Name}!"); + ApplyDamage(pm); + } + } + + protected virtual void DamageRandomArmor(PlayerMobile pm) + { + // Gather all equippable armor the player is wearing + List wornArmor = new List(); + foreach (Item item in pm.Items) + { + if (item is BaseArmor armor && armor.HitPoints > 0) + { + wornArmor.Add(armor); + } + } + + if (wornArmor.Count > 0) + { + // Pick a random piece of armor + BaseArmor hitArmor = wornArmor[Utility.Random(wornArmor.Count)]; + + // Calculate 5% to 10% of current hit points + int damage = Utility.RandomMinMax((int)(hitArmor.HitPoints * 0.05), (int)(hitArmor.HitPoints * 0.10)); + + // Ensure it does at least 1 damage, but doesn't drop below 0 + damage = Math.Max(1, damage); + hitArmor.HitPoints = Math.Max(0, hitArmor.HitPoints - damage); + } + else + { + // If they are totally naked but magically mitigated it, they take a small stamina penalty instead + pm.Stam -= 10; + } + } + + public abstract void PlayTrapEffect(PlayerMobile pm); + + public virtual void ApplyDamage(PlayerMobile pm) + { + int damage = Utility.RandomMinMax(MinDamage, MaxDamage); + AOS.Damage(pm, damage, DamageType == TrapDamageType.Physical ? 100 : 0, DamageType == TrapDamageType.Physical ? 0 : 100, 0, 0, 0); + } + } + + // ========================================== + // SPECIFIC TRAP DEFINITIONS + // ========================================== + + public class TripwireTrap : BaseRegionTrap + { + public override string Name => "tripwire"; + public override TrapDamageType DamageType => TrapDamageType.Physical; + public override int MinDamage => 15; + public override int MaxDamage => 30; + + public override void PlayTrapEffect(PlayerMobile pm) + { + pm.PlaySound(0x3E4); // Snapping sound + Effects.SendLocationEffect(pm.Location, pm.Map, 0x373A, 10, 10); // Dust cloud + } + + public override void ApplyDamage(PlayerMobile pm) + { + base.ApplyDamage(pm); + pm.Stam -= 15; // Tripwires knock the wind out of you + } + } + + public class ExplosionTrap : BaseRegionTrap + { + public override string Name => "hidden explosive"; + public override TrapDamageType DamageType => TrapDamageType.Physical; + public override int MinDamage => 20; + public override int MaxDamage => 40; + + public override void PlayTrapEffect(PlayerMobile pm) + { + pm.PlaySound(0x207); // Explosion sound + Effects.SendLocationEffect(pm.Location, pm.Map, 0x36BD, 10, 10); // Big explosion visual + } + } + + public class FlameTrap : BaseRegionTrap + { + public override string Name => "flame jet"; + public override TrapDamageType DamageType => TrapDamageType.Magical; + public override int MinDamage => 20; + public override int MaxDamage => 40; + + public override void PlayTrapEffect(PlayerMobile pm) + { + pm.PlaySound(0x226); // Fire sound + Effects.SendLocationParticles(EffectItem.Create(pm.Location, pm.Map, EffectItem.DefaultDuration), 0x3709, 10, 30, 5052); // Flame column + } + } + + public class GasTrap : BaseRegionTrap + { + public override string Name => "poison gas cloud"; + public override TrapDamageType DamageType => TrapDamageType.Magical; + public override int MinDamage => 5; + public override int MaxDamage => 10; // Low initial damage, but applies poison + + public override void PlayTrapEffect(PlayerMobile pm) + { + pm.PlaySound(0x230); // Hissing gas sound + Effects.SendLocationEffect(pm.Location, pm.Map, 0x3728, 10, 10, 63); // Green smoke + } + + public override void ApplyDamage(PlayerMobile pm) + { + base.ApplyDamage(pm); + + // Gas traps apply a poison effect if the player takes damage + if (!pm.Poisoned) + { + pm.ApplyPoison(pm, Poison.Regular); + } + } + } +} diff --git a/Projects/UOContent/Gumps/BuildWorld.cs b/Projects/UOContent/Gumps/BuildWorld.cs index 971915dbd..1ef39d98b 100644 --- a/Projects/UOContent/Gumps/BuildWorld.cs +++ b/Projects/UOContent/Gumps/BuildWorld.cs @@ -113,7 +113,10 @@ namespace Server.Gumps InvokeCommand("IS Data/Spawns/Underworld/**.json", from); break; case 4: - InvokeCommand("IS Data/Spawns/traps.json", from); + if (Settings.S_DungeonTraps) + { + InvokeCommand("IS Data/Spawns/traps.json", from); + } InvokeCommand("IS Data/Spawns/Chests/**.json", from); break; case 5: InvokeCommand("WipeWildMobs", from); break; @@ -132,7 +135,10 @@ namespace Server.Gumps InvokeCommand("Decorate", from); InvokeCommand("IS Data/Spawns/Overworld/**.json", from); InvokeCommand("IS Data/Spawns/Underworld/**.json", from); - InvokeCommand("IS Data/Spawns/traps.json", from); + if (Settings.S_DungeonTraps) + { + InvokeCommand("IS Data/Spawns/traps.json", from); + } InvokeCommand("IS Data/Spawns/Chests/**.json", from); from.SendMessage(0x3F, "World population complete."); diff --git a/Projects/UOContent/Misc/ValidSettings.cs b/Projects/UOContent/Misc/ValidSettings.cs index 7c27a1421..4f39ab9b3 100644 --- a/Projects/UOContent/Misc/ValidSettings.cs +++ b/Projects/UOContent/Misc/ValidSettings.cs @@ -64,5 +64,14 @@ namespace Server mins = 1; return (double)mins; } + public static double RegionDungeonTrapsChance() + { + int chance = Settings.S_RegionDungeonTrapsChance; + if (chance > 100) + chance = 100; + if (chance < 1 ) + chance = 1; + return (double)chance; + } } } diff --git a/Projects/UOContent/Regions/Custom/GreetingDungeonRegion.cs b/Projects/UOContent/Regions/Custom/GreetingDungeonRegion.cs index c956d0253..d17e1dd33 100644 --- a/Projects/UOContent/Regions/Custom/GreetingDungeonRegion.cs +++ b/Projects/UOContent/Regions/Custom/GreetingDungeonRegion.cs @@ -1,25 +1,46 @@ +using System; +using System.Collections.Generic; using Server; using Server.Mobiles; +using Server.Engines.DungeonTraps; namespace Server.Regions { public class GreetingDungeonRegion : NoTravelSpellsAllowedRegion { + // 1. Initialize the pool of available traps + private static readonly BaseRegionTrap[] _dungeonTraps = + [ + new TripwireTrap(), + new ExplosionTrap(), + new FlameTrap(), + new GasTrap() + ]; + + // 2. Track how many safe steps a player has remaining to prevent trap spam + private readonly Dictionary _safeSteps = new(); + public GreetingDungeonRegion(string name, Map map, int priority, params Rectangle3D[] area) : base(name, map, priority, area) { } + public override bool MountsAllowed => false; + public override void OnEnter(Mobile m) { base.OnEnter(m); if (m.Player) { m.SendMessage($"You entered {this.Name}."); + + // Give players a 10-step grace period right at the entrance + _safeSteps[m] = 10; } - if (m is PlayerMobile player && player.Mounted ) + + if (m is PlayerMobile player && player.Mounted) { - if ( player.Mount is BaseMount ) + if (player.Mount is BaseMount) { Server.Custom.AutoStable.StoreMount(player); } @@ -34,9 +55,64 @@ namespace Server.Regions public override void OnExit(Mobile m) { base.OnExit(m); - if ( m is PlayerMobile player ) + if (m is PlayerMobile player) { Server.Custom.AutoStable.RestoreMount(player); + + // Clean up the dictionary when they leave to free up memory + _safeSteps.Remove(player); + } + } + + // 3. Evaluate every step taken inside the region + public override void OnLocationChanged(Mobile m, Point3D oldLocation) + { + base.OnLocationChanged(m, oldLocation); + + if (!Settings.S_RegionDungeonTraps) + { + return; + } + + // Only check for traps if they are an alive player who actually moved to a new tile + if (m is PlayerMobile pm && pm.Alive && pm.Location != oldLocation && pm.AccessLevel == AccessLevel.Player) + { + CheckForTraps(pm); + } + } + + private void CheckForTraps(PlayerMobile pm) + { + // 4. Handle Safe Steps Cooldown + if (!_safeSteps.TryGetValue(pm, out int safeStepsLeft)) + { + safeStepsLeft = 0; + } + + if (safeStepsLeft > 0) + { + _safeSteps[pm] = safeStepsLeft - 1; + return; // Player is still in their grace period + } + + // 5. Calculate Trigger Chance (Base 5% chance per step) + // Detect Hidden actively reduces this chance. + // At 0 DH = 5% chance. At 100 DH = 1% chance. + double baseChance = ValidSettings.RegionDungeonTrapsChance(); + double detectHidden = pm.Skills[SkillName.DetectHidden].Value; + double finalChance = Math.Max(1.0, baseChance - (detectHidden / 25.0)); + + // 6. Roll for Trap Trigger + if (Utility.RandomDouble() * 100.0 <= finalChance) + { + // Pick a random trap from the pool + BaseRegionTrap trap = _dungeonTraps[Utility.Random(_dungeonTraps.Length)]; + + // Fire the trap logic cascade (Remove Trap -> Dodge -> AR/MR -> Damage) + trap.Trigger(pm); + + // Reset the player's safe steps to prevent back-to-back trap triggers + _safeSteps[pm] = Utility.RandomMinMax(8, 12); } } } diff --git a/Projects/UOContent/Settings.cs b/Projects/UOContent/Settings.cs index bda041618..2ad963c0c 100644 --- a/Projects/UOContent/Settings.cs +++ b/Projects/UOContent/Settings.cs @@ -9,5 +9,8 @@ namespace Server public static int S_Stables = 3; // 0-20 Extra stable slots public static int S_DungeonBountyBoardDelay = 60; // 1-1440 minutes delay between bountie quests public static int S_DungeonBountyRefusalDelay = 10; // 1-1440 minutes delay between refusing a quest + public static bool S_DungeonTraps = false; // Spawns Dugeon floor traps + public static bool S_RegionDungeonTraps = true; // Step activated dungeon traps + public static int S_RegionDungeonTrapsChance = 5; // 1-100 Chance to get a trap check per step } }