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 <slambamactionman@gmail.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
This commit is contained in:
osjarw
2026-01-20 22:37:51 +02:00
committed by GitHub
parent 0673809762
commit 892209db01
9 changed files with 202 additions and 64 deletions

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

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

View File

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

View 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

View 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();
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();
effects = new Dictionary<string, object>()
{
{"TargetList", targetList},
};
return (true, effects);
default:
throw new NotImplementedException();
}
}
var effects = new Dictionary<string, object>()
{
{Key, target},
{KeyCoordinates, new EntityCoordinates(target, Vector2.Zero)}
};
return (true, effects);
public enum ReturnTypeResult
{
Highest,
EnumerableDescending
}
}

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

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

View 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:HTNCompoundTask
task: MedibotGetInRange
- !type:HTNCompoundTask
task: MedibotInject
# Idle when targets not found
- tasks:
- !type:HTNCompoundTask
task: IdleCompound
- type: htnCompound
id: InjectNearbyCompound
id: MedibotGetInRange
branches:
- tasks:
# TODO: Kill this shit
- !type:HTNPrimitiveTask
operator: !type:PickNearbyInjectableOperator
targetKey: InjectTarget
targetMoveKey: TargetCoordinates
# 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:SpeakOperator
speech: !type:SingleSpeakOperatorSpeech
line: medibot-start-inject
hidden: true
- tasks:
- !type:HTNPrimitiveTask
operator: !type:NoOperator
- !type:HTNPrimitiveTask
operator: !type:MoveToOperator
pathfindInPlanning: false
# 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
- !type:HTNPrimitiveTask
operator: !type:SetFloatOperator
targetKey: IdleTime
amount: 3
- !type:HTNPrimitiveTask
operator: !type:WaitOperator
key: IdleTime
preconditions:
- !type:KeyExistsPrecondition
key: IdleTime
# TODO: Kill this
- !type:HTNPrimitiveTask
operator: !type:MedibotInjectOperator
targetKey: InjectTarget

View File

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