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);