| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 |
- using System.Numerics;
- using Content.Client.Animations;
- using Content.Client.Gameplay;
- using Content.Client.Items;
- using Content.Client.Weapons.Ranged.Components;
- using Content.Shared.Camera;
- using Content.Shared.CombatMode;
- using Content.Shared.Weapons.Ranged;
- using Content.Shared.Weapons.Ranged.Components;
- using Content.Shared.Weapons.Ranged.Events;
- using Content.Shared.Weapons.Ranged.Systems;
- using Robust.Client.Animations;
- using Robust.Client.GameObjects;
- using Robust.Client.Graphics;
- using Robust.Client.Input;
- using Robust.Client.Player;
- using Robust.Client.State;
- using Robust.Shared.Animations;
- using Robust.Shared.Input;
- using Robust.Shared.Map;
- using Robust.Shared.Map.Components;
- using Robust.Shared.Prototypes;
- using Robust.Shared.Utility;
- using SharedGunSystem = Content.Shared.Weapons.Ranged.Systems.SharedGunSystem;
- using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent;
- namespace Content.Client.Weapons.Ranged.Systems;
- public sealed partial class GunSystem : SharedGunSystem
- {
- [Dependency] private readonly IComponentFactory _factory = default!;
- [Dependency] private readonly IEyeManager _eyeManager = default!;
- [Dependency] private readonly IInputManager _inputManager = default!;
- [Dependency] private readonly IPlayerManager _player = default!;
- [Dependency] private readonly IStateManager _state = default!;
- [Dependency] private readonly AnimationPlayerSystem _animPlayer = default!;
- [Dependency] private readonly InputSystem _inputSystem = default!;
- [Dependency] private readonly SharedCameraRecoilSystem _recoil = default!;
- [Dependency] private readonly SharedMapSystem _maps = default!;
- [Dependency] private readonly SharedTransformSystem _xform = default!;
- [ValidatePrototypeId<EntityPrototype>]
- public const string HitscanProto = "HitscanEffect";
- public bool SpreadOverlay
- {
- get => _spreadOverlay;
- set
- {
- if (_spreadOverlay == value)
- return;
- _spreadOverlay = value;
- var overlayManager = IoCManager.Resolve<IOverlayManager>();
- if (_spreadOverlay)
- {
- overlayManager.AddOverlay(new GunSpreadOverlay(
- EntityManager,
- _eyeManager,
- Timing,
- _inputManager,
- _player,
- this,
- TransformSystem));
- }
- else
- {
- overlayManager.RemoveOverlay<GunSpreadOverlay>();
- }
- }
- }
- private bool _spreadOverlay;
- public override void Initialize()
- {
- base.Initialize();
- UpdatesOutsidePrediction = true;
- SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect);
- SubscribeLocalEvent<AmmoCounterComponent, UpdateClientAmmoEvent>(OnUpdateClientAmmo);
- SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash);
- // Plays animated effects on the client.
- SubscribeNetworkEvent<HitscanEvent>(OnHitscan);
- InitializeMagazineVisuals();
- InitializeSpentAmmo();
- }
- private void OnUpdateClientAmmo(EntityUid uid, AmmoCounterComponent ammoComp, ref UpdateClientAmmoEvent args)
- {
- UpdateAmmoCount(uid, ammoComp);
- }
- private void OnMuzzleFlash(MuzzleFlashEvent args)
- {
- var gunUid = GetEntity(args.Uid);
- CreateEffect(gunUid, args, gunUid);
- }
- private void OnHitscan(HitscanEvent ev)
- {
- // ALL I WANT IS AN ANIMATED EFFECT
- // TODO EFFECTS
- // This is very jank
- // because the effect consists of three unrelatd entities, the hitscan beam can be split appart.
- // E.g., if a grid rotates while part of the beam is parented to the grid, and part of it is parented to the map.
- // Ideally, there should only be one entity, with one sprite that has multiple layers
- // Or at the very least, have the other entities parented to the same entity to make sure they stick together.
- foreach (var a in ev.Sprites)
- {
- if (a.Sprite is not SpriteSpecifier.Rsi rsi)
- continue;
- var coords = GetCoordinates(a.coordinates);
- if (!TryComp(coords.EntityId, out TransformComponent? relativeXform))
- continue;
- var ent = Spawn(HitscanProto, coords);
- var sprite = Comp<SpriteComponent>(ent);
- var xform = Transform(ent);
- var targetWorldRot = a.angle + _xform.GetWorldRotation(relativeXform);
- var delta = targetWorldRot - _xform.GetWorldRotation(xform);
- _xform.SetLocalRotationNoLerp(ent, xform.LocalRotation + delta, xform);
- sprite[EffectLayers.Unshaded].AutoAnimated = false;
- sprite.LayerSetSprite(EffectLayers.Unshaded, rsi);
- sprite.LayerSetState(EffectLayers.Unshaded, rsi.RsiState);
- sprite.Scale = new Vector2(a.Distance, 1f);
- sprite[EffectLayers.Unshaded].Visible = true;
- var anim = new Animation()
- {
- Length = TimeSpan.FromSeconds(0.48f),
- AnimationTracks =
- {
- new AnimationTrackSpriteFlick()
- {
- LayerKey = EffectLayers.Unshaded,
- KeyFrames =
- {
- new AnimationTrackSpriteFlick.KeyFrame(rsi.RsiState, 0f),
- }
- }
- }
- };
- _animPlayer.Play(ent, anim, "hitscan-effect");
- }
- }
- public override void Update(float frameTime)
- {
- if (!Timing.IsFirstTimePredicted)
- return;
- var entityNull = _player.LocalEntity;
- if (entityNull == null || !TryComp<CombatModeComponent>(entityNull, out var combat) || !combat.IsInCombatMode)
- {
- return;
- }
- var entity = entityNull.Value;
- if (!TryGetGun(entity, out var gunUid, out var gun))
- {
- return;
- }
- var useKey = gun.UseKey ? EngineKeyFunctions.Use : EngineKeyFunctions.UseSecondary;
- if (_inputSystem.CmdStates.GetState(useKey) != BoundKeyState.Down && !gun.BurstActivated)
- {
- if (gun.ShotCounter != 0)
- EntityManager.RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) });
- return;
- }
- if (gun.NextFire > Timing.CurTime)
- return;
- var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition);
- if (mousePos.MapId == MapId.Nullspace)
- {
- if (gun.ShotCounter != 0)
- EntityManager.RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) });
- return;
- }
- // Define target coordinates relative to gun entity, so that network latency on moving grids doesn't fuck up the target location.
- var coordinates = TransformSystem.ToCoordinates(entity, mousePos);
- NetEntity? target = null;
- if (_state.CurrentState is GameplayStateBase screen)
- target = GetNetEntity(screen.GetClickedEntity(mousePos));
- Log.Debug($"Sending shoot request tick {Timing.CurTick} / {Timing.CurTime}");
- EntityManager.RaisePredictiveEvent(new RequestShootEvent
- {
- Target = target,
- Coordinates = GetNetCoordinates(coordinates),
- Gun = GetNetEntity(gunUid),
- });
- }
- 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;
- // Rather than splitting client / server for every ammo provider it's easier
- // to just delete the spawned entities. This is for programmer sanity despite the wasted perf.
- // This also means any ammo specific stuff can be grabbed as necessary.
- var direction = TransformSystem.ToMapCoordinates(fromCoordinates).Position - TransformSystem.ToMapCoordinates(toCoordinates).Position;
- var worldAngle = direction.ToAngle().Opposite();
- foreach (var (ent, shootable) in ammo)
- {
- if (throwItems)
- {
- Recoil(user, direction, gun.CameraRecoilScalarModified);
- if (IsClientSide(ent!.Value))
- Del(ent.Value);
- else
- RemoveShootable(ent.Value);
- continue;
- }
- switch (shootable)
- {
- case CartridgeAmmoComponent cartridge:
- if (!cartridge.Spent)
- {
- SetCartridgeSpent(ent!.Value, cartridge, true);
- MuzzleFlash(gunUid, cartridge, worldAngle, user);
- Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
- Recoil(user, direction, gun.CameraRecoilScalarModified);
- // TODO: Can't predict entity deletions.
- //if (cartridge.DeleteOnSpawn)
- // Del(cartridge.Owner);
- }
- else
- {
- userImpulse = false;
- Audio.PlayPredicted(gun.SoundEmpty, gunUid, user);
- }
- if (IsClientSide(ent!.Value))
- Del(ent.Value);
- break;
- case AmmoComponent newAmmo:
- MuzzleFlash(gunUid, newAmmo, worldAngle, user);
- Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
- Recoil(user, direction, gun.CameraRecoilScalarModified);
- if (IsClientSide(ent!.Value))
- Del(ent.Value);
- else
- RemoveShootable(ent.Value);
- break;
- case HitscanPrototype:
- Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
- Recoil(user, direction, gun.CameraRecoilScalarModified);
- break;
- }
- }
- }
- private void Recoil(EntityUid? user, Vector2 recoil, float recoilScalar)
- {
- if (!Timing.IsFirstTimePredicted || user == null || recoil == Vector2.Zero || recoilScalar == 0)
- return;
- _recoil.KickCamera(user.Value, recoil.Normalized() * 0.5f * recoilScalar);
- }
- protected override void Popup(string message, EntityUid? uid, EntityUid? user)
- {
- if (uid == null || user == null || !Timing.IsFirstTimePredicted)
- return;
- PopupSystem.PopupEntity(message, uid.Value, user.Value);
- }
- protected override void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, EntityUid? tracked = null)
- {
- if (!Timing.IsFirstTimePredicted)
- return;
- // EntityUid check added to stop throwing exceptions due to https://github.com/space-wizards/space-station-14/issues/28252
- // TODO: Check to see why invalid entities are firing effects.
- if (gunUid == EntityUid.Invalid)
- {
- Log.Debug($"Invalid Entity sent MuzzleFlashEvent (proto: {message.Prototype}, gun: {ToPrettyString(gunUid)})");
- return;
- }
- var gunXform = Transform(gunUid);
- var gridUid = gunXform.GridUid;
- EntityCoordinates coordinates;
- if (TryComp(gridUid, out MapGridComponent? mapGrid))
- {
- coordinates = new EntityCoordinates(gridUid.Value, _maps.LocalToGrid(gridUid.Value, mapGrid, gunXform.Coordinates));
- }
- else if (gunXform.MapUid != null)
- {
- coordinates = new EntityCoordinates(gunXform.MapUid.Value, TransformSystem.GetWorldPosition(gunXform));
- }
- else
- {
- return;
- }
- var ent = Spawn(message.Prototype, coordinates);
- TransformSystem.SetWorldRotationNoLerp(ent, message.Angle);
- if (tracked != null)
- {
- var track = EnsureComp<TrackUserComponent>(ent);
- track.User = tracked;
- track.Offset = Vector2.UnitX / 2f;
- }
- var lifetime = 0.4f;
- if (TryComp<TimedDespawnComponent>(gunUid, out var despawn))
- {
- lifetime = despawn.Lifetime;
- }
- var anim = new Animation()
- {
- Length = TimeSpan.FromSeconds(lifetime),
- AnimationTracks =
- {
- new AnimationTrackComponentProperty
- {
- ComponentType = typeof(SpriteComponent),
- Property = nameof(SpriteComponent.Color),
- InterpolationMode = AnimationInterpolationMode.Linear,
- KeyFrames =
- {
- new AnimationTrackProperty.KeyFrame(Color.White.WithAlpha(1f), 0),
- new AnimationTrackProperty.KeyFrame(Color.White.WithAlpha(0f), lifetime)
- }
- }
- }
- };
- _animPlayer.Play(ent, anim, "muzzle-flash");
- if (!TryComp(gunUid, out PointLightComponent? light))
- {
- light = (PointLightComponent) _factory.GetComponent(typeof(PointLightComponent));
- light.NetSyncEnabled = false;
- AddComp(gunUid, light);
- }
- Lights.SetEnabled(gunUid, true, light);
- Lights.SetRadius(gunUid, 2f, light);
- Lights.SetColor(gunUid, Color.FromHex("#cc8e2b"), light);
- Lights.SetEnergy(gunUid, 5f, light);
- var animTwo = new Animation()
- {
- Length = TimeSpan.FromSeconds(lifetime),
- AnimationTracks =
- {
- new AnimationTrackComponentProperty
- {
- ComponentType = typeof(PointLightComponent),
- Property = nameof(PointLightComponent.Energy),
- InterpolationMode = AnimationInterpolationMode.Linear,
- KeyFrames =
- {
- new AnimationTrackProperty.KeyFrame(5f, 0),
- new AnimationTrackProperty.KeyFrame(0f, lifetime)
- }
- },
- new AnimationTrackComponentProperty
- {
- ComponentType = typeof(PointLightComponent),
- Property = nameof(PointLightComponent.AnimatedEnable),
- InterpolationMode = AnimationInterpolationMode.Linear,
- KeyFrames =
- {
- new AnimationTrackProperty.KeyFrame(true, 0),
- new AnimationTrackProperty.KeyFrame(false, lifetime)
- }
- }
- }
- };
- var uidPlayer = EnsureComp<AnimationPlayerComponent>(gunUid);
- _animPlayer.Stop(gunUid, uidPlayer, "muzzle-flash-light");
- _animPlayer.Play((gunUid, uidPlayer), animTwo, "muzzle-flash-light");
- }
- }
|