diff --git a/src/Shared/Network/NormalOp.cs b/src/Shared/Network/NormalOp.cs
index 9f3d67af2..83e9c83fd 100644
--- a/src/Shared/Network/NormalOp.cs
+++ b/src/Shared/Network/NormalOp.cs
@@ -53,6 +53,7 @@ public static class Zone
public const int Cutscene = 0x6B;
public const int SetSkillSpeed = 0x77;
public const int SetHitDelay = 0x78;
+ public const int UpdateNormalAttackSkill = 0x87;
public const int SpinObject = 0x8A;
public const int Unknown_A1 = 0xA1;
public const int LeapJump = 0xC2;
diff --git a/src/ZoneServer/Buffs/Buff.cs b/src/ZoneServer/Buffs/Buff.cs
index 59d9fa5d4..c74233d34 100644
--- a/src/ZoneServer/Buffs/Buff.cs
+++ b/src/ZoneServer/Buffs/Buff.cs
@@ -3,6 +3,7 @@
using Melia.Shared.Game.Const;
using Melia.Zone.Buffs.Base;
using Melia.Zone.World.Actors;
+using Melia.Zone.World.Actors.CombatEntities.Components;
using Yggdrasil.Scheduling;
using Yggdrasil.Util;
@@ -168,7 +169,8 @@ public int OverbuffCounter
///
///
/// Id of the skill associated with this buff.
- public Buff(BuffId buffId, float numArg1, float numArg2, TimeSpan duration, TimeSpan runTime, ICombatEntity target, ICombatEntity caster, SkillId skillId)
+ /// OverBuff count, the quantity of stacking buffs
+ public Buff(BuffId buffId, float numArg1, float numArg2, TimeSpan duration, TimeSpan runTime, ICombatEntity target, ICombatEntity caster, SkillId skillId, int overBuffCount = 1)
{
this.Id = buffId;
this.NumArg1 = numArg1;
@@ -209,6 +211,8 @@ public Buff(BuffId buffId, float numArg1, float numArg2, TimeSpan duration, Time
if (this.HasUpdateTime)
this.NextUpdateTime = DateTime.Now.Add(this.UpdateTime);
+
+ this.OverbuffCounter = overBuffCount;
}
///
@@ -220,6 +224,15 @@ public void IncreaseOverbuff()
this.OverbuffCounter++;
}
+ ///
+ /// Update overbuff counter for a given value, capped to the buff's max overbuff
+ /// value.
+ ///
+ public void UpdateOverbuff(int value)
+ {
+ this.OverbuffCounter += value;
+ }
+
///
/// Extends the buff's duration and executes the buff handler's start
/// behavior. Does not add the buff to the actor.
@@ -251,6 +264,14 @@ internal void End()
this.Handler?.OnEnd(this);
}
+ ///
+ /// Removes/Ends the Buff
+ ///
+ internal void Stop()
+ {
+ this.Target.Components.Get()?.Stop(this.Id);
+ }
+
///
/// Updates the buff and handles effects that happen while the buff
/// is active.
diff --git a/src/ZoneServer/Buffs/Handlers/Common/Freeze.cs b/src/ZoneServer/Buffs/Handlers/Common/Freeze.cs
new file mode 100644
index 000000000..33c2e0b04
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Common/Freeze.cs
@@ -0,0 +1,39 @@
+using System;
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Network;
+using Melia.Zone.World.Actors.CombatEntities.Components;
+
+namespace Melia.Zone.Buffs.Handlers.Common
+{
+ ///
+ /// Handler for Freeze, which applies hold on the target
+ ///
+ [BuffHandler(BuffId.Freeze)]
+ public class Freeze : BuffHandler
+ {
+ ///
+ /// Starts buff
+ ///
+ ///
+ public override void OnStart(Buff buff)
+ {
+ var target = buff.Target;
+
+ if (target.Components.TryGet(out var movementComponent))
+ movementComponent.ApplyHold();
+ }
+
+ ///
+ /// Ends the buff
+ ///
+ ///
+ public override void OnEnd(Buff buff)
+ {
+ var target = buff.Target;
+
+ if (target.Components.TryGet(out var movementComponent))
+ movementComponent.ReleaseHold();
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/BulletMaker/DoubleGun_Attack.cs b/src/ZoneServer/Buffs/Handlers/Scouts/BulletMaker/DoubleGun_Attack.cs
new file mode 100644
index 000000000..2eeb72d86
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/BulletMaker/DoubleGun_Attack.cs
@@ -0,0 +1,39 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Network;
+using Melia.Zone.World.Actors.Characters;
+
+namespace Melia.Zone.Buffs.Handlers.Scouts.BulletMaker
+{
+ ///
+ /// Handle for the Double Gun Stance Buff, enables movement while attacking
+ ///
+ [BuffHandler(BuffId.DoubleGunStance_Buff)]
+ public class DoubleGunStance_Buff : BuffHandler
+ {
+ public override void OnStart(Buff buff)
+ {
+ AddPropertyModifier(buff, buff.Target, PropertyName.MovingShot_BM, this.GetMovingShotBonus(buff));
+
+ buff.Target.Properties.Invalidate(PropertyName.MovingShotable);
+
+ if (buff.Target is Character character)
+ Send.ZC_MOVE_SPEED(character);
+ }
+
+ public override void OnEnd(Buff buff)
+ {
+ RemovePropertyModifier(buff, buff.Target, PropertyName.MovingShot_BM);
+
+ buff.Target.Properties.Invalidate(PropertyName.MovingShotable);
+
+ if (buff.Target is Character character)
+ Send.ZC_MOVE_SPEED(character);
+ }
+
+ private float GetMovingShotBonus(Buff buff)
+ {
+ return 0.8f;
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/BulletMaker/FreezeBullet_Cold_Debuff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/BulletMaker/FreezeBullet_Cold_Debuff.cs
new file mode 100644
index 000000000..88b97ae67
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/BulletMaker/FreezeBullet_Cold_Debuff.cs
@@ -0,0 +1,41 @@
+using System;
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Network;
+using Melia.Zone.World.Actors;
+
+namespace Melia.Zone.Buffs.Handlers.Scouts.BulletMaker
+{
+ ///
+ /// Handle for the FreezeBullet Cold Debuff, which freezes the enemy for 2 seconds upon reaching 4 stacks
+ ///
+ [BuffHandler(BuffId.FreezeBullet_Cold_Debuff)]
+ public class FreezeBullet_Cold_Debuff : BuffHandler
+ {
+ public override void OnStart(Buff buff)
+ {
+ if (!buff.Vars.GetBool("Slow_FreezeBullet_Cold_Debuff"))
+ {
+ var reduceMspd = buff.Target.Properties.GetFloat(PropertyName.MSPD) * 0.5f;
+
+ AddPropertyModifier(buff, buff.Target, PropertyName.MSPD_BM, -reduceMspd);
+ Send.ZC_MSPD(buff.Target);
+
+ buff.Vars.SetBool("Slow_FreezeBullet_Cold_Debuff", true);
+ }
+
+ if (buff.OverbuffCounter >= 4)
+ {
+ buff.Target.StartBuff(BuffId.Freeze, 0f, 0f, TimeSpan.FromSeconds(2), buff.Caster);
+ buff.Stop();
+ }
+ }
+
+ public override void OnEnd(Buff buff)
+ {
+ RemovePropertyModifier(buff, buff.Target, PropertyName.MSPD_BM);
+ Send.ZC_MSPD(buff.Target);
+ buff.Vars.SetBool("Slow_FreezeBullet_Cold_Debuff", false);
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/BulletMaker/Tase_Debuff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/BulletMaker/Tase_Debuff.cs
new file mode 100644
index 000000000..e83fed3b4
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/BulletMaker/Tase_Debuff.cs
@@ -0,0 +1,22 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+
+namespace Melia.Zone.Buffs.Handlers.Scouts.BulletMaker
+{
+ ///
+ /// Handle for the Tase Debuff, which makes the target receive additional Lightning Property damage when hit
+ ///
+ [BuffHandler(BuffId.Tase_Debuff)]
+ public class Tase_Debuff : BuffHandler
+ {
+ public override void OnStart(Buff buff)
+ {
+ // @TODO: Reduce the Lightning property resistance of the target
+ }
+
+ public override void OnEnd(Buff buff)
+ {
+ // @TODO: Increase the Lightning property resistance of the target
+ }
+ }
+}
diff --git a/src/ZoneServer/Network/Send.Normal.cs b/src/ZoneServer/Network/Send.Normal.cs
index 753834aa2..32bdb8d41 100644
--- a/src/ZoneServer/Network/Send.Normal.cs
+++ b/src/ZoneServer/Network/Send.Normal.cs
@@ -1320,6 +1320,37 @@ public static void UpdateCollection(Character character, int collectionId, int i
character.Connection.Send(packet);
}
+
+ ///
+ /// Updates the character normal attack stance attack
+ ///
+ ///
+ ///
+ public static void UpdateNormalAttackSkill(ICombatEntity entity, SkillId skillId)
+ {
+ var packet = new Packet(Op.ZC_NORMAL);
+ packet.PutInt(NormalOp.Zone.UpdateNormalAttackSkill);
+
+ packet.PutInt(entity.Handle);
+ packet.PutInt((int)skillId);
+
+ entity.Map.Broadcast(packet, entity);
+ }
+
+ ///
+ /// Purpose unknown. Seems to enable smooth movement while normal attacking.
+ ///
+ ///
+ public static void Skill_45(ICombatEntity entity)
+ {
+ var packet = new Packet(Op.ZC_NORMAL);
+ packet.PutInt(NormalOp.Zone.Skill_45);
+
+ packet.PutInt(entity.Handle);
+ packet.PutByte(0);
+
+ entity.Map.Broadcast(packet, entity);
+ }
}
}
}
diff --git a/src/ZoneServer/Pads/Handlers/Scout/Ardito/Arditi_TreGranata_DamagePad.cs b/src/ZoneServer/Pads/Handlers/Scout/Ardito/Arditi_TreGranata_DamagePad.cs
index 9f946b2b2..14dec455d 100644
--- a/src/ZoneServer/Pads/Handlers/Scout/Ardito/Arditi_TreGranata_DamagePad.cs
+++ b/src/ZoneServer/Pads/Handlers/Scout/Ardito/Arditi_TreGranata_DamagePad.cs
@@ -1,5 +1,4 @@
using Melia.Zone.Network;
-using Melia.Zone.Skills;
using Melia.Zone.World.Actors.Monsters;
namespace Melia.Zone.Pads.Handlers.Scout.Ardito
diff --git a/src/ZoneServer/Pads/Handlers/Scout/BulletMaker/Bulletmarker_FreezeBullet.cs b/src/ZoneServer/Pads/Handlers/Scout/BulletMaker/Bulletmarker_FreezeBullet.cs
new file mode 100644
index 000000000..ae67b0959
--- /dev/null
+++ b/src/ZoneServer/Pads/Handlers/Scout/BulletMaker/Bulletmarker_FreezeBullet.cs
@@ -0,0 +1,45 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Network;
+using Melia.Zone.World.Actors;
+using Melia.Zone.World.Actors.Monsters;
+
+namespace Melia.Zone.Pads.Handlers.Scout.Ardito
+{
+ ///
+ /// Handler for the Bullet Marker Freeze Bullet pad, creates and disables the effect
+ ///
+ [PadHandler(PadName.Bulletmarker_FreezeBullet)]
+ public class Bulletmarker_FreezeBullet : ICreatePadHandler, IDestroyPadHandler
+ {
+ ///
+ /// Called when the pad is created.
+ ///
+ ///
+ ///
+ public void Created(object sender, PadTriggerArgs args)
+ {
+ var pad = args.Trigger;
+ var creator = args.Creator;
+
+ Send.ZC_NORMAL.PadUpdate(creator, pad, PadName.Bulletmarker_FreezeBullet, 0, 72.30239f, 50, true);
+ }
+
+ ///
+ /// Called when the pad is destroyed.
+ ///
+ ///
+ ///
+ public void Destroyed(object sender, PadTriggerArgs args)
+ {
+ var pad = args.Trigger;
+ var creator = args.Creator;
+
+ if (creator.TryGetSkill(SkillId.Bulletmarker_FreezeBullet, out var freezeSkill))
+ {
+ freezeSkill.Vars.SetBool("Pad_" + PadName.Bulletmarker_FreezeBullet, false);
+ }
+
+ Send.ZC_NORMAL.PadUpdate(creator, pad, PadName.Bulletmarker_FreezeBullet, 0, 72.30239f, 50, false);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Common/TargetSkill.cs b/src/ZoneServer/Skills/Handlers/Common/TargetSkill.cs
index 804a8de6e..5b73a726e 100644
--- a/src/ZoneServer/Skills/Handlers/Common/TargetSkill.cs
+++ b/src/ZoneServer/Skills/Handlers/Common/TargetSkill.cs
@@ -1,10 +1,14 @@
using System;
using Melia.Shared.Game.Const;
using Melia.Shared.L10N;
+using Melia.Shared.World;
using Melia.Zone.Network;
using Melia.Zone.Skills.Combat;
using Melia.Zone.Skills.Handlers.Base;
+using Melia.Zone.Skills.SplashAreas;
using Melia.Zone.World.Actors;
+using Melia.Zone.World.Actors.Monsters;
+using Melia.Zone.World.Actors.Pads;
using Yggdrasil.Util;
using static Melia.Zone.Skills.SkillUseFunctions;
@@ -13,7 +17,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.Magic_Attack, SkillId.Pistol_Attack, SkillId.DoubleGun_Attack)]
public class TargetSkill : ITargetSkillHandler
{
private const int DoubleAttackRate = 40;
@@ -32,13 +36,12 @@ public void Handle(Skill skill, ICombatEntity caster, ICombatEntity target)
return;
}
+ Send.ZC_NORMAL.Skill_45(caster);
+
skill.IncreaseOverheat();
caster.TurnTowards(target);
caster.SetAttackState(true);
- //Send.ZC_SKILL_READY(caster, skill, target.Position, Position.Zero);
- //Send.ZC_NORMAL.Unkown_1c(caster, target.Handle, target.Position, caster.Position.GetDirection(target.Position), Position.Zero);
-
if (target == null)
{
Send.ZC_SKILL_FORCE_TARGET(caster, null, skill, null);
@@ -50,11 +53,57 @@ public void Handle(Skill skill, ICombatEntity caster, ICombatEntity target)
var modifier = SkillModifier.Default;
- // Random chance to trigger double hit with pistol while buff is active
- if (skill.Id == SkillId.Pistol_Attack && caster.IsBuffActive(BuffId.DoubleAttack_Buff))
+ Send.ZC_SKILL_READY(caster, skill, caster.Position, target.Position);
+ Send.ZC_NORMAL.UpdateSkillEffect(caster, 0, caster.Position, caster.Direction, Position.Zero);
+
+ if (skill.Id == SkillId.Pistol_Attack)
+ {
+ // Random chance to trigger double hit with pistol while buff is active
+ if (caster.IsBuffActive(BuffId.DoubleAttack_Buff) && RandomProvider.Get().Next(100) < DoubleAttackRate)
+ {
+ modifier.HitCount = 2;
+ }
+ }
+ else if (skill.Id == SkillId.DoubleGun_Attack)
{
- if (RandomProvider.Get().Next(100) < DoubleAttackRate)
+ if (caster.IsBuffActive(BuffId.DoubleGunStance_Buff))
+ {
+ // Increase by one the stack count for Overheating buff
+ if (!caster.IsBuffActive(BuffId.Outrage_Buff))
+ caster.StartBuff(BuffId.Overheating_Buff, TimeSpan.FromSeconds(35));
+
modifier.HitCount = 2;
+ }
+ }
+
+ if (skill.Id == SkillId.DoubleGun_Attack || skill.Id == SkillId.Pistol_Attack)
+ {
+ if (caster.IsBuffActive(BuffId.FreezeBullet_Buff) && RandomProvider.Get().Next(100) < 30)
+ {
+ target.StartBuff(BuffId.Freeze, TimeSpan.FromSeconds(3));
+
+ //[Arts] Freeze Bullet: Fog
+ if (caster.IsAbilityActive(AbilityId.Bulletmarker16))
+ {
+ // Only one Pad will be created
+ if (caster.TryGetSkill(SkillId.Bulletmarker_FreezeBullet, out var freezeSkill) && !freezeSkill.Vars.GetBool("Pad_" + PadName.Bulletmarker_FreezeBullet, false))
+ {
+ freezeSkill.Vars.SetBool("Pad_" + PadName.Bulletmarker_FreezeBullet, true);
+
+ var pad = new Pad(PadName.Bulletmarker_FreezeBullet, caster, freezeSkill, new Circle(target.Position, 45));
+
+ pad.Position = target.Position;
+ pad.Trigger.LifeTime = TimeSpan.FromSeconds(8);
+ pad.Trigger.UpdateInterval = TimeSpan.FromSeconds(1);
+ pad.Trigger.MaxActorCount = 10;
+
+ pad.Trigger.Subscribe(TriggerType.Update, this.OnFreezePadTriggerUpdate);
+
+ caster.Map.AddPad(pad);
+ }
+ }
+ }
+
}
var skillHitResult = SCR_SkillHit(caster, target, skill, modifier);
@@ -65,5 +114,24 @@ public void Handle(Skill skill, ICombatEntity caster, ICombatEntity target)
Send.ZC_SKILL_FORCE_TARGET(caster, target, skill, skillHit);
}
+
+ ///
+ /// Called when an actor enters the area of the freeze pad.
+ ///
+ ///
+ ///
+ private void OnFreezePadTriggerUpdate(object sender, PadTriggerArgs args)
+ {
+ var pad = args.Trigger;
+ var caster = args.Creator;
+ var skill = args.Skill;
+
+ var targets = pad.Trigger.GetAttackableEntities(caster);
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ target.StartBuff(BuffId.FreezeBullet_Cold_Debuff, TimeSpan.FromSeconds(2));
+ }
+ }
}
}
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_BloodyOverdrive.cs b/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_BloodyOverdrive.cs
new file mode 100644
index 000000000..86eec7026
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_BloodyOverdrive.cs
@@ -0,0 +1,193 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Yggdrasil.Util;
+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 static Melia.Shared.Util.TaskHelper;
+using static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Scouts.BulletMaker
+{
+ ///
+ /// Handles the Bullet Maker's skill Bloody Overdrive.
+ ///
+ [SkillHandler(SkillId.Bulletmarker_BloodyOverdrive)]
+ public class Bulletmarker_BloodyOverdrive : IGroundSkillHandler
+ {
+ ///
+ /// Handles the skill, shots the pistol around damaging nearby enemies
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target)
+ {
+ if (!caster.IsBuffActive(BuffId.DoubleGunStance_Buff))
+ return;
+
+ if (!caster.TrySpendSp(skill))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+
+ // Bloody Overdrive: Ricochet
+ if (caster.IsAbilityActive(AbilityId.Bulletmarker8))
+ {
+ // Increase SP Consumption by 30%
+ var spendSp = skill.Properties.GetFloat(PropertyName.SpendSP) * 0.3f;
+ if (!caster.TrySpendSp(spendSp))
+ return;
+ }
+
+ // Bloody Overdrive: Invincible
+ if (caster.IsAbilityActive(AbilityId.Bulletmarker12))
+ {
+ // Increase SP Consumption by 30%
+ var spendSp = skill.Properties.GetFloat(PropertyName.SpendSP) * 0.3f;
+ if (!caster.TrySpendSp(spendSp))
+ return;
+ }
+
+ skill.IncreaseOverheat();
+ caster.TurnTowards(target);
+ caster.SetAttackState(true);
+
+ Send.ZC_SKILL_READY(caster, skill, caster.Position, caster.Position);
+ Send.ZC_NORMAL.UpdateSkillEffect(caster, caster.Handle, caster.Position, caster.Direction, Position.Zero);
+
+ // Increase by one the stack count for Overheating buff
+ if (!caster.IsBuffActive(BuffId.Outrage_Buff))
+ caster.StartBuff(BuffId.Overheating_Buff, TimeSpan.FromSeconds(35));
+
+ // @TODO: Can't be knockdown back/down while casting the skill
+ caster.StartBuff(BuffId.Skill_SuperArmor_Buff, TimeSpan.FromSeconds(1));
+
+ // Bloody Overdrive: Invincible
+ if (caster.IsAbilityActive(AbilityId.Bulletmarker12))
+ {
+ // Increase SP Consumption by 30%
+ var spendSp = skill.Properties.GetFloat(PropertyName.SpendSP) * 0.3f;
+ if (caster.TrySpendSp(spendSp))
+ return;
+
+ caster.StartBuff(BuffId.Skill_NoDamage_Buff, TimeSpan.FromSeconds(1));
+ }
+
+ if (caster.TryGetBuff(BuffId.Outrage_Buff, out var outrageBuff) && outrageBuff.OverbuffCounter > 0)
+ {
+ caster.Components.Get().Overbuff(outrageBuff, -1);
+ }
+
+ CallSafe(this.Attack(skill, caster));
+ }
+
+ ///
+ /// Execute the attack to nearby enemies wihtin a delay
+ ///
+ ///
+ ///
+ ///
+ private async Task Attack(Skill skill, ICombatEntity caster)
+ {
+ var splashArea = new Circle(caster.Position, 80);
+
+ var tagets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+ var indexHelper = 0;
+ var rnd = RandomProvider.Get();
+
+ Send.ZC_SKILL_MELEE_GROUND(caster, skill, caster.Position, null);
+
+ for (int i = 0; i < 4; i++)
+ {
+ var skillHits = new List();
+
+ foreach (var hitTarget in tagets.LimitBySDR(caster, skill))
+ {
+ this.AddSkillHitInfo(caster, hitTarget, skill, skillHits, indexHelper);
+ }
+
+ Send.ZC_SKILL_HIT_INFO(caster, skillHits);
+
+ await Task.Delay(TimeSpan.FromMilliseconds(400));
+ }
+
+ // Bloody Overdrive: Ricochet
+ if (caster.IsAbilityActive(AbilityId.Bulletmarker8))
+ {
+ foreach (var hitTarget in tagets.LimitBySDR(caster, skill))
+ {
+ if (this.TryGetRicochetTarget(caster, hitTarget, skill, out var ricochetTarget))
+ {
+ var skillHitResult = SCR_SkillHit(caster, ricochetTarget, skill);
+ ricochetTarget.TakeDamage(skillHitResult.Damage, caster);
+
+ var hit = new HitInfo(caster, hitTarget, skill, skillHitResult);
+ hit.ForceId = ForceId.GetNew();
+ hit.ResultType = HitResultType.Hit;
+
+ Send.ZC_HIT_INFO(caster, ricochetTarget, hit);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Adds a new HitInfo to the processing list
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private void AddSkillHitInfo(ICombatEntity caster, ICombatEntity target, Skill skill, List skillHits, int indexHelper)
+ {
+ indexHelper++;
+ var modifier = SkillModifier.Default;
+ modifier.HitCount = 2;
+
+ var skillHitResult = SCR_SkillHit(caster, target, skill, modifier);
+
+ target.TakeDamage(skillHitResult.Damage, caster);
+
+ var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, TimeSpan.FromMilliseconds(100 * indexHelper), TimeSpan.Zero);
+
+ skillHits.Add(skillHit);
+ }
+
+ ///
+ /// Returns the closest target to the main target to ricochet the attack off to.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private bool TryGetRicochetTarget(ICombatEntity caster, ICombatEntity mainTarget, Skill skill, out ICombatEntity ricochetTarget)
+ {
+ var splashPos = caster.Position;
+ var splashRadius = 50;
+ var splashArea = new Circle(mainTarget.Position, splashRadius);
+
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+ if (!targets.Any())
+ {
+ ricochetTarget = null;
+ return false;
+ }
+
+ ricochetTarget = targets.GetClosest(mainTarget.Position, a => a != mainTarget);
+ return ricochetTarget != null;
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_DoubleGunStance.cs b/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_DoubleGunStance.cs
new file mode 100644
index 000000000..cbf6c1251
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_DoubleGunStance.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;
+
+namespace Melia.Zone.Skills.Handlers.Scouts.BulletMaker
+{
+ ///
+ /// Handles the Bullet Maker's skill Double Gun Stance.
+ ///
+ [SkillHandler(SkillId.Bulletmarker_DoubleGunStance)]
+ public class Bulletmarker_DoubleGunStance : ISelfSkillHandler
+ {
+ ///
+ /// Handles the skill start the Double Gun Stance buff
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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);
+
+ if (caster is Character casterCharacter)
+ {
+ var rightHand = casterCharacter.Inventory.GetItem(EquipSlot.RightHand);
+ if (rightHand == null || rightHand.Data.EquipType1 != EquipType.Pistol)
+ return;
+ }
+
+ if (caster.IsBuffActive(BuffId.DoubleGunStance_Buff))
+ {
+ Send.ZC_NORMAL.UpdateNormalAttackSkill(caster, SkillId.Pistol_Attack);
+ caster.StopBuff(BuffId.DoubleGunStance_Buff);
+ }
+ else
+ {
+ Send.ZC_NORMAL.UpdateNormalAttackSkill(caster, SkillId.DoubleGun_Attack);
+ caster.StartBuff(BuffId.DoubleGunStance_Buff, 1, 0, TimeSpan.Zero, caster);
+ }
+
+ Send.ZC_SKILL_READY(caster, skill, originPos, originPos);
+ Send.ZC_NORMAL.UpdateSkillEffect(caster, caster.Handle, originPos, caster.Direction, Position.Zero);
+ Send.ZC_SKILL_MELEE_TARGET(caster, skill, caster, null);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_FreezeBullet.cs b/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_FreezeBullet.cs
new file mode 100644
index 000000000..3ab8e3135
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_FreezeBullet.cs
@@ -0,0 +1,51 @@
+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.BulletMaker
+{
+ ///
+ /// Handles the Bullet Maker's skill Freeze Bullet.
+ ///
+ [SkillHandler(SkillId.Bulletmarker_FreezeBullet)]
+ public class Bulletmarker_FreezeBullet : ISelfSkillHandler
+ {
+ ///
+ /// Handles the skill, applies a buff to self
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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);
+
+ caster.StartBuff(BuffId.FreezeBullet_Buff, 1, 0, this.GetBuffDuration(skill), caster);
+
+ Send.ZC_SKILL_READY(caster, skill, originPos, originPos);
+ Send.ZC_NORMAL.UpdateSkillEffect(caster, caster.Handle, originPos, caster.Direction, Position.Zero);
+ Send.ZC_SKILL_MELEE_TARGET(caster, skill, caster, null);
+ }
+
+ ///
+ /// Returns the FreezeBullet Buff duration
+ ///
+ ///
+ private TimeSpan GetBuffDuration(Skill skill)
+ {
+ return TimeSpan.FromSeconds(15 + skill.Level);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_MozambiqueDrill.cs b/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_MozambiqueDrill.cs
new file mode 100644
index 000000000..33b458c3d
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_MozambiqueDrill.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Linq;
+using Melia.Shared.Data;
+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 static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Scouts.BulletMaker
+{
+ ///
+ /// Handles the Bullet Maker's skill Mozambique Drill.
+ ///
+ [SkillHandler(SkillId.Bulletmarker_MozambiqueDrill)]
+ public class Bulletmarker_MozambiqueDrill : ITargetSkillHandler
+ {
+ ///
+ /// Handles the skill, shoots with the pistol at the target enemy.
+ ///
+ ///
+ ///
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, ICombatEntity target)
+ {
+ if (!caster.TrySpendSp(skill))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+
+ // Mozambique Drill: Ricochet
+ if (caster.IsAbilityActive(AbilityId.Bulletmarker10))
+ {
+ // Increase SP Consumption by 30%
+ var spendSp = skill.Properties.GetFloat(PropertyName.SpendSP) * 0.3f;
+ if (!caster.TrySpendSp(spendSp))
+ return;
+ }
+
+ // Mozambique Drill: Ignore Defense
+ if (caster.IsAbilityActive(AbilityId.Bulletmarker9))
+ {
+ // Increase SP Consumption by 30%
+ var spendSp = skill.Properties.GetFloat(PropertyName.SpendSP) * 0.3f;
+ if (!caster.TrySpendSp(spendSp))
+ return;
+ }
+
+ if (!caster.IsBuffActive(BuffId.DoubleGunStance_Buff))
+ 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;
+ }
+
+ Send.ZC_SKILL_READY(caster, skill, caster.Position, target.Position);
+ Send.ZC_NORMAL.UpdateSkillEffect(caster, caster.Handle, caster.Position, caster.Direction, Position.Zero);
+
+ // Increase by one the stack count for Overheating buff
+ if (!caster.IsBuffActive(BuffId.Outrage_Buff))
+ caster.StartBuff(BuffId.Overheating_Buff, TimeSpan.FromSeconds(35));
+
+ var skillHit = this.GetSkillHitInfo(caster, target, skill);
+ Send.ZC_SKILL_FORCE_TARGET(caster, target, skill, skillHit);
+
+ // Mozambique Drill: Ricochet
+ if (caster.IsAbilityActive(AbilityId.Bulletmarker10))
+ {
+ if (this.TryGetRicochetTarget(caster, target, skill, out var ricochetTarget))
+ {
+ var skillHitResult = SCR_SkillHit(caster, ricochetTarget, skill);
+ ricochetTarget.TakeDamage(skillHitResult.Damage, caster);
+
+ var hit = new HitInfo(caster, target, skill, skillHitResult);
+ hit.ForceId = ForceId.GetNew();
+ hit.ResultType = HitResultType.Hit;
+
+ Send.ZC_HIT_INFO(caster, ricochetTarget, hit);
+ }
+ }
+ }
+
+ ///
+ /// Get a new HitInfo
+ ///
+ ///
+ ///
+ ///
+ ///
+ private SkillHitInfo GetSkillHitInfo(ICombatEntity caster, ICombatEntity target, Skill skill)
+ {
+ var damageDelay = TimeSpan.FromMilliseconds(328);
+ var skillHitDelay = TimeSpan.FromMilliseconds(100);
+ var modifier = SkillModifier.Default;
+ modifier.HitCount = 2;
+
+ if (caster.IsAbilityActive(AbilityId.Bulletmarker9))
+ modifier.DefensePenetrationRate = this.GetIgnoreDefenseRatio(caster);
+
+ var skillHitResult = SCR_SkillHit(caster, target, skill, modifier);
+
+ if (caster.TryGetBuff(BuffId.Outrage_Buff, out var outrageBuff))
+ {
+ if (outrageBuff.OverbuffCounter > 0)
+ {
+ caster.Components.Get().Overbuff(outrageBuff, -1);
+ // Increase the HitCount by one
+ modifier.HitCount += 1;
+
+ skillHitResult = SCR_SkillHit(caster, target, skill, modifier);
+ // Decreases the Damage by 22.5%
+ skillHitResult.Damage *= 0.775f;
+ target.TakeDamage(skillHitResult.Damage, caster);
+
+ return new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay);
+ }
+ }
+
+ target.TakeDamage(skillHitResult.Damage, caster);
+
+ return new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay);
+ }
+
+ ///
+ /// Returns the closest target to the main target to ricochet the attack off to.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private bool TryGetRicochetTarget(ICombatEntity caster, ICombatEntity mainTarget, Skill skill, out ICombatEntity ricochetTarget)
+ {
+ var splashPos = caster.Position;
+ var splashRadius = 50;
+ var splashArea = new Circle(mainTarget.Position, splashRadius);
+
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+ if (!targets.Any())
+ {
+ ricochetTarget = null;
+ return false;
+ }
+
+ ricochetTarget = targets.GetClosest(mainTarget.Position, a => a != mainTarget);
+ return ricochetTarget != null;
+ }
+
+ ///
+ /// Returns the Ignore Defense Ratio once 'Mozambique Drill: Ignore Defense' is enabled
+ ///
+ ///
+ ///
+ private float GetIgnoreDefenseRatio(ICombatEntity caster)
+ {
+ if (caster.TryGetAbility(AbilityId.Bulletmarker9, out var ability))
+ {
+ return (ability.Level * 2) / 2;
+ }
+
+ return 0;
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_NapalmBullet.cs b/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_NapalmBullet.cs
new file mode 100644
index 000000000..223b95c1a
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_NapalmBullet.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+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 static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Scouts.BulletMaker
+{
+ ///
+ /// Handles the Bullet Maker's skill Napalm Bullet.
+ ///
+ [SkillHandler(SkillId.Bulletmarker_NapalmBullet)]
+ public class Bulletmarker_NapalmBullet : ITargetSkillHandler
+ {
+ ///
+ /// Handles the skill, shoot with the pistol at the enemy and hit others on the way.
+ ///
+ ///
+ ///
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, ICombatEntity target)
+ {
+ if (!caster.TrySpendSp(skill))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+
+ if (!caster.IsBuffActive(BuffId.DoubleGunStance_Buff))
+ 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;
+ }
+
+ Send.ZC_SKILL_READY(caster, skill, caster.Position, target.Position);
+ Send.ZC_NORMAL.UpdateSkillEffect(caster, caster.Handle, caster.Position, caster.Direction, Position.Zero);
+
+ // Increase by one the stack count for Overheating buff
+ if (!caster.IsBuffActive(BuffId.Outrage_Buff))
+ caster.StartBuff(BuffId.Overheating_Buff, TimeSpan.FromSeconds(35));
+
+ var skillHits = new List();
+ this.AddSkillHitInfo(caster, target, skill, skillHits);
+
+ var splashArea = new Square(caster.Position, caster.Direction, 130, 45);
+
+ var otherTargets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+
+ foreach (var otherTarget in otherTargets.LimitBySDR(caster, skill))
+ {
+ if (otherTarget.Handle == target.Handle)
+ continue;
+
+ this.AddSkillHitInfo(caster, otherTarget, skill, skillHits);
+ }
+
+ Send.ZC_SKILL_FORCE_TARGET(caster, target, skill, skillHits);
+
+ if (caster.TryGetBuff(BuffId.Outrage_Buff, out var outrageBuff))
+ {
+ if (outrageBuff.OverbuffCounter > 0)
+ {
+ foreach (var otherTarget in otherTargets.LimitBySDR(caster, skill))
+ {
+ var skillHitResult = SCR_SkillHit(caster, otherTarget, skill);
+ otherTarget.TakeDamage(skillHitResult.Damage, caster);
+ var hit = new HitInfo(caster, otherTarget, skill, skillHitResult.Damage, skillHitResult.Result);
+ Send.ZC_HIT_INFO(caster, otherTarget, hit);
+ otherTarget.StartBuff(BuffId.Tase_Debuff, TimeSpan.FromSeconds(10));
+ }
+
+ caster.Components.Get().Overbuff(outrageBuff, -1);
+ }
+ }
+ }
+
+ ///
+ /// Adds a new HitInfo to the processing list
+ ///
+ ///
+ ///
+ ///
+ ///
+ private void AddSkillHitInfo(ICombatEntity caster, ICombatEntity target, Skill skill, List skillHits)
+ {
+ var damageDelay = TimeSpan.FromMilliseconds(200);
+ var skillHitDelay = TimeSpan.FromMilliseconds(300);
+ var modifier = SkillModifier.Default;
+ modifier.HitCount = 2;
+
+ var skillHitResult = SCR_SkillHit(caster, target, skill, modifier);
+ target.TakeDamage(skillHitResult.Damage, caster);
+
+ var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay);
+ skillHits.Add(skillHit);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_Outrage.cs b/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_Outrage.cs
new file mode 100644
index 000000000..96a3beb5c
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_Outrage.cs
@@ -0,0 +1,57 @@
+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.BulletMaker
+{
+ ///
+ /// Handler for the Bullet Maker's skill Outrage.
+ ///
+ [SkillHandler(SkillId.Bulletmarker_Outrage)]
+ public class Bulletmarker_Outrage : IGroundSkillHandler
+ {
+ ///
+ /// Handles skill, applies buff.
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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);
+
+ // Cast re-cast if we already have Outrage Buff
+ if (caster.IsBuffActive(BuffId.Outrage_Buff))
+ return;
+
+ if (!caster.TryGetBuff(BuffId.Overheating_Buff, out var overheatingBuff) || overheatingBuff.OverbuffCounter < 4)
+ return;
+
+ var overBuffCounter = 0;
+
+ if (overheatingBuff.OverbuffCounter == 40)
+ overBuffCounter = 30;
+ else
+ overBuffCounter = (int)Math.Truncate((float)(overheatingBuff.OverbuffCounter / 2));
+
+ overheatingBuff.Stop();
+ caster.StartBuff(BuffId.Outrage_Buff, TimeSpan.Zero, overBuffCounter);
+
+ Send.ZC_SKILL_READY(caster, skill, originPos, farPos);
+ Send.ZC_NORMAL.UpdateSkillEffect(caster, caster.Handle, originPos, caster.Direction, Position.Zero);
+ Send.ZC_SKILL_MELEE_GROUND(caster, skill, farPos, null);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_RestInPeace.cs b/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_RestInPeace.cs
new file mode 100644
index 000000000..846c63cad
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/Bulletmarker_RestInPeace.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Collections.Generic;
+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 static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Scouts.BulletMaker
+{
+ ///
+ /// Handles the Bullet Maker's skill Rest In Peace (R.I.P.).
+ ///
+ [SkillHandler(SkillId.Bulletmarker_RestInPeace)]
+ public class Bulletmarker_RestInPeace : IGroundSkillHandler
+ {
+ ///
+ /// Handles the skill, shoot with the pistol and hits enemies in front
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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;
+ }
+
+ if (!caster.IsBuffActive(BuffId.DoubleGunStance_Buff))
+ return;
+
+ skill.IncreaseOverheat();
+ caster.TurnTowards(target);
+ caster.SetAttackState(true);
+
+ Send.ZC_SKILL_READY(caster, skill, caster.Position, caster.Position);
+ Send.ZC_NORMAL.UpdateSkillEffect(caster, caster.Handle, caster.Position, caster.Direction, Position.Zero);
+
+ // Increase by one the stack count for Overheating buff
+ if (!caster.IsBuffActive(BuffId.Outrage_Buff))
+ caster.StartBuff(BuffId.Overheating_Buff, TimeSpan.FromSeconds(35));
+
+ var skillHits1 = new List();
+ var skillHits2 = new List();
+
+ var splashArea = new Square(caster.Position, caster.Direction, 150, 30);
+
+ var tagets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+
+ foreach (var otherTarget in tagets.LimitBySDR(caster, skill))
+ {
+ this.AddSkillHitInfo(caster, otherTarget, skill, TimeSpan.FromMilliseconds(23), TimeSpan.Zero, skillHits1);
+ this.AddSkillHitInfo(caster, otherTarget, skill, TimeSpan.FromMilliseconds(478), TimeSpan.Zero, skillHits2);
+ }
+
+ Send.ZC_SKILL_MELEE_GROUND(caster, skill, caster.Position, skillHits1);
+ Send.ZC_SKILL_HIT_INFO(caster, skillHits2);
+
+ if (caster.TryGetBuff(BuffId.Outrage_Buff, out var outrageBuff) && outrageBuff.OverbuffCounter > 0)
+ {
+ caster.Components.Get().Overbuff(outrageBuff, -1);
+ }
+ }
+
+ ///
+ /// Adds a new HitInfo to the processing list
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private void AddSkillHitInfo(ICombatEntity caster, ICombatEntity target, Skill skill, TimeSpan damageDelay, TimeSpan skillHitDelay, List skillHits)
+ {
+ var modifier = SkillModifier.Default;
+ modifier.HitCount = 2;
+
+ var skillHitResult = SCR_SkillHit(caster, target, skill, modifier);
+
+ // Increase the damage 55% if caster has Outrage Buff
+ if (caster.IsBuffActive(BuffId.Outrage_Buff))
+ skillHitResult.Damage *= 1.55f;
+
+ target.TakeDamage(skillHitResult.Damage, caster);
+
+ var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay);
+
+ skillHits.Add(skillHit);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/TracerBullet.cs b/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/TracerBullet.cs
new file mode 100644
index 000000000..657a71993
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Scouts/BulletMaker/TracerBullet.cs
@@ -0,0 +1,53 @@
+using Melia.Shared.Game.Const;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.Skills.Handlers.Base;
+using Melia.Zone.World.Actors;
+
+namespace Melia.Zone.Skills.Handlers.Scouts.BulletMaker
+{
+ ///
+ /// Handler for the passive Bullet Maker skill Tracer Bullet.
+ ///
+ [SkillHandler(SkillId.Bulletmarker_TracerBullet)]
+ public class Bulletmarker_TracerBullet : ISkillHandler, ISkillCombatAttackBeforeCalcHandler
+ {
+ ///
+ /// Applies the skill's effect before the combat calculations.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void OnAttackBeforeCalc(Skill skill, ICombatEntity attacker, ICombatEntity target, Skill attackerSkill, SkillModifier modifier, SkillHitResult skillHitResult)
+ {
+ // Increase Accuracy by a percentage
+ var accuracyRateBonus = this.GetAccuracyRateBonus(skill);
+ attacker.Properties.Modify(PropertyName.HR_RATE_BM, accuracyRateBonus);
+
+ // Increase Minimum Critical Rate by a percentage
+ modifier.MinCritChance *= 1 + this.GetMinimumCriticalRateBonus(skill);
+ }
+
+ ///
+ /// Returns the accuracy rate bonus applied on the skill
+ ///
+ ///
+ ///
+ private float GetAccuracyRateBonus(Skill skill)
+ {
+ return (10 + (skill.Level * 2)) / 100;
+ }
+
+ ///
+ /// Returns the minmum Critical Rate Bonus applied on the skill
+ ///
+ ///
+ ///
+ private float GetMinimumCriticalRateBonus(Skill skill)
+ {
+ return (10 + (skill.Level * 2)) / 100;
+ }
+ }
+}
diff --git a/src/ZoneServer/World/Actors/CombatEntities/Components/BuffComponent.cs b/src/ZoneServer/World/Actors/CombatEntities/Components/BuffComponent.cs
index dd9652fed..5eeb573c8 100644
--- a/src/ZoneServer/World/Actors/CombatEntities/Components/BuffComponent.cs
+++ b/src/ZoneServer/World/Actors/CombatEntities/Components/BuffComponent.cs
@@ -83,6 +83,32 @@ private void Overbuff(Buff buff)
Send.ZC_BUFF_UPDATE(this.Entity, buff);
}
+ ///
+ /// Changes the buff's overbuff and updates the client.
+ ///
+ ///
+ ///
+ public void Overbuff(Buff buff, int value)
+ {
+ var overbuff = buff.OverbuffCounter;
+ buff.UpdateOverbuff(value);
+
+ if (overbuff != buff.OverbuffCounter)
+ {
+ buff.Start();
+ Send.ZC_BUFF_UPDATE(this.Entity, buff);
+ }
+ else if ((overbuff + value) <= 0)
+ {
+ buff.Stop();
+ }
+ else
+ {
+ buff.ExtendDuration();
+ Send.ZC_BUFF_UPDATE(this.Entity, buff);
+ }
+ }
+
///
/// Adds and activates given buffs. If a buff already exists,
/// it gets overbuffed.
@@ -319,8 +345,9 @@ public int GetOverbuffCount(BuffId buffId)
/// Custom duration of the buff.
/// The entity that casted the buff.
/// The id of the skill associated with the buff.
+ /// OverBuff count, the quantity of stacking buffs
///
- public Buff Start(BuffId buffId, float numArg1, float numArg2, TimeSpan duration, ICombatEntity caster, SkillId skillId)
+ public Buff Start(BuffId buffId, float numArg1, float numArg2, TimeSpan duration, ICombatEntity caster, SkillId skillId, int overBuffCount = 1)
{
// Attempt status resistance against debuffs
// TODO: Ideally, this should happen from the buff handler,
@@ -337,7 +364,7 @@ public Buff Start(BuffId buffId, float numArg1, float numArg2, TimeSpan duration
if (!this.TryGet(buffId, out var buff))
{
- buff = new Buff(buffId, numArg1, numArg2, duration, TimeSpan.Zero, this.Entity, caster ?? this.Entity, skillId);
+ buff = new Buff(buffId, numArg1, numArg2, duration, TimeSpan.Zero, this.Entity, caster ?? this.Entity, skillId, overBuffCount);
this.Add(buff);
}
else
diff --git a/src/ZoneServer/World/Actors/Entity.cs b/src/ZoneServer/World/Actors/Entity.cs
index 794c07983..19969a0ef 100644
--- a/src/ZoneServer/World/Actors/Entity.cs
+++ b/src/ZoneServer/World/Actors/Entity.cs
@@ -291,9 +291,10 @@ public static Buff StartBuff(this ICombatEntity entity, BuffId buffId)
///
///
///
+ ///
///
- public static Buff StartBuff(this ICombatEntity entity, BuffId buffId, TimeSpan duration)
- => entity.Components.Get()?.Start(buffId, 0, 0, duration, entity, SkillId.None);
+ public static Buff StartBuff(this ICombatEntity entity, BuffId buffId, TimeSpan duration, int overBuffCount = 1)
+ => entity.Components.Get()?.Start(buffId, 0, 0, duration, entity, SkillId.None, overBuffCount);
///
/// Starts the buff with the given id. If the buff is already active,
diff --git a/system/scripts/zone/core/calc_character.cs b/system/scripts/zone/core/calc_character.cs
index ce89d2cd8..a9974abe5 100644
--- a/system/scripts/zone/core/calc_character.cs
+++ b/system/scripts/zone/core/calc_character.cs
@@ -5,7 +5,6 @@
//---------------------------------------------------------------------------
using System;
-using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Melia.Shared.Game.Const;
@@ -15,7 +14,6 @@
using Melia.Zone.World.Actors.Characters;
using Melia.Zone.World.Actors.Characters.Components;
using Melia.Zone.World.Actors.CombatEntities.Components;
-using Yggdrasil.Logging;
using Yggdrasil.Util;
public class CharacterCalculationsScript : GeneralScript
@@ -1403,6 +1401,9 @@ public float SCR_Get_Character_MovingShotable(Character character)
if (anyBuffsActive)
return 1;
+ if (character.IsBuffActive(BuffId.DoubleGunStance_Buff))
+ return 1;
+
return 0;
}
@@ -1415,6 +1416,7 @@ public float SCR_Get_Character_MovingShotable(Character character)
public float SCR_Get_Character_MovingShot(Character character)
{
var canMoveWhileShooting = character.Properties.GetFloat(PropertyName.MovingShotable) == 1;
+
if (!canMoveWhileShooting)
return 0;