Merge pull request #42 from RebirthUO/feat/issue-35-purge-magic

feat: implement Purge Magic
This commit is contained in:
Crome696 2026-07-09 22:36:03 +02:00 committed by GitHub
commit fa3b9024cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 943 additions and 7 deletions

View file

@ -0,0 +1,432 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Server.Engines.BuffIcons;
using Server.Items;
using Server.Mobiles;
using Server.Spells;
using Server.Spells.Fifth;
using Server.Spells.First;
using Server.Spells.Mysticism;
using Server.Spells.Second;
using Server.Spells.Third;
using Xunit;
namespace Server.Tests.Spells.Mysticism;
[Collection("Sequential UOContent Tests")]
public class PurgeMagicSpellTests
{
private static Mobile NewMobile()
{
var m = new Mobile(World.NewMobile);
m.DefaultMobileInit();
m.InitStats(100, 100, 100);
m.Hits = 100;
m.Mana = 100;
return m;
}
private static Mobile NewMysticCaster()
{
var caster = NewMobile();
caster.Skills.Mysticism.Base = 120.0;
caster.Skills.Focus.Base = 120.0;
caster.Skills.Imbuing.Base = 0.0;
return caster;
}
[Fact]
public void SpellMetadata_MatchesSecondCircleMysticismSources()
{
var caster = NewMysticCaster();
var spell = new PurgeMagicSpell(caster);
Assert.Equal("Purge Magic", spell.Name);
Assert.Equal("An Ort Sanct", spell.Mantra);
Assert.Equal(SpellCircle.Second, spell.Circle);
Assert.Equal(TimeSpan.FromSeconds(0.75), spell.CastDelayBase);
Assert.Equal(6, spell.GetMana());
Assert.Equal(8.0, spell.RequiredSkill);
Assert.Equal(SkillName.Mysticism, spell.CastSkill);
Assert.Equal(
[Reagent.FertileDirt, Reagent.Garlic, Reagent.MandrakeRoot, Reagent.SulfurousAsh],
spell.Reagents
);
caster.Delete();
}
[Fact]
public void MysticSpellbookAndScroll_UsePurgeMagicSlot()
{
var book = new MysticSpellbook(1UL << (679 - 677));
var scroll = new PurgeMagicScroll();
Assert.Equal(677, book.BookOffset);
Assert.Equal(16, book.BookCount);
Assert.True(book.HasSpell(679));
Assert.Equal(679, GetSpellScrollId(scroll));
book.Delete();
scroll.Delete();
}
[Fact]
public void RegisterMysticism_PreSa_DoesNotExposePurgeMagic()
{
var previousExpansion = Core.Expansion;
var caster = NewMysticCaster();
try
{
ResetSpellRegistry();
Core.Expansion = Expansion.ML;
Initializer.Configure();
Assert.Null(SpellRegistry.NewSpell(679, caster, null));
Assert.Equal(-1, SpellRegistry.GetRegistryNumber(typeof(PurgeMagicSpell)));
}
finally
{
Core.Expansion = previousExpansion;
ResetSpellRegistry();
Initializer.Configure();
caster.Delete();
}
}
[Fact]
public void RegisterMysticism_Sa_ExposesPurgeMagicAtSpellId679()
{
var previousExpansion = Core.Expansion;
var caster = NewMysticCaster();
try
{
ResetSpellRegistry();
Core.Expansion = Expansion.SA;
Initializer.Configure();
Assert.Same(typeof(PurgeMagicSpell), SpellRegistry.Types[679]);
Assert.Equal(679, SpellRegistry.GetRegistryNumber(typeof(PurgeMagicSpell)));
Assert.IsType<PurgeMagicSpell>(SpellRegistry.NewSpell(679, caster, null));
Assert.IsType<EagleStrikeSpell>(SpellRegistry.NewSpell(682, caster, null));
}
finally
{
Core.Expansion = previousExpansion;
ResetSpellRegistry();
Initializer.Configure();
caster.Delete();
}
}
[Theory]
[InlineData(PurgeMagicSpell.PurgeWardType.MagicReflection)]
[InlineData(PurgeMagicSpell.PurgeWardType.Protection)]
[InlineData(PurgeMagicSpell.PurgeWardType.ReactiveArmor)]
[InlineData(PurgeMagicSpell.PurgeWardType.Bless)]
public void ApplyPurge_RemovesSupportedWard(PurgeMagicSpell.PurgeWardType wardType)
{
var caster = NewMysticCaster();
var target = NewMobile();
target.Skills.MagicResist.Base = 0.0;
try
{
ApplyWard(wardType, target);
Assert.True(HasWard(wardType, target));
using var random = new PredictableRandom(20);
Assert.True(PurgeMagicSpell.ApplyPurge(caster, target));
Assert.False(HasWard(wardType, target));
}
finally
{
PurgeMagicSpell.ClearState(target);
caster.Delete();
target.Delete();
}
}
[Fact]
public void ApplyPurge_RemovesExactlyOneSupportedWard()
{
var caster = NewMysticCaster();
var target = NewMobile();
target.Skills.MagicResist.Base = 0.0;
try
{
ApplyWard(PurgeMagicSpell.PurgeWardType.Protection, target);
ApplyWard(PurgeMagicSpell.PurgeWardType.ReactiveArmor, target);
using var random = new PredictableRandom(20);
Assert.True(PurgeMagicSpell.ApplyPurge(caster, target));
var remaining = 0;
remaining += ProtectionSpell.HasEffect(target) ? 1 : 0;
remaining += ReactiveArmorSpell.HasAosEffect(target) ? 1 : 0;
Assert.Equal(1, remaining);
}
finally
{
PurgeMagicSpell.ClearState(target);
ProtectionSpell.EndProtection(target);
ReactiveArmorSpell.EndArmor(target);
caster.Delete();
target.Delete();
}
}
[Fact]
public void ApplyPurge_ReappliedWardBypassesStandardImmunityForThatWard()
{
var caster = NewMysticCaster();
var target = NewMobile();
target.Skills.MagicResist.Base = 0.0;
try
{
ApplyWard(PurgeMagicSpell.PurgeWardType.Protection, target);
using (new PredictableRandom(20))
{
Assert.True(PurgeMagicSpell.ApplyPurge(caster, target));
}
Assert.False(ProtectionSpell.HasEffect(target));
Assert.True(PurgeMagicSpell.IsImmuneToPurge(target, null));
ApplyWard(PurgeMagicSpell.PurgeWardType.Protection, target);
using (new PredictableRandom(20))
{
Assert.True(PurgeMagicSpell.ApplyPurge(caster, target));
}
Assert.False(ProtectionSpell.HasEffect(target));
}
finally
{
PurgeMagicSpell.ClearState(target);
ProtectionSpell.EndProtection(target);
caster.Delete();
target.Delete();
}
}
[Fact]
public void ApplyPurge_StandardImmunityWithoutReappliedWardBlocksDisruption()
{
var caster = NewMysticCaster();
var target = NewMobile();
target.Skills.MagicResist.Base = 0.0;
try
{
ApplyWard(PurgeMagicSpell.PurgeWardType.Protection, target);
using (new PredictableRandom(20))
{
Assert.True(PurgeMagicSpell.ApplyPurge(caster, target));
}
Assert.True(PurgeMagicSpell.IsImmuneToPurge(target, null));
Assert.False(PurgeMagicSpell.ApplyPurge(caster, target));
Assert.False(PurgeMagicSpell.IsManaDisrupted(target));
}
finally
{
PurgeMagicSpell.ClearState(target);
caster.Delete();
target.Delete();
}
}
[Fact]
public void ApplyPurge_WithoutWard_AppliesManaDisruptionAndOutgoingDamageClearsIt()
{
var caster = NewMysticCaster();
var target = NewMobile();
var defender = NewMobile();
try
{
Assert.True(PurgeMagicSpell.ApplyPurge(caster, target));
Assert.True(PurgeMagicSpell.IsManaDisrupted(target));
Assert.True(new NetherBoltSpell(target).ScaleMana(10) > 10);
var hits = target.Hits;
PurgeMagicSpell.OnMobileDamaged(target, defender, 1);
Assert.False(PurgeMagicSpell.IsManaDisrupted(target));
Assert.True(target.Hits < hits);
}
finally
{
PurgeMagicSpell.ClearState(target);
caster.Delete();
target.Delete();
defender.Delete();
}
}
[Fact]
public void CheckCast_FailsWhileCasterManaIsDisrupted()
{
var caster = NewMysticCaster();
var attacker = NewMysticCaster();
try
{
Assert.True(PurgeMagicSpell.ApplyManaDisruption(attacker, caster));
var spell = new PurgeMagicSpell(caster);
Assert.False(spell.CheckCast());
}
finally
{
PurgeMagicSpell.ClearState(caster);
caster.Delete();
attacker.Delete();
}
}
[Fact]
public void ClearState_RemovesManaDisruptionAndImmunityWithoutDamage()
{
var caster = NewMysticCaster();
var target = NewMobile();
try
{
Assert.True(PurgeMagicSpell.ApplyManaDisruption(caster, target));
var hits = target.Hits;
PurgeMagicSpell.ClearState(target);
Assert.False(PurgeMagicSpell.IsManaDisrupted(target));
Assert.Equal(hits, target.Hits);
}
finally
{
caster.Delete();
target.Delete();
}
}
private static void ApplyWard(PurgeMagicSpell.PurgeWardType wardType, Mobile target)
{
switch (wardType)
{
case PurgeMagicSpell.PurgeWardType.MagicReflection:
ApplyMagicReflection(target);
break;
case PurgeMagicSpell.PurgeWardType.Protection:
ProtectionSpell.Toggle(target, target);
break;
case PurgeMagicSpell.PurgeWardType.ReactiveArmor:
new ReactiveArmorSpell(target).OnCastForTests();
break;
case PurgeMagicSpell.PurgeWardType.Bless:
var duration = TimeSpan.FromMinutes(1);
SpellHelper.AddStatBonus(target, target, StatType.Str, 10, duration);
SpellHelper.AddStatBonus(target, target, StatType.Dex, 10, duration);
SpellHelper.AddStatBonus(target, target, StatType.Int, 10, duration);
(target as PlayerMobile)?.AddBuff(new BuffInfo(BuffIcon.Bless, 1075847, 1075848, duration, "10\t10\t10"));
break;
}
}
private static bool HasWard(PurgeMagicSpell.PurgeWardType wardType, Mobile target) => wardType switch
{
PurgeMagicSpell.PurgeWardType.MagicReflection => MagicReflectSpell.HasEffect(target),
PurgeMagicSpell.PurgeWardType.Protection => ProtectionSpell.HasEffect(target),
PurgeMagicSpell.PurgeWardType.ReactiveArmor => ReactiveArmorSpell.HasAosEffect(target),
PurgeMagicSpell.PurgeWardType.Bless => target.GetStatMod("[Magic] Str Buff") != null &&
target.GetStatMod("[Magic] Dex Buff") != null &&
target.GetStatMod("[Magic] Int Buff") != null,
_ => false
};
private static void ApplyMagicReflection(Mobile target)
{
var mods = new[]
{
new ResistanceMod(ResistanceType.Physical, "PhysicalResistMagicResist", -20),
new ResistanceMod(ResistanceType.Fire, "FireResistMagicResist", 10),
new ResistanceMod(ResistanceType.Cold, "ColdResistMagicResist", 10),
new ResistanceMod(ResistanceType.Poison, "PoisonResistMagicResist", 10),
new ResistanceMod(ResistanceType.Energy, "EnergyResistMagicResist", 10)
};
foreach (var mod in mods)
{
target.AddResistanceMod(mod);
}
var table = (Dictionary<Mobile, ResistanceMod[]>)typeof(MagicReflectSpell)
.GetField("_table", BindingFlags.Static | BindingFlags.NonPublic)!
.GetValue(null)!;
table[target] = mods;
}
private static int GetSpellScrollId(SpellScroll scroll)
{
var field = typeof(SpellScroll).GetField("_spellID", BindingFlags.Instance | BindingFlags.NonPublic);
return (int)field!.GetValue(scroll)!;
}
private static void ResetSpellRegistry()
{
var types = (Type[])typeof(SpellRegistry)
.GetField("m_Types", BindingFlags.Static | BindingFlags.NonPublic)!
.GetValue(null)!;
Array.Clear(types);
var idsFromTypes = (Dictionary<Type, int>)typeof(SpellRegistry)
.GetField("m_IDsFromTypes", BindingFlags.Static | BindingFlags.NonPublic)!
.GetValue(null)!;
idsFromTypes.Clear();
typeof(SpellRegistry)
.GetField("m_Count", BindingFlags.Static | BindingFlags.NonPublic)!
.SetValue(null, 0);
SpellRegistry.SpecialMoves.Clear();
}
}
internal static class ReactiveArmorSpellTestExtensions
{
public static void OnCastForTests(this ReactiveArmorSpell spell)
{
var caster = spell.Caster;
var mods = new[]
{
new ResistanceMod(ResistanceType.Physical, "PhysicalResistReactiveArmorSpell", 20),
new ResistanceMod(ResistanceType.Fire, "FireResistReactiveArmorSpell", -5),
new ResistanceMod(ResistanceType.Cold, "ColdResistReactiveArmorSpell", -5),
new ResistanceMod(ResistanceType.Poison, "PoisonResistReactiveArmorSpell", -5),
new ResistanceMod(ResistanceType.Energy, "EnergyResistReactiveArmorSpell", -5)
};
foreach (var mod in mods)
{
caster.AddResistanceMod(mod);
}
var tableField = typeof(ReactiveArmorSpell).GetField("_table", BindingFlags.Static | BindingFlags.NonPublic)!;
var table = (Dictionary<Mobile, ResistanceMod[]>?)tableField.GetValue(null);
table ??= [];
table[caster] = mods;
tableField.SetValue(null, table);
}
}

