Merge branch 'master' into 2020-08-19-firelocks

This commit is contained in:
Víctor Aguilera Puerto
2020-09-11 19:54:12 +02:00
73 changed files with 1818 additions and 1519 deletions

View File

@@ -90,11 +90,11 @@ namespace Content.Client.Commands
{
public string Command => "mapping";
public string Description => "Creates and teleports you to a new uninitialized map for mapping.";
public string Help => $"Usage: {Command} <id> <mapname>";
public string Help => $"Usage: {Command} <mapname> / {Command} <id> <mapname>";
public bool Execute(IDebugConsole console, params string[] args)
{
if (args.Length != 2)
if (args.Length == 0)
{
console.AddLine(Help);
return false;

View File

@@ -64,7 +64,7 @@ namespace Content.Client
factory.Register<SharedLatheComponent>();
factory.Register<SharedSpawnPointComponent>();
factory.Register<SharedSolutionComponent>();
factory.Register<SharedSolutionContainerComponent>();
factory.Register<SharedVendingMachineComponent>();
factory.Register<SharedWiresComponent>();

View File

@@ -131,6 +131,7 @@ namespace Content.Client.State
_userInterfaceManager.Popup($"Unable to connect: {e.Message}", "Connection error.");
Logger.Warning(e.ToString());
_netManager.ConnectFailed -= _onConnectFailed;
_setConnectingState(false);
}
}

View File

@@ -10,7 +10,7 @@ namespace Content.Server.AI.Utility.Considerations.Nutrition.Drink
{
var target = context.GetState<TargetEntityState>().GetValue();
if (!target.TryGetComponent(out SolutionComponent drink))
if (!target.TryGetComponent(out SolutionContainerComponent drink))
{
return 0.0f;
}

View File

@@ -10,7 +10,7 @@ namespace Content.Server.AI.Utility.Considerations.Nutrition.Food
{
var target = context.GetState<TargetEntityState>().GetValue();
if (!target.TryGetComponent(out SolutionComponent food))
if (!target.TryGetComponent(out SolutionContainerComponent food))
{
return 0.0f;
}

View File

@@ -41,7 +41,7 @@ namespace Content.Server.Atmos
public void AfterInteract(AfterInteractEventArgs eventArgs)
{
if (!Owner.TryGetComponent(out SolutionComponent tank))
if (!Owner.TryGetComponent(out SolutionContainerComponent tank))
return;
if (tank.Solution.GetReagentQuantity(_fuelType) == 0)

View File

@@ -93,7 +93,7 @@ namespace Content.Server.Body
}
else
{
body.DisconnectBodyPart(hand.Value, true);
body.RemovePart(hand.Value, true);
}
}
}

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using Content.Shared.Body.Part;
using Content.Shared.Body.Preset;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.Body
@@ -13,24 +14,25 @@ namespace Content.Server.Body
/// </summary>
public class BodyPreset
{
public BodyPreset(BodyPresetPrototype data)
{
LoadFromPrototype(data);
}
[ViewVariables] public bool Initialized { get; private set; }
[ViewVariables] public string Name { get; private set; }
[ViewVariables] public string Name { get; protected set; }
/// <summary>
/// Maps a template slot to the ID of the <see cref="IBodyPart"/> that should
/// fill it. E.g. "right arm" : "BodyPart.arm.basic_human".
/// Maps a template slot to the ID of the <see cref="IBodyPart"/>
/// that should fill it. E.g. "right arm" : "BodyPart.arm.basic_human".
/// </summary>
[ViewVariables]
public Dictionary<string, string> PartIDs { get; private set; }
public Dictionary<string, string> PartIDs { get; protected set; }
protected virtual void LoadFromPrototype(BodyPresetPrototype data)
public virtual void Initialize(BodyPresetPrototype prototype)
{
Name = data.Name;
PartIDs = data.PartIDs;
DebugTools.Assert(!Initialized, $"{nameof(BodyPreset)} {Name} has already been initialized!");
Name = prototype.Name;
PartIDs = prototype.PartIDs;
Initialized = true;
}
}
}

View File

@@ -4,6 +4,7 @@ using System.Linq;
using Content.Server.GameObjects.Components.Body;
using Content.Shared.Body.Template;
using Content.Shared.GameObjects.Components.Body;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.Body
@@ -11,35 +12,22 @@ namespace Content.Server.Body
/// <summary>
/// This class is a data capsule representing the standard format of a
/// <see cref="BodyManagerComponent"/>.
/// For instance, the "humanoid" BodyTemplate defines two arms, each connected to
/// a torso and so on.
/// For instance, the "humanoid" BodyTemplate defines two arms, each
/// connected to a torso and so on.
/// Capable of loading data from a <see cref="BodyTemplatePrototype"/>.
/// </summary>
public class BodyTemplate
{
public BodyTemplate()
{
Name = "empty";
CenterSlot = "";
Slots = new Dictionary<string, BodyPartType>();
Connections = new Dictionary<string, List<string>>();
Layers = new Dictionary<string, string>();
MechanismLayers = new Dictionary<string, string>();
}
[ViewVariables] public bool Initialized { get; private set; }
public BodyTemplate(BodyTemplatePrototype data)
{
LoadFromPrototype(data);
}
[ViewVariables] public string Name { get; private set; }
[ViewVariables] public string Name { get; private set; } = "";
/// <summary>
/// The name of the center BodyPart. For humans, this is set to "torso".
/// Used in many calculations.
/// </summary>
[ViewVariables]
public string CenterSlot { get; set; }
public string CenterSlot { get; set; } = "";
/// <summary>
/// Maps all parts on this template to its BodyPartType.
@@ -47,7 +35,7 @@ namespace Content.Server.Body
/// template.
/// </summary>
[ViewVariables]
public Dictionary<string, BodyPartType> Slots { get; private set; }
public Dictionary<string, BodyPartType> Slots { get; private set; } = new Dictionary<string, BodyPartType>();
/// <summary>
/// Maps limb name to the list of their connections to other limbs.
@@ -58,13 +46,13 @@ namespace Content.Server.Body
/// map "left arm" to "torso".
/// </summary>
[ViewVariables]
public Dictionary<string, List<string>> Connections { get; private set; }
public Dictionary<string, List<string>> Connections { get; private set; } = new Dictionary<string, List<string>>();
[ViewVariables]
public Dictionary<string, string> Layers { get; private set; }
public Dictionary<string, string> Layers { get; private set; } = new Dictionary<string, string>();
[ViewVariables]
public Dictionary<string, string> MechanismLayers { get; private set; }
public Dictionary<string, string> MechanismLayers { get; private set; } = new Dictionary<string, string>();
public bool Equals(BodyTemplate other)
{
@@ -75,7 +63,7 @@ namespace Content.Server.Body
/// Checks if the given slot exists in this <see cref="BodyTemplate"/>.
/// </summary>
/// <returns>True if it does, false otherwise.</returns>
public bool SlotExists(string slotName)
public bool HasSlot(string slotName)
{
return Slots.Keys.Any(slot => slot == slotName);
}
@@ -132,14 +120,18 @@ namespace Content.Server.Body
return hash;
}
protected virtual void LoadFromPrototype(BodyTemplatePrototype data)
public virtual void Initialize(BodyTemplatePrototype prototype)
{
Name = data.Name;
CenterSlot = data.CenterSlot;
Slots = data.Slots;
Connections = data.Connections;
Layers = data.Layers;
MechanismLayers = data.MechanismLayers;
DebugTools.Assert(!Initialized, $"{nameof(BodyTemplate)} {Name} has already been initialized!");
Name = prototype.Name;
CenterSlot = prototype.CenterSlot;
Slots = new Dictionary<string, BodyPartType>(prototype.Slots);
Connections = new Dictionary<string, List<string>>(prototype.Connections);
Layers = new Dictionary<string, string>(prototype.Layers);
MechanismLayers = new Dictionary<string, string>(prototype.MechanismLayers);
Initialized = true;
}
}
}

View File

@@ -31,9 +31,16 @@ namespace Content.Server.Body.Network
public virtual void OnRemove() { }
/// <summary>
/// Called every update by <see cref="BodyManagerComponent.Update"/>.
/// Called every update by
/// <see cref="BodyManagerComponent.PreMetabolism"/>.
/// </summary>
public virtual void Update(float frameTime) { }
public virtual void PreMetabolism(float frameTime) { }
/// <summary>
/// Called every update by
/// <see cref="BodyManagerComponent.PostMetabolism"/>.
/// </summary>
public virtual void PostMetabolism(float frameTime) { }
}
public static class BodyNetworkExtensions

View File

@@ -243,7 +243,7 @@ namespace Content.Server.Body.Surgery
performer.PopupMessage(Loc.GetString("Saw off the limb!"));
// TODO do_after: Delay
bmTarget.DisconnectBodyPart(Parent, true);
bmTarget.RemovePart(Parent, true);
}
}
}

View File

@@ -40,7 +40,7 @@ namespace Content.Server.Chemistry.ReactionEffects
float floatIntensity = (float)intensity;
if (solutionEntity == null)
return;
if(!solutionEntity.TryGetComponent(out SolutionComponent solution))
if(!solutionEntity.TryGetComponent(out SolutionContainerComponent solution))
return;
//Handle scaling

View File

@@ -29,7 +29,7 @@ namespace Content.Server.GameObjects.Components
{
var random = IoCManager.Resolve<IRobustRandom>();
var rand = random.Next(100);
// Let's not pad ourselves on the back too hard.
// Let's not pat ourselves on the back too hard.
// 1% chance of zumos
if (rand == 0) Type = PlaqueType.Zumos;
// 9% FEA

View File

@@ -0,0 +1,543 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Body;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces.GameObjects.Components.Interaction;
using Content.Shared.Body.Part.Properties.Movement;
using Content.Shared.Body.Part.Properties.Other;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Movement;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Body
{
public partial class BodyManagerComponent
{
private readonly Dictionary<string, IBodyPart> _parts = new Dictionary<string, IBodyPart>();
[ViewVariables] public BodyPreset Preset { get; private set; } = default!;
/// <summary>
/// All <see cref="IBodyPart"></see> with <see cref="LegProperty"></see>
/// that are currently affecting move speed, mapped to how big that leg
/// they're on is.
/// </summary>
[ViewVariables]
private readonly Dictionary<IBodyPart, float> _activeLegs = new Dictionary<IBodyPart, float>();
/// <summary>
/// Maps <see cref="BodyTemplate"/> slot name to the <see cref="IBodyPart"/>
/// object filling it (if there is one).
/// </summary>
[ViewVariables]
public IReadOnlyDictionary<string, IBodyPart> Parts => _parts;
/// <summary>
/// List of all occupied slots in this body, taken from the values of
/// <see cref="Parts"/>.
/// </summary>
public IEnumerable<string> OccupiedSlots => Parts.Keys;
/// <summary>
/// List of all slots in this body, taken from the keys of
/// <see cref="Template"/> slots.
/// </summary>
public IEnumerable<string> AllSlots => Template.Slots.Keys;
public bool TryAddPart(string slot, DroppedBodyPartComponent part, bool force = false)
{
DebugTools.AssertNotNull(part);
if (!TryAddPart(slot, part.ContainedBodyPart, force))
{
return false;
}
part.Owner.Delete();
return true;
}
public bool TryAddPart(string slot, IBodyPart part, bool force = false)
{
DebugTools.AssertNotNull(part);
DebugTools.AssertNotNull(slot);
// Make sure the given slot exists
if (!force)
{
if (!HasSlot(slot))
{
return false;
}
// And that nothing is in it
if (!_parts.TryAdd(slot, part))
{
return false;
}
}
else
{
_parts[slot] = part;
}
part.Body = this;
var argsAdded = new BodyPartAddedEventArgs(part, slot);
foreach (var component in Owner.GetAllComponents<IBodyPartAdded>().ToArray())
{
component.BodyPartAdded(argsAdded);
}
// TODO: Sort this duplicate out
OnBodyChanged();
if (!Template.Layers.TryGetValue(slot, out var partMap) ||
!_reflectionManager.TryParseEnumReference(partMap, out var partEnum))
{
Logger.Warning($"Template {Template.Name} has an invalid RSI map key {partMap} for body part {part.Name}.");
return false;
}
part.RSIMap = partEnum;
var partMessage = new BodyPartAddedMessage(part.RSIPath, part.RSIState, partEnum);
SendNetworkMessage(partMessage);
foreach (var mechanism in part.Mechanisms)
{
if (!Template.MechanismLayers.TryGetValue(mechanism.Id, out var mechanismMap))
{
continue;
}
if (!_reflectionManager.TryParseEnumReference(mechanismMap, out var mechanismEnum))
{
Logger.Warning($"Template {Template.Name} has an invalid RSI map key {mechanismMap} for mechanism {mechanism.Id}.");
continue;
}
var mechanismMessage = new MechanismSpriteAddedMessage(mechanismEnum);
SendNetworkMessage(mechanismMessage);
}
return true;
}
public bool HasPart(string slot)
{
return _parts.ContainsKey(slot);
}
public void RemovePart(IBodyPart part, bool drop)
{
DebugTools.AssertNotNull(part);
var slotName = _parts.FirstOrDefault(x => x.Value == part).Key;
if (string.IsNullOrEmpty(slotName)) return;
RemovePart(slotName, drop);
}
public bool RemovePart(string slot, bool drop)
{
DebugTools.AssertNotNull(slot);
if (!_parts.Remove(slot, out var part))
{
return false;
}
IEntity? dropped = null;
if (drop)
{
part.SpawnDropped(out dropped);
}
part.Body = null;
var args = new BodyPartRemovedEventArgs(part, slot);
foreach (var component in Owner.GetAllComponents<IBodyPartRemoved>())
{
component.BodyPartRemoved(args);
}
if (part.RSIMap != null)
{
var message = new BodyPartRemovedMessage(part.RSIMap, dropped?.Uid);
SendNetworkMessage(message);
}
foreach (var mechanism in part.Mechanisms)
{
if (!Template.MechanismLayers.TryGetValue(mechanism.Id, out var mechanismMap))
{
continue;
}
if (!_reflectionManager.TryParseEnumReference(mechanismMap, out var mechanismEnum))
{
Logger.Warning($"Template {Template.Name} has an invalid RSI map key {mechanismMap} for mechanism {mechanism.Id}.");
continue;
}
var mechanismMessage = new MechanismSpriteRemovedMessage(mechanismEnum);
SendNetworkMessage(mechanismMessage);
}
if (CurrentDamageState == DamageState.Dead) return true;
// creadth: fall down if no legs
if (part.PartType == BodyPartType.Leg && Parts.Count(x => x.Value.PartType == BodyPartType.Leg) == 0)
{
EntitySystem.Get<StandingStateSystem>().Down(Owner);
}
// creadth: immediately kill entity if last vital part removed
if (part.IsVital && Parts.Count(x => x.Value.PartType == part.PartType) == 0)
{
CurrentDamageState = DamageState.Dead;
ForceHealthChangedEvent();
}
if (TryGetSlotConnections(slot, out var connections))
{
foreach (var connectionName in connections)
{
if (TryGetPart(connectionName, out var result) && !ConnectedToCenter(result))
{
RemovePart(connectionName, drop);
}
}
}
OnBodyChanged();
return true;
}
public bool RemovePart(IBodyPart part, [NotNullWhen(true)] out string? slot)
{
DebugTools.AssertNotNull(part);
var pair = _parts.FirstOrDefault(kvPair => kvPair.Value == part);
if (pair.Equals(default))
{
slot = null;
return false;
}
slot = pair.Key;
return RemovePart(slot, false);
}
public IEntity? DropPart(IBodyPart part)
{
DebugTools.AssertNotNull(part);
if (!_parts.ContainsValue(part))
{
return null;
}
if (!RemovePart(part, out var slotName))
{
return null;
}
// Call disconnect on all limbs that were hanging off this limb.
if (TryGetSlotConnections(slotName, out var connections))
{
// This loop is an unoptimized travesty. TODO: optimize to be less shit
foreach (var connectionName in connections)
{
if (TryGetPart(connectionName, out var result) && !ConnectedToCenter(result))
{
RemovePart(connectionName, true);
}
}
}
part.SpawnDropped(out var dropped);
OnBodyChanged();
return dropped;
}
public bool ConnectedToCenter(IBodyPart part)
{
var searchedSlots = new List<string>();
return TryGetSlot(part, out var result) &&
ConnectedToCenterPartRecursion(searchedSlots, result);
}
private bool ConnectedToCenterPartRecursion(ICollection<string> searchedSlots, string slotName)
{
if (!TryGetPart(slotName, out var part))
{
return false;
}
if (part == CenterPart())
{
return true;
}
searchedSlots.Add(slotName);
if (!TryGetSlotConnections(slotName, out var connections))
{
return false;
}
foreach (var connection in connections)
{
if (!searchedSlots.Contains(connection) &&
ConnectedToCenterPartRecursion(searchedSlots, connection))
{
return true;
}
}
return false;
}
public IBodyPart? CenterPart()
{
Parts.TryGetValue(Template.CenterSlot, out var center);
return center;
}
public bool HasSlot(string slot)
{
return Template.HasSlot(slot);
}
public bool TryGetPart(string slot, [NotNullWhen(true)] out IBodyPart? result)
{
return Parts.TryGetValue(slot, out result);
}
public bool TryGetSlot(IBodyPart part, [NotNullWhen(true)] out string? slot)
{
// We enforce that there is only one of each value in the dictionary,
// so we can iterate through the dictionary values to get the key from there.
var pair = Parts.FirstOrDefault(x => x.Value == part);
slot = pair.Key;
return !pair.Equals(default);
}
public bool TryGetSlotType(string slot, out BodyPartType result)
{
return Template.Slots.TryGetValue(slot, out result);
}
public bool TryGetSlotConnections(string slot, [NotNullWhen(true)] out List<string>? connections)
{
return Template.Connections.TryGetValue(slot, out connections);
}
public bool TryGetPartConnections(string slot, [NotNullWhen(true)] out List<IBodyPart>? result)
{
result = null;
if (!Template.Connections.TryGetValue(slot, out var connections))
{
return false;
}
var toReturn = new List<IBodyPart>();
foreach (var connection in connections)
{
if (TryGetPart(connection, out var partResult))
{
toReturn.Add(partResult);
}
}
if (toReturn.Count <= 0)
{
return false;
}
result = toReturn;
return true;
}
public bool TryGetPartConnections(IBodyPart part, [NotNullWhen(true)] out List<IBodyPart>? connections)
{
connections = null;
return TryGetSlot(part, out var slotName) &&
TryGetPartConnections(slotName, out connections);
}
public List<IBodyPart> GetPartsOfType(BodyPartType type)
{
var toReturn = new List<IBodyPart>();
foreach (var part in Parts.Values)
{
if (part.PartType == type)
{
toReturn.Add(part);
}
}
return toReturn;
}
private void CalculateSpeed()
{
if (!Owner.TryGetComponent(out MovementSpeedModifierComponent? playerMover))
{
return;
}
float speedSum = 0;
foreach (var part in _activeLegs.Keys)
{
if (!part.HasProperty<LegProperty>())
{
_activeLegs.Remove(part);
}
}
foreach (var (key, value) in _activeLegs)
{
if (key.TryGetProperty(out LegProperty? leg))
{
// Speed of a leg = base speed * (1+log1024(leg length))
speedSum += leg.Speed * (1 + (float) Math.Log(value, 1024.0));
}
}
if (speedSum <= 0.001f || _activeLegs.Count <= 0)
{
playerMover.BaseWalkSpeed = 0.8f;
playerMover.BaseSprintSpeed = 2.0f;
}
else
{
// Extra legs stack diminishingly.
// Final speed = speed sum/(leg count-log4(leg count))
playerMover.BaseWalkSpeed =
speedSum / (_activeLegs.Count - (float) Math.Log(_activeLegs.Count, 4.0));
playerMover.BaseSprintSpeed = playerMover.BaseWalkSpeed * 1.75f;
}
}
/// <summary>
/// Called when the layout of this body changes.
/// </summary>
private void OnBodyChanged()
{
// Calculate move speed based on this body.
if (Owner.HasComponent<MovementSpeedModifierComponent>())
{
_activeLegs.Clear();
var legParts = Parts.Values.Where(x => x.HasProperty(typeof(LegProperty)));
foreach (var part in legParts)
{
var footDistance = DistanceToNearestFoot(part);
if (Math.Abs(footDistance - float.MinValue) > 0.001f)
{
_activeLegs.Add(part, footDistance);
}
}
CalculateSpeed();
}
}
/// <summary>
/// Returns the combined length of the distance to the nearest <see cref="BodyPart"/> with a
/// <see cref="FootProperty"/>. Returns <see cref="float.MinValue"/>
/// if there is no foot found. If you consider a <see cref="BodyManagerComponent"/> a node map, then it will look for
/// a foot node from the given node. It can
/// only search through BodyParts with <see cref="ExtensionProperty"/>.
/// </summary>
public float DistanceToNearestFoot(IBodyPart source)
{
if (source.HasProperty<FootProperty>() && source.TryGetProperty<ExtensionProperty>(out var property))
{
return property.ReachDistance;
}
return LookForFootRecursion(source, new List<BodyPart>());
}
private float LookForFootRecursion(IBodyPart current,
ICollection<BodyPart> searchedParts)
{
if (!current.TryGetProperty<ExtensionProperty>(out var extProperty))
{
return float.MinValue;
}
// Get all connected parts if the current part has an extension property
if (!TryGetPartConnections(current, out var connections))
{
return float.MinValue;
}
// If a connected BodyPart is a foot, return this BodyPart's length.
foreach (var connection in connections)
{
if (!searchedParts.Contains(connection) && connection.HasProperty<FootProperty>())
{
return extProperty.ReachDistance;
}
}
// Otherwise, get the recursion values of all connected BodyParts and
// store them in a list.
var distances = new List<float>();
foreach (var connection in connections)
{
if (!searchedParts.Contains(connection))
{
continue;
}
var result = LookForFootRecursion(connection, searchedParts);
if (Math.Abs(result - float.MinValue) > 0.001f)
{
distances.Add(result);
}
}
// If one or more of the searches found a foot, return the smallest one
// and add this ones length.
if (distances.Count > 0)
{
return distances.Min<float>() + extProperty.ReachDistance;
}
return float.MinValue;
// No extension property, no go.
}
}
}

View File

@@ -2,16 +2,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Body;
using Content.Server.Body.Network;
using Content.Server.GameObjects.Components.Metabolism;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces.GameObjects.Components.Interaction;
using Content.Server.Observer;
using Content.Shared.Body.Part;
using Content.Shared.Body.Part.Properties.Movement;
using Content.Shared.Body.Part.Properties.Other;
using Content.Shared.Body.Preset;
using Content.Shared.Body.Template;
using Content.Shared.GameObjects.Components.Body;
@@ -19,11 +15,8 @@ using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Movement;
using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Reflection;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
@@ -39,8 +32,9 @@ namespace Content.Server.GameObjects.Components.Body
[RegisterComponent]
[ComponentReference(typeof(IDamageableComponent))]
[ComponentReference(typeof(ISharedBodyManagerComponent))]
[ComponentReference(typeof(IBodyPartManager))]
[ComponentReference(typeof(IBodyManagerComponent))]
public class BodyManagerComponent : SharedBodyManagerComponent, IBodyPartContainer, IRelayMoveInput, IBodyManagerComponent
public partial class BodyManagerComponent : SharedBodyManagerComponent, IBodyPartContainer, IRelayMoveInput, IBodyManagerComponent
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IBodyNetworkFactory _bodyNetworkFactory = default!;
@@ -48,41 +42,10 @@ namespace Content.Server.GameObjects.Components.Body
[ViewVariables] private string _presetName = default!;
private readonly Dictionary<string, IBodyPart> _parts = new Dictionary<string, IBodyPart>();
[ViewVariables] private readonly Dictionary<Type, BodyNetwork> _networks = new Dictionary<Type, BodyNetwork>();
/// <summary>
/// All <see cref="IBodyPart"></see> with <see cref="LegProperty"></see>
/// that are currently affecting move speed, mapped to how big that leg
/// they're on is.
/// </summary>
[ViewVariables]
private readonly Dictionary<IBodyPart, float> _activeLegs = new Dictionary<IBodyPart, float>();
[ViewVariables] public BodyTemplate Template { get; private set; } = default!;
[ViewVariables] public BodyPreset Preset { get; private set; } = default!;
/// <summary>
/// Maps <see cref="BodyTemplate"/> slot name to the <see cref="IBodyPart"/>
/// object filling it (if there is one).
/// </summary>
[ViewVariables]
public IReadOnlyDictionary<string, IBodyPart> Parts => _parts;
/// <summary>
/// List of all slots in this body, taken from the keys of
/// <see cref="Template"/> slots.
/// </summary>
public IEnumerable<string> AllSlots => Template.Slots.Keys;
/// <summary>
/// List of all occupied slots in this body, taken from the values of
/// <see cref="Parts"/>.
/// </summary>
public IEnumerable<string> OccupiedSlots => Parts.Keys;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
@@ -92,14 +55,15 @@ namespace Content.Server.GameObjects.Components.Body
"bodyTemplate.Humanoid",
template =>
{
if (!_prototypeManager.TryIndex(template, out BodyTemplatePrototype templateData))
if (!_prototypeManager.TryIndex(template, out BodyTemplatePrototype prototype))
{
// Invalid prototype
throw new InvalidOperationException(
$"No {nameof(BodyTemplatePrototype)} found with name {template}");
}
Template = new BodyTemplate(templateData);
Template = new BodyTemplate();
Template.Initialize(prototype);
},
() => Template.Name);
@@ -108,14 +72,15 @@ namespace Content.Server.GameObjects.Components.Body
"bodyPreset.BasicHuman",
preset =>
{
if (!_prototypeManager.TryIndex(preset, out BodyPresetPrototype presetData))
if (!_prototypeManager.TryIndex(preset, out BodyPresetPrototype prototype))
{
// Invalid prototype
throw new InvalidOperationException(
$"No {nameof(BodyPresetPrototype)} found with name {preset}");
}
Preset = new BodyPreset(presetData);
Preset = new BodyPreset();
Preset.Initialize(prototype);
},
() => _presetName);
}
@@ -156,7 +121,7 @@ namespace Content.Server.GameObjects.Components.Body
}
// Try and remove an existing limb if that exists.
RemoveBodyPart(slotName, false);
RemovePart(slotName, false);
// Add a new BodyPart with the BodyPartPrototype as a baseline to our
// BodyComponent.
@@ -167,21 +132,21 @@ namespace Content.Server.GameObjects.Components.Body
OnBodyChanged(); // TODO: Duplicate code
}
/// <summary>
/// Changes the current <see cref="BodyTemplate"/> to the given
/// <see cref="BodyTemplate"/>.
/// Attempts to keep previous <see cref="IBodyPart"/> if there is a
/// slot for them in both <see cref="BodyTemplate"/>.
/// </summary>
public void ChangeBodyTemplate(BodyTemplatePrototype newTemplate)
{
foreach (var part in Parts)
{
// TODO: Make this work.
}
OnBodyChanged();
}
// /// <summary>
// /// Changes the current <see cref="BodyTemplate"/> to the given
// /// <see cref="BodyTemplate"/>.
// /// Attempts to keep previous <see cref="IBodyPart"/> if there is a
// /// slot for them in both <see cref="BodyTemplate"/>.
// /// </summary>
// public void ChangeBodyTemplate(BodyTemplatePrototype newTemplate)
// {
// foreach (var part in Parts)
// {
// // TODO: Make this work.
// }
//
// OnBodyChanged();
// }
/// <summary>
/// This method is called by <see cref="BodySystem.Update"/> before
@@ -201,7 +166,7 @@ namespace Content.Server.GameObjects.Components.Body
foreach (var network in _networks.Values)
{
network.Update(frameTime);
network.PreMetabolism(frameTime);
}
}
@@ -223,73 +188,7 @@ namespace Content.Server.GameObjects.Components.Body
foreach (var network in _networks.Values)
{
network.Update(frameTime);
}
}
/// <summary>
/// Called when the layout of this body changes.
/// </summary>
private void OnBodyChanged()
{
// Calculate move speed based on this body.
if (Owner.HasComponent<MovementSpeedModifierComponent>())
{
_activeLegs.Clear();
var legParts = Parts.Values.Where(x => x.HasProperty(typeof(LegProperty)));
foreach (var part in legParts)
{
var footDistance = DistanceToNearestFoot(this, part);
if (Math.Abs(footDistance - float.MinValue) > 0.001f)
{
_activeLegs.Add(part, footDistance);
}
}
CalculateSpeed();
}
}
private void CalculateSpeed()
{
if (!Owner.TryGetComponent(out MovementSpeedModifierComponent? playerMover))
{
return;
}
float speedSum = 0;
foreach (var part in _activeLegs.Keys)
{
if (!part.HasProperty<LegProperty>())
{
_activeLegs.Remove(part);
}
}
foreach (var (key, value) in _activeLegs)
{
if (key.TryGetProperty(out LegProperty? leg))
{
// Speed of a leg = base speed * (1+log1024(leg length))
speedSum += leg.Speed * (1 + (float) Math.Log(value, 1024.0));
}
}
if (speedSum <= 0.001f || _activeLegs.Count <= 0)
{
playerMover.BaseWalkSpeed = 0.8f;
playerMover.BaseSprintSpeed = 2.0f;
}
else
{
// Extra legs stack diminishingly.
// Final speed = speed sum/(leg count-log4(leg count))
playerMover.BaseWalkSpeed =
speedSum / (_activeLegs.Count - (float) Math.Log(_activeLegs.Count, 4.0));
playerMover.BaseSprintSpeed = playerMover.BaseWalkSpeed * 1.75f;
network.PostMetabolism(frameTime);
}
}
@@ -301,482 +200,6 @@ namespace Content.Server.GameObjects.Components.Body
}
}
#region BodyPart Functions
/// <summary>
/// Recursively searches for if <see cref="target"/> is connected to
/// the center. Not efficient (O(n^2)), but most bodies don't have a ton
/// of <see cref="IBodyPart"/>s.
/// </summary>
/// <param name="target">The body part to find the center for.</param>
/// <returns>True if it is connected to the center, false otherwise.</returns>
private bool ConnectedToCenterPart(IBodyPart target)
{
var searchedSlots = new List<string>();
return TryGetSlotName(target, out var result) &&
ConnectedToCenterPartRecursion(searchedSlots, result);
}
private bool ConnectedToCenterPartRecursion(ICollection<string> searchedSlots, string slotName)
{
if (!TryGetBodyPart(slotName, out var part))
{
return false;
}
if (part == GetCenterBodyPart())
{
return true;
}
searchedSlots.Add(slotName);
if (!TryGetBodyPartConnections(slotName, out List<string> connections))
{
return false;
}
foreach (var connection in connections)
{
if (!searchedSlots.Contains(connection) &&
ConnectedToCenterPartRecursion(searchedSlots, connection))
{
return true;
}
}
return false;
}
/// <summary>
/// Finds the central <see cref="IBodyPart"/>, if any, of this body based on
/// the <see cref="BodyTemplate"/>. For humans, this is the torso.
/// </summary>
/// <returns>The <see cref="BodyPart"/> if one exists, null otherwise.</returns>
private IBodyPart? GetCenterBodyPart()
{
Parts.TryGetValue(Template.CenterSlot, out var center);
return center;
}
/// <summary>
/// Returns whether the given slot name exists within the current
/// <see cref="BodyTemplate"/>.
/// </summary>
private bool SlotExists(string slotName)
{
return Template.SlotExists(slotName);
}
/// <summary>
/// Finds the <see cref="IBodyPart"/> in the given <see cref="slotName"/> if
/// one exists.
/// </summary>
/// <param name="slotName">The slot to search in.</param>
/// <param name="result">The body part in that slot, if any.</param>
/// <returns>True if found, false otherwise.</returns>
private bool TryGetBodyPart(string slotName, [NotNullWhen(true)] out IBodyPart? result)
{
return Parts.TryGetValue(slotName, out result!);
}
/// <summary>
/// Finds the slotName that the given <see cref="IBodyPart"/> resides in.
/// </summary>
/// <param name="part">The <see cref="IBodyPart"/> to find the slot for.</param>
/// <param name="result">The slot found, if any.</param>
/// <returns>True if a slot was found, false otherwise</returns>
private bool TryGetSlotName(IBodyPart part, [NotNullWhen(true)] out string result)
{
// We enforce that there is only one of each value in the dictionary,
// so we can iterate through the dictionary values to get the key from there.
var pair = Parts.FirstOrDefault(x => x.Value == part);
result = pair.Key;
return !pair.Equals(default);
}
/// <summary>
/// Finds the <see cref="BodyPartType"/> in the given
/// <see cref="slotName"/> if one exists.
/// </summary>
/// <param name="slotName">The slot to search in.</param>
/// <param name="result">
/// The <see cref="BodyPartType"/> of that slot, if any.
/// </param>
/// <returns>True if found, false otherwise.</returns>
public bool TryGetSlotType(string slotName, out BodyPartType result)
{
return Template.Slots.TryGetValue(slotName, out result);
}
/// <summary>
/// Finds the names of all slots connected to the given
/// <see cref="slotName"/> for the template.
/// </summary>
/// <param name="slotName">The slot to search in.</param>
/// <param name="connections">The connections found, if any.</param>
/// <returns>True if the connections are found, false otherwise.</returns>
private bool TryGetBodyPartConnections(string slotName, [NotNullWhen(true)] out List<string> connections)
{
return Template.Connections.TryGetValue(slotName, out connections!);
}
/// <summary>
/// Grabs all occupied slots connected to the given slot,
/// regardless of whether the given <see cref="slotName"/> is occupied.
/// </summary>
/// <param name="slotName">The slot name to find connections from.</param>
/// <param name="result">The connected body parts, if any.</param>
/// <returns>
/// True if successful, false if there was an error or no connected
/// <see cref="BodyPart"/>s were found.
/// </returns>
public bool TryGetBodyPartConnections(string slotName, [NotNullWhen(true)] out List<IBodyPart> result)
{
result = null!;
if (!Template.Connections.TryGetValue(slotName, out var connections))
{
return false;
}
var toReturn = new List<IBodyPart>();
foreach (var connection in connections)
{
if (TryGetBodyPart(connection, out var bodyPartResult))
{
toReturn.Add(bodyPartResult);
}
}
if (toReturn.Count <= 0)
{
return false;
}
result = toReturn;
return true;
}
/// <summary>
/// Grabs all parts connected to the given <see cref="part"/>, regardless
/// of whether the given <see cref="part"/> is occupied.
/// </summary>
/// <returns>
/// True if successful, false if there was an error or no connected
/// <see cref="IBodyPart"/>s were found.
/// </returns>
private bool TryGetBodyPartConnections(IBodyPart part, [NotNullWhen(true)] out List<IBodyPart> result)
{
result = null!;
return TryGetSlotName(part, out var slotName) &&
TryGetBodyPartConnections(slotName, out result);
}
/// <summary>
/// Grabs all <see cref="IBodyPart"/> of the given type in this body.
/// </summary>
public List<IBodyPart> GetBodyPartsOfType(BodyPartType type)
{
var toReturn = new List<IBodyPart>();
foreach (var part in Parts.Values)
{
if (part.PartType == type)
{
toReturn.Add(part);
}
}
return toReturn;
}
/// <summary>
/// Installs the given <see cref="DroppedBodyPartComponent"/> into the
/// given slot, deleting the <see cref="IEntity"/> afterwards.
/// </summary>
/// <returns>True if successful, false otherwise.</returns>
public bool InstallDroppedBodyPart(DroppedBodyPartComponent part, string slotName)
{
DebugTools.AssertNotNull(part);
if (!TryAddPart(slotName, part.ContainedBodyPart))
{
return false;
}
part.Owner.Delete();
return true;
}
/// <summary>
/// Disconnects the given <see cref="IBodyPart"/> reference, potentially
/// dropping other <see cref="IBodyPart">BodyParts</see> if they were hanging
/// off of it.
/// </summary>
/// <returns>
/// The <see cref="IEntity"/> representing the dropped
/// <see cref="IBodyPart"/>, or null if none was dropped.
/// </returns>
public IEntity? DropPart(IBodyPart part)
{
DebugTools.AssertNotNull(part);
if (!_parts.ContainsValue(part))
{
return null;
}
if (!RemoveBodyPart(part, out var slotName))
{
return null;
}
// Call disconnect on all limbs that were hanging off this limb.
if (TryGetBodyPartConnections(slotName, out List<string> connections))
{
// This loop is an unoptimized travesty. TODO: optimize to be less shit
foreach (var connectionName in connections)
{
if (TryGetBodyPart(connectionName, out var result) && !ConnectedToCenterPart(result))
{
DisconnectBodyPart(connectionName, true);
}
}
}
part.SpawnDropped(out var dropped);
OnBodyChanged();
return dropped;
}
/// <summary>
/// Disconnects the given <see cref="IBodyPart"/> reference, potentially
/// dropping other <see cref="IBodyPart">BodyParts</see> if they were hanging
/// off of it.
/// </summary>
public void DisconnectBodyPart(IBodyPart part, bool dropEntity)
{
DebugTools.AssertNotNull(part);
var slotName = _parts.FirstOrDefault(x => x.Value == part).Key;
if (string.IsNullOrEmpty(slotName)) return;
DisconnectBodyPart(slotName, dropEntity);
}
/// <summary>
/// Disconnects a body part in the given slot if one exists,
/// optionally dropping it.
/// </summary>
/// <param name="slotName">The slot to remove the body part from</param>
/// <param name="dropEntity">
/// Whether or not to drop the body part as an entity if it exists.
/// </param>
private void DisconnectBodyPart(string slotName, bool dropEntity)
{
DebugTools.AssertNotNull(slotName);
if (!HasPart(slotName))
{
return;
}
RemoveBodyPart(slotName, dropEntity);
if (TryGetBodyPartConnections(slotName, out List<string> connections))
{
foreach (var connectionName in connections)
{
if (TryGetBodyPart(connectionName, out var result) && !ConnectedToCenterPart(result))
{
DisconnectBodyPart(connectionName, dropEntity);
}
}
}
OnBodyChanged();
}
public bool TryAddPart(string slot, IBodyPart part, bool force = false)
{
DebugTools.AssertNotNull(part);
DebugTools.AssertNotNull(slot);
// Make sure the given slot exists
if (!force)
{
if (!SlotExists(slot))
{
return false;
}
// And that nothing is in it
if (!_parts.TryAdd(slot, part))
{
return false;
}
}
else
{
_parts[slot] = part;
}
part.Body = this;
var argsAdded = new BodyPartAddedEventArgs(part, slot);
foreach (var component in Owner.GetAllComponents<IBodyPartAdded>().ToArray())
{
component.BodyPartAdded(argsAdded);
}
// TODO: Sort this duplicate out
OnBodyChanged();
if (!Template.Layers.TryGetValue(slot, out var partMap) ||
!_reflectionManager.TryParseEnumReference(partMap, out var partEnum))
{
Logger.Warning($"Template {Template.Name} has an invalid RSI map key {partMap} for body part {part.Name}.");
return false;
}
part.RSIMap = partEnum;
var partMessage = new BodyPartAddedMessage(part.RSIPath, part.RSIState, partEnum);
SendNetworkMessage(partMessage);
foreach (var mechanism in part.Mechanisms)
{
if (!Template.MechanismLayers.TryGetValue(mechanism.Id, out var mechanismMap))
{
continue;
}
if (!_reflectionManager.TryParseEnumReference(mechanismMap, out var mechanismEnum))
{
Logger.Warning($"Template {Template.Name} has an invalid RSI map key {mechanismMap} for mechanism {mechanism.Id}.");
continue;
}
var mechanismMessage = new MechanismSpriteAddedMessage(mechanismEnum);
SendNetworkMessage(mechanismMessage);
}
return true;
}
public bool HasPart(string slot)
{
return _parts.ContainsKey(slot);
}
/// <summary>
/// Removes the body part in slot <see cref="slotName"/> from this body,
/// if one exists.
/// </summary>
/// <param name="slotName">The slot to remove it from.</param>
/// <param name="drop">
/// Whether or not to drop the removed <see cref="IBodyPart"/>.
/// </param>
/// <returns></returns>
private bool RemoveBodyPart(string slotName, bool drop)
{
DebugTools.AssertNotNull(slotName);
if (!_parts.Remove(slotName, out var part))
{
return false;
}
IEntity? dropped = null;
if (drop)
{
part.SpawnDropped(out dropped);
}
part.Body = null;
var args = new BodyPartRemovedEventArgs(part, slotName);
foreach (var component in Owner.GetAllComponents<IBodyPartRemoved>())
{
component.BodyPartRemoved(args);
}
if (part.RSIMap != null)
{
var message = new BodyPartRemovedMessage(part.RSIMap, dropped?.Uid);
SendNetworkMessage(message);
}
foreach (var mechanism in part.Mechanisms)
{
if (!Template.MechanismLayers.TryGetValue(mechanism.Id, out var mechanismMap))
{
continue;
}
if (!_reflectionManager.TryParseEnumReference(mechanismMap, out var mechanismEnum))
{
Logger.Warning($"Template {Template.Name} has an invalid RSI map key {mechanismMap} for mechanism {mechanism.Id}.");
continue;
}
var mechanismMessage = new MechanismSpriteRemovedMessage(mechanismEnum);
SendNetworkMessage(mechanismMessage);
}
if (CurrentDamageState == DamageState.Dead) return true;
// creadth: fall down if no legs
if (part.PartType == BodyPartType.Leg && Parts.Count(x => x.Value.PartType == BodyPartType.Leg) == 0)
{
EntitySystem.Get<StandingStateSystem>().Down(Owner);
}
// creadth: immediately kill entity if last vital part removed
if (part.IsVital && Parts.Count(x => x.Value.PartType == part.PartType) == 0)
{
CurrentDamageState = DamageState.Dead;
ForceHealthChangedEvent();
}
return true;
}
/// <summary>
/// Removes the body part from this body, if one exists.
/// </summary>
/// <param name="part">The part to remove from this body.</param>
/// <param name="slotName">The slot that the part was in, if any.</param>
/// <returns>True if <see cref="part"/> was removed, false otherwise.</returns>
private bool RemoveBodyPart(IBodyPart part, [NotNullWhen(true)] out string? slotName)
{
DebugTools.AssertNotNull(part);
var pair = _parts.FirstOrDefault(kvPair => kvPair.Value == part);
if (pair.Equals(default))
{
slotName = null;
return false;
}
slotName = pair.Key;
return RemoveBodyPart(slotName, false);
}
#endregion
#region BodyNetwork Functions
private bool EnsureNetwork(BodyNetwork network)
@@ -854,81 +277,6 @@ namespace Content.Server.GameObjects.Components.Body
}
#endregion
#region Recursion Functions
/// <summary>
/// Returns the combined length of the distance to the nearest <see cref="BodyPart"/> with a
/// <see cref="FootProperty"/>. Returns <see cref="float.MinValue"/>
/// if there is no foot found. If you consider a <see cref="BodyManagerComponent"/> a node map, then it will look for
/// a foot node from the given node. It can
/// only search through BodyParts with <see cref="ExtensionProperty"/>.
/// </summary>
private static float DistanceToNearestFoot(BodyManagerComponent body, IBodyPart source)
{
if (source.HasProperty<FootProperty>() && source.TryGetProperty<ExtensionProperty>(out var property))
{
return property.ReachDistance;
}
return LookForFootRecursion(body, source, new List<BodyPart>());
}
// TODO: Make this not static and not keep me up at night
private static float LookForFootRecursion(BodyManagerComponent body, IBodyPart current,
ICollection<BodyPart> searchedParts)
{
if (!current.TryGetProperty<ExtensionProperty>(out var extProperty))
{
return float.MinValue;
}
// Get all connected parts if the current part has an extension property
if (!body.TryGetBodyPartConnections(current, out var connections))
{
return float.MinValue;
}
// If a connected BodyPart is a foot, return this BodyPart's length.
foreach (var connection in connections)
{
if (!searchedParts.Contains(connection) && connection.HasProperty<FootProperty>())
{
return extProperty.ReachDistance;
}
}
// Otherwise, get the recursion values of all connected BodyParts and
// store them in a list.
var distances = new List<float>();
foreach (var connection in connections)
{
if (!searchedParts.Contains(connection))
{
continue;
}
var result = LookForFootRecursion(body, connection, searchedParts);
if (Math.Abs(result - float.MinValue) > 0.001f)
{
distances.Add(result);
}
}
// If one or more of the searches found a foot, return the smallest one
// and add this ones length.
if (distances.Count > 0)
{
return distances.Min<float>() + extProperty.ReachDistance;
}
return float.MinValue;
// No extension property, no go.
}
#endregion
}
public interface IBodyManagerHealthChangeParams

