Station events (#1518)
* Station event system Adds 2 basic events: (Power) GridCheck and RadiationStorm (based on the goonstation version). The system itself to choose events is based on tgstation's implementation. This also adds the event command that can be run to force specific events. There's still some other TODO items for these to be complete, to my knowledge: 1. There's no worldspace DrawCircle method (though the radstorm could look a lot nicer with a shader). 2. The PlayGlobal power_off / power_on audio seems to cut out halfway-through 3. (I think this is a known issue) lights still emit light until you get closer in a gridcheck so PVS range might need bumping. * Invariants for event names * Fix random event shutdown * Mix stereo announcements to mono * Address feedback * Remove redundant client system and use the overlay component instead * Drop the server prefix * Fix radiation overlay enum * use entityquery instead * zum's feedback * Use EntityQuery Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
This commit is contained in:
89
Content.Server/StationEvents/PowerGridCheck.cs
Normal file
89
Content.Server/StationEvents/PowerGridCheck.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Power;
|
||||
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
|
||||
using Content.Server.GameObjects.Components.Power.PowerNetComponents;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Content.Server.StationEvents
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class PowerGridCheck : StationEvent
|
||||
{
|
||||
public override string Name => "PowerGridCheck";
|
||||
|
||||
public override StationEventWeight Weight => StationEventWeight.Normal;
|
||||
|
||||
public override int? MaxOccurrences => 3;
|
||||
|
||||
protected override string StartAnnouncement => Loc.GetString(
|
||||
"Abnormal activity detected in the station's powernet. As a precautionary measure, the station's power will be shut off for an indeterminate duration.");
|
||||
|
||||
protected override string EndAnnouncement => Loc.GetString(
|
||||
"Power has been restored to the station. We apologize for the inconvenience.");
|
||||
|
||||
private float _elapsedTime;
|
||||
private int _failDuration;
|
||||
|
||||
private Dictionary<IEntity, bool> _powered = new Dictionary<IEntity, bool>();
|
||||
|
||||
private readonly List<PowerReceiverComponent> _toPowerDown = new List<PowerReceiverComponent>();
|
||||
|
||||
public override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
EntitySystem.Get<AudioSystem>().PlayGlobal("/Audio/Announcements/power_off.ogg");
|
||||
|
||||
_elapsedTime = 0.0f;
|
||||
_failDuration = IoCManager.Resolve<IRobustRandom>().Next(30, 120);
|
||||
var componentManager = IoCManager.Resolve<IComponentManager>();
|
||||
|
||||
foreach (var component in componentManager.EntityQuery<PowerReceiverComponent>())
|
||||
{
|
||||
component.PowerDisabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
EntitySystem.Get<AudioSystem>().PlayGlobal("/Audio/Announcements/power_on.ogg");
|
||||
|
||||
foreach (var (entity, powered) in _powered)
|
||||
{
|
||||
if (entity.Deleted) continue;
|
||||
|
||||
if (entity.TryGetComponent(out PowerReceiverComponent powerReceiverComponent))
|
||||
{
|
||||
powerReceiverComponent.PowerDisabled = powered;
|
||||
}
|
||||
}
|
||||
|
||||
_powered.Clear();
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
if (!Running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_elapsedTime += frameTime;
|
||||
|
||||
if (_elapsedTime < _failDuration)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
131
Content.Server/StationEvents/RadiationStorm.cs
Normal file
131
Content.Server/StationEvents/RadiationStorm.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Map;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.StationEvents
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class RadiationStorm : StationEvent
|
||||
{
|
||||
// Based on Goonstation style radiation storm with some TG elements (announcer, etc.)
|
||||
|
||||
[Dependency] private IEntityManager _entityManager = default!;
|
||||
[Dependency] private IMapManager _mapManager = default!;
|
||||
[Dependency] private IRobustRandom _robustRandom = default!;
|
||||
|
||||
public override string Name => "RadiationStorm";
|
||||
|
||||
protected override string StartAnnouncement => Loc.GetString(
|
||||
"High levels of radiation detected near the station. Evacuate any areas containing abnormal green energy fields.");
|
||||
|
||||
protected override string EndAnnouncement => Loc.GetString(
|
||||
"The radiation threat has passed. Please return to your workplaces.");
|
||||
|
||||
/// <summary>
|
||||
/// How long until the radiation storm starts
|
||||
/// </summary>
|
||||
private const float StartupTime = 10;
|
||||
|
||||
/// <summary>
|
||||
/// How long the radiation storm has been running for
|
||||
/// </summary>
|
||||
private float _timeElapsed;
|
||||
|
||||
private int _pulsesRemaining;
|
||||
private float _timeUntilPulse;
|
||||
private const float MinPulseDelay = 0.2f;
|
||||
private const float MaxPulseDelay = 0.8f;
|
||||
|
||||
public override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
EntitySystem.Get<AudioSystem>().PlayGlobal("/Audio/Announcements/radiation.ogg");
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_timeElapsed = 0.0f;
|
||||
_pulsesRemaining = _robustRandom.Next(30, 100);
|
||||
|
||||
var componentManager = IoCManager.Resolve<IComponentManager>();
|
||||
|
||||
foreach (var overlay in componentManager.EntityQuery<ServerOverlayEffectsComponent>())
|
||||
{
|
||||
overlay.AddOverlay(SharedOverlayID.RadiationPulseOverlay);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
|
||||
// IOC uninject?
|
||||
_entityManager = null;
|
||||
_mapManager = null;
|
||||
_robustRandom = null;
|
||||
|
||||
var componentManager = IoCManager.Resolve<IComponentManager>();
|
||||
|
||||
foreach (var overlay in componentManager.EntityQuery<ServerOverlayEffectsComponent>())
|
||||
{
|
||||
overlay.RemoveOverlay(SharedOverlayID.RadiationPulseOverlay);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
_timeElapsed += frameTime;
|
||||
|
||||
if (_pulsesRemaining == 0)
|
||||
{
|
||||
Running = false;
|
||||
}
|
||||
|
||||
if (!Running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_timeElapsed < StartupTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_timeUntilPulse -= frameTime;
|
||||
|
||||
if (_timeUntilPulse <= 0.0f)
|
||||
{
|
||||
// TODO: Probably rate-limit this for small grids (e.g. no more than 25% covered)
|
||||
foreach (var grid in _mapManager.GetAllGrids())
|
||||
{
|
||||
if (grid.IsDefaultGrid) continue;
|
||||
SpawnPulse(grid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnPulse(IMapGrid mapGrid)
|
||||
{
|
||||
_entityManager.SpawnEntity("RadiationPulse", FindRandomGrid(mapGrid));
|
||||
_timeUntilPulse = _robustRandom.NextFloat() * (MaxPulseDelay - MinPulseDelay) + MinPulseDelay;
|
||||
_pulsesRemaining -= 1;
|
||||
}
|
||||
|
||||
private GridCoordinates FindRandomGrid(IMapGrid mapGrid)
|
||||
{
|
||||
// TODO: Need to get valid tiles? (maybe just move right if the tile we chose is invalid?)
|
||||
|
||||
var randomX = _robustRandom.Next((int) mapGrid.WorldBounds.Left, (int) mapGrid.WorldBounds.Right);
|
||||
var randomY = _robustRandom.Next((int) mapGrid.WorldBounds.Bottom, (int) mapGrid.WorldBounds.Top);
|
||||
|
||||
return mapGrid.GridTileToLocal(new MapIndices(randomX, randomY));
|
||||
}
|
||||
}
|
||||
}
|
||||
94
Content.Server/StationEvents/StationEvent.cs
Normal file
94
Content.Server/StationEvents/StationEvent.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using Content.Server.Interfaces.Chat;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Server.StationEvents
|
||||
{
|
||||
public abstract class StationEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// If the event has started and is currently running
|
||||
/// </summary>
|
||||
public bool Running { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable name for the event
|
||||
/// </summary>
|
||||
public abstract string Name { get; }
|
||||
|
||||
public virtual StationEventWeight Weight { get; } = StationEventWeight.Normal;
|
||||
|
||||
/// <summary>
|
||||
/// What should be said in chat when the event starts (if anything).
|
||||
/// </summary>
|
||||
protected virtual string StartAnnouncement { get; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// What should be said in chat when the event end (if anything).
|
||||
/// </summary>
|
||||
protected virtual string EndAnnouncement { get; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// In minutes, when is the first time this event can start
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public virtual int EarliestStart { get; } = 20;
|
||||
|
||||
/// <summary>
|
||||
/// How many players need to be present on station for the event to run
|
||||
/// </summary>
|
||||
/// To avoid running deadly events with low-pop
|
||||
public virtual int MinimumPlayers { get; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// How many times this event has run this round
|
||||
/// </summary>
|
||||
public int Occurrences { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// How many times this even can occur in a single round
|
||||
/// </summary>
|
||||
public virtual int? MaxOccurrences { get; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Called once when the station event starts
|
||||
/// </summary>
|
||||
public virtual void Startup()
|
||||
{
|
||||
Running = true;
|
||||
Occurrences += 1;
|
||||
if (StartAnnouncement != null)
|
||||
{
|
||||
var chatManager = IoCManager.Resolve<IChatManager>();
|
||||
chatManager.DispatchStationAnnouncement(StartAnnouncement);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called every tick when this event is active
|
||||
/// </summary>
|
||||
/// <param name="frameTime"></param>
|
||||
public abstract void Update(float frameTime);
|
||||
|
||||
/// <summary>
|
||||
/// Called once when the station event ends
|
||||
/// </summary>
|
||||
public virtual void Shutdown()
|
||||
{
|
||||
if (EndAnnouncement != null)
|
||||
{
|
||||
var chatManager = IoCManager.Resolve<IChatManager>();
|
||||
chatManager.DispatchStationAnnouncement(EndAnnouncement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum StationEventWeight
|
||||
{
|
||||
VeryLow = 0,
|
||||
Low = 5,
|
||||
Normal = 10,
|
||||
High = 15,
|
||||
VeryHigh = 20,
|
||||
}
|
||||
}
|
||||
102
Content.Server/StationEvents/StationEventCommand.cs
Normal file
102
Content.Server/StationEvents/StationEventCommand.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
#nullable enable
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.GameObjects.EntitySystems.StationEvents;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.Interfaces.Console;
|
||||
using Robust.Server.Interfaces.Player;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Content.Client.Commands
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class StationEventCommand : IClientCommand
|
||||
{
|
||||
public string Command => "events";
|
||||
public string Description => "Provides admin control to station events";
|
||||
public string Help => "events <list/pause/resume/random/stop/run <eventname>>\n" +
|
||||
"list: return all event names that can be run\n " +
|
||||
"pause: stop all random events from running\n" +
|
||||
"resume: allow random events to run again\n" +
|
||||
"random: choose a random event that is valid and run it\n" +
|
||||
"run: start a particular event now; <eventname> is case-insensitive and not localized";
|
||||
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
shell.SendText(player, "Need more args");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args[0] == "list")
|
||||
{
|
||||
var resultText = "Random\n" + EntitySystem.Get<StationEventSystem>().GetEventNames();
|
||||
shell.SendText(player, resultText);
|
||||
return;
|
||||
}
|
||||
|
||||
// Didn't use a "toggle" so it's explicit
|
||||
if (args[0] == "pause")
|
||||
{
|
||||
var stationEventSystem = EntitySystem.Get<StationEventSystem>();
|
||||
|
||||
if (!stationEventSystem.Enabled)
|
||||
{
|
||||
shell.SendText(player, Loc.GetString("Station events are already paused"));
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
stationEventSystem.Enabled = false;
|
||||
shell.SendText(player, Loc.GetString("Station events paused"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (args[0] == "resume")
|
||||
{
|
||||
var stationEventSystem = EntitySystem.Get<StationEventSystem>();
|
||||
|
||||
if (stationEventSystem.Enabled)
|
||||
{
|
||||
shell.SendText(player, Loc.GetString("Station events are already running"));
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
stationEventSystem.Enabled = true;
|
||||
shell.SendText(player, Loc.GetString("Station events resumed"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (args[0] == "stop")
|
||||
{
|
||||
var resultText = EntitySystem.Get<StationEventSystem>().StopEvent();
|
||||
shell.SendText(player, resultText);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args[0] == "run" && args.Length == 2)
|
||||
{
|
||||
var eventName = args[1];
|
||||
string resultText;
|
||||
|
||||
if (eventName == "random")
|
||||
{
|
||||
resultText = EntitySystem.Get<StationEventSystem>().RunRandomEvent();
|
||||
}
|
||||
else
|
||||
{
|
||||
resultText = EntitySystem.Get<StationEventSystem>().RunEvent(eventName);
|
||||
}
|
||||
|
||||
shell.SendText(player, resultText);
|
||||
return;
|
||||
}
|
||||
|
||||
shell.SendText(player, Loc.GetString("Invalid events command"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user