NPC refactor (#10122)
Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user