From d231c06b8e96d4b4a053be07951504a56e0ed819 Mon Sep 17 00:00:00 2001 From: Hondacrx Date: Mon, 19 Aug 2024 18:48:32 -0400 Subject: [PATCH] Core/Spells: Implemented evoker empower spell mechanic Port From (https://github.com/TrinityCore/TrinityCore/commit/a39d0db9ec64f6bf38716abaade5b7835f2db338) --- Source/Framework/Constants/ScriptsConst.cs | 4 +- .../Framework/Constants/Spells/SpellConst.cs | 3 + .../Database/Databases/HotfixDatabase.cs | 11 + Source/Game/DataStorage/CliDB.cs | 6 +- Source/Game/DataStorage/Structs/S_Records.cs | 15 ++ Source/Game/Entities/Creature/Creature.cs | 1 + Source/Game/Entities/Player/Player.Fields.cs | 1 + Source/Game/Entities/Player/Player.cs | 8 + Source/Game/Entities/Unit/Unit.Spells.cs | 2 +- Source/Game/Entities/Unit/Unit.cs | 4 + Source/Game/Handlers/SpellHandler.cs | 36 +++ .../Game/Networking/Packets/SpellPackets.cs | 123 ++++++++- Source/Game/Scripting/SpellScript.cs | 26 ++ Source/Game/Spells/Auras/Aura.cs | 10 +- Source/Game/Spells/Spell.cs | 242 +++++++++++++++--- Source/Game/Spells/SpellInfo.cs | 11 + Source/Game/Spells/SpellManager.cs | 109 ++++---- Source/Scripts/Spells/Evoker.cs | 93 +++++++ 18 files changed, 616 insertions(+), 89 deletions(-) diff --git a/Source/Framework/Constants/ScriptsConst.cs b/Source/Framework/Constants/ScriptsConst.cs index 455cf18b6..66e66f06d 100644 --- a/Source/Framework/Constants/ScriptsConst.cs +++ b/Source/Framework/Constants/ScriptsConst.cs @@ -34,7 +34,9 @@ namespace Framework.Constants CalcDamage, CalcHealing, OnPrecast, - CalcCastTime + CalcCastTime, + EmpowerStageCompleted, + EmpowerCompleted } // AuraScript interface - enum used for runtime checks of script function calls diff --git a/Source/Framework/Constants/Spells/SpellConst.cs b/Source/Framework/Constants/Spells/SpellConst.cs index 21236f886..623753075 100644 --- a/Source/Framework/Constants/Spells/SpellConst.cs +++ b/Source/Framework/Constants/Spells/SpellConst.cs @@ -26,6 +26,9 @@ namespace Framework.Constants public const uint VisualKitFood = 406; public const uint VisualKitDrink = 438; + + public const uint EmpowerHoldTimeAtMax = 1 * Time.InMilliseconds; + public const uint EmpowerHardcodedGCD = 359115; } diff --git a/Source/Framework/Database/Databases/HotfixDatabase.cs b/Source/Framework/Database/Databases/HotfixDatabase.cs index 11a8e6c57..5e679d433 100644 --- a/Source/Framework/Database/Databases/HotfixDatabase.cs +++ b/Source/Framework/Database/Databases/HotfixDatabase.cs @@ -1217,6 +1217,13 @@ namespace Framework.Database "EffectRadiusIndex2, EffectSpellClassMask1, EffectSpellClassMask2, EffectSpellClassMask3, EffectSpellClassMask4, ImplicitTarget1, " + "ImplicitTarget2, SpellID FROM spell_effect WHERE (`VerifiedBuild` > 0) = ?"); + // SpellEmpower.db2 + PrepareStatement(HotfixStatements.SEL_SPELL_EMPOWER, "SELECT ID, SpellID, Unused1000 FROM spell_empower WHERE (`VerifiedBuild` > 0) = ?"); + + // SpellEmpowerStage.db2 + PrepareStatement(HotfixStatements.SEL_SPELL_EMPOWER_STAGE, "SELECT ID, Stage, DurationMs, SpellEmpowerID FROM spell_empower_stage" + + " WHERE (`VerifiedBuild` > 0) = ?"); + // SpellEquippedItems.db2 PrepareStatement(HotfixStatements.SEL_SPELL_EQUIPPED_ITEMS, "SELECT ID, SpellID, EquippedItemClass, EquippedItemInvTypes, EquippedItemSubclass" + " FROM spell_equipped_items WHERE (`VerifiedBuild` > 0) = ?"); @@ -2183,6 +2190,10 @@ namespace Framework.Database SEL_SPELL_EFFECT, + SEL_SPELL_EMPOWER, + + SEL_SPELL_EMPOWER_STAGE, + SEL_SPELL_EQUIPPED_ITEMS, SEL_SPELL_FOCUS_OBJECT, diff --git a/Source/Game/DataStorage/CliDB.cs b/Source/Game/DataStorage/CliDB.cs index 3a0e74bf3..bb85e4c30 100644 --- a/Source/Game/DataStorage/CliDB.cs +++ b/Source/Game/DataStorage/CliDB.cs @@ -303,6 +303,8 @@ namespace Game.DataStorage SpellCooldownsStorage = ReadDB2("SpellCooldowns.db2", HotfixStatements.SEL_SPELL_COOLDOWNS); SpellDurationStorage = ReadDB2("SpellDuration.db2", HotfixStatements.SEL_SPELL_DURATION); SpellEffectStorage = ReadDB2("SpellEffect.db2", HotfixStatements.SEL_SPELL_EFFECT); + SpellEmpowerStorage = ReadDB2("SpellEmpower.db2", HotfixStatements.SEL_SPELL_EMPOWER); + SpellEmpowerStageStorage = ReadDB2("SpellEmpowerStage.db2", HotfixStatements.SEL_SPELL_EMPOWER_STAGE); SpellEquippedItemsStorage = ReadDB2("SpellEquippedItems.db2", HotfixStatements.SEL_SPELL_EQUIPPED_ITEMS); SpellFocusObjectStorage = ReadDB2("SpellFocusObject.db2", HotfixStatements.SEL_SPELL_FOCUS_OBJECT, HotfixStatements.SEL_SPELL_FOCUS_OBJECT_LOCALE); SpellInterruptsStorage = ReadDB2("SpellInterrupts.db2", HotfixStatements.SEL_SPELL_INTERRUPTS); @@ -378,7 +380,7 @@ namespace Game.DataStorage UnitPowerBarStorage = ReadDB2("UnitPowerBar.db2", HotfixStatements.SEL_UNIT_POWER_BAR, HotfixStatements.SEL_UNIT_POWER_BAR_LOCALE); VehicleStorage = ReadDB2("Vehicle.db2", HotfixStatements.SEL_VEHICLE); VehicleSeatStorage = ReadDB2("VehicleSeat.db2", HotfixStatements.SEL_VEHICLE_SEAT); - VignetteStorage = ReadDB2("Vignette.db2", HotfixStatements.SEL_VIGNETTE, HotfixStatements.SEL_VIGNETTE_LOCALE); + VignetteStorage = ReadDB2("Vignette.db2", HotfixStatements.SEL_VIGNETTE, HotfixStatements.SEL_VIGNETTE_LOCALE); WMOAreaTableStorage = ReadDB2("WMOAreaTable.db2", HotfixStatements.SEL_WMO_AREA_TABLE, HotfixStatements.SEL_WMO_AREA_TABLE_LOCALE); WorldEffectStorage = ReadDB2("WorldEffect.db2", HotfixStatements.SEL_WORLD_EFFECT); WorldMapOverlayStorage = ReadDB2("WorldMapOverlay.db2", HotfixStatements.SEL_WORLD_MAP_OVERLAY); @@ -740,6 +742,8 @@ namespace Game.DataStorage public static DB6Storage SpellCooldownsStorage; public static DB6Storage SpellDurationStorage; public static DB6Storage SpellEffectStorage; + public static DB6Storage SpellEmpowerStorage; + public static DB6Storage SpellEmpowerStageStorage; public static DB6Storage SpellEquippedItemsStorage; public static DB6Storage SpellFocusObjectStorage; public static DB6Storage SpellInterruptsStorage; diff --git a/Source/Game/DataStorage/Structs/S_Records.cs b/Source/Game/DataStorage/Structs/S_Records.cs index d3dcb238d..9a8c2e600 100644 --- a/Source/Game/DataStorage/Structs/S_Records.cs +++ b/Source/Game/DataStorage/Structs/S_Records.cs @@ -327,6 +327,21 @@ namespace Game.DataStorage public uint SpellID; } + public sealed class SpellEmpowerRecord + { + public uint Id; + public int SpellID; + public int Unused1000; + } + + public sealed class SpellEmpowerStageRecord + { + public uint Id; + public int Stage; + public int DurationMs; + public uint SpellEmpowerID; + } + public sealed class SpellEquippedItemsRecord { public uint Id; diff --git a/Source/Game/Entities/Creature/Creature.cs b/Source/Game/Entities/Creature/Creature.cs index ff49709d4..50005b2cb 100644 --- a/Source/Game/Entities/Creature/Creature.cs +++ b/Source/Game/Entities/Creature/Creature.cs @@ -262,6 +262,7 @@ namespace Game.Entities SetModRangedHaste(1.0f); SetModHasteRegen(1.0f); SetModTimeRate(1.0f); + SetSpellEmpowerStage(-1); SetSpeedRate(UnitMoveType.Walk, creatureInfo.SpeedWalk); SetSpeedRate(UnitMoveType.Run, creatureInfo.SpeedRun); diff --git a/Source/Game/Entities/Player/Player.Fields.cs b/Source/Game/Entities/Player/Player.Fields.cs index c180de319..a19284efc 100644 --- a/Source/Game/Entities/Player/Player.Fields.cs +++ b/Source/Game/Entities/Player/Player.Fields.cs @@ -112,6 +112,7 @@ namespace Game.Entities ObjectGuid m_temporaryUnsummonedBattlePet; Dictionary m_storedAuraTeleportLocations = new(); SpellCastRequest _pendingSpellCastRequest; + float m_empowerMinHoldStagePercent; //Mail List m_mail = new(); diff --git a/Source/Game/Entities/Player/Player.cs b/Source/Game/Entities/Player/Player.cs index d753b03e3..2d73a6222 100644 --- a/Source/Game/Entities/Player/Player.cs +++ b/Source/Game/Entities/Player/Player.cs @@ -73,6 +73,8 @@ namespace Game.Entities m_legacyRaidDifficulty = Difficulty.Raid10N; m_InstanceValid = true; + m_empowerMinHoldStagePercent = 1.0f; + _specializationInfo = new SpecializationInfo(); for (byte i = 0; i < (byte)BaseModGroup.End; ++i) @@ -3319,6 +3321,10 @@ namespace Game.Entities SetFaction(rEntry != null ? (uint)rEntry.FactionID : 0); } + public float GetEmpowerMinHoldStagePercent() { return m_empowerMinHoldStagePercent; } + + public void SetEmpowerMinHoldStagePercent(float empowerMinHoldStagePercent) { m_empowerMinHoldStagePercent = empowerMinHoldStagePercent; } + public void SetResurrectRequestData(WorldObject caster, uint health, uint mana, uint appliedAura) { Cypher.Assert(!IsResurrectRequested()); @@ -3329,6 +3335,7 @@ namespace Game.Entities _resurrectionData.Mana = mana; _resurrectionData.Aura = appliedAura; } + public void ClearResurrectRequestData() { _resurrectionData = null; @@ -5810,6 +5817,7 @@ namespace Game.Entities SetModRangedHaste(1.0f); SetModHasteRegen(1.0f); SetModTimeRate(1.0f); + SetSpellEmpowerStage(-1); // reset size before reapply auras SetObjectScale(1.0f); diff --git a/Source/Game/Entities/Unit/Unit.Spells.cs b/Source/Game/Entities/Unit/Unit.Spells.cs index 784417c42..d2626b5af 100644 --- a/Source/Game/Entities/Unit/Unit.Spells.cs +++ b/Source/Game/Entities/Unit/Unit.Spells.cs @@ -898,7 +898,7 @@ namespace Game.Entities return; if (spellType == CurrentSpellTypes.Channeled) - spell.SendChannelUpdate(0); + spell.SendChannelUpdate(0, result); spell.Finish(result); } diff --git a/Source/Game/Entities/Unit/Unit.cs b/Source/Game/Entities/Unit/Unit.cs index fbdc928ec..bdbea8d7d 100644 --- a/Source/Game/Entities/Unit/Unit.cs +++ b/Source/Game/Entities/Unit/Unit.cs @@ -2428,6 +2428,10 @@ namespace Game.Entities RemoveDynamicUpdateFieldValue(m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.ChannelObjects), index); } + public sbyte GetSpellEmpowerStage() { return m_unitData.SpellEmpowerStage; } + + public void SetSpellEmpowerStage(sbyte stage) { SetUpdateFieldValue(m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.SpellEmpowerStage), stage); } + public static bool IsDamageReducedByArmor(SpellSchoolMask schoolMask, SpellInfo spellInfo = null) { // only physical spells damage gets reduced by armor diff --git a/Source/Game/Handlers/SpellHandler.cs b/Source/Game/Handlers/SpellHandler.cs index 61e244e13..7b40d8bca 100644 --- a/Source/Game/Handlers/SpellHandler.cs +++ b/Source/Game/Handlers/SpellHandler.cs @@ -379,6 +379,42 @@ namespace Game mover.InterruptSpell(CurrentSpellTypes.Channeled); } + [WorldPacketHandler(ClientOpcodes.SetEmpowerMinHoldStagePercent, Processing = PacketProcessing.Inplace)] + void HandleSetEmpowerMinHoldStagePercent(SetEmpowerMinHoldStagePercent setEmpowerMinHoldStagePercent) + { + _player.SetEmpowerMinHoldStagePercent(setEmpowerMinHoldStagePercent.MinHoldStagePercent); + } + + [WorldPacketHandler(ClientOpcodes.SpellEmpowerRelease, Processing = PacketProcessing.Inplace)] + void HandleSpellEmpowerRelease(SpellEmpowerRelease spellEmpowerRelease) + { + // ignore for remote control state (for player case) + Unit mover = _player.GetUnitBeingMoved(); + if (mover != _player && mover.IsPlayer()) + return; + + Spell spell = mover.GetCurrentSpell(CurrentSpellTypes.Channeled); + if (spell == null || spell.GetSpellInfo().Id != spellEmpowerRelease.SpellID || !spell.IsEmpowerSpell()) + return; + + spell.SetEmpowerReleasedByClient(true); + } + + [WorldPacketHandler(ClientOpcodes.SpellEmpowerRestart, Processing = PacketProcessing.Inplace)] + void HandleSpellEmpowerRestart(SpellEmpowerRestart spellEmpowerRestart) + { + // ignore for remote control state (for player case) + Unit mover = _player.GetUnitBeingMoved(); + if (mover != _player && mover.IsPlayer()) + return; + + Spell spell = mover.GetCurrentSpell(CurrentSpellTypes.Channeled); + if (spell == null || spell.GetSpellInfo().Id != spellEmpowerRestart.SpellID || !spell.IsEmpowerSpell()) + return; + + spell.SetEmpowerReleasedByClient(false); + } + [WorldPacketHandler(ClientOpcodes.TotemDestroyed, Processing = PacketProcessing.Inplace)] void HandleTotemDestroyed(TotemDestroyed totemDestroyed) { diff --git a/Source/Game/Networking/Packets/SpellPackets.cs b/Source/Game/Networking/Packets/SpellPackets.cs index 09d929d9c..208680ecb 100644 --- a/Source/Game/Networking/Packets/SpellPackets.cs +++ b/Source/Game/Networking/Packets/SpellPackets.cs @@ -2,7 +2,6 @@ // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. using Framework.Constants; -using Framework.Dynamic; using Game.Entities; using Game.Spells; using System; @@ -834,6 +833,128 @@ namespace Game.Networking.Packets public int TimeRemaining; } + class SpellEmpowerStart : ServerPacket + { + public ObjectGuid CastID; + public ObjectGuid CasterGUID; + public int SpellID; + public SpellCastVisual Visual; + public TimeSpan EmpowerDuration; + public TimeSpan MinHoldTime; + public TimeSpan HoldAtMaxTime; + public List Targets = new(); + public List StageDurations = new(); + public SpellChannelStartInterruptImmunities? InterruptImmunities; + public SpellTargetedHealPrediction? HealPrediction; + + public SpellEmpowerStart() : base(ServerOpcodes.SpellEmpowerStart) { } + + public override void Write() + { + _worldPacket.WritePackedGuid(CastID); + _worldPacket.WritePackedGuid(CasterGUID); + _worldPacket.WriteInt32(Targets.Count); + _worldPacket.WriteInt32(SpellID); + Visual.Write(_worldPacket); + _worldPacket.WriteUInt32((uint)EmpowerDuration.TotalMilliseconds); + _worldPacket.WriteUInt32((uint)MinHoldTime.TotalMilliseconds); + _worldPacket.WriteUInt32((uint)HoldAtMaxTime.TotalMilliseconds); + _worldPacket.WriteInt32(StageDurations.Count); + + foreach (var target in Targets) + _worldPacket.WritePackedGuid(target); + + foreach (var stageDuration in StageDurations) + _worldPacket.WriteUInt32((uint)stageDuration.TotalMilliseconds); + + _worldPacket.WriteBit(InterruptImmunities.HasValue); + _worldPacket.WriteBit(HealPrediction.HasValue); + _worldPacket.FlushBits(); + + if (InterruptImmunities.HasValue) + InterruptImmunities.Value.Write(_worldPacket); + + if (HealPrediction.HasValue) + HealPrediction.Value.Write(_worldPacket); + } + } + + class SpellEmpowerUpdate : ServerPacket + { + public ObjectGuid CastID; + public ObjectGuid CasterGUID; + public TimeSpan TimeRemaining; + public List StageDurations = new(); + public byte Status; + + public SpellEmpowerUpdate() : base(ServerOpcodes.SpellEmpowerUpdate) { } + + public override void Write() + { + _worldPacket.WritePackedGuid(CastID); + _worldPacket.WritePackedGuid(CasterGUID); + _worldPacket.WriteUInt32((uint)TimeRemaining.TotalMilliseconds); + _worldPacket.WriteInt32(StageDurations.Count); + _worldPacket.WriteUInt8(Status); + _worldPacket.FlushBits(); + + foreach (var stageDuration in StageDurations) + _worldPacket.WriteUInt32((uint)stageDuration.TotalMilliseconds); + } + } + + class SetEmpowerMinHoldStagePercent : ClientPacket + { + public float MinHoldStagePercent = 1.0f; + + public SetEmpowerMinHoldStagePercent(WorldPacket packet) : base(packet) { } + + public override void Read() + { + MinHoldStagePercent = _worldPacket.ReadFloat(); + } + } + + class SpellEmpowerRelease : ClientPacket + { + public int SpellID; + + public SpellEmpowerRelease(WorldPacket packet) : base(packet) { } + + public override void Read() + { + SpellID = _worldPacket.ReadInt32(); + } + } + + class SpellEmpowerRestart : ClientPacket + { + public int SpellID; + + public SpellEmpowerRestart(WorldPacket packet) : base(packet) { } + + public override void Read() + { + SpellID = _worldPacket.ReadInt32(); + } + } + + class SpellEmpowerSetStage : ServerPacket + { + public ObjectGuid CastID; + public ObjectGuid CasterGUID; + public int Stage; + + public SpellEmpowerSetStage() : base(ServerOpcodes.SpellEmpowerSetStage) { } + + public override void Write() + { + _worldPacket.WritePackedGuid(CastID); + _worldPacket.WritePackedGuid(CasterGUID); + _worldPacket.WriteInt32(Stage); + } + } + class ResurrectRequest : ServerPacket { public ResurrectRequest() : base(ServerOpcodes.ResurrectRequest) { } diff --git a/Source/Game/Scripting/SpellScript.cs b/Source/Game/Scripting/SpellScript.cs index 2d95b64c9..b29874c58 100644 --- a/Source/Game/Scripting/SpellScript.cs +++ b/Source/Game/Scripting/SpellScript.cs @@ -174,6 +174,7 @@ namespace Game.Scripting public delegate void SpellObjectAreaTargetSelectFnType(List targets); public delegate void SpellObjectTargetSelectFnType(ref WorldObject targets); public delegate void SpellDestinationTargetSelectFnType(ref SpellDestination dest); + public delegate void SpellEmpowerStageFnType(int completedStagesCount); public class CastHandler { @@ -232,6 +233,21 @@ namespace Game.Scripting } } + public class EmpowerStageCompletedHandler + { + SpellEmpowerStageFnType _callImpl; + + public EmpowerStageCompletedHandler(SpellEmpowerStageFnType handler) + { + _callImpl = handler; + } + + public void Call(int completedStagesCount) + { + _callImpl(completedStagesCount); + } + } + public class EffectHandler : EffectHook { SpellEffectName _effName; @@ -572,6 +588,14 @@ namespace Game.Scripting // where function is void function(DamageInfo damageInfo, ref uint resistAmount, ref int absorbAmount) public List OnCalculateResistAbsorb = new(); + // example: OnEmpowerStageCompleted += SpellOnEmpowerStageCompletedFn(class::function); + // where function is void function(int32 completedStages) + public List OnEmpowerStageCompleted = new(); + + // example: OnEmpowerCompleted += SpellOnEmpowerCompletedFn(class::function); + // where function is void function(int32 completedStages) + public List OnEmpowerCompleted = new(); + // where function is void function(uint effIndex) public List OnEffectLaunch = new(); public List OnEffectLaunchTarget = new(); @@ -621,6 +645,8 @@ namespace Game.Scripting // 14. OnEffectHitTarget - executed just before specified effect handler call - called for each target from spell target map // 15. OnHit - executed just before spell deals damage and procs auras - when spell hits target - called for each target from spell target map // 16. AfterHit - executed just after spell finishes all it's jobs for target - called for each target from spell target map + // 17. OnEmpowerStageCompleted - executed when empowered spell completes each stage + // 18. OnEmpowerCompleted - executed when empowered spell is released // // methods allowing interaction with Spell object diff --git a/Source/Game/Spells/Auras/Aura.cs b/Source/Game/Spells/Auras/Aura.cs index 7df7dc9a8..2995e5032 100644 --- a/Source/Game/Spells/Auras/Aura.cs +++ b/Source/Game/Spells/Auras/Aura.cs @@ -764,9 +764,15 @@ namespace Game.Spells maxDuration = -1; // IsPermanent() checks max duration (which we are supposed to calculate here) - if (maxDuration != -1 && modOwner != null) - modOwner.ApplySpellMod(spellInfo, SpellModOp.Duration, ref maxDuration); + if (maxDuration != -1) + { + if (modOwner != null) + modOwner.ApplySpellMod(spellInfo, SpellModOp.Duration, ref maxDuration); + + if (spellInfo.IsEmpowerSpell()) + maxDuration += (int)SpellConst.EmpowerHoldTimeAtMax; + } return maxDuration; } diff --git a/Source/Game/Spells/Spell.cs b/Source/Game/Spells/Spell.cs index 719a034ab..0b8c3854f 100644 --- a/Source/Game/Spells/Spell.cs +++ b/Source/Game/Spells/Spell.cs @@ -91,14 +91,15 @@ namespace Game.Spells //Auto Shot & Shoot (wand) m_autoRepeat = m_spellInfo.IsAutoRepeatRangedSpell(); + if (m_spellInfo.IsEmpowerSpell()) + m_empower = new EmpowerData(); + // Determine if spell can be reflected back to the caster // Patch 1.2 notes: Spell Reflection no longer reflects abilities m_canReflect = caster.IsUnit() && ((m_spellInfo.DmgClass == SpellDmgClass.Magic && !m_spellInfo.HasAttribute(SpellAttr0.IsAbility)) || m_spellInfo.HasAttribute(SpellAttr7.AllowSpellReflection)) && !m_spellInfo.HasAttribute(SpellAttr1.NoReflection) && !m_spellInfo.HasAttribute(SpellAttr0.NoImmunities) && !m_spellInfo.IsPassive(); - CleanupTargetList(); - for (var i = 0; i < SpellConst.MaxEffects; ++i) m_destTargets[i] = new SpellDestination(m_caster); } @@ -1046,7 +1047,7 @@ namespace Game.Spells if (liquidLevel <= ground) // When there is no liquid Map.GetWaterOrGroundLevel returns ground level { SendCastResult(SpellCastResult.NotHere); - SendChannelUpdate(0); + SendChannelUpdate(0, SpellCastResult.NotHere); Finish(SpellCastResult.NotHere); return; } @@ -1054,7 +1055,7 @@ namespace Game.Spells if (ground + 0.75 > liquidLevel) { SendCastResult(SpellCastResult.TooShallow); - SendChannelUpdate(0); + SendChannelUpdate(0, SpellCastResult.TooShallow); Finish(SpellCastResult.TooShallow); return; } @@ -2609,7 +2610,7 @@ namespace Game.Spells // a possible alternative sollution for those would be validating aura target on unit state change if (triggeredByAura != null && triggeredByAura.IsPeriodic() && !triggeredByAura.GetBase().IsPassive()) { - SendChannelUpdate(0); + SendChannelUpdate(0, result); triggeredByAura.GetBase().SetDuration(0); } @@ -2741,7 +2742,7 @@ namespace Game.Spells } } - SendChannelUpdate(0); + SendChannelUpdate(0, SpellCastResult.Interrupted); SendInterrupted(0); SendCastResult(SpellCastResult.Interrupted); @@ -3141,10 +3142,12 @@ namespace Game.Spells if (m_spellInfo.IsChanneled()) { int duration = m_spellInfo.GetDuration(); - if (duration > 0 || m_spellValue.Duration.HasValue) + if (duration > 0 || m_spellValue.Duration > 0) { if (!m_spellValue.Duration.HasValue) { + int originalDuration = duration; + // First mod_duration then haste - see Missile Barrage // Apply duration mod Player modOwner = m_caster.GetSpellModOwner(); @@ -3155,6 +3158,27 @@ namespace Game.Spells // Apply haste mods m_caster.ModSpellDurationTime(m_spellInfo, ref duration, this); + + if (IsEmpowerSpell()) + { + float ratio = (float)duration / (float)originalDuration; + TimeSpan totalExceptLastStage = TimeSpan.Zero; + for (int i = 0; i < m_spellInfo.EmpowerStageThresholds.Count - 1; ++i) + { + m_empower.StageDurations[i] = TimeSpan.FromMilliseconds((long)(m_spellInfo.EmpowerStageThresholds[i].TotalMilliseconds * ratio)); + totalExceptLastStage += m_empower.StageDurations[i]; + } + + m_empower.StageDurations[^1] = TimeSpan.FromMilliseconds(duration) - totalExceptLastStage; + + Player playerCaster = m_caster.ToPlayer(); + if (playerCaster != null) + m_empower.MinHoldTime = TimeSpan.FromMilliseconds((long)(m_empower.StageDurations[0].TotalMilliseconds * playerCaster.GetEmpowerMinHoldStagePercent())); + else + m_empower.MinHoldTime = m_empower.StageDurations[0]; + + duration += (int)SpellConst.EmpowerHoldTimeAtMax; + } } else duration = m_spellValue.Duration.Value; @@ -3452,9 +3476,47 @@ namespace Game.Spells } } + if (IsEmpowerSpell()) + { + int getCompletedEmpowerStages() + { + TimeSpan passed = TimeSpan.FromMilliseconds(m_channeledDuration - m_timer); + for (int i = 0; i < m_empower.StageDurations.Count; ++i) + { + passed -= m_empower.StageDurations[i]; + if (passed < TimeSpan.Zero) + return i; + } + + return m_empower.StageDurations.Count; + }; + + int completedStages = getCompletedEmpowerStages(); + if (completedStages != m_empower.CompletedStages) + { + SpellEmpowerSetStage empowerSetStage = new(); + empowerSetStage.CastID = m_castId; + empowerSetStage.CasterGUID = m_caster.GetGUID(); + empowerSetStage.Stage = m_empower.CompletedStages; + m_caster.SendMessageToSet(empowerSetStage, true); + + m_empower.CompletedStages = completedStages; + m_caster.ToUnit().SetSpellEmpowerStage((sbyte)completedStages); + + CallScriptEmpowerStageCompletedHandlers(completedStages); + } + + if (CanReleaseEmpowerSpell()) + { + m_empower.IsReleased = true; + m_timer = 0; + CallScriptEmpowerCompletedHandlers(m_empower.CompletedStages); + } + } + if (m_timer == 0) { - SendChannelUpdate(0); + SendChannelUpdate(0, SpellCastResult.SpellCastOk); Finish(); // We call the hook here instead of in Spell::finish because we only want to call it for completed channeling. Everything else is handled by interrupts @@ -3520,6 +3582,9 @@ namespace Game.Spells if (m_customArg is TraitConfig) m_caster.ToPlayer().SendPacket(new TraitConfigCommitFailed((m_customArg as TraitConfig).ID)); + if (IsEmpowerSpell()) + unitCaster.GetSpellHistory().ResetCooldown(m_spellInfo.Id, true); + return; } @@ -3549,6 +3614,14 @@ namespace Game.Spells // Stop Attack for some spells if (m_spellInfo.HasAttribute(SpellAttr0.CancelsAutoAttackCombat)) unitCaster.AttackStop(); + + if (IsEmpowerSpell()) + { + // Empower spells trigger gcd at the end of cast instead of at start + SpellInfo gcd = Global.SpellMgr.GetSpellInfo(SpellConst.EmpowerHardcodedGCD, Difficulty.None); + if (gcd != null) + unitCaster.GetSpellHistory().AddGlobalCooldown(gcd, TimeSpan.FromMilliseconds(gcd.StartRecoveryTime)); + } } static void FillSpellCastFailedArgs(T packet, ObjectGuid castId, SpellInfo spellInfo, SpellCastResult result, SpellCustomErrors customError, int? param1, int? param2, Player caster) where T : CastFailedBase @@ -4354,7 +4427,7 @@ namespace Game.Spells m_caster.SendMessageToSet(failedPacket, true); } - public void SendChannelUpdate(uint time) + public void SendChannelUpdate(uint time, SpellCastResult? result = null) { // GameObjects don't channel Unit unitCaster = m_caster.ToUnit(); @@ -4366,12 +4439,31 @@ namespace Game.Spells unitCaster.ClearChannelObjects(); unitCaster.SetChannelSpellId(0); unitCaster.SetChannelVisual(new SpellCastVisualField()); + unitCaster.SetSpellEmpowerStage(-1); } - SpellChannelUpdate spellChannelUpdate = new(); - spellChannelUpdate.CasterGUID = unitCaster.GetGUID(); - spellChannelUpdate.TimeRemaining = (int)time; - unitCaster.SendMessageToSet(spellChannelUpdate, true); + if (IsEmpowerSpell()) + { + SpellEmpowerUpdate spellEmpowerUpdate = new(); + spellEmpowerUpdate.CastID = m_castId; + spellEmpowerUpdate.CasterGUID = unitCaster.GetGUID(); + spellEmpowerUpdate.TimeRemaining = TimeSpan.FromMilliseconds(time); + if (time > 0) + spellEmpowerUpdate.StageDurations.AddRange(m_empower.StageDurations); + else if (result.HasValue && result != SpellCastResult.SpellCastOk) + spellEmpowerUpdate.Status = 1; + else + spellEmpowerUpdate.Status = 4; + + unitCaster.SendMessageToSet(spellEmpowerUpdate, true); + } + else + { + SpellChannelUpdate spellChannelUpdate = new(); + spellChannelUpdate.CasterGUID = unitCaster.GetGUID(); + spellChannelUpdate.TimeRemaining = (int)time; + unitCaster.SendMessageToSet(spellChannelUpdate, true); + } } void SendChannelStart(uint duration) @@ -4427,36 +4519,62 @@ namespace Game.Spells unitCaster.SetChannelSpellId(m_spellInfo.Id); unitCaster.SetChannelVisual(m_SpellVisual); - SpellChannelStart spellChannelStart = new(); - spellChannelStart.CasterGUID = unitCaster.GetGUID(); - spellChannelStart.SpellID = (int)m_spellInfo.Id; - spellChannelStart.Visual = m_SpellVisual; - spellChannelStart.ChannelDuration = duration; - - uint schoolImmunityMask = unitCaster.GetSchoolImmunityMask(); - ulong mechanicImmunityMask = unitCaster.GetMechanicImmunityMask(); - - if (schoolImmunityMask != 0 || mechanicImmunityMask != 0) + void setImmunitiesAndHealPrediction(ref SpellChannelStartInterruptImmunities? interruptImmunities, ref SpellTargetedHealPrediction? healPrediction) { - SpellChannelStartInterruptImmunities interruptImmunities = new(); - interruptImmunities.SchoolImmunities = (int)schoolImmunityMask; - interruptImmunities.Immunities = (int)mechanicImmunityMask; + uint schoolImmunityMask = unitCaster.GetSchoolImmunityMask(); + ulong mechanicImmunityMask = unitCaster.GetMechanicImmunityMask(); - spellChannelStart.InterruptImmunities = interruptImmunities; - } + if (schoolImmunityMask != 0 || mechanicImmunityMask != 0) + { + SpellChannelStartInterruptImmunities immunities = new(); + immunities.SchoolImmunities = (int)schoolImmunityMask; + immunities.Immunities = (int)mechanicImmunityMask; - if (m_spellInfo.HasAttribute(SpellAttr8.HealPrediction) && m_caster.IsUnit()) + interruptImmunities = immunities; + } + + if (m_spellInfo.HasAttribute(SpellAttr8.HealPrediction) && m_caster.IsUnit()) + { + + SpellTargetedHealPrediction prediction = new(); + if (unitCaster.m_unitData.ChannelObjects.Size() == 1 && unitCaster.m_unitData.ChannelObjects[0].IsUnit()) + prediction.TargetGUID = unitCaster.m_unitData.ChannelObjects[0]; + + UpdateSpellHealPrediction(prediction.Predict, true); + + healPrediction = prediction; + } + }; + + if (IsEmpowerSpell()) { - SpellTargetedHealPrediction healPrediction = new(); - if (unitCaster.m_unitData.ChannelObjects.Size() == 1 && unitCaster.m_unitData.ChannelObjects[0].IsUnit()) - healPrediction.TargetGUID = unitCaster.m_unitData.ChannelObjects[0]; + unitCaster.SetSpellEmpowerStage(0); - UpdateSpellHealPrediction(healPrediction.Predict, true); + SpellEmpowerStart spellEmpowerStart = new(); + spellEmpowerStart.CastID = m_castId; + spellEmpowerStart.CasterGUID = unitCaster.GetGUID(); + spellEmpowerStart.SpellID = (int)m_spellInfo.Id; + spellEmpowerStart.Visual = m_SpellVisual; + spellEmpowerStart.EmpowerDuration = new TimeSpan(m_empower.StageDurations.Sum(r => r.Ticks)); + spellEmpowerStart.MinHoldTime = m_empower.MinHoldTime; + spellEmpowerStart.HoldAtMaxTime = TimeSpan.FromMilliseconds(SpellConst.EmpowerHoldTimeAtMax); + spellEmpowerStart.Targets.AddRange(unitCaster.m_unitData.ChannelObjects._values); + spellEmpowerStart.StageDurations.AddRange(m_empower.StageDurations); + setImmunitiesAndHealPrediction(ref spellEmpowerStart.InterruptImmunities, ref spellEmpowerStart.HealPrediction); - spellChannelStart.HealPrediction = healPrediction; + unitCaster.SendMessageToSet(spellEmpowerStart, true); } + else + { + SpellChannelStart spellChannelStart = new(); + spellChannelStart.CasterGUID = unitCaster.GetGUID(); + spellChannelStart.SpellID = (int)m_spellInfo.Id; + spellChannelStart.Visual = m_SpellVisual; + spellChannelStart.ChannelDuration = duration; + setImmunitiesAndHealPrediction(ref spellChannelStart.InterruptImmunities, ref spellChannelStart.HealPrediction); - unitCaster.SendMessageToSet(spellChannelStart, true); + unitCaster.SendMessageToSet(spellChannelStart, true); + } } void SendResurrectRequest(Player target) @@ -7332,6 +7450,25 @@ namespace Game.Spells return m_spellInfo.IsPositive() && (m_triggeredByAuraSpell == null || m_triggeredByAuraSpell.IsPositive()); } + public bool IsEmpowerSpell() { return m_empower != null; } + + public void SetEmpowerReleasedByClient(bool release) + { + m_empower.IsReleasedByClient = release; + } + + public bool CanReleaseEmpowerSpell() + { + if (m_empower.IsReleased) + return false; + + if (!m_empower.IsReleasedByClient && m_timer != 0) + return false; + + TimeSpan passedTime = TimeSpan.FromMilliseconds(m_channeledDuration - m_timer); + return passedTime >= m_empower.MinHoldTime; + } + bool IsNeedSendToClient() { return m_SpellVisual.SpellXSpellVisualID != 0 || m_SpellVisual.ScriptVisualID != 0 || m_spellInfo.IsChanneled() || @@ -7936,6 +8073,30 @@ namespace Game.Spells return source.IsWithinLOS(target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(), LineOfSightChecks.All, ignoreFlags); } + void CallScriptEmpowerStageCompletedHandlers(int completedStagesCount) + { + foreach (SpellScript script in m_loadedScripts) + { + script._PrepareScriptCall(SpellScriptHookType.EmpowerStageCompleted); + foreach (var empowerStageCompleted in script.OnEmpowerStageCompleted) + empowerStageCompleted.Call(completedStagesCount); + + script._FinishScriptCall(); + } + } + + void CallScriptEmpowerCompletedHandlers(int completedStagesCount) + { + foreach (SpellScript script in m_loadedScripts) + { + script._PrepareScriptCall(SpellScriptHookType.EmpowerCompleted); + foreach (var empowerStageCompleted in script.OnEmpowerCompleted) + empowerStageCompleted.Call(completedStagesCount); + + script._FinishScriptCall(); + } + } + bool CheckScriptEffectImplicitTargets(uint effIndex, uint effIndexToCheck) { // Skip if there are not any script @@ -8288,6 +8449,8 @@ namespace Game.Spells byte m_runesState; byte m_delayAtDamageCount; + EmpowerData m_empower; + // Delayed spells system ulong m_delayStart; // time of spell delay start, filled by event handler, zero = just started ulong m_delayMoment; // moment of next delay call, used internally @@ -9724,4 +9887,13 @@ namespace Game.Spells return (_storage[1] & (int)procFlags) != 0; } } + + public class EmpowerData + { + public TimeSpan MinHoldTime; + public List StageDurations = new(); + public int CompletedStages = 0; + public bool IsReleasedByClient; + public bool IsReleased; + } } \ No newline at end of file diff --git a/Source/Game/Spells/SpellInfo.cs b/Source/Game/Spells/SpellInfo.cs index 73d3d2aac..4bfdd479f 100644 --- a/Source/Game/Spells/SpellInfo.cs +++ b/Source/Game/Spells/SpellInfo.cs @@ -158,6 +158,11 @@ namespace Game.Spells EquippedItemClass = ItemClass.None; EquippedItemSubClassMask = 0; EquippedItemInventoryTypeMask = 0; + + // SpellEmpowerStageEntry + foreach (var stage in data.EmpowerStages) + EmpowerStageThresholds.Add(TimeSpan.FromMilliseconds(stage.DurationMs)); + // SpellEquippedItemsEntry SpellEquippedItemsRecord _equipped = data.EquippedItems; if (_equipped != null) @@ -566,6 +571,11 @@ namespace Game.Spells return HasAttribute(SpellAttr2.AutoRepeat); } + public bool IsEmpowerSpell() + { + return !EmpowerStageThresholds.Empty(); + } + public bool HasInitialAggro() { return !(HasAttribute(SpellAttr1.NoThreat) || HasAttribute(SpellAttr2.NoInitialThreat) || HasAttribute(SpellAttr4.NoHarmfulThreat)); @@ -3940,6 +3950,7 @@ namespace Game.Spells public SpellSchoolMask SchoolMask { get; set; } public uint ChargeCategoryId; public List Labels = new(); + public List EmpowerStageThresholds = new(); // SpellScalingEntry public ScalingInfo Scaling; diff --git a/Source/Game/Spells/SpellManager.cs b/Source/Game/Spells/SpellManager.cs index fc39999d4..d3a297f07 100644 --- a/Source/Game/Spells/SpellManager.cs +++ b/Source/Game/Spells/SpellManager.cs @@ -1,12 +1,10 @@ // Copyright (c) CypherCore All rights reserved. // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. -using Framework.Collections; using Framework.Constants; using Framework.Database; using Framework.Dynamic; using Game.BattleFields; -using Game.BattleGrounds; using Game.BattlePets; using Game.DataStorage; using Game.Miscellaneous; @@ -2248,6 +2246,17 @@ namespace Game.Entities foreach (SpellCooldownsRecord cooldowns in CliDB.SpellCooldownsStorage.Values) GetLoadHelper(cooldowns.SpellID, cooldowns.DifficultyID).Cooldowns = cooldowns; + foreach (var (_, empowerStage) in CliDB.SpellEmpowerStageStorage) + { + SpellEmpowerRecord empower = CliDB.SpellEmpowerStorage.LookupByKey(empowerStage.SpellEmpowerID); + if (empower != null) + GetLoadHelper((uint)empower.SpellID, 0).EmpowerStages.Add(empowerStage); + + } + + foreach (var data in loadData) + data.Value.EmpowerStages.OrderBy(stageRecord => stageRecord.Stage); + foreach (SpellEquippedItemsRecord equippedItems in CliDB.SpellEquippedItemsStorage.Values) GetLoadHelper(equippedItems.SpellID, 0).EquippedItems = equippedItems; @@ -2307,84 +2316,87 @@ namespace Game.Entities foreach (var data in loadData) data.Value.Visuals.Sort((left, right) => { return right.CasterPlayerConditionID.CompareTo(left.CasterPlayerConditionID); }); - foreach (var data in loadData) + foreach (var (key, data) in loadData) { - SpellNameRecord spellNameEntry = CliDB.SpellNameStorage.LookupByKey(data.Key.Id); + SpellNameRecord spellNameEntry = CliDB.SpellNameStorage.LookupByKey(key.Id); if (spellNameEntry == null) continue; // fill blanks - DifficultyRecord difficultyEntry = CliDB.DifficultyStorage.LookupByKey(data.Key.difficulty); + DifficultyRecord difficultyEntry = CliDB.DifficultyStorage.LookupByKey(key.difficulty); if (difficultyEntry != null) { do { - SpellInfoLoadHelper fallbackData = loadData.LookupByKey((data.Key.Id, (Difficulty)difficultyEntry.FallbackDifficultyID)); + SpellInfoLoadHelper fallbackData = loadData.LookupByKey((key.Id, (Difficulty)difficultyEntry.FallbackDifficultyID)); if (fallbackData != null) { - if (data.Value.AuraOptions == null) - data.Value.AuraOptions = fallbackData.AuraOptions; + if (data.AuraOptions == null) + data.AuraOptions = fallbackData.AuraOptions; - if (data.Value.AuraRestrictions == null) - data.Value.AuraRestrictions = fallbackData.AuraRestrictions; + if (data.AuraRestrictions == null) + data.AuraRestrictions = fallbackData.AuraRestrictions; - if (data.Value.CastingRequirements == null) - data.Value.CastingRequirements = fallbackData.CastingRequirements; + if (data.CastingRequirements == null) + data.CastingRequirements = fallbackData.CastingRequirements; - if (data.Value.Categories == null) - data.Value.Categories = fallbackData.Categories; + if (data.Categories == null) + data.Categories = fallbackData.Categories; - if (data.Value.ClassOptions == null) - data.Value.ClassOptions = fallbackData.ClassOptions; + if (data.ClassOptions == null) + data.ClassOptions = fallbackData.ClassOptions; - if (data.Value.Cooldowns == null) - data.Value.Cooldowns = fallbackData.Cooldowns; + if (data.Cooldowns == null) + data.Cooldowns = fallbackData.Cooldowns; - for (var i = 0; i < data.Value.Effects.Length; ++i) - if (data.Value.Effects[i] == null) - data.Value.Effects[i] = fallbackData.Effects[i]; + for (var i = 0; i < data.Effects.Length; ++i) + if (data.Effects[i] == null) + data.Effects[i] = fallbackData.Effects[i]; - if (data.Value.EquippedItems == null) - data.Value.EquippedItems = fallbackData.EquippedItems; + if (data.EmpowerStages.Empty()) + data.EmpowerStages = fallbackData.EmpowerStages; - if (data.Value.Interrupts == null) - data.Value.Interrupts = fallbackData.Interrupts; + if (data.EquippedItems == null) + data.EquippedItems = fallbackData.EquippedItems; - if (data.Value.Labels.Empty()) - data.Value.Labels = fallbackData.Labels; + if (data.Interrupts == null) + data.Interrupts = fallbackData.Interrupts; - if (data.Value.Levels == null) - data.Value.Levels = fallbackData.Levels; + if (data.Labels.Empty()) + data.Labels = fallbackData.Labels; - if (data.Value.Misc == null) - data.Value.Misc = fallbackData.Misc; + if (data.Levels == null) + data.Levels = fallbackData.Levels; + + if (data.Misc == null) + data.Misc = fallbackData.Misc; for (var i = 0; i < fallbackData.Powers.Length; ++i) - if (data.Value.Powers[i] == null) - data.Value.Powers[i] = fallbackData.Powers[i]; + if (data.Powers[i] == null) + data.Powers[i] = fallbackData.Powers[i]; - if (data.Value.Reagents == null) - data.Value.Reagents = fallbackData.Reagents; + if (data.Reagents == null) + data.Reagents = fallbackData.Reagents; - if (data.Value.ReagentsCurrency.Empty()) - data.Value.ReagentsCurrency = fallbackData.ReagentsCurrency; + if (data.ReagentsCurrency.Empty()) + data.ReagentsCurrency = fallbackData.ReagentsCurrency; - if (data.Value.Scaling == null) - data.Value.Scaling = fallbackData.Scaling; + if (data.Scaling == null) + data.Scaling = fallbackData.Scaling; - if (data.Value.Shapeshift == null) - data.Value.Shapeshift = fallbackData.Shapeshift; + if (data.Shapeshift == null) + data.Shapeshift = fallbackData.Shapeshift; - if (data.Value.TargetRestrictions == null) - data.Value.TargetRestrictions = fallbackData.TargetRestrictions; + if (data.TargetRestrictions == null) + data.TargetRestrictions = fallbackData.TargetRestrictions; - if (data.Value.Totems == null) - data.Value.Totems = fallbackData.Totems; + if (data.Totems == null) + data.Totems = fallbackData.Totems; // visuals fall back only to first difficulty that defines any visual // they do not stack all difficulties in fallback chain - if (data.Value.Visuals.Empty()) - data.Value.Visuals = fallbackData.Visuals; + if (data.Visuals.Empty()) + data.Visuals = fallbackData.Visuals; } difficultyEntry = CliDB.DifficultyStorage.LookupByKey(difficultyEntry.FallbackDifficultyID); @@ -2395,7 +2407,7 @@ namespace Game.Entities //second key = id - mSpellInfoMap.Add(spellNameEntry.Id, new SpellInfo(spellNameEntry, data.Key.difficulty, data.Value)); + mSpellInfoMap.Add(spellNameEntry.Id, new SpellInfo(spellNameEntry, key.difficulty, data)); } Log.outInfo(LogFilter.ServerLoading, "Loaded SpellInfo store in {0} ms", Time.GetMSTimeDiffToNow(oldMSTime)); @@ -4921,6 +4933,7 @@ namespace Game.Entities public SpellClassOptionsRecord ClassOptions; public SpellCooldownsRecord Cooldowns; public SpellEffectRecord[] Effects = new SpellEffectRecord[SpellConst.MaxEffects]; + public List EmpowerStages = new(); public SpellEquippedItemsRecord EquippedItems; public SpellInterruptsRecord Interrupts; public List Labels = new(); diff --git a/Source/Scripts/Spells/Evoker.cs b/Source/Scripts/Spells/Evoker.cs index a8ef2d7fe..fef3c2e64 100644 --- a/Source/Scripts/Spells/Evoker.cs +++ b/Source/Scripts/Spells/Evoker.cs @@ -14,7 +14,9 @@ namespace Scripts.Spells.Evoker { struct SpellIds { + public const uint BlastFurnace = 375510; public const uint EnergizingFlame = 400006; + public const uint FireBreathDamage = 357209; public const uint GlideKnockback = 358736; public const uint Hover = 358267; public const uint LivingFlame = 361469; @@ -22,6 +24,7 @@ namespace Scripts.Spells.Evoker public const uint LivingFlameHeal = 361509; public const uint PermeatingChillTalent = 370897; public const uint PyreDamage = 357212; + public const uint ScouringFlame = 378438; public const uint SoarRacial = 369536; public const uint LabelEvokerBlue = 1465; @@ -57,6 +60,68 @@ namespace Scripts.Spells.Evoker } } + // 357208 Fire Breath (Red) + [Script] // 382266 Fire Breath (Red) + class spell_evo_fire_breath : SpellScript + { + public override bool Validate(SpellInfo spellInfo) + { + return ValidateSpellInfo(SpellIds.FireBreathDamage, SpellIds.BlastFurnace); + } + + void OnComplete(int completedStageCount) + { + int dotTicks = 10 - (completedStageCount - 1) * 3; + AuraEffect blastFurnace = GetCaster().GetAuraEffect(SpellIds.BlastFurnace, 0); + if (blastFurnace != null) + dotTicks += blastFurnace.GetAmount() / 2; + + GetCaster().CastSpell(GetCaster(), SpellIds.FireBreathDamage, new CastSpellExtraArgs() + .SetTriggeringSpell(GetSpell()) + .SetTriggerFlags(TriggerCastFlags.IgnoreCastInProgress | TriggerCastFlags.DontReportCastError) + .AddSpellMod(SpellValueMod.DurationPct, 100 * dotTicks) + .SetCustomArg(completedStageCount)); + } + + public override void Register() + { + OnEmpowerCompleted.Add(new(OnComplete)); + } + } + + [Script] // 357209 Fire Breath (Red) + class spell_evo_fire_breath_damage : SpellScript + { + public override bool Validate(SpellInfo spellInfo) + { + return ValidateSpellEffect((spellInfo.Id, 2)) + && spellInfo.GetEffect(2).IsAura(AuraType.ModSilence); // validate we are removing the correct effect + } + + void AddBonusUpfrontDamage(Unit victim, ref int damage, ref int flatMod, ref float pctMod) + { + int empowerLevel = (int)GetSpell().m_customArg; + if (empowerLevel == 0) + return; + + // damage is done after aura is applied, grab periodic amount + AuraEffect fireBreath = victim.GetAuraEffect(GetSpellInfo().Id, 1, GetCaster().GetGUID()); + if (fireBreath != null) + flatMod += (int)(fireBreath.GetEstimatedAmount().GetValueOrDefault(fireBreath.GetAmount()) * (empowerLevel - 1) * 3); + } + + void RemoveUnusedEffect(List targets) + { + targets.Clear(); + } + + public override void Register() + { + CalcDamage.Add(new(AddBonusUpfrontDamage)); + OnObjectAreaTargetSelect.Add(new(RemoveUnusedEffect, 2, Targets.UnitConeCasterToDestEnemy)); + } + } + [Script] // 358733 - Glide (Racial) class spell_evo_glide : SpellScript { @@ -182,4 +247,32 @@ namespace Scripts.Spells.Evoker OnEffectHitTarget.Add(new(HandleDamage, 0, SpellEffectName.Dummy)); } } + + [Script] // 357209 Fire Breath (Red) + class spell_evo_scouring_flame : SpellScript + { + public override bool Validate(SpellInfo spellInfo) + { + return ValidateSpellInfo(SpellIds.ScouringFlame); + } + + void HandleScouringFlame(List targets) + { + if (!GetCaster().HasAura(SpellIds.ScouringFlame)) + targets.Clear(); + } + + void CalcDispelCount(uint effIndex) + { + int empowerLevel = (int)GetSpell().m_customArg; + if (empowerLevel != 0) + SetEffectValue(empowerLevel); + } + + public override void Register() + { + OnObjectAreaTargetSelect.Add(new(HandleScouringFlame, 3, Targets.UnitConeCasterToDestEnemy)); + OnEffectHitTarget.Add(new(CalcDispelCount, 3, SpellEffectName.Dispel)); + } + } } \ No newline at end of file