diff --git a/Content.Client/_Emberfall/Medical/CPR/CPRSystem.cs b/Content.Client/_Emberfall/Medical/CPR/CPRSystem.cs new file mode 100644 index 00000000000..2e2fe0a907c --- /dev/null +++ b/Content.Client/_Emberfall/Medical/CPR/CPRSystem.cs @@ -0,0 +1,6 @@ +using Content.Shared._Emberfall.Medical.CPR.Systems; + +namespace Content.Client._Emberfall.Medical.CPR; + +// ReSharper disable InconsistentNaming +public sealed class CPRSystem : SharedCPRSystem; diff --git a/Content.Server/_Emberfall/Medical/CPR/CPRSystem.cs b/Content.Server/_Emberfall/Medical/CPR/CPRSystem.cs new file mode 100644 index 00000000000..f36a6990f13 --- /dev/null +++ b/Content.Server/_Emberfall/Medical/CPR/CPRSystem.cs @@ -0,0 +1,6 @@ +using Content.Shared._Emberfall.Medical.CPR.Systems; + +namespace Content.Server._Emberfall.Medical.CPR; + +// ReSharper disable InconsistentNaming +public sealed class CPRSystem : SharedCPRSystem; diff --git a/Content.Shared/_Emberfall/Medical/CPR/Components/CPRReceivedComponent.cs b/Content.Shared/_Emberfall/Medical/CPR/Components/CPRReceivedComponent.cs new file mode 100644 index 00000000000..84420bb3707 --- /dev/null +++ b/Content.Shared/_Emberfall/Medical/CPR/Components/CPRReceivedComponent.cs @@ -0,0 +1,15 @@ +using Content.Shared._Emberfall.Medical.CPR.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared._Emberfall.Medical.CPR.Components; + +// ReSharper disable InconsistentNaming +[RegisterComponent, NetworkedComponent, Access(typeof(SharedCPRSystem))] +[AutoGenerateComponentState, AutoGenerateComponentPause] +public sealed partial class CPRReceivedComponent : Component +{ + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoNetworkedField, AutoPausedField] + public TimeSpan Last; +} diff --git a/Content.Shared/_Emberfall/Medical/CPR/Components/ReceivingCPRComponent.cs b/Content.Shared/_Emberfall/Medical/CPR/Components/ReceivingCPRComponent.cs new file mode 100644 index 00000000000..f1637ad91cd --- /dev/null +++ b/Content.Shared/_Emberfall/Medical/CPR/Components/ReceivingCPRComponent.cs @@ -0,0 +1,8 @@ +using Content.Shared._Emberfall.Medical.CPR.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared._Emberfall.Medical.CPR.Components; + +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedCPRSystem))] +public sealed partial class ReceivingCPRComponent : Component; diff --git a/Content.Shared/_Emberfall/Medical/CPR/Events/CPRDoAfterEvent.cs b/Content.Shared/_Emberfall/Medical/CPR/Events/CPRDoAfterEvent.cs new file mode 100644 index 00000000000..e7e06fe2b8b --- /dev/null +++ b/Content.Shared/_Emberfall/Medical/CPR/Events/CPRDoAfterEvent.cs @@ -0,0 +1,8 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared._Emberfall.Medical.CPR.Events; + +// ReSharper disable InconsistentNaming +[Serializable, NetSerializable] +public sealed partial class CPRDoAfterEvent : SimpleDoAfterEvent; diff --git a/Content.Shared/_Emberfall/Medical/CPR/Events/PerformCPRAttemptEvent.cs b/Content.Shared/_Emberfall/Medical/CPR/Events/PerformCPRAttemptEvent.cs new file mode 100644 index 00000000000..d3ad8bf8388 --- /dev/null +++ b/Content.Shared/_Emberfall/Medical/CPR/Events/PerformCPRAttemptEvent.cs @@ -0,0 +1,5 @@ +namespace Content.Shared._Emberfall.Medical.CPR.Events; + +// ReSharper disable InconsistentNaming +[ByRefEvent] +public record struct PerformCPRAttemptEvent(EntityUid Target, bool Cancelled = false); diff --git a/Content.Shared/_Emberfall/Medical/CPR/Events/ReceiveCPRAttemptEvent.cs b/Content.Shared/_Emberfall/Medical/CPR/Events/ReceiveCPRAttemptEvent.cs new file mode 100644 index 00000000000..cc21a8086fa --- /dev/null +++ b/Content.Shared/_Emberfall/Medical/CPR/Events/ReceiveCPRAttemptEvent.cs @@ -0,0 +1,12 @@ +using Content.Shared.Inventory; + +namespace Content.Shared._Emberfall.Medical.CPR.Events; + +// ReSharper disable InconsistentNaming +[ByRefEvent] +public record struct ReceiveCPRAttemptEvent( + EntityUid Performer, + EntityUid Target, + bool Start, + SlotFlags TargetSlots = SlotFlags.MASK, + bool Cancelled = false) : IInventoryRelayEvent; diff --git a/Content.Shared/_Emberfall/Medical/CPR/Systems/SharedCPRSystem.cs b/Content.Shared/_Emberfall/Medical/CPR/Systems/SharedCPRSystem.cs new file mode 100644 index 00000000000..66cff3e1bab --- /dev/null +++ b/Content.Shared/_Emberfall/Medical/CPR/Systems/SharedCPRSystem.cs @@ -0,0 +1,202 @@ +using Content.Shared._Emberfall.Medical.CPR.Components; +using Content.Shared._Emberfall.Medical.CPR.Events; +using Content.Shared.Atmos.Rotting; +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.DoAfter; +using Content.Shared.FixedPoint; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Humanoid; +using Content.Shared.Interaction; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Content.Shared.Popups; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared._Emberfall.Medical.CPR.Systems; + +// ReSharper disable InconsistentNaming +public abstract class SharedCPRSystem : EntitySystem +{ + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly SharedPopupSystem _popups = default!; + [Dependency] private readonly SharedRottingSystem _rotting = default!; + + // TODO: move this to a component + private readonly ProtoId _healType = "Asphyxiation"; + + private static readonly FixedPoint2 HealAmount = FixedPoint2.New(10); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInteractHand, + [typeof(InteractionPopupSystem)]); + SubscribeLocalEvent(OnDoAfter); + + SubscribeLocalEvent(OnReceivingCPRAttempt); + SubscribeLocalEvent(OnReceivedCPRAttempt); + SubscribeLocalEvent(OnMobStateCPRAttempt); + } + + private void OnInteractHand(Entity ent, ref InteractHandEvent args) + { + if (args.Handled) + return; + + args.Handled = StartCPR(args.User, args.Target); + } + + private void OnDoAfter(Entity ent, ref CPRDoAfterEvent args) + { + var performer = args.User; + + if (args.Target != null) + RemComp(args.Target.Value); + + if (args.Cancelled || + args.Handled || + args.Target is not { } target || + !CanCPRPopup(performer, target, false, out var damage)) + return; + + args.Handled = true; + + if (_net.IsServer) + _rotting.ReduceAccumulator(target, TimeSpan.FromSeconds(7)); + + if (!TryComp(target, out DamageableComponent? damageable) || + !damageable.Damage.DamageDict.TryGetValue(_healType, out damage)) + return; + + var heal = -FixedPoint2.Min(damage, HealAmount); + var healSpecifier = new DamageSpecifier(); + healSpecifier.DamageDict.Add(_healType, heal); + _damageable.TryChangeDamage(target, healSpecifier, true); + EnsureComp(target).Last = _timing.CurTime; + + if (_net.IsClient) + return; + + // TODO RMC14 move this value to a component + var selfPopup = Loc.GetString("cpr-self-perform", ("target", target), ("seconds", 7)); + _popups.PopupEntity(selfPopup, target, performer); + + var othersPopup = Loc.GetString("cpr-other-perform", ("performer", performer), ("target", target)); + var othersFilter = Filter.Pvs(performer).RemoveWhereAttachedEntity(e => e == performer); + _popups.PopupEntity(othersPopup, performer, othersFilter, true, PopupType.Medium); + } + + private void OnReceivingCPRAttempt(Entity ent, ref ReceiveCPRAttemptEvent args) + { + args.Cancelled = true; + + if (_net.IsClient) + return; + + var popup = Loc.GetString("cpr-already-being-performed", ("target", ent.Owner)); + _popups.PopupEntity(popup, ent, args.Performer, PopupType.Medium); + } + + private void OnReceivedCPRAttempt(Entity ent, ref ReceiveCPRAttemptEvent args) + { + if (args.Start) + return; + + var target = ent.Owner; + var performer = args.Performer; + + // TODO move this value to a component + if (ent.Comp.Last > _timing.CurTime - TimeSpan.FromSeconds(7)) + { + args.Cancelled = true; + + if (_net.IsClient) + return; + + var selfPopup = Loc.GetString("cpr-self-perform-fail-received-too-recently", ("target", target)); + _popups.PopupEntity(selfPopup, target, performer, PopupType.SmallCaution); + + var othersPopup = Loc.GetString("cpr-other-perform-fail", ("performer", performer), ("target", target)); + var othersFilter = Filter.Pvs(performer).RemoveWhereAttachedEntity(e => e == performer); + _popups.PopupEntity(othersPopup, performer, othersFilter, true, PopupType.SmallCaution); + } + } + + private void OnMobStateCPRAttempt(Entity ent, ref ReceiveCPRAttemptEvent args) + { + if (args.Cancelled) + return; + + if (_mobState.IsAlive(ent) || _rotting.IsRotten(ent)) + args.Cancelled = true; + } + + private bool CanCPRPopup(EntityUid performer, EntityUid target, bool start, out FixedPoint2 damage) + { + damage = default; + + if (!HasComp(target) || !HasComp(performer)) + return false; + + var performAttempt = new PerformCPRAttemptEvent(target); + RaiseLocalEvent(performer, ref performAttempt); + + if (performAttempt.Cancelled) + return false; + + var receiveAttempt = new ReceiveCPRAttemptEvent(performer, target, start); + RaiseLocalEvent(target, ref receiveAttempt); + + if (receiveAttempt.Cancelled) + return false; + + if (!_hands.TryGetEmptyHand(performer, out _)) + return false; + + return true; + } + + private bool StartCPR(EntityUid performer, EntityUid target) + { + if (!CanCPRPopup(performer, target, true, out _)) + return false; + + EnsureComp(target); + + var doAfter = new DoAfterArgs(EntityManager, + performer, + TimeSpan.FromSeconds(4), + new CPRDoAfterEvent(), + performer, + target) + { + BreakOnMove = true, + NeedHand = true, + BlockDuplicate = true, + DuplicateCondition = DuplicateConditions.SameEvent, + }; + _doAfter.TryStartDoAfter(doAfter); + + if (_net.IsClient) + return true; + + var selfPopup = Loc.GetString("cpr-self-start-perform", ("target", target)); + _popups.PopupEntity(selfPopup, target, performer); + + var othersPopup = Loc.GetString("cpr-other-start-perform", ("performer", performer), ("target", target)); + var othersFilter = Filter.Pvs(performer).RemoveWhereAttachedEntity(e => e == performer); + _popups.PopupEntity(othersPopup, performer, othersFilter, true); + + return true; + } +} diff --git a/Resources/Locale/en-US/_emberfall/medical/cpr.ftl b/Resources/Locale/en-US/_emberfall/medical/cpr.ftl new file mode 100644 index 00000000000..3ee7d3a9510 --- /dev/null +++ b/Resources/Locale/en-US/_emberfall/medical/cpr.ftl @@ -0,0 +1,7 @@ +cpr-already-being-performed = CPR is already being performed on {$target}! +cpr-self-start-perform = You start performing CPR on {$target}! +cpr-other-start-perform = {$performer} starts performing CPR on {$target}! +cpr-self-perform = You perform CPR on {$target}. Repeat at least every {$seconds} seconds. +cpr-other-perform = {$performer} performs CPR on {$target}. +cpr-self-perform-fail-received-too-recently = You fail to perform CPR on {$target}. Incorrect rhythm. Do it slower. +cpr-other-perform-fail = {$performer} fails to perform CPR on {$target}! diff --git a/Resources/Migrations/emberfallMigration.yml b/Resources/Migrations/emberfallMigration.yml index 9e194eb9081..a7b81fb2ecc 100644 --- a/Resources/Migrations/emberfallMigration.yml +++ b/Resources/Migrations/emberfallMigration.yml @@ -5,3 +5,6 @@ WeaponRifleLecter: WeaponRifleLecterEmberfall WeaponRifleM90GrenadeLauncher: WeaponRifleM90 WeaponSubMachineGunVector: WeaponSubMachineGunVectorEmberfall GunSafeLecter: GunSafeLightRifleLecter + +# 01-06-2025 +ComputerCloningConsole: null diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 7a34d53cb59..5e9990cd923 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -481,8 +481,8 @@ - ThermomachineFreezerMachineCircuitBoard - HellfireFreezerMachineCircuitBoard - PortableScrubberMachineCircuitBoard - - CloningPodMachineCircuitboard - - MedicalScannerMachineCircuitboard +# - CloningPodMachineCircuitboard +# - MedicalScannerMachineCircuitboard - CryoPodMachineCircuitboard - VaccinatorMachineCircuitboard - DiagnoserMachineCircuitboard @@ -509,7 +509,7 @@ - RadarConsoleCircuitboard - TechDiskComputerCircuitboard - DawInstrumentMachineCircuitboard - - CloningConsoleComputerCircuitboard +# - CloningConsoleComputerCircuitboard - StasisBedMachineCircuitboard - OreProcessorIndustrialMachineCircuitboard - CargoTelepadMachineCircuitboard diff --git a/Resources/Prototypes/Procedural/salvage_rewards.yml b/Resources/Prototypes/Procedural/salvage_rewards.yml index 7259d71529b..57d0f5c844d 100644 --- a/Resources/Prototypes/Procedural/salvage_rewards.yml +++ b/Resources/Prototypes/Procedural/salvage_rewards.yml @@ -31,9 +31,9 @@ CratePartsT3T4: 0.5 TechnologyDiskRare: 0.5 # cloning boards - CloningPodMachineCircuitboard: 0.5 - MedicalScannerMachineCircuitboard: 0.5 - CloningConsoleComputerCircuitboard: 0.5 +# CloningPodMachineCircuitboard: 0.5 +# MedicalScannerMachineCircuitboard: 0.5 +# CloningConsoleComputerCircuitboard: 0.5 BiomassReclaimerMachineCircuitboard: 0.5 # basic weapons Machete: 0.25 diff --git a/Resources/Prototypes/Recipes/Lathes/electronics.yml b/Resources/Prototypes/Recipes/Lathes/electronics.yml index db6f80fa3dc..359df934e09 100644 --- a/Resources/Prototypes/Recipes/Lathes/electronics.yml +++ b/Resources/Prototypes/Recipes/Lathes/electronics.yml @@ -128,10 +128,10 @@ id: SignalTimerElectronics result: SignalTimerElectronics -- type: latheRecipe - parent: BaseGoldCircuitboardRecipe - id: CloningPodMachineCircuitboard - result: CloningPodMachineCircuitboard +#- type: latheRecipe # Emberfall +# parent: BaseGoldCircuitboardRecipe +# id: CloningPodMachineCircuitboard +# result: CloningPodMachineCircuitboard - type: latheRecipe parent: BaseGoldCircuitboardRecipe @@ -429,10 +429,10 @@ id: SolarTrackerElectronics result: SolarTrackerElectronics -- type: latheRecipe - parent: BaseCircuitboardRecipe - id: CloningConsoleComputerCircuitboard - result: CloningConsoleComputerCircuitboard +#- type: latheRecipe # Emberfall +# parent: BaseCircuitboardRecipe +# id: CloningConsoleComputerCircuitboard +# result: CloningConsoleComputerCircuitboard - type: latheRecipe parent: BaseCircuitboardRecipe