From cd167b3173f2b2f24d179b06620e594837302678 Mon Sep 17 00:00:00 2001 From: larentoun <31931237+larentoun@users.noreply.github.com> Date: Wed, 22 Mar 2023 18:34:39 +0300 Subject: [PATCH] feat: Gas Analyzer TGUI (TG port) (#2333) --- .../components/unary_devices/tank.dm | 8 +- code/ATMOSPHERICS/pipes/pipe.dm | 11 +- code/LINDA/LINDA_turf_tile.dm | 3 + code/__DEFINES/atmospherics.dm | 18 +- code/__HELPERS/atmospherics.dm | 51 ++++ code/__HELPERS/unsorted.dm | 28 -- code/game/atoms.dm | 4 + .../atmoalter/portable_atmospherics.dm | 6 +- code/game/machinery/atmoalter/scrubber.dm | 9 +- code/game/mecha/mecha.dm | 5 + code/game/objects/items/devices/scanners.dm | 132 --------- .../items/devices/scanners/gas_analyzer.dm | 269 ++++++++++++++++++ .../objects/items/weapons/flamethrower.dm | 7 +- .../game/objects/items/weapons/tanks/tanks.dm | 6 +- code/modules/mob/dead/observer/observer.dm | 46 +-- code/modules/pda/utilities.dm | 27 +- code/modules/power/singularity/collector.dm | 7 +- paradise.dme | 2 + tgui/packages/common/GasmixParser.js | 64 +++++ tgui/packages/tgui/interfaces/GasAnalyzer.js | 116 ++++++++ tgui/packages/tgui/public/tgui.bundle.js | 6 +- 21 files changed, 558 insertions(+), 267 deletions(-) create mode 100644 code/__HELPERS/atmospherics.dm create mode 100644 code/game/objects/items/devices/scanners/gas_analyzer.dm create mode 100644 tgui/packages/common/GasmixParser.js create mode 100644 tgui/packages/tgui/interfaces/GasAnalyzer.js diff --git a/code/ATMOSPHERICS/components/unary_devices/tank.dm b/code/ATMOSPHERICS/components/unary_devices/tank.dm index fc59de12c92..a39c66a56e8 100644 --- a/code/ATMOSPHERICS/components/unary_devices/tank.dm +++ b/code/ATMOSPHERICS/components/unary_devices/tank.dm @@ -19,12 +19,8 @@ return add_underlay(T, node, dir) -/obj/machinery/atmospherics/unary/tank/attackby(obj/item/W, mob/user, params) - if(istype(W, /obj/item/analyzer)) - atmosanalyzer_scan(air_contents, user) - return - - return ..() +/obj/machinery/atmospherics/unary/tank/return_analyzable_air() + return air_contents /obj/machinery/atmospherics/unary/tank/air name = "Pressure Tank (Air)" diff --git a/code/ATMOSPHERICS/pipes/pipe.dm b/code/ATMOSPHERICS/pipes/pipe.dm index 31a7e94bf17..babe3dad999 100644 --- a/code/ATMOSPHERICS/pipes/pipe.dm +++ b/code/ATMOSPHERICS/pipes/pipe.dm @@ -40,12 +40,6 @@ /obj/machinery/atmospherics/pipe/returnPipenet(obj/machinery/atmospherics/A) return parent -/obj/machinery/atmospherics/pipe/attackby(obj/item/W, mob/user, params) - if(istype(W, /obj/item/analyzer)) - atmosanalyzer_scan(parent.air, user) - return - return ..() - /obj/machinery/atmospherics/proc/pipeline_expansion() return null @@ -66,6 +60,11 @@ return 0 return parent.air +/obj/machinery/atmospherics/pipe/return_analyzable_air() + if(!parent) + return 0 + return parent.air + /obj/machinery/atmospherics/pipe/build_network(remove_deferral = FALSE) if(!parent) parent = new /datum/pipeline() diff --git a/code/LINDA/LINDA_turf_tile.dm b/code/LINDA/LINDA_turf_tile.dm index fa8f498e25c..57c8994ee68 100644 --- a/code/LINDA/LINDA_turf_tile.dm +++ b/code/LINDA/LINDA_turf_tile.dm @@ -26,6 +26,9 @@ return GM +/turf/return_analyzable_air() + return return_air() + /turf/remove_air(amount) var/datum/gas_mixture/GM = new diff --git a/code/__DEFINES/atmospherics.dm b/code/__DEFINES/atmospherics.dm index fba9344f60d..54ece34f899 100644 --- a/code/__DEFINES/atmospherics.dm +++ b/code/__DEFINES/atmospherics.dm @@ -7,12 +7,18 @@ //ATMOS //stuff you should probably leave well alone! -#define R_IDEAL_GAS_EQUATION 8.31 //kPa*L/(K*mol) -#define ONE_ATMOSPHERE 101.325 //kPa -#define TCMB 2.7 // -270.3degC -#define TCRYO 265 // -48.15degC -#define T0C 273.15 // 0degC -#define T20C 293.15 // 20degC +/// kPa*L/(K*mol) +#define R_IDEAL_GAS_EQUATION 8.31 +/// kPa +#define ONE_ATMOSPHERE 101.325 +/// -270.3degC +#define TCMB 2.7 +/// -48.15degC +#define TCRYO 265 +/// 0degC +#define T0C 273.15 +/// 20degC +#define T20C 293.15 #define MOLES_CELLSTANDARD (ONE_ATMOSPHERE*CELL_VOLUME/(T20C*R_IDEAL_GAS_EQUATION)) //moles in a 2.5 m^3 cell at 101.325 Pa and 20 degC #define M_CELL_WITH_RATIO (MOLES_CELLSTANDARD * 0.005) //compared against for superconductivity diff --git a/code/__HELPERS/atmospherics.dm b/code/__HELPERS/atmospherics.dm new file mode 100644 index 00000000000..8a28ea84a0a --- /dev/null +++ b/code/__HELPERS/atmospherics.dm @@ -0,0 +1,51 @@ +/** A simple rudimentary gasmix to information list converter. Can be used for UIs. + * Args: + * * gasmix: [/datum/gas_mixture] + * * name: String used to name the list, optional. + * Returns: A list parsed_gasmixes with the following structure: + * - parsed_gasmixes Value: Assoc List Desc: The thing we return + * -- Key: name Value: String Desc: Gasmix Name + * -- Key: temperature Value: Number Desc: Temperature in kelvins + * -- Key: volume Value: Number Desc: Volume in liters + * -- Key: pressure Value: Number Desc: Pressure in kPa + * -- Key: oxygen Value: Number Desc: Amount of mols of O2 + * -- Key: carbon_dioxide Value: Number Desc: Amount of mols of CO2 + * -- Key: nitrogen Value: Number Desc: Amount of mols of N2 + * -- Key: toxins Value: Number Desc: Amount of mols of plasma + * -- Key: sleeping_agent Value: Number Desc: Amount of mols of N2O + * -- Key: agent_b Value: Number Desc: Amount of mols of agent B + * -- Key: total_moles Value: Number Desc: Total amount of mols in the mixture + * Returned list should always be filled with keys even if value are nulls. + */ + +//TODO: Port gas_mixture_parser from TG +/proc/gas_mixture_parser(datum/gas_mixture/gasmix, name) + . = list( + "oxygen" = null, + "carbon_dioxide" = null, + "nitrogen" = null, + "toxins" = null, + "sleeping_agent" = null, + "agent_b" = null, + "name" = format_text(name), + "total_moles" = null, + "temperature" = null, + "volume"= null, + "pressure"= null, + "heat_capacity" = null, + "thermal_energy" = null, + ) + if(!gasmix) + return + .["oxygen"] = gasmix.oxygen + .["carbon_dioxide"] = gasmix.carbon_dioxide + .["nitrogen"] = gasmix.nitrogen + .["toxins"] = gasmix.toxins + .["sleeping_agent"] = gasmix.sleeping_agent + .["agent_b"] = gasmix.agent_b + .["total_moles"] = gasmix.total_moles() + .["temperature"] = gasmix.temperature + .["volume"] = gasmix.volume + .["pressure"] = gasmix.return_pressure() + .["heat_capacity"] = display_joules(gasmix.heat_capacity()) + .["thermal_energy"] = display_joules(gasmix.thermal_energy()) diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index 92bfd8b5624..b01658cf2fa 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -254,34 +254,6 @@ Turf and target are seperate in case you want to teleport some distance from a t /proc/format_frequency(var/f) return "[round(f / 10)].[f % 10]" -/obj/proc/atmosanalyzer_scan(var/datum/gas_mixture/air_contents, mob/user, var/obj/target = src) - var/obj/icon = target - user.visible_message("[user] has used the analyzer on [target].", "You use the analyzer on [target].") - var/pressure = air_contents.return_pressure() - var/total_moles = air_contents.total_moles() - - user.show_message("Results of analysis of [bicon(icon)] [target].", 1) - if(total_moles>0) - var/o2_concentration = air_contents.oxygen/total_moles - var/n2_concentration = air_contents.nitrogen/total_moles - var/co2_concentration = air_contents.carbon_dioxide/total_moles - var/plasma_concentration = air_contents.toxins/total_moles - - var/unknown_concentration = 1-(o2_concentration+n2_concentration+co2_concentration+plasma_concentration) - - user.show_message("Pressure: [round(pressure,0.1)] kPa", 1) - user.show_message("Nitrogen: [round(n2_concentration*100)] % ([round(air_contents.nitrogen,0.01)] moles)", 1) - user.show_message("Oxygen: [round(o2_concentration*100)] % ([round(air_contents.oxygen,0.01)] moles)", 1) - user.show_message("CO2: [round(co2_concentration*100)] % ([round(air_contents.carbon_dioxide,0.01)] moles)", 1) - user.show_message("Plasma: [round(plasma_concentration*100)] % ([round(air_contents.toxins,0.01)] moles)", 1) - if(unknown_concentration>0.01) - user.show_message("Unknown: [round(unknown_concentration*100)] % ([round(unknown_concentration*total_moles,0.01)] moles)", 1) - user.show_message("Total: [round(total_moles,0.01)] moles", 1) - user.show_message("Temperature: [round(air_contents.temperature-T0C)] °C", 1) - else - user.show_message("[target] is empty!", 1) - return - //Picks a string of symbols to display as the law number for hacked or ion laws /proc/ionnum() return "[pick("!","@","#","$","%","^","&","*")][pick("!","@","#","$","%","^","&","*")][pick("!","@","#","$","%","^","&","*")][pick("!","@","#","$","%","^","&","*")]" diff --git a/code/game/atoms.dm b/code/game/atoms.dm index d651e8907de..77bceb593a6 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -243,6 +243,10 @@ else return null +///Return the air if we can analyze it +/atom/proc/return_analyzable_air() + return null + /atom/proc/check_eye(mob/user) return diff --git a/code/game/machinery/atmoalter/portable_atmospherics.dm b/code/game/machinery/atmoalter/portable_atmospherics.dm index cec04bf4f6f..0c9f460723a 100644 --- a/code/game/machinery/atmoalter/portable_atmospherics.dm +++ b/code/game/machinery/atmoalter/portable_atmospherics.dm @@ -100,6 +100,9 @@ if(holding) . += "\The [src] contains [holding]. Alt-click [src] to remove it." +/obj/machinery/portable_atmospherics/return_analyzable_air() + return air_contents + /obj/machinery/portable_atmospherics/proc/replace_tank(mob/living/user, close_valve, obj/item/tank/new_tank) if(holding) holding.forceMove(drop_location()) @@ -126,9 +129,6 @@ src.holding = T update_icon() return - if((istype(W, /obj/item/analyzer)) && get_dist(user, src) <= 1) - atmosanalyzer_scan(air_contents, user) - return return ..() /obj/machinery/portable_atmospherics/wrench_act(mob/user, obj/item/I) diff --git a/code/game/machinery/atmoalter/scrubber.dm b/code/game/machinery/atmoalter/scrubber.dm index e2356c9840d..d452c35078e 100644 --- a/code/game/machinery/atmoalter/scrubber.dm +++ b/code/game/machinery/atmoalter/scrubber.dm @@ -99,6 +99,9 @@ /obj/machinery/portable_atmospherics/scrubber/return_air() return air_contents +/obj/machinery/portable_atmospherics/scrubber/return_analyzable_air() + return air_contents + /obj/machinery/portable_atmospherics/scrubber/attack_ai(mob/user) add_hiddenprint(user) return attack_hand(user) @@ -185,12 +188,6 @@ else icon_state = "scrubber:0" -/obj/machinery/portable_atmospherics/scrubber/huge/attackby(obj/item/W, mob/user, params) - if((istype(W, /obj/item/analyzer)) && get_dist(user, src) <= 1) - atmosanalyzer_scan(air_contents, user) - return - return ..() - /obj/machinery/portable_atmospherics/scrubber/huge/wrench_act(mob/user, obj/item/I) . = TRUE if(stationary) diff --git a/code/game/mecha/mecha.dm b/code/game/mecha/mecha.dm index 71298c970e6..76ae767ed75 100644 --- a/code/game/mecha/mecha.dm +++ b/code/game/mecha/mecha.dm @@ -1009,6 +1009,11 @@ return cabin_air return get_turf_air() +/obj/mecha/return_analyzable_air() + if(use_internal_tank) + return cabin_air + return null + /obj/mecha/proc/return_pressure() var/datum/gas_mixture/t_air = return_air() if(t_air) diff --git a/code/game/objects/items/devices/scanners.dm b/code/game/objects/items/devices/scanners.dm index 0e2441a3c97..ae090e80dde 100644 --- a/code/game/objects/items/devices/scanners.dm +++ b/code/game/objects/items/devices/scanners.dm @@ -3,7 +3,6 @@ CONTAINS: T-RAY DETECTIVE SCANNER HEALTH ANALYZER -GAS ANALYZER PLANT ANALYZER REAGENT SCANNER */ @@ -585,137 +584,6 @@ REAGENT SCANNER origin_tech = "magnets=2;biotech=2" usesound = 'sound/items/deconstruct.ogg' -/obj/item/analyzer - desc = "A hand-held environmental scanner which reports current gas levels." - name = "analyzer" - icon = 'icons/obj/device.dmi' - icon_state = "atmos" - item_state = "analyzer" - w_class = WEIGHT_CLASS_SMALL - flags = CONDUCT - slot_flags = SLOT_BELT - throwforce = 0 - throw_speed = 3 - throw_range = 7 - materials = list(MAT_METAL=30, MAT_GLASS=20) - origin_tech = "magnets=1;engineering=1" - var/cooldown = FALSE - var/cooldown_time = 250 - var/accuracy // 0 is the best accuracy. - -/obj/item/analyzer/examine(mob/user) - . = ..() - . += "Alt-click [src] to activate the barometer function." - -/obj/item/analyzer/attack_self(mob/user as mob) - - if(user.stat) - return - - var/turf/location = user.loc - if(!( istype(location, /turf) )) - return - - var/datum/gas_mixture/environment = location.return_air() - - var/pressure = environment.return_pressure() - var/total_moles = environment.total_moles() - - to_chat(user, "Results:") - if(abs(pressure - ONE_ATMOSPHERE) < 10) - to_chat(user, "Pressure: [round(pressure,0.1)] kPa") - else - to_chat(user, "Pressure: [round(pressure,0.1)] kPa") - if(total_moles) - var/o2_concentration = environment.oxygen/total_moles - var/n2_concentration = environment.nitrogen/total_moles - var/co2_concentration = environment.carbon_dioxide/total_moles - var/plasma_concentration = environment.toxins/total_moles - - var/unknown_concentration = 1-(o2_concentration+n2_concentration+co2_concentration+plasma_concentration) - if(abs(n2_concentration - N2STANDARD) < 20) - to_chat(user, "Nitrogen: [round(n2_concentration*100)] %") - else - to_chat(user, "Nitrogen: [round(n2_concentration*100)] %") - - if(abs(o2_concentration - O2STANDARD) < 2) - to_chat(user, "Oxygen: [round(o2_concentration*100)] %") - else - to_chat(user, "Oxygen: [round(o2_concentration*100)] %") - - if(co2_concentration > 0.01) - to_chat(user, "CO2: [round(co2_concentration*100)] %") - else - to_chat(user, "CO2: [round(co2_concentration*100)] %") - - if(plasma_concentration > 0.01) - to_chat(user, "Plasma: [round(plasma_concentration*100)] %") - - if(unknown_concentration > 0.01) - to_chat(user, "Unknown: [round(unknown_concentration*100)] %") - - to_chat(user, "Temperature: [round(environment.temperature-T0C)] °C") - - add_fingerprint(user) - -/obj/item/analyzer/AltClick(mob/living/user) //Barometer output for measuring when the next storm happens - if(!istype(user) || user.incapacitated()) - to_chat(user, "You can't do that right now!") - return - if(!Adjacent(user)) - return - if(cooldown) - to_chat(user, "[src]'s barometer function is prepraring itself.") - return - var/turf/T = get_turf(user) - if(!T) - return - playsound(src, 'sound/effects/pop.ogg', 100) - var/area/user_area = T.loc - var/datum/weather/ongoing_weather = null - if(!user_area.outdoors) - to_chat(user, "[src]'s barometer function won't work indoors!") - return - for(var/V in SSweather.processing) - var/datum/weather/W = V - if(W.barometer_predictable && (T.z in W.impacted_z_levels) && W.area_type == user_area.type && !(W.stage == END_STAGE)) - ongoing_weather = W - break - if(ongoing_weather) - if((ongoing_weather.stage == MAIN_STAGE) || (ongoing_weather.stage == WIND_DOWN_STAGE)) - to_chat(user, "[src]'s barometer function can't trace anything while the storm is [ongoing_weather.stage == MAIN_STAGE ? "already here!" : "winding down."]") - return - to_chat(user, "The next [ongoing_weather] will hit in [butchertime(ongoing_weather.next_hit_time - world.time)].") - if(ongoing_weather.aesthetic) - to_chat(user, "[src]'s barometer function says that the next storm will breeze on by.") - else - var/next_hit = SSweather.next_hit_by_zlevel["[T.z]"] - var/fixed = next_hit ? next_hit - world.time : -1 - if(fixed < 0) - to_chat(user, "[src]'s barometer function was unable to trace any weather patterns.") - else - to_chat(user, "[src]'s barometer function says a storm will land in approximately [butchertime(fixed)].") - cooldown = TRUE - addtimer(CALLBACK(src,/obj/item/analyzer/proc/ping), cooldown_time) - -/obj/item/analyzer/proc/ping() - if(isliving(loc)) - var/mob/living/L = loc - to_chat(L, "[src]'s barometer function is ready!") - playsound(src, 'sound/machines/click.ogg', 100) - cooldown = FALSE - -/obj/item/analyzer/proc/butchertime(amount) - if(!amount) - return - if(accuracy) - var/inaccurate = round(accuracy * (1 / 3)) - if(prob(50)) - amount -= inaccurate - if(prob(50)) - amount += inaccurate - return DisplayTimeText(max(1, amount)) - /obj/item/reagent_scanner name = "reagent scanner" desc = "A hand-held reagent scanner which identifies chemical agents and blood types." diff --git a/code/game/objects/items/devices/scanners/gas_analyzer.dm b/code/game/objects/items/devices/scanners/gas_analyzer.dm new file mode 100644 index 00000000000..d90a07562a4 --- /dev/null +++ b/code/game/objects/items/devices/scanners/gas_analyzer.dm @@ -0,0 +1,269 @@ +#define ANALYZER_MODE_SURROUNDINGS 0 +#define ANALYZER_MODE_TARGET 1 +#define ANALYZER_HISTORY_SIZE 30 +#define ANALYZER_HISTORY_MODE_KPA "kpa" +#define ANALYZER_HISTORY_MODE_MOL "mol" + +/obj/item/analyzer + desc = "A hand-held environmental scanner which reports current gas levels." + name = "analyzer" + icon = 'icons/obj/device.dmi' + icon_state = "atmos" + item_state = "analyzer" + w_class = WEIGHT_CLASS_SMALL + flags = CONDUCT + slot_flags = SLOT_BELT + throwforce = 0 + throw_speed = 3 + throw_range = 7 + materials = list(MAT_METAL=30, MAT_GLASS=20) + origin_tech = "magnets=1;engineering=1" + var/cooldown = FALSE + var/cooldown_time = 250 + var/accuracy // 0 is the best accuracy. + var/list/last_gasmix_data + var/list/history_gasmix_data + var/history_gasmix_index = 0 + var/history_view_mode = ANALYZER_HISTORY_MODE_KPA + var/scan_range = 1 + var/auto_updating = TRUE + var/target_mode = ANALYZER_MODE_SURROUNDINGS + var/atom/scan_target + +/obj/item/analyzer/examine(mob/user) + . = ..() + . += span_notice("To scan an environment, activate it or use it on your location.") + . += span_notice("Alt-click [src] to activate the barometer function.") + +/obj/item/analyzer/suicide_act(mob/living/carbon/user) + user.visible_message(span_suicide("[user] begins to analyze [user.p_them()]self with [src]! The display shows that [user.p_theyre()] dead!")) + return BRUTELOSS + +/obj/item/analyzer/AltClick(mob/living/user) //Barometer output for measuring when the next storm happens + if(!istype(user) || user.incapacitated()) + to_chat(user, "You can't do that right now!") + return + if(!Adjacent(user)) + return + if(cooldown) + to_chat(user, "[src]'s barometer function is prepraring itself.") + return + var/turf/T = get_turf(user) + if(!T) + return + playsound(src, 'sound/effects/pop.ogg', 100) + var/area/user_area = T.loc + var/datum/weather/ongoing_weather = null + if(!user_area.outdoors) + to_chat(user, "[src]'s barometer function won't work indoors!") + return + for(var/V in SSweather.processing) + var/datum/weather/W = V + if(W.barometer_predictable && (T.z in W.impacted_z_levels) && W.area_type == user_area.type && !(W.stage == END_STAGE)) + ongoing_weather = W + break + if(ongoing_weather) + if((ongoing_weather.stage == MAIN_STAGE) || (ongoing_weather.stage == WIND_DOWN_STAGE)) + to_chat(user, "[src]'s barometer function can't trace anything while the storm is [ongoing_weather.stage == MAIN_STAGE ? "already here!" : "winding down."]") + return + to_chat(user, "The next [ongoing_weather] will hit in [butchertime(ongoing_weather.next_hit_time - world.time)].") + if(ongoing_weather.aesthetic) + to_chat(user, "[src]'s barometer function says that the next storm will breeze on by.") + else + var/next_hit = SSweather.next_hit_by_zlevel["[T.z]"] + var/fixed = next_hit ? next_hit - world.time : -1 + if(fixed < 0) + to_chat(user, "[src]'s barometer function was unable to trace any weather patterns.") + else + to_chat(user, "[src]'s barometer function says a storm will land in approximately [butchertime(fixed)].") + cooldown = TRUE + addtimer(CALLBACK(src,/obj/item/analyzer/proc/ping), cooldown_time) + +/obj/item/analyzer/proc/ping() + if(isliving(loc)) + var/mob/living/L = loc + to_chat(L, "[src]'s barometer function is ready!") + playsound(src, 'sound/machines/click.ogg', 100) + cooldown = FALSE + +/// Applies the barometer inaccuracy to the gas reading. +/obj/item/analyzer/proc/butchertime(amount) + if(!amount) + return + if(accuracy) + var/inaccurate = round(accuracy * (1 / 3)) + if(prob(50)) + amount -= inaccurate + if(prob(50)) + amount += inaccurate + return DisplayTimeText(max(1, amount)) + +/obj/item/analyzer/ui_interact(mob/user, ui_key, datum/tgui/ui, force_open, datum/tgui/master_ui, datum/ui_state/state) + ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open) + if(!ui) + ui = new(user, src, ui_key, "GasAnalyzer", name, 500, 500, master_ui, state) + ui.open() + +/obj/item/analyzer/ui_data(mob/user) + var/list/data = list() + if(auto_updating) + on_analyze(source=src, target=scan_target) + LAZYINITLIST(last_gasmix_data) + LAZYINITLIST(history_gasmix_data) + data["gasmixes"] = last_gasmix_data + data["autoUpdating"] = auto_updating + data["historyGasmixes"] = history_gasmix_data + data["historyViewMode"] = history_view_mode + data["historyIndex"] = history_gasmix_index + return data + +/obj/item/analyzer/ui_act(action, list/params) + . = ..() + if(.) + return + switch(action) + if("autoscantoggle") + auto_updating = !auto_updating + return TRUE + if("input") + if(!length(history_gasmix_data)) + return TRUE + var/target = text2num(params["target"]) + auto_updating = FALSE + history_gasmix_index = target + last_gasmix_data = history_gasmix_data[history_gasmix_index] + return TRUE + if("clearhistory") + history_gasmix_data = list() + return TRUE + if("modekpa") + history_view_mode = ANALYZER_HISTORY_MODE_KPA + return TRUE + if("modemol") + history_view_mode = ANALYZER_HISTORY_MODE_MOL + return TRUE + +/// Called when our analyzer is used on something +/obj/item/analyzer/proc/on_analyze(datum/source, atom/target, save_data=TRUE) + SIGNAL_HANDLER + LAZYINITLIST(history_gasmix_data) + switch(target_mode) + if(ANALYZER_MODE_SURROUNDINGS) + scan_target = get_turf(src) + if(ANALYZER_MODE_TARGET) + scan_target = target + if(!can_see(src, target, scan_range)) + target_mode = ANALYZER_MODE_SURROUNDINGS + scan_target = get_turf(src) + if(!scan_target) + target_mode = ANALYZER_MODE_SURROUNDINGS + scan_target = get_turf(src) + + var/mixture = scan_target.return_analyzable_air() + if(!mixture) + return FALSE + var/list/airs = islist(mixture) ? mixture : list(mixture) + var/list/new_gasmix_data = list() + for(var/datum/gas_mixture/air as anything in airs) + var/mix_name = capitalize(lowertext(scan_target.name)) + if(scan_target == get_turf(src)) + mix_name = "Location Reading" + if(airs.len != 1) //not a unary gas mixture + mix_name += " - Node [airs.Find(air)]" + new_gasmix_data += list(gas_mixture_parser(air, mix_name)) + last_gasmix_data = new_gasmix_data + history_gasmix_index = 0 + if(save_data) + if(length(history_gasmix_data) >= ANALYZER_HISTORY_SIZE) + history_gasmix_data.Cut(ANALYZER_HISTORY_SIZE, length(history_gasmix_data) + 1) + history_gasmix_data.Insert(1, list(new_gasmix_data)) + +/obj/item/analyzer/attack_self(mob/user) + if(user.stat != CONSCIOUS) + return + target_mode = ANALYZER_MODE_SURROUNDINGS + atmos_scan(user=user, target=get_turf(src), silent=FALSE, print=FALSE) + on_analyze(source=user, target=get_turf(src), save_data=!auto_updating) + ui_interact(user) + add_fingerprint(user) + +/obj/item/analyzer/afterattack(atom/target, mob/user, proximity, params) + . = ..() + if(!can_see(user, target, scan_range)) + return + target_mode = ANALYZER_MODE_TARGET + if(target == user || target == user.loc) + target_mode = ANALYZER_MODE_SURROUNDINGS + atmos_scan(user=user, target=(target.return_analyzable_air() ? target : get_turf(src)), print=FALSE) + on_analyze(source=user, target=(target.return_analyzable_air() ? target : get_turf(src)), save_data=!auto_updating) + ui_interact(user) + +/** + * Outputs a message to the user describing the target's gasmixes. + * + * Gets called by analyzer_act, which in turn is called by tool_act. + * Also used in other chat-based gas scans. + */ +/proc/atmos_scan(mob/user, atom/target, silent=FALSE, print=TRUE) + var/mixture = target.return_analyzable_air() + if(!mixture) + return FALSE + + var/icon = target + var/message = list() + if(!silent && isliving(user)) + user.visible_message(span_notice("[user] uses the analyzer on [bicon(icon)] [target]."), span_notice("You use the analyzer on [bicon(icon)] [target]")) + message += span_boldnotice("Results of analysis of [bicon(icon)] [target].") + + if(!print) + return TRUE + + var/list/airs = islist(mixture) ? mixture : list(mixture) + for(var/datum/gas_mixture/air as anything in airs) + var/mix_name = capitalize(lowertext(target.name)) + if(airs.len > 1) //not a unary gas mixture + var/mix_number = airs.Find(air) + message += span_boldnotice("Node [mix_number]") + mix_name += " - Node [mix_number]" + + var/total_moles = air.total_moles() + var/pressure = air.return_pressure() + var/volume = air.return_volume() //could just do mixture.volume... but safety, I guess? + var/temperature = air.return_temperature() + var/heat_capacity = air.heat_capacity() + var/thermal_energy = air.thermal_energy() + + //TODO: Port gas mixtures from TG + if(total_moles > 0) + message += span_notice("Moles: [round(total_moles, 0.01)] mol") + if(air.oxygen) + message += span_notice("Oxygen: [round(air.oxygen, 0.01)] mol ([round(air.oxygen / total_moles*100, 0.01)] %)") + if(air.carbon_dioxide) + message += span_notice("Carbon Dioxide: [round(air.carbon_dioxide, 0.01)] mol ([round(air.carbon_dioxide / total_moles*100, 0.01)] %)") + if(air.nitrogen) + message += span_notice("Nitrogen: [round(air.nitrogen, 0.01)] mol ([round(air.nitrogen / total_moles*100, 0.01)] %)") + if(air.toxins) + message += span_notice("Plasma: [round(air.toxins, 0.01)] mol ([round(air.toxins / total_moles*100, 0.01)] %)") + if(air.sleeping_agent) + message += span_notice("Nitrous Oxide: [round(air.sleeping_agent, 0.01)] mol ([round(air.sleeping_agent / total_moles*100, 0.01)] %)") + if(air.agent_b) + message += span_notice("Agent B: [round(air.agent_b, 0.01)] mol ([round(air.agent_b / total_moles*100, 0.01)] %)") + + message += span_notice("Temperature: [round(temperature - T0C,0.01)] °C ([round(temperature, 0.01)] K)") + message += span_notice("Volume: [volume] L") + message += span_notice("Pressure: [round(pressure, 0.01)] kPa") + message += span_notice("Heat Capacity: [display_joules(heat_capacity)] / K") + message += span_notice("Thermal Energy: [display_joules(thermal_energy)]") + else + message += airs.len > 1 ? span_notice("This node is empty!") : span_notice("[target] is empty!") + message += span_notice("Volume: [volume] L") // don't want to change the order volume appears in, suck it + + // we let the join apply newlines so we do need handholding + to_chat(user, ("