using System.Linq;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Physics;
using JetBrains.Annotations;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Physics;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.Shared.GameObjects.EntitySystems
{
///
/// Governs interactions during clicking on entities
///
[UsedImplicitly]
public class SharedInteractionSystem : EntitySystem
{
[Dependency] private readonly IPhysicsManager _physicsManager = default!;
public const float InteractionRange = 2;
public const float InteractionRangeSquared = InteractionRange * InteractionRange;
public delegate bool Ignored(IEntity entity);
///
/// Traces a ray from coords to otherCoords and returns the length
/// of the vector between coords and the ray's first hit.
///
/// Set of coordinates to use.
/// Other set of coordinates to use.
/// the mask to check for collisions
///
/// A predicate to check whether to ignore an entity or not.
/// If it returns true, it will be ignored.
///
/// Length of resulting ray.
public float UnobstructedDistance(
MapCoordinates origin,
MapCoordinates other,
int collisionMask = (int) CollisionGroup.Impassable,
Ignored predicate = null)
{
var dir = other.Position - origin.Position;
if (dir.LengthSquared.Equals(0f)) return 0f;
predicate ??= _ => false;
var ray = new CollisionRay(origin.Position, dir.Normalized, collisionMask);
var rayResults = _physicsManager.IntersectRayWithPredicate(origin.MapId, ray, dir.Length, predicate.Invoke, false).ToList();
if (rayResults.Count == 0) return dir.Length;
return (rayResults[0].HitPos - origin.Position).Length;
}
///
/// Traces a ray from coords to otherCoords and returns the length
/// of the vector between coords and the ray's first hit.
///
/// Set of coordinates to use.
/// Other set of coordinates to use.
/// The mask to check for collisions
///
/// The entity to be ignored when checking for collisions.
///
/// Length of resulting ray.
public float UnobstructedDistance(
MapCoordinates origin,
MapCoordinates other,
int collisionMask = (int) CollisionGroup.Impassable,
IEntity ignoredEnt = null)
{
var predicate = ignoredEnt == null
? null
: (Ignored) (e => e == ignoredEnt);
return UnobstructedDistance(origin, other, collisionMask, predicate);
}
///
/// Checks that these coordinates are within a certain distance without any
/// entity that matches the collision mask obstructing them.
/// If the is zero or negative,
/// this method will only check if nothing obstructs the two sets
/// of coordinates.
///
/// Set of coordinates to use.
/// Other set of coordinates to use.
///
/// Maximum distance between the two sets of coordinates.
///
/// The mask to check for collisions.
///
/// A predicate to check whether to ignore an entity or not.
/// If it returns true, it will be ignored.
///
///
/// If true and or are inside
/// the obstruction, ignores the obstruction and considers the interaction
/// unobstructed.
/// Therefore, setting this to true makes this check more permissive,
/// such as allowing an interaction to occur inside something impassable
/// (like a wall). The default, false, makes the check more restrictive.
///
///
/// True if the two points are within a given range without being obstructed.
///
public bool InRangeUnobstructed(
MapCoordinates origin,
MapCoordinates other,
float range = InteractionRange,
CollisionGroup collisionMask = CollisionGroup.Impassable,
Ignored predicate = null,
bool ignoreInsideBlocker = false)
{
if (!origin.InRange(other, range)) return false;
var dir = other.Position - origin.Position;
if (dir.LengthSquared.Equals(0f)) return true;
if (range > 0f && !(dir.LengthSquared <= range * range)) return false;
predicate ??= _ => false;
var ray = new CollisionRay(origin.Position, dir.Normalized, (int) collisionMask);
var rayResults = _physicsManager.IntersectRayWithPredicate(origin.MapId, ray, dir.Length, predicate.Invoke, false).ToList();
if (rayResults.Count == 0) return true;
if (!ignoreInsideBlocker) return false;
if (rayResults.Count <= 0) return false;
return (rayResults[0].HitPos - other.Position).Length < 1f;
}
///
/// Checks that two entities are within a certain distance without any
/// entity that matches the collision mask obstructing them.
/// If the is zero or negative,
/// this method will only check if nothing obstructs the two entities.
///
/// The first entity to use.
/// Other entity to use.
///
/// Maximum distance between the two entities.
///
/// The mask to check for collisions.
///
/// A predicate to check whether to ignore an entity or not.
/// If it returns true, it will be ignored.
///
///
/// If true and or are inside
/// the obstruction, ignores the obstruction and considers the interaction
/// unobstructed.
/// Therefore, setting this to true makes this check more permissive,
/// such as allowing an interaction to occur inside something impassable
/// (like a wall). The default, false, makes the check more restrictive.
///
///
/// Whether or not to popup a feedback message on the origin entity for
/// it to see.
///
///
/// True if the two points are within a given range without being obstructed.
///
public bool InRangeUnobstructed(
IEntity origin,
IEntity other,
float range = InteractionRange,
CollisionGroup collisionMask = CollisionGroup.Impassable,
Ignored predicate = null,
bool ignoreInsideBlocker = false,
bool popup = false)
{
var originPosition = origin.Transform.MapPosition;
var otherPosition = other.Transform.MapPosition;
predicate ??= e => e == origin || e == other;
var inRange = InRangeUnobstructed(originPosition, otherPosition, range, collisionMask, predicate, ignoreInsideBlocker);
if (!inRange && popup)
{
var message = Loc.GetString("You can't reach there!");
origin.PopupMessage(message);
}
return inRange;
}
///
/// Checks that an entity and a component are within a certain
/// distance without any entity that matches the collision mask
/// obstructing them.
/// If the is zero or negative,
/// this method will only check if nothing obstructs the entity and component.
///
/// The entity to use.
/// The component to use.
///
/// Maximum distance between the entity and component.
///
/// The mask to check for collisions.
///
/// A predicate to check whether to ignore an entity or not.
/// If it returns true, it will be ignored.
///
///
/// If true and or are inside
/// the obstruction, ignores the obstruction and considers the interaction
/// unobstructed.
/// Therefore, setting this to true makes this check more permissive,
/// such as allowing an interaction to occur inside something impassable
/// (like a wall). The default, false, makes the check more restrictive.
///
///
/// Whether or not to popup a feedback message on the origin entity for
/// it to see.
///
///
/// True if the two points are within a given range without being obstructed.
///
public bool InRangeUnobstructed(
IEntity origin,
IComponent other,
float range = InteractionRange,
CollisionGroup collisionMask = CollisionGroup.Impassable,
Ignored predicate = null,
bool ignoreInsideBlocker = false,
bool popup = false)
{
var originPosition = origin.Transform.MapPosition;
var otherPosition = other.Owner.Transform.MapPosition;
predicate ??= e => e == origin || e == other.Owner;
var inRange = InRangeUnobstructed(originPosition, otherPosition, range, collisionMask, predicate, ignoreInsideBlocker);
if (!inRange && popup)
{
var message = Loc.GetString("You can't reach there!");
origin.PopupMessage(message);
}
return inRange;
}
///
/// Checks that an entity and a set of grid coordinates are within a certain
/// distance without any entity that matches the collision mask
/// obstructing them.
/// If the is zero or negative,
/// this method will only check if nothing obstructs the entity and component.
///
/// The entity to use.
/// The grid coordinates to use.
///
/// Maximum distance between the two entity and set of grid coordinates.
///
/// The mask to check for collisions.
///
/// A predicate to check whether to ignore an entity or not.
/// If it returns true, it will be ignored.
///
///
/// If true and or are inside
/// the obstruction, ignores the obstruction and considers the interaction
/// unobstructed.
/// Therefore, setting this to true makes this check more permissive,
/// such as allowing an interaction to occur inside something impassable
/// (like a wall). The default, false, makes the check more restrictive.
///
///
/// Whether or not to popup a feedback message on the origin entity for
/// it to see.
///
///
/// True if the two points are within a given range without being obstructed.
///
public bool InRangeUnobstructed(
IEntity origin,
EntityCoordinates other,
float range = InteractionRange,
CollisionGroup collisionMask = CollisionGroup.Impassable,
Ignored predicate = null,
bool ignoreInsideBlocker = false,
bool popup = false)
{
var originPosition = origin.Transform.MapPosition;
var otherPosition = other.ToMap(EntityManager);
predicate ??= e => e == origin;
var inRange = InRangeUnobstructed(originPosition, otherPosition, range, collisionMask, predicate, ignoreInsideBlocker);
if (!inRange && popup)
{
var message = Loc.GetString("You can't reach there!");
origin.PopupMessage(message);
}
return inRange;
}
///
/// Checks that an entity and a set of map coordinates are within a certain
/// distance without any entity that matches the collision mask
/// obstructing them.
/// If the is zero or negative,
/// this method will only check if nothing obstructs the entity and component.
///
/// The entity to use.
/// The map coordinates to use.
///
/// Maximum distance between the two entity and set of map coordinates.
///
/// The mask to check for collisions.
///
/// A predicate to check whether to ignore an entity or not.
/// If it returns true, it will be ignored.
///
///
/// If true and or are inside
/// the obstruction, ignores the obstruction and considers the interaction
/// unobstructed.
/// Therefore, setting this to true makes this check more permissive,
/// such as allowing an interaction to occur inside something impassable
/// (like a wall). The default, false, makes the check more restrictive.
///
///
/// Whether or not to popup a feedback message on the origin entity for
/// it to see.
///
///
/// True if the two points are within a given range without being obstructed.
///
public bool InRangeUnobstructed(
IEntity origin,
MapCoordinates other,
float range = InteractionRange,
CollisionGroup collisionMask = CollisionGroup.Impassable,
Ignored predicate = null,
bool ignoreInsideBlocker = false,
bool popup = false)
{
var originPosition = origin.Transform.MapPosition;
predicate ??= e => e == origin;
var inRange = InRangeUnobstructed(originPosition, other, range, collisionMask, predicate, ignoreInsideBlocker);
if (!inRange && popup)
{
var message = Loc.GetString("You can't reach there!");
origin.PopupMessage(message);
}
return inRange;
}
///
/// Checks that the user and target of a
/// are within a certain
/// distance without any entity that matches the collision mask
/// obstructing them.
/// If the is zero or negative,
/// this method will only check if nothing obstructs the entity and component.
///
/// The event args to use.
///
/// Maximum distance between the two entity and set of map coordinates.
///
/// The mask to check for collisions.
///
/// A predicate to check whether to ignore an entity or not.
/// If it returns true, it will be ignored.
///
///
/// If true and both the user and target are inside
/// the obstruction, ignores the obstruction and considers the interaction
/// unobstructed.
/// Therefore, setting this to true makes this check more permissive,
/// such as allowing an interaction to occur inside something impassable
/// (like a wall). The default, false, makes the check more restrictive.
///
///
/// Whether or not to popup a feedback message on the user entity for
/// it to see.
///
///
/// True if the two points are within a given range without being obstructed.
///
public bool InRangeUnobstructed(
ITargetedInteractEventArgs args,
float range = InteractionRange,
CollisionGroup collisionMask = CollisionGroup.Impassable,
Ignored predicate = null,
bool ignoreInsideBlocker = false,
bool popup = false)
{
var origin = args.User;
var other = args.Target;
return InRangeUnobstructed(origin, other, range, collisionMask, predicate, ignoreInsideBlocker, popup);
}
///
/// Checks that the user of a is within a
/// certain distance of the target and dropped entities without any entity
/// that matches the collision mask obstructing them.
/// If the is zero or negative,
/// this method will only check if nothing obstructs the entity and component.
///
/// The event args to use.
///
/// Maximum distance between the two entity and set of map coordinates.
///
/// The mask to check for collisions.
///
/// A predicate to check whether to ignore an entity or not.
/// If it returns true, it will be ignored.
///
///
/// If true and both the user and target are inside
/// the obstruction, ignores the obstruction and considers the interaction
/// unobstructed.
/// Therefore, setting this to true makes this check more permissive,
/// such as allowing an interaction to occur inside something impassable
/// (like a wall). The default, false, makes the check more restrictive.
///
///
/// Whether or not to popup a feedback message on the user entity for
/// it to see.
///
///
/// True if the two points are within a given range without being obstructed.
///
public bool InRangeUnobstructed(
DragDropEventArgs args,
float range = InteractionRange,
CollisionGroup collisionMask = CollisionGroup.Impassable,
Ignored predicate = null,
bool ignoreInsideBlocker = false,
bool popup = false)
{
var user = args.User;
var dropped = args.Dropped;
var target = args.Target;
if (!InRangeUnobstructed(user, target, range, collisionMask, predicate, ignoreInsideBlocker))
{
if (popup)
{
var message = Loc.GetString("You can't reach there!");
target.PopupMessage(user, message);
}
return false;
}
if (!InRangeUnobstructed(user, dropped, range, collisionMask, predicate, ignoreInsideBlocker))
{
if (popup)
{
var message = Loc.GetString("You can't reach there!");
dropped.PopupMessage(user, message);
}
return false;
}
return true;
}
///
/// Checks that the user and target of a
/// are within a certain distance
/// without any entity that matches the collision mask obstructing them.
/// If the is zero or negative,
/// this method will only check if nothing obstructs the entity and component.
///
/// The event args to use.
///
/// Maximum distance between the two entity and set of map coordinates.
///
/// The mask to check for collisions.
///
/// A predicate to check whether to ignore an entity or not.
/// If it returns true, it will be ignored.
///
///
/// If true and both the user and target are inside
/// the obstruction, ignores the obstruction and considers the interaction
/// unobstructed.
/// Therefore, setting this to true makes this check more permissive,
/// such as allowing an interaction to occur inside something impassable
/// (like a wall). The default, false, makes the check more restrictive.
///
///
/// Whether or not to popup a feedback message on the user entity for
/// it to see.
///
///
/// True if the two points are within a given range without being obstructed.
///
public bool InRangeUnobstructed(
AfterInteractEventArgs args,
float range = InteractionRange,
CollisionGroup collisionMask = CollisionGroup.Impassable,
Ignored predicate = null,
bool ignoreInsideBlocker = false,
bool popup = false)
{
var user = args.User;
var target = args.Target;
predicate ??= e => e == user;
MapCoordinates otherPosition;
if (target == null)
{
otherPosition = args.ClickLocation.ToMap(EntityManager);
}
else
{
otherPosition = target.Transform.MapPosition;
predicate += e => e == target;
}
return InRangeUnobstructed(user, otherPosition, range, collisionMask, predicate, ignoreInsideBlocker, popup);
}
}
}