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 0000000000..e238a66ed2 Binary files /dev/null and b/Resources/Textures/Structures/Machines/cryogenics.rsi/cover-off.png differ diff --git a/Resources/Textures/Structures/Machines/cryogenics.rsi/cover-on.png b/Resources/Textures/Structures/Machines/cryogenics.rsi/cover-on.png new file mode 100644 index 0000000000..af593f99be Binary files /dev/null and b/Resources/Textures/Structures/Machines/cryogenics.rsi/cover-on.png differ 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 0000000000..c3194e0451 Binary files /dev/null and b/Resources/Textures/Structures/Machines/cryogenics.rsi/pod-off.png differ diff --git a/Resources/Textures/Structures/Machines/cryogenics.rsi/pod-on.png b/Resources/Textures/Structures/Machines/cryogenics.rsi/pod-on.png new file mode 100644 index 0000000000..2411bd3dab Binary files /dev/null and b/Resources/Textures/Structures/Machines/cryogenics.rsi/pod-on.png differ diff --git a/Resources/Textures/Structures/Machines/cryogenics.rsi/pod-open.png b/Resources/Textures/Structures/Machines/cryogenics.rsi/pod-open.png new file mode 100644 index 0000000000..cd84d7b3d6 Binary files /dev/null and b/Resources/Textures/Structures/Machines/cryogenics.rsi/pod-open.png differ 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 0000000000..c0f06cacbc Binary files /dev/null and b/Resources/Textures/Structures/Machines/cryogenics.rsi/pod-panel.png differ