diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs
index 0ce540980a..2a51408f8b 100644
--- a/Content.Client/Entry/IgnoredComponents.cs
+++ b/Content.Client/Entry/IgnoredComponents.cs
@@ -63,6 +63,7 @@ namespace Content.Client.Entry
"AtmosFixMarker",
"CablePlacer",
"Drink",
+ "Drain",
"Food",
"DeployableBarrier",
"MagicMirror",
diff --git a/Content.Server/Fluids/Components/DrainComponent.cs b/Content.Server/Fluids/Components/DrainComponent.cs
new file mode 100644
index 0000000000..174a820381
--- /dev/null
+++ b/Content.Server/Fluids/Components/DrainComponent.cs
@@ -0,0 +1,39 @@
+namespace Content.Server.Fluids.Components
+{
+ [RegisterComponent]
+ public sealed class DrainComponent : Component
+ {
+ public const string SolutionName = "drainBuffer";
+
+ [DataField("accumulator")]
+ public float Accumulator = 0f;
+
+ ///
+ /// How many units per second the drain can absorb from the surrounding puddles.
+ /// Divided by puddles, so if there are 5 puddles this will take 1/5 from each puddle.
+ /// This will stay fixed to 1 second no matter what DrainFrequency is.
+ ///
+ [DataField("unitsPerSecond")]
+ public float UnitsPerSecond = 6f;
+
+ ///
+ /// How many units are ejected from the buffer per second.
+ ///
+ [DataField("unitsDestroyedPerSecond")]
+ public float UnitsDestroyedPerSecond = 1f;
+
+ ///
+ /// How many (unobstructed) tiles away the drain will
+ /// drain puddles from.
+ ///
+ [DataField("range")]
+ public float Range = 2f;
+
+ ///
+ /// How often in seconds the drain checks for puddles around it.
+ /// If the EntityQuery seems a bit unperformant this can be increased.
+ ///
+ [DataField("drainFrequency")]
+ public float DrainFrequency = 1f;
+ }
+}
diff --git a/Content.Server/Fluids/Components/EvaporationComponent.cs b/Content.Server/Fluids/Components/EvaporationComponent.cs
index 94f9832586..8aaaf122c0 100644
--- a/Content.Server/Fluids/Components/EvaporationComponent.cs
+++ b/Content.Server/Fluids/Components/EvaporationComponent.cs
@@ -1,9 +1,5 @@
using Content.Server.Fluids.EntitySystems;
-using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
-using Robust.Shared.Analyzers;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Fluids.Components
{
diff --git a/Content.Server/Fluids/EntitySystems/DrainSystem.cs b/Content.Server/Fluids/EntitySystems/DrainSystem.cs
new file mode 100644
index 0000000000..072a2b0e66
--- /dev/null
+++ b/Content.Server/Fluids/EntitySystems/DrainSystem.cs
@@ -0,0 +1,89 @@
+using Content.Server.Fluids.Components;
+using Content.Server.Chemistry.EntitySystems;
+using Content.Shared.FixedPoint;
+using Content.Shared.Interaction;
+using Content.Shared.Audio;
+
+namespace Content.Server.Fluids.EntitySystems
+{
+ public sealed class DrainSystem : EntitySystem
+ {
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
+ [Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
+
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ foreach (var drain in EntityQuery())
+ {
+ drain.Accumulator += frameTime;
+ if (drain.Accumulator < drain.DrainFrequency)
+ {
+ continue;
+ }
+ drain.Accumulator -= drain.DrainFrequency;
+
+ /// Best to do this one every second rather than once every tick...
+ _solutionSystem.TryGetSolution(drain.Owner, DrainComponent.SolutionName, out var drainSolution);
+
+ if (drainSolution is null)
+ return;
+
+ /// Remove a bit from the buffer
+ _solutionSystem.SplitSolution(drain.Owner, drainSolution, (drain.UnitsDestroyedPerSecond * drain.DrainFrequency));
+
+ /// This will ensure that UnitsPerSecond is per second...
+ var amount = drain.UnitsPerSecond * drain.DrainFrequency;
+ var xform = Transform(drain.Owner);
+ List puddles = new();
+
+ foreach (var entity in _lookup.GetEntitiesInRange(xform.MapPosition, drain.Range))
+ {
+ /// No InRangeUnobstructed because there's no collision group that fits right now
+ /// and these are placed by mappers and not buildable/movable so shouldnt really be a problem...
+ if (TryComp(entity, out var puddleComp))
+ {
+ puddles.Add(puddleComp);
+ }
+ }
+
+ if (puddles.Count == 0)
+ {
+ _ambientSoundSystem.SetAmbience(drain.Owner, false);
+ continue;
+ }
+
+ _ambientSoundSystem.SetAmbience(drain.Owner, true);
+
+ amount /= puddles.Count;
+
+ foreach (var puddle in puddles)
+ {
+ /// Queue the solution deletion if it's empty. EvaporationSystem might also do this
+ /// but queuedelete should be pretty safe.
+ if (!_solutionSystem.TryGetSolution(puddle.Owner, puddle.SolutionName, out var puddleSolution))
+ {
+ EntityManager.QueueDeleteEntity(puddle.Owner);
+ continue;
+ }
+
+ /// Removes the lowest of:
+ /// the drain component's units per second adjusted for # of puddles
+ /// the puddle's remaining volume (making it cleanly zero)
+ /// the drain's remaining volume in its buffer.
+ var transferSolution = _solutionSystem.SplitSolution(puddle.Owner, puddleSolution,
+ FixedPoint2.Min(FixedPoint2.New(amount), puddleSolution.CurrentVolume, drainSolution.AvailableVolume));
+
+ _solutionSystem.TryAddSolution(drain.Owner, drainSolution, transferSolution);
+
+ if (puddleSolution.CurrentVolume <= 0)
+ {
+ EntityManager.QueueDeleteEntity(puddle.Owner);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Resources/Audio/Ambience/Objects/drain.ogg b/Resources/Audio/Ambience/Objects/drain.ogg
new file mode 100644
index 0000000000..bc27cd181b
Binary files /dev/null and b/Resources/Audio/Ambience/Objects/drain.ogg differ
diff --git a/Resources/Audio/Ambience/Objects/license.txt b/Resources/Audio/Ambience/Objects/license.txt
index 36750672e0..f315c60331 100644
--- a/Resources/Audio/Ambience/Objects/license.txt
+++ b/Resources/Audio/Ambience/Objects/license.txt
@@ -4,3 +4,4 @@ gas_hiss - https://freesound.org/people/geodylabs/sounds/122803/ - CC-BY-3.0
gas_vent - https://freesound.org/people/kyles/sounds/453642/ - CC0-1.0
flowing_water_open - https://freesound.org/people/sterferny/sounds/382322/ - CC0-1.0
server_fans - https://freesound.org/people/DeVern/sounds/610761/ - CC-BY-3.0
+drain.ogg - https://freesound.org/people/PhreaKsAccount/sounds/46266/ - CC-BY-3.0 (by PhreaKsAccount)
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml
index ff9f09dabf..8971b4382b 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml
@@ -18,7 +18,7 @@
maxVol: 50
- type: Tag
tags:
- - DroneUsable #No bucket because it holds chems, they can drag the thing around instead
+ - DroneUsable #No bucket because it holds chems, they can drag the cart or use a drain
- Mop
- type: entity
@@ -42,6 +42,7 @@
reagents:
- ReagentId: Water
Quantity: 250 # half-full at roundstart to leave room for puddles
+ - type: Spillable
- type: DrainableSolution
solution: bucket
- type: RefillableSolution
@@ -128,6 +129,7 @@
mask:
- VaultImpassable
mass: 100
+ - type: Spillable
- type: SolutionContainerManager
solutions:
bucket:
@@ -203,3 +205,37 @@
maxFillLevels: 3
fillBaseName: cart_water_
changeColor: false
+
+
+- type: entity
+ id: FloorDrain
+ name: drain
+ description: Drains puddles around it. Useful for dumping mop buckets or keeping certain rooms clean.
+ placement:
+ mode: SnapgridCenter
+ components:
+ - type: Sprite
+ netsync: false
+ drawdepth: FloorObjects
+ sprite: Objects/Specific/Janitorial/drain.rsi
+ state: icon
+ - type: InteractionOutline
+ - type: Clickable
+ - type: Transform
+ anchored: true
+ - type: Physics
+ bodyType: Static
+ - type: Drain
+ - type: AmbientSound
+ enabled: false
+ volume: -8
+ range: 8
+ sound:
+ path: /Audio/Ambience/Objects/drain.ogg
+ - type: SolutionContainerManager
+ solutions:
+ drainBuffer:
+ maxVol: 500
+ - type: DrainableSolution
+ solution: drainBuffer
+
diff --git a/Resources/Textures/Objects/Specific/Janitorial/drain.rsi/icon.png b/Resources/Textures/Objects/Specific/Janitorial/drain.rsi/icon.png
new file mode 100644
index 0000000000..de9a244bdf
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Janitorial/drain.rsi/icon.png differ
diff --git a/Resources/Textures/Objects/Specific/Janitorial/drain.rsi/meta.json b/Resources/Textures/Objects/Specific/Janitorial/drain.rsi/meta.json
new file mode 100644
index 0000000000..7835a4c7bf
--- /dev/null
+++ b/Resources/Textures/Objects/Specific/Janitorial/drain.rsi/meta.json
@@ -0,0 +1,15 @@
+{
+ "version":1,
+ "size":{
+ "x":32,
+ "y":32
+ },
+ "license":"CC-BY-SA-3.0",
+ "copyright":"Created by EmoGarbage",
+ "states":[
+ {
+ "name":"icon",
+ "directions": 4
+ }
+ ]
+}