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

371 lines
14 KiB
Plaintext

///if the ph_meter gives a detailed output
#define DETAILED_CHEM_OUTPUT 1
///if the pH meter gives a shorter output
#define SHORTENED_CHEM_OUTPUT 0
/*
* a pH booklet that contains pH paper pages that will change color depending on the pH of the reagents datum it's attacked onto
*/
/obj/item/ph_booklet
name = "pH indicator booklet"
desc = "A booklet containing paper soaked in universal indicator."
icon_state = "pHbooklet"
icon = 'icons/obj/medical/chemical.dmi'
item_flags = NOBLUDGEON
resistance_flags = FLAMMABLE
w_class = WEIGHT_CLASS_TINY
interaction_flags_mouse_drop = NEED_HANDS
custom_materials = list(/datum/material/paper = HALF_SHEET_MATERIAL_AMOUNT / 2)
///How many pages the booklet holds
var/number_of_pages = 50
//A little janky with pockets
/obj/item/ph_booklet/attack_hand(mob/user)
if(user.get_held_index_of_item(src))//Does this check pockets too..?
if(number_of_pages == 50)
icon_state = "pHbooklet_open"
if(!number_of_pages)
to_chat(user, span_warning("[src] is empty!"))
add_fingerprint(user)
return
var/obj/item/ph_paper/page = new(get_turf(user))
page.add_fingerprint(user)
user.put_in_active_hand(page)
to_chat(user, span_notice("You take [page] out of \the [src]."))
number_of_pages--
playsound(user.loc, 'sound/items/poster/poster_ripped.ogg', 50, TRUE)
add_fingerprint(user)
if(!number_of_pages)
icon_state = "pHbooklet_empty"
return
var/I = user.get_active_held_item()
if(!I)
user.put_in_active_hand(src)
return ..()
/obj/item/ph_booklet/mouse_drop_dragged(atom/over, mob/user, src_location, over_location, params)
if(!isliving(user))
return
if(!number_of_pages)
to_chat(user, span_warning("[src] is empty!"))
add_fingerprint(user)
return
if(number_of_pages == 50)
icon_state = "pHbooklet_open"
var/obj/item/ph_paper/P = new(get_turf(user))
P.add_fingerprint(user)
user.put_in_active_hand(P)
to_chat(user, span_notice("You take [P] out of \the [src]."))
number_of_pages--
playsound(user.loc, 'sound/items/poster/poster_ripped.ogg', 50, TRUE)
add_fingerprint(user)
if(!number_of_pages)
icon_state = "pHbookletEmpty"
/*
* pH paper will change color depending on the pH of the reagents datum it's attacked onto
*/
/obj/item/ph_paper
name = "pH indicator strip"
desc = "A piece of paper that will change colour depending on the pH of a solution."
icon_state = "pHpaper"
icon = 'icons/obj/medical/chemical.dmi'
item_flags = NOBLUDGEON
color = "#f5c352"
resistance_flags = FLAMMABLE
w_class = WEIGHT_CLASS_TINY
///If the paper was used, and therefore cannot change color again
var/used = FALSE
/obj/item/ph_paper/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
if(!is_reagent_container(interacting_with))
return
var/obj/item/reagent_containers/cont = interacting_with
if(!LAZYLEN(cont.reagents.reagent_list))
return NONE
if(used)
to_chat(user, span_warning("[src] has already been used!"))
return ITEM_INTERACT_BLOCKING
CONVERT_PH_TO_COLOR(round(cont.reagents.ph, 1), color)
desc += " The paper looks to be around a pH of [round(cont.reagents.ph, 1)]"
name = "used [name]"
used = TRUE
return ITEM_INTERACT_SUCCESS
/*
* pH meter that will give a detailed or truncated analysis of all the reagents in of an object with a reagents datum attached to it. Only way of detecting purity for now.
*/
/obj/item/ph_meter
name = "Chemical Analyzer"
desc = "An electrode attached to a small circuit box that will display details of a solution. Can be toggled to provide a description of each of the reagents. The screen currently displays nothing."
icon_state = "pHmeter"
icon = 'icons/obj/medical/chemical.dmi'
w_class = WEIGHT_CLASS_TINY
///level of detail for output for the meter
var/scanmode = DETAILED_CHEM_OUTPUT
/obj/item/ph_meter/attack_self(mob/user)
if(scanmode == SHORTENED_CHEM_OUTPUT)
to_chat(user, span_notice("You switch the chemical analyzer to provide a detailed description of each reagent."))
scanmode = DETAILED_CHEM_OUTPUT
else
to_chat(user, span_notice("You switch the chemical analyzer to not include reagent descriptions in its report."))
scanmode = SHORTENED_CHEM_OUTPUT
/obj/item/ph_meter/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
if(!is_reagent_container(interacting_with))
return NONE
var/obj/item/reagent_containers/cont = interacting_with
if(!LAZYLEN(cont.reagents.reagent_list))
return NONE
SEND_SIGNAL(interacting_with, COMSIG_ON_REAGENT_SCAN, user)
var/list/out_message = list()
to_chat(user, "<i>The chemistry meter beeps and displays:</i>")
out_message += "<b>Total volume: [round(cont.volume, 0.01)] Current temperature: [round(cont.reagents.chem_temp, 0.1)]K Total pH: [round(cont.reagents.ph, 0.01)]\n"
out_message += "Chemicals found in [interacting_with.name]:</b>\n"
if(cont.reagents.is_reacting)
out_message += "[span_warning("A reaction appears to be occuring currently.")]<span class='notice'>\n"
for(var/datum/reagent/reagent as anything in cont.reagents.reagent_list) // bloodtyping if blood present in container
var/blood_info = null
if(reagent.data)
var/blood = reagent.data["blood_type"]
if(istype(blood, /datum/blood_type))
var/datum/blood_type/blood_type = blood
var/type = blood_type.get_type()
blood_info = "[blood_type.get_blood_name()] [type ? "(type: [type])" : ""]"
else if(blood)
blood_info = "[reagent.name] (type: [blood])"
if(reagent.purity < reagent.inverse_chem_val && reagent.inverse_chem) //If the reagent is impure
var/datum/reagent/inverse_reagent = GLOB.chemical_reagents_list[reagent.inverse_chem]
out_message += "[span_warning("Inverted reagent detected: ")]<span class='notice'><b>[round(reagent.volume, 0.01)]u of [inverse_reagent.name]</b>, <b>Purity:</b> [round(1 - reagent.purity, 0.000001)*100]%, [(scanmode?"[(inverse_reagent.overdose_threshold?"<b>Overdose:</b> [inverse_reagent.overdose_threshold]u, ":"")]<b>Base pH:</b> [initial(inverse_reagent.ph)], <b>Current pH:</b> [reagent.ph].":"<b>Current pH:</b> [reagent.ph].")]\n"
else
out_message += "<b>[round(reagent.volume, CHEMICAL_VOLUME_ROUNDING)]u of [blood_info || reagent.name]</b>, <b>Purity:</b> [round(reagent.purity, 0.000001)*100]%, [(scanmode?"[(reagent.overdose_threshold?"<b>Overdose:</b> [reagent.overdose_threshold]u, ":"")]<b>Base pH:</b> [initial(reagent.ph)], <b>Current pH:</b> [reagent.ph].":"<b>Current pH:</b> [reagent.ph].")]\n"
if(scanmode)
out_message += "<b>Analysis:</b> [reagent.description]\n"
to_chat(user, boxed_message(span_notice("[out_message.Join()]")))
desc = "An electrode attached to a small circuit box that will display details of a solution. Can be toggled to provide a description of each of the reagents. The screen currently displays detected vol: [round(cont.volume, 0.01)] detected pH:[round(cont.reagents.ph, 0.1)]."
return ITEM_INTERACT_SUCCESS
/obj/item/burner
name = "burner"
desc = "A small table size burner used for heating up beakers."
icon = 'icons/obj/medical/chemical.dmi'
icon_state = "burner"
custom_materials = list(/datum/material/paper = HALF_SHEET_MATERIAL_AMOUNT / 2)
item_flags = NOBLUDGEON
resistance_flags = FLAMMABLE
w_class = WEIGHT_CLASS_TINY
heat = 2000
///If the flame is lit - i.e. if we're processing and burning
var/lit = FALSE
///total reagent volume
var/max_volume = 50
///What the creation reagent is
var/reagent_type = /datum/reagent/consumable/ethanol
/obj/item/burner/Initialize(mapload)
. = ..()
create_reagents(max_volume, TRANSPARENT)//We have our own refillable - since we want to heat and pour
if(reagent_type)
reagents.add_reagent(reagent_type, 15)
/obj/item/burner/grind_results()
return list(/datum/reagent/consumable/ethanol = 5, /datum/reagent/silicon = 10)
/obj/item/burner/attackby(obj/item/I, mob/living/user, list/modifiers, list/attack_modifiers)
. = ..()
if(is_reagent_container(I))
if(lit)
var/obj/item/reagent_containers/container = I
container.reagents.expose_temperature(get_temperature())
to_chat(user, span_notice("You heat up the [I] with the [src]."))
playsound(user.loc, 'sound/effects/chemistry/heatdam.ogg', 50, TRUE)
return
else if(I.is_drainable()) //Transfer FROM it TO us. Special code so it only happens when flame is off.
var/obj/item/reagent_containers/container = I
if(!container.reagents.total_volume)
to_chat(user, span_warning("[container] is empty and can't be poured!"))
return
if(reagents.holder_full())
to_chat(user, span_warning("[src] is full."))
return
var/trans = container.reagents.trans_to(src, container.amount_per_transfer_from_this, transferred_by = user)
to_chat(user, span_notice("You fill [src] with [trans] unit\s of the contents of [container]."))
if(I.heat < 1000)
return
set_lit(TRUE)
user.visible_message(span_notice("[user] lights up the [src]."))
/obj/item/burner/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
if(!lit)
return NONE
if(is_reagent_container(interacting_with))
var/obj/item/reagent_containers/container = interacting_with
container.reagents.expose_temperature(get_temperature())
user.visible_message(span_notice("[user] heats up [src]."), span_notice("You heat up [src]."))
playsound(user, 'sound/effects/chemistry/heatdam.ogg', 50, TRUE)
return ITEM_INTERACT_SUCCESS
else if(isitem(interacting_with))
var/obj/item/item = interacting_with
if(item.get_temperature() >= FIRE_MINIMUM_TEMPERATURE_TO_EXIST)
set_lit(TRUE)
user.visible_message(span_notice("[user] lights up [src]."), span_notice("You light up [src]."))
return ITEM_INTERACT_SUCCESS
return ITEM_INTERACT_BLOCKING
/obj/item/burner/update_icon_state()
. = ..()
icon_state = "[initial(icon_state)][lit ? "-on" : ""]"
/obj/item/burner/proc/set_lit(new_lit)
if(lit == new_lit)
return
lit = new_lit
if(lit)
force = 5
damtype = BURN
hitsound = 'sound/items/tools/welder.ogg'
attack_verb_continuous = string_list(list("burns", "singes"))
attack_verb_simple = string_list(list("burn", "singe"))
START_PROCESSING(SSobj, src)
else
hitsound = SFX_SWING_HIT
force = 0
attack_verb_continuous = null //human_defense.dm takes care of it
attack_verb_simple = null
STOP_PROCESSING(SSobj, src)
set_light_on(lit)
update_icon()
/obj/item/burner/extinguish()
. = ..()
set_lit(FALSE)
/obj/item/burner/attack_self(mob/living/user)
. = ..()
if(.)
return
if(lit)
set_lit(FALSE)
user.visible_message(span_notice("[user] snuffs out [src]'s flame."))
/obj/item/burner/attack(mob/living/carbon/M, mob/living/carbon/user)
if(lit && M.ignite_mob())
message_admins("[ADMIN_LOOKUPFLW(user)] set [key_name_admin(M)] on fire with [src] at [AREACOORD(user)]")
user.log_message("set [key_name(M)] on fire with [src]", LOG_GAME)
M.log_message("was set on fire by [key_name(user)] with [src]", LOG_VICTIM, log_globally = FALSE)
return ..()
/obj/item/burner/process()
var/current_heat = 0
var/number_of_burning_reagents = 0
for(var/datum/reagent/reagent as anything in reagents.reagent_list)
reagent.burn(reagents) //burn can set temperatures of reagents
if(!isnull(reagent.burning_temperature))
current_heat += reagent.burning_temperature
number_of_burning_reagents += 1
reagents.remove_reagent(reagent.type, reagent.burning_volume)
continue
if(!number_of_burning_reagents)
set_lit(FALSE)
heat = 0
return
open_flame()
current_heat /= number_of_burning_reagents
heat = current_heat
/obj/item/burner/get_temperature()
return lit * heat
/obj/item/burner/oil
reagent_type = /datum/reagent/fuel/oil
/obj/item/burner/oil/grind_results()
return list(/datum/reagent/fuel/oil = 5, /datum/reagent/silicon = 10)
/obj/item/burner/fuel
reagent_type = /datum/reagent/fuel
/obj/item/burner/fuel/grind_results()
return list(/datum/reagent/fuel = 5, /datum/reagent/silicon = 10)
/obj/item/thermometer
name = "thermometer"
desc = "A thermometer for checking a something's temperature."
icon_state = "thermometer"
icon = 'icons/obj/medical/chemical.dmi'
item_flags = NOBLUDGEON
w_class = WEIGHT_CLASS_TINY
custom_materials = list(/datum/material/glass = SHEET_MATERIAL_AMOUNT)
///The reagents datum that this object is attached to, so we know where we are when it's added to something.
var/datum/reagents/attached_to_reagents
/obj/item/thermometer/Destroy()
attached_to_reagents = null
return ..()
/obj/item/thermometer/grind_results()
return list(/datum/reagent/mercury = 5)
/obj/item/thermometer/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
if(isnull(interacting_with.reagents))
return NONE
if(!user.transferItemToLoc(src, interacting_with))
return ITEM_INTERACT_BLOCKING
attached_to_reagents = interacting_with.reagents
to_chat(user, span_notice("You add the [src] to [interacting_with]."))
ui_interact(user)
return ITEM_INTERACT_SUCCESS
/obj/item/thermometer/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "Thermometer", name)
ui.open()
/obj/item/thermometer/ui_close(mob/user)
. = ..()
INVOKE_ASYNC(src, PROC_REF(remove_thermometer), user)
/obj/item/thermometer/ui_status(mob/user, datum/ui_state/state)
if(!in_range(src, user))
return UI_CLOSE
return UI_INTERACTIVE
/obj/item/thermometer/ui_state(mob/user)
return GLOB.physical_state
/obj/item/thermometer/ui_data(mob/user)
if(!attached_to_reagents)
ui_close(user)
return
var/list/data = list()
data["Temperature"] = round(attached_to_reagents.chem_temp)
return data
/obj/item/thermometer/proc/remove_thermometer(mob/target)
try_put_in_hand(src, target)
attached_to_reagents = null
/obj/item/thermometer/proc/try_put_in_hand(obj/object, mob/living/user)
to_chat(user, span_notice("You remove the [src] from [attached_to_reagents.my_atom]."))
if(!issilicon(user) && in_range(loc, user))
user.put_in_hands(object)
else
object.forceMove(drop_location())
/obj/item/thermometer/pen
color = "#888888"
custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 0.1)
#undef DETAILED_CHEM_OUTPUT
#undef SHORTENED_CHEM_OUTPUT