Shooting NPCs and more (#18042)

* Add pirate shooting

* Shooting working

* Basics working

* Refactor time

* More conversion

* Update primitives

* Update yml

* weh

* Building again

* Draft

* weh

* b

* Start shutdown

* Starting to take form

* Code side done

* is it worky

* Fix prototypes

* stuff

* Shitty working

* Juke events working

* Even more cleanup

* RTX

* Fix interaction combat mode and compquery

* GetAmmoCount relays

* Fix rotation speed

* Juke fixes

* fixes

* weh

* The collision avoidance never ends

* Fixes

* Pause support

* framework

* lazy

* Fix idling

* Fix drip

* goobed

* Fix takeover shutdown bug

* Merge fixes

* shitter

* Fix carpos
This commit is contained in:
metalgearsloth
2023-08-02 10:48:56 +10:00
committed by GitHub
parent 018e465fad
commit c31c848afd
103 changed files with 2089 additions and 810 deletions

View File

@@ -18,66 +18,6 @@ public sealed partial class NPCCombatSystem
{
SubscribeLocalEvent<NPCMeleeCombatComponent, ComponentStartup>(OnMeleeStartup);
SubscribeLocalEvent<NPCMeleeCombatComponent, ComponentShutdown>(OnMeleeShutdown);
SubscribeLocalEvent<NPCMeleeCombatComponent, NPCSteeringEvent>(OnMeleeSteering);
}
private void OnMeleeSteering(EntityUid uid, NPCMeleeCombatComponent component, ref NPCSteeringEvent args)
{
args.Steering.CanSeek = true;
if (TryComp<MeleeWeaponComponent>(component.Weapon, out var weapon))
{
var cdRemaining = weapon.NextAttack - _timing.CurTime;
var attackCooldown = TimeSpan.FromSeconds(1f / _melee.GetAttackRate(component.Weapon, uid, weapon));
// Might as well get in range.
if (cdRemaining < attackCooldown * 0.45f)
return;
if (!_physics.TryGetNearestPoints(uid, component.Target, out var pointA, out var pointB))
return;
var obstacleDirection = pointB - args.WorldPosition;
// If they're moving away then pursue anyway.
// If just hit then always back up a bit.
if (cdRemaining < attackCooldown * 0.90f &&
TryComp<PhysicsComponent>(component.Target, out var targetPhysics) &&
Vector2.Dot(targetPhysics.LinearVelocity, obstacleDirection) > 0f)
{
return;
}
if (cdRemaining < TimeSpan.FromSeconds(1f / _melee.GetAttackRate(component.Weapon, uid, weapon)) * 0.45f)
return;
var idealDistance = weapon.Range * 4f;
var obstacleDistance = obstacleDirection.Length();
if (obstacleDistance > idealDistance || obstacleDistance == 0f)
{
// Don't want to get too far.
return;
}
args.Steering.CanSeek = false;
obstacleDirection = args.OffsetRotation.RotateVec(obstacleDirection);
var norm = obstacleDirection.Normalized();
var weight = (obstacleDistance <= args.AgentRadius
? 1f
: (idealDistance - obstacleDistance) / idealDistance);
for (var i = 0; i < SharedNPCSteeringSystem.InterestDirections; i++)
{
var result = -Vector2.Dot(norm, NPCSteeringSystem.Directions[i]) * weight;
if (result < 0f)
continue;
args.Interest[i] = MathF.Max(args.Interest[i], result);
}
}
}
private void OnMeleeShutdown(EntityUid uid, NPCMeleeCombatComponent component, ComponentShutdown args)
@@ -87,7 +27,7 @@ public sealed partial class NPCCombatSystem
_combat.SetInCombatMode(uid, false, combatMode);
}
_steering.Unregister(component.Owner);
_steering.Unregister(uid);
}
private void OnMeleeStartup(EntityUid uid, NPCMeleeCombatComponent component, ComponentStartup args)
@@ -96,9 +36,6 @@ public sealed partial class NPCCombatSystem
{
_combat.SetInCombatMode(uid, true, combatMode);
}
// TODO: Cleanup later, just looking for parity for now.
component.Weapon = uid;
}
private void UpdateMelee(float frameTime)
@@ -107,11 +44,10 @@ public sealed partial class NPCCombatSystem
var xformQuery = GetEntityQuery<TransformComponent>();
var physicsQuery = GetEntityQuery<PhysicsComponent>();
var curTime = _timing.CurTime;
var query = EntityQueryEnumerator<NPCMeleeCombatComponent, ActiveNPCComponent>();
foreach (var (comp, _) in EntityQuery<NPCMeleeCombatComponent, ActiveNPCComponent>())
while (query.MoveNext(out var uid, out var comp, out _))
{
var uid = comp.Owner;
if (!combatQuery.TryGetComponent(uid, out var combat) || !combat.IsInCombatMode)
{
RemComp<NPCMeleeCombatComponent>(uid);
@@ -126,7 +62,7 @@ public sealed partial class NPCCombatSystem
{
component.Status = CombatStatus.Normal;
if (!TryComp<MeleeWeaponComponent>(component.Weapon, out var weapon))
if (!_melee.TryGetWeapon(uid, out var weaponUid, out var weapon))
{
component.Status = CombatStatus.NoWeapon;
return;
@@ -167,12 +103,6 @@ public sealed partial class NPCCombatSystem
return;
}
steering = EnsureComp<NPCSteeringComponent>(uid);
steering.Range = MathF.Max(0.2f, weapon.Range - 0.4f);
// Gets unregistered on component shutdown.
_steering.TryRegister(uid, new EntityCoordinates(component.Target, Vector2.Zero), steering);
if (weapon.NextAttack > curTime || !Enabled)
return;
@@ -180,11 +110,11 @@ public sealed partial class NPCCombatSystem
physicsQuery.TryGetComponent(component.Target, out var targetPhysics) &&
targetPhysics.LinearVelocity.LengthSquared() != 0f)
{
_melee.AttemptLightAttackMiss(uid, component.Weapon, weapon, targetXform.Coordinates.Offset(_random.NextVector2(0.5f)));
_melee.AttemptLightAttackMiss(uid, weaponUid, weapon, targetXform.Coordinates.Offset(_random.NextVector2(0.5f)));
}
else
{
_melee.AttemptLightAttack(uid, component.Weapon, weapon, component.Target);
_melee.AttemptLightAttack(uid, weaponUid, weapon, component.Target);
}
}
}

