diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index e69faa19567..632740e701a 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -750,6 +750,10 @@
///(obj/item/insertion_candidate, mob/user, silent) - returns bool
#define COMSIG_TRY_STORAGE_CAN_INSERT "storage_can_equip"
+// /datum/component/swabbing signals
+#define COMSIG_SWAB_FOR_SAMPLES "swab_for_samples" ///Called when you try to swab something using the swabable component, includes a mutable list of what has been swabbed so far so it can be modified.
+ #define COMPONENT_SWAB_FOUND (1<<0)
+
// /datum/component/two_handed signals
///from base of datum/component/two_handed/proc/wield(mob/living/carbon/user): (/mob/user)
diff --git a/code/__DEFINES/research.dm b/code/__DEFINES/research.dm
index 86c4bf155c4..bf57a954a85 100644
--- a/code/__DEFINES/research.dm
+++ b/code/__DEFINES/research.dm
@@ -88,3 +88,44 @@
///Amount of points gained per second by a single R&D server, see: [/controllers/subsystem/research.dm][research]
#define TECHWEB_SINGLE_SERVER_INCOME 52.3
+
+///Swab cell line types
+#define CELL_LINE_TABLE_SLUDGE "cell_line_sludge_table"
+#define CELL_LINE_TABLE_MOLD "cell_line_mold_table"
+#define CELL_LINE_TABLE_MOIST "cell_line_moist_table"
+#define CELL_LINE_TABLE_BLOB "cell_line_blob_table"
+
+///Biopsy cell line types
+#define CELL_LINE_TABLE_BEAR "cell_line_bear_table"
+#define CELL_LINE_TABLE_BLOBBERNAUT "cell_line_blobbernaut_table"
+#define CELL_LINE_TABLE_BLOBSPORE "cell_line_blobspore_table"
+#define CELL_LINE_TABLE_CARP "cell_line_carp_table"
+#define CELL_LINE_TABLE_CAT "cell_line_cat_table"
+#define CELL_LINE_TABLE_CHICKEN "cell_line_chicken_table"
+#define CELL_LINE_TABLE_COCKROACH "cell_line_cockroach_table"
+#define CELL_LINE_TABLE_CORGI "cell_line_corgi_table"
+#define CELL_LINE_TABLE_COW "cell_line_cow_table"
+#define CELL_LINE_TABLE_GELATINOUS "cell_line_gelatinous_table"
+#define CELL_LINE_TABLE_GRAPE "cell_line_grape_table"
+#define CELL_LINE_TABLE_MEGACARP "cell_line_megacarp_table"
+#define CELL_LINE_TABLE_MOUSE "cell_line_mouse_table"
+#define CELL_LINE_TABLE_PINE "cell_line_pine_table"
+#define CELL_LINE_TABLE_PUG "cell_line_pug_table"
+#define CELL_LINE_TABLE_SLIME "cell_line_slime_table"
+#define CELL_LINE_TABLE_SNAKE "cell_line_snake_table"
+#define CELL_LINE_TABLE_VATBEAST "cell_line_vatbeast_table"
+
+///All cell virus types
+#define CELL_VIRUS_TABLE_GENERIC "cell_virus_generic_table"
+#define CELL_VIRUS_TABLE_GENERIC_MOB "cell_virus_generic_mob_table"
+
+///General defines for vatgrowing
+#define VATGROWING_DANGER_MINIMUM 30 //Past how much growth can the other cell_lines affect a finished cell line negatively
+
+
+///All color defines for samples
+#define SAMPLE_YELLOW "#c0b823"
+#define SAMPLE_PURPLE "#342941"
+#define SAMPLE_GREEN "#98b944"
+#define SAMPLE_BROWN "#91542d"
+#define SAMPLE_GRAY "#5e5856"
diff --git a/code/_globalvars/lists/construction.dm b/code/_globalvars/lists/construction.dm
index 3accb854ca1..c1dfae08378 100644
--- a/code/_globalvars/lists/construction.dm
+++ b/code/_globalvars/lists/construction.dm
@@ -1,5 +1,5 @@
///A cache with all the objects craftable with a plumbing constructor and their delay (doubles as their cost, 1sec/10 matter)
-GLOBAL_LIST_INIT(plumbing_constructables, list(
+GLOBAL_LIST_INIT(plumbing_constructables_default, list(
/obj/machinery/plumbing/input = 5,
/obj/machinery/plumbing/output = 5,
/obj/machinery/plumbing/tank = 20,
@@ -16,3 +16,16 @@ GLOBAL_LIST_INIT(plumbing_constructables, list(
/obj/machinery/plumbing/synthesizer = 15,
/obj/machinery/plumbing/sender = 20
))
+
+GLOBAL_LIST_INIT(plumbing_constructables_research, list(
+ /obj/machinery/plumbing/input = 5,
+ /obj/machinery/plumbing/output = 5,
+ /obj/machinery/plumbing/tank = 20,
+ /obj/machinery/plumbing/acclimator = 10,
+ /obj/machinery/plumbing/filter = 5,
+ /obj/machinery/plumbing/grinder_chemical = 30,
+ /obj/machinery/plumbing/reaction_chamber = 15,
+ /obj/machinery/plumbing/splitter = 5,
+ /obj/machinery/plumbing/disposer = 10,
+ /obj/machinery/plumbing/growing_vat = 20
+))
diff --git a/code/_globalvars/lists/xenobiology.dm b/code/_globalvars/lists/xenobiology.dm
new file mode 100644
index 00000000000..11cfa517396
--- /dev/null
+++ b/code/_globalvars/lists/xenobiology.dm
@@ -0,0 +1,36 @@
+///These global lists exist to allow our element to have weight tables without having to be seperate instances.
+
+///Assoc list of cell line define | assoc list of datum | cell_line
+GLOBAL_LIST_INIT_TYPED(cell_line_tables, /list, list(
+ CELL_LINE_TABLE_SLUDGE = list(/datum/micro_organism/cell_line/sholean_grapes = 2, /datum/micro_organism/cell_line/blobbernaut = 1, /datum/micro_organism/cell_line/slime = 2, /datum/micro_organism/cell_line/cow = 1, /datum/micro_organism/cell_line/mouse = 1, /datum/micro_organism/cell_line/chicken = 1, /datum/micro_organism/cell_line/cockroach = 2),
+ CELL_LINE_TABLE_MOIST = list(/datum/micro_organism/cell_line/gelatinous_cube = 2, /datum/micro_organism/cell_line/megacarp = 1, /datum/micro_organism/cell_line/slime = 2, /datum/micro_organism/cell_line/snake = 1, /datum/micro_organism/cell_line/carp = 1, /datum/micro_organism/cell_line/cockroach = 1),
+ CELL_LINE_TABLE_BLOB = list(/datum/micro_organism/cell_line/blobbernaut = 1, /datum/micro_organism/cell_line/blob_spore = 1),
+ CELL_LINE_TABLE_MOLD = list(/datum/micro_organism/cell_line/vat_beast = 2, /datum/micro_organism/cell_line/bear = 1, /datum/micro_organism/cell_line/slime = 1, /datum/micro_organism/cell_line/blob_spore = 1, /datum/micro_organism/cell_line/mouse = 2, /datum/micro_organism/cell_line/corgi = 1, /datum/micro_organism/cell_line/cockroach = 1, /datum/micro_organism/cell_line/cat = 1),
+ CELL_LINE_TABLE_BEAR = list(/datum/micro_organism/cell_line/bear = 1),
+ CELL_LINE_TABLE_BLOBBERNAUT = list(/datum/micro_organism/cell_line/blobbernaut = 1),
+ CELL_LINE_TABLE_BLOBSPORE = list(/datum/micro_organism/cell_line/blob_spore = 1),
+ CELL_LINE_TABLE_CARP = list(/datum/micro_organism/cell_line/carp = 1),
+ CELL_LINE_TABLE_CAT = list(/datum/micro_organism/cell_line/cat = 1),
+ CELL_LINE_TABLE_CHICKEN = list(/datum/micro_organism/cell_line/chicken = 1),
+ CELL_LINE_TABLE_COCKROACH = list(/datum/micro_organism/cell_line/cockroach = 1),
+ CELL_LINE_TABLE_CORGI = list(/datum/micro_organism/cell_line/corgi = 1),
+ CELL_LINE_TABLE_COW = list(/datum/micro_organism/cell_line/cow = 1),
+ CELL_LINE_TABLE_GELATINOUS = list(/datum/micro_organism/cell_line/gelatinous_cube = 1),
+ CELL_LINE_TABLE_GRAPE = list(/datum/micro_organism/cell_line/sholean_grapes = 1),
+ CELL_LINE_TABLE_MEGACARP = list(/datum/micro_organism/cell_line/megacarp = 1),
+ CELL_LINE_TABLE_MOUSE = list(/datum/micro_organism/cell_line/mouse = 1),
+ CELL_LINE_TABLE_PINE = list(/datum/micro_organism/cell_line/pine = 1),
+ CELL_LINE_TABLE_PUG = list(/datum/micro_organism/cell_line/pug = 1),
+ CELL_LINE_TABLE_SLIME = list(/datum/micro_organism/cell_line/slime = 1),
+ CELL_LINE_TABLE_SNAKE = list(/datum/micro_organism/cell_line/snake = 1),
+ CELL_LINE_TABLE_VATBEAST = list(/datum/micro_organism/cell_line/vat_beast = 1)
+ ))
+
+///Assoc list of cell virus define | assoc list of datum | cell_virus
+GLOBAL_LIST_INIT(cell_virus_tables, list(
+ CELL_VIRUS_TABLE_GENERIC = list(/datum/micro_organism/virus = 1),
+ CELL_VIRUS_TABLE_GENERIC_MOB = list(/datum/micro_organism/virus = 1)
+ ))
+
+///List of all possible sample colors
+GLOBAL_LIST_INIT(xeno_sample_colors, list(SAMPLE_YELLOW, SAMPLE_PURPLE, SAMPLE_GREEN, SAMPLE_BROWN, SAMPLE_GRAY))
diff --git a/code/_onclick/hud/ooze.dm b/code/_onclick/hud/ooze.dm
new file mode 100644
index 00000000000..13a70c034f2
--- /dev/null
+++ b/code/_onclick/hud/ooze.dm
@@ -0,0 +1,19 @@
+///Hud type with targetting dol and a nutrition bar
+/datum/hud/ooze/New(mob/living/owner)
+ ..()
+
+ zone_select = new /obj/screen/zone_sel()
+ zone_select.icon = ui_style
+ zone_select.hud = src
+ zone_select.update_icon()
+ static_inventory += zone_select
+
+ alien_plasma_display = new /obj/screen/ooze_nutrition_display //Just going to use the alien plasma display because making new vars for each object is braindead.
+ alien_plasma_display.hud = src
+ infodisplay += alien_plasma_display
+
+/obj/screen/ooze_nutrition_display
+ icon = 'icons/mob/screen_alien.dmi'
+ icon_state = "power_display"
+ name = "nutrition"
+ screen_loc = ui_alienplasmadisplay
diff --git a/code/_onclick/other_mobs.dm b/code/_onclick/other_mobs.dm
index 5a8777ec30c..a813ee519bd 100644
--- a/code/_onclick/other_mobs.dm
+++ b/code/_onclick/other_mobs.dm
@@ -248,7 +248,7 @@
if(dextrous && !ismob(A))
..()
else
- AttackingTarget()
+ AttackingTarget(A)
diff --git a/code/controllers/subsystem/processing/processing.dm b/code/controllers/subsystem/processing/processing.dm
index c5d6dfa1268..f81a7d3df61 100644
--- a/code/controllers/subsystem/processing/processing.dm
+++ b/code/controllers/subsystem/processing/processing.dm
@@ -30,6 +30,8 @@ SUBSYSTEM_DEF(processing)
if (MC_TICK_CHECK)
return
+
+///This proc is called on a datum if it is being processed in a subsystem. If you override this do not call parent, as it will return PROCESS_KILL. This is done to prevent objects that dont override process() from staying in the processing list
/datum/proc/process()
set waitfor = 0
return PROCESS_KILL
diff --git a/code/datums/components/swabbing.dm b/code/datums/components/swabbing.dm
new file mode 100644
index 00000000000..657b4052556
--- /dev/null
+++ b/code/datums/components/swabbing.dm
@@ -0,0 +1,125 @@
+/*!
+
+This component is used in vat growing to swab for microbiological samples which can then be mixed with reagents in a petridish to create a culture plate.
+
+*/
+/datum/component/swabbing
+ ///The current datums on the swab
+ var/list/swabbed_items
+ ///Can we swab objs?
+ var/CanSwabObj
+ ///Can we swab turfs?
+ var/CanSwabTurf
+ ///Can we swab mobs?
+ var/CanSwabMob
+ ///Callback for update_icon()
+ var/datum/callback/UpdateIcons
+ ///Callback for update_overlays()
+ var/datum/callback/UpdateOverlays
+
+/datum/component/swabbing/Initialize(CanSwabObj = TRUE, CanSwabTurf = TRUE, CanSwabMob = FALSE, datum/callback/UpdateIcons, datum/callback/UpdateOverlays, swab_time = 10, max_items = 3)
+ if(!isitem(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ RegisterSignal(parent, COMSIG_ITEM_PRE_ATTACK, .proc/TryToSwab)
+ RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/examine)
+ RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, .proc/handle_overlays)
+ RegisterSignal(parent, COMSIG_ATOM_UPDATE_ICON, .proc/handle_icon)
+
+ src.CanSwabObj = CanSwabObj
+ src.CanSwabTurf = CanSwabTurf
+ src.CanSwabMob = CanSwabMob
+ src.UpdateIcons = UpdateIcons
+ src.UpdateOverlays = UpdateOverlays
+
+/datum/component/swarming/Destroy()
+ . = ..()
+ for(var/i in swabbed_items)
+ var/datum/D = i
+ qdel(D)
+
+
+///Changes examine based on your sample
+/datum/component/swabbing/proc/examine(datum/source, mob/user, list/examine_list)
+ if(LAZYLEN(swabbed_items))
+ examine_list += "There is a microbiological sample on [parent]!"
+ examine_list += "You can see the following micro-organisms:\n"
+ for(var/i in swabbed_items)
+ var/datum/biological_sample/samp = i
+ examine_list += samp.GetAllDetails(user.research_scanner) //Get just the names nicely parsed.
+
+///Ran when you attack an object, tries to get a swab of the object. if a swabbable surface is found it will run behavior and hopefully
+/datum/component/swabbing/proc/TryToSwab(datum/source, atom/target, mob/user, params)
+ set waitfor = FALSE //This prevents do_after() from making this proc not return it's value.
+
+ if(istype(target, /obj/structure/table))//help how do i do this less shitty
+ return NONE //idk bro pls send help
+
+ if(istype(target, /obj/item/petri_dish))
+ if(!LAZYLEN(swabbed_items))
+ return NONE
+ var/obj/item/petri_dish/dish = target
+ if(dish.sample)
+ return
+
+ var/datum/biological_sample/deposited_sample
+
+ for(var/i in swabbed_items) //Typed in case there is a non sample on the swabbing tool because someone was fucking with swabbable element
+ if(!istype(i, /datum/biological_sample/))
+ stack_trace("Non biological sample being swabbed, no bueno.")
+ continue
+ var/datum/biological_sample/sample = i
+ //Collapse the samples into one sample; one gooey mess essentialy.
+ if(!deposited_sample)
+ deposited_sample = sample
+ else
+ deposited_sample.Merge(sample)
+
+ dish.deposit_sample(user, deposited_sample)
+ LAZYCLEARLIST(swabbed_items)
+
+ var/obj/item/I = parent
+ I.update_icon()
+
+ return COMPONENT_NO_ATTACK
+ if(!can_swab(target))
+ return NONE //Just do the normal attack.
+
+
+ . = COMPONENT_NO_ATTACK //Point of no return. No more attacking after this.
+
+ if(LAZYLEN(swabbed_items))
+ to_chat(user, "You cannot collect another sample on [parent]!")
+ return
+
+ to_chat(user, "You start swabbing [target] for samples!")
+ if(!do_after(user, 30, TRUE, target)) // Start swabbing boi
+ return
+
+ if(!SEND_SIGNAL(target, COMSIG_SWAB_FOR_SAMPLES, swabbed_items)) //If we found something to swab now we let the swabbed thing handle what it would do, we just sit back and relax now.
+ to_chat(user, "You do not manage to find a anything on [target]!")
+ return
+
+ to_chat(user, "You manage to collect a microbiological sample from [target]!")
+
+ var/obj/item/I = parent
+ I.update_icon()
+
+///Checks if the swabbing component can swab the specific object or nots
+/datum/component/swabbing/proc/can_swab(atom/target)
+ if(isobj(target))
+ return CanSwabObj
+ if(isturf(target))
+ return CanSwabTurf
+ if(ismob(target))
+ return CanSwabMob
+
+///Handle any special overlay cases on the item itself
+/datum/component/swabbing/proc/handle_overlays(datum/source, list/overlays)
+ if(UpdateOverlays)
+ UpdateOverlays.Invoke(overlays, swabbed_items)
+
+///Handle any special icon cases on the item itself
+/datum/component/swabbing/proc/handle_icon(datum/source)
+ if(UpdateIcons)
+ UpdateIcons.Invoke(swabbed_items)
diff --git a/code/datums/elements/swabbable.dm b/code/datums/elements/swabbable.dm
new file mode 100644
index 00000000000..8a6f4225ccc
--- /dev/null
+++ b/code/datums/elements/swabbable.dm
@@ -0,0 +1,49 @@
+/*!
+
+This element is used in vat growing to allow for the object to be
+
+*/
+/datum/element/swabable
+ element_flags = ELEMENT_BESPOKE
+ id_arg_index = 2
+ ///The define of the cell_line list to use
+ var/cell_line_define
+ ///The define of the cell_virus list to use
+ var/virus_define
+ ///Amount of cell lines on a single sample
+ var/cell_line_amount
+ ///Amount of viruses on a single sample
+ var/virus_chance
+
+///Listens for the swab signal and then generate a sample based on pre-determined lists that are saved as GLOBs. this allows us to have very few swabbable element instances.
+/datum/element/swabable/Attach(datum/target, cell_line_define, virus_define, cell_line_amount = 3, virus_chance = 10)
+ . = ..()
+ if(!isatom(target) || isarea(target))
+ return ELEMENT_INCOMPATIBLE
+
+ RegisterSignal(target, COMSIG_SWAB_FOR_SAMPLES, .proc/GetSwabbed)
+
+ src.cell_line_define = cell_line_define
+ src.virus_define = virus_define
+ src.cell_line_amount = cell_line_amount
+ src.virus_chance = virus_chance
+
+///Stops listening to the swab signal; you can no longer be swabbed.
+/datum/element/swabable/Detach(datum/source, force)
+ . = ..()
+ if(!isatom(source) || isarea(source))
+ return ELEMENT_INCOMPATIBLE
+ UnregisterSignal(source, COMSIG_SWAB_FOR_SAMPLES)
+
+///Ran when the parent is swabbed by an object that can swab that type of obj. The list is sent by ref, which means the thing which sent the signal will still have the updated list.
+/datum/element/swabable/proc/GetSwabbed(datum/source, list/mutable_results)
+ . = COMPONENT_SWAB_FOUND //Return this so the swabbing component knows hes a good boy and found something that needs swabbing.
+
+ LAZYADD(mutable_results, GenerateSample())
+ Detach(source)
+
+///Generates a /datum/biological_sample
+/datum/element/swabable/proc/GenerateSample()
+ var/datum/biological_sample/generated_sample = new
+ generated_sample.GenerateSample(cell_line_define, virus_define, cell_line_amount, virus_chance)
+ return generated_sample
diff --git a/code/game/objects/effects/decals/cleanable/misc.dm b/code/game/objects/effects/decals/cleanable/misc.dm
index f504e84acf2..55e2fef54cb 100644
--- a/code/game/objects/effects/decals/cleanable/misc.dm
+++ b/code/game/objects/effects/decals/cleanable/misc.dm
@@ -159,6 +159,8 @@
/obj/effect/decal/cleanable/vomit/old/Initialize(mapload, list/datum/disease/diseases)
. = ..()
icon_state += "-old"
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_SLUDGE, CELL_VIRUS_TABLE_GENERIC, rand(2,4), 10)
+
/obj/effect/decal/cleanable/chem_pile
name = "chemical pile"
@@ -238,3 +240,15 @@
desc = "Torn pieces of cardboard and paper, left over from a package."
icon = 'icons/obj/objects.dmi'
icon_state = "paper_shreds"
+
+/obj/effect/decal/cleanable/garbage
+ name = "decomposing garbage"
+ desc = "A split open garbage bag, its stinking content seems to be partially liquified. Yuck!"
+ icon = 'icons/obj/objects.dmi'
+ icon_state = "garbage"
+ layer = OBJ_LAYER //To display the decal over wires.
+ beauty = -150
+
+/obj/effect/decal/cleanable/garbage/Initialize()
+ . = ..()
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_SLUDGE, CELL_VIRUS_TABLE_GENERIC, rand(2,4), 15)
diff --git a/code/game/objects/effects/spawners/bundle.dm b/code/game/objects/effects/spawners/bundle.dm
index 3722218113b..70213a24435 100644
--- a/code/game/objects/effects/spawners/bundle.dm
+++ b/code/game/objects/effects/spawners/bundle.dm
@@ -195,3 +195,14 @@
/obj/item/clothing/head/fedora/beige,
/obj/item/clothing/under/suit/beige,
/obj/item/clothing/shoes/laceup)
+
+/obj/effect/spawner/bundle/hobo_squat
+ name = "hobo squat spawner"
+ items = list(/obj/structure/bed/maint,
+ /obj/effect/spawner/scatter/grime,
+ /obj/effect/spawner/lootdrop/maint_drugs)
+
+/obj/effect/spawner/bundle/moisture_trap
+ name = "moisture trap spawner"
+ items = list(/obj/effect/spawner/scatter/moisture,
+ /obj/structure/moisture_trap)
diff --git a/code/game/objects/effects/spawners/lootdrop.dm b/code/game/objects/effects/spawners/lootdrop.dm
index dbdfffbc798..5e24d53dc5e 100644
--- a/code/game/objects/effects/spawners/lootdrop.dm
+++ b/code/game/objects/effects/spawners/lootdrop.dm
@@ -122,6 +122,70 @@
/obj/item/reagent_containers/food/drinks/bottle/vodka/badminka,
)
+/obj/effect/spawner/lootdrop/garbage_spawner
+ name = "garbage_spawner"
+ loot = list(/obj/effect/spawner/lootdrop/food_packaging = 56,
+ /obj/item/trash/can = 8,
+ /obj/item/shard = 8,
+ /obj/effect/spawner/lootdrop/botanical_waste = 8,
+ /obj/effect/spawner/lootdrop/cigbutt = 8,
+ /obj/item/reagent_containers/syringe = 5,
+ /obj/item/reagent_containers/food/snacks/deadmouse = 2,
+ /obj/item/light/tube/broken = 3,
+ /obj/item/light/tube/broken = 1,
+ /obj/item/trash/candle = 1)
+
+/obj/effect/spawner/lootdrop/cigbutt
+ name = "cigarette butt spawner"
+ loot = list(/obj/item/cigbutt = 65,
+ /obj/item/cigbutt/roach = 20,
+ /obj/item/cigbutt/cigarbutt = 15)
+
+/obj/effect/spawner/lootdrop/food_packaging
+ name = "food packaging spawner"
+ loot = list(/obj/item/trash/raisins = 20,
+ /obj/item/trash/cheesie = 10,
+ /obj/item/trash/candy = 10,
+ /obj/item/trash/chips = 10,
+ /obj/item/trash/sosjerky = 10,
+ /obj/item/trash/pistachios = 10,
+ /obj/item/trash/boritos = 8,
+ /obj/item/trash/can/food/beans = 6,
+ /obj/item/trash/popcorn = 5,
+ /obj/item/trash/energybar = 5,
+ /obj/item/trash/can/food/peaches/maint = 4,
+ /obj/item/trash/semki = 2)
+
+/obj/effect/spawner/lootdrop/botanical_waste
+ name = "botanical waste spawner"
+ loot = list(/obj/item/grown/bananapeel = 60,
+ /obj/item/grown/corncob = 30,
+ /obj/item/reagent_containers/food/snacks/grown/bungopit = 10)
+
+/obj/effect/spawner/lootdrop/refreshing_beverage
+ name = "good soda spawner"
+ loot = list(/obj/item/reagent_containers/food/drinks/drinkingglass/filled/nuka_cola = 15,
+ /obj/item/reagent_containers/food/drinks/soda_cans/grey_bull = 15,
+ /obj/item/reagent_containers/food/drinks/soda_cans/monkey_energy = 10,
+ /obj/item/reagent_containers/food/drinks/soda_cans/thirteenloko = 10,
+ /obj/item/reagent_containers/food/drinks/beer/light = 10,
+ /obj/item/reagent_containers/food/drinks/soda_cans/shamblers = 5,
+ /obj/item/reagent_containers/food/drinks/soda_cans/pwr_game = 5,
+ /obj/item/reagent_containers/food/drinks/soda_cans/dr_gibb = 5,
+ /obj/item/reagent_containers/food/drinks/soda_cans/space_mountain_wind = 5,
+ /obj/item/reagent_containers/food/drinks/soda_cans/starkist = 5,
+ /obj/item/reagent_containers/food/drinks/soda_cans/space_up = 5,
+ /obj/item/reagent_containers/food/drinks/soda_cans/sol_dry = 5,
+ /obj/item/reagent_containers/food/drinks/soda_cans/cola = 5)
+
+/obj/effect/spawner/lootdrop/maint_drugs
+ name = "maint drugs spawner"
+ loot = list(/obj/item/reagent_containers/food/drinks/bottle/hooch = 50,
+ /obj/item/clothing/mask/cigarette/rollie/cannabis = 15,
+ /obj/item/clothing/mask/cigarette/rollie/mindbreaker = 5,
+ /obj/item/reagent_containers/syringe = 15,
+ /obj/item/cigbutt/roach = 15)
+
/obj/effect/spawner/lootdrop/grille_or_trash
name = "maint grille or trash spawner"
loot = list(/obj/structure/grille = 5,
diff --git a/code/game/objects/effects/spawners/lootsite.dm b/code/game/objects/effects/spawners/lootsite.dm
new file mode 100644
index 00000000000..f0cbed41b65
--- /dev/null
+++ b/code/game/objects/effects/spawners/lootsite.dm
@@ -0,0 +1,21 @@
+///this spawner usually spawns a boring crate, but has a chance to replace the crate with "loot crate" with a different loot table or a decorative site.
+/obj/effect/loot_site_spawner
+ icon = 'icons/effects/landmarks_static.dmi'
+ icon_state = "loot_site"
+ ///This is the loot table for the spawner. Try to make sure the weights add up to 1000, so it is easy to understand.
+ var/list/loot_table = list(/obj/structure/closet/crate/maint = 810,
+ /obj/structure/closet/crate/trashcart/filled = 70,
+ /obj/effect/spawner/bundle/moisture_trap = 50,
+ /obj/effect/spawner/bundle/hobo_squat = 35,
+ /obj/structure/closet/mini_fridge = 35)
+
+
+/obj/effect/loot_site_spawner/Initialize()
+ ..()
+ if(!length(loot_table))
+ return INITIALIZE_HINT_QDEL
+
+ var/spawned_object = pickweight(loot_table)
+ new spawned_object(get_turf(src))
+
+ return INITIALIZE_HINT_QDEL
diff --git a/code/game/objects/effects/spawners/scatter.dm b/code/game/objects/effects/spawners/scatter.dm
new file mode 100644
index 00000000000..74e7533ab8a
--- /dev/null
+++ b/code/game/objects/effects/spawners/scatter.dm
@@ -0,0 +1,55 @@
+///This spawner scatters the spawned stuff around where it is placed.
+/obj/effect/spawner/scatter
+ ///determines how many things to scatter
+ var/max_spawns = 3
+ ///determines how big of a range we should scatter things in.
+ var/radius = 2
+ ///This weighted list acts as the loot table for the spawner
+ var/list/loot_table
+
+/obj/effect/spawner/scatter/Initialize()
+ ..()
+ if(!length(loot_table))
+ return INITIALIZE_HINT_QDEL
+
+ var/list/candidate_locations = list()
+
+ for(var/turf/T in oview(radius, get_turf(src)))
+ if(!T.density)
+
+ candidate_locations += T
+
+ if(!candidate_locations.len)
+ return INITIALIZE_HINT_QDEL
+
+ var/loot_spawned = 0
+ while((max_spawns-loot_spawned) && candidate_locations.len)
+ var/spawned_thing = pickweight(loot_table)
+ while(islist(spawned_thing))
+ spawned_thing = pickweight(spawned_thing)
+ new spawned_thing(pick_n_take(candidate_locations))
+ loot_spawned++
+
+ return INITIALIZE_HINT_QDEL
+
+///This spawner will scatter garbage around a dirty site.
+/obj/effect/spawner/scatter/grime
+ name = "trash and grime scatterer"
+ max_spawns = 5
+ loot_table = list(/obj/effect/spawner/lootdrop/garbage_spawner = 30,
+ /mob/living/simple_animal/hostile/cockroach = 25,
+ /obj/effect/decal/cleanable/garbage = 20,
+ /obj/effect/decal/cleanable/vomit/old = 15,
+ /obj/effect/spawner/lootdrop/cigbutt = 10)
+
+///This spawner will scatter water related items around a moist site.
+/obj/effect/spawner/scatter/moisture
+ max_spawns = 2
+ radius = 1
+ loot_table = list(/obj/item/clothing/head/cone = 35,
+ /obj/item/clothing/suit/caution = 15,
+ /mob/living/simple_animal/hostile/retaliate/frog = 10,
+ /obj/item/reagent_containers/glass/rag = 10,
+ /obj/item/reagent_containers/glass/bucket = 10,
+ /obj/effect/decal/cleanable/blood/old = 10,
+ /obj/structure/mopbucket = 10)
diff --git a/code/game/objects/items/RCD.dm b/code/game/objects/items/RCD.dm
index 2903c8f8410..c969181d50a 100644
--- a/code/game/objects/items/RCD.dm
+++ b/code/game/objects/items/RCD.dm
@@ -925,11 +925,13 @@ RLD
var/list/name_to_type = list()
///All info for construction
var/list/machinery_data = list("cost" = list())
+ ///This list that holds all the plumbing design types the plumberer can construct. Its purpose is to make it easy to make new plumberer subtypes with a different selection of machines.
+ var/list/plumbing_design_types = GLOB.plumbing_constructables_default
/obj/item/construction/plumbing/attack_self(mob/user)
..()
if(!choices.len)
- for(var/A in GLOB.plumbing_constructables)
+ for(var/A in plumbing_design_types)
var/obj/machinery/plumbing/M = A
choices += list(initial(M.name) = image(icon = initial(M.icon), icon_state = initial(M.icon_state)))
@@ -982,6 +984,13 @@ RLD
else
create_machine(A, user)
+/obj/item/construction/plumbing/research
+ name = "research plumbing constructor"
+ desc = "A type of plumbing constructor designed to rapidly deploy the machines needed to conduct cytological research."
+ icon_state = "plumberer_sci"
+ has_ammobar = TRUE
+ plumbing_design_types = GLOB.plumbing_constructables_research
+
/obj/item/rcd_upgrade
name = "RCD advanced design disk"
desc = "It seems to be empty."
diff --git a/code/game/objects/items/storage/bags.dm b/code/game/objects/items/storage/bags.dm
index a54d7870614..299032b1deb 100644
--- a/code/game/objects/items/storage/bags.dm
+++ b/code/game/objects/items/storage/bags.dm
@@ -78,6 +78,14 @@
to_chat(user, "You are unable to fit your [name] into the [J.name].")
return
+/obj/item/storage/bag/trash/filled
+
+/obj/item/storage/bag/trash/filled/PopulateContents()
+ . = ..()
+ for(var/i in 1 to rand(1, 7))
+ new /obj/effect/spawner/lootdrop/garbage_spawner(src)
+ update_icon_state()
+
/obj/item/storage/bag/trash/bluespace
name = "trash bag of holding"
desc = "The latest and greatest in custodial convenience, a trashbag that is capable of holding vast quantities of garbage."
diff --git a/code/game/objects/items/storage/boxes.dm b/code/game/objects/items/storage/boxes.dm
index 49e7e2c6a59..43d28beb709 100644
--- a/code/game/objects/items/storage/boxes.dm
+++ b/code/game/objects/items/storage/boxes.dm
@@ -1386,3 +1386,21 @@
/obj/item/storage/box/skillchips/engineering/PopulateContents()
new/obj/item/skillchip/job/engineer(src)
new/obj/item/skillchip/job/engineer(src)
+
+/obj/item/storage/box/swab
+ name = "box of microbiological swabs"
+ desc = "Contains a number of sterile swabs for collecting microbiological samples."
+ illustration = "swab"
+
+/obj/item/storage/box/swab/PopulateContents()
+ for(var/i in 1 to 7)
+ new /obj/item/swab(src)
+
+/obj/item/storage/box/petridish
+ name = "box of petridishes"
+ desc = "This box purports to contain a number of high rim petridishes."
+ illustration = "petridish"
+
+/obj/item/storage/box/petridish/PopulateContents()
+ for(var/i in 1 to 7)
+ new /obj/item/petri_dish(src)
diff --git a/code/game/objects/structures/beds_chairs/bed.dm b/code/game/objects/structures/beds_chairs/bed.dm
index 9c6c23ebbcb..ae0d17c7456 100644
--- a/code/game/objects/structures/beds_chairs/bed.dm
+++ b/code/game/objects/structures/beds_chairs/bed.dm
@@ -179,7 +179,7 @@
desc = "Seems kind of... fishy."
name = "Cayenne's bed"
anchored = TRUE
-
+
/obj/structure/bed/dogbed/lia
desc = "Seems kind of... fishy."
name = "Lia's bed"
@@ -212,3 +212,13 @@
name = "resting contraption"
desc = "This looks similar to contraptions from Earth. Could aliens be stealing our technology?"
icon_state = "abed"
+
+
+/obj/structure/bed/maint
+ name = "dirty mattress"
+ desc = "An old grubby mattress. You try to not think about what could be the cause of those stains."
+ icon_state = "dirty_mattress"
+
+/obj/structure/bed/maint/Initialize()
+ . = ..()
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_MOLD, CELL_VIRUS_TABLE_GENERIC, rand(2,4), 25)
diff --git a/code/game/objects/structures/crates_lockers/closets/gimmick.dm b/code/game/objects/structures/crates_lockers/closets/gimmick.dm
index 6c389bc160c..2489563b236 100644
--- a/code/game/objects/structures/crates_lockers/closets/gimmick.dm
+++ b/code/game/objects/structures/crates_lockers/closets/gimmick.dm
@@ -109,3 +109,22 @@
new /obj/item/crowbar(src)
new /obj/item/stock_parts/cell(src)
new /obj/item/multitool(src)
+
+/obj/structure/closet/mini_fridge
+ name = "grimy mini-fridge"
+ desc = "A small contraption designed to imbue a few drinks with a pleasant chill. This antiquated unit however seems to serve no purpose other than keeping the roaches company."
+ icon_state = "mini_fridge"
+ icon_welded = "welded_small"
+ max_mob_size = MOB_SIZE_SMALL
+ storage_capacity = 7
+
+/obj/structure/closet/mini_fridge/PopulateContents()
+ . = ..()
+ new /obj/effect/spawner/lootdrop/refreshing_beverage(src)
+ new /obj/effect/spawner/lootdrop/refreshing_beverage(src)
+ if(prob(50))
+ new /obj/effect/spawner/lootdrop/refreshing_beverage(src)
+ if(prob(40))
+ new /obj/item/reagent_containers/food/snacks/pizzaslice/moldy(src)
+ else if(prob(30))
+ new /obj/item/reagent_containers/food/snacks/syndicake(src)
diff --git a/code/game/objects/structures/crates_lockers/closets/secure/scientist.dm b/code/game/objects/structures/crates_lockers/closets/secure/scientist.dm
index 7c286dc1a42..489e759b25f 100755
--- a/code/game/objects/structures/crates_lockers/closets/secure/scientist.dm
+++ b/code/game/objects/structures/crates_lockers/closets/secure/scientist.dm
@@ -27,3 +27,21 @@
new /obj/item/circuitboard/machine/techfab/department/science(src)
new /obj/item/storage/photo_album/rd(src)
new /obj/item/storage/box/skillchips/science(src)
+
+
+/obj/structure/closet/secure_closet/cytology
+ name = "cytology equipment locker"
+ icon_state = "science"
+ req_access = list(ACCESS_RESEARCH)
+
+/obj/structure/closet/secure_closet/cytology/PopulateContents()
+ . = ..()
+ new /obj/item/pushbroom(src)
+ new /obj/item/plunger(src)
+ new /obj/item/storage/bag/bio(src)
+ new /obj/item/storage/box/petridish(src)
+ new /obj/item/stack/ducts/fifty(src)
+ for(var/i in 1 to 2)
+ new /obj/item/biopsy_tool(src)
+ new /obj/item/storage/box/swab(src)
+ new /obj/item/construction/plumbing/research(src)
diff --git a/code/game/objects/structures/crates_lockers/crates.dm b/code/game/objects/structures/crates_lockers/crates.dm
index 119a8084a58..7af616d6dc6 100644
--- a/code/game/objects/structures/crates_lockers/crates.dm
+++ b/code/game/objects/structures/crates_lockers/crates.dm
@@ -83,6 +83,27 @@
open_sound_volume = 25
close_sound_volume = 50
+/obj/structure/closet/crate/maint
+
+/obj/structure/closet/crate/maint/PopulateContents()
+ . = ..()
+ for(var/i in 1 to rand(2,6))
+ new /obj/effect/spawner/lootdrop/maintenance(src)
+
+/obj/structure/closet/crate/trashcart/Initialize()
+ . = ..()
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_SLUDGE, CELL_VIRUS_TABLE_GENERIC, rand(2,3), 15)
+
+/obj/structure/closet/crate/trashcart/filled
+
+/obj/structure/closet/crate/trashcart/filled/PopulateContents()
+ . = ..()
+ for(var/i in 1 to rand(7,15))
+ new /obj/effect/spawner/lootdrop/garbage_spawner(src)
+ if(prob(12))
+ new /obj/item/storage/bag/trash/filled(src)
+ new /obj/effect/spawner/scatter/grime(loc)
+
/obj/structure/closet/crate/internals
desc = "An internals crate."
name = "internals crate"
diff --git a/code/game/objects/structures/maintenance.dm b/code/game/objects/structures/maintenance.dm
new file mode 100644
index 00000000000..a59bada2d15
--- /dev/null
+++ b/code/game/objects/structures/maintenance.dm
@@ -0,0 +1,101 @@
+/** This structure acts as a source of moisture loving cell lines,
+as well as a location where a hidden item can somtimes be retrieved
+at the cost of risking a vicious bite.**/
+/obj/structure/moisture_trap
+ name = "moisture trap"
+ desc = "A device installed in order to control moisture in poorly ventilated areas.\nThe stagnant water inside basin seems to produce serious biofouling issues when improperly maintained.\nThis unit in particular seems to be teeming with life!\nWho thought mother Gaia could assert herself so vigoriously in this sterile and desolate place?"
+ icon_state = "moisture_trap"
+ anchored = TRUE
+ density = FALSE
+ ///This var stores the hidden item that might be able to be retrieved from the trap
+ var/obj/item/hidden_item
+ ///This var determines if there is a chance to recieve a bite when sticking your hand into the water.
+ var/critter_infested = TRUE
+ var/list/loot = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/human/mutant/skeleton = 35,
+ /obj/item/reagent_containers/food/snacks/meat/slab/human/mutant/zombie = 15,
+ /obj/item/trash/can = 15,
+ /obj/item/clothing/head/helmet/skull = 10,
+ /obj/item/restraints/handcuffs = 4,
+ /obj/item/restraints/handcuffs/cable/red = 1,
+ /obj/item/restraints/handcuffs/cable/blue = 1,
+ /obj/item/restraints/handcuffs/cable/green = 1,
+ /obj/item/restraints/handcuffs/cable/pink = 1,
+ /obj/item/restraints/handcuffs/alien = 2,
+ /obj/item/coin/bananium = 9,
+ /obj/item/kitchen/knife/butcher = 5,
+ /obj/item/coin/mythril = 1) //the loot table isn't that great and should probably be improved and expanded later.
+
+
+/obj/structure/moisture_trap/Initialize()
+ . = ..()
+ if(prob(40))
+ critter_infested = FALSE
+ if(prob(75))
+ var/picked_item = pickweight(loot)
+ hidden_item = new picked_item(src)
+ loot = null
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_MOIST, CELL_VIRUS_TABLE_GENERIC, rand(2,4), 20)
+
+/obj/structure/moisture_trap/Destroy()
+ if(hidden_item)
+ QDEL_NULL(hidden_item)
+ return ..()
+
+///This proc checks if we are able to reach inside the trap to interact with it.
+/obj/structure/moisture_trap/proc/CanReachInside(mob/user)
+ if(!isliving(user))
+ return FALSE
+ var/mob/living/L = user
+ if((L.mobility_flags & MOBILITY_STAND) && ishuman(L)) //I dont think monkeys can crawl on command.
+ return FALSE
+ return TRUE
+
+/obj/structure/moisture_trap/attack_hand(mob/user)
+ . = ..()
+ if(iscyborg(user) || isalien(user))
+ return
+ if(!CanReachInside(user))
+ to_chat(user, "You need to lie down to reach into [src].")
+ return
+ to_chat(user, "You reach down into the cold water of the basin.")
+ if(!do_after(user, 20, target = src))
+ return
+ if(hidden_item)
+ user.put_in_hands(hidden_item)
+ to_chat(user, "As you poke around inside [src] you feel the contours of something hidden below the murky waters.\nYou retrieve [hidden_item] from [src].")
+ hidden_item = null
+ return
+ if(critter_infested && prob(50) && iscarbon(user))
+ var/mob/living/carbon/bite_victim = user
+ var/obj/item/bodypart/affecting = bite_victim.get_bodypart("[(user.active_hand_index % 2 == 0) ? "r" : "l" ]_arm")
+ if(affecting?.receive_damage(30))
+
+ to_chat(user, "You feel a sharp as an unseen creature sinks it's [pick("fangs", "beak", "proboscis")] into your arm!")
+ bite_victim.update_damage_overlays()
+ playsound(src,'sound/weapons/bite.ogg', 70, TRUE)
+ return
+ to_chat(user, "You find nothing of value...")
+
+/obj/structure/moisture_trap/attackby(obj/item/I, mob/user, params)
+ if(iscyborg(user) || isalien(user) || !CanReachInside(user))
+ return ..()
+ add_fingerprint(user)
+ if(istype(I, /obj/item/reagent_containers))
+ if(istype(I, /obj/item/reagent_containers/food/snacks/monkeycube))
+ var/obj/item/reagent_containers/food/snacks/monkeycube/cube = I
+ cube.Expand()
+ return
+ var/obj/item/reagent_containers/RG = I
+ if(RG.is_open_container())
+ RG.reagents.add_reagent(/datum/reagent/water, min(RG.volume - RG.reagents.total_volume, RG.amount_per_transfer_from_this))
+ to_chat(user, "You fill [RG] from [src].")
+ return
+ if(hidden_item)
+ to_chat(user, "There is already something inside [src].")
+ return
+ if(!user.transferItemToLoc(I, src))
+ to_chat(user, "\The [I] is stuck to your hand, you cannot put it in [src]!")
+ return
+ hidden_item = I
+ to_chat(user, "You hide [I] inside the basin.")
diff --git a/code/modules/antagonists/blob/blob_mobs.dm b/code/modules/antagonists/blob/blob_mobs.dm
index f4a9a021d2d..a204e3fcd2a 100644
--- a/code/modules/antagonists/blob/blob_mobs.dm
+++ b/code/modules/antagonists/blob/blob_mobs.dm
@@ -123,6 +123,7 @@
factory.spores += src
if(linked_node.overmind && istype(linked_node.overmind.blobstrain, /datum/blobstrain/reagent/distributed_neurons) && !istype(src, /mob/living/simple_animal/hostile/blob/blobspore/weak))
notify_ghosts("A controllable spore has been created in \the [get_area(src)].", source = src, action = NOTIFY_ORBIT, flashwindow = FALSE, header = "Sentient Spore Created")
+ add_cell_sample()
/mob/living/simple_animal/hostile/blob/blobspore/Life()
if(!is_zombie && isturf(src.loc))
@@ -225,6 +226,12 @@
color = initial(color)//looks better.
add_overlay(blob_head_overlay)
+/mob/living/simple_animal/hostile/blob/blobspore/add_cell_sample()
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOBSPORE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
+
+/mob/living/simple_animal/hostile/blob/blobspore/independent
+ independent = TRUE
+
/mob/living/simple_animal/hostile/blob/blobspore/weak
name = "fragile blob spore"
health = 15
@@ -261,6 +268,13 @@
mob_size = MOB_SIZE_LARGE
hud_type = /datum/hud/blobbernaut
+/mob/living/simple_animal/hostile/blob/blobbernaut/Initialize()
+ . = ..()
+ add_cell_sample()
+
+/mob/living/simple_animal/hostile/blob/blobbernaut/add_cell_sample()
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOBBERNAUT, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
+
/mob/living/simple_animal/hostile/blob/blobbernaut/Life()
if(..())
var/list/blobs_in_area = range(2, src)
diff --git a/code/modules/antagonists/blob/structures/_blob.dm b/code/modules/antagonists/blob/structures/_blob.dm
index 724b4136510..3944de36c8e 100644
--- a/code/modules/antagonists/blob/structures/_blob.dm
+++ b/code/modules/antagonists/blob/structures/_blob.dm
@@ -33,6 +33,7 @@
if(atmosblock)
air_update_turf(1)
ConsumeTile()
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOB, CELL_VIRUS_TABLE_GENERIC, 2, 2)
/obj/structure/blob/proc/creation_action() //When it's created by the overmind, do this.
return
diff --git a/tgstation.dme b/tgstation.dme
index ae64aee4f55..d431bd3ec75 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -193,6 +193,7 @@
#include "code\_globalvars\lists\objects.dm"
#include "code\_globalvars\lists\poll_ignore.dm"
#include "code\_globalvars\lists\typecache.dm"
+#include "code\_globalvars\lists\xenobiology.dm"
#include "code\_js\byjax.dm"
#include "code\_js\menus.dm"
#include "code\_onclick\adjacent.dm"
@@ -229,6 +230,7 @@
#include "code\_onclick\hud\map_popups.dm"
#include "code\_onclick\hud\monkey.dm"
#include "code\_onclick\hud\movable_screen_objects.dm"
+#include "code\_onclick\hud\ooze.dm"
#include "code\_onclick\hud\pai.dm"
#include "code\_onclick\hud\parallax.dm"
#include "code\_onclick\hud\picture_in_picture.dm"
@@ -464,6 +466,7 @@
#include "code\datums\components\stationstuck.dm"
#include "code\datums\components\summoning.dm"
#include "code\datums\components\surgery_initiator.dm"
+#include "code\datums\components\swabbing.dm"
#include "code\datums\components\swarming.dm"
#include "code\datums\components\tackle.dm"
#include "code\datums\components\tactical.dm"
@@ -567,6 +570,7 @@
#include "code\datums\elements\selfknockback.dm"
#include "code\datums\elements\snail_crawl.dm"
#include "code\datums\elements\squish.dm"
+#include "code\datums\elements\swabbable.dm"
#include "code\datums\elements\tool_flash.dm"
#include "code\datums\elements\undertile.dm"
#include "code\datums\elements\update_icon_blocker.dm"
@@ -948,6 +952,8 @@
#include "code\game\objects\effects\spawners\bundle.dm"
#include "code\game\objects\effects\spawners\gibspawner.dm"
#include "code\game\objects\effects\spawners\lootdrop.dm"
+#include "code\game\objects\effects\spawners\lootsite.dm"
+#include "code\game\objects\effects\spawners\scatter.dm"
#include "code\game\objects\effects\spawners\structure.dm"
#include "code\game\objects\effects\spawners\traps.dm"
#include "code\game\objects\effects\spawners\vaultspawner.dm"