Merge pull request #42 from RebirthUO/feat/issue-35-purge-magic
feat: implement Purge Magic
This commit is contained in:
commit
fa3b9024cb
8 changed files with 943 additions and 7 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
490
Projects/UOContent/Spells/Mysticism/PurgeMagicSpell.cs
Normal file
490
Projects/UOContent/Spells/Mysticism/PurgeMagicSpell.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue