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