Shooting NPCs and more (#18042)
* Add pirate shooting * Shooting working * Basics working * Refactor time * More conversion * Update primitives * Update yml * weh * Building again * Draft * weh * b * Start shutdown * Starting to take form * Code side done * is it worky * Fix prototypes * stuff * Shitty working * Juke events working * Even more cleanup * RTX * Fix interaction combat mode and compquery * GetAmmoCount relays * Fix rotation speed * Juke fixes * fixes * weh * The collision avoidance never ends * Fixes * Pause support * framework * lazy * Fix idling * Fix drip * goobed * Fix takeover shutdown bug * Merge fixes * shitter * Fix carpos
@@ -24,9 +24,9 @@ public sealed class NPCTest
|
||||
{
|
||||
var counts = new Dictionary<string, int>();
|
||||
|
||||
foreach (var compound in protoManager.EnumeratePrototypes<HTNCompoundTask>())
|
||||
foreach (var compound in protoManager.EnumeratePrototypes<HTNCompoundPrototype>())
|
||||
{
|
||||
Count(compound, counts, htnSystem);
|
||||
Count(compound, counts, htnSystem, protoManager);
|
||||
counts.Clear();
|
||||
}
|
||||
});
|
||||
@@ -34,13 +34,11 @@ public sealed class NPCTest
|
||||
await pool.CleanReturnAsync();
|
||||
}
|
||||
|
||||
private static void Count(HTNCompoundTask compound, Dictionary<string, int> counts, HTNSystem htnSystem)
|
||||
private static void Count(HTNCompoundPrototype compound, Dictionary<string, int> counts, HTNSystem htnSystem, IPrototypeManager protoManager)
|
||||
{
|
||||
var compoundBranches = htnSystem.CompoundBranches[compound];
|
||||
|
||||
for (var i = 0; i < compound.Branches.Count; i++)
|
||||
foreach (var branch in compound.Branches)
|
||||
{
|
||||
foreach (var task in compoundBranches[i])
|
||||
foreach (var task in branch.Tasks)
|
||||
{
|
||||
if (task is HTNCompoundTask compoundTask)
|
||||
{
|
||||
@@ -49,7 +47,7 @@ public sealed class NPCTest
|
||||
|
||||
Assert.That(count, Is.LessThan(50));
|
||||
counts[compound.ID] = count;
|
||||
Count(compoundTask, counts, htnSystem);
|
||||
Count(protoManager.Index<HTNCompoundPrototype>(compoundTask.Task), counts, htnSystem, protoManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Random;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Sprite;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
|
||||
namespace Content.Server.Dragon;
|
||||
|
||||
@@ -29,6 +31,7 @@ public sealed partial class DragonSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly ISerializationManager _serManager = default!;
|
||||
[Dependency] private readonly ITileDefinitionManager _tileDef = default!;
|
||||
[Dependency] private readonly ChatSystem _chat = default!;
|
||||
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
|
||||
@@ -149,8 +152,18 @@ public sealed partial class DragonSystem : EntitySystem
|
||||
if (comp.SpawnAccumulator > comp.SpawnCooldown)
|
||||
{
|
||||
comp.SpawnAccumulator -= comp.SpawnCooldown;
|
||||
var ent = Spawn(comp.SpawnPrototype, Transform(comp.Owner).MapPosition);
|
||||
_npc.SetBlackboard(ent, NPCBlackboard.FollowTarget, new EntityCoordinates(comp.Owner, Vector2.Zero));
|
||||
var ent = Spawn(comp.SpawnPrototype, Transform(comp.Owner).Coordinates);
|
||||
|
||||
// Update their look to match the leader.
|
||||
if (TryComp<RandomSpriteComponent>(comp.Dragon, out var randomSprite))
|
||||
{
|
||||
var spawnedSprite = EnsureComp<RandomSpriteComponent>(ent);
|
||||
_serManager.CopyTo(randomSprite, ref spawnedSprite, notNullableOverride: true);
|
||||
Dirty(ent, spawnedSprite);
|
||||
}
|
||||
|
||||
if (comp.Dragon != null)
|
||||
_npc.SetBlackboard(ent, NPCBlackboard.FollowTarget, new EntityCoordinates(comp.Dragon.Value, Vector2.Zero));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,10 @@ namespace Content.Server.NPC.Commands
|
||||
}
|
||||
|
||||
var comp = _entities.AddComponent<HTNComponent>(entId);
|
||||
comp.RootTask = args[1];
|
||||
comp.RootTask = new HTNCompoundTask()
|
||||
{
|
||||
Task = args[1]
|
||||
};
|
||||
shell.WriteLine("AI component added.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.NPC.HTN;
|
||||
using Content.Shared.Administration;
|
||||
@@ -13,9 +12,13 @@ namespace Content.Server.NPC.Commands;
|
||||
[AdminCommand(AdminFlags.Debug)]
|
||||
public sealed class NPCDomainCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _sysManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
|
||||
public string Command => "npcdomain";
|
||||
public string Description => "Lists the domain of a particular HTN compound task";
|
||||
public string Help => $"{Command} <htncompoundtask>";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
@@ -24,17 +27,15 @@ public sealed class NPCDomainCommand : IConsoleCommand
|
||||
return;
|
||||
}
|
||||
|
||||
var protoManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
|
||||
if (!protoManager.TryIndex<HTNCompoundTask>(args[0], out var compound))
|
||||
if (!_protoManager.HasIndex<HTNCompoundPrototype>(args[0]))
|
||||
{
|
||||
shell.WriteError($"Unable to find HTN compound task for '{args[0]}'");
|
||||
return;
|
||||
}
|
||||
|
||||
var htnSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<HTNSystem>();
|
||||
var htnSystem = _sysManager.GetEntitySystem<HTNSystem>();
|
||||
|
||||
foreach (var line in htnSystem.GetDomain(compound).Split("\n"))
|
||||
foreach (var line in htnSystem.GetDomain(new HTNCompoundTask {Task = args[0]}).Split("\n"))
|
||||
{
|
||||
shell.WriteLine(line);
|
||||
}
|
||||
@@ -42,11 +43,9 @@ public sealed class NPCDomainCommand : IConsoleCommand
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
var protoManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
|
||||
if (args.Length > 1)
|
||||
return CompletionResult.Empty;
|
||||
|
||||
return CompletionResult.FromHintOptions(protoManager.EnumeratePrototypes<HTNCompoundTask>().Select(o => o.ID), "compound task");
|
||||
return CompletionResult.FromHintOptions(CompletionHelper.PrototypeIDs<HTNCompoundPrototype>(proto: _protoManager), "compound task");
|
||||
}
|
||||
}
|
||||
|
||||
33
Content.Server/NPC/Components/NPCJukeComponent.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Server.NPC.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed class NPCJukeComponent : Component
|
||||
{
|
||||
[DataField("jukeType")]
|
||||
public JukeType JukeType = JukeType.Away;
|
||||
|
||||
[DataField("jukeDuration")]
|
||||
public float JukeDuration = 0.5f;
|
||||
|
||||
[DataField("nextJuke", customTypeSerializer:typeof(TimeOffsetSerializer))]
|
||||
public TimeSpan NextJuke;
|
||||
|
||||
[DataField("targetTile")]
|
||||
public Vector2i? TargetTile;
|
||||
}
|
||||
|
||||
public enum JukeType : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Will move directly away from target if applicable.
|
||||
/// </summary>
|
||||
Away,
|
||||
|
||||
/// <summary>
|
||||
/// Move to the adjacent tile for the specified duration.
|
||||
/// </summary>
|
||||
AdjacentTile
|
||||
}
|
||||
@@ -6,11 +6,6 @@ namespace Content.Server.NPC.Components;
|
||||
[RegisterComponent]
|
||||
public sealed class NPCMeleeCombatComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Weapon we're using to attack the target. Can also be ourselves.
|
||||
/// </summary>
|
||||
[ViewVariables] public EntityUid Weapon;
|
||||
|
||||
/// <summary>
|
||||
/// If the target is moving what is the chance for this NPC to miss.
|
||||
/// </summary>
|
||||
|
||||
@@ -69,6 +69,22 @@ public sealed class NPCSteeringComponent : Component
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public bool Pathfind => PathfindToken != null;
|
||||
|
||||
/// <summary>
|
||||
/// Are we considered arrived if we have line of sight of the target.
|
||||
/// </summary>
|
||||
[DataField("arriveOnLineOfSight")]
|
||||
public bool ArriveOnLineOfSight = false;
|
||||
|
||||
/// <summary>
|
||||
/// How long the target has been in line of sight if applicable.
|
||||
/// </summary>
|
||||
[DataField("lineOfSightTimer")]
|
||||
public float LineOfSightTimer = 0f;
|
||||
|
||||
[DataField("lineOfSightTimeRequired")]
|
||||
public float LineOfSightTimeRequired = 0.5f;
|
||||
|
||||
[ViewVariables] public CancellationTokenSource? PathfindToken = null;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -9,17 +9,6 @@ namespace Content.Server.NPC.Events;
|
||||
[ByRefEvent]
|
||||
public readonly record struct NPCSteeringEvent(
|
||||
NPCSteeringComponent Steering,
|
||||
float[] Interest,
|
||||
float[] Danger,
|
||||
float AgentRadius,
|
||||
Angle OffsetRotation,
|
||||
Vector2 WorldPosition)
|
||||
{
|
||||
public readonly NPCSteeringComponent Steering = Steering;
|
||||
public readonly float[] Interest = Interest;
|
||||
public readonly float[] Danger = Danger;
|
||||
|
||||
public readonly float AgentRadius = AgentRadius;
|
||||
public readonly Angle OffsetRotation = OffsetRotation;
|
||||
public readonly Vector2 WorldPosition = WorldPosition;
|
||||
}
|
||||
TransformComponent Transform,
|
||||
Vector2 WorldPosition,
|
||||
Angle OffsetRotation);
|
||||
|
||||
@@ -15,6 +15,6 @@ public sealed class HTNBranch
|
||||
/// <summary>
|
||||
/// Due to how serv3 works we need to defer getting the actual tasks until after they have all been serialized.
|
||||
/// </summary>
|
||||
[DataField("tasks", required: true, customTypeSerializer:typeof(HTNTaskListSerializer))]
|
||||
public List<string> TaskPrototypes = default!;
|
||||
[DataField("tasks", required: true)]
|
||||
public List<HTNTask> Tasks = new();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Threading;
|
||||
using Content.Server.NPC.Components;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
@@ -11,8 +10,8 @@ public sealed class HTNComponent : NPCComponent
|
||||
/// The base task to use for planning
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite),
|
||||
DataField("rootTask", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<HTNCompoundTask>))]
|
||||
public string RootTask = default!;
|
||||
DataField("rootTask", required: true)]
|
||||
public HTNCompoundTask RootTask = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Check any active services for our current plan. This is used to find new targets for example without changing our plan.
|
||||
|
||||
15
Content.Server/NPC/HTN/HTNCompoundPrototype.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a network of multiple tasks. This gets expanded out to its relevant nodes.
|
||||
/// </summary>
|
||||
[Prototype("htnCompound")]
|
||||
public sealed class HTNCompoundPrototype : IPrototype
|
||||
{
|
||||
[IdDataField] public string ID { get; } = string.Empty;
|
||||
|
||||
[DataField("branches", required: true)]
|
||||
public List<HTNBranch> Branches = new();
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a network of multiple tasks. This gets expanded out to its relevant nodes.
|
||||
/// </summary>
|
||||
[Prototype("htnCompound")]
|
||||
public sealed class HTNCompoundTask : HTNTask
|
||||
/// <remarks>
|
||||
/// This just points to a specific htnCompound prototype
|
||||
/// </remarks>
|
||||
public sealed class HTNCompoundTask : HTNTask, IHTNCompound
|
||||
{
|
||||
/// <summary>
|
||||
/// The available branches for this compound task.
|
||||
/// </summary>
|
||||
[DataField("branches", required: true)]
|
||||
public List<HTNBranch> Branches = default!;
|
||||
[DataField("task", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<HTNCompoundPrototype>))]
|
||||
public string Task = string.Empty;
|
||||
}
|
||||
|
||||
@@ -12,16 +12,19 @@ public sealed class HTNPlan
|
||||
/// </summary>
|
||||
public readonly List<Dictionary<string, object>?> Effects;
|
||||
|
||||
public List<int> BranchTraversalRecord;
|
||||
public readonly List<int> BranchTraversalRecord;
|
||||
|
||||
public List<HTNPrimitiveTask> Tasks;
|
||||
|
||||
public int Index = 0;
|
||||
public readonly List<HTNPrimitiveTask> Tasks;
|
||||
|
||||
public HTNPrimitiveTask CurrentTask => Tasks[Index];
|
||||
|
||||
public HTNOperator CurrentOperator => CurrentTask.Operator;
|
||||
|
||||
/// <summary>
|
||||
/// Where we are up to in the <see cref="Tasks"/>
|
||||
/// </summary>
|
||||
public int Index = 0;
|
||||
|
||||
public HTNPlan(List<HTNPrimitiveTask> tasks, List<int> branchTraversalRecord, List<Dictionary<string, object>?> effects)
|
||||
{
|
||||
Tasks = tasks;
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.CPUJob.JobQueues;
|
||||
using Content.Server.NPC.HTN.PrimitiveTasks;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
@@ -11,10 +12,11 @@ namespace Content.Server.NPC.HTN;
|
||||
/// </summary>
|
||||
public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
{
|
||||
private readonly HTNSystem _htn;
|
||||
private readonly HTNCompoundTask _rootTask;
|
||||
private readonly HTNTask _rootTask;
|
||||
private NPCBlackboard _blackboard;
|
||||
|
||||
private IPrototypeManager _protoManager;
|
||||
|
||||
/// <summary>
|
||||
/// Branch traversal of an existing plan (if applicable).
|
||||
/// </summary>
|
||||
@@ -22,13 +24,13 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
|
||||
public HTNPlanJob(
|
||||
double maxTime,
|
||||
HTNSystem htn,
|
||||
HTNCompoundTask rootTask,
|
||||
IPrototypeManager protoManager,
|
||||
HTNTask rootTask,
|
||||
NPCBlackboard blackboard,
|
||||
List<int>? branchTraversal,
|
||||
CancellationToken cancellationToken = default) : base(maxTime, cancellationToken)
|
||||
{
|
||||
_htn = htn;
|
||||
_protoManager = protoManager;
|
||||
_rootTask = rootTask;
|
||||
_blackboard = blackboard;
|
||||
_branchTraversal = branchTraversal;
|
||||
@@ -47,7 +49,6 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
|
||||
// branch traversal record. Whenever we find a new compound task this updates.
|
||||
var btrIndex = 0;
|
||||
var btr = new List<int>();
|
||||
|
||||
// For some tasks we may do something expensive or want to re-use the planning result.
|
||||
// e.g. pathfind to a target before deciding to attack it.
|
||||
@@ -83,8 +84,6 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
PrimitiveCount = primitiveCount,
|
||||
});
|
||||
|
||||
btr.Add(btrIndex);
|
||||
|
||||
// TODO: Early out if existing plan is better and save lots of time.
|
||||
// my brain is not working rn AAA
|
||||
|
||||
@@ -94,7 +93,7 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
}
|
||||
else
|
||||
{
|
||||
RestoreTolastDecomposedTask(decompHistory, tasksToProcess, appliedStates, finalPlan, ref primitiveCount, ref _blackboard, ref btrIndex, ref btr);
|
||||
RestoreTolastDecomposedTask(decompHistory, tasksToProcess, appliedStates, finalPlan, ref primitiveCount, ref _blackboard, ref btrIndex);
|
||||
}
|
||||
break;
|
||||
case HTNPrimitiveTask primitive:
|
||||
@@ -105,7 +104,7 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
}
|
||||
else
|
||||
{
|
||||
RestoreTolastDecomposedTask(decompHistory, tasksToProcess, appliedStates, finalPlan, ref primitiveCount, ref _blackboard, ref btrIndex, ref btr);
|
||||
RestoreTolastDecomposedTask(decompHistory, tasksToProcess, appliedStates, finalPlan, ref primitiveCount, ref _blackboard, ref btrIndex);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -157,9 +156,9 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
/// <summary>
|
||||
/// Goes through each compound task branch and tries to find an appropriate one.
|
||||
/// </summary>
|
||||
private bool TryFindSatisfiedMethod(HTNCompoundTask compound, Queue<HTNTask> tasksToProcess, NPCBlackboard blackboard, ref int mtrIndex)
|
||||
private bool TryFindSatisfiedMethod(HTNCompoundTask compoundId, Queue<HTNTask> tasksToProcess, NPCBlackboard blackboard, ref int mtrIndex)
|
||||
{
|
||||
var compBranches = _htn.CompoundBranches[compound];
|
||||
var compound = _protoManager.Index<HTNCompoundPrototype>(compoundId.Task);
|
||||
|
||||
for (var i = mtrIndex; i < compound.Branches.Count; i++)
|
||||
{
|
||||
@@ -178,9 +177,7 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
if (!isValid)
|
||||
continue;
|
||||
|
||||
var branchTasks = compBranches[i];
|
||||
|
||||
foreach (var task in branchTasks)
|
||||
foreach (var task in branch.Tasks)
|
||||
{
|
||||
tasksToProcess.Enqueue(task);
|
||||
}
|
||||
@@ -201,8 +198,7 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
List<HTNPrimitiveTask> finalPlan,
|
||||
ref int primitiveCount,
|
||||
ref NPCBlackboard blackboard,
|
||||
ref int mtrIndex,
|
||||
ref List<int> btr)
|
||||
ref int mtrIndex)
|
||||
{
|
||||
tasksToProcess.Clear();
|
||||
|
||||
@@ -214,11 +210,11 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
mtrIndex = lastDecomp.BranchTraversal + 1;
|
||||
|
||||
var count = finalPlan.Count;
|
||||
var reduction = count - primitiveCount;
|
||||
|
||||
// Final plan only has primitive tasks added to it so we can just remove the count we've tracked since the last decomp.
|
||||
finalPlan.RemoveRange(count - primitiveCount, primitiveCount);
|
||||
appliedStates.RemoveRange(count - primitiveCount, primitiveCount);
|
||||
btr.RemoveRange(count - primitiveCount, primitiveCount);
|
||||
finalPlan.RemoveRange(reduction, primitiveCount);
|
||||
appliedStates.RemoveRange(reduction, primitiveCount);
|
||||
|
||||
primitiveCount = lastDecomp.PrimitiveCount;
|
||||
blackboard = lastDecomp.Blackboard;
|
||||
@@ -241,7 +237,7 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
||||
public int PrimitiveCount;
|
||||
|
||||
/// <summary>
|
||||
/// The compound task that owns this decomposition.
|
||||
/// The task that owns this decomposition.
|
||||
/// </summary>
|
||||
public HTNCompoundTask CompoundTask = default!;
|
||||
|
||||
|
||||
9
Content.Server/NPC/HTN/HTNPlanState.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
[Flags]
|
||||
public enum HTNPlanState : byte
|
||||
{
|
||||
TaskFinished = 1 << 0,
|
||||
|
||||
PlanFinished = 1 << 1,
|
||||
}
|
||||
@@ -13,8 +13,7 @@ using JetBrains.Annotations;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
@@ -25,20 +24,14 @@ public sealed class HTNSystem : EntitySystem
|
||||
[Dependency] private readonly NPCSystem _npc = default!;
|
||||
[Dependency] private readonly NPCUtilitySystem _utility = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
private readonly JobQueue _planQueue = new(0.004);
|
||||
|
||||
private readonly HashSet<ICommonSession> _subscribers = new();
|
||||
|
||||
// hngngghghgh
|
||||
public IReadOnlyDictionary<HTNCompoundTask, List<HTNTask>[]> CompoundBranches => _compoundBranches;
|
||||
private Dictionary<HTNCompoundTask, List<HTNTask>[]> _compoundBranches = new();
|
||||
|
||||
// Hierarchical Task Network
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_sawmill = Logger.GetSawmill("npc.htn");
|
||||
SubscribeLocalEvent<HTNComponent, ComponentShutdown>(OnHTNShutdown);
|
||||
SubscribeNetworkEvent<RequestHTNMessage>(OnHTNMessage);
|
||||
|
||||
@@ -69,7 +62,9 @@ public sealed class HTNSystem : EntitySystem
|
||||
private void OnLoad()
|
||||
{
|
||||
// Clear all NPCs in case they're hanging onto stale tasks
|
||||
foreach (var comp in EntityQuery<HTNComponent>(true))
|
||||
var query = AllEntityQuery<HTNComponent>();
|
||||
|
||||
while (query.MoveNext(out var comp))
|
||||
{
|
||||
comp.PlanningToken?.Cancel();
|
||||
comp.PlanningToken = null;
|
||||
@@ -77,73 +72,64 @@ public sealed class HTNSystem : EntitySystem
|
||||
if (comp.Plan != null)
|
||||
{
|
||||
var currentOperator = comp.Plan.CurrentOperator;
|
||||
currentOperator.Shutdown(comp.Blackboard, HTNOperatorStatus.Failed);
|
||||
ShutdownTask(currentOperator, comp.Blackboard, HTNOperatorStatus.Failed);
|
||||
ShutdownPlan(comp);
|
||||
comp.Plan = null;
|
||||
RequestPlan(comp);
|
||||
}
|
||||
}
|
||||
|
||||
_compoundBranches.Clear();
|
||||
|
||||
// Add dependencies for all operators.
|
||||
// We put code on operators as I couldn't think of a clean way to put it on systems.
|
||||
foreach (var compound in _prototypeManager.EnumeratePrototypes<HTNCompoundTask>())
|
||||
foreach (var compound in _prototypeManager.EnumeratePrototypes<HTNCompoundPrototype>())
|
||||
{
|
||||
UpdateCompound(compound);
|
||||
}
|
||||
|
||||
foreach (var primitive in _prototypeManager.EnumeratePrototypes<HTNPrimitiveTask>())
|
||||
{
|
||||
UpdatePrimitive(primitive);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPrototypeLoad(PrototypesReloadedEventArgs obj)
|
||||
{
|
||||
if (!obj.ByType.ContainsKey(typeof(HTNCompoundPrototype)))
|
||||
return;
|
||||
|
||||
OnLoad();
|
||||
}
|
||||
|
||||
private void UpdatePrimitive(HTNPrimitiveTask primitive)
|
||||
private void UpdateCompound(HTNCompoundPrototype compound)
|
||||
{
|
||||
for (var i = 0; i < compound.Branches.Count; i++)
|
||||
{
|
||||
var branch = compound.Branches[i];
|
||||
|
||||
foreach (var precon in branch.Preconditions)
|
||||
{
|
||||
precon.Initialize(EntityManager.EntitySysManager);
|
||||
}
|
||||
|
||||
foreach (var task in branch.Tasks)
|
||||
{
|
||||
UpdateTask(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTask(HTNTask task)
|
||||
{
|
||||
switch (task)
|
||||
{
|
||||
case HTNCompoundTask:
|
||||
// NOOP, handled elsewhere
|
||||
break;
|
||||
case HTNPrimitiveTask primitive:
|
||||
foreach (var precon in primitive.Preconditions)
|
||||
{
|
||||
precon.Initialize(EntityManager.EntitySysManager);
|
||||
}
|
||||
|
||||
primitive.Operator.Initialize(EntityManager.EntitySysManager);
|
||||
}
|
||||
|
||||
private void UpdateCompound(HTNCompoundTask compound)
|
||||
{
|
||||
var branchies = new List<HTNTask>[compound.Branches.Count];
|
||||
_compoundBranches.Add(compound, branchies);
|
||||
|
||||
for (var i = 0; i < compound.Branches.Count; i++)
|
||||
{
|
||||
var branch = compound.Branches[i];
|
||||
var brancho = new List<HTNTask>(branch.TaskPrototypes.Count);
|
||||
branchies[i] = brancho;
|
||||
|
||||
// Didn't do this in a typeserializer because we can't recursively grab our own prototype during it, woohoo!
|
||||
foreach (var proto in branch.TaskPrototypes)
|
||||
{
|
||||
if (_prototypeManager.TryIndex<HTNCompoundTask>(proto, out var compTask))
|
||||
{
|
||||
brancho.Add(compTask);
|
||||
}
|
||||
else if (_prototypeManager.TryIndex<HTNPrimitiveTask>(proto, out var primTask))
|
||||
{
|
||||
brancho.Add(primTask);
|
||||
}
|
||||
else
|
||||
{
|
||||
_sawmill.Error($"Unable to find HTNTask for {proto} on {compound.ID}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var precon in branch.Preconditions)
|
||||
{
|
||||
precon.Initialize(EntityManager.EntitySysManager);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +163,7 @@ public sealed class HTNSystem : EntitySystem
|
||||
{
|
||||
if (comp.PlanningJob.Exception != null)
|
||||
{
|
||||
_sawmill.Fatal($"Received exception on planning job for {uid}!");
|
||||
Log.Fatal($"Received exception on planning job for {uid}!");
|
||||
_npc.SleepNPC(uid);
|
||||
var exc = comp.PlanningJob.Exception;
|
||||
RemComp<HTNComponent>(uid);
|
||||
@@ -209,7 +195,13 @@ public sealed class HTNSystem : EntitySystem
|
||||
if (comp.Plan == null || newPlanBetter)
|
||||
{
|
||||
comp.CheckServices = false;
|
||||
comp.Plan?.CurrentTask.Operator.Shutdown(comp.Blackboard, HTNOperatorStatus.BetterPlan);
|
||||
|
||||
if (comp.Plan != null)
|
||||
{
|
||||
ShutdownTask(comp.Plan.CurrentOperator, comp.Blackboard, HTNOperatorStatus.BetterPlan);
|
||||
ShutdownPlan(comp);
|
||||
}
|
||||
|
||||
comp.Plan = comp.PlanningJob.Result;
|
||||
|
||||
// Startup the first task and anything else we need to do.
|
||||
@@ -227,7 +219,7 @@ public sealed class HTNSystem : EntitySystem
|
||||
{
|
||||
text.AppendLine($"BTR: {string.Join(", ", comp.Plan.BranchTraversalRecord)}");
|
||||
text.AppendLine($"tasks:");
|
||||
var root = _prototypeManager.Index<HTNCompoundTask>(comp.RootTask);
|
||||
var root = comp.RootTask;
|
||||
var btr = new List<int>();
|
||||
var level = -1;
|
||||
AppendDebugText(root, text, comp.Plan.BranchTraversalRecord, btr, ref level);
|
||||
@@ -267,23 +259,24 @@ public sealed class HTNSystem : EntitySystem
|
||||
|
||||
if (task is HTNPrimitiveTask primitive)
|
||||
{
|
||||
text.AppendLine(primitive.ID);
|
||||
text.AppendLine(primitive.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (task is HTNCompoundTask compound)
|
||||
if (task is HTNCompoundTask compTask)
|
||||
{
|
||||
var compound = _prototypeManager.Index<HTNCompoundPrototype>(compTask.Task);
|
||||
level++;
|
||||
text.AppendLine(compound.ID);
|
||||
var branches = _compoundBranches[compound];
|
||||
var branches = compound.Branches;
|
||||
|
||||
for (var i = 0; i < branches.Length; i++)
|
||||
for (var i = 0; i < branches.Count; i++)
|
||||
{
|
||||
var branch = branches[i];
|
||||
btr.Add(i);
|
||||
text.AppendLine($" branch {string.Join(", ", btr)}:");
|
||||
|
||||
foreach (var sub in branch)
|
||||
foreach (var sub in branch.Tasks)
|
||||
{
|
||||
AppendDebugText(sub, text, planBtr, btr, ref level);
|
||||
}
|
||||
@@ -344,21 +337,22 @@ public sealed class HTNSystem : EntitySystem
|
||||
case HTNOperatorStatus.Continuing:
|
||||
break;
|
||||
case HTNOperatorStatus.Failed:
|
||||
currentOperator.Shutdown(blackboard, status);
|
||||
component.Plan = null;
|
||||
ShutdownTask(currentOperator, blackboard, status);
|
||||
ShutdownPlan(component);
|
||||
break;
|
||||
// Operator completed so go to the next one.
|
||||
case HTNOperatorStatus.Finished:
|
||||
currentOperator.Shutdown(blackboard, status);
|
||||
ShutdownTask(currentOperator, blackboard, status);
|
||||
component.Plan.Index++;
|
||||
|
||||
// Plan finished!
|
||||
if (component.Plan.Tasks.Count <= component.Plan.Index)
|
||||
{
|
||||
component.Plan = null;
|
||||
ShutdownPlan(component);
|
||||
break;
|
||||
}
|
||||
|
||||
ConditionalShutdown(component.Plan, currentOperator, blackboard, HTNPlanState.TaskFinished);
|
||||
StartupTask(component.Plan.Tasks[component.Plan.Index], component.Blackboard, component.Plan.Effects[component.Plan.Index]);
|
||||
break;
|
||||
default:
|
||||
@@ -367,6 +361,50 @@ public sealed class HTNSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
public void ShutdownTask(HTNOperator currentOperator, NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
{
|
||||
if (currentOperator is IHtnConditionalShutdown conditional &&
|
||||
(conditional.ShutdownState & HTNPlanState.TaskFinished) != 0x0)
|
||||
{
|
||||
conditional.ConditionalShutdown(blackboard);
|
||||
}
|
||||
|
||||
currentOperator.TaskShutdown(blackboard, status);
|
||||
}
|
||||
|
||||
public void ShutdownPlan(HTNComponent component)
|
||||
{
|
||||
DebugTools.Assert(component.Plan != null);
|
||||
var blackboard = component.Blackboard;
|
||||
|
||||
foreach (var task in component.Plan.Tasks)
|
||||
{
|
||||
if (task.Operator is IHtnConditionalShutdown conditional &&
|
||||
(conditional.ShutdownState & HTNPlanState.PlanFinished) != 0x0)
|
||||
{
|
||||
conditional.ConditionalShutdown(blackboard);
|
||||
}
|
||||
|
||||
task.Operator.PlanShutdown(component.Blackboard);
|
||||
}
|
||||
|
||||
component.Plan = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shuts down the current operator conditionally.
|
||||
/// </summary>
|
||||
private void ConditionalShutdown(HTNPlan plan, HTNOperator currentOperator, NPCBlackboard blackboard, HTNPlanState state)
|
||||
{
|
||||
if (currentOperator is not IHtnConditionalShutdown conditional)
|
||||
return;
|
||||
|
||||
if ((conditional.ShutdownState & state) == 0x0)
|
||||
return;
|
||||
|
||||
conditional.ConditionalShutdown(blackboard);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a new primitive task. Will apply effects from planning if applicable.
|
||||
/// </summary>
|
||||
@@ -400,8 +438,8 @@ public sealed class HTNSystem : EntitySystem
|
||||
|
||||
var job = new HTNPlanJob(
|
||||
0.02,
|
||||
this,
|
||||
_prototypeManager.Index<HTNCompoundTask>(component.RootTask),
|
||||
_prototypeManager,
|
||||
component.RootTask,
|
||||
component.Blackboard.ShallowClone(), branchTraversal, cancelToken.Token);
|
||||
|
||||
_planQueue.EnqueueJob(job);
|
||||
@@ -425,13 +463,13 @@ public sealed class HTNSystem : EntitySystem
|
||||
|
||||
if (task is HTNPrimitiveTask primitive)
|
||||
{
|
||||
builder.AppendLine(buffer + $"Primitive: {task.ID}");
|
||||
builder.AppendLine(buffer + $"Primitive: {task}");
|
||||
builder.AppendLine(buffer + $" operator: {primitive.Operator.GetType().Name}");
|
||||
}
|
||||
else if (task is HTNCompoundTask compound)
|
||||
else if (task is HTNCompoundTask compTask)
|
||||
{
|
||||
builder.AppendLine(buffer + $"Compound: {task.ID}");
|
||||
var compoundBranches = CompoundBranches[compound];
|
||||
var compound = _prototypeManager.Index<HTNCompoundPrototype>(compTask.Task);
|
||||
builder.AppendLine(buffer + $"Compound: {task}");
|
||||
|
||||
for (var i = 0; i < compound.Branches.Count; i++)
|
||||
{
|
||||
@@ -439,9 +477,8 @@ public sealed class HTNSystem : EntitySystem
|
||||
|
||||
builder.AppendLine(buffer + " branch:");
|
||||
indent++;
|
||||
var branchTasks = compoundBranches[i];
|
||||
|
||||
foreach (var branchTask in branchTasks)
|
||||
foreach (var branchTask in branch.Tasks)
|
||||
{
|
||||
AppendDomain(builder, branchTask, ref indent);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
public abstract class HTNTask : IPrototype
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
public abstract class HTNTask
|
||||
{
|
||||
[IdDataField] public string ID { get; } = default!;
|
||||
}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
using Content.Server.NPC.HTN.PrimitiveTasks;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using Robust.Shared.Serialization.Markdown.Sequence;
|
||||
using Robust.Shared.Serialization.Markdown.Validation;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
public sealed class HTNTaskListSerializer : ITypeSerializer<List<string>, SequenceDataNode>
|
||||
{
|
||||
public ValidationNode Validate(ISerializationManager serializationManager, SequenceDataNode node,
|
||||
IDependencyCollection dependencies, ISerializationContext? context = null)
|
||||
{
|
||||
var list = new List<ValidationNode>();
|
||||
var protoManager = dependencies.Resolve<IPrototypeManager>();
|
||||
|
||||
foreach (var data in node.Sequence)
|
||||
{
|
||||
if (data is not MappingDataNode mapping)
|
||||
{
|
||||
list.Add(new ErrorNode(data, $"Found invalid mapping node on {data}"));
|
||||
continue;
|
||||
}
|
||||
|
||||
var id = ((ValueDataNode) mapping["id"]).Value;
|
||||
|
||||
var isCompound = protoManager.HasIndex<HTNCompoundTask>(id);
|
||||
var isPrimitive = protoManager.HasIndex<HTNPrimitiveTask>(id);
|
||||
|
||||
list.Add(isCompound ^ isPrimitive
|
||||
? new ValidatedValueNode(node)
|
||||
: new ErrorNode(node, $"Found duplicated HTN compound and primitive tasks for {id}"));
|
||||
}
|
||||
|
||||
return new ValidatedSequenceNode(list);
|
||||
}
|
||||
|
||||
public List<string> Read(ISerializationManager serializationManager, SequenceDataNode node,
|
||||
IDependencyCollection dependencies,
|
||||
SerializationHookContext hookCtx, ISerializationContext? context = null,
|
||||
ISerializationManager.InstantiationDelegate<List<string>>? instanceProvider = null)
|
||||
{
|
||||
var value = instanceProvider != null ? instanceProvider() : new List<string>();
|
||||
foreach (var data in node.Sequence)
|
||||
{
|
||||
var mapping = (MappingDataNode) data;
|
||||
var id = ((ValueDataNode) mapping["id"]).Value;
|
||||
// Can't check prototypes here because we're still loading them so yay!
|
||||
value.Add(id);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public DataNode Write(ISerializationManager serializationManager, List<string> value,
|
||||
IDependencyCollection dependencies, bool alwaysWrite = false,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
var sequence = new SequenceDataNode();
|
||||
|
||||
foreach (var task in value)
|
||||
{
|
||||
var mapping = new MappingDataNode
|
||||
{
|
||||
["id"] = new ValueDataNode(task)
|
||||
};
|
||||
|
||||
sequence.Add(mapping);
|
||||
}
|
||||
|
||||
return sequence;
|
||||
}
|
||||
}
|
||||
8
Content.Server/NPC/HTN/IHTNCompound.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a HTN task that can be decomposed into primitive tasks.
|
||||
/// </summary>
|
||||
public interface IHTNCompound
|
||||
{
|
||||
}
|
||||
17
Content.Server/NPC/HTN/IHtnConditionalShutdown.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Content.Server.NPC.HTN;
|
||||
|
||||
/// <summary>
|
||||
/// Helper interface to run the appropriate shutdown for a particular task.
|
||||
/// </summary>
|
||||
public interface IHtnConditionalShutdown
|
||||
{
|
||||
/// <summary>
|
||||
/// When to shut the task down.
|
||||
/// </summary>
|
||||
HTNPlanState ShutdownState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Run whenever the <see cref="ShutdownState"/> specifies.
|
||||
/// </summary>
|
||||
void ConditionalShutdown(NPCBlackboard blackboard);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Content.Shared.Hands.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.NPC.HTN.Preconditions;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the active hand entity has the specified components.
|
||||
/// </summary>
|
||||
public sealed class ActiveHandComponentPrecondition : HTNPrecondition
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField("invert")]
|
||||
public bool Invert;
|
||||
|
||||
[DataField("components", required: true)]
|
||||
public ComponentRegistry Components = new();
|
||||
|
||||
public override bool IsMet(NPCBlackboard blackboard)
|
||||
{
|
||||
if (!blackboard.TryGetValue<Hand>(NPCBlackboard.ActiveHand, out var hand, _entManager) || hand.HeldEntity == null)
|
||||
{
|
||||
return Invert;
|
||||
}
|
||||
|
||||
foreach (var comp in Components)
|
||||
{
|
||||
var hasComp = _entManager.HasComponent(hand.HeldEntity, comp.Value.Component.GetType());
|
||||
|
||||
if (!hasComp ||
|
||||
Invert && hasComp)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Content.Shared.Hands.Components;
|
||||
|
||||
namespace Content.Server.NPC.HTN.Preconditions;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if an entity is held in the active hand.
|
||||
/// </summary>
|
||||
public sealed class ActiveHandEntityPrecondition : HTNPrecondition
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public override bool IsMet(NPCBlackboard blackboard)
|
||||
{
|
||||
if (!blackboard.TryGetValue(NPCBlackboard.ActiveHand, out Hand? activeHand, _entManager))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return activeHand.HeldEntity != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Content.Shared.Hands.Components;
|
||||
|
||||
namespace Content.Server.NPC.HTN.Preconditions;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the active hand is unoccupied.
|
||||
/// </summary>
|
||||
public sealed class ActiveHandFreePrecondition : HTNPrecondition
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public override bool IsMet(NPCBlackboard blackboard)
|
||||
{
|
||||
return blackboard.TryGetValue<bool>(NPCBlackboard.ActiveHandFree, out var handFree, _entManager) && handFree;
|
||||
}
|
||||
}
|
||||
48
Content.Server/NPC/HTN/Preconditions/GunAmmoPrecondition.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Content.Server.Weapons.Ranged.Systems;
|
||||
using Content.Shared.Weapons.Ranged.Events;
|
||||
|
||||
namespace Content.Server.NPC.HTN.Preconditions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets ammo for this NPC's selected gun; either active hand or itself.
|
||||
/// </summary>
|
||||
public sealed class GunAmmoPrecondition : HTNPrecondition
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField("minPercent")]
|
||||
public float MinPercent = 0f;
|
||||
|
||||
[DataField("maxPercent")]
|
||||
public float MaxPercent = 1f;
|
||||
|
||||
public override bool IsMet(NPCBlackboard blackboard)
|
||||
{
|
||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||
var gunSystem = _entManager.System<GunSystem>();
|
||||
|
||||
if (!gunSystem.TryGetGun(owner, out var gunUid, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ammoEv = new GetAmmoCountEvent();
|
||||
_entManager.EventBus.RaiseLocalEvent(gunUid, ref ammoEv);
|
||||
float percent;
|
||||
|
||||
if (ammoEv.Capacity == 0)
|
||||
percent = 0f;
|
||||
else
|
||||
percent = ammoEv.Count / (float) ammoEv.Capacity;
|
||||
|
||||
percent = Math.Clamp(percent, 0f, 1f);
|
||||
|
||||
if (MaxPercent < percent)
|
||||
return false;
|
||||
|
||||
if (MinPercent > percent)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ public sealed class TargetInLOSPrecondition : HTNPrecondition
|
||||
private InteractionSystem _interaction = default!;
|
||||
|
||||
[DataField("targetKey")]
|
||||
public string TargetKey = "CombatTarget";
|
||||
public string TargetKey = "Target";
|
||||
|
||||
[DataField("rangeKey")]
|
||||
public string RangeKey = "RangeKey";
|
||||
|
||||
@@ -40,6 +40,14 @@ public abstract class HTNOperator
|
||||
return HTNOperatorStatus.Finished;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the plan has finished running.
|
||||
/// </summary>
|
||||
public virtual void PlanShutdown(NPCBlackboard blackboard)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called the first time an operator runs.
|
||||
/// </summary>
|
||||
@@ -48,5 +56,5 @@ public abstract class HTNOperator
|
||||
/// <summary>
|
||||
/// Called whenever the operator stops running.
|
||||
/// </summary>
|
||||
public virtual void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status) {}
|
||||
public virtual void TaskShutdown(NPCBlackboard blackboard, HTNOperatorStatus status) {}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks;
|
||||
|
||||
[Prototype("htnPrimitive")]
|
||||
public sealed class HTNPrimitiveTask : HTNTask
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using Content.Server.NPC.Components;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat;
|
||||
|
||||
public sealed class JukeOperator : HTNOperator, IHtnConditionalShutdown
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField("jukeType")]
|
||||
public JukeType JukeType = JukeType.AdjacentTile;
|
||||
|
||||
[DataField("shutdownState")]
|
||||
public HTNPlanState ShutdownState { get; } = HTNPlanState.PlanFinished;
|
||||
|
||||
public override void Startup(NPCBlackboard blackboard)
|
||||
{
|
||||
base.Startup(blackboard);
|
||||
var juke = _entManager.EnsureComponent<NPCJukeComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
|
||||
juke.JukeType = JukeType;
|
||||
}
|
||||
|
||||
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
|
||||
{
|
||||
return HTNOperatorStatus.Finished;
|
||||
}
|
||||
|
||||
public void ConditionalShutdown(NPCBlackboard blackboard)
|
||||
{
|
||||
_entManager.RemoveComponent<NPCJukeComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,25 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.NPC.Components;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Melee;
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat.Melee;
|
||||
|
||||
/// <summary>
|
||||
/// Attacks the specified key in melee combat.
|
||||
/// </summary>
|
||||
public sealed class MeleeOperator : HTNOperator
|
||||
public sealed class MeleeOperator : HTNOperator, IHtnConditionalShutdown
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
/// <summary>
|
||||
/// When to shut the task down.
|
||||
/// </summary>
|
||||
[DataField("shutdownState")]
|
||||
public HTNPlanState ShutdownState { get; } = HTNPlanState.TaskFinished;
|
||||
|
||||
/// <summary>
|
||||
/// Key that contains the target entity.
|
||||
/// </summary>
|
||||
@@ -53,10 +60,11 @@ public sealed class MeleeOperator : HTNOperator
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
public void ConditionalShutdown(NPCBlackboard blackboard)
|
||||
{
|
||||
base.Shutdown(blackboard, status);
|
||||
_entManager.RemoveComponent<NPCMeleeCombatComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
|
||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||
_entManager.System<SharedCombatModeSystem>().SetInCombatMode(owner, false);
|
||||
_entManager.RemoveComponent<NPCMeleeCombatComponent>(owner);
|
||||
blackboard.Remove<EntityUid>(TargetKey);
|
||||
}
|
||||
|
||||
@@ -96,9 +104,10 @@ public sealed class MeleeOperator : HTNOperator
|
||||
status = HTNOperatorStatus.Failed;
|
||||
}
|
||||
|
||||
if (status != HTNOperatorStatus.Continuing)
|
||||
// Mark it as finished to continue the plan.
|
||||
if (status == HTNOperatorStatus.Continuing && ShutdownState == HTNPlanState.PlanFinished)
|
||||
{
|
||||
_entManager.RemoveComponent<NPCMeleeCombatComponent>(owner);
|
||||
status = HTNOperatorStatus.Finished;
|
||||
}
|
||||
|
||||
return status;
|
||||
@@ -1,16 +1,20 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.NPC.Components;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Ranged;
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat.Ranged;
|
||||
|
||||
public sealed class RangedOperator : HTNOperator
|
||||
public sealed class GunOperator : HTNOperator, IHtnConditionalShutdown
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField("shutdownState")]
|
||||
public HTNPlanState ShutdownState { get; } = HTNPlanState.TaskFinished;
|
||||
|
||||
/// <summary>
|
||||
/// Key that contains the target entity.
|
||||
/// </summary>
|
||||
@@ -23,6 +27,12 @@ public sealed class RangedOperator : HTNOperator
|
||||
[DataField("targetState")]
|
||||
public MobState TargetState = MobState.Alive;
|
||||
|
||||
/// <summary>
|
||||
/// Do we require line of sight of the target before failing.
|
||||
/// </summary>
|
||||
[DataField("requireLOS")]
|
||||
public bool RequireLOS = false;
|
||||
|
||||
// Like movement we add a component and pass it off to the dedicated system.
|
||||
|
||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||
@@ -60,10 +70,11 @@ public sealed class RangedOperator : HTNOperator
|
||||
}
|
||||
}
|
||||
|
||||
public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
public void ConditionalShutdown(NPCBlackboard blackboard)
|
||||
{
|
||||
base.Shutdown(blackboard, status);
|
||||
_entManager.RemoveComponent<NPCRangedCombatComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
|
||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||
_entManager.System<SharedCombatModeSystem>().SetInCombatMode(owner, false);
|
||||
_entManager.RemoveComponent<NPCRangedCombatComponent>(owner);
|
||||
blackboard.Remove<EntityUid>(TargetKey);
|
||||
}
|
||||
|
||||
@@ -89,9 +100,14 @@ public sealed class RangedOperator : HTNOperator
|
||||
switch (combat.Status)
|
||||
{
|
||||
case CombatStatus.TargetUnreachable:
|
||||
case CombatStatus.NotInSight:
|
||||
status = HTNOperatorStatus.Failed;
|
||||
break;
|
||||
case CombatStatus.NotInSight:
|
||||
if (RequireLOS)
|
||||
status = HTNOperatorStatus.Failed;
|
||||
else
|
||||
status = HTNOperatorStatus.Continuing;
|
||||
break;
|
||||
case CombatStatus.Normal:
|
||||
status = HTNOperatorStatus.Continuing;
|
||||
break;
|
||||
@@ -106,9 +122,10 @@ public sealed class RangedOperator : HTNOperator
|
||||
status = HTNOperatorStatus.Failed;
|
||||
}
|
||||
|
||||
if (status != HTNOperatorStatus.Continuing)
|
||||
// Mark it as finished to continue the plan.
|
||||
if (status == HTNOperatorStatus.Continuing && ShutdownState == HTNPlanState.PlanFinished)
|
||||
{
|
||||
_entManager.RemoveComponent<NPCRangedCombatComponent>(owner);
|
||||
status = HTNOperatorStatus.Finished;
|
||||
}
|
||||
|
||||
return status;
|
||||
@@ -4,14 +4,14 @@ using System.Threading.Tasks;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Interaction;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Interactions;
|
||||
|
||||
public sealed class AltInteractOperator : HTNOperator
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField("targetKey")]
|
||||
public string Key = "CombatTarget";
|
||||
public string Key = "Target";
|
||||
|
||||
/// <summary>
|
||||
/// If this alt-interaction started a do_after where does the key get stored.
|
||||
@@ -0,0 +1,31 @@
|
||||
using Content.Server.Hands.Systems;
|
||||
using Content.Shared.Hands.Components;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Interactions;
|
||||
|
||||
/// <summary>
|
||||
/// Drops the active hand entity underneath us.
|
||||
/// </summary>
|
||||
public sealed class DropOperator : HTNOperator
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
|
||||
{
|
||||
if (!blackboard.TryGetValue(NPCBlackboard.ActiveHand, out Hand? activeHand, _entManager))
|
||||
{
|
||||
return HTNOperatorStatus.Finished;
|
||||
}
|
||||
|
||||
var owner = blackboard.GetValueOrDefault<EntityUid>(NPCBlackboard.Owner, _entManager);
|
||||
// TODO: Need some sort of interaction cooldown probably.
|
||||
var handsSystem = _entManager.System<HandsSystem>();
|
||||
|
||||
if (handsSystem.TryDrop(owner))
|
||||
{
|
||||
return HTNOperatorStatus.Finished;
|
||||
}
|
||||
|
||||
return HTNOperatorStatus.Failed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Content.Server.Hands.Systems;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Interactions;
|
||||
|
||||
public sealed class EquipOperator : HTNOperator
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField("target")]
|
||||
public string Target = "Target";
|
||||
|
||||
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
|
||||
{
|
||||
if (!blackboard.TryGetValue<EntityUid>(Target, out var target, _entManager))
|
||||
{
|
||||
return HTNOperatorStatus.Failed;
|
||||
}
|
||||
|
||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||
var handsSystem = _entManager.System<HandsSystem>();
|
||||
|
||||
// TODO: As elsewhere need some generic interaction cooldown system
|
||||
if (handsSystem.TryPickup(owner, target))
|
||||
{
|
||||
return HTNOperatorStatus.Finished;
|
||||
}
|
||||
|
||||
return HTNOperatorStatus.Failed;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
using Content.Server.Interaction;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Timing;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Interactions;
|
||||
|
||||
public sealed class InteractWithOperator : HTNOperator
|
||||
{
|
||||
@@ -24,6 +25,7 @@ public sealed class InteractWithOperator : HTNOperator
|
||||
return HTNOperatorStatus.Continuing;
|
||||
}
|
||||
|
||||
_entManager.System<SharedCombatModeSystem>().SetInCombatMode(owner, false);
|
||||
_entManager.System<InteractionSystem>().UserInteraction(owner, targetXform.Coordinates, moveTarget);
|
||||
|
||||
return HTNOperatorStatus.Finished;
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Hands.Systems;
|
||||
using Content.Shared.Hands.Components;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Interactions;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Swaps to any free hand.
|
||||
/// </summary>
|
||||
public sealed class SwapToFreeHandOperator : HTNOperator
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard, CancellationToken cancelToken)
|
||||
{
|
||||
if (!blackboard.TryGetValue<List<string>>(NPCBlackboard.FreeHands, out var hands, _entManager) ||
|
||||
!_entManager.TryGetComponent<HandsComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner), out var handsComp))
|
||||
{
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
foreach (var hand in hands)
|
||||
{
|
||||
return (true, new Dictionary<string, object>()
|
||||
{
|
||||
{
|
||||
NPCBlackboard.ActiveHand, handsComp.Hands[hand]
|
||||
},
|
||||
{
|
||||
NPCBlackboard.ActiveHandFree, true
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
|
||||
{
|
||||
// TODO: Need interaction cooldown
|
||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||
var handSystem = _entManager.System<HandsSystem>();
|
||||
|
||||
if (!handSystem.TrySelectEmptyHand(owner))
|
||||
{
|
||||
return HTNOperatorStatus.Failed;
|
||||
}
|
||||
|
||||
return HTNOperatorStatus.Finished;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
||||
/// <summary>
|
||||
/// Moves an NPC to the specified target key. Hands the actual steering off to NPCSystem.Steering
|
||||
/// </summary>
|
||||
public sealed class MoveToOperator : HTNOperator
|
||||
public sealed class MoveToOperator : HTNOperator, IHtnConditionalShutdown
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
@@ -19,6 +19,12 @@ public sealed class MoveToOperator : HTNOperator
|
||||
private PathfindingSystem _pathfind = default!;
|
||||
private SharedTransformSystem _transform = default!;
|
||||
|
||||
/// <summary>
|
||||
/// When to shut the task down.
|
||||
/// </summary>
|
||||
[DataField("shutdownState")]
|
||||
public HTNPlanState ShutdownState { get; } = HTNPlanState.TaskFinished;
|
||||
|
||||
/// <summary>
|
||||
/// Should we assume the MovementTarget is reachable during planning or should we pathfind to it?
|
||||
/// </summary>
|
||||
@@ -35,7 +41,7 @@ public sealed class MoveToOperator : HTNOperator
|
||||
/// Target Coordinates to move to. This gets removed after execution.
|
||||
/// </summary>
|
||||
[DataField("targetKey")]
|
||||
public string TargetKey = "MovementTarget";
|
||||
public string TargetKey = "TargetCoordinates";
|
||||
|
||||
/// <summary>
|
||||
/// Where the pathfinding result will be stored (if applicable). This gets removed after execution.
|
||||
@@ -49,6 +55,12 @@ public sealed class MoveToOperator : HTNOperator
|
||||
[DataField("rangeKey")]
|
||||
public string RangeKey = "MovementRange";
|
||||
|
||||
/// <summary>
|
||||
/// Do we only need to move into line of sight.
|
||||
/// </summary>
|
||||
[DataField("stopOnLineOfSight")]
|
||||
public bool StopOnLineOfSight;
|
||||
|
||||
private const string MovementCancelToken = "MovementCancelToken";
|
||||
|
||||
public override void Initialize(IEntitySystemManager sysManager)
|
||||
@@ -132,6 +144,7 @@ public sealed class MoveToOperator : HTNOperator
|
||||
|
||||
// Re-use the path we may have if applicable.
|
||||
var comp = _steering.Register(uid, targetCoordinates);
|
||||
comp.ArriveOnLineOfSight = StopOnLineOfSight;
|
||||
|
||||
if (blackboard.TryGetValue<float>(RangeKey, out var range, _entManager))
|
||||
{
|
||||
@@ -150,10 +163,30 @@ public sealed class MoveToOperator : HTNOperator
|
||||
}
|
||||
}
|
||||
|
||||
public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
|
||||
{
|
||||
base.Shutdown(blackboard, status);
|
||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||
|
||||
if (!_entManager.TryGetComponent<NPCSteeringComponent>(owner, out var steering))
|
||||
return HTNOperatorStatus.Failed;
|
||||
|
||||
// Just keep moving in the background and let the other tasks handle it.
|
||||
if (ShutdownState == HTNPlanState.PlanFinished && steering.Status == SteeringStatus.Moving)
|
||||
{
|
||||
return HTNOperatorStatus.Finished;
|
||||
}
|
||||
|
||||
return steering.Status switch
|
||||
{
|
||||
SteeringStatus.InRange => HTNOperatorStatus.Finished,
|
||||
SteeringStatus.NoPath => HTNOperatorStatus.Failed,
|
||||
SteeringStatus.Moving => HTNOperatorStatus.Continuing,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
public void ConditionalShutdown(NPCBlackboard blackboard)
|
||||
{
|
||||
// Cleanup the blackboard and remove steering.
|
||||
if (blackboard.TryGetValue<CancellationTokenSource>(MovementCancelToken, out var cancelToken, _entManager))
|
||||
{
|
||||
@@ -171,20 +204,4 @@ public sealed class MoveToOperator : HTNOperator
|
||||
|
||||
_steering.Unregister(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
|
||||
}
|
||||
|
||||
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
|
||||
{
|
||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||
|
||||
if (!_entManager.TryGetComponent<NPCSteeringComponent>(owner, out var steering))
|
||||
return HTNOperatorStatus.Failed;
|
||||
|
||||
return steering.Status switch
|
||||
{
|
||||
SteeringStatus.InRange => HTNOperatorStatus.Finished,
|
||||
SteeringStatus.NoPath => HTNOperatorStatus.Failed,
|
||||
SteeringStatus.Moving => HTNOperatorStatus.Continuing,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
||||
|
||||
/// <summary>
|
||||
/// What it sounds like.
|
||||
/// </summary>
|
||||
public sealed class NoOperator : HTNOperator
|
||||
{
|
||||
|
||||
}
|
||||
@@ -16,8 +16,8 @@ public sealed class PickAccessibleOperator : HTNOperator
|
||||
[DataField("rangeKey", required: true)]
|
||||
public string RangeKey = string.Empty;
|
||||
|
||||
[DataField("targetKey", required: true)]
|
||||
public string TargetKey = string.Empty;
|
||||
[DataField("targetCoordinates")]
|
||||
public string TargetCoordinates = "TargetCoordinates";
|
||||
|
||||
/// <summary>
|
||||
/// Where the pathfinding result will be stored (if applicable). This gets removed after execution.
|
||||
@@ -58,7 +58,7 @@ public sealed class PickAccessibleOperator : HTNOperator
|
||||
|
||||
return (true, new Dictionary<string, object>()
|
||||
{
|
||||
{ TargetKey, target },
|
||||
{ TargetCoordinates, target },
|
||||
{ PathfindKey, path}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@ public sealed class RotateToTargetOperator : HTNOperator
|
||||
_rotate = sysManager.GetEntitySystem<RotateToFaceSystem>();
|
||||
}
|
||||
|
||||
public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
public override void TaskShutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
{
|
||||
base.Shutdown(blackboard, status);
|
||||
base.TaskShutdown(blackboard, status);
|
||||
blackboard.Remove<Angle>(TargetKey);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,9 +36,9 @@ public sealed class MedibotInjectOperator : HTNOperator
|
||||
_solution = sysManager.GetEntitySystem<SolutionContainerSystem>();
|
||||
}
|
||||
|
||||
public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
public override void TaskShutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
{
|
||||
base.Shutdown(blackboard, status);
|
||||
base.TaskShutdown(blackboard, status);
|
||||
blackboard.Remove<EntityUid>(TargetKey);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@ public sealed class UtilityOperator : HTNOperator
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField("key")] public string Key = "CombatTarget";
|
||||
[DataField("key")] public string Key = "Target";
|
||||
|
||||
/// <summary>
|
||||
/// The EntityCoordinates of the specified target.
|
||||
/// </summary>
|
||||
[DataField("keyCoordinates")]
|
||||
public string KeyCoordinates = "CombatTargetCoordinates";
|
||||
public string KeyCoordinates = "TargetCoordinates";
|
||||
|
||||
[DataField("proto", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<UtilityQueryPrototype>))]
|
||||
public string Prototype = string.Empty;
|
||||
|
||||
@@ -25,9 +25,9 @@ public sealed class WaitOperator : HTNOperator
|
||||
return timer <= 0f ? HTNOperatorStatus.Finished : HTNOperatorStatus.Continuing;
|
||||
}
|
||||
|
||||
public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
public override void TaskShutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
|
||||
{
|
||||
base.Shutdown(blackboard, status);
|
||||
base.TaskShutdown(blackboard, status);
|
||||
|
||||
// The replacement plan may want this value so only dump it if we're successful.
|
||||
if (status != HTNOperatorStatus.BetterPlan)
|
||||
|
||||
@@ -3,7 +3,9 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.Interaction;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Inventory;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -27,9 +29,10 @@ public sealed class NPCBlackboard : IEnumerable<KeyValuePair<string, object>>
|
||||
{MeleeMissChance, 0.3f},
|
||||
{"MeleeRange", 1f},
|
||||
{"MinimumIdleTime", 2f},
|
||||
{"MovementRangeClose", 0.2f},
|
||||
{"MovementRange", 1.5f},
|
||||
{"RangedRange", 10f},
|
||||
{"RotateSpeed", MathF.PI},
|
||||
{"RotateSpeed", float.MaxValue},
|
||||
{"VisionRadius", 10f},
|
||||
};
|
||||
|
||||
@@ -151,6 +154,7 @@ public sealed class NPCBlackboard : IEnumerable<KeyValuePair<string, object>>
|
||||
switch (key)
|
||||
{
|
||||
case Access:
|
||||
{
|
||||
if (!TryGetValue(Owner, out owner, entManager))
|
||||
{
|
||||
return false;
|
||||
@@ -159,7 +163,33 @@ public sealed class NPCBlackboard : IEnumerable<KeyValuePair<string, object>>
|
||||
var access = entManager.EntitySysManager.GetEntitySystem<AccessReaderSystem>();
|
||||
value = access.FindAccessTags(owner);
|
||||
return true;
|
||||
}
|
||||
case ActiveHand:
|
||||
{
|
||||
if (!TryGetValue(Owner, out owner, entManager) ||
|
||||
!entManager.TryGetComponent<HandsComponent>(owner, out var hands) ||
|
||||
hands.ActiveHand == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = hands.ActiveHand;
|
||||
return true;
|
||||
}
|
||||
case ActiveHandFree:
|
||||
{
|
||||
if (!TryGetValue(Owner, out owner, entManager) ||
|
||||
!entManager.TryGetComponent<HandsComponent>(owner, out var hands) ||
|
||||
hands.ActiveHand == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = hands.ActiveHand.IsEmpty;
|
||||
return true;
|
||||
}
|
||||
case CanMove:
|
||||
{
|
||||
if (!TryGetValue(Owner, out owner, entManager))
|
||||
{
|
||||
return false;
|
||||
@@ -168,7 +198,53 @@ public sealed class NPCBlackboard : IEnumerable<KeyValuePair<string, object>>
|
||||
var blocker = entManager.EntitySysManager.GetEntitySystem<ActionBlockerSystem>();
|
||||
value = blocker.CanMove(owner);
|
||||
return true;
|
||||
}
|
||||
case FreeHands:
|
||||
{
|
||||
if (!TryGetValue(Owner, out owner, entManager) ||
|
||||
!entManager.TryGetComponent<HandsComponent>(owner, out var hands) ||
|
||||
hands.ActiveHand == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var handos = new List<string>();
|
||||
|
||||
foreach (var (id, hand) in hands.Hands)
|
||||
{
|
||||
if (!hand.IsEmpty)
|
||||
continue;
|
||||
|
||||
handos.Add(id);
|
||||
}
|
||||
|
||||
value = handos;
|
||||
return true;
|
||||
}
|
||||
case Inventory:
|
||||
{
|
||||
if (!TryGetValue(Owner, out owner, entManager) ||
|
||||
!entManager.TryGetComponent<HandsComponent>(owner, out var hands) ||
|
||||
hands.ActiveHand == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var handos = new List<string>();
|
||||
|
||||
foreach (var (id, hand) in hands.Hands)
|
||||
{
|
||||
if (!hand.IsEmpty)
|
||||
continue;
|
||||
|
||||
handos.Add(id);
|
||||
}
|
||||
|
||||
value = handos;
|
||||
return true;
|
||||
}
|
||||
case OwnerCoordinates:
|
||||
{
|
||||
if (!TryGetValue(Owner, out owner, entManager))
|
||||
{
|
||||
return false;
|
||||
@@ -181,6 +257,7 @@ public sealed class NPCBlackboard : IEnumerable<KeyValuePair<string, object>>
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -200,8 +277,12 @@ public sealed class NPCBlackboard : IEnumerable<KeyValuePair<string, object>>
|
||||
*/
|
||||
|
||||
public const string Access = "Access";
|
||||
public const string ActiveHand = "ActiveHand";
|
||||
public const string ActiveHandFree = "ActiveHandFree";
|
||||
public const string CanMove = "CanMove";
|
||||
public const string FreeHands = "FreeHands";
|
||||
public const string FollowTarget = "FollowTarget";
|
||||
public const string Inventory = "Inventory";
|
||||
public const string MedibotInjectRange = "MedibotInjectRange";
|
||||
|
||||
public const string MeleeMissChance = "MeleeMissChance";
|
||||
@@ -237,7 +318,7 @@ public sealed class NPCBlackboard : IEnumerable<KeyValuePair<string, object>>
|
||||
|
||||
public const string RotateSpeed = "RotateSpeed";
|
||||
public const string VisionRadius = "VisionRadius";
|
||||
public const string UtilityTarget = "Target";
|
||||
public const string UtilityTarget = "UtilityTarget";
|
||||
|
||||
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
|
||||
{
|
||||
|
||||
@@ -16,8 +16,9 @@ public sealed class NPCBlackboardSerializer : ITypeReader<NPCBlackboard, Mapping
|
||||
{
|
||||
var validated = new List<ValidationNode>();
|
||||
|
||||
if (node.Count > 0)
|
||||
{
|
||||
if (node.Count <= 0)
|
||||
return new ValidatedSequenceNode(validated);
|
||||
|
||||
var reflection = dependencies.Resolve<IReflectionManager>();
|
||||
|
||||
foreach (var data in node)
|
||||
@@ -41,7 +42,6 @@ public sealed class NPCBlackboardSerializer : ITypeReader<NPCBlackboard, Mapping
|
||||
var validatedNode = serializationManager.ValidateNode(type, data.Value, context);
|
||||
validated.Add(validatedNode);
|
||||
}
|
||||
}
|
||||
|
||||
return new ValidatedSequenceNode(validated);
|
||||
}
|
||||
@@ -53,8 +53,9 @@ public sealed class NPCBlackboardSerializer : ITypeReader<NPCBlackboard, Mapping
|
||||
{
|
||||
var value = instanceProvider != null ? instanceProvider() : new NPCBlackboard();
|
||||
|
||||
if (node.Count > 0)
|
||||
{
|
||||
if (node.Count <= 0)
|
||||
return value;
|
||||
|
||||
var reflection = dependencies.Resolve<IReflectionManager>();
|
||||
|
||||
foreach (var data in node)
|
||||
@@ -76,7 +77,6 @@ public sealed class NPCBlackboardSerializer : ITypeReader<NPCBlackboard, Mapping
|
||||
|
||||
value.SetValue(key, bbData);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Content.Server.NPC.Queries.Considerations;
|
||||
|
||||
public sealed class TargetAmmoCon : UtilityConsideration
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Content.Server.NPC.Queries.Considerations;
|
||||
|
||||
/// <summary>
|
||||
/// Returns 1f where the specified target is valid for the active hand's whitelist.
|
||||
/// </summary>
|
||||
public sealed class TargetAmmoMatchesCon : UtilityConsideration
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Content.Server.NPC.Queries.Considerations;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DPS out of 100.
|
||||
/// </summary>
|
||||
public sealed class TargetMeleeCon : UtilityConsideration
|
||||
{
|
||||
|
||||
}
|
||||
@@ -2,9 +2,9 @@ namespace Content.Server.NPC.Queries.Curves;
|
||||
|
||||
public sealed class QuadraticCurve : IUtilityCurve
|
||||
{
|
||||
[DataField("slope")] public readonly float Slope;
|
||||
[DataField("slope")] public readonly float Slope = 1f;
|
||||
|
||||
[DataField("exponent")] public readonly float Exponent;
|
||||
[DataField("exponent")] public readonly float Exponent = 1f;
|
||||
|
||||
[DataField("yOffset")] public readonly float YOffset;
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using Content.Shared.Inventory;
|
||||
|
||||
namespace Content.Server.NPC.Queries.Queries;
|
||||
|
||||
public sealed class ClothingSlotFilter : UtilityQueryFilter
|
||||
{
|
||||
|
||||
[DataField("slotFlags", required: true)]
|
||||
public SlotFlags SlotFlags = SlotFlags.NONE;
|
||||
}
|
||||
|
||||
12
Content.Server/NPC/Queries/Queries/ComponentFilter.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.NPC.Queries.Queries;
|
||||
|
||||
public sealed class ComponentFilter : UtilityQueryFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Components to filter for.
|
||||
/// </summary>
|
||||
[DataField("components", required: true)]
|
||||
public ComponentRegistry Components = new();
|
||||
}
|
||||
9
Content.Server/NPC/Queries/Queries/InventoryQuery.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Content.Server.NPC.Queries.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Returns inventory entities recursively.
|
||||
/// </summary>
|
||||
public sealed class InventoryQuery : UtilityQuery
|
||||
{
|
||||
|
||||
}
|
||||
@@ -18,66 +18,6 @@ public sealed partial class NPCCombatSystem
|
||||
{
|
||||
SubscribeLocalEvent<NPCMeleeCombatComponent, ComponentStartup>(OnMeleeStartup);
|
||||
SubscribeLocalEvent<NPCMeleeCombatComponent, ComponentShutdown>(OnMeleeShutdown);
|
||||
SubscribeLocalEvent<NPCMeleeCombatComponent, NPCSteeringEvent>(OnMeleeSteering);
|
||||
}
|
||||
|
||||
private void OnMeleeSteering(EntityUid uid, NPCMeleeCombatComponent component, ref NPCSteeringEvent args)
|
||||
{
|
||||
args.Steering.CanSeek = true;
|
||||
|
||||
if (TryComp<MeleeWeaponComponent>(component.Weapon, out var weapon))
|
||||
{
|
||||
var cdRemaining = weapon.NextAttack - _timing.CurTime;
|
||||
var attackCooldown = TimeSpan.FromSeconds(1f / _melee.GetAttackRate(component.Weapon, uid, weapon));
|
||||
|
||||
// Might as well get in range.
|
||||
if (cdRemaining < attackCooldown * 0.45f)
|
||||
return;
|
||||
|
||||
if (!_physics.TryGetNearestPoints(uid, component.Target, out var pointA, out var pointB))
|
||||
return;
|
||||
|
||||
var obstacleDirection = pointB - args.WorldPosition;
|
||||
|
||||
// If they're moving away then pursue anyway.
|
||||
// If just hit then always back up a bit.
|
||||
if (cdRemaining < attackCooldown * 0.90f &&
|
||||
TryComp<PhysicsComponent>(component.Target, out var targetPhysics) &&
|
||||
Vector2.Dot(targetPhysics.LinearVelocity, obstacleDirection) > 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (cdRemaining < TimeSpan.FromSeconds(1f / _melee.GetAttackRate(component.Weapon, uid, weapon)) * 0.45f)
|
||||
return;
|
||||
|
||||
var idealDistance = weapon.Range * 4f;
|
||||
var obstacleDistance = obstacleDirection.Length();
|
||||
|
||||
if (obstacleDistance > idealDistance || obstacleDistance == 0f)
|
||||
{
|
||||
// Don't want to get too far.
|
||||
return;
|
||||
}
|
||||
|
||||
args.Steering.CanSeek = false;
|
||||
obstacleDirection = args.OffsetRotation.RotateVec(obstacleDirection);
|
||||
var norm = obstacleDirection.Normalized();
|
||||
|
||||
var weight = (obstacleDistance <= args.AgentRadius
|
||||
? 1f
|
||||
: (idealDistance - obstacleDistance) / idealDistance);
|
||||
|
||||
for (var i = 0; i < SharedNPCSteeringSystem.InterestDirections; i++)
|
||||
{
|
||||
var result = -Vector2.Dot(norm, NPCSteeringSystem.Directions[i]) * weight;
|
||||
|
||||
if (result < 0f)
|
||||
continue;
|
||||
|
||||
args.Interest[i] = MathF.Max(args.Interest[i], result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMeleeShutdown(EntityUid uid, NPCMeleeCombatComponent component, ComponentShutdown args)
|
||||
@@ -87,7 +27,7 @@ public sealed partial class NPCCombatSystem
|
||||
_combat.SetInCombatMode(uid, false, combatMode);
|
||||
}
|
||||
|
||||
_steering.Unregister(component.Owner);
|
||||
_steering.Unregister(uid);
|
||||
}
|
||||
|
||||
private void OnMeleeStartup(EntityUid uid, NPCMeleeCombatComponent component, ComponentStartup args)
|
||||
@@ -96,9 +36,6 @@ public sealed partial class NPCCombatSystem
|
||||
{
|
||||
_combat.SetInCombatMode(uid, true, combatMode);
|
||||
}
|
||||
|
||||
// TODO: Cleanup later, just looking for parity for now.
|
||||
component.Weapon = uid;
|
||||
}
|
||||
|
||||
private void UpdateMelee(float frameTime)
|
||||
@@ -107,11 +44,10 @@ public sealed partial class NPCCombatSystem
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var physicsQuery = GetEntityQuery<PhysicsComponent>();
|
||||
var curTime = _timing.CurTime;
|
||||
var query = EntityQueryEnumerator<NPCMeleeCombatComponent, ActiveNPCComponent>();
|
||||
|
||||
foreach (var (comp, _) in EntityQuery<NPCMeleeCombatComponent, ActiveNPCComponent>())
|
||||
while (query.MoveNext(out var uid, out var comp, out _))
|
||||
{
|
||||
var uid = comp.Owner;
|
||||
|
||||
if (!combatQuery.TryGetComponent(uid, out var combat) || !combat.IsInCombatMode)
|
||||
{
|
||||
RemComp<NPCMeleeCombatComponent>(uid);
|
||||
@@ -126,7 +62,7 @@ public sealed partial class NPCCombatSystem
|
||||
{
|
||||
component.Status = CombatStatus.Normal;
|
||||
|
||||
if (!TryComp<MeleeWeaponComponent>(component.Weapon, out var weapon))
|
||||
if (!_melee.TryGetWeapon(uid, out var weaponUid, out var weapon))
|
||||
{
|
||||
component.Status = CombatStatus.NoWeapon;
|
||||
return;
|
||||
@@ -167,12 +103,6 @@ public sealed partial class NPCCombatSystem
|
||||
return;
|
||||
}
|
||||
|
||||
steering = EnsureComp<NPCSteeringComponent>(uid);
|
||||
steering.Range = MathF.Max(0.2f, weapon.Range - 0.4f);
|
||||
|
||||
// Gets unregistered on component shutdown.
|
||||
_steering.TryRegister(uid, new EntityCoordinates(component.Target, Vector2.Zero), steering);
|
||||
|
||||
if (weapon.NextAttack > curTime || !Enabled)
|
||||
return;
|
||||
|
||||
@@ -180,11 +110,11 @@ public sealed partial class NPCCombatSystem
|
||||
physicsQuery.TryGetComponent(component.Target, out var targetPhysics) &&
|
||||
targetPhysics.LinearVelocity.LengthSquared() != 0f)
|
||||
{
|
||||
_melee.AttemptLightAttackMiss(uid, component.Weapon, weapon, targetXform.Coordinates.Offset(_random.NextVector2(0.5f)));
|
||||
_melee.AttemptLightAttackMiss(uid, weaponUid, weapon, targetXform.Coordinates.Offset(_random.NextVector2(0.5f)));
|
||||
}
|
||||
else
|
||||
{
|
||||
_melee.AttemptLightAttack(uid, component.Weapon, weapon, component.Target);
|
||||
_melee.AttemptLightAttack(uid, weaponUid, weapon, component.Target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Numerics;
|
||||
using Content.Server.NPC.Components;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Weapons.Ranged.Components;
|
||||
using Content.Shared.Weapons.Ranged.Events;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics.Components;
|
||||
|
||||
@@ -12,6 +13,12 @@ public sealed partial class NPCCombatSystem
|
||||
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
|
||||
[Dependency] private readonly RotateToFaceSystem _rotate = default!;
|
||||
|
||||
private EntityQuery<CombatModeComponent> _combatQuery;
|
||||
private EntityQuery<NPCSteeringComponent> _steeringQuery;
|
||||
private EntityQuery<RechargeBasicEntityAmmoComponent> _rechargeQuery;
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
|
||||
// TODO: Don't predict for hitscan
|
||||
private const float ShootSpeed = 20f;
|
||||
|
||||
@@ -22,6 +29,12 @@ public sealed partial class NPCCombatSystem
|
||||
|
||||
private void InitializeRanged()
|
||||
{
|
||||
_combatQuery = GetEntityQuery<CombatModeComponent>();
|
||||
_physicsQuery = GetEntityQuery<PhysicsComponent>();
|
||||
_rechargeQuery = GetEntityQuery<RechargeBasicEntityAmmoComponent>();
|
||||
_steeringQuery = GetEntityQuery<NPCSteeringComponent>();
|
||||
_xformQuery = GetEntityQuery<TransformComponent>();
|
||||
|
||||
SubscribeLocalEvent<NPCRangedCombatComponent, ComponentStartup>(OnRangedStartup);
|
||||
SubscribeLocalEvent<NPCRangedCombatComponent, ComponentShutdown>(OnRangedShutdown);
|
||||
}
|
||||
@@ -48,9 +61,6 @@ public sealed partial class NPCCombatSystem
|
||||
|
||||
private void UpdateRanged(float frameTime)
|
||||
{
|
||||
var bodyQuery = GetEntityQuery<PhysicsComponent>();
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var combatQuery = GetEntityQuery<CombatModeComponent>();
|
||||
var query = EntityQueryEnumerator<NPCRangedCombatComponent, TransformComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out var comp, out var xform))
|
||||
@@ -58,8 +68,15 @@ public sealed partial class NPCCombatSystem
|
||||
if (comp.Status == CombatStatus.Unspecified)
|
||||
continue;
|
||||
|
||||
if (!xformQuery.TryGetComponent(comp.Target, out var targetXform) ||
|
||||
!bodyQuery.TryGetComponent(comp.Target, out var targetBody))
|
||||
if (_steeringQuery.TryGetComponent(uid, out var steering) && steering.Status == SteeringStatus.NoPath)
|
||||
{
|
||||
comp.Status = CombatStatus.TargetUnreachable;
|
||||
comp.ShootAccumulator = 0f;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_xformQuery.TryGetComponent(comp.Target, out var targetXform) ||
|
||||
!_physicsQuery.TryGetComponent(comp.Target, out var targetBody))
|
||||
{
|
||||
comp.Status = CombatStatus.TargetUnreachable;
|
||||
comp.ShootAccumulator = 0f;
|
||||
@@ -73,7 +90,7 @@ public sealed partial class NPCCombatSystem
|
||||
continue;
|
||||
}
|
||||
|
||||
if (combatQuery.TryGetComponent(uid, out var combatMode))
|
||||
if (_combatQuery.TryGetComponent(uid, out var combatMode))
|
||||
{
|
||||
_combat.SetInCombatMode(uid, true, combatMode);
|
||||
}
|
||||
@@ -85,10 +102,26 @@ public sealed partial class NPCCombatSystem
|
||||
continue;
|
||||
}
|
||||
|
||||
var ammoEv = new GetAmmoCountEvent();
|
||||
RaiseLocalEvent(gunUid, ref ammoEv);
|
||||
|
||||
if (ammoEv.Count == 0)
|
||||
{
|
||||
// Recharging then?
|
||||
if (_rechargeQuery.HasComponent(gunUid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
comp.Status = CombatStatus.Unspecified;
|
||||
comp.ShootAccumulator = 0f;
|
||||
continue;
|
||||
}
|
||||
|
||||
comp.LOSAccumulator -= frameTime;
|
||||
|
||||
var (worldPos, worldRot) = _transform.GetWorldPositionRotation(xform, xformQuery);
|
||||
var (targetPos, targetRot) = _transform.GetWorldPositionRotation(targetXform, xformQuery);
|
||||
var worldPos = _transform.GetWorldPosition(xform);
|
||||
var targetPos = _transform.GetWorldPosition(targetXform);
|
||||
|
||||
// We'll work out the projected spot of the target and shoot there instead of where they are.
|
||||
var distance = (targetPos - worldPos).Length();
|
||||
@@ -105,7 +138,7 @@ public sealed partial class NPCCombatSystem
|
||||
if (!comp.TargetInLOS)
|
||||
{
|
||||
comp.ShootAccumulator = 0f;
|
||||
comp.Status = CombatStatus.TargetUnreachable;
|
||||
comp.Status = CombatStatus.NotInSight;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -156,6 +189,7 @@ public sealed partial class NPCCombatSystem
|
||||
}
|
||||
|
||||
_gun.AttemptShoot(uid, gunUid, gun, targetCordinates);
|
||||
comp.Status = CombatStatus.Normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
206
Content.Server/NPC/Systems/NPCJukeSystem.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
using System.Numerics;
|
||||
using Content.Server.NPC.Components;
|
||||
using Content.Server.NPC.Events;
|
||||
using Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat;
|
||||
using Content.Server.Weapons.Melee;
|
||||
using Content.Shared.NPC;
|
||||
using Content.Shared.Weapons.Melee;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.NPC.Systems;
|
||||
|
||||
public sealed class NPCJukeSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly MeleeWeaponSystem _melee = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
|
||||
private EntityQuery<NPCMeleeCombatComponent> _npcMeleeQuery;
|
||||
private EntityQuery<NPCRangedCombatComponent> _npcRangedQuery;
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_npcMeleeQuery = GetEntityQuery<NPCMeleeCombatComponent>();
|
||||
_npcRangedQuery = GetEntityQuery<NPCRangedCombatComponent>();
|
||||
_physicsQuery = GetEntityQuery<PhysicsComponent>();
|
||||
|
||||
SubscribeLocalEvent<NPCJukeComponent, EntityUnpausedEvent>(OnJukeUnpaused);
|
||||
SubscribeLocalEvent<NPCJukeComponent, NPCSteeringEvent>(OnJukeSteering);
|
||||
}
|
||||
|
||||
private void OnJukeUnpaused(EntityUid uid, NPCJukeComponent component, ref EntityUnpausedEvent args)
|
||||
{
|
||||
component.NextJuke += args.PausedTime;
|
||||
}
|
||||
|
||||
private void OnJukeSteering(EntityUid uid, NPCJukeComponent component, ref NPCSteeringEvent args)
|
||||
{
|
||||
if (component.JukeType == JukeType.AdjacentTile)
|
||||
{
|
||||
if (_npcRangedQuery.TryGetComponent(uid, out var ranged) &&
|
||||
ranged.Status == CombatStatus.NotInSight)
|
||||
{
|
||||
component.TargetTile = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_timing.CurTime < component.NextJuke)
|
||||
{
|
||||
component.TargetTile = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryComp<MapGridComponent>(args.Transform.GridUid, out var grid))
|
||||
{
|
||||
component.TargetTile = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var currentTile = grid.CoordinatesToTile(args.Transform.Coordinates);
|
||||
|
||||
if (component.TargetTile == null)
|
||||
{
|
||||
var targetTile = currentTile;
|
||||
var startIndex = _random.Next(8);
|
||||
_physicsQuery.TryGetComponent(uid, out var ownerPhysics);
|
||||
var collisionLayer = ownerPhysics?.CollisionLayer ?? 0;
|
||||
var collisionMask = ownerPhysics?.CollisionMask ?? 0;
|
||||
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
var index = (startIndex + i) % 8;
|
||||
var neighbor = ((Direction) index).ToIntVec() + currentTile;
|
||||
var valid = true;
|
||||
|
||||
// TODO: Probably make this a helper on engine maybe
|
||||
var tileBounds = new Box2(neighbor, neighbor + grid.TileSize);
|
||||
tileBounds = tileBounds.Enlarged(-0.1f);
|
||||
|
||||
foreach (var ent in _lookup.GetEntitiesIntersecting(args.Transform.GridUid.Value, tileBounds))
|
||||
{
|
||||
if (ent == uid ||
|
||||
!_physicsQuery.TryGetComponent(ent, out var physics) ||
|
||||
!physics.CanCollide ||
|
||||
!physics.Hard ||
|
||||
((physics.CollisionMask & collisionLayer) == 0x0 &&
|
||||
(physics.CollisionLayer & collisionMask) == 0x0))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!valid)
|
||||
continue;
|
||||
|
||||
targetTile = neighbor;
|
||||
break;
|
||||
}
|
||||
|
||||
component.TargetTile ??= targetTile;
|
||||
}
|
||||
|
||||
var elapsed = _timing.CurTime - component.NextJuke;
|
||||
|
||||
// Finished juke, reset timer.
|
||||
if (elapsed.TotalSeconds > component.JukeDuration ||
|
||||
currentTile == component.TargetTile)
|
||||
{
|
||||
component.TargetTile = null;
|
||||
component.NextJuke = _timing.CurTime + TimeSpan.FromSeconds(component.JukeDuration);
|
||||
return;
|
||||
}
|
||||
|
||||
var targetCoords = grid.GridTileToWorld(component.TargetTile.Value);
|
||||
var targetDir = (targetCoords.Position - args.WorldPosition);
|
||||
targetDir = args.OffsetRotation.RotateVec(targetDir);
|
||||
const float weight = 1f;
|
||||
var norm = targetDir.Normalized();
|
||||
|
||||
for (var i = 0; i < SharedNPCSteeringSystem.InterestDirections; i++)
|
||||
{
|
||||
var result = -Vector2.Dot(norm, NPCSteeringSystem.Directions[i]) * weight;
|
||||
|
||||
if (result < 0f)
|
||||
continue;
|
||||
|
||||
args.Steering.Interest[i] = MathF.Max(args.Steering.Interest[i], result);
|
||||
}
|
||||
|
||||
args.Steering.CanSeek = false;
|
||||
}
|
||||
|
||||
if (component.JukeType == JukeType.Away)
|
||||
{
|
||||
// TODO: Ranged away juking
|
||||
if (_npcMeleeQuery.TryGetComponent(uid, out var melee))
|
||||
{
|
||||
if (!_melee.TryGetWeapon(uid, out var weaponUid, out var weapon))
|
||||
return;
|
||||
|
||||
var cdRemaining = weapon.NextAttack - _timing.CurTime;
|
||||
var attackCooldown = TimeSpan.FromSeconds(1f / _melee.GetAttackRate(weaponUid, uid, weapon));
|
||||
|
||||
// Might as well get in range.
|
||||
if (cdRemaining < attackCooldown * 0.45f)
|
||||
return;
|
||||
|
||||
if (!_physics.TryGetNearestPoints(uid, melee.Target, out var pointA, out var pointB))
|
||||
return;
|
||||
|
||||
var obstacleDirection = pointB - args.WorldPosition;
|
||||
|
||||
// If they're moving away then pursue anyway.
|
||||
// If just hit then always back up a bit.
|
||||
if (cdRemaining < attackCooldown * 0.90f &&
|
||||
TryComp<PhysicsComponent>(melee.Target, out var targetPhysics) &&
|
||||
Vector2.Dot(targetPhysics.LinearVelocity, obstacleDirection) > 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (cdRemaining < TimeSpan.FromSeconds(1f / _melee.GetAttackRate(weaponUid, uid, weapon)) * 0.45f)
|
||||
return;
|
||||
|
||||
var idealDistance = weapon.Range * 4f;
|
||||
var obstacleDistance = obstacleDirection.Length();
|
||||
|
||||
if (obstacleDistance > idealDistance || obstacleDistance == 0f)
|
||||
{
|
||||
// Don't want to get too far.
|
||||
return;
|
||||
}
|
||||
|
||||
obstacleDirection = args.OffsetRotation.RotateVec(obstacleDirection);
|
||||
var norm = obstacleDirection.Normalized();
|
||||
|
||||
var weight = obstacleDistance <= args.Steering.Radius
|
||||
? 1f
|
||||
: (idealDistance - obstacleDistance) / idealDistance;
|
||||
|
||||
for (var i = 0; i < SharedNPCSteeringSystem.InterestDirections; i++)
|
||||
{
|
||||
var result = -Vector2.Dot(norm, NPCSteeringSystem.Directions[i]) * weight;
|
||||
|
||||
if (result < 0f)
|
||||
continue;
|
||||
|
||||
args.Steering.Interest[i] = MathF.Max(args.Steering.Interest[i], result);
|
||||
}
|
||||
}
|
||||
|
||||
args.Steering.CanSeek = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,12 +77,43 @@ public sealed partial class NPCSteeringSystem
|
||||
{
|
||||
var ourCoordinates = xform.Coordinates;
|
||||
var destinationCoordinates = steering.Coordinates;
|
||||
var inLos = true;
|
||||
|
||||
// We've arrived, nothing else matters.
|
||||
if (xform.Coordinates.TryDistance(EntityManager, destinationCoordinates, out var distance) &&
|
||||
distance <= steering.Range)
|
||||
// Check if we're in LOS if that's required.
|
||||
// TODO: Need something uhh better not sure on the interaction between these.
|
||||
if (steering.ArriveOnLineOfSight)
|
||||
{
|
||||
// TODO: use vision range
|
||||
inLos = _interaction.InRangeUnobstructed(uid, steering.Coordinates, 10f);
|
||||
|
||||
if (inLos)
|
||||
{
|
||||
steering.LineOfSightTimer += frameTime;
|
||||
|
||||
if (steering.LineOfSightTimer >= steering.LineOfSightTimeRequired)
|
||||
{
|
||||
steering.Status = SteeringStatus.InRange;
|
||||
ResetStuck(steering, ourCoordinates);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
steering.LineOfSightTimer = 0f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
steering.LineOfSightTimer = 0f;
|
||||
}
|
||||
|
||||
// We've arrived, nothing else matters.
|
||||
if (xform.Coordinates.TryDistance(EntityManager, destinationCoordinates, out var targetDistance) &&
|
||||
inLos &&
|
||||
targetDistance <= steering.Range)
|
||||
{
|
||||
steering.Status = SteeringStatus.InRange;
|
||||
ResetStuck(steering, ourCoordinates);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -117,7 +148,7 @@ public sealed partial class NPCSteeringSystem
|
||||
// This is to avoid popping it too early
|
||||
else if (steering.CurrentPath.TryPeek(out var node) && IsFreeSpace(uid, steering, node))
|
||||
{
|
||||
arrivalDistance = MathF.Min(node.Box.Width / 2f, node.Box.Height / 2f) - 0.01f;
|
||||
arrivalDistance = MathF.Max(0.05f, MathF.Min(node.Box.Width / 2f, node.Box.Height / 2f) - 0.05f);
|
||||
}
|
||||
// Try getting into blocked range I guess?
|
||||
// TODO: Consider melee range or the likes.
|
||||
@@ -172,7 +203,7 @@ public sealed partial class NPCSteeringSystem
|
||||
steering.Status = SteeringStatus.NoPath;
|
||||
return false;
|
||||
case SteeringObstacleStatus.Continuing:
|
||||
CheckPath(uid, steering, xform, needsPath, distance);
|
||||
CheckPath(uid, steering, xform, needsPath, targetDistance);
|
||||
return true;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
@@ -205,9 +236,7 @@ public sealed partial class NPCSteeringSystem
|
||||
}
|
||||
else
|
||||
{
|
||||
// This probably shouldn't happen as we check above but eh.
|
||||
steering.Status = SteeringStatus.NoPath;
|
||||
return false;
|
||||
needsPath = true;
|
||||
}
|
||||
}
|
||||
// Stuck detection
|
||||
@@ -228,23 +257,28 @@ public sealed partial class NPCSteeringSystem
|
||||
// B) NPCs still try to move in locked containers (e.g. cow, hamster)
|
||||
// and I don't want to spam grafana even harder than it gets spammed rn.
|
||||
Log.Debug($"NPC {ToPrettyString(uid)} found stuck at {ourCoordinates}");
|
||||
needsPath = true;
|
||||
|
||||
if (stuckTime.TotalSeconds > maxStuckTime * 3)
|
||||
{
|
||||
steering.Status = SteeringStatus.NoPath;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ResetStuck(steering, ourCoordinates);
|
||||
}
|
||||
|
||||
// Do we have no more nodes to follow OR has the target moved sufficiently? If so then re-path.
|
||||
if (!needsPath)
|
||||
// If not in LOS and no path then get a new one fam.
|
||||
if (!inLos && steering.CurrentPath.Count == 0)
|
||||
{
|
||||
needsPath = steering.CurrentPath.Count == 0 || (steering.CurrentPath.Peek().Data.Flags & PathfindingBreadcrumbFlag.Invalid) != 0x0;
|
||||
needsPath = true;
|
||||
}
|
||||
|
||||
// TODO: Probably need partial planning support i.e. patch from the last node to where the target moved to.
|
||||
CheckPath(uid, steering, xform, needsPath, distance);
|
||||
CheckPath(uid, steering, xform, needsPath, targetDistance);
|
||||
|
||||
// If we don't have a path yet then do nothing; this is to avoid stutter-stepping if it turns out there's no path
|
||||
// available but we assume there was.
|
||||
@@ -295,8 +329,10 @@ public sealed partial class NPCSteeringSystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (!needsPath)
|
||||
if (!needsPath && steering.CurrentPath.Count > 0)
|
||||
{
|
||||
needsPath = steering.CurrentPath.Count > 0 && (steering.CurrentPath.Peek().Data.Flags & PathfindingBreadcrumbFlag.Invalid) != 0x0;
|
||||
|
||||
// If the target has sufficiently moved.
|
||||
var lastNode = GetCoordinates(steering.CurrentPath.Last());
|
||||
|
||||
@@ -357,10 +393,6 @@ public sealed partial class NPCSteeringSystem
|
||||
mask = (CollisionGroup) physics.CollisionMask;
|
||||
}
|
||||
|
||||
// If we have to backtrack (for example, we're behind a table and the target is on the other side)
|
||||
// Then don't consider pruning.
|
||||
var goal = nodes.Last().Coordinates.ToMap(EntityManager, _transform);
|
||||
|
||||
for (var i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
var node = nodes[i];
|
||||
@@ -451,7 +483,9 @@ public sealed partial class NPCSteeringSystem
|
||||
|
||||
var xformB = _xformQuery.GetComponent(ent);
|
||||
|
||||
if (!_physics.TryGetNearest(uid, ent, out var pointA, out var pointB, out var distance, xform, xformB))
|
||||
if (!_physics.TryGetNearest(uid, ent,
|
||||
out var pointA, out var pointB, out var distance,
|
||||
xform, xformB))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -508,8 +542,7 @@ public sealed partial class NPCSteeringSystem
|
||||
var objectRadius = 0.25f;
|
||||
var detectionRadius = MathF.Max(0.35f, agentRadius + objectRadius);
|
||||
var ourVelocity = body.LinearVelocity;
|
||||
var factionQuery = GetEntityQuery<NpcFactionMemberComponent>();
|
||||
factionQuery.TryGetComponent(uid, out var ourFaction);
|
||||
_factionQuery.TryGetComponent(uid, out var ourFaction);
|
||||
|
||||
foreach (var ent in _lookup.GetEntitiesInRange(uid, detectionRadius, LookupFlags.Dynamic))
|
||||
{
|
||||
@@ -520,7 +553,7 @@ public sealed partial class NPCSteeringSystem
|
||||
!otherBody.CanCollide ||
|
||||
(mask & otherBody.CollisionLayer) == 0x0 &&
|
||||
(layer & otherBody.CollisionMask) == 0x0 ||
|
||||
!factionQuery.TryGetComponent(ent, out var otherFaction) ||
|
||||
!_factionQuery.TryGetComponent(ent, out var otherFaction) ||
|
||||
!_npcFaction.IsEntityFriendly(uid, ent, ourFaction, otherFaction) ||
|
||||
// Use <= 0 so we ignore stationary friends in case.
|
||||
Vector2.Dot(otherBody.LinearVelocity, ourVelocity) <= 0f)
|
||||
|
||||
@@ -65,8 +65,9 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
|
||||
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
|
||||
|
||||
private EntityQuery<FixturesComponent> _fixturesQuery;
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
private EntityQuery<MovementSpeedModifierComponent> _modifierQuery;
|
||||
private EntityQuery<NpcFactionMemberComponent> _factionQuery;
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
|
||||
/// <summary>
|
||||
@@ -89,8 +90,9 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
|
||||
base.Initialize();
|
||||
|
||||
_fixturesQuery = GetEntityQuery<FixturesComponent>();
|
||||
_physicsQuery = GetEntityQuery<PhysicsComponent>();
|
||||
_modifierQuery = GetEntityQuery<MovementSpeedModifierComponent>();
|
||||
_factionQuery = GetEntityQuery<NpcFactionMemberComponent>();
|
||||
_physicsQuery = GetEntityQuery<PhysicsComponent>();
|
||||
_xformQuery = GetEntityQuery<TransformComponent>();
|
||||
|
||||
#if DEBUG
|
||||
@@ -238,8 +240,16 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
|
||||
return;
|
||||
|
||||
// Not every mob has the modifier component so do it as a separate query.
|
||||
var npcs = EntityQuery<ActiveNPCComponent, NPCSteeringComponent, InputMoverComponent, TransformComponent>()
|
||||
.Select(o => (o.Item1.Owner, o.Item2, o.Item3, o.Item4)).ToArray();
|
||||
var npcs = new (EntityUid, NPCSteeringComponent, InputMoverComponent, TransformComponent)[Count<ActiveNPCComponent>()];
|
||||
|
||||
var query = EntityQueryEnumerator<ActiveNPCComponent, NPCSteeringComponent, InputMoverComponent, TransformComponent>();
|
||||
var index = 0;
|
||||
|
||||
while (query.MoveNext(out var uid, out _, out var steering, out var mover, out var xform))
|
||||
{
|
||||
npcs[index] = (uid, steering, mover, xform);
|
||||
index++;
|
||||
}
|
||||
|
||||
// Dependency issues across threads.
|
||||
var options = new ParallelOptions
|
||||
@@ -248,7 +258,7 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
|
||||
};
|
||||
var curTime = _timing.CurTime;
|
||||
|
||||
Parallel.For(0, npcs.Length, options, i =>
|
||||
Parallel.For(0, index, options, i =>
|
||||
{
|
||||
var (uid, steering, mover, xform) = npcs[i];
|
||||
Steer(uid, steering, mover, xform, frameTime, curTime);
|
||||
@@ -257,10 +267,12 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
|
||||
|
||||
if (_subscribedSessions.Count > 0)
|
||||
{
|
||||
var data = new List<NPCSteeringDebugData>(npcs.Length);
|
||||
var data = new List<NPCSteeringDebugData>(index);
|
||||
|
||||
foreach (var (uid, steering, mover, _) in npcs)
|
||||
for (var i = 0; i < index; i++)
|
||||
{
|
||||
var (uid, steering, mover, _) = npcs[i];
|
||||
|
||||
data.Add(new NPCSteeringDebugData(
|
||||
uid,
|
||||
mover.CurTickSprintMovement,
|
||||
@@ -341,7 +353,9 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
|
||||
steering.Danger[i] = 0f;
|
||||
}
|
||||
|
||||
var ev = new NPCSteeringEvent(steering, interest, danger, agentRadius, offsetRot, worldPos);
|
||||
steering.CanSeek = true;
|
||||
|
||||
var ev = new NPCSteeringEvent(steering, xform, worldPos, offsetRot);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
// If seek has arrived at the target node for example then immediately re-steer.
|
||||
var forceSteer = true;
|
||||
|
||||
@@ -19,8 +19,6 @@ namespace Content.Server.NPC.Systems
|
||||
[Dependency] private readonly HTNSystem _htn = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether any NPCs are allowed to run at all.
|
||||
/// </summary>
|
||||
@@ -35,8 +33,6 @@ namespace Content.Server.NPC.Systems
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_sawmill = Logger.GetSawmill("npc");
|
||||
_sawmill.Level = LogLevel.Info;
|
||||
SubscribeLocalEvent<NPCComponent, MobStateChangedEvent>(OnMobStateChange);
|
||||
SubscribeLocalEvent<NPCComponent, MapInitEvent>(OnNPCMapInit);
|
||||
SubscribeLocalEvent<NPCComponent, ComponentShutdown>(OnNPCShutdown);
|
||||
@@ -98,7 +94,7 @@ namespace Content.Server.NPC.Systems
|
||||
return;
|
||||
}
|
||||
|
||||
_sawmill.Debug($"Waking {ToPrettyString(uid)}");
|
||||
Log.Debug($"Waking {ToPrettyString(uid)}");
|
||||
EnsureComp<ActiveNPCComponent>(uid);
|
||||
}
|
||||
|
||||
@@ -109,7 +105,19 @@ namespace Content.Server.NPC.Systems
|
||||
return;
|
||||
}
|
||||
|
||||
_sawmill.Debug($"Sleeping {ToPrettyString(uid)}");
|
||||
// Don't bother with an event
|
||||
if (TryComp<HTNComponent>(uid, out var htn))
|
||||
{
|
||||
if (htn.Plan != null)
|
||||
{
|
||||
var currentOperator = htn.Plan.CurrentOperator;
|
||||
_htn.ShutdownTask(currentOperator, htn.Blackboard, HTNOperatorStatus.Failed);
|
||||
_htn.ShutdownPlan(htn);
|
||||
htn.Plan = null;
|
||||
}
|
||||
}
|
||||
|
||||
Log.Debug($"Sleeping {ToPrettyString(uid)}");
|
||||
RemComp<ActiveNPCComponent>(uid);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,18 @@ using Content.Server.Nutrition.EntitySystems;
|
||||
using Content.Server.Storage.Components;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Fluids.Components;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Weapons.Melee;
|
||||
using Content.Shared.Weapons.Ranged.Components;
|
||||
using Content.Shared.Weapons.Ranged.Events;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.NPC.Systems;
|
||||
|
||||
@@ -24,17 +31,29 @@ namespace Content.Server.NPC.Systems;
|
||||
/// </summary>
|
||||
public sealed class NPCUtilitySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly ContainerSystem _container = default!;
|
||||
[Dependency] private readonly DrinkSystem _drink = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly FoodSystem _food = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
|
||||
[Dependency] private readonly PuddleSystem _puddle = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SolutionContainerSystem _solutions = default!;
|
||||
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
|
||||
private ObjectPool<HashSet<EntityUid>> _entPool =
|
||||
new DefaultObjectPool<HashSet<EntityUid>>(new SetPolicy<EntityUid>(), 256);
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_xformQuery = GetEntityQuery<TransformComponent>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the UtilityQueryPrototype and returns the best-matching entities.
|
||||
/// </summary>
|
||||
@@ -47,7 +66,7 @@ public sealed class NPCUtilitySystem : EntitySystem
|
||||
// TODO: PickHostilesop or whatever needs to juse be UtilityQueryOperator
|
||||
|
||||
var weh = _proto.Index<UtilityQueryPrototype>(proto);
|
||||
var ents = new HashSet<EntityUid>();
|
||||
var ents = _entPool.Get();
|
||||
|
||||
foreach (var query in weh.Query)
|
||||
{
|
||||
@@ -63,7 +82,10 @@ public sealed class NPCUtilitySystem : EntitySystem
|
||||
}
|
||||
|
||||
if (ents.Count == 0)
|
||||
{
|
||||
_entPool.Return(ents);
|
||||
return UtilityResult.Empty;
|
||||
}
|
||||
|
||||
var results = new Dictionary<EntityUid, float>();
|
||||
var highestScore = 0f;
|
||||
@@ -101,6 +123,7 @@ public sealed class NPCUtilitySystem : EntitySystem
|
||||
|
||||
var result = new UtilityResult(results);
|
||||
blackboard.Remove<EntityUid>(NPCBlackboard.UtilityTarget);
|
||||
_entPool.Return(ents);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -115,7 +138,7 @@ public sealed class NPCUtilitySystem : EntitySystem
|
||||
case PresetCurve presetCurve:
|
||||
return GetScore(_proto.Index<UtilityCurvePresetPrototype>(presetCurve.Preset).Curve, conScore);
|
||||
case QuadraticCurve quadraticCurve:
|
||||
return Math.Clamp(quadraticCurve.Slope * (float) Math.Pow(conScore - quadraticCurve.XOffset, quadraticCurve.Exponent) + quadraticCurve.YOffset, 0f, 1f);
|
||||
return Math.Clamp(quadraticCurve.Slope * MathF.Pow(conScore - quadraticCurve.XOffset, quadraticCurve.Exponent) + quadraticCurve.YOffset, 0f, 1f);
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
@@ -189,6 +212,21 @@ public sealed class NPCUtilitySystem : EntitySystem
|
||||
// TODO: Pathfind there, though probably do it in a separate con.
|
||||
return 1f;
|
||||
}
|
||||
case TargetAmmoMatchesCon:
|
||||
{
|
||||
if (!blackboard.TryGetValue(NPCBlackboard.ActiveHand, out Hand? activeHand, EntityManager) ||
|
||||
!TryComp<BallisticAmmoProviderComponent>(activeHand.HeldEntity, out var heldGun))
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
if (heldGun.Whitelist?.IsValid(targetUid, EntityManager) != true)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
return 1f;
|
||||
}
|
||||
case TargetDistanceCon:
|
||||
{
|
||||
var radius = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntityManager);
|
||||
@@ -207,6 +245,23 @@ public sealed class NPCUtilitySystem : EntitySystem
|
||||
|
||||
return Math.Clamp(distance / radius, 0f, 1f);
|
||||
}
|
||||
case TargetAmmoCon:
|
||||
{
|
||||
if (!HasComp<GunComponent>(targetUid))
|
||||
return 0f;
|
||||
|
||||
var ev = new GetAmmoCountEvent();
|
||||
RaiseLocalEvent(targetUid, ref ev);
|
||||
|
||||
if (ev.Count == 0)
|
||||
return 0f;
|
||||
|
||||
// Wat
|
||||
if (ev.Capacity == 0)
|
||||
return 1f;
|
||||
|
||||
return (float) ev.Count / ev.Capacity;
|
||||
}
|
||||
case TargetHealthCon:
|
||||
{
|
||||
return 0f;
|
||||
@@ -222,7 +277,7 @@ public sealed class NPCUtilitySystem : EntitySystem
|
||||
var radius = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntityManager);
|
||||
const float bufferRange = 0.5f;
|
||||
|
||||
if (blackboard.TryGetValue<EntityUid>("CombatTarget", out var currentTarget, EntityManager) &&
|
||||
if (blackboard.TryGetValue<EntityUid>("Target", out var currentTarget, EntityManager) &&
|
||||
currentTarget == targetUid &&
|
||||
TryComp<TransformComponent>(owner, out var xform) &&
|
||||
TryComp<TransformComponent>(targetUid, out var targetXform) &&
|
||||
@@ -246,6 +301,15 @@ public sealed class NPCUtilitySystem : EntitySystem
|
||||
{
|
||||
return _mobState.IsDead(targetUid) ? 1f : 0f;
|
||||
}
|
||||
case TargetMeleeCon:
|
||||
{
|
||||
if (TryComp<MeleeWeaponComponent>(targetUid, out var melee))
|
||||
{
|
||||
return melee.Damage.Total.Float() * melee.AttackRate / 100f;
|
||||
}
|
||||
|
||||
return 0f;
|
||||
}
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
@@ -275,40 +339,109 @@ public sealed class NPCUtilitySystem : EntitySystem
|
||||
switch (query)
|
||||
{
|
||||
case ComponentQuery compQuery:
|
||||
var mapPos = Transform(owner).MapPosition;
|
||||
foreach (var compReg in compQuery.Components.Values)
|
||||
{
|
||||
foreach (var comp in _lookup.GetComponentsInRange(compReg.Component.GetType(), mapPos, vision))
|
||||
var mapPos = Transform(owner).MapPosition;
|
||||
var comps = compQuery.Components.Values.ToList();
|
||||
var compZero = comps[0];
|
||||
comps.RemoveAt(0);
|
||||
|
||||
foreach (var comp in _lookup.GetComponentsInRange(compZero.Component.GetType(), mapPos, vision))
|
||||
{
|
||||
var ent = comp.Owner;
|
||||
|
||||
if (ent == owner)
|
||||
continue;
|
||||
|
||||
var othersFound = true;
|
||||
|
||||
foreach (var compOther in comps)
|
||||
{
|
||||
if (!HasComp(ent, compOther.Component.GetType()))
|
||||
{
|
||||
othersFound = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!othersFound)
|
||||
continue;
|
||||
|
||||
entities.Add(ent);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case InventoryQuery:
|
||||
{
|
||||
if (!_inventory.TryGetContainerSlotEnumerator(owner, out var enumerator))
|
||||
break;
|
||||
|
||||
while (enumerator.MoveNext(out var slot))
|
||||
{
|
||||
foreach (var child in slot.ContainedEntities)
|
||||
{
|
||||
RecursiveAdd(child, entities);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case NearbyHostilesQuery:
|
||||
{
|
||||
foreach (var ent in _npcFaction.GetNearbyHostiles(owner, vision))
|
||||
{
|
||||
entities.Add(ent);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
private void RecursiveAdd(EntityUid uid, HashSet<EntityUid> entities)
|
||||
{
|
||||
// TODO: Probably need a recursive struct enumerator on engine.
|
||||
var xform = _xformQuery.GetComponent(uid);
|
||||
var enumerator = xform.ChildEnumerator;
|
||||
entities.Add(uid);
|
||||
|
||||
while (enumerator.MoveNext(out var child))
|
||||
{
|
||||
RecursiveAdd(child.Value, entities);
|
||||
}
|
||||
}
|
||||
|
||||
private void Filter(NPCBlackboard blackboard, HashSet<EntityUid> entities, UtilityQueryFilter filter)
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case ComponentFilter compFilter:
|
||||
{
|
||||
var toRemove = new ValueList<EntityUid>();
|
||||
|
||||
foreach (var ent in entities)
|
||||
{
|
||||
foreach (var comp in compFilter.Components)
|
||||
{
|
||||
if (HasComp(ent, comp.Value.Component.GetType()))
|
||||
continue;
|
||||
|
||||
toRemove.Add(ent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var ent in toRemove)
|
||||
{
|
||||
entities.Remove(ent);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case PuddleFilter:
|
||||
{
|
||||
var puddleQuery = GetEntityQuery<PuddleComponent>();
|
||||
|
||||
var toRemove = new ValueList<EntityUid>();
|
||||
|
||||
foreach (var ent in entities)
|
||||
|
||||
@@ -232,7 +232,7 @@ namespace Content.Server.Zombies
|
||||
else
|
||||
{
|
||||
var htn = EnsureComp<HTNComponent>(target);
|
||||
htn.RootTask = "SimpleHostileCompound";
|
||||
htn.RootTask = new HTNCompoundTask() {Task = "SimpleHostileCompound"};
|
||||
htn.Blackboard.SetValue(NPCBlackboard.Owner, target);
|
||||
_npc.WakeNPC(target, htn);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Inventory;
|
||||
|
||||
@@ -15,6 +15,7 @@ public abstract partial class SharedGunSystem
|
||||
protected virtual void InitializeChamberMagazine()
|
||||
{
|
||||
SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, TakeAmmoEvent>(OnChamberMagazineTakeAmmo);
|
||||
SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, GetAmmoCountEvent>(OnChamberAmmoCount);
|
||||
SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, GetVerbsEvent<AlternativeVerb>>(OnMagazineVerb);
|
||||
SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, EntInsertedIntoContainerMessage>(OnMagazineSlotChange);
|
||||
SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, EntRemovedFromContainerMessage>(OnMagazineSlotChange);
|
||||
@@ -73,6 +74,18 @@ public abstract partial class SharedGunSystem
|
||||
slot.Insert(ammo);
|
||||
}
|
||||
|
||||
private void OnChamberAmmoCount(EntityUid uid, ChamberMagazineAmmoProviderComponent component, ref GetAmmoCountEvent args)
|
||||
{
|
||||
OnMagazineAmmoCount(uid, component, ref args);
|
||||
args.Capacity += 1;
|
||||
var chambered = GetChamberEntity(uid);
|
||||
|
||||
if (chambered != null)
|
||||
{
|
||||
args.Count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChamberMagazineTakeAmmo(EntityUid uid, ChamberMagazineAmmoProviderComponent component, TakeAmmoEvent args)
|
||||
{
|
||||
// So chamber logic is kinda sussier than the others
|
||||
|
||||
@@ -14,6 +14,7 @@ public abstract partial class SharedGunSystem
|
||||
protected virtual void InitializeMagazine()
|
||||
{
|
||||
SubscribeLocalEvent<MagazineAmmoProviderComponent, TakeAmmoEvent>(OnMagazineTakeAmmo);
|
||||
SubscribeLocalEvent<MagazineAmmoProviderComponent, GetAmmoCountEvent>(OnMagazineAmmoCount);
|
||||
SubscribeLocalEvent<MagazineAmmoProviderComponent, GetVerbsEvent<AlternativeVerb>>(OnMagazineVerb);
|
||||
SubscribeLocalEvent<MagazineAmmoProviderComponent, EntInsertedIntoContainerMessage>(OnMagazineSlotChange);
|
||||
SubscribeLocalEvent<MagazineAmmoProviderComponent, EntRemovedFromContainerMessage>(OnMagazineSlotChange);
|
||||
@@ -96,6 +97,16 @@ public abstract partial class SharedGunSystem
|
||||
return slot.ContainedEntity;
|
||||
}
|
||||
|
||||
private void OnMagazineAmmoCount(EntityUid uid, MagazineAmmoProviderComponent component, ref GetAmmoCountEvent args)
|
||||
{
|
||||
var magEntity = GetMagazineEntity(uid);
|
||||
|
||||
if (magEntity == null)
|
||||
return;
|
||||
|
||||
RaiseLocalEvent(magEntity.Value, ref args);
|
||||
}
|
||||
|
||||
private void OnMagazineTakeAmmo(EntityUid uid, MagazineAmmoProviderComponent component, TakeAmmoEvent args)
|
||||
{
|
||||
var magEntity = GetMagazineEntity(uid);
|
||||
|
||||
@@ -133,7 +133,8 @@
|
||||
- type: InputMover
|
||||
- type: MobMover
|
||||
- type: HTN
|
||||
rootTask: SimpleHostileCompound
|
||||
rootTask:
|
||||
task: SimpleHostileCompound
|
||||
- type: NpcFactionMember
|
||||
factions:
|
||||
- SimpleHostile
|
||||
@@ -470,7 +471,8 @@
|
||||
- type: Body
|
||||
prototype: AnimalRuminant
|
||||
- type: HTN
|
||||
rootTask: RuminantCompound
|
||||
rootTask:
|
||||
task: RuminantCompound
|
||||
- type: GuideHelp
|
||||
guides:
|
||||
- Chef
|
||||
@@ -588,7 +590,8 @@
|
||||
- type: Body
|
||||
prototype: AnimalRuminant
|
||||
- type: HTN
|
||||
rootTask: RuminantCompound
|
||||
rootTask:
|
||||
task: RuminantCompound
|
||||
|
||||
# Note that we gotta make this bitch vomit someday when you feed it anthrax or sumthin. Needs to be a small item thief too and aggressive if attacked.
|
||||
- type: entity
|
||||
@@ -763,7 +766,8 @@
|
||||
- type: Loadout
|
||||
prototypes: [ BoxingKangarooGear ]
|
||||
- type: HTN
|
||||
rootTask: SimpleHostileCompound
|
||||
rootTask:
|
||||
task: SimpleHostileCompound
|
||||
- type: NpcFactionMember
|
||||
factions:
|
||||
- SimpleHostile
|
||||
@@ -906,7 +910,8 @@
|
||||
factions:
|
||||
- Mouse
|
||||
- type: HTN
|
||||
rootTask: MouseCompound
|
||||
rootTask:
|
||||
task: MouseCompound
|
||||
- type: Physics
|
||||
- type: Fixtures
|
||||
fixtures:
|
||||
@@ -1286,7 +1291,8 @@
|
||||
- type: InputMover
|
||||
- type: MobMover
|
||||
- type: HTN
|
||||
rootTask: SimpleHostileCompound
|
||||
rootTask:
|
||||
task: SimpleHostileCompound
|
||||
- type: NpcFactionMember
|
||||
factions:
|
||||
- Syndicate
|
||||
@@ -1562,7 +1568,8 @@
|
||||
- type: InputMover
|
||||
- type: MobMover
|
||||
- type: HTN
|
||||
rootTask: SimpleHostileCompound
|
||||
rootTask:
|
||||
task: SimpleHostileCompound
|
||||
- type: GhostRole
|
||||
makeSentient: true
|
||||
name: ghost-role-information-giant-spider-name
|
||||
@@ -1866,7 +1873,8 @@
|
||||
- type: InputMover
|
||||
- type: MobMover
|
||||
- type: HTN
|
||||
rootTask: SimpleHostileCompound
|
||||
rootTask:
|
||||
task: SimpleHostileCompound
|
||||
- type: NpcFactionMember
|
||||
factions:
|
||||
- SimpleHostile
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
description: ghost-role-information-behonker-description
|
||||
- type: GhostTakeoverAvailable
|
||||
- type: HTN
|
||||
rootTask: SimpleHostileCompound
|
||||
rootTask:
|
||||
task: SimpleHostileCompound
|
||||
- type: NpcFactionMember
|
||||
factions:
|
||||
- SimpleHostile
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
- type: InputMover
|
||||
- type: MobMover
|
||||
- type: HTN
|
||||
rootTask: SimpleHostileCompound
|
||||
rootTask:
|
||||
task: SimpleHostileCompound
|
||||
blackboard:
|
||||
NavSmash: !type:Bool
|
||||
true
|
||||
@@ -20,7 +21,7 @@
|
||||
sprite: Mobs/Aliens/Carps/space.rsi
|
||||
layers:
|
||||
- map: [ "enum.DamageStateVisualLayers.Base" ]
|
||||
state: base
|
||||
state: alive
|
||||
- type: CombatMode
|
||||
- type: Physics
|
||||
- type: Fixtures
|
||||
@@ -45,10 +46,10 @@
|
||||
- type: DamageStateVisuals
|
||||
states:
|
||||
Alive:
|
||||
Base: base
|
||||
Base: alive
|
||||
BaseUnshaded: mouth
|
||||
Dead:
|
||||
Base: base_dead
|
||||
Base: dead
|
||||
BaseUnshaded: dead_mouth
|
||||
- type: Butcherable
|
||||
spawned:
|
||||
@@ -82,14 +83,14 @@
|
||||
- type: Sprite
|
||||
layers:
|
||||
- map: [ "enum.DamageStateVisualLayers.Base" ]
|
||||
state: base
|
||||
state: alive
|
||||
- map: [ "enum.DamageStateVisualLayers.BaseUnshaded" ]
|
||||
state: mouth
|
||||
shader: unshaded
|
||||
- type: RandomSprite
|
||||
available:
|
||||
- enum.DamageStateVisualLayers.Base:
|
||||
base: Rainbow
|
||||
alive: Rainbow
|
||||
enum.DamageStateVisualLayers.BaseUnshaded:
|
||||
mouth: ""
|
||||
|
||||
@@ -145,7 +146,7 @@
|
||||
name: space carp
|
||||
id: MobCarpDragon
|
||||
suffix: DragonBrood
|
||||
parent: BaseMobCarp
|
||||
parent: MobCarp
|
||||
components:
|
||||
- type: GhostRole
|
||||
allowMovement: true
|
||||
@@ -155,7 +156,8 @@
|
||||
description: ghost-role-information-sentient-carp-description
|
||||
- type: GhostTakeoverAvailable
|
||||
- type: HTN
|
||||
rootTask: DragonCarpCompound
|
||||
rootTask:
|
||||
task: DragonCarpCompound
|
||||
|
||||
- type: entity
|
||||
id: MobCarpDungeon
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
abstract: true
|
||||
components:
|
||||
- type: HTN
|
||||
rootTask: SimpleHostileCompound
|
||||
rootTask:
|
||||
task: SimpleHostileCompound
|
||||
blackboard:
|
||||
NavSmash: !type:Bool
|
||||
true
|
||||
|
||||
@@ -12,6 +12,23 @@
|
||||
factions:
|
||||
- NanoTrasen
|
||||
|
||||
- type: entity
|
||||
name: Salvager
|
||||
parent: BaseMobHuman
|
||||
id: MobSalvager
|
||||
components:
|
||||
- type: NpcFactionMember
|
||||
factions:
|
||||
- Syndicate
|
||||
- type: Loadout
|
||||
prototypes:
|
||||
- SalvageSpecialistGear
|
||||
- type: InputMover
|
||||
- type: MobMover
|
||||
- type: HTN
|
||||
rootTask:
|
||||
task: SimpleHumanoidHostileCompound
|
||||
|
||||
- type: entity
|
||||
name: Spirate
|
||||
parent: BaseMobHuman
|
||||
@@ -21,10 +38,14 @@
|
||||
- type: NpcFactionMember
|
||||
factions:
|
||||
- Syndicate
|
||||
- type: Loadout
|
||||
prototypes:
|
||||
- PirateGear
|
||||
- type: InputMover
|
||||
- type: MobMover
|
||||
- type: HTN
|
||||
rootTask: RangedCombatCompound
|
||||
rootTask:
|
||||
task: SimpleHumanoidHostileCompound
|
||||
|
||||
- type: entity
|
||||
parent: BaseMobHuman
|
||||
|
||||
@@ -90,7 +90,8 @@
|
||||
factions:
|
||||
- PetsNT
|
||||
- type: HTN
|
||||
rootTask: SimpleHostileCompound
|
||||
rootTask:
|
||||
task: SimpleHostileCompound
|
||||
- type: Grammar
|
||||
attributes:
|
||||
gender: female
|
||||
@@ -487,7 +488,8 @@
|
||||
state: shiva
|
||||
sprite: Mobs/Pets/shiva.rsi
|
||||
- type: HTN
|
||||
rootTask: SimpleHostileCompound
|
||||
rootTask:
|
||||
task: SimpleHostileCompound
|
||||
- type: Physics
|
||||
- type: Fixtures
|
||||
fixtures:
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
- type: InputMover
|
||||
- type: MobMover
|
||||
- type: HTN
|
||||
rootTask: SimpleHostileCompound
|
||||
rootTask:
|
||||
task: SimpleHostileCompound
|
||||
- type: Reactive
|
||||
groups:
|
||||
Flammable: [Touch]
|
||||
@@ -176,7 +177,8 @@
|
||||
- type: InputMover
|
||||
- type: MobMover
|
||||
- type: HTN
|
||||
rootTask: SimpleHostileCompound
|
||||
rootTask:
|
||||
task: SimpleHostileCompound
|
||||
- type: Reactive
|
||||
groups:
|
||||
Flammable: [Touch]
|
||||
|
||||
@@ -227,7 +227,8 @@
|
||||
baseSprintSpeed: 3
|
||||
- type: NoSlip
|
||||
- type: HTN
|
||||
rootTask: CleanbotCompound
|
||||
rootTask:
|
||||
task: CleanbotCompound
|
||||
- type: DrainableSolution
|
||||
solution: drainBuffer
|
||||
- type: InteractionPopup
|
||||
@@ -256,7 +257,8 @@
|
||||
state: medibot
|
||||
- type: Speech
|
||||
- type: HTN
|
||||
rootTask: MedibotCompound
|
||||
rootTask:
|
||||
task: MedibotCompound
|
||||
- type: Construction
|
||||
graph: MediBot
|
||||
node: bot
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
- type: InputMover
|
||||
- type: MobMover
|
||||
- type: HTN
|
||||
rootTask: IdleCompound
|
||||
rootTask:
|
||||
task: IdleCompound
|
||||
- type: Input
|
||||
context: "human"
|
||||
- type: NpcFactionMember
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
factions:
|
||||
- SimpleNeutral
|
||||
- type: HTN
|
||||
rootTask: SimpleHostileCompound
|
||||
rootTask:
|
||||
task: SimpleHostileCompound
|
||||
- type: Sprite
|
||||
drawdepth: Mobs
|
||||
sprite: Mobs/Aliens/slimes.rsi
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
description: It looks friendly. Why don't you give it a hug?
|
||||
components:
|
||||
- type: HTN
|
||||
rootTask: SimpleHostileCompound
|
||||
rootTask:
|
||||
task: SimpleHostileCompound
|
||||
- type: NpcFactionMember
|
||||
factions:
|
||||
- SimpleHostile
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
- type: InputMover
|
||||
- type: MobMover
|
||||
- type: HTN
|
||||
rootTask: SimpleHostileCompound
|
||||
rootTask:
|
||||
task: SimpleHostileCompound
|
||||
- type: NpcFactionMember
|
||||
factions:
|
||||
- SimpleHostile
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
- type: InputMover
|
||||
- type: MobMover
|
||||
- type: HTN
|
||||
rootTask: XenoCompound
|
||||
rootTask:
|
||||
task: XenoCompound
|
||||
blackboard:
|
||||
NavInteract: !type:Bool
|
||||
true
|
||||
@@ -330,8 +331,24 @@
|
||||
thresholds:
|
||||
0: Alive
|
||||
75: Dead
|
||||
- type: HTN
|
||||
rootTask:
|
||||
task: SimpleRangedHostileCompound
|
||||
- type: Stamina
|
||||
excess: 300
|
||||
- type: RechargeBasicEntityAmmo
|
||||
rechargeCooldown: 0.75
|
||||
- type: BasicEntityAmmoProvider
|
||||
proto: BulletAcid
|
||||
capacity: 1
|
||||
count: 1
|
||||
- type: Gun
|
||||
fireRate: 0.75
|
||||
useKey: false
|
||||
selectedMode: FullAuto
|
||||
availableModes:
|
||||
- FullAuto
|
||||
soundGunshot: /Audio/Weapons/Xeno/alien_spitacid.ogg
|
||||
- type: SlowOnDamage
|
||||
speedModifierThresholds:
|
||||
50: 0.4
|
||||
@@ -374,7 +391,8 @@
|
||||
- type: InputMover
|
||||
- type: MobMover
|
||||
- type: HTN
|
||||
rootTask: SimpleHostileCompound
|
||||
rootTask:
|
||||
task: SimpleHostileCompound
|
||||
- type: NpcFactionMember
|
||||
factions:
|
||||
- Xeno
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
description: ghost-role-information-space-dragon-description
|
||||
- type: GhostTakeoverAvailable
|
||||
- type: HTN
|
||||
rootTask: XenoCompound
|
||||
rootTask:
|
||||
task: XenoCompound
|
||||
blackboard:
|
||||
NavInteract: !type:Bool
|
||||
true
|
||||
|
||||
@@ -94,9 +94,6 @@
|
||||
- type: InventorySlots
|
||||
- type: Clickable
|
||||
- type: InteractionOutline
|
||||
- type: Icon
|
||||
sprite: Mobs/Species/Human/parts.rsi
|
||||
state: full
|
||||
- type: Sprite
|
||||
noRot: true
|
||||
drawdepth: Mobs
|
||||
|
||||
@@ -106,7 +106,8 @@
|
||||
proto: CartridgeCaselessRifle
|
||||
capacity: 500
|
||||
- type: HTN
|
||||
rootTask: TurretCompound
|
||||
rootTask:
|
||||
task: TurretCompound
|
||||
blackboard:
|
||||
SoundTargetInLOS: !type:SoundPathSpecifier
|
||||
path: /Audio/Effects/double_beep.ogg
|
||||
@@ -211,7 +212,8 @@
|
||||
selectedMode: FullAuto
|
||||
soundGunshot: /Audio/Weapons/Xeno/alien_spitacid.ogg
|
||||
- type: HTN
|
||||
rootTask: TurretCompound
|
||||
rootTask:
|
||||
task: TurretCompound
|
||||
blackboard:
|
||||
SoundTargetInLOS: !type:SoundPathSpecifier
|
||||
path: /Audio/Animals/snake_hiss.ogg
|
||||
|
||||
158
Resources/Prototypes/NPCs/Combat/gun.yml
Normal file
@@ -0,0 +1,158 @@
|
||||
# Tries to shoot a target at range.
|
||||
- type: htnCompound
|
||||
id: GunCombatCompound
|
||||
branches:
|
||||
# Pick target, then move into range and shoot them.
|
||||
- tasks:
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:UtilityOperator
|
||||
proto: NearbyGunTargets
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:MoveToOperator
|
||||
shutdownState: PlanFinished
|
||||
pathfindInPlanning: true
|
||||
removeKeyOnFinish: false
|
||||
targetKey: TargetCoordinates
|
||||
pathfindKey: TargetPathfind
|
||||
stopOnLineOfSight: true
|
||||
rangeKey: MeleeRange
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:JukeOperator
|
||||
jukeType: AdjacentTile
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
preconditions:
|
||||
- !type:KeyExistsPrecondition
|
||||
key: Target
|
||||
operator: !type:GunOperator
|
||||
targetKey: Target
|
||||
services:
|
||||
- !type:UtilityService
|
||||
id: RangedService
|
||||
proto: NearbyGunTargets
|
||||
key: Target
|
||||
|
||||
# Selects ammo in range, then moves to it and picks it up
|
||||
- type: htnCompound
|
||||
id: PickupAmmoCompound
|
||||
branches:
|
||||
# Find ammo then pick it up.
|
||||
- tasks:
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:UtilityOperator
|
||||
proto: NearbyAmmo
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:MoveToOperator
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
preconditions:
|
||||
- !type:TargetInRangePrecondition
|
||||
targetKey: Target
|
||||
rangeKey: InteractRange
|
||||
operator: !type:InteractWithOperator
|
||||
targetKey: Target
|
||||
# TODO: Prioritise ammo for weapon we have equipped, otherwise grab anything if we don't have any.
|
||||
# TODO: Only works on ballistic
|
||||
|
||||
# Selects a gun in range, then moves to it and picks it up.
|
||||
- type: htnCompound
|
||||
id: PickupGunCompound
|
||||
branches:
|
||||
- tasks:
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:UtilityOperator
|
||||
proto: NearbyGuns
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:MoveToOperator
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
preconditions:
|
||||
- !type:TargetInRangePrecondition
|
||||
targetKey: Target
|
||||
rangeKey: InteractRange
|
||||
operator: !type:InteractWithOperator
|
||||
targetKey: Target
|
||||
|
||||
# TODO: Need a thing to recharge a laser gun
|
||||
# TODO: When selecting pickup guns also add chargers or easy container grabs.
|
||||
|
||||
# Shorted version of RangedCombatCompound for entities that are guns themselves.
|
||||
- type: htnCompound
|
||||
id: InnateRangedCombatCompound
|
||||
branches:
|
||||
- preconditions:
|
||||
- !type:GunAmmoPrecondition
|
||||
minPercent: 0.001
|
||||
tasks:
|
||||
- !type:HTNCompoundTask
|
||||
task: GunCombatCompound
|
||||
|
||||
|
||||
- type: htnCompound
|
||||
id: RangedCombatCompound
|
||||
branches:
|
||||
# Move to target and shoot them if ammo
|
||||
- preconditions:
|
||||
- !type:GunAmmoPrecondition
|
||||
minPercent: 0.001
|
||||
tasks:
|
||||
- !type:HTNCompoundTask
|
||||
task: GunCombatCompound
|
||||
|
||||
# Reload gun
|
||||
# TODO
|
||||
|
||||
# Equip a gun from inventory if one found, preferring over pickup.
|
||||
# TODO: Doing inventory cleanly will be a PITA so deferring to later
|
||||
# The issue is recursively checking items but also ignoring some recursive entities
|
||||
# i.e. we need to recursively go into storage containers.
|
||||
#- tasks:
|
||||
# - !type:HTNCompoundTask
|
||||
# task: ClearActiveHandCompound
|
||||
#
|
||||
# - !type:HTNPrimitiveTask
|
||||
# operator: !type:UtilityOperator
|
||||
# proto: InventoryGuns
|
||||
#
|
||||
# - !type:HTNPrimitiveTask
|
||||
# operator: !type:EquipOperator
|
||||
|
||||
# Pickup ammo if any nearby
|
||||
#- preconditions:
|
||||
# - !type:GunAmmoPrecondition
|
||||
# maxPercent: 0.0
|
||||
# tasks:
|
||||
# - !type:HTNCompoundTask
|
||||
# task: ClearActiveHandCompound
|
||||
#
|
||||
# - !type:HTNCompoundTask
|
||||
# task: PickupAmmoCompound
|
||||
|
||||
# Pickup gun with ammo if we have no ammo
|
||||
- preconditions:
|
||||
- !type:ActiveHandComponentPrecondition
|
||||
components:
|
||||
- type: Gun
|
||||
invert: true
|
||||
tasks:
|
||||
- !type:HTNCompoundTask
|
||||
task: PickupGunCompound
|
||||
|
||||
# Discard gun if no ammo
|
||||
- preconditions:
|
||||
- !type:ActiveHandComponentPrecondition
|
||||
components:
|
||||
- type: Gun
|
||||
- !type:GunAmmoPrecondition
|
||||
maxPercent: 0.001
|
||||
tasks:
|
||||
- !type:HTNPrimitiveTask
|
||||
preconditions:
|
||||
- !type:ActiveHandEntityPrecondition
|
||||
operator: !type:DropOperator
|
||||
|
||||
# TODO: Reload a nearby gun
|
||||
80
Resources/Prototypes/NPCs/Combat/melee.yml
Normal file
@@ -0,0 +1,80 @@
|
||||
# -- Melee --
|
||||
# Selects a target in melee and tries to attack it.
|
||||
- type: htnCompound
|
||||
id: MeleeCombatCompound
|
||||
branches:
|
||||
# Pickup weapon if we don't have one.
|
||||
- preconditions:
|
||||
- !type:ActiveHandComponentPrecondition
|
||||
components:
|
||||
# Just serializer things
|
||||
- type: MeleeWeapon
|
||||
damage:
|
||||
types:
|
||||
Brute: 0
|
||||
invert: true
|
||||
tasks:
|
||||
- !type:HTNCompoundTask
|
||||
task: PickupMeleeCompound
|
||||
|
||||
# Melee combat (unarmed or otherwise)
|
||||
- tasks:
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:UtilityOperator
|
||||
proto: NearbyMeleeTargets
|
||||
- !type:HTNCompoundTask
|
||||
task: MeleeAttackTargetCompound
|
||||
|
||||
- type: htnCompound
|
||||
id: PickupMeleeCompound
|
||||
branches:
|
||||
- tasks:
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:UtilityOperator
|
||||
proto: NearbyMeleeWeapons
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:MoveToOperator
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
preconditions:
|
||||
- !type:TargetInRangePrecondition
|
||||
targetKey: Target
|
||||
rangeKey: InteractRange
|
||||
operator: !type:InteractWithOperator
|
||||
targetKey: Target
|
||||
|
||||
# Tries to melee attack our target.
|
||||
- type: htnCompound
|
||||
id: MeleeAttackTargetCompound
|
||||
preconditions:
|
||||
- !type:KeyExistsPrecondition
|
||||
key: Target
|
||||
branches:
|
||||
# Move to melee range and hit them
|
||||
- tasks:
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:MoveToOperator
|
||||
shutdownState: PlanFinished
|
||||
pathfindInPlanning: true
|
||||
removeKeyOnFinish: false
|
||||
targetKey: TargetCoordinates
|
||||
pathfindKey: TargetPathfind
|
||||
rangeKey: MeleeRange
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:JukeOperator
|
||||
jukeType: Away
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:MeleeOperator
|
||||
targetKey: Target
|
||||
preconditions:
|
||||
- !type:KeyExistsPrecondition
|
||||
key: Target
|
||||
- !type:TargetInRangePrecondition
|
||||
targetKey: Target
|
||||
rangeKey: MeleeRange
|
||||
services:
|
||||
- !type:UtilityService
|
||||
id: MeleeService
|
||||
proto: NearbyMeleeTargets
|
||||
key: Target
|
||||
@@ -1,121 +0,0 @@
|
||||
# -- Ranged --
|
||||
# Tries to shoot a target in LOS in range.
|
||||
- type: htnCompound
|
||||
id: TurretCompound
|
||||
branches:
|
||||
- tasks:
|
||||
- id: PickRangedTargetPrimitive
|
||||
- id: RangedAttackTargetPrimitive
|
||||
- tasks:
|
||||
- id: IdleSpinCompound
|
||||
|
||||
# Tries to shoot a target at range.
|
||||
- type: htnCompound
|
||||
id: RangedCombatCompound
|
||||
branches:
|
||||
- tasks:
|
||||
- id: PickRangedTargetPrimitive
|
||||
- id: RangedAttackTargetCompound
|
||||
|
||||
# Tries to ranged attack our target.
|
||||
- type: htnCompound
|
||||
id: RangedAttackTargetCompound
|
||||
preconditions:
|
||||
- !type:KeyExistsPrecondition
|
||||
key: CombatTarget
|
||||
branches:
|
||||
# Keep hitting them if they're in LOS
|
||||
- tasks:
|
||||
- id: RangedAttackTargetPrimitive
|
||||
|
||||
# Move to range and hit them
|
||||
- tasks:
|
||||
- id: MoveToCombatTargetPrimitive
|
||||
- id: RangedAttackTargetPrimitive
|
||||
|
||||
|
||||
- type: htnPrimitive
|
||||
id: PickRangedTargetPrimitive
|
||||
operator: !type:UtilityOperator
|
||||
proto: NearbyRangedTargets
|
||||
|
||||
# Attacks the specified target if they're in LOS.
|
||||
- type: htnPrimitive
|
||||
id: RangedAttackTargetPrimitive
|
||||
operator: !type:RangedOperator
|
||||
targetKey: CombatTarget
|
||||
preconditions:
|
||||
- !type:KeyExistsPrecondition
|
||||
key: CombatTarget
|
||||
- !type:TargetInRangePrecondition
|
||||
targetKey: CombatTarget
|
||||
# TODO: Non-scuffed
|
||||
rangeKey: RangedRange
|
||||
- !type:TargetInLOSPrecondition
|
||||
targetKey: CombatTarget
|
||||
rangeKey: RangedRange
|
||||
services:
|
||||
- !type:UtilityService
|
||||
id: RangedService
|
||||
proto: NearbyRangedTargets
|
||||
key: CombatTarget
|
||||
|
||||
|
||||
# -- Melee --
|
||||
# Selects a target in melee and tries to attack it.
|
||||
- type: htnCompound
|
||||
id: MeleeCombatCompound
|
||||
branches:
|
||||
# Unarmed combat
|
||||
- tasks:
|
||||
- id: PickMeleeTargetPrimitive
|
||||
- id: MeleeAttackTargetCompound
|
||||
|
||||
# Tries to melee attack our target.
|
||||
- type: htnCompound
|
||||
id: MeleeAttackTargetCompound
|
||||
preconditions:
|
||||
- !type:KeyExistsPrecondition
|
||||
key: CombatTarget
|
||||
branches:
|
||||
# Keep hitting them if they're in range
|
||||
- tasks:
|
||||
- id: MeleeAttackTargetPrimitive
|
||||
|
||||
# Move to melee range and hit them
|
||||
- tasks:
|
||||
- id: MoveToCombatTargetPrimitive
|
||||
- id: MeleeAttackTargetPrimitive
|
||||
|
||||
|
||||
- type: htnPrimitive
|
||||
id: PickMeleeTargetPrimitive
|
||||
operator: !type:UtilityOperator
|
||||
proto: NearbyMeleeTargets
|
||||
|
||||
# Attacks the specified target if they're in range.
|
||||
- type: htnPrimitive
|
||||
id: MeleeAttackTargetPrimitive
|
||||
operator: !type:MeleeOperator
|
||||
targetKey: CombatTarget
|
||||
preconditions:
|
||||
- !type:KeyExistsPrecondition
|
||||
key: CombatTarget
|
||||
- !type:TargetInRangePrecondition
|
||||
targetKey: CombatTarget
|
||||
rangeKey: MeleeRange
|
||||
services:
|
||||
- !type:UtilityService
|
||||
id: MeleeService
|
||||
proto: NearbyMeleeTargets
|
||||
key: CombatTarget
|
||||
|
||||
# Moves the owner into range of the combat target.
|
||||
- type: htnPrimitive
|
||||
id: MoveToCombatTargetPrimitive
|
||||
operator: !type:MoveToOperator
|
||||
pathfindInPlanning: true
|
||||
removeKeyOnFinish: false
|
||||
targetKey: CombatTargetCoordinates
|
||||
pathfindKey: CombatTargetPathfind
|
||||
rangeKey: MeleeRange
|
||||
@@ -2,40 +2,37 @@
|
||||
id: CleanbotCompound
|
||||
branches:
|
||||
- tasks:
|
||||
- id: BufferNearbyPuddlesCompound
|
||||
- !type:HTNCompoundTask
|
||||
task: BufferNearbyPuddlesCompound
|
||||
- tasks:
|
||||
- id: IdleCompound
|
||||
- !type:HTNCompoundTask
|
||||
task: IdleCompound
|
||||
|
||||
# Picks a random puddle in range to move to and idle
|
||||
- type: htnCompound
|
||||
id: BufferNearbyPuddlesCompound
|
||||
branches:
|
||||
- tasks:
|
||||
- id: PickPuddlePrimitive
|
||||
- id: MoveToCombatTargetPrimitive
|
||||
- id: MopPrimitive
|
||||
|
||||
- type: htnPrimitive
|
||||
id: PickPuddlePrimitive
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:UtilityOperator
|
||||
proto: NearbyPuddles
|
||||
|
||||
- type: htnPrimitive
|
||||
id: SetIdleTimePrimitive
|
||||
operator: !type:SetFloatOperator
|
||||
targetKey: IdleTime
|
||||
amount: 3
|
||||
|
||||
- type: htnPrimitive
|
||||
id: MopPrimitive
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:MoveToOperator
|
||||
pathfindInPlanning: true
|
||||
removeKeyOnFinish: false
|
||||
targetKey: TargetCoordinates
|
||||
pathfindKey: TargetPathfind
|
||||
rangeKey: MeleeRange
|
||||
- !type:HTNPrimitiveTask
|
||||
preconditions:
|
||||
- !type:TargetInRangePrecondition
|
||||
targetKey: CombatTarget
|
||||
targetKey: Target
|
||||
rangeKey: InteractRange
|
||||
operator: !type:InteractWithOperator
|
||||
targetKey: CombatTarget
|
||||
targetKey: Target
|
||||
services:
|
||||
- !type:UtilityService
|
||||
id: PuddleService
|
||||
proto: NearbyPuddles
|
||||
key: CombatTarget
|
||||
key: Target
|
||||
|
||||
5
Resources/Prototypes/NPCs/clothing.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
# Generic pickups to not be naked.
|
||||
# TODO: Armor / Pressure pickups.
|
||||
# TODO: Preferred slots
|
||||
#- type: htnCompound
|
||||
# id:
|
||||
@@ -7,49 +7,53 @@
|
||||
branches:
|
||||
# Head to follow target
|
||||
- tasks:
|
||||
- id: FollowPrimitive
|
||||
- !type:HTNPrimitiveTask
|
||||
preconditions:
|
||||
- !type:CoordinatesNotInRangePrecondition
|
||||
targetKey: FollowTarget
|
||||
rangeKey: FollowRange
|
||||
operator: !type:MoveToOperator
|
||||
pathfindInPlanning: true
|
||||
targetKey: FollowTarget
|
||||
rangeKey: FollowCloseRange
|
||||
removeKeyOnFinish: false
|
||||
|
||||
# Keep idling near follow target
|
||||
- tasks:
|
||||
- id: WaitFollowPrimitive
|
||||
# Pick a new idle spot near the follow target
|
||||
- tasks:
|
||||
- id: PickAccessibleNearFollowPrimitive
|
||||
- id: IdleNearFollowPrimitive
|
||||
- id: RandomIdleTimePrimitive
|
||||
- id: WaitFollowPrimitive
|
||||
|
||||
|
||||
- type: htnPrimitive
|
||||
id: WaitFollowPrimitive
|
||||
operator: !type:WaitOperator
|
||||
key: IdleTime
|
||||
- !type:HTNPrimitiveTask
|
||||
preconditions:
|
||||
- !type:KeyExistsPrecondition
|
||||
key: IdleTime
|
||||
- !type:CoordinatesInRangePrecondition
|
||||
targetKey: FollowTarget
|
||||
rangeKey: FollowRange
|
||||
operator: !type:WaitOperator
|
||||
key: IdleTime
|
||||
|
||||
- type: htnPrimitive
|
||||
id: PickAccessibleNearFollowPrimitive
|
||||
# Pick a new idle spot near the follow target
|
||||
- tasks:
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:PickAccessibleOperator
|
||||
# originKey: FollowTarget
|
||||
rangeKey: FollowCloseRange
|
||||
targetKey: FollowIdleTarget
|
||||
targetCoordinates: FollowIdleTarget
|
||||
|
||||
- type: htnPrimitive
|
||||
id: IdleNearFollowPrimitive
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:MoveToOperator
|
||||
targetKey: FollowIdleTarget
|
||||
|
||||
- type: htnPrimitive
|
||||
id: FollowPrimitive
|
||||
operator: !type:MoveToOperator
|
||||
pathfindInPlanning: true
|
||||
targetKey: FollowTarget
|
||||
rangeKey: FollowCloseRange
|
||||
removeKeyOnFinish: false
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:RandomOperator
|
||||
targetKey: IdleTime
|
||||
minKey: MinimumIdleTime
|
||||
maxKey: MaximumIdleTime
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
preconditions:
|
||||
- !type:CoordinatesNotInRangePrecondition
|
||||
- !type:KeyExistsPrecondition
|
||||
key: IdleTime
|
||||
- !type:CoordinatesInRangePrecondition
|
||||
targetKey: FollowTarget
|
||||
rangeKey: FollowRange
|
||||
operator: !type:WaitOperator
|
||||
key: IdleTime
|
||||
|
||||
25
Resources/Prototypes/NPCs/generic.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
- type: htnCompound
|
||||
id: ClearActiveHandCompound
|
||||
branches:
|
||||
# Do nothing
|
||||
- preconditions:
|
||||
- !type:ActiveHandFreePrecondition
|
||||
tasks:
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:NoOperator
|
||||
|
||||
# Swap to another free hand
|
||||
- tasks:
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:SwapToFreeHandOperator
|
||||
|
||||
# TODO: Need to make sure this works with blackboard and handles storage + pockets + inventory slots
|
||||
# Put active hand into storage
|
||||
#- tasks:
|
||||
# - !type:HTNPrimitiveTask
|
||||
# operator: !type:StashActiveHandOperator
|
||||
|
||||
# Drop active hand
|
||||
- tasks:
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:DropOperator
|
||||
@@ -2,74 +2,63 @@
|
||||
- type: htnCompound
|
||||
id: IdleCompound
|
||||
branches:
|
||||
- tasks:
|
||||
- id: WaitIdleTimePrimitive
|
||||
# Pick a new spot and wait there.
|
||||
- tasks:
|
||||
- id: PickAccessiblePrimitive
|
||||
- id: MoveToAccessiblePrimitive
|
||||
- id: RandomIdleTimePrimitive
|
||||
- id: WaitIdleTimePrimitive
|
||||
preconditions:
|
||||
- preconditions:
|
||||
- !type:BuckledPrecondition
|
||||
isBuckled: false
|
||||
- !type:PulledPrecondition
|
||||
isPulled: false
|
||||
tasks:
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:PickAccessibleOperator
|
||||
rangeKey: IdleRange
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:MoveToOperator
|
||||
pathfindInPlanning: false
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:RandomOperator
|
||||
targetKey: IdleTime
|
||||
minKey: MinimumIdleTime
|
||||
maxKey: MaximumIdleTime
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:WaitOperator
|
||||
key: IdleTime
|
||||
preconditions:
|
||||
- !type:KeyExistsPrecondition
|
||||
key: IdleTime
|
||||
|
||||
# Spin to a random rotation and idle.
|
||||
- type: htnCompound
|
||||
id: IdleSpinCompound
|
||||
branches:
|
||||
- tasks:
|
||||
- id: WaitIdleTimePrimitive
|
||||
# Pick a new angle and spin there
|
||||
- tasks:
|
||||
- id: PickRandomRotationPrimitive
|
||||
- id: RotateToTargetPrimitive
|
||||
- id: RandomIdleTimePrimitive
|
||||
- id: WaitIdleTimePrimitive
|
||||
|
||||
|
||||
# Primitives
|
||||
- type: htnPrimitive
|
||||
id: InteractWithPrimitive
|
||||
preconditions:
|
||||
- !type:TargetInRangePrecondition
|
||||
targetKey: Target
|
||||
rangeKey: InteractRange
|
||||
operator: !type:InteractWithOperator
|
||||
targetKey: Target
|
||||
|
||||
- type: htnPrimitive
|
||||
id: MoveToAccessiblePrimitive
|
||||
operator: !type:MoveToOperator
|
||||
pathfindInPlanning: false
|
||||
|
||||
- type: htnPrimitive
|
||||
id: PickAccessiblePrimitive
|
||||
operator: !type:PickAccessibleOperator
|
||||
rangeKey: IdleRange
|
||||
targetKey: MovementTarget
|
||||
|
||||
- type: htnPrimitive
|
||||
id: PickRandomRotationPrimitive
|
||||
operator: !type:PickRandomRotationOperator
|
||||
targetKey: RotateTarget
|
||||
|
||||
- type: htnPrimitive
|
||||
id: RotateToTargetPrimitive
|
||||
operator: !type:RotateToTargetOperator
|
||||
targetKey: RotateTarget
|
||||
|
||||
- type: htnPrimitive
|
||||
id: RandomIdleTimePrimitive
|
||||
operator: !type:RandomOperator
|
||||
targetKey: IdleTime
|
||||
minKey: MinimumIdleTime
|
||||
maxKey: MaximumIdleTime
|
||||
|
||||
- type: htnPrimitive
|
||||
id: WaitIdleTimePrimitive
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:WaitOperator
|
||||
key: IdleTime
|
||||
preconditions:
|
||||
- !type:KeyExistsPrecondition
|
||||
key: IdleTime
|
||||
|
||||
# Pick a new angle and spin there
|
||||
- tasks:
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:PickRandomRotationOperator
|
||||
targetKey: RotateTarget
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:RotateToTargetOperator
|
||||
targetKey: RotateTarget
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:RandomOperator
|
||||
targetKey: IdleTime
|
||||
minKey: MinimumIdleTime
|
||||
maxKey: MaximumIdleTime
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:WaitOperator
|
||||
key: IdleTime
|
||||
preconditions:
|
||||
|
||||
@@ -2,33 +2,42 @@
|
||||
id: MedibotCompound
|
||||
branches:
|
||||
- tasks:
|
||||
- id: InjectNearbyCompound
|
||||
- !type:HTNCompoundTask
|
||||
task: InjectNearbyCompound
|
||||
- tasks:
|
||||
- id: IdleCompound
|
||||
- !type:HTNCompoundTask
|
||||
task: IdleCompound
|
||||
|
||||
- type: htnCompound
|
||||
id: InjectNearbyCompound
|
||||
branches:
|
||||
- tasks:
|
||||
- id: PickNearbyInjectablePrimitive
|
||||
- id: MoveToAccessiblePrimitive
|
||||
- id: MedibotSpeakPrimitive
|
||||
- id: SetIdleTimePrimitive
|
||||
- id: WaitIdleTimePrimitive
|
||||
- id: MedibotInjectPrimitive
|
||||
|
||||
- type: htnPrimitive
|
||||
id: MedibotSpeakPrimitive
|
||||
operator: !type:SpeakOperator
|
||||
speech: medibot-start-inject
|
||||
|
||||
- type: htnPrimitive
|
||||
id: PickNearbyInjectablePrimitive
|
||||
# TODO: Kill this shit
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:PickNearbyInjectableOperator
|
||||
targetKey: InjectTarget
|
||||
targetMoveKey: MovementTarget
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:SpeakOperator
|
||||
speech: medibot-start-inject
|
||||
|
||||
- type: htnPrimitive
|
||||
id: MedibotInjectPrimitive
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:MoveToOperator
|
||||
pathfindInPlanning: false
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:SetFloatOperator
|
||||
targetKey: IdleTime
|
||||
amount: 3
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:WaitOperator
|
||||
key: IdleTime
|
||||
preconditions:
|
||||
- !type:KeyExistsPrecondition
|
||||
key: IdleTime
|
||||
|
||||
# TODO: Kill this
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:MedibotInjectOperator
|
||||
targetKey: InjectTarget
|
||||
|
||||
@@ -3,32 +3,41 @@
|
||||
id: SimpleHostileCompound
|
||||
branches:
|
||||
- tasks:
|
||||
- id: MeleeCombatCompound
|
||||
- !type:HTNCompoundTask
|
||||
task: MeleeCombatCompound
|
||||
- tasks:
|
||||
- id: IdleCompound
|
||||
- !type:HTNCompoundTask
|
||||
task: IdleCompound
|
||||
|
||||
- type: htnCompound
|
||||
id: MouseCompound
|
||||
branches:
|
||||
- tasks:
|
||||
- id: FoodCompound
|
||||
- !type:HTNCompoundTask
|
||||
task: FoodCompound
|
||||
- tasks:
|
||||
- id: IdleCompound
|
||||
- !type:HTNCompoundTask
|
||||
task: IdleCompound
|
||||
|
||||
- type: htnCompound
|
||||
id: RuminantCompound
|
||||
branches:
|
||||
- tasks:
|
||||
- id: FoodCompound
|
||||
- !type:HTNCompoundTask
|
||||
task: FoodCompound
|
||||
- tasks:
|
||||
- id: IdleCompound
|
||||
- !type:HTNCompoundTask
|
||||
task: IdleCompound
|
||||
|
||||
- type: htnCompound
|
||||
id: DragonCarpCompound
|
||||
branches:
|
||||
- tasks:
|
||||
- id: MeleeCombatCompound
|
||||
- !type:HTNCompoundTask
|
||||
task: MeleeCombatCompound
|
||||
- tasks:
|
||||
- id: FollowCompound
|
||||
- !type:HTNCompoundTask
|
||||
task: FollowCompound
|
||||
- tasks:
|
||||
- id: IdleCompound
|
||||
- !type:HTNCompoundTask
|
||||
task: IdleCompound
|
||||
|
||||
@@ -1,31 +1,56 @@
|
||||
- type: htnCompound
|
||||
id: FoodCompound
|
||||
branches:
|
||||
# Picks a nearby food, moves into range, then eats it and waits the idle time.
|
||||
- tasks:
|
||||
- id: PickFoodTargetPrimitive
|
||||
- id: MoveToCombatTargetPrimitive
|
||||
- id: EatPrimitive
|
||||
- id: WaitIdleTimePrimitive
|
||||
- tasks:
|
||||
- id: PickDrinkTargetPrimitive
|
||||
- id: MoveToCombatTargetPrimitive
|
||||
- id: EatPrimitive
|
||||
- id: WaitIdleTimePrimitive
|
||||
|
||||
|
||||
- type: htnPrimitive
|
||||
id: PickFoodTargetPrimitive
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:UtilityOperator
|
||||
proto: NearbyFood
|
||||
|
||||
- type: htnPrimitive
|
||||
id: PickDrinkTargetPrimitive
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:MoveToOperator
|
||||
pathfindInPlanning: true
|
||||
removeKeyOnFinish: false
|
||||
targetKey: TargetCoordinates
|
||||
pathfindKey: TargetPathfind
|
||||
rangeKey: MeleeRange
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
preconditions:
|
||||
- !type:KeyExistsPrecondition
|
||||
key: Target
|
||||
operator: !type:AltInteractOperator
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
preconditions:
|
||||
- !type:KeyExistsPrecondition
|
||||
key: IdleTime
|
||||
operator: !type:WaitOperator
|
||||
key: IdleTime
|
||||
|
||||
# Picks nearby drink then consumes it and waits idle time
|
||||
- tasks:
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:UtilityOperator
|
||||
proto: NearbyDrink
|
||||
|
||||
- type: htnPrimitive
|
||||
id: EatPrimitive
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:MoveToOperator
|
||||
pathfindInPlanning: true
|
||||
removeKeyOnFinish: false
|
||||
targetKey: TargetCoordinates
|
||||
pathfindKey: TargetPathfind
|
||||
rangeKey: MeleeRange
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
preconditions:
|
||||
- !type:KeyExistsPrecondition
|
||||
key: CombatTarget
|
||||
key: Target
|
||||
operator: !type:AltInteractOperator
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
preconditions:
|
||||
- !type:KeyExistsPrecondition
|
||||
key: IdleTime
|
||||
operator: !type:WaitOperator
|
||||
key: IdleTime
|
||||
|
||||
57
Resources/Prototypes/NPCs/root.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
# Specific Root compound tasks being used for NPCs.
|
||||
|
||||
# Tries to shoot a target in LOS in range.
|
||||
- type: htnCompound
|
||||
id: TurretCompound
|
||||
branches:
|
||||
- tasks:
|
||||
# Shoot target if in range
|
||||
- !type:HTNPrimitiveTask
|
||||
preconditions:
|
||||
- !type:KeyExistsPrecondition
|
||||
key: Target
|
||||
- !type:TargetInRangePrecondition
|
||||
targetKey: Target
|
||||
# TODO: Non-scuffed
|
||||
rangeKey: RangedRange
|
||||
- !type:TargetInLOSPrecondition
|
||||
targetKey: Target
|
||||
rangeKey: RangedRange
|
||||
operator: !type:GunOperator
|
||||
targetKey: Target
|
||||
requireLOS: true
|
||||
services:
|
||||
- !type:UtilityService
|
||||
id: RangedService
|
||||
proto: NearbyGunTargets
|
||||
key: Target
|
||||
|
||||
- tasks:
|
||||
- !type:HTNCompoundTask
|
||||
task: IdleSpinCompound
|
||||
|
||||
- type: htnCompound
|
||||
id: SimpleRangedHostileCompound
|
||||
branches:
|
||||
- tasks:
|
||||
- !type:HTNCompoundTask
|
||||
task: InnateRangedCombatCompound
|
||||
- tasks:
|
||||
- !type:HTNCompoundTask
|
||||
task: MeleeCombatCompound
|
||||
- tasks:
|
||||
- !type:HTNCompoundTask
|
||||
task: IdleCompound
|
||||
|
||||
- type: htnCompound
|
||||
id: SimpleHumanoidHostileCompound
|
||||
branches:
|
||||
- tasks:
|
||||
- !type:HTNCompoundTask
|
||||
task: RangedCombatCompound
|
||||
- tasks:
|
||||
- !type:HTNCompoundTask
|
||||
task: MeleeCombatCompound
|
||||
- tasks:
|
||||
- !type:HTNCompoundTask
|
||||
task: IdleCompound
|
||||
@@ -3,14 +3,14 @@
|
||||
id: MoveToPathfindPointCompound
|
||||
branches:
|
||||
- tasks:
|
||||
- id: PickPathfindPointPrimitive
|
||||
- id: MoveToAccessiblePrimitive
|
||||
|
||||
|
||||
- type: htnPrimitive
|
||||
id: PickPathfindPointPrimitive
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:PickPathfindPointOperator
|
||||
|
||||
- !type:HTNPrimitiveTask
|
||||
operator: !type:MoveToOperator
|
||||
pathfindInPlanning: false
|
||||
|
||||
|
||||
- type: entity
|
||||
id: MobPathfindDummy
|
||||
name: Pathfind dummy
|
||||
@@ -18,4 +18,5 @@
|
||||
parent: MobXenoRouny
|
||||
components:
|
||||
- type: HTN
|
||||
rootTask: MoveToPathfindPointCompound
|
||||
rootTask:
|
||||
task: MoveToPathfindPointCompound
|
||||
|
||||
@@ -1,3 +1,31 @@
|
||||
- type: utilityQuery
|
||||
id: InventoryGuns
|
||||
query:
|
||||
- !type:InventoryQuery
|
||||
- !type:ComponentFilter
|
||||
components:
|
||||
- type: Gun
|
||||
considerations:
|
||||
- !type:TargetAmmoCon
|
||||
curve: !type:QuadraticCurve
|
||||
|
||||
- type: utilityQuery
|
||||
id: NearbyAmmo
|
||||
query:
|
||||
- !type:ComponentQuery
|
||||
components:
|
||||
- type: Ammo
|
||||
- type: Item
|
||||
considerations:
|
||||
- !type:TargetAmmoMatchesCon
|
||||
curve: !type:BoolCurve
|
||||
- !type:TargetDistanceCon
|
||||
curve: !type:PresetCurve
|
||||
preset: TargetDistance
|
||||
# TODO: Get ammo count.
|
||||
- !type:TargetAccessibleCon
|
||||
curve: !type:BoolCurve
|
||||
|
||||
- type: utilityQuery
|
||||
id: NearbyFood
|
||||
query:
|
||||
@@ -36,6 +64,23 @@
|
||||
- !type:TargetAccessibleCon
|
||||
curve: !type:BoolCurve
|
||||
|
||||
- type: utilityQuery
|
||||
id: NearbyGuns
|
||||
query:
|
||||
- !type:ComponentQuery
|
||||
components:
|
||||
- type: Gun
|
||||
- type: Item
|
||||
considerations:
|
||||
# TODO: Prefer highest DPC probably?
|
||||
- !type:TargetAmmoCon
|
||||
curve: !type:BoolCurve
|
||||
- !type:TargetDistanceCon
|
||||
curve: !type:PresetCurve
|
||||
preset: TargetDistance
|
||||
- !type:TargetAccessibleCon
|
||||
curve: !type:BoolCurve
|
||||
|
||||
- type: utilityQuery
|
||||
id: NearbyMeleeTargets
|
||||
query:
|
||||
@@ -54,6 +99,29 @@
|
||||
- !type:TargetInLOSOrCurrentCon
|
||||
curve: !type:BoolCurve
|
||||
|
||||
- type: utilityQuery
|
||||
id: NearbyMeleeWeapons
|
||||
query:
|
||||
- !type:ComponentQuery
|
||||
components:
|
||||
# Just serializer things
|
||||
- type: MeleeWeapon
|
||||
damage:
|
||||
types:
|
||||
Brute: 0
|
||||
- type: Item
|
||||
considerations:
|
||||
- !type:TargetMeleeCon
|
||||
curve: !type:QuadraticCurve
|
||||
slope: 0.5
|
||||
exponent: 0.5
|
||||
yOffset: 0
|
||||
- !type:TargetDistanceCon
|
||||
curve: !type:PresetCurve
|
||||
preset: TargetDistance
|
||||
- !type:TargetAccessibleCon
|
||||
curve: !type:BoolCurve
|
||||
|
||||
- type: utilityQuery
|
||||
id: NearbyPuddles
|
||||
query:
|
||||
@@ -71,7 +139,7 @@
|
||||
curve: !type:BoolCurve
|
||||
|
||||
- type: utilityQuery
|
||||
id: NearbyRangedTargets
|
||||
id: NearbyGunTargets
|
||||
query:
|
||||
- !type:NearbyHostilesQuery
|
||||
considerations:
|
||||
@@ -88,6 +156,21 @@
|
||||
- !type:TargetInLOSOrCurrentCon
|
||||
curve: !type:BoolCurve
|
||||
|
||||
#- type: utilityQuery
|
||||
# id: NearbyShoes
|
||||
# query:
|
||||
# - !type:ComponentQuery
|
||||
# components:
|
||||
# - type: Clothing
|
||||
# - !type:ClothingSlotFilter
|
||||
# slotFlags: Feet
|
||||
# considerations:
|
||||
# - !type:TargetDistanceCon
|
||||
# curve: !type:PresetCurve
|
||||
# preset: TargetDistance
|
||||
# - !type:TargetAccessibleCon
|
||||
# curve: !type:BoolCurve
|
||||
|
||||
|
||||
# Presets
|
||||
- type: utilityCurvePreset
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
id: XenoCompound
|
||||
branches:
|
||||
- tasks:
|
||||
- id: MeleeCombatCompound
|
||||
- !type:HTNCompoundTask
|
||||
task: MeleeCombatCompound
|
||||
- tasks:
|
||||
- id: IdleCompound
|
||||
- !type:HTNCompoundTask
|
||||
task: IdleCompound
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
- id: WeaponTurretXeno
|
||||
amount: 3
|
||||
prob: 0.25
|
||||
- entries:
|
||||
- id: MobXenoSpitter
|
||||
amount: 2
|
||||
prob: 0.25
|
||||
- entries:
|
||||
- id: MobXenoRavager
|
||||
amount: 1
|
||||
|
||||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -11,7 +11,7 @@
|
||||
"name": "icon"
|
||||
},
|
||||
{
|
||||
"name": "base_dead",
|
||||
"name": "dead",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
@@ -33,7 +33,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "base",
|
||||
"name": "alive",
|
||||
"directions": 4,
|
||||
"delays": [
|
||||
[
|
||||
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 491 B After Width: | Height: | Size: 491 B |
@@ -11,10 +11,10 @@
|
||||
"name": "icon"
|
||||
},
|
||||
{
|
||||
"name": "base_dead"
|
||||
"name": "dead"
|
||||
},
|
||||
{
|
||||
"name": "base",
|
||||
"name": "alive",
|
||||
"directions": 4,
|
||||
"delays": [
|
||||
[
|
||||
|
||||