diff --git a/code/game/gamemodes/cult/runes.dm b/code/game/gamemodes/cult/runes.dm
index 2dd6444094b..77c4075081d 100644
--- a/code/game/gamemodes/cult/runes.dm
+++ b/code/game/gamemodes/cult/runes.dm
@@ -133,6 +133,14 @@ To draw a rune, use a ritual dagger.
/obj/effect/rune/is_cleanable()
return TRUE
+/obj/effect/rune/cleaning_act(mob/user, atom/cleaner, cleanspeed = 5 SECONDS, text_verb = "scrub out", text_description = " with [cleaner].")
+ if(issimulatedturf(loc))
+ var/turf/simulated/T = get_turf(src)
+ T.cleaning_act(user, cleaner, cleanspeed = cleanspeed, text_verb = text_verb, text_description = text_description, text_targetname = name) //Strings are deliberately "A = A" to avoid overrides
+ return
+ else
+ ..()
+
/*
There are a few different procs each rune runs through when a cultist activates it.
diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm
index 0c442fd217e..d8646749a6d 100644
--- a/code/game/machinery/doors/door.dm
+++ b/code/game/machinery/doors/door.dm
@@ -249,27 +249,9 @@
/obj/machinery/door/proc/try_to_crowbar(mob/user, obj/item/I)
return
-/obj/machinery/door/proc/clean_cmag_ooze(obj/item/I, mob/user) //Emags are Engineering's problem, cmags are the janitor's problem
- var/cleaning = FALSE
- if(istype(I, /obj/item/reagent_containers/spray/cleaner))
- var/obj/item/reagent_containers/spray/cleaner/C = I
- if(C.reagents.total_volume >= C.amount_per_transfer_from_this)
- cleaning = TRUE
- else
- return
- if(istype(I, /obj/item/soap))
- cleaning = TRUE
-
- if(!cleaning)
- return
- user.visible_message("[user] starts to clean the ooze off the access panel.", "You start to clean the ooze off the access panel.")
- if(do_after(user, 50, target = src))
- user.visible_message("[user] cleans the ooze off [src].", "You clean the ooze off [src].")
- REMOVE_TRAIT(src, TRAIT_CMAGGED, "clown_emag")
-
/obj/machinery/door/attackby(obj/item/I, mob/user, params)
- if(HAS_TRAIT(src, TRAIT_CMAGGED))
- clean_cmag_ooze(I, user)
+ if(HAS_TRAIT(src, TRAIT_CMAGGED) && I.can_clean()) //If the cmagged door is being hit with cleaning supplies, don't open it, it's being cleaned!
+ return
if(user.a_intent != INTENT_HARM && istype(I, /obj/item/twohanded/fireaxe))
try_to_crowbar(user, I)
diff --git a/code/game/objects/cleaning.dm b/code/game/objects/cleaning.dm
new file mode 100644
index 00000000000..60198d8e2df
--- /dev/null
+++ b/code/game/objects/cleaning.dm
@@ -0,0 +1,53 @@
+#define CMAG_CLEANTIME 50 //The cleaning time for cmagged objects is locked to this, for balance reasons
+
+
+//For handling standard click-to-clean items like soap and mops.
+
+//text_verb and text_description carry strings that, when pieced together by this proc, make the cleaning messages.
+//text_verb is the verb that'll be used for cleaning, i.e. "User begins to [mop] the floor."
+
+//text_description by default appends the name of the cleaning object to the cleaning message, i.e. "User begins to clean the floor[ with soap.]"
+//If text_description is ".", the sentence will end early (so you don't say "Lusty Xenomorph Maid begins to clean the floor with the Lusty Xenomorph Maid")
+
+//text_targetname is the name of the object being interacted with, i.e. "User begins to scrub out the [rune] with the damp rag."
+//Can normally be left alone, but needs to be defined if the thing being cleaned isn't necessarily the thing being clicked on,
+//such as /obj/effect/rune/cleaning_act() bouncing to turf/simulated/cleaning_act().
+
+/atom/proc/cleaning_act(mob/user, atom/cleaner, cleanspeed = 5 SECONDS, text_verb = "clean", text_description = " with [cleaner].", text_targetname = name)
+ var/is_cmagged = FALSE
+
+ if(user.client && (src in user.client.screen)) //You can't clean items you're wearing for technical reasons
+ to_chat(user, "You need to take that [text_targetname] off before cleaning it.")
+ return FALSE
+
+ if(HAS_TRAIT(src, TRAIT_CMAGGED)) //So we don't need a cleaning_act for every cmaggable object
+ is_cmagged = TRUE
+ text_verb = "clean the ooze off"
+ cleanspeed = CMAG_CLEANTIME
+
+ user.visible_message("[user] begins to [text_verb] \the [text_targetname][text_description]")
+ if(!do_after(user, cleanspeed, target = src))
+ return FALSE
+
+ cleaner.post_clean(src, user)
+
+ if(!cleaner.can_clean())
+ return FALSE
+
+ user.visible_message("You [text_verb] \the [text_targetname][text_description]")
+
+ if(is_cmagged) //If we've cleaned a cmagged object
+ REMOVE_TRAIT(src, TRAIT_CMAGGED, "clown_emag")
+ return TRUE
+ else
+ //Generic cleaning functionality
+ var/obj/effect/decal/cleanable/C = locate() in src
+ qdel(C)
+ clean_blood()
+ return TRUE
+
+/atom/proc/can_clean() //For determining if a cleaning object can actually remove decals
+ return FALSE
+
+/atom/proc/post_clean(atom/target, mob/user) //For specific cleaning object behaviors after cleaning, such as mops making floors slippery.
+ return
diff --git a/code/game/objects/effects/decals/cleanable.dm b/code/game/objects/effects/decals/cleanable.dm
index ef10a43ff5e..fc66c80ed1a 100644
--- a/code/game/objects/effects/decals/cleanable.dm
+++ b/code/game/objects/effects/decals/cleanable.dm
@@ -7,6 +7,14 @@
if(mergeable_decal)
return TRUE
+/obj/effect/decal/cleanable/cleaning_act(mob/user, atom/cleaner, cleanspeed = 5 SECONDS, text_verb = "scrub out", text_description = " with [cleaner].")
+ if(issimulatedturf(loc))
+ var/turf/simulated/T = get_turf(src)
+ T.cleaning_act(user, cleaner, cleanspeed = cleanspeed, text_verb = text_verb, text_description = text_description, text_targetname = name) //Strings are deliberately "A = A" to avoid overrides
+ return
+ else
+ ..()
+
//Add "bloodiness" of this blood's type, to the human's shoes
//This is on /cleanable because fuck this ancient mess
/obj/effect/decal/cleanable/blood/Crossed(atom/movable/O)
diff --git a/code/game/objects/items/weapons/mop.dm b/code/game/objects/items/weapons/mop.dm
index b25e31d3794..268e5d9e663 100644
--- a/code/game/objects/items/weapons/mop.dm
+++ b/code/game/objects/items/weapons/mop.dm
@@ -35,33 +35,27 @@
to_chat(user, "You wet [src] in [o].")
playsound(loc, 'sound/effects/slosh.ogg', 25, 1)
-/obj/item/mop/proc/clean(turf/simulated/A)
- if(reagents.has_reagent("water", 1) || reagents.has_reagent("cleaner", 1) || reagents.has_reagent("holywater", 1))
- A.clean_blood()
- for(var/obj/effect/O in A)
- if(O.is_cleanable())
- qdel(O)
- reagents.reaction(A, REAGENT_TOUCH, 10) //10 is the multiplier for the reaction effect. probably needed to wet the floor properly.
- reagents.remove_any(1) //reaction() doesn't use up the reagents
-
/obj/item/mop/afterattack(atom/A, mob/user, proximity)
- if(!proximity) return
-
+ if(!proximity)
+ return
+ if(istype(A, /obj/item/reagent_containers/glass/bucket) || istype(A, /obj/structure/janitorialcart) || istype(A, /obj/structure/mopbucket))
+ return
if(reagents.total_volume < 1)
to_chat(user, "Your mop is dry!")
return
+ A.cleaning_act(user, src, mopspeed, text_verb = "mop", text_description = ".")
- var/turf/simulated/T = get_turf(A)
+/obj/item/mop/can_clean()
+ if(reagents.has_reagent("water", 1) || reagents.has_reagent("cleaner", 1) || reagents.has_reagent("holywater", 1))
+ return TRUE
+ else
+ return FALSE
- if(istype(A, /obj/item/reagent_containers/glass/bucket) || istype(A, /obj/structure/janitorialcart) || istype(A, /obj/structure/mopbucket))
- return
-
- if(istype(T))
- user.visible_message("[user] begins to clean [T] with [src].", "You begin to clean [T] with [src]...")
-
- if(do_after(user, src.mopspeed, target = T))
- to_chat(user, "You finish mopping.")
- clean(T)
+/obj/item/mop/post_clean(atom/target, mob/user)
+ var/turf/T = get_turf(target)
+ if(issimulatedturf(T))
+ reagents.reaction(T, REAGENT_TOUCH, 10) //10 is the multiplier for the reaction effect. probably needed to wet the floor properly.
+ reagents.remove_any(1) //reaction() doesn't use up the reagents
/obj/effect/attackby(obj/item/I, mob/user, params)
if(istype(I, /obj/item/mop) || istype(I, /obj/item/soap))
diff --git a/code/game/objects/items/weapons/soap.dm b/code/game/objects/items/weapons/soap.dm
index b1b9fa2eb5b..e2ef0fb6487 100644
--- a/code/game/objects/items/weapons/soap.dm
+++ b/code/game/objects/items/weapons/soap.dm
@@ -18,43 +18,16 @@
AddComponent(/datum/component/slippery, src, 8 SECONDS, 100, 0, FALSE)
/obj/item/soap/afterattack(atom/target, mob/user, proximity)
- if(!proximity) return
- //I couldn't feasibly fix the overlay bugs caused by cleaning items we are wearing.
- //So this is a workaround. This also makes more sense from an IC standpoint. ~Carn
- if(user.client && (target in user.client.screen))
- to_chat(user, "You need to take that [target.name] off before cleaning it.")
- else if(target == user && user.a_intent == INTENT_GRAB && ishuman(target))
+ if(!proximity)
+ return
+ if(target == user && user.a_intent == INTENT_GRAB && ishuman(target))
var/mob/living/carbon/human/muncher = user
if(muncher && isdrask(muncher))
to_chat(user, "You take a bite of [src]. Delicious!")
playsound(user.loc, 'sound/items/eatfood.ogg', 50, 0)
user.adjust_nutrition(2)
- else if(istype(target, /obj/effect/decal/cleanable) || istype(target, /obj/effect/rune))
- user.visible_message("[user] begins to scrub \the [target.name] out with [src].")
- if(do_after(user, cleanspeed, target = target) && target)
- to_chat(user, "You scrub \the [target.name] out.")
- if(issimulatedturf(target.loc))
- clean_turf(target.loc)
- return
- qdel(target)
- else if(issimulatedturf(target))
- user.visible_message("[user] begins to clean \the [target.name] with [src].")
- if(do_after(user, cleanspeed, target = target))
- to_chat(user, "You clean \the [target.name].")
- clean_turf(target)
- else
- user.visible_message("[user] begins to clean \the [target.name] with [src].")
- if(do_after(user, cleanspeed, target = target))
- to_chat(user, "You clean \the [target.name].")
- var/obj/effect/decal/cleanable/C = locate() in target
- qdel(C)
- target.clean_blood()
-
-/obj/item/soap/proc/clean_turf(turf/simulated/T)
- T.clean_blood()
- for(var/obj/effect/O in T)
- if(O.is_cleanable())
- qdel(O)
+ return
+ target.cleaning_act(user, src, cleanspeed)
/obj/item/soap/attack(mob/target as mob, mob/user as mob)
if(target && user && ishuman(target) && ishuman(user) && !target.stat && !user.stat && user.zone_selected == "mouth" )
@@ -62,6 +35,9 @@
return
..()
+/obj/item/soap/can_clean()
+ return TRUE
+
/obj/item/soap/nanotrasen
desc = "A Nanotrasen brand bar of soap. Smells of plasma."
icon_state = "soapnt"
diff --git a/code/game/turfs/simulated.dm b/code/game/turfs/simulated.dm
index 442ee9c6b44..f63c24dd90e 100644
--- a/code/game/turfs/simulated.dm
+++ b/code/game/turfs/simulated.dm
@@ -16,6 +16,18 @@
/turf/simulated/proc/burn_tile()
return
+/turf/simulated/cleaning_act(mob/user, atom/cleaner, cleanspeed = 50, text_verb = "clean", text_description = " with [cleaner].", text_targetname = name)
+ if(!..())
+ return
+
+ if(!cleaner.can_clean())
+ return
+
+ clean_blood()
+ for(var/obj/effect/O in src)
+ if(O.is_cleanable())
+ qdel(O)
+
/turf/simulated/water_act(volume, temperature, source)
. = ..()
diff --git a/code/modules/detective_work/footprints_and_rag.dm b/code/modules/detective_work/footprints_and_rag.dm
index 39486444e81..92ce57b5936 100644
--- a/code/modules/detective_work/footprints_and_rag.dm
+++ b/code/modules/detective_work/footprints_and_rag.dm
@@ -33,11 +33,10 @@
else
..()
-/obj/item/reagent_containers/glass/rag/afterattack(atom/A as obj|turf|area, mob/user as mob,proximity)
- if(!proximity) return
- if(istype(A) && (src in user))
- user.visible_message("[user] starts to wipe down [A] with [src]!")
- if(do_after(user, wipespeed, target = A))
- user.visible_message("[user] finishes wiping off [A]!")
- A.clean_blood()
- return
+/obj/item/reagent_containers/glass/rag/afterattack(atom/target, mob/user, proximity)
+ if(!proximity || ishuman(target)) //Human check so we don't clean the person we're trying to ether
+ return
+ target.cleaning_act(user, src, wipespeed)
+
+/obj/item/reagent_containers/glass/rag/can_clean()
+ return TRUE
diff --git a/code/modules/mob/living/simple_animal/hostile/alien.dm b/code/modules/mob/living/simple_animal/hostile/alien.dm
index ae3167612dd..423753aa5b2 100644
--- a/code/modules/mob/living/simple_animal/hostile/alien.dm
+++ b/code/modules/mob/living/simple_animal/hostile/alien.dm
@@ -151,14 +151,16 @@
icon_state = "maid"
icon_living = "maid"
icon_dead = "maid_dead"
+ var/cleanspeed = 15
/mob/living/simple_animal/hostile/alien/maid/AttackingTarget()
- if(ismovable(target))
- if(istype(target, /obj/effect/decal/cleanable))
- visible_message("\The [src] cleans up \the [target].")
- qdel(target)
- return TRUE
- var/atom/movable/M = target
- M.clean_blood()
+ if(ishuman(target))
+ var/atom/movable/H = target
+ H.clean_blood()
visible_message("\The [src] polishes \the [target].")
return TRUE
+ target.cleaning_act(src, src, cleanspeed, text_description = ".") //LXM is both the user and the cleaning implement itself. Wow!
+
+/mob/living/simple_animal/hostile/alien/maid/can_clean()
+ return TRUE
+
diff --git a/paradise.dme b/paradise.dme
index 34b286d6e26..297b941f4bf 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -804,6 +804,7 @@
#include "code\game\mecha\working\ripley.dm"
#include "code\game\mecha\working\working.dm"
#include "code\game\objects\buckling.dm"
+#include "code\game\objects\cleaning.dm"
#include "code\game\objects\empulse.dm"
#include "code\game\objects\explosion.dm"
#include "code\game\objects\items.dm"