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

383 lines
14 KiB
Plaintext

/obj/machinery/rnd/experimentor
name = "\improper E.X.P.E.R.I-MENTOR"
desc = "Experimental Xeon Particle Entropy Reaction Infuser or something like that. Nanotrasen's new reaction infuser, with a slight less tendency to catastrophically fail than the previous model... or so they say."
icon = 'icons/obj/machines/experimentator.dmi'
icon_state = "h_lathe"
base_icon_state = "h_lathe"
density = TRUE
use_power = IDLE_POWER_USE
circuit = /obj/item/circuitboard/machine/experimentor
interaction_flags_machine = INTERACT_MACHINE_WIRES_IF_OPEN | INTERACT_MACHINE_ALLOW_SILICON | INTERACT_MACHINE_OPEN_SILICON
/// Weakref to the station Ian the corgi (or whichever we can find)
var/datum/weakref/tracked_ian_ref
/// Weakref to the station Runtime the cat (or whichever we can find)
var/datum/weakref/tracked_runtime_ref
/// The current probability of a malfunction
var/malfunction_probability_coeff = 0
/// How many times we've had a critical reaction
var/critical_malfunction_counter = 0
/// How long is the cooldown between experiments
var/cooldown = EXPERIMENT_COOLDOWN_BASE
/// List of experiment handler datums by scantype
var/list/experimentor_result_handlers = list()
/// Reactions that can occur for specific items
var/list/item_reactions = list()
/// Items that we can get by transforming
var/static/list/valid_items = list()
/// Items that will cause critical reactions
var/static/list/critical_items_typecache
/// Items that the machine shouldn't interact with
var/static/list/banned_typecache
COOLDOWN_DECLARE(run_experiment)
/obj/machinery/rnd/experimentor/Initialize(mapload)
. = ..()
set_wires(new /datum/wires/experimentor(src))
load_handlers()
tracked_ian_ref = WEAKREF(locate(/mob/living/basic/pet/dog/corgi/ian) in GLOB.mob_living_list)
tracked_runtime_ref = WEAKREF(locate(/mob/living/basic/pet/cat/runtime) in GLOB.mob_living_list)
if(!critical_items_typecache)
critical_items_typecache = typecacheof(list(
/obj/item/construction/rcd,
/obj/item/grenade,
/obj/item/aicard,
/obj/item/slime_extract,
/obj/item/transfer_valve,
))
if(!banned_typecache)
banned_typecache = typecacheof(list(
/obj/item/stock_parts/power_store/cell/infinite,
/obj/item/stock_parts/power_store/cell/ethereal,
/obj/item/grenade/chem_grenade/tuberculosis,
))
if(!length(valid_items))
for(var/obj/item/item_path as anything in valid_subtypesof(/obj/item))
if(ispath(item_path, /obj/item/stock_parts) || ispath(item_path, /obj/item/grenade/chem_grenade) || ispath(item_path, /obj/item/knife))
valid_items["[item_path]"] += 15
if(ispath(item_path, /obj/item/food))
valid_items["[item_path]"] += rand(1,4)
/obj/machinery/rnd/experimentor/proc/load_handlers()
for(var/datum/experimentor_result_handler/scan/handler_type as anything in valid_subtypesof(/datum/experimentor_result_handler/scan))
var/datum/experimentor_result_handler/scan/handler = new handler_type()
experimentor_result_handlers[handler.scantype] = handler
experimentor_result_handlers[FAIL] = new /datum/experimentor_result_handler/fail
/obj/machinery/rnd/experimentor/proc/get_available_reactions()
var/list/all_reactions = list()
for(var/scantype in experimentor_result_handlers)
var/datum/experimentor_result_handler/handler = experimentor_result_handlers[scantype]
if(handler.is_special)
continue
all_reactions += scantype
return all_reactions
/obj/machinery/rnd/experimentor/proc/is_special_reaction(reaction_type)
var/datum/experimentor_result_handler/handler = experimentor_result_handlers[reaction_type]
return handler.is_special
/obj/machinery/rnd/experimentor/proc/run_experiment(experiment_type)
if(!loaded_item)
return
icon_state = "[base_icon_state]_wloop"
var/datum/experimentor_result_handler/handler = experimentor_result_handlers[experiment_type]
if(handler)
handler.execute(src, loaded_item)
else
fail_experiment()
handle_global_reactions(loaded_item)
update_appearance()
COOLDOWN_START(src, run_experiment, cooldown)
/obj/machinery/rnd/experimentor/proc/show_start_message(message, type)
switch(type)
if(MSG_TYPE_NOTICE)
visible_message(span_notice("[src] [message]"))
if(MSG_TYPE_WARNING)
visible_message(span_warning("[src] [message]"))
if(MSG_TYPE_DANGER)
visible_message(span_danger("[src] [message]"))
else
visible_message(span_notice("[src] [message]"))
/obj/machinery/rnd/experimentor/proc/is_critical_reaction()
return is_type_in_typecache(loaded_item, critical_items_typecache)
/obj/machinery/rnd/experimentor/proc/get_malfunction_chance()
return (100 - malfunction_probability_coeff) * 0.01
/obj/machinery/rnd/experimentor/proc/fail_experiment()
var/datum/experimentor_result_handler/fail_handler = experimentor_result_handlers[FAIL]
if(fail_handler)
fail_handler.execute(src, loaded_item)
/obj/machinery/rnd/experimentor/proc/try_generate_reaction_for_item(obj/item/some_item)
if(is_type_in_typecache(some_item.type, banned_typecache) || item_reactions["[some_item.type]"])
return
if(istype(some_item, /obj/item/relic))
item_reactions["[some_item.type]"] = SCANTYPE_DISCOVER
else
item_reactions["[some_item.type]"] = pick(get_available_reactions())
/obj/machinery/rnd/experimentor/RefreshParts()
. = ..()
for(var/datum/stock_part/servo/servo in component_parts)
cooldown = clamp((EXPERIMENT_COOLDOWN_BASE / servo.tier), 0.5, 2)
for(var/datum/stock_part/scanning_module/scanning_module in component_parts)
malfunction_probability_coeff += scanning_module.tier * 2
for(var/datum/stock_part/micro_laser/micro_laser in component_parts)
malfunction_probability_coeff += micro_laser.tier
/obj/machinery/rnd/experimentor/examine(mob/user)
. = ..()
if(in_range(user, src) || isobserver(user))
. += span_notice("The status display reads:<br>")
. += span_notice("Malfunction probability reduced by [span_bold("[malfunction_probability_coeff]")].")
. += span_notice("Cooldown interval between experiments at [span_bold("[cooldown]")] seconds.")
/obj/machinery/rnd/experimentor/on_deconstruction(disassembled)
. = ..()
item_eject()
/obj/machinery/rnd/experimentor/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new (user, src, "Experimentor")
ui.open()
/obj/machinery/rnd/experimentor/ui_data(mob/user)
var/list/data = list()
data["hasItem"] = !!loaded_item
data["isOnCooldown"] = !COOLDOWN_FINISHED(src, run_experiment)
data["isServerConnected"] = !!stored_research
var/list/available_experiments = list()
for(var/scantype in experimentor_result_handlers)
if(scantype == FAIL)
continue
var/datum/experimentor_result_handler/scan/handler = experimentor_result_handlers[scantype]
var/is_available = TRUE
var/is_discover = (scantype == SCANTYPE_DISCOVER)
if(loaded_item)
if(istype(loaded_item, /obj/item/relic))
is_available = is_discover
else
is_available = !is_discover
available_experiments += list(list(
"id" = scantype,
"name" = handler.name || capitalize(scantype),
"fa_icon" = handler.fa_icon || "question",
"isAvailable" = is_available,
"isDiscover" = is_discover
))
data["availableExperiments"] = available_experiments
if(!isnull(loaded_item))
var/list/item_data = list()
item_data["name"] = loaded_item.name
item_data["icon"] = icon2base64(getFlatIcon(loaded_item, no_anim = TRUE))
item_data["isRelic"] = istype(loaded_item, /obj/item/relic)
item_data["associatedNodes"] = list()
var/list/unlockable_nodes = techweb_item_unlock_check(loaded_item)
for(var/node_id in unlockable_nodes)
var/datum/techweb_node/node = SSresearch.techweb_node_by_id(node_id)
item_data["associatedNodes"] += list(list(
"name" = node.display_name,
"isUnlocked" = !(node_id in stored_research.hidden_nodes),
))
data["loadedItem"] = item_data
return data
/obj/machinery/rnd/experimentor/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
switch(action)
if("eject")
item_eject()
return TRUE
if("experiment")
var/reaction = params["id"]
if(isnull(reaction))
return
try_perform_experiment(reaction)
return TRUE
/obj/machinery/rnd/experimentor/proc/item_eject(should_clone = FALSE)
if(isnull(loaded_item))
return
var/atom/drop_atom = get_step(src, EAST) || drop_location()
if(should_clone)
visible_message(span_notice("A duplicate of \the [loaded_item] pops out!"))
new loaded_item.type(drop_atom)
return
loaded_item.forceMove(drop_atom)
loaded_item = null
/obj/machinery/rnd/experimentor/proc/match_reaction(obj/item/matching, target_reaction)
PRIVATE_PROC(TRUE)
if(isnull(matching) || isnull(target_reaction) || target_reaction == SCANTYPE_DISCOVER)
return FAIL
if(item_reactions["[matching.type]"] == target_reaction)
return item_reactions["[matching.type]"]
return FAIL
/obj/machinery/rnd/experimentor/proc/try_perform_experiment(reaction)
if(!stored_research || !loaded_item || !COOLDOWN_FINISHED(src, run_experiment))
return FALSE
if(istype(loaded_item, /obj/item/relic))
reaction = SCANTYPE_DISCOVER
else
reaction = match_reaction(loaded_item, reaction)
if(reaction != FAIL)
var/picked_node_id = pick(techweb_item_unlock_check(loaded_item))
stored_research.unhide_node(SSresearch.techweb_node_by_id(picked_node_id))
run_experiment(reaction)
use_energy(750 JOULES)
/obj/machinery/rnd/experimentor/proc/handle_global_reactions()
if(!prob(EFFECT_PROBABILITY * (100 - malfunction_probability_coeff) * 0.01) || !loaded_item)
return
var/malf_coefficient = rand(1, 100)
switch(malf_coefficient)
if(1 to 15)
visible_message(span_warning("[src]'s onboard detection system has malfunctioned!"))
item_reactions["[loaded_item.type]"] = pick(get_available_reactions())
item_eject()
if(16 to 35)
visible_message(span_warning("[src] melts [loaded_item], ian-izing the air around it!"))
do_smoke(1, src, src)
var/mob/living/tracked_ian = tracked_ian_ref?.resolve()
if(tracked_ian)
do_smoke(1, src, tracked_ian.loc)
tracked_ian.forceMove(loc)
investigate_log("Experimentor has stolen Ian!", INVESTIGATE_EXPERIMENTOR)
else
new /mob/living/basic/pet/dog/corgi(loc)
investigate_log("Experimentor has spawned a new corgi.", INVESTIGATE_EXPERIMENTOR)
if(36 to 50)
visible_message(span_warning("Experimentor draws the life essence of those nearby!"))
for(var/mob/living/m in view(4,src))
to_chat(m, span_danger("You feel your flesh being torn from you, mists of blood drifting to [src]!"))
playsound(src, pick('sound/effects/curse/curse1.ogg', 'sound/effects/curse/curse2.ogg', 'sound/effects/curse/curse3.ogg'), 30)
m.apply_damage(50, BRUTE, BODY_ZONE_CHEST)
investigate_log("Experimentor has taken 50 brute a blood sacrifice from [m]", INVESTIGATE_EXPERIMENTOR)
if(51 to 75)
visible_message(span_warning("[src] encounters a run-time error!"))
do_smoke(1, src, src)
var/mob/living/tracked_runtime = tracked_runtime_ref?.resolve()
if(tracked_runtime)
do_smoke(1, src, tracked_runtime.loc)
tracked_runtime.forceMove(drop_location())
investigate_log("Experimentor has stolen Runtime!", INVESTIGATE_EXPERIMENTOR)
else
new /mob/living/basic/pet/cat(loc)
investigate_log("Experimentor failed to steal Runtime the cat and instead spawned a new cat.", INVESTIGATE_EXPERIMENTOR)
if(76 to 98)
visible_message(span_warning("[src] emits a low hum."))
do_sparks(3, FALSE, src, src)
use_energy(500 KILO JOULES)
investigate_log("Experimentor has drained power from its APC", INVESTIGATE_EXPERIMENTOR)
if(99)
visible_message(span_warning("[src] begins to glow and vibrate. It's going to blow!"))
addtimer(CALLBACK(src, PROC_REF(boom)), 5 SECONDS)
if(100)
visible_message(span_warning("[src] begins to glow and vibrate. It's going to blow!"))
addtimer(CALLBACK(src, PROC_REF(honk)), 5 SECONDS)
/obj/machinery/rnd/experimentor/proc/boom()
explosion(src, devastation_range = 1, heavy_impact_range = 5, light_impact_range = 10, flash_range = 5, adminlog = TRUE)
/obj/machinery/rnd/experimentor/proc/honk()
playsound(src, 'sound/items/bikehorn.ogg', 500)
new /obj/item/grown/bananapeel(loc)
/obj/machinery/rnd/experimentor/item_interaction(mob/living/user, obj/item/weapon, list/modifiers)
if(user.combat_mode)
return NONE
if(panel_open && is_wire_tool(weapon))
wires.interact(user)
return ITEM_INTERACT_SUCCESS
if(!is_insertion_ready(user))
return ITEM_INTERACT_BLOCKING
if(!user.transferItemToLoc(weapon, src))
to_chat(user, span_warning("\The [weapon] is stuck to your hand!"))
return ITEM_INTERACT_BLOCKING
loaded_item = weapon
to_chat(user, span_notice("You put [weapon] in the chamber."))
flick("h_lathe_load", src)
try_generate_reaction_for_item(loaded_item)
return ITEM_INTERACT_SUCCESS
/obj/machinery/rnd/experimentor/attack_hand_secondary(mob/user, list/modifiers)
if(!panel_open)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
wires.interact(user)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/obj/machinery/rnd/experimentor/screwdriver_act_secondary(mob/living/user, obj/item/tool)
return default_deconstruction_screwdriver(user, tool)
/obj/machinery/rnd/experimentor/multitool_act(mob/living/user, obj/item/tool)
if(panel_open)
wires.interact(user)
return ITEM_INTERACT_SUCCESS
return NONE
/obj/machinery/rnd/experimentor/wirecutter_act(mob/living/user, obj/item/tool)
if(panel_open)
wires.interact(user)
return ITEM_INTERACT_SUCCESS
return NONE
/obj/machinery/rnd/experimentor/proc/warn_admins(user, ReactionName)
var/turf/T = get_turf(user)
message_admins("Experimentor reaction: [ReactionName] generated by [ADMIN_LOOKUPFLW(user)] at [ADMIN_VERBOSEJMP(T)]")
log_game("Experimentor reaction: [ReactionName] generated by [key_name(user)] in [AREACOORD(T)]")