refactor: keep Spell Focusing reusable without sash

This commit is contained in:
Crome696 2026-07-10 17:45:56 +02:00
parent dfee8f09c5
commit 7f40274c5c
5 changed files with 206 additions and 713 deletions

View file

@ -112,6 +112,191 @@ public class SpellFocusingPropertyTests
}
}
[Fact]
public void SpellFocusing_UsesThePvMSequenceAndResetsAfterThePeak()
{
var previousExpansion = Core.Expansion;
var caster = CreateMobile(player: true);
var target = CreateMobile(player: false);
var item = new Katana();
var spell = new TestSpell(caster);
try
{
Core.Expansion = Expansion.AOS;
caster.AddItem(item);
item.Attributes.SpellFocusing = 1;
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(SpellFocusing.TryGetDamageOffset(spell, caster, target, out var offset));
Assert.Equal(expectedOffset, offset);
}
Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, target, out var resetOffset));
Assert.Equal(-30, resetOffset);
}
finally
{
Core.Expansion = previousExpansion;
item.Delete();
target.Delete();
caster.Delete();
}
}
[Fact]
public void SpellFocusing_HoldsThePvPCapAndResetsOnTargetChange()
{
var previousExpansion = Core.Expansion;
var caster = CreateMobile(player: true);
var target = CreateMobile(player: true);
var secondTarget = CreateMobile(player: true);
var item = new Katana();
var spell = new TestSpell(caster);
try
{
Core.Expansion = Expansion.AOS;
caster.AddItem(item);
item.Attributes.SpellFocusing = 1;
for (var i = 0; i < 15; i++)
{
Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, target, out _));
}
for (var i = 0; i < 6; i++)
{
Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, target, out var offset));
Assert.Equal(20, offset);
}
Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, target, out var resetOffset));
Assert.Equal(-30, resetOffset);
Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, secondTarget, out var targetResetOffset));
Assert.Equal(-30, targetResetOffset);
}
finally
{
Core.Expansion = previousExpansion;
item.Delete();
secondTarget.Delete();
target.Delete();
caster.Delete();
}
}
[Fact]
public void SpellFocusing_AppliesTheModifierAtTheSpellDamageHook()
{
var previousExpansion = Core.Expansion;
var caster = CreateMobile(player: true);
var target = CreateMobile(player: false);
var item = new Katana();
var spell = new TestSpell(caster);
try
{
Core.Expansion = Expansion.AOS;
caster.AddItem(item);
item.Attributes.SpellFocusing = 1;
var startingHits = target.Hits;
SpellHelper.Damage(spell, target, 100, 100, 0, 0, 0, 0);
Assert.Equal(startingHits - 70, target.Hits);
}
finally
{
Core.Expansion = previousExpansion;
item.Delete();
target.Delete();
caster.Delete();
}
}
[Fact]
public void SpellFocusing_ClearsTheSequenceWhenTheCasterIsCleared()
{
var previousExpansion = Core.Expansion;
var caster = CreateMobile(player: true);
var target = CreateMobile(player: false);
var item = new Katana();
var spell = new TestSpell(caster);
try
{
Core.Expansion = Expansion.AOS;
caster.AddItem(item);
item.Attributes.SpellFocusing = 1;
Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, target, out var firstOffset));
Assert.Equal(-30, firstOffset);
Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, target, out var secondOffset));
Assert.Equal(-24, secondOffset);
SpellFocusing.Clear(caster);
Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, target, out var resetOffset));
Assert.Equal(-30, resetOffset);
}
finally
{
Core.Expansion = previousExpansion;
item.Delete();
target.Delete();
caster.Delete();
}
}
[Fact]
public void SpellFocusing_RequiresAnEligibleSpellAndTheAoSExpansion()
{
var previousExpansion = Core.Expansion;
var caster = CreateMobile(player: true);
var target = CreateMobile(player: false);
var item = new Katana();
var qualifyingSpell = new TestSpell(caster);
var nonQualifyingSpell = new NonQualifyingSpell(caster);
try
{
caster.AddItem(item);
item.Attributes.SpellFocusing = 1;
Core.Expansion = Expansion.UOR;
Assert.False(SpellFocusing.TryGetDamageOffset(qualifyingSpell, caster, target, out _));
Core.Expansion = Expansion.AOS;
Assert.False(SpellFocusing.TryGetDamageOffset(nonQualifyingSpell, caster, target, out _));
}
finally
{
Core.Expansion = previousExpansion;
item.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");
@ -130,6 +315,23 @@ public class SpellFocusingPropertyTests
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 class RecordingPropertyList : IPropertyList
{
public List<int> Numbers { get; } = [];

View file

@ -1,388 +0,0 @@
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 caster = CreateMobile(player: true);
var sash = new SpellFocusingSash();
try
{
Core.Expansion = Expansion.SA;
caster.AddItem(sash);
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);
Assert.Equal(1, AosAttributes.GetValue(caster, AosAttribute.BonusMana));
Assert.Equal(5, AosAttributes.GetValue(caster, AosAttribute.DefendChance));
Assert.True(NegativeAttributes.IsBrittle(sash));
var spellFocusingIndex = list.Entries.FindIndex(entry => entry.Number == SpellFocusingCliloc);
var brittleIndex = list.Entries.FindIndex(entry => entry.Number == BrittleCliloc);
var defendChanceIndex = list.Entries.FindIndex(entry => entry.Number == DefendChanceCliloc);
var manaIncreaseIndex = list.Entries.FindIndex(entry => entry.Number == ManaIncreaseCliloc);
Assert.True(spellFocusingIndex > 0);
Assert.Equal(1072788, list.Entries[spellFocusingIndex - 1].Number);
Assert.True(spellFocusingIndex < brittleIndex);
Assert.True(brittleIndex < manaIncreaseIndex);
Assert.True(manaIncreaseIndex < defendChanceIndex);
Assert.Contains(list.Entries, entry => entry.Number == StrengthRequirementCliloc && entry.Argument == "10");
Assert.Contains(list.Entries, entry => entry.Number == DurabilityCliloc && entry.Argument == "255\t255");
Core.Expansion = Expansion.ML;
var preStygianAbyss = new RecordingPropertyList();
sash.GetProperties(preStygianAbyss);
Assert.DoesNotContain(preStygianAbyss.Entries, entry => entry.Number == SpellFocusingCliloc);
Assert.DoesNotContain(preStygianAbyss.Entries, entry => entry.Number == ManaIncreaseCliloc);
Assert.DoesNotContain(preStygianAbyss.Entries, entry => entry.Number == DefendChanceCliloc);
Assert.Equal(0, AosAttributes.GetValue(caster, AosAttribute.BonusMana));
Assert.Equal(0, AosAttributes.GetValue(caster, AosAttribute.DefendChance));
Assert.False(NegativeAttributes.IsBrittle(sash));
}
finally
{
Core.Expansion = previousExpansion;
sash.Delete();
caster.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);
var writer = new BufferWriter(true);
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(SpellFocusing.TryGetDamageOffset(spell, caster, target, out var actual));
Assert.Equal(expectedOffset, actual);
}
Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, target, out var resetOffset));
Assert.Equal(-30, resetOffset);
}
finally
{
Core.Expansion = previousExpansion;
sash.Delete();
target.Delete();
caster.Delete();
}
}
[Fact]
public void SpellHelperDamage_AppliesTheSashModifierBeforeResists()
{
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 startingHits = target.Hits;
SpellHelper.Damage(spell, target, 100, 100, 0, 0, 0, 0);
Assert.Equal(startingHits - 70, target.Hits);
}
finally
{
Core.Expansion = previousExpansion;
sash.Delete();
target.Delete();
caster.Delete();
}
}
[Fact]
public void SpellFocusingSash_HoldsPvPCapForFiveAdditionalSpellsAndResetsOnTargetChange()
{
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(SpellFocusing.TryGetDamageOffset(spell, caster, target, out _));
}
for (var i = 0; i < 6; i++)
{
Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, target, out var offset));
Assert.Equal(20, offset);
}
Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, target, out var resetOffset));
Assert.Equal(-30, resetOffset);
Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, secondTarget, out var targetResetOffset));
Assert.Equal(-30, targetResetOffset);
sash.Enabled = false;
Assert.False(SpellFocusing.TryGetDamageOffset(spell, caster, target, out _));
}
finally
{
Core.Expansion = previousExpansion;
sash.Delete();
secondTarget.Delete();
target.Delete();
caster.Delete();
}
}
[Fact]
public void SpellFocusingSash_ResetsWhenTheCasterIsCleared()
{
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);
Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, target, out var firstOffset));
Assert.Equal(-30, firstOffset);
Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, target, out var secondOffset));
Assert.Equal(-24, secondOffset);
SpellFocusingSash.Clear(target);
Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, target, out var targetResetOffset));
Assert.Equal(-30, targetResetOffset);
SpellFocusingSash.Clear(caster);
Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, target, out var casterResetOffset));
Assert.Equal(-30, casterResetOffset);
target.Delete();
Assert.False(SpellFocusing.TryGetDamageOffset(spell, caster, target, out _));
var replacement = CreateMobile(player: false);
try
{
Assert.True(SpellFocusing.TryGetDamageOffset(spell, caster, replacement, out var invalidTargetReset));
Assert.Equal(-30, invalidTargetReset);
}
finally
{
replacement.Delete();
}
}
finally
{
Core.Expansion = previousExpansion;
sash.Delete();
target.Delete();
caster.Delete();
}
}
[Fact]
public void SpellFocusingSash_ExcludesNonQualifyingSpellsAndPreStygianAbyss()
{
var previousExpansion = Core.Expansion;
var caster = CreateMobile(player: true);
var target = CreateMobile(player: false);
var sash = new SpellFocusingSash();
try
{
caster.AddItem(sash);
var spell = new NonQualifyingSpell(caster);
var qualifyingSpell = new TestSpell(caster);
Core.Expansion = Expansion.ML;
Assert.False(SpellFocusing.TryGetDamageOffset(qualifyingSpell, caster, target, out _));
Assert.Equal(0, AosAttributes.GetValue(caster, AosAttribute.BonusMana));
Assert.Equal(0, AosAttributes.GetValue(caster, AosAttribute.DefendChance));
Assert.False(SpellFocusing.TryGetDamageOffset(spell, caster, target, out _));
Core.Expansion = Expansion.SA;
Assert.False(SpellFocusing.TryGetDamageOffset(spell, 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;
}
}

View file

@ -1,289 +0,0 @@
using System.Collections.Generic;
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 static readonly HashSet<SpellFocusingSash> ActiveSashes = [];
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);
if (Attributes.BonusMana != 0)
{
list.Add(1060439, Attributes.BonusMana);
}
if (Attributes.DefendChance != 0)
{
list.Add(1060408, Attributes.DefendChance);
}
}
}
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 == null)
{
return;
}
if (mobile.FindItemOnLayer(Layer.MiddleTorso) is SpellFocusingSash sash)
{
sash.ResetSequence(mobile);
}
if (ActiveSashes.Count == 0)
{
return;
}
using var sashes = PooledRefList<SpellFocusingSash>.Create();
foreach (var activeSash in ActiveSashes)
{
if (activeSash._spellCastTarget == mobile)
{
sashes.Add(activeSash);
}
}
for (var i = 0; i < sashes.Count; i++)
{
var activeSash = sashes[i];
activeSash.ResetSequence(activeSash.Parent as 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;
ActiveSashes.Add(this);
}
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)
{
ActiveSashes.Remove(this);
_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.");
}
}
}
}

View file

@ -615,8 +615,7 @@ namespace Server
list.Add(1060401, prop); // damage increase ~1_val~%
}
// The SA event sash emits its fixed Mana/Defense lines in artifact order from AddNameProperties.
if (Owner is not SpellFocusingSash && (prop = DefendChance) != 0)
if ((prop = DefendChance) != 0)
{
list.Add(1060408, prop); // defense chance increase ~1_val~%
}
@ -671,7 +670,7 @@ namespace Server
list.Add(1060436, prop); // luck ~1_val~
}
if (Owner is not SpellFocusingSash && (prop = BonusMana) != 0)
if ((prop = BonusMana) != 0)
{
list.Add(1060439, prop); // mana increase ~1_val~
}
@ -792,7 +791,7 @@ namespace Server
{
var attrs = clothing.Attributes;
if (attrs != null && (clothing is not SpellFocusingSash || Core.SA))
if (attrs != null)
{
value += attrs[attribute];
}
@ -1604,11 +1603,6 @@ namespace Server
public static bool IsBrittle(Item item)
{
if (item is SpellFocusingSash)
{
return Core.SA;
}
if (!Core.HS)
{
return false;

View file

@ -37,41 +37,15 @@ public static class SpellFocusing
{
offset = 0;
if (!Core.AOS || spell?.SpellFocusingEligible != true)
if (!Core.AOS || spell?.SpellFocusingEligible != true || caster?.Alive != true || target?.Alive != true)
{
return false;
}
if (caster?.Alive != true)
{
if (caster != null)
{
SpellFocusingSash.Clear(caster);
}
return false;
}
if (target?.Alive != true)
{
SpellFocusingSash.Clear(caster);
return false;
}
var items = caster.Items;
for (var i = 0; i < items.Count; i++)
{
if (items[i] is SpellFocusingSash)
{
if (SpellFocusingSash.TryGetDamageOffset(spell, caster, target, out offset))
{
return true;
}
continue;
}
if (items[i] is IAosItem { Attributes.SpellFocusing: not 0 } item)
{
offset = item.Attributes.GetSpellFocusingOffset(caster, target);