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