feat: implement mysticism enchant spell
This commit is contained in:
parent
1ad7593ddd
commit
cd526022fa
6 changed files with 1047 additions and 7 deletions
|
|
@ -0,0 +1,548 @@
|
|||
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 Server.Spells.Ninjitsu;
|
||||
using Server.Tests;
|
||||
using Xunit;
|
||||
|
||||
namespace Server.Tests.Spells.Mysticism;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class EnchantSpellTests
|
||||
{
|
||||
[Fact]
|
||||
public void SpellMetadata_MatchesEnchantSources()
|
||||
{
|
||||
var caster = NewCaster();
|
||||
var spell = new EnchantSpell(caster);
|
||||
|
||||
Assert.Equal("Enchant", spell.Name);
|
||||
Assert.Equal("In Ort Ylem", spell.Mantra);
|
||||
Assert.Equal(SpellCircle.Second, spell.Circle);
|
||||
Assert.Equal(TimeSpan.FromSeconds(0.75), spell.CastDelayBase);
|
||||
Assert.Equal(6, spell.GetMana());
|
||||
Assert.Equal(8.0, spell.RequiredSkill);
|
||||
Assert.Equal(SkillName.Mysticism, spell.CastSkill);
|
||||
Assert.Equal([Reagent.SpidersSilk, Reagent.MandrakeRoot, Reagent.SulfurousAsh], spell.Reagents);
|
||||
Assert.False(spell.ClearHandsOnCast);
|
||||
|
||||
caster.Delete();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MysticSpellbookAndScroll_UseEnchantSlot()
|
||||
{
|
||||
var book = new MysticSpellbook(1UL << (680 - 677));
|
||||
var scroll = new EnchantScroll();
|
||||
|
||||
Assert.Equal(677, book.BookOffset);
|
||||
Assert.Equal(16, book.BookCount);
|
||||
Assert.True(book.HasSpell(680));
|
||||
Assert.Equal(680, GetSpellScrollId(scroll));
|
||||
|
||||
book.Delete();
|
||||
scroll.Delete();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterMysticism_PreSaDoesNotExposeEnchant_AndSaUsesSpellId680()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var caster = NewCaster();
|
||||
|
||||
try
|
||||
{
|
||||
ResetSpellRegistry();
|
||||
Core.Expansion = Expansion.ML;
|
||||
Initializer.Configure();
|
||||
|
||||
Assert.Null(SpellRegistry.NewSpell(680, caster, null));
|
||||
Assert.Equal(-1, SpellRegistry.GetRegistryNumber(typeof(EnchantSpell)));
|
||||
|
||||
ResetSpellRegistry();
|
||||
Core.Expansion = Expansion.SA;
|
||||
Initializer.Configure();
|
||||
|
||||
Assert.Same(typeof(EnchantSpell), SpellRegistry.Types[680]);
|
||||
Assert.Equal(680, SpellRegistry.GetRegistryNumber(typeof(EnchantSpell)));
|
||||
Assert.IsType<EnchantSpell>(SpellRegistry.NewSpell(680, caster, null));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
ResetSpellRegistry();
|
||||
Initializer.Configure();
|
||||
caster.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Duration_ScalesWithSelectedHitSpellLevelAndCapsAt150Seconds()
|
||||
{
|
||||
Assert.Equal(TimeSpan.FromSeconds(30), EnchantSpell.GetDuration(AosWeaponAttribute.HitMagicArrow));
|
||||
Assert.Equal(TimeSpan.FromSeconds(60), EnchantSpell.GetDuration(AosWeaponAttribute.HitHarm));
|
||||
Assert.Equal(TimeSpan.FromSeconds(90), EnchantSpell.GetDuration(AosWeaponAttribute.HitFireball));
|
||||
Assert.Equal(TimeSpan.FromSeconds(120), EnchantSpell.GetDuration(AosWeaponAttribute.HitLightning));
|
||||
Assert.Equal(TimeSpan.FromSeconds(150), EnchantSpell.GetDuration(AosWeaponAttribute.HitDispel));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HitSpellChance_UsesHigherFocusOrImbuingAndCapsAt60()
|
||||
{
|
||||
var focusCaster = NewCaster(120, 120, 0);
|
||||
var imbuingCaster = NewCaster(120, 0, 120);
|
||||
var lowCaster = NewCaster(80, 0, 0);
|
||||
|
||||
Assert.Equal(60, EnchantSpell.GetHitSpellChance(focusCaster));
|
||||
Assert.Equal(60, EnchantSpell.GetHitSpellChance(imbuingCaster));
|
||||
var lowExpected = (int)(60 * (lowCaster.Skills.Mysticism.Value +
|
||||
Math.Max(lowCaster.Skills.Focus.Value, lowCaster.Skills.Imbuing.Value)) / 240.0);
|
||||
Assert.Equal(lowExpected, EnchantSpell.GetHitSpellChance(lowCaster));
|
||||
|
||||
focusCaster.Delete();
|
||||
imbuingCaster.Delete();
|
||||
lowCaster.Delete();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Selection_AppliesRuntimeOnlyHitSpellAndChannelingState()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
Core.Expansion = Expansion.SA;
|
||||
|
||||
var caster = NewCaster();
|
||||
var weapon = new Katana();
|
||||
AddReagents(caster);
|
||||
Assert.True(caster.EquipItem(weapon));
|
||||
|
||||
try
|
||||
{
|
||||
var spell = BeginSelection(caster);
|
||||
var baselineCastDelay = new NetherBoltSpell(caster).GetCastDelay();
|
||||
spell.FinishSelection(weapon, AosWeaponAttribute.HitLightning);
|
||||
|
||||
Assert.Null(caster.Spell);
|
||||
Assert.Equal(SpellState.None, spell.State);
|
||||
Assert.True(EnchantSpell.IsEnchanted(weapon));
|
||||
Assert.Equal(60, EnchantSpell.GetHitSpellBonus(weapon, AosWeaponAttribute.HitLightning));
|
||||
Assert.Equal(0, weapon.WeaponAttributes.HitLightning);
|
||||
Assert.Equal(0, weapon.Attributes.SpellChanneling);
|
||||
Assert.True(EnchantSpell.ProvidesSpellChanneling(weapon, caster));
|
||||
Assert.Equal(-1, EnchantSpell.GetFasterCasting(caster));
|
||||
Assert.Equal(TimeSpan.FromSeconds(0.25), new NetherBoltSpell(caster).GetCastDelay() - baselineCastDelay);
|
||||
Assert.True(weapon.AllowEquippedCast(caster));
|
||||
Assert.Equal(94, caster.Mana);
|
||||
Assert.Equal(9, caster.Backpack.FindItemByType<SpidersSilk>().Amount);
|
||||
Assert.Equal(9, caster.Backpack.FindItemByType<MandrakeRoot>().Amount);
|
||||
Assert.Equal(9, caster.Backpack.FindItemByType<SulfurousAsh>().Amount);
|
||||
|
||||
caster.RemoveItem(weapon);
|
||||
|
||||
Assert.False(EnchantSpell.IsEnchanted(weapon));
|
||||
Assert.Equal(0, EnchantSpell.GetHitSpellBonus(weapon, AosWeaponAttribute.HitLightning));
|
||||
Assert.Equal(0, EnchantSpell.GetFasterCasting(caster));
|
||||
|
||||
weapon.Attributes.SpellChanneling = 1;
|
||||
AddReagents(caster);
|
||||
Assert.True(caster.EquipItem(weapon));
|
||||
var permanentChannelingSpell = BeginSelection(caster);
|
||||
permanentChannelingSpell.FinishSelection(weapon, AosWeaponAttribute.HitHarm);
|
||||
|
||||
Assert.False(EnchantSpell.ProvidesSpellChanneling(weapon, caster));
|
||||
Assert.Equal(-1, EnchantSpell.GetFasterCasting(caster));
|
||||
}
|
||||
finally
|
||||
{
|
||||
EnchantSpell.StopEffect(weapon);
|
||||
weapon.Delete();
|
||||
caster.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Selection_BelowThresholdDoesNotGrantTemporaryChannelingOrFasterCasting()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
Core.Expansion = Expansion.SA;
|
||||
|
||||
var caster = NewCaster(79, 120, 0);
|
||||
var weapon = new Katana();
|
||||
AddReagents(caster);
|
||||
Assert.True(caster.EquipItem(weapon));
|
||||
|
||||
try
|
||||
{
|
||||
var spell = BeginSelection(caster);
|
||||
spell.FinishSelection(weapon, AosWeaponAttribute.HitMagicArrow);
|
||||
|
||||
Assert.True(EnchantSpell.IsEnchanted(weapon));
|
||||
Assert.False(EnchantSpell.ProvidesSpellChanneling(weapon, caster));
|
||||
Assert.Equal(0, EnchantSpell.GetFasterCasting(caster));
|
||||
var expected = (int)(60 * (caster.Skills.Mysticism.Value +
|
||||
Math.Max(caster.Skills.Focus.Value, caster.Skills.Imbuing.Value)) / 240.0);
|
||||
Assert.Equal(expected, EnchantSpell.GetHitSpellBonus(weapon, AosWeaponAttribute.HitMagicArrow));
|
||||
}
|
||||
finally
|
||||
{
|
||||
EnchantSpell.StopEffect(weapon);
|
||||
weapon.Delete();
|
||||
caster.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Threshold_At80MysticismAndSupportSkillGrantsAdvancedEffects()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
Core.Expansion = Expansion.SA;
|
||||
|
||||
var caster = NewCaster(80, 80, 0);
|
||||
var weapon = new Katana();
|
||||
AddReagents(caster);
|
||||
|
||||
try
|
||||
{
|
||||
Assert.True(caster.EquipItem(weapon));
|
||||
var spell = BeginSelection(caster);
|
||||
spell.FinishSelection(weapon, AosWeaponAttribute.HitMagicArrow);
|
||||
|
||||
Assert.True(EnchantSpell.ProvidesSpellChanneling(weapon, caster));
|
||||
Assert.Equal(-1, EnchantSpell.GetFasterCasting(caster));
|
||||
}
|
||||
finally
|
||||
{
|
||||
EnchantSpell.StopEffect(weapon);
|
||||
weapon.Delete();
|
||||
caster.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckCast_RejectsExistingHitSpellAndIncompatibleEnchantments()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
Core.Expansion = Expansion.SA;
|
||||
|
||||
var caster = NewCaster();
|
||||
var weapon = new Katana();
|
||||
Assert.True(caster.EquipItem(weapon));
|
||||
|
||||
try
|
||||
{
|
||||
var spell = new EnchantSpell(caster);
|
||||
weapon.WeaponAttributes.HitFireball = 1;
|
||||
Assert.False(spell.CheckCast());
|
||||
|
||||
weapon.WeaponAttributes.HitFireball = 0;
|
||||
weapon.Consecrated = true;
|
||||
Assert.False(spell.CheckCast());
|
||||
|
||||
weapon.Consecrated = false;
|
||||
caster.Skills.Ninjitsu.Base = 120.0;
|
||||
SpecialMove.Table[caster] = new FocusAttack();
|
||||
Assert.False(spell.CheckCast());
|
||||
SpecialMove.Table.Remove(caster);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SpecialMove.Table.Remove(caster);
|
||||
weapon.Delete();
|
||||
caster.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Expiry_RemovesTemporaryStateAndCasterLifecycleBreaksEffect()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
Core.Expansion = Expansion.SA;
|
||||
|
||||
var caster = NewCaster();
|
||||
var weapon = new Katana();
|
||||
AddReagents(caster);
|
||||
Assert.True(caster.EquipItem(weapon));
|
||||
|
||||
try
|
||||
{
|
||||
var spell = BeginSelection(caster);
|
||||
spell.FinishSelection(weapon, AosWeaponAttribute.HitDispel);
|
||||
Assert.True(EnchantSpell.IsEnchanted(weapon));
|
||||
|
||||
EnchantSpell.ExpireForTests(weapon);
|
||||
Assert.False(EnchantSpell.IsEnchanted(weapon));
|
||||
Assert.Equal(0, EnchantSpell.GetHitSpellBonus(weapon, AosWeaponAttribute.HitDispel));
|
||||
Assert.Equal(0, EnchantSpell.GetFasterCasting(caster));
|
||||
|
||||
spell = BeginSelection(caster);
|
||||
spell.FinishSelection(weapon, AosWeaponAttribute.HitHarm);
|
||||
Assert.True(EnchantSpell.IsEnchanted(weapon));
|
||||
|
||||
caster.RemoveItem(weapon);
|
||||
Assert.False(EnchantSpell.IsEnchanted(weapon));
|
||||
|
||||
Assert.True(caster.EquipItem(weapon));
|
||||
spell = BeginSelection(caster);
|
||||
spell.FinishSelection(weapon, AosWeaponAttribute.HitHarm);
|
||||
Assert.True(EnchantSpell.IsEnchanted(weapon));
|
||||
|
||||
EnchantSpell.Configure();
|
||||
EventSink.InvokeLogout(caster);
|
||||
Assert.False(EnchantSpell.IsEnchanted(weapon));
|
||||
|
||||
spell = BeginSelection(caster);
|
||||
spell.FinishSelection(weapon, AosWeaponAttribute.HitHarm);
|
||||
Assert.True(EnchantSpell.IsEnchanted(weapon));
|
||||
|
||||
PlayerMobile.PlayerDeletedEvent(caster);
|
||||
Assert.False(EnchantSpell.IsEnchanted(weapon));
|
||||
Assert.Equal(0, EnchantSpell.GetHitSpellBonus(weapon, AosWeaponAttribute.HitHarm));
|
||||
}
|
||||
finally
|
||||
{
|
||||
EnchantSpell.StopEffect(weapon);
|
||||
weapon.Delete();
|
||||
caster.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnHit_UsesTemporaryHitSpellBonus()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
using var random = new PredictableRandom(0);
|
||||
Core.Expansion = Expansion.SA;
|
||||
|
||||
var caster = NewCaster();
|
||||
caster.MoveToWorld(new Point3D(6200, 520, 0), Map.Felucca);
|
||||
var weapon = new TestEnchantKatana();
|
||||
var baselineDefender = NewCombatMobile(new Point3D(6201, 520, 0));
|
||||
var enchantedDefender = NewCombatMobile(new Point3D(6201, 520, 0));
|
||||
AddReagents(caster);
|
||||
|
||||
try
|
||||
{
|
||||
Assert.True(caster.EquipItem(weapon));
|
||||
var baselineHits = baselineDefender.Hits;
|
||||
weapon.OnHit(caster, baselineDefender);
|
||||
var baselineDamage = baselineHits - baselineDefender.Hits;
|
||||
|
||||
var spell = BeginSelection(caster);
|
||||
spell.FinishSelection(weapon, AosWeaponAttribute.HitMagicArrow);
|
||||
|
||||
weapon.OnHit(caster, enchantedDefender);
|
||||
|
||||
Assert.Equal(1, baselineDamage);
|
||||
Assert.Equal(1, weapon.MagicArrowCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
EnchantSpell.StopEffect(weapon);
|
||||
weapon.Delete();
|
||||
baselineDefender.Delete();
|
||||
enchantedDefender.Delete();
|
||||
caster.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectionGumpResponse_AppliesSelectedEffect()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
Core.Expansion = Expansion.SA;
|
||||
|
||||
var caster = NewCaster();
|
||||
var weapon = new Katana();
|
||||
AddReagents(caster);
|
||||
|
||||
try
|
||||
{
|
||||
Assert.True(caster.EquipItem(weapon));
|
||||
var spell = BeginSelection(caster);
|
||||
var gump = new EnchantGump(spell, weapon);
|
||||
var relay = new RelayInfo(
|
||||
1,
|
||||
ReadOnlySpan<int>.Empty,
|
||||
ReadOnlySpan<ushort>.Empty,
|
||||
ReadOnlySpan<Range>.Empty,
|
||||
ReadOnlySpan<byte>.Empty
|
||||
);
|
||||
|
||||
gump.OnResponse(null, in relay);
|
||||
|
||||
Assert.True(EnchantSpell.IsEnchanted(weapon));
|
||||
Assert.Equal(60, EnchantSpell.GetHitSpellBonus(weapon, AosWeaponAttribute.HitLightning));
|
||||
}
|
||||
finally
|
||||
{
|
||||
EnchantSpell.StopEffect(weapon);
|
||||
weapon.Delete();
|
||||
caster.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectionCancel_DoesNotConsumeManaOrReagents()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
Core.Expansion = Expansion.SA;
|
||||
|
||||
var caster = NewCaster();
|
||||
var weapon = new Katana();
|
||||
AddReagents(caster);
|
||||
|
||||
try
|
||||
{
|
||||
Assert.True(caster.EquipItem(weapon));
|
||||
var spell = BeginSelection(caster);
|
||||
var gump = new EnchantGump(spell, weapon);
|
||||
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.False(EnchantSpell.IsEnchanted(weapon));
|
||||
Assert.Equal(100, caster.Mana);
|
||||
Assert.Equal(10, caster.Backpack.FindItemByType<SpidersSilk>().Amount);
|
||||
Assert.Equal(10, caster.Backpack.FindItemByType<MandrakeRoot>().Amount);
|
||||
Assert.Equal(10, caster.Backpack.FindItemByType<SulfurousAsh>().Amount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
EnchantSpell.StopEffect(weapon);
|
||||
weapon.Delete();
|
||||
caster.Delete();
|
||||
Core.Expansion = previousExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectionGump_CompilesWithVisibleLayout()
|
||||
{
|
||||
var caster = NewCaster();
|
||||
var weapon = new Katana();
|
||||
var spell = new EnchantSpell(caster);
|
||||
var gump = new EnchantGump(spell, weapon);
|
||||
var buffer = GC.AllocateUninitializedArray<byte>(2048);
|
||||
var writer = new SpanWriter(buffer);
|
||||
gump.Compile(ref writer);
|
||||
|
||||
Assert.True(writer.BytesWritten > 0);
|
||||
|
||||
weapon.Delete();
|
||||
caster.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 Mobile NewCombatMobile(Point3D location)
|
||||
{
|
||||
var mobile = new Mobile(World.NewMobile);
|
||||
mobile.DefaultMobileInit();
|
||||
mobile.InitStats(100, 100, 100);
|
||||
mobile.Hits = mobile.HitsMax;
|
||||
mobile.MoveToWorld(location, Map.Felucca);
|
||||
return mobile;
|
||||
}
|
||||
|
||||
private static TestEnchantSpell BeginSelection(PlayerMobile caster)
|
||||
{
|
||||
var spell = new TestEnchantSpell(caster);
|
||||
caster.Spell = spell;
|
||||
spell.State = SpellState.Sequencing;
|
||||
return spell;
|
||||
}
|
||||
|
||||
private static void AddReagents(Mobile caster)
|
||||
{
|
||||
caster.AddToBackpack(new SpidersSilk(10));
|
||||
caster.AddToBackpack(new MandrakeRoot(10));
|
||||
caster.AddToBackpack(new SulfurousAsh(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 TestEnchantKatana : Katana
|
||||
{
|
||||
public int MagicArrowCount { get; private set; }
|
||||
|
||||
public override bool CheckHit(Mobile attacker, Mobile defender) => true;
|
||||
|
||||
public override int ComputeDamage(Mobile attacker, Mobile defender) => 1;
|
||||
|
||||
public override void DoMagicArrow(Mobile attacker, Mobile defender)
|
||||
{
|
||||
MagicArrowCount++;
|
||||
base.DoMagicArrow(attacker, defender);
|
||||
}
|
||||
|
||||
public override int AbsorbDamage(Mobile attacker, Mobile defender, int damage) => damage;
|
||||
|
||||
public override void AddBlood(Mobile attacker, Mobile defender, int damage)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestEnchantSpell : EnchantSpell
|
||||
{
|
||||
public TestEnchantSpell(Mobile caster) : base(caster)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool CheckFizzle() => true;
|
||||
}
|
||||
}
|
||||
90
Projects/UOContent/Gumps/EnchantGump.cs
Normal file
90
Projects/UOContent/Gumps/EnchantGump.cs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
using Server.Network;
|
||||
using Server.Spells;
|
||||
using Server.Spells.Mysticism;
|
||||
using Server.Items;
|
||||
|
||||
namespace Server.Gumps;
|
||||
|
||||
public sealed class EnchantGump : StaticGump<EnchantGump>
|
||||
{
|
||||
private readonly EnchantSpell _spell;
|
||||
private readonly BaseWeapon _weapon;
|
||||
|
||||
public EnchantGump(EnchantSpell spell, BaseWeapon weapon) : base(20, 20)
|
||||
{
|
||||
_spell = spell;
|
||||
_weapon = weapon;
|
||||
}
|
||||
|
||||
public override bool Singleton => true;
|
||||
|
||||
public static void DisplayTo(Mobile from, EnchantSpell spell, BaseWeapon weapon)
|
||||
{
|
||||
if (from == null || from.Deleted || from.NetState == null || spell == null || spell.Caster != from ||
|
||||
from.Spell != spell || spell.State != SpellState.Sequencing || weapon == null || weapon.Deleted ||
|
||||
weapon.Parent != from)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
from.CloseGump<EnchantGump>();
|
||||
from.SendGump(new EnchantGump(spell, weapon));
|
||||
}
|
||||
|
||||
protected override void BuildLayout(ref StaticGumpBuilder builder)
|
||||
{
|
||||
const int font = 0x07FF;
|
||||
|
||||
builder.AddPage();
|
||||
builder.AddBackground(0, 0, 260, 187, 3600);
|
||||
builder.AddAlphaRegion(5, 15, 242, 170);
|
||||
builder.AddImageTiled(220, 15, 30, 162, 10464);
|
||||
|
||||
builder.AddItem(0, 3, 6882);
|
||||
builder.AddItem(-8, 170, 6880);
|
||||
builder.AddItem(185, 3, 6883);
|
||||
builder.AddItem(192, 170, 6881);
|
||||
|
||||
builder.AddHtmlLocalized(20, 22, 150, 16, 1080133, font, false, false); // Select Enchant
|
||||
|
||||
AddOption(ref builder, 20, 50, 1, 1079705); // Hit Lightning
|
||||
AddOption(ref builder, 20, 75, 2, 1079703); // Hit Fireball
|
||||
AddOption(ref builder, 20, 100, 3, 1079704); // Hit Harm
|
||||
AddOption(ref builder, 20, 125, 4, 1079706); // Hit Magic Arrow
|
||||
AddOption(ref builder, 20, 150, 5, 1079702); // Hit Dispel
|
||||
}
|
||||
|
||||
public override void OnResponse(NetState sender, in RelayInfo info)
|
||||
{
|
||||
var attribute = info.ButtonID switch
|
||||
{
|
||||
1 => AosWeaponAttribute.HitLightning,
|
||||
2 => AosWeaponAttribute.HitFireball,
|
||||
3 => AosWeaponAttribute.HitHarm,
|
||||
4 => AosWeaponAttribute.HitMagicArrow,
|
||||
5 => AosWeaponAttribute.HitDispel,
|
||||
_ => (AosWeaponAttribute?)null
|
||||
};
|
||||
|
||||
if (attribute.HasValue)
|
||||
{
|
||||
_spell.FinishSelection(_weapon, attribute.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_spell.CancelSelection();
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddOption(
|
||||
ref StaticGumpBuilder builder,
|
||||
int x,
|
||||
int y,
|
||||
int buttonId,
|
||||
int cliloc
|
||||
)
|
||||
{
|
||||
builder.AddButton(x, y, 9702, 9703, buttonId);
|
||||
builder.AddHtmlLocalized(x + 25, y, 200, 16, cliloc, 0x07FF, false, false);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ using Server.Spells.Bushido;
|
|||
using Server.Spells.Chivalry;
|
||||
using Server.Spells.Necromancy;
|
||||
using Server.Spells.Ninjitsu;
|
||||
using Server.Spells.Mysticism;
|
||||
using Server.Spells.Sixth;
|
||||
using Server.Spells.Spellweaving;
|
||||
using Server.Text;
|
||||
|
|
@ -1187,6 +1188,7 @@ public abstract partial class BaseWeapon
|
|||
public override void OnRemoved(IEntity parent)
|
||||
{
|
||||
ClearLastParryChance();
|
||||
EnchantSpell.StopEffect(this);
|
||||
|
||||
if (parent is not Mobile m)
|
||||
{
|
||||
|
|
@ -1234,9 +1236,16 @@ public abstract partial class BaseWeapon
|
|||
m.Delta(MobileDelta.WeaponDamage);
|
||||
}
|
||||
|
||||
public override void OnAfterDelete()
|
||||
{
|
||||
EnchantSpell.StopEffect(this);
|
||||
base.OnAfterDelete();
|
||||
}
|
||||
|
||||
public override void OnMapChange()
|
||||
{
|
||||
base.OnMapChange();
|
||||
EnchantSpell.StopEffect(this);
|
||||
|
||||
if ((Map == null || Map == Map.Internal) && Parent is Mobile m && ExtendedWeaponAttributes.BattleLust != 0)
|
||||
{
|
||||
|
|
@ -2291,15 +2300,20 @@ public abstract partial class BaseWeapon
|
|||
}
|
||||
|
||||
var maChance =
|
||||
(int)(AosWeaponAttributes.GetValue(attacker, AosWeaponAttribute.HitMagicArrow) * propertyBonus);
|
||||
(int)((AosWeaponAttributes.GetValue(attacker, AosWeaponAttribute.HitMagicArrow) +
|
||||
EnchantSpell.GetHitSpellBonus(this, AosWeaponAttribute.HitMagicArrow)) * propertyBonus);
|
||||
var harmChance =
|
||||
(int)(AosWeaponAttributes.GetValue(attacker, AosWeaponAttribute.HitHarm) * propertyBonus);
|
||||
(int)((AosWeaponAttributes.GetValue(attacker, AosWeaponAttribute.HitHarm) +
|
||||
EnchantSpell.GetHitSpellBonus(this, AosWeaponAttribute.HitHarm)) * propertyBonus);
|
||||
var fireballChance =
|
||||
(int)(AosWeaponAttributes.GetValue(attacker, AosWeaponAttribute.HitFireball) * propertyBonus);
|
||||
(int)((AosWeaponAttributes.GetValue(attacker, AosWeaponAttribute.HitFireball) +
|
||||
EnchantSpell.GetHitSpellBonus(this, AosWeaponAttribute.HitFireball)) * propertyBonus);
|
||||
var lightningChance =
|
||||
(int)(AosWeaponAttributes.GetValue(attacker, AosWeaponAttribute.HitLightning) * propertyBonus);
|
||||
(int)((AosWeaponAttributes.GetValue(attacker, AosWeaponAttribute.HitLightning) +
|
||||
EnchantSpell.GetHitSpellBonus(this, AosWeaponAttribute.HitLightning)) * propertyBonus);
|
||||
var dispelChance =
|
||||
(int)(AosWeaponAttributes.GetValue(attacker, AosWeaponAttribute.HitDispel) * propertyBonus);
|
||||
(int)((AosWeaponAttributes.GetValue(attacker, AosWeaponAttribute.HitDispel) +
|
||||
EnchantSpell.GetHitSpellBonus(this, AosWeaponAttribute.HitDispel)) * propertyBonus);
|
||||
|
||||
if (maChance != 0 && maChance > Utility.Random(100))
|
||||
{
|
||||
|
|
@ -3063,7 +3077,7 @@ public abstract partial class BaseWeapon
|
|||
}
|
||||
|
||||
public override bool AllowEquippedCast(Mobile from) =>
|
||||
base.AllowEquippedCast(from) || Attributes.SpellChanneling != 0;
|
||||
base.AllowEquippedCast(from) || Attributes.SpellChanneling != 0 || EnchantSpell.ProvidesSpellChanneling(this, from);
|
||||
|
||||
public virtual int GetLuckBonus() => CraftResources.GetInfo(_resource)?.AttributeInfo?.WeaponLuck ?? 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -736,6 +736,8 @@ namespace Server.Spells
|
|||
fc -= EssenceOfWindSpell.GetFCMalus(Caster);
|
||||
}
|
||||
|
||||
fc += Mysticism.EnchantSpell.GetFasterCasting(Caster);
|
||||
|
||||
if (Core.SA)
|
||||
{
|
||||
// At some point OSI added 0.25s to every spell. This makes the minimum 0.5s
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ namespace Server.Spells
|
|||
Register(677, typeof(NetherBoltSpell));
|
||||
Register(678, typeof(HealingStoneSpell));
|
||||
Register(679, typeof(PurgeMagicSpell));
|
||||
// Register(680, typeof(EnchantSpell));
|
||||
Register(680, typeof(EnchantSpell));
|
||||
// Register(681, typeof(SleepSpell));
|
||||
Register(682, typeof(EagleStrikeSpell));
|
||||
Register(683, typeof(AnimatedWeaponSpell));
|
||||
|
|
|
|||
386
Projects/UOContent/Spells/Mysticism/EnchantSpell.cs
Normal file
386
Projects/UOContent/Spells/Mysticism/EnchantSpell.cs
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ModernUO.CodeGeneratedEvents;
|
||||
using Server.Engines.BuffIcons;
|
||||
using Server.Gumps;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
using Server.Spells.Ninjitsu;
|
||||
using Server.Spells.Spellweaving;
|
||||
|
||||
namespace Server.Spells.Mysticism;
|
||||
|
||||
public class EnchantSpell : MysticSpell
|
||||
{
|
||||
private const int MaxHitSpellChance = 60;
|
||||
private const double SpellChannelingSkillThreshold = 80.0;
|
||||
|
||||
private static readonly SpellInfo _info = new(
|
||||
"Enchant",
|
||||
"In Ort Ylem",
|
||||
230,
|
||||
9022,
|
||||
Reagent.SpidersSilk,
|
||||
Reagent.MandrakeRoot,
|
||||
Reagent.SulfurousAsh
|
||||
);
|
||||
|
||||
private static readonly AosWeaponAttribute[] _hitSpellAttributes =
|
||||
[
|
||||
AosWeaponAttribute.HitMagicArrow,
|
||||
AosWeaponAttribute.HitHarm,
|
||||
AosWeaponAttribute.HitFireball,
|
||||
AosWeaponAttribute.HitLightning,
|
||||
AosWeaponAttribute.HitDispel
|
||||
];
|
||||
|
||||
private static readonly Dictionary<BaseWeapon, EnchantmentTimer> _table = new();
|
||||
private static bool _configured;
|
||||
|
||||
private TimerExecutionToken _selectionTimer;
|
||||
|
||||
public EnchantSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
|
||||
{
|
||||
}
|
||||
|
||||
public override SpellCircle Circle => SpellCircle.Second;
|
||||
|
||||
public static void Configure()
|
||||
{
|
||||
if (_configured)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_configured = true;
|
||||
EventSink.Logout += OnLogout;
|
||||
}
|
||||
|
||||
public override bool ClearHandsOnCast => false;
|
||||
|
||||
public override bool CheckCast()
|
||||
{
|
||||
if (!base.CheckCast())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Caster.Weapon is not BaseWeapon weapon || weapon is Fists)
|
||||
{
|
||||
Caster.SendLocalizedMessage(501078); // You must be holding a weapon.
|
||||
return false;
|
||||
}
|
||||
|
||||
return ValidateWeapon(weapon);
|
||||
}
|
||||
|
||||
public override void OnCast()
|
||||
{
|
||||
if (Caster.Weapon is not BaseWeapon weapon || weapon is Fists)
|
||||
{
|
||||
Caster.SendLocalizedMessage(501078); // You must be holding a weapon.
|
||||
FinishSequence();
|
||||
}
|
||||
else if (!ValidateWeapon(weapon))
|
||||
{
|
||||
FinishSequence();
|
||||
}
|
||||
else
|
||||
{
|
||||
EnchantGump.DisplayTo(Caster, this, weapon);
|
||||
Timer.StartTimer(TimeSpan.FromSeconds(30), CancelSelection, out _selectionTimer);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDisturb(DisturbType type, bool message)
|
||||
{
|
||||
Caster.CloseGump<EnchantGump>();
|
||||
_selectionTimer.Cancel();
|
||||
base.OnDisturb(type, message);
|
||||
}
|
||||
|
||||
public override void FinishSequence()
|
||||
{
|
||||
_selectionTimer.Cancel();
|
||||
Caster.CloseGump<EnchantGump>();
|
||||
base.FinishSequence();
|
||||
}
|
||||
|
||||
internal void FinishSelection(BaseWeapon weapon, AosWeaponAttribute attribute)
|
||||
{
|
||||
if (Caster.Spell != this || State != SpellState.Sequencing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (Caster.Weapon != weapon || weapon.Deleted || weapon is Fists)
|
||||
{
|
||||
Caster.SendLocalizedMessage(501078); // You must be holding a weapon.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ValidateWeapon(weapon))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CheckSequence())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var value = GetHitSpellChance(Caster);
|
||||
var duration = GetDuration(attribute);
|
||||
var grantsAdvancedEffects =
|
||||
Caster.Skills.Mysticism.Value >= SpellChannelingSkillThreshold &&
|
||||
Math.Max(Caster.Skills.Imbuing.Value, Caster.Skills.Focus.Value) >= SpellChannelingSkillThreshold;
|
||||
var grantsSpellChanneling = grantsAdvancedEffects && weapon.Attributes.SpellChanneling == 0;
|
||||
|
||||
var timer = new EnchantmentTimer(
|
||||
Caster,
|
||||
weapon,
|
||||
attribute,
|
||||
value,
|
||||
grantsSpellChanneling,
|
||||
grantsAdvancedEffects,
|
||||
duration
|
||||
);
|
||||
_table[weapon] = timer;
|
||||
timer.Start();
|
||||
|
||||
Caster.PlaySound(0x64E);
|
||||
Caster.FixedEffect(0x36CB, 1, 9, 1915, 0);
|
||||
weapon.InvalidateProperties();
|
||||
|
||||
if (Caster is PlayerMobile player)
|
||||
{
|
||||
player.AddBuff(
|
||||
new BuffInfo(
|
||||
BuffIcon.Enchant,
|
||||
1080126,
|
||||
GetBuffCliloc(attribute),
|
||||
duration,
|
||||
$"{Caster.Name}\t{value}"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
FinishSequence();
|
||||
}
|
||||
}
|
||||
|
||||
internal void CancelSelection()
|
||||
{
|
||||
if (Caster.Spell == this && State == SpellState.Sequencing)
|
||||
{
|
||||
Caster.SendLocalizedMessage(1080132); // You decide not to enchant your weapon.
|
||||
FinishSequence();
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectionTimer.Cancel();
|
||||
Caster.CloseGump<EnchantGump>();
|
||||
}
|
||||
}
|
||||
|
||||
internal static int GetHitSpellChance(Mobile caster)
|
||||
{
|
||||
var skillTotal = GetBaseSkill(caster) + Math.Max(caster.Skills.Imbuing.Value, caster.Skills.Focus.Value);
|
||||
return Math.Clamp((int)(MaxHitSpellChance * skillTotal / 240.0), 0, MaxHitSpellChance);
|
||||
}
|
||||
|
||||
internal static TimeSpan GetDuration(AosWeaponAttribute attribute) =>
|
||||
attribute switch
|
||||
{
|
||||
// Publish 65 states that duration scales with the selected hit spell's level.
|
||||
// The issue does not provide an official per-level table, so keep the chosen
|
||||
// deterministic current-SA policy explicit and capped at the 150-second maximum.
|
||||
AosWeaponAttribute.HitMagicArrow => TimeSpan.FromSeconds(30),
|
||||
AosWeaponAttribute.HitHarm => TimeSpan.FromSeconds(60),
|
||||
AosWeaponAttribute.HitFireball => TimeSpan.FromSeconds(90),
|
||||
AosWeaponAttribute.HitLightning => TimeSpan.FromSeconds(120),
|
||||
AosWeaponAttribute.HitDispel => TimeSpan.FromSeconds(150),
|
||||
_ => TimeSpan.Zero
|
||||
};
|
||||
|
||||
internal static int GetHitSpellBonus(BaseWeapon weapon, AosWeaponAttribute attribute) =>
|
||||
_table.TryGetValue(weapon, out var timer) && timer.Attribute == attribute ? timer.Value : 0;
|
||||
|
||||
internal static bool ProvidesSpellChanneling(BaseWeapon weapon, Mobile caster) =>
|
||||
_table.TryGetValue(weapon, out var timer) && timer.Caster == caster && timer.Weapon.Parent == caster &&
|
||||
timer.GrantsSpellChanneling;
|
||||
|
||||
internal static int GetFasterCasting(Mobile caster)
|
||||
{
|
||||
foreach (var timer in _table.Values)
|
||||
{
|
||||
if (timer.Caster == caster && timer.Weapon?.Deleted == false && timer.Weapon.Parent == caster)
|
||||
{
|
||||
return timer.GrantsFasterCasting ? -1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static bool IsEnchanted(BaseWeapon weapon) => weapon != null && _table.ContainsKey(weapon);
|
||||
|
||||
public static void StopEffect(BaseWeapon weapon) => StopEffect(weapon, false);
|
||||
|
||||
internal static void ExpireForTests(BaseWeapon weapon) => StopEffect(weapon, true);
|
||||
|
||||
private static void StopEffect(BaseWeapon weapon, bool expired)
|
||||
{
|
||||
if (weapon == null || !_table.Remove(weapon, out var timer))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
timer.Stop();
|
||||
var caster = timer.Caster;
|
||||
|
||||
if (expired && caster?.Deleted == false)
|
||||
{
|
||||
caster.SendLocalizedMessage(1115273); // The enchantment on your weapon has expired.
|
||||
caster.PlaySound(0x1E6);
|
||||
}
|
||||
|
||||
(caster as PlayerMobile)?.RemoveBuff(BuffIcon.Enchant);
|
||||
timer.ClearReferences();
|
||||
|
||||
if (!weapon.Deleted)
|
||||
{
|
||||
weapon.InvalidateProperties();
|
||||
}
|
||||
}
|
||||
|
||||
[OnEvent(nameof(PlayerMobile.PlayerDeathEvent))]
|
||||
[OnEvent(nameof(PlayerMobile.PlayerDeletedEvent))]
|
||||
[OnEvent(nameof(BaseCreature.CreatureDeathEvent))]
|
||||
[OnEvent(nameof(BaseCreature.CreatureDeletedEvent))]
|
||||
public static void OnCasterRemoved(Mobile caster)
|
||||
{
|
||||
var weapons = new List<BaseWeapon>();
|
||||
|
||||
foreach (var pair in _table)
|
||||
{
|
||||
if (pair.Value.Caster == caster)
|
||||
{
|
||||
weapons.Add(pair.Key);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < weapons.Count; i++)
|
||||
{
|
||||
StopEffect(weapons[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnLogout(Mobile caster) => OnCasterRemoved(caster);
|
||||
|
||||
private static EnchantmentTimer FindEffect(Mobile caster)
|
||||
{
|
||||
foreach (var timer in _table.Values)
|
||||
{
|
||||
if (timer.Caster == caster)
|
||||
{
|
||||
return timer;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool ValidateWeapon(BaseWeapon weapon)
|
||||
{
|
||||
if (weapon == null || weapon.Deleted || weapon is Fists)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (FindEffect(Caster) != null || _table.ContainsKey(weapon))
|
||||
{
|
||||
Caster.SendLocalizedMessage(501775); // You already have an enchantment on a weapon.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (weapon.Cursed || weapon.Consecrated || ImmolatingWeaponSpell.IsImmolating(weapon))
|
||||
{
|
||||
Caster.SendLocalizedMessage(1080128); // You cannot enchant that weapon.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (weapon.Parent is Mobile wielder && SpecialMove.GetCurrentMove(wielder) is FocusAttack)
|
||||
{
|
||||
Caster.SendLocalizedMessage(1080446); // You cannot enchant your weapon while using Focus Attack.
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < _hitSpellAttributes.Length; i++)
|
||||
{
|
||||
if (weapon.WeaponAttributes[_hitSpellAttributes[i]] > 0)
|
||||
{
|
||||
Caster.SendLocalizedMessage(1080127); // That weapon already has a hit spell.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int GetBuffCliloc(AosWeaponAttribute attribute) =>
|
||||
attribute switch
|
||||
{
|
||||
AosWeaponAttribute.HitLightning => 1060423,
|
||||
AosWeaponAttribute.HitFireball => 1060420,
|
||||
AosWeaponAttribute.HitHarm => 1060421,
|
||||
AosWeaponAttribute.HitMagicArrow => 1060426,
|
||||
AosWeaponAttribute.HitDispel => 1060417,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
private sealed class EnchantmentTimer : Timer
|
||||
{
|
||||
public EnchantmentTimer(
|
||||
Mobile caster,
|
||||
BaseWeapon weapon,
|
||||
AosWeaponAttribute attribute,
|
||||
int value,
|
||||
bool grantsSpellChanneling,
|
||||
bool grantsFasterCasting,
|
||||
TimeSpan duration
|
||||
) : base(duration)
|
||||
{
|
||||
Caster = caster;
|
||||
Weapon = weapon;
|
||||
Attribute = attribute;
|
||||
Value = value;
|
||||
GrantsSpellChanneling = grantsSpellChanneling;
|
||||
GrantsFasterCasting = grantsFasterCasting;
|
||||
}
|
||||
|
||||
public Mobile Caster { get; private set; }
|
||||
|
||||
public BaseWeapon Weapon { get; private set; }
|
||||
|
||||
public AosWeaponAttribute Attribute { get; }
|
||||
|
||||
public int Value { get; }
|
||||
|
||||
public bool GrantsSpellChanneling { get; }
|
||||
|
||||
public bool GrantsFasterCasting { get; }
|
||||
|
||||
protected override void OnTick() => StopEffect(Weapon, true);
|
||||
|
||||
public void ClearReferences()
|
||||
{
|
||||
Caster = null;
|
||||
Weapon = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue