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
This commit is contained in:
metalgearsloth
2023-08-02 10:48:56 +10:00
committed by GitHub
parent 018e465fad
commit c31c848afd
103 changed files with 2089 additions and 810 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.

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

View File

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

View File

@@ -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;

View File

@@ -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!;

View File

@@ -0,0 +1,9 @@
namespace Content.Server.NPC.HTN;
[Flags]
public enum HTNPlanState : byte
{
TaskFinished = 1 << 0,
PlanFinished = 1 << 1,
}

View File

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

View File

@@ -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!;
}

View File

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

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

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

View File

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

View File

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

View File

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

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

View File

@@ -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";

View File

@@ -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) {}
}

View File

@@ -4,7 +4,6 @@ using Robust.Shared.Prototypes;
namespace Content.Server.NPC.HTN.PrimitiveTasks;
[Prototype("htnPrimitive")]
public sealed class HTNPrimitiveTask : HTNTask
{
/// <summary>

View File

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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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.

View File

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

View File

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

View File

@@ -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;

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
/// <summary>
/// What it sounds like.
/// </summary>
public sealed class NoOperator : HTNOperator
{
}

View File

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

View File

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

View File

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

View File

@@ -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;

View File

@@ -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)

View File

@@ -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()
{

View File

@@ -16,31 +16,31 @@ 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)
{
var reflection = dependencies.Resolve<IReflectionManager>();
var key = data.Key.ToYamlNode().AsString();
foreach (var data in node)
if (data.Value.Tag == null)
{
var key = data.Key.ToYamlNode().AsString();
if (data.Value.Tag == null)
{
validated.Add(new ErrorNode(data.Key, $"Unable to validate {key}'s type"));
continue;
}
var typeString = data.Value.Tag[6..];
if (!reflection.TryLooseGetType(typeString, out var type))
{
validated.Add(new ErrorNode(data.Key, $"Unable to find type for {typeString}"));
continue;
}
var validatedNode = serializationManager.ValidateNode(type, data.Value, context);
validated.Add(validatedNode);
validated.Add(new ErrorNode(data.Key, $"Unable to validate {key}'s type"));
continue;
}
var typeString = data.Value.Tag[6..];
if (!reflection.TryLooseGetType(typeString, out var type))
{
validated.Add(new ErrorNode(data.Key, $"Unable to find type for {typeString}"));
continue;
}
var validatedNode = serializationManager.ValidateNode(type, data.Value, context);
validated.Add(validatedNode);
}
return new ValidatedSequenceNode(validated);
@@ -53,29 +53,29 @@ 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)
{
var reflection = dependencies.Resolve<IReflectionManager>();
var key = data.Key.ToYamlNode().AsString();
foreach (var data in node)
{
var key = data.Key.ToYamlNode().AsString();
if (data.Value.Tag == null)
throw new NullReferenceException($"Found null tag for {key}");
if (data.Value.Tag == null)
throw new NullReferenceException($"Found null tag for {key}");
var typeString = data.Value.Tag[6..];
var typeString = data.Value.Tag[6..];
if (!reflection.TryLooseGetType(typeString, out var type))
throw new NullReferenceException($"Found null type for {key}");
if (!reflection.TryLooseGetType(typeString, out var type))
throw new NullReferenceException($"Found null type for {key}");
var bbData = serializationManager.Read(type, data.Value, hookCtx, context);
var bbData = serializationManager.Read(type, data.Value, hookCtx, context);
if (bbData == null)
throw new NullReferenceException($"Found null data for {key}, expected {type}");
if (bbData == null)
throw new NullReferenceException($"Found null data for {key}, expected {type}");
value.SetValue(key, bbData);
}
value.SetValue(key, bbData);
}
return value;

View File

@@ -0,0 +1,6 @@
namespace Content.Server.NPC.Queries.Considerations;
public sealed class TargetAmmoCon : UtilityConsideration
{
}

View File

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

View File

@@ -0,0 +1,9 @@
namespace Content.Server.NPC.Queries.Considerations;
/// <summary>
/// Gets the DPS out of 100.
/// </summary>
public sealed class TargetMeleeCon : UtilityConsideration
{
}

View File

@@ -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;

View File

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

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

View File

@@ -0,0 +1,9 @@
namespace Content.Server.NPC.Queries.Queries;
/// <summary>
/// Returns inventory entities recursively.
/// </summary>
public sealed class InventoryQuery : UtilityQuery
{
}

View File

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

View File

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

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

View File

@@ -77,12 +77,43 @@ public sealed partial class NPCSteeringSystem
{
var ourCoordinates = xform.Coordinates;
var destinationCoordinates = steering.Coordinates;
var inLos = true;
// 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 distance) &&
distance <= steering.Range)
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,8 +257,13 @@ 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}");
steering.Status = SteeringStatus.NoPath;
return false;
needsPath = true;
if (stuckTime.TotalSeconds > maxStuckTime * 3)
{
steering.Status = SteeringStatus.NoPath;
return false;
}
}
}
else
@@ -237,14 +271,14 @@ public sealed partial class NPCSteeringSystem
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)

View File

@@ -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;

View File

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

View File

@@ -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)
var comps = compQuery.Components.Values.ToList();
var compZero = comps[0];
comps.RemoveAt(0);
foreach (var comp in _lookup.GetComponentsInRange(compZero.Component.GetType(), mapPos, vision))
{
foreach (var comp in _lookup.GetComponentsInRange(compReg.Component.GetType(), mapPos, vision))
var ent = comp.Owner;
if (ent == owner)
continue;
var othersFound = true;
foreach (var compOther in comps)
{
var ent = comp.Owner;
if (!HasComp(ent, compOther.Component.GetType()))
{
othersFound = false;
break;
}
}
if (ent == owner)
continue;
if (!othersFound)
continue;
entities.Add(ent);
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)

View File

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

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Prototypes;
namespace Content.Shared.Inventory;

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,8 @@
description: ghost-role-information-behonker-description
- type: GhostTakeoverAvailable
- type: HTN
rootTask: SimpleHostileCompound
rootTask:
task: SimpleHostileCompound
- type: NpcFactionMember
factions:
- SimpleHostile

View File

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

View File

@@ -6,7 +6,8 @@
abstract: true
components:
- type: HTN
rootTask: SimpleHostileCompound
rootTask:
task: SimpleHostileCompound
blackboard:
NavSmash: !type:Bool
true

View File

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

View File

@@ -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:

View File

@@ -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]

View File

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

View File

@@ -16,7 +16,8 @@
- type: InputMover
- type: MobMover
- type: HTN
rootTask: IdleCompound
rootTask:
task: IdleCompound
- type: Input
context: "human"
- type: NpcFactionMember

View File

@@ -9,7 +9,8 @@
factions:
- SimpleNeutral
- type: HTN
rootTask: SimpleHostileCompound
rootTask:
task: SimpleHostileCompound
- type: Sprite
drawdepth: Mobs
sprite: Mobs/Aliens/slimes.rsi

View File

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

View File

@@ -7,7 +7,8 @@
- type: InputMover
- type: MobMover
- type: HTN
rootTask: SimpleHostileCompound
rootTask:
task: SimpleHostileCompound
- type: NpcFactionMember
factions:
- SimpleHostile

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View File

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

View File

@@ -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:HTNPrimitiveTask
operator: !type:UtilityOperator
proto: NearbyPuddles
- type: htnPrimitive
id: PickPuddlePrimitive
operator: !type:UtilityOperator
proto: NearbyPuddles
- type: htnPrimitive
id: SetIdleTimePrimitive
operator: !type:SetFloatOperator
targetKey: IdleTime
amount: 3
- type: htnPrimitive
id: MopPrimitive
preconditions:
- !type:TargetInRangePrecondition
targetKey: CombatTarget
rangeKey: InteractRange
operator: !type:InteractWithOperator
targetKey: CombatTarget
services:
- !type:UtilityService
id: PuddleService
proto: NearbyPuddles
key: CombatTarget
- !type:HTNPrimitiveTask
operator: !type:MoveToOperator
pathfindInPlanning: true
removeKeyOnFinish: false
targetKey: TargetCoordinates
pathfindKey: TargetPathfind
rangeKey: MeleeRange
- !type:HTNPrimitiveTask
preconditions:
- !type:TargetInRangePrecondition
targetKey: Target
rangeKey: InteractRange
operator: !type:InteractWithOperator
targetKey: Target
services:
- !type:UtilityService
id: PuddleService
proto: NearbyPuddles
key: Target

