per line would add paragraph spacing from the renderer.)
+ var skillHtml = new StringBuilder(2048);
+
+ for (int i = 0; i < visibleSkills.Count; i++)
+ {
+ Skill sk = visibleSkills[i];
+
+ if (i > 0)
+ {
+ skillHtml.Append("
");
+ }
+
+ skillHtml.Append($"{sk.Name} {sk.Value:F1} / {sk.Cap:F1}");
+ }
+
+ // scrollbar is named arg #11 in current ModernUO: AddHtml(x,y,w,h,text,color,size,fontStyle,align,background,scrollbar)
+ // Height 225 ends the area at Y=820 so the scrollbar's down-arrow clears the window frame.
+ builder.AddHtml(
+ RightCol, colY + 375, 280, 225,
+ $"
{skillHtml}", scrollbar: true
+ );
+ }
+
+ public override void OnResponse(NetState sender, in RelayInfo info)
+ {
+ if (info.ButtonID == 1) // Refresh
+ {
+ DisplayTo(from);
+ }
+ }
+
+ // Renders any batch of StatRows — attributes, resistances and ratings all share this loop.
+ private static void AddRows(ref DynamicGumpBuilder builder, ReadOnlySpan
rows)
+ {
+ for (int i = 0; i < rows.Length; i++)
+ {
+ var r = rows[i];
+ builder.AddLabel(r.X, r.Y, C_WHITE, r.Name);
+ builder.AddLabel(r.X + r.ValueOffset, r.Y, r.ValueHue, r.Value);
+ }
+ }
+
+ private static string ColorAndCenter(string color, string text) =>
+ $"{text}";
+
+ private static void DrawDivider(ref DynamicGumpBuilder builder, int x, int y, int width) =>
+ builder.AddImageTiled(x, y, width, 2, 9264);
+}
diff --git a/ToAdd/PET TRAINING.zip b/ToAdd/PET TRAINING.zip
new file mode 100644
index 000000000..68a83950e
Binary files /dev/null and b/ToAdd/PET TRAINING.zip differ
diff --git a/ToAdd/PirateSystem.zip b/ToAdd/PirateSystem.zip
new file mode 100644
index 000000000..95c3fd368
Binary files /dev/null and b/ToAdd/PirateSystem.zip differ
diff --git a/ToAdd/SecretDoorLink.cs b/ToAdd/SecretDoorLink.cs
new file mode 100644
index 000000000..625d82a20
--- /dev/null
+++ b/ToAdd/SecretDoorLink.cs
@@ -0,0 +1,239 @@
+using System;
+using ModernUO.Serialization;
+using Server;
+using Server.Commands;
+using Server.Items;
+using Server.Targeting;
+
+namespace Server.Items
+{
+ [SerializationGenerator(0, false)]
+ public partial class SecretDoorLink : Item
+ {
+ [SerializableField(0)]
+ private Serial _triggerSerial;
+
+ [SerializableField(1)]
+ private Serial _secretSerial;
+
+ [SerializableField(2)]
+ private Point3D _triggerSpot;
+
+ [SerializableField(3)]
+ private Map _triggerMap;
+
+ [SerializableField(4)]
+ private Point3D _secretHomeLoc;
+
+ [SerializableField(5)]
+ private Map _secretHomeMap;
+
+ [SerializableField(6)]
+ private bool _secretIsHidden;
+
+ private Timer _timer;
+
+ [Constructible]
+ public SecretDoorLink() : base(0x1F14)
+ {
+ Name = "Secret Door Link (controller)";
+ Movable = true;
+ StartTimer();
+ }
+
+ // ✅ This is the key fix for "doesn't work after restart"
+ [AfterDeserialization(false)]
+ private void AfterDeserialization()
+ {
+ StartTimer();
+ Timer.DelayCall(TimeSpan.Zero, Tick);
+ }
+
+ public override void OnAfterDelete()
+ {
+ base.OnAfterDelete();
+ _timer?.Stop();
+ _timer = null;
+ }
+
+ private void StartTimer()
+ {
+ _timer?.Stop();
+ _timer = Timer.DelayCall(TimeSpan.FromMilliseconds(400), TimeSpan.FromMilliseconds(400), Tick);
+ }
+
+ private Item TriggerItem => _triggerSerial.IsValid ? World.FindItem(_triggerSerial) : null;
+ private Item SecretItem => _secretSerial.IsValid ? World.FindItem(_secretSerial) : null;
+
+ private void Tick()
+ {
+ if (!_triggerSerial.IsValid || !_secretSerial.IsValid)
+ return;
+
+ var trigger = TriggerItem;
+ var secret = SecretItem;
+
+ if (trigger == null || trigger.Deleted || secret == null || secret.Deleted)
+ return;
+
+ if (_triggerMap == null || _secretHomeMap == null)
+ return;
+
+ bool onSpot =
+ trigger.Map == _triggerMap &&
+ trigger.Location == _triggerSpot;
+
+ // REVERSED behavior:
+ // Trigger ON spot => secret disappears
+ // Trigger OFF spot => secret appears
+ if (onSpot)
+ HideSecret(secret);
+ else
+ ShowSecret(secret);
+ }
+
+ private void ShowSecret(Item secret)
+ {
+ if (!_secretIsHidden)
+ return;
+
+ secret.MoveToWorld(_secretHomeLoc, _secretHomeMap);
+ secret.Visible = true;
+
+ _secretIsHidden = false;
+ }
+
+ private void HideSecret(Item secret)
+ {
+ if (_secretIsHidden)
+ return;
+
+ secret.Visible = false;
+ secret.MoveToWorld(Point3D.Zero, Map.Internal);
+
+ _secretIsHidden = true;
+ }
+
+ // ---------- GM Commands ----------
+ public static void Initialize()
+ {
+ CommandSystem.Register("SecretDoorLink", AccessLevel.GameMaster, SecretDoorLinkCommand);
+ CommandSystem.Register("SecretDoorStatus", AccessLevel.GameMaster, SecretDoorStatusCommand);
+ }
+
+ [Usage("SecretDoorLink")]
+ [Description("Links a trigger item to a secret item that disappears when the trigger is placed on a chosen tile, and reappears when moved off.")]
+ private static void SecretDoorLinkCommand(CommandEventArgs e)
+ {
+ e.Mobile.SendMessage("Target the TRIGGER item (the one you will move).");
+ e.Mobile.Target = new PickTriggerTarget();
+ }
+
+ [Usage("SecretDoorStatus")]
+ [Description("Target a SecretDoorLink controller to print its configuration.")]
+ private static void SecretDoorStatusCommand(CommandEventArgs e)
+ {
+ e.Mobile.SendMessage("Target the SecretDoorLink controller.");
+ e.Mobile.Target = new StatusTarget();
+ }
+
+ private class PickTriggerTarget : Target
+ {
+ public PickTriggerTarget() : base(12, false, TargetFlags.None) { }
+
+ protected override void OnTarget(Mobile from, object targeted)
+ {
+ if (targeted is not Item trigger || trigger.Deleted)
+ {
+ from.SendMessage("That is not a valid item.");
+ return;
+ }
+
+ from.SendMessage("Now target the SECRET item (the door/item that will appear/disappear).");
+ from.Target = new PickSecretTarget(trigger);
+ }
+ }
+
+ private class PickSecretTarget : Target
+ {
+ private readonly Item _trigger;
+
+ public PickSecretTarget(Item trigger) : base(12, false, TargetFlags.None)
+ {
+ _trigger = trigger;
+ }
+
+ protected override void OnTarget(Mobile from, object targeted)
+ {
+ if (targeted is not Item secret || secret.Deleted)
+ {
+ from.SendMessage("That is not a valid item.");
+ return;
+ }
+
+ from.SendMessage("Now target the LOCATION (ground tile) where the trigger must be placed.");
+ from.Target = new PickLocationTarget(_trigger, secret);
+ }
+ }
+
+ private class PickLocationTarget : Target
+ {
+ private readonly Item _trigger;
+ private readonly Item _secret;
+
+ public PickLocationTarget(Item trigger, Item secret) : base(18, true, TargetFlags.None)
+ {
+ _trigger = trigger;
+ _secret = secret;
+ }
+
+ protected override void OnTarget(Mobile from, object targeted)
+ {
+ if (targeted is not IPoint3D p)
+ {
+ from.SendMessage("Invalid location.");
+ return;
+ }
+
+ var map = from.Map;
+
+ var controller = new SecretDoorLink
+ {
+ _triggerSerial = _trigger.Serial,
+ _secretSerial = _secret.Serial,
+ _triggerSpot = new Point3D(p),
+ _triggerMap = map,
+ _secretHomeLoc = _secret.Location,
+ _secretHomeMap = _secret.Map,
+ _secretIsHidden = false
+ };
+
+ controller.MoveToWorld(from.Location, from.Map);
+
+ // Force initial evaluation
+ controller.Tick();
+
+ from.SendMessage("Linked! Trigger ON spot => secret disappears. Trigger OFF spot => secret appears.");
+ }
+ }
+
+ private class StatusTarget : Target
+ {
+ public StatusTarget() : base(12, false, TargetFlags.None) { }
+
+ protected override void OnTarget(Mobile from, object targeted)
+ {
+ if (targeted is not SecretDoorLink link || link.Deleted)
+ {
+ from.SendMessage("That is not a SecretDoorLink controller.");
+ return;
+ }
+
+ from.SendMessage($"Trigger: {link._triggerSerial} Secret: {link._secretSerial}");
+ from.SendMessage($"Spot: {link._triggerSpot} on {link._triggerMap}");
+ from.SendMessage($"Secret home: {link._secretHomeLoc} on {link._secretHomeMap}");
+ from.SendMessage($"Hidden now: {link._secretIsHidden}");
+ }
+ }
+ }
+}
diff --git a/ToAdd/SquireSystem.zip b/ToAdd/SquireSystem.zip
new file mode 100644
index 000000000..b594e1ec2
Binary files /dev/null and b/ToAdd/SquireSystem.zip differ
diff --git a/ToAdd/Traps/CloneTrap.cs b/ToAdd/Traps/CloneTrap.cs
new file mode 100644
index 000000000..551250e3d
--- /dev/null
+++ b/ToAdd/Traps/CloneTrap.cs
@@ -0,0 +1,309 @@
+/**********************************
+* Author: Expa *
+* Optimized ModernUO .Net 10/C#14 *
+**********************************/
+using System;
+using System.Collections.Concurrent;
+using System.Linq.Expressions;
+using ModernUO.Serialization;
+using Server.Items;
+using Server.Mobiles;
+
+namespace Server.Items
+{
+ [SerializationGenerator(0, false)]
+ public partial class CloneTrap : Item
+ {
+ private const int GraphicId = 0xA723;
+ private const int MoldHue = 2894;
+
+ [Constructible]
+ public CloneTrap() : base(GraphicId)
+ {
+ Name = "a Clone trap";
+ Movable = false;
+ Visible = false;
+ Hue = MoldHue;
+ }
+
+ public override bool OnMoveOver(Mobile m)
+ {
+ if (m?.CanBeDamaged() != true)
+ return base.OnMoveOver(m);
+
+ m.FixedParticles(0x36BD, 20, 10, 5044, EffectLayer.Head);
+ m.PlaySound(0x307);
+ _ = new MirrorClone(m) { Location = m.Location, Map = m.Map };
+ Delete();
+
+ return base.OnMoveOver(m);
+ }
+
+ private void Deserialize(IGenericReader reader, int version) { }
+ }
+
+ [SerializationGenerator(0, false)]
+ public partial class MirrorClone : BaseCreature
+ {
+ private const int CloneHue = 2894;
+ private const int TimerMin = 120;
+ private const int TimerMax = 300;
+ private const double DmgMult = 2.0;
+ private const double ResMult = 0.5;
+
+ [SerializableField(0)]
+ private Mobile _victim;
+
+ private MirrorCloneTimer _timer;
+
+ public MirrorClone(Mobile victim) : base(AIType.AI_Melee, FightMode.Closest, 10, 1)
+ {
+ _victim = victim;
+ Hue = CloneHue;
+ Copy();
+ (_timer = new MirrorCloneTimer(this)).Start();
+ }
+
+ public override bool AlwaysMurderer => true;
+
+ private void Copy()
+ {
+ var v = _victim;
+ if (v is null)
+ return;
+
+ BodyMod = v.Body;
+ Name = v.Name;
+ Title = v.Title;
+ Fame = v.Fame;
+ Female = v.Female;
+ NameHue = v.NameHue;
+ SpeechHue = v.SpeechHue;
+ Criminal = v.Criminal;
+ Karma = v.Karma;
+ Kills = v.Kills;
+
+ RawStr = Math.Clamp((int)(v.RawStr * DmgMult), 3, 225);
+ RawDex = v.RawDex;
+ RawInt = v.RawInt;
+
+ StatCap = Math.Clamp((int)(v.StatCap * DmgMult), 60, 900);
+ SkillsCap = v.SkillsCap;
+
+ Hits = Math.Clamp((int)(v.Hits * DmgMult), 1, 32767);
+ Mana = v.Mana;
+ Stam = v.Stam;
+
+ ApplyResistanceBalance();
+ CopySkills();
+ CopyEquipment();
+ CopyMount();
+ }
+
+ private void ApplyResistanceBalance()
+ {
+ if (_victim is null)
+ return;
+
+ SetResistance(ResistanceType.Physical, Math.Clamp((int)(_victim.PhysicalResistance * ResMult), 0, 100));
+ SetResistance(ResistanceType.Fire, Math.Clamp((int)(_victim.FireResistance * ResMult), 0, 100));
+ SetResistance(ResistanceType.Cold, Math.Clamp((int)(_victim.ColdResistance * ResMult), 0, 100));
+ SetResistance(ResistanceType.Poison, Math.Clamp((int)(_victim.PoisonResistance * ResMult), 0, 100));
+ SetResistance(ResistanceType.Energy, Math.Clamp((int)(_victim.EnergyResistance * ResMult), 0, 100));
+ }
+
+ private void CopySkills()
+ {
+ if (_victim is null)
+ return;
+
+ var skills = _victim.Skills;
+ var len = skills.Length;
+ for (int i = 0; i < len; i++)
+ {
+ Skills[i].Base = skills[i].Base;
+ Skills[i].Cap = skills[i].Cap;
+ }
+ }
+
+ private void CopyEquipment()
+ {
+ var items = _victim?.Items;
+ if (items is null || items.Count == 0)
+ return;
+
+ for (int i = 0, cnt = items.Count; i < cnt; i++)
+ {
+ var item = items[i];
+ if (item?.Parent != _victim || item is IMountItem)
+ continue;
+
+ var copy = ItemFactory.CreateInstance(item);
+ if (copy is not null)
+ {
+ CopyItemProperties(copy, item);
+ AddItem(copy);
+ }
+ }
+ }
+
+ private void CopyMount()
+ {
+ var mount = _victim?.FindItemOnLayer(Layer.Mount);
+ if (mount is not IMountItem)
+ return;
+
+ var copy = ItemFactory.CreateInstance(mount);
+ if (copy is null)
+ return;
+
+ CopyItemProperties(copy, mount);
+ AddItem(copy);
+
+ if (copy is IMountItem { Mount: BaseMount baseMount })
+ baseMount.Rider = this;
+ }
+
+ private static void CopyItemProperties(Item dest, Item src)
+ {
+ dest.ItemID = src.ItemID;
+ dest.Amount = src.Amount;
+ dest.Weight = src.Weight;
+ dest.Layer = src.Layer;
+ dest.Hue = src.Hue;
+ dest.Name = src.Name;
+ dest.Movable = src.Movable;
+ dest.LootType = src.LootType;
+ }
+
+ public override void OnDeath(Container c)
+ {
+ Cleanup();
+ base.OnDeath(c);
+ }
+
+ public override void OnDelete()
+ {
+ Cleanup();
+ base.OnDelete();
+ }
+
+ private void Cleanup()
+ {
+ _timer?.Stop();
+ DismountAndClear();
+ }
+
+ private void DismountAndClear()
+ {
+ if (Mount is BaseMount mount)
+ {
+ mount.Rider = null;
+ mount.Delete();
+ }
+
+ var items = Items;
+ for (int i = items.Count - 1; i >= 0; i--)
+ items[i].Delete();
+
+ Corpse?.Delete();
+ }
+
+ private void Deserialize(IGenericReader reader, int version)
+ {
+ _victim = reader.ReadEntity();
+ if (_victim is not null)
+ Delete();
+ }
+
+ private sealed class MirrorCloneTimer : Timer
+ {
+ private WeakReference _ref;
+
+ public MirrorCloneTimer(MirrorClone clone)
+ : base(TimeSpan.FromSeconds(Utility.Random(TimerMin, TimerMax)))
+ {
+ _ref = new WeakReference(clone);
+ }
+
+ protected override void OnTick()
+ {
+ if (!_ref.TryGetTarget(out var clone) || clone.Deleted)
+ {
+ Stop();
+ return;
+ }
+
+ clone.Warmode = false;
+ Effects.SendLocationParticles(
+ EffectItem.Create(clone.Location, clone.Map, EffectItem.DefaultDuration),
+ 0x3728, 10, 10, 2023
+ );
+ clone.Delete();
+ Stop();
+ }
+ }
+
+ private static class ItemFactory
+ {
+ private static readonly ConcurrentDictionary> _ctors = new();
+
+ public static Item CreateInstance(Item src)
+ {
+ if (src is null)
+ return null;
+
+ var type = src.GetType();
+ var factory = _ctors.GetOrAdd(type, CreateFactory);
+
+ if (factory is null)
+ {
+ try
+ {
+ return new Item(src.ItemID);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ try
+ {
+ var item = factory();
+ return item;
+ }
+ catch
+ {
+ try
+ {
+ return new Item(src.ItemID);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+ }
+
+ private static Func- CreateFactory(Type t)
+ {
+ try
+ {
+ var ctor = t.GetConstructor(Type.EmptyTypes);
+ if (ctor == null)
+ return null;
+
+ var newExp = Expression.New(ctor);
+ var conv = Expression.Convert(newExp, typeof(Item));
+ var lambda = Expression.Lambda>(conv);
+ return lambda.Compile();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/ToAdd/Traps/CurseItemsTrap.cs b/ToAdd/Traps/CurseItemsTrap.cs
new file mode 100644
index 000000000..a2b26ff4e
--- /dev/null
+++ b/ToAdd/Traps/CurseItemsTrap.cs
@@ -0,0 +1,301 @@
+/**********************************
+* Author: Expa *
+* Optimized ModernUO .Net 10/C#14 *
+**********************************/
+using System;
+using System.Collections.Generic;
+using ModernUO.Serialization;
+using Server;
+using Server.Items;
+using Server.Mobiles;
+using Server.Targeting;
+
+namespace Server.Items;
+
+static class CurseConfig
+{
+ public const int ScrollId = 0x1F2E;
+ public const int ScrollHue = 0x58;
+ public const int TrapId = 0x1B7B;
+ public const int Range = 1;
+ public const int SoundRemove = 0x1F7;
+ public const int SoundTrap = 0x1E1;
+ public const int PartRemove = 0x373A;
+ public const int PartTrap = 0x374B;
+ public const string TrapName = "Hidden Curse Trap";
+ public static readonly TimeSpan Cooldown = TimeSpan.FromMinutes(5);
+ public const bool ShowWhileCooling = true;
+ public const string Suffix = " [The Cursed!]";
+ public const string MsgNotInPack = "Scroll must be in backpack.";
+ public const string MsgPrompt = "Choose cursed item.";
+ public const string MsgNotCursed = "Not cursed.";
+ public const string MsgSelect = "Select an item.";
+ public const string MsgRemovedPack = "Curse removed (backpack).";
+ public const string MsgRemovedGround = "Curse removed (ground).";
+ public const string MsgReload = "Trap reloading!";
+ public const string MsgCursed = "Item cursed and moved.";
+}
+
+[SerializationGenerator(0, false)]
+public partial class CurseRemovalScroll : Item
+{
+ [Constructible]
+ public CurseRemovalScroll() : base(CurseConfig.ScrollId) =>
+ Name = "Curse Removal Scroll";
+
+ public CurseRemovalScroll(Serial s) : base(s) { }
+
+ public override void OnDoubleClick(Mobile from)
+ {
+ if (from?.Backpack is null || !this.IsChildOf(from.Backpack))
+ {
+ from?.SendMessage(CurseConfig.MsgNotInPack);
+ return;
+ }
+
+ from.Target = new T(this);
+ from.SendMessage(CurseConfig.MsgPrompt);
+ }
+
+ private sealed class T : Target
+ {
+ private readonly CurseRemovalScroll _s;
+
+ public T(CurseRemovalScroll s) : base(3, false, TargetFlags.None) => _s = s;
+
+ protected override void OnTarget(Mobile from, object target)
+ {
+ if (_s?.Deleted != false || from?.Deleted != false)
+ return;
+
+ if (target is not Item it)
+ {
+ from.SendMessage(CurseConfig.MsgSelect);
+ return;
+ }
+
+ var isCursed = it.LootType == LootType.Cursed ||
+ (it.Name?.EndsWith(CurseConfig.Suffix, StringComparison.Ordinal) == true);
+
+ if (!isCursed)
+ {
+ from.SendMessage(CurseConfig.MsgNotCursed);
+ return;
+ }
+
+ it.LootType = LootType.Regular;
+ it.Movable = true;
+
+ if (!string.IsNullOrEmpty(it.Name) && it.Name.EndsWith(CurseConfig.Suffix, StringComparison.Ordinal))
+ {
+ var baseName = it.Name[..^CurseConfig.Suffix.Length];
+ it.Name = string.IsNullOrEmpty(baseName) ? null : baseName;
+ }
+
+ from.PlaySound(CurseConfig.SoundRemove);
+ from.FixedParticles(CurseConfig.PartRemove, 10, 15, 5018, EffectLayer.Waist);
+ from.SendMessage(it.IsChildOf(from.Backpack) ? CurseConfig.MsgRemovedPack : CurseConfig.MsgRemovedGround);
+ _s.Delete();
+ }
+ }
+}
+
+[SerializationGenerator(0, false)]
+public partial class CurseTrap : BaseTrap
+{
+ private DateTime _next = DateTime.Now;
+
+ [Constructible]
+ public CurseTrap() : base(CurseConfig.TrapId) => InitTrap();
+
+ public CurseTrap(Serial s) : base(s) => InitTrap();
+
+ private void InitTrap()
+ {
+ Name = CurseConfig.TrapName;
+ Movable = false;
+ Visible = false;
+ }
+
+ public override bool PassivelyTriggered => true;
+ public override TimeSpan PassiveTriggerDelay => TimeSpan.Zero;
+ public override int PassiveTriggerRange => CurseConfig.Range;
+ public override TimeSpan ResetDelay => TimeSpan.Zero;
+
+ public override void OnTrigger(Mobile from)
+ {
+ if (from?.Alive != true || !from.Player)
+ return;
+
+ if (Core.Now < _next)
+ {
+ from.SendMessage(CurseConfig.MsgReload);
+ return;
+ }
+
+ var item = SelectTarget(from);
+ if (item == null)
+ return;
+
+ var oldName = item.Name;
+ var oldLoot = item.LootType;
+ var oldMovable = item.Movable;
+
+ try
+ {
+ if (item.Parent == from)
+ from.RemoveItem(item);
+
+ item.LootType = LootType.Regular;
+ item.Movable = false;
+
+ if (!(item.Name?.EndsWith(CurseConfig.Suffix) == true))
+ {
+ var baseName = oldName ?? item.ItemData.Name ?? item.GetType().Name;
+ item.Name = baseName + CurseConfig.Suffix;
+ }
+
+ if (!item.IsChildOf(from.Backpack) && !TryPlaceInBackpack(from, item))
+ item.MoveToWorld(from.Location, from.Map);
+
+ from.PlaySound(CurseConfig.SoundTrap);
+ from.FixedParticles(CurseConfig.PartTrap, 10, 10, 5044, EffectLayer.Head);
+ from.SendMessage(CurseConfig.MsgCursed);
+ _next = Core.Now + CurseConfig.Cooldown;
+ }
+ catch
+ {
+ if (item?.Deleted == false)
+ {
+ item.LootType = oldLoot;
+ item.Movable = oldMovable;
+ item.Name = oldName;
+ }
+ }
+ }
+
+ private static bool TryPlaceInBackpack(Mobile to, Item it)
+ {
+ if (to?.Backpack is null || it.IsChildOf(to.Backpack))
+ return false;
+
+ try
+ {
+ return to.Backpack.TryDropItem(to, it, false);
+ }
+ catch
+ {
+ try
+ {
+ to.Backpack.DropItem(it);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+
+ private static Item SelectTarget(Mobile m)
+ {
+ if (m == null)
+ return null;
+
+ double totalWeight = 0.0;
+ Item firstCandidate = null;
+
+ foreach (var itm in m.Items)
+ {
+ if (itm?.Deleted != false)
+ continue;
+
+ var inEquip = itm.Parent == m;
+ var inPack = m.Backpack != null && itm.IsChildOf(m.Backpack);
+
+ if (!inEquip && !inPack)
+ continue;
+
+ if (itm.LootType == LootType.Cursed || !itm.Movable)
+ continue;
+
+ if (itm.Layer is Layer.Backpack or Layer.Hair or Layer.FacialHair)
+ continue;
+
+ var weight = CalculateWeight(itm);
+ if (weight <= 0.0)
+ continue;
+
+ if (firstCandidate == null)
+ firstCandidate = itm;
+
+ totalWeight += weight;
+ }
+
+ if (totalWeight <= 0.0)
+ return firstCandidate;
+
+ var roll = Utility.RandomDouble() * totalWeight;
+ var accumulated = 0.0;
+
+ foreach (var itm in m.Items)
+ {
+ if (itm?.Deleted != false)
+ continue;
+
+ var inEquip = itm.Parent == m;
+ var inPack = m.Backpack != null && itm.IsChildOf(m.Backpack);
+
+ if (!inEquip && !inPack)
+ continue;
+
+ if (itm.LootType == LootType.Cursed || !itm.Movable)
+ continue;
+
+ if (itm.Layer is Layer.Backpack or Layer.Hair or Layer.FacialHair)
+ continue;
+
+ var weight = CalculateWeight(itm);
+ if (weight <= 0.0)
+ continue;
+
+ accumulated += weight;
+ if (accumulated >= roll)
+ return itm;
+ }
+
+ return firstCandidate;
+ }
+
+ private static double CalculateWeight(Item it)
+ {
+ var baseWeight = it.Amount > 1 ? Math.Min(it.Amount, 100) : 1.0;
+
+ return it switch
+ {
+ Gold g => baseWeight + Math.Min(g.Amount, 1000) / 10.0,
+ Bandage => baseWeight + 50,
+ Arrow => baseWeight + 50,
+ Bolt => baseWeight + 50,
+ SpellScroll => baseWeight + 40,
+ BasePotion => baseWeight + 40,
+ BaseReagent => baseWeight + 40,
+ Spellbook => baseWeight + 60,
+ Runebook => baseWeight + 80,
+ _ => GetByTypeName(it, baseWeight)
+ };
+ }
+
+ private static double GetByTypeName(Item it, double baseWeight)
+ {
+ var typeName = it.GetType().Name;
+
+ if (typeName.Contains("scroll", StringComparison.OrdinalIgnoreCase))
+ return baseWeight + 20;
+
+ if (typeName.Contains("book", StringComparison.OrdinalIgnoreCase) && it is not Spellbook)
+ return baseWeight + 15;
+
+ return baseWeight;
+ }
+}
\ No newline at end of file
diff --git a/ToAdd/Traps/LandMobTrap.cs b/ToAdd/Traps/LandMobTrap.cs
new file mode 100644
index 000000000..dc59f99d2
--- /dev/null
+++ b/ToAdd/Traps/LandMobTrap.cs
@@ -0,0 +1,167 @@
+/**********************************
+* Author: Expa *
+* Optimized ModernUO .Net 10/C#14 *
+**********************************/
+using System;
+using ModernUO.Serialization;
+using Server;
+using Server.Mobiles;
+
+namespace Server.Items;
+
+[SerializationGenerator(0, false)]
+public partial class LandMobTrap : Item
+{
+ [SerializableField(0)] private int _minSpawn = 3;
+ [SerializableField(1)] private int _maxSpawn = 6;
+ [SerializableField(2)] private double _triggerDelaySeconds = 0.5;
+ [SerializableField(3)] private int _soundID = 0x1C6;
+ [SerializableField(4)] private int _particleItemId = 0x373A;
+ [SerializableField(5)] private byte _particleCount = 20;
+ [SerializableField(6)] private byte _particleSpeed = 10;
+ [SerializableField(7)] private byte _flags = 1;
+
+ private const int MaxAllowedSpawn = 12;
+ private TimerExecutionToken _spawnToken;
+
+ private bool Armed
+ {
+ get => (_flags & 1) != 0;
+ set => _flags = (byte)(value ? (_flags | 1) : (_flags & ~1));
+ }
+
+ private bool BlockMovement
+ {
+ get => (_flags & 2) != 0;
+ set => _flags = (byte)(value ? (_flags | 2) : (_flags & ~2));
+ }
+
+ [Constructible]
+ public LandMobTrap() : base(0x1B7A)
+ {
+ Movable = false;
+ Hue = 1058;
+ Visible = false;
+ Name = "land mob trap";
+ }
+
+ public override bool HandlesOnMovement => true;
+
+ public override bool OnMoveOver(Mobile m)
+ {
+ var map = Map;
+ if (Deleted || map == null || map == Map.Internal)
+ return true;
+
+ if (!Armed || !m.Player || m.AccessLevel > AccessLevel.Player)
+ return true;
+
+ foreach (var bc in GetMobilesInRange(1))
+ {
+ if (bc is Skeleton or SkeletalMage or SkeletalKnight)
+ return true;
+ }
+
+ Armed = false;
+ InvalidateProperties();
+
+ var min = Math.Max(1, _minSpawn);
+ var max = Math.Clamp(_maxSpawn, min, MaxAllowedSpawn);
+
+ if (_triggerDelaySeconds <= 0.0)
+ DoSpawn(m, min, max);
+ else
+ Timer.StartTimer(TimeSpan.FromSeconds(_triggerDelaySeconds), () => DoSpawn(m, min, max), out _spawnToken);
+
+ return !BlockMovement;
+ }
+
+ private void DoSpawn(Mobile trigger, int minSpawn, int maxSpawn)
+ {
+ try
+ {
+ var map = Map;
+ if (Deleted || map == null || map == Map.Internal)
+ return;
+
+ var count = Utility.RandomMinMax(minSpawn, maxSpawn);
+
+ for (var i = 0; i < count; ++i)
+ {
+ var cr = CreateRandomCreature();
+ if (cr is null)
+ continue;
+
+ try
+ {
+ if (TryFindSpawnLocation(map, out var loc))
+ {
+ cr.MoveToWorld(loc, map);
+ cr.Combatant = trigger;
+ }
+ else
+ {
+ cr.Delete();
+ }
+ }
+ catch
+ {
+ cr?.Delete();
+ }
+ }
+
+ Effects.SendLocationParticles(
+ EffectItem.Create(Location, Map, EffectItem.DefaultDuration),
+ _particleItemId, _particleCount, _particleSpeed, 5044
+ );
+ Effects.PlaySound(Location, Map, _soundID);
+ }
+ finally
+ {
+ Delete();
+ }
+ }
+
+ private static BaseCreature CreateRandomCreature() =>
+ Utility.Random(5) switch
+ {
+ 0 or 1 => new Skeleton(),
+ 2 or 3 => new SkeletalMage(),
+ _ => new SkeletalKnight()
+ };
+
+ private bool TryFindSpawnLocation(Map map, out Point3D loc)
+ {
+ loc = Location;
+
+ for (var i = 0; i < 10; ++i)
+ {
+ var x = X + Utility.Random(3) - 1;
+ var y = Y + Utility.Random(3) - 1;
+ var z = map.GetAverageZ(x, y);
+
+ if (map.CanSpawnMobile(new Point2D(x, y), z))
+ {
+ loc = new Point3D(x, y, z);
+ return true;
+ }
+
+ if (map.CanFit(x, y, Z, 16, false, false))
+ {
+ loc = new Point3D(x, y, Z);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public override void OnAfterDelete()
+ {
+ _spawnToken.Cancel();
+ base.OnAfterDelete();
+ }
+
+ [AfterDeserialization]
+ private void AfterDeserialization() { }
+}
\ No newline at end of file
diff --git a/ToAdd/Traps/MagnetItemsTrap.cs b/ToAdd/Traps/MagnetItemsTrap.cs
new file mode 100644
index 000000000..3d90ec3cf
--- /dev/null
+++ b/ToAdd/Traps/MagnetItemsTrap.cs
@@ -0,0 +1,215 @@
+/**********************************
+* Author: Expa *
+* Optimized ModernUO .Net 10/C#14 *
+**********************************/
+using System;
+using System.Collections.Generic;
+using ModernUO.Serialization;
+using Server;
+using Server.Items;
+using Server.Mobiles;
+
+namespace Server.Items;
+
+[SerializationGenerator(0, false)]
+public partial class MagnetTrap : Item
+{
+ private TimerExecutionToken _timerToken;
+ private TimerExecutionToken _deleteTimerToken;
+ private readonly TimeSpan _autoDelete = TimeSpan.FromSeconds(30);
+ private readonly Dictionary _dropState = new();
+ private DateTime _lastCleanup = Core.Now;
+
+ private const int MetalDropChancePercent = 60;
+ private const int MaxDropsPerPlayer = 3;
+ private const int CleanupIntervalSeconds = 5;
+ private const int DropStateStaleSeconds = 10;
+
+ private static readonly HashSet MetalResourceNames = new(StringComparer.OrdinalIgnoreCase)
+ {
+ "Iron", "Steel", "DullCopper", "ShadowIron", "Copper", "Bronze",
+ "Gold", "Silver", "Agapite", "Verite", "Valorite"
+ };
+
+ [Constructible]
+ public MagnetTrap() : base(0x1AC5)
+ {
+ Movable = false;
+ Name = "a magnetic trap";
+ Hue = 1341;
+ Timer.StartTimer(TimeSpan.Zero, TimeSpan.FromSeconds(1), DoMagneticEffect, out _timerToken);
+ StartDeleteTimer();
+ }
+
+ private void DoMagneticEffect()
+ {
+ if (Deleted)
+ return;
+
+ Effects.SendLocationParticles(
+ EffectItem.Create(Location, Map, EffectItem.DefaultDuration),
+ 0x376A, 9, 32, 5024, 0, 5024, 0
+ );
+ Effects.PlaySound(Location, Map, 0x1F0);
+
+ foreach (var m in GetMobilesInRange(1))
+ {
+ if (m is PlayerMobile pm)
+ TryDropRandomItem(pm);
+ }
+
+ var now = Core.Now;
+ if ((now - _lastCleanup).TotalSeconds >= CleanupIntervalSeconds)
+ {
+ _lastCleanup = now;
+ CleanupDropState(now);
+ }
+ }
+
+ private void TryDropRandomItem(PlayerMobile player)
+ {
+ if (player?.Deleted != false)
+ return;
+
+ var now = Core.Now;
+
+ if (!_dropState.TryGetValue(player, out var state))
+ state = (0, now);
+
+ if (state.Count >= MaxDropsPerPlayer)
+ return;
+
+ _dropState[player] = (state.Count, now);
+
+ var preferMetal = Utility.Random(100) < MetalDropChancePercent;
+ var dropped = preferMetal
+ ? TryDropFrom(player.Items, IsMetal, player) || TryDropFrom(player.Backpack?.Items, IsMetal, player)
+ : TryDropFrom(player.Backpack?.Items, IsMetal, player) || TryDropFrom(player.Items, IsMetal, player);
+
+ if (dropped)
+ _dropState[player] = (state.Count + 1, now);
+ }
+
+ private bool TryDropFrom(IEnumerable
- source, Func
- predicate, PlayerMobile player)
+ {
+ if (source is null)
+ return false;
+
+ Item candidate = null;
+ var count = 0;
+
+ foreach (var item in source)
+ {
+ if (item?.Movable != true || !CanDropItem(item) || !predicate(item))
+ continue;
+
+ count++;
+ if (Utility.Random(count) == 0)
+ candidate = item;
+ }
+
+ return candidate is not null && DropItemToGround(candidate, player);
+ }
+
+ private static bool CanDropItem(Item item)
+ {
+ if (item?.Movable != true)
+ return false;
+
+ if (item.LootType == LootType.Blessed || item.LootType == LootType.Newbied)
+ return false;
+
+ if (item is LockableContainer lockable && lockable.Locked)
+ return false;
+
+ return true;
+ }
+
+ private static bool IsMetal(Item item)
+ {
+ if (item is null)
+ return false;
+
+ if (item is BaseWeapon weapon)
+ return IsMetalResource(weapon.Resource);
+
+ if (item is BaseArmor armor)
+ return IsMetalResource(armor.Resource);
+
+ if (item is BaseShield shield)
+ return IsMetalResource(shield.Resource);
+
+ if (item is BaseJewel)
+ return true;
+
+ var typeName = item.GetType().Name;
+ return typeName.Contains("sword", StringComparison.OrdinalIgnoreCase) ||
+ typeName.Contains("axe", StringComparison.OrdinalIgnoreCase) ||
+ typeName.Contains("mace", StringComparison.OrdinalIgnoreCase) ||
+ typeName.Contains("dagger", StringComparison.OrdinalIgnoreCase) ||
+ typeName.Contains("knife", StringComparison.OrdinalIgnoreCase) ||
+ typeName.Contains("plate", StringComparison.OrdinalIgnoreCase) ||
+ typeName.Contains("chain", StringComparison.OrdinalIgnoreCase) ||
+ typeName.Contains("shield", StringComparison.OrdinalIgnoreCase) ||
+ typeName.Contains("helmet", StringComparison.OrdinalIgnoreCase) ||
+ typeName.Contains("gauntlet", StringComparison.OrdinalIgnoreCase) ||
+ typeName.Contains("ring", StringComparison.OrdinalIgnoreCase) ||
+ typeName.Contains("necklace", StringComparison.OrdinalIgnoreCase) ||
+ typeName.Contains("earrings", StringComparison.OrdinalIgnoreCase) ||
+ typeName.Contains("bracelet", StringComparison.OrdinalIgnoreCase) ||
+ typeName.Contains("talisman", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool IsMetalResource(CraftResource resource)
+ {
+ var resName = CraftResources.GetName(resource);
+ return !string.IsNullOrEmpty(resName) && MetalResourceNames.Contains(resName);
+ }
+
+ private bool DropItemToGround(Item item, PlayerMobile player)
+ {
+ if (item?.Movable != true || player is null || !CanDropItem(item))
+ return false;
+
+ try
+ {
+ item.MoveToWorld(player.Location, player.Map);
+ player.SendLocalizedMessage(502690);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private void CleanupDropState(DateTime now)
+ {
+ var keysToRemove = new List();
+
+ foreach (var kv in _dropState)
+ {
+ var player = kv.Key;
+ var lastSeen = kv.Value.LastSeen;
+ if (player is null || player.Deleted || (now - lastSeen).TotalSeconds > DropStateStaleSeconds)
+ keysToRemove.Add(player);
+ }
+
+ foreach (var key in keysToRemove)
+ _dropState.Remove(key);
+ }
+
+ private void StartDeleteTimer()
+ {
+ _deleteTimerToken.Cancel();
+ Timer.StartTimer(_autoDelete, Delete, out _deleteTimerToken);
+ }
+
+ public override void OnDelete()
+ {
+ _timerToken.Cancel();
+ _deleteTimerToken.Cancel();
+ _dropState.Clear();
+ base.OnDelete();
+ }
+}
\ No newline at end of file
diff --git a/ToAdd/Traps/SealedItemsTrap.cs b/ToAdd/Traps/SealedItemsTrap.cs
new file mode 100644
index 000000000..166dd9af0
--- /dev/null
+++ b/ToAdd/Traps/SealedItemsTrap.cs
@@ -0,0 +1,182 @@
+/**********************************
+* Author: Expa *
+* Optimized ModernUO .Net 10/C#14 *
+**********************************/
+using System;
+using ModernUO.Serialization;
+using Server;
+using Server.Mobiles;
+using Server.Targeting;
+
+namespace Server.Items;
+
+public static class SealedTrapConfig
+{
+ public const int MinItemsToSeal = 1;
+ public const int MaxItemsToSeal = 5;
+ public const int TrapItemId = 0x9CDD;
+ public const int SealedChestItemId = 0x9AA;
+ public const bool DeleteAfterTrigger = true;
+ public const string ActivationMessage = "The trap has sealed {0} items in the chest!";
+ public const string SealedChestMessage = "This chest is magically sealed! Only the Seal Removal Scroll can open it.";
+}
+
+[SerializationGenerator(0, false)]
+public partial class ItemSealingTrap : Item
+{
+ [Constructible]
+ public ItemSealingTrap() : base(SealedTrapConfig.TrapItemId)
+ {
+ Movable = false;
+ Visible = false;
+ Name = "The Sealing trap";
+ }
+
+ public override bool OnMoveOver(Mobile m)
+ {
+ if (m is not PlayerMobile player || player.AccessLevel > AccessLevel.Player)
+ return base.OnMoveOver(m);
+
+ var backpack = player.Backpack;
+ if (backpack?.Items.Count == 0)
+ return base.OnMoveOver(m);
+
+ var itemsToSeal = Math.Min(Utility.RandomMinMax(SealedTrapConfig.MinItemsToSeal, SealedTrapConfig.MaxItemsToSeal), backpack.Items.Count);
+ var chest = new SealedItemsChest();
+
+ for (var i = 0; i < itemsToSeal; i++)
+ {
+ var idx = Utility.Random(backpack.Items.Count);
+ var item = backpack.Items[idx];
+ backpack.RemoveItem(item);
+ chest.DropItem(item);
+ }
+
+ chest.MoveToWorld(Location, Map);
+ player.SendMessage(string.Format(SealedTrapConfig.ActivationMessage, itemsToSeal));
+
+ if (SealedTrapConfig.DeleteAfterTrigger)
+ Delete();
+
+ return base.OnMoveOver(m);
+ }
+}
+
+[Furniture]
+[SerializationGenerator(0, false)]
+public partial class SealedItemsChest : LockableContainer
+{
+ private bool _sealed = true;
+
+ [CommandProperty(AccessLevel.GameMaster)]
+ public bool Sealed
+ {
+ get => _sealed;
+ set
+ {
+ _sealed = value;
+ Locked = value;
+ InvalidateProperties();
+ }
+ }
+
+ [Constructible]
+ public SealedItemsChest() : base(SealedTrapConfig.SealedChestItemId)
+ {
+ Locked = true;
+ Name = "A sealed little chest";
+ LockLevel = ILockpickable.CannotPick;
+ RequiredSkill = 0;
+ MaxLockLevel = 0;
+ }
+
+ public override void OnDoubleClick(Mobile from)
+ {
+ if (Sealed)
+ from?.SendMessage(SealedTrapConfig.SealedChestMessage);
+ else
+ base.OnDoubleClick(from);
+ }
+
+ public override void GetProperties(IPropertyList list)
+ {
+ base.GetProperties(list);
+ if (_sealed)
+ list.Add("Sealed");
+ }
+
+ public void Unseal()
+ {
+ Sealed = false;
+ LockLevel = ILockpickable.CannotPick;
+ Name = "A small chest";
+ }
+
+ public override void Open(Mobile from)
+ {
+ if (_sealed)
+ from?.SendMessage(SealedTrapConfig.SealedChestMessage);
+ else
+ base.Open(from);
+ }
+
+ public override bool CheckLocked(Mobile from)
+ {
+ if (!Locked)
+ return false;
+
+ if (_sealed)
+ {
+ from?.SendMessage(SealedTrapConfig.SealedChestMessage);
+ return true;
+ }
+
+ return base.CheckLocked(from);
+ }
+}
+
+[SerializationGenerator(0, false)]
+public partial class ScrollOfUnsealing : Item
+{
+ [Constructible]
+ public ScrollOfUnsealing() : base(0x1F4F)
+ {
+ Name = "The Seal Removal Scroll";
+ Weight = 1.0;
+ }
+
+ public override void OnDoubleClick(Mobile from)
+ {
+ from?.Target = new UnsealTarget(this);
+ from?.SendMessage("Select a sealed chest.");
+ }
+
+ private sealed class UnsealTarget : Target
+ {
+ private readonly ScrollOfUnsealing _scroll;
+
+ public UnsealTarget(ScrollOfUnsealing scroll) : base(3, false, TargetFlags.None) =>
+ _scroll = scroll;
+
+ protected override void OnTarget(Mobile from, object targeted)
+ {
+ if (_scroll?.Deleted != false)
+ return;
+
+ switch (targeted)
+ {
+ case SealedItemsChest { Sealed: true } chest:
+ chest.Unseal();
+ from?.SendMessage("The seal has been lifted!");
+ _scroll.Delete();
+ break;
+ case SealedItemsChest:
+ from?.SendMessage("The chest has already been opened!");
+ break;
+ default:
+ from?.SendMessage("It's not a sealed chest!");
+ break;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/publish.sh b/publish.sh
index 30d80c4f2..141f02b58 100755
--- a/publish.sh
+++ b/publish.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
set -e
REPO_ROOT="$(cd "$(dirname "$0")" && pwd)"