feat: Medbay cryo pods (#11349)

Fixes https://github.com/space-wizards/space-station-14/issues/11245
This commit is contained in:
Francesco
2022-12-25 12:35:51 +01:00
committed by GitHub
parent b7af5e6109
commit d47e001b18
30 changed files with 1188 additions and 27 deletions

View File

@@ -0,0 +1,7 @@
using Content.Shared.DragDrop;
using Content.Shared.Medical.Cryogenics;
namespace Content.Client.Medical.Cryogenics;
[RegisterComponent]
public sealed class CryoPodComponent : SharedCryoPodComponent { }

View File

@@ -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<CryoPodComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<CryoPodComponent, GetVerbsEvent<AlternativeVerb>>(AddAlternativeVerbs);
SubscribeLocalEvent<CryoPodComponent, GotEmaggedEvent>(OnEmagged);
SubscribeLocalEvent<CryoPodComponent, DoInsertCryoPodEvent>(DoInsertCryoPod);
SubscribeLocalEvent<CryoPodComponent, DoInsertCancelledCryoPodEvent>(DoInsertCancelCryoPod);
SubscribeLocalEvent<CryoPodComponent, CryoPodPryFinished>(OnCryoPodPryFinished);
SubscribeLocalEvent<CryoPodComponent, CryoPodPryInterrupted>(OnCryoPodPryInterrupted);
SubscribeLocalEvent<CryoPodComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<InsideCryoPodComponent, ComponentStartup>(OnCryoPodInsertion);
SubscribeLocalEvent<InsideCryoPodComponent, ComponentRemove>(OnCryoPodRemoval);
}
private void OnCryoPodInsertion(EntityUid uid, InsideCryoPodComponent component, ComponentStartup args)
{
if (!TryComp<SpriteComponent>(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<SpriteComponent>(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,
}

View File

@@ -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
{
/// <summary>
/// Local air buffer that will be mixed with the pipenet, if one exists, per tick.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("gasMixture")]
public GasMixture Air { get; set; } = new(Atmospherics.OneAtmosphere);
}

View File

@@ -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;
/// <summary>
/// Causes a failure in the cryo pod ejection system when cut. A crowbar will be needed to pry open the pod.
/// </summary>
[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<CryoPodComponent>(wire.Owner, out var cryoPodComponent) && !cryoPodComponent.PermaLocked)
{
cryoPodComponent.Locked = true;
}
return true;
}
public override bool Mend(EntityUid user, Wire wire)
{
if (EntityManager.TryGetComponent<CryoPodComponent>(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<CryoPodComponent>(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);
}
}

View File

@@ -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<CryoPodComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<CryoPodComponent, GetVerbsEvent<AlternativeVerb>>(AddAlternativeVerbs);
SubscribeLocalEvent<CryoPodComponent, GotEmaggedEvent>(OnEmagged);
SubscribeLocalEvent<CryoPodComponent, DoInsertCryoPodEvent>(DoInsertCryoPod);
SubscribeLocalEvent<CryoPodComponent, DoInsertCancelledCryoPodEvent>(DoInsertCancelCryoPod);
SubscribeLocalEvent<CryoPodComponent, CryoPodPryFinished>(OnCryoPodPryFinished);
SubscribeLocalEvent<CryoPodComponent, CryoPodPryInterrupted>(OnCryoPodPryInterrupted);
SubscribeLocalEvent<CryoPodComponent, AtmosDeviceUpdateEvent>(OnCryoPodUpdateAtmosphere);
SubscribeLocalEvent<CryoPodComponent, DragDropEvent>(HandleDragDropOn);
SubscribeLocalEvent<CryoPodComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<CryoPodComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<CryoPodComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<CryoPodComponent, GasAnalyzerScanEvent>(OnGasAnalyzed);
SubscribeLocalEvent<CryoPodComponent, ActivatableUIOpenAttemptEvent>(OnActivateUIAttempt);
SubscribeLocalEvent<CryoPodComponent, AfterActivatableUIOpenEvent>(OnActivateUI);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var curTime = _gameTiming.CurTime;
var bloodStreamQuery = GetEntityQuery<BloodstreamComponent>();
var metaDataQuery = GetEntityQuery<MetaDataComponent>();
var itemSlotsQuery = GetEntityQuery<ItemSlotsComponent>();
var fitsInDispenserQuery = GetEntityQuery<FitsInDispenserComponent>();
var solutionContainerManagerQuery = GetEntityQuery<SolutionContainerManagerComponent>();
foreach (var (_, cryoPod) in EntityQuery<ActiveCryoPodComponent, CryoPodComponent>())
{
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<ActiveCryoPodComponent>(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<ActiveCryoPodComponent>(uid);
}
else
{
RemComp<ActiveCryoPodComponent>(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<string, GasMixture?> { { 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
}

View File

@@ -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<InsideCryoPodComponent, InhaleLocationEvent>(OnInhaleLocation);
SubscribeLocalEvent<InsideCryoPodComponent, ExhaleLocationEvent>(OnExhaleLocation);
SubscribeLocalEvent<InsideCryoPodComponent, AtmosExposedGetAirEvent>(OnGetAir);
}
#region Atmos handlers
private void OnGetAir(EntityUid uid, InsideCryoPodComponent component, ref AtmosExposedGetAirEvent args)
{
if (TryComp<CryoPodComponent>(Transform(uid).ParentUid, out var cryoPodComponent))
{
args.Gas = cryoPodComponent.Air;
args.Handled = true;
}
}
private void OnInhaleLocation(EntityUid uid, InsideCryoPodComponent component, InhaleLocationEvent args)
{
if (TryComp<CryoPodComponent>(Transform(uid).ParentUid, out var cryoPodComponent))
{
args.Gas = cryoPodComponent.Air;
}
}
private void OnExhaleLocation(EntityUid uid, InsideCryoPodComponent component, ExhaleLocationEvent args)
{
if (TryComp<CryoPodComponent>(Transform(uid).ParentUid, out var cryoPodComponent))
{
args.Gas = cryoPodComponent.Air;
}
}
#endregion
}
}

View File

@@ -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;
}

View File

@@ -24,6 +24,18 @@ namespace Content.Server.Temperature.Components
[ViewVariables(VVAccess.ReadWrite)]
public float ColdDamageThreshold = 260f;
/// <summary>
/// Overrides HeatDamageThreshold if the entity's within a parent with the TemperatureDamageThresholdsComponent component.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float? ParentHeatDamageThreshold;
/// <summary>
/// Overrides ColdDamageThreshold if the entity's within a parent with the TemperatureDamageThresholdsComponent component.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float? ParentColdDamageThreshold;
[DataField("specificHeat")]
[ViewVariables(VVAccess.ReadWrite)]
public float SpecificHeat = 50f;

View File

@@ -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!;
/// <summary>
/// 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<TemperatureComponent, OnTemperatureChangeEvent>(EnqueueDamage);
SubscribeLocalEvent<TemperatureComponent, AtmosExposedUpdateEvent>(OnAtmosExposedUpdate);
SubscribeLocalEvent<AlertsComponent, OnTemperatureChangeEvent>(ServerAlert);
SubscribeLocalEvent<TemperatureProtectionComponent, InventoryRelayedEvent<ModifyChangedTemperatureEvent>>(OnTemperatureChangeAttempt);
SubscribeLocalEvent<TemperatureProtectionComponent, InventoryRelayedEvent<ModifyChangedTemperatureEvent>>(
OnTemperatureChangeAttempt);
// Allows overriding thresholds based on the parent's thresholds.
SubscribeLocalEvent<TemperatureComponent, EntParentChangedMessage>(OnParentChange);
SubscribeLocalEvent<ContainerTemperatureDamageThresholdsComponent, ComponentStartup>(
OnParentThresholdStartup);
SubscribeLocalEvent<ContainerTemperatureDamageThresholdsComponent, ComponentShutdown>(
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<ModifyChangedTemperatureEvent> args)
private void OnTemperatureChangeAttempt(EntityUid uid, TemperatureProtectionComponent component,
InventoryRelayedEvent<ModifyChangedTemperatureEvent> args)
{
args.Args.TemperatureDelta *= component.Coefficient;
}
private void OnParentChange(EntityUid uid, TemperatureComponent component,
ref EntParentChangedMessage args)
{
var temperatureQuery = GetEntityQuery<TemperatureComponent>();
var transformQuery = GetEntityQuery<TransformComponent>();
var thresholdsQuery = GetEntityQuery<ContainerTemperatureDamageThresholdsComponent>();
// 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<TemperatureComponent>(), GetEntityQuery<TransformComponent>(),
GetEntityQuery<ContainerTemperatureDamageThresholdsComponent>());
}
private void OnParentThresholdShutdown(EntityUid uid, ContainerTemperatureDamageThresholdsComponent component,
ComponentShutdown args)
{
RecursiveThresholdUpdate(uid, GetEntityQuery<TemperatureComponent>(), GetEntityQuery<TransformComponent>(),
GetEntityQuery<ContainerTemperatureDamageThresholdsComponent>());
}
/// <summary>
/// Recalculate and apply parent thresholds for the root entity and all its descendant.
/// </summary>
/// <param name="root"></param>
/// <param name="temperatureQuery"></param>
/// <param name="transformQuery"></param>
/// <param name="tempThresholdsQuery"></param>
private void RecursiveThresholdUpdate(EntityUid root, EntityQuery<TemperatureComponent> temperatureQuery,
EntityQuery<TransformComponent> transformQuery,
EntityQuery<ContainerTemperatureDamageThresholdsComponent> tempThresholdsQuery)
{
RecalculateAndApplyParentThresholds(root, temperatureQuery, transformQuery, tempThresholdsQuery);
foreach (var child in Transform(root).ChildEntities)
{
RecursiveThresholdUpdate(child, temperatureQuery, transformQuery, tempThresholdsQuery);
}
}
/// <summary>
/// Recalculate parent thresholds and apply them on the uid temperature component.
/// </summary>
/// <param name="uid"></param>
/// <param name="temperatureQuery"></param>
/// <param name="transformQuery"></param>
/// <param name="tempThresholdsQuery"></param>
private void RecalculateAndApplyParentThresholds(EntityUid uid,
EntityQuery<TemperatureComponent> temperatureQuery, EntityQuery<TransformComponent> transformQuery,
EntityQuery<ContainerTemperatureDamageThresholdsComponent> 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;
}
/// <summary>
/// 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).
/// </summary>
/// <param name="initialParentUid"></param>
/// <param name="transformQuery"></param>
/// <param name="tempThresholdsQuery"></param>
private (float?, float?) RecalculateParentThresholds(
EntityUid initialParentUid,
EntityQuery<TransformComponent> transformQuery,
EntityQuery<ContainerTemperatureDamageThresholdsComponent> 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

View File

@@ -0,0 +1,9 @@
namespace Content.Server.Medical.Components;
/// <summary>
/// Tracking component for an enabled cryo pod (which periodically tries to inject chemicals in the occupant, if one exists)
/// </summary>
[RegisterComponent]
public sealed class ActiveCryoPodComponent : Component
{
}

View File

@@ -0,0 +1,10 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Medical.Cryogenics
{
[Serializable, NetSerializable]
public enum CryoPodWireActionKey: byte
{
Key
}
}

View File

@@ -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);
}

View File

@@ -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
{
/// <summary>
/// Specifies the name of the atmospherics port to draw gas from.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("port")]
public string PortName { get; set; } = "port";
/// <summary>
/// Specifies the name of the atmospherics port to draw gas from.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("solutionContainerName")]
public string SolutionContainerName { get; set; } = "beakerSlot";
/// <summary>
/// How often (seconds) are chemicals transferred from the beaker to the body?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("beakerTransferTime")]
public float BeakerTransferTime = 1f;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("nextInjectionTime", customTypeSerializer:typeof(TimeOffsetSerializer))]
public TimeSpan? NextInjectionTime;
/// <summary>
/// How many units to transfer per tick from the beaker to the mob?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("beakerTransferAmount")]
public float BeakerTransferAmount = 1f;
/// <summary>
/// Delay applied when inserting a mob in the pod.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("entryDelay")]
public float EntryDelay = 2f;
/// <summary>
/// Delay applied when trying to pry open a locked pod.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("pryDelay")]
public float PryDelay = 5f;
/// <summary>
/// Container for mobs inserted in the pod.
/// </summary>
[ViewVariables]
public ContainerSlot BodyContainer = default!;
/// <summary>
/// If true, the eject verb will not work on the pod and the user must use a crowbar to pry the pod open.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("locked")]
public bool Locked { get; set; }
/// <summary>
/// Causes the pod to be locked without being fixable by messing with wires.
/// </summary>
[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<IEntityManager>().HasComponent<BodyComponent>(entity);
}
bool IDragDropOn.CanDragDropOn(DragDropEvent eventArgs)
{
return CanInsert(eventArgs.Dragged);
}
bool IDragDropOn.DragDropOn(DragDropEvent eventArgs)
{
return false;
}
}

