diff --git a/src/ZoneServer/Buffs/Buff.cs b/src/ZoneServer/Buffs/Buff.cs
index 08d87024b..c7f11b28d 100644
--- a/src/ZoneServer/Buffs/Buff.cs
+++ b/src/ZoneServer/Buffs/Buff.cs
@@ -267,6 +267,17 @@ internal void ExtendDuration()
this.NextUpdateTime = DateTime.Now.Add(this.Data.UpdateTime);
}
+ ///
+ /// Increases the buff's duration by a given amount.
+ ///
+ internal void IncreaseDuration(TimeSpan amount)
+ {
+ if (this.HasDuration)
+ {
+ this.RemovalTime = DateTime.Now.Add(amount);
+ }
+ }
+
///
/// Executes the buff handler's end behavior. Does not actually
/// end or remove the buff.
diff --git a/src/ZoneServer/Buffs/Handlers/Common/Common_Silence.cs b/src/ZoneServer/Buffs/Handlers/Common/Common_Silence.cs
new file mode 100644
index 000000000..90320f74e
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Common/Common_Silence.cs
@@ -0,0 +1,19 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.World.Actors;
+using Melia.Zone.World.Actors.Components;
+
+namespace Melia.Zone.Buffs.Handlers.Common
+{
+ ///
+ /// Handle for the Silence Debuff, which prevents taking any action
+ ///
+ [BuffHandler(BuffId.Common_Silence)]
+ public class Common_Silence : BuffHandler
+ {
+ public override void OnExtend(Buff buff)
+ {
+ buff.Target.AddState(StateType.Stunned, buff.Duration);
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Common/Skill_MomentaryEvasion_Buff.cs b/src/ZoneServer/Buffs/Handlers/Common/Skill_MomentaryEvasion_Buff.cs
new file mode 100644
index 000000000..db6711139
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Common/Skill_MomentaryEvasion_Buff.cs
@@ -0,0 +1,34 @@
+using Melia.Shared.Data.Database;
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Skills;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.World.Actors;
+
+namespace Melia.Zone.Buffs.Handlers.Common
+{
+ ///
+ /// Contains code related to the Momentary Block Buff
+ ///
+ ///
+ /// This buff is granted by certain skills and causes you to always
+ /// evade for a duration.
+ ///
+ [BuffHandler(BuffId.Skill_MomentaryEvasion_Buff)]
+ public class Skill_MomentaryEvasion_Buff : BuffHandler, IBuffCombatDefenseBeforeCalcHandler
+ {
+ ///
+ /// Applies the buff's effect during the combat calculations.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void OnDefenseBeforeCalc(Buff buff, ICombatEntity attacker, ICombatEntity target, Skill skill, SkillModifier modifier, SkillHitResult skillHitResult)
+ {
+ modifier.ForcedEvade = true;
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/Aggress_Buff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/Aggress_Buff.cs
new file mode 100644
index 000000000..9c063e2e1
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/Aggress_Buff.cs
@@ -0,0 +1,37 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Network;
+
+namespace Melia.Zone.Buffs.Handlers.Scouts.OutLaw
+{
+ ///
+ /// Handler for Aggress Buff, which massively increases movement speed
+ ///
+ [BuffHandler(BuffId.Aggress_Buff)]
+ public class Aggress_Buff : BuffHandler
+ {
+ private const float MspdBonus = 30f;
+
+ ///
+ /// Starts buff, modifying the movement speed
+ ///
+ ///
+ public override void OnActivate(Buff buff, ActivationType activationType)
+ {
+ var target = buff.Target;
+
+ AddPropertyModifier(buff, target, PropertyName.MSPD_BM, MspdBonus);
+ Send.ZC_MSPD(target);
+ }
+
+ ///
+ /// Ends the buff, resetting the movement speed
+ ///
+ ///
+ public override void OnEnd(Buff buff)
+ {
+ RemovePropertyModifier(buff, buff.Target, PropertyName.MSPD_BM);
+ Send.ZC_MSPD(buff.Target);
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/Aggress_Debuff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/Aggress_Debuff.cs
new file mode 100644
index 000000000..1bae39d3c
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/Aggress_Debuff.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Network;
+
+namespace Melia.Zone.Buffs.Handlers.Scouts.OutLaw
+{
+ ///
+ /// Handler for Break Brick Debuff, which reduces Evasion
+ ///
+ ///
+ /// NumArg1: Skill Level
+ /// NumArg2: Unhinge Level
+ /// This is related to Outlaw13, which changes the buff effect to
+ /// also increase movement speed but decrease accuracy
+ ///
+ [BuffHandler(BuffId.Aggress_Debuff)]
+ internal class Aggress_Debuff : BuffHandler
+ {
+ private const float DRPenaltyPerLevel = 0.02f;
+ private const float HRPenaltyPerLevel = 0.03f;
+ private const float MspdBonus = 5f;
+
+ ///
+ /// Starts buff, reducing Dodge Rate
+ ///
+ ///
+ public override void OnActivate(Buff buff, ActivationType activationType)
+ {
+ var reduceDR = buff.Target.Properties.GetFloat(PropertyName.DR) * buff.NumArg1 * DRPenaltyPerLevel;
+
+ AddPropertyModifier(buff, buff.Target, PropertyName.DR_BM, -reduceDR);
+
+ if (buff.NumArg2 > 0)
+ {
+ var reduceHR = buff.Target.Properties.GetFloat(PropertyName.HR) * buff.NumArg2 * HRPenaltyPerLevel;
+
+ AddPropertyModifier(buff, buff.Target, PropertyName.HR_BM, -reduceHR);
+ AddPropertyModifier(buff, buff.Target, PropertyName.MSPD_BM, MspdBonus);
+ Send.ZC_MSPD(buff.Target);
+ }
+ }
+
+ ///
+ /// Ends the buff, resetting dodge rate.
+ ///
+ ///
+ public override void OnEnd(Buff buff)
+ {
+ RemovePropertyModifier(buff, buff.Target, PropertyName.DR_BM);
+
+ if (buff.NumArg2 > 0)
+ {
+ RemovePropertyModifier(buff, buff.Target, PropertyName.HR_BM);
+ RemovePropertyModifier(buff, buff.Target, PropertyName.MSPD_BM);
+ Send.ZC_MSPD(buff.Target);
+ }
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/BreakBrick_Debuff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/BreakBrick_Debuff.cs
new file mode 100644
index 000000000..34948b5bb
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/BreakBrick_Debuff.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+
+namespace Melia.Zone.Buffs.Handlers.Scouts.OutLaw
+{
+ ///
+ /// Handler for Break Brick Debuff, which reduces Crit Chance
+ ///
+ ///
+ /// NumArg1: Skill Level
+ /// NumArg2: None
+ ///
+ [BuffHandler(BuffId.BreakBrick_Debuff)]
+ internal class BreakBrick_Debuff : BuffHandler
+ {
+ private const float CRTPenaltyPerLevel = 1f;
+
+ ///
+ /// Starts buff, reducing Crit rate
+ ///
+ ///
+ public override void OnActivate(Buff buff, ActivationType activationType)
+ {
+ var reduceCrt = buff.Target.Properties.GetFloat(PropertyName.CRTHR) * buff.NumArg1 * CRTPenaltyPerLevel;
+
+ AddPropertyModifier(buff, buff.Target, PropertyName.CRTHR_BM, -reduceCrt);
+ }
+
+ ///
+ /// Ends the buff, resetting Crit rate.
+ ///
+ ///
+ public override void OnEnd(Buff buff)
+ {
+ RemovePropertyModifier(buff, buff.Target, PropertyName.CRTHR_BM);
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/BullyPainBarrier_Buff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/BullyPainBarrier_Buff.cs
new file mode 100644
index 000000000..39af8e442
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/BullyPainBarrier_Buff.cs
@@ -0,0 +1,77 @@
+using System;
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Scripting.AI;
+using Melia.Zone.Skills;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.World.Actors;
+using Melia.Zone.World.Actors.CombatEntities.Components;
+using Melia.Zone.World.Actors.Monsters;
+
+namespace Melia.Zone.Buffs.Handlers.Scouts.OutLaw
+{
+ ///
+ /// Buff handler for Bully Pain Barrier Buff, which increases evasion
+ /// and adds threat on successful evade
+ /// It's completely identical to Bully Buff except that it also
+ /// grants immunity to knockback and Outlaw12's effect is nerfed
+ ///
+ ///
+ /// NumArg1: Skill Level
+ /// NumArg2: None
+ ///
+ [BuffHandler(BuffId.BullyPainBarrier_Buff)]
+ public class BullyPainBarrier_Buff : BuffHandler, IBuffCombatDefenseAfterCalcHandler
+ {
+ private const float DrBuffRateBase = 0.24f;
+ private const float DrBuffRatePerLevel = 0.04f;
+ private const float HatePerLevel = 3f;
+
+ ///
+ /// Starts buff, increasing dodge rate.
+ ///
+ ///
+ public override void OnActivate(Buff buff, ActivationType activationType)
+ {
+ var dr = buff.Target.Properties.GetFloat(PropertyName.DR);
+ var skillLevel = buff.NumArg1;
+ var rate = DrBuffRateBase + DrBuffRatePerLevel * skillLevel;
+ var bonus = dr * rate;
+
+ AddPropertyModifier(buff, buff.Target, PropertyName.DR_BM, bonus);
+ }
+
+ ///
+ /// Ends the buff, resetting dodge rate.
+ ///
+ ///
+ public override void OnEnd(Buff buff)
+ {
+ RemovePropertyModifier(buff, buff.Target, PropertyName.DR_BM);
+ }
+
+ ///
+ /// Applies the buff's effect during the combat calculations.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void OnDefenseAfterCalc(Buff buff, ICombatEntity attacker, ICombatEntity target, Skill skill, SkillModifier modifier, SkillHitResult skillHitResult)
+ {
+ if (skillHitResult.Result == HitResultType.Dodge && attacker.Components.TryGet(out var component))
+ {
+ component.Script.QueueEventAlert(new HateIncreaseAlert(target, buff.NumArg1 * HatePerLevel));
+
+ // Outlaw12 adds additional duration to the buff on successful evade
+ // For Pain Barrier buff, the maximum increase is 2 seconds
+ if (target.TryGetActiveAbilityLevel(AbilityId.Outlaw12, out var level))
+ {
+ buff.IncreaseDuration(TimeSpan.FromSeconds(Math.Max(level, 2)));
+ }
+ }
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/Bully_Buff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/Bully_Buff.cs
new file mode 100644
index 000000000..21e1dd388
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/Bully_Buff.cs
@@ -0,0 +1,76 @@
+using System;
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Scripting.AI;
+using Melia.Zone.Skills;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.World.Actors;
+using Melia.Zone.World.Actors.CombatEntities.Components;
+using Melia.Zone.World.Actors.Monsters;
+
+namespace Melia.Zone.Buffs.Handlers.Scouts.OutLaw
+{
+ ///
+ /// Buff handler for Bully Buff, which increases evasion
+ /// and adds threat on successful evade
+ ///
+ ///
+ /// NumArg1: Skill Level
+ /// NumArg2: None
+ ///
+ [BuffHandler(BuffId.Bully_Buff)]
+ public class Bully_Buff : BuffHandler, IBuffCombatDefenseAfterCalcHandler
+ {
+ private const float DrBuffRateBase = 0.24f;
+ private const float DrBuffRatePerLevel = 0.04f;
+ private const float HatePerLevel = 3f;
+
+ ///
+ /// Starts buff, increasing dodge rate.
+ ///
+ ///
+ public override void OnActivate(Buff buff, ActivationType activationType)
+ {
+ var dr = buff.Target.Properties.GetFloat(PropertyName.DR);
+ var skillLevel = buff.NumArg1;
+ var rate = DrBuffRateBase + DrBuffRatePerLevel * skillLevel;
+ var bonus = dr * rate;
+
+ AddPropertyModifier(buff, buff.Target, PropertyName.DR_BM, bonus);
+ }
+
+ ///
+ /// Ends the buff, resetting dodge rate.
+ ///
+ ///
+ public override void OnEnd(Buff buff)
+ {
+ RemovePropertyModifier(buff, buff.Target, PropertyName.DR_BM);
+ }
+
+ ///
+ /// Applies the buff's effect during the combat calculations.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void OnDefenseAfterCalc(Buff buff, ICombatEntity attacker, ICombatEntity target, Skill skill, SkillModifier modifier, SkillHitResult skillHitResult)
+ {
+ if (skillHitResult.Result == HitResultType.Dodge && attacker.Components.TryGet(out var component))
+ {
+ component.Script.QueueEventAlert(new HateIncreaseAlert(target, buff.NumArg1 * HatePerLevel));
+
+ // Outlaw12 adds additional duration to the buff on successful evade
+ // Note that it only applies to monster attacks, which is why
+ // it's done after the AI script check
+ if (target.TryGetActiveAbilityLevel(AbilityId.Outlaw12, out var level))
+ {
+ buff.IncreaseDuration(TimeSpan.FromSeconds(level));
+ }
+ }
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/FireBlindly_Buff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/FireBlindly_Buff.cs
new file mode 100644
index 000000000..a65d9258c
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/FireBlindly_Buff.cs
@@ -0,0 +1,29 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Skills;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.World.Actors;
+
+namespace Melia.Zone.Buffs.Handlers.Scouts.OutLaw
+{
+ ///
+ /// Handler for the Fire Blindly Buff, which grants forced evade
+ ///
+ [BuffHandler(BuffId.FireBlindly_Buff)]
+ public class FireBlindly_Buff : BuffHandler, IBuffCombatDefenseBeforeCalcHandler
+ {
+ ///
+ /// Applies the buff's effect during the combat calculations.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void OnDefenseBeforeCalc(Buff buff, ICombatEntity attacker, ICombatEntity target, Skill skill, SkillModifier modifier, SkillHitResult skillHitResult)
+ {
+ modifier.ForcedEvade = true;
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/MangleAndFireBlindly_Debuff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/MangleAndFireBlindly_Debuff.cs
new file mode 100644
index 000000000..c059b2f8a
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/MangleAndFireBlindly_Debuff.cs
@@ -0,0 +1,37 @@
+using Melia.Shared.Data.Database;
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Skills;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.World.Actors;
+
+namespace Melia.Zone.Buffs.Handlers.Scouts.OutLaw
+{
+ ///
+ /// Handle for the Mangle and Fire Blindly Debuff, which
+ /// reduces damage dealt to the inflictor of the buff
+ ///
+ ///
+ /// NumArg1: Skill Level
+ /// NumArg2: Extra Crit Chance
+ ///
+ [BuffHandler(BuffId.MangleAndFireBlindly_Debuff)]
+ public class MangleAndFireBlindly_Debuff : BuffHandler, IBuffCombatAttackBeforeCalcHandler
+ {
+ ///
+ /// Reduces damage dealt if the target is the caster of the buff
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void OnAttackBeforeCalc(Buff buff, ICombatEntity attacker, ICombatEntity target, Skill skill, SkillModifier modifier, SkillHitResult skillHitResult)
+ {
+ if (target == buff.Caster)
+ {
+ modifier.FinalDamageMultiplier -= 0.07f * buff.OverbuffCounter;
+ }
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/Mangle_Buff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/Mangle_Buff.cs
new file mode 100644
index 000000000..39399b03d
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/Mangle_Buff.cs
@@ -0,0 +1,29 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Skills;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.World.Actors;
+
+namespace Melia.Zone.Buffs.Handlers.Scouts.OutLaw
+{
+ ///
+ /// Handler for the Mangle Buff, which grants forced evade
+ ///
+ [BuffHandler(BuffId.Mangle_Buff)]
+ public class Mangle_Buff : BuffHandler, IBuffCombatDefenseBeforeCalcHandler
+ {
+ ///
+ /// Applies the buff's effect during the combat calculations.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void OnDefenseBeforeCalc(Buff buff, ICombatEntity attacker, ICombatEntity target, Skill skill, SkillModifier modifier, SkillHitResult skillHitResult)
+ {
+ modifier.ForcedEvade = true;
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/Rampage_After_Buff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/Rampage_After_Buff.cs
new file mode 100644
index 000000000..2ca0bc548
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/Rampage_After_Buff.cs
@@ -0,0 +1,51 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Skills;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.World.Actors;
+
+namespace Melia.Zone.Buffs.Handlers.Scouts.OutLaw
+{
+ ///
+ /// Handle for the Rampage After Buff, which increases damage
+ /// dealt and taken by 50% and prevents the caster from evading
+ ///
+ ///
+ /// NumArg1: Level
+ /// NumArg2: None
+ ///
+ [BuffHandler(BuffId.Rampage_After_Buff)]
+ public class Rampage_After_Buff : BuffHandler, IBuffCombatAttackBeforeCalcHandler, IBuffCombatDefenseBeforeCalcHandler
+ {
+ private const float DamageBonus = 0.5f;
+ private const float DamagePenalty = 0.5f;
+
+ ///
+ /// Adds the damage bonus
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void OnAttackBeforeCalc(Buff buff, ICombatEntity attacker, ICombatEntity target, Skill skill, SkillModifier modifier, SkillHitResult skillHitResult)
+ {
+ modifier.FinalDamageMultiplier += DamageBonus;
+ }
+
+ ///
+ /// Adds the damage penalty and prevents evasion
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void OnDefenseBeforeCalc(Buff buff, ICombatEntity attacker, ICombatEntity target, Skill skill, SkillModifier modifier, SkillHitResult skillHitResult)
+ {
+ modifier.FinalDamageMultiplier += DamagePenalty;
+ modifier.ForcedHit = true;
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/Rampage_Outlaw18_Buff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/Rampage_Outlaw18_Buff.cs
new file mode 100644
index 000000000..65fec8563
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/Rampage_Outlaw18_Buff.cs
@@ -0,0 +1,36 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Skills;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.World.Actors;
+
+namespace Melia.Zone.Buffs.Handlers.Scouts.OutLaw
+{
+ ///
+ /// Handle for the Rampage Outlaw18 Buff, which gives 10%
+ /// bonus crit chance per debuff that was active when it
+ /// was started (which was calculated when it was applied)
+ ///
+ ///
+ /// NumArg1: Debuff Count
+ /// NumArg2: None
+ ///
+ [BuffHandler(BuffId.Rampage_Outlaw18_Buff)]
+ public class Rampage_Outlaw18_Buff : BuffHandler, IBuffCombatAttackBeforeCalcHandler
+ {
+ public const float CritBonusPerDebuff = 10f;
+
+ ///
+ /// Adds the bonus crit chance
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void OnAttackBeforeCalc(Buff buff, ICombatEntity attacker, ICombatEntity target, Skill skill, SkillModifier modifier, SkillHitResult skillHitResult)
+ {
+ modifier.BonusCritChance = CritBonusPerDebuff * buff.NumArg1;
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/SprinkleSands_Debuff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/SprinkleSands_Debuff.cs
new file mode 100644
index 000000000..0c2a32b79
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/OutLaw/SprinkleSands_Debuff.cs
@@ -0,0 +1,42 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+
+namespace Melia.Zone.Buffs.Handlers.Scouts.OutLaw
+{
+ ///
+ /// Handler for Sprinkle Sands Debuff, which reduces Accuracy and Evasion
+ ///
+ ///
+ /// NumArg1: Skill Level
+ /// NumArg2: None
+ ///
+ [BuffHandler(BuffId.SprinkleSands_Debuff)]
+ public class SprinkleSands_Debuff : BuffHandler
+ {
+ private const float HRPenaltyRate = 0.2f;
+ private const float DRPenaltyRate = 0.2f;
+
+ ///
+ /// Starts buff, reducing HR and DR
+ ///
+ ///
+ public override void OnActivate(Buff buff, ActivationType activationType)
+ {
+ var reduceHR = buff.Target.Properties.GetFloat(PropertyName.HR) * HRPenaltyRate;
+ var reduceDR = buff.Target.Properties.GetFloat(PropertyName.DR) * DRPenaltyRate;
+
+ AddPropertyModifier(buff, buff.Target, PropertyName.HR_BM, -reduceHR);
+ AddPropertyModifier(buff, buff.Target, PropertyName.DR_BM, -reduceDR);
+ }
+
+ ///
+ /// Ends the buff, resetting HR and DR.
+ ///
+ ///
+ public override void OnEnd(Buff buff)
+ {
+ RemovePropertyModifier(buff, buff.Target, PropertyName.HR_BM);
+ RemovePropertyModifier(buff, buff.Target, PropertyName.DR_BM);
+ }
+ }
+}
diff --git a/src/ZoneServer/Scripting/AI/AiEvent.cs b/src/ZoneServer/Scripting/AI/AiEvent.cs
index ce8fe672c..935fd2f7e 100644
--- a/src/ZoneServer/Scripting/AI/AiEvent.cs
+++ b/src/ZoneServer/Scripting/AI/AiEvent.cs
@@ -59,4 +59,27 @@ public HateResetAlert(ICombatEntity target)
this.Target = target;
}
}
+
+ public class HateIncreaseAlert : IAiEventAlert
+ {
+ ///
+ /// Returns the entity for which the hate should increase
+ ///
+ public ICombatEntity Target { get; }
+
+ ///
+ /// Returns the amount of hate to gain.
+ ///
+ public float Amount { get; }
+
+ ///
+ /// Creates new event.
+ ///
+ ///
+ public HateIncreaseAlert(ICombatEntity target, float amount)
+ {
+ this.Target = target;
+ this.Amount = amount;
+ }
+ }
}
diff --git a/src/ZoneServer/Scripting/AI/AiScript.cs b/src/ZoneServer/Scripting/AI/AiScript.cs
index 30bf184e4..9df141991 100644
--- a/src/ZoneServer/Scripting/AI/AiScript.cs
+++ b/src/ZoneServer/Scripting/AI/AiScript.cs
@@ -488,6 +488,12 @@ private void ReactToAlert(IAiEventAlert eventAlert)
_hateLevels.Remove(targetHandle);
break;
}
+
+ case HateIncreaseAlert hateIncreaseAlert:
+ {
+ this.IncreaseHate(hateIncreaseAlert.Target, hateIncreaseAlert.Amount);
+ break;
+ }
}
}
diff --git a/src/ZoneServer/Skills/Combat/SkillHitInfo.cs b/src/ZoneServer/Skills/Combat/SkillHitInfo.cs
index b0c8d8487..50f28dbe0 100644
--- a/src/ZoneServer/Skills/Combat/SkillHitInfo.cs
+++ b/src/ZoneServer/Skills/Combat/SkillHitInfo.cs
@@ -99,6 +99,13 @@ public void ApplyKnockBack(ICombatEntity target)
if (this.KnockBackInfo == null)
throw new InvalidOperationException("Knock back info is not set.");
+ // Knockback immunity check - may need to move this
+ if (target.IsBuffActive(BuffId.BullyPainBarrier_Buff))
+ {
+ this.KnockBackInfo = null;
+ return;
+ }
+
var isKnockBack = this.KnockBackInfo.HitType == HitType.KnockBack;
var isKnockDown = this.KnockBackInfo.HitType == HitType.KnockDown;
diff --git a/src/ZoneServer/Skills/Combat/SkillModifier.cs b/src/ZoneServer/Skills/Combat/SkillModifier.cs
index 127e61a0e..18f6ae6b7 100644
--- a/src/ZoneServer/Skills/Combat/SkillModifier.cs
+++ b/src/ZoneServer/Skills/Combat/SkillModifier.cs
@@ -106,7 +106,16 @@ public class SkillModifier
///
/// Gets or sets whether the attack can be blocked. Beats out ForcedBlock.
///
- public bool Unblockable { get; set; }
+ public bool Unblockable { get; set; }
+
+ ///
+ /// Gets or sets forced block status.
+ ///
+ ///
+ /// If this is true, the attack is always blocked
+ /// unless it is unblockable.
+ ///
+ public bool ForcedBlock { get; set; }
///
/// Gets or sets forced hit status.
@@ -117,12 +126,13 @@ public class SkillModifier
public bool ForcedHit { get; set; }
///
- /// Gets or sets forced block status.
+ /// Gets or sets forced evade status.
///
///
- /// If this is true, the attack is always blocked.
+ /// If this is true, the attack is always evaded
+ /// unless it is unavoidable
///
- public bool ForcedBlock { get; set; }
+ public bool ForcedEvade { get; set; }
///
/// Gets or sets forced critical status.
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/OutLaw_Aggress.cs b/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/OutLaw_Aggress.cs
new file mode 100644
index 000000000..955d75c69
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/OutLaw_Aggress.cs
@@ -0,0 +1,82 @@
+using System;
+using Melia.Shared.L10N;
+using Melia.Shared.Game.Const;
+using Melia.Shared.World;
+using Melia.Zone.Network;
+using Melia.Zone.Skills.Handlers.Base;
+using Melia.Zone.World.Actors;
+using Melia.Zone.World.Actors.CombatEntities.Components;
+using Melia.Shared.Data.Database;
+using Melia.Zone.Scripting.AI;
+using Melia.Zone.Skills.SplashAreas;
+
+namespace Melia.Zone.Skills.Handlers.Scouts.OutLaw
+{
+ ///
+ /// Handler for the Outlaw skill Aggress.
+ ///
+ [SkillHandler(SkillId.OutLaw_Aggress)]
+ public class OutLaw_Aggress : IGroundSkillHandler
+ {
+ ///
+ /// Handles the skill, debuffing enemies in the target area.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity designatedTarget)
+ {
+ if (!caster.TrySpendSp(skill))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+
+ skill.IncreaseOverheat();
+ caster.SetAttackState(true);
+
+ Send.ZC_SKILL_MELEE_GROUND(caster, skill, caster.Position, null);
+
+ var duration = TimeSpan.FromSeconds(10);
+
+ // Splash area is a square of length 300, width 100, starting 100
+ // units behind the caster
+ var splashArea = new Square(caster.Position.GetRelative(caster.Direction, -100f), caster.Direction, 200f, 100f);
+ Debug.ShowShape(caster.Map, splashArea);
+
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+ var maxTargets = Math.Min(targets.Count, 3 * skill.Level);
+
+ var unhingeLevel = 0f;
+
+ if (caster.TryGetActiveAbilityLevel(AbilityId.Outlaw13, out var level))
+ unhingeLevel = level;
+
+ for (var i = 0; i < maxTargets; i++)
+ {
+ var target = targets[i];
+ if (target.IsBuffActive(BuffId.ProvocationImmunity_Debuff))
+ continue;
+
+ target.StartBuff(BuffId.Aggress_Debuff, skill.Level, unhingeLevel, duration, caster);
+ target.StartBuff(BuffId.ProvocationImmunity_Debuff, skill.Level, 0, duration, caster);
+
+ if (target.Components.TryGet(out var component))
+ {
+ // Reset hate and simulate a hit to build a small amount
+ // of threat
+ component.Script.QueueEventAlert(new HateResetAlert(caster));
+ component.Script.QueueEventAlert(new HitEventAlert(target, caster, 0));
+ }
+ }
+
+ // Outlaw14 adds a massive speed buff
+ if (caster.TryGetActiveAbilityLevel(AbilityId.Outlaw14, out var getawayLevel))
+ {
+ caster.StartBuff(BuffId.Aggress_Buff, skill.Level, 0, TimeSpan.FromSeconds(getawayLevel), caster);
+ }
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/OutLaw_BreakBrick.cs b/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/OutLaw_BreakBrick.cs
new file mode 100644
index 000000000..f8f19f2bf
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/OutLaw_BreakBrick.cs
@@ -0,0 +1,233 @@
+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 Yggdrasil.Util;
+using static Melia.Shared.Util.TaskHelper;
+using static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Scouts.OutLaw
+{
+ ///
+ /// Handler for the Assassin skill Brick Smash
+ ///
+ [SkillHandler(SkillId.OutLaw_BreakBrick)]
+ public class OutLaw_BreakBrick : IGroundSkillHandler
+ {
+ public const float JumpDistance = 60f;
+
+ ///
+ /// Handles skill
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity designatedTarget)
+ {
+ if (!caster.TrySpendSp(skill))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+
+ skill.IncreaseOverheat();
+ caster.SetAttackState(true);
+
+ var splashParam = skill.GetSplashParameters(caster, originPos, farPos, length: 90, 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);
+
+ // Outlaw26 is a totally different attack
+ if (caster.IsAbilityActive(AbilityId.Outlaw26))
+ {
+ // This should set the overheat max to 1
+
+ CallSafe(this.ShatterAttack(skill, caster, farPos));
+ }
+ else
+ {
+ CallSafe(this.Attack(skill, caster, splashArea, farPos));
+ }
+ }
+
+ ///
+ /// Executes the actual attack after a delay.
+ ///
+ ///
+ ///
+ ///
+ private async Task Attack(Skill skill, ICombatEntity caster, ISplashArea splashArea, Position farPos)
+ {
+ var damageDelay = TimeSpan.FromMilliseconds(50);
+ var skillHitDelay = TimeSpan.Zero;
+ var hitDelay = TimeSpan.FromMilliseconds(400);
+
+ // First perform the jump
+ var targetPos = caster.Position.GetRelative(caster.Direction, JumpDistance);
+ targetPos = caster.Map.Ground.GetLastValidPosition(caster.Position, targetPos);
+
+ caster.Position = targetPos;
+ Send.ZC_NORMAL.LeapJump(caster, targetPos, 0.15f, 0.1f, 1f, 0.2f, 1f, 3);
+
+ // Outlaw4 has a 20% chance to activate. If it does, it guarantees
+ // the stun and makes the attack a forced critical
+ var outlaw4Activates = caster.IsAbilityActive(AbilityId.Outlaw4) && RandomProvider.Get().Next(5) == 1;
+
+ await Task.Delay(hitDelay);
+
+ var hits = new List();
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var modifier = SkillModifier.MultiHit(6);
+
+ // 30% chance to Stun as base, 100% when behind target
+ var stunChance = 3;
+
+ if (caster.IsBehind(target))
+ stunChance = 10;
+
+ if (outlaw4Activates)
+ {
+ stunChance = 10;
+ modifier.ForcedCritical = true;
+ }
+
+ 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);
+
+ if (RandomProvider.Get().Next(10) <= stunChance)
+ target.StartBuff(BuffId.Stun, skill.Level, 0, TimeSpan.FromSeconds(3), caster);
+
+ target.StartBuff(BuffId.BreakBrick_Debuff, skill.Level, 0, TimeSpan.FromSeconds(20), caster);
+ }
+
+ Send.ZC_SKILL_HIT_INFO(caster, hits);
+ }
+
+ ///
+ /// Executes the alternate attack for Outlaw26
+ /// It doesn't inflict status but has a bigger range
+ /// and does 3 times as many hits
+ ///
+ ///
+ ///
+ ///
+ private async Task ShatterAttack(Skill skill, ICombatEntity caster, Position farPos)
+ {
+ var damageDelay = TimeSpan.FromMilliseconds(50);
+ var skillHitDelay = TimeSpan.Zero;
+ var hitDelay = TimeSpan.FromMilliseconds(250);
+
+ // This version has no jump
+
+ // Outlaw4 has a 20% chance to activate. If it does, it inflicts
+ // stun and forces a critical
+ var outlaw4Activates = caster.IsAbilityActive(AbilityId.Outlaw4) && RandomProvider.Get().Next(5) == 1;
+
+ await Task.Delay(hitDelay);
+
+ // This attack uses 3 hitboxes of different shapes, which all hit simultaneously
+
+ var splashParam = skill.GetSplashParameters(caster, caster.Position, farPos, length: 135, width: 30, angle: 0);
+ var splashArea = skill.GetSplashArea(SplashType.Square, splashParam);
+
+ var hits = new List();
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var modifier = SkillModifier.MultiHit(6);
+
+ // Outlaw4 has a 20% chance to activate. If it does, it causes
+ // a stun and makes the attack a forced critical
+ if (outlaw4Activates)
+ {
+ target.StartBuff(BuffId.Stun, skill.Level, 0, TimeSpan.FromSeconds(3), caster);
+ modifier.ForcedCritical = true;
+ }
+
+ 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);
+ hits.Clear();
+
+ // The second hitbox is identical to the first
+
+ targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var modifier = SkillModifier.MultiHit(6);
+
+ if (outlaw4Activates)
+ {
+ modifier.ForcedCritical = true;
+ }
+
+ 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);
+ hits.Clear();
+
+ // The third hitbox is a fan
+
+ splashParam = skill.GetSplashParameters(caster, caster.Position, farPos, length: 150, width: 100, angle: 78);
+ splashArea = skill.GetSplashArea(SplashType.Fan, splashParam);
+
+ targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var modifier = SkillModifier.MultiHit(6);
+
+ if (outlaw4Activates)
+ {
+ modifier.ForcedCritical = true;
+ }
+
+ 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/Scouts/OutLaw/OutLaw_Bully.cs b/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/OutLaw_Bully.cs
new file mode 100644
index 000000000..6cfb455c7
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/OutLaw_Bully.cs
@@ -0,0 +1,49 @@
+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.Scouts.OutLaw
+{
+ ///
+ /// Handler for the Outlaw skill Bully.
+ ///
+ [SkillHandler(SkillId.OutLaw_Bully)]
+ public class OutLaw_Bully : ISelfSkillHandler
+ {
+ ///
+ /// Handles skill, applying a buff to the caster.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, Position originPos, Direction dir)
+ {
+ if (!caster.TrySpendSp(skill))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+
+ skill.IncreaseOverheat();
+ caster.SetAttackState(true);
+
+ // Uses a totally different buff if Outlaw19 is active
+ if (caster.IsAbilityActive(AbilityId.Outlaw19))
+ {
+ caster.StartBuff(BuffId.BullyPainBarrier_Buff, skill.Level, 0, TimeSpan.FromSeconds(20), caster);
+ }
+ else
+ {
+ caster.StartBuff(BuffId.Bully_Buff, skill.Level, 0, TimeSpan.FromSeconds(60), caster);
+ }
+
+ Send.ZC_SKILL_MELEE_TARGET(caster, skill, caster, null);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/OutLaw_FireBlindly.cs b/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/OutLaw_FireBlindly.cs
new file mode 100644
index 000000000..fba7199c9
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/OutLaw_FireBlindly.cs
@@ -0,0 +1,135 @@
+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.Scouts.OutLaw
+{
+ ///
+ /// Handler for the Outlaw skill Blindfire
+ ///
+ [SkillHandler(SkillId.OutLaw_FireBlindly)]
+ public class OutLaw_FireBlindly : 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.SetAttackState(true);
+
+ var splashParam = skill.GetSplashParameters(caster, originPos, farPos, length: 120, width: 40, angle: 80);
+ var splashArea = skill.GetSplashArea(SplashType.Fan, splashParam);
+
+ Send.ZC_SKILL_READY(caster, skill, originPos, farPos);
+ Send.ZC_SKILL_MELEE_GROUND(caster, skill, farPos, null);
+
+ CallSafe(this.Attack(skill, caster, splashArea));
+ }
+
+ ///
+ /// Executes the actual attack after a delay.
+ ///
+ ///
+ ///
+ ///
+ private async Task Attack(Skill skill, ICombatEntity caster, ISplashArea splashArea)
+ {
+ var hitDelay = TimeSpan.FromMilliseconds(360);
+ var damageDelay = TimeSpan.FromMilliseconds(50);
+ var skillHitDelay = TimeSpan.Zero;
+
+ // Outlaw9 gives guaranteed evade throughout the whole animation
+ if (caster.IsAbilityActive(AbilityId.Outlaw9))
+ caster.StartBuff(BuffId.FireBlindly_Buff, skill.Level, 0, TimeSpan.FromMilliseconds(600), caster);
+
+ // First attack hits with no delay
+
+ var hits = new List();
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var modifier = SkillModifier.MultiHit(4);
+ modifier.BonusCritChance += GetCritBonus(target);
+
+ var skillHitResult = SCR_SkillHit(caster, target, skill, modifier);
+
+ // Outlaw10 adds 30% damage on a critical
+ if (caster.IsAbilityActive(AbilityId.Outlaw10) && skillHitResult.Result == HitResultType.Crit)
+ skillHitResult.Damage *= 1.3f;
+
+ 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);
+ hits.Clear();
+ await Task.Delay(hitDelay);
+
+ targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var modifier = SkillModifier.MultiHit(4);
+ modifier.BonusCritChance += GetCritBonus(target);
+
+ var skillHitResult = SCR_SkillHit(caster, target, skill, modifier);
+
+ // Outlaw10 adds 30% damage on a critical
+ if (caster.IsAbilityActive(AbilityId.Outlaw10) && skillHitResult.Result == HitResultType.Crit)
+ skillHitResult.Damage *= 1.3f;
+
+ target.TakeDamage(skillHitResult.Damage, caster);
+
+ var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay);
+ skillHit.HitEffect = HitEffect.Impact;
+ hits.Add(skillHit);
+
+ // Description doesn't mention this, but it's in skill_bytool
+ target.StartBuff(BuffId.MangleAndFireBlindly_Debuff, skill.Level, 0, TimeSpan.FromSeconds(10), caster);
+ }
+
+ Send.ZC_SKILL_HIT_INFO(caster, hits);
+ }
+
+
+ ///
+ /// Return the bonus damage
+ /// Adds 20 to crit chance if target has any of Blind, Bleed, or Stun
+ ///
+ ///
+ ///
+ private float GetCritBonus(ICombatEntity target)
+ {
+ if (target.IsBuffActive(BuffId.SprinkleSands_Debuff) || target.IsBuffActive(BuffId.HeavyBleeding) || target.IsBuffActive(BuffId.Behead_Debuff) || target.IsBuffActive(BuffId.Stun))
+ return 20f;
+
+ return 0f;
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/OutLaw_Rampage.cs b/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/OutLaw_Rampage.cs
new file mode 100644
index 000000000..6a3b075f7
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/OutLaw_Rampage.cs
@@ -0,0 +1,179 @@
+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.CombatEntities.Components;
+using Yggdrasil.Logging;
+using Yggdrasil.Util;
+using static Melia.Shared.Util.TaskHelper;
+using static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Scouts.OutLaw
+{
+ ///
+ /// Handler for the Outlaw skill Rampage.
+ ///
+ [SkillHandler(SkillId.OutLaw_Rampage)]
+ public class OutLaw_Rampage : IGroundSkillHandler
+ {
+ private const float BuffRemoveChancePerLevel = 3f;
+
+ ///
+ /// 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.SetAttackState(true);
+
+ var splashParam = skill.GetSplashParameters(caster, originPos, farPos, length: 0, width: 150, 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(this.Attack(skill, caster, splashArea));
+ }
+
+ ///
+ /// Executes the actual attack after a delay.
+ ///
+ ///
+ ///
+ ///
+ private async Task Attack(Skill skill, ICombatEntity caster, ISplashArea splashArea)
+ {
+ var damageDelay = TimeSpan.FromMilliseconds(50);
+ var skillHitDelay = TimeSpan.Zero;
+
+ var delayBetweenHits = new[]
+ {
+ TimeSpan.FromMilliseconds(50),
+ TimeSpan.FromMilliseconds(100),
+ TimeSpan.FromMilliseconds(500),
+ TimeSpan.FromMilliseconds(50),
+ TimeSpan.FromMilliseconds(700),
+ TimeSpan.FromMilliseconds(100),
+ TimeSpan.FromMilliseconds(50),
+ TimeSpan.FromMilliseconds(50)
+ };
+
+ // first hit hits instantly
+
+ var hits = new List();
+
+ var stunChance = 0f;
+ if (caster.TryGetActiveAbilityLevel(AbilityId.Outlaw16, out int level))
+ stunChance = level;
+
+ var iceVariant = caster.IsAbilityActive(AbilityId.Outlaw20);
+
+ // Caster avoids all attacks during the skill's animation
+ caster.StartBuff(BuffId.Skill_MomentaryEvasion_Buff, skill.Level, 0, TimeSpan.FromMilliseconds(1700), caster);
+
+ // Outlaw17 gives a buff that prevents removable debuffs
+ if (caster.IsAbilityActive(AbilityId.Outlaw17))
+ caster.StartBuff(BuffId.Rampage_Buff, skill.Level, 0, TimeSpan.FromMilliseconds(1700), caster);
+
+ // Outlaw18 gives a buff that adds 10% bonus crit chance per debuff currently active
+ if (caster.IsAbilityActive(AbilityId.Outlaw18))
+ {
+ int debuffCount = 0;
+ foreach (var buff in caster.Components.Get().GetList())
+ {
+ if (buff.Data.Type == BuffType.Debuff)
+ {
+ debuffCount++;
+ }
+ }
+
+ if (debuffCount > 0)
+ caster.StartBuff(BuffId.Rampage_Outlaw18_Buff, debuffCount, 0, TimeSpan.FromMilliseconds(1700), caster);
+ }
+
+ for (var i = 0; i < 9; i++)
+ {
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var modifier = SkillModifier.Default;
+
+ // Targets with certain statuses take 2 hits at 30% less damage
+ if (GetDoubleHit(target))
+ {
+ modifier.HitCount = 2;
+ modifier.FinalDamageMultiplier -= 0.3f;
+ }
+
+ if (iceVariant)
+ modifier.AttackAttribute = AttributeType.Ice;
+
+ 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 buffRemoveChance = BuffRemoveChancePerLevel * skill.Level;
+ if (RandomProvider.Get().Next(1000) < buffRemoveChance)
+ {
+ target.RemoveRandomBuff();
+ }
+
+ if (RandomProvider.Get().Next(100) < stunChance)
+ {
+ target.StartBuff(BuffId.Stun, skill.Level, 0, TimeSpan.FromSeconds(2), caster);
+ }
+
+ // Ice effect is only applied on last hit
+ if (i == 8 && iceVariant)
+ {
+ // TODO: this ability states it can knock down instead
+ target.StartBuff(BuffId.Freeze, skill.Level, 0, TimeSpan.FromSeconds(2), caster);
+ }
+ }
+
+ Send.ZC_SKILL_HIT_INFO(caster, hits);
+
+ hits.Clear();
+
+ if (i < 8)
+ await Task.Delay(delayBetweenHits[i]);
+ }
+
+ caster.StartBuff(BuffId.Rampage_After_Buff, skill.Level, 0, TimeSpan.FromSeconds(5), caster);
+ }
+
+
+ ///
+ /// Checks if a target takes 2 hits
+ /// This occurs if they have any of Blind, Bleed, or Stun
+ ///
+ ///
+ ///
+ private bool GetDoubleHit(ICombatEntity target)
+ {
+ return target.IsBuffActive(BuffId.SprinkleSands_Debuff) || target.IsBuffActive(BuffId.HeavyBleeding) || target.IsBuffActive(BuffId.Behead_Debuff) || target.IsBuffActive(BuffId.Stun);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/OutLaw_SprinkleSands.cs b/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/OutLaw_SprinkleSands.cs
new file mode 100644
index 000000000..b46bf2c23
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/OutLaw_SprinkleSands.cs
@@ -0,0 +1,109 @@
+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 Yggdrasil.Util;
+using static Melia.Shared.Util.TaskHelper;
+using static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Scouts.OutLaw
+{
+ ///
+ /// Handler for the Assassin skill Throw Sand
+ ///
+ [SkillHandler(SkillId.OutLaw_SprinkleSands)]
+ public class OutLaw_SprinkleSands : IGroundSkillHandler
+ {
+ ///
+ /// Handles skill
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity designatedTarget)
+ {
+ if (!caster.TrySpendSp(skill))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+
+ skill.IncreaseOverheat();
+ caster.SetAttackState(true);
+
+ var splashParam = skill.GetSplashParameters(caster, originPos, farPos, length: 100, width: 40, 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(this.Attack(skill, caster, splashArea, farPos));
+ }
+
+ ///
+ /// Executes the actual attack after a delay.
+ ///
+ ///
+ ///
+ ///
+ private async Task Attack(Skill skill, ICombatEntity caster, ISplashArea splashArea, Position farPos)
+ {
+ var damageDelay = TimeSpan.FromMilliseconds(50);
+ var skillHitDelay = TimeSpan.Zero;
+ var hitDelay = TimeSpan.FromMilliseconds(250);
+
+ await Task.Delay(hitDelay);
+
+ var hits = new List();
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+
+ var hitTargets = targets.LimitBySDR(caster, skill);
+
+ // Outlaw2 makes the max target count 17
+ if (caster.IsAbilityActive(AbilityId.Outlaw2))
+ {
+ hitTargets = targets.LimitRandom(17);
+ }
+
+ foreach (var target in hitTargets)
+ {
+ var modifier = SkillModifier.MultiHit(4);
+
+ 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);
+
+ target.StartBuff(BuffId.Common_Silence, skill.Level, 0, TimeSpan.FromMilliseconds(200 * skill.Level), caster);
+ target.StartBuff(BuffId.SprinkleSands_Debuff, skill.Level, 0, TimeSpan.FromSeconds(10), caster);
+
+ target.StartBuff(BuffId.DecreaseHeal_Debuff, skill.Level, this.GetHealingReduction(skill), TimeSpan.FromSeconds(5), caster);
+ }
+
+ Send.ZC_SKILL_HIT_INFO(caster, hits);
+ }
+
+ ///
+ /// Return the Healing Reduction value
+ ///
+ ///
+ ///
+ private float GetHealingReduction(Skill skill)
+ {
+ return (2.3f * skill.Level) * 1000;
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/Outlaw_Mangle.cs b/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/Outlaw_Mangle.cs
new file mode 100644
index 000000000..c967a4e97
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Scouts/OutLaw/Outlaw_Mangle.cs
@@ -0,0 +1,177 @@
+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.Scouts.OutLaw
+{
+ ///
+ /// Handler for the Outlaw skill Mangle.
+ ///
+ [SkillHandler(SkillId.OutLaw_Mangle)]
+ public class OutLaw_Mangle : 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.SetAttackState(true);
+
+ var splashParam = skill.GetSplashParameters(caster, originPos, farPos, length: 40, width: 20, 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(this.Attack(skill, caster, splashArea));
+ }
+
+ ///
+ /// Executes the actual attack after a delay.
+ ///
+ ///
+ ///
+ ///
+ private async Task Attack(Skill skill, ICombatEntity caster, ISplashArea splashArea)
+ {
+ var hitDelay1 = TimeSpan.FromMilliseconds(350);
+ var hitDelay2 = TimeSpan.FromMilliseconds(600);
+ var hitDelay3 = TimeSpan.FromMilliseconds(50);
+ var damageDelay = TimeSpan.FromMilliseconds(50);
+ var skillHitDelay = TimeSpan.Zero;
+
+ // Outlaw6 gives guaranteed evade throughout the whole animation
+ if (caster.IsAbilityActive(AbilityId.Outlaw6))
+ caster.StartBuff(BuffId.Mangle_Buff, skill.Level, 0, TimeSpan.FromMilliseconds(1300), caster);
+
+ // First attack hits with no delay
+
+ var hits = new List();
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var modifier = SkillModifier.MultiHit(3);
+ modifier.FinalDamageMultiplier += GetDamageBonus(target);
+
+ 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);
+ hits.Clear();
+ await Task.Delay(hitDelay1);
+
+ targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var modifier = SkillModifier.MultiHit(2);
+
+ // Outlaw7 adds 3 more hits
+ if (caster.IsAbilityActive(AbilityId.Outlaw7))
+ modifier.HitCount += 3;
+
+ modifier.FinalDamageMultiplier += GetDamageBonus(target);
+
+ 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);
+ hits.Clear();
+ await Task.Delay(hitDelay2);
+
+ targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var modifier = SkillModifier.Default;
+ modifier.FinalDamageMultiplier += GetDamageBonus(target);
+
+ 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);
+ hits.Clear();
+ await Task.Delay(hitDelay3);
+
+ targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var modifier = SkillModifier.MultiHit(3);
+ modifier.FinalDamageMultiplier += GetDamageBonus(target);
+
+ 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);
+
+ target.StartBuff(BuffId.MangleAndFireBlindly_Debuff, skill.Level, 0, TimeSpan.FromSeconds(10), caster);
+ }
+
+ Send.ZC_SKILL_HIT_INFO(caster, hits);
+ }
+
+
+ ///
+ /// Return the bonus damage
+ /// Adds 0.5 to final damage for each of Blind, Bleed, or Stun
+ ///
+ ///
+ ///
+ private float GetDamageBonus(ICombatEntity target)
+ {
+ float damageBonus = 0f;
+
+ if (target.IsBuffActive(BuffId.SprinkleSands_Debuff))
+ damageBonus += 0.5f;
+
+ if (target.IsBuffActive(BuffId.HeavyBleeding) || target.IsBuffActive(BuffId.Behead_Debuff))
+ damageBonus += 0.5f;
+
+ if (target.IsBuffActive(BuffId.Stun))
+ damageBonus += 0.5f;
+
+ return damageBonus;
+ }
+ }
+}
diff --git a/src/ZoneServer/World/Actors/CombatEntities/Components/BuffComponent.cs b/src/ZoneServer/World/Actors/CombatEntities/Components/BuffComponent.cs
index 12d995c29..0b3233e8f 100644
--- a/src/ZoneServer/World/Actors/CombatEntities/Components/BuffComponent.cs
+++ b/src/ZoneServer/World/Actors/CombatEntities/Components/BuffComponent.cs
@@ -369,6 +369,9 @@ private bool TryResistDebuff(BuffId buffId, ICombatEntity caster)
if (this.Has(BuffId.Skill_MomentaryImmune_Buff))
return true;
+ if (this.Has(BuffId.Rampage_Buff) && buffData.Removable)
+ return true;
+
if (this.TryGet(BuffId.Cyclone_Buff_ImmuneAbil, out var cycloneImmuneBuff))
{
if (RandomProvider.Get().Next(100) < cycloneImmuneBuff.NumArg1 * 15)
diff --git a/system/scripts/zone/core/calc_combat.cs b/system/scripts/zone/core/calc_combat.cs
index 412ebbcd0..203e2005f 100644
--- a/system/scripts/zone/core/calc_combat.cs
+++ b/system/scripts/zone/core/calc_combat.cs
@@ -557,6 +557,9 @@ public float SCR_GetDodgeChance(ICombatEntity attacker, ICombatEntity target, Sk
if (modifier.ForcedHit)
return 0;
+ if (modifier.ForcedEvade)
+ return 100;
+
var dr = target.Properties.GetFloat(PropertyName.DR);
var hr = attacker.Properties.GetFloat(PropertyName.HR);