From 4f9714818d222def73eddde41b7a29154b51c363 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 5 Dec 2021 09:40:57 -0800 Subject: [PATCH] fix: Fixes skill requirements and cleans up magery spells (#870) * Preps damage stacking for mysticism, necromancy fixes, and spellweaving * Removes extra LINQ allocations * Fixes skill requirements for magery spells. --- Projects/UOContent/Spells/Base/MagerySpell.cs | 31 +++-- Projects/UOContent/Spells/Base/SpecialMove.cs | 58 +++----- Projects/UOContent/Spells/Base/Spell.cs | 90 ++++++------ Projects/UOContent/Spells/Base/SpellHelper.cs | 124 ++++++----------- .../UOContent/Spells/Eighth/Resurrection.cs | 5 +- Projects/UOContent/Spells/First/Feeblemind.cs | 2 + Projects/UOContent/Spells/First/MagicArrow.cs | 9 +- Projects/UOContent/Spells/Fourth/ArchCure.cs | 31 +++-- .../UOContent/Spells/Fourth/ArchProtection.cs | 6 +- Projects/UOContent/Spells/Fourth/Curse.cs | 12 +- .../UOContent/Spells/Second/RemoveTrap.cs | 2 +- .../Spells/Seventh/ChainLightning.cs | 129 ++++++++---------- .../UOContent/Spells/Seventh/EnergyField.cs | 6 +- .../UOContent/Spells/Seventh/MeteorSwarm.cs | 115 +++++++--------- Projects/UOContent/Spells/Sixth/Explosion.cs | 50 +++---- Projects/UOContent/Spells/Third/Poison.cs | 23 +--- 16 files changed, 311 insertions(+), 382 deletions(-) diff --git a/Projects/UOContent/Spells/Base/MagerySpell.cs b/Projects/UOContent/Spells/Base/MagerySpell.cs index c42d60954..68c261e8e 100644 --- a/Projects/UOContent/Spells/Base/MagerySpell.cs +++ b/Projects/UOContent/Spells/Base/MagerySpell.cs @@ -5,9 +5,10 @@ namespace Server.Spells { public abstract class MagerySpell : Spell { - private const double ChanceOffset = 20.0, ChanceLength = 100.0 / 7.0; - - private static readonly int[] m_ManaTable = { 4, 6, 9, 11, 14, 20, 40, 50 }; + private static readonly int[] _manaTable = { 4, 6, 9, 11, 14, 20, 40, 50 }; + private static readonly double[] _requiredSkill = Core.ML ? + new[] { 0.0, -4.0, 10.0, 24.0, 38.0, 52.0, 66.0, 80.0 } : + new[] { 0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0 }; public MagerySpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info) { @@ -29,13 +30,20 @@ namespace Server.Spells circle -= 2; } - var avg = ChanceLength * circle; + // Original RunUO algorithm for required skill + // const double chanceOffset = 20.0 + // const double chanceLength = 100.0 / 7.0 + // var avg = chanceLength * circle; + // min = avg - chanceOffset; + // max = avg + chanceOffset; - min = avg - ChanceOffset; - max = avg + ChanceOffset; + // Correct algorithm according to OSI. + // TODO: Verify this algorithm on OSI for latest expansion. + min = _requiredSkill[circle]; + max = min + 40; } - public override int GetMana() => Scroll is BaseWand ? 0 : m_ManaTable[(int)Circle]; + public override int GetMana() => Scroll is BaseWand ? 0 : _manaTable[(int)Circle]; public override double GetResistSkill(Mobile m) { @@ -79,12 +87,13 @@ namespace Server.Spells public virtual double GetResistPercentForCircle(Mobile target, SpellCircle circle) { - var firstPercent = target.Skills.MagicResist.Value / 5.0; - var secondPercent = target.Skills.MagicResist.Value - + var magicResist = target.Skills.MagicResist.Value; + var firstPercent = magicResist / 5.0; + var secondPercent = magicResist - ((Caster.Skills[CastSkill].Value - 20.0) / 5.0 + (1 + (int)circle) * 5.0); - return (firstPercent > secondPercent ? firstPercent : secondPercent) / - 2.0; // Seems should be about half of what stratics says. + // Seems should be about half of what stratics says. + return (firstPercent > secondPercent ? firstPercent : secondPercent) / 2.0; } public virtual double GetResistPercent(Mobile target) => GetResistPercentForCircle(target, Circle); diff --git a/Projects/UOContent/Spells/Base/SpecialMove.cs b/Projects/UOContent/Spells/Base/SpecialMove.cs index f42ea076b..d0b98ac79 100644 --- a/Projects/UOContent/Spells/Base/SpecialMove.cs +++ b/Projects/UOContent/Spells/Base/SpecialMove.cs @@ -10,8 +10,7 @@ namespace Server.Spells { public abstract class SpecialMove { - private static readonly Dictionary m_PlayersTable = - new(); + private static readonly Dictionary _playersTable = new(); public virtual int BaseMana => 0; @@ -159,40 +158,18 @@ namespace Server.Spells return false; } - string option = null; - - if (this is Backstab) + string option = this switch { - option = "Backstab"; - } - else if (this is DeathStrike) - { - option = "Death Strike"; - } - else if (this is FocusAttack) - { - option = "Focus Attack"; - } - else if (this is KiAttack) - { - option = "Ki Attack"; - } - else if (this is SurpriseAttack) - { - option = "Surprise Attack"; - } - else if (this is HonorableExecution) - { - option = "Honorable Execution"; - } - else if (this is LightningStrike) - { - option = "Lightning Strike"; - } - else if (this is MomentumStrike) - { - option = "Momentum Strike"; - } + Backstab => "Backstab", + DeathStrike => "Death Strike", + FocusAttack => "Focus Attack", + KiAttack => "Ki Attack", + SurpriseAttack => "Surprise Attack", + HonorableExecution => "Honorable Execution", + LightningStrike => "Lightning Strike", + MomentumStrike => "Momentum Strike", + _ => null + }; if (option != null && !DuelContext.AllowSpecialMove(from, option, this)) { @@ -303,7 +280,7 @@ namespace Server.Spells private static void AddContext(Mobile m, SpecialMoveContext context) { - m_PlayersTable[m] = context; + _playersTable[m] = context; } private static void RemoveContext(Mobile m) @@ -312,23 +289,20 @@ namespace Server.Spells if (context != null) { - m_PlayersTable.Remove(m); + _playersTable.Remove(m); context.Timer.Stop(); } } private static SpecialMoveContext GetContext(Mobile m) => - m_PlayersTable.TryGetValue(m, out var context) ? context : null; + _playersTable.TryGetValue(m, out var context) ? context : null; private class SpecialMoveTimer : Timer { private readonly Mobile m_Mobile; - public SpecialMoveTimer(Mobile from) : base(TimeSpan.FromSeconds(3.0)) - { - m_Mobile = from; - } + public SpecialMoveTimer(Mobile from) : base(TimeSpan.FromSeconds(3.0)) => m_Mobile = from; protected override void OnTick() { diff --git a/Projects/UOContent/Spells/Base/Spell.cs b/Projects/UOContent/Spells/Base/Spell.cs index 120d85be1..78eea7bdc 100644 --- a/Projects/UOContent/Spells/Base/Spell.cs +++ b/Projects/UOContent/Spells/Base/Spell.cs @@ -17,19 +17,16 @@ namespace Server.Spells public abstract class Spell : ISpell { private static readonly TimeSpan NextSpellDelay = TimeSpan.FromSeconds(0.75); - private static readonly TimeSpan AnimateDelay = TimeSpan.FromSeconds(1.5); // In reality, it's ANY delayed Damage spell Post-AoS that can't stack, but, only // Expo & Magic Arrow have enough delay and a short enough cast time to bring up // the possibility of stacking 'em. Note that a MA & an Explosion will stack, but // of course, two MA's won't. - private static readonly Dictionary m_ContextTable = - new(); + private static readonly Dictionary _contextTable = new(); - private AnimTimer m_AnimTimer; - - private CastTimer m_CastTimer; + private AnimTimer _animTimer; + private CastTimer _castTimer; public Spell(Mobile caster, Item scroll, SpellInfo info) { @@ -60,13 +57,17 @@ namespace Server.Spells public virtual bool DelayedDamage => false; - public virtual bool DelayedDamageStacking => true; + public static readonly Type[] AOSNoDelayedDamageStackingSelf = Core.AOS ? Array.Empty() : null; + + // Null means stacking is allowed while empty indicates no stacking with self + // More than zero means no stacking with self and other spells + public virtual Type[] DelayedDamageSpellFamilyStacking => null; public virtual bool BlockedByHorrificBeast => true; public virtual bool BlockedByAnimalForm => true; public virtual bool BlocksMovement => true; - public virtual bool CheckNextSpellTime => !(Scroll is BaseWand); + public virtual bool CheckNextSpellTime => Scroll is not BaseWand; public virtual int CastRecoveryBase => 6; public virtual int CastRecoveryFastScalar => 1; @@ -149,22 +150,39 @@ namespace Server.Spells public void StartDelayedDamageContext(Mobile m, Timer t) { - if (DelayedDamageStacking) + var damageStacking = DelayedDamageSpellFamilyStacking; + if (damageStacking == null) { return; // Sanity } - if (!m_ContextTable.TryGetValue(GetType(), out var contexts)) + var type = GetType(); + + if (!_contextTable.TryGetValue(type, out var context)) { - m_ContextTable[GetType()] = contexts = new DelayedDamageContextWrapper(); + _contextTable[type] = context = new DelayedDamageContextWrapper(); + + for (int i = 0; i < damageStacking.Length; i++) + { + _contextTable.Add(damageStacking[i], context); + } } - contexts.Add(m, t); + context.Add(m, t); } + public bool HasDelayedDamageContext(Mobile m) => + DelayedDamageSpellFamilyStacking != null && + _contextTable.TryGetValue(GetType(), out var context) && context.Contains(m); + public void RemoveDelayedDamageContext(Mobile m) { - if (m_ContextTable.TryGetValue(GetType(), out var contexts)) + if (m == null || DelayedDamageSpellFamilyStacking == null) + { + return; // Sanity + } + + if (_contextTable.TryGetValue(GetType(), out var contexts)) { contexts.Remove(m); } @@ -389,8 +407,8 @@ namespace Server.Spells { OnDisturb(type, true); - m_CastTimer?.Stop(); - m_AnimTimer?.Stop(); + _castTimer?.Stop(); + _animTimer?.Stop(); if (Core.AOS && Caster.Player && type == DisturbType.Hurt) { @@ -469,7 +487,7 @@ namespace Server.Spells { Caster.SendLocalizedMessage(1061091); // You cannot cast that spell in this form. } - else if (!(Scroll is BaseWand) && (Caster.Paralyzed || Caster.Frozen)) + else if (Scroll is not BaseWand && (Caster.Paralyzed || Caster.Frozen)) { Caster.SendLocalizedMessage(502643); // You can not cast a spell while frozen. } @@ -492,7 +510,7 @@ namespace Server.Spells State = SpellState.Casting; Caster.Spell = this; - if (!(Scroll is BaseWand) && RevealOnCast) + if (Scroll is not BaseWand && RevealOnCast) { Caster.RevealingAction(); } @@ -507,8 +525,8 @@ namespace Server.Spells if (count != 0) { - m_AnimTimer = new AnimTimer(this, count); - m_AnimTimer.Start(); + _animTimer = new AnimTimer(this, count); + _animTimer.Start(); } if (Info.LeftHandEffect > 0) @@ -532,18 +550,18 @@ namespace Server.Spells WeaponAbility.ClearCurrentAbility(Caster); } - m_CastTimer = new CastTimer(this, castDelay); + _castTimer = new CastTimer(this, castDelay); // m_CastTimer.Start(); OnBeginCast(); if (castDelay > TimeSpan.Zero) { - m_CastTimer.Start(); + _castTimer.Start(); } else { - m_CastTimer.Tick(); + _castTimer.Tick(); } return true; @@ -647,11 +665,6 @@ namespace Server.Spells return TimeSpan.FromSeconds((double)delay / CastRecoveryPerSecond); } - // public virtual int CastDelayBase{ get{ return 3; } } - // public virtual int CastDelayFastScalar{ get{ return 1; } } - // public virtual int CastDelayPerSecond{ get{ return 4; } } - // public virtual int CastDelayMinimum{ get{ return 1; } } - public virtual TimeSpan GetCastDelay() { if (Scroll is BaseWand) @@ -665,7 +678,7 @@ namespace Server.Spells // Paladins with magery of 70.0 or above are subject to a faster casting cap of 2 var fcMax = 4; - if (CastSkill == SkillName.Magery || CastSkill == SkillName.Necromancy || + if (CastSkill is SkillName.Magery or SkillName.Necromancy || CastSkill == SkillName.Chivalry && Caster.Skills.Magery.Value >= 70.0) { fcMax = 2; @@ -749,12 +762,9 @@ namespace Server.Spells Scroll.Movable = m; } - else + else if (ClearHandsOnCast) { - if (ClearHandsOnCast) - { - Caster.ClearHands(); - } + Caster.ClearHands(); } var karma = ComputeKarmaAward(); @@ -838,10 +848,14 @@ namespace Server.Spells m_Contexts.Add(m, t); } + public bool Contains(Mobile m) => m_Contexts.ContainsKey(m); + public void Remove(Mobile m) { - m_Contexts.Remove(m); - // TODO: Should we stop the timer? + if (m_Contexts.Remove(m, out var t)) + { + t.Stop(); + } } } @@ -876,7 +890,7 @@ namespace Server.Spells if (!Running) { - m_Spell.m_AnimTimer = null; + m_Spell._animTimer = null; } } } @@ -900,7 +914,7 @@ namespace Server.Spells if (m_Spell.State == SpellState.Casting && m_Spell.Caster.Spell == m_Spell) { m_Spell.State = SpellState.Sequencing; - m_Spell.m_CastTimer = null; + m_Spell._castTimer = null; m_Spell.Caster.OnSpellCast(m_Spell); m_Spell.Caster.Region?.OnSpellCast(m_Spell.Caster, m_Spell); m_Spell.Caster.NextSpellTime = @@ -915,7 +929,7 @@ namespace Server.Spells m_Spell.Caster.Target?.BeginTimeout(m_Spell.Caster, 30000); // 30 seconds } - m_Spell.m_CastTimer = null; + m_Spell._castTimer = null; } } diff --git a/Projects/UOContent/Spells/Base/SpellHelper.cs b/Projects/UOContent/Spells/Base/SpellHelper.cs index f855ad5ac..383228b49 100644 --- a/Projects/UOContent/Spells/Base/SpellHelper.cs +++ b/Projects/UOContent/Spells/Base/SpellHelper.cs @@ -184,7 +184,7 @@ namespace Server.Spells public static void Turn(Mobile from, object to) { - if (!(to is IPoint3D target)) + if (to is not IPoint3D target) { return; } @@ -333,7 +333,7 @@ namespace Server.Spells public static bool AddStatCurse(Mobile caster, Mobile target, StatType type, int curse, TimeSpan duration) { var offset = -curse; - var name = $"[Magic] {type} Offset"; + var name = $"[Magic] {type} Curse"; var mod = target.GetStatMod(name); @@ -447,26 +447,8 @@ namespace Server.Spells var bcFrom = from as BaseCreature; var bcTarg = to as BaseCreature; - PlayerMobile pmFrom; - PlayerMobile pmTarg; - - if (bcFrom?.Summoned == true) - { - pmFrom = bcFrom.SummonMaster as PlayerMobile; - } - else - { - pmFrom = from as PlayerMobile; - } - - if (bcTarg?.Summoned == true) - { - pmTarg = bcTarg.SummonMaster as PlayerMobile; - } - else - { - pmTarg = to as PlayerMobile; - } + var pmFrom = (bcFrom?.Summoned == true ? bcFrom.SummonMaster : from) as PlayerMobile; + var pmTarg = (bcTarg?.Summoned == true ? bcTarg.SummonMaster : to) as PlayerMobile; if (pmFrom?.DuelContext != null && pmFrom.DuelContext == pmTarg?.DuelContext && pmFrom.DuelContext.Started && pmFrom.DuelPlayer != null && pmTarg?.DuelPlayer != null) @@ -864,17 +846,6 @@ namespace Server.Spells return x < 0 || y < 0 || x >= map.Width || y >= map.Height; } - // towns - public static bool IsTown(IPoint3D ip, Mobile caster) - { - if (ip is Item item) - { - ip = item.GetWorldLocation(); - } - - return IsTown(new Point3D(ip), caster); - } - public static bool IsTown(Point3D loc, Mobile caster) { var map = caster.Map; @@ -897,15 +868,8 @@ namespace Server.Spells return reg?.IsDisabled() == false; } - public static bool CheckTown(IPoint3D ip, Mobile caster) - { - if (ip is Item item) - { - ip = item.GetWorldLocation(); - } - - return CheckTown(new Point3D(ip), caster); - } + public static bool CheckTown(IPoint3D ip, Mobile caster) => + CheckTown((ip as Item)?.GetWorldLocation() ?? new Point3D(ip), caster); public static bool CheckTown(Point3D loc, Mobile caster) { @@ -919,13 +883,13 @@ namespace Server.Spells } // magic reflection - public static void CheckReflect(int circle, Mobile caster, ref Mobile target) - { + public static bool CheckReflect(int circle, Mobile caster, ref Mobile target) => CheckReflect(circle, ref caster, ref target); - } - public static void CheckReflect(int circle, ref Mobile caster, ref Mobile target) + public static bool CheckReflect(int circle, ref Mobile caster, ref Mobile target) { + var reflect = false; + if (target.MagicDamageAbsorb > 0) { ++circle; @@ -933,41 +897,26 @@ namespace Server.Spells target.MagicDamageAbsorb -= circle; // This order isn't very intuitive, but you have to nullify reflect before target gets switched - - var reflect = target.MagicDamageAbsorb >= 0; - - (target as BaseCreature)?.CheckReflect(caster, ref reflect); - + reflect = target.MagicDamageAbsorb >= 0; if (target.MagicDamageAbsorb <= 0) { target.MagicDamageAbsorb = 0; DefensiveSpell.Nullify(target); } - - if (reflect) - { - target.FixedEffect(0x37B9, 10, 5); - - var temp = caster; - caster = target; - target = temp; - } } - else if (target is BaseCreature creature) + + if (target is BaseCreature creature) { - var reflect = false; - creature.CheckReflect(caster, ref reflect); - - if (reflect) - { - creature.FixedEffect(0x37B9, 10, 5); - - var temp = caster; - caster = creature; - target = temp; - } } + + if (reflect) + { + target.FixedEffect(0x37B9, 10, 5); + (caster, target) = (target, caster); + } + + return reflect; } public static void Damage(Spell spell, Mobile target, double damage) @@ -995,20 +944,21 @@ namespace Server.Spells { (from as BaseCreature)?.AlterSpellDamageTo(target, ref iDamage); - (target as BaseCreature)?.AlterSpellDamageFrom(from, ref iDamage); + var bcTarget = target as BaseCreature; + bcTarget?.AlterSpellDamageFrom(from, ref iDamage); target.Damage(iDamage, from); + + if (from != null) + { + bcTarget?.OnHarmfulSpell(from); + bcTarget?.OnDamagedBySpell(from); + } } else { new SpellDamageTimer(spell, target, from, iDamage, delay).Start(); } - - if (target is BaseCreature c && from != null && delay == TimeSpan.Zero) - { - c.OnHarmfulSpell(from); - c.OnDamagedBySpell(from); - } } public static void Damage( @@ -1086,6 +1036,11 @@ namespace Server.Spells public static void DoLeech(int damageGiven, Mobile from, Mobile target) { + if (target == null) + { + return; + } + var context = TransformationSpellHelper.GetContext(from); if (context == null) /* cleanup */ @@ -1134,7 +1089,7 @@ namespace Server.Spells m_Damage = damage; m_Spell = s; - if (m_Spell?.DelayedDamage == true && !m_Spell.DelayedDamageStacking) + if (m_Spell?.DelayedDamage == true) { m_Spell.StartDelayedDamageContext(target, this); } @@ -1143,7 +1098,6 @@ namespace Server.Spells protected override void OnTick() { (m_From as BaseCreature)?.AlterSpellDamageTo(m_Target, ref m_Damage); - (m_Target as BaseCreature)?.AlterSpellDamageFrom(m_From, ref m_Damage); m_Target.Damage(m_Damage); @@ -1181,7 +1135,8 @@ namespace Server.Spells m_Chaos = chaos; m_DFA = dfa; m_Spell = s; - if (m_Spell?.DelayedDamage == true && !m_Spell.DelayedDamageStacking) + + if (m_Spell?.DelayedDamage == true) { m_Spell.StartDelayedDamageContext(target, this); } @@ -1189,10 +1144,9 @@ namespace Server.Spells protected override void OnTick() { - var bcFrom = m_From as BaseCreature; var bcTarg = m_Target as BaseCreature; - if (bcFrom != null && m_Target != null) + if (m_From is BaseCreature bcFrom && m_Target != null) { bcFrom.AlterSpellDamageTo(m_Target, ref m_Damage); } @@ -1253,7 +1207,7 @@ namespace Server.Spells public static bool OnCast(Mobile caster, Spell spell) { - if (!(spell is ITransformationSpell transformSpell)) + if (spell is not ITransformationSpell transformSpell) { return false; } diff --git a/Projects/UOContent/Spells/Eighth/Resurrection.cs b/Projects/UOContent/Spells/Eighth/Resurrection.cs index c485850fa..2bda277a0 100644 --- a/Projects/UOContent/Spells/Eighth/Resurrection.cs +++ b/Projects/UOContent/Spells/Eighth/Resurrection.cs @@ -51,9 +51,8 @@ namespace Server.Spells.Eighth } else if (m.Region?.IsPartOf("Khaldun") == true) { - Caster.SendLocalizedMessage( - 1010395 - ); // The veil of death in this area is too strong and resists thy efforts to restore life. + // The veil of death in this area is too strong and resists thy efforts to restore life. + Caster.SendLocalizedMessage(1010395); } else if (CheckBSequence(m, true)) { diff --git a/Projects/UOContent/Spells/First/Feeblemind.cs b/Projects/UOContent/Spells/First/Feeblemind.cs index ac339bca6..674c71b3e 100644 --- a/Projects/UOContent/Spells/First/Feeblemind.cs +++ b/Projects/UOContent/Spells/First/Feeblemind.cs @@ -27,6 +27,8 @@ namespace Server.Spells.First SpellHelper.CheckReflect((int)Circle, Caster, ref m); + // TODO: StoneForm immunity + SpellHelper.AddStatCurse(Caster, m, StatType.Int); m.Spell?.OnCasterHurt(); diff --git a/Projects/UOContent/Spells/First/MagicArrow.cs b/Projects/UOContent/Spells/First/MagicArrow.cs index 02f4de6e7..1461e3548 100644 --- a/Projects/UOContent/Spells/First/MagicArrow.cs +++ b/Projects/UOContent/Spells/First/MagicArrow.cs @@ -1,3 +1,4 @@ +using System; using Server.Targeting; namespace Server.Spells.First @@ -18,7 +19,7 @@ namespace Server.Spells.First public override SpellCircle Circle => SpellCircle.First; - public override bool DelayedDamageStacking => !Core.AOS; + public override Type[] DelayedDamageSpellFamilyStacking => AOSNoDelayedDamageStackingSelf; public override bool DelayedDamage => true; @@ -30,6 +31,12 @@ namespace Server.Spells.First SpellHelper.Turn(source, m); + if (Core.SA && HasDelayedDamageContext(m)) + { + DoHurtFizzle(); + return; + } + SpellHelper.CheckReflect((int)Circle, ref source, ref m); double damage; diff --git a/Projects/UOContent/Spells/Fourth/ArchCure.cs b/Projects/UOContent/Spells/Fourth/ArchCure.cs index 3d7158499..9cacd8790 100644 --- a/Projects/UOContent/Spells/Fourth/ArchCure.cs +++ b/Projects/UOContent/Spells/Fourth/ArchCure.cs @@ -1,6 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; +using Server.Collections; using Server.Mobiles; namespace Server.Spells.Fourth @@ -34,37 +33,39 @@ namespace Server.Spells.Fourth SpellHelper.GetSurfaceTop(ref p); - var targets = new List(); - var map = Caster.Map; - var directTarget = p as Mobile; - var loc = new Point3D(p); - if (map != null) { + using var pool = PooledRefQueue.Create(); + var directTarget = p as Mobile; + var loc = new Point3D(p); + var feluccaRules = map.Rules == MapRules.FeluccaRules; // You can target any living mobile directly, beneficial checks apply if (directTarget != null && Caster.CanBeBeneficial(directTarget, false)) { - targets.Add(directTarget); + pool.Enqueue(directTarget); } var eable = map.GetMobilesInRange(loc, 2); - targets.AddRange(eable.Where(m => m != directTarget).Where(m => AreaCanTarget(m, feluccaRules))); + foreach (var m in eable) + { + if (m != directTarget && AreaCanTarget(m, feluccaRules)) + { + pool.Enqueue(m); + } + } eable.Free(); - } - Effects.PlaySound(loc, Caster.Map, 0x299); + Effects.PlaySound(loc, Caster.Map, 0x299); - if (targets.Count > 0) - { var cured = 0; - for (var i = 0; i < targets.Count; ++i) + while (pool.Count > 0) { - var m = targets[i]; + var m = pool.Dequeue(); Caster.DoBeneficial(m); diff --git a/Projects/UOContent/Spells/Fourth/ArchProtection.cs b/Projects/UOContent/Spells/Fourth/ArchProtection.cs index d924d4c82..dfb258ec9 100644 --- a/Projects/UOContent/Spells/Fourth/ArchProtection.cs +++ b/Projects/UOContent/Spells/Fourth/ArchProtection.cs @@ -19,7 +19,7 @@ namespace Server.Spells.Fourth Reagent.SulfurousAsh ); - private static readonly Dictionary _Table = new(); + private static readonly Dictionary _table = new(); public ArchProtectionSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { @@ -107,12 +107,12 @@ namespace Server.Spells.Fourth private static void AddEntry(Mobile m, int v) { - _Table[m] = v; + _table[m] = v; } public static void RemoveEntry(Mobile m) { - if (_Table.Remove(m, out var v)) + if (_table.Remove(m, out var v)) { m.EndAction(); m.VirtualArmorMod -= Math.Min(v, m.VirtualArmorMod); diff --git a/Projects/UOContent/Spells/Fourth/Curse.cs b/Projects/UOContent/Spells/Fourth/Curse.cs index 84a076e3d..2eb06c82d 100644 --- a/Projects/UOContent/Spells/Fourth/Curse.cs +++ b/Projects/UOContent/Spells/Fourth/Curse.cs @@ -15,7 +15,7 @@ namespace Server.Spells.Fourth Reagent.SulfurousAsh ); - private static readonly HashSet m_UnderEffect = new(); + private static readonly HashSet _underEffect = new(); public CurseSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { @@ -41,7 +41,7 @@ namespace Server.Spells.Fourth if (Caster.Player && m.Player /*&& Caster != m */ && !UnderEffect(m)) { var duration = SpellHelper.GetDuration(Caster, m); - m_UnderEffect.Add(m); + _underEffect.Add(m); Timer.StartTimer(duration, () => RemoveEffect(m)); m.UpdateResistances(); } @@ -71,13 +71,13 @@ namespace Server.Spells.Fourth Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); } - public static void RemoveEffect(Mobile m) + public static bool RemoveEffect(Mobile m) { - m_UnderEffect.Remove(m); - + var effectRemoved = _underEffect.Remove(m); m.UpdateResistances(); + return effectRemoved; } - public static bool UnderEffect(Mobile m) => m_UnderEffect.Contains(m); + public static bool UnderEffect(Mobile m) => _underEffect.Contains(m); } } diff --git a/Projects/UOContent/Spells/Second/RemoveTrap.cs b/Projects/UOContent/Spells/Second/RemoveTrap.cs index cb800ddf2..085aa319f 100644 --- a/Projects/UOContent/Spells/Second/RemoveTrap.cs +++ b/Projects/UOContent/Spells/Second/RemoveTrap.cs @@ -55,7 +55,7 @@ namespace Server.Spells.Second public override void OnCast() { Caster.Target = new SpellTargetItem(this, range: Core.ML ? 10 : 12); - Caster.SendMessage("What do you wish to untrap?"); // TODO: Localization? + Caster.SendLocalizedMessage(502368); } } } diff --git a/Projects/UOContent/Spells/Seventh/ChainLightning.cs b/Projects/UOContent/Spells/Seventh/ChainLightning.cs index 6f3ec9923..d9edf0567 100644 --- a/Projects/UOContent/Spells/Seventh/ChainLightning.cs +++ b/Projects/UOContent/Spells/Seventh/ChainLightning.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Linq; +using Server.Collections; namespace Server.Spells.Seventh { @@ -27,89 +26,77 @@ namespace Server.Spells.Seventh public void Target(IPoint3D p) { - if (SpellHelper.CheckTown(p, Caster) && CheckSequence()) + var loc = (p as Item)?.GetWorldLocation() ?? new Point3D(p); + if (SpellHelper.CheckTown(loc, Caster) && CheckSequence()) { SpellHelper.Turn(Caster, p); - if (p is Item item) - { - p = item.GetWorldLocation(); - } - - var targets = new List(); - var map = Caster.Map; - var playerVsPlayer = false; - if (map != null) { - var eable = map.GetMobilesInRange(new Point3D(p), 2); + using var pool = PooledRefQueue.Create(); + var pvp = false; - targets.AddRange( - eable.Where( - m => - { - if (Core.AOS && (m == Caster || !Caster.InLOS(m)) || - !SpellHelper.ValidIndirectTarget(Caster, m) || - !Caster.CanBeHarmful(m, false)) - { - return false; - } - - if (m.Player) - { - playerVsPlayer = true; - } - - return true; - } - ) - .ToList() - ); - - eable.Free(); - } - - double damage; - - damage = Core.AOS - ? GetNewAosDamage(51, 1, 5, playerVsPlayer) - : Utility.Random(27, 22); - - if (targets.Count > 0) - { - if (Core.AOS && targets.Count > 2) + var eable = map.GetMobilesInRange(loc, 2); + foreach (var m in eable) { - damage = damage * 2 / targets.Count; - } - else if (!Core.AOS) - { - damage /= targets.Count; - } - - for (var i = 0; i < targets.Count; ++i) - { - var toDeal = damage; - var m = targets[i]; - - if (!Core.AOS && CheckResisted(m)) + if (Core.AOS && (m == Caster || !Caster.InLOS(m)) || + !SpellHelper.ValidIndirectTarget(Caster, m) || + !Caster.CanBeHarmful(m, false)) { - toDeal *= 0.5; - - m.SendLocalizedMessage(501783); // You feel yourself resisting magical energy. + continue; } - toDeal *= GetDamageScalar(m); - Caster.DoHarmful(m); - SpellHelper.Damage(this, m, toDeal, 0, 0, 0, 0, 100); + if (m.Player) + { + pvp = true; + } - m.BoltEffect(0); + pool.Enqueue(m); + } + + eable.Free(); + + if (pool.Count > 0) + { + double damage = Core.AOS + ? GetNewAosDamage(51, 1, 5, pvp) + : Utility.Random(27, 22); + + if (pool.Count > 2) + { + if (Core.AOS) + { + damage *= 2; + } + + damage /= pool.Count; + } + + while (pool.Count > 0) + { + var toDeal = damage; + var m = pool.Dequeue(); + + if (!Core.AOS && CheckResisted(m)) + { + toDeal *= 0.5; + + m.SendLocalizedMessage(501783); // You feel yourself resisting magical energy. + } + + toDeal *= GetDamageScalar(m); + Caster.DoHarmful(m); + SpellHelper.Damage(this, m, toDeal, 0, 0, 0, 0, 100); + + m.BoltEffect(0); + } + } + else + { + Caster.PlaySound(0x29); } - } - else - { - Caster.PlaySound(0x29); } } diff --git a/Projects/UOContent/Spells/Seventh/EnergyField.cs b/Projects/UOContent/Spells/Seventh/EnergyField.cs index d802cd5e8..abb0895ef 100644 --- a/Projects/UOContent/Spells/Seventh/EnergyField.cs +++ b/Projects/UOContent/Spells/Seventh/EnergyField.cs @@ -45,10 +45,8 @@ namespace Server.Spells.Seventh } else { - duration = TimeSpan.FromSeconds( - Caster.Skills.Magery.Value * 0.28 + - 2.0 - ); // (28% of magery) + 2.0 seconds + // (28% of magery) + 2.0 seconds + duration = TimeSpan.FromSeconds(Caster.Skills.Magery.Value * 0.28 + 2.0); } var itemID = eastToWest ? 0x3946 : 0x3956; diff --git a/Projects/UOContent/Spells/Seventh/MeteorSwarm.cs b/Projects/UOContent/Spells/Seventh/MeteorSwarm.cs index 19899f7b0..f8f934a18 100644 --- a/Projects/UOContent/Spells/Seventh/MeteorSwarm.cs +++ b/Projects/UOContent/Spells/Seventh/MeteorSwarm.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Linq; +using Server.Collections; namespace Server.Spells.Seventh { @@ -36,8 +35,6 @@ namespace Server.Spells.Seventh p = item.GetWorldLocation(); } - List targets; - var map = Caster.Map; var playerVsPlayer = false; @@ -46,69 +43,63 @@ namespace Server.Spells.Seventh if (map != null) { var eable = map.GetMobilesInRange(loc, 2); - - targets = eable.Where( - m => - { - if (Caster == m || !SpellHelper.ValidIndirectTarget(Caster, m) || - !Caster.CanBeHarmful(m, false) || - Core.AOS && !Caster.InLOS(m)) - { - return false; - } - - if (m.Player) - { - playerVsPlayer = true; - } - - return true; - } - ) - .ToList(); - - eable.Free(); - } - else - { - targets = new List(); - } - - double damage = Core.AOS - ? GetNewAosDamage(51, 1, 5, playerVsPlayer) - : Utility.Random(27, 22); - - if (targets.Count > 0) - { - Effects.PlaySound(loc, Caster.Map, 0x160); - - if (Core.AOS && targets.Count > 2) + using var queue = PooledRefQueue.Create(); + foreach (var m in eable) { - damage = damage * 2 / targets.Count; - } - else if (!Core.AOS) - { - damage /= targets.Count; - } - - for (var i = 0; i < targets.Count; ++i) - { - var m = targets[i]; - - var toDeal = damage; - - if (!Core.AOS && CheckResisted(m)) + if (Caster == m || !SpellHelper.ValidIndirectTarget(Caster, m) || + !Caster.CanBeHarmful(m, false) || Core.AOS && !Caster.InLOS(m)) { - damage *= 0.5; - - m.SendLocalizedMessage(501783); // You feel yourself resisting magical energy. + continue; } - toDeal *= GetDamageScalar(m); - Caster.DoHarmful(m); - SpellHelper.Damage(this, m, toDeal, 0, 100, 0, 0, 0); + if (m.Player) + { + playerVsPlayer = true; + } - Caster.MovingParticles(m, 0x36D4, 7, 0, false, true, 9501, 1, 0, 0x100); + queue.Enqueue(m); + } + + eable.Free(); + + double damage = Core.AOS + ? GetNewAosDamage(51, 1, 5, playerVsPlayer) + : Utility.Random(27, 22); + + int count = queue.Count; + + if (count > 0) + { + Effects.PlaySound(loc, Caster.Map, 0x160); + + if (Core.AOS && count > 2) + { + damage = damage * 2 / count; + } + else if (!Core.AOS) + { + damage /= count; + } + + while (queue.Count > 0) + { + var m = queue.Dequeue(); + + var toDeal = damage; + + if (!Core.AOS && CheckResisted(m)) + { + damage *= 0.5; + + m.SendLocalizedMessage(501783); // You feel yourself resisting magical energy. + } + + toDeal *= GetDamageScalar(m); + Caster.DoHarmful(m); + SpellHelper.Damage(this, m, toDeal, 0, 100, 0, 0, 0); + + Caster.MovingParticles(m, 0x36D4, 7, 0, false, true, 9501, 1, 0, 0x100); + } } } } diff --git a/Projects/UOContent/Spells/Sixth/Explosion.cs b/Projects/UOContent/Spells/Sixth/Explosion.cs index 16b0f69b3..7b21fd54e 100644 --- a/Projects/UOContent/Spells/Sixth/Explosion.cs +++ b/Projects/UOContent/Spells/Sixth/Explosion.cs @@ -20,22 +20,26 @@ namespace Server.Spells.Sixth public override SpellCircle Circle => SpellCircle.Sixth; - public override bool DelayedDamageStacking => !Core.AOS; + public override Type[] DelayedDamageSpellFamilyStacking => AOSNoDelayedDamageStackingSelf; public override bool DelayedDamage => false; public void Target(Mobile m) { + if (Core.SA && HasDelayedDamageContext(m)) + { + DoHurtFizzle(); + return; + } + if (Caster.CanBeHarmful(m) && CheckSequence()) { - Mobile attacker = Caster, defender = m; + Mobile defender = m; SpellHelper.Turn(Caster, m); - SpellHelper.CheckReflect((int)Circle, Caster, ref m); - var t = new InternalTimer(this, attacker, defender, m); - t.Start(); + var t = new InternalTimer(this, Caster, defender, m).Start(); } FinishSequence(); @@ -48,52 +52,52 @@ namespace Server.Spells.Sixth private class InternalTimer : Timer { - private readonly Mobile m_Attacker; - private readonly Mobile m_Defender; - private readonly MagerySpell m_Spell; - private readonly Mobile m_Target; + private readonly Mobile _attacker; + private readonly Mobile _defender; + private readonly MagerySpell _spell; + private readonly Mobile _target; public InternalTimer(MagerySpell spell, Mobile attacker, Mobile defender, Mobile target) : base(TimeSpan.FromSeconds(Core.AOS ? 3.0 : 2.5)) { - m_Spell = spell; - m_Attacker = attacker; - m_Defender = defender; - m_Target = target; + _spell = spell; + _attacker = attacker; + _defender = defender; + _target = target; - m_Spell?.StartDelayedDamageContext(attacker, this); + _spell?.StartDelayedDamageContext(_attacker, this); } protected override void OnTick() { - if (m_Attacker.HarmfulCheck(m_Defender)) + if (_attacker.HarmfulCheck(_defender)) { double damage; if (Core.AOS) { - damage = m_Spell.GetNewAosDamage(40, 1, 5, m_Defender); + damage = _spell.GetNewAosDamage(40, 1, 5, _defender); } else { damage = Utility.Random(23, 22); - if (m_Spell.CheckResisted(m_Target)) + if (_spell.CheckResisted(_target)) { damage *= 0.75; - m_Target.SendLocalizedMessage(501783); // You feel yourself resisting magical energy. + _target.SendLocalizedMessage(501783); // You feel yourself resisting magical energy. } - damage *= m_Spell.GetDamageScalar(m_Target); + damage *= _spell.GetDamageScalar(_target); } - m_Target.FixedParticles(0x36BD, 20, 10, 5044, EffectLayer.Head); - m_Target.PlaySound(0x307); + _target.FixedParticles(0x36BD, 20, 10, 5044, EffectLayer.Head); + _target.PlaySound(0x307); - SpellHelper.Damage(m_Spell, m_Target, damage, 0, 100, 0, 0, 0); + SpellHelper.Damage(_spell, _target, damage, 0, 100, 0, 0, 0); - m_Spell?.RemoveDelayedDamageContext(m_Attacker); + _spell?.RemoveDelayedDamageContext(_attacker); } } } diff --git a/Projects/UOContent/Spells/Third/Poison.cs b/Projects/UOContent/Spells/Third/Poison.cs index 584d3a20e..67d0e3b01 100644 --- a/Projects/UOContent/Spells/Third/Poison.cs +++ b/Projects/UOContent/Spells/Third/Poison.cs @@ -43,24 +43,13 @@ namespace Server.Spells.Third { if (Caster.InRange(m, 2)) { - var total = (Caster.Skills.Magery.Fixed + Caster.Skills.Poisoning.Fixed) / 2; - - if (total >= 1000) + level = (Caster.Skills.Magery.Fixed + Caster.Skills.Poisoning.Fixed) / 2 switch { - level = 3; - } - else if (total > 850) - { - level = 2; - } - else if (total > 650) - { - level = 1; - } - else - { - level = 0; - } + >= 1000 => 3, + > 850 => 2, + > 650 => 1, + _ => 0 + }; } else {