feat: Adds optimized dynamic/static layout gumps (#1652)
# New Gump API We are pleased to release a new API that is faster, allocates nearly zero memory, and still feels very similar to the original API. The API is broken into 3 types of gumps, dynamic, static with placeholders, and static without placeholders. ### Dynamic Gumps These gumps will inherit `DynamicGump` and are meant for gumps that have a dynamic layout. This includes specifying dynamic arguments to HtmlLocalized entries. ## Static Gumps Static gumps are those where the function to the build the layout is called only once and cached forever. They can optionally have placeholders. These placeholders allow the developer to specify the string values later, dynamically in a `BuildStrings` method on the gump. If a gump does not have any placeholders, the string entries will also be cached forever. ## Benchmarks To make sure we were going in the right direction and not wasting time, we took copious benchmarks. Here are the final benchmarks for a really simple gump. Note: * The majority of creating a gump is compressing the layout and the strings. Compressing each section takes ~6,000ns (12us total). ```cs | Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | |------------------------------- |-------------:|-------------:|-------------:|-------------:|------:|--------:|-------:|----------:|------------:| | OldGump | 13,308.29 ns | 1,059.695 ns | 1,883.608 ns | 14,330.72 ns | 1.000 | 0.00 | 0.1526 | 2400 B | 1.00 | | DynamicLayoutGump | 13,357.86 ns | 129.144 ns | 226.185 ns | 13,323.60 ns | 1.029 | 0.17 | - | 48 B | 0.02 | | StaticLayoutDynamicStringsGump | 6,653.10 ns | 81.815 ns | 143.292 ns | 6,617.45 ns | 0.514 | 0.09 | - | 40 B | 0.02 | | StaticLayoutGump | 86.33 ns | 0.760 ns | 1.350 ns | 86.07 ns | 0.007 | 0.00 | 0.0020 | 32 B | 0.01 | ``` # Non-Breaking Changes * All gump components in the core have been moved to `Gumps/Legacy`. * All legacy gumps will still inherit `Gump`, which now inherits `BaseGump` # Special Thanks Thank you to @stefanomerotta for considerable contributions/benchmarking/testing to make this effort a reality! We collectively went through over 10 iterations, but it is finally ready.
This commit is contained in:
parent
1c10b6dbed
commit
65532ea887
46 changed files with 2527 additions and 273 deletions
|
|
@ -952,10 +952,7 @@ public class AdvancedSearchGump : Gump
|
|||
|
||||
public void Resend(Mobile m)
|
||||
{
|
||||
TextEntries = 0;
|
||||
Switches = 0;
|
||||
Entries.Clear();
|
||||
Strings.Clear();
|
||||
Reset();
|
||||
Build(m);
|
||||
m.SendGump(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,88 +1,88 @@
|
|||
using Server.Mobiles;
|
||||
using Server.Network;
|
||||
|
||||
namespace Server.Gumps
|
||||
namespace Server.Gumps;
|
||||
|
||||
public class PetResurrectGump : StaticGump<PetResurrectGump>
|
||||
{
|
||||
public class PetResurrectGump : Gump
|
||||
private readonly double _hitsScalar;
|
||||
private readonly BaseCreature _pet;
|
||||
|
||||
public PetResurrectGump(Mobile from, BaseCreature pet, double hitsScalar = 0.0) : base(50, 50)
|
||||
{
|
||||
private readonly double m_HitsScalar;
|
||||
private readonly BaseCreature m_Pet;
|
||||
from.CloseGump<PetResurrectGump>();
|
||||
|
||||
public PetResurrectGump(Mobile from, BaseCreature pet, double hitsScalar = 0.0) : base(50, 50)
|
||||
_pet = pet;
|
||||
_hitsScalar = hitsScalar;
|
||||
}
|
||||
|
||||
protected override void BuildLayout(ref StaticGumpBuilder builder)
|
||||
{
|
||||
builder.AddPage();
|
||||
|
||||
builder.AddBackground(10, 10, 265, 140, 0x242C);
|
||||
|
||||
builder.AddItem(205, 40, 0x4);
|
||||
builder.AddItem(227, 40, 0x5);
|
||||
|
||||
builder.AddItem(180, 78, 0xCAE);
|
||||
builder.AddItem(195, 90, 0xCAD);
|
||||
builder.AddItem(218, 95, 0xCB0);
|
||||
|
||||
// <div align=center>Wilt thou sanctify the resurrection of:</div>
|
||||
builder.AddHtmlLocalized(30, 30, 150, 75, 1049665);
|
||||
builder.AddHtmlPlaceholder(30, 70, 150, 25, "petName", true);
|
||||
|
||||
builder.AddButton(40, 105, 0x81A, 0x81B, 0x1); // Okay
|
||||
builder.AddButton(110, 105, 0x819, 0x818, 0x2); // Cancel
|
||||
}
|
||||
|
||||
protected override void BuildStrings(ref GumpStringsBuilder builder)
|
||||
{
|
||||
builder.SetStringSlot("petName", $"<CENTER>{_pet.Name}</CENTER>");
|
||||
}
|
||||
|
||||
public override void OnResponse(NetState state, in RelayInfo info)
|
||||
{
|
||||
if (_pet.Deleted || !_pet.IsBonded || !_pet.IsDeadPet)
|
||||
{
|
||||
from.CloseGump<PetResurrectGump>();
|
||||
|
||||
m_Pet = pet;
|
||||
m_HitsScalar = hitsScalar;
|
||||
|
||||
AddPage(0);
|
||||
|
||||
AddBackground(10, 10, 265, 140, 0x242C);
|
||||
|
||||
AddItem(205, 40, 0x4);
|
||||
AddItem(227, 40, 0x5);
|
||||
|
||||
AddItem(180, 78, 0xCAE);
|
||||
AddItem(195, 90, 0xCAD);
|
||||
AddItem(218, 95, 0xCB0);
|
||||
|
||||
AddHtmlLocalized(30, 30, 150, 75, 1049665); // <div align=center>Wilt thou sanctify the resurrection of:</div>
|
||||
AddHtml(30, 70, 150, 25, $"<div align=CENTER>{pet.Name}</div>", true);
|
||||
|
||||
AddButton(40, 105, 0x81A, 0x81B, 0x1); // Okay
|
||||
AddButton(110, 105, 0x819, 0x818, 0x2); // Cancel
|
||||
return;
|
||||
}
|
||||
|
||||
public override void OnResponse(NetState state, in RelayInfo info)
|
||||
var from = state.Mobile;
|
||||
|
||||
if (info.ButtonID != 1)
|
||||
{
|
||||
if (m_Pet.Deleted || !m_Pet.IsBonded || !m_Pet.IsDeadPet)
|
||||
{
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var from = state.Mobile;
|
||||
if (_pet.Map?.CanFit(_pet.Location, 16, false, false) != true)
|
||||
{
|
||||
from.SendLocalizedMessage(503256); // You fail to resurrect the creature.
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.ButtonID == 1)
|
||||
{
|
||||
if (m_Pet.Map?.CanFit(m_Pet.Location, 16, false, false) != true)
|
||||
{
|
||||
from.SendLocalizedMessage(503256); // You fail to resurrect the creature.
|
||||
return;
|
||||
}
|
||||
if (_pet.Region?.IsPartOf("Khaldun") == true) // TODO: Confirm for pets, as per Bandage's script.
|
||||
{
|
||||
// The veil of death in this area is too strong and resists thy efforts to restore life.
|
||||
from.SendLocalizedMessage(1010395);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_Pet.Region?.IsPartOf("Khaldun") == true) // TODO: Confirm for pets, as per Bandage's script.
|
||||
{
|
||||
from.SendLocalizedMessage(
|
||||
1010395
|
||||
); // The veil of death in this area is too strong and resists thy efforts to restore life.
|
||||
return;
|
||||
}
|
||||
_pet.PlaySound(0x214);
|
||||
_pet.FixedEffect(0x376A, 10, 16);
|
||||
_pet.ResurrectPet();
|
||||
|
||||
m_Pet.PlaySound(0x214);
|
||||
m_Pet.FixedEffect(0x376A, 10, 16);
|
||||
m_Pet.ResurrectPet();
|
||||
var decreaseAmount = from == _pet.ControlMaster ? 0.1 : 0.2;
|
||||
|
||||
double decreaseAmount;
|
||||
for (var i = 0; i < _pet.Skills.Length; ++i) // Decrease all skills on pet.
|
||||
{
|
||||
_pet.Skills[i].Base -= decreaseAmount;
|
||||
}
|
||||
|
||||
if (from == m_Pet.ControlMaster)
|
||||
{
|
||||
decreaseAmount = 0.1;
|
||||
}
|
||||
else
|
||||
{
|
||||
decreaseAmount = 0.2;
|
||||
}
|
||||
|
||||
for (var i = 0; i < m_Pet.Skills.Length; ++i) // Decrease all skills on pet.
|
||||
{
|
||||
m_Pet.Skills[i].Base -= decreaseAmount;
|
||||
}
|
||||
|
||||
if (!m_Pet.IsDeadPet && m_HitsScalar > 0)
|
||||
{
|
||||
m_Pet.Hits = (int)(m_Pet.HitsMax * m_HitsScalar);
|
||||
}
|
||||
}
|
||||
if (!_pet.IsDeadPet && _hitsScalar > 0)
|
||||
{
|
||||
_pet.Hits = (int)(_pet.HitsMax * _hitsScalar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -350,7 +350,7 @@ public static class IncomingPlayerPackets
|
|||
var typeId = reader.ReadInt32();
|
||||
var buttonId = reader.ReadInt32();
|
||||
|
||||
Gump gump = null;
|
||||
BaseGump baseGump = null;
|
||||
|
||||
foreach (var g in state.Gumps)
|
||||
{
|
||||
|
|
@ -359,61 +359,58 @@ public static class IncomingPlayerPackets
|
|||
continue;
|
||||
}
|
||||
|
||||
gump = g;
|
||||
baseGump = g;
|
||||
break;
|
||||
}
|
||||
|
||||
if (gump != null)
|
||||
if (baseGump != null)
|
||||
{
|
||||
var buttonExists = buttonId == 0; // 0 is always 'close'
|
||||
|
||||
if (!buttonExists)
|
||||
if (baseGump is Gump gump)
|
||||
{
|
||||
foreach (var e in gump.Entries)
|
||||
{
|
||||
if (e is GumpButton button && button.ButtonID == buttonId)
|
||||
{
|
||||
buttonExists = true;
|
||||
break;
|
||||
}
|
||||
var buttonExists = buttonId == 0; // 0 is always 'close'
|
||||
|
||||
if (e is GumpImageTileButton tileButton && tileButton.ButtonID == buttonId)
|
||||
if (!buttonExists)
|
||||
{
|
||||
foreach (var e in gump.Entries)
|
||||
{
|
||||
buttonExists = true;
|
||||
break;
|
||||
if ((e as GumpButton)?.ButtonID == buttonId)
|
||||
{
|
||||
buttonExists = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ((e as GumpImageTileButton)?.ButtonID == buttonId)
|
||||
{
|
||||
buttonExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!buttonExists)
|
||||
{
|
||||
state.LogInfo("Invalid gump response, disconnecting...");
|
||||
var exception = new InvalidGumpResponseException($"Button {buttonId} doesn't exist");
|
||||
exception.SetStackTrace(new StackTrace());
|
||||
NetState.TraceException(exception);
|
||||
state.Mobile?.SendMessage("Invalid gump response.");
|
||||
|
||||
// state.Disconnect("Invalid gump response.");
|
||||
return;
|
||||
if (!buttonExists)
|
||||
{
|
||||
state.LogInfo("Invalid gump response, disconnecting...");
|
||||
var exception = new InvalidGumpResponseException($"Button {buttonId} doesn't exist");
|
||||
exception.SetStackTrace(new StackTrace());
|
||||
NetState.TraceException(exception);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var switchCount = reader.ReadInt32();
|
||||
|
||||
if (switchCount < 0 || switchCount > gump.Switches)
|
||||
if (switchCount < 0 || switchCount > baseGump.Switches)
|
||||
{
|
||||
state.LogInfo("Invalid gump response, disconnecting...");
|
||||
var exception = new InvalidGumpResponseException($"Bad switch count {switchCount}");
|
||||
exception.SetStackTrace(new StackTrace());
|
||||
NetState.TraceException(exception);
|
||||
state.Mobile?.SendMessage("Invalid gump response.");
|
||||
|
||||
// state.Disconnect("Invalid gump response.");
|
||||
return;
|
||||
}
|
||||
|
||||
int switchByteCount = switchCount * 4;
|
||||
|
||||
// Read in all of the integers
|
||||
// Read all the integers
|
||||
ReadOnlySpan<int> switchBlock =
|
||||
MemoryMarshal.Cast<byte, int>(reader.Buffer.Slice(reader.Position, switchByteCount));
|
||||
|
||||
|
|
@ -434,15 +431,12 @@ public static class IncomingPlayerPackets
|
|||
}
|
||||
|
||||
var textCount = reader.ReadInt32();
|
||||
if (textCount < 0 || textCount > gump.TextEntries)
|
||||
if (textCount < 0 || textCount > baseGump.TextEntries)
|
||||
{
|
||||
state.LogInfo("Invalid gump response, disconnecting...");
|
||||
var exception = new InvalidGumpResponseException($"Bad text entry count {textCount}");
|
||||
exception.SetStackTrace(new StackTrace());
|
||||
NetState.TraceException(exception);
|
||||
state.Mobile?.SendMessage("Invalid gump response.");
|
||||
|
||||
// state.Disconnect("Invalid gump response.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -461,9 +455,6 @@ public static class IncomingPlayerPackets
|
|||
var exception = new InvalidGumpResponseException($"Text entry {i} is too long ({textLength})");
|
||||
exception.SetStackTrace(new StackTrace());
|
||||
NetState.TraceException(exception);
|
||||
state.Mobile?.SendMessage("Invalid gump response.");
|
||||
|
||||
// state.Disconnect("Invalid gump response.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -476,9 +467,9 @@ public static class IncomingPlayerPackets
|
|||
|
||||
var textBlock = reader.Buffer.Slice(textOffset, reader.Position - textOffset);
|
||||
|
||||
state.RemoveGump(gump);
|
||||
state.RemoveGump(baseGump);
|
||||
|
||||
var prof = GumpProfile.Acquire(gump.GetType());
|
||||
var prof = GumpProfile.Acquire(baseGump.GetType());
|
||||
|
||||
prof?.Start();
|
||||
|
||||
|
|
@ -489,7 +480,7 @@ public static class IncomingPlayerPackets
|
|||
textFields,
|
||||
textBlock
|
||||
);
|
||||
gump.OnResponse(state, relayInfo);
|
||||
baseGump.OnResponse(state, relayInfo);
|
||||
|
||||
prof?.Finish();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -380,56 +380,59 @@ namespace Server.Spells.Ninjitsu
|
|||
public bool StealingBonus { get; }
|
||||
}
|
||||
|
||||
public class AnimalFormGump : Gump
|
||||
public class AnimalFormGump : DynamicGump
|
||||
{
|
||||
// TODO: Convert this for ML to the BaseImageTileButtonsGump
|
||||
private readonly Mobile m_Caster;
|
||||
private readonly AnimalForm m_Spell;
|
||||
private readonly Mobile _caster;
|
||||
private readonly AnimalForm _spell;
|
||||
private readonly AnimalFormEntry[] _entries;
|
||||
|
||||
public AnimalFormGump(Mobile caster, AnimalFormEntry[] entries, AnimalForm spell)
|
||||
: base(50, 50)
|
||||
public AnimalFormGump(Mobile caster, AnimalFormEntry[] entries, AnimalForm spell) : base(50, 50)
|
||||
{
|
||||
m_Caster = caster;
|
||||
m_Spell = spell;
|
||||
_caster = caster;
|
||||
_spell = spell;
|
||||
_entries = entries;
|
||||
}
|
||||
|
||||
AddPage(0);
|
||||
protected override void BuildLayout(ref DynamicGumpBuilder builder)
|
||||
{
|
||||
builder.AddPage();
|
||||
|
||||
AddBackground(0, 0, 520, 404, 0x13BE);
|
||||
AddImageTiled(10, 10, 500, 20, 0xA40);
|
||||
AddImageTiled(10, 40, 500, 324, 0xA40);
|
||||
AddImageTiled(10, 374, 500, 20, 0xA40);
|
||||
AddAlphaRegion(10, 10, 500, 384);
|
||||
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);
|
||||
|
||||
AddHtmlLocalized(14, 12, 500, 20, 1063394, 0x7FFF); // <center>Polymorph Selection Menu</center>
|
||||
builder.AddHtmlLocalized(14, 12, 500, 20, 1063394, 0x7FFF); // <center>Polymorph Selection Menu</center>
|
||||
|
||||
AddButton(10, 374, 0xFB1, 0xFB2, 0);
|
||||
AddHtmlLocalized(45, 376, 450, 20, 1011012, 0x7FFF); // CANCEL
|
||||
builder.AddButton(10, 374, 0xFB1, 0xFB2, 0);
|
||||
builder.AddHtmlLocalized(45, 376, 450, 20, 1011012, 0x7FFF); // CANCEL
|
||||
|
||||
var ninjitsu = caster.Skills.Ninjitsu.Value;
|
||||
int ninjitsu = _caster.Skills[SkillName.Ninjitsu].Fixed;
|
||||
int current = 0;
|
||||
|
||||
var current = 0;
|
||||
|
||||
for (var i = 0; i < entries.Length; ++i)
|
||||
for (int i = 0; i < _entries.Length; ++i)
|
||||
{
|
||||
var enabled = ninjitsu >= entries[i].ReqSkill && BaseFormTalisman.EntryEnabled(caster, entries[i].Type);
|
||||
bool enabled = ninjitsu >= _entries[i].ReqSkill && BaseFormTalisman.EntryEnabled(_caster, _entries[i].Type);
|
||||
|
||||
var page = current / 10 + 1;
|
||||
var pos = current % 10;
|
||||
int page = current / 10 + 1;
|
||||
int pos = current % 10;
|
||||
|
||||
if (pos == 0)
|
||||
{
|
||||
if (page > 1)
|
||||
{
|
||||
AddButton(400, 374, 0xFA5, 0xFA7, 0, GumpButtonType.Page, page);
|
||||
AddHtmlLocalized(440, 376, 60, 20, 1043353, 0x7FFF); // Next
|
||||
builder.AddButton(400, 374, 0xFA5, 0xFA7, 0, GumpButtonType.Page, page);
|
||||
builder.AddHtmlLocalized(440, 376, 60, 20, 1043353, 0x7FFF); // Next
|
||||
}
|
||||
|
||||
AddPage(page);
|
||||
builder.AddPage(page);
|
||||
|
||||
if (page > 1)
|
||||
{
|
||||
AddButton(300, 374, 0xFAE, 0xFB0, 0, GumpButtonType.Page, 1);
|
||||
AddHtmlLocalized(340, 376, 60, 20, 1011393, 0x7FFF); // Back
|
||||
builder.AddButton(300, 374, 0xFAE, 0xFB0, 0, GumpButtonType.Page, 1);
|
||||
builder.AddHtmlLocalized(340, 376, 60, 20, 1011393, 0x7FFF); // Back
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -438,26 +441,16 @@ namespace Server.Spells.Ninjitsu
|
|||
continue;
|
||||
}
|
||||
|
||||
var x = pos % 2 == 0 ? 14 : 264;
|
||||
var y = pos / 2 * 64 + 44;
|
||||
AnimalFormEntry entry = _entries[i];
|
||||
|
||||
var b = ItemBounds.Table[entries[i].ItemID];
|
||||
int y = Math.DivRem(pos, 2, out var rem) * 64 + 44;
|
||||
int x = rem == 0 ? 14 : 264;
|
||||
Rectangle2D b = ItemBounds.Table[entry.ItemID];
|
||||
|
||||
AddImageTiledButton(
|
||||
x,
|
||||
y,
|
||||
0x918,
|
||||
0x919,
|
||||
i + 1,
|
||||
GumpButtonType.Reply,
|
||||
0,
|
||||
entries[i].ItemID,
|
||||
entries[i].Hue,
|
||||
40 - b.Width / 2 - b.X,
|
||||
30 - b.Height / 2 - b.Y
|
||||
);
|
||||
AddTooltip(entries[i].Tooltip);
|
||||
AddHtmlLocalized(x + 84, y, 250, 60, entries[i].Name, 0x7FFF);
|
||||
builder.AddImageTiledButton(x, y, 0x918, 0x919, i + 1, GumpButtonType.Reply, 0, entry.ItemID,
|
||||
entry.Hue, 40 - b.Width / 2 - b.X, 30 - b.Height / 2 - b.Y, entry.Tooltip);
|
||||
|
||||
builder.AddHtmlLocalized(x + 84, y, 250, 60, entry.Name, 0x7FFF);
|
||||
|
||||
current++;
|
||||
}
|
||||
|
|
@ -467,39 +460,40 @@ namespace Server.Spells.Ninjitsu
|
|||
{
|
||||
var entryID = info.ButtonID - 1;
|
||||
|
||||
if (entryID < 0 || entryID >= AnimalForm.Entries.Length)
|
||||
if (entryID < 0 || entryID >= Entries.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var mana = m_Spell.ScaleMana(m_Spell.RequiredMana);
|
||||
var entry = AnimalForm.Entries[entryID];
|
||||
var mana = _spell.ScaleMana(_spell.RequiredMana);
|
||||
var entry = Entries[entryID];
|
||||
|
||||
if (mana > m_Caster.Mana)
|
||||
if (!BaseFormTalisman.EntryEnabled(sender.Mobile, entry.Type))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (mana > _caster.Mana)
|
||||
{
|
||||
// You must have at least ~1_MANA_REQUIREMENT~ Mana to use this ability.
|
||||
m_Caster.SendLocalizedMessage(1060174, mana.ToString());
|
||||
_caster.SendLocalizedMessage(1060174, mana.ToString());
|
||||
}
|
||||
else if (m_Caster is PlayerMobile mobile && mobile.MountBlockReason != BlockMountType.None)
|
||||
else if (_caster is PlayerMobile mobile
|
||||
&& (mobile.MountBlockReason != BlockMountType.None
|
||||
|| mobile.DuelContext?.AllowSpellCast(_caster, _spell) == false))
|
||||
{
|
||||
mobile.SendLocalizedMessage(1063108); // You cannot use this ability right now.
|
||||
_caster.SendLocalizedMessage(1063108); // You cannot use this ability right now.
|
||||
}
|
||||
else if (BaseFormTalisman.EntryEnabled(sender.Mobile, entry.Type))
|
||||
else if (Morph(_caster, entryID) == MorphResult.Fail)
|
||||
{
|
||||
if ((m_Caster as PlayerMobile)?.DuelContext?.AllowSpellCast(m_Caster, m_Spell) == false)
|
||||
{
|
||||
}
|
||||
else if (Morph(m_Caster, entryID) == MorphResult.Fail)
|
||||
{
|
||||
m_Caster.LocalOverheadMessage(MessageType.Regular, 0x3B2, 502632); // The spell fizzles.
|
||||
m_Caster.FixedParticles(0x3735, 1, 30, 9503, EffectLayer.Waist);
|
||||
m_Caster.PlaySound(0x5C);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Caster.FixedParticles(0x3728, 10, 13, 2023, EffectLayer.Waist);
|
||||
m_Caster.Mana -= mana;
|
||||
}
|
||||
_caster.LocalOverheadMessage(MessageType.Regular, 0x3B2, 502632); // The spell fizzles.
|
||||
_caster.FixedParticles(0x3735, 1, 30, 9503, EffectLayer.Waist);
|
||||
_caster.PlaySound(0x5C);
|
||||
}
|
||||
else
|
||||
{
|
||||
_caster.FixedParticles(0x3728, 10, 13, 2023, EffectLayer.Waist);
|
||||
_caster.Mana -= mana;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue