diff --git a/Projects/UOContent.Tests/Tests/Items/SpellFocusingPropertyTests.cs b/Projects/UOContent.Tests/Tests/Items/SpellFocusingPropertyTests.cs index 997171110..cf43be1ed 100644 --- a/Projects/UOContent.Tests/Tests/Items/SpellFocusingPropertyTests.cs +++ b/Projects/UOContent.Tests/Tests/Items/SpellFocusingPropertyTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Server; using Server.Items; +using Server.Spells; using Server.Text; using Xunit; @@ -60,6 +61,75 @@ public class SpellFocusingPropertyTests } } + [Fact] + public void SpellFocusing_UsesAnyEquippedItemWithTheProperty() + { + var previousExpansion = Core.Expansion; + var caster = new Mobile(World.NewMobile); + var target = new Mobile(World.NewMobile); + var item = new Katana(); + var spell = new TestSpell(caster); + + try + { + Core.Expansion = Expansion.AOS; + caster.DefaultMobileInit(); + caster.Player = true; + target.DefaultMobileInit(); + target.Player = false; + caster.AddItem(item); + item.Attributes.SpellFocusing = 1; + + var expected = new[] { -30, -24, -18, -12, -6, 0, 2 }; + + foreach (var expectedOffset in expected) + { + Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, target, out var offset)); + Assert.Equal(expectedOffset, offset); + } + + target.Delete(); + var replacement = new Mobile(World.NewMobile); + replacement.DefaultMobileInit(); + replacement.Player = false; + + try + { + Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, replacement, out var resetOffset)); + Assert.Equal(-30, resetOffset); + } + finally + { + replacement.Delete(); + } + } + finally + { + Core.Expansion = previousExpansion; + item.Delete(); + target.Delete(); + caster.Delete(); + } + } + + private sealed class TestSpell : Spell + { + private static readonly SpellInfo TestInfo = new("Test Spell", "test"); + + public TestSpell(Mobile caster) : base(caster, null, TestInfo) + { + } + + public override bool SpellFocusingEligible => true; + public override TimeSpan CastDelayBase => TimeSpan.Zero; + + public override void OnCast() + { + } + + public override int GetMana() => 0; + } + private sealed class RecordingPropertyList : IPropertyList { public List Numbers { get; } = []; diff --git a/Projects/UOContent/Misc/AOS.cs b/Projects/UOContent/Misc/AOS.cs index 498b17cc5..b7e1c704c 100644 --- a/Projects/UOContent/Misc/AOS.cs +++ b/Projects/UOContent/Misc/AOS.cs @@ -332,6 +332,10 @@ namespace Server public sealed class AosAttributes : BaseAttributes { + private Mobile _spellFocusingOwner; + private Mobile _spellFocusingTarget; + private int _spellFocusingCount; + public AosAttributes(Item owner) : base(owner) { } @@ -518,7 +522,56 @@ namespace Server public int SpellFocusing { get => this[AosAttribute.SpellFocusing]; - set => this[AosAttribute.SpellFocusing] = value; + set + { + if (SpellFocusing == value) + { + return; + } + + this[AosAttribute.SpellFocusing] = value; + ResetSpellFocusing(); + } + } + + internal void ResetSpellFocusing() + { + _spellFocusingOwner = null; + _spellFocusingTarget = null; + _spellFocusingCount = 0; + } + + internal int GetSpellFocusingOffset(Mobile caster, Mobile target) + { + if (_spellFocusingOwner != caster || _spellFocusingTarget?.Deleted != false || !_spellFocusingTarget.Alive) + { + ResetSpellFocusing(); + _spellFocusingOwner = caster; + _spellFocusingTarget = target; + } + else if (_spellFocusingTarget != target) + { + ResetSpellFocusing(); + _spellFocusingOwner = caster; + _spellFocusingTarget = target; + } + + var offset = GetSpellFocusingOffset(_spellFocusingCount, _spellFocusingTarget.Player); + + _spellFocusingCount++; + + if (_spellFocusingCount >= 21) + { + ResetSpellFocusing(); + } + + return offset; + } + + private static int GetSpellFocusingOffset(int castCount, bool playerTarget) + { + var offset = castCount < 6 ? -30 + castCount * 6 : (castCount - 5) * 2; + return Math.Min(offset, playerTarget ? 20 : 30); } public void GetProperties(IPropertyList list, int damageBonus = 0, int hitChanceBonus = 0, int luckBonus = 0) diff --git a/Projects/UOContent/Spells/Base/Spell.cs b/Projects/UOContent/Spells/Base/Spell.cs index 088ba0d4f..80a700c93 100644 --- a/Projects/UOContent/Spells/Base/Spell.cs +++ b/Projects/UOContent/Spells/Base/Spell.cs @@ -57,6 +57,8 @@ namespace Server.Spells public virtual bool DelayedDamage => false; + public virtual bool SpellFocusingEligible => false; + public static readonly Type[] AOSNoDelayedDamageStackingSelf = Core.AOS ? Array.Empty() : null; // Null means stacking is allowed while empty indicates no stacking with self diff --git a/Projects/UOContent/Spells/Base/SpellFocusing.cs b/Projects/UOContent/Spells/Base/SpellFocusing.cs new file mode 100644 index 000000000..9d0facc2a --- /dev/null +++ b/Projects/UOContent/Spells/Base/SpellFocusing.cs @@ -0,0 +1,58 @@ +using ModernUO.CodeGeneratedEvents; +using Server.Items; +using Server.Mobiles; + +namespace Server.Spells; + +public static class SpellFocusing +{ + public static void Configure() + { + EventSink.Logout += Clear; + } + + [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] + [OnEvent(nameof(PlayerMobile.PlayerDeletedEvent))] + [OnEvent(nameof(BaseCreature.CreatureDeathEvent))] + [OnEvent(nameof(BaseCreature.CreatureDeletedEvent))] + public static void Clear(Mobile mobile) + { + if (mobile == null) + { + return; + } + + var items = mobile.Items; + + for (var i = 0; i < items.Count; i++) + { + if (items[i] is IAosItem item) + { + item.Attributes.ResetSpellFocusing(); + } + } + } + + public static bool TryGetDamageOffset(Spell spell, Mobile caster, Mobile target, out int offset) + { + offset = 0; + + if (!Core.AOS || spell?.SpellFocusingEligible != true || caster?.Alive != true || target?.Alive != true) + { + return false; + } + + var items = caster.Items; + + for (var i = 0; i < items.Count; i++) + { + if (items[i] is IAosItem { Attributes.SpellFocusing: not 0 } item) + { + offset = item.Attributes.GetSpellFocusingOffset(caster, target); + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Base/SpellHelper.cs b/Projects/UOContent/Spells/Base/SpellHelper.cs index dc7e22f80..83379c5b4 100644 --- a/Projects/UOContent/Spells/Base/SpellHelper.cs +++ b/Projects/UOContent/Spells/Base/SpellHelper.cs @@ -958,6 +958,11 @@ namespace Server.Spells bcFrom?.AlterSpellDamageTo(target, ref damageGiven); bcTarget?.AlterSpellDamageFrom(from, ref damageGiven); + if (SpellFocusing.TryGetDamageOffset(spell, from, target, out var spellFocusingOffset)) + { + damageGiven = AOS.Scale(damageGiven, 100 + spellFocusingOffset); + } + target.Damage(damageGiven, from); bcFrom?.OnDamageSpell(target, damageGiven); @@ -1026,6 +1031,11 @@ namespace Server.Spells bcTarget?.AlterSpellDamageFrom(from, ref dmg); + if (SpellFocusing.TryGetDamageOffset(spell, from, target, out var spellFocusingOffset)) + { + dmg = AOS.Scale(dmg, 100 + spellFocusingOffset); + } + if (Feint.GetDamageReduction(from, target, out var feintReduction)) { // example: 35 damage * 50 / 100 = 17 damage @@ -1110,6 +1120,11 @@ namespace Server.Spells (m_From as BaseCreature)?.AlterSpellDamageTo(m_Target, ref m_Damage); (m_Target as BaseCreature)?.AlterSpellDamageFrom(m_From, ref m_Damage); + if (SpellFocusing.TryGetDamageOffset(m_Spell, m_From, m_Target, out var spellFocusingOffset)) + { + m_Damage = AOS.Scale(m_Damage, 100 + spellFocusingOffset); + } + m_Target.Damage(m_Damage); m_Spell?.RemoveDelayedDamageContext(m_Target); } diff --git a/Projects/UOContent/Spells/Fifth/MindBlast.cs b/Projects/UOContent/Spells/Fifth/MindBlast.cs index 38e0c5548..bf3302405 100644 --- a/Projects/UOContent/Spells/Fifth/MindBlast.cs +++ b/Projects/UOContent/Spells/Fifth/MindBlast.cs @@ -26,6 +26,8 @@ namespace Server.Spells.Fifth public override SpellCircle Circle => SpellCircle.Fifth; + public override bool SpellFocusingEligible => true; + public override bool DelayedDamage => !Core.AOS; public void Target(Mobile m) diff --git a/Projects/UOContent/Spells/First/MagicArrow.cs b/Projects/UOContent/Spells/First/MagicArrow.cs index d8425a3ac..1f98a8c11 100644 --- a/Projects/UOContent/Spells/First/MagicArrow.cs +++ b/Projects/UOContent/Spells/First/MagicArrow.cs @@ -20,6 +20,8 @@ namespace Server.Spells.First public override SpellCircle Circle => SpellCircle.First; + public override bool SpellFocusingEligible => true; + private static readonly Type[] _delayedDamageSpellFamilyStacking = Core.AOS ? [typeof(NetherBoltSpell)] : null; public override Type[] DelayedDamageSpellFamilyStacking => _delayedDamageSpellFamilyStacking; diff --git a/Projects/UOContent/Spells/Fourth/Lightning.cs b/Projects/UOContent/Spells/Fourth/Lightning.cs index 335580c33..4daf13e53 100644 --- a/Projects/UOContent/Spells/Fourth/Lightning.cs +++ b/Projects/UOContent/Spells/Fourth/Lightning.cs @@ -19,6 +19,8 @@ namespace Server.Spells.Fourth public override SpellCircle Circle => SpellCircle.Fourth; + public override bool SpellFocusingEligible => true; + public override bool DelayedDamage => false; public void Target(Mobile m) diff --git a/Projects/UOContent/Spells/Mysticism/BombardSpell.cs b/Projects/UOContent/Spells/Mysticism/BombardSpell.cs index baaea9c0c..cfa2d64b9 100644 --- a/Projects/UOContent/Spells/Mysticism/BombardSpell.cs +++ b/Projects/UOContent/Spells/Mysticism/BombardSpell.cs @@ -20,6 +20,7 @@ public class BombardSpell : MysticSpell, ITargetingSpell } public override SpellCircle Circle => SpellCircle.Sixth; + public override bool SpellFocusingEligible => true; public override bool DelayedDamage => true; public override Type[] DelayedDamageSpellFamilyStacking => AOSNoDelayedDamageStackingSelf; diff --git a/Projects/UOContent/Spells/Mysticism/EagleStrikeSpell.cs b/Projects/UOContent/Spells/Mysticism/EagleStrikeSpell.cs index 0e1d1af28..81ee12f68 100644 --- a/Projects/UOContent/Spells/Mysticism/EagleStrikeSpell.cs +++ b/Projects/UOContent/Spells/Mysticism/EagleStrikeSpell.cs @@ -21,6 +21,7 @@ public class EagleStrikeSpell : MysticSpell, ITargetingSpell } public override SpellCircle Circle => SpellCircle.Third; + public override bool SpellFocusingEligible => true; public void Target(Mobile m) { diff --git a/Projects/UOContent/Spells/Mysticism/NetherBoltSpell.cs b/Projects/UOContent/Spells/Mysticism/NetherBoltSpell.cs index 1397e7d9e..f7a57dde7 100644 --- a/Projects/UOContent/Spells/Mysticism/NetherBoltSpell.cs +++ b/Projects/UOContent/Spells/Mysticism/NetherBoltSpell.cs @@ -22,6 +22,7 @@ public class NetherBoltSpell : MysticSpell, ITargetingSpell } public override SpellCircle Circle => SpellCircle.First; + public override bool SpellFocusingEligible => true; public override bool DelayedDamage => true; diff --git a/Projects/UOContent/Spells/Necromancy/PainSpike.cs b/Projects/UOContent/Spells/Necromancy/PainSpike.cs index eca3bd275..3ee05f0f2 100644 --- a/Projects/UOContent/Spells/Necromancy/PainSpike.cs +++ b/Projects/UOContent/Spells/Necromancy/PainSpike.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Server.Engines.BuffIcons; using Server.Misc; using Server.Mobiles; +using Server.Spells; using Server.Targeting; namespace Server.Spells.Necromancy; @@ -30,6 +31,7 @@ public class PainSpikeSpell : NecromancerSpell, ITargetingSpell public override int RequiredMana => 5; public override bool DelayedDamage => false; + public override bool SpellFocusingEligible => true; public static bool UnderEffect(Mobile m) => _table.ContainsKey(m); @@ -76,11 +78,15 @@ public class PainSpikeSpell : NecromancerSpell, ITargetingSpell // TODO: Find a better way to do this StaminaSystem.DFA = DFAlgorithm.PainSpike; + + if (SpellFocusing.TryGetDamageOffset(this, Caster, m, out var spellFocusingOffset)) + { + damage = AOS.Scale((int)damage, 100 + spellFocusingOffset); + } + m.Damage((int)damage, Caster, ignoreEvilOmen: true); SpellHelper.DoLeech((int)damage, Caster, m); StaminaSystem.DFA = DFAlgorithm.Standard; - - // SpellHelper.Damage( this, m, damage, 100, 0, 0, 0, 0, Misc.DFAlgorithm.PainSpike ); HarmfulSpell(m); } } diff --git a/Projects/UOContent/Spells/Second/Harm.cs b/Projects/UOContent/Spells/Second/Harm.cs index 46eaf9856..c7f5b7ec0 100644 --- a/Projects/UOContent/Spells/Second/Harm.cs +++ b/Projects/UOContent/Spells/Second/Harm.cs @@ -19,6 +19,8 @@ namespace Server.Spells.Second public override SpellCircle Circle => SpellCircle.Second; + public override bool SpellFocusingEligible => true; + public override bool DelayedDamage => false; public void Target(Mobile m) diff --git a/Projects/UOContent/Spells/Seventh/FlameStrike.cs b/Projects/UOContent/Spells/Seventh/FlameStrike.cs index 1e021a5fa..ab871120a 100644 --- a/Projects/UOContent/Spells/Seventh/FlameStrike.cs +++ b/Projects/UOContent/Spells/Seventh/FlameStrike.cs @@ -19,6 +19,8 @@ namespace Server.Spells.Seventh public override SpellCircle Circle => SpellCircle.Seventh; + public override bool SpellFocusingEligible => true; + public override bool DelayedDamage => true; public void Target(Mobile m) diff --git a/Projects/UOContent/Spells/Sixth/EnergyBolt.cs b/Projects/UOContent/Spells/Sixth/EnergyBolt.cs index d72aaf55f..5bca8d5c7 100644 --- a/Projects/UOContent/Spells/Sixth/EnergyBolt.cs +++ b/Projects/UOContent/Spells/Sixth/EnergyBolt.cs @@ -19,6 +19,8 @@ namespace Server.Spells.Sixth public override SpellCircle Circle => SpellCircle.Sixth; + public override bool SpellFocusingEligible => true; + public override bool DelayedDamage => true; public void Target(Mobile m) diff --git a/Projects/UOContent/Spells/Sixth/Explosion.cs b/Projects/UOContent/Spells/Sixth/Explosion.cs index 9e101f03e..b58bbdc51 100644 --- a/Projects/UOContent/Spells/Sixth/Explosion.cs +++ b/Projects/UOContent/Spells/Sixth/Explosion.cs @@ -20,6 +20,8 @@ namespace Server.Spells.Sixth public override SpellCircle Circle => SpellCircle.Sixth; + public override bool SpellFocusingEligible => true; + public override Type[] DelayedDamageSpellFamilyStacking => AOSNoDelayedDamageStackingSelf; public override bool DelayedDamage => false; diff --git a/Projects/UOContent/Spells/Spellweaving/WordOfDeath.cs b/Projects/UOContent/Spells/Spellweaving/WordOfDeath.cs index 04143c6cf..252aef4ac 100644 --- a/Projects/UOContent/Spells/Spellweaving/WordOfDeath.cs +++ b/Projects/UOContent/Spells/Spellweaving/WordOfDeath.cs @@ -16,6 +16,8 @@ namespace Server.Spells.Spellweaving public override double RequiredSkill => 80.0; public override int RequiredMana => 50; + public override bool SpellFocusingEligible => true; + public void Target(Mobile m) { if (CheckHSequence(m)) diff --git a/Projects/UOContent/Spells/Third/Fireball.cs b/Projects/UOContent/Spells/Third/Fireball.cs index 84d8d5b82..2c17cf98d 100644 --- a/Projects/UOContent/Spells/Third/Fireball.cs +++ b/Projects/UOContent/Spells/Third/Fireball.cs @@ -18,6 +18,8 @@ namespace Server.Spells.Third public override SpellCircle Circle => SpellCircle.Third; + public override bool SpellFocusingEligible => true; + public override bool DelayedDamage => true; public void Target(Mobile m)