feat: implement Spell Trigger
This commit is contained in:
parent
bfd6284eac
commit
0f2d83eb3d
8 changed files with 1058 additions and 3 deletions
|
|
@ -0,0 +1,550 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Server.Gumps;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
using Server.Network;
|
||||
using Server.Spells;
|
||||
using Server.Spells.Mysticism;
|
||||
using Xunit;
|
||||
|
||||
namespace Server.Tests.Spells.Mysticism;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class SpellTriggerSpellTests
|
||||
{
|
||||
[Fact]
|
||||
public void SpellMetadata_MatchesFifthCircleMysticismSources()
|
||||
{
|
||||
var caster = NewCaster();
|
||||
var spell = new SpellTriggerSpell(caster);
|
||||
|
||||
Assert.Equal("Spell Trigger", spell.Name);
|
||||
Assert.Equal("In Vas Ort Ex", 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(SkillName.Mysticism, spell.CastSkill);
|
||||
Assert.Equal(
|
||||
[Reagent.DragonsBlood, Reagent.Garlic, Reagent.MandrakeRoot, Reagent.SpidersSilk],
|
||||
spell.Reagents
|
||||
);
|
||||
|
||||
caster.Delete();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MysticSpellbookAndScroll_UseSpellTriggerSlot()
|
||||
{
|
||||
var book = new MysticSpellbook(1UL << (685 - 677));
|
||||
var scroll = new SpellTriggerScroll();
|
||||
|
||||
Assert.Equal(677, book.BookOffset);
|
||||
Assert.Equal(16, book.BookCount);
|
||||
Assert.True(book.HasSpell(685));
|
||||
Assert.Equal(685, GetSpellScrollId(scroll));
|
||||
|
||||
book.Delete();
|
||||
scroll.Delete();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterMysticism_PreSaDoesNotExposeSpellTrigger()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var caster = NewCaster();
|
||||
|
||||
try
|
||||
{
|
||||
ResetSpellRegistry();
|
||||
Core.Expansion = Expansion.ML;
|
||||
Initializer.Configure();
|
||||
|
||||
Assert.Null(SpellRegistry.NewSpell(685, caster, null));
|
||||
Assert.Equal(-1, SpellRegistry.GetRegistryNumber(typeof(SpellTriggerSpell)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
ResetSpellRegistry();
|
||||
Initializer.Configure();
|
||||
caster.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterMysticism_SaExposesSpellTriggerAtSpellId685()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var caster = NewCaster();
|
||||
|
||||
try
|
||||
{
|
||||
ResetSpellRegistry();
|
||||
Core.Expansion = Expansion.SA;
|
||||
Initializer.Configure();
|
||||
|
||||
Assert.Same(typeof(SpellTriggerSpell), SpellRegistry.Types[685]);
|
||||
Assert.Equal(685, SpellRegistry.GetRegistryNumber(typeof(SpellTriggerSpell)));
|
||||
Assert.IsType<SpellTriggerSpell>(SpellRegistry.NewSpell(685, caster, null));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
ResetSpellRegistry();
|
||||
Initializer.Configure();
|
||||
caster.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaximumStorableCircle_UsesCombinedMysticismAndSupportSkillAndCapsAtSix()
|
||||
{
|
||||
var caster = NewCaster(20.0, 20.0);
|
||||
var cappedCaster = NewCaster();
|
||||
|
||||
Assert.Equal(1, SpellTriggerSpell.GetMaximumStorableCircle(caster));
|
||||
Assert.Equal(6, SpellTriggerSpell.GetMaximumStorableCircle(cappedCaster));
|
||||
|
||||
caster.Delete();
|
||||
cappedCaster.Delete();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EligibleDefinitions_RequireRegistrationKnowledgeAndStorageRank()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var caster = NewCaster();
|
||||
MysticSpellbook book = null;
|
||||
|
||||
try
|
||||
{
|
||||
ResetSpellRegistry();
|
||||
Core.Expansion = Expansion.SA;
|
||||
Initializer.Configure();
|
||||
book = AddMysticBook(caster, 677, 678, 679, 680, 682, 683, 684, 687, 688, 689, 690, 691);
|
||||
|
||||
var definitions = SpellTriggerSpell.GetEligibleDefinitions(caster);
|
||||
var spellIds = new int[definitions.Count];
|
||||
|
||||
for (var i = 0; i < definitions.Count; i++)
|
||||
{
|
||||
spellIds[i] = definitions[i].SpellId;
|
||||
}
|
||||
|
||||
Assert.Equal([677, 678, 679, 680, 682, 683, 684, 687, 688], spellIds);
|
||||
}
|
||||
finally
|
||||
{
|
||||
book?.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
ResetSpellRegistry();
|
||||
Initializer.Configure();
|
||||
caster.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckCast_EnforcesSaSkillManaAndEligibleSpellRequirements()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var caster = NewCaster();
|
||||
var book = AddMysticBook(caster, 677);
|
||||
caster.MoveToWorld(new Point3D(100, 100, 0), Map.Felucca);
|
||||
|
||||
try
|
||||
{
|
||||
ResetSpellRegistry();
|
||||
Core.Expansion = Expansion.SA;
|
||||
Initializer.Configure();
|
||||
|
||||
var spell = new SpellTriggerSpell(caster);
|
||||
Assert.True(spell.CheckCast());
|
||||
|
||||
caster.Skills.Mysticism.Base = 44.9;
|
||||
Assert.False(spell.CheckCast());
|
||||
|
||||
caster.Skills.Mysticism.Base = 120.0;
|
||||
caster.Mana = 0;
|
||||
Assert.False(spell.CheckCast());
|
||||
|
||||
caster.Mana = caster.ManaMax;
|
||||
Core.Expansion = Expansion.ML;
|
||||
Assert.False(spell.CheckCast());
|
||||
}
|
||||
finally
|
||||
{
|
||||
book.Delete();
|
||||
caster.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
ResetSpellRegistry();
|
||||
Initializer.Configure();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Selection_CreatesOneStoneReplacesPreviousStoneAndConsumesResources()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var caster = NewCaster();
|
||||
var book = AddMysticBook(caster, 677);
|
||||
var previousStone = new SpellStone(684);
|
||||
caster.AddToBackpack(previousStone);
|
||||
AddReagents(caster);
|
||||
|
||||
try
|
||||
{
|
||||
ResetSpellRegistry();
|
||||
Core.Expansion = Expansion.SA;
|
||||
Initializer.Configure();
|
||||
|
||||
var spell = BeginSelection(caster);
|
||||
spell.FinishSelection(677);
|
||||
|
||||
var stone = caster.Backpack.FindItemByType<SpellStone>();
|
||||
Assert.NotNull(stone);
|
||||
Assert.Equal(677, stone.SpellId);
|
||||
Assert.True(previousStone.Deleted);
|
||||
Assert.Null(caster.Spell);
|
||||
Assert.Equal(SpellState.None, spell.State);
|
||||
Assert.Equal(86, caster.Mana);
|
||||
Assert.Equal(9, caster.Backpack.FindItemByType<DragonsBlood>().Amount);
|
||||
Assert.Equal(9, caster.Backpack.FindItemByType<Garlic>().Amount);
|
||||
Assert.Equal(9, caster.Backpack.FindItemByType<MandrakeRoot>().Amount);
|
||||
Assert.Equal(9, caster.Backpack.FindItemByType<SpidersSilk>().Amount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
book.Delete();
|
||||
caster.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
ResetSpellRegistry();
|
||||
Initializer.Configure();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectionCancel_DoesNotConsumeResourcesOrCreateStone()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var caster = NewCaster();
|
||||
var book = AddMysticBook(caster, 677);
|
||||
AddReagents(caster);
|
||||
|
||||
try
|
||||
{
|
||||
ResetSpellRegistry();
|
||||
Core.Expansion = Expansion.SA;
|
||||
Initializer.Configure();
|
||||
|
||||
var spell = BeginSelection(caster);
|
||||
var gump = new SpellTriggerGump(spell, [SpellTriggerSpell.Definitions[0]]);
|
||||
var relay = new RelayInfo(
|
||||
0,
|
||||
ReadOnlySpan<int>.Empty,
|
||||
ReadOnlySpan<ushort>.Empty,
|
||||
ReadOnlySpan<Range>.Empty,
|
||||
ReadOnlySpan<byte>.Empty
|
||||
);
|
||||
|
||||
gump.OnResponse(null, in relay);
|
||||
|
||||
Assert.Null(caster.Spell);
|
||||
Assert.Equal(SpellState.None, spell.State);
|
||||
Assert.Null(caster.Backpack.FindItemByType<SpellStone>());
|
||||
Assert.Equal(100, caster.Mana);
|
||||
Assert.Equal(10, caster.Backpack.FindItemByType<DragonsBlood>().Amount);
|
||||
Assert.Equal(10, caster.Backpack.FindItemByType<Garlic>().Amount);
|
||||
Assert.Equal(10, caster.Backpack.FindItemByType<MandrakeRoot>().Amount);
|
||||
Assert.Equal(10, caster.Backpack.FindItemByType<SpidersSilk>().Amount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
book.Delete();
|
||||
caster.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
ResetSpellRegistry();
|
||||
Initializer.Configure();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectionGumpResponse_CreatesSelectedStone()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var caster = NewCaster();
|
||||
var book = AddMysticBook(caster, 677);
|
||||
AddReagents(caster);
|
||||
|
||||
try
|
||||
{
|
||||
ResetSpellRegistry();
|
||||
Core.Expansion = Expansion.SA;
|
||||
Initializer.Configure();
|
||||
|
||||
var spell = BeginSelection(caster);
|
||||
var gump = new SpellTriggerGump(spell, [SpellTriggerSpell.Definitions[0]]);
|
||||
var relay = new RelayInfo(
|
||||
100,
|
||||
ReadOnlySpan<int>.Empty,
|
||||
ReadOnlySpan<ushort>.Empty,
|
||||
ReadOnlySpan<Range>.Empty,
|
||||
ReadOnlySpan<byte>.Empty
|
||||
);
|
||||
|
||||
gump.OnResponse(null, in relay);
|
||||
|
||||
Assert.Equal(677, caster.Backpack.FindItemByType<SpellStone>()?.SpellId);
|
||||
Assert.Null(caster.Spell);
|
||||
}
|
||||
finally
|
||||
{
|
||||
book.Delete();
|
||||
caster.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
ResetSpellRegistry();
|
||||
Initializer.Configure();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectionGump_CompilesWithVisibleLayout()
|
||||
{
|
||||
var caster = NewCaster();
|
||||
var spell = new SpellTriggerSpell(caster);
|
||||
var gump = new SpellTriggerGump(spell, SpellTriggerSpell.Definitions);
|
||||
var buffer = GC.AllocateUninitializedArray<byte>(4096);
|
||||
var writer = new SpanWriter(buffer);
|
||||
gump.Compile(ref writer);
|
||||
|
||||
Assert.True(writer.BytesWritten > 0);
|
||||
|
||||
caster.Delete();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpellStoneActivation_UsesNormalSpellCastAndStartsCooldown()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var caster = NewCaster();
|
||||
var book = AddMysticBook(caster, 677);
|
||||
var stone = new SpellStone(677);
|
||||
var target = NewCaster();
|
||||
caster.AddToBackpack(stone);
|
||||
caster.MoveToWorld(new Point3D(100, 100, 0), Map.Felucca);
|
||||
target.MoveToWorld(new Point3D(101, 100, 0), Map.Felucca);
|
||||
|
||||
try
|
||||
{
|
||||
ResetSpellRegistry();
|
||||
Core.Expansion = Expansion.SA;
|
||||
Initializer.Configure();
|
||||
|
||||
Assert.True(stone.TryUseForTests(caster));
|
||||
Assert.True(stone.Deleted);
|
||||
Assert.True(SpellTriggerSpell.IsOnCooldown(caster, out var remaining));
|
||||
Assert.InRange(remaining.TotalSeconds, 299, SpellTriggerSpell.CooldownSeconds);
|
||||
|
||||
var spell = Assert.IsType<NetherBoltSpell>(caster.Spell);
|
||||
spell.State = SpellState.Sequencing;
|
||||
spell.OnCast();
|
||||
Assert.NotNull(caster.Target);
|
||||
spell.Target(target);
|
||||
spell.FinishSequence();
|
||||
}
|
||||
finally
|
||||
{
|
||||
SpellTriggerSpell.ClearCooldownForTests(caster);
|
||||
book.Delete();
|
||||
caster.Delete();
|
||||
target.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
ResetSpellRegistry();
|
||||
Initializer.Configure();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpellStoneActivation_UsesNormalSelfAndNoTargetSpellPipeline()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var caster = NewCaster();
|
||||
var book = AddMysticBook(caster, 678);
|
||||
var stone = new SpellStone(678);
|
||||
caster.AddToBackpack(stone);
|
||||
caster.AddToBackpack(new Bone(10));
|
||||
caster.AddToBackpack(new Garlic(10));
|
||||
caster.AddToBackpack(new Ginseng(10));
|
||||
caster.AddToBackpack(new SpidersSilk(10));
|
||||
caster.MoveToWorld(new Point3D(100, 100, 0), Map.Felucca);
|
||||
|
||||
try
|
||||
{
|
||||
ResetSpellRegistry();
|
||||
Core.Expansion = Expansion.SA;
|
||||
Initializer.Configure();
|
||||
|
||||
Assert.True(stone.TryUseForTests(caster));
|
||||
var spell = Assert.IsType<HealingStoneSpell>(caster.Spell);
|
||||
spell.State = SpellState.Sequencing;
|
||||
spell.OnCast();
|
||||
|
||||
Assert.Null(caster.Target);
|
||||
Assert.Null(caster.Spell);
|
||||
Assert.Equal(10, caster.Backpack.FindItemByType<Bone>().Amount);
|
||||
Assert.Equal(10, caster.Backpack.FindItemByType<Garlic>().Amount);
|
||||
Assert.Equal(10, caster.Backpack.FindItemByType<Ginseng>().Amount);
|
||||
Assert.Equal(10, caster.Backpack.FindItemByType<SpidersSilk>().Amount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SpellTriggerSpell.ClearCooldownForTests(caster);
|
||||
book.Delete();
|
||||
caster.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
ResetSpellRegistry();
|
||||
Initializer.Configure();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpellStoneActivation_RejectsUnknownStoredSpellAndHonorsCooldown()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var caster = NewCaster();
|
||||
var stone = new SpellStone(677);
|
||||
caster.AddToBackpack(stone);
|
||||
var secondStone = new SpellStone(677);
|
||||
caster.AddToBackpack(secondStone);
|
||||
|
||||
try
|
||||
{
|
||||
ResetSpellRegistry();
|
||||
Core.Expansion = Expansion.SA;
|
||||
Initializer.Configure();
|
||||
|
||||
Assert.False(stone.TryUseForTests(caster));
|
||||
Assert.False(stone.Deleted);
|
||||
|
||||
var book = AddMysticBook(caster, 677);
|
||||
Assert.True(stone.TryUseForTests(caster));
|
||||
caster.Spell?.FinishSequence();
|
||||
Assert.True(stone.Deleted);
|
||||
|
||||
Assert.False(secondStone.TryUseForTests(caster));
|
||||
Assert.False(secondStone.Deleted);
|
||||
|
||||
EventSink.InvokeLogout(caster);
|
||||
Assert.True(secondStone.TryUseForTests(caster));
|
||||
caster.Spell?.FinishSequence();
|
||||
book.Delete();
|
||||
}
|
||||
finally
|
||||
{
|
||||
SpellTriggerSpell.ClearCooldownForTests(caster);
|
||||
caster.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
ResetSpellRegistry();
|
||||
Initializer.Configure();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpellStone_IsBlessedNontransferableAndDestroyedWhenDropped()
|
||||
{
|
||||
var owner = NewCaster();
|
||||
var other = NewCaster();
|
||||
var stone = new SpellStone(677);
|
||||
owner.AddToBackpack(stone);
|
||||
|
||||
Assert.Equal(LootType.Blessed, stone.LootType);
|
||||
Assert.True(stone.Nontransferable);
|
||||
Assert.False(stone.AllowSecureTrade(owner, other, owner, true));
|
||||
Assert.False(stone.DropToWorld(owner, Point3D.Zero));
|
||||
Assert.True(stone.Deleted);
|
||||
|
||||
owner.Delete();
|
||||
other.Delete();
|
||||
}
|
||||
|
||||
private static PlayerMobile NewCaster(double mysticism = 120.0, double focus = 120.0, double imbuing = 0.0)
|
||||
{
|
||||
var caster = new PlayerMobile(World.NewMobile);
|
||||
caster.DefaultMobileInit();
|
||||
caster.Player = true;
|
||||
caster.InitStats(100, 100, 100);
|
||||
caster.Mana = caster.ManaMax;
|
||||
caster.AddItem(new Backpack());
|
||||
caster.Skills.Mysticism.Base = mysticism;
|
||||
caster.Skills.Focus.Base = focus;
|
||||
caster.Skills.Imbuing.Base = imbuing;
|
||||
return caster;
|
||||
}
|
||||
|
||||
private static TestSpellTriggerSpell BeginSelection(PlayerMobile caster)
|
||||
{
|
||||
var spell = new TestSpellTriggerSpell(caster);
|
||||
caster.Spell = spell;
|
||||
spell.State = SpellState.Sequencing;
|
||||
return spell;
|
||||
}
|
||||
|
||||
private static MysticSpellbook AddMysticBook(PlayerMobile caster, params int[] spellIds)
|
||||
{
|
||||
ulong content = 0;
|
||||
|
||||
for (var i = 0; i < spellIds.Length; i++)
|
||||
{
|
||||
content |= 1UL << (spellIds[i] - 677);
|
||||
}
|
||||
|
||||
var book = new MysticSpellbook(content);
|
||||
caster.AddToBackpack(book);
|
||||
return book;
|
||||
}
|
||||
|
||||
private static void AddReagents(Mobile caster)
|
||||
{
|
||||
caster.AddToBackpack(new DragonsBlood(10));
|
||||
caster.AddToBackpack(new Garlic(10));
|
||||
caster.AddToBackpack(new MandrakeRoot(10));
|
||||
caster.AddToBackpack(new SpidersSilk(10));
|
||||
}
|
||||
|
||||
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 TestSpellTriggerSpell : SpellTriggerSpell
|
||||
{
|
||||
public TestSpellTriggerSpell(Mobile caster) : base(caster)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool CheckFizzle() => true;
|
||||
}
|
||||
}
|
||||
88
Projects/UOContent/Gumps/SpellTriggerGump.cs
Normal file
88
Projects/UOContent/Gumps/SpellTriggerGump.cs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
using System.Collections.Generic;
|
||||
using Server.Items;
|
||||
using Server.Network;
|
||||
using Server.Spells;
|
||||
using Server.Spells.Mysticism;
|
||||
|
||||
namespace Server.Gumps;
|
||||
|
||||
public sealed class SpellTriggerGump : DynamicGump
|
||||
{
|
||||
private readonly SpellTriggerSpell _spell;
|
||||
private readonly SpellTriggerDefinition[] _definitions;
|
||||
|
||||
internal SpellTriggerGump(SpellTriggerSpell spell, IReadOnlyList<SpellTriggerDefinition> definitions) : base(20, 20)
|
||||
{
|
||||
_spell = spell;
|
||||
_definitions = new SpellTriggerDefinition[definitions.Count];
|
||||
|
||||
for (var i = 0; i < definitions.Count; i++)
|
||||
{
|
||||
_definitions[i] = definitions[i];
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Singleton => true;
|
||||
|
||||
public static bool DisplayTo(
|
||||
Mobile from,
|
||||
SpellTriggerSpell spell,
|
||||
IReadOnlyList<SpellTriggerDefinition> definitions
|
||||
)
|
||||
{
|
||||
if (from == null || from.Deleted || from.NetState == null || spell == null || spell.Caster != from ||
|
||||
from.Spell != spell || spell.State != SpellState.Sequencing || definitions == null || definitions.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
from.CloseGump<SpellTriggerGump>();
|
||||
from.SendGump(new SpellTriggerGump(spell, definitions));
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void BuildLayout(ref DynamicGumpBuilder builder)
|
||||
{
|
||||
builder.AddPage();
|
||||
builder.AddBackground(0, 0, 520, 404, 0x13BE);
|
||||
builder.AddImageTiled(10, 10, 500, 20, 0xA40);
|
||||
builder.AddImageTiled(10, 40, 500, 324, 0xA40);
|
||||
builder.AddImageTiled(10, 374, 500, 20, 0xA40);
|
||||
builder.AddAlphaRegion(10, 10, 500, 384);
|
||||
builder.AddHtmlLocalized(14, 12, 500, 20, 1080151, 0x7FFF); // <center>Spell Trigger Selection Menu</center>
|
||||
|
||||
builder.AddButton(10, 374, 0xFB1, 0xFB2, 0);
|
||||
builder.AddLabel(45, 376, 0x7FFF, "Cancel");
|
||||
|
||||
for (var i = 0; i < _definitions.Length; i++)
|
||||
{
|
||||
var definition = _definitions[i];
|
||||
var row = i / 2;
|
||||
var column = i % 2;
|
||||
var x = 14 + column * 250;
|
||||
var y = 44 + row * 64;
|
||||
|
||||
builder.AddImageTiledButton(x, y, 0x918, 0x919, 100 + i, GumpButtonType.Reply, 0, definition.ItemId, 0, 15, 20);
|
||||
builder.AddLabel(x + 84, y, 0x7FFF, definition.Name);
|
||||
builder.AddLabel(x + 84, y + 22, 0x7FFF, $"Circle {definition.Rank}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnResponse(NetState sender, in RelayInfo info)
|
||||
{
|
||||
if (sender != null && sender.Mobile != _spell.Caster)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var index = info.ButtonID - 100;
|
||||
|
||||
if (index < 0 || index >= _definitions.Length)
|
||||
{
|
||||
_spell.CancelSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
_spell.FinishSelection(_definitions[index].SpellId);
|
||||
}
|
||||
}
|
||||
77
Projects/UOContent/Items/Skill Items/Magical/SpellStone.cs
Normal file
77
Projects/UOContent/Items/Skill Items/Magical/SpellStone.cs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
using ModernUO.Serialization;
|
||||
using Server.Collections;
|
||||
using Server.ContextMenus;
|
||||
using Server.Multis;
|
||||
using Server.Spells.Mysticism;
|
||||
|
||||
namespace Server.Items;
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
public partial class SpellStone : SpellScroll
|
||||
{
|
||||
public const int ItemID = 0x4079;
|
||||
|
||||
[Constructible]
|
||||
public SpellStone(int spellId) : base(spellId, ItemID, 1)
|
||||
{
|
||||
Stackable = false;
|
||||
LootType = LootType.Blessed;
|
||||
}
|
||||
|
||||
public int SpellId => SpellID;
|
||||
|
||||
public override string DefaultName => "a spell stone";
|
||||
|
||||
public override bool Nontransferable => true;
|
||||
|
||||
public override void GetContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list)
|
||||
{
|
||||
}
|
||||
|
||||
public override void GetProperties(IPropertyList list)
|
||||
{
|
||||
base.GetProperties(list);
|
||||
|
||||
var definition = SpellTriggerSpell.GetDefinition(SpellId);
|
||||
|
||||
if (definition != null)
|
||||
{
|
||||
list.Add(1080166, definition.Name); // Use: ~1_spellName~
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDoubleClick(Mobile from)
|
||||
{
|
||||
if (!DesignContext.Check(from))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsChildOf(from.Backpack))
|
||||
{
|
||||
from.SendLocalizedMessage(1042001); // That must be in your pack for you to use it.
|
||||
return;
|
||||
}
|
||||
|
||||
SpellTriggerSpell.TryUseStone(this, from);
|
||||
}
|
||||
|
||||
internal bool TryUseForTests(Mobile from) => SpellTriggerSpell.TryUseStone(this, from);
|
||||
|
||||
public override bool AllowSecureTrade(Mobile from, Mobile to, Mobile newOwner, bool accepted) => false;
|
||||
|
||||
public override bool DropToWorld(Mobile from, Point3D p)
|
||||
{
|
||||
Delete();
|
||||
return false;
|
||||
}
|
||||
|
||||
[AfterDeserialization(false)]
|
||||
private void AfterDeserialization()
|
||||
{
|
||||
if (SpellTriggerSpell.GetDefinition(SpellId) == null)
|
||||
{
|
||||
Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
4
Projects/UOContent/Migrations/Server.Items.SpellStone.v0.json
generated
Normal file
4
Projects/UOContent/Migrations/Server.Items.SpellStone.v0.json
generated
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"version": 0,
|
||||
"type": "Server.Items.SpellStone"
|
||||
}
|
||||
|
|
@ -760,7 +760,8 @@ namespace Server.Spells
|
|||
{
|
||||
DoFizzle();
|
||||
}
|
||||
else if (Scroll != null && Scroll is not Runebook &&
|
||||
else if (Scroll != null && Scroll is not Runebook && Scroll is not SpellStone &&
|
||||
// Spell Stones are consumed at activation but still run the stored spell's sequence.
|
||||
(Scroll.Amount <= 0 || Scroll.Deleted || Scroll.RootParent != Caster || Scroll is BaseWand baseWand &&
|
||||
(baseWand.Charges <= 0 || baseWand.Parent != Caster)))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ namespace Server.Spells
|
|||
Register(682, typeof(EagleStrikeSpell));
|
||||
Register(683, typeof(AnimatedWeaponSpell));
|
||||
Register(684, typeof(StoneFormSpell));
|
||||
// Register(685, typeof(SpellTriggerSpell));
|
||||
Register(685, typeof(SpellTriggerSpell));
|
||||
// Register(686, typeof(MassSleepSpell));
|
||||
Register(687, typeof(CleansingWindsSpell));
|
||||
Register(688, typeof(BombardSpell));
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ public abstract class MysticSpell : Spell
|
|||
{
|
||||
var circle = (int)Circle;
|
||||
|
||||
if (Scroll != null)
|
||||
if (Scroll != null && Scroll is not SpellStone)
|
||||
{
|
||||
circle -= 2;
|
||||
}
|
||||
|
|
|
|||
335
Projects/UOContent/Spells/Mysticism/SpellTriggerSpell.cs
Normal file
335
Projects/UOContent/Spells/Mysticism/SpellTriggerSpell.cs
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ModernUO.CodeGeneratedEvents;
|
||||
using Server.Gumps;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
|
||||
namespace Server.Spells.Mysticism;
|
||||
|
||||
public class SpellTriggerSpell : MysticSpell
|
||||
{
|
||||
public const int SpellId = 685;
|
||||
public const int MaximumStorableCircle = 6;
|
||||
public const int CooldownSeconds = 300;
|
||||
|
||||
private static readonly SpellInfo _info = new(
|
||||
"Spell Trigger",
|
||||
"In Vas Ort Ex",
|
||||
230,
|
||||
9022,
|
||||
Reagent.DragonsBlood,
|
||||
Reagent.Garlic,
|
||||
Reagent.MandrakeRoot,
|
||||
Reagent.SpidersSilk
|
||||
);
|
||||
|
||||
private static readonly SpellTriggerDefinition[] _definitions =
|
||||
[
|
||||
new(677, "Nether Bolt", SpellCircle.First, 0x2D9E),
|
||||
new(678, "Healing Stone", SpellCircle.First, 0x2D9F),
|
||||
new(679, "Purge Magic", SpellCircle.Second, 0x2DA0),
|
||||
new(680, "Enchant", SpellCircle.Second, 0x2DA1),
|
||||
new(682, "Eagle Strike", SpellCircle.Third, 0x2DA3),
|
||||
new(683, "Animated Weapon", SpellCircle.Fourth, 0x2DA4),
|
||||
new(684, "Stone Form", SpellCircle.Fourth, 0x2DA5),
|
||||
new(687, "Cleansing Winds", SpellCircle.Sixth, 0x2DA8),
|
||||
new(688, "Bombard", SpellCircle.Sixth, 0x2DA9)
|
||||
];
|
||||
|
||||
private static readonly Dictionary<Mobile, DateTime> _cooldowns = new();
|
||||
|
||||
private TimerExecutionToken _selectionTimer;
|
||||
|
||||
static SpellTriggerSpell()
|
||||
{
|
||||
EventSink.Logout += OnLogout;
|
||||
}
|
||||
|
||||
public SpellTriggerSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
|
||||
{
|
||||
}
|
||||
|
||||
public override SpellCircle Circle => SpellCircle.Fifth;
|
||||
|
||||
public static IReadOnlyList<SpellTriggerDefinition> Definitions => _definitions;
|
||||
|
||||
public override bool CheckCast()
|
||||
{
|
||||
if (!Core.SA || !base.CheckCast())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Caster.Backpack == null)
|
||||
{
|
||||
Caster.SendLocalizedMessage(1042001); // That must be in your pack for you to use it.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GetEligibleDefinitions(Caster).Count == 0)
|
||||
{
|
||||
Caster.SendMessage("You do not know any Mysticism spells that can be stored.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void OnCast()
|
||||
{
|
||||
var definitions = GetEligibleDefinitions(Caster);
|
||||
|
||||
if (definitions.Count == 0 || !SpellTriggerGump.DisplayTo(Caster, this, definitions))
|
||||
{
|
||||
FinishSequence();
|
||||
return;
|
||||
}
|
||||
|
||||
Timer.StartTimer(TimeSpan.FromSeconds(30), CancelSelection, out _selectionTimer);
|
||||
}
|
||||
|
||||
public override void OnDisturb(DisturbType type, bool message)
|
||||
{
|
||||
Caster.CloseGump<SpellTriggerGump>();
|
||||
_selectionTimer.Cancel();
|
||||
base.OnDisturb(type, message);
|
||||
}
|
||||
|
||||
public override void FinishSequence()
|
||||
{
|
||||
_selectionTimer.Cancel();
|
||||
Caster.CloseGump<SpellTriggerGump>();
|
||||
base.FinishSequence();
|
||||
}
|
||||
|
||||
internal void FinishSelection(int spellId)
|
||||
{
|
||||
if (Caster.Spell != this || State != SpellState.Sequencing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var definition = GetDefinition(spellId);
|
||||
|
||||
if (definition == null || !IsEligible(Caster, definition))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newStone = new SpellStone(definition.SpellId);
|
||||
|
||||
if (!Caster.PlaceInBackpack(newStone))
|
||||
{
|
||||
newStone.Delete();
|
||||
Caster.SendMessage("There is not enough room in your backpack for a Spell Stone.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CheckSequence())
|
||||
{
|
||||
newStone.Delete();
|
||||
return;
|
||||
}
|
||||
|
||||
using (var stones = Caster.Backpack.EnumerateItemsByType<SpellStone>())
|
||||
{
|
||||
foreach (var stone in stones)
|
||||
{
|
||||
if (stone != newStone)
|
||||
{
|
||||
stone.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Caster.PlaySound(0x659);
|
||||
Caster.SendLocalizedMessage(1080165); // A Spell Stone appears in your backpack.
|
||||
}
|
||||
finally
|
||||
{
|
||||
FinishSequence();
|
||||
}
|
||||
}
|
||||
|
||||
internal void CancelSelection()
|
||||
{
|
||||
if (Caster.Spell == this && State == SpellState.Sequencing)
|
||||
{
|
||||
Caster.SendMessage("You decide not to create a Spell Stone.");
|
||||
FinishSequence();
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectionTimer.Cancel();
|
||||
Caster.CloseGump<SpellTriggerGump>();
|
||||
}
|
||||
}
|
||||
|
||||
internal static int GetMaximumStorableCircle(Mobile caster)
|
||||
{
|
||||
if (caster == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var skillTotal = GetBaseSkill(caster) + Math.Max(caster.Skills.Focus.Value, caster.Skills.Imbuing.Value);
|
||||
return Math.Min(MaximumStorableCircle, (int)(skillTotal / 40.0));
|
||||
}
|
||||
|
||||
internal static List<SpellTriggerDefinition> GetEligibleDefinitions(Mobile caster)
|
||||
{
|
||||
var definitions = new List<SpellTriggerDefinition>();
|
||||
var maximumCircle = GetMaximumStorableCircle(caster);
|
||||
|
||||
if (caster == null || caster.Deleted || caster.Backpack == null || maximumCircle == 0)
|
||||
{
|
||||
return definitions;
|
||||
}
|
||||
|
||||
for (var i = 0; i < _definitions.Length; i++)
|
||||
{
|
||||
var definition = _definitions[i];
|
||||
|
||||
if (definition.Rank > maximumCircle || !IsRegistered(definition.SpellId) || !IsKnown(caster, definition.SpellId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
definitions.Add(definition);
|
||||
}
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
internal static SpellTriggerDefinition GetDefinition(int spellId)
|
||||
{
|
||||
for (var i = 0; i < _definitions.Length; i++)
|
||||
{
|
||||
if (_definitions[i].SpellId == spellId)
|
||||
{
|
||||
return _definitions[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static bool TryUseStone(SpellStone stone, Mobile caster)
|
||||
{
|
||||
if (stone == null || stone.Deleted || caster == null || caster.Deleted || !caster.Alive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (caster.Backpack == null || !stone.IsChildOf(caster.Backpack))
|
||||
{
|
||||
caster.SendLocalizedMessage(1042001); // That must be in your pack for you to use it.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsOnCooldown(caster, out var remaining))
|
||||
{
|
||||
var seconds = Math.Max(1, (int)Math.Ceiling(remaining.TotalSeconds));
|
||||
caster.SendLocalizedMessage(1079263, seconds.ToString()); // You must wait ~1_seconds~ seconds before you can use this item.
|
||||
return false;
|
||||
}
|
||||
|
||||
var definition = GetDefinition(stone.SpellId);
|
||||
|
||||
if (definition == null || !IsRegistered(stone.SpellId))
|
||||
{
|
||||
caster.SendLocalizedMessage(502345); // This spell has been temporarily disabled.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsKnown(caster, stone.SpellId))
|
||||
{
|
||||
caster.SendLocalizedMessage(500015); // You do not have that spell!
|
||||
return false;
|
||||
}
|
||||
|
||||
var spell = SpellRegistry.NewSpell(stone.SpellId, caster, stone);
|
||||
|
||||
if (spell == null || !spell.Cast())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_cooldowns[caster] = Core.Now.AddSeconds(CooldownSeconds);
|
||||
stone.Delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool IsOnCooldown(Mobile caster, out TimeSpan remaining)
|
||||
{
|
||||
if (caster == null || !_cooldowns.TryGetValue(caster, out var cooldownUntil))
|
||||
{
|
||||
remaining = TimeSpan.Zero;
|
||||
return false;
|
||||
}
|
||||
|
||||
remaining = cooldownUntil - Core.Now;
|
||||
|
||||
if (remaining <= TimeSpan.Zero)
|
||||
{
|
||||
_cooldowns.Remove(caster);
|
||||
remaining = TimeSpan.Zero;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void ClearCooldownForTests(Mobile caster) => ClearCooldown(caster);
|
||||
|
||||
[OnEvent(nameof(PlayerMobile.PlayerDeathEvent))]
|
||||
[OnEvent(nameof(PlayerMobile.PlayerDeletedEvent))]
|
||||
[OnEvent(nameof(BaseCreature.CreatureDeathEvent))]
|
||||
[OnEvent(nameof(BaseCreature.CreatureDeletedEvent))]
|
||||
public static void OnCasterRemoved(Mobile caster) => ClearCooldown(caster);
|
||||
|
||||
private static void OnLogout(Mobile caster) => ClearCooldown(caster);
|
||||
|
||||
private static void ClearCooldown(Mobile caster)
|
||||
{
|
||||
if (caster != null)
|
||||
{
|
||||
_cooldowns.Remove(caster);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsEligible(Mobile caster, SpellTriggerDefinition definition) =>
|
||||
definition != null &&
|
||||
definition.Rank <= GetMaximumStorableCircle(caster) &&
|
||||
IsRegistered(definition.SpellId) &&
|
||||
IsKnown(caster, definition.SpellId);
|
||||
|
||||
private static bool IsKnown(Mobile caster, int spellId) => Spellbook.Find(caster, spellId)?.HasSpell(spellId) == true;
|
||||
|
||||
private static bool IsRegistered(int spellId) =>
|
||||
spellId >= 0 && spellId < SpellRegistry.Types.Length && SpellRegistry.Types[spellId] != null;
|
||||
}
|
||||
|
||||
public sealed class SpellTriggerDefinition
|
||||
{
|
||||
public SpellTriggerDefinition(int spellId, string name, SpellCircle circle, int itemId)
|
||||
{
|
||||
SpellId = spellId;
|
||||
Name = name;
|
||||
Circle = circle;
|
||||
ItemId = itemId;
|
||||
}
|
||||
|
||||
public int SpellId { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public SpellCircle Circle { get; }
|
||||
|
||||
public int ItemId { get; }
|
||||
|
||||
public int Rank => (int)Circle + 1;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue