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.
---
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, "