feat(items): add Brittle negative property

This commit is contained in:
Crome696 2026-07-09 10:24:19 +02:00
parent bbb2c05501
commit d4091e940d
3 changed files with 490 additions and 1 deletions

View file

@ -0,0 +1,455 @@
using System;
using System.Collections.Generic;
using Server;
using Server.Engines.Craft;
using Server.Items;
using Server.Misc;
using Server.Mobiles;
using Server.Text;
using Xunit;
namespace UOContent.Tests;
[Collection("Sequential UOContent Tests")]
public class BrittlePropertyTests
{
private const int BrittleCliloc = 1116209;
[Fact]
public void NegativeAttributes_StoresDupesAndSerializesBrittleOnSupportedFamilies()
{
AssertStoresAndDupesBrittle(new TestKatana(), new TestKatana());
AssertStoresAndDupesBrittle(new LeatherChest(), new LeatherChest());
AssertStoresAndDupesBrittle(new Buckler(), new Buckler());
AssertSerializesBrittle(new TestKatana(), new TestKatana());
AssertSerializesBrittle(new LeatherChest(), new LeatherChest());
AssertSerializesBrittle(new Buckler(), new Buckler());
}
[Fact]
public void NegativeAttributes_DoesNotStoreBrittleOnUnsupportedJewelry()
{
var ring = new GoldRing();
try
{
ring.NegativeAttributes.Brittle = 1;
Assert.Equal(0, ring.NegativeAttributes.Brittle);
}
finally
{
ring.Delete();
}
}
[Fact]
public void GetProperties_GatesBrittleTooltipToHighSeasOnSupportedFamilies()
{
var previousExpansion = Core.Expansion;
var items = CreateSupportedItems();
try
{
foreach (var item in items)
{
GetNegativeAttributes(item).Brittle = 1;
Core.Expansion = Expansion.SA;
var preHighSeas = new RecordingPropertyList();
item.GetProperties(preHighSeas);
Assert.DoesNotContain(preHighSeas.Entries, entry => entry.Number == BrittleCliloc);
Core.Expansion = Expansion.HS;
var highSeas = new RecordingPropertyList();
item.GetProperties(highSeas);
Assert.Contains(highSeas.Entries, entry => entry.Number == BrittleCliloc);
}
}
finally
{
Core.Expansion = previousExpansion;
DeleteItems(items);
}
}
[Fact]
public void PowderOfTemperament_FortifiesNonBrittleRepairableItem()
{
var previousExpansion = Core.Expansion;
var player = CreatePlayerMobile(new Point3D(6400, 500, 0));
var weapon = CreateDamagedWeapon();
var powder = new PowderOfTemperament();
try
{
Core.Expansion = Expansion.HS;
player.Backpack.AddItem(weapon);
player.Backpack.AddItem(powder);
UsePowder(player, powder, weapon);
Assert.Equal(9, powder.UsesRemaining);
Assert.True(weapon.MaxHitPoints > 100);
Assert.True(weapon.HitPoints > 50);
}
finally
{
Core.Expansion = previousExpansion;
weapon.Delete();
powder.Delete();
player.Delete();
}
}
[Fact]
public void PowderOfTemperament_RejectsBrittleWeaponAndArmorWithoutMutatingDurabilityOrCharges()
{
var previousExpansion = Core.Expansion;
var player = CreatePlayerMobile(new Point3D(6420, 500, 0));
var weapon = CreateDamagedWeapon();
var armor = CreateDamagedArmor();
var shield = CreateDamagedShield();
var powder = new PowderOfTemperament();
try
{
Core.Expansion = Expansion.HS;
player.Backpack.AddItem(weapon);
player.Backpack.AddItem(armor);
player.Backpack.AddItem(shield);
player.Backpack.AddItem(powder);
weapon.NegativeAttributes.Brittle = 1;
armor.NegativeAttributes.Brittle = 1;
shield.NegativeAttributes.Brittle = 1;
AssertPowderRejectsBrittle(player, powder, weapon);
AssertPowderRejectsBrittle(player, powder, armor);
AssertPowderRejectsBrittle(player, powder, shield);
}
finally
{
Core.Expansion = previousExpansion;
weapon.Delete();
armor.Delete();
shield.Delete();
powder.Delete();
player.Delete();
}
}
[Fact]
public void Brittle_IsNoOpBeforeHighSeasForPowderUse()
{
var previousExpansion = Core.Expansion;
var player = CreatePlayerMobile(new Point3D(6440, 500, 0));
var weapon = CreateDamagedWeapon();
var powder = new PowderOfTemperament();
try
{
Core.Expansion = Expansion.SA;
player.Backpack.AddItem(weapon);
player.Backpack.AddItem(powder);
weapon.NegativeAttributes.Brittle = 1;
UsePowder(player, powder, weapon);
Assert.False(NegativeAttributes.IsBrittle(weapon));
Assert.Equal(9, powder.UsesRemaining);
Assert.True(weapon.MaxHitPoints > 100);
}
finally
{
Core.Expansion = previousExpansion;
weapon.Delete();
powder.Delete();
player.Delete();
}
}
[Fact]
public void RepairFlow_StillRepairsBrittleWeaponsArmorAndShields()
{
var previousExpansion = Core.Expansion;
using var random = new Server.Tests.PredictableRandom(0);
var player = CreatePlayerMobile(new Point3D(6460, 500, 0));
var weapon = CreateDamagedWeapon();
var armor = CreateDamagedArmor();
var shield = CreateDamagedShield();
var smithHammer = new SmithHammer();
var sewingKit = new SewingKit();
var anvil = new Anvil();
var forge = new Forge();
try
{
Core.Expansion = Expansion.HS;
SkillCheck.Configure();
SkillCheck.Initialize();
player.SkillsCap = 7200;
player.Skills[SkillName.Blacksmith].Cap = 120.0;
player.Skills[SkillName.Tailoring].Cap = 120.0;
player.Skills[SkillName.Blacksmith].Base = 120.0;
player.Skills[SkillName.Tailoring].Base = 120.0;
player.Backpack.AddItem(weapon);
player.Backpack.AddItem(armor);
player.Backpack.AddItem(shield);
player.Backpack.AddItem(smithHammer);
player.Backpack.AddItem(sewingKit);
anvil.MoveToWorld(player.Location, player.Map);
forge.MoveToWorld(player.Location, player.Map);
weapon.NegativeAttributes.Brittle = 1;
armor.NegativeAttributes.Brittle = 1;
shield.NegativeAttributes.Brittle = 1;
RepairWithTool(player, GetOrInitBlacksmithySystem(), smithHammer, weapon);
RepairWithTool(player, GetOrInitTailoringSystem(), sewingKit, armor);
RepairWithTool(player, GetOrInitBlacksmithySystem(), smithHammer, shield);
Assert.Equal(weapon.MaxHitPoints, weapon.HitPoints);
Assert.Equal(armor.MaxHitPoints, armor.HitPoints);
Assert.Equal(shield.MaxHitPoints, shield.HitPoints);
}
finally
{
Core.Expansion = previousExpansion;
weapon.Delete();
armor.Delete();
shield.Delete();
smithHammer.Delete();
sewingKit.Delete();
anvil.Delete();
forge.Delete();
player.Delete();
}
}
[Fact]
public void RunicAttributeGeneration_DoesNotRollBrittle()
{
var previousExpansion = Core.Expansion;
var weapon = new TestKatana();
var armor = new LeatherChest();
var shield = new Buckler();
try
{
Core.Expansion = Expansion.HS;
BaseRunicTool.ApplyAttributesTo(weapon, false, 0, 25, 100, 100);
BaseRunicTool.ApplyAttributesTo(armor, false, 0, 25, 100, 100);
BaseRunicTool.ApplyAttributesTo(shield, false, 0, 25, 100, 100);
Assert.Equal(0, weapon.NegativeAttributes.Brittle);
Assert.Equal(0, armor.NegativeAttributes.Brittle);
Assert.Equal(0, shield.NegativeAttributes.Brittle);
}
finally
{
Core.Expansion = previousExpansion;
weapon.Delete();
armor.Delete();
shield.Delete();
}
}
private static void AssertStoresAndDupesBrittle<TItem>(TItem source, TItem dupe) where TItem : Item
{
try
{
GetNegativeAttributes(source).Brittle = 1;
source.Dupe(dupe);
Assert.Equal(1, GetNegativeAttributes(source).Brittle);
Assert.Equal(1, GetNegativeAttributes(dupe).Brittle);
}
finally
{
source.Delete();
dupe.Delete();
}
}
private static void AssertSerializesBrittle<TItem>(TItem source, TItem deserialized) where TItem : Item
{
try
{
GetNegativeAttributes(source).Brittle = 1;
var writer = new BufferWriter(true);
source.Serialize(writer);
var buffer = new byte[writer.Position];
writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer);
var reader = new BufferReader(buffer);
deserialized.Deserialize(reader);
Assert.Equal(buffer.Length, reader.Position);
Assert.Equal(1, GetNegativeAttributes(deserialized).Brittle);
}
finally
{
source.Delete();
deserialized.Delete();
}
}
private static void AssertPowderRejectsBrittle<TItem>(Mobile player, PowderOfTemperament powder, TItem item)
where TItem : Item, IDurability
{
var originalUses = powder.UsesRemaining;
var originalMaxHitPoints = item.MaxHitPoints;
var originalHitPoints = item.HitPoints;
UsePowder(player, powder, item);
Assert.Equal(originalUses, powder.UsesRemaining);
Assert.Equal(originalMaxHitPoints, item.MaxHitPoints);
Assert.Equal(originalHitPoints, item.HitPoints);
}
private static Katana CreateDamagedWeapon()
{
return new Katana
{
MaxHitPoints = 100,
HitPoints = 50
};
}
private static LeatherChest CreateDamagedArmor()
{
return new LeatherChest
{
MaxHitPoints = 100,
HitPoints = 50
};
}
private static Buckler CreateDamagedShield()
{
return new Buckler
{
MaxHitPoints = 100,
HitPoints = 50
};
}
private static void UsePowder(Mobile player, PowderOfTemperament powder, Item item)
{
powder.OnDoubleClick(player);
Assert.NotNull(player.Target);
player.Target.Invoke(player, item);
}
private static void RepairWithTool(Mobile player, CraftSystem craftSystem, BaseTool tool, Item item)
{
Repair.Do(player, craftSystem, tool);
Assert.NotNull(player.Target);
player.Target.Invoke(player, item);
}
private static CraftSystem GetOrInitBlacksmithySystem()
{
if (DefBlacksmithy.CraftSystem == null)
{
DefBlacksmithy.Initialize();
}
return DefBlacksmithy.CraftSystem;
}
private static CraftSystem GetOrInitTailoringSystem()
{
if (DefTailoring.CraftSystem == null)
{
DefTailoring.Initialize();
}
return DefTailoring.CraftSystem;
}
private static PlayerMobile CreatePlayerMobile(Point3D location)
{
var mobile = new PlayerMobile(World.NewMobile);
mobile.DefaultMobileInit();
mobile.MoveToWorld(location, Map.Felucca);
mobile.AddItem(new Backpack());
return mobile;
}
private static Item[] CreateSupportedItems() =>
[
new TestKatana(),
new LeatherChest(),
new Buckler()
];
private static NegativeAttributes GetNegativeAttributes(Item item) => item switch
{
BaseWeapon weapon => weapon.NegativeAttributes,
BaseArmor armor => armor.NegativeAttributes,
_ => throw new ArgumentException($"Unsupported Brittle host: {item.GetType().FullName}", nameof(item))
};
private static void DeleteItems(IEnumerable<Item> items)
{
foreach (var item in items)
{
item.Delete();
}
}
private class TestKatana : Katana
{
}
private sealed record PropertyEntry(int Number, string Argument);
private sealed class RecordingPropertyList : IPropertyList
{
private string _interpolated = string.Empty;
public List<PropertyEntry> Entries { get; } = [];
public void Reset()
{
}
public void Terminate()
{
}
public void Add(int number) => Entries.Add(new PropertyEntry(number, string.Empty));
public void Add(int number, string argument) => Entries.Add(new PropertyEntry(number, argument));
public void Add(ReadOnlySpan<char> argument) => Entries.Add(new PropertyEntry(0, argument.ToString()));
public void Add(int number, ReadOnlySpan<char> argument) => Entries.Add(new PropertyEntry(number, argument.ToString()));
public void AddChunked(ReadOnlySpan<char> text) => Entries.Add(new PropertyEntry(0, text.ToString()));
public OplTextBlock TextBlock() => new(this);
public void Add(int number, int value) => Entries.Add(new PropertyEntry(number, value.ToString()));
public void AddLocalized(int value) => Entries.Add(new PropertyEntry(0, value.ToString()));
public void AddLocalized(int number, int value) => Entries.Add(new PropertyEntry(number, value.ToString()));
public void Add(ref IPropertyList.InterpolatedStringHandler handler) => Entries.Add(new PropertyEntry(0, _interpolated));
public void Add(int number, ref IPropertyList.InterpolatedStringHandler handler) =>
Entries.Add(new PropertyEntry(number, _interpolated));
public void InitializeInterpolation(int literalLength, int formattedCount) => _interpolated = string.Empty;
public void AppendLiteral(string value) => _interpolated += value;
public void AppendFormatted<T>(T value) => _interpolated += value;
public void AppendFormatted<T>(T value, string format) =>
_interpolated += value is IFormattable formattable ? formattable.ToString(format, null) : value;
public void AppendFormatted<T>(T value, int alignment) => _interpolated += value;
public void AppendFormatted<T>(T value, int alignment, string format) =>
_interpolated += value is IFormattable formattable ? formattable.ToString(format, null) : value;
public void AppendFormatted(ReadOnlySpan<char> value) => _interpolated += value.ToString();
public void AppendFormatted(ReadOnlySpan<char> value, int alignment, string format = null) =>
_interpolated += value.ToString();
public void AppendFormatted(object value, int alignment = 0, string format = null) => _interpolated += value;
public void AppendFormatted(string value) => _interpolated += value;
public void AppendFormatted(string value, int alignment, string format = null) => _interpolated += value;
}
}

