From 99f55993287ebdb4cbfde8dbdd649ef9e66c08d4 Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 00:07:28 +0300 Subject: [PATCH 01/22] Update chemistry.yml --- .../Entities/Objects/Specific/chemistry.yml | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml b/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml index 968cfa91fd1..d651c0769ac 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml @@ -93,9 +93,6 @@ - type: StaticPrice price: 30 - type: DnaSubstanceTrace - - type: ReverseEngineering # Nyano - difficulty: 2 - newItem: BluespaceBeaker - type: entity parent: BaseItem @@ -158,9 +155,6 @@ - type: StaticPrice price: 30 - type: DnaSubstanceTrace - - type: ReverseEngineering # Nyano - difficulty: 2 - newItem: BluespaceBeaker - type: entity name: beaker @@ -180,6 +174,10 @@ fillBaseName: beaker inHandsMaxFillLevels: 3 inHandsFillBaseName: -fill- + - type: ReverseEngineering # Silver + difficulty: 3 + recipes: + - BluespaceBeaker - type: entity suffix: cryoxadone @@ -229,6 +227,10 @@ Glass: 100 - type: StaticPrice price: 20 + - type: ReverseEngineering # Silver + difficulty: 1 + recipes: + - BluespaceBeaker - type: entity name: cryostasis beaker @@ -268,6 +270,10 @@ solutions: beaker: maxVol: 1000 + - type: ReverseEngineering # Silver + difficulty: 1 + recipes: + - BluespaceBeaker - type: entity name: dropper @@ -395,6 +401,9 @@ tags: - Syringe - Trash + - type: ReverseEngineering # Nyano + difficulty: 2 + newItem: SyringeBluespace - type: entity name: mini syringe @@ -493,6 +502,9 @@ tags: - Syringe - Trash + - type: ReverseEngineering # Nyano + difficulty: 2 + newItem: SyringeBluespace - type: entity id: SyringeCryostasis From d140c1c679e332e9c117bcc2ffc6838ae80e998f Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 17:57:14 +0300 Subject: [PATCH 02/22] Delete Content.Client/Weapons/Ranged/Systems/GunSystem.cs --- .../Weapons/Ranged/Systems/GunSystem.cs | 390 ------------------ 1 file changed, 390 deletions(-) delete mode 100644 Content.Client/Weapons/Ranged/Systems/GunSystem.cs diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs deleted file mode 100644 index 710ee7c7cbf..00000000000 --- a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs +++ /dev/null @@ -1,390 +0,0 @@ -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!; - - [ValidatePrototypeId] - public const string HitscanProto = "HitscanEffect"; - - public bool SpreadOverlay - { - get => _spreadOverlay; - set - { - if (_spreadOverlay == value) - return; - - _spreadOverlay = value; - var overlayManager = IoCManager.Resolve(); - - if (_spreadOverlay) - { - overlayManager.AddOverlay(new GunSpreadOverlay( - EntityManager, - _eyeManager, - Timing, - _inputManager, - _player, - this, - TransformSystem)); - } - else - { - overlayManager.RemoveOverlay(); - } - } - } - - private bool _spreadOverlay; - - public override void Initialize() - { - base.Initialize(); - UpdatesOutsidePrediction = true; - SubscribeLocalEvent(OnAmmoCounterCollect); - SubscribeAllEvent(OnMuzzleFlash); - - // Plays animated effects on the client. - SubscribeNetworkEvent(OnHitscan); - - InitializeMagazineVisuals(); - InitializeSpentAmmo(); - } - - 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 - foreach (var a in ev.Sprites) - { - if (a.Sprite is not SpriteSpecifier.Rsi rsi) - continue; - - var coords = GetCoordinates(a.coordinates); - - if (Deleted(coords.EntityId)) - continue; - - var ent = Spawn(HitscanProto, coords); - var sprite = Comp(ent); - var xform = Transform(ent); - xform.LocalRotation = a.angle; - 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(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(ent); - track.User = tracked; - track.Offset = Vector2.UnitX / 2f; - } - - var lifetime = 0.4f; - - if (TryComp(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(gunUid); - - _animPlayer.Stop(gunUid, uidPlayer, "muzzle-flash-light"); - _animPlayer.Play((gunUid, uidPlayer), animTwo, "muzzle-flash-light"); - } -} From 178cf091140dfd420748a0f3083f1df204962086 Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 17:57:34 +0300 Subject: [PATCH 03/22] Add files via upload --- .../Weapons/Ranged/Systems/GunSystem.cs | 396 ++++++++++++++++++ 1 file changed, 396 insertions(+) create mode 100644 Content.Client/Weapons/Ranged/Systems/GunSystem.cs diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs new file mode 100644 index 00000000000..6ef0bae741d --- /dev/null +++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs @@ -0,0 +1,396 @@ +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.Mech.Components; +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!; + + [ValidatePrototypeId] + public const string HitscanProto = "HitscanEffect"; + + public bool SpreadOverlay + { + get => _spreadOverlay; + set + { + if (_spreadOverlay == value) + return; + + _spreadOverlay = value; + var overlayManager = IoCManager.Resolve(); + + if (_spreadOverlay) + { + overlayManager.AddOverlay(new GunSpreadOverlay( + EntityManager, + _eyeManager, + Timing, + _inputManager, + _player, + this, + TransformSystem)); + } + else + { + overlayManager.RemoveOverlay(); + } + } + } + + private bool _spreadOverlay; + + public override void Initialize() + { + base.Initialize(); + UpdatesOutsidePrediction = true; + SubscribeLocalEvent(OnAmmoCounterCollect); + SubscribeAllEvent(OnMuzzleFlash); + + // Plays animated effects on the client. + SubscribeNetworkEvent(OnHitscan); + + InitializeMagazineVisuals(); + InitializeSpentAmmo(); + } + + 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 + foreach (var a in ev.Sprites) + { + if (a.Sprite is not SpriteSpecifier.Rsi rsi) + continue; + + var coords = GetCoordinates(a.coordinates); + + if (Deleted(coords.EntityId)) + continue; + + var ent = Spawn(HitscanProto, coords); + var sprite = Comp(ent); + var xform = Transform(ent); + xform.LocalRotation = a.angle; + 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(entityNull, out var combat) || !combat.IsInCombatMode) + { + return; + } + + var entity = entityNull.Value; + + if (TryComp(entity, out var mechPilot)) + { + entity = mechPilot.Mech; + } + + 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) + { + 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(ent); + track.User = tracked; + track.Offset = Vector2.UnitX / 2f; + } + + var lifetime = 0.4f; + + if (TryComp(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(gunUid); + + _animPlayer.Stop(gunUid, uidPlayer, "muzzle-flash-light"); + _animPlayer.Play((gunUid, uidPlayer), animTwo, "muzzle-flash-light"); + } +} From 7844ee0da8c362c886ed6e05d6673a508d14b027 Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 17:58:40 +0300 Subject: [PATCH 04/22] Add files via upload --- .../Equipment/EntitySystems/MechGunSystem.cs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 Content.Server/Mech/Equipment/EntitySystems/MechGunSystem.cs diff --git a/Content.Server/Mech/Equipment/EntitySystems/MechGunSystem.cs b/Content.Server/Mech/Equipment/EntitySystems/MechGunSystem.cs new file mode 100644 index 00000000000..8ed55e7462a --- /dev/null +++ b/Content.Server/Mech/Equipment/EntitySystems/MechGunSystem.cs @@ -0,0 +1,61 @@ +using Content.Server.Mech.Systems; +using Content.Server.Power.Components; +using Content.Server.Power.EntitySystems; +using Content.Shared.Mech.Components; +using Content.Shared.Mech.Equipment.Components; +using Content.Shared.Throwing; +using Content.Shared.Weapons.Ranged.Systems; +using Robust.Shared.Random; + +namespace Content.Server.Mech.Equipment.EntitySystems; +public sealed class MechGunSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly ThrowingSystem _throwing = default!; + [Dependency] private readonly MechSystem _mech = default!; + [Dependency] private readonly BatterySystem _battery = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(MechGunShot); + } + + private void MechGunShot(EntityUid uid, MechEquipmentComponent component, ref GunShotEvent args) + { + if (!component.EquipmentOwner.HasValue) + return; + + if (!TryComp(component.EquipmentOwner.Value, out var mech)) + return; + + if (TryComp(uid, out var battery)) + { + ChargeGunBattery(uid, battery); + return; + } + } + + private void ChargeGunBattery(EntityUid uid, BatteryComponent component) + { + if (!TryComp(uid, out var mechEquipment) || !mechEquipment.EquipmentOwner.HasValue) + return; + + if (!TryComp(mechEquipment.EquipmentOwner.Value, out var mech)) + return; + + var maxCharge = component.MaxCharge; + var currentCharge = component.CurrentCharge; + + var chargeDelta = maxCharge - currentCharge; + + // TODO: The battery charge of the mech would be spent directly when fired. + if (chargeDelta <= 0 || mech.Energy - chargeDelta < 0) + return; + + if (!_mech.TryChangeEnergy(mechEquipment.EquipmentOwner.Value, -chargeDelta, mech)) + return; + + _battery.SetCharge(uid, component.MaxCharge, component); + } +} \ No newline at end of file From 357d0fe82637057c1c1ed0be277ba339ed53b6a1 Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 17:59:36 +0300 Subject: [PATCH 05/22] Delete Content.Server/Mech/Systems/MechSystem.cs --- Content.Server/Mech/Systems/MechSystem.cs | 439 ---------------------- 1 file changed, 439 deletions(-) delete mode 100644 Content.Server/Mech/Systems/MechSystem.cs diff --git a/Content.Server/Mech/Systems/MechSystem.cs b/Content.Server/Mech/Systems/MechSystem.cs deleted file mode 100644 index 9da96a76f8e..00000000000 --- a/Content.Server/Mech/Systems/MechSystem.cs +++ /dev/null @@ -1,439 +0,0 @@ -using System.Linq; -using Content.Server.Atmos.EntitySystems; -using Content.Server.Mech.Components; -using Content.Server.Power.Components; -using Content.Server.Power.EntitySystems; -using Content.Shared.ActionBlocker; -using Content.Shared.Damage; -using Content.Shared.DoAfter; -using Content.Shared.FixedPoint; -using Content.Shared.Interaction; -using Content.Shared.Mech; -using Content.Shared.Mech.Components; -using Content.Shared.Mech.EntitySystems; -using Content.Shared.Movement.Events; -using Content.Shared.Popups; -using Content.Shared.Tools.Components; -using Content.Shared.Verbs; -using Content.Shared.Wires; -using Content.Server.Body.Systems; -using Content.Shared.Tools.Systems; -using Robust.Server.Containers; -using Robust.Server.GameObjects; -using Robust.Shared.Containers; -using Robust.Shared.Player; -using Content.Shared.Whitelist; - -namespace Content.Server.Mech.Systems; - -/// -public sealed partial class MechSystem : SharedMechSystem -{ - [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; - [Dependency] private readonly AtmosphereSystem _atmosphere = default!; - [Dependency] private readonly BatterySystem _battery = default!; - [Dependency] private readonly ContainerSystem _container = default!; - [Dependency] private readonly DamageableSystem _damageable = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly UserInterfaceSystem _ui = default!; - [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; - [Dependency] private readonly SharedToolSystem _toolSystem = default!; - - /// - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnInteractUsing); - SubscribeLocalEvent(OnInsertBattery); - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent>(OnAlternativeVerb); - SubscribeLocalEvent(OnOpenUi); - SubscribeLocalEvent(OnRemoveBattery); - SubscribeLocalEvent(OnMechEntry); - SubscribeLocalEvent(OnMechExit); - - SubscribeLocalEvent(OnDamageChanged); - SubscribeLocalEvent(OnRemoveEquipmentMessage); - - SubscribeLocalEvent(OnMechCanMoveEvent); - - - SubscribeLocalEvent(OnToolUseAttempt); - SubscribeLocalEvent(OnInhale); - SubscribeLocalEvent(OnExhale); - SubscribeLocalEvent(OnExpose); - - SubscribeLocalEvent(OnGetFilterAir); - - #region Equipment UI message relays - SubscribeLocalEvent(ReceiveEquipmentUiMesssages); - SubscribeLocalEvent(ReceiveEquipmentUiMesssages); - #endregion - } - - private void OnMechCanMoveEvent(EntityUid uid, MechComponent component, UpdateCanMoveEvent args) - { - if (component.Broken || component.Integrity <= 0 || component.Energy <= 0) - args.Cancel(); - } - - private void OnInteractUsing(EntityUid uid, MechComponent component, InteractUsingEvent args) - { - if (TryComp(uid, out var panel) && !panel.Open) - return; - - if (component.BatterySlot.ContainedEntity == null && TryComp(args.Used, out var battery)) - { - InsertBattery(uid, args.Used, component, battery); - _actionBlocker.UpdateCanMove(uid); - return; - } - - if (_toolSystem.HasQuality(args.Used, "Prying") && component.BatterySlot.ContainedEntity != null) - { - var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.BatteryRemovalDelay, - new RemoveBatteryEvent(), uid, target: uid, used: args.Target) - { - BreakOnMove = true - }; - - _doAfter.TryStartDoAfter(doAfterEventArgs); - } - } - - private void OnInsertBattery(EntityUid uid, MechComponent component, EntInsertedIntoContainerMessage args) - { - if (args.Container != component.BatterySlot || !TryComp(args.Entity, out var battery)) - return; - - component.Energy = battery.CurrentCharge; - component.MaxEnergy = battery.MaxCharge; - - Dirty(uid, component); - _actionBlocker.UpdateCanMove(uid); - } - - private void OnRemoveBattery(EntityUid uid, MechComponent component, RemoveBatteryEvent args) - { - if (args.Cancelled || args.Handled) - return; - - RemoveBattery(uid, component); - _actionBlocker.UpdateCanMove(uid); - - args.Handled = true; - } - - private void OnMapInit(EntityUid uid, MechComponent component, MapInitEvent args) - { - var xform = Transform(uid); - // TODO: this should use containerfill? - foreach (var equipment in component.StartingEquipment) - { - var ent = Spawn(equipment, xform.Coordinates); - InsertEquipment(uid, ent, component); - } - - // TODO: this should just be damage and battery - component.Integrity = component.MaxIntegrity; - component.Energy = component.MaxEnergy; - - _actionBlocker.UpdateCanMove(uid); - Dirty(uid, component); - } - - private void OnRemoveEquipmentMessage(EntityUid uid, MechComponent component, MechEquipmentRemoveMessage args) - { - var equip = GetEntity(args.Equipment); - - if (!Exists(equip) || Deleted(equip)) - return; - - if (!component.EquipmentContainer.ContainedEntities.Contains(equip)) - return; - - RemoveEquipment(uid, equip, component); - } - - private void OnOpenUi(EntityUid uid, MechComponent component, MechOpenUiEvent args) - { - args.Handled = true; - ToggleMechUi(uid, component); - } - - private void OnToolUseAttempt(EntityUid uid, MechPilotComponent component, ref ToolUserAttemptUseEvent args) - { - if (args.Target == component.Mech) - args.Cancelled = true; - } - - private void OnAlternativeVerb(EntityUid uid, MechComponent component, GetVerbsEvent args) - { - if (!args.CanAccess || !args.CanInteract || component.Broken) - return; - - if (CanInsert(uid, args.User, component)) - { - var enterVerb = new AlternativeVerb - { - Text = Loc.GetString("mech-verb-enter"), - Act = () => - { - var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.EntryDelay, new MechEntryEvent(), uid, target: uid) - { - BreakOnMove = true, - }; - - _doAfter.TryStartDoAfter(doAfterEventArgs); - } - }; - var openUiVerb = new AlternativeVerb //can't hijack someone else's mech - { - Act = () => ToggleMechUi(uid, component, args.User), - Text = Loc.GetString("mech-ui-open-verb") - }; - args.Verbs.Add(enterVerb); - args.Verbs.Add(openUiVerb); - } - else if (!IsEmpty(component)) - { - var ejectVerb = new AlternativeVerb - { - Text = Loc.GetString("mech-verb-exit"), - Priority = 1, // Promote to top to make ejecting the ALT-click action - Act = () => - { - if (args.User == uid || args.User == component.PilotSlot.ContainedEntity) - { - TryEject(uid, component); - return; - } - - var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.ExitDelay, new MechExitEvent(), uid, target: uid) - { - BreakOnMove = true, - }; - _popup.PopupEntity(Loc.GetString("mech-eject-pilot-alert", ("item", uid), ("user", args.User)), uid, PopupType.Large); - - _doAfter.TryStartDoAfter(doAfterEventArgs); - } - }; - args.Verbs.Add(ejectVerb); - } - } - - private void OnMechEntry(EntityUid uid, MechComponent component, MechEntryEvent args) - { - if (args.Cancelled || args.Handled) - return; - - if (_whitelistSystem.IsWhitelistFail(component.PilotWhitelist, args.User)) - { - _popup.PopupEntity(Loc.GetString("mech-no-enter", ("item", uid)), args.User); - return; - } - - TryInsert(uid, args.Args.User, component); - _actionBlocker.UpdateCanMove(uid); - - args.Handled = true; - } - - private void OnMechExit(EntityUid uid, MechComponent component, MechExitEvent args) - { - if (args.Cancelled || args.Handled) - return; - - TryEject(uid, component); - - args.Handled = true; - } - - private void OnDamageChanged(EntityUid uid, MechComponent component, DamageChangedEvent args) - { - var integrity = component.MaxIntegrity - args.Damageable.TotalDamage; - SetIntegrity(uid, integrity, component); - - if (args.DamageIncreased && - args.DamageDelta != null && - component.PilotSlot.ContainedEntity != null) - { - var damage = args.DamageDelta * component.MechToPilotDamageMultiplier; - _damageable.TryChangeDamage(component.PilotSlot.ContainedEntity, damage); - } - } - - private void ToggleMechUi(EntityUid uid, MechComponent? component = null, EntityUid? user = null) - { - if (!Resolve(uid, ref component)) - return; - user ??= component.PilotSlot.ContainedEntity; - if (user == null) - return; - - if (!TryComp(user, out var actor)) - return; - - _ui.TryToggleUi(uid, MechUiKey.Key, actor.PlayerSession); - UpdateUserInterface(uid, component); - } - - private void ReceiveEquipmentUiMesssages(EntityUid uid, MechComponent component, T args) where T : MechEquipmentUiMessage - { - var ev = new MechEquipmentUiMessageRelayEvent(args); - var allEquipment = new List(component.EquipmentContainer.ContainedEntities); - var argEquip = GetEntity(args.Equipment); - - foreach (var equipment in allEquipment) - { - if (argEquip == equipment) - RaiseLocalEvent(equipment, ev); - } - } - - public override void UpdateUserInterface(EntityUid uid, MechComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - base.UpdateUserInterface(uid, component); - - var ev = new MechEquipmentUiStateReadyEvent(); - foreach (var ent in component.EquipmentContainer.ContainedEntities) - { - RaiseLocalEvent(ent, ev); - } - - var state = new MechBoundUiState - { - EquipmentStates = ev.States - }; - _ui.SetUiState(uid, MechUiKey.Key, state); - } - - public override void BreakMech(EntityUid uid, MechComponent? component = null) - { - base.BreakMech(uid, component); - - _ui.CloseUi(uid, MechUiKey.Key); - _actionBlocker.UpdateCanMove(uid); - } - - public override bool TryChangeEnergy(EntityUid uid, FixedPoint2 delta, MechComponent? component = null) - { - if (!Resolve(uid, ref component)) - return false; - - if (!base.TryChangeEnergy(uid, delta, component)) - return false; - - var battery = component.BatterySlot.ContainedEntity; - if (battery == null) - return false; - - if (!TryComp(battery, out var batteryComp)) - return false; - - _battery.SetCharge(battery!.Value, batteryComp.CurrentCharge + delta.Float(), batteryComp); - if (batteryComp.CurrentCharge != component.Energy) //if there's a discrepency, we have to resync them - { - Log.Debug($"Battery charge was not equal to mech charge. Battery {batteryComp.CurrentCharge}. Mech {component.Energy}"); - component.Energy = batteryComp.CurrentCharge; - Dirty(uid, component); - } - _actionBlocker.UpdateCanMove(uid); - return true; - } - - public void InsertBattery(EntityUid uid, EntityUid toInsert, MechComponent? component = null, BatteryComponent? battery = null) - { - if (!Resolve(uid, ref component, false)) - return; - - if (!Resolve(toInsert, ref battery, false)) - return; - - _container.Insert(toInsert, component.BatterySlot); - component.Energy = battery.CurrentCharge; - component.MaxEnergy = battery.MaxCharge; - - _actionBlocker.UpdateCanMove(uid); - - Dirty(uid, component); - UpdateUserInterface(uid, component); - } - - public void RemoveBattery(EntityUid uid, MechComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - _container.EmptyContainer(component.BatterySlot); - component.Energy = 0; - component.MaxEnergy = 0; - - _actionBlocker.UpdateCanMove(uid); - - Dirty(uid, component); - UpdateUserInterface(uid, component); - } - - #region Atmos Handling - private void OnInhale(EntityUid uid, MechPilotComponent component, InhaleLocationEvent args) - { - if (!TryComp(component.Mech, out var mech) || - !TryComp(component.Mech, out var mechAir)) - { - return; - } - - if (mech.Airtight) - args.Gas = mechAir.Air; - } - - private void OnExhale(EntityUid uid, MechPilotComponent component, ExhaleLocationEvent args) - { - if (!TryComp(component.Mech, out var mech) || - !TryComp(component.Mech, out var mechAir)) - { - return; - } - - if (mech.Airtight) - args.Gas = mechAir.Air; - } - - private void OnExpose(EntityUid uid, MechPilotComponent component, ref AtmosExposedGetAirEvent args) - { - if (args.Handled) - return; - - if (!TryComp(component.Mech, out MechComponent? mech)) - return; - - if (mech.Airtight && TryComp(component.Mech, out MechAirComponent? air)) - { - args.Handled = true; - args.Gas = air.Air; - return; - } - - args.Gas = _atmosphere.GetContainingMixture(component.Mech, excite: args.Excite); - args.Handled = true; - } - - private void OnGetFilterAir(EntityUid uid, MechAirComponent comp, ref GetFilterAirEvent args) - { - if (args.Air != null) - return; - - // only airtight mechs get internal air - if (!TryComp(uid, out var mech) || !mech.Airtight) - return; - - args.Air = comp.Air; - } - #endregion -} From e11dc515d6402167b78fe424f994868c9d33546c Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 17:59:56 +0300 Subject: [PATCH 06/22] Add files via upload --- Content.Server/Mech/Systems/MechSystem.cs | 450 ++++++++++++++++++++++ 1 file changed, 450 insertions(+) create mode 100644 Content.Server/Mech/Systems/MechSystem.cs diff --git a/Content.Server/Mech/Systems/MechSystem.cs b/Content.Server/Mech/Systems/MechSystem.cs new file mode 100644 index 00000000000..3a1b99bbb5b --- /dev/null +++ b/Content.Server/Mech/Systems/MechSystem.cs @@ -0,0 +1,450 @@ +using System.Linq; +using Content.Server.Atmos.EntitySystems; +using Content.Server.Mech.Components; +using Content.Server.Power.Components; +using Content.Server.Power.EntitySystems; +using Content.Shared.ActionBlocker; +using Content.Shared.Damage; +using Content.Shared.DoAfter; +using Content.Shared.FixedPoint; +using Content.Shared.Interaction; +using Content.Shared.Mech; +using Content.Shared.Mech.Components; +using Content.Shared.Mech.EntitySystems; +using Content.Shared.Movement.Events; +using Content.Shared.Popups; +using Content.Shared.Tools.Components; +using Content.Shared.Verbs; +using Content.Shared.Wires; +using Content.Server.Body.Systems; +using Content.Shared.Tools.Systems; +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Robust.Server.Containers; +using Robust.Server.GameObjects; +using Robust.Shared.Containers; +using Robust.Shared.Player; +using Content.Shared.Whitelist; + +namespace Content.Server.Mech.Systems; + +/// +public sealed partial class MechSystem : SharedMechSystem +{ + [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; + [Dependency] private readonly AtmosphereSystem _atmosphere = default!; + [Dependency] private readonly BatterySystem _battery = default!; + [Dependency] private readonly ContainerSystem _container = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly UserInterfaceSystem _ui = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + [Dependency] private readonly SharedToolSystem _toolSystem = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + + /// + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInteractUsing); + SubscribeLocalEvent(OnInsertBattery); + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent>(OnAlternativeVerb); + SubscribeLocalEvent(OnOpenUi); + SubscribeLocalEvent(OnRemoveBattery); + SubscribeLocalEvent(OnMechEntry); + SubscribeLocalEvent(OnMechExit); + + SubscribeLocalEvent(OnDamageChanged); + SubscribeLocalEvent(OnRemoveEquipmentMessage); + + SubscribeLocalEvent(OnMechCanMoveEvent); + + + SubscribeLocalEvent(OnToolUseAttempt); + SubscribeLocalEvent(OnInhale); + SubscribeLocalEvent(OnExhale); + SubscribeLocalEvent(OnExpose); + + SubscribeLocalEvent(OnGetFilterAir); + + #region Equipment UI message relays + SubscribeLocalEvent(ReceiveEquipmentUiMesssages); + SubscribeLocalEvent(ReceiveEquipmentUiMesssages); + #endregion + } + + private void OnMechCanMoveEvent(EntityUid uid, MechComponent component, UpdateCanMoveEvent args) + { + if (component.Broken || component.Integrity <= 0 || component.Energy <= 0) + args.Cancel(); + } + + private void OnInteractUsing(EntityUid uid, MechComponent component, InteractUsingEvent args) + { + if (TryComp(uid, out var panel) && !panel.Open) + return; + + if (component.BatterySlot.ContainedEntity == null && TryComp(args.Used, out var battery)) + { + InsertBattery(uid, args.Used, component, battery); + _actionBlocker.UpdateCanMove(uid); + return; + } + + if (_toolSystem.HasQuality(args.Used, "Prying") && component.BatterySlot.ContainedEntity != null) + { + var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.BatteryRemovalDelay, + new RemoveBatteryEvent(), uid, target: uid, used: args.Target) + { + BreakOnMove = true + }; + + _doAfter.TryStartDoAfter(doAfterEventArgs); + } + } + + private void OnInsertBattery(EntityUid uid, MechComponent component, EntInsertedIntoContainerMessage args) + { + if (args.Container != component.BatterySlot || !TryComp(args.Entity, out var battery)) + return; + + component.Energy = battery.CurrentCharge; + component.MaxEnergy = battery.MaxCharge; + + Dirty(uid, component); + _actionBlocker.UpdateCanMove(uid); + } + + private void OnRemoveBattery(EntityUid uid, MechComponent component, RemoveBatteryEvent args) + { + if (args.Cancelled || args.Handled) + return; + + RemoveBattery(uid, component); + _actionBlocker.UpdateCanMove(uid); + + args.Handled = true; + } + + private void OnMapInit(EntityUid uid, MechComponent component, MapInitEvent args) + { + var xform = Transform(uid); + // TODO: this should use containerfill? + foreach (var equipment in component.StartingEquipment) + { + var ent = Spawn(equipment, xform.Coordinates); + InsertEquipment(uid, ent, component); + } + + // TODO: this should just be damage and battery + component.Integrity = component.MaxIntegrity; + component.Energy = component.MaxEnergy; + + _actionBlocker.UpdateCanMove(uid); + Dirty(uid, component); + } + + private void OnRemoveEquipmentMessage(EntityUid uid, MechComponent component, MechEquipmentRemoveMessage args) + { + var equip = GetEntity(args.Equipment); + + if (!Exists(equip) || Deleted(equip)) + return; + + if (!component.EquipmentContainer.ContainedEntities.Contains(equip)) + return; + + RemoveEquipment(uid, equip, component); + } + + private void OnOpenUi(EntityUid uid, MechComponent component, MechOpenUiEvent args) + { + args.Handled = true; + ToggleMechUi(uid, component); + } + + private void OnToolUseAttempt(EntityUid uid, MechPilotComponent component, ref ToolUserAttemptUseEvent args) + { + if (args.Target == component.Mech) + args.Cancelled = true; + } + + private void OnAlternativeVerb(EntityUid uid, MechComponent component, GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || component.Broken) + return; + + if (CanInsert(uid, args.User, component)) + { + var enterVerb = new AlternativeVerb + { + Text = Loc.GetString("mech-verb-enter"), + Act = () => + { + var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.EntryDelay, new MechEntryEvent(), uid, target: uid) + { + BreakOnMove = true, + }; + + _doAfter.TryStartDoAfter(doAfterEventArgs); + } + }; + var openUiVerb = new AlternativeVerb //can't hijack someone else's mech + { + Act = () => ToggleMechUi(uid, component, args.User), + Text = Loc.GetString("mech-ui-open-verb") + }; + args.Verbs.Add(enterVerb); + args.Verbs.Add(openUiVerb); + } + else if (!IsEmpty(component)) + { + var ejectVerb = new AlternativeVerb + { + Text = Loc.GetString("mech-verb-exit"), + Priority = 1, // Promote to top to make ejecting the ALT-click action + Act = () => + { + if (args.User == uid || args.User == component.PilotSlot.ContainedEntity) + { + TryEject(uid, component); + return; + } + + var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.ExitDelay, new MechExitEvent(), uid, target: uid) + { + BreakOnMove = true, + }; + _popup.PopupEntity(Loc.GetString("mech-eject-pilot-alert", ("item", uid), ("user", args.User)), uid, PopupType.Large); + + _doAfter.TryStartDoAfter(doAfterEventArgs); + } + }; + args.Verbs.Add(ejectVerb); + } + } + + private void OnMechEntry(EntityUid uid, MechComponent component, MechEntryEvent args) + { + if (args.Cancelled || args.Handled) + return; + + if (_whitelistSystem.IsWhitelistFail(component.PilotWhitelist, args.User)) + { + _popup.PopupEntity(Loc.GetString("mech-no-enter", ("item", uid)), args.User); + return; + } + + if (!TryComp(args.Args.User, out var handsComponent)) + return; + + foreach (var hand in _hands.EnumerateHands(args.Args.User, handsComponent)) + { + _hands.DoDrop(args.Args.User, hand, true, handsComponent); + } + + TryInsert(uid, args.Args.User, component); + _actionBlocker.UpdateCanMove(uid); + + args.Handled = true; + } + + private void OnMechExit(EntityUid uid, MechComponent component, MechExitEvent args) + { + if (args.Cancelled || args.Handled) + return; + + TryEject(uid, component); + + args.Handled = true; + } + + private void OnDamageChanged(EntityUid uid, MechComponent component, DamageChangedEvent args) + { + var integrity = component.MaxIntegrity - args.Damageable.TotalDamage; + SetIntegrity(uid, integrity, component); + + if (args.DamageIncreased && + args.DamageDelta != null && + component.PilotSlot.ContainedEntity != null) + { + var damage = args.DamageDelta * component.MechToPilotDamageMultiplier; + _damageable.TryChangeDamage(component.PilotSlot.ContainedEntity, damage); + } + } + + private void ToggleMechUi(EntityUid uid, MechComponent? component = null, EntityUid? user = null) + { + if (!Resolve(uid, ref component)) + return; + user ??= component.PilotSlot.ContainedEntity; + if (user == null) + return; + + if (!TryComp(user, out var actor)) + return; + + _ui.TryToggleUi(uid, MechUiKey.Key, actor.PlayerSession); + UpdateUserInterface(uid, component); + } + + private void ReceiveEquipmentUiMesssages(EntityUid uid, MechComponent component, T args) where T : MechEquipmentUiMessage + { + var ev = new MechEquipmentUiMessageRelayEvent(args); + var allEquipment = new List(component.EquipmentContainer.ContainedEntities); + var argEquip = GetEntity(args.Equipment); + + foreach (var equipment in allEquipment) + { + if (argEquip == equipment) + RaiseLocalEvent(equipment, ev); + } + } + + public override void UpdateUserInterface(EntityUid uid, MechComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + base.UpdateUserInterface(uid, component); + + var ev = new MechEquipmentUiStateReadyEvent(); + foreach (var ent in component.EquipmentContainer.ContainedEntities) + { + RaiseLocalEvent(ent, ev); + } + + var state = new MechBoundUiState + { + EquipmentStates = ev.States + }; + _ui.SetUiState(uid, MechUiKey.Key, state); + } + + public override void BreakMech(EntityUid uid, MechComponent? component = null) + { + base.BreakMech(uid, component); + + _ui.CloseUi(uid, MechUiKey.Key); + _actionBlocker.UpdateCanMove(uid); + } + + public override bool TryChangeEnergy(EntityUid uid, FixedPoint2 delta, MechComponent? component = null) + { + if (!Resolve(uid, ref component)) + return false; + + if (!base.TryChangeEnergy(uid, delta, component)) + return false; + + var battery = component.BatterySlot.ContainedEntity; + if (battery == null) + return false; + + if (!TryComp(battery, out var batteryComp)) + return false; + + _battery.SetCharge(battery!.Value, batteryComp.CurrentCharge + delta.Float(), batteryComp); + if (batteryComp.CurrentCharge != component.Energy) //if there's a discrepency, we have to resync them + { + Log.Debug($"Battery charge was not equal to mech charge. Battery {batteryComp.CurrentCharge}. Mech {component.Energy}"); + component.Energy = batteryComp.CurrentCharge; + Dirty(uid, component); + } + _actionBlocker.UpdateCanMove(uid); + return true; + } + + public void InsertBattery(EntityUid uid, EntityUid toInsert, MechComponent? component = null, BatteryComponent? battery = null) + { + if (!Resolve(uid, ref component, false)) + return; + + if (!Resolve(toInsert, ref battery, false)) + return; + + _container.Insert(toInsert, component.BatterySlot); + component.Energy = battery.CurrentCharge; + component.MaxEnergy = battery.MaxCharge; + + _actionBlocker.UpdateCanMove(uid); + + Dirty(uid, component); + UpdateUserInterface(uid, component); + } + + public void RemoveBattery(EntityUid uid, MechComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + _container.EmptyContainer(component.BatterySlot); + component.Energy = 0; + component.MaxEnergy = 0; + + _actionBlocker.UpdateCanMove(uid); + + Dirty(uid, component); + UpdateUserInterface(uid, component); + } + + #region Atmos Handling + private void OnInhale(EntityUid uid, MechPilotComponent component, InhaleLocationEvent args) + { + if (!TryComp(component.Mech, out var mech) || + !TryComp(component.Mech, out var mechAir)) + { + return; + } + + if (mech.Airtight) + args.Gas = mechAir.Air; + } + + private void OnExhale(EntityUid uid, MechPilotComponent component, ExhaleLocationEvent args) + { + if (!TryComp(component.Mech, out var mech) || + !TryComp(component.Mech, out var mechAir)) + { + return; + } + + if (mech.Airtight) + args.Gas = mechAir.Air; + } + + private void OnExpose(EntityUid uid, MechPilotComponent component, ref AtmosExposedGetAirEvent args) + { + if (args.Handled) + return; + + if (!TryComp(component.Mech, out MechComponent? mech)) + return; + + if (mech.Airtight && TryComp(component.Mech, out MechAirComponent? air)) + { + args.Handled = true; + args.Gas = air.Air; + return; + } + + args.Gas = _atmosphere.GetContainingMixture(component.Mech, excite: args.Excite); + args.Handled = true; + } + + private void OnGetFilterAir(EntityUid uid, MechAirComponent comp, ref GetFilterAirEvent args) + { + if (args.Air != null) + return; + + // only airtight mechs get internal air + if (!TryComp(uid, out var mech) || !mech.Airtight) + return; + + args.Air = comp.Air; + } + #endregion +} From 19e11c616693b75c925d01eeecf586075fefc2af Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:00:47 +0300 Subject: [PATCH 07/22] Delete Content.Server/Weapons/Ranged/Systems/GunSystem.cs --- .../Weapons/Ranged/Systems/GunSystem.cs | 454 ------------------ 1 file changed, 454 deletions(-) delete mode 100644 Content.Server/Weapons/Ranged/Systems/GunSystem.cs diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs deleted file mode 100644 index d22b5ec2af7..00000000000 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs +++ /dev/null @@ -1,454 +0,0 @@ -using System.Linq; -using System.Numerics; -using Content.Server.Cargo.Systems; -using Content.Server.Power.EntitySystems; -using Content.Server.Weapons.Ranged.Components; -using Content.Shared.Damage; -using Content.Shared.Damage.Systems; -using Content.Shared.Database; -using Content.Shared.Effects; -using Content.Shared.Projectiles; -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; - -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 PricingSystem _pricing = default!; - [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly StaminaSystem _stamina = default!; - [Dependency] private readonly SharedContainerSystem _container = default!; - - private const float DamagePitchVariation = 0.05f; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnBallisticPrice); - } - - private void OnBallisticPrice(EntityUid uid, BallisticAmmoProviderComponent component, ref PriceCalculationEvent args) - { - if (string.IsNullOrEmpty(component.Proto) || component.UnspawnedCount == 0) - return; - - if (!ProtoManager.TryIndex(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 = 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; - 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(ammo.Count); - - 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) - { - 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)) - 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; - - 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 (collide.HitEntity != gun.Target && - CompOrNull(collide.HitEntity)?.Active == true) - { - continue; - } - - result = collide; - break; - } - } - - var hit = result.HitEntity; - lastHit = hit; - - FireEffects(fromEffect, result.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.AnyPositive()) - { - _color.RaiseEffect(Color.Red, new List() { 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, - }); - - void CreateAndFireProjectiles(EntityUid ammoEnt, AmmoComponent ammoComp) - { - if (TryComp(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); - } - } - - private void ShootOrThrow(EntityUid uid, Vector2 mapDirection, Vector2 gunVelocity, GunComponent gun, EntityUid gunUid, EntityUid? user) - { - if (gun.Target is { } target && !TerminatingOrDeleted(target)) - { - var targeted = EnsureComp(uid); - targeted.Target = target; - Dirty(uid, targeted); - } - - // Do a throw - if (!HasComp(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); - } - - /// - /// Gets a linear spread of angles between start and end. - /// - /// Start angle in degrees - /// End angle in degrees - /// How many shots there are - 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 gunUid, MuzzleFlashEvent message, EntityUid? user = null) - { - var filter = Filter.Pvs(gunUid, entityManager: EntityManager); - - if (TryComp(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(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(); - - if (xformQuery.TryGetComponent(gridUid, out var gridXform)) - { - var (_, gridRot, gridInvMatrix) = TransformSystem.GetWorldPositionRotationInvMatrix(gridXform, xformQuery); - - fromCoordinates = new EntityCoordinates(gridUid.Value, - Vector2.Transform(fromCoordinates.ToMapPos(EntityManager, TransformSystem), gridInvMatrix)); - - // 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 -} From 741f2f1cc84571e749022e2bad6faa53955bfc49 Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:01:38 +0300 Subject: [PATCH 08/22] Add files via upload --- .../Weapons/Ranged/Systems/GunSystem.cs | 474 ++++++++++++++++++ 1 file changed, 474 insertions(+) create mode 100644 Content.Server/Weapons/Ranged/Systems/GunSystem.cs diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs new file mode 100644 index 00000000000..781a7b11fe9 --- /dev/null +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs @@ -0,0 +1,474 @@ +using System.Linq; +using System.Numerics; +using Content.Server.Cargo.Systems; +using Content.Server.Interaction; +using Content.Server.Mech.Equipment.Components; +using Content.Server.Power.EntitySystems; +using Content.Server.Stunnable; +using Content.Server.Weapons.Ranged.Components; +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.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; + +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 SharedTransformSystem _transform = default!; + [Dependency] private readonly StaminaSystem _stamina = default!; + [Dependency] private readonly StunSystem _stun = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + + private const float DamagePitchVariation = 0.05f; + public const float GunClumsyChance = 0.5f; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnBallisticPrice); + } + + private void OnBallisticPrice(EntityUid uid, BallisticAmmoProviderComponent component, ref PriceCalculationEvent args) + { + if (string.IsNullOrEmpty(component.Proto) || component.UnspawnedCount == 0) + return; + + if (!ProtoManager.TryIndex(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; + + // Try a clumsy roll + // TODO: Who put this here + if (TryComp(user, out var clumsy) && gun.ClumsyProof == false) + { + 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); + userImpulse = false; + return; + } + } + } + + 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; + 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(ammo.Count); + + 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) + { + 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)) + 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; + + 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 (collide.HitEntity != gun.Target && + CompOrNull(collide.HitEntity)?.Active == true) + { + continue; + } + + result = collide; + break; + } + } + + var hit = result.HitEntity; + lastHit = hit; + + FireEffects(fromEffect, result.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.AnyPositive()) + { + _color.RaiseEffect(Color.Red, new List() { 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, + }); + + void CreateAndFireProjectiles(EntityUid ammoEnt, AmmoComponent ammoComp) + { + if (TryComp(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); + } + } + + private void ShootOrThrow(EntityUid uid, Vector2 mapDirection, Vector2 gunVelocity, GunComponent gun, EntityUid gunUid, EntityUid? user) + { + if (gun.Target is { } target && !TerminatingOrDeleted(target)) + { + var targeted = EnsureComp(uid); + targeted.Target = target; + Dirty(uid, targeted); + } + + // Do a throw + if (!HasComp(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); + } + + /// + /// Gets a linear spread of angles between start and end. + /// + /// Start angle in degrees + /// End angle in degrees + /// How many shots there are + 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 gunUid, MuzzleFlashEvent message, EntityUid? user = null) + { + var filter = Filter.Pvs(gunUid, entityManager: EntityManager); + + if (TryComp(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(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(); + + if (xformQuery.TryGetComponent(gridUid, out var gridXform)) + { + var (_, gridRot, gridInvMatrix) = TransformSystem.GetWorldPositionRotationInvMatrix(gridXform, xformQuery); + + fromCoordinates = new EntityCoordinates(gridUid.Value, + Vector2.Transform(fromCoordinates.ToMapPos(EntityManager, TransformSystem), gridInvMatrix)); + + // 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 +} From 5292c989b6a9f757e972258d8d3c1ad33da674cf Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:02:22 +0300 Subject: [PATCH 09/22] Delete Content.Shared/Mech/Components/MechComponent.cs --- .../Mech/Components/MechComponent.cs | 161 ------------------ 1 file changed, 161 deletions(-) delete mode 100644 Content.Shared/Mech/Components/MechComponent.cs diff --git a/Content.Shared/Mech/Components/MechComponent.cs b/Content.Shared/Mech/Components/MechComponent.cs deleted file mode 100644 index ba380492bc9..00000000000 --- a/Content.Shared/Mech/Components/MechComponent.cs +++ /dev/null @@ -1,161 +0,0 @@ -using Content.Shared.FixedPoint; -using Content.Shared.Whitelist; -using Robust.Shared.Containers; -using Robust.Shared.GameStates; -using Robust.Shared.Prototypes; - -namespace Content.Shared.Mech.Components; - -/// -/// A large, pilotable machine that has equipment that is -/// powered via an internal battery. -/// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] -public sealed partial class MechComponent : Component -{ - /// - /// How much "health" the mech has left. - /// - [ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] - public FixedPoint2 Integrity; - - /// - /// The maximum amount of damage the mech can take. - /// - [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] - public FixedPoint2 MaxIntegrity = 250; - - /// - /// How much energy the mech has. - /// Derived from the currently inserted battery. - /// - [ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] - public FixedPoint2 Energy = 0; - - /// - /// The maximum amount of energy the mech can have. - /// Derived from the currently inserted battery. - /// - [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] - public FixedPoint2 MaxEnergy = 0; - - /// - /// The slot the battery is stored in. - /// - [ViewVariables] - public ContainerSlot BatterySlot = default!; - - [ViewVariables] - public readonly string BatterySlotId = "mech-battery-slot"; - - /// - /// A multiplier used to calculate how much of the damage done to a mech - /// is transfered to the pilot - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float MechToPilotDamageMultiplier; - - /// - /// Whether the mech has been destroyed and is no longer pilotable. - /// - [ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] - public bool Broken = false; - - /// - /// The slot the pilot is stored in. - /// - [ViewVariables(VVAccess.ReadWrite)] - public ContainerSlot PilotSlot = default!; - - [ViewVariables] - public readonly string PilotSlotId = "mech-pilot-slot"; - - /// - /// The current selected equipment of the mech. - /// If null, the mech is using just its fists. - /// - [ViewVariables, AutoNetworkedField] - public EntityUid? CurrentSelectedEquipment; - - /// - /// The maximum amount of equipment items that can be installed in the mech - /// - [DataField("maxEquipmentAmount"), ViewVariables(VVAccess.ReadWrite)] - public int MaxEquipmentAmount = 3; - - /// - /// A whitelist for inserting equipment items. - /// - [DataField] - public EntityWhitelist? EquipmentWhitelist; - - [DataField] - public EntityWhitelist? PilotWhitelist; - - /// - /// A container for storing the equipment entities. - /// - [ViewVariables(VVAccess.ReadWrite)] - public Container EquipmentContainer = default!; - - [ViewVariables] - public readonly string EquipmentContainerId = "mech-equipment-container"; - - /// - /// How long it takes to enter the mech. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float EntryDelay = 3; - - /// - /// How long it takes to pull *another person* - /// outside of the mech. You can exit instantly yourself. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float ExitDelay = 3; - - /// - /// How long it takes to pull out the battery. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float BatteryRemovalDelay = 2; - - /// - /// Whether or not the mech is airtight. - /// - /// - /// This needs to be redone - /// when mech internals are added - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public bool Airtight; - - /// - /// The equipment that the mech initially has when it spawns. - /// Good for things like nukie mechs that start with guns. - /// - [DataField] - public List StartingEquipment = new(); - - #region Action Prototypes - [DataField] - public EntProtoId MechCycleAction = "ActionMechCycleEquipment"; - [DataField] - public EntProtoId MechUiAction = "ActionMechOpenUI"; - [DataField] - public EntProtoId MechEjectAction = "ActionMechEject"; - #endregion - - #region Visualizer States - [DataField] - public string? BaseState; - [DataField] - public string? OpenState; - [DataField] - public string? BrokenState; - #endregion - - [DataField] public EntityUid? MechCycleActionEntity; - [DataField] public EntityUid? MechUiActionEntity; - [DataField] public EntityUid? MechEjectActionEntity; -} From e63b23e2d91e378614d42da88041e8ab7a02e829 Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:02:33 +0300 Subject: [PATCH 10/22] Add files via upload --- .../Mech/Components/MechComponent.cs | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 Content.Shared/Mech/Components/MechComponent.cs diff --git a/Content.Shared/Mech/Components/MechComponent.cs b/Content.Shared/Mech/Components/MechComponent.cs new file mode 100644 index 00000000000..ce7026796b8 --- /dev/null +++ b/Content.Shared/Mech/Components/MechComponent.cs @@ -0,0 +1,168 @@ +using Content.Shared.FixedPoint; +using Content.Shared.Whitelist; +using Robust.Shared.Containers; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Mech.Components; + +/// +/// A large, pilotable machine that has equipment that is +/// powered via an internal battery. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class MechComponent : Component +{ + /// + /// Whether or not an emag disables it. + /// + [DataField("breakOnEmag")] + [AutoNetworkedField] + public bool BreakOnEmag = true; + + /// + /// How much "health" the mech has left. + /// + [ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public FixedPoint2 Integrity; + + /// + /// The maximum amount of damage the mech can take. + /// + [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public FixedPoint2 MaxIntegrity = 250; + + /// + /// How much energy the mech has. + /// Derived from the currently inserted battery. + /// + [ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public FixedPoint2 Energy = 0; + + /// + /// The maximum amount of energy the mech can have. + /// Derived from the currently inserted battery. + /// + [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public FixedPoint2 MaxEnergy = 0; + + /// + /// The slot the battery is stored in. + /// + [ViewVariables] + public ContainerSlot BatterySlot = default!; + + [ViewVariables] + public readonly string BatterySlotId = "mech-battery-slot"; + + /// + /// A multiplier used to calculate how much of the damage done to a mech + /// is transfered to the pilot + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float MechToPilotDamageMultiplier; + + /// + /// Whether the mech has been destroyed and is no longer pilotable. + /// + [ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public bool Broken = false; + + /// + /// The slot the pilot is stored in. + /// + [ViewVariables(VVAccess.ReadWrite)] + public ContainerSlot PilotSlot = default!; + + [ViewVariables] + public readonly string PilotSlotId = "mech-pilot-slot"; + + /// + /// The current selected equipment of the mech. + /// If null, the mech is using just its fists. + /// + [ViewVariables, AutoNetworkedField] + public EntityUid? CurrentSelectedEquipment; + + /// + /// The maximum amount of equipment items that can be installed in the mech + /// + [DataField("maxEquipmentAmount"), ViewVariables(VVAccess.ReadWrite)] + public int MaxEquipmentAmount = 3; + + /// + /// A whitelist for inserting equipment items. + /// + [DataField] + public EntityWhitelist? EquipmentWhitelist; + + [DataField] + public EntityWhitelist? PilotWhitelist; + + /// + /// A container for storing the equipment entities. + /// + [ViewVariables(VVAccess.ReadWrite)] + public Container EquipmentContainer = default!; + + [ViewVariables] + public readonly string EquipmentContainerId = "mech-equipment-container"; + + /// + /// How long it takes to enter the mech. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float EntryDelay = 3; + + /// + /// How long it takes to pull *another person* + /// outside of the mech. You can exit instantly yourself. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float ExitDelay = 3; + + /// + /// How long it takes to pull out the battery. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float BatteryRemovalDelay = 2; + + /// + /// Whether or not the mech is airtight. + /// + /// + /// This needs to be redone + /// when mech internals are added + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public bool Airtight; + + /// + /// The equipment that the mech initially has when it spawns. + /// Good for things like nukie mechs that start with guns. + /// + [DataField] + public List StartingEquipment = new(); + + #region Action Prototypes + [DataField] + public EntProtoId MechCycleAction = "ActionMechCycleEquipment"; + [DataField] + public EntProtoId MechUiAction = "ActionMechOpenUI"; + [DataField] + public EntProtoId MechEjectAction = "ActionMechEject"; + #endregion + + #region Visualizer States + [DataField] + public string? BaseState; + [DataField] + public string? OpenState; + [DataField] + public string? BrokenState; + #endregion + + [DataField] public EntityUid? MechCycleActionEntity; + [DataField] public EntityUid? MechUiActionEntity; + [DataField] public EntityUid? MechEjectActionEntity; +} From a12797453aa0939a9f7b19b862e72ef3df1726bf Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:03:14 +0300 Subject: [PATCH 11/22] Delete Content.Shared/Mech/EntitySystems/SharedMechSystem.cs --- .../Mech/EntitySystems/SharedMechSystem.cs | 478 ------------------ 1 file changed, 478 deletions(-) delete mode 100644 Content.Shared/Mech/EntitySystems/SharedMechSystem.cs diff --git a/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs b/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs deleted file mode 100644 index 2ec48085c47..00000000000 --- a/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs +++ /dev/null @@ -1,478 +0,0 @@ -using System.Linq; -using Content.Shared.Access.Components; -using Content.Shared.ActionBlocker; -using Content.Shared.Actions; -using Content.Shared.Destructible; -using Content.Shared.DoAfter; -using Content.Shared.DragDrop; -using Content.Shared.FixedPoint; -using Content.Shared.Interaction; -using Content.Shared.Interaction.Components; -using Content.Shared.Interaction.Events; -using Content.Shared.Mech.Components; -using Content.Shared.Mech.Equipment.Components; -using Content.Shared.Movement.Components; -using Content.Shared.Movement.Systems; -using Content.Shared.Popups; -using Content.Shared.Weapons.Melee; -using Content.Shared.Whitelist; -using Robust.Shared.Containers; -using Robust.Shared.Network; -using Robust.Shared.Serialization; -using Robust.Shared.Timing; - -namespace Content.Shared.Mech.EntitySystems; - -/// -/// Handles all of the interactions, UI handling, and items shennanigans for -/// -public abstract class SharedMechSystem : EntitySystem -{ - [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly INetManager _net = default!; - [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; - [Dependency] private readonly SharedActionsSystem _actions = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly SharedContainerSystem _container = default!; - [Dependency] private readonly SharedInteractionSystem _interaction = default!; - [Dependency] private readonly SharedMoverController _mover = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; - [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; - - /// - public override void Initialize() - { - SubscribeLocalEvent(OnToggleEquipmentAction); - SubscribeLocalEvent(OnEjectPilotEvent); - SubscribeLocalEvent(RelayInteractionEvent); - SubscribeLocalEvent(OnStartup); - SubscribeLocalEvent(OnDestruction); - SubscribeLocalEvent(OnGetAdditionalAccess); - SubscribeLocalEvent(OnDragDrop); - SubscribeLocalEvent(OnCanDragDrop); - - SubscribeLocalEvent(OnGetMeleeWeapon); - SubscribeLocalEvent(OnCanAttackFromContainer); - SubscribeLocalEvent(OnAttackAttempt); - } - - private void OnToggleEquipmentAction(EntityUid uid, MechComponent component, MechToggleEquipmentEvent args) - { - if (args.Handled) - return; - args.Handled = true; - CycleEquipment(uid); - } - - private void OnEjectPilotEvent(EntityUid uid, MechComponent component, MechEjectPilotEvent args) - { - if (args.Handled) - return; - args.Handled = true; - TryEject(uid, component); - } - - private void RelayInteractionEvent(EntityUid uid, MechComponent component, UserActivateInWorldEvent args) - { - var pilot = component.PilotSlot.ContainedEntity; - if (pilot == null) - return; - - // TODO why is this being blocked? - if (!_timing.IsFirstTimePredicted) - return; - - if (component.CurrentSelectedEquipment != null) - { - RaiseLocalEvent(component.CurrentSelectedEquipment.Value, args); - } - } - - private void OnStartup(EntityUid uid, MechComponent component, ComponentStartup args) - { - component.PilotSlot = _container.EnsureContainer(uid, component.PilotSlotId); - component.EquipmentContainer = _container.EnsureContainer(uid, component.EquipmentContainerId); - component.BatterySlot = _container.EnsureContainer(uid, component.BatterySlotId); - UpdateAppearance(uid, component); - } - - private void OnDestruction(EntityUid uid, MechComponent component, DestructionEventArgs args) - { - BreakMech(uid, component); - } - - private void OnGetAdditionalAccess(EntityUid uid, MechComponent component, ref GetAdditionalAccessEvent args) - { - var pilot = component.PilotSlot.ContainedEntity; - if (pilot == null) - return; - - args.Entities.Add(pilot.Value); - } - - private void SetupUser(EntityUid mech, EntityUid pilot, MechComponent? component = null) - { - if (!Resolve(mech, ref component)) - return; - - var rider = EnsureComp(pilot); - - // Warning: this bypasses most normal interaction blocking components on the user, like drone laws and the like. - var irelay = EnsureComp(pilot); - - _mover.SetRelay(pilot, mech); - _interaction.SetRelay(pilot, mech, irelay); - rider.Mech = mech; - Dirty(pilot, rider); - - if (_net.IsClient) - return; - - _actions.AddAction(pilot, ref component.MechCycleActionEntity, component.MechCycleAction, mech); - _actions.AddAction(pilot, ref component.MechUiActionEntity, component.MechUiAction, mech); - _actions.AddAction(pilot, ref component.MechEjectActionEntity, component.MechEjectAction, mech); - } - - private void RemoveUser(EntityUid mech, EntityUid pilot) - { - if (!RemComp(pilot)) - return; - RemComp(pilot); - RemComp(pilot); - - _actions.RemoveProvidedActions(pilot, mech); - } - - /// - /// Destroys the mech, removing the user and ejecting all installed equipment. - /// - /// - /// - public virtual void BreakMech(EntityUid uid, MechComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - TryEject(uid, component); - var equipment = new List(component.EquipmentContainer.ContainedEntities); - foreach (var ent in equipment) - { - RemoveEquipment(uid, ent, component, forced: true); - } - - component.Broken = true; - UpdateAppearance(uid, component); - } - - /// - /// Cycles through the currently selected equipment. - /// - /// - /// - public void CycleEquipment(EntityUid uid, MechComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - var allEquipment = component.EquipmentContainer.ContainedEntities.ToList(); - - var equipmentIndex = -1; - if (component.CurrentSelectedEquipment != null) - { - bool StartIndex(EntityUid u) => u == component.CurrentSelectedEquipment; - equipmentIndex = allEquipment.FindIndex(StartIndex); - } - - equipmentIndex++; - component.CurrentSelectedEquipment = equipmentIndex >= allEquipment.Count - ? null - : allEquipment[equipmentIndex]; - - var popupString = component.CurrentSelectedEquipment != null - ? Loc.GetString("mech-equipment-select-popup", ("item", component.CurrentSelectedEquipment)) - : Loc.GetString("mech-equipment-select-none-popup"); - - if (_net.IsServer) - _popup.PopupEntity(popupString, uid); - - Dirty(uid, component); - } - - /// - /// Inserts an equipment item into the mech. - /// - /// - /// - /// - /// - public void InsertEquipment(EntityUid uid, EntityUid toInsert, MechComponent? component = null, - MechEquipmentComponent? equipmentComponent = null) - { - if (!Resolve(uid, ref component)) - return; - - if (!Resolve(toInsert, ref equipmentComponent)) - return; - - if (component.EquipmentContainer.ContainedEntities.Count >= component.MaxEquipmentAmount) - return; - - if (_whitelistSystem.IsWhitelistFail(component.EquipmentWhitelist, toInsert)) - return; - - equipmentComponent.EquipmentOwner = uid; - _container.Insert(toInsert, component.EquipmentContainer); - var ev = new MechEquipmentInsertedEvent(uid); - RaiseLocalEvent(toInsert, ref ev); - UpdateUserInterface(uid, component); - } - - /// - /// Removes an equipment item from a mech. - /// - /// - /// - /// - /// - /// Whether or not the removal can be cancelled - public void RemoveEquipment(EntityUid uid, EntityUid toRemove, MechComponent? component = null, - MechEquipmentComponent? equipmentComponent = null, bool forced = false) - { - if (!Resolve(uid, ref component)) - return; - - if (!Resolve(toRemove, ref equipmentComponent)) - return; - - if (!forced) - { - var attemptev = new AttemptRemoveMechEquipmentEvent(); - RaiseLocalEvent(toRemove, ref attemptev); - if (attemptev.Cancelled) - return; - } - - var ev = new MechEquipmentRemovedEvent(uid); - RaiseLocalEvent(toRemove, ref ev); - - if (component.CurrentSelectedEquipment == toRemove) - CycleEquipment(uid, component); - - equipmentComponent.EquipmentOwner = null; - _container.Remove(toRemove, component.EquipmentContainer); - UpdateUserInterface(uid, component); - } - - /// - /// Attempts to change the amount of energy in the mech. - /// - /// The mech itself - /// The change in energy - /// - /// If the energy was successfully changed. - public virtual bool TryChangeEnergy(EntityUid uid, FixedPoint2 delta, MechComponent? component = null) - { - if (!Resolve(uid, ref component)) - return false; - - if (component.Energy + delta < 0) - return false; - - component.Energy = FixedPoint2.Clamp(component.Energy + delta, 0, component.MaxEnergy); - Dirty(uid, component); - UpdateUserInterface(uid, component); - return true; - } - - /// - /// Sets the integrity of the mech. - /// - /// The mech itself - /// The value the integrity will be set at - /// - public void SetIntegrity(EntityUid uid, FixedPoint2 value, MechComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - component.Integrity = FixedPoint2.Clamp(value, 0, component.MaxIntegrity); - - if (component.Integrity <= 0) - { - BreakMech(uid, component); - } - else if (component.Broken) - { - component.Broken = false; - UpdateAppearance(uid, component); - } - - Dirty(uid, component); - UpdateUserInterface(uid, component); - } - - /// - /// Checks if the pilot is present - /// - /// - /// Whether or not the pilot is present - public bool IsEmpty(MechComponent component) - { - return component.PilotSlot.ContainedEntity == null; - } - - /// - /// Checks if an entity can be inserted into the mech. - /// - /// - /// - /// - /// - public bool CanInsert(EntityUid uid, EntityUid toInsert, MechComponent? component = null) - { - if (!Resolve(uid, ref component)) - return false; - - return IsEmpty(component) && _actionBlocker.CanMove(toInsert); - } - - /// - /// Updates the user interface - /// - /// - /// This is defined here so that UI updates can be accessed from shared. - /// - public virtual void UpdateUserInterface(EntityUid uid, MechComponent? component = null) - { - } - - /// - /// Attempts to insert a pilot into the mech. - /// - /// - /// - /// - /// Whether or not the entity was inserted - public bool TryInsert(EntityUid uid, EntityUid? toInsert, MechComponent? component = null) - { - if (!Resolve(uid, ref component)) - return false; - - if (toInsert == null || component.PilotSlot.ContainedEntity == toInsert) - return false; - - if (!CanInsert(uid, toInsert.Value, component)) - return false; - - SetupUser(uid, toInsert.Value); - _container.Insert(toInsert.Value, component.PilotSlot); - UpdateAppearance(uid, component); - return true; - } - - /// - /// Attempts to eject the current pilot from the mech - /// - /// - /// - /// Whether or not the pilot was ejected. - public bool TryEject(EntityUid uid, MechComponent? component = null) - { - if (!Resolve(uid, ref component)) - return false; - - if (component.PilotSlot.ContainedEntity == null) - return false; - - var pilot = component.PilotSlot.ContainedEntity.Value; - - RemoveUser(uid, pilot); - _container.RemoveEntity(uid, pilot); - UpdateAppearance(uid, component); - return true; - } - - private void OnGetMeleeWeapon(EntityUid uid, MechPilotComponent component, GetMeleeWeaponEvent args) - { - if (args.Handled) - return; - - if (!TryComp(component.Mech, out var mech)) - return; - - var weapon = mech.CurrentSelectedEquipment ?? component.Mech; - args.Weapon = weapon; - args.Handled = true; - } - - private void OnCanAttackFromContainer(EntityUid uid, MechPilotComponent component, CanAttackFromContainerEvent args) - { - args.CanAttack = true; - } - - private void OnAttackAttempt(EntityUid uid, MechPilotComponent component, AttackAttemptEvent args) - { - if (args.Target == component.Mech) - args.Cancel(); - } - - private void UpdateAppearance(EntityUid uid, MechComponent? component = null, - AppearanceComponent? appearance = null) - { - if (!Resolve(uid, ref component, ref appearance, false)) - return; - - _appearance.SetData(uid, MechVisuals.Open, IsEmpty(component), appearance); - _appearance.SetData(uid, MechVisuals.Broken, component.Broken, appearance); - } - - private void OnDragDrop(EntityUid uid, MechComponent component, ref DragDropTargetEvent args) - { - if (args.Handled) - return; - - args.Handled = true; - - var doAfterEventArgs = new DoAfterArgs(EntityManager, args.Dragged, component.EntryDelay, new MechEntryEvent(), uid, target: uid) - { - BreakOnMove = true, - }; - - _doAfter.TryStartDoAfter(doAfterEventArgs); - } - - private void OnCanDragDrop(EntityUid uid, MechComponent component, ref CanDropTargetEvent args) - { - args.Handled = true; - - args.CanDrop |= !component.Broken && CanInsert(uid, args.Dragged, component); - } - -} - -/// -/// Event raised when the battery is successfully removed from the mech, -/// on both success and failure -/// -[Serializable, NetSerializable] -public sealed partial class RemoveBatteryEvent : SimpleDoAfterEvent -{ -} - -/// -/// Event raised when a person removes someone from a mech, -/// on both success and failure -/// -[Serializable, NetSerializable] -public sealed partial class MechExitEvent : SimpleDoAfterEvent -{ -} - -/// -/// Event raised when a person enters a mech, on both success and failure -/// -[Serializable, NetSerializable] -public sealed partial class MechEntryEvent : SimpleDoAfterEvent -{ -} From a97c3d7dcd0b1807581c0f68fbd3e6765a88de90 Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:04:16 +0300 Subject: [PATCH 12/22] Add files via upload --- .../Mech/EntitySystem/SharedMechSystem.cs | 490 ++++++++++++++++++ 1 file changed, 490 insertions(+) create mode 100644 Content.Shared/Mech/EntitySystem/SharedMechSystem.cs diff --git a/Content.Shared/Mech/EntitySystem/SharedMechSystem.cs b/Content.Shared/Mech/EntitySystem/SharedMechSystem.cs new file mode 100644 index 00000000000..97242973152 --- /dev/null +++ b/Content.Shared/Mech/EntitySystem/SharedMechSystem.cs @@ -0,0 +1,490 @@ +using System.Linq; +using Content.Shared.Access.Components; +using Content.Shared.ActionBlocker; +using Content.Shared.Actions; +using Content.Shared.Destructible; +using Content.Shared.DoAfter; +using Content.Shared.DragDrop; +using Content.Shared.Emag.Components; +using Content.Shared.Emag.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Components; +using Content.Shared.Interaction.Events; +using Content.Shared.Mech.Components; +using Content.Shared.Mech.Equipment.Components; +using Content.Shared.Movement.Components; +using Content.Shared.Movement.Systems; +using Content.Shared.Popups; +using Content.Shared.Weapons.Melee; +using Content.Shared.Weapons.Ranged.Events; +using Content.Shared.Whitelist; +using Robust.Shared.Containers; +using Robust.Shared.Network; +using Robust.Shared.Serialization; +using Robust.Shared.Timing; + +namespace Content.Shared.Mech.EntitySystems; + +/// +/// Handles all of the interactions, UI handling, and items shennanigans for +/// +public abstract class SharedMechSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly SharedMoverController _mover = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnToggleEquipmentAction); + SubscribeLocalEvent(OnEjectPilotEvent); + SubscribeLocalEvent(RelayInteractionEvent); + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnDestruction); + SubscribeLocalEvent(OnGetAdditionalAccess); + SubscribeLocalEvent(OnDragDrop); + SubscribeLocalEvent(OnCanDragDrop); + SubscribeLocalEvent(OnEmagged); + + SubscribeLocalEvent(OnGetMeleeWeapon); + SubscribeLocalEvent(OnCanAttackFromContainer); + SubscribeLocalEvent(OnAttackAttempt); + } + + private void OnToggleEquipmentAction(EntityUid uid, MechComponent component, MechToggleEquipmentEvent args) + { + if (args.Handled) + return; + args.Handled = true; + CycleEquipment(uid); + } + + private void OnEjectPilotEvent(EntityUid uid, MechComponent component, MechEjectPilotEvent args) + { + if (args.Handled) + return; + args.Handled = true; + TryEject(uid, component); + } + + private void RelayInteractionEvent(EntityUid uid, MechComponent component, UserActivateInWorldEvent args) + { + var pilot = component.PilotSlot.ContainedEntity; + if (pilot == null) + return; + + // TODO why is this being blocked? + if (!_timing.IsFirstTimePredicted) + return; + + if (component.CurrentSelectedEquipment != null) + { + RaiseLocalEvent(component.CurrentSelectedEquipment.Value, args); + } + } + + private void OnStartup(EntityUid uid, MechComponent component, ComponentStartup args) + { + component.PilotSlot = _container.EnsureContainer(uid, component.PilotSlotId); + component.EquipmentContainer = _container.EnsureContainer(uid, component.EquipmentContainerId); + component.BatterySlot = _container.EnsureContainer(uid, component.BatterySlotId); + UpdateAppearance(uid, component); + } + + private void OnDestruction(EntityUid uid, MechComponent component, DestructionEventArgs args) + { + BreakMech(uid, component); + } + + private void OnGetAdditionalAccess(EntityUid uid, MechComponent component, ref GetAdditionalAccessEvent args) + { + var pilot = component.PilotSlot.ContainedEntity; + if (pilot == null) + return; + + args.Entities.Add(pilot.Value); + } + + private void SetupUser(EntityUid mech, EntityUid pilot, MechComponent? component = null) + { + if (!Resolve(mech, ref component)) + return; + + var rider = EnsureComp(pilot); + + // Warning: this bypasses most normal interaction blocking components on the user, like drone laws and the like. + var irelay = EnsureComp(pilot); + + _mover.SetRelay(pilot, mech); + _interaction.SetRelay(pilot, mech, irelay); + rider.Mech = mech; + Dirty(pilot, rider); + + if (_net.IsClient) + return; + + _actions.AddAction(pilot, ref component.MechCycleActionEntity, component.MechCycleAction, mech); + _actions.AddAction(pilot, ref component.MechUiActionEntity, component.MechUiAction, mech); + _actions.AddAction(pilot, ref component.MechEjectActionEntity, component.MechEjectAction, mech); + } + + private void RemoveUser(EntityUid mech, EntityUid pilot) + { + if (!RemComp(pilot)) + return; + RemComp(pilot); + RemComp(pilot); + + _actions.RemoveProvidedActions(pilot, mech); + } + + /// + /// Destroys the mech, removing the user and ejecting all installed equipment. + /// + /// + /// + public virtual void BreakMech(EntityUid uid, MechComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + TryEject(uid, component); + var equipment = new List(component.EquipmentContainer.ContainedEntities); + foreach (var ent in equipment) + { + RemoveEquipment(uid, ent, component, forced: true); + } + + component.Broken = true; + UpdateAppearance(uid, component); + } + + /// + /// Cycles through the currently selected equipment. + /// + /// + /// + public void CycleEquipment(EntityUid uid, MechComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + var allEquipment = component.EquipmentContainer.ContainedEntities.ToList(); + + var equipmentIndex = -1; + if (component.CurrentSelectedEquipment != null) + { + bool StartIndex(EntityUid u) => u == component.CurrentSelectedEquipment; + equipmentIndex = allEquipment.FindIndex(StartIndex); + } + + equipmentIndex++; + component.CurrentSelectedEquipment = equipmentIndex >= allEquipment.Count + ? null + : allEquipment[equipmentIndex]; + + var popupString = component.CurrentSelectedEquipment != null + ? Loc.GetString("mech-equipment-select-popup", ("item", component.CurrentSelectedEquipment)) + : Loc.GetString("mech-equipment-select-none-popup"); + + if (_net.IsServer) + _popup.PopupEntity(popupString, uid); + + Dirty(uid, component); + } + + /// + /// Inserts an equipment item into the mech. + /// + /// + /// + /// + /// + public void InsertEquipment(EntityUid uid, EntityUid toInsert, MechComponent? component = null, + MechEquipmentComponent? equipmentComponent = null) + { + if (!Resolve(uid, ref component)) + return; + + if (!Resolve(toInsert, ref equipmentComponent)) + return; + + if (component.EquipmentContainer.ContainedEntities.Count >= component.MaxEquipmentAmount) + return; + + if (_whitelistSystem.IsWhitelistFail(component.EquipmentWhitelist, toInsert)) + return; + + equipmentComponent.EquipmentOwner = uid; + _container.Insert(toInsert, component.EquipmentContainer); + var ev = new MechEquipmentInsertedEvent(uid); + RaiseLocalEvent(toInsert, ref ev); + UpdateUserInterface(uid, component); + } + + /// + /// Removes an equipment item from a mech. + /// + /// + /// + /// + /// + /// Whether or not the removal can be cancelled + public void RemoveEquipment(EntityUid uid, EntityUid toRemove, MechComponent? component = null, + MechEquipmentComponent? equipmentComponent = null, bool forced = false) + { + if (!Resolve(uid, ref component)) + return; + + if (!Resolve(toRemove, ref equipmentComponent)) + return; + + if (!forced) + { + var attemptev = new AttemptRemoveMechEquipmentEvent(); + RaiseLocalEvent(toRemove, ref attemptev); + if (attemptev.Cancelled) + return; + } + + var ev = new MechEquipmentRemovedEvent(uid); + RaiseLocalEvent(toRemove, ref ev); + + if (component.CurrentSelectedEquipment == toRemove) + CycleEquipment(uid, component); + + equipmentComponent.EquipmentOwner = null; + _container.Remove(toRemove, component.EquipmentContainer); + UpdateUserInterface(uid, component); + } + + /// + /// Attempts to change the amount of energy in the mech. + /// + /// The mech itself + /// The change in energy + /// + /// If the energy was successfully changed. + public virtual bool TryChangeEnergy(EntityUid uid, FixedPoint2 delta, MechComponent? component = null) + { + if (!Resolve(uid, ref component)) + return false; + + if (component.Energy + delta < 0) + return false; + + component.Energy = FixedPoint2.Clamp(component.Energy + delta, 0, component.MaxEnergy); + Dirty(uid, component); + UpdateUserInterface(uid, component); + return true; + } + + /// + /// Sets the integrity of the mech. + /// + /// The mech itself + /// The value the integrity will be set at + /// + public void SetIntegrity(EntityUid uid, FixedPoint2 value, MechComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + component.Integrity = FixedPoint2.Clamp(value, 0, component.MaxIntegrity); + + if (component.Integrity <= 0) + { + BreakMech(uid, component); + } + else if (component.Broken) + { + component.Broken = false; + UpdateAppearance(uid, component); + } + + Dirty(uid, component); + UpdateUserInterface(uid, component); + } + + /// + /// Checks if the pilot is present + /// + /// + /// Whether or not the pilot is present + public bool IsEmpty(MechComponent component) + { + return component.PilotSlot.ContainedEntity == null; + } + + /// + /// Checks if an entity can be inserted into the mech. + /// + /// + /// + /// + /// + public bool CanInsert(EntityUid uid, EntityUid toInsert, MechComponent? component = null) + { + if (!Resolve(uid, ref component)) + return false; + + return IsEmpty(component) && _actionBlocker.CanMove(toInsert); + } + + /// + /// Updates the user interface + /// + /// + /// This is defined here so that UI updates can be accessed from shared. + /// + public virtual void UpdateUserInterface(EntityUid uid, MechComponent? component = null) + { + } + + /// + /// Attempts to insert a pilot into the mech. + /// + /// + /// + /// + /// Whether or not the entity was inserted + public bool TryInsert(EntityUid uid, EntityUid? toInsert, MechComponent? component = null) + { + if (!Resolve(uid, ref component)) + return false; + + if (toInsert == null || component.PilotSlot.ContainedEntity == toInsert) + return false; + + if (!CanInsert(uid, toInsert.Value, component)) + return false; + + SetupUser(uid, toInsert.Value); + _container.Insert(toInsert.Value, component.PilotSlot); + UpdateAppearance(uid, component); + return true; + } + + /// + /// Attempts to eject the current pilot from the mech + /// + /// + /// + /// Whether or not the pilot was ejected. + public bool TryEject(EntityUid uid, MechComponent? component = null) + { + if (!Resolve(uid, ref component)) + return false; + + if (component.PilotSlot.ContainedEntity == null) + return false; + + var pilot = component.PilotSlot.ContainedEntity.Value; + + RemoveUser(uid, pilot); + _container.RemoveEntity(uid, pilot); + UpdateAppearance(uid, component); + return true; + } + + private void OnGetMeleeWeapon(EntityUid uid, MechPilotComponent component, GetMeleeWeaponEvent args) + { + if (args.Handled) + return; + + if (!TryComp(component.Mech, out var mech)) + return; + + var weapon = mech.CurrentSelectedEquipment ?? component.Mech; + args.Weapon = weapon; + args.Handled = true; + } + + private void OnCanAttackFromContainer(EntityUid uid, MechPilotComponent component, CanAttackFromContainerEvent args) + { + args.CanAttack = true; + } + + private void OnAttackAttempt(EntityUid uid, MechPilotComponent component, AttackAttemptEvent args) + { + if (args.Target == component.Mech) + args.Cancel(); + } + + private void UpdateAppearance(EntityUid uid, MechComponent? component = null, + AppearanceComponent? appearance = null) + { + if (!Resolve(uid, ref component, ref appearance, false)) + return; + + _appearance.SetData(uid, MechVisuals.Open, IsEmpty(component), appearance); + _appearance.SetData(uid, MechVisuals.Broken, component.Broken, appearance); + } + + private void OnDragDrop(EntityUid uid, MechComponent component, ref DragDropTargetEvent args) + { + if (args.Handled) + return; + + args.Handled = true; + + var doAfterEventArgs = new DoAfterArgs(EntityManager, args.Dragged, component.EntryDelay, new MechEntryEvent(), uid, target: uid) + { + BreakOnMove = true, + }; + + _doAfter.TryStartDoAfter(doAfterEventArgs); + } + + private void OnCanDragDrop(EntityUid uid, MechComponent component, ref CanDropTargetEvent args) + { + args.Handled = true; + + args.CanDrop |= !component.Broken && CanInsert(uid, args.Dragged, component); + } + + private void OnEmagged(EntityUid uid, MechComponent component, ref GotEmaggedEvent args) + { + if (!component.BreakOnEmag) + return; + args.Handled = true; + component.EquipmentWhitelist = null; + Dirty(uid, component); + } +} + +/// +/// Event raised when the battery is successfully removed from the mech, +/// on both success and failure +/// +[Serializable, NetSerializable] +public sealed partial class RemoveBatteryEvent : SimpleDoAfterEvent +{ +} + +/// +/// Event raised when a person removes someone from a mech, +/// on both success and failure +/// +[Serializable, NetSerializable] +public sealed partial class MechExitEvent : SimpleDoAfterEvent +{ +} + +/// +/// Event raised when a person enters a mech, on both success and failure +/// +[Serializable, NetSerializable] +public sealed partial class MechEntryEvent : SimpleDoAfterEvent +{ +} From d9e1ca552e68cbd2c26b003fd39d549ad927cb69 Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:07:22 +0300 Subject: [PATCH 13/22] Delete Content.Shared/Weapons/Ranged/Components/GunComponent.cs --- .../Weapons/Ranged/Components/GunComponent.cs | 282 ------------------ 1 file changed, 282 deletions(-) delete mode 100644 Content.Shared/Weapons/Ranged/Components/GunComponent.cs diff --git a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs deleted file mode 100644 index bcc56c28ab1..00000000000 --- a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs +++ /dev/null @@ -1,282 +0,0 @@ -using System.Numerics; -using Content.Shared.Weapons.Ranged.Events; -using Content.Shared.Weapons.Ranged.Systems; -using Robust.Shared.Audio; -using Robust.Shared.GameStates; -using Robust.Shared.Map; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; - -namespace Content.Shared.Weapons.Ranged.Components; - -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] -[Access(typeof(SharedGunSystem))] -public sealed partial class GunComponent : Component -{ - #region Sound - - /// - /// The base sound to use when the gun is fired. - /// - [DataField] - public SoundSpecifier? SoundGunshot = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/smg.ogg"); - - /// - /// The sound to use when the gun is fired. - /// - /// - [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] - public SoundSpecifier? SoundGunshotModified; - - [DataField] - public SoundSpecifier? SoundEmpty = new SoundPathSpecifier("/Audio/Weapons/Guns/Empty/empty.ogg"); - - /// - /// Sound played when toggling the for this gun. - /// - [DataField] - public SoundSpecifier? SoundMode = new SoundPathSpecifier("/Audio/Weapons/Guns/Misc/selector.ogg"); - - #endregion - - #region Recoil - - // These values are very small for now until we get a debug overlay and fine tune it - - /// - /// The base scalar value applied to the vector governing camera recoil. - /// - [DataField, AutoNetworkedField] - public float CameraRecoilScalar = 1f; - - /// - /// A scalar value applied to the vector governing camera recoil. - /// If 0, there will be no camera recoil. - /// - /// - [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] - public float CameraRecoilScalarModified = 1f; - - /// - /// Last time the gun fired. - /// Used for recoil purposes. - /// - [DataField] - public TimeSpan LastFire = TimeSpan.Zero; - - /// - /// What the current spread is for shooting. This gets changed every time the gun fires. - /// - [DataField] - [AutoNetworkedField] - public Angle CurrentAngle; - - /// - /// The base value for how much the spread increases every time the gun fires. - /// - [DataField] - public Angle AngleIncrease = Angle.FromDegrees(0.5); - - /// - /// How much the spread increases every time the gun fires. - /// - /// - [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] - public Angle AngleIncreaseModified; - - /// - /// The base value for how much the decreases per second. - /// - [DataField] - public Angle AngleDecay = Angle.FromDegrees(4); - - /// - /// How much the decreases per second. - /// - /// - [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] - public Angle AngleDecayModified; - - /// - /// The base value for the maximum angle allowed for - /// - [DataField] - [AutoNetworkedField] - public Angle MaxAngle = Angle.FromDegrees(2); - - /// - /// The maximum angle allowed for - /// - /// - [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] - public Angle MaxAngleModified; - - /// - /// The base value for the minimum angle allowed for - /// - [DataField] - [AutoNetworkedField] - public Angle MinAngle = Angle.FromDegrees(1); - - /// - /// The minimum angle allowed for . - /// - /// - [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] - public Angle MinAngleModified; - - #endregion - - /// - /// Whether this gun is shot via the use key or the alt-use key. - /// - [DataField, AutoNetworkedField] - public bool UseKey = true; - - /// - /// Where the gun is being requested to shoot. - /// - [ViewVariables] - public EntityCoordinates? ShootCoordinates = null; - - /// - /// Who the gun is being requested to shoot at directly. - /// - [ViewVariables] - public EntityUid? Target = null; - - /// - /// The base value for how many shots to fire per burst. - /// - [DataField, AutoNetworkedField] - public int ShotsPerBurst = 3; - - /// - /// How many shots to fire per burst. - /// - /// - [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] - public int ShotsPerBurstModified = 3; - - /// - /// How long time must pass between burstfire shots. - /// - [DataField, AutoNetworkedField] - public float BurstCooldown = 0.25f; - - /// - /// The fire rate of the weapon in burst fire mode. - /// - [DataField, AutoNetworkedField] - public float BurstFireRate = 8f; - - /// - /// Whether the burst fire mode has been activated. - /// - [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] - public bool BurstActivated = false; - - /// - /// The burst fire bullet count. - /// - [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] - public int BurstShotsCount = 0; - - /// - /// Used for tracking semi-auto / burst - /// - [ViewVariables] - [AutoNetworkedField] - public int ShotCounter = 0; - - /// - /// The base value for how many times it shoots per second. - /// - [DataField] - [AutoNetworkedField] - public float FireRate = 8f; - - /// - /// How many times it shoots per second. - /// - /// - [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] - public float FireRateModified; - - /// - /// Starts fire cooldown when equipped if true. - /// - [DataField] - public bool ResetOnHandSelected = true; - - /// - /// The base value for how fast the projectile moves. - /// - [DataField] - public float ProjectileSpeed = 25f; - - /// - /// How fast the projectile moves. - /// - /// - [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] - public float ProjectileSpeedModified; - - /// - /// When the gun is next available to be shot. - /// Can be set multiple times in a single tick due to guns firing faster than a single tick time. - /// - [DataField(customTypeSerializer:typeof(TimeOffsetSerializer))] - [AutoNetworkedField] - [AutoPausedField] - public TimeSpan NextFire = TimeSpan.Zero; - - /// - /// What firemodes can be selected. - /// - [DataField] - [AutoNetworkedField] - public SelectiveFire AvailableModes = SelectiveFire.SemiAuto; - - /// - /// What firemode is currently selected. - /// - [DataField] - [AutoNetworkedField] - public SelectiveFire SelectedMode = SelectiveFire.SemiAuto; - - /// - /// Whether or not information about - /// the gun will be shown on examine. - /// - [DataField] - public bool ShowExamineText = true; - - /// - /// Whether or not someone with the - /// clumsy trait can shoot this - /// - [DataField] - public bool ClumsyProof = false; - - /// - /// Firing direction for an item not being held (e.g. shuttle cannons, thrown guns still firing). - /// - [DataField] - public Vector2 DefaultDirection = new Vector2(0, -1); - - /// - /// Corvax-Next. The percentage chance of a given gun to accidentally discharge if violently thrown into a wall or person - /// - [DataField] - public float FireOnDropChance = 0.1f; -} - -[Flags] -public enum SelectiveFire : byte -{ - Invalid = 0, - // Combat mode already functions as the equivalent of Safety - SemiAuto = 1 << 0, - Burst = 1 << 1, - FullAuto = 1 << 2, // Not in the building! -} From 7ceb5f5d86a394944da1ae1d006be4b129eb2f79 Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:07:37 +0300 Subject: [PATCH 14/22] Add files via upload --- .../Weapons/Ranged/Components/GunComponent.cs | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 Content.Shared/Weapons/Ranged/Components/GunComponent.cs diff --git a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs new file mode 100644 index 00000000000..e6c46ec7653 --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs @@ -0,0 +1,245 @@ +using Content.Shared.Weapons.Ranged.Events; +using Content.Shared.Weapons.Ranged.Systems; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Map; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Weapons.Ranged.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[Access(typeof(SharedGunSystem))] +public sealed partial class GunComponent : Component +{ + #region Sound + + /// + /// The base sound to use when the gun is fired. + /// + [DataField] + public SoundSpecifier? SoundGunshot = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/smg.ogg"); + + /// + /// The sound to use when the gun is fired. + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public SoundSpecifier? SoundGunshotModified; + + [DataField] + public SoundSpecifier? SoundEmpty = new SoundPathSpecifier("/Audio/Weapons/Guns/Empty/empty.ogg"); + + /// + /// Sound played when toggling the for this gun. + /// + [DataField] + public SoundSpecifier? SoundMode = new SoundPathSpecifier("/Audio/Weapons/Guns/Misc/selector.ogg"); + + #endregion + + #region Recoil + + // These values are very small for now until we get a debug overlay and fine tune it + + /// + /// The base scalar value applied to the vector governing camera recoil. + /// + [DataField, AutoNetworkedField] + public float CameraRecoilScalar = 1f; + + /// + /// A scalar value applied to the vector governing camera recoil. + /// If 0, there will be no camera recoil. + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public float CameraRecoilScalarModified = 1f; + + /// + /// Last time the gun fired. + /// Used for recoil purposes. + /// + [DataField] + public TimeSpan LastFire = TimeSpan.Zero; + + /// + /// What the current spread is for shooting. This gets changed every time the gun fires. + /// + [DataField] + [AutoNetworkedField] + public Angle CurrentAngle; + + /// + /// The base value for how much the spread increases every time the gun fires. + /// + [DataField] + public Angle AngleIncrease = Angle.FromDegrees(0.5); + + /// + /// How much the spread increases every time the gun fires. + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public Angle AngleIncreaseModified; + + /// + /// The base value for how much the decreases per second. + /// + [DataField] + public Angle AngleDecay = Angle.FromDegrees(4); + + /// + /// How much the decreases per second. + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public Angle AngleDecayModified; + + /// + /// The base value for the maximum angle allowed for + /// + [DataField] + [AutoNetworkedField] + public Angle MaxAngle = Angle.FromDegrees(2); + + /// + /// The maximum angle allowed for + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public Angle MaxAngleModified; + + /// + /// The base value for the minimum angle allowed for + /// + [DataField] + [AutoNetworkedField] + public Angle MinAngle = Angle.FromDegrees(1); + + /// + /// The minimum angle allowed for . + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public Angle MinAngleModified; + + #endregion + + /// + /// Whether this gun is shot via the use key or the alt-use key. + /// + [DataField, AutoNetworkedField] + public bool UseKey = true; + + /// + /// Where the gun is being requested to shoot. + /// + [ViewVariables] + public EntityCoordinates? ShootCoordinates = null; + + /// + /// Who the gun is being requested to shoot at directly. + /// + [ViewVariables] + public EntityUid? Target = null; + + /// + /// The base value for how many shots to fire per burst. + /// + [DataField, AutoNetworkedField] + public int ShotsPerBurst = 3; + + /// + /// How many shots to fire per burst. + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public int ShotsPerBurstModified = 3; + + /// + /// Used for tracking semi-auto / burst + /// + [ViewVariables] + [AutoNetworkedField] + public int ShotCounter = 0; + + /// + /// The base value for how many times it shoots per second. + /// + [DataField] + [AutoNetworkedField] + public float FireRate = 8f; + + /// + /// How many times it shoots per second. + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public float FireRateModified; + + /// + /// Starts fire cooldown when equipped if true. + /// + [DataField] + public bool ResetOnHandSelected = true; + + /// + /// The base value for how fast the projectile moves. + /// + [DataField] + public float ProjectileSpeed = 25f; + + /// + /// How fast the projectile moves. + /// + /// + [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public float ProjectileSpeedModified; + + /// + /// When the gun is next available to be shot. + /// Can be set multiple times in a single tick due to guns firing faster than a single tick time. + /// + [DataField(customTypeSerializer:typeof(TimeOffsetSerializer))] + [AutoNetworkedField] + [AutoPausedField] + public TimeSpan NextFire = TimeSpan.Zero; + + /// + /// What firemodes can be selected. + /// + [DataField] + [AutoNetworkedField] + public SelectiveFire AvailableModes = SelectiveFire.SemiAuto; + + /// + /// What firemode is currently selected. + /// + [DataField] + [AutoNetworkedField] + public SelectiveFire SelectedMode = SelectiveFire.SemiAuto; + + /// + /// Whether or not information about + /// the gun will be shown on examine. + /// + [DataField] + public bool ShowExamineText = true; + + /// + /// Whether or not someone with the + /// clumsy trait can shoot this + /// + [DataField] + public bool ClumsyProof = false; +} + +[Flags] +public enum SelectiveFire : byte +{ + Invalid = 0, + // Combat mode already functions as the equivalent of Safety + SemiAuto = 1 << 0, + Burst = 1 << 1, + FullAuto = 1 << 2, // Not in the building! +} From 797a4268abde211dbc8e0ffde3812293640f0072 Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:08:42 +0300 Subject: [PATCH 15/22] Delete Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs --- .../Weapons/Ranged/Systems/SharedGunSystem.cs | 610 ------------------ 1 file changed, 610 deletions(-) delete mode 100644 Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs deleted file mode 100644 index 0ab3f1e9989..00000000000 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs +++ /dev/null @@ -1,610 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Numerics; -using Content.Shared.ActionBlocker; -using Content.Shared.Actions; -using Content.Shared.Administration.Logs; -using Content.Shared.Audio; -using Content.Shared.CombatMode; -using Content.Shared.Containers.ItemSlots; -using Content.Shared.Damage; -using Content.Shared.Examine; -using Content.Shared.Gravity; -using Content.Shared.Hands; -using Content.Shared.Hands.Components; -using Content.Shared.Popups; -using Content.Shared.Projectiles; -using Content.Shared.Tag; -using Content.Shared.Throwing; -using Content.Shared.Timing; -using Content.Shared.Verbs; -using Content.Shared.Weapons.Melee; -using Content.Shared.Weapons.Melee.Events; -using Content.Shared.Weapons.Ranged.Components; -using Content.Shared.Weapons.Ranged.Events; -using Content.Shared.Whitelist; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Containers; -using Robust.Shared.Map; -using Robust.Shared.Network; -using Robust.Shared.Physics.Components; -using Robust.Shared.Physics.Systems; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using Robust.Shared.Serialization; -using Robust.Shared.Timing; -using Robust.Shared.Utility; - -namespace Content.Shared.Weapons.Ranged.Systems; - -public abstract partial class SharedGunSystem : EntitySystem -{ - [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; - [Dependency] protected readonly IGameTiming Timing = default!; - [Dependency] protected readonly IMapManager MapManager = default!; - [Dependency] private readonly INetManager _netManager = default!; - [Dependency] protected readonly IPrototypeManager ProtoManager = default!; - [Dependency] protected readonly IRobustRandom Random = default!; - [Dependency] protected readonly ISharedAdminLogManager Logs = default!; - [Dependency] protected readonly DamageableSystem Damageable = default!; - [Dependency] protected readonly ExamineSystemShared Examine = default!; - [Dependency] private readonly ItemSlotsSystem _slots = default!; - [Dependency] private readonly RechargeBasicEntityAmmoSystem _recharge = default!; - [Dependency] protected readonly SharedActionsSystem Actions = default!; - [Dependency] protected readonly SharedAppearanceSystem Appearance = default!; - [Dependency] protected readonly SharedAudioSystem Audio = default!; - [Dependency] private readonly SharedCombatModeSystem _combatMode = default!; - [Dependency] protected readonly SharedContainerSystem Containers = default!; - [Dependency] private readonly SharedGravitySystem _gravity = default!; - [Dependency] protected readonly SharedPointLightSystem Lights = default!; - [Dependency] protected readonly SharedPopupSystem PopupSystem = default!; - [Dependency] protected readonly SharedPhysicsSystem Physics = default!; - [Dependency] protected readonly SharedProjectileSystem Projectiles = default!; - [Dependency] protected readonly SharedTransformSystem TransformSystem = default!; - [Dependency] protected readonly TagSystem TagSystem = default!; - [Dependency] protected readonly ThrowingSystem ThrowingSystem = default!; - [Dependency] private readonly UseDelaySystem _useDelay = default!; - [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; - - private const float InteractNextFire = 0.3f; - private const double SafetyNextFire = 0.5; - private const float EjectOffset = 0.4f; - protected const string AmmoExamineColor = "yellow"; - protected const string FireRateExamineColor = "yellow"; - public const string ModeExamineColor = "cyan"; - - public override void Initialize() - { - SubscribeAllEvent(OnShootRequest); - SubscribeAllEvent(OnStopShootRequest); - SubscribeLocalEvent(OnGunMelee); - - // Ammo providers - InitializeBallistic(); - InitializeBattery(); - InitializeCartridge(); - InitializeChamberMagazine(); - InitializeMagazine(); - InitializeRevolver(); - InitializeBasicEntity(); - InitializeClothing(); - InitializeContainer(); - InitializeSolution(); - - // Interactions - SubscribeLocalEvent>(OnAltVerb); - SubscribeLocalEvent(OnExamine); - SubscribeLocalEvent(OnCycleMode); - SubscribeLocalEvent(OnGunSelected); - SubscribeLocalEvent(OnMapInit); - } - - private void OnMapInit(Entity gun, ref MapInitEvent args) - { -#if DEBUG - if (gun.Comp.NextFire > Timing.CurTime) - Log.Warning($"Initializing a map that contains an entity that is on cooldown. Entity: {ToPrettyString(gun)}"); - - DebugTools.Assert((gun.Comp.AvailableModes & gun.Comp.SelectedMode) != 0x0); -#endif - - RefreshModifiers((gun, gun)); - } - - private void OnGunMelee(EntityUid uid, GunComponent component, MeleeHitEvent args) - { - if (!TryComp(uid, out var melee)) - return; - - if (melee.NextAttack > component.NextFire) - { - component.NextFire = melee.NextAttack; - Dirty(uid, component); - } - } - - private void OnShootRequest(RequestShootEvent msg, EntitySessionEventArgs args) - { - var user = args.SenderSession.AttachedEntity; - - if (user == null || - !_combatMode.IsInCombatMode(user) || - !TryGetGun(user.Value, out var ent, out var gun)) - { - return; - } - - if (ent != GetEntity(msg.Gun)) - return; - - gun.ShootCoordinates = GetCoordinates(msg.Coordinates); - gun.Target = GetEntity(msg.Target); - AttemptShoot(user.Value, ent, gun); - } - - private void OnStopShootRequest(RequestStopShootEvent ev, EntitySessionEventArgs args) - { - var gunUid = GetEntity(ev.Gun); - - if (args.SenderSession.AttachedEntity == null || - !TryComp(gunUid, out var gun) || - !TryGetGun(args.SenderSession.AttachedEntity.Value, out _, out var userGun)) - { - return; - } - - if (userGun != gun) - return; - - StopShooting(gunUid, gun); - } - - public bool CanShoot(GunComponent component) - { - if (component.NextFire > Timing.CurTime) - return false; - - return true; - } - - public bool TryGetGun(EntityUid entity, out EntityUid gunEntity, [NotNullWhen(true)] out GunComponent? gunComp) - { - gunEntity = default; - gunComp = null; - - if (EntityManager.TryGetComponent(entity, out HandsComponent? hands) && - hands.ActiveHandEntity is { } held && - TryComp(held, out GunComponent? gun)) - { - gunEntity = held; - gunComp = gun; - return true; - } - - // Last resort is check if the entity itself is a gun. - if (TryComp(entity, out gun)) - { - gunEntity = entity; - gunComp = gun; - return true; - } - - return false; - } - - private void StopShooting(EntityUid uid, GunComponent gun) - { - if (gun.ShotCounter == 0) - return; - - gun.ShotCounter = 0; - gun.ShootCoordinates = null; - gun.Target = null; - Dirty(uid, gun); - } - - /// - /// Attempts to shoot at the target coordinates. Resets the shot counter after every shot. - /// - public void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun, EntityCoordinates toCoordinates) - { - gun.ShootCoordinates = toCoordinates; - AttemptShoot(user, gunUid, gun); - gun.ShotCounter = 0; - } - - /// - /// Shoots by assuming the gun is the user at default coordinates. - /// - public void AttemptShoot(EntityUid gunUid, GunComponent gun) - { - var coordinates = new EntityCoordinates(gunUid, gun.DefaultDirection); - gun.ShootCoordinates = coordinates; - AttemptShoot(gunUid, gunUid, gun); - gun.ShotCounter = 0; - } - - /// - /// Sets the targeted entity of the gun. Should be called before attempting to shoot to avoid shooting over the target. - /// - public void SetTarget(GunComponent gun, EntityUid target) - { - gun.Target = target; - } - - private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun) - { - if (gun.FireRateModified <= 0f || - !_actionBlockerSystem.CanAttack(user)) - return; - - var toCoordinates = gun.ShootCoordinates; - - if (toCoordinates == null) - return; - - var curTime = Timing.CurTime; - - // check if anything wants to prevent shooting - var prevention = new ShotAttemptedEvent - { - User = user, - Used = (gunUid, gun) - }; - RaiseLocalEvent(gunUid, ref prevention); - if (prevention.Cancelled) - return; - - RaiseLocalEvent(user, ref prevention); - if (prevention.Cancelled) - return; - - // Need to do this to play the clicking sound for empty automatic weapons - // but not play anything for burst fire. - if (gun.NextFire > curTime) - return; - - var fireRate = TimeSpan.FromSeconds(1f / gun.FireRateModified); - - if (gun.SelectedMode == SelectiveFire.Burst || gun.BurstActivated) - fireRate = TimeSpan.FromSeconds(1f / gun.BurstFireRate); - - // First shot - // Previously we checked shotcounter but in some cases all the bullets got dumped at once - // curTime - fireRate is insufficient because if you time it just right you can get a 3rd shot out slightly quicker. - if (gun.NextFire < curTime - fireRate || gun.ShotCounter == 0 && gun.NextFire < curTime) - gun.NextFire = curTime; - - var shots = 0; - var lastFire = gun.NextFire; - - while (gun.NextFire <= curTime) - { - gun.NextFire += fireRate; - shots++; - } - - // NextFire has been touched regardless so need to dirty the gun. - Dirty(gunUid, gun); - - // Get how many shots we're actually allowed to make, due to clip size or otherwise. - // Don't do this in the loop so we still reset NextFire. - if (!gun.BurstActivated) - { - switch (gun.SelectedMode) - { - case SelectiveFire.SemiAuto: - shots = Math.Min(shots, 1 - gun.ShotCounter); - break; - case SelectiveFire.Burst: - shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter); - break; - case SelectiveFire.FullAuto: - break; - default: - throw new ArgumentOutOfRangeException($"No implemented shooting behavior for {gun.SelectedMode}!"); - } - } else - { - shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter); - } - - var attemptEv = new AttemptShootEvent(user, null); - RaiseLocalEvent(gunUid, ref attemptEv); - - if (attemptEv.Cancelled) - { - if (attemptEv.Message != null) - { - PopupSystem.PopupClient(attemptEv.Message, gunUid, user); - } - gun.BurstActivated = false; - gun.BurstShotsCount = 0; - gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds)); - return; - } - - var fromCoordinates = Transform(user).Coordinates; - // Remove ammo - var ev = new TakeAmmoEvent(shots, new List<(EntityUid? Entity, IShootable Shootable)>(), fromCoordinates, user); - - // Listen it just makes the other code around it easier if shots == 0 to do this. - if (shots > 0) - RaiseLocalEvent(gunUid, ev); - - DebugTools.Assert(ev.Ammo.Count <= shots); - DebugTools.Assert(shots >= 0); - UpdateAmmoCount(gunUid); - - // Even if we don't actually shoot update the ShotCounter. This is to avoid spamming empty sounds - // where the gun may be SemiAuto or Burst. - gun.ShotCounter += shots; - - if (ev.Ammo.Count <= 0) - { - // triggers effects on the gun if it's empty - var emptyGunShotEvent = new OnEmptyGunShotEvent(); - RaiseLocalEvent(gunUid, ref emptyGunShotEvent); - - gun.BurstActivated = false; - gun.BurstShotsCount = 0; - gun.NextFire += TimeSpan.FromSeconds(gun.BurstCooldown); - - // Play empty gun sounds if relevant - // If they're firing an existing clip then don't play anything. - if (shots > 0) - { - if (ev.Reason != null && Timing.IsFirstTimePredicted) - { - PopupSystem.PopupCursor(ev.Reason); - } - - // Don't spam safety sounds at gun fire rate, play it at a reduced rate. - // May cause prediction issues? Needs more tweaking - gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds)); - Audio.PlayPredicted(gun.SoundEmpty, gunUid, user); - return; - } - - return; - } - - // Handle burstfire - if (gun.SelectedMode == SelectiveFire.Burst) - { - gun.BurstActivated = true; - } - if (gun.BurstActivated) - { - gun.BurstShotsCount += shots; - if (gun.BurstShotsCount >= gun.ShotsPerBurstModified) - { - gun.NextFire += TimeSpan.FromSeconds(gun.BurstCooldown); - gun.BurstActivated = false; - gun.BurstShotsCount = 0; - } - } - - // Shoot confirmed - sounds also played here in case it's invalid (e.g. cartridge already spent). - Shoot(gunUid, gun, ev.Ammo, fromCoordinates, toCoordinates.Value, out var userImpulse, user, throwItems: attemptEv.ThrowItems); - var shotEv = new GunShotEvent(user, ev.Ammo); - RaiseLocalEvent(gunUid, ref shotEv); - - if (userImpulse && TryComp(user, out var userPhysics)) - { - if (_gravity.IsWeightless(user, userPhysics)) - CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics); - } - - Dirty(gunUid, gun); - } - - public void Shoot( - EntityUid gunUid, - GunComponent gun, - EntityUid ammo, - EntityCoordinates fromCoordinates, - EntityCoordinates toCoordinates, - out bool userImpulse, - EntityUid? user = null, - bool throwItems = false) - { - var shootable = EnsureShootable(ammo); - Shoot(gunUid, gun, new List<(EntityUid? Entity, IShootable Shootable)>(1) { (ammo, shootable) }, fromCoordinates, toCoordinates, out userImpulse, user, throwItems); - } - - public abstract 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); - - public void ShootProjectile(EntityUid uid, Vector2 direction, Vector2 gunVelocity, EntityUid gunUid, EntityUid? user = null, float speed = 20f) - { - var physics = EnsureComp(uid); - Physics.SetBodyStatus(uid, physics, BodyStatus.InAir); - - var targetMapVelocity = gunVelocity + direction.Normalized() * speed; - var currentMapVelocity = Physics.GetMapLinearVelocity(uid, physics); - var finalLinear = physics.LinearVelocity + targetMapVelocity - currentMapVelocity; - Physics.SetLinearVelocity(uid, finalLinear, body: physics); - - var projectile = EnsureComp(uid); - Projectiles.SetShooter(uid, projectile, user ?? gunUid); - projectile.Weapon = gunUid; - - TransformSystem.SetWorldRotation(uid, direction.ToWorldAngle() + projectile.Angle); - } - - protected abstract void Popup(string message, EntityUid? uid, EntityUid? user); - - /// - /// Call this whenever the ammo count for a gun changes. - /// - protected virtual void UpdateAmmoCount(EntityUid uid, bool prediction = true) {} - - protected void SetCartridgeSpent(EntityUid uid, CartridgeAmmoComponent cartridge, bool spent) - { - if (cartridge.Spent != spent) - Dirty(uid, cartridge); - - cartridge.Spent = spent; - Appearance.SetData(uid, AmmoVisuals.Spent, spent); - } - - /// - /// Drops a single cartridge / shell - /// - protected void EjectCartridge( - EntityUid entity, - Angle? angle = null, - bool playSound = true) - { - // TODO: Sound limit version. - var offsetPos = Random.NextVector2(EjectOffset); - var xform = Transform(entity); - - var coordinates = xform.Coordinates; - coordinates = coordinates.Offset(offsetPos); - - TransformSystem.SetLocalRotation(xform, Random.NextAngle()); - TransformSystem.SetCoordinates(entity, xform, coordinates); - - // decides direction the casing ejects and only when not cycling - if (angle != null) - { - Angle ejectAngle = angle.Value; - ejectAngle += 3.7f; // 212 degrees; casings should eject slightly to the right and behind of a gun - ThrowingSystem.TryThrow(entity, ejectAngle.ToVec().Normalized() / 100, 5f); - } - if (playSound && TryComp(entity, out var cartridge)) - { - Audio.PlayPvs(cartridge.EjectSound, entity, AudioParams.Default.WithVariation(SharedContentAudioSystem.DefaultVariation).WithVolume(-1f)); - } - } - - protected IShootable EnsureShootable(EntityUid uid) - { - if (TryComp(uid, out var cartridge)) - return cartridge; - - return EnsureComp(uid); - } - - protected void RemoveShootable(EntityUid uid) - { - RemCompDeferred(uid); - RemCompDeferred(uid); - } - - protected void MuzzleFlash(EntityUid gun, AmmoComponent component, Angle worldAngle, EntityUid? user = null) - { - var attemptEv = new GunMuzzleFlashAttemptEvent(); - RaiseLocalEvent(gun, ref attemptEv); - if (attemptEv.Cancelled) - return; - - var sprite = component.MuzzleFlash; - - if (sprite == null) - return; - - var ev = new MuzzleFlashEvent(GetNetEntity(gun), sprite, worldAngle); - CreateEffect(gun, ev, gun); - } - - public void CauseImpulse(EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, EntityUid user, PhysicsComponent userPhysics) - { - var fromMap = fromCoordinates.ToMapPos(EntityManager, TransformSystem); - var toMap = toCoordinates.ToMapPos(EntityManager, TransformSystem); - var shotDirection = (toMap - fromMap).Normalized(); - - const float impulseStrength = 25.0f; - var impulseVector = shotDirection * impulseStrength; - Physics.ApplyLinearImpulse(user, -impulseVector, body: userPhysics); - } - - public void RefreshModifiers(Entity gun) - { - if (!Resolve(gun, ref gun.Comp)) - return; - - var comp = gun.Comp; - var ev = new GunRefreshModifiersEvent( - (gun, comp), - comp.SoundGunshot, - comp.CameraRecoilScalar, - comp.AngleIncrease, - comp.AngleDecay, - comp.MaxAngle, - comp.MinAngle, - comp.ShotsPerBurst, - comp.FireRate, - comp.ProjectileSpeed - ); - - RaiseLocalEvent(gun, ref ev); - - comp.SoundGunshotModified = ev.SoundGunshot; - comp.CameraRecoilScalarModified = ev.CameraRecoilScalar; - comp.AngleIncreaseModified = ev.AngleIncrease; - comp.AngleDecayModified = ev.AngleDecay; - comp.MaxAngleModified = ev.MaxAngle; - comp.MinAngleModified = ev.MinAngle; - comp.ShotsPerBurstModified = ev.ShotsPerBurst; - comp.FireRateModified = ev.FireRate; - comp.ProjectileSpeedModified = ev.ProjectileSpeed; - - Dirty(gun); - } - - protected abstract void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, EntityUid? user = null); - - /// - /// Used for animated effects on the client. - /// - [Serializable, NetSerializable] - public sealed class HitscanEvent : EntityEventArgs - { - public List<(NetCoordinates coordinates, Angle angle, SpriteSpecifier Sprite, float Distance)> Sprites = new(); - } -} - -/// -/// Raised directed on the gun before firing to see if the shot should go through. -/// -/// -/// Handling this in server exclusively will lead to mispredicts. -/// -/// The user that attempted to fire this gun. -/// Set this to true if the shot should be cancelled. -/// Set this to true if the ammo shouldn't actually be fired, just thrown. -[ByRefEvent] -public record struct AttemptShootEvent(EntityUid User, string? Message, bool Cancelled = false, bool ThrowItems = false); - -/// -/// Raised directed on the gun after firing. -/// -/// The user that fired this gun. -[ByRefEvent] -public record struct GunShotEvent(EntityUid User, List<(EntityUid? Uid, IShootable Shootable)> Ammo); - -public enum EffectLayers : byte -{ - Unshaded, -} - -[Serializable, NetSerializable] -public enum AmmoVisuals : byte -{ - Spent, - AmmoCount, - AmmoMax, - HasAmmo, // used for generic visualizers. c# stuff can just check ammocount != 0 - MagLoaded, - BoltClosed, -} From 1d3eb4cc5c535e50a53ab97118efd426d66131bb Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:08:53 +0300 Subject: [PATCH 16/22] Add files via upload --- .../Weapons/Ranged/Systems/SharedGunSystem.cs | 593 ++++++++++++++++++ 1 file changed, 593 insertions(+) create mode 100644 Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs new file mode 100644 index 00000000000..94c736f32fa --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs @@ -0,0 +1,593 @@ +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using Content.Shared.ActionBlocker; +using Content.Shared.Actions; +using Content.Shared.Administration.Logs; +using Content.Shared.Audio; +using Content.Shared.CombatMode; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Damage; +using Content.Shared.Examine; +using Content.Shared.Gravity; +using Content.Shared.Hands; +using Content.Shared.Hands.Components; +using Content.Shared.Mech.Components; +using Content.Shared.Popups; +using Content.Shared.Projectiles; +using Content.Shared.Tag; +using Content.Shared.Throwing; +using Content.Shared.Timing; +using Content.Shared.Verbs; +using Content.Shared.Weapons.Melee; +using Content.Shared.Weapons.Melee.Events; +using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Events; +using Content.Shared.Whitelist; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Containers; +using Robust.Shared.Map; +using Robust.Shared.Network; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Serialization; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Shared.Weapons.Ranged.Systems; + +public abstract partial class SharedGunSystem : EntitySystem +{ + [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; + [Dependency] protected readonly IGameTiming Timing = default!; + [Dependency] protected readonly IMapManager MapManager = default!; + [Dependency] private readonly INetManager _netManager = default!; + [Dependency] protected readonly IPrototypeManager ProtoManager = default!; + [Dependency] protected readonly IRobustRandom Random = default!; + [Dependency] protected readonly ISharedAdminLogManager Logs = default!; + [Dependency] protected readonly DamageableSystem Damageable = default!; + [Dependency] protected readonly ExamineSystemShared Examine = default!; + [Dependency] private readonly ItemSlotsSystem _slots = default!; + [Dependency] private readonly RechargeBasicEntityAmmoSystem _recharge = default!; + [Dependency] protected readonly SharedActionsSystem Actions = default!; + [Dependency] protected readonly SharedAppearanceSystem Appearance = default!; + [Dependency] protected readonly SharedAudioSystem Audio = default!; + [Dependency] private readonly SharedCombatModeSystem _combatMode = default!; + [Dependency] protected readonly SharedContainerSystem Containers = default!; + [Dependency] private readonly SharedGravitySystem _gravity = default!; + [Dependency] protected readonly SharedPointLightSystem Lights = default!; + [Dependency] protected readonly SharedPopupSystem PopupSystem = default!; + [Dependency] protected readonly SharedPhysicsSystem Physics = default!; + [Dependency] protected readonly SharedProjectileSystem Projectiles = default!; + [Dependency] protected readonly SharedTransformSystem TransformSystem = default!; + [Dependency] protected readonly TagSystem TagSystem = default!; + [Dependency] protected readonly ThrowingSystem ThrowingSystem = default!; + [Dependency] private readonly UseDelaySystem _useDelay = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + + private const float InteractNextFire = 0.3f; + private const double SafetyNextFire = 0.5; + private const float EjectOffset = 0.4f; + protected const string AmmoExamineColor = "yellow"; + protected const string FireRateExamineColor = "yellow"; + public const string ModeExamineColor = "cyan"; + + public override void Initialize() + { + SubscribeAllEvent(OnShootRequest); + SubscribeAllEvent(OnStopShootRequest); + SubscribeLocalEvent(OnGunMelee); + + // Ammo providers + InitializeBallistic(); + InitializeBattery(); + InitializeCartridge(); + InitializeChamberMagazine(); + InitializeMagazine(); + InitializeRevolver(); + InitializeBasicEntity(); + InitializeClothing(); + InitializeContainer(); + InitializeSolution(); + + // Interactions + SubscribeLocalEvent>(OnAltVerb); + SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent(OnCycleMode); + SubscribeLocalEvent(OnGunSelected); + SubscribeLocalEvent(OnMapInit); + } + + private void OnMapInit(Entity gun, ref MapInitEvent args) + { +#if DEBUG + if (gun.Comp.NextFire > Timing.CurTime) + Log.Warning($"Initializing a map that contains an entity that is on cooldown. Entity: {ToPrettyString(gun)}"); + + DebugTools.Assert((gun.Comp.AvailableModes & gun.Comp.SelectedMode) != 0x0); +#endif + + RefreshModifiers((gun, gun)); + } + + private void OnGunMelee(EntityUid uid, GunComponent component, MeleeHitEvent args) + { + if (!TryComp(uid, out var melee)) + return; + + if (melee.NextAttack > component.NextFire) + { + component.NextFire = melee.NextAttack; + Dirty(uid, component); + } + } + + private void OnShootRequest(RequestShootEvent msg, EntitySessionEventArgs args) + { + var user = args.SenderSession.AttachedEntity; + + if (user == null || !_combatMode.IsInCombatMode(user)) + return; + + if (TryComp(user.Value, out var mechPilot)) + user = mechPilot.Mech; + + if (!TryGetGun(user.Value, out var ent, out var gun)) + return; + + if (ent != GetEntity(msg.Gun)) + return; + + gun.ShootCoordinates = GetCoordinates(msg.Coordinates); + gun.Target = GetEntity(msg.Target); + AttemptShoot(user.Value, ent, gun); + } + + private void OnStopShootRequest(RequestStopShootEvent ev, EntitySessionEventArgs args) + { + var gunUid = GetEntity(ev.Gun); + + var user = args.SenderSession.AttachedEntity; + + if (user == null) + return; + + if (TryComp(user.Value, out var mechPilot)) + user = mechPilot.Mech; + + if (!TryGetGun(user.Value, out var ent, out var gun)) + return; + + if (ent != gunUid) + return; + + StopShooting(gunUid, gun); + } + + public bool CanShoot(GunComponent component) + { + if (component.NextFire > Timing.CurTime) + return false; + + return true; + } + + public bool TryGetGun(EntityUid entity, out EntityUid gunEntity, [NotNullWhen(true)] out GunComponent? gunComp) + { + gunEntity = default; + gunComp = null; + + if (TryComp(entity, out var mech) && + mech.CurrentSelectedEquipment.HasValue && + TryComp(mech.CurrentSelectedEquipment.Value, out var mechGun)) + { + gunEntity = mech.CurrentSelectedEquipment.Value; + gunComp = mechGun; + return true; + } + + if (EntityManager.TryGetComponent(entity, out HandsComponent? hands) && + hands.ActiveHandEntity is { } held && + TryComp(held, out GunComponent? gun)) + { + gunEntity = held; + gunComp = gun; + return true; + } + + // Last resort is check if the entity itself is a gun. + if (TryComp(entity, out gun)) + { + gunEntity = entity; + gunComp = gun; + return true; + } + + return false; + } + + private void StopShooting(EntityUid uid, GunComponent gun) + { + if (gun.ShotCounter == 0) + return; + + gun.ShotCounter = 0; + gun.ShootCoordinates = null; + gun.Target = null; + Dirty(uid, gun); + } + + /// + /// Attempts to shoot at the target coordinates. Resets the shot counter after every shot. + /// + public void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun, EntityCoordinates toCoordinates) + { + gun.ShootCoordinates = toCoordinates; + AttemptShoot(user, gunUid, gun); + gun.ShotCounter = 0; + } + + /// + /// Shoots by assuming the gun is the user at default coordinates. + /// + public void AttemptShoot(EntityUid gunUid, GunComponent gun) + { + var coordinates = new EntityCoordinates(gunUid, new Vector2(0, -1)); + gun.ShootCoordinates = coordinates; + AttemptShoot(gunUid, gunUid, gun); + gun.ShotCounter = 0; + } + + private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun) + { + if (gun.FireRateModified <= 0f || + !_actionBlockerSystem.CanAttack(user)) + return; + + var toCoordinates = gun.ShootCoordinates; + + if (toCoordinates == null) + return; + + var curTime = Timing.CurTime; + + // check if anything wants to prevent shooting + var prevention = new ShotAttemptedEvent + { + User = user, + Used = (gunUid, gun) + }; + RaiseLocalEvent(gunUid, ref prevention); + if (prevention.Cancelled) + return; + + RaiseLocalEvent(user, ref prevention); + if (prevention.Cancelled) + return; + + // Need to do this to play the clicking sound for empty automatic weapons + // but not play anything for burst fire. + if (gun.NextFire > curTime) + return; + + var fireRate = TimeSpan.FromSeconds(1f / gun.FireRateModified); + + // First shot + // Previously we checked shotcounter but in some cases all the bullets got dumped at once + // curTime - fireRate is insufficient because if you time it just right you can get a 3rd shot out slightly quicker. + if (gun.NextFire < curTime - fireRate || gun.ShotCounter == 0 && gun.NextFire < curTime) + gun.NextFire = curTime; + + var shots = 0; + var lastFire = gun.NextFire; + + Log.Debug($"Nextfire={gun.NextFire} curTime={curTime}"); + + while (gun.NextFire <= curTime) + { + Log.Debug("Shots++"); + gun.NextFire += fireRate; + shots++; + } + + // NextFire has been touched regardless so need to dirty the gun. + Dirty(gunUid, gun); + + // Get how many shots we're actually allowed to make, due to clip size or otherwise. + // Don't do this in the loop so we still reset NextFire. + switch (gun.SelectedMode) + { + case SelectiveFire.SemiAuto: + shots = Math.Min(shots, 1 - gun.ShotCounter); + break; + case SelectiveFire.Burst: + shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter); + break; + case SelectiveFire.FullAuto: + break; + default: + throw new ArgumentOutOfRangeException($"No implemented shooting behavior for {gun.SelectedMode}!"); + } + + Log.Debug($"Shots fired: {shots}"); + + var attemptEv = new AttemptShootEvent(user, null); + RaiseLocalEvent(gunUid, ref attemptEv); + + if (attemptEv.Cancelled) + { + if (attemptEv.Message != null) + { + PopupSystem.PopupClient(attemptEv.Message, gunUid, user); + } + + gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds)); + return; + } + + var fromCoordinates = Transform(user).Coordinates; + // Remove ammo + var ev = new TakeAmmoEvent(shots, new List<(EntityUid? Entity, IShootable Shootable)>(), fromCoordinates, user); + + // Listen it just makes the other code around it easier if shots == 0 to do this. + if (shots > 0) + RaiseLocalEvent(gunUid, ev); + + DebugTools.Assert(ev.Ammo.Count <= shots); + DebugTools.Assert(shots >= 0); + UpdateAmmoCount(gunUid); + + // Even if we don't actually shoot update the ShotCounter. This is to avoid spamming empty sounds + // where the gun may be SemiAuto or Burst. + gun.ShotCounter += shots; + + if (ev.Ammo.Count <= 0) + { + // triggers effects on the gun if it's empty + var emptyGunShotEvent = new OnEmptyGunShotEvent(); + RaiseLocalEvent(gunUid, ref emptyGunShotEvent); + + // Play empty gun sounds if relevant + // If they're firing an existing clip then don't play anything. + if (shots > 0) + { + if (ev.Reason != null && Timing.IsFirstTimePredicted) + { + PopupSystem.PopupCursor(ev.Reason); + } + + // Don't spam safety sounds at gun fire rate, play it at a reduced rate. + // May cause prediction issues? Needs more tweaking + gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds)); + Audio.PlayPredicted(gun.SoundEmpty, gunUid, user); + return; + } + + return; + } + + // Shoot confirmed - sounds also played here in case it's invalid (e.g. cartridge already spent). + Shoot(gunUid, gun, ev.Ammo, fromCoordinates, toCoordinates.Value, out var userImpulse, user, throwItems: attemptEv.ThrowItems); + var shotEv = new GunShotEvent(user, ev.Ammo); + RaiseLocalEvent(gunUid, ref shotEv); + + if (userImpulse && TryComp(user, out var userPhysics)) + { + if (_gravity.IsWeightless(user, userPhysics)) + CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics); + } + + Dirty(gunUid, gun); + } + + public void Shoot( + EntityUid gunUid, + GunComponent gun, + EntityUid ammo, + EntityCoordinates fromCoordinates, + EntityCoordinates toCoordinates, + out bool userImpulse, + EntityUid? user = null, + bool throwItems = false) + { + var shootable = EnsureShootable(ammo); + Shoot(gunUid, gun, new List<(EntityUid? Entity, IShootable Shootable)>(1) { (ammo, shootable) }, fromCoordinates, toCoordinates, out userImpulse, user, throwItems); + } + + public abstract 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); + + public void ShootProjectile(EntityUid uid, Vector2 direction, Vector2 gunVelocity, EntityUid gunUid, EntityUid? user = null, float speed = 20f) + { + var physics = EnsureComp(uid); + Physics.SetBodyStatus(uid, physics, BodyStatus.InAir); + + var targetMapVelocity = gunVelocity + direction.Normalized() * speed; + var currentMapVelocity = Physics.GetMapLinearVelocity(uid, physics); + var finalLinear = physics.LinearVelocity + targetMapVelocity - currentMapVelocity; + Physics.SetLinearVelocity(uid, finalLinear, body: physics); + + var projectile = EnsureComp(uid); + Projectiles.SetShooter(uid, projectile, user ?? gunUid); + projectile.Weapon = gunUid; + + TransformSystem.SetWorldRotation(uid, direction.ToWorldAngle()); + } + + protected abstract void Popup(string message, EntityUid? uid, EntityUid? user); + + /// + /// Call this whenever the ammo count for a gun changes. + /// + protected virtual void UpdateAmmoCount(EntityUid uid, bool prediction = true) {} + + protected void SetCartridgeSpent(EntityUid uid, CartridgeAmmoComponent cartridge, bool spent) + { + if (cartridge.Spent != spent) + Dirty(uid, cartridge); + + cartridge.Spent = spent; + Appearance.SetData(uid, AmmoVisuals.Spent, spent); + } + + /// + /// Drops a single cartridge / shell + /// + protected void EjectCartridge( + EntityUid entity, + Angle? angle = null, + bool playSound = true) + { + // TODO: Sound limit version. + var offsetPos = Random.NextVector2(EjectOffset); + var xform = Transform(entity); + + var coordinates = xform.Coordinates; + coordinates = coordinates.Offset(offsetPos); + + TransformSystem.SetLocalRotation(xform, Random.NextAngle()); + TransformSystem.SetCoordinates(entity, xform, coordinates); + + // decides direction the casing ejects and only when not cycling + if (angle != null) + { + Angle ejectAngle = angle.Value; + ejectAngle += 3.7f; // 212 degrees; casings should eject slightly to the right and behind of a gun + ThrowingSystem.TryThrow(entity, ejectAngle.ToVec().Normalized() / 100, 5f); + } + if (playSound && TryComp(entity, out var cartridge)) + { + Audio.PlayPvs(cartridge.EjectSound, entity, AudioParams.Default.WithVariation(SharedContentAudioSystem.DefaultVariation).WithVolume(-1f)); + } + } + + protected IShootable EnsureShootable(EntityUid uid) + { + if (TryComp(uid, out var cartridge)) + return cartridge; + + return EnsureComp(uid); + } + + protected void RemoveShootable(EntityUid uid) + { + RemCompDeferred(uid); + RemCompDeferred(uid); + } + + protected void MuzzleFlash(EntityUid gun, AmmoComponent component, Angle worldAngle, EntityUid? user = null) + { + var attemptEv = new GunMuzzleFlashAttemptEvent(); + RaiseLocalEvent(gun, ref attemptEv); + if (attemptEv.Cancelled) + return; + + var sprite = component.MuzzleFlash; + + if (sprite == null) + return; + + var ev = new MuzzleFlashEvent(GetNetEntity(gun), sprite, worldAngle); + CreateEffect(gun, ev, gun); + } + + public void CauseImpulse(EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, EntityUid user, PhysicsComponent userPhysics) + { + var fromMap = fromCoordinates.ToMapPos(EntityManager, TransformSystem); + var toMap = toCoordinates.ToMapPos(EntityManager, TransformSystem); + var shotDirection = (toMap - fromMap).Normalized(); + + const float impulseStrength = 25.0f; + var impulseVector = shotDirection * impulseStrength; + Physics.ApplyLinearImpulse(user, -impulseVector, body: userPhysics); + } + + public void RefreshModifiers(Entity gun) + { + if (!Resolve(gun, ref gun.Comp)) + return; + + var comp = gun.Comp; + var ev = new GunRefreshModifiersEvent( + (gun, comp), + comp.SoundGunshot, + comp.CameraRecoilScalar, + comp.AngleIncrease, + comp.AngleDecay, + comp.MaxAngle, + comp.MinAngle, + comp.ShotsPerBurst, + comp.FireRate, + comp.ProjectileSpeed + ); + + RaiseLocalEvent(gun, ref ev); + + comp.SoundGunshotModified = ev.SoundGunshot; + comp.CameraRecoilScalarModified = ev.CameraRecoilScalar; + comp.AngleIncreaseModified = ev.AngleIncrease; + comp.AngleDecayModified = ev.AngleDecay; + comp.MaxAngleModified = ev.MaxAngle; + comp.MinAngleModified = ev.MinAngle; + comp.ShotsPerBurstModified = ev.ShotsPerBurst; + comp.FireRateModified = ev.FireRate; + comp.ProjectileSpeedModified = ev.ProjectileSpeed; + + Dirty(gun); + } + + protected abstract void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, EntityUid? user = null); + + /// + /// Used for animated effects on the client. + /// + [Serializable, NetSerializable] + public sealed class HitscanEvent : EntityEventArgs + { + public List<(NetCoordinates coordinates, Angle angle, SpriteSpecifier Sprite, float Distance)> Sprites = new(); + } +} + +/// +/// Raised directed on the gun before firing to see if the shot should go through. +/// +/// +/// Handling this in server exclusively will lead to mispredicts. +/// +/// The user that attempted to fire this gun. +/// Set this to true if the shot should be cancelled. +/// Set this to true if the ammo shouldn't actually be fired, just thrown. +[ByRefEvent] +public record struct AttemptShootEvent(EntityUid User, string? Message, bool Cancelled = false, bool ThrowItems = false); + +/// +/// Raised directed on the gun after firing. +/// +/// The user that fired this gun. +[ByRefEvent] +public record struct GunShotEvent(EntityUid User, List<(EntityUid? Uid, IShootable Shootable)> Ammo); + +public enum EffectLayers : byte +{ + Unshaded, +} + +[Serializable, NetSerializable] +public enum AmmoVisuals : byte +{ + Spent, + AmmoCount, + AmmoMax, + HasAmmo, // used for generic visualizers. c# stuff can just check ammocount != 0 + MagLoaded, + BoltClosed, +} From 2dfc4fa79ae618a8f6f57c5a70b59ff462a08d22 Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:15:18 +0300 Subject: [PATCH 17/22] Update SharedGunSystem.cs --- Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs index 94c736f32fa..5ef4a5a3cbf 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs @@ -240,6 +240,14 @@ public void AttemptShoot(EntityUid gunUid, GunComponent gun) gun.ShotCounter = 0; } + /// + /// Sets the targeted entity of the gun. Should be called before attempting to shoot to avoid shooting over the target. + /// + public void SetTarget(GunComponent gun, EntityUid target) + { + gun.Target = target; + } + private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun) { if (gun.FireRateModified <= 0f || From f9c01993efbdeada6ec8bc940f3dbf6871f8dd44 Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:18:25 +0300 Subject: [PATCH 18/22] Update GunComponent.cs --- Content.Shared/Weapons/Ranged/Components/GunComponent.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs index e6c46ec7653..3ba7762d054 100644 --- a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs @@ -232,6 +232,12 @@ public sealed partial class GunComponent : Component /// [DataField] public bool ClumsyProof = false; + + /// + /// Corvax-Next. The percentage chance of a given gun to accidentally discharge if violently thrown into a wall or person + /// + [DataField] + public float FireOnDropChance = 0.1f; } [Flags] From e9c50694cda4125d849f884659764e6c5aa5a0e5 Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:24:44 +0300 Subject: [PATCH 19/22] Update GunComponent.cs --- .../Weapons/Ranged/Components/GunComponent.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs index 3ba7762d054..99d499fd8d3 100644 --- a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Systems; using Robust.Shared.Audio; @@ -156,6 +157,30 @@ public sealed partial class GunComponent : Component [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] public int ShotsPerBurstModified = 3; + /// + /// How long time must pass between burstfire shots. + /// + [DataField, AutoNetworkedField] + public float BurstCooldown = 0.25f; + + /// + /// The fire rate of the weapon in burst fire mode. + /// + [DataField, AutoNetworkedField] + public float BurstFireRate = 8f; + + /// + /// Whether the burst fire mode has been activated. + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public bool BurstActivated = false; + + /// + /// The burst fire bullet count. + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public int BurstShotsCount = 0; + /// /// Used for tracking semi-auto / burst /// @@ -232,6 +257,12 @@ public sealed partial class GunComponent : Component /// [DataField] public bool ClumsyProof = false; + + /// + /// Firing direction for an item not being held (e.g. shuttle cannons, thrown guns still firing). + /// + [DataField] + public Vector2 DefaultDirection = new Vector2(0, -1); /// /// Corvax-Next. The percentage chance of a given gun to accidentally discharge if violently thrown into a wall or person From b8441fc190527bb23c6137267edc27d00af0f5cd Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:29:19 +0300 Subject: [PATCH 20/22] Update GunComponent.cs --- Content.Shared/Weapons/Ranged/Components/GunComponent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs index 99d499fd8d3..bcc56c28ab1 100644 --- a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs @@ -218,7 +218,7 @@ public sealed partial class GunComponent : Component /// How fast the projectile moves. /// /// - [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] public float ProjectileSpeedModified; /// From be290cbe34c619be8029b07b64611284ba41d7a5 Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:38:19 +0300 Subject: [PATCH 21/22] Add files via upload --- .../Weapons/Ranged/Components/GunComponent.cs | 282 ++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 Content.Shared/Weapons/Ranged/Components/GunComponent.cs diff --git a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs new file mode 100644 index 00000000000..bcc56c28ab1 --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs @@ -0,0 +1,282 @@ +using System.Numerics; +using Content.Shared.Weapons.Ranged.Events; +using Content.Shared.Weapons.Ranged.Systems; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Map; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Weapons.Ranged.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[Access(typeof(SharedGunSystem))] +public sealed partial class GunComponent : Component +{ + #region Sound + + /// + /// The base sound to use when the gun is fired. + /// + [DataField] + public SoundSpecifier? SoundGunshot = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/smg.ogg"); + + /// + /// The sound to use when the gun is fired. + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public SoundSpecifier? SoundGunshotModified; + + [DataField] + public SoundSpecifier? SoundEmpty = new SoundPathSpecifier("/Audio/Weapons/Guns/Empty/empty.ogg"); + + /// + /// Sound played when toggling the for this gun. + /// + [DataField] + public SoundSpecifier? SoundMode = new SoundPathSpecifier("/Audio/Weapons/Guns/Misc/selector.ogg"); + + #endregion + + #region Recoil + + // These values are very small for now until we get a debug overlay and fine tune it + + /// + /// The base scalar value applied to the vector governing camera recoil. + /// + [DataField, AutoNetworkedField] + public float CameraRecoilScalar = 1f; + + /// + /// A scalar value applied to the vector governing camera recoil. + /// If 0, there will be no camera recoil. + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public float CameraRecoilScalarModified = 1f; + + /// + /// Last time the gun fired. + /// Used for recoil purposes. + /// + [DataField] + public TimeSpan LastFire = TimeSpan.Zero; + + /// + /// What the current spread is for shooting. This gets changed every time the gun fires. + /// + [DataField] + [AutoNetworkedField] + public Angle CurrentAngle; + + /// + /// The base value for how much the spread increases every time the gun fires. + /// + [DataField] + public Angle AngleIncrease = Angle.FromDegrees(0.5); + + /// + /// How much the spread increases every time the gun fires. + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public Angle AngleIncreaseModified; + + /// + /// The base value for how much the decreases per second. + /// + [DataField] + public Angle AngleDecay = Angle.FromDegrees(4); + + /// + /// How much the decreases per second. + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public Angle AngleDecayModified; + + /// + /// The base value for the maximum angle allowed for + /// + [DataField] + [AutoNetworkedField] + public Angle MaxAngle = Angle.FromDegrees(2); + + /// + /// The maximum angle allowed for + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public Angle MaxAngleModified; + + /// + /// The base value for the minimum angle allowed for + /// + [DataField] + [AutoNetworkedField] + public Angle MinAngle = Angle.FromDegrees(1); + + /// + /// The minimum angle allowed for . + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public Angle MinAngleModified; + + #endregion + + /// + /// Whether this gun is shot via the use key or the alt-use key. + /// + [DataField, AutoNetworkedField] + public bool UseKey = true; + + /// + /// Where the gun is being requested to shoot. + /// + [ViewVariables] + public EntityCoordinates? ShootCoordinates = null; + + /// + /// Who the gun is being requested to shoot at directly. + /// + [ViewVariables] + public EntityUid? Target = null; + + /// + /// The base value for how many shots to fire per burst. + /// + [DataField, AutoNetworkedField] + public int ShotsPerBurst = 3; + + /// + /// How many shots to fire per burst. + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public int ShotsPerBurstModified = 3; + + /// + /// How long time must pass between burstfire shots. + /// + [DataField, AutoNetworkedField] + public float BurstCooldown = 0.25f; + + /// + /// The fire rate of the weapon in burst fire mode. + /// + [DataField, AutoNetworkedField] + public float BurstFireRate = 8f; + + /// + /// Whether the burst fire mode has been activated. + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public bool BurstActivated = false; + + /// + /// The burst fire bullet count. + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public int BurstShotsCount = 0; + + /// + /// Used for tracking semi-auto / burst + /// + [ViewVariables] + [AutoNetworkedField] + public int ShotCounter = 0; + + /// + /// The base value for how many times it shoots per second. + /// + [DataField] + [AutoNetworkedField] + public float FireRate = 8f; + + /// + /// How many times it shoots per second. + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public float FireRateModified; + + /// + /// Starts fire cooldown when equipped if true. + /// + [DataField] + public bool ResetOnHandSelected = true; + + /// + /// The base value for how fast the projectile moves. + /// + [DataField] + public float ProjectileSpeed = 25f; + + /// + /// How fast the projectile moves. + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public float ProjectileSpeedModified; + + /// + /// When the gun is next available to be shot. + /// Can be set multiple times in a single tick due to guns firing faster than a single tick time. + /// + [DataField(customTypeSerializer:typeof(TimeOffsetSerializer))] + [AutoNetworkedField] + [AutoPausedField] + public TimeSpan NextFire = TimeSpan.Zero; + + /// + /// What firemodes can be selected. + /// + [DataField] + [AutoNetworkedField] + public SelectiveFire AvailableModes = SelectiveFire.SemiAuto; + + /// + /// What firemode is currently selected. + /// + [DataField] + [AutoNetworkedField] + public SelectiveFire SelectedMode = SelectiveFire.SemiAuto; + + /// + /// Whether or not information about + /// the gun will be shown on examine. + /// + [DataField] + public bool ShowExamineText = true; + + /// + /// Whether or not someone with the + /// clumsy trait can shoot this + /// + [DataField] + public bool ClumsyProof = false; + + /// + /// Firing direction for an item not being held (e.g. shuttle cannons, thrown guns still firing). + /// + [DataField] + public Vector2 DefaultDirection = new Vector2(0, -1); + + /// + /// Corvax-Next. The percentage chance of a given gun to accidentally discharge if violently thrown into a wall or person + /// + [DataField] + public float FireOnDropChance = 0.1f; +} + +[Flags] +public enum SelectiveFire : byte +{ + Invalid = 0, + // Combat mode already functions as the equivalent of Safety + SemiAuto = 1 << 0, + Burst = 1 << 1, + FullAuto = 1 << 2, // Not in the building! +} From cd1ad0dc1d7a5b59414458182e3ffb36cc834ced Mon Sep 17 00:00:00 2001 From: Mor-Dast <136860710+Mor-Dast@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:41:38 +0300 Subject: [PATCH 22/22] Update GunComponent.cs --- Content.Shared/Weapons/Ranged/Components/GunComponent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs index bcc56c28ab1..99d499fd8d3 100644 --- a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs @@ -218,7 +218,7 @@ public sealed partial class GunComponent : Component /// How fast the projectile moves. /// /// - [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] public float ProjectileSpeedModified; ///