feat: implement Mass Sleep stupor
This commit is contained in:
parent
b5b1b566cd
commit
ed19b8f4fd
8 changed files with 415 additions and 2 deletions
|
|
@ -0,0 +1,165 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Server.Items;
|
||||
using Server.Spells;
|
||||
using Server.Spells.Mysticism;
|
||||
using Xunit;
|
||||
|
||||
namespace Server.Tests.Spells.Mysticism;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class MassSleepSpellTests
|
||||
{
|
||||
[Fact]
|
||||
public void SpellMetadataAndScroll_MatchFifthCircleMysticismSources()
|
||||
{
|
||||
var caster = NewMysticCaster();
|
||||
var spell = new MassSleepSpell(caster);
|
||||
var scroll = new MassSleepScroll();
|
||||
|
||||
try
|
||||
{
|
||||
Assert.Equal("Mass Sleep", spell.Name);
|
||||
Assert.Equal("Vas Zu", spell.Mantra);
|
||||
Assert.Equal(SpellCircle.Fifth, spell.Circle);
|
||||
Assert.Equal(TimeSpan.FromSeconds(1.5), spell.CastDelayBase);
|
||||
Assert.Equal(14, spell.GetMana());
|
||||
Assert.Equal(45.0, spell.RequiredSkill);
|
||||
Assert.Equal([Reagent.Ginseng, Reagent.Nightshade, Reagent.SpidersSilk], spell.Reagents);
|
||||
Assert.Equal(686, GetSpellScrollId(scroll));
|
||||
}
|
||||
finally
|
||||
{
|
||||
caster.Delete();
|
||||
scroll.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterMysticism_RegistersMassSleepOnlyAtSa()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var caster = NewMysticCaster();
|
||||
|
||||
try
|
||||
{
|
||||
ResetSpellRegistry();
|
||||
Core.Expansion = Expansion.ML;
|
||||
Initializer.Configure();
|
||||
Assert.Null(SpellRegistry.NewSpell(686, caster, null));
|
||||
|
||||
ResetSpellRegistry();
|
||||
Core.Expansion = Expansion.SA;
|
||||
Initializer.Configure();
|
||||
Assert.Same(typeof(MassSleepSpell), SpellRegistry.Types[686]);
|
||||
Assert.IsType<MassSleepSpell>(SpellRegistry.NewSpell(686, caster, null));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
ResetSpellRegistry();
|
||||
Initializer.Configure();
|
||||
caster.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SleepDuration_UsesBetterSupportSkillAndTargetResist()
|
||||
{
|
||||
var caster = NewMysticCaster();
|
||||
var target = NewMobile();
|
||||
|
||||
try
|
||||
{
|
||||
caster.Skills.Mysticism.Base = 120.0;
|
||||
caster.Skills.Focus.Base = 80.0;
|
||||
caster.Skills.Imbuing.Base = 120.0;
|
||||
target.Skills.MagicResist.Base = 50.0;
|
||||
|
||||
Assert.Equal(TimeSpan.FromSeconds(10), SleepSpell.GetDuration(caster, target));
|
||||
|
||||
target.Skills.MagicResist.Base = 150.0;
|
||||
Assert.Equal(TimeSpan.Zero, SleepSpell.GetDuration(caster, target));
|
||||
}
|
||||
finally
|
||||
{
|
||||
caster.Delete();
|
||||
target.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DamageBreaksSleepAndAppliesResistScaledPlayerImmunity()
|
||||
{
|
||||
var caster = NewMysticCaster();
|
||||
var target = NewMobile();
|
||||
target.Player = true;
|
||||
target.Skills.MagicResist.Base = 120.0;
|
||||
|
||||
try
|
||||
{
|
||||
Assert.True(SleepSpell.Apply(caster, target, TimeSpan.FromSeconds(10)));
|
||||
Assert.True(SleepSpell.IsUnderEffect(target));
|
||||
Assert.Equal(SleepSpell.CastSpeedMalus, SleepSpell.GetCastSpeedMalus(target));
|
||||
Assert.Equal(SleepSpell.CastRecoveryMalus, SleepSpell.GetCastRecoveryMalus(target));
|
||||
Assert.Equal(SleepSpell.SwingSpeedMalus, SleepSpell.GetSwingSpeedMalus(target));
|
||||
Assert.Equal(TimeSpan.FromSeconds(12), SleepSpell.GetImmunityDuration(target));
|
||||
|
||||
SleepSpell.OnMobileDamaged(target, 1);
|
||||
|
||||
Assert.False(SleepSpell.IsUnderEffect(target));
|
||||
Assert.True(SleepSpell.IsImmune(target));
|
||||
Assert.False(SleepSpell.Apply(caster, target, TimeSpan.FromSeconds(10)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
SleepSpell.ClearAllForTests();
|
||||
caster.Delete();
|
||||
target.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
private static Mobile NewMobile()
|
||||
{
|
||||
var mobile = new Mobile(World.NewMobile);
|
||||
mobile.DefaultMobileInit();
|
||||
mobile.InitStats(100, 100, 100);
|
||||
mobile.Hits = mobile.HitsMax;
|
||||
mobile.Mana = mobile.ManaMax;
|
||||
return mobile;
|
||||
}
|
||||
|
||||
private static Mobile NewMysticCaster()
|
||||
{
|
||||
var caster = NewMobile();
|
||||
caster.Skills.Mysticism.Base = 120.0;
|
||||
caster.Skills.Focus.Base = 120.0;
|
||||
return caster;
|
||||
}
|
||||
|
||||
private static int GetSpellScrollId(SpellScroll scroll)
|
||||
{
|
||||
var field = typeof(SpellScroll).GetField("_spellID", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
return (int)field!.GetValue(scroll)!;
|
||||
}
|
||||
|
||||
private static void ResetSpellRegistry()
|
||||
{
|
||||
var types = (Type[])typeof(SpellRegistry)
|
||||
.GetField("m_Types", BindingFlags.Static | BindingFlags.NonPublic)!
|
||||
.GetValue(null)!;
|
||||
Array.Clear(types);
|
||||
|
||||
var idsFromTypes = (Dictionary<Type, int>)typeof(SpellRegistry)
|
||||
.GetField("m_IDsFromTypes", BindingFlags.Static | BindingFlags.NonPublic)!
|
||||
.GetValue(null)!;
|
||||
idsFromTypes.Clear();
|
||||
|
||||
typeof(SpellRegistry)
|
||||
.GetField("m_Count", BindingFlags.Static | BindingFlags.NonPublic)!
|
||||
.SetValue(null, 0);
|
||||
|
||||
SpellRegistry.SpecialMoves.Clear();
|
||||
}
|
||||
}
|
||||
|
|
@ -1533,6 +1533,8 @@ public abstract partial class BaseWeapon
|
|||
bonus -= EssenceOfWindSpell.GetSSIMalus(m);
|
||||
}
|
||||
|
||||
bonus -= SleepSpell.GetSwingSpeedMalus(m);
|
||||
|
||||
if (bonus > 60)
|
||||
{
|
||||
bonus = 60;
|
||||
|
|
|
|||
|
|
@ -1641,6 +1641,7 @@ namespace Server.Mobiles
|
|||
Confidence.StopRegenerating(this);
|
||||
|
||||
StaminaSystem.FatigueOnDamage(this, amount);
|
||||
Server.Spells.Mysticism.SleepSpell.OnMobileDamaged(this, amount);
|
||||
|
||||
var speechType = SpeechType;
|
||||
|
||||
|
|
|
|||
|
|
@ -2375,6 +2375,7 @@ namespace Server.Mobiles
|
|||
Confidence.StopRegenerating(this);
|
||||
|
||||
StaminaSystem.FatigueOnDamage(this, amount);
|
||||
Server.Spells.Mysticism.SleepSpell.OnMobileDamaged(this, amount);
|
||||
|
||||
ReceivedHonorContext?.OnTargetDamaged(from, amount);
|
||||
SentHonorContext?.OnSourceDamaged(from, amount);
|
||||
|
|
|
|||
|
|
@ -697,7 +697,8 @@ namespace Server.Spells
|
|||
}
|
||||
|
||||
var fcr = AosAttributes.GetValue(Caster, AosAttribute.CastRecovery) -
|
||||
ThunderstormSpell.GetCastRecoveryMalus(Caster);
|
||||
ThunderstormSpell.GetCastRecoveryMalus(Caster) -
|
||||
Mysticism.SleepSpell.GetCastRecoveryMalus(Caster);
|
||||
|
||||
var fcrDelay = -(CastRecoveryFastScalar * fcr);
|
||||
|
||||
|
|
@ -742,6 +743,8 @@ namespace Server.Spells
|
|||
fc -= EssenceOfWindSpell.GetFCMalus(Caster);
|
||||
}
|
||||
|
||||
fc -= Mysticism.SleepSpell.GetCastSpeedMalus(Caster);
|
||||
|
||||
fc += Mysticism.EnchantSpell.GetFasterCasting(Caster);
|
||||
|
||||
if (Core.SA)
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ namespace Server.Spells
|
|||
Register(683, typeof(AnimatedWeaponSpell));
|
||||
Register(684, typeof(StoneFormSpell));
|
||||
Register(685, typeof(SpellTriggerSpell));
|
||||
// Register(686, typeof(MassSleepSpell));
|
||||
Register(686, typeof(MassSleepSpell));
|
||||
Register(687, typeof(CleansingWindsSpell));
|
||||
Register(688, typeof(BombardSpell));
|
||||
Register(689, typeof(SpellPlagueSpell));
|
||||
|
|
|
|||
79
Projects/UOContent/Spells/Mysticism/MassSleepSpell.cs
Normal file
79
Projects/UOContent/Spells/Mysticism/MassSleepSpell.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
using System;
|
||||
using Server.Collections;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Spells.Mysticism;
|
||||
|
||||
public class MassSleepSpell : MysticSpell, ITargetingSpell<IPoint3D>
|
||||
{
|
||||
private const int Radius = 3;
|
||||
|
||||
private static readonly SpellInfo _info = new(
|
||||
"Mass Sleep",
|
||||
"Vas Zu",
|
||||
230,
|
||||
9022,
|
||||
Reagent.Ginseng,
|
||||
Reagent.Nightshade,
|
||||
Reagent.SpidersSilk
|
||||
);
|
||||
|
||||
public MassSleepSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
|
||||
{
|
||||
}
|
||||
|
||||
public override SpellCircle Circle => SpellCircle.Fifth;
|
||||
|
||||
public void Target(IPoint3D point)
|
||||
{
|
||||
var location = (point as Item)?.GetWorldLocation() ?? new Point3D(point);
|
||||
|
||||
if (!SpellHelper.CheckTown(location, Caster) || !CheckSequence())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SpellHelper.Turn(Caster, point);
|
||||
|
||||
var map = Caster.Map;
|
||||
|
||||
if (map == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var targets = PooledRefQueue<Mobile>.Create();
|
||||
|
||||
foreach (var target in map.GetMobilesInRange(location, Radius))
|
||||
{
|
||||
// Sleep is explicitly allowed to affect hidden players. The location target still
|
||||
// passes the normal target cursor, map, range, town, and LOS validation first.
|
||||
if (target == Caster || !SpellHelper.ValidIndirectTarget(Caster, target) || !Caster.CanBeHarmful(target, false) ||
|
||||
!Caster.InLOS(target))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
targets.Enqueue(target);
|
||||
}
|
||||
|
||||
while (targets.Count > 0)
|
||||
{
|
||||
var target = targets.Dequeue();
|
||||
var duration = SleepSpell.GetDuration(Caster, target);
|
||||
|
||||
if (duration <= TimeSpan.Zero)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Caster.DoHarmful(target);
|
||||
SleepSpell.Apply(Caster, target, duration);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnCast()
|
||||
{
|
||||
Caster.Target = new SpellTarget<IPoint3D>(this, allowGround: true);
|
||||
}
|
||||
}
|
||||
162
Projects/UOContent/Spells/Mysticism/SleepSpell.cs
Normal file
162
Projects/UOContent/Spells/Mysticism/SleepSpell.cs
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ModernUO.CodeGeneratedEvents;
|
||||
using Server.Collections;
|
||||
using Server.Engines.BuffIcons;
|
||||
using Server.Mobiles;
|
||||
using Server.Network;
|
||||
|
||||
namespace Server.Spells.Mysticism;
|
||||
|
||||
public static class SleepSpell
|
||||
{
|
||||
internal const int CastSpeedMalus = 4;
|
||||
internal const int CastRecoveryMalus = 6;
|
||||
internal const int SwingSpeedMalus = 60;
|
||||
|
||||
private static readonly Dictionary<Mobile, SleepContext> _effects = [];
|
||||
private static readonly Dictionary<Mobile, TimerExecutionToken> _immunities = [];
|
||||
|
||||
public static void Configure()
|
||||
{
|
||||
EventSink.Logout += Clear;
|
||||
EventSink.MapChanged += OnMapChanged;
|
||||
}
|
||||
|
||||
public static bool Apply(Mobile caster, Mobile target, TimeSpan duration)
|
||||
{
|
||||
if (caster == null || target is { Deleted: true } || !target.Alive || duration <= TimeSpan.Zero ||
|
||||
_effects.ContainsKey(target) || IsImmune(target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var context = new SleepContext();
|
||||
_effects[target] = context;
|
||||
Timer.StartTimer(duration, () => EndEffect(target, false), out context.TimerToken);
|
||||
|
||||
target.FixedParticles(0x373A, 1, 15, 0x48F, 7, 0, EffectLayer.Head, 0);
|
||||
target.PlaySound(0x1FB);
|
||||
|
||||
if (target is PlayerMobile player)
|
||||
{
|
||||
player.NetState?.SendSpeedControl(SpeedControlSetting.Walk);
|
||||
player.AddBuff(new BuffInfo(BuffIcon.MassSleep, 1075824, duration));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsUnderEffect(Mobile target) => target != null && _effects.ContainsKey(target);
|
||||
|
||||
public static bool IsImmune(Mobile target) => target != null && _immunities.ContainsKey(target);
|
||||
|
||||
public static void OnMobileDamaged(Mobile target, int damage)
|
||||
{
|
||||
if (damage > 0)
|
||||
{
|
||||
EndEffect(target, true);
|
||||
}
|
||||
}
|
||||
|
||||
internal static int GetCastSpeedMalus(Mobile target) => IsUnderEffect(target) ? CastSpeedMalus : 0;
|
||||
|
||||
internal static int GetCastRecoveryMalus(Mobile target) => IsUnderEffect(target) ? CastRecoveryMalus : 0;
|
||||
|
||||
internal static int GetSwingSpeedMalus(Mobile target) => IsUnderEffect(target) ? SwingSpeedMalus : 0;
|
||||
|
||||
internal static TimeSpan GetDuration(Mobile caster, Mobile target)
|
||||
{
|
||||
var supportSkill = Math.Max(caster.Skills.Focus.Value, caster.Skills.Imbuing.Value);
|
||||
var seconds = (caster.Skills.Mysticism.Value + supportSkill) / 20.0 + 3.0 - target.Skills.MagicResist.Value / 10.0;
|
||||
return TimeSpan.FromSeconds(Math.Max(seconds, 0.0));
|
||||
}
|
||||
|
||||
internal static TimeSpan GetImmunityDuration(Mobile target) =>
|
||||
TimeSpan.FromSeconds(Math.Clamp((int)(target.Skills.MagicResist.Value / 10.0), 3, 12));
|
||||
|
||||
internal static void EndEffectForTests(Mobile target, bool grantImmunity) => EndEffect(target, grantImmunity);
|
||||
|
||||
internal static void ClearAllForTests()
|
||||
{
|
||||
using var targets = PooledRefList<Mobile>.Create();
|
||||
|
||||
foreach (var target in _effects.Keys)
|
||||
{
|
||||
targets.Add(target);
|
||||
}
|
||||
|
||||
for (var i = 0; i < targets.Count; i++)
|
||||
{
|
||||
EndEffect(targets[i], false);
|
||||
}
|
||||
|
||||
foreach (var token in _immunities.Values)
|
||||
{
|
||||
token.Cancel();
|
||||
}
|
||||
|
||||
_immunities.Clear();
|
||||
}
|
||||
|
||||
[OnEvent(nameof(PlayerMobile.PlayerDeathEvent))]
|
||||
[OnEvent(nameof(PlayerMobile.PlayerDeletedEvent))]
|
||||
[OnEvent(nameof(BaseCreature.CreatureDeathEvent))]
|
||||
[OnEvent(nameof(BaseCreature.CreatureDeletedEvent))]
|
||||
public static void Clear(Mobile target)
|
||||
{
|
||||
EndEffect(target, false);
|
||||
|
||||
if (_immunities.Remove(target, out var token))
|
||||
{
|
||||
token.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnMapChanged(Mobile target, Map oldMap) => Clear(target);
|
||||
|
||||
private static void EndEffect(Mobile target, bool grantImmunity)
|
||||
{
|
||||
if (target == null || !_effects.Remove(target, out var context))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context.TimerToken.Cancel();
|
||||
|
||||
if (target is PlayerMobile player)
|
||||
{
|
||||
player.NetState?.SendSpeedControl(SpeedControlSetting.Disable);
|
||||
player.RemoveBuff(BuffIcon.MassSleep);
|
||||
}
|
||||
|
||||
if (grantImmunity && target.Player && !target.Deleted && target.Alive)
|
||||
{
|
||||
StartImmunity(target);
|
||||
}
|
||||
}
|
||||
|
||||
private static void StartImmunity(Mobile target)
|
||||
{
|
||||
if (_immunities.ContainsKey(target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Timer.StartTimer(GetImmunityDuration(target), () => RemoveImmunity(target), out var token);
|
||||
_immunities[target] = token;
|
||||
}
|
||||
|
||||
private static void RemoveImmunity(Mobile target)
|
||||
{
|
||||
if (_immunities.Remove(target, out var token))
|
||||
{
|
||||
token.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SleepContext
|
||||
{
|
||||
internal TimerExecutionToken TimerToken;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue