1
0

ElectrocutionSystem.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. using Content.Server.Administration.Logs;
  2. using Content.Server.Light.Components;
  3. using Content.Server.NodeContainer;
  4. using Content.Server.NodeContainer.EntitySystems;
  5. using Content.Server.NodeContainer.NodeGroups;
  6. using Content.Server.NodeContainer.Nodes;
  7. using Content.Server.Power.Components;
  8. using Content.Server.Power.EntitySystems;
  9. using Content.Server.Power.NodeGroups;
  10. using Content.Server.Weapons.Melee;
  11. using Content.Shared.Damage;
  12. using Content.Shared.Damage.Prototypes;
  13. using Content.Shared.Database;
  14. using Content.Shared.Electrocution;
  15. using Content.Shared.IdentityManagement;
  16. using Content.Shared.Interaction;
  17. using Content.Shared.Inventory;
  18. using Content.Shared.Jittering;
  19. using Content.Shared.Maps;
  20. using Content.Shared.Popups;
  21. using Content.Shared.Speech.EntitySystems;
  22. using Content.Shared.StatusEffect;
  23. using Content.Shared.Stunnable;
  24. using Content.Shared.Tag;
  25. using Content.Shared.Weapons.Melee.Events;
  26. using Robust.Shared.Audio;
  27. using Robust.Shared.Audio.Systems;
  28. using Robust.Shared.Map;
  29. using Robust.Shared.Physics.Events;
  30. using Robust.Shared.Player;
  31. using Robust.Shared.Prototypes;
  32. using Robust.Shared.Random;
  33. using PullableComponent = Content.Shared.Movement.Pulling.Components.PullableComponent;
  34. using PullerComponent = Content.Shared.Movement.Pulling.Components.PullerComponent;
  35. namespace Content.Server.Electrocution;
  36. public sealed class ElectrocutionSystem : SharedElectrocutionSystem
  37. {
  38. [Dependency] private readonly IAdminLogManager _adminLogger = default!;
  39. [Dependency] private readonly IMapManager _mapManager = default!;
  40. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  41. [Dependency] private readonly IRobustRandom _random = default!;
  42. [Dependency] private readonly DamageableSystem _damageable = default!;
  43. [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
  44. [Dependency] private readonly MeleeWeaponSystem _meleeWeapon = default!;
  45. [Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
  46. [Dependency] private readonly NodeGroupSystem _nodeGroup = default!;
  47. [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
  48. [Dependency] private readonly SharedAudioSystem _audio = default!;
  49. [Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
  50. [Dependency] private readonly SharedJitteringSystem _jittering = default!;
  51. [Dependency] private readonly SharedPopupSystem _popup = default!;
  52. [Dependency] private readonly SharedStunSystem _stun = default!;
  53. [Dependency] private readonly SharedStutteringSystem _stuttering = default!;
  54. [Dependency] private readonly TagSystem _tag = default!;
  55. [Dependency] private readonly MetaDataSystem _metaData = default!;
  56. [ValidatePrototypeId<StatusEffectPrototype>]
  57. private const string StatusEffectKey = "Electrocution";
  58. [ValidatePrototypeId<DamageTypePrototype>]
  59. private const string DamageType = "Shock";
  60. // Multiply and shift the log scale for shock damage.
  61. private const float RecursiveDamageMultiplier = 0.75f;
  62. private const float RecursiveTimeMultiplier = 0.8f;
  63. private const float ParalyzeTimeMultiplier = 1f;
  64. private const float StutteringTimeMultiplier = 1.5f;
  65. private const float JitterTimeMultiplier = 0.75f;
  66. private const float JitterAmplitude = 80f;
  67. private const float JitterFrequency = 8f;
  68. public override void Initialize()
  69. {
  70. base.Initialize();
  71. SubscribeLocalEvent<ElectrifiedComponent, StartCollideEvent>(OnElectrifiedStartCollide);
  72. SubscribeLocalEvent<ElectrifiedComponent, AttackedEvent>(OnElectrifiedAttacked);
  73. SubscribeLocalEvent<ElectrifiedComponent, InteractHandEvent>(OnElectrifiedHandInteract);
  74. SubscribeLocalEvent<ElectrifiedComponent, InteractUsingEvent>(OnElectrifiedInteractUsing);
  75. SubscribeLocalEvent<RandomInsulationComponent, MapInitEvent>(OnRandomInsulationMapInit);
  76. SubscribeLocalEvent<PoweredLightComponent, AttackedEvent>(OnLightAttacked);
  77. UpdatesAfter.Add(typeof(PowerNetSystem));
  78. }
  79. public override void Update(float frameTime)
  80. {
  81. UpdateElectrocutions(frameTime);
  82. UpdateState(frameTime);
  83. }
  84. private void UpdateElectrocutions(float frameTime)
  85. {
  86. var query = EntityQueryEnumerator<ElectrocutionComponent, PowerConsumerComponent>();
  87. while (query.MoveNext(out var uid, out var electrocution, out _))
  88. {
  89. var timePassed = Math.Min(frameTime, electrocution.TimeLeft);
  90. electrocution.TimeLeft -= timePassed;
  91. if (!MathHelper.CloseTo(electrocution.TimeLeft, 0))
  92. continue;
  93. // We tried damage scaling based on power in the past and it really wasn't good.
  94. // Various scaling types didn't fix tiders and HV grilles instantly critting players.
  95. QueueDel(uid);
  96. }
  97. }
  98. private void UpdateState(float frameTime)
  99. {
  100. var query = EntityQueryEnumerator<ActivatedElectrifiedComponent, ElectrifiedComponent, TransformComponent>();
  101. while (query.MoveNext(out var uid, out var activated, out var electrified, out var transform))
  102. {
  103. activated.TimeLeft -= frameTime;
  104. if (activated.TimeLeft <= 0 || !IsPowered(uid, electrified, transform))
  105. {
  106. _appearance.SetData(uid, ElectrifiedVisuals.ShowSparks, false);
  107. RemComp<ActivatedElectrifiedComponent>(uid);
  108. }
  109. }
  110. }
  111. private bool IsPowered(EntityUid uid, ElectrifiedComponent electrified, TransformComponent transform)
  112. {
  113. if (!electrified.Enabled)
  114. return false;
  115. if (electrified.NoWindowInTile)
  116. {
  117. var tileRef = transform.Coordinates.GetTileRef(EntityManager, _mapManager);
  118. if (tileRef != null)
  119. {
  120. foreach (var entity in _entityLookup.GetLocalEntitiesIntersecting(tileRef.Value, flags: LookupFlags.StaticSundries))
  121. {
  122. if (_tag.HasTag(entity, "Window"))
  123. return false;
  124. }
  125. }
  126. }
  127. if (electrified.UsesApcPower)
  128. {
  129. if (!this.IsPowered(uid, EntityManager))
  130. return false;
  131. }
  132. else if (electrified.RequirePower && PoweredNode(uid, electrified) == null)
  133. return false;
  134. return true;
  135. }
  136. private void OnElectrifiedStartCollide(EntityUid uid, ElectrifiedComponent electrified, ref StartCollideEvent args)
  137. {
  138. if (electrified.OnBump)
  139. TryDoElectrifiedAct(uid, args.OtherEntity, 1, electrified);
  140. }
  141. private void OnElectrifiedAttacked(EntityUid uid, ElectrifiedComponent electrified, AttackedEvent args)
  142. {
  143. if (!electrified.OnAttacked)
  144. return;
  145. if (_meleeWeapon.GetDamage(args.Used, args.User).Empty)
  146. return;
  147. TryDoElectrifiedAct(uid, args.User, 1, electrified);
  148. }
  149. private void OnElectrifiedHandInteract(EntityUid uid, ElectrifiedComponent electrified, InteractHandEvent args)
  150. {
  151. if (electrified.OnHandInteract)
  152. TryDoElectrifiedAct(uid, args.User, 1, electrified);
  153. }
  154. private void OnLightAttacked(EntityUid uid, PoweredLightComponent component, AttackedEvent args)
  155. {
  156. if (!component.CurrentLit || args.Used != args.User)
  157. return;
  158. if (_meleeWeapon.GetDamage(args.Used, args.User).Empty)
  159. return;
  160. DoCommonElectrocution(args.User, uid, component.UnarmedHitShock, component.UnarmedHitStun, false);
  161. }
  162. private void OnElectrifiedInteractUsing(EntityUid uid, ElectrifiedComponent electrified, InteractUsingEvent args)
  163. {
  164. if (!electrified.OnInteractUsing)
  165. return;
  166. var siemens = TryComp<InsulatedComponent>(args.Used, out var insulation)
  167. ? insulation.Coefficient
  168. : 1;
  169. TryDoElectrifiedAct(uid, args.User, siemens, electrified);
  170. }
  171. public bool TryDoElectrifiedAct(EntityUid uid, EntityUid targetUid,
  172. float siemens = 1,
  173. ElectrifiedComponent? electrified = null,
  174. NodeContainerComponent? nodeContainer = null,
  175. TransformComponent? transform = null)
  176. {
  177. if (!Resolve(uid, ref electrified, ref transform, false))
  178. return false;
  179. if (!IsPowered(uid, electrified, transform))
  180. return false;
  181. if (!_random.Prob(electrified.Probability))
  182. return false;
  183. EnsureComp<ActivatedElectrifiedComponent>(uid);
  184. _appearance.SetData(uid, ElectrifiedVisuals.ShowSparks, true);
  185. siemens *= electrified.SiemensCoefficient;
  186. if (!DoCommonElectrocutionAttempt(targetUid, uid, ref siemens) || siemens <= 0)
  187. return false; // If electrocution would fail, do nothing.
  188. var targets = new List<(EntityUid entity, int depth)>();
  189. GetChainedElectrocutionTargets(targetUid, targets);
  190. if (!electrified.RequirePower || electrified.UsesApcPower)
  191. {
  192. var lastRet = true;
  193. for (var i = targets.Count - 1; i >= 0; i--)
  194. {
  195. var (entity, depth) = targets[i];
  196. lastRet = TryDoElectrocution(
  197. entity,
  198. uid,
  199. (int) (electrified.ShockDamage * MathF.Pow(RecursiveDamageMultiplier, depth)),
  200. TimeSpan.FromSeconds(electrified.ShockTime * MathF.Pow(RecursiveTimeMultiplier, depth)),
  201. true,
  202. electrified.SiemensCoefficient
  203. );
  204. }
  205. return lastRet;
  206. }
  207. var node = PoweredNode(uid, electrified, nodeContainer);
  208. if (node?.NodeGroup is not IBasePowerNet)
  209. return false;
  210. var (damageScalar, timeScalar) = node.NodeGroupID switch
  211. {
  212. NodeGroupID.HVPower => (electrified.HighVoltageDamageMultiplier, electrified.HighVoltageTimeMultiplier),
  213. NodeGroupID.MVPower => (electrified.MediumVoltageDamageMultiplier, electrified.MediumVoltageTimeMultiplier),
  214. _ => (1f, 1f)
  215. };
  216. {
  217. var lastRet = true;
  218. for (var i = targets.Count - 1; i >= 0; i--)
  219. {
  220. var (entity, depth) = targets[i];
  221. lastRet = TryDoElectrocutionPowered(
  222. entity,
  223. uid,
  224. node,
  225. (int) (electrified.ShockDamage * MathF.Pow(RecursiveDamageMultiplier, depth) * damageScalar),
  226. TimeSpan.FromSeconds(electrified.ShockTime * MathF.Pow(RecursiveTimeMultiplier, depth) * timeScalar),
  227. true,
  228. electrified.SiemensCoefficient);
  229. }
  230. return lastRet;
  231. }
  232. }
  233. private Node? PoweredNode(EntityUid uid, ElectrifiedComponent electrified, NodeContainerComponent? nodeContainer = null)
  234. {
  235. if (!Resolve(uid, ref nodeContainer, false))
  236. return null;
  237. return TryNode(electrified.HighVoltageNode) ?? TryNode(electrified.MediumVoltageNode) ?? TryNode(electrified.LowVoltageNode);
  238. Node? TryNode(string? id)
  239. {
  240. if (id != null &&
  241. _nodeContainer.TryGetNode<Node>(nodeContainer, id, out var tryNode) &&
  242. tryNode.NodeGroup is IBasePowerNet { NetworkNode: { LastCombinedMaxSupply: > 0 } })
  243. {
  244. return tryNode;
  245. }
  246. return null;
  247. }
  248. }
  249. /// <inheritdoc/>
  250. public override bool TryDoElectrocution(
  251. EntityUid uid, EntityUid? sourceUid, int shockDamage, TimeSpan time, bool refresh, float siemensCoefficient = 1f,
  252. StatusEffectsComponent? statusEffects = null, bool ignoreInsulation = false)
  253. {
  254. if (!DoCommonElectrocutionAttempt(uid, sourceUid, ref siemensCoefficient, ignoreInsulation)
  255. || !DoCommonElectrocution(uid, sourceUid, shockDamage, time, refresh, siemensCoefficient, statusEffects))
  256. return false;
  257. RaiseLocalEvent(uid, new ElectrocutedEvent(uid, sourceUid, siemensCoefficient), true);
  258. return true;
  259. }
  260. private bool TryDoElectrocutionPowered(
  261. EntityUid uid,
  262. EntityUid sourceUid,
  263. Node node,
  264. int shockDamage,
  265. TimeSpan time,
  266. bool refresh,
  267. float siemensCoefficient = 1f,
  268. StatusEffectsComponent? statusEffects = null,
  269. TransformComponent? sourceTransform = null)
  270. {
  271. if (!DoCommonElectrocutionAttempt(uid, sourceUid, ref siemensCoefficient))
  272. return false;
  273. if (!DoCommonElectrocution(uid, sourceUid, shockDamage, time, refresh, siemensCoefficient, statusEffects))
  274. return false;
  275. // Coefficient needs to be higher than this to do a powered electrocution!
  276. if (siemensCoefficient <= 0.5f)
  277. return true;
  278. if (!Resolve(sourceUid, ref sourceTransform)) // This shouldn't really happen, but just in case...
  279. return true;
  280. var electrocutionEntity = Spawn($"VirtualElectrocutionLoad{node.NodeGroupID}", sourceTransform.Coordinates);
  281. var nodeContainer = Comp<NodeContainerComponent>(electrocutionEntity);
  282. if (!_nodeContainer.TryGetNode<ElectrocutionNode>(nodeContainer, "electrocution", out var electrocutionNode))
  283. return false;
  284. var electrocutionComponent = Comp<ElectrocutionComponent>(electrocutionEntity);
  285. // This shows up in the power monitor.
  286. // Yes. Yes exactly.
  287. _metaData.SetEntityName(electrocutionEntity, MetaData(uid).EntityName);
  288. electrocutionNode.CableEntity = sourceUid;
  289. electrocutionNode.NodeName = node.Name;
  290. _nodeGroup.QueueReflood(electrocutionNode);
  291. electrocutionComponent.TimeLeft = 1f;
  292. electrocutionComponent.Electrocuting = uid;
  293. electrocutionComponent.Source = sourceUid;
  294. RaiseLocalEvent(uid, new ElectrocutedEvent(uid, sourceUid, siemensCoefficient), true);
  295. return true;
  296. }
  297. private bool DoCommonElectrocutionAttempt(EntityUid uid, EntityUid? sourceUid, ref float siemensCoefficient, bool ignoreInsulation = false)
  298. {
  299. var attemptEvent = new ElectrocutionAttemptEvent(uid, sourceUid, siemensCoefficient,
  300. ignoreInsulation ? SlotFlags.NONE : ~SlotFlags.POCKET);
  301. RaiseLocalEvent(uid, attemptEvent, true);
  302. // Cancel the electrocution early, so we don't recursively electrocute anything.
  303. if (attemptEvent.Cancelled)
  304. return false;
  305. siemensCoefficient = attemptEvent.SiemensCoefficient;
  306. return true;
  307. }
  308. private bool DoCommonElectrocution(EntityUid uid, EntityUid? sourceUid,
  309. int? shockDamage, TimeSpan time, bool refresh, float siemensCoefficient = 1f,
  310. StatusEffectsComponent? statusEffects = null)
  311. {
  312. if (siemensCoefficient <= 0)
  313. return false;
  314. if (shockDamage != null)
  315. {
  316. shockDamage = (int) (shockDamage * siemensCoefficient);
  317. if (shockDamage.Value <= 0)
  318. return false;
  319. }
  320. if (!Resolve(uid, ref statusEffects, false) ||
  321. !_statusEffects.CanApplyEffect(uid, StatusEffectKey, statusEffects))
  322. {
  323. return false;
  324. }
  325. if (!_statusEffects.TryAddStatusEffect<ElectrocutedComponent>(uid, StatusEffectKey, time, refresh, statusEffects))
  326. return false;
  327. var shouldStun = siemensCoefficient > 0.5f;
  328. if (shouldStun)
  329. _stun.TryParalyze(uid, time * ParalyzeTimeMultiplier, refresh, statusEffects);
  330. // TODO: Sparks here.
  331. if (shockDamage is { } dmg)
  332. {
  333. var actual = _damageable.TryChangeDamage(uid,
  334. new DamageSpecifier(_prototypeManager.Index<DamageTypePrototype>(DamageType), dmg), origin: sourceUid);
  335. if (actual != null)
  336. {
  337. _adminLogger.Add(LogType.Electrocution,
  338. $"{ToPrettyString(uid):entity} received {actual.GetTotal():damage} powered electrocution damage{(sourceUid != null ? " from " + ToPrettyString(sourceUid.Value) : ""):source}");
  339. }
  340. }
  341. _stuttering.DoStutter(uid, time * StutteringTimeMultiplier, refresh, statusEffects);
  342. _jittering.DoJitter(uid, time * JitterTimeMultiplier, refresh, JitterAmplitude, JitterFrequency, true, statusEffects);
  343. _popup.PopupEntity(Loc.GetString("electrocuted-component-mob-shocked-popup-player"), uid, uid);
  344. var filter = Filter.PvsExcept(uid, entityManager: EntityManager);
  345. var identifiedUid = Identity.Entity(uid, ent: EntityManager);
  346. // TODO: Allow being able to pass EntityUid to Loc...
  347. if (sourceUid != null)
  348. {
  349. _popup.PopupEntity(Loc.GetString("electrocuted-component-mob-shocked-by-source-popup-others",
  350. ("mob", identifiedUid), ("source", (sourceUid.Value))), uid, filter, true);
  351. PlayElectrocutionSound(uid, sourceUid.Value);
  352. }
  353. else
  354. {
  355. _popup.PopupEntity(Loc.GetString("electrocuted-component-mob-shocked-popup-others",
  356. ("mob", identifiedUid)), uid, filter, true);
  357. }
  358. return true;
  359. }
  360. private void GetChainedElectrocutionTargets(EntityUid source, List<(EntityUid entity, int depth)> all)
  361. {
  362. var visited = new HashSet<EntityUid>();
  363. GetChainedElectrocutionTargetsRecurse(source, 1, visited, all);
  364. }
  365. private void GetChainedElectrocutionTargetsRecurse(
  366. EntityUid entity,
  367. int depth,
  368. HashSet<EntityUid> visited,
  369. List<(EntityUid entity, int depth)> all)
  370. {
  371. all.Add((entity, depth));
  372. visited.Add(entity);
  373. if (TryComp<PullableComponent>(entity, out var pullable) &&
  374. pullable.Puller is { Valid: true } pullerId &&
  375. !visited.Contains(pullerId))
  376. {
  377. GetChainedElectrocutionTargetsRecurse(pullerId, depth + 1, visited, all);
  378. }
  379. if (TryComp<PullerComponent>(entity, out var puller) &&
  380. puller.Pulling is { Valid: true } pullingId &&
  381. !visited.Contains(pullingId))
  382. {
  383. GetChainedElectrocutionTargetsRecurse(pullingId, depth + 1, visited, all);
  384. }
  385. }
  386. private void OnRandomInsulationMapInit(EntityUid uid, RandomInsulationComponent randomInsulation,
  387. MapInitEvent args)
  388. {
  389. if (!TryComp<InsulatedComponent>(uid, out var insulated))
  390. return;
  391. if (randomInsulation.List.Length == 0)
  392. return;
  393. SetInsulatedSiemensCoefficient(uid, _random.Pick(randomInsulation.List), insulated);
  394. }
  395. private void PlayElectrocutionSound(EntityUid targetUid, EntityUid sourceUid, ElectrifiedComponent? electrified = null)
  396. {
  397. if (!Resolve(sourceUid, ref electrified, false) || !electrified.PlaySoundOnShock)
  398. {
  399. return;
  400. }
  401. _audio.PlayPvs(electrified.ShockNoises, targetUid, AudioParams.Default.WithVolume(electrified.ShockVolume));
  402. }
  403. }