diff --git a/Content.Client/Nuke/NukeMenu.xaml.cs b/Content.Client/Nuke/NukeMenu.xaml.cs index 092b869074..a341a69e0d 100644 --- a/Content.Client/Nuke/NukeMenu.xaml.cs +++ b/Content.Client/Nuke/NukeMenu.xaml.cs @@ -103,7 +103,7 @@ namespace Content.Client.Nuke FirstStatusLabel.Text = firstMsg; SecondStatusLabel.Text = secondMsg; - EjectButton.Disabled = !state.DiskInserted; + EjectButton.Disabled = !state.DiskInserted || state.Status == NukeStatus.ARMED; AnchorButton.Disabled = !state.DiskInserted; AnchorButton.Pressed = state.IsAnchored; ArmButton.Disabled = !state.AllowArm; diff --git a/Content.Server/Nuke/NukeComponent.cs b/Content.Server/Nuke/NukeComponent.cs index 1741e83bf9..1f9d3b99f6 100644 --- a/Content.Server/Nuke/NukeComponent.cs +++ b/Content.Server/Nuke/NukeComponent.cs @@ -1,3 +1,4 @@ +using System.Threading; using Content.Shared.Containers.ItemSlots; using Content.Shared.Explosion; using Content.Shared.Nuke; @@ -18,11 +19,10 @@ namespace Content.Server.Nuke { /// /// Default bomb timer value in seconds. - /// Must be shorter then the nuke alarm song. /// [DataField("timer")] [ViewVariables(VVAccess.ReadWrite)] - public int Timer = 120; + public int Timer = 300; /// /// How long until the bomb can arm again after deactivation. @@ -40,11 +40,17 @@ namespace Content.Server.Nuke public ItemSlot DiskSlot = new(); /// - /// After this time nuke will play last alert sound + /// When this time is left, nuke will play last alert sound /// [DataField("alertTime")] public float AlertSoundTime = 10.0f; + /// + /// How long a user must wait to disarm the bomb. + /// + [DataField("disarmDoafterLength")] + public float DisarmDoafterLength = 30.0f; + [DataField("alertLevelOnActivate")] public string AlertLevelOnActivate = default!; [DataField("alertLevelOnDeactivate")] public string AlertLevelOnDeactivate = default!; @@ -136,11 +142,18 @@ namespace Content.Server.Nuke [ViewVariables] public NukeStatus Status = NukeStatus.AWAIT_DISK; + /// + /// Check if nuke has already played the nuke song so we don't do it again + /// + public bool PlayedNukeSong = false; + /// /// Check if nuke has already played last alert sound /// public bool PlayedAlertSound = false; + public CancellationToken? DisarmCancelToken = null; + public IPlayingAudioStream? AlertAudioStream = default; } } diff --git a/Content.Server/Nuke/NukeSystem.cs b/Content.Server/Nuke/NukeSystem.cs index 708b5259ca..a59a5bccc6 100644 --- a/Content.Server/Nuke/NukeSystem.cs +++ b/Content.Server/Nuke/NukeSystem.cs @@ -4,6 +4,7 @@ using Content.Server.Chat; using Content.Server.Chat.Managers; using Content.Server.Chat.Systems; using Content.Server.Coordinates.Helpers; +using Content.Server.DoAfter; using Content.Server.Explosion.EntitySystems; using Content.Server.Popups; using Content.Server.Station.Systems; @@ -30,6 +31,17 @@ namespace Content.Server.Nuke [Dependency] private readonly StationSystem _stationSystem = default!; [Dependency] private readonly ServerGlobalSoundSystem _soundSystem = default!; [Dependency] private readonly ChatSystem _chatSystem = default!; + [Dependency] private readonly DoAfterSystem _doAfterSystem = 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() { @@ -51,6 +63,10 @@ namespace Content.Server.Nuke SubscribeLocalEvent(OnKeypadButtonPressed); SubscribeLocalEvent(OnClearButtonPressed); SubscribeLocalEvent(OnEnterButtonPressed); + + // Doafter events + SubscribeLocalEvent(OnDisarmSuccess); + SubscribeLocalEvent(OnDisarmCancelled); } private void OnInit(EntityUid uid, NukeComponent component, ComponentInit args) @@ -98,6 +114,7 @@ namespace Content.Server.Nuke } #region Anchor + private void OnAnchorAttempt(EntityUid uid, NukeComponent component, AnchorAttemptEvent args) { CheckAnchorAttempt(uid, component, args); @@ -124,9 +141,11 @@ namespace Content.Server.Nuke { UpdateUserInterface(uid, component); } + #endregion #region UI Events + private async void OnAnchorButtonPressed(EntityUid uid, NukeComponent component, NukeAnchorMessage args) { if (!component.DiskSlot.HasItem) @@ -188,9 +207,28 @@ namespace Content.Server.Nuke } else { - DisarmBomb(uid, component); + if (args.Session.AttachedEntity is not { } user) + return; + + DisarmBombDoafter(uid, user, component); } } + + #endregion + + #region Doafter Events + + private void OnDisarmSuccess(EntityUid uid, NukeComponent component, NukeDisarmSuccessEvent args) + { + component.DisarmCancelToken = null; + DisarmBomb(uid, component); + } + + private void OnDisarmCancelled(EntityUid uid, NukeComponent component, NukeDisarmCancelledEvent args) + { + component.DisarmCancelToken = null; + } + #endregion private void TickCooldown(EntityUid uid, float frameTime, NukeComponent? nuke = null) @@ -217,6 +255,14 @@ namespace Content.Server.Nuke 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) { @@ -248,28 +294,29 @@ namespace Content.Server.Nuke component.Status = NukeStatus.AWAIT_CODE; break; case NukeStatus.AWAIT_CODE: + { + if (!component.DiskSlot.HasItem) { - if (!component.DiskSlot.HasItem) - { - component.Status = NukeStatus.AWAIT_DISK; - component.EnteredCode = ""; - break; - } - - var isValid = _codes.IsCodeValid(component.EnteredCode); - if (isValid) - { - component.Status = NukeStatus.AWAIT_ARM; - component.RemainingTime = component.Timer; - PlaySound(uid, component.AccessGrantedSound, 0, component); - } - else - { - component.EnteredCode = ""; - PlaySound(uid, component.AccessDeniedSound, 0, component); - } + component.Status = NukeStatus.AWAIT_DISK; + component.EnteredCode = ""; break; } + + var isValid = _codes.IsCodeValid(component.EnteredCode); + if (isValid) + { + component.Status = NukeStatus.AWAIT_ARM; + component.RemainingTime = component.Timer; + PlaySound(uid, component.AccessGrantedSound, 0, component); + } + else + { + component.EnteredCode = ""; + PlaySound(uid, component.AccessDeniedSound, 0, component); + } + + break; + } case NukeStatus.AWAIT_ARM: // do nothing, wait for arm button to be pressed break; @@ -322,6 +369,7 @@ namespace Content.Server.Nuke } #region Public API + /// /// Force a nuclear bomb to start a countdown timer /// @@ -350,6 +398,7 @@ namespace Content.Server.Nuke NukeArmedAudio(component); + _itemSlots.SetLock(uid, component.DiskSlot, true); component.Status = NukeStatus.ARMED; UpdateUserInterface(uid, component); } @@ -376,6 +425,7 @@ namespace Content.Server.Nuke var sender = Loc.GetString("nuke-component-announcement-sender"); _chatSystem.DispatchStationAnnouncement(uid, announcement, sender, false); + component.PlayedNukeSong = false; NukeDisarmedAudio(component); // disable sound and reset it @@ -383,6 +433,7 @@ namespace Content.Server.Nuke component.AlertAudioStream?.Stop(); // start bomb cooldown + _itemSlots.SetLock(uid, component.DiskSlot, false); component.Status = NukeStatus.COOLDOWN; component.CooldownTime = component.Cooldown; @@ -440,12 +491,30 @@ namespace Content.Server.Nuke component.RemainingTime = timer; UpdateUserInterface(uid, component); } + #endregion + private void DisarmBombDoafter(EntityUid uid, EntityUid user, NukeComponent nuke) + { + nuke.DisarmCancelToken = new(); + var doafter = new DoAfterEventArgs(user, nuke.DisarmDoafterLength, nuke.DisarmCancelToken.Value, uid) + { + TargetCancelledEvent = new NukeDisarmCancelledEvent(), + TargetFinishedEvent = new NukeDisarmSuccessEvent(), + BreakOnDamage = true, + BreakOnStun = true, + BreakOnTargetMove = true, + BreakOnUserMove = true, + NeedHand = true, + }; + + _doAfterSystem.DoAfter(doafter); + _popups.PopupEntity(Loc.GetString("nuke-component-doafter-warning"), user, Filter.Entities(user)); + } + private void NukeArmedAudio(NukeComponent component) { _soundSystem.PlayGlobalOnStation(component.Owner, component.ArmSound.GetSound()); - _soundSystem.DispatchStationEventMusic(component.Owner, component.ArmMusic, StationEventMusicType.Nuke); } private void NukeDisarmedAudio(NukeComponent component) @@ -456,4 +525,15 @@ namespace Content.Server.Nuke } public sealed class NukeExplodedEvent : EntityEventArgs {} + + /// + /// Raised directed on the nuke when its disarm doafter is successful. + /// + public sealed class NukeDisarmSuccessEvent : EntityEventArgs {} + + /// + /// Raised directed on the nuke when its disarm doafter is cancelled. + /// + public sealed class NukeDisarmCancelledEvent : EntityEventArgs {} + } diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 663799359d..68724b75d5 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -571,7 +571,7 @@ namespace Content.Shared.CCVar /// Actual area may be larger, as it currently doesn't terminate mid neighbor finding. I.e., area may be that of a ~51 tile radius circle instead. /// public static readonly CVarDef ExplosionMaxArea = - CVarDef.Create("explosion.max_area", (int) 3.14f * 50 * 50, CVar.SERVERONLY); + CVarDef.Create("explosion.max_area", (int) 3.14f * 256 * 256, CVar.SERVERONLY); /// /// Upper limit on the number of neighbor finding steps for the explosion system neighbor-finding algorithm. @@ -581,7 +581,7 @@ namespace Content.Shared.CCVar /// instances, will likely be hit before this becomes a limiting factor. /// public static readonly CVarDef ExplosionMaxIterations = - CVarDef.Create("explosion.max_iterations", 150, CVar.SERVERONLY); + CVarDef.Create("explosion.max_iterations", 500, CVar.SERVERONLY); /// /// Max Time in milliseconds to spend processing explosions every tick. diff --git a/Resources/Locale/en-US/nuke/nuke-component.ftl b/Resources/Locale/en-US/nuke/nuke-component.ftl index add3ede4f4..52fc260d08 100644 --- a/Resources/Locale/en-US/nuke/nuke-component.ftl +++ b/Resources/Locale/en-US/nuke/nuke-component.ftl @@ -3,6 +3,7 @@ nuke-component-announcement-sender = Nuclear Fission Explosive nuke-component-announcement-armed = Attention! The station's self-destruct mechanism has been engaged. {$time} seconds until detonation. nuke-component-announcement-unarmed = The station's self-destruct was deactivated! Have a nice day! nuke-component-announcement-send-codes = Attention! Requested self-destruction codes was sent to communication consoles. +nuke-component-doafter-warning = You start fiddling with wires and knobs in order to disarm the nuke.. This may take a while. # Nuke UI nuke-user-interface-title = Nuclear Fission Explosive diff --git a/Resources/Prototypes/Entities/Objects/Devices/nuke.yml b/Resources/Prototypes/Entities/Objects/Devices/nuke.yml index d9429140df..169bdc0c02 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/nuke.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/nuke.yml @@ -23,12 +23,10 @@ layer: - HalfWallLayer - type: Nuke - # ~50 tile radius in open space - # close to defaulkt max cap. explosionType: Default maxIntensity: 100 intensitySlope: 5 - totalIntensity: 500000 + totalIntensity: 5000000 diskSlot: name: Disk insertSound: