feat(items): implement antique property
This commit is contained in:
parent
9174917ca6
commit
e9d0d85385
7 changed files with 401 additions and 8 deletions
210
Projects/UOContent.Tests/Tests/Items/AntiquePropertyTests.cs
Normal file
210
Projects/UOContent.Tests/Tests/Items/AntiquePropertyTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue