diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/Assassin_Target_Debuff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/Assassin_Target_Debuff.cs
new file mode 100644
index 000000000..71888ac87
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/Assassin_Target_Debuff.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Network;
+using Melia.Zone.World.Actors;
+using Melia.Zone.World.Actors.Characters;
+
+namespace Melia.Zone.Buffs.Handlers.Scouts.Assassin
+{
+ ///
+ /// Handler for Assassin Target debuff, which can only
+ /// be applied to a single target and needs to clean
+ /// itself up when it ends
+ ///
+ [BuffHandler(BuffId.Assassin_Target_Debuff)]
+ internal class Assassin_Target_Debuff : BuffHandler
+ {
+ ///
+ /// Remove the variable from the caster when the buff ends,
+ /// assuming this entity is still the target
+ ///
+ ///
+ public override void OnEnd(Buff buff)
+ {
+ var targetHandle = buff.Target.Handle;
+ var caster = buff.Caster;
+ if (caster is Character character && character != null && character.Variables.Temp.TryGetInt("Melia.AssassinationTarget", out var assassinationTarget) && assassinationTarget == targetHandle)
+ character.Variables.Temp.Remove("Melia.AssassinationTarget");
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/HallucinationSmoke_Buff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/HallucinationSmoke_Buff.cs
new file mode 100644
index 000000000..c00e1f120
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/HallucinationSmoke_Buff.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Network;
+using Melia.Zone.World.Actors;
+
+namespace Melia.Zone.Buffs.Handlers.Scouts.Assassin
+{
+ ///
+ /// Handler for Hallucination Smoke Buff, which raises crit rate
+ ///
+ ///
+ /// NumArg1: Skill Level
+ /// NumArg2: Unused
+ ///
+ [BuffHandler(BuffId.HallucinationSmoke_Buff)]
+ internal class HallucinationSmoke_Buff : BuffHandler
+ {
+ // Official bonus rate is unknown
+ private const float CRTHRBonus = 0.2f;
+
+ ///
+ /// Starts buff, increasing Crit Rate
+ ///
+ ///
+ public override void OnActivate(Buff buff, ActivationType activationType)
+ {
+ var CRTHRbuff = buff.Target.Properties.GetFloat(PropertyName.CRTHR) * CRTHRBonus;
+
+ AddPropertyModifier(buff, buff.Target, PropertyName.CRTHR_BM, CRTHRbuff);
+ }
+
+ ///
+ /// Ends the buff, resetting crit rate
+ ///
+ ///
+ public override void OnEnd(Buff buff)
+ {
+ RemovePropertyModifier(buff, buff.Target, PropertyName.CRTHR_BM);
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/HallucinationSmoke_Debuff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/HallucinationSmoke_Debuff.cs
new file mode 100644
index 000000000..593e1163f
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/HallucinationSmoke_Debuff.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Network;
+using Melia.Zone.World.Actors;
+
+namespace Melia.Zone.Buffs.Handlers.Scouts.Assassin
+{
+ ///
+ /// Handler for Hallucination Smoke Debuff, which reduces
+ /// Accuracy and Evasion
+ ///
+ ///
+ /// NumArg1: Skill Level
+ /// NumArg2: Damage to take when buff ends
+ ///
+ [BuffHandler(BuffId.HallucinationSmoke_Debuff)]
+ internal class HallucinationSmoke_Debuff : BuffHandler
+ {
+ private const float DRPenaltyRate = 0.2f;
+ private const float HRPenaltyRate = 0.2f;
+
+ ///
+ /// Starts buff, reducing Hit and Dodge Rate
+ ///
+ ///
+ public override void OnActivate(Buff buff, ActivationType activationType)
+ {
+ var reduceDR = buff.Target.Properties.GetFloat(PropertyName.DR) * DRPenaltyRate;
+ var reduceHR = buff.Target.Properties.GetFloat(PropertyName.HR) * HRPenaltyRate;
+
+ AddPropertyModifier(buff, buff.Target, PropertyName.DR_BM, -reduceDR);
+ AddPropertyModifier(buff, buff.Target, PropertyName.HR_BM, -reduceHR);
+ }
+
+ ///
+ /// Ends the buff, resetting hit and dodge rate, and
+ /// potentially dealing damage
+ ///
+ ///
+ public override void OnEnd(Buff buff)
+ {
+ RemovePropertyModifier(buff, buff.Target, PropertyName.DR_BM);
+ RemovePropertyModifier(buff, buff.Target, PropertyName.HR_BM);
+
+ if (buff.NumArg2 > 0 && !buff.Target.IsDead)
+ {
+ var attacker = buff.Caster;
+ var target = buff.Target;
+ var damage = buff.NumArg2;
+
+ target.TakeSimpleHit(damage, attacker, SkillId.None);
+ }
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/Hasisas_Buff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/Hasisas_Buff.cs
index e63ddc3dd..a85405cb2 100644
--- a/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/Hasisas_Buff.cs
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/Hasisas_Buff.cs
@@ -7,6 +7,10 @@ namespace Melia.Zone.Buffs.Handlers.Scouts.Assassin
///
/// Handle for the Hasisas Buff, which increases the target's Attack speed and Crit damage
///
+ ///
+ /// NumArg1: Skill Level
+ /// NumArg2: 1 if the evasion bonus is applied
+ ///
[BuffHandler(BuffId.Hasisas_Buff)]
public class Hasisas_Buff : BuffHandler
{
@@ -14,6 +18,7 @@ public class Hasisas_Buff : BuffHandler
private const float AspdBonusPerLevel = 20;
private const float CritBonusBase = 3f;
private const float CritBonusPerLevel = 1.5f;
+ private const float DRBonusBase = 0.2f;
private const float HpLossRate = 0.01f;
public override void OnActivate(Buff buff, ActivationType activationType)
@@ -23,12 +28,22 @@ public override void OnActivate(Buff buff, ActivationType activationType)
AddPropertyModifier(buff, buff.Target, PropertyName.ASPD_BM, aspdBonus);
AddPropertyModifier(buff, buff.Target, PropertyName.CRTATK_BM, critBonus);
+
+ buff.Vars.SetInt("Hasisas.TickCounter", 0);
+
+ if (buff.NumArg2 > 0)
+ {
+ var drBonus = buff.Target.Properties.GetFloat(PropertyName.DR) * DRBonusBase;
+
+ AddPropertyModifier(buff, buff.Target, PropertyName.DR_BM, drBonus);
+ }
}
public override void OnEnd(Buff buff)
{
RemovePropertyModifier(buff, buff.Target, PropertyName.ASPD_BM);
RemovePropertyModifier(buff, buff.Target, PropertyName.CRTATK_BM);
+ RemovePropertyModifier(buff, buff.Target, PropertyName.DR_BM);
}
public override void WhileActive(Buff buff)
@@ -46,6 +61,24 @@ private void ReduceHp(Buff buff)
if (Feature.IsEnabled("HasisasNoHpLoss"))
return;
+ // Assassin2 increases the number of ticks before you take
+ // damage. This is done using a counter in a buff variable.
+ // The tick limit is set when applying the buff
+ var tickCounter = buff.Vars.GetInt("Hasisas.TickCounter");
+ tickCounter++;
+
+ if (tickCounter > buff.Vars.GetInt("Hasisas.TickLimit"))
+ {
+ // reset the counter and continue
+ buff.Vars.SetInt("Hasisas.TickCounter", 0);
+ }
+ else
+ {
+ // update the counter and return
+ buff.Vars.SetInt("Hasisas.TickCounter", tickCounter);
+ return;
+ }
+
// The description stats an HP loss of 1% per 10 seconds,
// which matches the buff's update time. Should a user
// change it, the description would no longer be accurate,
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/PiercingHeart_Buff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/PiercingHeart_Buff.cs
new file mode 100644
index 000000000..93192fce9
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/PiercingHeart_Buff.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.Network;
+using Melia.Zone.World.Actors;
+
+namespace Melia.Zone.Buffs.Handlers.Scouts.Assassin
+{
+ ///
+ /// Handler for Piercing Heart Buff, which raises crit rate
+ ///
+ ///
+ /// NumArg1: Skill Level
+ /// NumArg2: Unused
+ ///
+ [BuffHandler(BuffId.PiercingHeart_Buff)]
+ internal class PiercingHeart_Buff : BuffHandler
+ {
+ // Official bonus rate is unknown
+ private const float CRTHRBonus = 0.5f;
+
+ ///
+ /// Starts buff, increasing Crit Rate
+ ///
+ ///
+ public override void OnActivate(Buff buff, ActivationType activationType)
+ {
+ var CRTHRbuff = buff.Target.Properties.GetFloat(PropertyName.CRTHR) * CRTHRBonus;
+
+ AddPropertyModifier(buff, buff.Target, PropertyName.CRTHR_BM, CRTHRbuff);
+ }
+
+ ///
+ /// Ends the buff, resetting crit rate
+ ///
+ ///
+ public override void OnEnd(Buff buff)
+ {
+ RemovePropertyModifier(buff, buff.Target, PropertyName.CRTHR_BM);
+ }
+ }
+}
diff --git a/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/PiercingHeart_Debuff.cs b/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/PiercingHeart_Debuff.cs
new file mode 100644
index 000000000..6e4d40f0c
--- /dev/null
+++ b/src/ZoneServer/Buffs/Handlers/Scouts/Assassin/PiercingHeart_Debuff.cs
@@ -0,0 +1,30 @@
+using System;
+using Melia.Shared.Game.Const;
+using Melia.Zone.Buffs.Base;
+using Melia.Zone.World.Actors;
+
+namespace Melia.Zone.Buffs.Handlers.Scout.Assassin
+{
+ ///
+ /// Handle for the Piercing Heart debuff, which prevents healing
+ ///
+ [BuffHandler(BuffId.PiercingHeart_Debuff)]
+ public class PiercingHeart_Debuff : BuffHandler
+ {
+ ///
+ /// Eliminates all healing if the entity has the piercing heart debuff.
+ ///
+ ///
+ ///
+ ///
+ public static bool TryApply(ICombatEntity entity, ref float hpAmount)
+ {
+ if (!entity.TryGetBuff(BuffId.PiercingHeart_Debuff, out var buff))
+ return false;
+
+ hpAmount = 0;
+
+ return true;
+ }
+ }
+}
diff --git a/src/ZoneServer/Pads/Handlers/Scout/Assassin/Assassin_HallucinationSmoke.cs b/src/ZoneServer/Pads/Handlers/Scout/Assassin/Assassin_HallucinationSmoke.cs
new file mode 100644
index 000000000..c82ca3818
--- /dev/null
+++ b/src/ZoneServer/Pads/Handlers/Scout/Assassin/Assassin_HallucinationSmoke.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.Scout.Assassin
+{
+ ///
+ /// Handler for the Assassin HallucinationSmoke pad,
+ /// which displays a smoke cloud
+ ///
+ [PadHandler(PadName.Assassin_HallucinationSmoke)]
+ public class Assassin_HallucinationSmoke : ICreatePadHandler, IDestroyPadHandler
+ {
+ ///
+ /// Called when the pad is created.
+ ///
+ ///
+ ///
+ public void Created(object sender, PadTriggerArgs args)
+ {
+ Send.ZC_NORMAL.PadUpdate(args.Creator, args.Trigger, PadName.Assassin_HallucinationSmoke, -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.Assassin_HallucinationSmoke, 0, 145.8735f, 30, false);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Combat/SkillModifier.cs b/src/ZoneServer/Skills/Combat/SkillModifier.cs
index 127e61a0e..f77ad2f03 100644
--- a/src/ZoneServer/Skills/Combat/SkillModifier.cs
+++ b/src/ZoneServer/Skills/Combat/SkillModifier.cs
@@ -74,6 +74,14 @@ public class SkillModifier
///
public float DamageMultiplier { get; set; } = 1;
+ ///
+ /// Gets or sets the Crit Rate multiplier
+ ///
+ ///
+ /// This is applied before anything else that modifies crit rate
+ ///
+ public float CritRateMultiplier { get; set; } = 1;
+
///
/// Gets or sets the minimum critical chance.
///
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_Annihilation.cs b/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_Annihilation.cs
new file mode 100644
index 000000000..714f94b46
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_Annihilation.cs
@@ -0,0 +1,133 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Melia.Shared.Data.Database;
+using Melia.Shared.Game.Const;
+using Melia.Shared.L10N;
+using Melia.Shared.World;
+using Melia.Zone.Network;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.Skills.Handlers.Base;
+using Melia.Zone.Skills.SplashAreas;
+using Melia.Zone.World.Actors;
+using Melia.Zone.World.Actors.CombatEntities.Components;
+using Yggdrasil.Logging;
+using Yggdrasil.Util;
+using static Melia.Shared.Util.TaskHelper;
+using static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Scouts.Assassin
+{
+ ///
+ /// Handler for the Assassin Skill Annihilation
+ ///
+ [SkillHandler(SkillId.Assassin_Annihilation)]
+ public class Assassin_Annihilation : IGroundSkillHandler
+ {
+ ///
+ /// Handles skill, damaging targets.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target)
+ {
+ if (!caster.TrySpendSp(skill))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+
+ skill.IncreaseOverheat();
+ caster.SetAttackState(true);
+
+ var splashParam = skill.GetSplashParameters(caster, originPos, farPos, length: 0, width: 100, angle: 0);
+ var splashArea = skill.GetSplashArea(SplashType.Circle, splashParam);
+
+ Send.ZC_SKILL_READY(caster, skill, originPos, farPos);
+ Send.ZC_SKILL_MELEE_GROUND(caster, skill, farPos, null);
+
+ CallSafe(this.Attack(skill, caster, splashArea));
+ }
+
+ ///
+ /// Executes the actual attack after a delay.
+ ///
+ ///
+ ///
+ ///
+ private async Task Attack(Skill skill, ICombatEntity caster, ISplashArea splashArea)
+ {
+ var damageDelay = TimeSpan.FromMilliseconds(50);
+ var skillHitDelay = TimeSpan.Zero;
+
+ var delayBetweenHits = TimeSpan.FromMilliseconds(400);
+
+ // first hit hits instantly
+
+ var hits = new List();
+
+ // Assassin23 is a slight variation of this skill
+ var isFastVariant = caster.IsAbilityActive(AbilityId.Assassin23);
+
+ // Caster has invincibility during the normal version
+ if (!isFastVariant)
+ caster.StartBuff(BuffId.Skill_NoDamage_Buff, skill.Level, 0, TimeSpan.FromMilliseconds(2600), caster);
+
+ for (var i = 0; i < 7; i++)
+ {
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var modifier = SkillModifier.MultiHit(2);
+
+ // Fast variant does 10 hits
+ if (isFastVariant)
+ modifier.HitCount = 10;
+
+ // Assassin17 adds double crit rate to bleeding targets
+ if (caster.IsAbilityActive(AbilityId.Assassin17) && (target.IsBuffActive(BuffId.HeavyBleeding) || target.IsBuffActive(BuffId.Behead_Debuff)))
+ {
+ modifier.CritRateMultiplier++;
+ }
+
+ // Increase damage by 10% if target is under the effect of
+ // Assassination Target from the caster
+ if (target.TryGetBuff(BuffId.Assassin_Target_Debuff, out var assassinTargetDebuff))
+ {
+ if (assassinTargetDebuff.Caster == caster)
+ modifier.DamageMultiplier += 0.10f;
+ }
+
+ var skillHitResult = SCR_SkillHit(caster, target, skill, modifier);
+ target.TakeDamage(skillHitResult.Damage, caster);
+
+ var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay);
+ skillHit.HitEffect = HitEffect.Impact;
+ hits.Add(skillHit);
+ }
+
+ Send.ZC_SKILL_HIT_INFO(caster, hits);
+
+ hits.Clear();
+
+ // we actually have to wait for the last hit here as well
+ // due to the animation cancel and cloaking effect
+ if (i < 7)
+ await Task.Delay(delayBetweenHits);
+ }
+
+ // Have to send this to make you reappear afterwards
+ Send.ZC_NORMAL.SkillCancelCancel(caster, skill.Id);
+ Send.ZC_PLAY_ANI(caster, "idle1");
+
+ // Assassin16 gives cloak after the slow version
+ if (!isFastVariant && caster.TryGetActiveAbilityLevel(AbilityId.Assassin16, out var level))
+ {
+ caster.StartBuff(BuffId.Cloaking_Buff, skill.Level, 0, TimeSpan.FromSeconds(level), caster);
+ }
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_Behead.cs b/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_Behead.cs
index da5a37ae9..e790be85c 100644
--- a/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_Behead.cs
+++ b/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_Behead.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Net.Http.Headers;
using System.Threading.Tasks;
using Melia.Shared.Data.Database;
using Melia.Shared.Game.Const;
@@ -40,9 +41,24 @@ public void Handle(Skill skill, ICombatEntity caster, Position originPos, Positi
return;
}
+ var forcedJump = false;
+
+ // If the caster has an assassination target, it overrides
+ // the target for this attack and forces the jump attempt
+ // even if the target is a monster
+ if (caster is Character character && character.Variables.Temp.Has("Melia.AssassinationTarget"))
+ {
+ var possibleTarget = caster.Map.GetCombatEntity(character.Variables.Temp.GetInt("Melia.AssassinationTarget"));
+ if (possibleTarget != null)
+ {
+ target = possibleTarget;
+ forcedJump = true;
+ }
+ }
+
// If the target is a player, Behead will teleport behind them
// If any of these conditions fail, you just swing normally
- if (target is Character)
+ if (target is Character || forcedJump)
this.JumpBehind(caster, target);
skill.IncreaseOverheat();
@@ -114,6 +130,14 @@ private async Task Attack(Skill skill, ICombatEntity caster, ISplashArea splashA
modifier.HitCount = 3;
modifier.DefensePenetrationRate = 0.15f;
+ // Increase damage by 10% if target is under the effect of
+ // Assassination Target from the caster
+ if (target.TryGetBuff(BuffId.Assassin_Target_Debuff, out var assassinTargetDebuff))
+ {
+ if (assassinTargetDebuff.Caster == caster)
+ modifier.DamageMultiplier += 0.10f;
+ }
+
var skillHitResult = SCR_SkillHit(caster, target, skill, modifier);
target.TakeDamage(skillHitResult.Damage, caster);
@@ -135,6 +159,14 @@ private async Task Attack(Skill skill, ICombatEntity caster, ISplashArea splashA
modifier.HitCount = 3;
modifier.DefensePenetrationRate = 0.15f;
+ // Increase damage by 10% if target is under the effect of
+ // Assassination Target from the caster
+ if (target.TryGetBuff(BuffId.Assassin_Target_Debuff, out var assassinTargetDebuff))
+ {
+ if (assassinTargetDebuff.Caster == caster)
+ modifier.DamageMultiplier += 0.10f;
+ }
+
var skillHitResult2 = SCR_SkillHit(caster, target, skill, modifier);
target.TakeDamage(skillHitResult2.Damage, caster);
@@ -143,8 +175,23 @@ private async Task Attack(Skill skill, ICombatEntity caster, ISplashArea splashA
hits.Add(skillHit2);
- // Behead Debuff deals 5% of the damage every 1.5s for 30s
- target.StartBuff(BuffId.Behead_Debuff, skill.Level, skillHitResult2.Damage * 0.05f, TimeSpan.FromSeconds(30), caster);
+ // Behead Debuff normally deals 5% of the damage every 1.5s for 30s
+ var bleedingDamage = skillHitResult2.Damage * 0.05f;
+
+ // Assassin6 instead does 5% of their maximum HP, or 5% of the caster's
+ // maximum HP, whichever is less. It also doesn't work on bosses
+ if (target.Rank != MonsterRank.Boss && caster.IsAbilityActive(AbilityId.Assassin6))
+ {
+ bleedingDamage = MathF.Min(caster.Properties.GetFloat(PropertyName.MHP) * 0.05f, target.Properties.GetFloat(PropertyName.MHP) * 0.05f);
+ }
+
+ if (skillHitResult2.Result != HitResultType.Dodge) {
+ target.StartBuff(BuffId.Behead_Debuff, skill.Level, bleedingDamage, TimeSpan.FromSeconds(30), caster);
+
+ // Assassin5 adds 5 seconds of silence
+ if (caster.IsAbilityActive(AbilityId.Assassin5))
+ target.StartBuff(BuffId.Common_Silence, skill.Level, bleedingDamage, TimeSpan.FromSeconds(5), caster);
+ }
}
Send.ZC_SKILL_HIT_INFO(caster, hits);
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_HallucinationSmoke.cs b/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_HallucinationSmoke.cs
new file mode 100644
index 000000000..c654837bd
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_HallucinationSmoke.cs
@@ -0,0 +1,128 @@
+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 static Melia.Shared.Util.TaskHelper;
+using static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Scouts.Assassin
+{
+ ///
+ /// Handler for the Assassin skill Hallucination Smoke
+ ///
+ [SkillHandler(SkillId.Assassin_HallucinationSmoke)]
+ public class Assassin_HallucinationSmoke : IGroundSkillHandler
+ {
+ ///
+ /// Handles skill, damaging targets.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target)
+ {
+ if (!caster.TrySpendSp(skill))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+
+ skill.IncreaseOverheat();
+ caster.SetAttackState(true);
+
+ Send.ZC_SKILL_READY(caster, skill, originPos, farPos);
+ Send.ZC_SKILL_MELEE_GROUND(caster, skill, farPos, null);
+
+ CallSafe(this.SetPad(skill, caster));
+ }
+
+ ///
+ /// Places the pad
+ ///
+ ///
+ ///
+ ///
+ private async Task SetPad(Skill skill, ICombatEntity caster)
+ {
+ var initialDelay = TimeSpan.FromMilliseconds(300);
+ var skillHitDelay = TimeSpan.Zero;
+
+ await Task.Delay(initialDelay);
+
+ var pad = new Pad(PadName.Assassin_HallucinationSmoke, caster, skill, new Circle(caster.Position, 50));
+ pad.Position = caster.Position;
+ pad.Trigger.MaxActorCount = 8;
+ pad.Trigger.LifeTime = TimeSpan.FromSeconds(skill.Level + 5);
+ pad.Trigger.Subscribe(TriggerType.Enter, this.ApplyDebuff);
+
+ caster.Map.AddPad(pad);
+ }
+
+ ///
+ /// Called by the pad when anything enters the smoke
+ ///
+ ///
+ ///
+ private void ApplyDebuff(object sender, PadTriggerActorArgs args)
+ {
+ var pad = args.Trigger;
+ var creator = args.Creator;
+ var target = args.Initiator;
+ var skill = args.Skill;
+
+ // At some point, this applied HallucinationSmoke_Buff to the creator
+ //if (target == creator)
+ //target.StartBuff(BuffId.HallucinationSmoke_Buff, skill.Level, 0, TimeSpan.FromSeconds(20), creator);
+
+ if (pad.Trigger.AtCapacity)
+ return;
+
+ if (!creator.CanAttack(target))
+ return;
+
+ // Assassin18 applies a special debuff. Only one enemy can have this
+ // debuff at a time, so we use a character variable to track it.
+ if (creator.IsAbilityActive(AbilityId.Assassin18) && creator is Character character && !character.Variables.Temp.Has("Melia.AssassinationTarget"))
+ {
+ character.Variables.Temp.SetInt("Melia.AssassinationTarget", target.Handle);
+ target.StartBuff(BuffId.Assassin_Target_Debuff, skill.Level, 0, TimeSpan.FromSeconds(15), creator);
+ }
+
+ // Assassin11 causes damage when the effect wears off
+ // We calculate the damage now but don't apply it.
+ // It's not clear how much damage was dealt, we'll assume
+ // it just used the skill's factor for now
+ var endingDamage = 0f;
+ if (creator.IsAbilityActive(AbilityId.Assassin11))
+ {
+ var modifier = SkillModifier.Default;
+
+ // Increase damage by 10% if target is under the effect of
+ // Assassination Target from the caster
+ if (target.TryGetBuff(BuffId.Assassin_Target_Debuff, out var assassinTargetDebuff))
+ {
+ if (assassinTargetDebuff.Caster == creator)
+ modifier.DamageMultiplier += 0.10f;
+ }
+
+ var skillHitResult = SCR_SkillHit(creator, target, skill);
+ endingDamage = skillHitResult.Damage;
+ }
+
+ target.StartBuff(BuffId.HallucinationSmoke_Debuff, skill.Level, endingDamage, TimeSpan.FromSeconds(20), creator);
+ }
+ }
+}
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_Hasisas.cs b/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_Hasisas.cs
index 3e1a2a479..263418282 100644
--- a/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_Hasisas.cs
+++ b/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_Hasisas.cs
@@ -42,7 +42,18 @@ public void Handle(Skill skill, ICombatEntity caster, Position originPos, Positi
skill.IncreaseOverheat();
caster.SetAttackState(true);
- caster.StartBuff(BuffId.Hasisas_Buff, skill.Level, 0, BuffDuration, caster);
+ var tickLimit = 0;
+ if (caster.TryGetActiveAbilityLevel(AbilityId.Assassin2, out var level))
+ {
+ tickLimit = level;
+ }
+
+ var evasionVariant = 0f;
+ if (caster.IsAbilityActive(AbilityId.Assassin3))
+ evasionVariant++;
+
+ var buff = caster.StartBuff(BuffId.Hasisas_Buff, skill.Level, evasionVariant, BuffDuration, caster);
+ buff.Vars.SetInt("Hasisas.TickLimit", tickLimit);
Send.ZC_SKILL_MELEE_GROUND(caster, skill, caster.Position, null);
}
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_InstantaneousAcceleration.cs b/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_InstantaneousAcceleration.cs
index 2b122a0ca..6643055ac 100644
--- a/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_InstantaneousAcceleration.cs
+++ b/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_InstantaneousAcceleration.cs
@@ -78,11 +78,33 @@ private async Task Attack(Skill skill, ICombatEntity caster, ISplashArea splashA
var modifier = SkillModifier.Default;
modifier.HitCount = 4;
+ // Assassin8 doubles the hit count in exchange for -25% damage
+ if (caster.IsAbilityActive(AbilityId.Assassin8))
+ {
+ modifier.HitCount *= 2;
+ modifier.FinalDamageMultiplier -= 0.25f;
+ }
+
+ // Increase damage by 10% if target is under the effect of
+ // Assassination Target from the caster
+ if (target.TryGetBuff(BuffId.Assassin_Target_Debuff, out var assassinTargetDebuff))
+ {
+ if (assassinTargetDebuff.Caster == caster)
+ modifier.DamageMultiplier += 0.10f;
+ }
+
var skillHitResult = SCR_SkillHit(caster, target, skill, modifier);
target.TakeDamage(skillHitResult.Damage, caster);
var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay);
hits.Add(skillHit);
+
+ if (skillHitResult.Result != HitResultType.Dodge)
+ {
+ // Assassin9 adds 3 seconds of stun
+ if (caster.IsAbilityActive(AbilityId.Assassin9))
+ target.StartBuff(BuffId.Stun, skill.Level, 0, TimeSpan.FromSeconds(3), caster);
+ }
}
Send.ZC_SKILL_HIT_INFO(caster, hits);
diff --git a/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_PiercingHeart.cs b/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_PiercingHeart.cs
new file mode 100644
index 000000000..ee4ccd135
--- /dev/null
+++ b/src/ZoneServer/Skills/Handlers/Scouts/Assassin/Assassin_PiercingHeart.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Melia.Shared.Data.Database;
+using Melia.Shared.Game.Const;
+using Melia.Shared.L10N;
+using Melia.Shared.World;
+using Melia.Zone.Network;
+using Melia.Zone.Skills.Combat;
+using Melia.Zone.Skills.Handlers.Base;
+using Melia.Zone.Skills.SplashAreas;
+using Melia.Zone.World.Actors;
+using Yggdrasil.Util;
+using static Melia.Shared.Util.TaskHelper;
+using static Melia.Zone.Skills.SkillUseFunctions;
+
+namespace Melia.Zone.Skills.Handlers.Scouts.Assassin
+{
+ ///
+ /// Handler for the Assassin skill Piercing Heart
+ ///
+ [SkillHandler(SkillId.Assassin_PiercingHeart)]
+ public class Assassin_PiercingHeart : IGroundSkillHandler
+ {
+ ///
+ /// Handles skill, damaging targets.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target)
+ {
+ if (!caster.TrySpendSp(skill))
+ {
+ caster.ServerMessage(Localization.Get("Not enough SP."));
+ return;
+ }
+
+ skill.IncreaseOverheat();
+ caster.SetAttackState(true);
+
+ var splashParam = skill.GetSplashParameters(caster, originPos, farPos, length: 70, width: 25, angle: 0);
+ var splashArea = skill.GetSplashArea(SplashType.Square, splashParam);
+
+ Send.ZC_SKILL_READY(caster, skill, originPos, farPos);
+ Send.ZC_SKILL_MELEE_GROUND(caster, skill, farPos, null);
+
+ CallSafe(this.Attack(skill, caster, splashArea));
+ }
+
+ ///
+ /// Executes the actual attack after a delay.
+ ///
+ ///
+ ///
+ ///
+ private async Task Attack(Skill skill, ICombatEntity caster, ISplashArea splashArea)
+ {
+ var hitDelay = TimeSpan.FromMilliseconds(150);
+ var damageDelay = TimeSpan.FromMilliseconds(50);
+ var skillHitDelay = TimeSpan.Zero;
+
+ await Task.Delay(hitDelay);
+
+ var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea);
+ var hits = new List();
+
+ // Assassin13 increases duration of the debuff
+ var debuffDuration = 10f;
+ if (caster.TryGetActiveAbilityLevel(AbilityId.Assassin13, out var level))
+ debuffDuration += level;
+
+ foreach (var target in targets.LimitBySDR(caster, skill))
+ {
+ var modifier = SkillModifier.MultiHit(4);
+ modifier.DefensePenetrationRate += 0.15f;
+
+ // Increase damage by 10% if target is under the effect of
+ // Assassination Target from the caster
+ if (target.TryGetBuff(BuffId.Assassin_Target_Debuff, out var assassinTargetDebuff))
+ {
+ if (assassinTargetDebuff.Caster == caster)
+ modifier.DamageMultiplier += 0.10f;
+ }
+
+ var skillHitResult = SCR_SkillHit(caster, target, skill, modifier);
+ target.TakeDamage(skillHitResult.Damage, caster);
+
+ var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay);
+ skillHit.HitEffect = HitEffect.Impact;
+
+ hits.Add(skillHit);
+
+ if (skillHitResult.Result != HitResultType.Dodge)
+ target.StartBuff(BuffId.PiercingHeart_Debuff, skill.Level, 0, TimeSpan.FromSeconds(debuffDuration), caster);
+ }
+
+ // Assassin14 adds a critical buff
+ if (caster.IsAbilityActive(AbilityId.Assassin14))
+ {
+ caster.StartBuff(BuffId.PiercingHeart_Buff, skill.Level, 0, TimeSpan.FromSeconds(10), caster);
+ }
+
+ Send.ZC_SKILL_HIT_INFO(caster, hits);
+ }
+ }
+}
diff --git a/src/ZoneServer/World/Actors/Characters/Character.cs b/src/ZoneServer/World/Actors/Characters/Character.cs
index 8cc07eddf..9a80908b2 100644
--- a/src/ZoneServer/World/Actors/Characters/Character.cs
+++ b/src/ZoneServer/World/Actors/Characters/Character.cs
@@ -8,6 +8,7 @@
using Melia.Shared.Scripting;
using Melia.Shared.World;
using Melia.Zone.Buffs.Handlers.Common;
+using Melia.Zone.Buffs.Handlers.Scout.Assassin;
using Melia.Zone.Events.Arguments;
using Melia.Zone.Network;
using Melia.Zone.Scripting.AI;
@@ -770,6 +771,7 @@ public void Heal(float hpAmount, float spAmount)
// TODO: Move this somewhere else, perhaps with a hook/event?
DecreaseHeal_Debuff.TryApply(this, ref hpAmount);
+ PiercingHeart_Debuff.TryApply(this, ref hpAmount);
this.ModifyHpSafe(hpAmount, out var hp, out var priority);
this.Properties.Modify(PropertyName.SP, spAmount);
@@ -1423,6 +1425,9 @@ public bool CanAttack(ICombatEntity entity)
if (entity.IsDead)
return false;
+ if (entity.IsBuffActive(BuffId.Skill_NoDamage_Buff))
+ return false;
+
// For now, let's specify that characters can attack actual
// monsters.
var isHostileMonster = (entity is IMonster monster && monster.MonsterType == MonsterType.Mob);
diff --git a/src/ZoneServer/World/Actors/Monsters/Mob.cs b/src/ZoneServer/World/Actors/Monsters/Mob.cs
index 1bf8c2e2b..6eac91635 100644
--- a/src/ZoneServer/World/Actors/Monsters/Mob.cs
+++ b/src/ZoneServer/World/Actors/Monsters/Mob.cs
@@ -5,7 +5,9 @@
using Melia.Shared.Game.Const;
using Melia.Shared.ObjectProperties;
using Melia.Shared.World;
+using Melia.Zone.Buffs;
using Melia.Zone.Buffs.Handlers.Common;
+using Melia.Zone.Buffs.Handlers.Scout.Assassin;
using Melia.Zone.Events.Arguments;
using Melia.Zone.Network;
using Melia.Zone.Scripting;
@@ -295,6 +297,13 @@ public void Kill(ICombatEntity killer)
{
this.Properties.SetFloat(PropertyName.HP, 0);
this.Components.Get()?.Stop();
+
+ // End all of its buffs
+ if (this.Components.TryGet(out var buffComponent))
+ {
+ buffComponent.RemoveAll();
+ }
+
this.DisappearTime = DateTime.Now.AddSeconds(2);
var beneficiary = this.GetKillBeneficiary(killer);
@@ -736,6 +745,9 @@ public bool CanAttack(ICombatEntity entity)
if (entity.IsDead)
return false;
+ if (entity.IsBuffActive(BuffId.Skill_NoDamage_Buff))
+ return false;
+
// For now, let's specify that mobs can attack any combat
// entities, since we want them them to be able to attack
// both characters and other mobs.
@@ -773,6 +785,7 @@ public void Heal(float hpAmount, float spAmount)
// TODO: Move this somewhere else, perhaps with a hook/event?
DecreaseHeal_Debuff.TryApply(this, ref hpAmount);
+ PiercingHeart_Debuff.TryApply(this, ref hpAmount);
var healingModifier = Math.Max(0, 1 - healingReduction);
diff --git a/system/scripts/zone/core/calc_combat.cs b/system/scripts/zone/core/calc_combat.cs
index 412ebbcd0..b97dabb43 100644
--- a/system/scripts/zone/core/calc_combat.cs
+++ b/system/scripts/zone/core/calc_combat.cs
@@ -647,10 +647,10 @@ public float SCR_GetCritChance(ICombatEntity attacker, ICombatEntity target, Ski
return 100;
var critDodgeRate = target.Properties.GetFloat(PropertyName.CRTDR);
- var critHitRate = attacker.Properties.GetFloat(PropertyName.CRTHR) + modifier.BonusCritChance;
+ var critHitRate = attacker.Properties.GetFloat(PropertyName.CRTHR);
// Based on: https://treeofsavior.com/page/news/view.php?n=951
- var critChance = Math.Pow(Math.Max(0, Math.Max(0, critHitRate - critDodgeRate)), 0.6f);
+ var critChance = Math.Pow(Math.Max(0, Math.Max(0, critHitRate - critDodgeRate)), 0.6f) * modifier.CritRateMultiplier + modifier.BonusCritChance;
critChance = Math2.Clamp(modifier.MinCritChance, 100, critChance);