From d47e001b1840d52c8d9560e3540da7a4f08471a0 Mon Sep 17 00:00:00 2001 From: Francesco Date: Sun, 25 Dec 2022 12:35:51 +0100 Subject: [PATCH] feat: Medbay cryo pods (#11349) Fixes https://github.com/space-wizards/space-station-14/issues/11245 --- .../Medical/Cryogenics/CryoPodComponent.cs | 7 + .../Medical/Cryogenics/CryoPodSystem.cs | 83 ++++++ .../Medical/Components/CryoPodComponent.cs | 16 ++ .../Medical/CryoPodEjectLockWireAction.cs | 59 ++++ Content.Server/Medical/CryoPodSystem.cs | 267 ++++++++++++++++++ Content.Server/Medical/InsideCryoPodSystem.cs | 47 +++ ...nerTemperatureDamageThresholdsComponent.cs | 13 + .../Components/TemperatureComponent.cs | 12 + .../Temperature/Systems/TemperatureSystem.cs | 170 +++++++++-- .../Cryogenics/ActiveCryoPodComponent.cs | 9 + .../Medical/Cryogenics/CryoPodWireStatus.cs | 10 + .../Cryogenics/InsideCryoPodComponent.cs | 12 + .../Cryogenics/SharedCryoPodComponent.cs | 105 +++++++ .../Medical/Cryogenics/SharedCryoPodSystem.cs | 169 +++++++++++ .../Cryogenics/SharedInsideCryoPodSystem.cs | 29 ++ .../Standing/StandingStateSystem.cs | 14 +- .../medical/components/cryo-pod-component.ftl | 7 + .../Catalog/Research/technologies.yml | 5 +- .../Circuitboards/Machine/production.yml | 18 ++ .../Structures/Machines/Medical/cryo_pod.yml | 104 +++++++ .../Entities/Structures/Machines/lathe.yml | 1 + .../Prototypes/Recipes/Lathes/electronics.yml | 10 + Resources/Prototypes/Wires/layouts.yml | 7 + .../Machines/cryogenics.rsi/cover-off.png | Bin 0 -> 751 bytes .../Machines/cryogenics.rsi/cover-on.png | Bin 0 -> 6278 bytes .../Machines/cryogenics.rsi/meta.json | 41 +++ .../Machines/cryogenics.rsi/pod-off.png | Bin 0 -> 1612 bytes .../Machines/cryogenics.rsi/pod-on.png | Bin 0 -> 2409 bytes .../Machines/cryogenics.rsi/pod-open.png | Bin 0 -> 1554 bytes .../Machines/cryogenics.rsi/pod-panel.png | Bin 0 -> 296 bytes 30 files changed, 1188 insertions(+), 27 deletions(-) create mode 100644 Content.Client/Medical/Cryogenics/CryoPodComponent.cs create mode 100644 Content.Client/Medical/Cryogenics/CryoPodSystem.cs create mode 100644 Content.Server/Medical/Components/CryoPodComponent.cs create mode 100644 Content.Server/Medical/CryoPodEjectLockWireAction.cs create mode 100644 Content.Server/Medical/CryoPodSystem.cs create mode 100644 Content.Server/Medical/InsideCryoPodSystem.cs create mode 100644 Content.Server/Temperature/Components/ContainerTemperatureDamageThresholdsComponent.cs create mode 100644 Content.Shared/Medical/Cryogenics/ActiveCryoPodComponent.cs create mode 100644 Content.Shared/Medical/Cryogenics/CryoPodWireStatus.cs create mode 100644 Content.Shared/Medical/Cryogenics/InsideCryoPodComponent.cs create mode 100644 Content.Shared/Medical/Cryogenics/SharedCryoPodComponent.cs create mode 100644 Content.Shared/Medical/Cryogenics/SharedCryoPodSystem.cs create mode 100644 Content.Shared/Medical/Cryogenics/SharedInsideCryoPodSystem.cs create mode 100644 Resources/Locale/en-US/medical/components/cryo-pod-component.ftl create mode 100644 Resources/Prototypes/Entities/Structures/Machines/Medical/cryo_pod.yml create mode 100644 Resources/Textures/Structures/Machines/cryogenics.rsi/cover-off.png create mode 100644 Resources/Textures/Structures/Machines/cryogenics.rsi/cover-on.png create mode 100644 Resources/Textures/Structures/Machines/cryogenics.rsi/meta.json create mode 100644 Resources/Textures/Structures/Machines/cryogenics.rsi/pod-off.png create mode 100644 Resources/Textures/Structures/Machines/cryogenics.rsi/pod-on.png create mode 100644 Resources/Textures/Structures/Machines/cryogenics.rsi/pod-open.png create mode 100644 Resources/Textures/Structures/Machines/cryogenics.rsi/pod-panel.png diff --git a/Content.Client/Medical/Cryogenics/CryoPodComponent.cs b/Content.Client/Medical/Cryogenics/CryoPodComponent.cs new file mode 100644 index 0000000000..79a5d8a5e3 --- /dev/null +++ b/Content.Client/Medical/Cryogenics/CryoPodComponent.cs @@ -0,0 +1,7 @@ +using Content.Shared.DragDrop; +using Content.Shared.Medical.Cryogenics; + +namespace Content.Client.Medical.Cryogenics; + +[RegisterComponent] +public sealed class CryoPodComponent : SharedCryoPodComponent { } diff --git a/Content.Client/Medical/Cryogenics/CryoPodSystem.cs b/Content.Client/Medical/Cryogenics/CryoPodSystem.cs new file mode 100644 index 0000000000..0c6b86aef9 --- /dev/null +++ b/Content.Client/Medical/Cryogenics/CryoPodSystem.cs @@ -0,0 +1,83 @@ +using Content.Shared.Destructible; +using Content.Shared.Emag.Systems; +using Content.Shared.Medical.Cryogenics; +using Content.Shared.Verbs; +using Robust.Client.GameObjects; +using DrawDepth = Content.Shared.DrawDepth.DrawDepth; + +namespace Content.Client.Medical.Cryogenics; + +public sealed class CryoPodSystem: SharedCryoPodSystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent>(AddAlternativeVerbs); + SubscribeLocalEvent(OnEmagged); + SubscribeLocalEvent(DoInsertCryoPod); + SubscribeLocalEvent(DoInsertCancelCryoPod); + SubscribeLocalEvent(OnCryoPodPryFinished); + SubscribeLocalEvent(OnCryoPodPryInterrupted); + + SubscribeLocalEvent(OnAppearanceChange); + SubscribeLocalEvent(OnCryoPodInsertion); + SubscribeLocalEvent(OnCryoPodRemoval); + } + + private void OnCryoPodInsertion(EntityUid uid, InsideCryoPodComponent component, ComponentStartup args) + { + if (!TryComp(uid, out var spriteComponent)) + { + return; + } + + component.PreviousOffset = spriteComponent.Offset; + spriteComponent.Offset = new Vector2(0, 1); + } + + private void OnCryoPodRemoval(EntityUid uid, InsideCryoPodComponent component, ComponentRemove args) + { + if (!TryComp(uid, out var spriteComponent)) + { + return; + } + + spriteComponent.Offset = component.PreviousOffset; + } + + private void OnAppearanceChange(EntityUid uid, SharedCryoPodComponent component, ref AppearanceChangeEvent args) + { + if (args.Sprite == null) + { + return; + } + + if (!args.Component.TryGetData(SharedCryoPodComponent.CryoPodVisuals.ContainsEntity, out bool isOpen) + || !args.Component.TryGetData(SharedCryoPodComponent.CryoPodVisuals.IsOn, out bool isOn)) + { + return; + } + + if (isOpen) + { + args.Sprite.LayerSetState(CryoPodVisualLayers.Base, "pod-open"); + args.Sprite.LayerSetVisible(CryoPodVisualLayers.Cover, false); + args.Sprite.DrawDepth = (int) DrawDepth.Objects; + } + else + { + args.Sprite.DrawDepth = (int) DrawDepth.Mobs; + args.Sprite.LayerSetState(CryoPodVisualLayers.Base, isOn ? "pod-on" : "pod-off"); + args.Sprite.LayerSetState(CryoPodVisualLayers.Cover, isOn ? "cover-on" : "cover-off"); + args.Sprite.LayerSetVisible(CryoPodVisualLayers.Cover, true); + } + } +} + +public enum CryoPodVisualLayers : byte +{ + Base, + Cover, +} diff --git a/Content.Server/Medical/Components/CryoPodComponent.cs b/Content.Server/Medical/Components/CryoPodComponent.cs new file mode 100644 index 0000000000..6ede61b0e4 --- /dev/null +++ b/Content.Server/Medical/Components/CryoPodComponent.cs @@ -0,0 +1,16 @@ +using Content.Server.Atmos; +using Content.Shared.Atmos; +using Content.Shared.Medical.Cryogenics; + +namespace Content.Server.Medical.Components; + +[RegisterComponent] +public sealed class CryoPodComponent: SharedCryoPodComponent +{ + /// + /// Local air buffer that will be mixed with the pipenet, if one exists, per tick. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("gasMixture")] + public GasMixture Air { get; set; } = new(Atmospherics.OneAtmosphere); +} diff --git a/Content.Server/Medical/CryoPodEjectLockWireAction.cs b/Content.Server/Medical/CryoPodEjectLockWireAction.cs new file mode 100644 index 0000000000..337184ecbd --- /dev/null +++ b/Content.Server/Medical/CryoPodEjectLockWireAction.cs @@ -0,0 +1,59 @@ +using Content.Server.Medical.Components; +using Content.Server.Wires; +using Content.Shared.Medical.Cryogenics; +using Content.Shared.Wires; + +namespace Content.Server.Medical; + +/// +/// Causes a failure in the cryo pod ejection system when cut. A crowbar will be needed to pry open the pod. +/// +[DataDefinition] +public sealed class CryoPodEjectLockWireAction: BaseWireAction +{ + [DataField("color")] + private Color _statusColor = Color.Red; + + [DataField("name")] + private string _text = "LOCK"; + + public override object? StatusKey { get; } = CryoPodWireActionKey.Key; + public override bool Cut(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var cryoPodComponent) && !cryoPodComponent.PermaLocked) + { + cryoPodComponent.Locked = true; + } + + return true; + } + + public override bool Mend(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var cryoPodComponent) && !cryoPodComponent.PermaLocked) + { + cryoPodComponent.Locked = false; + } + + return true; + } + + public override bool Pulse(EntityUid user, Wire wire) + { + return true; + } + + public override StatusLightData? GetStatusLightData(Wire wire) + { + StatusLightState lightState = StatusLightState.Off; + if (EntityManager.TryGetComponent(wire.Owner, out var cryoPodComponent) && cryoPodComponent.Locked) + { + lightState = StatusLightState.On; //TODO figure out why this doesn't get updated when the pod is emagged + } + + return new StatusLightData( + _statusColor, + lightState, + _text); + } +} diff --git a/Content.Server/Medical/CryoPodSystem.cs b/Content.Server/Medical/CryoPodSystem.cs new file mode 100644 index 0000000000..e3ba596258 --- /dev/null +++ b/Content.Server/Medical/CryoPodSystem.cs @@ -0,0 +1,267 @@ +using System.Threading; +using Content.Server.Atmos; +using Content.Server.Atmos.EntitySystems; +using Content.Server.Atmos.Piping.Components; +using Content.Server.Atmos.Piping.Unary.EntitySystems; +using Content.Server.Body.Components; +using Content.Server.Body.Systems; +using Content.Server.Chemistry.Components.SolutionManager; +using Content.Server.Chemistry.EntitySystems; +using Content.Server.Climbing; +using Content.Server.DoAfter; +using Content.Server.Medical.Components; +using Content.Server.NodeContainer; +using Content.Server.NodeContainer.NodeGroups; +using Content.Server.NodeContainer.Nodes; +using Content.Server.Power.Components; +using Content.Server.Tools; +using Content.Server.UserInterface; +using Content.Shared.Chemistry; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Destructible; +using Content.Shared.DragDrop; +using Content.Shared.Emag.Systems; +using Content.Shared.Examine; +using Content.Shared.Interaction; +using Content.Shared.Medical.Cryogenics; +using Content.Shared.MedicalScanner; +using Content.Shared.Tools.Components; +using Content.Shared.Verbs; +using Robust.Server.GameObjects; +using Robust.Shared.Timing; + +namespace Content.Server.Medical; + +public sealed partial class CryoPodSystem: SharedCryoPodSystem +{ + [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; + [Dependency] private readonly GasCanisterSystem _gasCanisterSystem = default!; + [Dependency] private readonly ClimbSystem _climbSystem = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; + [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; + [Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!; + [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; + [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; + [Dependency] private readonly ToolSystem _toolSystem = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly MetaDataSystem _metaDataSystem = default!; + [Dependency] private readonly ReactiveSystem _reactiveSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent>(AddAlternativeVerbs); + SubscribeLocalEvent(OnEmagged); + SubscribeLocalEvent(DoInsertCryoPod); + SubscribeLocalEvent(DoInsertCancelCryoPod); + SubscribeLocalEvent(OnCryoPodPryFinished); + SubscribeLocalEvent(OnCryoPodPryInterrupted); + + SubscribeLocalEvent(OnCryoPodUpdateAtmosphere); + SubscribeLocalEvent(HandleDragDropOn); + SubscribeLocalEvent(OnInteractUsing); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnPowerChanged); + SubscribeLocalEvent(OnGasAnalyzed); + SubscribeLocalEvent(OnActivateUIAttempt); + SubscribeLocalEvent(OnActivateUI); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var curTime = _gameTiming.CurTime; + var bloodStreamQuery = GetEntityQuery(); + var metaDataQuery = GetEntityQuery(); + var itemSlotsQuery = GetEntityQuery(); + var fitsInDispenserQuery = GetEntityQuery(); + var solutionContainerManagerQuery = GetEntityQuery(); + foreach (var (_, cryoPod) in EntityQuery()) + { + metaDataQuery.TryGetComponent(cryoPod.Owner, out var metaDataComponent); + if (curTime < cryoPod.NextInjectionTime + _metaDataSystem.GetPauseTime(cryoPod.Owner, metaDataComponent)) + continue; + cryoPod.NextInjectionTime = curTime + TimeSpan.FromSeconds(cryoPod.BeakerTransferTime); + + if ( + !itemSlotsQuery.TryGetComponent(cryoPod.Owner, out var itemSlotsComponent) + || !fitsInDispenserQuery.TryGetComponent(cryoPod.Owner, out var fitsInDispenserComponent) + || !solutionContainerManagerQuery.TryGetComponent(cryoPod.Owner, out var solutionContainerManagerComponent)) + { + continue; + } + var container = _itemSlotsSystem.GetItemOrNull(cryoPod.Owner, cryoPod.SolutionContainerName, itemSlotsComponent); + var patient = cryoPod.BodyContainer.ContainedEntity; + if (container != null + && container.Value.Valid + && patient != null + && _solutionContainerSystem.TryGetFitsInDispenser(container.Value, out var containerSolution, dispenserFits: fitsInDispenserComponent, solutionManager: solutionContainerManagerComponent)) + { + if (!bloodStreamQuery.TryGetComponent(patient, out var bloodstream)) + { + continue; + } + + var solutionToInject = _solutionContainerSystem.SplitSolution(container.Value, containerSolution, cryoPod.BeakerTransferAmount); + _bloodstreamSystem.TryAddToChemicals(patient.Value, solutionToInject, bloodstream); + _reactiveSystem.DoEntityReaction(patient.Value, solutionToInject, ReactionMethod.Injection); + } + } + } + + public override void EjectBody(EntityUid uid, SharedCryoPodComponent? cryoPodComponent) + { + if (!Resolve(uid, ref cryoPodComponent)) + return; + if (cryoPodComponent.BodyContainer.ContainedEntity is not {Valid: true} contained) + return; + base.EjectBody(uid, cryoPodComponent); + _climbSystem.ForciblySetClimbing(contained, uid); + } + + #region Interaction + + private void HandleDragDropOn(EntityUid uid, CryoPodComponent cryoPodComponent, DragDropEvent args) + { + if (cryoPodComponent.BodyContainer.ContainedEntity != null) + { + return; + } + + if (cryoPodComponent.DragDropCancelToken != null) + { + cryoPodComponent.DragDropCancelToken.Cancel(); + cryoPodComponent.DragDropCancelToken = null; + return; + } + + cryoPodComponent.DragDropCancelToken = new CancellationTokenSource(); + var doAfterArgs = new DoAfterEventArgs(args.User, cryoPodComponent.EntryDelay, cryoPodComponent.DragDropCancelToken.Token, uid, args.Dragged) + { + BreakOnDamage = true, + BreakOnStun = true, + BreakOnTargetMove = true, + BreakOnUserMove = true, + NeedHand = false, + TargetFinishedEvent = new DoInsertCryoPodEvent(args.Dragged), + TargetCancelledEvent = new DoInsertCancelledCryoPodEvent() + }; + _doAfterSystem.DoAfter(doAfterArgs); + args.Handled = true; + } + + private void OnActivateUIAttempt(EntityUid uid, CryoPodComponent cryoPodComponent, ActivatableUIOpenAttemptEvent args) + { + if (args.Cancelled) + { + return; + } + + var containedEntity = cryoPodComponent.BodyContainer.ContainedEntity; + if (containedEntity == null || containedEntity == args.User || !HasComp(uid)) + { + args.Cancel(); + } + } + + private void OnActivateUI(EntityUid uid, CryoPodComponent cryoPodComponent, AfterActivatableUIOpenEvent args) + { + _userInterfaceSystem.TrySendUiMessage( + uid, + SharedHealthAnalyzerComponent.HealthAnalyzerUiKey.Key, + new SharedHealthAnalyzerComponent.HealthAnalyzerScannedUserMessage(cryoPodComponent.BodyContainer.ContainedEntity)); + } + + private void OnInteractUsing(EntityUid uid, CryoPodComponent cryoPodComponent, InteractUsingEvent args) + { + if (args.Handled || !cryoPodComponent.Locked || cryoPodComponent.BodyContainer.ContainedEntity == null) + return; + + if (TryComp(args.Used, out ToolComponent? tool) + && tool.Qualities.Contains("Prying")) // Why aren't those enums? + { + if (cryoPodComponent.IsPrying) + return; + cryoPodComponent.IsPrying = true; + + _toolSystem.UseTool(args.Used, args.User, uid, 0f, + cryoPodComponent.PryDelay, "Prying", + new CryoPodPryFinished(), new CryoPodPryInterrupted(), uid); + + args.Handled = true; + } + } + + private void OnExamined(EntityUid uid, CryoPodComponent component, ExaminedEvent args) + { + var container = _itemSlotsSystem.GetItemOrNull(component.Owner, component.SolutionContainerName); + if (args.IsInDetailsRange && container != null && _solutionContainerSystem.TryGetFitsInDispenser(container.Value, out var containerSolution)) + { + args.PushMarkup(Loc.GetString("cryo-pod-examine", ("beaker", Name(container.Value)))); + if (containerSolution.CurrentVolume == 0) + { + args.PushMarkup(Loc.GetString("cryo-pod-empty-beaker")); + } + } + } + + private void OnPowerChanged(EntityUid uid, CryoPodComponent component, ref PowerChangedEvent args) + { + // Needed to avoid adding/removing components on a deleted entity + if (Terminating(uid)) + { + return; + } + + if (args.Powered) + { + EnsureComp(uid); + } + else + { + RemComp(uid); + _uiSystem.TryCloseAll(uid, SharedHealthAnalyzerComponent.HealthAnalyzerUiKey.Key); + } + UpdateAppearance(uid, component); + } + + #endregion + + #region Atmos handler + + private void OnCryoPodUpdateAtmosphere(EntityUid uid, CryoPodComponent cryoPod, AtmosDeviceUpdateEvent args) + { + if (!TryComp(uid, out NodeContainerComponent? nodeContainer)) + return; + + if (!nodeContainer.TryGetNode(cryoPod.PortName, out PortablePipeNode? portNode)) + return; + _atmosphereSystem.React(cryoPod.Air, portNode); + + if (portNode.NodeGroup is PipeNet {NodeCount: > 1} net) + { + _gasCanisterSystem.MixContainerWithPipeNet(cryoPod.Air, net.Air); + } + } + + private void OnGasAnalyzed(EntityUid uid, CryoPodComponent component, GasAnalyzerScanEvent args) + { + var gasMixDict = new Dictionary { { Name(uid), component.Air } }; + // If it's connected to a port, include the port side + if (TryComp(uid, out NodeContainerComponent? nodeContainer)) + { + if(nodeContainer.TryGetNode(component.PortName, out PipeNode? port)) + gasMixDict.Add(component.PortName, port.Air); + } + args.GasMixtures = gasMixDict; + } + + + #endregion +} diff --git a/Content.Server/Medical/InsideCryoPodSystem.cs b/Content.Server/Medical/InsideCryoPodSystem.cs new file mode 100644 index 0000000000..a41896b726 --- /dev/null +++ b/Content.Server/Medical/InsideCryoPodSystem.cs @@ -0,0 +1,47 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Server.Medical.Components; +using Content.Shared.Medical.Cryogenics; + +namespace Content.Server.Medical +{ + public sealed partial class CryoPodSystem + { + public override void InitializeInsideCryoPod() + { + base.InitializeInsideCryoPod(); + // Atmos overrides + SubscribeLocalEvent(OnInhaleLocation); + SubscribeLocalEvent(OnExhaleLocation); + SubscribeLocalEvent(OnGetAir); + } + + #region Atmos handlers + + private void OnGetAir(EntityUid uid, InsideCryoPodComponent component, ref AtmosExposedGetAirEvent args) + { + if (TryComp(Transform(uid).ParentUid, out var cryoPodComponent)) + { + args.Gas = cryoPodComponent.Air; + args.Handled = true; + } + } + + private void OnInhaleLocation(EntityUid uid, InsideCryoPodComponent component, InhaleLocationEvent args) + { + if (TryComp(Transform(uid).ParentUid, out var cryoPodComponent)) + { + args.Gas = cryoPodComponent.Air; + } + } + + private void OnExhaleLocation(EntityUid uid, InsideCryoPodComponent component, ExhaleLocationEvent args) + { + if (TryComp(Transform(uid).ParentUid, out var cryoPodComponent)) + { + args.Gas = cryoPodComponent.Air; + } + } + + #endregion + } +} diff --git a/Content.Server/Temperature/Components/ContainerTemperatureDamageThresholdsComponent.cs b/Content.Server/Temperature/Components/ContainerTemperatureDamageThresholdsComponent.cs new file mode 100644 index 0000000000..78a9da8088 --- /dev/null +++ b/Content.Server/Temperature/Components/ContainerTemperatureDamageThresholdsComponent.cs @@ -0,0 +1,13 @@ +namespace Content.Server.Temperature.Components; + +[RegisterComponent] +public sealed class ContainerTemperatureDamageThresholdsComponent: Component +{ + [DataField("heatDamageThreshold")] + [ViewVariables(VVAccess.ReadWrite)] + public float? HeatDamageThreshold; + + [DataField("coldDamageThreshold")] + [ViewVariables(VVAccess.ReadWrite)] + public float? ColdDamageThreshold; +} diff --git a/Content.Server/Temperature/Components/TemperatureComponent.cs b/Content.Server/Temperature/Components/TemperatureComponent.cs index 0a0a77486f..1915c317b2 100644 --- a/Content.Server/Temperature/Components/TemperatureComponent.cs +++ b/Content.Server/Temperature/Components/TemperatureComponent.cs @@ -24,6 +24,18 @@ namespace Content.Server.Temperature.Components [ViewVariables(VVAccess.ReadWrite)] public float ColdDamageThreshold = 260f; + /// + /// Overrides HeatDamageThreshold if the entity's within a parent with the TemperatureDamageThresholdsComponent component. + /// + [ViewVariables(VVAccess.ReadWrite)] + public float? ParentHeatDamageThreshold; + + /// + /// Overrides ColdDamageThreshold if the entity's within a parent with the TemperatureDamageThresholdsComponent component. + /// + [ViewVariables(VVAccess.ReadWrite)] + public float? ParentColdDamageThreshold; + [DataField("specificHeat")] [ViewVariables(VVAccess.ReadWrite)] public float SpecificHeat = 50f; diff --git a/Content.Server/Temperature/Systems/TemperatureSystem.cs b/Content.Server/Temperature/Systems/TemperatureSystem.cs index f249914bf2..b28e7909ec 100644 --- a/Content.Server/Temperature/Systems/TemperatureSystem.cs +++ b/Content.Server/Temperature/Systems/TemperatureSystem.cs @@ -11,8 +11,6 @@ using Content.Shared.Database; using Content.Shared.Inventory; using Content.Shared.Temperature; using Robust.Server.GameObjects; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; namespace Content.Server.Temperature.Systems { @@ -22,7 +20,7 @@ namespace Content.Server.Temperature.Systems [Dependency] private readonly DamageableSystem _damageableSystem = default!; [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; [Dependency] private readonly AlertsSystem _alertsSystem = default!; - [Dependency] private readonly IAdminLogManager _adminLogger= default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; /// /// All the components that will have their damage updated at the end of the tick. @@ -40,7 +38,15 @@ namespace Content.Server.Temperature.Systems SubscribeLocalEvent(EnqueueDamage); SubscribeLocalEvent(OnAtmosExposedUpdate); SubscribeLocalEvent(ServerAlert); - SubscribeLocalEvent>(OnTemperatureChangeAttempt); + SubscribeLocalEvent>( + OnTemperatureChangeAttempt); + + // Allows overriding thresholds based on the parent's thresholds. + SubscribeLocalEvent(OnParentChange); + SubscribeLocalEvent( + OnParentThresholdStartup); + SubscribeLocalEvent( + OnParentThresholdShutdown); } public override void Update(float frameTime) @@ -76,11 +82,13 @@ namespace Content.Server.Temperature.Systems float lastTemp = temperature.CurrentTemperature; float delta = temperature.CurrentTemperature - temp; temperature.CurrentTemperature = temp; - RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), true); + RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), + true); } } - public void ChangeHeat(EntityUid uid, float heatAmount, bool ignoreHeatResistance=false, TemperatureComponent? temperature = null) + public void ChangeHeat(EntityUid uid, float heatAmount, bool ignoreHeatResistance = false, + TemperatureComponent? temperature = null) { if (Resolve(uid, ref temperature)) { @@ -95,11 +103,13 @@ namespace Content.Server.Temperature.Systems temperature.CurrentTemperature += heatAmount / temperature.HeatCapacity; float delta = temperature.CurrentTemperature - lastTemp; - RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), true); + RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), + true); } } - private void OnAtmosExposedUpdate(EntityUid uid, TemperatureComponent temperature, ref AtmosExposedUpdateEvent args) + private void OnAtmosExposedUpdate(EntityUid uid, TemperatureComponent temperature, + ref AtmosExposedUpdateEvent args) { var transform = args.Transform; @@ -109,9 +119,11 @@ namespace Content.Server.Temperature.Systems var position = _transformSystem.GetGridOrMapTilePosition(uid, transform); var temperatureDelta = args.GasMixture.Temperature - temperature.CurrentTemperature; - var tileHeatCapacity = _atmosphereSystem.GetTileHeatCapacity(transform.GridUid, transform.MapUid.Value, position); - var heat = temperatureDelta * (tileHeatCapacity * temperature.HeatCapacity / (tileHeatCapacity + temperature.HeatCapacity)); - ChangeHeat(uid, heat * temperature.AtmosTemperatureTransferEfficiency, temperature: temperature ); + var tileHeatCapacity = + _atmosphereSystem.GetTileHeatCapacity(transform.GridUid, transform.MapUid.Value, position); + var heat = temperatureDelta * (tileHeatCapacity * temperature.HeatCapacity / + (tileHeatCapacity + temperature.HeatCapacity)); + ChangeHeat(uid, heat * temperature.AtmosTemperatureTransferEfficiency, temperature: temperature); } private void ServerAlert(EntityUid uid, AlertsComponent status, OnTemperatureChangeEvent args) @@ -173,42 +185,160 @@ namespace Content.Server.Temperature.Systems var y = temperature.DamageCap; var c = y * 2; - if (temperature.CurrentTemperature >= temperature.HeatDamageThreshold) + var heatDamageThreshold = temperature.ParentHeatDamageThreshold ?? temperature.HeatDamageThreshold; + var coldDamageThreshold = temperature.ParentColdDamageThreshold ?? temperature.ColdDamageThreshold; + + if (temperature.CurrentTemperature >= heatDamageThreshold) { if (!temperature.TakingDamage) { - _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(temperature.Owner):entity} started taking high temperature damage"); + _adminLogger.Add(LogType.Temperature, + $"{ToPrettyString(temperature.Owner):entity} started taking high temperature damage"); temperature.TakingDamage = true; } - var diff = Math.Abs(temperature.CurrentTemperature - temperature.HeatDamageThreshold); + var diff = Math.Abs(temperature.CurrentTemperature - heatDamageThreshold); var tempDamage = c / (1 + a * Math.Pow(Math.E, -heatK * diff)) - y; _damageableSystem.TryChangeDamage(uid, temperature.HeatDamage * tempDamage, interruptsDoAfters: false); } - else if (temperature.CurrentTemperature <= temperature.ColdDamageThreshold) + else if (temperature.CurrentTemperature <= coldDamageThreshold) { if (!temperature.TakingDamage) { - _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(temperature.Owner):entity} started taking low temperature damage"); + _adminLogger.Add(LogType.Temperature, + $"{ToPrettyString(temperature.Owner):entity} started taking low temperature damage"); temperature.TakingDamage = true; } - var diff = Math.Abs(temperature.CurrentTemperature - temperature.ColdDamageThreshold); + var diff = Math.Abs(temperature.CurrentTemperature - coldDamageThreshold); var tempDamage = - Math.Sqrt(diff * (Math.Pow(temperature.DamageCap.Double(), 2) / temperature.ColdDamageThreshold)); + Math.Sqrt(diff * (Math.Pow(temperature.DamageCap.Double(), 2) / coldDamageThreshold)); _damageableSystem.TryChangeDamage(uid, temperature.ColdDamage * tempDamage, interruptsDoAfters: false); } else if (temperature.TakingDamage) { - _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(temperature.Owner):entity} stopped taking temperature damage"); + _adminLogger.Add(LogType.Temperature, + $"{ToPrettyString(temperature.Owner):entity} stopped taking temperature damage"); temperature.TakingDamage = false; } } - private void OnTemperatureChangeAttempt(EntityUid uid, TemperatureProtectionComponent component, InventoryRelayedEvent args) + private void OnTemperatureChangeAttempt(EntityUid uid, TemperatureProtectionComponent component, + InventoryRelayedEvent args) { args.Args.TemperatureDelta *= component.Coefficient; } + + private void OnParentChange(EntityUid uid, TemperatureComponent component, + ref EntParentChangedMessage args) + { + var temperatureQuery = GetEntityQuery(); + var transformQuery = GetEntityQuery(); + var thresholdsQuery = GetEntityQuery(); + // We only need to update thresholds if the thresholds changed for the entity's ancestors. + var oldThresholds = args.OldParent != null + ? RecalculateParentThresholds(args.OldParent.Value, transformQuery, thresholdsQuery) + : (null, null); + var newThresholds = RecalculateParentThresholds(transformQuery.GetComponent(uid).ParentUid, transformQuery, thresholdsQuery); + + if (oldThresholds != newThresholds) + { + RecursiveThresholdUpdate(uid, temperatureQuery, transformQuery, thresholdsQuery); + } + } + + private void OnParentThresholdStartup(EntityUid uid, ContainerTemperatureDamageThresholdsComponent component, + ComponentStartup args) + { + RecursiveThresholdUpdate(uid, GetEntityQuery(), GetEntityQuery(), + GetEntityQuery()); + } + + private void OnParentThresholdShutdown(EntityUid uid, ContainerTemperatureDamageThresholdsComponent component, + ComponentShutdown args) + { + RecursiveThresholdUpdate(uid, GetEntityQuery(), GetEntityQuery(), + GetEntityQuery()); + } + + /// + /// Recalculate and apply parent thresholds for the root entity and all its descendant. + /// + /// + /// + /// + /// + private void RecursiveThresholdUpdate(EntityUid root, EntityQuery temperatureQuery, + EntityQuery transformQuery, + EntityQuery tempThresholdsQuery) + { + RecalculateAndApplyParentThresholds(root, temperatureQuery, transformQuery, tempThresholdsQuery); + + foreach (var child in Transform(root).ChildEntities) + { + RecursiveThresholdUpdate(child, temperatureQuery, transformQuery, tempThresholdsQuery); + } + } + + /// + /// Recalculate parent thresholds and apply them on the uid temperature component. + /// + /// + /// + /// + /// + private void RecalculateAndApplyParentThresholds(EntityUid uid, + EntityQuery temperatureQuery, EntityQuery transformQuery, + EntityQuery tempThresholdsQuery) + { + if (!temperatureQuery.TryGetComponent(uid, out var temperature)) + { + return; + } + + var newThresholds = RecalculateParentThresholds(transformQuery.GetComponent(uid).ParentUid, transformQuery, tempThresholdsQuery); + temperature.ParentHeatDamageThreshold = newThresholds.Item1; + temperature.ParentColdDamageThreshold = newThresholds.Item2; + } + + /// + /// Recalculate Parent Heat/Cold DamageThreshold by recursively checking each ancestor and fetching the + /// maximum HeatDamageThreshold and the minimum ColdDamageThreshold if any exists (aka the best value for each). + /// + /// + /// + /// + private (float?, float?) RecalculateParentThresholds( + EntityUid initialParentUid, + EntityQuery transformQuery, + EntityQuery tempThresholdsQuery) + { + // Recursively check parents for the best threshold available + var parentUid = initialParentUid; + float? newHeatThreshold = null; + float? newColdThreshold = null; + while (parentUid.IsValid()) + { + if (tempThresholdsQuery.TryGetComponent(parentUid, out var newThresholds)) + { + if (newThresholds.HeatDamageThreshold != null) + { + newHeatThreshold = Math.Max(newThresholds.HeatDamageThreshold.Value, + newHeatThreshold ?? 0); + } + + if (newThresholds.ColdDamageThreshold != null) + { + newColdThreshold = Math.Min(newThresholds.ColdDamageThreshold.Value, + newColdThreshold ?? float.MaxValue); + } + } + + parentUid = transformQuery.GetComponent(parentUid).ParentUid; + } + + return (newHeatThreshold, newColdThreshold); + } } public sealed class OnTemperatureChangeEvent : EntityEventArgs diff --git a/Content.Shared/Medical/Cryogenics/ActiveCryoPodComponent.cs b/Content.Shared/Medical/Cryogenics/ActiveCryoPodComponent.cs new file mode 100644 index 0000000000..b48353fd77 --- /dev/null +++ b/Content.Shared/Medical/Cryogenics/ActiveCryoPodComponent.cs @@ -0,0 +1,9 @@ +namespace Content.Server.Medical.Components; + +/// +/// Tracking component for an enabled cryo pod (which periodically tries to inject chemicals in the occupant, if one exists) +/// +[RegisterComponent] +public sealed class ActiveCryoPodComponent : Component +{ +} diff --git a/Content.Shared/Medical/Cryogenics/CryoPodWireStatus.cs b/Content.Shared/Medical/Cryogenics/CryoPodWireStatus.cs new file mode 100644 index 0000000000..dfd3ada235 --- /dev/null +++ b/Content.Shared/Medical/Cryogenics/CryoPodWireStatus.cs @@ -0,0 +1,10 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Medical.Cryogenics +{ + [Serializable, NetSerializable] + public enum CryoPodWireActionKey: byte + { + Key + } +} diff --git a/Content.Shared/Medical/Cryogenics/InsideCryoPodComponent.cs b/Content.Shared/Medical/Cryogenics/InsideCryoPodComponent.cs new file mode 100644 index 0000000000..dc5f9b47d8 --- /dev/null +++ b/Content.Shared/Medical/Cryogenics/InsideCryoPodComponent.cs @@ -0,0 +1,12 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Cryogenics; + +[RegisterComponent] +[NetworkedComponent] +public sealed class InsideCryoPodComponent: Component +{ + [ViewVariables] + [DataField("previousOffset")] + public Vector2 PreviousOffset { get; set; } = new(0, 0); +} diff --git a/Content.Shared/Medical/Cryogenics/SharedCryoPodComponent.cs b/Content.Shared/Medical/Cryogenics/SharedCryoPodComponent.cs new file mode 100644 index 0000000000..b10a929879 --- /dev/null +++ b/Content.Shared/Medical/Cryogenics/SharedCryoPodComponent.cs @@ -0,0 +1,105 @@ +using System.Threading; +using Content.Shared.Body.Components; +using Content.Shared.DragDrop; +using Robust.Shared.Containers; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Medical.Cryogenics; + +[NetworkedComponent] +public abstract class SharedCryoPodComponent: Component, IDragDropOn +{ + /// + /// Specifies the name of the atmospherics port to draw gas from. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("port")] + public string PortName { get; set; } = "port"; + + /// + /// Specifies the name of the atmospherics port to draw gas from. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("solutionContainerName")] + public string SolutionContainerName { get; set; } = "beakerSlot"; + + /// + /// How often (seconds) are chemicals transferred from the beaker to the body? + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("beakerTransferTime")] + public float BeakerTransferTime = 1f; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("nextInjectionTime", customTypeSerializer:typeof(TimeOffsetSerializer))] + public TimeSpan? NextInjectionTime; + + /// + /// How many units to transfer per tick from the beaker to the mob? + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("beakerTransferAmount")] + public float BeakerTransferAmount = 1f; + + /// + /// Delay applied when inserting a mob in the pod. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("entryDelay")] + public float EntryDelay = 2f; + + /// + /// Delay applied when trying to pry open a locked pod. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("pryDelay")] + public float PryDelay = 5f; + + /// + /// Container for mobs inserted in the pod. + /// + [ViewVariables] + public ContainerSlot BodyContainer = default!; + + /// + /// If true, the eject verb will not work on the pod and the user must use a crowbar to pry the pod open. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("locked")] + public bool Locked { get; set; } + + /// + /// Causes the pod to be locked without being fixable by messing with wires. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("permaLocked")] + public bool PermaLocked { get; set; } + + public bool IsPrying { get; set; } + + public CancellationTokenSource? DragDropCancelToken; + + [Serializable, NetSerializable] + public enum CryoPodVisuals : byte + { + ContainsEntity, + IsOn + } + + public bool CanInsert(EntityUid entity) + { + return IoCManager.Resolve().HasComponent(entity); + } + + bool IDragDropOn.CanDragDropOn(DragDropEvent eventArgs) + { + return CanInsert(eventArgs.Dragged); + } + + bool IDragDropOn.DragDropOn(DragDropEvent eventArgs) + { + return false; + } +} diff --git a/Content.Shared/Medical/Cryogenics/SharedCryoPodSystem.cs b/Content.Shared/Medical/Cryogenics/SharedCryoPodSystem.cs new file mode 100644 index 0000000000..b9829f2ec6 --- /dev/null +++ b/Content.Shared/Medical/Cryogenics/SharedCryoPodSystem.cs @@ -0,0 +1,169 @@ +using Content.Server.Medical.Components; +using Content.Shared.Destructible; +using Content.Shared.Emag.Systems; +using Content.Shared.MobState.Components; +using Content.Shared.MobState.EntitySystems; +using Content.Shared.Popups; +using Content.Shared.Standing; +using Content.Shared.Stunnable; +using Content.Shared.Verbs; +using Robust.Shared.Containers; +using Robust.Shared.Player; + +namespace Content.Shared.Medical.Cryogenics; + +public abstract partial class SharedCryoPodSystem: EntitySystem +{ + [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; + [Dependency] private readonly StandingStateSystem _standingStateSystem = default!; + [Dependency] private readonly SharedMobStateSystem _mobStateSystem = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly SharedContainerSystem _containerSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + InitializeInsideCryoPod(); + } + + protected void OnComponentInit(EntityUid uid, SharedCryoPodComponent cryoPodComponent, ComponentInit args) + { + cryoPodComponent.BodyContainer = _containerSystem.EnsureContainer(uid, "scanner-body"); + } + + protected void UpdateAppearance(EntityUid uid, SharedCryoPodComponent? cryoPod = null, AppearanceComponent? appearance = null) + { + if (!Resolve(uid, ref cryoPod)) + return; + var cryoPodEnabled = HasComp(uid); + if (TryComp(uid, out var light)) + { + light.Enabled = cryoPodEnabled && cryoPod.BodyContainer.ContainedEntity != null; + } + + if (!Resolve(uid, ref appearance)) + return; + _appearanceSystem.SetData(uid, SharedCryoPodComponent.CryoPodVisuals.ContainsEntity, cryoPod.BodyContainer.ContainedEntity == null, appearance); + _appearanceSystem.SetData(uid, SharedCryoPodComponent.CryoPodVisuals.IsOn, cryoPodEnabled, appearance); + } + + public void InsertBody(EntityUid uid, EntityUid target, SharedCryoPodComponent cryoPodComponent) + { + if (cryoPodComponent.BodyContainer.ContainedEntity != null) + return; + + if (!HasComp(target)) + return; + + var xform = Transform(target); + cryoPodComponent.BodyContainer.Insert(target, transform: xform); + + EnsureComp(target); + _standingStateSystem.Stand(target, force: true); // Force-stand the mob so that the cryo pod sprite overlays it fully + + UpdateAppearance(uid, cryoPodComponent); + } + + public void TryEjectBody(EntityUid uid, EntityUid userId, SharedCryoPodComponent? cryoPodComponent) + { + if (!Resolve(uid, ref cryoPodComponent)) + { + return; + } + + if (cryoPodComponent.Locked) + { + _popupSystem.PopupEntity(Loc.GetString("cryo-pod-locked"), uid, userId); + return; + } + + EjectBody(uid, cryoPodComponent); + } + + public virtual void EjectBody(EntityUid uid, SharedCryoPodComponent? cryoPodComponent) + { + if (!Resolve(uid, ref cryoPodComponent)) + return; + + if (cryoPodComponent.BodyContainer.ContainedEntity is not {Valid: true} contained) + return; + + cryoPodComponent.BodyContainer.Remove(contained); + // InsideCryoPodComponent is removed automatically in its EntGotRemovedFromContainerMessage listener + // RemComp(contained); + + // Restore the correct position of the patient. Checking the components manually feels hacky, but I did not find a better way for now. + if (HasComp(contained) || _mobStateSystem.IsIncapacitated(contained)) + { + _standingStateSystem.Down(contained); + } + else + { + _standingStateSystem.Stand(contained); + } + + UpdateAppearance(uid, cryoPodComponent); + } + + protected void AddAlternativeVerbs(EntityUid uid, SharedCryoPodComponent cryoPodComponent, GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract) + return; + + // Eject verb + if (cryoPodComponent.BodyContainer.ContainedEntity != null) + { + args.Verbs.Add(new AlternativeVerb + { + Text = Loc.GetString("cryo-pod-verb-noun-occupant"), + Category = VerbCategory.Eject, + Priority = 1, // Promote to top to make ejecting the ALT-click action + Act = () => TryEjectBody(uid, args.User, cryoPodComponent) + }); + } + } + + protected void OnEmagged(EntityUid uid, SharedCryoPodComponent? cryoPodComponent, GotEmaggedEvent args) + { + if (!Resolve(uid, ref cryoPodComponent)) + { + return; + } + + cryoPodComponent.PermaLocked = true; + cryoPodComponent.Locked = true; + args.Handled = true; + } + + protected void DoInsertCryoPod(EntityUid uid, SharedCryoPodComponent cryoPodComponent, DoInsertCryoPodEvent args) + { + cryoPodComponent.DragDropCancelToken = null; + InsertBody(uid, args.ToInsert, cryoPodComponent); + } + + protected void DoInsertCancelCryoPod(EntityUid uid, SharedCryoPodComponent cryoPodComponent, DoInsertCancelledCryoPodEvent args) + { + cryoPodComponent.DragDropCancelToken = null; + } + + protected void OnCryoPodPryFinished(EntityUid uid, SharedCryoPodComponent cryoPodComponent, CryoPodPryFinished args) + { + cryoPodComponent.IsPrying = false; + EjectBody(uid, cryoPodComponent); + } + + protected void OnCryoPodPryInterrupted(EntityUid uid, SharedCryoPodComponent cryoPodComponent, CryoPodPryInterrupted args) + { + cryoPodComponent.IsPrying = false; + } + + #region Event records + + protected record DoInsertCryoPodEvent(EntityUid ToInsert); + protected record DoInsertCancelledCryoPodEvent; + protected record CryoPodPryFinished; + protected record CryoPodPryInterrupted; + + #endregion +} diff --git a/Content.Shared/Medical/Cryogenics/SharedInsideCryoPodSystem.cs b/Content.Shared/Medical/Cryogenics/SharedInsideCryoPodSystem.cs new file mode 100644 index 0000000000..da3604e6e5 --- /dev/null +++ b/Content.Shared/Medical/Cryogenics/SharedInsideCryoPodSystem.cs @@ -0,0 +1,29 @@ +using Content.Shared.Standing; +using Robust.Shared.Containers; + +namespace Content.Shared.Medical.Cryogenics; + +public abstract partial class SharedCryoPodSystem +{ + public virtual void InitializeInsideCryoPod() + { + SubscribeLocalEvent(HandleDown); + SubscribeLocalEvent(OnEntGotRemovedFromContainer); + } + + // Must stand in the cryo pod + private void HandleDown(EntityUid uid, InsideCryoPodComponent component, DownAttemptEvent args) + { + args.Cancel(); + } + + private void OnEntGotRemovedFromContainer(EntityUid uid, InsideCryoPodComponent component, EntGotRemovedFromContainerMessage args) + { + if (Terminating(uid)) + { + return; + } + + RemComp(uid); + } +} diff --git a/Content.Shared/Standing/StandingStateSystem.cs b/Content.Shared/Standing/StandingStateSystem.cs index f646f91de4..b7c17b1848 100644 --- a/Content.Shared/Standing/StandingStateSystem.cs +++ b/Content.Shared/Standing/StandingStateSystem.cs @@ -116,7 +116,8 @@ namespace Content.Shared.Standing public bool Stand(EntityUid uid, StandingStateComponent? standingState = null, - AppearanceComponent? appearance = null) + AppearanceComponent? appearance = null, + bool force = false) { // TODO: This should actually log missing comps... if (!Resolve(uid, ref standingState, false)) @@ -128,11 +129,14 @@ namespace Content.Shared.Standing if (standingState.Standing) return true; - var msg = new StandAttemptEvent(); - RaiseLocalEvent(uid, msg, false); + if (!force) + { + var msg = new StandAttemptEvent(); + RaiseLocalEvent(uid, msg, false); - if (msg.Cancelled) - return false; + if (msg.Cancelled) + return false; + } standingState.Standing = true; Dirty(standingState); diff --git a/Resources/Locale/en-US/medical/components/cryo-pod-component.ftl b/Resources/Locale/en-US/medical/components/cryo-pod-component.ftl new file mode 100644 index 0000000000..53ee8301de --- /dev/null +++ b/Resources/Locale/en-US/medical/components/cryo-pod-component.ftl @@ -0,0 +1,7 @@ +# Ejection verb label. +cryo-pod-verb-noun-occupant = Patient +# Examine text showing whether there's a beaker in the pod and if it is empty. +cryo-pod-examine = There's {INDEFINITE($beaker)} {$beaker} in here. +cryo-pod-empty-beaker = It is empty! +# Shown when a normal ejection through the eject verb is attempted on a locked pod. +cryo-pod-locked = The ejection mechanism is unresponsive! diff --git a/Resources/Prototypes/Catalog/Research/technologies.yml b/Resources/Prototypes/Catalog/Research/technologies.yml index 57c8385b1f..a1967a3fe5 100644 --- a/Resources/Prototypes/Catalog/Research/technologies.yml +++ b/Resources/Prototypes/Catalog/Research/technologies.yml @@ -1,4 +1,4 @@ -# In order to make this list somewhat organized, please place +# In order to make this list somewhat organized, please place # new technologies underneath their overarching "base" technology. # Base Technology @@ -160,6 +160,7 @@ - MedicalScannerMachineCircuitboard - StasisBedMachineCircuitboard - CloningConsoleComputerCircuitboard + - CryoPodMachineCircuitboard # Security Technology Tree @@ -386,7 +387,7 @@ name: technologies-rapid-upgrade id: RapidUpgrade description: technologies-rapid-upgrade-description - icon: + icon: sprite: Objects/Specific/Research/rped.rsi state: icon requiredPoints: 10000 diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml index becc60ef10..251c950d46 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml @@ -268,6 +268,24 @@ Glass: 5 Cable: 1 +- type: entity + id: CryoPodMachineCircuitboard + parent: BaseMachineCircuitboard + name: cryo pod machine board + description: A machine printed circuit board for a cryo pod + components: + - type: Sprite + state: medical + - type: MachineBoard + prototype: CryoPod + requirements: + ScanningModule: 1 + Manipulator: 1 + MatterBin: 2 + materialRequirements: + Glass: 1 + Cable: 1 + - type: entity id: ChemMasterMachineCircuitboard parent: BaseMachineCircuitboard diff --git a/Resources/Prototypes/Entities/Structures/Machines/Medical/cryo_pod.yml b/Resources/Prototypes/Entities/Structures/Machines/Medical/cryo_pod.yml new file mode 100644 index 0000000000..ed4c967df4 --- /dev/null +++ b/Resources/Prototypes/Entities/Structures/Machines/Medical/cryo_pod.yml @@ -0,0 +1,104 @@ +- type: entity + parent: [BaseStructure, ConstructibleMachine] # Not a BaseMachinePowered since we don't want the anchorable component + id: CryoPod + name: cryo pod + description: A special machine intended to create a safe environment for the use of chemicals that react in cold environments. + components: + - type: Sprite + netsync: false + sprite: Structures/Machines/cryogenics.rsi + drawdepth: Mobs + noRot: true + offset: 0, 0.5 + layers: + - sprite: Structures/Piping/Atmospherics/pipe.rsi + state: pipeHalf + offset: 0, -0.5 + map: [ "enum.PipeVisualLayers.Pipe" ] + - state: pod-open + map: [ "enum.CryoPodVisualLayers.Base" ] + - state: cover-on + map: [ "enum.CryoPodVisualLayers.Cover" ] + visible: false + - state: pod-panel + map: [ "enum.WiresVisualLayers.MaintenancePanel" ] + visible: false + - type: InteractionOutline + - type: Transform + noRot: true + - type: Fixtures + fixtures: + - shape: + !type:PhysShapeAabb + bounds: "-0.5,-0.5,0.5,0.90" + density: 200 + mask: + - MachineMask + layer: + - MachineLayer + - type: ContainerContainer + containers: + scanner-body: + !type:ContainerSlot + showEnts: true + beakerSlot: !type:ContainerSlot {} + machine_board: !type:Container + machine_parts: !type:Container + - type: AtmosDevice + - type: Appearance + - type: Machine + board: CryoPodMachineCircuitboard + - type: WiresVisuals + - type: Wires + BoardName: "Cryo pod" + LayoutId: CryoPod + - type: Damageable + damageContainer: Inorganic + damageModifierSet: Metallic + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 100 + behaviors: + - !type:EmptyAllContainersBehaviour + - !type:ChangeConstructionNodeBehavior + node: machineFrame + - !type:DoActsBehavior + acts: [ "Destruction" ] + - type: ApcPowerReceiver + powerLoad: 3000 + - type: ExtensionCableReceiver + - type: NodeContainer + nodes: + port: + !type:PortablePipeNode + nodeGroupID: Pipe + pipeDirection: South + - type: ItemSlots + slots: + beakerSlot: + whitelist: + components: + - FitsInDispenser + - type: UserInterface + interfaces: + - key: enum.HealthAnalyzerUiKey.Key + type: HealthAnalyzerBoundUserInterface + - key: enum.WiresUiKey.Key + type: WiresBoundUserInterface + - type: ActivatableUI + key: enum.HealthAnalyzerUiKey.Key + requireHands: false + - type: ActivatableUIRequiresPower + - type: PointLight + color: "#3a807f" + radius: 2 + energy: 10 + enabled: false + - type: EmptyOnMachineDeconstruct + containers: + - scanner-body + - type: CryoPod + - type: ContainerTemperatureDamageThresholds + coldDamageThreshold: 10 diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 1a7d84a844..a9c19be854 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -268,6 +268,7 @@ - PortableScrubberMachineCircuitBoard - CloningPodMachineCircuitboard - MedicalScannerMachineCircuitboard + - CryoPodMachineCircuitboard - CrewMonitoringComputerCircuitboard - VaccinatorMachineCircuitboard - DiagnoserMachineCircuitboard diff --git a/Resources/Prototypes/Recipes/Lathes/electronics.yml b/Resources/Prototypes/Recipes/Lathes/electronics.yml index 8814b7a76f..2d59384acb 100644 --- a/Resources/Prototypes/Recipes/Lathes/electronics.yml +++ b/Resources/Prototypes/Recipes/Lathes/electronics.yml @@ -91,6 +91,16 @@ Steel: 100 Glass: 900 +- type: latheRecipe + id: CryoPodMachineCircuitboard + icon: Objects/Misc/module.rsi/id_mod.png + result: CryoPodMachineCircuitboard + completetime: 4 + materials: + Steel: 100 + Glass: 900 + Gold: 100 + - type: latheRecipe id: ChemMasterMachineCircuitboard icon: { sprite: Objects/Misc/module.rsi, state: id_mod } diff --git a/Resources/Prototypes/Wires/layouts.yml b/Resources/Prototypes/Wires/layouts.yml index a4eae8c711..e130b561ca 100644 --- a/Resources/Prototypes/Wires/layouts.yml +++ b/Resources/Prototypes/Wires/layouts.yml @@ -68,3 +68,10 @@ dummyWires: 4 wires: - !type:PowerWireAction + +- type: wireLayout + id: CryoPod + dummyWires: 2 + wires: + - !type:PowerWireAction + - !type:CryoPodEjectLockWireAction diff --git a/Resources/Textures/Structures/Machines/cryogenics.rsi/cover-off.png b/Resources/Textures/Structures/Machines/cryogenics.rsi/cover-off.png new file mode 100644 index 0000000000000000000000000000000000000000..e238a66ed25a339f59d0ca2cfa4516b02661ae0e GIT binary patch literal 751 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 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Machines/cryogenics.rsi/meta.json b/Resources/Textures/Structures/Machines/cryogenics.rsi/meta.json new file mode 100644 index 0000000000..8eee377ff7 --- /dev/null +++ b/Resources/Textures/Structures/Machines/cryogenics.rsi/meta.json @@ -0,0 +1,41 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "https://github.com/tgstation/tgstation/commit/033d025f53051dc53ece230f486578be6f05f88f", + "size": { + "x": 32, + "y": 64 + }, + "states": [ + { + "name": "pod-panel" + }, + { + "name": "cover-off" + }, + { + "name": "cover-on", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "pod-open" + }, + { + "name": "pod-on" + }, + { + "name": "pod-off" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Structures/Machines/cryogenics.rsi/pod-off.png b/Resources/Textures/Structures/Machines/cryogenics.rsi/pod-off.png new file mode 100644 index 0000000000000000000000000000000000000000..c3194e04516b094d33e58d5de75b2859958aeab5 GIT binary patch literal 1612 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 Ef=yJ_a{vGU literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Machines/cryogenics.rsi/pod-panel.png b/Resources/Textures/Structures/Machines/cryogenics.rsi/pod-panel.png new file mode 100644 index 0000000000000000000000000000000000000000..c0f06cacbc2c8290e77a2f60aa81dc0384266e92 GIT binary patch literal 296 zcmeAS@N?(olHy`uVBq!ia0vp^3P9|@!3HF&`%2dVDb50q$YKTtz9S&aI8~cZ8YpJ=D+wCT{`X+OQ8LWm9tOtE>^D0Gb<4D>haYc#a z8t1(>vEK@ddr7GHX=MI>X`ymU4%%YawElgjs+IvlI-m=XEEQRP@N V{x?3m5DoMtgQu&X%Q~loCIIxGYE%FK literal 0 HcmV?d00001