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:
@@ -0,0 +1,24 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using Content.Shared.GameObjects.Components;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
|
||||||
|
namespace Content.Client.GameObjects.Components.StationEvents
|
||||||
|
{
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed class RadiationPulseComponent : SharedRadiationPulseComponent
|
||||||
|
{
|
||||||
|
public TimeSpan EndTime { get; private set; }
|
||||||
|
|
||||||
|
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||||
|
{
|
||||||
|
base.HandleComponentState(curState, nextState);
|
||||||
|
if (!(curState is RadiationPulseMessage state))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EndTime = state.EndTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
152
Content.Client/StationEvents/RadiationPulseOverlay.cs
Normal file
152
Content.Client/StationEvents/RadiationPulseOverlay.cs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Content.Client.GameObjects.Components.StationEvents;
|
||||||
|
using Content.Shared.GameObjects.Components.Mobs;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Client.Graphics.Drawing;
|
||||||
|
using Robust.Client.Graphics.Overlays;
|
||||||
|
using Robust.Client.Interfaces.Graphics.ClientEye;
|
||||||
|
using Robust.Client.Player;
|
||||||
|
using Robust.Shared.Interfaces.GameObjects;
|
||||||
|
using Robust.Shared.Interfaces.Map;
|
||||||
|
using Robust.Shared.Interfaces.Timing;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Color = Robust.Shared.Maths.Color;
|
||||||
|
|
||||||
|
namespace Content.Client.StationEvents
|
||||||
|
{
|
||||||
|
[UsedImplicitly]
|
||||||
|
public sealed class RadiationPulseOverlay : Overlay
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IComponentManager _componentManager = default!;
|
||||||
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||||
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||||
|
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current color of a pulse
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<IEntity, Color> _colors = new Dictionary<IEntity, Color>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether our alpha is increasing or decreasing and at what time does it flip (or stop)
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<IEntity, (bool EasingIn, TimeSpan TransitionTime)> _transitions =
|
||||||
|
new Dictionary<IEntity, (bool EasingIn, TimeSpan TransitionTime)>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How much the alpha changes per second for each pulse
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<IEntity, float> _alphaRateOfChange = new Dictionary<IEntity, float>();
|
||||||
|
|
||||||
|
private TimeSpan _lastTick;
|
||||||
|
|
||||||
|
// TODO: When worldHandle can do DrawCircle change this.
|
||||||
|
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
|
||||||
|
|
||||||
|
public RadiationPulseOverlay() : base(nameof(SharedOverlayID.RadiationPulseOverlay))
|
||||||
|
{
|
||||||
|
IoCManager.InjectDependencies(this);
|
||||||
|
_lastTick = _gameTiming.CurTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the current color for the entity,
|
||||||
|
/// accounting for what its alpha should be and whether it should be transitioning in or out
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity"></param>
|
||||||
|
/// <param name="elapsedTime">frametime</param>
|
||||||
|
/// <param name="endTime"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private Color GetColor(IEntity entity, float elapsedTime, TimeSpan endTime)
|
||||||
|
{
|
||||||
|
var currentTime = _gameTiming.CurTime;
|
||||||
|
|
||||||
|
// New pulse
|
||||||
|
if (!_colors.ContainsKey(entity))
|
||||||
|
{
|
||||||
|
UpdateTransition(entity, currentTime, endTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentColor = _colors[entity];
|
||||||
|
var alphaChange = _alphaRateOfChange[entity] * elapsedTime;
|
||||||
|
|
||||||
|
if (!_transitions[entity].EasingIn)
|
||||||
|
{
|
||||||
|
alphaChange *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTime > _transitions[entity].TransitionTime)
|
||||||
|
{
|
||||||
|
UpdateTransition(entity, currentTime, endTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
_colors[entity] = _colors[entity].WithAlpha(currentColor.A + alphaChange);
|
||||||
|
return _colors[entity];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTransition(IEntity entity, TimeSpan currentTime, TimeSpan endTime)
|
||||||
|
{
|
||||||
|
bool easingIn;
|
||||||
|
TimeSpan transitionTime;
|
||||||
|
|
||||||
|
if (!_transitions.TryGetValue(entity, out var transition))
|
||||||
|
{
|
||||||
|
// Start as false because it will immediately be flipped
|
||||||
|
easingIn = false;
|
||||||
|
transitionTime = (endTime - currentTime) / 2 + currentTime;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
easingIn = transition.EasingIn;
|
||||||
|
transitionTime = endTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
_transitions[entity] = (!easingIn, transitionTime);
|
||||||
|
_colors[entity] = Color.Green.WithAlpha(0.0f);
|
||||||
|
_alphaRateOfChange[entity] = 1.0f / (float) (transitionTime - currentTime).TotalSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
|
||||||
|
{
|
||||||
|
// PVS should control the overlay pretty well so the overlay doesn't get instantiated unless we're near one...
|
||||||
|
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
|
||||||
|
|
||||||
|
if (playerEntity == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var elapsedTime = (float) (_gameTiming.CurTime - _lastTick).TotalSeconds;
|
||||||
|
_lastTick = _gameTiming.CurTime;
|
||||||
|
|
||||||
|
var radiationPulses = _componentManager
|
||||||
|
.EntityQuery<RadiationPulseComponent>()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var screenHandle = (DrawingHandleScreen) handle;
|
||||||
|
var viewport = _eyeManager.GetWorldViewport();
|
||||||
|
|
||||||
|
foreach (var grid in _mapManager.FindGridsIntersecting(playerEntity.Transform.MapID, viewport))
|
||||||
|
{
|
||||||
|
foreach (var pulse in radiationPulses)
|
||||||
|
{
|
||||||
|
if (grid.Index != pulse.Owner.Transform.GridID) continue;
|
||||||
|
|
||||||
|
// TODO: Check if viewport intersects circle
|
||||||
|
var circlePosition = _eyeManager.WorldToScreen(pulse.Owner.Transform.WorldPosition);
|
||||||
|
var comp = (RadiationPulseComponent) pulse;
|
||||||
|
|
||||||
|
// change to worldhandle when implemented
|
||||||
|
screenHandle.DrawCircle(
|
||||||
|
circlePosition,
|
||||||
|
comp.Range * 64,
|
||||||
|
GetColor(pulse.Owner, elapsedTime, comp.EndTime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Server.GameObjects.EntitySystems;
|
||||||
|
using Content.Server.GameObjects.EntitySystems.StationEvents;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Robust.Shared.GameObjects.Systems;
|
||||||
|
using Robust.Shared.Interfaces.Timing;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Tests.StationEvents
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class StationEventsSystemTest : ContentIntegrationTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public async Task Test()
|
||||||
|
{
|
||||||
|
var server = StartServerDummyTicker();
|
||||||
|
|
||||||
|
server.Assert(() =>
|
||||||
|
{
|
||||||
|
// Idle each event once
|
||||||
|
var stationEventsSystem = EntitySystem.Get<StationEventSystem>();
|
||||||
|
var dummyFrameTime = (float) IoCManager.Resolve<IGameTiming>().TickPeriod.TotalSeconds;
|
||||||
|
|
||||||
|
foreach (var stationEvent in stationEventsSystem.StationEvents)
|
||||||
|
{
|
||||||
|
stationEvent.Startup();
|
||||||
|
stationEvent.Update(dummyFrameTime);
|
||||||
|
stationEvent.Shutdown();
|
||||||
|
Assert.That(stationEvent.Occurrences == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
stationEventsSystem.ResettingCleanup();
|
||||||
|
|
||||||
|
foreach (var stationEvent in stationEventsSystem.StationEvents)
|
||||||
|
{
|
||||||
|
Assert.That(stationEvent.Occurrences == 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.WaitIdleAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,15 @@ namespace Content.Server.Chat
|
|||||||
_netManager.ServerSendToAll(msg);
|
_netManager.ServerSendToAll(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void DispatchStationAnnouncement(string message)
|
||||||
|
{
|
||||||
|
var msg = _netManager.CreateNetMessage<MsgChatMessage>();
|
||||||
|
msg.Channel = ChatChannel.Radio;
|
||||||
|
msg.Message = message;
|
||||||
|
msg.MessageWrap = "Station: {0}";
|
||||||
|
_netManager.ServerSendToAll(msg);
|
||||||
|
}
|
||||||
|
|
||||||
public void DispatchServerMessage(IPlayerSession player, string message)
|
public void DispatchServerMessage(IPlayerSession player, string message)
|
||||||
{
|
{
|
||||||
var msg = _netManager.CreateNetMessage<MsgChatMessage>();
|
var msg = _netManager.CreateNetMessage<MsgChatMessage>();
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using Content.Shared.GameObjects.Components;
|
||||||
|
using Robust.Server.GameObjects.EntitySystems;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.GameObjects.Systems;
|
||||||
|
using Robust.Shared.Interfaces.Random;
|
||||||
|
using Robust.Shared.Interfaces.Timing;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
using Robust.Shared.Timers;
|
||||||
|
|
||||||
|
namespace Content.Server.GameObjects.Components.StationEvents
|
||||||
|
{
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed class RadiationPulseComponent : SharedRadiationPulseComponent
|
||||||
|
{
|
||||||
|
private const float MinPulseLifespan = 0.8f;
|
||||||
|
private const float MaxPulseLifespan = 2.5f;
|
||||||
|
|
||||||
|
public float DPS => _dps;
|
||||||
|
private float _dps;
|
||||||
|
|
||||||
|
private TimeSpan _endTime;
|
||||||
|
|
||||||
|
public override void ExposeData(ObjectSerializer serializer)
|
||||||
|
{
|
||||||
|
base.ExposeData(serializer);
|
||||||
|
serializer.DataField(ref _dps, "dps", 40.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
var currentTime = IoCManager.Resolve<IGameTiming>().CurTime;
|
||||||
|
var duration =
|
||||||
|
TimeSpan.FromSeconds(
|
||||||
|
IoCManager.Resolve<IRobustRandom>().NextFloat() * (MaxPulseLifespan - MinPulseLifespan) +
|
||||||
|
MinPulseLifespan);
|
||||||
|
|
||||||
|
_endTime = currentTime + duration;
|
||||||
|
|
||||||
|
Timer.Spawn(duration,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (!Owner.Deleted)
|
||||||
|
{
|
||||||
|
Owner.Delete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
EntitySystem.Get<AudioSystem>().PlayAtCoords("/Audio/Weapons/Guns/Gunshots/laser3.ogg", Owner.Transform.GridPosition);
|
||||||
|
Dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ComponentState GetComponentState()
|
||||||
|
{
|
||||||
|
return new RadiationPulseMessage(_endTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Server.GameObjects.Components.Damage;
|
||||||
|
using Content.Server.GameObjects.Components.Mobs;
|
||||||
|
using Content.Server.GameObjects.Components.StationEvents;
|
||||||
|
using Content.Shared.GameObjects;
|
||||||
|
using Content.Shared.GameObjects.Components.Damage;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.GameObjects.Systems;
|
||||||
|
using Robust.Shared.Interfaces.GameObjects;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
|
||||||
|
namespace Content.Server.GameObjects.EntitySystems.StationEvents
|
||||||
|
{
|
||||||
|
[UsedImplicitly]
|
||||||
|
public sealed class RadiationPulseSystem : EntitySystem
|
||||||
|
{
|
||||||
|
// Rather than stuffing around with collidables and checking entities on initialize etc. we'll just tick over
|
||||||
|
// for each entity in range. Seemed easier than checking entities on spawn, then checking collidables, etc.
|
||||||
|
// Especially considering each pulse is a big chonker, + no circle hitboxes yet.
|
||||||
|
|
||||||
|
private TypeEntityQuery _speciesQuery;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Damage works with ints so we'll just accumulate damage and once we hit this threshold we'll apply it.
|
||||||
|
/// </summary>
|
||||||
|
/// This also server to stop spamming the damagethreshold with 1 damage continuously.
|
||||||
|
private const int DamageThreshold = 10;
|
||||||
|
|
||||||
|
private Dictionary<IEntity, float> _accumulatedDamage = new Dictionary<IEntity, float>();
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
_speciesQuery = new TypeEntityQuery(typeof(SpeciesComponent));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(float frameTime)
|
||||||
|
{
|
||||||
|
base.Update(frameTime);
|
||||||
|
var anyPulses = false;
|
||||||
|
|
||||||
|
foreach (var comp in ComponentManager.EntityQuery<RadiationPulseComponent>())
|
||||||
|
{
|
||||||
|
anyPulses = true;
|
||||||
|
|
||||||
|
foreach (var species in EntityManager.GetEntities(_speciesQuery))
|
||||||
|
{
|
||||||
|
// Work out if we're in range and accumulate more damage
|
||||||
|
// If we've hit the DamageThreshold we'll also apply that damage to the mob
|
||||||
|
// If we're really lagging server can apply multiples of the DamageThreshold at once
|
||||||
|
if (species.Transform.MapID != comp.Owner.Transform.MapID) continue;
|
||||||
|
|
||||||
|
if ((species.Transform.WorldPosition - comp.Owner.Transform.WorldPosition).Length > comp.Range)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalDamage = frameTime * comp.DPS;
|
||||||
|
|
||||||
|
if (!_accumulatedDamage.TryGetValue(species, out var accumulatedSpecies))
|
||||||
|
{
|
||||||
|
_accumulatedDamage[species] = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalDamage += accumulatedSpecies;
|
||||||
|
_accumulatedDamage[species] = totalDamage;
|
||||||
|
|
||||||
|
if (totalDamage < DamageThreshold) continue;
|
||||||
|
if (!species.TryGetComponent(out DamageableComponent damageableComponent)) continue;
|
||||||
|
|
||||||
|
var damageMultiple = (int) (totalDamage / DamageThreshold);
|
||||||
|
_accumulatedDamage[species] = totalDamage % DamageThreshold;
|
||||||
|
|
||||||
|
damageableComponent.TakeDamage(DamageType.Heat, damageMultiple * DamageThreshold, comp.Owner, comp.Owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyPulses)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// probably don't need to worry about clearing this at roundreset unless you have a radiation pulse at roundstart
|
||||||
|
// (which is currently not possible)
|
||||||
|
_accumulatedDamage.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using Content.Server.StationEvents;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Server.Interfaces.Player;
|
||||||
|
using Robust.Shared.GameObjects.Systems;
|
||||||
|
using Robust.Shared.Interfaces.Random;
|
||||||
|
using Robust.Shared.Interfaces.Reflection;
|
||||||
|
using Robust.Shared.Interfaces.Timing;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
|
||||||
|
namespace Content.Server.GameObjects.EntitySystems.StationEvents
|
||||||
|
{
|
||||||
|
[UsedImplicitly]
|
||||||
|
public sealed class StationEventSystem : EntitySystem
|
||||||
|
{
|
||||||
|
// Somewhat based off of TG's implementation of events
|
||||||
|
|
||||||
|
public StationEvent CurrentEvent { get; private set; }
|
||||||
|
|
||||||
|
public IReadOnlyCollection<StationEvent> StationEvents => _stationEvents;
|
||||||
|
private List<StationEvent> _stationEvents = new List<StationEvent>();
|
||||||
|
|
||||||
|
private const float MinimumTimeUntilFirstEvent = 600;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How long until the next check for an event runs
|
||||||
|
/// </summary>
|
||||||
|
/// Default value is how long until first event is allowed
|
||||||
|
private float _timeUntilNextEvent = MinimumTimeUntilFirstEvent;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether random events can run
|
||||||
|
/// </summary>
|
||||||
|
/// If disabled while an event is running (even if admin run) it will disable it
|
||||||
|
public bool Enabled
|
||||||
|
{
|
||||||
|
get => _enabled;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_enabled == value)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_enabled = value;
|
||||||
|
CurrentEvent?.Shutdown();
|
||||||
|
CurrentEvent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _enabled = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Admins can get a list of all events available to run, regardless of whether their requirements have been met
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string GetEventNames()
|
||||||
|
{
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
|
||||||
|
foreach (var stationEvent in _stationEvents)
|
||||||
|
{
|
||||||
|
result.Append(stationEvent.Name + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Admins can forcibly run events by passing in the Name
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The exact string for Name, without localization</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string RunEvent(string name)
|
||||||
|
{
|
||||||
|
// Could use a dictionary but it's such a minor thing, eh.
|
||||||
|
// Wasn't sure on whether to localize this given it's a command
|
||||||
|
var upperName = name.ToUpperInvariant();
|
||||||
|
|
||||||
|
foreach (var stationEvent in _stationEvents)
|
||||||
|
{
|
||||||
|
if (stationEvent.Name.ToUpperInvariant() != upperName)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentEvent?.Shutdown();
|
||||||
|
CurrentEvent = stationEvent;
|
||||||
|
stationEvent.Startup();
|
||||||
|
return Loc.GetString("Running event ") + stationEvent.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// I had string interpolation but lord it made it hard to read
|
||||||
|
return Loc.GetString("No event named ") + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Randomly run a valid event immediately, ignoring earlieststart
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string RunRandomEvent()
|
||||||
|
{
|
||||||
|
var availableEvents = AvailableEvents(true);
|
||||||
|
var randomEvent = FindEvent(availableEvents);
|
||||||
|
|
||||||
|
if (randomEvent == null)
|
||||||
|
{
|
||||||
|
return Loc.GetString("No valid events available");
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentEvent?.Shutdown();
|
||||||
|
CurrentEvent = randomEvent;
|
||||||
|
CurrentEvent.Startup();
|
||||||
|
|
||||||
|
return Loc.GetString("Running ") + randomEvent.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Admins can stop the currently running event (if applicable) and reset the timer
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string StopEvent()
|
||||||
|
{
|
||||||
|
string resultText;
|
||||||
|
|
||||||
|
if (CurrentEvent == null)
|
||||||
|
{
|
||||||
|
resultText = Loc.GetString("No event running currently");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
resultText = Loc.GetString("Stopped event ") + CurrentEvent.Name;
|
||||||
|
CurrentEvent.Shutdown();
|
||||||
|
CurrentEvent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetTimer();
|
||||||
|
return resultText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
var reflectionManager = IoCManager.Resolve<IReflectionManager>();
|
||||||
|
var typeFactory = IoCManager.Resolve<IDynamicTypeFactory>();
|
||||||
|
|
||||||
|
foreach (var type in reflectionManager.GetAllChildren(typeof(StationEvent)))
|
||||||
|
{
|
||||||
|
if (type.IsAbstract) continue;
|
||||||
|
|
||||||
|
var stationEvent = (StationEvent) typeFactory.CreateInstance(type);
|
||||||
|
_stationEvents.Add(stationEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(float frameTime)
|
||||||
|
{
|
||||||
|
base.Update(frameTime);
|
||||||
|
|
||||||
|
if (!Enabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep running the current event
|
||||||
|
if (CurrentEvent != null)
|
||||||
|
{
|
||||||
|
CurrentEvent.Update(frameTime);
|
||||||
|
|
||||||
|
// Shutdown the event and set the timer for the next event
|
||||||
|
if (!CurrentEvent.Running)
|
||||||
|
{
|
||||||
|
CurrentEvent.Shutdown();
|
||||||
|
CurrentEvent = null;
|
||||||
|
ResetTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_timeUntilNextEvent > 0)
|
||||||
|
{
|
||||||
|
_timeUntilNextEvent -= frameTime;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No point hammering this trying to find events if none are available
|
||||||
|
var stationEvent = FindEvent(AvailableEvents());
|
||||||
|
if (stationEvent == null)
|
||||||
|
{
|
||||||
|
ResetTimer();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CurrentEvent = stationEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reset the event timer once the event is done.
|
||||||
|
/// </summary>
|
||||||
|
private void ResetTimer()
|
||||||
|
{
|
||||||
|
var robustRandom = IoCManager.Resolve<IRobustRandom>();
|
||||||
|
// 5 - 15 minutes. TG does 3-10 but that's pretty frequent
|
||||||
|
_timeUntilNextEvent = robustRandom.Next(300, 900);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pick a random event from the available events at this time, also considering their weightings.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
private StationEvent FindEvent(List<StationEvent> availableEvents)
|
||||||
|
{
|
||||||
|
if (availableEvents.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sumOfWeights = 0;
|
||||||
|
|
||||||
|
foreach (var stationEvent in availableEvents)
|
||||||
|
{
|
||||||
|
sumOfWeights += (int) stationEvent.Weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
var robustRandom = IoCManager.Resolve<IRobustRandom>();
|
||||||
|
sumOfWeights = robustRandom.Next(sumOfWeights);
|
||||||
|
|
||||||
|
foreach (var stationEvent in availableEvents)
|
||||||
|
{
|
||||||
|
sumOfWeights -= (int) stationEvent.Weight;
|
||||||
|
|
||||||
|
if (sumOfWeights <= 0)
|
||||||
|
{
|
||||||
|
return stationEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the events that have met their player count, time-until start, etc.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ignoreEarliestStart"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private List<StationEvent> AvailableEvents(bool ignoreEarliestStart = false)
|
||||||
|
{
|
||||||
|
TimeSpan currentTime;
|
||||||
|
var playerCount = IoCManager.Resolve<IPlayerManager>().PlayerCount;
|
||||||
|
|
||||||
|
// playerCount does a lock so we'll just keep the variable here
|
||||||
|
if (!ignoreEarliestStart)
|
||||||
|
{
|
||||||
|
currentTime = IoCManager.Resolve<IGameTiming>().CurTime;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentTime = TimeSpan.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new List<StationEvent>();
|
||||||
|
|
||||||
|
foreach (var stationEvent in _stationEvents)
|
||||||
|
{
|
||||||
|
if (CanRun(stationEvent, playerCount, currentTime))
|
||||||
|
{
|
||||||
|
result.Add(stationEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanRun(StationEvent stationEvent, int playerCount, TimeSpan currentTime)
|
||||||
|
{
|
||||||
|
if (stationEvent.MaxOccurrences.HasValue && stationEvent.Occurrences >= stationEvent.MaxOccurrences.Value)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerCount < stationEvent.MinimumPlayers)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTime != TimeSpan.Zero && currentTime.TotalMinutes < stationEvent.EarliestStart)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResettingCleanup()
|
||||||
|
{
|
||||||
|
if (CurrentEvent != null && CurrentEvent.Running)
|
||||||
|
{
|
||||||
|
CurrentEvent.Shutdown();
|
||||||
|
CurrentEvent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var stationEvent in _stationEvents)
|
||||||
|
{
|
||||||
|
stationEvent.Occurrences = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_timeUntilNextEvent = MinimumTimeUntilFirstEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Shutdown()
|
||||||
|
{
|
||||||
|
base.Shutdown();
|
||||||
|
CurrentEvent?.Shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ using Content.Server.GameObjects.Components.PDA;
|
|||||||
using Content.Server.GameObjects.EntitySystems;
|
using Content.Server.GameObjects.EntitySystems;
|
||||||
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding;
|
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding;
|
||||||
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible;
|
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible;
|
||||||
|
using Content.Server.GameObjects.EntitySystems.StationEvents;
|
||||||
using Content.Server.GameTicking.GamePresets;
|
using Content.Server.GameTicking.GamePresets;
|
||||||
using Content.Server.Interfaces;
|
using Content.Server.Interfaces;
|
||||||
using Content.Server.Interfaces.Chat;
|
using Content.Server.Interfaces.Chat;
|
||||||
@@ -619,15 +620,14 @@ namespace Content.Server.GameTicking
|
|||||||
|
|
||||||
_playerJoinLobby(player);
|
_playerJoinLobby(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset pathing system
|
|
||||||
EntitySystem.Get<PathfindingSystem>().ResettingCleanup();
|
EntitySystem.Get<PathfindingSystem>().ResettingCleanup();
|
||||||
EntitySystem.Get<AiReachableSystem>().ResettingCleanup();
|
EntitySystem.Get<AiReachableSystem>().ResettingCleanup();
|
||||||
|
EntitySystem.Get<WireHackingSystem>().ResetLayouts();
|
||||||
|
EntitySystem.Get<StationEventSystem>().ResettingCleanup();
|
||||||
|
|
||||||
_spawnedPositions.Clear();
|
_spawnedPositions.Clear();
|
||||||
_manifest.Clear();
|
_manifest.Clear();
|
||||||
|
|
||||||
EntitySystem.Get<WireHackingSystem>().ResetLayouts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void _preRoundSetup()
|
private void _preRoundSetup()
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ namespace Content.Server.Interfaces.Chat
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
void DispatchServerAnnouncement(string message);
|
void DispatchServerAnnouncement(string message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Station announcement to every player
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
void DispatchStationAnnouncement(string message);
|
||||||
|
|
||||||
void DispatchServerMessage(IPlayerSession player, string message);
|
void DispatchServerMessage(IPlayerSession player, string message);
|
||||||
|
|
||||||
void EntitySay(IEntity source, string message);
|
void EntitySay(IEntity source, string message);
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -114,6 +114,7 @@ namespace Content.Shared.GameObjects.Components.Mobs
|
|||||||
{
|
{
|
||||||
GradientCircleMaskOverlay,
|
GradientCircleMaskOverlay,
|
||||||
CircleMaskOverlay,
|
CircleMaskOverlay,
|
||||||
FlashOverlay
|
FlashOverlay,
|
||||||
|
RadiationPulseOverlay,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.GameObjects.Components
|
||||||
|
{
|
||||||
|
public abstract class SharedRadiationPulseComponent : Component
|
||||||
|
{
|
||||||
|
public override string Name => "RadiationPulse";
|
||||||
|
public override uint? NetID => ContentNetIDs.RADIATION_PULSE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Radius of the pulse from its position
|
||||||
|
/// </summary>
|
||||||
|
public float Range => _range;
|
||||||
|
private float _range;
|
||||||
|
|
||||||
|
public override void ExposeData(ObjectSerializer serializer)
|
||||||
|
{
|
||||||
|
base.ExposeData(serializer);
|
||||||
|
serializer.DataField(ref _range, "range", 5.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For syncing the pulse's lifespan between client and server for the overlay
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class RadiationPulseMessage : ComponentState
|
||||||
|
{
|
||||||
|
public TimeSpan EndTime { get; }
|
||||||
|
|
||||||
|
public RadiationPulseMessage(TimeSpan endTime) : base(ContentNetIDs.RADIATION_PULSE)
|
||||||
|
{
|
||||||
|
EndTime = endTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,7 @@
|
|||||||
public const uint DISPOSABLE = 1056;
|
public const uint DISPOSABLE = 1056;
|
||||||
public const uint GAS_ANALYZER = 1057;
|
public const uint GAS_ANALYZER = 1057;
|
||||||
public const uint DO_AFTER = 1058;
|
public const uint DO_AFTER = 1058;
|
||||||
|
public const uint RADIATION_PULSE = 1059;
|
||||||
|
|
||||||
// Net IDs for integration tests.
|
// Net IDs for integration tests.
|
||||||
public const uint PREDICTION_TEST = 10001;
|
public const uint PREDICTION_TEST = 10001;
|
||||||
|
|||||||
BIN
Resources/Audio/Announcements/power_off.ogg
Normal file
BIN
Resources/Audio/Announcements/power_off.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Announcements/power_on.ogg
Normal file
BIN
Resources/Audio/Announcements/power_on.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Announcements/radiation.ogg
Normal file
BIN
Resources/Audio/Announcements/radiation.ogg
Normal file
Binary file not shown.
@@ -36,6 +36,7 @@
|
|||||||
- listplayers
|
- listplayers
|
||||||
- loc
|
- loc
|
||||||
- hostlogin
|
- hostlogin
|
||||||
|
- events
|
||||||
|
|
||||||
- Index: 100
|
- Index: 100
|
||||||
Name: Administrator
|
Name: Administrator
|
||||||
@@ -93,6 +94,7 @@
|
|||||||
- unanchor
|
- unanchor
|
||||||
- tubeconnections
|
- tubeconnections
|
||||||
- tilewalls
|
- tilewalls
|
||||||
|
- events
|
||||||
CanViewVar: true
|
CanViewVar: true
|
||||||
CanAdminPlace: true
|
CanAdminPlace: true
|
||||||
|
|
||||||
@@ -181,6 +183,7 @@
|
|||||||
- settemp
|
- settemp
|
||||||
- setatmostemp
|
- setatmostemp
|
||||||
- tilewalls
|
- tilewalls
|
||||||
|
- events
|
||||||
CanViewVar: true
|
CanViewVar: true
|
||||||
CanAdminPlace: true
|
CanAdminPlace: true
|
||||||
CanScript: true
|
CanScript: true
|
||||||
|
|||||||
7
Resources/Prototypes/Entities/radiation.yml
Normal file
7
Resources/Prototypes/Entities/radiation.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
- type: entity
|
||||||
|
name: shimmering anomaly
|
||||||
|
id: RadiationPulse
|
||||||
|
abstract: true
|
||||||
|
description: Looking at this anomaly makes you feel strange, like something is pushing at your eyes.
|
||||||
|
components:
|
||||||
|
- type: RadiationPulse
|
||||||
Reference in New Issue
Block a user