View file

@ -5,6 +5,7 @@ using Server.Items;
using Server.Mobiles;
using Server.Spells;
using Server.Spells.Fifth;
using Server.Spells.Mysticism;
using Server.Spells.Ninjitsu;
using Server.Spells.Seventh;
@ -248,6 +249,8 @@ namespace Server
m.Damage(totalDamage, from);
var appliedDamage = Math.Max(0, oldHits - m.Hits);
PurgeMagicSpell.OnMobileDamaged(from, m, appliedDamage);
if (firePostResistDamage > 0 && appliedDamage > 0)
{
Swarm.ClearDefender(m);

View file

@ -652,6 +652,8 @@ namespace Server.Spells
scalar = 1.0;
}
Mysticism.PurgeMagicSpell.GetManaDisruptionScalar(Caster, ref scalar);
// Lower Mana Cost = 40%
var lmc = AosAttributes.GetValue(Caster, AosAttribute.LowerManaCost);
if (lmc > 40)

View file

@ -25,6 +25,8 @@ namespace Server.Spells.Fifth
public override SpellCircle Circle => SpellCircle.Fifth;
public static bool HasEffect(Mobile m) => _table.ContainsKey(m);
public override bool CheckCast()
{
if (Core.AOS)
@ -157,11 +159,11 @@ namespace Server.Spells.Fifth
}
[OnEvent(nameof(PlayerMobile.PlayerDeletedEvent))]
public static void EndReflect(Mobile m)
public static bool EndReflect(Mobile m)
{
if (!_table.Remove(m, out var mods))
{
return;
return false;
}
for (var i = 0; i < mods?.Length; ++i)
@ -170,6 +172,7 @@ namespace Server.Spells.Fifth
}
(m as PlayerMobile)?.RemoveBuff(BuffIcon.MagicReflection);
return true;
}
}
}

