From d28e26ddcd8c1fbebf18fc1ff9ea60aeef6ef55c Mon Sep 17 00:00:00 2001 From: lessthanthree <83487515+lessthnthree@users.noreply.github.com> Date: Sat, 4 Feb 2023 22:27:44 -0800 Subject: [PATCH] Disease Outbreak: Advanced v3 (#73092) ## About The Pull Request PR to replace https://github.com/tgstation/tgstation/pull/73001 Revamps the Disease Outbreak: Advanced random event to be more consistent in actually generating random diseases. This generates random viruses based on rolling severity, symptoms, and transmissibility separately and ensures the virus created through RNG is valid and transmissible. Revamps menu for admin spawned DO:A events. Custom outbreak with manual symptom selection is available in the Secrets panel. Parameters for generating the viruses can be tweaked using the defines in the event file. ## Why It's Good For The Game Current DO:A code has a tendency to generate viruses that aren't actually transmissible, be it through choosing too many symptoms or having a transmissibility that's too low to allow spread. ## Changelog :cl: LT3 refactor: Rewrite of random event Disease Outbreak: Advanced. Listen to those level 7 biohazard warnings! admin: Fully customizable build-your-own virus is available in the admin Secrets panel, for those people you really hate /:cl: --------- Co-authored-by: san7890 --- code/datums/diseases/advance/advance.dm | 13 + code/modules/events/disease_outbreak.dm | 306 ++++++++++++++++++++---- 2 files changed, 271 insertions(+), 48 deletions(-) diff --git a/code/datums/diseases/advance/advance.dm b/code/datums/diseases/advance/advance.dm index 405b161b7d2..ed88bf54b16 100644 --- a/code/datums/diseases/advance/advance.dm +++ b/code/datums/diseases/advance/advance.dm @@ -499,3 +499,16 @@ /datum/disease/advance/proc/totalTransmittable() return properties["transmittable"] + +/** + * Make virus visible to heath scanners + */ +/datum/disease/advance/proc/MakeVisible() + visibility_flags &= ~HIDDEN_SCANNER + for(var/datum/disease/advance/virus in SSdisease.active_diseases) + if(!virus.id) + stack_trace("Advanced virus ID is empty or null!") + return + + if(virus.id == id) + virus.visibility_flags &= ~HIDDEN_SCANNER diff --git a/code/modules/events/disease_outbreak.dm b/code/modules/events/disease_outbreak.dm index 0f246a9248f..98831fb5b69 100644 --- a/code/modules/events/disease_outbreak.dm +++ b/code/modules/events/disease_outbreak.dm @@ -1,3 +1,24 @@ +/// Advanced virus lower limit for symptoms +#define ADV_MIN_SYMPTOMS 3 +/// Advanced virus upper limit for symptoms +#define ADV_MAX_SYMPTOMS 4 +/// How long the virus stays hidden before announcement +#define ADV_ANNOUNCE_DELAY 75 +/// Numerical define for medium severity advanced virus +#define ADV_DISEASE_MEDIUM 4 +/// Numerical define for harmful severity advanced virus +#define ADV_DISEASE_HARMFUL 5 +/// Numerical define for dangerous severity advanced virus +#define ADV_DISEASE_DANGEROUS 7 +/// Percentile for low severity advanced virus +#define ADV_RNG_LOW 30 +/// Percentile for mid severity advanced virus +#define ADV_RNG_MID 85 +/// Percentile for low transmissibility advanced virus +#define ADV_SPREAD_LOW 30 +/// Percentile for mid transmissibility advanced virus +#define ADV_SPREAD_MID 90 + /datum/round_event_control/disease_outbreak name = "Disease Outbreak: Classic" typepath = /datum/round_event/disease_outbreak @@ -5,7 +26,7 @@ min_players = 10 weight = 5 category = EVENT_CATEGORY_HEALTH - description = "A 'classic' virus will infect some members of the crew." //These are the ones with PERSONALITY + description = "A 'classic' virus will infect some members of the crew." admin_setup = /datum/event_admin_setup/disease_outbreak ///Disease recipient candidates var/list/disease_candidates = list() @@ -35,8 +56,31 @@ continue disease_candidates += candidate +/datum/event_admin_setup/disease_outbreak + ///Admin selected disease, to be passed down to the round_event + var/virus_type + +/// Checks for candidates. Returns false if there isn't enough +/datum/event_admin_setup/disease_outbreak/proc/candidate_check() + var/datum/round_event_control/disease_outbreak/disease_control = event_control + disease_control.generate_candidates() //can_spawn_event() is bypassed by admin_setup, so this makes sure that the candidates are still generated + return length(disease_control.disease_candidates) + +/datum/event_admin_setup/disease_outbreak/prompt_admins() + var/candidate_count = candidate_check() + if(!candidate_count) + tgui_alert(usr, "There are no candidates eligible to recieve a disease!", "Error") + return ADMIN_CANCEL_EVENT + tgui_alert(usr, "[candidate_count] candidates found!", "Disease Outbreak") + + if(tgui_alert(usr, "Select a specific disease?", "Sickening behavior", list("Yes", "No")) == "Yes") + virus_type = tgui_input_list(usr, "Warning: Some of these are EXTREMELY dangerous.","Bacteria Hysteria", subtypesof(/datum/disease)) + +/datum/event_admin_setup/disease_outbreak/apply_to_event(datum/round_event/disease_outbreak/event) + event.virus_type = virus_type + /datum/round_event/disease_outbreak - announce_when = 120 + announce_when = ADV_ANNOUNCE_DELAY ///The disease type we will be spawning var/datum/disease/virus_type ///Disease recipient candidates, passed from the round_event_control object @@ -46,7 +90,7 @@ priority_announce("Confirmed outbreak of level 7 viral biohazard aboard [station_name()]. All personnel must contain the outbreak.", "Biohazard Alert", ANNOUNCER_OUTBREAK7) /datum/round_event/disease_outbreak/setup() - announce_when = rand(60, 180) + announce_when = ADV_ANNOUNCE_DELAY /datum/round_event/disease_outbreak/start() var/datum/round_event_control/disease_outbreak/disease_event = control @@ -83,81 +127,247 @@ name = "Disease Outbreak: Advanced" typepath = /datum/round_event/disease_outbreak/advanced category = EVENT_CATEGORY_HEALTH - description = "An 'advanced' disease will infect some members of the crew." //These are the ones that get viro lynched! + weight = 10 + description = "An 'advanced' disease will infect some members of the crew." admin_setup = /datum/event_admin_setup/disease_outbreak/advanced +/datum/event_admin_setup/disease_outbreak/advanced + ///Admin selected custom severity rating for the event + var/chosen_severity + ///Admin selected custom value for the maximum symptoms this virus should have + var/chosen_max_symptoms + +/** + * Admin virus customization + * + * If the admin wishes, give them the opportunity to select the severity and number of symptoms. + */ +/datum/event_admin_setup/disease_outbreak/advanced/prompt_admins() + var/candidate_count = candidate_check() + if(!candidate_count) + tgui_alert(usr, "There are no candidates eligible to recieve a disease!", "Error") + return ADMIN_CANCEL_EVENT + tgui_alert(usr, "[candidate_count] candidates found!", "Disease Outbreak") + + if(tgui_alert(usr,"Customize your virus?", "Glorified Debug Tool", list("Yes", "No")) == "Yes") + chosen_severity = tgui_alert(usr, "Pick a severity!", "In the event of an airborne virus, try not to breathe.", list("Medium", "Harmful", "Dangerous")) + switch(chosen_severity) + if("Medium") + chosen_severity = ADV_DISEASE_MEDIUM + if("Harmful") + chosen_severity = ADV_DISEASE_HARMFUL + if("Dangerous") + chosen_severity = ADV_DISEASE_DANGEROUS + else + return ADMIN_CANCEL_EVENT + + chosen_max_symptoms = tgui_input_number(usr, "How many symptoms do you want your virus to have?", "A pox upon ye!", 4, 15) + + else + chosen_severity = null + chosen_max_symptoms = null + + if(tgui_alert(usr,"Are you happy with your selections?", "Epidemic warning, Standby!", list("Yes", "Cancel")) != "Yes") + return ADMIN_CANCEL_EVENT + +/datum/event_admin_setup/disease_outbreak/advanced/apply_to_event(datum/round_event/disease_outbreak/advanced/event) + event.max_severity = chosen_severity + event.max_symptoms = chosen_max_symptoms + /datum/round_event/disease_outbreak/advanced ///Number of symptoms for our virus var/max_severity //Maximum symptoms for our virus var/max_symptoms +/** + * Generate virus base values + * + * Generates a virus with either the admin selected parameters for severity and symptoms + * or if it was not selected, randomly pick between the MIX and MAX configured in the defines. + */ /datum/round_event/disease_outbreak/advanced/start() var/datum/round_event_control/disease_outbreak/advanced/disease_event = control afflicted += disease_event.disease_candidates - disease_event.disease_candidates.Cut() //Clean the list after use + disease_event.disease_candidates.Cut() if(!max_symptoms) - max_symptoms = 3 + max(FLOOR((world.time - control.earliest_start)/6000, 1),0) //3 symptoms at 20 minutes, plus 1 per 10 minutes. - max_symptoms = clamp(max_symptoms, 3, 8) //Capping the virus symptoms prevents the event from becoming "smite one poor player with an -12 transmission hell virus" after a certain round length. + max_symptoms = rand(ADV_MIN_SYMPTOMS, ADV_MAX_SYMPTOMS) if(!max_severity) - max_severity = 3 + max(FLOOR((world.time - control.earliest_start)/6000, 1),0) //Max severity doesn't need clamping + var/rng_severity = rand(1, 100) + if(rng_severity < ADV_RNG_LOW) + max_severity = ADV_DISEASE_MEDIUM - var/datum/disease/advance/advanced_disease = new /datum/disease/advance/random(max_symptoms, max_severity) + else if(rng_severity < ADV_RNG_MID) + max_severity = ADV_DISEASE_HARMFUL - var/list/name_symptoms = list() //for feedback - for(var/datum/symptom/new_symptom in advanced_disease.symptoms) + else + max_severity = ADV_DISEASE_DANGEROUS + + var/datum/disease/advance/advanced_disease = new /datum/disease/advance/random/event(max_symptoms, max_severity) + + var/list/name_symptoms = list() + for(var/datum/symptom/new_symptom as anything in advanced_disease.symptoms) name_symptoms += new_symptom.name var/mob/living/carbon/human/victim = pick_n_take(afflicted) if(victim.ForceContractDisease(advanced_disease, FALSE)) - message_admins("An event has triggered a random advanced virus outbreak on [ADMIN_LOOKUPFLW(victim)]! It has these symptoms: [english_list(name_symptoms)]") - log_game("An event has triggered a random advanced virus outbreak on [key_name(victim)]! It has these symptoms: [english_list(name_symptoms)].") + message_admins("An event has triggered a random advanced virus outbreak on [ADMIN_LOOKUPFLW(victim)]! It has these symptoms: [english_list(name_symptoms)]. It is transmissable via [advanced_disease.spread_text].") + log_game("An event has triggered a random advanced virus outbreak on [key_name(victim)]! It has these symptoms: [english_list(name_symptoms)]. It is transmissable via [advanced_disease.spread_text].") announce_to_ghosts(victim) else log_game("An event attempted to trigger a random advanced virus outbreak on [key_name(victim)], but failed.") -/datum/event_admin_setup/disease_outbreak - ///Admin selected disease, to be passed down to the round_event - var/virus_type +/datum/disease/advance/random/event + name = "Experimental Disease" + copy_type = /datum/disease/advance -/// Checks for candidates. Returns false if there isn't enough -/datum/event_admin_setup/disease_outbreak/proc/candidate_check() - var/datum/round_event_control/disease_outbreak/disease_control = event_control - disease_control.generate_candidates() //can_spawn_event() is bypassed by admin_setup, so this makes sure that the candidates are still generated - return length(disease_control.disease_candidates) +/datum/round_event/disease_outbreak/advance/setup() + announce_when = ADV_ANNOUNCE_DELAY -/datum/event_admin_setup/disease_outbreak/prompt_admins() - var/candidate_count = candidate_check() - if(!candidate_check()) - tgui_alert(usr, "There are no candidates eligible to recieve a disease!", "Error") - return ADMIN_CANCEL_EVENT - tgui_alert(usr, "[candidate_count] candidates found!", "Disease Outbreak") +/** + * Generate advanced virus + * + * Uses the parameters to create a list of symptoms, picking from various severities + * Viral Evolution and Eternal Youth are special modifiers, so we roll separately. + */ - if(tgui_alert(usr, "Select a specific disease?", "Sickening behavior", list("Yes", "No")) == "Yes") - virus_type = tgui_input_list(usr, "Warning: Some of these are EXTREMELY dangerous.","Bacteria Hysteria", subtypesof(/datum/disease)) +/datum/disease/advance/random/event/New(max_symptoms, max_severity) + var/list/datum/symptom/possible_symptoms = list( + /datum/symptom/beard, + /datum/symptom/chills, + /datum/symptom/choking, + /datum/symptom/confusion, + /datum/symptom/cough, + /datum/symptom/deafness, + /datum/symptom/dizzy, + /datum/symptom/fever, + /datum/symptom/headache, + /datum/symptom/itching, + /datum/symptom/shedding, + /datum/symptom/sneeze, + /datum/symptom/vomit, + /datum/symptom/weight_loss, + ) -/datum/event_admin_setup/disease_outbreak/apply_to_event(datum/round_event/disease_outbreak/event) - event.virus_type = virus_type + switch(max_severity) + if(ADV_DISEASE_HARMFUL) + possible_symptoms += list( + /datum/symptom/disfiguration, + /datum/symptom/hallucigen, + /datum/symptom/polyvitiligo, + /datum/symptom/visionloss, + ) -/datum/event_admin_setup/disease_outbreak/advanced - ///Admin selected custom severity rating for the event - var/max_severity - ///Admin selected custom value for the maximum symptoms this virus should have - var/max_symptoms + if(ADV_DISEASE_DANGEROUS) + possible_symptoms += list( + /datum/symptom/alkali, + /datum/symptom/asphyxiation, + /datum/symptom/fire, + /datum/symptom/flesh_death, + /datum/symptom/flesh_eating, + /datum/symptom/narcolepsy, + /datum/symptom/voice_change, + ) -/datum/event_admin_setup/disease_outbreak/advanced/prompt_admins() - var/candidate_count = candidate_check() - if(!candidate_check()) - tgui_alert(usr, "There are no candidates eligible to recieve a disease!", "Error") - return ADMIN_CANCEL_EVENT - tgui_alert(usr, "[candidate_count] candidates found!", "Disease Outbreak") + for(var/i in 1 to max_symptoms) + var/datum/symptom/chosen_symptom = pick_n_take(possible_symptoms) + if(chosen_symptom) + var/datum/symptom/new_symptom = new chosen_symptom + symptoms += new_symptom - if(tgui_alert(usr,"Customize your virus?", "Glorified Debug Tool", list("Yes", "No")) == "Yes") - max_severity = tgui_input_number(usr, "Select a custom severity for your virus!", "Plague Incorporation!", 3, 8) - max_symptoms = tgui_input_number(usr, "How many symptoms do you want your virus to have?", "A pox upon ye!", 3, 15) + if(prob(50)) + symptoms += new /datum/symptom/viralevolution -/datum/event_admin_setup/disease_outbreak/advanced/apply_to_event(datum/round_event/disease_outbreak/advanced/event) - event.max_severity = max_severity - event.max_symptoms = max_symptoms + if(prob(50)) + symptoms += new /datum/symptom/youth + + Refresh() + + name = "Sample #[rand(1, 9999)]" + +/** + * Assign virus properties + * + * Now that we've picked our symptoms and severity, we determine the other stats + * (Stage Speed, Resistance, Transmissibility) + * The LOW/MID percentiles can be adjusted in the defines. + * If the virus is severity DANGEROUS we do not hide it from health scanners at event start. + * If the virus is airborne, also don't hide it. + */ +/datum/disease/advance/random/event/AssignProperties() + var/transmissibility = rand(1, 100) + + addtimer(CALLBACK(src, PROC_REF(MakeVisible)), ((ADV_ANNOUNCE_DELAY * 2) - 10) SECONDS) + + if(length(properties)) + spreading_modifier = max(CEILING(0.4 * properties["transmittable"], 1), 1) + cure_chance = clamp(7.5 - (0.5 * properties["resistance"]), 5, 10) // Can be between 5 and 10 + stage_prob = max(0.5 * properties["stage_rate"], 1) + SetSeverity(properties["severity"]) + visibility_flags |= HIDDEN_SCANNER + switch(severity) + if("Dangerous", "BIOHAZARD") + visibility_flags &= ~HIDDEN_SCANNER + + GenerateCure(properties) + + if(transmissibility < ADV_SPREAD_LOW) + SetSpread(DISEASE_SPREAD_CONTACT_FLUIDS) + + else if(transmissibility < ADV_SPREAD_MID) + SetSpread(DISEASE_SPREAD_CONTACT_SKIN) + + else + SetSpread(DISEASE_SPREAD_AIRBORNE) + visibility_flags &= ~HIDDEN_SCANNER + + else + CRASH("Advanced virus properties were empty or null!") + +/** + * Set the transmission methods on the generated virus + * + * Apply the transmission methods we rolled in the AssignProperties proc + */ +/datum/disease/advance/random/event/SetSpread(spread_id) + switch(spread_id) + if(DISEASE_SPREAD_CONTACT_FLUIDS) + spread_flags = DISEASE_SPREAD_BLOOD | DISEASE_SPREAD_CONTACT_FLUIDS + spread_text = "fluids" + if(DISEASE_SPREAD_CONTACT_SKIN) + spread_flags = DISEASE_SPREAD_BLOOD | DISEASE_SPREAD_CONTACT_FLUIDS | DISEASE_SPREAD_CONTACT_SKIN + spread_text = "skin contact" + if(DISEASE_SPREAD_AIRBORNE) + spread_flags = DISEASE_SPREAD_BLOOD | DISEASE_SPREAD_CONTACT_FLUIDS | DISEASE_SPREAD_CONTACT_SKIN | DISEASE_SPREAD_AIRBORNE + spread_text = "respiration" + +/** + * Determine the cure + * + * Rolls one of six possible cure groups, then selects a cure from it and applies it to the virus. + */ +/datum/disease/advance/random/event/GenerateCure() + if(!length(properties)) + stack_trace("Advanced virus properties were empty or null!") + return + var/res = rand(1, 6) + if(res == oldres) + return + cures = list(pick(advance_cures[res])) + oldres = res + // Get the cure name from the cure_id + var/datum/reagent/cure = GLOB.chemical_reagents_list[cures[1]] + cure_text = cure.name + +#undef ADV_MIN_SYMPTOMS +#undef ADV_MAX_SYMPTOMS +#undef ADV_ANNOUNCE_DELAY +#undef ADV_DISEASE_MEDIUM +#undef ADV_DISEASE_HARMFUL +#undef ADV_DISEASE_DANGEROUS +#undef ADV_RNG_LOW +#undef ADV_RNG_MID +#undef ADV_SPREAD_LOW +#undef ADV_SPREAD_MID