Electrocution. (#4958)

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
This commit is contained in:
Vera Aguilera Puerto
2021-10-25 16:21:56 +02:00
committed by GitHub
parent 66a3d5bf29
commit ed3bf94a3b
37 changed files with 934 additions and 30 deletions

View File

@@ -0,0 +1,7 @@
using Content.Shared.Electrocution;
namespace Content.Client.Electrocution
{
public sealed class ElectrocutionSystem : SharedElectrocutionSystem
{ }
}

View File

@@ -58,6 +58,9 @@ namespace Content.Client.Entry
"FloorTile",
"ShuttleController",
"HumanInventoryController",
"RandomInsulation",
"Electrified",
"Electrocution",
"Pourable",
"Paper",
"Write",

View File

@@ -0,0 +1,20 @@
using System.Threading.Tasks;
using Content.Server.Electrocution;
using Content.Shared.Construction;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Construction.Completions
{
[DataDefinition]
public class AttemptElectrocute : IGraphAction
{
public async Task PerformAction(IEntity entity, IEntity? user)
{
if (user == null)
return;
EntitySystem.Get<ElectrocutionSystem>().TryDoElectrifiedAct(entity.Uid, user.Uid);
}
}
}

View File

@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=electrocution_005Ccomponents/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -0,0 +1,65 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Electrocution
{
/// <summary>
/// Component for things that shock users on touch.
/// </summary>
[RegisterComponent]
public class ElectrifiedComponent : Component
{
public override string Name => "Electrified";
[DataField("enabled")]
public bool Enabled { get; set; } = true;
[DataField("onBump")]
public bool OnBump { get; set; } = true;
[DataField("onAttacked")]
public bool OnAttacked { get; set; } = true;
[DataField("noWindowInTile")]
public bool NoWindowInTile { get; set; } = false;
[DataField("onHandInteract")]
public bool OnHandInteract { get; set; } = true;
[DataField("requirePower")]
public bool RequirePower { get; } = true;
[DataField("highVoltageNode")]
public string? HighVoltageNode { get; }
[DataField("mediumVoltageNode")]
public string? MediumVoltageNode { get; }
[DataField("lowVoltageNode")]
public string? LowVoltageNode { get; }
[DataField("highVoltageDamageMultiplier")]
public float HighVoltageDamageMultiplier { get; } = 3f;
[DataField("highVoltageTimeMultiplier")]
public float HighVoltageTimeMultiplier { get; } = 1.5f;
[DataField("mediumVoltageDamageMultiplier")]
public float MediumVoltageDamageMultiplier { get; } = 2f;
[DataField("mediumVoltageTimeMultiplier")]
public float MediumVoltageTimeMultiplier { get; } = 1.25f;
[DataField("shockDamage")]
public int ShockDamage { get; } = 20;
/// <summary>
/// Shock time, in seconds.
/// </summary>
[DataField("shockTime")]
public float ShockTime { get; } = 30f;
[DataField("siemensCoefficient")]
public float SiemensCoefficient { get; } = 1f;
}
}

View File

@@ -0,0 +1,21 @@
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Electrocution
{
/// <summary>
/// Component for virtual electrocution entities (representing an in-progress shock).
/// </summary>
[RegisterComponent]
[Friend(typeof(ElectrocutionSystem))]
public sealed class ElectrocutionComponent : Component
{
public override string Name => "Electrocution";
[DataField("timeLeft")] public float TimeLeft { get; set; }
[DataField("electrocuting")] public EntityUid Electrocuting { get; set; }
[DataField("accumDamage")] public float AccumulatedDamage { get; set; }
}
}

View File

@@ -0,0 +1,29 @@
using System.Collections.Generic;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Nodes;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Electrocution
{
[DataDefinition]
public sealed class ElectrocutionNode : Node
{
[DataField("cable")]
public EntityUid CableEntity;
[DataField("node")]
public string NodeName = default!;
public override IEnumerable<Node> GetReachableNodes()
{
var ent = IoCManager.Resolve<IEntityManager>();
if (!ent.TryGetComponent(CableEntity, out NodeContainerComponent? nodeContainer))
yield break;
if (nodeContainer.TryGetNode(NodeName, out Node? node))
yield return node;
}
}
}

View File

@@ -0,0 +1,407 @@
using System;
using System.Collections.Generic;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Power.NodeGroups;
using Content.Server.Window;
using Content.Shared.Alert;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Electrocution;
using Content.Shared.Interaction;
using Content.Shared.Jittering;
using Content.Shared.Maps;
using Content.Shared.Popups;
using Content.Shared.Pulling.Components;
using Content.Shared.Speech.EntitySystems;
using Content.Shared.StatusEffect;
using Content.Shared.Stunnable;
using Content.Shared.Weapons.Melee;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Electrocution
{
public sealed class ElectrocutionSystem : SharedElectrocutionSystem
{
[Dependency] private readonly IEntityLookup _entityLookup = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!;
[Dependency] private readonly SharedJitteringSystem _jitteringSystem = default!;
[Dependency] private readonly SharedStunSystem _stunSystem = default!;
[Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly NodeGroupSystem _nodeGroupSystem = default!;
protected const string StatusEffectKey = "Electrocution";
protected const string DamageType = "Shock";
// Yes, this is absurdly small for a reason.
private const float ElectrifiedDamagePerWatt = 0.0015f;
private const float RecursiveDamageMultiplier = 0.75f;
private const float RecursiveTimeMultiplier = 0.8f;
private const float ParalyzeTimeMultiplier = 1f;
private const float StutteringTimeMultiplier = 1.5f;
private const float JitterTimeMultiplier = 0.75f;
private const float JitterAmplitude = 80f;
private const float JitterFrequency = 8f;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ElectrifiedComponent, StartCollideEvent>(OnElectrifiedStartCollide);
SubscribeLocalEvent<ElectrifiedComponent, AttackedEvent>(OnElectrifiedAttacked);
SubscribeLocalEvent<ElectrifiedComponent, InteractHandEvent>(OnElectrifiedHandInteract);
SubscribeLocalEvent<RandomInsulationComponent, MapInitEvent>(OnRandomInsulationMapInit);
UpdatesAfter.Add(typeof(PowerNetSystem));
}
public override void Update(float frameTime)
{
// Update "in progress" electrocutions
RemQueue<ElectrocutionComponent> finishedElectrocutionsQueue = new();
foreach (var (electrocution, consumer) in EntityManager
.EntityQuery<ElectrocutionComponent, PowerConsumerComponent>())
{
var ftAdjusted = Math.Min(frameTime, electrocution.TimeLeft);
electrocution.TimeLeft -= ftAdjusted;
electrocution.AccumulatedDamage += consumer.ReceivedPower * ElectrifiedDamagePerWatt * ftAdjusted;
if (MathHelper.CloseTo(electrocution.TimeLeft, 0))
finishedElectrocutionsQueue.Add(electrocution);
}
foreach (var finished in finishedElectrocutionsQueue)
{
var uid = finished.Owner.Uid;
if (EntityManager.EntityExists(finished.Electrocuting))
{
// TODO: damage should be scaled by shock damage multiplier
// TODO: better paralyze/jitter timing
var damage = new DamageSpecifier(
_prototypeManager.Index<DamageTypePrototype>(DamageType),
(int) finished.AccumulatedDamage);
_damageableSystem.TryChangeDamage(finished.Electrocuting, damage);
}
EntityManager.DeleteEntity(uid);
}
}
private void OnElectrifiedStartCollide(EntityUid uid, ElectrifiedComponent electrified, StartCollideEvent args)
{
if (!electrified.OnBump)
return;
TryDoElectrifiedAct(uid, args.OtherFixture.Body.Owner.Uid, electrified);
}
private void OnElectrifiedAttacked(EntityUid uid, ElectrifiedComponent electrified, AttackedEvent args)
{
if (!electrified.OnAttacked)
return;
TryDoElectrifiedAct(uid, args.User.Uid, electrified);
}
private void OnElectrifiedHandInteract(EntityUid uid, ElectrifiedComponent electrified, InteractHandEvent args)
{
if (!electrified.OnHandInteract)
return;
TryDoElectrifiedAct(uid, args.User.Uid, electrified);
}
public bool TryDoElectrifiedAct(EntityUid uid, EntityUid targetUid,
ElectrifiedComponent? electrified = null,
NodeContainerComponent? nodeContainer = null,
ITransformComponent? transform = null)
{
if (!Resolve(uid, ref electrified, ref transform, false))
return false;
if (!electrified.Enabled)
return false;
if (electrified.NoWindowInTile)
{
foreach (var entity in transform.Coordinates.GetEntitiesInTile(
LookupFlags.Approximate | LookupFlags.IncludeAnchored, _entityLookup))
{
if (entity.HasComponent<WindowComponent>())
return false;
}
}
var targets = new List<(EntityUid entity, int depth)>();
GetChainedElectrocutionTargets(targetUid, targets);
if (!electrified.RequirePower)
{
var lastRet = true;
for (var i = targets.Count - 1; i >= 0; i--)
{
var (entity, depth) = targets[i];
lastRet = TryDoElectrocution(
entity,
uid,
(int) (electrified.ShockDamage * MathF.Pow(RecursiveDamageMultiplier, depth)),
TimeSpan.FromSeconds(electrified.ShockTime * MathF.Pow(RecursiveTimeMultiplier, depth)),
electrified.SiemensCoefficient);
}
return lastRet;
}
if (!Resolve(uid, ref nodeContainer, false))
return false;
var node = TryNode(electrified.HighVoltageNode) ??
TryNode(electrified.MediumVoltageNode) ??
TryNode(electrified.LowVoltageNode);
if (node == null)
return false;
var (damageMult, timeMult) = node.NodeGroupID switch
{
NodeGroupID.HVPower => (electrified.HighVoltageDamageMultiplier, electrified.HighVoltageTimeMultiplier),
NodeGroupID.MVPower => (electrified.MediumVoltageDamageMultiplier,
electrified.MediumVoltageTimeMultiplier),
_ => (1f, 1f)
};
{
var lastRet = true;
for (var i = targets.Count - 1; i >= 0; i--)
{
var (entity, depth) = targets[i];
lastRet = TryDoElectrocutionPowered(
entity,
uid,
node,
(int) (electrified.ShockDamage * MathF.Pow(RecursiveDamageMultiplier, depth) * damageMult),
TimeSpan.FromSeconds(electrified.ShockTime * MathF.Pow(RecursiveTimeMultiplier, depth) *
timeMult),
electrified.SiemensCoefficient);
}
return lastRet;
}
Node? TryNode(string? id)
{
if (id != null && nodeContainer.TryGetNode<Node>(id, out var node)
&& node.NodeGroup is IBasePowerNet { NetworkNode: { LastAvailableSupplySum: >0 } })
{
return node;
}
return null;
}
}
/// <returns>Whether the entity <see cref="uid"/> was stunned by the shock.</returns>
public bool TryDoElectrocution(
EntityUid uid, EntityUid? sourceUid, int shockDamage, TimeSpan time, float siemensCoefficient = 1f,
StatusEffectsComponent? statusEffects = null,
SharedAlertsComponent? alerts = null)
{
if (!DoCommonElectrocutionAttempt(uid, sourceUid, ref siemensCoefficient)
|| !DoCommonElectrocution(uid, sourceUid, shockDamage, time, siemensCoefficient, statusEffects, alerts))
return false;
RaiseLocalEvent(uid, new ElectrocutedEvent(uid, sourceUid, siemensCoefficient));
return true;
}
private bool TryDoElectrocutionPowered(
EntityUid uid,
EntityUid sourceUid,
Node node,
int shockDamage,
TimeSpan time,
float siemensCoefficient = 1f,
StatusEffectsComponent? statusEffects = null,
SharedAlertsComponent? alerts = null,
ITransformComponent? sourceTransform = null)
{
if (!DoCommonElectrocutionAttempt(uid, sourceUid, ref siemensCoefficient))
return false;
// Coefficient needs to be higher than this to do a powered electrocution!
if(siemensCoefficient <= 0.5f)
return DoCommonElectrocution(uid, sourceUid, shockDamage, time, siemensCoefficient, statusEffects, alerts);
if (!DoCommonElectrocution(uid, sourceUid, null, time, siemensCoefficient, statusEffects, alerts))
return false;
if (!Resolve(sourceUid, ref sourceTransform)) // This shouldn't really happen, but just in case...
return true;
var electrocutionEntity = EntityManager.SpawnEntity(
$"VirtualElectrocutionLoad{node.NodeGroupID}", sourceTransform.Coordinates);
var electrocutionNode = electrocutionEntity
.GetComponent<NodeContainerComponent>()
.GetNode<ElectrocutionNode>("electrocution");
var electrocutionComponent = electrocutionEntity.GetComponent<ElectrocutionComponent>();
electrocutionNode.CableEntity = sourceUid;
electrocutionNode.NodeName = node.Name;
_nodeGroupSystem.QueueReflood(electrocutionNode);
electrocutionComponent.TimeLeft = 1f;
electrocutionComponent.Electrocuting = uid;
RaiseLocalEvent(uid, new ElectrocutedEvent(uid, sourceUid, siemensCoefficient));
return true;
}
private bool DoCommonElectrocutionAttempt(EntityUid uid, EntityUid? sourceUid, ref float siemensCoefficient)
{
var attemptEvent = new ElectrocutionAttemptEvent(uid, sourceUid, siemensCoefficient);
RaiseLocalEvent(uid, attemptEvent);
// Cancel the electrocution early, so we don't recursively electrocute anything.
if (attemptEvent.Cancelled)
return false;
siemensCoefficient = attemptEvent.SiemensCoefficient;
return true;
}
private bool DoCommonElectrocution(EntityUid uid, EntityUid? sourceUid,
int? shockDamage, TimeSpan time, float siemensCoefficient = 1f,
StatusEffectsComponent? statusEffects = null,
SharedAlertsComponent? alerts = null)
{
if (siemensCoefficient <= 0)
return false;
if (shockDamage != null)
{
shockDamage = (int) (shockDamage * siemensCoefficient);
if (shockDamage.Value <= 0)
return false;
}
// Optional component.
Resolve(uid, ref alerts, false);
if (!Resolve(uid, ref statusEffects, false) ||
!_statusEffectsSystem.CanApplyEffect(uid, StatusEffectKey, statusEffects))
return false;
if (!_statusEffectsSystem.TryAddStatusEffect<ElectrocutedComponent>(uid, StatusEffectKey, time,
statusEffects, alerts))
return false;
var shouldStun = siemensCoefficient > 0.5f;
if (shouldStun)
_stunSystem.TryParalyze(uid, time * ParalyzeTimeMultiplier, statusEffects, alerts);
// TODO: Sparks here.
if(shockDamage is {} dmg)
_damageableSystem.TryChangeDamage(uid,
new DamageSpecifier(_prototypeManager.Index<DamageTypePrototype>(DamageType), dmg));
_stutteringSystem.DoStutter(uid, time * StutteringTimeMultiplier, statusEffects, alerts);
_jitteringSystem.DoJitter(uid, time * JitterTimeMultiplier, JitterAmplitude, JitterFrequency, true,
statusEffects, alerts);
_popupSystem.PopupEntity(Loc.GetString("electrocuted-component-mob-shocked-popup-player"), uid,
Filter.Entities(uid).Unpredicted());
var filter = Filter.Pvs(uid, 2f, EntityManager).RemoveWhereAttachedEntity(puid => puid == uid)
.Unpredicted();
// TODO: Allow being able to pass EntityUid to Loc...
if (sourceUid != null)
{
_popupSystem.PopupEntity(Loc.GetString("electrocuted-component-mob-shocked-by-source-popup-others",
("mob", EntityManager.GetEntity(uid)), ("source", EntityManager.GetEntity(sourceUid.Value))),
uid, filter);
}
else
{
_popupSystem.PopupEntity(Loc.GetString("electrocuted-component-mob-shocked-popup-others",
("mob", EntityManager.GetEntity(uid))), uid, filter);
}
return true;
}
private void GetChainedElectrocutionTargets(EntityUid source, List<(EntityUid entity, int depth)> all)
{
var visited = new HashSet<EntityUid>();
GetChainedElectrocutionTargetsRecurse(source, 1, visited, all);
}
private void GetChainedElectrocutionTargetsRecurse(
EntityUid entity,
int depth,
HashSet<EntityUid> visited,
List<(EntityUid entity, int depth)> all)
{
all.Add((entity, depth));
visited.Add(entity);
if (EntityManager.TryGetComponent(entity, out SharedPullableComponent? pullable)
&& pullable.Puller != null
&& !visited.Contains(pullable.Puller.Uid))
{
GetChainedElectrocutionTargetsRecurse(pullable.Puller.Uid, depth + 1, visited, all);
}
if (EntityManager.TryGetComponent(entity, out SharedPullerComponent? puller)
&& puller.Pulling != null
&& !visited.Contains(puller.Pulling.Uid))
{
GetChainedElectrocutionTargetsRecurse(puller.Pulling.Uid, depth + 1, visited, all);
}
}
private void OnRandomInsulationMapInit(EntityUid uid, RandomInsulationComponent randomInsulation,
MapInitEvent args)
{
if (!EntityManager.TryGetComponent(uid, out InsulatedComponent? insulated))
return;
if (randomInsulation.List.Length == 0)
return;
SetInsulatedSiemensCoefficient(uid, _random.Pick(randomInsulation.List), insulated);
}
}
}

View File

@@ -0,0 +1,14 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Electrocution
{
[RegisterComponent]
public class RandomInsulationComponent : Component
{
public override string Name => "RandomInsulation";
[DataField("list")]
public readonly float[] List = { 0f };
}
}

View File

@@ -4,6 +4,7 @@ using Content.Server.Items;
using Content.Shared.Inventory;
using Content.Shared.Slippery;
using Content.Shared.Damage;
using Content.Shared.Electrocution;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
@@ -20,6 +21,7 @@ namespace Content.Server.Inventory
SubscribeLocalEvent<InventoryComponent, HighPressureEvent>(OnHighPressureEvent);
SubscribeLocalEvent<InventoryComponent, LowPressureEvent>(OnLowPressureEvent);
SubscribeLocalEvent<InventoryComponent, DamageModifyEvent>(OnDamageModify);
SubscribeLocalEvent<InventoryComponent, ElectrocutionAttemptEvent>(OnElectrocutionAttempt);
SubscribeLocalEvent<InventoryComponent, SlipAttemptEvent>(OnSlipAttemptEvent);
}
@@ -51,6 +53,14 @@ namespace Content.Server.Inventory
RelayPressureEvent(component, args);
}
private void OnElectrocutionAttempt(EntityUid uid, InventoryComponent component, ElectrocutionAttemptEvent args)
{
foreach (var equipped in component.GetAllHeldItems())
{
RaiseLocalEvent(equipped.Uid, args, false);
}
}
private void OnDamageModify(EntityUid uid, InventoryComponent component, DamageModifyEvent args)
{
foreach (var equipped in component.GetAllHeldItems())

View File

@@ -8,7 +8,14 @@ using Robust.Shared.ViewVariables;
namespace Content.Server.Power.Components
{
public abstract class BaseNetConnectorComponent<TNetType> : Component
public interface IBaseNetConnectorComponent<in TNetType>
{
public TNetType? Net { set; }
public Voltage Voltage { get; }
public string? NodeId { get; }
}
public abstract class BaseNetConnectorComponent<TNetType> : Component, IBaseNetConnectorComponent<TNetType>
{
[ViewVariables(VVAccess.ReadWrite)]
public Voltage Voltage { get => _voltage; set => SetVoltage(value); }
@@ -22,7 +29,7 @@ namespace Content.Server.Power.Components
[ViewVariables]
private bool _needsNet => _net != null;
[DataField("node")] [ViewVariables] public string? NodeId;
[DataField("node")] [ViewVariables] public string? NodeId { get; set; }
protected override void Initialize()
{

View File

@@ -3,6 +3,11 @@ using Content.Server.Power.NodeGroups;
namespace Content.Server.Power.Components
{
public interface IBasePowerNetComponent : IBaseNetConnectorComponent<IPowerNet>
{
}
public abstract class BasePowerNetComponent : BaseNetConnectorComponent<IPowerNet>
{
}

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using Content.Server.Electrocution;
using Content.Server.Stack;
using Content.Server.Tools;
using Content.Server.Tools.Components;
@@ -43,6 +44,8 @@ namespace Content.Server.Power.Components
if (!await EntitySystem.Get<ToolSystem>().UseTool(eventArgs.Using.Uid, eventArgs.User.Uid, Owner.Uid, 0f, 0.25f, _cuttingQuality)) return false;
if (EntitySystem.Get<ElectrocutionSystem>().TryDoElectrifiedAct(Owner.Uid, eventArgs.User.Uid)) return false;
Owner.Delete();
var droppedEnt = Owner.EntityManager.SpawnEntity(_cableDroppedOnCutPrototype, eventArgs.ClickLocation);

View File

@@ -10,7 +10,7 @@ namespace Content.Server.Power.Components
/// Draws power directly from an MV or HV wire it is on top of.
/// </summary>
[RegisterComponent]
public class PowerConsumerComponent : BasePowerNetComponent
public class PowerConsumerComponent : BaseNetConnectorComponent<IBasePowerNet>
{
public override string Name => "PowerConsumer";
@@ -31,12 +31,12 @@ namespace Content.Server.Power.Components
public PowerState.Load NetworkLoad { get; } = new();
protected override void AddSelfToNet(IPowerNet powerNet)
protected override void AddSelfToNet(IBasePowerNet powerNet)
{
powerNet.AddConsumer(this);
}
protected override void RemoveSelfFromNet(IPowerNet powerNet)
protected override void RemoveSelfFromNet(IBasePowerNet powerNet)
{
powerNet.RemoveConsumer(this);
}

View File

@@ -279,6 +279,12 @@ namespace Content.Server.Power.EntitySystems
}
}
foreach (var consumer in net.Consumers)
{
netNode.Loads.Add(consumer.NetworkLoad.Id);
consumer.NetworkLoad.LinkedNetwork = netNode.Id;
}
foreach (var apc in net.Apcs)
{
var netBattery = apc.Owner.GetComponent<PowerNetworkBatteryComponent>();

View File

@@ -13,7 +13,7 @@ using Robust.Shared.ViewVariables;
namespace Content.Server.Power.NodeGroups
{
public interface IApcNet
public interface IApcNet : IBasePowerNet
{
void AddApc(ApcComponent apc);
@@ -25,20 +25,18 @@ namespace Content.Server.Power.NodeGroups
void QueueNetworkReconnect();
PowerState.Network NetworkNode { get; }
GridId? GridId { get; }
}
[NodeGroup(NodeGroupID.Apc)]
[UsedImplicitly]
public class ApcNet : BaseNetConnectorNodeGroup<BaseApcNetComponent, IApcNet>, IApcNet
public class ApcNet : BaseNetConnectorNodeGroup<IApcNet>, IApcNet
{
private readonly PowerNetSystem _powerNetSystem = EntitySystem.Get<PowerNetSystem>();
[ViewVariables] public readonly List<ApcComponent> Apcs = new();
[ViewVariables] public readonly List<ApcPowerProviderComponent> Providers = new();
[ViewVariables] public readonly List<PowerConsumerComponent> Consumers = new();
//Debug property
[ViewVariables] private int TotalReceivers => Providers.Sum(provider => provider.LinkedReceivers.Count);
@@ -71,7 +69,7 @@ namespace Content.Server.Power.NodeGroups
if (apc.Owner.TryGetComponent(out PowerNetworkBatteryComponent? netBattery))
netBattery.NetworkBattery.LinkedNetworkDischarging = default;
_powerNetSystem.QueueReconnectApcNet(this);
QueueNetworkReconnect();
Apcs.Add(apc);
}
@@ -80,7 +78,7 @@ namespace Content.Server.Power.NodeGroups
if (apc.Owner.TryGetComponent(out PowerNetworkBatteryComponent? netBattery))
netBattery.NetworkBattery.LinkedNetworkDischarging = default;
_powerNetSystem.QueueReconnectApcNet(this);
QueueNetworkReconnect();
Apcs.Remove(apc);
}
@@ -88,14 +86,28 @@ namespace Content.Server.Power.NodeGroups
{
Providers.Add(provider);
_powerNetSystem.QueueReconnectApcNet(this);
QueueNetworkReconnect();
}
public void RemovePowerProvider(ApcPowerProviderComponent provider)
{
Providers.Remove(provider);
_powerNetSystem.QueueReconnectApcNet(this);
QueueNetworkReconnect();
}
public void AddConsumer(PowerConsumerComponent consumer)
{
consumer.NetworkLoad.LinkedNetwork = default;
Consumers.Add(consumer);
QueueNetworkReconnect();
}
public void RemoveConsumer(PowerConsumerComponent consumer)
{
consumer.NetworkLoad.LinkedNetwork = default;
Consumers.Remove(consumer);
QueueNetworkReconnect();
}
public void QueueNetworkReconnect()
@@ -103,7 +115,7 @@ namespace Content.Server.Power.NodeGroups
_powerNetSystem.QueueReconnectApcNet(this);
}
protected override void SetNetConnectorNet(BaseApcNetComponent netConnectorComponent)
protected override void SetNetConnectorNet(IBaseNetConnectorComponent<IApcNet> netConnectorComponent)
{
netConnectorComponent.Net = this;
}

View File

@@ -6,8 +6,7 @@ using Content.Server.Power.Components;
namespace Content.Server.Power.NodeGroups
{
public abstract class BaseNetConnectorNodeGroup<TNetConnector, TNetType> : BaseNodeGroup
where TNetConnector : BaseNetConnectorComponent<TNetType>
public abstract class BaseNetConnectorNodeGroup<TNetType> : BaseNodeGroup
{
public override void LoadNodes(List<Node> groupNodes)
{
@@ -16,7 +15,7 @@ namespace Content.Server.Power.NodeGroups
foreach (var node in groupNodes)
{
var newNetConnectorComponents = node.Owner
.GetAllComponents<TNetConnector>()
.GetAllComponents<IBaseNetConnectorComponent<TNetType>>()
.Where(powerComp => (powerComp.NodeId == null || powerComp.NodeId == node.Name) &&
(NodeGroupID) powerComp.Voltage == node.NodeGroupID)
.ToList();
@@ -28,6 +27,6 @@ namespace Content.Server.Power.NodeGroups
}
}
protected abstract void SetNetConnectorNet(TNetConnector netConnectorComponent);
protected abstract void SetNetConnectorNet(IBaseNetConnectorComponent<TNetType> netConnectorComponent);
}
}

View File

@@ -0,0 +1,14 @@
using Content.Server.Power.Components;
using Content.Server.Power.Pow3r;
namespace Content.Server.Power.NodeGroups
{
public interface IBasePowerNet
{
void AddConsumer(PowerConsumerComponent consumer);
void RemoveConsumer(PowerConsumerComponent consumer);
PowerState.Network NetworkNode { get; }
}
}

View File

@@ -12,16 +12,12 @@ using Robust.Shared.ViewVariables;
namespace Content.Server.Power.NodeGroups
{
public interface IPowerNet
public interface IPowerNet : IBasePowerNet
{
void AddSupplier(PowerSupplierComponent supplier);
void RemoveSupplier(PowerSupplierComponent supplier);
void AddConsumer(PowerConsumerComponent consumer);
void RemoveConsumer(PowerConsumerComponent consumer);
void AddDischarger(BatteryDischargerComponent discharger);
void RemoveDischarger(BatteryDischargerComponent discharger);
@@ -33,7 +29,7 @@ namespace Content.Server.Power.NodeGroups
[NodeGroup(NodeGroupID.HVPower, NodeGroupID.MVPower)]
[UsedImplicitly]
public class PowerNet : BaseNetConnectorNodeGroup<BasePowerNetComponent, IPowerNet>, IPowerNet
public class PowerNet : BaseNetConnectorNodeGroup<IPowerNet>, IPowerNet
{
private readonly PowerNetSystem _powerNetSystem = EntitySystem.Get<PowerNetSystem>();
@@ -59,7 +55,7 @@ namespace Content.Server.Power.NodeGroups
_powerNetSystem.DestroyPowerNet(this);
}
protected override void SetNetConnectorNet(BasePowerNetComponent netConnectorComponent)
protected override void SetNetConnectorNet(IBaseNetConnectorComponent<IPowerNet> netConnectorComponent)
{
netConnectorComponent.Net = this;
}

View File

@@ -171,6 +171,9 @@ namespace Content.Server.Power.Pow3r
battery.LoadingDemandMarked = true;
}
network.LastAvailableSupplySum = availableSupplySum;
network.LastMaxSupplySum = maxSupplySum;
var met = Math.Min(demand, availableSupplySum);
if (met != 0)

View File

@@ -451,6 +451,9 @@ namespace Content.Server.Power.Pow3r
// "Supplying" means the network is connected to the OUTPUT port of the battery.
[ViewVariables] public List<NodeId> BatteriesDischarging = new();
[ViewVariables] public float LastAvailableSupplySum = 0f;
[ViewVariables] public float LastMaxSupplySum = 0f;
[ViewVariables] [JsonIgnore] public int Height;
[JsonIgnore] public bool HeightTouched;
}

View File

@@ -90,6 +90,8 @@ namespace Content.Server.Weapon.Melee
var targets = new[] { target };
SendAnimation(comp.ClickArc, angle, args.User, owner, targets, comp.ClickAttackEffect, false);
RaiseLocalEvent(target.Uid, new AttackedEvent(args.Used, args.User, args.ClickLocation));
_damageableSystem.TryChangeDamage(target.Uid,
DamageSpecifier.ApplyModifierSets(comp.Damage, hitEvent.ModifiersList));
SoundSystem.Play(Filter.Pvs(owner), comp.HitSound.GetSound(), target);
@@ -156,6 +158,8 @@ namespace Content.Server.Weapon.Melee
foreach (var entity in hitEntities)
{
RaiseLocalEvent(entity.Uid, new AttackedEvent(args.Used, args.User, args.ClickLocation));
_damageableSystem.TryChangeDamage(entity.Uid,
DamageSpecifier.ApplyModifierSets(comp.Damage, hitEvent.ModifiersList));
}

View File

@@ -1,8 +1,10 @@
using System.Threading.Tasks;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Shared.Construction
{
[ImplicitDataDefinitionForInheritors]
public interface IGraphAction
{
Task PerformAction(IEntity entity, IEntity? user);

View File

@@ -0,0 +1,10 @@
using Robust.Shared.GameObjects;
namespace Content.Shared.Electrocution
{
[RegisterComponent]
public sealed class ElectrocutedComponent : Component
{
public override string Name => "Electrocuted";
}
}

View File

@@ -0,0 +1,32 @@
using Robust.Shared.GameObjects;
namespace Content.Shared.Electrocution
{
public class ElectrocutionAttemptEvent : CancellableEntityEventArgs
{
public readonly EntityUid TargetUid;
public readonly EntityUid? SourceUid;
public float SiemensCoefficient = 1f;
public ElectrocutionAttemptEvent(EntityUid targetUid, EntityUid? sourceUid, float siemensCoefficient)
{
TargetUid = targetUid;
SourceUid = sourceUid;
SiemensCoefficient = siemensCoefficient;
}
}
public class ElectrocutedEvent : EntityEventArgs
{
public readonly EntityUid TargetUid;
public readonly EntityUid? SourceUid;
public readonly float SiemensCoefficient;
public ElectrocutedEvent(EntityUid targetUid, EntityUid? sourceUid, float siemensCoefficient)
{
TargetUid = targetUid;
SourceUid = sourceUid;
SiemensCoefficient = siemensCoefficient;
}
}
}

View File

@@ -0,0 +1,35 @@
using System;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Shared.Electrocution
{
[Friend(typeof(SharedElectrocutionSystem))]
[RegisterComponent, NetworkedComponent]
public class InsulatedComponent : Component
{
public override string Name => "Insulated";
/// <summary>
/// Siemens coefficient. Zero means completely insulated.
/// </summary>
[DataField("coefficient")]
public float SiemensCoefficient { get; set; } = 0f;
}
// Technically, people could cheat and figure out which budget insulated gloves are gud and which ones are bad.
// We might want to rethink this a little bit.
[NetSerializable, Serializable]
public class InsulatedComponentState : ComponentState
{
public float SiemensCoefficient { get; private set; }
public InsulatedComponentState(float siemensCoefficient)
{
SiemensCoefficient = siemensCoefficient;
}
}
}

View File

@@ -0,0 +1,45 @@
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
namespace Content.Shared.Electrocution
{
public abstract class SharedElectrocutionSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<InsulatedComponent, ElectrocutionAttemptEvent>(OnInsulatedElectrocutionAttempt);
SubscribeLocalEvent<InsulatedComponent, ComponentGetState>(OnInsulatedGetState);
SubscribeLocalEvent<InsulatedComponent, ComponentHandleState>(OnInsulatedHandleState);
}
public void SetInsulatedSiemensCoefficient(EntityUid uid, float siemensCoefficient, InsulatedComponent? insulated = null)
{
if (!Resolve(uid, ref insulated))
return;
insulated.SiemensCoefficient = siemensCoefficient;
insulated.Dirty();
}
private void OnInsulatedElectrocutionAttempt(EntityUid uid, InsulatedComponent insulated, ElectrocutionAttemptEvent args)
{
args.SiemensCoefficient *= insulated.SiemensCoefficient;
}
private void OnInsulatedGetState(EntityUid uid, InsulatedComponent insulated, ref ComponentGetState args)
{
args.State = new InsulatedComponentState(insulated.SiemensCoefficient);
}
private void OnInsulatedHandleState(EntityUid uid, InsulatedComponent insulated, ref ComponentHandleState args)
{
if (args.Current is not InsulatedComponentState state)
return;
insulated.SiemensCoefficient = state.SiemensCoefficient;
}
}
}

View File

@@ -57,8 +57,10 @@ namespace Content.Shared.Jittering
/// <param name="frequency">Frequency for jittering. See <see cref="MaxFrequency"/> and <see cref="MinFrequency"/>.</param>
/// <param name="forceValueChange">Whether to change any existing jitter value even if they're greater than the ones we're setting.</param>
/// <param name="status">The status effects component to modify.</param>
/// <param name="alerts">The alerts component.</param>
public void DoJitter(EntityUid uid, TimeSpan time, float amplitude = 10f, float frequency = 4f, bool forceValueChange = false,
StatusEffectsComponent? status=null)
StatusEffectsComponent? status = null,
SharedAlertsComponent? alerts = null)
{
if (!Resolve(uid, ref status, false))
return;
@@ -66,7 +68,7 @@ namespace Content.Shared.Jittering
amplitude = Math.Clamp(amplitude, MinAmplitude, MaxAmplitude);
frequency = Math.Clamp(frequency, MinFrequency, MaxFrequency);
if (StatusEffects.TryAddStatusEffect<JitteringComponent>(uid, "Jitter", time, status))
if (StatusEffects.TryAddStatusEffect<JitteringComponent>(uid, "Jitter", time, status, alerts))
{
var jittering = EntityManager.GetComponent<JitteringComponent>(uid);

View File

@@ -73,4 +73,32 @@ namespace Content.Shared.Weapons.Melee
ClickLocation = clickLocation;
}
}
/// <summary>
/// Event raised on entities that have been attacked.
/// </summary>
public class AttackedEvent : EntityEventArgs
{
/// <summary>
/// Entity used to attack, for broadcast purposes.
/// </summary>
public IEntity Used { get; }
/// <summary>
/// Entity that triggered the attack.
/// </summary>
public IEntity User { get; }
/// <summary>
/// The original location that was clicked by the user.
/// </summary>
public EntityCoordinates ClickLocation { get; }
public AttackedEvent(IEntity used, IEntity user, EntityCoordinates clickLocation)
{
Used = used;
User = user;
ClickLocation = clickLocation;
}
}
}

View File

@@ -0,0 +1,3 @@
electrocuted-component-mob-shocked-by-source-popup-others = { CAPITALIZE(THE($mob)) } is shocked by { THE($source) }!
electrocuted-component-mob-shocked-popup-others = { CAPITALIZE(THE($mob)) } is shocked!
electrocuted-component-mob-shocked-popup-player = You feel a powerful shock coursing through your body!

View File

@@ -120,3 +120,30 @@
- type: Clothing
sprite: Clothing/Hands/Gloves/Color/yellow.rsi
HeatResistance: 1400
- type: Insulated
- type: entity
parent: ClothingHandsGlovesColorYellow
id: ClothingHandsGlovesColorYellowCheap
name: budget insulated gloves
description: These gloves are cheap knockoffs of the coveted ones - no way this can end badly.
components:
- type: Clothing
HeatResistance: 0
- type: Insulated
- type: RandomInsulation
# Why repeated numbers? So some numbers are more common, of course!
list:
- 0
- 0
- 0
- 0.5
- 0.5
- 0.5
- 0.75
- 1.25
- 1.25
- 1.5
- 1.5
- 1.5
- 1.5

View File

@@ -59,6 +59,7 @@
- type: Clothing
sprite: Clothing/Hands/Gloves/captain.rsi
HeatResistance: 1400
- type: Insulated
- type: entity
parent: ClothingHandsBase
@@ -126,6 +127,8 @@
sprite: Clothing/Hands/Gloves/spaceninja.rsi
- type: Clothing
sprite: Clothing/Hands/Gloves/spaceninja.rsi
HeatResistance: 1400
- type: Insulated
- type: entity
parent: ClothingHandsBase

View File

@@ -68,6 +68,7 @@
- KnockedDown
- SlowedDown
- Stutter
- Electrocution
# Other
- type: Inventory
- type: Clickable

View File

@@ -30,6 +30,12 @@
- !type:DoActsBehavior
acts: ["Destruction"]
- type: SubFloorHide
- type: Electrified
onBump: false
requirePower: true
highVoltageNode: power
mediumVoltageNode: power
lowVoltageNode: power
- type: CableVis
node: power

View File

@@ -23,6 +23,36 @@
- type: Damageable
damageContainer: Inorganic
damageModifierSet: FlimsyMetallic
- type: PowerConsumer
- type: Electrified
requirePower: true
noWindowInTile: true
highVoltageNode: high
mediumVoltageNode: medium
lowVoltageNode: low
- type: NodeContainer
nodes:
high:
!type:CableDeviceNode
nodeGroupID: HVPower
medium:
!type:CableDeviceNode
nodeGroupID: MVPower
low:
!type:CableDeviceNode
nodeGroupID: Apc
- type: Physics
bodyType: Static
fixtures:
- shape:
!type:PhysShapeAabb
bounds: "-0.5,-0.5,0.5,0.5"
layer:
- Opaque
- Impassable
- MobImpassable
- VaultImpassable
- SmallImpassable
- type: Destructible
thresholds:
- trigger:
@@ -36,7 +66,7 @@
- type: entity
id: GrilleBroken
parent: Grille
parent: BaseStructure
name: grille
description: A flimsy framework of iron rods. It has seen better days.
components:
@@ -59,12 +89,15 @@
fixtures:
- shape:
!type:PhysShapeAabb
bounds: "-0.45,-0.45,0.45,0.45"
bounds: "-0.5,-0.5,0.5,0.5"
mass: 50
layer:
- Passable
mask:
- Passable
- type: Damageable
damageContainer: Inorganic
damageModifierSet: FlimsyMetallic
- type: Destructible
thresholds:
- trigger:

View File

@@ -0,0 +1,44 @@
# Special entity used to attach to power networks as load when somebody gets electrocuted.
- type: entity
id: VirtualElectrocutionLoadHVPower
name: ELECTROCUTION ENTITY YOU SHOULD NOT SEE THIS
abstract: true
components:
- type: NodeContainer
nodes:
electrocution: !type:ElectrocutionNode
nodeGroupID: HVPower
- type: PowerConsumer
voltage: High
drawRate: 50000
- type: Electrocution
- type: entity
id: VirtualElectrocutionLoadMVPower
name: ELECTROCUTION ENTITY YOU SHOULD NOT SEE THIS
abstract: true
components:
- type: NodeContainer
nodes:
electrocution: !type:ElectrocutionNode
nodeGroupID: MVPower
- type: PowerConsumer
voltage: Medium
drawRate: 50000
- type: Electrocution
- type: entity
id: VirtualElectrocutionLoadApc
name: ELECTROCUTION ENTITY YOU SHOULD NOT SEE THIS
abstract: true
components:
- type: NodeContainer
nodes:
electrocution: !type:ElectrocutionNode
nodeGroupID: Apc
- type: PowerConsumer
voltage: Apc
drawRate: 50000
- type: Electrocution

View File

@@ -18,3 +18,6 @@
- type: statusEffect
id: Stutter
- type: statusEffect
id: Electrocution