View File

@@ -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<ContainerSlot>(uid, "scanner-body");
}
protected void UpdateAppearance(EntityUid uid, SharedCryoPodComponent? cryoPod = null, AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref cryoPod))
return;
var cryoPodEnabled = HasComp<ActiveCryoPodComponent>(uid);
if (TryComp<SharedPointLightComponent>(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<MobStateComponent>(target))
return;
var xform = Transform(target);
cryoPodComponent.BodyContainer.Insert(target, transform: xform);
EnsureComp<InsideCryoPodComponent>(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<InsideCryoPodComponent>(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<KnockedDownComponent>(contained) || _mobStateSystem.IsIncapacitated(contained))
{
_standingStateSystem.Down(contained);
}
else
{
_standingStateSystem.Stand(contained);
}
UpdateAppearance(uid, cryoPodComponent);
}
protected void AddAlternativeVerbs(EntityUid uid, SharedCryoPodComponent cryoPodComponent, GetVerbsEvent<AlternativeVerb> 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
}

View File

@@ -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<InsideCryoPodComponent, DownAttemptEvent>(HandleDown);
SubscribeLocalEvent<InsideCryoPodComponent, EntGotRemovedFromContainerMessage>(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<InsideCryoPodComponent>(uid);
}
}

View File

@@ -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;
if (!force)
{
var msg = new StandAttemptEvent();
RaiseLocalEvent(uid, msg, false);
if (msg.Cancelled)
return false;
}
standingState.Standing = true;
Dirty(standingState);

View File

@@ -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!

View File

@@ -160,6 +160,7 @@
- MedicalScannerMachineCircuitboard
- StasisBedMachineCircuitboard
- CloningConsoleComputerCircuitboard
- CryoPodMachineCircuitboard
# Security Technology Tree

View File

@@ -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

View File

@@ -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

View File

@@ -268,6 +268,7 @@
- PortableScrubberMachineCircuitBoard
- CloningPodMachineCircuitboard
- MedicalScannerMachineCircuitboard
- CryoPodMachineCircuitboard
- CrewMonitoringComputerCircuitboard
- VaccinatorMachineCircuitboard
- DiagnoserMachineCircuitboard

View File

@@ -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 }

View File

@@ -68,3 +68,10 @@
dummyWires: 4
wires:
- !type:PowerWireAction
- type: wireLayout
id: CryoPod
dummyWires: 2
wires:
- !type:PowerWireAction
- !type:CryoPodEjectLockWireAction

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -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"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 B