Add puddles / reagent spills (#743)

This commit is contained in:
metalgearsloth
2020-04-22 04:23:12 +10:00
committed by GitHub
parent 1ba222142a
commit 60ce5b5089
53 changed files with 1143 additions and 13 deletions

View File

@@ -138,7 +138,11 @@ namespace Content.Client
"TransformableContainer", "TransformableContainer",
"Mind", "Mind",
"MovementSpeedModifier", "MovementSpeedModifier",
"StorageFill" "StorageFill",
"Mop",
"Bucket",
"Puddle",
"CanSpill",
}; };
foreach (var ignoreName in registerIgnore) foreach (var ignoreName in registerIgnore)

View File

@@ -0,0 +1,120 @@
using System;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.Components.Sound;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Chemistry;
using Content.Shared.Interfaces;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Fluids
{
/// <summary>
/// Can a mop click on this entity and dump its fluids
/// </summary>
[RegisterComponent]
public class BucketComponent : Component, IAttackBy
{
#pragma warning disable 649
[Dependency] private readonly ILocalizationManager _localizationManager;
#pragma warning restore 649
public override string Name => "Bucket";
public ReagentUnit MaxVolume
{
get => _contents.MaxVolume;
set => _contents.MaxVolume = value;
}
public ReagentUnit CurrentVolume => _contents.CurrentVolume;
private SolutionComponent _contents;
private string _sound;
/// <inheritdoc />
public override void ExposeData(ObjectSerializer serializer)
{
serializer.DataFieldCached(ref _sound, "sound", "/Audio/effects/Fluids/watersplash.ogg");
}
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
_contents = Owner.GetComponent<SolutionComponent>();
}
private bool TryGiveToMop(MopComponent mopComponent)
{
// Let's fill 'er up
// If this is called the mop should be empty but just in case we'll do Max - Current
var transferAmount = ReagentUnit.Min(mopComponent.MaxVolume - mopComponent.CurrentVolume, CurrentVolume);
var solution = _contents.SplitSolution(transferAmount);
if (!mopComponent.Contents.TryAddSolution(solution) || mopComponent.CurrentVolume == 0)
{
return false;
}
if (_sound == null)
{
return true;
}
Owner.TryGetComponent(out SoundComponent soundComponent);
soundComponent?.Play(_sound);
return true;
}
public bool AttackBy(AttackByEventArgs eventArgs)
{
if (!eventArgs.AttackWith.TryGetComponent(out MopComponent mopComponent))
{
return false;
}
// Give to the mop if it's empty
if (mopComponent.CurrentVolume == 0)
{
if (!TryGiveToMop(mopComponent))
{
return false;
}
Owner.PopupMessage(eventArgs.User, _localizationManager.GetString("Splish"));
return true;
}
var transferAmount = ReagentUnit.Min(mopComponent.CurrentVolume, MaxVolume - CurrentVolume);
if (transferAmount == 0)
{
return false;
}
var solution = mopComponent.Contents.SplitSolution(transferAmount);
if (!_contents.TryAddSolution(solution))
{
//This really shouldn't happen
throw new InvalidOperationException();
}
// Give some visual feedback shit's happening (for anyone who can't hear sound)
Owner.PopupMessage(eventArgs.User, _localizationManager.GetString("Sploosh"));
if (_sound == null)
{
return true;
}
Owner.TryGetComponent(out SoundComponent soundComponent);
soundComponent?.Play(_sound);
return true;
}
}
}

View File

@@ -0,0 +1,47 @@
using System;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
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 string GetText(IEntity user, CanSpillComponent component)
{
return "Spill liquid";
}
protected override VerbVisibility GetVisibility(IEntity user, CanSpillComponent component)
{
if (component.Owner.TryGetComponent(out SolutionComponent solutionComponent))
{
return solutionComponent.CurrentVolume > ReagentUnit.Zero ? VerbVisibility.Visible : VerbVisibility.Disabled;
}
return VerbVisibility.Invisible;
}
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.GridPosition;
var solution = solutionComponent.SplitSolution(solutionComponent.CurrentVolume);
SpillHelper.SpillAt(entityLocation, solution, "PuddleSmear");
}
}
}
}

View File

@@ -0,0 +1,112 @@
using System;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.Components.Sound;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Chemistry;
using Content.Shared.Interfaces;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Fluids
{
/// <summary>
/// For cleaning up puddles
/// </summary>
[RegisterComponent]
public class MopComponent : Component, IAfterAttack
{
#pragma warning disable 649
[Dependency] private readonly ILocalizationManager _localizationManager;
#pragma warning restore 649
public override string Name => "Mop";
internal SolutionComponent Contents => _contents;
private SolutionComponent _contents;
public ReagentUnit MaxVolume
{
get => _contents.MaxVolume;
set => _contents.MaxVolume = value;
}
public ReagentUnit CurrentVolume => _contents.CurrentVolume;
// Currently there's a separate amount for pickup and dropoff so
// Picking up a puddle requires multiple clicks
// Dumping in a bucket requires 1 click
// Long-term you'd probably use a cooldown and start the pickup once we have some form of global cooldown
public ReagentUnit PickupAmount => _pickupAmount;
private ReagentUnit _pickupAmount;
private string _pickupSound;
/// <inheritdoc />
public override void ExposeData(ObjectSerializer serializer)
{
serializer.DataFieldCached(ref _pickupSound, "pickup_sound", "/Audio/effects/Fluids/slosh.ogg");
// The turbo mop will pickup more
serializer.DataFieldCached(ref _pickupAmount, "pickup_amount", ReagentUnit.New(5));
}
public override void Initialize()
{
base.Initialize();
_contents = Owner.GetComponent<SolutionComponent>();
}
void IAfterAttack.AfterAttack(AfterAttackEventArgs eventArgs)
{
Solution solution;
if (eventArgs.Attacked == null)
{
if (CurrentVolume <= 0)
{
return;
}
// Drop the liquid on the mop on to the ground I guess? Potentially change by design
// Maybe even use a toggle mode instead of "Pickup" and "dropoff"
solution = _contents.SplitSolution(CurrentVolume);
SpillHelper.SpillAt(eventArgs.ClickLocation, solution, "PuddleSmear");
return;
}
if (!eventArgs.Attacked.TryGetComponent(out PuddleComponent puddleComponent))
{
return;
}
// Essentially pickup either:
// - _pickupAmount,
// - whatever's left in the puddle, or
// - whatever we can still hold (whichever's smallest)
var transferAmount = ReagentUnit.Min(ReagentUnit.New(5), puddleComponent.CurrentVolume, MaxVolume - CurrentVolume);
if (transferAmount == 0)
{
return;
}
solution = puddleComponent.SplitSolution(transferAmount);
// Probably don't recolor a mop? Could work, if we layered it maybe
if (!_contents.TryAddSolution(solution, false, true))
{
// I can't imagine why this would happen
throw new InvalidOperationException();
}
// Give some visual feedback shit's happening (for anyone who can't hear sound)
Owner.PopupMessage(eventArgs.User, _localizationManager.GetString("Swish"));
if (_pickupSound == null)
{
return;
}
Owner.TryGetComponent(out SoundComponent soundComponent);
soundComponent?.Play(_pickupSound);
}
}
}

View File

