diff --git a/code/__DEFINES/MC.dm b/code/__DEFINES/MC.dm
index 3b39140fcde..94593dec5b2 100644
--- a/code/__DEFINES/MC.dm
+++ b/code/__DEFINES/MC.dm
@@ -102,3 +102,11 @@
}\
/datum/controller/subsystem/processing/##X/fire() {..() /*just so it shows up on the profiler*/} \
/datum/controller/subsystem/processing/##X
+
+#define FLUID_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/fluids/##X);\
+/datum/controller/subsystem/fluids/##X/New(){\
+ NEW_SS_GLOBAL(SS##X);\
+ PreInit();\
+}\
+/datum/controller/subsystem/fluids/##X/fire() {..() /*just so it shows up on the profiler*/} \
+/datum/controller/subsystem/fluids/##X
diff --git a/code/__DEFINES/maths.dm b/code/__DEFINES/maths.dm
index 91831182b99..b4de855bef9 100644
--- a/code/__DEFINES/maths.dm
+++ b/code/__DEFINES/maths.dm
@@ -44,6 +44,9 @@
// Similar to clamp but the bottom rolls around to the top and vice versa. min is inclusive, max is exclusive
#define WRAP(val, min, max) clamp(( min == max ? min : (val) - (round(((val) - (min))/((max) - (min))) * ((max) - (min))) ),min,max)
+/// Increments a value and wraps it if it exceeds some value. Can be used to circularly iterate through a list through `idx = WRAP_UP(idx, length_of_list)`.
+#define WRAP_UP(val, max) (((val) % (max)) + 1)
+
// Real modulus that handles decimals
#define MODULUS(x, y) ( (x) - FLOOR(x, y))
@@ -226,3 +229,6 @@
// )
#define GET_TRUE_DIST(a, b) (a == null || b == null) ? -1 : max(abs(a.x -b.x), abs(a.y-b.y), abs(a.z-b.z))
+
+/// The number of cells in a taxicab circle (rasterized diamond) of radius X.
+#define DIAMOND_AREA(X) (1 + 2*(X)*((X)+1))
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index 91dc340204a..aa942b9d13e 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -175,6 +175,7 @@
#define FIRE_PRIORITY_GARBAGE 15
#define FIRE_PRIORITY_DATABASE 16
#define FIRE_PRIORITY_WET_FLOORS 20
+#define FIRE_PRIORITY_FLUIDS 20
#define FIRE_PRIORITY_AIR 20
#define FIRE_PRIORITY_NPC 20
#define FIRE_PRIORITY_NPC_MOVEMENT 21
@@ -302,7 +303,7 @@
// Subsystem delta times or tickrates, in seconds. I.e, how many seconds in between each process() call for objects being processed by that subsystem.
// Only use these defines if you want to access some other objects processing delta_time, otherwise use the delta_time that is sent as a parameter to process()
-#define SSFLUIDS_DT (SSfluids.wait/10)
+#define SSFLUIDS_DT (SSplumbing.wait/10)
#define SSMACHINES_DT (SSmachines.wait/10)
#define SSMOBS_DT (SSmobs.wait/10)
#define SSOBJ_DT (SSobj.wait/10)
diff --git a/code/__HELPERS/reagents.dm b/code/__HELPERS/reagents.dm
index 39d5b1ea37b..bfd00ed4698 100644
--- a/code/__HELPERS/reagents.dm
+++ b/code/__HELPERS/reagents.dm
@@ -79,11 +79,13 @@
GLOB.chemical_reactions_list_reactant_index[primary_reagent] += R
//Creates foam from the reagent. Metaltype is for metal foam, notification is what to show people in textbox
-/datum/reagents/proc/create_foam(foamtype,foam_volume,metaltype = 0,notification = null)
+/datum/reagents/proc/create_foam(foamtype, foam_volume, result_type = null, notification = null)
var/location = get_turf(my_atom)
- var/datum/effect_system/foam_spread/foam = new foamtype()
- foam.set_up(foam_volume, location, src, metaltype)
+
+ var/datum/effect_system/fluid_spread/foam/foam = new foamtype()
+ foam.set_up(amount = foam_volume, location = location, carry = src, result_type = result_type)
foam.start()
+
clear_reagents()
if(!notification)
return
diff --git a/code/controllers/subsystem/fluids.dm b/code/controllers/subsystem/fluids.dm
new file mode 100644
index 00000000000..cc77430b7d2
--- /dev/null
+++ b/code/controllers/subsystem/fluids.dm
@@ -0,0 +1,261 @@
+// Flags indicating what parts of the fluid the subsystem processes.
+/// Indicates that a fluid subsystem processes fluid spreading.
+#define SS_PROCESSES_SPREADING (1<<0)
+/// Indicates that a fluid subsystem processes fluid effects.
+#define SS_PROCESSES_EFFECTS (1<<1)
+
+/**
+ * # Fluid Subsystem
+ *
+ * A subsystem that processes the propagation and effects of a particular fluid.
+ *
+ * Both fluid spread and effect processing are handled through a carousel system.
+ * Fluids being spread and fluids being processed are organized into buckets.
+ * Each fresh (non-resumed) fire one bucket of each is selected to be processed.
+ * These selected buckets are then fully processed.
+ * The next fresh fire selects the next bucket in each set for processing.
+ * If this would walk off the end of a carousel list we wrap back to the first element.
+ * This effectively makes each set a circular list, hence a carousel.
+ */
+SUBSYSTEM_DEF(fluids)
+ name = "Fluid"
+ wait = 0 // Will be autoset to whatever makes the most sense given the spread and effect waits.
+ flags = SS_BACKGROUND|SS_KEEP_TIMING
+ runlevels = RUNLEVEL_GAME|RUNLEVEL_POSTGAME
+
+ // Fluid spread processing:
+ /// The amount of time (in deciseconds) before a fluid node is created and when it spreads.
+ var/spread_wait = 1 SECONDS
+ /// The number of buckets in the spread carousel.
+ var/num_spread_buckets
+ /// The set of buckets containing fluid nodes to spread.
+ var/list/spread_carousel
+ /// The index of the spread carousel bucket currently being processed.
+ var/spread_bucket_index
+ /// The set of fluid nodes we are currently processing spreading for.
+ var/list/currently_spreading
+ /// Whether the subsystem has resumed spreading fluid.
+ var/resumed_spreading
+
+ // Fluid effect processing:
+ /// The amount of time (in deciseconds) between effect processing ticks for each fluid node.
+ var/effect_wait = 1 SECONDS
+ /// The number of buckets in the effect carousel.
+ var/num_effect_buckets
+ /// The set of buckets containing fluid nodes to process effects for.
+ var/list/effect_carousel
+ /// The index of the currently processing bucket on the effect carousel.
+ var/effect_bucket_index
+ /// The set of fluid nodes we are currently processing effects for.
+ var/list/currently_processing
+ /// Whether the subsystem has resumed processing fluid effects.
+ var/resumed_effect_processing
+
+/datum/controller/subsystem/fluids/Initialize(start_timeofday)
+ initialize_waits()
+ initialize_spread_carousel()
+ initialize_effect_carousel()
+ return ..()
+
+/**
+ * Initializes the subsystem waits.
+ *
+ * Ensures that the subsystem's fire wait evenly splits the spread and effect waits.
+ */
+/datum/controller/subsystem/fluids/proc/initialize_waits()
+ if (spread_wait <= 0)
+ WARNING("[src] has the invalid spread wait [spread_wait].")
+ spread_wait = 1 SECONDS
+ if (effect_wait <= 0)
+ WARNING("[src] has the invalid effect wait [effect_wait].")
+ spread_wait = 1 SECONDS
+
+ // Sets the overall wait of the subsystem to evenly divide both the effect and spread waits.
+ var/max_wait = Gcd(spread_wait, effect_wait)
+ if (max_wait < wait || wait <= 0)
+ wait = max_wait
+ else
+ // If the wait of the subsystem overall is set to a valid value make the actual wait of the subsystem evenly divide that as well.
+ // Makes effect bubbling possible with identical spread and effect waits.
+ wait = Gcd(wait, max_wait)
+
+
+/**
+ * Initializes the carousel used to process fluid spreading.
+ *
+ * Synchronizes the spread delta time with the actual target spread tick rate.
+ * Builds the carousel buckets used to queue spreads.
+ */
+/datum/controller/subsystem/fluids/proc/initialize_spread_carousel()
+ // Make absolutely certain that the spread wait is in sync with the target spread tick rate.
+ num_spread_buckets = round(spread_wait / wait)
+ spread_wait = wait * num_spread_buckets
+
+ spread_carousel = list()
+ spread_carousel.len = num_spread_buckets
+ for(var/i in 1 to num_spread_buckets)
+ spread_carousel[i] = list()
+ currently_spreading = list()
+ spread_bucket_index = 1
+
+/**
+ * Initializes the carousel used to process fluid effects.
+ *
+ * Synchronizes the spread delta time with the actual target spread tick rate.
+ * Builds the carousel buckets used to bubble processing.
+ */
+/datum/controller/subsystem/fluids/proc/initialize_effect_carousel()
+ // Make absolutely certain that the effect wait is in sync with the target effect tick rate.
+ num_effect_buckets = round(effect_wait / wait)
+ effect_wait = wait * num_effect_buckets
+
+ effect_carousel = list()
+ effect_carousel.len = num_effect_buckets
+ for(var/i in 1 to num_effect_buckets)
+ effect_carousel[i] = list()
+ currently_processing = list()
+ effect_bucket_index = 1
+
+
+/datum/controller/subsystem/fluids/fire(resumed)
+ var/delta_time
+ var/cached_bucket_index
+ var/list/obj/effect/particle_effect/fluid/currentrun
+ MC_SPLIT_TICK_INIT(2)
+
+ MC_SPLIT_TICK // Start processing fluid spread:
+ if(!resumed_spreading)
+ spread_bucket_index = WRAP_UP(spread_bucket_index, num_spread_buckets)
+ currently_spreading = spread_carousel[spread_bucket_index]
+ spread_carousel[spread_bucket_index] = list() // Reset the bucket so we don't process an _entire station's worth of foam_ spreading every 2 ticks when the foam flood event happens.
+ resumed_spreading = TRUE
+
+ delta_time = spread_wait / (1 SECONDS)
+ currentrun = currently_spreading
+ while(currentrun.len)
+ var/obj/effect/particle_effect/fluid/to_spread = currentrun[currentrun.len]
+ currentrun.len--
+
+ if(!QDELETED(to_spread))
+ to_spread.spread(delta_time)
+ to_spread.spread_bucket = null
+
+ if (MC_TICK_CHECK)
+ break
+
+ if(!currentrun.len)
+ resumed_spreading = FALSE
+
+ MC_SPLIT_TICK // Start processing fluid effects:
+ if(!resumed_effect_processing)
+ effect_bucket_index = WRAP_UP(effect_bucket_index, num_effect_buckets)
+ var/list/tmp_list = effect_carousel[effect_bucket_index]
+ currently_processing = tmp_list.Copy()
+ resumed_effect_processing = TRUE
+
+ delta_time = effect_wait / (1 SECONDS)
+ cached_bucket_index = effect_bucket_index
+ currentrun = currently_processing
+ while(currentrun.len)
+ var/obj/effect/particle_effect/fluid/to_process = currentrun[currentrun.len]
+ currentrun.len--
+
+ if (QDELETED(to_process) || to_process.process(delta_time) == PROCESS_KILL)
+ effect_carousel[cached_bucket_index] -= to_process
+ to_process.effect_bucket = null
+ to_process.datum_flags &= ~DF_ISPROCESSING
+
+ if (MC_TICK_CHECK)
+ break
+
+ if(!currentrun.len)
+ resumed_effect_processing = FALSE
+
+
+/**
+ * Queues a fluid node to spread later after one full carousel rotation.
+ *
+ * Arguments:
+ * - [node][/obj/effect/particle_effect/fluid]: The node to queue to spread.
+ */
+/datum/controller/subsystem/fluids/proc/queue_spread(obj/effect/particle_effect/fluid/node)
+ if (node.spread_bucket)
+ return
+
+ spread_carousel[spread_bucket_index] += node
+ node.spread_bucket = spread_bucket_index
+
+/**
+ * Cancels a queued spread of a fluid node.
+ *
+ * Arguments:
+ * - [node][/obj/effect/particle_effect/fluid]: The node to cancel the spread of.
+ */
+/datum/controller/subsystem/fluids/proc/cancel_spread(obj/effect/particle_effect/fluid/node)
+ if(!node.spread_bucket)
+ return
+
+ var/bucket_index = node.spread_bucket
+ spread_carousel[bucket_index] -= node
+ if (bucket_index == spread_bucket_index)
+ currently_spreading -= node
+
+ node.spread_bucket = null
+
+/**
+ * Starts processing the effects of a fluid node.
+ *
+ * The fluid node will next process after one full bucket rotation.
+ *
+ * Arguments:
+ * - [node][/obj/effect/particle_effect/fluid]: The node to start processing.
+ */
+/datum/controller/subsystem/fluids/proc/start_processing(obj/effect/particle_effect/fluid/node)
+ if (node.datum_flags & DF_ISPROCESSING || node.effect_bucket)
+ return
+
+ // Edit this value to make all fluids process effects (at the same time|offset by when they started processing| -> offset by a random amount <- )
+ var/bucket_index = rand(1, num_effect_buckets)
+ effect_carousel[bucket_index] += node
+ node.effect_bucket = bucket_index
+ node.datum_flags |= DF_ISPROCESSING
+
+/**
+ * Stops processing the effects of a fluid node.
+ *
+ * Arguments:
+ * - [node][/obj/effect/particle_effect/fluid]: The node to stop processing.
+ */
+/datum/controller/subsystem/fluids/proc/stop_processing(obj/effect/particle_effect/fluid/node)
+ if(!(node.datum_flags & DF_ISPROCESSING))
+ return
+
+ var/bucket_index = node.effect_bucket
+ if(!bucket_index)
+ return
+
+ effect_carousel[bucket_index] -= node
+ if (bucket_index == effect_bucket_index)
+ currently_processing -= node
+
+ node.effect_bucket = null
+ node.datum_flags &= ~DF_ISPROCESSING
+
+#undef SS_PROCESSES_SPREADING
+#undef SS_PROCESSES_EFFECTS
+
+
+// Subtypes:
+
+/// The subsystem responsible for processing smoke propagation and effects.
+FLUID_SUBSYSTEM_DEF(smoke)
+ name = "Smoke"
+ spread_wait = 0.1 SECONDS
+ effect_wait = 2.0 SECONDS
+
+/// The subsystem responsible for processing foam propagation and effects.
+FLUID_SUBSYSTEM_DEF(foam)
+ name = "Foam"
+ wait = 0.1 SECONDS // Makes effect bubbling work with foam.
+ spread_wait = 0.2 SECONDS
+ effect_wait = 0.2 SECONDS
diff --git a/code/controllers/subsystem/processing/fluids.dm b/code/controllers/subsystem/processing/plumbing.dm
similarity index 58%
rename from code/controllers/subsystem/processing/fluids.dm
rename to code/controllers/subsystem/processing/plumbing.dm
index 70903e0088c..34d6d8f882e 100644
--- a/code/controllers/subsystem/processing/fluids.dm
+++ b/code/controllers/subsystem/processing/plumbing.dm
@@ -1,5 +1,5 @@
-PROCESSING_SUBSYSTEM_DEF(fluids)
- name = "Fluids"
+PROCESSING_SUBSYSTEM_DEF(plumbing)
+ name = "Plumbing"
wait = 10
stat_tag = "FD" //its actually Fluid Ducts
flags = SS_NO_INIT
diff --git a/code/datums/components/plumbing/_plumbing.dm b/code/datums/components/plumbing/_plumbing.dm
index c8547a9b271..47e4f580732 100644
--- a/code/datums/components/plumbing/_plumbing.dm
+++ b/code/datums/components/plumbing/_plumbing.dm
@@ -68,7 +68,7 @@
/datum/component/plumbing/process()
if(!demand_connects || !reagents)
- STOP_PROCESSING(SSfluids, src)
+ STOP_PROCESSING(SSplumbing, src)
return
if(reagents.total_volume < reagents.maximum_volume)
for(var/D in GLOB.cardinals)
@@ -199,7 +199,7 @@
if(!active)
return
- STOP_PROCESSING(SSfluids, src)
+ STOP_PROCESSING(SSplumbing, src)
for(var/A in ducts)
var/datum/ductnet/D = ducts[A]
@@ -231,7 +231,7 @@
D.disconnect_duct()
if(demand_connects)
- START_PROCESSING(SSfluids, src)
+ START_PROCESSING(SSplumbing, src)
for(var/D in GLOB.cardinals)
diff --git a/code/game/machinery/ai_slipper.dm b/code/game/machinery/ai_slipper.dm
index 2a28d35cb95..1427f16e77b 100644
--- a/code/game/machinery/ai_slipper.dm
+++ b/code/game/machinery/ai_slipper.dm
@@ -37,7 +37,7 @@
if(cooldown_time > world.time)
to_chat(user, span_warning("[src] cannot be activated for [DisplayTimeText(world.time - cooldown_time)]!"))
return
- new /obj/effect/particle_effect/foam(loc)
+ new /obj/effect/particle_effect/fluid/foam(loc)
uses--
to_chat(user, span_notice("You activate [src]. It now has [uses] uses of foam remaining."))
cooldown = world.time + cooldown_time
diff --git a/code/game/machinery/suit_storage_unit.dm b/code/game/machinery/suit_storage_unit.dm
index 7fa56c56c8c..7bcda26b8b0 100644
--- a/code/game/machinery/suit_storage_unit.dm
+++ b/code/game/machinery/suit_storage_unit.dm
@@ -388,8 +388,8 @@
if(uv_super)
visible_message(span_warning("[src]'s door creaks open with a loud whining noise. A cloud of foul black smoke escapes from its chamber."))
playsound(src, 'sound/machines/airlock_alien_prying.ogg', 50, TRUE)
- var/datum/effect_system/smoke_spread/bad/black/smoke = new
- smoke.set_up(0, src)
+ var/datum/effect_system/fluid_spread/smoke/bad/black/smoke = new
+ smoke.set_up(0, location = src)
smoke.start()
QDEL_NULL(helmet)
QDEL_NULL(suit)
diff --git a/code/game/objects/effects/anomalies.dm b/code/game/objects/effects/anomalies.dm
index 5c69ad2e9d4..f1e3dd3ba8d 100644
--- a/code/game/objects/effects/anomalies.dm
+++ b/code/game/objects/effects/anomalies.dm
@@ -79,7 +79,7 @@
qdel(src)
/obj/effect/anomaly/proc/anomalyNeutralize()
- new /obj/effect/particle_effect/smoke/bad(loc)
+ new /obj/effect/particle_effect/fluid/smoke/bad(loc)
if(drops_core)
aSignal.forceMove(drop_location())
diff --git a/code/game/objects/effects/effect_system/effects_explosion.dm b/code/game/objects/effects/effect_system/effects_explosion.dm
index 60ed4f31dcb..13d32870de8 100644
--- a/code/game/objects/effects/effect_system/effects_explosion.dm
+++ b/code/game/objects/effects/effect_system/effects_explosion.dm
@@ -57,8 +57,8 @@
/datum/effect_system/explosion/smoke
/datum/effect_system/explosion/smoke/proc/create_smoke()
- var/datum/effect_system/smoke_spread/S = new
- S.set_up(2, location)
+ var/datum/effect_system/fluid_spread/smoke/S = new
+ S.set_up(2, location = location)
S.start()
/datum/effect_system/explosion/smoke/start()
diff --git a/code/game/objects/effects/effect_system/effects_foam.dm b/code/game/objects/effects/effect_system/effects_foam.dm
deleted file mode 100644
index 59ef1d376c2..00000000000
--- a/code/game/objects/effects/effect_system/effects_foam.dm
+++ /dev/null
@@ -1,386 +0,0 @@
-// Foam
-// Similar to smoke, but slower and mobs absorb its reagent through their exposed skin.
-#define ALUMINUM_FOAM 1
-#define IRON_FOAM 2
-#define RESIN_FOAM 3
-
-
-/obj/effect/particle_effect/foam
- name = "foam"
- icon_state = "foam"
- opacity = FALSE
- anchored = TRUE
- density = FALSE
- layer = EDGED_TURF_LAYER
- plane = GAME_PLANE_UPPER
- mouse_opacity = MOUSE_OPACITY_TRANSPARENT
- var/amount = 3
- animate_movement = NO_STEPS
- var/metal = 0
- var/lifetime = 40
- var/reagent_divisor = 7
- var/static/list/blacklisted_turfs = typecacheof(list(
- /turf/open/space/transit,
- /turf/open/chasm,
- /turf/open/lava,
- ))
- var/slippery_foam = TRUE
-
-/obj/effect/particle_effect/foam/firefighting
- name = "firefighting foam"
- lifetime = 20 //doesn't last as long as normal foam
- amount = 0 //no spread
- slippery_foam = FALSE
- var/absorbed_plasma = 0
-
-/obj/effect/particle_effect/foam/firefighting/Initialize(mapload)
- . = ..()
- RemoveElement(/datum/element/atmos_sensitive)
-
-/obj/effect/particle_effect/foam/firefighting/process()
- ..()
-
- var/turf/open/T = get_turf(src)
- var/obj/effect/hotspot/hotspot = (locate(/obj/effect/hotspot) in T)
- if(hotspot && istype(T) && T.air)
- qdel(hotspot)
- var/datum/gas_mixture/G = T.air
- if(G.gases[/datum/gas/plasma])
- var/plas_amt = min(30,G.gases[/datum/gas/plasma][MOLES]) //Absorb some plasma
- G.gases[/datum/gas/plasma][MOLES] -= plas_amt
- absorbed_plasma += plas_amt
- if(G.temperature > T20C)
- G.temperature = max(G.temperature/2,T20C)
- G.garbage_collect()
- T.air_update_turf(FALSE, FALSE)
-
-/obj/effect/particle_effect/foam/firefighting/kill_foam()
- STOP_PROCESSING(SSfastprocess, src)
-
- if(absorbed_plasma)
- var/obj/effect/decal/cleanable/plasma/P = (locate(/obj/effect/decal/cleanable/plasma) in get_turf(src))
- if(!P)
- P = new(loc)
- P.reagents.add_reagent(/datum/reagent/stable_plasma, absorbed_plasma)
-
- flick("[icon_state]-disolve", src)
- QDEL_IN(src, 5)
-
-/obj/effect/particle_effect/foam/firefighting/foam_mob(mob/living/L)
- if(!istype(L))
- return
- L.adjust_wet_stacks(2)
-
-/obj/effect/particle_effect/foam/metal
- name = "aluminium foam"
- metal = ALUMINUM_FOAM
- icon_state = "mfoam"
- slippery_foam = FALSE
-
-/obj/effect/particle_effect/foam/metal/smart
- name = "smart foam"
-
-/obj/effect/particle_effect/foam/metal/iron
- name = "iron foam"
- metal = IRON_FOAM
-
-/obj/effect/particle_effect/foam/metal/resin
- name = "resin foam"
- metal = RESIN_FOAM
-
-/obj/effect/particle_effect/foam/long_life
- lifetime = 150
-
-/obj/effect/particle_effect/foam/Initialize(mapload)
- . = ..()
- create_reagents(1000) //limited by the size of the reagent holder anyway.
- START_PROCESSING(SSfastprocess, src)
- playsound(src, 'sound/effects/bubbles2.ogg', 80, TRUE, -3)
- AddElement(/datum/element/atmos_sensitive, mapload)
-
-/obj/effect/particle_effect/foam/ComponentInitialize()
- . = ..()
- if(slippery_foam)
- AddComponent(/datum/component/slippery, 100)
-
-/obj/effect/particle_effect/foam/Destroy()
- STOP_PROCESSING(SSfastprocess, src)
- return ..()
-
-
-/obj/effect/particle_effect/foam/proc/kill_foam()
- STOP_PROCESSING(SSfastprocess, src)
- switch(metal)
- if(ALUMINUM_FOAM)
- new /obj/structure/foamedmetal(get_turf(src))
- if(IRON_FOAM)
- new /obj/structure/foamedmetal/iron(get_turf(src))
- if(RESIN_FOAM)
- new /obj/structure/foamedmetal/resin(get_turf(src))
- flick("[icon_state]-disolve", src)
- QDEL_IN(src, 5)
-
-/obj/effect/particle_effect/foam/smart/kill_foam() //Smart foam adheres to area borders for walls
- STOP_PROCESSING(SSfastprocess, src)
- if(metal)
- var/turf/T = get_turf(src)
- if(isspaceturf(T)) //Block up any exposed space
- T.PlaceOnTop(/turf/open/floor/plating/foam, flags = CHANGETURF_INHERIT_AIR)
- for(var/direction in GLOB.cardinals)
- var/turf/cardinal_turf = get_step(T, direction)
- if(get_area(cardinal_turf) != get_area(T)) //We're at an area boundary, so let's block off this turf!
- new/obj/structure/foamedmetal(T)
- break
- flick("[icon_state]-disolve", src)
- QDEL_IN(src, 5)
-
-/obj/effect/particle_effect/foam/process()
- lifetime--
- if(lifetime < 1)
- kill_foam()
- return
-
- var/fraction = 1/initial(reagent_divisor)
- for(var/obj/O in range(0,src))
- if(O.type == src.type)
- continue
- if(isturf(O.loc))
- var/turf/T = O.loc
- if(T.underfloor_accessibility < UNDERFLOOR_INTERACTABLE && HAS_TRAIT(O, TRAIT_T_RAY_VISIBLE))
- continue
- if(lifetime % reagent_divisor)
- reagents.expose(O, VAPOR, fraction)
- var/hit = 0
- for(var/mob/living/L in range(0,src))
- hit += foam_mob(L)
- if(hit)
- lifetime++ //this is so the decrease from mobs hit and the natural decrease don't cumulate.
- var/T = get_turf(src)
- if(lifetime % reagent_divisor)
- reagents.expose(T, VAPOR, fraction)
-
- if(--amount < 0)
- return
- spread_foam()
-
-/obj/effect/particle_effect/foam/proc/foam_mob(mob/living/L)
- if(lifetime<1)
- return FALSE
- if(!istype(L))
- return FALSE
- var/fraction = 1/initial(reagent_divisor)
- if(lifetime % reagent_divisor)
- reagents.expose(L, VAPOR, fraction)
- lifetime--
- return TRUE
-
-/obj/effect/particle_effect/foam/proc/spread_foam()
- var/turf/t_loc = get_turf(src)
- //This should just be atmos adjacent turfs, come on guys
- for(var/turf/T in t_loc.reachableAdjacentTurfs())
- var/obj/effect/particle_effect/foam/foundfoam = locate() in T //Don't spread foam where there's already foam!
- if(foundfoam)
- continue
-
- if(is_type_in_typecache(T, blacklisted_turfs))
- continue
-
- for(var/mob/living/L in T)
- foam_mob(L)
- var/obj/effect/particle_effect/foam/F = new src.type(T)
- F.amount = amount
- reagents.copy_to(F, (reagents.total_volume))
- F.add_atom_colour(color, FIXED_COLOUR_PRIORITY)
- F.metal = metal
-
-/obj/effect/particle_effect/foam/should_atmos_process(datum/gas_mixture/air, exposed_temperature)
- return exposed_temperature > 475
-
-/obj/effect/particle_effect/foam/atmos_expose(datum/gas_mixture/air, exposed_temperature)
- if(prob(max(0, exposed_temperature - 475))) //foam dissolves when heated
- kill_foam()
-
-
-///////////////////////////////////////////////
-//FOAM EFFECT DATUM
-/datum/effect_system/foam_spread
- /// the size of the foam spread.
- var/amount = 10
- /// Stupid hack alertm exists to hold chems for us
- var/atom/movable/chem_holder/chemholder
- effect_type = /obj/effect/particle_effect/foam
- var/metal = 0
-
-
-/datum/effect_system/foam_spread/metal
- effect_type = /obj/effect/particle_effect/foam/metal
-
-
-/datum/effect_system/foam_spread/metal/smart
- effect_type = /obj/effect/particle_effect/foam/smart
-
-
-/datum/effect_system/foam_spread/long
- effect_type = /obj/effect/particle_effect/foam/long_life
-
-/datum/effect_system/foam_spread/New()
- ..()
- chemholder = new()
- chemholder.create_reagents(1000, NO_REACT)
-
-/datum/effect_system/foam_spread/Destroy()
- QDEL_NULL(chemholder)
- return ..()
-
-/datum/effect_system/foam_spread/set_up(amt=5, loca, datum/reagents/carry = null, metaltype = 0)
- if(isturf(loca))
- location = loca
- else
- location = get_turf(loca)
-
- amount = round(sqrt(amt / 2), 1)
- carry.copy_to(chemholder, carry.total_volume)
- if(metaltype)
- metal = metaltype
-
-/datum/effect_system/foam_spread/start()
- var/obj/effect/particle_effect/foam/F = new effect_type(location)
- var/foamcolor = mix_color_from_reagents(chemholder.reagents.reagent_list)
- // To prevent insane reagent multiplication with 1u foam
- // I am capping amount of reagent foam recieves by limiting how low it can go
- // Any radius of foam less than 3 makes foam recieve same amount of reagents as foam of radius 3
- // Maximum multiplication of reagents is about 166% (3 times as low as before, it was about 500% with 1u foam)
- //
- // amount is radius of the foam
- // 10u foam has radius of 3
- // 5u foam has radius of 2
- // 1u foam has radius of 1
- var/effective_amount = chemholder.reagents.total_volume / max(amount, 3)
- chemholder.reagents.copy_to(F, effective_amount)
- F.add_atom_colour(foamcolor, FIXED_COLOUR_PRIORITY)
- F.amount = amount
- F.metal = metal
-
-
-//////////////////////////////////////////////////////////
-// FOAM STRUCTURE. Formed by metal foams. Dense and opaque, but easy to break
-/obj/structure/foamedmetal
- icon = 'icons/effects/effects.dmi'
- icon_state = "metalfoam"
- density = TRUE
- opacity = TRUE // changed in New()
- anchored = TRUE
- layer = EDGED_TURF_LAYER
- plane = GAME_PLANE_UPPER
- resistance_flags = FIRE_PROOF | ACID_PROOF
- name = "foamed metal"
- desc = "A lightweight foamed metal wall that can be used as base to construct a wall."
- gender = PLURAL
- max_integrity = 20
- can_atmos_pass = ATMOS_PASS_DENSITY
- obj_flags = CAN_BE_HIT | BLOCK_Z_IN_DOWN | BLOCK_Z_IN_UP
- ///Var used to prevent spamming of the construction sound
- var/next_beep = 0
-
-/obj/structure/foamedmetal/Initialize(mapload)
- . = ..()
- air_update_turf(TRUE, TRUE)
-
-/obj/structure/foamedmetal/Destroy()
- air_update_turf(TRUE, FALSE)
- . = ..()
-
-/obj/structure/foamedmetal/Move()
- var/turf/T = loc
- . = ..()
- move_update_air(T)
-
-/obj/structure/foamedmetal/attack_paw(mob/user, list/modifiers)
- return attack_hand(user, modifiers)
-
-/obj/structure/foamedmetal/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0)
- playsound(src.loc, 'sound/weapons/tap.ogg', 100, TRUE)
-
-/obj/structure/foamedmetal/attack_hand(mob/user, list/modifiers)
- . = ..()
- if(.)
- return
- user.changeNext_move(CLICK_CD_MELEE)
- user.do_attack_animation(src, ATTACK_EFFECT_PUNCH)
- to_chat(user, span_warning("You hit [src] but bounce off it!"))
- playsound(src.loc, 'sound/weapons/tap.ogg', 100, TRUE)
-
-/obj/structure/foamedmetal/attackby(obj/item/W, mob/user, params)
- ///A speed modifier for how fast the wall is build
- var/platingmodifier = 1
- if(HAS_TRAIT(user, TRAIT_QUICK_BUILD))
- platingmodifier = 0.7
- if(next_beep <= world.time)
- next_beep = world.time + 1 SECONDS
- playsound(src, 'sound/machines/clockcult/integration_cog_install.ogg', 50, TRUE)
- add_fingerprint(user)
-
- if(!istype(W, /obj/item/stack/sheet))
- return ..()
-
- var/obj/item/stack/sheet/sheet_for_plating = W
- if(istype(sheet_for_plating, /obj/item/stack/sheet/iron))
- if(sheet_for_plating.get_amount() < 2)
- to_chat(user, span_warning("You need two sheets of iron to finish a wall on [src]!"))
- return
- to_chat(user, span_notice("You start adding plating to the foam structure..."))
- if (do_after(user, 40*platingmodifier, target = src))
- if(!sheet_for_plating.use(2))
- return
- to_chat(user, span_notice("You add the plating."))
- var/turf/T = get_turf(src)
- T.PlaceOnTop(/turf/closed/wall/metal_foam_base)
- transfer_fingerprints_to(T)
- qdel(src)
- return
-
- add_hiddenprint(user)
-
-/obj/structure/foamedmetal/iron
- max_integrity = 50
- icon_state = "ironfoam"
-
-//Atmos Backpack Resin, transparent, prevents atmos and filters the air
-/obj/structure/foamedmetal/resin
- name = "\improper ATMOS Resin"
- desc = "A lightweight, transparent resin used to suffocate fires, scrub the air of toxins, and restore the air to a safe temperature. It can be used as base to construct a wall."
- opacity = FALSE
- icon_state = "atmos_resin"
- alpha = 120
- max_integrity = 10
- pass_flags_self = PASSGLASS
-
-/obj/structure/foamedmetal/resin/Initialize(mapload)
- . = ..()
- if(isopenturf(loc))
- var/turf/open/O = loc
- O.ClearWet()
- if(O.air)
- var/datum/gas_mixture/G = O.air
- G.temperature = 293.15
- for(var/obj/effect/hotspot/H in O)
- qdel(H)
- var/list/G_gases = G.gases
- for(var/I in G_gases)
- if(I == /datum/gas/oxygen || I == /datum/gas/nitrogen)
- continue
- G_gases[I][MOLES] = 0
- G.garbage_collect()
- for(var/obj/machinery/atmospherics/components/unary/U in O)
- if(!U.welded)
- U.welded = TRUE
- U.update_appearance()
- U.visible_message(span_danger("[U] sealed shut!"))
- for(var/mob/living/L in O)
- L.extinguish_mob()
- for(var/obj/item/Item in O)
- Item.extinguish()
-
-#undef ALUMINUM_FOAM
-#undef IRON_FOAM
-#undef RESIN_FOAM
diff --git a/code/game/objects/effects/effect_system/effects_smoke.dm b/code/game/objects/effects/effect_system/effects_smoke.dm
deleted file mode 100644
index 69609a41026..00000000000
--- a/code/game/objects/effects/effect_system/effects_smoke.dm
+++ /dev/null
@@ -1,381 +0,0 @@
-/////////////////////////////////////////////
-//// SMOKE SYSTEMS
-/////////////////////////////////////////////
-
-/obj/effect/particle_effect/smoke
- name = "smoke"
- icon = 'icons/effects/96x96.dmi'
- icon_state = "smoke"
- pixel_x = -32
- pixel_y = -32
- opacity = FALSE
- plane = ABOVE_GAME_PLANE
- layer = FLY_LAYER
- anchored = TRUE
- mouse_opacity = MOUSE_OPACITY_TRANSPARENT
- animate_movement = FALSE
- var/amount = 4
- var/lifetime = 5
- var/opaque = 1 //whether the smoke can block the view when in enough amount
-
-
-/obj/effect/particle_effect/smoke/proc/fade_out(frames = 16)
- if(alpha == 0) //Handle already transparent case
- return
- if(frames == 0)
- frames = 1 //We will just assume that by 0 frames, the coder meant "during one frame".
- var/step = alpha / frames
- for(var/i in 1 to frames)
- alpha -= step
- if(alpha < 160)
- set_opacity(0) //if we were blocking view, we aren't now because we're fading out
- stoplag()
-
-/obj/effect/particle_effect/smoke/Initialize(mapload)
- . = ..()
- create_reagents(1000)
- START_PROCESSING(SSobj, src)
-
-
-/obj/effect/particle_effect/smoke/Destroy()
- STOP_PROCESSING(SSobj, src)
- return ..()
-
-/obj/effect/particle_effect/smoke/proc/kill_smoke()
- STOP_PROCESSING(SSobj, src)
- INVOKE_ASYNC(src, .proc/fade_out)
- QDEL_IN(src, 10)
-
-/obj/effect/particle_effect/smoke/process()
- lifetime--
- if(lifetime < 1)
- kill_smoke()
- return FALSE
- for(var/mob/living/L in range(0,src))
- smoke_mob(L)
- return TRUE
-
-/obj/effect/particle_effect/smoke/proc/smoke_mob(mob/living/carbon/C)
- if(!istype(C))
- return FALSE
- if(lifetime<1)
- return FALSE
- if(C.internal != null || C.has_smoke_protection())
- return FALSE
- if(C.smoke_delay)
- return FALSE
- C.smoke_delay++
- addtimer(CALLBACK(src, .proc/remove_smoke_delay, C), 10)
- return TRUE
-
-/obj/effect/particle_effect/smoke/proc/remove_smoke_delay(mob/living/carbon/C)
- if(C)
- C.smoke_delay = 0
-
-/obj/effect/particle_effect/smoke/proc/spread_smoke()
- var/turf/t_loc = get_turf(src)
- if(!t_loc)
- return
- var/list/newsmokes = list()
- for(var/turf/T in t_loc.get_atmos_adjacent_turfs())
- var/obj/effect/particle_effect/smoke/foundsmoke = locate() in T //Don't spread smoke where there's already smoke!
- if(foundsmoke)
- continue
- for(var/mob/living/L in T)
- smoke_mob(L)
- var/obj/effect/particle_effect/smoke/S = new type(T)
- reagents.copy_to(S, reagents.total_volume)
- S.setDir(pick(GLOB.cardinals))
- S.amount = amount-1
- S.add_atom_colour(color, FIXED_COLOUR_PRIORITY)
- S.lifetime = lifetime
- if(S.amount>0)
- if(opaque)
- S.set_opacity(TRUE)
- newsmokes.Add(S)
-
- //the smoke spreads rapidly but not instantly
- for(var/obj/effect/particle_effect/smoke/SM in newsmokes)
- addtimer(CALLBACK(SM, /obj/effect/particle_effect/smoke.proc/spread_smoke), 1)
-
-
-/datum/effect_system/smoke_spread
- var/amount = 10
- effect_type = /obj/effect/particle_effect/smoke
-
-/datum/effect_system/smoke_spread/set_up(radius = 5, loca)
- if(isturf(loca))
- location = loca
- else
- location = get_turf(loca)
- amount = radius
-
-/datum/effect_system/smoke_spread/start()
- if(holder)
- location = get_turf(holder)
- var/obj/effect/particle_effect/smoke/S = new effect_type(location)
- S.amount = amount
- if(S.amount)
- S.spread_smoke()
-
-
-/////////////////////////////////////////////
-// Bad smoke
-/////////////////////////////////////////////
-
-/obj/effect/particle_effect/smoke/bad
- lifetime = 8
-
-/obj/effect/particle_effect/smoke/bad/Initialize(mapload)
- . = ..()
- var/static/list/loc_connections = list(
- COMSIG_ATOM_ENTERED = .proc/on_entered,
- )
- AddElement(/datum/element/connect_loc, loc_connections)
-
-/obj/effect/particle_effect/smoke/bad/smoke_mob(mob/living/carbon/M)
- . = ..()
- if(.)
- M.drop_all_held_items()
- M.adjustOxyLoss(1)
- M.emote("cough")
- return TRUE
-
-/obj/effect/particle_effect/smoke/bad/proc/on_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
- SIGNAL_HANDLER
- if(istype(arrived, /obj/projectile/beam))
- var/obj/projectile/beam/beam = arrived
- beam.damage *= 0.5
-
-/datum/effect_system/smoke_spread/bad
- effect_type = /obj/effect/particle_effect/smoke/bad
-
-/////////////////////////////////////////////
-// Nanofrost smoke
-/////////////////////////////////////////////
-
-/obj/effect/particle_effect/smoke/freezing
- name = "nanofrost smoke"
- color = "#B2FFFF"
- opaque = FALSE
-
-/datum/effect_system/smoke_spread/freezing
- effect_type = /obj/effect/particle_effect/smoke/freezing
- var/blast = 0
- var/temperature = 2
- var/weldvents = TRUE
- var/distcheck = TRUE
-
-/datum/effect_system/smoke_spread/freezing/proc/Chilled(atom/A)
- if(isopenturf(A))
- var/turf/open/T = A
- if(T.air)
- var/datum/gas_mixture/G = T.air
- if(!distcheck || get_dist(T, location) < blast) // Otherwise we'll get silliness like people using Nanofrost to kill people through walls with cold air
- G.temperature = temperature
- T.air_update_turf(FALSE, FALSE)
- for(var/obj/effect/hotspot/H in T)
- qdel(H)
- var/list/G_gases = G.gases
- if(G_gases[/datum/gas/plasma])
- G.assert_gas(/datum/gas/nitrogen)
- G_gases[/datum/gas/nitrogen][MOLES] += (G_gases[/datum/gas/plasma][MOLES])
- G_gases[/datum/gas/plasma][MOLES] = 0
- G.garbage_collect()
- if (weldvents)
- for(var/obj/machinery/atmospherics/components/unary/U in T)
- if(!isnull(U.welded) && !U.welded) //must be an unwelded vent pump or vent scrubber.
- U.welded = TRUE
- U.update_appearance()
- U.visible_message(span_danger("[U] is frozen shut!"))
- for(var/mob/living/L in T)
- L.extinguish_mob()
- for(var/obj/item/Item in T)
- Item.extinguish()
-
-/datum/effect_system/smoke_spread/freezing/set_up(radius = 5, loca, blast_radius = 0)
- ..()
- blast = blast_radius
-
-/datum/effect_system/smoke_spread/freezing/start()
- if(blast)
- for(var/turf/T in RANGE_TURFS(blast, location))
- Chilled(T)
- ..()
-
-/datum/effect_system/smoke_spread/freezing/decon
- temperature = 293.15
- distcheck = FALSE
- weldvents = FALSE
-
-
-/////////////////////////////////////////////
-// Sleep smoke
-/////////////////////////////////////////////
-
-/obj/effect/particle_effect/smoke/sleeping
- color = "#9C3636"
- lifetime = 10
-
-/obj/effect/particle_effect/smoke/sleeping/smoke_mob(mob/living/carbon/M)
- if(..())
- M.Sleeping(200)
- M.emote("cough")
- return 1
-
-/datum/effect_system/smoke_spread/sleeping
- effect_type = /obj/effect/particle_effect/smoke/sleeping
-
-/////////////////////////////////////////////
-// Chem smoke
-/////////////////////////////////////////////
-
-/obj/effect/particle_effect/smoke/chem
- lifetime = 10
-
-
-/obj/effect/particle_effect/smoke/chem/process()
- . = ..()
- if(.)
- var/turf/T = get_turf(src)
- var/fraction = 1/initial(lifetime)
- for(var/atom/movable/AM in T)
- if(AM.type == src.type)
- continue
- if(T.underfloor_accessibility < UNDERFLOOR_INTERACTABLE && HAS_TRAIT(AM, TRAIT_T_RAY_VISIBLE))
- continue
- reagents.expose(AM, TOUCH, fraction)
-
- reagents.expose(T, TOUCH, fraction)
- return TRUE
-
-/obj/effect/particle_effect/smoke/chem/smoke_mob(mob/living/carbon/M)
- if(lifetime<1)
- return FALSE
- if(!istype(M))
- return FALSE
- var/mob/living/carbon/C = M
- if(C.internal != null || C.has_smoke_protection())
- return FALSE
- var/fraction = 1/initial(lifetime)
- reagents.copy_to(C, fraction*reagents.total_volume)
- reagents.expose(M, INGEST, fraction)
- return TRUE
-
-
-
-/datum/effect_system/smoke_spread/chem
- /// Evil evil hack so we have something to "hold" our reagents
- var/atom/movable/chem_holder/chemholder
- effect_type = /obj/effect/particle_effect/smoke/chem
-
-/datum/effect_system/smoke_spread/chem/New()
- ..()
- chemholder = new()
- chemholder.create_reagents(1000, NO_REACT)
-
-/datum/effect_system/smoke_spread/chem/Destroy()
- QDEL_NULL(chemholder)
- return ..()
-
-/datum/effect_system/smoke_spread/chem/set_up(datum/reagents/carry = null, radius = 1, loca, silent = FALSE)
- if(isturf(loca))
- location = loca
- else
- location = get_turf(loca)
- amount = radius
- carry.copy_to(chemholder, carry.total_volume)
-
- if(!silent)
- var/contained = ""
- for(var/reagent in carry.reagent_list)
- contained += " [reagent] "
- if(contained)
- contained = "\[[contained]\]"
-
- var/where = "[AREACOORD(location)]"
- if(carry.my_atom?.fingerprintslast) //Some reagents don't have a my_atom in some cases
- var/mob/M = get_mob_by_key(carry.my_atom.fingerprintslast)
- var/more = ""
- if(M)
- more = "[ADMIN_LOOKUPFLW(M)] "
- if(!istype(carry.my_atom, /obj/machinery/plumbing))
- message_admins("Smoke: ([ADMIN_VERBOSEJMP(location)])[contained]. Key: [more ? more : carry.my_atom.fingerprintslast].")
- log_game("A chemical smoke reaction has taken place in ([where])[contained]. Last touched by [carry.my_atom.fingerprintslast].")
- else
- if(!istype(carry.my_atom, /obj/machinery/plumbing))
- message_admins("Smoke: ([ADMIN_VERBOSEJMP(location)])[contained]. No associated key.")
- log_game("A chemical smoke reaction has taken place in ([where])[contained]. No associated key.")
-
-
-/datum/effect_system/smoke_spread/chem/start()
- var/mixcolor = mix_color_from_reagents(chemholder.reagents.reagent_list)
- if(holder)
- location = get_turf(holder)
- var/obj/effect/particle_effect/smoke/chem/S = new effect_type(location)
-
- if(chemholder.reagents.total_volume > 1) // can't split 1 very well
- chemholder.reagents.copy_to(S, chemholder.reagents.total_volume)
-
- if(mixcolor)
- S.add_atom_colour(mixcolor, FIXED_COLOUR_PRIORITY) // give the smoke color, if it has any to begin with
- S.amount = amount
- if(S.amount)
- S.spread_smoke() //calling process right now so the smoke immediately attacks mobs.
-
-
-/////////////////////////////////////////////
-// Transparent smoke
-/////////////////////////////////////////////
-
-//Same as the base type, but the smoke produced is not opaque
-/datum/effect_system/smoke_spread/transparent
- effect_type = /obj/effect/particle_effect/smoke/transparent
-
-/obj/effect/particle_effect/smoke/transparent
- opaque = FALSE
-
-/proc/do_smoke(range=0, location=null, smoke_type=/obj/effect/particle_effect/smoke)
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.effect_type = smoke_type
- smoke.set_up(range, location)
- smoke.start()
-
-/////////////////////////////////////////////
-// Bad Smoke (But Green (and Black))
-/////////////////////////////////////////////
-
-/obj/effect/particle_effect/smoke/bad/green
- name = "green smoke"
- color = "#00FF00"
- opaque = FALSE
-
-/datum/effect_system/smoke_spread/bad/green
- effect_type = /obj/effect/particle_effect/smoke/bad/green
-
-/obj/effect/particle_effect/smoke/bad/black
- name = "black smoke"
- color = "#383838"
- opaque = FALSE
-
-/datum/effect_system/smoke_spread/bad/black
- effect_type = /obj/effect/particle_effect/smoke/bad/black
-
-/////////////////////////////////////////////
-// Quick smoke
-/////////////////////////////////////////////
-
-/obj/effect/particle_effect/smoke/quick
- lifetime = 1
- opaque = FALSE
-
-/datum/effect_system/smoke_spread/quick
- effect_type = /obj/effect/particle_effect/smoke/quick
-
-/obj/effect/particle_effect/smoke/chem/quick
- lifetime = 2 //under lifetime 1, this kills itself the first time it processes, not working. i hate smoke code
- opaque = FALSE
- alpha = 100
-
-/datum/effect_system/smoke_spread/chem/quick
- effect_type = /obj/effect/particle_effect/smoke/chem/quick
diff --git a/code/game/objects/effects/effect_system/fluid_spread/_fluid_spread.dm b/code/game/objects/effects/effect_system/fluid_spread/_fluid_spread.dm
new file mode 100644
index 00000000000..1e1df733396
--- /dev/null
+++ b/code/game/objects/effects/effect_system/fluid_spread/_fluid_spread.dm
@@ -0,0 +1,126 @@
+/////////////////////////////////////////////
+//// SMOKE SYSTEMS
+/////////////////////////////////////////////
+
+/**
+ * A group of fluid objects.
+ */
+/datum/fluid_group
+ /// The set of fluid objects currently in this group.
+ var/list/nodes
+ /// The number of fluid object that this group wants to have contained.
+ var/target_size
+ /// The total number of fluid objects that have ever been in this group.
+ var/total_size = 0
+
+/datum/fluid_group/New(target_size = 0)
+ . = ..()
+ src.nodes = list()
+ src.target_size = target_size
+
+/datum/fluid_group/Destroy(force)
+ QDEL_LAZYLIST(nodes)
+ return ..()
+
+/**
+ * Adds a fluid node to this fluid group.
+ *
+ * Is a noop if the node is already in the group.
+ * Removes the node from any other fluid groups it is in.
+ * Syncs the group of the node with the group it is being added to (this one).
+ * Increments the total size of the fluid group.
+ *
+ * Arguments:
+ * - [node][/obj/effect/particle_effect/fluid]: The fluid node that is going to be added to this group.
+ *
+ * Returns:
+ * - [TRUE]: If the node to be added is in this group by the end of the proc.
+ * - [FALSE]: Otherwise.
+ */
+/datum/fluid_group/proc/add_node(obj/effect/particle_effect/fluid/node)
+ if(!istype(node))
+ CRASH("Attempted to add non-fluid node [isnull(node) ? "NULL" : node] to a fluid group.")
+ if(QDELING(node))
+ CRASH("Attempted to add qdeling node to a fluid group")
+
+ if(node.group)
+ if(node.group == src)
+ return TRUE
+ if(!node.group.remove_node(node))
+ return FALSE
+
+ nodes += node
+ node.group = src
+ total_size++
+ return TRUE
+
+
+/**
+ * Removes a fluid node from this fluid group.
+ *
+ * Is a noop if the node is not in this group.
+ * Nulls the nodes fluid group ref to sync it with its new state.
+ * DOES NOT decrement the total size of the fluid group.
+ *
+ * Arguments:
+ * - [node][/obj/effect/particle_effect/fluid]: The fluid node that is going to be removed from this group.
+ *
+ * Returns:
+ * - [TRUE]: If the node to be removed is not in the group by the end of the proc.
+ */
+/datum/fluid_group/proc/remove_node(obj/effect/particle_effect/fluid/node)
+ if(node.group != src)
+ return TRUE
+
+ nodes -= node
+ node.group = null
+ return TRUE // Note: does not decrement total size since we don't want the group to expand again when it begins to dissipate or it will never stop.
+
+
+/**
+ * A particle effect that belongs to a fluid group.
+ */
+/obj/effect/particle_effect/fluid
+ name = "fluid"
+ /// The fluid group that this particle effect belongs to.
+ var/datum/fluid_group/group
+ /// What SSfluid bucket this particle effect is currently in.
+ var/tmp/effect_bucket
+ /// The index of the fluid spread bucket this is being spread in.
+ var/tmp/spread_bucket
+
+/obj/effect/particle_effect/fluid/Initialize(mapload, datum/fluid_group/group, obj/effect/particle_effect/fluid/source)
+ . = ..()
+ if(!group)
+ group = source?.group || new
+ group.add_node(src)
+
+/obj/effect/particle_effect/fluid/Destroy()
+ group.remove_node(src)
+ return ..()
+
+/**
+ * Attempts to spread this fluid node to wherever it can spread.
+ *
+ * Exact results vary by subtype implementation.
+ */
+/obj/effect/particle_effect/fluid/proc/spread()
+ CRASH("The base fluid spread proc is not implemented and should not be called. You called it.")
+
+
+/**
+ * A factory which produces fluid groups.
+ */
+/datum/effect_system/fluid_spread
+ effect_type = /obj/effect/particle_effect/fluid
+ /// The amount of smoke to produce.
+ var/amount = 10
+
+/datum/effect_system/fluid_spread/set_up(range = 1, amount = DIAMOND_AREA(range), atom/location, ...)
+ src.location = get_turf(location)
+ src.amount = amount
+
+/datum/effect_system/fluid_spread/start()
+ var/location = holder ? get_turf(holder) : src.location
+ var/obj/effect/particle_effect/fluid/flood = new effect_type(location, new /datum/fluid_group(amount))
+ flood.spread()
diff --git a/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm b/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm
new file mode 100644
index 00000000000..aa13d37e576
--- /dev/null
+++ b/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm
@@ -0,0 +1,429 @@
+/// The minimum foam range required to start diluting the reagents past the minimum dilution rate.
+#define MINIMUM_FOAM_DILUTION_RANGE 3
+/// The minumum foam-area based divisor used to decrease foam exposure volume.
+#define MINIMUM_FOAM_DILUTION DIAMOND_AREA(MINIMUM_FOAM_DILUTION_RANGE)
+/// The effective scaling of the reagents in the foam. (Total delivered at or below [MINIMUM_FOAM_DILUTION])
+#define FOAM_REAGENT_SCALE 3.2
+
+/**
+ * ## Foam
+ *
+ * Similar to smoke, but slower and mobs absorb its reagent through their exposed skin.
+ */
+/obj/effect/particle_effect/fluid/foam
+ name = "foam"
+ icon_state = "foam"
+ opacity = FALSE
+ anchored = TRUE
+ density = FALSE
+ layer = EDGED_TURF_LAYER
+ plane = GAME_PLANE_UPPER
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ animate_movement = NO_STEPS
+ /// The types of turfs that this foam cannot spread to.
+ var/static/list/blacklisted_turfs = typecacheof(list(
+ /turf/open/space/transit,
+ /turf/open/chasm,
+ /turf/open/lava,
+ ))
+ /// The typepath for what this foam leaves behind when it dissipates.
+ var/atom/movable/result_type = null
+ /// Whether or not this foam can produce a remnant movable if something of the same type is already on its turf.
+ var/allow_duplicate_results = TRUE
+ /// The amount of time this foam stick around for before it dissipates.
+ var/lifetime = 8 SECONDS
+ /// Whether or not this foam should be slippery.
+ var/slippery_foam = TRUE
+
+
+/obj/effect/particle_effect/fluid/foam/Initialize(mapload)
+ . = ..()
+ create_reagents(1000)
+ playsound(src, 'sound/effects/bubbles2.ogg', 80, TRUE, -3)
+ AddElement(/datum/element/atmos_sensitive, mapload)
+ SSfoam.start_processing(src)
+
+/obj/effect/particle_effect/fluid/foam/ComponentInitialize()
+ . = ..()
+ if(slippery_foam)
+ AddComponent(/datum/component/slippery, 100)
+
+/obj/effect/particle_effect/fluid/foam/Destroy()
+ SSfoam.stop_processing(src)
+ if (spread_bucket)
+ SSfoam.cancel_spread(src)
+ return ..()
+
+/**
+ * Makes the foam dissipate and create whatever remnants it must.
+ */
+/obj/effect/particle_effect/fluid/foam/proc/kill_foam()
+ SSfoam.stop_processing(src)
+ if (spread_bucket)
+ SSfoam.cancel_spread(src)
+ make_result()
+ flick("[icon_state]-disolve", src)
+ QDEL_IN(src, 0.5 SECONDS)
+
+/**
+ * Makes the foam leave behind something when it dissipates.
+ *
+ * Returns the thing the foam leaves behind for further modification by subtypes.
+ */
+/obj/effect/particle_effect/fluid/foam/proc/make_result()
+ if(isnull(result_type))
+ return null
+
+ var/atom/location = loc
+ return (!allow_duplicate_results && (locate(result_type) in location)) || (new result_type(location))
+
+/obj/effect/particle_effect/fluid/foam/process(delta_time)
+ var/ds_delta_time = delta_time SECONDS
+ lifetime -= ds_delta_time
+ if(lifetime <= 0)
+ kill_foam()
+ return
+
+ var/fraction = (ds_delta_time * MINIMUM_FOAM_DILUTION) / (initial(lifetime) * max(MINIMUM_FOAM_DILUTION, group.total_size))
+ var/turf/location = loc
+ for(var/obj/object in location)
+ if(object == src)
+ continue
+ if(location.underfloor_accessibility < UNDERFLOOR_INTERACTABLE && HAS_TRAIT(object, TRAIT_T_RAY_VISIBLE))
+ continue
+ reagents.expose(object, VAPOR, fraction)
+
+ var/hit = 0
+ for(var/mob/living/foamer in location)
+ hit += foam_mob(foamer, delta_time)
+ if(hit)
+ lifetime += ds_delta_time //this is so the decrease from mobs hit and the natural decrease don't cumulate.
+
+ reagents.expose(location, VAPOR, fraction)
+
+/**
+ * Applies the effect of this foam to a mob.
+ *
+ * Arguments:
+ * - [foaming][/mob/living]: The mob that this foam is acting on.
+ * - delta_time: The amount of time that this foam is acting on them over.
+ *
+ * Returns:
+ * - [TRUE]: If the foam was successfully applied to the mob. Used to scale how quickly foam dissipates according to the number of mobs it is applied to.
+ * - [FALSE]: Otherwise.
+ */
+/obj/effect/particle_effect/fluid/foam/proc/foam_mob(mob/living/foaming, delta_time)
+ if(lifetime <= 0)
+ return FALSE
+ if(!istype(foaming))
+ return FALSE
+
+ delta_time = min(delta_time SECONDS, lifetime)
+ var/fraction = (delta_time * MINIMUM_FOAM_DILUTION) / (initial(lifetime) * max(MINIMUM_FOAM_DILUTION, group.total_size))
+ reagents.expose(foaming, VAPOR, fraction)
+ lifetime -= delta_time
+ return TRUE
+
+/obj/effect/particle_effect/fluid/foam/spread(delta_time = 0.2 SECONDS)
+ if(group.total_size > group.target_size)
+ return
+ var/turf/location = get_turf(src)
+ if(!istype(location))
+ return FALSE
+
+ for(var/turf/spread_turf as anything in location.reachableAdjacentTurfs())
+ var/obj/effect/particle_effect/fluid/foam/foundfoam = locate() in spread_turf //Don't spread foam where there's already foam!
+ if(foundfoam)
+ continue
+ if(is_type_in_typecache(spread_turf, blacklisted_turfs))
+ continue
+
+ for(var/mob/living/foaming in spread_turf)
+ foam_mob(foaming, delta_time)
+
+ var/obj/effect/particle_effect/fluid/foam/spread_foam = new type(spread_turf, group, src)
+ reagents.copy_to(spread_foam, (reagents.total_volume))
+ spread_foam.add_atom_colour(color, FIXED_COLOUR_PRIORITY)
+ spread_foam.result_type = result_type
+ SSfoam.queue_spread(spread_foam)
+
+/obj/effect/particle_effect/fluid/foam/should_atmos_process(datum/gas_mixture/air, exposed_temperature)
+ return exposed_temperature > 475
+
+/obj/effect/particle_effect/fluid/foam/atmos_expose(datum/gas_mixture/air, exposed_temperature)
+ if(prob(max(0, exposed_temperature - 475))) //foam dissolves when heated
+ kill_foam()
+
+/// A factory for foam fluid floods.
+/datum/effect_system/fluid_spread/foam
+ effect_type = /obj/effect/particle_effect/fluid/foam
+ /// A container for all of the chemicals we distribute through the foam.
+ var/datum/reagents/chemholder
+ /// The amount that
+ var/reagent_scale = FOAM_REAGENT_SCALE
+ /// What type of thing the foam should leave behind when it dissipates.
+ var/atom/movable/result_type = null
+
+
+/datum/effect_system/fluid_spread/foam/New()
+ ..()
+ chemholder = new(1000, NO_REACT)
+
+/datum/effect_system/fluid_spread/foam/Destroy()
+ QDEL_NULL(chemholder)
+ return ..()
+
+/datum/effect_system/fluid_spread/foam/set_up(range = 1, amount = DIAMOND_AREA(range), atom/location = null, datum/reagents/carry = null, result_type = null)
+ . = ..()
+ carry?.copy_to(chemholder, carry.total_volume)
+ if(!isnull(result_type))
+ src.result_type = result_type
+
+/datum/effect_system/fluid_spread/foam/start()
+ var/obj/effect/particle_effect/fluid/foam/foam = new effect_type(location, new /datum/fluid_group(amount))
+ var/foamcolor = mix_color_from_reagents(chemholder.reagent_list)
+ if(reagent_scale > 1) // Make room in case we were created by a particularly stuffed payload.
+ foam.reagents.maximum_volume *= reagent_scale
+ chemholder.copy_to(foam, chemholder.total_volume, reagent_scale) // Foam has an amplifying effect on the reagents it is supplied with. This is balanced by the reagents being diluted as the area the foam covers increases.
+ foam.add_atom_colour(foamcolor, FIXED_COLOUR_PRIORITY)
+ if(!isnull(result_type))
+ foam.result_type = result_type
+ SSfoam.queue_spread(foam)
+
+
+// Long lasting foam
+/// A foam variant which lasts for an extended amount of time.
+/obj/effect/particle_effect/fluid/foam/long_life
+ lifetime = 30 SECONDS
+
+/// A factory which produces foam with an extended lifespan.
+/datum/effect_system/fluid_spread/foam/long
+ effect_type = /obj/effect/particle_effect/fluid/foam/long_life
+ reagent_scale = FOAM_REAGENT_SCALE * (30 / 8)
+
+
+// Firefighting foam
+/// A variant of foam which absorbs plasma in the air if there is a fire.
+/obj/effect/particle_effect/fluid/foam/firefighting
+ name = "firefighting foam"
+ lifetime = 20 //doesn't last as long as normal foam
+ result_type = /obj/effect/decal/cleanable/plasma
+ allow_duplicate_results = FALSE
+ slippery_foam = FALSE
+ /// The amount of plasma gas this foam has absorbed. To be deposited when the foam dissipates.
+ var/absorbed_plasma = 0
+
+/obj/effect/particle_effect/fluid/foam/firefighting/Initialize(mapload)
+ . = ..()
+ RemoveElement(/datum/element/atmos_sensitive)
+
+/obj/effect/particle_effect/fluid/foam/firefighting/process()
+ ..()
+
+ var/turf/open/location = loc
+ if(!istype(location))
+ return
+
+ var/obj/effect/hotspot/hotspot = locate() in location
+ if(!(hotspot && location.air))
+ return
+
+ QDEL_NULL(hotspot)
+ var/datum/gas_mixture/air = location.air
+ var/list/gases = air.gases
+ if (gases[/datum/gas/plasma])
+ var/scrub_amt = min(30, gases[/datum/gas/plasma][MOLES]) //Absorb some plasma
+ gases[/datum/gas/plasma][MOLES] -= scrub_amt
+ absorbed_plasma += scrub_amt
+ if (air.temperature > T20C)
+ air.temperature = max(air.temperature / 2, T20C)
+ air.garbage_collect()
+ location.air_update_turf(FALSE, FALSE)
+
+/obj/effect/particle_effect/fluid/foam/firefighting/make_result()
+ var/atom/movable/deposit = ..()
+ if(istype(deposit) && deposit.reagents && absorbed_plasma > 0)
+ deposit.reagents.add_reagent(/datum/reagent/stable_plasma, absorbed_plasma)
+ absorbed_plasma = 0
+ return deposit
+
+/obj/effect/particle_effect/fluid/foam/firefighting/foam_mob(mob/living/foaming, delta_time)
+ if(!istype(foaming))
+ return
+ foaming.adjust_wet_stacks(2)
+
+
+// Metal foam
+
+/// A foam variant which
+/obj/effect/particle_effect/fluid/foam/metal
+ name = "aluminium foam"
+ result_type = /obj/structure/foamedmetal
+ icon_state = "mfoam"
+ slippery_foam = FALSE
+
+/// A factory which produces aluminium metal foam.
+/datum/effect_system/fluid_spread/foam/metal
+ effect_type = /obj/effect/particle_effect/fluid/foam/metal
+
+/// FOAM STRUCTURE. Formed by metal foams. Dense and opaque, but easy to break
+/obj/structure/foamedmetal
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "metalfoam"
+ density = TRUE
+ opacity = TRUE // changed in New()
+ anchored = TRUE
+ layer = EDGED_TURF_LAYER
+ plane = GAME_PLANE_UPPER
+ resistance_flags = FIRE_PROOF | ACID_PROOF
+ name = "foamed metal"
+ desc = "A lightweight foamed metal wall that can be used as base to construct a wall."
+ gender = PLURAL
+ max_integrity = 20
+ can_atmos_pass = ATMOS_PASS_DENSITY
+ obj_flags = CAN_BE_HIT | BLOCK_Z_IN_DOWN | BLOCK_Z_IN_UP
+ ///Var used to prevent spamming of the construction sound
+ var/next_beep = 0
+
+/obj/structure/foamedmetal/Initialize(mapload)
+ . = ..()
+ air_update_turf(TRUE, TRUE)
+
+/obj/structure/foamedmetal/Destroy()
+ air_update_turf(TRUE, FALSE)
+ . = ..()
+
+/obj/structure/foamedmetal/Move()
+ var/turf/T = loc
+ . = ..()
+ move_update_air(T)
+
+/obj/structure/foamedmetal/attack_paw(mob/user, list/modifiers)
+ return attack_hand(user, modifiers)
+
+/obj/structure/foamedmetal/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0)
+ playsound(src.loc, 'sound/weapons/tap.ogg', 100, TRUE)
+
+/obj/structure/foamedmetal/attack_hand(mob/user, list/modifiers)
+ . = ..()
+ if(.)
+ return
+ user.changeNext_move(CLICK_CD_MELEE)
+ user.do_attack_animation(src, ATTACK_EFFECT_PUNCH)
+ to_chat(user, span_warning("You hit [src] but bounce off it!"))
+ playsound(src.loc, 'sound/weapons/tap.ogg', 100, TRUE)
+
+/obj/structure/foamedmetal/attackby(obj/item/W, mob/user, params)
+ ///A speed modifier for how fast the wall is build
+ var/platingmodifier = 1
+ if(HAS_TRAIT(user, TRAIT_QUICK_BUILD))
+ platingmodifier = 0.7
+ if(next_beep <= world.time)
+ next_beep = world.time + 1 SECONDS
+ playsound(src, 'sound/machines/clockcult/integration_cog_install.ogg', 50, TRUE)
+ add_fingerprint(user)
+
+ if(!istype(W, /obj/item/stack/sheet))
+ return ..()
+
+ var/obj/item/stack/sheet/sheet_for_plating = W
+ if(istype(sheet_for_plating, /obj/item/stack/sheet/iron))
+ if(sheet_for_plating.get_amount() < 2)
+ to_chat(user, span_warning("You need two sheets of iron to finish a wall on [src]!"))
+ return
+ to_chat(user, span_notice("You start adding plating to the foam structure..."))
+ if (do_after(user, 40 * platingmodifier, target = src))
+ if(!sheet_for_plating.use(2))
+ return
+ to_chat(user, span_notice("You add the plating."))
+ var/turf/T = get_turf(src)
+ T.PlaceOnTop(/turf/closed/wall/metal_foam_base)
+ transfer_fingerprints_to(T)
+ qdel(src)
+ return
+
+ add_hiddenprint(user)
+
+/// A metal foam variant which produces slightly sturdier walls.
+/obj/effect/particle_effect/fluid/foam/metal/iron
+ name = "iron foam"
+ result_type = /obj/structure/foamedmetal/iron
+
+/// A factory which produces iron metal foam.
+/datum/effect_system/fluid_spread/foam/metal/iron
+ effect_type = /obj/effect/particle_effect/fluid/foam/metal/iron
+
+/// A variant of metal foam walls with higher durability.
+/obj/structure/foamedmetal/iron
+ max_integrity = 50
+ icon_state = "ironfoam"
+
+/// A variant of metal foam which only produces walls at area boundaries.
+/obj/effect/particle_effect/fluid/foam/metal/smart
+ name = "smart foam"
+
+/// A factory which produces smart aluminium metal foam.
+/datum/effect_system/fluid_spread/foam/metal/smart
+ effect_type = /obj/effect/particle_effect/fluid/foam/metal/smart
+
+/obj/effect/particle_effect/fluid/foam/metal/smart/make_result() //Smart foam adheres to area borders for walls
+ var/turf/open/location = loc
+ if(isspaceturf(location))
+ location.PlaceOnTop(/turf/open/floor/plating/foam)
+
+ for(var/cardinal in GLOB.cardinals)
+ var/turf/cardinal_turf = get_step(location, cardinal)
+ if(get_area(cardinal_turf) != get_area(location))
+ return ..()
+ return null
+
+/datum/effect_system/fluid_spread/foam/metal/resin
+ effect_type = /obj/effect/particle_effect/fluid/foam/metal/resin
+
+/// A foam variant which produces atmos resin walls.
+/obj/effect/particle_effect/fluid/foam/metal/resin
+ name = "resin foam"
+ result_type = /obj/structure/foamedmetal/resin
+
+/// Atmos Backpack Resin, transparent, prevents atmos and filters the air
+/obj/structure/foamedmetal/resin
+ name = "\improper ATMOS Resin"
+ desc = "A lightweight, transparent resin used to suffocate fires, scrub the air of toxins, and restore the air to a safe temperature. It can be used as base to construct a wall."
+ opacity = FALSE
+ icon_state = "atmos_resin"
+ alpha = 120
+ max_integrity = 10
+ pass_flags_self = PASSGLASS
+
+/obj/structure/foamedmetal/resin/Initialize(mapload)
+ . = ..()
+ var/turf/open/location = loc
+ if(!istype(location))
+ return
+
+ location.ClearWet()
+ if(location.air)
+ var/datum/gas_mixture/air = location.air
+ air.temperature = 293.15
+ for(var/obj/effect/hotspot/fire in location)
+ qdel(fire)
+
+ var/list/gases = air.gases
+ for(var/gas_type in gases)
+ switch(gas_type)
+ if(/datum/gas/oxygen, /datum/gas/nitrogen)
+ continue
+ else
+ gases[gas_type][MOLES] = 0
+ air.garbage_collect()
+
+ for(var/obj/machinery/atmospherics/components/unary/comp in location)
+ if(!comp.welded)
+ comp.welded = TRUE
+ comp.update_appearance()
+ comp.visible_message(span_danger("[comp] sealed shut!"))
+
+ for(var/mob/living/potential_tinder in location)
+ potential_tinder.extinguish_mob()
+ for(var/obj/item/potential_tinder in location)
+ potential_tinder.extinguish()
diff --git a/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm b/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm
new file mode 100644
index 00000000000..320c2b5add1
--- /dev/null
+++ b/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm
@@ -0,0 +1,438 @@
+/**
+ * A fluid which spreads through the air affecting every mob it engulfs.
+ */
+/obj/effect/particle_effect/fluid/smoke
+ name = "smoke"
+ icon = 'icons/effects/96x96.dmi'
+ icon_state = "smoke"
+ pixel_x = -32
+ pixel_y = -32
+ opacity = TRUE
+ plane = ABOVE_GAME_PLANE
+ layer = FLY_LAYER
+ anchored = TRUE
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ animate_movement = FALSE
+ /// How long the smoke sticks around before it dissipates.
+ var/lifetime = 10 SECONDS
+
+/obj/effect/particle_effect/fluid/smoke/Initialize(mapload, datum/fluid_group/group, ...)
+ . = ..()
+ create_reagents(1000)
+ setDir(pick(GLOB.cardinals))
+ SSsmoke.start_processing(src)
+
+/obj/effect/particle_effect/fluid/smoke/Destroy()
+ SSsmoke.stop_processing(src)
+ if (spread_bucket)
+ SSsmoke.cancel_spread(src)
+ return ..()
+
+
+/**
+ * Makes the smoke fade out and then deletes it.
+ */
+/obj/effect/particle_effect/fluid/smoke/proc/kill_smoke()
+ SSsmoke.stop_processing(src)
+ if (spread_bucket)
+ SSsmoke.cancel_spread(src)
+ INVOKE_ASYNC(src, .proc/fade_out)
+ QDEL_IN(src, 1 SECONDS)
+
+/**
+ * Animates the smoke gradually fading out of visibility.
+ * Also makes the smoke turf transparent as it passes 160 alpha.
+ *
+ * Arguments:
+ * - frames = 0.8 [SECONDS]: The amount of time the smoke should fade out over.
+ */
+/obj/effect/particle_effect/fluid/smoke/proc/fade_out(frames = 0.8 SECONDS)
+ if(alpha == 0) //Handle already transparent case
+ if(opacity)
+ set_opacity(FALSE)
+ return
+
+ if(frames == 0)
+ set_opacity(FALSE)
+ alpha = 0
+ return
+
+ var/time_to_transparency = round(((alpha - 160) / alpha) * frames)
+ if(time_to_transparency >= 1)
+ addtimer(CALLBACK(src, /atom.proc/set_opacity, FALSE), time_to_transparency)
+ else
+ set_opacity(FALSE)
+ animate(src, time = frames, alpha = 0)
+
+
+/obj/effect/particle_effect/fluid/smoke/spread(delta_time = 0.1 SECONDS)
+ if(group.total_size > group.target_size)
+ return
+ var/turf/t_loc = get_turf(src)
+ if(!t_loc)
+ return
+
+ for(var/turf/spread_turf in t_loc.get_atmos_adjacent_turfs())
+ if(group.total_size > group.target_size)
+ break
+ if(locate(type) in spread_turf)
+ continue // Don't spread smoke where there's already smoke!
+ for(var/mob/living/smoker in spread_turf)
+ smoke_mob(smoker, delta_time)
+
+ var/obj/effect/particle_effect/fluid/smoke/spread_smoke = new type(spread_turf, group)
+ reagents.copy_to(spread_smoke, reagents.total_volume)
+ spread_smoke.add_atom_colour(color, FIXED_COLOUR_PRIORITY)
+ spread_smoke.lifetime = lifetime
+
+ // the smoke spreads rapidly, but not instantly
+ SSfoam.queue_spread(spread_smoke)
+
+
+/obj/effect/particle_effect/fluid/smoke/process(delta_time)
+ lifetime -= delta_time SECONDS
+ if(lifetime <= 0)
+ kill_smoke()
+ return FALSE
+ for(var/mob/living/smoker in loc) // In case smoke somehow winds up in a locker or something this should still behave sanely.
+ smoke_mob(smoker, delta_time)
+ return TRUE
+
+/**
+ * Handles the effects of this smoke on any mobs it comes into contact with.
+ *
+ * Arguments:
+ * - [smoker][/mob/living/carbon]: The mob that is being exposed to this smoke.
+ * - delta_time: A scaling factor for the effects this has. Primarily based off of tick rate to normalize effects to units of rate/sec.
+ *
+ * Returns whether the smoke effect was applied to the mob.
+ */
+/obj/effect/particle_effect/fluid/smoke/proc/smoke_mob(mob/living/carbon/smoker, delta_time)
+ if(!istype(smoker))
+ return FALSE
+ if(lifetime < 1)
+ return FALSE
+ if(smoker.internal != null || smoker.has_smoke_protection())
+ return FALSE
+ if(smoker.smoke_delay)
+ return FALSE
+
+ smoker.smoke_delay = TRUE
+ addtimer(VARSET_CALLBACK(smoker, smoke_delay, FALSE), 1 SECONDS)
+ return TRUE
+
+/// A factory which produces clouds of smoke.
+/datum/effect_system/fluid_spread/smoke
+ effect_type = /obj/effect/particle_effect/fluid/smoke
+
+/////////////////////////////////////////////
+// Transparent smoke
+/////////////////////////////////////////////
+
+/// Same as the base type, but the smoke produced is not opaque
+/datum/effect_system/fluid_spread/smoke/transparent
+ effect_type = /obj/effect/particle_effect/fluid/smoke/transparent
+
+/// Same as the base type, but is not opaque.
+/obj/effect/particle_effect/fluid/smoke/transparent
+ opacity = FALSE
+
+/**
+ * A helper proc used to spawn small puffs of smoke.
+ *
+ * Arguments:
+ * - range: The amount of smoke to produce as number of steps from origin covered.
+ * - amount: The amount of smoke to produce as the total desired coverage area. Autofilled from the range arg if not set.
+ * - location: Where to produce the smoke cloud.
+ * - smoke_type: The smoke typepath to spawn.
+ */
+/proc/do_smoke(range = 0, amount = DIAMOND_AREA(range), location = null, smoke_type = /obj/effect/particle_effect/fluid/smoke)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.effect_type = smoke_type
+ smoke.set_up(amount = amount, location = location)
+ smoke.start()
+
+/////////////////////////////////////////////
+// Quick smoke
+/////////////////////////////////////////////
+
+/// Smoke that dissipates as quickly as possible.
+/obj/effect/particle_effect/fluid/smoke/quick
+ lifetime = 1 SECONDS
+ opacity = FALSE
+
+/// A factory which produces smoke that dissipates as quickly as possible.
+/datum/effect_system/fluid_spread/smoke/quick
+ effect_type = /obj/effect/particle_effect/fluid/smoke/quick
+
+/////////////////////////////////////////////
+// Bad smoke
+/////////////////////////////////////////////
+
+/// Smoke that makes you cough and reduces the power of lasers.
+/obj/effect/particle_effect/fluid/smoke/bad
+ lifetime = 16 SECONDS
+
+/obj/effect/particle_effect/fluid/smoke/bad/Initialize(mapload)
+ . = ..()
+ var/static/list/loc_connections = list(
+ COMSIG_ATOM_ENTERED = .proc/on_entered,
+ )
+ AddElement(/datum/element/connect_loc, loc_connections)
+
+/obj/effect/particle_effect/fluid/smoke/bad/smoke_mob(mob/living/carbon/smoker)
+ . = ..()
+ if(!.)
+ return
+
+ smoker.drop_all_held_items()
+ smoker.adjustOxyLoss(1)
+ smoker.emote("cough")
+
+/**
+ * Reduces the power of any beam projectile that passes through the smoke.
+ *
+ * Arguments:
+ * - [source][/datum]: The location that has just been entered. If [/datum/element/connect_loc] is working this is [src.loc][/atom/var/loc].
+ * - [arrived][/atom/movable]: The atom that has just entered the source location.
+ * - [old_loc][/atom]: The location the entering atom just was in.
+ * - [old_locs][/list/atom]: The set of locations the entering atom was just in.
+ */
+/obj/effect/particle_effect/fluid/smoke/bad/proc/on_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+ SIGNAL_HANDLER
+ if(istype(arrived, /obj/projectile/beam))
+ var/obj/projectile/beam/beam = arrived
+ beam.damage *= 0.5
+
+/// A factory which produces smoke that makes you cough.
+/datum/effect_system/fluid_spread/smoke/bad
+ effect_type = /obj/effect/particle_effect/fluid/smoke/bad
+
+/////////////////////////////////////////////
+// Bad Smoke (But Green (and Black))
+/////////////////////////////////////////////
+
+/// Green smoke that makes you cough.
+/obj/effect/particle_effect/fluid/smoke/bad/green
+ name = "green smoke"
+ color = "#00FF00"
+ opacity = FALSE
+
+/// A factory which produces green smoke that makes you cough.
+/datum/effect_system/fluid_spread/smoke/bad/green
+ effect_type = /obj/effect/particle_effect/fluid/smoke/bad/green
+
+/// Black smoke that makes you cough. (Actually dark grey)
+/obj/effect/particle_effect/fluid/smoke/bad/black
+ name = "black smoke"
+ color = "#383838"
+ opacity = FALSE
+
+/// A factory which produces black smoke that makes you cough.
+/datum/effect_system/fluid_spread/smoke/bad/black
+ effect_type = /obj/effect/particle_effect/fluid/smoke/bad/black
+
+/////////////////////////////////////////////
+// Nanofrost smoke
+/////////////////////////////////////////////
+
+/// Light blue, transparent smoke which is usually paired with a blast that chills every turf in the area.
+/obj/effect/particle_effect/fluid/smoke/freezing
+ name = "nanofrost smoke"
+ color = "#B2FFFF"
+ opacity = FALSE
+
+/// A factory which produces light blue, transparent smoke and a blast that chills every turf in the area.
+/datum/effect_system/fluid_spread/smoke/freezing
+ effect_type = /obj/effect/particle_effect/fluid/smoke/freezing
+ /// The radius in which to chill every open turf.
+ var/blast = 0
+ /// The temperature to set the turfs air temperature to.
+ var/temperature = 2
+ /// Whether to weld every vent and air scrubber in the affected area shut.
+ var/weldvents = TRUE
+ /// Whether to make sure each affected turf is actually within range before cooling it.
+ var/distcheck = TRUE
+
+/**
+ * Chills an open turf.
+ *
+ * Forces the air temperature to a specific value.
+ * Transmutes all of the plasma in the air into nitrogen.
+ * Extinguishes all fires and burning objects/mobs in the turf.
+ * May freeze all vents and vent scrubbers shut.
+ *
+ * Arguments:
+ * - [chilly][/turf/open]: The open turf to chill
+ */
+/datum/effect_system/fluid_spread/smoke/freezing/proc/Chilled(turf/open/chilly)
+ if(!istype(chilly))
+ return
+
+ if(chilly.air)
+ var/datum/gas_mixture/air = chilly.air
+ if(!distcheck || get_dist(location, chilly) < blast) // Otherwise we'll get silliness like people using Nanofrost to kill people through walls with cold air
+ air.temperature = temperature
+
+ var/list/gases = air.gases
+ if(gases[/datum/gas/plasma])
+ air.assert_gas(/datum/gas/nitrogen)
+ gases[/datum/gas/nitrogen][MOLES] += gases[/datum/gas/plasma][MOLES]
+ gases[/datum/gas/plasma][MOLES] = 0
+ air.garbage_collect()
+
+ for(var/obj/effect/hotspot/fire in chilly)
+ qdel(fire)
+ chilly.air_update_turf(FALSE, FALSE)
+
+ if(weldvents)
+ for(var/obj/machinery/atmospherics/components/unary/comp in chilly)
+ if(!isnull(comp.welded) && !comp.welded) //must be an unwelded vent pump or vent scrubber.
+ comp.welded = TRUE
+ comp.update_appearance()
+ comp.visible_message(span_danger("[comp] is frozen shut!"))
+
+ // Extinguishes everything in the turf
+ for(var/mob/living/potential_tinder in chilly)
+ potential_tinder.extinguish_mob()
+ for(var/obj/item/potential_tinder in chilly)
+ potential_tinder.extinguish()
+
+/datum/effect_system/fluid_spread/smoke/freezing/set_up(range = 5, amount = DIAMOND_AREA(range), atom/location, blast_radius = 0)
+ . = ..()
+ blast = blast_radius
+
+/datum/effect_system/fluid_spread/smoke/freezing/start()
+ if(blast)
+ for(var/turf/T in RANGE_TURFS(blast, location))
+ Chilled(T)
+ return ..()
+
+/// A variant of the base freezing smoke formerly used by the vent decontamination event.
+/datum/effect_system/fluid_spread/smoke/freezing/decon
+ temperature = 293.15
+ distcheck = FALSE
+ weldvents = FALSE
+
+
+/////////////////////////////////////////////
+// Sleep smoke
+/////////////////////////////////////////////
+
+/// Smoke which knocks you out if you breathe it in.
+/obj/effect/particle_effect/fluid/smoke/sleeping
+ color = "#9C3636"
+ lifetime = 20 SECONDS
+
+/obj/effect/particle_effect/fluid/smoke/sleeping/smoke_mob(mob/living/carbon/smoker, delta_time)
+ if(..())
+ smoker.Sleeping(20 SECONDS)
+ smoker.emote("cough")
+ return TRUE
+
+/// A factory which produces sleeping smoke.
+/datum/effect_system/fluid_spread/smoke/sleeping
+ effect_type = /obj/effect/particle_effect/fluid/smoke/sleeping
+
+/////////////////////////////////////////////
+// Chem smoke
+/////////////////////////////////////////////
+
+/**
+ * Smoke which contains reagents which it applies to everything it comes into contact with.
+ */
+/obj/effect/particle_effect/fluid/smoke/chem
+ lifetime = 20 SECONDS
+
+/obj/effect/particle_effect/fluid/smoke/chem/process(delta_time)
+ . = ..()
+ if(!.)
+ return
+
+ var/turf/location = get_turf(src)
+ var/fraction = (delta_time SECONDS) / initial(lifetime)
+ for(var/atom/movable/thing as anything in location)
+ if(thing == src)
+ continue
+ if(location.underfloor_accessibility < UNDERFLOOR_INTERACTABLE && HAS_TRAIT(thing, TRAIT_T_RAY_VISIBLE))
+ continue
+ reagents.expose(thing, TOUCH, fraction)
+
+ reagents.expose(location, TOUCH, fraction)
+ return TRUE
+
+/obj/effect/particle_effect/fluid/smoke/chem/smoke_mob(mob/living/carbon/smoker, delta_time)
+ if(lifetime < 1)
+ return FALSE
+ if(!istype(smoker))
+ return FALSE
+ if(smoker.internal != null || smoker.has_smoke_protection())
+ return FALSE
+
+ var/fraction = (delta_time SECONDS) / initial(lifetime)
+ reagents.copy_to(smoker, reagents.total_volume, fraction)
+ reagents.expose(smoker, INGEST, fraction)
+ return TRUE
+
+
+/// A factory which produces clouds of chemical bearing smoke.
+/datum/effect_system/fluid_spread/smoke/chem
+ /// Evil evil hack so we have something to "hold" our reagents
+ var/datum/reagents/chemholder
+ effect_type = /obj/effect/particle_effect/fluid/smoke/chem
+
+/datum/effect_system/fluid_spread/smoke/chem/New()
+ ..()
+ chemholder = new(1000, NO_REACT)
+
+/datum/effect_system/fluid_spread/smoke/chem/Destroy()
+ QDEL_NULL(chemholder)
+ return ..()
+
+
+/datum/effect_system/fluid_spread/smoke/chem/set_up(range = 1, amount = DIAMOND_AREA(range), atom/location = null, datum/reagents/carry = null, silent = FALSE)
+ . = ..()
+ carry?.copy_to(chemholder, carry.total_volume)
+
+ if(silent)
+ return
+
+ var/list/contained_reagents = list()
+ for(var/datum/reagent/reagent as anything in chemholder.reagent_list)
+ contained_reagents += "[reagent.volume]u [reagent]"
+
+ var/where = "[AREACOORD(location)]"
+ var/contained = length(contained_reagents) ? "[contained_reagents.Join(", ", " \[", "\]")] @ [chemholder.chem_temp]K" : null
+ if(carry.my_atom?.fingerprintslast) //Some reagents don't have a my_atom in some cases
+ var/mob/M = get_mob_by_key(carry.my_atom.fingerprintslast)
+ var/more = ""
+ if(M)
+ more = "[ADMIN_LOOKUPFLW(M)] "
+ if(!istype(carry.my_atom, /obj/machinery/plumbing))
+ message_admins("Smoke: ([ADMIN_VERBOSEJMP(location)])[contained]. Key: [more ? more : carry.my_atom.fingerprintslast].")
+ log_game("A chemical smoke reaction has taken place in ([where])[contained]. Last touched by [carry.my_atom.fingerprintslast].")
+ else
+ if(!istype(carry.my_atom, /obj/machinery/plumbing))
+ message_admins("Smoke: ([ADMIN_VERBOSEJMP(location)])[contained]. No associated key.")
+ log_game("A chemical smoke reaction has taken place in ([where])[contained]. No associated key.")
+
+/datum/effect_system/fluid_spread/smoke/chem/start()
+ var/start_loc = holder ? get_turf(holder) : src.location
+ var/mixcolor = mix_color_from_reagents(chemholder.reagent_list)
+ var/obj/effect/particle_effect/fluid/smoke/chem/smoke = new effect_type(start_loc, new /datum/fluid_group(amount))
+ chemholder.copy_to(smoke, chemholder.total_volume)
+
+ if(mixcolor)
+ smoke.add_atom_colour(mixcolor, FIXED_COLOUR_PRIORITY) // give the smoke color, if it has any to begin with
+ smoke.spread() // Making the smoke spread immediately.
+
+/**
+ * A version of chemical smoke with a very short lifespan.
+ */
+/obj/effect/particle_effect/fluid/smoke/chem/quick
+ lifetime = 4 SECONDS
+ opacity = FALSE
+ alpha = 100
+
+/datum/effect_system/fluid_spread/smoke/chem/quick
+ effect_type = /obj/effect/particle_effect/fluid/smoke/chem/quick
diff --git a/code/game/objects/effects/mines.dm b/code/game/objects/effects/mines.dm
index 0faa180d74f..5221955196a 100644
--- a/code/game/objects/effects/mines.dm
+++ b/code/game/objects/effects/mines.dm
@@ -205,6 +205,6 @@
playsound(loc, 'sound/machines/chime.ogg', 30, FALSE, -3)
var/obj/effect/mine/new_mine = new mine_type(get_turf(src))
visible_message(span_danger("\The [src] releases a puff of smoke, revealing \a [new_mine]!"))
- var/obj/effect/particle_effect/smoke/poof = new (get_turf(src))
+ var/obj/effect/particle_effect/fluid/smoke/poof = new (get_turf(src))
poof.lifetime = 3
qdel(src)
diff --git a/code/game/objects/effects/step_triggers.dm b/code/game/objects/effects/step_triggers.dm
index e1f83b002b9..821cba8c495 100644
--- a/code/game/objects/effects/step_triggers.dm
+++ b/code/game/objects/effects/step_triggers.dm
@@ -163,12 +163,12 @@
s.start()
if(entersmoke)
- var/datum/effect_system/smoke_spread/s = new
- s.set_up(4, 1, src, 0)
+ var/datum/effect_system/fluid_spread/smoke/s = new
+ s.set_up(4, location = src)
s.start()
if(exitsmoke)
- var/datum/effect_system/smoke_spread/s = new
- s.set_up(4, 1, dest, 0)
+ var/datum/effect_system/fluid_spread/smoke/s = new
+ s.set_up(4, location = dest)
s.start()
uses--
diff --git a/code/game/objects/items/cigs_lighters.dm b/code/game/objects/items/cigs_lighters.dm
index 10b19f187ca..f5745958ecb 100644
--- a/code/game/objects/items/cigs_lighters.dm
+++ b/code/game/objects/items/cigs_lighters.dm
@@ -1055,9 +1055,9 @@ CIGARETTE PACKETS ARE IN FANCY.DM
//Time to start puffing those fat vapes, yo.
COOLDOWN_START(src, drag_cooldown, dragtime)
if(obj_flags & EMAGGED)
- var/datum/effect_system/smoke_spread/chem/smoke_machine/s = new
- s.set_up(reagents, 4, 24, loc)
- s.start()
+ var/datum/effect_system/fluid_spread/smoke/chem/smoke_machine/puff = new
+ puff.set_up(4, location = loc, carry = reagents, efficiency = 24)
+ puff.start()
if(prob(5)) //small chance for the vape to break and deal damage if it's emagged
playsound(get_turf(src), 'sound/effects/pop_expl.ogg', 50, FALSE)
M.apply_damage(20, BURN, BODY_ZONE_HEAD)
@@ -1069,8 +1069,8 @@ CIGARETTE PACKETS ARE IN FANCY.DM
qdel(src)
return
else if(super)
- var/datum/effect_system/smoke_spread/chem/smoke_machine/s = new
- s.set_up(reagents, 1, 24, loc)
- s.start()
+ var/datum/effect_system/fluid_spread/smoke/chem/smoke_machine/puff = new
+ puff.set_up(1, location = loc, carry = reagents, efficiency = 24)
+ puff.start()
handle_reagents()
diff --git a/code/game/objects/items/clown_items.dm b/code/game/objects/items/clown_items.dm
index 4d1290eee0e..a87d15d3af0 100644
--- a/code/game/objects/items/clown_items.dm
+++ b/code/game/objects/items/clown_items.dm
@@ -100,7 +100,7 @@
/obj/item/soap/suicide_act(mob/user)
user.say(";FFFFFFFFFFFFFFFFUUUUUUUDGE!!", forced="soap suicide")
user.visible_message(span_suicide("[user] lifts [src] to [user.p_their()] mouth and gnaws on it furiously, producing a thick froth! [user.p_they(TRUE)]'ll never get that BB gun now!"))
- new /obj/effect/particle_effect/foam(loc)
+ new /obj/effect/particle_effect/fluid/foam(loc)
return (TOXLOSS)
/**
diff --git a/code/game/objects/items/food/burgers.dm b/code/game/objects/items/food/burgers.dm
index 90fc6dee78d..363b74027d1 100644
--- a/code/game/objects/items/food/burgers.dm
+++ b/code/game/objects/items/food/burgers.dm
@@ -19,7 +19,7 @@
/obj/item/food/burger/plain/Initialize(mapload)
. = ..()
if(prob(1))
- new/obj/effect/particle_effect/smoke(get_turf(src))
+ new/obj/effect/particle_effect/fluid/smoke(get_turf(src))
playsound(src, 'sound/effects/smoke.ogg', 50, TRUE)
visible_message(span_warning("Oh, ye gods! [src] is ruined! But what if...?"))
name = "steamed ham"
@@ -428,8 +428,8 @@
/obj/item/food/burger/crazy/process(delta_time) // DIT EES HORRIBLE
if(DT_PROB(2.5, delta_time))
- var/datum/effect_system/smoke_spread/bad/green/smoke = new
- smoke.set_up(0, src)
+ var/datum/effect_system/fluid_spread/smoke/bad/green/smoke = new
+ smoke.set_up(0, location = src)
smoke.start()
// empty burger you can customize
diff --git a/code/game/objects/items/grenades/smokebomb.dm b/code/game/objects/items/grenades/smokebomb.dm
index 3184c0e9d35..6bb7ad0b19d 100644
--- a/code/game/objects/items/grenades/smokebomb.dm
+++ b/code/game/objects/items/grenades/smokebomb.dm
@@ -26,8 +26,8 @@
update_mob()
playsound(src, 'sound/effects/smoke.ogg', 50, TRUE, -3)
- var/datum/effect_system/smoke_spread/bad/smoke = new
- smoke.set_up(4, src)
+ var/datum/effect_system/fluid_spread/smoke/bad/smoke = new
+ smoke.set_up(4, location = src)
smoke.start()
qdel(smoke) //And deleted again. Sad really.
for(var/obj/structure/blob/blob in view(8, src))
diff --git a/code/game/objects/items/nitrium_crystals.dm b/code/game/objects/items/nitrium_crystals.dm
index b81a96bca18..f40961e1326 100644
--- a/code/game/objects/items/nitrium_crystals.dm
+++ b/code/game/objects/items/nitrium_crystals.dm
@@ -7,12 +7,12 @@
/obj/item/nitrium_crystal/attack_self(mob/user)
. = ..()
- var/datum/effect_system/smoke_spread/chem/smoke = new
+ var/datum/effect_system/fluid_spread/smoke/chem/smoke = new
var/turf/location = get_turf(src)
create_reagents(5)
reagents.add_reagent(/datum/reagent/nitrium_low_metabolization, 3)
reagents.add_reagent(/datum/reagent/nitrium_high_metabolization, 2)
smoke.attach(location)
- smoke.set_up(reagents, cloud_size, location, silent = TRUE)
+ smoke.set_up(cloud_size, location = location, carry = reagents, silent = TRUE)
smoke.start()
qdel(src)
diff --git a/code/game/objects/items/robot/robot_upgrades.dm b/code/game/objects/items/robot/robot_upgrades.dm
index 9fe29d87639..90b70e89f8f 100644
--- a/code/game/objects/items/robot/robot_upgrades.dm
+++ b/code/game/objects/items/robot/robot_upgrades.dm
@@ -562,8 +562,8 @@
var/prev_lockcharge = R.lockcharge
R.SetLockdown(TRUE)
R.set_anchored(TRUE)
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(1, R.loc)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(1, location = R.loc)
smoke.start()
sleep(2)
for(var/i in 1 to 4)
diff --git a/code/game/objects/items/scrolls.dm b/code/game/objects/items/scrolls.dm
index c55f4def71c..108cb8c0e0c 100644
--- a/code/game/objects/items/scrolls.dm
+++ b/code/game/objects/items/scrolls.dm
@@ -50,8 +50,8 @@
return
var/area/thearea = GLOB.teleportlocs[jump_target]
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(2, user.loc)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(2, location = user.loc)
smoke.attach(user)
smoke.start()
var/list/possible_locations = list()
diff --git a/code/game/objects/items/tanks/watertank.dm b/code/game/objects/items/tanks/watertank.dm
index abd1e7a9a0c..29cf2d3a80d 100644
--- a/code/game/objects/items/tanks/watertank.dm
+++ b/code/game/objects/items/tanks/watertank.dm
@@ -324,12 +324,12 @@
balloon_alert(user, "too far!")
return
for(var/S in target)
- if(istype(S, /obj/effect/particle_effect/foam/metal/resin) || istype(S, /obj/structure/foamedmetal/resin))
+ if(istype(S, /obj/effect/particle_effect/fluid/foam/metal/resin) || istype(S, /obj/structure/foamedmetal/resin))
balloon_alert(user, "already has resin!")
return
if(metal_synthesis_cooldown < 5)
- var/obj/effect/particle_effect/foam/metal/resin/F = new (get_turf(target))
- F.amount = 0
+ var/obj/effect/particle_effect/fluid/foam/metal/resin/foam = new (get_turf(target))
+ foam.group.target_size = 0
metal_synthesis_cooldown++
addtimer(CALLBACK(src, .proc/reduce_metal_synth_cooldown), 10 SECONDS)
else
@@ -363,8 +363,9 @@
anchored = TRUE
/obj/effect/resin_container/proc/Smoke()
- var/obj/effect/particle_effect/foam/metal/resin/S = new /obj/effect/particle_effect/foam/metal/resin(get_turf(loc))
- S.amount = 4
+ var/datum/effect_system/fluid_spread/foam/metal/resin/foaming = new
+ foaming.set_up(4, location = src)
+ foaming.start()
playsound(src,'sound/effects/bamf.ogg',100,TRUE)
qdel(src)
diff --git a/code/game/objects/structures/hivebot.dm b/code/game/objects/structures/hivebot.dm
index 493c7eabca0..c86748137e3 100644
--- a/code/game/objects/structures/hivebot.dm
+++ b/code/game/objects/structures/hivebot.dm
@@ -10,8 +10,8 @@
/obj/structure/hivebot_beacon/Initialize(mapload)
. = ..()
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(2, loc)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(2, location = loc)
smoke.start()
visible_message(span_boldannounce("[src] warps in!"))
playsound(src.loc, 'sound/effects/empulse.ogg', 25, TRUE)
diff --git a/code/game/objects/structures/lavaland/geyser.dm b/code/game/objects/structures/lavaland/geyser.dm
index bfa9bcd80d0..cba7f2b01b9 100644
--- a/code/game/objects/structures/lavaland/geyser.dm
+++ b/code/game/objects/structures/lavaland/geyser.dm
@@ -40,7 +40,7 @@
activated = TRUE
create_reagents(max_volume, DRAINABLE)
reagents.add_reagent(reagent_id, start_volume)
- START_PROCESSING(SSfluids, src) //It's main function is to be plumbed, so use SSfluids
+ START_PROCESSING(SSplumbing, src) //It's main function is to be plumbed, so use SSplumbing
if(erupting_state)
icon_state = erupting_state
else
diff --git a/code/game/objects/structures/watercloset.dm b/code/game/objects/structures/watercloset.dm
index 32897631252..b615dc9874f 100644
--- a/code/game/objects/structures/watercloset.dm
+++ b/code/game/objects/structures/watercloset.dm
@@ -437,7 +437,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/urinal, 32)
/obj/structure/sink/proc/begin_reclamation()
if(!reclaiming)
reclaiming = TRUE
- START_PROCESSING(SSfluids, src)
+ START_PROCESSING(SSplumbing, src)
/obj/structure/sink/kitchen
name = "kitchen sink"
diff --git a/code/modules/antagonists/_common/antag_spawner.dm b/code/modules/antagonists/_common/antag_spawner.dm
index eedc2314789..b01a8551534 100644
--- a/code/modules/antagonists/_common/antag_spawner.dm
+++ b/code/modules/antagonists/_common/antag_spawner.dm
@@ -67,7 +67,7 @@
spawn_antag(student.client, get_turf(src), apprentice_school, teacher.mind)
/obj/item/antag_spawner/contract/spawn_antag(client/C, turf/T, kind, datum/mind/user)
- new /obj/effect/particle_effect/smoke(T)
+ new /obj/effect/particle_effect/fluid/smoke(T)
var/mob/living/carbon/human/M = new/mob/living/carbon/human(T)
C.prefs.safe_transfer_prefs_to(M, is_antag = TRUE)
M.key = C.key
diff --git a/code/modules/antagonists/blob/blob_mobs.dm b/code/modules/antagonists/blob/blob_mobs.dm
index 457da47373f..44adf6483cc 100644
--- a/code/modules/antagonists/blob/blob_mobs.dm
+++ b/code/modules/antagonists/blob/blob_mobs.dm
@@ -198,7 +198,7 @@
/mob/living/simple_animal/hostile/blob/blobspore/death(gibbed)
// On death, create a small smoke of harmful gas (s-Acid)
- var/datum/effect_system/smoke_spread/chem/S = new
+ var/datum/effect_system/fluid_spread/smoke/chem/spores = new
var/turf/location = get_turf(src)
// Create the reagents to put into the air
@@ -212,9 +212,9 @@
reagents.add_reagent(/datum/reagent/toxin/spore, 10)
// Attach the smoke spreader and setup/start it.
- S.attach(location)
- S.set_up(reagents, death_cloud_size, location, silent = TRUE)
- S.start()
+ spores.attach(location)
+ spores.set_up(death_cloud_size, location = location, carry = reagents, silent = TRUE)
+ spores.start()
if(factory)
factory.spore_delay = world.time + factory.spore_cooldown //put the factory on cooldown
diff --git a/code/modules/antagonists/nukeop/equipment/nuclearbomb.dm b/code/modules/antagonists/nukeop/equipment/nuclearbomb.dm
index a41eacdc3e3..9a44528380f 100644
--- a/code/modules/antagonists/nukeop/equipment/nuclearbomb.dm
+++ b/code/modules/antagonists/nukeop/equipment/nuclearbomb.dm
@@ -589,8 +589,8 @@ GLOBAL_VAR(station_nuke_source)
R.my_atom = src
R.add_reagent(/datum/reagent/consumable/ethanol/beer, 100)
- var/datum/effect_system/foam_spread/foam = new
- foam.set_up(200, get_turf(src), R)
+ var/datum/effect_system/fluid_spread/foam/foam = new
+ foam.set_up(200, location = get_turf(src), carry = R)
foam.start()
disarm()
@@ -605,7 +605,7 @@ GLOBAL_VAR(station_nuke_source)
var/datum/reagents/beer = new /datum/reagents(1000)
beer.my_atom = vent
beer.add_reagent(/datum/reagent/consumable/ethanol/beer, 100)
- beer.create_foam(/datum/effect_system/foam_spread, 200)
+ beer.create_foam(/datum/effect_system/fluid_spread/foam, DIAMOND_AREA(10))
CHECK_TICK
diff --git a/code/modules/awaymissions/mission_code/Academy.dm b/code/modules/awaymissions/mission_code/Academy.dm
index cd32ccec532..fb303cd5aeb 100644
--- a/code/modules/awaymissions/mission_code/Academy.dm
+++ b/code/modules/awaymissions/mission_code/Academy.dm
@@ -284,7 +284,7 @@
//Cookie
T.visible_message(span_userdanger("A cookie appears out of thin air!"))
var/obj/item/food/cookie/C = new(drop_location())
- do_smoke(0, drop_location())
+ do_smoke(DIAMOND_AREA(0), drop_location())
C.name = "Cookie of Fate"
if(12)
//Healing
@@ -305,18 +305,18 @@
if(14)
//Free Gun
T.visible_message(span_userdanger("An impressive gun appears!"))
- do_smoke(0, drop_location())
+ do_smoke(DIAMOND_AREA(0), drop_location())
new /obj/item/gun/ballistic/revolver/mateba(drop_location())
if(15)
//Random One-use spellbook
T.visible_message(span_userdanger("A magical looking book drops to the floor!"))
- do_smoke(0, drop_location())
+ do_smoke(DIAMOND_AREA(0), drop_location())
new /obj/item/book/granter/spell/random(drop_location())
if(16)
//Servant & Servant Summon
T.visible_message(span_userdanger("A Dice Servant appears in a cloud of smoke!"))
var/mob/living/carbon/human/H = new(drop_location())
- do_smoke(0, drop_location())
+ do_smoke(DIAMOND_AREA(0), drop_location())
H.equipOutfit(/datum/outfit/butler)
var/datum/mind/servant_mind = new /datum/mind()
@@ -339,12 +339,12 @@
//Tator Kit
T.visible_message(span_userdanger("A suspicious box appears!"))
new /obj/item/storage/box/syndicate/bundle_a(drop_location())
- do_smoke(0, drop_location())
+ do_smoke(DIAMOND_AREA(0), drop_location())
if(18)
//Captain ID
T.visible_message(span_userdanger("A golden identification card appears!"))
new /obj/item/card/id/advanced/gold/captains_spare(drop_location())
- do_smoke(0, drop_location())
+ do_smoke(DIAMOND_AREA(0), drop_location())
if(19)
//Instrinct Resistance
T.visible_message(span_userdanger("[user] looks very robust!"))
diff --git a/code/modules/awaymissions/super_secret_room.dm b/code/modules/awaymissions/super_secret_room.dm
index af9ee82a6ca..e04a268f6bc 100644
--- a/code/modules/awaymissions/super_secret_room.dm
+++ b/code/modules/awaymissions/super_secret_room.dm
@@ -29,8 +29,8 @@
SpeakPeace(list("Welcome to the error handling room.","Something's goofed up bad to send you here.","You should probably tell an admin what you were doing, or make a bug report."))
for(var/obj/structure/signpost/salvation/S in orange(7))
S.invisibility = 0
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(1, S.loc)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(1, location = S.loc)
smoke.start()
break
if(1)
diff --git a/code/modules/clothing/head/tophat.dm b/code/modules/clothing/head/tophat.dm
index dd60bab0510..17aa72ea43f 100644
--- a/code/modules/clothing/head/tophat.dm
+++ b/code/modules/clothing/head/tophat.dm
@@ -22,7 +22,7 @@
COOLDOWN_START(src, rabbit_cooldown, RABBIT_CD_TIME)
playsound(get_turf(src), 'sound/weapons/emitter.ogg', 70)
- do_smoke(range=1, location=src, smoke_type=/obj/effect/particle_effect/smoke/quick)
+ do_smoke(amount = DIAMOND_AREA(1), location=src, smoke_type=/obj/effect/particle_effect/fluid/smoke/quick)
if(prob(10))
magician.visible_message(span_danger("[magician] taps [src] with [hitby_wand], then reaches in and pulls out a bu- wait, those are bees!"), span_danger("You tap [src] with your [hitby_wand.name] and pull out... BEES!"))
diff --git a/code/modules/clothing/under/jobs/Plasmaman/civilian_service.dm b/code/modules/clothing/under/jobs/Plasmaman/civilian_service.dm
index c785574e6fb..9811ccf812c 100644
--- a/code/modules/clothing/under/jobs/Plasmaman/civilian_service.dm
+++ b/code/modules/clothing/under/jobs/Plasmaman/civilian_service.dm
@@ -132,4 +132,4 @@
extinguishes_left--
H.visible_message(span_warning("[H]'s suit spews space lube everywhere!"),span_warning("Your suit spews space lube everywhere!"))
H.extinguish_mob()
- new /obj/effect/particle_effect/foam(loc) //Truely terrifying.
+ new /obj/effect/particle_effect/fluid/foam(loc) //Truely terrifying.
diff --git a/code/modules/events/immovable_rod.dm b/code/modules/events/immovable_rod.dm
index b68dbead040..dce106ee6d1 100644
--- a/code/modules/events/immovable_rod.dm
+++ b/code/modules/events/immovable_rod.dm
@@ -211,8 +211,8 @@ In my current plan for it, 'solid' will be defined as anything with density == 1
// they ALSO collapse into a singulo.
if(istype(clong, /obj/effect/immovablerod))
visible_message(span_danger("[src] collides with [clong]! This cannot end well."))
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(2, get_turf(src))
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(2, location = get_turf(src))
smoke.start()
var/obj/singularity/bad_luck = new(get_turf(src))
bad_luck.energy = 800
diff --git a/code/modules/events/wisdomcow.dm b/code/modules/events/wisdomcow.dm
index b12938b3f94..c04bb38f5e1 100644
--- a/code/modules/events/wisdomcow.dm
+++ b/code/modules/events/wisdomcow.dm
@@ -10,5 +10,5 @@
/datum/round_event/wisdomcow/start()
var/turf/targetloc = get_safe_random_station_turf()
new /mob/living/basic/cow/wisdom(targetloc)
- do_smoke(1, targetloc)
+ do_smoke(DIAMOND_AREA(1), targetloc)
diff --git a/code/modules/events/wizard/curseditems.dm b/code/modules/events/wizard/curseditems.dm
index c3701c0908c..cdd44547d11 100644
--- a/code/modules/events/wizard/curseditems.dm
+++ b/code/modules/events/wizard/curseditems.dm
@@ -55,6 +55,6 @@
I.name = "cursed " + I.name
for(var/mob/living/carbon/human/H in GLOB.alive_mob_list)
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(0, H.loc)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(0, location = H.loc)
smoke.start()
diff --git a/code/modules/events/wizard/imposter.dm b/code/modules/events/wizard/imposter.dm
index 1a272265836..34bca08f3f9 100644
--- a/code/modules/events/wizard/imposter.dm
+++ b/code/modules/events/wizard/imposter.dm
@@ -15,7 +15,7 @@
return //Sad Trombone
var/mob/dead/observer/C = pick(candidates)
- new /obj/effect/particle_effect/smoke(W.loc)
+ new /obj/effect/particle_effect/fluid/smoke(W.loc)
var/mob/living/carbon/human/I = new /mob/living/carbon/human(W.loc)
W.dna.transfer_identity(I, transfer_SE=1)
diff --git a/code/modules/events/wizard/shuffle.dm b/code/modules/events/wizard/shuffle.dm
index c00917edd73..f8e37ee3a42 100644
--- a/code/modules/events/wizard/shuffle.dm
+++ b/code/modules/events/wizard/shuffle.dm
@@ -31,8 +31,8 @@
moblocs.len -= 1
for(var/mob/living/carbon/human/H in GLOB.alive_mob_list)
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(0, H.loc)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(0, location = H.loc)
smoke.start()
//---//
@@ -65,8 +65,8 @@
mobnames.len -= 1
for(var/mob/living/carbon/human/H in GLOB.alive_mob_list)
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(0, H.loc)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(0, location = H.loc)
smoke.start()
//---//
@@ -99,6 +99,6 @@
mobs -= mobs[mobs.len]
for(var/mob/living/carbon/human/H in GLOB.alive_mob_list)
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(0, H.loc)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(0, location = H.loc)
smoke.start()
diff --git a/code/modules/explorer_drone/exodrone.dm b/code/modules/explorer_drone/exodrone.dm
index 3f28b312320..ff5fc05f40a 100644
--- a/code/modules/explorer_drone/exodrone.dm
+++ b/code/modules/explorer_drone/exodrone.dm
@@ -418,7 +418,7 @@ GLOBAL_LIST_EMPTY(exodrone_launchers)
*/
/obj/machinery/exodrone_launcher/proc/launch_effect()
playsound(src,'sound/effects/podwoosh.ogg',50, FALSE)
- do_smoke(1,get_turf(src))
+ do_smoke(DIAMOND_AREA(1), get_turf(src))
/obj/machinery/exodrone_launcher/handle_atom_del(atom/A)
if(A == fuel_canister)
diff --git a/code/modules/food_and_drinks/kitchen_machinery/grill.dm b/code/modules/food_and_drinks/kitchen_machinery/grill.dm
index e00492ad33f..78ef29110c7 100644
--- a/code/modules/food_and_drinks/kitchen_machinery/grill.dm
+++ b/code/modules/food_and_drinks/kitchen_machinery/grill.dm
@@ -85,8 +85,8 @@
else
grill_fuel -= GRILL_FUELUSAGE_IDLE * delta_time
if(DT_PROB(0.5, delta_time))
- var/datum/effect_system/smoke_spread/bad/smoke = new
- smoke.set_up(1, loc)
+ var/datum/effect_system/fluid_spread/smoke/bad/smoke = new
+ smoke.set_up(1, location = loc)
smoke.start()
if(grilled_item)
SEND_SIGNAL(grilled_item, COMSIG_ITEM_GRILLED, src, delta_time)
diff --git a/code/modules/hydroponics/grown/onion.dm b/code/modules/hydroponics/grown/onion.dm
index 25a046acd6d..93940e1a82c 100644
--- a/code/modules/hydroponics/grown/onion.dm
+++ b/code/modules/hydroponics/grown/onion.dm
@@ -50,12 +50,12 @@
AddElement(/datum/element/processable, TOOL_KNIFE, /obj/item/food/onion_slice/red, 2, 15)
/obj/item/food/grown/onion/UsedforProcessing(mob/living/user, obj/item/I, list/chosen_option)
- var/datum/effect_system/smoke_spread/chem/S = new //Since the onion is destroyed when it's sliced,
+ var/datum/effect_system/fluid_spread/smoke/chem/cry_about_it = new //Since the onion is destroyed when it's sliced,
var/splat_location = get_turf(src) //we need to set up the smoke beforehand
- S.attach(splat_location)
- S.set_up(reagents, 0, splat_location, 0)
- S.start()
- qdel(S)
+ cry_about_it.attach(splat_location)
+ cry_about_it.set_up(0, location = splat_location, carry = reagents, silent = FALSE)
+ cry_about_it.start()
+ qdel(cry_about_it)
return ..()
/obj/item/food/onion_slice
diff --git a/code/modules/hydroponics/plant_genes.dm b/code/modules/hydroponics/plant_genes.dm
index 5195978eef0..24a2ec57450 100644
--- a/code/modules/hydroponics/plant_genes.dm
+++ b/code/modules/hydroponics/plant_genes.dm
@@ -646,12 +646,12 @@
SIGNAL_HANDLER
our_plant.investigate_log("made smoke at [AREACOORD(target)]. Last touched by: [our_plant.fingerprintslast].", INVESTIGATE_BOTANY)
- var/datum/effect_system/smoke_spread/chem/smoke = new ()
+ var/datum/effect_system/fluid_spread/smoke/chem/smoke = new ()
var/obj/item/seeds/our_seed = our_plant.get_plant_seed()
var/splat_location = get_turf(target)
- var/smoke_amount = round(sqrt(our_seed.potency * 0.1), 1)
+ var/range = sqrt(our_seed.potency * 0.1)
smoke.attach(splat_location)
- smoke.set_up(our_plant.reagents, smoke_amount, splat_location, 0)
+ smoke.set_up(round(range), location = splat_location, carry = our_plant.reagents, silent = FALSE)
smoke.start()
our_plant.reagents.clear_reagents()
diff --git a/code/modules/mining/equipment/survival_pod.dm b/code/modules/mining/equipment/survival_pod.dm
index 0baefc9041b..66b5a7cc465 100644
--- a/code/modules/mining/equipment/survival_pod.dm
+++ b/code/modules/mining/equipment/survival_pod.dm
@@ -64,7 +64,7 @@
log_admin("[key_name(usr)] activated a bluespace capsule away from the mining level at [AREACOORD(T)]")
playsound(src, 'sound/effects/phasein.ogg', 100, TRUE)
- new /obj/effect/particle_effect/smoke(get_turf(src))
+ new /obj/effect/particle_effect/fluid/smoke(get_turf(src))
qdel(src)
//Non-default pods
diff --git a/code/modules/mob/living/basic/farm_animals/cows.dm b/code/modules/mob/living/basic/farm_animals/cows.dm
index e1960d2143e..2f66d4601cb 100644
--- a/code/modules/mob/living/basic/farm_animals/cows.dm
+++ b/code/modules/mob/living/basic/farm_animals/cows.dm
@@ -109,7 +109,7 @@
if(!stat && !user.combat_mode)
to_chat(user, span_nicegreen("[src] whispers you some intense wisdoms and then disappears!"))
user.mind?.adjust_experience(pick(GLOB.skill_types), 500)
- do_smoke(1, get_turf(src))
+ do_smoke(DIAMOND_AREA(1), get_turf(src))
qdel(src)
return
return ..()
diff --git a/code/modules/mob/living/simple_animal/bot/cleanbot.dm b/code/modules/mob/living/simple_animal/bot/cleanbot.dm
index 8395dc4fb09..98b583a4c1d 100644
--- a/code/modules/mob/living/simple_animal/bot/cleanbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/cleanbot.dm
@@ -355,7 +355,7 @@
T.MakeSlippery(TURF_WET_WATER, min_wet_time = 20 SECONDS, wet_time_to_add = 15 SECONDS)
else
visible_message(span_danger("[src] whirs and bubbles violently, before releasing a plume of froth!"))
- new /obj/effect/particle_effect/foam(loc)
+ new /obj/effect/particle_effect/fluid/foam(loc)
else
..()
diff --git a/code/modules/mob/living/simple_animal/bot/firebot.dm b/code/modules/mob/living/simple_animal/bot/firebot.dm
index 9ffc4680868..64a6510299c 100644
--- a/code/modules/mob/living/simple_animal/bot/firebot.dm
+++ b/code/modules/mob/living/simple_animal/bot/firebot.dm
@@ -267,7 +267,7 @@
/mob/living/simple_animal/bot/firebot/atmos_expose(datum/gas_mixture/air, exposed_temperature)
if(COOLDOWN_FINISHED(src, foam_cooldown))
- new /obj/effect/particle_effect/foam/firefighting(loc)
+ new /obj/effect/particle_effect/fluid/foam/firefighting(loc)
COOLDOWN_START(src, foam_cooldown, FOAM_INTERVAL)
/mob/living/simple_animal/bot/firebot/proc/spray_water(atom/target, mob/user)
diff --git a/code/modules/mob/living/simple_animal/bot/hygienebot.dm b/code/modules/mob/living/simple_animal/bot/hygienebot.dm
index 41ef078a6ec..a1d9ab81793 100644
--- a/code/modules/mob/living/simple_animal/bot/hygienebot.dm
+++ b/code/modules/mob/living/simple_animal/bot/hygienebot.dm
@@ -53,7 +53,7 @@
ADD_TRAIT(src, TRAIT_SPRAY_PAINTABLE, INNATE_TRAIT)
/mob/living/simple_animal/bot/hygienebot/explode()
- new /obj/effect/particle_effect/foam(loc)
+ new /obj/effect/particle_effect/fluid/foam(loc)
return ..()
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm
index 9769cdd7b23..f4aa963e61f 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm
@@ -230,8 +230,8 @@
visible_message(span_boldwarning("[src] spews smoke from the tip of their spine!"))
else
visible_message(span_boldwarning("[src] spews smoke from its maw!"))
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(2, smoke_location)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(2, location = smoke_location)
smoke.start()
//The legionnaire's head. Basically the same as any legion head, but we have to tell our creator when we die so they can generate another head.
diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/clown.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/clown.dm
index 6bb9f05f2a2..0c9a4e569ad 100644
--- a/code/modules/mob/living/simple_animal/hostile/retaliate/clown.dm
+++ b/code/modules/mob/living/simple_animal/hostile/retaliate/clown.dm
@@ -65,7 +65,7 @@
response_disarm_continuous = "gently scoops and pours aside"
response_disarm_simple = "gently scoop and pour aside"
emote_see = list("bubbles", "oozes")
- loot = list(/obj/item/clothing/mask/gas/clown_hat, /obj/effect/particle_effect/foam)
+ loot = list(/obj/item/clothing/mask/gas/clown_hat, /obj/effect/particle_effect/fluid/foam)
/mob/living/simple_animal/hostile/retaliate/clown/lube/Initialize(mapload)
. = ..()
@@ -293,7 +293,7 @@
attack_verb_continuous = "steals the girlfriend of"
attack_verb_simple = "steal the girlfriend of"
attack_sound = 'sound/items/airhorn2.ogg'
- loot = list(/obj/item/clothing/mask/gas/clown_hat, /obj/effect/gibspawner/human, /obj/effect/particle_effect/foam, /obj/item/soap)
+ loot = list(/obj/item/clothing/mask/gas/clown_hat, /obj/effect/gibspawner/human, /obj/effect/particle_effect/fluid/foam, /obj/item/soap)
/mob/living/simple_animal/hostile/retaliate/clown/clownhulk/honcmunculus
name = "Honkmunculus"
@@ -316,7 +316,7 @@
attack_verb_continuous = "ferociously mauls"
attack_verb_simple = "ferociously maul"
environment_smash = ENVIRONMENT_SMASH_NONE
- loot = list(/obj/item/clothing/mask/gas/clown_hat, /obj/effect/gibspawner/xeno/bodypartless, /obj/effect/particle_effect/foam, /obj/item/soap)
+ loot = list(/obj/item/clothing/mask/gas/clown_hat, /obj/effect/gibspawner/xeno/bodypartless, /obj/effect/particle_effect/fluid/foam, /obj/item/soap)
attack_reagent = /datum/reagent/peaceborg/confuse
/mob/living/simple_animal/hostile/retaliate/clown/clownhulk/destroyer
@@ -339,7 +339,7 @@
attack_verb_simple = "act out divine vengeance on"
obj_damage = 50
environment_smash = ENVIRONMENT_SMASH_RWALLS
- loot = list(/obj/item/clothing/mask/gas/clown_hat, /obj/effect/gibspawner/human, /obj/effect/particle_effect/foam, /obj/item/soap)
+ loot = list(/obj/item/clothing/mask/gas/clown_hat, /obj/effect/gibspawner/human, /obj/effect/particle_effect/fluid/foam, /obj/item/soap)
/mob/living/simple_animal/hostile/retaliate/clown/mutant
name = "Unknown"
diff --git a/code/modules/mod/modules/modules_security.dm b/code/modules/mod/modules/modules_security.dm
index 067735b092d..24076d99d1d 100644
--- a/code/modules/mod/modules/modules_security.dm
+++ b/code/modules/mod/modules/modules_security.dm
@@ -154,9 +154,10 @@
playsound(src, 'sound/effects/spray.ogg', 30, TRUE, -6)
var/datum/reagents/capsaicin_holder = new(10)
capsaicin_holder.add_reagent(/datum/reagent/consumable/condensedcapsaicin, 10)
- var/datum/effect_system/smoke_spread/chem/quick/smoke = new
- smoke.set_up(capsaicin_holder, 1, get_turf(src))
+ var/datum/effect_system/fluid_spread/smoke/chem/quick/smoke = new
+ smoke.set_up(1, location = get_turf(src), carry = capsaicin_holder)
smoke.start()
+ QDEL_NULL(capsaicin_holder) // Reagents have a ref to their holder which has a ref to them. No leaks please.
/obj/item/mod/module/pepper_shoulders/proc/on_check_shields()
SIGNAL_HANDLER
diff --git a/code/modules/projectiles/guns/magic/wand.dm b/code/modules/projectiles/guns/magic/wand.dm
index 228328629d4..67fca3ba646 100644
--- a/code/modules/projectiles/guns/magic/wand.dm
+++ b/code/modules/projectiles/guns/magic/wand.dm
@@ -169,8 +169,8 @@
/obj/item/gun/magic/wand/teleport/zap_self(mob/living/user)
if(do_teleport(user, user, 10, channel = TELEPORT_CHANNEL_MAGIC))
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(3, user.loc)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(3, location = user.loc)
smoke.start()
charges--
..()
@@ -192,8 +192,8 @@
if(do_teleport(user, destination, channel=TELEPORT_CHANNEL_MAGIC))
for(var/t in list(origin, destination))
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(0, t)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(0, location = t)
smoke.start()
..()
diff --git a/code/modules/projectiles/projectile/magic.dm b/code/modules/projectiles/projectile/magic.dm
index 070369cc580..252afe41300 100644
--- a/code/modules/projectiles/projectile/magic.dm
+++ b/code/modules/projectiles/projectile/magic.dm
@@ -77,8 +77,9 @@
if(!stuff.anchored && stuff.loc && !isobserver(stuff))
if(do_teleport(stuff, stuff, 10, channel = TELEPORT_CHANNEL_MAGIC))
teleammount++
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(max(round(4 - teleammount),0), stuff.loc) //Smoke drops off if a lot of stuff is moved for the sake of sanity
+ var/smoke_range = max(round(4 - teleammount), 0)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(smoke_range, location = stuff.loc) //Smoke drops off if a lot of stuff is moved for the sake of sanity
smoke.start()
/obj/projectile/magic/safety
@@ -98,8 +99,8 @@
if(do_teleport(target, destination_turf, channel=TELEPORT_CHANNEL_MAGIC))
for(var/t in list(origin_turf, destination_turf))
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(0, t)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(0, location = t)
smoke.start()
/obj/projectile/magic/door
diff --git a/code/modules/reagents/chemistry/machinery/smoke_machine.dm b/code/modules/reagents/chemistry/machinery/smoke_machine.dm
index e7a62c7c2df..f5cd71c5c02 100644
--- a/code/modules/reagents/chemistry/machinery/smoke_machine.dm
+++ b/code/modules/reagents/chemistry/machinery/smoke_machine.dm
@@ -10,7 +10,7 @@
circuit = /obj/item/circuitboard/machine/smoke_machine
processing_flags = NONE
- var/efficiency = 10
+ var/efficiency = 20
var/on = FALSE
var/cooldown = 0
var/screen = "home"
@@ -18,17 +18,19 @@
var/setting = 1 // displayed range is 3 * setting
var/max_range = 3 // displayed max range is 3 * max range
-/datum/effect_system/smoke_spread/chem/smoke_machine/set_up(datum/reagents/carry, setting=1, efficiency=10, loc, silent=FALSE)
- amount = setting
- carry.copy_to(chemholder, 20)
- carry.remove_any(amount * 16 / efficiency)
- location = loc
+/datum/effect_system/fluid_spread/smoke/chem/smoke_machine/set_up(range = 1, amount = DIAMOND_AREA(range), atom/location = null, datum/reagents/carry = null, efficiency = 10, silent=FALSE)
+ src.location = get_turf(location)
+ src.amount = amount
+ carry?.copy_to(chemholder, 20)
+ carry?.remove_any(amount / efficiency)
-/datum/effect_system/smoke_spread/chem/smoke_machine
- effect_type = /obj/effect/particle_effect/smoke/chem/smoke_machine
+/// A factory which produces clouds of smoke for the smoke machine.
+/datum/effect_system/fluid_spread/smoke/chem/smoke_machine
+ effect_type = /obj/effect/particle_effect/fluid/smoke/chem/smoke_machine
-/obj/effect/particle_effect/smoke/chem/smoke_machine
- opaque = FALSE
+/// Smoke which is produced by the smoke machine. Slightly transparent and does not block line of sight.
+/obj/effect/particle_effect/fluid/smoke/chem/smoke_machine
+ opacity = FALSE
alpha = 100
/obj/machinery/smoke_machine/Initialize(mapload)
@@ -59,9 +61,9 @@
if(new_volume < reagents.total_volume)
reagents.expose(loc, TOUCH) // if someone manages to downgrade it without deconstructing
reagents.clear_reagents()
- efficiency = 9
+ efficiency = 18
for(var/obj/item/stock_parts/capacitor/C in component_parts)
- efficiency += C.rating
+ efficiency += 2 * C.rating
max_range = 1
for(var/obj/item/stock_parts/manipulator/M in component_parts)
max_range += M.rating
@@ -80,12 +82,12 @@
on = FALSE
update_appearance()
return
- var/turf/T = get_turf(src)
- var/smoke_test = locate(/obj/effect/particle_effect/smoke) in T
+ var/turf/location = get_turf(src)
+ var/smoke_test = locate(/obj/effect/particle_effect/fluid/smoke) in location
if(on && !smoke_test)
update_appearance()
- var/datum/effect_system/smoke_spread/chem/smoke_machine/smoke = new()
- smoke.set_up(reagents, setting*3, efficiency, T)
+ var/datum/effect_system/fluid_spread/smoke/chem/smoke_machine/smoke = new()
+ smoke.set_up(setting * 3, location = location, carry = reagents, efficiency = efficiency)
smoke.start()
use_power(active_power_usage)
diff --git a/code/modules/reagents/chemistry/reagents/pyrotechnic_reagents.dm b/code/modules/reagents/chemistry/reagents/pyrotechnic_reagents.dm
index e745a777c4f..dd4d7fe633e 100644
--- a/code/modules/reagents/chemistry/reagents/pyrotechnic_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/pyrotechnic_reagents.dm
@@ -371,7 +371,7 @@
return
if(reac_volume >= 1)
- var/obj/effect/particle_effect/foam/firefighting/foam = (locate(/obj/effect/particle_effect/foam) in exposed_turf)
+ var/obj/effect/particle_effect/fluid/foam/firefighting/foam = (locate(/obj/effect/particle_effect/fluid/foam) in exposed_turf)
if(!foam)
foam = new(exposed_turf)
else if(istype(foam))
diff --git a/code/modules/reagents/chemistry/recipes.dm b/code/modules/reagents/chemistry/recipes.dm
index bc133b55a48..f17574d1f53 100644
--- a/code/modules/reagents/chemistry/recipes.dm
+++ b/code/modules/reagents/chemistry/recipes.dm
@@ -359,7 +359,7 @@
//Spews out the inverse of the chems in the beaker of the products/reactants only
/datum/chemical_reaction/proc/explode_invert_smoke(datum/reagents/holder, datum/equilibrium/equilibrium, force_range = 0, clear_products = TRUE, clear_reactants = TRUE, accept_impure = TRUE)
var/datum/reagents/invert_reagents = new (2100, NO_REACT)//I think the biggest size we can get is 2100?
- var/datum/effect_system/smoke_spread/chem/smoke = new()
+ var/datum/effect_system/fluid_spread/smoke/chem/smoke = new()
var/sum_volume = 0
invert_reagents.my_atom = holder.my_atom //Give the gas a fingerprint
for(var/datum/reagent/reagent as anything in holder.reagent_list) //make gas for reagents, has to be done this way, otherwise it never stops Exploding
@@ -379,7 +379,7 @@
if(!force_range)
force_range = (sum_volume/6) + 3
if(invert_reagents.reagent_list)
- smoke.set_up(invert_reagents, force_range, holder.my_atom)
+ smoke.set_up(force_range, location = holder.my_atom, carry = invert_reagents)
smoke.start()
holder.my_atom.audible_message("The [holder.my_atom] suddenly explodes, launching the aerosolized reagents into the air!")
if(clear_reactants)
@@ -390,7 +390,7 @@
//Spews out the corrisponding reactions reagents (products/required) of the beaker in a smokecloud. Doesn't spew catalysts
/datum/chemical_reaction/proc/explode_smoke(datum/reagents/holder, datum/equilibrium/equilibrium, force_range = 0, clear_products = TRUE, clear_reactants = TRUE)
var/datum/reagents/reagents = new/datum/reagents(2100, NO_REACT)//Lets be safe first
- var/datum/effect_system/smoke_spread/chem/smoke = new()
+ var/datum/effect_system/fluid_spread/smoke/chem/smoke = new()
reagents.my_atom = holder.my_atom //fingerprint
var/sum_volume = 0
for (var/datum/reagent/reagent as anything in holder.reagent_list)
@@ -400,7 +400,7 @@
if(!force_range)
force_range = (sum_volume/6) + 3
if(reagents.reagent_list)
- smoke.set_up(reagents, force_range, holder.my_atom)
+ smoke.set_up(force_range, location = holder.my_atom, carry = reagents)
smoke.start()
holder.my_atom.audible_message("The [holder.my_atom] suddenly explodes, launching the aerosolized reagents into the air!")
if(clear_reactants)
diff --git a/code/modules/reagents/chemistry/recipes/others.dm b/code/modules/reagents/chemistry/recipes/others.dm
index 49e3b8ac06d..ac88ba89f78 100644
--- a/code/modules/reagents/chemistry/recipes/others.dm
+++ b/code/modules/reagents/chemistry/recipes/others.dm
@@ -328,7 +328,7 @@
reaction_flags = REACTION_INSTANT
/datum/chemical_reaction/foam/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume)
- holder.create_foam(/datum/effect_system/foam_spread,2*created_volume,notification=span_danger("The solution spews out foam!"))
+ holder.create_foam(/datum/effect_system/fluid_spread/foam, 2 * created_volume, notification = span_danger("The solution spews out foam!"))
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_UNIQUE
/datum/chemical_reaction/metalfoam
@@ -338,7 +338,7 @@
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_UNIQUE
/datum/chemical_reaction/metalfoam/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume)
- holder.create_foam(/datum/effect_system/foam_spread/metal,5*created_volume,1,span_danger("The solution spews out a metallic foam!"))
+ holder.create_foam(/datum/effect_system/fluid_spread/foam/metal, 5 * created_volume, /obj/structure/foamedmetal, span_danger("The solution spews out a metallic foam!"))
/datum/chemical_reaction/smart_foam
required_reagents = list(/datum/reagent/aluminium = 3, /datum/reagent/smart_foaming_agent = 1, /datum/reagent/toxin/acid/fluacid = 1)
@@ -347,7 +347,7 @@
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_UNIQUE
/datum/chemical_reaction/smart_foam/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume)
- holder.create_foam(/datum/effect_system/foam_spread/metal/smart,5*created_volume,1,span_danger("The solution spews out metallic foam!"))
+ holder.create_foam(/datum/effect_system/fluid_spread/foam/metal/smart, 5 * created_volume, /obj/structure/foamedmetal, span_danger("The solution spews out metallic foam!"))
/datum/chemical_reaction/ironfoam
required_reagents = list(/datum/reagent/iron = 3, /datum/reagent/foaming_agent = 1, /datum/reagent/toxin/acid/fluacid = 1)
@@ -356,7 +356,7 @@
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_UNIQUE
/datum/chemical_reaction/ironfoam/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume)
- holder.create_foam(/datum/effect_system/foam_spread/metal,5*created_volume,2,span_danger("The solution spews out a metallic foam!"))
+ holder.create_foam(/datum/effect_system/fluid_spread/foam/metal/iron, 5 * created_volume, /obj/structure/foamedmetal/iron, span_danger("The solution spews out a metallic foam!"))
/datum/chemical_reaction/foaming_agent
results = list(/datum/reagent/foaming_agent = 1)
diff --git a/code/modules/reagents/chemistry/recipes/pyrotechnics.dm b/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
index bc47036a636..7c5f4e80cc7 100644
--- a/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
+++ b/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
@@ -331,13 +331,12 @@
if(holder.has_reagent(/datum/reagent/stabilizing_agent))
return
holder.remove_reagent(/datum/reagent/smoke_powder, created_volume*3)
- var/smoke_radius = round(sqrt(created_volume * 1.5), 1)
var/location = get_turf(holder.my_atom)
- var/datum/effect_system/smoke_spread/chem/S = new
+ var/datum/effect_system/fluid_spread/smoke/chem/S = new
S.attach(location)
playsound(location, 'sound/effects/smoke.ogg', 50, TRUE, -3)
if(S)
- S.set_up(holder, smoke_radius, location, 0)
+ S.set_up(amount = created_volume * 3, location = location, carry = holder, silent = FALSE)
S.start()
if(holder?.my_atom)
holder.clear_reagents()
@@ -351,12 +350,11 @@
/datum/chemical_reaction/smoke_powder_smoke/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume)
var/location = get_turf(holder.my_atom)
- var/smoke_radius = round(sqrt(created_volume / 2), 1)
- var/datum/effect_system/smoke_spread/chem/S = new
+ var/datum/effect_system/fluid_spread/smoke/chem/S = new
S.attach(location)
playsound(location, 'sound/effects/smoke.ogg', 50, TRUE, -3)
if(S)
- S.set_up(holder, smoke_radius, location, 0)
+ S.set_up(amount = created_volume, location = location, carry = holder, silent = FALSE)
S.start()
if(holder?.my_atom)
holder.clear_reagents()
diff --git a/code/modules/reagents/chemistry/recipes/slime_extracts.dm b/code/modules/reagents/chemistry/recipes/slime_extracts.dm
index 331a8431dac..49398f41bc2 100644
--- a/code/modules/reagents/chemistry/recipes/slime_extracts.dm
+++ b/code/modules/reagents/chemistry/recipes/slime_extracts.dm
@@ -190,7 +190,7 @@
required_other = TRUE
/datum/chemical_reaction/slime/slimefoam/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume)
- holder.create_foam(/datum/effect_system/foam_spread,80, span_danger("[src] spews out foam!"))
+ holder.create_foam(/datum/effect_system/fluid_spread/foam, 80, span_danger("[src] spews out foam!"))
//Dark Blue
/datum/chemical_reaction/slime/slimefreeze
diff --git a/code/modules/research/experimentor.dm b/code/modules/research/experimentor.dm
index 629333422dc..5266c89ad01 100644
--- a/code/modules/research/experimentor.dm
+++ b/code/modules/research/experimentor.dm
@@ -232,8 +232,8 @@
loaded_item = null
/obj/machinery/rnd/experimentor/proc/throwSmoke(turf/where)
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(0, where)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(0, location = where)
smoke.start()
@@ -305,27 +305,27 @@
else if(prob(EFFECT_PROB_VERYLOW-badThingCoeff))
visible_message(span_danger("[src] destroys [exp_on], leaking dangerous gas!"))
chosenchem = pick(/datum/reagent/carbon,/datum/reagent/uranium/radium,/datum/reagent/toxin,/datum/reagent/consumable/condensedcapsaicin,/datum/reagent/drug/mushroomhallucinogen,/datum/reagent/drug/space_drugs,/datum/reagent/consumable/ethanol,/datum/reagent/consumable/ethanol/beepsky_smash)
- var/datum/reagents/R = new/datum/reagents(50)
- R.my_atom = src
- R.add_reagent(chosenchem , 50)
+ var/datum/reagents/tmp_holder = new/datum/reagents(50)
+ tmp_holder.my_atom = src
+ tmp_holder.add_reagent(chosenchem , 50)
investigate_log("Experimentor has released [chosenchem] smoke.", INVESTIGATE_EXPERIMENTOR)
- var/datum/effect_system/smoke_spread/chem/smoke = new
- smoke.set_up(R, 0, src, silent = TRUE)
+ var/datum/effect_system/fluid_spread/smoke/chem/smoke = new
+ smoke.set_up(0, location = src, carry = tmp_holder, silent = TRUE)
playsound(src, 'sound/effects/smoke.ogg', 50, TRUE, -3)
smoke.start()
- qdel(R)
+ qdel(tmp_holder)
ejectItem(TRUE)
else if(prob(EFFECT_PROB_VERYLOW-badThingCoeff))
visible_message(span_danger("[src]'s chemical chamber has sprung a leak!"))
chosenchem = pick(/datum/reagent/mutationtoxin/classic,/datum/reagent/cyborg_mutation_nanomachines,/datum/reagent/toxin/acid)
- var/datum/reagents/R = new/datum/reagents(50)
- R.my_atom = src
- R.add_reagent(chosenchem , 50)
- var/datum/effect_system/smoke_spread/chem/smoke = new
- smoke.set_up(R, 0, src, silent = TRUE)
+ var/datum/reagents/tmp_holder = new/datum/reagents(50)
+ tmp_holder.my_atom = src
+ tmp_holder.add_reagent(chosenchem , 50)
+ var/datum/effect_system/fluid_spread/smoke/chem/smoke = new
+ smoke.set_up(0, location = src, carry = tmp_holder, silent = TRUE)
playsound(src, 'sound/effects/smoke.ogg', 50, TRUE, -3)
smoke.start()
- qdel(R)
+ qdel(tmp_holder)
ejectItem(TRUE)
warn_admins(usr, "[chosenchem] smoke")
investigate_log("Experimentor has released [chosenchem] smoke!", INVESTIGATE_EXPERIMENTOR)
@@ -401,15 +401,15 @@
investigate_log("Experimentor has made a cup of [chosenchem] coffee.", INVESTIGATE_EXPERIMENTOR)
else if(prob(EFFECT_PROB_VERYLOW-badThingCoeff))
visible_message(span_danger("[src] malfunctions, shattering [exp_on] and releasing a dangerous cloud of coolant!"))
- var/datum/reagents/R = new/datum/reagents(50)
- R.my_atom = src
- R.add_reagent(/datum/reagent/consumable/frostoil , 50)
+ var/datum/reagents/tmp_holder = new/datum/reagents(50)
+ tmp_holder.my_atom = src
+ tmp_holder.add_reagent(/datum/reagent/consumable/frostoil, 50)
investigate_log("Experimentor has released frostoil gas.", INVESTIGATE_EXPERIMENTOR)
- var/datum/effect_system/smoke_spread/chem/smoke = new
- smoke.set_up(R, 0, src, silent = TRUE)
+ var/datum/effect_system/fluid_spread/smoke/chem/smoke = new
+ smoke.set_up(0, location = src, carry = tmp_holder, silent = TRUE)
playsound(src, 'sound/effects/smoke.ogg', 50, TRUE, -3)
smoke.start()
- qdel(R)
+ qdel(tmp_holder)
ejectItem(TRUE)
else if(prob(EFFECT_PROB_LOW-badThingCoeff))
visible_message(span_warning("[src] malfunctions, shattering [exp_on] and leaking cold air!"))
@@ -427,8 +427,8 @@
ejectItem(TRUE)
else if(prob(EFFECT_PROB_MEDIUM-badThingCoeff))
visible_message(span_warning("[src] malfunctions, releasing a flurry of chilly air as [exp_on] pops out!"))
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(0, loc)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(0, location = loc)
smoke.start()
ejectItem()
////////////////////////////////////////////////////////////////////////////////////////////////
@@ -598,8 +598,8 @@
//////////////// RELIC PROCS /////////////////////////////
/obj/item/relic/proc/throwSmoke(turf/where)
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(0, get_turf(where))
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(0, location = get_turf(where))
smoke.start()
/obj/item/relic/proc/corgicannon(mob/user)
diff --git a/code/modules/research/xenobiology/crossbreeding/burning.dm b/code/modules/research/xenobiology/crossbreeding/burning.dm
index 5b364021dc2..80a856ce453 100644
--- a/code/modules/research/xenobiology/crossbreeding/burning.dm
+++ b/code/modules/research/xenobiology/crossbreeding/burning.dm
@@ -45,11 +45,11 @@ Burning extracts:
/obj/item/slimecross/burning/orange/do_effect(mob/user)
user.visible_message(span_danger("[src] boils over with a caustic gas!"))
- var/datum/reagents/R = new/datum/reagents(100)
- R.add_reagent(/datum/reagent/consumable/condensedcapsaicin, 100)
+ var/datum/reagents/tmp_holder = new/datum/reagents(100)
+ tmp_holder.add_reagent(/datum/reagent/consumable/condensedcapsaicin, 100)
- var/datum/effect_system/smoke_spread/chem/smoke = new
- smoke.set_up(R, 7, get_turf(user))
+ var/datum/effect_system/fluid_spread/smoke/chem/smoke = new
+ smoke.set_up(7, location = get_turf(user), carry = tmp_holder)
smoke.start()
..()
@@ -120,11 +120,11 @@ Burning extracts:
/obj/item/slimecross/burning/darkblue/do_effect(mob/user)
user.visible_message(span_danger("[src] releases a burst of chilling smoke!"))
- var/datum/reagents/R = new/datum/reagents(100)
- R.add_reagent(/datum/reagent/consumable/frostoil, 40)
- user.reagents.add_reagent(/datum/reagent/medicine/regen_jelly,10)
- var/datum/effect_system/smoke_spread/chem/smoke = new
- smoke.set_up(R, 7, get_turf(user))
+ var/datum/reagents/tmp_holder = new/datum/reagents(100)
+ tmp_holder.add_reagent(/datum/reagent/consumable/frostoil, 40)
+ user.reagents.add_reagent(/datum/reagent/medicine/regen_jelly, 10)
+ var/datum/effect_system/fluid_spread/smoke/chem/smoke = new
+ smoke.set_up(7, location = get_turf(user), carry = tmp_holder)
smoke.start()
..()
diff --git a/code/modules/research/xenobiology/vatgrowing/samples/_micro_organism.dm b/code/modules/research/xenobiology/vatgrowing/samples/_micro_organism.dm
index b3d2b45f566..da1bf67c7ad 100644
--- a/code/modules/research/xenobiology/vatgrowing/samples/_micro_organism.dm
+++ b/code/modules/research/xenobiology/vatgrowing/samples/_micro_organism.dm
@@ -95,8 +95,8 @@
QDEL_NULL(vat.biological_sample)
/datum/micro_organism/cell_line/proc/succeed_growing(obj/machinery/plumbing/growing_vat/vat)
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(0, vat.loc)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(0, location = vat.loc)
smoke.start()
for(var/created_thing in resulting_atoms)
for(var/x in 1 to resulting_atoms[created_thing])
diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm
index f34e1dba11c..e626ea76467 100644
--- a/code/modules/research/xenobiology/xenobiology.dm
+++ b/code/modules/research/xenobiology/xenobiology.dm
@@ -316,7 +316,7 @@
return 250
if(SLIME_ACTIVATE_MAJOR)
- user.reagents.create_foam(/datum/effect_system/foam_spread,20)
+ user.reagents.create_foam(/datum/effect_system/fluid_spread/foam, 20)
user.visible_message(span_danger("Foam spews out from [user]'s skin!"), span_warning("You activate [src], and foam bursts out of your skin!"))
return 600
diff --git a/code/modules/spells/spell.dm b/code/modules/spells/spell.dm
index 370dbbdd788..6188ec76be4 100644
--- a/code/modules/spells/spell.dm
+++ b/code/modules/spells/spell.dm
@@ -146,8 +146,10 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell)) //needed for th
var/sparks_spread = 0
var/sparks_amt = 0 //cropped at 10
- var/smoke_spread = 0 //1 - harmless, 2 - harmful
- var/smoke_amt = 0 //cropped at 10
+ /// The typepath of the smoke to create on cast.
+ var/smoke_spread = null
+ /// The amount of smoke to create on case. This is a range so a value of 5 will create enough smoke to cover everything within 5 steps.
+ var/smoke_amt = 0
var/centcom_cancast = TRUE //Whether or not the spell should be allowed on z2
@@ -340,19 +342,10 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell)) //needed for th
to_chat(target, text("[message]"))
if(sparks_spread)
do_sparks(sparks_amt, FALSE, location)
- if(smoke_spread)
- if(smoke_spread == 1)
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(smoke_amt, location)
- smoke.start()
- else if(smoke_spread == 2)
- var/datum/effect_system/smoke_spread/bad/smoke = new
- smoke.set_up(smoke_amt, location)
- smoke.start()
- else if(smoke_spread == 3)
- var/datum/effect_system/smoke_spread/sleeping/smoke = new
- smoke.set_up(smoke_amt, location)
- smoke.start()
+ if(ispath(smoke_spread, /datum/effect_system/fluid_spread/smoke)) // Dear god this code is :agony:
+ var/datum/effect_system/fluid_spread/smoke/smoke = new smoke_spread()
+ smoke.set_up(smoke_amt, location = location)
+ smoke.start()
/obj/effect/proc_holder/spell/proc/cast(list/targets,mob/user = usr)
diff --git a/code/modules/spells/spell_types/construct_spells.dm b/code/modules/spells/spell_types/construct_spells.dm
index 12c1c815357..35f9810e489 100644
--- a/code/modules/spells/spell_types/construct_spells.dm
+++ b/code/modules/spells/spell_types/construct_spells.dm
@@ -178,7 +178,7 @@
include_user = TRUE
cooldown_min = 20 //25 deciseconds reduction per rank
- smoke_spread = 3
+ smoke_spread = /datum/effect_system/fluid_spread/smoke/sleeping
smoke_amt = 4
action_icon_state = "smoke"
action_background_icon_state = "bg_cult"
diff --git a/code/modules/spells/spell_types/wizard.dm b/code/modules/spells/spell_types/wizard.dm
index e803bf74d74..2160c7a2991 100644
--- a/code/modules/spells/spell_types/wizard.dm
+++ b/code/modules/spells/spell_types/wizard.dm
@@ -62,7 +62,7 @@
include_user = TRUE
cooldown_min = 20 //25 deciseconds reduction per rank
- smoke_spread = 2
+ smoke_spread = /datum/effect_system/fluid_spread/smoke/bad
smoke_amt = 4
action_icon_state = "smoke"
@@ -81,7 +81,7 @@
include_user = TRUE
antimagic_flags = NONE // no cast restrictions
- smoke_spread = 1
+ smoke_spread = /datum/effect_system/fluid_spread/smoke
smoke_amt = 2
action_icon_state = "smoke"
@@ -115,7 +115,7 @@
cooldown_min = 5 //4 deciseconds reduction per rank
- smoke_spread = 1
+ smoke_spread = /datum/effect_system/fluid_spread/smoke
smoke_amt = 0
inner_tele_radius = 0
@@ -139,7 +139,7 @@
cooldown_min = 200 //100 deciseconds reduction per rank
action_icon_state = "teleport"
- smoke_spread = 1
+ smoke_spread = /datum/effect_system/fluid_spread/smoke
smoke_amt = 2
sound1 = 'sound/magic/teleport_diss.ogg'
sound2 = 'sound/magic/teleport_app.ogg'
diff --git a/code/modules/station_goals/bsa.dm b/code/modules/station_goals/bsa.dm
index af2324a2608..f77b8d78fc1 100644
--- a/code/modules/station_goals/bsa.dm
+++ b/code/modules/station_goals/bsa.dm
@@ -350,9 +350,9 @@ GLOBAL_VAR_INIT(bsa_unlock, FALSE)
if(notice)
return null
//Totally nanite construction system not an immersion breaking spawning
- var/datum/effect_system/smoke_spread/s = new
- s.set_up(4,get_turf(centerpiece))
- s.start()
+ var/datum/effect_system/fluid_spread/smoke/fourth_wall_guard = new
+ fourth_wall_guard.set_up(4, location = get_turf(centerpiece))
+ fourth_wall_guard.start()
var/obj/machinery/bsa/full/cannon = new(get_turf(centerpiece),centerpiece.get_cannon_direction())
QDEL_NULL(centerpiece.front_ref)
QDEL_NULL(centerpiece.back_ref)
diff --git a/code/modules/vehicles/atv.dm b/code/modules/vehicles/atv.dm
index 86a5cd35320..88d9431e0d1 100644
--- a/code/modules/vehicles/atv.dm
+++ b/code/modules/vehicles/atv.dm
@@ -88,8 +88,8 @@
return PROCESS_KILL
if(DT_PROB(10, delta_time))
return
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(0, src)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(0, location = src)
smoke.start()
/obj/vehicle/ridden/atv/bullet_act(obj/projectile/P)
diff --git a/code/modules/vehicles/cars/clowncar.dm b/code/modules/vehicles/cars/clowncar.dm
index 2c68c55baf3..99322ba93ea 100644
--- a/code/modules/vehicles/cars/clowncar.dm
+++ b/code/modules/vehicles/cars/clowncar.dm
@@ -82,7 +82,7 @@
. = ..()
if(prob(33))
visible_message(span_danger("[src] spews out a ton of space lube!"))
- new /obj/effect/particle_effect/foam(loc) //YEET
+ new /obj/effect/particle_effect/fluid/foam(loc) //YEET
/obj/vehicle/sealed/car/clowncar/attacked_by(obj/item/I, mob/living/user)
. = ..()
@@ -154,7 +154,7 @@
var/datum/reagents/randomchems = new/datum/reagents(300)
randomchems.my_atom = src
randomchems.add_reagent(get_random_reagent_id(), 100)
- var/datum/effect_system/foam_spread/foam = new
+ var/datum/effect_system/fluid_spread/foam/foam = new
foam.set_up(200, loc, randomchems)
foam.start()
if(3)
@@ -167,8 +167,8 @@
var/datum/reagents/funnychems = new/datum/reagents(300)
funnychems.my_atom = src
funnychems.add_reagent(/datum/reagent/consumable/superlaughter, 50)
- var/datum/effect_system/smoke_spread/chem/smoke = new()
- smoke.set_up(funnychems, 4)
+ var/datum/effect_system/fluid_spread/smoke/chem/smoke = new()
+ smoke.set_up(4, location = src, carry = funnychems)
smoke.attach(src)
smoke.start()
if(5)
diff --git a/code/modules/vehicles/mecha/_mecha.dm b/code/modules/vehicles/mecha/_mecha.dm
index b1c3cbbd6b3..bb9e4af3430 100644
--- a/code/modules/vehicles/mecha/_mecha.dm
+++ b/code/modules/vehicles/mecha/_mecha.dm
@@ -149,7 +149,7 @@
///Safety for weapons. Won't fire if enabled, and toggled by middle click.
var/weapons_safety = FALSE
- var/datum/effect_system/smoke_spread/smoke_system = new
+ var/datum/effect_system/fluid_spread/smoke/smoke_system = new
////Action vars
///Ref to any active thrusters we might have
@@ -209,7 +209,7 @@
spark_system.set_up(2, 0, src)
spark_system.attach(src)
- smoke_system.set_up(3, src)
+ smoke_system.set_up(3, location = src)
smoke_system.attach(src)
radio = new(src)
diff --git a/code/modules/vehicles/mecha/equipment/tools/other_tools.dm b/code/modules/vehicles/mecha/equipment/tools/other_tools.dm
index 276138e8b2d..0bdfc2b72f7 100644
--- a/code/modules/vehicles/mecha/equipment/tools/other_tools.dm
+++ b/code/modules/vehicles/mecha/equipment/tools/other_tools.dm
@@ -434,7 +434,7 @@
/obj/item/mecha_parts/mecha_equipment/thrusters/gas
name = "RCS thruster package"
desc = "A set of thrusters that allow for exosuit movement in zero-gravity environments, by expelling gas from the internal life support tank."
- effect_type = /obj/effect/particle_effect/smoke
+ effect_type = /obj/effect/particle_effect/fluid/smoke
var/move_cost = 20 //moles per step
/obj/item/mecha_parts/mecha_equipment/thrusters/gas/try_attach_part(mob/user, obj/vehicle/sealed/mecha/M, attach_right = FALSE)
diff --git a/code/modules/vehicles/secway.dm b/code/modules/vehicles/secway.dm
index 55641607fd1..1a2e6472441 100644
--- a/code/modules/vehicles/secway.dm
+++ b/code/modules/vehicles/secway.dm
@@ -24,8 +24,8 @@
return PROCESS_KILL
if(DT_PROB(10, delta_time))
return
- var/datum/effect_system/smoke_spread/smoke = new
- smoke.set_up(0, src)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(0, location = src)
smoke.start()
/obj/vehicle/ridden/secway/welder_act(mob/living/user, obj/item/I)
diff --git a/tgstation.dme b/tgstation.dme
index 13f14fa898a..dc8232e70dc 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -456,6 +456,7 @@
#include "code\controllers\subsystem\events.dm"
#include "code\controllers\subsystem\explosions.dm"
#include "code\controllers\subsystem\fire_burning.dm"
+#include "code\controllers\subsystem\fluids.dm"
#include "code\controllers\subsystem\garbage.dm"
#include "code\controllers\subsystem\icon_smooth.dm"
#include "code\controllers\subsystem\id_access.dm"
@@ -528,11 +529,11 @@
#include "code\controllers\subsystem\processing\clock_component.dm"
#include "code\controllers\subsystem\processing\conveyors.dm"
#include "code\controllers\subsystem\processing\fastprocess.dm"
-#include "code\controllers\subsystem\processing\fluids.dm"
#include "code\controllers\subsystem\processing\greyscale.dm"
#include "code\controllers\subsystem\processing\instruments.dm"
#include "code\controllers\subsystem\processing\networks.dm"
#include "code\controllers\subsystem\processing\obj.dm"
+#include "code\controllers\subsystem\processing\plumbing.dm"
#include "code\controllers\subsystem\processing\processing.dm"
#include "code\controllers\subsystem\processing\projectiles.dm"
#include "code\controllers\subsystem\processing\quirks.dm"
@@ -1386,11 +1387,12 @@
#include "code\game\objects\effects\effect_system\effect_shield.dm"
#include "code\game\objects\effects\effect_system\effect_system.dm"
#include "code\game\objects\effects\effect_system\effects_explosion.dm"
-#include "code\game\objects\effects\effect_system\effects_foam.dm"
#include "code\game\objects\effects\effect_system\effects_other.dm"
-#include "code\game\objects\effects\effect_system\effects_smoke.dm"
#include "code\game\objects\effects\effect_system\effects_sparks.dm"
#include "code\game\objects\effects\effect_system\effects_water.dm"
+#include "code\game\objects\effects\effect_system\fluid_spread\_fluid_spread.dm"
+#include "code\game\objects\effects\effect_system\fluid_spread\effects_foam.dm"
+#include "code\game\objects\effects\effect_system\fluid_spread\effects_smoke.dm"
#include "code\game\objects\effects\spawners\bombspawner.dm"
#include "code\game\objects\effects\spawners\costume.dm"
#include "code\game\objects\effects\spawners\gibspawner.dm"