View File

@@ -22,7 +22,7 @@ namespace Content.Server.GameObjects.Components.Body.Circulatory
/// <summary>
/// Internal solution for reagent storage
/// </summary>
[ViewVariables] private SolutionComponent _internalSolution;
[ViewVariables] private SolutionContainerComponent _internalSolution;
/// <summary>
/// Empty volume of internal solution
@@ -31,13 +31,13 @@ namespace Content.Server.GameObjects.Components.Body.Circulatory
[ViewVariables] public GasMixture Air { get; set; }
[ViewVariables] public SolutionComponent Solution => _internalSolution;
[ViewVariables] public SolutionContainerComponent Solution => _internalSolution;
public override void Initialize()
{
base.Initialize();
_internalSolution = Owner.EnsureComponent<SolutionComponent>();
_internalSolution = Owner.EnsureComponent<SolutionContainerComponent>();
_internalSolution.MaxVolume = _initialMaxVolume;
}

View File

@@ -24,10 +24,10 @@ namespace Content.Server.GameObjects.Components.Body.Digestive
/// </summary>
public ReagentUnit MaxVolume
{
get => Owner.TryGetComponent(out SolutionComponent? solution) ? solution.MaxVolume : ReagentUnit.Zero;
get => Owner.TryGetComponent(out SolutionContainerComponent? solution) ? solution.MaxVolume : ReagentUnit.Zero;
set
{
if (Owner.TryGetComponent(out SolutionComponent? solution))
if (Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
solution.MaxVolume = value;
}
@@ -64,9 +64,9 @@ namespace Content.Server.GameObjects.Components.Body.Digestive
{
base.Startup();
if (!Owner.EnsureComponent(out SolutionComponent solution))
if (!Owner.EnsureComponent(out SolutionContainerComponent solution))
{
Logger.Warning($"Entity {Owner} at {Owner.Transform.MapPosition} didn't have a {nameof(SolutionComponent)}");
Logger.Warning($"Entity {Owner} at {Owner.Transform.MapPosition} didn't have a {nameof(SolutionContainerComponent)}");
}
solution.MaxVolume = _initialMaxVolume;
@@ -74,7 +74,7 @@ namespace Content.Server.GameObjects.Components.Body.Digestive
public bool TryTransferSolution(Solution solution)
{
if (!Owner.TryGetComponent(out SolutionComponent? solutionComponent))
if (!Owner.TryGetComponent(out SolutionContainerComponent? solutionComponent))
{
return false;
}
@@ -104,7 +104,7 @@ namespace Content.Server.GameObjects.Components.Body.Digestive
/// <param name="frameTime">The time since the last update in seconds.</param>
public void Update(float frameTime)
{
if (!Owner.TryGetComponent(out SolutionComponent? solutionComponent) ||
if (!Owner.TryGetComponent(out SolutionContainerComponent? solutionComponent) ||
!Owner.TryGetComponent(out BloodstreamComponent? bloodstream))
{
return;

View File

@@ -91,7 +91,7 @@ namespace Content.Server.GameObjects.Components.Body
{
if (!bodyManager.TryGetSlotType(slot, out var typeResult) ||
typeResult != ContainedBodyPart?.PartType ||
!bodyManager.TryGetBodyPartConnections(slot, out var parts))
!bodyManager.TryGetPartConnections(slot, out var parts))
{
continue;
}
@@ -151,7 +151,7 @@ namespace Content.Server.GameObjects.Components.Body
var target = (string) targetObject!;
string message;
if (_bodyManagerComponentCache.InstallDroppedBodyPart(this, target))
if (_bodyManagerComponentCache.TryAddPart(target, this))
{
message = Loc.GetString("You attach {0:theName}.", ContainedBodyPart);
}

View File

@@ -6,20 +6,14 @@ using Content.Shared.GameObjects.Components.Body;
namespace Content.Server.GameObjects.Components.Body
{
// TODO: Merge with ISharedBodyManagerComponent
public interface IBodyManagerComponent : ISharedBodyManagerComponent
public interface IBodyManagerComponent : ISharedBodyManagerComponent, IBodyPartManager
{
/// <summary>
/// The <see cref="BodyTemplate"/> that this <see cref="BodyManagerComponent"/>
/// is adhering to.
/// The <see cref="BodyTemplate"/> that this
/// <see cref="BodyManagerComponent"/> is adhering to.
/// </summary>
public BodyTemplate Template { get; }
/// <summary>
/// The <see cref="BodyPreset"/> that this <see cref="BodyManagerComponent"/>
/// is adhering to.
/// </summary>
public BodyPreset Preset { get; }
/// <summary>
/// Installs the given <see cref="IBodyPart"/> into the given slot.
/// </summary>

View File

@@ -0,0 +1,154 @@
#nullable enable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Content.Server.Body;
using Content.Shared.GameObjects.Components.Body;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.GameObjects.Components.Body
{
public interface IBodyPartManager : IComponent
{
/// <summary>
/// The <see cref="BodyPreset"/> that this
/// <see cref="BodyManagerComponent"/>
/// is adhering to.
/// </summary>
public BodyPreset Preset { get; }
/// <summary>
/// Installs the given <see cref="DroppedBodyPartComponent"/> into the
/// given slot, deleting the <see cref="IEntity"/> afterwards.
/// </summary>
/// <returns>True if successful, false otherwise.</returns>
bool TryAddPart(string slot, DroppedBodyPartComponent part, bool force = false);
bool TryAddPart(string slot, IBodyPart part, bool force = false);
bool HasPart(string slot);
/// <summary>
/// Removes the given <see cref="IBodyPart"/> reference, potentially
/// dropping other <see cref="IBodyPart">BodyParts</see> if they
/// were hanging off of it.
/// </summary>
void RemovePart(IBodyPart part, bool drop);
/// <summary>
/// Removes the body part in slot <see cref="slot"/> from this body,
/// if one exists.
/// </summary>
/// <param name="slot">The slot to remove it from.</param>
/// <param name="drop">
/// Whether or not to drop the removed <see cref="IBodyPart"/>.
/// </param>
/// <returns>True if the part was removed, false otherwise.</returns>
bool RemovePart(string slot, bool drop);
/// <summary>
/// Removes the body part from this body, if one exists.
/// </summary>
/// <param name="part">The part to remove from this body.</param>
/// <param name="slotName">The slot that the part was in, if any.</param>
/// <returns>True if <see cref="part"/> was removed, false otherwise.</returns>
bool RemovePart(IBodyPart part, [NotNullWhen(true)] out string? slotName);
/// <summary>
/// Disconnects the given <see cref="IBodyPart"/> reference, potentially
/// dropping other <see cref="IBodyPart">BodyParts</see> if they were hanging
/// off of it.
/// </summary>
/// <returns>
/// The <see cref="IEntity"/> representing the dropped
/// <see cref="IBodyPart"/>, or null if none was dropped.
/// </returns>
IEntity? DropPart(IBodyPart part);
/// <summary>
/// Recursively searches for if <see cref="part"/> is connected to
/// the center.
/// </summary>
/// <param name="part">The body part to find the center for.</param>
/// <returns>True if it is connected to the center, false otherwise.</returns>
bool ConnectedToCenter(IBodyPart part);
/// <summary>
/// Finds the central <see cref="IBodyPart"/>, if any, of this body based on
/// the <see cref="BodyTemplate"/>. For humans, this is the torso.
/// </summary>
/// <returns>The <see cref="BodyPart"/> if one exists, null otherwise.</returns>
IBodyPart? CenterPart();
/// <summary>
/// Returns whether the given part slot name exists within the current
/// <see cref="BodyTemplate"/>.
/// </summary>
/// <param name="slot">The slot to check for.</param>
/// <returns>True if the slot exists in this body, false otherwise.</returns>
bool HasSlot(string slot);
/// <summary>
/// Finds the <see cref="IBodyPart"/> in the given <see cref="slot"/> if
/// one exists.
/// </summary>
/// <param name="slot">The part slot to search in.</param>
/// <param name="result">The body part in that slot, if any.</param>
/// <returns>True if found, false otherwise.</returns>
bool TryGetPart(string slot, [NotNullWhen(true)] out IBodyPart? result);
/// <summary>
/// Finds the slotName that the given <see cref="IBodyPart"/> resides in.
/// </summary>
/// <param name="part">The <see cref="IBodyPart"/> to find the slot for.</param>
/// <param name="slot">The slot found, if any.</param>
/// <returns>True if a slot was found, false otherwise</returns>
bool TryGetSlot(IBodyPart part, [NotNullWhen(true)] out string? slot);
/// <summary>
/// Finds the <see cref="BodyPartType"/> in the given
/// <see cref="slot"/> if one exists.
/// </summary>
/// <param name="slot">The slot to search in.</param>
/// <param name="result">
/// The <see cref="BodyPartType"/> of that slot, if any.
/// </param>
/// <returns>True if found, false otherwise.</returns>
bool TryGetSlotType(string slot, out BodyPartType result);
/// <summary>
/// Finds the names of all slots connected to the given
/// <see cref="slot"/> for the template.
/// </summary>
/// <param name="slot">The slot to search in.</param>
/// <param name="connections">The connections found, if any.</param>
/// <returns>True if the connections are found, false otherwise.</returns>
bool TryGetSlotConnections(string slot, [NotNullWhen(true)] out List<string>? connections);
/// <summary>
/// Grabs all occupied slots connected to the given slot,
/// regardless of whether the given <see cref="slot"/> is occupied.
/// </summary>
/// <param name="slot">The slot name to find connections from.</param>
/// <param name="connections">The connected body parts, if any.</param>
/// <returns>
/// True if successful, false if the slot couldn't be found on this body.
/// </returns>
bool TryGetPartConnections(string slot, [NotNullWhen(true)] out List<IBodyPart>? connections);
/// <summary>
/// Grabs all parts connected to the given <see cref="part"/>, regardless
/// of whether the given <see cref="part"/> is occupied.
/// </summary>
/// <param name="part">The part to find connections from.</param>
/// <param name="connections">The connected body parts, if any.</param>
/// <returns>
/// True if successful, false if the part couldn't be found on this body.
/// </returns>
bool TryGetPartConnections(IBodyPart part, [NotNullWhen(true)] out List<IBodyPart>? connections);
/// <summary>
/// Grabs all <see cref="IBodyPart"/> of the given type in this body.
/// </summary>
List<IBodyPart> GetPartsOfType(BodyPartType type);
}
}

View File

@@ -42,14 +42,12 @@ namespace Content.Server.GameObjects.Components.Chemistry
{
[ViewVariables] private ContainerSlot _beakerContainer = default!;
[ViewVariables] private string _packPrototypeId = "";
[ViewVariables] private bool HasBeaker => _beakerContainer.ContainedEntity != null;
[ViewVariables] private bool _bufferModeTransfer = true;
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables] private readonly SolutionComponent BufferSolution = new SolutionComponent();
[ViewVariables] private readonly SolutionContainerComponent BufferSolution = new SolutionContainerComponent();
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(ChemMasterUiKey.Key);
@@ -179,7 +177,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
"", Owner.Name, new List<Solution.ReagentQuantity>(), BufferSolution.ReagentList.ToList(), _bufferModeTransfer, BufferSolution.CurrentVolume, BufferSolution.MaxVolume);
}
var solution = beaker.GetComponent<SolutionComponent>();
var solution = beaker.GetComponent<SolutionContainerComponent>();
return new ChemMasterBoundUserInterfaceState(Powered, true, solution.CurrentVolume, solution.MaxVolume,
beaker.Name, Owner.Name, solution.ReagentList.ToList(), BufferSolution.ReagentList.ToList(), _bufferModeTransfer, BufferSolution.CurrentVolume, BufferSolution.MaxVolume);
}
@@ -191,7 +189,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
}
/// <summary>
/// If this component contains an entity with a <see cref="SolutionComponent"/>, eject it.
/// If this component contains an entity with a <see cref="SolutionContainerComponent"/>, eject it.
/// Tries to eject into user's hands first, then ejects onto chem master if both hands are full.
/// </summary>
private void TryEject(IEntity user)
@@ -213,7 +211,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
{
if (!HasBeaker && _bufferModeTransfer) return;
var beaker = _beakerContainer.ContainedEntity;
var beakerSolution = beaker.GetComponent<SolutionComponent>();
var beakerSolution = beaker.GetComponent<SolutionContainerComponent>();
if (isBuffer)
{
foreach (var reagent in BufferSolution.Solution.Contents)
@@ -283,7 +281,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
var bufferSolution = BufferSolution.Solution.SplitSolution(actualVolume);
bottle.TryGetComponent<SolutionComponent>(out var bottleSolution);
bottle.TryGetComponent<SolutionContainerComponent>(out var bottleSolution);
bottleSolution?.Solution.AddSolution(bufferSolution);
//Try to give them the bottle
@@ -317,7 +315,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
var bufferSolution = BufferSolution.Solution.SplitSolution(actualVolume);
pill.TryGetComponent<SolutionComponent>(out var pillSolution);
pill.TryGetComponent<SolutionContainerComponent>(out var pillSolution);
pillSolution?.Solution.AddSolution(bufferSolution);
//Try to give them the bottle
@@ -368,7 +366,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
/// <summary>
/// Called when you click the owner entity with something in your active hand. If the entity in your hand
/// contains a <see cref="SolutionComponent"/>, if you have hands, and if the chem master doesn't already
/// contains a <see cref="SolutionContainerComponent"/>, if you have hands, and if the chem master doesn't already
/// hold a container, it will be added to the chem master.
/// </summary>
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
@@ -377,27 +375,27 @@ namespace Content.Server.GameObjects.Components.Chemistry
{
if (!args.User.TryGetComponent(out IHandsComponent? hands))
{
Owner.PopupMessage(args.User, Loc.GetString("You have no hands."));
Owner.PopupMessage(args.User, Loc.GetString("You have no hands!"));
return true;
}
if (hands.GetActiveHand == null)
{
Owner.PopupMessage(args.User, Loc.GetString("You have nothing on your hand."));
Owner.PopupMessage(args.User, Loc.GetString("You have nothing in your hand!"));
return false;
}
var activeHandEntity = hands.GetActiveHand.Owner;
if (activeHandEntity.TryGetComponent<SolutionComponent>(out var solution))
if (activeHandEntity.TryGetComponent<SolutionContainerComponent>(out var solution))
{
if (HasBeaker)
{
Owner.PopupMessage(args.User, Loc.GetString("This ChemMaster already has a container in it."));
}
else if ((solution.Capabilities & SolutionCaps.FitsInDispenser) == 0) //Close enough to a chem master...
else if (!solution.CanUseWithChemDispenser)
{
//If it can't fit in the chem master, don't put it in. For example, buckets and mop buckets can't fit.
Owner.PopupMessage(args.User, Loc.GetString("That can't fit in the ChemMaster."));
Owner.PopupMessage(args.User, Loc.GetString("The {0:theName} is too large for the ChemMaster!", activeHandEntity));
}
else
{
@@ -407,7 +405,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
}
else
{
Owner.PopupMessage(args.User, Loc.GetString("You can't put this in the ChemMaster."));
Owner.PopupMessage(args.User, Loc.GetString("You can't put {0:theName} in the ChemMaster!", activeHandEntity));
}
return true;

View File

@@ -61,12 +61,8 @@ namespace Content.Server.GameObjects.Components.Chemistry
{
base.Startup();
Owner.EnsureComponent<SolutionComponent>();
if (Owner.TryGetComponent(out SolutionComponent? solution))
{
solution.Capabilities |= SolutionCaps.Injector;
}
var solution = Owner.EnsureComponent<SolutionContainerComponent>();
solution.Capabilities = SolutionContainerCaps.AddTo | SolutionContainerCaps.RemoveFrom;
// Set _toggleState based on prototype
_toggleState = _injectOnly ? InjectorToggleMode.Inject : InjectorToggleMode.Draw;
@@ -111,30 +107,51 @@ namespace Content.Server.GameObjects.Components.Chemistry
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) return;
//Make sure we have the attacking entity
if (eventArgs.Target == null || !Owner.TryGetComponent(out SolutionComponent? solution) || !solution.Injector)
if (eventArgs.Target == null || !Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
return;
}
var targetEntity = eventArgs.Target;
//Handle injecting/drawing for solutions
if (targetEntity.TryGetComponent<SolutionComponent>(out var targetSolution) && targetSolution.Injectable)
// Handle injecting/drawing for solutions
if (targetEntity.TryGetComponent<SolutionContainerComponent>(out var targetSolution))
{
if (_toggleState == InjectorToggleMode.Inject)
{
TryInject(targetSolution, eventArgs.User);
if (solution.CanRemoveSolutions && targetSolution.CanAddSolutions)
{
TryInject(targetSolution, eventArgs.User);
}
else
{
eventArgs.User.PopupMessage(eventArgs.User, Loc.GetString("You aren't able to transfer to {0:theName}!", targetSolution.Owner));
}
}
else if (_toggleState == InjectorToggleMode.Draw)
{
TryDraw(targetSolution, eventArgs.User);
if (targetSolution.CanRemoveSolutions && solution.CanAddSolutions)
{
TryDraw(targetSolution, eventArgs.User);
}
else
{
eventArgs.User.PopupMessage(eventArgs.User, Loc.GetString("You aren't able to draw from {0:theName}!", targetSolution.Owner));
}
}
}
else //Handle injecting into bloodstream
else // Handle injecting into bloodstream
{
if (targetEntity.TryGetComponent(out BloodstreamComponent? bloodstream) &&
_toggleState == InjectorToggleMode.Inject)
if (targetEntity.TryGetComponent(out BloodstreamComponent? bloodstream) && _toggleState == InjectorToggleMode.Inject)
{
TryInjectIntoBloodstream(bloodstream, eventArgs.User);
if (solution.CanRemoveSolutions)
{
TryInjectIntoBloodstream(bloodstream, eventArgs.User);
}
else
{
eventArgs.User.PopupMessage(eventArgs.User, Loc.GetString("You aren't able to inject {0:theName}!", targetEntity));
}
}
}
}
@@ -152,88 +169,91 @@ namespace Content.Server.GameObjects.Components.Chemistry
private void TryInjectIntoBloodstream(BloodstreamComponent targetBloodstream, IEntity user)
{
if (!Owner.TryGetComponent(out SolutionComponent? solution) ||
solution.CurrentVolume == 0)
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.CurrentVolume == 0)
{
return;
}
//Get transfer amount. May be smaller than _transferAmount if not enough room
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetBloodstream.EmptyVolume);
if (realTransferAmount <= 0)
{
Owner.PopupMessage(user, Loc.GetString("Container full."));
Owner.PopupMessage(user, Loc.GetString("You aren't able to inject {0:theName}!", targetBloodstream.Owner));
return;
}
//Move units from attackSolution to targetSolution
// Move units from attackSolution to targetSolution
var removedSolution = solution.SplitSolution(realTransferAmount);
if (!targetBloodstream.TryTransferSolution(removedSolution))
{
return;
}
Owner.PopupMessage(user, Loc.GetString("Injected {0}u", removedSolution.TotalVolume));
Owner.PopupMessage(user, Loc.GetString("You inject {0}u into {1:theName}!", removedSolution.TotalVolume, targetBloodstream.Owner));
Dirty();
}
private void TryInject(SolutionComponent targetSolution, IEntity user)
private void TryInject(SolutionContainerComponent targetSolution, IEntity user)
{
if (!Owner.TryGetComponent(out SolutionComponent? solution) ||
solution.CurrentVolume == 0)
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.CurrentVolume == 0)
{
return;
}
//Get transfer amount. May be smaller than _transferAmount if not enough room
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetSolution.EmptyVolume);
if (realTransferAmount <= 0)
{
Owner.PopupMessage(user, Loc.GetString("Container full."));
Owner.PopupMessage(user, Loc.GetString("{0:theName} is already full!", targetSolution.Owner));
return;
}
//Move units from attackSolution to targetSolution
// Move units from attackSolution to targetSolution
var removedSolution = solution.SplitSolution(realTransferAmount);
if (!targetSolution.TryAddSolution(removedSolution))
{
return;
}
Owner.PopupMessage(user, Loc.GetString("Injected {0}u", removedSolution.TotalVolume));
Owner.PopupMessage(user, Loc.GetString("You transfter {0}u to {1:theName}", removedSolution.TotalVolume, targetSolution.Owner));
Dirty();
}
private void TryDraw(SolutionComponent targetSolution, IEntity user)
private void TryDraw(SolutionContainerComponent targetSolution, IEntity user)
{
if (!Owner.TryGetComponent(out SolutionComponent? solution) ||
solution.EmptyVolume == 0)
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.EmptyVolume == 0)
{
return;
}
//Get transfer amount. May be smaller than _transferAmount if not enough room
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetSolution.CurrentVolume);
if (realTransferAmount <= 0)
{
Owner.PopupMessage(user, Loc.GetString("Container empty"));
Owner.PopupMessage(user, Loc.GetString("{0:theName} is empty!", targetSolution.Owner));
return;
}
//Move units from attackSolution to targetSolution
// Move units from attackSolution to targetSolution
var removedSolution = targetSolution.SplitSolution(realTransferAmount);
if (!solution.TryAddSolution(removedSolution))
{
return;
}
Owner.PopupMessage(user, Loc.GetString("Drew {0}u", removedSolution.TotalVolume));
Owner.PopupMessage(user, Loc.GetString("Drew {0}u from {1:theName}", removedSolution.TotalVolume, targetSolution.Owner));
Dirty();
}
public override ComponentState GetComponentState()
{
Owner.TryGetComponent(out SolutionComponent? solution);
Owner.TryGetComponent(out SolutionContainerComponent? solution);
var currentVolume = solution?.CurrentVolume ?? ReagentUnit.Zero;
var maxVolume = solution?.MaxVolume ?? ReagentUnit.Zero;

View File

@@ -29,7 +29,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
[ViewVariables]
private string _trashPrototype;
[ViewVariables]
private SolutionComponent _contents;
private SolutionContainerComponent _contents;
[ViewVariables]
private ReagentUnit _transferAmount;
@@ -45,7 +45,8 @@ namespace Content.Server.GameObjects.Components.Chemistry
public override void Initialize()
{
base.Initialize();
_contents = Owner.GetComponent<SolutionComponent>();
_contents = Owner.GetComponent<SolutionContainerComponent>();
}
bool IUse.UseEntity(UseEntityEventArgs eventArgs)

View File

@@ -48,22 +48,22 @@ namespace Content.Server.GameObjects.Components.Chemistry
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
//Get target solution component
if (!Owner.TryGetComponent<SolutionComponent>(out var targetSolution))
if (!Owner.TryGetComponent<SolutionContainerComponent>(out var targetSolution))
return false;
//Get attack solution component
var attackEntity = eventArgs.Using;
if (!attackEntity.TryGetComponent<SolutionComponent>(out var attackSolution))
if (!attackEntity.TryGetComponent<SolutionContainerComponent>(out var attackSolution))
return false;
// Calculate possibe solution transfer
if (targetSolution.CanPourIn && attackSolution.CanPourOut)
if (targetSolution.CanAddSolutions && attackSolution.CanRemoveSolutions)
{
// default logic (beakers and glasses)
// transfer solution from object in hand to attacked
return TryTransfer(eventArgs, attackSolution, targetSolution);
}
else if (targetSolution.CanPourOut && attackSolution.CanPourIn)
else if (targetSolution.CanRemoveSolutions && attackSolution.CanAddSolutions)
{
// storage tanks and sinks logic
// drain solution from attacked object to object in hand
@@ -74,26 +74,38 @@ namespace Content.Server.GameObjects.Components.Chemistry
return false;
}
bool TryTransfer(InteractUsingEventArgs eventArgs, SolutionComponent fromSolution, SolutionComponent toSolution)
bool TryTransfer(InteractUsingEventArgs eventArgs, SolutionContainerComponent fromSolution, SolutionContainerComponent toSolution)
{
var fromEntity = fromSolution.Owner;
if (!fromEntity.TryGetComponent<PourableComponent>(out var fromPourable))
return false;
//Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(fromPourable.TransferAmount, toSolution.EmptyVolume);
if (realTransferAmount <= 0) //Special message if container is full
if (!fromEntity.TryGetComponent<PourableComponent>(out var fromPourable))
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("Container is full"));
return false;
}
//Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(fromPourable.TransferAmount, toSolution.EmptyVolume);
if (realTransferAmount <= 0) // Special message if container is full
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("{0:theName} is full!", toSolution.Owner));
return false;
}
//Move units from attackSolution to targetSolution
var removedSolution = fromSolution.SplitSolution(realTransferAmount);
if (!toSolution.TryAddSolution(removedSolution))
return false;
Owner.PopupMessage(eventArgs.User, Loc.GetString("Transferred {0}u", removedSolution.TotalVolume));
if (removedSolution.TotalVolume <= ReagentUnit.Zero)
{
return false;
}
if (!toSolution.TryAddSolution(removedSolution))
{
return false;
}
Owner.PopupMessage(eventArgs.User, Loc.GetString("You transfer {0}u to {1:theName}.", removedSolution.TotalVolume, toSolution.Owner));
return true;
}

