diff --git a/code/__DEFINES/reagents.dm b/code/__DEFINES/reagents.dm
index e9d417699f2..03bb57f017b 100644
--- a/code/__DEFINES/reagents.dm
+++ b/code/__DEFINES/reagents.dm
@@ -91,6 +91,8 @@
#define REAGENT_IGNORE_STASIS (1<<6)
///This reagent won't be used in most randomized recipes. Meant for reagents that could be synthetized but are normally inaccessible or TOO hard to get.
#define REAGENT_NO_RANDOM_RECIPE (1<<7)
+///Does this reagent clean things?
+#define REAGENT_CLEANS (1<<8)
//Chemical reaction flags, for determining reaction specialties
///Convert into impure/pure on reaction completion
diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm
index 5da614dd693..530b79d938b 100644
--- a/code/_globalvars/bitfields.dm
+++ b/code/_globalvars/bitfields.dm
@@ -322,6 +322,9 @@ DEFINE_BITFIELD(chemical_flags, list(
"REAGENT_SNEAKYNAME" = REAGENT_SNEAKYNAME,
"REAGENT_SPLITRETAINVOL" = REAGENT_SPLITRETAINVOL,
"REAGENT_CAN_BE_SYNTHESIZED" = REAGENT_CAN_BE_SYNTHESIZED,
+ "REAGENT_IGNORE_STASIS" = REAGENT_IGNORE_STASIS,
+ "REAGENT_NO_RANDOM_RECIPE" = REAGENT_NO_RANDOM_RECIPE,
+ "REAGENT_CLEANS" = REAGENT_CLEANS,
))
DEFINE_BITFIELD(reaction_flags, list(
diff --git a/code/datums/looping_sounds/_looping_sound.dm b/code/datums/looping_sounds/_looping_sound.dm
index b893ef6a6fa..d71a05d4312 100644
--- a/code/datums/looping_sounds/_looping_sound.dm
+++ b/code/datums/looping_sounds/_looping_sound.dm
@@ -1,6 +1,4 @@
/*
- output_atoms (list of atoms) The destination(s) for the sounds
-
mid_sounds (list or soundfile) Since this can be either a list or a single soundfile you can have random sounds. May contain further lists but must contain a soundfile at the end.
mid_length (num) The length to wait between playing mid_sounds
@@ -60,7 +58,7 @@
return
on_start()
-/datum/looping_sound/proc/stop(null_parent)
+/datum/looping_sound/proc/stop(null_parent = FALSE)
if(null_parent)
set_parent(null)
if(!timerid)
@@ -114,6 +112,9 @@
if(parent)
RegisterSignal(parent, COMSIG_PARENT_QDELETING, .proc/handle_parent_del)
+/datum/looping_sound/proc/is_active()
+ return !!timerid
+
/datum/looping_sound/proc/handle_parent_del(datum/source)
SIGNAL_HANDLER
set_parent(null)
diff --git a/code/datums/looping_sounds/cyborg.dm b/code/datums/looping_sounds/cyborg.dm
new file mode 100644
index 00000000000..499e61757c6
--- /dev/null
+++ b/code/datums/looping_sounds/cyborg.dm
@@ -0,0 +1,10 @@
+/datum/looping_sound/wash
+ mid_sounds = list('sound/creatures/cyborg/wash1.ogg' = 1, 'sound/creatures/cyborg/wash2.ogg' = 1)
+ mid_length = 1.5 SECONDS // This makes them overlap slightly, which works out well for masking the fade in/out
+ start_volume = 100
+ start_sound = 'sound/creatures/cyborg/wash_start.ogg'
+ start_length = 3.6 SECONDS // again, slightly shorter then the real time of 4 seconds, will make the transition to midsounds more seemless
+ end_volume = 100
+ end_sound = 'sound/creatures/cyborg/wash_end.ogg'
+ vary = TRUE
+ extra_range = 5
diff --git a/code/game/objects/items/mop.dm b/code/game/objects/items/mop.dm
index 4f41f59adf5..8d7af51a9a2 100644
--- a/code/game/objects/items/mop.dm
+++ b/code/game/objects/items/mop.dm
@@ -39,7 +39,7 @@
//SKYRAT EDIT END
/obj/item/mop/proc/clean(turf/A, mob/living/cleaner)
- if(reagents.has_reagent(/datum/reagent/water, 1) || reagents.has_reagent(/datum/reagent/water/holywater, 1) || reagents.has_reagent(/datum/reagent/consumable/ethanol/vodka, 1) || reagents.has_reagent(/datum/reagent/space_cleaner, 1))
+ if(reagents.has_chemical_flag(REAGENT_CLEANS, 1))
// If there's a cleaner with a mind, let's gain some experience!
if(cleaner?.mind)
var/total_experience_gain = 0
diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm
index f57bb0199e8..bd6e97976bd 100644
--- a/code/modules/mob/living/silicon/robot/robot.dm
+++ b/code/modules/mob/living/silicon/robot/robot.dm
@@ -768,11 +768,6 @@
for(var/trait in model.model_traits)
ADD_TRAIT(src, trait, MODEL_TRAIT)
- if(model.clean_on_move)
- AddElement(/datum/element/cleaning)
- else
- RemoveElement(/datum/element/cleaning)
-
hat_offset = model.hat_offset
INVOKE_ASYNC(src, .proc/updatename)
diff --git a/code/modules/mob/living/silicon/robot/robot_model.dm b/code/modules/mob/living/silicon/robot/robot_model.dm
index 14a8c434f55..dfe269a26f4 100644
--- a/code/modules/mob/living/silicon/robot/robot_model.dm
+++ b/code/modules/mob/living/silicon/robot/robot_model.dm
@@ -37,8 +37,6 @@
var/list/model_traits = null
///List of radio channels added to the cyborg
var/list/radio_channels = list()
- ///Do we clean when we move
- var/clean_on_move = FALSE
///Whether the borg loses tool slots with damage.
var/breakable_modules = TRUE
///Whether swapping to this configuration should lockcharge the borg
@@ -361,14 +359,212 @@
/obj/item/reagent_containers/glass/bucket,
/obj/item/paint/paint_remover,
/obj/item/lightreplacer/cyborg,
- /obj/item/holosign_creator/janibarrier,
+ /obj/item/holosign_creator,
/obj/item/reagent_containers/spray/cyborg_drying)
radio_channels = list(RADIO_CHANNEL_SERVICE)
emag_modules = list(/obj/item/reagent_containers/spray/cyborg_lube)
cyborg_base_icon = "janitor"
model_select_icon = "janitor"
hat_offset = -5
- clean_on_move = TRUE
+ /// Weakref to the wash toggle action we own
+ var/datum/weakref/wash_toggle_ref
+
+/obj/item/robot_model/janitor/be_transformed_to(obj/item/robot_model/old_model, forced = FALSE)
+ . = ..()
+ if(!.)
+ return
+ var/datum/action/wash_toggle = new /datum/action/toggle_buffer(loc)
+ wash_toggle.Grant(loc)
+ wash_toggle_ref = WEAKREF(wash_toggle)
+
+/obj/item/robot_model/janitor/Destroy()
+ QDEL_NULL(wash_toggle_ref)
+ return ..()
+
+/datum/action/toggle_buffer
+ name = "Activate Auto-Wash"
+ desc = "Trade speed and water for a clean floor."
+ icon_icon = 'icons/mob/actions/actions_silicon.dmi'
+ button_icon_state = "activate_wash"
+ var/static/datum/callback/allow_buffer_activate
+ var/block_buffer_change = FALSE
+ var/buffer_on = FALSE
+ ///The bucket we draw water from
+ var/datum/weakref/bucket_ref
+ ///Our looping sound
+ var/datum/looping_sound/wash/wash_audio
+ ///Toggle cooldown to prevent sound spam
+ COOLDOWN_DECLARE(toggle_cooldown)
+
+/datum/action/toggle_buffer/New(Target)
+ if(!allow_buffer_activate)
+ allow_buffer_activate = CALLBACK(src, .proc/allow_buffer_activate)
+ return ..()
+
+/datum/action/toggle_buffer/Destroy()
+ if(buffer_on)
+ turn_off_wash()
+ QDEL_NULL(wash_audio)
+ return ..()
+
+/datum/action/toggle_buffer/Grant(mob/M)
+ . = ..()
+ wash_audio = new(owner)
+
+/datum/action/toggle_buffer/IsAvailable()
+ if(!istype(owner, /mob/living/silicon/robot))
+ return FALSE
+ return ..()
+
+/datum/action/toggle_buffer/Trigger(trigger_flags)
+ . = ..()
+ if(!.)
+ return
+ var/mob/living/silicon/robot/robot_owner = owner
+
+ block_buffer_change = DOING_INTERACTION(owner, "auto_wash_toggle")
+ if(block_buffer_change)
+ return FALSE
+
+ var/obj/item/reagent_containers/glass/bucket/our_bucket = locate(/obj/item/reagent_containers/glass/bucket) in robot_owner.model.modules
+ bucket_ref = WEAKREF(our_bucket)
+
+ if(!buffer_on)
+ if(!COOLDOWN_FINISHED(src, toggle_cooldown))
+ robot_owner.balloon_alert(robot_owner, "auto-wash refreshing, please hold...")
+ return FALSE
+ COOLDOWN_START(src, toggle_cooldown, 4 SECONDS)
+ if(!allow_buffer_activate())
+ return FALSE
+
+ robot_owner.balloon_alert(robot_owner, "activating auto-wash...")
+ // Start the sound. it'll just last the 4 seconds it takes for us to rev up
+ wash_audio.start()
+ // We're just gonna shake the borg a bit. Not a ton, but just enough that it feels like the audio makes sense
+ var/base_x = robot_owner.base_pixel_x
+ var/base_y = robot_owner.base_pixel_y
+ animate(robot_owner, pixel_x = base_x, pixel_y = base_y, time = 1, loop = -1)
+ for(var/i in 1 to 17) //Startup rumble
+ var/x_offset = base_x + rand(-1, 1)
+ var/y_offset = base_y + rand(-1, 1)
+ animate(pixel_x = x_offset, pixel_y = y_offset, time = 1)
+
+ if(!do_after(robot_owner, 4 SECONDS, interaction_key = "auto_wash_toggle", extra_checks = allow_buffer_activate))
+ wash_audio.stop() // Coward
+ animate(robot_owner, pixel_x = base_x, pixel_y = base_y, time = 1)
+ return FALSE
+ else
+ if(!COOLDOWN_FINISHED(src, toggle_cooldown))
+ robot_owner.balloon_alert(robot_owner, "auto-wash deactivating, please hold...")
+ return FALSE
+ robot_owner.balloon_alert(robot_owner, "de-activating auto-wash...")
+
+ toggle_wash()
+
+/// Toggle our wash mode
+/datum/action/toggle_buffer/proc/toggle_wash()
+ if(buffer_on)
+ deactivate_wash()
+ else
+ activate_wash()
+
+/// Activate the buffer, comes with a nice animation that loops while it's on
+/datum/action/toggle_buffer/proc/activate_wash()
+ var/mob/living/silicon/robot/robot_owner = owner
+ buffer_on = TRUE
+ // Slow em down a bunch
+ robot_owner.add_movespeed_modifier(/datum/movespeed_modifier/auto_wash)
+ RegisterSignal(robot_owner, COMSIG_MOVABLE_MOVED, .proc/clean)
+ //This is basically just about adding a shake to the borg, effect should look ilke an engine's running
+ var/base_x = robot_owner.base_pixel_x
+ var/base_y = robot_owner.base_pixel_y
+ robot_owner.pixel_x = base_x + rand(-7, 7)
+ robot_owner.pixel_y = base_y + rand(-7, 7)
+ //Larger shake with more changes to start out, feels like "Revving"
+ animate(robot_owner, pixel_x = base_x, pixel_y = base_y, time = 1, loop = -1)
+ for(var/i in 1 to 100)
+ var/x_offset = base_x + rand(-2, 2)
+ var/y_offset = base_y + rand(-2, 2)
+ animate(pixel_x = x_offset, pixel_y = y_offset, time = 1)
+ if(!wash_audio.is_active())
+ wash_audio.start()
+ clean()
+ UpdateButtonIcon()
+
+/// Start the process of disabling the buffer. Plays some effects, waits a bit, then finishes
+/datum/action/toggle_buffer/proc/deactivate_wash()
+ var/mob/living/silicon/robot/robot_owner = owner
+ var/time_left = timeleft(wash_audio.timerid) // We delay by the timer of our wash cause well, we want to hear the ramp down
+ var/finished_by = time_left + 2.6 SECONDS
+ // Need to ensure that people don't spawn the deactivate button
+ COOLDOWN_START(src, toggle_cooldown, finished_by)
+ // Diable the cleaning, we're revving down
+ UnregisterSignal(robot_owner, COMSIG_MOVABLE_MOVED)
+ // Do the rumble animation till we're all finished
+ var/base_x = robot_owner.base_pixel_x
+ var/base_y = robot_owner.base_pixel_y
+ animate(robot_owner, pixel_x = base_x, pixel_y = base_y, time = 1)
+ for(var/i in 1 to finished_by - 0.1 SECONDS) //We rumble until we're finished making noise
+ var/x_offset = base_x + rand(-1, 1)
+ var/y_offset = base_y + rand(-1, 1)
+ animate(pixel_x = x_offset, pixel_y = y_offset, time = 1)
+ // Reset our animations
+ animate(pixel_x = base_x, pixel_y = base_y, time = 2)
+ addtimer(CALLBACK(wash_audio, /datum/looping_sound/proc/stop), time_left)
+ addtimer(CALLBACK(src, .proc/turn_off_wash), finished_by)
+
+/// Called by [deactivate_wash] on a timer to allow noises and animation to play out.
+/// Finally disables the buffer. Doesn't do everything mind, just the stuff that we wanted to delay
+/datum/action/toggle_buffer/proc/turn_off_wash()
+ var/mob/living/silicon/robot/robot_owner = owner
+ buffer_on = FALSE
+ robot_owner.remove_movespeed_modifier(/datum/movespeed_modifier/auto_wash)
+ UpdateButtonIcon()
+
+/// Should we keep trying to activate our buffer, or did you fuck it up somehow
+/datum/action/toggle_buffer/proc/allow_buffer_activate()
+ var/mob/living/silicon/robot/robot_owner = owner
+ if(block_buffer_change)
+ robot_owner.balloon_alert(robot_owner, "activation cancelled!")
+ return FALSE
+
+ var/obj/item/reagent_containers/glass/bucket/our_bucket = bucket_ref?.resolve()
+ if(!buffer_on && our_bucket?.reagents?.total_volume < 0.1)
+ robot_owner.balloon_alert(robot_owner, "bucket is empty!")
+ return FALSE
+ return TRUE
+
+/// Call this to attempt to actually clean the turf underneath us
+/datum/action/toggle_buffer/proc/clean()
+ SIGNAL_HANDLER
+ var/mob/living/silicon/robot/robot_owner = owner
+
+ var/obj/item/reagent_containers/glass/bucket/our_bucket = bucket_ref?.resolve()
+ var/datum/reagents/reagents = our_bucket?.reagents
+
+ if(!reagents || reagents.total_volume < 0.1)
+ robot_owner.balloon_alert(robot_owner, "bucket is empty, de-activating...")
+ deactivate_wash()
+ return
+
+ var/turf/our_turf = get_turf(robot_owner)
+
+ if(reagents.has_chemical_flag(REAGENT_CLEANS, 1))
+ our_turf.wash(CLEAN_SCRUB)
+
+ reagents.expose(our_turf, TOUCH, 10)
+ // We use more water doing this then mopping
+ reagents.remove_any(2) //reaction() doesn't use up the reagents
+
+/datum/action/toggle_buffer/UpdateButtonIcon(status_only = FALSE, force = FALSE)
+ if(buffer_on)
+ name = "De-Activate Auto-Wash"
+ button_icon_state = "deactivate_wash"
+ else
+ name = "Activate Auto-Wash"
+ button_icon_state = "activate_wash"
+
+ return ..()
/obj/item/reagent_containers/spray/cyborg_drying
name = "drying agent spray"
diff --git a/code/modules/movespeed/modifiers/mobs.dm b/code/modules/movespeed/modifiers/mobs.dm
index 4afd7946752..1c373b4de40 100644
--- a/code/modules/movespeed/modifiers/mobs.dm
+++ b/code/modules/movespeed/modifiers/mobs.dm
@@ -126,3 +126,6 @@
/datum/movespeed_modifier/morph_disguised
multiplicative_slowdown = 1
+
+/datum/movespeed_modifier/auto_wash
+ multiplicative_slowdown = 3
diff --git a/code/modules/reagents/chemistry/chem_wiki_render.dm b/code/modules/reagents/chemistry/chem_wiki_render.dm
index c56aa592eb8..f060cf9d00d 100644
--- a/code/modules/reagents/chemistry/chem_wiki_render.dm
+++ b/code/modules/reagents/chemistry/chem_wiki_render.dm
@@ -194,5 +194,8 @@
if(reagent.chemical_flags & REAGENT_DEAD_PROCESS)
outstring += "\n
Works on the dead"
+ if(reagent.chemical_flags & REAGENT_CLEANS)
+ outstring += "\n
Sanitizes well"
+
outstring += "\n|-"
return outstring
diff --git a/code/modules/reagents/chemistry/holder.dm b/code/modules/reagents/chemistry/holder.dm
index d126d69ddf2..c7ad9e12402 100644
--- a/code/modules/reagents/chemistry/holder.dm
+++ b/code/modules/reagents/chemistry/holder.dm
@@ -428,6 +428,21 @@
return holder_reagent
return FALSE
+/**
+ * Check if this holder contains a reagent with a chemical_flags containing this flag
+ * Reagent takes the bitflag to search for
+ * Amount checks for having a specific amount of reagents matching that chemical
+ */
+/datum/reagents/proc/has_chemical_flag(chemical_flag, amount = 0)
+ var/found_amount = 0
+ var/list/cached_reagents = reagent_list
+ for(var/datum/reagent/holder_reagent as anything in cached_reagents)
+ if (holder_reagent.chemical_flags & chemical_flag)
+ found_amount += holder_reagent.volume
+ if(found_amount >= amount)
+ return TRUE
+ return FALSE
+
/**
* Transfer some stuff from this holder to a target object
diff --git a/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm b/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm
index 46f843862d5..40a212708bd 100644
--- a/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm
@@ -290,7 +290,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_desc = "The glass contain wodka. Xynta."
shot_glass_icon_state = "shotglassclear"
ph = 8.1
- chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
+ chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_CLEANS //Very high proof
/datum/reagent/consumable/ethanol/bilk
name = "Bilk"
diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm
index ad81cdb964e..ee61e0ac2ce 100644
--- a/code/modules/reagents/chemistry/reagents/other_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm
@@ -156,7 +156,7 @@
glass_name = "glass of water"
glass_desc = "The father of all refreshments."
shot_glass_icon_state = "shotglassclear"
- chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
+ chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_CLEANS
evaporates = TRUE //SKYRAT EDIT ADDITION
/*
@@ -242,7 +242,7 @@
glass_desc = "A glass of holy water."
self_consuming = TRUE //divine intervention won't be limited by the lack of a liver
ph = 7.5 //God is alkaline
- chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
+ chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_CLEANS
// Holy water. Mostly the same as water, it also heals the plant a little with the power of the spirits. Also ALSO increases instability.
/datum/reagent/water/holywater/on_hydroponics_apply(obj/item/seeds/myseed, datum/reagents/chems, obj/machinery/hydroponics/mytray)
@@ -1157,7 +1157,7 @@
penetrates_skin = NONE
var/clean_types = CLEAN_WASH
ph = 5.5
- chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
+ chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_CLEANS
/datum/reagent/space_cleaner/expose_obj(obj/exposed_obj, reac_volume)
. = ..()
diff --git a/icons/mob/actions/actions_silicon.dmi b/icons/mob/actions/actions_silicon.dmi
index 8a03ef91cab..7be634e9f6c 100644
Binary files a/icons/mob/actions/actions_silicon.dmi and b/icons/mob/actions/actions_silicon.dmi differ
diff --git a/sound/creatures/cyborg/wash1.ogg b/sound/creatures/cyborg/wash1.ogg
new file mode 100644
index 00000000000..8bbd1d54e5c
Binary files /dev/null and b/sound/creatures/cyborg/wash1.ogg differ
diff --git a/sound/creatures/cyborg/wash2.ogg b/sound/creatures/cyborg/wash2.ogg
new file mode 100644
index 00000000000..6e950e4132b
Binary files /dev/null and b/sound/creatures/cyborg/wash2.ogg differ
diff --git a/sound/creatures/cyborg/wash_end.ogg b/sound/creatures/cyborg/wash_end.ogg
new file mode 100644
index 00000000000..57b2ce1838f
Binary files /dev/null and b/sound/creatures/cyborg/wash_end.ogg differ
diff --git a/sound/creatures/cyborg/wash_start.ogg b/sound/creatures/cyborg/wash_start.ogg
new file mode 100644
index 00000000000..c6faf4126d1
Binary files /dev/null and b/sound/creatures/cyborg/wash_start.ogg differ
diff --git a/tgstation.dme b/tgstation.dme
index 099cf563b15..7dc2c782be1 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -1032,6 +1032,7 @@
#include "code\datums\keybinding\robot.dm"
#include "code\datums\looping_sounds\_looping_sound.dm"
#include "code\datums\looping_sounds\acid.dm"
+#include "code\datums\looping_sounds\cyborg.dm"
#include "code\datums\looping_sounds\item_sounds.dm"
#include "code\datums\looping_sounds\machinery_sounds.dm"
#include "code\datums\looping_sounds\weather.dm"