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