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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
206
Content.Server/NPC/Systems/NPCJukeSystem.cs
Normal file
206
Content.Server/NPC/Systems/NPCJukeSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user