View File

@@ -1,7 +1,8 @@
using System.Numerics;
using Content.Server.NPC.Components;
using Content.Shared.CombatMode;
using Content.Shared.Interaction;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Shared.Map;
using Robust.Shared.Physics.Components;
@@ -12,6 +13,12 @@ public sealed partial class NPCCombatSystem
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
[Dependency] private readonly RotateToFaceSystem _rotate = default!;
private EntityQuery<CombatModeComponent> _combatQuery;
private EntityQuery<NPCSteeringComponent> _steeringQuery;
private EntityQuery<RechargeBasicEntityAmmoComponent> _rechargeQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<TransformComponent> _xformQuery;
// TODO: Don't predict for hitscan
private const float ShootSpeed = 20f;
@@ -22,6 +29,12 @@ public sealed partial class NPCCombatSystem
private void InitializeRanged()
{
_combatQuery = GetEntityQuery<CombatModeComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_rechargeQuery = GetEntityQuery<RechargeBasicEntityAmmoComponent>();
_steeringQuery = GetEntityQuery<NPCSteeringComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
SubscribeLocalEvent<NPCRangedCombatComponent, ComponentStartup>(OnRangedStartup);
SubscribeLocalEvent<NPCRangedCombatComponent, ComponentShutdown>(OnRangedShutdown);
}
@@ -48,9 +61,6 @@ public sealed partial class NPCCombatSystem
private void UpdateRanged(float frameTime)
{
var bodyQuery = GetEntityQuery<PhysicsComponent>();
var xformQuery = GetEntityQuery<TransformComponent>();
var combatQuery = GetEntityQuery<CombatModeComponent>();
var query = EntityQueryEnumerator<NPCRangedCombatComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var comp, out var xform))
@@ -58,8 +68,15 @@ public sealed partial class NPCCombatSystem
if (comp.Status == CombatStatus.Unspecified)
continue;
if (!xformQuery.TryGetComponent(comp.Target, out var targetXform) ||
!bodyQuery.TryGetComponent(comp.Target, out var targetBody))
if (_steeringQuery.TryGetComponent(uid, out var steering) && steering.Status == SteeringStatus.NoPath)
{
comp.Status = CombatStatus.TargetUnreachable;
comp.ShootAccumulator = 0f;
continue;
}
if (!_xformQuery.TryGetComponent(comp.Target, out var targetXform) ||
!_physicsQuery.TryGetComponent(comp.Target, out var targetBody))
{
comp.Status = CombatStatus.TargetUnreachable;
comp.ShootAccumulator = 0f;
@@ -73,7 +90,7 @@ public sealed partial class NPCCombatSystem
continue;
}
if (combatQuery.TryGetComponent(uid, out var combatMode))
if (_combatQuery.TryGetComponent(uid, out var combatMode))
{
_combat.SetInCombatMode(uid, true, combatMode);
}
@@ -85,10 +102,26 @@ public sealed partial class NPCCombatSystem
continue;
}
var ammoEv = new GetAmmoCountEvent();
RaiseLocalEvent(gunUid, ref ammoEv);
if (ammoEv.Count == 0)
{
// Recharging then?
if (_rechargeQuery.HasComponent(gunUid))
{
continue;
}
comp.Status = CombatStatus.Unspecified;
comp.ShootAccumulator = 0f;
continue;
}
comp.LOSAccumulator -= frameTime;
var (worldPos, worldRot) = _transform.GetWorldPositionRotation(xform, xformQuery);
var (targetPos, targetRot) = _transform.GetWorldPositionRotation(targetXform, xformQuery);
var worldPos = _transform.GetWorldPosition(xform);
var targetPos = _transform.GetWorldPosition(targetXform);
// We'll work out the projected spot of the target and shoot there instead of where they are.
var distance = (targetPos - worldPos).Length();
@@ -105,7 +138,7 @@ public sealed partial class NPCCombatSystem
if (!comp.TargetInLOS)
{
comp.ShootAccumulator = 0f;
comp.Status = CombatStatus.TargetUnreachable;
comp.Status = CombatStatus.NotInSight;
continue;
}
@@ -156,6 +189,7 @@ public sealed partial class NPCCombatSystem
}
_gun.AttemptShoot(uid, gunUid, gun, targetCordinates);
comp.Status = CombatStatus.Normal;
}
}
}