View File

@@ -47,9 +47,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
[ViewVariables] private bool HasBeaker => _beakerContainer.ContainedEntity != null;
[ViewVariables] private ReagentUnit _dispenseAmount = ReagentUnit.New(10);
[ViewVariables]
private SolutionComponent? Solution => _beakerContainer.ContainedEntity?.GetComponent<SolutionComponent>();
[ViewVariables] private SolutionContainerComponent? Solution => _beakerContainer.ContainedEntity.GetComponent<SolutionContainerComponent>();
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
@@ -210,7 +208,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
"", Inventory, Owner.Name, null, _dispenseAmount);
}
var solution = beaker.GetComponent<SolutionComponent>();
var solution = beaker.GetComponent<SolutionContainerComponent>();
return new ReagentDispenserBoundUserInterfaceState(Powered, true, solution.CurrentVolume, solution.MaxVolume,
beaker.Name, Inventory, Owner.Name, solution.ReagentList.ToList(), _dispenseAmount);
}
@@ -222,7 +220,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
}
/// <summary>
/// If this component contains an entity with a <see cref="SolutionComponent"/>, eject it.
/// If this component contains an entity with a <see cref="SolutionContainerComponent"/>, eject it.
/// Tries to eject into user's hands first, then ejects onto dispenser if both hands are full.
/// </summary>
private void TryEject(IEntity user)
@@ -241,26 +239,26 @@ namespace Content.Server.GameObjects.Components.Chemistry
}
/// <summary>
/// If this component contains an entity with a <see cref="SolutionComponent"/>, remove all of it's reagents / solutions.
/// If this component contains an entity with a <see cref="SolutionContainerComponent"/>, remove all of it's reagents / solutions.
/// </summary>
private void TryClear()
{
if (!HasBeaker) return;
var solution = _beakerContainer.ContainedEntity.GetComponent<SolutionComponent>();
var solution = _beakerContainer.ContainedEntity.GetComponent<SolutionContainerComponent>();
solution.RemoveAllSolution();
UpdateUserInterface();
}
/// <summary>
/// If this component contains an entity with a <see cref="SolutionComponent"/>, attempt to dispense the specified reagent to it.
/// If this component contains an entity with a <see cref="SolutionContainerComponent"/>, attempt to dispense the specified reagent to it.
/// </summary>
/// <param name="dispenseIndex">The index of the reagent in <c>Inventory</c>.</param>
private void TryDispense(int dispenseIndex)
{
if (!HasBeaker) return;
var solution = _beakerContainer.ContainedEntity.GetComponent<SolutionComponent>();
var solution = _beakerContainer.ContainedEntity.GetComponent<SolutionContainerComponent>();
solution.TryAddReagent(Inventory[dispenseIndex].ID, _dispenseAmount, out _);
UpdateUserInterface();
@@ -292,7 +290,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
/// <summary>
/// Called when you click the owner entity with something in your active hand. If the entity in your hand
/// contains a <see cref="SolutionComponent"/>, if you have hands, and if the dispenser doesn't already
/// contains a <see cref="SolutionContainerComponent"/>, if you have hands, and if the dispenser doesn't already
/// hold a container, it will be added to the dispenser.
/// </summary>
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
@@ -312,13 +310,13 @@ namespace Content.Server.GameObjects.Components.Chemistry
}
var activeHandEntity = hands.GetActiveHand.Owner;
if (activeHandEntity.TryGetComponent<SolutionComponent>(out var solution))
if (activeHandEntity.TryGetComponent<SolutionContainerComponent>(out var solution))
{
if (HasBeaker)
{
Owner.PopupMessage(args.User, Loc.GetString("This dispenser already has a container in it."));
}
else if ((solution.Capabilities & SolutionCaps.FitsInDispenser) == 0)
else if ((solution.Capabilities & SolutionContainerCaps.FitsInDispenser) == 0)
{
//If it can't fit in the dispenser, don't put it in. For example, buckets and mop buckets can't fit.
Owner.PopupMessage(args.User, Loc.GetString("That can't fit in the dispenser."));

View File

@@ -20,6 +20,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using System;
namespace Content.Server.GameObjects.Components.Chemistry
{
@@ -27,41 +28,26 @@ namespace Content.Server.GameObjects.Components.Chemistry
/// ECS component that manages a liquid solution of reagents.
/// </summary>
[RegisterComponent]
public class SolutionComponent : SharedSolutionComponent, IExamine
public class SolutionContainerComponent : SharedSolutionContainerComponent, IExamine
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
private IEnumerable<ReactionPrototype> _reactions;
private AudioSystem _audioSystem;
private ChemistrySystem _chemistrySystem;
private SpriteComponent _spriteComponent;
private Solution _containedSolution = new Solution();
private ReagentUnit _maxVolume;
private SolutionCaps _capabilities;
private string _fillInitState;
private int _fillInitSteps;
private string _fillPathString = "Objects/Specific/Chemistry/fillings.rsi";
private ResourcePath _fillPath;
private SpriteSpecifier _fillSprite;
/// <summary>
/// The maximum volume of the container.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public ReagentUnit MaxVolume
{
get => _maxVolume;
set => _maxVolume = value; // Note that the contents won't spill out if the capacity is reduced.
}
private AudioSystem _audioSystem;
private ChemistrySystem _chemistrySystem;
private SpriteComponent _spriteComponent;
/// <summary>
/// The total volume of all the of the reagents in the container.
/// </summary>
[ViewVariables]
public ReagentUnit CurrentVolume => _containedSolution.TotalVolume;
public ReagentUnit CurrentVolume => Solution.TotalVolume;
/// <summary>
/// The volume without reagents remaining in the container.
@@ -79,49 +65,35 @@ namespace Content.Server.GameObjects.Components.Chemistry
/// The current capabilities of this container (is the top open to pour? can I inject it into another object?).
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public SolutionCaps Capabilities
{
get => _capabilities;
set => _capabilities = value;
}
public SolutionContainerCaps Capabilities { get; set; }
/// <summary>
/// The contained solution.
/// </summary>
[ViewVariables]
public Solution Solution
{
get => _containedSolution;
set => _containedSolution = value;
}
public IReadOnlyList<Solution.ReagentQuantity> ReagentList => _containedSolution.Contents;
public Solution Solution { get; set; }
/// <summary>
/// Shortcut for Capabilities PourIn flag to avoid binary operators.
/// The maximum volume of the container.
/// </summary>
public bool CanPourIn => (Capabilities & SolutionCaps.PourIn) != 0;
/// <summary>
/// Shortcut for Capabilities PourOut flag to avoid binary operators.
/// </summary>
public bool CanPourOut => (Capabilities & SolutionCaps.PourOut) != 0;
/// <summary>
/// Shortcut for Capabilities Injectable flag
/// </summary>
public bool Injectable => (Capabilities & SolutionCaps.Injectable) != 0;
/// <summary>
/// Shortcut for Capabilities Injector flag
/// </summary>
public bool Injector => (Capabilities & SolutionCaps.Injector) != 0;
[ViewVariables(VVAccess.ReadWrite)]
public ReagentUnit MaxVolume { get; set; }
public bool NoExamine => (Capabilities & SolutionCaps.NoExamine) != 0;
public IReadOnlyList<Solution.ReagentQuantity> ReagentList => Solution.Contents;
public bool CanExamineContents => (Capabilities & SolutionContainerCaps.NoExamine) == 0;
public bool CanUseWithChemDispenser => (Capabilities & SolutionContainerCaps.FitsInDispenser) != 0;
public bool CanAddSolutions => (Capabilities & SolutionContainerCaps.AddTo) != 0;
public bool CanRemoveSolutions => (Capabilities & SolutionContainerCaps.RemoveFrom) != 0;
/// <inheritdoc />
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _maxVolume, "maxVol", ReagentUnit.New(0));
serializer.DataField(ref _containedSolution, "contents", _containedSolution);
serializer.DataField(ref _capabilities, "caps", SolutionCaps.None);
serializer.DataField(ref _fillInitState, "fillingState", "");
serializer.DataField(this, x => MaxVolume, "maxVol", ReagentUnit.New(0));
serializer.DataField(this, x => Solution, "contents", new Solution());
serializer.DataField(this, x => Capabilities, "caps", SolutionContainerCaps.AddTo | SolutionContainerCaps.RemoveFrom);
serializer.DataField(ref _fillInitState, "fillingState", string.Empty);
serializer.DataField(ref _fillInitSteps, "fillingSteps", 7);
}
@@ -149,15 +121,18 @@ namespace Content.Server.GameObjects.Components.Chemistry
public void RemoveAllSolution()
{
_containedSolution.RemoveAllSolution();
Solution.RemoveAllSolution();
OnSolutionChanged(false);
}
public bool TryRemoveReagent(string reagentId, ReagentUnit quantity)
{
if (!ContainsReagent(reagentId, out var currentQuantity)) return false;
if (!ContainsReagent(reagentId, out var currentQuantity))
{
return false;
}
_containedSolution.RemoveReagent(reagentId, quantity);
Solution.RemoveReagent(reagentId, quantity);
OnSolutionChanged(false);
return true;
}
@@ -170,23 +145,25 @@ namespace Content.Server.GameObjects.Components.Chemistry
public bool TryRemoveSolution(ReagentUnit quantity)
{
if (CurrentVolume == 0)
{
return false;
_containedSolution.RemoveSolution(quantity);
}
Solution.RemoveSolution(quantity);
OnSolutionChanged(false);
return true;
}
public Solution SplitSolution(ReagentUnit quantity)
{
var solutionSplit = _containedSolution.SplitSolution(quantity);
var solutionSplit = Solution.SplitSolution(quantity);
OnSolutionChanged(false);
return solutionSplit;
}
protected void RecalculateColor()
{
if (_containedSolution.TotalVolume == 0)
if (Solution.TotalVolume == 0)
{
SubstanceColor = Color.Transparent;
return;
@@ -195,16 +172,23 @@ namespace Content.Server.GameObjects.Components.Chemistry
Color mixColor = default;
var runningTotalQuantity = ReagentUnit.New(0);
foreach (var reagent in _containedSolution)
foreach (var reagent in Solution)
{
runningTotalQuantity += reagent.Quantity;
if(!_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype proto))
if (!_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype proto))
{
continue;
}
if (mixColor == default)
{
mixColor = proto.SubstanceColor;
mixColor = Color.InterpolateBetween(mixColor, proto.SubstanceColor,
(1 / runningTotalQuantity.Float()) * reagent.Quantity.Float());
continue;
}
var interpolateValue = (1 / runningTotalQuantity.Float()) * reagent.Quantity.Float();
mixColor = Color.InterpolateBetween(mixColor, proto.SubstanceColor, interpolateValue);
}
SubstanceColor = mixColor;
@@ -214,56 +198,53 @@ namespace Content.Server.GameObjects.Components.Chemistry
/// Transfers solution from the held container to the target container.
/// </summary>
[Verb]
private sealed class FillTargetVerb : Verb<SolutionComponent>
private sealed class FillTargetVerb : Verb<SolutionContainerComponent>
{
protected override void GetData(IEntity user, SolutionComponent component, VerbData data)
protected override void GetData(IEntity user, SolutionContainerComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user) ||
!user.TryGetComponent<HandsComponent>(out var hands) ||
hands.GetActiveHand == null ||
hands.GetActiveHand.Owner == component.Owner ||
!hands.GetActiveHand.Owner.TryGetComponent<SolutionComponent>(out var solution))
!hands.GetActiveHand.Owner.TryGetComponent<SolutionContainerComponent>(out var solution) ||
!solution.CanRemoveSolutions ||
!component.CanAddSolutions)
{
data.Visibility = VerbVisibility.Invisible;
return;
}
if ((solution.Capabilities & SolutionCaps.PourOut) != 0 &&
(component.Capabilities & SolutionCaps.PourIn) != 0)
{
var heldEntityName = hands.GetActiveHand.Owner?.Prototype?.Name ?? "<Item>";
var myName = component.Owner.Prototype?.Name ?? "<Item>";
var heldEntityName = hands.GetActiveHand.Owner?.Prototype?.Name ?? "<Item>";
var myName = component.Owner.Prototype?.Name ?? "<Item>";
var locHeldEntityName = Loc.GetString(heldEntityName);
var locMyName = Loc.GetString(myName);
var locHeldEntityName = Loc.GetString(heldEntityName);
var locMyName = Loc.GetString(myName);
data.Text = Loc.GetString("Transfer liquid from [{0}] to [{1}].", locHeldEntityName, locMyName);
return;
}
data.Visibility = VerbVisibility.Invisible;
data.Visibility = VerbVisibility.Visible;
data.Text = Loc.GetString("Transfer liquid from [{0}] to [{1}].", locHeldEntityName, locMyName);
}
protected override void Activate(IEntity user, SolutionComponent component)
protected override void Activate(IEntity user, SolutionContainerComponent component)
{
if (!user.TryGetComponent<HandsComponent>(out var hands))
if (!user.TryGetComponent<HandsComponent>(out var hands) || hands.GetActiveHand == null)
{
return;
if (hands.GetActiveHand == null)
return;
if (!hands.GetActiveHand.Owner.TryGetComponent<SolutionComponent>(out var handSolutionComp))
return;
if ((handSolutionComp.Capabilities & SolutionCaps.PourOut) == 0 || (component.Capabilities & SolutionCaps.PourIn) == 0)
}
if (!hands.GetActiveHand.Owner.TryGetComponent<SolutionContainerComponent>(out var handSolutionComp) ||
!handSolutionComp.CanRemoveSolutions ||
!component.CanAddSolutions)
{
return;
}
var transferQuantity = ReagentUnit.Min(component.MaxVolume - component.CurrentVolume, handSolutionComp.CurrentVolume, ReagentUnit.New(10));
// nothing to transfer
if (transferQuantity <= 0)
{
return;
}
var transferSolution = handSolutionComp.SplitSolution(transferQuantity);
component.TryAddSolution(transferSolution);
}
@@ -271,44 +252,37 @@ namespace Content.Server.GameObjects.Components.Chemistry
void IExamine.Examine(FormattedMessage message, bool inDetailsRange)
{
if (NoExamine)
if (!CanExamineContents)
{
return;
}
message.AddText(Loc.GetString("Contains:\n"));
if (ReagentList.Count == 0)
{
message.AddText("Nothing.\n");
message.AddText(Loc.GetString("It's empty."));
}
foreach (var reagent in ReagentList)
else if (ReagentList.Count == 1)
{
var reagent = ReagentList[0];
if (_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype proto))
{
if (inDetailsRange)
{
message.AddText($"{proto.Name}: {reagent.Quantity}u\n");
}
else
{
//This is trash but it shows the general idea
var color = proto.SubstanceColor;
var colorIsh = "Red";
if (color.G > color.R)
{
colorIsh = "Green";
}
if (color.B > color.G && color.B > color.R)
{
colorIsh = "Blue";
}
message.AddText(Loc.GetString("A {0} liquid\n", colorIsh));
}
var colorStr = $" [color={proto.GetSubstanceTextColor().ToHexNoAlpha()}]";
message.AddText(Loc.GetString("It contains a"));
message.AddMarkup(colorStr + Loc.GetString(proto.PhysicalDescription) + "[/color] ");
message.AddText(Loc.GetString("substance."));
}
else
}
else
{
var reagent = ReagentList.Max();
if (_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype proto))
{
message.AddText(Loc.GetString("Unknown reagent: {0}u\n", reagent.Quantity));
var colorStr = $" [color={SubstanceColor.ToHexNoAlpha()}]";
message.AddText(Loc.GetString("It contains a"));
message.AddMarkup(colorStr + Loc.GetString(proto.PhysicalDescription) + "[/color] ");
message.AddText(Loc.GetString("mixture of substances."));
}
}
}
@@ -317,55 +291,53 @@ namespace Content.Server.GameObjects.Components.Chemistry
/// Transfers solution from a target container to the held container.
/// </summary>
[Verb]
private sealed class EmptyTargetVerb : Verb<SolutionComponent>
private sealed class EmptyTargetVerb : Verb<SolutionContainerComponent>
{
protected override void GetData(IEntity user, SolutionComponent component, VerbData data)
protected override void GetData(IEntity user, SolutionContainerComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user) ||
!user.TryGetComponent<HandsComponent>(out var hands) ||
hands.GetActiveHand == null ||
hands.GetActiveHand.Owner == component.Owner ||
!hands.GetActiveHand.Owner.TryGetComponent<SolutionComponent>(out var solution))
!hands.GetActiveHand.Owner.TryGetComponent<SolutionContainerComponent>(out var solution) ||
!solution.CanAddSolutions ||
!component.CanRemoveSolutions)
{
data.Visibility = VerbVisibility.Invisible;
return;
}
if ((solution.Capabilities & SolutionCaps.PourIn) != 0 &&
(component.Capabilities & SolutionCaps.PourOut) != 0)
var heldEntityName = hands.GetActiveHand.Owner?.Prototype?.Name ?? "<Item>";
var myName = component.Owner.Prototype?.Name ?? "<Item>";
var locHeldEntityName = Loc.GetString(heldEntityName);
var locMyName = Loc.GetString(myName);
data.Visibility = VerbVisibility.Visible;
data.Text = Loc.GetString("Transfer liquid from [{0}] to [{1}].", locMyName, locHeldEntityName);
return;
}
protected override void Activate(IEntity user, SolutionContainerComponent component)
{
if (!user.TryGetComponent<HandsComponent>(out var hands) || hands.GetActiveHand == null)
{
var heldEntityName = hands.GetActiveHand.Owner?.Prototype?.Name ?? "<Item>";
var myName = component.Owner.Prototype?.Name ?? "<Item>";
var locHeldEntityName = Loc.GetString(heldEntityName);
var locMyName = Loc.GetString(myName);
data.Text = Loc.GetString("Transfer liquid from [{0}] to [{1}].", locMyName, locHeldEntityName);
return;
}
data.Visibility = VerbVisibility.Invisible;
}
protected override void Activate(IEntity user, SolutionComponent component)
{
if (!user.TryGetComponent<HandsComponent>(out var hands))
return;
if (hands.GetActiveHand == null)
return;
if(!hands.GetActiveHand.Owner.TryGetComponent<SolutionComponent>(out var handSolutionComp))
return;
if ((handSolutionComp.Capabilities & SolutionCaps.PourIn) == 0 || (component.Capabilities & SolutionCaps.PourOut) == 0)
if(!hands.GetActiveHand.Owner.TryGetComponent<SolutionContainerComponent>(out var handSolutionComp) ||
!handSolutionComp.CanAddSolutions ||
!component.CanRemoveSolutions)
{
return;
}
var transferQuantity = ReagentUnit.Min(handSolutionComp.MaxVolume - handSolutionComp.CurrentVolume, component.CurrentVolume, ReagentUnit.New(10));
// pulling from an empty container, pointless to continue
if (transferQuantity <= 0)
{
return;
}
var transferSolution = component.SplitSolution(transferQuantity);
handSolutionComp.TryAddSolution(transferSolution);
@@ -401,7 +373,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
public bool TryAddReagent(string reagentId, ReagentUnit quantity, out ReagentUnit acceptedQuantity, bool skipReactionCheck = false, bool skipColor = false)
{
var toAcceptQuantity = MaxVolume - _containedSolution.TotalVolume;
var toAcceptQuantity = MaxVolume - Solution.TotalVolume;
if (quantity > toAcceptQuantity)
{
acceptedQuantity = toAcceptQuantity;
@@ -412,7 +384,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
acceptedQuantity = quantity;
}
_containedSolution.AddReagent(reagentId, acceptedQuantity);
Solution.AddReagent(reagentId, acceptedQuantity);
if (!skipColor) {
RecalculateColor();
}
@@ -424,10 +396,10 @@ namespace Content.Server.GameObjects.Components.Chemistry
public bool TryAddSolution(Solution solution, bool skipReactionCheck = false, bool skipColor = false)
{
if (solution.TotalVolume > (MaxVolume - _containedSolution.TotalVolume))
if (solution.TotalVolume > (MaxVolume - Solution.TotalVolume))
return false;
_containedSolution.AddSolution(solution);
Solution.AddSolution(solution);
if (!skipColor) {
RecalculateColor();
}
@@ -487,18 +459,20 @@ namespace Content.Server.GameObjects.Components.Chemistry
TryRemoveReagent(reactant.Key, amountToRemove);
}
}
//Add products
// Add products
foreach (var product in reaction.Products)
{
TryAddReagent(product.Key, product.Value * unitReactions, out var acceptedQuantity, true);
}
//Trigger reaction effects
// Trigger reaction effects
foreach (var effect in reaction.Effects)
{
effect.React(Owner, unitReactions.Double());
}
//Play reaction sound client-side
// Play reaction sound client-side
_audioSystem.PlayAtCoords("/Audio/Effects/Chemistry/bubbles.ogg", Owner.Transform.Coordinates);
}
@@ -510,7 +484,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
/// <returns>Return true if the solution contains the reagent.</returns>
public bool ContainsReagent(string reagentId, out ReagentUnit quantity)
{
foreach (var reagent in _containedSolution.Contents)
foreach (var reagent in Solution.Contents)
{
if (reagent.ReagentId == reagentId)
{
@@ -518,43 +492,50 @@ namespace Content.Server.GameObjects.Components.Chemistry
return true;
}
}
quantity = ReagentUnit.New(0);
return false;
}
public string GetMajorReagentId()
{
if (_containedSolution.Contents.Count == 0)
if (Solution.Contents.Count == 0)
{
return "";
}
var majorReagent = _containedSolution.Contents.OrderByDescending(reagent => reagent.Quantity).First();;
var majorReagent = Solution.Contents.OrderByDescending(reagent => reagent.Quantity).First();;
return majorReagent.ReagentId;
}
protected void UpdateFillIcon()
{
if (string.IsNullOrEmpty(_fillInitState)) return;
if (string.IsNullOrEmpty(_fillInitState))
{
return;
}
var percentage = (CurrentVolume / MaxVolume).Double();
var level = ContentHelpers.RoundToLevels(percentage * 100, 100, _fillInitSteps);
//Transformed glass uses special fancy sprites so we don't bother
if (level == 0 || Owner.TryGetComponent<TransformableContainerComponent>(out var transformableContainerComponent)
&& transformableContainerComponent.Transformed)
if (level == 0 || (Owner.TryGetComponent<TransformableContainerComponent>(out var transformComp) && transformComp.Transformed))
{
_spriteComponent.LayerSetColor(1, Color.Transparent);
return;
}
_fillSprite = new SpriteSpecifier.Rsi(_fillPath, _fillInitState+level);
_fillSprite = new SpriteSpecifier.Rsi(_fillPath, _fillInitState + level);
_spriteComponent.LayerSetSprite(1, _fillSprite);
_spriteComponent.LayerSetColor(1,SubstanceColor);
_spriteComponent.LayerSetColor(1, SubstanceColor);
}
protected virtual void OnSolutionChanged(bool skipColor)
{
if (!skipColor)
{
RecalculateColor();
}
UpdateFillIcon();
_chemistrySystem.HandleSolutionChange(Owner);

View File

@@ -42,13 +42,13 @@ namespace Content.Server.GameObjects.Components.Chemistry
{
base.Startup();
if (!Owner.EnsureComponent(out SolutionComponent solution))
if (!Owner.EnsureComponent(out SolutionContainerComponent solution))
{
Logger.Warning(
$"Entity {Owner.Name} at {Owner.Transform.MapPosition} didn't have a {nameof(SolutionComponent)}");
$"Entity {Owner.Name} at {Owner.Transform.MapPosition} didn't have a {nameof(SolutionContainerComponent)}");
}
solution.Capabilities |= SolutionCaps.FitsInDispenser;
solution.Capabilities |= SolutionContainerCaps.FitsInDispenser;
}
public void CancelTransformation()
@@ -68,7 +68,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs)
{
var solution = eventArgs.Owner.GetComponent<SolutionComponent>();
var solution = eventArgs.Owner.GetComponent<SolutionContainerComponent>();
//Transform container into initial state when emptied
if (_currentReagent != null && solution.ReagentList.Count == 0)
{

View File

@@ -31,10 +31,10 @@ namespace Content.Server.GameObjects.Components.Chemistry
{
base.Initialize();
if (!Owner.EnsureComponent(out SolutionComponent _))
if (!Owner.EnsureComponent(out SolutionContainerComponent _))
{
Logger.Warning(
$"Entity {Owner.Name} at {Owner.Transform.MapPosition} didn't have a {nameof(SolutionComponent)}");
$"Entity {Owner.Name} at {Owner.Transform.MapPosition} didn't have a {nameof(SolutionContainerComponent)}");
}
}
@@ -59,7 +59,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
public void Update()
{
if (!Owner.TryGetComponent(out SolutionComponent contents))
if (!Owner.TryGetComponent(out SolutionContainerComponent contents))
return;
if (!_running)
@@ -94,7 +94,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
return false;
}
if (!Owner.TryGetComponent(out SolutionComponent contents))
if (!Owner.TryGetComponent(out SolutionContainerComponent contents))
{
return false;
}

View File

@@ -24,17 +24,17 @@ namespace Content.Server.GameObjects.Components.Fluids
public ReagentUnit MaxVolume
{
get => Owner.TryGetComponent(out SolutionComponent? solution) ? solution.MaxVolume : ReagentUnit.Zero;
get => Owner.TryGetComponent(out SolutionContainerComponent? solution) ? solution.MaxVolume : ReagentUnit.Zero;
set
{
if (Owner.TryGetComponent(out SolutionComponent? solution))
if (Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
solution.MaxVolume = value;
}
}
}
public ReagentUnit CurrentVolume => Owner.TryGetComponent(out SolutionComponent? solution)
public ReagentUnit CurrentVolume => Owner.TryGetComponent(out SolutionContainerComponent? solution)
? solution.CurrentVolume
: ReagentUnit.Zero;
@@ -50,12 +50,12 @@ namespace Content.Server.GameObjects.Components.Fluids
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponent<SolutionComponent>();
Owner.EnsureComponent<SolutionContainerComponent>();
}
private bool TryGiveToMop(MopComponent mopComponent)
{
if (!Owner.TryGetComponent(out SolutionComponent? contents))
if (!Owner.TryGetComponent(out SolutionContainerComponent? contents))
{
return false;
}
@@ -88,7 +88,7 @@ namespace Content.Server.GameObjects.Components.Fluids
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!Owner.TryGetComponent(out SolutionComponent? contents))
if (!Owner.TryGetComponent(out SolutionContainerComponent? contents))
{
return false;
}

View File

@@ -1,48 +0,0 @@
using Content.Server.GameObjects.Components.Chemistry;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Verbs;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Localization;
namespace Content.Server.GameObjects.Components.Fluids
{
[RegisterComponent]
public class CanSpillComponent : Component
{
public override string Name => "CanSpill";
// TODO: If the Owner doesn't have a SolutionComponent straight up just have this remove itself?
/// <summary>
/// Transfers solution from the held container to the target container.
/// </summary>
[Verb]
private sealed class FillTargetVerb : Verb<CanSpillComponent>
{
protected override void GetData(IEntity user, CanSpillComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user) ||
!component.Owner.TryGetComponent(out SolutionComponent solutionComponent))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Spill liquid");
data.Visibility = solutionComponent.CurrentVolume > ReagentUnit.Zero
? VerbVisibility.Visible
: VerbVisibility.Disabled;
}
protected override void Activate(IEntity user, CanSpillComponent component)
{
var solutionComponent = component.Owner.GetComponent<SolutionComponent>();
// Need this as when we split the component's owner may be deleted
var entityLocation = component.Owner.Transform.Coordinates;
var solution = solutionComponent.SplitSolution(solutionComponent.CurrentVolume);
solution.SpillAt(entityLocation, "PuddleSmear");
}
}
}
}

View File

@@ -22,14 +22,14 @@ namespace Content.Server.GameObjects.Components.Fluids
{
public override string Name => "Mop";
public SolutionComponent? Contents => Owner.GetComponentOrNull<SolutionComponent>();
public SolutionContainerComponent? Contents => Owner.GetComponentOrNull<SolutionContainerComponent>();
public ReagentUnit MaxVolume
{
get => Owner.GetComponentOrNull<SolutionComponent>()?.MaxVolume ?? ReagentUnit.Zero;
get => Owner.GetComponentOrNull<SolutionContainerComponent>()?.MaxVolume ?? ReagentUnit.Zero;
set
{
if (Owner.TryGetComponent(out SolutionComponent? solution))
if (Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
solution.MaxVolume = value;
}
@@ -37,7 +37,7 @@ namespace Content.Server.GameObjects.Components.Fluids
}
public ReagentUnit CurrentVolume =>
Owner.GetComponentOrNull<SolutionComponent>()?.CurrentVolume ?? ReagentUnit.Zero;
Owner.GetComponentOrNull<SolutionContainerComponent>()?.CurrentVolume ?? ReagentUnit.Zero;
// Currently there's a separate amount for pickup and dropoff so
// Picking up a puddle requires multiple clicks
@@ -60,15 +60,15 @@ namespace Content.Server.GameObjects.Components.Fluids
{
base.Initialize();
if (!Owner.EnsureComponent(out SolutionComponent _))
if (!Owner.EnsureComponent(out SolutionContainerComponent _))
{
Logger.Warning($"Entity {Owner.Name} at {Owner.Transform.MapPosition} didn't have a {nameof(SolutionComponent)}");
Logger.Warning($"Entity {Owner.Name} at {Owner.Transform.MapPosition} didn't have a {nameof(SolutionContainerComponent)}");
}
}
void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (!Owner.TryGetComponent(out SolutionComponent? contents)) return;
if (!Owner.TryGetComponent(out SolutionContainerComponent? contents)) return;
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) return;
if (CurrentVolume <= 0)
@@ -76,7 +76,6 @@ namespace Content.Server.GameObjects.Components.Fluids
return;
}
//Solution solution;
if (eventArgs.Target == null)
{
// Drop the liquid on the mop on to the ground
@@ -98,7 +97,7 @@ namespace Content.Server.GameObjects.Components.Fluids
if (transferAmount == 0)
{
if(puddleComponent.EmptyHolder) //The puddle doesn't actually *have* reagents, for example vomit because there's no "vomit" reagent.
if (puddleComponent.EmptyHolder) //The puddle doesn't actually *have* reagents, for example vomit because there's no "vomit" reagent.
{
puddleComponent.Owner.Delete();
transferAmount = ReagentUnit.Min(ReagentUnit.New(5), CurrentVolume);

View File

@@ -94,7 +94,7 @@ namespace Content.Server.GameObjects.Components.Fluids
private ReagentUnit _overflowVolume;
private ReagentUnit OverflowLeft => CurrentVolume - OverflowVolume;
private SolutionComponent _contents;
private SolutionContainerComponent _contents;
public bool EmptyHolder => _contents.ReagentList.Count == 0;
private int _spriteVariants;
// Whether the underlying solution color should be used
@@ -118,13 +118,13 @@ namespace Content.Server.GameObjects.Components.Fluids
{
base.Initialize();
if (Owner.TryGetComponent(out SolutionComponent solutionComponent))
if (Owner.TryGetComponent(out SolutionContainerComponent solutionComponent))
{
_contents = solutionComponent;
}
else
{
_contents = Owner.AddComponent<SolutionComponent>();
_contents = Owner.AddComponent<SolutionContainerComponent>();
}
_snapGrid = Owner.EnsureComponent<SnapGridComponent>();

View File

@@ -0,0 +1,59 @@
using Content.Server.GameObjects.Components.Chemistry;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Localization;
namespace Content.Server.GameObjects.Components.Fluids
{
[RegisterComponent]
public class SpillableComponent : Component
{
public override string Name => "Spillable";
/// <summary>
/// Transfers solution from the held container to the floor.
/// </summary>
[Verb]
private sealed class SpillTargetVerb : Verb<SpillableComponent>
{
protected override void GetData(IEntity user, SpillableComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user) ||
!component.Owner.TryGetComponent(out SolutionContainerComponent solutionComponent) ||
!solutionComponent.CanRemoveSolutions)
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Spill liquid");
data.Visibility = solutionComponent.CurrentVolume > ReagentUnit.Zero ? VerbVisibility.Visible : VerbVisibility.Disabled;
}
protected override void Activate(IEntity user, SpillableComponent component)
{
if (component.Owner.TryGetComponent<SolutionContainerComponent>(out var solutionComponent))
{
if (!solutionComponent.CanRemoveSolutions)
{
user.PopupMessage(user, Loc.GetString("You can't pour anything from {0:theName}!", component.Owner));
}
if (solutionComponent.CurrentVolume.Float() <= 0)
{
user.PopupMessage(user, Loc.GetString("{0:theName} is empty!", component.Owner));
}
// Need this as when we split the component's owner may be deleted
var entityLocation = component.Owner.Transform.Coordinates;
var solution = solutionComponent.SplitSolution(solutionComponent.CurrentVolume);
solution.SpillAt(entityLocation, "PuddleSmear");
}
}
}
}
}

View File

@@ -45,16 +45,16 @@ namespace Content.Server.GameObjects.Components.Fluids
set => _sprayVelocity = value;
}
public ReagentUnit CurrentVolume => Owner.GetComponentOrNull<SolutionComponent>()?.CurrentVolume ?? ReagentUnit.Zero;
public ReagentUnit CurrentVolume => Owner.GetComponentOrNull<SolutionContainerComponent>()?.CurrentVolume ?? ReagentUnit.Zero;
public override void Initialize()
{
base.Initialize();
if (!Owner.EnsureComponent(out SolutionComponent _))
if (!Owner.EnsureComponent(out SolutionContainerComponent _))
{
Logger.Warning(
$"Entity {Owner.Name} at {Owner.Transform.MapPosition} didn't have a {nameof(SolutionComponent)}");
$"Entity {Owner.Name} at {Owner.Transform.MapPosition} didn't have a {nameof(SolutionContainerComponent)}");
}
}
@@ -78,7 +78,7 @@ namespace Content.Server.GameObjects.Components.Fluids
if (eventArgs.ClickLocation.GetGridId(_serverEntityManager) != playerPos.GetGridId(_serverEntityManager))
return;
if (!Owner.TryGetComponent(out SolutionComponent contents))
if (!Owner.TryGetComponent(out SolutionContainerComponent contents))
return;
var direction = (eventArgs.ClickLocation.Position - playerPos.Position).Normalized;

View File

@@ -224,8 +224,10 @@ namespace Content.Server.GameObjects.Components.GUI
if (!interactionSystem.TryDroppedInteraction(Owner, item.Owner))
return false;
}
interactionSystem.DroppedInteraction(Owner, item.Owner);
else
{
interactionSystem.DroppedInteraction(Owner, item.Owner);
}
return true;
}
@@ -248,7 +250,7 @@ namespace Content.Server.GameObjects.Components.GUI
public bool Drop(string slot, EntityCoordinates coords, bool doMobChecks = true)
{
var hand = GetHand(slot);
if (!CanDrop(slot) || hand?.Entity == null)
if (!CanDrop(slot, doMobChecks) || hand?.Entity == null)
{
return false;
}
@@ -260,12 +262,17 @@ namespace Content.Server.GameObjects.Components.GUI
return false;
}
if (!DroppedInteraction(item, doMobChecks))
if (!DroppedInteraction(item, false))
return false;
item.RemovedFromSlot();
item.Owner.Transform.Coordinates = coords;
if (item.Owner.TryGetComponent<SpriteComponent>(out var spriteComponent))
{
spriteComponent.RenderOrder = item.Owner.EntityManager.CurrentTick.Value;
}
if (ContainerHelpers.TryGetContainer(Owner, out var container))
{
container.Insert(item.Owner);
@@ -294,39 +301,7 @@ namespace Content.Server.GameObjects.Components.GUI
public bool Drop(string slot, bool mobChecks = true)
{
var hand = GetHand(slot);
if (!CanDrop(slot, mobChecks) || hand?.Entity == null)
{
return false;
}
var item = hand.Entity.GetComponent<ItemComponent>();
if (!DroppedInteraction(item, mobChecks))
return false;
if (!hand.Container.Remove(hand.Entity))
{
return false;
}
item.RemovedFromSlot();
item.Owner.Transform.Coordinates = Owner.Transform.Coordinates;
if (item.Owner.TryGetComponent<SpriteComponent>(out var spriteComponent))
{
spriteComponent.RenderOrder = item.Owner.EntityManager.CurrentTick.Value;
}
if (ContainerHelpers.TryGetContainer(Owner, out var container))
{
container.Insert(item.Owner);
}
OnItemChanged?.Invoke();
Dirty();
return true;
return Drop(slot, Owner.Transform.Coordinates, mobChecks);
}
public bool Drop(IEntity entity, bool mobChecks = true)
@@ -341,7 +316,7 @@ namespace Content.Server.GameObjects.Components.GUI
throw new ArgumentException("Entity must be held in one of our hands.", nameof(entity));
}
return Drop(slot, mobChecks);
return Drop(slot, Owner.Transform.Coordinates, mobChecks);
}
public bool Drop(string slot, BaseContainer targetContainer, bool doMobChecks = true)
@@ -357,16 +332,11 @@ namespace Content.Server.GameObjects.Components.GUI
}
var hand = GetHand(slot);
if (!CanDrop(slot) || hand?.Entity == null)
if (!CanDrop(slot, doMobChecks) || hand?.Entity == null)
{
return false;
}
var item = hand.Entity.GetComponent<ItemComponent>();
if (!DroppedInteraction(item, doMobChecks))
return false;
if (!hand.Container.CanRemove(hand.Entity))
{
return false;
@@ -377,11 +347,16 @@ namespace Content.Server.GameObjects.Components.GUI
return false;
}
var item = hand.Entity.GetComponent<ItemComponent>();
if (!hand.Container.Remove(hand.Entity))
{
throw new InvalidOperationException();
}
if (!DroppedInteraction(item, doMobChecks))
return false;
item.RemovedFromSlot();
if (!targetContainer.Insert(item.Owner))

View File

@@ -48,7 +48,7 @@ namespace Content.Server.GameObjects.Components.Interactable
private bool _welderLit;
private WelderSystem _welderSystem = default!;
private SpriteComponent? _spriteComponent;
private SolutionComponent? _solutionComponent;
private SolutionContainerComponent? _solutionComponent;
private PointLightComponent? _pointLightComponent;
public string? WeldSoundCollection { get; set; }

View File

@@ -63,7 +63,7 @@ namespace Content.Server.GameObjects.Components.Kitchen
private uint _currentCookTimerTime = 1;
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
private bool _hasContents => Owner.TryGetComponent(out SolutionComponent? solution) && (solution.ReagentList.Count > 0 || _storage.ContainedEntities.Count > 0);
private bool _hasContents => Owner.TryGetComponent(out SolutionContainerComponent? solution) && (solution.ReagentList.Count > 0 || _storage.ContainedEntities.Count > 0);
private bool _uiDirty = true;
private bool _lostPower = false;
private int _currentCookTimeButtonIndex = 0;
@@ -88,7 +88,7 @@ namespace Content.Server.GameObjects.Components.Kitchen
{
base.Initialize();
Owner.EnsureComponent<SolutionComponent>();
Owner.EnsureComponent<SolutionContainerComponent>();
_storage = ContainerManagerComponent.Ensure<Container>("microwave_entity_container", Owner, out var existed);
_audioSystem = EntitySystem.Get<AudioSystem>();
@@ -165,7 +165,7 @@ namespace Content.Server.GameObjects.Components.Kitchen
_uiDirty = true;
}
if (_uiDirty && Owner.TryGetComponent(out SolutionComponent? solution))
if (_uiDirty && Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
UserInterface?.SetState(new MicrowaveUpdateUserInterfaceState
(
@@ -216,13 +216,13 @@ namespace Content.Server.GameObjects.Components.Kitchen
if (itemEntity.TryGetComponent<PourableComponent>(out var attackPourable))
{
if (!itemEntity.TryGetComponent<SolutionComponent>(out var attackSolution)
|| !attackSolution.CanPourOut)
if (!itemEntity.TryGetComponent<SolutionContainerComponent>(out var attackSolution)
|| !attackSolution.CanRemoveSolutions)
{
return false;
}
if (!Owner.TryGetComponent(out SolutionComponent? solution))
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
return false;
}
@@ -355,7 +355,7 @@ namespace Content.Server.GameObjects.Components.Kitchen
private void VaporizeReagents()
{
if (Owner.TryGetComponent(out SolutionComponent? solution))
if (Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
solution.RemoveAllSolution();
}
@@ -363,7 +363,7 @@ namespace Content.Server.GameObjects.Components.Kitchen
private void VaporizeReagentQuantity(Solution.ReagentQuantity reagentQuantity)
{
if (Owner.TryGetComponent(out SolutionComponent? solution))
if (Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
solution?.TryRemoveReagent(reagentQuantity.ReagentId, reagentQuantity.Quantity);
}
@@ -399,7 +399,7 @@ namespace Content.Server.GameObjects.Components.Kitchen
private void SubtractContents(FoodRecipePrototype recipe)
{
if (!Owner.TryGetComponent(out SolutionComponent? solution))
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
return;
}
@@ -434,7 +434,7 @@ namespace Content.Server.GameObjects.Components.Kitchen
private MicrowaveSuccessState CanSatisfyRecipe(FoodRecipePrototype recipe, Dictionary<string,int> solids)
{
if (!Owner.TryGetComponent(out SolutionComponent? solution))
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
return MicrowaveSuccessState.RecipeFail;
}
@@ -479,7 +479,7 @@ namespace Content.Server.GameObjects.Components.Kitchen
var headCount = 0;
if (victim.TryGetComponent<BodyManagerComponent>(out var bodyManagerComponent))
{
var heads = bodyManagerComponent.GetBodyPartsOfType(BodyPartType.Head);
var heads = bodyManagerComponent.GetPartsOfType(BodyPartType.Head);
foreach (var head in heads)
{
var droppedHead = bodyManagerComponent.DropPart(head);

View File

@@ -101,8 +101,8 @@ namespace Content.Server.GameObjects.Components.Movement
var bodyManager = user.GetComponent<BodyManagerComponent>();
if (bodyManager.GetBodyPartsOfType(Shared.GameObjects.Components.Body.BodyPartType.Leg).Count == 0 ||
bodyManager.GetBodyPartsOfType(Shared.GameObjects.Components.Body.BodyPartType.Foot).Count == 0)
if (bodyManager.GetPartsOfType(Shared.GameObjects.Components.Body.BodyPartType.Leg).Count == 0 ||
bodyManager.GetPartsOfType(Shared.GameObjects.Components.Body.BodyPartType.Foot).Count == 0)
{
reason = Loc.GetString("You are unable to climb!");
return false;

View File

@@ -35,7 +35,7 @@ namespace Content.Server.GameObjects.Components.Nutrition
public override string Name => "Drink";
[ViewVariables]
private SolutionComponent _contents;
private SolutionContainerComponent _contents;
[ViewVariables]
private string _useSound;
[ViewVariables]
@@ -56,9 +56,9 @@ namespace Content.Server.GameObjects.Components.Nutrition
{
base.ExposeData(serializer);
serializer.DataField(ref _useSound, "useSound", "/Audio/Items/drink.ogg");
serializer.DataField(ref _defaultToOpened, "isOpen", false); //For things like cups of coffee.
serializer.DataField(ref _soundCollection, "openSounds","canOpenSounds");
serializer.DataField(ref _pressurized, "pressurized",false);
serializer.DataField(ref _defaultToOpened, "isOpen", false); // For things like cups of coffee.
serializer.DataField(ref _soundCollection, "openSounds", "canOpenSounds");
serializer.DataField(ref _pressurized, "pressurized", false);
serializer.DataField(ref _burstSound, "burstSound", "/Audio/Effects/flash_bang.ogg");
}
@@ -66,14 +66,13 @@ namespace Content.Server.GameObjects.Components.Nutrition
{
base.Initialize();
Owner.TryGetComponent(out _appearanceComponent);
if(!Owner.TryGetComponent(out _contents))
if (!Owner.TryGetComponent(out _contents))
{
_contents = Owner.AddComponent<SolutionComponent>();
_contents = Owner.AddComponent<SolutionContainerComponent>();
}
_contents.Capabilities = SolutionCaps.PourIn
| SolutionCaps.PourOut
| SolutionCaps.Injectable;
_contents.Capabilities = SolutionContainerCaps.AddTo | SolutionContainerCaps.RemoveFrom;
Opened = _defaultToOpened;
UpdateAppearance();
}
@@ -83,11 +82,11 @@ namespace Content.Server.GameObjects.Components.Nutrition
UpdateAppearance();
}
private void UpdateAppearance()
{
_appearanceComponent?.SetData(SharedFoodComponent.FoodVisuals.Visual, _contents.CurrentVolume.Float());
}
bool IUse.UseEntity(UseEntityEventArgs args)
{
if (!Opened)
@@ -100,13 +99,20 @@ namespace Content.Server.GameObjects.Components.Nutrition
Opened = true;
return false;
}
if (_contents.CurrentVolume.Float() <= 0)
{
args.User.PopupMessage(Loc.GetString("{0:theName} is empty!", Owner));
return true;
}
return TryUseDrink(args.User);
}
//Force feeding a drink to someone.
void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
TryUseDrink(eventArgs.Target);
TryUseDrink(eventArgs.Target, forced: true);
}
public void Examine(FormattedMessage message, bool inDetailsRange)
@@ -118,25 +124,28 @@ namespace Content.Server.GameObjects.Components.Nutrition
var color = Empty ? "gray" : "yellow";
var openedText = Loc.GetString(Empty ? "Empty" : "Opened");
message.AddMarkup(Loc.GetString("[color={0}]{1}[/color]", color, openedText));
}
private bool TryUseDrink(IEntity target)
private bool TryUseDrink(IEntity target, bool forced = false)
{
if (target == null)
if (target == null || !_contents.CanRemoveSolutions)
{
return false;
}
if (!Opened)
{
target.PopupMessage(Loc.GetString("Open it first!"));
target.PopupMessage(Loc.GetString("Open {0:theName} first!", Owner));
return false;
}
if (_contents.CurrentVolume.Float() <= 0)
{
target.PopupMessage(Loc.GetString("It's empty!"));
if (!forced)
{
target.PopupMessage(Loc.GetString("{0:theName} is empty!", Owner));
}
return false;
}
@@ -147,18 +156,23 @@ namespace Content.Server.GameObjects.Components.Nutrition
var transferAmount = ReagentUnit.Min(TransferAmount, _contents.CurrentVolume);
var split = _contents.SplitSolution(transferAmount);
if (stomachComponent.TryTransferSolution(split))
{
if (_useSound == null) return false;
if (_useSound == null)
{
return false;
}
EntitySystem.Get<AudioSystem>().PlayFromEntity(_useSound, target, AudioParams.Default.WithVolume(-2f));
target.PopupMessage(Loc.GetString("Slurp"));
UpdateAppearance();
return true;
}
//Stomach was full or can't handle whatever solution we have.
// Stomach was full or can't handle whatever solution we have.
_contents.TryAddSolution(split);
target.PopupMessage(Loc.GetString("You've had enough {0}!", Owner.Name));
target.PopupMessage(Loc.GetString("You've had enough {0:theName}!", Owner));
return false;
}
@@ -167,7 +181,7 @@ namespace Content.Server.GameObjects.Components.Nutrition
if (_pressurized &&
!Opened &&
_random.Prob(0.25f) &&
Owner.TryGetComponent(out SolutionComponent component))
Owner.TryGetComponent(out SolutionContainerComponent component))
{
Opened = true;

View File

@@ -41,7 +41,7 @@ namespace Content.Server.GameObjects.Components.Nutrition
{
get
{
if (!Owner.TryGetComponent(out SolutionComponent? solution))
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
return 0;
}
@@ -83,7 +83,7 @@ namespace Content.Server.GameObjects.Components.Nutrition
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponent<SolutionComponent>();
Owner.EnsureComponent<SolutionContainerComponent>();
}
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
@@ -110,7 +110,7 @@ namespace Content.Server.GameObjects.Components.Nutrition
public virtual bool TryUseFood(IEntity? user, IEntity? target, UtensilComponent? utensilUsed = null)
{
if (!Owner.TryGetComponent(out SolutionComponent? solution))
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
return false;
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.Interfaces.GameTicking;
using Content.Server.Players;
using Content.Shared.Maps;
@@ -321,7 +322,7 @@ namespace Content.Server.GameTicking
{
public string Command => "mapping";
public string Description => "Creates and teleports you to a new uninitialized map for mapping.";
public string Help => $"Usage: {Command} <id> <mapname>";
public string Help => $"Usage: {Command} <mapname> / {Command} <id> <mapname>";
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
{
@@ -331,18 +332,45 @@ namespace Content.Server.GameTicking
return;
}
if (args.Length != 2)
var mapManager = IoCManager.Resolve<IMapManager>();
int mapId;
string mapName;
switch (args.Length)
{
shell.SendText(player, Help);
return;
case 1:
if (player.AttachedEntity == null)
{
shell.SendText(player, "The map name argument cannot be omitted if you have no entity.");
return;
}
mapId = (int) mapManager.NextMapId();
mapName = args[0];
break;
case 2:
if (!int.TryParse(args[0], out var id))
{
shell.SendText(player, $"{args[0]} is not a valid integer.");
return;
}
mapId = id;
mapName = args[1];
break;
default:
shell.SendText(player, Help);
return;
}
shell.ExecuteCommand(player, $"addmap {args[0]} false");
shell.ExecuteCommand(player, $"loadbp {args[0]} \"{CommandParsing.Escape(args[1])}\"");
shell.ExecuteCommand(player, $"addmap {mapId} false");
shell.ExecuteCommand(player, $"loadbp {mapId} \"{CommandParsing.Escape(mapName)}\"");
shell.ExecuteCommand(player, $"aghost");
shell.ExecuteCommand(player, $"tp 0 0 {args[0]}");
shell.ExecuteCommand(player, $"tp 0 0 {mapId}");
shell.SendText(player, $"Created unloaded map from file {args[1]} with id {args[0]}. Use \"savebp 4 foo.yml\" to save it.");
var newGridId = mapManager.GetAllGrids().Max(g => (int) g.Index);
shell.SendText(player, $"Created unloaded map from file {mapName} with id {mapId}. Use \"savebp {newGridId} foo.yml\" to save the new grid as a map.");
}
}

View File

@@ -6,6 +6,7 @@ using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using YamlDotNet.RepresentationModel;
using System;
namespace Content.Shared.Chemistry
{
@@ -19,6 +20,7 @@ namespace Content.Shared.Chemistry
private string _id;
private string _name;
private string _description;
private string _physicalDescription;
private Color _substanceColor;
private List<IMetabolizable> _metabolism;
private string _spritePath;
@@ -26,7 +28,9 @@ namespace Content.Shared.Chemistry
public string ID => _id;
public string Name => _name;
public string Description => _description;
public string PhysicalDescription => _physicalDescription;
public Color SubstanceColor => _substanceColor;
//List of metabolism effects this reagent has, should really only be used server-side.
public List<IMetabolizable> Metabolism => _metabolism;
public string SpriteReplacementPath => _spritePath;
@@ -43,13 +47,36 @@ namespace Content.Shared.Chemistry
serializer.DataField(ref _id, "id", string.Empty);
serializer.DataField(ref _name, "name", string.Empty);
serializer.DataField(ref _description, "desc", string.Empty);
serializer.DataField(ref _physicalDescription, "physicalDesc", string.Empty);
serializer.DataField(ref _substanceColor, "color", Color.White);
serializer.DataField(ref _spritePath, "spritePath", string.Empty);
if (_moduleManager.IsServerModule)
serializer.DataField(ref _metabolism, "metabolism", new List<IMetabolizable> {new DefaultMetabolizable()});
{
serializer.DataField(ref _metabolism, "metabolism", new List<IMetabolizable> { new DefaultMetabolizable() });
}
else
{
_metabolism = new List<IMetabolizable> { new DefaultMetabolizable() };
}
}
/// <summary>
/// If the substance color is too dark we user a lighter version to make the text color readable when the user examines a solution.
/// </summary>
public Color GetSubstanceTextColor()
{
var highestValue = MathF.Max(SubstanceColor.R, MathF.Max(SubstanceColor.G, SubstanceColor.B));
var difference = 0.5f - highestValue;
if (difference > 0f)
{
return new Color(SubstanceColor.R + difference,
SubstanceColor.G + difference,
SubstanceColor.B + difference);
}
return SubstanceColor;
}
}
}

View File

@@ -246,7 +246,7 @@ namespace Content.Shared.Chemistry
}
[Serializable, NetSerializable]
public readonly struct ReagentQuantity
public readonly struct ReagentQuantity: IComparable<ReagentQuantity>
{
public readonly string ReagentId;
public readonly ReagentUnit Quantity;
@@ -262,6 +262,8 @@ namespace Content.Shared.Chemistry
{
return $"{ReagentId}:{Quantity}";
}
public int CompareTo(ReagentQuantity other) { return Quantity.Float().CompareTo(other.Quantity.Float()); }
}
#region Enumeration

View File

@@ -8,15 +8,19 @@ namespace Content.Shared.Chemistry
/// </summary>
[Flags]
[Serializable, NetSerializable]
public enum SolutionCaps
public enum SolutionContainerCaps
{
None = 0,
None = 0,
PourIn = 1,
PourOut = 2,
/// <summary>
/// Can solutions be added into the container?
/// </summary>
AddTo = 1,
Injector = 4,
Injectable = 8,
/// <summary>
/// Can solutions be removed from the container?
/// </summary>
RemoveFrom = 2,
/// <summary>
/// Allows the container to be placed in a <c>ReagentDispenserComponent</c>.
@@ -24,8 +28,11 @@ namespace Content.Shared.Chemistry
/// <para>Allows us to have obscenely large containers that are harder to abuse in chem dispensers
/// since they can't be placed directly in them.</para>
/// </summary>
FitsInDispenser = 16,
FitsInDispenser = 4,
NoExamine = 32,
/// <summary>
/// Can people examine the solution in the container or is it impossible to see?
/// </summary>
NoExamine = 8,
}
}

View File

@@ -4,9 +4,9 @@ using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Chemistry
{
public class SharedSolutionComponent : Component
public class SharedSolutionContainerComponent : Component
{
public override string Name => "Solution";
public override string Name => "SolutionContainer";
/// <inheritdoc />
public sealed override uint? NetID => ContentNetIDs.SOLUTION;
@@ -28,13 +28,13 @@ namespace Content.Shared.GameObjects.Components.Chemistry
{
base.HandleComponentState(curState, nextState);
if(curState == null)
if (curState == null)
{
return;
var compState = (SolutionComponentState)curState;
//TODO: Make me work!
}
// var compState = (SolutionComponentState)curState;
// Is there anything we even need to sync with client?
}
}
}

View File

@@ -580,8 +580,8 @@ entities:
pos: -15.694785,24.608267
rot: -1.5707963267948966 rad
type: Transform
- caps: PourIn, PourOut, Injectable
type: Solution
- caps: AddTo, RemoveFrom
type: SolutionContainer
- anchored: False
type: Collidable
- uid: 60
@@ -718,8 +718,8 @@ entities:
pos: -3.470539,16.956116
rot: -1.5707963267948966 rad
type: Transform
- caps: PourIn, PourOut, Injectable
type: Solution
- caps: AddTo, RemoveFrom
type: SolutionContainer
- anchored: False
type: Collidable
- uid: 74
@@ -1037,8 +1037,8 @@ entities:
- parent: 857
pos: 8.661116,25.513401
type: Transform
- caps: PourIn, PourOut, Injectable
type: Solution
- caps: AddTo, RemoveFrom
type: SolutionContainer
- anchored: False
type: Collidable
- uid: 114
@@ -22848,7 +22848,7 @@ entities:
rot: 3.141592653589793 rad
type: Transform
- fillingSteps: 0
type: Solution
type: SolutionContainer
- anchored: False
type: Collidable
- uid: 1219
@@ -32479,8 +32479,8 @@ entities:
pos: -8.476567,-17.420076
rot: -1.5707963267948966 rad
type: Transform
- caps: PourIn, PourOut, Injectable
type: Solution
- caps: AddTo, RemoveFrom
type: SolutionContainer
- anchored: False
type: Collidable
- uid: 2454

View File

@@ -118,8 +118,8 @@
- type: entity
parent: GlovesBase
id: GlovesGray
name: gray gloves
description: Regular gray gloves that do not keep you from frying.
name: grey gloves
description: Regular grey gloves that do not keep you from frying.
components:
- type: Sprite
sprite: Clothing/Gloves/gray.rsi

View File

@@ -28,7 +28,7 @@
parent: HatBase
id: HatMouseGray
name: mouse gray
description: This is a head of a gray mouse. Squeak!
description: This is a head of a grey mouse. Squeak!
components:
- type: Sprite
sprite: Clothing/Head/mouse_gray.rsi

View File

@@ -10,9 +10,9 @@
- type: Microwave
- type: Clickable
- type: InteractionOutline
- type: Solution
- type: SolutionContainer
maxVol: 100
caps: 1
caps: AddTo
- type: Appearance
visuals:
- type: MicrowaveVisualizer

View File

@@ -13,7 +13,7 @@
heavyImpactRange: 2
lightImpactRange: 6
flashRange: 5
- type: Solution
- type: SolutionContainer
contents:
reagents:
- ReagentId: chem.WeldingFuel

View File

@@ -27,9 +27,9 @@
Anchored: false
- type: Destructible
deadThreshold: 10
- type: Solution
- type: SolutionContainer
maxVol: 1500
caps: 2
caps: RemoveFrom
- type: Pourable
transferAmount: 100.0
placement:

View File

@@ -17,7 +17,7 @@
id: WaterTankFull
suffix: Full
components:
- type: Solution
- type: SolutionContainer
contents:
reagents:
- ReagentId: chem.H2O

View File

@@ -18,9 +18,9 @@
- type: Hunger
- type: Thirst
# Organs
- type: Solution
- type: SolutionContainer
maxVol: 250
caps: 32
caps: AddTo, RemoveFrom, NoExamine
- type: Bloodstream
max_volume: 100
- type: Stomach

View File

@@ -6,8 +6,9 @@
id: DrinkBase
abstract: true
components:
- type: Solution
- type: SolutionContainer
maxVol: 50
caps: AddTo, RemoveFrom
- type: Pourable
transferAmount: 5
- type: Drink
@@ -15,7 +16,7 @@
state: icon
- type: Icon
state: icon
- type: CanSpill
- type: Spillable
- type: entity
parent: DrinkBase
@@ -35,10 +36,10 @@
sprite: Objects/Consumable/Drinks/glass_clear.rsi
- type: Icon
sprite: Objects/Consumable/Drinks/glass_clear.rsi
- type: Solution
- type: SolutionContainer
fillingState: glass
maxVol: 50
caps: 16
caps: AddTo, RemoveFrom
- type: Pourable
transferAmount: 5
- type: TransformableContainer
@@ -47,9 +48,9 @@
parent: DrinkGlassBase
id: DrinkAleglass
name: ale
description: A dark alchoholic beverage made by malted barley and yeast.
description: A dark alchoholic beverage made with malted barley and yeast.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -66,7 +67,7 @@
name: anti-freeze
description: Ultimate refreshment.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -84,7 +85,7 @@
name: atomic bomb glass
description: Nuclear proliferation never tasted so good.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -102,7 +103,7 @@
name: b-52 glass
description: Coffee, Irish Cream, and cognac. You will get bombed.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -144,7 +145,7 @@
name: beer # beer it is. coffee. beer? coff-ee? be-er? c-o... b-e
description: An alcoholic beverage made from malted grains, hops, yeast, and water.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -161,7 +162,7 @@
name: beer glass
description: An alcoholic beverage made from malted grains, hops, yeast, and water.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -305,7 +306,7 @@
name: coffee
description: Coffee is a brewed drink prepared from roasted seeds, commonly called coffee beans, of the coffee plant.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -322,7 +323,7 @@
name: cognac glass
description: A sweet and strongly alchoholic drink, made after numerous distillations and years of maturing. Classy as fornication.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -339,7 +340,7 @@
name: space cola bottle
description: Cola. in space
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -356,7 +357,7 @@
name: cream
description: Dairy product composed of the higher-fat layer skimmed from the top of milk before homogenization.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -373,7 +374,7 @@
name: cuba libre glass
description: Rum, mixed with cola. Viva la revolucion.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -642,7 +643,7 @@
name: milk
description: An opaque white liquid produced by the mammary glands of mammals.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -804,7 +805,7 @@
name: ice glass
description: Water frozen into a solid state.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -821,7 +822,7 @@
name: irish car bomb
description: Mmm, tastes like chocolate cake...
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -838,7 +839,7 @@
name: irish coffee glass
description: Coffee, and alcohol. More fun than a Mimosa to drink in the morning.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -855,7 +856,7 @@
name: irish cream glass
description: Whiskey-imbued cream, what else would you expect from the Irish.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -908,7 +909,7 @@
name: kahlua glass
description: A widely known, Mexican coffee-flavoured liqueur.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -1045,7 +1046,7 @@
name: the manly dorf glass
description: Beer and Ale, brought together in a delicious mix. Intended for true men only.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -1098,7 +1099,7 @@
name: milk jug
description: An opaque white liquid produced by the mammary glands of mammals.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -1597,7 +1598,7 @@
name: syndicate bomb
description: Tastes like terrorism!
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -1614,7 +1615,7 @@
name: tea glass
description: Tasty black tea. Contains caffeine.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -1631,7 +1632,7 @@
name: teapot # short and stout
description: An elegant teapot. It simply oozes class.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -1822,7 +1823,7 @@
name: whiskey cola glass
description: Whiskey, mixed with cola. Surprisingly refreshing.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -1839,7 +1840,7 @@
name: special blend whiskey glass
description: Just when you thought regular station whiskey was good... This silky, amber goodness has to come along and ruin everything.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -1892,7 +1893,7 @@
name: wine glass
description: An premium alchoholic beverage made from distilled grape juice.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:

View File

@@ -5,14 +5,15 @@
components:
- type: Drink
openSounds: bottleOpenSounds
- type: Solution
- type: SolutionContainer
caps: AddTo, RemoveFrom
- type: Pourable
transferAmount: 5
- type: Sprite
state: icon
- type: Icon
state: icon
- type: CanSpill
- type: Spillable
- type: entity
parent: DrinkBottleBaseFull
@@ -44,7 +45,7 @@
name: magm-ale
description: A true dorf's drink of choice.
components:
- type: Solution
- type: SolutionContainer
maxVol: 80
contents:
reagents:
@@ -73,7 +74,7 @@
name: cognac bottle
description: A sweet and strongly alchoholic drink, made after numerous distillations and years of maturing. You might as well not scream 'SHITCURITY' this time.
components:
- type: Solution
- type: SolutionContainer
maxVol: 80
contents:
reagents:
@@ -114,7 +115,7 @@
name: kahlua bottle
description: A widely known, Mexican coffee-flavoured liqueur. In production since 1936, HONK
components:
- type: Solution
- type: SolutionContainer
maxVol: 80
contents:
reagents:
@@ -191,7 +192,7 @@
name: vodka bottle
description: Aah, vodka. Prime choice of drink AND fuel by Russians worldwide.
components:
- type: Solution
- type: SolutionContainer
maxVol: 80
contents:
reagents:
@@ -208,7 +209,7 @@
name: uncle git's special reserve
description: A premium single-malt whiskey, gently matured inside the tunnels of a nuclear shelter. TUNNEL WHISKEY RULES.
components:
- type: Solution
- type: SolutionContainer
maxVol: 80
contents:
reagents:
@@ -225,7 +226,7 @@
name: doublebearded bearded special wine bottle
description: A faint aura of unease and asspainery surrounds the bottle.
components:
- type: Solution
- type: SolutionContainer
maxVol: 80
contents:
reagents:

View File

@@ -6,8 +6,9 @@
- type: Drink
openSounds: canOpenSounds
pressurized: true
- type: Solution
- type: SolutionContainer
maxVol: 20
caps: AddTo, RemoveFrom
contents:
reagents:
- ReagentId: chem.Cola
@@ -18,7 +19,7 @@
state: icon
- type: Icon
state: icon
- type: CanSpill
- type: Spillable
- type: entity
parent: DrinkCanBaseFull
@@ -26,7 +27,7 @@
name: space cola
description: A refreshing beverage.
components:
- type: Solution
- type: SolutionContainer
- type: Sprite
sprite: Objects/Consumable/Drinks/cola.rsi
- type: Icon
@@ -37,8 +38,8 @@
- type: entity
parent: DrinkCanBaseFull
id: DrinkIceTeaCan
name: ice tea can
description: A can of refreshing ice tea.
name: iced tea can
description: A refreshing can of iced tea.
components:
- type: Sprite
sprite: Objects/Consumable/Drinks/ice_tea_can.rsi
@@ -51,7 +52,7 @@
parent: DrinkCanBaseFull
id: DrinkLemonLimeCan
name: lemon-lime can
description: You wanted ORANGE. It gave you Lemon Lime.
description: You wanted ORANGE. It gave you Lemon-Lime.
components:
- type: Sprite
sprite: Objects/Consumable/Drinks/lemon-lime.rsi
@@ -64,7 +65,7 @@
parent: DrinkCanBaseFull
id: DrinkGrapeCan
name: grape soda can
description: 'Sweetened drink with a grape flavor and a deep purple color.'
description: Sweetened drink with a grape flavor and a deep purple color.
components:
- type: Sprite
sprite: Objects/Consumable/Drinks/purple_can.rsi
@@ -156,8 +157,8 @@
- type: entity
parent: DrinkCanBaseFull
id: DrinkEnergyDrink
name: Energy drink
description: ''
name: red bool energy drink
description: A can of Red Bool, with enough caffeine to kill a horse.
components:
- type: Drink
- type: Sprite

View File

@@ -5,8 +5,9 @@
name: base cup
abstract: true
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
caps: AddTo, RemoveFrom
- type: Pourable
transferAmount: 5
- type: Drink
@@ -15,7 +16,7 @@
state: icon
- type: Icon
state: icon
- type: CanSpill
- type: Spillable
- type: entity
parent: DrinkBaseCup
@@ -23,7 +24,7 @@
name: golden cup
description: A golden cup
components:
- type: Solution
- type: SolutionContainer
maxVol: 10
- type: Sprite
sprite: Objects/Consumable/Drinks/golden_cup.rsi
@@ -36,7 +37,7 @@
name: insulated pitcher
description: A stainless steel insulated pitcher. Everyone's best friend in the morning.
components:
- type: Solution
- type: SolutionContainer
maxVol: 15
- type: Sprite
sprite: Objects/Consumable/Drinks/pitcher.rsi
@@ -55,7 +56,7 @@
name: mug
description: A plain white mug.
components:
- type: Solution
- type: SolutionContainer
maxVol: 10
- type: Sprite
sprite: Objects/Consumable/Drinks/mug.rsi
@@ -74,7 +75,7 @@
name: mug black
description: A sleek black mug.
components:
- type: Solution
- type: SolutionContainer
maxVol: 10
- type: Sprite
sprite: Objects/Consumable/Drinks/mug_black.rsi
@@ -93,7 +94,7 @@
name: mug blue
description: A blue and black mug.
components:
- type: Solution
- type: SolutionContainer
maxVol: 10
- type: Sprite
sprite: Objects/Consumable/Drinks/mug_blue.rsi
@@ -112,7 +113,7 @@
name: mug green
description: A pale green and pink mug.
components:
- type: Solution
- type: SolutionContainer
maxVol: 10
- type: Sprite
sprite: Objects/Consumable/Drinks/mug_green.rsi
@@ -131,7 +132,7 @@
name: mug heart
description: A white mug, it prominently features a red heart.
components:
- type: Solution
- type: SolutionContainer
maxVol: 10
- type: Sprite
sprite: Objects/Consumable/Drinks/mug_heart.rsi
@@ -150,7 +151,7 @@
name: mug metal
description: A metal mug. You're not sure which metal.
components:
- type: Solution
- type: SolutionContainer
maxVol: 10
- type: Sprite
sprite: Objects/Consumable/Drinks/mug_metal.rsi
@@ -169,7 +170,7 @@
name: mug moebius
description: A mug with a Moebius Laboratories logo on it. Not even your morning coffee is safe from corporate advertising.
components:
- type: Solution
- type: SolutionContainer
maxVol: 10
- type: Sprite
sprite: Objects/Consumable/Drinks/mug_moebius.rsi
@@ -188,7 +189,7 @@
name: "#1 mug"
description: "A white mug, it prominently features a #1."
components:
- type: Solution
- type: SolutionContainer
maxVol: 10
- type: Sprite
sprite: Objects/Consumable/Drinks/mug_one.rsi
@@ -207,7 +208,7 @@
name: mug rainbow
description: A rainbow mug. The colors are almost as blinding as a welder.
components:
- type: Solution
- type: SolutionContainer
maxVol: 10
- type: Sprite
sprite: Objects/Consumable/Drinks/mug_rainbow.rsi
@@ -226,7 +227,7 @@
name: mug red
description: A red and black mug.
components:
- type: Solution
- type: SolutionContainer
maxVol: 10
- type: Sprite
sprite: Objects/Consumable/Drinks/mug_red.rsi
@@ -257,7 +258,7 @@
name: Coffee
description: Coffee is a brewed drink prepared from roasted seeds, commonly called coffee beans, of the coffee plant.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -274,7 +275,7 @@
name: Teacup
description: A plain white porcelain teacup.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:
@@ -295,9 +296,9 @@
parent: DrinkBaseCup
id: DrinkLean
name: Lean
description: Bro Drake has such a cool fade bro Drake is the best bro c'mon bro he's
description: Bro Drake has such a cool fade bro Drake is the best bro c'mon bro.
components:
- type: Solution
- type: SolutionContainer
maxVol: 20
contents:
reagents:

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,9 @@
name: flour
description: Not to be confused with flower.
components:
- type: Solution
- type: SolutionContainer
maxVol: 50
caps: AddTo, RemoveFrom
contents:
reagents:
- ReagentId: chem.Flour
@@ -19,4 +20,4 @@
- type: Icon
sprite: Objects/Consumable/Food/flour.rsi
state: icon
- type: CanSpill
- type: Spillable

View File

@@ -11,13 +11,14 @@
state: icon
- type: Icon
state: icon
- type: Solution
- type: SolutionContainer
maxVol: 10
caps: AddTo, RemoveFrom
- type: Pourable
transferAmount: 5
- type: Drink
isOpen: true
- type: CanSpill
- type: Spillable
# Containers

View File

@@ -14,9 +14,9 @@
- type: Item
sprite: Objects/Misc/fire_extinguisher.rsi
size: 10
- type: Solution
- type: SolutionContainer
maxVol: 1000
caps: 9
caps: AddTo, RemoveFrom, NoExamine
contents:
reagents:
- ReagentId: chem.H2O

View File

@@ -11,13 +11,13 @@
- type: Item
sprite: Objects/Specific/Chemistry/beakers.rsi
HeldPrefix: beaker
- type: Solution
- type: SolutionContainer
fillingState: beaker
maxVol: 50
caps: 27
caps: AddTo, RemoveFrom, FitsInDispenser # can add and remove solutions and fits in the chemmaster.
- type: Pourable
transferAmount: 5.0
- type: CanSpill
- type: Spillable
- type: entity
name: large beaker
@@ -32,13 +32,13 @@
- type: Item
sprite: Objects/Specific/Chemistry/beakers.rsi
HeldPrefix: beaker
- type: Solution
- type: SolutionContainer
fillingState: beakerlarge
maxVol: 100
caps: 27
caps: AddTo, RemoveFrom, FitsInDispenser
- type: Pourable
transferAmount: 5.0
- type: CanSpill
- type: Spillable
- type: entity
name: dropper
@@ -50,14 +50,14 @@
texture: Objects/Specific/Chemistry/dropper.rsi/dropper.png
- type: Icon
texture: Objects/Specific/Chemistry/dropper.rsi/dropper.png
- type: Solution
- type: SolutionContainer
fillingState: dropper
fillingSteps: 2
maxVol: 5
caps: 19
caps: AddTo, RemoveFrom
- type: Pourable
transferAmount: 5.0
- type: CanSpill
- type: Spillable
- type: entity
name: syringe
@@ -69,14 +69,14 @@
texture: Objects/Specific/Chemistry/syringe.rsi/0.png
- type: Icon
texture: Objects/Specific/Chemistry/syringe.rsi/0.png
- type: Solution
- type: SolutionContainer
fillingState: syringe
fillingSteps: 5
maxVol: 15
caps: 19
caps: AddTo, RemoveFrom
- type: Injector
injectOnly: false
- type: CanSpill
- type: Spillable
- type: entity
name: bottle
@@ -84,15 +84,16 @@
id: bottle
components:
- type: Drink
- type: Solution
- type: SolutionContainer
maxVol: 30
caps: AddTo, RemoveFrom
- type: Pourable
transferAmount: 5
- type: Sprite
texture: Objects/Specific/Chemistry/bottle.rsi/bottle.png
- type: Icon
texture: Objects/Specific/Chemistry/bottle.rsi/bottle.png
- type: CanSpill
- type: Spillable
- type: entity
name: pill
@@ -101,8 +102,9 @@
description: It's not a suppository.
components:
- type: Pill
- type: Solution
- type: SolutionContainer
maxVol: 50
caps: RemoveFrom
- type: Sprite
texture: Objects/Specific/Chemistry/pills.rsi/pill.png
- type: Icon

View File

@@ -14,9 +14,9 @@
size: 10
sprite: Objects/Specific/Janitorial/mop.rsi
- type: Mop
- type: Solution
- type: SolutionContainer
maxVol: 10
caps: 1
caps: AddTo, RemoveFrom
- type: LoopingSound
- type: entity
@@ -33,9 +33,9 @@
- type: InteractionOutline
- type: Bucket
- type: LoopingSound
- type: Solution
- type: SolutionContainer
maxVol: 500
caps: 3
caps: AddTo, RemoveFrom
- type: Collidable
shapes:
- !type:PhysShapeAabb
@@ -49,14 +49,14 @@
- type: Physics
mass: 5
anchored: false
- type: CanSpill
- type: Spillable
- type: Pullable
- type: entity
parent: BaseItem
name: bucket
id: Bucket
description: "Dear God."
description: It's a boring old bucket.
components:
- type: Clickable
- type: Sprite
@@ -66,9 +66,9 @@
texture: Objects/Specific/Janitorial/bucket.png
- type: Bucket
- type: LoopingSound
- type: Solution
- type: SolutionContainer
maxVol: 500
caps: 3
caps: AddTo, RemoveFrom
- type: Collidable
shapes:
- !type:PhysShapeAabb
@@ -79,7 +79,7 @@
- type: Physics
mass: 5
anchored: false
- type: CanSpill
- type: Spillable
- type: entity
name: wet floor sign
@@ -190,12 +190,12 @@
texture: Objects/Specific/Janitorial/cleaner.png
- type: Icon
texture: Objects/Specific/Janitorial/cleaner.png
- type: Solution
- type: SolutionContainer
maxVol: 100
caps: 1
caps: AddTo, RemoveFrom
- type: Pourable
transferAmount: 5.0
- type: CanSpill
- type: Spillable
- type: Spray
transferAmount: 10
sprayVelocity: 5

View File

@@ -89,9 +89,9 @@
- type: ItemCooldown
- type: MeleeWeapon
- type: ItemStatus
- type: Solution
- type: SolutionContainer
maxVol: 100
caps: 9
caps: AddTo, NoExamine
contents:
reagents:
- ReagentId: chem.WeldingFuel

View File

@@ -4,7 +4,7 @@
name: "reagent item"
abstract: true
components:
- type: Solution
- type: SolutionContainer
maxVol: 5
- type: entity
@@ -14,7 +14,7 @@
components:
- type: SnapGrid
offset: Center
- type: Solution
- type: SolutionContainer
maxVol: 50
- type: Vapor
- type: Physics

View File

@@ -7,8 +7,8 @@
offset: Center
- type: Sprite
drawdepth: FloorObjects
- type: Solution
caps: 1
- type: SolutionContainer
caps: AddTo, RemoveFrom
- type: Puddle
spill_sound: /Audio/Effects/Fluids/splat.ogg
recolor: true

View File

@@ -2,6 +2,7 @@
id: chem.Nutriment
name: nutriment
desc: All the vitamins, minerals, and carbohydrates the body needs in pure form.
physicalDesc: opaque
color: "#664330"
metabolism:
- !type:DefaultFood
@@ -11,6 +12,7 @@
id: chem.H2SO4
name: sulfuric acid
desc: A highly corrosive, oily, colorless liquid.
physicalDesc: oily
color: "#BF8C00"
boilingPoint: 337.0
meltingPoint: 10.31
@@ -18,7 +20,8 @@
- type: reagent
id: chem.H2O
name: water
desc: A tasty colorless liquid.
desc: A colorless liquid that humans need in order to survive.
physicalDesc: translucent
color: "#c0e0ff20"
boilingPoint: 100.0
meltingPoint: 0.0
@@ -30,6 +33,7 @@
id: chem.Ice
name: ice
desc: Frozen water.
physicalDesc: frosty
color: "#bed8e6"
meltingPoint: 0.0
boilingPoint: 100.0
@@ -38,7 +42,8 @@
id: chem.Phoron
name: phoron
desc: Funky, space-magic pixie dust. You probably shouldn't eat this, but we both know you will anyways.
color: "#500064"
physicalDesc: gaseous
color: "#7e009e"
boilingPoint: -127.3 # Random values picked between the actual values for CO2 and O2
meltingPoint: -186.4
@@ -46,6 +51,7 @@
id: chem.Ethanol
name: ethanol
desc: A simple alcohol, makes you drunk if consumed, flammable.
physicalDesc: strong-smelling
color: "#b05b3c"
boilingPoint: 78.2
meltingPoint: -114.1
@@ -54,6 +60,7 @@
id: chem.Glucose
name: glucose
desc: A simple sugar found in many foods.
physicalDesc: syrupy
color: "#ffffff"
boilingPoint: 340282300000000000000000000000000000000 #Fun fact: Glucose can't boil. So let's just set it to the maximum float value.
meltingPoint: 146.0
@@ -62,6 +69,7 @@
id: chem.Ammonia
name: ammonia
desc: An effective fertilizer which is better than what is available to the botanist initially, though it isn't as powerful as Diethylamine
physicalDesc: pungent
color: "#77b58e"
boilingPoint: -33.0
meltingPoint: -77.7
@@ -70,6 +78,7 @@
id: chem.Bleach
name: bleach
desc: Heavy duty cleaner that can clean tiles the same as Space Cleaner and also decontaminate clothes. Extremely toxic when ingested.
physicalDesc: strong-smelling
color: "#a1000b"
boilingPoint: 111.0
meltingPoint: -5.0
@@ -78,6 +87,7 @@
id: chem.Diethylamine
name: diethylamine
desc: A very potent fertilizer.
physicalDesc: strong-smelling
color: "#a1000b"
boilingPoint: 55.5
meltingPoint: -50.0
@@ -86,6 +96,7 @@
id: chem.FoamingAgent
name: foaming agent
desc: Makes foam such as that required in metal foam grenades
physicalDesc: foamy
color: "#215263"
boilingPoint: 418.0 # I went with ammonium lauryl sulfate as the basis for this
meltingPoint: 7.4 # I made this up
@@ -94,6 +105,7 @@
id: chem.PolytrinicAcid
name: polytrinic acid
desc: An extremely corrosive chemical substance. The slightest touch of it will melt off most masks and headgear, and it deals extreme damage to anyone who comes directly into contact with it. Spraying it on other items will usually melt them too, which does make it useful if the clown has covered the entire hallway in banana peels.
physicalDesc: strong-smelling
color: "#a1000b"
boilingPoint: 78.2 # This isn't a real chemical...
meltingPoint: -19.4
@@ -102,7 +114,8 @@
id: chem.SpaceCleaner
name: space cleaner
desc: This is able to clean almost all surfaces of almost anything that may dirty them. The janitor is likely to appreciate refills.
color: "#215263"
physicalDesc: lemony fresh
color: "#c8ff69"
boilingPoint: 147.0 # Made this up, loosely based on bleach
meltingPoint: -11.0
@@ -110,6 +123,7 @@
id: chem.SpaceLube
name: space lube
desc: Space Lube is a high performance lubricant intended for maintenance of extremely complex mechanical equipment (and certainly not used to make people slip).
physicalDesc: shiny
color: "#77b58e"
boilingPoint: 290.0 # Glycerin
meltingPoint: 18.2
@@ -118,6 +132,7 @@
id: chem.TableSalt
name: table salt
desc: Commonly known as salt, Sodium Chloride is often used to season food or kill borers instantly.
physicalDesc: grainy
color: "#a1000b"
boilingPoint: 1465.0
meltingPoint: 800.7
@@ -126,7 +141,8 @@
id: chem.Thermite
name: thermite
desc: A mixture that becomes extremely hot when ignited, and which can burn straight through walls when applied and ignited. It'll slowly inflict burn damage to anybody dumb enough to ingest it, but can't be ignited inside inside said dumb person.
color: "#77b58e"
physicalDesc: grainy
color: "#757245"
boilingPoint: 2977.0 # Aluminum oxide
meltingPoint: 2030.0
@@ -134,7 +150,8 @@
id: chem.UnstableMutagen
name: unstable mutagen
desc: Causes mutations when injected into living people or plants. High doses may be lethal, especially in humans.
color: "#77b58e"
physicalDesc: glowing
color: "#00ff5f"
boilingPoint: 340282300000000000000000000000000000000 # Ethidium bromide, which doesn't boil.
meltingPoint: 261.0
@@ -142,6 +159,7 @@
id: chem.WeldingFuel
name: welding fuel
desc: Used by welders to weld.
physicalDesc: oily
color: "#a76b1c"
boilingPoint: -84.7 # Acetylene. Close enough.
meltingPoint: -80.7

View File

@@ -2,6 +2,7 @@
id: chem.Whiskey
name: whiskey
desc: An alcoholic beverage made from fermented grain mash
physicalDesc: strong-smelling
color: "#664300"
spritePath: whiskeyglass.rsi
@@ -9,13 +10,15 @@
id: chem.Ale
name: ale
desc: A type of beer brewed using a warm fermentation method, resulting in a sweet, full-bodied and fruity taste.
color: "#664300"
physicalDesc: bubbly
color: "#663100"
spritePath: aleglass.rsi
- type: reagent
id: chem.Wine
name: wine
desc: An alcoholic drink made from fermented grapes
physicalDesc: translucent
color: "#7E4043"
spritePath: wineglass.rsi
@@ -23,19 +26,36 @@
id: chem.Beer
name: beer
desc: A cold pint of pale lager.
color: "#664300"
physicalDesc: bubbly
color: "#cfa85f"
spritePath: beerglass.rsi
- type: reagent
id: chem.Vodka
name: vodka
desc: The glass contain wodka. Xynta.
color: "#664300"
physicalDesc: strong-smelling
color: "#d1d1d155"
- type: reagent
id: chem.Moonshine
name: moonshine
desc: Artisanal homemade liquor. What could go wrong?
physicalDesc: strong-smelling
color: "#d1d7d155"
- type: reagent
id: chem.Tequila
name: tequila
desc: This stuff will get you fucked up.
physicalDesc: strong-smelling
color: "#d7d1d155"
- type: reagent
id: chem.Kahlua
name: kahlua
desc: A widely known, Mexican coffee-flavoured liqueur. In production since 1936!
physicalDesc: cloudy
color: "#664300"
spritePath: kahluaglass.rsi
@@ -43,6 +63,7 @@
id: chem.Cognac
name: cognac
desc: A sweet and strongly alcoholic drink, twice distilled and left to mature for several years. Classy as fornication.
physicalDesc: strong-smelling
color: "#AB3C05"
spritePath: cognacglass.rsi
@@ -50,6 +71,7 @@
id: chem.ManlyDorf
name: manly dorf
desc: A dwarfy concoction made from ale and beer. Intended for stout dwarves only.
physicalDesc: bubbly
color: "#664300"
spritePath: manlydorfglass.rsi
@@ -57,6 +79,7 @@
id: chem.CubaLibre
name: cuba libre
desc: A classic mix of rum and cola.
physicalDesc: bubbly
color: "#3E1B00"
spritePath: cubalibreglass.rsi
@@ -64,6 +87,7 @@
id: chem.IrishCarBomb
name: irish car bomb
desc: A troubling mixture of irish cream and ale.
physicalDesc: bubbly
color: "#2E6671"
spritePath: irishcarbomb.rsi
@@ -71,6 +95,7 @@
id: chem.IrishCoffee
name: irish coffee
desc: Coffee served with irish cream. Regular cream just isn't the same!
physicalDesc: cloudy
color: "#664300"
spritePath: irishcoffeeglass.rsi
@@ -78,6 +103,7 @@
id: chem.IrishCream
name: irish cream
desc: Whiskey-imbued cream. What else could you expect from the Irish.
physicalDesc: creamy
color: "#664300"
spritePath: irishcreamglass.rsi
@@ -85,6 +111,7 @@
id: chem.B52
name: b-52
desc: Coffee, irish cream, and cognac. You will get bombed.
physicalDesc: bubbly
color: "#664300"
spritePath: b52glass.rsi
@@ -92,6 +119,7 @@
id: chem.AtomicBomb
name: atomic bomb
desc: Nuclear proliferation never tasted so good.
physicalDesc: cloudy
color: "#666300"
spritePath: atomicbombglass.rsi
@@ -99,6 +127,7 @@
id: chem.WhiskeyCola
name: whiskey cola
desc: An innocent-looking mixture of cola and whiskey. Delicious.
physicalDesc: bubbly
color: "#3E1B00"
spritePath: whiskeycolaglass.rsi
@@ -106,22 +135,24 @@
id: chem.SyndicateBomb
name: syndicate bomb
desc: Somebody set us up the bomb!
color: "#2E6671"
physicalDesc: opaque
color: "#2E6660"
spritePath: syndicatebomb.rsi
- type: reagent
id: chem.Antifreeze
name: antifreeze
desc: The ultimate refreshment.
color: "#664300"
physicalDesc: translucent
color: "#ff7d63"
spritePath: antifreeze.rsi
- type: reagent
id: chem.Cola
name: cola
desc: A sweet, carbonated soft drink. Caffeine free.
color: "#100800"
physicalDesc: fizzy
color: "#422912"
metabolism:
- !type:DefaultDrink
rate: 1
@@ -130,6 +161,7 @@
id: chem.Coffee
name: coffee
desc: A drink made from brewed coffee beans. Contains a moderate amount of caffeine.
physicalDesc: aromatic
color: "#664300"
metabolism:
- !type:DefaultDrink
@@ -139,7 +171,8 @@
id: chem.Tea
name: tea
desc: A made by boiling leaves of the tea tree, Camellia sinensis.
color: "#101000"
physicalDesc: aromatic
color: "#8a5a3a"
metabolism:
- !type:DefaultDrink
rate: 1
@@ -148,6 +181,7 @@
id: chem.Cream
name: cream
desc: The fatty, still liquid part of milk. Why don't you mix this with sum scotch, eh?
physicalDesc: creamy
color: "#DFD7AF"
metabolism:
- !type:DefaultDrink
@@ -157,16 +191,48 @@
id: chem.Milk
name: milk
desc: An opaque white liquid produced by the mammary glands of mammals.
physicalDesc: opaque
color: "#DFDFDF"
metabolism:
- !type:DefaultDrink
rate: 1
- type: reagent
id: chem.SpoiledMilk
name: spoiled milk
desc: This milk has gone rancid.
physicalDesc: putrid
color: "#faffba"
metabolism:
- !type:DefaultDrink
rate: 1
- type: reagent
id: chem.CreamyDelight
name: creamy delight
desc: A combination of cream, wine and moonshine. Why would you do this?
physicalDesc: foul
color: "#a6969a"
metabolism:
- !type:DefaultDrink
rate: 1
- type: reagent
id: chem.Lean
name: lean
desc: Turn up for days
desc: Turn up for days.
physicalDesc: bubbly
color: "#9400D3"
metabolism:
- !type:DefaultDrink
rate: 1
- type: reagent
id: chem.LeanShine
name: leanshine
desc: Lean mixed with moonshine. Turn up for months.
physicalDesc: bubbly
color: "#9d5fb8"
metabolism:
- !type:DefaultDrink
rate: 1

View File

@@ -2,6 +2,7 @@
id: chem.H
name: hydrogen
desc: A light, flammable gas.
physicalDesc: gaseous
color: "#808080"
boilingPoint: -253.0
meltingPoint: -259.2
@@ -10,6 +11,7 @@
id: chem.O
name: oxygen
desc: An oxidizing, colorless gas.
physicalDesc: gaseous
color: "#808080"
boilingPoint: -183.0
meltingPoint: -218.4
@@ -19,7 +21,8 @@
id: chem.S
name: sulfur
desc: A yellow, crystalline solid.
color: "#FFFACD"
physicalDesc: powdery
color: "#fff385"
boilingPoint: 445.0
meltingPoint: 120.0
@@ -27,6 +30,7 @@
id: chem.C
name: carbon
desc: A black, crystalline solid.
physicalDesc: crystalline
color: "#22282b"
boilingPoint: 4200.0
meltingPoint: 3550.0
@@ -34,7 +38,8 @@
- type: reagent
id: chem.Al
name: aluminum
desc: A silvery-white, soft, non-magnetic, and ductile metal.
desc: A silver, soft, non-magnetic, and ductile metal.
physicalDesc: metallic
color: "#848789"
boilingPoint: 2327.0
meltingPoint: 660.0
@@ -43,6 +48,7 @@
id: chem.Cu
name: copper
desc: A soft, malleable, and ductile metal with very high thermal and electrical conductivity.
physicalDesc: metallic
color: "#b05b3c"
boilingPoint: 2595.0
meltingPoint: 1083.0
@@ -51,6 +57,7 @@
id: chem.N
name: nitrogen
desc: A colorless, odorless unreactive gas. Highly stable.
physicalDesc: gaseous
color: "#808080"
boilingPoint: -195.8
meltingPoint: -210.0
@@ -59,6 +66,7 @@
id: chem.Fe
name: iron
desc: A silvery-grey metal which forms iron oxides (rust) with contact with air. Commonly alloyed with other elements to create alloys such as steel.
physicalDesc: metallic
color: "#434b4d"
boilingPoint: 2862.0
meltingPoint: 1538.0
@@ -67,6 +75,7 @@
id: chem.F
name: fluorine
desc: A highly toxic pale yellow gas. Extremely reactive.
physicalDesc: gaseous
color: "#808080"
boilingPoint: -188.11
meltingPoint: -219.67
@@ -75,6 +84,7 @@
id: chem.Si
name: silicon
desc: A hard and brittle crystalline solid with a blue-grey color.
physicalDesc: crystalline
color: "#364266"
boilingPoint: 3265.0
meltingPoint: 1414.0
@@ -83,6 +93,7 @@
id: chem.Cl
name: chlorine
desc: A yellow-green gas which is toxic to humans.
physicalDesc: gaseous
color: "#a2ff00"
meltingPoint: -101.5
boilingPoint: -34.04
@@ -91,6 +102,7 @@
id: chem.Li
name: lithium
desc: A soft, silvery-white alkali metal. It is highly reactive, and ignites if it makes contact with water.
physicalDesc: shiny
color: "#c6c8cc"
meltingPoint: 180.5
boilingPoint: 1330.0
@@ -99,6 +111,7 @@
id: chem.Hg
name: mercury
desc: A silver metal which is liquid at room temperature. It is highly toxic to humans.
physicalDesc: shiny
color: "#929296"
meltingPoint: -38.83
boilingPoint: 356.73
@@ -107,14 +120,16 @@
id: chem.P
name: phosphorus
desc: A reactive metal used in pyrotechnics and weapons.
color: "#803330"
physicalDesc: powdery
color: "#ede4e4"
meltingPoint: 44.2
boilingPoint: 280.5
- type: reagent
id: chem.K
name: potassium
desc: A soft, silvery-white metal. Even more reactive than lithium.
desc: A soft, shiny grey metal. Even more reactive than lithium.
physicalDesc: shiny
color: "#c6c8cc"
meltingPoint: 65.5
boilingPoint: 759.0
@@ -123,6 +138,7 @@
id: chem.Ra
name: radium
desc: A radioactive metal, silvery-white in it's pure form. It glows due to it's radioactivity and is highly toxic.
physicalDesc: glowing
color: "#00ff04"
meltingPoint: 700.0
boilingPoint: 1737.0
@@ -131,6 +147,7 @@
id: chem.Na
name: sodium
desc: A silvery-white alkali metal. Highly reactive in it's pure form.
physicalDesc: metallic
color: "#c6c8cc"
meltingPoint: 97.8
boilingPoint: 883.0
@@ -138,7 +155,8 @@
- type: reagent
id: chem.U
name: uranium
desc: A silvery-white metallic chemical element in the actinide series, weakly radioactive.
color: "#00ff06"
desc: A grey metallic chemical element in the actinide series, weakly radioactive.
physicalDesc: metallic
color: "#8fa191"
meltingPoint: 1133.0
boilingPoint: 4131.0

View File

@@ -2,6 +2,7 @@
id: chem.Flour
name: flour
desc: Used for baking.
physicalDesc: powdery
color: "#FFFFFF"
metabolism:
- !type:DefaultFood

View File

@@ -2,245 +2,285 @@
id: chem.Alkycosine
name: alkycosine
desc: Lessens the damage to neurological tissue. More effective at treating brain damage than alkysine and also a fairly effective painkiller as well. Caution is needed in its creation to avoid mixing bleach and the chlorine needed to make alkysine.
physicalDesc: strong-smelling
color: "#9e232b"
- type: reagent
id: chem.Alkysine
name: alkysine
desc: Lessens the damage to neurological tissue, effective even after catastrophic injury. Used for treating brain damage and also a fairly effective painkiller.
color: "#a1000b"
physicalDesc: oily
color: "#ff8c00"
- type: reagent
id: chem.Dylovene
name: dylovene
desc: A broad-spectrum anti-toxin, which treats toxin damage in the blood stream. Overdosing will cause vomiting, dizzyness and pain.
physicalDesc: translucent
color: "#3a1d8a"
- type: reagent
id: chem.Arithrazine
name: arithrazine
desc: A slightly unstable medication used for the most extreme any serious case of radiation poisoning. Lowers radiation level at over twice the rate Hyronalin does and will heal toxin damage at the same time. Deals very minor brute damage to the patient over time, but the patient's body will typically out-regenerate it easily.
physicalDesc: cloudy
color: "#bd5902"
- type: reagent
id: chem.Bicaridine
name: bicaridine
desc: An analgesic which is highly effective at treating brute damage. It is useful for stabilizing people who have been severely beaten, as well as treating less life-threatening injuries. In the case of bleeding (internal or external), bicaridine will slow down the bleeding heavily. If the dosage exceeds the overdose limit, it'll stop it outright.
physicalDesc: opaque
color: "#ffaa00"
- type: reagent
id: chem.Cryoxadone
name: cryoxadone
desc: Required for the proper function of cryogenics. Heals all standard types of damage very swiftly, but only works in temperatures under 170K (usually this means cryo cells). Can also slowly heal clone damage, such as caused by cloning or Slimes.
physicalDesc: fizzy
color: "#0091ff"
- type: reagent
id: chem.Clonexadone
name: clonexadone
desc: Heals standard damage in the same as Cryoxadone, with the same temperature requirement. Significantly more effective than the former at treating clone damage, although both can be used simultaneously. Best used in cryo cells.
color: "#0091ff"
desc: Heals standard damage in the same as Cryoxadone, with the same temperature requirement. Significantly more effective than the former at treating cellular damage, although both can be used simultaneously. Best used in cryo cells.
physicalDesc: bubbly
color: "#0666ff"
- type: reagent
id: chem.Citalopram
name: citalopram
desc: Prevents hallucination slightly.
physicalDesc: cloudy
color: "#21693c"
- type: reagent
id: chem.Dermaline
name: dermaline
desc: An advanced chemical that is more effective at treating burns damage than Kelotane.
desc: An advanced chemical that is more effective at treating burn damage than Kelotane.
physicalDesc: translucent
color: "#215263"
- type: reagent
id: chem.Dexalin
name: dexalin
desc: Used for treating oxygen deprivation. In most cases where it is likely to be needed, the strength of Dexalin Plus will probably be more useful (Results in 1 unit instead of 2).
physicalDesc: opaque
color: "#0041a8"
- type: reagent
id: chem.DexalinPlus
name: dexalin plus
desc: Used in treatment of extreme cases of oxygen deprivation. Even a single unit immediately counters all oxygen loss, which is hugely useful in many circumstances. Any dose beyond this will continue to counter oxygen loss until it is metabolized, essentially removing the need to breathe.
color: "#297691"
physicalDesc: cloudy
color: "#4da0bd"
- type: reagent
id: chem.Ethylredoxrazine
name: ethylredoxrazine
desc: Neutralises the effects of alcohol in the blood stream. Though it is commonly needed, it is rarely requested.
physicalDesc: opaque
color: "#2d5708"
- type: reagent
id: chem.Hyperzine
name: hyperzine
desc: A highly effective, long lasting muscle stimulant. It allows greater freedom of movement in bulky clothing although it has the side effect of causing some twitching. Dangerous in higher doses.
physicalDesc: translucent
color: "#17bd61"
- type: reagent
id: chem.Hyronalin
name: hyronalin
desc: A weak treatment for radiation damage. Considered to be useful mainly for genetic modification, where it reduces radiation levels, and thus the chance of genetic mutations. Largely outclassed by Arithrazine.
color: "#17ac61"
physicalDesc: cloudy
color: "#4cb580"
- type: reagent
id: chem.Imidazoline
name: imidazoline
desc: Effective in treating eye trauma. It heals damage caused by physical or chemical trauma, though it is ineffective in treating genetic defects in the eyes.
physicalDesc: pungent
color: "#f7ef00"
- type: reagent
id: chem.Inacusiate
name: inacusiate
desc: You only need 1u for Inacusiate work. Cures deafness instantly. Useful after an explosion.
color: "#f7ef00"
desc: You only need 1u for Inacusiate to be effective. Cures deafness instantly. Useful after an explosion.
physicalDesc: pungent
color: "#c4c04b"
- type: reagent
id: chem.Inaprovaline
name: inaprovaline
desc: Inaprovaline is a synaptic stimulant and cardiostimulant. Commonly used to stabilize patients- it stops oxygen loss when the patient is in critical health. It'll also slow down bleeding (internal or external) by half while in the body. Acts as a decent painkiller.
color: "#73103b"
physicalDesc: opaque
color: "#731024"
- type: reagent
id: chem.Kelotane
name: kelotane
desc: Treats burn damage and prevents infection.
physicalDesc: strong-smelling
color: "#bf3d19"
- type: reagent
id: chem.Leporazine
name: leporazine
desc: This keeps a patient's body temperature stable. High doses can allow short periods of unprotected EVA, but prevents use of the cryogenics tubes.
physicalDesc: pungent
color: "#ff7db5"
- type: reagent
id: chem.Methylin
name: methylin
desc: An intelligence enhancer, also used in the treatment of attention deficit hyperactivity disorder. Also known as Ritalin. Allows monkeys (not diona nymphs) to understand human speech and improves their dexterity as long as they have some in their system. Causes toxin and brain damage in higher doses.
physicalDesc: acrid
color: "#a700c4"
- type: reagent
id: chem.Oxycodone
name: oxycodone
desc: A very effective painkiller, about 250% as strong as Tramadol.
physicalDesc: acrid
color: "#c4a300"
- type: reagent
id: chem.Phalanximine
name: phalanximine
desc: Used in the treatment of cancer, is as effective as Anti-Toxin. Causes moderate radiation and hair loss.
physicalDesc: acrid
color: "#c8ff75"
- type: reagent
id: chem.Paroxetine
name: paroxetine
desc: Prevents hallucination, but has a 10% chance of causing intense hallucinations.
color: "#c8ff75"
physicalDesc: acrid
color: "#fffbad"
- type: reagent
id: chem.Ryetalyn
name: ryetalyn
desc: You only need 1u for Ryetalin to work. Deactivates genetic defects and powers, restoring a patient to an ideal state. May be useful if genetics is unable to function properly. Deactivated effects return if the patient's genes are modified again.
physicalDesc: cloudy
color: "#532fd4"
- type: reagent
id: chem.Spaceacillin
name: spaceacillin
desc: A theta-lactam antibiotic. A common and very useful medicine, effective against many diseases likely to be encountered in space. Slows progression of diseases.
color: "#7f2fd4"
physicalDesc: opaque
color: "#9942f5"
- type: reagent
id: chem.Synaptizine
name: synaptizine
desc: Toxic, but treats hallucinations, drowsiness & halves the duration of paralysis, stuns and knockdowns. It is metabolized very slowly. One unit is enough to treat hallucinations; two units is deadly.
physicalDesc: pungent
color: "#d49a2f"
- type: reagent
id: chem.Tramadol
name: tramadol
desc: A simple, yet effective painkiller. Very effective for patients in shock.
physicalDesc: strong-smelling
color: "#2f6ed4"
- type: reagent
id: chem.Tricordrazine
name: tricordrazine
desc: A wide-spectrum stimulant, originally derived from Cordrazine. Is capable of healing all four main damage types simultaneously, however it only heals at half the rate of conventional healing chemicals. Because of its low potency, it's best used as a supplement to other medicines.
physicalDesc: opaque
color: "#00e5ff"
- type: reagent
id: chem.Vaccine
name: vaccine
desc: Introduces antigens to the body, allowing the immune system to produce enough antibodies to combat present or future infections. Is blank by default, vaccine carrying antigen is produced at a centrifuge. Each unit raises the associated antibody concentration by 20% so at most 5 units are needed to cure the strongest diseases.
color: "#ba0b60"
physicalDesc: translucent
color: "#ffc9f6"
- type: reagent
id: chem.Albuterol
name: albuterol
desc: A bronchodilator that relaxes muscles in the airways and increases air flow to the lungs. It'll remove mucus from your system as long as it's inside your body. Only useful to people with the Asthma disability.
color: "#00e5ff"
physicalDesc: cloudy
color: "#00b89f"
- type: reagent
id: chem.ChloralHydrate
name: chloral hydrate
desc: A powerful sedative which causes death in doses upwards of 16.2 units. Sends the patient to sleep almost instantly.
color: "#00e5ff"
physicalDesc: acrid
color: "#18c9b1"
- type: reagent
id: chem.Creatine
name: creatine
desc: A muscle-building drug that grants the user enormous strength, before their muscles seize and tear their own body to shreds. In practical terms, 1-25 units just causes toxin damage, and 26 units turns you into a hulk with all its associated benefits with the side effect of taking a little toxin damage over time. You'll remain as a hulk until it's all metabolized, at which point you'll take 200 brute damage and gib. With the correct administration, it can be the most potent drug there is, but even at the best of times it is best considered a double-edged sword.
physicalDesc: cloudy
color: "#a1000b"
- type: reagent
id: chem.Cryptobiolin
name: cryptobiolin
desc: Causes confusion and dizziness. This is essential to make Spaceacillin.
color: "#00e5ff"
physicalDesc: fizzy
color: "#081a80"
- type: reagent
id: chem.HeartbreakerToxin
name: heartbreaker toxin
desc: A hallucinogenic compound that is illegal under space law. A synthetic drug derived from Mindbreaker toxin, it blocks some neurological signals to the respiratory system which causes choking.
color: "#00e5ff"
physicalDesc: strong-smelling
color: "#5f959c"
- type: reagent
id: chem.Impedrezene
name: impedrezene
desc: A narcotic that impedes one's ability by slowing down the higher brain cell functions. Causes massive brain damage.
physicalDesc: acrid
color: "#215263"
- type: reagent
id: chem.Lexorin
name: lexorin
desc: Temporarily stops respiration and causes tissue damage. Large doses are fatal, and will cause people to pass out very quickly. Dexalin and Dexalin Plus will both remove it, however.
color: "#a1000b"
physicalDesc: pungent
color: "#6b0007"
- type: reagent
id: chem.Lipozine
name: lipozine
desc: Causes weight loss upon consumption.
color: "#215263"
physicalDesc: oily
color: "#2690b5"
- type: reagent
id: chem.MindbreakerToxin
name: mindbreaker toxin
desc: A potent hallucinogenic compound that is illegal under space law. Formerly known as LSD.
physicalDesc: opaque
color: "#77b58e"
- type: reagent
id: chem.Soporific
name: soporific (sleep-toxin)
desc: A less powerful sedative that takes a while to work, intended to help insomniacs and patients that are too aggressive to be treated normally. Takes roughly 50 seconds to make the patient fall asleep. Is safe in large quantities. Can be counteracted with Anti-Toxin.
physicalDesc: acrid
color: "#215263"
- type: reagent
id: chem.Sterilizine
name: sterilizine
desc: Helps the patient when used during surgery, may also decontaminate objects and surfaces that bear pathogens. Is currently useless due to infections not being a thing.
color: "#215263"
physicalDesc: strong-smelling
color: "#7cad37"
- type: reagent
id: chem.SpaceDrugs
name: space drugs
desc: An illegal compound which induces a number of effects such as loss of balance and visual artefacts.
color: "#a1000b"
physicalDesc: syrupy
color: "#63806e"

View File

@@ -100,4 +100,38 @@
chem.Ice:
amount: 1
products:
chem.Antifreeze: 4
chem.Antifreeze: 4
- type: reaction
id: react.Moonshine
reactants:
chem.Vodka:
amount: 1
chem.Tequila:
amount: 1
chem.Whiskey:
amount: 1
products:
chem.Moonshine: 3
- type: reaction
id: react.CreamyDelight
reactants:
chem.Moonshine:
amount: 1
chem.Cream:
amount: 1
chem.Wine:
amount: 1
products:
chem.CreamyDelight: 3
- type: reaction
id: react.LeanShine
reactants:
chem.Lean:
amount: 1
chem.Moonshine:
amount: 1
products:
chem.LeanShine: 2