diff --git a/Projects/UOContent.Tests/Tests/Items/Weapons/BattleLustPropertyTests.cs b/Projects/UOContent.Tests/Tests/Items/Weapons/BattleLustPropertyTests.cs new file mode 100644 index 000000000..bd49f3bcb --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Items/Weapons/BattleLustPropertyTests.cs @@ -0,0 +1,624 @@ +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 BattleLustPropertyTests +{ + private const int BattleLustCliloc = 1113710; + private static readonly DateTime TestNow = new(2035, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + [Fact] + public void WeaponAttributes_StoresAndDupesBattleLust() + { + var weapon = new TestKatana(); + var dupe = new TestKatana(); + + try + { + weapon.WeaponAttributes.BattleLust = 1; + + weapon.Dupe(dupe); + + Assert.Equal(1, weapon.WeaponAttributes.BattleLust); + Assert.Equal(1, dupe.WeaponAttributes.BattleLust); + } + finally + { + weapon.Delete(); + dupe.Delete(); + } + } + + [Fact] + public void WeaponAttributes_GetProperties_GatesBattleLustTooltipToStygianAbyss() + { + var previousExpansion = Core.Expansion; + var weapon = new TestKatana(); + + try + { + weapon.WeaponAttributes.BattleLust = 1; + + Core.Expansion = Expansion.ML; + var preStygianAbyss = new RecordingPropertyList(); + weapon.WeaponAttributes.GetProperties(preStygianAbyss); + Assert.DoesNotContain(preStygianAbyss.Numbers, number => number == BattleLustCliloc); + + Core.Expansion = Expansion.SA; + var stygianAbyss = new RecordingPropertyList(); + weapon.WeaponAttributes.GetProperties(stygianAbyss); + Assert.Contains(stygianAbyss.Numbers, number => number == BattleLustCliloc); + } + finally + { + Core.Expansion = previousExpansion; + weapon.Delete(); + } + } + + [Fact] + public void DamageTaken_PreStygianAbyss_DoesNotGainBattleLustPoints() + { + var previousExpansion = Core.Expansion; + var previousNow = Core._now; + var wielder = CreateMobile(); + var source = CreateMobile(location: new Point3D(6201, 500, 0)); + var weapon = EquipBattleLustWeapon(wielder); + + try + { + Core.Expansion = Expansion.ML; + Core._now = TestNow; + + ApplyDamage(wielder, source, BattleLust.DamageThreshold); + + Assert.Equal(0, BattleLust.GetPoints(wielder)); + } + finally + { + Core.Expansion = previousExpansion; + Core._now = previousNow; + BattleLust.Clear(wielder); + weapon.Delete(); + wielder.Delete(); + source.Delete(); + } + } + + [Fact] + public void DamageTaken_GainsOnlyFromQualifyingExternalLivingMobileDamage() + { + var previousExpansion = Core.Expansion; + var previousNow = Core._now; + var wielder = CreateMobile(hitsMax: 5000, hits: 5000); + var source = CreateMobile(location: new Point3D(6201, 500, 0)); + var deletedSource = CreateMobile(location: new Point3D(6202, 500, 0)); + var deadSource = CreateDeadPlayerMobile(new Point3D(6203, 500, 0)); + var weapon = EquipBattleLustWeapon(wielder); + + try + { + Core.Expansion = Expansion.SA; + Core._now = TestNow; + + ApplyDamage(wielder, source, BattleLust.DamageThreshold - 1); + Assert.Equal(0, BattleLust.GetPoints(wielder)); + + Core._now = Core._now.AddSeconds(2); + ApplyDamage(wielder, null, BattleLust.DamageThreshold); + Assert.Equal(0, BattleLust.GetPoints(wielder)); + + Core._now = Core._now.AddSeconds(2); + ApplyDamage(wielder, wielder, BattleLust.DamageThreshold); + Assert.Equal(0, BattleLust.GetPoints(wielder)); + + deletedSource.Delete(); + Core._now = Core._now.AddSeconds(2); + ApplyDamage(wielder, deletedSource, BattleLust.DamageThreshold); + Assert.Equal(0, BattleLust.GetPoints(wielder)); + + Core._now = Core._now.AddSeconds(2); + ApplyDamage(wielder, deadSource, BattleLust.DamageThreshold); + Assert.Equal(0, BattleLust.GetPoints(wielder)); + + Core._now = Core._now.AddSeconds(2); + ApplyDamage(wielder, source, BattleLust.DamageThreshold); + Assert.Equal(1, BattleLust.GetPoints(wielder)); + } + finally + { + Core.Expansion = previousExpansion; + Core._now = previousNow; + BattleLust.Clear(wielder); + weapon.Delete(); + wielder.Delete(); + source.Delete(); + deletedSource.Delete(); + deadSource.Delete(); + } + } + + [Fact] + public void DamageTaken_DoesNotGainWhenMobileDamageAppliesNoHitPointLoss() + { + var previousExpansion = Core.Expansion; + var previousNow = Core._now; + var wielder = CreateMobile(hitsMax: 5000, hits: 5000); + var source = CreateMobile(location: new Point3D(6201, 500, 0)); + var weapon = EquipBattleLustWeapon(wielder); + + try + { + Core.Expansion = Expansion.SA; + Core._now = TestNow; + wielder.Blessed = true; + + ApplyDamage(wielder, source, BattleLust.DamageThreshold); + + Assert.Equal(5000, wielder.Hits); + Assert.Equal(0, BattleLust.GetPoints(wielder)); + } + finally + { + Core.Expansion = previousExpansion; + Core._now = previousNow; + BattleLust.Clear(wielder); + weapon.Delete(); + wielder.Delete(); + source.Delete(); + } + } + + [Fact] + public void DamageTaken_GainCooldownAllowsOnlyOnePointEveryTwoSecondsAndCapsAtFifteen() + { + var previousExpansion = Core.Expansion; + var previousNow = Core._now; + var wielder = CreateMobile(hitsMax: 5000, hits: 5000); + var source = CreateMobile(location: new Point3D(6201, 500, 0)); + var weapon = EquipBattleLustWeapon(wielder); + + try + { + Core.Expansion = Expansion.SA; + Core._now = TestNow; + + ApplyDamage(wielder, source, BattleLust.DamageThreshold); + Assert.Equal(1, BattleLust.GetPoints(wielder)); + + Core._now = Core._now.AddSeconds(1); + ApplyDamage(wielder, source, BattleLust.DamageThreshold); + Assert.Equal(1, BattleLust.GetPoints(wielder)); + + Core._now = Core._now.AddSeconds(1); + ApplyDamage(wielder, source, BattleLust.DamageThreshold); + Assert.Equal(2, BattleLust.GetPoints(wielder)); + + for (var i = 0; i < 20; i++) + { + Core._now = Core._now.AddSeconds(2); + ApplyDamage(wielder, source, BattleLust.DamageThreshold); + } + + Assert.Equal(BattleLust.MaxPoints, BattleLust.GetPoints(wielder)); + } + finally + { + Core.Expansion = previousExpansion; + Core._now = previousNow; + BattleLust.Clear(wielder); + weapon.Delete(); + wielder.Delete(); + source.Delete(); + } + } + + [Fact] + public void DamageTaken_DecaysOnePointEverySixSecondsAndCleansUpAtZero() + { + var previousExpansion = Core.Expansion; + var previousNow = Core._now; + var wielder = CreateMobile(); + var source = CreateMobile(location: new Point3D(6201, 500, 0)); + var weapon = EquipBattleLustWeapon(wielder); + + try + { + Core.Expansion = Expansion.SA; + Core._now = TestNow; + + ApplyDamage(wielder, source, BattleLust.DamageThreshold); + Assert.Equal(1, BattleLust.GetPoints(wielder)); + + Core._now = Core._now.AddSeconds(6); + Core._tickCount += 6000; + Timer.Slice(Core._tickCount); + + Assert.Equal(0, BattleLust.GetPoints(wielder)); + } + finally + { + Core.Expansion = previousExpansion; + Core._now = previousNow; + BattleLust.Clear(wielder); + weapon.Delete(); + wielder.Delete(); + source.Delete(); + } + } + + [Fact] + public void RuntimeContext_CleansUpWhenOwnerDiesOrLosesBattleLustWeapon() + { + var previousExpansion = Core.Expansion; + var previousNow = Core._now; + var deadWielder = CreateMobile(); + var disarmedWielder = CreateMobile(location: new Point3D(6202, 500, 0)); + var source = CreateMobile(location: new Point3D(6201, 500, 0)); + var deadWeapon = EquipBattleLustWeapon(deadWielder); + var disarmedWeapon = EquipBattleLustWeapon(disarmedWielder); + + try + { + Core.Expansion = Expansion.SA; + Core._now = TestNow; + + ApplyDamage(deadWielder, source, BattleLust.DamageThreshold); + Assert.Equal(1, BattleLust.GetPoints(deadWielder)); + deadWielder.Kill(); + Assert.Equal(0, BattleLust.GetPoints(deadWielder)); + + ApplyDamage(disarmedWielder, source, BattleLust.DamageThreshold); + Assert.Equal(1, BattleLust.GetPoints(disarmedWielder)); + disarmedWeapon.WeaponAttributes.BattleLust = 0; + Assert.Equal(0, BattleLust.GetDamageBonus(disarmedWielder, source)); + Assert.Equal(0, BattleLust.GetPoints(disarmedWielder)); + } + finally + { + Core.Expansion = previousExpansion; + Core._now = previousNow; + BattleLust.Clear(deadWielder); + BattleLust.Clear(disarmedWielder); + deadWeapon.Delete(); + disarmedWeapon.Delete(); + deadWielder.Delete(); + disarmedWielder.Delete(); + source.Delete(); + } + } + + [Fact] + public void RuntimeContext_ClearsOnWeaponLossEvenIfReequippedBeforeLazyCleanup() + { + var previousExpansion = Core.Expansion; + var previousNow = Core._now; + var wielder = CreateMobile(); + var source = CreateMobile(location: new Point3D(6201, 500, 0)); + var weapon = EquipBattleLustWeapon(wielder); + + try + { + Core.Expansion = Expansion.SA; + Core._now = TestNow; + + ApplyDamage(wielder, source, BattleLust.DamageThreshold); + Assert.Equal(1, BattleLust.GetPoints(wielder)); + + wielder.RemoveItem(weapon); + wielder.AddItem(weapon); + + Assert.Equal(0, BattleLust.GetPoints(wielder)); + } + finally + { + Core.Expansion = previousExpansion; + Core._now = previousNow; + BattleLust.Clear(wielder); + weapon.Delete(); + wielder.Delete(); + source.Delete(); + } + } + + [Fact] + public void RuntimeContext_ClearsWhenBattleLustPropertyIsRemovedEvenIfRestoredBeforeLazyCleanup() + { + var previousExpansion = Core.Expansion; + var previousNow = Core._now; + var wielder = CreateMobile(); + var source = CreateMobile(location: new Point3D(6201, 500, 0)); + var weapon = EquipBattleLustWeapon(wielder); + + try + { + Core.Expansion = Expansion.SA; + Core._now = TestNow; + + ApplyDamage(wielder, source, BattleLust.DamageThreshold); + Assert.Equal(1, BattleLust.GetPoints(wielder)); + + weapon.WeaponAttributes.BattleLust = 0; + weapon.WeaponAttributes.BattleLust = 1; + + Assert.Equal(0, BattleLust.GetPoints(wielder)); + } + finally + { + Core.Expansion = previousExpansion; + Core._now = previousNow; + BattleLust.Clear(wielder); + weapon.Delete(); + wielder.Delete(); + source.Delete(); + } + } + + [Fact] + public void RuntimeContext_ClearsOnInvalidMapEvenIfOwnerReturnsBeforeDecayTick() + { + var previousExpansion = Core.Expansion; + var previousNow = Core._now; + var wielder = CreateMobile(); + var source = CreateMobile(location: new Point3D(6201, 500, 0)); + var weapon = EquipBattleLustWeapon(wielder); + + try + { + Core.Expansion = Expansion.SA; + Core._now = TestNow; + + ApplyDamage(wielder, source, BattleLust.DamageThreshold); + Assert.Equal(1, BattleLust.GetPoints(wielder)); + + wielder.MoveToWorld(Point3D.Zero, Map.Internal); + wielder.MoveToWorld(new Point3D(6200, 500, 0), Map.Felucca); + + Assert.Equal(0, BattleLust.GetPoints(wielder)); + } + finally + { + Core.Expansion = previousExpansion; + Core._now = previousNow; + BattleLust.Clear(wielder); + weapon.Delete(); + wielder.Delete(); + source.Delete(); + } + } + + [Fact] + public void GetDamageBonus_UsesAggressedCountAndPropertySpecificPvpPvmCaps() + { + var previousExpansion = Core.Expansion; + var previousNow = Core._now; + var attacker = CreateMobile(hitsMax: 5000, hits: 5000); + var source = CreateMobile(location: new Point3D(6201, 500, 0)); + var playerDefender = CreateMobile(player: true, location: new Point3D(6202, 500, 0)); + var creatureDefender = CreateMobile(location: new Point3D(6203, 500, 0)); + var weapon = EquipBattleLustWeapon(attacker); + + try + { + Core.Expansion = Expansion.SA; + Core._now = TestNow; + BuildBattleLustPoints(attacker, source, BattleLust.MaxPoints); + + Assert.Equal(0, BattleLust.GetDamageBonus(attacker, creatureDefender)); + + AddAggressed(attacker, creatureDefender); + Assert.Equal(15, BattleLust.GetDamageBonus(attacker, creatureDefender)); + + AddAggressed(attacker, CreateMobile(location: new Point3D(6204, 500, 0))); + AddAggressed(attacker, playerDefender); + Assert.Equal(BattleLust.PvpDamageCap, BattleLust.GetDamageBonus(attacker, playerDefender)); + + for (var i = 0; i < 6; i++) + { + AddAggressed(attacker, CreateMobile(location: new Point3D(6205 + i, 500, 0))); + } + + Assert.Equal(BattleLust.PvmDamageCap, BattleLust.GetDamageBonus(attacker, creatureDefender)); + } + finally + { + Core.Expansion = previousExpansion; + Core._now = previousNow; + BattleLust.Clear(attacker); + weapon.Delete(); + DeleteAggressedDefenders(attacker); + attacker.Delete(); + source.Delete(); + } + } + + [Fact] + public void OnHit_AppliesBattleLustBeforeGlobalWeaponDamageCap() + { + var previousExpansion = Core.Expansion; + var previousNow = Core._now; + var attacker = CreateMobile(hitsMax: 5000, hits: 5000); + var source = CreateMobile(location: new Point3D(6201, 500, 0)); + var defender = CreateMobile(location: new Point3D(6202, 500, 0)); + var weapon = EquipBattleLustWeapon(attacker); + + try + { + Core.Expansion = Expansion.SA; + Core._now = TestNow; + BuildBattleLustPoints(attacker, source, BattleLust.MaxPoints); + AddAggressed(attacker, defender); + + var before = defender.Hits; + weapon.OnHit(attacker, defender); + Assert.Equal(115, before - defender.Hits); + + defender.Hits = before; + weapon.OnHit(attacker, defender, 4.0); + Assert.Equal(400, before - defender.Hits); + } + finally + { + Core.Expansion = previousExpansion; + Core._now = previousNow; + BattleLust.Clear(attacker); + weapon.Delete(); + DeleteAggressedDefenders(attacker); + attacker.Delete(); + source.Delete(); + } + } + + [Fact] + public void RunicAttributeGeneration_DoesNotRollBattleLust() + { + var previousExpansion = Core.Expansion; + var weapon = new TestKatana(); + + try + { + Core.Expansion = Expansion.SA; + + BaseRunicTool.ApplyAttributesTo(weapon, false, 0, 25, 100, 100); + + Assert.Equal(0, weapon.WeaponAttributes.BattleLust); + } + finally + { + Core.Expansion = previousExpansion; + weapon.Delete(); + } + } + + private static TestKatana EquipBattleLustWeapon(Mobile wielder) + { + var weapon = new TestKatana(); + weapon.WeaponAttributes.BattleLust = 1; + wielder.AddItem(weapon); + return weapon; + } + + private static void BuildBattleLustPoints(Mobile wielder, Mobile source, int points) + { + for (var i = 0; i < points; i++) + { + ApplyDamage(wielder, source, BattleLust.DamageThreshold); + Core._now = Core._now.AddSeconds(2); + } + + Assert.Equal(points, BattleLust.GetPoints(wielder)); + } + + private static void ApplyDamage(Mobile damaged, Mobile source, int damage) => + AOS.Damage(damaged, source, damage, false, 100, 0, 0, 0, 0); + + private static Mobile CreateMobile( + int hitsMax = 500, + int hits = 500, + bool player = false, + Point3D location = default + ) + { + var mobile = new Mobile(World.NewMobile) + { + Player = player + }; + + mobile.DefaultMobileInit(); + InitializeHits(mobile, hitsMax, hits); + mobile.MoveToWorld(location == default ? new Point3D(6200, 500, 0) : location, Map.Felucca); + return mobile; + } + + private static PlayerMobile CreateDeadPlayerMobile(Point3D location) + { + var mobile = new PlayerMobile(World.NewMobile); + mobile.DefaultMobileInit(); + mobile.Player = true; + mobile.Hits = mobile.HitsMax; + mobile.MoveToWorld(location, Map.Felucca); + mobile.Body = mobile.Race.GhostBody(mobile); + Assert.False(mobile.Alive); + return mobile; + } + + private static void InitializeHits(Mobile mobile, int hitsMax, int hits) + { + mobile.RawStr = Math.Max(1, (hitsMax - 50) * 2); + Assert.Equal(hitsMax, mobile.HitsMax); + mobile.Hits = hits; + } + + private static void AddAggressed(Mobile attacker, Mobile defender) => + attacker.Aggressed.Add(AggressorInfo.Create(attacker, defender, false)); + + private static void DeleteAggressedDefenders(Mobile attacker) + { + for (var i = 0; i < attacker.Aggressed.Count; i++) + { + var info = attacker.Aggressed[i]; + var defender = info.Defender; + defender.Delete(); + info.Free(); + } + + attacker.Aggressed.Clear(); + } + + private class TestKatana : Katana + { + public override int ComputeDamage(Mobile attacker, Mobile defender) => 100; + public override int AbsorbDamage(Mobile attacker, Mobile defender, int damage) => damage; + public override void AddBlood(Mobile attacker, Mobile defender, int damage) + { + } + } + + private sealed class RecordingPropertyList : IPropertyList + { + private string _interpolated = string.Empty; + + public List Numbers { get; } = []; + + public void Reset() + { + } + + public void Terminate() + { + } + + public void Add(int number) => Numbers.Add(number); + public void Add(int number, string argument) => Numbers.Add(number); + public void Add(ReadOnlySpan argument) => Numbers.Add(0); + public void Add(int number, ReadOnlySpan argument) => Numbers.Add(number); + public void AddChunked(ReadOnlySpan text) => Numbers.Add(0); + public OplTextBlock TextBlock() => new(this); + public void Add(int number, int value) => Numbers.Add(number); + public void AddLocalized(int value) => Numbers.Add(0); + public void AddLocalized(int number, int value) => Numbers.Add(number); + public void Add(ref IPropertyList.InterpolatedStringHandler handler) => Numbers.Add(0); + public void Add(int number, ref IPropertyList.InterpolatedStringHandler handler) => Numbers.Add(number); + public void InitializeInterpolation(int literalLength, int formattedCount) => _interpolated = string.Empty; + public void AppendLiteral(string value) => _interpolated += value; + public void AppendFormatted(T value) => _interpolated += value; + public void AppendFormatted(T value, string format) => + _interpolated += value is IFormattable formattable ? formattable.ToString(format, null) : value; + public void AppendFormatted(T value, int alignment) => _interpolated += value; + public void AppendFormatted(T value, int alignment, string format) => + _interpolated += value is IFormattable formattable ? formattable.ToString(format, null) : value; + public void AppendFormatted(ReadOnlySpan value) => _interpolated += value.ToString(); + public void AppendFormatted(ReadOnlySpan 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; + } +} diff --git a/Projects/UOContent/Items/Weapons/BaseWeapon.cs b/Projects/UOContent/Items/Weapons/BaseWeapon.cs index 7fb35661f..8087b0a2a 100644 --- a/Projects/UOContent/Items/Weapons/BaseWeapon.cs +++ b/Projects/UOContent/Items/Weapons/BaseWeapon.cs @@ -1140,6 +1140,11 @@ public abstract partial class BaseWeapon return; } + if (WeaponAttributes.BattleLust != 0) + { + BattleLust.Clear(m); + } + var serial = Serial; m.RemoveStatMod($"{serial}Str"); @@ -1176,6 +1181,16 @@ public abstract partial class BaseWeapon m.Delta(MobileDelta.WeaponDamage); } + public override void OnMapChange() + { + base.OnMapChange(); + + if ((Map == null || Map == Map.Internal) && Parent is Mobile m && WeaponAttributes.BattleLust != 0) + { + BattleLust.Clear(m); + } + } + public virtual SkillName GetUsedSkill(Mobile m, bool checkSkillAttrs) { SkillName sk; @@ -1886,6 +1901,8 @@ public abstract partial class BaseWeapon percentageBonus += talisman.Killer.DamageBonus(defender); } + percentageBonus += BattleLust.GetDamageBonus(attacker, defender); + percentageBonus = Math.Min(percentageBonus, 300); damage = AOS.Scale(damage, 100 + percentageBonus); diff --git a/Projects/UOContent/Items/Weapons/BattleLust.cs b/Projects/UOContent/Items/Weapons/BattleLust.cs new file mode 100644 index 000000000..9555851ec --- /dev/null +++ b/Projects/UOContent/Items/Weapons/BattleLust.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using ModernUO.CodeGeneratedEvents; +using Server.Mobiles; + +namespace Server.Items; + +public static class BattleLust +{ + internal const int DamageThreshold = 30; + internal const int MaxPoints = 15; + internal const int PvpDamageCap = 45; + internal const int PvmDamageCap = 90; + + private static readonly TimeSpan GainDelay = TimeSpan.FromSeconds(2.0); + private static readonly TimeSpan DecayDelay = TimeSpan.FromSeconds(6.0); + + private static readonly Dictionary _contexts = []; + + public static void OnDamageTaken(Mobile damaged, Mobile source, int damage) + { + if (damaged == null) + { + return; + } + + if (!CanGain(damaged, source, damage)) + { + if (_contexts.TryGetValue(damaged, out var existing) && !IsValidOwner(damaged)) + { + Remove(damaged, existing); + } + + return; + } + + if (!_contexts.TryGetValue(damaged, out var context)) + { + context = new BattleLustContext(damaged); + _contexts[damaged] = context; + } + + context.TryGain(); + } + + internal static int GetDamageBonus(Mobile attacker, Mobile defender) + { + if (attacker == null || defender?.Deleted != false) + { + return 0; + } + + if (!_contexts.TryGetValue(attacker, out var context)) + { + return 0; + } + + if (!IsValidOwner(attacker)) + { + Remove(attacker, context); + return 0; + } + + var opponentCount = GetActiveAggressedCount(attacker); + + if (opponentCount <= 0) + { + return 0; + } + + var cap = defender.Player ? PvpDamageCap : PvmDamageCap; + return Math.Min(context.Points * opponentCount, cap); + } + + internal static int GetPoints(Mobile mobile) + { + if (mobile == null || !_contexts.TryGetValue(mobile, out var context)) + { + return 0; + } + + if (!IsValidOwner(mobile)) + { + Remove(mobile, context); + return 0; + } + + return context.Points; + } + + [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 CanGain(Mobile damaged, Mobile source, int damage) => + Core.SA && + damage >= DamageThreshold && + source?.Deleted == false && + source.Alive && + source != damaged && + IsValidOwner(damaged); + + private static bool IsValidOwner(Mobile mobile) => + Core.SA && + mobile?.Deleted == false && + mobile.Alive && + mobile.Map != null && + mobile.Map != Map.Internal && + mobile.Weapon is BaseWeapon weapon && + weapon.WeaponAttributes.BattleLust != 0; + + private static int GetActiveAggressedCount(Mobile mobile) + { + var count = 0; + var list = mobile.Aggressed; + + for (var i = 0; i < list.Count; i++) + { + var info = list[i]; + var defender = info.Defender; + + if (!info.Expired && defender?.Deleted == false && defender.Alive) + { + count++; + } + } + + return count; + } + + private static void Remove(Mobile mobile, BattleLustContext context) + { + _contexts.Remove(mobile); + context.Stop(); + } + + private sealed class BattleLustContext + { + private readonly Mobile _owner; + private TimerExecutionToken _decayTimerToken; + private DateTime _nextGain; + + public BattleLustContext(Mobile owner) + { + _owner = owner; + Timer.StartTimer(DecayDelay, DecayDelay, Decay, out _decayTimerToken); + } + + public int Points { get; private set; } + + public void TryGain() + { + if (Core.Now < _nextGain) + { + return; + } + + if (Points < MaxPoints) + { + Points++; + } + + _nextGain = Core.Now + GainDelay; + } + + public void Stop() + { + _decayTimerToken.Cancel(); + } + + private void Decay() + { + if (!IsValidOwner(_owner)) + { + BattleLust.Clear(_owner); + return; + } + + Points--; + + if (Points <= 0) + { + BattleLust.Clear(_owner); + } + } + } +} \ No newline at end of file diff --git a/Projects/UOContent/Misc/AOS.cs b/Projects/UOContent/Misc/AOS.cs index b650c278e..9a0e524af 100644 --- a/Projects/UOContent/Misc/AOS.cs +++ b/Projects/UOContent/Misc/AOS.cs @@ -236,7 +236,9 @@ namespace Server SpellHelper.DoLeech(totalDamage, from, m); } + var oldHits = m.Hits; m.Damage(totalDamage, from); + BattleLust.OnDamageTaken(m, from, Math.Max(0, oldHits - m.Hits)); return totalDamage; } @@ -780,7 +782,8 @@ namespace Server UseBestSkill = 0x00400000, MageWeapon = 0x00800000, DurabilityBonus = 0x01000000, - Bane = 0x02000000 + Bane = 0x02000000, + BattleLust = 0x04000000 } public sealed class AosWeaponAttributes : BaseAttributes @@ -981,6 +984,23 @@ namespace Server set => this[AosWeaponAttribute.Bane] = value; } + [CommandProperty(AccessLevel.GameMaster)] + public int BattleLust + { + get => this[AosWeaponAttribute.BattleLust]; + set + { + var hadBattleLust = BattleLust != 0; + + this[AosWeaponAttribute.BattleLust] = value; + + if (hadBattleLust && value == 0 && Owner is BaseWeapon { Parent: Mobile m }) + { + Server.Items.BattleLust.Clear(m); + } + } + } + public static int GetValue(Mobile m, AosWeaponAttribute attribute) { if (!Core.AOS) @@ -1027,6 +1047,11 @@ namespace Server list.Add(1154671); // Bane } + if (Core.SA && BattleLust != 0) + { + list.Add(1113710); // Battle Lust + } + if (UseBestSkill != 0) { list.Add(1060400); // use best weapon skill