From b1a4e0ba7ee4c4c0b9b2b4144a2bbdf5cfe97da6 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Sat, 1 Jun 2024 11:51:04 -0500 Subject: [PATCH] Makes disease airborne spread more realistic (#83423) ## About The Pull Request 1. Rather than checking every life tick to try to spread via air, it checks every breath tick. IE, every 8 seconds. 2. Wearing a mask now protects you from catching airborne diseases, rather than only protecting you from spreading it. 3. Wearing a mask is no longer guaranteed effective (unless on internals), it now scales based on BIO armor. This is doubly effectively for people wearing a mask to stop spreading to others vs wearing a mask to block spreading to them. ## Why It's Good For The Game Wearing any kind of mask means airborne viruses will never spread, but also wearing any kind of mask on the opposite end is pointless unless on internals. Kinda silly. This PR unifies those behaviors. Wearing a mask to stop spread is still rewarded, as BIO armor is doubly effective at stopping a spread attempt as blocking a spread attempt. Ultimately the goal of this is to give players a realistic way to protect themselves against airborne transmission vectors besides staying on internals 100% of the time. ## Changelog :cl: Melbert balance: Wearing a mask can now prevent you from being spread airborne diseases. Prior, it only stopped you from spreading it yourself. balance: Wearing a mask no longer 100% guarantees you don't spread viruses to others people - it now scales on the masks BIO armor. Get a sterile mask from Medical for max safety. balance: Wearing a mask to stop you from spreading to other people is doubly effective than wearing a mask to prevent spread to yourself - Mask up for the greater good! balance: Passive airborne virus spread attempts are now done every time you breathe (every eight seconds), rather than every two seconds. The chance of spread is the same, though - it has been scaled up to accommodate. code: Cleaned up a fair bid of airborne-transmission-code. Report any oddities. /:cl: --- code/datums/diseases/_MobProcs.dm | 85 +++++++++++++---- code/datums/diseases/_disease.dm | 95 ++++++++++++------- code/datums/diseases/advance/advance.dm | 12 +-- .../datums/diseases/advance/symptoms/cough.dm | 3 +- code/datums/diseases/advance/symptoms/fire.dm | 2 +- .../diseases/advance/symptoms/narcolepsy.dm | 6 +- .../diseases/advance/symptoms/sneeze.dm | 5 +- code/modules/events/disease_outbreak.dm | 6 +- code/modules/mob/living/carbon/life.dm | 6 +- .../surgery/organs/internal/lungs/_lungs.dm | 1 - 10 files changed, 143 insertions(+), 78 deletions(-) diff --git a/code/datums/diseases/_MobProcs.dm b/code/datums/diseases/_MobProcs.dm index 62ee5e457ca..337aee46c03 100644 --- a/code/datums/diseases/_MobProcs.dm +++ b/code/datums/diseases/_MobProcs.dm @@ -94,24 +94,19 @@ if(passed) disease.try_infect(src) -/mob/living/proc/AirborneContractDisease(datum/disease/disease, force_spread) - if(HAS_TRAIT(src, TRAIT_VIRUS_RESISTANCE) && prob(75)) - return - - if(((disease.spread_flags & DISEASE_SPREAD_AIRBORNE) || force_spread) && prob(min((50*disease.spreading_modifier - 1), 50))) - ForceContractDisease(disease) - -/mob/living/carbon/AirborneContractDisease(datum/disease/disease, force_spread) - if(internal) - return - if(HAS_TRAIT(src, TRAIT_NOBREATH)) - return - +/** + * Handle being contracted a disease via airborne transmission + * + * * disease - the disease datum that's infecting us + */ +/mob/living/proc/contract_airborne_disease(datum/disease/disease) + if(!can_be_spread_airborne_disease()) + return FALSE + if(!prob(min((50 * disease.spreading_modifier - 1), 50))) + return FALSE if(!disease.has_required_infectious_organ(src, ORGAN_SLOT_LUNGS)) - return - - ..() - + return FALSE + return ForceContractDisease(disease) //Proc to use when you 100% want to try to infect someone (ignoreing protective clothing and such), as long as they aren't immune /mob/living/proc/ForceContractDisease(datum/disease/D, make_copy = TRUE, del_on_fail = FALSE) @@ -136,8 +131,56 @@ return ..() -/mob/living/proc/CanSpreadAirborneDisease() - return !is_mouth_covered() +/// Checks if this mob can currently spread air based diseases. +/// Nondeterministic +/mob/living/proc/can_spread_airborne_diseases() + SHOULD_CALL_PARENT(TRUE) + if(HAS_TRAIT(src, TRAIT_NOBREATH)) + return FALSE + if(losebreath >= 1) + return FALSE + // I don't know how you are spreading via air with no head but sure + if(!get_bodypart(BODY_ZONE_HEAD)) + return TRUE + // Check both hat and mask for bio protection + // Anything above 50 individually is a shoe-in, and stacking two items at 25 is also a shoe-in + var/obj/item/clothing/hat = is_mouth_covered(ITEM_SLOT_HEAD) + var/obj/item/clothing/mask = is_mouth_covered(ITEM_SLOT_MASK) + var/total_prot = 2 * (hat?.get_armor_rating(BIO) + mask?.get_armor_rating(BIO)) + if(prob(total_prot)) + return FALSE -/mob/living/carbon/CanSpreadAirborneDisease() - return !((head && (head.flags_cover & HEADCOVERSMOUTH) && (head.get_armor_rating(BIO) >= 25)) || (wear_mask && (wear_mask.flags_cover & MASKCOVERSMOUTH) && (wear_mask.get_armor_rating(BIO) >= 25))) + return TRUE + +/mob/living/carbon/can_spread_airborne_diseases() + if(internal || external) + return FALSE + + return ..() + +/// Checks if this mob can currently be infected by air based diseases +/// Nondeterministic +/mob/living/proc/can_be_spread_airborne_disease() + if(HAS_TRAIT(src, TRAIT_NOBREATH)) + return FALSE + if(losebreath >= 1) + return FALSE + // Spaceacillin for infection resistance + if(HAS_TRAIT(src, TRAIT_VIRUS_RESISTANCE) && prob(75)) + return FALSE + // Bio check for head AND mask + // Meaning if we're masked up and wearing a dome, we are very likely never getting sick + var/obj/item/clothing/hat = is_mouth_covered(ITEM_SLOT_HEAD) + var/obj/item/clothing/mask = is_mouth_covered(ITEM_SLOT_MASK) + var/total_prot = (hat?.get_armor_rating(BIO) + mask?.get_armor_rating(BIO)) + if(prob(total_prot)) + return FALSE + + return TRUE + +/mob/living/carbon/can_be_spread_airborne_disease() + // Using an isolated air supply is also effective + if((internal || external) && prob(75)) + return FALSE + + return ..() diff --git a/code/datums/diseases/_disease.dm b/code/datums/diseases/_disease.dm index da0a849989e..d66bc85bb55 100644 --- a/code/datums/diseases/_disease.dm +++ b/code/datums/diseases/_disease.dm @@ -70,10 +70,33 @@ D.after_add() infectee.med_hud_set_status() + register_disease_signals() var/turf/source_turf = get_turf(infectee) log_virus("[key_name(infectee)] was infected by virus: [src.admin_details()] at [loc_name(source_turf)]") +/// Updates the spread flags set, ensuring signals are updated as necessary +/datum/disease/proc/update_spread_flags(new_flags) + if(spread_flags == new_flags) + return + + spread_flags = new_flags + unregister_disease_signals() + register_disease_signals() + +/// Register any relevant signals for the disease +/datum/disease/proc/register_disease_signals() + if(isnull(affected_mob)) + return + if(spread_flags & DISEASE_SPREAD_AIRBORNE) + RegisterSignal(affected_mob, COMSIG_CARBON_PRE_BREATHE, PROC_REF(on_breath)) + +/// Unregister any relevant signals for the disease +/datum/disease/proc/unregister_disease_signals() + if(isnull(affected_mob)) + return + UnregisterSignal(affected_mob, COMSIG_CARBON_PRE_BREATHE) + ///Proc to process the disease and decide on whether to advance, cure or make the symptoms appear. Returns a boolean on whether to continue acting on the symptoms or not. /datum/disease/proc/stage_act(seconds_per_tick, times_fired) var/slowdown = HAS_TRAIT(affected_mob, TRAIT_VIRUS_RESISTANCE) ? 0.5 : 1 // spaceacillin slows stage speed by 50% @@ -219,41 +242,41 @@ if(!. || (needs_all_cures && . < cures.len)) return FALSE -//Airborne spreading -/datum/disease/proc/spread(force_spread = 0) - if(!affected_mob) - return - +/** + * Handles performing a spread-via-air + * + * Checks for stuff like "is our mouth covered" for you + * + * * spread_range - How far the disease can spread + * * force_spread - If TRUE, the disease will spread regardless of the spread_flags + * * require_facing - If TRUE, the disease will only spread if the source mob is facing the target mob + */ +/datum/disease/proc/airborne_spread(spread_range = 2, force_spread = TRUE, require_facing = FALSE) + if(isnull(affected_mob)) + return FALSE if(!(spread_flags & DISEASE_SPREAD_AIRBORNE) && !force_spread) - return - - if(affected_mob.internal) //if you keep your internals on, no airborne spread at least - return - - if(HAS_TRAIT(affected_mob, TRAIT_NOBREATH)) //also if you don't breathe - return - + return FALSE + if(affected_mob.can_spread_airborne_diseases()) + return FALSE if(!has_required_infectious_organ(affected_mob, ORGAN_SLOT_LUNGS)) //also if you lack lungs - return - - if(!affected_mob.CanSpreadAirborneDisease()) //should probably check this huh - return - - if(HAS_TRAIT(affected_mob, TRAIT_VIRUS_RESISTANCE) || (affected_mob.satiety > 0 && prob(affected_mob.satiety/2))) //being full or on spaceacillin makes you less likely to spread a virus - return - - var/spread_range = 2 - - if(force_spread) - spread_range = force_spread - - var/turf/T = affected_mob.loc - if(istype(T)) - for(var/mob/living/carbon/C in oview(spread_range, affected_mob)) - var/turf/V = get_turf(C) - if(disease_air_spread_walk(T, V)) - C.AirborneContractDisease(src, force_spread) + return FALSE + if(HAS_TRAIT(affected_mob, TRAIT_VIRUS_RESISTANCE) || (affected_mob.satiety > 0 && prob(affected_mob.satiety / 2))) //being full or on spaceacillin makes you less likely to spread a virus + return FALSE + var/turf/mob_loc = affected_mob.loc + if(!istype(mob_loc)) + return FALSE + for(var/mob/living/carbon/to_infect in oview(spread_range, affected_mob)) + var/turf/infect_loc = to_infect.loc + if(!istype(infect_loc)) + continue + if(require_facing && !is_source_facing_target(affected_mob, to_infect)) + continue + if(!disease_air_spread_walk(mob_loc, infect_loc)) + continue + to_infect.contract_airborne_disease(src) + return TRUE +/// Helper for checking if there is an air path between two turfs /proc/disease_air_spread_walk(turf/start, turf/end) if(!start || !end) return FALSE @@ -307,6 +330,7 @@ return "[type]" /datum/disease/proc/remove_disease() + unregister_disease_signals() LAZYREMOVE(affected_mob.diseases, src) //remove the datum from the list affected_mob.med_hud_set_status() affected_mob = null @@ -346,6 +370,13 @@ return TRUE +/// Handles spreading via air when our mob breathes +/datum/disease/proc/on_breath(datum/source, seconds_per_tick, ...) + SIGNAL_HANDLER + + if(SPT_PROB(infectivity * 4, seconds_per_tick)) + airborne_spread() + //Use this to compare severities /proc/get_disease_severity_value(severity) switch(severity) diff --git a/code/datums/diseases/advance/advance.dm b/code/datums/diseases/advance/advance.dm index bce368237ad..8c8e8e02169 100644 --- a/code/datums/diseases/advance/advance.dm +++ b/code/datums/diseases/advance/advance.dm @@ -299,22 +299,22 @@ /datum/disease/advance/proc/set_spread(spread_id) switch(spread_id) if(DISEASE_SPREAD_NON_CONTAGIOUS) - spread_flags = DISEASE_SPREAD_NON_CONTAGIOUS + update_spread_flags(DISEASE_SPREAD_NON_CONTAGIOUS) spread_text = "None" if(DISEASE_SPREAD_SPECIAL) - spread_flags = DISEASE_SPREAD_SPECIAL + update_spread_flags(DISEASE_SPREAD_SPECIAL) spread_text = "None" if(DISEASE_SPREAD_BLOOD) - spread_flags = DISEASE_SPREAD_BLOOD + update_spread_flags(DISEASE_SPREAD_BLOOD) spread_text = "Blood" if(DISEASE_SPREAD_CONTACT_FLUIDS) - spread_flags = DISEASE_SPREAD_BLOOD | DISEASE_SPREAD_CONTACT_FLUIDS + update_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 + update_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 + update_spread_flags(DISEASE_SPREAD_BLOOD | DISEASE_SPREAD_CONTACT_FLUIDS | DISEASE_SPREAD_CONTACT_SKIN | DISEASE_SPREAD_AIRBORNE) spread_text = "Respiration" /datum/disease/advance/proc/set_severity(level_sev) diff --git a/code/datums/diseases/advance/symptoms/cough.dm b/code/datums/diseases/advance/symptoms/cough.dm index 0a29a633a30..6b46667741c 100644 --- a/code/datums/diseases/advance/symptoms/cough.dm +++ b/code/datums/diseases/advance/symptoms/cough.dm @@ -68,8 +68,7 @@ off_cooldown_coughed = TRUE else off_cooldown_coughed = FALSE - if(affected_mob.CanSpreadAirborneDisease()) - active_disease.spread(spread_range) + active_disease.airborne_spread(spread_range) if(power >= 1.5) var/obj/item/held_object = affected_mob.get_active_held_item() if(held_object && held_object.w_class == WEIGHT_CLASS_TINY) diff --git a/code/datums/diseases/advance/symptoms/fire.dm b/code/datums/diseases/advance/symptoms/fire.dm index 0708841fca0..3ec095feb5c 100644 --- a/code/datums/diseases/advance/symptoms/fire.dm +++ b/code/datums/diseases/advance/symptoms/fire.dm @@ -65,7 +65,7 @@ warn_mob(living_mob) if(infective) - A.spread(advanced_stage ? 4 : 2) + A.airborne_spread(advanced_stage ? 4 : 2) /datum/symptom/fire/proc/warn_mob(mob/living/living_mob) if(prob(33.33)) diff --git a/code/datums/diseases/advance/symptoms/narcolepsy.dm b/code/datums/diseases/advance/symptoms/narcolepsy.dm index cbf6462f2c4..a137e8acd91 100644 --- a/code/datums/diseases/advance/symptoms/narcolepsy.dm +++ b/code/datums/diseases/advance/symptoms/narcolepsy.dm @@ -64,8 +64,7 @@ if(yawning) M.emote("yawn") - if(M.CanSpreadAirborneDisease()) - A.spread(6) + A.airborne_spread(6) if(5) if(prob(50)) @@ -75,5 +74,4 @@ if(yawning) M.emote("yawn") - if(M.CanSpreadAirborneDisease()) - A.spread(6) + A.airborne_spread(6) diff --git a/code/datums/diseases/advance/symptoms/sneeze.dm b/code/datums/diseases/advance/symptoms/sneeze.dm index 6771d57f20b..a35e700f7b4 100644 --- a/code/datums/diseases/advance/symptoms/sneeze.dm +++ b/code/datums/diseases/advance/symptoms/sneeze.dm @@ -53,10 +53,7 @@ if(!suppress_warning) affected_mob.emote("sniff") else - if(affected_mob.CanSpreadAirborneDisease()) //don't spread germs if they covered their mouth - for(var/mob/living/exposed_mob in oview(spread_range, affected_mob)) - if(is_source_facing_target(affected_mob, exposed_mob) && disease_air_spread_walk(get_turf(affected_mob), get_turf(exposed_mob))) - exposed_mob.AirborneContractDisease(active_disease, TRUE) + active_disease.airborne_spread(spread_range, force_spread = TRUE, require_facing = TRUE) if(cartoon_sneezing) //Yeah, this can fling you around even if you have a space suit helmet on. It's, uh, bluespace snot, yeah. affected_mob.emote("sneeze") to_chat(affected_mob, span_userdanger("You are launched violently backwards by an all-mighty sneeze!")) diff --git a/code/modules/events/disease_outbreak.dm b/code/modules/events/disease_outbreak.dm index 881709c3165..8fcc05f345b 100644 --- a/code/modules/events/disease_outbreak.dm +++ b/code/modules/events/disease_outbreak.dm @@ -450,13 +450,13 @@ /datum/disease/advance/random/event/set_spread(spread_id) switch(spread_id) if(DISEASE_SPREAD_CONTACT_FLUIDS) - spread_flags = DISEASE_SPREAD_BLOOD | DISEASE_SPREAD_CONTACT_FLUIDS + update_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 + update_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 + update_spread_flags(DISEASE_SPREAD_BLOOD | DISEASE_SPREAD_CONTACT_FLUIDS | DISEASE_SPREAD_CONTACT_SKIN | DISEASE_SPREAD_AIRBORNE) spread_text = "Respiration" /** diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm index e5aa87a9422..96fc7f5ecd6 100644 --- a/code/modules/mob/living/carbon/life.dm +++ b/code/modules/mob/living/carbon/life.dm @@ -68,10 +68,10 @@ // Second link in a breath chain, calls [carbon/proc/check_breath()] /mob/living/carbon/proc/breathe(seconds_per_tick, times_fired) var/obj/item/organ/internal/lungs = get_organ_slot(ORGAN_SLOT_LUNGS) - if(SEND_SIGNAL(src, COMSIG_CARBON_ATTEMPT_BREATHE) & COMSIG_CARBON_BLOCK_BREATH) + if(SEND_SIGNAL(src, COMSIG_CARBON_ATTEMPT_BREATHE, seconds_per_tick, times_fired) & COMSIG_CARBON_BLOCK_BREATH) return - SEND_SIGNAL(src, COMSIG_CARBON_PRE_BREATHE) + SEND_SIGNAL(src, COMSIG_CARBON_PRE_BREATHE, seconds_per_tick, times_fired) var/datum/gas_mixture/environment if(loc) @@ -477,8 +477,6 @@ for(var/datum/disease/disease as anything in diseases) if(QDELETED(disease)) //Got cured/deleted while the loop was still going. continue - if(SPT_PROB(disease.infectivity, seconds_per_tick)) - disease.spread() if(stat != DEAD || disease.process_dead) disease.stage_act(seconds_per_tick, times_fired) diff --git a/code/modules/surgery/organs/internal/lungs/_lungs.dm b/code/modules/surgery/organs/internal/lungs/_lungs.dm index 6be62d7b2a2..25db3cdabfb 100644 --- a/code/modules/surgery/organs/internal/lungs/_lungs.dm +++ b/code/modules/surgery/organs/internal/lungs/_lungs.dm @@ -462,7 +462,6 @@ /obj/item/organ/internal/lungs/proc/too_much_miasma(mob/living/carbon/breather, datum/gas_mixture/breath, miasma_pp, old_miasma_pp) // Inhale Miasma. Exhale nothing. breathe_gas_volume(breath, /datum/gas/miasma) - // Miasma side effects if (HAS_TRAIT(breather, TRAIT_ANOSMIA)) //Anosmia quirk holder cannot smell miasma, but can get diseases from it. return