feat(items): add Assassin Honed property
This commit is contained in:
parent
1bf9c263d5
commit
b5b1b566cd
4 changed files with 369 additions and 1 deletions
|
|
@ -0,0 +1,332 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Server;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
using Server.Tests;
|
||||
using Server.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class AssassinHonedPropertyTests
|
||||
{
|
||||
private const int AssassinHonedCliloc = 1152206;
|
||||
|
||||
[Fact]
|
||||
public void ExtendedWeaponAttributes_StoresAndDupesAssassinHoned()
|
||||
{
|
||||
var weapon = new TestKatana();
|
||||
var dupe = new TestKatana();
|
||||
|
||||
try
|
||||
{
|
||||
weapon.ExtendedWeaponAttributes.AssassinHoned = 1;
|
||||
weapon.Dupe(dupe);
|
||||
|
||||
Assert.Equal(1, weapon.ExtendedWeaponAttributes.AssassinHoned);
|
||||
Assert.Equal(1, dupe.ExtendedWeaponAttributes.AssassinHoned);
|
||||
}
|
||||
finally
|
||||
{
|
||||
weapon.Delete();
|
||||
dupe.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtendedWeaponAttributes_GetProperties_GatesAssassinHonedTooltipToHighSeas()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestKatana();
|
||||
|
||||
try
|
||||
{
|
||||
weapon.ExtendedWeaponAttributes.AssassinHoned = 1;
|
||||
|
||||
Core.Expansion = Expansion.ML;
|
||||
var preHighSeas = new RecordingPropertyList();
|
||||
weapon.ExtendedWeaponAttributes.GetProperties(preHighSeas);
|
||||
Assert.DoesNotContain(preHighSeas.Numbers, number => number == AssassinHonedCliloc);
|
||||
|
||||
Core.Expansion = Expansion.HS;
|
||||
var highSeas = new RecordingPropertyList();
|
||||
weapon.ExtendedWeaponAttributes.GetProperties(highSeas);
|
||||
Assert.Contains(highSeas.Numbers, number => number == AssassinHonedCliloc);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
weapon.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(2.0f, 73)]
|
||||
[InlineData(4.0f, 36)]
|
||||
public void GetAssassinHonedDamageBonus_UsesOriginalMlSpeed(float mlSpeed, int expectedBonus)
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestKatana(mlSpeed);
|
||||
var attacker = CreateMobile(Direction.East | Direction.Running);
|
||||
var defender = CreateMobile(Direction.East);
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.HS;
|
||||
weapon.ExtendedWeaponAttributes.AssassinHoned = 1;
|
||||
weapon.Attributes.WeaponSpeed = 60;
|
||||
|
||||
Assert.Equal(expectedBonus, weapon.GetAssassinHonedDamageBonus(attacker, defender));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAssassinHonedDamageBonus_RequiresHighSeasAndRearFacingRelation()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestKatana();
|
||||
var attacker = CreateMobile(Direction.East);
|
||||
var defender = CreateMobile(Direction.East);
|
||||
|
||||
try
|
||||
{
|
||||
weapon.ExtendedWeaponAttributes.AssassinHoned = 1;
|
||||
|
||||
Core.Expansion = Expansion.ML;
|
||||
Assert.Equal(0, weapon.GetAssassinHonedDamageBonus(attacker, defender));
|
||||
|
||||
Core.Expansion = Expansion.HS;
|
||||
defender.Direction = Direction.West;
|
||||
Assert.Equal(0, weapon.GetAssassinHonedDamageBonus(attacker, defender));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 73)]
|
||||
[InlineData(1, 0)]
|
||||
public void GetAssassinHonedDamageBonus_RangedWeaponsUseIndependentHalfChance(int randomValue, int expectedBonus)
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestBow();
|
||||
var attacker = CreateMobile(Direction.East);
|
||||
var defender = CreateMobile(Direction.East);
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.HS;
|
||||
weapon.ExtendedWeaponAttributes.AssassinHoned = 1;
|
||||
|
||||
using var random = new PredictableRandom(randomValue);
|
||||
Assert.Equal(expectedBonus, weapon.GetAssassinHonedDamageBonus(attacker, defender));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnHit_AssassinHonedUsesTheExistingThreeHundredPercentDamageCap()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestKatana();
|
||||
var attacker = CreateMobile(Direction.East);
|
||||
var defender = CreateMobile(Direction.East);
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.HS;
|
||||
weapon.ExtendedWeaponAttributes.AssassinHoned = 1;
|
||||
var before = defender.Hits;
|
||||
|
||||
weapon.OnHit(attacker, defender, 4.0);
|
||||
|
||||
Assert.Equal(400, before - defender.Hits);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnSwing_MissDoesNotApplyAssassinHoned()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestKatana { Hit = false };
|
||||
var attacker = CreateMobile(Direction.East, Map.Felucca, new Point3D(6200, 500, 0));
|
||||
var defender = CreateMobile(Direction.East, Map.Felucca, new Point3D(6201, 500, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.HS;
|
||||
weapon.ExtendedWeaponAttributes.AssassinHoned = 1;
|
||||
var before = defender.Hits;
|
||||
|
||||
weapon.OnSwing(attacker, defender);
|
||||
|
||||
Assert.Equal(before, defender.Hits);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnHit_ZeroDamageAfterAbsorptionDoesNotApplyAssassinHoned()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestKatana { AbsorbAllDamage = true };
|
||||
var attacker = CreateMobile(Direction.East);
|
||||
var defender = CreateMobile(Direction.East);
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.HS;
|
||||
weapon.ExtendedWeaponAttributes.AssassinHoned = 1;
|
||||
var before = defender.Hits;
|
||||
|
||||
weapon.OnHit(attacker, defender);
|
||||
|
||||
Assert.Equal(before, defender.Hits);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
private static Mobile CreateMobile(Direction direction, Map map = null, Point3D location = default)
|
||||
{
|
||||
var mobile = new Mobile(World.NewMobile);
|
||||
mobile.DefaultMobileInit();
|
||||
mobile.RawStr = 1000;
|
||||
mobile.Hits = mobile.HitsMax;
|
||||
mobile.Direction = direction;
|
||||
|
||||
if (map != null)
|
||||
{
|
||||
mobile.MoveToWorld(location, map);
|
||||
}
|
||||
|
||||
return mobile;
|
||||
}
|
||||
|
||||
private sealed class TestKatana(float mlSpeed = 2.0f) : Katana
|
||||
{
|
||||
public bool AbsorbAllDamage { get; init; }
|
||||
public bool Hit { get; init; } = true;
|
||||
|
||||
public override float MlSpeed => mlSpeed;
|
||||
public override bool CheckHit(Mobile attacker, Mobile defender) => Hit;
|
||||
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 TestBow : Bow
|
||||
{
|
||||
public override float MlSpeed => 2.0f;
|
||||
}
|
||||
|
||||
private sealed class RecordingPropertyList : IPropertyList
|
||||
{
|
||||
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)
|
||||
{
|
||||
}
|
||||
|
||||
public void AppendLiteral(string value)
|
||||
{
|
||||
}
|
||||
|
||||
public void AppendFormatted<T>(T value)
|
||||
{
|
||||
}
|
||||
|
||||
public void AppendFormatted<T>(T value, string format)
|
||||
{
|
||||
}
|
||||
|
||||
public void AppendFormatted<T>(T value, int alignment)
|
||||
{
|
||||
}
|
||||
|
||||
public void AppendFormatted<T>(T value, int alignment, string format)
|
||||
{
|
||||
}
|
||||
|
||||
public void AppendFormatted(ReadOnlySpan<char> value)
|
||||
{
|
||||
}
|
||||
|
||||
public void AppendFormatted(ReadOnlySpan<char> value, int alignment, string format = null)
|
||||
{
|
||||
}
|
||||
|
||||
public void AppendFormatted(object value, int alignment = 0, string format = null)
|
||||
{
|
||||
}
|
||||
|
||||
public void AppendFormatted(string value)
|
||||
{
|
||||
}
|
||||
|
||||
public void AppendFormatted(string value, int alignment, string format = null)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ public class ExtendedWeaponAttributesTests
|
|||
Assert.Equal(0x00000020, (int)ExtendedWeaponAttribute.SplinteringWeapon);
|
||||
Assert.Equal(0x00000040, (int)ExtendedWeaponAttribute.Focus);
|
||||
Assert.Equal(0x00000080, (int)ExtendedWeaponAttribute.BoneBreaker);
|
||||
Assert.Equal(0x00000100, (int)ExtendedWeaponAttribute.AssassinHoned);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -49,6 +50,7 @@ public class ExtendedWeaponAttributesTests
|
|||
Assert.Equal(0, weapon.ExtendedWeaponAttributes.SplinteringWeapon);
|
||||
Assert.Equal(0, weapon.ExtendedWeaponAttributes.Focus);
|
||||
Assert.Equal(0, weapon.ExtendedWeaponAttributes.BoneBreaker);
|
||||
Assert.Equal(0, weapon.ExtendedWeaponAttributes.AssassinHoned);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -88,6 +90,7 @@ public class ExtendedWeaponAttributesTests
|
|||
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.SplinteringWeapon));
|
||||
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.Focus));
|
||||
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.BoneBreaker));
|
||||
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.AssassinHoned));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -108,6 +111,7 @@ public class ExtendedWeaponAttributesTests
|
|||
weapon.ExtendedWeaponAttributes.SplinteringWeapon = 25;
|
||||
weapon.ExtendedWeaponAttributes.Focus = 1;
|
||||
weapon.ExtendedWeaponAttributes.BoneBreaker = 1;
|
||||
weapon.ExtendedWeaponAttributes.AssassinHoned = 1;
|
||||
|
||||
var writer = new BufferWriter(true);
|
||||
weapon.Serialize(writer);
|
||||
|
|
@ -126,6 +130,7 @@ public class ExtendedWeaponAttributesTests
|
|||
Assert.Equal(25, deserialized.ExtendedWeaponAttributes.SplinteringWeapon);
|
||||
Assert.Equal(1, deserialized.ExtendedWeaponAttributes.Focus);
|
||||
Assert.Equal(1, deserialized.ExtendedWeaponAttributes.BoneBreaker);
|
||||
Assert.Equal(1, deserialized.ExtendedWeaponAttributes.AssassinHoned);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2050,6 +2050,7 @@ public abstract partial class BaseWeapon
|
|||
// 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 += GetAssassinHonedDamageBonus(attacker, defender);
|
||||
|
||||
percentageBonus = Math.Min(percentageBonus, 300);
|
||||
|
||||
|
|
@ -2580,6 +2581,23 @@ public abstract partial class BaseWeapon
|
|||
return (int)(potentialDamage - defender.Hits / (double)defender.HitsMax * potentialDamage);
|
||||
}
|
||||
|
||||
internal int GetAssassinHonedDamageBonus(Mobile attacker, Mobile defender)
|
||||
{
|
||||
if (!Core.HS || ExtendedWeaponAttributes.AssassinHoned == 0 || MlSpeed <= 0 ||
|
||||
(attacker.Direction & Direction.Mask) != (defender.Direction & Direction.Mask))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (this is BaseRanged && Utility.Random(2) != 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Publish 74 policy: original ML speed only; SSI does not affect this bonus.
|
||||
return (int)(146.0 / MlSpeed);
|
||||
}
|
||||
|
||||
public virtual void GetDamageTypes(
|
||||
Mobile wielder, out int phys, out int fire, out int cold, out int pois,
|
||||
out int nrgy, out int chaos, out int direct
|
||||
|
|
|
|||
|
|
@ -1274,7 +1274,8 @@ namespace Server
|
|||
HitSwarm = 0x00000010,
|
||||
SplinteringWeapon = 0x00000020,
|
||||
Focus = 0x00000040,
|
||||
BoneBreaker = 0x00000080
|
||||
BoneBreaker = 0x00000080,
|
||||
AssassinHoned = 0x00000100
|
||||
}
|
||||
|
||||
public sealed class ExtendedWeaponAttributes : BaseAttributes
|
||||
|
|
@ -1369,6 +1370,13 @@ namespace Server
|
|||
set => this[ExtendedWeaponAttribute.BoneBreaker] = value;
|
||||
}
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public int AssassinHoned
|
||||
{
|
||||
get => this[ExtendedWeaponAttribute.AssassinHoned];
|
||||
set => this[ExtendedWeaponAttribute.AssassinHoned] = value;
|
||||
}
|
||||
|
||||
public void GetProperties(IPropertyList list)
|
||||
{
|
||||
if (Core.HS && Bane != 0)
|
||||
|
|
@ -1410,6 +1418,11 @@ namespace Server
|
|||
{
|
||||
list.Add(1157318); // Bone Breaker
|
||||
}
|
||||
|
||||
if (Core.HS && AssassinHoned != 0)
|
||||
{
|
||||
list.Add(1152206); // Assassin Honed
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => "...";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue