feat: add combat and deleveling AI

This commit is contained in:
Иванов Иван 2024-08-15 17:23:24 +02:00
parent bdd026519f
commit 2943f7a50b
79 changed files with 61368 additions and 6746 deletions

View File

@ -1,7 +1,8 @@
<Application x:Class="Client.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Client">
xmlns:local="clr-namespace:Client"
ShutdownMode="OnMainWindowClose">
<Application.Resources>
</Application.Resources>

View File

@ -22,6 +22,9 @@ using System;
using Client.Infrastructure.Service;
using System.Collections.Generic;
using System.Linq;
using Client.Domain.AI;
using Client.Infrastructure.AI.IO;
using Client.Domain.AI.IO;
namespace Client
{
@ -48,10 +51,10 @@ namespace Client
var startupForm = AppHost.Services.GetRequiredService<MainWindow>();
startupForm.Show();
var application = AppHost.Services.GetRequiredService<Bot>();
application.StartAsync();
base.OnStartup(e);
var application = AppHost.Services.GetRequiredService<Bot>();
await application.StartAsync();
}
protected override async void OnExit(ExitEventArgs e)
@ -67,6 +70,8 @@ namespace Client
.AddJsonFile("config.json", optional: false, reloadOnChange: true)
.AddJsonFile("Assets/data/experience.json", optional: false, reloadOnChange: true)
.AddJsonFile("Assets/data/npcInfo.json", optional: false, reloadOnChange: true)
.AddJsonFile("Assets/data/itemInfo.json", optional: false, reloadOnChange: true)
.AddJsonFile("Assets/data/skillInfo.json", optional: false, reloadOnChange: true)
.Build();
services
@ -90,6 +95,8 @@ namespace Client
)
.AddSingleton(typeof(ExperienceHelperInterface), typeof(ConfigurationExperienceHelper))
.AddSingleton(typeof(NpcInfoHelperInterface), typeof(ConfigurationNpcInfoHelper))
.AddSingleton(typeof(ItemInfoHelperInterface), typeof(ConfigurationItemInfoHelper))
.AddSingleton(typeof(SkillInfoHelperInterface), typeof(ConfigurationSkillInfoHelper))
.AddSingleton(typeof(EventBusInterface), typeof(InMemoryEventBus))
.AddTransient(typeof(EntityFactoryInterface<Entity>), typeof(EntityFactory<Entity>))
@ -129,7 +136,23 @@ namespace Client
)
)
.AddSingleton<MainViewModel>();
.AddSingleton<Config>()
.AddSingleton<TransitionBuilderLocator>()
.AddSingleton(
typeof(AIInterface),
x => new AI(
x.GetRequiredService<WorldHandler>(),
x.GetRequiredService<Config>(),
x.GetRequiredService<AsyncPathMoverInterface>(),
x.GetRequiredService<TransitionBuilderLocator>()
)
)
.AddTransient(typeof(ConfigSerializerInterface), typeof(JsonConfigSerializer))
.AddTransient(typeof(ConfigDeserializerInterface), typeof(JsonConfigDeserializer))
.AddSingleton<MainViewModel>()
.AddSingleton<AIConfigViewModel>();
}
}
}

View File

@ -41,6 +41,18 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Grid DataContext="{Binding CombatZone}" ClipToBounds="True">
<Path StrokeThickness="2" Fill="#1100ff00" Stroke="#3300ff00" >
<Path.Data>
<EllipseGeometry
RadiusX="{Binding Radius,Mode=OneWay}"
RadiusY="{Binding Radius,Mode=OneWay}"/>
</Path.Data>
<Path.RenderTransform>
<TranslateTransform X="{Binding Center.X,Mode=OneWay}" Y="{Binding Center.Y,Mode=OneWay}"/>
</Path.RenderTransform>
</Path>
</Grid>
<ItemsControl ItemsSource="{Binding Path=Creatures}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
@ -141,9 +153,17 @@
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Type,Mode=OneWay}" Value="NPC">
<Setter TargetName="CreatureBody" Property="Stroke" Value="DarkGreen" />
<Setter TargetName="CreatureDirection" Property="Stroke" Value="DarkGreen" />
</DataTrigger>
<DataTrigger Binding="{Binding IsHostile,Mode=OneWay}" Value="True">
<Setter TargetName="CreatureBody" Property="Stroke" Value="LimeGreen" />
<Setter TargetName="CreatureDirection" Property="Stroke" Value="LimeGreen" />
</DataTrigger>
<DataTrigger Binding="{Binding IsDead,Mode=OneWay}" Value="True">
<Setter TargetName="CreatureBody" Property="Stroke" Value="Gray" />
<Setter TargetName="CreatureDirection" Property="Stroke" Value="Gray" />
</DataTrigger>
<DataTrigger Binding="{Binding Type,Mode=OneWay}" Value="Player">
<Setter TargetName="CreatureBody" Property="Stroke" Value="Blue" />
<Setter TargetName="CreatureDirection" Property="Stroke" Value="Blue" />
@ -156,6 +176,10 @@
<Setter TargetName="CreatureBody" Property="Stroke" Value="Red" />
<Setter TargetName="CreatureDirection" Property="Stroke" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding IsSweepable,Mode=OneWay}" Value="True">
<Setter TargetName="CreatureBody" Property="Stroke" Value="Magenta" />
<Setter TargetName="CreatureDirection" Property="Stroke" Value="Magenta" />
</DataTrigger>
<DataTrigger Binding="{Binding IsTarget,Mode=OneWay}" Value="True">
<Setter TargetName="CreatureName" Property="Visibility" Value="Visible" />
</DataTrigger>

View File

@ -0,0 +1,102 @@
<UserControl x:Class="Client.Application.Components.MultipleObjectSelector"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Client.Application.Components"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
mc:Ignorable="d"
x:Name="root">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="{Binding Header,ElementName=root}" />
<StackPanel Grid.Row="1" Grid.Column="0">
<TextBox
x:Name="sourceSearch"
HorizontalAlignment="Stretch">
<i:Interaction.Triggers>
<i:EventTrigger EventName="KeyUp">
<i:InvokeCommandAction
Command="{x:Static local:MultipleObjectSelector.SearchSourceCommand}"
CommandParameter="{Binding Text,ElementName=sourceSearch}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
<ListBox
ItemsSource="{Binding Source,ElementName=root}"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Height="100"
Width="Auto"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
x:Name="source">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}">
<TextBlock.InputBindings>
<MouseBinding
MouseAction="LeftDoubleClick"
Command="{x:Static local:MultipleObjectSelector.AddItemCommand}"
CommandParameter="{Binding SelectedValue,ElementName=source}"/>
</TextBlock.InputBindings>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Grid.Row="2" Grid.Column="0">
<Button
Width="20"
Command="{x:Static local:MultipleObjectSelector.AddItemCommand}"
CommandParameter="{Binding SelectedValue,ElementName=source}"
>&#8595;</Button>
<Button
Width="20"
Command="{x:Static local:MultipleObjectSelector.RemoveItemCommand}"
CommandParameter="{Binding SelectedValue,ElementName=target}"
>&#8593;</Button>
</StackPanel>
<StackPanel Grid.Row="3" Grid.Column="0">
<TextBox
x:Name="targetSearch"
HorizontalAlignment="Stretch">
<i:Interaction.Triggers>
<i:EventTrigger EventName="KeyUp">
<i:InvokeCommandAction
Command="{x:Static local:MultipleObjectSelector.SearchTargetCommand}"
CommandParameter="{Binding Text,ElementName=targetSearch}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
<ListBox
ItemsSource="{Binding Target,ElementName=root}"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Height="100"
Width="Auto"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
x:Name="target">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}">
<TextBlock.InputBindings>
<MouseBinding
MouseAction="LeftDoubleClick"
Command="{x:Static local:MultipleObjectSelector.RemoveItemCommand}"
CommandParameter="{Binding SelectedValue,ElementName=target}"/>
</TextBlock.InputBindings>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Grid>
</UserControl>

View File

@ -0,0 +1,109 @@
using Client.Domain.Helpers;
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
namespace Client.Application.Components
{
/// <summary>
/// Interaction logic for MobSelector.xaml
/// </summary>
public partial class MultipleObjectSelector : UserControl
{
public static readonly DependencyProperty SourceProperty =
DependencyProperty.Register("Source", typeof(ICollection<ObjectInfo>), typeof(MultipleObjectSelector), new PropertyMetadata(default(ICollection<ObjectInfo>)));
public ICollection<ObjectInfo> Source
{
get { return (ICollection<ObjectInfo>)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
public static readonly DependencyProperty TargetProperty =
DependencyProperty.Register("Target", typeof(ICollection<ObjectInfo>), typeof(MultipleObjectSelector), new PropertyMetadata(default(ICollection<ObjectInfo>)));
public ICollection<ObjectInfo> Target
{
get { return (ICollection<ObjectInfo>)GetValue(TargetProperty); }
set { SetValue(TargetProperty, value); }
}
public string Header
{
get { return (string)GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
public static readonly DependencyProperty HeaderProperty =
DependencyProperty.Register("Header", typeof(string), typeof(MultipleObjectSelector), new PropertyMetadata(""));
public static RoutedUICommand AddItemCommand { get; } = new RoutedUICommand(
"Add item",
nameof(AddItemCommand),
typeof(MultipleObjectSelector)
);
public static RoutedUICommand RemoveItemCommand { get; } = new RoutedUICommand(
"Remove item",
nameof(RemoveItemCommand),
typeof(MultipleObjectSelector)
);
public static RoutedUICommand SearchSourceCommand { get; } = new RoutedUICommand(
"Search in source",
nameof(SearchSourceCommand),
typeof(MultipleObjectSelector)
);
public static RoutedUICommand SearchTargetCommand { get; } = new RoutedUICommand(
"Search in target",
nameof(SearchTargetCommand),
typeof(MultipleObjectSelector)
);
public MultipleObjectSelector()
{
InitializeComponent();
CommandBindings.Add(new CommandBinding(AddItemCommand, AddItem));
CommandBindings.Add(new CommandBinding(RemoveItemCommand, RemoveItem));
CommandBindings.Add(new CommandBinding(SearchSourceCommand, SearchSource));
CommandBindings.Add(new CommandBinding(SearchTargetCommand, SearchTarget));
}
private void AddItem(object sender, ExecutedRoutedEventArgs e)
{
var item = e.Parameter as ObjectInfo;
if (item != null)
{
Source.Remove(item);
Target.Add(item);
}
}
private void RemoveItem(object sender, ExecutedRoutedEventArgs e)
{
var item = e.Parameter as ObjectInfo;
if (item != null)
{
Target.Remove(item);
Source.Add(item);
}
}
private void SearchSource(object sender, ExecutedRoutedEventArgs e)
{
var searchPredicate = (string)e.Parameter ?? null;
if (searchPredicate != null)
{
CollectionViewSource.GetDefaultView(Source).Filter = item => (item as ObjectInfo)?.Name.Contains(searchPredicate, StringComparison.OrdinalIgnoreCase) ?? false;
}
}
private void SearchTarget(object sender, ExecutedRoutedEventArgs e)
{
var searchPredicate = (string)e.Parameter ?? null;
if (searchPredicate != null)
{
CollectionViewSource.GetDefaultView(Target).Filter = item => (item as ObjectInfo)?.Name.Contains(searchPredicate, StringComparison.OrdinalIgnoreCase) ?? false;
}
}
}
}

View File

@ -0,0 +1,30 @@
<UserControl x:Class="Client.Application.Components.ObjectSelector"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Client.Application.Components"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
mc:Ignorable="d"
x:Name="root">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Content="{Binding Header,ElementName=root}" Grid.Row="0" Grid.Column="0"/>
<ComboBox
Grid.Row="1" Grid.Column="0"
SelectedValuePath="Id"
DisplayMemberPath="Name"
ItemsSource="{Binding Source,ElementName=root}"
SelectedValue="{Binding SelectedValue,ElementName=root}"
>
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
</ComboBox>
</Grid>
</UserControl>

View File

@ -0,0 +1,55 @@
using Client.Domain.Entities;
using Client.Domain.Helpers;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Client.Application.Components
{
/// <summary>
/// Interaction logic for SearchableCombobox.xaml
/// </summary>
public partial class ObjectSelector : UserControl
{
public static readonly DependencyProperty SourceProperty =
DependencyProperty.Register("Source", typeof(ICollection<ObjectInfo>), typeof(ObjectSelector), new PropertyMetadata(default(ICollection<ObjectInfo>)));
public ICollection<ObjectInfo> Source
{
get { return (ICollection<ObjectInfo>)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
public static readonly DependencyProperty SelectedValueProperty =
DependencyProperty.Register("SelectedValue", typeof(object), typeof(ObjectSelector), new PropertyMetadata(default(object)));
public object SelectedValue
{
get { return GetValue(SelectedValueProperty); }
set { SetValue(SelectedValueProperty, value); }
}
public string Header
{
get { return (string)GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
public static readonly DependencyProperty HeaderProperty =
DependencyProperty.Register("Header", typeof(string), typeof(ObjectSelector), new PropertyMetadata(""));
public ObjectSelector()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
namespace Client.Application.Services
{
public class EqualityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length < 2)
{
return false;
}
var firstValue = values.First();
for (var i = 1; i < values.Length; i++)
{
if (!firstValue.Equals(values[i]))
{
return false;
}
}
return true;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,89 @@
using Client.Domain.AI.Combat;
using Client.Domain.Common;
using Client.Domain.Entities;
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 AICombatZoneMapViewModel : ObservableObject
{
public void MapUpdated(float scale, float viewportWidth, float viewportHeight)
{
Scale = scale;
VieportSize = new Vector3(viewportWidth, viewportHeight, 0);
}
public AICombatZoneMapViewModel(CombatZone combatZone, Hero hero)
{
this.combatZone = combatZone;
this.hero = hero;
hero.Transform.Position.PropertyChanged += HeroPosition_PropertyChanged;
combatZone.PropertyChanged += CombatZone_PropertyChanged;
combatZone.Center.PropertyChanged += CombatZoneCenter_PropertyChanged;
}
private void CombatZoneCenter_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
OnPropertyChanged("Center");
}
private void CombatZone_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "Radius")
{
OnPropertyChanged("Radius");
}
}
public Vector3 Center => new Vector3(
(combatZone.Center.X - hero.Transform.Position.X) / scale + (VieportSize.X / 2),
(combatZone.Center.Y - hero.Transform.Position.Y) / scale + (VieportSize.Y / 2),
0
);
public float Radius => combatZone.Radius / scale;
public float Scale
{
get => scale;
set
{
if (scale != value)
{
scale = value;
OnPropertyChanged("Center");
OnPropertyChanged("Radius");
}
}
}
public Vector3 VieportSize
{
get => vieportSize;
set
{
if (vieportSize != value)
{
vieportSize = value;
OnPropertyChanged("Center");
}
}
}
private void HeroPosition_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
OnPropertyChanged("Center");
}
private readonly CombatZone combatZone;
private readonly Hero hero;
private float scale = 1;
private Vector3 vieportSize = new Vector3(0, 0, 0);
}
}

View File

@ -0,0 +1,344 @@
using Client.Application.Commands;
using Client.Application.Components;
using Client.Application.Views;
using Client.Domain.AI;
using Client.Domain.AI.IO;
using Client.Domain.Common;
using Client.Domain.Entities;
using Client.Domain.Events;
using Client.Domain.Helpers;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
namespace Client.Application.ViewModels
{
public class AIConfigViewModel :
ObservableObject,
EventHandlerInterface<HeroCreatedEvent>,
EventHandlerInterface<HeroDeletedEvent>
{
public void Handle(HeroCreatedEvent @event)
{
hero = @event.Hero;
}
public void Handle(HeroDeletedEvent @event)
{
hero = null;
}
public class SkillCondition : ObservableObject
{
private uint id;
private byte maxTargetPercentHp = 100;
private byte minPlayerPercentMp = 0;
private byte maxPlayerPercentHp = 100;
public uint Id { get => id; set { if (value != id) { id = value; OnPropertyChanged(); } } }
public byte MaxTargetPercentHp { get => maxTargetPercentHp; set { if (value != maxTargetPercentHp) { maxTargetPercentHp = value; OnPropertyChanged(); } } }
public byte MinPlayerPercentMp { get => minPlayerPercentMp; set { if (value != minPlayerPercentMp) { minPlayerPercentMp = value; OnPropertyChanged(); } } }
public byte MaxPlayerPercentHp { get => maxPlayerPercentHp; set { if (value != maxPlayerPercentHp) { maxPlayerPercentHp = value; OnPropertyChanged(); } } }
}
public class CombatZone : ObservableObject
{
private float x = 0;
private float y = 0;
private float radius = 0;
public float X { get => x; set { if (value != x) { x = value; OnPropertyChanged(); } } }
public float Y { get => y; set { if (value != y) { y = value; OnPropertyChanged(); } } }
public float Radius { get => radius; set { if (value != radius) { radius = value; OnPropertyChanged(); } } }
}
public AIConfigViewModel(NpcInfoHelperInterface npcInfoHelper, ItemInfoHelperInterface itemInfoHelper, SkillInfoHelperInterface skillInfoHelper, Config config, ConfigSerializerInterface configSerializer, ConfigDeserializerInterface configDeserializer)
{
this.npcInfoHelper = npcInfoHelper;
this.itemInfoHelper = itemInfoHelper;
this.skillInfoHelper = skillInfoHelper;
this.config = config;
this.configSerializer = configSerializer;
this.configDeserializer = configDeserializer;
SaveDialogCommand = new RelayCommand(OnSaveDialog);
OpenDialogCommand = new RelayCommand(OnOpenDialog);
SaveCommand = new RelayCommand(OnSave);
ResetCommand = new RelayCommand(OnReset);
GetHeroPosition = new RelayCommand(OnGetHeroPosition);
Skills = new ObservableCollection<ObjectInfo>(skillInfoHelper.GetAllSkills().Select(x => x.Value).Where(x => x.IsActive).ToList());
}
public ICommand SaveDialogCommand { get; }
public ICommand OpenDialogCommand { get; }
public ICommand SaveCommand { get; }
public ICommand ResetCommand { get; }
public ICommand GetHeroPosition { get; }
public Action? Close { get; set; }
public Action<string>? OpenSaveDialog { get; set; }
public Func<string?>? OpenOpenDialog { get; set; }
public ObservableCollection<ObjectInfo> Skills { get; set; }
public uint MobsMaxDeltaZ { get => mobsMaxDeltaZ; set { if (value != mobsMaxDeltaZ) { mobsMaxDeltaZ = value; OnPropertyChanged(); } } }
public ObservableCollection<ObjectInfo> ExcludedMobs { get; set; } = new ObservableCollection<ObjectInfo>();
public ObservableCollection<ObjectInfo> SelectedExcludedMobs { get; set; } = new ObservableCollection<ObjectInfo>();
public ObservableCollection<ObjectInfo> IncludedMobs { get; set; } = new ObservableCollection<ObjectInfo>();
public ObservableCollection<ObjectInfo> SelectedIncludedMobs { get; set; } = new ObservableCollection<ObjectInfo>();
public byte? MobLevelLowerLimit { get => mobLevelLowerLimit; set { if (value != mobLevelLowerLimit) { mobLevelLowerLimit = value; OnPropertyChanged(); } } }
public byte? MobLevelUpperLimit { get => mobLevelUpperLimit; set { if (value != mobLevelUpperLimit) { mobLevelUpperLimit = value; OnPropertyChanged(); } } }
public ObservableCollection<ObjectInfo> ExcludedSpoilMobs { get; set; } = new ObservableCollection<ObjectInfo>();
public ObservableCollection<ObjectInfo> SelectedExcludedSpoilMobs { get; set; } = new ObservableCollection<ObjectInfo>();
public ObservableCollection<ObjectInfo> IncludedSpoilMobs { get; set; } = new ObservableCollection<ObjectInfo>();
public ObservableCollection<ObjectInfo> SelectedIncludedSpoilMobs { get; set; } = new ObservableCollection<ObjectInfo>();
public bool SpoilIfPossible { get => spoilIfPossible; set { if (value != spoilIfPossible) { spoilIfPossible = value; OnPropertyChanged(); } } }
public bool SpoilIsPriority { get => spoilIsPriority; set { if (value != spoilIsPriority) { spoilIsPriority = value; OnPropertyChanged(); } } }
public uint SpoilSkillId { get => spoilSkillId; set { if (value != spoilSkillId) { spoilSkillId = value; OnPropertyChanged(); } } }
public uint SweeperSkillId { get => sweeperSkillId; set { if (value != sweeperSkillId) { sweeperSkillId = value; OnPropertyChanged(); } } }
public byte SweepAttemptsCount { get => sweepAttemptsCount; set { if (value != sweepAttemptsCount) { sweepAttemptsCount = value; OnPropertyChanged(); } } }
public ObservableCollection<ObjectInfo> ExcludedItems { get; set; } = new ObservableCollection<ObjectInfo>();
public ObservableCollection<ObjectInfo> SelectedExcludedItems { get; set; } = new ObservableCollection<ObjectInfo>();
public ObservableCollection<ObjectInfo> IncludedItems { get; set; } = new ObservableCollection<ObjectInfo>();
public ObservableCollection<ObjectInfo> SelectedIncludedItems { get; set; } = new ObservableCollection<ObjectInfo>();
public bool PickupIfPossible { get => pickupIfPossible; set { if (value != pickupIfPossible) { pickupIfPossible = value; OnPropertyChanged(); } } }
public uint PickupMaxDeltaZ { get => pickupMaxDeltaZ; set { if (value != pickupMaxDeltaZ) { pickupMaxDeltaZ = value; OnPropertyChanged(); } } }
public byte PickupAttemptsCount { get => pickupAttemptsCount; set { if (value != pickupAttemptsCount) { pickupAttemptsCount = value; OnPropertyChanged(); } } }
public byte RestStartPercentHp { get => restStartPercentHp; set { if (value != restStartPercentHp) { restStartPercentHp = value; OnPropertyChanged(); } } }
public byte RestEndPercentHp { get => restEndPercentHp; set { if (value != restEndPercentHp) { restEndPercentHp = value; OnPropertyChanged(); } } }
public byte RestStartPercentMp { get => restStartPercentMp; set { if (value != restStartPercentMp) { restStartPercentMp = value; OnPropertyChanged(); } } }
public byte RestEndPercentMp { get => restEndPercentMp; set { if (value != restEndPercentMp) { restEndPercentMp = value; OnPropertyChanged(); } } }
public ObservableCollection<SkillCondition> CombatSkills { get; set; } = new ObservableCollection<SkillCondition>();
public bool AutoUseShots { get => autoUseShots; set { if (value != autoUseShots) { autoUseShots = value; OnPropertyChanged(); } } }
public uint AttackDistanceMili { get => attackDistanceMili; set { if (value != attackDistanceMili) { attackDistanceMili = value; OnPropertyChanged(); } } }
public uint AttackDistanceBow { get => attackDistanceBow; set { if (value != attackDistanceBow) { attackDistanceBow = value; OnPropertyChanged(); } } }
public bool UseOnlySkills { get => useOnlySkills; set { if (value != useOnlySkills) { useOnlySkills = value; OnPropertyChanged(); } } }
public CombatZone Zone { get => combatZone; set { if (value != combatZone) { combatZone = value; OnPropertyChanged(); } } }
public byte DelevelingTargetLevel { get => delevelingTargetLevel; set { if (value != delevelingTargetLevel) { delevelingTargetLevel = value; OnPropertyChanged(); } } }
public uint DelevelingAttackDistance { get => delevelingAttackDistance; set { if (value != delevelingAttackDistance) { delevelingAttackDistance = value; OnPropertyChanged(); } } }
public uint DelevelingSkillId { get => delevelingSkillId; set { if (value != delevelingSkillId) { delevelingSkillId = value; OnPropertyChanged(); } } }
public void LoadConfig()
{
LoadConfigFrom(config);
}
private void LoadConfigFrom(Config config)
{
LoadCollections(config);
MobsMaxDeltaZ = config.Combat.MobsMaxDeltaZ;
MobLevelLowerLimit = config.Combat.MobLevelLowerLimit;
MobLevelUpperLimit = config.Combat.MobLevelUpperLimit;
SpoilIfPossible = config.Combat.SpoilIfPossible;
SpoilIsPriority = config.Combat.SpoilIsPriority;
SpoilSkillId = config.Combat.SpoilSkillId;
SweeperSkillId = config.Combat.SweeperSkillId;
SweepAttemptsCount = config.Combat.SweepAttemptsCount;
PickupIfPossible = config.Combat.PickupIfPossible;
PickupMaxDeltaZ = config.Combat.PickupMaxDeltaZ;
PickupAttemptsCount = config.Combat.PickupAttemptsCount;
RestStartPercentHp = config.Combat.RestStartPercentHp;
RestEndPercentHp = config.Combat.RestEndPecentHp;
RestStartPercentMp = config.Combat.RestStartPecentMp;
RestEndPercentMp = config.Combat.RestEndPecentMp;
AutoUseShots = config.Combat.AutoUseShots;
AttackDistanceMili = config.Combat.AttackDistanceMili;
AttackDistanceBow = config.Combat.AttackDistanceBow;
UseOnlySkills = config.Combat.UseOnlySkills;
Zone.X = config.Combat.Zone.Center.X;
Zone.Y = config.Combat.Zone.Center.Y;
Zone.Radius = config.Combat.Zone.Radius;
DelevelingTargetLevel = config.Deleveling.TargetLevel;
DelevelingAttackDistance = config.Deleveling.AttackDistance;
DelevelingSkillId = config.Deleveling.SkillId;
}
private void SaveConfig()
{
config.Combat.MobsMaxDeltaZ = MobsMaxDeltaZ;
config.Combat.MobLevelLowerLimit = MobLevelLowerLimit;
config.Combat.MobLevelUpperLimit = MobLevelUpperLimit;
config.Combat.SpoilIfPossible = SpoilIfPossible;
config.Combat.SpoilIsPriority = SpoilIsPriority;
config.Combat.SpoilSkillId = SpoilSkillId;
config.Combat.SweeperSkillId = SweeperSkillId;
config.Combat.SweepAttemptsCount = SweepAttemptsCount;
config.Combat.PickupIfPossible = PickupIfPossible;
config.Combat.PickupMaxDeltaZ = PickupMaxDeltaZ;
config.Combat.PickupAttemptsCount = PickupAttemptsCount;
config.Combat.RestStartPercentHp = RestStartPercentHp;
config.Combat.RestEndPecentHp = RestEndPercentHp;
config.Combat.RestStartPecentMp = RestStartPercentMp;
config.Combat.RestEndPecentMp = RestEndPercentMp;
config.Combat.AutoUseShots = AutoUseShots;
config.Combat.AttackDistanceMili = AttackDistanceMili;
config.Combat.AttackDistanceBow = AttackDistanceBow;
config.Combat.UseOnlySkills = UseOnlySkills;
config.Combat.Zone.Center.X = Zone.X;
config.Combat.Zone.Center.Y = Zone.Y;
config.Combat.Zone.Center.Z = 0;
config.Combat.Zone.Radius = Zone.Radius;
config.Deleveling.TargetLevel = DelevelingTargetLevel;
config.Deleveling.AttackDistance = DelevelingAttackDistance;
config.Deleveling.SkillId = DelevelingSkillId;
SaveCollections();
}
private void LoadCollections(Config config)
{
ExcludedMobs.Clear();
npcInfoHelper.GetAllNpc().ForEach(n => ExcludedMobs.Add(n));
SelectedExcludedMobs.Clear();
IncludedMobs.Clear();
npcInfoHelper.GetAllNpc().ForEach(n => IncludedMobs.Add(n));
SelectedIncludedMobs.Clear();
ExcludedSpoilMobs.Clear();
npcInfoHelper.GetAllNpc().ForEach(n => ExcludedSpoilMobs.Add(n));
SelectedExcludedSpoilMobs.Clear();
IncludedSpoilMobs.Clear();
npcInfoHelper.GetAllNpc().ForEach(n => IncludedSpoilMobs.Add(n));
SelectedIncludedSpoilMobs.Clear();
ExcludedItems.Clear();
itemInfoHelper.GetAllItems().ForEach(n => ExcludedItems.Add(n));
SelectedExcludedItems.Clear();
IncludedItems.Clear();
itemInfoHelper.GetAllItems().ForEach(n => IncludedItems.Add(n));
SelectedIncludedItems.Clear();
var loadCollection = (ObservableCollection<ObjectInfo> items, ObservableCollection<ObjectInfo> selectedItems, Dictionary<uint, bool> configItems) =>
{
for (var i = items.Count - 1; i >= 0; i--)
{
var mob = items[i];
if (configItems.ContainsKey(mob.Id))
{
items.Remove(mob);
selectedItems.Add(mob);
}
}
};
loadCollection(ExcludedMobs, SelectedExcludedMobs, config.Combat.ExcludedMobIds);
loadCollection(IncludedMobs, SelectedIncludedMobs, config.Combat.IncludedMobIds);
loadCollection(ExcludedSpoilMobs, SelectedExcludedSpoilMobs, config.Combat.ExcludedSpoilMobIds);
loadCollection(IncludedSpoilMobs, SelectedIncludedSpoilMobs, config.Combat.IncludedSpoilMobIds);
loadCollection(ExcludedItems, SelectedExcludedItems, config.Combat.ExcludedItemIdsToPickup);
loadCollection(IncludedItems, SelectedIncludedItems, config.Combat.IncludedItemIdsToPickup);
CombatSkills.RemoveAll();
config.Combat.SkillConditions.ForEach(x =>
{
CombatSkills.Add(new SkillCondition()
{
Id = x.Id,
MaxTargetPercentHp = x.MaxTargetPercentHp,
MinPlayerPercentMp = x.MinPlayerPercentMp,
MaxPlayerPercentHp = x.MaxPlayerPercentHp
});
});
}
private void SaveCollections()
{
config.Combat.ExcludedMobIds = SelectedExcludedMobs.ToDictionary(x => x.Id, x => true);
config.Combat.IncludedMobIds = SelectedIncludedMobs.ToDictionary(x => x.Id, x => true);
config.Combat.ExcludedSpoilMobIds = SelectedExcludedSpoilMobs.ToDictionary(x => x.Id, x => true);
config.Combat.IncludedSpoilMobIds = SelectedIncludedSpoilMobs.ToDictionary(x => x.Id, x => true);
config.Combat.ExcludedItemIdsToPickup = SelectedExcludedItems.ToDictionary(x => x.Id, x => true);
config.Combat.IncludedItemIdsToPickup = SelectedIncludedItems.ToDictionary(x => x.Id, x => true);
config.Combat.SkillConditions = CombatSkills.Select(x => new Config.SkillCondition()
{
Id = x.Id,
MaxTargetPercentHp = x.MaxTargetPercentHp,
MinPlayerPercentMp = x.MinPlayerPercentMp,
MaxPlayerPercentHp = x.MaxPlayerPercentHp
}).ToList();
}
private void OnSaveDialog(object? sender)
{
SaveConfig();
if (OpenSaveDialog != null)
{
OpenSaveDialog(configSerializer.Serialize(config));
}
if (Close != null)
{
Close();
}
}
private void OnOpenDialog(object? sender)
{
if (OpenOpenDialog != null)
{
var data = OpenOpenDialog();
if (data != null)
{
var config = configDeserializer.Deserialize(data);
if (config != null)
{
LoadConfigFrom(config);
}
}
}
}
private void OnSave(object? sender)
{
SaveConfig();
if (Close != null)
{
Close();
}
}
private void OnReset(object? sender)
{
LoadConfigFrom(config);
}
private void OnGetHeroPosition(object? sender)
{
if (hero != null)
{
Zone.X = hero.Transform.Position.X;
Zone.Y = hero.Transform.Position.Y;
}
}
private readonly NpcInfoHelperInterface npcInfoHelper;
private readonly ItemInfoHelperInterface itemInfoHelper;
private readonly SkillInfoHelperInterface skillInfoHelper;
private readonly Config config;
private readonly ConfigSerializerInterface configSerializer;
private readonly ConfigDeserializerInterface configDeserializer;
private uint mobsMaxDeltaZ = 0;
private byte? mobLevelLowerLimit = null;
private byte? mobLevelUpperLimit = null;
private bool spoilIfPossible = false;
private bool spoilIsPriority = false;
private uint spoilSkillId = 0;
private uint sweeperSkillId = 0;
private byte sweepAttemptsCount = 0;
private bool pickupIfPossible = false;
private uint pickupMaxDeltaZ = 0;
private byte pickupAttemptsCount = 0;
private byte restStartPercentHp = 0;
private byte restEndPercentHp = 0;
private byte restStartPercentMp = 0;
private byte restEndPercentMp = 0;
private bool autoUseShots = false;
private uint attackDistanceMili = 0;
private uint attackDistanceBow = 0;
private bool useOnlySkills = false;
private CombatZone combatZone = new CombatZone();
private Hero? hero;
private byte delevelingTargetLevel = 0;
private uint delevelingAttackDistance = 0;
private uint delevelingSkillId = 0;
}
}

View File

@ -17,6 +17,12 @@ namespace Client.Application.ViewModels
{
public class CreatureMapViewModel : ObservableObject
{
public void MapUpdated(float scale, float viewportWidth, float viewportHeight)
{
Scale = scale;
VieportSize = new Vector3(viewportWidth, viewportHeight, 0);
}
public uint Id => creature.Id;
public string Name => creature.Name;
public Vector3 Position => new Vector3(
@ -64,6 +70,9 @@ namespace Client.Application.ViewModels
public bool IsAggressive => creature.AggroRadius > 0 && !creature.VitalStats.IsDead && creature.IsHostile;
public float AggroRadius => creature.AggroRadius / scale;
public bool IsAttacker => hero.AttackerIds.Contains(creature.Id);
public bool IsDead => creature.VitalStats.IsDead;
public bool IsHostile => creature.IsHostile;
public bool IsSweepable => creature is NPC && ((NPC) creature).SpoilState == SpoilStateEnum.Sweepable;
public ICommand MouseLeftClickCommand { get; }
public ICommand MouseLeftDoubleClickCommand { get; }
@ -109,6 +118,7 @@ namespace Client.Application.ViewModels
if (e.PropertyName == "IsDead")
{
OnPropertyChanged("IsAggressive");
OnPropertyChanged("IsDead");
}
}
@ -150,6 +160,10 @@ namespace Client.Application.ViewModels
{
OnPropertyChanged("Name");
}
if (e.PropertyName == "SpoilState")
{
OnPropertyChanged("IsSweepable");
}
}
private readonly CreatureInterface creature;

View File

@ -16,6 +16,12 @@ namespace Client.Application.ViewModels
{
public class DropMapViewModel : ObservableObject
{
public void MapUpdated(float scale, float viewportWidth, float viewportHeight)
{
Scale = scale;
VieportSize = new Vector3(viewportWidth, viewportHeight, 0);
}
public uint Id => drop.Id;
public string Name => drop.Name + " (" + drop.Amount + ")";
public Vector3 Position => new Vector3(

View File

@ -1,4 +1,6 @@
using Client.Application.Components;
using Client.Application.Commands;
using Client.Application.Components;
using Client.Domain.AI;
using Client.Domain.Common;
using Client.Domain.Entities;
using Client.Domain.Events;
@ -14,6 +16,7 @@ using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace Client.Application.ViewModels
@ -32,11 +35,13 @@ namespace Client.Application.ViewModels
EventHandlerInterface<ItemCreatedEvent>,
EventHandlerInterface<ItemDeletedEvent>
{
public void Handle(HeroCreatedEvent @event)
{
Hero = new HeroSummaryInfoViewModel(@event.Hero);
hero = @event.Hero;
Map.Hero = hero;
Map.CombatZone = new AICombatZoneMapViewModel(aiConfig.Combat.Zone, hero);
AddCreature(hero);
OnPropertyChanged("Hero");
OnPropertyChanged("Map");
@ -51,6 +56,7 @@ namespace Client.Application.ViewModels
Hero = null;
hero = null;
Map.Hero = null;
Map.CombatZone = null;
OnPropertyChanged("Hero");
OnPropertyChanged("Map");
}
@ -115,13 +121,13 @@ namespace Client.Application.ViewModels
{
if (hero != null)
{
if (!@event.Item.IsQuest)
if (@event.Item is EtcItem && ((EtcItem) @event.Item).IsQuest)
{
Items.Add(new ItemListViewModel(worldHandler, @event.Item));
QuestItems.Add(new ItemListViewModel(worldHandler, @event.Item));
}
else
{
QuestItems.Add(new ItemListViewModel(worldHandler, @event.Item));
Items.Add(new ItemListViewModel(worldHandler, @event.Item));
}
}
}
@ -132,6 +138,14 @@ namespace Client.Application.ViewModels
QuestItems.RemoveAll(x => x.Id == @event.Id);
}
public Dictionary<TypeEnum, string> AITypes
{
get
{
return Enum.GetValues(typeof(TypeEnum)).Cast<TypeEnum>().ToDictionary(x => x, x => x.ToString());
}
}
private void AddCreature(CreatureInterface creature)
{
if (hero != null)
@ -145,13 +159,36 @@ namespace Client.Application.ViewModels
Map.Creatures.RemoveAll(x => x.Id == id);
}
public MainViewModel(WorldHandler worldHandler, AsyncPathMoverInterface pathMover)
private void OnToggleAI(object? sender)
{
ai.Toggle();
OnPropertyChanged("AIStatus");
}
private void OnChangeAIType(object? param)
{
if (!(param is TypeEnum))
{
return;
}
ai.Type = (TypeEnum) param;
OnPropertyChanged("AIType");
}
public MainViewModel(WorldHandler worldHandler, AsyncPathMoverInterface pathMover, AIInterface ai, Config aiConfig)
{
this.worldHandler = worldHandler;
this.pathMover = pathMover;
this.ai = ai;
this.aiConfig = aiConfig;
Map = new MapViewModel(pathMover);
ToggleAICommand = new RelayCommand(OnToggleAI);
ChangeAITypeCommand = new RelayCommand(OnChangeAIType);
}
public ICommand ToggleAICommand { get; set; }
public ICommand ChangeAITypeCommand { get; set; }
public ObservableCollection<ChatMessageViewModel> ChatMessages { get; } = new ObservableCollection<ChatMessageViewModel>();
public ObservableCollection<CreatureListViewModel> Creatures { get; } = new ObservableCollection<CreatureListViewModel>();
public ObservableCollection<DropListViewModel> Drops { get; } = new ObservableCollection<DropListViewModel>();
@ -161,8 +198,13 @@ namespace Client.Application.ViewModels
public ObservableCollection<ItemListViewModel> QuestItems { get; } = new ObservableCollection<ItemListViewModel>();
public HeroSummaryInfoViewModel? Hero { get; private set; }
public MapViewModel Map { get; private set; }
public bool AIStatus => ai.IsEnabled;
public TypeEnum AIType => ai.Type;
public Hero? hero;
private readonly WorldHandler worldHandler;
private readonly AsyncPathMoverInterface pathMover;
private readonly AIInterface ai;
private readonly Config aiConfig;
}
}

View File

@ -1,28 +1,25 @@
using Client.Domain.Common;
using Client.Application.Commands;
using Client.Domain.Common;
using Client.Domain.DTO;
using Client.Domain.Entities;
using Client.Domain.Service;
using Client.Domain.ValueObjects;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows.Media.Animation;
using System.Collections.Specialized;
using Client.Application.Commands;
using System.Reflection.Metadata;
using System.Threading.Tasks;
using System.Windows;
using Client.Infrastructure.Service;
using System.Windows.Data;
using Client.Domain.DTO;
using System.Windows.Documents;
using System.Windows.Input;
namespace Client.Application.ViewModels
{
public class MapViewModel : ObservableObject
{
public delegate void MapUpdatedEventHandler(float scale, float viewportWidth, float viewportHeight);
private event MapUpdatedEventHandler? MapUpdated;
public Hero? Hero
{
get => hero;
@ -131,22 +128,10 @@ namespace Client.Application.ViewModels
this.blocks[block.Id].Visible = true;
}
foreach (var creature in Creatures)
{
creature.Scale = scale;
creature.VieportSize = new Vector3((float)ViewportWidth, (float)ViewportHeight, 0);
}
foreach (var drop in Drops)
{
drop.Scale = scale;
drop.VieportSize = new Vector3((float)ViewportWidth, (float)ViewportHeight, 0);
}
foreach (var node in Path)
if (MapUpdated != null)
{
node.Scale = scale;
node.VieportSize = new Vector3((float)ViewportWidth, (float)ViewportHeight, 0);
MapUpdated(scale, (float)ViewportWidth, (float)ViewportHeight);
}
}
}
@ -236,7 +221,7 @@ namespace Client.Application.ViewModels
}
else if (e.Action == NotifyCollectionChangedAction.Reset)
{
Path.Clear();
Path.RemoveAll();
}
}
}
@ -248,8 +233,16 @@ namespace Client.Application.ViewModels
foreach (var item in e.NewItems)
{
var node = (PathNodeViewModel)item;
node.Scale = scale;
node.VieportSize = new Vector3((float)ViewportWidth, (float)ViewportHeight, 0);
MapUpdated += node.MapUpdated;
node.MapUpdated(scale, (float)ViewportWidth, (float)ViewportHeight);
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems != null)
{
foreach (var item in e.OldItems)
{
var node = (PathNodeViewModel)item;
MapUpdated -= node.MapUpdated;
}
}
}
@ -265,9 +258,17 @@ namespace Client.Application.ViewModels
{
foreach (var item in e.NewItems)
{
var creature = (DropMapViewModel)item;
creature.Scale = scale;
creature.VieportSize = new Vector3((float)ViewportWidth, (float)ViewportHeight, 0);
var drop = (DropMapViewModel)item;
MapUpdated += drop.MapUpdated;
drop.MapUpdated(scale, (float)ViewportWidth, (float)ViewportHeight);
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems != null)
{
foreach (var item in e.OldItems)
{
var drop = (DropMapViewModel)item;
MapUpdated -= drop.MapUpdated;
}
}
}
@ -279,8 +280,16 @@ namespace Client.Application.ViewModels
foreach (var item in e.NewItems)
{
var creature = (CreatureMapViewModel)item;
creature.Scale = scale;
creature.VieportSize = new Vector3((float)ViewportWidth, (float)ViewportHeight, 0);
MapUpdated += creature.MapUpdated;
creature.MapUpdated(scale, (float)ViewportWidth, (float)ViewportHeight);
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems != null)
{
foreach (var item in e.OldItems)
{
var creature = (CreatureMapViewModel)item;
MapUpdated -= creature.MapUpdated;
}
}
}
@ -289,6 +298,31 @@ namespace Client.Application.ViewModels
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 AICombatZoneMapViewModel? CombatZone {
get
{
return combatZone;
}
set
{
if (value != combatZone)
{
if (value == null)
{
if (combatZone != null)
{
MapUpdated -= combatZone.MapUpdated;
}
}
else
{
MapUpdated += value.MapUpdated;
}
combatZone = value;
OnPropertyChanged();
}
}
}
public readonly static float MIN_SCALE = 1;
public readonly static float MAX_SCALE = 128;
@ -301,5 +335,6 @@ namespace Client.Application.ViewModels
private double viewportHeight = 0;
private Vector3 mousePosition = new Vector3(0, 0, 0);
private object pathCollectionLock = new object();
private AICombatZoneMapViewModel? combatZone = null;
}
}

View File

@ -13,6 +13,12 @@ namespace Client.Application.ViewModels
{
public class PathNodeViewModel : ObservableObject
{
public void MapUpdated(float scale, float viewportWidth, float viewportHeight)
{
Scale = scale;
VieportSize = new Vector3(viewportWidth, viewportHeight, 0);
}
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),

View File

@ -0,0 +1,373 @@
<Window x:Class="Client.Application.Views.AIConfig"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Client.Application.Views"
xmlns:components="clr-namespace:Client.Application.Components"
mc:Ignorable="d"
IsVisibleChanged="Window_IsVisibleChanged"
Closing="Window_Closing"
SizeToContent="Height"
ResizeMode="NoResize"
Title="Combat AI Config" Height="600" Width="800"
x:Name="root">
<Grid Margin="0,0,0,10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Menu>
<MenuItem Header="File">
<MenuItem Header="Open" Command="{Binding OpenDialogCommand}"/>
<MenuItem Header="Save" Command="{Binding SaveDialogCommand}"/>
</MenuItem>
</Menu>
<TabControl Grid.Row="1" TabStripPlacement="Left">
<TabItem>
<TabItem.Header>Combat AI</TabItem.Header>
<TabItem.Content>
<TabControl>
<TabItem>
<TabItem.Header>Combat</TabItem.Header>
<TabItem.Content>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<CheckBox Grid.Row="0" Grid.Column="0" IsChecked="{Binding AutoUseShots}">Auto use soul and spiritshots</CheckBox>
<CheckBox Grid.Row="1" Grid.Column="0" IsChecked="{Binding UseOnlySkills}">Use only skills</CheckBox>
<StackPanel Grid.Row="2" Grid.Column="0">
<Label>Attack distance for mili weapon:</Label>
<TextBox Width="100" HorizontalAlignment="Left">
<TextBox.Text>
<Binding Path="AttackDistanceMili" UpdateSourceTrigger="PropertyChanged"/>
</TextBox.Text>
</TextBox>
</StackPanel>
<StackPanel Grid.Row="3" Grid.Column="0">
<Label>Attack distance for bows:</Label>
<TextBox Width="100" HorizontalAlignment="Left">
<TextBox.Text>
<Binding Path="AttackDistanceBow" UpdateSourceTrigger="PropertyChanged"/>
</TextBox.Text>
</TextBox>
</StackPanel>
<StackPanel Grid.Row="4" Grid.Column="0">
<Label>Skill conditions:</Label>
<DataGrid
AutoGenerateColumns="False"
Height="150"
HorizontalAlignment="Left"
VerticalAlignment="Top"
ItemsSource="{Binding CombatSkills}">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Skill" Width="300">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox
ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Skills}"
SelectedValuePath="Id"
SelectedValue="{Binding Id}"
DisplayMemberPath="Name">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Max target % HP" Width="100" Binding="{Binding MaxTargetPercentHp}" />
<DataGridTextColumn Header="Min player % MP" Width="100" Binding="{Binding MinPlayerPercentMp}" />
<DataGridTextColumn Header="Max player % HP" Width="100" Binding="{Binding MaxPlayerPercentHp}" />
</DataGrid.Columns>
</DataGrid>
</StackPanel>
<StackPanel Grid.Row="5" Grid.Column="0">
<Label>Combat zone:</Label>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"></ColumnDefinition>
<ColumnDefinition Width="120"></ColumnDefinition>
<ColumnDefinition Width="80"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0">X:</Label>
<TextBox Width="100" HorizontalAlignment="Left" Grid.Row="0" Grid.Column="1">
<TextBox.Text>
<Binding Path="Zone.X" UpdateSourceTrigger="PropertyChanged"/>
</TextBox.Text>
</TextBox>
<Button Grid.Row="0" Grid.Column="2" Command="{Binding GetHeroPosition}">Current</Button>
<Label Grid.Row="1" Grid.Column="0">Y:</Label>
<TextBox Width="100" HorizontalAlignment="Left" Grid.Row="1" Grid.Column="1">
<TextBox.Text>
<Binding Path="Zone.Y" UpdateSourceTrigger="PropertyChanged"/>
</TextBox.Text>
</TextBox>
<Label Grid.Row="2" Grid.Column="0">Radius:</Label>
<TextBox Width="100" HorizontalAlignment="Left" Grid.Row="2" Grid.Column="1">
<TextBox.Text>
<Binding Path="Zone.Radius" UpdateSourceTrigger="PropertyChanged"/>
</TextBox.Text>
</TextBox>
</Grid>
</StackPanel>
</Grid>
</TabItem.Content>
</TabItem>
<TabItem>
<TabItem.Header>Mobs</TabItem.Header>
<TabItem.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2">
<Label>Max delta z:</Label>
<TextBox Width="100" HorizontalAlignment="Left">
<TextBox.Text>
<Binding Path="MobsMaxDeltaZ" UpdateSourceTrigger="PropertyChanged"/>
</TextBox.Text>
</TextBox>
</StackPanel>
<components:MultipleObjectSelector
Grid.Row="1"
Grid.Column="0"
Source="{Binding ExcludedMobs}"
Target="{Binding SelectedExcludedMobs}"
Header="Excluded:"/>
<components:MultipleObjectSelector
Grid.Row="1"
Grid.Column="1"
Source="{Binding IncludedMobs}"
Target="{Binding SelectedIncludedMobs}"
Header="Included:"/>
<StackPanel Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2">
<Label>Mobs level range:</Label>
<StackPanel Orientation="Horizontal">
<TextBox Width="100">
<TextBox.Text>
<Binding Path="MobLevelLowerLimit" UpdateSourceTrigger="PropertyChanged" TargetNullValue=""/>
</TextBox.Text>
</TextBox>
<TextBlock>&lt;= Player level &lt;=</TextBlock>
<TextBox Width="100">
<TextBox.Text>
<Binding Path="MobLevelUpperLimit" UpdateSourceTrigger="PropertyChanged" TargetNullValue=""/>
</TextBox.Text>
</TextBox>
</StackPanel>
</StackPanel>
</Grid>
</TabItem.Content>
</TabItem>
<TabItem>
<TabItem.Header>Drop</TabItem.Header>
<TabItem.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<CheckBox Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" IsChecked="{Binding PickupIfPossible}">Pickup if possible</CheckBox>
<StackPanel Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2">
<Label>Max delta z:</Label>
<TextBox Width="100" HorizontalAlignment="Left">
<TextBox.Text>
<Binding Path="PickupMaxDeltaZ" UpdateSourceTrigger="PropertyChanged"/>
</TextBox.Text>
</TextBox>
</StackPanel>
<StackPanel Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2">
<Label>Pickup attempts count:</Label>
<TextBox Width="100" HorizontalAlignment="Left">
<TextBox.Text>
<Binding Path="PickupAttemptsCount" UpdateSourceTrigger="PropertyChanged"/>
</TextBox.Text>
</TextBox>
</StackPanel>
<components:MultipleObjectSelector
Grid.Row="3"
Grid.Column="0"
Source="{Binding ExcludedItems}"
Target="{Binding SelectedExcludedItems}"
Header="Excluded:"/>
<components:MultipleObjectSelector
Grid.Row="3"
Grid.Column="1"
Source="{Binding IncludedItems}"
Target="{Binding SelectedIncludedItems}"
Header="Included:"/>
</Grid>
</TabItem.Content>
</TabItem>
<TabItem>
<TabItem.Header>Spoil</TabItem.Header>
<TabItem.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<CheckBox Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" IsChecked="{Binding SpoilIfPossible}">Spoil if possible</CheckBox>
<CheckBox Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" IsChecked="{Binding SpoilIsPriority}">Spoil is priority</CheckBox>
<StackPanel Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2">
<Label>Sweep attempts count:</Label>
<TextBox Width="100" HorizontalAlignment="Left">
<TextBox.Text>
<Binding Path="SweepAttemptsCount" UpdateSourceTrigger="PropertyChanged"/>
</TextBox.Text>
</TextBox>
</StackPanel>
<components:MultipleObjectSelector
Grid.Row="3"
Grid.Column="0"
Source="{Binding ExcludedSpoilMobs}"
Target="{Binding SelectedExcludedSpoilMobs}"
Header="Excluded:"/>
<components:MultipleObjectSelector
Grid.Row="3"
Grid.Column="1"
Source="{Binding IncludedSpoilMobs}"
Target="{Binding SelectedIncludedSpoilMobs}"
Header="Included:"/>
<StackPanel Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2">
<components:ObjectSelector
Source="{Binding Skills}"
SelectedValue="{Binding SpoilSkillId, Mode=TwoWay}"
Header="Spoil skill:"/>
</StackPanel>
<StackPanel Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="2">
<components:ObjectSelector
Source="{Binding Skills}"
SelectedValue="{Binding SweeperSkillId, Mode=TwoWay}"
Header="Sweeper skill:"/>
</StackPanel>
</Grid>
</TabItem.Content>
</TabItem>
<TabItem>
<TabItem.Header>Rest</TabItem.Header>
<TabItem.Content>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<Label>% HP range:</Label>
<StackPanel Orientation="Horizontal">
<TextBox Width="100">
<TextBox.Text>
<Binding Path="RestStartPercentHp" UpdateSourceTrigger="PropertyChanged"/>
</TextBox.Text>
</TextBox>
<TextBlock>-</TextBlock>
<TextBox Width="100">
<TextBox.Text>
<Binding Path="RestEndPercentHp" UpdateSourceTrigger="PropertyChanged"/>
</TextBox.Text>
</TextBox>
</StackPanel>
</StackPanel>
<StackPanel Grid.Row="1">
<Label>% MP range:</Label>
<StackPanel Orientation="Horizontal">
<TextBox Width="100">
<TextBox.Text>
<Binding Path="RestStartPercentMp" UpdateSourceTrigger="PropertyChanged"/>
</TextBox.Text>
</TextBox>
<TextBlock>-</TextBlock>
<TextBox Width="100">
<TextBox.Text>
<Binding Path="RestEndPercentMp" UpdateSourceTrigger="PropertyChanged"/>
</TextBox.Text>
</TextBox>
</StackPanel>
</StackPanel>
</Grid>
</TabItem.Content>
</TabItem>
</TabControl>
</TabItem.Content>
</TabItem>
<TabItem>
<TabItem.Header>Deleveling AI</TabItem.Header>
<TabItem.Content>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Grid.Column="0">
<Label>Target level:</Label>
<TextBox Width="100" HorizontalAlignment="Left">
<TextBox.Text>
<Binding Path="DelevelingTargetLevel" UpdateSourceTrigger="PropertyChanged"/>
</TextBox.Text>
</TextBox>
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="0">
<Label>Attack distance:</Label>
<TextBox Width="100" HorizontalAlignment="Left">
<TextBox.Text>
<Binding Path="DelevelingAttackDistance" UpdateSourceTrigger="PropertyChanged"/>
</TextBox.Text>
</TextBox>
</StackPanel>
<StackPanel Grid.Row="2" Grid.Column="0">
<components:ObjectSelector
Source="{Binding Skills}"
SelectedValue="{Binding DelevelingSkillId, Mode=TwoWay}"
Header="Attacking skill:"/>
</StackPanel>
</Grid>
</TabItem.Content>
</TabItem>
</TabControl>
<StackPanel Orientation="Horizontal" Grid.Row="2" VerticalAlignment="Bottom" HorizontalAlignment="Right" Margin="0,10,10,0">
<StackPanel.Resources>
<Style TargetType="{x:Type Button}">
<Setter Property="Margin" Value="8,0,0,0"/>
</Style>
</StackPanel.Resources>
<Button Command="{Binding SaveCommand}">Save</Button>
<Button Command="{Binding ResetCommand}">Reset</Button>
</StackPanel>
</Grid>
</Window>

View File

@ -0,0 +1,83 @@
using Client.Application.Commands;
using Client.Application.ViewModels;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace Client.Application.Views
{
/// <summary>
/// Interaction logic for CombatAIConfigView.xaml
/// </summary>
public partial class AIConfig : Window
{
private AIConfigViewModel viewModel;
public AIConfig(AIConfigViewModel viewModel)
{
InitializeComponent();
this.viewModel = viewModel;
DataContext = viewModel;
if (viewModel.Close == null)
{
viewModel.Close = () => Close();
}
if (viewModel.OpenSaveDialog == null)
{
viewModel.OpenSaveDialog = (string data) =>
{
var dialog = new SaveFileDialog()
{
Filter = "Json files(*.json)|*.json"
};
if (dialog.ShowDialog() == true)
{
File.WriteAllText(dialog.FileName, data);
}
};
}
if (viewModel.OpenOpenDialog == null)
{
viewModel.OpenOpenDialog = () =>
{
var dialog = new OpenFileDialog()
{
Filter = "Json files(*.json)|*.json"
};
if (dialog.ShowDialog() == true)
{
return File.ReadAllText(dialog.FileName);
}
return null;
};
}
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
e.Cancel = true;
Hide();
}
private void Window_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if ((bool) e.NewValue)
{
viewModel.LoadConfig();
}
}
}
}

View File

@ -29,18 +29,59 @@
<ColumnDefinition Width="444"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="22"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition Height="240"></RowDefinition>
</Grid.RowDefinitions>
<ListBox x:Name="listBox" Grid.Row="1" Grid.Column="0" ItemsSource="{Binding ChatMessages, Mode=OneWay}">
<Menu Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2">
<MenuItem Header="AI options">
<MenuItem Command="{Binding ToggleAICommand}">
<MenuItem.Style>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="Start"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding AIStatus, Mode=OneWay}" Value="True">
<Setter Property="Header" Value="Stop"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.Style>
</MenuItem>
<MenuItem ItemsSource="{Binding AITypes}" Header="AI type">
<MenuItem.ItemContainerStyle>
<Style>
<Setter Property="MenuItem.Header" Value="{Binding Value}"/>
<Setter Property="MenuItem.Command" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.ChangeAITypeCommand}"/>
<Setter Property="MenuItem.CommandParameter" Value="{Binding Key}"/>
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding>
<MultiBinding.Converter>
<services:EqualityConverter />
</MultiBinding.Converter>
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}" Path="DataContext.AIType" />
<Binding Path="Key" />
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="MenuItem.IsChecked" Value="True"/>
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
<MenuItem Header="Config" Click="AIConfig_Click"/>
</MenuItem>
</Menu>
<ListBox x:Name="listBox" Grid.Row="2" Grid.Column="0" ItemsSource="{Binding ChatMessages, Mode=OneWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Text, Mode=OneWay}" Foreground="{Binding Path=Color, Mode=OneWay}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<components:Map DataContext="{Binding Map}" />
<TabControl Grid.Row="0" Grid.Column="1">
<components:Map Grid.Column="0" Grid.Row="1" DataContext="{Binding Map}" />
<TabControl Grid.Row="1" Grid.Column="1">
<TabItem>
<TabItem.Header>Environment</TabItem.Header>
<TabItem.Content>
@ -156,7 +197,7 @@
</TabItem.Content>
</TabItem>
</TabControl>
<Grid Grid.Row="1" Grid.Column="1" DataContext="{Binding Hero, Mode=OneWay}" Margin="4" Visibility="{Binding Path=.,Converter={StaticResource NullToVisibilityConverter}}">
<Grid Grid.Row="2" Grid.Column="1" DataContext="{Binding Hero, Mode=OneWay}" Margin="4" Visibility="{Binding Path=.,Converter={StaticResource NullToVisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"></ColumnDefinition>
<ColumnDefinition Width="1*"></ColumnDefinition>
@ -224,13 +265,6 @@
</TextBlock>
</StackPanel>
</DockPanel>
<ListBox Grid.Row="3" Margin="4" ItemsSource="{Binding Attackers, Mode=OneWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Mode=OneWay}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Column="1" DataContext="{Binding Target, Mode=OneWay}" Visibility="{Binding Path=.,Converter={StaticResource NullToVisibilityConverter}}" Margin="4">
<TextBlock FontSize="16" Text="{Binding Path=Name, Mode=OneWay}"></TextBlock>
<TextBlock Text="{Binding Path=BriefInfo, Mode=OneWay}"></TextBlock>

View File

@ -1,7 +1,9 @@
using Client.Application.Components;
using Client.Application.ViewModels;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -23,13 +25,23 @@ namespace Client.Application.Views
public partial class MainWindow : Window
{
private readonly MainViewModel mainViewModel;
private readonly AIConfigViewModel aiConfigViewModel;
private AIConfig aiConfigView;
public MainWindow(MainViewModel mainViewModel)
public MainWindow(MainViewModel mainViewModel, AIConfigViewModel aiConfigViewModel)
{
InitializeComponent();
DataContext = mainViewModel;
this.mainViewModel = mainViewModel;
this.aiConfigViewModel = aiConfigViewModel;
aiConfigView = new AIConfig(aiConfigViewModel);
}
private void AIConfig_Click(object sender, RoutedEventArgs e)
{
aiConfigView.Owner = this;
aiConfigView.ShowDialog();
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,11 +1,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Client.Application.ViewModels;
using Client.Domain.AI;
using Client.Domain.Events;
using Client.Domain.Factories;
using Client.Domain.Parsers;
@ -26,6 +29,7 @@ namespace Client
private readonly EventBusInterface eventBus;
private readonly IServiceProvider serviceProvider;
private readonly string dllName;
private readonly AIInterface ai;
public Bot(
IServiceProvider serviceProvider,
@ -36,11 +40,12 @@ namespace Client
messageParser = serviceProvider.GetRequiredService<MessageParserInterface>();
entityHandlerFactory = serviceProvider.GetRequiredService<EntityHandlerFactoryInterface>();
eventBus = serviceProvider.GetRequiredService<EventBusInterface>();
ai = serviceProvider.GetRequiredService<AIInterface>();
this.serviceProvider = serviceProvider;
this.dllName = dllName;
}
public async void StartAsync()
public async Task StartAsync()
{
int hDll = LoadLibrary(dllName);
@ -56,9 +61,16 @@ namespace Client
await transport.ConnectAsync();
await transport.SendAsync("invalidate");
var aiTask = Task.Run(async () =>
{
while (true)
{
await ai.Update();
}
});
while (true)
{
await transport.StartReceiveAsync();
await transport.ReceiveAsync();
await transport.ConnectAsync();
}
}
@ -90,9 +102,13 @@ namespace Client
eventBus.Subscrbe((EventHandlerInterface<ItemCreatedEvent>)worldHandler);
eventBus.Subscrbe((EventHandlerInterface<ItemDeletedEvent>)worldHandler);
eventBus.Subscrbe((EventHandlerInterface<TargetChangedEvent>)serviceProvider.GetRequiredService<HeroHandler>());
eventBus.Subscrbe((EventHandlerInterface<TargetChangedEvent>)serviceProvider.GetRequiredService<NpcHandler>());
eventBus.Subscrbe((EventHandlerInterface<TargetChangedEvent>)serviceProvider.GetRequiredService<PlayerHandler>());
eventBus.Subscrbe(serviceProvider.GetRequiredService<HeroHandler>());
eventBus.Subscrbe(serviceProvider.GetRequiredService<NpcHandler>());
eventBus.Subscrbe(serviceProvider.GetRequiredService<PlayerHandler>());
var configViewModel = serviceProvider.GetRequiredService<AIConfigViewModel>();
eventBus.Subscrbe((EventHandlerInterface<HeroCreatedEvent>)configViewModel);
eventBus.Subscrbe((EventHandlerInterface<HeroDeletedEvent>)configViewModel);
}
private void OnMessage(string args)

View File

@ -26,9 +26,16 @@
</Content>
</ItemGroup>
<ItemGroup>
<None Remove="Assets\data\itemInfo.json" />
<None Remove="Assets\data\skillInfo.json" />
<None Remove="Assets\icons\_fallback - Copy.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.122" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
</ItemGroup>

102
Client/Domain/AI/AI.cs Normal file
View File

@ -0,0 +1,102 @@
using Client.Domain.AI.State;
using Client.Domain.Entities;
using Client.Domain.Events;
using Client.Domain.Service;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace Client.Domain.AI
{
public class AI : AIInterface
{
public AI(WorldHandler worldHandler, Config config, AsyncPathMoverInterface asyncPathMover, TransitionBuilderLocator locator)
{
this.worldHandler = worldHandler;
this.config = config;
this.asyncPathMover = asyncPathMover;
this.locator = locator;
states = StateBuilder.Build(this);
ResetState();
}
public void Toggle()
{
isEnabled = !isEnabled;
if (isEnabled)
{
ResetState();
}
}
public bool IsEnabled => isEnabled;
public TypeEnum Type { get { return type; } set { if (type != value) { type = value; ResetState(); } } }
public async Task Update()
{
await Task.Delay((int) config.DelayBetweenTransitions);
await Task.Run(() =>
{
if (isEnabled && worldHandler.Hero != null)
{
states[currentState].Execute();
foreach (var transition in locator.Get(Type).Build())
{
if (transition.fromStates.ContainsKey(BaseState.Type.Any) && transition.toState != currentState || transition.fromStates.ContainsKey(currentState))
{
if (transition.predicate(worldHandler, config, states[currentState]))
{
states[currentState].OnLeave();
currentState = transition.toState;
Debug.WriteLine(currentState.ToString());
states[currentState].OnEnter();
break;
}
}
}
}
else
{
ResetState();
}
});
}
public WorldHandler GetWorldHandler()
{
return worldHandler;
}
public Config GetConfig()
{
return config;
}
public AsyncPathMoverInterface GetAsyncPathMover()
{
return asyncPathMover;
}
private void ResetState()
{
currentState = BaseState.Type.Idle;
}
private readonly WorldHandler worldHandler;
private readonly Config config;
private readonly AsyncPathMoverInterface asyncPathMover;
private readonly TransitionBuilderLocator locator;
private BaseState.Type currentState;
private Dictionary<BaseState.Type, BaseState> states = new Dictionary<BaseState.Type, BaseState>();
private bool isEnabled = false;
private TypeEnum type = TypeEnum.Combat;
}
}

View File

@ -0,0 +1,21 @@
using Client.Domain.Events;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.AI
{
public interface AIInterface
{
Task Update();
void Toggle();
bool IsEnabled { get; }
TypeEnum Type { get; set; }
}
}

View File

@ -0,0 +1,25 @@
using Client.Domain.Common;
using Client.Domain.ValueObjects;
namespace Client.Domain.AI.Combat
{
public class CombatZone : ObservableObject
{
public CombatZone(Vector3 center, float radius)
{
Center = center;
Radius = radius;
}
public bool IsInside(Vector3 point)
{
return Center.HorizontalDistance(point) <= Radius;
}
public Vector3 Center { get { return center; } set { if (center != value) { center = value; OnPropertyChanged(); } }}
public float Radius { get { return radius; } set { if (radius != value) { radius = value; OnPropertyChanged(); } }}
private float radius;
private Vector3 center = new Vector3(0, 0, 0);
}
}

View File

@ -0,0 +1,111 @@
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.Domain.AI.Combat
{
public static class Helper
{
public static Skill? GetSkillByConfig(WorldHandler worldHandler, Config config, Hero hero, CreatureInterface target)
{
var conditions = config.Combat.SkillConditions;
var targetHp = target.VitalStats.HpPercent;
var heroMp = hero.VitalStats.MpPercent;
var heroHp = hero.VitalStats.HpPercent;
foreach (var condition in conditions )
{
var skill = worldHandler.GetSkillById(condition.Id);
if (skill != null)
{
if (condition.MaxTargetPercentHp < targetHp || condition.MinPlayerPercentMp > heroMp || condition.MaxPlayerPercentHp < heroHp)
{
continue;
}
if (skill.IsReadyToUse && hero.VitalStats.Mp >= skill.Cost)
{
return skill;
}
}
}
return null;
}
public static List<Drop> GetDropByConfig(WorldHandler worldHandler, Config config)
{
if (!config.Combat.PickupIfPossible)
{
return new List<Drop>();
}
var result = worldHandler.GetDropsSortedByDistanceToHero(config.Combat.PickupMaxDeltaZ)
.Where(x => !config.Combat.ExcludedItemIdsToPickup.ContainsKey(x.ItemId));
if (config.Combat.IncludedItemIdsToPickup.Count > 0)
{
result = result.Where(x => config.Combat.IncludedItemIdsToPickup.ContainsKey(x.ItemId));
}
return result.ToList();
}
public static List<NPC> GetMobsToAttackByConfig(WorldHandler worldHandler, Config config, Hero hero)
{
var result = worldHandler.GetAliveMobsSortedByDistanceToHero(config.Combat.MobsMaxDeltaZ)
.Where(x => !config.Combat.ExcludedMobIds.ContainsKey(x.NpcId));
result = result.Where(x => config.Combat.Zone.IsInside(x.Transform.Position));
if (config.Combat.IncludedMobIds.Count > 0)
{
result = result.Where(x => config.Combat.IncludedMobIds.ContainsKey(x.NpcId));
}
if (config.Combat.MobLevelLowerLimit != null)
{
result = result.Where(x => (int) (hero.ExperienceInfo.Level - x.Level) <= config.Combat.MobLevelLowerLimit);
}
if (config.Combat.MobLevelUpperLimit != null)
{
result = result.Where(x => (int) (x.Level - hero.ExperienceInfo.Level) <= config.Combat.MobLevelUpperLimit);
}
return result.ToList();
}
public static bool IsOnSpot(WorldHandler worldHandler, Config config, Hero hero)
{
if (config.Combat.Zone == null)
{
return true;
}
var spot = new Vector3(config.Combat.Zone.Center.X, config.Combat.Zone.Center.Y, hero.Transform.Position.Z);
return spot.Distance(hero.Transform.Position) <= 200;
}
public static uint GetAttackDistanceByConfig(WorldHandler worldHandler, Config config, Hero hero, CreatureInterface target)
{
Skill? skill = GetSkillByConfig(worldHandler, config, hero, target);
var equippedWeapon = worldHandler.GetEquippedWeapon();
var expectedDistance = equippedWeapon != null && equippedWeapon.WeaponType == Enums.WeaponTypeEnum.Bow
? config.Combat.AttackDistanceBow
: config.Combat.AttackDistanceMili;
if (skill != null)
{
expectedDistance = (uint)skill.Range;
}
return expectedDistance;
}
}
}

View File

@ -0,0 +1,147 @@
using Client.Domain.AI.State;
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.Domain.AI.Combat
{
public class TransitionBuilder : TransitionBuilderInterface
{
public List<TransitionBuilderInterface.Transition> Build()
{
if (transitions.Count == 0)
{
transitions = new List<TransitionBuilderInterface.Transition>()
{
new(new List<BaseState.Type>{BaseState.Type.Any}, BaseState.Type.Dead, (worldHandler, config, state) => {
if (worldHandler.Hero == null) {
return false;
}
return worldHandler.Hero.VitalStats.IsDead;
}),
new(new List<BaseState.Type>{BaseState.Type.Dead}, BaseState.Type.Idle, (worldHandler, config, state) => {
if (worldHandler.Hero == null) {
return false;
}
return !worldHandler.Hero.VitalStats.IsDead;
}),
new(new List<BaseState.Type>{BaseState.Type.Idle, BaseState.Type.MoveToTarget, BaseState.Type.Rest, BaseState.Type.MoveToSpot}, BaseState.Type.FindTarget, (worldHandler, config, state) => {
if (worldHandler.Hero == null) {
return false;
}
// TODO если нет цели, а тебя атаковали, то моб берется автоматом в таргет, из-за этого баг в Rest и MoveToSpot
// а без этой проверки зацикливается MoveToTarget->FindTarget->MoveToTarget
// один из вариантов решения, брать себя в таргет при входе в Rest и MoveToSpot
if (worldHandler.Hero.Target != null && (worldHandler.Hero.AttackerIds.Contains(worldHandler.Hero.Target.Id) || worldHandler.Hero.Target.VitalStats.IsDead))
{
return false;
}
return worldHandler.Hero.AttackerIds.Count > 0;
}),
new(new List<BaseState.Type>{BaseState.Type.FindTarget}, BaseState.Type.MoveToTarget, (worldHandler, config, state) => {
if (worldHandler.Hero == null) {
return false;
}
return worldHandler.Hero.HasValidTarget;
}),
new(new List<BaseState.Type>{BaseState.Type.FindTarget}, BaseState.Type.MoveToSpot, (worldHandler, config, state) => {
if (worldHandler.Hero == null) {
return false;
}
return Helper.GetMobsToAttackByConfig(worldHandler, config, worldHandler.Hero).Count == 0
&& !Helper.IsOnSpot(worldHandler, config, worldHandler.Hero);
}),
new(new List<BaseState.Type>{BaseState.Type.MoveToSpot}, BaseState.Type.Idle, (worldHandler, config, state) => {
if (worldHandler.Hero == null) {
return false;
}
if (Helper.GetMobsToAttackByConfig(worldHandler, config, worldHandler.Hero).Count > 0)
{
return true;
}
return Helper.IsOnSpot(worldHandler, config, worldHandler.Hero);
}),
new(new List<BaseState.Type>{BaseState.Type.MoveToTarget}, BaseState.Type.Idle, (worldHandler, config, state) => {
if (worldHandler.Hero == null) {
return false;
}
return !worldHandler.Hero.HasValidTarget;
}),
new(new List<BaseState.Type>{BaseState.Type.Idle}, BaseState.Type.Rest, (worldHandler, config, 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>{BaseState.Type.Rest}, BaseState.Type.Idle, (worldHandler, config, 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>{BaseState.Type.MoveToTarget}, BaseState.Type.Attack, (worldHandler, config, state) => {
if (worldHandler.Hero == null) {
return false;
}
if (worldHandler.Hero.Target == null)
{
return false;
}
if (config.Combat.SpoilIsPriority) {
var spoil = worldHandler.GetSkillById(config.Combat.SpoilSkillId);
if (spoil != null && !spoil.IsReadyToUse) {
return false;
}
}
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>{BaseState.Type.Attack}, BaseState.Type.Pickup, (worldHandler, config, state) => {
if (worldHandler.Hero == null) {
return false;
}
return !worldHandler.Hero.HasValidTarget;
}),
new(new List<BaseState.Type>{BaseState.Type.Attack}, BaseState.Type.FindTarget, (worldHandler, config, 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>{BaseState.Type.Pickup}, BaseState.Type.Idle, (worldHandler, config, state) => {
if (worldHandler.Hero == null) {
return false;
}
var currentState = (PickupState) state;
if (worldHandler.GetSkillById(config.Combat.SweeperSkillId) != null && currentState.IsSweeperMustBeUsed(worldHandler, config)) {
return false;
}
return currentState.GetDrops(worldHandler, config).Count == 0;
}),
new(new List<BaseState.Type>{BaseState.Type.Idle}, BaseState.Type.FindTarget),
};
}
return transitions;
}
private List<TransitionBuilderInterface.Transition> transitions = new List<TransitionBuilderInterface.Transition>();
}
}

View File

@ -0,0 +1,65 @@
using Client.Domain.AI.Combat;
using Client.Domain.ValueObjects;
using System.Collections.Generic;
namespace Client.Domain.AI
{
public class Config
{
public struct SkillCondition
{
public uint Id { get; set; }
public byte MaxTargetPercentHp { get; set; }
public byte MinPlayerPercentMp { get; set; }
public byte MaxPlayerPercentHp { get; set; }
}
public class CombatSection
{
public uint MobsMaxDeltaZ { get; set; } = 500;
public Dictionary<uint, bool> ExcludedMobIds { get; set; } = new Dictionary<uint, bool>();
public Dictionary<uint, bool> IncludedMobIds { get; set; } = new Dictionary<uint, bool>();
public byte? MobLevelLowerLimit { get; set; } = null;
public byte? MobLevelUpperLimit { get; set; } = null;
public byte RestStartPercentHp { get; set; } = 30;
public byte RestEndPecentHp { get; set; } = 100;
public byte RestStartPecentMp { get; set; } = 10;
public byte RestEndPecentMp { get; set; } = 100;
public CombatZone Zone { get; set; } = new CombatZone(new Vector3(0, 0, 0), 0);
public bool AutoUseShots { get; set; } = true;
public uint AttackDistanceMili { get; set; } = 80;
public uint AttackDistanceBow { get; set; } = 500;
public bool UseOnlySkills { get; set; } = false;
public List<SkillCondition> SkillConditions { get; set; } = new List<SkillCondition>();
public bool SpoilIfPossible { get; set; } = true;
public bool SpoilIsPriority { get; set; } = false;
public Dictionary<uint, bool> ExcludedSpoilMobIds { get; set; } = new Dictionary<uint, bool>();
public Dictionary<uint, bool> IncludedSpoilMobIds { get; set; } = new Dictionary<uint, bool>();
public uint SpoilSkillId { get; set; } = 254;
public uint SweeperSkillId { get; set; } = 42;
public byte SweepAttemptsCount { get; set; } = 10;
public bool PickupIfPossible { get; set; } = true;
public uint PickupMaxDeltaZ { get; set; } = 500;
public byte PickupAttemptsCount { get; set; } = 10;
public Dictionary<uint, bool> ExcludedItemIdsToPickup { get; set; } = new Dictionary<uint, bool>();
public Dictionary<uint, bool> IncludedItemIdsToPickup { get; set; } = new Dictionary<uint, bool>();
}
public class DelevelingSection
{
public byte TargetLevel { get; set; } = 20;
public uint AttackDistance { get; set; } = 80;
public uint SkillId { get; set; } = 0;
}
public uint DelayBetweenTransitions { get; set; } = 250;
public CombatSection Combat { get; } = new CombatSection();
public DelevelingSection Deleveling { get; } = new DelevelingSection();
}
}

View File

@ -0,0 +1,86 @@
using Client.Domain.AI.State;
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.Domain.AI.Deleveling
{
public class TransitionBuilder : TransitionBuilderInterface
{
public List<TransitionBuilderInterface.Transition> Build()
{
if (transitions.Count == 0)
{
transitions = new List<TransitionBuilderInterface.Transition>()
{
new(new List<BaseState.Type>{BaseState.Type.Any}, BaseState.Type.Dead, (worldHandler, config, state) => {
if (worldHandler.Hero == null) {
return false;
}
return worldHandler.Hero.VitalStats.IsDead;
}),
new(new List<BaseState.Type>{BaseState.Type.Dead}, BaseState.Type.Idle, (worldHandler, config, state) => {
if (worldHandler.Hero == null) {
return false;
}
return !worldHandler.Hero.VitalStats.IsDead;
}),
new(new List<BaseState.Type>{BaseState.Type.FindGuard}, BaseState.Type.MoveToTarget, (worldHandler, config, state) => {
if (worldHandler.Hero == null) {
return false;
}
return worldHandler.Hero.Target != null;
}),
new(new List<BaseState.Type>{BaseState.Type.MoveToTarget}, BaseState.Type.Idle, (worldHandler, config, state) => {
if (worldHandler.Hero == null) {
return false;
}
return worldHandler.Hero.Target == null;
}),
new(new List<BaseState.Type>{BaseState.Type.MoveToTarget}, BaseState.Type.AttackGuard, (worldHandler, config, state) => {
if (worldHandler.Hero == null) {
return false;
}
if (worldHandler.Hero.Target == null)
{
return false;
}
var distance = worldHandler.Hero.Transform.Position.HorizontalDistance(worldHandler.Hero.Target.Transform.Position);
var expectedDistance = config.Deleveling.AttackDistance;
return distance < expectedDistance;
}),
new(new List<BaseState.Type>{BaseState.Type.AttackGuard}, BaseState.Type.FindGuard, (worldHandler, config, state) => {
if (worldHandler.Hero == null) {
return false;
}
if (worldHandler.Hero.Target == null)
{
return true;
}
var distance = worldHandler.Hero.Transform.Position.HorizontalDistance(worldHandler.Hero.Target.Transform.Position);
var expectedDistance = config.Deleveling.AttackDistance;
return distance >= expectedDistance;
}),
new(new List<BaseState.Type>{BaseState.Type.Idle}, BaseState.Type.FindGuard, (worldHandler, config, state) => {
if (worldHandler.Hero == null) {
return false;
}
return worldHandler.Hero.ExperienceInfo.Level > config.Deleveling.TargetLevel;
}),
};
}
return transitions;
}
private List<TransitionBuilderInterface.Transition> transitions = new List<TransitionBuilderInterface.Transition>();
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.AI.IO
{
public interface ConfigDeserializerInterface
{
Config? Deserialize(string data);
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.AI.IO
{
public interface ConfigSerializerInterface
{
string Serialize(Config config);
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.AI.State
{
public class AnyState : BaseState
{
public AnyState(AI ai) : base(ai)
{
}
}
}

View File

@ -0,0 +1,28 @@
using Client.Application.Components;
using Client.Domain.Entities;
using Client.Domain.Service;
namespace Client.Domain.AI.State
{
public class AttackGuardState : BaseState
{
public AttackGuardState(AI ai) : base(ai)
{
}
protected override void DoExecute(WorldHandler worldHandler, Config config, AsyncPathMoverInterface asyncPathMover, Hero hero)
{
if (hero.Target == null)
{
return;
}
var skill = worldHandler.GetSkillById(config.Deleveling.SkillId);
if (skill != null && skill.IsReadyToUse && skill.Cost <= hero.VitalStats.Mp)
{
worldHandler.RequestUseSkill(skill.Id, true, false);
}
}
}
}

View File

@ -0,0 +1,72 @@
using Client.Domain.AI.Combat;
using Client.Domain.Entities;
using Client.Domain.Service;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace Client.Domain.AI.State
{
public class AttackState : BaseState
{
public AttackState(AI ai) : base(ai)
{
}
protected override void DoExecute(WorldHandler worldHandler, Config config, AsyncPathMoverInterface asyncPathMover, Hero hero)
{
if (hero.Target == null)
{
return;
}
if (!config.Combat.UseOnlySkills)
{
worldHandler.RequestAttackOrFollow(hero.Target.Id);
}
if (config.Combat.SpoilIfPossible)
{
NPC? npc = hero.Target as NPC;
var spoil = worldHandler.GetSkillById(config.Combat.SpoilSkillId);
if (spoil != null && npc != null && npc.SpoilState == Enums.SpoilStateEnum.None)
{
var excluded = config.Combat.ExcludedSpoilMobIds;
var included = config.Combat.IncludedSpoilMobIds;
if (!excluded.ContainsKey(npc.NpcId) && (included.Count == 0 || included.ContainsKey(npc.NpcId)))
{
if (spoil.IsReadyToUse && hero.VitalStats.Mp >= spoil.Cost)
{
worldHandler.RequestUseSkill(spoil.Id, false, false);
}
}
}
}
var skill = Helper.GetSkillByConfig(worldHandler, config, hero, hero.Target);
if (skill != null)
{
worldHandler.RequestUseSkill(skill.Id, false, false);
}
}
protected override void DoOnEnter(WorldHandler worldHandler, Config config, Hero hero)
{
if (config.Combat.AutoUseShots)
{
// todo use only appropriate grade
foreach (var item in worldHandler.GetShotItems())
{
if (!item.IsAutoused)
{
worldHandler.RequestToggleAutouseSoulshot(item.Id);
}
}
}
}
}
}

View File

@ -0,0 +1,85 @@
using Client.Domain.Entities;
using Client.Domain.Service;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.AI.State
{
public abstract class BaseState
{
public enum Type
{
Any,
Attack,
Dead,
FindTarget,
Idle,
MoveToTarget,
Pickup,
Rest,
MoveToSpot,
AttackGuard,
FindGuard
}
public BaseState(AI ai)
{
this.ai = ai;
}
public void Execute()
{
var hero = ai.GetWorldHandler().Hero;
if (hero == null)
{
return;
}
DoExecute(ai.GetWorldHandler(), ai.GetConfig(), ai.GetAsyncPathMover(), hero);
}
public void OnEnter()
{
var hero = ai.GetWorldHandler().Hero;
if (hero == null)
{
return;
}
DoOnEnter(ai.GetWorldHandler(), ai.GetConfig(), hero);
}
public void OnLeave()
{
var hero = ai.GetWorldHandler().Hero;
if (hero == null)
{
return;
}
ai.GetAsyncPathMover().Unlock();
DoOnLeave(ai.GetWorldHandler(), ai.GetConfig(), hero);
}
protected virtual void DoExecute(WorldHandler worldHandler, Config config, AsyncPathMoverInterface asyncPathMover, Hero hero)
{
}
protected virtual void DoOnEnter(WorldHandler worldHandler, Config config, Hero hero)
{
}
protected virtual void DoOnLeave(WorldHandler worldHandler, Config config, Hero hero)
{
}
private readonly AI ai;
}
}

View File

@ -0,0 +1,22 @@
using Client.Domain.Entities;
using Client.Domain.Service;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.AI.State
{
public class DeadState : BaseState
{
public DeadState(AI ai) : base(ai)
{
}
protected override void DoOnEnter(WorldHandler worldHandler, Config config, Hero hero)
{
worldHandler.RequestRestartPoint(Enums.RestartPointTypeEnum.Village);
}
}
}

View File

@ -0,0 +1,28 @@
using Client.Domain.Entities;
using Client.Domain.Service;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.AI.State
{
public class FindGuardState : BaseState
{
public FindGuardState(AI ai) : base(ai)
{
}
protected override void DoExecute(WorldHandler worldHandler, Config config, AsyncPathMoverInterface asyncPathMover, Hero hero)
{
var targetId = worldHandler.GetGuards().FirstOrDefault()?.Id;
if (targetId != null)
{
worldHandler.RequestAcquireTarget((uint)targetId);
}
}
}
}

View File

@ -0,0 +1,34 @@
using Client.Domain.AI.Combat;
using Client.Domain.Entities;
using Client.Domain.Service;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.AI.State
{
public class FindTargetState : BaseState
{
public FindTargetState(AI ai) : base(ai)
{
}
protected override void DoExecute(WorldHandler worldHandler, Config config, AsyncPathMoverInterface asyncPathMover, Hero hero)
{
uint? targetId = hero.AttackerIds.Count > 0 ? hero.AttackerIds.First() : null;
if (targetId == null)
{
targetId = Helper.GetMobsToAttackByConfig(worldHandler, config, hero).FirstOrDefault()?.Id;
}
if (targetId != null)
{
worldHandler.RequestAcquireTarget((uint)targetId);
}
}
}
}

View File

@ -0,0 +1,17 @@
using Client.Domain.Entities;
using Client.Domain.Service;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.AI.State
{
public class IdleState : BaseState
{
public IdleState(AI ai) : base(ai)
{
}
}
}

View File

@ -0,0 +1,37 @@
using Client.Domain.Entities;
using Client.Domain.Service;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace Client.Domain.AI.State
{
public class MoveToSpotState : BaseState
{
public MoveToSpotState(AI ai) : base(ai)
{
}
protected override void DoOnEnter(WorldHandler worldHandler, Config config, Hero hero)
{
worldHandler.RequestAcquireTarget(hero.Id);
}
protected override void DoExecute(WorldHandler worldHandler, Config config, AsyncPathMoverInterface asyncPathMover, Hero hero)
{
if (asyncPathMover.IsLocked)
{
return;
}
asyncPathMover.MoveAsync(new ValueObjects.Vector3(
config.Combat.Zone.Center.X,
config.Combat.Zone.Center.Y,
hero.Transform.Position.Z
));
}
}
}

View File

@ -0,0 +1,34 @@
using Client.Domain.AI.Combat;
using Client.Domain.Entities;
using Client.Domain.Service;
using Client.Infrastructure.Service;
namespace Client.Domain.AI.State
{
public class MoveToTargetState : BaseState
{
public MoveToTargetState(AI ai) : base(ai)
{
}
protected override void DoExecute(WorldHandler worldHandler, Config config, AsyncPathMoverInterface asyncPathMover, Hero hero)
{
var target = hero.Target;
if (target == null)
{
target = hero;
}
var distance = hero.Transform.Position.HorizontalDistance(target.Transform.Position);
if (asyncPathMover.IsLocked)
{
return;
}
if (distance >= Helper.GetAttackDistanceByConfig(worldHandler, config, hero, target))
{
asyncPathMover.MoveAsync(target.Transform.Position);
}
}
}
}

View File

@ -0,0 +1,87 @@
using Client.Domain.AI.Combat;
using Client.Domain.Entities;
using Client.Domain.Service;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Client.Domain.AI.State
{
public class PickupState : BaseState
{
public PickupState(AI ai) : base(ai)
{
}
public List<Drop> GetDrops(WorldHandler worldHandler, Config config)
{
var drops = Helper.GetDropByConfig(worldHandler, config);
for (var i = drops.Count - 1; i >= 0; i--)
{
if (pickupAttempts.ContainsKey(drops[0].Id) && pickupAttempts[drops[0].Id] > config.Combat.PickupAttemptsCount)
{
drops.RemoveAt(i);
}
}
return drops;
}
public bool IsSweeperMustBeUsed(WorldHandler worldHandler, Config config)
{
return GetSweepableMobs(worldHandler, config).Count > 0;
}
protected override void DoExecute(WorldHandler worldHandler, Config config, AsyncPathMoverInterface asyncPathMover, Hero hero)
{
if (IsSweeperMustBeUsed(worldHandler, config))
{
var mob = GetSweepableMobs(worldHandler, config).First();
var sweeper = worldHandler.GetSkillById(config.Combat.SweeperSkillId);
if (sweeper != null && sweeper.IsReadyToUse && hero.VitalStats.Mp >= sweeper.Cost)
{
worldHandler.RequestAcquireTarget(mob.Id);
worldHandler.RequestUseSkill(sweeper.Id, false, false);
if (!sweepAttempts.ContainsKey(mob.Id))
{
sweepAttempts[mob.Id] = 0;
}
sweepAttempts[mob.Id]++;
}
}
if (!hero.Transform.IsMoving)
{
var drops = GetDrops(worldHandler, config);
if (drops.Count > 0)
{
worldHandler.RequestPickUp(drops[0].Id);
if (!pickupAttempts.ContainsKey(drops[0].Id))
{
pickupAttempts[drops[0].Id] = 0;
}
pickupAttempts[drops[0].Id]++;
}
}
}
protected override void DoOnLeave(WorldHandler worldHandler, Config config, Hero hero)
{
pickupAttempts.Clear();
sweepAttempts.Clear();
}
private List<NPC> GetSweepableMobs(WorldHandler worldHandler, Config config)
{
return worldHandler.GetDeadMobsSortedByDistanceToHero(config.Combat.MobsMaxDeltaZ)
.Where(x =>
{
return x.SpoilState == Enums.SpoilStateEnum.Sweepable &&
(!sweepAttempts.ContainsKey(x.Id) || sweepAttempts[x.Id] <= config.Combat.SweepAttemptsCount);
})
.ToList();
}
private Dictionary<uint, short> pickupAttempts = new Dictionary<uint, short>();
private Dictionary<uint, short> sweepAttempts = new Dictionary<uint, short>();
}
}

View File

@ -0,0 +1,42 @@
using Client.Domain.Entities;
using Client.Domain.Service;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.AI.State
{
public class RestState : BaseState
{
public RestState(AI ai) : base(ai)
{
}
protected override void DoOnEnter(WorldHandler worldHandler, Config config, Hero hero)
{
worldHandler.RequestAcquireTarget(hero.Id);
}
protected override void DoExecute(WorldHandler worldHandler, Config config, AsyncPathMoverInterface asyncPathMover, Hero hero)
{
if (!hero.IsStanding)
{
return;
}
worldHandler.RequestSit();
}
protected override void DoOnLeave(WorldHandler worldHandler, Config config, Hero hero)
{
if (hero.IsStanding)
{
return;
}
worldHandler.RequestStand();
}
}
}

View File

@ -0,0 +1,30 @@
using Client.Domain.AI.State;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.AI
{
public static class StateBuilder
{
public static Dictionary<BaseState.Type, BaseState> Build(AI ai)
{
return new Dictionary<BaseState.Type, BaseState>
{
{ BaseState.Type.Any, new AnyState(ai) },
{ BaseState.Type.Attack, new AttackState(ai) },
{ BaseState.Type.Dead, new DeadState(ai) },
{ BaseState.Type.FindTarget, new FindTargetState(ai) },
{ BaseState.Type.Idle, new IdleState(ai) },
{ BaseState.Type.MoveToTarget, new MoveToTargetState(ai) },
{ BaseState.Type.Pickup, new PickupState(ai) },
{ BaseState.Type.Rest, new RestState(ai) },
{ BaseState.Type.MoveToSpot, new MoveToSpotState(ai) },
{ BaseState.Type.AttackGuard, new AttackGuardState(ai) },
{ BaseState.Type.FindGuard, new FindGuardState(ai) }
};
}
}
}

View File

@ -0,0 +1,29 @@
using Client.Domain.AI.State;
using Client.Domain.Service;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.AI
{
public interface TransitionBuilderInterface
{
public struct Transition
{
public readonly Dictionary<BaseState.Type, BaseState.Type> fromStates;
public readonly BaseState.Type toState;
public readonly Func<WorldHandler, Config, BaseState, bool> predicate;
public Transition(List<BaseState.Type> fromStates, BaseState.Type toState, Func<WorldHandler, Config, BaseState, bool>? predicate = null)
{
this.fromStates = fromStates.ToDictionary(x => x, x => x);
this.toState = toState;
this.predicate = predicate != null ? predicate : (worldHandler, config, state) => { return true; };
}
}
List<Transition> Build();
}
}

View File

@ -0,0 +1,29 @@
using System.Collections.Generic;
using CombatTransitionBuilder = Client.Domain.AI.Combat.TransitionBuilder;
using DelevelingTransitionBuilder = Client.Domain.AI.Deleveling.TransitionBuilder;
namespace Client.Domain.AI
{
public class TransitionBuilderLocator
{
public TransitionBuilderInterface Get(TypeEnum type)
{
if (!builders.ContainsKey(type))
{
switch (type)
{
case TypeEnum.Combat:
builders.Add(type, new CombatTransitionBuilder());
break;
case TypeEnum.Deleveling:
builders.Add(type, new DelevelingTransitionBuilder());
break;
}
}
return builders[type];
}
private Dictionary<TypeEnum, TransitionBuilderInterface> builders = new Dictionary<TypeEnum, TransitionBuilderInterface>();
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.AI
{
public enum TypeEnum: byte
{
Combat,
Deleveling
}
}

View File

@ -10,11 +10,11 @@ namespace Client.Domain.Common
public static class ObservableCollectionExtensions
{
public static void RemoveAll<T>(this ObservableCollection<T> collection,
Func<T, bool> condition)
Func<T, bool>? condition = null)
{
for (int i = collection.Count - 1; i >= 0; i--)
{
if (condition(collection[i]))
if (condition == null || condition(collection[i]))
{
collection.RemoveAt(i);
}

View File

@ -15,8 +15,9 @@ namespace Client.Domain.Entities
public string Name { get; set; }
public string IconName { get; set; }
public string Description { get; set; }
public int Mana { get { return mana; } set { mana = value; }}
public int Mana { get; set; }
public uint Weight { get; set; }
public virtual string FullDescription => Description;
public BaseItem(uint id, uint itemId, ItemTypeEnum type, string name, string iconName, string description, int mana, uint weight)
{
@ -26,10 +27,8 @@ namespace Client.Domain.Entities
Name = name;
IconName = iconName;
Description = description;
this.mana = mana;
Mana = mana;
Weight = weight;
}
private int mana;
}
}

View File

@ -12,7 +12,6 @@ namespace Client.Domain.Entities
public uint Amount { get => amount; set => amount = value; }
public bool IsQuest { get; set; }
public bool IsAutoused { get => isAutoused; set => isAutoused = value; }
public string FullDescription { get => Description; }
public EtcItem(uint id, uint itemId, ItemTypeEnum type, string name, string iconName, string description, int mana, uint weight, uint amount, bool isQuest, bool isAutoused) :
base(id, itemId, type, name, iconName, description, mana, weight)

View File

@ -70,6 +70,14 @@ namespace Client.Domain.Entities
[JsonProperty("AttackerIds", ObjectCreationHandling = ObjectCreationHandling.Replace)]
public List<uint> AttackerIds { get => attackerIds; set { if (!value.All(attackerIds.Contains) || !attackerIds.All(value.Contains)) { attackerIds = value; OnPropertyChanged("AttackerIds"); } } }
public bool HasValidTarget
{
get
{
return Target != null && Target.IsHostile && !Target.VitalStats.IsDead;
}
}
public Hero(uint id, Transform transform, FullName fullName, VitalStats vitalStats, Phenotype phenotype, ExperienceInfo experienceInfo, PermanentStats permanentStats, VariableStats variableStats, Reputation reputation, InventoryInfo inventoryInfo, uint targetId, bool isStanding)
{
Id = id;

View File

@ -16,9 +16,6 @@ namespace Client.Domain.Entities
string Description { get; set; }
int Mana { get; set; }
uint Weight { get; set; }
uint Amount { get; set; }
bool IsQuest { get; set; }
bool IsAutoused { get; set; }
string FullDescription { get; }
}
}

View File

@ -16,7 +16,7 @@ namespace Client.Domain.Entities
public Transform Transform { get; set; }
public bool IsHostile { get; set; }
public uint NpcId { get; set; }
public SpoilStateEnum SpoilState { get; set; }
public SpoilStateEnum SpoilState { get { return spoilState; } set { if (spoilState != value) { spoilState = value; OnPropertyChanged(); } } }
public FullName FullName
{
get => fullName;
@ -134,5 +134,6 @@ namespace Client.Domain.Entities
private uint aggroRadius;
private VitalStats vitalStats;
private FullName fullName;
private SpoilStateEnum spoilState;
}
}

View File

@ -0,0 +1,28 @@
using Client.Domain.Enums;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.Entities
{
public class WeaponItem : BaseItem, ItemInterface
{
public WeaponTypeEnum WeaponType { get; set; }
public CrystalTypeEnum CrystalType { get; set; }
public byte SoulshotCount { get; set; }
public byte SpiritshotCount { get; set; }
public bool IsEquipped { get; set; }
public WeaponItem(uint id, uint itemId, ItemTypeEnum type, string name, string iconName, string description, int mana, uint weight, WeaponTypeEnum weaponType, CrystalTypeEnum crystalType, byte soulshotCount, byte spiritshotCount, bool isEquipped) :
base(id, itemId, type, name, iconName, description, mana, weight)
{
WeaponType = weaponType;
CrystalType = crystalType;
SoulshotCount = soulshotCount;
SpiritshotCount = spiritshotCount;
IsEquipped = isEquipped;
}
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.Enums
{
public enum CrystalTypeEnum
{
None = -1,
Ng,
D,
C,
B,
A,
S
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.Enums
{
public enum WeaponTypeEnum
{
None = 0,
Sword,
Blunt,
Dagger,
Pole,
Fist,
Bow,
Etc,
Dualsword,
Pet,
FishingRod
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.Helpers
{
public class ItemInfo : ObjectInfo
{
public bool IsShot { get; internal set; }
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.Helpers
{
public interface ItemInfoHelperInterface
{
List<ItemInfo> GetAllItems();
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.Helpers
{
public class NpcInfo : ObjectInfo
{
public uint Level { get; internal set; }
public uint AggroRadius { get; internal set; }
public bool IsGuard { get; internal set; }
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static Client.Infrastructure.Helpers.ConfigurationNpcInfoHelper;
namespace Client.Domain.Helpers
{
@ -10,5 +11,6 @@ namespace Client.Domain.Helpers
{
uint GetLevel(uint id);
uint GetAggroRadius(uint id);
List<NpcInfo> GetAllNpc();
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.Helpers
{
public class ObjectInfo
{
public uint Id { get; internal set; }
public string Name { get; internal set; } = "";
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.Helpers
{
public class SkillInfo : ObjectInfo
{
public bool IsActive { get; internal set; }
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Domain.Helpers
{
public interface SkillInfoHelperInterface
{
Dictionary<uint, SkillInfo> GetAllSkills();
}
}

View File

@ -14,5 +14,8 @@ namespace Client.Domain.Service
public ObservableCollection<PathSegment> Path { get; }
public Task<bool> MoveAsync(Vector3 location);
public Task MoveUntilReachedAsync(Vector3 location);
public bool IsLocked { get; }
public void Unlock();
}
}

View File

@ -12,6 +12,8 @@ using System.Text;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using Client.Domain.Transports;
using Client.Domain.Common;
using Client.Domain.Helpers;
namespace Client.Domain.Service
{
@ -104,7 +106,7 @@ namespace Client.Domain.Service
Debug.WriteLine("RequestUseSkill: skill " + id + " not found");
return;
}
if (!skill.IsActive)
{
Debug.WriteLine("RequestUseSkill: skill " + id + " is passive");
@ -136,7 +138,7 @@ namespace Client.Domain.Service
SendMessage(OutgoingMessageTypeEnum.UseItem, id);
}
public void RequestToggleAutouseSoulshot(uint id)
{
if (hero == null)
@ -184,6 +186,7 @@ namespace Client.Domain.Service
SendMessage(OutgoingMessageTypeEnum.Stand);
}
public void RequestRestartPoint(RestartPointTypeEnum type)
{
if (hero == null)
@ -194,13 +197,126 @@ namespace Client.Domain.Service
SendMessage(OutgoingMessageTypeEnum.RestartPoint, type);
}
public List<NPC> GetAliveMobsSortedByDistanceToHero(uint deltaZ)
{
if (hero == null)
{
return new List<NPC> { };
}
return creatures
.Where(a =>
{
return a.Value is NPC
&& a.Value.IsHostile
&& !a.Value.VitalStats.IsDead
&& MathF.Abs(a.Value.DeltaZ(hero)) <= deltaZ;
})
.OrderBy(a => a.Value.Distance(hero))
.Select(a => (NPC)a.Value)
.ToList();
}
public List<NPC> GetDeadMobsSortedByDistanceToHero(uint deltaZ)
{
if (hero == null)
{
return new List<NPC> { };
}
return creatures
.Where(a =>
{
return a.Value is NPC
&& a.Value.IsHostile
&& a.Value.VitalStats.IsDead
&& MathF.Abs(a.Value.DeltaZ(hero)) <= deltaZ;
})
.OrderBy(a => a.Value.Distance(hero))
.Select(a => (NPC)a.Value)
.ToList();
}
public List<Drop> GetDropsSortedByDistanceToHero(uint deltaZ)
{
if (hero == null)
{
return new List<Drop> { };
}
return drops
.Where(x => MathF.Abs(x.Value.Transform.Position.Z - hero.Transform.Position.Z) <= deltaZ)
.OrderBy(a => a.Value.Transform.Position.HorizontalDistance(hero.Transform.Position))
.Select(a => a.Value)
.ToList();
}
public Skill? GetSkillById(uint id)
{
return skills.GetValueOrDefault(id);
}
public ItemInterface? GetItemById(uint id)
{
return items.Select(x => x.Value)
.Where(x => x.ItemId == id)
.FirstOrDefault();
}
public List<EtcItem> GetShotItems()
{
var shotIds = itemInfoHelper.GetAllItems()
.Where(x => x.IsShot)
.Select(x => x.Id)
.ToDictionary(x => x, x => x);
return items.Select(x => x.Value)
.Where(x => x is EtcItem && shotIds.ContainsKey(x.ItemId))
.Cast<EtcItem>()
.ToList();
}
public List<NPC> GetGuards()
{
if (hero == null)
{
return new List<NPC> { };
}
var npcIds = npcInfoHelper.GetAllNpc()
.Where(x => x.IsGuard)
.Select(x => x.Id)
.ToDictionary(x => x, x => x);
return creatures
.Where(x =>
{
return x.Value is NPC && npcIds.ContainsKey(((NPC)x.Value).NpcId);
})
.Select(x => (NPC) x.Value)
.OrderBy(x => x.Distance(hero))
.ToList();
}
public WeaponItem? GetEquippedWeapon()
{
return items.Select(x => x.Value)
.Where(x =>
{
return x is WeaponItem
&& ((WeaponItem)x).IsEquipped;
})
.Cast<WeaponItem>()
.FirstOrDefault();
}
private void SendMessage<T>(OutgoingMessageTypeEnum type, T? content = default)
{
var message = outgoingMessageBuilder.Build(
new OutgoingMessage<T>(type, content)
);
transport.SendAsync(message);
Debug.WriteLine(message);
}
private void SendMessage(OutgoingMessageTypeEnum type)
@ -272,10 +388,12 @@ namespace Client.Domain.Service
}
#endregion
public WorldHandler(OutgoingMessageBuilderInterface outgoingMessageBuilder, TransportInterface transport)
public WorldHandler(OutgoingMessageBuilderInterface outgoingMessageBuilder, TransportInterface transport, ItemInfoHelperInterface itemInfoHelper, NpcInfoHelperInterface npcInfoHelper)
{
this.outgoingMessageBuilder = outgoingMessageBuilder;
this.transport = transport;
this.itemInfoHelper = itemInfoHelper;
this.npcInfoHelper = npcInfoHelper;
}
private Hero? hero;
@ -285,5 +403,7 @@ namespace Client.Domain.Service
private ConcurrentDictionary<uint, ItemInterface> items = new ConcurrentDictionary<uint, ItemInterface>();
private readonly OutgoingMessageBuilderInterface outgoingMessageBuilder;
private readonly TransportInterface transport;
private ItemInfoHelperInterface itemInfoHelper;
private readonly NpcInfoHelperInterface npcInfoHelper;
}
}

View File

@ -13,7 +13,7 @@ namespace Client.Domain.Transports
bool IsConnected();
Task ConnectAsync();
Task SendAsync(string data);
Task StartReceiveAsync();
Task ReceiveAsync();
public delegate void DelegateMessage(string args);
}

View File

@ -45,6 +45,14 @@ namespace Client.Domain.ValueObjects
}
}
public bool IsMoving
{
get
{
return !velocity.ApproximatelyEquals(Vector3.Zero, 0.0001f);
}
}
public Transform(Vector3 position, Vector3 rotation, Vector3 velocity, Vector3 acceleration)
{
this.position = position;

View File

@ -57,6 +57,31 @@ namespace Client.Domain.ValueObjects
return equals;
}
public float GetAngleBetweenDegree(Vector3 other)
{
return GetAngleBetween(other) / MathF.PI * 180;
}
public float GetAngleBetween(Vector3 other)
{
return MathF.Acos(DotProduct(other) / (Distance(Zero) * other.Distance(Zero)));
}
public float DotProduct(Vector3 other)
{
return x * other.x + y * other.y + z * other.z;
}
public float Distance(Vector3 other)
{
return MathF.Sqrt(MathF.Pow(x - other.x, 2) + MathF.Pow(y - other.y, 2) + MathF.Pow(z - other.z, 2));
}
public static Vector3 operator -(Vector3 left, Vector3 right)
{
return new Vector3(left.x - right.x, left.y - right.y, left.z - right.z);
}
public static readonly Vector3 Zero = new Vector3(0, 0, 0);
}
}

View File

@ -0,0 +1,19 @@
using Client.Domain.AI;
using Client.Domain.AI.IO;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Infrastructure.AI.IO
{
public class JsonConfigDeserializer : ConfigDeserializerInterface
{
public Config? Deserialize(string data)
{
return JsonConvert.DeserializeObject<Config>(data);
}
}
}

View File

@ -0,0 +1,20 @@
using Client.Domain.AI;
using Client.Domain.AI.IO;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client.Infrastructure.AI.IO
{
public class JsonConfigSerializer : ConfigSerializerInterface
{
public string Serialize(Config config)
{
return JsonConvert.SerializeObject(config, Formatting.Indented, new JsonSerializerSettings() { Culture = CultureInfo.InvariantCulture, ReferenceLoopHandling = ReferenceLoopHandling.Ignore });
}
}
}

View File

@ -26,6 +26,8 @@ namespace Client.Infrastructure.Factories
{
case ItemTypeEnum.Etc:
return JsonConvert.DeserializeObject<EtcItem>(data, settings);
case ItemTypeEnum.Weapon:
return JsonConvert.DeserializeObject<WeaponItem>(data, settings);
default:
return JsonConvert.DeserializeObject<EtcItem>(data, settings); //fixme temporary
}

View File

@ -0,0 +1,54 @@
using Client.Domain.Helpers;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;
namespace Client.Infrastructure.Helpers
{
public class ConfigurationItemInfoHelper : ItemInfoHelperInterface
{
public List<ItemInfo> GetAllItems()
{
LoadItems();
return itemsInfo;
}
public ConfigurationItemInfoHelper(IConfiguration configuration)
{
this.configuration = configuration;
}
private void LoadItems()
{
if (itemsInfo.Count == 0)
{
var items = configuration.GetRequiredSection("itemInfo").GetChildren();
foreach (var item in items)
{
var id = uint.Parse(item.Key);
var isShot = false;
if (item != null)
{
bool.TryParse(item.GetSection("isShot").Value, out isShot);
}
itemsInfo.Add(new ItemInfo
{
Id = id,
Name = string.Format("{0} [{1}]", item?.GetRequiredSection("name").Value ?? "", id),
IsShot = isShot
});
}
}
}
private readonly IConfiguration configuration;
private List<ItemInfo> itemsInfo = new List<ItemInfo>();
}
}

View File

@ -6,7 +6,6 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using static Client.Infrastructure.Helpers.ConfigurationExperienceHelper;
namespace Client.Infrastructure.Helpers
{
@ -14,12 +13,30 @@ namespace Client.Infrastructure.Helpers
{
public uint GetLevel(uint id)
{
return GetNpcInfo(id).level;
LoadNpc();
if (npcInfo.ContainsKey(id))
{
return npcInfo[id].Level;
}
return 0;
}
public uint GetAggroRadius(uint id)
{
return GetNpcInfo(id).aggroRadius;
LoadNpc();
if (npcInfo.ContainsKey(id))
{
return npcInfo[id].AggroRadius;
}
return 0;
}
public List<NpcInfo> GetAllNpc()
{
LoadNpc();
return npcInfo.Select(x => x.Value).ToList();
}
public ConfigurationNpcInfoHelper(IConfiguration configuration)
@ -27,36 +44,37 @@ namespace Client.Infrastructure.Helpers
this.configuration = configuration;
}
private NpcInfo GetNpcInfo(uint id)
private void LoadNpc()
{
if (!npcInfo.ContainsKey(id))
if (npcInfo.Count == 0)
{
var item = configuration.GetRequiredSection("npcInfo").GetChildren()
.Where(x => x.Key == id.ToString())
.FirstOrDefault();
uint level = 0;
uint aggroRadius = 0;
if (item != null)
var items = configuration.GetRequiredSection("npcInfo").GetChildren();
foreach (var item in items)
{
uint.TryParse(item.GetRequiredSection("level").Value, out level);
uint.TryParse(item.GetRequiredSection("aggroRadius").Value, out aggroRadius);
var id = uint.Parse(item.Key);
uint level = 0;
uint aggroRadius = 0;
bool isGuard = false;
if (item != null)
{
uint.TryParse(item.GetRequiredSection("level").Value, out level);
uint.TryParse(item.GetRequiredSection("aggroRadius").Value, out aggroRadius);
bool.TryParse(item.GetRequiredSection("isGuard").Value, out isGuard);
}
npcInfo[id] = new NpcInfo
{
Id = id,
Level = level,
AggroRadius = aggroRadius,
Name = string.Format("{0} [{1}]", item?.GetRequiredSection("name").Value ?? "", id),
IsGuard = isGuard
};
}
npcInfo[id] = new NpcInfo
{
level = level,
aggroRadius = aggroRadius
};
}
return npcInfo[id];
}
private readonly IConfiguration configuration;
private Dictionary<uint, NpcInfo> npcInfo = new Dictionary<uint, NpcInfo>();
private class NpcInfo
{
public uint level { get; set; }
public uint aggroRadius { get; set; }
}
}
}

View File

@ -0,0 +1,53 @@
using Client.Domain.Helpers;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;
namespace Client.Infrastructure.Helpers
{
public class ConfigurationSkillInfoHelper : SkillInfoHelperInterface
{
public Dictionary<uint, SkillInfo> GetAllSkills()
{
LoadSkills();
return skillsInfo;
}
public ConfigurationSkillInfoHelper(IConfiguration configuration)
{
this.configuration = configuration;
}
private void LoadSkills()
{
if (skillsInfo.Count == 0)
{
var items = configuration.GetRequiredSection("skillInfo").GetChildren();
foreach (var item in items)
{
var id = uint.Parse(item.Key);
var isActive = false;
if (item != null)
{
bool.TryParse(item.GetRequiredSection("IsActive").Value, out isActive);
}
skillsInfo[id]=(new SkillInfo
{
Id = id,
Name = string.Format("{0} [{1}]", item?.GetRequiredSection("name").Value ?? "", id),
IsActive = isActive
});
}
}
}
private readonly IConfiguration configuration;
private Dictionary<uint, SkillInfo> skillsInfo = new Dictionary<uint, SkillInfo>();
}
}

View File

@ -28,7 +28,19 @@ namespace Client.Infrastructure.Service
private CancellationTokenSource? cancellationTokenSource;
public ObservableCollection<PathSegment> Path { get; private set; } = new ObservableCollection<PathSegment>();
public bool IsBusy { get; private set; } = false;
public bool IsLocked { get; private set; } = false;
public void Unlock()
{
IsLocked = false;
if (cancellationTokenSource != null)
{
cancellationTokenSource.Cancel();
cancellationTokenSource.Dispose();
cancellationTokenSource = null;
Path.Clear();
}
}
public async Task MoveUntilReachedAsync(Vector3 location)
{
@ -41,7 +53,7 @@ namespace Client.Infrastructure.Service
public async Task<bool> MoveAsync(Vector3 location)
{
IsBusy = true;
IsLocked = true;
if (cancellationTokenSource != null)
{
@ -55,11 +67,9 @@ namespace Client.Infrastructure.Service
{
return await Task.Run(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
Debug.WriteLine("Find path start");
Debug.WriteLine("Find path started");
FindPath(location);
Debug.WriteLine("Find path finish");
Debug.WriteLine("Find path finished");
foreach (var node in Path.ToList())
@ -69,19 +79,21 @@ namespace Client.Infrastructure.Service
var reached = await WaitForNodeReaching(cancellationToken, node);
if (!reached)
{
IsBusy = false;
IsLocked = false;
return false;
}
Path.Remove(node);
}
IsBusy = false;
IsLocked = false;
return true;
}, cancellationToken);
}
catch (OperationCanceledException)
{
IsBusy = false;
Debug.WriteLine("Path cancelled");
IsLocked = false;
return true;
}
}
@ -118,13 +130,8 @@ namespace Client.Infrastructure.Service
var hero = worldHandler.Hero;
var start = DateTime.Now;
while (hero != null && !hero.Transform.Position.ApproximatelyEquals(node.To, nodeDistanceTolerance))
while (!token.IsCancellationRequested && 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;
@ -138,7 +145,9 @@ namespace Client.Infrastructure.Service
return false;
}
}
await Task.Delay(25);
token.ThrowIfCancellationRequested();
await Task.Delay(25, token);
}
return true;

View File

@ -44,9 +44,9 @@ namespace Client.Infrastructure.Transports
Debug.WriteLine("Connected to main pipe\n");
}
}
public async Task StartReceiveAsync()
public async Task ReceiveAsync()
{
while (IsConnected())
if (IsConnected())
{
byte[] buffer = new byte[16384 * 2];
int readBytes = await mainPipe!.ReadAsync(buffer, 0, buffer.Length);