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

🆑 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.
/🆑
This commit is contained in:
MrMelbert
2024-06-01 17:51:04 +01:00
committed by GitHub
parent 8a571c8bdf
commit b1a4e0ba7e
10 changed files with 143 additions and 78 deletions
+64 -21
View File
@@ -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 ..()
+63 -32
View File
@@ -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)
+6 -6
View File
@@ -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)
@@ -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)
@@ -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))
@@ -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)
@@ -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!"))
+3 -3
View File
@@ -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"
/**
+2 -4
View File
@@ -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)
@@ -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