diff --git a/src/ZoneServer/Buffs/Handlers/Common/Warrior_EnableMovingShot_Buff.cs b/src/ZoneServer/Buffs/Handlers/Common/Warrior_EnableMovingShot_Buff.cs new file mode 100644 index 000000000..91ecc505f --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Common/Warrior_EnableMovingShot_Buff.cs @@ -0,0 +1,27 @@ +using Melia.Shared.Game.Const; +using Melia.Zone.Buffs.Base; + +namespace Melia.Zone.Buffs.Handlers.Common +{ + /// + /// Handle for the Warrior_EnableMovingShot Buff, which + /// gives moving shot with a given value + /// + /// + /// NumArg1: MovingShot_BM value + /// NumArg2: None + /// + [BuffHandler(BuffId.Warrior_EnableMovingShot_Buff)] + public class Warrior_EnableMovingShot_Buff : BuffHandler + { + public override void OnStart(Buff buff) + { + AddPropertyModifier(buff, buff.Target, PropertyName.MovingShot_BM, buff.NumArg1); + } + + public override void OnEnd(Buff buff) + { + RemovePropertyModifier(buff, buff.Target, PropertyName.MovingShot_BM); + } + } +} diff --git a/src/ZoneServer/Buffs/Handlers/Swordsmen/Cataphract/Impaler_Buff.cs b/src/ZoneServer/Buffs/Handlers/Swordsmen/Cataphract/Impaler_Buff.cs new file mode 100644 index 000000000..616740aae --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Swordsmen/Cataphract/Impaler_Buff.cs @@ -0,0 +1,52 @@ +using Melia.Shared.Game.Const; +using Melia.Shared.L10N; +using Melia.Zone.Buffs.Base; +using Melia.Zone.Network; +using Melia.Zone.Skills; +using Melia.Zone.Skills.Combat; +using Melia.Zone.World.Actors; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Melia.Zone.Buffs.Handlers.Swordsmen.Cataphract +{ + /// + /// Buff handler for Impaler Buff, which triggers the slam portion of Impaler. + /// It needs to end if the skewered monster dies + /// + /// + /// caster in this case is the skewer monster + /// + [BuffHandler(BuffId.Impaler_Buff)] + public class Impaler_Buff : BuffHandler + { + /// + /// Ends the buff, activating the overheat + /// + /// + public override void OnEnd(Buff buff) + { + var target = buff.Target; + + if (target.TryGetSkill(SkillId.Cataphract_Impaler, out var skill)) + { + skill.IncreaseOverheat(); + } + } + + + /// + /// Checks to see if the entity died + /// + /// + public override void WhileActive(Buff buff) + { + var caster = buff.Caster; + var target = buff.Target; + + if (caster.IsDead) + target.StopBuff(BuffId.Impaler_Buff); + } + } + + +} diff --git a/src/ZoneServer/Buffs/Handlers/Swordsmen/Cataphract/Impaler_Debuff.cs b/src/ZoneServer/Buffs/Handlers/Swordsmen/Cataphract/Impaler_Debuff.cs new file mode 100644 index 000000000..24c7177c2 --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Swordsmen/Cataphract/Impaler_Debuff.cs @@ -0,0 +1,72 @@ +using Melia.Shared.Game.Const; +using Melia.Shared.L10N; +using Melia.Zone.Buffs.Base; +using Melia.Zone.Network; +using Melia.Zone.Skills; +using Melia.Zone.Skills.Combat; +using Melia.Zone.World.Actors; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Melia.Zone.Buffs.Handlers.Swordsmen.Cataphract +{ + /// + /// Buff handler for Impaler Debuff, which reduces def and makes it + /// impossible to block or dodge. + /// This also needs to prevent you from taking any action + /// + /// + /// NumArg1: Skill Level + /// NumArg2: None + /// + [BuffHandler(BuffId.Impaler_Debuff)] + public class Impaler_Debuff : BuffHandler, IBuffCombatDefenseBeforeCalcHandler + { + public const float DefPenalty = 0.3f; + + /// + /// Starts buff, attaching the entity to the spear + /// + /// + public override void OnStart(Buff buff) + { + var target = buff.Target; + var caster = buff.Caster; + + var reduceDef = target.Properties.GetFloat(PropertyName.DEF) * DefPenalty; + + AddPropertyModifier(buff, target, PropertyName.DEF_BM, -reduceDef); + + Send.ZC_ATTACH_TO_OBJ(target, caster, "Dummy_Impaler", "", 0.01f, 1, 1, "", 1, 0, 1); + } + + /// + /// Ends the buff, freeing the entity + /// + /// + public override void OnEnd(Buff buff) + { + var target = buff.Target; + var caster = buff.Caster; + + RemovePropertyModifier(buff, target, PropertyName.DEF_BM); + + Send.ZC_DETACH_FROM_OBJ(target, caster); + } + + + /// + /// Applies the debuff's effect during the combat calculations. + /// + /// + /// + /// + /// + /// + /// + public void OnDefenseBeforeCalc(Buff buff, ICombatEntity attacker, ICombatEntity target, Skill skill, SkillModifier modifier, SkillHitResult skillHitResult) + { + modifier.Unblockable = true; + modifier.ForcedHit = true; + } + } +} diff --git a/src/ZoneServer/Buffs/Handlers/Swordsmen/Cataphract/Trot_Buff.cs b/src/ZoneServer/Buffs/Handlers/Swordsmen/Cataphract/Trot_Buff.cs new file mode 100644 index 000000000..7baf7b1c3 --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Swordsmen/Cataphract/Trot_Buff.cs @@ -0,0 +1,74 @@ +using Melia.Shared.Game.Const; +using Melia.Shared.L10N; +using Melia.Zone.Buffs.Base; +using Melia.Zone.Network; +using Melia.Zone.Skills; +using Melia.Zone.Skills.Combat; +using Melia.Zone.World.Actors; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Melia.Zone.Buffs.Handlers.Swordsmen.Cataphract +{ + /// + /// Buff handler for Trot, which increases movement speed while riding + /// + /// + /// NumArg1: Skill Level + /// NumArg2: None + /// + [BuffHandler(BuffId.Trot_Buff)] + public class Trot_Buff : BuffHandler + { + /// + /// Starts buff, increasing movement speed + /// + /// + public override void OnStart(Buff buff) + { + var target = buff.Target; + + AddPropertyModifier(buff, target, PropertyName.MSPD_BM, GetSpeedBonus(buff)); + Send.ZC_MSPD(target); + } + + /// + /// Ends the buff, resetting movement speed + /// + /// + public override void OnEnd(Buff buff) + { + RemovePropertyModifier(buff, buff.Target, PropertyName.MSPD_BM); + Send.ZC_MSPD(buff.Target); + } + + + /// + /// Returns the speed bonus granted by the buff. + /// + /// + /// + private float GetSpeedBonus(Buff buff) + { + return 5 + buff.NumArg1; + } + + + /// + /// Drains SP over time to keep the buff active + /// + /// + public override void WhileActive(Buff buff) + { + if (!buff.Target.IsBuffActive(BuffId.RidingCompanion)) + { + buff.Target.StopBuff(buff.Id); + return; + } + if (!buff.Target.TrySpendSp(20)) + { + buff.Target.StopBuff(buff.Id); + return; + } + } + } +} diff --git a/src/ZoneServer/Network/Send.cs b/src/ZoneServer/Network/Send.cs index 7d3586044..505041d65 100644 --- a/src/ZoneServer/Network/Send.cs +++ b/src/ZoneServer/Network/Send.cs @@ -24,6 +24,7 @@ using Melia.Zone.World.Items; using Melia.Zone.World.Maps; using Yggdrasil.Extensions; +using Yggdrasil.Logging; using Yggdrasil.Util; namespace Melia.Zone.Network @@ -2256,6 +2257,34 @@ public static void ZC_OWNER(Character character, IActor actor) character.Connection.Send(packet); } + /// + /// Tells the client that a monster is a Summoned monster + /// + /// + /// + public static void ZC_IS_SUMMONING_MONSTER(IActor actor, bool isSummon) + { + var packet = new Packet(Op.ZC_IS_SUMMONING_MONSTER); + packet.PutInt(actor.Handle); + packet.PutByte(isSummon); + + actor.Map.Broadcast(packet, actor); + } + + /// + /// Tells the client that a monster is a Sorcerer Summon + /// + /// + /// + public static void ZC_IS_SUMMON_SORCERER_MONSTER(IActor actor, bool isSummon) + { + var packet = new Packet(Op.ZC_IS_SUMMON_SORCERER_MONSTER); + packet.PutInt(actor.Handle); + packet.PutByte(isSummon); + + actor.Map.Broadcast(packet, actor); + } + /// /// Draws circle area on ground at position for characters in range /// of the caster. @@ -3188,7 +3217,7 @@ public static void ZC_PLAY_ANI(IActor actor, string animationName, bool stopOnLa packet.PutByte(stopOnLastFrame); packet.PutByte(0); packet.PutFloat(0); - packet.PutFloat(1); + packet.PutFloat(1); // animation speed // [i373230] Maybe added earlier { @@ -3199,6 +3228,65 @@ public static void ZC_PLAY_ANI(IActor actor, string animationName, bool stopOnLa actor.Map.Broadcast(packet, actor); } + /// + /// Attaches an entity to an object + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static void ZC_ATTACH_TO_OBJ(IActor actor, IActor attachToEntity, string nodeName, + string packetString1, float attachDelay, long l1, long l2, string packetString2, byte b1, byte b2, byte b3) + { + if (!ZoneServer.Instance.Data.PacketStringDb.TryFind(nodeName, out var nodeNameData)) + throw new ArgumentException($"Unknown packet string '{nodeName}'."); + + if (!ZoneServer.Instance.Data.PacketStringDb.TryFind(packetString1, out var packetStringData1)) + throw new ArgumentException($"Unknown packet string '{packetString1}'."); + + if (!ZoneServer.Instance.Data.PacketStringDb.TryFind(packetString2, out var packetStringData2)) + throw new ArgumentException($"Unknown packet string '{packetString2}'."); + + var packet = new Packet(Op.ZC_ATTACH_TO_OBJ); + + packet.PutInt(actor?.Handle ?? 0); + packet.PutInt(attachToEntity?.Handle ?? 0); + packet.PutInt(nodeNameData?.Id ?? 0); + packet.PutInt(packetStringData1?.Id ?? 0); + packet.PutFloat(attachDelay); + packet.PutLong(l1); + packet.PutLong(l2); + packet.PutInt(packetStringData2?.Id ?? 0); + packet.PutByte(b1); + packet.PutByte(b2); + packet.PutByte(b3); + + actor.Map.Broadcast(packet); + } + + + /// + /// Detach from another object (usually another actor). + /// + /// + /// + public static void ZC_DETACH_FROM_OBJ(IActor actor, IActor detachFromActor) + { + var packet = new Packet(Op.ZC_DETACH_FROM_OBJ); + + packet.PutInt(actor.Handle); + packet.PutInt(detachFromActor?.Handle ?? 0); + + actor.Map.Broadcast(packet); + } + /// /// Sends ZC_COMMON_SKILL_LIST to character (dummy). /// diff --git a/src/ZoneServer/Pads/Handlers/Swordsman/Cataphract/Cataphract_SteedCharge.cs b/src/ZoneServer/Pads/Handlers/Swordsman/Cataphract/Cataphract_SteedCharge.cs new file mode 100644 index 000000000..f25bac633 --- /dev/null +++ b/src/ZoneServer/Pads/Handlers/Swordsman/Cataphract/Cataphract_SteedCharge.cs @@ -0,0 +1,40 @@ +using Melia.Shared.Game.Const; +using Melia.Zone.Network; +using Melia.Zone.Skills; +using Melia.Zone.World.Actors.Monsters; + +namespace Melia.Zone.Pads.Handlers.Swordsman.Cataphract +{ + /// + /// Handler for the Arditi_TreGranata pad, creates and disables the effect + /// + [PadHandler(PadName.Cataphract_SteedCharge)] + public class Cataphract_SteedCharge : 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, "Cataphract_SteedCharge", 0, 0, 150, true); + } + + /// + /// Called when the pad is destroyed. + /// + /// + /// + public void Destroyed(object sender, PadTriggerArgs args) + { + var pad = args.Trigger; + var creator = args.Creator; + + Send.ZC_NORMAL.PadUpdate(creator, pad, "Cataphract_SteedCharge", 0, 0, 150, false); + } + } +} diff --git a/src/ZoneServer/Skills/Combat/SkillModifier.cs b/src/ZoneServer/Skills/Combat/SkillModifier.cs index 6e0d05bff..8abdb6a8c 100644 --- a/src/ZoneServer/Skills/Combat/SkillModifier.cs +++ b/src/ZoneServer/Skills/Combat/SkillModifier.cs @@ -1,4 +1,7 @@ -namespace Melia.Zone.Skills.Combat +using Melia.Shared.Data.Database; +using Melia.Shared.Game.Const; + +namespace Melia.Zone.Skills.Combat { /// /// A class for properties that can modify the damage calculation for a skill. @@ -110,6 +113,20 @@ public class SkillModifier /// public bool ForcedCritical { get; set; } + + /// + /// Gets or sets whether or not the natural attack attribute is overwritten + /// + public bool OverrideAttackAttribute { get; set; } + + /// + /// Gets or sets the attribute for the attack + /// + /// + /// This is only read if OverrideAttackAttribute is true + /// + public SkillAttribute AttackAttribute { get; set; } + /// /// Returns a new skill modifier with default values. /// diff --git a/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_DoomSpike.cs b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_DoomSpike.cs new file mode 100644 index 000000000..a5a4fd932 --- /dev/null +++ b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_DoomSpike.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Melia.Shared.Data.Database; +using Melia.Shared.Game.Const; +using Melia.Shared.L10N; +using Melia.Shared.World; +using Melia.Zone.Network; +using Melia.Zone.Skills.Combat; +using Melia.Zone.Skills.Handlers.Base; +using Melia.Zone.Skills.SplashAreas; +using Melia.Zone.World.Actors; +using static Melia.Shared.Util.TaskHelper; +using static Melia.Zone.Skills.SkillUseFunctions; + +namespace Melia.Zone.Skills.Handlers.Swordsmen.Cataphract +{ + /// + /// Handler for the Cataphract skill Doom Spike. + /// + [SkillHandler(SkillId.Cataphract_DoomSpike)] + public class Cataphract_DoomSpike : IGroundSkillHandler + { + /// + /// Handles skill, damaging targets. + /// + /// + /// + /// + /// + public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target) + { + if (!caster.TrySpendSp(skill)) + { + caster.ServerMessage(Localization.Get("Not enough SP.")); + return; + } + + skill.IncreaseOverheat(); + caster.TurnTowards(farPos); + caster.SetAttackState(true); + + var splashParam = skill.GetSplashParameters(caster, originPos, farPos, length: 120, width: 30, angle: 0); + var splashArea = skill.GetSplashArea(SplashType.Square, splashParam); + + Send.ZC_SKILL_READY(caster, skill, originPos, farPos); + Send.ZC_SKILL_MELEE_GROUND(caster, skill, farPos, null); + + CallSafe(Attack(skill, caster, splashArea)); + } + + /// + /// Executes the actual attack after a delay. + /// + /// + /// + /// + private static async Task Attack(Skill skill, ICombatEntity caster, ISplashArea splashArea) + { + var hitDelay = TimeSpan.FromMilliseconds(200); + var damageDelay = TimeSpan.FromMilliseconds(50); + var skillHitDelay = TimeSpan.Zero; + + await Task.Delay(hitDelay); + + var bonusMultiplier = 0f; + if (caster.TryGetSkill(SkillId.Cataphract_AcrobaticMount, out var acrobaticMount)) + { + bonusMultiplier = 0.5f + acrobaticMount.Level * 0.06f; + } + + var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea); + var hits = new List(); + + foreach (var target in targets.LimitBySDR(caster, skill)) + { + var modifier = SkillModifier.MultiHit(5); + modifier.DamageMultiplier += bonusMultiplier; + + modifier.MinCritChance = MathF.Max(10 + skill.Level, modifier.MinCritChance); + + var skillHitResult = SCR_SkillHit(caster, target, skill, modifier); + target.TakeDamage(skillHitResult.Damage, caster); + + var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay); + + if (caster.IsAbilityActive(AbilityId.Cataphract30)) + { + skillHit.HitEffect = HitEffect.Impact; + } + else + { + skillHit.KnockBackInfo = new KnockBackInfo(caster.Position, target.Position, skill); + skillHit.HitInfo.Type = skill.Data.KnockDownHitType; + target.Position = skillHit.KnockBackInfo.ToPosition; + } + + hits.Add(skillHit); + } + + Send.ZC_SKILL_HIT_INFO(caster, hits); + } + } +} diff --git a/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_EarthWave.cs b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_EarthWave.cs new file mode 100644 index 000000000..75062a7ca --- /dev/null +++ b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_EarthWave.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Melia.Shared.Data.Database; +using Melia.Shared.Game.Const; +using Melia.Shared.L10N; +using Melia.Shared.World; +using Melia.Zone.Network; +using Melia.Zone.Skills.Combat; +using Melia.Zone.Skills.Handlers.Base; +using Melia.Zone.Skills.SplashAreas; +using Melia.Zone.World.Actors; +using static Melia.Shared.Util.TaskHelper; +using static Melia.Zone.Skills.SkillUseFunctions; + +namespace Melia.Zone.Skills.Handlers.Swordsmen.Cataphract +{ + /// + /// Handler for the Cataphract skill Earth Wave. + /// + [SkillHandler(SkillId.Cataphract_EarthWave)] + public class Cataphract_EarthWave : IGroundSkillHandler + { + /// + /// Handles skill, damaging targets. + /// + /// + /// + /// + /// + public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target) + { + if (!caster.TrySpendSp(skill)) + { + caster.ServerMessage(Localization.Get("Not enough SP.")); + return; + } + + skill.IncreaseOverheat(); + caster.TurnTowards(farPos); + caster.SetAttackState(true); + + var splashParam = skill.GetSplashParameters(caster, originPos, farPos, length: 35, width: 65, angle: 0); + var splashArea = skill.GetSplashArea(SplashType.Circle, splashParam); + + Send.ZC_SKILL_READY(caster, skill, originPos, farPos); + Send.ZC_SKILL_MELEE_GROUND(caster, skill, farPos, null); + + CallSafe(Attack(skill, caster, splashArea)); + } + + /// + /// Executes the actual attack after a delay. + /// + /// + /// + /// + private static async Task Attack(Skill skill, ICombatEntity caster, ISplashArea splashArea) + { + var hitDelay = TimeSpan.FromMilliseconds(550); + var damageDelay = TimeSpan.FromMilliseconds(50); + var skillHitDelay = TimeSpan.Zero; + + await Task.Delay(hitDelay); + + var bonusMultiplier = 0f; + if (caster.TryGetSkill(SkillId.Cataphract_AcrobaticMount, out var acrobaticMount)) + { + bonusMultiplier = 0.5f + acrobaticMount.Level * 0.06f; + } + + var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea); + var hits = new List(); + + foreach (var target in targets.LimitBySDR(caster, skill)) + { + var modifier = SkillModifier.MultiHit(5); + modifier.DamageMultiplier += bonusMultiplier; + + if (caster.IsAbilityActive(AbilityId.Cataphract26)) + { + modifier.OverrideAttackAttribute = true; + modifier.AttackAttribute = SkillAttribute.Earth; + } + + var skillHitResult = SCR_SkillHit(caster, target, skill, modifier); + target.TakeDamage(skillHitResult.Damage, caster); + + var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay); + skillHit.HitEffect = HitEffect.Impact; + hits.Add(skillHit); + } + + Send.ZC_SKILL_HIT_INFO(caster, hits); + } + } +} diff --git a/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_Impaler.cs b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_Impaler.cs new file mode 100644 index 000000000..01d818814 --- /dev/null +++ b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_Impaler.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Linq; +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 Microsoft.CodeAnalysis.CSharp.Syntax; +using static Melia.Shared.Util.TaskHelper; +using static Melia.Zone.Skills.SkillUseFunctions; + +namespace Melia.Zone.Skills.Handlers.Swordsmen.Cataphract +{ + /// + /// Handler for the Cataphract skill Impaler. + /// + [SkillHandler(SkillId.Cataphract_Impaler)] + public class Cataphract_Impaler : IGroundSkillHandler + { + /// + /// Handles skill, damaging targets. + /// + /// + /// + /// + /// + public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target) + { + if (!caster.TrySpendSp(skill)) + { + caster.ServerMessage(Localization.Get("Not enough SP.")); + return; + } + + caster.TurnTowards(farPos); + caster.SetAttackState(true); + + if (!caster.IsBuffActive(BuffId.Impaler_Buff)) + { + var grabParam = skill.GetSplashParameters(caster, originPos, farPos, length: 15, width: 50, angle: 0); + var grabArea = skill.GetSplashArea(SplashType.Circle, grabParam); + + Send.ZC_SKILL_READY(caster, skill, originPos, farPos); + Send.ZC_SKILL_MELEE_GROUND(caster, skill, farPos, null); + + CallSafe(Grab(skill, caster, grabArea)); + } + else + { + var slamParam = skill.GetSplashParameters(caster, originPos, farPos, length: 25, width: 35, angle: 0); + var slamArea = skill.GetSplashArea(SplashType.Circle, slamParam); + + Send.ZC_SKILL_READY(caster, skill, originPos, farPos); + Send.ZC_SKILL_MELEE_GROUND(caster, skill, farPos, null); + + CallSafe(Slam(skill, caster, slamArea)); + } + } + + /// + /// Attempts to grab a target + /// + /// + /// + /// + private static async Task Grab(Skill skill, ICombatEntity caster, ISplashArea splashArea) + { + var hitDelay = TimeSpan.FromMilliseconds(250); + var damageDelay = TimeSpan.FromMilliseconds(50); + var skillHitDelay = TimeSpan.Zero; + + await Task.Delay(hitDelay); + + var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea); + var hits = new List(); + + var grabbedSomething = false; + + foreach (var target in targets.LimitBySDR(caster, skill)) + { + var targetSize = target.EffectiveSize; + + if (targetSize != SizeType.PC && targetSize >= SizeType.L) + continue; + + target.StartBuff(BuffId.Impaler_Debuff, skill.Level, 0, TimeSpan.FromSeconds(10), caster); + var impalerBuff = caster.StartBuff(BuffId.Impaler_Buff, skill.Level, 0, TimeSpan.FromSeconds(10), target); + + impalerBuff.UpdateTime = TimeSpan.FromMilliseconds(100); + grabbedSomething = true; + break; + } + + // Only overheat here on a miss + if (!grabbedSomething) + { + skill.IncreaseOverheat(); + return; + } + } + + /// + /// Slam the target to deal damage + /// + /// + /// This does not play the right animation. It should play SKL_IMPALER_THROW_RIDE. + /// + /// + /// + /// + private static async Task Slam(Skill skill, ICombatEntity caster, ISplashArea splashArea) + { + var hitDelay = TimeSpan.FromMilliseconds(200); + var damageDelay = TimeSpan.FromMilliseconds(50); + var skillHitDelay = TimeSpan.Zero; + + await Task.Delay(hitDelay); + + var bonusMultiplier = 0f; + if (caster.TryGetSkill(SkillId.Cataphract_AcrobaticMount, out var acrobaticMount)) + { + bonusMultiplier = 0.5f + acrobaticMount.Level * 0.06f; + } + + var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea); + var hits = new List(); + + // remove the skewered mob from the list of targets if they're present + // They get hit separately at the end. + if (caster.TryGetBuff(BuffId.Impaler_Buff, out var buff)) + { + var skewered = buff.Caster; + targets.Remove(skewered); + + + foreach (var target in targets.LimitBySDR(caster, skill)) + { + var modifier = SkillModifier.MultiHit(3); + modifier.DamageMultiplier += bonusMultiplier; + + if (caster.IsAbilityActive(AbilityId.Cataphract26)) + { + modifier.OverrideAttackAttribute = true; + modifier.AttackAttribute = SkillAttribute.Earth; + } + + var skillHitResult = SCR_SkillHit(caster, target, skill, modifier); + target.TakeDamage(skillHitResult.Damage, caster); + + var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay); + skillHit.HitEffect = HitEffect.Impact; + hits.Add(skillHit); + } + + + var modifier2 = SkillModifier.MultiHit(3); + modifier2.ForcedCritical = true; + modifier2.DamageMultiplier += bonusMultiplier; + + var skillHitResult2 = SCR_SkillHit(caster, skewered, skill, modifier2); + skewered.TakeDamage(skillHitResult2.Damage, caster); + + var skillHit2 = new SkillHitInfo(caster, skewered, skill, skillHitResult2, damageDelay, skillHitDelay); + skillHit2.HitEffect = HitEffect.Impact; + hits.Add(skillHit2); + + } + + Send.ZC_SKILL_HIT_INFO(caster, hits); + } + } +} diff --git a/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_Rush.cs b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_Rush.cs new file mode 100644 index 000000000..124036d68 --- /dev/null +++ b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_Rush.cs @@ -0,0 +1,166 @@ +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.ObjectProperties; +using Melia.Shared.World; +using Melia.Zone.Network; +using Melia.Zone.Skills.Combat; +using Melia.Zone.Skills.Handlers.Base; +using Melia.Zone.World.Actors; +using Melia.Zone.World.Actors.Characters; +using Yggdrasil.Logging; +using Yggdrasil.Util; +using static Melia.Shared.Util.TaskHelper; +using static Melia.Zone.Skills.SkillUseFunctions; + +namespace Melia.Zone.Skills.Handlers.Swordsmen.Cataphract +{ + /// + /// Handler for the Cataphract skill Rush. + /// + [SkillHandler(SkillId.Cataphract_Rush)] + public class Cataphract_Rush : IGroundSkillHandler, IDynamicCasted + { + public void StartDynamicCast(Skill skill, ICombatEntity caster) + { + caster.StartBuff(BuffId.Warrior_RushMove_Buff, 1, 0, TimeSpan.Zero, caster); + caster.StartBuff(BuffId.Warrior_EnableMovingShot_Buff, 2, 0, TimeSpan.Zero, caster); + + // This has to be sent here to set the MovingShotable Property + // which isn't applied until after the character has the buff + if (caster is Character character) + { + Send.ZC_OBJECT_PROPERTY(character); + Send.ZC_MOVE_SPEED(character); + } + + Send.ZC_PLAY_SOUND(caster, "voice_war_atk_long_cast"); + } + + /// + /// Called when the user stops casting the skill. + /// + /// + /// + public void EndDynamicCast(Skill skill, ICombatEntity caster) + { + caster.StopBuff(BuffId.Warrior_RushMove_Buff); + caster.StopBuff(BuffId.Warrior_EnableMovingShot_Buff); + + if (caster is Character character) + { + Send.ZC_OBJECT_PROPERTY(character); + Send.ZC_MOVE_SPEED(character); + } + + Send.ZC_STOP_SOUND(caster, "voice_war_atk_long_cast"); + } + + + /// + /// Handles skill, damaging targets. + /// + /// + /// + /// + /// + public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target) + { + skill.IncreaseOverheat(); + + Send.ZC_SKILL_READY(caster, skill, originPos, farPos); + Send.ZC_SKILL_MELEE_GROUND(caster, skill, farPos, null); + + CallSafe(this.Attack(skill, caster)); + } + + /// + /// Executes the actual attack after a delay. + /// + /// + /// + private async Task Attack(Skill skill, ICombatEntity caster) + { + var hits = new List(); + var hitDelay = TimeSpan.FromMilliseconds(30); + var damageDelay = TimeSpan.FromMilliseconds(50); + var skillHitDelay = TimeSpan.Zero; + + var totalHits = 10; + var attackWidth = 40f; + var delayBetweenRepeats = TimeSpan.FromMilliseconds(270); + + for (var i = 0; i < totalHits; ++i) + { + await Task.Delay(hitDelay); + + if (!caster.TrySpendSp(10 + skill.Level)) + { + caster.ServerMessage(Localization.Get("Not enough SP.")); + Send.ZC_SKILL_CAST_CANCEL(caster); + return; + } + + if (caster.IsAbilityActive(AbilityId.Cataphract34)) + { + // TODO: this art changes this move into a kind of vaccuum effect + } + + var bonusMultiplier = 0f; + if (caster.TryGetSkill(SkillId.Cataphract_AcrobaticMount, out var acrobaticMount)) + { + bonusMultiplier = 0.5f + acrobaticMount.Level * 0.06f; + } + + var splashParam = skill.GetSplashParameters(caster, caster.Position, caster.Position, length: 0, width: attackWidth, angle: 0); + var splashArea = skill.GetSplashArea(SplashType.Circle, splashParam); + var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea); + + foreach (var target in targets.LimitBySDR(caster, skill)) + { + var modifier = SkillModifier.Default; + modifier.Unblockable = true; + modifier.DamageMultiplier += bonusMultiplier; + + // double crit rate if they have fear + if (target.IsBuffActive(BuffId.Common_Fear)) + { + modifier.BonusCritChance = caster.Properties.GetFloat(PropertyName.CRTHR); + } + + if (caster.TryGetActiveAbilityLevel(AbilityId.Cataphract1, out var level)) + { + var stunChance = level * 5f; + if (RandomProvider.Get().Next(100) < stunChance) + { + target.StartBuff(BuffId.Stun, TimeSpan.FromSeconds(2)); + } + } + + var skillHitResult = SCR_SkillHit(caster, target, skill, modifier); + target.TakeDamage(skillHitResult.Damage, caster); + + var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay); + skillHit.HitEffect = HitEffect.Impact; + + hits.Add(skillHit); + } + + Send.ZC_SKILL_HIT_INFO(caster, hits); + + if (i < totalHits - 1) + await Task.Delay(delayBetweenRepeats); + + hits.Clear(); + + if (!caster.IsCasting()) + break; + } + + Send.ZC_SKILL_DISABLE(caster as Character); + } + } +} diff --git a/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_SteedCharge.cs b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_SteedCharge.cs new file mode 100644 index 000000000..0f9e8f792 --- /dev/null +++ b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_SteedCharge.cs @@ -0,0 +1,228 @@ +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.Monsters; +using Melia.Zone.World.Actors.Pads; +using Yggdrasil.Logging; +using static Melia.Shared.Util.TaskHelper; +using static Melia.Zone.Skills.SkillUseFunctions; + +namespace Melia.Zone.Skills.Handlers.Swordsmen.Cataphract +{ + /// + /// Handler for the Cataphract skill Steed Charge. + /// + [SkillHandler(SkillId.Cataphract_SteedCharge)] + public class Cataphract_SteedCharge : IGroundSkillHandler + { + public const float JumpDistance = 175f; + public const float AttackDistance = 25f; + + /// + /// Handles skill, damaging targets. + /// + /// + /// + /// + /// + public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target) + { + if (!caster.TrySpendSp(skill)) + { + caster.ServerMessage(Localization.Get("Not enough SP.")); + return; + } + + skill.IncreaseOverheat(); + // Note that the "Direction" sent with the attack packet for + // this skill is actually wrong, so we have to use the farpos + caster.TurnTowards(farPos); + caster.SetAttackState(true); + + var jumpDistance = JumpDistance; + var attackDistance = AttackDistance; + + // Cataphract40 makes it go only half as far + if (caster.IsAbilityActive(AbilityId.Cataphract40)) + { + jumpDistance /= 2f; + attackDistance /= 2f; + } + + var targetPos = caster.Position.GetRelative(caster.Direction, jumpDistance); + + targetPos = caster.Map.Ground.GetLastValidPosition(caster.Position, targetPos); + var actualDistance = (float)caster.Position.Get2DDistance(targetPos); + + var splashParam = skill.GetSplashParameters(caster, originPos, targetPos, length: actualDistance + attackDistance, width: 28, angle: 0); + var splashArea = skill.GetSplashArea(SplashType.Square, splashParam); + + Send.ZC_SKILL_READY(caster, skill, originPos, targetPos); + Send.ZC_SKILL_MELEE_GROUND(caster, skill, targetPos, null); + + Send.ZC_NORMAL.LeapJump(caster, targetPos, 0, 0, 1f, 0.35f, 0.7f, 0); + + CallSafe(Attack(skill, caster, splashArea)); + + // Spawn Fire Pads if this is the Fire Chariot version + if (caster.IsAbilityActive(AbilityId.Cataphract40)) + { + // The visual of the pad extends 25 units behind the + // pad's position. The second pad doesn't include + // this part in its hit area so they don't overlap. + var pad1StartPosition = originPos.GetRelative(caster.Direction.Backwards, 25f); + var pad1Param = skill.GetSplashParameters(caster, pad1StartPosition, targetPos, length: 75, width: 28, angle: 0); + var pad1Area = skill.GetSplashArea(SplashType.Square, pad1Param); + var pad1 = new Pad(PadName.Cataphract_SteedCharge, caster, skill, pad1Area); + pad1.Position = originPos; + pad1.Trigger.LifeTime = TimeSpan.FromSeconds(5); + pad1.Trigger.UpdateInterval = TimeSpan.FromSeconds(1); + pad1.Trigger.MaxActorCount = 15; + caster.Map.AddPad(pad1); + pad1.Trigger.Subscribe(TriggerType.Update, this.OnTriggerUpdate); + + // We don't spawn the second pad if the jump is cut short + if (actualDistance >= 75f) + { + var pad2StartPosition = originPos.GetRelative(caster.Direction, 50f); + var pad2Param = skill.GetSplashParameters(caster, pad2StartPosition, targetPos, length: 50, width: 28, angle: 0); + var pad2Area = skill.GetSplashArea(SplashType.Square, pad1Param); + var pad2 = new Pad(PadName.Cataphract_SteedCharge, caster, skill, pad2Area); + pad2.Position = originPos.GetRelative(caster.Direction, 50f); + pad2.Trigger.LifeTime = TimeSpan.FromSeconds(5); + pad2.Trigger.UpdateInterval = TimeSpan.FromSeconds(1); + pad2.Trigger.MaxActorCount = 15; + caster.Map.AddPad(pad2); + pad2.Trigger.Subscribe(TriggerType.Update, this.OnTriggerUpdate); + } + } + } + + /// + /// Executes the actual attack after a delay. + /// + /// + /// + /// + private static async Task Attack(Skill skill, ICombatEntity caster, ISplashArea splashArea) + { + var hitDelay = TimeSpan.FromMilliseconds(200); + var damageDelay = TimeSpan.FromMilliseconds(50); + var skillHitDelay = TimeSpan.Zero; + + await Task.Delay(hitDelay); + + var bonusMultiplier = 0f; + if (caster.TryGetSkill(SkillId.Cataphract_AcrobaticMount, out var acrobaticMount)) + { + bonusMultiplier = 0.5f + acrobaticMount.Level * 0.06f; + } + + var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea); + var hits = new List(); + + foreach (var target in targets.LimitBySDR(caster, skill)) + { + var modifier = SkillModifier.MultiHit(3); + modifier.DamageMultiplier += bonusMultiplier; + + // Always criticals against slowed targets + if (target.IsBuffActive(BuffId.Common_Slow) && caster.IsAbilityActive(AbilityId.Cataphract27)) + { + modifier.ForcedCritical = true; + } + + if (caster.IsAbilityActive(AbilityId.Cataphract40)) + { + modifier.OverrideAttackAttribute = true; + modifier.AttackAttribute = SkillAttribute.Fire; + } + + var skillHitResult = SCR_SkillHit(caster, target, skill, modifier); + target.TakeDamage(skillHitResult.Damage, caster); + + var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay); + + if (caster.IsAbilityActive(AbilityId.Cataphract29)) + { + skillHit.HitEffect = HitEffect.Impact; + } + else + { + skillHit.KnockBackInfo = new KnockBackInfo(caster.Position, target.Position, skill); + skillHit.HitInfo.Type = HitType.KnockBack; + target.Position = skillHit.KnockBackInfo.ToPosition; + } + + hits.Add(skillHit); + + // Inflicts 3 second slow on critical hit only + // Presumably this doesn't apply if Cataphract27 is on + if (!caster.IsAbilityActive(AbilityId.Cataphract27)) + { + if (skillHitResult.Result == HitResultType.Crit) + { + target.StartBuff(BuffId.Common_Slow, skill.Level, 0, TimeSpan.FromSeconds(3), caster); + } + } + } + + Send.ZC_SKILL_HIT_INFO(caster, hits); + } + + /// + /// Called when an actor enters the area of the attack. + /// + /// + /// + private void OnTriggerUpdate(object sender, PadTriggerArgs args) + { + var pad = args.Trigger; + var caster = args.Creator; + var skill = args.Skill; + + Log.Warning("Flame tick"); + + var targets = pad.Trigger.GetAttackableEntities(caster); + + foreach (var target in targets.LimitBySDR(caster, skill)) + FireDamage(skill, caster, target); + } + + + /// + /// Deals the damage from the pad + /// + /// + /// + /// + private static void FireDamage(Skill skill, ICombatEntity caster, ICombatEntity target) + { + var damageDelay = TimeSpan.FromMilliseconds(50); + var skillHitDelay = TimeSpan.Zero; + + + var modifier = SkillModifier.Default; + modifier.OverrideAttackAttribute = true; + modifier.AttackAttribute = SkillAttribute.Fire; + + var skillHitResult = SCR_SkillHit(caster, target, skill, modifier); + + target.TakeDamage(skillHitResult.Damage / 2, caster); + + var skillHit = new SkillHitInfo(caster, target, skill, skillHitResult, damageDelay, skillHitDelay); + skillHit.HitEffect = HitEffect.Impact; + + Send.ZC_SKILL_HIT_INFO(caster, skillHit); + } + } +} diff --git a/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_Trot.cs b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_Trot.cs new file mode 100644 index 000000000..960cadd62 --- /dev/null +++ b/src/ZoneServer/Skills/Handlers/Swordsmen/Cataphract/Cataphract_Trot.cs @@ -0,0 +1,52 @@ +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.Cataphract +{ + /// + /// Handler for the Cataphract skill Trot. + /// + [SkillHandler(SkillId.Cataphract_Trot)] + public class Cataphract_Trot : ISelfSkillHandler + { + /// + /// Handles skill, applying the buff to the caster. + /// + /// + /// + /// + /// + public void Handle(Skill skill, ICombatEntity caster, Position originPos, Direction dir) + { + skill.IncreaseOverheat(); + caster.SetAttackState(true); + + var target = caster; + + // This functions like a stance, it's removed if you cast it again + + if (target.IsBuffActive(BuffId.Trot_Buff)) + { + target.StopBuff(BuffId.Trot_Buff); + } + else + { + // It's not clear if this consumes SP on cast, but it probably + // should so you can't just keep reapplying it when out of SP. + if (!caster.TrySpendSp(20)) + { + caster.ServerMessage(Localization.Get("Not enough SP.")); + return; + } + target.StartBuff(BuffId.Trot_Buff, skill.Level, 0, TimeSpan.Zero, caster); + } + + Send.ZC_SKILL_MELEE_TARGET(caster, skill, target, null); + } + } +} diff --git a/system/scripts/zone/core/calc_combat.cs b/system/scripts/zone/core/calc_combat.cs index c4d5c57d4..71743440a 100644 --- a/system/scripts/zone/core/calc_combat.cs +++ b/system/scripts/zone/core/calc_combat.cs @@ -277,6 +277,9 @@ public float SCR_AttributeMultiplier(ICombatEntity attacker, ICombatEntity targe var attackerAttr = skill.Data.Attribute; var targetAttr = target.Attribute; + if (modifier.OverrideAttackAttribute) + attackerAttr = modifier.AttackAttribute; + if (!Feature.IsEnabled("AttributeBonusRevamp")) { if (attackerAttr == SkillAttribute.Fire)