Files
OldThink/Content.Server/Weapons/Ranged/Systems/GunSystem.cs
Remuchi d2d501dbc0 Merge remote-tracking branch 'upstream/master' into upstream
# Conflicts:
#	Content.Client/Access/AccessOverlay.cs
#	Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs
#	Content.Client/Access/UI/IdCardConsoleWindow.xaml
#	Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs
#	Content.Client/Chemistry/UI/InjectorStatusControl.cs
#	Content.Client/StatusIcon/StatusIconOverlay.cs
#	Content.Client/Stylesheets/StyleNano.cs
#	Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
#	Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml
#	Content.Server/Access/Systems/IdCardConsoleSystem.cs
#	Content.Server/Administration/Commands/AGhost.cs
#	Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs
#	Content.Server/Connection/ConnectionManager.cs
#	Content.Server/DeviceLinking/Systems/SignalTimerSystem.cs
#	Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs
#	Content.Server/GameTicking/GameTicker.RoundFlow.cs
#	Content.Server/GameTicking/GameTicker.Spawning.cs
#	Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs
#	Content.Server/Resist/EscapeInventorySystem.cs
#	Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs
#	Content.Shared/Access/Components/IdCardConsoleComponent.cs
#	Content.Shared/Anomaly/SharedAnomalySystem.cs
#	Content.Shared/Bed/Sleep/SharedSleepingSystem.cs
#	Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs
#	Content.Shared/Lock/LockSystem.cs
#	Content.Shared/RCD/Systems/RCDSystem.cs
#	Content.Shared/Roles/JobPrototype.cs
#	Content.Shared/StatusIcon/StatusIconPrototype.cs
#	Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
#	Resources/Audio/Machines/attributions.yml
#	Resources/Locale/en-US/rcd/components/rcd-component.ftl
#	Resources/Maps/reach.yml
#	Resources/Prototypes/Catalog/Cargo/cargo_vending.yml
#	Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
#	Resources/Prototypes/Catalog/Fills/Lockers/security.yml
#	Resources/Prototypes/Catalog/ReagentDispensers/beverage.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/boozeomat.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/cola.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/lawdrobe.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/pwrgame.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/shamblersjuice.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/soda.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/spaceup.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/starkist.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/theater.yml
#	Resources/Prototypes/DeviceLinking/sink_ports.yml
#	Resources/Prototypes/Entities/Clothing/Back/duffel.yml
#	Resources/Prototypes/Entities/Clothing/Belt/base_clothingbelt.yml
#	Resources/Prototypes/Entities/Clothing/Neck/misc.yml
#	Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml
#	Resources/Prototypes/Entities/Clothing/OuterClothing/wintercoats.yml
#	Resources/Prototypes/Entities/Mobs/Customization/Markings/gauze.yml
#	Resources/Prototypes/Entities/Objects/Devices/Electronics/door.yml
#	Resources/Prototypes/Entities/Objects/Magic/books.yml
#	Resources/Prototypes/Entities/Objects/Materials/Sheets/glass.yml
#	Resources/Prototypes/Entities/Objects/Materials/Sheets/metal.yml
#	Resources/Prototypes/Entities/Objects/Materials/Sheets/other.yml
#	Resources/Prototypes/Entities/Objects/Misc/tiles.yml
#	Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml
#	Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml
#	Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_assembly.yml
#	Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml
#	Resources/Prototypes/Entities/Structures/Doors/Airlocks/highsec.yml
#	Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml
#	Resources/Prototypes/Entities/Structures/Doors/Firelocks/frame.yml
#	Resources/Prototypes/Entities/Structures/Doors/MaterialDoors/material_doors.yml
#	Resources/Prototypes/Entities/Structures/Doors/SecretDoor/secret_door.yml
#	Resources/Prototypes/Entities/Structures/Doors/Windoors/assembly.yml
#	Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml
#	Resources/Prototypes/Entities/Structures/Machines/lathe.yml
#	Resources/Prototypes/Entities/Structures/Power/cable_terminal.yml
#	Resources/Prototypes/Entities/Structures/Storage/Tanks/base_structuretanks.yml
#	Resources/Prototypes/Entities/Structures/Walls/grille.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/structures/shutter.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/improvised/flowercrown.yml
#	Resources/Prototypes/Recipes/Crafting/improvised.yml
#	Resources/Prototypes/Roles/Jobs/Security/detective.yml
#	Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml
#	Resources/Prototypes/Roles/Jobs/Security/security_officer.yml
#	Resources/Prototypes/Roles/Jobs/Security/warden.yml
#	Resources/Prototypes/StatusEffects/health.yml
#	Resources/Prototypes/Voice/speech_emotes.yml
#	Resources/Prototypes/lobbyscreens.yml
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/ERTSuits/ertchaplain.rsi/equipped-OUTERCLOTHING-body-slim.png
#	Resources/Textures/Decals/bricktile.rsi/white_box.png
#	Resources/Textures/Objects/Misc/books.rsi/meta.json
#	Resources/migration.yml
2024-04-13 11:29:33 +07:00

621 lines
26 KiB
C#

using System.Linq;
using System.Numerics;
using Content.Server.Cargo.Systems;
using Content.Server.Interaction;
using Content.Server.Power.EntitySystems;
using Content.Server.Projectiles;
using Content.Server.Stunnable;
using Content.Server.Weapons.Ranged.Components;
using Content.Server._White.Crossbow;
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.Projectiles;
using Content.Shared.Throwing;
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 Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Weapons.Ranged.Systems;
public sealed partial class GunSystem : SharedGunSystem
{
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly DamageExamineSystem _damageExamine = default!;
[Dependency] private readonly InteractionSystem _interaction = default!;
[Dependency] private readonly PricingSystem _pricing = default!;
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
[Dependency] private readonly ProjectileSystem _projectile = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly StaminaSystem _stamina = default!;
[Dependency] private readonly StunSystem _stun = default!;
[Dependency] private readonly PoweredSystem _powered = default!; // WD
private const float DamagePitchVariation = 0.05f;
public const float GunClumsyChance = 0.5f;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BallisticAmmoProviderComponent, PriceCalculationEvent>(OnBallisticPrice);
}
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;
}
protected override bool ShootDirect(EntityUid gunUid, GunComponent gun, EntityUid target, List<(EntityUid? Entity, IShootable Shootable)> ammo, EntityUid user)
{
var result = false;
// TODO: This is dogshit. I just want to get executions slightly better.
// Ideally you'd pull out cartridge + ammo to separate handling functions and re-use it here, then hitscan you need to bypass entirely.
// You should also make shooting into a struct of args given how many there are now.
var fromCoordinates = Transform(gunUid).Coordinates;
var toCoordinates = Transform(target).Coordinates;
var fromMap = fromCoordinates.ToMap(EntityManager, TransformSystem);
var toMap = toCoordinates.ToMapPos(EntityManager, TransformSystem);
var mapDirection = toMap - fromMap.Position;
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 _)
? fromCoordinates.WithEntityId(gridUid, EntityManager)
: new EntityCoordinates(MapManager.GetMapEntityId(fromMap.MapId), fromMap.Position);
// I must be high because this was getting tripped even when true.
// DebugTools.Assert(direction != Vector2.Zero);
var shotProjectiles = new List<EntityUid>(ammo.Count);
var cartridgeBullets = new List<EntityUid>();
foreach (var (ent, shootable) in ammo)
{
switch (shootable)
{
// Cartridge shoots something else
case CartridgeAmmoComponent cartridge:
if (!cartridge.Spent)
{
for (var i = 0; i < cartridge.Count; i++)
{
var uid = Spawn(cartridge.Prototype, fromEnt);
cartridgeBullets.Add(uid);
}
RaiseLocalEvent(ent!.Value, new AmmoShotEvent()
{
FiredProjectiles = cartridgeBullets,
});
shotProjectiles.AddRange(cartridgeBullets);
cartridgeBullets.Clear();
SetCartridgeSpent(ent.Value, cartridge, true);
MuzzleFlash(gunUid, cartridge, user);
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
if (cartridge.DeleteOnSpawn)
Del(ent.Value);
}
else
{
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))
EjectCartridge(ent.Value, angle);
result = true;
Dirty(ent!.Value, cartridge);
break;
// Ammo shoots itself
case AmmoComponent newAmmo:
result = true;
shotProjectiles.Add(ent!.Value);
MuzzleFlash(gunUid, newAmmo, user);
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
break;
case HitscanPrototype hitscan:
result = true;
var hitEntity = target;
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, origin: user);
// check null again, as TryChangeDamage returns modified damage values
if (dmg != null)
{
if (!Deleted(hitEntity))
{
if (dmg.Any())
{
_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);
}
Logs.Add(LogType.HitScanHit,
$"{ToPrettyString(user):user} hit {hitName:target} using hitscan and dealt {dmg.GetTotal():damage} damage");
}
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
foreach (var ammoUid in shotProjectiles)
{
// TODO: Handle this shit
if (!TryComp(ammoUid, out ProjectileComponent? projectileComponent))
{
QueueDel(ammoUid);
continue;
}
_projectile.TryHandleProjectile(target, (ammoUid, projectileComponent));
// Even this deletion handling is mega sussy.
Del(ammoUid);
}
RaiseLocalEvent(gunUid, new AmmoShotEvent()
{
FiredProjectiles = shotProjectiles,
});
return result;
}
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;
// Try a clumsy roll
// TODO: Who put this here
if (TryComp<ClumsyComponent>(user, out var clumsy) && !gun.ClumsyProof)
{
for (var i = 0; i < ammo.Count; i++)
{
if (_interaction.TryRollClumsy(user.Value, GunClumsyChance, clumsy))
{
// Wound them
Damageable.TryChangeDamage(user, clumsy.ClumsyDamage, origin: user);
_stun.TryParalyze(user.Value, TimeSpan.FromSeconds(3f), true);
// Apply salt to the wound ("Honk!")
Audio.PlayPvs(new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/bang.ogg"), gunUid);
Audio.PlayPvs(clumsy.ClumsySound, gunUid);
PopupSystem.PopupEntity(Loc.GetString("gun-clumsy"), user.Value);
Logs.Add(LogType.EntityDelete, LogImpact.Medium, $"{Loc.GetString("clumsy-gun-delete-log", ("prettyUser", ToPrettyString(user.Value)), ("prettyGun", ToPrettyString(gunUid)))}");
Del(gunUid);
userImpulse = false;
return;
}
}
}
// As the above message wasn't obvious stop putting stuff here and use events
var fromMap = fromCoordinates.ToMap(EntityManager, TransformSystem);
var toMap = toCoordinates.ToMapPos(EntityManager, TransformSystem);
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 var grid)
? fromCoordinates.WithEntityId(gridUid, EntityManager)
: new EntityCoordinates(MapManager.GetMapEntityId(fromMap.MapId), fromMap.Position);
// Update shot based on the recoil
toMap = fromMap.Position + angle.ToVec() * mapDirection.Length();
mapDirection = toMap - fromMap.Position;
// WD EDIT START
// var gunVelocity = Physics.GetMapLinearVelocity(gunUid);
var gunVelocity = Vector2.Zero;
if (grid != null && TryComp(gridUid, out PhysicsComponent? physics))
gunVelocity = physics.LinearVelocity;
// WD EDIT END
// I must be high because this was getting tripped even when true.
// DebugTools.Assert(direction != Vector2.Zero);
var shotProjectiles = new List<EntityUid>(ammo.Count);
var cartridgeBullets = new List<EntityUid>();
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)
{
// Cartridge shoots something else
case CartridgeAmmoComponent cartridge:
if (!cartridge.Spent)
{
if (cartridge.Count > 1)
{
var ev = new GunGetAmmoSpreadEvent(cartridge.Spread);
RaiseLocalEvent(gunUid, ref ev);
var angles = LinearSpread(mapAngle - ev.Spread / 2,
mapAngle + ev.Spread / 2, cartridge.Count);
for (var i = 0; i < cartridge.Count; i++)
{
var uid = Spawn(cartridge.Prototype, fromEnt);
ShootOrThrow(uid, angles[i].ToVec(), gunVelocity, gun, gunUid, user);
cartridgeBullets.Add(uid);
}
}
else
{
var uid = Spawn(cartridge.Prototype, fromEnt);
ShootOrThrow(uid, mapDirection, gunVelocity, gun, gunUid, user);
cartridgeBullets.Add(uid);
}
RaiseLocalEvent(ent!.Value, new AmmoShotEvent()
{
FiredProjectiles = cartridgeBullets,
});
shotProjectiles.AddRange(cartridgeBullets);
cartridgeBullets.Clear();
SetCartridgeSpent(ent.Value, cartridge, true);
MuzzleFlash(gunUid, cartridge, user);
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
if (cartridge.DeleteOnSpawn)
Del(ent.Value);
}
else
{
userImpulse = false;
Audio.PlayPredicted(gun.SoundEmpty, gunUid, user);
break; // WD EDIT
}
// Something like ballistic might want to leave it in the container still
if (!cartridge.DeleteOnSpawn && !Containers.IsEntityInContainer(ent!.Value) &&
(!TryComp(gunUid, out BallisticAmmoProviderComponent? ballistic) || ballistic.AutoCycle)) // WD EDIT
EjectCartridge(ent.Value, angle);
Dirty(ent!.Value, cartridge);
break;
// Ammo shoots itself
case AmmoComponent newAmmo:
shotProjectiles.Add(ent!.Value);
MuzzleFlash(gunUid, newAmmo, user);
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
ShootOrThrow(ent.Value, mapDirection, gunVelocity, gun, gunUid, user);
break;
case HitscanPrototype hitscan:
EntityUid? lastHit = null;
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.Count == 0)
break;
RayCastResults? result = null;
foreach (var castResults in rayCastResults)
{
var hitscanAttemptEv = new HitscanHitAttemptEvent(castResults.HitEntity, gun.Target);
RaiseLocalEvent(castResults.HitEntity, ref hitscanAttemptEv);
if (hitscanAttemptEv.Cancelled)
{
continue;
}
result = castResults;
break;
}
if (!result.HasValue)
{
return;
}
var hit = result.Value.HitEntity;
lastHit = hit;
FireEffects(fromEffect, result.Value.Distance, dir.Normalized().ToAngle(), hitscan, hit);
var ev = new HitScanReflectAttemptEvent(user, gunUid, hitscan.Reflective, dir, false);
RaiseLocalEvent(hit, ref ev);
if (!ev.Reflected)
break;
fromEffect = Transform(hit).Coordinates;
from = fromEffect.ToMap(EntityManager, _transform);
dir = ev.Direction;
lastUser = hit;
}
}
if (lastHit != null)
{
var hitEntity = lastHit.Value;
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, origin: user);
// check null again, as TryChangeDamage returns modified damage values
if (dmg != null)
{
if (!Deleted(hitEntity))
{
if (dmg.Any())
{
_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
{
FireEffects(fromEffect, hitscan.MaxLength, dir.ToAngle(), hitscan);
}
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
RaiseLocalEvent(gunUid, new AmmoShotEvent()
{
FiredProjectiles = shotProjectiles,
});
}
private void ShootOrThrow(EntityUid uid, Vector2 mapDirection, Vector2 gunVelocity, GunComponent gun, EntityUid gunUid, EntityUid? user)
{
// Do a throw
if (TryComp(uid, out ProjectileComponent? projectile))
{
projectile.Target = gun.Target;
ShootProjectile(uid, mapDirection, gunVelocity, gunUid, user, gun.ProjectileSpeedModified);
return;
}
RemoveShootable(uid);
// TODO: Someone can probably yeet this a billion miles so need to pre-validate input somewhere up the call stack.
// WD EDIT START
var coefficient = _powered.GetPowerCoefficient(gunUid);
if (gun.ForceThrowingAngle)
{
var angle = EnsureComp<ThrowingAngleComponent>(uid);
angle.Angle = gun.Angle;
}
ThrowingSystem.TryThrow(uid, mapDirection.Normalized() * 7f * coefficient, gun.ProjectileSpeedModified,
user);
if (gun.ForceThrowingAngle)
RemComp<ThrowingAngleComponent>(uid);
// WD EDIT END
}
/// <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;
}
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 uid, MuzzleFlashEvent message, EntityUid? user = null)
{
var filter = Filter.Pvs(uid, 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
private void FireEffects(EntityCoordinates fromCoordinates, float distance, Angle mapDirection, HitscanPrototype hitscan, EntityUid? hitEntity = null)
{
// Lord
// Forgive me for the shitcode I am about to do
// Effects tempt me not
var sprites = new List<(NetCoordinates coordinates, Angle angle, SpriteSpecifier sprite, float scale)>();
var gridUid = fromCoordinates.GetGridUid(EntityManager);
var angle = mapDirection;
// 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 xformQuery = GetEntityQuery<TransformComponent>();
if (xformQuery.TryGetComponent(gridUid, out var gridXform))
{
var (_, gridRot, gridInvMatrix) = TransformSystem.GetWorldPositionRotationInvMatrix(gridXform, xformQuery);
fromCoordinates = new EntityCoordinates(gridUid.Value,
gridInvMatrix.Transform(fromCoordinates.ToMapPos(EntityManager, TransformSystem)));
// Use the fallback angle I guess?
angle -= gridRot;
}
if (distance >= 1f)
{
if (hitscan.MuzzleFlash != null)
{
var coords = fromCoordinates.Offset(angle.ToVec().Normalized() / 2);
var netCoords = GetNetCoordinates(coords);
sprites.Add((netCoords, angle, hitscan.MuzzleFlash, 1f));
}
if (hitscan.TravelFlash != null)
{
var coords = fromCoordinates.Offset(angle.ToVec() * (distance + 0.5f) / 2);
var netCoords = GetNetCoordinates(coords);
sprites.Add((netCoords, angle, hitscan.TravelFlash, distance - 1.5f));
}
}
if (hitscan.ImpactFlash != null)
{
var coords = fromCoordinates.Offset(angle.ToVec() * distance);
var netCoords = GetNetCoordinates(coords);
sprites.Add((netCoords, angle.FlipPositive(), hitscan.ImpactFlash, 1f));
}
if (sprites.Count > 0)
{
RaiseNetworkEvent(new HitscanEvent
{
Sprites = sprites,
}, Filter.Pvs(fromCoordinates, entityMan: EntityManager));
}
}
#endregion
}