diff --git a/Content.Server/Nuke/NukeSystem.cs b/Content.Server/Nuke/NukeSystem.cs
index 70dfd2ed92..625c958cde 100644
--- a/Content.Server/Nuke/NukeSystem.cs
+++ b/Content.Server/Nuke/NukeSystem.cs
@@ -16,577 +16,578 @@ using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Random;
-namespace Content.Server.Nuke
+namespace Content.Server.Nuke;
+
+public sealed class NukeSystem : EntitySystem
{
- public sealed class NukeSystem : EntitySystem
+ [Dependency] private readonly AlertLevelSystem _alertLevel = default!;
+ [Dependency] private readonly ChatSystem _chatSystem = default!;
+ [Dependency] private readonly ExplosionSystem _explosions = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
+ [Dependency] private readonly PopupSystem _popups = default!;
+ [Dependency] private readonly ServerGlobalSoundSystem _sound = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly StationSystem _station = default!;
+ [Dependency] private readonly UserInterfaceSystem _ui = default!;
+
+ ///
+ /// Used to calculate when the nuke song should start playing for maximum kino with the nuke sfx
+ ///
+ private const float NukeSongLength = 60f + 51.6f;
+
+ ///
+ /// Time to leave between the nuke song and the nuke alarm playing.
+ ///
+ private const float NukeSongBuffer = 1.5f;
+
+ public override void Initialize()
{
- [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 _station = default!;
- [Dependency] private readonly ServerGlobalSoundSystem _soundSystem = default!;
- [Dependency] private readonly ChatSystem _chatSystem = default!;
- [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly UserInterfaceSystem _ui = default!;
- [Dependency] private readonly SharedTransformSystem _xformSystem = default!;
+ base.Initialize();
- ///
- /// Used to calculate when the nuke song should start playing for maximum kino with the nuke sfx
- ///
- private const float NukeSongLength = 60f + 51.6f;
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnRemove);
+ SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnItemSlotChanged);
+ SubscribeLocalEvent(OnItemSlotChanged);
- ///
- /// Time to leave between the nuke song and the nuke alarm playing.
- ///
- private const float NukeSongBuffer = 1.5f;
+ // anchoring logic
+ SubscribeLocalEvent(OnAnchorAttempt);
+ SubscribeLocalEvent(OnUnanchorAttempt);
+ // Shouldn't need re-anchoring.
+ SubscribeLocalEvent(OnAnchorChanged);
- public override void Initialize()
+ // ui events
+ SubscribeLocalEvent(OnAnchorButtonPressed);
+ SubscribeLocalEvent(OnArmButtonPressed);
+ SubscribeLocalEvent(OnKeypadButtonPressed);
+ SubscribeLocalEvent(OnClearButtonPressed);
+ SubscribeLocalEvent(OnEnterButtonPressed);
+
+ // Doafter events
+ SubscribeLocalEvent(OnDoAfter);
+ }
+
+ private void OnInit(EntityUid uid, NukeComponent component, ComponentInit args)
+ {
+ component.RemainingTime = component.Timer;
+ _itemSlots.AddItemSlot(uid, SharedNukeComponent.NukeDiskSlotId, component.DiskSlot);
+
+ UpdateStatus(uid, component);
+ UpdateUserInterface(uid, component);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var nuke))
{
- base.Initialize();
-
- SubscribeLocalEvent(OnInit);
- SubscribeLocalEvent(OnRemove);
- SubscribeLocalEvent(OnMapInit);
- SubscribeLocalEvent(OnItemSlotChanged);
- SubscribeLocalEvent(OnItemSlotChanged);
-
- // anchoring logic
- SubscribeLocalEvent(OnAnchorAttempt);
- SubscribeLocalEvent(OnUnanchorAttempt);
- // Shouldn't need re-anchoring.
- SubscribeLocalEvent(OnAnchorChanged);
-
- // ui events
- SubscribeLocalEvent(OnAnchorButtonPressed);
- SubscribeLocalEvent(OnArmButtonPressed);
- SubscribeLocalEvent(OnKeypadButtonPressed);
- SubscribeLocalEvent(OnClearButtonPressed);
- SubscribeLocalEvent(OnEnterButtonPressed);
-
- // Doafter events
- SubscribeLocalEvent(OnDoAfter);
- }
-
- private void OnInit(EntityUid uid, NukeComponent component, ComponentInit args)
- {
- component.RemainingTime = component.Timer;
- _itemSlots.AddItemSlot(uid, SharedNukeComponent.NukeDiskSlotId, component.DiskSlot);
-
- UpdateStatus(uid, component);
- UpdateUserInterface(uid, component);
- }
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out var nuke))
+ switch (nuke.Status)
{
- switch (nuke.Status)
- {
- case NukeStatus.ARMED:
- TickTimer(uid, frameTime, nuke);
- break;
- case NukeStatus.COOLDOWN:
- TickCooldown(uid, frameTime, nuke);
- break;
- }
- }
- }
-
- private void OnMapInit(EntityUid uid, NukeComponent nuke, MapInitEvent args)
- {
- var originStation = _station.GetOwningStation(uid);
-
- if (originStation != null)
- nuke.OriginStation = originStation;
-
- else
- {
- var transform = Transform(uid);
- nuke.OriginMapGrid = (transform.MapID, transform.GridUid);
- }
-
- nuke.Code = GenerateRandomNumberString(nuke.CodeLength);
- }
-
- private void OnRemove(EntityUid uid, NukeComponent component, ComponentRemove args)
- {
- _itemSlots.RemoveItemSlot(uid, component.DiskSlot);
- }
-
- private void OnItemSlotChanged(EntityUid uid, NukeComponent component, ContainerModifiedMessage args)
- {
- if (!component.Initialized)
- return;
-
- if (args.Container.ID != component.DiskSlot.ID)
- return;
-
- UpdateStatus(uid, component);
- UpdateUserInterface(uid, component);
- }
-
- #region Anchor
-
- private void OnAnchorAttempt(EntityUid uid, NukeComponent component, AnchorAttemptEvent args)
- {
- CheckAnchorAttempt(uid, component, args);
- }
-
- private void OnUnanchorAttempt(EntityUid uid, NukeComponent component, UnanchorAttemptEvent args)
- {
- CheckAnchorAttempt(uid, component, args);
- }
-
- private void CheckAnchorAttempt(EntityUid uid, NukeComponent component, BaseAnchoredAttemptEvent args)
- {
- // cancel any anchor attempt if armed
- if (component.Status == NukeStatus.ARMED)
- {
- var msg = Loc.GetString("nuke-component-cant-anchor");
- _popups.PopupEntity(msg, uid, args.User);
-
- args.Cancel();
- }
- }
-
- private void OnAnchorChanged(EntityUid uid, NukeComponent component, ref AnchorStateChangedEvent args)
- {
- UpdateUserInterface(uid, component);
-
- if (args.Anchored == false && component.Status == NukeStatus.ARMED && component.RemainingTime > component.DisarmDoafterLength)
- {
- // yes, this means technically if you can find a way to unanchor the nuke, you can disarm it
- // without the doafter. but that takes some effort, and it won't allow you to disarm a nuke that can't be disarmed by the doafter.
- DisarmBomb(uid, component);
- }
- }
-
- #endregion
-
- #region UI Events
-
- private async void OnAnchorButtonPressed(EntityUid uid, NukeComponent component, NukeAnchorMessage args)
- {
- if (!component.DiskSlot.HasItem)
- return;
-
- if (!EntityManager.TryGetComponent(uid, out TransformComponent? transform))
- return;
-
- // manually set transform anchor (bypassing anchorable)
- // todo: it will break pullable system
- _xformSystem.SetCoordinates(uid, transform, transform.Coordinates.SnapToGrid());
- if (transform.Anchored)
- _xformSystem.Unanchor(uid, transform);
- else
- _xformSystem.AnchorEntity(uid, transform);
-
- UpdateUserInterface(uid, component);
- }
-
- private void OnEnterButtonPressed(EntityUid uid, NukeComponent component, NukeKeypadEnterMessage args)
- {
- if (component.Status != NukeStatus.AWAIT_CODE)
- return;
-
- UpdateStatus(uid, component);
- UpdateUserInterface(uid, component);
- }
-
- private void OnKeypadButtonPressed(EntityUid uid, NukeComponent component, NukeKeypadMessage args)
- {
- PlayNukeKeypadSound(uid, args.Value, component);
-
- if (component.Status != NukeStatus.AWAIT_CODE)
- return;
-
- if (component.EnteredCode.Length >= component.CodeLength)
- return;
-
- component.EnteredCode += args.Value.ToString();
- UpdateUserInterface(uid, component);
- }
-
- private void OnClearButtonPressed(EntityUid uid, NukeComponent component, NukeKeypadClearMessage args)
- {
- _audio.Play(component.KeypadPressSound, Filter.Pvs(uid), uid, true);
-
- if (component.Status != NukeStatus.AWAIT_CODE)
- return;
-
- component.EnteredCode = "";
- UpdateUserInterface(uid, component);
- }
-
- private void OnArmButtonPressed(EntityUid uid, NukeComponent component, NukeArmedMessage args)
- {
- if (!component.DiskSlot.HasItem)
- return;
-
- if (component.Status == NukeStatus.AWAIT_ARM && Transform(uid).Anchored)
- ArmBomb(uid, component);
-
- else
- {
- if (args.Session.AttachedEntity is not { } user)
- return;
-
- DisarmBombDoafter(uid, user, component);
- }
- }
-
- #endregion
-
- #region Doafter Events
-
- private void OnDoAfter(EntityUid uid, NukeComponent component, DoAfterEvent args)
- {
- if (args.Handled || args.Cancelled)
- return;
-
- DisarmBomb(uid, component);
-
- var ev = new NukeDisarmSuccessEvent();
- RaiseLocalEvent(ev);
-
- args.Handled = true;
- }
- #endregion
-
- private void TickCooldown(EntityUid uid, float frameTime, NukeComponent? nuke = null)
- {
- if (!Resolve(uid, ref nuke))
- return;
-
- nuke.CooldownTime -= frameTime;
- if (nuke.CooldownTime <= 0)
- {
- // reset nuke to default state
- nuke.CooldownTime = 0;
- nuke.Status = NukeStatus.AWAIT_ARM;
- UpdateStatus(uid, nuke);
- }
-
- UpdateUserInterface(uid, nuke);
- }
-
- private void TickTimer(EntityUid uid, float frameTime, NukeComponent? nuke = null)
- {
- if (!Resolve(uid, ref nuke))
- return;
-
- nuke.RemainingTime -= frameTime;
-
- // Start playing the nuke event song so that it ends a couple seconds before the alert sound
- // should play
- if (nuke.RemainingTime <= NukeSongLength + nuke.AlertSoundTime + NukeSongBuffer && !nuke.PlayedNukeSong)
- {
- _soundSystem.DispatchStationEventMusic(uid, nuke.ArmMusic, StationEventMusicType.Nuke);
- nuke.PlayedNukeSong = true;
- }
-
- // play alert sound if time is running out
- if (nuke.RemainingTime <= nuke.AlertSoundTime && !nuke.PlayedAlertSound)
- {
- nuke.AlertAudioStream = _audio.Play(nuke.AlertSound, Filter.Broadcast(), uid, true);
- _soundSystem.StopStationEventMusic(uid, StationEventMusicType.Nuke);
- nuke.PlayedAlertSound = true;
- }
-
- if (nuke.RemainingTime <= 0)
- {
- nuke.RemainingTime = 0;
- ActivateBomb(uid, nuke);
- }
-
- else
- UpdateUserInterface(uid, nuke);
- }
-
- private void UpdateStatus(EntityUid uid, NukeComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
-
- switch (component.Status)
- {
- case NukeStatus.AWAIT_DISK:
- if (component.DiskSlot.HasItem)
- component.Status = NukeStatus.AWAIT_CODE;
- break;
- case NukeStatus.AWAIT_CODE:
- if (!component.DiskSlot.HasItem)
- {
- component.Status = NukeStatus.AWAIT_DISK;
- component.EnteredCode = "";
- break;
- }
-
- // var isValid = _codes.IsCodeValid(uid, component.EnteredCode);
- if (component.EnteredCode == component.Code)
- {
- component.Status = NukeStatus.AWAIT_ARM;
- component.RemainingTime = component.Timer;
- _audio.Play(component.AccessGrantedSound, Filter.Pvs(uid), uid, true);
- }
- else
- {
- component.EnteredCode = "";
- _audio.Play(component.AccessDeniedSound, Filter.Pvs(uid), uid, true);
- }
-
- break;
- case NukeStatus.AWAIT_ARM:
- // do nothing, wait for arm button to be pressed
- break;
case NukeStatus.ARMED:
- // do nothing, wait for arm button to be unpressed
+ TickTimer(uid, frameTime, nuke);
+ break;
+ case NukeStatus.COOLDOWN:
+ TickCooldown(uid, frameTime, nuke);
break;
}
}
-
- private void UpdateUserInterface(EntityUid uid, NukeComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
-
- var ui = _ui.GetUiOrNull(uid, NukeUiKey.Key);
- if (ui == null)
- return;
-
- var anchored = false;
- if (EntityManager.TryGetComponent(uid, out TransformComponent? transform))
- anchored = transform.Anchored;
-
- var allowArm = component.DiskSlot.HasItem &&
- (component.Status == NukeStatus.AWAIT_ARM ||
- component.Status == NukeStatus.ARMED);
-
- var state = new NukeUiState
- {
- Status = component.Status,
- RemainingTime = (int) component.RemainingTime,
- DiskInserted = component.DiskSlot.HasItem,
- IsAnchored = anchored,
- AllowArm = allowArm,
- EnteredCodeLength = component.EnteredCode.Length,
- MaxCodeLength = component.CodeLength,
- CooldownTime = (int) component.CooldownTime
- };
-
- UserInterfaceSystem.SetUiState(ui, state);
- }
-
- private void PlayNukeKeypadSound(EntityUid uid, int number, NukeComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
-
- // This is a C mixolydian blues scale.
- // 1 2 3 C D Eb
- // 4 5 6 E F F#
- // 7 8 9 G A Bb
- var semitoneShift = number switch
- {
- 1 => 0,
- 2 => 2,
- 3 => 3,
- 4 => 4,
- 5 => 5,
- 6 => 6,
- 7 => 7,
- 8 => 9,
- 9 => 10,
- 0 => component.LastPlayedKeypadSemitones + 12,
- _ => 0
- };
-
- // Don't double-dip on the octave shifting
- component.LastPlayedKeypadSemitones = number == 0 ? component.LastPlayedKeypadSemitones : semitoneShift;
-
- _audio.Play(component.KeypadPressSound, Filter.Pvs(uid), uid, true, AudioHelpers.ShiftSemitone(semitoneShift).WithVolume(-5f));
- }
-
- public string GenerateRandomNumberString(int length)
- {
- var ret = "";
- for (var i = 0; i < length; i++)
- {
- var c = (char) _random.Next('0', '9' + 1);
- ret += c;
- }
-
- return ret;
- }
-
- #region Public API
-
- ///
- /// Force a nuclear bomb to start a countdown timer
- ///
- public void ArmBomb(EntityUid uid, NukeComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
-
- if (component.Status == NukeStatus.ARMED)
- return;
-
- var nukeXform = Transform(uid);
- var stationUid = _station.GetStationInMap(nukeXform.MapID);
- // 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, true);
-
- var pos = nukeXform.MapPosition;
- var x = (int) pos.X;
- var y = (int) pos.Y;
- var posText = $"({x}, {y})";
-
- // warn a crew
- var announcement = Loc.GetString("nuke-component-announcement-armed",
- ("time", (int) component.RemainingTime), ("position", posText));
- var sender = Loc.GetString("nuke-component-announcement-sender");
- _chatSystem.DispatchStationAnnouncement(stationUid ?? uid, announcement, sender, false, null, Color.Red);
-
- _soundSystem.PlayGlobalOnStation(uid, _audio.GetSound(component.ArmSound));
-
- _itemSlots.SetLock(uid, component.DiskSlot, true);
- _xformSystem.AnchorEntity(uid, nukeXform);
- component.Status = NukeStatus.ARMED;
- UpdateUserInterface(uid, component);
- }
-
- ///
- /// Stop nuclear bomb timer
- ///
- public void DisarmBomb(EntityUid uid, NukeComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
-
- if (component.Status != NukeStatus.ARMED)
- return;
-
- var stationUid = _station.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");
- _chatSystem.DispatchStationAnnouncement(uid, announcement, sender, false);
-
- component.PlayedNukeSong = false;
- _soundSystem.PlayGlobalOnStation(uid, _audio.GetSound(component.DisarmSound));
- _soundSystem.StopStationEventMusic(uid, StationEventMusicType.Nuke);
-
- // disable sound and reset it
- component.PlayedAlertSound = false;
- component.AlertAudioStream?.Stop();
-
- // start bomb cooldown
- _itemSlots.SetLock(uid, component.DiskSlot, false);
- component.Status = NukeStatus.COOLDOWN;
- component.CooldownTime = component.Cooldown;
-
- UpdateUserInterface(uid, component);
- }
-
- ///
- /// Toggle bomb arm button
- ///
- public void ToggleBomb(EntityUid uid, NukeComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
-
- if (component.Status == NukeStatus.ARMED)
- DisarmBomb(uid, component);
- else
- ArmBomb(uid, component);
- }
-
- ///
- /// Force bomb to explode immediately
- ///
- public void ActivateBomb(EntityUid uid, NukeComponent? component = null,
- TransformComponent? transform = null)
- {
- if (!Resolve(uid, ref component, ref transform))
- return;
-
- if (component.Exploded)
- return;
-
- component.Exploded = true;
-
- _explosions.QueueExplosion(uid,
- component.ExplosionType,
- component.TotalIntensity,
- component.IntensitySlope,
- component.MaxIntensity);
-
- RaiseLocalEvent(new NukeExplodedEvent()
- {
- OwningStation = transform.GridUid,
- });
-
- _soundSystem.StopStationEventMusic(uid, StationEventMusicType.Nuke);
- EntityManager.DeleteEntity(uid);
- }
-
- ///
- /// Set remaining time value
- ///
- public void SetRemainingTime(EntityUid uid, float timer, NukeComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
-
- component.RemainingTime = timer;
- UpdateUserInterface(uid, component);
- }
-
- #endregion
-
- private void DisarmBombDoafter(EntityUid uid, EntityUid user, NukeComponent nuke)
- {
- var doafter = new DoAfterArgs(user, nuke.DisarmDoafterLength, new NukeDisarmDoAfterEvent(), uid, target: uid)
- {
- BreakOnDamage = true,
- BreakOnTargetMove = true,
- BreakOnUserMove = true,
- NeedHand = true
- };
-
- if (!_doAfterSystem.TryStartDoAfter(doafter))
- return;
-
- _popups.PopupEntity(Loc.GetString("nuke-component-doafter-warning"), user,
- user, PopupType.LargeCaution);
- }
}
- public sealed class NukeExplodedEvent : EntityEventArgs
+ private void OnMapInit(EntityUid uid, NukeComponent nuke, MapInitEvent args)
{
- public EntityUid? OwningStation;
+ var originStation = _station.GetOwningStation(uid);
+
+ if (originStation != null)
+ nuke.OriginStation = originStation;
+
+ else
+ {
+ var transform = Transform(uid);
+ nuke.OriginMapGrid = (transform.MapID, transform.GridUid);
+ }
+
+ nuke.Code = GenerateRandomNumberString(nuke.CodeLength);
+ }
+
+ private void OnRemove(EntityUid uid, NukeComponent component, ComponentRemove args)
+ {
+ _itemSlots.RemoveItemSlot(uid, component.DiskSlot);
+ }
+
+ private void OnItemSlotChanged(EntityUid uid, NukeComponent component, ContainerModifiedMessage args)
+ {
+ if (!component.Initialized)
+ return;
+
+ if (args.Container.ID != component.DiskSlot.ID)
+ return;
+
+ UpdateStatus(uid, component);
+ UpdateUserInterface(uid, component);
+ }
+
+ #region Anchor
+
+ private void OnAnchorAttempt(EntityUid uid, NukeComponent component, AnchorAttemptEvent args)
+ {
+ CheckAnchorAttempt(uid, component, args);
+ }
+
+ private void OnUnanchorAttempt(EntityUid uid, NukeComponent component, UnanchorAttemptEvent args)
+ {
+ CheckAnchorAttempt(uid, component, args);
+ }
+
+ private void CheckAnchorAttempt(EntityUid uid, NukeComponent component, BaseAnchoredAttemptEvent args)
+ {
+ // cancel any anchor attempt if armed
+ if (component.Status == NukeStatus.ARMED)
+ {
+ var msg = Loc.GetString("nuke-component-cant-anchor");
+ _popups.PopupEntity(msg, uid, args.User);
+
+ args.Cancel();
+ }
+ }
+
+ private void OnAnchorChanged(EntityUid uid, NukeComponent component, ref AnchorStateChangedEvent args)
+ {
+ UpdateUserInterface(uid, component);
+
+ if (args.Anchored == false && component.Status == NukeStatus.ARMED && component.RemainingTime > component.DisarmDoafterLength)
+ {
+ // yes, this means technically if you can find a way to unanchor the nuke, you can disarm it
+ // without the doafter. but that takes some effort, and it won't allow you to disarm a nuke that can't be disarmed by the doafter.
+ DisarmBomb(uid, component);
+ }
+ }
+
+ #endregion
+
+ #region UI Events
+
+ private async void OnAnchorButtonPressed(EntityUid uid, NukeComponent component, NukeAnchorMessage args)
+ {
+ // malicious client sanity check
+ if (component.Status == NukeStatus.ARMED)
+ return;
+
+ // manually set transform anchor (bypassing anchorable)
+ // todo: it will break pullable system
+ var xform = Transform(uid);
+ if (xform.Anchored)
+ {
+ _transform.Unanchor(uid, xform);
+ }
+ else
+ {
+ _transform.SetCoordinates(uid, xform, xform.Coordinates.SnapToGrid());
+ _transform.AnchorEntity(uid, xform);
+ }
+
+ UpdateUserInterface(uid, component);
+ }
+
+ private void OnEnterButtonPressed(EntityUid uid, NukeComponent component, NukeKeypadEnterMessage args)
+ {
+ if (component.Status != NukeStatus.AWAIT_CODE)
+ return;
+
+ UpdateStatus(uid, component);
+ UpdateUserInterface(uid, component);
+ }
+
+ private void OnKeypadButtonPressed(EntityUid uid, NukeComponent component, NukeKeypadMessage args)
+ {
+ PlayNukeKeypadSound(uid, args.Value, component);
+
+ if (component.Status != NukeStatus.AWAIT_CODE)
+ return;
+
+ if (component.EnteredCode.Length >= component.CodeLength)
+ return;
+
+ component.EnteredCode += args.Value.ToString();
+ UpdateUserInterface(uid, component);
+ }
+
+ private void OnClearButtonPressed(EntityUid uid, NukeComponent component, NukeKeypadClearMessage args)
+ {
+ _audio.Play(component.KeypadPressSound, Filter.Pvs(uid), uid, true);
+
+ if (component.Status != NukeStatus.AWAIT_CODE)
+ return;
+
+ component.EnteredCode = "";
+ UpdateUserInterface(uid, component);
+ }
+
+ private void OnArmButtonPressed(EntityUid uid, NukeComponent component, NukeArmedMessage args)
+ {
+ if (!component.DiskSlot.HasItem)
+ return;
+
+ if (component.Status == NukeStatus.AWAIT_ARM && Transform(uid).Anchored)
+ ArmBomb(uid, component);
+
+ else
+ {
+ if (args.Session.AttachedEntity is not { } user)
+ return;
+
+ DisarmBombDoafter(uid, user, component);
+ }
+ }
+
+ #endregion
+
+ #region Doafter Events
+
+ private void OnDoAfter(EntityUid uid, NukeComponent component, DoAfterEvent args)
+ {
+ if (args.Handled || args.Cancelled)
+ return;
+
+ DisarmBomb(uid, component);
+
+ var ev = new NukeDisarmSuccessEvent();
+ RaiseLocalEvent(ev);
+
+ args.Handled = true;
+ }
+ #endregion
+
+ private void TickCooldown(EntityUid uid, float frameTime, NukeComponent? nuke = null)
+ {
+ if (!Resolve(uid, ref nuke))
+ return;
+
+ nuke.CooldownTime -= frameTime;
+ if (nuke.CooldownTime <= 0)
+ {
+ // reset nuke to default state
+ nuke.CooldownTime = 0;
+ nuke.Status = NukeStatus.AWAIT_ARM;
+ UpdateStatus(uid, nuke);
+ }
+
+ UpdateUserInterface(uid, nuke);
+ }
+
+ private void TickTimer(EntityUid uid, float frameTime, NukeComponent? nuke = null)
+ {
+ if (!Resolve(uid, ref nuke))
+ return;
+
+ nuke.RemainingTime -= frameTime;
+
+ // Start playing the nuke event song so that it ends a couple seconds before the alert sound
+ // should play
+ if (nuke.RemainingTime <= NukeSongLength + nuke.AlertSoundTime + NukeSongBuffer && !nuke.PlayedNukeSong)
+ {
+ _sound.DispatchStationEventMusic(uid, nuke.ArmMusic, StationEventMusicType.Nuke);
+ nuke.PlayedNukeSong = true;
+ }
+
+ // play alert sound if time is running out
+ if (nuke.RemainingTime <= nuke.AlertSoundTime && !nuke.PlayedAlertSound)
+ {
+ nuke.AlertAudioStream = _audio.Play(nuke.AlertSound, Filter.Broadcast(), uid, true);
+ _sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
+ nuke.PlayedAlertSound = true;
+ }
+
+ if (nuke.RemainingTime <= 0)
+ {
+ nuke.RemainingTime = 0;
+ ActivateBomb(uid, nuke);
+ }
+
+ else
+ UpdateUserInterface(uid, nuke);
+ }
+
+ private void UpdateStatus(EntityUid uid, NukeComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ switch (component.Status)
+ {
+ case NukeStatus.AWAIT_DISK:
+ if (component.DiskSlot.HasItem)
+ component.Status = NukeStatus.AWAIT_CODE;
+ break;
+ case NukeStatus.AWAIT_CODE:
+ if (!component.DiskSlot.HasItem)
+ {
+ component.Status = NukeStatus.AWAIT_DISK;
+ component.EnteredCode = "";
+ break;
+ }
+
+ // var isValid = _codes.IsCodeValid(uid, component.EnteredCode);
+ if (component.EnteredCode == component.Code)
+ {
+ component.Status = NukeStatus.AWAIT_ARM;
+ component.RemainingTime = component.Timer;
+ _audio.Play(component.AccessGrantedSound, Filter.Pvs(uid), uid, true);
+ }
+ else
+ {
+ component.EnteredCode = "";
+ _audio.Play(component.AccessDeniedSound, Filter.Pvs(uid), uid, true);
+ }
+
+ break;
+ case NukeStatus.AWAIT_ARM:
+ // do nothing, wait for arm button to be pressed
+ break;
+ case NukeStatus.ARMED:
+ // do nothing, wait for arm button to be unpressed
+ break;
+ }
+ }
+
+ private void UpdateUserInterface(EntityUid uid, NukeComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ var ui = _ui.GetUiOrNull(uid, NukeUiKey.Key);
+ if (ui == null)
+ return;
+
+ var anchored = Transform(uid).Anchored;
+
+ var allowArm = component.DiskSlot.HasItem &&
+ (component.Status == NukeStatus.AWAIT_ARM ||
+ component.Status == NukeStatus.ARMED);
+
+ var state = new NukeUiState
+ {
+ Status = component.Status,
+ RemainingTime = (int) component.RemainingTime,
+ DiskInserted = component.DiskSlot.HasItem,
+ IsAnchored = anchored,
+ AllowArm = allowArm,
+ EnteredCodeLength = component.EnteredCode.Length,
+ MaxCodeLength = component.CodeLength,
+ CooldownTime = (int) component.CooldownTime
+ };
+
+ UserInterfaceSystem.SetUiState(ui, state);
+ }
+
+ private void PlayNukeKeypadSound(EntityUid uid, int number, NukeComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ // This is a C mixolydian blues scale.
+ // 1 2 3 C D Eb
+ // 4 5 6 E F F#
+ // 7 8 9 G A Bb
+ var semitoneShift = number switch
+ {
+ 1 => 0,
+ 2 => 2,
+ 3 => 3,
+ 4 => 4,
+ 5 => 5,
+ 6 => 6,
+ 7 => 7,
+ 8 => 9,
+ 9 => 10,
+ 0 => component.LastPlayedKeypadSemitones + 12,
+ _ => 0
+ };
+
+ // Don't double-dip on the octave shifting
+ component.LastPlayedKeypadSemitones = number == 0 ? component.LastPlayedKeypadSemitones : semitoneShift;
+
+ _audio.Play(component.KeypadPressSound, Filter.Pvs(uid), uid, true, AudioHelpers.ShiftSemitone(semitoneShift).WithVolume(-5f));
+ }
+
+ public string GenerateRandomNumberString(int length)
+ {
+ var ret = "";
+ for (var i = 0; i < length; i++)
+ {
+ var c = (char) _random.Next('0', '9' + 1);
+ ret += c;
+ }
+
+ return ret;
+ }
+
+ #region Public API
+
+ ///
+ /// Force a nuclear bomb to start a countdown timer
+ ///
+ public void ArmBomb(EntityUid uid, NukeComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ if (component.Status == NukeStatus.ARMED)
+ return;
+
+ var nukeXform = Transform(uid);
+ var stationUid = _station.GetStationInMap(nukeXform.MapID);
+ // 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, true);
+
+ var pos = nukeXform.MapPosition;
+ var x = (int) pos.X;
+ var y = (int) pos.Y;
+ var posText = $"({x}, {y})";
+
+ // warn a crew
+ var announcement = Loc.GetString("nuke-component-announcement-armed",
+ ("time", (int) component.RemainingTime), ("position", posText));
+ var sender = Loc.GetString("nuke-component-announcement-sender");
+ _chatSystem.DispatchStationAnnouncement(stationUid ?? uid, announcement, sender, false, null, Color.Red);
+
+ _sound.PlayGlobalOnStation(uid, _audio.GetSound(component.ArmSound));
+
+ _itemSlots.SetLock(uid, component.DiskSlot, true);
+ _transform.AnchorEntity(uid, nukeXform);
+ component.Status = NukeStatus.ARMED;
+ UpdateUserInterface(uid, component);
}
///
- /// Raised directed on the nuke when its disarm doafter is successful.
- /// So the game knows not to end.
+ /// Stop nuclear bomb timer
///
- public sealed class NukeDisarmSuccessEvent : EntityEventArgs
+ public void DisarmBomb(EntityUid uid, NukeComponent? component = null)
{
+ if (!Resolve(uid, ref component))
+ return;
+ if (component.Status != NukeStatus.ARMED)
+ return;
+
+ var stationUid = _station.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");
+ _chatSystem.DispatchStationAnnouncement(uid, announcement, sender, false);
+
+ component.PlayedNukeSong = false;
+ _sound.PlayGlobalOnStation(uid, _audio.GetSound(component.DisarmSound));
+ _sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
+
+ // disable sound and reset it
+ component.PlayedAlertSound = false;
+ component.AlertAudioStream?.Stop();
+
+ // start bomb cooldown
+ _itemSlots.SetLock(uid, component.DiskSlot, false);
+ component.Status = NukeStatus.COOLDOWN;
+ component.CooldownTime = component.Cooldown;
+
+ UpdateUserInterface(uid, component);
+ }
+
+ ///
+ /// Toggle bomb arm button
+ ///
+ public void ToggleBomb(EntityUid uid, NukeComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ if (component.Status == NukeStatus.ARMED)
+ DisarmBomb(uid, component);
+ else
+ ArmBomb(uid, component);
+ }
+
+ ///
+ /// Force bomb to explode immediately
+ ///
+ public void ActivateBomb(EntityUid uid, NukeComponent? component = null,
+ TransformComponent? transform = null)
+ {
+ if (!Resolve(uid, ref component, ref transform))
+ return;
+
+ if (component.Exploded)
+ return;
+
+ component.Exploded = true;
+
+ _explosions.QueueExplosion(uid,
+ component.ExplosionType,
+ component.TotalIntensity,
+ component.IntensitySlope,
+ component.MaxIntensity);
+
+ RaiseLocalEvent(new NukeExplodedEvent()
+ {
+ OwningStation = transform.GridUid,
+ });
+
+ _sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
+ Del(uid);
+ }
+
+ ///
+ /// Set remaining time value
+ ///
+ public void SetRemainingTime(EntityUid uid, float timer, NukeComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ component.RemainingTime = timer;
+ UpdateUserInterface(uid, component);
+ }
+
+ #endregion
+
+ private void DisarmBombDoafter(EntityUid uid, EntityUid user, NukeComponent nuke)
+ {
+ var doAfter = new DoAfterArgs(user, nuke.DisarmDoafterLength, new NukeDisarmDoAfterEvent(), uid, target: uid)
+ {
+ BreakOnDamage = true,
+ BreakOnTargetMove = true,
+ BreakOnUserMove = true,
+ NeedHand = true
+ };
+
+ if (!_doAfter.TryStartDoAfter(doAfter))
+ return;
+
+ _popups.PopupEntity(Loc.GetString("nuke-component-doafter-warning"), user,
+ user, PopupType.LargeCaution);
}
}
+
+public sealed class NukeExplodedEvent : EntityEventArgs
+{
+ public EntityUid? OwningStation;
+}
+
+///
+/// Raised directed on the nuke when its disarm doafter is successful.
+/// So the game knows not to end.
+///
+public sealed class NukeDisarmSuccessEvent : EntityEventArgs
+{
+
+}
+
diff --git a/Resources/Prototypes/Entities/Objects/Devices/nuke.yml b/Resources/Prototypes/Entities/Objects/Devices/nuke.yml
index c77f826001..f5a23c01e7 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/nuke.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/nuke.yml
@@ -61,8 +61,10 @@
id: NuclearBombUnanchored
suffix: unanchored
components:
- - type: Transform
- anchored: false
+ - type: Transform
+ anchored: false
+ - type: Physics
+ bodyType: Dynamic
- type: entity
parent: StorageTank