--- /dev/null
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Explosion.Components.OnTrigger;
+using Content.Shared.Explosion.EntitySystems;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Explosion.EntitySystems;
+
+/// <summary>
+/// Releases a gas mixture to the atmosphere when triggered.
+/// Can also release gas over a set timespan to prevent trolling people
+/// with the instant-wall-of-pressure-inator.
+/// </summary>
+public sealed partial class ReleaseGasOnTriggerSystem : SharedReleaseGasOnTriggerSystem
+{
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<ReleaseGasOnTriggerComponent, TriggerEvent>(OnTrigger);
+ }
+
+ /// <summary>
+ /// Shrimply sets the component to active when triggered, allowing it to release over time.
+ /// </summary>
+ private void OnTrigger(Entity<ReleaseGasOnTriggerComponent> ent, ref TriggerEvent args)
+ {
+ ent.Comp.Active = true;
+ ent.Comp.NextReleaseTime = _timing.CurTime;
+ ent.Comp.StartingTotalMoles = ent.Comp.Air.TotalMoles;
+ UpdateAppearance(ent.Owner, true);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var curTime = _timing.CurTime;
+ var query = EntityQueryEnumerator<ReleaseGasOnTriggerComponent>();
+
+ while (query.MoveNext(out var uid, out var comp))
+ {
+ if (!comp.Active || comp.NextReleaseTime > curTime)
+ continue;
+
+ var giverGasMix = comp.Air.Remove(comp.StartingTotalMoles * comp.RemoveFraction);
+ var environment = _atmosphereSystem.GetContainingMixture(uid, false, true);
+
+ if (environment == null)
+ {
+ UpdateAppearance(uid, false);
+ RemCompDeferred<ReleaseGasOnTriggerComponent>(uid);
+ continue;
+ }
+
+ _atmosphereSystem.Merge(environment, giverGasMix);
+ comp.NextReleaseTime += comp.ReleaseInterval;
+
+ if (comp.PressureLimit != 0 && environment.Pressure >= comp.PressureLimit ||
+ comp.Air.TotalMoles <= 0)
+ {
+ UpdateAppearance(uid, false);
+ RemCompDeferred<ReleaseGasOnTriggerComponent>(uid);
+ continue;
+ }
+
+ if (comp.ExponentialRise)
+ UpdateExponentialRise(comp, comp.RemoveFraction);
+ }
+ }
+
+ /// <summary>
+ /// Updates the RemoveFraction for exponential rise.
+ /// </summary>
+ /// <remarks>See https://www.desmos.com/calculator/fx9gfrhoim</remarks>
+ private static void UpdateExponentialRise(ReleaseGasOnTriggerComponent comp, float baseFraction)
+ {
+ comp.TimesReleased++;
+ comp.RemoveFraction = 1f - MathF.Pow(1f - baseFraction, comp.TimesReleased);
+ }
+
+ private void UpdateAppearance(Entity<AppearanceComponent?> entity, bool state)
+ {
+ if (!Resolve(entity, ref entity.Comp, false))
+ return;
+
+ _appearance.SetData(entity, ReleaseGasOnTriggerComponent.ReleaseGasOnTriggerVisuals.Key, state);
+ }
+}
--- /dev/null
+using Content.Shared.Atmos;
+using Content.Shared.Explosion.EntitySystems;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.Explosion.Components.OnTrigger;
+
+/// <summary>
+/// Contains a GasMixture that will release its contents to the atmosphere when triggered.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+[AutoGenerateComponentPause]
+[Access(typeof(SharedReleaseGasOnTriggerSystem))]
+public sealed partial class ReleaseGasOnTriggerComponent : Component
+{
+ /// <summary>
+ /// Represents visual states for whatever visuals that need to be applied
+ /// on state changes.
+ /// </summary>
+ [Serializable] [NetSerializable]
+ public enum ReleaseGasOnTriggerVisuals : byte
+ {
+ Key,
+ }
+
+ /// <summary>
+ /// Whether this grenade is active and releasing gas.
+ /// Set to true when triggered, which starts gas release.
+ /// </summary>
+ [DataField]
+ public bool Active;
+
+ /// <summary>
+ /// The gas mixture that will be released to the current tile atmosphere when triggered.
+ /// </summary>
+ [DataField]
+ public GasMixture Air;
+
+ /// <summary>
+ /// If true, the gas will be released in an exponential manner.
+ /// </summary>
+ [DataField]
+ public bool ExponentialRise;
+
+ /// <summary>
+ /// Time at which the next release will occur.
+ /// This is automatically set when the grenade activates.
+ /// </summary>
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [AutoPausedField]
+ public TimeSpan NextReleaseTime = TimeSpan.Zero;
+
+ /// <summary>
+ /// The cap at which this grenade can fill the exposed atmosphere to.
+ /// The grenade automatically deletes itself when the pressure is reached.
+ /// </summary>
+ /// <example>If set to 101.325, the grenade will only fill the exposed
+ /// atmosphere up to 101.325 kPa.</example>
+ /// <remarks>If zero, this limit won't be respected.</remarks>
+ [DataField]
+ public float PressureLimit;
+
+ /// <summary>
+ /// How often the grenade will release gas.
+ /// </summary>
+ [DataField]
+ public TimeSpan ReleaseInterval = TimeSpan.FromSeconds(1);
+
+ /// <summary>
+ /// A float from 0 to 1, representing a partial portion of the moles
+ /// of the gas mixture that will be
+ /// released to the current tile atmosphere when triggered.
+ /// </summary>
+ /// <remarks>If undefined on the prototype, the entire molar amount will be transferred.</remarks>
+ [DataField]
+ public float RemoveFraction = 1;
+
+ /// <summary>
+ /// Stores the total moles initially in the grenade upon activation.
+ /// Used to calculate the moles released over time.
+ /// </summary>
+ /// <remarks>Set when the grenade is activated.</remarks>
+ [DataField(readOnly: true)]
+ public float StartingTotalMoles;
+
+ /// <summary>
+ /// Stores the number of times the grenade has been released,
+ /// for exponential rise calculations.
+ /// </summary>
+ [DataField]
+ public int TimesReleased;
+}
--- /dev/null
+namespace Content.Shared.Explosion.EntitySystems;
+
+public abstract partial class SharedReleaseGasOnTriggerSystem : EntitySystem;
+
+// I have dreams of Atmos in shared.
- id: HolofanProjector
- id: RCD
- id: RCDAmmo
+ - id: AirGrenade
- type: entity
id: LockerAtmosphericsFilled
- id: HolofanProjector
- id: RCD
- id: RCDAmmo
+ - id: AirGrenade
- type: entity
id: LockerEngineerFilledHardsuit
- type: StaticPrice
price: 350
+- type: entity
+ parent: [ BaseEngineeringContraband, GrenadeBase ] # Prevent inheriting DeleteOnTrigger from SmokeGrenade
+ id: AirGrenade
+ name: air grenade
+ description: A special solid state chemical grenade used for quickly releasing standard air into a spaced area. Fills up to 30 tiles!
+ components:
+ - type: Sprite
+ sprite: Objects/Weapons/Grenades/airboom.rsi
+ - type: SoundOnTrigger
+ sound: /Audio/Items/smoke_grenade_smoke.ogg
+ - type: TimerTriggerVisuals
+ primingSound:
+ path: /Audio/Items/smoke_grenade_prime.ogg
+ - type: OnUseTimerTrigger
+ delay: 3
+ - type: ReleaseGasOnTrigger
+ removeFraction: 0.25
+ air:
+ volume: 1000
+ moles: # Target is 3117.84 mols total for filling 30 tiles (goal is 101.325 kPa @ 20C)
+ - 654.7464 # oxygen
+ - 2463.0936 # nitrogen
+ temperature: 293.15
+ - type: StaticPrice
+ price: 350
+ - type: GenericVisualizer
+ visuals:
+ enum.ReleaseGasOnTriggerVisuals.Key:
+ enabled:
+ True: { state: active }
+ False: { state: spent }
+
# Non-explosive "dummy" grenades to use as a distraction.
- type: entity
--- /dev/null
+{
+ "version": 1,
+ "license": "CC0-1.0",
+ "copyright": "Created by EmoGarbage404 (github) for Space Station 14",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon"
+ },
+ {
+ "name": "primed"
+ },
+ {
+ "name": "active",
+ "delays": [
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ]
+ ]
+ },
+ {
+ "name": "spent"
+ },
+ {
+ "name": "equipped-BELT",
+ "directions": 4
+ },
+ {
+ "name": "inhand-left",
+ "directions": 4
+ },
+ {
+ "name": "inhand-right",
+ "directions": 4
+ }
+ ]
+}