View File

@@ -0,0 +1,206 @@
using System.Numerics;
using Content.Server.NPC.Components;
using Content.Server.NPC.Events;
using Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat;
using Content.Server.Weapons.Melee;
using Content.Shared.NPC;
using Content.Shared.Weapons.Melee;
using Robust.Shared.Collections;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.NPC.Systems;
public sealed class NPCJukeSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly MeleeWeaponSystem _melee = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
private EntityQuery<NPCMeleeCombatComponent> _npcMeleeQuery;
private EntityQuery<NPCRangedCombatComponent> _npcRangedQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
public override void Initialize()
{
base.Initialize();
_npcMeleeQuery = GetEntityQuery<NPCMeleeCombatComponent>();
_npcRangedQuery = GetEntityQuery<NPCRangedCombatComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
SubscribeLocalEvent<NPCJukeComponent, EntityUnpausedEvent>(OnJukeUnpaused);
SubscribeLocalEvent<NPCJukeComponent, NPCSteeringEvent>(OnJukeSteering);
}
private void OnJukeUnpaused(EntityUid uid, NPCJukeComponent component, ref EntityUnpausedEvent args)
{
component.NextJuke += args.PausedTime;
}
private void OnJukeSteering(EntityUid uid, NPCJukeComponent component, ref NPCSteeringEvent args)
{
if (component.JukeType == JukeType.AdjacentTile)
{
if (_npcRangedQuery.TryGetComponent(uid, out var ranged) &&
ranged.Status == CombatStatus.NotInSight)
{
component.TargetTile = null;
return;
}
if (_timing.CurTime < component.NextJuke)
{
component.TargetTile = null;
return;
}
if (!TryComp<MapGridComponent>(args.Transform.GridUid, out var grid))
{
component.TargetTile = null;
return;
}
var currentTile = grid.CoordinatesToTile(args.Transform.Coordinates);
if (component.TargetTile == null)
{
var targetTile = currentTile;
var startIndex = _random.Next(8);
_physicsQuery.TryGetComponent(uid, out var ownerPhysics);
var collisionLayer = ownerPhysics?.CollisionLayer ?? 0;
var collisionMask = ownerPhysics?.CollisionMask ?? 0;
for (var i = 0; i < 8; i++)
{
var index = (startIndex + i) % 8;
var neighbor = ((Direction) index).ToIntVec() + currentTile;
var valid = true;
// TODO: Probably make this a helper on engine maybe
var tileBounds = new Box2(neighbor, neighbor + grid.TileSize);
tileBounds = tileBounds.Enlarged(-0.1f);
foreach (var ent in _lookup.GetEntitiesIntersecting(args.Transform.GridUid.Value, tileBounds))
{
if (ent == uid ||
!_physicsQuery.TryGetComponent(ent, out var physics) ||
!physics.CanCollide ||
!physics.Hard ||
((physics.CollisionMask & collisionLayer) == 0x0 &&
(physics.CollisionLayer & collisionMask) == 0x0))
{
continue;
}
valid = false;
break;
}
if (!valid)
continue;
targetTile = neighbor;
break;
}
component.TargetTile ??= targetTile;
}
var elapsed = _timing.CurTime - component.NextJuke;
// Finished juke, reset timer.
if (elapsed.TotalSeconds > component.JukeDuration ||
currentTile == component.TargetTile)
{
component.TargetTile = null;
component.NextJuke = _timing.CurTime + TimeSpan.FromSeconds(component.JukeDuration);
return;
}
var targetCoords = grid.GridTileToWorld(component.TargetTile.Value);
var targetDir = (targetCoords.Position - args.WorldPosition);
targetDir = args.OffsetRotation.RotateVec(targetDir);
const float weight = 1f;
var norm = targetDir.Normalized();
for (var i = 0; i < SharedNPCSteeringSystem.InterestDirections; i++)
{
var result = -Vector2.Dot(norm, NPCSteeringSystem.Directions[i]) * weight;
if (result < 0f)
continue;
args.Steering.Interest[i] = MathF.Max(args.Steering.Interest[i], result);
}
args.Steering.CanSeek = false;
}
if (component.JukeType == JukeType.Away)
{
// TODO: Ranged away juking
if (_npcMeleeQuery.TryGetComponent(uid, out var melee))
{
if (!_melee.TryGetWeapon(uid, out var weaponUid, out var weapon))
return;
var cdRemaining = weapon.NextAttack - _timing.CurTime;
var attackCooldown = TimeSpan.FromSeconds(1f / _melee.GetAttackRate(weaponUid, uid, weapon));
// Might as well get in range.
if (cdRemaining < attackCooldown * 0.45f)
return;
if (!_physics.TryGetNearestPoints(uid, melee.Target, out var pointA, out var pointB))
return;
var obstacleDirection = pointB - args.WorldPosition;
// If they're moving away then pursue anyway.
// If just hit then always back up a bit.
if (cdRemaining < attackCooldown * 0.90f &&
TryComp<PhysicsComponent>(melee.Target, out var targetPhysics) &&
Vector2.Dot(targetPhysics.LinearVelocity, obstacleDirection) > 0f)
{
return;
}
if (cdRemaining < TimeSpan.FromSeconds(1f / _melee.GetAttackRate(weaponUid, uid, weapon)) * 0.45f)
return;
var idealDistance = weapon.Range * 4f;
var obstacleDistance = obstacleDirection.Length();
if (obstacleDistance > idealDistance || obstacleDistance == 0f)
{
// Don't want to get too far.
return;
}
obstacleDirection = args.OffsetRotation.RotateVec(obstacleDirection);
var norm = obstacleDirection.Normalized();
var weight = obstacleDistance <= args.Steering.Radius
? 1f
: (idealDistance - obstacleDistance) / idealDistance;
for (var i = 0; i < SharedNPCSteeringSystem.InterestDirections; i++)
{
var result = -Vector2.Dot(norm, NPCSteeringSystem.Directions[i]) * weight;
if (result < 0f)
continue;
args.Steering.Interest[i] = MathF.Max(args.Steering.Interest[i], result);
}
}
args.Steering.CanSeek = false;
}
}
}