View file

@ -28,6 +28,8 @@ namespace Server.Spells.First
public override SpellCircle Circle => SpellCircle.First;
public static bool HasAosEffect(Mobile m) => _table?.ContainsKey(m) == true;
public static bool HasEffect(Mobile m) => _t2aTable?.ContainsKey(m) == true;
public static void RemoveEffect(Mobile m)
@ -256,13 +258,13 @@ namespace Server.Spells.First
}
[OnEvent(nameof(PlayerMobile.PlayerDeletedEvent))]
public static void EndArmor(Mobile m)
public static bool EndArmor(Mobile m)
{
RemoveEffect(m);
if (_table?.Remove(m, out var mods) != true)
{
return;
return false;
}
for (var i = 0; i < mods?.Length; ++i)
@ -271,6 +273,7 @@ namespace Server.Spells.First
}
(m as PlayerMobile)?.RemoveBuff(BuffIcon.ReactiveArmor);
return true;
}
}
}

View file

@ -183,7 +183,7 @@ namespace Server.Spells
// Mysticism spells
Register(677, typeof(NetherBoltSpell));
// Register(678, typeof(HealingStoneSpell));
// Register(679, typeof(PurgeMagicSpell));
Register(679, typeof(PurgeMagicSpell));
// Register(680, typeof(EnchantSpell));
// Register(681, typeof(SleepSpell));
Register(682, typeof(EagleStrikeSpell));

View file

@ -0,0 +1,490 @@
using System;
using System.Collections.Generic;
using ModernUO.CodeGeneratedEvents;
using Server.Engines.BuffIcons;
using Server.Mobiles;
using Server.Spells.Fifth;
using Server.Spells.First;
using Server.Spells.Second;
using Server.Targeting;
namespace Server.Spells.Mysticism;
public class PurgeMagicSpell : MysticSpell, ITargetingSpell<Mobile>
{
private const int StandardPurgeImmunitySeconds = 8;
private const int HistoricalSkillScaledImmunityMinSeconds = 1;
private const int HistoricalSkillScaledImmunityMaxSeconds = 6;
private const int ManaDisruptionSeconds = 8;
private const int ManaDisruptionAdditionalImmunitySeconds = 16;
private const double MinManaDisruptionScalar = 1.10;
private const double MaxManaDisruptionScalar = 1.50;
private const int MaxManaDisruptionDamage = 40;
private static readonly SpellInfo _info = new(
"Purge Magic",
"An Ort Sanct",
-1,
9002,
Reagent.FertileDirt,
Reagent.Garlic,
Reagent.MandrakeRoot,
Reagent.SulfurousAsh
);
private static readonly Dictionary<Mobile, DateTime> _purgeImmunity = new();
private static readonly Dictionary<Mobile, Dictionary<PurgeWardType, DateTime>> _rePurgeableWards = new();
private static readonly Dictionary<Mobile, ManaDisruptionContext> _manaDisruptions = new();
private static bool _applyingDisruptionDamage;
public PurgeMagicSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
{
}
public override SpellCircle Circle => SpellCircle.Second;
public static void Configure()
{
EventSink.Logout += OnLogout;
}
public override bool CheckCast()
{
if (IsManaDisrupted(Caster))
{
Caster.SendMessage("Your disrupted mana flow prevents you from casting Purge Magic.");
return false;
}
return base.CheckCast();
}
public override void OnCast()
{
Caster.Target = new SpellTarget<Mobile>(this, TargetFlags.Harmful);
}
public void Target(Mobile m)
{
if (CheckHSequence(m))
{
var source = Caster;
SpellHelper.Turn(source, m);
SpellHelper.CheckReflect((int)Circle, ref source, ref m);
if (ApplyPurge(source, m, sendMessages: true))
{
HarmfulSpell(m);
}
}
}
public static bool ApplyPurge(Mobile caster, Mobile target, bool sendMessages = false)
{
if (caster == null || target == null || caster.Deleted || target.Deleted || !caster.Alive || !target.Alive)
{
return false;
}
if (TryPurgeWard(caster, target, out var wardName))
{
if (wardName.Length == 0)
{
return false;
}
StartPurgeImmunity(target, TimeSpan.FromSeconds(StandardPurgeImmunitySeconds));
if (sendMessages)
{
caster.SendMessage($"You purge {wardName} from your target.");
target.SendMessage($"Your {wardName} ward has been purged.");
}
target.FixedParticles(0x3728, 1, 13, 0x26B8, 0x834, 7, EffectLayer.Head, 0);
target.PlaySound(0x655);
return true;
}
if (IsImmuneToPurge(target, null))
{
if (sendMessages)
{
caster.SendMessage("That target is temporarily immune to purge effects.");
}
return false;
}
return ApplyManaDisruption(caster, target, sendMessages);
}
public static bool TryPurgeWard(Mobile caster, Mobile target, out string wardName)
{
wardName = null;
if (!TryGetRandomPurgeableWard(target, out var ward))
{
return false;
}
if (IsImmuneToPurge(target, ward.Type))
{
wardName = string.Empty;
return true;
}
if (CheckPurgeResisted(caster, target, ward.Circle))
{
target.SendMessage("You resist the purge magic.");
wardName = string.Empty;
return true;
}
if (!ward.Remove(target))
{
return false;
}
wardName = ward.Name;
MarkRePurgeable(target, ward.Type, TimeSpan.FromSeconds(StandardPurgeImmunitySeconds));
return true;
}
public static bool ApplyManaDisruption(Mobile caster, Mobile target, bool sendMessages = false)
{
if (_manaDisruptions.ContainsKey(target) || target.Deleted || !target.Alive)
{
return false;
}
var skillTotal = caster.Skills.Mysticism.Value + Math.Max(caster.Skills.Focus.Value, caster.Skills.Imbuing.Value);
var scalar = Math.Clamp(1.0 + skillTotal / 240.0 * 0.5, MinManaDisruptionScalar, MaxManaDisruptionScalar);
var context = new ManaDisruptionContext(caster, target, scalar);
_manaDisruptions[target] = context;
Timer.StartTimer(TimeSpan.FromSeconds(ManaDisruptionSeconds), () => EndManaDisruption(target, applyDamage: true), out context.TimerToken);
if (sendMessages)
{
caster.SendMessage("You disrupt the target's mana flow.");
target.SendMessage("Your mana flow has been disrupted.");
}
target.FixedParticles(0x3728, 1, 13, 0x26B8, 0x834, 7, EffectLayer.Head, 0);
target.PlaySound(0x655);
return true;
}
public static bool EndManaDisruption(Mobile target, bool applyDamage)
{
if (!_manaDisruptions.Remove(target, out var context))
{
return false;
}
context.TimerToken.Cancel();
StartPurgeImmunity(target, TimeSpan.FromSeconds(ManaDisruptionAdditionalImmunitySeconds));
if (applyDamage && target?.Deleted == false && target.Alive)
{
var elapsed = Core.Now - context.Started;
var damage = Math.Clamp((int)Math.Ceiling(elapsed.TotalSeconds / ManaDisruptionSeconds * MaxManaDisruptionDamage), 1, MaxManaDisruptionDamage);
_applyingDisruptionDamage = true;
try
{
AOS.Damage(target, context.Caster, damage, 0, 0, 0, 0, 0, 100);
}
finally
{
_applyingDisruptionDamage = false;
}
target.SendMessage("Chaotic energy rushes back through your disrupted mana flow.");
}
return true;
}
public static bool IsManaDisrupted(Mobile m) => m != null && _manaDisruptions.ContainsKey(m);
public static bool GetManaDisruptionScalar(Mobile m, ref double scalar)
{
if (m != null && _manaDisruptions.TryGetValue(m, out var context))
{
scalar = Math.Max(scalar, context.Scalar);
return true;
}
return false;
}
public static bool IsImmuneToPurge(Mobile target, PurgeWardType? wardType)
{
if (target == null)
{
return false;
}
if (wardType.HasValue && IsRePurgeable(target, wardType.Value))
{
return false;
}
if (IsManaDisrupted(target))
{
return true;
}
if (_purgeImmunity.TryGetValue(target, out var immuneUntil))
{
if (immuneUntil > Core.Now)
{
return true;
}
_purgeImmunity.Remove(target);
}
return false;
}
public static void OnMobileDamaged(Mobile attacker, Mobile defender, int damage)
{
if (_applyingDisruptionDamage)
{
return;
}
if (damage > 0 && attacker?.Deleted == false && defender?.Deleted == false && attacker != defender)
{
EndManaDisruption(attacker, applyDamage: true);
}
}
public static void ClearState(Mobile m)
{
if (m == null)
{
return;
}
if (_manaDisruptions.Remove(m, out var context))
{
context.TimerToken.Cancel();
}
_purgeImmunity.Remove(m);
_rePurgeableWards.Remove(m);
}
[OnEvent(nameof(PlayerMobile.PlayerDeathEvent))]
public static void OnPlayerDeath(PlayerMobile m) => ClearState(m);
[OnEvent(nameof(PlayerMobile.PlayerDeletedEvent))]
public static void OnPlayerDeleted(PlayerMobile m) => ClearState(m);
private static void OnLogout(Mobile m) => ClearState(m);
private static bool TryGetRandomPurgeableWard(Mobile target, out PurgeWard selectedWard)
{
var selected = default(PurgeWard);
var wardCount = 0;
if (MagicReflectSpell.HasEffect(target))
{
TrySelectWard(new PurgeWard(PurgeWardType.MagicReflection, "Magic Reflection", SpellCircle.Fifth, MagicReflectSpell.EndReflect));
}
if (ProtectionSpell.HasEffect(target))
{
TrySelectWard(new PurgeWard(PurgeWardType.Protection, "Protection", SpellCircle.Second, ProtectionSpell.EndProtection));
}
if (ReactiveArmorSpell.HasAosEffect(target))
{
TrySelectWard(new PurgeWard(PurgeWardType.ReactiveArmor, "Reactive Armor", SpellCircle.First, ReactiveArmorSpell.EndArmor));
}
if (HasBless(target))
{
TrySelectWard(new PurgeWard(PurgeWardType.Bless, "Bless", SpellCircle.Third, RemoveBless));
}
selectedWard = selected;
return wardCount > 0;
void TrySelectWard(PurgeWard ward)
{
wardCount++;
if (Utility.Random(wardCount) == 0)
{
selected = ward;
}
}
}
private static bool CheckPurgeResisted(Mobile caster, Mobile target, SpellCircle wardCircle)
{
var resistPercent = GetPurgeResistPercent(caster, target, wardCircle);
if (resistPercent <= 0.0)
{
return false;
}
if (resistPercent >= 100.0)
{
return true;
}
if (target.Skills.MagicResist.Value < (1 + (int)wardCircle) * 10)
{
target.CheckSkill(SkillName.MagicResist, 0.0, target.Skills.MagicResist.Cap);
}
return resistPercent >= Utility.RandomDouble() * 100.0;
}
private static double GetPurgeResistPercent(Mobile caster, Mobile target, SpellCircle wardCircle)
{
var effectiveSkill = (caster.Skills.Mysticism.Value + Math.Max(caster.Skills.Focus.Value, caster.Skills.Imbuing.Value)) / 2.0;
var magicResist = target.Skills.MagicResist.Value;
var firstPercent = magicResist / 5.0;
var secondPercent = magicResist - ((effectiveSkill - 20.0) / 5.0 + (1 + (int)wardCircle) * 5.0);
return Math.Max(firstPercent, secondPercent) / 2.0;
}
private static bool HasBless(Mobile target) =>
target.GetStatMod("[Magic] Str Buff") != null &&
target.GetStatMod("[Magic] Dex Buff") != null &&
target.GetStatMod("[Magic] Int Buff") != null;
private static bool RemoveBless(Mobile target)
{
if (!HasBless(target))
{
return false;
}
target.RemoveStatMod("[Magic] Str Buff");
target.RemoveStatMod("[Magic] Dex Buff");
target.RemoveStatMod("[Magic] Int Buff");
(target as PlayerMobile)?.RemoveBuff(BuffIcon.Bless);
return true;
}
private static void StartPurgeImmunity(Mobile target, TimeSpan duration)
{
if (target == null || target.Deleted)
{
return;
}
var until = Core.Now + duration;
if (!_purgeImmunity.TryGetValue(target, out var current) || current < until)
{
_purgeImmunity[target] = until;
Timer.StartTimer(duration, () => ExpirePurgeImmunity(target, until));
}
}
private static void MarkRePurgeable(Mobile target, PurgeWardType wardType, TimeSpan duration)
{
if (target == null || target.Deleted)
{
return;
}
if (!_rePurgeableWards.TryGetValue(target, out var wards))
{
wards = new Dictionary<PurgeWardType, DateTime>();
_rePurgeableWards[target] = wards;
}
var until = Core.Now + duration;
wards[wardType] = until;
Timer.StartTimer(duration, () => ExpireRePurgeableWard(target, wardType, until));
}
private static void ExpirePurgeImmunity(Mobile target, DateTime until)
{
if (_purgeImmunity.TryGetValue(target, out var current) && current <= until)
{
_purgeImmunity.Remove(target);
}
}
private static void ExpireRePurgeableWard(Mobile target, PurgeWardType wardType, DateTime until)
{
if (!_rePurgeableWards.TryGetValue(target, out var wards) || !wards.TryGetValue(wardType, out var current) || current > until)
{
return;
}
wards.Remove(wardType);
if (wards.Count == 0)
{
_rePurgeableWards.Remove(target);
}
}
private static bool IsRePurgeable(Mobile target, PurgeWardType wardType)
{
if (!_rePurgeableWards.TryGetValue(target, out var wards) || !wards.TryGetValue(wardType, out var until))
{
return false;
}
if (until > Core.Now)
{
return true;
}
wards.Remove(wardType);
if (wards.Count == 0)
{
_rePurgeableWards.Remove(target);
}
return false;
}
public enum PurgeWardType
{
MagicReflection,
Protection,
ReactiveArmor,
Bless
}
private readonly record struct PurgeWard(
PurgeWardType Type,
string Name,
SpellCircle Circle,
Func<Mobile, bool> Remove
);
private sealed class ManaDisruptionContext
{
public ManaDisruptionContext(Mobile caster, Mobile target, double scalar)
{
Caster = caster;
Target = target;
Scalar = scalar;
Started = Core.Now;
}
public Mobile Caster { get; }
public Mobile Target { get; }
public double Scalar { get; }
public DateTime Started { get; }
public TimerExecutionToken TimerToken;
}
}

View file

@ -32,6 +32,8 @@ namespace Server.Spells.Second
public override SpellCircle Circle => SpellCircle.Second;
public static bool HasEffect(Mobile m) => Registry.ContainsKey(m) || HasT2AProtection(m);
public static bool HasT2AProtection(Mobile m) => _t2aTable?.ContainsKey(m) ?? false;
public static void RemoveT2AProtection(Mobile m)
@ -153,13 +155,13 @@ namespace Server.Spells.Second
}
[OnEvent(nameof(PlayerMobile.PlayerDeletedEvent))]
public static void EndProtection(Mobile m)
public static bool EndProtection(Mobile m)
{
RemoveT2AProtection(m);
if (_table?.Remove(m, out var mods) != true)
{
return;
return false;
}
Registry.Remove(m);
@ -168,6 +170,7 @@ namespace Server.Spells.Second
m.RemoveSkillMod(mods.Item2);
(m as PlayerMobile)?.RemoveBuff(BuffIcon.Protection);
return true;
}
public override void OnCast()