From a4d6f09a4fc11c2cd7cdacc390f4c42995757ff6 Mon Sep 17 00:00:00 2001
From: Plykiya <58439124+Plykiya@users.noreply.github.com>
Date: Mon, 16 Dec 2024 04:08:07 -0800
Subject: [PATCH] Cluster grenade refactor and contra markings (#31108)
* Cluster grenade refactor
* oopsies on the name
* Solve client-side errors
* reviews addressed
* filling scattering grenades is now predicted
* reviews addressed
---
.../Explosion/ScatteringGrenadeSystem.cs | 8 +
.../Components/ClusterGrenadeComponent.cs | 117 --------
.../Components/ProjectileGrenadeComponent.cs | 48 ++++
.../EntitySystems/ClusterGrenadeSystem.cs | 177 ------------
.../EntitySystems/ProjectileGrenadeSystem.cs | 110 ++++++++
.../EntitySystems/ScatteringGrenadeSystem.cs | 122 ++++++++
.../Systems/RequireProjectileTargetSystem.cs | 5 +
.../Components/ScatteringGrenadeComponent.cs | 109 ++++++++
.../SharedScatteringGrenadeSystem.cs | 70 +++++
.../Objects/Weapons/Throwable/clusterbang.yml | 264 ------------------
.../Objects/Weapons/Throwable/grenades.yml | 3 +
.../Weapons/Throwable/projectile_grenades.yml | 105 +++++++
.../Weapons/Throwable/scattering_grenades.yml | 174 ++++++++++++
Resources/Prototypes/tags.yml | 3 +
14 files changed, 757 insertions(+), 558 deletions(-)
create mode 100644 Content.Client/Explosion/ScatteringGrenadeSystem.cs
delete mode 100644 Content.Server/Explosion/Components/ClusterGrenadeComponent.cs
create mode 100644 Content.Server/Explosion/Components/ProjectileGrenadeComponent.cs
delete mode 100644 Content.Server/Explosion/EntitySystems/ClusterGrenadeSystem.cs
create mode 100644 Content.Server/Explosion/EntitySystems/ProjectileGrenadeSystem.cs
create mode 100644 Content.Server/Explosion/EntitySystems/ScatteringGrenadeSystem.cs
create mode 100644 Content.Shared/Explosion/Components/ScatteringGrenadeComponent.cs
create mode 100644 Content.Shared/Explosion/EntitySystems/SharedScatteringGrenadeSystem.cs
delete mode 100644 Resources/Prototypes/Entities/Objects/Weapons/Throwable/clusterbang.yml
create mode 100644 Resources/Prototypes/Entities/Objects/Weapons/Throwable/projectile_grenades.yml
create mode 100644 Resources/Prototypes/Entities/Objects/Weapons/Throwable/scattering_grenades.yml
diff --git a/Content.Client/Explosion/ScatteringGrenadeSystem.cs b/Content.Client/Explosion/ScatteringGrenadeSystem.cs
new file mode 100644
index 0000000000..2897677915
--- /dev/null
+++ b/Content.Client/Explosion/ScatteringGrenadeSystem.cs
@@ -0,0 +1,8 @@
+using Content.Shared.Explosion.EntitySystems;
+
+namespace Content.Client.Explosion;
+
+public sealed class ScatteringGrenadeSystem : SharedScatteringGrenadeSystem
+{
+
+}
diff --git a/Content.Server/Explosion/Components/ClusterGrenadeComponent.cs b/Content.Server/Explosion/Components/ClusterGrenadeComponent.cs
deleted file mode 100644
index fe1b8caede..0000000000
--- a/Content.Server/Explosion/Components/ClusterGrenadeComponent.cs
+++ /dev/null
@@ -1,117 +0,0 @@
-using Content.Server.Explosion.EntitySystems;
-using Robust.Shared.Containers;
-using Robust.Shared.Prototypes;
-
-namespace Content.Server.Explosion.Components
-{
- [RegisterComponent, Access(typeof(ClusterGrenadeSystem))]
- public sealed partial class ClusterGrenadeComponent : Component
- {
- public Container GrenadesContainer = default!;
-
- ///
- /// What we fill our prototype with if we want to pre-spawn with grenades.
- ///
- [DataField("fillPrototype")]
- public EntProtoId? FillPrototype;
-
- ///
- /// If we have a pre-fill how many more can we spawn.
- ///
- public int UnspawnedCount;
-
- ///
- /// Maximum grenades in the container.
- ///
- [DataField("maxGrenadesCount")]
- public int MaxGrenades = 3;
-
- ///
- /// Maximum delay in seconds between individual grenade triggers
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("grenadeTriggerIntervalMax")]
- public float GrenadeTriggerIntervalMax = 0f;
-
- ///
- /// Minimum delay in seconds between individual grenade triggers
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("grenadeTriggerIntervalMin")]
- public float GrenadeTriggerIntervalMin = 0f;
-
- ///
- /// Minimum delay in seconds before any grenades start to be triggered.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("baseTriggerDelay")]
- public float BaseTriggerDelay = 1.0f;
-
- ///
- /// Decides if grenades trigger after getting launched
- ///
- [DataField("triggerGrenades")]
- public bool TriggerGrenades = true;
-
- ///
- /// Does the cluster grenade shoot or throw
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("grenadeType")]
- public Enum GrenadeType = Components.GrenadeType.Throw;
-
- ///
- /// The speed at which grenades get thrown
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("velocity")]
- public float Velocity = 5;
-
- ///
- /// Should the spread be random
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("randomSpread")]
- public bool RandomSpread = false;
-
- ///
- /// Should the angle be random
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("randomAngle")]
- public bool RandomAngle = false;
-
- ///
- /// Static distance grenades will be thrown to.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("distance")]
- public float Distance = 1f;
-
- ///
- /// Max distance grenades should randomly be thrown to.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("maxSpreadDistance")]
- public float MaxSpreadDistance = 2.5f;
-
- ///
- /// Minimal distance grenades should randomly be thrown to.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("minSpreadDistance")]
- public float MinSpreadDistance = 0f;
-
- ///
- /// This is the end.
- ///
- public bool CountDown;
- }
-
- public enum GrenadeType
- {
- Throw,
- Shoot
- }
-
-}
diff --git a/Content.Server/Explosion/Components/ProjectileGrenadeComponent.cs b/Content.Server/Explosion/Components/ProjectileGrenadeComponent.cs
new file mode 100644
index 0000000000..58d687e025
--- /dev/null
+++ b/Content.Server/Explosion/Components/ProjectileGrenadeComponent.cs
@@ -0,0 +1,48 @@
+using Content.Server.Explosion.EntitySystems;
+using Robust.Shared.Containers;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Explosion.Components;
+///
+/// Grenades that, when triggered, explode into projectiles
+///
+[RegisterComponent, Access(typeof(ProjectileGrenadeSystem))]
+public sealed partial class ProjectileGrenadeComponent : Component
+{
+ public Container Container = default!;
+
+ ///
+ /// The kind of projectile that the prototype is filled with.
+ ///
+ [DataField]
+ public EntProtoId? FillPrototype;
+
+ ///
+ /// If we have a pre-fill how many more can we spawn.
+ ///
+ public int UnspawnedCount;
+
+ ///
+ /// Total amount of projectiles
+ ///
+ [DataField]
+ public int Capacity = 3;
+
+ ///
+ /// Should the angle of the projectiles be uneven?
+ ///
+ [DataField]
+ public bool RandomAngle = false;
+
+ ///
+ /// The minimum speed the projectiles may come out at
+ ///
+ [DataField]
+ public float MinVelocity = 2f;
+
+ ///
+ /// The maximum speed the projectiles may come out at
+ ///
+ [DataField]
+ public float MaxVelocity = 6f;
+}
diff --git a/Content.Server/Explosion/EntitySystems/ClusterGrenadeSystem.cs b/Content.Server/Explosion/EntitySystems/ClusterGrenadeSystem.cs
deleted file mode 100644
index 78e41c59ae..0000000000
--- a/Content.Server/Explosion/EntitySystems/ClusterGrenadeSystem.cs
+++ /dev/null
@@ -1,177 +0,0 @@
-using Content.Server.Explosion.Components;
-using Content.Shared.Flash.Components;
-using Content.Shared.Interaction;
-using Content.Shared.Throwing;
-using Robust.Shared.Containers;
-using Robust.Shared.Random;
-using Content.Server.Weapons.Ranged.Systems;
-using System.Numerics;
-using Content.Shared.Explosion.Components;
-using Robust.Server.Containers;
-using Robust.Server.GameObjects;
-
-namespace Content.Server.Explosion.EntitySystems;
-
-public sealed class ClusterGrenadeSystem : EntitySystem
-{
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly SharedContainerSystem _container = default!;
- [Dependency] private readonly ThrowingSystem _throwingSystem = default!;
- [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
- [Dependency] private readonly GunSystem _gun = default!;
- [Dependency] private readonly TransformSystem _transformSystem = default!;
- [Dependency] private readonly ContainerSystem _containerSystem = default!;
-
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent(OnClugInit);
- SubscribeLocalEvent(OnClugStartup);
- SubscribeLocalEvent(OnClugUsing);
- SubscribeLocalEvent(OnClugTrigger);
- }
-
- private void OnClugInit(EntityUid uid, ClusterGrenadeComponent component, ComponentInit args)
- {
- component.GrenadesContainer = _container.EnsureContainer(uid, "cluster-payload");
- }
-
- private void OnClugStartup(Entity clug, ref ComponentStartup args)
- {
- var component = clug.Comp;
- if (component.FillPrototype != null)
- {
- component.UnspawnedCount = Math.Max(0, component.MaxGrenades - component.GrenadesContainer.ContainedEntities.Count);
- UpdateAppearance(clug);
- }
- }
-
- private void OnClugUsing(Entity clug, ref InteractUsingEvent args)
- {
- if (args.Handled)
- return;
-
- var component = clug.Comp;
-
- // TODO: Should use whitelist.
- if (component.GrenadesContainer.ContainedEntities.Count >= component.MaxGrenades ||
- !HasComp(args.Used))
- return;
-
- _containerSystem.Insert(args.Used, component.GrenadesContainer);
- UpdateAppearance(clug);
- args.Handled = true;
- }
-
- private void OnClugTrigger(Entity clug, ref TriggerEvent args)
- {
- var component = clug.Comp;
- component.CountDown = true;
- args.Handled = true;
- }
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
- var query = EntityQueryEnumerator();
-
- while (query.MoveNext(out var uid, out var clug))
- {
- if (clug.CountDown && clug.UnspawnedCount > 0)
- {
- var grenadesInserted = clug.GrenadesContainer.ContainedEntities.Count + clug.UnspawnedCount;
- var thrownCount = 0;
- var segmentAngle = 360 / grenadesInserted;
- var grenadeDelay = 0f;
-
- while (TryGetGrenade(uid, clug, out var grenade))
- {
- // var distance = random.NextFloat() * _throwDistance;
- var angleMin = segmentAngle * thrownCount;
- var angleMax = segmentAngle * (thrownCount + 1);
- var angle = Angle.FromDegrees(_random.Next(angleMin, angleMax));
- if (clug.RandomAngle)
- angle = _random.NextAngle();
- thrownCount++;
-
- switch (clug.GrenadeType)
- {
- case GrenadeType.Shoot:
- ShootProjectile(grenade, angle, clug, uid);
- break;
- case GrenadeType.Throw:
- ThrowGrenade(grenade, angle, clug);
- break;
- }
-
- // give an active timer trigger to the contained grenades when they get launched
- if (clug.TriggerGrenades)
- {
- grenadeDelay += _random.NextFloat(clug.GrenadeTriggerIntervalMin, clug.GrenadeTriggerIntervalMax);
- var grenadeTimer = EnsureComp(grenade);
- grenadeTimer.TimeRemaining = (clug.BaseTriggerDelay + grenadeDelay);
- var ev = new ActiveTimerTriggerEvent(grenade, uid);
- RaiseLocalEvent(uid, ref ev);
- }
- }
- // delete the empty shell of the clusterbomb
- Del(uid);
- }
- }
- }
-
- private void ShootProjectile(EntityUid grenade, Angle angle, ClusterGrenadeComponent clug, EntityUid clugUid)
- {
- var direction = angle.ToVec().Normalized();
-
- if (clug.RandomSpread)
- direction = _random.NextVector2().Normalized();
-
- _gun.ShootProjectile(grenade, direction, Vector2.One.Normalized(), clugUid);
-
- }
-
- private void ThrowGrenade(EntityUid grenade, Angle angle, ClusterGrenadeComponent clug)
- {
- var direction = angle.ToVec().Normalized() * clug.Distance;
-
- if (clug.RandomSpread)
- direction = angle.ToVec().Normalized() * _random.NextFloat(clug.MinSpreadDistance, clug.MaxSpreadDistance);
-
- _throwingSystem.TryThrow(grenade, direction, clug.Velocity);
- }
-
- private bool TryGetGrenade(EntityUid clugUid, ClusterGrenadeComponent component, out EntityUid grenade)
- {
- grenade = default;
-
- if (component.UnspawnedCount > 0)
- {
- component.UnspawnedCount--;
- grenade = Spawn(component.FillPrototype, _transformSystem.GetMapCoordinates(clugUid));
- return true;
- }
-
- if (component.GrenadesContainer.ContainedEntities.Count > 0)
- {
- grenade = component.GrenadesContainer.ContainedEntities[0];
-
- // This shouldn't happen but you never know.
- if (!_containerSystem.Remove(grenade, component.GrenadesContainer))
- return false;
-
- return true;
- }
-
- return false;
- }
-
- private void UpdateAppearance(Entity clug)
- {
- var component = clug.Comp;
- if (!TryComp(clug, out var appearance))
- return;
-
- _appearance.SetData(clug, ClusterGrenadeVisuals.GrenadesCounter, component.GrenadesContainer.ContainedEntities.Count + component.UnspawnedCount, appearance);
- }
-}
diff --git a/Content.Server/Explosion/EntitySystems/ProjectileGrenadeSystem.cs b/Content.Server/Explosion/EntitySystems/ProjectileGrenadeSystem.cs
new file mode 100644
index 0000000000..555ce3399e
--- /dev/null
+++ b/Content.Server/Explosion/EntitySystems/ProjectileGrenadeSystem.cs
@@ -0,0 +1,110 @@
+using Content.Server.Explosion.Components;
+using Content.Server.Weapons.Ranged.Systems;
+using Robust.Server.GameObjects;
+using Robust.Shared.Containers;
+using Robust.Shared.Map;
+using Robust.Shared.Random;
+
+namespace Content.Server.Explosion.EntitySystems;
+
+public sealed class ProjectileGrenadeSystem : EntitySystem
+{
+ [Dependency] private readonly GunSystem _gun = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly SharedContainerSystem _container = default!;
+ [Dependency] private readonly TransformSystem _transformSystem = default!;
+
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnFragInit);
+ SubscribeLocalEvent(OnFragStartup);
+ SubscribeLocalEvent(OnFragTrigger);
+ }
+
+ private void OnFragInit(Entity entity, ref ComponentInit args)
+ {
+ entity.Comp.Container = _container.EnsureContainer(entity.Owner, "cluster-payload");
+ }
+
+ ///
+ /// Setting the unspawned count based on capacity so we know how many new entities to spawn
+ ///
+ private void OnFragStartup(Entity entity, ref ComponentStartup args)
+ {
+ if (entity.Comp.FillPrototype == null)
+ return;
+
+ entity.Comp.UnspawnedCount = Math.Max(0, entity.Comp.Capacity - entity.Comp.Container.ContainedEntities.Count);
+ }
+
+ ///
+ /// Can be triggered either by damage or the use in hand timer
+ ///
+ private void OnFragTrigger(Entity entity, ref TriggerEvent args)
+ {
+ FragmentIntoProjectiles(entity.Owner, entity.Comp);
+ args.Handled = true;
+ }
+
+ ///
+ /// Spawns projectiles at the coordinates of the grenade upon triggering
+ /// Can customize the angle and velocity the projectiles come out at
+ ///
+ private void FragmentIntoProjectiles(EntityUid uid, ProjectileGrenadeComponent component)
+ {
+ var grenadeCoord = _transformSystem.GetMapCoordinates(uid);
+ var shootCount = 0;
+ var totalCount = component.Container.ContainedEntities.Count + component.UnspawnedCount;
+ var segmentAngle = 360 / totalCount;
+
+ while (TrySpawnContents(grenadeCoord, component, out var contentUid))
+ {
+ Angle angle;
+ if (component.RandomAngle)
+ angle = _random.NextAngle();
+ else
+ {
+ var angleMin = segmentAngle * shootCount;
+ var angleMax = segmentAngle * (shootCount + 1);
+ angle = Angle.FromDegrees(_random.Next(angleMin, angleMax));
+ shootCount++;
+ }
+
+ // velocity is randomized to make the projectiles look
+ // slightly uneven, doesn't really change much, but it looks better
+ var direction = angle.ToVec().Normalized();
+ var velocity = _random.NextVector2(component.MinVelocity, component.MaxVelocity);
+ _gun.ShootProjectile(contentUid, direction, velocity, uid, null);
+ }
+ }
+
+ ///
+ /// Spawns one instance of the fill prototype or contained entity at the coordinate indicated
+ ///
+ private bool TrySpawnContents(MapCoordinates spawnCoordinates, ProjectileGrenadeComponent component, out EntityUid contentUid)
+ {
+ contentUid = default;
+
+ if (component.UnspawnedCount > 0)
+ {
+ component.UnspawnedCount--;
+ contentUid = Spawn(component.FillPrototype, spawnCoordinates);
+ return true;
+ }
+
+ if (component.Container.ContainedEntities.Count > 0)
+ {
+ contentUid = component.Container.ContainedEntities[0];
+
+ if (!_container.Remove(contentUid, component.Container))
+ return false;
+
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/Content.Server/Explosion/EntitySystems/ScatteringGrenadeSystem.cs b/Content.Server/Explosion/EntitySystems/ScatteringGrenadeSystem.cs
new file mode 100644
index 0000000000..2657ba3449
--- /dev/null
+++ b/Content.Server/Explosion/EntitySystems/ScatteringGrenadeSystem.cs
@@ -0,0 +1,122 @@
+using Content.Shared.Explosion.Components;
+using Content.Shared.Throwing;
+using Robust.Server.GameObjects;
+using Robust.Shared.Containers;
+using Robust.Shared.Map;
+using Robust.Shared.Random;
+using System.Numerics;
+using Content.Shared.Explosion.EntitySystems;
+
+namespace Content.Server.Explosion.EntitySystems;
+
+public sealed class ScatteringGrenadeSystem : SharedScatteringGrenadeSystem
+{
+ [Dependency] private readonly SharedContainerSystem _container = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly ThrowingSystem _throwingSystem = default!;
+ [Dependency] private readonly TransformSystem _transformSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnScatteringTrigger);
+ }
+
+ ///
+ /// Can be triggered either by damage or the use in hand timer, either way
+ /// will store the event happening in IsTriggered for the next frame update rather than
+ /// handling it here to prevent crashing the game
+ ///
+ private void OnScatteringTrigger(Entity entity, ref TriggerEvent args)
+ {
+ entity.Comp.IsTriggered = true;
+ args.Handled = true;
+ }
+
+ ///
+ /// Every frame update we look for scattering grenades that were triggered (by damage or timer)
+ /// Then we spawn the contents, throw them, optionally trigger them, then delete the original scatter grenade entity
+ ///
+ public override void Update(float frametime)
+ {
+ base.Update(frametime);
+ var query = EntityQueryEnumerator();
+
+ while (query.MoveNext(out var uid, out var component))
+ {
+ var totalCount = component.Container.ContainedEntities.Count + component.UnspawnedCount;
+
+ // if triggered while empty, (if it's blown up while empty) it'll just delete itself
+ if (component.IsTriggered && totalCount > 0)
+ {
+ var grenadeCoord = _transformSystem.GetMapCoordinates(uid);
+ var thrownCount = 0;
+ var segmentAngle = 360 / totalCount;
+ var additionalIntervalDelay = 0f;
+
+ while (TrySpawnContents(grenadeCoord, component, out var contentUid))
+ {
+ Angle angle;
+ if (component.RandomAngle)
+ angle = _random.NextAngle();
+ else
+ {
+ var angleMin = segmentAngle * thrownCount;
+ var angleMax = segmentAngle * (thrownCount + 1);
+ angle = Angle.FromDegrees(_random.Next(angleMin, angleMax));
+ thrownCount++;
+ }
+
+ Vector2 direction = angle.ToVec().Normalized();
+ if (component.RandomDistance)
+ direction *= _random.NextFloat(component.RandomThrowDistanceMin, component.RandomThrowDistanceMax);
+ else
+ direction *= component.Distance;
+
+ _throwingSystem.TryThrow(contentUid, direction, component.Velocity);
+
+ if (component.TriggerContents)
+ {
+ additionalIntervalDelay += _random.NextFloat(component.IntervalBetweenTriggersMin, component.IntervalBetweenTriggersMax);
+ var contentTimer = EnsureComp(contentUid);
+ contentTimer.TimeRemaining = component.DelayBeforeTriggerContents + additionalIntervalDelay;
+ var ev = new ActiveTimerTriggerEvent(contentUid, uid);
+ RaiseLocalEvent(contentUid, ref ev);
+ }
+ }
+
+ // Normally we'd use DeleteOnTrigger but because we need to wait for the frame update
+ // we have to delete it here instead
+ Del(uid);
+ }
+ }
+ }
+
+ ///
+ /// Spawns one instance of the fill prototype or contained entity at the coordinate indicated
+ ///
+ private bool TrySpawnContents(MapCoordinates spawnCoordinates, ScatteringGrenadeComponent component, out EntityUid contentUid)
+ {
+ contentUid = default;
+
+ if (component.UnspawnedCount > 0)
+ {
+ component.UnspawnedCount--;
+ contentUid = Spawn(component.FillPrototype, spawnCoordinates);
+ return true;
+ }
+
+ if (component.Container.ContainedEntities.Count > 0)
+ {
+ contentUid = component.Container.ContainedEntities[0];
+
+ if (!_container.Remove(contentUid, component.Container))
+ return false;
+
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/Content.Shared/Damage/Systems/RequireProjectileTargetSystem.cs b/Content.Shared/Damage/Systems/RequireProjectileTargetSystem.cs
index 12838eb04d..66b1de65e8 100644
--- a/Content.Shared/Damage/Systems/RequireProjectileTargetSystem.cs
+++ b/Content.Shared/Damage/Systems/RequireProjectileTargetSystem.cs
@@ -34,6 +34,11 @@ public sealed class RequireProjectileTargetSystem : EntitySystem
if (!shooter.HasValue)
return;
+ // ProjectileGrenades delete the entity that's shooting the projectile,
+ // so it's impossible to check if the entity is in a container
+ if (TerminatingOrDeleted(shooter.Value))
+ return;
+
if (!_container.IsEntityOrParentInContainer(shooter.Value))
args.Cancelled = true;
}
diff --git a/Content.Shared/Explosion/Components/ScatteringGrenadeComponent.cs b/Content.Shared/Explosion/Components/ScatteringGrenadeComponent.cs
new file mode 100644
index 0000000000..be27c49ffa
--- /dev/null
+++ b/Content.Shared/Explosion/Components/ScatteringGrenadeComponent.cs
@@ -0,0 +1,109 @@
+using Content.Shared.Explosion.EntitySystems;
+using Content.Shared.Whitelist;
+using Robust.Shared.Containers;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Explosion.Components;
+
+///
+/// Use this component if the grenade splits into entities that make use of Timers
+/// or if you just want it to throw entities out in the world
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedScatteringGrenadeSystem))]
+public sealed partial class ScatteringGrenadeComponent : Component
+{
+ public Container Container = default!;
+
+ [DataField]
+ public EntityWhitelist? Whitelist;
+
+ ///
+ /// What we fill our prototype with if we want to pre-spawn with entities.
+ ///
+ [DataField]
+ public EntProtoId? FillPrototype;
+
+ ///
+ /// If we have a pre-fill how many more can we spawn.
+ ///
+ [AutoNetworkedField]
+ public int UnspawnedCount;
+
+ ///
+ /// Max amount of entities inside the container
+ ///
+ [DataField]
+ public int Capacity = 3;
+
+ ///
+ /// Decides if contained entities trigger after getting launched
+ ///
+ [DataField]
+ public bool TriggerContents = true;
+
+ #region Trigger time parameters for scattered entities
+ ///
+ /// Minimum delay in seconds before any entities start to be triggered.
+ ///
+ [DataField]
+ public float DelayBeforeTriggerContents = 1.0f;
+
+ ///
+ /// Maximum delay in seconds to add between individual entity triggers
+ ///
+ [DataField]
+ public float IntervalBetweenTriggersMax;
+
+ ///
+ /// Minimum delay in seconds to add between individual entity triggers
+ ///
+ [DataField]
+ public float IntervalBetweenTriggersMin;
+ #endregion
+
+ #region Throwing parameters for the scattered entities
+ ///
+ /// Should the angle the entities get thrown at be random
+ /// instead of uniformly distributed
+ ///
+ [DataField]
+ public bool RandomAngle;
+
+ ///
+ /// The speed at which the entities get thrown
+ ///
+ [DataField]
+ public float Velocity = 5;
+
+ ///
+ /// Static distance grenades will be thrown to if RandomDistance is false.
+ ///
+ [DataField]
+ public float Distance = 1f;
+
+ ///
+ /// Should the distance the entities get thrown be random
+ ///
+ [DataField]
+ public bool RandomDistance;
+
+ ///
+ /// Max distance grenades can randomly be thrown to.
+ ///
+ [DataField]
+ public float RandomThrowDistanceMax = 2.5f;
+
+ ///
+ /// Minimal distance grenades can randomly be thrown to.
+ ///
+ [DataField]
+ public float RandomThrowDistanceMin;
+ #endregion
+
+ ///
+ /// Whether the main grenade has been triggered or not
+ /// We need to store this because we are only allowed to spawn and trigger timed entities on the next available frame update
+ ///
+ public bool IsTriggered = false;
+}
diff --git a/Content.Shared/Explosion/EntitySystems/SharedScatteringGrenadeSystem.cs b/Content.Shared/Explosion/EntitySystems/SharedScatteringGrenadeSystem.cs
new file mode 100644
index 0000000000..b704fbf86f
--- /dev/null
+++ b/Content.Shared/Explosion/EntitySystems/SharedScatteringGrenadeSystem.cs
@@ -0,0 +1,70 @@
+using Content.Shared.Explosion.Components;
+using Content.Shared.Interaction;
+using Content.Shared.Whitelist;
+using Robust.Shared.Containers;
+
+namespace Content.Shared.Explosion.EntitySystems;
+
+public abstract class SharedScatteringGrenadeSystem : EntitySystem
+{
+ [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly SharedContainerSystem _container = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnScatteringInit);
+ SubscribeLocalEvent(OnScatteringStartup);
+ SubscribeLocalEvent(OnScatteringInteractUsing);
+ }
+
+ private void OnScatteringInit(Entity entity, ref ComponentInit args)
+ {
+ entity.Comp.Container = _container.EnsureContainer(entity.Owner, "cluster-payload");
+ }
+
+ ///
+ /// Setting the unspawned count based on capacity, so we know how many new entities to spawn
+ /// Update appearance based on initial fill amount
+ ///
+ private void OnScatteringStartup(Entity entity, ref ComponentStartup args)
+ {
+ if (entity.Comp.FillPrototype == null)
+ return;
+
+ entity.Comp.UnspawnedCount = Math.Max(0, entity.Comp.Capacity - entity.Comp.Container.ContainedEntities.Count);
+ UpdateAppearance(entity);
+ Dirty(entity, entity.Comp);
+
+ }
+
+ ///
+ /// There are some scattergrenades you can fill up with more grenades (like clusterbangs)
+ /// This covers how you insert more into it
+ ///
+ private void OnScatteringInteractUsing(Entity entity, ref InteractUsingEvent args)
+ {
+ if (entity.Comp.Whitelist == null)
+ return;
+
+ if (args.Handled || !_whitelistSystem.IsValid(entity.Comp.Whitelist, args.Used))
+ return;
+
+ _container.Insert(args.Used, entity.Comp.Container);
+ UpdateAppearance(entity);
+ args.Handled = true;
+ }
+
+ ///
+ /// Update appearance based off of total count of contents
+ ///
+ private void UpdateAppearance(Entity entity)
+ {
+ if (!TryComp(entity, out var appearanceComponent))
+ return;
+
+ _appearance.SetData(entity, ClusterGrenadeVisuals.GrenadesCounter, entity.Comp.UnspawnedCount + entity.Comp.Container.ContainedEntities.Count, appearanceComponent);
+ }
+}
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/clusterbang.yml b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/clusterbang.yml
deleted file mode 100644
index b041349d26..0000000000
--- a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/clusterbang.yml
+++ /dev/null
@@ -1,264 +0,0 @@
-- type: entity
- parent: [BaseItem, BaseRestrictedContraband]
- id: ClusterBang
- name: clusterbang
- description: Can be used only with flashbangs. Explodes several times.
- components:
- - type: Sprite
- sprite: Objects/Weapons/Grenades/clusterbang.rsi
- state: base-0
- - type: Appearance
- - type: ClusterGrenadeVisuals
- state: base
- - type: ClusterGrenade
- - type: OnUseTimerTrigger
- delay: 3.5
- - type: ContainerContainer
- containers:
- cluster-payload: !type:Container
-
-- type: entity
- parent: GrenadeBase
- id: ClusterBangFull
- name: clusterbang
- description: Launches three flashbangs after the timer runs out.
- suffix: Full
- components:
- - type: Sprite
- sprite: Objects/Weapons/Grenades/clusterbang.rsi
- layers:
- - state: icon
- map: ["enum.TriggerVisualLayers.Base"]
- - type: ClusterGrenade
- fillPrototype: GrenadeFlashBang
- distance: 7
- velocity: 7
- - type: TimerTriggerVisuals
- primingSound:
- path: /Audio/Effects/countdown.ogg
- - type: GenericVisualizer
- visuals:
- enum.Trigger.TriggerVisuals.VisualState:
- enum.ConstructionVisuals.Layer:
- Primed: { state: primed }
- Unprimed: { state: icon }
- - type: EmitSoundOnTrigger
- sound:
- path: "/Audio/Machines/door_lock_off.ogg"
- - type: ContainerContainer
- containers:
- cluster-payload: !type:Container
-
-- type: entity
- parent: [GrenadeBase, BaseSyndicateContraband]
- id: ClusterGrenade
- name: clustergrenade
- description: Why use one grenade when you can use three at once!
- components:
- - type: Sprite
- sprite: Objects/Weapons/Grenades/clusterbomb.rsi
- layers:
- - state: icon
- map: ["enum.TriggerVisualLayers.Base"]
- - type: ClusterGrenade
- fillPrototype: ExGrenade
- velocity: 3.5
- distance: 5
- - type: OnUseTimerTrigger
- beepSound:
- path: "/Audio/Effects/beep1.ogg"
- params:
- volume: 5
- initialBeepDelay: 0
- beepInterval: 0.5
- - type: EmitSoundOnTrigger
- sound:
- path: "/Audio/Machines/door_lock_off.ogg"
- - type: ContainerContainer
- containers:
- cluster-payload: !type:Container
-
-- type: entity
- parent: [BaseItem, BaseSyndicateContraband]
- id: ClusterBananaPeel
- name: cluster banana peel
- description: Splits into 6 explosive banana peels after throwing, guaranteed fun!
- components:
- - type: Sprite
- sprite: Objects/Specific/Hydroponics/banana.rsi
- state: produce
- - type: Appearance
- - type: ClusterGrenade
- fillPrototype: TrashBananaPeelExplosive
- maxGrenadesCount: 6
- baseTriggerDelay: 20
- - type: DamageOnLand
- damage:
- types:
- Blunt: 10
- - type: LandAtCursor
- - type: Damageable
- damageContainer: Inorganic
- - type: EmitSoundOnTrigger
- sound:
- path: "/Audio/Items/bikehorn.ogg"
- - type: Destructible
- thresholds:
- - trigger:
- !type:DamageTrigger
- damage: 10
- behaviors:
- - !type:TriggerBehavior
- - !type:DoActsBehavior
- acts: ["Destruction"]
- - type: ContainerContainer
- containers:
- cluster-payload: !type:Container
-
-- type: entity
- parent: [GrenadeBase, BaseSecurityContraband]
- id: GrenadeStinger
- name: stinger grenade
- description: Nothing to see here, please disperse.
- components:
- - type: Sprite
- sprite: Objects/Weapons/Grenades/stingergrenade.rsi
- layers:
- - state: icon
- map: ["enum.TriggerVisualLayers.Base"]
- - type: ClusterGrenade
- fillPrototype: PelletClusterRubber
- maxGrenadesCount: 30
- grenadeType: enum.GrenadeType.Shoot
- - type: FlashOnTrigger
- range: 7
- - type: EmitSoundOnTrigger
- sound:
- path: "/Audio/Effects/flash_bang.ogg"
- - type: SpawnOnTrigger
- proto: GrenadeFlashEffect
- - type: TimerTriggerVisuals
- primingSound:
- path: /Audio/Effects/countdown.ogg
- - type: ContainerContainer
- containers:
- cluster-payload: !type:Container
-
-- type: entity
- parent: [GrenadeBase, BaseSyndicateContraband]
- id: GrenadeIncendiary
- name: incendiary grenade
- description: Guaranteed to light up the mood.
- components:
- - type: Sprite
- sprite: Objects/Weapons/Grenades/pyrogrenade.rsi
- layers:
- - state: icon
- map: ["enum.TriggerVisualLayers.Base"]
- - type: ClusterGrenade
- fillPrototype: PelletClusterIncendiary
- maxGrenadesCount: 30
- grenadeType: enum.GrenadeType.Shoot
- - type: OnUseTimerTrigger
- beepSound:
- path: "/Audio/Effects/beep1.ogg"
- params:
- volume: 5
- initialBeepDelay: 0
- beepInterval: 2
- - type: EmitSoundOnTrigger
- sound:
- path: "/Audio/Weapons/Guns/Gunshots/batrifle.ogg"
- - type: ContainerContainer
- containers:
- cluster-payload: !type:Container
-
-- type: entity
- parent: [GrenadeBase, BaseSyndicateContraband]
- id: GrenadeShrapnel
- name: shrapnel grenade
- description: Releases a deadly spray of shrapnel that causes severe bleeding.
- components:
- - type: Sprite
- sprite: Objects/Weapons/Grenades/shrapnelgrenade.rsi
- layers:
- - state: icon
- map: ["enum.TriggerVisualLayers.Base"]
- - type: ClusterGrenade
- fillPrototype: PelletClusterLethal
- maxGrenadesCount: 30
- grenadeType: enum.GrenadeType.Shoot
- - type: OnUseTimerTrigger
- beepSound:
- path: "/Audio/Effects/beep1.ogg"
- params:
- volume: 5
- initialBeepDelay: 0
- beepInterval: 2
- - type: EmitSoundOnTrigger
- sound:
- path: "/Audio/Weapons/Guns/Gunshots/batrifle.ogg"
- - type: ContainerContainer
- containers:
- cluster-payload: !type:Container
-
-- type: entity
- parent: SoapSyndie
- id: SlipocalypseClusterSoap
- name: slipocalypse clustersoap
- description: Spreads small pieces of syndicate soap over an area upon landing on the floor.
- components:
- - type: Sprite
- sprite: Objects/Specific/Janitorial/soap.rsi
- layers:
- - state: syndie-4
- - type: Appearance
- - type: ClusterGrenade
- fillPrototype: SoapletSyndie
- maxGrenadesCount: 30
- grenadeTriggerIntervalMax: 0
- grenadeTriggerIntervalMin: 0
- baseTriggerDelay: 60
- randomSpread: true
- velocity: 3
- - type: DamageOnLand
- damage:
- types:
- Blunt: 10
- - type: LandAtCursor
- - type: EmitSoundOnTrigger
- sound:
- path: "/Audio/Effects/flash_bang.ogg"
- - type: Damageable
- damageContainer: Inorganic
- - type: Destructible
- thresholds:
- - trigger:
- !type:DamageTrigger
- damage: 10
- behaviors:
- - !type:TriggerBehavior
- - !type:DoActsBehavior
- acts: ["Destruction"]
- - type: ContainerContainer
- containers:
- cluster-payload: !type:Container
-
-- type: entity
- parent: GrenadeShrapnel
- id: GrenadeFoamDart
- name: foam dart grenade
- description: Releases a bothersome spray of foam darts that cause severe welching.
- components:
- - type: Sprite
- sprite: Objects/Weapons/Grenades/foamdart.rsi
- layers:
- - state: icon
- map: ["Base"]
- - state: primed
- map: ["enum.TriggerVisualLayers.Base"]
- - type: ClusterGrenade
- fillPrototype: BulletFoam
- maxGrenadesCount: 30
- grenadeType: enum.GrenadeType.Throw
- velocity: 70
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml
index 8f253097ab..08d6d855a8 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml
@@ -81,6 +81,9 @@
guides:
- Security
- Antagonists
+ - type: Tag
+ tags:
+ - GrenadeFlashBang
- type: entity
id: GrenadeFlashEffect
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/projectile_grenades.yml b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/projectile_grenades.yml
new file mode 100644
index 0000000000..07c2a7ad3c
--- /dev/null
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/projectile_grenades.yml
@@ -0,0 +1,105 @@
+- type: entity
+ abstract: true
+ parent: BaseItem
+ id: ProjectileGrenadeBase
+ components:
+ - type: Appearance
+ - type: Damageable
+ damageContainer: Inorganic
+ - type: DeleteOnTrigger
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 10
+ behaviors:
+ - !type:TriggerBehavior
+ - type: ContainerContainer
+ containers:
+ cluster-payload: !type:Container
+ - type: ProjectileGrenade
+
+- type: entity
+ parent: [ProjectileGrenadeBase, BaseRestrictedContraband]
+ id: GrenadeStinger
+ name: stinger grenade
+ description: Nothing to see here, please disperse.
+ components:
+ - type: Sprite
+ sprite: Objects/Weapons/Grenades/stingergrenade.rsi
+ layers:
+ - state: icon
+ map: ["enum.TriggerVisualLayers.Base"]
+ - type: ProjectileGrenade
+ fillPrototype: PelletClusterRubber
+ capacity: 30
+ - type: FlashOnTrigger
+ range: 7
+ - type: EmitSoundOnTrigger
+ sound:
+ path: "/Audio/Effects/flash_bang.ogg"
+ - type: SpawnOnTrigger
+ proto: GrenadeFlashEffect
+ - type: OnUseTimerTrigger
+ beepSound:
+ path: "/Audio/Effects/beep1.ogg"
+ params:
+ volume: 5
+ initialBeepDelay: 0
+ beepInterval: 2
+ delay: 3.5
+ - type: TimerTriggerVisuals
+ primingSound:
+ path: /Audio/Effects/countdown.ogg
+
+- type: entity
+ parent: [ProjectileGrenadeBase, BaseSyndicateContraband]
+ id: GrenadeIncendiary
+ name: incendiary grenade
+ description: Guaranteed to light up the mood.
+ components:
+ - type: Sprite
+ sprite: Objects/Weapons/Grenades/pyrogrenade.rsi
+ layers:
+ - state: icon
+ map: ["enum.TriggerVisualLayers.Base"]
+ - type: ProjectileGrenade
+ fillPrototype: PelletClusterIncendiary
+ capacity: 30
+ - type: OnUseTimerTrigger
+ beepSound:
+ path: "/Audio/Effects/beep1.ogg"
+ params:
+ volume: 5
+ initialBeepDelay: 0
+ beepInterval: 2
+ delay: 3.5
+ - type: EmitSoundOnTrigger
+ sound:
+ path: "/Audio/Weapons/Guns/Gunshots/batrifle.ogg"
+
+- type: entity
+ parent: [ProjectileGrenadeBase, BaseSyndicateContraband]
+ id: GrenadeShrapnel
+ name: shrapnel grenade
+ description: Releases a deadly spray of shrapnel that causes severe bleeding.
+ components:
+ - type: Sprite
+ sprite: Objects/Weapons/Grenades/shrapnelgrenade.rsi
+ layers:
+ - state: icon
+ map: ["enum.TriggerVisualLayers.Base"]
+ - type: ProjectileGrenade
+ fillPrototype: PelletClusterLethal
+ capacity: 30
+ - type: OnUseTimerTrigger
+ beepSound:
+ path: "/Audio/Effects/beep1.ogg"
+ params:
+ volume: 5
+ initialBeepDelay: 0
+ beepInterval: 2
+ delay: 3.5
+ - type: EmitSoundOnTrigger
+ sound:
+ path: "/Audio/Weapons/Guns/Gunshots/batrifle.ogg"
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/scattering_grenades.yml b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/scattering_grenades.yml
new file mode 100644
index 0000000000..f68498bb51
--- /dev/null
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/scattering_grenades.yml
@@ -0,0 +1,174 @@
+# ScatteringGrenade is intended for grenades that spawn entities, especially those with timers
+- type: entity
+ abstract: true
+ parent: BaseItem
+ id: ScatteringGrenadeBase
+ components:
+ - type: Appearance
+ - type: ContainerContainer
+ containers:
+ cluster-payload: !type:Container
+ - type: Damageable
+ damageContainer: Inorganic
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 10
+ behaviors:
+ - !type:TriggerBehavior
+ - type: ScatteringGrenade
+
+- type: entity
+ parent: [ScatteringGrenadeBase, BaseRestrictedContraband]
+ id: ClusterBang
+ name: clusterbang
+ description: Can be used only with flashbangs. Explodes several times.
+ components:
+ - type: ScatteringGrenade
+ whitelist:
+ tags:
+ - GrenadeFlashBang
+ distance: 6
+ velocity: 6
+ - type: ClusterGrenadeVisuals
+ state: base
+ - type: Sprite
+ sprite: Objects/Weapons/Grenades/clusterbang.rsi
+ state: base-0
+ - type: OnUseTimerTrigger
+ delay: 3.5
+
+- type: entity
+ parent: ClusterBang
+ id: ClusterBangFull
+ name: ClusterBang
+ description: Launches three flashbangs after the timer runs out.
+ suffix: Full
+ components:
+ - type: Sprite
+ sprite: Objects/Weapons/Grenades/clusterbang.rsi
+ layers:
+ - state: icon
+ map: ["enum.TriggerVisualLayers.Base"]
+ - type: ScatteringGrenade
+ whitelist:
+ tags:
+ - GrenadeFlashBang
+ fillPrototype: GrenadeFlashBang
+ distance: 6
+ velocity: 6
+ - type: TimerTriggerVisuals
+ primingSound:
+ path: /Audio/Effects/countdown.ogg
+ - type: GenericVisualizer
+ visuals:
+ enum.Trigger.TriggerVisuals.VisualState:
+ enum.ConstructionVisuals.Layer:
+ Primed: { state: primed }
+ Unprimed: { state: icon }
+ - type: EmitSoundOnTrigger
+ sound:
+ path: "/Audio/Machines/door_lock_off.ogg"
+
+- type: entity
+ parent: [ScatteringGrenadeBase, BaseSyndicateContraband]
+ id: ClusterGrenade
+ name: clustergrenade
+ description: Why use one grenade when you can use three at once!
+ components:
+ - type: Sprite
+ sprite: Objects/Weapons/Grenades/clusterbomb.rsi
+ layers:
+ - state: icon
+ map: ["enum.TriggerVisualLayers.Base"]
+ - type: ScatteringGrenade
+ fillPrototype: ExGrenade
+ distance: 4
+ - type: OnUseTimerTrigger
+ beepSound:
+ path: "/Audio/Effects/beep1.ogg"
+ params:
+ volume: 5
+ initialBeepDelay: 0
+ beepInterval: 0.5
+ delay: 3.5
+ - type: EmitSoundOnTrigger
+ sound:
+ path: "/Audio/Machines/door_lock_off.ogg"
+
+- type: entity
+ parent: [ScatteringGrenadeBase, BaseSyndicateContraband]
+ id: ClusterBananaPeel
+ name: cluster banana peel
+ description: Splits into 6 explosive banana peels after throwing, guaranteed fun!
+ components:
+ - type: Sprite
+ sprite: Objects/Specific/Hydroponics/banana.rsi
+ state: produce
+ - type: ScatteringGrenade
+ fillPrototype: TrashBananaPeelExplosive
+ capacity: 6
+ delayBeforeTriggerContents: 20
+ - type: LandAtCursor
+ - type: DamageOnLand
+ damage:
+ types:
+ Blunt: 10
+ - type: EmitSoundOnTrigger
+ sound:
+ path: "/Audio/Items/bikehorn.ogg"
+
+- type: entity
+ parent: [SoapSyndie, ScatteringGrenadeBase, BaseSyndicateContraband]
+ id: SlipocalypseClusterSoap
+ name: slipocalypse clustersoap
+ description: Spreads small pieces of syndicate soap over an area upon landing on the floor.
+ components:
+ - type: Sprite
+ sprite: Objects/Specific/Janitorial/soap.rsi
+ layers:
+ - state: syndie-4
+ - type: ScatteringGrenade
+ fillPrototype: SoapletSyndie
+ capacity: 30
+ delayBeforeTriggerContents: 60
+ randomDistance: true
+ randomThrowDistanceMax: 3
+ - type: LandAtCursor
+ - type: DamageOnLand
+ damage:
+ types:
+ Blunt: 10
+ - type: EmitSoundOnTrigger
+ sound:
+ path: "/Audio/Effects/flash_bang.ogg"
+
+- type: entity
+ parent: ScatteringGrenadeBase
+ id: GrenadeFoamDart
+ name: foam dart grenade
+ description: Releases a bothersome spray of foam darts that cause severe welching.
+ components:
+ - type: Sprite
+ sprite: Objects/Weapons/Grenades/foamdart.rsi
+ layers:
+ - state: icon
+ map: ["Base"]
+ - state: primed
+ map: ["enum.TriggerVisualLayers.Base"]
+ - type: ScatteringGrenade
+ fillPrototype: BulletFoam
+ capacity: 30
+ velocity: 30
+ - type: OnUseTimerTrigger
+ beepSound:
+ path: "/Audio/Effects/beep1.ogg"
+ params:
+ volume: 5
+ initialBeepDelay: 0
+ beepInterval: 2
+ delay: 3.5
+ - type: EmitSoundOnTrigger
+ sound:
+ path: "/Audio/Weapons/Guns/Gunshots/batrifle.ogg"
diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml
index 6112f93c16..5085894ce5 100644
--- a/Resources/Prototypes/tags.yml
+++ b/Resources/Prototypes/tags.yml
@@ -611,6 +611,9 @@
- type: Tag
id: Grenade
+- type: Tag
+ id: GrenadeFlashBang
+
- type: Tag
id: HudMedical
--
2.51.2