]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Medibot doAfter and some other improvements (#32932)
authorosjarw <62134478+osjarw@users.noreply.github.com>
Tue, 20 Jan 2026 20:37:51 +0000 (22:37 +0200)
committerGitHub <noreply@github.com>
Tue, 20 Jan 2026 20:37:51 +0000 (20:37 +0000)
* Medibot doAfter and some other improvements

* Clean-up

* Review fixes

* the army of medibots chasing someone is really funny

* misc cleanup

---------

Co-authored-by: SlamBamActionman <slambamactionman@gmail.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Content.Server/NPC/HTN/Preconditions/TargetInRangePrecondition.cs
Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs
Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/EnsureComponentOperator.cs [new file with mode: 0644]
Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickNearbyInjectableOperator.cs
Content.Server/NPC/HTN/PrimitiveTasks/Operators/UtilityOperator.cs
Content.Server/NPC/Systems/NPCUtilitySystem.cs
Content.Shared/Silicons/Bots/MedibotSystem.cs
Resources/Prototypes/NPCs/medibot.yml
Resources/Prototypes/NPCs/utility_queries.yml

index 921b5ffa226339385daade67c9eb031bdbbc3b48..b88d17b13d94a57a9c6a6accedcf91af32d128f8 100644 (file)
@@ -20,6 +20,9 @@ public sealed partial class TargetInRangePrecondition : HTNPrecondition
         _transformSystem = sysManager.GetEntitySystem<SharedTransformSystem>();
     }
 
+    [DataField]
+    public bool Invert;
+
     public override bool IsMet(NPCBlackboard blackboard)
     {
         if (!blackboard.TryGetValue<EntityCoordinates>(NPCBlackboard.OwnerCoordinates, out var coordinates, _entManager))
@@ -29,7 +32,6 @@ public sealed partial class TargetInRangePrecondition : HTNPrecondition
             !_entManager.TryGetComponent<TransformComponent>(target, out var targetXform))
             return false;
 
-        var transformSystem = _entManager.System<SharedTransformSystem>;
-        return _transformSystem.InRange(coordinates, targetXform.Coordinates, blackboard.GetValueOrDefault<float>(RangeKey, _entManager));
+        return _transformSystem.InRange(coordinates, targetXform.Coordinates, blackboard.GetValueOrDefault<float>(RangeKey, _entManager)) ^ Invert;
     }
 }
index f3b977518b8a18e34ad0a87924df47e8e8f4428e..98c63dd912c9b5b4fe57e763334af614e9d2d07e 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Server.Chat.Systems;
+using Robust.Shared.Timing;
 using Content.Shared.Chat;
 using Content.Shared.Dataset;
 using Content.Shared.Random.Helpers;
