diff --git a/src/Shared/Data/Database/PacketStrings.cs b/src/Shared/Data/Database/PacketStrings.cs index de86a5fa1..a6d3c4aab 100644 --- a/src/Shared/Data/Database/PacketStrings.cs +++ b/src/Shared/Data/Database/PacketStrings.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Newtonsoft.Json.Linq; using Yggdrasil.Data.JSON; @@ -16,6 +17,17 @@ public class PacketStringData /// public class PacketStringDb : DatabaseJsonIndexed { + /// + /// Tries to find a Packet string data for a given id. + /// + /// + /// + public bool TryFind(int id, out PacketStringData data) + { + data = this.Entries.Values.FirstOrDefault(a => a.Id == id); + return data != null; + } + /// /// Reads given entry and adds it to the database. /// diff --git a/src/Shared/Data/Database/SkillTree.cs b/src/Shared/Data/Database/SkillTree.cs index 9bfacecb6..ec6f15c3d 100644 --- a/src/Shared/Data/Database/SkillTree.cs +++ b/src/Shared/Data/Database/SkillTree.cs @@ -25,7 +25,7 @@ public class SkillTreeDb : DatabaseJson /// level. /// /// - /// + /// /// public SkillTreeData[] FindSkills(JobId jobId, int jobLevel) { diff --git a/src/Shared/Game/Const/HitType.cs b/src/Shared/Game/Const/HitType.cs index d61cf9783..d4e979cbe 100644 --- a/src/Shared/Game/Const/HitType.cs +++ b/src/Shared/Game/Const/HitType.cs @@ -8,5 +8,6 @@ public enum HitType : short KnockBack = 3, KnockDown = 4, Type18 = 18, + Type33 = 33, } } diff --git a/src/Shared/Network/NormalOp.cs b/src/Shared/Network/NormalOp.cs index 597fda8d6..2cd540bb1 100644 --- a/src/Shared/Network/NormalOp.cs +++ b/src/Shared/Network/NormalOp.cs @@ -41,6 +41,7 @@ public static class Zone public const int PlayEffect = 0x16; public const int PlayForceEffect = 0x17; public const int UpdateSkillEffect = 0x1F; + public const int UpdateModelColor = 0x20; public const int FadeOut = 0x38; public const int BarrackSlotCount = 0x3C; public const int AttackCancel = 0x41; @@ -69,6 +70,8 @@ public static class Zone public const int PlayTextEffect = 0xE3; public const int Unknown_E4 = 0xE7; public const int Unknown_EF = 0xF2; + public const int EnableUseSkillWhileOutOfBody = 0x10B; + public const int EndOutOfBodyBuff = 0x10C; public const int ChannelTraffic = 0x12D; public const int SetGreetingMessage = 0x136; public const int Unk13E = 0x13E; diff --git a/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Anila_Buff.cs b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Anila_Buff.cs new file mode 100644 index 000000000..baf01ae9f --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Anila_Buff.cs @@ -0,0 +1,153 @@ +using System; +using Melia.Shared.Game.Const; +using Melia.Zone.Buffs.Base; +using Melia.Zone.Network; +using Melia.Zone.Skills.Combat; +using Melia.Zone.Skills; +using Melia.Zone.Skills.SplashAreas; +using Melia.Zone.World.Actors; +using Melia.Zone.World.Actors.Characters; +using Melia.Zone.World.Actors.Pads; +using Melia.Zone.World.Actors.Monsters; +using static Melia.Zone.Skills.SkillUseFunctions; +using Melia.Shared.World; + +namespace Melia.Zone.Buffs.Handlers.Clerics.Sadhu +{ + /// + /// Handler for the Out Of Body Experience (OOBE) Anila Buff + /// which makes the character go back to original position after a while + /// and leave an effect that damages enemies on hit by a wave effect. + /// + [BuffHandler(BuffId.OOBE_Anila_Buff)] + public class OOBE_Anila_Buff : Sadhu_BuffHandler_Base + { + /// + /// Starts buff, adding an event handler for + /// the death of the dummy character. + /// + /// + /// + public override void OnActivate(Buff buff, ActivationType activationType) + { + var caster = buff.Caster; + + AddPropertyModifier(buff, caster, PropertyName.MSPD_BM, (int)buff.NumArg1); + + // Don't continue if the caster is not a Character + if (caster is not Character casterCharacter) + return; + + var dummyCharacter = casterCharacter.Map.GetCharacter((int)buff.NumArg2); + + if (dummyCharacter != null) + dummyCharacter.Died += this.OnDummyDied; + } + + /// + /// Executes the buff handler's end behavior. + /// Does not actually end or remove the buff. + /// + /// + public override void OnEnd(Buff buff) + { + var caster = buff.Caster; + + RemovePropertyModifier(buff, caster, PropertyName.MSPD_BM); + + // Ignore if the caster is not a Character + if (caster is not Character casterCharacter) + return; + + // It spawns an clone character in form of a spirit that is controlled by AI. + // So we need to identify the Source character that will be used on the buff ending. + var characterSkillHandling = casterCharacter is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + // Don't continue if the caster doesn't have the skill. + if (!characterSkillHandling.TryGetSkill(SkillId.Sadhu_Anila, out var skill)) + return; + + characterSkillHandling.SetAttackState(true); + + // Creates the Pad that will handles the buff ending effect that damages enemies that touches it. + var pad = new Pad(PadName.Sadhu_Anila_Effect_Pad, characterSkillHandling, skill, new Square(caster.Position, caster.Direction, 50, 65)); + + pad.Position = new Position(pad.Trigger.Area.Center.X, caster.Position.Y, pad.Trigger.Area.Center.Y); + pad.Trigger.MaxActorCount = 7; + pad.Trigger.LifeTime = TimeSpan.FromSeconds(10); + pad.Trigger.Subscribe(TriggerType.Enter, this.OnCollisionEnter); + + caster.Map.AddPad(pad); + + // [Arts] Spirit Expert: Wandering Soul - AI Controlled Spirit + if (casterCharacter is DummyCharacter dummyCharacter2 && dummyCharacter2.Owner.IsAbilityActive(AbilityId.Sadhu35)) + { + Send.ZC_SKILL_READY(dummyCharacter2.Owner, caster, skill, caster.Position, caster.Position); + Send.ZC_NORMAL.UpdateSkillEffect(dummyCharacter2.Owner, caster.Handle, caster.Position, caster.Direction, Position.Zero); + Send.ZC_SKILL_MELEE_GROUND(dummyCharacter2.Owner, caster, skill, caster.Position, ForceId.GetNew(), null); + } + else // Spirit was controlled by the player + { + skill.IncreaseOverheat(); + } + + // [Arts] Spirit Expert: Wandering Soul: AI Controlled Spirit + // Removes the clone character (spirit) from the map. + if (casterCharacter is DummyCharacter dummyCharacter3 && dummyCharacter3.Owner.IsAbilityActive(AbilityId.Sadhu35)) + { + this.RemoveSpritCloneCharacter(dummyCharacter3); + return; + } + + Send.ZC_NORMAL.EndOutOfBodyBuff(casterCharacter, BuffId.OOBE_Anila_Buff); + Send.ZC_NORMAL.UpdateModelColor(casterCharacter, 255, 255, 255, 255, 0.01f); + Send.ZC_PLAY_SOUND(casterCharacter, "skl_eff_yuchae_end_2"); + + this.ReturnToBody(casterCharacter, (int)buff.NumArg2); + } + + /// + /// Called when an actor enters the skill's pad area. + /// + /// + /// + private void OnCollisionEnter(object sender, PadTriggerActorArgs args) + { + var pad = args.Trigger; + var creator = args.Creator; + var target = args.Initiator; + + if (pad.Trigger.AtCapacity) + return; + + if (!creator.CanAttack(target)) + return; + + this.Attack(pad.Skill, creator, target); + } + + /// + /// Pad's attack handler. + /// + /// + /// + /// + private void Attack(Skill skill, ICombatEntity caster, ICombatEntity target) + { + var dealsDamageCharacter = caster is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + var modifier = SkillModifier.MultiHit(3); + var skillHitResult = SCR_SkillHit(dealsDamageCharacter, target, skill, modifier); + + target.TakeDamage(skillHitResult.Damage, dealsDamageCharacter); + + var hit = new HitInfo(dealsDamageCharacter, target, skill, skillHitResult, TimeSpan.FromMilliseconds(200)); + + Send.ZC_HIT_INFO(dealsDamageCharacter, target, hit); + } + } +} diff --git a/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Moksha_Buff.cs b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Moksha_Buff.cs new file mode 100644 index 000000000..0675697bf --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Moksha_Buff.cs @@ -0,0 +1,210 @@ +using System; +using Melia.Shared.Game.Const; +using Melia.Zone.Buffs.Base; +using Melia.Zone.Network; +using Melia.Zone.Skills.Combat; +using Melia.Zone.Skills; +using Melia.Zone.Skills.SplashAreas; +using Melia.Zone.World.Actors; +using Melia.Zone.World.Actors.Characters; +using Melia.Zone.World.Actors.Pads; +using Melia.Zone.World.Actors.Monsters; +using static Melia.Zone.Skills.SkillUseFunctions; +using Melia.Shared.World; + +namespace Melia.Zone.Buffs.Handlers.Clerics.Sadhu +{ + /// + /// Handler for the Out Of Body Experience (OOBE) Anila Buff + /// which makes the character go back to original position after a while + /// and leave an effect that damages enemies on hit by a wave effect. + /// + [BuffHandler(BuffId.OOBE_Moksha_Buff)] + public class OOBE_Moksha_Buff : Sadhu_BuffHandler_Base + { + /// + /// Starts buff, adding an event handler for + /// the death of the dummy character. + /// + /// + /// + public override void OnActivate(Buff buff, ActivationType activationType) + { + var caster = buff.Caster; + + AddPropertyModifier(buff, caster, PropertyName.MSPD_BM, (int)buff.NumArg1); + + // Don't continue if the caster is not a Character + if (caster is not Character casterCharacter) + return; + + var dummyCharacter = casterCharacter.Map.GetCharacter((int)buff.NumArg2); + + if (dummyCharacter != null) + dummyCharacter.Died += this.OnDummyDied; + } + + /// + /// Executes the buff handler's end behavior. + /// Does not actually end or remove the buff. + /// + /// + public override void OnEnd(Buff buff) + { + var caster = buff.Caster; + + RemovePropertyModifier(buff, caster, PropertyName.MSPD_BM); + + // Ignore if the caster is not a Character + if (caster is not Character casterCharacter) + return; + + // It spawns an clone character in form of a spirit that is controlled by AI. + // So we need to identify the Source character that will be used on the buff ending. + var characterSkillHandling = casterCharacter is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + // Don't continue if the caster doesn't have the skill. + if (!characterSkillHandling.TryGetSkill(SkillId.Sadhu_Moksha, out var skill)) + return; + + characterSkillHandling.SetAttackState(true); + + // Creates the Pad that will handles the buff ending effect that damages while enemies are inside. + var pad = new Pad(PadName.Sadhu_Moksha_Pad, characterSkillHandling, skill, new Circle(caster.Position, 100)); + + pad.Position = caster.Position; + pad.Trigger.MaxActorCount = 10; + pad.Trigger.LifeTime = TimeSpan.FromSeconds(5); + pad.Trigger.UpdateInterval = TimeSpan.FromSeconds(1); + pad.Trigger.Subscribe(TriggerType.Update, this.OnUpdate); + pad.Trigger.Subscribe(TriggerType.Destroy, this.OnDestroyPad); + + caster.Map.AddPad(pad); + + // [Arts] Spirit Expert: Wandering Soul - AI Controlled Spirit + if (casterCharacter is DummyCharacter dummyCharacter2 && dummyCharacter2.Owner.IsAbilityActive(AbilityId.Sadhu35)) + { + Send.ZC_SKILL_READY(dummyCharacter2.Owner, caster, skill, caster.Position, caster.Position); + Send.ZC_NORMAL.UpdateSkillEffect(dummyCharacter2.Owner, caster.Handle, caster.Position, caster.Direction, Position.Zero); + Send.ZC_SKILL_MELEE_GROUND(dummyCharacter2.Owner, caster, skill, caster.Position, ForceId.GetNew(), null); + } + else // Spirit was controlled by the player + { + skill.IncreaseOverheat(); + } + + // [Arts] Spirit Expert: Wandering Soul: AI Controlled Spirit + // Removes the clone character (spirit) from the map. + if (casterCharacter is DummyCharacter dummyCharacter3 && dummyCharacter3.Owner.IsAbilityActive(AbilityId.Sadhu35)) + { + this.RemoveSpritCloneCharacter(dummyCharacter3); + return; + } + + Send.ZC_NORMAL.EndOutOfBodyBuff(casterCharacter, BuffId.OOBE_Moksha_Buff); + Send.ZC_NORMAL.UpdateModelColor(casterCharacter, 255, 255, 255, 255, 0.01f); + Send.ZC_PLAY_SOUND(casterCharacter, "skl_eff_yuchae_end_2"); + + this.ReturnToBody(casterCharacter, (int)buff.NumArg2); + } + + /// + /// Called in regular intervals while the pad is on a map. + /// + /// + /// + private void OnUpdate(object sender, PadTriggerArgs args) + { + var pad = args.Trigger; + var caster = args.Creator; + var skill = args.Skill; + + var targets = pad.Trigger.GetAttackableEntities(caster); + + // The explosion has its own maximum target count which is separate from the skill + var maxTargets = pad.Trigger.MaxActorCount; + + if (ZoneServer.Instance.Conf.World.DisableSDR) + maxTargets = int.MaxValue; + + foreach (var target in targets.LimitRandom(maxTargets)) + { + this.Attack(skill, caster, target); + } + } + + /// + /// Executes end attack when the pad ends. + /// + /// + /// + private void OnDestroyPad(object sender, PadTriggerArgs args) + { + var pad = args.Trigger; + var creator = args.Creator; + + this.EndAttack(pad.Skill, creator, (ISplashArea)pad.Area); + } + + /// + /// Pad's attack handler. + /// + /// + /// + /// + private void Attack(Skill skill, ICombatEntity caster, ICombatEntity target) + { + var dealsDamageCharacter = caster is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + var skillHitResult = SCR_SkillHit(dealsDamageCharacter, target, skill); + + target.TakeDamage(skillHitResult.Damage, dealsDamageCharacter); + + var hit = new HitInfo(dealsDamageCharacter, target, skill, skillHitResult, TimeSpan.FromMilliseconds(0)); + + Send.ZC_HIT_INFO(dealsDamageCharacter, target, hit); + } + + /// + /// Executes the end attack when the skill's pad ends. + /// + /// + /// + /// + private void EndAttack(Skill skill, ICombatEntity caster, ISplashArea splashArea) + { + var damageDelay = TimeSpan.FromMilliseconds(50); + var skillHitDelay = TimeSpan.Zero; + + var targets = caster.Map.GetAttackableEntitiesIn(caster, splashArea); + + // The explosion has its own maximum target count which is separate from the skill + var maxTargets = 10; + + if (ZoneServer.Instance.Conf.World.DisableSDR) + maxTargets = int.MaxValue; + + foreach (var target in targets.LimitRandom(maxTargets)) + { + var dealsDamageCharacter = caster is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + var skillHitResult = SCR_SkillHit(dealsDamageCharacter, target, skill); + + target.TakeDamage(skillHitResult.Damage, dealsDamageCharacter); + + // 6 Consecutive hits instead of a single packet + for (int i = 0; i < 6; i++) + { + var hit = new HitInfo(dealsDamageCharacter, target, skill, skillHitResult, TimeSpan.FromMilliseconds(i * 150)); + Send.ZC_HIT_INFO(caster, dealsDamageCharacter, hit); + } + } + } + } +} diff --git a/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Patati_Buff.cs b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Patati_Buff.cs new file mode 100644 index 000000000..c423e5547 --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Patati_Buff.cs @@ -0,0 +1,175 @@ +using System; +using Yggdrasil.Util; +using Melia.Shared.Game.Const; +using Melia.Zone.Buffs.Base; +using Melia.Zone.Network; +using Melia.Zone.Skills.Combat; +using Melia.Zone.Skills; +using Melia.Zone.World.Actors; +using Melia.Zone.World.Actors.Characters; +using Melia.Zone.Skills.SplashAreas; +using Melia.Shared.World; +using static Melia.Zone.Skills.SkillUseFunctions; + +namespace Melia.Zone.Buffs.Handlers.Clerics.Sadhu +{ + /// + /// Handler for the Out Of Body Experience (OOBE) Patati Buff + /// which makes the character go back to original position + /// after a while and creates an effect that damages enemies + /// inside within a chance of knocking-down them. + /// + [BuffHandler(BuffId.OOBE_Patati_Buff)] + public class OOBE_Patati_Buff : Sadhu_BuffHandler_Base + { + private const int MaxTargets = 10; + + /// + /// Starts buff, adding an event handler for + /// the death of the dummy character. + /// + /// + /// + public override void OnActivate(Buff buff, ActivationType activationType) + { + var caster = buff.Caster; + + AddPropertyModifier(buff, caster, PropertyName.MSPD_BM, (int)buff.NumArg1); + + // Don't continue if the caster is not a Character + if (caster is not Character casterCharacter) + return; + + var dummyCharacter = casterCharacter.Map.GetCharacter((int)buff.NumArg2); + + if (dummyCharacter != null) + dummyCharacter.Died += this.OnDummyDied; + } + + /// + /// Executes the buff handler's end behavior. + /// Does not actually end or remove the buff. + /// + /// + public override void OnEnd(Buff buff) + { + var caster = buff.Caster; + + RemovePropertyModifier(buff, caster, PropertyName.MSPD_BM); + + // Ignore if the caster is not a Character + if (caster is not Character casterCharacter) + return; + + // It spawns an clone character in form of a spirit that is controlled by AI. + // So we need to identify the Source character that will be used on the buff ending. + var characterSkillHandling = casterCharacter is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + // Don't continue if the caster doesn't have the skill. + if (!characterSkillHandling.TryGetSkill(SkillId.Sadhu_Patati, out var skill)) + return; + + characterSkillHandling.SetAttackState(true); + + // Creates the Area Of Effect for that will damages enemies inside. + this.AreaOfEffect(caster, skill, caster.Position); + + // [Arts] Spirit Expert: Wandering Soul - AI Controlled Spirit + if (casterCharacter is DummyCharacter dummyCharacter2 && dummyCharacter2.Owner.IsAbilityActive(AbilityId.Sadhu35)) + { + Send.ZC_SKILL_READY(dummyCharacter2.Owner, caster, skill, caster.Position, caster.Position); + Send.ZC_NORMAL.UpdateSkillEffect(dummyCharacter2.Owner, caster.Handle, caster.Position, caster.Direction, Position.Zero); + Send.ZC_SKILL_MELEE_GROUND(dummyCharacter2.Owner, caster, skill, caster.Position, ForceId.GetNew(), null); + } + else // Spirit was controlled by the player + { + skill.IncreaseOverheat(); + } + + // [Arts] Spirit Expert: Wandering Soul: AI Controlled Spirit + // Removes the clone character (spirit) from the map. + if (casterCharacter is DummyCharacter dummyCharacter3 && dummyCharacter3.Owner.IsAbilityActive(AbilityId.Sadhu35)) + { + this.RemoveSpritCloneCharacter(dummyCharacter3); + return; + } + + Send.ZC_NORMAL.EndOutOfBodyBuff(casterCharacter, BuffId.OOBE_Patati_Buff); + Send.ZC_NORMAL.UpdateModelColor(casterCharacter, 255, 255, 255, 255, 0.01f); + Send.ZC_PLAY_SOUND(casterCharacter, "skl_eff_yuchae_end_2"); + + this.ReturnToBody(casterCharacter, (int)buff.NumArg2); + } + + /// + /// Creates the Area of Effect + /// + /// + /// + /// + private void AreaOfEffect(ICombatEntity caster, Skill skill, Position position) + { + Send.ZC_GROUND_EFFECT(caster, "F_cleric_patati_explosion", position, 0.8f, 1f, 0, 0, 0); + + var circle = new Circle(position, 60); + var targets = caster.Map.GetAttackableEntitiesIn(caster, circle); + + foreach (var target in targets.LimitRandom(MaxTargets)) + { + var chance = this.GetKnockdownChance(skill); + + if (chance >= RandomProvider.Get().Next(100)) + this.KnockdownEntity(caster, target, skill); + + this.Attack(skill, caster, target); + } + } + + /// + /// Attacks the targets. + /// + /// + /// + /// + private void Attack(Skill skill, ICombatEntity caster, ICombatEntity target) + { + var dealsDamageCharacter = caster is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + var modifier = SkillModifier.MultiHit(6); + var skillHitResult = SCR_SkillHit(dealsDamageCharacter, target, skill, modifier); + + target.TakeDamage(skillHitResult.Damage, dealsDamageCharacter); + + var hit = new HitInfo(dealsDamageCharacter, target, skill, skillHitResult, TimeSpan.FromMilliseconds(200)); + + Send.ZC_HIT_INFO(dealsDamageCharacter, target, hit); + } + + /// + /// Knockdown the entity close to the caster position. + /// + /// + /// + /// + private void KnockdownEntity(ICombatEntity caster, ICombatEntity target, Skill skill) + { + var kb = new KnockBackInfo(caster.Position, target.Position, skill); + target.Position = kb.ToPosition; + + Send.ZC_KNOCKDOWN_INFO(caster, target, kb); + } + + /// + /// Returns the knockdown chance once the monster is hit. + /// + /// + private float GetKnockdownChance(Skill skill) + { + return 35 + (4.5f * skill.Level); + } + } +} diff --git a/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Possession_Buff.cs b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Possession_Buff.cs new file mode 100644 index 000000000..f61c9dc21 --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Possession_Buff.cs @@ -0,0 +1,164 @@ +using System; +using Melia.Shared.Game.Const; +using Melia.Zone.Buffs.Base; +using Melia.Zone.Network; +using Melia.Zone.Skills.Combat; +using Melia.Zone.Skills; +using Melia.Zone.World.Actors; +using Melia.Zone.World.Actors.Characters; +using Melia.Zone.Skills.SplashAreas; +using Melia.Shared.World; +using static Melia.Zone.Skills.SkillUseFunctions; + +namespace Melia.Zone.Buffs.Handlers.Clerics.Sadhu +{ + /// + /// Handler for the Out Of Body Experience (OOBE) Posession Buff + /// which makes the character go back to original position after a while + /// and leave an effect that damages enemies inside + /// + [BuffHandler(BuffId.OOBE_Possession_Buff)] + public class OOBE_Possession_Buff : Sadhu_BuffHandler_Base + { + private const int MaxTargets = 7; + // The skill tooltip says that a movement hold just be applied but it doesn't happen. + // For that reason I left this here in case if it changes. + private const bool ApplySelfHold = false; + + /// + /// Starts buff, adding an event handler for + /// the death of the dummy character. + /// + /// + /// + public override void OnActivate(Buff buff, ActivationType activationType) + { + var caster = buff.Caster; + + AddPropertyModifier(buff, caster, PropertyName.MSPD_BM, (int)buff.NumArg1); + + // Don't continue if the caster is not a Character + if (caster is not Character casterCharacter) + return; + + var dummyCharacter = casterCharacter.Map.GetCharacter((int)buff.NumArg2); + + if (dummyCharacter != null) + dummyCharacter.Died += this.OnDummyDied; + } + + /// + /// Executes the buff handler's end behavior. + /// Does not actually end or remove the buff. + /// + /// + public override void OnEnd(Buff buff) + { + var caster = buff.Caster; + + RemovePropertyModifier(buff, caster, PropertyName.MSPD_BM); + + // Ignore if the caster is not a Character + if (caster is not Character casterCharacter) + return; + + // [Arts] Spirit Expert: Wandering Soul: Instead of letting the player control the spirit, + // It spawns an clone character in form of a spirit that is controlled by AI. + // So we need to identify the Source character that will be used on the buff ending. + var characterSkillHandling = casterCharacter is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + // Don't continue if the caster doesn't have the skill. + if (!characterSkillHandling.TryGetSkill(SkillId.Sadhu_Possession, out var skill)) + return; + + characterSkillHandling.SetAttackState(true); + + // Creates the Area Of Effect for that will damages enemies inside. + this.AreaOfEffect(characterSkillHandling, skill, caster.Position); + + if (ApplySelfHold && casterCharacter is not DummyCharacter) + characterSkillHandling.StartBuff(BuffId.Common_Hold, TimeSpan.FromMilliseconds(this.GetHoldTime(skill))); + + // [Arts] Spirit Expert: Wandering Soul - AI Controlled Spirit + if (casterCharacter is DummyCharacter dummyCharacter2 && dummyCharacter2.Owner.IsAbilityActive(AbilityId.Sadhu35)) + { + Send.ZC_SKILL_READY(dummyCharacter2.Owner, caster, skill, caster.Position, caster.Position); + Send.ZC_NORMAL.UpdateSkillEffect(dummyCharacter2.Owner, caster.Handle, caster.Position, caster.Direction, Position.Zero); + Send.ZC_SKILL_MELEE_GROUND(dummyCharacter2.Owner, caster, skill, caster.Position, ForceId.GetNew(), null); + } + else // Spirit was controlled by the player + { + skill.IncreaseOverheat(); + } + + // [Arts] Spirit Expert: Wandering Soul: AI Controlled Spirit + // Removes the clone character (spirit) from the map. + if (casterCharacter is DummyCharacter dummyCharacter3 && dummyCharacter3.Owner.IsAbilityActive(AbilityId.Sadhu35)) + { + this.RemoveSpritCloneCharacter(dummyCharacter3); + return; + } + + Send.ZC_NORMAL.EndOutOfBodyBuff(casterCharacter, BuffId.OOBE_Possession_Buff); + Send.ZC_NORMAL.UpdateModelColor(casterCharacter, 255, 255, 255, 255, 0.01f); + Send.ZC_PLAY_SOUND(casterCharacter, "skl_eff_yuchae_end_2"); + + this.ReturnToBody(casterCharacter, (int)buff.NumArg2); + } + + /// + /// Creates the Area of Effect. + /// + /// + /// + /// + private void AreaOfEffect(ICombatEntity caster, Skill skill, Position position) + { + Send.ZC_GROUND_EFFECT(caster, "F_spread_out026_mint", position, 3f, 3f, 0, 0, 0); + Send.ZC_GROUND_EFFECT(caster, "F_explosion086_mint", position, 1.2f, 3f, 0, 0, 0); + Send.ZC_GROUND_EFFECT(caster, "F_burstup047_mint", position, 0.7f, 3f, 0, 0, 0); + Send.ZC_GROUND_EFFECT(caster, "F_pattern025_loop", position, 1.5f, 3f, 0, 0, 0); + + var circle = new Circle(position, 120); + var targets = caster.Map.GetAttackableEntitiesIn(caster, circle); + + foreach (var target in targets.LimitRandom(MaxTargets)) + { + this.Attack(skill, caster, target); + } + } + + /// + /// Attacks the target. + /// + /// + /// + /// + private void Attack(Skill skill, ICombatEntity caster, ICombatEntity target) + { + var dealsDamageCharacter = caster is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + var modifier = SkillModifier.MultiHit(5); + var skillHitResult = SCR_SkillHit(dealsDamageCharacter, target, skill, modifier); + + target.TakeDamage(skillHitResult.Damage, dealsDamageCharacter); + + var hit = new HitInfo(dealsDamageCharacter, target, skill, skillHitResult, TimeSpan.FromMilliseconds(200)); + + Send.ZC_HIT_INFO(dealsDamageCharacter, target, hit); + } + + /// + /// Returns the amount of hold time in milliseconds + /// + /// + private int GetHoldTime(Skill skill) + { + return 1000 + (300 * skill.Level); + } + } +} diff --git a/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Prakriti_Buff.cs b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Prakriti_Buff.cs new file mode 100644 index 000000000..ec3b5a699 --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Prakriti_Buff.cs @@ -0,0 +1,159 @@ +using System; +using Melia.Shared.Game.Const; +using Melia.Zone.Buffs.Base; +using Melia.Zone.Network; +using Melia.Zone.Skills.Combat; +using Melia.Zone.Skills; +using Melia.Zone.Skills.SplashAreas; +using Melia.Zone.World.Actors; +using Melia.Zone.World.Actors.Characters; +using Melia.Zone.World.Actors.Pads; +using Melia.Zone.World.Actors.Monsters; +using Melia.Shared.World; +using static Melia.Zone.Skills.SkillUseFunctions; + +namespace Melia.Zone.Buffs.Handlers.Clerics.Sadhu +{ + /// + /// Handler for the Out Of Body Experience (OOBE) Prakriti Buff + /// which makes the character go back to original position after a while + /// and creates an effect that damages enemies inside. + /// + [BuffHandler(BuffId.OOBE_Prakriti_Buff)] + public class OOBE_Prakriti_Buff : Sadhu_BuffHandler_Base + { + /// + /// Starts buff, adding an event handler for + /// the death of the dummy character. + /// + /// + /// + public override void OnActivate(Buff buff, ActivationType activationType) + { + var caster = buff.Caster; + + AddPropertyModifier(buff, caster, PropertyName.MSPD_BM, (int)buff.NumArg1); + + // Don't continue if the caster is not a Character + if (caster is not Character casterCharacter) + return; + + var dummyCharacter = casterCharacter.Map.GetCharacter((int)buff.NumArg2); + + if (dummyCharacter != null) + dummyCharacter.Died += this.OnDummyDied; + } + + /// + /// Executes the buff handler's end behavior. + /// Does not actually end or remove the buff. + /// + /// + public override void OnEnd(Buff buff) + { + var caster = buff.Caster; + + RemovePropertyModifier(buff, caster, PropertyName.MSPD_BM); + + // Ignore if the caster is not a Character + if (caster is not Character casterCharacter) + return; + + // It spawns an clone character in form of a spirit that is controlled by AI. + // So we need to identify the Source character that will be used on the buff ending. + var skillCharacter = casterCharacter is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + // Don't continue if the caster doesn't have the skill. + if (!skillCharacter.TryGetSkill(SkillId.Sadhu_Prakriti, out var skill)) + return; + + skillCharacter.SetAttackState(true); + + // Creates the Pad that will handles the buff ending effect that damages while enemies are inside. + var pad = new Pad(PadName.Sadhu_Prakriti_Pad, skillCharacter, skill, new Circle(caster.Position, 90)); + + pad.Position = new Position(pad.Trigger.Area.Center.X, caster.Position.Y, pad.Trigger.Area.Center.Y); + pad.Trigger.MaxActorCount = 10; + pad.Trigger.LifeTime = TimeSpan.FromSeconds(10); + pad.Trigger.UpdateInterval = TimeSpan.FromSeconds(1); + pad.Trigger.Subscribe(TriggerType.Update, this.OnUpdate); + + caster.Map.AddPad(pad); + + // [Arts] Spirit Expert: Wandering Soul - AI Controlled Spirit + if (casterCharacter is DummyCharacter dummyCharacter2 && dummyCharacter2.Owner.IsAbilityActive(AbilityId.Sadhu35)) + { + Send.ZC_SKILL_READY(dummyCharacter2.Owner, caster, skill, caster.Position, caster.Position); + Send.ZC_NORMAL.UpdateSkillEffect(dummyCharacter2.Owner, caster.Handle, caster.Position, caster.Direction, Position.Zero); + Send.ZC_SKILL_MELEE_GROUND(dummyCharacter2.Owner, caster, skill, caster.Position, ForceId.GetNew(), null); + } + else // Spirit was controlled by the player + { + skill.IncreaseOverheat(); + } + + // [Arts] Spirit Expert: Wandering Soul: AI Controlled Spirit + // Removes the clone character (spirit) from the map. + if (casterCharacter is DummyCharacter dummyCharacter3 && dummyCharacter3.Owner.IsAbilityActive(AbilityId.Sadhu35)) + { + this.RemoveSpritCloneCharacter(dummyCharacter3); + return; + } + + Send.ZC_NORMAL.EndOutOfBodyBuff(casterCharacter, BuffId.OOBE_Prakriti_Buff); + Send.ZC_NORMAL.UpdateModelColor(casterCharacter, 255, 255, 255, 255, 0.01f); + Send.ZC_PLAY_SOUND(casterCharacter, "skl_eff_yuchae_end_2"); + + this.ReturnToBody(casterCharacter, (int)buff.NumArg2); + } + + /// + /// Called in regular intervals while the pad is on a map. + /// + /// + /// + private void OnUpdate(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.LimitRandom(pad.Trigger.MaxActorCount)) + { + if (caster.IsAbilityActive(AbilityId.Sadhu46) && !target.IsBuffActive(BuffId.OOBE_Prakriti_Sadhu46_Debuff)) + target.StartBuff(BuffId.OOBE_Prakriti_Sadhu46_Debuff, 0, 0, TimeSpan.FromSeconds(60), caster); + + if (!target.IsBuffActive(BuffId.Common_Slow)) + target.StartBuff(BuffId.Common_Slow, 0, 0, TimeSpan.FromSeconds(1), caster); + + this.Attack(skill, caster, target); + } + } + + /// + /// Attacks the target. + /// + /// + /// + /// + private void Attack(Skill skill, ICombatEntity caster, ICombatEntity target) + { + var dealsDamageCharacter = caster is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + var skillHitResult = SCR_SkillHit(dealsDamageCharacter, target, skill); + + target.TakeDamage(skillHitResult.Damage, dealsDamageCharacter); + + var hit = new HitInfo(dealsDamageCharacter, target, skill, skillHitResult); + hit.Type = HitType.Type33; + + Send.ZC_HIT_INFO(dealsDamageCharacter, target, hit); + } + } +} diff --git a/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Prakriti_Sadhu46_Debuff.cs b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Prakriti_Sadhu46_Debuff.cs new file mode 100644 index 000000000..916518c50 --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Prakriti_Sadhu46_Debuff.cs @@ -0,0 +1,32 @@ +using Melia.Shared.Data.Database; +using Melia.Shared.Game.Const; +using Melia.Zone.Buffs.Base; +using Melia.Zone.Skills; +using Melia.Zone.Skills.Combat; +using Melia.Zone.World.Actors; + +namespace Melia.Zone.Buffs.Handlers.Swordsmen.Barbarian +{ + /// + /// Handle for the OOBE_Prakriti Sadhu46 ([Arts] Prakriti: Penance) Debuff, + /// which increases damage final damage taken from crit attacks. + /// + [BuffHandler(BuffId.OOBE_Prakriti_Sadhu46_Debuff)] + public class OOBE_Prakriti_Sadhu46_Debuff : BuffHandler, IBuffCombatAttackAfterCalcHandler + { + /// + /// Applies the debuff's effect during the combat calculations. + /// + /// + /// + /// + /// + /// + /// + public void OnAttackAfterCalc(Buff buff, ICombatEntity attacker, ICombatEntity target, Skill skill, SkillModifier modifier, SkillHitResult skillHitResult) + { + if (skillHitResult.Result == HitResultType.Crit) + modifier.DamageMultiplier += 0.15f; + } + } +} diff --git a/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Tanoti_Buff.cs b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Tanoti_Buff.cs new file mode 100644 index 000000000..5d1be3f2c --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Tanoti_Buff.cs @@ -0,0 +1,172 @@ +using System; +using Yggdrasil.Util; +using Melia.Shared.Game.Const; +using Melia.Zone.Buffs.Base; +using Melia.Zone.Network; +using Melia.Zone.Skills.Combat; +using Melia.Zone.Skills; +using Melia.Zone.World.Actors; +using Melia.Zone.World.Actors.Characters; +using Melia.Zone.Skills.SplashAreas; +using Melia.Shared.World; +using static Melia.Zone.Skills.SkillUseFunctions; + +namespace Melia.Zone.Buffs.Handlers.Clerics.Sadhu +{ + /// + /// Handler for the Out Of Body Experience (OOBE) Tanoti Buff + /// which makes the character go back to original position after a while + /// and creates an effect that damages enemies inside within a chance of pulling them + /// + [BuffHandler(BuffId.OOBE_Tanoti_Buff)] + public class OOBE_Tanoti_Buff : Sadhu_BuffHandler_Base + { + private const int MaxTargets = 5; + + /// + /// Starts buff, adding an event handler for + /// the death of the dummy character. + /// + /// + /// + public override void OnActivate(Buff buff, ActivationType activationType) + { + var caster = buff.Caster; + + AddPropertyModifier(buff, caster, PropertyName.MSPD_BM, (int)buff.NumArg1); + + // Don't continue if the caster is not a Character + if (caster is not Character casterCharacter) + return; + + var dummyCharacter = casterCharacter.Map.GetCharacter((int)buff.NumArg2); + + if (dummyCharacter != null) + dummyCharacter.Died += this.OnDummyDied; + } + + /// + /// Executes the buff handler's end behavior. + /// Does not actually end or remove the buff. + /// + /// + public override void OnEnd(Buff buff) + { + var caster = buff.Caster; + + RemovePropertyModifier(buff, caster, PropertyName.MSPD_BM); + + // Ignore if the caster is not a Character + if (caster is not Character casterCharacter) + return; + + // It spawns an clone character in form of a spirit that is controlled by AI. + // So we need to identify the Source character that will be used on the buff ending. + var characterSkillHandling = casterCharacter is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + // Don't continue if the caster doesn't have the skill. + if (!characterSkillHandling.TryGetSkill(SkillId.Sadhu_Tanoti, out var skill)) + return; + + characterSkillHandling.SetAttackState(true); + + // Creates the Area Of Effect for that will pulling enemies inside the area for the center. + this.AreaOfEffect(caster, skill, caster.Position); + + // [Arts] Spirit Expert: Wandering Soul - AI Controlled Spirit + if (casterCharacter is DummyCharacter dummyCharacter2 && dummyCharacter2.Owner.IsAbilityActive(AbilityId.Sadhu35)) + { + Send.ZC_SKILL_READY(dummyCharacter2.Owner, caster, skill, caster.Position, caster.Position); + Send.ZC_NORMAL.UpdateSkillEffect(dummyCharacter2.Owner, caster.Handle, caster.Position, caster.Direction, Position.Zero); + Send.ZC_SKILL_MELEE_GROUND(dummyCharacter2.Owner, caster, skill, caster.Position, ForceId.GetNew(), null); + } + else // Spirit was controlled by the player + { + skill.IncreaseOverheat(); + } + + // [Arts] Spirit Expert: Wandering Soul: AI Controlled Spirit + // Removes the clone character (spirit) from the map. + if (casterCharacter is DummyCharacter dummyCharacter3 && dummyCharacter3.Owner.IsAbilityActive(AbilityId.Sadhu35)) + { + this.RemoveSpritCloneCharacter(dummyCharacter3); + return; + } + + Send.ZC_NORMAL.EndOutOfBodyBuff(casterCharacter, BuffId.OOBE_Tanoti_Buff); + Send.ZC_NORMAL.UpdateModelColor(casterCharacter, 255, 255, 255, 255, 0.01f); + Send.ZC_PLAY_SOUND(casterCharacter, "skl_eff_yuchae_end_2"); + + this.ReturnToBody(casterCharacter, (int)buff.NumArg2); + } + + /// + /// Creates the Area of Effect. + /// + /// + /// + /// + private void AreaOfEffect(ICombatEntity caster, Skill skill, Position position) + { + Send.ZC_GROUND_EFFECT(caster, "F_pose_magical2_light01_mint", position, 1.5f, 1f, 0, 0, 0); + Send.ZC_GROUND_EFFECT(caster, "E_cleric_tanoti001", position, 1f, 1f, 0, 0, 0); + + var circle = new Circle(position, 60); + var targets = caster.Map.GetAttackableEntitiesIn(caster, circle); + + foreach (var target in targets.LimitRandom(MaxTargets)) + { + var chance = this.GetPullChance(skill); + + if (chance >= RandomProvider.Get().Next(100)) + this.PullEntity(caster, target); + + this.Attack(skill, caster, target); + } + } + + /// + /// Attacks the target. + /// + /// + /// + private void Attack(Skill skill, ICombatEntity caster, ICombatEntity target) + { + var dealsDamageCharacter = caster is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + var modifier = SkillModifier.MultiHit(5); + var skillHitResult = SCR_SkillHit(dealsDamageCharacter, target, skill, modifier); + + target.TakeDamage(skillHitResult.Damage, dealsDamageCharacter); + + var hit = new HitInfo(dealsDamageCharacter, target, skill, skillHitResult, TimeSpan.FromMilliseconds(200)); + + Send.ZC_HIT_INFO(dealsDamageCharacter, target, hit); + } + + /// + /// Pull the entity close to the caster position + /// + /// + /// + private void PullEntity(ICombatEntity caster, ICombatEntity target) + { + target.Position = caster.Position; + Send.ZC_SET_POS(target, caster.Position); + } + + /// + /// Returns the pull chance once the monster is hit + /// + /// + private float GetPullChance(Skill skill) + { + return 35 + (4.5f * skill.Level); + } + } +} diff --git a/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/Sadhu_BuffHandler_Base.cs b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/Sadhu_BuffHandler_Base.cs new file mode 100644 index 000000000..997a4c57b --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/Sadhu_BuffHandler_Base.cs @@ -0,0 +1,89 @@ +using Melia.Shared.Game.Const; +using Melia.Zone.Buffs.Base; +using Melia.Zone.Network; +using Melia.Zone.Skills.Combat; +using Melia.Zone.Skills; +using Melia.Zone.World.Actors; +using Melia.Zone.World.Actors.Characters; + +namespace Melia.Zone.Buffs.Handlers.Clerics.Sadhu +{ + /// + /// Base Buff for sadhu's buffs + /// + public class Sadhu_BuffHandler_Base : BuffHandler, IBuffCombatAttackAfterCalcHandler + { + /// + /// Remove the spirit clone character from the map. + /// + /// + protected void RemoveSpritCloneCharacter(DummyCharacter dummyCharacter) + { + Send.ZC_OWNER(dummyCharacter.Owner, dummyCharacter, 0); + Send.ZC_LEAVE(dummyCharacter); + + dummyCharacter.Map.RemoveCharacter(dummyCharacter); + } + + /// + /// Makes the character returns to original position + /// and also get ride of the dummy character. + /// + /// + /// + protected void ReturnToBody(Character character, int dummyHandle) + { + var dummyCharacter = character.Map.GetCharacter(dummyHandle); + + if (dummyCharacter == null) + return; + + character.Position = dummyCharacter.Position; + character.Direction = dummyCharacter.Direction; + + dummyCharacter.Died -= this.OnDummyDied; + + Send.ZC_ROTATE(character); + Send.ZC_SET_POS(character, dummyCharacter.Position); + Send.ZC_OWNER(character, dummyCharacter, 0); + Send.ZC_LEAVE(dummyCharacter); + + character.Map.RemoveCharacter(dummyCharacter); + } + + /// + /// Called when the clone dummy character dies. + /// + /// + /// + protected void OnDummyDied(ICombatEntity character, ICombatEntity killer) + { + if (character is DummyCharacter dummyCharacter) + dummyCharacter.Owner.StopBuff(BuffId.OOBE_Anila_Buff); + else if (character is Character ch) + ch.StopBuff(BuffId.OOBE_Anila_Buff); + } + + /// + /// Applies the buff's effect during the combat calculations. + /// + /// + /// + /// + /// + /// + /// + public void OnAttackAfterCalc(Buff buff, ICombatEntity attacker, ICombatEntity target, Skill skill, SkillModifier modifier, SkillHitResult skillHitResult) + { + // While in OOBE the character won't receive damage from any sources + // besides Holy Damage (which will double) or if the attacker is Elite/Boss + if (target is Character tagetCharacter && tagetCharacter.IsOutOfBody()) + { + if (attacker.Rank != MonsterRank.Boss && (skill.Data.Attribute != AttributeType.Holy || !attacker.IsBuffActive(BuffId.EliteMonsterBuff))) + skillHitResult.Damage = 0; + else if (skill.Data.Attribute == AttributeType.Holy) + skillHitResult.Damage = (int)(skillHitResult.Damage * 2); + } + } + } +} diff --git a/src/ZoneServer/Buffs/Handlers/Common_Hold.cs b/src/ZoneServer/Buffs/Handlers/Common_Hold.cs new file mode 100644 index 000000000..a0c505da6 --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Common_Hold.cs @@ -0,0 +1,24 @@ +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 +{ + /// + /// Handler for Hold Buff which sets the target to a on hold position + /// + [BuffHandler(BuffId.Common_Hold)] + public class Common_Hold : BuffHandler + { + public override void OnActivate(Buff buff, ActivationType activationType) + { + buff.Target.Lock(LockType.Movement); + } + + public override void OnEnd(Buff buff) + { + buff.Target.Unlock(LockType.Movement); + } + } +} diff --git a/src/ZoneServer/Network/Send.Normal.cs b/src/ZoneServer/Network/Send.Normal.cs index ebb09e5e9..e25425b44 100644 --- a/src/ZoneServer/Network/Send.Normal.cs +++ b/src/ZoneServer/Network/Send.Normal.cs @@ -9,6 +9,7 @@ using Melia.Zone.World.Actors.Characters.Components; using Melia.Zone.World.Actors.Monsters; using Melia.Zone.World.Actors.Pads; +using Yggdrasil.Logging; namespace Melia.Zone.Network { @@ -347,7 +348,7 @@ public static void UnkDynamicCastStart(Character character, SkillId skillId) packet.PutInt(character.Handle); packet.PutInt((int)skillId); - character.Connection.Send(packet); + character.Map.Broadcast(packet, character); } /// @@ -560,16 +561,24 @@ public static void SetSessionKey(IZoneConnection conn) /// clients in range. /// /// - public static void HeadgearVisibilityUpdate(Character character) + public static void HeadgearVisibilityUpdate(Character character) => HeadgearVisibilityUpdate(character, character); + + /// + /// Updates which headgears are visible for the character on + /// clients in range. + /// + /// + /// + public static void HeadgearVisibilityUpdate(Character character, Character targetCharacter) { var packet = new Packet(Op.ZC_NORMAL); packet.PutInt(NormalOp.Zone.HeadgearVisibilityUpdate); - packet.PutInt(character.Handle); - packet.PutByte((character.VisibleEquip & VisibleEquip.Headgear1) != 0); - packet.PutByte((character.VisibleEquip & VisibleEquip.Headgear2) != 0); - packet.PutByte((character.VisibleEquip & VisibleEquip.Headgear3) != 0); - packet.PutByte((character.VisibleEquip & VisibleEquip.Wig) != 0); + packet.PutInt(targetCharacter.Handle); + packet.PutByte((targetCharacter.VisibleEquip & VisibleEquip.Headgear1) != 0); + packet.PutByte((targetCharacter.VisibleEquip & VisibleEquip.Headgear2) != 0); + packet.PutByte((targetCharacter.VisibleEquip & VisibleEquip.Headgear3) != 0); + packet.PutByte((targetCharacter.VisibleEquip & VisibleEquip.Wig) != 0); character.Map.Broadcast(packet, character); } @@ -643,6 +652,18 @@ public static void ParticleEffect(Character character, int actorId, int enable) character.Map.Broadcast(packet); } + /// + /// Appears to update information about a skill effect on the + /// clients in range of entity. + /// + /// + /// + /// + /// + /// + public static void UpdateSkillEffect(ICombatEntity entity, int targetHandle, Position originPos, Direction direction, Position farPos) + => UpdateSkillEffect(entity, targetHandle, originPos, direction, farPos, 0); + /// /// Appears to update information about a skill effect on the /// clients in range of entity. @@ -660,13 +681,14 @@ public static void ParticleEffect(Character character, int actorId, int enable) /// /// /// - public static void UpdateSkillEffect(ICombatEntity entity, int targetHandle, Position originPos, Direction direction, Position farPos) + /// + public static void UpdateSkillEffect(ICombatEntity entity, int targetHandle, Position originPos, Direction direction, Position farPos, int unknowInt) { var packet = new Packet(Op.ZC_NORMAL); packet.PutInt(NormalOp.Zone.UpdateSkillEffect); packet.PutInt(entity.Handle); - packet.PutInt(0); + packet.PutInt(unknowInt); packet.PutInt(0); packet.PutInt(targetHandle); packet.PutPosition(originPos); @@ -1299,6 +1321,80 @@ public static void UpdateCollection(Character character, int collectionId, int i character.Connection.Send(packet); } + /// + /// Updates the entity model color + /// + /// + /// + /// + /// + /// + /// + /// + public static void UpdateModelColor(Character character, int red, int green, int blue, int alpha, float f1) + { + var packet = new Packet(Op.ZC_NORMAL); + packet.PutInt(NormalOp.Zone.UpdateModelColor); + + packet.PutInt(character.Handle); + packet.PutByte((byte)red); + packet.PutByte((byte)green); + packet.PutByte((byte)blue); + packet.PutByte((byte)alpha); + packet.PutByte(1); + packet.PutFloat(f1); + packet.PutByte(1); + + character.Map.Broadcast(packet); + } + + /// + /// Enable to use a skill while being out of body (Sadhu). + /// + /// + /// + /// + public static void EnableUseSkillWhileOutOfBody(Character character, BuffId buffId, SkillId skillId) + { + if (!ZoneServer.Instance.Data.BuffDb.TryFind(buffId, out var buffData)) + { + Log.Error("EnableUseSkillWhileOutOfBody: BuffId '{0}' was not found.", buffId); + return; + } + + var packet = new Packet(Op.ZC_NORMAL); + packet.PutInt(NormalOp.Zone.EnableUseSkillWhileOutOfBody); + + packet.PutInt(character.Handle); + packet.PutLpString(buffData.ClassName); + packet.PutInt((int)skillId); + packet.PutByte(1); + + character.Connection.Send(packet); + } + + /// + /// Set the buff that will be used while out of body (Sadhu). + /// + /// + /// + public static void EndOutOfBodyBuff(Character character, BuffId buffId) + { + if (!ZoneServer.Instance.Data.BuffDb.TryFind(buffId, out var buffData)) + { + Log.Error("EndOutOfBodyBuff: BuffId '{0}' was not found.", buffId); + return; + } + + var packet = new Packet(Op.ZC_NORMAL); + packet.PutInt(NormalOp.Zone.EndOutOfBodyBuff); + + packet.PutInt(character.Handle); + packet.PutLpString(buffData.ClassName); + + character.Connection.Send(packet); + } + /// /// Exact purpose unknown, used in some skills when there's no target. /// @@ -1343,6 +1439,7 @@ public static void Skill_43(IActor actor) actor.Map.Broadcast(packet); } + /// /// Opens book for the player. /// /// diff --git a/src/ZoneServer/Network/Send.cs b/src/ZoneServer/Network/Send.cs index 8adb3d947..12bf12391 100644 --- a/src/ZoneServer/Network/Send.cs +++ b/src/ZoneServer/Network/Send.cs @@ -157,7 +157,7 @@ public static void ZC_ENTER_PC(IZoneConnection conn, Character character) packet.PutInt(character.Stamina); packet.PutInt(character.MaxStamina); packet.PutByte(0); - packet.PutShort(0); + packet.PutShort(character is DummyCharacter ? 5 : 0); packet.PutInt(-1); // titleAchievmentId packet.PutInt(0); packet.PutByte(0); @@ -512,6 +512,17 @@ public static void ZC_SKILL_MELEE_GROUND(ICombatEntity entity, Skill skill, Posi /// /// public static void ZC_SKILL_MELEE_GROUND(ICombatEntity entity, Skill skill, Position targetPos, int forceId, IEnumerable hits) + => ZC_SKILL_MELEE_GROUND(entity, entity, skill, targetPos, forceId, hits); + + /// + /// Shows entity using the skill. + /// + /// + /// + /// + /// + /// + public static void ZC_SKILL_MELEE_GROUND(ICombatEntity entity, ICombatEntity target, Skill skill, Position targetPos, int forceId, IEnumerable hits) { var shootTime = skill.Properties.GetFloat(PropertyName.ShootTime); var sklSpdRate = skill.Properties.GetFloat(PropertyName.SklSpdRate); @@ -528,10 +539,10 @@ public static void ZC_SKILL_MELEE_GROUND(ICombatEntity entity, Skill skill, Posi var packet = new Packet(Op.ZC_SKILL_MELEE_GROUND); - packet.PutInt((int)skillId); - packet.PutInt(entity.Handle); - packet.PutFloat(entity.Direction.Cos); - packet.PutFloat(entity.Direction.Sin); + packet.PutInt((int)skill.Id); + packet.PutInt(target.Handle); + packet.PutFloat(target.Direction.Cos); + packet.PutFloat(target.Direction.Sin); packet.PutInt(1); packet.PutFloat(shootTime); packet.PutFloat(1); @@ -633,7 +644,7 @@ public static void ZC_OVERHEAT_CHANGED(Character character, Skill skill) packet.PutInt(4352); packet.PutLong(0); - character.Connection.Send(packet); + character.Connection?.Send(packet); } /// @@ -1401,7 +1412,7 @@ public static void ZC_CAMPINFO(IZoneConnection conn) } /// - /// Broadcasts ZC_SET_POS in range of actor, updating its position. + /// Broadcasts ZC_SET_POS in range of actor, updating its own position. /// /// public static void ZC_SET_POS(IActor actor) @@ -1411,6 +1422,8 @@ public static void ZC_SET_POS(IActor actor) /// Broadcasts ZC_SET_POS in range of actor, updating its position. /// /// + /// + /// public static void ZC_SET_POS(IActor actor, Position pos) { var packet = new Packet(Op.ZC_SET_POS); @@ -2255,21 +2268,35 @@ public static void ZC_DIALOG_TRADE(IZoneConnection conn, string shopName) /// /// public static void ZC_SKILL_READY(ICombatEntity entity, Skill skill, Position position1, Position position2) + => ZC_SKILL_READY(entity, entity, skill, position1, position2); + + /// + /// Notifies the client that the skill is ready? Exact purpose + /// currently unknown. + /// + /// + /// + /// + /// + /// + public static void ZC_SKILL_READY(ICombatEntity entity, ICombatEntity caster, Skill skill, Position position1, Position position2) { + // Temporary solution until our skill handling system is + // more streamlined + if (entity is not Character character) + return; + var packet = new Packet(Op.ZC_SKILL_READY); - packet.PutInt(entity.Handle); + packet.PutInt(caster.Handle); packet.PutInt((int)skill.Id); packet.PutFloat(1); packet.PutFloat(1); packet.PutInt(0); packet.PutPosition(position1); packet.PutPosition(position2); - - // Temporary solution until our skill handling system is - // more streamlined - if (entity is Character character) - character.Connection.Send(packet); + + character.Connection.Send(packet); } /// @@ -2305,11 +2332,12 @@ public static void ZC_TEAMID(IZoneConnection conn, IActor actor, byte team) /// /// /// - public static void ZC_OWNER(Character character, IActor actor) + /// + public static void ZC_OWNER(Character character, IActor actor, int ownerHandle) { var packet = new Packet(Op.ZC_OWNER); packet.PutInt(actor.Handle); - packet.PutInt(character.Handle); + packet.PutInt(ownerHandle); character.Connection.Send(packet); } @@ -3254,11 +3282,12 @@ public static void ZC_TRUST_INFO(IZoneConnection conn) } /// - /// Plays animation for actor on nearby clients. + /// Plays animation for actor on nearby clients with a given animation name. /// /// Entity to animate. /// Name of the animation to play (uses packet string database to retrieve the id of the string). /// If true, the animation plays once and then stops on the last frame. + /// Used for Sadhu animations. public static void ZC_PLAY_ANI(IActor actor, string animationName, bool stopOnLastFrame = false) { var packet = new Packet(Op.ZC_PLAY_ANI); @@ -3311,11 +3340,11 @@ public static void ZC_PCBANG_POINT(IZoneConnection conn) conn.Send(packet); } - /// - /// Updates character's movement speed. - /// - /// - public static void ZC_MSPD(ICombatEntity entity) + /// + /// Updates entity's movement speed. + /// + /// + public static void ZC_MSPD(ICombatEntity entity) { var packet = new Packet(Op.ZC_MSPD); @@ -4470,6 +4499,36 @@ public static void ZC_KNOCKDOWN_INFO(ICombatEntity entity, ICombatEntity target, entity.Map.Broadcast(packet, entity); } + /// + /// Display an effect on the floor for nearby players + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static void ZC_GROUND_EFFECT(ICombatEntity entity, string packetString, Position position, float f1, float f2, float f3, float f4, float f5) + { + var packet = new Packet(Op.ZC_GROUND_EFFECT); + + packet.PutInt(entity.Handle); + packet.AddStringId(packetString); + packet.PutPosition(position); + packet.PutFloat(f1); + packet.PutFloat(f2); + packet.PutFloat(f3); + packet.PutFloat(f4); + packet.PutShort(0); + packet.PutShort(11336); + packet.PutFloat(f5); + packet.PutShort(0); + + entity.Map.Broadcast(packet, entity); + } + /// /// Attaches actor to a given node on the other actor's model on clients /// in range of actor. diff --git a/src/ZoneServer/Pads/Handlers/Cleric/Sadhu/Sadhu_Anila_Effect_Pad.cs b/src/ZoneServer/Pads/Handlers/Cleric/Sadhu/Sadhu_Anila_Effect_Pad.cs new file mode 100644 index 000000000..a8e025a92 --- /dev/null +++ b/src/ZoneServer/Pads/Handlers/Cleric/Sadhu/Sadhu_Anila_Effect_Pad.cs @@ -0,0 +1,66 @@ +using System.Threading.Tasks; +using Melia.Shared.Game.Const; +using Melia.Zone.Network; +using Melia.Zone.World.Actors; +using Melia.Zone.World.Actors.Monsters; +using Melia.Zone.World.Actors.Pads; +using static Melia.Shared.Util.TaskHelper; + +namespace Melia.Zone.Pads.Handlers.Cleric.Sadhu +{ + /// + /// Handler for the Sadhu Anila Effect Pad, creates and disables the effect + /// + [PadHandler(PadName.Sadhu_Anila_Effect_Pad)] + public class Sadhu_Anila_Effect_Pad : ICreatePadHandler, IDestroyPadHandler + { + private const float PadMoveDistance = 180; + private const float PadMoveSpeedForward = 185; + + /// + /// Called when the pad is created. + /// + /// + /// + public void Created(object sender, PadTriggerArgs args) + { + var pad = args.Trigger; + var creator = args.Creator; + pad.Movement.Speed = PadMoveSpeedForward; + + Send.ZC_NORMAL.PadUpdate(creator, pad, PadName.Sadhu_Anila_Effect_Pad, 2.356195f, 0, 50, true); + + CallSafe(this.MovePad(pad, args.Creator)); + } + + /// + /// 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, PadName.Sadhu_Anila_Effect_Pad, 2.356195f, 0, 50, false); + } + + /// + /// Makes pad moves forwards + /// + /// + /// + private async Task MovePad(Pad pad, ICombatEntity creator) + { + // Moves the pad forward. + var destination = creator.Position.GetRelative2D(creator.Direction, PadMoveDistance); + var moveTime = pad.Movement.MoveTo(destination); + + await Task.Delay(moveTime); + await Task.Delay(150); + + pad.Destroy(); + } + } +} diff --git a/src/ZoneServer/Pads/Handlers/Cleric/Sadhu/Sadhu_Moksha_Pad.cs b/src/ZoneServer/Pads/Handlers/Cleric/Sadhu/Sadhu_Moksha_Pad.cs new file mode 100644 index 000000000..bc0849dc4 --- /dev/null +++ b/src/ZoneServer/Pads/Handlers/Cleric/Sadhu/Sadhu_Moksha_Pad.cs @@ -0,0 +1,39 @@ +using Melia.Shared.Game.Const; +using Melia.Zone.Network; +using Melia.Zone.World.Actors.Monsters; + +namespace Melia.Zone.Pads.Handlers.Cleric.Sadhu +{ + /// + /// Handler for the Sadhu Moksha Pad, creates and disables the effect + /// + [PadHandler(PadName.Sadhu_Moksha_Pad)] + public class Sadhu_Moksha_Pad : 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.Sadhu_Moksha_Pad, -0.7853982f, 0, 100, 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, PadName.Sadhu_Moksha_Pad, -0.7853982f, 0, 100, false); + } + } +} diff --git a/src/ZoneServer/Pads/Handlers/Cleric/Sadhu/Sadhu_Prakriti_Pad.cs b/src/ZoneServer/Pads/Handlers/Cleric/Sadhu/Sadhu_Prakriti_Pad.cs new file mode 100644 index 000000000..33e12001d --- /dev/null +++ b/src/ZoneServer/Pads/Handlers/Cleric/Sadhu/Sadhu_Prakriti_Pad.cs @@ -0,0 +1,39 @@ +using Melia.Shared.Game.Const; +using Melia.Zone.Network; +using Melia.Zone.World.Actors.Monsters; + +namespace Melia.Zone.Pads.Handlers.Cleric.Sadhu +{ + /// + /// Handler for the Sadhu Prakriti Pad, creates and disables the effect + /// + [PadHandler(PadName.Sadhu_Prakriti_Pad)] + public class Sadhu_Prakriti_Pad : 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.Sadhu_Prakriti_Pad, 1.570796f, 0, 70, 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, PadName.Sadhu_Prakriti_Pad, 1.570796f, 0, 70, false); + } + } +} diff --git a/src/ZoneServer/Scripting/AI/AiScript.Routines.cs b/src/ZoneServer/Scripting/AI/AiScript.Routines.cs index 1f55cd2fb..973c487a8 100644 --- a/src/ZoneServer/Scripting/AI/AiScript.Routines.cs +++ b/src/ZoneServer/Scripting/AI/AiScript.Routines.cs @@ -275,8 +275,9 @@ protected IEnumerable Animation(string packetString) /// The target to follow. /// The minimum distance to the target the AI attempts to stay in. /// If true, the entity's speed will be changed to match the target's. + /// If true, the entity will teleport instantly to the target location if the distance is too high. /// - protected IEnumerable Follow(ICombatEntity followTarget, float minDistance = 50, bool matchSpeed = false) + protected IEnumerable Follow(ICombatEntity followTarget, float minDistance = 50, bool matchSpeed = false, bool teleport = true) { var movement = this.Entity.Components.Get(); var targetWasInRange = false; @@ -327,7 +328,7 @@ protected IEnumerable Follow(ICombatEntity followTarget, float minDistance = 50, var teleportDistance = minDistance * 4; var distance = followTarget.Position.Get2DDistance(this.Entity.Position); - if (distance > teleportDistance) + if (teleport && distance > teleportDistance) { movement.Stop(); diff --git a/src/ZoneServer/Scripting/AI/AiScript.cs b/src/ZoneServer/Scripting/AI/AiScript.cs index 9df141991..3ce3f8a17 100644 --- a/src/ZoneServer/Scripting/AI/AiScript.cs +++ b/src/ZoneServer/Scripting/AI/AiScript.cs @@ -5,6 +5,7 @@ using Melia.Shared.Game.Const; using Melia.Shared.World; 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 Yggdrasil.Ai.Enumerable; diff --git a/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Anila.cs b/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Anila.cs new file mode 100644 index 000000000..9376eb8fd --- /dev/null +++ b/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Anila.cs @@ -0,0 +1,27 @@ +using Melia.Shared.Game.Const; +using Melia.Shared.World; +using Melia.Zone.Skills.Handlers.Base; +using Melia.Zone.World.Actors; + +namespace Melia.Zone.Skills.Handlers.Clerics.Sadhu +{ + /// + /// Handler for the Sadhu skill Enira (Anila). + /// + [SkillHandler(SkillId.Sadhu_Anila)] + public class Sadhu_Anila : Sadhu_Skill_Base, IGroundSkillHandler + { + /// + /// Handles skill, makes the character out of body. + /// + /// + /// + /// + /// + /// + public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target) + { + this.Handle(skill, caster, originPos, farPos, target, BuffId.OOBE_Anila_Buff); + } + } +} diff --git a/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Moksha.cs b/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Moksha.cs new file mode 100644 index 000000000..fc3407cd9 --- /dev/null +++ b/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Moksha.cs @@ -0,0 +1,27 @@ +using Melia.Shared.Game.Const; +using Melia.Shared.World; +using Melia.Zone.Skills.Handlers.Base; +using Melia.Zone.World.Actors; + +namespace Melia.Zone.Skills.Handlers.Clerics.Sadhu +{ + /// + /// Handler for the Sadhu skill Moksha. + /// + [SkillHandler(SkillId.Sadhu_Moksha)] + public class Sadhu_Moksha : Sadhu_Skill_Base, IGroundSkillHandler + { + /// + /// Handles skill, makes the character out of body. + /// + /// + /// + /// + /// + /// + public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target) + { + this.Handle(skill, caster, originPos, farPos, target, BuffId.OOBE_Moksha_Buff); + } + } +} diff --git a/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Patati.cs b/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Patati.cs new file mode 100644 index 000000000..755f10588 --- /dev/null +++ b/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Patati.cs @@ -0,0 +1,27 @@ +using Melia.Shared.Game.Const; +using Melia.Shared.World; +using Melia.Zone.Skills.Handlers.Base; +using Melia.Zone.World.Actors; + +namespace Melia.Zone.Skills.Handlers.Clerics.Sadhu +{ + /// + /// Handler for the Sadhu skill Patati. + /// + [SkillHandler(SkillId.Sadhu_Patati)] + public class Sadhu_Patati : Sadhu_Skill_Base, IGroundSkillHandler + { + /// + /// Handles skill, makes the character out of body. + /// + /// + /// + /// + /// + /// + public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target) + { + this.Handle(skill, caster, originPos, farPos, target, BuffId.OOBE_Patati_Buff); + } + } +} diff --git a/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Possession.cs b/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Possession.cs new file mode 100644 index 000000000..f76e40683 --- /dev/null +++ b/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Possession.cs @@ -0,0 +1,27 @@ +using Melia.Shared.Game.Const; +using Melia.Shared.World; +using Melia.Zone.Skills.Handlers.Base; +using Melia.Zone.World.Actors; + +namespace Melia.Zone.Skills.Handlers.Clerics.Sadhu +{ + /// + /// Handler for the Sadhu skill Posession. + /// + [SkillHandler(SkillId.Sadhu_Possession)] + public class Sadhu_Possession : Sadhu_Skill_Base, IGroundSkillHandler + { + /// + /// Handles skill, makes the character out of body. + /// + /// + /// + /// + /// + /// + public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target) + { + this.Handle(skill, caster, originPos, farPos, target, BuffId.OOBE_Possession_Buff); + } + } +} diff --git a/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Prakriti.cs b/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Prakriti.cs new file mode 100644 index 000000000..f60d7b528 --- /dev/null +++ b/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Prakriti.cs @@ -0,0 +1,27 @@ +using Melia.Shared.Game.Const; +using Melia.Shared.World; +using Melia.Zone.Skills.Handlers.Base; +using Melia.Zone.World.Actors; + +namespace Melia.Zone.Skills.Handlers.Clerics.Sadhu +{ + /// + /// Handler for the Sadhu skill Prakriti. + /// + [SkillHandler(SkillId.Sadhu_Prakriti)] + public class Sadhu_Prakriti : Sadhu_Skill_Base, IGroundSkillHandler + { + /// + /// Handles skill, makes the character out of body. + /// + /// + /// + /// + /// + /// + public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target) + { + this.Handle(skill, caster, originPos, farPos, target, BuffId.OOBE_Prakriti_Buff); + } + } +} diff --git a/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Skill_Base.cs b/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Skill_Base.cs new file mode 100644 index 000000000..d6f78ecfd --- /dev/null +++ b/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Skill_Base.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.Linq; +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.World.Actors; +using Melia.Zone.World.Actors.Characters; +using Melia.Zone.World.Actors.CombatEntities.Components; + +namespace Melia.Zone.Skills.Handlers.Clerics.Sadhu +{ + /// + /// Base skill class Sadhu skills. + /// + public class Sadhu_Skill_Base + { + /// + /// Handles a sadhu skill for the given BuffId. + /// + /// + /// + /// + /// + /// + /// + public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target, BuffId buffId) + { + // The skill casting of this skill requires Soul Master Buff. + if (!caster.IsBuffActive(BuffId.OOBE_Soulmaster_Buff) || caster is not Character casterCharacter) + return; + + // This will happens in the second usage: returns to body. + if (caster.IsBuffActive(buffId)) + { + this.ReturnToBody(caster, skill, farPos, buffId); + return; + } + + // Prevents from spamming the skills if + // somehow the user bypass clients checks + if (casterCharacter.IsOutOfBody()) + return; + + if (!caster.TrySpendSp(skill)) + { + caster.ServerMessage(Localization.Get("Not enough SP.")); + return; + } + + farPos = caster.Position.GetRelative2D(caster.Direction, 30); + caster.SetAttackState(true); + this.SkillReady(caster, skill, farPos); + + // Checks if the [Arts] Spirit Expert: Wandering Soul is active. + // This will switch the flow of the skill. Instead of letting + // the player be the sprit and control it, a clone AI based will be created. + if (caster.IsAbilityActive(AbilityId.Sadhu35)) + { + this.CreateAISpiritClone(skill, casterCharacter, originPos, farPos, target, buffId); + return; + } + + var moveSpeedBonus = this.GetMoveSpeedBonus(skill); + + // A dummy character, it will stays on the old character position + // While the player apparence will assume the spirit form. + var dummyCharacter = casterCharacter.Clone(caster.Position); + + Send.ZC_PLAY_ANI(dummyCharacter, "BORN", false); + Send.ZC_PLAY_ANI(dummyCharacter, "skl_OOBE_loop", true); + Send.ZC_NORMAL.UnkDynamicCastStart(dummyCharacter, SkillId.None); + + this.SkillEffects(casterCharacter, dummyCharacter, dummyCharacter.Position, farPos); + + casterCharacter.Position = farPos; + Send.ZC_SET_POS(casterCharacter, farPos); + + Send.ZC_NORMAL.UpdateModelColor(casterCharacter, 255, 200, 100, 150, 0.01f); + Send.ZC_NORMAL.UnkDynamicCastStart(casterCharacter, SkillId.None); + + this.SendAvailableSkills(casterCharacter, buffId, skill); + + dummyCharacter.StartBuff(BuffId.ReduceDmgCommonAbil_Buff, 0, 0, TimeSpan.FromSeconds(10), dummyCharacter); + casterCharacter.StartBuff(buffId, moveSpeedBonus, dummyCharacter.Handle, TimeSpan.FromSeconds(10), casterCharacter); + } + + /// + /// Creates a clone of the character (spirit) that attacks nearby + /// entities and disappears after a few seconds leaving an effect + /// that works similar as skill. + /// + /// + /// + /// + /// + /// + /// + public void CreateAISpiritClone(Skill skill, Character casterCharacter, Position originPos, Position farPos, ICombatEntity target, BuffId buffId) + { + skill.IncreaseOverheat(); + + // Clones the character that will be perform + // attacks to nearby enemies and leave an buff skill + // once it disappears or dies. + var spirit = casterCharacter.Clone(farPos); + + this.SkillEffects(casterCharacter, spirit, spirit.Position, farPos); + Send.ZC_SET_POS(spirit, farPos); + + Send.ZC_NORMAL.UpdateModelColor(spirit, 255, 200, 100, 150, 0.01f); + Send.ZC_NORMAL.UnkDynamicCastStart(spirit, SkillId.None); + + spirit.StartBuff(buffId, 0, 0, TimeSpan.FromSeconds(3), spirit); + + var aiComponent = new AiComponent(spirit, "SadhuDummy"); + aiComponent.Script.SetMaster(casterCharacter); + + spirit.Components.Add(aiComponent); + } + + /// + /// Creates the skill's related effects and play animations. + /// + /// + /// + /// + /// + private void SkillEffects(Character character, IActor target, Position position, Position farPos) + { + Send.ZC_PLAY_SOUND(character, "skl_eff_yuchae_start_2"); + Send.ZC_GROUND_EFFECT(character, "I_only_quest_smoke013_blue_smoke", farPos, 1, 0.7f, 0, 0, 0); + Send.ZC_GROUND_EFFECT(character, "I_only_quest_smoke013_blue_smoke", position, 1.5f, 0.3f, 0, 0, 0); + Send.ZC_GROUND_EFFECT(character, "I_only_quest_smoke058_blue", farPos, 3f, 0.5f, 0, 0, 0); + Send.ZC_GROUND_EFFECT(character, "I_only_quest_smoke058_blue", position, 3, 0.5f, 0, 0, 0); + } + + /// + /// Returns the move speed bonus, sometimes the value can be negative. + /// + /// + private float GetMoveSpeedBonus(Skill skill) + { + return skill.Id == SkillId.Sadhu_Prakriti ? 18 - (skill.Level + 6) : 18; + } + + /// + /// Sends the list of available skills to cast to the client. + /// + /// + /// + /// + private void SendAvailableSkills(Character casterCharacter, BuffId buffId, Skill skill) + { + var sadhuSkillIds = new HashSet(ZoneServer.Instance.Data.SkillTreeDb.FindSkills(JobId.Sadhu, 45).Select(s => s.SkillId)); + + foreach (var availableSkill in casterCharacter.Skills.GetList()) + { + // Skip if it's a Sadhu skill other than the current skill + if (sadhuSkillIds.Contains(availableSkill.Id) && availableSkill.Id != skill.Id) + continue; + + Send.ZC_NORMAL.EnableUseSkillWhileOutOfBody(casterCharacter, buffId, availableSkill.Id); + } + } + + /// + /// Makes the character (spirit) returns to original body position. + /// + /// + /// + /// + /// + private async void ReturnToBody(ICombatEntity caster, Skill skill, Position farPos, BuffId buffId) + { + Send.ZC_SKILL_READY(caster, skill, caster.Position, farPos); + Send.ZC_NORMAL.UpdateSkillEffect(caster, caster.Handle, farPos, caster.Direction, Position.Zero, 1); + Send.ZC_SKILL_MELEE_GROUND(caster, skill, caster.Position, ForceId.GetNew(), null); + + await Task.Delay(TimeSpan.FromMilliseconds(750)); + + // This skill doesn't enter on cooldown on the first usage. + // But at the second usage it will return to the original + // body then it trigger the cooldown. + skill.IncreaseOverheat(); + + caster.StopBuff(buffId); + } + + /// + /// Triggers the skill usage and sends it to nearby characters. + /// + /// + /// + /// + private void SkillReady(ICombatEntity caster, Skill skill, Position farPos) + { + Send.ZC_SKILL_READY(caster, skill, caster.Position, farPos); + Send.ZC_NORMAL.UpdateSkillEffect(caster, caster.Handle, farPos, caster.Direction, Position.Zero); + Send.ZC_SKILL_MELEE_GROUND(caster, skill, caster.Position, ForceId.GetNew(), null); + } + } +} diff --git a/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Soulmaster.cs b/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Soulmaster.cs new file mode 100644 index 000000000..4da6ce86f --- /dev/null +++ b/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Soulmaster.cs @@ -0,0 +1,44 @@ +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.World.Actors; + +namespace Melia.Zone.Skills.Handlers.Clerics.Sadhu +{ + /// + /// Handler for the Cleric skill Spirit Expert (Soul Master). + /// + [SkillHandler(SkillId.Sadhu_Soulmaster)] + public class Sadhu_Soulmaster : IGroundSkillHandler + { + /// + /// Handles skill, applying buff to the caster. + /// + /// + /// + /// + /// + /// + 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); + + caster.StartBuff(BuffId.OOBE_Soulmaster_Buff, skill.Level, 0, TimeSpan.FromMinutes(30), caster); + + Send.ZC_SKILL_READY(caster, skill, caster.Position, farPos); + Send.ZC_NORMAL.UpdateSkillEffect(caster, caster.Handle, caster.Position, caster.Direction, Position.Zero); + Send.ZC_SKILL_MELEE_GROUND(caster, skill, caster.Position, ForceId.GetNew(), null); + } + } +} diff --git a/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Tanoti.cs b/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Tanoti.cs new file mode 100644 index 000000000..3cec97daf --- /dev/null +++ b/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Tanoti.cs @@ -0,0 +1,27 @@ +using Melia.Shared.Game.Const; +using Melia.Shared.World; +using Melia.Zone.Skills.Handlers.Base; +using Melia.Zone.World.Actors; + +namespace Melia.Zone.Skills.Handlers.Clerics.Sadhu +{ + /// + /// Handler for the Sadhu skill Tanoti. + /// + [SkillHandler(SkillId.Sadhu_Tanoti)] + public class Sadhu_Tanoti : Sadhu_Skill_Base, IGroundSkillHandler + { + /// + /// Handles skill, makes the character out of body. + /// + /// + /// + /// + /// + /// + public void Handle(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target) + { + this.Handle(skill, caster, originPos, farPos, target, BuffId.OOBE_Tanoti_Buff); + } + } +} diff --git a/src/ZoneServer/World/Actors/Characters/Character.cs b/src/ZoneServer/World/Actors/Characters/Character.cs index 0d2d659c9..8bc701a51 100644 --- a/src/ZoneServer/World/Actors/Characters/Character.cs +++ b/src/ZoneServer/World/Actors/Characters/Character.cs @@ -21,6 +21,9 @@ using Yggdrasil.Logging; using Yggdrasil.Scheduling; using Yggdrasil.Util; +using System.Collections.Generic; +using Melia.Zone.Skills; +using Melia.Zone.World.Items; namespace Melia.Zone.World.Actors.Characters { @@ -612,7 +615,7 @@ public void Warp(int mapId, Position pos) { if (!ZoneServer.Instance.Data.MapDb.TryFind(mapId, out var map)) throw new ArgumentException("Map '" + mapId + "' not found in data."); - + this.Position = pos; if (this.MapId == mapId) @@ -1309,6 +1312,8 @@ public void Kill(ICombatEntity killer) Send.ZC_DEAD(this); + this.Died?.Invoke(this, killer); + _resurrectDialogTimer = ResurrectDialogDelay; } @@ -1493,5 +1498,88 @@ public void ChangeHair(int hairTypeIndex) this.Hair = hairTypeIndex; Send.ZC_UPDATED_PCAPPEARANCE(this); } + + /// + /// Clones the character within it's same appearance and + /// spawns it on the current map at a given position. + /// + /// + public Character Clone(Position position) + { + var dummyCharacter = new DummyCharacter(); + + dummyCharacter.DbId = this.DbId; + dummyCharacter.AccountId = this.AccountId; + dummyCharacter.Name = this.Name; + dummyCharacter.TeamName = this.TeamName; + dummyCharacter.JobId = this.JobId; + dummyCharacter.Gender = this.Gender; + dummyCharacter.Hair = this.Hair; + dummyCharacter.SkinColor = this.SkinColor; + dummyCharacter.MapId = this.MapId; + + dummyCharacter.Position = position; + dummyCharacter.Direction = this.Direction; + + foreach (var item in this.Inventory.GetEquip()) + { + var newItem = new Item(item.Value.Id, item.Value.Amount); + dummyCharacter.Inventory.SetEquipSilent(item.Key, newItem); + } + + foreach (var job in this.Jobs.GetList()) + { + dummyCharacter.Jobs.AddSilent(new Job(dummyCharacter, job.Id)); + } + + foreach (var skill in this.Skills.GetList()) + { + var newSkill = new Skill(dummyCharacter, skill.Id, skill.Level); + dummyCharacter.Skills.AddSilent(newSkill); + } + + dummyCharacter.InitProperties(); + dummyCharacter.Properties.Stamina = (int)this.Properties.GetFloat(PropertyName.MaxSta); + dummyCharacter.UpdateStance(); + dummyCharacter.ModifyHpSafe(this.MaxHp, out var hp, out var priority); + + dummyCharacter.Owner = this; + + this.Map.AddCharacter(dummyCharacter); + + Send.ZC_ENTER_PC(this.Connection, dummyCharacter); + Send.ZC_OWNER(this, dummyCharacter, this.Handle); + Send.ZC_UPDATED_PCAPPEARANCE(dummyCharacter); + + Send.ZC_NORMAL.HeadgearVisibilityUpdate(dummyCharacter); + + return dummyCharacter; + } + + /// + /// Return true in case of the character has used Out Of Body Skill + /// + /// + public bool IsOutOfBody() + { + var sadhuBuffList = new List() + { + BuffId.OOBE_Prakriti_Buff, + BuffId.OOBE_Anila_Buff, + BuffId.OOBE_Possession_Buff, + BuffId.OOBE_Patati_Buff, + BuffId.OOBE_Moksha_Buff, + BuffId.OOBE_Tanoti_Buff, + BuffId.OOBE_Strong_Buff, + BuffId.OOBE_Stack_Buff + }; + + foreach (var buffId in sadhuBuffList) { + if (this.IsBuffActive(buffId)) + return true; + } + + return false; + } } } diff --git a/src/ZoneServer/World/Actors/Characters/CharacterProperties.cs b/src/ZoneServer/World/Actors/Characters/CharacterProperties.cs index 7c1bc99a1..befdc5e1b 100644 --- a/src/ZoneServer/World/Actors/Characters/CharacterProperties.cs +++ b/src/ZoneServer/World/Actors/Characters/CharacterProperties.cs @@ -1,12 +1,11 @@ using System; using Melia.Shared.Game.Const; using Melia.Shared.ObjectProperties; +using Melia.Zone.Buffs; using Melia.Zone.Network; -using Yggdrasil.Util; using Melia.Zone.Scripting; using Melia.Zone.World.Items; -using Melia.Zone.Buffs; -using Melia.Shared.Data.Database; +using Yggdrasil.Util; namespace Melia.Zone.World.Actors.Characters { @@ -210,6 +209,10 @@ public void InitAutoUpdates() /// private void InitEvents() { + // This was done to avoid sending packets for dummies (while receiving buffs) + if (this.Character.Connection == null) + return; + // Update recovery times when the character sits down, // as those properties are affected by the sitting status. this.Character.SitStatusChanged += this.SitStatusChanged; diff --git a/src/ZoneServer/World/Actors/Characters/DummyCharacter.cs b/src/ZoneServer/World/Actors/Characters/DummyCharacter.cs new file mode 100644 index 000000000..28258b098 --- /dev/null +++ b/src/ZoneServer/World/Actors/Characters/DummyCharacter.cs @@ -0,0 +1,31 @@ +using Melia.Zone.Network; + +namespace Melia.Zone.World.Actors.Characters +{ + /// + /// Represents a dummy character. + /// + public class DummyCharacter : Character + { + /// + /// Returns reference to the character's owner (In case of being a dummy). + /// + public Character Owner { get; set; } + + /// + /// Returns true if the DummyCharacter has Owner + /// + public bool HasOwner => Owner != null; + + /// + /// Despawns/Removes this entity from the map. + /// + public void Despawn() + { + Send.ZC_OWNER(this.Owner, this, 0); + Send.ZC_LEAVE(this); + + this.Map.RemoveCharacter(this); + } + } +} diff --git a/src/ZoneServer/World/Actors/CombatEntities/Components/CooldownComponent.cs b/src/ZoneServer/World/Actors/CombatEntities/Components/CooldownComponent.cs index c64819256..9cd64c043 100644 --- a/src/ZoneServer/World/Actors/CombatEntities/Components/CooldownComponent.cs +++ b/src/ZoneServer/World/Actors/CombatEntities/Components/CooldownComponent.cs @@ -49,7 +49,7 @@ public void Start(CooldownId cooldownId, TimeSpan duration) } } - if (this.Entity is Character character) + if (this.Entity is Character character && this.Entity is not DummyCharacter) Send.ZC_COOLDOWN_CHANGED(character, cooldown); } diff --git a/src/ZoneServer/World/Actors/CombatEntities/Components/MovementComponent.cs b/src/ZoneServer/World/Actors/CombatEntities/Components/MovementComponent.cs index 02bc99b6b..01427540c 100644 --- a/src/ZoneServer/World/Actors/CombatEntities/Components/MovementComponent.cs +++ b/src/ZoneServer/World/Actors/CombatEntities/Components/MovementComponent.cs @@ -404,13 +404,23 @@ private void CheckWarp() /// public void SetMoveSpeedType(MoveSpeedType type) { - if (this.Entity is Mob mob && this.MoveSpeedType != type) + if (this.MoveSpeedType == type) + return; + + if (this.Entity is Mob mob) { this.MoveSpeedType = type; this.Entity.Properties.Invalidate(PropertyName.MSPD); Send.ZC_MSPD(this.Entity); } + else if (this.Entity is DummyCharacter dummyCharacter) + { + this.MoveSpeedType = type; + dummyCharacter.Properties.Invalidate(PropertyName.MSPD); + + Send.ZC_MSPD(dummyCharacter); + } } /// diff --git a/src/ZoneServer/World/Maps/Map.cs b/src/ZoneServer/World/Maps/Map.cs index d65b07dd7..738e037d9 100644 --- a/src/ZoneServer/World/Maps/Map.cs +++ b/src/ZoneServer/World/Maps/Map.cs @@ -794,7 +794,7 @@ public virtual void Broadcast(Packet packet) lock (_characters) { foreach (var character in _characters.Values) - character.Connection.Send(packet); + character.Connection?.Send(packet); } } @@ -810,7 +810,7 @@ public virtual void Broadcast(Packet packet, IActor source, bool includeSource = lock (_characters) { foreach (var character in _characters.Values.Where(a => (includeSource || a != source) && a.Position.InRange2D(source.Position, VisibleRange))) - character.Connection.Send(packet); + character.Connection?.Send(packet); } } } diff --git a/system/scripts/zone/ais/sadhu_dummy.cs b/system/scripts/zone/ais/sadhu_dummy.cs new file mode 100644 index 000000000..2fd7bf021 --- /dev/null +++ b/system/scripts/zone/ais/sadhu_dummy.cs @@ -0,0 +1,198 @@ +using System.Collections; +using System.Collections.Generic; +using Melia.Shared.Game.Const; +using Melia.Zone; +using Melia.Zone.Network; +using Melia.Zone.Scripting; +using Melia.Zone.Scripting.AI; +using Melia.Zone.Skills; +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 Melia.Zone.World.Actors.CombatEntities.Components; + +[Ai("SadhuDummy")] +public class SadhuDummyAiScript : AiScript +{ + private const int MaxChaseDistance = 100; + private const int MaxMasterDistance = 120; + private const int AttackRange = 15; + + ICombatEntity target; + + protected override void Setup() + { + SetViewDistance(350); + + SetTendency(TendencyType.Aggressive); + HatesFaction(FactionType.Peaceful); + HatesFaction(FactionType.Pet); + HatesFaction(FactionType.Monster); + HatesFaction(FactionType.Neutral); + HatesFaction(FactionType.Summon); + + During("Idle", CheckEnemies); + During("Attack", CheckTarget); + During("Attack", CheckMaster); + } + + protected override void Root() + { + StartRoutine("Idle", Idle()); + } + + protected IEnumerable Idle() + { + var movement = this.Entity.Components.Get(); + + movement.SetMoveSpeedType(MoveSpeedType.Run); + + if (this.Entity is Character entityCharacter && this.Entity is DummyCharacter dummyCharacter) + { + SetFixedMoveSpeed(85); + Send.ZC_MSPD(entityCharacter); + } + + var master = GetMaster(); + if (master != null && !InRangeOf(master, MaxMasterDistance)) + { + yield return Follow(master, 25, true, false); + yield break; + } + + yield return Wait(250, 500); + } + + protected IEnumerable Attack() + { + // Remove the dummy character if the master is gone + if (TryGetMaster(out var master) && EntityGone(master) && this.Entity is DummyCharacter dummyCharacter) + { + dummyCharacter.Despawn(); + yield break; + } + + while (!target.IsDead) + { + if (!CanUseAutoAttackSkill(SkillId.Normal_Attack, out var skill)) + { + if (CanUseAutoAttackSkill(SkillId.Hammer_Attack, out var skillHammer)) + { + skill = skillHammer; + } else + { + yield return Wait(3000); + continue; + } + } + + yield return StopMove(); + + yield return UseAutoAttackSkill(skill, target); + yield return Wait(100, 200); + } + + yield break; + } + + protected IEnumerable StopAndIdle() + { + yield return StopMove(); + StartRoutine("Idle", Idle()); + } + + protected IEnumerable StopAndAttack() + { + yield return StopMove(); + StartRoutine("Attack", Attack()); + } + + protected IEnumerable MoveToTarget() + { + ExecuteOnce(TurnTowards(target)); + yield return MoveTo(target.Position.GetRelative2D(this.Entity.Position, AttackRange - 5), wait: false); + + if (InRangeOf(this.Entity, AttackRange)) + { + StartRoutine("StopAndAttack", StopAndAttack()); + } + } + + private IEnumerable UseAutoAttackSkill(Skill skill, ICombatEntity target) + { + this.Entity.TurnTowards(target); + + if (!ZoneServer.Instance.SkillHandlers.TryGetHandler(skill.Id, out var handler)) + { + yield return this.Wait(3000); + yield break; + } + + var targets = new List(); + targets.Add(target); + + handler.Handle(skill, this.Entity, this.Entity.Position, this.Entity.Position, targets); + + var useTime = skill.Properties.ShootTime; + yield return this.Wait(useTime); + } + + private bool CanUseAutoAttackSkill(SkillId skillId, out Skill skill) + { + skill = null; + return this.Entity.Components.Get()?.TryGet(skillId, out skill) ?? false; + } + + private void CheckEnemies() + { + if (target != null && !target.IsDead) + { + return; + } + + var attackableEntities = this.Entity.Map.GetAttackableEntitiesInRange(this.Entity, Entity.Position, MaxChaseDistance); + + if (attackableEntities != null && attackableEntities.Count > 0) + { + var closestEnemy = attackableEntities[0]; + + foreach (var enemy in attackableEntities) + { + if (enemy.Position.Get2DDistance(this.Entity.Position) < closestEnemy.Position.Get2DDistance(this.Entity.Position)) + { + closestEnemy = enemy; + } + } + + target = closestEnemy; + StartRoutine("MoveToTarget", MoveToTarget()); + } + } + + private void CheckTarget() + { + // Transition to idle if the target has vanished or is out of range + if (EntityGone(target) || !InRangeOf(target, MaxChaseDistance)) + { + target = null; + StartRoutine("StopAndIdle", StopAndIdle()); + } + } + + private void CheckMaster() + { + if (target == null) + return; + + if (!TryGetMaster(out var master)) + return; + + // Reset aggro if the master is out of range + if (!InRangeOf(master, MaxMasterDistance)) + { + target = null; + StartRoutine("StopAndIdle", StopAndIdle()); + } + } +} diff --git a/system/scripts/zone/core/calc_combat.cs b/system/scripts/zone/core/calc_combat.cs index fcf215483..beb8d8a93 100644 --- a/system/scripts/zone/core/calc_combat.cs +++ b/system/scripts/zone/core/calc_combat.cs @@ -535,6 +535,8 @@ public SkillHitResult SCR_SkillHit(ICombatEntity attacker, ICombatEntity target, var buffComponent = attacker.Components.Get(); if (buffComponent.Has(BuffId.Cloaking_Buff)) buffComponent.Remove(BuffId.Cloaking_Buff); + if (target.IsBuffActive(BuffId.Skill_NoDamage_Buff)) + result.Damage = 0; return result; } diff --git a/system/scripts/zone/other/character_adv.cs b/system/scripts/zone/other/character_adv.cs index 4095149b3..af31e3859 100644 --- a/system/scripts/zone/other/character_adv.cs +++ b/system/scripts/zone/other/character_adv.cs @@ -793,7 +793,6 @@ private static void GrantDefaults(Character character, JobId jobId) { LearnSkill(character, SkillId.Hammer_Attack); LearnSkill(character, SkillId.Hammer_Attack_TH); - LearnSkill(character, SkillId.Sadhu_OutofBodyCancel); GiveItem(character, ItemId.Costume_Char4_6, 1); break;