View File

@@ -77,12 +77,43 @@ public sealed partial class NPCSteeringSystem
{
var ourCoordinates = xform.Coordinates;
var destinationCoordinates = steering.Coordinates;
var inLos = true;
// Check if we're in LOS if that's required.
// TODO: Need something uhh better not sure on the interaction between these.
if (steering.ArriveOnLineOfSight)
{
// TODO: use vision range
inLos = _interaction.InRangeUnobstructed(uid, steering.Coordinates, 10f);
if (inLos)
{
steering.LineOfSightTimer += frameTime;
if (steering.LineOfSightTimer >= steering.LineOfSightTimeRequired)
{
steering.Status = SteeringStatus.InRange;
ResetStuck(steering, ourCoordinates);
return true;
}
}
else
{
steering.LineOfSightTimer = 0f;
}
}
else
{
steering.LineOfSightTimer = 0f;
}
// We've arrived, nothing else matters.
if (xform.Coordinates.TryDistance(EntityManager, destinationCoordinates, out var distance) &&
distance <= steering.Range)
if (xform.Coordinates.TryDistance(EntityManager, destinationCoordinates, out var targetDistance) &&
inLos &&
targetDistance <= steering.Range)
{
steering.Status = SteeringStatus.InRange;
ResetStuck(steering, ourCoordinates);
return true;
}
@@ -117,7 +148,7 @@ public sealed partial class NPCSteeringSystem
// This is to avoid popping it too early
else if (steering.CurrentPath.TryPeek(out var node) && IsFreeSpace(uid, steering, node))
{
arrivalDistance = MathF.Min(node.Box.Width / 2f, node.Box.Height / 2f) - 0.01f;
arrivalDistance = MathF.Max(0.05f, MathF.Min(node.Box.Width / 2f, node.Box.Height / 2f) - 0.05f);
}
// Try getting into blocked range I guess?
// TODO: Consider melee range or the likes.
@@ -172,7 +203,7 @@ public sealed partial class NPCSteeringSystem
steering.Status = SteeringStatus.NoPath;
return false;
case SteeringObstacleStatus.Continuing:
CheckPath(uid, steering, xform, needsPath, distance);
CheckPath(uid, steering, xform, needsPath, targetDistance);
return true;
default:
throw new ArgumentOutOfRangeException();
@@ -205,9 +236,7 @@ public sealed partial class NPCSteeringSystem
}
else
{
// This probably shouldn't happen as we check above but eh.
steering.Status = SteeringStatus.NoPath;
return false;
needsPath = true;
}
}
// Stuck detection
@@ -228,8 +257,13 @@ public sealed partial class NPCSteeringSystem
// B) NPCs still try to move in locked containers (e.g. cow, hamster)
// and I don't want to spam grafana even harder than it gets spammed rn.
Log.Debug($"NPC {ToPrettyString(uid)} found stuck at {ourCoordinates}");
steering.Status = SteeringStatus.NoPath;
return false;
needsPath = true;
if (stuckTime.TotalSeconds > maxStuckTime * 3)
{
steering.Status = SteeringStatus.NoPath;
return false;
}
}
}
else
@@ -237,14 +271,14 @@ public sealed partial class NPCSteeringSystem
ResetStuck(steering, ourCoordinates);
}
// Do we have no more nodes to follow OR has the target moved sufficiently? If so then re-path.
if (!needsPath)
// If not in LOS and no path then get a new one fam.
if (!inLos && steering.CurrentPath.Count == 0)
{
needsPath = steering.CurrentPath.Count == 0 || (steering.CurrentPath.Peek().Data.Flags & PathfindingBreadcrumbFlag.Invalid) != 0x0;
needsPath = true;
}
// TODO: Probably need partial planning support i.e. patch from the last node to where the target moved to.
CheckPath(uid, steering, xform, needsPath, distance);
CheckPath(uid, steering, xform, needsPath, targetDistance);
// If we don't have a path yet then do nothing; this is to avoid stutter-stepping if it turns out there's no path
// available but we assume there was.
@@ -295,8 +329,10 @@ public sealed partial class NPCSteeringSystem
return;
}
if (!needsPath)
if (!needsPath && steering.CurrentPath.Count > 0)
{
needsPath = steering.CurrentPath.Count > 0 && (steering.CurrentPath.Peek().Data.Flags & PathfindingBreadcrumbFlag.Invalid) != 0x0;
// If the target has sufficiently moved.
var lastNode = GetCoordinates(steering.CurrentPath.Last());
@@ -357,10 +393,6 @@ public sealed partial class NPCSteeringSystem
mask = (CollisionGroup) physics.CollisionMask;
}
// If we have to backtrack (for example, we're behind a table and the target is on the other side)
// Then don't consider pruning.
var goal = nodes.Last().Coordinates.ToMap(EntityManager, _transform);
for (var i = 0; i < nodes.Count; i++)
{
var node = nodes[i];
@@ -451,7 +483,9 @@ public sealed partial class NPCSteeringSystem
var xformB = _xformQuery.GetComponent(ent);
if (!_physics.TryGetNearest(uid, ent, out var pointA, out var pointB, out var distance, xform, xformB))
if (!_physics.TryGetNearest(uid, ent,
out var pointA, out var pointB, out var distance,
xform, xformB))
{
continue;
}
@@ -508,8 +542,7 @@ public sealed partial class NPCSteeringSystem
var objectRadius = 0.25f;
var detectionRadius = MathF.Max(0.35f, agentRadius + objectRadius);
var ourVelocity = body.LinearVelocity;
var factionQuery = GetEntityQuery<NpcFactionMemberComponent>();
factionQuery.TryGetComponent(uid, out var ourFaction);
_factionQuery.TryGetComponent(uid, out var ourFaction);
foreach (var ent in _lookup.GetEntitiesInRange(uid, detectionRadius, LookupFlags.Dynamic))
{
@@ -520,7 +553,7 @@ public sealed partial class NPCSteeringSystem
!otherBody.CanCollide ||
(mask & otherBody.CollisionLayer) == 0x0 &&
(layer & otherBody.CollisionMask) == 0x0 ||
!factionQuery.TryGetComponent(ent, out var otherFaction) ||
!_factionQuery.TryGetComponent(ent, out var otherFaction) ||
!_npcFaction.IsEntityFriendly(uid, ent, ourFaction, otherFaction) ||
// Use <= 0 so we ignore stationary friends in case.
Vector2.Dot(otherBody.LinearVelocity, ourVelocity) <= 0f)

View File

@@ -65,8 +65,9 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
private EntityQuery<FixturesComponent> _fixturesQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<MovementSpeedModifierComponent> _modifierQuery;
private EntityQuery<NpcFactionMemberComponent> _factionQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<TransformComponent> _xformQuery;
/// <summary>
@@ -89,8 +90,9 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
base.Initialize();
_fixturesQuery = GetEntityQuery<FixturesComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_modifierQuery = GetEntityQuery<MovementSpeedModifierComponent>();
_factionQuery = GetEntityQuery<NpcFactionMemberComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
#if DEBUG
@@ -238,8 +240,16 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
return;
// Not every mob has the modifier component so do it as a separate query.
var npcs = EntityQuery<ActiveNPCComponent, NPCSteeringComponent, InputMoverComponent, TransformComponent>()
.Select(o => (o.Item1.Owner, o.Item2, o.Item3, o.Item4)).ToArray();
var npcs = new (EntityUid, NPCSteeringComponent, InputMoverComponent, TransformComponent)[Count<ActiveNPCComponent>()];
var query = EntityQueryEnumerator<ActiveNPCComponent, NPCSteeringComponent, InputMoverComponent, TransformComponent>();
var index = 0;
while (query.MoveNext(out var uid, out _, out var steering, out var mover, out var xform))
{
npcs[index] = (uid, steering, mover, xform);
index++;
}
// Dependency issues across threads.
var options = new ParallelOptions
@@ -248,7 +258,7 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
};
var curTime = _timing.CurTime;
Parallel.For(0, npcs.Length, options, i =>
Parallel.For(0, index, options, i =>
{
var (uid, steering, mover, xform) = npcs[i];
Steer(uid, steering, mover, xform, frameTime, curTime);
@@ -257,10 +267,12 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
if (_subscribedSessions.Count > 0)
{
var data = new List<NPCSteeringDebugData>(npcs.Length);
var data = new List<NPCSteeringDebugData>(index);
foreach (var (uid, steering, mover, _) in npcs)
for (var i = 0; i < index; i++)
{
var (uid, steering, mover, _) = npcs[i];
data.Add(new NPCSteeringDebugData(
uid,
mover.CurTickSprintMovement,
@@ -341,7 +353,9 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
steering.Danger[i] = 0f;
}
var ev = new NPCSteeringEvent(steering, interest, danger, agentRadius, offsetRot, worldPos);
steering.CanSeek = true;
var ev = new NPCSteeringEvent(steering, xform, worldPos, offsetRot);
RaiseLocalEvent(uid, ref ev);
// If seek has arrived at the target node for example then immediately re-steer.
var forceSteer = true;

View File

@@ -19,8 +19,6 @@ namespace Content.Server.NPC.Systems
[Dependency] private readonly HTNSystem _htn = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
private ISawmill _sawmill = default!;
/// <summary>
/// Whether any NPCs are allowed to run at all.
/// </summary>
@@ -35,8 +33,6 @@ namespace Content.Server.NPC.Systems
{
base.Initialize();
_sawmill = Logger.GetSawmill("npc");
_sawmill.Level = LogLevel.Info;
SubscribeLocalEvent<NPCComponent, MobStateChangedEvent>(OnMobStateChange);
SubscribeLocalEvent<NPCComponent, MapInitEvent>(OnNPCMapInit);
SubscribeLocalEvent<NPCComponent, ComponentShutdown>(OnNPCShutdown);
@@ -98,7 +94,7 @@ namespace Content.Server.NPC.Systems
return;
}
_sawmill.Debug($"Waking {ToPrettyString(uid)}");
Log.Debug($"Waking {ToPrettyString(uid)}");
EnsureComp<ActiveNPCComponent>(uid);
}
@@ -109,7 +105,19 @@ namespace Content.Server.NPC.Systems
return;
}
_sawmill.Debug($"Sleeping {ToPrettyString(uid)}");
// Don't bother with an event
if (TryComp<HTNComponent>(uid, out var htn))
{
if (htn.Plan != null)
{
var currentOperator = htn.Plan.CurrentOperator;
_htn.ShutdownTask(currentOperator, htn.Blackboard, HTNOperatorStatus.Failed);
_htn.ShutdownPlan(htn);
htn.Plan = null;
}
}
Log.Debug($"Sleeping {ToPrettyString(uid)}");
RemComp<ActiveNPCComponent>(uid);
}

View File

@@ -11,11 +11,18 @@ using Content.Server.Nutrition.EntitySystems;
using Content.Server.Storage.Components;
using Content.Shared.Examine;
using Content.Shared.Fluids.Components;
using Content.Shared.Hands.Components;
using Content.Shared.Inventory;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Microsoft.Extensions.ObjectPool;
using Robust.Server.Containers;
using Robust.Shared.Collections;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.NPC.Systems;
@@ -24,17 +31,29 @@ namespace Content.Server.NPC.Systems;
/// </summary>
public sealed class NPCUtilitySystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly DrinkSystem _drink = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly FoodSystem _food = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly PuddleSystem _puddle = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SolutionContainerSystem _solutions = default!;
private EntityQuery<TransformComponent> _xformQuery;
private ObjectPool<HashSet<EntityUid>> _entPool =
new DefaultObjectPool<HashSet<EntityUid>>(new SetPolicy<EntityUid>(), 256);
public override void Initialize()
{
base.Initialize();
_xformQuery = GetEntityQuery<TransformComponent>();
}
/// <summary>
/// Runs the UtilityQueryPrototype and returns the best-matching entities.
/// </summary>
@@ -47,7 +66,7 @@ public sealed class NPCUtilitySystem : EntitySystem
// TODO: PickHostilesop or whatever needs to juse be UtilityQueryOperator
var weh = _proto.Index<UtilityQueryPrototype>(proto);
var ents = new HashSet<EntityUid>();
var ents = _entPool.Get();
foreach (var query in weh.Query)
{
@@ -63,7 +82,10 @@ public sealed class NPCUtilitySystem : EntitySystem
}
if (ents.Count == 0)
{
_entPool.Return(ents);
return UtilityResult.Empty;
}
var results = new Dictionary<EntityUid, float>();
var highestScore = 0f;
@@ -101,6 +123,7 @@ public sealed class NPCUtilitySystem : EntitySystem
var result = new UtilityResult(results);
blackboard.Remove<EntityUid>(NPCBlackboard.UtilityTarget);
_entPool.Return(ents);
return result;
}
@@ -115,7 +138,7 @@ public sealed class NPCUtilitySystem : EntitySystem
case PresetCurve presetCurve:
return GetScore(_proto.Index<UtilityCurvePresetPrototype>(presetCurve.Preset).Curve, conScore);
case QuadraticCurve quadraticCurve:
return Math.Clamp(quadraticCurve.Slope * (float) Math.Pow(conScore - quadraticCurve.XOffset, quadraticCurve.Exponent) + quadraticCurve.YOffset, 0f, 1f);
return Math.Clamp(quadraticCurve.Slope * MathF.Pow(conScore - quadraticCurve.XOffset, quadraticCurve.Exponent) + quadraticCurve.YOffset, 0f, 1f);
default:
throw new NotImplementedException();
}
@@ -189,6 +212,21 @@ public sealed class NPCUtilitySystem : EntitySystem
// TODO: Pathfind there, though probably do it in a separate con.
return 1f;
}
case TargetAmmoMatchesCon:
{
if (!blackboard.TryGetValue(NPCBlackboard.ActiveHand, out Hand? activeHand, EntityManager) ||
!TryComp<BallisticAmmoProviderComponent>(activeHand.HeldEntity, out var heldGun))
{
return 0f;
}
if (heldGun.Whitelist?.IsValid(targetUid, EntityManager) != true)
{
return 0f;
}
return 1f;
}
case TargetDistanceCon:
{
var radius = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntityManager);
@@ -207,6 +245,23 @@ public sealed class NPCUtilitySystem : EntitySystem
return Math.Clamp(distance / radius, 0f, 1f);
}
case TargetAmmoCon:
{
if (!HasComp<GunComponent>(targetUid))
return 0f;
var ev = new GetAmmoCountEvent();
RaiseLocalEvent(targetUid, ref ev);
if (ev.Count == 0)
return 0f;
// Wat
if (ev.Capacity == 0)
return 1f;
return (float) ev.Count / ev.Capacity;
}
case TargetHealthCon:
{
return 0f;
@@ -222,7 +277,7 @@ public sealed class NPCUtilitySystem : EntitySystem
var radius = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntityManager);
const float bufferRange = 0.5f;
if (blackboard.TryGetValue<EntityUid>("CombatTarget", out var currentTarget, EntityManager) &&
if (blackboard.TryGetValue<EntityUid>("Target", out var currentTarget, EntityManager) &&
currentTarget == targetUid &&
TryComp<TransformComponent>(owner, out var xform) &&
TryComp<TransformComponent>(targetUid, out var targetXform) &&
@@ -246,6 +301,15 @@ public sealed class NPCUtilitySystem : EntitySystem
{
return _mobState.IsDead(targetUid) ? 1f : 0f;
}
case TargetMeleeCon:
{
if (TryComp<MeleeWeaponComponent>(targetUid, out var melee))
{
return melee.Damage.Total.Float() * melee.AttackRate / 100f;
}
return 0f;
}
default:
throw new NotImplementedException();
}
@@ -275,40 +339,109 @@ public sealed class NPCUtilitySystem : EntitySystem
switch (query)
{
case ComponentQuery compQuery:
{
var mapPos = Transform(owner).MapPosition;
foreach (var compReg in compQuery.Components.Values)
var comps = compQuery.Components.Values.ToList();
var compZero = comps[0];
comps.RemoveAt(0);
foreach (var comp in _lookup.GetComponentsInRange(compZero.Component.GetType(), mapPos, vision))
{
foreach (var comp in _lookup.GetComponentsInRange(compReg.Component.GetType(), mapPos, vision))
var ent = comp.Owner;
if (ent == owner)
continue;
var othersFound = true;
foreach (var compOther in comps)
{
var ent = comp.Owner;
if (!HasComp(ent, compOther.Component.GetType()))
{
othersFound = false;
break;
}
}
if (ent == owner)
continue;
if (!othersFound)
continue;
entities.Add(ent);
entities.Add(ent);
}
break;
}
case InventoryQuery:
{
if (!_inventory.TryGetContainerSlotEnumerator(owner, out var enumerator))
break;
while (enumerator.MoveNext(out var slot))
{
foreach (var child in slot.ContainedEntities)
{
RecursiveAdd(child, entities);
}
}
break;
}
case NearbyHostilesQuery:
{
foreach (var ent in _npcFaction.GetNearbyHostiles(owner, vision))
{
entities.Add(ent);
}
break;
}
default:
throw new NotImplementedException();
}
}
private void RecursiveAdd(EntityUid uid, HashSet<EntityUid> entities)
{
// TODO: Probably need a recursive struct enumerator on engine.
var xform = _xformQuery.GetComponent(uid);
var enumerator = xform.ChildEnumerator;
entities.Add(uid);
while (enumerator.MoveNext(out var child))
{
RecursiveAdd(child.Value, entities);
}
}
private void Filter(NPCBlackboard blackboard, HashSet<EntityUid> entities, UtilityQueryFilter filter)
{
switch (filter)
{
case ComponentFilter compFilter:
{
var toRemove = new ValueList<EntityUid>();
foreach (var ent in entities)
{
foreach (var comp in compFilter.Components)
{
if (HasComp(ent, comp.Value.Component.GetType()))
continue;
toRemove.Add(ent);
break;
}
}
foreach (var ent in toRemove)
{
entities.Remove(ent);
}
break;
}
case PuddleFilter:
{
var puddleQuery = GetEntityQuery<PuddleComponent>();
var toRemove = new ValueList<EntityUid>();
foreach (var ent in entities)