@@ -11,6 +12,8 @@ namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
 
 public sealed partial class SpeakOperator : HTNOperator
 {
+    [Dependency] private readonly IEntityManager _entMan = default!;
+    [Dependency] private readonly IGameTiming _gameTiming = default!;
     private ChatSystem _chat = default!;
     [Dependency] private readonly IPrototypeManager _proto = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
@@ -24,6 +27,18 @@ public sealed partial class SpeakOperator : HTNOperator
     [DataField]
     public bool Hidden;
 
+    /// <summary>
+    /// Skip speaking for `cooldown` seconds, intended to stop spam
+    /// </summary>
+    [DataField]
+    public TimeSpan Cooldown = TimeSpan.Zero;
+
+    /// <summary>
+    /// Define what key is used for storing the cooldown
+    /// </summary>
+    [DataField]
+    public string CooldownID = string.Empty;
+
     public override void Initialize(IEntitySystemManager sysManager)
     {
         base.Initialize(sysManager);
@@ -32,6 +47,14 @@ public sealed partial class SpeakOperator : HTNOperator
 
     public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
     {
+        if (Cooldown != TimeSpan.Zero && CooldownID != string.Empty)
+        {
+            if (blackboard.TryGetValue<TimeSpan>(CooldownID, out var nextSpeechTime, _entMan) && _gameTiming.CurTime < nextSpeechTime)
+                return base.Update(blackboard, frameTime);
+
+            blackboard.SetValue(CooldownID, _gameTiming.CurTime + Cooldown);
+        }
+
         LocId speechLocId;
         switch (Speech)
         {
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/EnsureComponentOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/EnsureComponentOperator.cs
new file mode 100644 (file)
index 0000000..317b7aa
--- /dev/null
@@ -0,0 +1,29 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Specific;
+
+public sealed partial class EnsureComponentOperator : HTNOperator
+{
+    [Dependency] private readonly IEntityManager _entMan = default!;
+
+    /// <summary>
+    /// Target entity to inject.
+    /// </summary>
+    [DataField(required: true)]
+    public string TargetKey = string.Empty;
+
+    /// <summary>
+    /// Components to be added
+    /// </summary>
+    [DataField]
+    public ComponentRegistry Components = new();
+
+    public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
+    {
+        if (!blackboard.TryGetValue<EntityUid>(TargetKey, out var target, _entMan))
+            return HTNOperatorStatus.Failed;
+
+        _entMan.AddComponents(target, Components);
+        return HTNOperatorStatus.Finished;
+    }
+}
index 67a8198c385f2d570d68224ba45711fe1e3b5a7e..6f656b0e29128193a8ac22e8e3c1b54bb0f565b1 100644 (file)
@@ -14,10 +14,15 @@ namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Specific;
 public sealed partial class PickNearbyInjectableOperator : HTNOperator
 {
     [Dependency] private readonly IEntityManager _entManager = default!;
-    private EntityLookupSystem _lookup = default!;
     private MedibotSystem _medibot = default!;
     private PathfindingSystem _pathfinding = default!;
 
+    private EntityQuery<DamageableComponent> _damageQuery = default!;
+    private EntityQuery<InjectableSolutionComponent> _injectQuery = default!;
+    private EntityQuery<NPCRecentlyInjectedComponent> _recentlyInjected = default!;
+    private EntityQuery<MobStateComponent> _mobState = default!;
+    private EntityQuery<EmaggedComponent> _emaggedQuery = default!;
+
     [DataField("rangeKey")] public string RangeKey = NPCBlackboard.MedibotInjectRange;
 
     /// <summary>
@@ -35,9 +40,14 @@ public sealed partial class PickNearbyInjectableOperator : HTNOperator
     public override void Initialize(IEntitySystemManager sysManager)
     {
         base.Initialize(sysManager);
-        _lookup = sysManager.GetEntitySystem<EntityLookupSystem>();
         _medibot = sysManager.GetEntitySystem<MedibotSystem>();
         _pathfinding = sysManager.GetEntitySystem<PathfindingSystem>();
+
+        _damageQuery = _entManager.GetEntityQuery<DamageableComponent>();
+        _injectQuery = _entManager.GetEntityQuery<InjectableSolutionComponent>();
+        _recentlyInjected = _entManager.GetEntityQuery<NPCRecentlyInjectedComponent>();
+        _mobState = _entManager.GetEntityQuery<MobStateComponent>();
+        _emaggedQuery = _entManager.GetEntityQuery<EmaggedComponent>();
     }
 
     public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
@@ -51,18 +61,16 @@ public sealed partial class PickNearbyInjectableOperator : HTNOperator
         if (!_entManager.TryGetComponent<MedibotComponent>(owner, out var medibot))
             return (false, null);
 
-        var damageQuery = _entManager.GetEntityQuery<DamageableComponent>();
-        var injectQuery = _entManager.GetEntityQuery<InjectableSolutionComponent>();
-        var recentlyInjected = _entManager.GetEntityQuery<NPCRecentlyInjectedComponent>();
-        var mobState = _entManager.GetEntityQuery<MobStateComponent>();
-        var emaggedQuery = _entManager.GetEntityQuery<EmaggedComponent>();
 
-        foreach (var entity in _lookup.GetEntitiesInRange(owner, range))
+        if (!blackboard.TryGetValue<IEnumerable<KeyValuePair<EntityUid, float>>>("TargetList", out var patients, _entManager))
+            return (false, null);
+
+        foreach (var (entity, _) in patients)
         {
-            if (mobState.TryGetComponent(entity, out var state) &&
-                injectQuery.HasComponent(entity) &&
-                damageQuery.TryGetComponent(entity, out var damage) &&
-                !recentlyInjected.HasComponent(entity))
+            if (_mobState.TryGetComponent(entity, out var state) &&
+                _injectQuery.HasComponent(entity) &&
+                _damageQuery.TryGetComponent(entity, out var damage) &&
+                !_recentlyInjected.HasComponent(entity))
             {
                 // no treating dead bodies
                 if (!_medibot.TryGetTreatment(medibot, state.CurrentState, out var treatment))
@@ -71,7 +79,7 @@ public sealed partial class PickNearbyInjectableOperator : HTNOperator
                 // Only go towards a target if the bot can actually help them or if the medibot is emagged
                 // note: this and the actual injecting don't check for specific damage types so for example,
                 // radiation damage will trigger injection but the tricordrazine won't heal it.
-                if (!emaggedQuery.HasComponent(entity) && !treatment.IsValid(damage.TotalDamage))
+                if (!_emaggedQuery.HasComponent(entity) && !treatment.IsValid(damage.TotalDamage))
                     continue;
 
                 //Needed to make sure it doesn't sometimes stop right outside it's interaction range
index 2bf9b09b10690d29f1e3bed16c73c254106e569f..16f18ae59a7ad152f5ea03e8c732badfcaf699cd 100644 (file)
@@ -1,3 +1,4 @@
+using System.Linq;
 using System.Numerics;
 using System.Threading;
 using System.Threading.Tasks;
@@ -15,7 +16,9 @@ public sealed partial class UtilityOperator : HTNOperator
 {
     [Dependency] private readonly IEntityManager _entManager = default!;
 
-    [DataField("key")] public string Key = "Target";
+    [DataField] public string Key = "Target";
+
+    [DataField] public ReturnTypeResult ReturnType = ReturnTypeResult.Highest;
 
     /// <summary>
     /// The EntityCoordinates of the specified target.
@@ -30,19 +33,44 @@ public sealed partial class UtilityOperator : HTNOperator
         CancellationToken cancelToken)
     {
         var result = _entManager.System<NPCUtilitySystem>().GetEntities(blackboard, Prototype);
-        var target = result.GetHighest();
+        Dictionary<string, object> effects;
 
-        if (!target.IsValid())
+        switch (ReturnType)
         {
-            return (false, new Dictionary<string, object>());
-        }
+            case ReturnTypeResult.Highest:
+                var target = result.GetHighest();
 
-        var effects = new Dictionary<string, object>()
-        {
-            {Key, target},
-            {KeyCoordinates, new EntityCoordinates(target, Vector2.Zero)}
-        };
+                if (!target.IsValid())
+                {
+                    return (false, new Dictionary<string, object>());
+                }
+
+                effects = new Dictionary<string, object>()
+                {
+                    {Key, target},
+                    {KeyCoordinates, new EntityCoordinates(target, Vector2.Zero)},
+                };
+
+                return (true, effects);
+
+            case ReturnTypeResult.EnumerableDescending:
+                var targetList = result.GetEnumerable();
 
-        return (true, effects);
+                effects = new Dictionary<string, object>()
+                {
+                    {"TargetList", targetList},
+                };
+
+                return (true, effects);
+
+            default:
+                throw new NotImplementedException();
+        }
+    }
+
+    public enum ReturnTypeResult
+    {
+        Highest,
+        EnumerableDescending
     }
 }
index 9605b6284746304f3e11003428b4e3282a357da5..9b791ae2f05ea4bab6f916a2c2ba20f85a762906 100644 (file)
@@ -602,4 +602,12 @@ public readonly record struct UtilityResult(Dictionary<EntityUid, float> Entitie
 
         return Entities.MinBy(x => x.Value).Key;
     }
+
+    /// <summary>
+    /// Returns a GetEnumerable sorted in descending score.
+    /// </summary>
+    public IEnumerable<KeyValuePair<EntityUid, float>> GetEnumerable()
+    {
+        return Entities.OrderByDescending(x => x.Value);
+    }
 }
index 2e832da456c3a338ac1cad4bed1799fbc716ec51..b960e190682d4116d0098d6698cfc6d8fcb51574 100644 (file)
@@ -132,7 +132,6 @@ public sealed class MedibotSystem : EntitySystem
         if (!TryGetTreatment(medibot.Comp, mobState.CurrentState, out var treatment)) return false;
         if (!_solutionContainer.TryGetInjectableSolution(target, out var injectable, out _)) return false;
 
-        EnsureComp<NPCRecentlyInjectedComponent>(target);
         _solutionContainer.TryAddReagent(injectable.Value, treatment.Reagent, treatment.Quantity, out _);
 
         _popup.PopupEntity(Loc.GetString("injector-component-feel-prick-message"), target, target);
index 1cd6352e168086e8808c1f54195dc0fd98d4afd2..cd15904ff64e4dbd5d6c828b4992e1b17e98f9ec 100644 (file)
@@ -1,46 +1,64 @@
 - type: htnCompound
   id: MedibotCompound
   branches:
-    - tasks:
-        - !type:HTNCompoundTask
-          task: InjectNearbyCompound
-    - tasks:
-        - !type:HTNCompoundTask
-          task: IdleCompound
+  # Observe for targets
+  - tasks:
+    - !type:HTNPrimitiveTask
+      operator: !type:UtilityOperator
+        proto: MedibotInjectable
+        returnType: EnumerableDescending
+    - !type:HTNPrimitiveTask
+      operator: !type:PickNearbyInjectableOperator
+        targetKey: Target
+        targetMoveKey: TargetCoordinates
 
-- type: htnCompound
-  id: InjectNearbyCompound
-  branches:
-    - tasks:
-        # TODO: Kill this shit
-        - !type:HTNPrimitiveTask
-          operator: !type:PickNearbyInjectableOperator
-            targetKey: InjectTarget
-            targetMoveKey: TargetCoordinates
+    - !type:HTNCompoundTask
+      task: MedibotGetInRange
+    - !type:HTNCompoundTask
+      task: MedibotInject
 
-        - !type:HTNPrimitiveTask
-          operator: !type:SpeakOperator
-            speech: !type:SingleSpeakOperatorSpeech
-              line: medibot-start-inject
-            hidden: true
+  # Idle when targets not found
+  - tasks:
+    - !type:HTNCompoundTask
+      task: IdleCompound
 
-        - !type:HTNPrimitiveTask
-          operator: !type:MoveToOperator
-            pathfindInPlanning: false
+- type: htnCompound
+  id: MedibotGetInRange
+  branches:
+  # Move to target if out of range
+  - preconditions:
+    - !type:TargetInRangePrecondition
+      invert: true
+      targetKey: Target
+      rangeKey: InteractRange
+    tasks:
+    - !type:HTNPrimitiveTask
+      operator: !type:SpeakOperator
+        speech: !type:SingleSpeakOperatorSpeech
+          line: medibot-start-inject
+        hidden: true
+        cooldownID: medibot-start-inject
+        cooldown: 5
+    - !type:HTNPrimitiveTask
+      operator: !type:MoveToOperator
+        pathfindInPlanning: false
 
-        - !type:HTNPrimitiveTask
-          operator: !type:SetFloatOperator
-            targetKey: IdleTime
-            amount: 3
+  - tasks:
+    - !type:HTNPrimitiveTask
+      operator: !type:NoOperator
 
-        - !type:HTNPrimitiveTask
-          operator: !type:WaitOperator
-            key: IdleTime
-          preconditions:
-            - !type:KeyExistsPrecondition
-              key: IdleTime
+# Should be called only when in range
+- type: htnCompound
+  id: MedibotInject
+  branches:
+  - tasks:
+    - !type:HTNPrimitiveTask
+      operator: !type:InteractWithOperator
+        expectDoAfter: true
+        targetKey: Target
+    - !type:HTNPrimitiveTask
+      operator: !type:EnsureComponentOperator
+        targetKey: Target
+        components:
+        - type: NPCRecentlyInjected
 
-        # TODO: Kill this
-        - !type:HTNPrimitiveTask
-          operator: !type:MedibotInjectOperator
-            targetKey: InjectTarget
index 3274bdf97746bd1aea22a2dd670735cb01380e69..49b085b29ab600423a061415e975d0c89b211099 100644 (file)
     - !type:TargetInLOSOrCurrentCon
       curve: !type:BoolCurve
 
+- type: utilityQuery
+  id: MedibotInjectable
+  query:
+    - !type:ComponentQuery
+      components:
+        - type: InjectableSolution
+        - type: Damageable
+        - type: MobState
+    - !type:ComponentFilter
+      components:
+        - type: NPCRecentlyInjected
+      retainWithComp: false
+  considerations:
+    - !type:TargetIsCritCon
+      curve: !type:QuadraticCurve
+        slope: 1
+        exponent: 1
+        yOffset: 0.1
+        xOffset: 0
+    - !type:TargetDistanceCon
+      curve: !type:PresetCurve
+        preset: TargetDistance
+
 - type: utilityQuery
   id: NearbyGunTargets
   query: