Files
Alexis 21b4095dfd [MDB IGNORE] [IDB IGNORE] Upstream Sync - 04/17/2026 (#5453)
Upstream 04/17/2026

fixes https://github.com/Bubberstation/Bubberstation/issues/5549

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: tgstation-ci[bot] <179393467+tgstation-ci[bot]@users.noreply.github.com>
Co-authored-by: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com>
Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
Co-authored-by: Rhials <28870487+Rhials@users.noreply.github.com>
Co-authored-by: rageguy505 <54517726+rageguy505@users.noreply.github.com>
Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
Co-authored-by: John Willard <53777086+JohnFulpWillard@users.noreply.github.com>
Co-authored-by: Aliceee2ch <160794176+Aliceee2ch@users.noreply.github.com>
Co-authored-by: Time-Green <7501474+Time-Green@users.noreply.github.com>
Co-authored-by: Tsar-Salat <62388554+Tsar-Salat@users.noreply.github.com>
Co-authored-by: SmArtKar <44720187+SmArtKar@users.noreply.github.com>
Co-authored-by: Maxipat <108554989+Maxipat112@users.noreply.github.com>
Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: SimplyLogan <47579821+loganuk@users.noreply.github.com>
Co-authored-by: loganuk <fakeemail123@aol.com>
Co-authored-by: Leland Kemble <70413276+lelandkemble@users.noreply.github.com>
Co-authored-by: FalloutFalcon <86381784+FalloutFalcon@users.noreply.github.com>
Co-authored-by: Roxy <75404941+TealSeer@users.noreply.github.com>
Co-authored-by: Lucy <lucy@absolucy.moe>
Co-authored-by: siliconOpossum <138069572+siliconOpossum@users.noreply.github.com>
Co-authored-by: Isratosh <Isratosh@hotmail.com>
Co-authored-by: TheRyeGuyWhoWillNowDie <70169560+TheRyeGuyWhoWillNowDie@users.noreply.github.com>
Co-authored-by: Neocloudy <88008002+Neocloudy@users.noreply.github.com>
Co-authored-by: Alexander V. <volas@ya.ru>
Co-authored-by: ElGitificador <168473461+ElGitificador@users.noreply.github.com>
Co-authored-by: Twaticus <46540570+Twaticus@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bloop <13398309+vinylspiders@users.noreply.github.com>
Co-authored-by: Cameron Lennox <killer65311@gmail.com>
Co-authored-by: Tim <timothymtorres@gmail.com>
Co-authored-by: Iamgoofball <iamgoofball@gmail.com>
Co-authored-by: Layzu666 <121319428+Layzu666@users.noreply.github.com>
Co-authored-by: Arturlang <24881678+Arturlang@users.noreply.github.com>
Co-authored-by: _0Steven <42909981+00-Steven@users.noreply.github.com>
Co-authored-by: mrmanlikesbt <99309552+mrmanlikesbt@users.noreply.github.com>
Co-authored-by: Ben10Omintrix <138636438+Ben10Omintrix@users.noreply.github.com>
Co-authored-by: John F. Kennedy <54908920+MacaroniCritter@users.noreply.github.com>
Co-authored-by: Cursor <102828457+theselfish@users.noreply.github.com>
Co-authored-by: Josh <josh.adam.powell@gmail.com>
Co-authored-by: Josh Powell <josh.powell@softwire.com>
Co-authored-by: Yobrocharlie <Charliemiller5617@gmail.com>
Co-authored-by: Hardly3D <66234359+Hardly3D@users.noreply.github.com>
Co-authored-by: shayoki <96078776+shayoki@users.noreply.github.com>
Co-authored-by: LT3 <83487515+lessthnthree@users.noreply.github.com>
2026-05-16 00:56:00 +02:00

443 lines
16 KiB
Plaintext

/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
custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT)
/// 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
/// 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
/// Does the toilet have a water recycler to recollect its water supply?
var/has_water_reclaimer = TRUE
/// Units of water to reclaim per second
var/reclaim_rate = 0.5
/// What reagent does the toilet flush with
var/reagent_id = /datum/reagent/water
/// How much reagent can the cistern contain
var/reagent_capacity = 200
/// Item stuck in the basin of the toilet
var/obj/item/stuck_item = null
/obj/structure/toilet/Initialize(mapload, has_water_reclaimer = null)
. = ..()
cover_open = round(rand(0, 1))
if(!isnull(has_water_reclaimer))
src.has_water_reclaimer = has_water_reclaimer
update_appearance(UPDATE_ICON)
if(mapload && SSmapping.level_trait(z, ZTRAIT_STATION))
AddComponent(/datum/component/fishing_spot, GLOB.preset_fish_sources[/datum/fish_source/toilet])
AddElement(/datum/element/fish_safe_storage)
register_context()
create_reagents(reagent_capacity)
if(src.has_water_reclaimer)
reagents.add_reagent(reagent_id, reagent_capacity)
AddComponent(/datum/component/plumbing/simple_demand/extended)
/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"
if(cover_open)
if(isnull(held_item))
if(LAZYLEN(fishes))
context[SCREENTIP_CONTEXT_LMB] = "Grab Fish"
else if(istype(held_item, /obj/item/fish))
context[SCREENTIP_CONTEXT_LMB] = "Insert Fish"
else if(istype(held_item, /obj/item/plunger))
context[SCREENTIP_CONTEXT_LMB] = "Unclog"
else if(held_item.w_class <= WEIGHT_CLASS_SMALL)
context[SCREENTIP_CONTEXT_LMB] = "Insert Item"
else if(cistern_open)
if(isnull(held_item))
context[SCREENTIP_CONTEXT_LMB] = "Check Cistern"
else if(held_item.tool_behaviour == TOOL_SCREWDRIVER && has_water_reclaimer)
context[SCREENTIP_CONTEXT_LMB] = "Remove Reclaimer"
else if(istype(held_item, /obj/item/stock_parts/water_recycler) && !has_water_reclaimer)
context[SCREENTIP_CONTEXT_LMB] = "Install Reclaimer"
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)
if(LAZYLEN(fishes))
. += span_notice("You can see fish in the toilet, you can probably take one out.")
if(stuck_item)
. += span_notice("There seems to be something small in [src]'s bowl...")
if(cistern_open && has_water_reclaimer)
. += span_notice("A water recycler is installed. Its attached by a pair of screws.")
. += span_notice("Its display states: [reagents.total_volume]/[reagents.maximum_volume] liquids remaining.")
/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)
QDEL_NULL(stuck_item)
/obj/structure/toilet/Exited(atom/movable/gone, direction)
. = ..()
if(gone in cistern_items)
LAZYREMOVE(cistern_items, gone)
if (isitem(gone))
var/obj/item/removed_item = gone
w_items -= removed_item.w_class
return
if(gone in fishes)
LAZYREMOVE(fishes, gone)
else if(gone == stuck_item)
stuck_item = null
/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.adjust_brute_loss(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)
if(!reagents.total_volume)
to_chat(user, span_notice("\The [src] is dry!"))
return
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
if(!reagents.total_volume)
to_chat(user, span_notice("\The [src] is dry!"))
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.adjust_oxy_loss(5)
else
log_combat(user, grabbed_mob, "swirlied (oxy)")
grabbed_mob.adjust_oxy_loss(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.adjust_brute_loss(5)
return
if(cistern_open && !cover_open && IsReachableBy(user))
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."))
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
if(reagents.total_volume <= 50)
to_chat(user, span_notice("You press the flush lever, but nothing happens."))
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
flushing = TRUE
var/something_stuck = !isnull(stuck_item)
if(!something_stuck && LAZYLEN(fishes))
for(var/obj/item/fish/fish as anything in fishes)
if(fish.w_class >= WEIGHT_CLASS_NORMAL)
something_stuck = TRUE
break
if(something_stuck)
reagents.create_foam(/datum/effect_system/fluid_spread/foam, 10, notification = span_danger("[src] overflows, spilling its cistern's contents everywhere!"), log = TRUE)
else
reagents.remove_all(50)
begin_reclamation()
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)
. += "[base_icon_state]-water"
/obj/structure/toilet/dump_contents()
for(var/obj/toilet_item in (cistern_items + fishes))
toilet_item.forceMove(drop_location())
stuck_item?.forceMove(drop_location())
/obj/structure/toilet/atom_deconstruct(dissambled = TRUE)
dump_contents()
drop_custom_materials()
if(has_water_reclaimer)
new /obj/item/stock_parts/water_recycler(drop_location())
/obj/structure/toilet/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
if(user.combat_mode)
return NONE
add_fingerprint(user)
if(cover_open && istype(tool, /obj/item/fish))
if(LAZYLEN(fishes) >= 3)
to_chat(user, span_warning("There's too many fishes, flush them down first."))
return ITEM_INTERACT_BLOCKING
if(!user.transferItemToLoc(tool, src))
to_chat(user, span_warning("\The [tool] is stuck to your hand!"))
return ITEM_INTERACT_BLOCKING
var/obj/item/fish/the_fish = tool
if(the_fish.status == FISH_DEAD)
to_chat(user, span_warning("You place [tool] into [src], may it rest in peace."))
else
to_chat(user, span_notice("You place [tool] into [src], hopefully no one will miss it!"))
LAZYADD(fishes, tool)
return ITEM_INTERACT_SUCCESS
if(cistern_open)
if(istype(tool, /obj/item/stock_parts/water_recycler))
if(has_water_reclaimer)
to_chat(user, span_warning("[src] already has a water recycler installed."))
return ITEM_INTERACT_BLOCKING
playsound(src, 'sound/machines/click.ogg', 20, TRUE)
qdel(tool)
has_water_reclaimer = TRUE
begin_reclamation()
return ITEM_INTERACT_SUCCESS
if(tool.w_class > WEIGHT_CLASS_NORMAL)
to_chat(user, span_warning("[tool] does not fit!"))
return ITEM_INTERACT_BLOCKING
if(w_items + tool.w_class > WEIGHT_CLASS_HUGE)
to_chat(user, span_warning("The cistern is full!"))
return ITEM_INTERACT_BLOCKING
if(!user.transferItemToLoc(tool, src))
to_chat(user, span_warning("\The [tool] is stuck to your hand, you cannot put it in the cistern!"))
return ITEM_INTERACT_BLOCKING
add_cistern_item(tool)
to_chat(user, span_notice("You carefully place [tool] into the cistern."))
return ITEM_INTERACT_SUCCESS
if(!cover_open)
return NONE
if(!is_reagent_container(tool))
if(tool.w_class > WEIGHT_CLASS_SMALL)
return NONE
if(stuck_item)
to_chat(user, span_warning("There's already something blocking [src]'s drain pipe!"))
return ITEM_INTERACT_BLOCKING
if(!user.transferItemToLoc(tool, src))
to_chat(user, span_warning("\The [tool] is stuck to your hand!"))
return ITEM_INTERACT_BLOCKING
stuck_item = tool
to_chat(user, span_notice("You drop [tool] into [src]'s bowl."))
return ITEM_INTERACT_SUCCESS
if(reagents.total_volume <= 0)
to_chat(user, span_notice("\The [src] is dry."))
return ITEM_INTERACT_BLOCKING
if(istype(tool, /obj/item/food/monkeycube))
var/obj/item/food/monkeycube/cube = tool
cube.Expand()
return ITEM_INTERACT_SUCCESS
var/obj/item/reagent_containers/container = tool
if(!container.is_refillable())
return NONE
if(container.reagents.holder_full())
to_chat(user, span_notice("\The [container] is full."))
return ITEM_INTERACT_BLOCKING
reagents.trans_to(container, container.amount_per_transfer_from_this, transferred_by = user)
begin_reclamation()
to_chat(user, span_notice("You fill [container] from [src]. Gross."))
return ITEM_INTERACT_SUCCESS
/// Hides an item inside the toilet for later retrievalk
/obj/structure/toilet/proc/add_cistern_item(obj/item/thing)
if (isitem(thing))
w_items += thing.w_class
LAZYADD(cistern_items, thing)
/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/screwdriver_act(mob/living/user, obj/item/tool)
if(!cistern_open)
to_chat(user, span_warning("You need to open [src]'s cistern first!"))
return ITEM_INTERACT_BLOCKING
if(!has_water_reclaimer)
to_chat(user, span_warning("\the [src] doesn't have a water reclaimer installed."))
return ITEM_INTERACT_BLOCKING
tool.play_tool_sound(src)
has_water_reclaimer = FALSE
new /obj/item/stock_parts/water_recycler(drop_location())
to_chat(user, span_notice("You remove the water reclaimer from \the [src]."))
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
/obj/structure/toilet/plunger_act(obj/item/plunger/attacking_plunger, mob/living/user, reinforced)
user.balloon_alert_to_viewers("furiously plunging...")
if(!do_after(user, 3 SECONDS, target = src))
return TRUE
user.balloon_alert_to_viewers("finished plunging")
reagents.expose(get_turf(src), TOUCH) //splash on the floor
reagents.clear_reagents()
begin_reclamation()
if(stuck_item)
stuck_item.forceMove(drop_location())
stuck_item = null
return TRUE
///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/proc/begin_reclamation()
START_PROCESSING(SSobj, src)
/obj/structure/toilet/process(seconds_per_tick)
// Water reclamation complete?
if(!has_water_reclaimer || reagents.total_volume >= reagents.maximum_volume)
return PROCESS_KILL
reagents.add_reagent(reagent_id, reclaim_rate * seconds_per_tick)
/obj/structure/toilet/greyscale
material_flags = MATERIAL_EFFECTS | MATERIAL_ADD_PREFIX | MATERIAL_COLOR | MATERIAL_AFFECT_STATISTICS
custom_materials = null
has_water_reclaimer = FALSE
/obj/structure/toilet/secret
var/secret_type = null
/obj/structure/toilet/secret/Initialize(mapload)
. = ..()
if(!secret_type)
return
var/obj/item/secret = new secret_type(src)
secret.desc += " It's a secret!"
add_cistern_item(secret)
///A toilet made of meat that only drops remains when deconstructed, often unleashed unto this cursed plane of existence by hopeless people off'ing themselves with experi-scanners.
/obj/structure/toilet/greyscale/flesh
desc = "A horrendous mass of fused flesh resembling a standard-issue HT-451 model toilet. How it manages to function as one is beyond you. \
This one seems to be made out of the flesh of a devoted employee of the RnD department."
/obj/structure/toilet/greyscale/flesh/Initialize(mapload, mob/living/carbon/suicide)
. = ..()
///The suicide victim's brain that will be placed inside the toilet's cistern
var/obj/item/organ/brain/toilet_brain
if(suicide)
toilet_brain = suicide.get_organ_slot(ORGAN_SLOT_BRAIN)
for(var/obj/item/thing in suicide)
if (suicide.transferItemToLoc(thing, newloc = src, silent = TRUE))
add_cistern_item(thing)
suicide.gib(DROP_BRAIN) //we delete everything but the brain, as it's going to be moved to the cistern
set_custom_materials(list(SSmaterials.get_material(/datum/material/meat/mob_meat, suicide) = SHEET_MATERIAL_AMOUNT))
else
toilet_brain = new(drop_location())
set_custom_materials(list(/datum/material/meat = SHEET_MATERIAL_AMOUNT))
toilet_brain.forceMove(src)
add_cistern_item(toilet_brain)
//this also prevents the toilet from dropping meat sheets. if you want to cheese the meat exepriments, sacrifice more people
/obj/structure/toilet/greyscale/flesh/atom_deconstruct(dissambled = TRUE)
for(var/obj/toilet_item in cistern_items)
toilet_item.forceMove(drop_location())
new /obj/effect/decal/remains/human(loc)