Merge pull request #38 from RebirthUO/feat/issue-32-nether-bolt

This commit is contained in:
Crome696 2026-07-09 20:07:43 +02:00 committed by GitHub
commit 752bf3cc29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 281 additions and 2 deletions

View file

@ -0,0 +1,205 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Server.Items;
using Server.Spells;
using Server.Spells.First;
using Server.Spells.Mysticism;
using Xunit;
namespace Server.Tests.Spells.Mysticism;
[Collection("Sequential UOContent Tests")]
public class NetherBoltSpellTests
{
private static Mobile NewMobile()
{
var m = new Mobile(World.NewMobile);
m.DefaultMobileInit();
m.InitStats(100, 100, 100);
m.Mana = 100;
return m;
}
[Fact]
public void SpellMetadata_MatchesFirstCircleMysticismSources()
{
var caster = NewMobile();
var spell = new NetherBoltSpell(caster);
Assert.Equal("Nether Bolt", spell.Name);
Assert.Equal("In Corp Ylem", spell.Mantra);
Assert.Equal(SpellCircle.First, spell.Circle);
Assert.Equal(TimeSpan.FromSeconds(0.5), spell.CastDelayBase);
Assert.Equal(4, spell.GetMana());
Assert.Equal(0.0, spell.RequiredSkill);
Assert.Equal(SkillName.Mysticism, spell.CastSkill);
Assert.Equal([Reagent.BlackPearl, Reagent.SulfurousAsh], spell.Reagents);
Assert.True(spell.DelayedDamage);
caster.Delete();
}
[Fact]
public void MysticSpellbookAndScroll_UseNetherBoltSlot()
{
var book = new MysticSpellbook(1);
var scroll = new NetherBoltScroll();
Assert.Equal(677, book.BookOffset);
Assert.Equal(16, book.BookCount);
Assert.True(book.HasSpell(677));
Assert.Equal(677, GetSpellScrollId(scroll));
book.Delete();
scroll.Delete();
}
[Fact]
public void RegisterMysticism_PreSa_DoesNotExposeNetherBolt()
{
var previousExpansion = Core.Expansion;
var caster = NewMobile();
try
{
ResetSpellRegistry();
Core.Expansion = Expansion.ML;
Initializer.Configure();
Assert.Null(SpellRegistry.NewSpell(677, caster, null));
Assert.Equal(-1, SpellRegistry.GetRegistryNumber(typeof(NetherBoltSpell)));
Assert.IsType<MagicArrowSpell>(SpellRegistry.NewSpell(4, caster, null));
}
finally
{
Core.Expansion = previousExpansion;
ResetSpellRegistry();
Initializer.Configure();
caster.Delete();
}
}
[Fact]
public void RegisterMysticism_Sa_ExposesNetherBoltAtSpellId677()
{
var previousExpansion = Core.Expansion;
var caster = NewMobile();
try
{
ResetSpellRegistry();
Core.Expansion = Expansion.SA;
Initializer.Configure();
Assert.Same(typeof(NetherBoltSpell), SpellRegistry.Types[677]);
Assert.Equal(677, SpellRegistry.GetRegistryNumber(typeof(NetherBoltSpell)));
Assert.IsType<NetherBoltSpell>(SpellRegistry.NewSpell(677, caster, null));
Assert.IsType<EagleStrikeSpell>(SpellRegistry.NewSpell(682, caster, null));
}
finally
{
Core.Expansion = previousExpansion;
ResetSpellRegistry();
Initializer.Configure();
caster.Delete();
}
}
[Fact]
public void DelayedDamageFamily_PreventsNetherBoltAndMagicArrowStackingInEitherOrder()
{
var caster = NewMobile();
var target = NewMobile();
var netherBolt = new NetherBoltSpell(caster);
var magicArrow = new MagicArrowSpell(caster);
netherBolt.StartDelayedDamageContext(target, new TestTimer());
Assert.True(netherBolt.HasDelayedDamageContext(target));
Assert.True(magicArrow.HasDelayedDamageContext(target));
magicArrow.RemoveDelayedDamageContext(target);
magicArrow.StartDelayedDamageContext(target, new TestTimer());
Assert.True(magicArrow.HasDelayedDamageContext(target));
Assert.True(netherBolt.HasDelayedDamageContext(target));
netherBolt.RemoveDelayedDamageContext(target);
caster.Delete();
target.Delete();
}
[Fact]
public void GetNewAosDamage_UsesHigherFocusOrImbuingSupportSkill()
{
var focusCaster = NewMobile();
var imbuingCaster = NewMobile();
var lowSupportCaster = NewMobile();
var target = NewMobile();
focusCaster.Skills.Mysticism.Base = 120.0;
focusCaster.Skills.Focus.Base = 120.0;
focusCaster.Skills.Imbuing.Base = 0.0;
imbuingCaster.Skills.Mysticism.Base = 120.0;
imbuingCaster.Skills.Focus.Base = 0.0;
imbuingCaster.Skills.Imbuing.Base = 120.0;
lowSupportCaster.Skills.Mysticism.Base = 120.0;
lowSupportCaster.Skills.Focus.Base = 0.0;
lowSupportCaster.Skills.Imbuing.Base = 0.0;
using var random = new PredictableRandom(0);
var focusDamage = new NetherBoltSpell(focusCaster).GetNewAosDamage(10, 1, 4, target);
var imbuingDamage = new NetherBoltSpell(imbuingCaster).GetNewAosDamage(10, 1, 4, target);
var lowSupportDamage = new NetherBoltSpell(lowSupportCaster).GetNewAosDamage(10, 1, 4, target);
Assert.Equal(focusDamage, imbuingDamage);
Assert.True(focusDamage > lowSupportDamage);
focusCaster.Delete();
imbuingCaster.Delete();
lowSupportCaster.Delete();
target.Delete();
}
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();
}
private sealed class TestTimer : Timer
{
public TestTimer() : base(TimeSpan.FromMinutes(5))
{
}
protected override void OnTick()
{
}
}
}

