feat: implement Spell Focusing Sash (Closes #22)
Adds the Stygian Abyss-era Spell Focusing Sash with full lifecycle, equipment stat block, and damage sequence behavior: - New SpellFocusingSash (BaseMiddleTorso) with Brittle, Mana Increase, Defense Chance Increase, +10 Strength Requirement, and 255/255 durability. - Context-menu enable/disable on the worn sash. - Sequence damage modifier: -30 -> 0 over six casts, then +2 per cast capped at +30 (PvM) or +20 held for five casts (PvP), resetting at the peak, on target change, on unequip, on disable, on death, and on logout. - Centralized eligibility gate (SpellFocusingEligible) and shared SpellHelper.Damage hooks, plus a direct hook in PainSpike. - Brittle is forced on under Core.SA via AOS.IsBrittle. - Regression tests covering stats/property ordering, enable state serialization, context-menu toggle, PvM and PvP sequences, target resets, disabled state, and non-eligible spells.
This commit is contained in:
parent
fa3b9024cb
commit
28513b00dd
18 changed files with 583 additions and 0 deletions
280
Projects/UOContent.Tests/Tests/Items/SpellFocusingSashTests.cs
Normal file
280
Projects/UOContent.Tests/Tests/Items/SpellFocusingSashTests.cs
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Server;
|
||||
using Server.Collections;
|
||||
using Server.ContextMenus;
|
||||
using Server.Items;
|
||||
using Server.Spells;
|
||||
using Server.Tests;
|
||||
using Server.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class SpellFocusingSashTests
|
||||
{
|
||||
private const int SpellFocusingCliloc = 1150058;
|
||||
private const int BrittleCliloc = 1116209;
|
||||
private const int ManaIncreaseCliloc = 1060439;
|
||||
private const int DefendChanceCliloc = 1060408;
|
||||
private const int StrengthRequirementCliloc = 1061170;
|
||||
private const int DurabilityCliloc = 1060639;
|
||||
|
||||
[Fact]
|
||||
public void SpellFocusingSash_HasArtifactStatsAndPropertyOrder()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var sash = new SpellFocusingSash();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
var list = new RecordingPropertyList();
|
||||
sash.GetProperties(list);
|
||||
|
||||
Assert.Equal(1.0, sash.DefaultWeight);
|
||||
Assert.Equal(1, sash.Attributes.BonusMana);
|
||||
Assert.Equal(5, sash.Attributes.DefendChance);
|
||||
Assert.Equal(10, sash.StrRequirement);
|
||||
Assert.Equal(255, sash.HitPoints);
|
||||
Assert.Equal(255, sash.MaxHitPoints);
|
||||
|
||||
var spellFocusingIndex = list.Entries.FindIndex(entry => entry.Number == SpellFocusingCliloc);
|
||||
Assert.True(spellFocusingIndex > 0);
|
||||
Assert.Equal(1072788, list.Entries[spellFocusingIndex - 1].Number);
|
||||
Assert.True(spellFocusingIndex < list.Entries.FindIndex(entry => entry.Number == BrittleCliloc));
|
||||
Assert.Contains(list.Entries, entry => entry.Number == StrengthRequirementCliloc && entry.Argument == "10");
|
||||
Assert.Contains(list.Entries, entry => entry.Number == DurabilityCliloc && entry.Argument == "255\t255");
|
||||
Assert.True(
|
||||
list.Entries.FindIndex(entry => entry.Number == DefendChanceCliloc) <
|
||||
list.Entries.FindIndex(entry => entry.Number == ManaIncreaseCliloc)
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
sash.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpellFocusingSash_EnabledStateSerializesAndContextMenuTogglesIt()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var caster = CreateMobile(player: true);
|
||||
var sash = new SpellFocusingSash();
|
||||
var deserialized = new SpellFocusingSash();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
caster.AddItem(sash);
|
||||
|
||||
var menu = ContextMenuSystem.CreateContextMenu(caster, sash);
|
||||
var entry = Assert.Single(menu.Entries, e => e.Number == 3006151);
|
||||
entry.OnClick(caster, sash);
|
||||
Assert.False(sash.Enabled);
|
||||
|
||||
sash.Enabled = true;
|
||||
var writer = new BufferWriter(true);
|
||||
sash.Enabled = false;
|
||||
sash.Serialize(writer);
|
||||
var buffer = new byte[writer.Position];
|
||||
writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer);
|
||||
deserialized.Deserialize(new BufferReader(buffer));
|
||||
Assert.False(deserialized.Enabled);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
sash.Delete();
|
||||
deserialized.Delete();
|
||||
caster.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpellFocusingSash_AppliesPvMSequenceAndResetsAfterPeak()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var caster = CreateMobile(player: true);
|
||||
var target = CreateMobile(player: false);
|
||||
var sash = new SpellFocusingSash();
|
||||
var spell = new TestSpell(caster);
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
caster.AddItem(sash);
|
||||
var expected = new[] { -30, -24, -18, -12, -6, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30 };
|
||||
|
||||
foreach (var expectedOffset in expected)
|
||||
{
|
||||
Assert.True(SpellFocusingSash.TryGetDamageOffset(spell, caster, target, out var actual));
|
||||
Assert.Equal(expectedOffset, actual);
|
||||
}
|
||||
|
||||
Assert.True(SpellFocusingSash.TryGetDamageOffset(spell, caster, target, out var resetOffset));
|
||||
Assert.Equal(-30, resetOffset);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
sash.Delete();
|
||||
target.Delete();
|
||||
caster.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpellFocusingSash_HoldsPvPCapForFiveSpellsAndResetsOnTargetChange()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var caster = CreateMobile(player: true);
|
||||
var target = CreateMobile(player: true);
|
||||
var secondTarget = CreateMobile(player: true);
|
||||
var sash = new SpellFocusingSash();
|
||||
var spell = new TestSpell(caster);
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
caster.AddItem(sash);
|
||||
|
||||
for (var i = 0; i < 15; i++)
|
||||
{
|
||||
Assert.True(SpellFocusingSash.TryGetDamageOffset(spell, caster, target, out _));
|
||||
}
|
||||
|
||||
for (var i = 0; i < 6; i++)
|
||||
{
|
||||
Assert.True(SpellFocusingSash.TryGetDamageOffset(spell, caster, target, out var offset));
|
||||
Assert.Equal(20, offset);
|
||||
}
|
||||
|
||||
Assert.True(SpellFocusingSash.TryGetDamageOffset(spell, caster, target, out var resetOffset));
|
||||
Assert.Equal(-30, resetOffset);
|
||||
|
||||
Assert.True(SpellFocusingSash.TryGetDamageOffset(spell, caster, secondTarget, out var targetResetOffset));
|
||||
Assert.Equal(-30, targetResetOffset);
|
||||
|
||||
sash.Enabled = false;
|
||||
Assert.False(SpellFocusingSash.TryGetDamageOffset(spell, caster, target, out _));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
sash.Delete();
|
||||
secondTarget.Delete();
|
||||
target.Delete();
|
||||
caster.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpellFocusingSash_ExcludesNonQualifyingSpells()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var caster = CreateMobile(player: true);
|
||||
var target = CreateMobile(player: false);
|
||||
var sash = new SpellFocusingSash();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
caster.AddItem(sash);
|
||||
Assert.False(SpellFocusingSash.TryGetDamageOffset(new NonQualifyingSpell(caster), caster, target, out _));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
sash.Delete();
|
||||
target.Delete();
|
||||
caster.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
private static Mobile CreateMobile(bool player)
|
||||
{
|
||||
var mobile = new Mobile(World.NewMobile);
|
||||
mobile.DefaultMobileInit();
|
||||
mobile.Player = player;
|
||||
mobile.RawStr = 100;
|
||||
mobile.Hits = mobile.HitsMax;
|
||||
return mobile;
|
||||
}
|
||||
|
||||
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 NonQualifyingSpell : Spell
|
||||
{
|
||||
private static readonly SpellInfo TestInfo = new("Non-Qualifying Spell", "no");
|
||||
|
||||
public NonQualifyingSpell(Mobile caster) : base(caster, null, TestInfo)
|
||||
{
|
||||
}
|
||||
|
||||
public override TimeSpan CastDelayBase => TimeSpan.Zero;
|
||||
public override void OnCast()
|
||||
{
|
||||
}
|
||||
|
||||
public override int GetMana() => 0;
|
||||
}
|
||||
|
||||
private sealed record PropertyEntry(int Number, string Argument);
|
||||
|
||||
private sealed class RecordingPropertyList : IPropertyList
|
||||
{
|
||||
private string _interpolated = string.Empty;
|
||||
|
||||
public List<PropertyEntry> Entries { get; } = [];
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
}
|
||||
|
||||
public void Terminate()
|
||||
{
|
||||
}
|
||||
|
||||
public void Add(int number) => Entries.Add(new PropertyEntry(number, string.Empty));
|
||||
public void Add(int number, string argument) => Entries.Add(new PropertyEntry(number, argument));
|
||||
public void Add(ReadOnlySpan<char> argument) => Entries.Add(new PropertyEntry(0, argument.ToString()));
|
||||
public void Add(int number, ReadOnlySpan<char> argument) => Entries.Add(new PropertyEntry(number, argument.ToString()));
|
||||
public void AddChunked(ReadOnlySpan<char> text) => Entries.Add(new PropertyEntry(0, text.ToString()));
|
||||
public OplTextBlock TextBlock() => new(this);
|
||||
public void Add(int number, int value) => Entries.Add(new PropertyEntry(number, value.ToString()));
|
||||
public void AddLocalized(int value) => Entries.Add(new PropertyEntry(0, value.ToString()));
|
||||
public void AddLocalized(int number, int value) => Entries.Add(new PropertyEntry(number, value.ToString()));
|
||||
public void Add(ref IPropertyList.InterpolatedStringHandler handler) => Entries.Add(new PropertyEntry(0, _interpolated));
|
||||
public void Add(int number, ref IPropertyList.InterpolatedStringHandler handler) => Entries.Add(new PropertyEntry(number, _interpolated));
|
||||
public void InitializeInterpolation(int literalLength, int formattedCount) => _interpolated = string.Empty;
|
||||
public void AppendLiteral(string value) => _interpolated += value;
|
||||
public void AppendFormatted<T>(T value) => _interpolated += value;
|
||||
public void AppendFormatted<T>(T value, string format) => _interpolated += value is IFormattable formattable ? formattable.ToString(format, null) : value;
|
||||
public void AppendFormatted<T>(T value, int alignment) => _interpolated += value;
|
||||
public void AppendFormatted<T>(T value, int alignment, string format) => _interpolated += value is IFormattable formattable ? formattable.ToString(format, null) : value;
|
||||
public void AppendFormatted(ReadOnlySpan<char> value) => _interpolated += value.ToString();
|
||||
public void AppendFormatted(ReadOnlySpan<char> value, int alignment, string format = null) => _interpolated += value.ToString();
|
||||
public void AppendFormatted(object value, int alignment = 0, string format = null) => _interpolated += value;
|
||||
public void AppendFormatted(string value) => _interpolated += value;
|
||||
public void AppendFormatted(string value, int alignment, string format = null) => _interpolated += value;
|
||||
}
|
||||
}
|
||||
248
Projects/UOContent/Items/Clothing/SpellFocusingSash.cs
Normal file
248
Projects/UOContent/Items/Clothing/SpellFocusingSash.cs
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
using ModernUO.CodeGeneratedEvents;
|
||||
using ModernUO.Serialization;
|
||||
using Server.Collections;
|
||||
using Server.ContextMenus;
|
||||
using Server.Engines.BuffIcons;
|
||||
using Server.Mobiles;
|
||||
using Server.Spells;
|
||||
|
||||
namespace Server.Items;
|
||||
|
||||
[Flippable(0x1541, 0x1542)]
|
||||
[SerializationGenerator(0, false)]
|
||||
public partial class SpellFocusingSash : BaseMiddleTorso
|
||||
{
|
||||
private const int SpellFocusingCliloc = 1150058;
|
||||
private const int BrittleCliloc = 1116209;
|
||||
private const int ResetMessage = 1150117;
|
||||
private const int TunedMessage = 1150118;
|
||||
private const int PeakMessage = 1150116;
|
||||
private const int BuffTitleCliloc = 1151391;
|
||||
private const int BuffSecondaryCliloc = 1151392;
|
||||
private const int SequenceLength = 21;
|
||||
|
||||
private Mobile _spellCastTarget;
|
||||
private int _spellCastCount;
|
||||
private bool _enabled = true;
|
||||
|
||||
[Constructible]
|
||||
public SpellFocusingSash() : base(0x1541)
|
||||
{
|
||||
Attributes.BonusMana = 1;
|
||||
Attributes.DefendChance = 5;
|
||||
HitPoints = MaxHitPoints = 255;
|
||||
}
|
||||
|
||||
public override int LabelNumber => 1150059;
|
||||
public override double DefaultWeight => 1.0;
|
||||
public override int InitMinHits => 255;
|
||||
public override int InitMaxHits => 255;
|
||||
public override int AosStrReq => 10;
|
||||
|
||||
[SerializableProperty(0, useField: nameof(_enabled))]
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public bool Enabled
|
||||
{
|
||||
get => _enabled;
|
||||
set
|
||||
{
|
||||
if (_enabled == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_enabled = value;
|
||||
ResetSequence(Parent as Mobile);
|
||||
InvalidateProperties();
|
||||
this.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public static void Configure()
|
||||
{
|
||||
EventSink.Logout += Clear;
|
||||
}
|
||||
|
||||
public override void AddNameProperties(IPropertyList list)
|
||||
{
|
||||
base.AddNameProperties(list);
|
||||
|
||||
if (Core.SA)
|
||||
{
|
||||
list.Add(SpellFocusingCliloc);
|
||||
list.Add(BrittleCliloc);
|
||||
}
|
||||
}
|
||||
|
||||
public override void GetContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list)
|
||||
{
|
||||
base.GetContextMenuEntries(from, ref list);
|
||||
|
||||
if (Core.SA && from == Parent && from.Alive)
|
||||
{
|
||||
list.Add(new ToggleSpellFocusingEntry(this));
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnAdded(IEntity parent)
|
||||
{
|
||||
base.OnAdded(parent);
|
||||
|
||||
if (parent is Mobile mobile)
|
||||
{
|
||||
ResetSequence(mobile);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnRemoved(IEntity parent)
|
||||
{
|
||||
if (parent is Mobile mobile)
|
||||
{
|
||||
ResetSequence(mobile);
|
||||
}
|
||||
|
||||
base.OnRemoved(parent);
|
||||
}
|
||||
|
||||
public override void OnDelete()
|
||||
{
|
||||
ResetSequence(Parent as Mobile);
|
||||
base.OnDelete();
|
||||
}
|
||||
|
||||
public static bool TryGetDamageOffset(Spell spell, Mobile caster, Mobile target, out int offset)
|
||||
{
|
||||
offset = 0;
|
||||
|
||||
if (!Core.SA || spell?.SpellFocusingEligible != true || caster?.Deleted != false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (caster.FindItemOnLayer(Layer.MiddleTorso) is not SpellFocusingSash sash || !sash.Enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target?.Deleted != false || !target.Alive || !caster.Alive)
|
||||
{
|
||||
sash.ResetSequence(caster);
|
||||
return false;
|
||||
}
|
||||
|
||||
return sash.TryGetDamageOffset(caster, target, out offset);
|
||||
}
|
||||
|
||||
[OnEvent(nameof(PlayerMobile.PlayerDeathEvent))]
|
||||
[OnEvent(nameof(PlayerMobile.PlayerDeletedEvent))]
|
||||
[OnEvent(nameof(BaseCreature.CreatureDeathEvent))]
|
||||
[OnEvent(nameof(BaseCreature.CreatureDeletedEvent))]
|
||||
public static void Clear(Mobile mobile)
|
||||
{
|
||||
if (mobile?.FindItemOnLayer(Layer.MiddleTorso) is SpellFocusingSash sash)
|
||||
{
|
||||
sash.ResetSequence(mobile);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetDamageOffset(Mobile caster, Mobile target, out int offset)
|
||||
{
|
||||
offset = 0;
|
||||
|
||||
if (_spellCastTarget?.Deleted != false || !_spellCastTarget.Alive)
|
||||
{
|
||||
ResetSequence(caster);
|
||||
}
|
||||
|
||||
if (_spellCastTarget != target)
|
||||
{
|
||||
if (_spellCastTarget != null)
|
||||
{
|
||||
caster.SendLocalizedMessage(ResetMessage);
|
||||
}
|
||||
|
||||
ResetSequence(caster);
|
||||
_spellCastTarget = target;
|
||||
}
|
||||
|
||||
offset = GetDamageOffset(_spellCastCount, target.Player);
|
||||
_spellCastCount++;
|
||||
|
||||
if (offset == 0)
|
||||
{
|
||||
caster.SendLocalizedMessage(TunedMessage);
|
||||
}
|
||||
|
||||
if (_spellCastCount >= SequenceLength)
|
||||
{
|
||||
caster.SendLocalizedMessage(PeakMessage);
|
||||
ResetSequence(caster);
|
||||
}
|
||||
else
|
||||
{
|
||||
RefreshBuffInfo(caster, offset);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int GetDamageOffset(int castCount, bool playerTarget)
|
||||
{
|
||||
if (castCount < 6)
|
||||
{
|
||||
return -30 + castCount * 6;
|
||||
}
|
||||
|
||||
var offset = (castCount - 5) * 2;
|
||||
return playerTarget ? System.Math.Min(offset, 20) : System.Math.Min(offset, 30);
|
||||
}
|
||||
|
||||
private void ResetSequence(Mobile caster)
|
||||
{
|
||||
_spellCastTarget = null;
|
||||
_spellCastCount = 0;
|
||||
|
||||
if (caster is PlayerMobile player)
|
||||
{
|
||||
player.RemoveBuff(BuffIcon.SpellFocusingBuff);
|
||||
player.RemoveBuff(BuffIcon.SpellFocusingDebuff);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshBuffInfo(Mobile caster, int offset)
|
||||
{
|
||||
if (caster is not PlayerMobile player || caster != Parent || !Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
player.RemoveBuff(offset < 0 ? BuffIcon.SpellFocusingBuff : BuffIcon.SpellFocusingDebuff);
|
||||
player.AddBuff(
|
||||
new BuffInfo(
|
||||
offset < 0 ? BuffIcon.SpellFocusingDebuff : BuffIcon.SpellFocusingBuff,
|
||||
BuffTitleCliloc,
|
||||
BuffSecondaryCliloc,
|
||||
args: $"{_spellCastTarget?.Name ?? "None"}\t{offset}"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private sealed class ToggleSpellFocusingEntry : ContextMenuEntry
|
||||
{
|
||||
private readonly SpellFocusingSash _sash;
|
||||
|
||||
public ToggleSpellFocusingEntry(SpellFocusingSash sash) : base(sash.Enabled ? 3006151 : 3006150, 2)
|
||||
{
|
||||
_sash = sash;
|
||||
}
|
||||
|
||||
public override void OnClick(Mobile from, IEntity target)
|
||||
{
|
||||
if (target is SpellFocusingSash sash && sash == _sash && from == sash.Parent && from.Alive)
|
||||
{
|
||||
sash.Enabled = !sash.Enabled;
|
||||
from.SendMessage(sash.Enabled ? "Spell Focusing enabled." : "Spell Focusing disabled.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1359,6 +1359,11 @@ namespace Server
|
|||
|
||||
public static bool IsBrittle(Item item)
|
||||
{
|
||||
if (item is SpellFocusingSash)
|
||||
{
|
||||
return Core.SA;
|
||||
}
|
||||
|
||||
if (!Core.HS)
|
||||
{
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -958,6 +958,11 @@ namespace Server.Spells
|
|||
bcFrom?.AlterSpellDamageTo(target, ref damageGiven);
|
||||
bcTarget?.AlterSpellDamageFrom(from, ref damageGiven);
|
||||
|
||||
if (SpellFocusingSash.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 (SpellFocusingSash.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 (SpellFocusingSash.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,8 @@ 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;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ public class EagleStrikeSpell : MysticSpell, ITargetingSpell<Mobile>
|
|||
|
||||
public override SpellCircle Circle => SpellCircle.Third;
|
||||
|
||||
public override bool SpellFocusingEligible => true;
|
||||
|
||||
public void Target(Mobile m)
|
||||
{
|
||||
if (CheckHSequence(m))
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ public class NetherBoltSpell : MysticSpell, ITargetingSpell<Mobile>
|
|||
|
||||
public override SpellCircle Circle => SpellCircle.First;
|
||||
|
||||
public override bool SpellFocusingEligible => true;
|
||||
|
||||
public override bool DelayedDamage => true;
|
||||
|
||||
public override Type[] DelayedDamageSpellFamilyStacking => _delayedDamageSpellFamilyStacking;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Server.Engines.BuffIcons;
|
||||
using Server.Items;
|
||||
using Server.Misc;
|
||||
using Server.Mobiles;
|
||||
using Server.Targeting;
|
||||
|
|
@ -31,6 +32,8 @@ public class PainSpikeSpell : NecromancerSpell, ITargetingSpell<Mobile>
|
|||
|
||||
public override bool DelayedDamage => false;
|
||||
|
||||
public override bool SpellFocusingEligible => true;
|
||||
|
||||
public static bool UnderEffect(Mobile m) => _table.ContainsKey(m);
|
||||
|
||||
public void Target(Mobile m)
|
||||
|
|
@ -76,6 +79,12 @@ public class PainSpikeSpell : NecromancerSpell, ITargetingSpell<Mobile>
|
|||
|
||||
// TODO: Find a better way to do this
|
||||
StaminaSystem.DFA = DFAlgorithm.PainSpike;
|
||||
|
||||
if (SpellFocusingSash.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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ namespace Server.Spells.Spellweaving
|
|||
{
|
||||
}
|
||||
|
||||
public override bool SpellFocusingEligible => true;
|
||||
|
||||
public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(3.5);
|
||||
|
||||
public override double RequiredSkill => 80.0;
|
||||
|
|
|
|||
|
|
@ -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