diff --git a/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsComponent.cs b/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsComponent.cs
index da21106476..d84e838c2b 100644
--- a/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsComponent.cs
+++ b/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsComponent.cs
@@ -36,6 +36,13 @@ namespace Content.Client.Chemistry.Visualizers
[DataField("metamorphicNameFull")]
public string MetamorphicNameFull = "transformable-container-component-glass";
+ ///
+ /// Which solution of the SolutionContainerManagerComponent to represent.
+ /// If not set, will work as default.
+ ///
+ [DataField("solutionName")]
+ public string SolutionName = "";
+
public string InitialName = string.Empty;
public string InitialDescription = string.Empty;
}
diff --git a/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs b/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs
index 3eea146050..6f5c14852e 100644
--- a/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs
+++ b/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs
@@ -25,6 +25,16 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem(uid, SolutionContainerVisuals.SolutionName, out var name,
+ args.Component) && name != component.SolutionName)
+ {
+ return;
+ }
+ }
+
if (!AppearanceSystem.TryGetData(uid, SolutionContainerVisuals.FillFraction, out var fraction, args.Component))
return;
diff --git a/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs
index bd6ea0d1ac..fa33309f58 100644
--- a/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs
+++ b/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs
@@ -152,6 +152,10 @@ public sealed partial class SolutionContainerSystem : EntitySystem
_appearance.SetData(uid, SolutionContainerVisuals.FillFraction, solution.FillFraction, appearanceComponent);
_appearance.SetData(uid, SolutionContainerVisuals.Color, solution.GetColor(_prototypeManager), appearanceComponent);
+ if (solution.Name != null)
+ {
+ _appearance.SetData(uid, SolutionContainerVisuals.SolutionName, solution.Name, appearanceComponent);
+ }
if (solution.GetPrimaryReagentId() is { } reagent)
{
diff --git a/Content.Server/Fluids/EntitySystems/DrainSystem.cs b/Content.Server/Fluids/EntitySystems/DrainSystem.cs
index 50f359ddc5..8d415599f5 100644
--- a/Content.Server/Fluids/EntitySystems/DrainSystem.cs
+++ b/Content.Server/Fluids/EntitySystems/DrainSystem.cs
@@ -1,101 +1,265 @@
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Fluids.Components;
using Content.Server.Chemistry.EntitySystems;
+using Content.Server.DoAfter;
+using Content.Server.Popups;
using Content.Shared.FixedPoint;
using Content.Shared.Audio;
+using Content.Shared.Database;
+using Content.Shared.DoAfter;
+using Content.Shared.Examine;
+using Content.Shared.Fluids;
+using Content.Shared.Interaction;
+using Content.Shared.Tag;
+using Content.Shared.Verbs;
using Content.Shared.Fluids.Components;
using Robust.Shared.Collections;
+using Robust.Shared.Random;
+using Robust.Shared.Utility;
-namespace Content.Server.Fluids.EntitySystems
+namespace Content.Server.Fluids.EntitySystems;
+
+public sealed class DrainSystem : SharedDrainSystem
{
- public sealed class DrainSystem : EntitySystem
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
+ [Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
+ [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly TagSystem _tagSystem = default!;
+ [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly PuddleSystem _puddleSystem = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+
+ public override void Initialize()
{
- [Dependency] private readonly EntityLookupSystem _lookup = default!;
- [Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
- [Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
+ base.Initialize();
+ SubscribeLocalEvent>(AddEmptyVerb);
+ SubscribeLocalEvent(OnExamined);
+ SubscribeLocalEvent(OnInteract);
+ SubscribeLocalEvent(OnDoAfter);
+ }
- public override void Update(float frameTime)
+ private void AddEmptyVerb(EntityUid uid, DrainComponent component, GetVerbsEvent args)
+ {
+ if (!args.CanAccess || !args.CanInteract || args.Using == null)
+ return;
+
+ if (!TryComp(args.Using, out SpillableComponent? spillable) ||
+ !TryComp(args.Target, out DrainComponent? drain))
+ return;
+
+ Verb verb = new()
{
- base.Update(frameTime);
- var managerQuery = GetEntityQuery();
- var xformQuery = GetEntityQuery();
- var puddleQuery = GetEntityQuery();
- var puddles = new ValueList<(EntityUid Entity, string Solution)>();
-
- foreach (var drain in EntityQuery())
+ Text = Loc.GetString("drain-component-empty-verb-inhand", ("object", Name(args.Using.Value))),
+ Act = () =>
{
- drain.Accumulator += frameTime;
- if (drain.Accumulator < drain.DrainFrequency)
+ Empty(args.Using.Value, spillable, args.Target, drain);
+ },
+ Impact = LogImpact.Low,
+ Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/eject.svg.192dpi.png"))
+
+ };
+ args.Verbs.Add(verb);
+ }
+
+ private void Empty(EntityUid container, SpillableComponent spillable, EntityUid target, DrainComponent drain)
+ {
+ // Find the solution in the container that is emptied
+ if (!_solutionSystem.TryGetDrainableSolution(container, out var containerSolution) ||
+ containerSolution.Volume == FixedPoint2.Zero)
+ {
+ _popupSystem.PopupEntity(
+ Loc.GetString("drain-component-empty-verb-using-is-empty-message", ("object", container)),
+ container);
+ return;
+ }
+
+ // try to find the drain's solution
+ if (!_solutionSystem.TryGetSolution(target, DrainComponent.SolutionName, out var drainSolution))
+ {
+ return;
+ }
+
+ // Try to transfer as much solution as possible to the drain
+
+ var transferSolution = _solutionSystem.SplitSolution(container, containerSolution,
+ FixedPoint2.Min(containerSolution.Volume, drainSolution.AvailableVolume));
+
+ _solutionSystem.TryAddSolution(target, drainSolution, transferSolution);
+
+ _audioSystem.PlayPvs(drain.ManualDrainSound, target);
+ _ambientSoundSystem.SetAmbience(target, true);
+
+ // If drain is full, spill
+
+ if (drainSolution.MaxVolume == drainSolution.Volume)
+ {
+ _puddleSystem.TrySpillAt(Transform(target).Coordinates, containerSolution, out var puddle);
+ _popupSystem.PopupEntity(
+ Loc.GetString("drain-component-empty-verb-target-is-full-message", ("object", target)),
+ container);
+ }
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ var managerQuery = GetEntityQuery();
+ var xformQuery = GetEntityQuery();
+ var puddleQuery = GetEntityQuery();
+ var puddles = new ValueList<(EntityUid Entity, string Solution)>();
+
+ foreach (var drain in EntityQuery())
+ {
+ drain.Accumulator += frameTime;
+ if (drain.Accumulator < drain.DrainFrequency)
+ {
+ continue;
+ }
+ drain.Accumulator -= drain.DrainFrequency;
+
+ // Disable ambient sound from emptying manually
+ if (!drain.AutoDrain)
+ {
+ _ambientSoundSystem.SetAmbience(drain.Owner, false);
+ return;
+ }
+
+ if (!managerQuery.TryGetComponent(drain.Owner, out var manager))
+ continue;
+
+ // Best to do this one every second rather than once every tick...
+ _solutionSystem.TryGetSolution(drain.Owner, DrainComponent.SolutionName, out var drainSolution, manager);
+
+ if (drainSolution is null)
+ continue;
+
+ if (drainSolution.AvailableVolume <= 0)
+ {
+ _ambientSoundSystem.SetAmbience(drain.Owner, false);
+ continue;
+ }
+
+ // 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;
+
+ if (!xformQuery.TryGetComponent(drain.Owner, out var xform))
+ continue;
+
+ puddles.Clear();
+
+ 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 (puddleQuery.TryGetComponent(entity, out var puddle))
{
- continue;
+ puddles.Add((entity, puddle.SolutionName));
}
- drain.Accumulator -= drain.DrainFrequency;
+ }
- if (!managerQuery.TryGetComponent(drain.Owner, out var manager))
- continue;
+ if (puddles.Count == 0)
+ {
+ _ambientSoundSystem.SetAmbience(drain.Owner, false);
+ continue;
+ }
- // Best to do this one every second rather than once every tick...
- _solutionSystem.TryGetSolution(drain.Owner, DrainComponent.SolutionName, out var drainSolution, manager);
+ _ambientSoundSystem.SetAmbience(drain.Owner, true);
- if (drainSolution is null)
- continue;
+ amount /= puddles.Count;
- // 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;
-
- if (!xformQuery.TryGetComponent(drain.Owner, out var xform))
- continue;
-
- puddles.Clear();
-
- foreach (var entity in _lookup.GetEntitiesInRange(xform.MapPosition, drain.Range))
+ foreach (var (puddle, solution) 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, solution, out var puddleSolution))
{
- // 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 (puddleQuery.TryGetComponent(entity, out var puddle))
- {
- puddles.Add((entity, puddle.SolutionName));
- }
- }
-
- if (puddles.Count == 0)
- {
- _ambientSoundSystem.SetAmbience(drain.Owner, false);
+ EntityManager.QueueDeleteEntity(puddle);
continue;
}
- _ambientSoundSystem.SetAmbience(drain.Owner, true);
+ // 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, puddleSolution,
+ FixedPoint2.Min(FixedPoint2.New(amount), puddleSolution.Volume, drainSolution.AvailableVolume));
- amount /= puddles.Count;
+ _solutionSystem.TryAddSolution(drain.Owner, drainSolution, transferSolution);
- foreach (var (puddle, solution) in puddles)
+ if (puddleSolution.Volume <= 0)
{
- // Queue the solution deletion if it's empty. EvaporationSystem might also do this
- // but queuedelete should be pretty safe.
- if (!_solutionSystem.TryGetSolution(puddle, solution, out var puddleSolution))
- {
- EntityManager.QueueDeleteEntity(puddle);
- 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, puddleSolution,
- FixedPoint2.Min(FixedPoint2.New(amount), puddleSolution.Volume, drainSolution.AvailableVolume));
-
- _solutionSystem.TryAddSolution(drain.Owner, drainSolution, transferSolution);
-
- if (puddleSolution.Volume <= 0)
- {
- QueueDel(puddle);
- }
+ QueueDel(puddle);
}
}
}
}
+
+ private void OnExamined(EntityUid uid, DrainComponent component, ExaminedEvent args)
+ {
+ if (!args.IsInDetailsRange ||
+ !TryComp(uid, out SolutionContainerManagerComponent? solutionComp) ||
+ !_solutionSystem.TryGetSolution(uid, DrainComponent.SolutionName, out var drainSolution))
+ { return; }
+
+ var text = drainSolution.AvailableVolume != 0 ?
+ Loc.GetString("drain-component-examine-volume", ("volume", drainSolution.AvailableVolume)) :
+ Loc.GetString("drain-component-examine-hint-full");
+ args.Message.AddMarkup($"\n\n{text}");
+ }
+
+ private void OnInteract(EntityUid uid, DrainComponent component, InteractEvent args)
+ {
+ if (!args.CanReach || args.Target == null ||
+ !_tagSystem.HasTag(args.Used, DrainComponent.PlungerTag) ||
+ !_solutionSystem.TryGetSolution(args.Target.Value, DrainComponent.SolutionName, out var drainSolution))
+ { return; }
+
+ if (drainSolution.AvailableVolume > 0)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("drain-component-unclog-notapplicable", ("object", args.Target.Value)), args.Target.Value);
+ return;
+ }
+
+ _audioSystem.PlayPvs(component.PlungerSound, uid);
+
+
+ var doAfterArgs = new DoAfterArgs(args.User, component.UnclogDuration, new DrainDoAfterEvent(),uid, args.Used)
+ {
+ BreakOnTargetMove = true,
+ BreakOnUserMove = true,
+ BreakOnDamage = true,
+ BreakOnHandChange = true
+ };
+
+ _doAfterSystem.TryStartDoAfter(doAfterArgs);
+ }
+
+ private void OnDoAfter(EntityUid uid, DrainComponent component, DoAfterEvent args)
+ {
+ if (args.Target == null)
+ return;
+
+ if (!_random.Prob(component.UnclogProbability))
+ {
+ _popupSystem.PopupEntity(Loc.GetString("drain-component-unclog-fail", ("object", args.Target.Value)), args.Target.Value);
+ return;
+ }
+
+
+ if (!_solutionSystem.TryGetSolution(args.Target.Value, DrainComponent.SolutionName,
+ out var drainSolution))
+ {
+ return;
+ }
+
+
+ _solutionSystem.RemoveAllSolution(args.Target.Value, drainSolution);
+ _audioSystem.PlayPvs(component.UnclogSound, args.Target.Value);
+ _popupSystem.PopupEntity(Loc.GetString("drain-component-unclog-success", ("object", args.Target.Value)), args.Target.Value);
+ }
}
diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.Transfers.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Transfers.cs
index 4ecdefa7f5..c73685c288 100644
--- a/Content.Server/Fluids/EntitySystems/PuddleSystem.Transfers.cs
+++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.Transfers.cs
@@ -1,3 +1,4 @@
+using Content.Server.Fluids.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.DragDrop;
using Content.Shared.FixedPoint;
diff --git a/Content.Shared/Chemistry/SolutionVisuals.cs b/Content.Shared/Chemistry/SolutionVisuals.cs
index a0ac0ea403..f24be50364 100644
--- a/Content.Shared/Chemistry/SolutionVisuals.cs
+++ b/Content.Shared/Chemistry/SolutionVisuals.cs
@@ -8,6 +8,7 @@ namespace Content.Shared.Chemistry
Color,
FillFraction,
BaseOverride,
+ SolutionName
}
public enum SolutionContainerLayers : byte
diff --git a/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs b/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs
index 0ab3eb40b7..8903e23b89 100644
--- a/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs
+++ b/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs
@@ -89,7 +89,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
// I feel like this is somewhat cursed, but its the only way I can think of without having to just send
// redundant data over the network and increasing DoAfter boilerplate.
var evType = typeof(DoAfterAttemptEvent<>).MakeGenericType(args.Event.GetType());
- doAfter.AttemptEvent = _factory.CreateInstance(evType, new object[] { doAfter, args.Event }, inject: false);
+ doAfter.AttemptEvent = _factory.CreateInstance(evType, new object[] { doAfter, args.Event });
}
if (args.EventTarget != null)
diff --git a/Content.Shared/Fluids/Components/DrainComponent.cs b/Content.Shared/Fluids/Components/DrainComponent.cs
index b456a87fad..f7a88c80e4 100644
--- a/Content.Shared/Fluids/Components/DrainComponent.cs
+++ b/Content.Shared/Fluids/Components/DrainComponent.cs
@@ -1,15 +1,31 @@
-using Robust.Shared.GameStates;
+using Robust.Shared.Audio;
namespace Content.Shared.Fluids.Components;
-[RegisterComponent, NetworkedComponent]
+///
+/// A Drain allows an entity to absorb liquid in a disposal goal. Drains can be filled manually (with the Empty verb)
+/// or they can absorb puddles of liquid around them when AutoDrain is set to true.
+/// When the entity also has a SolutionContainerManager attached with a solution named drainBuffer, this solution
+/// gets filled until the drain is full.
+/// When the drain is full, it can be unclogged using a plunger (i.e. an entity with a Plunger tag attached).
+/// Later this can be refactored into a proper Plunger component if needed.
+///
+[RegisterComponent, Access(typeof(SharedDrainSystem))]
public sealed class DrainComponent : Component
{
public const string SolutionName = "drainBuffer";
+ public const string PlungerTag = "Plunger";
[DataField("accumulator")]
public float Accumulator = 0f;
+ ///
+ /// Does this drain automatically absorb surrouding puddles? Or is it a drain designed to empty
+ /// solutions in it manually?
+ ///
+ [DataField("autoDrain"), ViewVariables(VVAccess.ReadOnly)]
+ public bool AutoDrain = true;
+
///
/// 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.
@@ -28,7 +44,7 @@ public sealed class DrainComponent : Component
/// How many (unobstructed) tiles away the drain will
/// drain puddles from.
///
- [DataField("range")]
+ [DataField("range"), ViewVariables(VVAccess.ReadWrite)]
public float Range = 2f;
///
@@ -37,4 +53,25 @@ public sealed class DrainComponent : Component
///
[DataField("drainFrequency")]
public float DrainFrequency = 1f;
+
+ ///
+ /// How much time it takes to unclog it with a plunger
+ ///
+ [DataField("unclogDuration"), ViewVariables(VVAccess.ReadWrite)]
+ public float UnclogDuration = 1f;
+
+ ///
+ /// What's the probability of uncloging on each try
+ ///
+ [DataField("unclogProbability"), ViewVariables(VVAccess.ReadWrite)]
+ public float UnclogProbability = 0.3f;
+
+ [DataField("manualDrainSound"), ViewVariables(VVAccess.ReadOnly)]
+ public SoundSpecifier ManualDrainSound = new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg");
+
+ [DataField("plungerSound"), ViewVariables(VVAccess.ReadOnly)]
+ public SoundSpecifier PlungerSound = new SoundPathSpecifier("/Audio/Items/Janitor/plunger.ogg");
+
+ [DataField("unclogSound"), ViewVariables(VVAccess.ReadOnly)]
+ public SoundSpecifier UnclogSound = new SoundPathSpecifier("/Audio/Effects/Fluids/glug.ogg");
}
diff --git a/Content.Shared/Fluids/SharedDrainSystem.cs b/Content.Shared/Fluids/SharedDrainSystem.cs
new file mode 100644
index 0000000000..43df0c352b
--- /dev/null
+++ b/Content.Shared/Fluids/SharedDrainSystem.cs
@@ -0,0 +1,12 @@
+using Content.Shared.DoAfter;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Fluids;
+
+public class SharedDrainSystem : EntitySystem
+{
+ [Serializable, NetSerializable]
+ public sealed class DrainDoAfterEvent : SimpleDoAfterEvent
+ {
+ }
+}
diff --git a/Resources/Audio/Effects/Fluids/ATTRIBUTION.txt b/Resources/Audio/Effects/Fluids/ATTRIBUTION.txt
deleted file mode 100644
index 78c9a1114a..0000000000
--- a/Resources/Audio/Effects/Fluids/ATTRIBUTION.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-blood1.ogg under CC-0 from https://freesound.org/people/kyles/sounds/453769/
-blood2.ogg under CC-BY 3.0 from https://freesound.org/people/EminYILDIRIM/sounds/554284/
\ No newline at end of file
diff --git a/Resources/Audio/Effects/Fluids/attributions.yml b/Resources/Audio/Effects/Fluids/attributions.yml
new file mode 100644
index 0000000000..4e28c99252
--- /dev/null
+++ b/Resources/Audio/Effects/Fluids/attributions.yml
@@ -0,0 +1,14 @@
+- files: ["blood1.ogg"]
+ license: "CC0-1.0"
+ copyright: "Created by Kyles"
+ source: "https://freesound.org/people/kyles/sounds/453769/"
+
+- files: ["blood2.ogg"]
+ license: "CC-BY-4.0"
+ copyright: "Created by EminYILDIRIM"
+ source: "https://freesound.org/people/EminYILDIRIM/sounds/554284/"
+
+- files: ["glug.ogg"]
+ license: "CC0-1.0"
+ copyright: "Created by brittmosel"
+ source: "https://freesound.org/people/brittmosel/sounds/529300/"
diff --git a/Resources/Audio/Effects/Fluids/glug.ogg b/Resources/Audio/Effects/Fluids/glug.ogg
new file mode 100644
index 0000000000..5828bb0568
Binary files /dev/null and b/Resources/Audio/Effects/Fluids/glug.ogg differ
diff --git a/Resources/Audio/Items/Janitor/attributions.yml b/Resources/Audio/Items/Janitor/attributions.yml
new file mode 100644
index 0000000000..f9cc68c82d
--- /dev/null
+++ b/Resources/Audio/Items/Janitor/attributions.yml
@@ -0,0 +1,4 @@
+- files: ["plunger.ogg"]
+ license: "CC-BY-4.0"
+ copyright: "Created by tosha73"
+ source: "https://freesound.org/people/tosha73/sounds/540784/"
diff --git a/Resources/Audio/Items/Janitor/plunger.ogg b/Resources/Audio/Items/Janitor/plunger.ogg
new file mode 100644
index 0000000000..f0605b8aee
Binary files /dev/null and b/Resources/Audio/Items/Janitor/plunger.ogg differ
diff --git a/Resources/Locale/en-US/fluids/components/drain-component.ftl b/Resources/Locale/en-US/fluids/components/drain-component.ftl
new file mode 100644
index 0000000000..897971a788
--- /dev/null
+++ b/Resources/Locale/en-US/fluids/components/drain-component.ftl
@@ -0,0 +1,8 @@
+drain-component-empty-verb-using-is-empty-message = { CAPITALIZE(THE($object)) } is empty!
+drain-component-empty-verb-target-is-full-message = { CAPITALIZE(THE($object)) } is full!
+drain-component-empty-verb-inhand = Empty {$object}
+drain-component-examine-hint-full = [color="blue"]It is filled to the brim. Maybe a plunger can help?[/color]
+drain-component-examine-volume = [color="blue"]Remaining space - {$volume}u.[/color]
+drain-component-unclog-fail = { CAPITALIZE(THE($object)) } is still full.
+drain-component-unclog-success = { CAPITALIZE(THE($object)) } unclogs.
+drain-component-unclog-notapplicable = { CAPITALIZE(THE($object)) } isn't clogged.
diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_service.yml b/Resources/Prototypes/Catalog/Cargo/cargo_service.yml
index c3674f5a27..a8880fe4f0 100644
--- a/Resources/Prototypes/Catalog/Cargo/cargo_service.yml
+++ b/Resources/Prototypes/Catalog/Cargo/cargo_service.yml
@@ -4,7 +4,7 @@
sprite: Objects/Specific/Janitorial/janitorial.rsi
state: cleaner
product: CrateServiceJanitorialSupplies
- cost: 500
+ cost: 560
category: Service
group: market
diff --git a/Resources/Prototypes/Catalog/Fills/Crates/service.yml b/Resources/Prototypes/Catalog/Fills/Crates/service.yml
index 87323512a8..ed6970400d 100644
--- a/Resources/Prototypes/Catalog/Fills/Crates/service.yml
+++ b/Resources/Prototypes/Catalog/Fills/Crates/service.yml
@@ -15,6 +15,8 @@
amount: 2
- id: TrashBag
amount: 2
+ - id: Plunger
+ amount: 2
- type: entity
id: CrateServiceReplacementLights
diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/service.yml b/Resources/Prototypes/Catalog/Fills/Lockers/service.yml
index 7da64f2d1a..75dfcc2de4 100644
--- a/Resources/Prototypes/Catalog/Fills/Lockers/service.yml
+++ b/Resources/Prototypes/Catalog/Fills/Lockers/service.yml
@@ -71,6 +71,8 @@
amount: 2
- id: FlashlightLantern
amount: 2
+ - id: Plunger
+ amount: 2
- type: entity
id: ClosetLegalFilled
diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/maintenance.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/maintenance.yml
index 01cb8f653b..da09e3baff 100644
--- a/Resources/Prototypes/Entities/Markers/Spawners/Random/maintenance.yml
+++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/maintenance.yml
@@ -94,6 +94,7 @@
- SheetSteel
- SheetPlastic
- ResearchDisk
+ - Plunger
chance: 0.6
offset: 0.0
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml
index 501901a2a7..0d0d3822e9 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml
@@ -317,6 +317,26 @@
path: /Audio/Effects/metalbreak.ogg
- type: ItemMapper
mapLayers:
+ cart_plunger:
+ whitelist:
+ tags:
+ - Plunger
+ cart_mop:
+ whitelist:
+ tags:
+ - Mop
+ cart_garbage:
+ whitelist:
+ tags:
+ - TrashBag
+ cart_replacer:
+ whitelist:
+ components:
+ - LightReplacer
+ cart_spray:
+ whitelist:
+ tags:
+ - Spray
cart_sign1: # this is like stack of floor signs
minCount: 1
whitelist:
@@ -337,26 +357,10 @@
whitelist:
tags:
- WetFloorSign
- cart_spray:
- whitelist:
- tags:
- - Spray
- cart_garbage:
- whitelist:
- tags:
- - TrashBag
- cart_replacer:
- whitelist:
- components:
- - LightReplacer
cart_bucket:
whitelist:
tags:
- Bucket
- cart_mop:
- whitelist:
- tags:
- - Mop
sprite: Objects/Specific/Janitorial/janitorial_cart.rsi
- type: Appearance
- type: SolutionContainerVisuals
@@ -392,7 +396,11 @@
- type: Sprite
drawdepth: FloorObjects
sprite: Objects/Specific/Janitorial/drain.rsi
- state: icon
+ layers:
+ - state: icon
+ - map: [ "enum.SolutionContainerLayers.Fill" ]
+ state: fill-1
+ visible: false
- type: InteractionOutline
- type: Clickable
- type: Transform
@@ -400,13 +408,18 @@
- type: Physics
bodyType: Static
canCollide: false
- - type: Drain
- type: AmbientSound
enabled: false
volume: -8
range: 8
sound:
path: /Audio/Ambience/Objects/drain.ogg
+ - type: Drain
+ - type: Appearance
+ - type: SolutionContainerVisuals
+ maxFillLevels: 1
+ fillBaseName: fill-
+ solutionName: drainBuffer
- type: SolutionContainerManager
solutions:
drainBuffer:
@@ -433,6 +446,24 @@
- !type:PlaySoundBehavior
sound:
path: /Audio/Effects/metalbreak.ogg
+
+- type: entity
+ name: plunger
+ id: Plunger
+ parent: BaseItem
+ description: A plunger with a red plastic suction-cup and a wooden handle. Used to unclog drains.
+ components:
+ - type: Tag
+ tags:
+ - Plunger
+ - type: Sprite
+ netsync: false
+ sprite: Objects/Specific/Janitorial/janitorial.rsi
+ state: plunger
+ - type: Item
+ sprite: Objects/Specific/Janitorial/janitorial.rsi
+ heldPrefix: plunger
+ - type: ItemCooldown
- type: GuideHelp
guides:
- Janitorial
diff --git a/Resources/Prototypes/Entities/Structures/Furniture/sink.yml b/Resources/Prototypes/Entities/Structures/Furniture/sink.yml
index 0e1654f598..507134364d 100644
--- a/Resources/Prototypes/Entities/Structures/Furniture/sink.yml
+++ b/Resources/Prototypes/Entities/Structures/Furniture/sink.yml
@@ -10,9 +10,21 @@
- type: InteractionOutline
- type: Sprite
sprite: Structures/Furniture/sink.rsi
- state: sink_stem
+ layers:
+ - state: sink_stem
+ - map: [ "enum.SolutionContainerLayers.Fill" ]
+ state: sink-fill-1
+ visible: false
+ netsync: false
+ - type: Appearance
+ - type: SolutionContainerVisuals
+ maxFillLevels: 1
+ fillBaseName: sink-fill-
+ solutionName: drainBuffer
- type: SolutionContainerManager
solutions:
+ drainBuffer:
+ maxVol: 100
tank:
maxVol: 500
- type: SolutionRegeneration
@@ -24,6 +36,8 @@
- type: DrainableSolution
solution: tank
- type: ReagentTank
+ - type: Drain
+ autoDrain: false
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
@@ -44,6 +58,13 @@
- !type:PlaySoundBehavior
sound:
path: /Audio/Effects/metalbreak.ogg
+ - type: AmbientSound
+ enabled: false
+ volume: -8
+ range: 8
+ sound:
+ path: /Audio/Ambience/Objects/drain.ogg
+
- type: entity
name: sink
@@ -53,6 +74,8 @@
components:
- type: SolutionContainerManager
solutions:
+ drainBuffer:
+ maxVol: 200
tank:
reagents:
- ReagentId: Water
@@ -63,9 +86,17 @@
id: SinkWide
parent: Sink
components:
- - type: Sprite
- sprite: Structures/Furniture/sink.rsi
- state: sink_wide
+ - type: Sprite
+ sprite: Structures/Furniture/sink.rsi
+ layers:
+ - state: sink_wide
+ - map: [ "enum.SolutionContainerLayers.Fill" ]
+ state: sink_wide-fill-1
+ visible: false
+ - type: SolutionContainerVisuals
+ maxFillLevels: 1
+ fillBaseName: sink_wide-fill-
+ solutionName: drainBuffer
#Stemless Sink
@@ -76,7 +107,11 @@
components:
- type: Sprite
sprite: Structures/Furniture/sink.rsi
- state: sink
+ layers:
+ - state: sink
+ - map: [ "enum.SolutionContainerLayers.Fill" ]
+ state: sink-fill-1
+ visible: false
- type: entity
name: sink
@@ -86,6 +121,8 @@
components:
- type: SolutionContainerManager
solutions:
+ drainBuffer:
+ maxVol: 100
tank:
reagents:
- ReagentId: Water
diff --git a/Resources/Prototypes/Entities/Structures/Furniture/toilet.yml b/Resources/Prototypes/Entities/Structures/Furniture/toilet.yml
index 9d476ddca1..af0d2f1794 100644
--- a/Resources/Prototypes/Entities/Structures/Furniture/toilet.yml
+++ b/Resources/Prototypes/Entities/Structures/Furniture/toilet.yml
@@ -22,6 +22,8 @@
stash: !type:ContainerSlot {}
- type: SolutionContainerManager
solutions:
+ drainBuffer:
+ maxVol: 500
toilet:
maxVol: 250
- type: Transform
@@ -32,6 +34,12 @@
graph: Toilet
node: toilet
- type: Appearance
+ - type: Drain
+ autoDrain: false
+ - type: SolutionContainerVisuals
+ maxFillLevels: 1
+ fillBaseName: fill-
+ solutionName: drainBuffer
- type: StaticPrice
price: 25
diff --git a/Resources/Prototypes/Recipes/Lathes/janitorial.yml b/Resources/Prototypes/Recipes/Lathes/janitorial.yml
index 902569e4b0..2fd153852f 100644
--- a/Resources/Prototypes/Recipes/Lathes/janitorial.yml
+++ b/Resources/Prototypes/Recipes/Lathes/janitorial.yml
@@ -18,7 +18,7 @@
result: Bucket
completetime: 2
materials:
- Plastic: 100
+ Steel: 100
- type: latheRecipe
id: WetFloorSign
@@ -81,6 +81,14 @@
Steel: 100
Glass: 100
+- type: latheRecipe
+ id: Plunger
+ result: Plunger
+ completetime: 2
+ materials:
+ Plastic: 50
+ Wood: 200
+
- type: latheRecipe
id: WeaponSprayNozzle
result: WeaponSprayNozzle
diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml
index 8c712a7dcc..208e99dd1f 100644
--- a/Resources/Prototypes/tags.yml
+++ b/Resources/Prototypes/tags.yml
@@ -631,9 +631,13 @@
- type: Tag
id: Plastic
+- type: Tag
+ id: Plunger
+
- type: Tag
id: PlushieGhost
+
- type: Tag
id: Powerdrill
diff --git a/Resources/Textures/Objects/Specific/Janitorial/drain.rsi/fill-1.png b/Resources/Textures/Objects/Specific/Janitorial/drain.rsi/fill-1.png
new file mode 100644
index 0000000000..31540a6e02
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Janitorial/drain.rsi/fill-1.png differ
diff --git a/Resources/Textures/Objects/Specific/Janitorial/drain.rsi/icon.png b/Resources/Textures/Objects/Specific/Janitorial/drain.rsi/icon.png
index de9a244bdf..f725b227bc 100644
Binary files a/Resources/Textures/Objects/Specific/Janitorial/drain.rsi/icon.png 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
index 7835a4c7bf..e40dbb086d 100644
--- a/Resources/Textures/Objects/Specific/Janitorial/drain.rsi/meta.json
+++ b/Resources/Textures/Objects/Specific/Janitorial/drain.rsi/meta.json
@@ -5,11 +5,15 @@
"y":32
},
"license":"CC-BY-SA-3.0",
- "copyright":"Created by EmoGarbage",
+ "copyright":"Created by EmoGarbage, fill-1 created by Topy for SS14",
"states":[
{
"name":"icon",
"directions": 4
- }
+ },
+ {
+ "name": "fill-1",
+ "directions": 4
+ }
]
}
diff --git a/Resources/Textures/Objects/Specific/Janitorial/janitorial.rsi/meta.json b/Resources/Textures/Objects/Specific/Janitorial/janitorial.rsi/meta.json
index ae3103e2be..177bd8e619 100644
--- a/Resources/Textures/Objects/Specific/Janitorial/janitorial.rsi/meta.json
+++ b/Resources/Textures/Objects/Specific/Janitorial/janitorial.rsi/meta.json
@@ -24,6 +24,9 @@
},
{
"name": "mopbucket_water-3"
+ },
+ {
+ "name": "plunger"
},
{
"name": "inhand-left",
@@ -36,6 +39,14 @@
{
"name": "equipped-BELT",
"directions": 4
+ },
+ {
+ "name": "plunger-inhand-left",
+ "directions": 4
+ },
+ {
+ "name": "plunger-inhand-right",
+ "directions": 4
}
]
}
diff --git a/Resources/Textures/Objects/Specific/Janitorial/janitorial.rsi/plunger-inhand-left.png b/Resources/Textures/Objects/Specific/Janitorial/janitorial.rsi/plunger-inhand-left.png
new file mode 100644
index 0000000000..a1b319f401
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Janitorial/janitorial.rsi/plunger-inhand-left.png differ
diff --git a/Resources/Textures/Objects/Specific/Janitorial/janitorial.rsi/plunger-inhand-right.png b/Resources/Textures/Objects/Specific/Janitorial/janitorial.rsi/plunger-inhand-right.png
new file mode 100644
index 0000000000..09a35f6831
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Janitorial/janitorial.rsi/plunger-inhand-right.png differ
diff --git a/Resources/Textures/Objects/Specific/Janitorial/janitorial.rsi/plunger.png b/Resources/Textures/Objects/Specific/Janitorial/janitorial.rsi/plunger.png
new file mode 100644
index 0000000000..bc5f68be63
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Janitorial/janitorial.rsi/plunger.png differ
diff --git a/Resources/Textures/Objects/Specific/Janitorial/janitorial_cart.rsi/cart_plunger.png b/Resources/Textures/Objects/Specific/Janitorial/janitorial_cart.rsi/cart_plunger.png
new file mode 100644
index 0000000000..37779d4361
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Janitorial/janitorial_cart.rsi/cart_plunger.png differ
diff --git a/Resources/Textures/Objects/Specific/Janitorial/janitorial_cart.rsi/meta.json b/Resources/Textures/Objects/Specific/Janitorial/janitorial_cart.rsi/meta.json
index 846a3157a5..8691dc073c 100644
--- a/Resources/Textures/Objects/Specific/Janitorial/janitorial_cart.rsi/meta.json
+++ b/Resources/Textures/Objects/Specific/Janitorial/janitorial_cart.rsi/meta.json
@@ -46,6 +46,10 @@
{
"name": "cart_spray",
"directions": 4
+ },
+ {
+ "name": "cart_plunger",
+ "directions": 4
},
{
"name": "cart_water-1",
diff --git a/Resources/Textures/Structures/Furniture/sink.rsi/meta.json b/Resources/Textures/Structures/Furniture/sink.rsi/meta.json
index e00dcab8e4..a2c289e8cf 100644
--- a/Resources/Textures/Structures/Furniture/sink.rsi/meta.json
+++ b/Resources/Textures/Structures/Furniture/sink.rsi/meta.json
@@ -5,7 +5,7 @@
"y": 32
},
"license": "CC-BY-SA-3.0",
- "copyright": "Taken from tgstation https://github.com/tgstation/tgstation/commit/f01afc7edd39b28dd718407d5bbfca3a5dfe995f#diff-378d1b8f0f0a73185e7c82e4ccfdb65102561992a7abb306090ce851f8419780",
+ "copyright": "Taken from tgstation https://github.com/tgstation/tgstation/commit/f01afc7edd39b28dd718407d5bbfca3a5dfe995f#diff-378d1b8f0f0a73185e7c82e4ccfdb65102561992a7abb306090ce851f8419780, sink-fill-1 and sink_wide-fill-1 made by Topy for SS14",
"states": [
{
"name": "sink",
@@ -18,6 +18,14 @@
{
"name": "sink_stem",
"directions": 4
+ },
+ {
+ "name": "sink-fill-1",
+ "directions": 4
+ },
+ {
+ "name": "sink_wide-fill-1",
+ "directions": 4
}
]
}
diff --git a/Resources/Textures/Structures/Furniture/sink.rsi/sink-fill-1.png b/Resources/Textures/Structures/Furniture/sink.rsi/sink-fill-1.png
new file mode 100644
index 0000000000..5ec7cf03e2
Binary files /dev/null and b/Resources/Textures/Structures/Furniture/sink.rsi/sink-fill-1.png differ
diff --git a/Resources/Textures/Structures/Furniture/sink.rsi/sink_wide-fill-1.png b/Resources/Textures/Structures/Furniture/sink.rsi/sink_wide-fill-1.png
new file mode 100644
index 0000000000..bba702a28d
Binary files /dev/null and b/Resources/Textures/Structures/Furniture/sink.rsi/sink_wide-fill-1.png differ