@@ -0,0 +1,333 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Shared.Chemistry;
using Content.Shared.Physics;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Components.Transform;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using Timer = Robust.Shared.Timers.Timer;
namespace Content.Server.GameObjects.Components.Fluids
{
/// <summary>
/// Puddle on a floor
/// </summary>
[RegisterComponent]
public class PuddleComponent : Component
{
// Current design: Something calls the SpillHelper.Spill, that will either
// A) Add to an existing puddle at the location (normalised to tile-center) or
// B) add a new one
// From this every time a puddle is spilt on it will try and overflow to its neighbours if possible,
// and also update its appearance based on volume level (opacity) and chemistry color
// Small puddles will evaporate after a set delay
// TODO: 'leaves fluidtracks', probably in a separate component for stuff like gibb chunks?;
// TODO: Add stuff like slipping -> probably in a separate component (for stuff like bananas) and using BumpEntMsg
// based on behaviour (e.g. someone being punched vs slashed with a sword would have different blood sprite)
// to check for low volumes for evaporation or whatever
public override string Name => "Puddle";
private CancellationTokenSource _evaporationToken;
private ReagentUnit _evaporateThreshold; // How few <Solution Quantity> we can hold prior to self-destructing
private float _evaporateTime;
private string _spillSound;
private DateTime _lastOverflow = DateTime.Now;
private SpriteComponent _spriteComponent;
private SnapGridComponent _snapGrid;
public ReagentUnit MaxVolume
{
get => _contents.MaxVolume;
set => _contents.MaxVolume = value;
}
[ViewVariables]
public ReagentUnit CurrentVolume => _contents.CurrentVolume;
// Volume at which the fluid will try to spill to adjacent components
// Currently a random number, potentially change
public ReagentUnit OverflowVolume => _overflowVolume;
[ViewVariables]
private ReagentUnit _overflowVolume;
private SolutionComponent _contents;
private int _spriteVariants;
// Whether the underlying solution color should be used
private bool _recolor;
/// <inheritdoc />
public override void ExposeData(ObjectSerializer serializer)
{
serializer.DataFieldCached(ref _spillSound, "spill_sound", "/Audio/effects/Fluids/splat.ogg");
serializer.DataField(ref _overflowVolume, "overflow_volume", ReagentUnit.New(20));
serializer.DataField(ref _evaporateTime, "evaporate_time", 600.0f);
// Long-term probably have this based on the underlying reagents
serializer.DataField(ref _evaporateThreshold, "evaporate_threshold", ReagentUnit.New(2));
serializer.DataField(ref _spriteVariants, "variants", 1);
serializer.DataField(ref _recolor, "recolor", false);
}
public override void Initialize()
{
base.Initialize();
if (Owner.TryGetComponent(out SolutionComponent solutionComponent))
{
_contents = solutionComponent;
}
else
{
_contents = Owner.AddComponent<SolutionComponent>();
_contents.Initialize();
}
_snapGrid = Owner.GetComponent<SnapGridComponent>();
// Smaller than 1m^3 for now but realistically this shouldn't be hit
MaxVolume = ReagentUnit.New(1000);
// Random sprite state set server-side so it's consistent across all clients
_spriteComponent = Owner.GetComponent<SpriteComponent>();
var robustRandom = IoCManager.Resolve<IRobustRandom>();
var randomVariant = robustRandom.Next(0, _spriteVariants - 1);
var baseName = new ResourcePath(_spriteComponent.BaseRSIPath).FilenameWithoutExtension;
_spriteComponent.LayerSetState(0, $"{baseName}-{randomVariant}"); // TODO: Remove hardcode
_spriteComponent.Rotation = Angle.FromDegrees(robustRandom.Next(0, 359));
// UpdateAppearance should get called soon after this so shouldn't need to call Dirty() here
}
// Flow rate should probably be controlled globally so this is it for now
internal bool TryAddSolution(Solution solution, bool sound = true, bool checkForEvaporate = true)
{
if (solution.TotalVolume == 0)
{
return false;
}
var result = _contents.TryAddSolution(solution);
if (!result)
{
return false;
}
UpdateStatus();
CheckOverflow();
if (checkForEvaporate)
{
CheckEvaporate();
}
UpdateAppearance();
if (!sound)
{
return true;
}
var entitySystemManager = IoCManager.Resolve<IEntitySystemManager>();
entitySystemManager.GetEntitySystem<AudioSystem>().Play(_spillSound);
return true;
}
internal Solution SplitSolution(ReagentUnit quantity)
{
var split = _contents.SplitSolution(quantity);
CheckEvaporate();
UpdateAppearance();
return split;
}
public void CheckEvaporate()
{
if (CurrentVolume == 0)
{
Owner.Delete();
}
}
private void UpdateStatus()
{
// If UpdateStatus is getting called again it means more fluid has been updated so let's just wait
_evaporationToken?.Cancel();
if (CurrentVolume > _evaporateThreshold)
{
return;
}
_evaporationToken = new CancellationTokenSource();
// KYS to evaporate
Timer.Spawn(TimeSpan.FromSeconds(_evaporateTime), CheckEvaporate, _evaporationToken.Token);
}
private void UpdateAppearance()
{
if (Owner.Deleted)
{
return;
}
// Opacity based on level of fullness to overflow
// Hard-cap lower bound for visibility reasons
var volumeScale = (CurrentVolume.Float() / OverflowVolume.Float()) * 0.75f + 0.25f;
var cappedScale = Math.Min(1.0f, volumeScale);
// Color based on the underlying solutioncomponent
Color newColor;
if (_recolor)
{
newColor = _contents.SubstanceColor.WithAlpha(cappedScale);
}
else
{
newColor = _spriteComponent.Color.WithAlpha(cappedScale);
}
_spriteComponent.Color = newColor;
_spriteComponent.Dirty();
}
/// <summary>
/// Will overflow this entity to neighboring entities if required
/// </summary>
private void CheckOverflow()
{
if (CurrentVolume <= _overflowVolume)
{
return;
}
// Essentially:
// Spill at least 1 solution to each neighbor (so most of the time each puddle is getting 1 max)
// If there's no puddle at the neighbor then add one.
// Setup
// If there's more neighbors to spill to then there are reagents to go around (coz integers)
var overflowAmount = CurrentVolume - OverflowVolume;
var neighborPuddles = new List<IEntity>(8);
// Will overflow to each neighbor; if it already has a puddle entity then add to that
foreach (var direction in RandomDirections())
{
// Can't spill < 1 reagent so stop overflowing
if ((ReagentUnit.Epsilon * neighborPuddles.Count) == overflowAmount)
{
break;
}
// If we found an existing puddle on that tile then we don't need to spawn a new one
var noSpawn = false;
foreach (var entity in _snapGrid.GetInDir(direction))
{
// Don't overflow to walls
if (entity.TryGetComponent(out CollidableComponent collidableComponent) &&
collidableComponent.CollisionLayer == (int) CollisionGroup.Impassable)
{
noSpawn = true;
break;
}
if (!entity.TryGetComponent(out PuddleComponent puddleComponent))
{
continue;
}
// If we've overflowed recently don't include it
noSpawn = true;
// TODO: PauseManager
if ((DateTime.Now - puddleComponent._lastOverflow).TotalSeconds < 1)
{
break;
}
neighborPuddles.Add(entity);
break;
}
if (noSpawn)
{
continue;
}
var grid = _snapGrid.DirectionToGrid(direction);
// We'll just add the co-ordinates as we need to figure out how many puddles we need to spawn first
var entityManager = IoCManager.Resolve<IEntityManager>();
neighborPuddles.Add(entityManager.SpawnEntity(Owner.Prototype.ID, grid));
}
if (neighborPuddles.Count == 0)
{
return;
}
var spillAmount = overflowAmount / ReagentUnit.New(neighborPuddles.Count);
SpillToNeighbours(neighborPuddles, spillAmount);
}
// TODO: Move the below to SnapGrid?
/// <summary>
/// Will yield a random direction until none are left
/// </summary>
/// <returns></returns>
private static IEnumerable<Direction> RandomDirections()
{
var directions = new[]
{
Direction.East,
Direction.SouthEast,
Direction.South,
Direction.SouthWest,
Direction.West,
Direction.NorthWest,
Direction.North,
Direction.NorthEast,
};
var robustRandom = IoCManager.Resolve<IRobustRandom>();
var n = directions.Length;
while (n > 1)
{
n--;
var k = robustRandom.Next(n + 1);
var value = directions[k];
directions[k] = directions[n];
directions[n] = value;
}
foreach (var direction in directions)
{
yield return direction;
}
}
private void SpillToNeighbours(IEnumerable<IEntity> neighbors, ReagentUnit spillAmount)
{
foreach (var neighborPuddle in neighbors)
{
var solution = _contents.SplitSolution(spillAmount);
neighborPuddle.GetComponent<PuddleComponent>().TryAddSolution(solution, false, false);
}
}
}
}

View File

@@ -0,0 +1,90 @@
using Content.Shared.Chemistry;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Map;
namespace Content.Server.GameObjects.Components.Fluids
{
public static class SpillHelper
{
/// <summary>
/// Spills the specified solution at the entity's location if possible.
/// </summary>
/// <param name="entity">Entity location to spill at</param>
/// <param name="solution">Initial solution for the prototype</param>
/// <param name="prototype">Prototype to use</param>
internal static void SpillAt(IEntity entity, Solution solution, string prototype)
{
var entityLocation = entity.Transform.GridPosition;
SpillAt(entityLocation, solution, prototype);
}
// Other functions will be calling this one
/// <summary>
/// Spills solution at the specified grid co-ordinates
/// </summary>
/// <param name="gridCoordinates"></param>
/// <param name="solution">Initial solution for the prototype</param>
/// <param name="prototype">Prototype to use</param>
internal static void SpillAt(GridCoordinates gridCoordinates, Solution solution, string prototype)
{
if (solution.TotalVolume == 0)
{
return;
}
var mapManager = IoCManager.Resolve<IMapManager>();
var entityManager = IoCManager.Resolve<IEntityManager>();
var serverEntityManager = IoCManager.Resolve<IServerEntityManager>();
var mapGrid = mapManager.GetGrid(gridCoordinates.GridID);
// If space return early, let that spill go out into the void
var tileRef = mapGrid.GetTileRef(gridCoordinates);
if (tileRef.Tile.IsEmpty)
{
return;
}
// Get normalized co-ordinate for spill location and spill it in the centre
// TODO: Does SnapGrid or something else already do this?
var spillTileMapGrid = mapManager.GetGrid(gridCoordinates.GridID);
var spillTileRef = spillTileMapGrid.GetTileRef(gridCoordinates).GridIndices;
var spillGridCoords = spillTileMapGrid.GridTileToLocal(spillTileRef);
var spilt = false;
foreach (var spillEntity in entityManager.GetEntitiesAt(spillTileMapGrid.ParentMapId, spillGridCoords.Position))
{
if (!spillEntity.TryGetComponent(out PuddleComponent puddleComponent))
{
continue;
}
if (!puddleComponent.TryAddSolution(solution))
{
continue;
}
spilt = true;
break;
}
// Did we add to an existing puddle
if (spilt)
{
return;
}
var puddle = serverEntityManager.SpawnEntity(prototype, spillGridCoords);
puddle.GetComponent<PuddleComponent>().TryAddSolution(solution);
}
}
}

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using Content.Server.GameObjects.Components.Chemistry; using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.Components.Sound; using Content.Server.GameObjects.Components.Sound;
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
@@ -7,6 +8,7 @@ using Content.Shared.GameObjects.Components.Nutrition;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
using Content.Shared.Maths; using Content.Shared.Maths;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
@@ -119,9 +121,12 @@ namespace Content.Server.GameObjects.Components.Nutrition
var split = _contents.SplitSolution(transferAmount); var split = _contents.SplitSolution(transferAmount);
if (stomachComponent.TryTransferSolution(split)) if (stomachComponent.TryTransferSolution(split))
{ {
// When we split Finish gets called which may delete the can so need to use the entity system for sound
if (_useSound != null) if (_useSound != null)
{ {
Owner.GetComponent<SoundComponent>()?.Play(_useSound); var entitySystemManager = IoCManager.Resolve<IEntitySystemManager>();
var audioSystem = entitySystemManager.GetEntitySystem<AudioSystem>();
audioSystem.Play(_useSound);
user.PopupMessage(user, _localizationManager.GetString("Slurp")); user.PopupMessage(user, _localizationManager.GetString("Slurp"));
} }
} }

