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)
|
if (!shooter.HasValue)
|
||||||
return;
|
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))
|
if (!_container.IsEntityOrParentInContainer(shooter.Value))
|
||||||
args.Cancelled = true;
|
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:
|
guides:
|
||||||
- Security
|
- Security
|
||||||
- Antagonists
|
- Antagonists
|
||||||
|
- type: Tag
|
||||||
|
tags:
|
||||||
|
- GrenadeFlashBang
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: GrenadeFlashEffect
|
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
|
- type: Tag
|
||||||
id: Grenade
|
id: Grenade
|
||||||
|
|
||||||
|
- type: Tag
|
||||||
|
id: GrenadeFlashBang
|
||||||
|
|
||||||
- type: Tag
|
- type: Tag
|
||||||
id: HudMedical
|
id: HudMedical
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user