feat(items): add Swarm weapon property
This commit is contained in:
parent
81231bedb3
commit
407fdd59b7
6 changed files with 790 additions and 3 deletions
|
|
@ -26,6 +26,7 @@ public class ExtendedWeaponAttributesTests
|
|||
Assert.Equal(0x00000002, (int)ExtendedWeaponAttribute.BattleLust);
|
||||
Assert.Equal(0x00000004, (int)ExtendedWeaponAttribute.HitSparks);
|
||||
Assert.Equal(0x00000008, (int)ExtendedWeaponAttribute.BloodDrinker);
|
||||
Assert.Equal(0x00000010, (int)ExtendedWeaponAttribute.HitSwarm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -41,6 +42,7 @@ public class ExtendedWeaponAttributesTests
|
|||
Assert.Equal(0, weapon.ExtendedWeaponAttributes.BattleLust);
|
||||
Assert.Equal(0, weapon.ExtendedWeaponAttributes.HitSparks);
|
||||
Assert.Equal(0, weapon.ExtendedWeaponAttributes.BloodDrinker);
|
||||
Assert.Equal(0, weapon.ExtendedWeaponAttributes.HitSwarm);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -76,6 +78,7 @@ public class ExtendedWeaponAttributesTests
|
|||
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.BattleLust));
|
||||
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.HitSparks));
|
||||
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.BloodDrinker));
|
||||
AssertStaffCommandProperty(nameof(ExtendedWeaponAttributes.HitSwarm));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -92,6 +95,7 @@ public class ExtendedWeaponAttributesTests
|
|||
weapon.ExtendedWeaponAttributes.BattleLust = 1;
|
||||
weapon.ExtendedWeaponAttributes.HitSparks = 20;
|
||||
weapon.ExtendedWeaponAttributes.BloodDrinker = 1;
|
||||
weapon.ExtendedWeaponAttributes.HitSwarm = 30;
|
||||
|
||||
var writer = new BufferWriter(true);
|
||||
weapon.Serialize(writer);
|
||||
|
|
@ -106,6 +110,7 @@ public class ExtendedWeaponAttributesTests
|
|||
Assert.Equal(1, deserialized.ExtendedWeaponAttributes.BattleLust);
|
||||
Assert.Equal(20, deserialized.ExtendedWeaponAttributes.HitSparks);
|
||||
Assert.Equal(1, deserialized.ExtendedWeaponAttributes.BloodDrinker);
|
||||
Assert.Equal(30, deserialized.ExtendedWeaponAttributes.HitSwarm);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,558 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Server;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
using Server.Spells;
|
||||
using Server.Spells.Bushido;
|
||||
using Server.Spells.Ninjitsu;
|
||||
using Server.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class SwarmPropertyTests
|
||||
{
|
||||
private const int SwarmCliloc = 1157325;
|
||||
private static readonly Point3D TestLocation = new(6200, 520, 0);
|
||||
|
||||
[Fact]
|
||||
public void ExtendedWeaponAttributes_StoresAndDupesSwarm()
|
||||
{
|
||||
var weapon = new TestKatana();
|
||||
var dupe = new TestKatana();
|
||||
|
||||
try
|
||||
{
|
||||
weapon.ExtendedWeaponAttributes.HitSwarm = 30;
|
||||
|
||||
weapon.Dupe(dupe);
|
||||
|
||||
Assert.Equal(30, weapon.ExtendedWeaponAttributes.HitSwarm);
|
||||
Assert.Equal(30, dupe.ExtendedWeaponAttributes.HitSwarm);
|
||||
}
|
||||
finally
|
||||
{
|
||||
weapon.Delete();
|
||||
dupe.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtendedWeaponAttributes_GetProperties_GatesSwarmTooltipToTimeOfLegends()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestKatana();
|
||||
|
||||
try
|
||||
{
|
||||
weapon.ExtendedWeaponAttributes.HitSwarm = 30;
|
||||
|
||||
Core.Expansion = Expansion.HS;
|
||||
var preTimeOfLegends = new RecordingPropertyList();
|
||||
weapon.ExtendedWeaponAttributes.GetProperties(preTimeOfLegends);
|
||||
Assert.DoesNotContain(preTimeOfLegends.Entries, entry => entry.Number == SwarmCliloc);
|
||||
|
||||
Core.Expansion = Expansion.TOL;
|
||||
var timeOfLegends = new RecordingPropertyList();
|
||||
weapon.ExtendedWeaponAttributes.GetProperties(timeOfLegends);
|
||||
Assert.Contains(
|
||||
timeOfLegends.Entries,
|
||||
entry => entry.Number == SwarmCliloc && entry.Argument == "30"
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
weapon.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnHit_PreTimeOfLegends_DoesNotStartSwarmContext()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestKatana();
|
||||
var attacker = CreateMobile();
|
||||
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.HS;
|
||||
weapon.ExtendedWeaponAttributes.HitSwarm = 100;
|
||||
|
||||
weapon.OnHit(attacker, defender);
|
||||
|
||||
Assert.False(Swarm.HasActiveContext(attacker, defender));
|
||||
Assert.Equal(0, Swarm.ActiveContextCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
Swarm.ClearAll();
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnHit_NormalSuccessfulHit_StartsOneSwarmContextAndRejectsDuplicateDefenderProc()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestKatana();
|
||||
var attacker = CreateMobile();
|
||||
var secondAttacker = CreateMobile(location: new Point3D(6199, 520, 0));
|
||||
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.TOL;
|
||||
weapon.ExtendedWeaponAttributes.HitSwarm = 100;
|
||||
|
||||
weapon.OnHit(attacker, defender);
|
||||
|
||||
Assert.True(Swarm.HasActiveContext(attacker, defender));
|
||||
Assert.True(Swarm.HasActiveContext(defender));
|
||||
Assert.Equal(1, Swarm.ActiveContextCount);
|
||||
Assert.Equal(Swarm.TickCount, Swarm.GetTicksRemainingForTests(attacker, defender));
|
||||
|
||||
Swarm.TryProcOnNormalHit(secondAttacker, defender, 100);
|
||||
|
||||
Assert.True(Swarm.HasActiveContext(attacker, defender));
|
||||
Assert.False(Swarm.HasActiveContext(secondAttacker, defender));
|
||||
Assert.Equal(1, Swarm.ActiveContextCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
Swarm.ClearAll();
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
secondAttacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnHit_CurrentWeaponAbility_DoesNotStartSwarmContext()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestKatana();
|
||||
var attacker = CreateMobile();
|
||||
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.TOL;
|
||||
weapon.ExtendedWeaponAttributes.HitSwarm = 100;
|
||||
WeaponAbility.Table[attacker] = new TestWeaponAbility();
|
||||
|
||||
weapon.OnHit(attacker, defender);
|
||||
|
||||
Assert.False(Swarm.HasActiveContext(attacker, defender));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
WeaponAbility.Table.Remove(attacker);
|
||||
Swarm.ClearAll();
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(TestSpecialMove))]
|
||||
[InlineData(typeof(LightningStrike))]
|
||||
[InlineData(typeof(DeathStrike))]
|
||||
public void OnHit_SpecialMoves_DoNotStartSwarmContext(Type moveType)
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestKatana();
|
||||
var attacker = CreateMobile();
|
||||
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.TOL;
|
||||
weapon.ExtendedWeaponAttributes.HitSwarm = 100;
|
||||
SpecialMove.Table[attacker] = (SpecialMove)Activator.CreateInstance(moveType);
|
||||
|
||||
weapon.OnHit(attacker, defender);
|
||||
|
||||
Assert.False(Swarm.HasActiveContext(attacker, defender));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
SpecialMove.Table.Remove(attacker);
|
||||
Swarm.ClearAll();
|
||||
weapon.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySwarmTick_UsesPhysicalResistanceAndReturnsAppliedDamage()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var attacker = CreateMobile();
|
||||
var defender = CreateMobile(hitsMax: 200, hits: 200, location: new Point3D(6201, 520, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.TOL;
|
||||
AddResistance(defender, ResistanceType.Physical, 70);
|
||||
|
||||
var damageApplied = Swarm.ApplySwarmTick(attacker, defender, Swarm.RawDamage);
|
||||
|
||||
Assert.Equal(3, damageApplied);
|
||||
Assert.Equal(197, defender.Hits);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
Swarm.ClearAll();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SwarmContext_TicksThreeTimesAtFiveSecondIntervalAndThenCleansUp()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var attacker = CreateMobile();
|
||||
var defender = CreateMobile(hitsMax: 200, hits: 200, location: new Point3D(6201, 520, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.TOL;
|
||||
|
||||
Swarm.TryProcOnNormalHit(attacker, defender, 100);
|
||||
|
||||
Assert.True(Swarm.HasActiveContext(attacker, defender));
|
||||
Assert.Equal(TimeSpan.FromSeconds(5.0), Swarm.TickInterval);
|
||||
Assert.Equal(TimeSpan.FromSeconds(15.0), Swarm.EffectDuration);
|
||||
|
||||
for (var i = 0; i < Swarm.TickCount; i++)
|
||||
{
|
||||
Assert.True(Swarm.TickForTests(attacker, defender));
|
||||
}
|
||||
|
||||
Assert.False(Swarm.HasActiveContext(attacker, defender));
|
||||
Assert.Equal(170, defender.Hits);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
Swarm.ClearAll();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PositivePostResistFireDamageClearsSwarmContext()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var attacker = CreateMobile();
|
||||
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.TOL;
|
||||
Swarm.TryProcOnNormalHit(attacker, defender, 100);
|
||||
|
||||
AOS.Damage(defender, attacker, 10, false, 0, 100, 0, 0, 0);
|
||||
|
||||
Assert.False(Swarm.HasActiveContext(attacker, defender));
|
||||
Assert.Equal(0, Swarm.ActiveContextCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
Swarm.ClearAll();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullyResistedFireDamageDoesNotClearSwarmContext()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var attacker = CreateMobile();
|
||||
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.TOL;
|
||||
AddResistance(defender, ResistanceType.Fire, 100);
|
||||
Swarm.TryProcOnNormalHit(attacker, defender, 100);
|
||||
|
||||
AOS.Damage(defender, attacker, 10, false, 0, 100, 0, 0, 0);
|
||||
|
||||
Assert.True(Swarm.HasActiveContext(attacker, defender));
|
||||
Assert.Equal(1, Swarm.ActiveContextCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
Swarm.ClearAll();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BurningTorchEquipClearsSwarmContextImmediately()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var attacker = CreateMobile();
|
||||
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
|
||||
var torch = new Torch();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.TOL;
|
||||
Swarm.TryProcOnNormalHit(attacker, defender, 100);
|
||||
torch.Burning = true;
|
||||
|
||||
Assert.True(defender.EquipItem(torch));
|
||||
|
||||
Assert.False(Swarm.HasActiveContext(attacker, defender));
|
||||
Assert.Equal(0, Swarm.ActiveContextCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
Swarm.ClearAll();
|
||||
torch.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BurningTorchIgniteClearsSwarmContextImmediately()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var attacker = CreateMobile();
|
||||
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
|
||||
var torch = new Torch();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.TOL;
|
||||
Swarm.TryProcOnNormalHit(attacker, defender, 100);
|
||||
Assert.True(defender.EquipItem(torch));
|
||||
|
||||
torch.Ignite();
|
||||
|
||||
Assert.False(Swarm.HasActiveContext(attacker, defender));
|
||||
Assert.Equal(0, Swarm.ActiveContextCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
Swarm.ClearAll();
|
||||
torch.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SwarmTickWithBurningTorchClearsContextWithoutDamage()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var attacker = CreateMobile();
|
||||
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
|
||||
var torch = new Torch();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.TOL;
|
||||
Assert.True(defender.EquipItem(torch));
|
||||
Swarm.TryProcOnNormalHit(attacker, defender, 100);
|
||||
torch.Burning = true;
|
||||
|
||||
var damageApplied = Swarm.ApplySwarmTick(attacker, defender, Swarm.RawDamage);
|
||||
|
||||
Assert.Equal(0, damageApplied);
|
||||
Assert.Equal(200, defender.Hits);
|
||||
Assert.False(Swarm.HasActiveContext(attacker, defender));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
Swarm.ClearAll();
|
||||
torch.Delete();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SwarmContext_TickCleansUpInternalizedCombatant()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var attacker = CreateMobile();
|
||||
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.TOL;
|
||||
|
||||
Swarm.TryProcOnNormalHit(attacker, defender, 100);
|
||||
defender.Internalize();
|
||||
|
||||
Assert.True(Swarm.TickForTests(attacker, defender));
|
||||
Assert.False(Swarm.HasActiveContext(attacker, defender));
|
||||
Assert.Equal(0, Swarm.ActiveContextCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
Swarm.ClearAll();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SwarmClear_RemovesContextsForDeletedOrDeadEventMobile()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var attacker = CreateMobile();
|
||||
var defender = CreateMobile(location: new Point3D(6201, 520, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.TOL;
|
||||
|
||||
Swarm.TryProcOnNormalHit(attacker, defender, 100);
|
||||
Swarm.Clear(defender);
|
||||
|
||||
Assert.False(Swarm.HasActiveContext(attacker, defender));
|
||||
Assert.Equal(0, Swarm.ActiveContextCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
Swarm.ClearAll();
|
||||
attacker.Delete();
|
||||
defender.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RunicAttributeGeneration_DoesNotRollSwarm()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var weapon = new TestKatana();
|
||||
|
||||
try
|
||||
{
|
||||
Core.Expansion = Expansion.TOL;
|
||||
|
||||
BaseRunicTool.ApplyAttributesTo(weapon, false, 0, 25, 100, 100);
|
||||
|
||||
Assert.Equal(0, weapon.ExtendedWeaponAttributes.HitSwarm);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
weapon.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
private static Mobile CreateMobile(
|
||||
int hitsMax = 200,
|
||||
int hits = 200,
|
||||
int manaMax = 100,
|
||||
int mana = 0,
|
||||
Point3D location = default
|
||||
)
|
||||
{
|
||||
var mobile = new Mobile(World.NewMobile);
|
||||
|
||||
mobile.DefaultMobileInit();
|
||||
mobile.RawStr = Math.Max(1, (hitsMax - 50) * 2);
|
||||
mobile.RawInt = manaMax;
|
||||
Assert.Equal(hitsMax, mobile.HitsMax);
|
||||
Assert.Equal(manaMax, mobile.ManaMax);
|
||||
mobile.Hits = hits;
|
||||
mobile.Mana = mana;
|
||||
mobile.MoveToWorld(location == default ? TestLocation : location, Map.Felucca);
|
||||
return mobile;
|
||||
}
|
||||
|
||||
private static void AddResistance(Mobile mobile, ResistanceType type, int offset) =>
|
||||
mobile.AddResistanceMod(new ResistanceMod(type, $"SwarmTest{type}{mobile.Serial}", offset, mobile));
|
||||
|
||||
private class TestKatana : Katana
|
||||
{
|
||||
public override bool CheckHit(Mobile attacker, Mobile defender) => true;
|
||||
public override int ComputeDamage(Mobile attacker, Mobile defender) => 1;
|
||||
public override int AbsorbDamage(Mobile attacker, Mobile defender, int damage) => damage;
|
||||
public override void AddBlood(Mobile attacker, Mobile defender, int damage)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private class TestWeaponAbility : WeaponAbility
|
||||
{
|
||||
}
|
||||
|
||||
private class TestSpecialMove : SpecialMove
|
||||
{
|
||||
}
|
||||
|
||||
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, null));
|
||||
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(CultureInfo.InvariantCulture)));
|
||||
public void AddLocalized(int value) => Entries.Add((0, value.ToString(CultureInfo.InvariantCulture)));
|
||||
public void AddLocalized(int number, int value) => Entries.Add((number, value.ToString(CultureInfo.InvariantCulture)));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ public partial class Torch : BaseEquipableLight
|
|||
if (parent is Mobile mobile && Burning)
|
||||
{
|
||||
MeerMage.StopEffect(mobile, true);
|
||||
Swarm.ClearDefender(mobile);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -42,6 +43,7 @@ public partial class Torch : BaseEquipableLight
|
|||
if (Parent is Mobile mobile && Burning)
|
||||
{
|
||||
MeerMage.StopEffect(mobile, true);
|
||||
Swarm.ClearDefender(mobile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2286,6 +2286,7 @@ public abstract partial class BaseWeapon
|
|||
if (a == null && move == null)
|
||||
{
|
||||
Sparks.TryProcOnNormalHit(attacker, defender, ExtendedWeaponAttributes.HitSparks);
|
||||
Swarm.TryProcOnNormalHit(attacker, defender, ExtendedWeaponAttributes.HitSwarm);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
196
Projects/UOContent/Items/Weapons/Swarm.cs
Normal file
196
Projects/UOContent/Items/Weapons/Swarm.cs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ModernUO.CodeGeneratedEvents;
|
||||
using Server.Collections;
|
||||
using Server.Engines.BuffIcons;
|
||||
using Server.Mobiles;
|
||||
|
||||
namespace Server.Items;
|
||||
|
||||
public static class Swarm
|
||||
{
|
||||
internal const int TickCount = 3;
|
||||
internal const int RawDamage = 10;
|
||||
|
||||
internal static readonly TimeSpan TickInterval = TimeSpan.FromSeconds(5.0);
|
||||
internal static readonly TimeSpan EffectDuration = TimeSpan.FromSeconds(15.0);
|
||||
|
||||
private static readonly Dictionary<Mobile, SwarmContext> _contexts = [];
|
||||
|
||||
public static void TryProcOnNormalHit(Mobile attacker, Mobile defender, int hitSwarmChance)
|
||||
{
|
||||
if (!Core.TOL || hitSwarmChance <= 0 || attacker == null || defender == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsValidCombatant(attacker) || !IsValidCombatant(defender))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (hitSwarmChance <= Utility.Random(100))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_contexts.ContainsKey(defender))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_contexts[defender] = new SwarmContext(attacker, defender);
|
||||
}
|
||||
|
||||
internal static int ApplySwarmTick(Mobile attacker, Mobile defender, int rawDamage)
|
||||
{
|
||||
if (!Core.TOL || !IsValidCombatant(attacker) || !IsValidCombatant(defender))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (HasBurningTorchEquipped(defender))
|
||||
{
|
||||
ClearDefender(defender);
|
||||
return 0;
|
||||
}
|
||||
|
||||
var defenderHits = defender.Hits;
|
||||
AOS.Damage(defender, attacker, rawDamage, false, 100, 0, 0, 0, 0);
|
||||
return Math.Max(0, defenderHits - defender.Hits);
|
||||
}
|
||||
|
||||
internal static bool HasActiveContext(Mobile attacker, Mobile defender) =>
|
||||
attacker != null && defender != null && _contexts.TryGetValue(defender, out var context) && context.Attacker == attacker;
|
||||
|
||||
internal static bool HasActiveContext(Mobile defender) => defender != null && _contexts.ContainsKey(defender);
|
||||
|
||||
internal static int ActiveContextCount => _contexts.Count;
|
||||
|
||||
internal static int GetTicksRemainingForTests(Mobile attacker, Mobile defender) =>
|
||||
HasActiveContext(attacker, defender) ? _contexts[defender].TicksRemaining : 0;
|
||||
|
||||
internal static bool TickForTests(Mobile attacker, Mobile defender)
|
||||
{
|
||||
if (!HasActiveContext(attacker, defender))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_contexts[defender].Tick();
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void ClearAll()
|
||||
{
|
||||
foreach (var context in _contexts.Values)
|
||||
{
|
||||
context.Stop();
|
||||
}
|
||||
|
||||
_contexts.Clear();
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var defenders = PooledRefList<Mobile>.Create();
|
||||
|
||||
foreach (var (defender, context) in _contexts)
|
||||
{
|
||||
if (context.Attacker == mobile || defender == mobile)
|
||||
{
|
||||
defenders.Add(defender);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < defenders.Count; i++)
|
||||
{
|
||||
StopContext(defenders[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ClearDefender(Mobile defender)
|
||||
{
|
||||
if (defender == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StopContext(defender);
|
||||
}
|
||||
|
||||
private static void StopContext(Mobile defender)
|
||||
{
|
||||
if (_contexts.Remove(defender, out var context))
|
||||
{
|
||||
context.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasBurningTorchEquipped(Mobile mobile) =>
|
||||
mobile.FindItemOnLayer<Torch>(Layer.TwoHanded) is { Burning: true };
|
||||
|
||||
private static bool IsValidCombatant(Mobile mobile) =>
|
||||
mobile is { Deleted: false, Alive: true } && mobile.Map != null && mobile.Map != Map.Internal;
|
||||
|
||||
private sealed class SwarmContext
|
||||
{
|
||||
private readonly Mobile _defender;
|
||||
private TimerExecutionToken _tickTimerToken;
|
||||
|
||||
public SwarmContext(Mobile attacker, Mobile defender)
|
||||
{
|
||||
Attacker = attacker;
|
||||
_defender = defender;
|
||||
TicksRemaining = TickCount;
|
||||
|
||||
if (_defender is PlayerMobile pm && BuffInfo.Enabled)
|
||||
{
|
||||
pm.AddBuff(new BuffInfo(BuffIcon.Swarm, 1157328, 1157362, EffectDuration));
|
||||
}
|
||||
|
||||
Attacker.PlaySound(0x00E);
|
||||
Timer.StartTimer(TickInterval, TickInterval, Tick, out _tickTimerToken);
|
||||
}
|
||||
|
||||
public Mobile Attacker { get; }
|
||||
|
||||
public int TicksRemaining { get; private set; }
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_tickTimerToken.Cancel();
|
||||
|
||||
if (_defender is PlayerMobile pm && BuffInfo.Enabled)
|
||||
{
|
||||
pm.RemoveBuff(BuffIcon.Swarm);
|
||||
}
|
||||
}
|
||||
|
||||
public void Tick()
|
||||
{
|
||||
if (!IsValidCombatant(Attacker) || !IsValidCombatant(_defender) || HasBurningTorchEquipped(_defender))
|
||||
{
|
||||
StopContext(_defender);
|
||||
return;
|
||||
}
|
||||
|
||||
ApplySwarmTick(Attacker, _defender, RawDamage);
|
||||
TicksRemaining--;
|
||||
|
||||
if (TicksRemaining <= 0)
|
||||
{
|
||||
StopContext(_defender);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -76,6 +76,8 @@ namespace Server
|
|||
Fix(ref chaos);
|
||||
Fix(ref direct);
|
||||
|
||||
var firePostResistDamage = ignoreArmor ? damage * fire / 100 : 0;
|
||||
|
||||
if (Core.ML && chaos > 0)
|
||||
{
|
||||
switch (Utility.Random(5))
|
||||
|
|
@ -126,13 +128,16 @@ namespace Server
|
|||
var resPois = m.PoisonResistance;
|
||||
var resNrgy = m.EnergyResistance;
|
||||
|
||||
var fireDamageNumerator = damage * fire * (100 - resFire);
|
||||
|
||||
totalDamage = damage * phys * (100 - resPhys);
|
||||
totalDamage += damage * fire * (100 - resFire);
|
||||
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)
|
||||
{
|
||||
|
|
@ -241,7 +246,14 @@ namespace Server
|
|||
|
||||
var oldHits = m.Hits;
|
||||
m.Damage(totalDamage, from);
|
||||
BattleLust.OnDamageTaken(m, from, Math.Max(0, oldHits - m.Hits));
|
||||
var appliedDamage = Math.Max(0, oldHits - m.Hits);
|
||||
|
||||
if (firePostResistDamage > 0 && appliedDamage > 0)
|
||||
{
|
||||
Swarm.ClearDefender(m);
|
||||
}
|
||||
|
||||
BattleLust.OnDamageTaken(m, from, appliedDamage);
|
||||
return totalDamage;
|
||||
}
|
||||
|
||||
|
|
@ -1119,7 +1131,8 @@ namespace Server
|
|||
Bane = 0x00000001,
|
||||
BattleLust = 0x00000002,
|
||||
HitSparks = 0x00000004,
|
||||
BloodDrinker = 0x00000008
|
||||
BloodDrinker = 0x00000008,
|
||||
HitSwarm = 0x00000010
|
||||
}
|
||||
|
||||
public sealed class ExtendedWeaponAttributes : BaseAttributes
|
||||
|
|
@ -1176,6 +1189,13 @@ namespace Server
|
|||
set => this[ExtendedWeaponAttribute.BloodDrinker] = value;
|
||||
}
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public int HitSwarm
|
||||
{
|
||||
get => this[ExtendedWeaponAttribute.HitSwarm];
|
||||
set => this[ExtendedWeaponAttribute.HitSwarm] = value;
|
||||
}
|
||||
|
||||
public void GetProperties(IPropertyList list)
|
||||
{
|
||||
if (Core.HS && Bane != 0)
|
||||
|
|
@ -1197,6 +1217,11 @@ namespace Server
|
|||
{
|
||||
list.Add(1157326, HitSparks); // Sparks ~1_val~%
|
||||
}
|
||||
|
||||
if (Core.TOL && HitSwarm != 0)
|
||||
{
|
||||
list.Add(1157325, HitSwarm); // Swarm ~1_val~%
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => "...";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue