diff --git a/Content.Client/AlertLevel/AlertLevelDisplaySystem.cs b/Content.Client/AlertLevel/AlertLevelDisplaySystem.cs new file mode 100644 index 0000000000..cbeda8ff7c --- /dev/null +++ b/Content.Client/AlertLevel/AlertLevelDisplaySystem.cs @@ -0,0 +1,46 @@ +using System.Linq; +using Content.Shared.AlertLevel; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Shared.Utility; + +namespace Content.Client.AlertLevel; + +public sealed class AlertLevelDisplaySystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAppearanceChange); + } + + private void OnAppearanceChange(EntityUid uid, AlertLevelDisplayComponent component, ref AppearanceChangeEvent args) + { + if (args.Sprite == null) + { + return; + } + + if (!args.Sprite.LayerMapTryGet(AlertLevelDisplay.Layer, out _)) + { + var layer = args.Sprite.AddLayer(new RSI.StateId(component.AlertVisuals.Values.First())); + args.Sprite.LayerMapSet(AlertLevelDisplay.Layer, layer); + } + + if (!args.AppearanceData.TryGetValue(AlertLevelDisplay.CurrentLevel, out var level)) + { + args.Sprite.LayerSetState(AlertLevelDisplay.Layer, new RSI.StateId(component.AlertVisuals.Values.First())); + return; + } + + if (component.AlertVisuals.TryGetValue((string) level, out var visual)) + { + args.Sprite.LayerSetState(AlertLevelDisplay.Layer, new RSI.StateId(visual)); + } + else + { + args.Sprite.LayerSetState(AlertLevelDisplay.Layer, new RSI.StateId(component.AlertVisuals.Values.First())); + } + } +} diff --git a/Content.Client/Communications/UI/CommunicationsConsoleBoundUserInterface.cs b/Content.Client/Communications/UI/CommunicationsConsoleBoundUserInterface.cs index 5e6a35dc40..955b008e8b 100644 --- a/Content.Client/Communications/UI/CommunicationsConsoleBoundUserInterface.cs +++ b/Content.Client/Communications/UI/CommunicationsConsoleBoundUserInterface.cs @@ -19,6 +19,10 @@ namespace Content.Client.Communications.UI public bool CountdownStarted { get; private set; } + public bool AlertLevelSelectable { get; private set; } + + public string CurrentLevel { get; private set; } = default!; + public int Countdown => _expectedCountdownTime == null ? 0 : Math.Max((int)_expectedCountdownTime.Value.Subtract(_gameTiming.CurTime).TotalSeconds, 0); private TimeSpan? _expectedCountdownTime; @@ -36,6 +40,15 @@ namespace Content.Client.Communications.UI _menu.OpenCentered(); } + public void AlertLevelSelected(string level) + { + if (AlertLevelSelectable) + { + CurrentLevel = level; + SendMessage(new CommunicationsConsoleSelectAlertLevelMessage(level)); + } + } + public void EmergencyShuttleButtonPressed() { if (CountdownStarted) @@ -71,10 +84,14 @@ namespace Content.Client.Communications.UI CanCall = commsState.CanCall; _expectedCountdownTime = commsState.ExpectedCountdownEnd; CountdownStarted = commsState.CountdownStarted; + AlertLevelSelectable = commsState.AlertLevels != null && !float.IsNaN(commsState.CurrentAlertDelay) && commsState.CurrentAlertDelay <= 0; + CurrentLevel = commsState.CurrentAlert; if (_menu != null) { _menu.UpdateCountdown(); + _menu.UpdateAlertLevels(commsState.AlertLevels, CurrentLevel); + _menu.AlertLevelButton.Disabled = !AlertLevelSelectable; _menu.EmergencyShuttleButton.Disabled = !CanCall; _menu.AnnounceButton.Disabled = !CanAnnounce; } diff --git a/Content.Client/Communications/UI/CommunicationsConsoleMenu.cs b/Content.Client/Communications/UI/CommunicationsConsoleMenu.cs index 8b2fc31f76..66de4561f0 100644 --- a/Content.Client/Communications/UI/CommunicationsConsoleMenu.cs +++ b/Content.Client/Communications/UI/CommunicationsConsoleMenu.cs @@ -18,6 +18,7 @@ namespace Content.Client.Communications.UI public readonly Button AnnounceButton; public readonly Button EmergencyShuttleButton; private readonly RichTextLabel _countdownLabel; + public readonly OptionButton AlertLevelButton; public CommunicationsConsoleMenu(CommunicationsConsoleBoundUserInterface owner) { @@ -38,6 +39,17 @@ namespace Content.Client.Communications.UI AnnounceButton.OnPressed += (_) => Owner.AnnounceButtonPressed(_messageInput.Text.Trim()); AnnounceButton.Disabled = !owner.CanAnnounce; + AlertLevelButton = new OptionButton(); + AlertLevelButton.OnItemSelected += args => + { + var metadata = AlertLevelButton.GetItemMetadata(args.Id); + if (metadata != null && metadata is string cast) + { + Owner.AlertLevelSelected(cast); + } + }; + AlertLevelButton.Disabled = !owner.AlertLevelSelectable; + _countdownLabel = new RichTextLabel(){MinSize = new Vector2(0, 200)}; EmergencyShuttleButton = new Button(); EmergencyShuttleButton.OnPressed += (_) => Owner.EmergencyShuttleButtonPressed(); @@ -52,6 +64,7 @@ namespace Content.Client.Communications.UI vbox.AddChild(_messageInput); vbox.AddChild(new Control(){MinSize = new Vector2(0,10), HorizontalExpand = true}); vbox.AddChild(AnnounceButton); + vbox.AddChild(AlertLevelButton); vbox.AddChild(new Control(){MinSize = new Vector2(0,10), HorizontalExpand = true}); vbox.AddChild(_countdownLabel); vbox.AddChild(EmergencyShuttleButton); @@ -72,6 +85,34 @@ namespace Content.Client.Communications.UI Timer.SpawnRepeating(1000, UpdateCountdown, _timerCancelTokenSource.Token); } + // The current alert could make levels unselectable, so we need to ensure that the UI reacts properly. + // If the current alert is unselectable, the only item in the alerts list will be + // the current alert. Otherwise, it will be the list of alerts, with the current alert + // selected. + public void UpdateAlertLevels(List? alerts, string currentAlert) + { + AlertLevelButton.Clear(); + + if (alerts == null) + { + AlertLevelButton.AddItem(Loc.GetString($"alert-level-{currentAlert}")); + AlertLevelButton.SetItemMetadata(AlertLevelButton.ItemCount - 1, currentAlert); + } + else + { + foreach (var alert in alerts) + { + AlertLevelButton.AddItem(Loc.GetString($"alert-level-{alert}")); + AlertLevelButton.SetItemMetadata(AlertLevelButton.ItemCount - 1, alert); + + if (alert == currentAlert) + { + AlertLevelButton.Select(AlertLevelButton.ItemCount - 1); + } + } + } + } + public void UpdateCountdown() { if (!Owner.CountdownStarted) diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index b4cd2ef576..3492ce70ec 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -106,6 +106,7 @@ namespace Content.Client.Entry prototypes.RegisterIgnore("instantSpell"); prototypes.RegisterIgnore("roundAnnouncement"); prototypes.RegisterIgnore("wireLayout"); + prototypes.RegisterIgnore("alertLevels"); prototypes.RegisterIgnore("nukeopsRole"); ClientContentIoC.Register(); diff --git a/Content.Server/AlertLevel/AlertLevelComponent.cs b/Content.Server/AlertLevel/AlertLevelComponent.cs new file mode 100644 index 0000000000..4a9c32d4a3 --- /dev/null +++ b/Content.Server/AlertLevel/AlertLevelComponent.cs @@ -0,0 +1,46 @@ +namespace Content.Server.AlertLevel; + +/// +/// Alert level component. This is the component given to a station to +/// signify its alert level state. +/// +[RegisterComponent] +public sealed class AlertLevelComponent : Component +{ + /// + /// The current set of alert levels on the station. + /// + [ViewVariables] + public AlertLevelPrototype? AlertLevels; + + // Once stations are a prototype, this should be used. + [DataField("alertLevelPrototype")] + public string AlertLevelPrototype = default!; + + /// + /// The current level on the station. + /// + [ViewVariables(VVAccess.ReadWrite)] public string CurrentLevel = string.Empty; + + [ViewVariables] public const float Delay = 300; + [ViewVariables] public float CurrentDelay = 0; + [ViewVariables] public bool ActiveDelay; + + /// + /// If the level can be selected on the station. + /// + [ViewVariables] + public bool IsSelectable + { + get + { + if (AlertLevels == null + || !AlertLevels.Levels.TryGetValue(CurrentLevel, out var level)) + { + return false; + } + + return level.Selectable && !level.DisableSelection; + } + } +} diff --git a/Content.Server/AlertLevel/AlertLevelDisplaySystem.cs b/Content.Server/AlertLevel/AlertLevelDisplaySystem.cs new file mode 100644 index 0000000000..1660cc68cf --- /dev/null +++ b/Content.Server/AlertLevel/AlertLevelDisplaySystem.cs @@ -0,0 +1,35 @@ +using Content.Server.Station.Systems; +using Content.Shared.AlertLevel; + +namespace Content.Server.AlertLevel; + +public sealed class AlertLevelDisplaySystem : EntitySystem +{ + [Dependency] private readonly StationSystem _stationSystem = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnAlertChanged); + SubscribeLocalEvent(OnDisplayInit); + } + + private void OnAlertChanged(AlertLevelChangedEvent args) + { + foreach (var (_, appearance) in EntityManager.EntityQuery()) + { + appearance.SetData(AlertLevelDisplay.CurrentLevel, args.AlertLevel); + } + } + + private void OnDisplayInit(EntityUid uid, AlertLevelDisplayComponent component, ComponentInit args) + { + if (TryComp(uid, out AppearanceComponent? appearance)) + { + var stationUid = _stationSystem.GetOwningStation(uid); + if (stationUid != null && TryComp(stationUid, out AlertLevelComponent? alert)) + { + appearance.SetData(AlertLevelDisplay.CurrentLevel, alert.CurrentLevel); + } + } + } +} diff --git a/Content.Server/AlertLevel/AlertLevelPrototype.cs b/Content.Server/AlertLevel/AlertLevelPrototype.cs new file mode 100644 index 0000000000..b74e64013b --- /dev/null +++ b/Content.Server/AlertLevel/AlertLevelPrototype.cs @@ -0,0 +1,61 @@ +using System.Collections.Specialized; +using Content.Shared.Sound; +using Robust.Shared.Prototypes; + +namespace Content.Server.AlertLevel; + +[Prototype("alertLevels")] +public sealed class AlertLevelPrototype : IPrototype +{ + [IdDataField] public string ID { get; } = default!; + + /// + /// Dictionary of alert levels. Keyed by string - the string key is the most important + /// part here. Visualizers will use this in order to dictate what alert level to show on + /// client side sprites, and localization uses each key to dictate the alert level name. + /// + [DataField("levels")] public Dictionary Levels = new(); + + /// + /// Default level that the station is on upon initialization. + /// If this isn't in the dictionary, this will default to whatever .First() gives. + /// + [DataField("defaultLevel")] public string DefaultLevel { get; }= default!; +} + +/// +/// Alert level detail. Does not contain an ID, that is handled by +/// the Levels field in AlertLevelPrototype. +/// +[DataDefinition] +public sealed class AlertLevelDetail +{ + /// + /// What is announced upon this alert level change. Can be a localized string. + /// + [DataField("announcement")] public string Announcement { get; } = string.Empty; + + /// + /// Whether this alert level is selectable from a communications console. + /// + [DataField("selectable")] public bool Selectable { get; } = true; + + /// + /// If this alert level disables user selection while it is active. Beware - + /// setting this while something is selectable will disable selection permanently! + /// This should only apply to entities or gamemodes that auto-select an alert level, + /// such as a nuclear bomb being set to active. + /// + [DataField("disableSelection")] public bool DisableSelection { get; } + + /// + /// The sound that this alert level will play in-game once selected. + /// + [DataField("sound")] public SoundSpecifier? Sound { get; } + + /// + /// The color that this alert level will show in-game in chat. + /// + [DataField("color")] public Color Color { get; } = Color.White; +} + diff --git a/Content.Server/AlertLevel/AlertLevelSystem.cs b/Content.Server/AlertLevel/AlertLevelSystem.cs new file mode 100644 index 0000000000..5000f96ba3 --- /dev/null +++ b/Content.Server/AlertLevel/AlertLevelSystem.cs @@ -0,0 +1,158 @@ +using System.Linq; +using Content.Server.Administration.Logs; +using Content.Server.Chat.Managers; +using Content.Server.Station.Components; +using Content.Server.Station.Systems; +using Content.Shared.AlertLevel; +using Robust.Shared.Audio; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Server.AlertLevel; + +public sealed class AlertLevelSystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly StationSystem _stationSystem = default!; + + // Until stations are a prototype, this is how it's going to have to be. + public const string DefaultAlertLevelSet = "stationAlerts"; + + public override void Initialize() + { + SubscribeLocalEvent(OnStationInitialize); + } + + public override void Update(float time) + { + foreach (var station in _stationSystem.Stations) + { + if (!TryComp(station, out AlertLevelComponent? alert)) + { + continue; + } + + if (alert.CurrentDelay <= 0) + { + if (alert.ActiveDelay) + { + RaiseLocalEvent(new AlertLevelDelayFinishedEvent()); + alert.ActiveDelay = false; + } + continue; + } + + alert.CurrentDelay--; + } + } + + private void OnStationInitialize(StationInitializedEvent args) + { + var alertLevelComponent = AddComp(args.Station); + + if (!_prototypeManager.TryIndex(DefaultAlertLevelSet, out AlertLevelPrototype? alerts)) + { + return; + } + + alertLevelComponent.AlertLevels = alerts; + + var defaultLevel = alertLevelComponent.AlertLevels.DefaultLevel; + if (string.IsNullOrEmpty(defaultLevel)) + { + defaultLevel = alertLevelComponent.AlertLevels.Levels.Keys.First(); + } + + SetLevel(args.Station, defaultLevel, false, false, true); + } + + public float GetAlertLevelDelay(EntityUid station, AlertLevelComponent? alert = null) + { + if (!Resolve(station, ref alert)) + { + return float.NaN; + } + + return alert.CurrentDelay; + } + + /// + /// Set the alert level based on the station's entity ID. + /// + /// Station entity UID. + /// Level to change the station's alert level to. + /// Play the alert level's sound. + /// Say the alert level's announcement. + /// Force the alert change. This applies if the alert level is not selectable or not. + public void SetLevel(EntityUid station, string level, bool playSound, bool announce, bool force = false, + MetaDataComponent? dataComponent = null, AlertLevelComponent? component = null) + { + if (!Resolve(station, ref component, ref dataComponent) + || component.AlertLevels == null + || !component.AlertLevels.Levels.TryGetValue(level, out var detail) + || component.CurrentLevel == level) + { + return; + } + + if (!force) + { + if (!detail.Selectable + || component.CurrentDelay > 0) + { + return; + } + + component.CurrentDelay = AlertLevelComponent.Delay; + component.ActiveDelay = true; + } + + component.CurrentLevel = level; + + var stationName = dataComponent.EntityName; + + var name = Loc.GetString($"alert-level-{level}").ToLower(); + + // Announcement text. Is passed into announcementFull. + var announcement = Loc.GetString(detail.Announcement); + + // The full announcement to be spat out into chat. + var announcementFull = Loc.GetString("alert-level-announcement", ("name", name), ("announcement", announcement)); + + var playDefault = false; + if (playSound) + { + if (detail.Sound != null) + { + SoundSystem.Play(Filter.Broadcast(), detail.Sound.GetSound()); + } + else + { + playDefault = true; + } + } + + if (announce) + { + + _chatManager.DispatchStationAnnouncement(announcementFull, playDefaultSound: playDefault, + colorOverride: detail.Color, sender: stationName); + } + + RaiseLocalEvent(new AlertLevelChangedEvent(level)); + } +} + +public sealed class AlertLevelDelayFinishedEvent : EntityEventArgs +{} + +public sealed class AlertLevelChangedEvent : EntityEventArgs +{ + public string AlertLevel { get; } + + public AlertLevelChangedEvent(string alertLevel) + { + AlertLevel = alertLevel; + } +} diff --git a/Content.Server/Atmos/Monitor/Systems/FireAlarmSystem.cs b/Content.Server/Atmos/Monitor/Systems/FireAlarmSystem.cs index 0e2aa12928..2895367420 100644 --- a/Content.Server/Atmos/Monitor/Systems/FireAlarmSystem.cs +++ b/Content.Server/Atmos/Monitor/Systems/FireAlarmSystem.cs @@ -1,5 +1,7 @@ +using Content.Server.AlertLevel; using Content.Server.Atmos.Monitor.Components; using Content.Server.Power.Components; +using Content.Shared.AlertLevel; using Content.Shared.Atmos.Monitor; using Content.Shared.Interaction; using Content.Shared.Emag.Systems; diff --git a/Content.Server/Communications/CommunicationsConsoleComponent.cs b/Content.Server/Communications/CommunicationsConsoleComponent.cs index c7ff75c107..b31bf5d32b 100644 --- a/Content.Server/Communications/CommunicationsConsoleComponent.cs +++ b/Content.Server/Communications/CommunicationsConsoleComponent.cs @@ -1,9 +1,12 @@ using System.Globalization; +using System.Linq; using System.Threading; using Content.Server.Access.Systems; +using Content.Server.AlertLevel; using Content.Server.Chat.Managers; using Content.Server.Power.Components; using Content.Server.RoundEnd; +using Content.Server.Station.Systems; using Content.Server.UserInterface; using Content.Shared.Communications; using Robust.Server.GameObjects; @@ -12,6 +15,7 @@ using Timer = Robust.Shared.Timing.Timer; namespace Content.Server.Communications { + // TODO: ECS [RegisterComponent] public sealed class CommunicationsConsoleComponent : SharedCommunicationsConsoleComponent, IEntityEventSubscriber { @@ -24,6 +28,8 @@ namespace Content.Server.Communications private bool Powered => !_entities.TryGetComponent(Owner, out ApcPowerReceiverComponent? receiver) || receiver.Powered; private RoundEndSystem RoundEndSystem => EntitySystem.Get(); + private AlertLevelSystem AlertLevelSystem => EntitySystem.Get(); + private StationSystem StationSystem => EntitySystem.Get(); [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(CommunicationsConsoleUiKey.Key); @@ -41,6 +47,8 @@ namespace Content.Server.Communications } _entityManager.EventBus.SubscribeEvent(EventSource.Local, this, (s) => UpdateBoundInterface()); + _entityManager.EventBus.SubscribeEvent(EventSource.Local, this, _ => UpdateBoundInterface()); + _entityManager.EventBus.SubscribeEvent(EventSource.Local, this, _ => UpdateBoundInterface()); } protected override void Startup() @@ -56,7 +64,33 @@ namespace Content.Server.Communications { var system = RoundEndSystem; - UserInterface?.SetState(new CommunicationsConsoleInterfaceState(CanAnnounce(), system.CanCall(), system.ExpectedCountdownEnd)); + List? levels = null; + string currentLevel = default!; + float currentDelay = 0; + var stationUid = StationSystem.GetOwningStation(Owner); + if (stationUid != null) + { + if (_entityManager.TryGetComponent(stationUid.Value, out AlertLevelComponent? alerts) + && alerts.AlertLevels != null) + { + if (alerts.IsSelectable) + { + levels = new(); + foreach (var (id, detail) in alerts.AlertLevels.Levels) + { + if (detail.Selectable) + { + levels.Add(id); + } + } + } + + currentLevel = alerts.CurrentLevel; + currentDelay = AlertLevelSystem.GetAlertLevelDelay(stationUid.Value, alerts); + } + } + + UserInterface?.SetState(new CommunicationsConsoleInterfaceState(CanAnnounce(), system.CanCall(), levels, currentLevel, currentDelay, system.ExpectedCountdownEnd)); } } @@ -108,6 +142,14 @@ namespace Content.Server.Communications message += $"\nSent by {author}"; _chatManager.DispatchStationAnnouncement(message, "Communications Console", colorOverride: Color.Gold); + break; + case CommunicationsConsoleSelectAlertLevelMessage alertMsg: + var stationUid = StationSystem.GetOwningStation(Owner); + if (stationUid != null) + { + AlertLevelSystem.SetLevel(stationUid.Value, alertMsg.Level, true, true); + } + break; } } diff --git a/Content.Server/Nuke/NukeComponent.cs b/Content.Server/Nuke/NukeComponent.cs index ba0e25830c..a6f7260556 100644 --- a/Content.Server/Nuke/NukeComponent.cs +++ b/Content.Server/Nuke/NukeComponent.cs @@ -44,6 +44,9 @@ namespace Content.Server.Nuke [DataField("alertTime")] public float AlertSoundTime = 10.0f; + [DataField("alertLevelOnActivate")] public string AlertLevelOnActivate = default!; + [DataField("alertLevelOnDeactivate")] public string AlertLevelOnDeactivate = default!; + [DataField("keypadPressSound")] public SoundSpecifier KeypadPressSound = new SoundPathSpecifier("/Audio/Machines/Nuke/general_beep.ogg"); diff --git a/Content.Server/Nuke/NukeSystem.cs b/Content.Server/Nuke/NukeSystem.cs index 1192f55b68..7e62cda098 100644 --- a/Content.Server/Nuke/NukeSystem.cs +++ b/Content.Server/Nuke/NukeSystem.cs @@ -1,7 +1,9 @@ +using Content.Server.AlertLevel; using Content.Server.Chat.Managers; using Content.Server.Coordinates.Helpers; using Content.Server.Explosion.EntitySystems; using Content.Server.Popups; +using Content.Server.Station.Systems; using Content.Server.UserInterface; using Content.Shared.Audio; using Content.Shared.Construction.Components; @@ -20,6 +22,8 @@ namespace Content.Server.Nuke [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; [Dependency] private readonly PopupSystem _popups = default!; [Dependency] private readonly ExplosionSystem _explosions = default!; + [Dependency] private readonly AlertLevelSystem _alertLevel = default!; + [Dependency] private readonly StationSystem _stationSystem = default!; [Dependency] private readonly IChatManager _chat = default!; public override void Initialize() @@ -323,6 +327,15 @@ namespace Content.Server.Nuke if (component.Status == NukeStatus.ARMED) return; + var stationUid = _stationSystem.GetOwningStation(uid); + // The nuke may not be on a station, so it's more important to just + // let people know that a nuclear bomb was armed in their vicinity instead. + // Otherwise, you could set every station to whatever AlertLevelOnActivate is. + if (stationUid != null) + { + _alertLevel.SetLevel(stationUid.Value, component.AlertLevelOnActivate, true, true, true); + } + // warn a crew var announcement = Loc.GetString("nuke-component-announcement-armed", ("time", (int) component.RemainingTime)); @@ -347,6 +360,12 @@ namespace Content.Server.Nuke if (component.Status != NukeStatus.ARMED) return; + var stationUid = _stationSystem.GetOwningStation(uid); + if (stationUid != null) + { + _alertLevel.SetLevel(stationUid.Value, component.AlertLevelOnDeactivate, true, true, true); + } + // warn a crew var announcement = Loc.GetString("nuke-component-announcement-unarmed"); var sender = Loc.GetString("nuke-component-announcement-sender"); diff --git a/Content.Shared/AlertLevel/AlertLevelDisplayComponent.cs b/Content.Shared/AlertLevel/AlertLevelDisplayComponent.cs new file mode 100644 index 0000000000..b3a697d729 --- /dev/null +++ b/Content.Shared/AlertLevel/AlertLevelDisplayComponent.cs @@ -0,0 +1,8 @@ +namespace Content.Shared.AlertLevel; + +[RegisterComponent] +public sealed class AlertLevelDisplayComponent : Component +{ + [DataField("alertVisuals")] + public readonly Dictionary AlertVisuals = new(); +} diff --git a/Content.Shared/AlertLevel/SharedAlertLevelDisplay.cs b/Content.Shared/AlertLevel/SharedAlertLevelDisplay.cs new file mode 100644 index 0000000000..37634740f6 --- /dev/null +++ b/Content.Shared/AlertLevel/SharedAlertLevelDisplay.cs @@ -0,0 +1,10 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.AlertLevel; + +[Serializable, NetSerializable] +public enum AlertLevelDisplay +{ + CurrentLevel, + Layer +} diff --git a/Content.Shared/Communications/SharedCommunicationsConsoleComponent.cs b/Content.Shared/Communications/SharedCommunicationsConsoleComponent.cs index 0a572e36ec..324f705500 100644 --- a/Content.Shared/Communications/SharedCommunicationsConsoleComponent.cs +++ b/Content.Shared/Communications/SharedCommunicationsConsoleComponent.cs @@ -14,13 +14,30 @@ namespace Content.Shared.Communications public readonly bool CanCall; public readonly TimeSpan? ExpectedCountdownEnd; public readonly bool CountdownStarted; + public List? AlertLevels; + public string CurrentAlert; + public float CurrentAlertDelay; - public CommunicationsConsoleInterfaceState(bool canAnnounce, bool canCall, TimeSpan? expectedCountdownEnd = null) + public CommunicationsConsoleInterfaceState(bool canAnnounce, bool canCall, List? alertLevels, string currentAlert, float currentAlertDelay, TimeSpan? expectedCountdownEnd = null) { CanAnnounce = canAnnounce; CanCall = canCall; ExpectedCountdownEnd = expectedCountdownEnd; CountdownStarted = expectedCountdownEnd != null; + AlertLevels = alertLevels; + CurrentAlert = currentAlert; + CurrentAlertDelay = currentAlertDelay; + } + } + + [Serializable, NetSerializable] + public sealed class CommunicationsConsoleSelectAlertLevelMessage : BoundUserInterfaceMessage + { + public readonly string Level; + + public CommunicationsConsoleSelectAlertLevelMessage(string level) + { + Level = level; } } diff --git a/Resources/Audio/Misc/delta.ogg b/Resources/Audio/Misc/delta.ogg new file mode 100644 index 0000000000..7a9522c1ff Binary files /dev/null and b/Resources/Audio/Misc/delta.ogg differ diff --git a/Resources/Audio/Misc/delta_alt.ogg b/Resources/Audio/Misc/delta_alt.ogg new file mode 100644 index 0000000000..b389910f13 Binary files /dev/null and b/Resources/Audio/Misc/delta_alt.ogg differ diff --git a/Resources/Locale/en-US/alert-levels/alert-levels.ftl b/Resources/Locale/en-US/alert-levels/alert-levels.ftl new file mode 100644 index 0000000000..c310d91088 --- /dev/null +++ b/Resources/Locale/en-US/alert-levels/alert-levels.ftl @@ -0,0 +1,19 @@ +alert-level-announcement = Attention! Station alert level is now {$name}! {$announcement} + +alert-level-green = Green +alert-level-green-announcement = It is now safe to return to your workplaces. + +alert-level-blue = Blue +alert-level-blue-announcement = There is an unknown threat on the station. Security is allowed to perform random checks. Crewmembers are advised to follow commands issued by any relevant authority. + +alert-level-red = Red +alert-level-red-announcement = There is a known threat on the station. Security is allowed to use lethal force if necessary. Crewmembers should find a safe place to shelter in, and are advised to follow any present authorities. + +alert-level-violet = Violet +alert-level-violet-announcement = There is a viral threat on the station. Medical staff are advised to isolate crewmembers with any symptoms. Crewmembers are advised to distance themselves from others and perform safety measures to prevent further spread. + +alert-level-yellow = Yellow +alert-level-yellow-announcement = There is a structural or atmospheric threat within the station. Engineering staff are advised to immediately respond and perform safety measures. Crewmembers are advised to stay away from the threat, and stay in their workplaces if necessary. + +alert-level-delta = Delta +alert-level-delta-announcement = The station is currently under threat of imminent destruction. Crewmembers are advised to listen to heads of staff for more information. diff --git a/Resources/Prototypes/AlertLevels/alert_levels.yml b/Resources/Prototypes/AlertLevels/alert_levels.yml new file mode 100644 index 0000000000..1d05249d3c --- /dev/null +++ b/Resources/Prototypes/AlertLevels/alert_levels.yml @@ -0,0 +1,29 @@ +- type: alertLevels + id: stationAlerts + defaultLevel: green + levels: + green: + announcement: alert-level-green-announcement + color: Green + blue: + announcement: alert-level-blue-announcement + sound: /Audio/Misc/notice1.ogg + color: DodgerBlue + red: + announcement: alert-level-red-announcement + sound: /Audio/Misc/notice1.ogg + color: Red + violet: + announcement: alert-level-violet-announcement + sound: /Audio/Misc/notice1.ogg + color: Violet + yellow: + announcement: alert-level-yellow-announcement + sound: /Audio/Misc/notice1.ogg + color: Yellow + delta: + announcement: alert-level-delta-announcement + selectable: false + sound: /Audio/Misc/delta.ogg + disableSelection: true + color: DarkRed diff --git a/Resources/Prototypes/Entities/Objects/Devices/nuke.yml b/Resources/Prototypes/Entities/Objects/Devices/nuke.yml index 482eab957c..7ea25e2d8d 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/nuke.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/nuke.yml @@ -38,6 +38,8 @@ whitelist: components: - NukeDisk + alertLevelOnActivate: delta + alertLevelOnDeactivate: green - type: InteractionOutline - type: ActivatableUI key: enum.NukeUiKey.Key diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/fire_alarm.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/fire_alarm.yml index ef4b4e7ad5..8b275e53ae 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/fire_alarm.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/fire_alarm.yml @@ -34,6 +34,14 @@ Emagged: fire_emagged hideOnDepowered: ["fireAlarmState"] - type: WiresVisualizer + - type: AlertLevelDisplay + alertVisuals: + green: fire_0 + blue: fire_1 + red: fire_2 + violet: fire_1 + yellow: fire_1 + delta: fire_3 - type: UserInterface interfaces: - key: enum.WiresUiKey.Key