View File

@@ -0,0 +1,47 @@
using Content.Server.GameObjects.Components.Fluids;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Transform;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Map;
namespace Content.Server.GameObjects.EntitySystems
{
public class PuddleSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
EntityQuery = new TypeEntityQuery(typeof(PuddleComponent));
var mapManager = IoCManager.Resolve<IMapManager>();
mapManager.TileChanged += HandleTileChanged;
}
public override void Shutdown()
{
base.Shutdown();
var mapManager = IoCManager.Resolve<IMapManager>();
mapManager.TileChanged -= HandleTileChanged;
}
private void HandleTileChanged(object sender, TileChangedEventArgs eventArgs)
{
// If this gets hammered you could probably queue up all the tile changes every tick but I doubt that would ever happen.
var entities = EntityManager.GetEntities(EntityQuery);
foreach (var entity in entities)
{
// If the tile becomes space then delete it (potentially change by design)
if (eventArgs.NewTile.GridIndex == entity.Transform.GridID &&
entity.TryGetComponent(out SnapGridComponent snapGridComponent) &&
snapGridComponent.Position == eventArgs.NewTile.GridIndices &&
eventArgs.NewTile.Tile.IsEmpty)
{
entity.Delete();
break; // Currently it's one puddle per tile, if that changes remove this
}
}
}
}
}

View File

