feat: Medbay cryo pods (#11349)
Fixes https://github.com/space-wizards/space-station-14/issues/11245
This commit is contained in:
7
Content.Client/Medical/Cryogenics/CryoPodComponent.cs
Normal file
7
Content.Client/Medical/Cryogenics/CryoPodComponent.cs
Normal 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 { }
|
||||
83
Content.Client/Medical/Cryogenics/CryoPodSystem.cs
Normal file
83
Content.Client/Medical/Cryogenics/CryoPodSystem.cs
Normal 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,
|
||||
}
|
||||
16
Content.Server/Medical/Components/CryoPodComponent.cs
Normal file
16
Content.Server/Medical/Components/CryoPodComponent.cs
Normal 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);
|
||||
}
|
||||
59
Content.Server/Medical/CryoPodEjectLockWireAction.cs
Normal file
59
Content.Server/Medical/CryoPodEjectLockWireAction.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
267
Content.Server/Medical/CryoPodSystem.cs
Normal file
267
Content.Server/Medical/CryoPodSystem.cs
Normal 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
|
||||
}
|
||||
47
Content.Server/Medical/InsideCryoPodSystem.cs
Normal file
47
Content.Server/Medical/InsideCryoPodSystem.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -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,8 +119,10 @@ 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));
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
10
Content.Shared/Medical/Cryogenics/CryoPodWireStatus.cs
Normal file
10
Content.Shared/Medical/Cryogenics/CryoPodWireStatus.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Medical.Cryogenics
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public enum CryoPodWireActionKey: byte
|
||||
{
|
||||
Key
|
||||
}
|
||||
}
|
||||
12
Content.Shared/Medical/Cryogenics/InsideCryoPodComponent.cs
Normal file
12
Content.Shared/Medical/Cryogenics/InsideCryoPodComponent.cs
Normal 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);
|
||||
}
|
||||
105
Content.Shared/Medical/Cryogenics/SharedCryoPodComponent.cs
Normal file
105
Content.Shared/Medical/Cryogenics/SharedCryoPodComponent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
169
Content.Shared/Medical/Cryogenics/SharedCryoPodSystem.cs
Normal file
169
Content.Shared/Medical/Cryogenics/SharedCryoPodSystem.cs
Normal 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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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!
|
||||
@@ -160,6 +160,7 @@
|
||||
- MedicalScannerMachineCircuitboard
|
||||
- StasisBedMachineCircuitboard
|
||||
- CloningConsoleComputerCircuitboard
|
||||
- CryoPodMachineCircuitboard
|
||||
|
||||
# Security Technology Tree
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -268,6 +268,7 @@
|
||||
- PortableScrubberMachineCircuitBoard
|
||||
- CloningPodMachineCircuitboard
|
||||
- MedicalScannerMachineCircuitboard
|
||||
- CryoPodMachineCircuitboard
|
||||
- CrewMonitoringComputerCircuitboard
|
||||
- VaccinatorMachineCircuitboard
|
||||
- DiagnoserMachineCircuitboard
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 |
@@ -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 |
BIN
Resources/Textures/Structures/Machines/cryogenics.rsi/pod-on.png
Normal file
BIN
Resources/Textures/Structures/Machines/cryogenics.rsi/pod-on.png
Normal file
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 |
Reference in New Issue
Block a user