feat(items): implement antique property

This commit is contained in:
Crome696 2026-07-11 09:05:59 +02:00
parent 9174917ca6
commit e9d0d85385
7 changed files with 401 additions and 8 deletions

View file

@ -0,0 +1,210 @@
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 AntiquePropertyTests
{
[Fact]
public void NegativeAttribute_ReservesTheNextNonCollidingBitForAntique()
{
Assert.True(Enum.TryParse<NegativeAttribute>("Antique", out var antique));
Assert.Equal(0x00000008, (int)antique);
}
[Fact]
public void Antique_IsStoredAndDisplayedOnlyFromHighSeasOnEverySupportedHost()
{
var previousExpansion = Core.Expansion;
Item[] items = [new Katana(), new LeatherChest(), new Buckler(), new GoldRing()];
try
{
foreach (var item in items)
{
GetNegativeAttributes(item).Antique = 1;
Core.Expansion = Expansion.SA;
var beforeHighSeas = new RecordingPropertyList();
item.GetProperties(beforeHighSeas);
Assert.False(NegativeAttributes.IsAntique(item));
Assert.DoesNotContain(beforeHighSeas.Entries, entry => entry.Number == 1076187);
Core.Expansion = Expansion.HS;
var highSeas = new RecordingPropertyList();
item.GetProperties(highSeas);
Assert.True(NegativeAttributes.IsAntique(item));
Assert.Single(highSeas.Entries, entry => entry.Number == 1076187 && entry.Argument == string.Empty);
}
}
finally
{
Core.Expansion = previousExpansion;
foreach (var item in items)
{
item.Delete();
}
}
}
[Fact]
public void Antique_EquippedHostsLoseOneDurabilityOnAnAcceptedMissButBackpackItemsDoNot()
{
var previousExpansion = Core.Expansion;
using var random = new Server.Tests.PredictableRandom(0);
var attacker = CreateMobile(new Point3D(6200, 500, 0));
var defender = CreateMobile(new Point3D(6201, 500, 0));
var weapon = new TestKatana { MaxHitPoints = 10, HitPoints = 5 };
var armor = new LeatherChest { MaxHitPoints = 10, HitPoints = 5 };
var shield = new Buckler { Layer = Layer.TwoHanded, MaxHitPoints = 10, HitPoints = 5 };
var jewel = new GoldRing { MaxHitPoints = 10, HitPoints = 5 };
var backpackWeapon = new Katana { MaxHitPoints = 10, HitPoints = 5 };
try
{
Core.Expansion = Expansion.HS;
weapon.NegativeAttributes.Antique = 1;
armor.NegativeAttributes.Antique = 1;
shield.NegativeAttributes.Antique = 1;
jewel.NegativeAttributes.Antique = 1;
backpackWeapon.NegativeAttributes.Antique = 1;
attacker.AddItem(weapon);
attacker.AddItem(armor);
attacker.AddItem(shield);
attacker.AddItem(jewel);
attacker.AddItem(new Backpack());
attacker.Backpack.AddItem(backpackWeapon);
weapon.OnSwing(attacker, defender);
Assert.Equal(4, weapon.HitPoints);
Assert.Equal(4, armor.HitPoints);
Assert.Equal(4, shield.HitPoints);
Assert.Equal(4, jewel.HitPoints);
Assert.Equal(5, backpackWeapon.HitPoints);
}
finally
{
Core.Expansion = previousExpansion;
weapon.Delete();
armor.Delete();
shield.Delete();
jewel.Delete();
backpackWeapon.Delete();
attacker.Delete();
defender.Delete();
}
}
[Fact]
public void PowderOfTemperament_AntiqueWeaponAppliesTheFirstCapWithoutFreeRepair()
{
var previousExpansion = Core.Expansion;
var player = new PlayerMobile(World.NewMobile);
player.DefaultMobileInit();
player.MoveToWorld(new Point3D(6200, 500, 0), Map.Felucca);
player.AddItem(new Backpack());
var weapon = new Katana { MaxHitPoints = 245, HitPoints = 200 };
var powder = new PowderOfTemperament();
try
{
Core.Expansion = Expansion.HS;
weapon.NegativeAttributes.Antique = 1;
Assert.True(NegativeAttributes.IsAntique(weapon));
player.Backpack.AddItem(weapon);
player.Backpack.AddItem(powder);
powder.OnDoubleClick(player);
player.Target.Invoke(player, weapon);
Assert.Equal(250, weapon.MaxHitPoints);
Assert.Equal(200, weapon.HitPoints);
Assert.Equal(2, weapon.NegativeAttributes.Antique);
Assert.Equal(9, powder.UsesRemaining);
}
finally
{
Core.Expansion = previousExpansion;
weapon.Delete();
powder.Delete();
player.Delete();
}
}
private static Mobile CreateMobile(Point3D location)
{
var mobile = new Mobile(World.NewMobile);
mobile.DefaultMobileInit();
mobile.MoveToWorld(location, Map.Felucca);
return mobile;
}
private sealed class TestKatana : Katana
{
public override bool CheckHit(Mobile attacker, Mobile defender) => false;
public override int ComputeDamage(Mobile attacker, Mobile defender) => 1;
public override void AddBlood(Mobile attacker, Mobile defender, int damage)
{
}
}
private static NegativeAttributes GetNegativeAttributes(Item item) => item switch
{
BaseWeapon weapon => weapon.NegativeAttributes,
BaseArmor armor => armor.NegativeAttributes,
BaseJewel jewel => jewel.NegativeAttributes,
_ => throw new ArgumentException($"Unsupported Antique host: {item.GetType().FullName}", nameof(item))
};
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

@ -40,6 +40,11 @@ namespace Server.Engines.Craft
return EnhanceResult.BadItem;
}
if (NegativeAttributes.IsAntique(item))
{
return EnhanceResult.BadItem;
}
if (item is IArcaneEquip eq && eq.IsArcane)
{
return EnhanceResult.BadItem;

View file

@ -377,6 +377,50 @@ namespace Server.Engines.Craft
toDelete = true;
}
}
else if (targeted is BaseJewel jewel)
{
var skill = m_CraftSystem.MainSkill;
const int toWeaken = 1;
if (!NegativeAttributes.IsAntique(jewel) || m_CraftSystem.CraftItems.SearchForSubclass(jewel.GetType()) == null)
{
number = usingDeed ? 1061136 : 1044277; // That item cannot be repaired.
}
else if (!jewel.IsChildOf(from.Backpack) && (!Core.ML || jewel.Parent != from))
{
number = 1044275; // The item must be in your backpack to repair it.
}
else if (jewel.MaxHitPoints <= 0 || jewel.HitPoints == jewel.MaxHitPoints)
{
number = 1044281; // That item is in full repair
}
else if (jewel.MaxHitPoints <= toWeaken)
{
number = 1044278; // That item has been repaired many times, and will break if repairs are attempted again.
}
else
{
if (CheckWeaken(from, skill, jewel.HitPoints, jewel.MaxHitPoints))
{
jewel.MaxHitPoints -= toWeaken;
jewel.HitPoints = Math.Max(0, jewel.HitPoints - toWeaken);
}
if (CheckRepairDifficulty(from, skill, jewel.HitPoints, jewel.MaxHitPoints))
{
number = 1044279; // You repair the item.
m_CraftSystem.PlayCraftEffect(from);
jewel.HitPoints = jewel.MaxHitPoints;
}
else
{
number = usingDeed ? 1061137 : 1044280; // You fail to repair the item.
m_CraftSystem.PlayCraftEffect(from);
}
toDelete = true;
}
}
else if (targeted is BaseClothing clothing)
{
var skill = m_CraftSystem.MainSkill;

View file

@ -19,7 +19,7 @@ public enum GemType
}
[SerializationGenerator(6, false)]
public abstract partial class BaseJewel : Item, ICraftable, IAosItem
public abstract partial class BaseJewel : Item, ICraftable, IAosItem, IDurability
{
[EncodedInt]
[InvalidateProperties]
@ -123,6 +123,15 @@ public abstract partial class BaseJewel : Item, ICraftable, IAosItem
public virtual int InitMinHits => 0;
public virtual int InitMaxHits => 0;
public virtual bool CanFortify => false;
public void UnscaleDurability()
{
}
public void ScaleDurability()
{
}
public override int LabelNumber
{

View file

@ -1,3 +1,4 @@
using System;
using ModernUO.Serialization;
using Server.Network;
using Server.Targeting;
@ -80,18 +81,49 @@ public partial class PowderOfTemperament : Item, IUsesRemaining
return;
}
if (!wearable.CanFortify)
{
from.SendLocalizedMessage(1049083); // You cannot use the powder on that item.
return;
}
if (NegativeAttributes.IsBrittle(item))
{
from.SendLocalizedMessage(1149799); // That cannot be used on brittle items.
return;
}
if (NegativeAttributes.IsAntique(item))
{
if (item is BaseJewel || GetAntiqueMaximumDurability(item) is not { } maximumDurability)
{
from.SendLocalizedMessage(1049083); // You cannot use the powder on that item.
return;
}
if (!item.IsChildOf(from.Backpack) && (!Core.ML || item.Parent != from) ||
!_powder.IsChildOf(from.Backpack))
{
from.SendLocalizedMessage(1042001); // That must be in your pack for you to use it.
return;
}
wearable.MaxHitPoints = maximumDurability;
wearable.HitPoints = Math.Min(wearable.HitPoints, maximumDurability);
GetNegativeAttributes(item).Antique++;
from.SendLocalizedMessage(1049084); // You successfully use the powder on the item.
from.PlaySound(0x247);
--_powder.UsesRemaining;
if (_powder.UsesRemaining <= 0)
{
from.SendLocalizedMessage(1049086); // You have used up your powder of fortifying.
_powder.Delete();
}
return;
}
if (!wearable.CanFortify)
{
from.SendLocalizedMessage(1049083); // You cannot use the powder on that item.
return;
}
if (!item.IsChildOf(from.Backpack) && (!Core.ML || item.Parent != from) ||
!_powder.IsChildOf(from.Backpack))
{
@ -161,5 +193,21 @@ public partial class PowderOfTemperament : Item, IUsesRemaining
from.SendLocalizedMessage(1049085); // The item cannot be improved any further.
}
}
private static int? GetAntiqueMaximumDurability(Item item) => GetNegativeAttributes(item).Antique switch
{
1 => 250,
2 => 200,
3 => 150,
_ => null
};
private static NegativeAttributes GetNegativeAttributes(Item item) => item switch
{
BaseWeapon weapon => weapon.NegativeAttributes,
BaseArmor armor => armor.NegativeAttributes,
BaseJewel jewel => jewel.NegativeAttributes,
_ => throw new ArgumentException($"Unsupported Antique host: {item.GetType().FullName}", nameof(item))
};
}
}

