mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-21 23:48:15 +03:00
<!-- 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 --> 🆑 - tweak: EMP Grenades can now disable basically any electrical device, and stack in disable duration. --------- Signed-off-by: VMSolidus <evilexecutive@gmail.com> Co-authored-by: VMSolidus <evilexecutive@gmail.com>
320 lines
12 KiB
C#
320 lines
12 KiB
C#
using Content.Server.Power.Components;
|
|
using Content.Server.Emp;
|
|
using Content.Server.PowerCell;
|
|
using Content.Shared.Examine;
|
|
using Content.Shared.Power;
|
|
using Content.Shared.PowerCell.Components;
|
|
using Content.Shared.Emp;
|
|
using JetBrains.Annotations;
|
|
using Robust.Shared.Containers;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using Content.Shared.Storage.Components;
|
|
using Robust.Server.Containers;
|
|
using Content.Shared.Whitelist;
|
|
|
|
namespace Content.Server.Power.EntitySystems;
|
|
|
|
[UsedImplicitly]
|
|
internal sealed class ChargerSystem : EntitySystem
|
|
{
|
|
[Dependency] private readonly ContainerSystem _container = default!;
|
|
[Dependency] private readonly PowerCellSystem _powerCell = default!;
|
|
[Dependency] private readonly BatterySystem _battery = default!;
|
|
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
|
|
|
public override void Initialize()
|
|
{
|
|
SubscribeLocalEvent<ChargerComponent, ComponentStartup>(OnStartup);
|
|
SubscribeLocalEvent<ChargerComponent, PowerChangedEvent>(OnPowerChanged);
|
|
SubscribeLocalEvent<ChargerComponent, EntInsertedIntoContainerMessage>(OnInserted);
|
|
SubscribeLocalEvent<ChargerComponent, EntRemovedFromContainerMessage>(OnRemoved);
|
|
SubscribeLocalEvent<ChargerComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
|
|
SubscribeLocalEvent<ChargerComponent, InsertIntoEntityStorageAttemptEvent>(OnEntityStorageInsertAttempt);
|
|
SubscribeLocalEvent<ChargerComponent, ExaminedEvent>(OnChargerExamine);
|
|
|
|
SubscribeLocalEvent<ChargerComponent, ChargerUpdateStatusEvent>(OnUpdateStatus);
|
|
|
|
SubscribeLocalEvent<ChargerComponent, EmpPulseEvent>(OnEmpPulse);
|
|
SubscribeLocalEvent<ChargerComponent, EmpDisabledRemoved>(OnEmpDisabledRemoved);
|
|
}
|
|
|
|
private void OnStartup(EntityUid uid, ChargerComponent component, ComponentStartup args)
|
|
{
|
|
UpdateStatus(uid, component);
|
|
}
|
|
|
|
private void OnChargerExamine(EntityUid uid, ChargerComponent component, ExaminedEvent args)
|
|
{
|
|
args.PushMarkup(Loc.GetString("charger-examine", ("color", "yellow"), ("chargeRate", (int) component.ChargeRate)));
|
|
}
|
|
|
|
private void StartChargingBattery(EntityUid uid, ChargerComponent component, EntityUid target)
|
|
{
|
|
bool charge = true;
|
|
|
|
if (HasComp<EmpDisabledComponent>(uid))
|
|
charge = false;
|
|
else
|
|
if (!TryComp<BatteryComponent>(target, out var battery))
|
|
charge = false;
|
|
else
|
|
if (Math.Abs(battery.MaxCharge - battery.CurrentCharge) < 0.01)
|
|
charge = false;
|
|
|
|
// wrap functionality in an if statement instead of returning...
|
|
if (charge)
|
|
{
|
|
var charging = EnsureComp<ChargingComponent>(target);
|
|
charging.ChargerUid = uid;
|
|
charging.ChargerComponent = component;
|
|
}
|
|
|
|
// ...so the status always updates (for insertin a power cell)
|
|
UpdateStatus(uid, component);
|
|
}
|
|
|
|
private void StopChargingBattery(EntityUid uid, ChargerComponent component, EntityUid target)
|
|
{
|
|
if (HasComp<ChargingComponent>(target))
|
|
RemComp<ChargingComponent>(target);
|
|
UpdateStatus(uid, component);
|
|
}
|
|
|
|
public override void Update(float frameTime)
|
|
{
|
|
var query = EntityQueryEnumerator<ChargingComponent>();
|
|
while (query.MoveNext(out var uid, out var charging))
|
|
{
|
|
if (!TryComp<ChargerComponent>(charging.ChargerUid, out var chargerComponent))
|
|
continue;
|
|
|
|
if (charging.ChargerComponent.Status == CellChargerStatus.Off || charging.ChargerComponent.Status == CellChargerStatus.Empty)
|
|
continue;
|
|
|
|
if (HasComp<EmpDisabledComponent>(charging.ChargerUid))
|
|
continue;
|
|
|
|
if (!TryComp<BatteryComponent>(uid, out var battery))
|
|
continue;
|
|
|
|
if (Math.Abs(battery.MaxCharge - battery.CurrentCharge) < 0.01)
|
|
StopChargingBattery(charging.ChargerUid, charging.ChargerComponent, uid);
|
|
TransferPower(charging.ChargerUid, uid, charging.ChargerComponent, frameTime);
|
|
}
|
|
}
|
|
|
|
private void OnPowerChanged(EntityUid uid, ChargerComponent component, ref PowerChangedEvent args)
|
|
{
|
|
UpdateStatus(uid, component);
|
|
}
|
|
|
|
private void OnInserted(EntityUid uid, ChargerComponent component, EntInsertedIntoContainerMessage args)
|
|
{
|
|
if (!component.Initialized)
|
|
return;
|
|
|
|
if (args.Container.ID != component.SlotId)
|
|
return;
|
|
|
|
StartChargingBattery(uid, component, args.Entity);
|
|
}
|
|
|
|
private void OnRemoved(EntityUid uid, ChargerComponent component, EntRemovedFromContainerMessage args)
|
|
{
|
|
if (args.Container.ID != component.SlotId)
|
|
return;
|
|
|
|
StopChargingBattery(uid, component, args.Entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that the entity being inserted is actually rechargeable.
|
|
/// </summary>
|
|
private void OnInsertAttempt(EntityUid uid, ChargerComponent component, ContainerIsInsertingAttemptEvent args)
|
|
{
|
|
if (!component.Initialized)
|
|
return;
|
|
|
|
if (args.Container.ID != component.SlotId)
|
|
return;
|
|
|
|
if (!TryComp<PowerCellSlotComponent>(args.EntityUid, out var cellSlot))
|
|
return;
|
|
|
|
if (!cellSlot.FitsInCharger)
|
|
args.Cancel();
|
|
}
|
|
|
|
private void OnEntityStorageInsertAttempt(EntityUid uid, ChargerComponent component, ref InsertIntoEntityStorageAttemptEvent args)
|
|
{
|
|
if (!component.Initialized || args.Cancelled)
|
|
return;
|
|
|
|
if (!TryComp<PowerCellSlotComponent>(uid, out var cellSlot))
|
|
return;
|
|
|
|
if (!cellSlot.FitsInCharger)
|
|
args.Cancelled = true;
|
|
}
|
|
|
|
private void OnUpdateStatus(EntityUid uid, ChargerComponent component, ref ChargerUpdateStatusEvent args)
|
|
{
|
|
UpdateStatus(uid, component);
|
|
}
|
|
|
|
private void UpdateStatus(EntityUid uid, ChargerComponent component)
|
|
{
|
|
var status = GetStatus(uid, component);
|
|
TryComp(uid, out AppearanceComponent? appearance);
|
|
|
|
if (!_container.TryGetContainer(uid, component.SlotId, out var container))
|
|
return;
|
|
|
|
_appearance.SetData(uid, CellVisual.Occupied, container.ContainedEntities.Count != 0, appearance);
|
|
if (component.Status == status || !TryComp(uid, out ApcPowerReceiverComponent? receiver))
|
|
return;
|
|
|
|
component.Status = status;
|
|
|
|
switch (component.Status)
|
|
{
|
|
case CellChargerStatus.Off:
|
|
receiver.Load = 0;
|
|
_appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Off, appearance);
|
|
break;
|
|
case CellChargerStatus.Empty:
|
|
receiver.Load = 0;
|
|
_appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Empty, appearance);
|
|
break;
|
|
case CellChargerStatus.Charging:
|
|
receiver.Load = component.ChargeRate; //does not scale with multiple slotted batteries
|
|
_appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Charging, appearance);
|
|
break;
|
|
case CellChargerStatus.Charged:
|
|
receiver.Load = 0;
|
|
_appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Charged, appearance);
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
}
|
|
|
|
private void OnEmpPulse(EntityUid uid, ChargerComponent component, ref EmpPulseEvent args)
|
|
{
|
|
// we don't care if we haven't been disabled
|
|
if (!args.Disabled)
|
|
return;
|
|
|
|
// if the recharger is hit by an emp pulse,
|
|
// stop recharging contained batteries to save resources
|
|
if (!_container.TryGetContainer(uid, component.SlotId, out var container))
|
|
return;
|
|
|
|
foreach (var containedEntity in container.ContainedEntities)
|
|
{
|
|
if (!SearchForBattery(containedEntity, out _, out _))
|
|
continue;
|
|
|
|
StopChargingBattery(uid, component, containedEntity);
|
|
}
|
|
}
|
|
|
|
private void OnEmpDisabledRemoved(EntityUid uid, ChargerComponent component, ref EmpDisabledRemoved args)
|
|
{
|
|
// if an emp disable subsides,
|
|
// attempt to start charging all batteries
|
|
if (!_container.TryGetContainer(uid, component.SlotId, out var container))
|
|
return;
|
|
|
|
foreach (var containedEntity in container.ContainedEntities)
|
|
{
|
|
if (!SearchForBattery(containedEntity, out _, out _))
|
|
continue;
|
|
|
|
StartChargingBattery(uid, component, containedEntity);
|
|
}
|
|
}
|
|
|
|
private CellChargerStatus GetStatus(EntityUid uid, ChargerComponent component)
|
|
{
|
|
if (!TryComp(uid, out TransformComponent? transformComponent))
|
|
return CellChargerStatus.Off;
|
|
|
|
if (!transformComponent.Anchored)
|
|
return CellChargerStatus.Off;
|
|
|
|
if (!TryComp(uid, out ApcPowerReceiverComponent? apcPowerReceiverComponent))
|
|
return CellChargerStatus.Off;
|
|
|
|
if (!apcPowerReceiverComponent.Powered)
|
|
return CellChargerStatus.Off;
|
|
|
|
if (!_container.TryGetContainer(uid, component.SlotId, out var container))
|
|
return CellChargerStatus.Off;
|
|
|
|
if (container.ContainedEntities.Count == 0)
|
|
return CellChargerStatus.Empty;
|
|
|
|
var statusOut = CellChargerStatus.Off;
|
|
|
|
foreach (var containedEntity in container.ContainedEntities)
|
|
{
|
|
// if none of the slotted items are actually batteries, represent the charger as off
|
|
if (!SearchForBattery(containedEntity, out _, out _))
|
|
continue;
|
|
|
|
// if all batteries are either EMP'd or fully charged, represent the charger as fully charged
|
|
statusOut = CellChargerStatus.Charged;
|
|
if (HasComp<EmpDisabledComponent>(containedEntity))
|
|
continue;
|
|
|
|
if (!HasComp<ChargingComponent>(containedEntity))
|
|
continue;
|
|
|
|
// if we have atleast one battery being charged, represent the charger as charging;
|
|
statusOut = CellChargerStatus.Charging;
|
|
break;
|
|
}
|
|
|
|
return statusOut;
|
|
}
|
|
|
|
private void TransferPower(EntityUid uid, EntityUid targetEntity, ChargerComponent component, float frameTime)
|
|
{
|
|
if (!TryComp(uid, out ApcPowerReceiverComponent? receiverComponent))
|
|
return;
|
|
|
|
if (!receiverComponent.Powered)
|
|
return;
|
|
|
|
if (component.Whitelist?.IsValid(targetEntity, EntityManager) == false)
|
|
return;
|
|
|
|
if (!SearchForBattery(targetEntity, out var batteryUid, out var heldBattery))
|
|
return;
|
|
|
|
_battery.TrySetCharge(batteryUid.Value, heldBattery.CurrentCharge + component.ChargeRate * frameTime, heldBattery);
|
|
// Just so the sprite won't be set to 99.99999% visibility
|
|
if (heldBattery.MaxCharge - heldBattery.CurrentCharge < 0.01)
|
|
{
|
|
_battery.TrySetCharge(batteryUid.Value, heldBattery.MaxCharge, heldBattery);
|
|
}
|
|
|
|
UpdateStatus(uid, component);
|
|
}
|
|
|
|
private bool SearchForBattery(EntityUid uid, [NotNullWhen(true)] out EntityUid? batteryUid, [NotNullWhen(true)] out BatteryComponent? component)
|
|
{
|
|
// try get a battery directly on the inserted entity
|
|
if (!TryComp(uid, out component))
|
|
{
|
|
// or by checking for a power cell slot on the inserted entity
|
|
return _powerCell.TryGetBatteryFromSlot(uid, out batteryUid, out component);
|
|
}
|
|
batteryUid = uid;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
[ByRefEvent]
|
|
public record struct ChargerUpdateStatusEvent(); |