feat: Medbay cryo pods (#11349)
Fixes https://github.com/space-wizards/space-station-14/issues/11245
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user