View file

@ -933,6 +933,7 @@ public abstract partial class BaseWeapon
attacker.DisruptiveAction();
attacker.NetState?.SendSwing(attacker.Serial, defender.Serial);
NegativeAttributes.ApplyAntiqueWear(attacker);
if (attacker is BaseCreature bc)
{

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using ModernUO.Serialization;
using Server.Collections;
using Server.Items;
using Server.Mobiles;
using Server.Spells;
@ -1561,7 +1562,8 @@ namespace Server
{
Prized = 0x00000001,
Massive = 0x00000002,
Brittle = 0x00000004
Brittle = 0x00000004,
Antique = 0x00000008
}
public sealed class NegativeAttributes : BaseAttributes
@ -1594,6 +1596,13 @@ namespace Server
set => this[NegativeAttribute.Massive] = Owner is BaseWeapon or BaseArmor ? value : 0;
}
[CommandProperty(AccessLevel.GameMaster)]
public int Antique
{
get => Owner is BaseWeapon or BaseArmor or BaseJewel ? this[NegativeAttribute.Antique] : 0;
set => this[NegativeAttribute.Antique] = Owner is BaseWeapon or BaseArmor or BaseJewel ? value : 0;
}
[CommandProperty(AccessLevel.GameMaster)]
public int Brittle
{
@ -1601,6 +1610,68 @@ namespace Server
set => this[NegativeAttribute.Brittle] = Owner is BaseWeapon or BaseArmor ? value : 0;
}
public static bool IsAntique(Item item)
{
if (!Core.HS)
{
return false;
}
return item switch
{
BaseWeapon weapon => weapon.NegativeAttributes.Antique != 0,
BaseArmor armor => armor.NegativeAttributes.Antique != 0,
BaseJewel jewel => jewel.NegativeAttributes.Antique != 0,
_ => false
};
}
public static void ApplyAntiqueWear(Mobile wearer)
{
if (!wearer.Alive || !Core.HS)
{
return;
}
using var equipped = PooledRefList<Item>.Create(wearer.Items.Count);
foreach (var item in wearer.Items)
{
equipped.Add(item);
}
foreach (var item in equipped)
{
ApplyAntiqueWear(item);
}
}
private static void ApplyAntiqueWear(Item item)
{
if (!IsAntique(item) || Utility.Random(100) >= 2 || item is not IDurability durability)
{
return;
}
if (durability.HitPoints > 0)
{
--durability.HitPoints;
}
else if (durability.MaxHitPoints > 1)
{
--durability.MaxHitPoints;
if (item.Parent is Mobile mobile)
{
mobile.LocalOverheadMessage(MessageType.Regular, 0x3B2, 1061121);
}
}
else
{
item.Delete();
}
}
public static bool IsBrittle(Item item)
{
if (!Core.HS)
@ -1649,6 +1720,11 @@ namespace Server
public void GetProperties(IPropertyList list)
{
if (Core.HS && Antique != 0)
{
list.Add(1076187); // Antique
}
if (Core.HS && Prized != 0)
{
list.Add(1154910); // Prized