feat: add combat and deleveling AI
This commit is contained in:
25
Client/Domain/AI/Combat/CombatZone.cs
Normal file
25
Client/Domain/AI/Combat/CombatZone.cs
Normal 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);
|
||||
}
|
||||
}
|
111
Client/Domain/AI/Combat/Helper.cs
Normal file
111
Client/Domain/AI/Combat/Helper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
147
Client/Domain/AI/Combat/TransitionBuilder.cs
Normal file
147
Client/Domain/AI/Combat/TransitionBuilder.cs
Normal 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>();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user