Gas tanks and masks (#2409)

Co-authored-by: a.rudenko <creadth@gmail.com>
Co-authored-by: creadth <creadth@users.noreply.github.com>
Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>
This commit is contained in:
Víctor Aguilera Puerto
2020-10-27 20:53:44 +01:00
committed by GitHub
parent 329926b175
commit 870d052354
77 changed files with 1653 additions and 58 deletions

View File

@@ -0,0 +1,70 @@
#nullable enable
using Content.Server.GameObjects.Components.Body.Respiratory;
using Content.Shared.GameObjects.Components.Inventory;
using Content.Shared.Interfaces.GameObjects.Components;
using Npgsql.TypeHandlers;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Atmos
{
/// <summary>
/// Used in internals as breath tool.
/// </summary>
[RegisterComponent]
public class BreathToolComponent : Component, IEquipped, IUnequipped
{
/// <summary>
/// Tool is functional only in allowed slots
/// </summary>
private EquipmentSlotDefines.SlotFlags _allowedSlots;
public override string Name => "BreathMask";
public bool IsFunctional { get; private set; }
public IEntity? ConnectedInternalsEntity { get; private set; }
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _allowedSlots, "allowedSlots", EquipmentSlotDefines.SlotFlags.MASK);
}
protected override void Shutdown()
{
base.Shutdown();
DisconnectInternals();
}
public void Equipped(EquippedEventArgs eventArgs)
{
if ((EquipmentSlotDefines.SlotMasks[eventArgs.Slot] & _allowedSlots) != _allowedSlots) return;
IsFunctional = true;
if (eventArgs.User.TryGetComponent(out InternalsComponent? internals))
{
ConnectedInternalsEntity = eventArgs.User;
internals.ConnectBreathTool(Owner);
}
}
public void Unequipped(UnequippedEventArgs eventArgs)
{
DisconnectInternals();
}
public void DisconnectInternals()
{
var old = ConnectedInternalsEntity;
ConnectedInternalsEntity = null;
if (old != null && old.TryGetComponent<InternalsComponent>(out var internalsComponent))
{
internalsComponent.DisconnectBreathTool();
}
IsFunctional = false;
}
}
}

View File

@@ -1,4 +1,5 @@
using Content.Server.Atmos;
using Content.Server.Interfaces;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
@@ -6,23 +7,19 @@ using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos
{
[RegisterComponent]
public class GasMixtureHolderComponent : Component
public class GasMixtureHolderComponent : Component, IGasMixtureHolder
{
public override string Name => "GasMixtureHolder";
[ViewVariables] public GasMixture GasMixture { get; set; }
[ViewVariables] public GasMixture Air { get; set; }
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
GasMixture = new GasMixture();
Air = new GasMixture();
serializer.DataReadWriteFunction(
"volume",
0f,
vol => GasMixture.Volume = vol,
() => GasMixture.Volume);
serializer.DataField(this, x => x.Air, "air", new GasMixture());
}
}
}

View File

