GunSystem.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. using System.Linq;
  2. using System.Numerics;
  3. using Content.Client.Animations;
  4. using Content.Client.Gameplay;
  5. using Content.Client.Items;
  6. using Content.Client.Weapons.Ranged.Components;
  7. using Content.Shared._RMC14.Weapons.Ranged.Prediction;
  8. using Content.Shared.CombatMode;
  9. using Content.Shared.Weapons.Ranged.Events;
  10. using Content.Shared.Weapons.Ranged.Systems;
  11. using Robust.Client.Animations;
  12. using Robust.Client.GameObjects;
  13. using Robust.Client.Graphics;
  14. using Robust.Client.Input;
  15. using Robust.Client.Physics;
  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 SharedMapSystem _maps = default!;
  37. [Dependency] private readonly PhysicsSystem _physics = default!;
  38. [ValidatePrototypeId<EntityPrototype>]
  39. public const string HitscanProto = "HitscanEffect";
  40. public bool SpreadOverlay
  41. {
  42. get => _spreadOverlay;
  43. set
  44. {
  45. if (_spreadOverlay == value)
  46. return;
  47. _spreadOverlay = value;
  48. var overlayManager = IoCManager.Resolve<IOverlayManager>();
  49. if (_spreadOverlay)
  50. {
  51. overlayManager.AddOverlay(new GunSpreadOverlay(
  52. EntityManager,
  53. _eyeManager,
  54. Timing,
  55. _inputManager,
  56. _player,
  57. this,
  58. TransformSystem));
  59. }
  60. else
  61. {
  62. overlayManager.RemoveOverlay<GunSpreadOverlay>();
  63. }
  64. }
  65. }
  66. private bool _spreadOverlay;
  67. public override void Initialize()
  68. {
  69. base.Initialize();
  70. UpdatesOutsidePrediction = true;
  71. SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect);
  72. SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash);
  73. // Plays animated effects on the client.
  74. SubscribeNetworkEvent<HitscanEvent>(OnHitscan);
  75. InitializeMagazineVisuals();
  76. InitializeSpentAmmo();
  77. }
  78. private void OnMuzzleFlash(MuzzleFlashEvent args)
  79. {
  80. var gunUid = GetEntity(args.Uid);
  81. CreateEffect(gunUid, args, gunUid, _player.LocalEntity);
  82. }
  83. private void OnHitscan(HitscanEvent ev)
  84. {
  85. // ALL I WANT IS AN ANIMATED EFFECT
  86. foreach (var a in ev.Sprites)
  87. {
  88. if (a.Sprite is not SpriteSpecifier.Rsi rsi)
  89. continue;
  90. var coords = GetCoordinates(a.coordinates);
  91. if (Deleted(coords.EntityId))
  92. continue;
  93. var ent = Spawn(HitscanProto, coords);
  94. var sprite = Comp<SpriteComponent>(ent);
  95. var xform = Transform(ent);
  96. xform.LocalRotation = a.angle;
  97. sprite[EffectLayers.Unshaded].AutoAnimated = false;
  98. sprite.LayerSetSprite(EffectLayers.Unshaded, rsi);
  99. sprite.LayerSetState(EffectLayers.Unshaded, rsi.RsiState);
  100. sprite.Scale = new Vector2(a.Distance, 1f);
  101. sprite[EffectLayers.Unshaded].Visible = true;
  102. var anim = new Animation()
  103. {
  104. Length = TimeSpan.FromSeconds(0.48f),
  105. AnimationTracks =
  106. {
  107. new AnimationTrackSpriteFlick()
  108. {
  109. LayerKey = EffectLayers.Unshaded,
  110. KeyFrames =
  111. {
  112. new AnimationTrackSpriteFlick.KeyFrame(rsi.RsiState, 0f),
  113. }
  114. }
  115. }
  116. };
  117. _animPlayer.Play(ent, anim, "hitscan-effect");
  118. }
  119. }
  120. public override void Update(float frameTime)
  121. {
  122. if (!Timing.IsFirstTimePredicted)
  123. return;
  124. var entityNull = _player.LocalEntity;
  125. if (entityNull == null || !TryComp<CombatModeComponent>(entityNull, out var combat) || !combat.IsInCombatMode)
  126. {
  127. return;
  128. }
  129. var entity = entityNull.Value;
  130. if (!TryGetGun(entity, out var gunUid, out var gun))
  131. {
  132. return;
  133. }
  134. var useKey = gun.UseKey ? EngineKeyFunctions.Use : EngineKeyFunctions.UseSecondary;
  135. if (_inputSystem.CmdStates.GetState(useKey) != BoundKeyState.Down)
  136. {
  137. if (gun.ShotCounter != 0)
  138. EntityManager.RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) });
  139. return;
  140. }
  141. if (gun.NextFire > Timing.CurTime)
  142. return;
  143. var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition);
  144. if (mousePos.MapId == MapId.Nullspace)
  145. {
  146. if (gun.ShotCounter != 0)
  147. EntityManager.RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) });
  148. return;
  149. }
  150. // Define target coordinates relative to gun entity, so that network latency on moving grids doesn't fuck up the target location.
  151. var coordinates = TransformSystem.ToCoordinates(entity, mousePos);
  152. NetEntity? target = null;
  153. if (_state.CurrentState is GameplayStateBase screen)
  154. target = GetNetEntity(screen.GetClickedEntity(mousePos));
  155. if (_player.LocalSession is not { } session)
  156. return;
  157. Log.Debug($"Sending shoot request tick {Timing.CurTick} / {Timing.CurTime}");
  158. var projectiles = ShootRequested(GetNetEntity(gunUid), GetNetCoordinates(coordinates), target, null, session);
  159. RaisePredictiveEvent(new RequestShootEvent()
  160. {
  161. Target = target,
  162. Coordinates = GetNetCoordinates(coordinates),
  163. Gun = GetNetEntity(gunUid),
  164. Shot = projectiles?.Select(e => e.Id).ToList(),
  165. });
  166. }
  167. protected override void Popup(string message, EntityUid? uid, EntityUid? user)
  168. {
  169. if (uid == null || user == null || !Timing.IsFirstTimePredicted)
  170. return;
  171. PopupSystem.PopupEntity(message, uid.Value, user.Value);
  172. }
  173. protected override void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, EntityUid? tracked = null, EntityUid? player = null)
  174. {
  175. if (!Timing.IsFirstTimePredicted)
  176. return;
  177. // EntityUid check added to stop throwing exceptions due to https://github.com/space-wizards/space-station-14/issues/28252
  178. // TODO: Check to see why invalid entities are firing effects.
  179. if (gunUid == EntityUid.Invalid)
  180. {
  181. Log.Debug($"Invalid Entity sent MuzzleFlashEvent (proto: {message.Prototype}, gun: {ToPrettyString(gunUid)})");
  182. return;
  183. }
  184. var gunXform = Transform(gunUid);
  185. var gridUid = gunXform.GridUid;
  186. EntityCoordinates coordinates;
  187. if (TryComp(gridUid, out MapGridComponent? mapGrid))
  188. {
  189. coordinates = new EntityCoordinates(gridUid.Value, _maps.LocalToGrid(gridUid.Value, mapGrid, gunXform.Coordinates));
  190. }
  191. else if (gunXform.MapUid != null)
  192. {
  193. coordinates = new EntityCoordinates(gunXform.MapUid.Value, TransformSystem.GetWorldPosition(gunXform));
  194. }
  195. else
  196. {
  197. return;
  198. }
  199. var ent = Spawn(message.Prototype, coordinates);
  200. TransformSystem.SetWorldRotationNoLerp(ent, message.Angle);
  201. if (tracked != null)
  202. {
  203. var track = EnsureComp<TrackUserComponent>(ent);
  204. track.User = tracked;
  205. track.Offset = Vector2.UnitX / 2f;
  206. }
  207. var lifetime = 0.4f;
  208. if (TryComp<TimedDespawnComponent>(gunUid, out var despawn))
  209. {
  210. lifetime = despawn.Lifetime;
  211. }
  212. var anim = new Animation()
  213. {
  214. Length = TimeSpan.FromSeconds(lifetime),
  215. AnimationTracks =
  216. {
  217. new AnimationTrackComponentProperty
  218. {
  219. ComponentType = typeof(SpriteComponent),
  220. Property = nameof(SpriteComponent.Color),
  221. InterpolationMode = AnimationInterpolationMode.Linear,
  222. KeyFrames =
  223. {
  224. new AnimationTrackProperty.KeyFrame(Color.White.WithAlpha(1f), 0),
  225. new AnimationTrackProperty.KeyFrame(Color.White.WithAlpha(0f), lifetime)
  226. }
  227. }
  228. }
  229. };
  230. _animPlayer.Play(ent, anim, "muzzle-flash");
  231. if (!TryComp(gunUid, out PointLightComponent? light))
  232. {
  233. light = (PointLightComponent)_factory.GetComponent(typeof(PointLightComponent));
  234. light.NetSyncEnabled = false;
  235. AddComp(gunUid, light);
  236. }
  237. Lights.SetEnabled(gunUid, true, light);
  238. Lights.SetRadius(gunUid, 2f, light);
  239. Lights.SetColor(gunUid, Color.FromHex("#cc8e2b"), light);
  240. Lights.SetEnergy(gunUid, 5f, light);
  241. var animTwo = new Animation()
  242. {
  243. Length = TimeSpan.FromSeconds(lifetime),
  244. AnimationTracks =
  245. {
  246. new AnimationTrackComponentProperty
  247. {
  248. ComponentType = typeof(PointLightComponent),
  249. Property = nameof(PointLightComponent.Energy),
  250. InterpolationMode = AnimationInterpolationMode.Linear,
  251. KeyFrames =
  252. {
  253. new AnimationTrackProperty.KeyFrame(5f, 0),
  254. new AnimationTrackProperty.KeyFrame(0f, lifetime)
  255. }
  256. },
  257. new AnimationTrackComponentProperty
  258. {
  259. ComponentType = typeof(PointLightComponent),
  260. Property = nameof(PointLightComponent.AnimatedEnable),
  261. InterpolationMode = AnimationInterpolationMode.Linear,
  262. KeyFrames =
  263. {
  264. new AnimationTrackProperty.KeyFrame(true, 0),
  265. new AnimationTrackProperty.KeyFrame(false, lifetime)
  266. }
  267. }
  268. }
  269. };
  270. var uidPlayer = EnsureComp<AnimationPlayerComponent>(gunUid);
  271. _animPlayer.Stop(gunUid, uidPlayer, "muzzle-flash-light");
  272. _animPlayer.Play((gunUid, uidPlayer), animTwo, "muzzle-flash-light");
  273. }
  274. public override void ShootProjectile(EntityUid uid,
  275. Vector2 direction,
  276. Vector2 gunVelocity,
  277. EntityUid gunUid,
  278. EntityUid? user = null,
  279. float speed = 20)
  280. {
  281. EnsureComp<PredictedProjectileClientComponent>(uid);
  282. _physics.UpdateIsPredicted(uid);
  283. base.ShootProjectile(uid, direction, gunVelocity, gunUid, user, speed);
  284. }
  285. }