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
This commit is contained in:
8
Content.Client/Explosion/ScatteringGrenadeSystem.cs
Normal file
8
Content.Client/Explosion/ScatteringGrenadeSystem.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Content.Shared.Explosion.EntitySystems;
|
||||
|
||||
namespace Content.Client.Explosion;
|
||||
|
||||
public sealed class ScatteringGrenadeSystem : SharedScatteringGrenadeSystem
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -81,6 +81,9 @@
|
||||
guides:
|
||||
- Security
|
||||
- Antagonists
|
||||
- type: Tag
|
||||
tags:
|
||||
- GrenadeFlashBang
|
||||
|
||||
- type: entity
|
||||
id: GrenadeFlashEffect
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -611,6 +611,9 @@
|
||||
- type: Tag
|
||||
id: Grenade
|
||||
|
||||
- type: Tag
|
||||
id: GrenadeFlashBang
|
||||
|
||||
- type: Tag
|
||||
id: HudMedical
|
||||
|
||||
|
||||
Reference in New Issue
Block a user