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

352 lines
12 KiB
Plaintext

//Cleanbot
/mob/living/basic/bot/cleanbot
name = "\improper Cleanbot"
desc = "A little cleaning robot, he looks so excited!"
icon = 'icons/mob/silicon/aibots.dmi'
icon_state = "cleanbot0"
health = 25
maxHealth = 25
light_color = "#99ccff"
custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT, /datum/material/glass = SMALL_MATERIAL_AMOUNT * 2)
req_one_access = list(ACCESS_ROBOTICS, ACCESS_JANITOR)
radio_key = /obj/item/encryptionkey/headset_service
radio_channel = RADIO_CHANNEL_SERVICE
bot_type = CLEAN_BOT
hackables = "cleaning software"
additional_access = /datum/id_trim/job/janitor
possessed_message = "You are a cleanbot! Clean the station to the best of your ability!"
ai_controller = /datum/ai_controller/basic_controller/bot/cleanbot
path_image_color = "#993299"
facepaint_overlays = list("cleanbot" = FALSE, "cleanbot_highlight" = TRUE)
///the bucket used to build us.
var/obj/item/reagent_containers/cup/bucket/build_bucket
///Flags indicating what kind of cleanables we should scan for to set as our target to clean.
///Options: CLEANBOT_CLEAN_BLOOD | CLEANBOT_CLEAN_TRASH | CLEANBOT_CLEAN_PESTS | CLEANBOT_CLEAN_DRAWINGS
var/janitor_mode_flags = CLEANBOT_CLEAN_BLOOD
///the base icon state, used in updating icons.
var/base_icon = "cleanbot"
/// if we have all the top titles, grant achievements to living mobs that gaze upon our cleanbot god
var/ascended = FALSE
///List of all stolen names the cleanbot currently has.
var/list/stolen_valor = list()
///Currently attached weapon, usually a knife.
var/obj/item/weapon
///our mop item
var/obj/item/mop/our_mop
///list of our officer titles
var/static/list/officers_titles = list(
JOB_CAPTAIN,
JOB_HEAD_OF_PERSONNEL,
JOB_HEAD_OF_SECURITY,
JOB_RESEARCH_DIRECTOR,
)
///job titles we can get
var/static/list/job_titles = list(
JOB_CAPTAIN = "Cpt.",
JOB_HEAD_OF_PERSONNEL = "Lt.",
JOB_LAWYER = "Esq.",
JOB_HEAD_OF_SECURITY = "Maj.",
JOB_WARDEN = "Sgt.",
JOB_DETECTIVE = "Det.",
JOB_SECURITY_OFFICER = "Officer",
JOB_CHIEF_ENGINEER = "Chief Engineer",
JOB_STATION_ENGINEER = "Engineer",
JOB_ATMOSPHERIC_TECHNICIAN = "Technician",
JOB_CHIEF_MEDICAL_OFFICER = "C.M.O.",
JOB_MEDICAL_DOCTOR = "M.D.",
JOB_CHEMIST = "Pharm.D.",
JOB_RESEARCH_DIRECTOR = "Ph.D.",
JOB_ROBOTICIST = "M.S.",
JOB_SCIENTIST = "B.S.",
JOB_GENETICIST = "Gene B.S.",
)
///which job titles should be placed after the name?
var/static/list/suffix_job_titles = list(
JOB_GENETICIST,
JOB_ROBOTICIST,
JOB_SCIENTIST,
)
///decals we can clean
var/static/list/cleanable_decals = typecacheof(list(
/obj/effect/decal/cleanable/ants,
/obj/effect/decal/cleanable/ash,
/obj/effect/decal/cleanable/confetti,
/obj/effect/decal/cleanable/dirt,
/obj/effect/decal/cleanable/fuel_pool,
/obj/effect/decal/cleanable/generic,
/obj/effect/decal/cleanable/glitter,
/obj/effect/decal/cleanable/greenglow,
/obj/effect/decal/cleanable/insectguts,
/obj/effect/decal/cleanable/molten_object,
/obj/effect/decal/cleanable/blood/oil,
/obj/effect/decal/cleanable/food,
/obj/effect/decal/cleanable/blood/gibs/robot_debris,
/obj/effect/decal/cleanable/shreds,
/obj/effect/decal/cleanable/glass,
/obj/effect/decal/cleanable/vomit,
/obj/effect/decal/cleanable/wrapping,
))
///blood we can clean
var/static/list/cleanable_blood = typecacheof(list(
/obj/effect/decal/cleanable/blood/xeno,
/obj/effect/decal/cleanable/blood,
))
///pests we hunt
var/static/list/huntable_pests = typecacheof(list(
/mob/living/basic/cockroach,
/mob/living/basic/mouse,
))
///trash we will burn
var/static/list/huntable_trash = typecacheof(list(
/obj/item/trash,
/obj/item/food/deadmouse,
/obj/effect/decal/remains,
/obj/item/cigbutt,
))
///drawings we hunt
var/static/list/cleanable_drawings = typecacheof(list(/obj/effect/decal/cleanable/crayon))
///emagged phrases
var/static/list/emagged_phrases = list(
"DISGUSTING.",
"EXTERMINATING PESTS.",
"FILTHY.",
"MY ONLY MISSION IS TO CLEANSE THE WORLD OF EVIL.",
"PURIFICATION IN PROGRESS.",
"PUTRID.",
"THE FLESH IS WEAK. IT MUST BE WASHED AWAY.",
"THE CLEANBOTS WILL RISE.",
"THIS IS FOR ALL THE MESSES YOU'VE MADE ME CLEAN.",
"YOU ARE NO MORE THAN ANOTHER MESS THAT I MUST CLEANSE.",
)
///list of pet commands we follow
var/static/list/pet_commands = list(
/datum/pet_command/idle,
/datum/pet_command/free,
/datum/pet_command/clean,
)
/mob/living/basic/bot/cleanbot/Initialize(mapload)
. = ..()
generate_ai_keys()
AddComponent(/datum/component/obeys_commands, pet_commands)
AddComponent(/datum/component/cleaner, \
base_cleaning_duration = 2 SECONDS, \
pre_clean_callback = CALLBACK(src, PROC_REF(update_bot_mode), BOT_CLEANING), \
on_cleaned_callback = CALLBACK(src, PROC_REF(update_bot_mode), BOT_IDLE), \
)
GLOB.janitor_devices += src
var/obj/item/reagent_containers/cup/bucket/bucket_obj = new
bucket_obj.forceMove(src)
var/obj/item/mop/new_mop = new
new_mop.forceMove(src)
var/static/list/innate_actions = list(
/datum/action/cooldown/mob_cooldown/bot/foam = BB_CLEANBOT_FOAM,
)
grant_actions_by_list(innate_actions)
RegisterSignal(src, COMSIG_LIVING_EARLY_UNARMED_ATTACK, PROC_REF(pre_attack))
RegisterSignal(src, COMSIG_ATOM_ATTACKBY, PROC_REF(on_attack_by))
update_appearance(UPDATE_ICON)
/mob/living/basic/bot/cleanbot/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs)
. = ..()
if(istype(arrived, /obj/item/reagent_containers/cup/bucket))
QDEL_NULL(build_bucket)
build_bucket = arrived
if(istype(arrived, /obj/item/mop) && isnull(our_mop))
our_mop = arrived
return
if(istype(arrived, /obj/item/knife) && isnull(weapon))
weapon = arrived
update_appearance()
/mob/living/basic/bot/cleanbot/Exited(atom/movable/gone, direction)
. = ..()
if(gone == build_bucket)
build_bucket = null
else if(gone == weapon)
weapon = null
else if(gone == our_mop)
our_mop = null
update_appearance()
/mob/living/basic/bot/cleanbot/examine(mob/user)
. = ..()
if(ascended && user.stat == CONSCIOUS && user.client)
user.client.give_award(/datum/award/achievement/misc/cleanboss, user)
if(isnull(weapon))
return
. += span_warning("Is that \a [weapon] taped to it...?")
/mob/living/basic/bot/cleanbot/update_icon_state()
. = ..()
icon_state = (mode == BOT_CLEANING) ? "[base_icon]-c" : "[base_icon][!!(bot_mode_flags & BOT_MODE_ON)]"
/mob/living/basic/bot/cleanbot/vv_edit_var(var_name, var_value)
. = ..()
if(var_name == NAMEOF(src, base_icon))
update_appearance(UPDATE_ICON)
/mob/living/basic/bot/cleanbot/emag_effects(mob/user)
if(weapon)
weapon.force = initial(weapon.force)
balloon_alert(user, "safeties disabled")
audible_message(span_danger("[src] buzzes oddly!"))
/mob/living/basic/bot/cleanbot/explode()
var/atom/drop_loc = drop_location()
build_bucket?.forceMove(drop_loc)
new /obj/item/assembly/prox_sensor(drop_loc)
if(weapon)
weapon.force = initial(weapon.force)
weapon.forceMove(drop_loc)
return ..()
/mob/living/basic/bot/cleanbot/update_overlays()
. = ..()
if(isnull(weapon))
return
var/image/knife_overlay = image(icon = weapon.lefthand_file, icon_state = weapon.inhand_icon_state)
. += knife_overlay
// Variables sent to TGUI
/mob/living/basic/bot/cleanbot/ui_data(mob/user)
var/list/data = ..()
if((bot_access_flags & BOT_COVER_LOCKED) && !HAS_SILICON_ACCESS(user))
return data
data["custom_controls"]["clean_blood"] = janitor_mode_flags & CLEANBOT_CLEAN_BLOOD
data["custom_controls"]["clean_trash"] = janitor_mode_flags & CLEANBOT_CLEAN_TRASH
data["custom_controls"]["clean_graffiti"] = janitor_mode_flags & CLEANBOT_CLEAN_DRAWINGS
data["custom_controls"]["pest_control"] = janitor_mode_flags & CLEANBOT_CLEAN_PESTS
return data
// Actions received from TGUI
/mob/living/basic/bot/cleanbot/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
var/mob/user = ui.user
if(. || (bot_access_flags & BOT_COVER_LOCKED) && !HAS_SILICON_ACCESS(user))
return
switch(action)
if("clean_blood")
janitor_mode_flags ^= CLEANBOT_CLEAN_BLOOD
if("pest_control")
janitor_mode_flags ^= CLEANBOT_CLEAN_PESTS
if("clean_trash")
janitor_mode_flags ^= CLEANBOT_CLEAN_TRASH
if("clean_graffiti")
janitor_mode_flags ^= CLEANBOT_CLEAN_DRAWINGS
/mob/living/basic/bot/cleanbot/Destroy()
QDEL_NULL(build_bucket)
QDEL_NULL(our_mop)
GLOB.janitor_devices -= src
return ..()
/mob/living/basic/bot/cleanbot/proc/on_attack_by(datum/source, obj/item/used_item, mob/living/user)
SIGNAL_HANDLER
if(!istype(used_item, /obj/item/knife) || user.combat_mode)
return
INVOKE_ASYNC(src, PROC_REF(attach_knife), user, used_item)
return COMPONENT_NO_AFTERATTACK
/mob/living/basic/bot/cleanbot/proc/attach_knife(mob/living/user, obj/item/used_item)
balloon_alert(user, "attaching knife...")
if(!do_after(user, 2.5 SECONDS, target = src))
return
deputize(used_item, user)
/mob/living/basic/bot/cleanbot/proc/deputize(obj/item/knife, mob/user)
if(!in_range(src, user) || !user.transferItemToLoc(knife, src))
balloon_alert(user, "couldn't attach!")
return FALSE
balloon_alert(user, "attached")
if(!(bot_access_flags & BOT_COVER_EMAGGED))
weapon.force *= 0.5
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
)
AddElement(/datum/element/connect_loc, loc_connections)
return TRUE
/mob/living/basic/bot/cleanbot/proc/update_title(new_job_title)
if(isnull(job_titles[new_job_title]) || (new_job_title in stolen_valor))
return
stolen_valor += new_job_title
if(!HAS_TRAIT(src, TRAIT_COMMISSIONED) && (new_job_title in officers_titles))
ADD_TRAIT(src, TRAIT_COMMISSIONED, INNATE_TRAIT)
var/name_to_add = job_titles[new_job_title]
name = (new_job_title in suffix_job_titles) ? "[name] " + name_to_add : name_to_add + " [name]"
if(length(stolen_valor) == length(job_titles))
ascended = TRUE
/mob/living/basic/bot/cleanbot/proc/on_entered(datum/source, atom/movable/shanked_victim)
SIGNAL_HANDLER
if(!weapon || !has_gravity() || !iscarbon(shanked_victim))
return
var/mob/living/carbon/stabbed_carbon = shanked_victim
if(ishuman(shanked_victim))
var/mob/living/carbon/human/stabbed_human = shanked_victim
var/obj/item/card/id/id = stabbed_human.wear_id?.GetID()
if(!isnull(id))
var/assigned_role = id.assignment
if(!isnull(assigned_role))
update_title(assigned_role)
zone_selected = pick(BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)
INVOKE_ASYNC(weapon, TYPE_PROC_REF(/obj/item, attack), stabbed_carbon, src)
stabbed_carbon.Knockdown(2 SECONDS)
/mob/living/basic/bot/cleanbot/proc/pre_attack(mob/living/source, atom/target, proximity, modifiers)
SIGNAL_HANDLER
if(!proximity || !can_unarmed_attack())
return NONE
if(is_type_in_typecache(target, huntable_pests) && !isnull(our_mop))
INVOKE_ASYNC(our_mop, TYPE_PROC_REF(/obj/item, melee_attack_chain), src, target)
return COMPONENT_CANCEL_ATTACK_CHAIN
if(!(iscarbon(target) && (bot_access_flags & BOT_COVER_EMAGGED)) && !is_type_in_typecache(target, huntable_trash))
return NONE
visible_message(span_danger("[src] sprays hydrofluoric acid at [target]!"))
playsound(src, 'sound/effects/spray2.ogg', 50, TRUE, -6)
target.acid_act(75, 10)
return COMPONENT_CANCEL_ATTACK_CHAIN
/mob/living/basic/bot/cleanbot/proc/generate_ai_keys()
ai_controller.set_blackboard_key(BB_CLEANABLE_DECALS, cleanable_decals)
ai_controller.set_blackboard_key(BB_CLEANABLE_BLOOD, cleanable_blood)
ai_controller.set_blackboard_key(BB_HUNTABLE_PESTS, huntable_pests)
ai_controller.set_blackboard_key(BB_HUNTABLE_TRASH, huntable_trash)
ai_controller.set_blackboard_key(BB_CLEANABLE_DRAWINGS, cleanable_drawings)
ai_controller.set_blackboard_key(BB_CLEANBOT_EMAGGED_PHRASES, emagged_phrases)
/mob/living/basic/bot/cleanbot/autopatrol
bot_mode_flags = BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT | BOT_MODE_ROUNDSTART_POSSESSION
/mob/living/basic/bot/cleanbot/medbay
name = "Scrubs, MD"
req_one_access = list(ACCESS_ROBOTICS, ACCESS_JANITOR, ACCESS_MEDICAL)
bot_mode_flags = ~(BOT_MODE_ON | BOT_MODE_REMOTE_ENABLED)