feat(items): add Splintering Weapon property
This commit is contained in:
parent
6883e5eaa0
commit
5810ce85cb
5 changed files with 803 additions and 3 deletions
|
|
@ -27,6 +27,7 @@ public class ExtendedWeaponAttributesTests
|
|||
Assert.Equal(0x00000004, (int)ExtendedWeaponAttribute.HitSparks);
|
||||
Assert.Equal(0x00000008, (int)ExtendedWeaponAttribute.BloodDrinker);
|
||||
Assert.Equal(0x00000010, (int)ExtendedWeaponAttribute.HitSwarm);
|
||||
Assert.Equal(0x00000020, (int)ExtendedWeaponAttribute.SplinteringWeapon);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -43,6 +44,7 @@ public class ExtendedWeaponAttributesTests
|
|||
Assert.Equal(0, weapon.ExtendedWeaponAttributes.HitSparks);
|
||||
Assert.Equal(0, weapon.ExtendedWeaponAttributes.BloodDrinker);
|
||||
Assert.Equal(0, weapon.ExtendedWeaponAttributes.HitSwarm);
|
||||
Assert.Equal(0, weapon.ExtendedWeaponAttributes.SplinteringWeapon);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -79,6 +81,7 @@ public class ExtendedWeaponAttributesTests
|
|||
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.HitSparks));
|
||||
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.BloodDrinker));
|
||||
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.HitSwarm));
|
||||
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.SplinteringWeapon));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -96,6 +99,7 @@ public class ExtendedWeaponAttributesTests
|
|||
weapon.ExtendedWeaponAttributes.HitSparks = 20;
|
||||
weapon.ExtendedWeaponAttributes.BloodDrinker = 1;
|
||||
weapon.ExtendedWeaponAttributes.HitSwarm = 30;
|
||||
weapon.ExtendedWeaponAttributes.SplinteringWeapon = 25;
|
||||
|
||||
var writer = new BufferWriter(true);
|
||||
weapon.Serialize(writer);
|
||||
|
|
@ -111,6 +115,7 @@ public class ExtendedWeaponAttributesTests
|
|||
Assert.Equal(20, deserialized.ExtendedWeaponAttributes.HitSparks);
|
||||
Assert.Equal(1, deserialized.ExtendedWeaponAttributes.BloodDrinker);
|
||||
Assert.Equal(30, deserialized.ExtendedWeaponAttributes.HitSwarm);
|
||||
Assert.Equal(25, deserialized.ExtendedWeaponAttributes.SplinteringWeapon);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,474 @@
|
|||
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 SplinteringWeaponPropertyTests
|
||||
{
|
||||
private const int SplinteringWeaponCliloc = 1112857;
|
||||
private static readonly Point3D TestLocation = new(6200, 540, 0);
|
||||
|
||||
[Fact]
|
||||
public void ExtendedWeaponAttributes_StoresAndDupesSplinteringWeapon()
|
||||
{
|
||||
var weapon = new TestKatana();
|
||||
var dupe = new TestKatana();
|
||||
|
||||
try
|
||||
{
|
||||
weapon.ExtendedWeaponAttributes.SplinteringWeapon = 30;
|
||||
|
||||
weapon.Dupe(dupe);
|
||||
|
||||
Assert.Equal(30, weapon.ExtendedWeaponAttributes.SplinteringWeapon);
|
||||
Assert.Equal(30, dupe.ExtendedWeaponAttributes.SplinteringWeapon);
|
||||
}
|
||||
finally
|
||||
{
|
||||
weapon.Delete();
|
||||
dupe.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtendedWeaponAttributes_GetProperties_GatesSplinteringWeaponTooltipToStygianAbyss()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestKatana();
|
||||
|
||||
try
|
||||
{
|
||||
weapon.ExtendedWeaponAttributes.SplinteringWeapon = 30;
|
||||
|
||||
Core.Expansion = Expansion.ML;
|
||||
var preStygianAbyss = new RecordingPropertyList();
|
||||
weapon.ExtendedWeaponAttributes.GetProperties(preStygianAbyss);
|
||||
Assert.DoesNotContain(preStygianAbyss.Entries, entry => entry.Number == SplinteringWeaponCliloc);
|
||||
|
||||
Core.Expansion = Expansion.SA;
|
||||
var stygianAbyss = new RecordingPropertyList();
|
||||
weapon.ExtendedWeaponAttributes.GetProperties(stygianAbyss);
|
||||
Assert.Contains(
|
||||
stygianAbyss.Entries,
|
||||
entry => entry.Number == SplinteringWeaponCliloc && entry.Argument == "30"
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
weapon.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnHit_PreStygianAbyss_DoesNotStartSplinteringContext()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = EquipSplinteringWeapon(CreateMobile());
|
||||
var attacker = (Mobile)weapon.Parent;
|
||||
var defender = CreateMobile(location: new Point3D(6201, 540, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.ML;
|
||||
|
||||
weapon.OnHit(attacker, defender);
|
||||
|
||||
Assert.Equal(0, SplinteringWeapon.ActiveContextCount);
|
||||
Assert.False(SplinteringWeapon.IsForceWalking(defender));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
SplinteringWeapon.ClearAll();
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnHit_EligibleSuccessfulHit_StartsBleedForceWalkAndDamagesDurability()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = EquipSplinteringWeapon(CreateMobile());
|
||||
var attacker = (Mobile)weapon.Parent;
|
||||
var defender = CreateMobile(location: new Point3D(6201, 540, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
weapon.HitPoints = 50;
|
||||
weapon.MaxHitPoints = 50;
|
||||
var defenderHits = defender.Hits;
|
||||
|
||||
weapon.OnHit(attacker, defender);
|
||||
|
||||
Assert.True(weapon.HitPoints <= 40);
|
||||
Assert.True(SplinteringWeapon.IsForceWalking(defender));
|
||||
Assert.Equal(1, SplinteringWeapon.ActiveContextCount);
|
||||
|
||||
Assert.True(SplinteringWeapon.TickForTests(attacker, defender));
|
||||
|
||||
Assert.True(defender.Hits < defenderHits);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
SplinteringWeapon.ClearAll();
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForceWalk_EndsAfterFourSecondsWhileSplinteringBleedCanContinue()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = EquipSplinteringWeapon(CreateMobile());
|
||||
var attacker = (Mobile)weapon.Parent;
|
||||
var defender = CreateMobile(location: new Point3D(6201, 540, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
|
||||
weapon.OnHit(attacker, defender);
|
||||
Assert.True(SplinteringWeapon.IsForceWalking(defender));
|
||||
|
||||
Assert.True(SplinteringWeapon.EndForceWalkForTests(attacker, defender));
|
||||
|
||||
Assert.False(SplinteringWeapon.IsForceWalking(defender));
|
||||
Assert.Equal(1, SplinteringWeapon.ActiveContextCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
SplinteringWeapon.ClearAll();
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForceWalk_BlocksRunningMovementUntilEffectEnds()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = EquipSplinteringWeapon(CreateMobile());
|
||||
var attacker = (Mobile)weapon.Parent;
|
||||
var defender = CreateMobile(location: new Point3D(6201, 540, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
|
||||
weapon.OnHit(attacker, defender);
|
||||
var run = MovementEventArgs.Create(defender, Direction.Running);
|
||||
SplinteringWeapon.OnMovement(run);
|
||||
Assert.True(run.Blocked);
|
||||
run.Free();
|
||||
|
||||
var walk = MovementEventArgs.Create(defender, Direction.North);
|
||||
SplinteringWeapon.OnMovement(walk);
|
||||
Assert.False(walk.Blocked);
|
||||
walk.Free();
|
||||
|
||||
Assert.True(SplinteringWeapon.EndForceWalkForTests(attacker, defender));
|
||||
var afterEffect = MovementEventArgs.Create(defender, Direction.Running);
|
||||
SplinteringWeapon.OnMovement(afterEffect);
|
||||
Assert.False(afterEffect.Blocked);
|
||||
afterEffect.Free();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
SplinteringWeapon.ClearAll();
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogoutClearsSplinteringEffectsAndPlayerImmunity()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestKatana();
|
||||
var attacker = CreateMobile();
|
||||
var defender = CreatePlayerMobile(location: new Point3D(6201, 540, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
Assert.True(SplinteringWeapon.TryProcOnEligibleHit(attacker, defender, weapon, null, 100));
|
||||
Assert.True(SplinteringWeapon.IsForceWalking(defender));
|
||||
Assert.True(SplinteringWeapon.IsPlayerImmune(defender));
|
||||
|
||||
SplinteringWeapon.Clear(defender);
|
||||
|
||||
Assert.False(SplinteringWeapon.IsForceWalking(defender));
|
||||
Assert.False(SplinteringWeapon.IsPlayerImmune(defender));
|
||||
Assert.Equal(0, SplinteringWeapon.ActiveContextCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
SplinteringWeapon.ClearAll();
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlayerVictim_ReceivesFifteenSecondImmunityToRepeatedSplinteringEffects()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestKatana();
|
||||
var attacker = CreateMobile();
|
||||
var defender = CreatePlayerMobile(location: new Point3D(6201, 540, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
|
||||
Assert.True(SplinteringWeapon.TryProcOnEligibleHit(attacker, defender, weapon, null, 100));
|
||||
Assert.True(SplinteringWeapon.IsPlayerImmune(defender));
|
||||
|
||||
Assert.True(SplinteringWeapon.EndForceWalkForTests(attacker, defender));
|
||||
Assert.False(SplinteringWeapon.IsForceWalking(defender));
|
||||
|
||||
Assert.False(SplinteringWeapon.TryProcOnEligibleHit(attacker, defender, weapon, null, 100));
|
||||
|
||||
Assert.False(SplinteringWeapon.IsForceWalking(defender));
|
||||
|
||||
SplinteringWeapon.ExpirePlayerImmunityForTests(defender);
|
||||
Assert.False(SplinteringWeapon.IsPlayerImmune(defender));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
SplinteringWeapon.ClearAll();
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(5)]
|
||||
[InlineData(8)]
|
||||
public void NamedExcludedWeaponAbilities_DoNotTriggerSplinteringWeapon(int abilityIndex)
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestKatana();
|
||||
var attacker = CreateMobile();
|
||||
var defender = CreateMobile(location: new Point3D(6201, 540, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
var triggered = SplinteringWeapon.TryProcOnEligibleHit(
|
||||
attacker,
|
||||
defender,
|
||||
weapon,
|
||||
WeaponAbility.Abilities[abilityIndex],
|
||||
100
|
||||
);
|
||||
|
||||
Assert.False(triggered);
|
||||
Assert.False(SplinteringWeapon.IsForceWalking(defender));
|
||||
Assert.Equal(0, SplinteringWeapon.ActiveContextCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
SplinteringWeapon.ClearAll();
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NonExcludedWeaponAbility_CanTriggerSplinteringWeapon()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestKatana();
|
||||
var attacker = CreateMobile();
|
||||
var defender = CreateMobile(location: new Point3D(6201, 540, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
var triggered = SplinteringWeapon.TryProcOnEligibleHit(
|
||||
attacker,
|
||||
defender,
|
||||
weapon,
|
||||
WeaponAbility.ArmorIgnore,
|
||||
100
|
||||
);
|
||||
|
||||
Assert.True(triggered);
|
||||
Assert.True(SplinteringWeapon.IsForceWalking(defender));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
SplinteringWeapon.ClearAll();
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplinteringBleed_DoesNotReplaceRegularBleedAttackContext()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestKatana();
|
||||
var attacker = CreateMobile();
|
||||
var defender = CreateMobile(location: new Point3D(6201, 540, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.SA;
|
||||
BleedAttack.BeginBleed(defender, attacker);
|
||||
Assert.True(BleedAttack.IsBleeding(defender));
|
||||
|
||||
var triggered = SplinteringWeapon.TryProcOnEligibleHit(attacker, defender, weapon, null, 100);
|
||||
|
||||
Assert.True(triggered);
|
||||
Assert.True(BleedAttack.IsBleeding(defender));
|
||||
Assert.True(SplinteringWeapon.IsForceWalking(defender));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
BleedAttack.EndBleed(defender, false);
|
||||
SplinteringWeapon.ClearAll();
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RunicAttributeGeneration_DoesNotRollSplinteringWeapon()
|
||||
{
|
||||
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.ExtendedWeaponAttributes.SplinteringWeapon);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
weapon.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
private static TestKatana EquipSplinteringWeapon(Mobile wielder)
|
||||
{
|
||||
var weapon = new TestKatana();
|
||||
weapon.ExtendedWeaponAttributes.SplinteringWeapon = 100;
|
||||
wielder.AddItem(weapon);
|
||||
return weapon;
|
||||
}
|
||||
|
||||
private static Mobile CreateMobile(bool player = false, Point3D location = default)
|
||||
{
|
||||
var mobile = new Mobile(World.NewMobile)
|
||||
{
|
||||
Player = player
|
||||
};
|
||||
|
||||
mobile.DefaultMobileInit();
|
||||
InitializeHits(mobile, 500, 500);
|
||||
mobile.MoveToWorld(location == default ? TestLocation : location, Map.Felucca);
|
||||
return mobile;
|
||||
}
|
||||
|
||||
private static PlayerMobile CreatePlayerMobile(Point3D location)
|
||||
{
|
||||
var mobile = new PlayerMobile(World.NewMobile);
|
||||
|
||||
mobile.DefaultMobileInit();
|
||||
mobile.Player = true;
|
||||
mobile.Hits = mobile.HitsMax;
|
||||
mobile.MoveToWorld(location, Map.Felucca);
|
||||
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 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<(int Number, string Argument)> Entries { get; } = [];
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
}
|
||||
|
||||
public void Terminate()
|
||||
{
|
||||
}
|
||||
|
||||
public void Add(int number) => Entries.Add((number, string.Empty));
|
||||
public void Add(int number, string argument) => Entries.Add((number, argument));
|
||||
public void Add(ReadOnlySpan<char> argument) => Entries.Add((0, argument.ToString()));
|
||||
public void Add(int number, ReadOnlySpan<char> argument) => Entries.Add((number, argument.ToString()));
|
||||
public void AddChunked(ReadOnlySpan<char> text) => Entries.Add((0, text.ToString()));
|
||||
public OplTextBlock TextBlock() => new(this);
|
||||
public void Add(int number, int value) => Entries.Add((number, value.ToString()));
|
||||
public void AddLocalized(int value) => Entries.Add((0, value.ToString()));
|
||||
public void AddLocalized(int number, int value) => Entries.Add((number, value.ToString()));
|
||||
public void Add(ref IPropertyList.InterpolatedStringHandler handler) => Entries.Add((0, _interpolated));
|
||||
public void Add(int number, ref IPropertyList.InterpolatedStringHandler handler) =>
|
||||
Entries.Add((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;
|
||||
}
|
||||
}
|
||||
|
|
@ -2087,6 +2087,13 @@ public abstract partial class BaseWeapon
|
|||
if (damageGiven > 0)
|
||||
{
|
||||
var propertyBonus = move?.GetPropertyBonus(attacker) ?? 1.0;
|
||||
var splintering = SplinteringWeapon.TryProcOnEligibleHit(
|
||||
attacker,
|
||||
defender,
|
||||
this,
|
||||
a,
|
||||
ExtendedWeaponAttributes.SplinteringWeapon
|
||||
);
|
||||
|
||||
// Leech abilities
|
||||
if (Core.AOS)
|
||||
|
|
@ -2144,14 +2151,18 @@ public abstract partial class BaseWeapon
|
|||
defender is Slime or AcidElemental;
|
||||
|
||||
// Stratics says 50% chance, seems more like 4%..
|
||||
if (isAcidMonster || Utility.Random(25) == 0)
|
||||
if (isAcidMonster || splintering || Utility.Random(25) == 0)
|
||||
{
|
||||
if (isAcidMonster)
|
||||
{
|
||||
attacker.LocalOverheadMessage(MessageType.Regular, 0x3B2, 500263); // *Acid blood scars your weapon!*
|
||||
}
|
||||
|
||||
if (Core.AOS && WeaponAttributes.SelfRepair > Utility.Random(10))
|
||||
if (splintering && _hitPoints > 0)
|
||||
{
|
||||
HitPoints = Math.Max(0, _hitPoints - SplinteringWeapon.DurabilityLoss);
|
||||
}
|
||||
else if (Core.AOS && WeaponAttributes.SelfRepair > Utility.Random(10))
|
||||
{
|
||||
HitPoints += 2;
|
||||
}
|
||||
|
|
|
|||
297
Projects/UOContent/Items/Weapons/SplinteringWeapon.cs
Normal file
297
Projects/UOContent/Items/Weapons/SplinteringWeapon.cs
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ModernUO.CodeGeneratedEvents;
|
||||
using Server.Collections;
|
||||
using Server.Engines.BuffIcons;
|
||||
using Server.Mobiles;
|
||||
using Server.Network;
|
||||
|
||||
namespace Server.Items;
|
||||
|
||||
public static class SplinteringWeapon
|
||||
{
|
||||
internal const int DurabilityLoss = 10;
|
||||
internal const int TickCount = 5;
|
||||
|
||||
internal static readonly TimeSpan ForcedWalkDuration = TimeSpan.FromSeconds(4.0);
|
||||
internal static readonly TimeSpan BleedTickInterval = TimeSpan.FromSeconds(2.0);
|
||||
internal static readonly TimeSpan PlayerImmunityDuration = TimeSpan.FromSeconds(15.0);
|
||||
|
||||
private static readonly Dictionary<(Mobile Attacker, Mobile Defender), SplinteringContext> _contexts = [];
|
||||
private static readonly Dictionary<Mobile, TimerExecutionToken> _playerImmunity = [];
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
EventSink.Logout += Clear;
|
||||
EventSink.Movement += OnMovement;
|
||||
}
|
||||
|
||||
public static bool TryProcOnEligibleHit(
|
||||
Mobile attacker,
|
||||
Mobile defender,
|
||||
BaseWeapon weapon,
|
||||
WeaponAbility ability,
|
||||
int splinteringChance
|
||||
)
|
||||
{
|
||||
if (!Core.SA || splinteringChance <= 0 || weapon == null || attacker == null || defender == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsValidCombatant(attacker) || !IsValidCombatant(defender))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsExcludedAbility(ability) || IsPlayerImmune(defender))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (splinteringChance <= Utility.Random(100))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var key = (attacker, defender);
|
||||
if (_contexts.ContainsKey(key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_contexts[key] = new SplinteringContext(attacker, defender);
|
||||
|
||||
if (defender is PlayerMobile)
|
||||
{
|
||||
AddPlayerImmunity(defender);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool IsForceWalking(Mobile defender)
|
||||
{
|
||||
if (defender == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var context in _contexts.Values)
|
||||
{
|
||||
if (context.Defender == defender && context.ForceWalking)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool IsPlayerImmune(Mobile defender) =>
|
||||
defender is PlayerMobile && _playerImmunity.ContainsKey(defender);
|
||||
|
||||
internal static int ActiveContextCount => _contexts.Count;
|
||||
|
||||
internal static bool EndForceWalkForTests(Mobile attacker, Mobile defender)
|
||||
{
|
||||
if (attacker == null || defender == null || !_contexts.TryGetValue((attacker, defender), out var context))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
context.EndForceWalk();
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool TickForTests(Mobile attacker, Mobile defender)
|
||||
{
|
||||
if (attacker == null || defender == null || !_contexts.TryGetValue((attacker, defender), out var context))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
context.OnBleedTick();
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void ExpirePlayerImmunityForTests(Mobile mobile) => RemovePlayerImmunity(mobile);
|
||||
|
||||
internal static void ClearAll()
|
||||
{
|
||||
foreach (var context in _contexts.Values)
|
||||
{
|
||||
context.Stop();
|
||||
}
|
||||
|
||||
_contexts.Clear();
|
||||
|
||||
foreach (var token in _playerImmunity.Values)
|
||||
{
|
||||
token.Cancel();
|
||||
}
|
||||
|
||||
_playerImmunity.Clear();
|
||||
}
|
||||
|
||||
internal static void ClearEffects(Mobile mobile)
|
||||
{
|
||||
if (mobile == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var pairs = PooledRefList<(Mobile Attacker, Mobile Defender)>.Create();
|
||||
|
||||
foreach (var pair in _contexts.Keys)
|
||||
{
|
||||
if (pair.Attacker == mobile || pair.Defender == mobile)
|
||||
{
|
||||
pairs.Add(pair);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < pairs.Count; i++)
|
||||
{
|
||||
StopContext(pairs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
[OnEvent(nameof(PlayerMobile.PlayerDeathEvent))]
|
||||
[OnEvent(nameof(PlayerMobile.PlayerDeletedEvent))]
|
||||
[OnEvent(nameof(BaseCreature.CreatureDeathEvent))]
|
||||
[OnEvent(nameof(BaseCreature.CreatureDeletedEvent))]
|
||||
public static void Clear(Mobile mobile)
|
||||
{
|
||||
ClearEffects(mobile);
|
||||
RemovePlayerImmunity(mobile);
|
||||
}
|
||||
|
||||
internal static void OnMovement(MovementEventArgs e)
|
||||
{
|
||||
if ((e.Direction & Direction.Running) != 0 && IsForceWalking(e.Mobile) && e.Mobile.AccessLevel < AccessLevel.GameMaster)
|
||||
{
|
||||
e.Blocked = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsExcludedAbility(WeaponAbility ability) => ability is Disarm or InfectiousStrike;
|
||||
|
||||
private static bool IsValidCombatant(Mobile mobile) =>
|
||||
mobile is { Deleted: false, Alive: true } && mobile.Map != null && mobile.Map != Map.Internal;
|
||||
|
||||
private static void AddPlayerImmunity(Mobile defender)
|
||||
{
|
||||
if (defender is not PlayerMobile || _playerImmunity.ContainsKey(defender))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Timer.StartTimer(PlayerImmunityDuration, () => RemovePlayerImmunity(defender), out var token);
|
||||
_playerImmunity[defender] = token;
|
||||
}
|
||||
|
||||
private static void RemovePlayerImmunity(Mobile mobile)
|
||||
{
|
||||
if (mobile != null && _playerImmunity.Remove(mobile, out var token))
|
||||
{
|
||||
token.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private static void StopContext((Mobile Attacker, Mobile Defender) key)
|
||||
{
|
||||
if (_contexts.Remove(key, out var context))
|
||||
{
|
||||
context.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SplinteringContext
|
||||
{
|
||||
private readonly Mobile _attacker;
|
||||
private readonly Mobile _defender;
|
||||
private TimerExecutionToken _forcedWalkTimerToken;
|
||||
private TimerExecutionToken _bleedTimerToken;
|
||||
private int _bleedLevel = TickCount;
|
||||
|
||||
public SplinteringContext(Mobile attacker, Mobile defender)
|
||||
{
|
||||
_attacker = attacker;
|
||||
_defender = defender;
|
||||
ForceWalking = true;
|
||||
|
||||
StartForceWalk();
|
||||
|
||||
defender.SendLocalizedMessage(1112486); // A shard of the brittle weapon has become lodged in you!
|
||||
attacker.SendLocalizedMessage(1113077); // A shard of your blade breaks off and sticks in your opponent!
|
||||
Effects.PlaySound(defender.Location, defender.Map, 0x1DF);
|
||||
|
||||
if (defender is PlayerMobile pm && BuffInfo.Enabled)
|
||||
{
|
||||
pm.AddBuff(new BuffInfo(BuffIcon.Unknown2, 1154670, 1152144, ForcedWalkDuration));
|
||||
}
|
||||
|
||||
Timer.StartTimer(ForcedWalkDuration, EndForceWalk, out _forcedWalkTimerToken);
|
||||
Timer.StartTimer(BleedTickInterval, BleedTickInterval, OnBleedTick, out _bleedTimerToken);
|
||||
}
|
||||
|
||||
public Mobile Defender => _defender;
|
||||
|
||||
public bool ForceWalking { get; private set; }
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_forcedWalkTimerToken.Cancel();
|
||||
_bleedTimerToken.Cancel();
|
||||
EndForceWalk();
|
||||
}
|
||||
|
||||
private void StartForceWalk()
|
||||
{
|
||||
if (_defender.NetState != null && _defender.AccessLevel < AccessLevel.GameMaster)
|
||||
{
|
||||
_defender.NetState.SendSpeedControl(SpeedControlSetting.Walk);
|
||||
}
|
||||
}
|
||||
|
||||
public void EndForceWalk()
|
||||
{
|
||||
if (!ForceWalking)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ForceWalking = false;
|
||||
|
||||
if (_defender.NetState != null)
|
||||
{
|
||||
_defender.NetState.SendSpeedControl(SpeedControlSetting.Disable);
|
||||
}
|
||||
|
||||
if (_defender is PlayerMobile pm && BuffInfo.Enabled)
|
||||
{
|
||||
pm.RemoveBuff(BuffIcon.Unknown2);
|
||||
}
|
||||
|
||||
_defender.SendLocalizedMessage(1112487); // The shard is successfully removed.
|
||||
}
|
||||
|
||||
public void OnBleedTick()
|
||||
{
|
||||
if (!IsValidCombatant(_attacker) || !IsValidCombatant(_defender))
|
||||
{
|
||||
StopContext((_attacker, _defender));
|
||||
return;
|
||||
}
|
||||
|
||||
BleedAttack.DoBleed(_defender, _attacker, _bleedLevel, bloodDrinker: false);
|
||||
_bleedLevel--;
|
||||
|
||||
if (_bleedLevel <= 0)
|
||||
{
|
||||
StopContext((_attacker, _defender));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1132,7 +1132,8 @@ namespace Server
|
|||
BattleLust = 0x00000002,
|
||||
HitSparks = 0x00000004,
|
||||
BloodDrinker = 0x00000008,
|
||||
HitSwarm = 0x00000010
|
||||
HitSwarm = 0x00000010,
|
||||
SplinteringWeapon = 0x00000020
|
||||
}
|
||||
|
||||
public sealed class ExtendedWeaponAttributes : BaseAttributes
|
||||
|
|
@ -1196,6 +1197,13 @@ namespace Server
|
|||
set => this[ExtendedWeaponAttribute.HitSwarm] = value;
|
||||
}
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public int SplinteringWeapon
|
||||
{
|
||||
get => this[ExtendedWeaponAttribute.SplinteringWeapon];
|
||||
set => this[ExtendedWeaponAttribute.SplinteringWeapon] = value;
|
||||
}
|
||||
|
||||
public void GetProperties(IPropertyList list)
|
||||
{
|
||||
if (Core.HS && Bane != 0)
|
||||
|
|
@ -1213,6 +1221,11 @@ namespace Server
|
|||
list.Add(1113591); // Blood Drinker
|
||||
}
|
||||
|
||||
if (Core.SA && SplinteringWeapon != 0)
|
||||
{
|
||||
list.Add(1112857, SplinteringWeapon); // splintering weapon ~1_val~%
|
||||
}
|
||||
|
||||
if (Core.TOL && HitSparks != 0)
|
||||
{
|
||||
list.Add(1157326, HitSparks); // Sparks ~1_val~%
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue