feat(items): add Bone Breaker property

This commit is contained in:
Crome696 2026-07-11 22:56:23 +02:00
parent 8a02a60f80
commit 519e8debbd
8 changed files with 720 additions and 1 deletions

View file

@ -25,6 +25,9 @@ public static partial class EventSink
public static event Action<Mobile> Logout;
public static void InvokeLogout(Mobile m) => Logout?.Invoke(m);
public static event Action<Mobile, Map> MapChanged;
public static void InvokeMapChanged(Mobile mobile, Map oldMap) => MapChanged?.Invoke(mobile, oldMap);
public static event Action<Mobile> Connected;
public static void InvokeConnected(Mobile m) => Connected?.Invoke(m);

View file

@ -2581,6 +2581,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
ns.SendMobileAttributes(this);
OnMapChange(oldMap);
EventSink.InvokeMapChanged(this, oldMap);
}
public virtual void MoveToWorld(Point3D newLocation, Map map)

View file

@ -0,0 +1,490 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Server;
using Server.Items;
using Server.Mobiles;
using Server.Spells;
using Xunit;
namespace UOContent.Tests;
[Collection("Sequential UOContent Tests")]
public class BoneBreakerPropertyTests
{
private const int BoneBreakerCliloc = 1157318;
private static readonly Point3D TestLocation = new(6200, 520, 0);
[Fact]
public void ExtendedWeaponAttributes_StoresAndDupesBoneBreaker()
{
var weapon = new TestKatana();
var dupe = new TestKatana();
try
{
weapon.ExtendedWeaponAttributes.BoneBreaker = 1;
weapon.Dupe(dupe);
Assert.Equal(1, weapon.ExtendedWeaponAttributes.BoneBreaker);
Assert.Equal(1, dupe.ExtendedWeaponAttributes.BoneBreaker);
}
finally
{
weapon.Delete();
dupe.Delete();
}
}
[Fact]
public void ExtendedWeaponAttributes_GetProperties_GatesBoneBreakerTooltipToTimeOfLegends()
{
var previousExpansion = Core.Expansion;
var weapon = new TestKatana();
try
{
weapon.ExtendedWeaponAttributes.BoneBreaker = 1;
Core.Expansion = Expansion.HS;
var highSeas = new RecordingPropertyList();
weapon.ExtendedWeaponAttributes.GetProperties(highSeas);
Assert.DoesNotContain(highSeas.Entries, entry => entry.Number == BoneBreakerCliloc);
Core.Expansion = Expansion.TOL;
var timeOfLegends = new RecordingPropertyList();
weapon.ExtendedWeaponAttributes.GetProperties(timeOfLegends);
Assert.Contains(timeOfLegends.Entries, entry => entry.Number == BoneBreakerCliloc);
}
finally
{
Core.Expansion = previousExpansion;
weapon.Delete();
}
}
[Fact]
public void OnHit_NormalHitAppliesIndependentManaBonus()
{
var previousExpansion = Core.Expansion;
var weapon = new TestKatana();
var attacker = CreateMobile(mana: BoneBreaker.ManaThreshold);
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
try
{
Core.Expansion = Expansion.TOL;
weapon.ExtendedWeaponAttributes.BoneBreaker = 1;
weapon.OnHit(attacker, defender);
Assert.Equal(0, attacker.Mana);
Assert.Equal(149, defender.Hits);
}
finally
{
Core.Expansion = previousExpansion;
BoneBreaker.ClearAll();
weapon.Delete();
attacker.Delete();
defender.Delete();
}
}
[Theory]
[InlineData(0, 30)]
[InlineData(20, 24)]
[InlineData(40, 18)]
[InlineData(75, 18)]
public void TryProcOnNormalHit_ScalesManaCostWithLowerManaCost(int lowerManaCost, int expectedCost)
{
var previousExpansion = Core.Expansion;
var weapon = new TestKatana();
var attacker = CreateMobile(mana: BoneBreaker.ManaThreshold);
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
try
{
Core.Expansion = Expansion.TOL;
weapon.Attributes.LowerManaCost = lowerManaCost;
Assert.True(attacker.EquipItem(weapon));
BoneBreaker.TryProcOnNormalHit(attacker, defender, 1, 0);
Assert.Equal(BoneBreaker.ManaThreshold - expectedCost, attacker.Mana);
Assert.Equal(150, defender.Hits);
}
finally
{
Core.Expansion = previousExpansion;
BoneBreaker.ClearAll();
weapon.Delete();
attacker.Delete();
defender.Delete();
}
}
[Fact]
public void TryProcOnNormalHit_BelowManaThresholdCanDrainButDoesNotDamageOrSpendMana()
{
var previousExpansion = Core.Expansion;
var attacker = CreateMobile(mana: BoneBreaker.ManaThreshold - 1);
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
try
{
Core.Expansion = Expansion.TOL;
BoneBreaker.TryProcOnNormalHit(attacker, defender, 1, 100);
Assert.Equal(BoneBreaker.ManaThreshold - 1, attacker.Mana);
Assert.Equal(200, defender.Hits);
Assert.True(BoneBreaker.HasActiveDrain(defender));
}
finally
{
Core.Expansion = previousExpansion;
BoneBreaker.ClearAll();
attacker.Delete();
defender.Delete();
}
}
[Theory]
[InlineData(typeof(TestWeaponAbility))]
[InlineData(typeof(TestSpecialMove))]
public void OnHit_WeaponAbilitiesAndSpecialMovesDoNotTriggerBoneBreaker(Type actionType)
{
var previousExpansion = Core.Expansion;
var weapon = new TestKatana();
var attacker = CreateMobile(mana: BoneBreaker.ManaThreshold);
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
try
{
Core.Expansion = Expansion.TOL;
weapon.ExtendedWeaponAttributes.BoneBreaker = 1;
if (typeof(WeaponAbility).IsAssignableFrom(actionType))
{
WeaponAbility.Table[attacker] = (WeaponAbility)Activator.CreateInstance(actionType);
}
else
{
SpecialMove.Table[attacker] = (SpecialMove)Activator.CreateInstance(actionType);
}
weapon.OnHit(attacker, defender);
Assert.Equal(BoneBreaker.ManaThreshold, attacker.Mana);
Assert.False(BoneBreaker.HasActiveDrain(defender));
}
finally
{
Core.Expansion = previousExpansion;
WeaponAbility.Table.Remove(attacker);
SpecialMove.Table.Remove(attacker);
BoneBreaker.ClearAll();
weapon.Delete();
attacker.Delete();
defender.Delete();
}
}
[Fact]
public void Drain_BlocksOnlyRefreshPotionsUntilTheFourTicksComplete()
{
var previousExpansion = Core.Expansion;
var attacker = CreateMobile();
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
var refresh = new RefreshPotion();
var totalRefresh = new TotalRefreshPotion();
try
{
Core.Expansion = Expansion.TOL;
defender.Stam = defender.StamMax - 1;
refresh.MoveToWorld(defender.Location, defender.Map);
totalRefresh.MoveToWorld(defender.Location, defender.Map);
BoneBreaker.TryProcOnNormalHit(attacker, defender, 1, 100);
Assert.True(BoneBreaker.HasActiveDrain(defender));
Assert.False(refresh.CanDrink(defender));
Assert.False(totalRefresh.CanDrink(defender));
for (var i = 0; i < BoneBreaker.DrainTickCount; i++)
{
Assert.True(BoneBreaker.TickForTests(defender));
}
Assert.False(BoneBreaker.HasActiveDrain(defender));
Assert.True(BoneBreaker.IsImmune(defender));
Assert.True(refresh.CanDrink(defender));
Assert.True(totalRefresh.CanDrink(defender));
}
finally
{
Core.Expansion = previousExpansion;
BoneBreaker.ClearAll();
refresh.Delete();
totalRefresh.Delete();
attacker.Delete();
defender.Delete();
}
}
[Fact]
public void Drain_UsesFourOneSecondTicksAndPreservesOneStamina()
{
var previousExpansion = Core.Expansion;
var attacker = CreateMobile();
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
try
{
Core.Expansion = Expansion.TOL;
defender.Stam = 2;
BoneBreaker.TryProcOnNormalHit(attacker, defender, 1, 100);
Assert.Equal(TimeSpan.FromSeconds(1.0), BoneBreaker.DrainTickInterval);
for (var i = 0; i < BoneBreaker.DrainTickCount; i++)
{
Assert.True(BoneBreaker.TickForTests(defender));
Assert.Equal(1, defender.Stam);
}
Assert.False(BoneBreaker.HasActiveDrain(defender));
}
finally
{
Core.Expansion = previousExpansion;
BoneBreaker.ClearAll();
attacker.Delete();
defender.Delete();
}
}
[Fact]
public void ImmunityAfterDrainExpires_SuppressesBothBranchesWithoutManaCost()
{
var previousExpansion = Core.Expansion;
var attacker = CreateMobile(mana: 100);
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
try
{
Core.Expansion = Expansion.TOL;
BoneBreaker.TryProcOnNormalHit(attacker, defender, 1, 100);
for (var i = 0; i < BoneBreaker.DrainTickCount; i++)
{
BoneBreaker.TickForTests(defender);
}
var hitsAfterFirstProc = defender.Hits;
attacker.Mana = 100;
BoneBreaker.TryProcOnNormalHit(attacker, defender, 1, 100);
Assert.True(BoneBreaker.IsImmune(defender));
Assert.False(BoneBreaker.HasActiveDrain(defender));
Assert.Equal(100, attacker.Mana);
Assert.Equal(hitsAfterFirstProc, defender.Hits);
}
finally
{
Core.Expansion = previousExpansion;
BoneBreaker.ClearAll();
attacker.Delete();
defender.Delete();
}
}
[Fact]
public void ExpiredImmunity_AllowsBoneBreakerToProcAgain()
{
var previousExpansion = Core.Expansion;
var attacker = CreateMobile(mana: 100);
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
try
{
Core.Expansion = Expansion.TOL;
Assert.Equal(TimeSpan.FromSeconds(60), BoneBreaker.ImmunityDuration);
BoneBreaker.TryProcOnNormalHit(attacker, defender, 1, 100);
for (var i = 0; i < BoneBreaker.DrainTickCount; i++)
{
BoneBreaker.TickForTests(defender);
}
BoneBreaker.ExpireImmunityForTests(defender);
attacker.Mana = 100;
var hitsBeforeSecondProc = defender.Hits;
BoneBreaker.TryProcOnNormalHit(attacker, defender, 1, 100);
Assert.False(BoneBreaker.IsImmune(defender));
Assert.True(BoneBreaker.HasActiveDrain(defender));
Assert.Equal(70, attacker.Mana);
Assert.Equal(hitsBeforeSecondProc - BoneBreaker.Damage, defender.Hits);
}
finally
{
Core.Expansion = previousExpansion;
BoneBreaker.ClearAll();
attacker.Delete();
defender.Delete();
}
}
[Fact]
public void DrainCleanup_ReleasesStateWhenTargetLeavesTheWorld()
{
var previousExpansion = Core.Expansion;
var attacker = CreateMobile();
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
try
{
BoneBreaker.Configure();
Core.Expansion = Expansion.TOL;
var map = defender.Map;
BoneBreaker.TryProcOnNormalHit(attacker, defender, 1, 100);
defender.Internalize();
defender.MoveToWorld(new Point3D(6201, 520, 0), map);
Assert.False(BoneBreaker.HasActiveDrain(defender));
Assert.Equal(0, BoneBreaker.ActiveDrainCount);
}
finally
{
Core.Expansion = previousExpansion;
BoneBreaker.ClearAll();
attacker.Delete();
defender.Delete();
}
}
[Fact]
public void DrainCleanup_ReleasesStateWhenAttackerDies()
{
var previousExpansion = Core.Expansion;
var attacker = CreateMobile();
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
try
{
Core.Expansion = Expansion.TOL;
BoneBreaker.TryProcOnNormalHit(attacker, defender, 1, 100);
BoneBreaker.Clear(attacker);
Assert.False(BoneBreaker.HasActiveDrain(defender));
Assert.Equal(0, BoneBreaker.ActiveDrainCount);
}
finally
{
Core.Expansion = previousExpansion;
BoneBreaker.ClearAll();
attacker.Delete();
defender.Delete();
}
}
[Fact]
public void RunicAttributeGeneration_DoesNotRollBoneBreaker()
{
var previousExpansion = Core.Expansion;
var weapon = new TestKatana();
try
{
Core.Expansion = Expansion.TOL;
BaseRunicTool.ApplyAttributesTo(weapon, false, 0, 25, 100, 100);
Assert.Equal(0, weapon.ExtendedWeaponAttributes.BoneBreaker);
}
finally
{
Core.Expansion = previousExpansion;
weapon.Delete();
}
}
private static Mobile CreateMobile(int mana = 0, Point3D location = default)
{
var mobile = new Mobile(World.NewMobile);
mobile.DefaultMobileInit();
mobile.RawStr = 300;
mobile.RawDex = 100;
mobile.RawInt = 100;
mobile.Hits = mobile.HitsMax;
mobile.Stam = mobile.StamMax;
mobile.Mana = mana;
mobile.MoveToWorld(location == default ? TestLocation : location, Map.Felucca);
return mobile;
}
private class TestKatana : Katana
{
public override bool CheckHit(Mobile attacker, Mobile defender) => true;
public override int ComputeDamage(Mobile attacker, Mobile defender) => 1;
public override int AbsorbDamage(Mobile attacker, Mobile defender, int damage) => damage;
public override void AddBlood(Mobile attacker, Mobile defender, int damage)
{
}
}
private class TestWeaponAbility : WeaponAbility
{
}
private class TestSpecialMove : SpecialMove
{
}
private sealed class RecordingPropertyList : IPropertyList
{
private string _interpolated = string.Empty;
public List<(int Number, string Argument)> Entries { get; } = [];
public void Reset()
{
}
public void Terminate()
{
}
public void Add(int number) => Entries.Add((number, null));
public void Add(int number, string argument) => Entries.Add((number, argument));
public void Add(ReadOnlySpan<char> argument) => Entries.Add((0, argument.ToString()));
public void Add(int number, ReadOnlySpan<char> argument) => Entries.Add((number, argument.ToString()));
public void AddChunked(ReadOnlySpan<char> text) => Entries.Add((0, text.ToString()));
public OplTextBlock TextBlock() => new(this);
public void Add(int number, int value) => Entries.Add((number, value.ToString(CultureInfo.InvariantCulture)));
public void AddLocalized(int value) => Entries.Add((0, value.ToString(CultureInfo.InvariantCulture)));
public void AddLocalized(int number, int value) => Entries.Add((number, value.ToString(CultureInfo.InvariantCulture)));
public void Add(ref IPropertyList.InterpolatedStringHandler handler) => Entries.Add((0, _interpolated));
public void Add(int number, ref IPropertyList.InterpolatedStringHandler handler) => Entries.Add((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

@ -29,6 +29,7 @@ public class ExtendedWeaponAttributesTests
Assert.Equal(0x00000010, (int)ExtendedWeaponAttribute.HitSwarm);
Assert.Equal(0x00000020, (int)ExtendedWeaponAttribute.SplinteringWeapon);
Assert.Equal(0x00000040, (int)ExtendedWeaponAttribute.Focus);
Assert.Equal(0x00000080, (int)ExtendedWeaponAttribute.BoneBreaker);
}
[Fact]
@ -47,6 +48,7 @@ public class ExtendedWeaponAttributesTests
Assert.Equal(0, weapon.ExtendedWeaponAttributes.HitSwarm);
Assert.Equal(0, weapon.ExtendedWeaponAttributes.SplinteringWeapon);
Assert.Equal(0, weapon.ExtendedWeaponAttributes.Focus);
Assert.Equal(0, weapon.ExtendedWeaponAttributes.BoneBreaker);
}
finally
{
@ -85,6 +87,7 @@ public class ExtendedWeaponAttributesTests
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.HitSwarm));
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.SplinteringWeapon));
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.Focus));
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.BoneBreaker));
}
[Fact]
@ -104,6 +107,7 @@ public class ExtendedWeaponAttributesTests
weapon.ExtendedWeaponAttributes.HitSwarm = 30;
weapon.ExtendedWeaponAttributes.SplinteringWeapon = 25;
weapon.ExtendedWeaponAttributes.Focus = 1;
weapon.ExtendedWeaponAttributes.BoneBreaker = 1;
var writer = new BufferWriter(true);
weapon.Serialize(writer);
@ -121,6 +125,7 @@ public class ExtendedWeaponAttributesTests
Assert.Equal(30, deserialized.ExtendedWeaponAttributes.HitSwarm);
Assert.Equal(25, deserialized.ExtendedWeaponAttributes.SplinteringWeapon);
Assert.Equal(1, deserialized.ExtendedWeaponAttributes.Focus);
Assert.Equal(1, deserialized.ExtendedWeaponAttributes.BoneBreaker);
}
finally
{

View file

@ -18,6 +18,11 @@ public abstract partial class BaseRefreshPotion : BasePotion
return false;
}
if (BoneBreaker.HasActiveDrain(from))
{
return false;
}
if (from.Stam >= from.StamMax)
{
from.SendMessage("You decide against drinking this potion, as you are already at full stamina.");

View file

@ -2390,6 +2390,7 @@ public abstract partial class BaseWeapon
{
Sparks.TryProcOnNormalHit(attacker, defender, ExtendedWeaponAttributes.HitSparks);
Swarm.TryProcOnNormalHit(attacker, defender, ExtendedWeaponAttributes.HitSwarm);
BoneBreaker.TryProcOnNormalHit(attacker, defender, ExtendedWeaponAttributes.BoneBreaker);
}
}

