feat: apply Spell Focusing to eligible spell damage
This commit is contained in:
parent
84e73b81c8
commit
1ad7593ddd
18 changed files with 228 additions and 3 deletions
|
|
@ -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; } = [];
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
58
Projects/UOContent/Spells/Base/SpellFocusing.cs
Normal file
58
Projects/UOContent/Spells/Base/SpellFocusing.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue