feat: implement Focus weapon property
This commit is contained in:
parent
57774b1f38
commit
8cba41fb2a
5 changed files with 877 additions and 2 deletions
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
229
Projects/UOContent/Items/Weapons/Focus.cs
Normal file
229
Projects/UOContent/Items/Weapons/Focus.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue