NPC refactor (#10122)

Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
This commit is contained in:
metalgearsloth
2022-09-06 00:28:23 +10:00
committed by GitHub
parent 138e328c04
commit 0286b88388
290 changed files with 13842 additions and 5939 deletions

View File

@@ -0,0 +1,99 @@
using System.Threading.Tasks;
using Content.Server.MobState;
using Content.Server.NPC.Components;
using Content.Shared.MobState;
using Content.Shared.MobState.Components;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Melee;
/// <summary>
/// Attacks the specified key in melee combat.
/// </summary>
public sealed class MeleeOperator : HTNOperator
{
[Dependency] private readonly IEntityManager _entManager = default!;
/// <summary>
/// Key that contains the target entity.
/// </summary>
[ViewVariables, DataField("targetKey", required: true)]
public string TargetKey = default!;
/// <summary>
/// Minimum damage state that the target has to be in for us to consider attacking.
/// </summary>
[ViewVariables, DataField("targetState")]
public DamageState TargetState = DamageState.Alive;
// Like movement we add a component and pass it off to the dedicated system.
public override void Startup(NPCBlackboard blackboard)
{
base.Startup(blackboard);
var melee = _entManager.EnsureComponent<NPCMeleeCombatComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
melee.Target = blackboard.GetValue<EntityUid>(TargetKey);
}
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
{
// Don't attack if they're already as wounded as we want them.
if (!blackboard.TryGetValue<EntityUid>(TargetKey, out var target))
{
return (false, null);
}
if (_entManager.TryGetComponent<MobStateComponent>(target, out var mobState) &&
mobState.CurrentState != null &&
mobState.CurrentState > TargetState)
{
return (false, null);
}
return (true, null);
}
public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
{
base.Shutdown(blackboard, status);
_entManager.RemoveComponent<NPCMeleeCombatComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
blackboard.Remove<EntityUid>(TargetKey);
}
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
{
base.Update(blackboard, frameTime);
// TODO:
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
var status = HTNOperatorStatus.Continuing;
if (_entManager.TryGetComponent<NPCMeleeCombatComponent>(owner, out var combat))
{
// Success
if (_entManager.TryGetComponent<MobStateComponent>(combat.Target, out var mobState) &&
mobState.CurrentState != null &&
mobState.CurrentState > TargetState)
{
status = HTNOperatorStatus.Finished;
}
else
{
switch (combat.Status)
{
case CombatStatus.Normal:
status = HTNOperatorStatus.Continuing;
break;
default:
status = HTNOperatorStatus.Failed;
break;
}
}
}
if (status != HTNOperatorStatus.Continuing)
{
_entManager.RemoveComponent<NPCMeleeCombatComponent>(owner);
}
return status;
}
}

View File

@@ -0,0 +1,33 @@
using Robust.Shared.Map;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Melee;
/// <summary>
/// Selects a target for melee.
/// </summary>
public sealed class PickMeleeTargetOperator : NPCCombatOperator
{
protected override float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, bool canMove, EntityQuery<TransformComponent> xformQuery)
{
var ourCoordinates = blackboard.GetValueOrDefault<EntityCoordinates>(NPCBlackboard.OwnerCoordinates);
if (!xformQuery.TryGetComponent(uid, out var targetXform))
return -1f;
var targetCoordinates = targetXform.Coordinates;
if (!ourCoordinates.TryDistance(EntManager, targetCoordinates, out var distance))
return -1f;
var rating = 0f;
if (existingTarget == uid)
{
rating += 3f;
}
rating += 1f / distance * 4f;
return rating;
}
}

View File

@@ -0,0 +1,190 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.NPC.Components;
using Content.Server.NPC.Pathfinding;
using Content.Server.NPC.Pathfinding.Pathfinders;
using Content.Server.NPC.Systems;
using Robust.Shared.Map;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
/// <summary>
/// Moves an NPC to the specified target key. Hands the actual steering off to NPCSystem.Steering
/// </summary>
public sealed class MoveToOperator : HTNOperator
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
private NPCSteeringSystem _steering = default!;
private PathfindingSystem _pathfind = default!;
/// <summary>
/// Should we assume the MovementTarget is reachable during planning or should we pathfind to it?
/// </summary>
[ViewVariables, DataField("pathfindInPlanning")]
public bool PathfindInPlanning = true;
/// <summary>
/// When we're finished moving to the target should we remove its key?
/// </summary>
[ViewVariables, DataField("removeKeyOnFinish")]
public bool RemoveKeyOnFinish = true;
/// <summary>
/// Target Coordinates to move to. This gets removed after execution.
/// </summary>
[ViewVariables, DataField("targetKey")]
public string TargetKey = "MovementTarget";
/// <summary>
/// Where the pathfinding result will be stored (if applicable). This gets removed after execution.
/// </summary>
[ViewVariables, DataField("pathfindKey")]
public string PathfindKey = "MovementPathfind";
/// <summary>
/// How close we need to get before considering movement finished.
/// </summary>
[ViewVariables, DataField("rangeKey")]
public string RangeKey = "MovementRange";
private const string MovementCancelToken = "MovementCancelToken";
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
_pathfind = sysManager.GetEntitySystem<PathfindingSystem>();
_steering = sysManager.GetEntitySystem<NPCSteeringSystem>();
}
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
{
if (!blackboard.TryGetValue<EntityCoordinates>(TargetKey, out var targetCoordinates))
{
return (false, null);
}
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
if (!_entManager.TryGetComponent<TransformComponent>(owner, out var xform) ||
!_entManager.TryGetComponent<PhysicsComponent>(owner, out var body))
return (false, null);
if (!_mapManager.TryGetGrid(xform.GridUid, out var ownerGrid) ||
!_mapManager.TryGetGrid(targetCoordinates.GetGridUid(_entManager), out var targetGrid) ||
ownerGrid != targetGrid)
{
return (false, null);
}
var range = blackboard.GetValueOrDefault<float>(RangeKey);
if (xform.Coordinates.TryDistance(_entManager, targetCoordinates, out var distance) && distance <= range)
{
// In range
return (true, new Dictionary<string, object>()
{
{NPCBlackboard.OwnerCoordinates, blackboard.GetValueOrDefault<EntityCoordinates>(NPCBlackboard.OwnerCoordinates)}
});
}
if (!PathfindInPlanning)
{
return (true, new Dictionary<string, object>()
{
{NPCBlackboard.OwnerCoordinates, targetCoordinates}
});
}
var cancelToken = new CancellationTokenSource();
var access = blackboard.GetValueOrDefault<ICollection<string>>(NPCBlackboard.Access) ?? new List<string>();
var job = _pathfind.RequestPath(
new PathfindingArgs(
blackboard.GetValue<EntityUid>(NPCBlackboard.Owner),
access,
body.CollisionMask,
ownerGrid.GetTileRef(xform.Coordinates),
ownerGrid.GetTileRef(targetCoordinates),
range), cancelToken.Token);
job.Run();
await job.AsTask.WaitAsync(cancelToken.Token);
if (job.Result == null)
return (false, null);
return (true, new Dictionary<string, object>()
{
{NPCBlackboard.OwnerCoordinates, targetCoordinates},
{PathfindKey, job.Result}
});
}
// Given steering is complicated we'll hand it off to a dedicated system rather than this singleton operator.
public override void Startup(NPCBlackboard blackboard)
{
base.Startup(blackboard);
// Need to remove the planning value for execution.
blackboard.Remove<EntityCoordinates>(NPCBlackboard.OwnerCoordinates);
// Re-use the path we may have if applicable.
var comp = _steering.Register(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner), blackboard.GetValue<EntityCoordinates>(TargetKey));
if (blackboard.TryGetValue<float>(RangeKey, out var range))
{
comp.Range = range;
}
if (blackboard.TryGetValue<Queue<TileRef>>(PathfindKey, out var path))
{
if (blackboard.TryGetValue<EntityCoordinates>(NPCBlackboard.OwnerCoordinates, out var coordinates))
{
_steering.PrunePath(coordinates, path);
}
comp.CurrentPath = path;
}
}
public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
{
base.Shutdown(blackboard, status);
// Cleanup the blackboard and remove steering.
if (blackboard.TryGetValue<CancellationTokenSource>(MovementCancelToken, out var cancelToken))
{
cancelToken.Cancel();
blackboard.Remove<CancellationTokenSource>(MovementCancelToken);
}
// OwnerCoordinates is only used in planning so dump it.
blackboard.Remove<Queue<TileRef>>(PathfindKey);
if (RemoveKeyOnFinish)
{
blackboard.Remove<EntityCoordinates>(TargetKey);
}
_steering.Unregister(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
}
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
{
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
if (!_entManager.TryGetComponent<NPCSteeringComponent>(owner, out var steering))
return HTNOperatorStatus.Failed;
return steering.Status switch
{
SteeringStatus.InRange => HTNOperatorStatus.Finished,
SteeringStatus.NoPath => HTNOperatorStatus.Failed,
SteeringStatus.Moving => HTNOperatorStatus.Continuing,
_ => throw new ArgumentOutOfRangeException()
};
}
}

View File

@@ -0,0 +1,82 @@
using System.Threading.Tasks;
using Content.Server.Interaction;
using Content.Server.NPC.Systems;
using Content.Shared.MobState;
using Content.Shared.MobState.Components;
using Robust.Shared.Map;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
public abstract class NPCCombatOperator : HTNOperator
{
[Dependency] protected readonly IEntityManager EntManager = default!;
private AiFactionTagSystem _tags = default!;
protected InteractionSystem Interaction = default!;
[ViewVariables, DataField("key")] public string Key = "CombatTarget";
/// <summary>
/// The EntityCoordinates of the specified target.
/// </summary>
[ViewVariables, DataField("keyCoordinates")]
public string KeyCoordinates = "CombatTargetCoordinates";
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
_tags = sysManager.GetEntitySystem<AiFactionTagSystem>();
Interaction = sysManager.GetEntitySystem<InteractionSystem>();
}
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
{
var targets = GetTargets(blackboard);
if (targets.Count == 0)
{
return (false, null);
}
// TODO: Need some level of rng in ratings (outside of continuing to attack the same target)
var selectedTarget = targets[0].Entity;
var effects = new Dictionary<string, object>()
{
{Key, selectedTarget},
{KeyCoordinates, new EntityCoordinates(selectedTarget, Vector2.Zero)}
};
return (true, effects);
}
private List<(EntityUid Entity, float Rating)> GetTargets(NPCBlackboard blackboard)
{
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
var radius = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntManager);
var targets = new List<(EntityUid Entity, float Rating)>();
blackboard.TryGetValue<EntityUid>(Key, out var existingTarget);
var xformQuery = EntManager.GetEntityQuery<TransformComponent>();
var mobQuery = EntManager.GetEntityQuery<MobStateComponent>();
var canMove = blackboard.GetValueOrDefault<bool>(NPCBlackboard.CanMove, EntManager);
// TODO: Need a perception system instead
foreach (var target in _tags
.GetNearbyHostiles(owner, radius))
{
if (mobQuery.TryGetComponent(target, out var mobState) &&
mobState.CurrentState > DamageState.Alive)
{
continue;
}
targets.Add((target, GetRating(blackboard, target, existingTarget, canMove, xformQuery)));
}
targets.Sort((x, y) => y.Rating.CompareTo(x.Rating));
return targets;
}
protected abstract float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, bool canMove,
EntityQuery<TransformComponent> xformQuery);
}

View File

@@ -0,0 +1,95 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.NPC.Pathfinding;
using Robust.Shared.Map;
using Robust.Shared.Random;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
/// <summary>
/// Picks a nearby component that is accessible.
/// </summary>
public sealed class PickAccessibleComponentOperator : HTNOperator
{
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private PathfindingSystem _path = default!;
private EntityLookupSystem _lookup = default!;
[DataField("rangeKey", required: true)]
public string RangeKey = string.Empty;
[ViewVariables, DataField("targetKey", required: true)]
public string TargetKey = string.Empty;
[ViewVariables, DataField("component", required: true)]
public string Component = string.Empty;
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
_path = sysManager.GetEntitySystem<PathfindingSystem>();
_lookup = sysManager.GetEntitySystem<EntityLookupSystem>();
}
/// <inheritdoc/>
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
{
// Check if the component exists
if (!_factory.TryGetRegistration(Component, out var registration))
{
return (false, null);
}
var range = blackboard.GetValueOrDefault<float>(RangeKey);
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
if (!blackboard.TryGetValue<EntityCoordinates>(NPCBlackboard.OwnerCoordinates, out var coordinates))
{
return (false, null);
}
var compType = registration.Type;
var query = _entManager.GetEntityQuery(compType);
var targets = new List<Component>();
// TODO: Need to get ones that are accessible.
// TODO: Look at unreal HTN to see repeatable ones maybe?
foreach (var entity in _lookup.GetEntitiesInRange(coordinates, range))
{
if (entity == owner || !query.TryGetComponent(entity, out var comp))
continue;
targets.Add(comp);
}
if (targets.Count == 0)
{
return (false, null);
}
while (targets.Count > 0)
{
// TODO: Get nearest at some stage
var target = _random.PickAndTake(targets);
// TODO: God the path api sucks PLUS I need some fast way to get this.
var job = _path.RequestPath(owner, target.Owner, CancellationToken.None);
await job.AsTask;
if (job.Result == null || !_entManager.TryGetComponent<TransformComponent>(target.Owner, out var targetXform))
{
continue;
}
return (true, new Dictionary<string, object>()
{
{ TargetKey, targetXform.Coordinates },
});
}
return (false, null);
}
}

View File

@@ -0,0 +1,63 @@
using System.Threading.Tasks;
using Content.Server.NPC.Pathfinding;
using Content.Server.NPC.Pathfinding.Accessible;
using Robust.Shared.Random;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
/// <summary>
/// Chooses a nearby coordinate and puts it into the resulting key.
/// </summary>
public sealed class PickAccessibleOperator : HTNOperator
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private AiReachableSystem _reachable = default!;
[DataField("rangeKey", required: true)]
public string RangeKey = string.Empty;
[ViewVariables, DataField("targetKey", required: true)]
public string TargetKey = string.Empty;
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
_reachable = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AiReachableSystem>();
}
/// <inheritdoc/>
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
{
// Very inefficient (should weight each region by its node count) but better than the old system
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
if (!_entManager.TryGetComponent(_entManager.GetComponent<TransformComponent>(owner).GridUid, out IMapGridComponent? grid))
return (false, null);
var reachableArgs = ReachableArgs.GetArgs(owner, blackboard.GetValueOrDefault<float>(RangeKey));
var entityRegion = _reachable.GetRegion(owner);
var reachableRegions = _reachable.GetReachableRegions(reachableArgs, entityRegion);
if (reachableRegions.Count == 0)
return (false, null);
var reachableNodes = new List<PathfindingNode>();
foreach (var region in reachableRegions)
{
foreach (var node in region.Nodes)
{
reachableNodes.Add(node);
}
}
var targetNode = _random.Pick(reachableNodes);
var target = grid.Grid.GridTileToLocal(targetNode.TileRef.GridIndices);
return (true, new Dictionary<string, object>()
{
{ TargetKey, target },
});
}
}

View File

@@ -0,0 +1,21 @@
using System.Threading.Tasks;
using Robust.Shared.Random;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
public sealed class PickRandomRotationOperator : HTNOperator
{
[Dependency] private readonly IRobustRandom _random = default!;
[ViewVariables, DataField("targetKey")]
public string TargetKey = "RotateTarget";
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
{
var rotation = _random.NextAngle();
return (true, new Dictionary<string, object>()
{
{TargetKey, rotation}
});
}
}

View File

@@ -0,0 +1,36 @@
using System.Threading.Tasks;
using Robust.Shared.Random;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
public sealed class RandomOperator : HTNOperator
{
[Dependency] private readonly IRobustRandom _random = default!;
/// <summary>
/// Target blackboard key to set the value to. Doesn't need to exist beforehand.
/// </summary>
[DataField("targetKey", required: true)] public string TargetKey = string.Empty;
/// <summary>
/// Minimum idle time.
/// </summary>
[DataField("minKey", required: true)] public string MinKey = string.Empty;
/// <summary>
/// Maximum idle time.
/// </summary>
[DataField("maxKey", required: true)] public string MaxKey = string.Empty;
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
{
return (true, new Dictionary<string, object>()
{
{
TargetKey,
_random.NextFloat(blackboard.GetValueOrDefault<float>(MinKey),
blackboard.GetValueOrDefault<float>(MaxKey))
}
});
}
}

View File

@@ -0,0 +1,44 @@
using Robust.Shared.Map;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Ranged;
/// <summary>
/// Selects a target for ranged combat.
/// </summary>
public sealed class PickRangedTargetOperator : NPCCombatOperator
{
protected override float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, bool canMove, EntityQuery<TransformComponent> xformQuery)
{
var ourCoordinates = blackboard.GetValueOrDefault<EntityCoordinates>(NPCBlackboard.OwnerCoordinates);
if (!xformQuery.TryGetComponent(uid, out var targetXform))
return -1f;
var targetCoordinates = targetXform.Coordinates;
if (!ourCoordinates.TryDistance(EntManager, targetCoordinates, out var distance))
return -1f;
// TODO: Uhh make this better with penetration or something.
var inLOS = Interaction.InRangeUnobstructed(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner),
uid, distance + 0.1f);
if (!canMove && !inLOS)
return -1f;
// Yeah look I just came up with values that seemed okay but they will need a lot of tweaking.
// Having a debug overlay just to project these would be very useful when finetuning in future.
var rating = 0f;
if (inLOS)
rating += 4f;
if (existingTarget == uid)
{
rating += 2f;
}
rating += 1f / distance * 4f;
return rating;
}
}

View File

@@ -0,0 +1,109 @@
using System.Threading.Tasks;
using Content.Server.NPC.Components;
using Content.Shared.MobState;
using Content.Shared.MobState.Components;
using Robust.Shared.Audio;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Ranged;
public sealed class RangedOperator : HTNOperator
{
[Dependency] private readonly IEntityManager _entManager = default!;
/// <summary>
/// Key that contains the target entity.
/// </summary>
[ViewVariables, DataField("targetKey", required: true)]
public string TargetKey = default!;
/// <summary>
/// Minimum damage state that the target has to be in for us to consider attacking.
/// </summary>
[ViewVariables, DataField("targetState")]
public DamageState TargetState = DamageState.Alive;
// Like movement we add a component and pass it off to the dedicated system.
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
{
// Don't attack if they're already as wounded as we want them.
if (!blackboard.TryGetValue<EntityUid>(TargetKey, out var target))
{
return (false, null);
}
if (_entManager.TryGetComponent<MobStateComponent>(target, out var mobState) &&
mobState.CurrentState != null &&
mobState.CurrentState > TargetState)
{
return (false, null);
}
return (true, null);
}
public override void Startup(NPCBlackboard blackboard)
{
base.Startup(blackboard);
var ranged = _entManager.EnsureComponent<NPCRangedCombatComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
ranged.Target = blackboard.GetValue<EntityUid>(TargetKey);
if (blackboard.TryGetValue<float>(NPCBlackboard.RotateSpeed, out var rotSpeed))
{
ranged.RotationSpeed = new Angle(rotSpeed);
}
if (blackboard.TryGetValue<SoundSpecifier>("SoundTargetInLOS", out var losSound))
{
ranged.SoundTargetInLOS = losSound;
}
}
public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
{
base.Shutdown(blackboard, status);
_entManager.RemoveComponent<NPCRangedCombatComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
blackboard.Remove<EntityUid>(TargetKey);
}
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
{
base.Update(blackboard, frameTime);
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
var status = HTNOperatorStatus.Continuing;
if (_entManager.TryGetComponent<NPCRangedCombatComponent>(owner, out var combat))
{
// Success
if (_entManager.TryGetComponent<MobStateComponent>(combat.Target, out var mobState) &&
mobState.CurrentState != null &&
mobState.CurrentState > TargetState)
{
status = HTNOperatorStatus.Finished;
}
else
{
switch (combat.Status)
{
case CombatStatus.TargetUnreachable:
case CombatStatus.NotInSight:
status = HTNOperatorStatus.Failed;
break;
case CombatStatus.Normal:
status = HTNOperatorStatus.Continuing;
break;
default:
status = HTNOperatorStatus.Failed;
break;
}
}
}
if (status != HTNOperatorStatus.Continuing)
{
_entManager.RemoveComponent<NPCRangedCombatComponent>(owner);
}
return status;
}
}

View File

@@ -0,0 +1,53 @@
using Content.Shared.Interaction;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
public sealed class RotateToTargetOperator : HTNOperator
{
[Dependency] private readonly IEntityManager _entityManager = default!;
private RotateToFaceSystem _rotate = default!;
[ViewVariables, DataField("targetKey")]
public string TargetKey = "RotateTarget";
[ViewVariables, DataField("rotateSpeedKey")]
public string RotationSpeedKey = NPCBlackboard.RotateSpeed;
// Didn't use a key because it's likely the same between all NPCs
[ViewVariables, DataField("tolerance")]
public Angle Tolerance = Angle.FromDegrees(1);
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
_rotate = sysManager.GetEntitySystem<RotateToFaceSystem>();
}
public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
{
base.Shutdown(blackboard, status);
blackboard.Remove<Angle>(TargetKey);
}
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
{
if (!blackboard.TryGetValue<Angle>(TargetKey, out var rotateTarget, _entityManager))
{
return HTNOperatorStatus.Failed;
}
if (!blackboard.TryGetValue<float>(RotationSpeedKey, out var rotateSpeed, _entityManager))
{
return HTNOperatorStatus.Failed;
}
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
if (_rotate.TryRotateTo(owner, rotateTarget, frameTime, Tolerance, rotateSpeed))
{
return HTNOperatorStatus.Finished;
}
return HTNOperatorStatus.Continuing;
}
}

View File

@@ -0,0 +1,22 @@
using System.Threading.Tasks;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
/// <summary>
/// Just sets a blackboard key to a float
/// </summary>
public sealed class SetFloatOperator : HTNOperator
{
[DataField("targetKey", required: true)] public string TargetKey = string.Empty;
[ViewVariables(VVAccess.ReadWrite), DataField("amount")]
public float Amount;
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
{
return (true, new Dictionary<string, object>()
{
{TargetKey, Amount},
});
}
}

View File

@@ -0,0 +1,25 @@
using Content.Server.Chat.Systems;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
public sealed class SpeakOperator : HTNOperator
{
private ChatSystem _chat = default!;
[ViewVariables, DataField("speech", required: true)]
public string Speech = string.Empty;
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
_chat = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>();
}
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
{
var speaker = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
_chat.TrySendInGameICMessage(speaker, Loc.GetString(Speech), InGameICChatType.Speak, false);
return base.Update(blackboard, frameTime);
}
}

View File

@@ -0,0 +1,87 @@
using Content.Server.Chat.Systems;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.NPC.Components;
using Content.Server.Silicons.Bots;
using Content.Shared.Damage;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Robust.Shared.Audio;
using Robust.Shared.Player;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Specific;
public sealed class MedibotInjectOperator : HTNOperator
{
[Dependency] private readonly IEntityManager _entManager = default!;
private ChatSystem _chat = default!;
private SharedInteractionSystem _interactionSystem = default!;
private SharedPopupSystem _popupSystem = default!;
private SolutionContainerSystem _solutionSystem = default!;
/// <summary>
/// Target entity to inject.
/// </summary>
[ViewVariables, DataField("targetKey", required: true)]
public string TargetKey = string.Empty;
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
_chat = sysManager.GetEntitySystem<ChatSystem>();
_interactionSystem = sysManager.GetEntitySystem<SharedInteractionSystem>();
_popupSystem = sysManager.GetEntitySystem<SharedPopupSystem>();
_solutionSystem = sysManager.GetEntitySystem<SolutionContainerSystem>();
}
public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
{
base.Shutdown(blackboard, status);
blackboard.Remove<EntityUid>(TargetKey);
}
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
{
// TODO: Wat
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
if (!blackboard.TryGetValue<EntityUid>(TargetKey, out var target))
return HTNOperatorStatus.Failed;
if (!_entManager.TryGetComponent<MedibotComponent>(owner, out var botComp))
return HTNOperatorStatus.Failed;
if (!_entManager.TryGetComponent<DamageableComponent>(target, out var damage))
return HTNOperatorStatus.Failed;
if (!_solutionSystem.TryGetInjectableSolution(target, out var injectable))
return HTNOperatorStatus.Failed;
if (!_interactionSystem.InRangeUnobstructed(owner, target))
return HTNOperatorStatus.Failed;
if (damage.TotalDamage == 0)
return HTNOperatorStatus.Failed;
if (damage.TotalDamage <= MedibotComponent.StandardMedDamageThreshold)
{
_solutionSystem.TryAddReagent(target, injectable, botComp.StandardMed, botComp.StandardMedInjectAmount, out var accepted);
_entManager.EnsureComponent<NPCRecentlyInjectedComponent>(target);
_popupSystem.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, Filter.Entities(target));
SoundSystem.Play("/Audio/Items/hypospray.ogg", Filter.Pvs(target), target);
_chat.TrySendInGameICMessage(owner, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, false);
return HTNOperatorStatus.Finished;
}
if (damage.TotalDamage >= MedibotComponent.EmergencyMedDamageThreshold)
{
_solutionSystem.TryAddReagent(target, injectable, botComp.EmergencyMed, botComp.EmergencyMedInjectAmount, out var accepted);
_entManager.EnsureComponent<NPCRecentlyInjectedComponent>(target);
_popupSystem.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, Filter.Entities(target));
SoundSystem.Play("/Audio/Items/hypospray.ogg", Filter.Pvs(target), target);
_chat.TrySendInGameICMessage(owner, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, false);
return HTNOperatorStatus.Finished;
}
return HTNOperatorStatus.Failed;
}
}

View File

@@ -0,0 +1,64 @@
using System.Threading.Tasks;
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.NPC.Components;
using Content.Shared.Damage;
using Content.Shared.MobState.Components;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Specific;
public sealed class PickNearbyInjectableOperator : HTNOperator
{
[Dependency] private readonly IEntityManager _entManager = default!;
private EntityLookupSystem _lookup = default!;
[ViewVariables, DataField("rangeKey")] public string RangeKey = NPCBlackboard.MedibotInjectRange;
/// <summary>
/// Target entity to inject
/// </summary>
[ViewVariables, DataField("targetKey", required: true)]
public string TargetKey = string.Empty;
/// <summary>
/// Target entitycoordinates to move to.
/// </summary>
[ViewVariables, DataField("targetMoveKey", required: true)]
public string TargetMoveKey = string.Empty;
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
_lookup = sysManager.GetEntitySystem<EntityLookupSystem>();
}
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
{
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
if (!blackboard.TryGetValue<float>(RangeKey, out var range))
return (false, null);
var damageQuery = _entManager.GetEntityQuery<DamageableComponent>();
var injectQuery = _entManager.GetEntityQuery<InjectableSolutionComponent>();
var recentlyInjected = _entManager.GetEntityQuery<NPCRecentlyInjectedComponent>();
var mobState = _entManager.GetEntityQuery<MobStateComponent>();
foreach (var entity in _lookup.GetEntitiesInRange(owner, range))
{
if (mobState.HasComponent(entity) &&
injectQuery.HasComponent(entity) &&
damageQuery.TryGetComponent(entity, out var damage) &&
damage.TotalDamage > 0 &&
!recentlyInjected.HasComponent(entity))
{
return (true, new Dictionary<string, object>()
{
{TargetKey, entity},
{TargetMoveKey, _entManager.GetComponent<TransformComponent>(entity).Coordinates}
});
}
}
return (false, null);
}
}

View File

@@ -0,0 +1,44 @@
using System.Linq;
using System.Threading.Tasks;
using Content.Server.NPC.Components;
using Robust.Shared.Random;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Test;
public sealed class PickPathfindPointOperator : HTNOperator
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
{
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
// Find all pathfind points on the same grid and choose to move to it.
var xform = _entManager.GetComponent<TransformComponent>(owner);
var gridUid = xform.GridUid;
if (gridUid == null)
return (false, null);
var points = new List<TransformComponent>();
foreach (var (point, pointXform) in _entManager.EntityQuery<NPCPathfindPointComponent, TransformComponent>(true))
{
if (gridUid != pointXform.GridUid)
continue;
points.Add(pointXform);
}
if (points.Count == 0)
return (false, null);
var selected = _random.Pick(points);
return (true, new Dictionary<string, object>()
{
{ NPCBlackboard.MovementTarget, selected.Coordinates }
});
}
}

View File

@@ -0,0 +1,36 @@
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
/// <summary>
/// Waits the specified amount of time. Removes the key when finished.
/// </summary>
public sealed class WaitOperator : HTNOperator
{
/// <summary>
/// Blackboard key for the time we'll wait for.
/// </summary>
[ViewVariables, DataField("key", required: true)] public string Key = string.Empty;
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
{
if (!blackboard.TryGetValue<float>(Key, out var timer))
{
return HTNOperatorStatus.Finished;
}
timer -= frameTime;
blackboard.SetValue(Key, timer);
return timer <= 0f ? HTNOperatorStatus.Finished : HTNOperatorStatus.Continuing;
}
public override void Shutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
{
base.Shutdown(blackboard, status);
// The replacement plan may want this value so only dump it if we're successful.
if (status != HTNOperatorStatus.BetterPlan)
{
blackboard.Remove<float>(Key);
}
}
}