#W# Traps: Added Dungeon traps that are step based but can be avoided with skill/armor/stats. New settings in Settings.cs
This commit is contained in:
parent
c66ce46f90
commit
c43f543611
6 changed files with 294 additions and 6 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -13,7 +13,8 @@
|
|||
/Distribution/Logs
|
||||
/Distribution/Archives
|
||||
/Distribution/Backups
|
||||
/Distribution/Saves
|
||||
/Distribution/Saves/*
|
||||
/Distribution/Saves.*
|
||||
/Distribution/docs
|
||||
/Distribution/Client
|
||||
/Distribution/Data/Spawns/Exported/*
|
||||
|
|
|
|||
193
Projects/UOContent/Engines/RegionDungeonTraps.cs
Normal file
193
Projects/UOContent/Engines/RegionDungeonTraps.cs
Normal file
|
|
@ -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<BaseArmor> wornArmor = new List<BaseArmor>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Mobile, int> _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 <BASEFONT COLOR='#FF0000'>{this.Name}</BASEFONT>.");
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue