diff --git a/Content.Server/Projectiles/Components/ProjectileComponent.cs b/Content.Server/Projectiles/Components/ProjectileComponent.cs
index 990a2da08a..d307be7458 100644
--- a/Content.Server/Projectiles/Components/ProjectileComponent.cs
+++ b/Content.Server/Projectiles/Components/ProjectileComponent.cs
@@ -26,7 +26,7 @@ namespace Content.Server.Projectiles.Components
public bool DeleteOnCollide { get; } = true;
// Get that juicy FPS hit sound
- [DataField("soundHit", required: true)] public SoundSpecifier SoundHit = default!;
+ [DataField("soundHit", required: true)] public SoundSpecifier? SoundHit = default!;
[DataField("soundHitSpecies")] public SoundSpecifier? SoundHitSpecies = null;
public bool DamagedEntity;
diff --git a/Content.Server/Projectiles/ProjectileSystem.cs b/Content.Server/Projectiles/ProjectileSystem.cs
index 7837a728d4..4894e81878 100644
--- a/Content.Server/Projectiles/ProjectileSystem.cs
+++ b/Content.Server/Projectiles/ProjectileSystem.cs
@@ -39,7 +39,10 @@ namespace Content.Server.Projectiles
}
else
{
- SoundSystem.Play(playerFilter, component.SoundHit.GetSound(), coordinates);
+ var soundHit = component.SoundHit?.GetSound();
+
+ if (!string.IsNullOrEmpty(soundHit))
+ SoundSystem.Play(playerFilter, soundHit, coordinates);
}
if (!otherEntity.Deleted && otherEntity.TryGetComponent(out IDamageableComponent? damage))
diff --git a/Content.Server/StationEvents/Events/MeteorSwarm.cs b/Content.Server/StationEvents/Events/MeteorSwarm.cs
new file mode 100644
index 0000000000..99656db046
--- /dev/null
+++ b/Content.Server/StationEvents/Events/MeteorSwarm.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Linq;
+using Content.Server.GameTicking;
+using Content.Server.Projectiles.Components;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Server.StationEvents.Events
+{
+ public sealed class MeteorSwarm : StationEvent
+ {
+ [Dependency] private readonly IComponentManager _compManager = default!;
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly IRobustRandom _robustRandom = default!;
+
+ public override string Name => "MeteorSwarm";
+
+ public override int EarliestStart => 30;
+ public override float Weight => WeightLow;
+ public override int? MaxOccurrences => 2;
+ public override int MinimumPlayers => 20;
+
+ public override string StartAnnouncement => "Meteors are on a collision course with the station. Brace for impact.";
+ protected override string EndAnnouncement => "The meteor swarm has passed. Please return to your stations.";
+
+ public override string? StartAudio => "/Audio/Announcements/bloblarm.ogg";
+
+ protected override float StartAfter => 30f;
+ protected override float EndAfter => float.MaxValue;
+
+ private float _cooldown;
+
+ ///
+ /// We'll send a specific amount of waves of meteors towards the station per ending rather than using a timer.
+ ///
+ private int _waveCounter;
+
+ private const int MinimumWaves = 3;
+ private const int MaximumWaves = 8;
+
+ private const float MinimumCooldown = 10f;
+ private const float MaximumCooldown = 60f;
+
+ private const int MeteorsPerWave = 5;
+ private const float MeteorVelocity = 10f;
+ private const float MaxAngularVelocity = 0.25f;
+ private const float MinAngularVelocity = -0.25f;
+
+ public override void Startup()
+ {
+ base.Startup();
+ var robustRandom = IoCManager.Resolve();
+ _waveCounter = robustRandom.Next(MinimumWaves, MaximumWaves);
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ _waveCounter = 0;
+ _cooldown = 0f;
+ EndAfter = float.MaxValue;
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ if (!Started) return;
+
+ if (_waveCounter <= 0)
+ {
+ EndAfter = float.MinValue;
+ return;
+ }
+ _cooldown -= frameTime;
+
+ if (_cooldown > 0f) return;
+
+ _waveCounter--;
+
+ _cooldown += (MaximumCooldown - MinimumCooldown) * _robustRandom.NextFloat() + MinimumCooldown;
+
+ Box2? playableArea = null;
+ var mapId = EntitySystem.Get().DefaultMap;
+
+ foreach (var grid in _mapManager.GetAllGrids())
+ {
+ if (grid.ParentMapId != mapId || !_compManager.TryGetComponent(grid.GridEntityId, out PhysicsComponent? gridBody)) continue;
+ var aabb = gridBody.GetWorldAABB();
+ playableArea = playableArea?.Union(aabb) ?? aabb;
+ }
+
+ if (playableArea == null)
+ {
+ EndAfter = float.MinValue;
+ return;
+ }
+
+ var minimumDistance = (playableArea.Value.TopRight - playableArea.Value.Center).Length + 50f;
+ var maximumDistance = minimumDistance + 100f;
+
+ var center = playableArea.Value.Center;
+
+ for (var i = 0; i < MeteorsPerWave; i++)
+ {
+ var angle = new Angle(_robustRandom.NextFloat() * MathF.Tau);
+ var offset = angle.RotateVec(new Vector2((maximumDistance - minimumDistance) * _robustRandom.NextFloat() + minimumDistance, 0));
+ var spawnPosition = new MapCoordinates(center + offset, mapId);
+ var meteor = _entityManager.SpawnEntity("MeteorLarge", spawnPosition);
+ var physics = _compManager.GetComponent(meteor.Uid);
+ physics.BodyStatus = BodyStatus.InAir;
+ physics.ApplyLinearImpulse(-offset.Normalized * MeteorVelocity * physics.Mass);
+ physics.ApplyAngularImpulse(
+ // Get a random angular velocity.
+ physics.Mass * ((MaxAngularVelocity - MinAngularVelocity) * _robustRandom.NextFloat() +
+ MinAngularVelocity));
+ // TODO: God this disgusts me but projectile needs a refactor.
+ meteor.GetComponent().TimeLeft = 120f;
+ }
+ }
+ }
+}
diff --git a/Content.Server/StationEvents/StationEventSystem.cs b/Content.Server/StationEvents/StationEventSystem.cs
index 736590a5c3..aeab52e2c7 100644
--- a/Content.Server/StationEvents/StationEventSystem.cs
+++ b/Content.Server/StationEvents/StationEventSystem.cs
@@ -175,6 +175,7 @@ namespace Content.Server.StationEvents
if (type.IsAbstract) continue;
var stationEvent = (StationEvent) typeFactory.CreateInstance(type);
+ IoCManager.InjectDependencies(stationEvent);
_stationEvents.Add(stationEvent);
}
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/meteors.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/meteors.yml
new file mode 100644
index 0000000000..58e99697d8
--- /dev/null
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/meteors.yml
@@ -0,0 +1,39 @@
+- type: entity
+ id: MeteorLarge
+ name: meteor
+ abstract: true
+ components:
+ - type: Sprite
+ noRot: false
+ netsync: false
+ sprite: Objects/Weapons/Guns/Projectiles/meteor.rsi
+ scale: 4,4
+ layers:
+ - state: large
+ shader: unshaded
+ - type: ExplodeOnTrigger
+ - type: DeleteOnTrigger
+ - type: TriggerOnCollide
+ - type: Projectile
+ deleteOnCollide: false
+ - type: Explosive
+ devastationRange: 3
+ heavyImpactRange: 5
+ lightImpactRange: 7
+ flashRange: 10
+ - type: Physics
+ bodyType: Dynamic
+ fixedRotation: false
+ fixtures:
+ - shape:
+ !type:PhysShapeCircle
+ radius: 0.5
+ mass: 200
+ hard: true
+ # Didn't use MapGrid for now as the bounds are stuffed.
+ layer:
+ - Impassable
+ - SmallImpassable
+ - VaultImpassable
+ mask:
+ - Impassable
diff --git a/Resources/Textures/Objects/Weapons/Guns/Projectiles/meteor.rsi/large.png b/Resources/Textures/Objects/Weapons/Guns/Projectiles/meteor.rsi/large.png
new file mode 100644
index 0000000000..bb27b75967
Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Projectiles/meteor.rsi/large.png differ
diff --git a/Resources/Textures/Objects/Weapons/Guns/Projectiles/meteor.rsi/meta.json b/Resources/Textures/Objects/Weapons/Guns/Projectiles/meteor.rsi/meta.json
new file mode 100644
index 0000000000..86a5b835c6
--- /dev/null
+++ b/Resources/Textures/Objects/Weapons/Guns/Projectiles/meteor.rsi/meta.json
@@ -0,0 +1,15 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Taken from cev-eris at https://github.com/discordia-space/CEV-Eris/raw/2acc4d34a894dbcc9dbf3779b696ddf296aa2c56/icons/obj/projectiles.dmi",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "large"
+ }
+ ]
+ }
+
\ No newline at end of file