From: osjarw <62134478+osjarw@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:37:51 +0000 (+0200) Subject: Medibot doAfter and some other improvements (#32932) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=892209db0184e5dfc09c0672cb5823fa58fb5cc4;p=space-station-14.git Medibot doAfter and some other improvements (#32932) * 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 Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> --- diff --git a/Content.Server/NPC/HTN/Preconditions/TargetInRangePrecondition.cs b/Content.Server/NPC/HTN/Preconditions/TargetInRangePrecondition.cs index 921b5ffa22..b88d17b13d 100644 --- a/Content.Server/NPC/HTN/Preconditions/TargetInRangePrecondition.cs +++ b/Content.Server/NPC/HTN/Preconditions/TargetInRangePrecondition.cs @@ -20,6 +20,9 @@ public sealed partial class TargetInRangePrecondition : HTNPrecondition _transformSystem = sysManager.GetEntitySystem(); } + [DataField] + public bool Invert; + public override bool IsMet(NPCBlackboard blackboard) { if (!blackboard.TryGetValue(NPCBlackboard.OwnerCoordinates, out var coordinates, _entManager)) @@ -29,7 +32,6 @@ public sealed partial class TargetInRangePrecondition : HTNPrecondition !_entManager.TryGetComponent(target, out var targetXform)) return false; - var transformSystem = _entManager.System; - return _transformSystem.InRange(coordinates, targetXform.Coordinates, blackboard.GetValueOrDefault(RangeKey, _entManager)); + return _transformSystem.InRange(coordinates, targetXform.Coordinates, blackboard.GetValueOrDefault(RangeKey, _entManager)) ^ Invert; } } diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs index f3b977518b..98c63dd912 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs @@ -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; + /// + /// Skip speaking for `cooldown` seconds, intended to stop spam + /// + [DataField] + public TimeSpan Cooldown = TimeSpan.Zero; + + /// + /// Define what key is used for storing the cooldown + /// + [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(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 index 0000000000..317b7aacf2 --- /dev/null +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/EnsureComponentOperator.cs @@ -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!; + + /// + /// Target entity to inject. + /// + [DataField(required: true)] + public string TargetKey = string.Empty; + + /// + /// Components to be added + /// + [DataField] + public ComponentRegistry Components = new(); + + public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) + { + if (!blackboard.TryGetValue(TargetKey, out var target, _entMan)) + return HTNOperatorStatus.Failed; + + _entMan.AddComponents(target, Components); + return HTNOperatorStatus.Finished; + } +} diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickNearbyInjectableOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickNearbyInjectableOperator.cs index 67a8198c38..6f656b0e29 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickNearbyInjectableOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickNearbyInjectableOperator.cs @@ -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 _damageQuery = default!; + private EntityQuery _injectQuery = default!; + private EntityQuery _recentlyInjected = default!; + private EntityQuery _mobState = default!; + private EntityQuery _emaggedQuery = default!; + [DataField("rangeKey")] public string RangeKey = NPCBlackboard.MedibotInjectRange; /// @@ -35,9 +40,14 @@ public sealed partial class PickNearbyInjectableOperator : HTNOperator public override void Initialize(IEntitySystemManager sysManager) { base.Initialize(sysManager); - _lookup = sysManager.GetEntitySystem(); _medibot = sysManager.GetEntitySystem(); _pathfinding = sysManager.GetEntitySystem(); + + _damageQuery = _entManager.GetEntityQuery(); + _injectQuery = _entManager.GetEntityQuery(); + _recentlyInjected = _entManager.GetEntityQuery(); + _mobState = _entManager.GetEntityQuery(); + _emaggedQuery = _entManager.GetEntityQuery(); } public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard, @@ -51,18 +61,16 @@ public sealed partial class PickNearbyInjectableOperator : HTNOperator if (!_entManager.TryGetComponent(owner, out var medibot)) return (false, null); - var damageQuery = _entManager.GetEntityQuery(); - var injectQuery = _entManager.GetEntityQuery(); - var recentlyInjected = _entManager.GetEntityQuery(); - var mobState = _entManager.GetEntityQuery(); - var emaggedQuery = _entManager.GetEntityQuery(); - foreach (var entity in _lookup.GetEntitiesInRange(owner, range)) + if (!blackboard.TryGetValue>>("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 diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/UtilityOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/UtilityOperator.cs index 2bf9b09b10..16f18ae59a 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/UtilityOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/UtilityOperator.cs @@ -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; /// /// The EntityCoordinates of the specified target. @@ -30,19 +33,44 @@ public sealed partial class UtilityOperator : HTNOperator CancellationToken cancelToken) { var result = _entManager.System().GetEntities(blackboard, Prototype); - var target = result.GetHighest(); + Dictionary effects; - if (!target.IsValid()) + switch (ReturnType) { - return (false, new Dictionary()); - } + case ReturnTypeResult.Highest: + var target = result.GetHighest(); - var effects = new Dictionary() - { - {Key, target}, - {KeyCoordinates, new EntityCoordinates(target, Vector2.Zero)} - }; + if (!target.IsValid()) + { + return (false, new Dictionary()); + } + + effects = new Dictionary() + { + {Key, target}, + {KeyCoordinates, new EntityCoordinates(target, Vector2.Zero)}, + }; + + return (true, effects); + + case ReturnTypeResult.EnumerableDescending: + var targetList = result.GetEnumerable(); - return (true, effects); + effects = new Dictionary() + { + {"TargetList", targetList}, + }; + + return (true, effects); + + default: + throw new NotImplementedException(); + } + } + + public enum ReturnTypeResult + { + Highest, + EnumerableDescending } } diff --git a/Content.Server/NPC/Systems/NPCUtilitySystem.cs b/Content.Server/NPC/Systems/NPCUtilitySystem.cs index 9605b62847..9b791ae2f0 100644 --- a/Content.Server/NPC/Systems/NPCUtilitySystem.cs +++ b/Content.Server/NPC/Systems/NPCUtilitySystem.cs @@ -602,4 +602,12 @@ public readonly record struct UtilityResult(Dictionary Entitie return Entities.MinBy(x => x.Value).Key; } + + /// + /// Returns a GetEnumerable sorted in descending score. + /// + public IEnumerable> GetEnumerable() + { + return Entities.OrderByDescending(x => x.Value); + } } diff --git a/Content.Shared/Silicons/Bots/MedibotSystem.cs b/Content.Shared/Silicons/Bots/MedibotSystem.cs index 2e832da456..b960e19068 100644 --- a/Content.Shared/Silicons/Bots/MedibotSystem.cs +++ b/Content.Shared/Silicons/Bots/MedibotSystem.cs @@ -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(target); _solutionContainer.TryAddReagent(injectable.Value, treatment.Reagent, treatment.Quantity, out _); _popup.PopupEntity(Loc.GetString("injector-component-feel-prick-message"), target, target); diff --git a/Resources/Prototypes/NPCs/medibot.yml b/Resources/Prototypes/NPCs/medibot.yml index 1cd6352e16..cd15904ff6 100644 --- a/Resources/Prototypes/NPCs/medibot.yml +++ b/Resources/Prototypes/NPCs/medibot.yml @@ -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 diff --git a/Resources/Prototypes/NPCs/utility_queries.yml b/Resources/Prototypes/NPCs/utility_queries.yml index 3274bdf977..49b085b29a 100644 --- a/Resources/Prototypes/NPCs/utility_queries.yml +++ b/Resources/Prototypes/NPCs/utility_queries.yml @@ -202,6 +202,29 @@ - !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: