fish-station/Content.Server/Weapons/Ranged/Systems/GunSystem.cs
2025-10-27 02:35:00 +01:00

818 lines
35 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.Atmos.Components;
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<BallisticAmmoProviderComponent, PriceCalculationEvent>(OnBallisticPrice);
CacheDecals();
}
private void CacheDecals() // 🌟Starlight🌟
{
_bloodDecals = _proto.EnumeratePrototypes<DecalPrototype>().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<EntityPrototype>(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<EntityUid>(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<EntityUid>();
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<List<(EntityCoordinates, float, Angle, EntityUid?)>> 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<RequireProjectileTargetComponent>(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<StatusEffectsComponent>(hitEntity, out var status))
{
_stunSystem.TryAddParalyzeDuration(hitEntity, TimeSpan.FromSeconds(hitscan.StunAmount));
_stunSystem.TryKnockdown(hitEntity, TimeSpan.FromSeconds(hitscan.KnockdownAmount), true);
}
if (hitscan.Ignite)
{
if (TryComp<FlammableComponent>(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, TimeSpan.FromSeconds(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<EntityUid>() { 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<ProjectileSpreadComponent>(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<EntityUid> 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<RequireProjectileTargetComponent>(collide.HitEntity)?.Active == true)
continue;
if (collide.Distance < pointer - 2f && HasComp<MobMoverComponent>(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<TargetedProjectileComponent>(uid);
targeted.Targets = new(gun.Targets);
Dirty(uid, targeted);
}
// Do a throw
if (!HasComp<ProjectileComponent>(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);
}
/// <summary>
/// Gets a linear spread of angles between start and end.
/// </summary>
/// <param name="start">Start angle in degrees</param>
/// <param name="end">End angle in degrees</param>
/// <param name="intervals">How many shots there are</param>
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<ActorComponent>(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<RangedDamageSoundComponent>(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<List<(EntityCoordinates fromCoordinates, float distance, Angle angle, EntityUid? hitEntity)>> hits, HitscanPrototype hitscan)
{
if (hits.Count == 0) return;
var hitscanEvent = new HitscanEvent
{
Hitscan = hitscan.ID,
Effects = new Effect[hits.Count][]
};
var spreadIndex = -1;
HashSet<EntityCoordinates> 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<BloodstreamComponent>(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 didnt 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
}