Skip to content

Commit

Permalink
Cherry-Pick "EMP Grenade Actually Sabotages Power" From Wizden (#516)
Browse files Browse the repository at this point in the history
<!-- Please read these guidelines before opening your PR:
https://docs.spacestation14.io/en/getting-started/pr-guideline -->
<!-- The text between the arrows are comments - they will not be visible
on your PR. -->

## About the PR
<!-- What did you change in this PR? -->
EMP has been changed to target APC-powered-devices (most electrical
devices) as well as batteries to disable them. This means EMP can
interfere with autolathes, airlocks, atmos devices, substations and
SMES. The power draw of a single EMP grenade now cuts out a substation,
and the disabling effect prevents further recharge until it subsides.

EMP duration now also stacks, which creates a novel way to quietly black
out the station by attacking engineering SMES with 3 EMP grenades (6tc
EMP bundle) to black out the station for 3 minutes.

Edit, here's a detailed changelog of the PR,
Functionality:
- EMP disable has been generalised to kill and prevent further function
of every device/battery by interrupting recharge
- As a result of the above, some hard coded interactions have been
culled
- EMP disable duration now stacks with multiple EMP blasts
- EMP is now capable of draining from gravity generators
- The Charger system has been slightly reworked to facilitate
communication between batteries and chargers

Results:
- EMP grenade can disable basically every powered machine, most notably
doors
- EMP grenade has had its power drain upped to 2.7MW, which is slightly
more than a substation and 1/3 a SMES
- EMP grenade can now instantly kill substations
- EMP grenade can now instantly kill gravity generators
- 3 EMP grenades (6tc) can be used to kill SMES and disable recharge for
3 minutes with no evidence on the power monitor.

## Why / Balance
<!-- Why was it changed? Link any discussions or issues here. Please
discuss how this would affect game balance. -->
EMP at 2tc has a relatively low value-proposition when compared to C4
which is also 2tc. While EMP can probably black out one (or two if
you're lucky) APCs and can be used as a defensive option against
Stun/Lasers. C4 can be used to cut wires, substations, SMES, generators,
doors, reinforced walls, people and the list probably continues.

New EMP can be used to soft-bomb station power in an explosion that
isn't globally alarming (salv boom). Targeting the captain's office
directly may let you crowbar in and steal the locker but it leaves
ephemeral evidence in the form of everything electrical shimmering blue.
Opting to bomb substations blacks out a wider area, providing several
degrees of separation from your target. That is to say, new EMP grenade
favours map knowledge and rewards better stealth.

## Technical details
<!-- If this is a code change, summarize at high level how your new code
works. This makes it easier to review. -->
- `C.S/.../EmpSystem.cs` uses TryComp to turn on/off charging for
`C.S/Power/Components/PowerNetworkBatteryComponent`
- `C.S/Power/EntitySystems/PowerReceiverSystem.cs` listens to
`EmpPulseEvent` to turn off. Requests to turn back on are additionally
intercepted by `EmpSystem.cs` and cancelled.
- `C.S/.../GravityGeneratorSystem.cs` listens to `EmpPulseEvent` and
converts energy consumption to a normalised charge
- `C.S/Power/EntitySystems/ApcSystem.cs` no longer toggles its breaker,
but still listens to `EmpPulseEvent` for updating visuals.
- `C.S/Power/EntitySystems/ChargerSystem.cs` was refactored to add a
`ChargingComponent` flag to power cells instead of `ActiveCharger` on
itself. Battery and Charger communicate through this flag. Listens to
`EmpPulseEvent` for updating its state machine. New
`ChargerUpdateStatusEvent` allows batteries to update the charger's
status.
- `C.S/Power/EntitySystems/BatterySystem.cs` can now be disabled, and
checks for disabling before updating its charge. Raises
`ChargerUpdateStatusEvent` when hearing `EmpDisabledRemoved` to tell its
charger to start charging again.
- `C.S/Power/PowerWireAction.cs` checks for `EmpDisabledComponent`
before turning power back on.
- `C.S/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs` and
`C.S/VendingMachines/VendingMachineSystem.cs` had redundant
`EmpPulseEvent` listeners culled.
- `Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml`
buffed EMP grenade.

## Media
<!-- 
PRs which make ingame changes (adding clothing, items, new features,
etc) are required to have media attached that showcase the changes.
Small fixes/refactors are exempt.
Any media may be used in SS14 progress reports, with clear credit given.

If you're unsure whether your PR will require media, ask a maintainer.

Check the box below to confirm that you have in fact seen this (put an X
in the brackets, like [X]):
-->
https://www.youtube.com/embed/rSVph6OIg1s?si=8o4bx9Vx16B6usuu - outdated
video demonstrating changes on a wizden map
https://www.youtube.com/embed/B3iPhLcfs-0?si=trB1HY2ccjMf96Bj -
electrical anomaly crit with updated emp

- [x] I have added screenshots/videos to this PR showcasing its changes
ingame, **or** this PR does not require an ingame showcase

**Changelog**
<!--
Make players aware of new features and changes that could affect how
they play the game by adding a Changelog entry. Please read the
Changelog guidelines located at:
https://docs.spacestation14.io/en/getting-started/pr-guideline#changelog
-->

:cl:
- tweak: EMP Grenades can now disable basically any electrical device,
and stack in disable duration.

---------

Signed-off-by: VMSolidus <[email protected]>
Co-authored-by: VMSolidus <[email protected]>
  • Loading branch information
WarMechanic and VMSolidus authored Aug 6, 2024
1 parent 8400f6c commit 93ece39
Show file tree
Hide file tree
Showing 15 changed files with 273 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ private bool IsHeater(GasThermoMachineComponent comp)

private void OnToggleMessage(EntityUid uid, GasThermoMachineComponent thermoMachine, GasThermomachineToggleMessage args)
{
var powerState = _power.TogglePower(uid);
var powerState = _power.TryTogglePower(uid);
_adminLogger.Add(LogType.AtmosPowerChanged, $"{ToPrettyString(args.Session.AttachedEntity)} turned {(powerState ? "On" : "Off")} {ToPrettyString(uid)}");
DirtyUI(uid, thermoMachine);
}
Expand Down
2 changes: 1 addition & 1 deletion Content.Server/Atmos/Portable/SpaceHeaterSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ private void OnToggle(EntityUid uid, SpaceHeaterComponent spaceHeater, SpaceHeat
if (!Resolve(uid, ref powerReceiver))
return;

_power.TogglePower(uid);
_power.TryTogglePower(uid);

UpdateAppearance(uid);
DirtyUI(uid, spaceHeater);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ private void OnUiButtonPressed(EntityUid uid, SharedDisposalUnitComponent compon
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(player):player} hit flush button on {ToPrettyString(uid)}, it's now {(component.Engaged ? "on" : "off")}");
break;
case SharedDisposalUnitComponent.UiButton.Power:
_power.TogglePower(uid, user: args.Session.AttachedEntity);
_power.TryTogglePower(uid, user: args.Session.AttachedEntity);
break;
default:
throw new ArgumentOutOfRangeException($"{ToPrettyString(player):player} attempted to hit a nonexistant button on {ToPrettyString(uid)}");
Expand Down
33 changes: 19 additions & 14 deletions Content.Server/Emp/EmpSystem.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Content.Server.Explosion.EntitySystems;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Radio;
using Content.Server.SurveillanceCamera;
using Content.Shared.Emp;
using Content.Shared.Examine;
using Robust.Shared.Map;
Expand All @@ -22,8 +22,6 @@ public override void Initialize()

SubscribeLocalEvent<EmpDisabledComponent, RadioSendAttemptEvent>(OnRadioSendAttempt);
SubscribeLocalEvent<EmpDisabledComponent, RadioReceiveAttemptEvent>(OnRadioReceiveAttempt);
SubscribeLocalEvent<EmpDisabledComponent, ApcToggleMainBreakerAttemptEvent>(OnApcToggleMainBreaker);
SubscribeLocalEvent<EmpDisabledComponent, SurveillanceCameraSetActiveAttemptEvent>(OnCameraSetActive);
}

/// <summary>
Expand Down Expand Up @@ -75,7 +73,19 @@ public void DoEmpEffects(EntityUid uid, float energyConsumption, float duration)
if (ev.Disabled)
{
var disabled = EnsureComp<EmpDisabledComponent>(uid);
disabled.DisabledUntil = Timing.CurTime + TimeSpan.FromSeconds(duration);
// couldnt use null-coalescing operator here sadge
if (disabled.DisabledUntil == TimeSpan.Zero)
{
disabled.DisabledUntil = Timing.CurTime;
}
disabled.DisabledUntil = disabled.DisabledUntil + TimeSpan.FromSeconds(duration);

/// i tried my best to go through the Pow3r server code but i literally couldn't find in relation to PowerNetworkBatteryComponent that uses the event system
/// the code is otherwise too esoteric for my innocent eyes
if (TryComp<PowerNetworkBatteryComponent>(uid, out var powerNetBattery))
{
powerNetBattery.CanCharge = false;
}
}
}

Expand All @@ -91,6 +101,11 @@ public override void Update(float frameTime)
RemComp<EmpDisabledComponent>(uid);
var ev = new EmpDisabledRemoved();
RaiseLocalEvent(uid, ref ev);

if (TryComp<PowerNetworkBatteryComponent>(uid, out var powerNetBattery))
{
powerNetBattery.CanCharge = true;
}
}
}
}
Expand All @@ -115,16 +130,6 @@ private void OnRadioReceiveAttempt(EntityUid uid, EmpDisabledComponent component
{
args.Cancelled = true;
}

private void OnApcToggleMainBreaker(EntityUid uid, EmpDisabledComponent component, ref ApcToggleMainBreakerAttemptEvent args)
{
args.Cancelled = true;
}

private void OnCameraSetActive(EntityUid uid, EmpDisabledComponent component, ref SurveillanceCameraSetActiveAttemptEvent args)
{
args.Cancelled = true;
}
}

