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

@@ -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>();
}
}