GunSystem.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. using System.Numerics;
  2. using Content.Client.Animations;
  3. using Content.Client.Gameplay;
  4. using Content.Client.Items;
  5. using Content.Client.Weapons.Ranged.Components;
  6. using Content.Shared.Camera;
  7. using Content.Shared.CombatMode;
  8. using Content.Shared.Weapons.Ranged;
  9. using Content.Shared.Weapons.Ranged.Components;
  10. using Content.Shared.Weapons.Ranged.Events;
  11. using Content.Shared.Weapons.Ranged.Systems;
  12. using Robust.Client.Animations;
  13. using Robust.Client.GameObjects;
  14. using Robust.Client.Graphics;
  15. using Robust.Client.Input;
  16. using Robust.Client.Player;
  17. using Robust.Client.State;
  18. using Robust.Shared.Animations;
  19. using Robust.Shared.Input;
  20. using Robust.Shared.Map;
  21. using Robust.Shared.Map.Components;
  22. using Robust.Shared.Prototypes;
  23. using Robust.Shared.Utility;
  24. using SharedGunSystem = Content.Shared.Weapons.Ranged.Systems.SharedGunSystem;
  25. using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent;
  26. namespace Content.Client.Weapons.Ranged.Systems;
  27. public sealed partial class GunSystem : SharedGunSystem
  28. {
  29. [Dependency] private readonly IComponentFactory _factory = default!;
  30. [Dependency] private readonly IEyeManager _eyeManager = default!;
  31. [Dependency] private readonly IInputManager _inputManager = default!;
  32. [Dependency] private readonly IPlayerManager _player = default!;
  33. [Dependency] private readonly IStateManager _state = default!;
  34. [Dependency] private readonly AnimationPlayerSystem _animPlayer = default!;
  35. [Dependency] private readonly InputSystem _inputSystem = default!;
  36. [Dependency] private readonly SharedCameraRecoilSystem _recoil = default!;
  37. [Dependency] private readonly SharedMapSystem _maps = default!;
  38. [Dependency] private readonly SharedTransformSystem _xform = default!;
  39. [ValidatePrototypeId<EntityPrototype>]
  40. public const string HitscanProto = "HitscanEffect";
  41. public bool SpreadOverlay
  42. {
  43. get => _spreadOverlay;
  44. set
  45. {
  46. if (_spreadOverlay == value)
  47. return;
  48. _spreadOverlay = value;
  49. var overlayManager = IoCManager.Resolve<IOverlayManager>();
  50. if (_spreadOverlay)
  51. {
  52. overlayManager.AddOverlay(new GunSpreadOverlay(
  53. EntityManager,
  54. _eyeManager,
  55. Timing,
  56. _inputManager,
  57. _player,
  58. this,
  59. TransformSystem));
  60. }
  61. else
  62. {
  63. overlayManager.RemoveOverlay<GunSpreadOverlay>();
  64. }
  65. }
  66. }
  67. private bool _spreadOverlay;
  68. public override void Initialize()
  69. {
  70. base.Initialize();
  71. UpdatesOutsidePrediction = true;
  72. SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect);
  73. SubscribeLocalEvent<AmmoCounterComponent, UpdateClientAmmoEvent>(OnUpdateClientAmmo);
  74. SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash);
  75. // Plays animated effects on the client.
  76. SubscribeNetworkEvent<HitscanEvent>(OnHitscan);
  77. InitializeMagazineVisuals();
  78. InitializeSpentAmmo();
  79. }
  80. private void OnUpdateClientAmmo(EntityUid uid, AmmoCounterComponent ammoComp, ref UpdateClientAmmoEvent args)
  81. {
  82. UpdateAmmoCount(uid, ammoComp);
  83. }
  84. private void OnMuzzleFlash(MuzzleFlashEvent args)
  85. {
  86. var gunUid = GetEntity(args.Uid);
  87. CreateEffect(gunUid, args, gunUid);
  88. }
  89. private void OnHitscan(HitscanEvent ev)
  90. {
  91. // ALL I WANT IS AN ANIMATED EFFECT
  92. // TODO EFFECTS
  93. // This is very jank
  94. // because the effect consists of three unrelatd entities, the hitscan beam can be split appart.
  95. // 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.
  96. // Ideally, there should only be one entity, with one sprite that has multiple layers
  97. // Or at the very least, have the other entities parented to the same entity to make sure they stick together.
  98. foreach (var a in ev.Sprites)
  99. {
  100. if (a.Sprite is not SpriteSpecifier.Rsi rsi)
  101. continue;
  102. var coords = GetCoordinates(a.coordinates);
  103. if (!TryComp(coords.EntityId, out TransformComponent? relativeXform))
  104. continue;
  105. var ent = Spawn(HitscanProto, coords);
  106. var sprite = Comp<SpriteComponent>(ent);
  107. var xform = Transform(ent);
  108. var targetWorldRot = a.angle + _xform.GetWorldRotation(relativeXform);
  109. var delta = targetWorldRot - _xform.GetWorldRotation(xform);
  110. _xform.SetLocalRotationNoLerp(ent, xform.LocalRotation + delta, xform);
  111. sprite[EffectLayers.Unshaded].AutoAnimated = false;
  112. sprite.LayerSetSprite(EffectLayers.Unshaded, rsi);
  113. sprite.LayerSetState(EffectLayers.Unshaded, rsi.RsiState);
  114. sprite.Scale = new Vector2(a.Distance, 1f);
  115. sprite[EffectLayers.Unshaded].Visible = true;
  116. var anim = new Animation()
  117. {
  118. Length = TimeSpan.FromSeconds(0.48f),
  119. AnimationTracks =
  120. {
  121. new AnimationTrackSpriteFlick()
  122. {
  123. LayerKey = EffectLayers.Unshaded,
  124. KeyFrames =
  125. {
  126. new AnimationTrackSpriteFlick.KeyFrame(rsi.RsiState, 0f),
  127. }
  128. }
  129. }
  130. };
  131. _animPlayer.Play(ent, anim, "hitscan-effect");
  132. }
  133. }
  134. public override void Update(float frameTime)
  135. {
  136. if (!Timing.IsFirstTimePredicted)
  137. return;
  138. var entityNull = _player.LocalEntity;
  139. if (entityNull == null || !TryComp<CombatModeComponent>(entityNull, out var combat) || !combat.IsInCombatMode)
  140. {
  141. return;
  142. }
  143. var entity = entityNull.Value;
  144. if (!TryGetGun(entity, out var gunUid, out var gun))
  145. {
  146. return;
  147. }
  148. var useKey = gun.UseKey ? EngineKeyFunctions.Use : EngineKeyFunctions.UseSecondary;
  149. if (_inputSystem.CmdStates.GetState(useKey) != BoundKeyState.Down && !gun.BurstActivated)
  150. {
  151. if (gun.ShotCounter != 0)
  152. EntityManager.RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) });
  153. return;
  154. }
  155. if (gun.NextFire > Timing.CurTime)
  156. return;
  157. var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition);
  158. if (mousePos.MapId == MapId.Nullspace)
  159. {
  160. if (gun.ShotCounter != 0)
  161. EntityManager.RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) });
  162. return;
  163. }
  164. // Define target coordinates relative to gun entity, so that network latency on moving grids doesn't fuck up the target location.
  165. var coordinates = TransformSystem.ToCoordinates(entity, mousePos);
  166. NetEntity? target = null;
  167. if (_state.CurrentState is GameplayStateBase screen)
  168. target = GetNetEntity(screen.GetClickedEntity(mousePos));
  169. Log.Debug($"Sending shoot request tick {Timing.CurTick} / {Timing.CurTime}");
  170. EntityManager.RaisePredictiveEvent(new RequestShootEvent
  171. {
  172. Target = target,
  173. Coordinates = GetNetCoordinates(coordinates),
  174. Gun = GetNetEntity(gunUid),
  175. });
  176. }
  177. public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? Entity, IShootable Shootable)> ammo,
  178. EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, out bool userImpulse, EntityUid? user = null, bool throwItems = false)
  179. {
  180. userImpulse = true;
  181. // Rather than splitting client / server for every ammo provider it's easier
  182. // to just delete the spawned entities. This is for programmer sanity despite the wasted perf.
  183. // This also means any ammo specific stuff can be grabbed as necessary.
  184. var direction = TransformSystem.ToMapCoordinates(fromCoordinates).Position - TransformSystem.ToMapCoordinates(toCoordinates).Position;
  185. var worldAngle = direction.ToAngle().Opposite();
  186. foreach (var (ent, shootable) in ammo)
  187. {
  188. if (throwItems)
  189. {
  190. Recoil(user, direction, gun.CameraRecoilScalarModified);
  191. if (IsClientSide(ent!.Value))
  192. Del(ent.Value);
  193. else
  194. RemoveShootable(ent.Value);
  195. continue;
  196. }
  197. switch (shootable)
  198. {
  199. case CartridgeAmmoComponent cartridge:
  200. if (!cartridge.Spent)
  201. {
  202. SetCartridgeSpent(ent!.Value, cartridge, true);
  203. MuzzleFlash(gunUid, cartridge, worldAngle, user);
  204. Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
  205. Recoil(user, direction, gun.CameraRecoilScalarModified);
  206. // TODO: Can't predict entity deletions.
  207. //if (cartridge.DeleteOnSpawn)
  208. // Del(cartridge.Owner);
  209. }
  210. else
  211. {
  212. userImpulse = false;
  213. Audio.PlayPredicted(gun.SoundEmpty, gunUid, user);
  214. }
  215. if (IsClientSide(ent!.Value))
  216. Del(ent.Value);
  217. break;
  218. case AmmoComponent newAmmo:
  219. MuzzleFlash(gunUid, newAmmo, worldAngle, user);
  220. Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
  221. Recoil(user, direction, gun.CameraRecoilScalarModified);
  222. if (IsClientSide(ent!.Value))
  223. Del(ent.Value);
  224. else
  225. RemoveShootable(ent.Value);
  226. break;
  227. case HitscanPrototype:
  228. Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
  229. Recoil(user, direction, gun.CameraRecoilScalarModified);
  230. break;
  231. }
  232. }
  233. }
  234. private void Recoil(EntityUid? user, Vector2 recoil, float recoilScalar)
  235. {
  236. if (!Timing.IsFirstTimePredicted || user == null || recoil == Vector2.Zero || recoilScalar == 0)
  237. return;
  238. _recoil.KickCamera(user.Value, recoil.Normalized() * 0.5f * recoilScalar);
  239. }
  240. protected override void Popup(string message, EntityUid? uid, EntityUid? user)
  241. {
  242. if (uid == null || user == null || !Timing.IsFirstTimePredicted)
  243. return;
  244. PopupSystem.PopupEntity(message, uid.Value, user.Value);
  245. }
  246. protected override void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, EntityUid? tracked = null)
  247. {
  248. if (!Timing.IsFirstTimePredicted)
  249. return;
  250. // EntityUid check added to stop throwing exceptions due to https://github.com/space-wizards/space-station-14/issues/28252
  251. // TODO: Check to see why invalid entities are firing effects.
  252. if (gunUid == EntityUid.Invalid)
  253. {
  254. Log.Debug($"Invalid Entity sent MuzzleFlashEvent (proto: {message.Prototype}, gun: {ToPrettyString(gunUid)})");
  255. return;
  256. }
  257. var gunXform = Transform(gunUid);
  258. var gridUid = gunXform.GridUid;
  259. EntityCoordinates coordinates;
  260. if (TryComp(gridUid, out MapGridComponent? mapGrid))
  261. {
  262. coordinates = new EntityCoordinates(gridUid.Value, _maps.LocalToGrid(gridUid.Value, mapGrid, gunXform.Coordinates));
  263. }
  264. else if (gunXform.MapUid != null)
  265. {
  266. coordinates = new EntityCoordinates(gunXform.MapUid.Value, TransformSystem.GetWorldPosition(gunXform));
  267. }
  268. else
  269. {
  270. return;
  271. }
  272. var ent = Spawn(message.Prototype, coordinates);
  273. TransformSystem.SetWorldRotationNoLerp(ent, message.Angle);
  274. if (tracked != null)
  275. {
  276. var track = EnsureComp<TrackUserComponent>(ent);
  277. track.User = tracked;
  278. track.Offset = Vector2.UnitX / 2f;
  279. }
  280. var lifetime = 0.4f;
  281. if (TryComp<TimedDespawnComponent>(gunUid, out var despawn))
  282. {
  283. lifetime = despawn.Lifetime;
  284. }
  285. var anim = new Animation()
  286. {
  287. Length = TimeSpan.FromSeconds(lifetime),
  288. AnimationTracks =
  289. {
  290. new AnimationTrackComponentProperty
  291. {
  292. ComponentType = typeof(SpriteComponent),
  293. Property = nameof(SpriteComponent.Color),
  294. InterpolationMode = AnimationInterpolationMode.Linear,
  295. KeyFrames =
  296. {
  297. new AnimationTrackProperty.KeyFrame(Color.White.WithAlpha(1f), 0),
  298. new AnimationTrackProperty.KeyFrame(Color.White.WithAlpha(0f), lifetime)
  299. }
  300. }
  301. }
  302. };
  303. _animPlayer.Play(ent, anim, "muzzle-flash");
  304. if (!TryComp(gunUid, out PointLightComponent? light))
  305. {
  306. light = (PointLightComponent) _factory.GetComponent(typeof(PointLightComponent));
  307. light.NetSyncEnabled = false;
  308. AddComp(gunUid, light);
  309. }
  310. Lights.SetEnabled(gunUid, true, light);
  311. Lights.SetRadius(gunUid, 2f, light);
  312. Lights.SetColor(gunUid, Color.FromHex("#cc8e2b"), light);
  313. Lights.SetEnergy(gunUid, 5f, light);
  314. var animTwo = new Animation()
  315. {
  316. Length = TimeSpan.FromSeconds(lifetime),
  317. AnimationTracks =
  318. {
  319. new AnimationTrackComponentProperty
  320. {
  321. ComponentType = typeof(PointLightComponent),
  322. Property = nameof(PointLightComponent.Energy),
  323. InterpolationMode = AnimationInterpolationMode.Linear,
  324. KeyFrames =
  325. {
  326. new AnimationTrackProperty.KeyFrame(5f, 0),
  327. new AnimationTrackProperty.KeyFrame(0f, lifetime)
  328. }
  329. },
  330. new AnimationTrackComponentProperty
  331. {
  332. ComponentType = typeof(PointLightComponent),
  333. Property = nameof(PointLightComponent.AnimatedEnable),
  334. InterpolationMode = AnimationInterpolationMode.Linear,
  335. KeyFrames =
  336. {
  337. new AnimationTrackProperty.KeyFrame(true, 0),
  338. new AnimationTrackProperty.KeyFrame(false, lifetime)
  339. }
  340. }
  341. }
  342. };
  343. var uidPlayer = EnsureComp<AnimationPlayerComponent>(gunUid);
  344. _animPlayer.Stop(gunUid, uidPlayer, "muzzle-flash-light");
  345. _animPlayer.Play((gunUid, uidPlayer), animTwo, "muzzle-flash-light");
  346. }
  347. }