Toilet and urinal updates (flushing dead fish included) (#83490)

## About The Pull Request

I split the water closets file into a folder for all the different
structures and also did a few other things
1. Remakes https://github.com/tgstation/tgstation/pull/81914
2. Reworks toilets, it’s now Alt Click to open/close the cover, left
click takes things out of the toilet and cistern, right click flushes
3. You can now put dead fish in the toilet to flush it
4. Adds context tips to toilets. 

## Why It's Good For The Game

When I was a 7 year old boy, I had a friend goldfish named Jeremy. He
was my greatest friend until one day when my brother told me it’s for
babies and flushed him alive down the toilet, and I’ve never seen it
again.

Closes https://github.com/tgstation/tgstation/issues/81903
Lets you flush down people’s fish in the toilet, and re-experience their
loss.

This was inspired by my lawyer fish PR, to add things more on the
non-fishing side of fish.

## Changelog

🆑
add: You can now flush toilets. You can also put fish in the toilet. And
flush them.
fix: Urinals can no longer be used to delete items.
fix: Urinals no longer grant infinite urinal cakes. 
/🆑

---------

Co-authored-by: Jacquerel <hnevard@gmail.com>
Co-authored-by: GoldenAlpharex <58045821+GoldenAlpharex@users.noreply.github.com>
This commit is contained in:
John Willard
2024-06-01 17:49:33 +00:00
committed by GitHub
co-authored by Jacquerel GoldenAlpharex
parent 432b88a05b
commit 7d6adad613
14 changed files with 1020 additions and 882 deletions
@@ -0,0 +1,301 @@
/obj/structure/sink
name = "sink"
icon = 'icons/obj/watercloset.dmi'
icon_state = "sink"
desc = "A sink used for washing one's hands and face. Passively reclaims water over time."
anchored = TRUE
layer = ABOVE_OBJ_LAYER
pixel_z = 1
///Something's being washed at the moment
var/busy = FALSE
///What kind of reagent is produced by this sink by default? (We now have actual plumbing, Arcane, August 2020)
var/dispensedreagent = /datum/reagent/water
///Material to drop when broken or deconstructed.
var/buildstacktype = /obj/item/stack/sheet/iron
///Number of sheets of material to drop when broken or deconstructed.
var/buildstackamount = 1
///Does the sink have a water recycler to recollect it's water supply?
var/has_water_reclaimer = TRUE
///Units of water to reclaim per second
var/reclaim_rate = 0.5
///Amount of shift the pixel for placement
var/pixel_shift = 14
MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sink, (-14))
/obj/structure/sink/Initialize(mapload, ndir = 0, has_water_reclaimer = null)
. = ..()
if(ndir)
dir = ndir
if(has_water_reclaimer != null)
src.has_water_reclaimer = has_water_reclaimer
switch(dir)
if(NORTH)
pixel_x = 0
pixel_y = -pixel_shift
if(SOUTH)
pixel_x = 0
pixel_y = pixel_shift
if(EAST)
pixel_x = -pixel_shift
pixel_y = 0
if(WEST)
pixel_x = pixel_shift
pixel_y = 0
create_reagents(100, NO_REACT)
if(src.has_water_reclaimer)
reagents.add_reagent(dispensedreagent, 100)
AddComponent(/datum/component/plumbing/simple_demand, extend_pipe_to_edge = TRUE)
/obj/structure/sink/examine(mob/user)
. = ..()
if(has_water_reclaimer)
. += span_notice("A water recycler is installed. It looks like you could pry it out.")
. += span_notice("[reagents.total_volume]/[reagents.maximum_volume] liquids remaining.")
/obj/structure/sink/attack_hand(mob/living/user, list/modifiers)
. = ..()
if(.)
return
if(!user || !istype(user))
return
if(!iscarbon(user))
return
if(!Adjacent(user))
return
if(reagents.total_volume < 5)
to_chat(user, span_warning("The sink has no more contents left!"))
return
if(busy)
to_chat(user, span_warning("Someone's already washing here!"))
return
var/selected_area = user.parse_zone_with_bodypart(user.zone_selected)
var/washing_face = 0
if(selected_area in list(BODY_ZONE_HEAD, BODY_ZONE_PRECISE_MOUTH, BODY_ZONE_PRECISE_EYES))
washing_face = 1
user.visible_message(span_notice("[user] starts washing [user.p_their()] [washing_face ? "face" : "hands"]..."), \
span_notice("You start washing your [washing_face ? "face" : "hands"]..."))
busy = TRUE
if(!do_after(user, 4 SECONDS, target = src))
busy = FALSE
return
busy = FALSE
reagents.remove_all(5)
reagents.expose(user, TOUCH, 5 / max(reagents.total_volume, 5))
begin_reclamation()
if(washing_face)
SEND_SIGNAL(user, COMSIG_COMPONENT_CLEAN_FACE_ACT, CLEAN_WASH)
else if(ishuman(user))
var/mob/living/carbon/human/human_user = user
if(!human_user.wash_hands(CLEAN_WASH))
to_chat(user, span_warning("Your hands are covered by something!"))
return
else
user.wash(CLEAN_WASH)
user.visible_message(span_notice("[user] washes [user.p_their()] [washing_face ? "face" : "hands"] using [src]."), \
span_notice("You wash your [washing_face ? "face" : "hands"] using [src]."))
/obj/structure/sink/attackby(obj/item/O, mob/living/user, params)
if(busy)
to_chat(user, span_warning("Someone's already washing here!"))
return
if(is_reagent_container(O))
var/obj/item/reagent_containers/RG = O
if(reagents.total_volume <= 0)
to_chat(user, span_notice("\The [src] is dry."))
return FALSE
if(RG.is_refillable())
if(!RG.reagents.holder_full())
reagents.trans_to(RG, RG.amount_per_transfer_from_this, transferred_by = user)
begin_reclamation()
to_chat(user, span_notice("You fill [RG] from [src]."))
return TRUE
to_chat(user, span_notice("\The [RG] is full."))
return FALSE
if(istype(O, /obj/item/melee/baton/security))
var/obj/item/melee/baton/security/baton = O
if(baton.cell?.charge && baton.active)
flick("baton_active", src)
user.Paralyze(baton.knockdown_time)
user.set_stutter(baton.knockdown_time)
baton.cell.use(baton.cell_hit_cost)
user.visible_message(span_warning("[user] shocks [user.p_them()]self while attempting to wash the active [baton.name]!"), \
span_userdanger("You unwisely attempt to wash [baton] while it's still on."))
playsound(src, baton.on_stun_sound, 50, TRUE)
return
if(istype(O, /obj/item/mop))
if(reagents.total_volume <= 0)
to_chat(user, span_notice("\The [src] is dry."))
return FALSE
reagents.trans_to(O, 5, transferred_by = user)
begin_reclamation()
to_chat(user, span_notice("You wet [O] in [src]."))
playsound(loc, 'sound/effects/slosh.ogg', 25, TRUE)
return
if(O.tool_behaviour == TOOL_WRENCH)
O.play_tool_sound(src)
deconstruct()
return
if(O.tool_behaviour == TOOL_CROWBAR)
if(!has_water_reclaimer)
to_chat(user, span_warning("There isn't a water recycler to remove."))
return
O.play_tool_sound(src)
has_water_reclaimer = FALSE
new/obj/item/stock_parts/water_recycler(get_turf(loc))
to_chat(user, span_notice("You remove the water reclaimer from [src]"))
return
if(istype(O, /obj/item/stack/medical/gauze))
var/obj/item/stack/medical/gauze/G = O
new /obj/item/reagent_containers/cup/rag(src.loc)
to_chat(user, span_notice("You tear off a strip of gauze and make a rag."))
G.use(1)
return
if(istype(O, /obj/item/stack/sheet/cloth))
var/obj/item/stack/sheet/cloth/cloth = O
new /obj/item/reagent_containers/cup/rag(loc)
to_chat(user, span_notice("You tear off a strip of cloth and make a rag."))
cloth.use(1)
return
if(istype(O, /obj/item/stack/ore/glass))
new /obj/item/stack/sheet/sandblock(loc)
to_chat(user, span_notice("You wet the sand in the sink and form it into a block."))
O.use(1)
return
if(istype(O, /obj/item/stock_parts/water_recycler))
if(has_water_reclaimer)
to_chat(user, span_warning("There is already has a water recycler installed."))
return
playsound(src, 'sound/machines/click.ogg', 20, TRUE)
qdel(O)
has_water_reclaimer = TRUE
begin_reclamation()
return
if(istype(O, /obj/item/storage/fancy/pickles_jar))
if(O.contents.len)
to_chat(user, span_notice("Looks like there's something left in the jar"))
return
new /obj/item/reagent_containers/cup/beaker/large(loc)
to_chat(user, span_notice("You washed the jar, ridding it of the brine."))
qdel(O)
return
if(!istype(O))
return
if(O.item_flags & ABSTRACT) //Abstract items like grabs won't wash. No-drop items will though because it's still technically an item in your hand.
return
if(!user.combat_mode)
to_chat(user, span_notice("You start washing [O]..."))
busy = TRUE
if(!do_after(user, 4 SECONDS, target = src))
busy = FALSE
return 1
busy = FALSE
O.wash(CLEAN_WASH)
reagents.expose(O, TOUCH, 5 / max(reagents.total_volume, 5))
user.visible_message(span_notice("[user] washes [O] using [src]."), \
span_notice("You wash [O] using [src]."))
return 1
else
return ..()
/obj/structure/sink/atom_deconstruct(dissambled = TRUE)
drop_materials()
if(has_water_reclaimer)
new /obj/item/stock_parts/water_recycler(drop_location())
/obj/structure/sink/process(seconds_per_tick)
// Water reclamation complete?
if(!has_water_reclaimer || reagents.total_volume >= reagents.maximum_volume)
return PROCESS_KILL
reagents.add_reagent(dispensedreagent, reclaim_rate * seconds_per_tick)
/obj/structure/sink/proc/drop_materials()
if(buildstacktype)
new buildstacktype(loc,buildstackamount)
else
for(var/i in custom_materials)
var/datum/material/M = i
new M.sheet_type(loc, FLOOR(custom_materials[M] / SHEET_MATERIAL_AMOUNT, 1))
/obj/structure/sink/proc/begin_reclamation()
START_PROCESSING(SSplumbing, src)
/obj/structure/sink/kitchen
name = "kitchen sink"
icon_state = "sink_alt"
pixel_z = 4
pixel_shift = 16
MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sink/kitchen, (-16))
/obj/structure/sink/greyscale
icon_state = "sink_greyscale"
material_flags = MATERIAL_EFFECTS | MATERIAL_ADD_PREFIX | MATERIAL_COLOR | MATERIAL_AFFECT_STATISTICS
buildstacktype = null
/obj/structure/sinkframe
name = "sink frame"
icon = 'icons/obj/watercloset.dmi'
icon_state = "sink_frame"
desc = "A sink frame, that needs a water recycler to finish construction."
anchored = FALSE
material_flags = MATERIAL_EFFECTS | MATERIAL_ADD_PREFIX | MATERIAL_COLOR | MATERIAL_AFFECT_STATISTICS
/obj/structure/sinkframe/Initialize(mapload)
. = ..()
AddComponent(/datum/component/simple_rotation)
/obj/structure/sinkframe/attackby(obj/item/tool, mob/living/user, params)
if(istype(tool, /obj/item/stock_parts/water_recycler))
qdel(tool)
var/obj/structure/sink/greyscale/new_sink = new(loc, REVERSE_DIR(dir), TRUE)
new_sink.set_custom_materials(custom_materials)
qdel(src)
playsound(new_sink, 'sound/machines/click.ogg', 20, TRUE)
return
return ..()
/obj/structure/sinkframe/wrench_act(mob/living/user, obj/item/tool)
. = ..()
tool.play_tool_sound(src)
var/obj/structure/sink/greyscale/new_sink = new(loc, REVERSE_DIR(dir), FALSE)
new_sink.set_custom_materials(custom_materials)
qdel(src)
return TRUE
/obj/structure/sinkframe/wrench_act_secondary(mob/living/user, obj/item/tool)
. = ..()
tool.play_tool_sound(src)
deconstruct()
return TRUE
/obj/structure/sinkframe/atom_deconstruct(dissambled = TRUE)
drop_materials()
/obj/structure/sinkframe/proc/drop_materials()
for(var/datum/material/material as anything in custom_materials)
new material.sheet_type(loc, FLOOR(custom_materials[material] / SHEET_MATERIAL_AMOUNT, 1))
@@ -0,0 +1,271 @@
/obj/structure/toilet
name = "toilet"
desc = "The HT-451, a torque rotation-based, waste disposal unit for small matter. This one seems remarkably clean."
icon = 'icons/obj/watercloset.dmi'
icon_state = "toilet00" //The first number represents if the toilet lid is up, the second is if the cistern is open.
base_icon_state = "toilet"
density = FALSE
anchored = TRUE
///Boolean if whether the toilet is currently flushing.
var/flushing = FALSE
///Boolean if the toilet seat is up.
var/cover_open = FALSE
///Boolean if the cistern is up, allowing items to be put in/out.
var/cistern_open = FALSE
///The combined weight of all items in the cistern put together.
var/w_items = 0
///Reference to the mob being given a swirlie.
var/mob/living/swirlie
///The type of material used to build the toilet.
var/buildstacktype = /obj/item/stack/sheet/iron
///How much of the buildstacktype is needed to construct the toilet.
var/buildstackamount = 1
///Lazylist of items in the cistern.
var/list/cistern_items
///Lazylist of fish in the toilet, not to be mixed with the items in the cistern. Max of 3
var/list/fishes
///Static toilet water overlay given to toilets that are facing a direction we can see the water in.
var/static/mutable_appearance/toilet_water_overlay
/obj/structure/toilet/Initialize(mapload)
. = ..()
if(isnull(toilet_water_overlay))
toilet_water_overlay = mutable_appearance(icon, "[base_icon_state]-water")
cover_open = round(rand(0, 1))
update_appearance(UPDATE_ICON)
if(mapload && SSmapping.level_trait(z, ZTRAIT_STATION))
AddElement(/datum/element/lazy_fishing_spot, /datum/fish_source/toilet)
register_context()
/obj/structure/toilet/add_context(atom/source, list/context, obj/item/held_item, mob/user)
. = ..()
if(user.pulling && isliving(user.pulling))
context[SCREENTIP_CONTEXT_LMB] = "Give Swirlie"
else if(cover_open && istype(held_item, /obj/item/fish))
context[SCREENTIP_CONTEXT_LMB] = "Insert Fish"
else if(cover_open && LAZYLEN(fishes))
context[SCREENTIP_CONTEXT_LMB] = "Grab Fish"
else if(cistern_open)
if(isnull(held_item))
context[SCREENTIP_CONTEXT_LMB] = "Check Cistern"
else
context[SCREENTIP_CONTEXT_LMB] = "Insert Item"
context[SCREENTIP_CONTEXT_RMB] = "Flush"
context[SCREENTIP_CONTEXT_ALT_LMB] = "[cover_open ? "Close" : "Open"] Lid"
return CONTEXTUAL_SCREENTIP_SET
/obj/structure/toilet/examine(mob/user)
. = ..()
if(cover_open && LAZYLEN(fishes))
. += span_notice("You can see fish in the toilet, you can probably take one out.")
/obj/structure/toilet/examine_more(mob/user)
. = ..()
if(cistern_open && LAZYLEN(cistern_items))
. += span_notice("You can see [cistern_items.len] items inside of the cistern.")
/obj/structure/toilet/Destroy(force)
. = ..()
QDEL_LAZYLIST(fishes)
QDEL_LAZYLIST(cistern_items)
/obj/structure/toilet/Exited(atom/movable/gone, direction)
. = ..()
if(gone in cistern_items)
LAZYREMOVE(cistern_items, gone)
return
if(gone in fishes)
LAZYREMOVE(fishes, gone)
return
/obj/structure/toilet/attack_hand(mob/living/user, list/modifiers)
. = ..()
if(.)
return
if(swirlie)
user.changeNext_move(CLICK_CD_MELEE)
playsound(src.loc, SFX_SWING_HIT, 25, TRUE)
swirlie.visible_message(span_danger("[user] slams the toilet seat onto [swirlie]'s head!"), span_userdanger("[user] slams the toilet seat onto your head!"), span_hear("You hear reverberating porcelain."))
log_combat(user, swirlie, "swirlied (brute)")
swirlie.adjustBruteLoss(5)
return
if(user.pulling && isliving(user.pulling))
user.changeNext_move(CLICK_CD_MELEE)
var/mob/living/grabbed_mob = user.pulling
if(user.grab_state < GRAB_AGGRESSIVE)
to_chat(user, span_warning("You need a tighter grip!"))
return
if(grabbed_mob.loc != get_turf(src))
to_chat(user, span_warning("[grabbed_mob] needs to be on [src]!"))
return
if(swirlie)
return
if(cover_open)
grabbed_mob.visible_message(span_danger("[user] starts to give [grabbed_mob] a swirlie!"), span_userdanger("[user] starts to give you a swirlie..."))
swirlie = grabbed_mob
var/was_alive = (swirlie.stat != DEAD)
if(!do_after(user, 3 SECONDS, target = src, timed_action_flags = IGNORE_HELD_ITEM))
swirlie = null
return
grabbed_mob.visible_message(span_danger("[user] gives [grabbed_mob] a swirlie!"), span_userdanger("[user] gives you a swirlie!"), span_hear("You hear a toilet flushing."))
if(iscarbon(grabbed_mob))
var/mob/living/carbon/carbon_grabbed = grabbed_mob
if(!carbon_grabbed.internal)
log_combat(user, carbon_grabbed, "swirlied (oxy)")
carbon_grabbed.adjustOxyLoss(5)
else
log_combat(user, grabbed_mob, "swirlied (oxy)")
grabbed_mob.adjustOxyLoss(5)
if(was_alive && swirlie.stat == DEAD && swirlie.client)
swirlie.client.give_award(/datum/award/achievement/misc/swirlie, swirlie) // just like space high school all over again!
swirlie = null
else
playsound(src.loc, 'sound/effects/bang.ogg', 25, TRUE)
grabbed_mob.visible_message(span_danger("[user] slams [grabbed_mob.name] into [src]!"), span_userdanger("[user] slams you into [src]!"))
log_combat(user, grabbed_mob, "toilet slammed")
grabbed_mob.adjustBruteLoss(5)
return
if(cistern_open && !cover_open && user.CanReach(src))
if(!LAZYLEN(cistern_items))
to_chat(user, span_notice("The cistern is empty."))
return
var/obj/item/random_cistern_item = pick(cistern_items)
if(ishuman(user))
user.put_in_hands(random_cistern_item)
else
random_cistern_item.forceMove(drop_location())
to_chat(user, span_notice("You find [random_cistern_item] in the cistern."))
w_items -= random_cistern_item.w_class
return
if(!flushing && LAZYLEN(fishes) && cover_open)
var/obj/item/random_fish = pick(fishes)
if(ishuman(user))
user.put_in_hands(random_fish)
else
random_fish.forceMove(drop_location())
to_chat(user, span_notice("You take [random_fish] out of the toilet, poor thing."))
/obj/structure/toilet/click_alt(mob/living/user)
if(flushing)
return CLICK_ACTION_BLOCKING
cover_open = !cover_open
update_appearance(UPDATE_ICON)
return CLICK_ACTION_SUCCESS
/obj/structure/toilet/attack_hand_secondary(mob/user, list/modifiers)
. = ..()
if(flushing)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
flushing = TRUE
playsound(src, "sound/machines/toilet_flush.ogg", cover_open ? 40 : 20, TRUE)
if(cover_open && (dir & SOUTH))
update_appearance(UPDATE_OVERLAYS)
flick_overlay_view(mutable_appearance(icon, "[base_icon_state]-water-flick"), 3 SECONDS)
addtimer(CALLBACK(src, PROC_REF(end_flushing)), 4 SECONDS)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/obj/structure/toilet/update_icon_state()
icon_state = "[base_icon_state][cover_open][cistern_open]"
return ..()
/obj/structure/toilet/update_overlays()
. = ..()
if(!flushing && cover_open && (dir & SOUTH))
. += toilet_water_overlay
/obj/structure/toilet/atom_deconstruct(dissambled = TRUE)
for(var/obj/toilet_item in cistern_items)
toilet_item.forceMove(drop_location())
if(buildstacktype)
new buildstacktype(loc,buildstackamount)
else
for(var/datum/material/M as anything in custom_materials)
new M.sheet_type(loc, FLOOR(custom_materials[M] / SHEET_MATERIAL_AMOUNT, 1))
/obj/structure/toilet/attackby(obj/item/attacking_item, mob/living/user, params)
add_fingerprint(user)
if(cover_open && istype(attacking_item, /obj/item/fish))
if(fishes >= 3)
to_chat(user, span_warning("There's too many fishes, flush them down first."))
return
if(!user.transferItemToLoc(attacking_item, src))
to_chat(user, span_warning("\The [attacking_item] is stuck to your hand!"))
return
var/obj/item/fish/the_fish = attacking_item
if(the_fish.status == FISH_DEAD)
to_chat(user, span_warning("You place [attacking_item] into [src], may it rest in peace."))
else
to_chat(user, span_notice("You place [attacking_item] into [src], hopefully no one will miss it!"))
LAZYADD(fishes, attacking_item)
return
if(cistern_open && !user.combat_mode)
if(attacking_item.w_class > WEIGHT_CLASS_NORMAL)
to_chat(user, span_warning("[attacking_item] does not fit!"))
return
if(w_items + attacking_item.w_class > WEIGHT_CLASS_HUGE)
to_chat(user, span_warning("The cistern is full!"))
return
if(!user.transferItemToLoc(attacking_item, src))
to_chat(user, span_warning("\The [attacking_item] is stuck to your hand, you cannot put it in the cistern!"))
return
LAZYADD(cistern_items, attacking_item)
w_items += attacking_item.w_class
to_chat(user, span_notice("You carefully place [attacking_item] into the cistern."))
return
if(is_reagent_container(attacking_item) && !user.combat_mode)
if (!cover_open)
return
if(istype(attacking_item, /obj/item/food/monkeycube))
var/obj/item/food/monkeycube/cube = attacking_item
cube.Expand()
return
var/obj/item/reagent_containers/RG = attacking_item
RG.reagents.add_reagent(/datum/reagent/water, min(RG.volume - RG.reagents.total_volume, RG.amount_per_transfer_from_this))
to_chat(user, span_notice("You fill [RG] from [src]. Gross."))
return ..()
/obj/structure/toilet/crowbar_act(mob/living/user, obj/item/tool)
to_chat(user, span_notice("You start to [cistern_open ? "replace the lid on" : "lift the lid off"] the cistern..."))
playsound(loc, 'sound/effects/stonedoor_openclose.ogg', 50, TRUE)
if(tool.use_tool(src, user, 30))
user.visible_message(
span_notice("[user] [cistern_open ? "replaces the lid on" : "lifts the lid off"] the cistern!"),
span_notice("You [cistern_open ? "replace the lid on" : "lift the lid off"] the cistern!"),
span_hear("You hear grinding porcelain."))
cistern_open = !cistern_open
update_appearance(UPDATE_ICON_STATE)
return ITEM_INTERACT_SUCCESS
/obj/structure/toilet/wrench_act(mob/living/user, obj/item/tool)
tool.play_tool_sound(src)
deconstruct()
return ITEM_INTERACT_SUCCESS
///Ends the flushing animation and updates overlays if necessary
/obj/structure/toilet/proc/end_flushing()
flushing = FALSE
if(cover_open && (dir & SOUTH))
update_appearance(UPDATE_OVERLAYS)
QDEL_LAZYLIST(fishes)
/obj/structure/toilet/greyscale
material_flags = MATERIAL_EFFECTS | MATERIAL_ADD_PREFIX | MATERIAL_COLOR | MATERIAL_AFFECT_STATISTICS
buildstacktype = null
/obj/structure/toilet/secret
var/secret_type = null
/obj/structure/toilet/secret/Initialize(mapload)
. = ..()
if(secret_type)
var/obj/item/secret = new secret_type(src)
secret.desc += " It's a secret!"
w_items += secret.w_class
LAZYADD(cistern_items, secret)
@@ -0,0 +1,116 @@
/obj/structure/urinal
name = "urinal"
desc = "The HU-452, an experimental urinal. Comes complete with experimental urinal cake."
icon = 'icons/obj/watercloset.dmi'
icon_state = "urinal"
density = FALSE
anchored = TRUE
/// Can you currently put an item inside
var/exposed = FALSE
/// What's in the urinal
var/obj/item/hidden_item
MAPPING_DIRECTIONAL_HELPERS(/obj/structure/urinal, 32)
/obj/structure/urinal/Initialize(mapload)
. = ..()
if(mapload)
hidden_item = new /obj/item/food/urinalcake(src)
find_and_hang_on_wall()
/obj/structure/urinal/Exited(atom/movable/gone, direction)
. = ..()
if(gone == hidden_item)
hidden_item = null
/obj/structure/urinal/attack_hand(mob/living/user, list/modifiers)
. = ..()
if(.)
return
if(user.pulling && isliving(user.pulling))
var/mob/living/grabbed_mob = user.pulling
if(user.grab_state >= GRAB_AGGRESSIVE)
if(grabbed_mob.loc != get_turf(src))
to_chat(user, span_notice("[grabbed_mob.name] needs to be on [src]."))
return
user.changeNext_move(CLICK_CD_MELEE)
user.visible_message(span_danger("[user] slams [grabbed_mob] into [src]!"), span_danger("You slam [grabbed_mob] into [src]!"))
grabbed_mob.emote("scream")
grabbed_mob.adjustBruteLoss(8)
else
to_chat(user, span_warning("You need a tighter grip!"))
return
if(exposed)
if(hidden_item)
to_chat(user, span_notice("You fish [hidden_item] out of the drain enclosure."))
user.put_in_hands(hidden_item)
else
to_chat(user, span_warning("There is nothing in the drain holder!"))
return
return ..()
/obj/structure/urinal/attackby(obj/item/attacking_item, mob/user, params)
if(exposed)
if(hidden_item)
to_chat(user, span_warning("There is already something in the drain enclosure!"))
return
if(attacking_item.w_class > WEIGHT_CLASS_TINY)
to_chat(user, span_warning("[attacking_item] is too large for the drain enclosure."))
return
if(!user.transferItemToLoc(attacking_item, src))
to_chat(user, span_warning("[attacking_item] is stuck to your hand, you cannot put it in the drain enclosure!"))
return
hidden_item = attacking_item
to_chat(user, span_notice("You place [attacking_item] into the drain enclosure."))
return
return ..()
/obj/structure/urinal/screwdriver_act(mob/living/user, obj/item/I)
if(..())
return TRUE
to_chat(user, span_notice("You start to [exposed ? "screw the cap back into place" : "unscrew the cap to the drain protector"]..."))
playsound(loc, 'sound/effects/stonedoor_openclose.ogg', 50, TRUE)
if(I.use_tool(src, user, 20))
user.visible_message(span_notice("[user] [exposed ? "screws the cap back into place" : "unscrew the cap to the drain protector"]!"),
span_notice("You [exposed ? "screw the cap back into place" : "unscrew the cap on the drain"]!"),
span_hear("You hear metal and squishing noises."))
exposed = !exposed
return TRUE
/obj/structure/urinal/wrench_act_secondary(mob/living/user, obj/item/tool)
tool.play_tool_sound(user)
deconstruct(TRUE)
balloon_alert(user, "removed urinal")
return ITEM_INTERACT_SUCCESS
/obj/structure/urinal/atom_deconstruct(disassembled = TRUE)
new /obj/item/wallframe/urinal(loc)
hidden_item?.forceMove(drop_location())
/obj/item/wallframe/urinal
name = "urinal frame"
desc = "An unmounted urinal. Attach it to a wall to use."
icon = 'icons/obj/watercloset.dmi'
icon_state = "urinal"
result_path = /obj/structure/urinal
pixel_shift = 32
/obj/item/food/urinalcake
name = "urinal cake"
desc = "The noble urinal cake, protecting the station's pipes from the station's pee. Do not eat."
icon = 'icons/obj/watercloset.dmi'
icon_state = "urinalcake"
w_class = WEIGHT_CLASS_TINY
food_reagents = list(
/datum/reagent/chlorine = 3,
/datum/reagent/ammonia = 1,
)
foodtypes = TOXIC | GROSS
preserved_food = TRUE
/obj/item/food/urinalcake/attack_self(mob/living/user)
user.visible_message(span_notice("[user] squishes [src]!"), span_notice("You squish [src]."), "<i>You hear a squish.</i>")
icon_state = "urinalcake_squish"
addtimer(VARSET_CALLBACK(src, icon_state, "urinalcake"), 0.8 SECONDS)
@@ -0,0 +1,149 @@
//Water source, use the type water_source for unlimited water sources like classic sinks.
/obj/structure/water_source
name = "Water Source"
icon = 'icons/obj/watercloset.dmi'
icon_state = "sink"
desc = "A sink used for washing one's hands and face. This one seems to be infinite!"
anchored = TRUE
///Boolean on whether something is currently being washed, preventing multiple people from cleaning at once.
var/busy = FALSE
///The reagent that is dispensed from this source, by default it's water.
var/datum/reagent/dispensedreagent = /datum/reagent/water
/obj/structure/water_source/Initialize(mapload)
. = ..()
create_reagents(INFINITY, NO_REACT)
reagents.add_reagent(dispensedreagent, INFINITY)
/obj/structure/water_source/attack_hand(mob/living/user, list/modifiers)
. = ..()
if(.)
return
if(!iscarbon(user))
return
if(!Adjacent(user))
return
if(busy)
to_chat(user, span_warning("Someone's already washing here!"))
return
var/selected_area = user.parse_zone_with_bodypart(user.zone_selected)
var/washing_face = FALSE
if(selected_area in list(BODY_ZONE_HEAD, BODY_ZONE_PRECISE_MOUTH, BODY_ZONE_PRECISE_EYES))
washing_face = TRUE
user.visible_message(
span_notice("[user] starts washing [user.p_their()] [washing_face ? "face" : "hands"]..."),
span_notice("You start washing your [washing_face ? "face" : "hands"]..."))
busy = TRUE
if(!do_after(user, 4 SECONDS, target = src))
busy = FALSE
return
busy = FALSE
if(washing_face)
SEND_SIGNAL(user, COMSIG_COMPONENT_CLEAN_FACE_ACT, CLEAN_WASH)
else if(ishuman(user))
var/mob/living/carbon/human/human_user = user
if(!human_user.wash_hands(CLEAN_WASH))
to_chat(user, span_warning("Your hands are covered by something!"))
return
else
user.wash(CLEAN_WASH)
user.visible_message(
span_notice("[user] washes [user.p_their()] [washing_face ? "face" : "hands"] using [src]."),
span_notice("You wash your [washing_face ? "face" : "hands"] using [src]."),
)
/obj/structure/water_source/attackby(obj/item/attacking_item, mob/living/user, params)
if(busy)
to_chat(user, span_warning("Someone's already washing here!"))
return
if(attacking_item.item_flags & ABSTRACT) //Abstract items like grabs won't wash. No-drop items will though because it's still technically an item in your hand.
return
if(is_reagent_container(attacking_item))
var/obj/item/reagent_containers/container = attacking_item
if(container.is_refillable())
if(!container.reagents.holder_full())
container.reagents.add_reagent(dispensedreagent, min(container.volume - container.reagents.total_volume, container.amount_per_transfer_from_this))
to_chat(user, span_notice("You fill [container] from [src]."))
return TRUE
to_chat(user, span_notice("\The [container] is full."))
return FALSE
if(istype(attacking_item, /obj/item/melee/baton/security))
var/obj/item/melee/baton/security/baton = attacking_item
if(baton.cell?.charge && baton.active)
flick("baton_active", src)
user.Paralyze(baton.knockdown_time)
user.set_stutter(baton.knockdown_time)
baton.cell.use(baton.cell_hit_cost)
user.visible_message(
span_warning("[user] shocks [user.p_them()]self while attempting to wash the active [baton.name]!"),
span_userdanger("You unwisely attempt to wash [baton] while it's still on."))
playsound(src, baton.on_stun_sound, 50, TRUE)
return
if(istype(attacking_item, /obj/item/mop))
attacking_item.reagents.add_reagent(dispensedreagent, 5)
to_chat(user, span_notice("You wet [attacking_item] in [src]."))
playsound(loc, 'sound/effects/slosh.ogg', 25, TRUE)
return
if(istype(attacking_item, /obj/item/stack/medical/gauze))
var/obj/item/stack/medical/gauze/G = attacking_item
new /obj/item/reagent_containers/cup/rag(loc)
to_chat(user, span_notice("You tear off a strip of gauze and make a rag."))
G.use(1)
return
if(istype(attacking_item, /obj/item/stack/sheet/cloth))
var/obj/item/stack/sheet/cloth/cloth = attacking_item
new /obj/item/reagent_containers/cup/rag(loc)
to_chat(user, span_notice("You tear off a strip of cloth and make a rag."))
cloth.use(1)
return
if(istype(attacking_item, /obj/item/stack/ore/glass))
new /obj/item/stack/sheet/sandblock(loc)
to_chat(user, span_notice("You wet the sand and form it into a block."))
attacking_item.use(1)
return
if(!user.combat_mode)
to_chat(user, span_notice("You start washing [attacking_item]..."))
busy = TRUE
if(!do_after(user, 4 SECONDS, target = src))
busy = FALSE
return TRUE
busy = FALSE
attacking_item.wash(CLEAN_WASH)
reagents.expose(attacking_item, TOUCH, 5 / max(reagents.total_volume, 5))
user.visible_message(
span_notice("[user] washes [attacking_item] using [src]."),
span_notice("You wash [attacking_item] using [src]."))
return TRUE
return ..()
/obj/structure/water_source/puddle //splishy splashy ^_^
name = "puddle"
desc = "A puddle used for washing one's hands and face."
icon_state = "puddle"
base_icon_state = "puddle"
resistance_flags = UNACIDABLE
//ATTACK HAND IGNORING PARENT RETURN VALUE
/obj/structure/water_source/puddle/attack_hand(mob/user, list/modifiers)
icon_state = "[base_icon_state]-splash"
. = ..()
icon_state = base_icon_state
/obj/structure/water_source/puddle/attackby(obj/item/attacking_item, mob/user, params)
icon_state = "[base_icon_state]-splash"
. = ..()
icon_state = base_icon_state