using System.Numerics; using Content.Shared.Administration.Logs; using Content.Shared.Damage.Components; using Content.Shared.Database; using Content.Shared.Weapons.Hitscan.Components; using Content.Shared.Weapons.Hitscan.Events; using Content.Shared.Weapons.Ranged.Systems; using Robust.Shared.Containers; using Robust.Shared.Map; using Robust.Shared.Physics; using Robust.Shared.Physics.Systems; using Robust.Shared.Player; using Robust.Shared.Utility; namespace Content.Shared.Weapons.Hitscan.Systems; public sealed class HitscanBasicRaycastSystem : EntitySystem { [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly ISharedAdminLogManager _log = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; private EntityQuery _visualsQuery; public override void Initialize() { base.Initialize(); _visualsQuery = GetEntityQuery(); SubscribeLocalEvent(OnHitscanFired); } private void OnHitscanFired(Entity ent, ref HitscanTraceEvent args) { var shooter = args.Shooter ?? args.Gun; var mapCords = _transform.ToMapCoordinates(args.FromCoordinates); var ray = new CollisionRay(mapCords.Position, args.ShotDirection, (int) ent.Comp.CollisionMask); var rayCastResults = _physics.IntersectRay(mapCords.MapId, ray, ent.Comp.MaxDistance, shooter, false); var target = args.Target; // If you are in a container, use the raycast result // Otherwise: // 1.) Hit the first entity that you targeted. // 2.) Hit the first entity that doesn't require you to aim at it specifically to be hit. var result = _container.IsEntityOrParentInContainer(shooter) ? rayCastResults.FirstOrNull() : rayCastResults.FirstOrNull(hit => hit.HitEntity == target || CompOrNull(hit.HitEntity)?.Active != true); var distanceTried = result?.Distance ?? ent.Comp.MaxDistance; // Do visuals without an event. They should always happen and putting it on the attempt event is weird! // If more stuff gets added here, it should probably be turned into an event. FireEffects(args.FromCoordinates, distanceTried, args.ShotDirection.ToAngle(), ent.Owner); // Admin logging if (result?.HitEntity != null) { _log.Add(LogType.HitScanHit, $"{ToPrettyString(shooter):user} hit {ToPrettyString(result.Value.HitEntity):target}" + $" using {ToPrettyString(args.Gun):entity}."); } var data = new HitscanRaycastFiredData { ShotDirection = args.ShotDirection, Gun = args.Gun, Shooter = args.Shooter, HitEntity = result?.HitEntity, }; var attemptEvent = new AttemptHitscanRaycastFiredEvent { Data = data }; RaiseLocalEvent(ent, ref attemptEvent); if (attemptEvent.Cancelled) return; var hitEvent = new HitscanRaycastFiredEvent { Data = data }; RaiseLocalEvent(ent, ref hitEvent); } /// /// Create visual effects for the fired hitscan weapon. /// /// Location to start the effect. /// Distance of the hitscan shot. /// Angle of the shot. /// The hitscan entity itself. private void FireEffects(EntityCoordinates fromCoordinates, float distance, Angle shotAngle, EntityUid hitscanUid) { if (distance == 0 || !_visualsQuery.TryComp(hitscanUid, out var vizComp)) return; var sprites = new List<(NetCoordinates coordinates, Angle angle, SpriteSpecifier sprite, float scale)>(); var fromXform = Transform(fromCoordinates.EntityId); // We'll get the effects relative to the grid / map of the firer // Look you could probably optimise this a bit with redundant transforms at this point. var gridUid = fromXform.GridUid; if (gridUid != fromCoordinates.EntityId && TryComp(gridUid, out TransformComponent? gridXform)) { var (_, gridRot, gridInvMatrix) = _transform.GetWorldPositionRotationInvMatrix(gridXform); var map = _transform.ToMapCoordinates(fromCoordinates); fromCoordinates = new EntityCoordinates(gridUid.Value, Vector2.Transform(map.Position, gridInvMatrix)); shotAngle -= gridRot; } else { shotAngle -= _transform.GetWorldRotation(fromXform); } if (distance >= 1f) { if (vizComp.MuzzleFlash != null) { var coords = fromCoordinates.Offset(shotAngle.ToVec().Normalized() / 2); var netCoords = GetNetCoordinates(coords); sprites.Add((netCoords, shotAngle, vizComp.MuzzleFlash, 1f)); } if (vizComp.TravelFlash != null) { var coords = fromCoordinates.Offset(shotAngle.ToVec() * (distance + 0.5f) / 2); var netCoords = GetNetCoordinates(coords); sprites.Add((netCoords, shotAngle, vizComp.TravelFlash, distance - 1.5f)); } } if (vizComp.ImpactFlash != null) { var coords = fromCoordinates.Offset(shotAngle.ToVec() * distance); var netCoords = GetNetCoordinates(coords); sprites.Add((netCoords, shotAngle.FlipPositive(), vizComp.ImpactFlash, 1f)); } if (sprites.Count > 0) { RaiseNetworkEvent(new SharedGunSystem.HitscanEvent { Sprites = sprites, }, Filter.Pvs(fromCoordinates, entityMan: EntityManager)); } } }