View file

@ -1,4 +1,5 @@
using System;
using Server.Spells.Mysticism;
using Server.Targeting;
namespace Server.Spells.First
@ -19,7 +20,9 @@ namespace Server.Spells.First
public override SpellCircle Circle => SpellCircle.First;
public override Type[] DelayedDamageSpellFamilyStacking => AOSNoDelayedDamageStackingSelf;
private static readonly Type[] _delayedDamageSpellFamilyStacking = Core.AOS ? [typeof(NetherBoltSpell)] : null;
public override Type[] DelayedDamageSpellFamilyStacking => _delayedDamageSpellFamilyStacking;
public override bool DelayedDamage => true;

View file

@ -181,7 +181,7 @@ namespace Server.Spells
if (Core.SA)
{
// Mysticism spells
// Register(677, typeof(NetherBoltSpell));
Register(677, typeof(NetherBoltSpell));
// Register(678, typeof(HealingStoneSpell));
// Register(679, typeof(PurgeMagicSpell));
// Register(680, typeof(EnchantSpell));

View file

@ -0,0 +1,71 @@
using System;
using Server.Spells.First;
using Server.Targeting;
namespace Server.Spells.Mysticism;
public class NetherBoltSpell : MysticSpell, ITargetingSpell<Mobile>
{
private static readonly SpellInfo _info = new(
"Nether Bolt",
"In Corp Ylem",
-1,
9002,
Reagent.BlackPearl,
Reagent.SulfurousAsh
);
private static readonly Type[] _delayedDamageSpellFamilyStacking = Core.AOS ? [typeof(MagicArrowSpell)] : null;
public NetherBoltSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
{
}
public override SpellCircle Circle => SpellCircle.First;
public override bool DelayedDamage => true;
public override Type[] DelayedDamageSpellFamilyStacking => _delayedDamageSpellFamilyStacking;
public void Target(Mobile m)
{
if (CheckHSequence(m))
{
var source = Caster;
SpellHelper.Turn(source, m);
if (Core.SA && HasDelayedDamageContext(m))
{
DoHurtFizzle();
return;
}
SpellHelper.CheckReflect((int)Circle, ref source, ref m);
source.MovingParticles(
m,
0x36D4,
5,
0,
false,
true,
0x49A,
0,
9502,
1,
0,
EffectLayer.Head,
0x100
);
source.PlaySound(0x211);
SpellHelper.Damage(this, m, GetNewAosDamage(10, 1, 4, m), 0, 0, 0, 0, 0, 100);
}
}
public override void OnCast()
{
Caster.Target = new SpellTarget<Mobile>(this, TargetFlags.Harmful);
}
}