Merge pull request #24 from RebirthUO/feat/issue-9-casting-focus

feat(items): add Casting Focus property
This commit is contained in:
Crome696 2026-07-09 14:24:40 +02:00 committed by GitHub
commit 81231bedb3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 772 additions and 3 deletions

View file

@ -0,0 +1,402 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Server;
using Server.Items;
using Server.Mobiles;
using Server.Spells;
using Server.Spells.Second;
using Server.Tests;
using Server.Text;
using Xunit;
namespace UOContent.Tests;
[Collection("Sequential UOContent Tests")]
public class CastingFocusPropertyTests
{
private const int CastingFocusCliloc = 1113696;
[Fact]
public void AbsorptionAttributes_StoresDupesAndSerializesCastingFocusOnArmor()
{
var armor = new LeatherChest();
var dupe = new LeatherChest();
var deserialized = new LeatherChest();
try
{
armor.AbsorptionAttributes.CastingFocus = 3;
armor.Dupe(dupe);
Assert.Equal(3, dupe.AbsorptionAttributes.CastingFocus);
var writer = new BufferWriter(true);
armor.Serialize(writer);
var buffer = new byte[writer.Position];
writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer);
var reader = new BufferReader(buffer);
deserialized.Deserialize(reader);
Assert.Equal(buffer.Length, reader.Position);
Assert.Equal(3, deserialized.AbsorptionAttributes.CastingFocus);
}
finally
{
armor.Delete();
dupe.Delete();
deserialized.Delete();
}
}
[Fact]
public void AbsorptionAttributes_AreStaffEditableThroughCommandProperties()
{
var containerProperty = typeof(BaseArmor).GetProperty(nameof(BaseArmor.AbsorptionAttributes));
Assert.NotNull(containerProperty);
var containerCommandProperty = containerProperty.GetCustomAttribute<CommandPropertyAttribute>();
Assert.NotNull(containerCommandProperty);
Assert.True(containerCommandProperty.CanModify);
var property = typeof(AbsorptionAttributes).GetProperty(nameof(AbsorptionAttributes.CastingFocus));
Assert.NotNull(property);
var attribute = property.GetCustomAttribute<CommandPropertyAttribute>();
Assert.NotNull(attribute);
Assert.Equal(AccessLevel.GameMaster, attribute.ReadLevel);
Assert.Equal(AccessLevel.GameMaster, attribute.WriteLevel);
}
[Fact]
public void AbsorptionAttributes_GetValue_SumsEquippedArmorAndRespectsStygianAbyssGate()
{
var previousExpansion = Core.Expansion;
var caster = CreateMobile(player: true);
var chest = new LeatherChest();
var legs = new LeatherLegs();
try
{
chest.AbsorptionAttributes.CastingFocus = 5;
legs.AbsorptionAttributes.CastingFocus = 10;
caster.AddItem(chest);
caster.AddItem(legs);
Core.Expansion = Expansion.ML;
Assert.Equal(0, AbsorptionAttributes.GetValue(caster, AbsorptionAttribute.CastingFocus));
Core.Expansion = Expansion.SA;
Assert.Equal(15, AbsorptionAttributes.GetValue(caster, AbsorptionAttribute.CastingFocus));
Assert.Equal(
AOS.CastingFocusChanceCap,
Math.Min(
AOS.CastingFocusChanceCap,
AbsorptionAttributes.GetValue(caster, AbsorptionAttribute.CastingFocus)
)
);
}
finally
{
Core.Expansion = previousExpansion;
chest.Delete();
legs.Delete();
caster.Delete();
}
}
[Fact]
public void BaseArmor_GetProperties_GatesCastingFocusTooltipToStygianAbyss()
{
var previousExpansion = Core.Expansion;
var armor = new LeatherChest();
try
{
armor.AbsorptionAttributes.CastingFocus = 3;
Core.Expansion = Expansion.ML;
var preStygianAbyss = new RecordingPropertyList();
armor.GetProperties(preStygianAbyss);
Assert.DoesNotContain(preStygianAbyss.Entries, entry => entry.Number == CastingFocusCliloc);
Core.Expansion = Expansion.SA;
var stygianAbyss = new RecordingPropertyList();
armor.GetProperties(stygianAbyss);
Assert.Contains(stygianAbyss.Entries, entry =>
entry.Number == CastingFocusCliloc && entry.Argument == "3"
);
}
finally
{
Core.Expansion = previousExpansion;
armor.Delete();
}
}
[Fact]
public void OnCasterHurt_CastingFocusSuccessPreservesCastWithoutPreventingDamage()
{
var previousExpansion = Core.Expansion;
using var random = new PredictableRandom(0);
var caster = CreateMobile(player: true);
var armor = EquipCastingFocusArmor(caster, 1);
var spell = SetCastingSpell(caster);
try
{
Core.Expansion = Expansion.SA;
var hits = caster.Hits;
caster.Damage(10);
Assert.Equal(hits - 10, caster.Hits);
Assert.Equal(SpellState.Casting, spell.State);
Assert.Same(spell, caster.Spell);
Assert.Equal(0, spell.DisturbCount);
}
finally
{
Core.Expansion = previousExpansion;
armor.Delete();
caster.Delete();
}
}
[Fact]
public void OnCasterHurt_CastingFocusFailedRollDisturbsNormally()
{
var previousExpansion = Core.Expansion;
using var random = new PredictableRandom(99);
var caster = CreateMobile(player: true);
var armor = EquipCastingFocusArmor(caster, 12);
var spell = SetCastingSpell(caster);
try
{
Core.Expansion = Expansion.SA;
caster.Damage(10);
Assert.Equal(SpellState.None, spell.State);
Assert.Null(caster.Spell);
Assert.Equal(1, spell.DisturbCount);
Assert.Equal(DisturbType.Hurt, spell.LastDisturbType);
}
finally
{
Core.Expansion = previousExpansion;
armor.Delete();
caster.Delete();
}
}
[Fact]
public void OnCasterHurt_PreStygianAbyssCastingFocusDoesNotPreserveCast()
{
var previousExpansion = Core.Expansion;
using var random = new PredictableRandom(0);
var caster = CreateMobile(player: true);
var armor = EquipCastingFocusArmor(caster, 12);
var spell = SetCastingSpell(caster);
try
{
Core.Expansion = Expansion.ML;
caster.Damage(10);
Assert.Equal(SpellState.None, spell.State);
Assert.Null(caster.Spell);
Assert.Equal(1, spell.DisturbCount);
}
finally
{
Core.Expansion = previousExpansion;
armor.Delete();
caster.Delete();
}
}
[Fact]
public void OnCasterHurt_ProtectionSuccessStillPreservesBeforeCastingFocus()
{
var previousExpansion = Core.Expansion;
using var random = new PredictableRandom(99);
var caster = CreateMobile(player: true);
var armor = EquipCastingFocusArmor(caster, 0);
var spell = SetCastingSpell(caster);
try
{
Core.Expansion = Expansion.SA;
ProtectionSpell.Registry[caster] = 1000;
caster.Damage(10);
Assert.Equal(SpellState.Casting, spell.State);
Assert.Same(spell, caster.Spell);
Assert.Equal(0, spell.DisturbCount);
}
finally
{
ProtectionSpell.Registry.Remove(caster);
Core.Expansion = previousExpansion;
armor.Delete();
caster.Delete();
}
}
[Fact]
public void OnCasterHurt_CastingFocusCanPreserveAfterProtectionFails()
{
var previousExpansion = Core.Expansion;
using var random = new PredictableRandom(1);
var caster = CreateMobile(player: true);
var armor = EquipCastingFocusArmor(caster, 12);
var spell = SetCastingSpell(caster);
try
{
Core.Expansion = Expansion.SA;
ProtectionSpell.Registry[caster] = 0;
caster.Damage(10);
Assert.Equal(SpellState.Casting, spell.State);
Assert.Same(spell, caster.Spell);
Assert.Equal(0, spell.DisturbCount);
}
finally
{
ProtectionSpell.Registry.Remove(caster);
Core.Expansion = previousExpansion;
armor.Delete();
caster.Delete();
}
}
[Fact]
public void OnCasterHurt_NonPlayerCasterRemainsUnchanged()
{
var previousExpansion = Core.Expansion;
using var random = new PredictableRandom(99);
var caster = CreateMobile(player: false);
var armor = EquipCastingFocusArmor(caster, 12);
var spell = SetCastingSpell(caster);
try
{
Core.Expansion = Expansion.SA;
caster.Damage(10);
Assert.Equal(SpellState.Casting, spell.State);
Assert.Same(spell, caster.Spell);
Assert.Equal(0, spell.DisturbCount);
}
finally
{
Core.Expansion = previousExpansion;
armor.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 static LeatherChest EquipCastingFocusArmor(Mobile mobile, int value)
{
var armor = new LeatherChest();
armor.AbsorptionAttributes.CastingFocus = value;
mobile.AddItem(armor);
return armor;
}
private static TestSpell SetCastingSpell(Mobile caster)
{
var spell = new TestSpell(caster)
{
State = SpellState.Casting
};
caster.Spell = spell;
return spell;
}
private sealed class TestSpell : Spell
{
private static readonly SpellInfo TestInfo = new("Test Spell", "test");
public TestSpell(Mobile caster) : base(caster, null, TestInfo)
{
}
public int DisturbCount { get; private set; }
public DisturbType LastDisturbType { get; private set; }
public override TimeSpan CastDelayBase => TimeSpan.Zero;
public override void OnCast()
{
}
public override int GetMana() => 0;
public override void OnDisturb(DisturbType type, bool message)
{
DisturbCount++;
LastDisturbType = type;
}
}
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

@ -31,6 +31,7 @@ public partial class BaseArmor
_skillBonuses = content.SkillBonuses ?? SkillBonusesDefaultValue();
_playerConstructed = content.PlayerConstructed;
_negativeAttributes = NegativeAttributesDefaultValue();
_absorptionAttributes = AbsorptionAttributesDefaultValue();
}
private void MigrateFrom(V9Content content)
@ -61,6 +62,38 @@ public partial class BaseArmor
_skillBonuses = content.SkillBonuses ?? SkillBonusesDefaultValue();
_playerConstructed = content.PlayerConstructed;
_negativeAttributes = NegativeAttributesDefaultValue();
_absorptionAttributes = AbsorptionAttributesDefaultValue();
}
private void MigrateFrom(V10Content content)
{
_attributes = content.Attributes ?? AttributesDefaultValue();
_armorAttributes = content.ArmorAttributes ?? ArmorAttributesDefaultValue();
_physicalBonus = content.PhysicalBonus ?? 0;
_fireBonus = content.FireBonus ?? 0;
_coldBonus = content.ColdBonus ?? 0;
_poisonBonus = content.PoisonBonus ?? 0;
_energyBonus = content.EnergyBonus ?? 0;
_identified = content.Identified;
_maxHitPoints = content.MaxHitPoints ?? 0;
_hitPoints = content.HitPoints ?? 0;
_crafter = content.Crafter;
_quality = content.Quality ?? ArmorQuality.Regular;
_durability = content.Durability ?? ArmorDurabilityLevel.Regular;
_protectionLevel = content.ProtectionLevel ?? ArmorProtectionLevel.Regular;
_resource = content.Resource ?? DefaultResource;
_armorBase = content.BaseArmorRating ?? -1;
_strBonus = content.StrBonus ?? -1;
_dexBonus = content.DexBonus ?? -1;
_intBonus = content.IntBonus ?? -1;
_strReq = content.StrRequirement ?? -1;
_dexReq = content.DexRequirement ?? -1;
_intReq = content.IntRequirement ?? -1;
_meditate = content.MeditationAllowance ?? (AMA)(-1);
_skillBonuses = content.SkillBonuses ?? SkillBonusesDefaultValue();
_playerConstructed = content.PlayerConstructed;
_negativeAttributes = content.NegativeAttributes ?? NegativeAttributesDefaultValue();
_absorptionAttributes = AbsorptionAttributesDefaultValue();
}
// Version 7 (pre-codegen)
@ -192,5 +225,7 @@ public partial class BaseArmor
}
PlayerConstructed = GetSaveFlag(flags, OldSaveFlag.PlayerConstructed);
NegativeAttributes = new NegativeAttributes(this);
AbsorptionAttributes = new AbsorptionAttributes(this);
}
}

