diff --git a/src/ZoneServer/Buffs/Handlers/Common/Warrior_EnableMovingShot_Buff.cs b/src/ZoneServer/Buffs/Handlers/Common/Warrior_EnableMovingShot_Buff.cs
new file mode 100644
index 000000000..91ecc505f
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Common/Warrior_EnableMovingShot_Buff.cs
@@ -0,0 +1,27 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+
+namespace Melia.Zone.Buffs.Handlers.Common
+{
+ ///
+ /// Handle for the Warrior_EnableMovingShot Buff, which
+ /// gives moving shot with a given value
+ ///
+ ///
+ /// NumArg1: MovingShot_BM value
+ /// NumArg2: None
+ ///
+ [BuffHandler(BuffId.Warrior_EnableMovingShot_Buff)]
+ public class Warrior_EnableMovingShot_Buff : BuffHandler
+ {
+ public override void OnStart(Buff buff)
+ {
+ AddPropertyModifier(buff, buff.Target, PropertyName.MovingShot_BM, buff.NumArg1);
+ }
+
+ public override void OnEnd(Buff buff)
+ {
+ RemovePropertyModifier(buff, buff.Target, PropertyName.MovingShot_BM);
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Swordsmen/Cataphract/Impaler_Buff.cs b/src/ZoneServer/Buffs/Handlers/Swordsmen/Cataphract/Impaler_Buff.cs
new file mode 100644
index 000000000..616740aae
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Swordsmen/Cataphract/Impaler_Buff.cs
@@ -0,0 +1,52 @@
+using Melia.Shared.Game.Const;
+using Melia.Shared.L10N;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Network;
+using Melia.Zone.Skills;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.World.Actors;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Melia.Zone.Buffs.Handlers.Swordsmen.Cataphract
+{
+ ///
+ /// Buff handler for Impaler Buff, which triggers the slam portion of Impaler.
+ /// It needs to end if the skewered monster dies
+ ///
+ ///
+ /// caster in this case is the skewer monster
+ ///
+ [BuffHandler(BuffId.Impaler_Buff)]
+ public class Impaler_Buff : BuffHandler
+ {
+ ///
+ /// Ends the buff, activating the overheat
+ ///
+ ///
+ public override void OnEnd(Buff buff)
+ {
+ var target = buff.Target;
+
+ if (target.TryGetSkill(SkillId.Cataphract_Impaler, out var skill))
+ {
+ skill.IncreaseOverheat();
+ }
+ }
+
+
+ ///
+ /// Checks to see if the entity died
+ ///
+ ///
+ public override void WhileActive(Buff buff)
+ {
+ var caster = buff.Caster;
+ var target = buff.Target;
+
+ if (caster.IsDead)
+ target.StopBuff(BuffId.Impaler_Buff);
+ }
+ }
+
+
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Swordsmen/Cataphract/Impaler_Debuff.cs b/src/ZoneServer/Buffs/Handlers/Swordsmen/Cataphract/Impaler_Debuff.cs
new file mode 100644
index 000000000..24c7177c2
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Swordsmen/Cataphract/Impaler_Debuff.cs
@@ -0,0 +1,72 @@
+using Melia.Shared.Game.Const;
+using Melia.Shared.L10N;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Network;
+using Melia.Zone.Skills;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.World.Actors;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Melia.Zone.Buffs.Handlers.Swordsmen.Cataphract
+{
+ ///
+ /// Buff handler for Impaler Debuff, which reduces def and makes it
+ /// impossible to block or dodge.
+ /// This also needs to prevent you from taking any action
+ ///
+ ///
+ /// NumArg1: Skill Level
+ /// NumArg2: None
+ ///
+ [BuffHandler(BuffId.Impaler_Debuff)]
+ public class Impaler_Debuff : BuffHandler, IBuffCombatDefenseBeforeCalcHandler
+ {
+ public const float DefPenalty = 0.3f;
+
+ ///
+ /// Starts buff, attaching the entity to the spear
+ ///
+ ///
+ public override void OnStart(Buff buff)
+ {
+ var target = buff.Target;
+ var caster = buff.Caster;
+
+ var reduceDef = target.Properties.GetFloat(PropertyName.DEF) * DefPenalty;
+
+ AddPropertyModifier(buff, target, PropertyName.DEF_BM, -reduceDef);
+
+ Send.ZC_ATTACH_TO_OBJ(target, caster, "Dummy_Impaler", "", 0.01f, 1, 1, "", 1, 0, 1);
+ }
+
+ ///
+ /// Ends the buff, freeing the entity
+ ///
+ ///
+ public override void OnEnd(Buff buff)
+ {
+ var target = buff.Target;
+ var caster = buff.Caster;
+
+ RemovePropertyModifier(buff, target, PropertyName.DEF_BM);
+
+ Send.ZC_DETACH_FROM_OBJ(target, caster);
+ }
+
+
+ ///
+ /// Applies the debuff's effect during the combat calculations.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void OnDefenseBeforeCalc(Buff buff, ICombatEntity attacker, ICombatEntity target, Skill skill, SkillModifier modifier, SkillHitResult skillHitResult)
+ {
+ modifier.Unblockable = true;
+ modifier.ForcedHit = true;
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Swordsmen/Cataphract/Trot_Buff.cs b/src/ZoneServer/Buffs/Handlers/Swordsmen/Cataphract/Trot_Buff.cs
new file mode 100644
index 000000000..7baf7b1c3
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Swordsmen/Cataphract/Trot_Buff.cs
@@ -0,0 +1,74 @@
+using Melia.Shared.Game.Const;
+using Melia.Shared.L10N;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Network;
+using Melia.Zone.Skills;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.World.Actors;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Melia.Zone.Buffs.Handlers.Swordsmen.Cataphract
+{
+ ///
+ /// Buff handler for Trot, which increases movement speed while riding
+ ///
+ ///
+ /// NumArg1: Skill Level
+ /// NumArg2: None
+ ///
+ [BuffHandler(BuffId.Trot_Buff)]
+ public class Trot_Buff : BuffHandler
+ {
+ ///
+ /// Starts buff, increasing movement speed
+ ///
+ ///
+ public override void OnStart(Buff buff)
+ {
+ var target = buff.Target;
+
+ AddPropertyModifier(buff, target, PropertyName.MSPD_BM, GetSpeedBonus(buff));
+ Send.ZC_MSPD(target);
+ }
+
+ ///
+ /// Ends the buff, resetting movement speed
+ ///
+ ///
+ public override void OnEnd(Buff buff)
+ {
+ RemovePropertyModifier(buff, buff.Target, PropertyName.MSPD_BM);
+ Send.ZC_MSPD(buff.Target);
+ }
+
+
+ ///
+ /// Returns the speed bonus granted by the buff.
+ ///
+ ///
+ ///
+ private float GetSpeedBonus(Buff buff)
+ {
+ return 5 + buff.NumArg1;
+ }
+
+
+ ///
+ /// Drains SP over time to keep the buff active
+ ///
+ ///
+ public override void WhileActive(Buff buff)
+ {
+ if (!buff.Target.IsBuffActive(BuffId.RidingCompanion))
+ {
+ buff.Target.StopBuff(buff.Id);
+ return;
+ }
+ if (!buff.Target.TrySpendSp(20))
+ {
+ buff.Target.StopBuff(buff.Id);
+ return;
+ }
+ }
+ }
+}
diff --git a/src/ZoneServer/Network/Send.cs b/src/ZoneServer/Network/Send.cs
index 7d3586044..505041d65 100644
--- a/src/ZoneServer/Network/Send.cs
+++ b/src/ZoneServer/Network/Send.cs
@@ -24,6 +24,7 @@
using Melia.Zone.World.Items;
using Melia.Zone.World.Maps;
using Yggdrasil.Extensions;
+using Yggdrasil.Logging;
using Yggdrasil.Util;
namespace Melia.Zone.Network
@@ -2256,6 +2257,34 @@ public static void ZC_OWNER(Character character, IActor actor)
character.Connection.Send(packet);
}
+ ///
+ /// Tells the client that a monster is a Summoned monster
+ ///
+ ///
+ ///
+ public static void ZC_IS_SUMMONING_MONSTER(IActor actor, bool isSummon)
+ {
+ var packet = new Packet(Op.ZC_IS_SUMMONING_MONSTER);
+ packet.PutInt(actor.Handle);
+ packet.PutByte(isSummon);
+
+ actor.Map.Broadcast(packet, actor);
+ }
+
+ ///
+ /// Tells the client that a monster is a Sorcerer Summon
+ ///
+ ///
+ ///
+ public static void ZC_IS_SUMMON_SORCERER_MONSTER(IActor actor, bool isSummon)
+ {
+ var packet = new Packet(Op.ZC_IS_SUMMON_SORCERER_MONSTER);
+ packet.PutInt(actor.Handle);
+ packet.PutByte(isSummon);
+
+ actor.Map.Broadcast(packet, actor);
+ }
+
///
/// Draws circle area on ground at position for characters in range
/// of the caster.
@@ -3188,7 +3217,7 @@ public static void ZC_PLAY_ANI(IActor actor, string animationName, bool stopOnLa
packet.PutByte(stopOnLastFrame);
packet.PutByte(0);
packet.PutFloat(0);
- packet.PutFloat(1);
+ packet.PutFloat(1); // animation speed
// [i373230] Maybe added earlier
{
@@ -3199,6 +3228,65 @@ public static void ZC_PLAY_ANI(IActor actor, string animationName, bool stopOnLa
actor.Map.Broadcast(packet, actor);
}
+ ///
+ /// Attaches an entity to an object
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static void ZC_ATTACH_TO_OBJ(IActor actor, IActor attachToEntity, string nodeName,
+ string packetString1, float attachDelay, long l1, long l2, string packetString2, byte b1, byte b2, byte b3)
+ {
+ if (!ZoneServer.Instance.Data.PacketStringDb.TryFind(nodeName, out var nodeNameData))
+ throw new ArgumentException($"Unknown packet string '{nodeName}'.");
+
+ if (!ZoneServer.Instance.Data.PacketStringDb.TryFind(packetString1, out var packetStringData1))
+ throw new ArgumentException($"Unknown packet string '{packetString1}'.");
+
+ if (!ZoneServer.Instance.Data.PacketStringDb.TryFind(packetString2, out var packetStringData2))
+ throw new ArgumentException($"Unknown packet string '{packetString2}'.");
+
+ var packet = new Packet(Op.ZC_ATTACH_TO_OBJ);
+
+ packet.PutInt(actor?.Handle ?? 0);
+ packet.PutInt(attachToEntity?.Handle ?? 0);
+ packet.PutInt(nodeNameData?.Id ?? 0);
+ packet.PutInt(packetStringData1?.Id ?? 0);
+ packet.PutFloat(attachDelay);
+ packet.PutLong(l1);
+ packet.PutLong(l2);
+ packet.PutInt(packetStringData2?.Id ?? 0);
+ packet.PutByte(b1);
+ packet.PutByte(b2);
+ packet.PutByte(b3);
+
+ actor.Map.Broadcast(packet);
+ }
+
+
+ ///
+ /// Detach from another object (usually another actor).
+ ///
+ ///
+ ///
+ public static void ZC_DETACH_FROM_OBJ(IActor actor, IActor detachFromActor)
+ {
+ var packet = new Packet(Op.ZC_DETACH_FROM_OBJ);
+
+ packet.PutInt(actor.Handle);
+ packet.PutInt(detachFromActor?.Handle ?? 0);
+
+ actor.Map.Broadcast(packet);
+ }
+
///
/// Sends ZC_COMMON_SKILL_LIST to character (dummy).
///
diff --git a/src/ZoneServer/Pads/Handlers/Swordsman/Cataphract/Cataphract_SteedCharge.cs b/src/ZoneServer/Pads/Handlers/Swordsman/Cataphract/Cataphract_SteedCharge.cs
new file mode 100644
index 000000000..f25bac633
--- /dev/null
+++ b/src/ZoneServer/Pads/Handlers/Swordsman/Cataphract/Cataphract_SteedCharge.cs
@@ -0,0 +1,40 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Network;
+using Melia.Zone.Skills;
+using Melia.Zone.World.Actors.Monsters;
+
+namespace Melia.Zone.Pads.Handlers.Swordsman.Cataphract
+{
+ ///
+ /// Handler for the Arditi_TreGranata pad, creates and disables the effect
+ ///
+ [PadHandler(PadName.Cataphract_SteedCharge)]
+ public class Cataphract_SteedCharge : ICreatePadHandler, IDestroyPadHandler
+ {
+ ///
+ /// Called when the pad is created.
+ ///
+ ///
+ ///
+ public void Created(object sender, PadTriggerArgs args)
+ {
+ var pad = args.Trigger;
+ var creator = args.Creator;
+
+ Send.ZC_NORMAL.PadUpdate(creator, pad, "Cataphract_SteedCharge", 0, 0, 150, true);
+ }
+
+ ///
+ /// Called when the pad is destroyed.
+ ///
+ ///
+ ///
+ public void Destroyed(object sender, PadTriggerArgs args)
+ {
+ var pad = args.Trigger;
+ var creator = args.Creator;
+
+ Send.ZC_NORMAL.PadUpdate(creator, pad, "Cataphract_SteedCharge", 0, 0, 150, false);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Combat/SkillModifier.cs b/src/ZoneServer/Skills/Combat/SkillModifier.cs
index 6e0d05bff..8abdb6a8c 100644
--- a/src/ZoneServer/Skills/Combat/SkillModifier.cs
+++ b/src/ZoneServer/Skills/Combat/SkillModifier.cs
@@ -1,4 +1,7 @@
-namespace Melia.Zone.Skills.Combat
+using Melia.Shared.Data.Database;
+using Melia.Shared.Game.Const;
+
+namespace Melia.Zone.Skills.Combat
{
///
/// A class for properties that can modify the damage calculation for a skill.
@@ -110,6 +113,20 @@ public class SkillModifier
///
public bool ForcedCritical { get; set; }
+
+ ///
+ /// Gets or sets whether or not the natural attack attribute is overwritten
+ ///
+ public bool OverrideAttackAttribute { get; set; }
+
+ ///
+ /// Gets or sets the attribute for the attack
+ ///
+ ///
+ /// This is only read if OverrideAttackAttribute is true
+ ///
+ public SkillAttribute AttackAttribute { get; set; }
+
///
/// Returns a new skill modifier with default values.
///
diff --git a/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_DoomSpike.cs b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_DoomSpike.cs
new file mode 100644
index 000000000..a5a4fd932
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_DoomSpike.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Melia.Shared.Data.Database;
+using Melia.Shared.Game.Const;
+using Melia.Shared.L10N;
+using Melia.Shared.World;
+using Melia.Zone.Network;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.Skills.Handlers.Base;
+using Melia.Zone.Skills.SplashAreas;
+using Melia.Zone.World.Actors;
+using static Melia.Shared.Util.TaskHelper;
+using static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Swordsmen.Cataphract
+{
+ ///
+ /// Handler for the Cataphract skill Doom Spike.
+ ///
+ [SkillHandler(SkillId.Cataphract_DoomSpike)]
+ public class Cataphract_DoomSpike : IGroundSkillHandler
+ {
+ ///
+ /// Handles skill, damaging targets.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target)
+ {
+ if (!caster.TrySpendSp(skill))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+
+ skill.IncreaseOverheat();
+ caster.TurnTowards(farPos);
+ caster.SetAttackState(true);
+
+ var splashParam = skill.GetSplashParameters(caster, originPos, farPos, length: 120, width: 30, angle: 0);
+ var splashArea = skill.GetSplashArea(SplashType.Square, splashParam);
+
+ Send.ZC_SKILL_READY(caster, skill, originPos, farPos);
+ Send.ZC_SKILL_MELEE_GROUND(caster, skill, farPos, null);
+
+ CallSafe(Attack(skill, caster, splashArea));
+ }
+
+ ///
+ /// Executes the actual attack after a delay.
+ ///
+ ///
+ ///
+ ///
+ private static async Task Attack(Skill skill, ICombatEntity caster, ISplashArea splashArea)
+ {
+ var hitDelay = TimeSpan.FromMilliseconds(200);
+ var damageDelay = TimeSpan.FromMilliseconds(50);
+ var skillHitDelay = TimeSpan.Zero;
+
+ await Task.Delay(hitDelay);
+
+ var bonusMultiplier = 0f;
+ if (caster.TryGetSkill(SkillId.Cataphract_AcrobaticMount, out var acrobaticMount))
+ {
+ bonusMultiplier = 0.5f + acrobaticMount.Level * 0.06f;
+ }
+
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+ var hits = new List();
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var modifier = SkillModifier.MultiHit(5);
+ modifier.DamageMultiplier += bonusMultiplier;
+
+ modifier.MinCritChance = MathF.Max(10 + skill.Level, modifier.MinCritChance);
+
+ var skillHitResult = SCR_SkillHit(caster, target, skill, modifier);
+ target.TakeDamage(skillHitResult.Damage, caster);
+
+ var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay);
+
+ if (caster.IsAbilityActive(AbilityId.Cataphract30))
+ {
+ skillHit.HitEffect = HitEffect.Impact;
+ }
+ else
+ {
+ skillHit.KnockBackInfo = new KnockBackInfo(caster.Position, target.Position, skill);
+ skillHit.HitInfo.Type = skill.Data.KnockDownHitType;
+ target.Position = skillHit.KnockBackInfo.ToPosition;
+ }
+
+ hits.Add(skillHit);
+ }
+
+ Send.ZC_SKILL_HIT_INFO(caster, hits);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_EarthWave.cs b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_EarthWave.cs
new file mode 100644
index 000000000..75062a7ca
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_EarthWave.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Melia.Shared.Data.Database;
+using Melia.Shared.Game.Const;
+using Melia.Shared.L10N;
+using Melia.Shared.World;
+using Melia.Zone.Network;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.Skills.Handlers.Base;
+using Melia.Zone.Skills.SplashAreas;
+using Melia.Zone.World.Actors;
+using static Melia.Shared.Util.TaskHelper;
+using static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Swordsmen.Cataphract
+{
+ ///
+ /// Handler for the Cataphract skill Earth Wave.
+ ///
+ [SkillHandler(SkillId.Cataphract_EarthWave)]
+ public class Cataphract_EarthWave : IGroundSkillHandler
+ {
+ ///
+ /// Handles skill, damaging targets.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target)
+ {
+ if (!caster.TrySpendSp(skill))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+
+ skill.IncreaseOverheat();
+ caster.TurnTowards(farPos);
+ caster.SetAttackState(true);
+
+ var splashParam = skill.GetSplashParameters(caster, originPos, farPos, length: 35, width: 65, angle: 0);
+ var splashArea = skill.GetSplashArea(SplashType.Circle, splashParam);
+
+ Send.ZC_SKILL_READY(caster, skill, originPos, farPos);
+ Send.ZC_SKILL_MELEE_GROUND(caster, skill, farPos, null);
+
+ CallSafe(Attack(skill, caster, splashArea));
+ }
+
+ ///
+ /// Executes the actual attack after a delay.
+ ///
+ ///
+ ///
+ ///
+ private static async Task Attack(Skill skill, ICombatEntity caster, ISplashArea splashArea)
+ {
+ var hitDelay = TimeSpan.FromMilliseconds(550);
+ var damageDelay = TimeSpan.FromMilliseconds(50);
+ var skillHitDelay = TimeSpan.Zero;
+
+ await Task.Delay(hitDelay);
+
+ var bonusMultiplier = 0f;
+ if (caster.TryGetSkill(SkillId.Cataphract_AcrobaticMount, out var acrobaticMount))
+ {
+ bonusMultiplier = 0.5f + acrobaticMount.Level * 0.06f;
+ }
+
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+ var hits = new List();
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var modifier = SkillModifier.MultiHit(5);
+ modifier.DamageMultiplier += bonusMultiplier;
+
+ if (caster.IsAbilityActive(AbilityId.Cataphract26))
+ {
+ modifier.OverrideAttackAttribute = true;
+ modifier.AttackAttribute = SkillAttribute.Earth;
+ }
+
+ var skillHitResult = SCR_SkillHit(caster, target, skill, modifier);
+ target.TakeDamage(skillHitResult.Damage, caster);
+
+ var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay);
+ skillHit.HitEffect = HitEffect.Impact;
+ hits.Add(skillHit);
+ }
+
+ Send.ZC_SKILL_HIT_INFO(caster, hits);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_Impaler.cs b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_Impaler.cs
new file mode 100644
index 000000000..01d818814
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_Impaler.cs
@@ -0,0 +1,178 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Melia.Shared.Data.Database;
+using Melia.Shared.Game.Const;
+using Melia.Shared.L10N;
+using Melia.Shared.World;
+using Melia.Zone.Network;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.Skills.Handlers.Base;
+using Melia.Zone.Skills.SplashAreas;
+using Melia.Zone.World.Actors;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using static Melia.Shared.Util.TaskHelper;
+using static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Swordsmen.Cataphract
+{
+ ///
+ /// Handler for the Cataphract skill Impaler.
+ ///
+ [SkillHandler(SkillId.Cataphract_Impaler)]
+ public class Cataphract_Impaler : IGroundSkillHandler
+ {
+ ///
+ /// Handles skill, damaging targets.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target)
+ {
+ if (!caster.TrySpendSp(skill))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+
+ caster.TurnTowards(farPos);
+ caster.SetAttackState(true);
+
+ if (!caster.IsBuffActive(BuffId.Impaler_Buff))
+ {
+ var grabParam = skill.GetSplashParameters(caster, originPos, farPos, length: 15, width: 50, angle: 0);
+ var grabArea = skill.GetSplashArea(SplashType.Circle, grabParam);
+
+ Send.ZC_SKILL_READY(caster, skill, originPos, farPos);
+ Send.ZC_SKILL_MELEE_GROUND(caster, skill, farPos, null);
+
+ CallSafe(Grab(skill, caster, grabArea));
+ }
+ else
+ {
+ var slamParam = skill.GetSplashParameters(caster, originPos, farPos, length: 25, width: 35, angle: 0);
+ var slamArea = skill.GetSplashArea(SplashType.Circle, slamParam);
+
+ Send.ZC_SKILL_READY(caster, skill, originPos, farPos);
+ Send.ZC_SKILL_MELEE_GROUND(caster, skill, farPos, null);
+
+ CallSafe(Slam(skill, caster, slamArea));
+ }
+ }
+
+ ///
+ /// Attempts to grab a target
+ ///
+ ///
+ ///
+ ///
+ private static async Task Grab(Skill skill, ICombatEntity caster, ISplashArea splashArea)
+ {
+ var hitDelay = TimeSpan.FromMilliseconds(250);
+ var damageDelay = TimeSpan.FromMilliseconds(50);
+ var skillHitDelay = TimeSpan.Zero;
+
+ await Task.Delay(hitDelay);
+
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+ var hits = new List();
+
+ var grabbedSomething = false;
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var targetSize = target.EffectiveSize;
+
+ if (targetSize != SizeType.PC && targetSize >= SizeType.L)
+ continue;
+
+ target.StartBuff(BuffId.Impaler_Debuff, skill.Level, 0, TimeSpan.FromSeconds(10), caster);
+ var impalerBuff = caster.StartBuff(BuffId.Impaler_Buff, skill.Level, 0, TimeSpan.FromSeconds(10), target);
+
+ impalerBuff.UpdateTime = TimeSpan.FromMilliseconds(100);
+ grabbedSomething = true;
+ break;
+ }
+
+ // Only overheat here on a miss
+ if (!grabbedSomething)
+ {
+ skill.IncreaseOverheat();
+ return;
+ }
+ }
+
+ ///
+ /// Slam the target to deal damage
+ ///
+ ///
+ /// This does not play the right animation. It should play SKL_IMPALER_THROW_RIDE.
+ ///
+ ///
+ ///
+ ///
+ private static async Task Slam(Skill skill, ICombatEntity caster, ISplashArea splashArea)
+ {
+ var hitDelay = TimeSpan.FromMilliseconds(200);
+ var damageDelay = TimeSpan.FromMilliseconds(50);
+ var skillHitDelay = TimeSpan.Zero;
+
+ await Task.Delay(hitDelay);
+
+ var bonusMultiplier = 0f;
+ if (caster.TryGetSkill(SkillId.Cataphract_AcrobaticMount, out var acrobaticMount))
+ {
+ bonusMultiplier = 0.5f + acrobaticMount.Level * 0.06f;
+ }
+
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+ var hits = new List();
+
+ // remove the skewered mob from the list of targets if they're present
+ // They get hit separately at the end.
+ if (caster.TryGetBuff(BuffId.Impaler_Buff, out var buff))
+ {
+ var skewered = buff.Caster;
+ targets.Remove(skewered);
+
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var modifier = SkillModifier.MultiHit(3);
+ modifier.DamageMultiplier += bonusMultiplier;
+
+ if (caster.IsAbilityActive(AbilityId.Cataphract26))
+ {
+ modifier.OverrideAttackAttribute = true;
+ modifier.AttackAttribute = SkillAttribute.Earth;
+ }
+
+ var skillHitResult = SCR_SkillHit(caster, target, skill, modifier);
+ target.TakeDamage(skillHitResult.Damage, caster);
+
+ var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay);
+ skillHit.HitEffect = HitEffect.Impact;
+ hits.Add(skillHit);
+ }
+
+
+ var modifier2 = SkillModifier.MultiHit(3);
+ modifier2.ForcedCritical = true;
+ modifier2.DamageMultiplier += bonusMultiplier;
+
+ var skillHitResult2 = SCR_SkillHit(caster, skewered, skill, modifier2);
+ skewered.TakeDamage(skillHitResult2.Damage, caster);
+
+ var skillHit2 = new SkillHitInfo(caster, skewered, skill, skillHitResult2, damageDelay, skillHitDelay);
+ skillHit2.HitEffect = HitEffect.Impact;
+ hits.Add(skillHit2);
+
+ }
+
+ Send.ZC_SKILL_HIT_INFO(caster, hits);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_Rush.cs b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_Rush.cs
new file mode 100644
index 000000000..124036d68
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_Rush.cs
@@ -0,0 +1,166 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Melia.Shared.Data.Database;
+using Melia.Shared.Game.Const;
+using Melia.Shared.L10N;
+using Melia.Shared.ObjectProperties;
+using Melia.Shared.World;
+using Melia.Zone.Network;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.Skills.Handlers.Base;
+using Melia.Zone.World.Actors;
+using Melia.Zone.World.Actors.Characters;
+using Yggdrasil.Logging;
+using Yggdrasil.Util;
+using static Melia.Shared.Util.TaskHelper;
+using static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Swordsmen.Cataphract
+{
+ ///
+ /// Handler for the Cataphract skill Rush.
+ ///
+ [SkillHandler(SkillId.Cataphract_Rush)]
+ public class Cataphract_Rush : IGroundSkillHandler, IDynamicCasted
+ {
+ public void StartDynamicCast(Skill skill, ICombatEntity caster)
+ {
+ caster.StartBuff(BuffId.Warrior_RushMove_Buff, 1, 0, TimeSpan.Zero, caster);
+ caster.StartBuff(BuffId.Warrior_EnableMovingShot_Buff, 2, 0, TimeSpan.Zero, caster);
+
+ // This has to be sent here to set the MovingShotable Property
+ // which isn't applied until after the character has the buff
+ if (caster is Character character)
+ {
+ Send.ZC_OBJECT_PROPERTY(character);
+ Send.ZC_MOVE_SPEED(character);
+ }
+
+ Send.ZC_PLAY_SOUND(caster, "voice_war_atk_long_cast");
+ }
+
+ ///
+ /// Called when the user stops casting the skill.
+ ///
+ ///
+ ///
+ public void EndDynamicCast(Skill skill, ICombatEntity caster)
+ {
+ caster.StopBuff(BuffId.Warrior_RushMove_Buff);
+ caster.StopBuff(BuffId.Warrior_EnableMovingShot_Buff);
+
+ if (caster is Character character)
+ {
+ Send.ZC_OBJECT_PROPERTY(character);
+ Send.ZC_MOVE_SPEED(character);
+ }
+
+ Send.ZC_STOP_SOUND(caster, "voice_war_atk_long_cast");
+ }
+
+
+ ///
+ /// Handles skill, damaging targets.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target)
+ {
+ skill.IncreaseOverheat();
+
+ Send.ZC_SKILL_READY(caster, skill, originPos, farPos);
+ Send.ZC_SKILL_MELEE_GROUND(caster, skill, farPos, null);
+
+ CallSafe(this.Attack(skill, caster));
+ }
+
+ ///
+ /// Executes the actual attack after a delay.
+ ///
+ ///
+ ///
+ private async Task Attack(Skill skill, ICombatEntity caster)
+ {
+ var hits = new List();
+ var hitDelay = TimeSpan.FromMilliseconds(30);
+ var damageDelay = TimeSpan.FromMilliseconds(50);
+ var skillHitDelay = TimeSpan.Zero;
+
+ var totalHits = 10;
+ var attackWidth = 40f;
+ var delayBetweenRepeats = TimeSpan.FromMilliseconds(270);
+
+ for (var i = 0; i < totalHits; ++i)
+ {
+ await Task.Delay(hitDelay);
+
+ if (!caster.TrySpendSp(10 + skill.Level))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ Send.ZC_SKILL_CAST_CANCEL(caster);
+ return;
+ }
+
+ if (caster.IsAbilityActive(AbilityId.Cataphract34))
+ {
+ // TODO: this art changes this move into a kind of vaccuum effect
+ }
+
+ var bonusMultiplier = 0f;
+ if (caster.TryGetSkill(SkillId.Cataphract_AcrobaticMount, out var acrobaticMount))
+ {
+ bonusMultiplier = 0.5f + acrobaticMount.Level * 0.06f;
+ }
+
+ var splashParam = skill.GetSplashParameters(caster, caster.Position, caster.Position, length: 0, width: attackWidth, angle: 0);
+ var splashArea = skill.GetSplashArea(SplashType.Circle, splashParam);
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var modifier = SkillModifier.Default;
+ modifier.Unblockable = true;
+ modifier.DamageMultiplier += bonusMultiplier;
+
+ // double crit rate if they have fear
+ if (target.IsBuffActive(BuffId.Common_Fear))
+ {
+ modifier.BonusCritChance = caster.Properties.GetFloat(PropertyName.CRTHR);
+ }
+
+ if (caster.TryGetActiveAbilityLevel(AbilityId.Cataphract1, out var level))
+ {
+ var stunChance = level * 5f;
+ if (RandomProvider.Get().Next(100) < stunChance)
+ {
+ target.StartBuff(BuffId.Stun, TimeSpan.FromSeconds(2));
+ }
+ }
+
+ var skillHitResult = SCR_SkillHit(caster, target, skill, modifier);
+ target.TakeDamage(skillHitResult.Damage, caster);
+
+ var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay);
+ skillHit.HitEffect = HitEffect.Impact;
+
+ hits.Add(skillHit);
+ }
+
+ Send.ZC_SKILL_HIT_INFO(caster, hits);
+
+ if (i < totalHits - 1)
+ await Task.Delay(delayBetweenRepeats);
+
+ hits.Clear();
+
+ if (!caster.IsCasting())
+ break;
+ }
+
+ Send.ZC_SKILL_DISABLE(caster as Character);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_SteedCharge.cs b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_SteedCharge.cs
new file mode 100644
index 000000000..0f9e8f792
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_SteedCharge.cs
@@ -0,0 +1,228 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Melia.Shared.Data.Database;
+using Melia.Shared.Game.Const;
+using Melia.Shared.L10N;
+using Melia.Shared.World;
+using Melia.Zone.Network;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.Skills.Handlers.Base;
+using Melia.Zone.Skills.SplashAreas;
+using Melia.Zone.World.Actors;
+using Melia.Zone.World.Actors.Monsters;
+using Melia.Zone.World.Actors.Pads;
+using Yggdrasil.Logging;
+using static Melia.Shared.Util.TaskHelper;
+using static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Swordsmen.Cataphract
+{
+ ///
+ /// Handler for the Cataphract skill Steed Charge.
+ ///
+ [SkillHandler(SkillId.Cataphract_SteedCharge)]
+ public class Cataphract_SteedCharge : IGroundSkillHandler
+ {
+ public const float JumpDistance = 175f;
+ public const float AttackDistance = 25f;
+
+ ///
+ /// Handles skill, damaging targets.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target)
+ {
+ if (!caster.TrySpendSp(skill))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+
+ skill.IncreaseOverheat();
+ // Note that the "Direction" sent with the attack packet for
+ // this skill is actually wrong, so we have to use the farpos
+ caster.TurnTowards(farPos);
+ caster.SetAttackState(true);
+
+ var jumpDistance = JumpDistance;
+ var attackDistance = AttackDistance;
+
+ // Cataphract40 makes it go only half as far
+ if (caster.IsAbilityActive(AbilityId.Cataphract40))
+ {
+ jumpDistance /= 2f;
+ attackDistance /= 2f;
+ }
+
+ var targetPos = caster.Position.GetRelative(caster.Direction, jumpDistance);
+
+ targetPos = caster.Map.Ground.GetLastValidPosition(caster.Position, targetPos);
+ var actualDistance = (float)caster.Position.Get2DDistance(targetPos);
+
+ var splashParam = skill.GetSplashParameters(caster, originPos, targetPos, length: actualDistance + attackDistance, width: 28, angle: 0);
+ var splashArea = skill.GetSplashArea(SplashType.Square, splashParam);
+
+ Send.ZC_SKILL_READY(caster, skill, originPos, targetPos);
+ Send.ZC_SKILL_MELEE_GROUND(caster, skill, targetPos, null);
+
+ Send.ZC_NORMAL.LeapJump(caster, targetPos, 0, 0, 1f, 0.35f, 0.7f, 0);
+
+ CallSafe(Attack(skill, caster, splashArea));
+
+ // Spawn Fire Pads if this is the Fire Chariot version
+ if (caster.IsAbilityActive(AbilityId.Cataphract40))
+ {
+ // The visual of the pad extends 25 units behind the
+ // pad's position. The second pad doesn't include
+ // this part in its hit area so they don't overlap.
+ var pad1StartPosition = originPos.GetRelative(caster.Direction.Backwards, 25f);
+ var pad1Param = skill.GetSplashParameters(caster, pad1StartPosition, targetPos, length: 75, width: 28, angle: 0);
+ var pad1Area = skill.GetSplashArea(SplashType.Square, pad1Param);
+ var pad1 = new Pad(PadName.Cataphract_SteedCharge, caster, skill, pad1Area);
+ pad1.Position = originPos;
+ pad1.Trigger.LifeTime = TimeSpan.FromSeconds(5);
+ pad1.Trigger.UpdateInterval = TimeSpan.FromSeconds(1);
+ pad1.Trigger.MaxActorCount = 15;
+ caster.Map.AddPad(pad1);
+ pad1.Trigger.Subscribe(TriggerType.Update, this.OnTriggerUpdate);
+
+ // We don't spawn the second pad if the jump is cut short
+ if (actualDistance >= 75f)
+ {
+ var pad2StartPosition = originPos.GetRelative(caster.Direction, 50f);
+ var pad2Param = skill.GetSplashParameters(caster, pad2StartPosition, targetPos, length: 50, width: 28, angle: 0);
+ var pad2Area = skill.GetSplashArea(SplashType.Square, pad1Param);
+ var pad2 = new Pad(PadName.Cataphract_SteedCharge, caster, skill, pad2Area);
+ pad2.Position = originPos.GetRelative(caster.Direction, 50f);
+ pad2.Trigger.LifeTime = TimeSpan.FromSeconds(5);
+ pad2.Trigger.UpdateInterval = TimeSpan.FromSeconds(1);
+ pad2.Trigger.MaxActorCount = 15;
+ caster.Map.AddPad(pad2);
+ pad2.Trigger.Subscribe(TriggerType.Update, this.OnTriggerUpdate);
+ }
+ }
+ }
+
+ ///
+ /// Executes the actual attack after a delay.
+ ///
+ ///
+ ///
+ ///
+ private static async Task Attack(Skill skill, ICombatEntity caster, ISplashArea splashArea)
+ {
+ var hitDelay = TimeSpan.FromMilliseconds(200);
+ var damageDelay = TimeSpan.FromMilliseconds(50);
+ var skillHitDelay = TimeSpan.Zero;
+
+ await Task.Delay(hitDelay);
+
+ var bonusMultiplier = 0f;
+ if (caster.TryGetSkill(SkillId.Cataphract_AcrobaticMount, out var acrobaticMount))
+ {
+ bonusMultiplier = 0.5f + acrobaticMount.Level * 0.06f;
+ }
+
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+ var hits = new List();
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var modifier = SkillModifier.MultiHit(3);
+ modifier.DamageMultiplier += bonusMultiplier;
+
+ // Always criticals against slowed targets
+ if (target.IsBuffActive(BuffId.Common_Slow) && caster.IsAbilityActive(AbilityId.Cataphract27))
+ {
+ modifier.ForcedCritical = true;
+ }
+
+ if (caster.IsAbilityActive(AbilityId.Cataphract40))
+ {
+ modifier.OverrideAttackAttribute = true;
+ modifier.AttackAttribute = SkillAttribute.Fire;
+ }
+
+ var skillHitResult = SCR_SkillHit(caster, target, skill, modifier);
+ target.TakeDamage(skillHitResult.Damage, caster);
+
+ var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay);
+
+ if (caster.IsAbilityActive(AbilityId.Cataphract29))
+ {
+ skillHit.HitEffect = HitEffect.Impact;
+ }
+ else
+ {
+ skillHit.KnockBackInfo = new KnockBackInfo(caster.Position, target.Position, skill);
+ skillHit.HitInfo.Type = HitType.KnockBack;
+ target.Position = skillHit.KnockBackInfo.ToPosition;
+ }
+
+ hits.Add(skillHit);
+
+ // Inflicts 3 second slow on critical hit only
+ // Presumably this doesn't apply if Cataphract27 is on
+ if (!caster.IsAbilityActive(AbilityId.Cataphract27))
+ {
+ if (skillHitResult.Result == HitResultType.Crit)
+ {
+ target.StartBuff(BuffId.Common_Slow, skill.Level, 0, TimeSpan.FromSeconds(3), caster);
+ }
+ }
+ }
+
+ Send.ZC_SKILL_HIT_INFO(caster, hits);
+ }
+
+ ///
+ /// Called when an actor enters the area of the attack.
+ ///
+ ///
+ ///
+ private void OnTriggerUpdate(object sender, PadTriggerArgs args)
+ {
+ var pad = args.Trigger;
+ var caster = args.Creator;
+ var skill = args.Skill;
+
+ Log.Warning("Flame tick");
+
+ var targets = pad.Trigger.GetAttackableEntities(caster);
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ FireDamage(skill, caster, target);
+ }
+
+
+ ///
+ /// Deals the damage from the pad
+ ///
+ ///
+ ///
+ ///
+ private static void FireDamage(Skill skill, ICombatEntity caster, ICombatEntity target)
+ {
+ var damageDelay = TimeSpan.FromMilliseconds(50);
+ var skillHitDelay = TimeSpan.Zero;
+
+
+ var modifier = SkillModifier.Default;
+ modifier.OverrideAttackAttribute = true;
+ modifier.AttackAttribute = SkillAttribute.Fire;
+
+ var skillHitResult = SCR_SkillHit(caster, target, skill, modifier);
+
+ target.TakeDamage(skillHitResult.Damage / 2, caster);
+
+ var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay);
+ skillHit.HitEffect = HitEffect.Impact;
+
+ Send.ZC_SKILL_HIT_INFO(caster, skillHit);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_Trot.cs b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_Trot.cs
new file mode 100644
index 000000000..960cadd62
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_Trot.cs
@@ -0,0 +1,52 @@
+using System;
+using Melia.Shared.Game.Const;
+using Melia.Shared.L10N;
+using Melia.Shared.World;
+using Melia.Zone.Network;
+using Melia.Zone.Skills.Handlers.Base;
+using Melia.Zone.World.Actors;
+
+namespace Melia.Zone.Skills.Handlers.Swordsmen.Cataphract
+{
+ ///
+ /// Handler for the Cataphract skill Trot.
+ ///
+ [SkillHandler(SkillId.Cataphract_Trot)]
+ public class Cataphract_Trot : ISelfSkillHandler
+ {
+ ///
+ /// Handles skill, applying the buff to the caster.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, Position originPos, Direction dir)
+ {
+ skill.IncreaseOverheat();
+ caster.SetAttackState(true);
+
+ var target = caster;
+
+ // This functions like a stance, it's removed if you cast it again
+
+ if (target.IsBuffActive(BuffId.Trot_Buff))
+ {
+ target.StopBuff(BuffId.Trot_Buff);
+ }
+ else
+ {
+ // It's not clear if this consumes SP on cast, but it probably
+ // should so you can't just keep reapplying it when out of SP.
+ if (!caster.TrySpendSp(20))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+ target.StartBuff(BuffId.Trot_Buff, skill.Level, 0, TimeSpan.Zero, caster);
+ }
+
+ Send.ZC_SKILL_MELEE_TARGET(caster, skill, target, null);
+ }
+ }
+}
diff --git a/system/scripts/zone/core/calc_combat.cs b/system/scripts/zone/core/calc_combat.cs
index c4d5c57d4..71743440a 100644
--- a/system/scripts/zone/core/calc_combat.cs
+++ b/system/scripts/zone/core/calc_combat.cs
@@ -277,6 +277,9 @@ public float SCR_AttributeMultiplier(ICombatEntity attacker, ICombatEntity targe
var attackerAttr = skill.Data.Attribute;
var targetAttr = target.Attribute;
+ if (modifier.OverrideAttackAttribute)
+ attackerAttr = modifier.AttackAttribute;
+
if (!Feature.IsEnabled("AttributeBonusRevamp"))
{
if (attackerAttr == SkillAttribute.Fire)