Infection Component & Germs (#19265)

* update these two

* Germs!

* Update vorestation.dme

* Basic signal application

* fixed

* Update germ_sensitive.dm

* Update germ_sensitive.dm

* Apply suggestion from @Cameron-The-Raven

* fix naming issues

---------

Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
This commit is contained in:
Cameron Lennox
2026-04-12 19:42:30 -04:00
committed by GitHub
co-authored by Kashargul
parent d94a4387f4
commit 439b534aee
12 changed files with 378 additions and 43 deletions
+1
View File
@@ -83,6 +83,7 @@ GLOBAL_VAR_INIT(refid_filter, TYPEID(filter(type="angular_blur")))
#define issimulatedturf(A) istype(A, /turf/simulated)
#define isopenspace(A) istype(A, /turf/simulated/open)
#define isspace(A) istype(A, /turf/space)
#define islava(A) istype(A, /turf/simulated/floor/lava)
#define isopenturf(A) (istype(A, /turf/simulated/open) || istype(A, /turf/space))
#define isnonsolidturf(A) (istype(A, /turf/simulated/open) || istype(A, /turf/space) || istype(A, /turf/simulated/floor/water) || istype(A, /turf/simulated/floor/lava))
#define ismineralturf(A) istype(A, /turf/simulated/mineral)
+4 -1
View File
@@ -59,8 +59,11 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_SLOBBER "slobber"
// Owner will be considered a tiny mob for some interactions, such as airlocks not opening unless they have a client, or being vacuumed up by the vacpack
#define TRAIT_AMBIENT_PEST_MOB "ambient_pest_mob"
///Trait given by /datum/component/germ_sensitive
#define TRAIT_GERM_SENSITIVE "germ_sensitive"
#define TRAIT_STRONG_STOMACH "strong_stomach"
/// Marks that this object is irradiated
#define TRAIT_IRRADIATED "iraddiated"
#define TRAIT_IRRADIATED "irradiated"
/// Whether or not this item will allow the radiation SS to go through standard
/// radiation processing as if this wasn't already irradiated.
/// Basically, without this, COMSIG_IN_RANGE_OF_IRRADIATION won't fire once the object is irradiated.
+25
View File
@@ -4,3 +4,28 @@
//Note: typecache can only replace istype if you know for sure the thing is at least a datum.
GLOBAL_LIST_INIT(typecache_stack, typecacheof(/obj/item/stack))
/// A typecache listing structures that are considered to have surfaces that you can place items on that are higher than the floor. This, of course, should be restricted to /atom/movables. This is primarily used for food decomposition code.
GLOBAL_LIST_INIT(typecache_elevated_structures, typecacheof(list(
/obj/machinery,
/obj/structure,
/obj/structure/table,
//Kitchen
/obj/machinery/smartfridge,
/obj/structure/bonfire,
/obj/structure/fireplace,
/obj/machinery/appliance/cooker/grill,
// /obj/machinery/griddle,
/obj/machinery/appliance/cooker/fryer,
/obj/machinery/processor,
/obj/machinery/microwave,
/obj/machinery/appliance/cooker/oven,
/obj/machinery/gibber,
/obj/machinery/icecream_vat,
//Botany
/obj/machinery/biogenerator,
/obj/machinery/portable_atmospherics/hydroponics, // So that harvest doesn't catch germs or decompose (includes dirt piles)
//Medbay
/obj/machinery/atmospherics/unary/cryo_cell,
/obj/machinery/chem_master, // Pills may catch germs
)))
+23
View File
@@ -637,3 +637,26 @@ GLOBAL_LIST_INIT(modulo_angle_to_dir, list(NORTH,NORTHEAST,EAST,SOUTHEAST,SOUTH,
var/list/l = i
return l.Copy()
return i
/proc/slot2body_zone(slot)
switch(slot)
if(SLOT_BACK, SLOT_OCLOTHING, SLOT_ICLOTHING, SLOT_BELT, SLOT_ID)
return BP_TORSO
if(SLOT_GLOVES)
return pick(BP_L_HAND, BP_R_HAND)
if(SLOT_HEAD, SLOT_TWOEARS, SLOT_TIE, SLOT_EARS)
return BP_HEAD
if(SLOT_MASK)
return BP_HEAD
if(SLOT_EYES)
return BP_HEAD
if(SLOT_FEET)
return pick(BP_R_FOOT, BP_L_FOOT)
// if(SLOT_LEGCUFFED)
// return pick(BP_L_LEG, BODY_ZONE_R_LEG)
@@ -0,0 +1,132 @@
// Don't eat off the floor or hold parent object with dirty hands, you'll get sick
/// Time needed for bacteria to infect the parent object
#define GERM_EXPOSURE_DELAY (5 SECONDS) // Five-second rule
/// Possible diseases
GLOBAL_LIST_INIT(floor_diseases, list(
/datum/disease/cold = 2,
/datum/disease/food_poisoning = 7,
/datum/disease/lycan = 1,
))
/// Makes items infective if left on floor, also sending corresponding signals to parent
/datum/component/germ_sensitive
/// Timer for counting delay before becoming infective
var/timer_id
/// Whether it is already infective
var/infective = FALSE
/datum/component/germ_sensitive/Initialize(mapload)
if(!isobj(parent))
return COMPONENT_INCOMPATIBLE
ADD_TRAIT(parent, TRAIT_GERM_SENSITIVE, REF(src))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(examine))
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(handle_movement))
RegisterSignals(parent, list(COMSIG_COMPONENT_CLEAN_ACT, COMSIG_ITEM_FRIED, COMSIG_ITEM_BARBEQUE_GRILLED, COMSIG_ATOM_FIRE_ACT), PROC_REF(delete_germs))
RegisterSignals(parent, list(
COMSIG_ITEM_DROPPED, //Dropped into the world
COMSIG_ATOM_EXITED, //Object exits a storage object (tables, boxes, etc)
),
PROC_REF(dropped))
RegisterSignals(parent, list(
COMSIG_ITEM_PICKUP, //Picked up by mob
COMSIG_ATOM_ENTERED, //Object enters a storage object (tables, boxes, etc.)
),
PROC_REF(picked_up))
// Map spawned items are protected until moved
if(!mapload)
handle_movement()
/datum/component/germ_sensitive/UnregisterFromParent()
REMOVE_TRAIT(parent, TRAIT_GERM_SENSITIVE, REF(src))
UnregisterSignal(parent, list(
COMSIG_ATOM_ENTERED,
COMSIG_ATOM_EXAMINE,
COMSIG_ATOM_EXITED,
COMSIG_COMPONENT_CLEAN_ACT,
COMSIG_ITEM_FRIED,
COMSIG_ITEM_BARBEQUE_GRILLED,
COMSIG_ATOM_FIRE_ACT,
COMSIG_ITEM_DROPPED,
COMSIG_ITEM_PICKUP,
COMSIG_MOVABLE_MOVED,
))
/datum/component/germ_sensitive/Destroy()
remove_timer()
return ..()
/datum/component/germ_sensitive/proc/remove_timer()
if(!timer_id)
return
deltimer(timer_id)
timer_id = null
/datum/component/germ_sensitive/proc/handle_movement()
SIGNAL_HANDLER
var/obj/parent_object = parent
var/turf/simulated/open_turf = parent_object.loc
// Is parent on simulated valid turf that is safe? Or held/in a pocket/in a closet/etc?
if(!istype(open_turf) || islava(open_turf) || ismineralturf(open_turf) || !parent_object.get_gravity())
remove_timer()
SEND_SIGNAL(parent, COMSIG_ATOM_GERM_UNEXPOSED, src)
return
// Is parent on an elevated structure?
for(var/atom/movable/content as anything in open_turf.contents)
if(GLOB.typecache_elevated_structures[content.type])
remove_timer()
SEND_SIGNAL(parent, COMSIG_ATOM_GERM_UNEXPOSED, src)
return
// Exposed to bacteria, start countdown until becoming infected
timer_id = addtimer(CALLBACK(src, PROC_REF(expose_to_germs)), GERM_EXPOSURE_DELAY, TIMER_STOPPABLE | TIMER_UNIQUE)
/datum/component/germ_sensitive/proc/picked_up()
SIGNAL_HANDLER
SEND_SIGNAL(parent, COMSIG_ATOM_GERM_UNEXPOSED, src)
remove_timer()
/datum/component/germ_sensitive/proc/dropped()
SIGNAL_HANDLER
handle_movement()
/datum/component/germ_sensitive/proc/examine(datum/source, mob/user, list/examine_list)
SIGNAL_HANDLER
if(infective)
examine_list += span_warning("[parent] looks dirty and not safe to consume.")
/datum/component/germ_sensitive/proc/expose_to_germs()
// Admin spawned items are never exposed NYI
// var/atom/parent_atom = parent
// if(parent_atom.flags_1 & ADMIN_SPAWNED_1)
// return
SEND_SIGNAL(parent, COMSIG_ATOM_GERM_EXPOSED, src)
if(infective)
return
infective = TRUE
var/random_disease = pick_weight(GLOB.floor_diseases)
parent.AddComponent(/datum/component/infective, new random_disease, weak = TRUE)
/datum/component/germ_sensitive/proc/delete_germs()
SIGNAL_HANDLER
. = NONE
if(infective)
infective = FALSE
qdel(parent.GetComponent(/datum/component/infective))
return COMPONENT_CLEANED
#undef GERM_EXPOSURE_DELAY
+182 -39
View File
@@ -1,43 +1,203 @@
/datum/component/infective
dupe_mode = COMPONENT_DUPE_ALLOWED
var/list/datum/disease/diseases
var/list/datum/disease/diseases //make sure these are the static, non-processing versions!
var/expire_time
var/required_clean_types = CLEAN_TYPE_DISEASE
/// The infection is weak and can only infect on consumption with small chance
var/is_weak = FALSE
/// Chance of weak infection on consumption
var/weak_infection_chance = 10
/datum/component/infective/Initialize(list/datum/disease/_diseases, expire_in)
if(islist(_diseases))
diseases = _diseases
else
diseases = list(_diseases)
if(!diseases.len || isnull(diseases[1]))
stack_trace("Infective component initialized without any diseases!")
qdel(src)
if(expire_in)
expire_time = world.time + expire_in
QDEL_IN(src, expire_in)
/datum/component/infective/Initialize(list/datum/disease/diseases, expire_in, weak = FALSE, weak_infection_chance = 10)
if(!ismovable(parent))
return COMPONENT_INCOMPATIBLE
if(!islist(diseases))
diseases = list(diseases)
///Make sure the diseases list is populated with instances of diseases so that it doesn't have to be for each AddComponent call.
for(var/datum/disease/disease as anything in diseases)
if(!disease) //empty entry, remove.
diseases -= disease
if(ispath(disease, /datum/disease))
var/datum/disease/instance = new disease
diseases -= disease
diseases += instance
else if(!istype(disease))
stack_trace("found [isdatum(disease) ? "an instance of [disease.type]" : disease] inside the diseases list argument for [type]")
diseases -= disease
src.diseases = diseases
if(expire_in)
expire_time = world.time + expire_in
QDEL_IN(src, expire_in)
is_weak = weak
src.weak_infection_chance = weak_infection_chance
/datum/component/infective/Destroy()
QDEL_LIST(diseases)
return ..()
/datum/component/infective/RegisterWithParent()
if(is_weak && isitem(parent))
RegisterSignal(parent, COMSIG_FOOD_EATEN, PROC_REF(try_infect_eat))
RegisterSignal(parent, COMSIG_PILL_CONSUMED, PROC_REF(try_infect_eat))
return
var/static/list/disease_connections = list(
COMSIG_ATOM_ENTERED = PROC_REF(try_infect_crossed),
)
AddComponent(/datum/component/connect_loc_behalf, parent, disease_connections)
RegisterSignal(parent, COMSIG_COMPONENT_CLEAN_ACT, PROC_REF(clean))
RegisterSignal(parent, COMSIG_MOVABLE_BUCKLE, PROC_REF(try_infect_buckle))
RegisterSignal(parent, COMSIG_MOVABLE_BUMP, PROC_REF(try_infect_collide))
RegisterSignal(parent, COMSIG_MOVABLE_IMPACT_ZONE, PROC_REF(try_infect_impact_zone))
RegisterSignal(parent, COMSIG_ATOM_EXTRAPOLATOR_ACT, PROC_REF(extrapolation))
if(isitem(parent))
RegisterSignal(parent, COMSIG_ITEM_ATTACK, PROC_REF(try_infect_attack))
// RegisterSignal(parent, COMSIG_FOOD_EATEN, PROC_REF(try_infect_eat)) - TODO: Send signal when eating
if(isitem(parent))
RegisterSignal(parent, COMSIG_ITEM_ATTACK_ZONE, PROC_REF(try_infect_attack_zone))
RegisterSignal(parent, COMSIG_ITEM_ATTACK, PROC_REF(try_infect_attack))
RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(try_infect_equipped))
RegisterSignal(parent, COMSIG_FOOD_EATEN, PROC_REF(try_infect_eat))
RegisterSignal(parent, COMSIG_PILL_CONSUMED, PROC_REF(try_infect_eat))
if(istype(parent, /obj/item/reagent_containers/glass))
RegisterSignal(parent, COMSIG_GLASS_DRANK, PROC_REF(try_infect_drink))
if(isorgan(parent))
RegisterSignal(parent, COMSIG_ORGAN_IMPLANTED, PROC_REF(on_organ_insertion))
/datum/component/infective/UnregisterFromParent()
. = ..()
UnregisterSignal(parent, list(
COMSIG_FOOD_EATEN,
COMSIG_PILL_CONSUMED,
COMSIG_COMPONENT_CLEAN_ACT,
COMSIG_MOVABLE_BUMP,
COMSIG_MOVABLE_IMPACT_ZONE,
COMSIG_ITEM_ATTACK_ZONE,
COMSIG_ITEM_ATTACK,
COMSIG_ITEM_EQUIPPED,
COMSIG_GLASS_DRANK,
COMSIG_ORGAN_IMPLANTED,
COMSIG_ATOM_EXTRAPOLATOR_ACT
))
qdel(GetComponent(/datum/component/connect_loc_behalf))
/datum/component/infective/proc/on_organ_insertion(obj/item/organ/target, mob/living/carbon/receiver)
SIGNAL_HANDLER
for(var/datum/disease/disease in diseases)
receiver.ForceContractDisease(disease)
qdel(src) // once organ is implanted delete the infective component
/datum/component/infective/proc/try_infect_eat(datum/source, mob/living/eater, mob/living/feeder)
SIGNAL_HANDLER
if(HAS_TRAIT(eater, TRAIT_STRONG_STOMACH))
return
// eater.add_mood_event("disgust", /datum/mood_event/disgust/dirty_food)
for(var/datum/disease/disease as anything in diseases)
if(is_weak && !prob(weak_infection_chance))
continue
// if(!disease.has_required_infectious_organ(eater, ORGAN_SLOT_STOMACH))
// continue
eater.ForceContractDisease(disease)
if(!is_weak)
try_infect(feeder, BP_L_ARM)
/datum/component/infective/proc/try_infect_drink(datum/source, mob/living/drinker, mob/living/feeder)
SIGNAL_HANDLER
if(HAS_TRAIT(drinker, TRAIT_STRONG_STOMACH))
return
if(!is_weak)
var/bodypart_type
if(feeder.l_hand == source)
bodypart_type = BP_R_ARM
else if(feeder.r_hand == source)
bodypart_type = BP_L_ARM
else
bodypart_type = BP_TORSO
try_infect(feeder, bodypart_type)
for(var/datum/disease/disease as anything in diseases)
if(is_weak && !prob(weak_infection_chance))
continue
// if(!disease.has_required_infectious_organ(drinker, ORGAN_SLOT_STOMACH))
// continue
drinker.ForceContractDisease(disease)
/datum/component/infective/proc/clean(datum/source, clean_types)
SIGNAL_HANDLER
. = NONE
if(clean_types & required_clean_types)
qdel(src)
return TRUE
return COMPONENT_CLEANED|COMPONENT_CLEANED_GAIN_XP
/datum/component/infective/proc/try_infect_buckle(datum/source, mob/M, force)
SIGNAL_HANDLER
if(isliving(M))
try_infect(M)
/datum/component/infective/proc/try_infect_collide(datum/source, atom/A)
SIGNAL_HANDLER
var/atom/movable/P = parent
if(P.throwing)
//this will be handled by try_infect_impact_zone()
return
if(isliving(A))
try_infect(A)
/datum/component/infective/proc/try_infect_impact_zone(datum/source, mob/living/target, hit_zone)
SIGNAL_HANDLER
try_infect(target, hit_zone)
/datum/component/infective/proc/try_infect_attack_zone(obj/item/source, mob/living/carbon/target, mob/living/user, hit_zone)
SIGNAL_HANDLER
try_infect(target, hit_zone)
/datum/component/infective/proc/try_infect_attack(obj/item/source, mob/living/target, mob/living/user)
SIGNAL_HANDLER
if(source.loc == user)
var/obj/item/organ/external/hand = user.get_active_hand()
if(!istype(hand)) //Some fuckery is going on here.
return
if(hand.body_part == HAND_LEFT)
try_infect(user, BP_L_HAND)
else
try_infect(user, BP_R_HAND)
/datum/component/infective/proc/try_infect_equipped(datum/source, mob/living/L, slot)
SIGNAL_HANDLER
var/old_bio_armor
if(isitem(parent))
//if you are putting an infective item on, it obviously will not protect you, so set its bio armor low enough that it will never block ContactContractDisease()
var/obj/item/equipped_item = parent
old_bio_armor = equipped_item.armor["bio"]
equipped_item.armor["bio"] = 0
try_infect(L, slot2body_zone(slot))
if(isitem(parent))
var/obj/item/equipped_item = parent
equipped_item.armor["bio"] = old_bio_armor
/datum/component/infective/proc/try_infect_crossed(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
SIGNAL_HANDLER
@@ -45,35 +205,18 @@
if(isliving(arrived))
try_infect(arrived, BP_L_FOOT)
/*
/datum/component/proc/try_infect_eat(datum/source, mob/living/eater, mob/living/feeder)
/datum/component/infective/proc/try_infect_streak(datum/source, list/directions, list/output_diseases)
SIGNAL_HANDLER
for(var/datum/disease/D in diseases)
eater.ForceContractDisease(D)
try_infect(feeder, BP_L_ARM)
*/
/datum/component/infective/proc/try_infect_collide(datum/source, atom/A)
SIGNAL_HANDLER
var/atom/movable/P = parent
if(P.throwing)
// This will be handled by COMSIG_MOVABLE_IMPACT_ZONE, whenever we get that.
// This blood is not infectable / does not have a diseases list
if(!islist(output_diseases))
return
if(isliving(A))
try_infect(A)
/datum/component/infective/proc/try_infect_attack(datum/source, mob/living/target, mob/living/user)
SIGNAL_HANDLER
if(!iscarbon(target))
try_infect(target)
try_infect(user, BP_L_ARM)
output_diseases |= diseases
/datum/component/infective/proc/try_infect(mob/living/L, target_zone)
for(var/datum/disease/D in diseases)
L.ContractDisease(D)
for(var/V in diseases)
L.ContractDisease(V, target_zone)
/datum/component/infective/proc/extrapolation(datum/source, mob/user, obj/item/extrapolator/extrapolator, dry_run = FALSE, list/result)
SIGNAL_HANDLER
@@ -168,6 +168,7 @@
I.reagents.remove_reagent(R.id, R.volume)
else
total_our_oil += R.volume
SEND_SIGNAL(I, COMSIG_ITEM_FRIED)
if (total_removed > 0 || total_oil != CI.max_oil)
@@ -49,3 +49,8 @@
icon_state = off_icon
if(grill_loop)
grill_loop.stop(src)
/obj/machinery/appliance/cooker/grill/finish_cooking(var/datum/cooking_item/CI)
..()
for(var/obj/item/I in CI.container)
SEND_SIGNAL(I, COMSIG_ITEM_BARBEQUE_GRILLED)
@@ -847,13 +847,14 @@
/datum/trait/positive/toxin_gut
name ="Robust Gut"
desc = "You are immune to most ingested toxins. Does not protect from possible harm caused by other drugs, meds, allergens etc."
desc = "You are immune to most ingested toxins and raw food. Does not protect from possible harm caused by other drugs, meds, allergens etc."
cost = 1
custom_only = FALSE
/datum/trait/positive/toxin_gut/apply(var/datum/species/S,var/mob/living/carbon/human/H)
..()
ADD_TRAIT(H, INGESTED_TOXIN_IMMUNE, ROUNDSTART_TRAIT)
ADD_TRAIT(H, TRAIT_STRONG_STOMACH, ROUNDSTART_TRAIT)
/datum/trait/positive/nobreathe
name = "Breathless"
+3 -2
View File
@@ -771,6 +771,7 @@
#include "code\datums\components\disabilities\pollen.dm"
#include "code\datums\components\disabilities\rotting.dm"
#include "code\datums\components\disabilities\tourettes.dm"
#include "code\datums\components\food\germ_sensitive.dm"
#include "code\datums\components\machinery\disposal_connection.dm"
#include "code\datums\components\materials\machine_shim.dm"
#include "code\datums\components\materials\material_container.dm"
@@ -2893,7 +2894,7 @@
#include "code\modules\flufftext\hallucination_events.dm"
#include "code\modules\flufftext\look_up.dm"
#include "code\modules\flufftext\TextFilters.dm"
#include "code\modules\food\compile_recipies.dm"
#include "code\modules\food\compile_recipes.dm"
#include "code\modules\food\food.dm"
#include "code\modules\food\recipe.dm"
#include "code\modules\food\recipe_dump.dm"
@@ -3559,7 +3560,7 @@
#include "code\modules\mob\living\carbon\human\species\station\traits\trait.dm"
#include "code\modules\mob\living\carbon\human\species\station\traits\traits_tutorial.dm"
#include "code\modules\mob\living\carbon\human\species\station\traits\weaver_objs.dm"
#include "code\modules\mob\living\carbon\human\species\station\traits\weaver_recipies.dm"
#include "code\modules\mob\living\carbon\human\species\station\traits\weaver_recipes.dm"
#include "code\modules\mob\living\carbon\human\species\virtual_reality\avatar.dm"
#include "code\modules\mob\living\carbon\human\species\virtual_reality\opaque_form.dm"
#include "code\modules\mob\living\carbon\human\species\xenomorphs\alien_powers.dm"