#W# Added: More things to the ToAdd Folder.
This commit is contained in:
parent
cdc061dc54
commit
910cdde124
16 changed files with 3389 additions and 0 deletions
180
ToAdd/AutoBandCommand.cs
Normal file
180
ToAdd/AutoBandCommand.cs
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
/**********************************
|
||||
* Author: Expa (refactored) *
|
||||
* Script Name: AutoBandCommand.cs *
|
||||
* Target: ModernUO (.NET 10/C#14) *
|
||||
**********************************/
|
||||
using System;
|
||||
using ModernUO.Serialization;
|
||||
using Server;
|
||||
using Server.Commands;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Scripts.Commands;
|
||||
|
||||
public static class AutoBandCommand
|
||||
{
|
||||
public static void Initialize()
|
||||
{
|
||||
CommandSystem.Register("bas", AccessLevel.Player, OnBandSelf);
|
||||
CommandSystem.Register("bat", AccessLevel.Player, OnBandTarget);
|
||||
}
|
||||
|
||||
public static void OnBandSelf(CommandEventArgs e)
|
||||
{
|
||||
if (e.Mobile is not PlayerMobile pm) return;
|
||||
Start(pm, pm);
|
||||
}
|
||||
|
||||
public static void OnBandTarget(CommandEventArgs e)
|
||||
=> e.Mobile.BeginTarget(-1, false, TargetFlags.None, OnTarget);
|
||||
|
||||
private static void OnTarget(Mobile from, object targeted)
|
||||
{
|
||||
if (from is not PlayerMobile pm || targeted is not Mobile target || target.Deleted || !target.Alive)
|
||||
{
|
||||
from.SendMessage("Invalid target.");
|
||||
return;
|
||||
}
|
||||
|
||||
Start(pm, target);
|
||||
}
|
||||
|
||||
private static void Start(PlayerMobile pm, Mobile target)
|
||||
{
|
||||
if (pm.AutoBandageTimer is AutoBandageTimer existing && existing.Targets(target))
|
||||
{
|
||||
StopTimer(pm);
|
||||
SendHealthMessage(pm, $"Bandage auto-heal DISABLED{(pm == target ? "" : $" on {target.Name}")}.");
|
||||
return;
|
||||
}
|
||||
|
||||
StopTimer(pm);
|
||||
|
||||
var band = pm.Backpack?.FindItemByType<Bandage>();
|
||||
if (band == null || band.Amount <= 0)
|
||||
{
|
||||
pm.SendMessage("You have no bandages!");
|
||||
return;
|
||||
}
|
||||
|
||||
var timer = new AutoBandageTimer(pm, target);
|
||||
pm.AutoBandageTimer = timer;
|
||||
timer.Start();
|
||||
|
||||
SendHealthMessage(pm, $"Bandage auto-heal ENABLED{(pm == target ? "" : $" on {target.Name}")}. Health: {target.Hits}/{target.HitsMax}");
|
||||
}
|
||||
|
||||
internal static void SendHealthMessage(Mobile m, string msg)
|
||||
{
|
||||
var denom = Math.Max(1, m.HitsMax);
|
||||
double p = (double)m.Hits / denom;
|
||||
var hue = p < 0.3 ? 33 : p < 0.7 ? 53 : 63;
|
||||
m.SendMessage(hue, msg);
|
||||
}
|
||||
|
||||
private static void StopTimer(PlayerMobile pm)
|
||||
{
|
||||
pm.AutoBandageTimer?.Stop();
|
||||
pm.AutoBandageTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
[SerializationGenerator(0)]
|
||||
public partial class AutoBandageTimer : Timer
|
||||
{
|
||||
private const double ResumeAt = 90.0;
|
||||
private const double PauseAt = 100.0;
|
||||
private static readonly TimeSpan TickInterval = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
private readonly WeakReference<Mobile> _casterRef;
|
||||
private readonly WeakReference<Mobile> _targetRef;
|
||||
private bool _paused;
|
||||
private DateTime _next;
|
||||
|
||||
public AutoBandageTimer(Mobile caster, Mobile target)
|
||||
: base(TimeSpan.Zero, TickInterval)
|
||||
{
|
||||
_casterRef = new(caster);
|
||||
_targetRef = new(target);
|
||||
_next = Core.Now;
|
||||
}
|
||||
|
||||
public bool Targets(Mobile m)
|
||||
=> m is not null && _targetRef.TryGetTarget(out var t) && t == m;
|
||||
|
||||
protected override void OnTick()
|
||||
{
|
||||
if (!_casterRef.TryGetTarget(out var caster) || caster.Deleted || !caster.Alive || caster.Map == Map.Internal)
|
||||
{
|
||||
Stop();
|
||||
Cleanup(caster);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_targetRef.TryGetTarget(out var target) || target.Deleted || !target.Alive || target.Map == Map.Internal)
|
||||
{
|
||||
caster.SendMessage("Target is no longer valid. Automatic bandaging stopped.");
|
||||
Stop();
|
||||
Cleanup(caster);
|
||||
return;
|
||||
}
|
||||
|
||||
if (BandageContext.GetContext(caster) != null)
|
||||
return;
|
||||
|
||||
var denom = Math.Max(1, target.HitsMax);
|
||||
double hpPercent = (double)target.Hits / denom * 100.0;
|
||||
|
||||
if (!_paused && hpPercent >= PauseAt)
|
||||
{
|
||||
_paused = true;
|
||||
caster.SendMessage("Bandaging paused. Target's health is full.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_paused && hpPercent < ResumeAt)
|
||||
{
|
||||
_paused = false;
|
||||
caster.SendMessage($"Bandaging resumed. Target's health dropped below {ResumeAt}%.");
|
||||
_next = Core.Now;
|
||||
}
|
||||
|
||||
if (_paused || Core.Now < _next) return;
|
||||
|
||||
var band = caster.Backpack?.FindItemByType<Bandage>();
|
||||
if (band == null || band.Amount <= 0)
|
||||
{
|
||||
caster.SendMessage("Out of bandages. Automatic bandaging stopped.");
|
||||
Stop();
|
||||
Cleanup(caster);
|
||||
return;
|
||||
}
|
||||
|
||||
int preHits = target.Hits;
|
||||
double delaySec = 5.0 + 0.05 * Math.Max(0, 120 - caster.Dex);
|
||||
_next = Core.Now + TimeSpan.FromSeconds(delaySec);
|
||||
|
||||
Bandage.BandageTargetRequest(caster, band, target);
|
||||
Timer.DelayCall(TimeSpan.FromSeconds(delaySec * 0.95), () => ShowResult(caster, target, preHits));
|
||||
}
|
||||
|
||||
private static void ShowResult(Mobile? caster, Mobile? target, int preHits)
|
||||
{
|
||||
if (caster == null || target == null) return;
|
||||
if (caster.Deleted || !caster.Alive || target.Deleted || !target.Alive) return;
|
||||
|
||||
int healed = target.Hits - preHits;
|
||||
string msg = healed > 0
|
||||
? $"Healing {target.Name}: {target.Hits}/{target.HitsMax} HP (+{healed})"
|
||||
: $"Healing {target.Name}: {target.Hits}/{target.HitsMax} HP";
|
||||
|
||||
AutoBandCommand.SendHealthMessage(caster, msg);
|
||||
}
|
||||
|
||||
private static void Cleanup(Mobile? caster)
|
||||
{
|
||||
if (caster is PlayerMobile pm) pm.AutoBandageTimer = null;
|
||||
}
|
||||
}
|
||||
815
ToAdd/GraveRobbing/GraveRobbingCore.cs
Normal file
815
ToAdd/GraveRobbing/GraveRobbingCore.cs
Normal file
|
|
@ -0,0 +1,815 @@
|
|||
/**********************************
|
||||
* Author: Unknow *
|
||||
* Update/Fixed/Refactoring: Expa *
|
||||
* Script: GraveRobbingCore.cs *
|
||||
* Optimized for ModernUO 0.15.4.18*
|
||||
**********************************/
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using ModernUO.Serialization;
|
||||
using Server;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
using Server.Engines.Harvest;
|
||||
using Server.Network;
|
||||
|
||||
static class GRUtil
|
||||
{
|
||||
public static double GetSkillValue(Mobile m, SkillName s) => m?.Skills?[s]?.Value ?? 0.0;
|
||||
}
|
||||
|
||||
namespace Server.Engines.Harvest
|
||||
{
|
||||
[SerializationGenerator(0, false)]
|
||||
public partial class GraveRobbing : HarvestSystem
|
||||
{
|
||||
static GraveRobbing _system;
|
||||
public static GraveRobbing System => _system ??= new GraveRobbing();
|
||||
|
||||
readonly HarvestDefinition _def;
|
||||
public HarvestDefinition Definition => _def;
|
||||
|
||||
private GraveRobbing()
|
||||
{
|
||||
_def = new HarvestDefinition
|
||||
{
|
||||
BankWidth = 2,
|
||||
BankHeight = 2,
|
||||
MinTotal = 1,
|
||||
MaxTotal = 2,
|
||||
MinRespawn = TimeSpan.FromMinutes(20),
|
||||
MaxRespawn = TimeSpan.FromMinutes(30),
|
||||
Skill = SkillName.Mining,
|
||||
MaxRange = 2,
|
||||
ConsumedPerHarvest = 1,
|
||||
ConsumedPerFeluccaHarvest = 1,
|
||||
EffectActions = new[] { 13 },
|
||||
EffectSounds = new[] { 0x13E },
|
||||
EffectCounts = new[] { 1 },
|
||||
StaticTiles = new[] { 0xED3, 0xEDF, 0xEE0, 0xEE1, 0xEE2, 0xEE8 },
|
||||
RangedTiles = false,
|
||||
NoResourcesMessage = "There is nothing here to dig up.",
|
||||
FailMessage = "You dig but find nothing of value.",
|
||||
OutOfRangeMessage = 500446,
|
||||
TimedOutOfRangeMessage = 503041,
|
||||
DoubleHarvestMessage = 503042,
|
||||
PackFullMessage = 1010481,
|
||||
ToolBrokeMessage = 1044038
|
||||
};
|
||||
|
||||
_def.Resources = new[]
|
||||
{
|
||||
new HarvestResource(60, 10, 150, "You dig up some bones.", typeof(Bone)),
|
||||
new HarvestResource(40, 30, 150, "You uncover some gold.", typeof(Gold))
|
||||
};
|
||||
|
||||
_def.Veins = new[]
|
||||
{
|
||||
new HarvestVein(70, 0, _def.Resources[0], null),
|
||||
new HarvestVein(30, 0, _def.Resources[1], null)
|
||||
};
|
||||
|
||||
Definitions = new[] { _def };
|
||||
|
||||
PandoraChestConfig.Load();
|
||||
PandoraChestLogger.Initialize();
|
||||
PandoraChestSpawnGuard.Initialize();
|
||||
}
|
||||
|
||||
public override bool CheckHarvest(Mobile from, Item tool)
|
||||
{
|
||||
var region = from?.Region;
|
||||
var regionName = region?.Name ?? "(null)";
|
||||
var ok = false;
|
||||
|
||||
if (region != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (region.IsPartOf("Cemetery"))
|
||||
ok = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PandoraChestLogger.LogError("GraveRobbing.CheckHarvest.IsPartOf", ex, $"RegionName={regionName}", from);
|
||||
}
|
||||
|
||||
if (!ok && !string.IsNullOrEmpty(regionName))
|
||||
{
|
||||
var rn = regionName.ToLowerInvariant();
|
||||
if (rn.Contains("grave") || rn.Contains("graveyard") || rn.Contains("cemetery"))
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
from?.SendMessage($"You may only rob graves in a cemetery. Region: {regionName}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tool == null)
|
||||
{
|
||||
from?.SendMessage("You need to use a shovel or pickaxe to dig here.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.CheckHarvest(from, tool);
|
||||
}
|
||||
|
||||
public override void OnHarvestFinished(Mobile from, Item tool, HarvestDefinition def, HarvestVein vein, HarvestBank bank, HarvestResource resource, object harvested)
|
||||
{
|
||||
base.OnHarvestFinished(from, tool, def, vein, bank, resource, harvested);
|
||||
|
||||
GetHarvestDetails(from, tool, harvested, out _, out var map, out var loc, out _);
|
||||
|
||||
if (from != null)
|
||||
{
|
||||
from.Karma -= Utility.RandomMinMax(50, 150);
|
||||
}
|
||||
|
||||
var hour = DateTime.Now.Hour;
|
||||
var night = hour < 6 || hour > 20;
|
||||
|
||||
MoonPhase? moon = null;
|
||||
if (map != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
moon = Clock.GetMoonPhase(map, loc.X, loc.Y);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PandoraChestLogger.LogError("GraveRobbing.OnHarvestFinished.GetMoonPhase", ex, $"Map={map},Loc={loc}", from);
|
||||
moon = null;
|
||||
}
|
||||
}
|
||||
|
||||
double baseSpiritChance = 0.10;
|
||||
if (moon == MoonPhase.FullMoon) baseSpiritChance += 0.15;
|
||||
if (night) baseSpiritChance += 0.10;
|
||||
|
||||
double spiritSpeak = GRUtil.GetSkillValue(from, SkillName.SpiritSpeak);
|
||||
double necromancy = GRUtil.GetSkillValue(from, SkillName.Necromancy);
|
||||
double skillReduction = (spiritSpeak + necromancy) / 2000.0;
|
||||
double spiritChanceFinal = Math.Max(0.01, baseSpiritChance - skillReduction);
|
||||
|
||||
if (Utility.RandomDouble() < spiritChanceFinal)
|
||||
{
|
||||
var spirit = new VengefulSpirit();
|
||||
if (night)
|
||||
{
|
||||
spirit.Hits += 20;
|
||||
spirit.DamageMin += 2;
|
||||
spirit.DamageMax += 2;
|
||||
}
|
||||
|
||||
Point3D spawnLoc;
|
||||
Map spawnMap;
|
||||
if (map != null)
|
||||
{
|
||||
spawnLoc = loc;
|
||||
spawnMap = map;
|
||||
}
|
||||
else
|
||||
{
|
||||
spawnLoc = from?.Location ?? Point3D.Zero;
|
||||
spawnMap = from?.Map;
|
||||
}
|
||||
|
||||
if (spawnMap != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
spirit.MoveToWorld(spawnLoc, spawnMap);
|
||||
spirit.Combatant = from;
|
||||
spirit.Say("You should not have disturbed the dead!");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PandoraChestLogger.LogError("GraveRobbing.OnHarvestFinished.SpawnSpirit", ex, $"Player={from?.Name},Map={spawnMap},Loc={spawnLoc}", from);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (from != null && Utility.RandomDouble() < 0.25)
|
||||
{
|
||||
from.SendMessage(0x21, "A grave curse weakens you!");
|
||||
try
|
||||
{
|
||||
from.AddStatMod(new StatMod(StatType.Str, "GraveCurse", -5, TimeSpan.FromMinutes(5)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PandoraChestLogger.LogError("GraveRobbing.OnHarvestFinished.AddStatMod", ex, $"Player={from?.Name}", from);
|
||||
}
|
||||
}
|
||||
|
||||
TrySpawnPandoraChest(from, loc, map);
|
||||
}
|
||||
|
||||
void TrySpawnPandoraChest(Mobile from, Point3D loc, Map map)
|
||||
{
|
||||
if (from == null || map == null) return;
|
||||
var cfg = PandoraChestConfig.Current;
|
||||
if (!PandoraChestSpawnGuard.CanSpawnAt(map, loc, cfg)) return;
|
||||
|
||||
string acc = "(null)";
|
||||
try
|
||||
{
|
||||
acc = from?.Account?.Username ?? from?.Account?.ToString() ?? "(null)";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PandoraChestLogger.LogError("GraveRobbing.TrySpawnPandoraChest.GetAccount", ex, $"Player={from?.Name}", from);
|
||||
acc = "(null)";
|
||||
}
|
||||
|
||||
if (!PandoraChestSpawnGuard.CanSpawnForAccount(acc, cfg)) return;
|
||||
|
||||
double mining = GRUtil.GetSkillValue(from, SkillName.Mining);
|
||||
double spiritSpeak = GRUtil.GetSkillValue(from, SkillName.SpiritSpeak);
|
||||
double necro = GRUtil.GetSkillValue(from, SkillName.Necromancy);
|
||||
double spawn = cfg.Spawn.BaseSpawnChance + mining * cfg.Spawn.MiningFactor + spiritSpeak * cfg.Spawn.SpiritSpeakFactor + necro * cfg.Spawn.NecromancyFactor;
|
||||
spawn = Math.Min(spawn, cfg.Spawn.MaxSpawnChance);
|
||||
if (Utility.RandomDouble() >= spawn) return;
|
||||
|
||||
int level = DetermineChestLevel(mining, spiritSpeak, necro, cfg);
|
||||
var chest = new PandoraChest(level, from);
|
||||
try
|
||||
{
|
||||
chest.MoveToWorld(loc, map);
|
||||
chest.Populate(from, map);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PandoraChestLogger.LogError("GraveRobbing.TrySpawnPandoraChest.SpawnChest", ex, $"Player={from?.Name},Map={map},Loc={loc},Level={level}", from);
|
||||
}
|
||||
|
||||
PandoraChestLogger.LogSpawn(from, map, loc, level, spawn);
|
||||
PandoraChestSpawnGuard.RecordAt(map, loc);
|
||||
PandoraChestSpawnGuard.RecordAccount(acc);
|
||||
|
||||
double guardChance = cfg.Guardian.BaseChance + level * cfg.Guardian.LevelMultiplier;
|
||||
guardChance = Math.Min(guardChance, cfg.Guardian.MaxChance);
|
||||
if (Utility.RandomDouble() < guardChance)
|
||||
{
|
||||
int count = 1 + (level / 3);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var g = new VengefulSpirit();
|
||||
g.Hits += (int)(g.Hits * (1.0 + cfg.Guardian.HpMultiplierPerQuality * (level - 1)));
|
||||
g.DamageMin += (int)(cfg.Guardian.DamageMultiplierPerQuality * (level - 1));
|
||||
g.DamageMax += (int)(cfg.Guardian.DamageMultiplierPerQuality * (level - 1) * 2);
|
||||
try
|
||||
{
|
||||
g.MoveToWorld(new Point3D(loc.X + Utility.RandomMinMax(-2, 2), loc.Y + Utility.RandomMinMax(-2, 2), loc.Z), map);
|
||||
g.Combatant = from;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PandoraChestLogger.LogError("GraveRobbing.TrySpawnPandoraChest.SpawnGuard", ex, $"Player={from?.Name},Map={map},Loc={loc},Level={level}", from);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int DetermineChestLevel(double mining, double spirit, double necro, PandoraChestConfig cfg)
|
||||
{
|
||||
double weighted = mining * cfg.Weights.Mining + spirit * cfg.Weights.SpiritSpeak + necro * cfg.Weights.Necromancy;
|
||||
double norm = Math.Min(weighted / 120.0, 1.0);
|
||||
var q = cfg.LevelQualityBaseCached;
|
||||
int levels = Math.Max(1, Math.Min(cfg.Spawn.Levels, q.Length));
|
||||
double[] adj = new double[levels];
|
||||
for (int i = 0; i < levels; i++)
|
||||
{
|
||||
double qq = (i < q.Length) ? q[i] : q.Last();
|
||||
adj[i] = Math.Max(0.001, (0.2 + qq) * (1.0 + norm * (i / (double)levels) * 3.0));
|
||||
}
|
||||
double sum = adj.Sum();
|
||||
double r = Utility.RandomDouble() * sum;
|
||||
double acc = 0;
|
||||
for (int i = 0; i < adj.Length; i++)
|
||||
{
|
||||
acc += adj[i];
|
||||
if (r <= acc) return i + 1;
|
||||
}
|
||||
return levels;
|
||||
}
|
||||
}
|
||||
|
||||
public static class GraveRobbingInit { public static void Initialize() { _ = GraveRobbing.System; } }
|
||||
}
|
||||
|
||||
namespace Server.Items
|
||||
{
|
||||
[SerializationGenerator(0, false)]
|
||||
[Flippable(0x09AA, 0x0E7D)]
|
||||
public partial class PandoraChest : LockableContainer
|
||||
{
|
||||
int _level;
|
||||
bool _isCursed;
|
||||
bool _isTrapped;
|
||||
int _trapPower;
|
||||
Mobile _owner;
|
||||
enum TrapType { None, Explosion, Poison, SummonUndead }
|
||||
TrapType _trapType = TrapType.None;
|
||||
|
||||
[Constructible] public PandoraChest() : this(1, null) { }
|
||||
public PandoraChest(int level, Mobile owner) : base(0x09AA)
|
||||
{
|
||||
Weight = 5.0;
|
||||
LootType = LootType.Regular;
|
||||
_owner = owner;
|
||||
|
||||
var cfg = PandoraChestConfig.Current;
|
||||
_level = Math.Max(1, Math.Min(level, cfg.Spawn.Levels));
|
||||
Name = $"Pandora's Chest [L{_level}] {(_level >= 4 ? "(Jeweled)" : _level >= 3 ? "(Polished)" : "(Simple)")}";
|
||||
int hue = (cfg.HueByLevelCached != null && cfg.HueByLevelCached.Length >= _level) ? cfg.HueByLevelCached[_level - 1] : 0;
|
||||
if (hue != 0) Hue = hue;
|
||||
|
||||
_isCursed = Utility.RandomDouble() < cfg.BaseCursedChance;
|
||||
Locked = Utility.RandomDouble() < cfg.BaseLockedChance;
|
||||
_isTrapped = Utility.RandomDouble() < cfg.BaseTrappedChance;
|
||||
LockLevel = 10 * _level;
|
||||
MaxLockLevel = Math.Max(LockLevel, 120);
|
||||
RequiredSkill = 10 * _level;
|
||||
_trapPower = _isTrapped ? (10 + 15 * _level) : 0;
|
||||
if (_isCursed) Name = $"Cursed {Name}";
|
||||
}
|
||||
|
||||
public void Populate(Mobile opener, Map map) => FillWithLoot(_level, opener, map);
|
||||
|
||||
void FillWithLoot(int level, Mobile opener, Map map)
|
||||
{
|
||||
if (Items.Count > 0) return;
|
||||
|
||||
var cfg = PandoraChestConfig.Current;
|
||||
var ranges = cfg.GoldRangeByLevelCached;
|
||||
int idx = Math.Max(0, Math.Min(level - 1, ranges.Length - 1));
|
||||
int gold = Utility.RandomMinMax(ranges[idx].Min, ranges[idx].Max);
|
||||
DropItem(new Gold(gold));
|
||||
var dropped = new List<string> { $"Gold:{gold}" };
|
||||
|
||||
int minR = cfg.ReagentsMinCached.Length > idx ? cfg.ReagentsMinCached[idx] : 1;
|
||||
int maxR = cfg.ReagentsMaxCached.Length > idx ? cfg.ReagentsMaxCached[idx] : Math.Max(1, minR);
|
||||
int reagCount = Utility.RandomMinMax(minR, maxR);
|
||||
|
||||
var reagentStacks = new Dictionary<Type, int>();
|
||||
for (int i = 0; i < reagCount; i++)
|
||||
{
|
||||
var r = ReagentForLevel(level, cfg);
|
||||
if (r == null) continue;
|
||||
var t = r.GetType();
|
||||
reagentStacks.TryGetValue(t, out var cur);
|
||||
reagentStacks[t] = cur + (r.Amount > 0 ? r.Amount : 1);
|
||||
}
|
||||
|
||||
foreach (var kv in reagentStacks)
|
||||
{
|
||||
try
|
||||
{
|
||||
var inst = (Item)Activator.CreateInstance(kv.Key)!;
|
||||
try { inst.Amount = Math.Max(1, kv.Value); } catch (Exception ex) { PandoraChestLogger.LogError("PandoraChest.FillWithLoot.SetReagentAmount", ex, $"Type={kv.Key.FullName},Level={level}", opener); }
|
||||
DropItem(inst);
|
||||
dropped.Add(inst.GetType().Name + (inst.Amount > 1 ? $"x{inst.Amount}" : ""));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PandoraChestLogger.LogError("PandoraChest.FillWithLoot.CreateReagentInstance", ex, $"Type={kv.Key.FullName},Level={level}", opener);
|
||||
}
|
||||
}
|
||||
|
||||
if (level >= 4)
|
||||
{
|
||||
if (level >= 5)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bp = new BlackPearl();
|
||||
try { bp.Amount = 1; } catch (Exception ex) { PandoraChestLogger.LogError("PandoraChest.FillWithLoot.SetBlackPearlAmount", ex, $"Level={level}", opener); }
|
||||
DropItem(bp);
|
||||
dropped.Add("Guaranteed:BlackPearl");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PandoraChestLogger.LogError("PandoraChest.FillWithLoot.CreateBlackPearl", ex, $"Level={level}", opener);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
var sa = new SulfurousAsh();
|
||||
try { sa.Amount = 1; } catch (Exception ex) { PandoraChestLogger.LogError("PandoraChest.FillWithLoot.SetSulfurousAshAmount", ex, $"Level={level}", opener); }
|
||||
DropItem(sa);
|
||||
dropped.Add("Guaranteed:SulfurousAsh");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PandoraChestLogger.LogError("PandoraChest.FillWithLoot.CreateSulfurousAsh", ex, $"Level={level}", opener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Utility.Random(100) < Math.Min(95, cfg.Weights.Level * 10 + 30 + level * 8))
|
||||
{
|
||||
DropItem(new NightSightPotion());
|
||||
dropped.Add("NightSightPotion");
|
||||
}
|
||||
|
||||
if (Utility.Random(100) < Math.Min(95, 20 + level * 6))
|
||||
{
|
||||
DropItem(new LesserHealPotion());
|
||||
dropped.Add("LesserHealPotion");
|
||||
}
|
||||
|
||||
double jewelryChance = Math.Min(95.0, 8 + level * 6 + cfg.Weights.Level * 10);
|
||||
bool jewelryDropped = false;
|
||||
if (Utility.Random(100) < jewelryChance)
|
||||
{
|
||||
Item j = (level >= 4) ? MakeJeweledJewelry(level) : (level >= 3) ? MakePolishedJewelry(level) : MakeSimpleJewelry(level);
|
||||
if (j != null) { DropItem(j); dropped.Add(j.GetType().Name + ":" + (j.Name ?? "j")); jewelryDropped = true; }
|
||||
}
|
||||
|
||||
if (level >= 5 && !jewelryDropped)
|
||||
{
|
||||
var j = MakeJeweledJewelry(level);
|
||||
if (j != null) { DropItem(j); dropped.Add("GuaranteedJeweled:" + (j.Name ?? j.GetType().Name)); }
|
||||
}
|
||||
|
||||
if (Utility.Random(100) < (20 + level * 8)) { DropItem(new Bone()); dropped.Add("Bone"); }
|
||||
|
||||
if (Utility.RandomDouble() < cfg.RareDrop.BaseChance * level) { DropItem(new SpecialRareBoneItem()); dropped.Add("SpecialRareBoneItem"); }
|
||||
|
||||
if (Utility.Random(100) < 10 + level * 4) { DropItem(new Lockpick()); dropped.Add("Lockpick"); }
|
||||
|
||||
if (cfg.Magic.Enabled)
|
||||
{
|
||||
double mapMult = MapMultiplier(map, cfg);
|
||||
double luck = opener?.Luck ?? 0;
|
||||
double skillSum = (GRUtil.GetSkillValue(opener, SkillName.Mining) + GRUtil.GetSkillValue(opener, SkillName.SpiritSpeak) + GRUtil.GetSkillValue(opener, SkillName.Necromancy)) / 3.0;
|
||||
double skillFactor = 1.0 + skillSum / 200.0;
|
||||
double chance = cfg.Magic.BaseChanceByLevelMultiplier * level * mapMult * (1.0 + luck / 1000.0) * skillFactor;
|
||||
chance = Math.Min(chance, 0.95);
|
||||
if (Utility.RandomDouble() < chance)
|
||||
{
|
||||
Item? magic = null;
|
||||
try
|
||||
{
|
||||
magic = GenerateMagicItem(level, opener, cfg);
|
||||
if (magic != null)
|
||||
{
|
||||
DropItem(magic);
|
||||
PandoraChestLogger.LogMagicDrop(opener, level, magic);
|
||||
dropped.Add("Magic:" + (magic.Name ?? magic.GetType().Name));
|
||||
int minutes = Math.Max(1, cfg.Magic.DecayMinutesIfDropped);
|
||||
Timer.DelayCall(TimeSpan.FromMinutes(minutes), () =>
|
||||
{
|
||||
try { if (magic != null && !magic.Deleted && magic.Parent == this) magic.Delete(); } catch (Exception ex) { PandoraChestLogger.LogError("PandoraChest.FillWithLoot.MagicDecayDelete", ex, $"Player={opener?.Name},Level={level},Item={magic?.GetType().Name}", opener); }
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PandoraChestLogger.LogError("PandoraChest.FillWithLoot.GenerateMagicItem", ex, $"Player={opener?.Name},Level={level}", opener);
|
||||
}
|
||||
}
|
||||
|
||||
if (Utility.RandomDouble() < cfg.Magic.TreasureMapChancePerLevel * level * MapMultiplier(map, cfg))
|
||||
{
|
||||
try { var tm = new TreasureMap(level, map); DropItem(tm); dropped.Add("TreasureMap:L" + level); } catch (Exception ex) { PandoraChestLogger.LogError("PandoraChest.FillWithLoot.TreasureMap", ex, $"Player={opener?.Name},Level={level}", opener); }
|
||||
}
|
||||
}
|
||||
|
||||
var tcfg = PandoraChestConfig.Current.Trap;
|
||||
if (tcfg != null)
|
||||
{
|
||||
double mapMult = MapMultiplier(map, PandoraChestConfig.Current);
|
||||
double spirit = GRUtil.GetSkillValue(_owner, SkillName.SpiritSpeak);
|
||||
double necro = GRUtil.GetSkillValue(_owner, SkillName.Necromancy);
|
||||
double luck = _owner?.Luck ?? 0;
|
||||
double reduction = (spirit + necro) / 300.0 + luck / 2000.0;
|
||||
double trapBase = tcfg.BaseChancePerLevel * level * mapMult;
|
||||
if (Utility.RandomDouble() < Math.Max(0.02, trapBase - reduction))
|
||||
{
|
||||
try
|
||||
{
|
||||
int total = tcfg.TypeWeights.Values.Sum();
|
||||
int r = Utility.Random(total);
|
||||
foreach (var kv in tcfg.TypeWeights)
|
||||
{
|
||||
r -= kv.Value;
|
||||
if (r < 0)
|
||||
{
|
||||
if (Enum.TryParse<TrapType>(kv.Key, out var tt)) _trapType = tt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_isTrapped = true;
|
||||
_trapPower = 1 + (int)Math.Round(level * tcfg.PowerMultiplierPerLevel);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PandoraChestLogger.LogError("PandoraChest.FillWithLoot.DetermineTrap", ex, $"Player={_owner?.Name},Level={level}", _owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PandoraChestLogger.LogContents(opener, _level, dropped);
|
||||
}
|
||||
|
||||
static double MapMultiplier(Map m, PandoraChestConfig cfg)
|
||||
{
|
||||
if (m == null) return 1.0;
|
||||
var nm = m.ToString().ToLowerInvariant();
|
||||
if (nm.Contains("felucca")) return cfg.MapMultiplier.Felucca;
|
||||
if (nm.Contains("trammel")) return cfg.MapMultiplier.Trammel;
|
||||
return cfg.MapMultiplier.Trammel;
|
||||
}
|
||||
|
||||
static Item? GenerateMagicItem(int level, Mobile opener, PandoraChestConfig cfg)
|
||||
{
|
||||
try
|
||||
{
|
||||
double pick = Utility.RandomDouble();
|
||||
Item it = (pick < 0.55) ? Loot.RandomWeapon() : (pick < 0.90) ? Loot.RandomArmor() : Loot.RandomJewelry();
|
||||
if (it == null) return null;
|
||||
|
||||
it.Name = $"Enchanted {it.GetType().Name} (L{level})";
|
||||
if (level >= 4) it.Hue = 0x496;
|
||||
else if (level >= 3) it.Hue = 0x47E;
|
||||
|
||||
int baseAff = cfg.Magic.MaxAffixesByLevelBase;
|
||||
int extra = level >= 5 ? 1 : 0;
|
||||
int affixes = baseAff + extra;
|
||||
|
||||
double luck = opener?.Luck ?? 0;
|
||||
double skillSum = (GRUtil.GetSkillValue(opener, SkillName.Mining) + GRUtil.GetSkillValue(opener, SkillName.SpiritSpeak) + GRUtil.GetSkillValue(opener, SkillName.Necromancy)) / 3.0;
|
||||
double mult = 1.0 + (level - 1) * 0.15 + luck / 500.0 + skillSum / 200.0;
|
||||
|
||||
var keys = cfg.Magic.AffixRanges?.Keys.ToArray() ?? Array.Empty<string>();
|
||||
for (int i = 0; i < affixes && keys.Length > 0; i++)
|
||||
{
|
||||
var key = keys[Utility.Random(keys.Length)];
|
||||
var range = cfg.Magic.AffixRanges[key];
|
||||
int raw = Utility.RandomMinMax(range.Min, range.Max);
|
||||
int val = Math.Max(1, (int)Math.Round(raw * mult));
|
||||
ApplyAffixToItem(it, key, val);
|
||||
}
|
||||
return it;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PandoraChestLogger.LogError("PandoraChest.GenerateMagicItem", ex, $"Level={level},Player={opener?.Name}", opener);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static void ApplyAffixToItem(Item it, string affixName, int value)
|
||||
{
|
||||
if (it == null) return;
|
||||
try
|
||||
{
|
||||
if (it is BaseWeapon weapon)
|
||||
{
|
||||
if (weapon.Attributes != null)
|
||||
{
|
||||
switch (affixName)
|
||||
{
|
||||
case "BonusStr": weapon.Attributes.BonusStr += value; return;
|
||||
case "BonusDex": weapon.Attributes.BonusDex += value; return;
|
||||
case "BonusHits": weapon.Attributes.BonusHits += value; return;
|
||||
case "WeaponDamage": weapon.Attributes.WeaponDamage += value; return;
|
||||
}
|
||||
}
|
||||
|
||||
var wattrsProp = weapon.GetType().GetProperty("WeaponAttributes", BindingFlags.Public | BindingFlags.Instance);
|
||||
if (wattrsProp != null)
|
||||
{
|
||||
var wattrs = wattrsProp.GetValue(weapon);
|
||||
if (wattrs != null)
|
||||
{
|
||||
var p = wattrs.GetType().GetProperty(affixName, BindingFlags.Public | BindingFlags.Instance);
|
||||
if (p != null && p.PropertyType == typeof(int))
|
||||
{
|
||||
int old = (int)p.GetValue(wattrs);
|
||||
p.SetValue(wattrs, old + value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (it is BaseArmor armor)
|
||||
{
|
||||
if (armor.Attributes != null)
|
||||
{
|
||||
switch (affixName)
|
||||
{
|
||||
case "BonusStr": armor.Attributes.BonusStr += value; return;
|
||||
case "BonusDex": armor.Attributes.BonusDex += value; return;
|
||||
case "BonusHits": armor.Attributes.BonusHits += value; return;
|
||||
}
|
||||
}
|
||||
|
||||
var aattrsProp = armor.GetType().GetProperty("ArmorAttributes", BindingFlags.Public | BindingFlags.Instance);
|
||||
if (aattrsProp != null)
|
||||
{
|
||||
var aattrs = aattrsProp.GetValue(armor);
|
||||
if (aattrs != null)
|
||||
{
|
||||
var p = aattrs.GetType().GetProperty(affixName, BindingFlags.Public | BindingFlags.Instance);
|
||||
if (p != null && p.PropertyType == typeof(int))
|
||||
{
|
||||
int old = (int)p.GetValue(aattrs);
|
||||
p.SetValue(aattrs, old + value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (it is BaseJewel jewel)
|
||||
{
|
||||
if (jewel.Attributes != null)
|
||||
{
|
||||
switch (affixName)
|
||||
{
|
||||
case "BonusStr": jewel.Attributes.BonusStr += value; return;
|
||||
case "BonusDex": jewel.Attributes.BonusDex += value; return;
|
||||
case "BonusHits": jewel.Attributes.BonusHits += value; return;
|
||||
case "WeaponDamage": jewel.Attributes.WeaponDamage += value; return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var attrsProp = it.GetType().GetProperty("Attributes", BindingFlags.Public | BindingFlags.Instance);
|
||||
if (attrsProp != null)
|
||||
{
|
||||
var attrsObj = attrsProp.GetValue(it);
|
||||
if (attrsObj != null)
|
||||
{
|
||||
var prop = attrsObj.GetType().GetProperty(affixName, BindingFlags.Public | BindingFlags.Instance);
|
||||
if (prop != null && prop.PropertyType == typeof(int))
|
||||
{
|
||||
int old = (int)prop.GetValue(attrsObj);
|
||||
prop.SetValue(attrsObj, old + value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it.Name = $"{it.Name} [+{value} {affixName}]";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PandoraChestLogger.LogError("PandoraChest.ApplyAffixToItem", ex, $"Item={it?.GetType().FullName},Affix={affixName},Value={value}");
|
||||
try { it.Name = $"{it.Name} [+{value} {affixName}]"; } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
static Item ReagentForLevel(int level, PandoraChestConfig cfg)
|
||||
{
|
||||
int r = Utility.Random(100);
|
||||
if (level >= 5 && r < 25) return new BlackPearl();
|
||||
if (level >= 4 && r < 30) return new SulfurousAsh();
|
||||
Item[] common = { new Garlic(), new Ginseng(), new MandrakeRoot(), new Bloodmoss() };
|
||||
return common[Utility.Random(common.Length)];
|
||||
}
|
||||
|
||||
static Item MakeSimpleJewelry(int level) => Utility.RandomBool() ? new GoldRing() : new GoldNecklace();
|
||||
|
||||
static Item MakePolishedJewelry(int level)
|
||||
{
|
||||
var it = MakeSimpleJewelry(level);
|
||||
if (it is GoldRing r) { r.Name = "Polished Gold Ring"; r.Hue = 0x47E; }
|
||||
if (it is GoldNecklace n) { n.Name = "Polished Gold Necklace"; n.Hue = 0x47E; }
|
||||
return it;
|
||||
}
|
||||
|
||||
static Item MakeJeweledJewelry(int level)
|
||||
{
|
||||
var it = MakeSimpleJewelry(level);
|
||||
if (it is GoldRing r) { r.Name = "Jeweled Gold Ring"; r.Hue = 0x496; }
|
||||
if (it is GoldNecklace n) { n.Name = "Jeweled Gold Necklace"; n.Hue = 0x496; }
|
||||
return it;
|
||||
}
|
||||
|
||||
public override bool CheckLocked(Mobile from)
|
||||
{
|
||||
if (_isCursed && from != null)
|
||||
{
|
||||
double necro = GRUtil.GetSkillValue(from, SkillName.Necromancy);
|
||||
double spirit = GRUtil.GetSkillValue(from, SkillName.SpiritSpeak);
|
||||
double resist = (necro + spirit) / 240.0;
|
||||
if (Utility.RandomDouble() > resist)
|
||||
{
|
||||
from.SendMessage(0x22, "An ancient curse flares as you touch the chest...");
|
||||
try { from.ApplyPoison(from, Poison.Deadly); }
|
||||
catch (Exception ex) { PandoraChestLogger.LogError("PandoraChest.CheckLocked.ApplyPoison", ex, $"Player={from?.Name}", from); }
|
||||
}
|
||||
}
|
||||
return base.CheckLocked(from);
|
||||
}
|
||||
|
||||
public override void OnDoubleClick(Mobile from)
|
||||
{
|
||||
if (from == null || from.Deleted) return;
|
||||
if (Locked) { base.OnDoubleClick(from); return; }
|
||||
|
||||
if (_isTrapped && _trapType != TrapType.None)
|
||||
{
|
||||
double trapChance = _trapPower / 100.0;
|
||||
double spirit = GRUtil.GetSkillValue(from, SkillName.SpiritSpeak);
|
||||
double necro = GRUtil.GetSkillValue(from, SkillName.Necromancy);
|
||||
double resist = (spirit + necro) / 240.0;
|
||||
trapChance = Math.Max(0.05, trapChance - resist);
|
||||
if (Utility.RandomDouble() < trapChance) { TriggerTrap(from); _isTrapped = false; Locked = true; return; }
|
||||
}
|
||||
|
||||
if (_isCursed)
|
||||
{
|
||||
from.SendMessage(0x21, "A dark curse hits you for opening a desecrated chest!");
|
||||
try { from.AddStatMod(new StatMod(StatType.Dex, "PandoraCurseDex", -3, TimeSpan.FromMinutes(3))); }
|
||||
catch (Exception ex) { PandoraChestLogger.LogError("PandoraChest.OnDoubleClick.AddStatMod.Dex", ex, $"Player={from?.Name}", from); }
|
||||
try { from.AddStatMod(new StatMod(StatType.Str, "PandoraCurseStr", -3, TimeSpan.FromMinutes(3))); }
|
||||
catch (Exception ex) { PandoraChestLogger.LogError("PandoraChest.OnDoubleClick.AddStatMod.Str", ex, $"Player={from?.Name}", from); }
|
||||
if (Utility.RandomDouble() < 0.35) try { from.ApplyPoison(from, Poison.Lesser); } catch (Exception ex) { PandoraChestLogger.LogError("PandoraChest.OnDoubleClick.ApplyPoison", ex, $"Player={from?.Name}", from); }
|
||||
}
|
||||
|
||||
base.OnDoubleClick(from);
|
||||
}
|
||||
|
||||
void TriggerTrap(Mobile from)
|
||||
{
|
||||
if (from == null || from.Deleted) return;
|
||||
var tcfg = PandoraChestConfig.Current.Trap;
|
||||
switch (_trapType)
|
||||
{
|
||||
case TrapType.Explosion:
|
||||
try
|
||||
{
|
||||
int min = tcfg.ExplosionBaseMin + (_trapPower * 2);
|
||||
int max = tcfg.ExplosionBaseMax + (_trapPower * 4);
|
||||
int dmg = Utility.RandomMinMax(min, max);
|
||||
from.Damage(dmg);
|
||||
foreach (var m in from.GetMobilesInRange(2))
|
||||
if (m != from && m.Alive) try { m.Damage(dmg / 2); } catch (Exception ex) { PandoraChestLogger.LogError("PandoraChest.TriggerTrap.DamageNearby", ex, $"Player={from?.Name},Target={m?.Name}", from); }
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PandoraChestLogger.LogError("PandoraChest.TriggerTrap.Explosion", ex, $"Player={from?.Name},TrapPower={_trapPower},Level={_level}", from);
|
||||
}
|
||||
break;
|
||||
case TrapType.Poison:
|
||||
try
|
||||
{
|
||||
var p = _trapPower >= 3 ? Poison.Deadly : (_trapPower == 2 ? Poison.Greater : Poison.Lesser);
|
||||
from.ApplyPoison(from, p);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PandoraChestLogger.LogError("PandoraChest.TriggerTrap.Poison", ex, $"Player={from?.Name},TrapPower={_trapPower},Level={_level}", from);
|
||||
}
|
||||
break;
|
||||
case TrapType.SummonUndead:
|
||||
try
|
||||
{
|
||||
int count = Math.Max(1, PandoraChestConfig.Current.Trap.SummonPerPower * _trapPower);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var z = new VengefulSpirit();
|
||||
z.Hits += 10 * _trapPower;
|
||||
z.DamageMin += _trapPower;
|
||||
z.DamageMax += _trapPower * 2;
|
||||
try { z.MoveToWorld(from.Location, from.Map); z.Combatant = from; } catch (Exception ex) { PandoraChestLogger.LogError("PandoraChest.TriggerTrap.SummonUndead.Move", ex, $"Player={from?.Name}", from); }
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PandoraChestLogger.LogError("PandoraChest.TriggerTrap.SummonUndead", ex, $"Player={from?.Name},TrapPower={_trapPower},Level={_level}", from);
|
||||
}
|
||||
break;
|
||||
}
|
||||
try { from.SendMessage(0x22, "A hidden trap explodes as you touch the chest!"); } catch (Exception ex) { PandoraChestLogger.LogError("PandoraChest.TriggerTrap.SendMessage", ex, $"Player={from?.Name}", from); }
|
||||
}
|
||||
}
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
public partial class SpecialRareBoneItem : Item { [Constructible] public SpecialRareBoneItem() : base(0x1B) { Name = "Ancient Bone Relic"; Hue = 0x485; Weight = 1.0; } }
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
public partial class VengefulSpirit : BaseCreature
|
||||
{
|
||||
[Constructible]
|
||||
public VengefulSpirit() : base(AIType.AI_Mage, FightMode.Closest, 10, 1)
|
||||
{
|
||||
Name = "Vengeful Spirit"; Body = 26; Hue = 0x4001; SetStr(50); SetDex(50); SetInt(100); SetHits(40); SetDamage(8, 12); Fame = 4000; Karma = -4000; VirtualArmor = 30;
|
||||
}
|
||||
public override bool AlwaysAttackable => true;
|
||||
public override bool BleedImmune => true;
|
||||
}
|
||||
}
|
||||
280
ToAdd/GraveRobbing/PandoraChestConfig.cs
Normal file
280
ToAdd/GraveRobbing/PandoraChestConfig.cs
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
/**********************************
|
||||
* Author: Expa *
|
||||
* Script: PandoraChestConfig.cs *
|
||||
* Optimized for ModernUO 0.15.4.18*
|
||||
**********************************/
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
public class PandoraChestConfig
|
||||
{
|
||||
private static PandoraChestConfig _current;
|
||||
private static readonly object _lock = new();
|
||||
|
||||
public static PandoraChestConfig Current
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_current == null) Load();
|
||||
return _current!;
|
||||
}
|
||||
}
|
||||
|
||||
public SpawnSection Spawn { get; set; } = new();
|
||||
public double[] LevelQualityBase { get; set; } = new double[] { 0.0, 0.10, 0.25, 0.45, 0.85 };
|
||||
public GoldRange[] GoldRangeByLevel { get; set; } = new[]
|
||||
{
|
||||
new GoldRange{ Min = 10, Max = 60},
|
||||
new GoldRange{ Min = 30, Max = 120},
|
||||
new GoldRange{ Min = 80, Max = 300},
|
||||
new GoldRange{ Min = 200, Max = 1200},
|
||||
new GoldRange{ Min = 1500, Max = 5000}
|
||||
};
|
||||
public RareDropSection RareDrop { get; set; } = new RareDropSection();
|
||||
public ReagentsSection Reagents { get; set; } = new ReagentsSection();
|
||||
public WeightsSection Weights { get; set; } = new WeightsSection();
|
||||
public GuardianSection Guardian { get; set; } = new GuardianSection();
|
||||
public LoggingSection Logging { get; set; } = new LoggingSection();
|
||||
public MagicSection Magic { get; set; } = new MagicSection();
|
||||
public TrapSection Trap { get; set; } = new TrapSection();
|
||||
public MapMultiplierSection MapMultiplier { get; set; } = new MapMultiplierSection();
|
||||
public int[] HueByLevel { get; set; } = new int[] { 0, 0x47E, 0x47E, 0x496, 0x496 };
|
||||
public double BaseCursedChance { get; set; } = 0.10;
|
||||
public double BaseTrappedChance { get; set; } = 0.12;
|
||||
public double BaseLockedChance { get; set; } = 0.65;
|
||||
|
||||
public double[] LevelQualityBaseCached = Array.Empty<double>();
|
||||
public GoldRange[] GoldRangeByLevelCached = Array.Empty<GoldRange>();
|
||||
public int[] ReagentsMinCached = Array.Empty<int>();
|
||||
public int[] ReagentsMaxCached = Array.Empty<int>();
|
||||
public int[] HueByLevelCached = Array.Empty<int>();
|
||||
|
||||
public class SpawnSection
|
||||
{
|
||||
public int Levels { get; set; } = 5;
|
||||
public double BaseSpawnChance { get; set; } = 0.08;
|
||||
public double MiningFactor { get; set; } = 0.0006;
|
||||
public double SpiritSpeakFactor { get; set; } = 0.0008;
|
||||
public double NecromancyFactor { get; set; } = 0.0008;
|
||||
public double MaxSpawnChance { get; set; } = 0.60;
|
||||
public bool AllowIfLoggingDisabled { get; set; } = true;
|
||||
}
|
||||
|
||||
public class GoldRange
|
||||
{
|
||||
public int Min { get; set; }
|
||||
public int Max { get; set; }
|
||||
}
|
||||
|
||||
public class RareDropSection
|
||||
{
|
||||
public double BaseChance { get; set; } = 0.02;
|
||||
}
|
||||
|
||||
public class ReagentsSection
|
||||
{
|
||||
public int[] MinPerLevel { get; set; } = new int[] { 1, 2, 3, 4, 6 };
|
||||
public int[] MaxPerLevel { get; set; } = new int[] { 2, 4, 6, 8, 12 };
|
||||
}
|
||||
|
||||
public class WeightsSection
|
||||
{
|
||||
public double Mining { get; set; } = 0.6;
|
||||
public double SpiritSpeak { get; set; } = 1.2;
|
||||
public double Necromancy { get; set; } = 1.2;
|
||||
public double Level { get; set; } = 0.1;
|
||||
}
|
||||
|
||||
public class GuardianSection
|
||||
{
|
||||
public double BaseChance { get; set; } = 0.05;
|
||||
public double LevelMultiplier { get; set; } = 0.06;
|
||||
public double MaxChance { get; set; } = 0.95;
|
||||
public double HpMultiplierPerQuality { get; set; } = 1.0;
|
||||
public double DamageMultiplierPerQuality { get; set; } = 0.8;
|
||||
}
|
||||
|
||||
public class LoggingSection
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
public long MaxFileSizeBytes { get; set; } = 10 * 1024 * 1024;
|
||||
public int FlushIntervalMs { get; set; } = 500;
|
||||
public int LocationCooldownSeconds { get; set; } = 60;
|
||||
public int AccountCooldownSeconds { get; set; } = 30;
|
||||
}
|
||||
|
||||
public class MagicSection
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
public double BaseChanceByLevelMultiplier { get; set; } = 0.02;
|
||||
public int DecayMinutesIfDropped { get; set; } = 30;
|
||||
public int MaxAffixesByLevelBase { get; set; } = 1;
|
||||
public System.Collections.Generic.Dictionary<string, AffixRange> AffixRanges { get; set; } = new()
|
||||
{
|
||||
{ "BonusStr", new AffixRange{ Min = 1, Max = 5 } },
|
||||
{ "BonusDex", new AffixRange{ Min = 1, Max = 5 } },
|
||||
{ "BonusHits", new AffixRange{ Min = 1, Max = 10 } },
|
||||
{ "WeaponDamage", new AffixRange{ Min = 1, Max = 10 } }
|
||||
};
|
||||
public double TreasureMapChancePerLevel { get; set; } = 0.03;
|
||||
}
|
||||
|
||||
public class TrapSection
|
||||
{
|
||||
public double BaseChancePerLevel { get; set; } = 0.12;
|
||||
public System.Collections.Generic.Dictionary<string, int> TypeWeights { get; set; } = new()
|
||||
{
|
||||
{ "Explosion", 30 },
|
||||
{ "Poison", 40 },
|
||||
{ "SummonUndead", 30 }
|
||||
};
|
||||
public double PowerMultiplierPerLevel { get; set; } = 0.6;
|
||||
public int ExplosionBaseMin { get; set; } = 8;
|
||||
public int ExplosionBaseMax { get; set; } = 16;
|
||||
public int SummonPerPower { get; set; } = 1;
|
||||
}
|
||||
|
||||
public class AffixRange
|
||||
{
|
||||
public int Min { get; set; }
|
||||
public int Max { get; set; }
|
||||
}
|
||||
|
||||
public class MapMultiplierSection
|
||||
{
|
||||
public double Felucca { get; set; } = 1.15;
|
||||
public double Trammel { get; set; } = 1.0;
|
||||
}
|
||||
|
||||
public static void Load(string path = null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
try
|
||||
{
|
||||
string baseDir = AppDomain.CurrentDomain.BaseDirectory ?? Environment.CurrentDirectory;
|
||||
string cfgPath = path ?? Path.Combine(baseDir, "PandoraChestConfig.json");
|
||||
if (!File.Exists(cfgPath))
|
||||
{
|
||||
_current = new PandoraChestConfig();
|
||||
_current.FinalizeCache();
|
||||
return;
|
||||
}
|
||||
|
||||
string json = File.ReadAllText(cfgPath);
|
||||
var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||
var cfg = JsonSerializer.Deserialize<PandoraChestConfig>(json, opts) ?? new PandoraChestConfig();
|
||||
|
||||
cfg.Spawn ??= new SpawnSection();
|
||||
cfg.RareDrop ??= new RareDropSection();
|
||||
cfg.Reagents ??= new ReagentsSection();
|
||||
cfg.Weights ??= new WeightsSection();
|
||||
cfg.Guardian ??= new GuardianSection();
|
||||
cfg.Logging ??= new LoggingSection();
|
||||
cfg.Magic ??= new MagicSection();
|
||||
cfg.Trap ??= new TrapSection();
|
||||
cfg.MapMultiplier ??= new MapMultiplierSection();
|
||||
|
||||
if (cfg.LevelQualityBase == null) cfg.LevelQualityBase = Enumerable.Repeat(0.2, cfg.Spawn.Levels).ToArray();
|
||||
|
||||
if (cfg.GoldRangeByLevel == null || cfg.GoldRangeByLevel.Length < cfg.Spawn.Levels)
|
||||
{
|
||||
var arr = new GoldRange[cfg.Spawn.Levels];
|
||||
var src = cfg.GoldRangeByLevel ?? Array.Empty<GoldRange>();
|
||||
for (int i = 0; i < arr.Length; i++)
|
||||
arr[i] = (i < src.Length && src[i] != null) ? src[i] : new GoldRange { Min = 10, Max = 60 };
|
||||
cfg.GoldRangeByLevel = arr;
|
||||
}
|
||||
|
||||
if (cfg.Reagents.MinPerLevel == null || cfg.Reagents.MinPerLevel.Length < cfg.Spawn.Levels)
|
||||
{
|
||||
cfg.Reagents.MinPerLevel = new int[cfg.Spawn.Levels];
|
||||
for (int i = 0; i < cfg.Spawn.Levels; i++) cfg.Reagents.MinPerLevel[i] = Math.Max(1, 1 + i);
|
||||
}
|
||||
|
||||
if (cfg.Reagents.MaxPerLevel == null || cfg.Reagents.MaxPerLevel.Length < cfg.Spawn.Levels)
|
||||
{
|
||||
cfg.Reagents.MaxPerLevel = new int[cfg.Spawn.Levels];
|
||||
for (int i = 0; i < cfg.Spawn.Levels; i++) cfg.Reagents.MaxPerLevel[i] = Math.Max(cfg.Reagents.MinPerLevel[i], 2 + i * 2);
|
||||
}
|
||||
|
||||
ValidateAndClamp(cfg);
|
||||
cfg.FinalizeCache();
|
||||
_current = cfg;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { Console.Error.WriteLine($"PandoraChestConfig.Load failed: {ex}"); } catch { }
|
||||
_current = new PandoraChestConfig();
|
||||
_current.FinalizeCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Reload() => Load();
|
||||
|
||||
private static void ValidateAndClamp(PandoraChestConfig cfg)
|
||||
{
|
||||
cfg.Spawn ??= new SpawnSection();
|
||||
cfg.RareDrop ??= new RareDropSection();
|
||||
cfg.Reagents ??= new ReagentsSection();
|
||||
cfg.Weights ??= new WeightsSection();
|
||||
cfg.Guardian ??= new GuardianSection();
|
||||
cfg.Logging ??= new LoggingSection();
|
||||
cfg.Magic ??= new MagicSection();
|
||||
cfg.Trap ??= new TrapSection();
|
||||
cfg.MapMultiplier ??= new MapMultiplierSection();
|
||||
|
||||
cfg.Spawn.BaseSpawnChance = Clamp(cfg.Spawn.BaseSpawnChance, 0.0, 1.0);
|
||||
cfg.Spawn.MiningFactor = Math.Max(0.0, cfg.Spawn.MiningFactor);
|
||||
cfg.Spawn.SpiritSpeakFactor = Math.Max(0.0, cfg.Spawn.SpiritSpeakFactor);
|
||||
cfg.Spawn.NecromancyFactor = Math.Max(0.0, cfg.Spawn.NecromancyFactor);
|
||||
cfg.Spawn.MaxSpawnChance = Clamp(cfg.Spawn.MaxSpawnChance, 0.0, 1.0);
|
||||
|
||||
cfg.RareDrop.BaseChance = Clamp(cfg.RareDrop.BaseChance, 0.0, 1.0);
|
||||
cfg.BaseCursedChance = Clamp(cfg.BaseCursedChance, 0.0, 1.0);
|
||||
cfg.BaseTrappedChance = Clamp(cfg.BaseTrappedChance, 0.0, 1.0);
|
||||
cfg.BaseLockedChance = Clamp(cfg.BaseLockedChance, 0.0, 1.0);
|
||||
cfg.Guardian.BaseChance = Clamp(cfg.Guardian.BaseChance, 0.0, 1.0);
|
||||
cfg.Guardian.MaxChance = Clamp(cfg.Guardian.MaxChance, 0.0, 1.0);
|
||||
|
||||
if (cfg.GoldRangeByLevel != null)
|
||||
foreach (var gr in cfg.GoldRangeByLevel)
|
||||
if (gr != null)
|
||||
{
|
||||
if (gr.Min < 0) gr.Min = 0;
|
||||
if (gr.Max < gr.Min) gr.Max = gr.Min;
|
||||
}
|
||||
|
||||
if (cfg.Reagents.MinPerLevel == null || cfg.Reagents.MinPerLevel.Length < cfg.Spawn.Levels)
|
||||
{
|
||||
cfg.Reagents.MinPerLevel = new int[cfg.Spawn.Levels];
|
||||
for (int i = 0; i < cfg.Spawn.Levels; i++) cfg.Reagents.MinPerLevel[i] = Math.Max(1, 1 + i);
|
||||
}
|
||||
|
||||
if (cfg.Reagents.MaxPerLevel == null || cfg.Reagents.MaxPerLevel.Length < cfg.Spawn.Levels)
|
||||
{
|
||||
cfg.Reagents.MaxPerLevel = new int[cfg.Spawn.Levels];
|
||||
for (int i = 0; i < cfg.Spawn.Levels; i++) cfg.Reagents.MaxPerLevel[i] = Math.Max(cfg.Reagents.MinPerLevel[i], 2 + i * 2);
|
||||
}
|
||||
|
||||
if (cfg.Logging.MaxFileSizeBytes <= 0) cfg.Logging.MaxFileSizeBytes = 10 * 1024 * 1024;
|
||||
if (cfg.Logging.FlushIntervalMs < 50) cfg.Logging.FlushIntervalMs = 50;
|
||||
if (cfg.Logging.LocationCooldownSeconds < 1) cfg.Logging.LocationCooldownSeconds = 60;
|
||||
if (cfg.Logging.AccountCooldownSeconds < 1) cfg.Logging.AccountCooldownSeconds = 30;
|
||||
if (cfg.Magic.DecayMinutesIfDropped < 1) cfg.Magic.DecayMinutesIfDropped = 30;
|
||||
}
|
||||
|
||||
private static double Clamp(double v, double min, double max) => v < min ? min : (v > max ? max : v);
|
||||
|
||||
public void FinalizeCache()
|
||||
{
|
||||
LevelQualityBaseCached = LevelQualityBase ?? Array.Empty<double>();
|
||||
GoldRangeByLevelCached = GoldRangeByLevel ?? Array.Empty<GoldRange>();
|
||||
ReagentsMinCached = Reagents?.MinPerLevel ?? Array.Empty<int>();
|
||||
ReagentsMaxCached = Reagents?.MaxPerLevel ?? Array.Empty<int>();
|
||||
HueByLevelCached = HueByLevel ?? Array.Empty<int>();
|
||||
}
|
||||
}
|
||||
62
ToAdd/GraveRobbing/PandoraChestConfig.json
Normal file
62
ToAdd/GraveRobbing/PandoraChestConfig.json
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"Spawn": {
|
||||
"Levels": 5,
|
||||
"BaseSpawnChance": 0.08,
|
||||
"MiningFactor": 0.0006,
|
||||
"SpiritSpeakFactor": 0.0008,
|
||||
"NecromancyFactor": 0.0008,
|
||||
"MaxSpawnChance": 0.6
|
||||
},
|
||||
"LevelQualityBase": [0.0, 0.10, 0.25, 0.45, 0.85],
|
||||
"GoldRangeByLevel": [
|
||||
{ "Min": 10, "Max": 60 },
|
||||
{ "Min": 30, "Max": 120 },
|
||||
{ "Min": 80, "Max": 300 },
|
||||
{ "Min": 200, "Max": 1200 },
|
||||
{ "Min": 1500, "Max": 5000 }
|
||||
],
|
||||
"RareDrop": { "BaseChance": 0.02 },
|
||||
"Reagents": {
|
||||
"MinPerLevel": [1, 2, 3, 4, 6],
|
||||
"MaxPerLevel": [2, 4, 6, 8, 12]
|
||||
},
|
||||
"Weights": {
|
||||
"Mining": 0.6,
|
||||
"SpiritSpeak": 1.2,
|
||||
"Necromancy": 1.2,
|
||||
"Level": 0.1
|
||||
},
|
||||
"Guardian": {
|
||||
"BaseChance": 0.05,
|
||||
"LevelMultiplier": 0.06,
|
||||
"MaxChance": 0.95,
|
||||
"HpMultiplierPerQuality": 1.0,
|
||||
"DamageMultiplierPerQuality": 0.8
|
||||
},
|
||||
"Logging": {
|
||||
"Enabled": true,
|
||||
"MaxFileSizeBytes": 10485760,
|
||||
"FlushIntervalMs": 500,
|
||||
"LocationCooldownSeconds": 60,
|
||||
"AccountCooldownSeconds": 30
|
||||
},
|
||||
"Magic": {
|
||||
"Enabled": true,
|
||||
"BaseChanceByLevelMultiplier": 0.02,
|
||||
"DecayMinutesIfDropped": 30,
|
||||
"MaxAffixesByLevelBase": 1,
|
||||
"TreasureMapChancePerLevel": 0.03,
|
||||
"AffixRanges": {
|
||||
"BonusStr": { "Min": 1, "Max": 5 },
|
||||
"BonusDex": { "Min": 1, "Max": 5 },
|
||||
"BonusHits": { "Min": 1, "Max": 10 },
|
||||
"WeaponDamage": { "Min": 1, "Max": 10 },
|
||||
"ArmorPhysical": { "Min": 1, "Max": 10 }
|
||||
}
|
||||
},
|
||||
"MapMultiplier": {
|
||||
"Felucca": 1.15,
|
||||
"Trammel": 1.0
|
||||
},
|
||||
"HueByLevel": [0, 114, 114, 118, 118]
|
||||
}
|
||||
270
ToAdd/GraveRobbing/PandoraChestLogger.cs
Normal file
270
ToAdd/GraveRobbing/PandoraChestLogger.cs
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
/**********************************
|
||||
* Author: Unknow *
|
||||
* Update/Fixed/Refactoring: Expa *
|
||||
* Script: PandoraChestLogger.cs *
|
||||
* Optimized for ModernUO 0.15.4.18*
|
||||
**********************************/
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Server;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
|
||||
static class PandoraChestLogger
|
||||
{
|
||||
static readonly ConcurrentQueue<string> Q = new();
|
||||
static CancellationTokenSource _cts;
|
||||
static Task _worker;
|
||||
static string _logFile;
|
||||
static long _maxSize;
|
||||
static int _flushInterval;
|
||||
static readonly object _initLock = new();
|
||||
static readonly JsonSerializerOptions _jsonOpts = new() { WriteIndented = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
lock (_initLock)
|
||||
{
|
||||
if (_worker != null && !_worker.IsCompleted) return;
|
||||
var baseDir = AppDomain.CurrentDomain.BaseDirectory ?? Environment.CurrentDirectory;
|
||||
var logsDir = Path.Combine(baseDir, "Logs");
|
||||
try { Directory.CreateDirectory(logsDir); } catch (Exception ex) { try { Console.Error.WriteLine($"PandoraChestLogger.Initialize: CreateDirectory failed: {ex}"); } catch { } }
|
||||
_logFile = Path.Combine(logsDir, "PandoraChestSpawns.log");
|
||||
var cfg = PandoraChestConfig.Current ?? new PandoraChestConfig();
|
||||
_maxSize = cfg.Logging?.MaxFileSizeBytes ?? (10 * 1024 * 1024);
|
||||
_flushInterval = Math.Max(50, cfg.Logging?.FlushIntervalMs ?? 500);
|
||||
_cts = new CancellationTokenSource();
|
||||
_worker = Task.Run(() => Worker(_cts.Token));
|
||||
}
|
||||
}
|
||||
|
||||
static async Task Worker(CancellationToken token)
|
||||
{
|
||||
var batch = new List<string>();
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try { await Task.Delay(_flushInterval, token).ConfigureAwait(false); } catch { break; }
|
||||
batch.Clear();
|
||||
while (Q.TryDequeue(out var l)) { batch.Add(l); if (batch.Count >= 1000) break; }
|
||||
if (batch.Count == 0) continue;
|
||||
try
|
||||
{
|
||||
RotateIfNeeded();
|
||||
if (!string.IsNullOrEmpty(_logFile)) File.AppendAllLines(_logFile, batch, Encoding.UTF8);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { Console.Error.WriteLine($"PandoraChestLogger.Worker AppendAllLines failed: {ex}"); } catch { }
|
||||
foreach (var l in batch) try { Console.WriteLine(l); } catch { }
|
||||
}
|
||||
}
|
||||
var rem = new List<string>();
|
||||
while (Q.TryDequeue(out var l)) rem.Add(l);
|
||||
if (rem.Count > 0)
|
||||
{
|
||||
try { RotateIfNeeded(); if (!string.IsNullOrEmpty(_logFile)) File.AppendAllLines(_logFile, rem, Encoding.UTF8); } catch (Exception ex) { try { Console.Error.WriteLine($"PandoraChestLogger.Worker final flush failed: {ex}"); } catch { } }
|
||||
}
|
||||
}
|
||||
|
||||
static void RotateIfNeeded()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(_logFile)) return;
|
||||
var fi = new FileInfo(_logFile);
|
||||
if (fi.Exists && fi.Length > _maxSize)
|
||||
{
|
||||
var dest = _logFile + "." + DateTime.UtcNow.ToString("yyyyMMddHHmmss");
|
||||
try { File.Move(_logFile, dest); } catch (Exception ex) { try { Console.Error.WriteLine($"PandoraChestLogger.RotateIfNeeded File.Move failed: {ex}"); } catch { } }
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { try { Console.Error.WriteLine($"PandoraChestLogger.RotateIfNeeded failed: {ex}"); } catch { } }
|
||||
}
|
||||
|
||||
static string HashShort(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input)) return "(null)";
|
||||
try
|
||||
{
|
||||
using var sha = SHA256.Create();
|
||||
var h = sha.ComputeHash(Encoding.UTF8.GetBytes(input));
|
||||
var hex = BitConverter.ToString(h).Replace("-", "").ToLowerInvariant();
|
||||
return hex.Length >= 16 ? hex.Substring(0, 16) : hex;
|
||||
}
|
||||
catch (Exception ex) { try { Console.Error.WriteLine($"PandoraChestLogger.HashShort failed: {ex}"); } catch { } return "(err)"; }
|
||||
}
|
||||
|
||||
static void EnqueueStructured(object obj)
|
||||
{
|
||||
try
|
||||
{
|
||||
var line = JsonSerializer.Serialize(obj, _jsonOpts);
|
||||
Q.Enqueue(line);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { Console.Error.WriteLine($"PandoraChestLogger.EnqueueStructured failed: {ex}"); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogInfo(string tag, string context = null, Mobile who = null, object extra = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entry = new
|
||||
{
|
||||
timestamp = DateTime.UtcNow,
|
||||
level = "INFO",
|
||||
tag,
|
||||
context,
|
||||
player = who?.Name ?? "(null)",
|
||||
accountHash = SafeAccountHash(who),
|
||||
extra
|
||||
};
|
||||
EnqueueStructured(entry);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public static void LogWarn(string tag, string context = null, Mobile who = null, object extra = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entry = new
|
||||
{
|
||||
timestamp = DateTime.UtcNow,
|
||||
level = "WARN",
|
||||
tag,
|
||||
context,
|
||||
player = who?.Name ?? "(null)",
|
||||
accountHash = SafeAccountHash(who),
|
||||
extra
|
||||
};
|
||||
EnqueueStructured(entry);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public static void LogError(string tag, Exception ex, string context = null, Mobile who = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entry = new
|
||||
{
|
||||
timestamp = DateTime.UtcNow,
|
||||
level = "ERROR",
|
||||
tag,
|
||||
context,
|
||||
player = who?.Name ?? "(null)",
|
||||
accountHash = SafeAccountHash(who),
|
||||
exception = ex?.ToString()
|
||||
};
|
||||
EnqueueStructured(entry);
|
||||
}
|
||||
catch (Exception inner)
|
||||
{
|
||||
try { Console.Error.WriteLine($"PandoraChestLogger.LogError failed to enqueue: {inner}. Original: {tag} {context} {ex}"); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
static string SafeAccountHash(Mobile who)
|
||||
{
|
||||
try { var acc = who?.Account?.Username ?? who?.Account?.ToString() ?? "(null)"; return HashShort(acc); } catch { return "(err)"; }
|
||||
}
|
||||
|
||||
public static void LogSpawn(Mobile who, Map map, Point3D loc, int level, double spawnChance)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!PandoraChestConfig.Current.Logging.Enabled) return;
|
||||
}
|
||||
catch { /* best-effort: fall through */ }
|
||||
|
||||
try
|
||||
{
|
||||
var entry = new
|
||||
{
|
||||
timestamp = DateTime.UtcNow,
|
||||
level = "INFO",
|
||||
tag = "SPAWN",
|
||||
player = who?.Name ?? "(null)",
|
||||
accountHash = SafeAccountHash(who),
|
||||
map = map?.ToString(),
|
||||
loc = new { x = loc.X, y = loc.Y, z = loc.Z },
|
||||
levelNumber = level,
|
||||
chance = spawnChance
|
||||
};
|
||||
EnqueueStructured(entry);
|
||||
}
|
||||
catch (Exception ex) { LogError("PandoraChestLogger.LogSpawn", ex, null, who); }
|
||||
}
|
||||
|
||||
public static void LogContents(Mobile who, int level, IEnumerable<string> items)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!PandoraChestConfig.Current.Logging.Enabled) return;
|
||||
}
|
||||
catch { }
|
||||
|
||||
try
|
||||
{
|
||||
var entry = new
|
||||
{
|
||||
timestamp = DateTime.UtcNow,
|
||||
level = "INFO",
|
||||
tag = "CONTENTS",
|
||||
player = who?.Name ?? "(null)",
|
||||
accountHash = SafeAccountHash(who),
|
||||
levelNumber = level,
|
||||
items = items
|
||||
};
|
||||
EnqueueStructured(entry);
|
||||
}
|
||||
catch (Exception ex) { LogError("PandoraChestLogger.LogContents", ex, null, who); }
|
||||
}
|
||||
|
||||
public static void LogMagicDrop(Mobile who, int level, Item it)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!PandoraChestConfig.Current.Logging.Enabled) return;
|
||||
}
|
||||
catch { }
|
||||
|
||||
try
|
||||
{
|
||||
var entry = new
|
||||
{
|
||||
timestamp = DateTime.UtcNow,
|
||||
level = "INFO",
|
||||
tag = "MAGIC",
|
||||
player = who?.Name ?? "(null)",
|
||||
accountHash = SafeAccountHash(who),
|
||||
levelNumber = level,
|
||||
itemType = it?.GetType().Name,
|
||||
itemName = it?.Name,
|
||||
hue = it?.Hue ?? 0
|
||||
};
|
||||
EnqueueStructured(entry);
|
||||
}
|
||||
catch (Exception ex) { LogError("PandoraChestLogger.LogMagicDrop", ex, null, who); }
|
||||
}
|
||||
|
||||
public static void Shutdown(int waitMs = 3000)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_worker?.Wait(waitMs);
|
||||
}
|
||||
catch (Exception ex) { try { Console.Error.WriteLine($"PandoraChestLogger.Shutdown error: {ex}"); } catch { } }
|
||||
}
|
||||
}
|
||||
66
ToAdd/GraveRobbing/PandoraChestSpawnGuard.cs
Normal file
66
ToAdd/GraveRobbing/PandoraChestSpawnGuard.cs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/**********************************
|
||||
* Author: Unknow *
|
||||
* Update/Fixed/Refactoring: Expa *
|
||||
* Script: PandoraChestSpawnGuard.cs*
|
||||
* Optimized for ModernUO 0.15.4.18 *
|
||||
***********************************/
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Server;
|
||||
|
||||
static class PandoraChestSpawnGuard
|
||||
{
|
||||
static readonly ConcurrentDictionary<string, DateTime> locs = new();
|
||||
static readonly ConcurrentDictionary<string, DateTime> accs = new();
|
||||
|
||||
public static void Initialize() => Task.Run(Cleanup);
|
||||
|
||||
static async Task Cleanup()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(30000).ConfigureAwait(false);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
foreach (var kv in locs.ToArray())
|
||||
{
|
||||
if ((now - kv.Value).TotalSeconds > 3600) locs.TryRemove(kv.Key, out _);
|
||||
}
|
||||
|
||||
foreach (var kv in accs.ToArray())
|
||||
{
|
||||
if ((now - kv.Value).TotalSeconds > 3600) accs.TryRemove(kv.Key, out _);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
static string LocKey(Map? m, Point3D p, int bucket = 2)
|
||||
{
|
||||
if (m == null) return $"(null):{p.X / bucket}:{p.Y / bucket}";
|
||||
return $"{m}:{p.X / bucket}:{p.Y / bucket}";
|
||||
}
|
||||
|
||||
public static bool CanSpawnAt(Map m, Point3D p, PandoraChestConfig cfg)
|
||||
{
|
||||
var k = LocKey(m, p);
|
||||
if (locs.TryGetValue(k, out var t) && (DateTime.UtcNow - t).TotalSeconds < cfg.Logging.LocationCooldownSeconds) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void RecordAt(Map m, Point3D p) => locs[LocKey(m, p)] = DateTime.UtcNow;
|
||||
|
||||
public static bool CanSpawnForAccount(string a, PandoraChestConfig cfg)
|
||||
{
|
||||
if (string.IsNullOrEmpty(a)) a = "(null)";
|
||||
if (accs.TryGetValue(a, out var t) && (DateTime.UtcNow - t).TotalSeconds < cfg.Logging.AccountCooldownSeconds) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void RecordAccount(string a) { if (string.IsNullOrEmpty(a)) a = "(null)"; accs[a] = DateTime.UtcNow; }
|
||||
}
|
||||
220
ToAdd/LockTrapBox.cs
Normal file
220
ToAdd/LockTrapBox.cs
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
/***********************************
|
||||
* Author: Zik (12/21/2013) *
|
||||
* Refactoring: Expa (05/08/2026) *
|
||||
* Optimized for ModernUO 0.15.6.48 *
|
||||
***********************************/
|
||||
using ModernUO.Serialization;
|
||||
using Server;
|
||||
using Server.Mobiles;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Server.Items;
|
||||
|
||||
public static class TrainingConfig
|
||||
{
|
||||
public const int MaxDistance = 1;
|
||||
public const int AntiMacroDelayMs = 5000;
|
||||
public const double MaxSkill = 100.0;
|
||||
public const double MaxSkillDifficulty = 100.0;
|
||||
}
|
||||
|
||||
public abstract class SkillTrainingBoxBase : LockableContainer
|
||||
{
|
||||
protected SkillTrainingBoxBase(int itemID) : base(itemID)
|
||||
{
|
||||
RequiredSkill = 0;
|
||||
Movable = false;
|
||||
}
|
||||
|
||||
protected SkillTrainingBoxBase(Serial serial) : base(serial)
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class TrainTimeRecord
|
||||
{
|
||||
public DateTime LastTime { get; set; }
|
||||
}
|
||||
|
||||
private static readonly ConditionalWeakTable<Mobile, TrainTimeRecord> _lastTrainTime = new();
|
||||
private static readonly object _syncLock = new();
|
||||
|
||||
protected bool ValidateTraining(Mobile from)
|
||||
{
|
||||
if (!from.Alive)
|
||||
{
|
||||
from.SendMessage("You are dead.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!InRange(from.Location, TrainingConfig.MaxDistance))
|
||||
{
|
||||
from.SendMessage("You are too far away.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return CheckAntiMacro(from);
|
||||
}
|
||||
|
||||
private static bool CheckAntiMacro(Mobile from)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
if (_lastTrainTime.TryGetValue(from, out var record))
|
||||
{
|
||||
if ((now - record.LastTime).TotalMilliseconds < TrainingConfig.AntiMacroDelayMs)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
protected static void UpdateTrainingTime(Mobile from)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_lastTrainTime.TryGetValue(from, out var record))
|
||||
record.LastTime = DateTime.UtcNow;
|
||||
else
|
||||
_lastTrainTime.Add(from, new TrainTimeRecord { LastTime = DateTime.UtcNow });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[SerializationGenerator(1, false)]
|
||||
[Flippable(0xE86, 0xE85)]
|
||||
public sealed partial class LockpickTrainingBox : SkillTrainingBoxBase
|
||||
{
|
||||
[Constructible]
|
||||
public LockpickTrainingBox() : base(0x9A8)
|
||||
{
|
||||
Name = "Lockpick Training chest";
|
||||
Locked = true;
|
||||
LockLevel = 1;
|
||||
}
|
||||
|
||||
public LockpickTrainingBox(Serial serial) : base(serial)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnDoubleClick(Mobile from)
|
||||
{
|
||||
if (!ValidateTraining(from))
|
||||
return;
|
||||
|
||||
var skill = from.Skills.Lockpicking;
|
||||
|
||||
if (skill.Base >= TrainingConfig.MaxSkill)
|
||||
{
|
||||
from.SendMessage($"Your Lockpicking skill is already at the maximum level of {TrainingConfig.MaxSkill}.");
|
||||
return;
|
||||
}
|
||||
|
||||
var skillValue = Math.Min(skill.Value, TrainingConfig.MaxSkillDifficulty);
|
||||
RequiredSkill = (int)skillValue;
|
||||
LockLevel = RequiredSkill;
|
||||
from.SendMessage($"Training Chest adjusted to {skillValue:F1} Lockpicking");
|
||||
Locked = true;
|
||||
UpdateTrainingTime(from);
|
||||
}
|
||||
|
||||
public override void LockPick(Mobile from)
|
||||
{
|
||||
if (!ValidateTraining(from))
|
||||
return;
|
||||
|
||||
var difficulty = RequiredSkill > 0 ? RequiredSkill : 50;
|
||||
|
||||
from.CheckSkill(SkillName.Lockpicking, difficulty * 0.8, difficulty * 1.2);
|
||||
|
||||
Locked = true;
|
||||
}
|
||||
|
||||
public override void OnSingleClick(Mobile from)
|
||||
{
|
||||
LabelTo(from, $"Set to {RequiredSkill} Lockpicking");
|
||||
LabelTo(from, Name);
|
||||
from.SendMessage("Double click the box to set it to your Lockpicking Skill.");
|
||||
}
|
||||
|
||||
private void Deserialize(IGenericReader reader, int version)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[SerializationGenerator(1, false)]
|
||||
[Flippable(0xE86, 0xE85)]
|
||||
public sealed partial class RemoveTrapTrainingBox : SkillTrainingBoxBase
|
||||
{
|
||||
[Constructible]
|
||||
public RemoveTrapTrainingBox() : base(0x9A8)
|
||||
{
|
||||
Name = "Remove Trap Training chest";
|
||||
Locked = false;
|
||||
LockLevel = 0;
|
||||
TrapOnLockpick = true;
|
||||
TrapLevel = 1;
|
||||
TrapType = TrapType.ExplosionTrap;
|
||||
}
|
||||
|
||||
public RemoveTrapTrainingBox(Serial serial) : base(serial)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnDoubleClick(Mobile from)
|
||||
{
|
||||
if (!ValidateTraining(from))
|
||||
return;
|
||||
|
||||
var skill = from.Skills.RemoveTrap;
|
||||
|
||||
if (skill.Base >= TrainingConfig.MaxSkill)
|
||||
{
|
||||
from.SendMessage($"Your Remove Trap skill is already at the maximum level of {TrainingConfig.MaxSkill}.");
|
||||
return;
|
||||
}
|
||||
|
||||
var skillValue = Math.Min(skill.Value, TrainingConfig.MaxSkillDifficulty);
|
||||
RequiredSkill = (int)skillValue;
|
||||
TrapLevel = RequiredSkill;
|
||||
from.SendMessage($"Training Chest adjusted to {skillValue:F1} Remove Trap");
|
||||
Locked = false;
|
||||
TrapOnLockpick = true;
|
||||
TrapType = TrapType.ExplosionTrap;
|
||||
UpdateTrainingTime(from);
|
||||
}
|
||||
|
||||
public override void LockPick(Mobile from)
|
||||
{
|
||||
if (!ValidateTraining(from))
|
||||
return;
|
||||
|
||||
var difficulty = TrapLevel > 0 ? TrapLevel : 50;
|
||||
|
||||
if (from.CheckSkill(SkillName.RemoveTrap, difficulty * 0.8, difficulty * 1.2))
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
ExecuteTrap(from);
|
||||
}
|
||||
|
||||
TrapType = TrapType.ExplosionTrap;
|
||||
TrapLevel = RequiredSkill > 0 ? RequiredSkill : 1;
|
||||
TrapOnLockpick = true;
|
||||
}
|
||||
|
||||
public override void OnSingleClick(Mobile from)
|
||||
{
|
||||
LabelTo(from, $"Set to {RequiredSkill} Remove Trap");
|
||||
LabelTo(from, Name);
|
||||
from.SendMessage("Double click the box to set it to your Remove Trap Skill.");
|
||||
}
|
||||
|
||||
private void Deserialize(IGenericReader reader, int version)
|
||||
{
|
||||
}
|
||||
}
|
||||
83
ToAdd/LoginStats.cs
Normal file
83
ToAdd/LoginStats.cs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/*********************************
|
||||
* Author: Anna *
|
||||
* Fixed and Refactoring: Expa *
|
||||
* Optimized ModernUO .Net10/C#14 *
|
||||
**********************************/
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Diagnostics;
|
||||
using Server;
|
||||
using Server.Mobiles;
|
||||
using Server.Network;
|
||||
|
||||
namespace Server.Misc;
|
||||
|
||||
public static class LoginStats
|
||||
{
|
||||
static readonly TimeSpan ReadyDelay = TimeSpan.FromSeconds(1);
|
||||
static readonly TimeSpan InvulDuration = TimeSpan.FromSeconds(10);
|
||||
const int HuePersonal = 0x35, HueGlobalLoginLogout = 0x3B2, HueDisconnected = 0x22;
|
||||
const int MaxReadyAttempts = 10;
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
EventSink.Connected += OnConnected;
|
||||
EventSink.BeforeDisconnected += OnBeforeDisconnected;
|
||||
EventSink.Logout += OnDisconnect;
|
||||
EventSink.Disconnected += OnDisconnect;
|
||||
Trace.WriteLine("[LoginStats] initialized.");
|
||||
}
|
||||
|
||||
static void OnConnected(Mobile m)
|
||||
{
|
||||
if (m is PlayerMobile pm && pm.AccessLevel == AccessLevel.Player)
|
||||
Timer.StartTimer(ReadyDelay, () => HandleLogin(pm, 0));
|
||||
}
|
||||
|
||||
static void OnBeforeDisconnected(Mobile m)
|
||||
{
|
||||
if (m is PlayerMobile pm)
|
||||
try { pm.NetState?.Disconnect("Connection lost"); } catch { }
|
||||
}
|
||||
|
||||
static void OnDisconnect(Mobile m)
|
||||
{
|
||||
if (m is PlayerMobile pm)
|
||||
{
|
||||
World.Broadcast(HueDisconnected, false, $"{pm.Name} has disconnected.");
|
||||
Trace.WriteLine($"[LoginStats] Disconnected: {pm.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
static void HandleLogin(PlayerMobile pm, int attempt)
|
||||
{
|
||||
if (pm == null || pm.Deleted) return;
|
||||
if ((pm.NetState == null || pm.SessionStart == default) && attempt < MaxReadyAttempts)
|
||||
{
|
||||
Timer.StartTimer(ReadyDelay, () => HandleLogin(pm, attempt + 1)); return;
|
||||
}
|
||||
if (pm.NetState == null || pm.SessionStart == default) return;
|
||||
|
||||
var gt = pm.GameTime;
|
||||
if (gt.TotalDays > 10000) gt = TimeSpan.Zero;
|
||||
pm.SendMessage(HuePersonal, $"Welcome back {pm.Name}! Total play time: {FormatPlaytime(gt)}");
|
||||
|
||||
Timer.StartTimer(TimeSpan.FromMilliseconds(500), () =>
|
||||
{
|
||||
if (pm == null || pm.Deleted) return;
|
||||
var text = $"{pm.Name} has logged in.";
|
||||
foreach (var ns in NetState.Instances.ToArray())
|
||||
{
|
||||
var mob = ns?.Mobile;
|
||||
if (mob == null || mob == pm) continue;
|
||||
try { mob.SendMessage(HueGlobalLoginLogout, text); } catch { }
|
||||
}
|
||||
});
|
||||
|
||||
pm.Hidden = true; pm.Blessed = true;
|
||||
Timer.StartTimer(InvulDuration, () => { if (pm != null && !pm.Deleted) { pm.Hidden = false; pm.Blessed = false; } });
|
||||
Trace.WriteLine($"[LoginStats] Login: {pm.Name}");
|
||||
}
|
||||
|
||||
static string FormatPlaytime(TimeSpan ts) => $"{ts.Days}d {ts.Hours}h {ts.Minutes}m";
|
||||
}
|
||||
BIN
ToAdd/PirateSystem.zip
Normal file
BIN
ToAdd/PirateSystem.zip
Normal file
Binary file not shown.
239
ToAdd/SecretDoorLink.cs
Normal file
239
ToAdd/SecretDoorLink.cs
Normal file
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
ToAdd/SquireSystem.zip
Normal file
BIN
ToAdd/SquireSystem.zip
Normal file
Binary file not shown.
309
ToAdd/Traps/CloneTrap.cs
Normal file
309
ToAdd/Traps/CloneTrap.cs
Normal file
|
|
@ -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<Mobile>();
|
||||
if (_victim is not null)
|
||||
Delete();
|
||||
}
|
||||
|
||||
private sealed class MirrorCloneTimer : Timer
|
||||
{
|
||||
private WeakReference<MirrorClone> _ref;
|
||||
|
||||
public MirrorCloneTimer(MirrorClone clone)
|
||||
: base(TimeSpan.FromSeconds(Utility.Random(TimerMin, TimerMax)))
|
||||
{
|
||||
_ref = new WeakReference<MirrorClone>(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<Type, Func<Item>> _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<Item> 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<Func<Item>>(conv);
|
||||
return lambda.Compile();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
301
ToAdd/Traps/CurseItemsTrap.cs
Normal file
301
ToAdd/Traps/CurseItemsTrap.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
167
ToAdd/Traps/LandMobTrap.cs
Normal file
167
ToAdd/Traps/LandMobTrap.cs
Normal file
|
|
@ -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<BaseCreature>(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() { }
|
||||
}
|
||||
215
ToAdd/Traps/MagnetItemsTrap.cs
Normal file
215
ToAdd/Traps/MagnetItemsTrap.cs
Normal file
|
|
@ -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<PlayerMobile, (int Count, DateTime LastSeen)> _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<string> 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<Item> source, Func<Item, bool> 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<PlayerMobile>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
182
ToAdd/Traps/SealedItemsTrap.cs
Normal file
182
ToAdd/Traps/SealedItemsTrap.cs
Normal file
|
|
@ -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("<BASEFONT COLOR=#FF0000>Sealed</BASEFONT>");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue