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