diff --git a/Content.Server/PowerSink/PowerSinkComponent.cs b/Content.Server/PowerSink/PowerSinkComponent.cs
index 4654205a38..1b55739c8c 100644
--- a/Content.Server/PowerSink/PowerSinkComponent.cs
+++ b/Content.Server/PowerSink/PowerSinkComponent.cs
@@ -1,8 +1,44 @@
-namespace Content.Server.PowerSink
+using Robust.Shared.Audio;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Server.PowerSink
{
///
/// Absorbs power up to its capacity when anchored then explodes.
///
[RegisterComponent]
- public sealed class PowerSinkComponent : Component {}
+ public sealed class PowerSinkComponent : Component
+ {
+ ///
+ /// When the power sink is nearing its explosion, warn the crew so they can look for it
+ /// (if they're not already).
+ ///
+ [DataField("sentImminentExplosionWarning")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public bool SentImminentExplosionWarningMessage = false;
+
+ ///
+ /// If explosion has been triggered, time at which to explode.
+ ///
+ [DataField("explosionTime", customTypeSerializer:typeof(TimeOffsetSerializer))]
+ public System.TimeSpan? ExplosionTime = null;
+
+ ///
+ /// The highest sound warning threshold that has been hit (plays sfx occasionally as explosion nears)
+ ///
+ [DataField("highestWarningSoundThreshold")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float HighestWarningSoundThreshold = 0f;
+
+ [DataField("chargeFireSound")]
+ public SoundSpecifier ChargeFireSound = new SoundPathSpecifier("/Audio/Effects/PowerSink/charge_fire.ogg");
+
+ [DataField("electricSound")] public SoundSpecifier ElectricSound =
+ new SoundPathSpecifier("/Audio/Effects/PowerSink/electric.ogg")
+ {
+ Params = AudioParams.Default
+ .WithVolume(15f) // audible even behind walls
+ .WithRolloffFactor(10)
+ };
+ }
}
diff --git a/Content.Server/PowerSink/PowerSinkSystem.cs b/Content.Server/PowerSink/PowerSinkSystem.cs
index d67cda1d47..e5a118b07e 100644
--- a/Content.Server/PowerSink/PowerSinkSystem.cs
+++ b/Content.Server/PowerSink/PowerSinkSystem.cs
@@ -2,18 +2,41 @@
using Content.Server.Power.Components;
using Content.Shared.Examine;
using Robust.Shared.Utility;
+using Content.Server.Chat.Systems;
+using Content.Server.Station.Systems;
+using Robust.Shared.Timing;
+using Robust.Shared.Audio;
namespace Content.Server.PowerSink
{
public sealed class PowerSinkSystem : EntitySystem
{
+ ///
+ /// Percentage of battery full to trigger the announcement warning at.
+ ///
+ private const float WarningMessageThreshold = 0.70f;
+
+ private readonly float[] _warningSoundThresholds = new[] { .80f, .90f, .95f, .98f };
+
+ ///
+ /// Length of time to delay explosion from battery full state -- this is used to play
+ /// a brief SFX winding up the explosion.
+ ///
+ ///
+ private readonly TimeSpan _explosionDelayTime = TimeSpan.FromSeconds(1.465);
+
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly ExplosionSystem _explosionSystem = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly StationSystem _station = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnExamine);
+ SubscribeLocalEvent(OnUnpaused);
}
private void OnExamine(EntityUid uid, PowerSinkComponent component, ExaminedEvent args)
@@ -30,26 +53,91 @@ namespace Content.Server.PowerSink
);
}
+ private void OnUnpaused(EntityUid uid, PowerSinkComponent component, ref EntityUnpausedEvent args)
+ {
+ if (component.ExplosionTime == null)
+ return;
+
+ component.ExplosionTime = component.ExplosionTime + args.PausedTime;
+ }
+
public override void Update(float frameTime)
{
- var toRemove = new RemQueue<(PowerSinkComponent Sink, BatteryComponent Battery)>();
+ var toRemove = new RemQueue<(EntityUid Entity, PowerSinkComponent Sink)>();
+ var query = EntityQueryEnumerator();
// Realistically it's gonna be like <5 per station.
- foreach (var (comp, networkLoad, battery, xform) in EntityManager.EntityQuery())
+ while (query.MoveNext(out var entity, out var component, out var networkLoad, out var battery, out var transform))
{
- if (!xform.Anchored) continue;
+ if (!transform.Anchored)
+ continue;
battery.CurrentCharge += networkLoad.NetworkLoad.ReceivingPower / 1000;
- if (battery.CurrentCharge < battery.MaxCharge) continue;
- toRemove.Add((comp, battery));
+ var currentBatteryThreshold = battery.CurrentCharge / battery.MaxCharge;
+
+ // Check for warning message threshold
+ if (!component.SentImminentExplosionWarningMessage &&
+ currentBatteryThreshold >= WarningMessageThreshold)
+ {
+ NotifyStationOfImminentExplosion(entity, component);
+ }
+
+ // Check for warning sound threshold
+ foreach (var testThreshold in _warningSoundThresholds)
+ {
+ if (currentBatteryThreshold >= testThreshold &&
+ testThreshold > component.HighestWarningSoundThreshold)
+ {
+ component.HighestWarningSoundThreshold = currentBatteryThreshold; // Don't re-play in future until next threshold hit
+ _audio.PlayPvs(component.ElectricSound, entity); // Play SFX
+ break;
+ }
+ }
+
+ // Check for explosion
+ if (battery.CurrentCharge < battery.MaxCharge)
+ continue;
+
+ if (component.ExplosionTime == null)
+ {
+ // Set explosion sequence to start soon
+ component.ExplosionTime = _gameTiming.CurTime.Add(_explosionDelayTime);
+
+ // Wind-up SFX
+ _audio.PlayPvs(component.ChargeFireSound, entity); // Play SFX
+ }
+ else if (_gameTiming.CurTime >= component.ExplosionTime)
+ {
+ // Explode!
+ toRemove.Add((entity, component));
+ }
}
- foreach (var (comp, battery) in toRemove)
+ foreach (var (entity, component) in toRemove)
{
- _explosionSystem.QueueExplosion(comp.Owner, "Default", 5 * (battery.MaxCharge / 2500000), 0.5f, 10, canCreateVacuum: false);
- EntityManager.RemoveComponent(comp.Owner, comp);
+ _explosionSystem.QueueExplosion(entity, "PowerSink", 2000f, 4f, 20f, canCreateVacuum: true);
+ EntityManager.RemoveComponent(entity, component);
}
}
+
+ private void NotifyStationOfImminentExplosion(EntityUid uid, PowerSinkComponent powerSinkComponent)
+ {
+ if (powerSinkComponent.SentImminentExplosionWarningMessage)
+ return;
+
+ powerSinkComponent.SentImminentExplosionWarningMessage = true;
+ var station = _station.GetOwningStation(uid);
+
+ if (station == null)
+ return;
+
+ _chat.DispatchStationAnnouncement(
+ station.Value,
+ Loc.GetString("powersink-immiment-explosion-announcement"),
+ playDefaultSound: true,
+ colorOverride: Color.Yellow
+ );
+ }
}
}
diff --git a/Resources/Audio/Effects/PowerSink/charge_fire.ogg b/Resources/Audio/Effects/PowerSink/charge_fire.ogg
new file mode 100644
index 0000000000..c16b08bd9c
Binary files /dev/null and b/Resources/Audio/Effects/PowerSink/charge_fire.ogg differ
diff --git a/Resources/Audio/Effects/PowerSink/electric.ogg b/Resources/Audio/Effects/PowerSink/electric.ogg
new file mode 100644
index 0000000000..f6360d6f8e
Binary files /dev/null and b/Resources/Audio/Effects/PowerSink/electric.ogg differ
diff --git a/Resources/Audio/Effects/PowerSink/licenses.txt b/Resources/Audio/Effects/PowerSink/licenses.txt
new file mode 100644
index 0000000000..52898e4710
--- /dev/null
+++ b/Resources/Audio/Effects/PowerSink/licenses.txt
@@ -0,0 +1,10 @@
+- files: ["electric.ogg"]
+ license: "CC0 1.0"
+ copyright: "The-Sacha-Rush"
+ source: "https://freesound.org/people/The-Sacha-Rush/sounds/657802/"
+
+- files: ["charge_fire.ogg"]
+ license: "CC BY 3.0"
+ copyright: "Teh_Bucket, dylanperitz, satanicupsman, CaptainGusterd, arightwizard, BigKahuna360, michael_grinnell, weaveofkev, MichelleGrobler, Alex_John73, sandyrb, breo2012"
+ source: "https://freesound.org/people/Teh_Bucket/sounds/518739/"
+
diff --git a/Resources/Locale/en-US/powersink/powersink.ftl b/Resources/Locale/en-US/powersink/powersink.ftl
index 2d5b73be19..92e87dd90f 100644
--- a/Resources/Locale/en-US/powersink/powersink.ftl
+++ b/Resources/Locale/en-US/powersink/powersink.ftl
@@ -1 +1,2 @@
powersink-examine-drain-amount = The power sink is draining [color={$markupDrainColor}]{$amount} kW[/color].
+powersink-immiment-explosion-announcement = System scans have detected a rogue power consuming device is becoming unstable. Staff are advised to locate and disconnect this device immediately before the station is damaged.
diff --git a/Resources/Prototypes/explosion.yml b/Resources/Prototypes/explosion.yml
index 7a196ae31e..de55aaa3f4 100644
--- a/Resources/Prototypes/explosion.yml
+++ b/Resources/Prototypes/explosion.yml
@@ -85,3 +85,19 @@
lightColor: Orange
texturePath: /Textures/Effects/fire.rsi
fireStates: 3
+
+- type: explosion
+ id: PowerSink
+ damagePerIntensity:
+ types:
+ Heat: 12
+ Blunt: 12
+ Piercing: 12
+ Structural: 40
+ tileBreakChance: [ 0, 0.5, 1 ]
+ tileBreakIntensity: [ 1, 5, 10 ]
+ tileBreakRerollReduction: 3
+ intensityPerState: 20
+ lightColor: Orange
+ texturePath: /Textures/Effects/fire.rsi
+ fireStates: 6