feat: add combat and deleveling AI
This commit is contained in:
102
Client/Domain/AI/AI.cs
Normal file
102
Client/Domain/AI/AI.cs
Normal 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;
|
||||
}
|
||||
}
|
21
Client/Domain/AI/AIInterface.cs
Normal file
21
Client/Domain/AI/AIInterface.cs
Normal 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; }
|
||||
}
|
||||
}
|
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>();
|
||||
}
|
||||
}
|
65
Client/Domain/AI/Config.cs
Normal file
65
Client/Domain/AI/Config.cs
Normal 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();
|
||||
}
|
||||
}
|
86
Client/Domain/AI/Deleveling/TransitionBuilder.cs
Normal file
86
Client/Domain/AI/Deleveling/TransitionBuilder.cs
Normal 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>();
|
||||
}
|
||||
}
|
13
Client/Domain/AI/IO/ConfigDeserializerInterface.cs
Normal file
13
Client/Domain/AI/IO/ConfigDeserializerInterface.cs
Normal 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);
|
||||
}
|
||||
}
|
13
Client/Domain/AI/IO/ConfigSerializerInterface.cs
Normal file
13
Client/Domain/AI/IO/ConfigSerializerInterface.cs
Normal 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);
|
||||
}
|
||||
}
|
15
Client/Domain/AI/State/AnyState.cs
Normal file
15
Client/Domain/AI/State/AnyState.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
28
Client/Domain/AI/State/AttackGuardState.cs
Normal file
28
Client/Domain/AI/State/AttackGuardState.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
72
Client/Domain/AI/State/AttackState.cs
Normal file
72
Client/Domain/AI/State/AttackState.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
85
Client/Domain/AI/State/BaseState.cs
Normal file
85
Client/Domain/AI/State/BaseState.cs
Normal 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;
|
||||
}
|
||||
}
|
22
Client/Domain/AI/State/DeadState.cs
Normal file
22
Client/Domain/AI/State/DeadState.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
28
Client/Domain/AI/State/FindGuardState.cs
Normal file
28
Client/Domain/AI/State/FindGuardState.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
Client/Domain/AI/State/FindTargetState.cs
Normal file
34
Client/Domain/AI/State/FindTargetState.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
Client/Domain/AI/State/IdleState.cs
Normal file
17
Client/Domain/AI/State/IdleState.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
37
Client/Domain/AI/State/MoveToSpotState.cs
Normal file
37
Client/Domain/AI/State/MoveToSpotState.cs
Normal 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
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
34
Client/Domain/AI/State/MoveToTargetState.cs
Normal file
34
Client/Domain/AI/State/MoveToTargetState.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
87
Client/Domain/AI/State/PickupState.cs
Normal file
87
Client/Domain/AI/State/PickupState.cs
Normal 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>();
|
||||
}
|
||||
}
|
42
Client/Domain/AI/State/RestState.cs
Normal file
42
Client/Domain/AI/State/RestState.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
30
Client/Domain/AI/StateBuilder.cs
Normal file
30
Client/Domain/AI/StateBuilder.cs
Normal 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) }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
29
Client/Domain/AI/TransitionBuilderInterface.cs
Normal file
29
Client/Domain/AI/TransitionBuilderInterface.cs
Normal 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();
|
||||
}
|
||||
}
|
29
Client/Domain/AI/TransitionBuilderLocator.cs
Normal file
29
Client/Domain/AI/TransitionBuilderLocator.cs
Normal 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>();
|
||||
}
|
||||
}
|
14
Client/Domain/AI/TypeEnum.cs
Normal file
14
Client/Domain/AI/TypeEnum.cs
Normal 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
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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;
|
||||
|
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
28
Client/Domain/Entities/WeaponItem.cs
Normal file
28
Client/Domain/Entities/WeaponItem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
19
Client/Domain/Enums/CrystalTypeEnum.cs
Normal file
19
Client/Domain/Enums/CrystalTypeEnum.cs
Normal 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
|
||||
}
|
||||
}
|
23
Client/Domain/Enums/WeaponTypeEnum.cs
Normal file
23
Client/Domain/Enums/WeaponTypeEnum.cs
Normal 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
|
||||
}
|
||||
}
|
13
Client/Domain/Helpers/ItemInfo.cs
Normal file
13
Client/Domain/Helpers/ItemInfo.cs
Normal 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; }
|
||||
}
|
||||
}
|
13
Client/Domain/Helpers/ItemInfoHelperInterface.cs
Normal file
13
Client/Domain/Helpers/ItemInfoHelperInterface.cs
Normal 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();
|
||||
}
|
||||
}
|
15
Client/Domain/Helpers/NpcInfo.cs
Normal file
15
Client/Domain/Helpers/NpcInfo.cs
Normal 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; }
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
14
Client/Domain/Helpers/ObjectInfo.cs
Normal file
14
Client/Domain/Helpers/ObjectInfo.cs
Normal 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; } = "";
|
||||
}
|
||||
}
|
13
Client/Domain/Helpers/SkillInfo.cs
Normal file
13
Client/Domain/Helpers/SkillInfo.cs
Normal 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; }
|
||||
}
|
||||
}
|
13
Client/Domain/Helpers/SkillInfoHelperInterface.cs
Normal file
13
Client/Domain/Helpers/SkillInfoHelperInterface.cs
Normal 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();
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user