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