mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 10:21:18 +03:00
* fix: fix NPC route cost overlay mode * feat: adjust pathfinding weights Plus drive-by import trim. * feat: rework pathfinding obstacle avoidance * review by Pok * fix merge * Update NPCSteeringSystem.Obstacles.cs --------- Co-authored-by: Pok245 <113675512+Pok27@users.noreply.github.com>
192 lines
6.9 KiB
C#
192 lines
6.9 KiB
C#
using Content.Server.Destructible;
|
|
using Content.Server.NPC.Components;
|
|
using Content.Server.NPC.Pathfinding;
|
|
using Content.Shared.CombatMode;
|
|
using Content.Shared.DoAfter;
|
|
using Content.Shared.Doors.Components;
|
|
using Content.Shared.NPC;
|
|
using Robust.Shared.Map.Components;
|
|
using Robust.Shared.Physics;
|
|
using Robust.Shared.Utility;
|
|
using ClimbableComponent = Content.Shared.Climbing.Components.ClimbableComponent;
|
|
using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent;
|
|
|
|
namespace Content.Server.NPC.Systems;
|
|
|
|
public sealed partial class NPCSteeringSystem
|
|
{
|
|
/*
|
|
* For any custom path handlers, e.g. destroying walls, opening airlocks, etc.
|
|
* Putting it onto steering seemed easier than trying to make a custom compound task for it.
|
|
* I also considered task interrupts although the problem is handling stuff like pathfinding overlaps
|
|
* Ideally we could do interrupts but that's TODO.
|
|
*/
|
|
|
|
/*
|
|
* TODO:
|
|
* - Add path cap
|
|
* - Circle cast BFS in LOS to determine targets.
|
|
* - Store last known coordinates of X targets.
|
|
* - Require line of sight for melee
|
|
* - Add new behavior where they move to melee target's last known position (diffing theirs and current)
|
|
* then do the thing like from dishonored where it gets passed to a search system that opens random stuff.
|
|
*
|
|
* Also need to make sure it picks nearest obstacle path so it starts smashing in front of it.
|
|
*/
|
|
|
|
[Dependency] private EntityQuery<DoorComponent> _doorQuery = default!;
|
|
[Dependency] private EntityQuery<ClimbableComponent> _climbableQuery = default!;
|
|
[Dependency] private EntityQuery<DestructibleComponent> _destructibleQuery = default!;
|
|
|
|
private SteeringObstacleStatus TryHandleFlags(EntityUid uid, NPCSteeringComponent component, PathPoly poly)
|
|
{
|
|
DebugTools.Assert(!poly.Data.IsFreeSpace);
|
|
// TODO: Store PathFlags on the steering comp
|
|
// and be able to re-check it.
|
|
|
|
var layer = 0;
|
|
var mask = 0;
|
|
|
|
if (TryComp<FixturesComponent>(uid, out var manager))
|
|
{
|
|
(layer, mask) = _physics.GetHardCollision(uid, manager);
|
|
}
|
|
else
|
|
{
|
|
return SteeringObstacleStatus.Failed;
|
|
}
|
|
|
|
// TODO: Should cache the fact we're doing this somewhere.
|
|
// See https://github.com/space-wizards/space-station-14/issues/11475
|
|
if ((poly.Data.CollisionLayer & mask) == 0x0 &&
|
|
(poly.Data.CollisionMask & layer) == 0x0)
|
|
return SteeringObstacleStatus.Completed;
|
|
|
|
var id = component.DoAfterId;
|
|
|
|
// Still doing what we were doing before.
|
|
var doAfterStatus = _doAfter.GetStatus(id);
|
|
|
|
switch (doAfterStatus)
|
|
{
|
|
case DoAfterStatus.Running:
|
|
return SteeringObstacleStatus.Continuing;
|
|
case DoAfterStatus.Cancelled:
|
|
return SteeringObstacleStatus.Failed;
|
|
}
|
|
|
|
if (!TryComp<MapGridComponent>(poly.GraphUid, out var grid))
|
|
return SteeringObstacleStatus.Completed;
|
|
|
|
var obstacleEnts = new HashSet<EntityUid>(_mapSystem.GetLocalAnchoredEntities(poly.GraphUid, grid, poly.Box));
|
|
FilterObstacleEntities((uid, component), mask, layer, obstacleEnts);
|
|
|
|
// Nothing actually near us.
|
|
if (obstacleEnts.Count == 0)
|
|
return SteeringObstacleStatus.Completed;
|
|
|
|
var isDoor = (poly.Data.Flags & PathfindingBreadcrumbFlag.Door) != 0x0;
|
|
var isClimbable = (poly.Data.Flags & PathfindingBreadcrumbFlag.Climb) != 0x0;
|
|
|
|
// Just walk into it stupid.
|
|
if (isDoor)
|
|
{
|
|
foreach (var ent in obstacleEnts)
|
|
{
|
|
// Includes checking if we have access:
|
|
if (!CanHandleDoor((uid, component), poly.Data.Flags, ent, false))
|
|
continue;
|
|
|
|
// Interacts are bit nicer than bumps, so try interacting regardless.
|
|
_interaction.InteractionActivate(uid, ent);
|
|
|
|
return SteeringObstacleStatus.Continuing;
|
|
}
|
|
|
|
// Couldn't normal-open the door, and can't pry. Give up.
|
|
if ((component.Flags & PathFlags.Prying) == 0x0)
|
|
return SteeringObstacleStatus.Failed;
|
|
|
|
foreach (var ent in obstacleEnts)
|
|
{
|
|
if (!CanHandleDoor((uid, component), poly.Data.Flags, ent))
|
|
continue;
|
|
|
|
// Should be able to pry from CanHandleDoor:
|
|
// TODO: Use the verb.
|
|
_pryingSystem.TryPry(ent, uid, out id, uid);
|
|
|
|
if (id == null)
|
|
return SteeringObstacleStatus.Failed;
|
|
|
|
component.DoAfterId = id;
|
|
return SteeringObstacleStatus.Continuing;
|
|
}
|
|
}
|
|
// Try climbing obstacles
|
|
else if ((component.Flags & PathFlags.Climbing) != 0x0 && isClimbable)
|
|
{
|
|
if (!TryComp<ClimbingComponent>(uid, out var climbing))
|
|
return SteeringObstacleStatus.Failed;
|
|
|
|
if (climbing.IsClimbing)
|
|
return SteeringObstacleStatus.Completed;
|
|
|
|
if (climbing.NextTransition != null)
|
|
return SteeringObstacleStatus.Continuing;
|
|
|
|
// Get the relevant obstacle.
|
|
foreach (var ent in obstacleEnts)
|
|
{
|
|
if (CanHandleClimb((uid, climbing), ent, out var climbable) &&
|
|
_climb.TryClimb(uid, uid, ent, out id, climbable, climbing))
|
|
{
|
|
component.DoAfterId = id;
|
|
return SteeringObstacleStatus.Continuing;
|
|
}
|
|
}
|
|
}
|
|
// Try smashing obstacles.
|
|
else if ((component.Flags & PathFlags.Smashing) != 0x0)
|
|
{
|
|
// Check we have a weapon, can (probably) swing it, and have combat mode.
|
|
if (!_melee.TryGetWeapon(uid, out var weaponUid, out var weaponComp) ||
|
|
weaponComp.NextAttack > _timing.CurTime ||
|
|
!TryComp<CombatModeComponent>(uid, out var combatMode))
|
|
return SteeringObstacleStatus.Failed;
|
|
|
|
_combat.SetInCombatMode(uid, true, combatMode);
|
|
|
|
var attackResult = false;
|
|
foreach (var ent in obstacleEnts)
|
|
{
|
|
if (!_destructibleQuery.HasComponent(ent) ||
|
|
!_interaction.InRangeUnobstructed(uid, ent, weaponComp.Range))
|
|
continue;
|
|
|
|
if (_melee.AttemptLightAttack(uid, weaponUid, weaponComp, ent))
|
|
{
|
|
attackResult = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
_combat.SetInCombatMode(uid, false, combatMode);
|
|
|
|
// Blocked or the likes?
|
|
if (!attackResult)
|
|
return SteeringObstacleStatus.Failed;
|
|
|
|
return SteeringObstacleStatus.Continuing;
|
|
}
|
|
|
|
return SteeringObstacleStatus.Failed;
|
|
}
|
|
|
|
private enum SteeringObstacleStatus : byte
|
|
{
|
|
Completed,
|
|
Failed,
|
|
Continuing
|
|
}
|
|
}
|