View File

@@ -0,0 +1,5 @@
# Generic pickups to not be naked.
# TODO: Armor / Pressure pickups.
# TODO: Preferred slots
#- type: htnCompound
# id:

View File

@@ -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
- !type:HTNPrimitiveTask
preconditions:
- !type:KeyExistsPrecondition
key: IdleTime
- !type:CoordinatesInRangePrecondition
targetKey: FollowTarget
rangeKey: FollowRange
operator: !type:WaitOperator
key: IdleTime
# Pick a new idle spot near the follow target
- tasks:
- id: PickAccessibleNearFollowPrimitive
- id: IdleNearFollowPrimitive
- id: RandomIdleTimePrimitive
- id: WaitFollowPrimitive
- !type:HTNPrimitiveTask
operator: !type:PickAccessibleOperator
# originKey: FollowTarget
rangeKey: FollowCloseRange
targetCoordinates: FollowIdleTarget
- !type:HTNPrimitiveTask
operator: !type:MoveToOperator
targetKey: FollowIdleTarget
- type: htnPrimitive
id: WaitFollowPrimitive
operator: !type:WaitOperator
key: IdleTime
preconditions:
- !type:KeyExistsPrecondition
key: IdleTime
- !type:CoordinatesInRangePrecondition
targetKey: FollowTarget
rangeKey: FollowRange
- !type:HTNPrimitiveTask
operator: !type:RandomOperator
targetKey: IdleTime
minKey: MinimumIdleTime
maxKey: MaximumIdleTime
- type: htnPrimitive
id: PickAccessibleNearFollowPrimitive
operator: !type:PickAccessibleOperator
# originKey: FollowTarget
rangeKey: FollowCloseRange
targetKey: FollowIdleTarget
- type: htnPrimitive
id: IdleNearFollowPrimitive
operator: !type:MoveToOperator
targetKey: FollowIdleTarget
- type: htnPrimitive
id: FollowPrimitive
operator: !type:MoveToOperator
pathfindInPlanning: true
targetKey: FollowTarget
rangeKey: FollowCloseRange
removeKeyOnFinish: false
preconditions:
- !type:CoordinatesNotInRangePrecondition
targetKey: FollowTarget
rangeKey: FollowRange
- !type:HTNPrimitiveTask
preconditions:
- !type:KeyExistsPrecondition
key: IdleTime
- !type:CoordinatesInRangePrecondition
targetKey: FollowTarget
rangeKey: FollowRange
operator: !type:WaitOperator
key: IdleTime

View 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

View File

@@ -2,76 +2,65 @@
- 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
- !type:HTNPrimitiveTask
operator: !type:WaitOperator
key: IdleTime
preconditions:
- !type:KeyExistsPrecondition
key: IdleTime
# Pick a new angle and spin there
- tasks:
- id: PickRandomRotationPrimitive
- id: RotateToTargetPrimitive
- id: RandomIdleTimePrimitive
- id: WaitIdleTimePrimitive
- !type:HTNPrimitiveTask
operator: !type:PickRandomRotationOperator
targetKey: RotateTarget
- !type:HTNPrimitiveTask
operator: !type:RotateToTargetOperator
targetKey: RotateTarget
# Primitives
- type: htnPrimitive
id: InteractWithPrimitive
preconditions:
- !type:TargetInRangePrecondition
targetKey: Target
rangeKey: InteractRange
operator: !type:InteractWithOperator
targetKey: Target
- !type:HTNPrimitiveTask
operator: !type:RandomOperator
targetKey: IdleTime
minKey: MinimumIdleTime
maxKey: MaximumIdleTime
- 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
operator: !type:WaitOperator
key: IdleTime
preconditions:
- !type:KeyExistsPrecondition
key: IdleTime
- !type:HTNPrimitiveTask
operator: !type:WaitOperator
key: IdleTime
preconditions:
- !type:KeyExistsPrecondition
key: IdleTime

View File