View file

@ -0,0 +1,201 @@
using System;
using System.Collections.Generic;
using ModernUO.CodeGeneratedEvents;
using Server.Collections;
using Server.Mobiles;
namespace Server.Items;
public static class BoneBreaker
{
internal const int Damage = 50;
internal const int DrainChance = 20;
internal const int ManaThreshold = 30;
internal const int DrainTickCount = 4;
internal static readonly TimeSpan DrainTickInterval = TimeSpan.FromSeconds(1.0);
internal static readonly TimeSpan ImmunityDuration = TimeSpan.FromSeconds(60.0);
private static readonly Dictionary<Mobile, DrainContext> _drains = [];
private static readonly Dictionary<Mobile, TimerExecutionToken> _immunities = [];
public static void Configure()
{
EventSink.Logout += Clear;
EventSink.MapChanged += OnMapChanged;
}
private static void OnMapChanged(Mobile mobile, Map oldMap) => Clear(mobile);
public static void TryProcOnNormalHit(Mobile attacker, Mobile defender, int boneBreaker)
{
TryProcOnNormalHit(attacker, defender, boneBreaker, DrainChance);
}
internal static void TryProcOnNormalHit(Mobile attacker, Mobile defender, int boneBreaker, int drainChance)
{
if (!Core.TOL || boneBreaker <= 0 || !IsValidCombatant(attacker) || !IsValidCombatant(defender) ||
IsImmune(defender))
{
return;
}
if (attacker.Mana >= ManaThreshold)
{
var manaCost = GetManaCost(attacker);
attacker.Mana -= manaCost;
AOS.Damage(defender, attacker, Damage, false, 100, 0, 0, 0, 0);
}
if (drainChance > Utility.Random(100) && IsValidCombatant(defender) && !_drains.ContainsKey(defender))
{
_drains[defender] = new DrainContext(attacker, defender);
}
}
public static bool HasActiveDrain(Mobile mobile) => mobile != null && _drains.ContainsKey(mobile);
internal static bool IsImmune(Mobile mobile) => mobile != null && _immunities.ContainsKey(mobile);
internal static int ActiveDrainCount => _drains.Count;
internal static int GetManaCost(Mobile attacker)
{
var lowerManaCost = Math.Min(AosAttributes.GetValue(attacker, AosAttribute.LowerManaCost), 40);
return (int)(ManaThreshold * (1.0 - lowerManaCost / 100.0));
}
internal static bool TickForTests(Mobile defender)
{
if (!_drains.TryGetValue(defender, out var context))
{
return false;
}
context.Tick();
return true;
}
internal static void ExpireImmunityForTests(Mobile mobile) => RemoveImmunity(mobile);
internal static void ClearAll()
{
foreach (var context in _drains.Values)
{
context.Stop();
}
_drains.Clear();
foreach (var token in _immunities.Values)
{
token.Cancel();
}
_immunities.Clear();
}
[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 defenders = PooledRefList<Mobile>.Create();
foreach (var (defender, context) in _drains)
{
if (defender == mobile || context.Attacker == mobile)
{
defenders.Add(defender);
}
}
for (var i = 0; i < defenders.Count; i++)
{
StopDrain(defenders[i], false);
}
RemoveImmunity(mobile);
}
private static bool IsValidCombatant(Mobile mobile) =>
mobile is { Deleted: false, Alive: true } && mobile.Map != null && mobile.Map != Map.Internal;
private static void StopDrain(Mobile defender, bool grantImmunity)
{
if (!_drains.Remove(defender, out var context))
{
return;
}
context.Stop();
if (grantImmunity)
{
AddImmunity(defender);
}
}
private static void AddImmunity(Mobile defender)
{
if (_immunities.ContainsKey(defender))
{
return;
}
Timer.StartTimer(ImmunityDuration, () => RemoveImmunity(defender), out var token);
_immunities[defender] = token;
}
private static void RemoveImmunity(Mobile mobile)
{
if (mobile != null && _immunities.Remove(mobile, out var token))
{
token.Cancel();
}
}
private sealed class DrainContext
{
private readonly Mobile _defender;
private TimerExecutionToken _tickTimerToken;
public DrainContext(Mobile attacker, Mobile defender)
{
Attacker = attacker;
_defender = defender;
TicksRemaining = DrainTickCount;
Timer.StartTimer(DrainTickInterval, DrainTickInterval, Tick, out _tickTimerToken);
}
public Mobile Attacker { get; }
public int TicksRemaining { get; private set; }
public void Stop() => _tickTimerToken.Cancel();
public void Tick()
{
if (!IsValidCombatant(Attacker) || !IsValidCombatant(_defender))
{
StopDrain(_defender, false);
return;
}
var staminaDrain = Math.Max(1, _defender.StamMax / 10);
_defender.Stam = Math.Max(1, _defender.Stam - staminaDrain);
TicksRemaining--;
if (TicksRemaining <= 0)
{
StopDrain(_defender, true);
}
}
}
}

View file

@ -1257,7 +1257,8 @@ namespace Server
BloodDrinker = 0x00000008,
HitSwarm = 0x00000010,
SplinteringWeapon = 0x00000020,
Focus = 0x00000040
Focus = 0x00000040,
BoneBreaker = 0x00000080
}
public sealed class ExtendedWeaponAttributes : BaseAttributes
@ -1345,6 +1346,13 @@ namespace Server
}
}
[CommandProperty(AccessLevel.GameMaster)]
public int BoneBreaker
{
get => this[ExtendedWeaponAttribute.BoneBreaker];
set => this[ExtendedWeaponAttribute.BoneBreaker] = value;
}
public void GetProperties(IPropertyList list)
{
if (Core.HS && Bane != 0)
@ -1381,6 +1389,11 @@ namespace Server
{
list.Add(1157325, HitSwarm); // Swarm ~1_val~%
}
if (Core.TOL && BoneBreaker != 0)
{
list.Add(1157318); // Bone Breaker
}
}
public override string ToString() => "...";