using Content.Server.Chat.Managers; using Content.Server.Coordinates.Helpers; using Content.Server.Explosion.EntitySystems; using Content.Server.Popups; using Content.Server.UserInterface; using Content.Shared.Audio; using Content.Shared.Body.Components; using Content.Shared.Construction.Components; using Content.Shared.Containers.ItemSlots; using Content.Shared.Nuke; using Content.Shared.Sound; using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.Player; namespace Content.Server.Nuke { public sealed class NukeSystem : EntitySystem { [Dependency] private readonly NukeCodeSystem _codes = default!; [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; [Dependency] private readonly PopupSystem _popups = default!; [Dependency] private readonly ExplosionSystem _explosions = default!; [Dependency] private readonly IChatManager _chat = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnRemove); 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); } 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 = EntityQuery(); foreach (var nuke in query) { switch (nuke.Status) { case NukeStatus.ARMED: TickTimer(nuke.Owner, frameTime, nuke); break; case NukeStatus.COOLDOWN: TickCooldown(nuke.Owner, frameTime, nuke); break; } } } 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 without nuke disk if (!component.DiskSlot.HasItem) { var msg = Loc.GetString("nuke-component-cant-anchor"); _popups.PopupEntity(msg, uid, Filter.Entities(args.User)); args.Cancel(); } } private void OnAnchorChanged(EntityUid uid, NukeComponent component, ref AnchorStateChangedEvent args) { UpdateUserInterface(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 transform.Coordinates = transform.Coordinates.SnapToGrid(); transform.Anchored = !transform.Anchored; 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) { PlaySound(uid, component.KeypadPressSound, 0.125f, component); if (component.Status != NukeStatus.AWAIT_CODE) return; if (component.EnteredCode.Length >= _codes.Code.Length) return; component.EnteredCode += args.Value.ToString(); UpdateUserInterface(uid, component); } private void OnClearButtonPressed(EntityUid uid, NukeComponent component, NukeKeypadClearMessage args) { PlaySound(uid, component.KeypadPressSound, 0f, component); 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) { ArmBomb(uid, component); } else { DisarmBomb(uid, component); } } #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(nuke.Owner, nuke); } private void TickTimer(EntityUid uid, float frameTime, NukeComponent? nuke = null) { if (!Resolve(uid, ref nuke)) return; nuke.RemainingTime -= frameTime; // play alert sound if time is running out if (nuke.RemainingTime <= nuke.AlertSoundTime && !nuke.PlayedAlertSound) { nuke.AlertAudioStream = SoundSystem.Play(Filter.Broadcast(), nuke.AlertSound.GetSound()); 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(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; 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 = component.Owner.GetUIOrNull(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 = _codes.Code.Length, CooldownTime = (int) component.CooldownTime }; ui.SetState(state); } private void PlaySound(EntityUid uid, SoundSpecifier sound, float varyPitch = 0f, NukeComponent? component = null) { if (!Resolve(uid, ref component)) return; SoundSystem.Play(Filter.Pvs(uid), sound.GetSound(), uid, AudioHelpers.WithVariation(varyPitch).WithVolume(-5f)); } #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; // warn a crew var announcement = Loc.GetString("nuke-component-announcement-armed", ("time", (int) component.RemainingTime)); var sender = Loc.GetString("nuke-component-announcement-sender"); _chat.DispatchStationAnnouncement(announcement, sender, false, Color.Red); // todo: move it to announcements system SoundSystem.Play(Filter.Broadcast(), component.ArmSound.GetSound()); 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; // warn a crew var announcement = Loc.GetString("nuke-component-announcement-unarmed"); var sender = Loc.GetString("nuke-component-announcement-sender"); _chat.DispatchStationAnnouncement(announcement, sender, false); // todo: move it to announcements system SoundSystem.Play(Filter.Broadcast(), component.DisarmSound.GetSound()); // disable sound and reset it component.PlayedAlertSound = false; component.AlertAudioStream?.Stop(); // start bomb cooldown 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); 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 } }