#W# Rework: Complete rework of ThiefPedestals.

This commit is contained in:
WarrentyExpired 2026-10-24 17:45:20 -04:00
parent a440cb72d1
commit bc3c5143b9
4 changed files with 443 additions and 221 deletions

View file

@ -8,35 +8,12 @@ using Server.Network;
namespace Server.Custom
{
[SerializationGenerator(0)]
public partial class ThiefPedestal1 : ThiefPedestal { [Constructible] public ThiefPedestal1() : base(1) { } public ThiefPedestal1(Serial serial) : base(serial) { } }
[SerializationGenerator(0)]
public partial class ThiefPedestal2 : ThiefPedestal { [Constructible] public ThiefPedestal2() : base(2) { } public ThiefPedestal2(Serial serial) : base(serial) { } }
[SerializationGenerator(0)]
public partial class ThiefPedestal3 : ThiefPedestal { [Constructible] public ThiefPedestal3() : base(3) { } public ThiefPedestal3(Serial serial) : base(serial) { } }
[SerializationGenerator(0)]
public partial class ThiefPedestal4 : ThiefPedestal { [Constructible] public ThiefPedestal4() : base(4) { } public ThiefPedestal4(Serial serial) : base(serial) { } }
[SerializationGenerator(0)]
public partial class ThiefPedestal5 : ThiefPedestal { [Constructible] public ThiefPedestal5() : base(5) { } public ThiefPedestal5(Serial serial) : base(serial) { } }
[SerializationGenerator(0)]
public partial class ThiefPedestal6 : ThiefPedestal { [Constructible] public ThiefPedestal6() : base(6) { } public ThiefPedestal6(Serial serial) : base(serial) { } }
// ==========================================
// 1. THE CUSTOM BAG
// ==========================================
[SerializationGenerator(0)]
public partial class ThiefPedestalBag : Bag
public partial class ThiefPedestalProp : Item
{
// Links this bag to the pedestal that spawned it
[SerializableField(0)]
private ThiefPedestal m_Pedestal;
private Timer m_DecayTimer;
public ThiefPedestalBag(ThiefPedestal pedestal)
public ThiefPedestalProp(ThiefPedestal pedestal) : base(0xE76)
{
m_Pedestal = pedestal;
Hue = 0x8A5;
@ -44,99 +21,53 @@ namespace Server.Custom
Movable = false;
}
// Called when the player successfully heists the entire bag
public void ClearTether()
public override void OnDoubleClick(Mobile from)
{
m_Pedestal = null;
m_DecayTimer?.Stop();
m_DecayTimer = null;
}
// Triggers every time an item is removed from the bag
public override void OnItemRemoved(Item item)
{
base.OnItemRemoved(item); // Match the new single-parameter signature
// If the bag was already successfully stolen, act like a normal bag
if (m_Pedestal == null) return;
// Attempt to find the thief (if it went into their backpack, they are the RootParent)
Mobile from = item.RootParent as Mobile;
if (Items.Count == 0)
if (m_Pedestal != null && !m_Pedestal.Deleted)
{
// Mechanic 1: The bag is empty. Destroy it immediately.
if (from != null)
{
from.SendMessage(89, "As you take the last item, the bag crumbles to dust!");
}
m_Pedestal.SinkAndDestroy();
Delete();
}
else if (m_DecayTimer == null)
{
// Mechanic 2: The bag was disturbed but not emptied. Start a 5-minute decay.
if (from != null)
{
from.SendMessage(33, "The bag's magical tether has been disturbed! It begins to rapidly decay.");
}
m_DecayTimer = Timer.DelayCall(TimeSpan.FromMinutes(1), () =>
{
if (m_Pedestal != null && !Deleted)
{
m_Pedestal.SinkAndDestroy();
Delete();
}
});
m_Pedestal.OnDoubleClick(from);
}
}
public ThiefPedestalBag(Serial serial) : base(serial) { }
public ThiefPedestalProp(Serial serial) : base(serial) { }
}
// ==========================================
// 2. THE PEDESTAL
// ==========================================
[SerializationGenerator(0)]
public partial class ThiefPedestal : Item
{
[SerializableField(0)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int m_Level;
private int m_TrapDifficulty;
[SerializableField(1)]
private ThiefPedestalBag m_LootBag;
private ThiefPedestalProp m_PropBag;
// Tracks how many times a player has failed
[SerializableField(2)]
private int m_Tries;
[Constructible]
public ThiefPedestal() : this(1) { }
[Constructible]
public ThiefPedestal(int level) : base(0x32F2)
public ThiefPedestal() : base(0x32F2)
{
Movable = false;
m_Level = level;
Name = "an ancient pedestal";
//SpawnBag();
m_TrapDifficulty = Utility.RandomMinMax(1, 6);
}
public void SinkAndDestroy()
{
if (Deleted) return;
Effects.SendLocationEffect(Location, Map, 0x3728, 10); // Puff of smoke
Effects.PlaySound(Location, Map, 0x247); // Sound of grinding stone
Effects.SendLocationEffect(Location, Map, 0x3728, 10);
Effects.PlaySound(Location, Map, 0x247);
Delete();
}
public override void OnDelete()
{
// If the pedestal is deleted by a GM or Spawner, clean up the tethered bag
if (m_LootBag != null && !m_LootBag.Deleted)
if (m_PropBag != null && !m_PropBag.Deleted)
{
m_LootBag.Delete();
m_PropBag.Delete();
}
base.OnDelete();
@ -148,14 +79,13 @@ namespace Server.Custom
if (Map != null && Map != Map.Internal)
{
if (m_LootBag == null || m_LootBag.Deleted)
if (m_PropBag == null || m_PropBag.Deleted)
{
SpawnBag();
SpawnPropBag();
}
else
{
// If a GM changes the facet, move the bag with it
m_LootBag.MoveToWorld(new Point3D(X, Y, Z + 6), Map);
m_PropBag.MoveToWorld(new Point3D(X, Y, Z + 6), Map);
}
}
}
@ -164,119 +94,104 @@ namespace Server.Custom
{
base.OnLocationChange(oldLocation);
// If a GM uses the [Move command, drag the bag with the pedestal
if (Map != null && Map != Map.Internal && m_LootBag != null && !m_LootBag.Deleted)
if (Map != null && Map != Map.Internal && m_PropBag != null && !m_PropBag.Deleted)
{
m_LootBag.MoveToWorld(new Point3D(X, Y, Z + ItemData.Height), Map);
m_PropBag.MoveToWorld(new Point3D(X, Y, Z + 6), Map);
}
}
public void SpawnBag()
public void SpawnPropBag()
{
if (m_LootBag != null && !m_LootBag.Deleted) return;
if (m_PropBag != null && !m_PropBag.Deleted) return;
if (Map == null || Map == Map.Internal) return;
m_LootBag = new ThiefPedestalBag(this);
// Fill it with scaling gold and gems
m_LootBag.DropItem(new Gold(m_Level * 500, m_Level * 1000));
for (int i = 0; i < m_Level; i++)
{
m_LootBag.DropItem(Loot.RandomGem());
}
// Move the bag to the exact top of the pedestal dynamically
m_LootBag.MoveToWorld(new Point3D(X, Y, Z + 6), Map);
m_PropBag = new ThiefPedestalProp(this);
m_PropBag.MoveToWorld(new Point3D(X, Y, Z + 6), Map);
}
public override void OnDoubleClick(Mobile from)
{
if (!from.InRange(GetWorldLocation(), 1))
{
from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 1019045); // I can't reach that.
from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 1019045);
return;
}
if (m_LootBag == null || m_LootBag.Deleted)
if (m_PropBag == null || m_PropBag.Deleted)
{
from.SendMessage("The pedestal is currently empty. The mechanism is resetting.");
return;
}
// Calculate max tries: Base 5 attempts, +1 attempt for every 200 Luck
int playerLuck = Math.Max(0, from.Luck);
int maxTries = 5 + (playerLuck / 200);
if (m_Tries >= maxTries)
{
from.SendMessage(33, "Your fingers were not nimble enough and the counterweight slams down! The mechanism destroys the loot.");
m_PropBag.Delete();
m_PropBag = null;
Timer.DelayCall(TimeSpan.FromSeconds(3), SinkAndDestroy);
return;
}
from.SendMessage("You attempt to carefully swap the bag with a counterweight...");
// --- THE UNIFIED TIER MATH ---
double minSkill = m_Level switch
// 1. Snooping / Trap Check (Linear 0-125 Chance)
if (!from.CheckSkill(SkillName.Snooping, 0, 125))
{
1 => 15.0,
2 => 35.0,
3 => 55.0,
4 => 75.0,
5 => 90.0,
6 => 100.1,
_ => m_Level * 16.7
};
m_Tries++;
double maxSkill = minSkill + 25.0;
// 1. Snooping Check
if (from.Skills.Snooping.Value < minSkill)
{
from.SendMessage(33, "You don't even understand how this pressure plate works!");
TriggerTrap(from);
// If Snooping fails, roll RemoveTrap to see if they dodge the explosion/poison
if (from.CheckSkill(SkillName.RemoveTrap, 0, 125))
{
from.SendMessage(54, "You trigger a pressure plate, but your knowledge of traps allows you to pull your hands back just in time!");
}
else
{
from.SendMessage(33, "You miscalculate the exact weight needed for the pressure plate!");
TriggerTrap(from);
}
return;
}
if (!from.CheckSkill(SkillName.Snooping, minSkill, maxSkill))
// 2. Stealing Check (Linear 0-125 Chance)
if (!from.CheckSkill(SkillName.Stealing, 0, 125))
{
from.SendMessage(33, "You miscalculate the exact weight needed for the pressure plate!");
TriggerTrap(from);
m_Tries++;
from.SendMessage(33, "Your hand slips during the swap! You carefully reset your grip to try again.");
return;
}
// 2. Stealing Check
if (from.Skills.Stealing.Value < minSkill)
{
from.SendMessage(33, "Your hands aren't fast enough for a heist of this caliber!");
TriggerTrap(from);
return;
}
if (!from.CheckSkill(SkillName.Stealing, minSkill, maxSkill))
{
from.SendMessage(33, "Your hand slips during the swap!");
TriggerTrap(from);
return;
}
// 3. Success!
// 3. Success
from.SendMessage(89, "In a blur of motion, you swap the bag with a stone! The trap remains silent.");
from.PlaySound(0x248);
m_LootBag.ClearTether(); // Untether the bag so it stops decaying
m_LootBag.Movable = true;
from.AddToBackpack(m_LootBag);
m_LootBag = null;
m_PropBag.Delete();
m_PropBag = null;
ThiefPedestalLootBag loot = new ThiefPedestalLootBag();
from.AddToBackpack(loot);
Timer.DelayCall(TimeSpan.FromSeconds(3), SinkAndDestroy);
}
private void TriggerTrap(Mobile m)
{
// 50/50 chance to either deal physical damage or apply poison
// Note: The trap no longer deletes the bag! It only punishes the player's health.
if (Utility.RandomBool())
{
// --- POISON TRAP ---
m.PlaySound(0x22F); // Poison gas sound
Effects.SendLocationEffect(Location, Map, 0x3728, 15); // Puff of smoke visual
m.PlaySound(0x22F);
Effects.SendLocationEffect(Location, Map, 0x3728, 15);
Poison trapPoison = m_Level switch
Poison trapPoison = m_TrapDifficulty switch
{
1 => Poison.Lesser,
2 => Poison.Regular,
3 => Poison.Greater,
4 => Poison.Deadly,
_ => Poison.Lethal // Level 5 and 6 apply Lethal poison
_ => Poison.Lethal
};
m.ApplyPoison(m, trapPoison);
@ -284,26 +199,13 @@ namespace Server.Custom
}
else
{
// --- EXPLOSION TRAP ---
m.PlaySound(0x207); // Explosion sound
Effects.SendLocationEffect(Location, Map, 0x36BD, 15); // Large explosion visual
m.PlaySound(0x207);
Effects.SendLocationEffect(Location, Map, 0x36BD, 15);
int damage = Utility.RandomMinMax(20, 35);
// 100% Physical damage to respect UOR armor ratings
AOS.Damage(m, damage, 100, 0, 0, 0, 0);
m.SendMessage(33, "The pedestal detonates, blasting you with shrapnel!");
}
// Destroy the bag so players cannot just tank the hit and grab the loot
if (m_LootBag != null && !m_LootBag.Deleted)
{
m_LootBag.Delete();
m_LootBag = null;
m.SendMessage(33, "The trap's mechanism destroys the loot in the process!");
Timer.DelayCall(TimeSpan.FromSeconds(3), SinkAndDestroy);
}
}
public ThiefPedestal(Serial serial) : base(serial) { }
@ -311,9 +213,9 @@ namespace Server.Custom
[AfterDeserialization]
private void AfterDeserialization()
{
if (m_LootBag == null || m_LootBag.Deleted)
if (m_PropBag == null || m_PropBag.Deleted)
{
SpawnBag();
SpawnPropBag();
}
}
}

View file

@ -0,0 +1,255 @@
using System;
using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)]
public partial class ThiefPedestalLootBag : Bag
{
private static int GetWeightedTier()
{
var roll = Utility.Random(100);
if (roll < 25) return 0; // 25% chance (Rolls 0-24)
else if (roll < 55) return 1; // 30% chance (Rolls 25-54)
else if (roll < 75) return 2; // 20% chance (Rolls 55-74)
else if (roll < 90) return 3; // 15% chance (Rolls 75-89)
else if (roll < 96) return 4; // 6% chance (Rolls 90-95)
else if (roll < 99) return 5; // 3% chance (Rolls 96-98)
else return 6; // 1% chance (Rolls 99)
}
private static void GetRandomLevel(out int attributeCount, out int min, out int max)
{
var pwr = GetWeightedTier();
if (pwr == 1)
{
attributeCount = 1;
min = 10;
max = 20;
}
else if (pwr == 2)
{
attributeCount = Utility.RandomMinMax(1, 2);
min = 20;
max = 40;
}
else if (pwr == 3)
{
attributeCount = Utility.RandomMinMax(2, 3);
min = 30;
max = 60;
}
else if (pwr == 4)
{
attributeCount = Utility.RandomMinMax(2, 4);
min = 40;
max = 80;
}
else if (pwr == 5)
{
attributeCount = Utility.RandomMinMax(3, 5);
min = 60;
max = 80;
}
else if (pwr == 6)
{
attributeCount = Utility.RandomMinMax(4, 5);
min = 60;
max = 100;
}
else
{
attributeCount = 1;
min = 5;
max = 10;
}
}
private static void GetRandomResists(out int phy, out int fire, out int cold, out int poison, out int energy)
{
var pwr = GetWeightedTier();
if (pwr == 1)
{
phy = Utility.RandomMinMax(1, 3);
fire = Utility.RandomMinMax(1, 3);
cold = Utility.RandomMinMax(1, 3);
poison = Utility.RandomMinMax(1, 3);
energy = Utility.RandomMinMax(1, 3);
}
else if (pwr == 2)
{
phy = Utility.RandomMinMax(2, 5);
fire = Utility.RandomMinMax(2, 5);
cold = Utility.RandomMinMax(2, 5);
poison = Utility.RandomMinMax(2, 5);
energy = Utility.RandomMinMax(2, 5);
}
else if (pwr == 3)
{
phy = Utility.RandomMinMax(4, 7);
fire = Utility.RandomMinMax(4, 7);
cold = Utility.RandomMinMax(4, 7);
poison = Utility.RandomMinMax(4, 7);
energy = Utility.RandomMinMax(4, 7);
}
else if (pwr == 4)
{
phy = Utility.RandomMinMax(6, 10);
fire = Utility.RandomMinMax(6, 10);
cold = Utility.RandomMinMax(6, 10);
poison = Utility.RandomMinMax(6, 10);
energy = Utility.RandomMinMax(6, 10);
}
else if (pwr == 5)
{
phy = Utility.RandomMinMax(9, 12);
fire = Utility.RandomMinMax(9, 12);
cold = Utility.RandomMinMax(9, 12);
poison = Utility.RandomMinMax(9, 12);
energy = Utility.RandomMinMax(9, 12);
}
else if (pwr == 6)
{
phy = Utility.RandomMinMax(12, 15);
fire = Utility.RandomMinMax(12, 15);
cold = Utility.RandomMinMax(12, 15);
poison = Utility.RandomMinMax(12, 15);
energy = Utility.RandomMinMax(12, 15);
}
else
{
phy = 0;
fire = 0;
cold = 0;
poison = 0;
energy = 0;
}
}
[Constructible]
public ThiefPedestalLootBag()
{
int bagTier = GetWeightedTier();
int weaponCount = Utility.RandomMinMax(0, bagTier);
for (var i = 0; i < weaponCount; i++)
{
var weapon = Loot.RandomWeapon();
GetRandomLevel(out var attributeCount, out var min, out var max);
BaseRunicTool.ApplyAttributesTo(weapon, attributeCount, min, max);
DropItem(weapon);
}
int armorCount = Utility.RandomMinMax(0, bagTier);
for (var i = 0; i < armorCount; i++)
{
var armor = Loot.RandomArmorOrShield();
GetRandomLevel(out var attributeCount, out var min, out var max);
BaseRunicTool.ApplyAttributesTo(armor, attributeCount, min, max);
GetRandomResists(out var phy, out var fire, out var cold, out var poison, out var energy);
armor.PhysicalBonus = phy;
armor.FireBonus = fire;
armor.ColdBonus = cold;
armor.PoisonBonus = poison;
armor.EnergyBonus = energy;
DropItem(armor);
}
int jewelryCount = Utility.RandomMinMax(0, bagTier);
for (var i = 0; i < jewelryCount; i++)
{
var jewelry = Loot.RandomJewelry();
GetRandomLevel(out var attributeCount, out var min, out var max);
BaseRunicTool.ApplyAttributesTo(jewelry, attributeCount, min, max);
DropItem(jewelry);
}
int reagentCount = Utility.RandomMinMax(0, bagTier);
for (var i = 0; i < reagentCount; i++)
{
var reagents = Loot.RandomReagent();
reagents.Amount = Math.Max(1, GetWeightedTier() * 2);
DropItem(reagents);
}
int necroreagentCount = Utility.RandomMinMax(0, bagTier);
for (var i = 0; i < necroreagentCount; i++)
{
var necroreagents = Loot.RandomNecromancyReagent();
necroreagents.Amount = Math.Max(1, GetWeightedTier() * 2);
DropItem(necroreagents);
}
int gemCount = Utility.RandomMinMax(0, bagTier);
for (var i = 0; i < gemCount; i++)
{
var gems = Loot.RandomGem();
gems.Amount = Math.Max(1, bagTier * 2);
DropItem(gems);
}
int mageScrollCount = Utility.RandomMinMax(0, bagTier);
int mlevel = 0;
if (bagTier < 3)
{
mlevel = 42;
}
else
{
mlevel = 63;
}
for (var i = 0; i < mageScrollCount; i++)
{
var scroll = Loot.RandomScroll(0, mlevel, SpellbookType.Regular);
scroll.Amount = Math.Max(1, bagTier);
DropItem(scroll);
}
int necroScrollCount = Utility.RandomMinMax(0, bagTier);
int nlevel = 0;
if (bagTier < 3)
{
nlevel = 6;
}
else
{
nlevel = 15;
}
for (var i = 0; i < necroScrollCount; i++)
{
var scroll = Loot.RandomScroll(0, nlevel, SpellbookType.Necromancer);
scroll.Amount = Math.Max(1, bagTier);
DropItem(scroll);
}
for (var i = Math.Max(1, bagTier); i > 0; i--)
{
DropItem(Loot.RandomPotion());
}
for (var i = Math.Max(1, bagTier); i > 0; i--)
{
DropItem(Loot.RandomClothing());
}
if (bagTier > 3)
{
if (Utility.RandomBool())
{
DropItem(new Arrow((bagTier + (Utility.RandomMinMax(10, 20)))));
}
else
{
DropItem(new Bolt((bagTier + (Utility.RandomMinMax(10, 20)))));
}
DropItem(new BlankScroll(Math.Max(1, GetWeightedTier())));
}
int minGold = 100 + (bagTier * 100);
int maxGold = minGold + 99;
DropItem(new Gold(Utility.RandomMinMax(minGold, maxGold)));
}
}