mirror of
https://github.com/space-wizards/space-station-14.git
synced 2026-02-15 03:31:30 +01:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user