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 40e0856cc..485af5d1f 100644 --- a/src/Shared/Network/NormalOp.cs +++ b/src/Shared/Network/NormalOp.cs @@ -39,6 +39,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; @@ -67,6 +68,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..e6f6bb810 --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Anila_Buff.cs @@ -0,0 +1,131 @@ +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 + { + public override void OnActivate(Buff buff, ActivationType activationType) + { + var caster = buff.Caster; + + // [Arts] Spirit Expert: Wandering Soul + if (caster.IsAbilityActive(AbilityId.Sadhu35) || caster is not Character casterCharacter) + return; + + var dummyCharacter = casterCharacter.Map.GetDummyCharacter((int)buff.NumArg2); + + if (dummyCharacter != null) + { + dummyCharacter.Died += this.OnDummyDied; + } + } + + public override void OnEnd(Buff buff) + { + var caster = buff.Caster; + + if (caster is not Character casterCharacter) + return; + + var skillCharacter = casterCharacter is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + if (skillCharacter.TryGetSkill(SkillId.Sadhu_Anila, out var skill)) + { + skillCharacter.SetAttackState(true); + + var pad = new Pad(PadName.Sadhu_Anila_Effect_Pad, skillCharacter, skill, new Square(caster.Position, caster.Direction, 50, 65)); + + pad.Position = caster.Position; + 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 + 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 + { + skill.IncreaseOverheat(); + } + } + + // [Arts] Spirit Expert: Wandering Soul + if (casterCharacter is DummyCharacter dummyCharacter3 && dummyCharacter3.Owner.IsAbilityActive(AbilityId.Sadhu35)) + { + this.RemoveDummyCharacter(dummyCharacter3); + return; + } + + casterCharacter.Properties.Modify(PropertyName.MSPD_BM, -buff.NumArg1); + + 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); + } + + /// + /// Attacks the target + /// + /// + /// + private void Attack(Skill skill, ICombatEntity caster, ICombatEntity target) + { + var modifier = SkillModifier.MultiHit(3); + var skillHitResult = SCR_SkillHit(caster, target, skill, modifier); + + target.TakeDamage(skillHitResult.Damage, caster); + + var hit = new HitInfo(caster, target, skill, skillHitResult, TimeSpan.FromMilliseconds(200)); + + Send.ZC_HIT_INFO(caster, 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..6a1f6aef5 --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Moksha_Buff.cs @@ -0,0 +1,185 @@ +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; +using System.Collections.Generic; + +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 + { + public override void OnActivate(Buff buff, ActivationType activationType) + { + var caster = buff.Caster; + + // [Arts] Spirit Expert: Wandering Soul + if (caster.IsAbilityActive(AbilityId.Sadhu35) || caster is not Character casterCharacter) + return; + + var dummyCharacter = casterCharacter.Map.GetDummyCharacter((int)buff.NumArg2); + + if (dummyCharacter != null) + { + dummyCharacter.Died += this.OnDummyDied; + } + } + + public override void OnEnd(Buff buff) + { + var caster = buff.Caster; + + if (caster is not Character casterCharacter) + return; + + var skillCharacter = casterCharacter is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + if (skillCharacter.TryGetSkill(SkillId.Sadhu_Moksha, out var skill)) + { + skillCharacter.SetAttackState(true); + + var pad = new Pad(PadName.Sadhu_Moksha_Pad, skillCharacter, 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 + 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 + { + skill.IncreaseOverheat(); + } + } + + // [Arts] Spirit Expert: Wandering Soul + if (casterCharacter is DummyCharacter dummyCharacter3 && dummyCharacter3.Owner.IsAbilityActive(AbilityId.Sadhu35)) + { + this.RemoveDummyCharacter(dummyCharacter3); + return; + } + + casterCharacter.Properties.Modify(PropertyName.MSPD_BM, -buff.NumArg1); + + 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); + } + + /// + /// Attacks the target + /// + /// + /// + private void Attack(Skill skill, ICombatEntity caster, ICombatEntity target) + { + var skillHitResult = SCR_SkillHit(caster, target, skill); + + target.TakeDamage(skillHitResult.Damage, caster); + + var hit = new HitInfo(caster, target, skill, skillHitResult, TimeSpan.FromMilliseconds(0)); + + Send.ZC_HIT_INFO(caster, 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 skillHitResult = SCR_SkillHit(caster, target, skill); + + target.TakeDamage(skillHitResult.Damage, caster); + + // 6 Consecutive hits instead of a single packet + for (int i = 0; i < 6; i++) + { + var hit = new HitInfo(caster, target, skill, skillHitResult, TimeSpan.FromMilliseconds(i * 150)); + Send.ZC_HIT_INFO(caster, target, 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..dd087169b --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Patati_Buff.cs @@ -0,0 +1,152 @@ +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; + + public override void OnActivate(Buff buff, ActivationType activationType) + { + var caster = buff.Caster; + + // [Arts] Spirit Expert: Wandering Soul + if (caster.IsAbilityActive(AbilityId.Sadhu35) || caster is not Character casterCharacter) + return; + + var dummyCharacter = casterCharacter.Map.GetDummyCharacter((int)buff.NumArg2); + + if (dummyCharacter != null) + { + dummyCharacter.Died += this.OnDummyDied; + } + } + + public override void OnEnd(Buff buff) + { + var caster = buff.Caster; + + if (caster is not Character casterCharacter) + return; + + var skillCharacter = casterCharacter is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + if (skillCharacter.TryGetSkill(SkillId.Sadhu_Patati, out var skill)) + { + skillCharacter.SetAttackState(true); + + this.AreaOfEffect(caster, skill, caster.Position); + + // [Arts] Spirit Expert: Wandering Soul + 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 + { + skill.IncreaseOverheat(); + } + } + + // [Arts] Spirit Expert: Wandering Soul + if (casterCharacter is DummyCharacter dummyCharacter3 && dummyCharacter3.Owner.IsAbilityActive(AbilityId.Sadhu35)) + { + this.RemoveDummyCharacter(dummyCharacter3); + return; + } + + casterCharacter.Properties.Modify(PropertyName.MSPD_BM, -buff.NumArg1); + + 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 target + /// + /// + /// + private void Attack(Skill skill, ICombatEntity caster, ICombatEntity target) + { + var modifier = SkillModifier.MultiHit(6); + var skillHitResult = SCR_SkillHit(caster, target, skill, modifier); + + target.TakeDamage(skillHitResult.Damage, caster); + + var hit = new HitInfo(caster, target, skill, skillHitResult, TimeSpan.FromMilliseconds(200)); + + Send.ZC_HIT_INFO(caster, 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..353cc3794 --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Possession_Buff.cs @@ -0,0 +1,141 @@ +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 that case it changes + private const bool ApplySelfHold = false; + + public override void OnActivate(Buff buff, ActivationType activationType) + { + var caster = buff.Caster; + + // [Arts] Spirit Expert: Wandering Soul + if (caster.IsAbilityActive(AbilityId.Sadhu35) || caster is not Character casterCharacter) + return; + + var dummyCharacter = casterCharacter.Map.GetDummyCharacter((int)buff.NumArg2); + + if (dummyCharacter != null) + { + dummyCharacter.Died += this.OnDummyDied; + } + } + + public override void OnEnd(Buff buff) + { + var caster = buff.Caster; + + if (caster is not Character casterCharacter) + return; + + var skillCharacter = casterCharacter is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + if (skillCharacter.TryGetSkill(SkillId.Sadhu_Possession, out var skill)) + { + skillCharacter.SetAttackState(true); + + this.AreaOfEffect(skillCharacter, skill, caster.Position); + + if (ApplySelfHold && casterCharacter is not DummyCharacter) + skillCharacter.StartBuff(BuffId.Common_Hold, TimeSpan.FromMilliseconds(this.GetHoldTime(skill))); + + // [Arts] Spirit Expert: Wandering Soul + 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 + { + skill.IncreaseOverheat(); + } + } + + // [Arts] Spirit Expert: Wandering Soul + if (casterCharacter is DummyCharacter dummyCharacter3 && dummyCharacter3.Owner.IsAbilityActive(AbilityId.Sadhu35)) + { + this.RemoveDummyCharacter(dummyCharacter3); + return; + } + + casterCharacter.Properties.Modify(PropertyName.MSPD_BM, -buff.NumArg1); + + 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); + } + + /// + /// Returns the amount of hold time in milliseconds + /// + /// + private int GetHoldTime(Skill skill) + { + return 1000 + (300 * skill.Level); + } + + /// + /// 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 modifier = SkillModifier.MultiHit(5); + var skillHitResult = SCR_SkillHit(caster, target, skill, modifier); + + target.TakeDamage(skillHitResult.Damage, caster); + + var hit = new HitInfo(caster, target, skill, skillHitResult, TimeSpan.FromMilliseconds(200)); + + Send.ZC_HIT_INFO(caster, target, hit); + } + } +} 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..20ac0ebe9 --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Prakriti_Buff.cs @@ -0,0 +1,133 @@ +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 + { + public override void OnActivate(Buff buff, ActivationType activationType) + { + var caster = buff.Caster; + + // [Arts] Spirit Expert: Wandering Soul + if (caster.IsAbilityActive(AbilityId.Sadhu35) || caster is not Character casterCharacter) + return; + + var dummyCharacter = casterCharacter.Map.GetDummyCharacter((int)buff.NumArg2); + + if (dummyCharacter != null) + { + dummyCharacter.Died += this.OnDummyDied; + } + } + + public override void OnEnd(Buff buff) + { + var caster = buff.Caster; + + if (caster is not Character casterCharacter) + return; + + var skillCharacter = casterCharacter is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + if (skillCharacter.TryGetSkill(SkillId.Sadhu_Prakriti, out var skill)) + { + skillCharacter.SetAttackState(true); + + 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 + 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 + { + skill.IncreaseOverheat(); + } + } + + // [Arts] Spirit Expert: Wandering Soul + if (casterCharacter is DummyCharacter dummyCharacter3 && dummyCharacter3.Owner.IsAbilityActive(AbilityId.Sadhu35)) + { + this.RemoveDummyCharacter(dummyCharacter3); + return; + } + + casterCharacter.Properties.Modify(PropertyName.MSPD_BM, -buff.NumArg1); + + 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)) + { + target.StartBuff(BuffId.Common_Hold, 0, 0, TimeSpan.FromMilliseconds(5500), caster); + this.Attack(skill, caster, target); + } + } + + /// + /// Attacks the target + /// + /// + /// + private void Attack(Skill skill, ICombatEntity caster, ICombatEntity target) + { + var skillHitResult = SCR_SkillHit(caster, target, skill); + + target.TakeDamage(skillHitResult.Damage, caster); + + var hit = new HitInfo(caster, target, skill, skillHitResult); + hit.Type = HitType.Type33; + + Send.ZC_HIT_INFO(caster, target, hit); + } + } +} 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..47639010c --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/OOBE_Tanoti_Buff.cs @@ -0,0 +1,150 @@ +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; + + public override void OnActivate(Buff buff, ActivationType activationType) + { + var caster = buff.Caster; + + // [Arts] Spirit Expert: Wandering Soul + if (caster.IsAbilityActive(AbilityId.Sadhu35) || caster is not Character casterCharacter) + return; + + var dummyCharacter = casterCharacter.Map.GetDummyCharacter((int)buff.NumArg2); + + if (dummyCharacter != null) + { + dummyCharacter.Died += this.OnDummyDied; + } + } + + public override void OnEnd(Buff buff) + { + var caster = buff.Caster; + + if (caster is not Character casterCharacter) + return; + + var skillCharacter = casterCharacter is DummyCharacter dummyCharacter && dummyCharacter.Owner.IsAbilityActive(AbilityId.Sadhu35) + ? dummyCharacter.Owner + : caster; + + if (skillCharacter.TryGetSkill(SkillId.Sadhu_Tanoti, out var skill)) + { + skillCharacter.SetAttackState(true); + + this.AreaOfEffect(caster, skill, caster.Position); + + // [Arts] Spirit Expert: Wandering Soul + 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 + { + skill.IncreaseOverheat(); + } + } + + // [Arts] Spirit Expert: Wandering Soul + if (casterCharacter is DummyCharacter dummyCharacter3 && dummyCharacter3.Owner.IsAbilityActive(AbilityId.Sadhu35)) + { + this.RemoveDummyCharacter(dummyCharacter3); + return; + } + + casterCharacter.Properties.Modify(PropertyName.MSPD_BM, -buff.NumArg1); + + 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 modifier = SkillModifier.MultiHit(5); + var skillHitResult = SCR_SkillHit(caster, target, skill, modifier); + + target.TakeDamage(skillHitResult.Damage, caster); + + var hit = new HitInfo(caster, target, skill, skillHitResult, TimeSpan.FromMilliseconds(200)); + + Send.ZC_HIT_INFO(caster, 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..3683f286d --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Clerics/Sadhu/Sadhu_BuffHandler_Base.cs @@ -0,0 +1,64 @@ +using Melia.Shared.Game.Const; +using Melia.Zone.Buffs.Base; +using Melia.Zone.Network; +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 + { + /// + /// Remove the dummy character from the map + /// + /// + protected void RemoveDummyCharacter(DummyCharacter dummyCharacter) + { + Send.ZC_OWNER(dummyCharacter.Owner, dummyCharacter, 0); + Send.ZC_LEAVE(dummyCharacter); + + dummyCharacter.Map.RemoveDummyCharacter(dummyCharacter); + } + + /// + /// Makes the chararacter returns to original position + /// and also get ride of the dummy character + /// + /// + /// + protected void ReturnToBody(Character character, int dummyHandle) + { + var dummyCharacter = character.Map.GetDummyCharacter(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.RemoveDummyCharacter(dummyCharacter); + } + + /// + /// Called when the dummy character died + /// disappeared. + /// + /// + /// + protected void OnDummyDied(Character character, ICombatEntity killer) + { + if (character is DummyCharacter dummyCharacter) + dummyCharacter.Owner.StopBuff(BuffId.OOBE_Anila_Buff); + } + } +} 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..9bc8bcbec 100644 --- a/src/ZoneServer/Network/Send.Normal.cs +++ b/src/ZoneServer/Network/Send.Normal.cs @@ -340,11 +340,21 @@ public static void AttackCancel(Character character) /// /// public static void UnkDynamicCastStart(Character character, SkillId skillId) + => UnkDynamicCastStart(character, character.Handle, skillId); + + /// + /// Packet with unknown purpose that's sent during dynamic + /// casting. + /// + /// + /// + /// + public static void UnkDynamicCastStart(Character character, int targetHandle, SkillId skillId) { var packet = new Packet(Op.ZC_NORMAL); packet.PutInt(NormalOp.Zone.UnkDynamicCastStart); - packet.PutInt(character.Handle); + packet.PutInt(targetHandle); packet.PutInt((int)skillId); character.Connection.Send(packet); @@ -560,16 +570,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 +661,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 +690,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 +1330,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) + => UpdateModelColor(character, character.Handle, red, green, blue, alpha, f1); + + /// + /// Updates the entity model color + /// + /// + /// + /// + /// + /// + /// + /// + public static void UpdateModelColor(Character character, int targetHandle, int red, int green, int blue, int alpha, float f1) + { + var packet = new Packet(Op.ZC_NORMAL); + packet.PutInt(NormalOp.Zone.UpdateModelColor); + + packet.PutInt(targetHandle); + 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, int skillId) + { + var packet = new Packet(Op.ZC_NORMAL); + packet.PutInt(NormalOp.Zone.EnableUseSkillWhileOutOfBody); + + packet.PutInt(character.Handle); + packet.PutLpString(buffId.ToString()); + packet.PutInt(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) + { + var packet = new Packet(Op.ZC_NORMAL); + packet.PutInt(NormalOp.Zone.EndOutOfBodyBuff); + + packet.PutInt(character.Handle); + packet.PutLpString(buffId.ToString()); + + character.Connection.Send(packet); + } + /// /// Exact purpose unknown, used in some skills when there's no target. /// @@ -1343,6 +1448,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 7320a1af2..271b01bae 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 @@ -124,6 +125,8 @@ public static void ZC_MYPC_ENTER(Character character) /// /// /// + /// + /// public static void ZC_ENTER_PC(IZoneConnection conn, Character character) { var packet = new Packet(Op.ZC_ENTER_PC); @@ -148,7 +151,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); @@ -458,6 +461,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); @@ -474,10 +488,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); @@ -576,7 +590,7 @@ public static void ZC_OVERHEAT_CHANGED(Character character, Skill skill) packet.PutInt((int)overheatTime); packet.PutInt(0); packet.PutInt((int)resetTime); - packet.PutInt(4352); + packet.PutInt(47872); packet.PutLong(0); character.Connection.Send(packet); @@ -1254,6 +1268,7 @@ public static void ZC_ITEM_INVENTORY_INDEX_LIST(Character character, IDictionary /// their appearance. /// /// + /// public static void ZC_UPDATED_PCAPPEARANCE(Character character) { var packet = new Packet(Op.ZC_UPDATED_PCAPPEARANCE); @@ -1358,10 +1373,17 @@ public static void ZC_SET_POS(IActor actor) /// /// public static void ZC_SET_POS(IActor actor, Position pos) + => ZC_SET_POS(actor, actor.Handle, actor.Position); + + /// + /// Broadcasts ZC_SET_POS in range of actor, updating its position. + /// + /// + public static void ZC_SET_POS(IActor actor, int targetHandle, Position pos) { var packet = new Packet(Op.ZC_SET_POS); - packet.PutInt(actor.Handle); + packet.PutInt(targetHandle); packet.PutPosition(pos); packet.PutByte(0); @@ -1489,6 +1511,10 @@ public static void ZC_OBJECT_PROPERTY(IZoneConnection conn, IPropertyObject obj, /// public static void ZC_OBJECT_PROPERTY(IZoneConnection conn, long objectId, PropertyList propertyList) { + // TODO: Find a better way to check this - This was done to avoid sending packets for dummies (while receiving buffs) + if (conn == null) + return; + var packet = new Packet(Op.ZC_OBJECT_PROPERTY); packet.PutLong(objectId); @@ -2201,21 +2227,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); } /// @@ -2251,11 +2291,11 @@ 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); } @@ -3183,7 +3223,7 @@ public static void ZC_TRUST_INFO(IZoneConnection conn) /// 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. - public static void ZC_PLAY_ANI(IActor actor, string animationName, bool stopOnLastFrame = false) + public static void ZC_PLAY_ANI(IActor actor, string animationName, bool stopOnLastFrame = false, bool unknowBoolean = false) { var packet = new Packet(Op.ZC_PLAY_ANI); @@ -3194,10 +3234,9 @@ public static void ZC_PLAY_ANI(IActor actor, string animationName, bool stopOnLa packet.PutFloat(0); packet.PutFloat(1); - // [i373230] Maybe added earlier + // [i373230] Maybe added earlier [Updated ib 28/07/24] { - packet.PutByte(0); - packet.PutByte(0); + packet.PutShort(unknowBoolean ? 248 : 0); } actor.Map.Broadcast(packet, actor); @@ -3236,15 +3275,23 @@ public static void ZC_PCBANG_POINT(IZoneConnection conn) } /// - /// Updates character's movement speed. + /// Updates entity's movement speed. /// - /// + /// public static void ZC_MSPD(ICombatEntity entity) + => ZC_MSPD(entity, entity); + + /// + /// Updates entity movement speed for another entity that has connection. + /// + /// + /// + public static void ZC_MSPD(ICombatEntity entity, ICombatEntity target) { var packet = new Packet(Op.ZC_MSPD); - packet.PutInt(entity.Handle); - packet.PutFloat(entity.Properties.GetFloat(PropertyName.MSPD)); + packet.PutInt(target.Handle); + packet.PutFloat(target.Properties.GetFloat(PropertyName.MSPD)); packet.PutLong(0); entity.Map.Broadcast(packet, entity); @@ -4384,6 +4431,39 @@ 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) + { + if (!ZoneServer.Instance.Data.PacketStringDb.TryFind(packetString, out var packetStringData)) + throw new ArgumentException($"Packet string '{packetString}' not found."); + + var packet = new Packet(Op.ZC_GROUND_EFFECT); + + packet.PutInt(entity.Handle); + packet.PutInt(packetStringData.Id); + 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..026308a63 --- /dev/null +++ b/src/ZoneServer/Pads/Handlers/Cleric/Sadhu/Sadhu_Anila_Effect_Pad.cs @@ -0,0 +1,67 @@ +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; + +namespace Melia.Zone.Pads.Handlers.Scout.Ardito +{ + /// + /// 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 = 200; + private const float PadMoveSpeedForward = 250; + + /// + /// 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); + + 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); + + this.MovePad(pad, args.Creator); + } + + /// + /// Makes pad moves forwards + /// + /// + /// + private async void MovePad(Pad pad, ICombatEntity creator) + { + // Forward and back, hovering a moment in between. + var dest = creator.Position.GetRelative(creator.Direction, PadMoveDistance); + var moveTime = pad.Movement.MoveTo(dest); + + await Task.Delay(moveTime); + await Task.Delay(300); + + 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..c0c2cd000 --- /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.Scout.Ardito +{ + /// + /// Handler for the Sadhu Prakriti 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..1b68e1ec4 --- /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.Scout.Ardito +{ + /// + /// 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..724056ccc 100644 --- a/src/ZoneServer/Scripting/AI/AiScript.Routines.cs +++ b/src/ZoneServer/Scripting/AI/AiScript.Routines.cs @@ -276,7 +276,7 @@ protected IEnumerable Animation(string packetString) /// 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. /// - 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 +327,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 30bf184e4..5b695dff9 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..c284b2fa7 --- /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) + { + 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..d39502b68 --- /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) + { + 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..0b3ff78ae --- /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) + { + 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..86243a39c --- /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) + { + 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..945d654cd --- /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) + { + 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..27e1e6c9e --- /dev/null +++ b/src/ZoneServer/Skills/Handlers/Clerics/Sadhu/Sadhu_Skill_Base.cs @@ -0,0 +1,271 @@ +using System; +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.Characters.Components; +using Melia.Zone.World.Actors.CombatEntities.Components; +using Melia.Zone.World.Items; +using Yggdrasil.Logging; + +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) + { + if (!caster.IsBuffActive(BuffId.OOBE_Soulmaster_Buff) || caster is not Character casterCharacter) + return; + + if (caster.IsAbilityActive(AbilityId.Sadhu35)) + { + this.CreateClone(skill, caster, originPos, farPos, target, buffId); + return; + } + + // This skill doesn't enter on Cooldown on the first usage. + // On the second usage it will return to the original body. + if (caster.IsBuffActive(buffId)) + { + this.ReturnToBody(caster, skill, farPos, buffId); + return; + } + + if (!caster.TrySpendSp(skill)) + { + caster.ServerMessage(Localization.Get("Not enough SP.")); + return; + } + + farPos = caster.Position.GetRelative(caster.Direction, 30); + + caster.SetAttackState(true); + this.SkillReady(caster, skill, farPos); + + var moveSpeedBonus = this.GetMoveSpeedBonus(skill); + casterCharacter.Properties.Modify(PropertyName.MSPD_BM, moveSpeedBonus); + + var dummyCharacter = this.SpawnDummyClone(casterCharacter, caster.Position); + + Send.ZC_PLAY_ANI(dummyCharacter, "F_archer_bodkinpoint_finish2", false, true); + Send.ZC_PLAY_SOUND(caster, "skl_eff_yuchae_start_2"); + Send.ZC_GROUND_EFFECT(casterCharacter, "I_only_quest_smoke013_blue_smoke", farPos, 1, 0.7f, 0, 0, 0); + Send.ZC_GROUND_EFFECT(casterCharacter, "I_only_quest_smoke013_blue_smoke", dummyCharacter.Position, 1.5f, 0.3f, 0, 0, 0); + Send.ZC_GROUND_EFFECT(casterCharacter, "I_only_quest_smoke058_blue", farPos, 3f, 0.5f, 0, 0, 0); + Send.ZC_GROUND_EFFECT(casterCharacter, "I_only_quest_smoke058_blue", dummyCharacter.Position, 3, 0.5f, 0, 0, 0); + + 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); + + Send.ZC_PLAY_ANI(dummyCharacter, "E_cleric_ProtectionOfGoddess_ground_red##1.1", true, false); + + this.SendAvailableSkills(casterCharacter, buffId, skill); + + casterCharacter.StartBuff(buffId, moveSpeedBonus, dummyCharacter.Handle, TimeSpan.FromSeconds(10), casterCharacter); + } + + /// + /// Creates a clone of the character that attacks nearby + /// entities and disappears after a while leaving an effect + /// + /// + /// + /// + /// + /// + /// + public void CreateClone(Skill skill, ICombatEntity caster, Position originPos, Position farPos, ICombatEntity target, BuffId buffId) + { + if (!caster.IsBuffActive(BuffId.OOBE_Soulmaster_Buff) || caster is not Character casterCharacter) + return; + + // This skill doesn't enter on Cooldown on the first usage. + // On the second usage it will return to the original body. + if (caster.IsBuffActive(buffId)) + { + this.ReturnToBody(caster, skill, farPos, buffId); + return; + } + + if (!caster.TrySpendSp(skill)) + { + caster.ServerMessage(Localization.Get("Not enough SP.")); + return; + } + + farPos = caster.Position.GetRelative(caster.Direction, 20); + + caster.SetAttackState(true); + skill.IncreaseOverheat(); + + this.SkillReady(caster, skill, caster.Position); + + var moveSpeedBonus = this.GetMoveSpeedBonus(skill); + + var dummyCharacter = this.SpawnDummyClone(casterCharacter, farPos); + + Send.ZC_PLAY_SOUND(caster, "skl_eff_yuchae_start_2"); + Send.ZC_GROUND_EFFECT(casterCharacter, "I_only_quest_smoke013_blue_smoke", farPos, 1, 0.7f, 0, 0, 0); + Send.ZC_GROUND_EFFECT(casterCharacter, "I_only_quest_smoke013_blue_smoke", dummyCharacter.Position, 1.5f, 0.3f, 0, 0, 0); + Send.ZC_GROUND_EFFECT(casterCharacter, "I_only_quest_smoke058_blue", farPos, 3f, 0.5f, 0, 0, 0); + Send.ZC_GROUND_EFFECT(casterCharacter, "I_only_quest_smoke058_blue", dummyCharacter.Position, 3, 0.5f, 0, 0, 0); + Send.ZC_SET_POS(casterCharacter, dummyCharacter.Handle, farPos); + Send.ZC_PLAY_ANI(dummyCharacter, "F_archer_bodkinpoint_finish2", false, true); + + Send.ZC_NORMAL.UpdateModelColor(casterCharacter, dummyCharacter.Handle, 255, 200, 100, 150, 0.01f); + Send.ZC_NORMAL.UnkDynamicCastStart(casterCharacter, dummyCharacter.Handle, SkillId.None); + + dummyCharacter.StartBuff(buffId, 0, 0, TimeSpan.FromSeconds(3), dummyCharacter); + + var aiComponent = new AiComponent(dummyCharacter, "SadhuDummy"); + aiComponent.Script.SetMaster(casterCharacter); + + dummyCharacter.Components.Add(aiComponent); + } + + /// + /// 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; + } + + /// + /// Spawns a dummy character that will looks like the original character + /// + /// + private Character SpawnDummyClone(Character casterCharacter, Position position) + { + var dummyCharacter = new DummyCharacter(); + + dummyCharacter.DbId = casterCharacter.DbId; + dummyCharacter.AccountId = casterCharacter.AccountId; + dummyCharacter.Name = casterCharacter.Name; + dummyCharacter.TeamName = casterCharacter.TeamName; + dummyCharacter.JobId = casterCharacter.JobId; + dummyCharacter.Gender = casterCharacter.Gender; + dummyCharacter.Hair = casterCharacter.Hair; + dummyCharacter.SkinColor = casterCharacter.SkinColor; + dummyCharacter.MapId = casterCharacter.MapId; + + dummyCharacter.Position = position; + dummyCharacter.Direction = casterCharacter.Direction; + + foreach (var item in casterCharacter.Inventory.GetEquip()) + { + var newItem = new Item(item.Value.Id, item.Value.Amount); + dummyCharacter.Inventory.SetEquipSilent(item.Key, newItem); + } + + foreach (var job in casterCharacter.Jobs.GetList()) + { + dummyCharacter.Jobs.AddSilent(new Job(dummyCharacter, job.Id)); + } + + foreach (var skill in casterCharacter.Skills.GetList()) + { + var newSkill = new Skill(dummyCharacter, skill.Id, skill.Level); + dummyCharacter.Skills.AddSilent(newSkill); + } + + dummyCharacter.InitProperties(); + dummyCharacter.Properties.Stamina = (int)casterCharacter.Properties.GetFloat(PropertyName.MaxSta); + dummyCharacter.UpdateStance(); + dummyCharacter.ModifyHpSafe(casterCharacter.MaxHp, out var hp, out var priority); + + dummyCharacter.Owner = casterCharacter; + + casterCharacter.Map.AddDummyCharacter(dummyCharacter); + + Send.ZC_ENTER_PC(casterCharacter.Connection, dummyCharacter); + Send.ZC_OWNER(casterCharacter, dummyCharacter, casterCharacter.Handle); + Send.ZC_UPDATED_PCAPPEARANCE(dummyCharacter); + + Send.ZC_NORMAL.HeadgearVisibilityUpdate(dummyCharacter); + + return dummyCharacter; + } + + /// + /// Sends the to the client the list of available skills to cast + /// + /// + private void SendAvailableSkills(Character casterCharacter, BuffId buffId, Skill skill) + { + var skillTreeData = ZoneServer.Instance.Data.SkillTreeDb.FindSkills(JobId.Sadhu, 45); + + foreach (var availableSkill in casterCharacter.Skills.GetList()) + { + var isSadhuSkill = false; + + for (var i = 0; i < skillTreeData.Length; i++) + { + if (skillTreeData[i].SkillId == availableSkill.Id) + { + isSadhuSkill = true; + break; + } + } + + // We are skipping all Sadhu skills besides this current own (that may be used a second time) + if (isSadhuSkill && availableSkill.Id != skill.Id) + continue; + + Send.ZC_NORMAL.EnableUseSkillWhileOutOfBody(casterCharacter, buffId, (int)availableSkill.Id); + } + } + + /// + /// Makes the character 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)); + + 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..a940c54df --- /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) + { + 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 74c857267..70621e718 100644 --- a/src/ZoneServer/World/Actors/Characters/Character.cs +++ b/src/ZoneServer/World/Actors/Characters/Character.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using Melia.Shared.Database; using Melia.Shared.L10N; using Melia.Shared.Network.Helpers; using Melia.Shared.ObjectProperties; @@ -17,7 +16,7 @@ using Yggdrasil.Logging; using Yggdrasil.Scheduling; using Yggdrasil.Util; -using Melia.Zone.Buffs; +using System.Collections.Generic; using Melia.Zone.Buffs.Handlers.Common; using Melia.Zone.World.Actors.Components; @@ -39,6 +38,15 @@ public class Character : Actor, IActor, ICombatEntity, ICommander, IPropertyObje private readonly static TimeSpan ResurrectDialogDelay = TimeSpan.FromSeconds(2); private TimeSpan _resurrectDialogTimer = ResurrectDialogDelay; + /// + /// Returns the list of sadhu buffs + /// + private static readonly List 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 + }; + /// /// Returns true if the character was just saved before a warp. /// @@ -386,6 +394,11 @@ public Localizer Localizer /// public event Action SitStatusChanged; + /// + /// Raised when the character died. + /// + public event Action Died; + /// /// Creates new character. /// @@ -602,6 +615,10 @@ 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."); + // Prevents the player to Warp while he is out of body (sadhu's skills) + if (this.IsOutOfBody()) + return; + this.Position = pos; if (this.MapId == mapId) @@ -1297,6 +1314,8 @@ public void Kill(ICombatEntity killer) Send.ZC_DEAD(this); + this.Died?.Invoke(this, killer); + _resurrectDialogTimer = ResurrectDialogDelay; } @@ -1412,5 +1431,19 @@ public void ChangeHair(int hairTypeIndex) this.Hair = hairTypeIndex; Send.ZC_UPDATED_PCAPPEARANCE(this); } + + /// + /// Return true in case of the character has used Out Of Body Skill + /// + /// + public bool IsOutOfBody() + { + 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 02fb529ce..095c39d9b 100644 --- a/src/ZoneServer/World/Actors/Characters/CharacterProperties.cs +++ b/src/ZoneServer/World/Actors/Characters/CharacterProperties.cs @@ -7,6 +7,7 @@ using Melia.Zone.World.Items; using Melia.Zone.Buffs; using Melia.Shared.Data.Database; +using Yggdrasil.Logging; namespace Melia.Zone.World.Actors.Characters { diff --git a/src/ZoneServer/World/Actors/Characters/DummyCharacter.cs b/src/ZoneServer/World/Actors/Characters/DummyCharacter.cs new file mode 100644 index 000000000..3c35a560d --- /dev/null +++ b/src/ZoneServer/World/Actors/Characters/DummyCharacter.cs @@ -0,0 +1,20 @@ +using System; + +namespace Melia.Zone.World.Actors.Characters +{ + /// + /// Represents a player 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; + } +} diff --git a/src/ZoneServer/World/Actors/CombatEntities/Components/CooldownComponent.cs b/src/ZoneServer/World/Actors/CombatEntities/Components/CooldownComponent.cs index 855e06f74..378ea7221 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 && character 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..47ea52e7d 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 Character character && character is DummyCharacter dummyCharacter) + { + this.MoveSpeedType = type; + character.Properties.Invalidate(PropertyName.MSPD); + + Send.ZC_MSPD(dummyCharacter.Owner, character); + } } /// diff --git a/src/ZoneServer/World/Maps/Map.cs b/src/ZoneServer/World/Maps/Map.cs index 362eddb94..ac9596d9a 100644 --- a/src/ZoneServer/World/Maps/Map.cs +++ b/src/ZoneServer/World/Maps/Map.cs @@ -1,14 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using Melia.Shared.Data.Database; using Melia.Shared.Network; using Melia.Shared.Game.Const; using Melia.Shared.World; using Melia.Zone.Scripting; using Melia.Zone.Scripting.AI; -using Melia.Zone.Skills.SplashAreas; using Melia.Zone.World.Actors; using Melia.Zone.World.Actors.Characters; using Melia.Zone.World.Actors.CombatEntities.Components; @@ -43,6 +41,13 @@ public class Map : IUpdateable /// private readonly Dictionary _characters = new(); + /// + /// Collection of dummy characters. + /// Key: + /// Value: + /// + private readonly Dictionary _dummies = new Dictionary(); + /// /// Collection of monsters. /// Key: @@ -196,6 +201,9 @@ private void UpdateEntities(TimeSpan elapsed) lock (_characters) _updateEntities.AddRange(_characters.Values); + lock (_dummies) + _updateEntities.AddRange(_dummies.Values); + lock (_pads) _updateEntities.AddRange(_pads.Values); @@ -270,6 +278,24 @@ public void AddCharacter(Character character) ZoneServer.Instance.UpdateServerInfo(); } + /// + /// Adds a dummy character to map. + /// + /// + public void AddDummyCharacter(Character character) + { + character.Map = this; + + lock (_dummies) + _dummies[character.Handle] = character; + + if (character is ICombatEntity) + { + lock (_combatEntities) + _combatEntities[character.Handle] = character; + } + } + /// /// Removes character from map. /// @@ -287,6 +313,24 @@ public void RemoveCharacter(Character character) ZoneServer.Instance.UpdateServerInfo(); } + /// + /// Removes a dummy character from map. + /// + /// + public void RemoveDummyCharacter(Character character) + { + lock (_dummies) + _dummies.Remove(character.Handle); + + if (character is ICombatEntity) + { + lock (_combatEntities) + _combatEntities.Remove(character.Handle); + } + + character.Map = null; + } + /// /// Returns character by handle, or null if it doesn't exist. /// @@ -300,6 +344,19 @@ public Character GetCharacter(int handle) return result; } + /// + /// Returns a dummy character by handle, or null if it doesn't exist. + /// + /// + /// + public Character GetDummyCharacter(int handle) + { + Character result; + lock (_dummies) + _dummies.TryGetValue(handle, out result); + return result; + } + /// /// Returns first character found by team name, or null if none exist. /// @@ -496,6 +553,15 @@ public List GetActorsIn(IShapeF area, Func predica } } + lock (_dummies) + { + foreach (var character in _dummies.Values) + { + if (character is TActor actor && area.IsInside(actor.Position)) + result.Add(actor); + } + } + return result; } @@ -580,6 +646,12 @@ public ICombatEntity GetCombatEntity(int handle) return entity; } + lock (_dummies) + { + if (_dummies.TryGetValue(handle, out var entity)) + return entity; + } + return null; } @@ -623,6 +695,15 @@ public bool TryGetActor(int handle, out IActor actor) } } + lock (_dummies) + { + if (_dummies.TryGetValue(handle, out var character)) + { + actor = character; + return true; + } + } + actor = null; return false; } diff --git a/system/scripts/zone/ais/sadhu_dummy.cs b/system/scripts/zone/ais/sadhu_dummy.cs new file mode 100644 index 000000000..5edf5e2aa --- /dev/null +++ b/system/scripts/zone/ais/sadhu_dummy.cs @@ -0,0 +1,193 @@ +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 = 50; + private const int MaxMasterDistance = 80; + + ICombatEntity target; + + protected override void Setup() + { + SetViewDistance(200); + + 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 && entityCharacter is DummyCharacter dummyCharacter) + { + SetFixedMoveSpeed(55); + entityCharacter.Properties.Modify(PropertyName.MSPD_BM, 55); + Send.ZC_MSPD(dummyCharacter.Owner, entityCharacter); + } + + var master = GetMaster(); + if (master != null && !InRangeOf(master, MaxMasterDistance)) + { + yield return Follow(master, 25, true, false); + yield break; + } + + yield return Wait(250, 500); + + SwitchRandom(); + + if (Case(80)) + { + yield return MoveRandom(); + } + } + + protected IEnumerable Attack() + { + // Remove the dummy character if the master is gone + if (TryGetMaster(out var master) && EntityGone(master) && this.Entity is DummyCharacter dummyCharacter) + { + Send.ZC_OWNER(dummyCharacter.Owner, dummyCharacter, 0); + Send.ZC_LEAVE(dummyCharacter); + + dummyCharacter.Map.RemoveDummyCharacter(dummyCharacter); + yield break; + } + + while (!target.IsDead) + { + if (!TryGetAutoAttackSkill(SkillId.Normal_Attack, out var skill)) + { + if (TryGetAutoAttackSkill(SkillId.Hammer_Attack, out var skillHammer)) + { + skill = skillHammer; + } else + { + yield return Wait(3000); + continue; + } + } + + while (!InRangeOf(target, 30)) + yield return MoveTo(target.Position.GetRelative(this.Entity.Position, 25), wait: false); + + yield return StopMove(); + + yield return UseAutoAttackSkill(skill, target); + yield return Wait(250, 500); + } + + yield break; + } + + protected IEnumerable StopAndIdle() + { + yield return StopMove(); + StartRoutine("Idle", Idle()); + } + + protected IEnumerable StopAndAttack() + { + ExecuteOnce(TurnTowards(target)); + + yield return StopMove(); + StartRoutine("Attack", Attack()); + } + + /// + /// Execute an auto attack towards the target + /// + 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); + } + + /// + /// Gets the Auto Attack Skill for the dummy + /// + private bool TryGetAutoAttackSkill(SkillId skillId, out Skill skill) + { + skill = null; + return this.Entity.Components.Get()?.TryGet(skillId, out skill) ?? false; + } + + private void CheckEnemies() + { + var attackableEntities = this.Entity.Map.GetAttackableEntitiesInRange(this.Entity, Entity.Position, MaxChaseDistance); + + if (attackableEntities != null && attackableEntities.Count > 0) + { + target = attackableEntities[0]; + StartRoutine("StopAndAttack", StopAndAttack()); + } + } + + 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 c4d5c57d4..a4bf2802d 100644 --- a/system/scripts/zone/core/calc_combat.cs +++ b/system/scripts/zone/core/calc_combat.cs @@ -197,6 +197,17 @@ public float SCR_CalculateDamage(ICombatEntity attacker, ICombatEntity target, S SCR_Combat_AfterCalc(attacker, target, skill, modifier, skillHitResult); + // TODO: There may be a better place to check this + // 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 != SkillAttribute.Holy || !attacker.IsBuffActive(BuffId.EliteMonsterBuff))) + return 0; + else if (skill.Data.Attribute == SkillAttribute.Holy) + return (int)(skillHitResult.Damage * 2); + } + // Let monster-specific functions override the damage calculation, // but do it after the basic calculations have been done, so we // can utilize them. For example, we can double or half damage @@ -529,6 +540,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 e6363232e..66eaeee33 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;