From 95dac4990d3e2375a9c103b331499ed6522157c7 Mon Sep 17 00:00:00 2001 From: Casper3667 <8396443+Casper3667@users.noreply.github.com> Date: Mon, 18 May 2026 23:46:09 +0200 Subject: [PATCH] Updates the health analyzer to use TGUI (#22421) - rscadd: "Health analyzers now use a TGUI instead of outputting to the chat." - rscadd: "The handheld health analyzer now take 1.5 seconds to scan a person." - rscadd: "It is now possible to pull up the last scan on the handheld health analyzer." - rscdel: "It is no longer possible to switch limb mode on the handheld health analyzer." Beyond the above, device levels were added, primarily as a concept though it is not implemented in full, so all the ingame health analyzers still show the same data as before. image image image image --- aurorastation.dme | 1 + .../components/medical/medicalAnalyzer.dm | 370 ++++++++++++++++++ .../skills/medical/anatomy_skill_component.dm | 2 +- code/game/objects/items/devices/scanners.dm | 314 +-------------- .../heavy_vehicle/equipment/medical.dm | 6 +- .../mob/abstract/ghost/observer/observer.dm | 7 +- .../mob/living/simple_animal/borer/borer.dm | 1 + .../simple_animal/borer/borer_powers.dm | 5 +- .../computers/modular_computer/core.dm | 1 + .../computers/modular_computer/interaction.dm | 9 +- .../augment/augments/health_analyzer.dm | 9 +- code/modules/organs/subtypes/autakh.dm | 9 +- code/modules/organs/subtypes/vaurca.dm | 9 +- code/modules/psionics/abilities/skinsight.dm | 9 +- html/changelogs/HealthTGUI.yml | 9 + .../tgui/interfaces/HealthAnalyzer.tsx | 61 +++ .../styles/interfaces/HealthAnalyzer.scss | 46 +++ tgui/packages/tgui/styles/main.scss | 1 + 18 files changed, 555 insertions(+), 314 deletions(-) create mode 100644 code/datums/components/medical/medicalAnalyzer.dm create mode 100644 html/changelogs/HealthTGUI.yml create mode 100644 tgui/packages/tgui/interfaces/HealthAnalyzer.tsx create mode 100644 tgui/packages/tgui/styles/interfaces/HealthAnalyzer.scss diff --git a/aurorastation.dme b/aurorastation.dme index c41d742ef2d..12456425071 100644 --- a/aurorastation.dme +++ b/aurorastation.dme @@ -508,6 +508,7 @@ #include "code\datums\components\eye\base_planner.dm" #include "code\datums\components\eye\blueprints.dm" #include "code\datums\components\eye\freelook.dm" +#include "code\datums\components\medical\medicalAnalyzer.dm" #include "code\datums\components\morale\moodlets.dm" #include "code\datums\components\morale\morale_component.dm" #include "code\datums\components\multitool\_multitool.dm" diff --git a/code/datums/components/medical/medicalAnalyzer.dm b/code/datums/components/medical/medicalAnalyzer.dm new file mode 100644 index 00000000000..1eebdafa338 --- /dev/null +++ b/code/datums/components/medical/medicalAnalyzer.dm @@ -0,0 +1,370 @@ +#define SPAN_SCAN_GREEN(str) ("" + str + "") +#define SPAN_SCAN_BLUE(str) ("" + str + "") +#define SPAN_SCAN_ORANGE(str) ("" + str + "") +#define SPAN_SCAN_ORANGE_DANGER(str) ("" + str + "") +#define SPAN_SCAN_RED(str) ("" + str + "") +#define SPAN_SCAN_NOTICE(str) ("" + str + "") +#define SPAN_SCAN_WARNING(str) ("" + str + "") +#define SPAN_SCAN_DANGER(str) ("" + str + "") + +/datum/component/health_analyzer + var/name = "health analyzer" + var/last_scan = 0 + var/device_level = 4 + var/sound_scan = FALSE + var/list/scan_results = list() + var/list/reagent_results = list() + var/scan_title = null + /// The owner object, also known as parent. Defined to easily do obj specific procs + var/obj/owner + +// This one can't scan limbs. Like a simpler version of the analyzer +/datum/component/health_analyzer/simple + device_level = 1 + +/datum/component/health_analyzer/mech + name = "mech health analyzer" + +/datum/component/health_analyzer/mech/ui_state(mob/user) + return GLOB.heavy_vehicle_state + +/datum/component/health_analyzer/borer + +/datum/component/health_analyzer/borer/ui_state(mob/user) + return GLOB.conscious_state + +/datum/component/health_analyzer/observer + name = "observer health analyzer" + +/datum/component/health_analyzer/observer/ui_state(mob/user) + return GLOB.observer_state + +/datum/component/health_analyzer/Initialize(...) + . = ..() + if(!isobj(parent)) + return + owner = parent + +/datum/component/health_analyzer/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "HealthAnalyzer", name, 520, 620) + ui.open() + +/datum/component/health_analyzer/ui_data(mob/user) + var/list/data = list() + data["scan_title"] = scan_title + data["scan_results"] = scan_results + data["reagent_results"] = reagent_results + return data + +/datum/component/health_analyzer/ui_act(action, list/params) + . = ..() + if(.) + return + + switch(action) + if("clear_list") + scan_results = list() + reagent_results = list() + scan_title = null + return TRUE + +/datum/component/health_analyzer/proc/attack(mob/living/target_mob, mob/living/user, target_zone) + sound_scan = TRUE + + user.visible_message("\The [user] starts scanning [user == target_mob ? "themself" : "\the [target_mob]"] with \the [owner].") + var/anatomy = GET_SKILL_LEVEL(user, ANATOMY_SKILL_COMPONENT) + + // each device level and anatomy rank reduces the time by half a second. Get that skill up + var/time = max(5 - (0.5 * (device_level + (anatomy ? anatomy : 1))), 1) + + if(do_after(user, time SECONDS, target_mob, DO_UNIQUE)) + flick("[owner.icon_state]-scan", owner) + + health_scan_mob(target_mob, user, device_level, sound_scan = sound_scan) + ui_interact(user) + + owner.add_fingerprint(user) + else + user.visible_message("\The [user] stops scanning \the [target_mob].") + +/datum/component/health_analyzer/proc/attack_self(mob/user) + ui_interact(user) + + owner.add_fingerprint(user) + +/datum/component/health_analyzer/proc/health_scan_mob(var/mob/M, var/mob/living/user, var/device_level = 2, var/just_scan = FALSE, var/sound_scan) + scan_results = list() + reagent_results = list() + scan_title = null + + if(!just_scan) + if (((user.is_clumsy()) || (user.mutations & DUMB)) && prob(50)) + user.visible_message("[user] runs the scanner over the floor.", + SPAN_NOTICE("You run the scanner over the floor."), + SPAN_NOTICE("You hear metal repeatedly clunking against the floor.")) + + to_chat(user, SPAN_NOTICE("Scan results for the ERROR:")) + if(sound_scan) + playsound(user.loc, 'sound/items/healthscanner/healthscanner_used.ogg', 25, extrarange = SILENCED_SOUND_EXTRARANGE) + return + + if(!user.IsAdvancedToolUser()) + to_chat(user, SPAN_WARNING("You don't have the dexterity to do this!")) + return + + user.visible_message("[user] runs a scanner over [M].",SPAN_NOTICE("You run the scanner over [M].")) + + if(!istype(M, /mob/living/carbon/human)) + scan_title = "Scan failed" + scan_results += SPAN_SCAN_WARNING("This scanner is designed for humanoid patients only.") + if(sound_scan) + playsound(user.loc, 'sound/items/healthscanner/healthscanner_used.ogg', 25, extrarange = SILENCED_SOUND_EXTRARANGE) + return + + var/mob/living/carbon/human/H = M + scan_title = "Scan results for \the [H]" + + if(H.isSynthetic() && !H.isFBP()) + to_chat(user, SPAN_WARNING("This scanner is designed for organic humanoid patients only.")) + if(sound_scan) + playsound(user.loc, 'sound/items/healthscanner/healthscanner_used.ogg', 25, extrarange = SILENCED_SOUND_EXTRARANGE) + return + + var/list/dat = list() + var/b = "" + var/endb = "" + + if(H.stat == DEAD || H.status_flags & FAKEDEATH) + dat += SPAN_SCAN_WARNING("[b]Time of Death:[endb] [worldtime2text(H.timeofdeath)]") + + // Brain activity. + var/brain_status = H.get_brain_status() + dat += "Brain activity: [brain_status]" + var/brain_result = H.get_brain_result() + + if(sound_scan) + switch(brain_result) + if(0) + playsound(user.loc, 'sound/items/healthscanner/healthscanner_dead.ogg', 25, extrarange = SILENCED_SOUND_EXTRARANGE) + if(-1) + playsound(user.loc, 'sound/items/healthscanner/healthscanner_used.ogg', 25, extrarange = SILENCED_SOUND_EXTRARANGE) + else + if(brain_result <= 25) + playsound(user.loc, 'sound/items/healthscanner/healthscanner_critical.ogg', 25, extrarange = SHORT_RANGE_SOUND_EXTRARANGE) + else if(brain_result <= 50) + playsound(user.loc, 'sound/items/healthscanner/healthscanner_danger.ogg', 25, extrarange = SHORT_RANGE_SOUND_EXTRARANGE) + else if(brain_result <= 90) + playsound(user.loc, 'sound/items/healthscanner/healthscanner_used.ogg', 25, extrarange = SILENCED_SOUND_EXTRARANGE) + else + playsound(user.loc, 'sound/items/healthscanner/healthscanner_stable.ogg', 25, extrarange = SILENCED_SOUND_EXTRARANGE) + + // Pulse rate. + var/pulse_result = "normal" + if(H.should_have_organ(BP_HEART)) + if(H.status_flags & FAKEDEATH) + pulse_result = SPAN_DANGER("0") + else + pulse_result = H.get_pulse(GETPULSE_TOOL) + if(H.pulse() == PULSE_NONE) + pulse_result = SPAN_SCAN_DANGER("[pulse_result] BPM") + else if(H.pulse() < PULSE_NORM) + pulse_result = SPAN_SCAN_NOTICE("[pulse_result] BPM") + else if(H.pulse() > PULSE_NORM) + pulse_result = SPAN_SCAN_WARNING("[pulse_result] BPM") + else + pulse_result = SPAN_SCAN_GREEN("[pulse_result] BPM") + else + pulse_result = SPAN_SCAN_DANGER("0") + dat += "Pulse rate: [pulse_result]" + + // Body temperature. Rounds to one digit after decimal. + var/temperature_string + if(H.bodytemperature < H.species.cold_level_1 || H.bodytemperature > H.species.heat_level_1) + temperature_string = "Body temperature: [SPAN_SCAN_WARNING("[round(H.bodytemperature-T0C, 0.1)]°C ([round(H.bodytemperature*1.8-459.67, 0.1)]°F")]" + else + temperature_string = "Body temperature: [SPAN_SCAN_GREEN("[round(H.bodytemperature-T0C, 0.1)]°C ([round(H.bodytemperature*1.8-459.67, 0.1)]°F)")]" + dat += temperature_string + + // Blood pressure and blood type. Based on the idea of a normal blood pressure being 120 over 80. + if(H.should_have_organ(BP_HEART)) + var/blood_pressure_string + switch(H.get_blood_pressure_alert()) + if(1) + blood_pressure_string = SPAN_SCAN_DANGER("[H.get_blood_pressure()]") + if(2) + blood_pressure_string = SPAN_SCAN_GREEN("[H.get_blood_pressure()]") + if(3) + blood_pressure_string = SPAN_SCAN_WARNING("[H.get_blood_pressure()]") + if(4) + blood_pressure_string = SPAN_SCAN_DANGER("[H.get_blood_pressure()]") + + var/blood_volume_string = SPAN_SCAN_GREEN("\>[BLOOD_VOLUME_SAFE]%") + switch(H.get_blood_volume()) + if(BLOOD_VOLUME_OKAY to BLOOD_VOLUME_SAFE) + blood_volume_string = SPAN_SCAN_NOTICE("\<[BLOOD_VOLUME_SAFE]%") + if(BLOOD_VOLUME_SURVIVE to BLOOD_VOLUME_OKAY) + blood_volume_string = SPAN_SCAN_WARNING("\<[BLOOD_VOLUME_OKAY]%") + if(-(INFINITY) to BLOOD_VOLUME_SURVIVE) + blood_volume_string = SPAN_SCAN_DANGER("\<[BLOOD_VOLUME_SURVIVE]%") + + var/oxygenation = H.get_blood_oxygenation() + var/oxygenation_string = SPAN_SCAN_GREEN("[oxygenation]%") + switch(oxygenation) + if(BLOOD_VOLUME_OKAY to BLOOD_VOLUME_SAFE) + oxygenation_string = SPAN_SCAN_NOTICE("[oxygenation]%") + if(BLOOD_VOLUME_SURVIVE to BLOOD_VOLUME_OKAY) + oxygenation_string = SPAN_SCAN_WARNING("[oxygenation]%") + if(-(INFINITY) to BLOOD_VOLUME_SURVIVE) + oxygenation_string = SPAN_SCAN_DANGER("[oxygenation]%") + if(H.status_flags & FAKEDEATH) + oxygenation_string = SPAN_SCAN_DANGER("[rand(0,10)]%") + dat += "Blood pressure: [blood_pressure_string]" + dat += "Blood oxygenation: [oxygenation_string]" + dat += "Blood volume: [blood_volume_string]" + dat += "Blood type: [SPAN_SCAN_GREEN("[H.dna.b_type]")]" + else + dat += "Blood pressure: N/A" + + // Traumatic shock. + if(H.is_asystole() || (H.status_flags & FAKEDEATH)) + dat += SPAN_SCAN_DANGER("Cardiovascular shock detected. Administer CPR immediately.") + else if(H.shock_stage > 80) + dat += SPAN_SCAN_WARNING("Imminent cardiovascular shock. Pain relief recommended.") + + if(H.getOxyLoss() > 50) + dat += SPAN_SCAN_BLUE("[b]Severe oxygen deprivation detected.[endb]") + if(H.getToxLoss() > 50) + dat += SPAN_SCAN_ORANGE("[b]Major systemic organ failure detected.[endb]") + if(H.getFireLoss() > 50) + dat += SPAN_SCAN_ORANGE("[b]Severe burn damage detected.[endb]") + if(H.getBruteLoss() > 50) + dat += SPAN_SCAN_RED("[b]Severe anatomical damage detected.[endb]") + + if(device_level >= 2) + var/list/damaged = H.get_damaged_organs(1,1) + if(damaged.len) + for(var/obj/item/organ/external/org in damaged) + var/limb_result = "[capitalize(org.name)][BP_IS_ROBOTIC(org) ? " (Cybernetic)" : ""]:" + if(org.brute_dam > 0) + limb_result = "[limb_result] [SPAN_SCAN_DANGER("[get_wound_severity(org.brute_dam, (org.limb_flags & ORGAN_HEALS_OVERKILL), TRUE)] physical trauma")]" + if(org.burn_dam > 0) + limb_result = "[limb_result] [SPAN_SCAN_ORANGE_DANGER("[get_wound_severity(org.burn_dam, (org.limb_flags & ORGAN_HEALS_OVERKILL), TRUE)] burns")]" + if(org.status & ORGAN_BLEEDING) + limb_result = "[limb_result] [SPAN_SCAN_DANGER("bleeding")]" + var/is_bandaged = org.is_bandaged() + var/is_salved = org.is_salved() + if(is_bandaged && is_salved) + var/icon/B = icon('icons/obj/item/stacks/medical.dmi', "bandaged") + var/icon/S = icon('icons/obj/item/stacks/medical.dmi', "salved") + limb_result = "[limb_result] \[[icon2html(B, user)] | [icon2html(S, user)]\]" + else if(is_bandaged) + var/icon/B = icon('icons/obj/item/stacks/medical.dmi', "bandaged") + limb_result = "[limb_result] \[[icon2html(B, user)]\]" + else if(is_salved) + var/icon/S = icon('icons/obj/item/stacks/medical.dmi', "salved") + limb_result = "[limb_result] \[[icon2html(S, user)]\]" + dat += limb_result + else + dat += "No detectable limb injuries." + + if(device_level >= 3) + for(var/name in H.organs_by_name) + var/obj/item/organ/external/e = H.organs_by_name[name] + if(!e) + continue + var/limb = e.name + if(e.status & ORGAN_BROKEN) + if(((e.name == BP_L_ARM) || (e.name == BP_R_ARM) || (e.name == BP_L_LEG) || (e.name == BP_R_LEG)) && !(e.status & ORGAN_SPLINTED)) + dat += SPAN_SCAN_WARNING("Unsecured fracture in subject [limb]. Splinting recommended for transport.") + + for(var/name in H.organs_by_name) + var/obj/item/organ/external/e = H.organs_by_name[name] + if(e && e.status & ORGAN_BROKEN) + dat += SPAN_SCAN_WARNING("Bone fractures detected. Advanced scanner required for location.") + break + + if(device_level >= 4) + var/found_bleed + var/found_tendon + var/found_disloc + for(var/obj/item/organ/external/e in H.organs) + if(e) + if(!found_disloc && e.dislocated == 2) + dat += SPAN_SCAN_WARNING("Dislocation detected. Advanced scanner required for location.") + found_disloc = TRUE + if(!found_bleed && (e.status & ORGAN_ARTERY_CUT)) + dat += SPAN_SCAN_WARNING("Arterial bleeding detected. Advanced scanner required for location.") + found_bleed = TRUE + if(!found_tendon && (e.tendon_status() & TENDON_CUT)) + dat += SPAN_SCAN_WARNING("Tendon or ligament damage detected. Advanced scanner required for location.") + found_tendon = TRUE + if(found_disloc && found_bleed && found_tendon) + break + + scan_results += dat + dat = list() + + // Reagent data. + . += "[b]Reagent scan:[endb]" + + var/print_reagent_default_message = TRUE + + if(device_level >= 3) + if(H.reagents.total_volume) + var/unknown = 0 + var/reagentdata[0] + for(var/_R in H.reagents.reagent_volumes) + var/singleton/reagent/R = GET_SINGLETON(_R) + if(R.scannable) + print_reagent_default_message = FALSE + reagentdata["[_R]"] = SPAN_NOTICE(" [round(REAGENT_VOLUME(H.reagents, _R), 1)]u [R.name]") + else + unknown++ + if(reagentdata.len) + print_reagent_default_message = FALSE + dat += SPAN_NOTICE("Beneficial reagents detected in subject's blood:") + for(var/d in reagentdata) + dat += reagentdata[d] + if(unknown) + print_reagent_default_message = FALSE + dat += SPAN_WARNING("Warning: Unknown substance[(unknown>1)?"s":""] detected in subject's blood.") + + if(device_level >= 4) + var/datum/reagents/ingested = H.get_ingested_reagents() + if(ingested && ingested.total_volume) + var/unknown = 0 + var/ingesteddata[0] + + for(var/_R in ingested.reagent_volumes) + var/singleton/reagent/R = GET_SINGLETON(_R) + if(R.scannable) + print_reagent_default_message = FALSE + ingesteddata["[_R]"] = SPAN_NOTICE(" [round(REAGENT_VOLUME(ingested, _R), 1)]u [R.name]") + else + ++unknown + + if(ingesteddata.len) + print_reagent_default_message = FALSE + dat += SPAN_NOTICE("Reagents detected in subject's stomach:") + for(var/d in ingesteddata) + dat += ingesteddata[d] + + if(unknown) + print_reagent_default_message = FALSE + dat += SPAN_WARNING("Warning: Unknown substance[(unknown > 1) ? "s" : ""] detected in subject's stomach.") + if(print_reagent_default_message) + dat += "No results." + + reagent_results += dat + ui_interact(user) + +#undef SPAN_SCAN_GREEN +#undef SPAN_SCAN_BLUE +#undef SPAN_SCAN_ORANGE +#undef SPAN_SCAN_ORANGE_DANGER +#undef SPAN_SCAN_RED +#undef SPAN_SCAN_NOTICE +#undef SPAN_SCAN_WARNING +#undef SPAN_SCAN_DANGER diff --git a/code/datums/components/skills/medical/anatomy_skill_component.dm b/code/datums/components/skills/medical/anatomy_skill_component.dm index 7f89c1e17b0..6de39140895 100644 --- a/code/datums/components/skills/medical/anatomy_skill_component.dm +++ b/code/datums/components/skills/medical/anatomy_skill_component.dm @@ -1,5 +1,5 @@ /** - * Not currently implemented anywhere. Finish this in its own PR so as to avoid Scope Creep. + * Currently only implemented in health analyzer usage. Finish this in its own PR so as to avoid Scope Creep. * * This skill should influence the information a character receives when medically examining another person (with or without a health analyzer). * With extremely high ranks in the skill giving more detailed information about a character's injuries at a glance to make diagnosing injuries easier. diff --git a/code/game/objects/items/devices/scanners.dm b/code/game/objects/items/devices/scanners.dm index c35855ec141..3b3f6cb1a8f 100644 --- a/code/game/objects/items/devices/scanners.dm +++ b/code/game/objects/items/devices/scanners.dm @@ -22,29 +22,23 @@ BREATH ANALYZER throw_range = 10 matter = list(MATERIAL_ALUMINIUM = 200) origin_tech = list(TECH_MAGNET = 1, TECH_BIO = 1) - var/last_scan = 0 - var/mode = 1 - var/sound_scan = FALSE + +/obj/item/healthanalyzer/Initialize(mapload, ...) + . = ..() + src.LoadComponent(/datum/component/health_analyzer) + flick("[icon_state]-scan", src) /obj/item/healthanalyzer/attack(mob/living/target_mob, mob/living/user, target_zone) - sound_scan = FALSE - if(last_scan <= world.time - 20) //Spam limiter. - last_scan = world.time - sound_scan = TRUE - user.setClickCooldown(DEFAULT_QUICK_COOLDOWN) - flick("[icon_state]-scan", src) //makes it so that it plays the scan animation on a successful scan - health_scan_mob(target_mob, user, mode, sound_scan = sound_scan) - add_fingerprint(user) + var/datum/component/health_analyzer/h_analyzer = src.GetComponent(/datum/component/health_analyzer) + if(!h_analyzer) + return + h_analyzer.attack(target_mob, user, target_zone) /obj/item/healthanalyzer/attack_self(mob/user) - sound_scan = FALSE - if(last_scan <= world.time - 20) //Spam limiter. - last_scan = world.time - sound_scan = TRUE - user.setClickCooldown(DEFAULT_QUICK_COOLDOWN) - flick("[icon_state]-scan", src) //makes it so that it plays the scan animation on a successful scan - health_scan_mob(user, user, mode, sound_scan = sound_scan) - add_fingerprint(user) + var/datum/component/health_analyzer/h_analyzer = src.GetComponent(/datum/component/health_analyzer) + if(!h_analyzer) + return + h_analyzer.attack_self(user) /// Calculates severity based on the ratios defined external limbs. /proc/get_wound_severity(damage_ratio, can_heal_overkill, uppercase = FALSE) @@ -99,288 +93,6 @@ BREATH ANALYZER output = capitalize(output) return output -/proc/health_scan_mob(var/mob/M, var/mob/living/user, var/show_limb_damage = TRUE, var/just_scan = FALSE, var/sound_scan) - if(!just_scan) - if (((user.is_clumsy()) || (user.mutations & DUMB)) && prob(50)) - user.visible_message("[user] runs the scanner over the floor.", - SPAN_NOTICE("You run the scanner over the floor."), - SPAN_NOTICE("You hear metal repeatedly clunking against the floor.")) - - to_chat(user, SPAN_NOTICE("Scan results for the ERROR:")) - if(sound_scan) - playsound(user.loc, 'sound/items/healthscanner/healthscanner_used.ogg', 25, extrarange = SILENCED_SOUND_EXTRARANGE) - return - - if(!user.IsAdvancedToolUser()) - to_chat(user, SPAN_WARNING("You don't have the dexterity to do this!")) - return - - user.visible_message("[user] runs a scanner over [M].",SPAN_NOTICE("You run the scanner over [M].")) - - if(!istype(M, /mob/living/carbon/human)) - to_chat(user, SPAN_WARNING("This scanner is designed for humanoid patients only.")) - if(sound_scan) - playsound(user.loc, 'sound/items/healthscanner/healthscanner_used.ogg', 25, extrarange = SILENCED_SOUND_EXTRARANGE) - return - - var/mob/living/carbon/human/H = M - - if(H.isSynthetic() && !H.isFBP()) - to_chat(user, SPAN_WARNING("This scanner is designed for organic humanoid patients only.")) - if(sound_scan) - playsound(user.loc, 'sound/items/healthscanner/healthscanner_used.ogg', 25, extrarange = SILENCED_SOUND_EXTRARANGE) - return - - . = list() - var/header = list() - var/b - var/endb - var/dat = list() - - header += "" - header += "" - header += "" - header += "" - header += "" - header += "" - header += "" - b = "" - endb = "" - - . += "[b]Scan results for \the [H]:[endb]" - - if(H.stat == DEAD || H.status_flags & FAKEDEATH) - dat += "[b]Time of Death:[endb] [worldtime2text(H.timeofdeath)]" - - // Brain activity. - var/brain_status = H.get_brain_status() - dat += "Brain activity: [brain_status]" - var/brain_result = H.get_brain_result() - - if(sound_scan) - switch(brain_result) - if(0) - playsound(user.loc, 'sound/items/healthscanner/healthscanner_dead.ogg', 25, extrarange = SILENCED_SOUND_EXTRARANGE) - if(-1) - playsound(user.loc, 'sound/items/healthscanner/healthscanner_used.ogg', 25, extrarange = SILENCED_SOUND_EXTRARANGE) - else - if(brain_result <= 25) - playsound(user.loc, 'sound/items/healthscanner/healthscanner_critical.ogg', 25, extrarange = SHORT_RANGE_SOUND_EXTRARANGE) - else if(brain_result <= 50) - playsound(user.loc, 'sound/items/healthscanner/healthscanner_danger.ogg', 25, extrarange = SHORT_RANGE_SOUND_EXTRARANGE) - else if(brain_result <= 90) - playsound(user.loc, 'sound/items/healthscanner/healthscanner_used.ogg', 25, extrarange = SILENCED_SOUND_EXTRARANGE) - else - playsound(user.loc, 'sound/items/healthscanner/healthscanner_stable.ogg', 25, extrarange = SILENCED_SOUND_EXTRARANGE) - - // Pulse rate. - var/pulse_result = "normal" - if(H.should_have_organ(BP_HEART)) - if(H.status_flags & FAKEDEATH) - pulse_result = SPAN_DANGER("0") - else - pulse_result = H.get_pulse(GETPULSE_TOOL) - if(H.pulse() == PULSE_NONE) - pulse_result = "[pulse_result] BPM" - else if(H.pulse() < PULSE_NORM) - pulse_result = "[pulse_result] BPM" - else if(H.pulse() > PULSE_NORM) - pulse_result = "[pulse_result] BPM" - else - pulse_result = "[pulse_result] BPM" - else - pulse_result = "0" - dat += "Pulse rate: [pulse_result]" - - // Body temperature. Rounds to one digit after decimal. - var/temperature_string - if(H.bodytemperature < H.species.cold_level_1 || H.bodytemperature > H.species.heat_level_1) - temperature_string = "Body temperature: [round(H.bodytemperature-T0C, 0.1)]°C ([round(H.bodytemperature*1.8-459.67, 0.1)]°F)" - else - temperature_string = "Body temperature: [round(H.bodytemperature-T0C, 0.1)]°C ([round(H.bodytemperature*1.8-459.67, 0.1)]°F)" - dat += temperature_string - - // Blood pressure and blood type. Based on the idea of a normal blood pressure being 120 over 80. - if(H.should_have_organ(BP_HEART)) - var/blood_pressure_string - switch(H.get_blood_pressure_alert()) - if(1) - blood_pressure_string = "[H.get_blood_pressure()]" - if(2) - blood_pressure_string = "[H.get_blood_pressure()]" - if(3) - blood_pressure_string = "[H.get_blood_pressure()]" - if(4) - blood_pressure_string = "[H.get_blood_pressure()]" - - var/blood_volume_string = "\>[BLOOD_VOLUME_SAFE]%" - switch(H.get_blood_volume()) - if(BLOOD_VOLUME_OKAY to BLOOD_VOLUME_SAFE) - blood_volume_string = "\<[BLOOD_VOLUME_SAFE]%" - if(BLOOD_VOLUME_SURVIVE to BLOOD_VOLUME_OKAY) - blood_volume_string = "\<[BLOOD_VOLUME_OKAY]%" - if(-(INFINITY) to BLOOD_VOLUME_SURVIVE) - blood_volume_string = "\<[BLOOD_VOLUME_SURVIVE]%" - - var/oxygenation = H.get_blood_oxygenation() - var/oxygenation_string = "[oxygenation]%" - switch(oxygenation) - if(BLOOD_VOLUME_OKAY to BLOOD_VOLUME_SAFE) - oxygenation_string = "[oxygenation]%" - if(BLOOD_VOLUME_SURVIVE to BLOOD_VOLUME_OKAY) - oxygenation_string = "[oxygenation]%" - if(-(INFINITY) to BLOOD_VOLUME_SURVIVE) - oxygenation_string = "[oxygenation]%" - if(H.status_flags & FAKEDEATH) - oxygenation_string = "[rand(0,10)]%" - dat += "Blood pressure: [blood_pressure_string]" - dat += "Blood oxygenation: [oxygenation_string]" - dat += "Blood volume: [blood_volume_string]" - dat += "Blood type: [H.dna.b_type]" - else - dat += "Blood pressure: N/A" - - // Traumatic shock. - if(H.is_asystole() || (H.status_flags & FAKEDEATH)) - dat += "Cardiovascular shock detected. Administer CPR immediately." - else if(H.shock_stage > 80) - dat += "Imminent cardiovascular shock. Pain relief recommended." - - if(H.getOxyLoss() > 50) - dat += "[b]Severe oxygen deprivation detected.[endb]" - if(H.getToxLoss() > 50) - dat += "[b]Major systemic organ failure detected.[endb]" - if(H.getFireLoss() > 50) - dat += "[b]Severe burn damage detected.[endb]" - if(H.getBruteLoss() > 50) - dat += "[b]Severe anatomical damage detected.[endb]" - - if(show_limb_damage) - var/list/damaged = H.get_damaged_organs(1,1) - if(damaged.len) - for(var/obj/item/organ/external/org in damaged) - var/limb_result = "[capitalize(org.name)][BP_IS_ROBOTIC(org) ? " (Cybernetic)" : ""]:" - if(org.brute_dam > 0) - limb_result = "[limb_result] \[[get_wound_severity(org.brute_dam, (org.limb_flags & ORGAN_HEALS_OVERKILL), TRUE)] physical trauma\]" - if(org.burn_dam > 0) - limb_result = "[limb_result] \[[get_wound_severity(org.burn_dam, (org.limb_flags & ORGAN_HEALS_OVERKILL), TRUE)] burns\]" - if(org.status & ORGAN_BLEEDING) - limb_result = "[limb_result] \[bleeding\]" - var/is_bandaged = org.is_bandaged() - var/is_salved = org.is_salved() - if(is_bandaged && is_salved) - var/icon/B = icon('icons/obj/item/stacks/medical.dmi', "bandaged") - var/icon/S = icon('icons/obj/item/stacks/medical.dmi', "salved") - limb_result = "[limb_result] \[[icon2html(B, user)] | [icon2html(S, user)]\]" - else if(is_bandaged) - var/icon/B = icon('icons/obj/item/stacks/medical.dmi', "bandaged") - limb_result = "[limb_result] \[[icon2html(B, user)]\]" - else if(is_salved) - var/icon/S = icon('icons/obj/item/stacks/medical.dmi', "salved") - limb_result = "[limb_result] \[[icon2html(S, user)]\]" - dat += limb_result - else - dat += "No detectable limb injuries." - - for(var/name in H.organs_by_name) - var/obj/item/organ/external/e = H.organs_by_name[name] - if(!e) - continue - var/limb = e.name - if(e.status & ORGAN_BROKEN) - if(((e.name == BP_L_ARM) || (e.name == BP_R_ARM) || (e.name == BP_L_LEG) || (e.name == BP_R_LEG)) && !(e.status & ORGAN_SPLINTED)) - dat += "Unsecured fracture in subject [limb]. Splinting recommended for transport." - - for(var/name in H.organs_by_name) - var/obj/item/organ/external/e = H.organs_by_name[name] - if(e && e.status & ORGAN_BROKEN) - dat += "Bone fractures detected. Advanced scanner required for location." - break - - var/found_bleed - var/found_tendon - var/found_disloc - for(var/obj/item/organ/external/e in H.organs) - if(e) - if(!found_disloc && e.dislocated == 2) - dat += "Dislocation detected. Advanced scanner required for location." - found_disloc = TRUE - if(!found_bleed && (e.status & ORGAN_ARTERY_CUT)) - dat += "Arterial bleeding detected. Advanced scanner required for location." - found_bleed = TRUE - if(!found_tendon && (e.tendon_status() & TENDON_CUT)) - dat += "Tendon or ligament damage detected. Advanced scanner required for location." - found_tendon = TRUE - if(found_disloc && found_bleed && found_tendon) - break - - . += dat - dat = list() - - // Reagent data. - . += "[b]Reagent scan:[endb]" - - var/print_reagent_default_message = TRUE - - if(H.reagents.total_volume) - var/unknown = 0 - var/reagentdata[0] - for(var/_R in H.reagents.reagent_volumes) - var/singleton/reagent/R = GET_SINGLETON(_R) - if(R.scannable) - print_reagent_default_message = FALSE - reagentdata["[_R]"] = SPAN_NOTICE(" [round(REAGENT_VOLUME(H.reagents, _R), 1)]u [R.name]") - else - unknown++ - if(reagentdata.len) - print_reagent_default_message = FALSE - dat += SPAN_NOTICE("Beneficial reagents detected in subject's blood:") - for(var/d in reagentdata) - dat += reagentdata[d] - if(unknown) - print_reagent_default_message = FALSE - dat += SPAN_WARNING("Warning: Unknown substance[(unknown>1)?"s":""] detected in subject's blood.") - - var/datum/reagents/ingested = H.get_ingested_reagents() - if(ingested && ingested.total_volume) - var/unknown = 0 - for(var/_R in ingested.reagent_volumes) - var/singleton/reagent/R = GET_SINGLETON(_R) - if(R.scannable) - print_reagent_default_message = FALSE - dat += SPAN_NOTICE("[R.name] found in subject's stomach.") - else - ++unknown - if(unknown) - print_reagent_default_message = FALSE - dat += SPAN_WARNING("Non-medical reagent[(unknown > 1)?"s":""] found in subject's stomach.") - - if(print_reagent_default_message) - dat += "No results." - - . += dat - - header = jointext(header, null) - . = jointext(.,"
") - . = jointext(list(header,.),null) - - if(user) - to_chat(user, "
") - to_chat(user, .) - to_chat(user, "
") - -/obj/item/healthanalyzer/verb/toggle_mode() - set name = "Switch Verbosity" - set category = "Object.Held" - set src in usr - - mode = !mode - - if(mode) - to_chat(usr, "The scanner now shows specific limb damage.") - else - to_chat(usr, "The scanner no longer shows limb damage.") - /obj/item/analyzer name = "gas analyzer" desc = "A hand-held environmental scanner which reports current gas levels." diff --git a/code/modules/heavy_vehicle/equipment/medical.dm b/code/modules/heavy_vehicle/equipment/medical.dm index 66dac7818b8..dfa22ce76fa 100644 --- a/code/modules/heavy_vehicle/equipment/medical.dm +++ b/code/modules/heavy_vehicle/equipment/medical.dm @@ -344,6 +344,7 @@ S.forceMove(src) S.update_use_power(POWER_USE_OFF) connected = S + src.LoadComponent(/datum/component/health_analyzer/mech) /obj/item/healthanalyzer/mech/Destroy() if(connected) @@ -371,7 +372,10 @@ return FALSE if(!fullScan) for(var/mob/pilot in user_vehicle.pilots) - health_scan_mob(target_mob, pilot, TRUE, TRUE, sound_scan = TRUE) + var/datum/component/health_analyzer/mech/h_analyzer = src.GetComponent(/datum/component/health_analyzer/mech) + if(!h_analyzer) + return + h_analyzer.health_scan_mob(target_mob, pilot, TRUE, TRUE, sound_scan = TRUE) else user_vehicle.visible_message("[user_vehicle] starts scanning \the [target_mob] with \the [src].", SPAN_NOTICE("You start scanning \the [target_mob] with \the [src].")) diff --git a/code/modules/mob/abstract/ghost/observer/observer.dm b/code/modules/mob/abstract/ghost/observer/observer.dm index 97f47bf0111..5ba7ce53ac9 100644 --- a/code/modules/mob/abstract/ghost/observer/observer.dm +++ b/code/modules/mob/abstract/ghost/observer/observer.dm @@ -84,6 +84,8 @@ name = capitalize(pick(GLOB.first_names_male)) + " " + capitalize(pick(GLOB.last_names)) real_name = name + src.LoadComponent(/datum/component/health_analyzer/observer) + /mob/abstract/ghost/observer/Destroy() if(client) for(var/image/I in client.images) @@ -268,7 +270,10 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp if(isipc(orbit_target) || isrobot(orbit_target)) robotic_analyze_mob(orbit_target, usr, TRUE) else if(ishuman(orbit_target)) - health_scan_mob(orbit_target, usr, TRUE, TRUE) + var/datum/component/health_analyzer/observer/h_analyzer = src.GetComponent(/datum/component/health_analyzer/observer) + if(!h_analyzer) + return + h_analyzer.health_scan_mob(orbit_target, usr, TRUE, TRUE) else to_chat(src, SPAN_WARNING("This isn't a scannable target.")) diff --git a/code/modules/mob/living/simple_animal/borer/borer.dm b/code/modules/mob/living/simple_animal/borer/borer.dm index 0363df62117..713bd2406ba 100644 --- a/code/modules/mob/living/simple_animal/borer/borer.dm +++ b/code/modules/mob/living/simple_animal/borer/borer.dm @@ -66,6 +66,7 @@ SSghostroles.add_spawn_atom("borer", src) name = initial(name) + " ([number])" real_name = name + src.LoadComponent(/datum/component/health_analyzer/borer) /mob/living/simple_animal/borer/Destroy() QDEL_NULL(ability_bar) diff --git a/code/modules/mob/living/simple_animal/borer/borer_powers.dm b/code/modules/mob/living/simple_animal/borer/borer_powers.dm index 864a52b1846..bb9ec343eae 100644 --- a/code/modules/mob/living/simple_animal/borer/borer_powers.dm +++ b/code/modules/mob/living/simple_animal/borer/borer_powers.dm @@ -561,4 +561,7 @@ to_chat(src, SPAN_WARNING("You cannot do that in your current state.")) return - health_scan_mob(host, src, TRUE, TRUE) + var/datum/component/health_analyzer/borer/h_analyzer = src.GetComponent(/datum/component/health_analyzer/borer) + if(!h_analyzer) + return + h_analyzer.health_scan_mob(host, src, TRUE, TRUE) diff --git a/code/modules/modular_computers/computers/modular_computer/core.dm b/code/modules/modular_computers/computers/modular_computer/core.dm index d4dd9ded97e..7d5bc187dd7 100644 --- a/code/modules/modular_computers/computers/modular_computer/core.dm +++ b/code/modules/modular_computers/computers/modular_computer/core.dm @@ -96,6 +96,7 @@ initial_name = name listener = new("modular_computers", src) sync_linked() + src.LoadComponent(/datum/component/health_analyzer) /obj/item/modular_computer/Destroy() STOP_PROCESSING(SSprocessing, src) diff --git a/code/modules/modular_computers/computers/modular_computer/interaction.dm b/code/modules/modular_computers/computers/modular_computer/interaction.dm index 5f3cc8b45e8..d62ff268155 100644 --- a/code/modules/modular_computers/computers/modular_computer/interaction.dm +++ b/code/modules/modular_computers/computers/modular_computer/interaction.dm @@ -166,12 +166,11 @@ eject_item() /obj/item/modular_computer/attack(mob/living/target_mob, mob/living/user, target_zone) - var/sound_scan = FALSE - if(last_scan <= world.time - 20) //Spam limiter. - last_scan = world.time - sound_scan = TRUE if(scan_mode == SCANNER_MEDICAL) - health_scan_mob(target_mob, user, TRUE, sound_scan = sound_scan) + var/datum/component/health_analyzer/h_analyzer = src.GetComponent(/datum/component/health_analyzer) + if(!h_analyzer) + return + h_analyzer.attack(target_mob, user) /obj/item/modular_computer/afterattack(atom/A, mob/user, proximity_flag, click_parameters) . = ..() diff --git a/code/modules/organs/subtypes/augment/augments/health_analyzer.dm b/code/modules/organs/subtypes/augment/augments/health_analyzer.dm index 054d214c9ad..c975463cd99 100644 --- a/code/modules/organs/subtypes/augment/augments/health_analyzer.dm +++ b/code/modules/organs/subtypes/augment/augments/health_analyzer.dm @@ -6,9 +6,16 @@ activable = TRUE cooldown = 8 +/obj/item/organ/internal/augment/health_scanner/Initialize() + . = ..() + src.LoadComponent(/datum/component/health_analyzer) + /obj/item/organ/internal/augment/health_scanner/attack_self(var/mob/user) . = ..() if(!.) return FALSE - health_scan_mob(owner, owner, TRUE, TRUE) + var/datum/component/health_analyzer/h_analyzer = src.GetComponent(/datum/component/health_analyzer) + if(!h_analyzer) + return + h_analyzer.health_scan_mob(owner, owner, TRUE, TRUE) diff --git a/code/modules/organs/subtypes/autakh.dm b/code/modules/organs/subtypes/autakh.dm index 203aeadc01a..7675fdcb349 100644 --- a/code/modules/organs/subtypes/autakh.dm +++ b/code/modules/organs/subtypes/autakh.dm @@ -364,6 +364,10 @@ name = "medical grasper" action_button_name = "Deploy Mounted Health Scanner" +/obj/item/organ/external/hand/right/autakh/medical/Initialize(mapload) + . = ..() + src.LoadComponent(/datum/component/health_analyzer) + /obj/item/organ/external/hand/right/autakh/medical/refresh_action_button() . = ..() if(.) @@ -399,7 +403,10 @@ owner.last_special = world.time + 50 if(ishuman(G.affecting)) var/mob/living/carbon/human/H = G.affecting - health_scan_mob(H, owner) + var/datum/component/health_analyzer/h_analyzer = src.GetComponent(/datum/component/health_analyzer) + if(!h_analyzer) + return + h_analyzer.health_scan_mob(H, owner) /obj/item/organ/external/hand/right/autakh/security name = "security grasper" diff --git a/code/modules/organs/subtypes/vaurca.dm b/code/modules/organs/subtypes/vaurca.dm index e3f38124230..8ae60a4a05b 100644 --- a/code/modules/organs/subtypes/vaurca.dm +++ b/code/modules/organs/subtypes/vaurca.dm @@ -679,6 +679,10 @@ encased = "support frame" robotize_type = PROSTHETIC_VAURCA +/obj/item/organ/external/hand/right/vaurca/medical/Initialize(mapload) + . = ..() + src.LoadComponent(/datum/component/health_analyzer) + /obj/item/organ/external/hand/right/vaurca/medical/refresh_action_button() . = ..() if(.) @@ -714,4 +718,7 @@ owner.last_special = world.time + 50 if(ishuman(G.affecting)) var/mob/living/carbon/human/H = G.affecting - health_scan_mob(H, owner) + var/datum/component/health_analyzer/h_analyzer = src.GetComponent(/datum/component/health_analyzer) + if(!h_analyzer) + return + h_analyzer.health_scan_mob(H, owner) diff --git a/code/modules/psionics/abilities/skinsight.dm b/code/modules/psionics/abilities/skinsight.dm index 15298b05fa4..fbff089be8f 100644 --- a/code/modules/psionics/abilities/skinsight.dm +++ b/code/modules/psionics/abilities/skinsight.dm @@ -18,6 +18,10 @@ psi_cost = 10 var/body_scan_mode = FALSE +/obj/item/spell/skinsight/Initialize() + . = ..() + src.LoadComponent(/datum/component/health_analyzer) + /obj/item/spell/skinsight/on_use_cast(mob/user) . = ..() body_scan_mode = !body_scan_mode @@ -50,7 +54,10 @@ if(!body_scan_mode) user.visible_message(SPAN_NOTICE("[user] passes [user.get_pronoun("his")] hand over [target]."), SPAN_NOTICE("You pass your hand over [target].")) - health_scan_mob(target, user, TRUE, TRUE) + var/datum/component/health_analyzer/h_analyzer = src.GetComponent(/datum/component/health_analyzer) + if(!h_analyzer) + return + h_analyzer.health_scan_mob(target, user, TRUE, TRUE) else user.visible_message(SPAN_NOTICE("[user] slowly passes [user.get_pronoun("his")] hand over [target]..."), SPAN_NOTICE("You slowly pass your hand over [target]...")) diff --git a/html/changelogs/HealthTGUI.yml b/html/changelogs/HealthTGUI.yml new file mode 100644 index 00000000000..975d21f2c59 --- /dev/null +++ b/html/changelogs/HealthTGUI.yml @@ -0,0 +1,9 @@ +author: TheGreyWolf + +delete-after: True + +changes: + - rscadd: "Health analyzers now use a TGUI instead of outputting to the chat." + - rscadd: "The handheld health analyzer now takes time to use, depending on the user's anatomy skill level." + - rscadd: "It is now possible to pull up the last scan on the handheld health analyzer." + - rscdel: "It is no longer possible to switch limb mode on the handheld health analyzer." diff --git a/tgui/packages/tgui/interfaces/HealthAnalyzer.tsx b/tgui/packages/tgui/interfaces/HealthAnalyzer.tsx new file mode 100644 index 00000000000..1fe28aefcd4 --- /dev/null +++ b/tgui/packages/tgui/interfaces/HealthAnalyzer.tsx @@ -0,0 +1,61 @@ +import { useBackend } from '../backend'; +import { Box, Button, Section, Stack } from '../components'; +import { Window } from '../layouts'; + +type HealthAnalyzerData = { + scan_title?: string; + scan_results: string[]; + reagent_results: string[]; +}; + +export const HealthAnalyzer = (props, context) => { + const { act, data } = useBackend(context); + const { scan_title, scan_results, reagent_results } = data; + + return ( + + +
act('clear_list')}> + Clear scan + + } + > + {!scan_results?.length ? ( + No scan data. + ) : ( + + {scan_results.map((line, i) => ( + + + + ))} + + )} +
+ +
+ {!reagent_results?.length ? ( + No results. + ) : ( + + {reagent_results.map((line, i) => ( + + + + ))} + + )} +
+
+
+ ); +}; diff --git a/tgui/packages/tgui/styles/interfaces/HealthAnalyzer.scss b/tgui/packages/tgui/styles/interfaces/HealthAnalyzer.scss new file mode 100644 index 00000000000..c9a2a8f4187 --- /dev/null +++ b/tgui/packages/tgui/styles/interfaces/HealthAnalyzer.scss @@ -0,0 +1,46 @@ +.HealthAnalyzer__line { + line-height: 1.35; +} + +.HealthAnalyzer__line img { + width: 16px; + height: 16px; + object-fit: contain; + vertical-align: middle; + image-rendering: pixelated; +} + +.scan_notice { + color: #5f94af; +} + +.scan_warning { + color: #ff4444; + font-style: italic; +} + +.scan_danger { + color: #ff4444; + font-weight: bold; +} + +.scan_red { + color: red; +} + +.scan_green { + color: #6fff6f; +} + +.scan_blue { + color: #5f94af; +} + +.scan_orange { + color: #ffa500; +} + +.scan_orange_danger { + color: #ffa500; + font-weight: bold; +} diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss index 3f4fb553e78..5bcee3437c1 100644 --- a/tgui/packages/tgui/styles/main.scss +++ b/tgui/packages/tgui/styles/main.scss @@ -50,6 +50,7 @@ @include meta.load-css('./interfaces/AlertModal.scss'); @include meta.load-css('./interfaces/CrewManifest.scss'); @include meta.load-css('./interfaces/FactionSelect.scss'); +@include meta.load-css('./interfaces/HealthAnalyzer.scss'); @include meta.load-css('./interfaces/ListInput.scss'); @include meta.load-css('./interfaces/PersonalCrafting.scss'); @include meta.load-css('./interfaces/RequestManager.scss');