diff --git a/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/Archer_Kneelingshot.cs b/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/Archer_Kneelingshot.cs
new file mode 100644
index 000000000..bdbb99c76
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/Archer_Kneelingshot.cs
@@ -0,0 +1,72 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Network;
+using Melia.Zone.Skills;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.World.Actors;
+using Melia.Zone.World.Actors.Characters;
+using Melia.Zone.World.Actors.Components;
+
+namespace Melia.Zone.Buffs.Handlers.Archers.QuarrelShooter
+{
+ ///
+ /// Buff handler for Archer Kneelingshot Buff, which grants
+ /// extra damage, range, attack speed, and possibly
+ /// crit rate, but prevents movement.
+ ///
+ ///
+ /// NumArg1: Skill Level
+ /// NumArg2: Crit bonus
+ ///
+ [BuffHandler(BuffId.Archer_Kneelingshot)]
+ public class Archer_Kneelingshot : BuffHandler, IBuffCombatAttackBeforeCalcHandler
+ {
+ private const float PAtkBuffRate = 0.15f;
+ private const float RangeBonus = 35f;
+ private const float AspdBonus = 250f;
+
+ ///
+ /// Starts buff, increasing stats and disallowing movement.
+ ///
+ ///
+ public override void OnActivate(Buff buff, ActivationType activationType)
+ {
+ var target = buff.Target;
+ buff.Target.AddState(StateType.Held);
+
+ var bonusPatk = target.Properties.GetFloat(PropertyName.PATK) * PAtkBuffRate;
+
+ AddPropertyModifier(buff, buff.Target, PropertyName.PATK_BM, bonusPatk);
+ AddPropertyModifier(buff, buff.Target, PropertyName.SkillRange_BM, RangeBonus);
+ AddPropertyModifier(buff, buff.Target, PropertyName.ASPD_BM, AspdBonus);
+ }
+
+ ///
+ /// Ends the buff, resetting stats and allowing movement.
+ ///
+ ///
+ public override void OnEnd(Buff buff)
+ {
+ buff.Target.RemoveState(StateType.Held);
+
+ RemovePropertyModifier(buff, buff.Target, PropertyName.PATK_BM);
+ RemovePropertyModifier(buff, buff.Target, PropertyName.SkillRange_BM);
+ RemovePropertyModifier(buff, buff.Target, PropertyName.ASPD_BM);
+ }
+
+ ///
+ /// Applies the debuff's effect during the combat calculations.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void OnAttackBeforeCalc(Buff buff, ICombatEntity attacker, ICombatEntity target, Skill skill, SkillModifier modifier, SkillHitResult skillHitResult)
+ {
+ if (buff.NumArg2 > 0)
+ modifier.BonusCritChance += buff.NumArg2;
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/BlockAndShoot_Buff.cs b/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/BlockAndShoot_Buff.cs
new file mode 100644
index 000000000..0775d1b61
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/BlockAndShoot_Buff.cs
@@ -0,0 +1,89 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Network;
+using Melia.Zone.Skills;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.World.Actors;
+
+namespace Melia.Zone.Buffs.Handlers.Archers.QuarrelShooter
+{
+ ///
+ /// Buff handler for Block and Shoot Buff, which grants
+ /// extra block.
+ /// The covered shot version decreases damage dealt and
+ /// taken by 50%, and cuts accuracy by 70%
+ ///
+ ///
+ /// NumArg1: Skill Level
+ /// NumArg2: 1 if Covered Shot is active
+ ///
+ [BuffHandler(BuffId.BlockAndShoot_Buff)]
+ public class BlockAndShoot_Buff : BuffHandler, IBuffCombatAttackBeforeCalcHandler, IBuffCombatDefenseBeforeCalcHandler
+ {
+ private const float BlkBuffRate = 0.5f;
+ private const float HRDebuffRate = 0.7f;
+
+ ///
+ /// Starts buff, increasing Block
+ ///
+ ///
+ public override void OnActivate(Buff buff, ActivationType activationType)
+ {
+ var target = buff.Target;
+
+ var bonusBlk = target.Properties.GetFloat(PropertyName.BLK) * BlkBuffRate;
+
+ AddPropertyModifier(buff, buff.Target, PropertyName.BLK_BM, bonusBlk);
+
+ if (buff.NumArg2 > 0)
+ {
+ var reduceHR = target.Properties.GetFloat(PropertyName.HR) * HRDebuffRate;
+
+ AddPropertyModifier(buff, buff.Target, PropertyName.HR_BM, -reduceHR);
+ }
+ }
+
+ ///
+ /// Ends the buff, resetting Block.
+ ///
+ ///
+ public override void OnEnd(Buff buff)
+ {
+ RemovePropertyModifier(buff, buff.Target, PropertyName.BLK_BM);
+ if (buff.NumArg2 > 0)
+ RemovePropertyModifier(buff, buff.Target, PropertyName.HR_BM);
+ }
+
+
+ ///
+ /// Applies the debuff's effect during the combat calculations.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void OnAttackBeforeCalc(Buff buff, ICombatEntity attacker, ICombatEntity target, Skill skill, SkillModifier modifier, SkillHitResult skillHitResult)
+ {
+ if (buff.NumArg2 > 0)
+ modifier.DamageMultiplier -= 0.5f;
+ }
+
+
+ ///
+ /// Applies the debuff's effect during the combat calculations.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void OnDefenseBeforeCalc(Buff buff, ICombatEntity attacker, ICombatEntity target, Skill skill, SkillModifier modifier, SkillHitResult skillHitResult)
+ {
+ if (buff.NumArg2 > 0)
+ modifier.DamageMultiplier -= 0.5f;
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/RapidFire_Block_Buff.cs b/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/RapidFire_Block_Buff.cs
new file mode 100644
index 000000000..edb24ed02
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/RapidFire_Block_Buff.cs
@@ -0,0 +1,40 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Network;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.World.Actors;
+
+namespace Melia.Zone.Buffs.Handlers.Archers.QuarrelShooter
+{
+ ///
+ /// Buff handler for Rapidfire Block Buff, which grants
+ /// extra block.
+ ///
+ [BuffHandler(BuffId.RapidFire_Block_Buff)]
+ public class RapidFire_Block_Buff : BuffHandler
+ {
+ private const float BlkBuffRate = 0.2f;
+
+ ///
+ /// Starts buff, increasing Block
+ ///
+ ///
+ public override void OnActivate(Buff buff, ActivationType activationType)
+ {
+ var target = buff.Target;
+
+ var bonusBlk = target.Properties.GetFloat(PropertyName.BLK) * BlkBuffRate;
+
+ AddPropertyModifier(buff, buff.Target, PropertyName.BLK_BM, bonusBlk);
+ }
+
+ ///
+ /// Ends the buff, resetting Block.
+ ///
+ ///
+ public override void OnEnd(Buff buff)
+ {
+ RemovePropertyModifier(buff, buff.Target, PropertyName.BLK_BM);
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/RapidFire_Debuff.cs b/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/RapidFire_Debuff.cs
new file mode 100644
index 000000000..5a806a36a
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/RapidFire_Debuff.cs
@@ -0,0 +1,29 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+
+namespace Melia.Zone.Buffs.Handlers.Archers.Ranger
+{
+ ///
+ /// Handle for the Rapidfire Debuff, which flatly reduces
+ /// Crit Dodge by 150
+ ///
+ ///
+ /// NumArg1: Skill Level
+ /// NumArg2: None
+ ///
+ [BuffHandler(BuffId.RapidFire_Debuff)]
+ public class RapidFire_Debuff : BuffHandler
+ {
+ private const float CRTDRPenalty = -150f;
+
+ public override void OnActivate(Buff buff, ActivationType activationType)
+ {
+ AddPropertyModifier(buff, buff.Target, PropertyName.CRTDR_BM, CRTDRPenalty);
+ }
+
+ public override void OnEnd(Buff buff)
+ {
+ RemovePropertyModifier(buff, buff.Target, PropertyName.CRTDR_BM);
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/RunningShot_Buff.cs b/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/RunningShot_Buff.cs
new file mode 100644
index 000000000..d4f93979d
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/RunningShot_Buff.cs
@@ -0,0 +1,76 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Network;
+using Melia.Zone.Skills;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.World.Actors;
+
+namespace Melia.Zone.Buffs.Handlers.Archers.QuarrelShooter
+{
+ ///
+ /// Handle for the Running Shot buff, which adds bonus factor
+ /// and an extra hit to crossbow normal attacks, as well as
+ /// increasing attack speed by 10%
+ /// It also debuffs your movespeed if you use it on a PVP map
+ ///
+ ///
+ /// NumArg1: Skill Level
+ /// NumArg2: Skill Factor
+ ///
+ [BuffHandler(BuffId.RunningShot_Buff)]
+ public class RunningShot_Buff : BuffHandler, IBuffCombatAttackBeforeCalcHandler
+ {
+ private const float AspdRateBonus = 0.1f;
+ private const float MspdDebuff = 2f;
+
+ ///
+ /// Starts buff, modifying the movement speed.
+ ///
+ ///
+ public override void OnActivate(Buff buff, ActivationType activationType)
+ {
+ var target = buff.Target;
+
+ var aspdBonus = target.Properties.GetFloat(PropertyName.ASPD) * AspdRateBonus;
+ AddPropertyModifier(buff, target, PropertyName.ASPD_BM, aspdBonus);
+
+ // TODO: Should only apply the debuff on PVP maps
+ // if (caster.Map.IsPvp)
+
+ AddPropertyModifier(buff, target, PropertyName.MSPD_BM, -MspdDebuff);
+ Send.ZC_MSPD(target);
+ }
+
+ ///
+ /// Ends the buff, resetting the movement speed.
+ ///
+ ///
+ public override void OnEnd(Buff buff)
+ {
+ RemovePropertyModifier(buff, buff.Target, PropertyName.ASPD_BM);
+ RemovePropertyModifier(buff, buff.Target, PropertyName.MSPD_BM);
+ Send.ZC_MSPD(buff.Target);
+ }
+
+
+ ///
+ /// Applies the buff's effect during the combat calculations.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void OnAttackBeforeCalc(Buff buff, ICombatEntity attacker, ICombatEntity target, Skill skill, SkillModifier modifier, SkillHitResult skillHitResult)
+ {
+ // only applies to crossbow normal attack
+ if (skill.Id != SkillId.CrossBow_Attack)
+ return;
+
+ // Add extra Factor and +1 hit count
+ modifier.BonusFactor += buff.NumArg2;
+ modifier.HitCount++;
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/ScatterCaltrop_Debuff.cs b/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/ScatterCaltrop_Debuff.cs
new file mode 100644
index 000000000..20241577e
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/ScatterCaltrop_Debuff.cs
@@ -0,0 +1,38 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Network;
+
+namespace Melia.Zone.Buffs.Handlers.Archers.QuarrelShooter
+{
+ ///
+ /// Handler for ScatterCaltrop_Debuff, which flatly reduces movement speed
+ ///
+ [BuffHandler(BuffId.ScatterCaltrop_Debuff)]
+ public class ScatterCaltrop_Debuff : BuffHandler
+ {
+ private const float MspdDebuff = 10f;
+
+ ///
+ /// Starts buff, modifying the movement speed.
+ ///
+ ///
+ public override void OnActivate(Buff buff, ActivationType activationType)
+ {
+ var target = buff.Target;
+ var caster = buff.Caster;
+
+ AddPropertyModifier(buff, target, PropertyName.MSPD_BM, -MspdDebuff);
+ 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/Archers/QuarrelShooter/ScatterCaltrop_Hole_Debuff.cs b/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/ScatterCaltrop_Hole_Debuff.cs
new file mode 100644
index 000000000..6fa2729d8
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Archers/QuarrelShooter/ScatterCaltrop_Hole_Debuff.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.Archers.QuarrelShooter
+{
+ ///
+ /// Handle for the ScatterCaltrop Hole Debuff, which prevents movement
+ ///
+ [BuffHandler(BuffId.ScatterCaltrop_Hole_Debuff)]
+ public class ScatterCaltrop_Hole_Debuff : BuffHandler
+ {
+ public override void OnExtend(Buff buff)
+ {
+ buff.Target.AddState(StateType.Held, buff.Duration);
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Common/Common_ShieldDesrption.cs b/src/ZoneServer/Buffs/Handlers/Common/Common_ShieldDesrption.cs
new file mode 100644
index 000000000..97466ff47
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Common/Common_ShieldDesrption.cs
@@ -0,0 +1,32 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.World.Actors.Characters;
+using Melia.Zone.World.Actors.Characters.Components;
+
+namespace Melia.Zone.Buffs.Handlers.Common
+{
+ ///
+ /// Handler for the Disarm Shield Debuff,
+ /// which unequips your shield and prevents
+ /// equipping shields for its duration
+ ///
+ [BuffHandler(BuffId.Common_ShieldDesrption)]
+ public class Common_ShieldDesrption : BuffHandler
+ {
+ public override void OnActivate(Buff buff, ActivationType activationType)
+ {
+ var target = buff.Target;
+
+ if (target.Components.TryGet(out var inventory))
+ {
+ var lhItem = inventory.GetItem(EquipSlot.LeftHand);
+ if (lhItem.Data.EquipType1 != EquipType.Shield)
+ return;
+
+ inventory.Unequip(EquipSlot.LeftHand);
+ }
+
+ // TODO: does this have any effect on monsters?
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Common/UC_bleed.cs b/src/ZoneServer/Buffs/Handlers/Common/UC_bleed.cs
new file mode 100644
index 000000000..77234f2cd
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Common/UC_bleed.cs
@@ -0,0 +1,25 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.World.Actors;
+
+namespace Melia.Zone.Buffs.Handlers.Common
+{
+ ///
+ /// Buff handler for UC_bleed, which deals damage in regular intervals.
+ ///
+ [BuffHandler(BuffId.UC_bleed)]
+ public class UC_bleed : BuffHandler
+ {
+ public override void WhileActive(Buff buff)
+ {
+ if (buff.Target.IsDead)
+ return;
+
+ var attacker = buff.Caster;
+ var target = buff.Target;
+ var damage = buff.NumArg2;
+
+ target.TakeSimpleHit(damage, attacker, SkillId.None);
+ }
+ }
+}
diff --git a/src/ZoneServer/Pads/Handlers/Archer/QuarrelShooter/QuarrelShooter_BlockAndShoot.cs b/src/ZoneServer/Pads/Handlers/Archer/QuarrelShooter/QuarrelShooter_BlockAndShoot.cs
new file mode 100644
index 000000000..efd318c6f
--- /dev/null
+++ b/src/ZoneServer/Pads/Handlers/Archer/QuarrelShooter/QuarrelShooter_BlockAndShoot.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Threading.Tasks;
+using Melia.Shared.Game.Const;
+using Melia.Shared.World;
+using Melia.Zone.Network;
+using Melia.Zone.World.Actors;
+using Melia.Zone.World.Actors.Characters;
+using Melia.Zone.World.Actors.CombatEntities.Components;
+using Melia.Zone.World.Actors.Monsters;
+using Melia.Zone.World.Actors.Pads;
+using Yggdrasil.Logging;
+
+namespace Melia.Zone.Pads.Handlers.Archer.QuarrelShooter
+{
+ ///
+ /// Handler for the QuarrelShooter_BlockAndShoot pad,
+ /// which inflicts damage to enemies inside of it
+ ///
+ [PadHandler(PadName.QuarrelShooter_BlockAndShoot)]
+ public class QuarrelShooter_BlockAndShoot : ICreatePadHandler, IDestroyPadHandler
+ {
+ ///
+ /// Called when the pad is created.
+ ///
+ ///
+ ///
+ public void Created(object sender, PadTriggerArgs args)
+ {
+ Send.ZC_NORMAL.PadUpdate(args.Creator, args.Trigger, PadName.QuarrelShooter_BlockAndShoot, -0.7853982f, 0, 30, true);
+ }
+
+ ///
+ /// Called when the pad is destroyed.
+ ///
+ ///
+ ///
+ public void Destroyed(object sender, PadTriggerArgs args)
+ {
+ Send.ZC_NORMAL.PadUpdate(args.Creator, args.Trigger, PadName.QuarrelShooter_BlockAndShoot, 0, 145.8735f, 30, false);
+ }
+ }
+}
diff --git a/src/ZoneServer/Pads/Handlers/Archer/QuarrelShooter/ScatterCaltrop_Pad.cs b/src/ZoneServer/Pads/Handlers/Archer/QuarrelShooter/ScatterCaltrop_Pad.cs
new file mode 100644
index 000000000..ccc396633
--- /dev/null
+++ b/src/ZoneServer/Pads/Handlers/Archer/QuarrelShooter/ScatterCaltrop_Pad.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Threading.Tasks;
+using Melia.Shared.Game.Const;
+using Melia.Shared.World;
+using Melia.Zone.Network;
+using Melia.Zone.World.Actors;
+using Melia.Zone.World.Actors.Characters;
+using Melia.Zone.World.Actors.CombatEntities.Components;
+using Melia.Zone.World.Actors.Monsters;
+using Melia.Zone.World.Actors.Pads;
+using Yggdrasil.Logging;
+
+namespace Melia.Zone.Pads.Handlers.Archer.QuarrelShooter
+{
+ ///
+ /// Handler for the ScatterCaltrop_Pad,
+ /// which inflicts damage and status when stepped on
+ ///
+ [PadHandler(PadName.ScatterCaltrop_Pad)]
+ public class ScatterCaltrop_Pad : ICreatePadHandler, IDestroyPadHandler
+ {
+ ///
+ /// Called when the pad is created.
+ ///
+ ///
+ ///
+ public void Created(object sender, PadTriggerArgs args)
+ {
+ Send.ZC_NORMAL.PadUpdate(args.Creator, args.Trigger, PadName.ScatterCaltrop_Pad, -0.7853982f, 0, 30, true);
+ }
+
+ ///
+ /// Called when the pad is destroyed.
+ ///
+ ///
+ ///
+ public void Destroyed(object sender, PadTriggerArgs args)
+ {
+ Send.ZC_NORMAL.PadUpdate(args.Creator, args.Trigger, PadName.ScatterCaltrop_Pad, 0, 145.8735f, 30, false);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Combat/SkillModifier.cs b/src/ZoneServer/Skills/Combat/SkillModifier.cs
index 127e61a0e..07dfa08e2 100644
--- a/src/ZoneServer/Skills/Combat/SkillModifier.cs
+++ b/src/ZoneServer/Skills/Combat/SkillModifier.cs
@@ -18,6 +18,11 @@ public class SkillModifier
///
public float BonusMAtk { get; set; }
+ ///
+ /// Gets or sets bonus skill factor.
+ ///
+ public float BonusFactor { get; set; }
+
///
/// Gets or sets flat damage bonus added to skill damage.
///
diff --git a/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_BlockAndShoot.cs b/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_BlockAndShoot.cs
new file mode 100644
index 000000000..47fc85c4c
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_BlockAndShoot.cs
@@ -0,0 +1,175 @@
+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.Characters;
+using Melia.Zone.World.Actors.Characters.Components;
+using Melia.Zone.World.Actors.Monsters;
+using Melia.Zone.World.Actors.Pads;
+using Yggdrasil.Util;
+using static Melia.Shared.Util.TaskHelper;
+using static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Swordsmen.Highlander
+{
+ ///
+ /// Handler for the Quarrel Shooter skill Block and Shoot.
+ ///
+ [SkillHandler(SkillId.QuarrelShooter_BlockAndShoot)]
+ public class QuarrelShooter_BlockAndShoot : IGroundSkillHandler, IDynamicCasted
+ {
+ private const int TotalHits = 6;
+
+ public void StartDynamicCast(Skill skill, ICombatEntity caster)
+ {
+ //Send.ZC_PLAY_SOUND_Gendered(caster, "voice_archer_m_blockandshoot_cast", "voice_archer_f_blockandshoot_cast");
+
+ // QuarrelShooter35 alters the buff significantly
+ var coveredShotVersion = 0;
+ if (caster.IsAbilityActive(AbilityId.QuarrelShooter35))
+ coveredShotVersion++;
+
+ caster.StartBuff(BuffId.BlockAndShoot_Buff, skill.Level, coveredShotVersion, TimeSpan.FromSeconds(3), caster);
+ }
+
+ ///
+ /// Called when the user stops casting the skill.
+ ///
+ ///
+ ///
+ public void EndDynamicCast(Skill skill, ICombatEntity caster)
+ {
+ //Send.ZC_STOP_SOUND_Gendered(caster, "voice_archer_m_blockandshoot_cast", "voice_archer_f_blockandshoot_cast");
+ caster.StopBuff(BuffId.BlockAndShoot_Buff);
+ }
+
+
+ ///
+ /// Handles skill, creating a pad
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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, originPos, length: 100, width: 50, 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.SetPad(skill, caster, splashArea));
+ }
+
+ ///
+ /// Places the pad
+ ///
+ ///
+ ///
+ ///
+ private async Task SetPad(Skill skill, ICombatEntity caster, ISplashArea padArea)
+ {
+ var initialDelay = TimeSpan.FromMilliseconds(300);
+ var skillHitDelay = TimeSpan.Zero;
+
+ await Task.Delay(initialDelay);
+
+ // TODO Missing animation. Needs MSL Pad Throw
+
+ var pad = new Pad(PadName.QuarrelShooter_BlockAndShoot, caster, skill, padArea);
+ pad.Position = caster.Position.GetRelative(caster.Direction, 50f);
+ // Base target count 2, gains 1 more for every 3 AOE attack ratio
+ pad.Trigger.MaxActorCount = 2 + (int)(caster.Properties.GetFloat(PropertyName.SR) / 3f);
+ // Although the description says the cast lasts 3 seconds,
+ // it actually only lasts a bit over 2 seconds, so the pad
+ // hits slightly more often than 500ms to deal all 5 hits
+ pad.Trigger.LifeTime = TimeSpan.FromMilliseconds(2400);
+ pad.Trigger.UpdateInterval = TimeSpan.FromMilliseconds(400);
+ pad.Trigger.Subscribe(TriggerType.Create, this.Attack);
+ pad.Trigger.Subscribe(TriggerType.Update, this.Attack);
+
+ caster.Map.AddPad(pad);
+ }
+
+ ///
+ /// Called by the pad every 0.5 sec to damage
+ /// targets inside of it
+ ///
+ ///
+ ///
+ private void Attack(object sender, PadTriggerArgs args)
+ {
+ var pad = args.Trigger;
+ var creator = args.Creator;
+ var skill = args.Skill;
+
+ // skill ends early if you stop the dynamic cast
+ if (!creator.IsCasting())
+ {
+ pad.Destroy();
+ return;
+ }
+
+ var damageDelay = TimeSpan.FromMilliseconds(50);
+ var skillHitDelay = TimeSpan.Zero;
+
+ var targets = pad.Trigger.GetAttackableEntities(creator);
+ var hits = new List();
+
+ foreach (var target in targets)
+ {
+ var modifier = SkillModifier.Default;
+
+ // At some point this skill gained 15% of your shield's Def
+ // modifier.BonusPAtk += GetBonusPAtk(creator);
+
+ var skillHitResult = SCR_SkillHit(creator, target, skill);
+ target.TakeDamage(skillHitResult.Damage, creator);
+
+ var skillHit = new SkillHitInfo(creator, target, skill, skillHitResult, damageDelay, skillHitDelay);
+ skillHit.HitEffect = HitEffect.Impact;
+
+ hits.Add(skillHit);
+ }
+
+ Send.ZC_SKILL_HIT_INFO(creator, hits);
+ }
+
+ ///
+ /// Calculates bonus PATK if using a shield
+ ///
+ ///
+ private float GetBonusPAtk(ICombatEntity caster)
+ {
+ if (!caster.Components.TryGet(out var inv))
+ return 0;
+
+ var lhItem = inv.GetItem(EquipSlot.LeftHand);
+ if (lhItem.Data.EquipType1 != EquipType.Shield)
+ return 0;
+
+ var shieldDef = lhItem.Data.Def;
+ var bonusPatk = 0.15f * shieldDef;
+
+ return bonusPatk;
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_Kneelingshot.cs b/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_Kneelingshot.cs
new file mode 100644
index 000000000..f9f7de711
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_Kneelingshot.cs
@@ -0,0 +1,53 @@
+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.Archers.QuarrelShooter
+{
+ ///
+ /// Handler for the Quarrel Shooter skill Kneeling Shot.
+ ///
+ [SkillHandler(SkillId.QuarrelShooter_Kneelingshot)]
+ public class QuarrelShooter_Kneelingshot : ISelfSkillHandler
+ {
+ ///
+ /// Handles skill, applying a buff to the caster.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, Position originPos, Direction dir)
+ {
+ if (caster.IsBuffActive(BuffId.Archer_Kneelingshot))
+ {
+ caster.StopBuff(BuffId.Archer_Kneelingshot);
+ }
+ else
+ {
+ if (!caster.TrySpendSp(skill))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+
+ var criticalBonus = 0;
+
+ // Focusing gives 2% crit bonus per level
+ if (caster.TryGetActiveAbilityLevel(AbilityId.Focusing, out var level))
+ criticalBonus = 2 * level;
+
+ caster.StartBuff(BuffId.Archer_Kneelingshot, skill.Level, criticalBonus, TimeSpan.Zero, caster);
+ }
+
+ skill.IncreaseOverheat();
+ caster.SetAttackState(true);
+
+ Send.ZC_SKILL_MELEE_TARGET(caster, skill, caster, null);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_RapidFire.cs b/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_RapidFire.cs
new file mode 100644
index 000000000..2bc0a778e
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_RapidFire.cs
@@ -0,0 +1,161 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection.Emit;
+using System.Threading.Tasks;
+using Melia.Shared.Game.Const;
+using Melia.Shared.L10N;
+using Melia.Zone.Buffs;
+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.Characters.Components;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Yggdrasil.Util;
+using static Melia.Shared.Util.TaskHelper;
+using static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Archers.QuarrelShooter
+{
+ ///
+ /// Handles the Quarrel Shooter skill Rapid Fire.
+ ///
+ [SkillHandler(SkillId.QuarrelShooter_RapidFire)]
+ public class QuarrelShooter_RapidFire : ITargetSkillHandler
+ {
+ private const float knockbackRange = 75f;
+
+ ///
+ /// Handles the skill, dealing knockback and damage
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, ICombatEntity target)
+ {
+ if (!caster.TrySpendSp(skill))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+
+ skill.IncreaseOverheat();
+ caster.TurnTowards(target);
+ caster.SetAttackState(true);
+
+ if (target == null)
+ {
+ Send.ZC_SKILL_FORCE_TARGET(caster, null, skill, null);
+ return;
+ }
+
+ if (!caster.InSkillUseRange(skill, target))
+ {
+ caster.ServerMessage(Localization.Get("Too far away."));
+ Send.ZC_SKILL_FORCE_TARGET(caster, null, skill, null);
+ return;
+ }
+
+ CallSafe(this.Attack(skill, caster, target));
+ }
+
+
+ ///
+ /// Executes the actual attack.
+ ///
+ ///
+ ///
+ ///
+ private async Task Attack(Skill skill, ICombatEntity caster, ICombatEntity target)
+ {
+ var hitDelay = TimeSpan.FromMilliseconds(200);
+ var damageDelay = TimeSpan.FromMilliseconds(30);
+ var skillHitDelay = TimeSpan.Zero;
+
+ await Task.Delay(hitDelay);
+
+ // The initial shot is a dummy hit.
+ // This exists only to display the animation
+ // and apply the knockback, it does not deal damage
+ SkillHitResult skillHitResultDummy = new SkillHitResult();
+ skillHitResultDummy.Result = HitResultType.Hit;
+ skillHitResultDummy.Effect = HitEffect.Impact;
+ skillHitResultDummy.HitCount = 1;
+ skillHitResultDummy.Damage = 0;
+
+ var skillHitDummy = new SkillHitInfo(caster, target, skill, skillHitResultDummy, damageDelay, skillHitDelay);
+ skillHitDummy.ForceId = ForceId.GetNew();
+
+ // Check to see if we do the knockback
+ // This is only applied if you have a shield,
+ // and the target is within 75 units
+
+ if (caster.Components.TryGet(out var inventory))
+ {
+ var lhItem = inventory.GetItem(EquipSlot.LeftHand);
+ if (lhItem.Data.EquipType1 == EquipType.Shield && caster.Position.Get2DDistance(target.Position) <= knockbackRange)
+ {
+ // TODO: This is actually a Knockback: Motion
+ // which isn't implemented yet
+ skillHitDummy.KnockBackInfo = new KnockBackInfo(caster.Position, target.Position, HitType.KnockBack, skill.Data.KnockDownVelocity, skill.Data.KnockDownVAngle);
+ skillHitDummy.ApplyKnockBack(target);
+
+ // gain block buff on successful knockback
+ caster.StartBuff(BuffId.RapidFire_Block_Buff, skill.Level, 0, TimeSpan.FromSeconds(10), caster);
+ }
+ }
+
+ Send.ZC_SKILL_FORCE_TARGET(caster, target, skill, skillHitDummy);
+
+ // The actual damage is applied as a series of standard ground hitboxes
+
+ var blastDelay = TimeSpan.FromMilliseconds(120);
+ var delayBetweenHits = TimeSpan.FromMilliseconds(20);
+ float[] blastSizes = { 0.5f, 1f, 1f, 1f, 1.2f };
+
+ var splashArea = new Circle(target.Position, 50);
+
+ var hits = new List();
+
+ await Task.Delay(blastDelay);
+
+ for (var i = 0; i < 5; i++)
+ {
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+
+ // play the animation
+ Send.ZC_NORMAL.PlayEffect(target, "F_archer_caltrop_hit_explosion", blastSizes[i]);
+
+ // there's a second animation on the last hit
+ if (i == 4)
+ {
+ Send.ZC_NORMAL.PlayEffect(target, "F_explosion097", 1f);
+
+ // QuarrelShooter23 gives a 50% chance to reduce crit dr
+ if (caster.TryGetActiveAbilityLevel(AbilityId.QuarrelShooter23, out var level) && RandomProvider.Get().Next(2) == 1)
+ {
+ target.StartBuff(BuffId.RapidFire_Debuff, skill.Level, 0, TimeSpan.FromSeconds(3 * level), caster);
+ }
+ }
+
+ foreach (var hitTarget in targets.LimitBySDR(caster, skill))
+ {
+ var skillHitResult = SCR_SkillHit(caster, hitTarget, skill);
+ hitTarget.TakeDamage(skillHitResult.Damage, caster);
+
+ var skillHit = new SkillHitInfo(caster, hitTarget, skill, skillHitResult, damageDelay, skillHitDelay);
+ skillHit.HitEffect = HitEffect.Impact;
+ hits.Add(skillHit);
+ }
+
+ Send.ZC_SKILL_HIT_INFO(caster, hits);
+
+ hits.Clear();
+
+ if (i < 5)
+ await Task.Delay(delayBetweenHits);
+ }
+
+ // Have to send a Cast cancel at the end
+ Send.ZC_SKILL_CAST_CANCEL(caster);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_RunningShot.cs b/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_RunningShot.cs
new file mode 100644
index 000000000..3eac9bb51
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_RunningShot.cs
@@ -0,0 +1,43 @@
+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.Swordsman
+{
+ ///
+ /// Handler for the Quarrel Shooter skill Running Shot
+ ///
+ [SkillHandler(SkillId.QuarrelShooter_RunningShot)]
+ public class QuarrelShooter_RunningShot : ISelfSkillHandler
+ {
+ ///
+ /// Handles skill, applying the 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);
+
+ var target = caster;
+
+ var duration = TimeSpan.FromMinutes(30);
+ target.StartBuff(BuffId.RunningShot_Buff, skill.Level, skill.Properties.GetFloat(PropertyName.SkillFactor), duration, caster);
+
+ Send.ZC_SKILL_MELEE_TARGET(caster, skill, target, null);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_ScatterCaltrop.cs b/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_ScatterCaltrop.cs
new file mode 100644
index 000000000..358e2debe
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_ScatterCaltrop.cs
@@ -0,0 +1,228 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+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 Melia.Zone.World.Actors.Monsters;
+using Melia.Zone.World.Actors.Pads;
+using Yggdrasil.Logging;
+using Yggdrasil.Util;
+using static Melia.Shared.Util.TaskHelper;
+using static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Swordsmen.Hoplite
+{
+ ///
+ /// Handler for the Hoplite skill Long Stride
+ ///
+ [SkillHandler(SkillId.QuarrelShooter_ScatterCaltrop)]
+ public class QuarrelShooter_ScatterCaltrop : IGroundSkillHandler
+ {
+ public const int MaxDistance = 180;
+
+ ///
+ /// Handles skill, damaging targets.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target)
+ {
+ if (!skill.Vars.TryGet("Melia.ToolGroundPos", out var targetPos))
+ {
+ caster.ServerMessage(Localization.Get("No target location specified."));
+ Send.ZC_SKILL_CAST_CANCEL(caster);
+ return;
+ }
+
+ if (!caster.Map.Ground.IsValidPosition(targetPos))
+ {
+ caster.ServerMessage(Localization.Get("Invalid target location."));
+ Send.ZC_SKILL_CAST_CANCEL(caster);
+ return;
+ }
+
+ if ((int)caster.Position.Get2DDistance(targetPos) > MaxDistance)
+ {
+ caster.ServerMessage(Localization.Get("Too far."));
+ Send.ZC_SKILL_CAST_CANCEL(caster);
+ return;
+ }
+
+ if (!caster.TrySpendSp(skill))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+
+ skill.IncreaseOverheat();
+ caster.SetAttackState(true);
+
+ Send.ZC_SKILL_READY(caster, skill, originPos, targetPos);
+ Send.ZC_SKILL_MELEE_GROUND(caster, skill, targetPos, null);
+
+ var padArea = new Circle(targetPos, 50);
+
+ CallSafe(this.SetPad(skill, caster, padArea));
+ }
+
+ ///
+ /// Places the pad
+ ///
+ ///
+ ///
+ ///
+ private async Task SetPad(Skill skill, ICombatEntity caster, Circle padArea)
+ {
+ var initialDelay = TimeSpan.FromMilliseconds(100);
+ var padDelay = TimeSpan.FromMilliseconds(70);
+ var skillHitDelay = TimeSpan.Zero;
+
+ Send.ZC_PLAY_ANI(caster, "CALTROP");
+
+ await Task.Delay(initialDelay);
+
+ await CaltropsAnimation(caster, padArea.Center);
+
+ await Task.Delay(padDelay);
+
+ // TODO Missing MSL Pad Throw
+
+ var pad = new Pad(PadName.ScatterCaltrop_Pad, caster, skill, padArea);
+ pad.Position = padArea.Center;
+ pad.Trigger.MaxActorCount = 6;
+ pad.Trigger.LifeTime = TimeSpan.FromSeconds(10);
+ pad.Trigger.UpdateInterval = TimeSpan.FromSeconds(1);
+ pad.Trigger.Subscribe(TriggerType.Create, this.Attack);
+ pad.Trigger.Subscribe(TriggerType.Update, this.Attack);
+ pad.Trigger.Subscribe(TriggerType.Destroy, this.Expire);
+
+ caster.Map.AddPad(pad);
+ }
+
+
+ ///
+ /// Displays the animation of throwing the caltrops,
+ /// which takes 10 separate packets
+ ///
+ ///
+ ///
+ private async Task CaltropsAnimation(ICombatEntity caster, Position center)
+ {
+ var projectileDelay = TimeSpan.FromMilliseconds(20);
+
+ Send.ZC_NORMAL.SkillProjectile(caster, "I_archer_shot_caltrops_mash#Bip01 R Hand", 0.2f, "F_smoke008##1", 1f, center.GetRelative(new Direction(-65.3201909439d), 14.57909f), 10f, 0.5f, 0, 600);
+
+ await Task.Delay(projectileDelay);
+
+ Send.ZC_NORMAL.SkillProjectile(caster, "I_archer_shot_caltrops_mash#Bip01 R Hand", 0.2f, "F_smoke008##1", 1f, center.GetRelative(new Direction(157.219126241d), 9.0695782f), 10f, 0.5f, 0, 600);
+
+ await Task.Delay(projectileDelay);
+
+ Send.ZC_NORMAL.SkillProjectile(caster, "I_archer_shot_caltrops_mash#Bip01 R Hand", 0.2f, "F_smoke008##1", 1f, center.GetRelative(new Direction(44.72427704d), 8.0112038f), 10f, 0.5f, 0, 600);
+
+ await Task.Delay(projectileDelay);
+
+ Send.ZC_NORMAL.SkillProjectile(caster, "I_archer_shot_caltrops_mash#Bip01 R Hand", 0.2f, "F_smoke008##1", 1f, center.GetRelative(new Direction(-132.92109d), 5.9680967f), 10f, 0.5f, 0, 600);
+
+ await Task.Delay(projectileDelay);
+
+ Send.ZC_NORMAL.SkillProjectile(caster, "I_archer_shot_caltrops_mash#Bip01 R Hand", 0.2f, "F_smoke008##1", 1f, center.GetRelative(new Direction(-6.799365d), 20.712799f), 10f, 0.5f, 0, 600);
+
+ await Task.Delay(projectileDelay);
+
+ Send.ZC_NORMAL.SkillProjectile(caster, "I_archer_shot_caltrops_mash#Bip01 R Hand", 0.2f, "F_smoke008##1", 1f, center.GetRelative(new Direction(-146.21099d), 17.101896f), 10f, 0.5f, 0, 600);
+
+ await Task.Delay(projectileDelay);
+
+ Send.ZC_NORMAL.SkillProjectile(caster, "I_archer_shot_caltrops_mash#Bip01 R Hand", 0.2f, "F_smoke008##1", 1f, center.GetRelative(new Direction(133.31455d), 16.745005f), 10f, 0.5f, 0, 600);
+
+ await Task.Delay(projectileDelay);
+
+ Send.ZC_NORMAL.SkillProjectile(caster, "I_archer_shot_caltrops_mash#Bip01 R Hand", 0.2f, "F_smoke008##1", 1f, center.GetRelative(new Direction(-168.79307d), 3.601325f), 10f, 0.5f, 0, 600);
+
+ await Task.Delay(projectileDelay);
+
+ Send.ZC_NORMAL.SkillProjectile(caster, "I_archer_shot_caltrops_mash#Bip01 R Hand", 0.2f, "F_smoke008##1", 1f, center.GetRelative(new Direction(-6.691923d), 7.9787741f), 10f, 0.5f, 0, 600);
+
+ await Task.Delay(projectileDelay);
+
+ Send.ZC_NORMAL.SkillProjectile(caster, "I_archer_shot_caltrops_mash#Bip01 R Hand", 0.2f, "F_smoke008##1", 1f, center.GetRelative(new Direction(-127.51038d), 12.984505f), 10f, 0.5f, 0, 600);
+ }
+
+ ///
+ /// Called by the pad every second to damage
+ /// targets inside of it
+ ///
+ ///
+ ///
+ private void Attack(object sender, PadTriggerArgs args)
+ {
+ var pad = args.Trigger;
+ var creator = args.Creator;
+ var skill = args.Skill;
+
+ var damageDelay = TimeSpan.FromMilliseconds(100);
+ var skillHitDelay = TimeSpan.Zero;
+
+ var targets = pad.Trigger.GetAttackableEntities(creator);
+ var hits = new List();
+
+ foreach (var target in targets)
+ {
+ if (target.MoveType == MoveType.Flying || target.MoveType == MoveType.Fly)
+ continue;
+
+ var skillHitResult = SCR_SkillHit(creator, target, skill);
+ target.TakeDamage(skillHitResult.Damage, creator);
+
+ var skillHit = new SkillHitInfo(creator, target, skill, skillHitResult, damageDelay, skillHitDelay);
+ skillHit.HitEffect = HitEffect.Impact;
+
+ target.StartBuff(BuffId.ScatterCaltrop_Debuff, skill.Level, 0, TimeSpan.FromSeconds(1), creator);
+
+ // QuarrelShooter3 inflicts bleed at 8% chance per level
+ if (creator.TryGetActiveAbilityLevel(AbilityId.QuarrelShooter3, out var level) && RandomProvider.Get().Next(100) < 8 * level)
+ {
+ // Unknown duration and damage
+ var damage = skillHitResult.Damage * 0.05f;
+ target.StartBuff(BuffId.UC_bleed, skill.Level, damage, TimeSpan.FromSeconds(5), creator);
+ }
+
+ hits.Add(skillHit);
+ }
+
+ Send.ZC_SKILL_HIT_INFO(creator, hits);
+ }
+
+ ///
+ /// Called by the pad when its time elapses
+ ///
+ ///
+ ///
+ private void Expire(object sender, PadTriggerArgs args)
+ {
+ var pad = args.Trigger;
+ var creator = args.Creator;
+ var skill = args.Skill;
+
+ var targets = pad.Trigger.GetAttackableEntities(creator);
+
+ foreach (var target in targets)
+ {
+ if (target.MoveType == MoveType.Flying || target.MoveType == MoveType.Fly)
+ continue;
+
+ target.StartBuff(BuffId.ScatterCaltrop_Hole_Debuff, skill.Level, 0, TimeSpan.FromSeconds(2), creator);
+ }
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_StonePicking.cs b/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_StonePicking.cs
new file mode 100644
index 000000000..ad2880178
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_StonePicking.cs
@@ -0,0 +1,59 @@
+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;
+using Melia.Zone.World.Actors.Characters;
+using Melia.Zone.World.Actors.Characters.Components;
+using Yggdrasil.Util;
+
+namespace Melia.Zone.Skills.Handlers.Archers.QuarrelShooter
+{
+ ///
+ /// Handler for the Quarrel Shooter skill Pick Stone.
+ ///
+ [SkillHandler(SkillId.QuarrelShooter_StonePicking)]
+ public class QuarrelShooter_StonePicking : IGroundSkillHandler
+ {
+ private const int StoneBulletId = 645503;
+
+
+ ///
+ /// Handles skill, acquiring Stone Bullets
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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);
+
+ Send.ZC_SKILL_MELEE_GROUND(caster, skill, originPos, null);
+
+ var quantity = skill.Level;
+
+ // Initially, the quantity was random between 1 and skill.level
+ // unless you had QuarrelShooter6
+
+ //if (!caster.IsAbilityActive(AbilityId.QuarrelShooter6)) {
+ // quantity = RandomProvider.Get().Next(skill.Level);
+ //}
+
+ if (caster.Components.TryGet(out var inventory))
+ {
+ inventory.Add(StoneBulletId, quantity, InventoryAddType.PickUp);
+ }
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_StoneShot.cs b/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_StoneShot.cs
new file mode 100644
index 000000000..cc4b982c7
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_StoneShot.cs
@@ -0,0 +1,196 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Melia.Shared.Game.Const;
+using Melia.Shared.L10N;
+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.Characters.Components;
+using Yggdrasil.Util;
+using static Melia.Shared.Util.TaskHelper;
+using static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Archers.QuarrelShooter
+{
+ ///
+ /// Handles the Quarrel Shooter skill Stone Shot.
+ ///
+ [SkillHandler(SkillId.QuarrelShooter_StoneShot)]
+ public class QuarrelShooter_StoneShot : ITargetSkillHandler
+ {
+ private const int StoneBulletId = 645503;
+ private const float StunDuration = 4f;
+
+ ///
+ /// Handles the skill, shoot enemy and apply debuff
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, ICombatEntity target)
+ {
+ if (!caster.TrySpendSp(skill))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+
+ if (target == null)
+ {
+ Send.ZC_SKILL_FORCE_TARGET(caster, null, skill, null);
+ return;
+ }
+
+ if (!this.UseAmmo(caster))
+ {
+ caster.ServerMessage(Localization.Get("You need a Stone Bullet."));
+ Send.ZC_SKILL_CAST_CANCEL(caster);
+ return;
+ }
+
+ if (!caster.InSkillUseRange(skill, target))
+ {
+ caster.ServerMessage(Localization.Get("Too far away."));
+ Send.ZC_SKILL_FORCE_TARGET(caster, null, skill, null);
+ return;
+ }
+
+ skill.IncreaseOverheat();
+ caster.TurnTowards(target);
+ caster.SetAttackState(true);
+
+ CallSafe(this.Attack(skill, caster, target));
+ }
+
+ ///
+ /// Executes the actual attack.
+ ///
+ ///
+ ///
+ ///
+ private async Task Attack(Skill skill, ICombatEntity caster, ICombatEntity target)
+ {
+ var damageDelay = TimeSpan.FromMilliseconds(280);
+ var animationDelay = TimeSpan.FromMilliseconds(300);
+ var skillHitDelay = TimeSpan.Zero;
+
+ var skillHitResult = SCR_SkillHit(caster, target, skill);
+ target.TakeDamage(skillHitResult.Damage, caster);
+
+ var hit = new HitInfo(caster, target, skill, skillHitResult);
+ hit.ForceId = ForceId.GetNew();
+ hit.ResultType = HitResultType.Unk8;
+
+ Send.ZC_NORMAL.PlayForceEffect(hit.ForceId, caster, caster, target, "I_archer_stonearrow_force_circle#Dummy_q_Force", 1f, "arrow_cast", "F_archer_stonearrow_hit_spread_out", 1, "arrow_blow", "SLOW", 600);
+ Send.ZC_HIT_INFO(caster, target, hit);
+
+
+ if (skillHitResult.Damage > 0)
+ {
+ var duration = TimeSpan.FromSeconds(10);
+ target.StartBuff(BuffId.StoneShot_Debuff, skill.Level, 0, duration, caster);
+
+ // QuarrelShooter34 adds a chance to inflict Disarm Shield
+ if (caster.TryGetActiveAbilityLevel(AbilityId.QuarrelShooter34, out var level) && RandomProvider.Get().Next(100) < 2 * level)
+ {
+ target.StartBuff(BuffId.Common_ShieldDesrption, skill.Level, 0, TimeSpan.FromSeconds(5), caster);
+ }
+ }
+
+ await Task.Delay(animationDelay);
+
+
+ // If the buff has reached max overbuff, we trigger the explosion
+ // and remove the buff
+ if (target.TryGetBuff(BuffId.StoneShot_Debuff, out var buff))
+ {
+ if (buff.IsFullyOverbuffed)
+ {
+ StoneBurst(skill.Level, caster, target);
+ target.StopBuff(BuffId.StoneShot_Debuff);
+ }
+ }
+
+ // This skill has an unusually long aftercast immobilization
+ // that probably requires this packet
+ Send.ZC_SKILL_CAST_CANCEL(caster);
+ }
+
+
+ ///
+ /// Uses one Stone Bullet if applicable. Returns false if consumption
+ /// failed and the skill should not be used.
+ ///
+ ///
+ ///
+ private bool UseAmmo(ICombatEntity caster)
+ {
+ if (Feature.IsEnabled("StoneShotNoAmmo"))
+ return true;
+
+ if (caster.Components.TryGet(out var inventory))
+ {
+ var removedCount = inventory.Remove(StoneBulletId, 1, InventoryItemRemoveMsg.Used);
+ if (removedCount == 0)
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Handles the Burst effect when the debuff hits 3 stacks
+ ///
+ ///
+ ///
+ ///
+ public void StoneBurst(float level, ICombatEntity caster, ICombatEntity target)
+ {
+ BurstTarget(level, caster, target);
+ BurstSplash(level, caster, target);
+ }
+
+ ///
+ /// Handles the burst on the primary target
+ ///
+ ///
+ ///
+ ///
+ private void BurstTarget(float level, ICombatEntity caster, ICombatEntity target)
+ {
+ // Animation is unknown
+ Send.ZC_NORMAL.PlayEffect(target, "F_archer_explosiontrap_hit_explosion", 1);
+
+ // Always stuns primary target?
+ target.StartBuff(BuffId.Stun, level, 0, TimeSpan.FromSeconds(StunDuration), caster);
+ }
+
+ ///
+ /// Handles the burst effect on nearby targets.
+ ///
+ ///
+ ///
+ ///
+ private void BurstSplash(float level, ICombatEntity caster, ICombatEntity target)
+ {
+ // Range is unknown
+ var splashArea = new Circle(target.Position, 50);
+
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+
+ // Exclude main target from potential splash targets
+ targets.Remove(target);
+
+ var burstTargets = targets.LimitRandom(6);
+
+ foreach (var burstTarget in burstTargets)
+ {
+ if (RandomProvider.Get().Next(100) < 25 + 5 * level)
+ {
+ burstTarget.StartBuff(BuffId.Stun, level, 0, TimeSpan.FromSeconds(StunDuration), caster);
+ }
+ }
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_Teardown.cs b/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_Teardown.cs
new file mode 100644
index 000000000..b864cea9c
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Archers/QuarrelShooter/QuarrelShooter_Teardown.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading.Tasks;
+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.Characters.Components;
+using Melia.Zone.World.Actors.Monsters;
+using Yggdrasil.Logging;
+using Yggdrasil.Util;
+using static Melia.Shared.Util.TaskHelper;
+using static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Archers.QuarrelShooter
+{
+ ///
+ /// Handles the Quarrel Shooter skill Teardown
+ ///
+ [SkillHandler(SkillId.QuarrelShooter_Teardown)]
+ public class QuarrelShooter_Teardown : IGroundSkillHandler
+ {
+ // This specifies the valid targets for this skill
+ // They differ between friendly and enemy targets
+ // and QuarrelShooter7 allows extra targets to be hit
+ int[] EnemyTargetIds = [47452, 58283, 58284, 58285, 58287, 57709, 57417, 46013, 58288, 800032, 800033, 800034, 800035, 800036, 800037, 800038, 800039, 800040, 800041];
+ int[] QuarrelShooter7EnemyIds = [58282];
+ int[] FriendlyTargetIds = [47452, 57417, 46013];
+ private const float maxRange = 50f;
+
+ ///
+ /// Handles skill, insta-killing designated 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;
+ }
+
+ Send.ZC_SKILL_READY(caster, skill, originPos, farPos);
+ Send.ZC_SKILL_MELEE_GROUND(caster, skill, farPos, null);
+
+ if (target == null)
+ {
+ caster.ServerMessage(Localization.Get("No target."));
+ return;
+ }
+
+ if (target is Mob mob && (caster.CanAttack(mob) && EnemyTargetIds.Contains(mob.Data.Id) || caster.CanAttack(mob) && caster.IsAbilityActive(AbilityId.QuarrelShooter7) && QuarrelShooter7EnemyIds.Contains(mob.Data.Id) || FriendlyTargetIds.Contains(mob.Data.Id)))
+ {
+ if (caster.Position.Get2DDistance(target.Position) > maxRange)
+ {
+ caster.ServerMessage(Localization.Get("Too far away."));
+ return;
+ }
+
+ skill.IncreaseOverheat();
+ caster.TurnTowards(target);
+ caster.SetAttackState(true);
+
+ CallSafe(this.Attack(skill, caster, mob));
+ }
+ else
+ {
+ caster.ServerMessage(Localization.Get("Invalid Target."));
+ return;
+ }
+ }
+
+ ///
+ /// Destroys the target
+ ///
+ ///
+ ///
+ private async Task Attack(Skill skill, ICombatEntity caster, Mob target)
+ {
+ var hitDelay = TimeSpan.FromMilliseconds(300);
+
+ await Task.Delay(hitDelay);
+
+ // TODO: Probably missing a packet here to display the death animation
+ target.Kill(caster);
+
+ // TODO: QuarrelShooter31 affects the cooldown time of skills
+ // if you teardown an ally installation the cooldown decreases
+ // while it increases for the enemy if the installation was hostile
+ // We currently don't have a good way to track this
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Common/TargetSkill.cs b/src/ZoneServer/Skills/Handlers/Common/TargetSkill.cs
index 804a8de6e..0b25e8d87 100644
--- a/src/ZoneServer/Skills/Handlers/Common/TargetSkill.cs
+++ b/src/ZoneServer/Skills/Handlers/Common/TargetSkill.cs
@@ -13,7 +13,7 @@ namespace Melia.Zone.Skills.Handlers.Common
///
/// Handles ranged skills that target a single entity.
///
- [SkillHandler(SkillId.Bow_Attack, SkillId.Magic_Attack, SkillId.Pistol_Attack)]
+ [SkillHandler(SkillId.Bow_Attack, SkillId.CrossBow_Attack, SkillId.Magic_Attack, SkillId.Pistol_Attack)]
public class TargetSkill : ITargetSkillHandler
{
private const int DoubleAttackRate = 40;
diff --git a/src/ZoneServer/World/Actors/Characters/Components/InventoryComponent.cs b/src/ZoneServer/World/Actors/Characters/Components/InventoryComponent.cs
index ae06e9d82..02ada4257 100644
--- a/src/ZoneServer/World/Actors/Characters/Components/InventoryComponent.cs
+++ b/src/ZoneServer/World/Actors/Characters/Components/InventoryComponent.cs
@@ -456,6 +456,10 @@ public InventoryResult Equip(EquipSlot slot, long worldId)
if (item == null)
return InventoryResult.ItemNotFound;
+ // Check if we are prevented from equipping this item
+ if (item.Data.EquipType1 == EquipType.Shield && this.Character.IsBuffActive(BuffId.Common_ShieldDesrption))
+ return InventoryResult.InvalidOperation;
+
// Unequip existing item first.
var collision = false;
lock (_syncLock)
diff --git a/system/db/features.txt b/system/db/features.txt
index c0662ace5..228835725 100644
--- a/system/db/features.txt
+++ b/system/db/features.txt
@@ -106,6 +106,9 @@
// Changes Hasisas to not base crit bonus on HP lost
{ name: "HasisasNoHpCritBonus", enabled: true },
+ // Removes Stone Bullet requirement for Stone Shot
+ { name: "StoneShotNoAmmo", enabled: true },
+
// Changes Pierce to not base its hit count on target size
{ name: "PierceNoSizeEffect", enabled: true },
diff --git a/system/scripts/zone/core/calc_combat.cs b/system/scripts/zone/core/calc_combat.cs
index 412ebbcd0..cf9011ef1 100644
--- a/system/scripts/zone/core/calc_combat.cs
+++ b/system/scripts/zone/core/calc_combat.cs
@@ -119,7 +119,7 @@ public float SCR_CalculateDamage(ICombatEntity attacker, ICombatEntity target, S
// Apply the skill factor, raising the damage based on the skill's
// damage multiplier
- var skillFactor = skill.Properties.GetFloat(PropertyName.SkillFactor);
+ var skillFactor = skill.Properties.GetFloat(PropertyName.SkillFactor) + modifier.BonusFactor;
skillHitResult.Damage *= skillFactor / 100f;
// Block needs to be calculated before criticals happen,