View file

@ -86,6 +86,12 @@ public partial class PowderOfTemperament : Item, IUsesRemaining
return;
}
if (NegativeAttributes.IsBrittle(item))
{
from.SendLocalizedMessage(1149799); // That cannot be used on brittle items.
return;
}
if (!item.IsChildOf(from.Backpack) && (!Core.ML || item.Parent != from) ||
!_powder.IsChildOf(from.Backpack))
{

View file

@ -1205,7 +1205,8 @@ namespace Server
public enum NegativeAttribute
{
Prized = 0x00000001,
Massive = 0x00000002
Massive = 0x00000002,
Brittle = 0x00000004
}
public sealed class NegativeAttributes : BaseAttributes
@ -1238,6 +1239,28 @@ namespace Server
set => this[NegativeAttribute.Massive] = Owner is BaseWeapon or BaseArmor ? value : 0;
}
[CommandProperty(AccessLevel.GameMaster)]
public int Brittle
{
get => Owner is BaseWeapon or BaseArmor ? this[NegativeAttribute.Brittle] : 0;
set => this[NegativeAttribute.Brittle] = Owner is BaseWeapon or BaseArmor ? value : 0;
}
public static bool IsBrittle(Item item)
{
if (!Core.HS)
{
return false;
}
return item switch
{
BaseWeapon weapon => weapon.NegativeAttributes.Brittle != 0,
BaseArmor armor => armor.NegativeAttributes.Brittle != 0,
_ => false
};
}
public static bool IsMassive(Item item)
{
if (!Core.HS)
@ -1275,6 +1298,11 @@ namespace Server
{
list.Add(1154910); // Prized
}
if (Core.HS && Brittle != 0)
{
list.Add(1116209); // Brittle
}
}
public override string ToString() => "...";