@@ -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
# 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: MedibotSpeakPrimitive
operator: !type:SpeakOperator
speech: medibot-start-inject
- !type:HTNPrimitiveTask
operator: !type:MoveToOperator
pathfindInPlanning: false
- type: htnPrimitive
id: PickNearbyInjectablePrimitive
operator: !type:PickNearbyInjectableOperator
targetKey: InjectTarget
targetMoveKey: MovementTarget
- !type:HTNPrimitiveTask
operator: !type:SetFloatOperator
targetKey: IdleTime
amount: 3
- type: htnPrimitive
id: MedibotInjectPrimitive
operator: !type:MedibotInjectOperator
targetKey: InjectTarget
- !type:HTNPrimitiveTask
operator: !type:WaitOperator
key: IdleTime
preconditions:
- !type:KeyExistsPrecondition
key: IdleTime
# TODO: Kill this
- !type:HTNPrimitiveTask
operator: !type:MedibotInjectOperator
targetKey: InjectTarget

View File

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

View File

@@ -1,31 +1,56 @@
- type: htnCompound
id: FoodCompound
branches:
- tasks:
- id: PickFoodTargetPrimitive
- id: MoveToCombatTargetPrimitive
- id: EatPrimitive
- id: WaitIdleTimePrimitive
- tasks:
- id: PickDrinkTargetPrimitive
- id: MoveToCombatTargetPrimitive
- id: EatPrimitive
- id: WaitIdleTimePrimitive
# Picks a nearby food, moves into range, then eats it and waits the idle time.
- tasks:
- !type:HTNPrimitiveTask
operator: !type:UtilityOperator
proto: NearbyFood
- !type:HTNPrimitiveTask
operator: !type:MoveToOperator
pathfindInPlanning: true
removeKeyOnFinish: false
targetKey: TargetCoordinates
pathfindKey: TargetPathfind
rangeKey: MeleeRange
- type: htnPrimitive
id: PickFoodTargetPrimitive
operator: !type:UtilityOperator
proto: NearbyFood
- !type:HTNPrimitiveTask
preconditions:
- !type:KeyExistsPrecondition
key: Target
operator: !type:AltInteractOperator
- type: htnPrimitive
id: PickDrinkTargetPrimitive
operator: !type:UtilityOperator
proto: NearbyDrink
- !type:HTNPrimitiveTask
preconditions:
- !type:KeyExistsPrecondition
key: IdleTime
operator: !type:WaitOperator
key: IdleTime
- type: htnPrimitive
id: EatPrimitive
preconditions:
- !type:KeyExistsPrecondition
key: CombatTarget
operator: !type:AltInteractOperator
# Picks nearby drink then consumes it and waits idle time
- tasks:
- !type:HTNPrimitiveTask
operator: !type:UtilityOperator
proto: NearbyDrink
- !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

View 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

View File

@@ -3,13 +3,13 @@
id: MoveToPathfindPointCompound
branches:
- tasks:
- id: PickPathfindPointPrimitive
- id: MoveToAccessiblePrimitive
- !type:HTNPrimitiveTask
operator: !type:PickPathfindPointOperator
- !type:HTNPrimitiveTask
operator: !type:MoveToOperator
pathfindInPlanning: false
- type: htnPrimitive
id: PickPathfindPointPrimitive
operator: !type:PickPathfindPointOperator
- type: entity
id: MobPathfindDummy
@@ -18,4 +18,5 @@
parent: MobXenoRouny
components:
- type: HTN
rootTask: MoveToPathfindPointCompound
rootTask:
task: MoveToPathfindPointCompound

View File

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

View File

@@ -2,6 +2,8 @@
id: XenoCompound
branches:
- tasks:
- id: MeleeCombatCompound
- !type:HTNCompoundTask
task: MeleeCombatCompound
- tasks:
- id: IdleCompound
- !type:HTNCompoundTask
task: IdleCompound

View File

@@ -21,6 +21,10 @@
- id: WeaponTurretXeno
amount: 3
prob: 0.25
- entries:
- id: MobXenoSpitter
amount: 2
prob: 0.25
- entries:
- id: MobXenoRavager
amount: 1

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -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": [
[

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 491 B

After

Width:  |  Height:  |  Size: 491 B

View File

@@ -11,10 +11,10 @@
"name": "icon"
},
{
"name": "base_dead"
"name": "dead"
},
{
"name": "base",
"name": "alive",
"directions": 4,
"delays": [
[

Some files were not shown because too many files have changed in this diff Show More