diff --git a/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs b/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs index 367114f2aa..fc7b234bcc 100644 --- a/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs +++ b/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs @@ -134,7 +134,7 @@ public sealed partial class RoboticsConsoleWindow : FancyWindow BorgInfo.SetMessage(text); // how the turntables - DisableButton.Disabled = !data.HasBrain; + DisableButton.Disabled = !(data.HasBrain && data.CanDisable); DestroyButton.Disabled = _timing.CurTime < _console.Comp1.NextDestroy; } diff --git a/Content.Server/Explosion/Components/AutomatedTimerComponent.cs b/Content.Server/Explosion/Components/AutomatedTimerComponent.cs index 7019c08d43..c01aeb91e5 100644 --- a/Content.Server/Explosion/Components/AutomatedTimerComponent.cs +++ b/Content.Server/Explosion/Components/AutomatedTimerComponent.cs @@ -1,7 +1,7 @@ namespace Content.Server.Explosion.Components; /// -/// Disallows starting the timer by hand, must be stuck or triggered by a system. +/// Disallows starting the timer by hand, must be stuck or triggered by a system using StartTimer. /// [RegisterComponent] public sealed partial class AutomatedTimerComponent : Component diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs index 8725dd1ae7..dcd11062bb 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs @@ -26,13 +26,7 @@ public sealed partial class TriggerSystem if (!component.StartOnStick) return; - HandleTimerTrigger( - uid, - args.User, - component.Delay, - component.BeepInterval, - component.InitialBeepDelay, - component.BeepSound); + StartTimer((uid, component), args.User); } private void OnExamined(EntityUid uid, OnUseTimerTriggerComponent component, ExaminedEvent args) @@ -54,14 +48,7 @@ public sealed partial class TriggerSystem args.Verbs.Add(new AlternativeVerb() { Text = Loc.GetString("verb-start-detonation"), - Act = () => HandleTimerTrigger( - uid, - args.User, - component.Delay, - component.BeepInterval, - component.InitialBeepDelay, - component.BeepSound - ), + Act = () => StartTimer((uid, component), args.User), Priority = 2 }); } @@ -174,13 +161,7 @@ public sealed partial class TriggerSystem if (component.DoPopup) _popupSystem.PopupEntity(Loc.GetString("trigger-activated", ("device", uid)), args.User, args.User); - HandleTimerTrigger( - uid, - args.User, - component.Delay, - component.BeepInterval, - component.InitialBeepDelay, - component.BeepSound); + StartTimer((uid, component), args.User); args.Handled = true; } diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs index e03b8aff54..92e065bf4c 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs @@ -265,6 +265,18 @@ namespace Content.Server.Explosion.EntitySystems comp.TimeRemaining += amount; } + /// + /// Start the timer for triggering the device. + /// + public void StartTimer(Entity ent, EntityUid? user) + { + if (!Resolve(ent, ref ent.Comp, false)) + return; + + var comp = ent.Comp; + HandleTimerTrigger(ent, user, comp.Delay, comp.BeepInterval, comp.InitialBeepDelay, comp.BeepSound); + } + public void HandleTimerTrigger(EntityUid uid, EntityUid? user, float delay, float beepInterval, float? initialBeepDelay, SoundSpecifier? beepSound) { if (delay <= 0) diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs b/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs index 1c10cbe667..781f847be3 100644 --- a/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs +++ b/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs @@ -1,5 +1,6 @@ using Content.Shared.DeviceNetwork; using Content.Shared.Emag.Components; +using Content.Shared.Movement.Components; using Content.Shared.Popups; using Content.Shared.Robotics; using Content.Shared.Silicons.Borgs.Components; @@ -26,6 +27,9 @@ public sealed partial class BorgSystem var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var comp, out var chassis, out var device, out var meta)) { + if (comp.NextDisable is {} nextDisable && now >= nextDisable) + DoDisable((uid, comp, chassis, meta)); + if (now < comp.NextBroadcast) continue; @@ -33,13 +37,16 @@ public sealed partial class BorgSystem if (_powerCell.TryGetBatteryFromSlot(uid, out var battery)) charge = battery.CurrentCharge / battery.MaxCharge; + var hasBrain = chassis.BrainEntity != null && !comp.FakeDisabled; + var canDisable = comp.NextDisable == null && !comp.FakeDisabling; var data = new CyborgControlData( comp.Sprite, comp.Name, meta.EntityName, charge, chassis.ModuleCount, - chassis.BrainEntity != null); + hasBrain, + canDisable); var payload = new NetworkPayload() { @@ -52,6 +59,24 @@ public sealed partial class BorgSystem } } + private void DoDisable(Entity ent) + { + ent.Comp1.NextDisable = null; + if (ent.Comp1.FakeDisabling) + { + ent.Comp1.FakeDisabled = true; + ent.Comp1.FakeDisabling = false; + return; + } + + if (ent.Comp2.BrainEntity is not {} brain) + return; + + var message = Loc.GetString(ent.Comp1.DisabledPopup, ("name", Name(ent, ent.Comp3))); + Popup.PopupEntity(message, ent); + _container.Remove(brain, ent.Comp2.BrainContainer); + } + private void OnPacketReceived(Entity ent, ref DeviceNetworkPacketEvent args) { var payload = args.Data; @@ -61,28 +86,28 @@ public sealed partial class BorgSystem if (command == RoboticsConsoleConstants.NET_DISABLE_COMMAND) Disable(ent); else if (command == RoboticsConsoleConstants.NET_DESTROY_COMMAND) - Destroy(ent.Owner); + Destroy(ent); } private void Disable(Entity ent) { - if (!Resolve(ent, ref ent.Comp2) || ent.Comp2.BrainEntity is not {} brain) + if (!Resolve(ent, ref ent.Comp2) || ent.Comp2.BrainEntity == null || ent.Comp1.NextDisable != null) return; - // this won't exactly be stealthy but if you are malf its better than actually disabling you + // update ui immediately + ent.Comp1.NextBroadcast = _timing.CurTime; + + // pretend the borg is being disabled forever now if (CheckEmagged(ent, "disabled")) - return; + ent.Comp1.FakeDisabling = true; + else + Popup.PopupEntity(Loc.GetString(ent.Comp1.DisablingPopup), ent); - var message = Loc.GetString(ent.Comp1.DisabledPopup, ("name", Name(ent))); - Popup.PopupEntity(message, ent); - _container.Remove(brain, ent.Comp2.BrainContainer); + ent.Comp1.NextDisable = _timing.CurTime + ent.Comp1.DisableDelay; } - private void Destroy(Entity ent) + private void Destroy(Entity ent) { - if (!Resolve(ent, ref ent.Comp)) - return; - // this is stealthy until someone realises you havent exploded if (CheckEmagged(ent, "destroyed")) { @@ -91,7 +116,12 @@ public sealed partial class BorgSystem return; } - _explosion.TriggerExplosive(ent, ent.Comp, delete: false); + var message = Loc.GetString(ent.Comp.DestroyingPopup, ("name", Name(ent))); + Popup.PopupEntity(message, ent); + _trigger.StartTimer(ent.Owner, user: null); + + // prevent a shitter borg running into people + RemComp(ent); } private bool CheckEmagged(EntityUid uid, string name) diff --git a/Content.Server/Silicons/Borgs/BorgSystem.cs b/Content.Server/Silicons/Borgs/BorgSystem.cs index 1ab7f5387f..c97ca9cbc0 100644 --- a/Content.Server/Silicons/Borgs/BorgSystem.cs +++ b/Content.Server/Silicons/Borgs/BorgSystem.cs @@ -43,8 +43,8 @@ public sealed partial class BorgSystem : SharedBorgSystem [Dependency] private readonly ActionsSystem _actions = default!; [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!; - [Dependency] private readonly ExplosionSystem _explosion = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly TriggerSystem _trigger = default!; [Dependency] private readonly HandsSystem _hands = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly SharedMindSystem _mind = default!; diff --git a/Content.Shared/Robotics/Components/RoboticsConsoleComponent.cs b/Content.Shared/Robotics/Components/RoboticsConsoleComponent.cs index 4329e437a2..9e4b51866f 100644 --- a/Content.Shared/Robotics/Components/RoboticsConsoleComponent.cs +++ b/Content.Shared/Robotics/Components/RoboticsConsoleComponent.cs @@ -36,7 +36,7 @@ public sealed partial class RoboticsConsoleComponent : Component /// Radio message sent when destroying a borg. /// [DataField] - public LocId DestroyMessage = "robotics-console-cyborg-destroyed"; + public LocId DestroyMessage = "robotics-console-cyborg-destroying"; /// /// Cooldown on destroying borgs to prevent complete abuse. diff --git a/Content.Shared/Robotics/RoboticsConsoleUi.cs b/Content.Shared/Robotics/RoboticsConsoleUi.cs index 1be89beff0..996c65cb0e 100644 --- a/Content.Shared/Robotics/RoboticsConsoleUi.cs +++ b/Content.Shared/Robotics/RoboticsConsoleUi.cs @@ -97,6 +97,13 @@ public record struct CyborgControlData [DataField] public bool HasBrain; + /// + /// Whether the borg can currently be disabled if the brain is installed, + /// if on cooldown then can't queue up multiple disables. + /// + [DataField] + public bool CanDisable; + /// /// When this cyborg's data will be deleted. /// Set by the console when receiving the packet. @@ -104,7 +111,7 @@ public record struct CyborgControlData [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] public TimeSpan Timeout = TimeSpan.Zero; - public CyborgControlData(SpriteSpecifier? chassisSprite, string chassisName, string name, float charge, int moduleCount, bool hasBrain) + public CyborgControlData(SpriteSpecifier? chassisSprite, string chassisName, string name, float charge, int moduleCount, bool hasBrain, bool canDisable) { ChassisSprite = chassisSprite; ChassisName = chassisName; @@ -112,6 +119,7 @@ public record struct CyborgControlData Charge = charge; ModuleCount = moduleCount; HasBrain = hasBrain; + CanDisable = canDisable; } } diff --git a/Content.Shared/Silicons/Borgs/Components/BorgTransponderComponent.cs b/Content.Shared/Silicons/Borgs/Components/BorgTransponderComponent.cs index 8c15e20d5d..577056bb46 100644 --- a/Content.Shared/Silicons/Borgs/Components/BorgTransponderComponent.cs +++ b/Content.Shared/Silicons/Borgs/Components/BorgTransponderComponent.cs @@ -23,12 +23,25 @@ public sealed partial class BorgTransponderComponent : Component public string Name = string.Empty; /// - /// Popup shown to everyone when a borg is disabled. + /// Popup shown to everyone after a borg is disabled. /// Gets passed a string "name". /// [DataField] public LocId DisabledPopup = "borg-transponder-disabled-popup"; + /// + /// Popup shown to the borg when it is being disabled. + /// + [DataField] + public LocId DisablingPopup = "borg-transponder-disabling-popup"; + + /// + /// Popup shown to everyone when a borg is being destroyed. + /// Gets passed a string "name". + /// + [DataField] + public LocId DestroyingPopup = "borg-transponder-destroying-popup"; + /// /// How long to wait between each broadcast. /// @@ -40,4 +53,28 @@ public sealed partial class BorgTransponderComponent : Component /// [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] public TimeSpan NextBroadcast = TimeSpan.Zero; + + /// + /// When to next disable the borg. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan? NextDisable; + + /// + /// How long to wait to disable the borg after RD has ordered it. + /// + [DataField] + public TimeSpan DisableDelay = TimeSpan.FromSeconds(5); + + /// + /// Pretend that the borg cannot be disabled due to being on delay. + /// + [DataField] + public bool FakeDisabling; + + /// + /// Pretend that the borg has no brain inserted. + /// + [DataField] + public bool FakeDisabled; } diff --git a/Resources/Locale/en-US/borg/borg.ftl b/Resources/Locale/en-US/borg/borg.ftl index 72667f318e..6c495510b0 100644 --- a/Resources/Locale/en-US/borg/borg.ftl +++ b/Resources/Locale/en-US/borg/borg.ftl @@ -21,5 +21,7 @@ borg-ui-module-counter = {$actual}/{$max} # Transponder borg-transponder-disabled-popup = A brain shoots out the top of {$name}! +borg-transponder-disabling-popup = Your transponder begins to lock you out of the chassis! +borg-transponder-destroying-popup = The self destruct of {$name} starts beeping! borg-transponder-emagged-disabled-popup = Your transponder's lights go out! borg-transponder-emagged-destroyed-popup = Your transponder's fuse blows! diff --git a/Resources/Locale/en-US/research/components/robotics-console.ftl b/Resources/Locale/en-US/research/components/robotics-console.ftl index 978fa9a43c..a4c82bd032 100644 --- a/Resources/Locale/en-US/research/components/robotics-console.ftl +++ b/Resources/Locale/en-US/research/components/robotics-console.ftl @@ -16,4 +16,4 @@ robotics-console-locked-message = Controls locked, swipe ID. robotics-console-disable = Disable robotics-console-destroy = Destroy -robotics-console-cyborg-destroyed = The cyborg {$name} has been remotely destroyed. +robotics-console-cyborg-destroying = {$name} is being remotely detonated! diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml index baea453cb8..6ff9a9ec90 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml @@ -227,9 +227,20 @@ deviceNetId: Wireless receiveFrequencyId: CyborgControl transmitFrequencyId: RoboticsConsole + - type: OnUseTimerTrigger + delay: 10 + examinable: false + beepSound: + path: /Audio/Effects/Cargo/buzz_two.ogg + params: + volume: -4 + # prevent any funnies if someone makes a cyborg item... + - type: AutomatedTimer + - type: ExplodeOnTrigger # explosion does most of its damage in the center and less at the edges - type: Explosive explosionType: Minibomb + deleteAfterExplosion: false # let damage threshold gib the borg totalIntensity: 30 intensitySlope: 20 maxIntensity: 20