From 5f19cc1f7697fcd238d7dc15fca0ea510a640361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=98=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD?= Date: Sun, 18 Aug 2024 17:32:29 +0200 Subject: [PATCH] feat: add check for line of sight for range and skill attacks --- Client/Domain/AI/AI.cs | 4 +-- Client/Domain/AI/Combat/Helper.cs | 5 +-- Client/Domain/AI/Combat/TransitionBuilder.cs | 33 +++++++++++-------- .../Domain/AI/Deleveling/TransitionBuilder.cs | 18 +++++----- Client/Domain/AI/State/AttackState.cs | 2 +- Client/Domain/AI/State/MoveToTargetState.cs | 3 +- .../Domain/AI/TransitionBuilderInterface.cs | 8 ++--- .../Domain/Service/AsyncPathMoverInterface.cs | 1 + Client/Domain/Service/PathfinderInterface.cs | 1 + .../Infrastructure/Service/AsyncPathMover.cs | 1 + .../Service/L2jGeoDataPathfinder.cs | 8 +++++ 11 files changed, 50 insertions(+), 34 deletions(-) diff --git a/Client/Domain/AI/AI.cs b/Client/Domain/AI/AI.cs index 42052e9..ec64b1e 100644 --- a/Client/Domain/AI/AI.cs +++ b/Client/Domain/AI/AI.cs @@ -47,11 +47,11 @@ namespace Client.Domain.AI if (isEnabled && worldHandler.Hero != null) { states[currentState].Execute(); - foreach (var transition in locator.Get(Type).Build()) + foreach (var transition in locator.Get(Type).Build(worldHandler, config, asyncPathMover)) { if (transition.fromStates.ContainsKey(BaseState.Type.Any) && transition.toState != currentState || transition.fromStates.ContainsKey(currentState)) { - if (transition.predicate(worldHandler, config, states[currentState])) + if (transition.predicate(states[currentState])) { states[currentState].OnLeave(); currentState = transition.toState; diff --git a/Client/Domain/AI/Combat/Helper.cs b/Client/Domain/AI/Combat/Helper.cs index f3cd8fa..ae47d44 100644 --- a/Client/Domain/AI/Combat/Helper.cs +++ b/Client/Domain/AI/Combat/Helper.cs @@ -27,10 +27,7 @@ namespace Client.Domain.AI.Combat { continue; } - if (skill.IsReadyToUse && hero.VitalStats.Mp >= skill.Cost) - { - return skill; - } + return skill; } } diff --git a/Client/Domain/AI/Combat/TransitionBuilder.cs b/Client/Domain/AI/Combat/TransitionBuilder.cs index 4169da9..df83cd3 100644 --- a/Client/Domain/AI/Combat/TransitionBuilder.cs +++ b/Client/Domain/AI/Combat/TransitionBuilder.cs @@ -12,25 +12,25 @@ namespace Client.Domain.AI.Combat { public class TransitionBuilder : TransitionBuilderInterface { - public List Build() + public List Build(WorldHandler worldHandler, Config config, AsyncPathMoverInterface pathMover) { if (transitions.Count == 0) { transitions = new List() { - new(new List{BaseState.Type.Any}, BaseState.Type.Dead, (worldHandler, config, state) => { + new(new List{BaseState.Type.Any}, BaseState.Type.Dead, (state) => { if (worldHandler.Hero == null) { return false; } return worldHandler.Hero.VitalStats.IsDead; }), - new(new List{BaseState.Type.Dead}, BaseState.Type.Idle, (worldHandler, config, state) => { + new(new List{BaseState.Type.Dead}, BaseState.Type.Idle, (state) => { if (worldHandler.Hero == null) { return false; } return !worldHandler.Hero.VitalStats.IsDead; }), - new(new List{BaseState.Type.Idle, BaseState.Type.MoveToTarget, BaseState.Type.Rest, BaseState.Type.MoveToSpot}, BaseState.Type.FindTarget, (worldHandler, config, state) => { + new(new List{BaseState.Type.Idle, BaseState.Type.MoveToTarget, BaseState.Type.Rest, BaseState.Type.MoveToSpot}, BaseState.Type.FindTarget, (state) => { if (worldHandler.Hero == null) { return false; } @@ -45,13 +45,13 @@ namespace Client.Domain.AI.Combat return worldHandler.Hero.AttackerIds.Count > 0; }), - new(new List{BaseState.Type.FindTarget}, BaseState.Type.MoveToTarget, (worldHandler, config, state) => { + new(new List{BaseState.Type.FindTarget}, BaseState.Type.MoveToTarget, (state) => { if (worldHandler.Hero == null) { return false; } return worldHandler.Hero.HasValidTarget; }), - new(new List{BaseState.Type.FindTarget}, BaseState.Type.MoveToSpot, (worldHandler, config, state) => { + new(new List{BaseState.Type.FindTarget}, BaseState.Type.MoveToSpot, (state) => { if (worldHandler.Hero == null) { return false; } @@ -59,7 +59,7 @@ namespace Client.Domain.AI.Combat return Helper.GetMobsToAttackByConfig(worldHandler, config, worldHandler.Hero).Count == 0 && !Helper.IsOnSpot(worldHandler, config, worldHandler.Hero); }), - new(new List{BaseState.Type.MoveToSpot}, BaseState.Type.Idle, (worldHandler, config, state) => { + new(new List{BaseState.Type.MoveToSpot}, BaseState.Type.Idle, (state) => { if (worldHandler.Hero == null) { return false; } @@ -70,27 +70,27 @@ namespace Client.Domain.AI.Combat return Helper.IsOnSpot(worldHandler, config, worldHandler.Hero); }), - new(new List{BaseState.Type.MoveToTarget}, BaseState.Type.Idle, (worldHandler, config, state) => { + new(new List{BaseState.Type.MoveToTarget}, BaseState.Type.Idle, (state) => { if (worldHandler.Hero == null) { return false; } return !worldHandler.Hero.HasValidTarget; }), - new(new List{BaseState.Type.Idle}, BaseState.Type.Rest, (worldHandler, config, state) => { + new(new List{BaseState.Type.Idle}, BaseState.Type.Rest, (state) => { if (worldHandler.Hero == null) { return false; }; return worldHandler.Hero.AttackerIds.Count == 0 && (worldHandler.Hero.VitalStats.HpPercent < config.Combat.RestStartPercentHp || worldHandler.Hero.VitalStats.MpPercent < config.Combat.RestStartPecentMp); }), - new(new List{BaseState.Type.Rest}, BaseState.Type.Idle, (worldHandler, config, state) => { + new(new List{BaseState.Type.Rest}, BaseState.Type.Idle, (state) => { if (worldHandler.Hero == null) { return false; } return worldHandler.Hero.VitalStats.HpPercent >= config.Combat.RestEndPecentHp && worldHandler.Hero.VitalStats.MpPercent >= config.Combat.RestEndPecentMp; }), - new(new List{BaseState.Type.MoveToTarget}, BaseState.Type.Attack, (worldHandler, config, state) => { + new(new List{BaseState.Type.MoveToTarget}, BaseState.Type.Attack, (state) => { if (worldHandler.Hero == null) { return false; } @@ -99,6 +99,11 @@ namespace Client.Domain.AI.Combat return false; } + if (!pathMover.Pathfinder.HasLineOfSight(worldHandler.Hero.Transform.Position, worldHandler.Hero.Target.Transform.Position)) + { + return false; + } + if (config.Combat.SpoilIsPriority) { var spoil = worldHandler.GetSkillById(config.Combat.SpoilSkillId); if (spoil != null && !spoil.IsReadyToUse) { @@ -109,21 +114,21 @@ namespace Client.Domain.AI.Combat var distance = worldHandler.Hero.Transform.Position.HorizontalDistance(worldHandler.Hero.Target.Transform.Position); return distance < Helper.GetAttackDistanceByConfig(worldHandler, config, worldHandler.Hero, worldHandler.Hero.Target); }), - new(new List{BaseState.Type.Attack}, BaseState.Type.Pickup, (worldHandler, config, state) => { + new(new List{BaseState.Type.Attack}, BaseState.Type.Pickup, (state) => { if (worldHandler.Hero == null) { return false; } return !worldHandler.Hero.HasValidTarget; }), - new(new List{BaseState.Type.Attack}, BaseState.Type.FindTarget, (worldHandler, config, state) => { + new(new List{BaseState.Type.Attack}, BaseState.Type.FindTarget, (state) => { if (worldHandler.Hero == null) { return false; } return worldHandler.Hero.HasValidTarget && worldHandler.Hero.AttackerIds.Count > 0 && !worldHandler.Hero.AttackerIds.Contains(worldHandler.Hero.TargetId); }), - new(new List{BaseState.Type.Pickup}, BaseState.Type.Idle, (worldHandler, config, state) => { + new(new List{BaseState.Type.Pickup}, BaseState.Type.Idle, (state) => { if (worldHandler.Hero == null) { return false; } diff --git a/Client/Domain/AI/Deleveling/TransitionBuilder.cs b/Client/Domain/AI/Deleveling/TransitionBuilder.cs index 84ace4d..15ab491 100644 --- a/Client/Domain/AI/Deleveling/TransitionBuilder.cs +++ b/Client/Domain/AI/Deleveling/TransitionBuilder.cs @@ -12,37 +12,39 @@ namespace Client.Domain.AI.Deleveling { public class TransitionBuilder : TransitionBuilderInterface { - public List Build() + + // todo add MoveToDropState, SweepState + public List Build(WorldHandler worldHandler, Config config, AsyncPathMoverInterface pathMover) { if (transitions.Count == 0) { transitions = new List() { - new(new List{BaseState.Type.Any}, BaseState.Type.Dead, (worldHandler, config, state) => { + new(new List{BaseState.Type.Any}, BaseState.Type.Dead, (state) => { if (worldHandler.Hero == null) { return false; } return worldHandler.Hero.VitalStats.IsDead; }), - new(new List{BaseState.Type.Dead}, BaseState.Type.Idle, (worldHandler, config, state) => { + new(new List{BaseState.Type.Dead}, BaseState.Type.Idle, (state) => { if (worldHandler.Hero == null) { return false; } return !worldHandler.Hero.VitalStats.IsDead; }), - new(new List{BaseState.Type.FindGuard}, BaseState.Type.MoveToTarget, (worldHandler, config, state) => { + new(new List{BaseState.Type.FindGuard}, BaseState.Type.MoveToTarget, (state) => { if (worldHandler.Hero == null) { return false; } return worldHandler.Hero.Target != null; }), - new(new List{BaseState.Type.MoveToTarget}, BaseState.Type.Idle, (worldHandler, config, state) => { + new(new List{BaseState.Type.MoveToTarget}, BaseState.Type.Idle, (state) => { if (worldHandler.Hero == null) { return false; } return worldHandler.Hero.Target == null; }), - new(new List{BaseState.Type.MoveToTarget}, BaseState.Type.AttackGuard, (worldHandler, config, state) => { + new(new List{BaseState.Type.MoveToTarget}, BaseState.Type.AttackGuard, (state) => { if (worldHandler.Hero == null) { return false; } @@ -55,7 +57,7 @@ namespace Client.Domain.AI.Deleveling var expectedDistance = config.Deleveling.AttackDistance; return distance < expectedDistance; }), - new(new List{BaseState.Type.AttackGuard}, BaseState.Type.FindGuard, (worldHandler, config, state) => { + new(new List{BaseState.Type.AttackGuard}, BaseState.Type.FindGuard, (state) => { if (worldHandler.Hero == null) { return false; } @@ -68,7 +70,7 @@ namespace Client.Domain.AI.Deleveling var expectedDistance = config.Deleveling.AttackDistance; return distance >= expectedDistance; }), - new(new List{BaseState.Type.Idle}, BaseState.Type.FindGuard, (worldHandler, config, state) => { + new(new List{BaseState.Type.Idle}, BaseState.Type.FindGuard, (state) => { if (worldHandler.Hero == null) { return false; } diff --git a/Client/Domain/AI/State/AttackState.cs b/Client/Domain/AI/State/AttackState.cs index cff5363..5f6976c 100644 --- a/Client/Domain/AI/State/AttackState.cs +++ b/Client/Domain/AI/State/AttackState.cs @@ -48,7 +48,7 @@ namespace Client.Domain.AI.State } var skill = Helper.GetSkillByConfig(worldHandler, config, hero, hero.Target); - if (skill != null) + if (skill != null && skill.IsReadyToUse && hero.VitalStats.Mp >= skill.Cost) { worldHandler.RequestUseSkill(skill.Id, false, false); } diff --git a/Client/Domain/AI/State/MoveToTargetState.cs b/Client/Domain/AI/State/MoveToTargetState.cs index 8fc3528..8018092 100644 --- a/Client/Domain/AI/State/MoveToTargetState.cs +++ b/Client/Domain/AI/State/MoveToTargetState.cs @@ -25,7 +25,8 @@ namespace Client.Domain.AI.State { return; } - if (distance >= Helper.GetAttackDistanceByConfig(worldHandler, config, hero, target)) + var hasLineOfSight = asyncPathMover.Pathfinder.HasLineOfSight(hero.Transform.Position, target.Transform.Position); + if (distance >= Helper.GetAttackDistanceByConfig(worldHandler, config, hero, target) || !hasLineOfSight) { asyncPathMover.MoveAsync(target.Transform.Position); } diff --git a/Client/Domain/AI/TransitionBuilderInterface.cs b/Client/Domain/AI/TransitionBuilderInterface.cs index ce4997e..96a364e 100644 --- a/Client/Domain/AI/TransitionBuilderInterface.cs +++ b/Client/Domain/AI/TransitionBuilderInterface.cs @@ -14,16 +14,16 @@ namespace Client.Domain.AI { public readonly Dictionary fromStates; public readonly BaseState.Type toState; - public readonly Func predicate; + public readonly Func predicate; - public Transition(List fromStates, BaseState.Type toState, Func? predicate = null) + public Transition(List fromStates, BaseState.Type toState, Func? predicate = null) { this.fromStates = fromStates.ToDictionary(x => x, x => x); this.toState = toState; - this.predicate = predicate != null ? predicate : (worldHandler, config, state) => { return true; }; + this.predicate = predicate != null ? predicate : (state) => { return true; }; } } - List Build(); + List Build(WorldHandler worldHandler, Config config, AsyncPathMoverInterface pathMover); } } diff --git a/Client/Domain/Service/AsyncPathMoverInterface.cs b/Client/Domain/Service/AsyncPathMoverInterface.cs index da76903..beab996 100644 --- a/Client/Domain/Service/AsyncPathMoverInterface.cs +++ b/Client/Domain/Service/AsyncPathMoverInterface.cs @@ -11,6 +11,7 @@ namespace Client.Domain.Service { public interface AsyncPathMoverInterface { + public PathfinderInterface Pathfinder { get; } public ObservableCollection Path { get; } public Task MoveAsync(Vector3 location); public Task MoveUntilReachedAsync(Vector3 location); diff --git a/Client/Domain/Service/PathfinderInterface.cs b/Client/Domain/Service/PathfinderInterface.cs index 841e8ca..a4662b9 100644 --- a/Client/Domain/Service/PathfinderInterface.cs +++ b/Client/Domain/Service/PathfinderInterface.cs @@ -11,5 +11,6 @@ namespace Client.Domain.Service public interface PathfinderInterface { public List FindPath(Vector3 start, Vector3 end); + public bool HasLineOfSight(Vector3 start, Vector3 end); } } diff --git a/Client/Infrastructure/Service/AsyncPathMover.cs b/Client/Infrastructure/Service/AsyncPathMover.cs index 6d171ac..2df9677 100644 --- a/Client/Infrastructure/Service/AsyncPathMover.cs +++ b/Client/Infrastructure/Service/AsyncPathMover.cs @@ -27,6 +27,7 @@ namespace Client.Infrastructure.Service private readonly int nextNodeDistanceTolerance; private CancellationTokenSource? cancellationTokenSource; + public PathfinderInterface Pathfinder => pathfinder; public ObservableCollection Path { get; private set; } = new ObservableCollection(); public bool IsLocked { get; private set; } = false; diff --git a/Client/Infrastructure/Service/L2jGeoDataPathfinder.cs b/Client/Infrastructure/Service/L2jGeoDataPathfinder.cs index 2c007e3..e977f72 100644 --- a/Client/Infrastructure/Service/L2jGeoDataPathfinder.cs +++ b/Client/Infrastructure/Service/L2jGeoDataPathfinder.cs @@ -17,6 +17,9 @@ namespace Client.Infrastructure.Service [DllImport("L2JGeoDataPathFinder.dll", CallingConvention = CallingConvention.Cdecl)] private static extern uint ReleasePath(IntPtr arrayPtr); + [DllImport("L2JGeoDataPathFinder.dll", CallingConvention = CallingConvention.Cdecl)] + private static extern bool HasLineOfSight(string geoDataDirectory, float startX, float startY, float startZ, float endX, float endY, ushort maxPassableHeight); + public L2jGeoDataPathfinder(string geodataDirectory, ushort maxPassableHeight) { this.geodataDirectory = geodataDirectory; @@ -48,6 +51,11 @@ namespace Client.Infrastructure.Service return BuildPath(nodes); } + public bool HasLineOfSight(Vector3 start, Vector3 end) + { + return HasLineOfSight(GetGeodataFullpath(), start.X, start.Y, start.Z, end.X, end.Y, maxPassableHeight); + } + private List BuildPath(List nodes) { var result = new List();