@@ -1,10 +1,356 @@
using Robust.Shared.GameObjects;
#nullable enable
using System;
using Content.Server.Atmos;
using Content.Server.Explosions;
using Content.Server.GameObjects.Components.Body.Respiratory;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.Interfaces;
using Content.Server.Utility;
using Content.Shared.Atmos;
using Content.Shared.Audio;
using Content.Shared.GameObjects.Components.Atmos.GasTank;
using Content.Shared.GameObjects.Components.Inventory;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Utility;
using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.EntitySystemMessages;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos
{
[RegisterComponent]
public class GasTankComponent : Component
[ComponentReference(typeof(IActivate))]
public class GasTankComponent : SharedGasTankComponent, IExamine, IGasMixtureHolder, IUse, IDropped, IActivate
{
public override string Name => "GasTank";
private const float MaxExplosionRange = 14f;
private const float DefaultOutputPressure = Atmospherics.OneAtmosphere;
private float _pressureResistance;
private int _integrity = 3;
[Dependency] private readonly IEntityManager _entityManager = default!;
[ViewVariables] private BoundUserInterface? _userInterface;
[ViewVariables] public GasMixture? Air { get; set; }
/// <summary>
/// Distributed pressure.
/// </summary>
[ViewVariables] public float OutputPressure { get; private set; }
/// <summary>
/// Tank is connected to internals.
/// </summary>
[ViewVariables] public bool IsConnected { get; set; }
/// <summary>
/// Represents that tank is functional and can be connected to internals.
/// </summary>
public bool IsFunctional => GetInternalsComponent() != null;
/// <summary>
/// Pressure at which tanks start leaking.
/// </summary>
public float TankLeakPressure { get; set; } = 30 * Atmospherics.OneAtmosphere;
/// <summary>
/// Pressure at which tank spills all contents into atmosphere.
/// </summary>
public float TankRupturePressure { get; set; } = 40 * Atmospherics.OneAtmosphere;
/// <summary>
/// Base 3x3 explosion.
/// </summary>
public float TankFragmentPressure { get; set; } = 50 * Atmospherics.OneAtmosphere;
/// <summary>
/// Increases explosion for each scale kPa above threshold.
/// </summary>
public float TankFragmentScale { get; set; } = 10 * Atmospherics.OneAtmosphere;
public override void Initialize()
{
base.Initialize();
_userInterface = Owner.GetUIOrNull(SharedGasTankUiKey.Key);
if (_userInterface != null)
{
_userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
}
public void OpenInterface(IPlayerSession session)
{
_userInterface?.Open(session);
UpdateUserInterface(true);
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, x => x.Air, "air", new GasMixture());
serializer.DataField(this, x => x.OutputPressure, "outputPressure", DefaultOutputPressure);
serializer.DataField(this, x => x.TankLeakPressure, "tankLeakPressure", 30 * Atmospherics.OneAtmosphere);
serializer.DataField(this, x => x.TankRupturePressure, "tankRupturePressure", 40 * Atmospherics.OneAtmosphere);
serializer.DataField(this, x => x.TankFragmentPressure, "tankFragmentPressure", 50 * Atmospherics.OneAtmosphere);
serializer.DataField(this, x => x.TankFragmentScale, "tankFragmentScale", 10 * Atmospherics.OneAtmosphere);
serializer.DataField(ref _pressureResistance, "pressureResistance", Atmospherics.OneAtmosphere * 5f);
}
public void Examine(FormattedMessage message, bool inDetailsRange)
{
message.AddMarkup(Loc.GetString("Pressure: [color=orange]{0}[/color] kPa.\n",
Math.Round(Air?.Pressure ?? 0)));
if (IsConnected)
{
message.AddMarkup(Loc.GetString("Connected to external component"));
}
}
protected override void Shutdown()
{
base.Shutdown();
DisconnectFromInternals();
}
public void Update()
{
Air?.React(this);
CheckStatus();
UpdateUserInterface();
}
public GasMixture? RemoveAir(float amount)
{
var gas = Air?.Remove(amount);
CheckStatus();
return gas;
}
public GasMixture RemoveAirVolume(float volume)
{
if (Air == null)
return new GasMixture(volume);
var tankPressure = Air.Pressure;
if (tankPressure < OutputPressure)
{
OutputPressure = tankPressure;
UpdateUserInterface();
}
var molesNeeded = OutputPressure * volume / (Atmospherics.R * Air.Temperature);
var air = RemoveAir(molesNeeded);
if (air != null)
air.Volume = volume;
else
return new GasMixture(volume);
return air;
}
public bool UseEntity(UseEntityEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out IActorComponent? actor)) return false;
OpenInterface(actor.playerSession);
return true;
}
public void Activate(ActivateEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out IActorComponent? actor)) return;
OpenInterface(actor.playerSession);
}
public void ConnectToInternals()
{
if (IsConnected || !IsFunctional) return;
var internals = GetInternalsComponent();
if (internals == null) return;
IsConnected = internals.TryConnectTank(Owner);
UpdateUserInterface();
}
public void DisconnectFromInternals(IEntity? owner = null)
{
if (!IsConnected) return;
IsConnected = false;
GetInternalsComponent(owner)?.DisconnectTank();
UpdateUserInterface();
}
private void UpdateUserInterface(bool initialUpdate = false)
{
_userInterface?.SetState(
new GasTankBoundUserInterfaceState
{
TankPressure = Air?.Pressure ?? 0,
OutputPressure = initialUpdate ? OutputPressure : (float?) null,
InternalsConnected = IsConnected,
CanConnectInternals = IsFunctional && GetInternalsComponent() != null
});
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
{
switch (message.Message)
{
case GasTankSetPressureMessage msg:
OutputPressure = msg.Pressure;
break;
case GasTankToggleInternalsMessage _:
ToggleInternals();
break;
}
}
private void ToggleInternals()
{
if (IsConnected)
{
DisconnectFromInternals();
return;
}
ConnectToInternals();
}
private InternalsComponent? GetInternalsComponent(IEntity? owner = null)
{
if (owner != null) return owner.GetComponentOrNull<InternalsComponent>();
return ContainerHelpers.TryGetContainer(Owner, out var container)
? container.Owner.GetComponentOrNull<InternalsComponent>()
: null;
}
public void AssumeAir(GasMixture giver)
{
Air?.Merge(giver);
CheckStatus();
}
private void CheckStatus()
{
if (Air == null)
return;
var pressure = Air.Pressure;
if (pressure > TankFragmentPressure)
{
// Give the gas a chance to build up more pressure.
for (var i = 0; i < 3; i++)
{
Air.React(this);
}
pressure = Air.Pressure;
var range = (pressure - TankFragmentPressure) / TankFragmentScale;
// Let's cap the explosion, yeah?
if (range > MaxExplosionRange)
{
range = MaxExplosionRange;
}
Owner.SpawnExplosion((int) (range * 0.25f), (int) (range * 0.5f), (int) (range * 1.5f), 1);
Owner.Delete();
return;
}
if (pressure > TankRupturePressure)
{
if (_integrity <= 0)
{
var tileAtmos = Owner.Transform.Coordinates.GetTileAtmosphere();
tileAtmos?.AssumeAir(Air);
EntitySystem.Get<AudioSystem>().PlayAtCoords("Audio/Effects/spray.ogg", Owner.Transform.Coordinates,
AudioHelpers.WithVariation(0.125f));
Owner.Delete();
return;
}
_integrity--;
return;
}
if (pressure > TankLeakPressure)
{
if (_integrity <= 0)
{
var tileAtmos = Owner.Transform.Coordinates.GetTileAtmosphere();
if (tileAtmos == null)
return;
var leakedGas = Air.RemoveRatio(0.25f);
tileAtmos.AssumeAir(leakedGas);
}
else
{
_integrity--;
}
return;
}
if (_integrity < 3)
_integrity++;
}
/// <summary>
/// Open interaction window
/// </summary>
[Verb]
private sealed class ControlVerb : Verb<GasTankComponent>
{
public override bool RequireInteractionRange => true;
protected override void GetData(IEntity user, GasTankComponent component, VerbData data)
{
data.Visibility = VerbVisibility.Invisible;
if (!user.HasComponent<IActorComponent>())
{
return;
}
data.Visibility = VerbVisibility.Visible;
data.Text = "Open Control Panel";
}
protected override void Activate(IEntity user, GasTankComponent component)
{
if (!user.TryGetComponent<IActorComponent>(out var actor))
{
return;
}
component.OpenInterface(actor.playerSession);
}
}
public void Dropped(DroppedEventArgs eventArgs)
{
DisconnectFromInternals(eventArgs.User);
}
}
}

View File

@@ -2,7 +2,9 @@
using System;
using System.Linq;
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.Atmos;
using Content.Server.GameObjects.Components.Body.Circulatory;
using Content.Server.GameObjects.Components.Body.Respiratory;
using Content.Server.Utility;
using Content.Shared.Atmos;
using Content.Shared.GameObjects.Components.Body.Behavior;
@@ -10,6 +12,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
@@ -147,6 +150,16 @@ namespace Content.Server.GameObjects.Components.Body.Behavior
public override void Inhale(float frameTime)
{
if (Body != null && Body.Owner.TryGetComponent(out InternalsComponent? internals)
&& internals.BreathToolEntity != null && internals.GasTankEntity != null
&& internals.BreathToolEntity.TryGetComponent(out BreathToolComponent? breathTool)
&& breathTool.IsFunctional && internals.GasTankEntity.TryGetComponent(out GasTankComponent? gasTank)
&& gasTank.Air != null)
{
Inhale(frameTime, gasTank.RemoveAirVolume(Atmospherics.BreathVolume));
return;
}
if (!Owner.Transform.Coordinates.TryGetTileAir(out var tileAir))
{
return;
@@ -157,8 +170,7 @@ namespace Content.Server.GameObjects.Components.Body.Behavior
public void Inhale(float frameTime, GasMixture from)
{
var ratio = Atmospherics.BreathPercentage * frameTime;
var ratio = (Atmospherics.BreathVolume / from.Volume) * frameTime;
Transfer(from, Air, ratio);
ToBloodstream(Air);

View File

@@ -0,0 +1,63 @@
#nullable enable
using Content.Server.GameObjects.Components.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Body.Respiratory
{
[RegisterComponent]
public class InternalsComponent : Component
{
public override string Name => "Internals";
[ViewVariables] public IEntity? GasTankEntity { get; set; }
[ViewVariables] public IEntity? BreathToolEntity { get; set; }
public void DisconnectBreathTool()
{
var old = BreathToolEntity;
BreathToolEntity = null;
if (old != null && old.TryGetComponent(out BreathToolComponent? breathTool) )
{
breathTool.DisconnectInternals();
DisconnectTank();
}
}
public void ConnectBreathTool(IEntity toolEntity)
{
if (BreathToolEntity != null && BreathToolEntity.TryGetComponent(out BreathToolComponent? tool))
{
tool.DisconnectInternals();
}
BreathToolEntity = toolEntity;
}
public void DisconnectTank()
{
if (GasTankEntity != null && GasTankEntity.TryGetComponent(out GasTankComponent? tank))
{
tank.DisconnectFromInternals(Owner);
}
GasTankEntity = null;
}
public bool TryConnectTank(IEntity tankEntity)
{
if (BreathToolEntity == null)
return false;
if (GasTankEntity != null && GasTankEntity.TryGetComponent(out GasTankComponent? tank))
{
tank.DisconnectFromInternals(Owner);
}
GasTankEntity = tankEntity;
return true;
}
}
}

View File

@@ -245,7 +245,7 @@ namespace Content.Server.GameObjects.Components.GUI
return false;
}
public bool Drop(string slot, EntityCoordinates coords, bool doMobChecks = true)
public bool Drop(string slot, EntityCoordinates coords, bool doMobChecks = true, bool doDropInteraction = true)
{
var hand = GetHand(slot);
if (!CanDrop(slot, doMobChecks) || hand?.Entity == null)
@@ -260,7 +260,7 @@ namespace Content.Server.GameObjects.Components.GUI
return false;
}
if (!DroppedInteraction(item, false))
if (doDropInteraction && !DroppedInteraction(item, false))
return false;
item.RemovedFromSlot();
@@ -282,7 +282,7 @@ namespace Content.Server.GameObjects.Components.GUI
return true;
}
public bool Drop(IEntity entity, EntityCoordinates coords, bool doMobChecks = true)
public bool Drop(IEntity entity, EntityCoordinates coords, bool doMobChecks = true, bool doDropInteraction = true)
{
if (entity == null)
{
@@ -294,15 +294,15 @@ namespace Content.Server.GameObjects.Components.GUI
throw new ArgumentException("Entity must be held in one of our hands.", nameof(entity));
}
return Drop(slot, coords, doMobChecks);
return Drop(slot, coords, doMobChecks, doDropInteraction);
}
public bool Drop(string slot, bool mobChecks = true)
public bool Drop(string slot, bool mobChecks = true, bool doDropInteraction = true)
{
return Drop(slot, Owner.Transform.Coordinates, mobChecks);
return Drop(slot, Owner.Transform.Coordinates, mobChecks, doDropInteraction);
}
public bool Drop(IEntity entity, bool mobChecks = true)
public bool Drop(IEntity entity, bool mobChecks = true, bool doDropInteraction = true)
{
if (entity == null)
{
@@ -314,10 +314,10 @@ namespace Content.Server.GameObjects.Components.GUI
throw new ArgumentException("Entity must be held in one of our hands.", nameof(entity));
}
return Drop(slot, Owner.Transform.Coordinates, mobChecks);
return Drop(slot, Owner.Transform.Coordinates, mobChecks, doDropInteraction);
}
public bool Drop(string slot, BaseContainer targetContainer, bool doMobChecks = true)
public bool Drop(string slot, BaseContainer targetContainer, bool doMobChecks = true, bool doDropInteraction = true)
{
if (slot == null)
{
@@ -352,7 +352,7 @@ namespace Content.Server.GameObjects.Components.GUI
throw new InvalidOperationException();
}
if (!DroppedInteraction(item, doMobChecks))
if (doDropInteraction && !DroppedInteraction(item, doMobChecks))
return false;
item.RemovedFromSlot();
@@ -368,7 +368,7 @@ namespace Content.Server.GameObjects.Components.GUI
return true;
}
public bool Drop(IEntity entity, BaseContainer targetContainer, bool doMobChecks = true)
public bool Drop(IEntity entity, BaseContainer targetContainer, bool doMobChecks = true, bool doDropInteraction = true)
{
if (entity == null)
{
@@ -380,7 +380,7 @@ namespace Content.Server.GameObjects.Components.GUI
throw new ArgumentException("Entity must be held in one of our hands.", nameof(entity));
}
return Drop(slot, targetContainer, doMobChecks);
return Drop(slot, targetContainer, doMobChecks, doDropInteraction);
}
/// <summary>

View File

@@ -156,6 +156,13 @@ namespace Content.Server.GameObjects.Components.GUI
{
return GetSlotItem<ItemComponent>(slot);
}
public IEnumerable<T> LookupItems<T>() where T: Component
{
return _slotContainers.Values.SelectMany(x => x.ContainedEntities.Select(e => e.GetComponentOrNull<T>()))
.Where(x => x != null);
}
public T GetSlotItem<T>(Slots slot) where T : ItemComponent
{
if (!_slotContainers.ContainsKey(slot))
@@ -435,7 +442,7 @@ namespace Content.Server.GameObjects.Components.GUI
var activeHand = hands.GetActiveHand;
if (activeHand != null && activeHand.Owner.TryGetComponent(out ItemComponent clothing))
{
hands.Drop(hands.ActiveHand);
hands.Drop(hands.ActiveHand, doDropInteraction:false);
if (!Equip(msg.Inventoryslot, clothing, true, out var reason))
{
hands.PutInHand(clothing);

View File

@@ -58,12 +58,12 @@ namespace Content.Server.GameObjects.Components.Items.Storage
}
}
public void Equipped(EquippedEventArgs eventArgs)
public virtual void Equipped(EquippedEventArgs eventArgs)
{
EquippedToSlot();
}
public void Unequipped(UnequippedEventArgs eventArgs)
public virtual void Unequipped(UnequippedEventArgs eventArgs)
{
RemovedFromSlot();
}