View file

@ -12,7 +12,7 @@ using AMT = Server.Items.ArmorMaterialType;
namespace Server.Items
{
[SerializationGenerator(10, false)]
[SerializationGenerator(11, false)]
public abstract partial class BaseArmor
: Item, IScissorable, IFactionItem, ICraftable, IWearableDurability, IAosItem, IIdentifiable
{
@ -162,6 +162,17 @@ namespace Server.Items
[SerializableFieldDefault(25)]
private NegativeAttributes NegativeAttributesDefaultValue() => new(this);
[SerializedIgnoreDupe]
[SerializableField(26, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AbsorptionAttributes _absorptionAttributes;
[SerializableFieldSaveFlag(26)]
private bool ShouldSerializeAbsorptionAttributes() => !_absorptionAttributes.IsEmpty;
[SerializableFieldDefault(26)]
private AbsorptionAttributes AbsorptionAttributesDefaultValue() => new(this);
private FactionItem m_FactionState;
public BaseArmor(int itemID) : base(itemID)
@ -179,6 +190,7 @@ namespace Server.Items
Attributes = new AosAttributes(this);
ArmorAttributes = new AosArmorAttributes(this);
NegativeAttributes = new NegativeAttributes(this);
AbsorptionAttributes = new AbsorptionAttributes(this);
SkillBonuses = new AosSkillBonuses(this);
}
@ -827,6 +839,7 @@ namespace Server.Items
armor.Attributes = new AosAttributes(newItem, Attributes);
armor.ArmorAttributes = new AosArmorAttributes(newItem, ArmorAttributes);
armor.NegativeAttributes = new NegativeAttributes(newItem, NegativeAttributes);
armor.AbsorptionAttributes = new AbsorptionAttributes(newItem, AbsorptionAttributes);
armor.SkillBonuses = new AosSkillBonuses(newItem, SkillBonuses);
// Set hue again because of resource
@ -1050,6 +1063,7 @@ namespace Server.Items
private void AfterDeserialization()
{
_negativeAttributes ??= NegativeAttributesDefaultValue();
_absorptionAttributes ??= AbsorptionAttributesDefaultValue();
var m = Parent as Mobile;
@ -1344,6 +1358,7 @@ namespace Server.Items
}
NegativeAttributes.GetProperties(list);
AbsorptionAttributes.GetProperties(list);
ArmorAttributes.GetProperties(list);
Attributes.GetProperties(list, luckBonus: GetLuckBonus());

View file

@ -0,0 +1,234 @@
{
"version": 11,
"type": "Server.Items.BaseArmor",
"properties": [
{
"name": "Attributes",
"type": "Server.AosAttributes",
"usesSaveFlag": true,
"rule": "RawSerializableMigrationRule",
"ruleArguments": [
"DeserializationRequiresParent"
]
},
{
"name": "ArmorAttributes",
"type": "Server.AosArmorAttributes",
"usesSaveFlag": true,
"rule": "RawSerializableMigrationRule",
"ruleArguments": [
"DeserializationRequiresParent"
]
},
{
"name": "PhysicalBonus",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "FireBonus",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "ColdBonus",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "PoisonBonus",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "EnergyBonus",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "Identified",
"type": "bool",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "MaxHitPoints",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "HitPoints",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "Crafter",
"type": "string",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Quality",
"type": "Server.Items.ArmorQuality",
"usesSaveFlag": true,
"rule": "EnumMigrationRule"
},
{
"name": "Durability",
"type": "Server.Items.ArmorDurabilityLevel",
"usesSaveFlag": true,
"rule": "EnumMigrationRule"
},
{
"name": "ProtectionLevel",
"type": "Server.Items.ArmorProtectionLevel",
"usesSaveFlag": true,
"rule": "EnumMigrationRule"
},
{
"name": "Resource",
"type": "Server.Items.CraftResource",
"usesSaveFlag": true,
"rule": "EnumMigrationRule"
},
{
"name": "BaseArmorRating",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "StrBonus",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "DexBonus",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "IntBonus",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "StrRequirement",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "DexRequirement",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "IntRequirement",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "MeditationAllowance",
"type": "Server.Items.ArmorMeditationAllowance",
"usesSaveFlag": true,
"rule": "EnumMigrationRule"
},
{
"name": "SkillBonuses",
"type": "Server.AosSkillBonuses",
"usesSaveFlag": true,
"rule": "RawSerializableMigrationRule",
"ruleArguments": [
"DeserializationRequiresParent"
]
},
{
"name": "PlayerConstructed",
"type": "bool",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "NegativeAttributes",
"type": "Server.NegativeAttributes",
"usesSaveFlag": true,
"rule": "RawSerializableMigrationRule",
"ruleArguments": [
"DeserializationRequiresParent"
]
},
{
"name": "AbsorptionAttributes",
"type": "Server.AbsorptionAttributes",
"usesSaveFlag": true,
"rule": "RawSerializableMigrationRule",
"ruleArguments": [
"DeserializationRequiresParent"
]
}
]
}

View file

@ -12,6 +12,7 @@ namespace Server
{
public static class AOS
{
public const int CastingFocusChanceCap = 12;
public const int MassiveStrengthRequirement = 125;
public static void DisableStatInfluences()
@ -1201,6 +1202,75 @@ namespace Server
public override string ToString() => "...";
}
[Flags]
public enum AbsorptionAttribute
{
CastingFocus = 0x00000001
}
public sealed class AbsorptionAttributes : BaseAttributes
{
public AbsorptionAttributes(Item owner) : base(owner)
{
}
public AbsorptionAttributes(Item owner, AbsorptionAttributes other) : base(owner, other)
{
}
public int this[AbsorptionAttribute attribute]
{
get => GetValue((int)attribute);
set => SetValue((int)attribute, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public int CastingFocus
{
get => Owner is BaseArmor ? this[AbsorptionAttribute.CastingFocus] : 0;
set => this[AbsorptionAttribute.CastingFocus] = Owner is BaseArmor ? value : 0;
}
public static int GetValue(Mobile m, AbsorptionAttribute attribute)
{
if (!Core.SA)
{
return 0;
}
var items = m.Items;
var value = 0;
for (var i = 0; i < items.Count; ++i)
{
var obj = items[i];
if (obj is BaseArmor armor)
{
var attrs = armor.AbsorptionAttributes;
if (attrs != null)
{
value += attrs[attribute];
}
}
}
return value;
}
public void GetProperties(IPropertyList list)
{
var castingFocus = CastingFocus;
if (Core.SA && castingFocus != 0)
{
list.Add(1113696, castingFocus); // Casting Focus ~1_val~%
}
}
public override string ToString() => "...";
}
[Flags]
public enum NegativeAttribute
{

View file

@ -88,10 +88,23 @@ namespace Server.Spells
if (Caster.Player && IsCasting)
{
var hasProtection = ProtectionSpell.Registry.TryGetValue(Caster, out var d);
if (!hasProtection || d < 1000 && d < Utility.Random(1000))
if (hasProtection && (d >= 1000 || d >= Utility.Random(1000)))
{
Disturb(DisturbType.Hurt, false, true);
return;
}
var castingFocus = Math.Min(
AOS.CastingFocusChanceCap,
AbsorptionAttributes.GetValue(Caster, AbsorptionAttribute.CastingFocus)
);
if (castingFocus > Utility.Random(100))
{
Caster.SendLocalizedMessage(1113690); // You regain your focus and continue casting the spell.
return;
}
Disturb(DisturbType.Hurt, false, true);
}
}