/// <summary>
Expand Down
26 changes: 26 additions & 0 deletions Content.Server/Gravity/GravityGeneratorSystem.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Content.Server.Administration.Logs;
using Content.Server.Audio;
using Content.Server.Power.Components;
using Content.Server.Emp;
using Content.Shared.Database;
using Content.Shared.Gravity;
using Content.Shared.Interaction;
Expand Down Expand Up @@ -28,6 +29,8 @@ public override void Initialize()
SubscribeLocalEvent<GravityGeneratorComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<GravityGeneratorComponent, SharedGravityGeneratorComponent.SwitchGeneratorMessage>(
OnSwitchGenerator);

SubscribeLocalEvent<GravityGeneratorComponent, EmpPulseEvent>(OnEmpPulse);
}

private void OnParentChanged(EntityUid uid, GravityGeneratorComponent component, ref EntParentChangedMessage args)
Expand Down Expand Up @@ -289,5 +292,28 @@ private void OnSwitchGenerator(
{
SetSwitchedOn(uid, component, args.On, session:args.Session);
}

private void OnEmpPulse(EntityUid uid, GravityGeneratorComponent component, EmpPulseEvent args)
{
/// i really don't think that the gravity generator should use normalised 0-1 charge
/// as opposed to watts charge that every other battery uses

ApcPowerReceiverComponent? powerReceiver = null;
if (!Resolve(uid, ref powerReceiver, false))
return;

var ent = (uid, component, powerReceiver);

// convert from normalised energy to watts and subtract
float maxEnergy = component.ActivePowerUse / component.ChargeRate;
float currentEnergy = maxEnergy * component.Charge;
currentEnergy = Math.Max(0, currentEnergy - args.EnergyConsumption);

// apply renormalised energy to charge variable
component.Charge = currentEnergy / maxEnergy;

// update power state
UpdateState(ent);
}
}
}
10 changes: 0 additions & 10 deletions Content.Server/Power/Components/ActiveChargerComponent.cs

This file was deleted.

19 changes: 19 additions & 0 deletions Content.Server/Power/Components/ChargingComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Power;

namespace Content.Server.Power.Components
{
[RegisterComponent]
public sealed partial class ChargingComponent : Component
{
///<summary>
///References the entity of the charger that is currently powering this battery
///</summary>
public EntityUid ChargerUid;

///<summary>
///References the component of the charger that is currently powering this battery
///</summary>
public ChargerComponent ChargerComponent;
}
}
19 changes: 11 additions & 8 deletions Content.Server/Power/EntitySystems/ApcSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Content.Shared.APC;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Emp;
using Content.Shared.Popups;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
Expand Down Expand Up @@ -37,6 +38,7 @@ public override void Initialize()
SubscribeLocalEvent<ApcComponent, GotEmaggedEvent>(OnEmagged);

SubscribeLocalEvent<ApcComponent, EmpPulseEvent>(OnEmpPulse);
SubscribeLocalEvent<ApcComponent, EmpDisabledRemoved>(OnEmpDisabledRemoved);
}

public override void Update(float deltaTime)
Expand Down Expand Up @@ -163,7 +165,7 @@ public void UpdateUIState(EntityUid uid,

private ApcChargeState CalcChargeState(EntityUid uid, PowerState.Battery battery)
{
if (HasComp<EmaggedComponent>(uid))
if (HasComp<EmaggedComponent>(uid) || HasComp<EmpDisabledComponent>(uid))
return ApcChargeState.Emag;

if (battery.CurrentStorage / battery.Capacity > ApcComponent.HighPowerThreshold)
Expand All @@ -190,15 +192,16 @@ private ApcExternalPowerState CalcExtPowerState(EntityUid uid, PowerState.Batter

return ApcExternalPowerState.Good;
}

private void OnEmpPulse(EntityUid uid, ApcComponent component, ref EmpPulseEvent args)
{
if (component.MainBreakerEnabled)
{
args.Affected = true;
args.Disabled = true;
ApcToggleBreaker(uid, component);
}
EnsureComp<EmpDisabledComponent>(uid, out var emp); //event calls before EmpDisabledComponent is added, ensure it to force sprite update
UpdateApcState(uid);
}

private void OnEmpDisabledRemoved(EntityUid uid, ApcComponent component, ref EmpDisabledRemoved args)
{
UpdateApcState(uid);
}
}

Expand Down
32 changes: 31 additions & 1 deletion Content.Server/Power/EntitySystems/BatterySystem.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Content.Server.Cargo.Systems;
using Content.Server.Emp;
using Content.Shared.Emp;
using Content.Server.Power.Components;
using Content.Shared.Examine;
using Content.Shared.Rejuvenate;
Expand All @@ -20,6 +21,7 @@ public override void Initialize()
SubscribeLocalEvent<BatteryComponent, RejuvenateEvent>(OnBatteryRejuvenate);
SubscribeLocalEvent<BatteryComponent, PriceCalculationEvent>(CalculateBatteryPrice);
SubscribeLocalEvent<BatteryComponent, EmpPulseEvent>(OnEmpPulse);
SubscribeLocalEvent<BatteryComponent, EmpDisabledRemoved>(OnEmpDisabledRemoved);

SubscribeLocalEvent<NetworkBatteryPreSync>(PreSync);
SubscribeLocalEvent<NetworkBatteryPostSync>(PostSync);
Expand Down Expand Up @@ -85,7 +87,7 @@ public override void Update(float frameTime)
{
if (!comp.AutoRecharge) continue;
if (batt.IsFullyCharged) continue;
SetCharge(uid, batt.CurrentCharge + comp.AutoRechargeRate * frameTime, batt);
TrySetCharge(uid, batt.CurrentCharge + comp.AutoRechargeRate * frameTime, batt);
}
}

Expand All @@ -100,9 +102,21 @@ private void CalculateBatteryPrice(EntityUid uid, BatteryComponent component, re
private void OnEmpPulse(EntityUid uid, BatteryComponent component, ref EmpPulseEvent args)
{
args.Affected = true;
args.Disabled = true;
UseCharge(uid, args.EnergyConsumption, component);
}

// if a disabled battery is put into a recharged,
// allow the recharger to start recharging again after the disable ends
private void OnEmpDisabledRemoved(EntityUid uid, BatteryComponent component, ref EmpDisabledRemoved args)
{
if (!TryComp<ChargingComponent>(uid, out var charging))
return;

var ev = new ChargerUpdateStatusEvent();
RaiseLocalEvent<ChargerUpdateStatusEvent>(charging.ChargerUid, ref ev);
}

public float UseCharge(EntityUid uid, float value, BatteryComponent? battery = null)
{
if (value <= 0 || !Resolve(uid, ref battery) || battery.CurrentCharge == 0)
Expand Down Expand Up @@ -157,6 +171,18 @@ public bool TryUseCharge(EntityUid uid, float value, BatteryComponent? battery =
return true;
}

/// <summary>
/// Like SetCharge, but checks for conditions like EmpDisabled before executing
/// </summary>
public bool TrySetCharge(EntityUid uid, float value, BatteryComponent? battery = null)
{
if (!Resolve(uid, ref battery, false) || TryComp<EmpDisabledComponent>(uid, out var emp))
return false;

SetCharge(uid, value, battery);
return true;
}

/// <summary>
/// Returns whether the battery is at least 99% charged, basically full.
/// </summary>
Expand All @@ -165,6 +191,10 @@ public bool IsFull(EntityUid uid, BatteryComponent? battery = null)
if (!Resolve(uid, ref battery))
return false;

// If the battery is full, remove its charging component.
if (TryComp<ChargingComponent>(uid, out _))
RemComp<ChargingComponent>(uid);

return battery.CurrentCharge / battery.MaxCharge >= 0.99f;
}
}
Expand Down
Loading

0 comments on commit 93ece39

Please sign in to comment.