@@ -1,7 +1,5 @@
using Robust.Shared.Interfaces.Serialization; using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Serialization;
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@@ -13,7 +11,9 @@ namespace Content.Shared.Chemistry
private int _value; private int _value;
private static readonly int Shift = 2; private static readonly int Shift = 2;
public static ReagentUnit MaxValue => new ReagentUnit(int.MaxValue); public static ReagentUnit MaxValue { get; } = new ReagentUnit(int.MaxValue);
public static ReagentUnit Epsilon { get; } = new ReagentUnit(1);
public static ReagentUnit Zero { get; } = new ReagentUnit(0);
private double ShiftDown() private double ShiftDown()
{ {
@@ -131,6 +131,16 @@ namespace Content.Shared.Chemistry
return a.ShiftDown() != b; return a.ShiftDown() != b;
} }
public static bool operator ==(ReagentUnit a, ReagentUnit b)
{
return a.Equals(b);
}
public static bool operator !=(ReagentUnit a, ReagentUnit b)
{
return !a.Equals(b);
}
public static bool operator <=(ReagentUnit a, ReagentUnit b) public static bool operator <=(ReagentUnit a, ReagentUnit b)
{ {
return a._value <= b._value; return a._value <= b._value;
@@ -204,12 +214,12 @@ namespace Content.Shared.Chemistry
return ToString(); return ToString();
} }
public bool Equals([AllowNull] ReagentUnit other) public bool Equals(ReagentUnit other)
{ {
return _value == other._value; return _value == other._value;
} }
public int CompareTo([AllowNull] ReagentUnit other) public int CompareTo(ReagentUnit other)
{ {
if(other._value > _value) if(other._value > _value)
{ {

View File

@@ -302,6 +302,27 @@ namespace Content.Tests.Shared.Chemistry
Assert.That(splitSolution.TotalVolume.Int(), Is.EqualTo(0)); Assert.That(splitSolution.TotalVolume.Int(), Is.EqualTo(0));
} }
[Test]
public void SplitSolutionZero()
{
var solution = new Solution();
solution.AddReagent("chem.Impedrezene", ReagentUnit.New(0.01 + 0.19));
solution.AddReagent("chem.Thermite", ReagentUnit.New(0.01 + 0.39));
solution.AddReagent("chem.Li", ReagentUnit.New(0.01 + 0.17));
solution.AddReagent("chem.F", ReagentUnit.New(0.01 + 0.17));
solution.AddReagent("chem.Na", ReagentUnit.New(0 + 0.13));
solution.AddReagent("chem.Hg", ReagentUnit.New(0.15 + 4.15));
solution.AddReagent("chem.Cu", ReagentUnit.New(0 + 0.13));
solution.AddReagent("chem.U", ReagentUnit.New(0.76 + 20.77));
solution.AddReagent("chem.Fe", ReagentUnit.New(0.01 + 0.36));
solution.AddReagent("chem.SpaceDrugs", ReagentUnit.New(0.02 + 0.41));
solution.AddReagent("chem.Al", ReagentUnit.New(0));
solution.AddReagent("chem.Glucose", ReagentUnit.New(0));
solution.AddReagent("chem.O", ReagentUnit.New(0));
solution.SplitSolution(ReagentUnit.New(0.98));
}
[Test] [Test]
public void AddSolution() public void AddSolution()
{ {

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,119 @@
# TODO: Add the other mess types
- type: entity
id: PuddleBase
abstract: true
components:
- type: SnapGrid
offset: Center
- type: Sprite
drawdepth: FloorObjects
- type: Solution
caps: 1
- type: Puddle
spill_sound: /Audio/effects/Fluids/splat.ogg
recolor: true
- type: Sound
- type: InteractionOutline
- type: Clickable
- type: Collidable
shapes:
- !type:PhysShapeAabb
bounds: "-0.5,-0.5,0.5,0.5"
hard: false
- type: entity
name: Puddle
id: PuddleGeneric
parent: PuddleSmear
- type: entity
name: Gibblets
id: PuddleGibblet
parent: PuddleBase
description: Holds spilt milk
components:
- type: Sprite
sprite: Fluids/gibblet.rsi # Placeholder
state: gibblet-0
- type: Icon
icon: Fluids/gibblet.rsi
state: gibblet-0
- type: Puddle
variants: 5
- type: entity
name: Smear
id: PuddleSmear
parent: PuddleBase
description: Holds spilt milk
components:
- type: Sprite
sprite: Fluids/smear.rsi # Placeholder
state: smear-0
- type: Icon
icon: Fluids/smear.rsi
state: smear-0
- type: Puddle
variants: 7
- type: entity
name: Splatter
id: PuddleSplatter
parent: PuddleBase
description: Holds spilt milk
components:
- type: Sprite
sprite: Fluids/splatter.rsi # Placeholder
state: splatter-0
- type: Icon
icon: Fluids/splatter.rsi
state: splatter-0
- type: Puddle
variants: 6
- type: entity
name: Vomit
id: PuddleVomit
parent: PuddleBase
description:
components:
- type: Sprite
sprite: Fluids/vomit.rsi
state: vomit-0
- type: Icon
icon: Fluids/vomit.rsi
state: vomit-0
- type: Puddle
variants: 4
recolor: false
- type: entity
name: Toxins vomit
id: PuddleVomitToxin
parent: PuddleBase
description: You probably don't want to get too close to this
components:
- type: Sprite
sprite: Fluids/vomit_toxin.rsi
state: vomit_toxin-0
- type: Icon
icon: Fluids/vomit_toxin.rsi
state: vomit_toxin-0
- type: Puddle
variants: 4
recolor: false
- type: entity
name: Writing
id: PuddleWriting
parent: PuddleBase
description: Holds spilt milk
components:
- type: Sprite
sprite: Fluids/writing.rsi # Placeholder
state: writing-0
- type: Icon
icon: Fluids/writing.rsi
state: writing-0
- type: Puddle
variants: 5

View File

@@ -1,5 +1,5 @@
- type: entity - type: entity
parent: ReagentItem parent: BaseItem
name: "Extra-Grip™ Mop" name: "Extra-Grip™ Mop"
id: MopItem id: MopItem
description: A mop that can't be stopped, viscera cleanup detail awaits. description: A mop that can't be stopped, viscera cleanup detail awaits.
@@ -10,36 +10,68 @@
texture: Objects/Janitorial/mop.png texture: Objects/Janitorial/mop.png
- type: Item - type: Item
Size: 10 Size: 10
- type: Mop
- type: Solution - type: Solution
maxVol: 10 maxVol: 10
caps: 1 caps: 1
- type: Sound
- type: entity - type: entity
parent: ReagentItem parent: BaseItem
name: Mop Bucket name: Mop Bucket
id: MopBucket id: MopBucket
description: Holds water and the tears of the janitor. description: Holds water and the tears of the janitor.
components: components:
- type: Clickable
- type: Sprite - type: Sprite
texture: Objects/Janitorial/mopbucket.png texture: Objects/Janitorial/mopbucket.png
drawdepth: Objects
- type: Icon - type: Icon
texture: Objects/Janitorial/mopbucket.png texture: Objects/Janitorial/mopbucket.png
- type: Clickable - type: Clickable
- type: InteractionOutline - type: InteractionOutline
- type: Bucket
- type: Sound
- type: Solution - type: Solution
maxVol: 500 maxVol: 500
caps: 3 caps: 3
- type: Collidable
shapes:
- !type:PhysShapeAabb
bounds: "-0.25,-0.25,0.25,0.25"
mask: 3
layer: 1
IsScrapingFloor: true
- type: Physics
mass: 5
Anchored: false
- type: Sound
- type: entity - type: entity
parent: ReagentItem parent: BaseItem
name: Bucket name: Bucket
id: Bucket id: Bucket
description: "It's a bucket." description: "It's a bucket."
components: components:
- type: Clickable
- type: Sprite - type: Sprite
texture: Objects/Janitorial/bucket.png texture: Objects/Janitorial/bucket.png
drawdepth: Objects
- type: Icon - type: Icon
texture: Objects/Janitorial/bucket.png texture: Objects/Janitorial/bucket.png
- type: Bucket
- type: Sound
- type: Solution - type: Solution
maxVol: 500 maxVol: 500
caps: 3 caps: 3
- type: Collidable
shapes:
- !type:PhysShapeAabb
bounds: "-0.25,-0.25,0.25,0.25"
mask: 3
layer: 1
IsScrapingFloor: true
- type: Physics
mass: 5
Anchored: false
- type: Sound

View File

@@ -1,7 +1,7 @@
- type: reagent - type: reagent
id: chem.Nutriment id: chem.Nutriment
name: Nutriment name: Nutriment
desc: Generic nutrition desc: All the vitamins, minerals, and carbohydrates the body needs in pure form.
color: "#664330" color: "#664330"
metabolism: metabolism:
- !type:DefaultFood - !type:DefaultFood
@@ -17,7 +17,7 @@
id: chem.H2O id: chem.H2O
name: Water name: Water
desc: A tasty colorless liquid. desc: A tasty colorless liquid.
color: "#808080" color: "#DEF7F5"
metabolism: metabolism:
- !type:DefaultDrink - !type:DefaultDrink
rate: 1 rate: 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

View File

@@ -0,0 +1,31 @@
{
"version": 1,
"size": {
"x": 32,
"y": 32
},
"license": "CC-BY-SA-3.0",
"copyright": "https://github.com/discordia-space/CEV-Eris/raw/aff0d780742ca3902d8b05f854c212c8cda32c4f/icons/effects/blood.dmi",
"states": [
{
"name": "gibblet-0",
"directions": 1
},
{
"name": "gibblet-1",
"directions": 1
},
{
"name": "gibblet-2",
"directions": 1
},
{
"name": "gibblet-3",
"directions": 1
},
{
"name": "gibblet-4",
"directions": 1
}
]
}

View File

@@ -0,0 +1,39 @@
{
"version": 1,
"size": {
"x": 32,
"y": 32
},
"license": "CC-BY-SA-3.0",
"copyright": "https://github.com/discordia-space/CEV-Eris/raw/aff0d780742ca3902d8b05f854c212c8cda32c4f/icons/effects/blood.dmi",
"states": [
{
"name": "smear-0",
"directions": 1
},
{
"name": "smear-1",
"directions": 1
},
{
"name": "smear-2",
"directions": 1
},
{
"name": "smear-3",
"directions": 1
},
{
"name": "smear-4",
"directions": 1
},
{
"name": "smear-5",
"directions": 1
},
{
"name": "smear-6",
"directions": 1
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

View File

@@ -0,0 +1,35 @@
{
"version": 1,
"size": {
"x": 32,
"y": 32
},
"license": "CC-BY-SA-3.0",
"copyright": "https://github.com/discordia-space/CEV-Eris/raw/aff0d780742ca3902d8b05f854c212c8cda32c4f/icons/effects/blood.dmi",
"states": [
{
"name": "splatter-0",
"directions": 1
},
{
"name": "splatter-1",
"directions": 1
},
{
"name": "splatter-2",
"directions": 1
},
{
"name": "splatter-3",
"directions": 1
},
{
"name": "splatter-4",
"directions": 1
},
{
"name": "splatter-5",
"directions": 1
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

View File

@@ -0,0 +1,27 @@
{
"version": 1,
"size": {
"x": 32,
"y": 32
},
"license": "CC-BY-SA-3.0",
"copyright": "https://github.com/discordia-space/CEV-Eris/raw/aff0d780742ca3902d8b05f854c212c8cda32c4f/icons/effects/blood.dmi",
"states": [
{
"name": "vomit-0",
"directions": 1
},
{
"name": "vomit-1",
"directions": 1
},
{
"name": "vomit-2",
"directions": 1
},
{
"name": "vomit-3",
"directions": 1
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

View File

@@ -0,0 +1,27 @@
{
"version": 1,
"size": {
"x": 32,
"y": 32
},
"license": "CC-BY-SA-3.0",
"copyright": "https://github.com/discordia-space/CEV-Eris/raw/aff0d780742ca3902d8b05f854c212c8cda32c4f/icons/effects/blood.dmi",
"states": [
{
"name": "vomit_toxin-0",
"directions": 1
},
{
"name": "vomit_toxin-1",
"directions": 1
},
{
"name": "vomit_toxin-2",
"directions": 1
},
{
"name": "vomit_toxin-3",
"directions": 1
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

View File

@@ -0,0 +1,31 @@
{
"version": 1,
"size": {
"x": 32,
"y": 32
},
"license": "CC-BY-SA-3.0",
"copyright": "https://github.com/discordia-space/CEV-Eris/raw/aff0d780742ca3902d8b05f854c212c8cda32c4f/icons/effects/blood.dmi",
"states": [
{
"name": "writing-0",
"directions": 1
},
{
"name": "writing-1",
"directions": 1
},
{
"name": "writing-2",
"directions": 1
},
{
"name": "writing-3",
"directions": 1
},
{
"name": "writing-4",
"directions": 1
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 B