diff --git a/Content.Client/Atmos/Visualizers/Components/PortableScrubberVisualsComponent.cs b/Content.Client/Atmos/Visualizers/Components/PortableScrubberVisualsComponent.cs
new file mode 100644
index 0000000000..d829f6b17b
--- /dev/null
+++ b/Content.Client/Atmos/Visualizers/Components/PortableScrubberVisualsComponent.cs
@@ -0,0 +1,23 @@
+namespace Content.Client.Atmos.Visualizers;
+
+///
+/// Holds 2 pairs of states. The idle/running pair controls animation, while
+/// the ready / full pair controls the color of the light.
+///
+[RegisterComponent]
+public sealed class PortableScrubberVisualsComponent : Component
+{
+ [DataField("idleState", required: true)]
+ public string IdleState = default!;
+
+ [DataField("runningState", required: true)]
+ public string RunningState = default!;
+
+ /// Powered and not full
+ [DataField("readyState", required: true)]
+ public string ReadyState = default!;
+
+ /// Powered and full
+ [DataField("fullState", required: true)]
+ public string FullState = default!;
+}
diff --git a/Content.Client/Atmos/Visualizers/PortableScrubberVisualsSystem.cs b/Content.Client/Atmos/Visualizers/PortableScrubberVisualsSystem.cs
new file mode 100644
index 0000000000..00e25097b3
--- /dev/null
+++ b/Content.Client/Atmos/Visualizers/PortableScrubberVisualsSystem.cs
@@ -0,0 +1,39 @@
+using Robust.Client.GameObjects;
+using Content.Shared.Atmos.Visuals;
+using Content.Client.Power;
+
+namespace Content.Client.Atmos.Visualizers
+{
+ ///
+ /// Controls client-side visuals for portable scrubbers.
+ ///
+ public sealed class PortableScrubberSystem : VisualizerSystem
+ {
+ protected override void OnAppearanceChange(EntityUid uid, PortableScrubberVisualsComponent component, ref AppearanceChangeEvent args)
+ {
+ if (args.Sprite == null)
+ return;
+
+ if (args.Component.TryGetData(PortableScrubberVisuals.IsFull, out bool isFull)
+ && args.Component.TryGetData(PortableScrubberVisuals.IsRunning, out bool isRunning))
+ {
+ var runningState = isRunning ? component.RunningState : component.IdleState;
+ args.Sprite.LayerSetState(PortableScrubberVisualLayers.IsRunning, runningState);
+
+ var fullState = isFull ? component.FullState : component.ReadyState;
+ args.Sprite.LayerSetState(PowerDeviceVisualLayers.Powered, fullState);
+ }
+
+ if (args.Component.TryGetData(PortableScrubberVisuals.IsDraining, out bool isDraining))
+ {
+ args.Sprite.LayerSetVisible(PortableScrubberVisualLayers.IsDraining, isDraining);
+ }
+ }
+ }
+}
+public enum PortableScrubberVisualLayers : byte
+{
+ IsRunning,
+
+ IsDraining
+}
diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs
index 081f796163..15a18baa42 100644
--- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs
+++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs
@@ -150,18 +150,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
if (portNode.NodeGroup is PipeNet {NodeCount: > 1} net)
{
- var buffer = new GasMixture(net.Air.Volume + canister.Air.Volume);
-
- _atmosphereSystem.Merge(buffer, net.Air);
- _atmosphereSystem.Merge(buffer, canister.Air);
-
- net.Air.Clear();
- _atmosphereSystem.Merge(net.Air, buffer);
- net.Air.Multiply(net.Air.Volume / buffer.Volume);
-
- canister.Air.Clear();
- _atmosphereSystem.Merge(canister.Air, buffer);
- canister.Air.Multiply(canister.Air.Volume / buffer.Volume);
+ MixContainerWithPipeNet(canister.Air, net.Air);
}
ContainerManagerComponent? containerManager = null;
@@ -275,5 +264,25 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
appearance.SetData(GasCanisterVisuals.TankInserted, false);
}
+
+ ///
+ /// Mix air from a gas container into a pipe net.
+ /// Useful for anything that uses connector ports.
+ ///
+ public void MixContainerWithPipeNet(GasMixture containerAir, GasMixture pipeNetAir)
+ {
+ var buffer = new GasMixture(pipeNetAir.Volume + containerAir.Volume);
+
+ _atmosphereSystem.Merge(buffer, pipeNetAir);
+ _atmosphereSystem.Merge(buffer, containerAir);
+
+ pipeNetAir.Clear();
+ _atmosphereSystem.Merge(pipeNetAir, buffer);
+ pipeNetAir.Multiply(pipeNetAir.Volume / buffer.Volume);
+
+ containerAir.Clear();
+ _atmosphereSystem.Merge(containerAir, buffer);
+ containerAir.Multiply(containerAir.Volume / buffer.Volume);
+ }
}
}
diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPortableSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPortableSystem.cs
index b46dfa213c..6e81764367 100644
--- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPortableSystem.cs
+++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPortableSystem.cs
@@ -50,7 +50,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
}
}
- private bool FindGasPortIn(EntityUid? gridId, EntityCoordinates coordinates, [NotNullWhen(true)] out GasPortComponent? port)
+ public bool FindGasPortIn(EntityUid? gridId, EntityCoordinates coordinates, [NotNullWhen(true)] out GasPortComponent? port)
{
port = null;
diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs
index fb0f24e7a9..89521f6f41 100644
--- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs
+++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs
@@ -86,33 +86,42 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
AtmosDeviceEnabledEvent args) => UpdateState(uid, component);
private void Scrub(float timeDelta, GasVentScrubberComponent scrubber, GasMixture? tile, PipeNode outlet)
+ {
+ Scrub(timeDelta, scrubber.TransferRate, scrubber.PumpDirection, scrubber.FilterGases, tile, outlet.Air);
+ }
+
+ ///
+ /// True if we were able to scrub, false if we were not.
+ ///
+ public bool Scrub(float timeDelta, float transferRate, ScrubberPumpDirection mode, HashSet filterGases, GasMixture? tile, GasMixture destination)
{
// Cannot scrub if tile is null or air-blocked.
if (tile == null
- || outlet.Air.Pressure >= 50 * Atmospherics.OneAtmosphere) // Cannot scrub if pressure too high.
+ || destination.Pressure >= 50 * Atmospherics.OneAtmosphere) // Cannot scrub if pressure too high.
{
- return;
+ return false;
}
// Take a gas sample.
- var ratio = MathF.Min(1f, timeDelta * scrubber.TransferRate / tile.Volume);
+ var ratio = MathF.Min(1f, timeDelta * transferRate / tile.Volume);
var removed = tile.RemoveRatio(ratio);
// Nothing left to remove from the tile.
if (MathHelper.CloseToPercent(removed.TotalMoles, 0f))
- return;
+ return false;
- if (scrubber.PumpDirection == ScrubberPumpDirection.Scrubbing)
+ if (mode == ScrubberPumpDirection.Scrubbing)
{
- _atmosphereSystem.ScrubInto(removed, outlet.Air, scrubber.FilterGases);
+ _atmosphereSystem.ScrubInto(removed, destination, filterGases);
// Remix the gases.
_atmosphereSystem.Merge(tile, removed);
}
- else if (scrubber.PumpDirection == ScrubberPumpDirection.Siphoning)
+ else if (mode == ScrubberPumpDirection.Siphoning)
{
- _atmosphereSystem.Merge(outlet.Air, removed);
+ _atmosphereSystem.Merge(destination, removed);
}
+ return true;
}
private void OnAtmosAlarm(EntityUid uid, GasVentScrubberComponent component, AtmosMonitorAlarmEvent args)
diff --git a/Content.Server/Atmos/Portable/PortableScrubberComponent.cs b/Content.Server/Atmos/Portable/PortableScrubberComponent.cs
new file mode 100644
index 0000000000..ac0ffdc226
--- /dev/null
+++ b/Content.Server/Atmos/Portable/PortableScrubberComponent.cs
@@ -0,0 +1,48 @@
+using Content.Shared.Atmos;
+
+namespace Content.Server.Atmos.Portable
+{
+ [RegisterComponent]
+ public sealed class PortableScrubberComponent : Component
+ {
+ ///
+ /// The air inside this machine.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("gasMixture")]
+ public GasMixture Air { get; } = new();
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("port")]
+ public string PortName { get; set; } = "port";
+
+ ///
+ /// Which gases this machine will scrub out.
+ /// Unlike fixed scrubbers controlled by an air alarm,
+ /// this can't be changed in game.
+ ///
+ [DataField("filterGases")]
+ public HashSet FilterGases = new()
+ {
+ Gas.CarbonDioxide,
+ Gas.Plasma,
+ Gas.Tritium,
+ Gas.WaterVapor,
+ Gas.Miasma
+ };
+
+ ///
+ /// Can this scrubber hold more gas?
+ ///
+ public bool Full => Air.Pressure >= MaxPressure;
+
+ ///
+ /// Maximum internal pressure before it refuses to take more.
+ ///
+ [DataField("maxPressure")]
+ public float MaxPressure = 3000f;
+ [DataField("transferRate")]
+ public float TransferRate = 1000f;
+ public bool Enabled = true;
+ }
+}
diff --git a/Content.Server/Atmos/Portable/PortableScrubberSystem.cs b/Content.Server/Atmos/Portable/PortableScrubberSystem.cs
new file mode 100644
index 0000000000..cc96a19b58
--- /dev/null
+++ b/Content.Server/Atmos/Portable/PortableScrubberSystem.cs
@@ -0,0 +1,162 @@
+using Content.Server.Atmos.Piping.Unary.EntitySystems;
+using Content.Shared.Atmos.Piping.Unary.Components;
+using Content.Shared.Atmos.Visuals;
+using Content.Shared.Examine;
+using Content.Shared.Destructible;
+using Content.Server.Atmos.Piping.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Power.Components;
+using Content.Server.NodeContainer;
+using Robust.Shared.Timing;
+using Robust.Server.GameObjects;
+using Content.Server.NodeContainer.Nodes;
+using Content.Server.NodeContainer.NodeGroups;
+using Content.Server.Audio;
+using Content.Server.Administration.Logs;
+using Content.Shared.Database;
+
+
+
+namespace Content.Server.Atmos.Portable
+{
+ public sealed class PortableScrubberSystem : EntitySystem
+ {
+ [Dependency] private readonly GasVentScrubberSystem _scrubberSystem = default!;
+ [Dependency] private readonly GasCanisterSystem _canisterSystem = default!;
+ [Dependency] private readonly GasPortableSystem _gasPortableSystem = default!;
+ [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly TransformSystem _transformSystem = default!;
+ [Dependency] private readonly IAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly AmbientSoundSystem _ambientSound = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnDeviceUpdated);
+ SubscribeLocalEvent(OnAnchorChanged);
+ SubscribeLocalEvent(OnPowerChanged);
+ SubscribeLocalEvent(OnExamined);
+ SubscribeLocalEvent(OnDestroyed);
+ }
+
+ private void OnDeviceUpdated(EntityUid uid, PortableScrubberComponent component, AtmosDeviceUpdateEvent args)
+ {
+ if (!TryComp(uid, out AtmosDeviceComponent? device))
+ return;
+
+ var timeDelta = (float) (_gameTiming.CurTime - device.LastProcess).TotalSeconds;
+
+ if (!component.Enabled)
+ return;
+
+ /// If we are on top of a connector port, empty into it.
+ if (TryComp(uid, out var nodeContainer)
+ && nodeContainer.TryGetNode(component.PortName, out PortablePipeNode? portableNode)
+ && portableNode.ConnectionsEnabled)
+ {
+ _atmosphereSystem.React(component.Air, portableNode);
+ if (portableNode.NodeGroup is PipeNet {NodeCount: > 1} net)
+ _canisterSystem.MixContainerWithPipeNet(component.Air, net.Air);
+ }
+
+ if (component.Full)
+ {
+ UpdateAppearance(uid, true, false);
+ return;
+ }
+
+ var xform = Transform(uid);
+
+ if (xform.GridUid == null)
+ return;
+
+ var position = _transformSystem.GetGridOrMapTilePosition(uid, xform);
+
+ var environment = _atmosphereSystem.GetTileMixture(xform.GridUid, xform.MapUid, position, true);
+
+ var running = Scrub(timeDelta, component, environment);
+
+ UpdateAppearance(uid, false, running);
+ /// We scrub once to see if we can and set the animation
+ if (!running)
+ return;
+ /// widenet
+ foreach (var adjacent in _atmosphereSystem.GetAdjacentTileMixtures(xform.GridUid.Value, position, false, true))
+ {
+ Scrub(timeDelta, component, environment);
+ }
+ }
+
+ ///
+ /// If there is a port under us, let us connect with adjacent atmos pipes.
+ ///
+ private void OnAnchorChanged(EntityUid uid, PortableScrubberComponent component, ref AnchorStateChangedEvent args)
+ {
+ if (!TryComp(uid, out NodeContainerComponent? nodeContainer))
+ return;
+
+ if (!nodeContainer.TryGetNode(component.PortName, out PipeNode? portableNode))
+ return;
+
+ portableNode.ConnectionsEnabled = (args.Anchored && _gasPortableSystem.FindGasPortIn(Transform(uid).GridUid, Transform(uid).Coordinates, out _));
+
+ UpdateDrainingAppearance(uid, portableNode.ConnectionsEnabled);
+ }
+ private void OnPowerChanged(EntityUid uid, PortableScrubberComponent component, PowerChangedEvent args)
+ {
+ UpdateAppearance(uid, component.Full, args.Powered);
+ component.Enabled = args.Powered;
+ }
+
+ ///
+ /// Examining tells you how full it is as a %.
+ ///
+ private void OnExamined(EntityUid uid, PortableScrubberComponent component, ExaminedEvent args)
+ {
+ if (args.IsInDetailsRange)
+ {
+ var percentage = Math.Round(((component.Air.Pressure) / component.MaxPressure) * 100);
+ args.PushMarkup(Loc.GetString("portable-scrubber-fill-level", ("percent", percentage)));
+ }
+ }
+
+ ///
+ /// When this is destroyed, we dump out all the gas inside.
+ ///
+ private void OnDestroyed(EntityUid uid, PortableScrubberComponent component, DestructionEventArgs args)
+ {
+ var environment = _atmosphereSystem.GetContainingMixture(uid, false, true);
+
+ if (environment != null)
+ _atmosphereSystem.Merge(environment, component.Air);
+
+ _adminLogger.Add(LogType.CanisterPurged, LogImpact.Medium, $"Portable scrubber {ToPrettyString(uid):canister} purged its contents of {component.Air:gas} into the environment.");
+ component.Air.Clear();
+ }
+
+ private bool Scrub(float timeDelta, PortableScrubberComponent scrubber, GasMixture? tile)
+ {
+ return _scrubberSystem.Scrub(timeDelta, scrubber.TransferRate, ScrubberPumpDirection.Scrubbing, scrubber.FilterGases, tile, scrubber.Air);
+ }
+
+ private void UpdateAppearance(EntityUid uid, bool isFull, bool isRunning)
+ {
+ if (!TryComp(uid, out var appearance))
+ return;
+
+ _ambientSound.SetAmbience(uid, isRunning);
+
+ appearance.SetData(PortableScrubberVisuals.IsFull, isFull);
+ appearance.SetData(PortableScrubberVisuals.IsRunning, isRunning);
+ }
+
+ private void UpdateDrainingAppearance(EntityUid uid, bool isDraining)
+ {
+ if (!TryComp(uid, out var appearance))
+ return;
+
+ appearance.SetData(PortableScrubberVisuals.IsDraining, isDraining);
+ }
+ }
+}
diff --git a/Content.Server/Entry/IgnoredComponents.cs b/Content.Server/Entry/IgnoredComponents.cs
index daa15a1a0b..82e714c4f6 100644
--- a/Content.Server/Entry/IgnoredComponents.cs
+++ b/Content.Server/Entry/IgnoredComponents.cs
@@ -11,6 +11,7 @@ namespace Content.Server.Entry
"MeleeWeaponArcAnimation",
"EffectVisuals",
"DamageStateVisuals",
+ "PortableScrubberVisuals",
"AnimationsTest",
"ItemStatus",
"VehicleVisuals",
diff --git a/Content.Shared/Atmos/Visuals/PortableScrubberVisuals.cs b/Content.Shared/Atmos/Visuals/PortableScrubberVisuals.cs
new file mode 100644
index 0000000000..20de97da8b
--- /dev/null
+++ b/Content.Shared/Atmos/Visuals/PortableScrubberVisuals.cs
@@ -0,0 +1,15 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Atmos.Visuals
+{
+ [Serializable, NetSerializable]
+ ///
+ /// Used for the visualizer
+ ///
+ public enum PortableScrubberVisuals : byte
+ {
+ IsFull,
+ IsRunning,
+ IsDraining
+ }
+}
diff --git a/Resources/Audio/Ambience/Objects/license.txt b/Resources/Audio/Ambience/Objects/license.txt
index cdac322938..67d622f7a5 100644
--- a/Resources/Audio/Ambience/Objects/license.txt
+++ b/Resources/Audio/Ambience/Objects/license.txt
@@ -5,4 +5,5 @@ 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)
-alarm.ogg - https://github.com/Baystation12/Baystation12/commit/41b11ef289bccfdfa2940480beb9c1e3f50c3b93, fire_alarm.ogg CC-BY-SA-3.0
\ No newline at end of file
+portable_scrubber.ogg - https://freesound.org/people/Beethovenboy/sounds/384335/ - CC0 (by Beethovenboy)
+alarm.ogg - https://github.com/Baystation12/Baystation12/commit/41b11ef289bccfdfa2940480beb9c1e3f50c3b93, fire_alarm.ogg CC-BY-SA-3.0
diff --git a/Resources/Audio/Ambience/Objects/portable_scrubber.ogg b/Resources/Audio/Ambience/Objects/portable_scrubber.ogg
new file mode 100644
index 0000000000..b49149c81c
Binary files /dev/null and b/Resources/Audio/Ambience/Objects/portable_scrubber.ogg differ
diff --git a/Resources/Locale/en-US/atmos/portable-scrubber.ftl b/Resources/Locale/en-US/atmos/portable-scrubber.ftl
new file mode 100644
index 0000000000..c4071b4acc
--- /dev/null
+++ b/Resources/Locale/en-US/atmos/portable-scrubber.ftl
@@ -0,0 +1 @@
+portable-scrubber-fill-level = It's at about [color=yellow]{$percent}%[/color] of its maximum internal pressure.
diff --git a/Resources/Prototypes/Catalog/Research/technologies.yml b/Resources/Prototypes/Catalog/Research/technologies.yml
index 7a4a83ba5a..063a4adc60 100644
--- a/Resources/Prototypes/Catalog/Research/technologies.yml
+++ b/Resources/Prototypes/Catalog/Research/technologies.yml
@@ -265,6 +265,7 @@
- IndustrialEngineering
unlockedRecipes:
- ThermomachineFreezerMachineCircuitBoard
+ - PortableScrubberMachineCircuitBoard
# Avionics Circuitry Technology Tree
diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml
index 620f6613ce..84cf92d267 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml
@@ -156,6 +156,21 @@
graph: ThermomachineBoard
node: heater
+- type: entity
+ id: PortableScrubberMachineCircuitBoard
+ parent: BaseMachineCircuitboard
+ name: portable scrubber machine board
+ description: A PCB for a portable scrubber.
+ components:
+ - type: MachineBoard
+ prototype: PortableScrubber
+ requirements:
+ MatterBin: 3
+ Laser: 2
+ ScanningModule: 1
+ materialRequirements:
+ Cable: 5
+
- type: entity
id: CloningPodMachineCircuitboard
parent: BaseMachineCircuitboard
@@ -396,7 +411,7 @@
materialRequirements:
Glass: 2
Cable: 2
-
+
- type: entity
id: EmitterCircuitboard
parent: BaseMachineCircuitboard
diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
index 30ae9989cd..2851bb44fb 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
@@ -240,6 +240,7 @@
- SMESMachineCircuitboard
- SubstationMachineCircuitboard
- ThermomachineFreezerMachineCircuitBoard
+ - PortableScrubberMachineCircuitBoard
- CloningPodMachineCircuitboard
- MedicalScannerMachineCircuitboard
- CrewMonitoringComputerCircuitboard
diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/portable.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/portable.yml
new file mode 100644
index 0000000000..367aed5c44
--- /dev/null
+++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/portable.yml
@@ -0,0 +1,90 @@
+- type: entity
+ id: PortableScrubber
+ parent: BaseStructureDynamic
+ name: portable scrubber
+ description: It scrubs, portably!
+ components:
+ - type: Transform
+ noRot: true
+ - type: InteractionOutline
+ - type: Physics
+ bodyType: Dynamic
+ - type: Fixtures
+ fixtures:
+ - shape:
+ !type:PhysShapeCircle
+ radius: 0.4
+ mass: 50
+ mask:
+ - MachineMask
+ layer:
+ - MachineLayer
+ - type: Sprite
+ netsync: false
+ sprite: Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi
+ layers:
+ - state: icon
+ map: ["enum.PortableScrubberVisualLayers.IsRunning"]
+ - state: unlit
+ shader: unshaded
+ map: ["enum.PowerDeviceVisualLayers.Powered"]
+ - state: draining
+ shader: unshaded
+ visible: false
+ map: ["enum.PortableScrubberVisualLayers.IsDraining"]
+ - type: Pullable
+ - type: AtmosDevice
+ joinSystem: true
+ - type: PortableScrubber
+ gasMixture:
+ volume: 1250
+ - type: NodeContainer
+ nodes:
+ port:
+ !type:PortablePipeNode
+ nodeGroupID: Pipe
+ rotationsEnabled: false
+ volume: 1
+ - type: ApcPowerReceiver
+ powerLoad: 2000
+ - type: ExtensionCableReceiver
+ - type: Appearance
+ visuals:
+ - type: PowerDeviceVisualizer
+ - type: PortableScrubberVisuals
+ idleState: icon
+ runningState: icon-running
+ readyState: unlit
+ fullState: unlit-full
+ - type: AmbientSound
+ enabled: false
+ volume: -5
+ range: 5
+ sound:
+ path: /Audio/Ambience/Objects/portable_scrubber.ogg
+ - type: Machine
+ board: PortableScrubberMachineCircuitBoard
+ - type: Damageable
+ damageContainer: Inorganic
+ damageModifierSet: Metallic
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 300
+ behaviors:
+ - !type:PlaySoundBehavior
+ sound:
+ path: /Audio/Effects/metalbreak.ogg
+ - !type:SpawnEntitiesBehavior
+ spawn:
+ SheetSteel1:
+ min: 1
+ max: 3
+ SheetGlass1:
+ min: 1
+ max: 3
+ - !type:DoActsBehavior
+ acts: [ "Destruction" ]
+ - type: CollideOnAnchor
+ enable: true
diff --git a/Resources/Prototypes/Recipes/Lathes/electronics.yml b/Resources/Prototypes/Recipes/Lathes/electronics.yml
index 9eccf9ca47..8adff9b177 100644
--- a/Resources/Prototypes/Recipes/Lathes/electronics.yml
+++ b/Resources/Prototypes/Recipes/Lathes/electronics.yml
@@ -54,6 +54,16 @@
Glass: 900
Gold: 50
+- type: latheRecipe
+ id: PortableScrubberMachineCircuitBoard
+ icon: Objects/Misc/module.rsi/id_mod.png
+ result: PortableScrubberMachineCircuitBoard
+ completetime: 4
+ materials:
+ Steel: 150
+ Glass: 900
+ Gold: 50
+
- type: latheRecipe
id: MedicalScannerMachineCircuitboard
icon: Objects/Misc/module.rsi/id_mod.png
@@ -357,7 +367,7 @@
materials:
Steel: 100
Glass: 900
-
+
- type: latheRecipe
id: EmitterCircuitboard
icon: Objects/Misc/module.rsi/id_mod.png
diff --git a/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/draining.png b/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/draining.png
new file mode 100644
index 0000000000..d69c34fe57
Binary files /dev/null and b/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/draining.png differ
diff --git a/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/icon-running.png b/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/icon-running.png
new file mode 100644
index 0000000000..c145e2993e
Binary files /dev/null and b/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/icon-running.png differ
diff --git a/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/icon.png b/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/icon.png
new file mode 100644
index 0000000000..06cf190185
Binary files /dev/null and b/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/icon.png differ
diff --git a/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/meta.json b/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/meta.json
new file mode 100644
index 0000000000..82a7dfe0cb
--- /dev/null
+++ b/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/meta.json
@@ -0,0 +1,32 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/40d89d11ea4a5cb81d61dc1018b46f4e7d32c62a, and modified a bit by Rane",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon"
+ },
+ {
+ "name": "icon-running",
+ "delays": [
+ [
+ 0.2,
+ 0.2
+ ]
+ ]
+ },
+ {
+ "name": "unlit"
+ },
+ {
+ "name": "unlit-full"
+ },
+ {
+ "name": "draining"
+ }
+ ]
+}
diff --git a/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/unlit-full.png b/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/unlit-full.png
new file mode 100644
index 0000000000..b688c5b73a
Binary files /dev/null and b/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/unlit-full.png differ
diff --git a/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/unlit.png b/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/unlit.png
new file mode 100644
index 0000000000..00695432c2
Binary files /dev/null and b/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/unlit.png differ