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

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