[MIRROR] Refactors pandemic machine [MDB IGNORE] (#15363)

* Refactors pandemic machine

* Update text.dm

Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com>
Co-authored-by: Gandalf <9026500+Gandalf2k15@users.noreply.github.com>
This commit is contained in:
SkyratBot
2022-08-04 15:48:11 +01:00
committed by GitHub
co-authored by Jeremiah Gandalf
parent 136f1fa910
commit bab710c04c
11 changed files with 775 additions and 712 deletions
+8 -9
View File
@@ -22,15 +22,15 @@
///returns nothing with an alert instead of the message if it contains something in the ic filter, and sanitizes normally if the name is fine. It returns nothing so it backs out of the input the same way as if you had entered nothing.
/proc/sanitize_name(t,allow_numbers=FALSE)
if(is_ic_filtered(t) || is_soft_ic_filtered(t))
/proc/sanitize_name(target, allow_numbers = FALSE, cap_after_symbols = TRUE)
if(is_ic_filtered(target) || is_soft_ic_filtered(target))
tgui_alert(usr, "You cannot set a name that contains a word prohibited in IC chat!")
return ""
var/r = reject_bad_name(t,allow_numbers=allow_numbers,strict=TRUE)
if(!r)
var/result = reject_bad_name(target, allow_numbers = allow_numbers, strict = TRUE, cap_after_symbols = cap_after_symbols)
if(!result)
tgui_alert(usr, "Invalid name.")
return ""
return sanitize(r)
return sanitize(result)
/// Runs byond's html encoding sanitization proc, after replacing new-lines and tabs for the # character.
@@ -150,9 +150,9 @@
*
* * strict - return null immidiately instead of filtering out
* * allow_numbers - allows numbers and common special characters - used for silicon/other weird things names
* * cap_after_symbols - words like Bob's will be capitalized to Bob'S by default. False is good for titles.
*/
///proc/reject_bad_name(t_in, allow_numbers = FALSE, max_length = MAX_NAME_LEN, ascii_only = TRUE, strict = FALSE) //ORIGINAL
/proc/reject_bad_name(t_in, allow_numbers = TRUE, max_length = MAX_NAME_LEN, ascii_only = TRUE, strict = FALSE) //SKYRAT EDIT CHANGE
/proc/reject_bad_name(t_in, allow_numbers = TRUE, max_length = MAX_NAME_LEN, ascii_only = TRUE, strict = FALSE, cap_after_symbols = TRUE) // SKYRAT EDIT CHANGE
if(!t_in)
return //Rejects the input if it is null
@@ -179,7 +179,7 @@
// a .. z
if(97 to 122) //Lowercase Letters
if(last_char_group == NO_CHARS_DETECTED || last_char_group == SPACES_DETECTED || last_char_group == SYMBOLS_DETECTED) //start of a word
if(last_char_group == NO_CHARS_DETECTED || last_char_group == SPACES_DETECTED || cap_after_symbols && last_char_group == SYMBOLS_DETECTED) //start of a word
char = uppertext(char)
number_of_alphanumeric++
last_char_group = LETTERS_DETECTED
@@ -192,7 +192,6 @@
continue
number_of_alphanumeric++
last_char_group = NUMBERS_DETECTED
// ' - .
if(39,45,46) //Common name punctuation
if(last_char_group == NO_CHARS_DETECTED)
@@ -86,3 +86,22 @@
///Overload for running after processing.
/datum/symptom/proc/OnRemove(datum/disease/advance/A)
return
/**
* Returns a list for all of the traits of this symptom.
*
*
* @returns {list} symptom - The desired symptoms as a list.
*/
/datum/symptom/proc/get_symptom_data()
var/list/data = list()
data["name"] = name
data["desc"] = desc
data["stealth"] = stealth
data["resistance"] = resistance
data["stage_speed"] = stage_speed
data["transmission"] = transmittable
data["level"] = level
data["neutered"] = neutered
data["threshold_desc"] = threshold_descs
return data
@@ -1,5 +1,3 @@
#define MAIN_SCREEN 1
#define SYMPTOM_DETAILS 2
/obj/machinery/computer/pandemic
name = "PanD.E.M.I.C 2200"
@@ -12,8 +10,11 @@
resistance_flags = ACID_PROOF
circuit = /obj/item/circuitboard/computer/pandemic
/// Whether the pandemic is ready to make another culture/vaccine
var/wait
/// The currently selected symptom
var/datum/symptom/selected_symptom
/// The inserted beaker
var/obj/item/reagent_containers/beaker
/obj/machinery/computer/pandemic/Initialize(mapload)
@@ -56,85 +57,25 @@
update_appearance()
return ..()
/obj/machinery/computer/pandemic/proc/get_by_index(thing, index)
if(!beaker || !beaker.reagents)
return
var/datum/reagent/blood/B = locate() in beaker.reagents.reagent_list
if(B?.data[thing])
return B.data[thing][index]
/obj/machinery/computer/pandemic/proc/get_virus_id_by_index(index)
var/datum/disease/D = get_by_index("viruses", index)
if(D)
return D.GetDiseaseID()
/obj/machinery/computer/pandemic/proc/get_viruses_data(datum/reagent/blood/B)
. = list()
var/list/V = B.get_diseases()
var/index = 1
for(var/virus in V)
var/datum/disease/D = virus
if(!istype(D) || D.visibility_flags & HIDDEN_PANDEMIC)
continue
var/list/this = list()
this["name"] = D.name
if(istype(D, /datum/disease/advance))
var/datum/disease/advance/A = D
var/disease_name = SSdisease.get_disease_name(A.GetDiseaseID())
this["can_rename"] = ((disease_name == "Unknown") && A.mutable)
this["name"] = disease_name
this["is_adv"] = TRUE
this["symptoms"] = list()
for(var/symptom in A.symptoms)
var/datum/symptom/S = symptom
var/list/this_symptom = list()
this_symptom = get_symptom_data(S)
this["symptoms"] += list(this_symptom)
this["resistance"] = A.totalResistance()
this["stealth"] = A.totalStealth()
this["stage_speed"] = A.totalStageSpeed()
this["transmission"] = A.totalTransmittable()
this["index"] = index++
this["agent"] = D.agent
this["description"] = D.desc || "none"
this["spread"] = D.spread_text || "none"
this["cure"] = D.cure_text || "none"
. += list(this)
/obj/machinery/computer/pandemic/proc/get_symptom_data(datum/symptom/S)
. = list()
var/list/this = list()
this["name"] = S.name
this["desc"] = S.desc
this["stealth"] = S.stealth
this["resistance"] = S.resistance
this["stage_speed"] = S.stage_speed
this["transmission"] = S.transmittable
this["level"] = S.level
this["neutered"] = S.neutered
this["threshold_desc"] = S.threshold_descs
. += this
/obj/machinery/computer/pandemic/proc/get_resistance_data(datum/reagent/blood/B)
. = list()
if(!islist(B.data["resistances"]))
return
var/list/resistances = B.data["resistances"]
for(var/id in resistances)
var/list/this = list()
var/datum/disease/D = SSdisease.archive_diseases[id]
if(D)
this["id"] = id
this["name"] = D.name
. += list(this)
/obj/machinery/computer/pandemic/proc/reset_replicator_cooldown()
wait = FALSE
/obj/machinery/computer/pandemic/attackby(obj/item/held_item, mob/user, params)
if(!istype(held_item, /obj/item/reagent_containers) || held_item.item_flags & ABSTRACT || !held_item.is_open_container())
return ..()
. = TRUE //no afterattack
if(machine_stat & (NOPOWER|BROKEN))
return ..()
if(beaker)
balloon_alert(user, "pandemic full")
return ..()
if(!user.transferItemToLoc(held_item, src))
return ..()
beaker = held_item
balloon_alert(user, "beaker loaded")
update_appearance()
playsound(src, 'sound/machines/ping.ogg', 30, TRUE)
SStgui.update_uis(src)
/obj/machinery/computer/pandemic/on_deconstruction()
eject_beaker()
. = ..()
/obj/machinery/computer/pandemic/update_icon_state()
icon_state = "[base_icon_state][beaker ? 1 : 0][(machine_stat & BROKEN) ? "_b" : (powered() ? null : "_nopower")]"
@@ -145,42 +86,36 @@
if(wait)
. += "waitlight"
/obj/machinery/computer/pandemic/proc/eject_beaker()
if(beaker)
try_put_in_hand(beaker, usr)
beaker = null
update_appearance()
/obj/machinery/computer/pandemic/ui_interact(mob/user, datum/tgui/ui)
. = ..()
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "Pandemic", name)
ui.open()
ui.set_autoupdate(FALSE)
/obj/machinery/computer/pandemic/ui_data(mob/user)
var/list/data = list()
data["is_ready"] = !wait
if(beaker)
data["has_beaker"] = TRUE
data["beaker"] = list(
"volume" = round(beaker.reagents?.total_volume, 0.01) || 0,
"capacity" = beaker.volume,
)
var/datum/reagent/blood/B = locate() in beaker.reagents.reagent_list
if(B)
data["has_blood"] = TRUE
data["blood"] = list()
data["blood"]["dna"] = B.data["blood_DNA"] || "none"
data["blood"]["type"] = B.data["blood_type"] || "none"
data["viruses"] = get_viruses_data(B)
data["resistances"] = get_resistance_data(B)
else
data["has_blood"] = FALSE
else
if(!beaker)
data["has_beaker"] = FALSE
data["has_blood"] = FALSE
return data
data["has_beaker"] = TRUE
data["beaker"] = list(
"volume" = round(beaker.reagents?.total_volume, 0.01) || 0,
"capacity" = beaker.volume,
)
var/datum/reagent/blood/blood = locate() in beaker.reagents.reagent_list
if(!blood)
data["has_blood"] = FALSE
return data
data["has_blood"] = TRUE
data["blood"] = list()
data["blood"]["dna"] = blood.data["blood_DNA"] || "none"
data["blood"]["type"] = blood.data["blood_type"] || "none"
data["viruses"] = get_viruses_data(blood)
data["resistances"] = get_resistance_data(blood)
return data
/obj/machinery/computer/pandemic/ui_act(action, params)
@@ -190,80 +125,218 @@
switch(action)
if("eject_beaker")
eject_beaker()
. = TRUE
return TRUE
if("empty_beaker")
if(beaker)
beaker.reagents.clear_reagents()
. = TRUE
return TRUE
if("empty_eject_beaker")
if(beaker)
beaker.reagents.clear_reagents()
eject_beaker()
. = TRUE
return TRUE
if("rename_disease")
var/id = get_virus_id_by_index(text2num(params["index"]))
var/datum/disease/advance/A = SSdisease.archive_diseases[id]
if(!A.mutable)
return
if(A)
var/new_name = sanitize_name(html_encode(params["name"]), allow_numbers = TRUE)
if(!new_name || ..())
return
A.AssignName(new_name)
. = TRUE
rename_disease(params["index"], params["name"])
return TRUE
if("create_culture_bottle")
if (wait)
return
var/id = get_virus_id_by_index(text2num(params["index"]))
var/datum/disease/advance/A = SSdisease.archive_diseases[id]
if(!istype(A) || !A.mutable)
to_chat(usr, span_warning("ERROR: Cannot replicate virus strain."))
return
use_power(active_power_usage)
A = A.Copy()
var/list/data = list("viruses" = list(A))
var/obj/item/reagent_containers/glass/bottle/B = new(drop_location())
B.name = "[A.name] culture bottle"
B.desc = "A small bottle. Contains [A.agent] culture in synthblood medium."
B.reagents.add_reagent(/datum/reagent/blood, 20, data)
wait = TRUE
update_appearance()
var/turf/source_turf = get_turf(src)
log_virus("A culture bottle was printed for the virus [A.admin_details()] at [loc_name(source_turf)] by [key_name(usr)]")
addtimer(CALLBACK(src, .proc/reset_replicator_cooldown), 50)
. = TRUE
return FALSE
create_culture_bottle(params["index"])
return TRUE
if("create_vaccine_bottle")
if (wait)
return
use_power(active_power_usage)
var/id = params["index"]
var/datum/disease/D = SSdisease.archive_diseases[id]
var/obj/item/reagent_containers/glass/bottle/B = new(drop_location())
B.name = "[D.name] vaccine bottle"
B.reagents.add_reagent(/datum/reagent/vaccine, 15, list(id))
wait = TRUE
update_appearance()
addtimer(CALLBACK(src, .proc/reset_replicator_cooldown), 200)
. = TRUE
return FALSE
create_vaccine_bottle(params["index"])
return TRUE
return FALSE
/**
* Creates a culture bottle (ie: replicates) of the the specified disease.
*
* @param {number} index - The index of the disease to replicate.
*
* @returns {boolean} - Success or failure.
*/
/obj/machinery/computer/pandemic/proc/create_culture_bottle(index)
var/id = get_virus_id_by_index(text2num(index))
var/datum/disease/advance/adv_disease = SSdisease.archive_diseases[id]
if(!istype(adv_disease) || !adv_disease.mutable)
to_chat(usr, span_warning("ERROR: Cannot replicate virus strain."))
return FALSE
use_power(active_power_usage)
adv_disease = adv_disease.Copy()
var/list/data = list("viruses" = list(adv_disease))
var/obj/item/reagent_containers/glass/bottle/bottle = new(drop_location())
bottle.name = "[adv_disease.name] culture bottle"
bottle.desc = "A small bottle. Contains [adv_disease.agent] culture in synthblood medium."
bottle.reagents.add_reagent(/datum/reagent/blood, 20, data)
wait = TRUE
update_appearance()
var/turf/source_turf = get_turf(src)
log_virus("A culture bottle was printed for the virus [adv_disease.admin_details()] at [loc_name(source_turf)] by [key_name(usr)]")
addtimer(CALLBACK(src, .proc/reset_replicator_cooldown), 5 SECONDS)
return TRUE
/obj/machinery/computer/pandemic/attackby(obj/item/I, mob/user, params)
if(istype(I, /obj/item/reagent_containers) && !(I.item_flags & ABSTRACT) && I.is_open_container())
. = TRUE //no afterattack
if(machine_stat & (NOPOWER|BROKEN))
return
if(beaker)
to_chat(user, span_warning("A container is already loaded into [src]!"))
return
if(!user.transferItemToLoc(I, src))
return
/**
* Creates a vaccine bottle for the specified disease.
*
* @param {number} index - The index of the disease to replicate.
*
* @returns {boolean} - Success or failure.
*/
/obj/machinery/computer/pandemic/proc/create_vaccine_bottle(index)
use_power(active_power_usage)
var/id = index
var/datum/disease/disease = SSdisease.archive_diseases[id]
var/obj/item/reagent_containers/glass/bottle/bottle = new(drop_location())
bottle.name = "[disease.name] vaccine bottle"
bottle.reagents.add_reagent(/datum/reagent/vaccine, 15, list(id))
wait = TRUE
update_appearance()
addtimer(CALLBACK(src, .proc/reset_replicator_cooldown), 20 SECONDS)
return TRUE
beaker = I
to_chat(user, span_notice("You insert [I] into [src]."))
update_appearance()
else
return ..()
/**
* Supporting proc to eject a beaker from the machine.
*
* Places it in hand if possible.
*
* @returns {boolean} - Success or failure.
*/
/obj/machinery/computer/pandemic/proc/eject_beaker()
if(!beaker)
return FALSE
try_put_in_hand(beaker, usr)
beaker = null
update_appearance()
return TRUE
/obj/machinery/computer/pandemic/on_deconstruction()
eject_beaker()
. = ..()
/**
* Displays a thing if it exists within the contents of a beaker.
*
* @param {any} thing - The key to look for.
*
* @param {any} index - Nested objects within the thing.
*
* @returns {any | boolean} The thing found or FALSE if unsuccessful.
*/
/obj/machinery/computer/pandemic/proc/get_by_index(thing, index)
if(!beaker || !beaker.reagents)
return FALSE
var/datum/reagent/blood/blood = locate() in beaker.reagents.reagent_list
if(blood?.data[thing])
return blood.data[thing][index]
return FALSE
/**
* Gets resistances of a given blood sample as a list
*
* @param {reagent/blood} blood - The sample.
*
* @returns {list} - The resistances.
*/
/obj/machinery/computer/pandemic/proc/get_resistance_data(datum/reagent/blood/blood)
var/list/data = list()
if(!islist(blood.data["resistances"]))
return data
var/list/resistances = blood.data["resistances"]
for(var/id in resistances)
var/list/resistance = list()
var/datum/disease/disease = SSdisease.archive_diseases[id]
if(disease)
resistance["id"] = id
resistance["name"] = disease.name
data += list(resistance)
return data
/**
* A very hefty proc that I am not proud to see.
*
* Given a blood sample, this proc will return a list of viruses that are present in the sample.
*
* Contains traits, symptoms, thresholds etc.
*
* @param {reagent/blood} blood - The sample to analyze.
*
* @returns {list} - A list of virus info present in the sample.
*/
/obj/machinery/computer/pandemic/proc/get_viruses_data(datum/reagent/blood/blood)
var/list/data = list()
var/list/viruses = blood.get_diseases()
var/index = 1
for(var/datum/disease/disease as anything in viruses)
if(!istype(disease) || disease.visibility_flags & HIDDEN_PANDEMIC)
continue
var/list/traits = list()
traits["agent"] = disease.agent
traits["cure"] = disease.cure_text || "none"
traits["description"] = disease.desc || "none"
traits["index"] = index++
traits["name"] = disease.name
traits["spread"] = disease.spread_text || "none"
if(!istype(disease, /datum/disease/advance)) // Advanced diseases get more info
continue
var/datum/disease/advance/adv_disease = disease
var/disease_name = SSdisease.get_disease_name(adv_disease.GetDiseaseID())
traits["can_rename"] = ((disease_name == "Unknown") && adv_disease.mutable)
traits["is_adv"] = TRUE
traits["name"] = disease_name
traits["resistance"] = adv_disease.totalResistance()
traits["stage_speed"] = adv_disease.totalStageSpeed()
traits["stealth"] = adv_disease.totalStealth()
traits["symptoms"] = list()
for(var/datum/symptom/symptom as anything in adv_disease.symptoms)
var/list/this_symptom = list()
this_symptom = symptom.get_symptom_data()
traits["symptoms"] += list(this_symptom)
traits["transmission"] = adv_disease.totalTransmittable()
data += list(traits)
return data
/**
* Gets the ID of the virus by its index in the list of viruses.
*
* @param {number} index - The index of the virus in the list of viruses.
*
* @returns {string | boolean} - The ID of the virus or FALSE if unable
* to find the virus.
*/
/obj/machinery/computer/pandemic/proc/get_virus_id_by_index(index)
var/datum/disease/disease = get_by_index("viruses", index)
if(!disease)
return FALSE
return disease.GetDiseaseID()
/**
* Renames an advanced disease after running it through sanitize_name().
*
* @param {string} id - The ID of the disease to rename.
*
* @param {string} name - The new name of the disease.
*
* @returns {boolean} - Success or failure.
*/
/obj/machinery/computer/pandemic/proc/rename_disease(index, name)
var/id = get_virus_id_by_index(text2num(index))
var/datum/disease/advance/adv_disease = SSdisease.archive_diseases[id]
if(!adv_disease.mutable)
return FALSE
if(adv_disease)
var/new_name = sanitize_name(name, allow_numbers = TRUE, cap_after_symbols = FALSE)
if(!new_name)
return FALSE
adv_disease.AssignName(new_name)
return TRUE
return FALSE
/**
* Allows a user to create another vaccine/culture bottle again.
*
* @returns {boolean} - Success or failure.
*/
/obj/machinery/computer/pandemic/proc/reset_replicator_cooldown()
wait = FALSE
SStgui.update_uis(src)
update_appearance()
playsound(src, 'sound/machines/ping.ogg', 30, TRUE)
return TRUE
-538
View File
@@ -1,538 +0,0 @@
import { capitalizeFirst } from 'common/string';
import { BooleanLike } from 'common/react';
import { useBackend, useLocalState } from 'tgui/backend';
import { Box, Button, Collapsible, Input, LabeledList, NoticeBox, ProgressBar, Section, Stack, Tabs, Tooltip } from 'tgui/components';
import { Window } from 'tgui/layouts';
type PandemicContext = {
beaker?: Beaker;
blood?: Blood;
has_beaker: BooleanLike;
has_blood: BooleanLike;
is_ready: BooleanLike;
resistances?: Resistance[];
viruses?: Virus[];
};
type Beaker = {
volume: number;
capacity: number;
};
type Blood = {
dna: string;
type: string;
};
type Resistance = {
id: string;
name: string;
};
type Virus = {
name: string;
can_rename: BooleanLike;
is_adv: BooleanLike;
symptoms?: Symptom[];
resistance: number;
stealth: number;
stage_speed: number;
transmission: number;
index: number;
agent: string;
description: string;
spread: string;
cure: string;
};
type VirusDisplayProps = {
virus: Virus;
};
type VirusInfoProps = {
virus: Virus;
};
type TabsProps = {
tab: number;
tabHandler: (tab: number) => void;
};
type Symptom = {
name: string;
desc: string;
stealth: number;
resistance: number;
stage_speed: number;
transmission: number;
level: number;
neutered: BooleanLike;
threshold_desc: Threshold[];
};
type SymptomDisplayProps = {
symptoms: Symptom[];
};
type SymptomInfoProps = {
symptom: Symptom;
};
type Threshold = {
label: string;
descr: string;
};
type ThresholdDisplayProps = {
thresholds: Threshold[];
};
export const Pandemic = (props, context) => {
const { data } = useBackend<PandemicContext>(context);
const { has_beaker, has_blood } = data;
return (
<Window width={650} height={500}>
<Window.Content>
<Stack fill vertical>
<Stack.Item>
<BeakerDisplay />
</Stack.Item>
{!!has_beaker && !!has_blood && (
<Stack.Item grow>
<SpecimenDisplay />
</Stack.Item>
)}
</Stack>
</Window.Content>
</Window>
);
};
/** Displays loaded container info, if it exists */
const BeakerDisplay = (props, context) => {
const { act, data } = useBackend<PandemicContext>(context);
const { has_beaker, beaker, has_blood } = data;
const cant_empty = !has_beaker || !beaker?.volume;
let content;
if (!has_beaker) {
content = <NoticeBox>No beaker loaded.</NoticeBox>;
} else if (!beaker?.volume) {
content = <NoticeBox>Beaker is empty.</NoticeBox>;
} else if (!has_blood) {
content = <NoticeBox>No blood sample loaded.</NoticeBox>;
} else {
content = (
<Stack vertical>
<Stack.Item>
<BeakerInfoDisplay />
</Stack.Item>
<Stack.Item>
<AntibodyInfoDisplay />
</Stack.Item>
</Stack>
);
}
return (
<Section
title="Beaker"
buttons={
<>
<Button
icon="times"
content="Empty and Eject"
color="bad"
disabled={cant_empty}
onClick={() => act('empty_eject_beaker')}
/>
<Button
icon="trash"
content="Empty"
disabled={cant_empty}
onClick={() => act('empty_beaker')}
/>
<Button
icon="eject"
content="Eject"
disabled={!has_beaker}
onClick={() => act('eject_beaker')}
/>
</>
}>
{content}
</Section>
);
};
/** Displays info about the blood type, beaker capacity - volume */
const BeakerInfoDisplay = (props, context) => {
const { data } = useBackend<PandemicContext>(context);
const { beaker, blood } = data;
if (!beaker || !blood) {
return <NoticeBox>No beaker loaded</NoticeBox>;
}
return (
<Stack>
<Stack.Item grow={2}>
<LabeledList>
<LabeledList.Item label="DNA">
{capitalizeFirst(blood.dna)}
</LabeledList.Item>
<LabeledList.Item label="Type">
{capitalizeFirst(blood.type)}
</LabeledList.Item>
</LabeledList>
</Stack.Item>
<Stack.Item grow={2}>
<LabeledList>
<LabeledList.Item label="Container">
<ProgressBar
color="darkred"
value={beaker.volume}
minValue={0}
maxValue={beaker.capacity}
ranges={{
'good': [beaker.capacity * 0.85, beaker.capacity],
'average': [beaker.capacity * 0.25, beaker.capacity * 0.85],
'bad': [0, beaker.capacity * 0.25],
}}
/>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
</Stack>
);
};
/** If antibodies are present, returns buttons to create vaccines */
const AntibodyInfoDisplay = (props, context) => {
const { act, data } = useBackend<PandemicContext>(context);
const { is_ready, resistances = [] } = data;
if (!resistances) {
return <NoticeBox>Nothing detected</NoticeBox>;
}
return (
<LabeledList>
<LabeledList.Item label="Antibodies">
{!resistances.length
? 'None'
: resistances.map((resistance) => {
return (
<Button
key={resistance.name}
icon="eye-dropper"
disabled={!is_ready}
tooltip="Creates a vaccine bottle."
onClick={() =>
act('create_vaccine_bottle', {
index: resistance.id,
})
}>
{`${resistance.name}`}
</Button>
);
})}
</LabeledList.Item>
</LabeledList>
);
};
/** Displays info for the loaded blood, if any */
const SpecimenDisplay = (props, context) => {
const { act, data } = useBackend<PandemicContext>(context);
const [tab, setTab] = useLocalState(context, 'tab', 0);
const { is_ready, viruses = [] } = data;
const virus = viruses[tab];
const setTabHandler = (index: number) => {
setTab(index);
};
if (!viruses?.length || !virus) {
return <NoticeBox>Nothing detected.</NoticeBox>;
}
return (
<Section
fill
scrollable
title="Specimen"
buttons={
<Stack>
{viruses.length > 1 && (
<Stack.Item>
<VirusTabs tab={tab} tabHandler={setTabHandler} />
</Stack.Item>
)}
<Stack.Item>
<Button
icon="flask"
content="Create culture bottle"
disabled={!is_ready}
onClick={() =>
act('create_culture_bottle', {
index: virus.index,
})
}
/>
</Stack.Item>
</Stack>
}>
<Stack fill vertical>
<Stack.Item>
<VirusDisplay virus={virus} />
</Stack.Item>
<Stack.Item>
{virus?.symptoms && <SymptomDisplay symptoms={virus.symptoms} />}
</Stack.Item>
</Stack>
</Section>
);
};
/** Virus Tab display - changes the tab for virus info
* Whenever the tab changes, the virus info is updated
*/
const VirusTabs = (props: TabsProps, context) => {
const { data } = useBackend<PandemicContext>(context);
const { tab, tabHandler } = props;
const { viruses = [] } = data;
return (
<Tabs>
{viruses.map((virus, index) => {
return (
<Tabs.Tab
selected={tab === index}
onClick={() => tabHandler(index)}
key={virus.name}>
{virus.name}
</Tabs.Tab>
);
})}
</Tabs>
);
};
/** Displays info about the virus. Child elements display
* the virus's traits and descriptions.
*/
const VirusDisplay = (props: VirusDisplayProps) => {
const { virus } = props;
return (
<Stack fill>
<Stack.Item grow={3}>
<VirusTextInfo virus={virus} />
</Stack.Item>
{virus.is_adv && (
<>
<Stack.Divider />
<Stack.Item grow={1}>
<VirusTraitInfo virus={virus} />
</Stack.Item>
</>
)}
</Stack>
);
};
/** Displays the description, name and other info for the virus. */
const VirusTextInfo = (props: VirusInfoProps, context) => {
const { act } = useBackend<PandemicContext>(context);
const { virus } = props;
return (
<LabeledList>
<LabeledList.Item label="Name">
{virus.can_rename ? (
<Input
placeholder="Input a name"
value={virus.name === 'Unknown' ? '' : virus.name}
onChange={(_, value) =>
act('rename_disease', {
index: virus.index,
name: value,
})
}
/>
) : (
<Box color="bad">{virus.name}</Box>
)}
</LabeledList.Item>
<LabeledList.Item label="Description">
{virus.description}
</LabeledList.Item>
<LabeledList.Item label="Agent">
{capitalizeFirst(virus.agent)}
</LabeledList.Item>
<LabeledList.Item label="Spread">{virus.spread}</LabeledList.Item>
<LabeledList.Item label="Possible Cure">{virus.cure}</LabeledList.Item>
</LabeledList>
);
};
/** Displays the traits of the virus. This could be iterated over
* with object.keys but you would need a helper function for the tooltips.
* I would rather hard code it here.
*/
const VirusTraitInfo = (props: VirusInfoProps) => {
const { virus } = props;
return (
<Section title="Statistics">
<LabeledList>
<Tooltip content="Decides the cure complexity.">
<LabeledList.Item
color={GetColor(virus.resistance)}
label="Resistance">
{virus.resistance}
</LabeledList.Item>
</Tooltip>
<Tooltip content="Symptomic progression.">
<LabeledList.Item
color={GetColor(virus.stage_speed)}
label="Stage speed">
{virus.stage_speed}
</LabeledList.Item>
</Tooltip>
<Tooltip content="Detection difficulty from medical equipment.">
<LabeledList.Item color={GetColor(virus.stealth)} label="Stealth">
{virus.stealth}
</LabeledList.Item>
</Tooltip>
<Tooltip content="Decides the spread type.">
<LabeledList.Item
color={GetColor(virus.transmission)}
label="Transmissibility">
{virus.transmission}
</LabeledList.Item>
</Tooltip>
</LabeledList>
</Section>
);
};
/** Similar to the virus info display.
* Returns info about symptoms as collapsibles.
*/
const SymptomDisplay = (props: SymptomDisplayProps) => {
const { symptoms = [] } = props;
if (!symptoms || !symptoms.length) {
return <NoticeBox>No symptoms detected.</NoticeBox>;
}
return (
<Section fill title="Symptoms">
{symptoms.map((symptom) => {
return (
<Collapsible key={symptom.name} title={symptom.name}>
<Stack fill>
<Stack.Item grow={3}>
{symptom.desc}
<ThresholdDisplay thresholds={symptom.threshold_desc} />
</Stack.Item>
<Stack.Divider />
<Stack.Item grow={1}>
<SymptomTraitInfo symptom={symptom} />
</Stack.Item>
</Stack>
</Collapsible>
);
})}
</Section>
);
};
/** Displays the numerical trait modifiers for a virus symptom */
const SymptomTraitInfo = (props: SymptomInfoProps) => {
const { symptom } = props;
return (
<Section title="Modifiers">
<LabeledList>
<Tooltip content="Rarity of the symptom.">
<LabeledList.Item color={GetColor(symptom.level)} label="Level">
{symptom.level}
</LabeledList.Item>
</Tooltip>
<Tooltip content="Decides the cure complexity.">
<LabeledList.Item
color={GetColor(symptom.resistance)}
label="Resistance">
{symptom.resistance}
</LabeledList.Item>
</Tooltip>
<Tooltip content="Symptomic progression.">
<LabeledList.Item
color={GetColor(symptom.stage_speed)}
label="Stage Speed">
{symptom.stage_speed}
</LabeledList.Item>
</Tooltip>
<Tooltip content="Detection difficulty from medical equipment.">
<LabeledList.Item color={GetColor(symptom.stealth)} label="Stealth">
{symptom.stealth}
</LabeledList.Item>
</Tooltip>
<Tooltip content="Decides the spread type.">
<LabeledList.Item
color={GetColor(symptom.transmission)}
label="Transmission">
{symptom.transmission}
</LabeledList.Item>
</Tooltip>
</LabeledList>
</Section>
);
};
/** Displays threshold data */
const ThresholdDisplay = (props: ThresholdDisplayProps) => {
const { thresholds = [] } = props;
let convertedThresholds: Threshold[] = [];
// Converts obj of obj => array of thresholds
// I'm sure there's a more succinct way to do this
Object.entries(thresholds).map((label) => {
return convertedThresholds.push({
label: label[0],
descr: label[1].toString(),
});
});
return (
<Section mt={1} title="Thresholds">
{!convertedThresholds.length ? (
<NoticeBox>None</NoticeBox>
) : (
<LabeledList>
{convertedThresholds.map((threshold) => {
return (
<LabeledList.Item key={threshold.label} label={threshold.label}>
{threshold.descr}
</LabeledList.Item>
);
})}
</LabeledList>
)}
</Section>
);
};
/** Gives a color gradient based on the severity of the symptom. */
const GetColor = (severity: number) => {
if (severity <= -10) {
return 'blue';
} else if (severity <= -5) {
return 'darkturquoise';
} else if (severity <= 0) {
return 'green';
} else if (severity <= 7) {
return 'yellow';
} else if (severity <= 13) {
return 'orange';
} else {
return 'bad';
}
};
@@ -0,0 +1,135 @@
import { capitalizeFirst } from 'common/string';
import { useBackend } from 'tgui/backend';
import { Button, LabeledList, NoticeBox, ProgressBar, Section, Stack } from 'tgui/components';
import { Data } from './types';
/** Displays loaded container info, if it exists */
export const BeakerDisplay = (props, context) => {
const { act, data } = useBackend<Data>(context);
const { has_beaker, beaker, has_blood } = data;
const cant_empty = !has_beaker || !beaker?.volume;
let content;
if (!has_beaker) {
content = <NoticeBox>No beaker loaded.</NoticeBox>;
} else if (!beaker?.volume) {
content = <NoticeBox>Beaker is empty.</NoticeBox>;
} else if (!has_blood) {
content = <NoticeBox>No blood sample loaded.</NoticeBox>;
} else {
content = (
<Stack vertical>
<Stack.Item>
<Info />
</Stack.Item>
<Stack.Item>
<Antibodies />
</Stack.Item>
</Stack>
);
}
return (
<Section
title="Beaker"
buttons={
<>
<Button
icon="times"
content="Empty and Eject"
color="bad"
disabled={cant_empty}
onClick={() => act('empty_eject_beaker')}
/>
<Button
icon="trash"
content="Empty"
disabled={cant_empty}
onClick={() => act('empty_beaker')}
/>
<Button
icon="eject"
content="Eject"
disabled={!has_beaker}
onClick={() => act('eject_beaker')}
/>
</>
}>
{content}
</Section>
);
};
/** Displays info about the blood type, beaker capacity - volume */
const Info = (props, context) => {
const { data } = useBackend<Data>(context);
const { beaker, blood } = data;
if (!beaker || !blood) {
return <NoticeBox>No beaker loaded</NoticeBox>;
}
return (
<Stack>
<Stack.Item grow={2}>
<LabeledList>
<LabeledList.Item label="DNA">
{capitalizeFirst(blood.dna)}
</LabeledList.Item>
<LabeledList.Item label="Type">
{capitalizeFirst(blood.type)}
</LabeledList.Item>
</LabeledList>
</Stack.Item>
<Stack.Item grow={2}>
<LabeledList>
<LabeledList.Item label="Container">
<ProgressBar
color="darkred"
value={beaker.volume}
minValue={0}
maxValue={beaker.capacity}
ranges={{
'good': [beaker.capacity * 0.85, beaker.capacity],
'average': [beaker.capacity * 0.25, beaker.capacity * 0.85],
'bad': [0, beaker.capacity * 0.25],
}}
/>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
</Stack>
);
};
/** If antibodies are present, returns buttons to create vaccines */
const Antibodies = (props, context) => {
const { act, data } = useBackend<Data>(context);
const { is_ready, resistances = [] } = data;
if (!resistances) {
return <NoticeBox>Nothing detected</NoticeBox>;
}
return (
<LabeledList>
<LabeledList.Item label="Antibodies">
{!resistances.length
? 'None'
: resistances.map((resistance) => {
return (
<Button
key={resistance.name}
icon="eye-dropper"
disabled={!is_ready}
tooltip="Creates a vaccine bottle."
onClick={() =>
act('create_vaccine_bottle', {
index: resistance.id,
})
}>
{`${resistance.name}`}
</Button>
);
})}
</LabeledList.Item>
</LabeledList>
);
};
@@ -0,0 +1,69 @@
import { useBackend, useLocalState } from 'tgui/backend';
import { Button, NoticeBox, Section, Stack, Tabs } from 'tgui/components';
import { Data } from './types';
import { SymptomDisplay } from './Symptom';
import { VirusDisplay } from './Virus';
export const SpecimenDisplay = (props, context) => {
const { data } = useBackend<Data>(context);
const { viruses = [] } = data;
const [tab, setTab] = useLocalState(context, 'tab', 0);
const virus = viruses[tab];
return (
<Section fill scrollable title="Specimen" buttons={<Buttons />}>
{!virus ? (
<NoticeBox success>Nothing detected.</NoticeBox>
) : (
<Stack fill vertical>
<Stack.Item>
<VirusDisplay virus={virus} />
</Stack.Item>
<Stack.Item>
{virus?.symptoms && <SymptomDisplay symptoms={virus.symptoms} />}
</Stack.Item>
</Stack>
)}
</Section>
);
};
const Buttons = (props, context) => {
const { act, data } = useBackend<Data>(context);
const { is_ready, viruses = [] } = data;
const [tab, setTab] = useLocalState(context, 'tab', 0);
const virus = viruses[tab];
return (
<Stack>
{viruses.length > 1 && (
<Stack.Item>
<Tabs>
{viruses.map((virus, index) => {
return (
<Tabs.Tab
selected={tab === index}
onClick={() => setTab(index)}
key={index}>
{virus.name}
</Tabs.Tab>
);
})}
</Tabs>
</Stack.Item>
)}
<Stack.Item>
<Button
icon="flask"
content="Create culture bottle"
disabled={!is_ready}
onClick={() =>
act('create_culture_bottle', {
index: virus.index,
})
}
/>
</Stack.Item>
</Stack>
);
};
@@ -0,0 +1,99 @@
import { Collapsible, LabeledList, NoticeBox, Section, Stack, Tooltip } from 'tgui/components';
import { getColor } from './helpers';
import { Threshold } from './types';
/**
* Similar to the virus info display.
* Returns info about symptoms as collapsibles.
*/
export const SymptomDisplay = (props, context) => {
const { symptoms = [] } = props;
if (!symptoms?.length) {
return <NoticeBox>No symptoms detected.</NoticeBox>;
}
return (
<Section fill title="Symptoms">
{symptoms.map((symptom) => {
const { name, desc, threshold_desc } = symptom;
return (
<Collapsible key={name} title={name}>
<Stack fill>
<Stack.Item grow={3}>
{desc}
<Thresholds thresholds={threshold_desc} />
</Stack.Item>
<Stack.Divider />
<Stack.Item grow={1}>
<Traits symptom={symptom} />
</Stack.Item>
</Stack>
</Collapsible>
);
})}
</Section>
);
};
/** Displays threshold data */
const Thresholds = (props, context) => {
const { thresholds = [] } = props;
let convertedThresholds = Object.entries<Threshold>(thresholds);
return (
<Section mt={1} title="Thresholds">
{!convertedThresholds.length ? (
<NoticeBox>None</NoticeBox>
) : (
<LabeledList>
{convertedThresholds.map(([label, descr], index) => {
return (
<LabeledList.Item key={index} label={label}>
{descr}
</LabeledList.Item>
);
})}
</LabeledList>
)}
</Section>
);
};
/** Displays the numerical trait modifiers for a virus symptom */
const Traits = (props, context) => {
const {
symptom: { level, resistance, stage_speed, stealth, transmission },
} = props;
return (
<Section title="Modifiers">
<LabeledList>
<Tooltip content="Rarity of the symptom.">
<LabeledList.Item color={getColor(level)} label="Level">
{level}
</LabeledList.Item>
</Tooltip>
<Tooltip content="Decides the cure complexity.">
<LabeledList.Item color={getColor(resistance)} label="Resistance">
{resistance}
</LabeledList.Item>
</Tooltip>
<Tooltip content="Symptomic progression.">
<LabeledList.Item color={getColor(stage_speed)} label="Stage Speed">
{stage_speed}
</LabeledList.Item>
</Tooltip>
<Tooltip content="Detection difficulty from medical equipment.">
<LabeledList.Item color={getColor(stealth)} label="Stealth">
{stealth}
</LabeledList.Item>
</Tooltip>
<Tooltip content="Decides the spread type.">
<LabeledList.Item color={getColor(transmission)} label="Transmission">
{transmission}
</LabeledList.Item>
</Tooltip>
</LabeledList>
</Section>
);
};
@@ -0,0 +1,104 @@
import { capitalizeFirst, decodeHtmlEntities } from 'common/string';
import { useBackend } from 'tgui/backend';
import { Box, Input, LabeledList, Section, Stack, Tooltip } from 'tgui/components';
import { getColor } from './helpers';
import { Data } from './types';
/**
* Displays info about the virus. Child elements display
* the virus's traits and descriptions.
*/
export const VirusDisplay = (props, context) => {
const { virus } = props;
return (
<Stack fill>
<Stack.Item grow={3}>
<Info virus={virus} />
</Stack.Item>
{virus.is_adv && (
<>
<Stack.Divider />
<Stack.Item grow={1}>
<Traits virus={virus} />
</Stack.Item>
</>
)}
</Stack>
);
};
/** Displays the description, name and other info for the virus. */
const Info = (props, context) => {
const { act } = useBackend<Data>(context);
const {
virus: { agent, can_rename, cure, description, index, name, spread },
} = props;
return (
<LabeledList>
<LabeledList.Item label="Name">
{can_rename ? (
<Input
placeholder="Input a name"
value={name === 'Unknown' ? '' : name}
onChange={(_, value) =>
act('rename_disease', {
index: index,
name: value,
})
}
/>
) : (
<Box color="bad">{decodeHtmlEntities(name)}</Box>
)}
</LabeledList.Item>
<LabeledList.Item label="Description">{description}</LabeledList.Item>
<LabeledList.Item label="Agent">
{capitalizeFirst(agent)}
</LabeledList.Item>
<LabeledList.Item label="Spread">{spread}</LabeledList.Item>
<LabeledList.Item label="Possible Cure">{cure}</LabeledList.Item>
</LabeledList>
);
};
/**
* Displays the traits of the virus. This could be iterated over
* with object.keys but you would need a helper function for the tooltips.
* I would rather hard code it here.
*/
const Traits = (props, context) => {
const {
virus: { resistance, stage_speed, stealth, transmission },
} = props;
return (
<Section title="Statistics">
<LabeledList>
<Tooltip content="Decides the cure complexity.">
<LabeledList.Item color={getColor(resistance)} label="Resistance">
{resistance}
</LabeledList.Item>
</Tooltip>
<Tooltip content="Symptomic progression.">
<LabeledList.Item color={getColor(stage_speed)} label="Stage speed">
{stage_speed}
</LabeledList.Item>
</Tooltip>
<Tooltip content="Detection difficulty from medical equipment.">
<LabeledList.Item color={getColor(stealth)} label="Stealth">
{stealth}
</LabeledList.Item>
</Tooltip>
<Tooltip content="Decides the spread type.">
<LabeledList.Item
color={getColor(transmission)}
label="Transmissibility">
{transmission}
</LabeledList.Item>
</Tooltip>
</LabeledList>
</Section>
);
};
@@ -0,0 +1,16 @@
/** Gives a color gradient based on the severity of the symptom. */
export const getColor = (severity: number) => {
if (severity <= -10) {
return 'blue';
} else if (severity <= -5) {
return 'darkturquoise';
} else if (severity <= 0) {
return 'green';
} else if (severity <= 7) {
return 'yellow';
} else if (severity <= 13) {
return 'orange';
} else {
return 'bad';
}
};
@@ -0,0 +1,28 @@
import { useBackend } from 'tgui/backend';
import { Stack } from 'tgui/components';
import { Window } from 'tgui/layouts';
import { Data } from './types';
import { BeakerDisplay } from './Beaker';
import { SpecimenDisplay } from './Specimen';
export const Pandemic = (props, context) => {
const { data } = useBackend<Data>(context);
const { has_beaker, has_blood } = data;
return (
<Window width={650} height={500}>
<Window.Content>
<Stack fill vertical>
<Stack.Item>
<BeakerDisplay />
</Stack.Item>
{!!has_beaker && !!has_blood && (
<Stack.Item grow>
<SpecimenDisplay />
</Stack.Item>
)}
</Stack>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,59 @@
import { BooleanLike } from 'common/react';
export type Data = {
beaker?: Beaker;
blood?: Blood;
has_beaker: BooleanLike;
has_blood: BooleanLike;
is_ready: BooleanLike;
resistances?: Resistance[];
viruses?: Virus[];
};
type Beaker = {
volume: number;
capacity: number;
};
type Blood = {
dna: string;
type: string;
};
type Resistance = {
id: string;
name: string;
};
type Virus = {
name: string;
can_rename: BooleanLike;
is_adv: BooleanLike;
symptoms?: Symptom[];
resistance: number;
stealth: number;
stage_speed: number;
transmission: number;
index: number;
agent: string;
description: string;
spread: string;
cure: string;
};
export type Symptom = {
name: string;
desc: string;
stealth: number;
resistance: number;
stage_speed: number;
transmission: number;
level: number;
neutered: BooleanLike;
threshold_desc: Threshold[];
};
export type Threshold = {
label: string;
descr: string;
};