diff --git a/Content.IntegrationTests/Tests/NPC/NPCTest.cs b/Content.IntegrationTests/Tests/NPC/NPCTest.cs index a58f5af1cc..84457e7ba8 100644 --- a/Content.IntegrationTests/Tests/NPC/NPCTest.cs +++ b/Content.IntegrationTests/Tests/NPC/NPCTest.cs @@ -24,9 +24,9 @@ public sealed class NPCTest { var counts = new Dictionary(); - foreach (var compound in protoManager.EnumeratePrototypes()) + foreach (var compound in protoManager.EnumeratePrototypes()) { - 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 counts, HTNSystem htnSystem) + private static void Count(HTNCompoundPrototype compound, Dictionary 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(compoundTask.Task), counts, htnSystem, protoManager); } } } diff --git a/Content.Server/Dragon/DragonSystem.cs b/Content.Server/Dragon/DragonSystem.cs index de110fd694..e9abc266ee 100644 --- a/Content.Server/Dragon/DragonSystem.cs +++ b/Content.Server/Dragon/DragonSystem.cs @@ -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(comp.Dragon, out var randomSprite)) + { + var spawnedSprite = EnsureComp(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)); } } } diff --git a/Content.Server/NPC/Commands/AddNPCCommand.cs b/Content.Server/NPC/Commands/AddNPCCommand.cs index 207733efa1..070b9f35d3 100644 --- a/Content.Server/NPC/Commands/AddNPCCommand.cs +++ b/Content.Server/NPC/Commands/AddNPCCommand.cs @@ -39,7 +39,10 @@ namespace Content.Server.NPC.Commands } var comp = _entities.AddComponent(entId); - comp.RootTask = args[1]; + comp.RootTask = new HTNCompoundTask() + { + Task = args[1] + }; shell.WriteLine("AI component added."); } } diff --git a/Content.Server/NPC/Commands/NPCDomainCommand.cs b/Content.Server/NPC/Commands/NPCDomainCommand.cs index d387993c89..ee3d108b16 100644 --- a/Content.Server/NPC/Commands/NPCDomainCommand.cs +++ b/Content.Server/NPC/Commands/NPCDomainCommand.cs @@ -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} "; + 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(); - - if (!protoManager.TryIndex(args[0], out var compound)) + if (!_protoManager.HasIndex(args[0])) { shell.WriteError($"Unable to find HTN compound task for '{args[0]}'"); return; } - var htnSystem = IoCManager.Resolve().GetEntitySystem(); + var htnSystem = _sysManager.GetEntitySystem(); - 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(); - if (args.Length > 1) return CompletionResult.Empty; - return CompletionResult.FromHintOptions(protoManager.EnumeratePrototypes().Select(o => o.ID), "compound task"); + return CompletionResult.FromHintOptions(CompletionHelper.PrototypeIDs(proto: _protoManager), "compound task"); } } diff --git a/Content.Server/NPC/Components/NPCJukeComponent.cs b/Content.Server/NPC/Components/NPCJukeComponent.cs new file mode 100644 index 0000000000..670ea345e8 --- /dev/null +++ b/Content.Server/NPC/Components/NPCJukeComponent.cs @@ -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 +{ + /// + /// Will move directly away from target if applicable. + /// + Away, + + /// + /// Move to the adjacent tile for the specified duration. + /// + AdjacentTile +} diff --git a/Content.Server/NPC/Components/NPCMeleeCombatComponent.cs b/Content.Server/NPC/Components/NPCMeleeCombatComponent.cs index cf5131f7a8..17f4fee090 100644 --- a/Content.Server/NPC/Components/NPCMeleeCombatComponent.cs +++ b/Content.Server/NPC/Components/NPCMeleeCombatComponent.cs @@ -6,11 +6,6 @@ namespace Content.Server.NPC.Components; [RegisterComponent] public sealed class NPCMeleeCombatComponent : Component { - /// - /// Weapon we're using to attack the target. Can also be ourselves. - /// - [ViewVariables] public EntityUid Weapon; - /// /// If the target is moving what is the chance for this NPC to miss. /// diff --git a/Content.Server/NPC/Components/NPCSteeringComponent.cs b/Content.Server/NPC/Components/NPCSteeringComponent.cs index 9772393b2d..d1ec137aa9 100644 --- a/Content.Server/NPC/Components/NPCSteeringComponent.cs +++ b/Content.Server/NPC/Components/NPCSteeringComponent.cs @@ -69,6 +69,22 @@ public sealed class NPCSteeringComponent : Component /// [ViewVariables] public bool Pathfind => PathfindToken != null; + + /// + /// Are we considered arrived if we have line of sight of the target. + /// + [DataField("arriveOnLineOfSight")] + public bool ArriveOnLineOfSight = false; + + /// + /// How long the target has been in line of sight if applicable. + /// + [DataField("lineOfSightTimer")] + public float LineOfSightTimer = 0f; + + [DataField("lineOfSightTimeRequired")] + public float LineOfSightTimeRequired = 0.5f; + [ViewVariables] public CancellationTokenSource? PathfindToken = null; /// diff --git a/Content.Server/NPC/Events/NPCSteeringEvent.cs b/Content.Server/NPC/Events/NPCSteeringEvent.cs index 21c03eefcb..56870d18c5 100644 --- a/Content.Server/NPC/Events/NPCSteeringEvent.cs +++ b/Content.Server/NPC/Events/NPCSteeringEvent.cs @@ -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); diff --git a/Content.Server/NPC/HTN/HTNBranch.cs b/Content.Server/NPC/HTN/HTNBranch.cs index 96645b2906..0294134381 100644 --- a/Content.Server/NPC/HTN/HTNBranch.cs +++ b/Content.Server/NPC/HTN/HTNBranch.cs @@ -15,6 +15,6 @@ public sealed class HTNBranch /// /// Due to how serv3 works we need to defer getting the actual tasks until after they have all been serialized. /// - [DataField("tasks", required: true, customTypeSerializer:typeof(HTNTaskListSerializer))] - public List TaskPrototypes = default!; + [DataField("tasks", required: true)] + public List Tasks = new(); } diff --git a/Content.Server/NPC/HTN/HTNComponent.cs b/Content.Server/NPC/HTN/HTNComponent.cs index de56627cb1..f2a8005912 100644 --- a/Content.Server/NPC/HTN/HTNComponent.cs +++ b/Content.Server/NPC/HTN/HTNComponent.cs @@ -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 /// [ViewVariables(VVAccess.ReadWrite), - DataField("rootTask", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string RootTask = default!; + DataField("rootTask", required: true)] + public HTNCompoundTask RootTask = default!; /// /// Check any active services for our current plan. This is used to find new targets for example without changing our plan. diff --git a/Content.Server/NPC/HTN/HTNCompoundPrototype.cs b/Content.Server/NPC/HTN/HTNCompoundPrototype.cs new file mode 100644 index 0000000000..82d6f029a7 --- /dev/null +++ b/Content.Server/NPC/HTN/HTNCompoundPrototype.cs @@ -0,0 +1,15 @@ +using Robust.Shared.Prototypes; + +namespace Content.Server.NPC.HTN; + +/// +/// Represents a network of multiple tasks. This gets expanded out to its relevant nodes. +/// +[Prototype("htnCompound")] +public sealed class HTNCompoundPrototype : IPrototype +{ + [IdDataField] public string ID { get; } = string.Empty; + + [DataField("branches", required: true)] + public List Branches = new(); +} diff --git a/Content.Server/NPC/HTN/HTNCompoundTask.cs b/Content.Server/NPC/HTN/HTNCompoundTask.cs index e3e31317b9..43a10aa356 100644 --- a/Content.Server/NPC/HTN/HTNCompoundTask.cs +++ b/Content.Server/NPC/HTN/HTNCompoundTask.cs @@ -1,16 +1,15 @@ -using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Server.NPC.HTN; /// /// Represents a network of multiple tasks. This gets expanded out to its relevant nodes. /// -[Prototype("htnCompound")] -public sealed class HTNCompoundTask : HTNTask +/// +/// This just points to a specific htnCompound prototype +/// +public sealed class HTNCompoundTask : HTNTask, IHTNCompound { - /// - /// The available branches for this compound task. - /// - [DataField("branches", required: true)] - public List Branches = default!; + [DataField("task", required: true, customTypeSerializer:typeof(PrototypeIdSerializer))] + public string Task = string.Empty; } diff --git a/Content.Server/NPC/HTN/HTNPlan.cs b/Content.Server/NPC/HTN/HTNPlan.cs index af17ab4cee..aa29ae292b 100644 --- a/Content.Server/NPC/HTN/HTNPlan.cs +++ b/Content.Server/NPC/HTN/HTNPlan.cs @@ -12,16 +12,19 @@ public sealed class HTNPlan /// public readonly List?> Effects; - public List BranchTraversalRecord; + public readonly List BranchTraversalRecord; - public List Tasks; - - public int Index = 0; + public readonly List Tasks; public HTNPrimitiveTask CurrentTask => Tasks[Index]; public HTNOperator CurrentOperator => CurrentTask.Operator; + /// + /// Where we are up to in the + /// + public int Index = 0; + public HTNPlan(List tasks, List branchTraversalRecord, List?> effects) { Tasks = tasks; diff --git a/Content.Server/NPC/HTN/HTNPlanJob.cs b/Content.Server/NPC/HTN/HTNPlanJob.cs index b1a8064953..d6d10ad311 100644 --- a/Content.Server/NPC/HTN/HTNPlanJob.cs +++ b/Content.Server/NPC/HTN/HTNPlanJob.cs @@ -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; /// public sealed class HTNPlanJob : Job { - private readonly HTNSystem _htn; - private readonly HTNCompoundTask _rootTask; + private readonly HTNTask _rootTask; private NPCBlackboard _blackboard; + private IPrototypeManager _protoManager; + /// /// Branch traversal of an existing plan (if applicable). /// @@ -22,13 +24,13 @@ public sealed class HTNPlanJob : Job public HTNPlanJob( double maxTime, - HTNSystem htn, - HTNCompoundTask rootTask, + IPrototypeManager protoManager, + HTNTask rootTask, NPCBlackboard blackboard, List? 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 // branch traversal record. Whenever we find a new compound task this updates. var btrIndex = 0; - var btr = new List(); // 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 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 } 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 } 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 /// /// Goes through each compound task branch and tries to find an appropriate one. /// - private bool TryFindSatisfiedMethod(HTNCompoundTask compound, Queue tasksToProcess, NPCBlackboard blackboard, ref int mtrIndex) + private bool TryFindSatisfiedMethod(HTNCompoundTask compoundId, Queue tasksToProcess, NPCBlackboard blackboard, ref int mtrIndex) { - var compBranches = _htn.CompoundBranches[compound]; + var compound = _protoManager.Index(compoundId.Task); for (var i = mtrIndex; i < compound.Branches.Count; i++) { @@ -178,9 +177,7 @@ public sealed class HTNPlanJob : Job 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 List finalPlan, ref int primitiveCount, ref NPCBlackboard blackboard, - ref int mtrIndex, - ref List btr) + ref int mtrIndex) { tasksToProcess.Clear(); @@ -214,11 +210,11 @@ public sealed class HTNPlanJob : Job 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 public int PrimitiveCount; /// - /// The compound task that owns this decomposition. + /// The task that owns this decomposition. /// public HTNCompoundTask CompoundTask = default!; diff --git a/Content.Server/NPC/HTN/HTNPlanState.cs b/Content.Server/NPC/HTN/HTNPlanState.cs new file mode 100644 index 0000000000..c3b67055cc --- /dev/null +++ b/Content.Server/NPC/HTN/HTNPlanState.cs @@ -0,0 +1,9 @@ +namespace Content.Server.NPC.HTN; + +[Flags] +public enum HTNPlanState : byte +{ + TaskFinished = 1 << 0, + + PlanFinished = 1 << 1, +} diff --git a/Content.Server/NPC/HTN/HTNSystem.cs b/Content.Server/NPC/HTN/HTNSystem.cs index 5d280099ad..14b19bd2ea 100644 --- a/Content.Server/NPC/HTN/HTNSystem.cs +++ b/Content.Server/NPC/HTN/HTNSystem.cs @@ -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 _subscribers = new(); - // hngngghghgh - public IReadOnlyDictionary[]> CompoundBranches => _compoundBranches; - private Dictionary[]> _compoundBranches = new(); - // Hierarchical Task Network public override void Initialize() { base.Initialize(); - _sawmill = Logger.GetSawmill("npc.htn"); SubscribeLocalEvent(OnHTNShutdown); SubscribeNetworkEvent(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(true)) + var query = AllEntityQuery(); + + 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()) + foreach (var compound in _prototypeManager.EnumeratePrototypes()) { UpdateCompound(compound); } - - foreach (var primitive in _prototypeManager.EnumeratePrototypes()) - { - 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[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(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(proto, out var compTask)) - { - brancho.Add(compTask); - } - else if (_prototypeManager.TryIndex(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(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(comp.RootTask); + var root = comp.RootTask; var btr = new List(); 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(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; + } + + /// + /// Shuts down the current operator conditionally. + /// + 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); + } + /// /// Starts a new primitive task. Will apply effects from planning if applicable. /// @@ -400,8 +438,8 @@ public sealed class HTNSystem : EntitySystem var job = new HTNPlanJob( 0.02, - this, - _prototypeManager.Index(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(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); } diff --git a/Content.Server/NPC/HTN/HTNTask.cs b/Content.Server/NPC/HTN/HTNTask.cs index e438edbf50..f06b15234a 100644 --- a/Content.Server/NPC/HTN/HTNTask.cs +++ b/Content.Server/NPC/HTN/HTNTask.cs @@ -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!; } diff --git a/Content.Server/NPC/HTN/HTNTaskListSerializer.cs b/Content.Server/NPC/HTN/HTNTaskListSerializer.cs deleted file mode 100644 index 98d6295fac..0000000000 --- a/Content.Server/NPC/HTN/HTNTaskListSerializer.cs +++ /dev/null @@ -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, SequenceDataNode> -{ - public ValidationNode Validate(ISerializationManager serializationManager, SequenceDataNode node, - IDependencyCollection dependencies, ISerializationContext? context = null) - { - var list = new List(); - var protoManager = dependencies.Resolve(); - - 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(id); - var isPrimitive = protoManager.HasIndex(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 Read(ISerializationManager serializationManager, SequenceDataNode node, - IDependencyCollection dependencies, - SerializationHookContext hookCtx, ISerializationContext? context = null, - ISerializationManager.InstantiationDelegate>? instanceProvider = null) - { - var value = instanceProvider != null ? instanceProvider() : new List(); - 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 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; - } -} diff --git a/Content.Server/NPC/HTN/IHTNCompound.cs b/Content.Server/NPC/HTN/IHTNCompound.cs new file mode 100644 index 0000000000..2194da8ad3 --- /dev/null +++ b/Content.Server/NPC/HTN/IHTNCompound.cs @@ -0,0 +1,8 @@ +namespace Content.Server.NPC.HTN; + +/// +/// Represents a HTN task that can be decomposed into primitive tasks. +/// +public interface IHTNCompound +{ +} diff --git a/Content.Server/NPC/HTN/IHtnConditionalShutdown.cs b/Content.Server/NPC/HTN/IHtnConditionalShutdown.cs new file mode 100644 index 0000000000..d8748e2620 --- /dev/null +++ b/Content.Server/NPC/HTN/IHtnConditionalShutdown.cs @@ -0,0 +1,17 @@ +namespace Content.Server.NPC.HTN; + +/// +/// Helper interface to run the appropriate shutdown for a particular task. +/// +public interface IHtnConditionalShutdown +{ + /// + /// When to shut the task down. + /// + HTNPlanState ShutdownState { get; } + + /// + /// Run whenever the specifies. + /// + void ConditionalShutdown(NPCBlackboard blackboard); +} diff --git a/Content.Server/NPC/HTN/Preconditions/ActiveHandComponentPrecondition.cs b/Content.Server/NPC/HTN/Preconditions/ActiveHandComponentPrecondition.cs new file mode 100644 index 0000000000..063c9f5072 --- /dev/null +++ b/Content.Server/NPC/HTN/Preconditions/ActiveHandComponentPrecondition.cs @@ -0,0 +1,39 @@ +using Content.Shared.Hands.Components; +using Robust.Shared.Prototypes; + +namespace Content.Server.NPC.HTN.Preconditions; + +/// +/// Returns true if the active hand entity has the specified components. +/// +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(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; + } +} diff --git a/Content.Server/NPC/HTN/Preconditions/ActiveHandEntityPrecondition.cs b/Content.Server/NPC/HTN/Preconditions/ActiveHandEntityPrecondition.cs new file mode 100644 index 0000000000..a194ff486b --- /dev/null +++ b/Content.Server/NPC/HTN/Preconditions/ActiveHandEntityPrecondition.cs @@ -0,0 +1,21 @@ +using Content.Shared.Hands.Components; + +namespace Content.Server.NPC.HTN.Preconditions; + +/// +/// Returns true if an entity is held in the active hand. +/// +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; + } +} diff --git a/Content.Server/NPC/HTN/Preconditions/ActiveHandFreePrecondition.cs b/Content.Server/NPC/HTN/Preconditions/ActiveHandFreePrecondition.cs new file mode 100644 index 0000000000..041ef085ee --- /dev/null +++ b/Content.Server/NPC/HTN/Preconditions/ActiveHandFreePrecondition.cs @@ -0,0 +1,16 @@ +using Content.Shared.Hands.Components; + +namespace Content.Server.NPC.HTN.Preconditions; + +/// +/// Returns true if the active hand is unoccupied. +/// +public sealed class ActiveHandFreePrecondition : HTNPrecondition +{ + [Dependency] private readonly IEntityManager _entManager = default!; + + public override bool IsMet(NPCBlackboard blackboard) + { + return blackboard.TryGetValue(NPCBlackboard.ActiveHandFree, out var handFree, _entManager) && handFree; + } +} diff --git a/Content.Server/NPC/HTN/Preconditions/GunAmmoPrecondition.cs b/Content.Server/NPC/HTN/Preconditions/GunAmmoPrecondition.cs new file mode 100644 index 0000000000..6bc0bf0ee5 --- /dev/null +++ b/Content.Server/NPC/HTN/Preconditions/GunAmmoPrecondition.cs @@ -0,0 +1,48 @@ +using Content.Server.Weapons.Ranged.Systems; +using Content.Shared.Weapons.Ranged.Events; + +namespace Content.Server.NPC.HTN.Preconditions; + +/// +/// Gets ammo for this NPC's selected gun; either active hand or itself. +/// +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(NPCBlackboard.Owner); + var gunSystem = _entManager.System(); + + 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; + } +} diff --git a/Content.Server/NPC/HTN/Preconditions/TargetInLOSPrecondition.cs b/Content.Server/NPC/HTN/Preconditions/TargetInLOSPrecondition.cs index b8a6826123..a80451075f 100644 --- a/Content.Server/NPC/HTN/Preconditions/TargetInLOSPrecondition.cs +++ b/Content.Server/NPC/HTN/Preconditions/TargetInLOSPrecondition.cs @@ -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"; diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/HTNOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/HTNOperator.cs index 693137442e..145fb7a5b9 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/HTNOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/HTNOperator.cs @@ -40,6 +40,14 @@ public abstract class HTNOperator return HTNOperatorStatus.Finished; } + /// + /// Called when the plan has finished running. + /// + public virtual void PlanShutdown(NPCBlackboard blackboard) + { + + } + /// /// Called the first time an operator runs. /// @@ -48,5 +56,5 @@ public abstract class HTNOperator /// /// Called whenever the operator stops running. /// - public virtual void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status) {} + public virtual void TaskShutdown(NPCBlackboard blackboard, HTNOperatorStatus status) {} } diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/HTNPrimitiveTask.cs b/Content.Server/NPC/HTN/PrimitiveTasks/HTNPrimitiveTask.cs index 2c574ba298..49be119d1b 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/HTNPrimitiveTask.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/HTNPrimitiveTask.cs @@ -4,7 +4,6 @@ using Robust.Shared.Prototypes; namespace Content.Server.NPC.HTN.PrimitiveTasks; -[Prototype("htnPrimitive")] public sealed class HTNPrimitiveTask : HTNTask { /// diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/JukeOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/JukeOperator.cs new file mode 100644 index 0000000000..8ca900954f --- /dev/null +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/JukeOperator.cs @@ -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(blackboard.GetValue(NPCBlackboard.Owner)); + juke.JukeType = JukeType; + } + + public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) + { + return HTNOperatorStatus.Finished; + } + + public void ConditionalShutdown(NPCBlackboard blackboard) + { + _entManager.RemoveComponent(blackboard.GetValue(NPCBlackboard.Owner)); + } +} diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Melee/MeleeOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Melee/MeleeOperator.cs similarity index 78% rename from Content.Server/NPC/HTN/PrimitiveTasks/Operators/Melee/MeleeOperator.cs rename to Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Melee/MeleeOperator.cs index 8831b87850..80ac6f10e7 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Melee/MeleeOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Melee/MeleeOperator.cs @@ -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; /// /// Attacks the specified key in melee combat. /// -public sealed class MeleeOperator : HTNOperator +public sealed class MeleeOperator : HTNOperator, IHtnConditionalShutdown { [Dependency] private readonly IEntityManager _entManager = default!; + /// + /// When to shut the task down. + /// + [DataField("shutdownState")] + public HTNPlanState ShutdownState { get; } = HTNPlanState.TaskFinished; + /// /// Key that contains the target entity. /// @@ -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(blackboard.GetValue(NPCBlackboard.Owner)); + var owner = blackboard.GetValue(NPCBlackboard.Owner); + _entManager.System().SetInCombatMode(owner, false); + _entManager.RemoveComponent(owner); blackboard.Remove(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(owner); + status = HTNOperatorStatus.Finished; } return status; diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Ranged/RangedOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Ranged/GunOperator.cs similarity index 74% rename from Content.Server/NPC/HTN/PrimitiveTasks/Operators/Ranged/RangedOperator.cs rename to Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Ranged/GunOperator.cs index 3446cd964a..e0fe35fbf2 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Ranged/RangedOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Ranged/GunOperator.cs @@ -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; + /// /// Key that contains the target entity. /// @@ -23,6 +27,12 @@ public sealed class RangedOperator : HTNOperator [DataField("targetState")] public MobState TargetState = MobState.Alive; + /// + /// Do we require line of sight of the target before failing. + /// + [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? 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(blackboard.GetValue(NPCBlackboard.Owner)); + var owner = blackboard.GetValue(NPCBlackboard.Owner); + _entManager.System().SetInCombatMode(owner, false); + _entManager.RemoveComponent(owner); blackboard.Remove(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(owner); + status = HTNOperatorStatus.Finished; } return status; diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/AltInteractOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/AltInteractOperator.cs similarity index 94% rename from Content.Server/NPC/HTN/PrimitiveTasks/Operators/AltInteractOperator.cs rename to Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/AltInteractOperator.cs index 4229886059..9938613966 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/AltInteractOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/AltInteractOperator.cs @@ -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"; /// /// If this alt-interaction started a do_after where does the key get stored. diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/DropOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/DropOperator.cs new file mode 100644 index 0000000000..68d49a60fb --- /dev/null +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/DropOperator.cs @@ -0,0 +1,31 @@ +using Content.Server.Hands.Systems; +using Content.Shared.Hands.Components; + +namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Interactions; + +/// +/// Drops the active hand entity underneath us. +/// +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(NPCBlackboard.Owner, _entManager); + // TODO: Need some sort of interaction cooldown probably. + var handsSystem = _entManager.System(); + + if (handsSystem.TryDrop(owner)) + { + return HTNOperatorStatus.Finished; + } + + return HTNOperatorStatus.Failed; + } +} diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/EquipOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/EquipOperator.cs new file mode 100644 index 0000000000..fbd3d7ab57 --- /dev/null +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/EquipOperator.cs @@ -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(Target, out var target, _entManager)) + { + return HTNOperatorStatus.Failed; + } + + var owner = blackboard.GetValue(NPCBlackboard.Owner); + var handsSystem = _entManager.System(); + + // TODO: As elsewhere need some generic interaction cooldown system + if (handsSystem.TryPickup(owner, target)) + { + return HTNOperatorStatus.Finished; + } + + return HTNOperatorStatus.Failed; + } +} diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/InteractWithOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/InteractWithOperator.cs similarity index 84% rename from Content.Server/NPC/HTN/PrimitiveTasks/Operators/InteractWithOperator.cs rename to Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/InteractWithOperator.cs index 1a4238d438..cdbf8c8753 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/InteractWithOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/InteractWithOperator.cs @@ -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().SetInCombatMode(owner, false); _entManager.System().UserInteraction(owner, targetXform.Coordinates, moveTarget); return HTNOperatorStatus.Finished; diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/SwapToFreeHandOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/SwapToFreeHandOperator.cs new file mode 100644 index 0000000000..fc1b671762 --- /dev/null +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/SwapToFreeHandOperator.cs @@ -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; + + +/// +/// Swaps to any free hand. +/// +public sealed class SwapToFreeHandOperator : HTNOperator +{ + [Dependency] private readonly IEntityManager _entManager = default!; + + public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard, CancellationToken cancelToken) + { + if (!blackboard.TryGetValue>(NPCBlackboard.FreeHands, out var hands, _entManager) || + !_entManager.TryGetComponent(blackboard.GetValue(NPCBlackboard.Owner), out var handsComp)) + { + return (false, null); + } + + foreach (var hand in hands) + { + return (true, new Dictionary() + { + { + 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(NPCBlackboard.Owner); + var handSystem = _entManager.System(); + + if (!handSystem.TrySelectEmptyHand(owner)) + { + return HTNOperatorStatus.Failed; + } + + return HTNOperatorStatus.Finished; + } +} diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/MoveToOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/MoveToOperator.cs index 5d22862c2a..e63026a8c9 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/MoveToOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/MoveToOperator.cs @@ -11,7 +11,7 @@ namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators; /// /// Moves an NPC to the specified target key. Hands the actual steering off to NPCSystem.Steering /// -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!; + /// + /// When to shut the task down. + /// + [DataField("shutdownState")] + public HTNPlanState ShutdownState { get; } = HTNPlanState.TaskFinished; + /// /// Should we assume the MovementTarget is reachable during planning or should we pathfind to it? /// @@ -35,7 +41,7 @@ public sealed class MoveToOperator : HTNOperator /// Target Coordinates to move to. This gets removed after execution. /// [DataField("targetKey")] - public string TargetKey = "MovementTarget"; + public string TargetKey = "TargetCoordinates"; /// /// 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"; + /// + /// Do we only need to move into line of sight. + /// + [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(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(NPCBlackboard.Owner); + if (!_entManager.TryGetComponent(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(MovementCancelToken, out var cancelToken, _entManager)) { @@ -171,20 +204,4 @@ public sealed class MoveToOperator : HTNOperator _steering.Unregister(blackboard.GetValue(NPCBlackboard.Owner)); } - - public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) - { - var owner = blackboard.GetValue(NPCBlackboard.Owner); - - if (!_entManager.TryGetComponent(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() - }; - } } diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/NoOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/NoOperator.cs new file mode 100644 index 0000000000..550d232a73 --- /dev/null +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/NoOperator.cs @@ -0,0 +1,9 @@ +namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators; + +/// +/// What it sounds like. +/// +public sealed class NoOperator : HTNOperator +{ + +} diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickAccessibleOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickAccessibleOperator.cs index 70efa83be8..2de9b6d695 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickAccessibleOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickAccessibleOperator.cs @@ -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"; /// /// 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() { - { TargetKey, target }, + { TargetCoordinates, target }, { PathfindKey, path} }); } diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/RotateToTargetOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/RotateToTargetOperator.cs index 910bd1e9f1..1adc552b30 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/RotateToTargetOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/RotateToTargetOperator.cs @@ -23,9 +23,9 @@ public sealed class RotateToTargetOperator : HTNOperator _rotate = sysManager.GetEntitySystem(); } - 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(TargetKey); } diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs index ef7a30fd3e..7c60add427 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs @@ -36,9 +36,9 @@ public sealed class MedibotInjectOperator : HTNOperator _solution = sysManager.GetEntitySystem(); } - 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(TargetKey); } diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/UtilityOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/UtilityOperator.cs index 698329635c..8f716ceee0 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/UtilityOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/UtilityOperator.cs @@ -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"; /// /// The EntityCoordinates of the specified target. /// [DataField("keyCoordinates")] - public string KeyCoordinates = "CombatTargetCoordinates"; + public string KeyCoordinates = "TargetCoordinates"; [DataField("proto", required: true, customTypeSerializer:typeof(PrototypeIdSerializer))] public string Prototype = string.Empty; diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/WaitOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/WaitOperator.cs index a992d5eccc..6c45f2daec 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/WaitOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/WaitOperator.cs @@ -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) diff --git a/Content.Server/NPC/NPCBlackboard.cs b/Content.Server/NPC/NPCBlackboard.cs index 596b89f71c..039ad4b3dc 100644 --- a/Content.Server/NPC/NPCBlackboard.cs +++ b/Content.Server/NPC/NPCBlackboard.cs @@ -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> {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> switch (key) { case Access: + { if (!TryGetValue(Owner, out owner, entManager)) { return false; @@ -159,7 +163,33 @@ public sealed class NPCBlackboard : IEnumerable> var access = entManager.EntitySysManager.GetEntitySystem(); value = access.FindAccessTags(owner); return true; + } + case ActiveHand: + { + if (!TryGetValue(Owner, out owner, entManager) || + !entManager.TryGetComponent(owner, out var hands) || + hands.ActiveHand == null) + { + return false; + } + + value = hands.ActiveHand; + return true; + } + case ActiveHandFree: + { + if (!TryGetValue(Owner, out owner, entManager) || + !entManager.TryGetComponent(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> var blocker = entManager.EntitySysManager.GetEntitySystem(); value = blocker.CanMove(owner); return true; + } + case FreeHands: + { + if (!TryGetValue(Owner, out owner, entManager) || + !entManager.TryGetComponent(owner, out var hands) || + hands.ActiveHand == null) + { + return false; + } + + var handos = new List(); + + 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(owner, out var hands) || + hands.ActiveHand == null) + { + return false; + } + + var handos = new List(); + + 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> } return false; + } default: return false; } @@ -200,8 +277,12 @@ public sealed class NPCBlackboard : IEnumerable> */ 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> public const string RotateSpeed = "RotateSpeed"; public const string VisionRadius = "VisionRadius"; - public const string UtilityTarget = "Target"; + public const string UtilityTarget = "UtilityTarget"; public IEnumerator> GetEnumerator() { diff --git a/Content.Server/NPC/NPCBlackboardSerializer.cs b/Content.Server/NPC/NPCBlackboardSerializer.cs index 276e1d072b..56db17a0a7 100644 --- a/Content.Server/NPC/NPCBlackboardSerializer.cs +++ b/Content.Server/NPC/NPCBlackboardSerializer.cs @@ -16,31 +16,31 @@ public sealed class NPCBlackboardSerializer : ITypeReader(); - if (node.Count > 0) + if (node.Count <= 0) + return new ValidatedSequenceNode(validated); + + var reflection = dependencies.Resolve(); + + foreach (var data in node) { - var reflection = dependencies.Resolve(); + 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 0) + if (node.Count <= 0) + return value; + + var reflection = dependencies.Resolve(); + + foreach (var data in node) { - var reflection = dependencies.Resolve(); + 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; diff --git a/Content.Server/NPC/Queries/Considerations/TargetAmmoCon.cs b/Content.Server/NPC/Queries/Considerations/TargetAmmoCon.cs new file mode 100644 index 0000000000..afae94eb47 --- /dev/null +++ b/Content.Server/NPC/Queries/Considerations/TargetAmmoCon.cs @@ -0,0 +1,6 @@ +namespace Content.Server.NPC.Queries.Considerations; + +public sealed class TargetAmmoCon : UtilityConsideration +{ + +} diff --git a/Content.Server/NPC/Queries/Considerations/TargetAmmoMatchesCon.cs b/Content.Server/NPC/Queries/Considerations/TargetAmmoMatchesCon.cs new file mode 100644 index 0000000000..4eac5d5138 --- /dev/null +++ b/Content.Server/NPC/Queries/Considerations/TargetAmmoMatchesCon.cs @@ -0,0 +1,9 @@ +namespace Content.Server.NPC.Queries.Considerations; + +/// +/// Returns 1f where the specified target is valid for the active hand's whitelist. +/// +public sealed class TargetAmmoMatchesCon : UtilityConsideration +{ + +} diff --git a/Content.Server/NPC/Queries/Considerations/TargetMeleeCon.cs b/Content.Server/NPC/Queries/Considerations/TargetMeleeCon.cs new file mode 100644 index 0000000000..5be6ee65b5 --- /dev/null +++ b/Content.Server/NPC/Queries/Considerations/TargetMeleeCon.cs @@ -0,0 +1,9 @@ +namespace Content.Server.NPC.Queries.Considerations; + +/// +/// Gets the DPS out of 100. +/// +public sealed class TargetMeleeCon : UtilityConsideration +{ + +} diff --git a/Content.Server/NPC/Queries/Curves/QuadraticCurve.cs b/Content.Server/NPC/Queries/Curves/QuadraticCurve.cs index 3dead779af..76ad92c2a4 100644 --- a/Content.Server/NPC/Queries/Curves/QuadraticCurve.cs +++ b/Content.Server/NPC/Queries/Curves/QuadraticCurve.cs @@ -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; diff --git a/Content.Server/NPC/Queries/Queries/ClothingSlotFilter.cs b/Content.Server/NPC/Queries/Queries/ClothingSlotFilter.cs index 2ca04e1369..427b388112 100644 --- a/Content.Server/NPC/Queries/Queries/ClothingSlotFilter.cs +++ b/Content.Server/NPC/Queries/Queries/ClothingSlotFilter.cs @@ -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; } diff --git a/Content.Server/NPC/Queries/Queries/ComponentFilter.cs b/Content.Server/NPC/Queries/Queries/ComponentFilter.cs new file mode 100644 index 0000000000..f89a00d77b --- /dev/null +++ b/Content.Server/NPC/Queries/Queries/ComponentFilter.cs @@ -0,0 +1,12 @@ +using Robust.Shared.Prototypes; + +namespace Content.Server.NPC.Queries.Queries; + +public sealed class ComponentFilter : UtilityQueryFilter +{ + /// + /// Components to filter for. + /// + [DataField("components", required: true)] + public ComponentRegistry Components = new(); +} diff --git a/Content.Server/NPC/Queries/Queries/InventoryQuery.cs b/Content.Server/NPC/Queries/Queries/InventoryQuery.cs new file mode 100644 index 0000000000..b1b250d820 --- /dev/null +++ b/Content.Server/NPC/Queries/Queries/InventoryQuery.cs @@ -0,0 +1,9 @@ +namespace Content.Server.NPC.Queries.Queries; + +/// +/// Returns inventory entities recursively. +/// +public sealed class InventoryQuery : UtilityQuery +{ + +} diff --git a/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs b/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs index 558d7c5170..1b7bf19198 100644 --- a/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs +++ b/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs @@ -18,66 +18,6 @@ public sealed partial class NPCCombatSystem { SubscribeLocalEvent(OnMeleeStartup); SubscribeLocalEvent(OnMeleeShutdown); - SubscribeLocalEvent(OnMeleeSteering); - } - - private void OnMeleeSteering(EntityUid uid, NPCMeleeCombatComponent component, ref NPCSteeringEvent args) - { - args.Steering.CanSeek = true; - - if (TryComp(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(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(); var physicsQuery = GetEntityQuery(); var curTime = _timing.CurTime; + var query = EntityQueryEnumerator(); - foreach (var (comp, _) in EntityQuery()) + while (query.MoveNext(out var uid, out var comp, out _)) { - var uid = comp.Owner; - if (!combatQuery.TryGetComponent(uid, out var combat) || !combat.IsInCombatMode) { RemComp(uid); @@ -126,7 +62,7 @@ public sealed partial class NPCCombatSystem { component.Status = CombatStatus.Normal; - if (!TryComp(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(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); } } } diff --git a/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs b/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs index c3822eea3d..cbec56aeb3 100644 --- a/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs +++ b/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs @@ -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 _combatQuery; + private EntityQuery _steeringQuery; + private EntityQuery _rechargeQuery; + private EntityQuery _physicsQuery; + private EntityQuery _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(); + _physicsQuery = GetEntityQuery(); + _rechargeQuery = GetEntityQuery(); + _steeringQuery = GetEntityQuery(); + _xformQuery = GetEntityQuery(); + SubscribeLocalEvent(OnRangedStartup); SubscribeLocalEvent(OnRangedShutdown); } @@ -48,9 +61,6 @@ public sealed partial class NPCCombatSystem private void UpdateRanged(float frameTime) { - var bodyQuery = GetEntityQuery(); - var xformQuery = GetEntityQuery(); - var combatQuery = GetEntityQuery(); var query = EntityQueryEnumerator(); 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; } } } diff --git a/Content.Server/NPC/Systems/NPCJukeSystem.cs b/Content.Server/NPC/Systems/NPCJukeSystem.cs new file mode 100644 index 0000000000..1f089a666c --- /dev/null +++ b/Content.Server/NPC/Systems/NPCJukeSystem.cs @@ -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 _npcMeleeQuery; + private EntityQuery _npcRangedQuery; + private EntityQuery _physicsQuery; + + public override void Initialize() + { + base.Initialize(); + _npcMeleeQuery = GetEntityQuery(); + _npcRangedQuery = GetEntityQuery(); + _physicsQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnJukeUnpaused); + SubscribeLocalEvent(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(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(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; + } + } +} diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs index 797030bd5f..8be04ec116 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs @@ -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(); - 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) diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.cs index 9fe9f5ace3..dcfd7ecc5f 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.cs @@ -65,8 +65,9 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem [Dependency] private readonly SharedCombatModeSystem _combat = default!; private EntityQuery _fixturesQuery; - private EntityQuery _physicsQuery; private EntityQuery _modifierQuery; + private EntityQuery _factionQuery; + private EntityQuery _physicsQuery; private EntityQuery _xformQuery; /// @@ -89,8 +90,9 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem base.Initialize(); _fixturesQuery = GetEntityQuery(); - _physicsQuery = GetEntityQuery(); _modifierQuery = GetEntityQuery(); + _factionQuery = GetEntityQuery(); + _physicsQuery = GetEntityQuery(); _xformQuery = GetEntityQuery(); #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() - .Select(o => (o.Item1.Owner, o.Item2, o.Item3, o.Item4)).ToArray(); + var npcs = new (EntityUid, NPCSteeringComponent, InputMoverComponent, TransformComponent)[Count()]; + + var query = EntityQueryEnumerator(); + 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(npcs.Length); + var data = new List(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; diff --git a/Content.Server/NPC/Systems/NPCSystem.cs b/Content.Server/NPC/Systems/NPCSystem.cs index b3e263a689..1a2d3fcab3 100644 --- a/Content.Server/NPC/Systems/NPCSystem.cs +++ b/Content.Server/NPC/Systems/NPCSystem.cs @@ -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!; - /// /// Whether any NPCs are allowed to run at all. /// @@ -35,8 +33,6 @@ namespace Content.Server.NPC.Systems { base.Initialize(); - _sawmill = Logger.GetSawmill("npc"); - _sawmill.Level = LogLevel.Info; SubscribeLocalEvent(OnMobStateChange); SubscribeLocalEvent(OnNPCMapInit); SubscribeLocalEvent(OnNPCShutdown); @@ -98,7 +94,7 @@ namespace Content.Server.NPC.Systems return; } - _sawmill.Debug($"Waking {ToPrettyString(uid)}"); + Log.Debug($"Waking {ToPrettyString(uid)}"); EnsureComp(uid); } @@ -109,7 +105,19 @@ namespace Content.Server.NPC.Systems return; } - _sawmill.Debug($"Sleeping {ToPrettyString(uid)}"); + // Don't bother with an event + if (TryComp(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(uid); } diff --git a/Content.Server/NPC/Systems/NPCUtilitySystem.cs b/Content.Server/NPC/Systems/NPCUtilitySystem.cs index 535eba6a54..10354e1336 100644 --- a/Content.Server/NPC/Systems/NPCUtilitySystem.cs +++ b/Content.Server/NPC/Systems/NPCUtilitySystem.cs @@ -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; /// 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 _xformQuery; + + private ObjectPool> _entPool = + new DefaultObjectPool>(new SetPolicy(), 256); + + public override void Initialize() + { + base.Initialize(); + _xformQuery = GetEntityQuery(); + } + /// /// Runs the UtilityQueryPrototype and returns the best-matching entities. /// @@ -47,7 +66,7 @@ public sealed class NPCUtilitySystem : EntitySystem // TODO: PickHostilesop or whatever needs to juse be UtilityQueryOperator var weh = _proto.Index(proto); - var ents = new HashSet(); + 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(); var highestScore = 0f; @@ -101,6 +123,7 @@ public sealed class NPCUtilitySystem : EntitySystem var result = new UtilityResult(results); blackboard.Remove(NPCBlackboard.UtilityTarget); + _entPool.Return(ents); return result; } @@ -115,7 +138,7 @@ public sealed class NPCUtilitySystem : EntitySystem case PresetCurve presetCurve: return GetScore(_proto.Index(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(activeHand.HeldEntity, out var heldGun)) + { + return 0f; + } + + if (heldGun.Whitelist?.IsValid(targetUid, EntityManager) != true) + { + return 0f; + } + + return 1f; + } case TargetDistanceCon: { var radius = blackboard.GetValueOrDefault(NPCBlackboard.VisionRadius, EntityManager); @@ -207,6 +245,23 @@ public sealed class NPCUtilitySystem : EntitySystem return Math.Clamp(distance / radius, 0f, 1f); } + case TargetAmmoCon: + { + if (!HasComp(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(NPCBlackboard.VisionRadius, EntityManager); const float bufferRange = 0.5f; - if (blackboard.TryGetValue("CombatTarget", out var currentTarget, EntityManager) && + if (blackboard.TryGetValue("Target", out var currentTarget, EntityManager) && currentTarget == targetUid && TryComp(owner, out var xform) && TryComp(targetUid, out var targetXform) && @@ -246,6 +301,15 @@ public sealed class NPCUtilitySystem : EntitySystem { return _mobState.IsDead(targetUid) ? 1f : 0f; } + case TargetMeleeCon: + { + if (TryComp(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 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 entities, UtilityQueryFilter filter) { switch (filter) { + case ComponentFilter compFilter: + { + var toRemove = new ValueList(); + + 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(); - var toRemove = new ValueList(); foreach (var ent in entities) diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs index a016ca4a92..214a06fdcf 100644 --- a/Content.Server/Zombies/ZombieSystem.Transform.cs +++ b/Content.Server/Zombies/ZombieSystem.Transform.cs @@ -232,7 +232,7 @@ namespace Content.Server.Zombies else { var htn = EnsureComp(target); - htn.RootTask = "SimpleHostileCompound"; + htn.RootTask = new HTNCompoundTask() {Task = "SimpleHostileCompound"}; htn.Blackboard.SetValue(NPCBlackboard.Owner, target); _npc.WakeNPC(target, htn); } diff --git a/Content.Shared/Inventory/InventorySystem.Helpers.cs b/Content.Shared/Inventory/InventorySystem.Helpers.cs index eec4e0e0fd..41bd6e0372 100644 --- a/Content.Shared/Inventory/InventorySystem.Helpers.cs +++ b/Content.Shared/Inventory/InventorySystem.Helpers.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Linq; using Robust.Shared.Prototypes; namespace Content.Shared.Inventory; diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.ChamberMagazine.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.ChamberMagazine.cs index 39e3a83a36..84c65eca95 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.ChamberMagazine.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.ChamberMagazine.cs @@ -15,6 +15,7 @@ public abstract partial class SharedGunSystem protected virtual void InitializeChamberMagazine() { SubscribeLocalEvent(OnChamberMagazineTakeAmmo); + SubscribeLocalEvent(OnChamberAmmoCount); SubscribeLocalEvent>(OnMagazineVerb); SubscribeLocalEvent(OnMagazineSlotChange); SubscribeLocalEvent(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 diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Magazine.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Magazine.cs index 2faa6da960..a0375e3b4a 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Magazine.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Magazine.cs @@ -14,6 +14,7 @@ public abstract partial class SharedGunSystem protected virtual void InitializeMagazine() { SubscribeLocalEvent(OnMagazineTakeAmmo); + SubscribeLocalEvent(OnMagazineAmmoCount); SubscribeLocalEvent>(OnMagazineVerb); SubscribeLocalEvent(OnMagazineSlotChange); SubscribeLocalEvent(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); diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 3e86569f64..01133abae0 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml b/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml index eea5b2e843..0845c53168 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml @@ -12,7 +12,8 @@ description: ghost-role-information-behonker-description - type: GhostTakeoverAvailable - type: HTN - rootTask: SimpleHostileCompound + rootTask: + task: SimpleHostileCompound - type: NpcFactionMember factions: - SimpleHostile diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml index 33caa1f19a..c725a49dd5 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml b/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml index 12ff6b8684..2a6fd443c8 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml @@ -6,7 +6,8 @@ abstract: true components: - type: HTN - rootTask: SimpleHostileCompound + rootTask: + task: SimpleHostileCompound blackboard: NavSmash: !type:Bool true diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/human.yml b/Resources/Prototypes/Entities/Mobs/NPCs/human.yml index 381904c801..4d16276346 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/human.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/human.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml index 4e32ede55c..529cc302be 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml @@ -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: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml index a5a9569f20..19586553c0 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml @@ -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] diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml index dd94bb616c..0d4f881f8d 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml index f03520adee..5939413208 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml @@ -16,7 +16,8 @@ - type: InputMover - type: MobMover - type: HTN - rootTask: IdleCompound + rootTask: + task: IdleCompound - type: Input context: "human" - type: NpcFactionMember diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml index 3bf94c23e0..760d642eb1 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml @@ -9,7 +9,8 @@ factions: - SimpleNeutral - type: HTN - rootTask: SimpleHostileCompound + rootTask: + task: SimpleHostileCompound - type: Sprite drawdepth: Mobs sprite: Mobs/Aliens/slimes.rsi diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml index c53770f583..34142c8393 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml b/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml index 6a097192e3..4119eb3b01 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml @@ -7,7 +7,8 @@ - type: InputMover - type: MobMover - type: HTN - rootTask: SimpleHostileCompound + rootTask: + task: SimpleHostileCompound - type: NpcFactionMember factions: - SimpleHostile diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml index 3958aa4fbe..ab2586670c 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml index eb57e32ec2..85f77ee000 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 1746e091e5..654a86a088 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/turrets.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/turrets.yml index 9bdee06750..67cd5534aa 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/turrets.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/turrets.yml @@ -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 diff --git a/Resources/Prototypes/NPCs/Combat/gun.yml b/Resources/Prototypes/NPCs/Combat/gun.yml new file mode 100644 index 0000000000..21066440b8 --- /dev/null +++ b/Resources/Prototypes/NPCs/Combat/gun.yml @@ -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 diff --git a/Resources/Prototypes/NPCs/Combat/melee.yml b/Resources/Prototypes/NPCs/Combat/melee.yml new file mode 100644 index 0000000000..aab6e4d148 --- /dev/null +++ b/Resources/Prototypes/NPCs/Combat/melee.yml @@ -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 diff --git a/Resources/Prototypes/NPCs/attack.yml b/Resources/Prototypes/NPCs/attack.yml deleted file mode 100644 index 4bfd50fb63..0000000000 --- a/Resources/Prototypes/NPCs/attack.yml +++ /dev/null @@ -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 diff --git a/Resources/Prototypes/NPCs/cleanbot.yml b/Resources/Prototypes/NPCs/cleanbot.yml index 60e8bdb8b6..887360372f 100644 --- a/Resources/Prototypes/NPCs/cleanbot.yml +++ b/Resources/Prototypes/NPCs/cleanbot.yml @@ -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 diff --git a/Resources/Prototypes/NPCs/clothing.yml b/Resources/Prototypes/NPCs/clothing.yml new file mode 100644 index 0000000000..2ea6061e35 --- /dev/null +++ b/Resources/Prototypes/NPCs/clothing.yml @@ -0,0 +1,5 @@ +# Generic pickups to not be naked. +# TODO: Armor / Pressure pickups. +# TODO: Preferred slots +#- type: htnCompound +# id: diff --git a/Resources/Prototypes/NPCs/follow.yml b/Resources/Prototypes/NPCs/follow.yml index e505a45f6d..094f7219c9 100644 --- a/Resources/Prototypes/NPCs/follow.yml +++ b/Resources/Prototypes/NPCs/follow.yml @@ -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 diff --git a/Resources/Prototypes/NPCs/generic.yml b/Resources/Prototypes/NPCs/generic.yml new file mode 100644 index 0000000000..5a24e37bc4 --- /dev/null +++ b/Resources/Prototypes/NPCs/generic.yml @@ -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 diff --git a/Resources/Prototypes/NPCs/idle.yml b/Resources/Prototypes/NPCs/idle.yml index f97e7dff63..6ddded67f7 100644 --- a/Resources/Prototypes/NPCs/idle.yml +++ b/Resources/Prototypes/NPCs/idle.yml @@ -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 diff --git a/Resources/Prototypes/NPCs/medibot.yml b/Resources/Prototypes/NPCs/medibot.yml index 08c30cb2a7..4710e69443 100644 --- a/Resources/Prototypes/NPCs/medibot.yml +++ b/Resources/Prototypes/NPCs/medibot.yml @@ -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 diff --git a/Resources/Prototypes/NPCs/mob.yml b/Resources/Prototypes/NPCs/mob.yml index dc1b970af8..61ab47c5b2 100644 --- a/Resources/Prototypes/NPCs/mob.yml +++ b/Resources/Prototypes/NPCs/mob.yml @@ -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 diff --git a/Resources/Prototypes/NPCs/nutrition.yml b/Resources/Prototypes/NPCs/nutrition.yml index 05a2c6cf75..da64989c33 100644 --- a/Resources/Prototypes/NPCs/nutrition.yml +++ b/Resources/Prototypes/NPCs/nutrition.yml @@ -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 diff --git a/Resources/Prototypes/NPCs/root.yml b/Resources/Prototypes/NPCs/root.yml new file mode 100644 index 0000000000..3d5de0915c --- /dev/null +++ b/Resources/Prototypes/NPCs/root.yml @@ -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 diff --git a/Resources/Prototypes/NPCs/test.yml b/Resources/Prototypes/NPCs/test.yml index f1e2399099..a26b72edf8 100644 --- a/Resources/Prototypes/NPCs/test.yml +++ b/Resources/Prototypes/NPCs/test.yml @@ -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 diff --git a/Resources/Prototypes/NPCs/utility_queries.yml b/Resources/Prototypes/NPCs/utility_queries.yml index f4cf83fa94..b6e037fdd0 100644 --- a/Resources/Prototypes/NPCs/utility_queries.yml +++ b/Resources/Prototypes/NPCs/utility_queries.yml @@ -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 diff --git a/Resources/Prototypes/NPCs/xeno.yml b/Resources/Prototypes/NPCs/xeno.yml index fca253c29c..1c51c09cb2 100644 --- a/Resources/Prototypes/NPCs/xeno.yml +++ b/Resources/Prototypes/NPCs/xeno.yml @@ -2,6 +2,8 @@ id: XenoCompound branches: - tasks: - - id: MeleeCombatCompound + - !type:HTNCompoundTask + task: MeleeCombatCompound - tasks: - - id: IdleCompound + - !type:HTNCompoundTask + task: IdleCompound diff --git a/Resources/Prototypes/Procedural/salvage_factions.yml b/Resources/Prototypes/Procedural/salvage_factions.yml index 74757697f2..f0aba69908 100644 --- a/Resources/Prototypes/Procedural/salvage_factions.yml +++ b/Resources/Prototypes/Procedural/salvage_factions.yml @@ -21,6 +21,10 @@ - id: WeaponTurretXeno amount: 3 prob: 0.25 + - entries: + - id: MobXenoSpitter + amount: 2 + prob: 0.25 - entries: - id: MobXenoRavager amount: 1 diff --git a/Resources/Textures/Mobs/Aliens/Carps/holo.rsi/base.png b/Resources/Textures/Mobs/Aliens/Carps/holo.rsi/alive.png similarity index 100% rename from Resources/Textures/Mobs/Aliens/Carps/holo.rsi/base.png rename to Resources/Textures/Mobs/Aliens/Carps/holo.rsi/alive.png diff --git a/Resources/Textures/Mobs/Aliens/Carps/holo.rsi/base_dead.png b/Resources/Textures/Mobs/Aliens/Carps/holo.rsi/dead.png similarity index 100% rename from Resources/Textures/Mobs/Aliens/Carps/holo.rsi/base_dead.png rename to Resources/Textures/Mobs/Aliens/Carps/holo.rsi/dead.png diff --git a/Resources/Textures/Mobs/Aliens/Carps/holo.rsi/meta.json b/Resources/Textures/Mobs/Aliens/Carps/holo.rsi/meta.json index 3fbb43275a..9f2c4fb5fa 100644 --- a/Resources/Textures/Mobs/Aliens/Carps/holo.rsi/meta.json +++ b/Resources/Textures/Mobs/Aliens/Carps/holo.rsi/meta.json @@ -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": [ [ diff --git a/Resources/Textures/Mobs/Aliens/Carps/magic.rsi/base.png b/Resources/Textures/Mobs/Aliens/Carps/magic.rsi/alive.png similarity index 100% rename from Resources/Textures/Mobs/Aliens/Carps/magic.rsi/base.png rename to Resources/Textures/Mobs/Aliens/Carps/magic.rsi/alive.png diff --git a/Resources/Textures/Mobs/Aliens/Carps/magic.rsi/base_dead.png b/Resources/Textures/Mobs/Aliens/Carps/magic.rsi/dead.png similarity index 100% rename from Resources/Textures/Mobs/Aliens/Carps/magic.rsi/base_dead.png rename to Resources/Textures/Mobs/Aliens/Carps/magic.rsi/dead.png diff --git a/Resources/Textures/Mobs/Aliens/Carps/magic.rsi/meta.json b/Resources/Textures/Mobs/Aliens/Carps/magic.rsi/meta.json index 9c8c90999c..05175508a3 100644 --- a/Resources/Textures/Mobs/Aliens/Carps/magic.rsi/meta.json +++ b/Resources/Textures/Mobs/Aliens/Carps/magic.rsi/meta.json @@ -11,10 +11,10 @@ "name": "icon" }, { - "name": "base_dead" + "name": "dead" }, { - "name": "base", + "name": "alive", "directions": 4, "delays": [ [ diff --git a/Resources/Textures/Mobs/Aliens/Carps/space.rsi/base.png b/Resources/Textures/Mobs/Aliens/Carps/space.rsi/alive.png similarity index 100% rename from Resources/Textures/Mobs/Aliens/Carps/space.rsi/base.png rename to Resources/Textures/Mobs/Aliens/Carps/space.rsi/alive.png diff --git a/Resources/Textures/Mobs/Aliens/Carps/space.rsi/base_dead.png b/Resources/Textures/Mobs/Aliens/Carps/space.rsi/dead.png similarity index 100% rename from Resources/Textures/Mobs/Aliens/Carps/space.rsi/base_dead.png rename to Resources/Textures/Mobs/Aliens/Carps/space.rsi/dead.png diff --git a/Resources/Textures/Mobs/Aliens/Carps/space.rsi/meta.json b/Resources/Textures/Mobs/Aliens/Carps/space.rsi/meta.json index 4539505970..68851e549f 100644 --- a/Resources/Textures/Mobs/Aliens/Carps/space.rsi/meta.json +++ b/Resources/Textures/Mobs/Aliens/Carps/space.rsi/meta.json @@ -40,10 +40,10 @@ "name": "dead_mouth" }, { - "name": "base_dead" + "name": "dead" }, { - "name": "base", + "name": "alive", "directions": 4, "delays": [ [