feat: add Damage Eater armor properties
This commit is contained in:
parent
7a40f7f0a7
commit
a969ac0d45
4 changed files with 1031 additions and 6 deletions
|
|
@ -0,0 +1,576 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Server;
|
||||
using Server.Items;
|
||||
using Server.Tests;
|
||||
using Server.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class DamageEaterPropertyTests
|
||||
{
|
||||
private const int DamageEaterCliloc = 1113598;
|
||||
private const int KineticEaterCliloc = 1113597;
|
||||
private const int FireEaterCliloc = 1113593;
|
||||
private const int ColdEaterCliloc = 1113594;
|
||||
private const int PoisonEaterCliloc = 1113595;
|
||||
private const int EnergyEaterCliloc = 1113596;
|
||||
private static readonly DateTime TestNow = new(2035, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
[Fact]
|
||||
public void AbsorptionAttributes_StoresDupesAndSerializesDamageEatersOnArmorAndShields()
|
||||
{
|
||||
var armor = new LeatherChest();
|
||||
var armorDupe = new LeatherChest();
|
||||
var deserialized = new LeatherChest();
|
||||
var shield = new BronzeShield();
|
||||
var shieldDupe = new BronzeShield();
|
||||
|
||||
try
|
||||
{
|
||||
armor.AbsorptionAttributes.DamageEater = 12;
|
||||
armor.AbsorptionAttributes.KineticEater = 13;
|
||||
armor.AbsorptionAttributes.FireEater = 14;
|
||||
armor.AbsorptionAttributes.ColdEater = 15;
|
||||
armor.AbsorptionAttributes.PoisonEater = 16;
|
||||
armor.AbsorptionAttributes.EnergyEater = 17;
|
||||
shield.AbsorptionAttributes.FireEater = 20;
|
||||
|
||||
armor.Dupe(armorDupe);
|
||||
shield.Dupe(shieldDupe);
|
||||
|
||||
Assert.Equal(12, armorDupe.AbsorptionAttributes.DamageEater);
|
||||
Assert.Equal(13, armorDupe.AbsorptionAttributes.KineticEater);
|
||||
Assert.Equal(14, armorDupe.AbsorptionAttributes.FireEater);
|
||||
Assert.Equal(15, armorDupe.AbsorptionAttributes.ColdEater);
|
||||
Assert.Equal(16, armorDupe.AbsorptionAttributes.PoisonEater);
|
||||
Assert.Equal(17, armorDupe.AbsorptionAttributes.EnergyEater);
|
||||
Assert.Equal(20, shieldDupe.AbsorptionAttributes.FireEater);
|
||||
|
||||
var writer = new BufferWriter(true);
|
||||
armor.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(12, deserialized.AbsorptionAttributes.DamageEater);
|
||||
Assert.Equal(13, deserialized.AbsorptionAttributes.KineticEater);
|
||||
Assert.Equal(14, deserialized.AbsorptionAttributes.FireEater);
|
||||
Assert.Equal(15, deserialized.AbsorptionAttributes.ColdEater);
|
||||
Assert.Equal(16, deserialized.AbsorptionAttributes.PoisonEater);
|
||||
Assert.Equal(17, deserialized.AbsorptionAttributes.EnergyEater);
|
||||
}
|
||||
finally
|
||||
{
|
||||
armor.Delete();
|
||||
armorDupe.Delete();
|
||||
deserialized.Delete();
|
||||
shield.Delete();
|
||||
shieldDupe.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AbsorptionAttributes_EaterPropertiesAreStaffEditable()
|
||||
{
|
||||
var names = new[]
|
||||
{
|
||||
nameof(AbsorptionAttributes.DamageEater),
|
||||
nameof(AbsorptionAttributes.KineticEater),
|
||||
nameof(AbsorptionAttributes.FireEater),
|
||||
nameof(AbsorptionAttributes.ColdEater),
|
||||
nameof(AbsorptionAttributes.PoisonEater),
|
||||
nameof(AbsorptionAttributes.EnergyEater)
|
||||
};
|
||||
|
||||
foreach (var name in names)
|
||||
{
|
||||
var property = typeof(AbsorptionAttributes).GetProperty(name);
|
||||
Assert.NotNull(property);
|
||||
|
||||
var attribute = property.GetCustomAttribute<CommandPropertyAttribute>();
|
||||
Assert.NotNull(attribute);
|
||||
Assert.Equal(AccessLevel.GameMaster, attribute.ReadLevel);
|
||||
Assert.Equal(AccessLevel.GameMaster, attribute.WriteLevel);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BaseArmor_GetProperties_GatesAllDamageEaterTooltipsToStygianAbyss()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var armor = new LeatherChest();
|
||||
|
||||
try
|
||||
{
|
||||
armor.AbsorptionAttributes.DamageEater = 3;
|
||||
armor.AbsorptionAttributes.KineticEater = 4;
|
||||
armor.AbsorptionAttributes.FireEater = 5;
|
||||
armor.AbsorptionAttributes.ColdEater = 6;
|
||||
armor.AbsorptionAttributes.PoisonEater = 7;
|
||||
armor.AbsorptionAttributes.EnergyEater = 8;
|
||||
|
||||
Core.Expansion = Expansion.ML;
|
||||
var preStygianAbyss = new RecordingPropertyList();
|
||||
armor.GetProperties(preStygianAbyss);
|
||||
Assert.DoesNotContain(
|
||||
preStygianAbyss.Entries,
|
||||
entry => entry.Number is DamageEaterCliloc or KineticEaterCliloc or FireEaterCliloc or ColdEaterCliloc
|
||||
or PoisonEaterCliloc or EnergyEaterCliloc
|
||||
);
|
||||
|
||||
Core.Expansion = Expansion.SA;
|
||||
var stygianAbyss = new RecordingPropertyList();
|
||||
armor.GetProperties(stygianAbyss);
|
||||
|
||||
Assert.Contains(stygianAbyss.Entries, entry => entry.Number == DamageEaterCliloc && entry.Argument == "3");
|
||||
Assert.Contains(stygianAbyss.Entries, entry => entry.Number == KineticEaterCliloc && entry.Argument == "4");
|
||||
Assert.Contains(stygianAbyss.Entries, entry => entry.Number == FireEaterCliloc && entry.Argument == "5");
|
||||
Assert.Contains(stygianAbyss.Entries, entry => entry.Number == ColdEaterCliloc && entry.Argument == "6");
|
||||
Assert.Contains(stygianAbyss.Entries, entry => entry.Number == PoisonEaterCliloc && entry.Argument == "7");
|
||||
Assert.Contains(stygianAbyss.Entries, entry => entry.Number == EnergyEaterCliloc && entry.Argument == "8");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
armor.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AbsorptionAttributes_GetEaterValue_AppliesSpecificAndAllTypeCaps()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var mobile = CreateMobile();
|
||||
var first = new LeatherChest();
|
||||
var second = new LeatherLegs();
|
||||
|
||||
try
|
||||
{
|
||||
first.AbsorptionAttributes.FireEater = 20;
|
||||
second.AbsorptionAttributes.FireEater = 20;
|
||||
first.AbsorptionAttributes.DamageEater = 12;
|
||||
second.AbsorptionAttributes.DamageEater = 12;
|
||||
mobile.AddItem(first);
|
||||
mobile.AddItem(second);
|
||||
|
||||
Core.Expansion = Expansion.ML;
|
||||
Assert.Equal(0, AbsorptionAttributes.GetEaterValue(mobile, AbsorptionAttribute.FireEater));
|
||||
|
||||
Core.Expansion = Expansion.SA;
|
||||
Assert.Equal(DamageEater.SpecificCap, AbsorptionAttributes.GetEaterValue(mobile, AbsorptionAttribute.FireEater));
|
||||
Assert.Equal(DamageEater.AllTypesCap, AbsorptionAttributes.GetEaterValue(mobile, AbsorptionAttribute.DamageEater));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
first.Delete();
|
||||
second.Delete();
|
||||
mobile.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DamagePipeline_PreStygianAbyssStoredValuesAreInert()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var previousNow = Core._now;
|
||||
var defender = CreateMobile(hitsMax: 1000, hits: 500);
|
||||
var source = CreateMobile();
|
||||
var armor = new LeatherChest();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.ML;
|
||||
Core._now = TestNow;
|
||||
armor.AbsorptionAttributes.KineticEater = 20;
|
||||
defender.AddItem(armor);
|
||||
|
||||
var appliedDamage = ApplyDamage(defender, source, 100, 100, 0, 0, 0, 0);
|
||||
Assert.True(appliedDamage > 0);
|
||||
Assert.Equal(500 - appliedDamage, defender.Hits);
|
||||
Assert.Equal(0, DamageEater.GetPendingChargesForTests(defender));
|
||||
Assert.False(DamageEater.TickForTests(defender));
|
||||
}
|
||||
finally
|
||||
{
|
||||
DamageEater.ClearAll();
|
||||
Core._now = previousNow;
|
||||
Core.Expansion = previousExpansion;
|
||||
armor.Delete();
|
||||
defender.Delete();
|
||||
source.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DamagePipeline_UsesMatchingPostResistDamageAndNonAdditiveAllTypeEater()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var previousNow = Core._now;
|
||||
var defender = CreateMobile(hitsMax: 1000, hits: 500);
|
||||
var source = CreateMobile();
|
||||
var armor = new LeatherChest();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
Core._now = TestNow;
|
||||
armor.AbsorptionAttributes.FireEater = 20;
|
||||
defender.AddItem(armor);
|
||||
|
||||
var wrongTypeDamage = ApplyDamage(defender, source, 100, 100, 0, 0, 0, 0);
|
||||
Assert.True(wrongTypeDamage > 0);
|
||||
Assert.Equal(0, DamageEater.GetPendingChargesForTests(defender));
|
||||
|
||||
armor.AbsorptionAttributes.DamageEater = 10;
|
||||
var appliedDamage = ApplyDamage(defender, source, 100, 0, 100, 0, 0, 0);
|
||||
Assert.True(appliedDamage > 0);
|
||||
Assert.Equal(1, DamageEater.GetPendingChargesForTests(defender));
|
||||
|
||||
Core._now = TestNow.Add(DamageEater.ConversionDelay);
|
||||
Assert.True(DamageEater.TickForTests(defender));
|
||||
Assert.Equal(500 - wrongTypeDamage - appliedDamage + appliedDamage * 20 / 100, defender.Hits);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DamageEater.ClearAll();
|
||||
Core._now = previousNow;
|
||||
Core.Expansion = previousExpansion;
|
||||
armor.Delete();
|
||||
defender.Delete();
|
||||
source.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DamagePipeline_ArmorIgnorePreservesTypedDamageForSpecificEaters()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var previousNow = Core._now;
|
||||
var defender = CreateMobile(hitsMax: 1000, hits: 500);
|
||||
var source = CreateMobile();
|
||||
var armor = new LeatherChest();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
Core._now = TestNow;
|
||||
armor.AbsorptionAttributes.KineticEater = 20;
|
||||
defender.AddItem(armor);
|
||||
|
||||
var appliedDamage = ApplyDamage(defender, source, 100, 100, 0, 0, 0, 0, ignoreArmor: true);
|
||||
Assert.True(appliedDamage > 0);
|
||||
Assert.Equal(1, DamageEater.GetPendingChargesForTests(defender));
|
||||
|
||||
Core._now = TestNow.Add(DamageEater.ConversionDelay);
|
||||
DamageEater.TickForTests(defender);
|
||||
Assert.Equal(500 - appliedDamage + appliedDamage * 20 / 100, defender.Hits);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DamageEater.ClearAll();
|
||||
Core._now = previousNow;
|
||||
Core.Expansion = previousExpansion;
|
||||
armor.Delete();
|
||||
defender.Delete();
|
||||
source.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DamagePipeline_AllTypeEaterIsCappedAndHandlesDirectDamage()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var previousNow = Core._now;
|
||||
var defender = CreateMobile(hitsMax: 1000, hits: 500);
|
||||
var source = CreateMobile();
|
||||
var armor = new LeatherChest();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
Core._now = TestNow;
|
||||
armor.AbsorptionAttributes.DamageEater = 25;
|
||||
armor.AbsorptionAttributes.KineticEater = 30;
|
||||
defender.AddItem(armor);
|
||||
|
||||
var appliedDamage = ApplyDamage(defender, source, 100, 0, 0, 0, 0, 0, 100);
|
||||
Assert.True(appliedDamage > 0);
|
||||
Assert.Equal(1, DamageEater.GetPendingChargesForTests(defender));
|
||||
|
||||
Core._now = TestNow.Add(DamageEater.ConversionDelay);
|
||||
DamageEater.TickForTests(defender);
|
||||
Assert.Equal(500 - appliedDamage + appliedDamage * DamageEater.AllTypesCap / 100, defender.Hits);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DamageEater.ClearAll();
|
||||
Core._now = previousNow;
|
||||
Core.Expansion = previousExpansion;
|
||||
armor.Delete();
|
||||
defender.Delete();
|
||||
source.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DamagePipeline_CapsPendingHealingAtTwentyCharges()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var previousNow = Core._now;
|
||||
var defender = CreateMobile(hitsMax: 5000, hits: 5000);
|
||||
var source = CreateMobile();
|
||||
var armor = new LeatherChest();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
Core._now = TestNow;
|
||||
armor.AbsorptionAttributes.KineticEater = 10;
|
||||
defender.AddItem(armor);
|
||||
|
||||
for (var i = 0; i < DamageEater.MaxCharges + 5; i++)
|
||||
{
|
||||
ApplyDamage(defender, source, 100, 100, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
Assert.Equal(DamageEater.MaxCharges, DamageEater.GetPendingChargesForTests(defender));
|
||||
}
|
||||
finally
|
||||
{
|
||||
DamageEater.ClearAll();
|
||||
Core._now = previousNow;
|
||||
Core.Expansion = previousExpansion;
|
||||
armor.Delete();
|
||||
defender.Delete();
|
||||
source.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DamagePipeline_FullQueueStillResetsConversionCadence()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var previousNow = Core._now;
|
||||
var defender = CreateMobile(hitsMax: 5000, hits: 5000);
|
||||
var source = CreateMobile();
|
||||
var armor = new LeatherChest();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
Core._now = TestNow;
|
||||
armor.AbsorptionAttributes.KineticEater = 10;
|
||||
defender.AddItem(armor);
|
||||
|
||||
var firstDamage = 0;
|
||||
for (var i = 0; i < DamageEater.MaxCharges; i++)
|
||||
{
|
||||
var appliedDamage = ApplyDamage(defender, source, 100, 100, 0, 0, 0, 0);
|
||||
firstDamage = i == 0 ? appliedDamage : firstDamage;
|
||||
}
|
||||
|
||||
Core._now = TestNow.AddSeconds(2);
|
||||
ApplyDamage(defender, source, 100, 100, 0, 0, 0, 0);
|
||||
var hitsAfterDamage = defender.Hits;
|
||||
|
||||
Core._now = TestNow.Add(DamageEater.ConversionDelay);
|
||||
DamageEater.TickForTests(defender);
|
||||
Assert.Equal(hitsAfterDamage, defender.Hits);
|
||||
Assert.Equal(DamageEater.MaxCharges, DamageEater.GetPendingChargesForTests(defender));
|
||||
|
||||
Core._now = TestNow.AddSeconds(5);
|
||||
DamageEater.TickForTests(defender);
|
||||
Assert.Equal(hitsAfterDamage + firstDamage * 10 / 100, defender.Hits);
|
||||
Assert.Equal(DamageEater.MaxCharges - 1, DamageEater.GetPendingChargesForTests(defender));
|
||||
}
|
||||
finally
|
||||
{
|
||||
DamageEater.ClearAll();
|
||||
Core._now = previousNow;
|
||||
Core.Expansion = previousExpansion;
|
||||
armor.Delete();
|
||||
defender.Delete();
|
||||
source.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DamagePipeline_ConvertsOneChargeEveryThreeSecondsAfterLastDamage()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var previousNow = Core._now;
|
||||
var defender = CreateMobile(hitsMax: 1000, hits: 500);
|
||||
var source = CreateMobile();
|
||||
var armor = new LeatherChest();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
Core._now = TestNow;
|
||||
armor.AbsorptionAttributes.KineticEater = 10;
|
||||
defender.AddItem(armor);
|
||||
|
||||
var firstDamage = ApplyDamage(defender, source, 100, 100, 0, 0, 0, 0);
|
||||
var secondDamage = ApplyDamage(defender, source, 100, 100, 0, 0, 0, 0);
|
||||
var hitsAfterDamage = defender.Hits;
|
||||
|
||||
Core._now = TestNow.AddSeconds(2);
|
||||
DamageEater.TickForTests(defender);
|
||||
Assert.Equal(hitsAfterDamage, defender.Hits);
|
||||
Assert.Equal(2, DamageEater.GetPendingChargesForTests(defender));
|
||||
|
||||
var interveningDamage = ApplyDamage(defender, source, 100, 0, 100, 0, 0, 0);
|
||||
hitsAfterDamage -= interveningDamage;
|
||||
|
||||
Core._now = TestNow.Add(DamageEater.ConversionDelay);
|
||||
DamageEater.TickForTests(defender);
|
||||
Assert.Equal(hitsAfterDamage, defender.Hits);
|
||||
Assert.Equal(2, DamageEater.GetPendingChargesForTests(defender));
|
||||
|
||||
Core._now = TestNow.Add(DamageEater.ConversionDelay + DamageEater.ConversionDelay);
|
||||
DamageEater.TickForTests(defender);
|
||||
Assert.Equal(hitsAfterDamage + firstDamage * 10 / 100, defender.Hits);
|
||||
Assert.Equal(1, DamageEater.GetPendingChargesForTests(defender));
|
||||
|
||||
Core._now = TestNow.Add(DamageEater.ConversionDelay + DamageEater.ConversionDelay + DamageEater.ConversionDelay);
|
||||
DamageEater.TickForTests(defender);
|
||||
Assert.Equal(hitsAfterDamage + firstDamage * 10 / 100 + secondDamage * 10 / 100, defender.Hits);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DamageEater.ClearAll();
|
||||
Core._now = previousNow;
|
||||
Core.Expansion = previousExpansion;
|
||||
armor.Delete();
|
||||
defender.Delete();
|
||||
source.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DamagePipeline_CleansPendingHealingWhenArmorIsRemovedOrOwnerDies()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var previousNow = Core._now;
|
||||
var defender = CreateMobile(hitsMax: 1000, hits: 500);
|
||||
var source = CreateMobile();
|
||||
var armor = new LeatherChest();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
Core._now = TestNow;
|
||||
armor.AbsorptionAttributes.KineticEater = 10;
|
||||
defender.AddItem(armor);
|
||||
ApplyDamage(defender, source, 100, 100, 0, 0, 0, 0);
|
||||
Assert.Equal(1, DamageEater.GetPendingChargesForTests(defender));
|
||||
|
||||
armor.Delete();
|
||||
Assert.Equal(0, DamageEater.GetPendingChargesForTests(defender));
|
||||
|
||||
var secondArmor = new LeatherChest();
|
||||
secondArmor.AbsorptionAttributes.KineticEater = 10;
|
||||
defender.AddItem(secondArmor);
|
||||
ApplyDamage(defender, source, 100, 100, 0, 0, 0, 0);
|
||||
defender.Kill();
|
||||
|
||||
Core._now = TestNow.Add(DamageEater.ConversionDelay);
|
||||
DamageEater.TickForTests(defender);
|
||||
Assert.Equal(0, defender.Hits);
|
||||
Assert.Equal(0, DamageEater.GetPendingChargesForTests(defender));
|
||||
secondArmor.Delete();
|
||||
}
|
||||
finally
|
||||
{
|
||||
DamageEater.ClearAll();
|
||||
Core._now = previousNow;
|
||||
Core.Expansion = previousExpansion;
|
||||
armor.Delete();
|
||||
defender.Delete();
|
||||
source.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
private static int ApplyDamage(
|
||||
Mobile defender,
|
||||
Mobile source,
|
||||
int damage,
|
||||
int physical,
|
||||
int fire,
|
||||
int cold,
|
||||
int poison,
|
||||
int energy,
|
||||
int direct = 0,
|
||||
bool ignoreArmor = false
|
||||
)
|
||||
{
|
||||
var oldHits = defender.Hits;
|
||||
AOS.Damage(defender, source, damage, ignoreArmor, physical, fire, cold, poison, energy, 0, direct);
|
||||
|
||||
return Math.Max(0, oldHits - defender.Hits);
|
||||
}
|
||||
|
||||
private static Mobile CreateMobile(int hitsMax = 1000, int hits = 1000)
|
||||
{
|
||||
var mobile = new Mobile(World.NewMobile);
|
||||
mobile.DefaultMobileInit();
|
||||
mobile.RawStr = Math.Max(1, (hitsMax - 50) * 2);
|
||||
Assert.Equal(hitsMax, mobile.HitsMax);
|
||||
mobile.Hits = hits;
|
||||
mobile.MoveToWorld(new Point3D(6200, 500, 0), Map.Felucca);
|
||||
return mobile;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1234,6 +1234,7 @@ namespace Server.Items
|
|||
{
|
||||
if (parent is Mobile m)
|
||||
{
|
||||
var hadDamageEater = AbsorptionAttributes?.HasDamageEater == true;
|
||||
var serial = Serial;
|
||||
|
||||
m.RemoveStatMod($"{serial}Str");
|
||||
|
|
@ -1247,6 +1248,11 @@ namespace Server.Items
|
|||
|
||||
m.Delta(MobileDelta.Armor); // Tell them armor rating has changed
|
||||
m.CheckStatTimers();
|
||||
|
||||
if (hadDamageEater)
|
||||
{
|
||||
DamageEater.Clear(m);
|
||||
}
|
||||
}
|
||||
|
||||
base.OnRemoved(parent);
|
||||
|
|
|
|||
289
Projects/UOContent/Items/Armor/DamageEater.cs
Normal file
289
Projects/UOContent/Items/Armor/DamageEater.cs
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ModernUO.CodeGeneratedEvents;
|
||||
using Server.Mobiles;
|
||||
|
||||
namespace Server.Items;
|
||||
|
||||
public static class DamageEater
|
||||
{
|
||||
internal const int SpecificCap = 30;
|
||||
internal const int AllTypesCap = 18;
|
||||
internal const int MaxCharges = 20;
|
||||
internal const int EffectCliloc = 1113617;
|
||||
|
||||
internal static readonly TimeSpan ConversionDelay = TimeSpan.FromSeconds(3.0);
|
||||
|
||||
// UO.com describes conversion every three seconds from the last received damage.
|
||||
// Treat each queued type portion as one charge: after a quiet interval, one charge converts,
|
||||
// and remaining charges continue at the same cadence. Any new damage resets that interval.
|
||||
|
||||
private static readonly Dictionary<Mobile, DamageEaterContext> _contexts = [];
|
||||
|
||||
public static void OnDamageTaken(
|
||||
Mobile defender,
|
||||
int appliedDamage,
|
||||
int physicalDamage,
|
||||
int fireDamage,
|
||||
int coldDamage,
|
||||
int poisonDamage,
|
||||
int energyDamage,
|
||||
int directDamage
|
||||
)
|
||||
{
|
||||
if (!IsValidOwner(defender) || appliedDamage <= 0)
|
||||
{
|
||||
Clear(defender);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HasValue(defender))
|
||||
{
|
||||
Clear(defender);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_contexts.TryGetValue(defender, out var context))
|
||||
{
|
||||
context = new DamageEaterContext(defender);
|
||||
_contexts[defender] = context;
|
||||
}
|
||||
|
||||
context.AddDamage(
|
||||
appliedDamage,
|
||||
physicalDamage,
|
||||
fireDamage,
|
||||
coldDamage,
|
||||
poisonDamage,
|
||||
energyDamage,
|
||||
directDamage
|
||||
);
|
||||
|
||||
if (context.PendingCharges == 0)
|
||||
{
|
||||
Clear(defender);
|
||||
}
|
||||
}
|
||||
|
||||
internal static int GetPendingChargesForTests(Mobile mobile) =>
|
||||
_contexts.TryGetValue(mobile, out var context) ? context.PendingCharges : 0;
|
||||
|
||||
internal static int GetPendingHealingForTests(Mobile mobile) =>
|
||||
_contexts.TryGetValue(mobile, out var context) ? context.PendingHealing : 0;
|
||||
|
||||
internal static bool TickForTests(Mobile mobile)
|
||||
{
|
||||
if (!_contexts.TryGetValue(mobile, out var context))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
context.Convert();
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void ClearAll()
|
||||
{
|
||||
foreach (var context in _contexts.Values)
|
||||
{
|
||||
context.Stop();
|
||||
}
|
||||
|
||||
_contexts.Clear();
|
||||
}
|
||||
|
||||
internal static void ClearIfInactive(Mobile mobile)
|
||||
{
|
||||
if (mobile != null && _contexts.ContainsKey(mobile) && !HasValue(mobile))
|
||||
{
|
||||
Clear(mobile);
|
||||
}
|
||||
}
|
||||
|
||||
[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 && _contexts.Remove(mobile, out var context))
|
||||
{
|
||||
context.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasValue(Mobile mobile) => AbsorptionAttributes.HasDamageEaterOn(mobile);
|
||||
|
||||
private static bool IsValidOwner(Mobile mobile) =>
|
||||
Core.SA && mobile?.Deleted == false && mobile.Alive && mobile.Map != null && mobile.Map != Map.Internal;
|
||||
|
||||
private static int ScaleDamagePortion(int appliedDamage, int portion, int totalPortion)
|
||||
{
|
||||
return (int)((long)appliedDamage * portion / totalPortion);
|
||||
}
|
||||
|
||||
private sealed class DamageEaterContext
|
||||
{
|
||||
private readonly Mobile _owner;
|
||||
private readonly Queue<int> _pending = [];
|
||||
private TimerExecutionToken _conversionToken;
|
||||
private DateTime _nextConversion;
|
||||
private int _pendingHealing;
|
||||
|
||||
public DamageEaterContext(Mobile owner)
|
||||
{
|
||||
_owner = owner;
|
||||
}
|
||||
|
||||
public int PendingCharges => _pending.Count;
|
||||
public int PendingHealing => _pendingHealing;
|
||||
|
||||
public void AddDamage(
|
||||
int appliedDamage,
|
||||
int physicalDamage,
|
||||
int fireDamage,
|
||||
int coldDamage,
|
||||
int poisonDamage,
|
||||
int energyDamage,
|
||||
int directDamage
|
||||
)
|
||||
{
|
||||
var totalPortion = physicalDamage + fireDamage + coldDamage + poisonDamage + energyDamage + directDamage;
|
||||
|
||||
if (totalPortion <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
QueueHealing(
|
||||
ScaleDamagePortion(appliedDamage, physicalDamage, totalPortion),
|
||||
AbsorptionAttribute.KineticEater
|
||||
);
|
||||
QueueHealing(
|
||||
ScaleDamagePortion(appliedDamage, fireDamage, totalPortion),
|
||||
AbsorptionAttribute.FireEater
|
||||
);
|
||||
QueueHealing(
|
||||
ScaleDamagePortion(appliedDamage, coldDamage, totalPortion),
|
||||
AbsorptionAttribute.ColdEater
|
||||
);
|
||||
QueueHealing(
|
||||
ScaleDamagePortion(appliedDamage, poisonDamage, totalPortion),
|
||||
AbsorptionAttribute.PoisonEater
|
||||
);
|
||||
QueueHealing(
|
||||
ScaleDamagePortion(appliedDamage, energyDamage, totalPortion),
|
||||
AbsorptionAttribute.EnergyEater
|
||||
);
|
||||
QueueDirectHealing(ScaleDamagePortion(appliedDamage, directDamage, totalPortion));
|
||||
|
||||
if (_pending.Count > 0)
|
||||
{
|
||||
_nextConversion = Core.Now + ConversionDelay;
|
||||
_conversionToken.Cancel();
|
||||
Timer.StartTimer(ConversionDelay, Convert, out _conversionToken);
|
||||
}
|
||||
}
|
||||
|
||||
public void Convert()
|
||||
{
|
||||
if (Core.Now < _nextConversion)
|
||||
{
|
||||
_conversionToken.Cancel();
|
||||
Timer.StartTimer(_nextConversion - Core.Now, Convert, out _conversionToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsValidOwner(_owner) || !HasValue(_owner))
|
||||
{
|
||||
DamageEater.Clear(_owner);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pending.Count == 0)
|
||||
{
|
||||
DamageEater.Clear(_owner);
|
||||
return;
|
||||
}
|
||||
|
||||
var amount = _pending.Dequeue();
|
||||
_pendingHealing -= amount;
|
||||
|
||||
var oldHits = _owner.Hits;
|
||||
_owner.Heal(amount, _owner, false);
|
||||
|
||||
if (_owner.Hits > oldHits)
|
||||
{
|
||||
_owner.SendLocalizedMessage(EffectCliloc);
|
||||
}
|
||||
|
||||
if (_pending.Count == 0)
|
||||
{
|
||||
DamageEater.Clear(_owner);
|
||||
return;
|
||||
}
|
||||
|
||||
_nextConversion = Core.Now + ConversionDelay;
|
||||
Timer.StartTimer(ConversionDelay, Convert, out _conversionToken);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_conversionToken.Cancel();
|
||||
_pending.Clear();
|
||||
_pendingHealing = 0;
|
||||
}
|
||||
|
||||
private void QueueHealing(int portionDamage, AbsorptionAttribute specificAttribute)
|
||||
{
|
||||
if (portionDamage <= 0 || _pending.Count >= MaxCharges)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var specific = AbsorptionAttributes.GetEaterValue(_owner, specificAttribute);
|
||||
var allTypes = AbsorptionAttributes.GetEaterValue(_owner, AbsorptionAttribute.DamageEater);
|
||||
var healing = GetHealing(portionDamage, specific, allTypes, specific >= allTypes);
|
||||
|
||||
Enqueue(healing);
|
||||
}
|
||||
|
||||
private void QueueDirectHealing(int portionDamage)
|
||||
{
|
||||
if (portionDamage <= 0 || _pending.Count >= MaxCharges)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var allTypes = AbsorptionAttributes.GetEaterValue(_owner, AbsorptionAttribute.DamageEater);
|
||||
Enqueue(GetHealing(portionDamage, 0, allTypes, false));
|
||||
}
|
||||
|
||||
private static int GetHealing(int portionDamage, int specific, int allTypes, bool useSpecific)
|
||||
{
|
||||
var rate = useSpecific ? specific : allTypes;
|
||||
var cap = useSpecific ? SpecificCap : AllTypesCap;
|
||||
|
||||
if (rate <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var healing = portionDamage * (double)rate / 100.0;
|
||||
var cappedHealing = portionDamage * (double)cap / 100.0;
|
||||
|
||||
return (int)Math.Min(healing, cappedHealing);
|
||||
}
|
||||
|
||||
private void Enqueue(int healing)
|
||||
{
|
||||
if (healing <= 0 || _pending.Count >= MaxCharges)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pending.Enqueue(healing);
|
||||
_pendingHealing += healing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +77,12 @@ namespace Server
|
|||
Fix(ref chaos);
|
||||
Fix(ref direct);
|
||||
|
||||
var firePostResistDamage = ignoreArmor ? damage * fire / 100 : 0;
|
||||
var physicalPostResistDamage = 0;
|
||||
var firePostResistDamage = 0;
|
||||
var coldPostResistDamage = 0;
|
||||
var poisonPostResistDamage = 0;
|
||||
var energyPostResistDamage = 0;
|
||||
var directPostResistDamage = 0;
|
||||
|
||||
if (Core.ML && chaos > 0)
|
||||
{
|
||||
|
|
@ -111,6 +116,16 @@ namespace Server
|
|||
}
|
||||
}
|
||||
|
||||
if (ignoreArmor)
|
||||
{
|
||||
physicalPostResistDamage = damage * phys / 100;
|
||||
firePostResistDamage = damage * fire / 100;
|
||||
coldPostResistDamage = damage * cold / 100;
|
||||
poisonPostResistDamage = damage * pois / 100;
|
||||
energyPostResistDamage = damage * nrgy / 100;
|
||||
directPostResistDamage = damage * direct / 100;
|
||||
}
|
||||
|
||||
BaseQuiver quiver = null;
|
||||
|
||||
if (archer && from != null)
|
||||
|
|
@ -131,18 +146,23 @@ namespace Server
|
|||
|
||||
var fireDamageNumerator = damage * fire * (100 - resFire);
|
||||
|
||||
physicalPostResistDamage = damage * phys * (100 - resPhys) / 10000;
|
||||
firePostResistDamage = fireDamageNumerator / 10000;
|
||||
coldPostResistDamage = damage * cold * (100 - resCold) / 10000;
|
||||
poisonPostResistDamage = damage * pois * (100 - resPois) / 10000;
|
||||
energyPostResistDamage = damage * nrgy * (100 - resNrgy) / 10000;
|
||||
|
||||
totalDamage = damage * phys * (100 - resPhys);
|
||||
totalDamage += fireDamageNumerator;
|
||||
totalDamage += damage * cold * (100 - resCold);
|
||||
totalDamage += damage * pois * (100 - resPois);
|
||||
totalDamage += damage * nrgy * (100 - resNrgy);
|
||||
|
||||
totalDamage /= 10000;
|
||||
firePostResistDamage = fireDamageNumerator / 10000;
|
||||
|
||||
if (Core.ML)
|
||||
{
|
||||
totalDamage += damage * direct / 100;
|
||||
directPostResistDamage = damage * direct / 100;
|
||||
totalDamage += directPostResistDamage;
|
||||
|
||||
if (quiver != null)
|
||||
{
|
||||
|
|
@ -251,6 +271,17 @@ namespace Server
|
|||
|
||||
PurgeMagicSpell.OnMobileDamaged(from, m, appliedDamage);
|
||||
|
||||
DamageEater.OnDamageTaken(
|
||||
m,
|
||||
appliedDamage,
|
||||
physicalPostResistDamage,
|
||||
firePostResistDamage,
|
||||
coldPostResistDamage,
|
||||
poisonPostResistDamage,
|
||||
energyPostResistDamage,
|
||||
directPostResistDamage
|
||||
);
|
||||
|
||||
if (firePostResistDamage > 0 && appliedDamage > 0)
|
||||
{
|
||||
Swarm.ClearDefender(m);
|
||||
|
|
@ -1313,7 +1344,13 @@ namespace Server
|
|||
[Flags]
|
||||
public enum AbsorptionAttribute
|
||||
{
|
||||
CastingFocus = 0x00000001
|
||||
CastingFocus = 0x00000001,
|
||||
DamageEater = 0x00000002,
|
||||
KineticEater = 0x00000004,
|
||||
FireEater = 0x00000008,
|
||||
ColdEater = 0x00000010,
|
||||
PoisonEater = 0x00000020,
|
||||
EnergyEater = 0x00000040
|
||||
}
|
||||
|
||||
public sealed class AbsorptionAttributes : BaseAttributes
|
||||
|
|
@ -1339,9 +1376,64 @@ namespace Server
|
|||
set => this[AbsorptionAttribute.CastingFocus] = Owner is BaseArmor ? value : 0;
|
||||
}
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public int DamageEater
|
||||
{
|
||||
get => Owner is BaseArmor ? this[AbsorptionAttribute.DamageEater] : 0;
|
||||
set => SetEaterValue(AbsorptionAttribute.DamageEater, value);
|
||||
}
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public int KineticEater
|
||||
{
|
||||
get => Owner is BaseArmor ? this[AbsorptionAttribute.KineticEater] : 0;
|
||||
set => SetEaterValue(AbsorptionAttribute.KineticEater, value);
|
||||
}
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public int FireEater
|
||||
{
|
||||
get => Owner is BaseArmor ? this[AbsorptionAttribute.FireEater] : 0;
|
||||
set => SetEaterValue(AbsorptionAttribute.FireEater, value);
|
||||
}
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public int ColdEater
|
||||
{
|
||||
get => Owner is BaseArmor ? this[AbsorptionAttribute.ColdEater] : 0;
|
||||
set => SetEaterValue(AbsorptionAttribute.ColdEater, value);
|
||||
}
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public int PoisonEater
|
||||
{
|
||||
get => Owner is BaseArmor ? this[AbsorptionAttribute.PoisonEater] : 0;
|
||||
set => SetEaterValue(AbsorptionAttribute.PoisonEater, value);
|
||||
}
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public int EnergyEater
|
||||
{
|
||||
get => Owner is BaseArmor ? this[AbsorptionAttribute.EnergyEater] : 0;
|
||||
set => SetEaterValue(AbsorptionAttribute.EnergyEater, value);
|
||||
}
|
||||
|
||||
internal bool HasDamageEater =>
|
||||
DamageEater != 0 || KineticEater != 0 || FireEater != 0 || ColdEater != 0 || PoisonEater != 0 || EnergyEater != 0;
|
||||
|
||||
private void SetEaterValue(AbsorptionAttribute attribute, int value)
|
||||
{
|
||||
this[attribute] = Owner is BaseArmor ? value : 0;
|
||||
|
||||
if (Owner is Item { Parent: Mobile mobile })
|
||||
{
|
||||
Server.Items.DamageEater.ClearIfInactive(mobile);
|
||||
}
|
||||
}
|
||||
|
||||
public static int GetValue(Mobile m, AbsorptionAttribute attribute)
|
||||
{
|
||||
if (!Core.SA)
|
||||
if (!Core.SA || m == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1367,6 +1459,35 @@ namespace Server
|
|||
return value;
|
||||
}
|
||||
|
||||
internal static bool HasDamageEaterOn(Mobile m)
|
||||
{
|
||||
if (!Core.SA || m == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var items = m.Items;
|
||||
for (var i = 0; i < items.Count; ++i)
|
||||
{
|
||||
if (items[i] is BaseArmor armor && armor.AbsorptionAttributes?.HasDamageEater == true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static int GetEaterValue(Mobile m, AbsorptionAttribute attribute)
|
||||
{
|
||||
var value = Math.Max(0, GetValue(m, attribute));
|
||||
var cap = attribute == AbsorptionAttribute.DamageEater
|
||||
? Server.Items.DamageEater.AllTypesCap
|
||||
: Server.Items.DamageEater.SpecificCap;
|
||||
|
||||
return Math.Min(value, cap);
|
||||
}
|
||||
|
||||
public void GetProperties(IPropertyList list)
|
||||
{
|
||||
var castingFocus = CastingFocus;
|
||||
|
|
@ -1374,6 +1495,39 @@ namespace Server
|
|||
{
|
||||
list.Add(1113696, castingFocus); // Casting Focus ~1_val~%
|
||||
}
|
||||
|
||||
if (Core.SA)
|
||||
{
|
||||
if (DamageEater != 0)
|
||||
{
|
||||
list.Add(1113598, DamageEater); // Damage Eater ~1_val~%
|
||||
}
|
||||
|
||||
if (KineticEater != 0)
|
||||
{
|
||||
list.Add(1113597, KineticEater); // Kinetic Eater ~1_val~%
|
||||
}
|
||||
|
||||
if (FireEater != 0)
|
||||
{
|
||||
list.Add(1113593, FireEater); // Fire Eater ~1_val~%
|
||||
}
|
||||
|
||||
if (ColdEater != 0)
|
||||
{
|
||||
list.Add(1113594, ColdEater); // Cold Eater ~1_val~%
|
||||
}
|
||||
|
||||
if (PoisonEater != 0)
|
||||
{
|
||||
list.Add(1113595, PoisonEater); // Poison Eater ~1_val~%
|
||||
}
|
||||
|
||||
if (EnergyEater != 0)
|
||||
{
|
||||
list.Add(1113596, EnergyEater); // Energy Eater ~1_val~%
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => "...";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue