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

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
}
}