feat: add pathfinding
This commit is contained in:
parent
efffe8a7cb
commit
17a4f82f24
@ -19,6 +19,9 @@ using Client.Infrastructure.Helpers;
|
||||
using Client.Domain.Events;
|
||||
using Client.Infrastructure.Events;
|
||||
using System;
|
||||
using Client.Infrastructure.Service;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Client
|
||||
{
|
||||
@ -106,6 +109,25 @@ namespace Client
|
||||
.AddSingleton<ItemHander>()
|
||||
.AddSingleton<WorldHandler>()
|
||||
|
||||
.AddSingleton(
|
||||
typeof(PathfinderInterface),
|
||||
x => new L2jGeoDataPathfinder(
|
||||
config.GetValue<string>("GeoDataDirectory") ?? "",
|
||||
config.GetValue<ushort>("MaxPassableHeight")
|
||||
)
|
||||
)
|
||||
.AddSingleton(
|
||||
typeof(AsyncPathMoverInterface),
|
||||
x => new AsyncPathMover(
|
||||
x.GetRequiredService<WorldHandler>(),
|
||||
x.GetRequiredService<PathfinderInterface>(),
|
||||
config.GetValue<int>("PathNumberOfAttempts"),
|
||||
config.GetValue<double>("NodeWaitingTime"),
|
||||
config.GetValue<int>("NodeDistanceTolerance"),
|
||||
config.GetValue<int>("NextNodeDistanceTolerance")
|
||||
)
|
||||
)
|
||||
|
||||
.AddSingleton<MainViewModel>();
|
||||
}
|
||||
}
|
||||
|
@ -220,6 +220,41 @@
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<ItemsControl ItemsSource="{Binding Path=Path}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas ClipToBounds="True" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Line
|
||||
X1="{Binding From.X}"
|
||||
Y1="{Binding From.Y}"
|
||||
X2="{Binding To.X}"
|
||||
Y2="{Binding To.Y}"
|
||||
Stroke="Red"
|
||||
StrokeDashArray="1 3"
|
||||
StrokeThickness="1"
|
||||
/>
|
||||
<Path
|
||||
Stroke="Red"
|
||||
StrokeThickness="1"
|
||||
>
|
||||
<Path.Data>
|
||||
<EllipseGeometry
|
||||
RadiusX="{Binding Radius,Mode=OneWay}"
|
||||
RadiusY="{Binding Radius,Mode=OneWay}" />
|
||||
</Path.Data>
|
||||
<Path.RenderTransform>
|
||||
<TranslateTransform X="{Binding To.X,Mode=OneWay}" Y="{Binding To.Y,Mode=OneWay}"/>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<StackPanel VerticalAlignment="Bottom" HorizontalAlignment="Right" Background="#66ffffff">
|
||||
<Grid Margin="10 5">
|
||||
<Grid.ColumnDefinitions>
|
||||
|
@ -39,12 +39,12 @@ namespace Client.Application.ViewModels
|
||||
worldHandler.RequestAttackOrFollow(Id);
|
||||
}
|
||||
|
||||
private void OnMouseRightClick(object? obj)
|
||||
private async Task OnMouseRightClick(object? obj)
|
||||
{
|
||||
worldHandler.RequestMoveToEntity(Id);
|
||||
await pathMover.MoveUntilReachedAsync(creature.Transform.Position);
|
||||
}
|
||||
|
||||
public CreatureListViewModel(WorldHandler worldHandler, CreatureInterface creature, Hero hero)
|
||||
public CreatureListViewModel(WorldHandler worldHandler, AsyncPathMoverInterface pathMover, CreatureInterface creature, Hero hero)
|
||||
{
|
||||
creature.PropertyChanged += Creature_PropertyChanged;
|
||||
creature.Transform.Position.PropertyChanged += Position_PropertyChanged;
|
||||
@ -52,11 +52,12 @@ namespace Client.Application.ViewModels
|
||||
hero.PropertyChanged += Hero_PropertyChanged;
|
||||
MouseLeftClickCommand = new RelayCommand(OnMouseLeftClick);
|
||||
MouseLeftDoubleClickCommand = new RelayCommand(OnMouseLeftDoubleClick);
|
||||
MouseRightClickCommand = new RelayCommand(OnMouseRightClick);
|
||||
MouseRightClickCommand = new RelayCommand(async (o) => await OnMouseRightClick(o));
|
||||
|
||||
this.creature = creature;
|
||||
this.hero = hero;
|
||||
this.worldHandler = worldHandler;
|
||||
this.pathMover = pathMover;
|
||||
}
|
||||
|
||||
private void HeroPosition_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
@ -94,5 +95,6 @@ namespace Client.Application.ViewModels
|
||||
private readonly CreatureInterface creature;
|
||||
private readonly Hero hero;
|
||||
private readonly WorldHandler worldHandler;
|
||||
private readonly AsyncPathMoverInterface pathMover;
|
||||
}
|
||||
}
|
||||
|
@ -77,16 +77,17 @@ namespace Client.Application.ViewModels
|
||||
worldHandler.RequestAttackOrFollow(Id);
|
||||
}
|
||||
|
||||
private void OnMouseRightClick(object? obj)
|
||||
private async Task OnMouseRightClick(object? obj)
|
||||
{
|
||||
worldHandler.RequestMoveToEntity(Id);
|
||||
await pathMover.MoveUntilReachedAsync(creature.Transform.Position);
|
||||
}
|
||||
|
||||
public CreatureMapViewModel(WorldHandler worldHandler, CreatureInterface creature, Hero hero)
|
||||
public CreatureMapViewModel(WorldHandler worldHandler, AsyncPathMoverInterface pathMover, CreatureInterface creature, Hero hero)
|
||||
{
|
||||
this.creature = creature;
|
||||
this.hero = hero;
|
||||
this.worldHandler = worldHandler;
|
||||
this.pathMover = pathMover;
|
||||
creature.PropertyChanged += Creature_PropertyChanged;
|
||||
creature.Transform.PropertyChanged += Transform_PropertyChanged;
|
||||
creature.Transform.Position.PropertyChanged += Position_PropertyChanged;
|
||||
@ -95,7 +96,7 @@ namespace Client.Application.ViewModels
|
||||
hero.PropertyChanged += Hero_PropertyChanged;
|
||||
MouseLeftClickCommand = new RelayCommand(OnMouseLeftClick);
|
||||
MouseLeftDoubleClickCommand = new RelayCommand(OnMouseLeftDoubleClick);
|
||||
MouseRightClickCommand = new RelayCommand(OnMouseRightClick);
|
||||
MouseRightClickCommand = new RelayCommand(async (o) => await OnMouseRightClick(o));
|
||||
}
|
||||
|
||||
private void VitalStats_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
@ -149,6 +150,7 @@ namespace Client.Application.ViewModels
|
||||
private readonly CreatureInterface creature;
|
||||
private readonly Hero hero;
|
||||
private readonly WorldHandler worldHandler;
|
||||
private readonly AsyncPathMoverInterface pathMover;
|
||||
private float scale = 1;
|
||||
private static readonly float MAX_RADIUS = 10;
|
||||
private static readonly float MIN_RADIUS = 2;
|
||||
|
@ -74,21 +74,22 @@ namespace Client.Application.ViewModels
|
||||
{
|
||||
worldHandler.RequestPickUp(Id);
|
||||
}
|
||||
private void OnMouseRightClick(object? obj)
|
||||
private async Task OnMouseRightClick(object? obj)
|
||||
{
|
||||
worldHandler.RequestMoveToEntity(Id);
|
||||
await pathMover.MoveUntilReachedAsync(drop.Transform.Position);
|
||||
}
|
||||
|
||||
public DropListViewModel(WorldHandler worldHandler, Drop drop, Hero hero)
|
||||
public DropListViewModel(WorldHandler worldHandler, AsyncPathMoverInterface pathMover, Drop drop, Hero hero)
|
||||
{
|
||||
this.drop = drop;
|
||||
this.hero = hero;
|
||||
this.worldHandler = worldHandler;
|
||||
this.pathMover = pathMover;
|
||||
drop.PropertyChanged += Drop_PropertyChanged;
|
||||
drop.Transform.Position.PropertyChanged += DropPosition_PropertyChanged;
|
||||
hero.Transform.Position.PropertyChanged += HeroPosition_PropertyChanged;
|
||||
MouseLeftClickCommand = new RelayCommand(OnMouseLeftClick);
|
||||
MouseRightClickCommand = new RelayCommand(OnMouseRightClick);
|
||||
MouseRightClickCommand = new RelayCommand(async (o) => await OnMouseRightClick(o));
|
||||
}
|
||||
|
||||
private void HeroPosition_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
@ -126,5 +127,6 @@ namespace Client.Application.ViewModels
|
||||
private readonly Drop drop;
|
||||
private readonly Hero hero;
|
||||
private readonly WorldHandler worldHandler;
|
||||
private readonly AsyncPathMoverInterface pathMover;
|
||||
}
|
||||
}
|
||||
|
@ -56,21 +56,22 @@ namespace Client.Application.ViewModels
|
||||
{
|
||||
worldHandler.RequestPickUp(Id);
|
||||
}
|
||||
private void OnMouseRightClick(object? obj)
|
||||
private async Task OnMouseRightClick(object? obj)
|
||||
{
|
||||
worldHandler.RequestMoveToEntity(Id);
|
||||
await pathMover.MoveUntilReachedAsync(drop.Transform.Position);
|
||||
}
|
||||
|
||||
public DropMapViewModel(WorldHandler worldHandler, Drop drop, Hero hero)
|
||||
public DropMapViewModel(WorldHandler worldHandler, AsyncPathMoverInterface pathMover, Drop drop, Hero hero)
|
||||
{
|
||||
this.drop = drop;
|
||||
this.hero = hero;
|
||||
this.worldHandler = worldHandler;
|
||||
this.pathMover = pathMover;
|
||||
drop.PropertyChanged += Creature_PropertyChanged;
|
||||
drop.Transform.Position.PropertyChanged += Position_PropertyChanged;
|
||||
hero.Transform.Position.PropertyChanged += HeroPosition_PropertyChanged;
|
||||
MouseLeftClickCommand = new RelayCommand(OnMouseLeftClick);
|
||||
MouseRightClickCommand = new RelayCommand(OnMouseRightClick);
|
||||
MouseRightClickCommand = new RelayCommand(async (o) => await OnMouseRightClick(o));
|
||||
}
|
||||
|
||||
private void HeroPosition_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
@ -94,6 +95,7 @@ namespace Client.Application.ViewModels
|
||||
private readonly Drop drop;
|
||||
private readonly Hero hero;
|
||||
private readonly WorldHandler worldHandler;
|
||||
private readonly AsyncPathMoverInterface pathMover;
|
||||
private float scale = 1;
|
||||
private static readonly float MAX_RADIUS = 8;
|
||||
private static readonly float MIN_RADIUS = 2;
|
||||
|
@ -59,7 +59,7 @@ namespace Client.Application.ViewModels
|
||||
{
|
||||
if (hero != null)
|
||||
{
|
||||
Creatures.Add(new CreatureListViewModel(worldHandler, @event.Creature, hero));
|
||||
Creatures.Add(new CreatureListViewModel(worldHandler, pathMover, @event.Creature, hero));
|
||||
AddCreature(@event.Creature);
|
||||
}
|
||||
}
|
||||
@ -74,8 +74,8 @@ namespace Client.Application.ViewModels
|
||||
{
|
||||
if (hero != null)
|
||||
{
|
||||
Drops.Add(new DropListViewModel(worldHandler, @event.Drop, hero));
|
||||
Map.Drops.Add(new DropMapViewModel(worldHandler, @event.Drop, hero));
|
||||
Drops.Add(new DropListViewModel(worldHandler, pathMover, @event.Drop, hero));
|
||||
Map.Drops.Add(new DropMapViewModel(worldHandler, pathMover, @event.Drop, hero));
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,7 +136,7 @@ namespace Client.Application.ViewModels
|
||||
{
|
||||
if (hero != null)
|
||||
{
|
||||
Map.Creatures.Add(new CreatureMapViewModel(worldHandler, creature, hero));
|
||||
Map.Creatures.Add(new CreatureMapViewModel(worldHandler, pathMover, creature, hero));
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,10 +145,11 @@ namespace Client.Application.ViewModels
|
||||
Map.Creatures.RemoveAll(x => x.Id == id);
|
||||
}
|
||||
|
||||
public MainViewModel(WorldHandler worldHandler)
|
||||
public MainViewModel(WorldHandler worldHandler, AsyncPathMoverInterface pathMover)
|
||||
{
|
||||
this.worldHandler = worldHandler;
|
||||
Map = new MapViewModel(worldHandler);
|
||||
this.pathMover = pathMover;
|
||||
Map = new MapViewModel(pathMover);
|
||||
}
|
||||
|
||||
public ObservableCollection<ChatMessageViewModel> ChatMessages { get; } = new ObservableCollection<ChatMessageViewModel>();
|
||||
@ -162,5 +163,6 @@ namespace Client.Application.ViewModels
|
||||
public MapViewModel Map { get; private set; }
|
||||
public Hero? hero;
|
||||
private readonly WorldHandler worldHandler;
|
||||
private readonly AsyncPathMoverInterface pathMover;
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,10 @@ using System.Collections.Specialized;
|
||||
using Client.Application.Commands;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Windows;
|
||||
using Client.Infrastructure.Service;
|
||||
using System.Windows.Data;
|
||||
using Client.Domain.DTO;
|
||||
using System.Windows.Documents;
|
||||
|
||||
namespace Client.Application.ViewModels
|
||||
{
|
||||
@ -138,11 +142,17 @@ namespace Client.Application.ViewModels
|
||||
drop.Scale = scale;
|
||||
drop.VieportSize = new Vector3((float)ViewportWidth, (float)ViewportHeight, 0);
|
||||
}
|
||||
|
||||
foreach (var node in Path)
|
||||
{
|
||||
node.Scale = scale;
|
||||
node.VieportSize = new Vector3((float)ViewportWidth, (float)ViewportHeight, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand MouseLeftClickCommand { get; }
|
||||
private void OnLeftMouseClick(object? obj)
|
||||
private async Task OnLeftMouseClick(object? obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
@ -159,7 +169,8 @@ namespace Client.Application.ViewModels
|
||||
(float)(mousePos.Y - ViewportHeight / 2) * scale + hero.Transform.Position.Y,
|
||||
hero.Transform.Position.Z
|
||||
);
|
||||
worldHandler.RequestMoveToLocation(location);
|
||||
|
||||
await pathMover.MoveUntilReachedAsync(location);
|
||||
}
|
||||
|
||||
public void OnMouseWheel(object sender, MouseWheelEventArgs e)
|
||||
@ -189,13 +200,58 @@ namespace Client.Application.ViewModels
|
||||
mousePosition.Y = (float)(mousePos.Y - ViewportHeight / 2) * scale + hero.Transform.Position.Y;
|
||||
}
|
||||
|
||||
public MapViewModel(WorldHandler worldHandler)
|
||||
public MapViewModel(AsyncPathMoverInterface pathMover)
|
||||
{
|
||||
Creatures.CollectionChanged += Creatures_CollectionChanged;
|
||||
Drops.CollectionChanged += Drops_CollectionChanged;
|
||||
this.worldHandler = worldHandler;
|
||||
MouseLeftClickCommand = new RelayCommand(OnLeftMouseClick);
|
||||
Path.CollectionChanged += Path_CollectionChanged;
|
||||
MouseLeftClickCommand = new RelayCommand(async (o) => await OnLeftMouseClick(o));
|
||||
mousePosition.PropertyChanged += MousePosition_PropertyChanged;
|
||||
BindingOperations.EnableCollectionSynchronization(Path, pathCollectionLock);
|
||||
this.pathMover = pathMover;
|
||||
this.pathMover.Path.CollectionChanged += PathMover_Path_CollectionChanged;
|
||||
}
|
||||
|
||||
private void PathMover_Path_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
lock(pathCollectionLock)
|
||||
{
|
||||
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null)
|
||||
{
|
||||
if (hero != null)
|
||||
{
|
||||
foreach (var item in e.NewItems)
|
||||
{
|
||||
var node = (PathSegment)item;
|
||||
Path.Add(new PathNodeViewModel(node.From, node.To, hero));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems != null)
|
||||
{
|
||||
foreach (var item in e.OldItems)
|
||||
{
|
||||
Path.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
else if (e.Action == NotifyCollectionChangedAction.Reset)
|
||||
{
|
||||
Path.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Path_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null)
|
||||
{
|
||||
foreach (var item in e.NewItems)
|
||||
{
|
||||
var node = (PathNodeViewModel)item;
|
||||
node.Scale = scale;
|
||||
node.VieportSize = new Vector3((float)ViewportWidth, (float)ViewportHeight, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void MousePosition_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
@ -232,9 +288,11 @@ namespace Client.Application.ViewModels
|
||||
public ObservableCollection<MapBlockViewModel> Blocks { get; } = new ObservableCollection<MapBlockViewModel>();
|
||||
public ObservableCollection<CreatureMapViewModel> Creatures { get; } = new ObservableCollection<CreatureMapViewModel>();
|
||||
public ObservableCollection<DropMapViewModel> Drops { get; } = new ObservableCollection<DropMapViewModel>();
|
||||
public ObservableCollection<PathNodeViewModel> Path { get; } = new ObservableCollection<PathNodeViewModel>();
|
||||
|
||||
public readonly static float MIN_SCALE = 1;
|
||||
public readonly static float MAX_SCALE = 64;
|
||||
private readonly AsyncPathMoverInterface pathMover;
|
||||
private MapImageSelector selector = new MapImageSelector();
|
||||
private Dictionary<uint, MapBlockViewModel> blocks = new Dictionary<uint, MapBlockViewModel>();
|
||||
private Hero? hero;
|
||||
@ -242,6 +300,6 @@ namespace Client.Application.ViewModels
|
||||
private double viewportWidth = 0;
|
||||
private double viewportHeight = 0;
|
||||
private Vector3 mousePosition = new Vector3(0, 0, 0);
|
||||
private readonly WorldHandler worldHandler;
|
||||
private object pathCollectionLock = new object();
|
||||
}
|
||||
}
|
||||
|
80
Client/Application/ViewModels/PathNodeViewModel.cs
Normal file
80
Client/Application/ViewModels/PathNodeViewModel.cs
Normal file
@ -0,0 +1,80 @@
|
||||
using Client.Application.Commands;
|
||||
using Client.Domain.Common;
|
||||
using Client.Domain.Entities;
|
||||
using Client.Domain.Service;
|
||||
using Client.Domain.ValueObjects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Client.Application.ViewModels
|
||||
{
|
||||
public class PathNodeViewModel : ObservableObject
|
||||
{
|
||||
public Vector3 From => new Vector3(
|
||||
(from.X - hero.Transform.Position.X) / scale + (VieportSize.X / 2),
|
||||
(from.Y - hero.Transform.Position.Y) / scale + (VieportSize.Y / 2),
|
||||
0
|
||||
);
|
||||
|
||||
public Vector3 To => new Vector3(
|
||||
(to.X - hero.Transform.Position.X) / scale + (VieportSize.X / 2),
|
||||
(to.Y - hero.Transform.Position.Y) / scale + (VieportSize.Y / 2),
|
||||
0
|
||||
);
|
||||
|
||||
public float Radius => MAX_RADIUS - (1 / MapViewModel.MIN_SCALE - 1 / scale) / (1 / MapViewModel.MIN_SCALE - 1 / MapViewModel.MAX_SCALE) * (MAX_RADIUS - MIN_RADIUS);
|
||||
|
||||
public float Scale
|
||||
{
|
||||
get => scale;
|
||||
set
|
||||
{
|
||||
if (scale != value)
|
||||
{
|
||||
scale = value;
|
||||
OnPropertyChanged("From");
|
||||
OnPropertyChanged("To");
|
||||
OnPropertyChanged("Radius");
|
||||
}
|
||||
}
|
||||
}
|
||||
public Vector3 VieportSize
|
||||
{
|
||||
get => vieportSize;
|
||||
set
|
||||
{
|
||||
if (vieportSize != value)
|
||||
{
|
||||
vieportSize = value;
|
||||
OnPropertyChanged("From");
|
||||
OnPropertyChanged("To");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PathNodeViewModel(Vector3 from, Vector3 to, Hero hero)
|
||||
{
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.hero = hero;
|
||||
hero.Transform.Position.PropertyChanged += HeroPosition_PropertyChanged;
|
||||
}
|
||||
|
||||
private void HeroPosition_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
OnPropertyChanged("From");
|
||||
OnPropertyChanged("To");
|
||||
}
|
||||
|
||||
private readonly Vector3 from;
|
||||
private readonly Vector3 to;
|
||||
private readonly Hero hero;
|
||||
private float scale = 1;
|
||||
private static readonly float MAX_RADIUS = 10;
|
||||
private static readonly float MIN_RADIUS = 2;
|
||||
private Vector3 vieportSize = new Vector3(0, 0, 0);
|
||||
}
|
||||
}
|
15
Client/Domain/DTO/PathSegment.cs
Normal file
15
Client/Domain/DTO/PathSegment.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using Client.Domain.ValueObjects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Client.Domain.DTO
|
||||
{
|
||||
public class PathSegment
|
||||
{
|
||||
public Vector3 From = new Vector3(0, 0, 0);
|
||||
public Vector3 To = new Vector3(0, 0, 0);
|
||||
}
|
||||
}
|
18
Client/Domain/Service/AsyncPathMoverInterface.cs
Normal file
18
Client/Domain/Service/AsyncPathMoverInterface.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using Client.Domain.DTO;
|
||||
using Client.Domain.ValueObjects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Client.Domain.Service
|
||||
{
|
||||
public interface AsyncPathMoverInterface
|
||||
{
|
||||
public ObservableCollection<PathSegment> Path { get; }
|
||||
public Task<bool> MoveAsync(Vector3 location);
|
||||
public Task MoveUntilReachedAsync(Vector3 location);
|
||||
}
|
||||
}
|
15
Client/Domain/Service/PathfinderInterface.cs
Normal file
15
Client/Domain/Service/PathfinderInterface.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using Client.Domain.DTO;
|
||||
using Client.Domain.ValueObjects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Client.Domain.Service
|
||||
{
|
||||
public interface PathfinderInterface
|
||||
{
|
||||
public List<PathSegment> FindPath(Vector3 start, Vector3 end);
|
||||
}
|
||||
}
|
@ -27,6 +27,8 @@ namespace Client.Domain.Service
|
||||
EventHandlerInterface<ItemCreatedEvent>,
|
||||
EventHandlerInterface<ItemDeletedEvent>
|
||||
{
|
||||
public Hero? Hero => hero;
|
||||
|
||||
public void RequestMoveToLocation(Vector3 location)
|
||||
{
|
||||
if (hero == null)
|
||||
@ -37,24 +39,6 @@ namespace Client.Domain.Service
|
||||
SendMessage(OutgoingMessageTypeEnum.Move, location);
|
||||
}
|
||||
|
||||
public void RequestMoveToEntity(uint id)
|
||||
{
|
||||
if (hero == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!creatures.ContainsKey(id) && !drops.ContainsKey(id))
|
||||
{
|
||||
Debug.WriteLine("RequestMoveToEntity: entity " + id + " not found");
|
||||
return;
|
||||
}
|
||||
|
||||
var position = creatures.ContainsKey(id) ? creatures[id].Transform.Position : drops[id].Transform.Position;
|
||||
|
||||
RequestMoveToLocation(position);
|
||||
}
|
||||
|
||||
public void RequestAcquireTarget(uint id)
|
||||
{
|
||||
if (hero == null)
|
||||
|
18
Client/Domain/ValueObjects/PathNode.cs
Normal file
18
Client/Domain/ValueObjects/PathNode.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Client.Domain.ValueObjects
|
||||
{
|
||||
public struct PathNode
|
||||
{
|
||||
public readonly uint minX;
|
||||
public readonly uint minY;
|
||||
public readonly uint maxX;
|
||||
public readonly uint maxY;
|
||||
public readonly short height;
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ namespace Client.Domain.ValueObjects
|
||||
|
||||
public float X { get => x; set { if (value != x) { x = value; OnPropertyChanged("X"); } } }
|
||||
public float Y { get => y; set { if (value != y) { y = value; OnPropertyChanged("Y"); } } }
|
||||
public float Z { get => z; set { if (value != z) { z = value; OnPropertyChanged("X"); } } }
|
||||
public float Z { get => z; set { if (value != z) { z = value; OnPropertyChanged("Z"); } } }
|
||||
|
||||
public Vector3(float x, float y, float z)
|
||||
{
|
||||
@ -29,5 +29,34 @@ namespace Client.Domain.ValueObjects
|
||||
{
|
||||
return MathF.Sqrt(HorizontalSqrDistance(other));
|
||||
}
|
||||
|
||||
public override bool Equals(object? other)
|
||||
{
|
||||
if (!(other is Vector3))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var obj = (Vector3)other;
|
||||
return MathF.Abs(x - obj.x) < float.Epsilon && MathF.Abs(y - obj.y) < float.Epsilon && MathF.Abs(z - obj.z) < float.Epsilon;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return base.GetHashCode();
|
||||
}
|
||||
|
||||
public bool ApproximatelyEquals(Vector3 other, float epsilon, bool withZ = false)
|
||||
{
|
||||
var equals = MathF.Abs(x - other.x) < epsilon && MathF.Abs(y - other.y) < epsilon;
|
||||
if (withZ)
|
||||
{
|
||||
equals = equals && MathF.Abs(z - other.z) < epsilon;
|
||||
}
|
||||
|
||||
return equals;
|
||||
}
|
||||
|
||||
public static readonly Vector3 Zero = new Vector3(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
140
Client/Infrastructure/Service/AsyncPathMover.cs
Normal file
140
Client/Infrastructure/Service/AsyncPathMover.cs
Normal file
@ -0,0 +1,140 @@
|
||||
using Client.Application.ViewModels;
|
||||
using Client.Domain.DTO;
|
||||
using Client.Domain.Entities;
|
||||
using Client.Domain.Enums;
|
||||
using Client.Domain.Service;
|
||||
using Client.Domain.ValueObjects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Client.Infrastructure.Service
|
||||
{
|
||||
public class AsyncPathMover : AsyncPathMoverInterface
|
||||
{
|
||||
private readonly WorldHandler worldHandler;
|
||||
private readonly PathfinderInterface pathfinder;
|
||||
private readonly int pathNumberOfAttempts;
|
||||
private readonly double nodeWaitingTime;
|
||||
private readonly int nodeDistanceTolerance;
|
||||
private readonly int nextNodeDistanceTolerance;
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
|
||||
public ObservableCollection<PathSegment> Path { get; private set; } = new ObservableCollection<PathSegment>();
|
||||
|
||||
public async Task MoveUntilReachedAsync(Vector3 location)
|
||||
{
|
||||
var remainingAttempts = pathNumberOfAttempts;
|
||||
while (!await MoveAsync(location) && remainingAttempts > 0)
|
||||
{
|
||||
remainingAttempts--;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> MoveAsync(Vector3 location)
|
||||
{
|
||||
if (cancellationTokenSource != null)
|
||||
{
|
||||
cancellationTokenSource.Cancel();
|
||||
cancellationTokenSource.Dispose();
|
||||
}
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
|
||||
try
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
Debug.WriteLine("Find path start");
|
||||
FindPath(location);
|
||||
Debug.WriteLine("Find path finish");
|
||||
|
||||
|
||||
foreach (var node in Path.ToList())
|
||||
{
|
||||
worldHandler.RequestMoveToLocation(node.To);
|
||||
|
||||
if (!WaitForNodeReaching(cancellationToken, node))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Path.Remove(node);
|
||||
}
|
||||
|
||||
return true;
|
||||
}, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public AsyncPathMover(WorldHandler worldHandler, PathfinderInterface pathfinder, int pathNumberOfAttempts, double nodeWaitingTime, int nodeDistanceTolerance, int nextNodeDistanceTolerance)
|
||||
{
|
||||
this.worldHandler = worldHandler;
|
||||
this.pathfinder = pathfinder;
|
||||
this.pathNumberOfAttempts = pathNumberOfAttempts;
|
||||
this.nodeWaitingTime = nodeWaitingTime;
|
||||
this.nodeDistanceTolerance = nodeDistanceTolerance;
|
||||
this.nextNodeDistanceTolerance = nextNodeDistanceTolerance;
|
||||
}
|
||||
|
||||
private void FindPath(Vector3 location)
|
||||
{
|
||||
var hero = worldHandler.Hero;
|
||||
|
||||
Path.Clear();
|
||||
if (hero == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var path = pathfinder.FindPath(hero.Transform.Position, location);
|
||||
foreach (var segment in path)
|
||||
{
|
||||
Path.Add(segment);
|
||||
}
|
||||
}
|
||||
|
||||
private bool WaitForNodeReaching(CancellationToken token, PathSegment node)
|
||||
{
|
||||
var hero = worldHandler.Hero;
|
||||
|
||||
var start = DateTime.Now;
|
||||
while (hero != null && !hero.Transform.Position.ApproximatelyEquals(node.To, nodeDistanceTolerance))
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
if (hero.Transform.Velocity.Equals(Vector3.Zero))
|
||||
{
|
||||
var elapsedSeconds = (DateTime.Now - start).TotalSeconds;
|
||||
if (hero.Transform.Position.ApproximatelyEquals(node.To, nextNodeDistanceTolerance))
|
||||
{
|
||||
break;
|
||||
}
|
||||
else if (elapsedSeconds >= nodeWaitingTime)
|
||||
{
|
||||
Path.Clear();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Task.Delay(25);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
95
Client/Infrastructure/Service/L2jGeoDataPathfinder.cs
Normal file
95
Client/Infrastructure/Service/L2jGeoDataPathfinder.cs
Normal file
@ -0,0 +1,95 @@
|
||||
using Client.Domain.DTO;
|
||||
using Client.Domain.Service;
|
||||
using Client.Domain.ValueObjects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Client.Infrastructure.Service
|
||||
{
|
||||
public class L2jGeoDataPathfinder : PathfinderInterface
|
||||
{
|
||||
[DllImport("L2JGeoDataPathFinder.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern uint FindPath(out IntPtr arrayPtr, string geoDataDirectory, float startX, float startY, float startZ, float endX, float endY, ushort maxPassableHeight);
|
||||
[DllImport("L2JGeoDataPathFinder.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern uint ReleasePath(IntPtr arrayPtr);
|
||||
|
||||
public L2jGeoDataPathfinder(string geodataDirectory, ushort maxPassableHeight)
|
||||
{
|
||||
this.geodataDirectory = geodataDirectory;
|
||||
this.maxPassableHeight = maxPassableHeight;
|
||||
}
|
||||
|
||||
public List<PathSegment> FindPath(Vector3 start, Vector3 end)
|
||||
{
|
||||
var arrayPtr = IntPtr.Zero;
|
||||
var size = FindPath(out arrayPtr, GetGeodataFullpath(), start.X, start.Y, start.Z, end.X, end.Y, maxPassableHeight);
|
||||
var originalArrayPtr = arrayPtr;
|
||||
|
||||
var nodes = new List<PathNode>();
|
||||
if (size > 0)
|
||||
{
|
||||
var entrySize = Marshal.SizeOf(typeof(PathNode));
|
||||
for (var i = 0; i < size; i++)
|
||||
{
|
||||
var node = Marshal.PtrToStructure(arrayPtr, typeof(PathNode));
|
||||
if (node != null)
|
||||
{
|
||||
nodes.Add((PathNode)node);
|
||||
}
|
||||
arrayPtr = new IntPtr(arrayPtr.ToInt32() + entrySize);
|
||||
}
|
||||
ReleasePath(originalArrayPtr);
|
||||
}
|
||||
|
||||
return BuildPath(nodes);
|
||||
}
|
||||
|
||||
private List<PathSegment> BuildPath(List<PathNode> nodes)
|
||||
{
|
||||
var result = new List<PathSegment>();
|
||||
|
||||
var points = new List<Vector3>();
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
points.Add(NodeToVector(node));
|
||||
}
|
||||
|
||||
for (var i = 0; i < points.Count - 1; i++)
|
||||
{
|
||||
var point = points[i];
|
||||
var nextPoint = points[i + 1];
|
||||
|
||||
result.Add(new PathSegment
|
||||
{
|
||||
From = point,
|
||||
To = nextPoint
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Vector3 NodeToVector(PathNode node)
|
||||
{
|
||||
var rnd = new Random();
|
||||
|
||||
return new Vector3(
|
||||
rnd.Next((int)node.minX, (int)node.maxX + 1),
|
||||
rnd.Next((int)node.minY, (int)node.maxY + 1),
|
||||
node.height
|
||||
);
|
||||
}
|
||||
|
||||
private string GetGeodataFullpath()
|
||||
{
|
||||
return System.IO.Directory.GetCurrentDirectory() + "/Assets/" + geodataDirectory + "/";
|
||||
}
|
||||
|
||||
private readonly string geodataDirectory;
|
||||
private readonly ushort maxPassableHeight;
|
||||
}
|
||||
}
|
8
Client/Properties/launchSettings.json
Normal file
8
Client/Properties/launchSettings.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Client": {
|
||||
"commandName": "Project",
|
||||
"nativeDebugging": true
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -237,9 +237,11 @@
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Domain\Entities\ChatMessage.cpp">
|
||||
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">..\..\pch.h</PrecompiledHeaderFile>
|
||||
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">..\..\pch.h</PrecompiledHeaderFile>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Domain\Helpers\HashCombiner.cpp">
|
||||
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">..\..\pch.h</PrecompiledHeaderFile>
|
||||
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">..\..\pch.h</PrecompiledHeaderFile>
|
||||
</ClCompile>
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
||||
|
Loading…
Reference in New Issue
Block a user