diff --git a/Content.Client/GameObjects/Components/Explosion/ClusterFlashVisualizer.cs b/Content.Client/GameObjects/Components/Explosion/ClusterFlashVisualizer.cs new file mode 100644 index 0000000000..b366a11fd8 --- /dev/null +++ b/Content.Client/GameObjects/Components/Explosion/ClusterFlashVisualizer.cs @@ -0,0 +1,40 @@ +using Content.Shared.GameObjects.Components.Explosion; +using JetBrains.Annotations; +using Robust.Client.GameObjects; +using Robust.Client.Interfaces.GameObjects.Components; +using Robust.Shared.Utility; +using YamlDotNet.RepresentationModel; + +namespace Content.Client.GameObjects.Components.Explosion +{ + [UsedImplicitly] + // ReSharper disable once InconsistentNaming + public class ClusterFlashVisualizer : AppearanceVisualizer + { + private string _state; + + public override void LoadData(YamlMappingNode node) + { + base.LoadData(node); + if (node.TryGetNode("state", out var state)) + { + _state = state.AsString(); + } + } + + public override void OnChangeData(AppearanceComponent component) + { + base.OnChangeData(component); + + if (!component.Owner.TryGetComponent(out var sprite)) + { + return; + } + + if (component.TryGetData(ClusterFlashVisuals.GrenadesCounter, out int grenadesCounter)) + { + sprite.LayerSetState(0, $"{_state}-{grenadesCounter}"); + } + } + } +} diff --git a/Content.Server/GameObjects/Components/Explosion/ClusterFlashComponent.cs b/Content.Server/GameObjects/Components/Explosion/ClusterFlashComponent.cs new file mode 100644 index 0000000000..bd2f90f054 --- /dev/null +++ b/Content.Server/GameObjects/Components/Explosion/ClusterFlashComponent.cs @@ -0,0 +1,181 @@ +#nullable enable +using Content.Shared.Interfaces.GameObjects.Components; +using Content.Server.GameObjects.Components.Explosion; +using Robust.Shared.GameObjects; +using System.Threading.Tasks; +using Robust.Server.GameObjects.Components.Container; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Serialization; +using System; +using System.Diagnostics.CodeAnalysis; +using Content.Server.GameObjects.Components.Trigger.TimerTrigger; +using Content.Server.Throw; +using Robust.Server.GameObjects; +using Content.Shared.GameObjects.Components.Explosion; +using Robust.Shared.GameObjects.Components.Timers; +using Robust.Shared.Interfaces.Random; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Random; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Explosives +{ + [RegisterComponent] + public sealed class ClusterFlashComponent : Component, IInteractUsing, IUse + { + public override string Name => "ClusterFlash"; + + private Container _grenadesContainer = default!; + + /// + /// What we fill our prototype with if we want to pre-spawn with grenades. + /// + [ViewVariables] + private string? _fillPrototype; + + /// + /// If we have a pre-fill how many more can we spawn. + /// + private int _unspawnedCount; + + /// + /// Maximum grenades in the container. + /// + [ViewVariables] + private int _maxGrenades; + + /// + /// How long until our grenades are shot out and armed. + /// + [ViewVariables(VVAccess.ReadWrite)] + private float _delay; + + /// + /// Max distance grenades can be thrown. + /// + [ViewVariables(VVAccess.ReadWrite)] + private float _throwDistance; + + /// + /// This is the end. + /// + private bool _countDown; + + async Task IInteractUsing.InteractUsing(InteractUsingEventArgs args) + { + if (_grenadesContainer.ContainedEntities.Count >= _maxGrenades || !args.Using.HasComponent()) + return false; + + _grenadesContainer.Insert(args.Using); + UpdateAppearance(); + return true; + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(ref _fillPrototype, "fillPrototype", null); + serializer.DataField(ref _maxGrenades, "maxGrenadesCount", 3); + serializer.DataField(ref _delay, "delay", 1.0f); + serializer.DataField(ref _throwDistance, "distance", 3.0f); + } + + public override void Initialize() + { + base.Initialize(); + + _grenadesContainer = ContainerManagerComponent.Ensure("cluster-flash", Owner); + + } + + protected override void Startup() + { + base.Startup(); + + if (_fillPrototype != null) + { + _unspawnedCount = Math.Max(0, _maxGrenades - _grenadesContainer.ContainedEntities.Count); + UpdateAppearance(); + } + } + + bool IUse.UseEntity(UseEntityEventArgs eventArgs) + { + if (_countDown || (_grenadesContainer.ContainedEntities.Count + _unspawnedCount) <= 0) + return false; + Owner.SpawnTimer((int) (_delay * 1000), () => + { + if (Owner.Deleted) + return; + _countDown = true; + var random = IoCManager.Resolve(); + var delay = 20; + var grenadesInserted = _grenadesContainer.ContainedEntities.Count + _unspawnedCount; + var thrownCount = 0; + var segmentAngle = (int) (360 / grenadesInserted); + while (TryGetGrenade(out var grenade)) + { + var angleMin = segmentAngle * thrownCount; + var angleMax = segmentAngle * (thrownCount + 1); + var angle = Angle.FromDegrees(random.Next(angleMin, angleMax)); + var distance = (float)random.NextFloat() * _throwDistance; + var target = new EntityCoordinates(Owner.Uid, angle.ToVec().Normalized * distance); + + grenade.Throw(0.5f, target, grenade.Transform.Coordinates); + + grenade.SpawnTimer(delay, () => + { + if (grenade.Deleted) + return; + + if (grenade.TryGetComponent(out OnUseTimerTriggerComponent? useTimer)) + { + useTimer.Trigger(eventArgs.User); + } + }); + + delay += random.Next(550, 900); + thrownCount++; + } + + Owner.Delete(); + }); + return true; + } + + private bool TryGetGrenade([NotNullWhen(true)] out IEntity? grenade) + { + grenade = null; + + if (_unspawnedCount > 0) + { + _unspawnedCount--; + grenade = Owner.EntityManager.SpawnEntity(_fillPrototype, Owner.Transform.Coordinates); + return true; + } + + if (_grenadesContainer.ContainedEntities.Count > 0) + { + grenade = _grenadesContainer.ContainedEntities[0]; + + // This shouldn't happen but you never know. + if (!_grenadesContainer.Remove(grenade)) + return false; + + return true; + } + + return false; + } + + private void UpdateAppearance() + { + if (!Owner.TryGetComponent(out AppearanceComponent? appearance)) return; + + appearance.SetData(ClusterFlashVisuals.GrenadesCounter, _grenadesContainer.ContainedEntities.Count + _unspawnedCount); + } + } +} diff --git a/Content.Server/GameObjects/Components/Explosion/FlashExplosiveComponent.cs b/Content.Server/GameObjects/Components/Explosion/FlashExplosiveComponent.cs index a6fa3dbcc1..47355f5942 100644 --- a/Content.Server/GameObjects/Components/Explosion/FlashExplosiveComponent.cs +++ b/Content.Server/GameObjects/Components/Explosion/FlashExplosiveComponent.cs @@ -19,7 +19,9 @@ namespace Content.Server.GameObjects.Components.Explosion public override string Name => "FlashExplosive"; private float _range; + private float _duration; + private string _sound; private bool _deleteOnFlash; diff --git a/Content.Server/GameObjects/Components/Projectiles/ThrownItemComponent.cs b/Content.Server/GameObjects/Components/Projectiles/ThrownItemComponent.cs index 9082154b55..fb5f23ca4b 100644 --- a/Content.Server/GameObjects/Components/Projectiles/ThrownItemComponent.cs +++ b/Content.Server/GameObjects/Components/Projectiles/ThrownItemComponent.cs @@ -50,7 +50,7 @@ namespace Content.Server.GameObjects.Components.Projectiles // after impacting the first object. // For realism this should actually be changed when the velocity of the object is less than a threshold. // This would allow ricochets off walls, and weird gravity effects from slowing the object. - if (Owner.TryGetComponent(out IPhysicsComponent body) && body.PhysicsShapes.Count >= 1) + if (!Owner.Deleted && Owner.TryGetComponent(out IPhysicsComponent body) && body.PhysicsShapes.Count >= 1) { _shouldCollide = false; } diff --git a/Content.Server/GameObjects/Components/Trigger/TimerTrigger/OnUseTimerTriggerComponent.cs b/Content.Server/GameObjects/Components/Trigger/TimerTrigger/OnUseTimerTriggerComponent.cs index 800fc03f9e..e65cbfac74 100644 --- a/Content.Server/GameObjects/Components/Trigger/TimerTrigger/OnUseTimerTriggerComponent.cs +++ b/Content.Server/GameObjects/Components/Trigger/TimerTrigger/OnUseTimerTriggerComponent.cs @@ -1,11 +1,12 @@ -using System; +#nullable enable +using System; using Content.Server.GameObjects.EntitySystems; using Content.Shared.GameObjects.Components.Trigger; using Content.Shared.Interfaces.GameObjects.Components; using Robust.Server.GameObjects; using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; using Robust.Shared.Interfaces.GameObjects; -using Robust.Shared.IoC; using Robust.Shared.Serialization; namespace Content.Server.GameObjects.Components.Trigger.TimerTrigger @@ -13,11 +14,9 @@ namespace Content.Server.GameObjects.Components.Trigger.TimerTrigger [RegisterComponent] public class OnUseTimerTriggerComponent : Component, IUse { - [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; - public override string Name => "OnUseTimerTrigger"; - private float _delay = 0f; + private float _delay; public override void ExposeData(ObjectSerializer serializer) { @@ -26,13 +25,17 @@ namespace Content.Server.GameObjects.Components.Trigger.TimerTrigger serializer.DataField(ref _delay, "delay", 0f); } + public void Trigger(IEntity user) + { + if (Owner.TryGetComponent(out AppearanceComponent? appearance)) + appearance.SetData(TriggerVisuals.VisualState, TriggerVisualState.Primed); + + EntitySystem.Get().HandleTimerTrigger(TimeSpan.FromSeconds(_delay), user, Owner); + } + bool IUse.UseEntity(UseEntityEventArgs eventArgs) { - var triggerSystem = _entitySystemManager.GetEntitySystem(); - if (Owner.TryGetComponent(out var appearance)) { - appearance.SetData(TriggerVisuals.VisualState, TriggerVisualState.Primed); - } - triggerSystem.HandleTimerTrigger(TimeSpan.FromSeconds(_delay), eventArgs.User, Owner); + Trigger(eventArgs.User); return true; } } diff --git a/Content.Shared/GameObjects/Components/Explosion/SharedClusterFlashComponent.cs b/Content.Shared/GameObjects/Components/Explosion/SharedClusterFlashComponent.cs new file mode 100644 index 0000000000..f7822392dd --- /dev/null +++ b/Content.Shared/GameObjects/Components/Explosion/SharedClusterFlashComponent.cs @@ -0,0 +1,13 @@ +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Explosion +{ + [Serializable, NetSerializable] + public enum ClusterFlashVisuals : byte + { + GrenadesCounter + } +} diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Explosives/clusterbang.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Explosives/clusterbang.yml new file mode 100644 index 0000000000..f307bc77cd --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Explosives/clusterbang.yml @@ -0,0 +1,26 @@ +- type: entity + parent: BaseItem + id: ClusterBang + name: ClusterBang + description: Can be used only with flashbangs. Explodes several times. + components: + - type: Sprite + sprite: Objects/Weapons/Grenades/clusterbang.rsi + netsync: false + state: base-0 + - type: Appearance + visuals: + - type: ClusterFlashVisualizer + state: base + - type: ClusterFlash + +- type: entity + parent: ClusterBang + id: ClusterBangFull + suffix: Full + components: + - type: Sprite + state: base-3 + - type: ClusterFlash + fillPrototype: GrenadeFlashBang + diff --git a/Resources/Textures/Objects/Weapons/Grenades/clusterbang.rsi/base-0.png b/Resources/Textures/Objects/Weapons/Grenades/clusterbang.rsi/base-0.png new file mode 100644 index 0000000000..16e1f3ccc8 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Grenades/clusterbang.rsi/base-0.png differ diff --git a/Resources/Textures/Objects/Weapons/Grenades/clusterbang.rsi/base-1.png b/Resources/Textures/Objects/Weapons/Grenades/clusterbang.rsi/base-1.png new file mode 100644 index 0000000000..f6f6a52f5b Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Grenades/clusterbang.rsi/base-1.png differ diff --git a/Resources/Textures/Objects/Weapons/Grenades/clusterbang.rsi/base-2.png b/Resources/Textures/Objects/Weapons/Grenades/clusterbang.rsi/base-2.png new file mode 100644 index 0000000000..208ad4c9e8 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Grenades/clusterbang.rsi/base-2.png differ diff --git a/Resources/Textures/Objects/Weapons/Grenades/clusterbang.rsi/base-3.png b/Resources/Textures/Objects/Weapons/Grenades/clusterbang.rsi/base-3.png new file mode 100644 index 0000000000..18a1d3042c Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Grenades/clusterbang.rsi/base-3.png differ diff --git a/Resources/Textures/Objects/Weapons/Grenades/clusterbang.rsi/meta.json b/Resources/Textures/Objects/Weapons/Grenades/clusterbang.rsi/meta.json new file mode 100644 index 0000000000..55dc82377a --- /dev/null +++ b/Resources/Textures/Objects/Weapons/Grenades/clusterbang.rsi/meta.json @@ -0,0 +1,27 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/29c0ed1b000619cb5398ef921000a8d4502ba0b6 and modified by Swept", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "base-0", + "directions": 1 + }, + { + "name": "base-1", + "directions": 1 + }, + { + "name": "base-2", + "directions": 1 + }, + { + "name": "base-3", + "directions": 1 + } + ] +}