using System.Linq; using System.Numerics; using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.Components; using Content.Server.Cargo.Systems; using Content.Server.Interaction; using Content.Server.Mech.Equipment.Components; using Content.Server.Power.EntitySystems; using Content.Server.Weapons.Ranged.Components; using Content.Server.Stunnable; using Content.Server.Stunnable.Components; using Content.Server.Emp; using Content.Shared.Damage; using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.Effects; using Content.Shared.Interaction.Components; using Content.Shared.Mech.Equipment.Components; using Content.Shared.Projectiles; using Content.Shared.StatusEffect; using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Ranged; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Systems; using Content.Shared.Weapons.Reflect; using Content.Shared.Damage.Components; using Robust.Shared.Audio; using Robust.Shared.Map; using Robust.Shared.Physics; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Utility; using Robust.Shared.Containers; using Content.Shared._Starlight.Weapon.Components; using Robust.Shared.Physics.Dynamics; using Content.Shared.Movement.Components; using Robust.Shared.Random; using Content.Shared.Decals; using Content.Server.Body.Components; using Content.Shared.Chemistry.Reagent; using Robust.Shared.Timing; using Content.Server.Decals; using System; using Content.Server.IgnitionSource; using Content.Server.Atmos.EntitySystems; using Microsoft.CodeAnalysis.Elfie.Diagnostics; using Content.Server.Atmos.Components; using Content.Shared._Starlight.Weapon; using Robust.Shared.Maths; using Content.Shared.Pinpointer; using Robust.Server.GameObjects; using System.Collections.Generic; using Content.Server.PowerCell; using Content.Shared.Body.Components; using Content.Shared.Cargo; namespace Content.Server.Weapons.Ranged.Systems; public sealed partial class GunSystem : SharedGunSystem { [Dependency] private readonly IComponentFactory _factory = default!; [Dependency] private readonly IRobustRandom _rand = default!; // 🌟Starlight🌟 [Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly DamageExamineSystem _damageExamine = default!; [Dependency] private readonly PricingSystem _pricing = default!; [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; [Dependency] private readonly TransformSystem _transform = default!; // 🌟Starlight🌟 [Dependency] private readonly SharedStaminaSystem _stamina = default!; [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedMapSystem _map = default!; [Dependency] private readonly IPrototypeManager _proto = default!; // 🌟Starlight🌟 [Dependency] private readonly DecalSystem _decals = default!; // 🌟Starlight🌟 [Dependency] private readonly FlammableSystem _flammableSystem = default!; // 🌟Starlight🌟 [Dependency] private readonly AtmosphereSystem _atmosphere = default!; // 🌟Starlight🌟 [Dependency] private readonly StunSystem _stunSystem = default!; // 🌟Starlight🌟 [Dependency] private readonly EmpSystem _emp = default!; // 🌟Starlight🌟 private const float DamagePitchVariation = 0.05f; private string[] _bloodDecals = []; // 🌟Starlight🌟 public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnBallisticPrice); CacheDecals(); } private void CacheDecals() // 🌟Starlight🌟 { _bloodDecals = _proto.EnumeratePrototypes().Where(x => x.Tags.Contains("BloodSplatter")).Select(x => x.ID).ToArray(); } private void OnBallisticPrice(EntityUid uid, BallisticAmmoProviderComponent component, ref PriceCalculationEvent args) { if (string.IsNullOrEmpty(component.Proto) || component.UnspawnedCount == 0) return; if (!ProtoManager.TryIndex(component.Proto, out var proto)) { Log.Error($"Unable to find fill prototype for price on {component.Proto} on {ToPrettyString(uid)}"); return; } // Probably good enough for most. var price = _pricing.GetEstimatedPrice(proto); args.Price += price * component.UnspawnedCount; } public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? Entity, IShootable Shootable)> ammo, EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, out bool userImpulse, EntityUid? user = null, bool throwItems = false) { userImpulse = true; if (user != null) { var selfEvent = new SelfBeforeGunShotEvent(user.Value, (gunUid, gun), ammo); RaiseLocalEvent(user.Value, selfEvent); if (selfEvent.Cancelled) { userImpulse = false; return; } } var fromMap = TransformSystem.ToMapCoordinates(fromCoordinates); var toMap = TransformSystem.ToMapCoordinates(toCoordinates).Position; var mapDirection = toMap - fromMap.Position; var mapAngle = mapDirection.ToAngle(); var angle = GetRecoilAngle(Timing.CurTime, gun, mapDirection.ToAngle()); // If applicable, this ensures the projectile is parented to grid on spawn, instead of the map. var fromEnt = MapManager.TryFindGridAt(fromMap, out var gridUid, out _) ? TransformSystem.WithEntityId(fromCoordinates, gridUid) : new EntityCoordinates(_map.GetMapOrInvalid(fromMap.MapId), fromMap.Position); var pointerLength = mapDirection.Length(); // 🌟Starlight🌟 // Update shot based on the recoil toMap = fromMap.Position + (angle.ToVec() * pointerLength); // 🌟Starlight🌟 mapDirection = toMap - fromMap.Position; var gunVelocity = Physics.GetMapLinearVelocity(fromEnt); // I must be high because this was getting tripped even when true. // DebugTools.Assert(direction != Vector2.Zero); var shotProjectiles = new List(ammo.Count); bool bulletSoundCheck = false; //starlight foreach (var (ent, shootable) in ammo) { // pneumatic cannon doesn't shoot bullets it just throws them, ignore ammo handling if (throwItems && ent != null) { ShootOrThrow(ent.Value, mapDirection, gunVelocity, gun, gunUid, user); continue; } switch (shootable) { //🌟Starlight🌟 case HitScanCartridgeAmmoComponent cartridge: if (!cartridge.Spent) { var hitscanPrototype = ProtoManager.Index(cartridge.Hitscan); var hitHashSet = new HashSet(); if (hitscanPrototype.Count > 1) { var spread = (hitscanPrototype.Spread + gun.Spread) / 2; var spreadEvent = new GunGetAmmoSpreadEvent(spread); RaiseLocalEvent(gunUid, ref spreadEvent); var angles = LinearSpreadWithRandom(mapAngle - (spreadEvent.Spread / 2), mapAngle + (spreadEvent.Spread / 2), hitscanPrototype.Count, 3f); List> hits = new(hitscanPrototype.Count); for (var i = 0; i < hitscanPrototype.Count; i++) hits.Add(Hitscan(gunUid, gun, fromCoordinates, user, fromMap, pointerLength, angles[i].ToVec(), hitscanPrototype, hitHashSet)); FireEffects(hits.ToList(), hitscanPrototype); } else { var hits = Hitscan(gunUid, gun, fromCoordinates, user, fromMap, pointerLength, mapDirection, hitscanPrototype, hitHashSet); FireEffects([hits], hitscanPrototype); } RaiseLocalEvent(ent!.Value, new AmmoShotEvent() { FiredProjectiles = shotProjectiles, }); SetCartridgeSpent(ent!.Value, cartridge, true); if (cartridge.DeleteOnSpawn) Del(ent.Value); } else { userImpulse = false; Audio.PlayPredicted(gun.SoundEmpty, gunUid, user); } // Something like ballistic might want to leave it in the container still if (!cartridge.DeleteOnSpawn && !Containers.IsEntityInContainer(ent!.Value) && !gun.Pump) EjectCartridge(ent.Value, angle); Dirty(ent!.Value, cartridge); break; // Cartridge shoots something else case CartridgeAmmoComponent cartridge: if (!cartridge.Spent) { var uid = Spawn(cartridge.Prototype, fromEnt); CreateAndFireProjectiles(uid, cartridge); RaiseLocalEvent(ent!.Value, new AmmoShotEvent() { FiredProjectiles = shotProjectiles, }); SetCartridgeSpent(ent.Value, cartridge, true); if (cartridge.DeleteOnSpawn) Del(ent.Value); } else { userImpulse = false; Audio.PlayPredicted(gun.SoundEmpty, gunUid, user); } // Something like ballistic might want to leave it in the container still if (!cartridge.DeleteOnSpawn && !Containers.IsEntityInContainer(ent!.Value) && !gun.Pump) EjectCartridge(ent.Value, angle); Dirty(ent!.Value, cartridge); break; // Ammo shoots itself case AmmoComponent newAmmo: if (ent == null) break; CreateAndFireProjectiles(ent.Value, newAmmo); break; case HitscanPrototype hitscan: EntityUid? lastHit = null; List<(EntityCoordinates fromCoordinates, float distance, Angle mapDirection, EntityUid? hitEntity)> effects = []; var from = fromMap; // can't use map coords above because funny FireEffects var fromEffect = fromCoordinates; var dir = mapDirection.Normalized(); //in the situation when user == null, means that the cannon fires on its own (via signals). And we need the gun to not fire by itself in this case var lastUser = user ?? gunUid; if (hitscan.Reflective != ReflectType.None) { for (var reflectAttempt = 0; reflectAttempt < 3; reflectAttempt++) { var ray = new CollisionRay(from.Position, dir, hitscan.CollisionMask); var rayCastResults = Physics.IntersectRay(from.MapId, ray, hitscan.MaxLength, lastUser, false).ToList(); if (!rayCastResults.Any()) break; var result = rayCastResults[0]; // Check if laser is shot from in a container if (!_container.IsEntityOrParentInContainer(lastUser)) { // Checks if the laser should pass over unless targeted by its user foreach (var collide in rayCastResults) { if (!gun.Targets.Contains(collide.HitEntity) && // Sunrise-Edit CompOrNull(collide.HitEntity)?.Active == true) { continue; } result = collide; break; } } var hit = result.HitEntity; lastHit = hit; effects.Add((fromEffect, result.Distance, dir.Normalized().ToAngle(), hit)); var ev = new HitScanReflectAttemptEvent(user, gunUid, hitscan.Reflective, dir, false); RaiseLocalEvent(hit, ref ev); if (!ev.Reflected) break; fromEffect = Transform(hit).Coordinates; from = TransformSystem.ToMapCoordinates(fromEffect); dir = ev.Direction; lastUser = hit; } } if (lastHit != null) { var hitEntity = lastHit.Value; if (hitscan.StaminaDamage > 0f) _stamina.TakeStaminaDamage(hitEntity, hitscan.StaminaDamage, source: user); if (TryComp(hitEntity, out var status)) { _stunSystem.TryAddParalyzeDuration(hitEntity, TimeSpan.FromSeconds(hitscan.StunAmount)); _stunSystem.TryKnockdown(hitEntity, TimeSpan.FromSeconds(hitscan.KnockdownAmount), true); } if (hitscan.Ignite) { if (TryComp(hitEntity, out var flammable)) _flammableSystem.SetFireStacks(hitEntity, flammable.FireStacks + (flammable.MinIgnitionTemperature / hitscan.Temperature), flammable, true); if (Transform(hitEntity) is TransformComponent xform && xform.GridUid is { } hitGridUid) { var position = _transform.GetGridOrMapTilePosition(hitEntity, xform); _atmosphere.HotspotExpose(hitGridUid, position, hitscan.Temperature, 50, user, true); } } if (hitscan.Emp != null) _emp.EmpPulse(_transform.GetMapCoordinates(hitEntity), hitscan.Emp.Range, hitscan.Emp.EnergyConsumption, hitscan.Emp.DisableDuration); var dmg = hitscan.Damage; var hitName = ToPrettyString(hitEntity); if (dmg != null) dmg = Damageable.TryChangeDamage(hitEntity, dmg, origin: user); // check null again, as TryChangeDamage returns modified damage values if (dmg != null) { if (!Deleted(hitEntity)) { if (dmg.AnyPositive()) { _color.RaiseEffect(Color.Red, new List() { hitEntity }, Filter.Pvs(hitEntity, entityManager: EntityManager)); } // TODO get fallback position for playing hit sound. PlayImpactSound(hitEntity, dmg, hitscan.Sound, hitscan.ForceSound); } if (user != null) { Logs.Add(LogType.HitScanHit, $"{ToPrettyString(user.Value):user} hit {hitName:target} using hitscan and dealt {dmg.GetTotal():damage} damage"); } else { Logs.Add(LogType.HitScanHit, $"{hitName:target} hit by hitscan dealing {dmg.GetTotal():damage} damage"); } } } else { effects.Add((fromEffect, hitscan.MaxLength, dir.ToAngle(), null)); } FireEffects([effects], hitscan); Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user); break; default: throw new ArgumentOutOfRangeException(); } } RaiseLocalEvent(gunUid, new AmmoShotEvent() { FiredProjectiles = shotProjectiles, Shooter = user, //starlight }); void CreateAndFireProjectiles(EntityUid ammoEnt, AmmoComponent ammoComp) { if (TryComp(ammoEnt, out var ammoSpreadComp)) { var spreadEvent = new GunGetAmmoSpreadEvent(ammoSpreadComp.Spread); RaiseLocalEvent(gunUid, ref spreadEvent); var angles = LinearSpread(mapAngle - spreadEvent.Spread / 2, mapAngle + spreadEvent.Spread / 2, ammoSpreadComp.Count); ShootOrThrow(ammoEnt, angles[0].ToVec(), gunVelocity, gun, gunUid, user); shotProjectiles.Add(ammoEnt); for (var i = 1; i < ammoSpreadComp.Count; i++) { var newuid = Spawn(ammoSpreadComp.Proto, fromEnt); ShootOrThrow(newuid, angles[i].ToVec(), gunVelocity, gun, gunUid, user); shotProjectiles.Add(newuid); } } else { ShootOrThrow(ammoEnt, mapDirection, gunVelocity, gun, gunUid, user); shotProjectiles.Add(ammoEnt); } MuzzleFlash(gunUid, ammoComp, mapDirection.ToAngle(), user); Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user); } //🌟Starlight🌟 // This is fucked already, we need to just comment out the entire Wizden shooting system, take over full maintenance, and rewrite it from scratch. List<(EntityCoordinates, float, Angle, EntityUid?)> Hitscan ( EntityUid gunUid, GunComponent gun, EntityCoordinates fromCoordinates, EntityUid? user, MapCoordinates fromMap, float pointer, Vector2 mapDirection, HitscanPrototype hitscan, HashSet hitHashSet ) { EntityUid? lastHit = null; List<(EntityCoordinates fromCoordinates, float distance, Angle mapDirection, EntityUid? hitEntity)> effects = []; var from = fromMap; // can't use map coords above because funny FireEffects var fromEffect = fromCoordinates; var dir = mapDirection.Normalized(); //in the situation when user == null, means that the cannon fires on its own (via signals). And we need the gun to not fire by itself in this case var lastUser = user ?? gunUid; hitHashSet.Clear(); hitHashSet.Add(lastUser); if (hitscan.Reflective != ReflectType.None) { for (var reflectAttempt = 0; reflectAttempt < hitscan.Steps; reflectAttempt++) { var ray = new CollisionRay(from.Position, dir, hitscan.CollisionMask); var rayCastResults = Physics.IntersectRay(from.MapId, ray, hitscan.MaxLength, lastUser, false).ToList(); if (rayCastResults.Count == 0) break; var result = rayCastResults[0]; // Check if laser is shot from in a container if (!_container.IsEntityOrParentInContainer(lastUser)) { // Checks if the laser should pass over unless targeted by its user foreach (var collide in rayCastResults) { if (!gun.Targets.Contains(collide.HitEntity) && // Sunrise-Edit CompOrNull(collide.HitEntity)?.Active == true) continue; if (collide.Distance < pointer - 2f && HasComp(collide.HitEntity)) { if (pointer - collide.Distance > 4f) continue; var chance = Math.Clamp(1f - ((collide.Distance - 2f) / 2f), 0f, 1f); if (!_rand.Prob(chance)) continue; } if (!hitHashSet.Contains(collide.HitEntity)) hitHashSet.Add(collide.HitEntity); else continue; result = collide; break; } } var hit = result.HitEntity; lastHit = hit; effects.Add((fromEffect, result.Distance, dir.Normalized().ToAngle(), hit)); if (hitscan.Reflective != ReflectType.None) { var ev = new HitScanReflectAttemptEvent(user, gunUid, hitscan.Reflective, dir, false); RaiseLocalEvent(hit, ref ev); if (ev.Reflected) { hitHashSet.Clear(); hitHashSet.Add(hit); fromEffect = Transform(hit).Coordinates; from = TransformSystem.ToMapCoordinates(fromEffect); dir = ev.Direction; lastUser = hit; continue; } } if (hitscan.RicochetChance > 0f) { var ev = new HitScanRicochetAttemptEvent(hitscan.RicochetChance, from.Position, dir, false); RaiseLocalEvent(hit, ref ev); if (ev.Ricocheted) { hitHashSet.Clear(); hitHashSet.Add(hit); fromEffect = _transform.ToCoordinates(result.HitEntity, new MapCoordinates(result.HitPos, fromMap.MapId)); from = TransformSystem.ToMapCoordinates(fromEffect); dir = ev.Dir; lastUser = hit; continue; } } Hit(user, hitscan, lastHit.Value); if (hitscan.PierceChance >= 1f || (hitscan.PierceChance > 0f && _rand.Prob(hitscan.PierceChance))) { var ev = new HitScanPierceAttemptEvent(hitscan.PierceLevel, true); RaiseLocalEvent(hit, ref ev); if (ev.Pierced) { var random = Random.NextFloat(-hitscan.Derivation, hitscan.Derivation); fromEffect = _transform.ToCoordinates(result.HitEntity, new MapCoordinates(result.HitPos, fromMap.MapId)); from = TransformSystem.ToMapCoordinates(fromEffect); dir = (dir.ToAngle() + random).ToVec(); lastUser = hit; continue; } } break; } if (lastHit == null) effects.Add((fromEffect, hitscan.MaxLength, dir.ToAngle(), null)); } // Starlight confirm bullet sound should play bulletSoundCheck = true; return effects; void Hit(EntityUid? user, HitscanPrototype hitscan, EntityUid hitEntity) { if (Deleted(hitEntity)) return; if (hitscan.StaminaDamage > 0f) _stamina.TakeStaminaDamage(hitEntity, hitscan.StaminaDamage, source: user); var dmg = hitscan.Damage; var hitName = ToPrettyString(hitEntity); if (dmg != null) dmg = Damageable.TryChangeDamage( hitEntity, dmg, ignoreResistances: hitscan.IgnoreResistances, origin: user, armorPenetration: hitscan.ArmorPenetration, canHeal: false ); // check null again, as TryChangeDamage returns modified damage values if (dmg != null) { if (dmg.AnyPositive()) { _color.RaiseEffect(Color.Red, [hitEntity], Filter.Pvs(hitEntity, entityManager: EntityManager)); } // TODO get fallback position for playing hit sound. PlayImpactSound(hitEntity, dmg, hitscan.Sound, hitscan.ForceSound); if (user != null) { Logs.Add(LogType.HitScanHit, $"{ToPrettyString(user.Value):user} hit {hitName:target} using hitscan and dealt {dmg.GetTotal():damage} damage"); } else { Logs.Add(LogType.HitScanHit, $"{hitName:target} hit by hitscan dealing {dmg.GetTotal():damage} damage"); } } } } //starlight check to see if bullet sound should play if (bulletSoundCheck){ Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user); } } private void ShootOrThrow(EntityUid uid, Vector2 mapDirection, Vector2 gunVelocity, GunComponent gun, EntityUid gunUid, EntityUid? user) { if (gun.Targets.Count > 0 && !TerminatingOrDeleted(gun.Targets.First())) { var targeted = EnsureComp(uid); targeted.Targets = new(gun.Targets); Dirty(uid, targeted); } // Do a throw if (!HasComp(uid)) { RemoveShootable(uid); // TODO: Someone can probably yeet this a billion miles so need to pre-validate input somewhere up the call stack. ThrowingSystem.TryThrow(uid, mapDirection, gun.ProjectileSpeedModified, user); return; } ShootProjectile(uid, mapDirection, gunVelocity, gunUid, user, gun.ProjectileSpeedModified); } /// /// Gets a linear spread of angles between start and end. /// /// Start angle in degrees /// End angle in degrees /// How many shots there are private Angle[] LinearSpread(Angle start, Angle end, int intervals) { var angles = new Angle[intervals]; DebugTools.Assert(intervals > 1); for (var i = 0; i <= intervals - 1; i++) { angles[i] = new Angle(start + (end - start) * i / (intervals - 1)); } return angles; } // 🌟Starlight🌟 private Angle[] LinearSpreadWithRandom(Angle start, Angle end, int intervals, float randomSpread) { var angles = new Angle[intervals]; DebugTools.Assert(intervals > 1); for (var i = 0; i < intervals; i++) { var t = (float)i / (intervals - 1); var baseAngle = start + (end - start) * t; var randomFactor = _rand.NextFloat() - 0.5f; var randomOffset = Angle.FromDegrees(randomFactor * randomSpread); angles[i] = baseAngle + randomOffset; } return angles; } private Angle GetRecoilAngle(TimeSpan curTime, GunComponent component, Angle direction) { var timeSinceLastFire = (curTime - component.LastFire).TotalSeconds; var newTheta = MathHelper.Clamp(component.CurrentAngle.Theta + component.AngleIncreaseModified.Theta - component.AngleDecayModified.Theta * timeSinceLastFire, component.MinAngleModified.Theta, component.MaxAngleModified.Theta); component.CurrentAngle = new Angle(newTheta); component.LastFire = component.NextFire; // Convert it so angle can go either side. var random = Random.NextFloat(-0.5f, 0.5f); var spread = component.CurrentAngle.Theta * random; var angle = new Angle(direction.Theta + component.CurrentAngle.Theta * random); DebugTools.Assert(spread <= component.MaxAngleModified.Theta); return angle; } protected override void Popup(string message, EntityUid? uid, EntityUid? user) { } protected override void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, EntityUid? user = null) { var filter = Filter.Pvs(gunUid, entityManager: EntityManager); if (TryComp(user, out var actor)) filter.RemovePlayer(actor.PlayerSession); RaiseNetworkEvent(message, filter); } public void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound) { DebugTools.Assert(!Deleted(otherEntity), "Impact sound entity was deleted"); // Like projectiles and melee, // 1. Entity specific sound // 2. Ammo's sound // 3. Nothing var playedSound = false; if (!forceWeaponSound && modifiedDamage != null && modifiedDamage.GetTotal() > 0 && TryComp(otherEntity, out var rangedSound)) { var type = SharedMeleeWeaponSystem.GetHighestDamageSound(modifiedDamage, ProtoManager); if (type != null && rangedSound.SoundTypes?.TryGetValue(type, out var damageSoundType) == true) { Audio.PlayPvs(damageSoundType, otherEntity, AudioParams.Default.WithVariation(DamagePitchVariation)); playedSound = true; } else if (type != null && rangedSound.SoundGroups?.TryGetValue(type, out var damageSoundGroup) == true) { Audio.PlayPvs(damageSoundGroup, otherEntity, AudioParams.Default.WithVariation(DamagePitchVariation)); playedSound = true; } } if (!playedSound && weaponSound != null) { Audio.PlayPvs(weaponSound, otherEntity); } } // TODO: Pseudo RNG so the client can predict these. #region Hitscan effects // 🌟Starlight🌟 private void FireEffects(List> hits, HitscanPrototype hitscan) { if (hits.Count == 0) return; var hitscanEvent = new HitscanEvent { Hitscan = hitscan.ID, Effects = new Effect[hits.Count][] }; var spreadIndex = -1; HashSet pvs = []; foreach (var hit in hits) { spreadIndex++; var index = -1; hitscanEvent.Effects[spreadIndex] = new Effect[hit.Count]; ref var effects = ref hitscanEvent.Effects[spreadIndex]; foreach (var item in hit) { var (fromCoordinates, distance, angle, hitEntity) = item; var fromXform = Transform(fromCoordinates.EntityId); var gridUid = fromXform.GridUid; if (gridUid != fromCoordinates.EntityId && TryComp(gridUid, out TransformComponent? gridXform)) { var (_, gridRot, gridInvMatrix) = TransformSystem.GetWorldPositionRotationInvMatrix(gridXform); var map = _transform.ToMapCoordinates(fromCoordinates); fromCoordinates = new EntityCoordinates(gridUid.Value, Vector2.Transform(map.Position, gridInvMatrix)); angle -= gridRot; } else { angle -= _transform.GetWorldRotation(fromXform); } index++; effects[index] = new Effect { Angle = angle, Distance = distance, }; ref var effect = ref effects[index]; if (distance >= 1f) { var muzzleCoords = fromCoordinates.Offset(angle.ToVec().Normalized() / 2); var travelCoords = fromCoordinates.Offset(angle.ToVec() * (distance + 0.5f) / 2); effect.MuzzleCoordinates = GetNetCoordinates(muzzleCoords); effect.TravelCoordinates = GetNetCoordinates(travelCoords); } var impactCoords = fromCoordinates.Offset(angle.ToVec() * distance); effect.ImpactCoordinates = GetNetCoordinates(impactCoords); if (hitEntity is not null) { if (hitscan.Reflective == ReflectType.NonEnergy) { if (TryComp(hitEntity, out var bloodstream)) { Timer.Spawn(200, () => { var color = _proto.Index(bloodstream.BloodReagent).SubstanceColor; // A flash of the neuralyzer, then a man in a black suit says that you didn’t see any “vector crutch” here, and if you did—read it again. var coords = fromCoordinates.Offset((angle.ToVec() * (distance + 1.3f)) + new Vector2(-0.5f, -0.5f)); _decals.TryAddDecal(_rand.Pick(_bloodDecals), coords, out _, color, angle + Angle.FromDegrees(-45), cleanable: true); }); } else { effect.ImpactEnt = GetNetEntity(hitEntity.Value); } } } pvs.Add(fromCoordinates); } } if (pvs.Count > 0) { var filter = Filter.Empty(); foreach (var pos in pvs.Where(x => x.IsValid(EntityManager))) filter.Merge(Filter.Pvs(pos, entityMan: EntityManager)); RaiseNetworkEvent(hitscanEvent, filter); } } #endregion }