]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Cluster grenade refactor and contra markings (#31108)
authorPlykiya <58439124+Plykiya@users.noreply.github.com>
Mon, 16 Dec 2024 12:08:07 +0000 (04:08 -0800)
committerGitHub <noreply@github.com>
Mon, 16 Dec 2024 12:08:07 +0000 (13:08 +0100)
* Cluster grenade refactor

* oopsies on the name

* Solve client-side errors

* reviews addressed

* filling scattering grenades is now predicted

* reviews addressed

14 files changed:
Content.Client/Explosion/ScatteringGrenadeSystem.cs [new file with mode: 0644]
Content.Server/Explosion/Components/ClusterGrenadeComponent.cs [deleted file]
Content.Server/Explosion/Components/ProjectileGrenadeComponent.cs [new file with mode: 0644]
Content.Server/Explosion/EntitySystems/ClusterGrenadeSystem.cs [deleted file]
Content.Server/Explosion/EntitySystems/ProjectileGrenadeSystem.cs [new file with mode: 0644]
Content.Server/Explosion/EntitySystems/ScatteringGrenadeSystem.cs [new file with mode: 0644]
Content.Shared/Damage/Systems/RequireProjectileTargetSystem.cs
Content.Shared/Explosion/Components/ScatteringGrenadeComponent.cs [new file with mode: 0644]
Content.Shared/Explosion/EntitySystems/SharedScatteringGrenadeSystem.cs [new file with mode: 0644]
Resources/Prototypes/Entities/Objects/Weapons/Throwable/clusterbang.yml [deleted file]
Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml
Resources/Prototypes/Entities/Objects/Weapons/Throwable/projectile_grenades.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Objects/Weapons/Throwable/scattering_grenades.yml [new file with mode: 0644]
Resources/Prototypes/tags.yml

diff --git a/Content.Client/Explosion/ScatteringGrenadeSystem.cs b/Content.Client/Explosion/ScatteringGrenadeSystem.cs
new file mode 100644 (file)
index 0000000..2897677
--- /dev/null
@@ -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 (file)
index fe1b8ca..0000000
+++ /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!;
-
-        /// <summary>
-        ///     What we fill our prototype with if we want to pre-spawn with grenades.
-        /// </summary>
-        [DataField("fillPrototype")]
-        public EntProtoId? FillPrototype;
-
-        /// <summary>
-        ///     If we have a pre-fill how many more can we spawn.
-        /// </summary>
-        public int UnspawnedCount;
-
-        /// <summary>
-        ///     Maximum grenades in the container.
-        /// </summary>
-        [DataField("maxGrenadesCount")]
-        public int MaxGrenades = 3;
-
-        /// <summary>
-        ///     Maximum delay in seconds between individual grenade triggers
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("grenadeTriggerIntervalMax")]
-        public float GrenadeTriggerIntervalMax = 0f;
-
-        /// <summary>
-        ///     Minimum delay in seconds between individual grenade triggers
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("grenadeTriggerIntervalMin")]
-        public float GrenadeTriggerIntervalMin = 0f;
-
-        /// <summary>
-        ///     Minimum delay in seconds before any grenades start to be triggered.
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("baseTriggerDelay")]
-        public float BaseTriggerDelay = 1.0f;
-
-        /// <summary>
-        ///     Decides if grenades trigger after getting launched
-        /// </summary>
-        [DataField("triggerGrenades")]
-        public bool TriggerGrenades = true;
-
-        /// <summary>
-        ///     Does the cluster grenade shoot or throw
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("grenadeType")]
-        public Enum GrenadeType = Components.GrenadeType.Throw;
-
-        /// <summary>
-        ///     The speed at which grenades get thrown
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("velocity")]
-        public float Velocity = 5;
-
-        /// <summary>
-        ///     Should the spread be random
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("randomSpread")]
-        public bool RandomSpread = false;
-
-        /// <summary>
-        ///     Should the angle be random
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("randomAngle")]
-        public bool RandomAngle = false;
-
-        /// <summary>
-        ///     Static distance grenades will be thrown to.
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("distance")]
-        public float Distance = 1f;
-
-        /// <summary>
-        ///     Max distance grenades should randomly be thrown to.
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("maxSpreadDistance")]
-        public float MaxSpreadDistance = 2.5f;
-
-        /// <summary>
-        ///     Minimal distance grenades should randomly be thrown to.
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("minSpreadDistance")]
-        public float MinSpreadDistance = 0f;
-
-        /// <summary>
-        ///     This is the end.
-        /// </summary>
-        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 (file)
index 0000000..58d687e
--- /dev/null
@@ -0,0 +1,48 @@
+using Content.Server.Explosion.EntitySystems;
+using Robust.Shared.Containers;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Explosion.Components;
+/// <summary>
+/// Grenades that, when triggered, explode into projectiles
+/// </summary>
+[RegisterComponent, Access(typeof(ProjectileGrenadeSystem))]
+public sealed partial class ProjectileGrenadeComponent : Component
+{
+    public Container Container = default!;
+
+    /// <summary>
+    /// The kind of projectile that the prototype is filled with.
+    /// </summary>
+    [DataField]
+    public EntProtoId? FillPrototype;
+
+    /// <summary>
+    ///     If we have a pre-fill how many more can we spawn.
+    /// </summary>
+    public int UnspawnedCount;
+
+    /// <summary>
+    ///     Total amount of projectiles
+    /// </summary>
+    [DataField]
+    public int Capacity = 3;
+
+    /// <summary>
+    ///     Should the angle of the projectiles be uneven?
+    /// </summary>
+    [DataField]
+    public bool RandomAngle = false;
+
+    /// <summary>
+    /// The minimum speed the projectiles may come out at
+    /// </summary>
+    [DataField]
+    public float MinVelocity = 2f;
+
+    /// <summary>
+    /// The maximum speed the projectiles may come out at
+    /// </summary>
+    [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 (file)
index 78e41c5..0000000
+++ /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<ClusterGrenadeComponent, ComponentInit>(OnClugInit);
-        SubscribeLocalEvent<ClusterGrenadeComponent, ComponentStartup>(OnClugStartup);
-        SubscribeLocalEvent<ClusterGrenadeComponent, InteractUsingEvent>(OnClugUsing);
-        SubscribeLocalEvent<ClusterGrenadeComponent, TriggerEvent>(OnClugTrigger);
-    }
-
-    private void OnClugInit(EntityUid uid, ClusterGrenadeComponent component, ComponentInit args)
-    {
-        component.GrenadesContainer = _container.EnsureContainer<Container>(uid, "cluster-payload");
-    }
-
-    private void OnClugStartup(Entity<ClusterGrenadeComponent> 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<ClusterGrenadeComponent> clug, ref InteractUsingEvent args)
-    {
-        if (args.Handled)
-            return;
-
-        var component = clug.Comp;
-
-        // TODO: Should use whitelist.
-        if (component.GrenadesContainer.ContainedEntities.Count >= component.MaxGrenades ||
-            !HasComp<FlashOnTriggerComponent>(args.Used))
-            return;
-
-        _containerSystem.Insert(args.Used, component.GrenadesContainer);
-        UpdateAppearance(clug);
-        args.Handled = true;
-    }
-
-    private void OnClugTrigger(Entity<ClusterGrenadeComponent> 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<ClusterGrenadeComponent>();
-
-        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<ActiveTimerTriggerComponent>(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<ClusterGrenadeComponent> clug)
-    {
-        var component = clug.Comp;
-        if (!TryComp<AppearanceComponent>(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 (file)
index 0000000..555ce33
--- /dev/null
@@ -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<ProjectileGrenadeComponent, ComponentInit>(OnFragInit);
+        SubscribeLocalEvent<ProjectileGrenadeComponent, ComponentStartup>(OnFragStartup);
+        SubscribeLocalEvent<ProjectileGrenadeComponent, TriggerEvent>(OnFragTrigger);
+    }
+
+    private void OnFragInit(Entity<ProjectileGrenadeComponent> entity, ref ComponentInit args)
+    {
+        entity.Comp.Container = _container.EnsureContainer<Container>(entity.Owner, "cluster-payload");
+    }
+
+    /// <summary>
+    /// Setting the unspawned count based on capacity so we know how many new entities to spawn
+    /// </summary>
+    private void OnFragStartup(Entity<ProjectileGrenadeComponent> entity, ref ComponentStartup args)
+    {
+        if (entity.Comp.FillPrototype == null)
+            return;
+
+        entity.Comp.UnspawnedCount = Math.Max(0, entity.Comp.Capacity - entity.Comp.Container.ContainedEntities.Count);
+    }
+
+    /// <summary>
+    /// Can be triggered either by damage or the use in hand timer
+    /// </summary>
+    private void OnFragTrigger(Entity<ProjectileGrenadeComponent> entity, ref TriggerEvent args)
+    {
+        FragmentIntoProjectiles(entity.Owner, entity.Comp);
+        args.Handled = true;
+    }
+
+    /// <summary>
+    /// Spawns projectiles at the coordinates of the grenade upon triggering
+    /// Can customize the angle and velocity the projectiles come out at
+    /// </summary>
+    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);
+        }
+    }
+
+    /// <summary>
+    /// Spawns one instance of the fill prototype or contained entity at the coordinate indicated
+    /// </summary>
+    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 (file)
index 0000000..2657ba3
--- /dev/null
@@ -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<ScatteringGrenadeComponent, TriggerEvent>(OnScatteringTrigger);
+    }
+
+    /// <summary>
+    /// 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
+    /// </summary>
+    private void OnScatteringTrigger(Entity<ScatteringGrenadeComponent> entity, ref TriggerEvent args)
+    {
+        entity.Comp.IsTriggered = true;
+        args.Handled = true;
+    }
+
+    /// <summary>
+    /// 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
+    /// </summary>
+    public override void Update(float frametime)
+    {
+        base.Update(frametime);
+        var query = EntityQueryEnumerator<ScatteringGrenadeComponent>();
+
+        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<ActiveTimerTriggerComponent>(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);
+            }
+        }
+    }
+
+    /// <summary>
+    /// Spawns one instance of the fill prototype or contained entity at the coordinate indicated
+    /// </summary>
+    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;
+    }
+}
index 12838eb04d9b877eb4576368bdf6e8c5a8397890..66b1de65e8eeb178770cd2798980363d6d76df29 100644 (file)
@@ -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 (file)
index 0000000..be27c49
--- /dev/null
@@ -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;
+
+/// <summary>
+/// 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
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedScatteringGrenadeSystem))]
+public sealed partial class ScatteringGrenadeComponent : Component
+{
+    public Container Container = default!;
+
+    [DataField]
+    public EntityWhitelist? Whitelist;
+
+    /// <summary>
+    /// What we fill our prototype with if we want to pre-spawn with entities.
+    /// </summary>
+    [DataField]
+    public EntProtoId? FillPrototype;
+
+    /// <summary>
+    /// If we have a pre-fill how many more can we spawn.
+    /// </summary>
+    [AutoNetworkedField]
+    public int UnspawnedCount;
+
+    /// <summary>
+    /// Max amount of entities inside the container
+    /// </summary>
+    [DataField]
+    public int Capacity = 3;
+
+    /// <summary>
+    /// Decides if contained entities trigger after getting launched
+    /// </summary>
+    [DataField]
+    public bool TriggerContents = true;
+
+    #region Trigger time parameters for scattered entities
+    /// <summary>
+    ///  Minimum delay in seconds before any entities start to be triggered.
+    /// </summary>
+    [DataField]
+    public float DelayBeforeTriggerContents = 1.0f;
+
+    /// <summary>
+    /// Maximum delay in seconds to add between individual entity triggers
+    /// </summary>
+    [DataField]
+    public float IntervalBetweenTriggersMax;
+
+    /// <summary>
+    /// Minimum delay in seconds to add between individual entity triggers
+    /// </summary>
+    [DataField]
+    public float IntervalBetweenTriggersMin;
+    #endregion
+
+    #region Throwing parameters for the scattered entities
+    /// <summary>
+    /// Should the angle the entities get thrown at be random
+    /// instead of uniformly distributed
+    /// </summary>
+    [DataField]
+    public bool RandomAngle;
+
+    /// <summary>
+    /// The speed at which the entities get thrown
+    /// </summary>
+    [DataField]
+    public float Velocity = 5;
+
+    /// <summary>
+    /// Static distance grenades will be thrown to if RandomDistance is false.
+    /// </summary>
+    [DataField]
+    public float Distance = 1f;
+
+    /// <summary>
+    /// Should the distance the entities get thrown be random
+    /// </summary>
+    [DataField]
+    public bool RandomDistance;
+
+    /// <summary>
+    /// Max distance grenades can randomly be thrown to.
+    /// </summary>
+    [DataField]
+    public float RandomThrowDistanceMax = 2.5f;
+
+    /// <summary>
+    /// Minimal distance grenades can randomly be thrown to.
+    /// </summary>
+    [DataField]
+    public float RandomThrowDistanceMin;
+    #endregion
+
+    /// <summary>
+    /// 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
+    /// </summary>
+    public bool IsTriggered = false;
+}
diff --git a/Content.Shared/Explosion/EntitySystems/SharedScatteringGrenadeSystem.cs b/Content.Shared/Explosion/EntitySystems/SharedScatteringGrenadeSystem.cs
new file mode 100644 (file)
index 0000000..b704fbf
--- /dev/null
@@ -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<ScatteringGrenadeComponent, ComponentInit>(OnScatteringInit);
+        SubscribeLocalEvent<ScatteringGrenadeComponent, ComponentStartup>(OnScatteringStartup);
+        SubscribeLocalEvent<ScatteringGrenadeComponent, InteractUsingEvent>(OnScatteringInteractUsing);
+    }
+
+    private void OnScatteringInit(Entity<ScatteringGrenadeComponent> entity, ref ComponentInit args)
+    {
+        entity.Comp.Container = _container.EnsureContainer<Container>(entity.Owner, "cluster-payload");
+    }
+
+    /// <summary>
+    /// Setting the unspawned count based on capacity, so we know how many new entities to spawn
+    /// Update appearance based on initial fill amount
+    /// </summary>
+    private void OnScatteringStartup(Entity<ScatteringGrenadeComponent> 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);
+
+    }
+
+    /// <summary>
+    /// There are some scattergrenades you can fill up with more grenades (like clusterbangs)
+    /// This covers how you insert more into it
+    /// </summary>
+    private void OnScatteringInteractUsing(Entity<ScatteringGrenadeComponent> 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;
+    }
+
+    /// <summary>
+    /// Update appearance based off of total count of contents
+    /// </summary>
+    private void UpdateAppearance(Entity<ScatteringGrenadeComponent> entity)
+    {
+        if (!TryComp<AppearanceComponent>(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 (file)
index b041349..0000000
+++ /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
index 8f253097abc3cd73f237848e82f30d6e5f5fee1a..08d6d855a880d85d6d3dec8863db295e82927556 100644 (file)
@@ -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 (file)
index 0000000..07c2a7a
--- /dev/null
@@ -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 (file)
index 0000000..f68498b
--- /dev/null
@@ -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"
index 6112f93c16486a2ce7c658c5ac3ec30c6a13c947..5085894ce5b1a8ca6782e864a0279d47610e6f84 100644 (file)
 - type: Tag
   id: Grenade
 
+- type: Tag
+  id: GrenadeFlashBang
+
 - type: Tag
   id: HudMedical