feat: apply Spell Focusing to eligible spell damage

This commit is contained in:
Crome696 2026-07-10 09:29:16 +02:00
parent 84e73b81c8
commit 1ad7593ddd
18 changed files with 228 additions and 3 deletions

View file

@ -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<int> Numbers { get; } = [];

View file

@ -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)

View file

@ -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<Type>() : null;
// Null means stacking is allowed while empty indicates no stacking with self

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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)

View file

@ -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;

View file

@ -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)

View file

@ -20,6 +20,7 @@ public class BombardSpell : MysticSpell, ITargetingSpell<Mobile>
}
public override SpellCircle Circle => SpellCircle.Sixth;
public override bool SpellFocusingEligible => true;
public override bool DelayedDamage => true;
public override Type[] DelayedDamageSpellFamilyStacking => AOSNoDelayedDamageStackingSelf;

View file

@ -21,6 +21,7 @@ public class EagleStrikeSpell : MysticSpell, ITargetingSpell<Mobile>
}
public override SpellCircle Circle => SpellCircle.Third;
public override bool SpellFocusingEligible => true;
public void Target(Mobile m)
{

View file

@ -22,6 +22,7 @@ public class NetherBoltSpell : MysticSpell, ITargetingSpell<Mobile>
}
public override SpellCircle Circle => SpellCircle.First;
public override bool SpellFocusingEligible => true;
public override bool DelayedDamage => true;

View file

@ -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<Mobile>
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<Mobile>
// 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);
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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;

View file

@ -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))

View file

@ -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)