diff --git a/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs b/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs index 817442b4c5..385658b3c5 100644 --- a/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs +++ b/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs @@ -22,6 +22,8 @@ namespace Content.IntegrationTests.Tests.Interaction private const float InteractionRangeDivided15Times3 = InteractionRangeDivided15 * 3; + private const float HumanRadius = 0.35f; + [Test] public async Task EntityEntityTest() { @@ -74,7 +76,7 @@ namespace Content.IntegrationTests.Tests.Interaction Assert.True(interactionSys.InRangeUnobstructed(mapCoordinates, origin)); // Move them out of range - sEntities.GetComponent(origin).LocalPosition += _interactionRangeDivided15X; + sEntities.GetComponent(origin).LocalPosition += new Vector2(InteractionRangeDivided15 + HumanRadius * 2f, 0f); // Entity <-> Entity Assert.False(interactionSys.InRangeUnobstructed(origin, other)); diff --git a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs index 020d5f523d..c955d1c891 100644 --- a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs +++ b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs @@ -121,11 +121,8 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem { return; } - - // InRangeUnobstructed is insufficient rn as it checks the centre of the body rather than the nearest edge. - // TODO: Look at fixing it - // This is mainly to keep consistency between the wide attack raycast and the click attack raycast. - if (!_interaction.InRangeUnobstructed(user, ev.Target.Value, component.Range + 0.35f)) + + if (!_interaction.InRangeUnobstructed(user, ev.Target.Value, component.Range)) return; var damage = component.Damage * GetModifier(component, true); diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index b9f723966d..b5d917a6df 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -54,7 +54,7 @@ namespace Content.Shared.Interaction private const CollisionGroup InRangeUnobstructedMask = CollisionGroup.Impassable | CollisionGroup.InteractImpassable; - public const float InteractionRange = 2f; + public const float InteractionRange = 1.5f; public const float InteractionRangeSquared = InteractionRange * InteractionRange; public const float MaxRaycastRange = 100f; @@ -440,10 +440,63 @@ namespace Content.Shared.Interaction CollisionGroup collisionMask = InRangeUnobstructedMask, Ignored? predicate = null, bool popup = false) - {; + { Ignored combinedPredicate = e => e == origin || (predicate?.Invoke(e) ?? false); + var inRange = true; + MapCoordinates originPos = default; + MapCoordinates targetPos = default; + Angle targetRot = default; - var inRange = InRangeUnobstructed(Transform(origin).MapPosition, other, range, collisionMask, combinedPredicate); + // Alternatively we could check centre distances first though + // that means we wouldn't be able to easily check overlap interactions. + if (range > 0f && + TryComp(origin, out var fixtureA) && + TryComp(other, out var fixtureB) && + TryComp(origin, out var xformA) && + TryComp(other, out var xformB)) + { + // Different map or the likes. + if (!_sharedBroadphaseSystem.TryGetNearest(origin, other, + out var pointA, out var pointB, out var distance, + xformA, xformB, fixtureA, fixtureB)) + { + inRange = false; + } + // Overlap, early out and no raycast. + else if (distance.Equals(0f)) + { + return true; + } + // Out of range so don't raycast. + else if (distance > range) + { + inRange = false; + } + else + { + // We'll still do the raycast from the centres but we'll bump the range as we know they're in range. + originPos = xformA.MapPosition; + (var targetWorld, targetRot) = xformB.GetWorldPositionRotation(); + targetPos = new MapCoordinates(targetWorld, xformB.MapID); + + range = (originPos.Position - targetWorld).Length; + } + } + else + { + originPos = Transform(origin).MapPosition; + + xformB = Transform(other); + (var targetWorld, targetRot) = xformB.GetWorldPositionRotation(); + targetPos = new MapCoordinates(targetWorld, xformB.MapID); + } + + // Do a raycast to check if relevant + if (inRange) + { + var rayPredicate = GetPredicate(originPos, other, targetPos, targetRot, collisionMask, combinedPredicate); + inRange = InRangeUnobstructed(originPos, targetPos, range, collisionMask, rayPredicate); + } if (!inRange && popup && _gameTiming.IsFirstTimePredicted) { @@ -464,7 +517,25 @@ namespace Content.Shared.Interaction var transform = Transform(target); var (position, rotation) = transform.GetWorldPositionRotation(); var mapPos = new MapCoordinates(position, transform.MapID); + var combinedPredicate = GetPredicate(origin, target, mapPos, rotation, collisionMask, predicate); + return InRangeUnobstructed(origin, mapPos, range, collisionMask, combinedPredicate); + } + + /// + /// Gets the entities to ignore for an unobstructed raycast + /// + /// + /// if the target entity is a wallmount we ignore all other entities on the tile. + /// + private Ignored GetPredicate( + MapCoordinates origin, + EntityUid target, + MapCoordinates targetCoords, + Angle targetRotation, + CollisionGroup collisionMask, + Ignored? predicate = null) + { HashSet ignored = new(); bool ignoreAnchored = false; @@ -483,23 +554,23 @@ namespace Content.Shared.Interaction ignoreAnchored = true; else { - var angle = Angle.FromWorldVec(origin.Position - position); - var angleDelta = (wallMount.Direction + rotation - angle).Reduced().FlipPositive(); + var angle = Angle.FromWorldVec(origin.Position - targetCoords.Position); + var angleDelta = (wallMount.Direction + targetRotation - angle).Reduced().FlipPositive(); ignoreAnchored = angleDelta < wallMount.Arc / 2 || Math.Tau - angleDelta < wallMount.Arc / 2; } - if (ignoreAnchored && _mapManager.TryFindGridAt(mapPos, out var grid)) - ignored.UnionWith(grid.GetAnchoredEntities(mapPos)); + if (ignoreAnchored && _mapManager.TryFindGridAt(targetCoords, out var grid)) + ignored.UnionWith(grid.GetAnchoredEntities(targetCoords)); } Ignored combinedPredicate = e => { return e == target - || (predicate?.Invoke(e) ?? false) - || ignored.Contains(e); + || (predicate?.Invoke(e) ?? false) + || ignored.Contains(e); }; - return InRangeUnobstructed(origin, mapPos, range, collisionMask, combinedPredicate); + return combinedPredicate; } /// @@ -569,9 +640,9 @@ namespace Content.Shared.Interaction Ignored? predicate = null, bool popup = false) { - Ignored combinedPredicatre = e => e == origin || (predicate?.Invoke(e) ?? false); + Ignored combinedPredicate = e => e == origin || (predicate?.Invoke(e) ?? false); var originPosition = Transform(origin).MapPosition; - var inRange = InRangeUnobstructed(originPosition, other, range, collisionMask, combinedPredicatre); + var inRange = InRangeUnobstructed(originPosition, other, range, collisionMask, combinedPredicate); if (!inRange && popup && _gameTiming.IsFirstTimePredicted) {