feat: implement Focus weapon property

This commit is contained in:
Crome696 2026-07-10 18:41:24 +02:00
parent 57774b1f38
commit 8cba41fb2a
5 changed files with 877 additions and 2 deletions

View file

@ -28,6 +28,7 @@ public class ExtendedWeaponAttributesTests
Assert.Equal(0x00000008, (int)ExtendedWeaponAttribute.BloodDrinker);
Assert.Equal(0x00000010, (int)ExtendedWeaponAttribute.HitSwarm);
Assert.Equal(0x00000020, (int)ExtendedWeaponAttribute.SplinteringWeapon);
Assert.Equal(0x00000040, (int)ExtendedWeaponAttribute.Focus);
}
[Fact]
@ -45,6 +46,7 @@ public class ExtendedWeaponAttributesTests
Assert.Equal(0, weapon.ExtendedWeaponAttributes.BloodDrinker);
Assert.Equal(0, weapon.ExtendedWeaponAttributes.HitSwarm);
Assert.Equal(0, weapon.ExtendedWeaponAttributes.SplinteringWeapon);
Assert.Equal(0, weapon.ExtendedWeaponAttributes.Focus);
}
finally
{
@ -82,10 +84,11 @@ public class ExtendedWeaponAttributesTests
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.BloodDrinker));
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.HitSwarm));
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.SplinteringWeapon));
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.Focus));
}
[Fact]
public void ExtendedWeaponAttributes_PersistBaneAndBattleLustThroughBaseWeaponSerialization()
public void ExtendedWeaponAttributes_PersistThroughBaseWeaponSerialization()
{
var previousExpansion = Core.Expansion;
var weapon = new TestKatana();
@ -100,6 +103,7 @@ public class ExtendedWeaponAttributesTests
weapon.ExtendedWeaponAttributes.BloodDrinker = 1;
weapon.ExtendedWeaponAttributes.HitSwarm = 30;
weapon.ExtendedWeaponAttributes.SplinteringWeapon = 25;
weapon.ExtendedWeaponAttributes.Focus = 1;
var writer = new BufferWriter(true);
weapon.Serialize(writer);
@ -116,6 +120,7 @@ public class ExtendedWeaponAttributesTests
Assert.Equal(1, deserialized.ExtendedWeaponAttributes.BloodDrinker);
Assert.Equal(30, deserialized.ExtendedWeaponAttributes.HitSwarm);
Assert.Equal(25, deserialized.ExtendedWeaponAttributes.SplinteringWeapon);
Assert.Equal(1, deserialized.ExtendedWeaponAttributes.Focus);
}
finally
{

View file

@ -0,0 +1,605 @@
using System;
using System.Collections.Generic;
using Server;
using Server.Items;
using Server.Mobiles;
using Server.Text;
using Xunit;
namespace UOContent.Tests;
[Collection("Sequential UOContent Tests")]
public class FocusPropertyTests
{
private const int FocusCliloc = 1150018;
private static readonly Point3D TestLocation = new(6200, 500, 0);
public FocusPropertyTests()
{
FocusContext.Configure();
}
[Fact]
public void ExtendedWeaponAttributes_StoresAndDupesFocus()
{
var weapon = new TestKatana();
var dupe = new TestKatana();
try
{
weapon.ExtendedWeaponAttributes.Focus = 1;
weapon.Dupe(dupe);
Assert.Equal(1, weapon.ExtendedWeaponAttributes.Focus);
Assert.Equal(1, dupe.ExtendedWeaponAttributes.Focus);
}
finally
{
weapon.Delete();
dupe.Delete();
}
}
[Fact]
public void FocusContext_IsTransientAndIsNotRestoredByWeaponSerialization()
{
var previousExpansion = Core.Expansion;
var attacker = CreateMobile();
var defender = CreateMobile(location: new Point3D(6201, 500, 0));
var weapon = EquipFocusWeapon(attacker);
var deserialized = new TestKatana();
try
{
Core.Expansion = Expansion.HS;
weapon.OnHit(attacker, defender);
Assert.Equal(1, FocusContext.ActiveContextCount);
var writer = new BufferWriter(true);
weapon.Serialize(writer);
var buffer = new byte[writer.Position];
writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer);
deserialized.Deserialize(new BufferReader(buffer));
Assert.Equal(1, deserialized.ExtendedWeaponAttributes.Focus);
Assert.Equal(0, FocusContext.GetCurrentModifierForTests(deserialized));
}
finally
{
Core.Expansion = previousExpansion;
FocusContext.ClearAll();
weapon.Delete();
deserialized.Delete();
attacker.Delete();
defender.Delete();
}
}
[Fact]
public void ExtendedWeaponAttributes_GetProperties_GatesFocusTooltipToHighSeas()
{
var previousExpansion = Core.Expansion;
var weapon = new TestKatana();
try
{
weapon.ExtendedWeaponAttributes.Focus = 1;
Core.Expansion = Expansion.HS - 1;
var preHighSeas = new RecordingPropertyList();
weapon.ExtendedWeaponAttributes.GetProperties(preHighSeas);
Assert.DoesNotContain(preHighSeas.Numbers, number => number == FocusCliloc);
Core.Expansion = Expansion.HS;
var highSeas = new RecordingPropertyList();
weapon.ExtendedWeaponAttributes.GetProperties(highSeas);
Assert.Contains(highSeas.Numbers, number => number == FocusCliloc);
}
finally
{
Core.Expansion = previousExpansion;
weapon.Delete();
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void OnHit_UsesTheSameFocusSequenceForPvmAndPvp(bool playerTarget)
{
var previousExpansion = Core.Expansion;
var attacker = CreateMobile();
var defender = CreateMobile(
player: playerTarget,
location: new Point3D(6201, 500, 0),
hitsMax: 5000,
hits: 5000
);
var weapon = EquipFocusWeapon(attacker);
try
{
Core.Expansion = Expansion.HS;
var expectedDamage = new[] { 50, 60, 70, 80, 90, 100, 110, 120, 120 };
foreach (var expected in expectedDamage)
{
var before = defender.Hits;
weapon.OnHit(attacker, defender);
Assert.Equal(expected, before - defender.Hits);
}
Assert.Equal(FocusContext.MaximumDamageModifier, FocusContext.GetCurrentModifierForTests(weapon));
}
finally
{
Core.Expansion = previousExpansion;
FocusContext.ClearAll();
weapon.Delete();
attacker.Delete();
defender.Delete();
}
}
[Fact]
public void OnHit_ChangingTargetResetsToTheNegativeStartingModifier()
{
var previousExpansion = Core.Expansion;
var attacker = CreateMobile();
var firstTarget = CreateMobile(location: new Point3D(6201, 500, 0));
var secondTarget = CreateMobile(location: new Point3D(6202, 500, 0));
var weapon = EquipFocusWeapon(attacker);
try
{
Core.Expansion = Expansion.HS;
var firstBefore = firstTarget.Hits;
weapon.OnHit(attacker, firstTarget);
Assert.Equal(50, firstBefore - firstTarget.Hits);
firstBefore = firstTarget.Hits;
weapon.OnHit(attacker, firstTarget);
Assert.Equal(60, firstBefore - firstTarget.Hits);
var secondBefore = secondTarget.Hits;
weapon.OnHit(attacker, secondTarget);
Assert.Equal(50, secondBefore - secondTarget.Hits);
Assert.Equal(-40, FocusContext.GetCurrentModifierForTests(weapon));
}
finally
{
Core.Expansion = previousExpansion;
FocusContext.ClearAll();
weapon.Delete();
attacker.Delete();
firstTarget.Delete();
secondTarget.Delete();
}
}
[Fact]
public void MissesParriesAndZeroDamageHitsDoNotAdvanceFocus()
{
var previousExpansion = Core.Expansion;
var attacker = CreateMobile();
var defender = CreateMobile(location: new Point3D(6201, 500, 0));
var weapon = EquipFocusWeapon(attacker);
try
{
Core.Expansion = Expansion.HS;
var before = defender.Hits;
weapon.OnHit(attacker, defender);
Assert.Equal(50, before - defender.Hits);
weapon.OnMiss(attacker, defender);
before = defender.Hits;
weapon.OnHit(attacker, defender);
Assert.Equal(60, before - defender.Hits);
weapon.AbsorbAllDamage = true;
WeaponAbility.Table[attacker] = new TestWeaponAbility();
before = defender.Hits;
weapon.OnHit(attacker, defender);
Assert.Equal(before, defender.Hits);
WeaponAbility.Table.Remove(attacker);
before = defender.Hits;
weapon.OnHit(attacker, defender);
Assert.Equal(before, defender.Hits);
weapon.AbsorbAllDamage = false;
before = defender.Hits;
weapon.OnHit(attacker, defender);
Assert.Equal(70, before - defender.Hits);
}
finally
{
Core.Expansion = previousExpansion;
WeaponAbility.Table.Remove(attacker);
FocusContext.ClearAll();
weapon.Delete();
attacker.Delete();
defender.Delete();
}
}
[Fact]
public void OnHit_CombinesFocusOnceWithAnExistingWeaponDamageModifier()
{
var previousExpansion = Core.Expansion;
var attacker = CreateMobile();
var defender = CreateMobile(location: new Point3D(6201, 500, 0));
var weapon = EquipFocusWeapon(attacker);
try
{
Core.Expansion = Expansion.HS;
var before = defender.Hits;
weapon.OnHit(attacker, defender, 2.0);
// Existing +100% bonus and Focus -50% are combined once in the shared percentage stage.
Assert.Equal(150, before - defender.Hits);
}
finally
{
Core.Expansion = previousExpansion;
FocusContext.ClearAll();
weapon.Delete();
attacker.Delete();
defender.Delete();
}
}
[Fact]
public void OnHit_PreHighSeas_DoesNotApplyOrCreateFocusState()
{
var previousExpansion = Core.Expansion;
var attacker = CreateMobile();
var defender = CreateMobile(location: new Point3D(6201, 500, 0));
var weapon = EquipFocusWeapon(attacker);
try
{
Core.Expansion = Expansion.SA;
var before = defender.Hits;
weapon.OnHit(attacker, defender);
Assert.Equal(100, before - defender.Hits);
Assert.Equal(0, FocusContext.ActiveContextCount);
}
finally
{
Core.Expansion = previousExpansion;
FocusContext.ClearAll();
weapon.Delete();
attacker.Delete();
defender.Delete();
}
}
[Fact]
public void OnHit_PlayerTargetsUseTheSameSequenceAndResetOnTargetSwitch()
{
var previousExpansion = Core.Expansion;
var attacker = CreateMobile();
var firstTarget = CreatePlayerMobile(new Point3D(6201, 500, 0));
var secondTarget = CreatePlayerMobile(new Point3D(6202, 500, 0));
var weapon = EquipFocusWeapon(attacker);
try
{
Core.Expansion = Expansion.HS;
var before = firstTarget.Hits;
weapon.OnHit(attacker, firstTarget);
Assert.Equal(50, before - firstTarget.Hits);
before = firstTarget.Hits;
weapon.OnHit(attacker, firstTarget);
Assert.Equal(60, before - firstTarget.Hits);
before = secondTarget.Hits;
weapon.OnHit(attacker, secondTarget);
Assert.Equal(50, before - secondTarget.Hits);
}
finally
{
Core.Expansion = previousExpansion;
FocusContext.ClearAll();
weapon.Delete();
attacker.Delete();
firstTarget.Delete();
secondTarget.Delete();
}
}
[Fact]
public void FocusContext_ClearsWhenWeaponIsUnequippedDeletedOrPropertyRemoved()
{
var previousExpansion = Core.Expansion;
var attacker = CreateMobile();
var defender = CreateMobile(location: new Point3D(6201, 500, 0));
var weapon = EquipFocusWeapon(attacker);
try
{
Core.Expansion = Expansion.HS;
weapon.OnHit(attacker, defender);
Assert.Equal(1, FocusContext.ActiveContextCount);
attacker.RemoveItem(weapon);
Assert.Equal(0, FocusContext.ActiveContextCount);
attacker.AddItem(weapon);
weapon.OnHit(attacker, defender);
Assert.Equal(1, FocusContext.ActiveContextCount);
weapon.ExtendedWeaponAttributes.Focus = 0;
Assert.Equal(0, FocusContext.ActiveContextCount);
weapon.ExtendedWeaponAttributes.Focus = 1;
attacker.AddItem(weapon);
weapon.OnHit(attacker, defender);
Assert.Equal(1, FocusContext.ActiveContextCount);
weapon.Delete();
Assert.Equal(0, FocusContext.ActiveContextCount);
}
finally
{
Core.Expansion = previousExpansion;
FocusContext.ClearAll();
weapon.Delete();
attacker.Delete();
defender.Delete();
}
}
[Fact]
public void FocusContext_ClearsWhenTargetDiesAndWhenAttackerLogsOutOrDies()
{
var previousExpansion = Core.Expansion;
var attacker = CreatePlayerMobile();
var target = CreatePlayerMobile(new Point3D(6201, 500, 0));
var deletedTarget = CreatePlayerMobile(new Point3D(6202, 500, 0));
var secondTarget = CreateMobile(location: new Point3D(6203, 500, 0));
var weapon = EquipFocusWeapon(attacker);
try
{
Core.Expansion = Expansion.HS;
weapon.OnHit(attacker, target);
Assert.Equal(1, FocusContext.ActiveContextCount);
PlayerMobile.PlayerDeathEvent(target);
Assert.Equal(0, FocusContext.ActiveContextCount);
weapon.OnHit(attacker, deletedTarget);
Assert.Equal(1, FocusContext.ActiveContextCount);
deletedTarget.Delete();
Assert.Equal(0, FocusContext.ActiveContextCount);
weapon.OnHit(attacker, secondTarget);
Assert.Equal(1, FocusContext.ActiveContextCount);
EventSink.InvokeLogout(attacker);
Assert.Equal(0, FocusContext.ActiveContextCount);
weapon.OnHit(attacker, secondTarget);
Assert.Equal(1, FocusContext.ActiveContextCount);
PlayerMobile.PlayerDeathEvent(attacker);
Assert.Equal(0, FocusContext.ActiveContextCount);
}
finally
{
Core.Expansion = previousExpansion;
FocusContext.ClearAll();
weapon.Delete();
attacker.Delete();
target.Delete();
deletedTarget.Delete();
secondTarget.Delete();
}
}
[Fact]
public void FocusContext_ClearsOnRealCreatureDeathAndDeletion()
{
var previousExpansion = Core.Expansion;
var attacker = CreateCreature(new Point3D(6200, 500, 0));
var target = CreateCreature(new Point3D(6201, 500, 0));
var deletedTarget = CreateCreature(new Point3D(6202, 500, 0));
var replacementTarget = CreateCreature(new Point3D(6203, 500, 0));
var weapon = EquipFocusWeapon(attacker);
try
{
Core.Expansion = Expansion.HS;
weapon.OnHit(attacker, target);
Assert.Equal(1, FocusContext.ActiveContextCount);
target.Kill();
Assert.Equal(0, FocusContext.ActiveContextCount);
weapon.OnHit(attacker, deletedTarget);
Assert.Equal(1, FocusContext.ActiveContextCount);
deletedTarget.Delete();
Assert.Equal(0, FocusContext.ActiveContextCount);
weapon.OnHit(attacker, replacementTarget);
Assert.Equal(1, FocusContext.ActiveContextCount);
attacker.Kill();
Assert.Equal(0, FocusContext.ActiveContextCount);
}
finally
{
Core.Expansion = previousExpansion;
FocusContext.ClearAll();
weapon.Delete();
attacker.Delete();
target.Delete();
deletedTarget.Delete();
replacementTarget.Delete();
}
}
[Fact]
public void RunicAttributeGeneration_DoesNotRollFocus()
{
var previousExpansion = Core.Expansion;
var weapon = new TestKatana();
try
{
Core.Expansion = Expansion.HS;
BaseRunicTool.ApplyAttributesTo(weapon, false, 0, 25, 100, 100);
Assert.Equal(0, weapon.ExtendedWeaponAttributes.Focus);
}
finally
{
Core.Expansion = previousExpansion;
weapon.Delete();
}
}
private static TestKatana EquipFocusWeapon(Mobile wielder)
{
var weapon = new TestKatana
{
Layer = Layer.OneHanded
};
weapon.ExtendedWeaponAttributes.Focus = 1;
wielder.AddItem(weapon);
Assert.Same(weapon, wielder.Weapon);
return weapon;
}
private static Mobile CreateMobile(
bool player = false,
Point3D location = default,
int hitsMax = 500,
int hits = 500
)
{
var mobile = new Mobile(World.NewMobile)
{
Player = player
};
mobile.DefaultMobileInit();
InitializeHits(mobile, hitsMax, hits);
mobile.MoveToWorld(location == default ? TestLocation : location, Map.Felucca);
return mobile;
}
private static PlayerMobile CreatePlayerMobile(Point3D location = default)
{
var mobile = new PlayerMobile(World.NewMobile);
mobile.DefaultMobileInit();
mobile.RawStr = 200;
mobile.Player = true;
mobile.Hits = mobile.HitsMax;
mobile.MoveToWorld(location == default ? TestLocation : location, Map.Felucca);
return mobile;
}
private static TestCreature CreateCreature(Point3D location)
{
var creature = new TestCreature();
creature.RawStr = 200;
creature.Hits = creature.HitsMax;
creature.MoveToWorld(location, Map.Felucca);
return creature;
}
private static void InitializeHits(Mobile mobile, int hitsMax, int hits)
{
mobile.RawStr = Math.Max(1, (hitsMax - 50) * 2);
Assert.Equal(hitsMax, mobile.HitsMax);
mobile.Hits = hits;
}
private sealed class TestKatana : Katana
{
public bool AbsorbAllDamage { get; set; }
public override int ComputeDamage(Mobile attacker, Mobile defender) => 100;
public override int AbsorbDamage(Mobile attacker, Mobile defender, int damage) => AbsorbAllDamage ? 0 : damage;
public override void AddBlood(Mobile attacker, Mobile defender, int damage)
{
}
}
private sealed class TestWeaponAbility : WeaponAbility
{
}
private sealed class TestCreature : BaseCreature
{
public TestCreature() : base(AIType.AI_Melee, FightMode.Closest, 10, 1)
{
Body = 0xC8;
}
public override void GetSpeeds(out double activeSpeed, out double passiveSpeed)
{
activeSpeed = 0.2;
passiveSpeed = 0.4;
}
}
private sealed class RecordingPropertyList : IPropertyList
{
private string _interpolated = string.Empty;
public List<int> Numbers { get; } = [];
public void Reset()
{
}
public void Terminate()
{
}
public void Add(int number) => Numbers.Add(number);
public void Add(int number, string argument) => Numbers.Add(number);
public void Add(ReadOnlySpan<char> argument) => Numbers.Add(0);
public void Add(int number, ReadOnlySpan<char> argument) => Numbers.Add(number);
public void AddChunked(ReadOnlySpan<char> text) => Numbers.Add(0);
public OplTextBlock TextBlock() => new(this);
public void Add(int number, int value) => Numbers.Add(number);
public void AddLocalized(int value) => Numbers.Add(0);
public void AddLocalized(int number, int value) => Numbers.Add(number);
public void Add(ref IPropertyList.InterpolatedStringHandler handler) => Numbers.Add(0);
public void Add(int number, ref IPropertyList.InterpolatedStringHandler handler) => Numbers.Add(number);
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

@ -1188,6 +1188,7 @@ public abstract partial class BaseWeapon
public override void OnRemoved(IEntity parent)
{
ClearLastParryChance();
FocusContext.Clear(this);
EnchantSpell.StopEffect(this);
if (parent is not Mobile m)
@ -1238,6 +1239,7 @@ public abstract partial class BaseWeapon
public override void OnAfterDelete()
{
FocusContext.Clear(this);
EnchantSpell.StopEffect(this);
base.OnAfterDelete();
}
@ -1247,6 +1249,11 @@ public abstract partial class BaseWeapon
base.OnMapChange();
EnchantSpell.StopEffect(this);
if (Map == null || Map == Map.Internal)
{
FocusContext.Clear(this);
}
if ((Map == null || Map == Map.Internal) && Parent is Mobile m && ExtendedWeaponAttributes.BattleLust != 0)
{
BattleLust.Clear(m);
@ -1984,6 +1991,8 @@ public abstract partial class BaseWeapon
percentageBonus += talisman.Killer.DamageBonus(defender);
}
// Canonical Focus endpoints are -50% and +20%. Intermediate values use a documented 10-point step.
percentageBonus += FocusContext.GetDamageBonus(this, attacker, defender);
percentageBonus += BattleLust.GetDamageBonus(attacker, defender);
percentageBonus = Math.Min(percentageBonus, 300);
@ -2143,6 +2152,9 @@ public abstract partial class BaseWeapon
if (damageGiven > 0)
{
// A Focus cycle advances only after positive damage is applied; misses, parries, and
// zero-damage post-absorption hits leave the current modifier unchanged.
FocusContext.OnSuccessfulHit(this, attacker, defender);
var propertyBonus = move?.GetPropertyBonus(attacker) ?? 1.0;
var splintering = SplinteringWeapon.TryProcOnEligibleHit(
attacker,
@ -2586,6 +2598,7 @@ public abstract partial class BaseWeapon
public virtual void OnMiss(Mobile attacker, Mobile defender)
{
FocusContext.OnMiss(this, attacker, defender);
PlaySwingAnimation(attacker);
attacker.PlaySound(GetMissAttackSound(attacker, defender));
defender.PlaySound(GetMissDefendSound(attacker, defender));

View file

@ -0,0 +1,229 @@
using System;
using System.Collections.Generic;
using ModernUO.CodeGeneratedEvents;
using Server.Collections;
using Server.Mobiles;
namespace Server.Items;
public static class FocusContext
{
// UO.com defines the -50% starting point and +20% cap but not the intermediate table.
// A deterministic 10-point step preserves those endpoints without copying ServUO's older sequence.
internal const int InitialDamageModifier = -50;
internal const int MaximumDamageModifier = 20;
internal const int DamageModifierStep = 10;
private static readonly Dictionary<BaseWeapon, Context> _contexts = [];
private static readonly Dictionary<Mobile, HashSet<BaseWeapon>> _attackerIndex = [];
private static readonly Dictionary<Mobile, HashSet<BaseWeapon>> _targetIndex = [];
private static bool _configured;
static FocusContext() => Configure();
public static void Configure()
{
if (_configured)
{
return;
}
_configured = true;
EventSink.Logout += Clear;
}
internal static int ActiveContextCount => _contexts.Count;
internal static int GetCurrentModifierForTests(BaseWeapon weapon) =>
weapon != null && _contexts.TryGetValue(weapon, out var context) ? context.DamageModifier : 0;
internal static int GetDamageBonus(BaseWeapon weapon, Mobile attacker, Mobile defender)
{
if (!CanUse(weapon, attacker, defender))
{
Clear(weapon);
return 0;
}
if (!_contexts.TryGetValue(weapon, out var context) || context.Attacker != attacker)
{
Clear(weapon);
context = new Context(attacker, defender);
_contexts[weapon] = context;
AddToIndex(_attackerIndex, attacker, weapon);
AddToIndex(_targetIndex, defender, weapon);
}
else if (context.Target != defender)
{
SetTarget(weapon, context, defender);
context.DamageModifier = InitialDamageModifier;
}
return context.DamageModifier;
}
internal static void OnSuccessfulHit(BaseWeapon weapon, Mobile attacker, Mobile defender)
{
if (!CanUse(weapon, attacker, defender))
{
Clear(weapon);
return;
}
if (!_contexts.TryGetValue(weapon, out var context) || context.Attacker != attacker ||
context.Target != defender)
{
return;
}
context.DamageModifier = Math.Min(
MaximumDamageModifier,
context.DamageModifier + DamageModifierStep
);
}
internal static void OnMiss(BaseWeapon weapon, Mobile attacker, Mobile defender)
{
if (!CanUse(weapon, attacker, defender))
{
Clear(weapon);
return;
}
if (_contexts.TryGetValue(weapon, out var context) && context.Attacker == attacker &&
context.Target != defender)
{
SetTarget(weapon, context, defender);
context.DamageModifier = InitialDamageModifier;
}
}
internal static void Clear(BaseWeapon weapon)
{
if (weapon == null || !_contexts.Remove(weapon, out var context))
{
return;
}
RemoveFromIndex(_attackerIndex, context.Attacker, weapon);
RemoveFromIndex(_targetIndex, context.Target, weapon);
}
[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;
}
using var weapons = PooledRefList<BaseWeapon>.Create();
if (_attackerIndex.TryGetValue(mobile, out var attackers))
{
foreach (var weapon in attackers)
{
weapons.Add(weapon);
}
}
if (_targetIndex.TryGetValue(mobile, out var targets))
{
foreach (var weapon in targets)
{
weapons.Add(weapon);
}
}
for (var i = 0; i < weapons.Count; i++)
{
Clear(weapons[i]);
}
}
internal static void ClearAll()
{
using var weapons = PooledRefList<BaseWeapon>.Create();
foreach (var weapon in _contexts.Keys)
{
weapons.Add(weapon);
}
for (var i = 0; i < weapons.Count; i++)
{
Clear(weapons[i]);
}
}
private static bool CanUse(BaseWeapon weapon, Mobile attacker, Mobile defender) =>
Core.HS &&
weapon?.Deleted == false &&
weapon.ExtendedWeaponAttributes.Focus != 0 &&
attacker?.Deleted == false &&
attacker.Alive &&
attacker.Map != null &&
attacker.Map != Map.Internal &&
attacker.Weapon == weapon &&
weapon.Parent == attacker &&
defender?.Deleted == false &&
defender.Alive &&
defender.Map != null &&
defender.Map != Map.Internal;
private static void SetTarget(BaseWeapon weapon, Context context, Mobile target)
{
RemoveFromIndex(_targetIndex, context.Target, weapon);
context.Target = target;
AddToIndex(_targetIndex, target, weapon);
}
private static void AddToIndex(
Dictionary<Mobile, HashSet<BaseWeapon>> index,
Mobile mobile,
BaseWeapon weapon
)
{
if (!index.TryGetValue(mobile, out var weapons))
{
weapons = [];
index[mobile] = weapons;
}
weapons.Add(weapon);
}
private static void RemoveFromIndex(
Dictionary<Mobile, HashSet<BaseWeapon>> index,
Mobile mobile,
BaseWeapon weapon
)
{
if (mobile == null || !index.TryGetValue(mobile, out var weapons) || !weapons.Remove(weapon))
{
return;
}
if (weapons.Count == 0)
{
index.Remove(mobile);
}
}
private sealed class Context
{
public Context(Mobile attacker, Mobile target)
{
Attacker = attacker;
Target = target;
DamageModifier = InitialDamageModifier;
}
public Mobile Attacker { get; }
public Mobile Target { get; set; }
public int DamageModifier { get; set; }
}
}

View file

@ -1234,7 +1234,8 @@ namespace Server
HitSparks = 0x00000004,
BloodDrinker = 0x00000008,
HitSwarm = 0x00000010,
SplinteringWeapon = 0x00000020
SplinteringWeapon = 0x00000020,
Focus = 0x00000040
}
public sealed class ExtendedWeaponAttributes : BaseAttributes
@ -1305,6 +1306,23 @@ namespace Server
set => this[ExtendedWeaponAttribute.SplinteringWeapon] = value;
}
[CommandProperty(AccessLevel.GameMaster)]
public int Focus
{
get => this[ExtendedWeaponAttribute.Focus];
set
{
var hadFocus = Focus != 0;
this[ExtendedWeaponAttribute.Focus] = value;
if (hadFocus && value == 0 && Owner is BaseWeapon weapon)
{
FocusContext.Clear(weapon);
}
}
}
public void GetProperties(IPropertyList list)
{
if (Core.HS && Bane != 0)
@ -1312,6 +1330,11 @@ namespace Server
list.Add(1154671); // Bane
}
if (Core.HS && Focus != 0)
{
list.Add(1150018); // Focus
}
if (Core.SA && BattleLust != 0)
{
list.Add(1113710); // Battle Lust