_transformSystem = sysManager.GetEntitySystem<SharedTransformSystem>();
}
+ [DataField]
+ public bool Invert;
+
public override bool IsMet(NPCBlackboard blackboard)
{
if (!blackboard.TryGetValue<EntityCoordinates>(NPCBlackboard.OwnerCoordinates, out var coordinates, _entManager))
!_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;
}
}
using Content.Server.Chat.Systems;
+using Robust.Shared.Timing;
using Content.Shared.Chat;
using Content.Shared.Dataset;
using Content.Shared.Random.Helpers;
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!;
[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);
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)
{
--- /dev/null
+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;
+ }
+}
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>
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,
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))
// 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
+using System.Linq;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
{
[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.
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
}
}
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);
+ }
}
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);
- 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
- !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: