diff --git a/Content.Server/Body/Components/RespiratorComponent.cs b/Content.Server/Body/Components/RespiratorComponent.cs index 9f080a3dd9..8e1d8dbbca 100644 --- a/Content.Server/Body/Components/RespiratorComponent.cs +++ b/Content.Server/Body/Components/RespiratorComponent.cs @@ -1,5 +1,6 @@ using Content.Server.Body.Systems; using Content.Shared.Damage; +using Robust.Shared.Audio; // WD namespace Content.Server.Body.Components { @@ -60,6 +61,15 @@ namespace Content.Server.Body.Components public float CycleDelay = 2.0f; public float AccumulatedFrametime; + + // WD start + [DataField("CPRSound")] + public SoundSpecifier CPRSound { get; set; } = new SoundPathSpecifier("/White/Audio/CPR.ogg"); + + public IPlayingAudioStream? CPRPlayingStream; + + public EntityUid? CPRPerformedBy = null; + // WD end } } diff --git a/Content.Server/Body/Systems/RespiratorSystem.cs b/Content.Server/Body/Systems/RespiratorSystem.cs index b814422181..6e190927e6 100644 --- a/Content.Server/Body/Systems/RespiratorSystem.cs +++ b/Content.Server/Body/Systems/RespiratorSystem.cs @@ -3,14 +3,28 @@ using Content.Server.Atmos; using Content.Server.Atmos.EntitySystems; using Content.Server.Body.Components; using Content.Server.Chemistry.Containers.EntitySystems; +using Content.Server.DoAfter; +using Content.Server.Nutrition.Components; // WD using Content.Server.Popups; +using Content.Shared.ActionBlocker; // WD using Content.Shared.Alert; using Content.Shared.Atmos; using Content.Shared.Body.Components; using Content.Shared.Damage; using Content.Shared.Database; +using Content.Shared.DoAfter; // WD +using Content.Shared.IdentityManagement; // WD +using Content.Shared.Interaction; // WD +using Content.Shared.Inventory; // WD +using Content.Shared.Mobs; // WD +using Content.Shared.Mobs.Components; // WD using Content.Shared.Mobs.Systems; +using Content.Shared.Popups; // WD +using Content.Shared.White.CPR.Events; // WD using JetBrains.Annotations; +using Robust.Server.Audio; // WD +using Robust.Shared.Audio; // WD +// WD removed using Robust.Shared.Timing; namespace Content.Server.Body.Systems @@ -28,6 +42,11 @@ namespace Content.Server.Body.Systems [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; + [Dependency] private readonly InventorySystem _inventorySystem = default!; // WD + [Dependency] private readonly ActionBlockerSystem _blocker = default!; // WD + [Dependency] private readonly AudioSystem _audio = default!; // WD + [Dependency] private readonly DoAfterSystem _doAfter = default!; // WD + [Dependency] private readonly DamageableSystem _damageable = default!; // WD public override void Initialize() { @@ -36,6 +55,8 @@ namespace Content.Server.Body.Systems // We want to process lung reagents before we inhale new reagents. UpdatesAfter.Add(typeof(MetabolizerSystem)); SubscribeLocalEvent(OnApplyMetabolicMultiplier); + SubscribeLocalEvent(OnHandInteract); // WD + SubscribeLocalEvent(OnCPRDoAfterEnd); // WD } public override void Update(float frameTime) @@ -209,6 +230,118 @@ namespace Content.Server.Body.Systems if (component.AccumulatedFrametime >= component.CycleDelay) component.AccumulatedFrametime = component.CycleDelay; } + + // WD start + private void OnHandInteract(EntityUid uid, RespiratorComponent component, InteractHandEvent args) + { + args.Handled = true; + + if (CanCPR(uid, component, args.User)) + DoCPR(uid, component, args.User); + } + + private bool CanCPR(EntityUid target, RespiratorComponent comp, EntityUid user) + { + if (!_blocker.CanInteract(user, target)) + return false; + + if (target == user) + return false; + + if (comp.CPRPerformedBy != null && comp.CPRPerformedBy != user) + return false; + + if (!TryComp(target, out var body)) + return false; + + if (body.Prototype?.Id is not ("Human" or "Diona" or "Slime" or "Dwarf" or "Reptilian" or "Skrell" or "Arachnid")) + return false; + + if (!TryComp(target, out MobStateComponent? targetState)) + return false; + + if (targetState.CurrentState == MobState.Dead) + { + _popupSystem.PopupEntity(Loc.GetString("cpr-too-late", ("target", Identity.Entity(target, EntityManager))), target, user); + return false; + } + + if (targetState.CurrentState != MobState.Critical) + return false; + + if (_inventorySystem.TryGetSlotEntity(user, "mask", out var maskUidUser) && + EntityManager.TryGetComponent(maskUidUser, out var blockerUser) && + blockerUser.Enabled) + { + _popupSystem.PopupEntity(Loc.GetString("cpr-mask-block-user"), user, user); + return false; + } + + if (_inventorySystem.TryGetSlotEntity(target, "mask", out var maskUidTarget) && + EntityManager.TryGetComponent(maskUidTarget, out var blockerTarget) && + blockerTarget.Enabled) + { + _popupSystem.PopupEntity(Loc.GetString("cpr-mask-block-target", ("target", Identity.Entity(target, EntityManager))), target, user); + return false; + } + + return true; + } + + private void DoCPR(EntityUid target, RespiratorComponent comp, EntityUid user) + { + + var doAfterEventArgs = new DoAfterArgs(EntityManager, user, comp.CycleDelay * 4, new CPREndedEvent(user, target), target, target: target) + { + BreakOnTargetMove = true, + BreakOnUserMove = true, + BreakOnDamage = true, + NeedHand = true, + BreakOnHandChange = true + }; + + if (!_doAfter.TryStartDoAfter(doAfterEventArgs)) + { + _popupSystem.PopupEntity(Loc.GetString("cpr-failed"), user, user); + return; + } + + comp.CPRPerformedBy = user; + + _popupSystem.PopupEntity(Loc.GetString("cpr-started", ("target", Identity.Entity(target, EntityManager)), ("user", Identity.Entity(user, EntityManager))), target, PopupType.Medium); + comp.CPRPlayingStream = _audio.PlayPvs(comp.CPRSound, target, audioParams: AudioParams.Default.WithVolume(-3f).WithLoop(true)); + + _adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(user):entity} начал произовдить СЛР на {ToPrettyString(target):entity}"); + } + + private void OnCPRDoAfterEnd(EntityUid uid, RespiratorComponent component, CPREndedEvent args) + { + if (args.Handled) + return; + + if (args.Cancelled || !TryComp(args.Target, out var targetState) || targetState!.CurrentState != MobState.Critical) + { + component.CPRPlayingStream?.Stop(); + component.CPRPerformedBy = null; + _popupSystem.PopupEntity(Loc.GetString("cpr-failed"), args.User, args.User); + _adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(args.User):entity} не удалось произвести СЛР на {ToPrettyString(args.Target):entity}"); + return; + } + + args.Handled = true; + + _damageable.TryChangeDamage(uid, -component.Damage * 2, true, false); + + _popupSystem.PopupEntity(Loc.GetString("cpr-cycle-ended", ("target", Identity.Entity(uid, EntityManager)), ("user", Identity.Entity(args.User, EntityManager))), uid); + + _adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(args.User):entity} произвёл СЛР на {ToPrettyString(args.Target):entity}"); + + if (CanCPR(args.Target, component, args.User)) + args.Repeat = true; + else + component.CPRPerformedBy = null; + } + //WD end } } diff --git a/Content.Shared/White/CPR/Events/CPREndedEvent.cs b/Content.Shared/White/CPR/Events/CPREndedEvent.cs new file mode 100644 index 0000000000..18da7c7318 --- /dev/null +++ b/Content.Shared/White/CPR/Events/CPREndedEvent.cs @@ -0,0 +1,26 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.White.CPR.Events; + +[Serializable, NetSerializable] +public sealed class CPREndedEvent : DoAfterEvent +{ + [DataField("user", required: true)] + public readonly EntityUid User = default!; + + [DataField("target", required: true)] + public readonly EntityUid Target = default!; + + private CPREndedEvent() + { + } + + public CPREndedEvent(EntityUid user, EntityUid target) + { + User = user; + Target = target; + } + + public override DoAfterEvent Clone() => this; +} diff --git a/Resources/Locale/ru-RU/white/cpr/cpr.ftl b/Resources/Locale/ru-RU/white/cpr/cpr.ftl new file mode 100644 index 0000000000..96d6a4a58c --- /dev/null +++ b/Resources/Locale/ru-RU/white/cpr/cpr.ftl @@ -0,0 +1,6 @@ +cpr-too-late = {CAPITALIZE($target)} мертво +cpr-mask-block-user = Не могу сделать СЛР в маске +cpr-mask-block-target = Сначала нужно снять маску с {$target} +cpr-failed = Не удалось произвести СЛР +cpr-started = {$user} начал производить СЛР на {$target} +cpr-cycle-ended = {$user} произвёл СЛР на {$target} diff --git a/Resources/White/Audio/CPR.ogg b/Resources/White/Audio/CPR.ogg new file mode 100644 index 0000000000..e4204d2e7a Binary files /dev/null and b/Resources/White/Audio/CPR.ogg differ