diff --git a/baystation12.dme b/baystation12.dme
index 75cc64228b5..3936f304d8c 100644
--- a/baystation12.dme
+++ b/baystation12.dme
@@ -59,6 +59,7 @@
#include "code\__defines\languages.dm"
#include "code\__defines\lazy_value.dm"
#include "code\__defines\lighting.dm"
+#include "code\__defines\liquids.dm"
#include "code\__defines\machinery.dm"
#include "code\__defines\mapping.dm"
#include "code\__defines\materials.dm"
@@ -175,6 +176,7 @@
#include "code\_helpers\time.dm"
#include "code\_helpers\turfs.dm"
#include "code\_helpers\type2type.dm"
+#include "code\_helpers\type_processing.dm"
#include "code\_helpers\unsorted.dm"
#include "code\_helpers\vector.dm"
#include "code\_helpers\warnings.dm"
@@ -245,6 +247,7 @@
#include "code\controllers\subsystems\garbage.dm"
#include "code\controllers\subsystems\inactivity.dm"
#include "code\controllers\subsystems\lighting.dm"
+#include "code\controllers\subsystems\liquids.dm"
#include "code\controllers\subsystems\machines.dm"
#include "code\controllers\subsystems\mapping.dm"
#include "code\controllers\subsystems\misc_late.dm"
@@ -1953,6 +1956,20 @@
#include "code\modules\lighting\lighting_source.dm"
#include "code\modules\lighting\lighting_turf.dm"
#include "code\modules\lighting\~lighting_undefs.dm"
+#include "code\modules\liquids\drains.dm"
+#include "code\modules\liquids\height_floors.dm"
+#include "code\modules\liquids\tools.dm"
+#include "code\modules\liquids\liquid_systems\liquid_effect.dm"
+#include "code\modules\liquids\liquid_systems\liquid_groups.dm"
+#include "code\modules\liquids\liquid_systems\liquid_height.dm"
+#include "code\modules\liquids\liquid_systems\liquid_interaction.dm"
+#include "code\modules\liquids\liquid_systems\liquid_plumbers.dm"
+#include "code\modules\liquids\liquid_systems\liquid_pump.dm"
+#include "code\modules\liquids\liquid_systems\liquid_status_effect.dm"
+#include "code\modules\liquids\liquid_systems\liquid_turf.dm"
+#include "code\modules\liquids\reagents\reagent_containers.dm"
+#include "code\modules\liquids\reagents\chemistry\holder.dm"
+#include "code\modules\liquids\reagents\chemistry\reagents.dm"
#include "code\modules\lobby_music\_licenses.dm"
#include "code\modules\lobby_music\_lobby_music.dm"
#include "code\modules\lobby_music\crocketts_theme.dm"
diff --git a/code/__defines/ces/signals.dm b/code/__defines/ces/signals.dm
index 0ca3a4c7f75..99ae4702f44 100644
--- a/code/__defines/ces/signals.dm
+++ b/code/__defines/ces/signals.dm
@@ -13,3 +13,6 @@
#define SIGNAL_ELEMENT_ATTACH "element_attach"
/// Fires on the target datum when an element is attached to it (/datum/element).
#define SIGNAL_ELEMENT_DETACH "element_detach"
+
+/// Fires on the afterattack when an element is designed to clean liquids
+#define SIGNAL_CLEAN_LIQUIDS "clean_liquids"
diff --git a/code/__defines/ces/signals_atom.dm b/code/__defines/ces/signals_atom.dm
index 04c83054120..283298ff633 100644
--- a/code/__defines/ces/signals_atom.dm
+++ b/code/__defines/ces/signals_atom.dm
@@ -25,6 +25,9 @@
/// From base of atom/proc/Initialize(): sent any time a new atom is created in this atom
#define SIGNAL_ATOM_INITIALIZED_ON "atom_initialized_on"
+/// Called on `/atom/movable/proc/handle_fall` (turf)
+#define SIGNAL_ATOM_FALL "atom_fall"
+
/// Called on 'atom/Move' (/atom, old_turf, new_turf)
#define SIGNAL_Z_CHANGED "movable_z_changed"
diff --git a/code/__defines/ces/signals_turf.dm b/code/__defines/ces/signals_turf.dm
index 066bbb3f53c..2f57934871a 100644
--- a/code/__defines/ces/signals_turf.dm
+++ b/code/__defines/ces/signals_turf.dm
@@ -1,2 +1,3 @@
/// Called on `/turf/proc/ChangeTurf` (/turf, old_density, density, old_opacity, opacity)
#define SIGNAL_TURF_CHANGED "turf_changed"
+#define SIGNAL_TURF_LIQUIDS_CREATION "turf_liquids_creation"
diff --git a/code/__defines/lighting.dm b/code/__defines/lighting.dm
index abd5ff50970..1d3ecf992b8 100644
--- a/code/__defines/lighting.dm
+++ b/code/__defines/lighting.dm
@@ -30,3 +30,9 @@
#define LIGHTMODE_ALARM "alarm"
#define LIGHTMODE_READY "ready"
#define LIGHTMODE_RADSTORM "radiation_storm"
+
+///How many tiles standard fires glow.
+#define LIGHT_RANGE_FIRE 3
+#define LIGHT_FIRE_BLOSSOM 2.1
+#define LIGHT_RANGE_FIRE_BLOSSOM_HARVESTED 2.7
+#define LIGHT_POWER_FIRE_BLOSSOM_HARVESTED 1.5
diff --git a/code/__defines/liquids.dm b/code/__defines/liquids.dm
new file mode 100644
index 00000000000..d67b6017e10
--- /dev/null
+++ b/code/__defines/liquids.dm
@@ -0,0 +1,83 @@
+#define WATER_HEIGH_DIFFERENCE_SOUND_CHANCE 50
+#define WATER_HEIGH_DIFFERENCE_DELTA_SPLASH 7 //Delta needed for the splash effect to be made in 1 go
+
+#define REQUIRED_EVAPORATION_PROCESSES 80
+#define EVAPORATION_CHANCE 30
+
+/// Portion (out of 1) of reagents that are lost during the transfer from a mop/towel to a container.
+#define SQUEEZING_DISPERSAL_RATIO 0.75
+
+#define REQUIRED_FIRE_PROCESSES 4
+#define REQUIRED_FIRE_POWER_PER_UNIT 5
+
+#define PARTIAL_TRANSFER_AMOUNT 0.3
+
+#define LIQUID_MUTUAL_SHARE 1
+#define LIQUID_NOT_MUTUAL_SHARE 2
+
+#define LIQUID_GIVER 1
+#define LIQUID_TAKER 2
+
+//Required amount of a reagent to be simulated on turf exposures from liquids (to prevent gaming the system with cheap dillutions)
+#define LIQUID_REAGENT_THRESHOLD_TURF_EXPOSURE 5
+
+//Threshold at which the difference of height makes us need to climb/blocks movement/allows to fall down
+#define TURF_HEIGHT_BLOCK_THRESHOLD 20
+
+#define LIQUID_HEIGHT_DIVISOR 10
+
+#define ONE_LIQUIDS_HEIGHT LIQUID_HEIGHT_DIVISOR
+
+#define LIQUID_ATTRITION_TO_STOP_ACTIVITY 2
+
+//Percieved heat capacity for calculations with atmos sharing
+#define REAGENT_HEAT_CAPACITY 5
+
+#define LIQUID_STATE_PUDDLE 1
+#define LIQUID_STATE_ANKLES 2
+#define LIQUID_STATE_WAIST 3
+#define LIQUID_STATE_SHOULDERS 4
+#define LIQUID_STATE_FULLTILE 5
+#define TOTAL_LIQUID_STATES 5
+#define LYING_DOWN_SUBMERGEMENT_STATE_BONUS 2
+
+#define LIQUID_STATE_FOR_HEAT_EXCHANGERS LIQUID_STATE_WAIST
+
+#define LIQUID_ANKLES_LEVEL_HEIGHT 8
+#define LIQUID_WAIST_LEVEL_HEIGHT 19
+#define LIQUID_SHOULDERS_LEVEL_HEIGHT 29
+#define LIQUID_FULLTILE_LEVEL_HEIGHT 39
+
+#define LIQUID_FIRE_STATE_NONE 0
+#define LIQUID_FIRE_STATE_SMALL 1
+#define LIQUID_FIRE_STATE_MILD 2
+#define LIQUID_FIRE_STATE_MEDIUM 3
+#define LIQUID_FIRE_STATE_HUGE 4
+#define LIQUID_FIRE_STATE_INFERNO 5
+
+//Threshold at which we "choke" on the water, instead of holding our breath
+#define OXYGEN_DAMAGE_CHOKING_THRESHOLD 15
+
+#define IMMUTABLE_LIQUID_SHARE 1
+
+#define LIQUID_RECURSIVE_LOOP_SAFETY 100 //Hundred loops at maximum for adjacency checking
+
+//Height at which we consider the tile "full" and dont drop liquids on it from the upper Z level
+#define LIQUID_HEIGHT_CONSIDER_FULL_TILE 50
+
+#define SSLIQUIDS_RUN_TYPE_TURFS 1
+#define SSLIQUIDS_RUN_TYPE_GROUPS 2
+#define SSLIQUIDS_RUN_TYPE_IMMUTABLES 3
+#define SSLIQUIDS_RUN_TYPE_EVAPORATION 4
+#define SSLIQUIDS_RUN_TYPE_FIRE 5
+
+#define LIQUID_GROUP_DECAY_TIME 3
+
+//Scaled with how much a person is submerged
+#define SUBMERGEMENT_REAGENTS_TOUCH_AMOUNT 60
+
+#define CHOKE_REAGENTS_INGEST_ON_FALL_AMOUNT 4
+
+#define CHOKE_REAGENTS_INGEST_ON_BREATH_AMOUNT 2
+
+#define SUBMERGEMENT_PERCENT(carbon, liquids) min(1,(carbon.lying ? liquids.liquid_state+LYING_DOWN_SUBMERGEMENT_STATE_BONUS : liquids.liquid_state)/TOTAL_LIQUID_STATES)
diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm
index da375872552..9451205d441 100644
--- a/code/__defines/misc.dm
+++ b/code/__defines/misc.dm
@@ -101,7 +101,7 @@
#define TEMPLATE_FLAG_ALLOW_DUPLICATES 1 // Lets multiple copies of the template to be spawned
#define TEMPLATE_FLAG_SPAWN_GUARANTEED 2 // Makes it ignore away site budget and just spawn (only for away sites)
#define TEMPLATE_FLAG_CLEAR_CONTENTS 4 // if it should destroy objects it spawns on top of
-#define TEMPLATE_FLAG_NO_RUINS 8 // if it should forbid ruins from spawning on top of it
+#define TEMPLATE_FLAG_TURF_FLAG_NORUINS 8 // if it should forbid ruins from spawning on top of it
#define CUSTOM_ITEM_OBJ 'icons/obj/custom_items_obj.dmi'
#define CUSTOM_ITEM_MOB null
diff --git a/code/_global_vars/mapping.dm b/code/_global_vars/mapping.dm
index 2b11bdc4778..630dab2eb98 100644
--- a/code/_global_vars/mapping.dm
+++ b/code/_global_vars/mapping.dm
@@ -1 +1,2 @@
GLOBAL_LIST_EMPTY(ruin_landmarks)
+GLOBAL_LIST_EMPTY(areas)
diff --git a/code/_helpers/atmospherics.dm b/code/_helpers/atmospherics.dm
index 8fa90bd9c65..e7b79c11b49 100644
--- a/code/_helpers/atmospherics.dm
+++ b/code/_helpers/atmospherics.dm
@@ -54,3 +54,35 @@
return
. += SPAN_WARNING("\The [target] has no gases!")
+
+/turf/proc/get_atmos_adjacent_turfs()
+ var/list/atmos_adjacent_turfs = list()
+ var/canpass = CanZASPass(src)
+ for(var/direction in GLOB.cardinalz)
+ var/turf/current_turf
+ if(direction != UP && direction != DOWN)
+ current_turf = get_step(src, direction)
+ if(direction == UP)
+ current_turf = GetAbove(src)
+ current_turf = istype(current_turf, /turf/simulated/open) ? current_turf : null
+
+ if(direction == DOWN)
+ current_turf = istype(src, /turf/simulated/open) ? GetBelow(src) : null
+
+ if(!istype(current_turf, /turf/simulated)) // not interested in you brother
+ continue
+
+ if(canpass && CanZASPass(current_turf) && !(blocks_air || current_turf.blocks_air))
+ LAZYINITLIST(current_turf.atmos_adjacent_turfs)
+ LAZYINITLIST(atmos_adjacent_turfs)
+ atmos_adjacent_turfs[current_turf] = TRUE
+ current_turf.atmos_adjacent_turfs[src] = TRUE
+ else
+ LAZYREMOVE(atmos_adjacent_turfs, current_turf)
+ if (current_turf.atmos_adjacent_turfs)
+ LAZYREMOVE(current_turf.atmos_adjacent_turfs, src)
+ UNSETEMPTY(current_turf.atmos_adjacent_turfs)
+
+ UNSETEMPTY(atmos_adjacent_turfs)
+ src.atmos_adjacent_turfs = atmos_adjacent_turfs
+ return atmos_adjacent_turfs
diff --git a/code/_helpers/atom_movables.dm b/code/_helpers/atom_movables.dm
index fceac71a518..6836fdeef86 100644
--- a/code/_helpers/atom_movables.dm
+++ b/code/_helpers/atom_movables.dm
@@ -42,3 +42,34 @@
turfs -= get_turf(src)
if(length(turfs))
throw_at(pick(turfs), maxrange, speed, src)
+
+///Returns a chosen path that is the closest to a list of matches
+/proc/pick_closest_path(value, list/matches = get_fancy_list_of_atom_types())
+ if (value == FALSE) //nothing should be calling us with a number, so this is safe
+ value = input("Enter type to find (blank for all, cancel to cancel)", "Search for type") as null|text
+ if (isnull(value))
+ return
+ value = trim(value)
+
+ var/random = FALSE
+ if(findtext(value, "?"))
+ value = replacetext(value, "?", "")
+ random = TRUE
+
+ if(!isnull(value) && value != "")
+ matches = filter_fancy_list(matches, value)
+
+ if(matches.len == 0)
+ return
+
+ var/chosen
+ if(matches.len == 1)
+ chosen = matches[1]
+ else if(random)
+ chosen = pick(matches) || null
+ else
+ chosen = input("Select a type", "Pick Type", matches[1]) as null|anything in sort_list(matches)
+ if(!chosen)
+ return
+ chosen = matches[chosen]
+ return chosen
diff --git a/code/_helpers/type_processing.dm b/code/_helpers/type_processing.dm
new file mode 100644
index 00000000000..85a46fd640f
--- /dev/null
+++ b/code/_helpers/type_processing.dm
@@ -0,0 +1,54 @@
+/proc/make_types_fancy(list/types)
+ if (ispath(types))
+ types = list(types)
+ . = list()
+ for(var/type in types)
+ var/typename = "[type]"
+ var/static/list/TYPES_SHORTCUTS = list(
+ /obj/effect/decal/cleanable = "CLEANABLE",
+ /obj/item/device/radio/headset = "HEADSET",
+ /obj/item/clothing/head/helmet/space = "SPESSHELMET",
+ /obj/item/reagent_containers/vessel/glass = "DRINK", //longest paths comes first
+ /obj/item/reagent_containers/food = "FOOD",
+ /obj/item/reagent_containers = "REAGENT_CONTAINERS",
+ /obj/machinery/atmospherics = "ATMOS_MECH",
+ /obj/machinery/portable_atmospherics = "PORT_ATMOS",
+ /obj/item/mecha_parts/mecha_equipment/weapon/ballistic/missile_rack = "MECHA_MISSILE_RACK",
+ /obj/item/mecha_parts/mecha_equipment = "MECHA_EQUIP",
+ /obj/item/organ = "ORGAN",
+ /obj/item = "ITEM",
+ /obj/machinery = "MACHINERY",
+ /obj/effect = "EFFECT",
+ /obj = "O",
+ /datum = "D",
+ /turf = "T",
+ /mob/living/carbon = "CARBON",
+ /mob/living/simple_animal = "SIMPLE",
+ /mob/living = "LIVING",
+ /mob = "M"
+ )
+ for (var/tn in TYPES_SHORTCUTS)
+ if(copytext(typename, 1, length("[tn]/") + 1) == "[tn]/" /*findtextEx(typename,"[tn]/",1,2)*/ )
+ typename = TYPES_SHORTCUTS[tn] + copytext(typename, length("[tn]/"))
+ break
+ .[typename] = type
+
+/proc/get_fancy_list_of_atom_types()
+ var/static/list/pre_generated_list
+ if (!pre_generated_list) //init
+ pre_generated_list = make_types_fancy(typesof(/atom))
+ return pre_generated_list
+
+/proc/filter_fancy_list(list/L, filter as text)
+ var/list/matches = new
+ var/end_len = -1
+ var/list/endcheck = splittext(filter, "!")
+ if(endcheck.len > 1)
+ filter = endcheck[1]
+ end_len = length_char(filter)
+
+ for(var/key in L)
+ var/value = L[key]
+ if(findtext("[key]", filter, -end_len) || findtext("[value]", filter, -end_len))
+ matches[key] = value
+ return matches
diff --git a/code/controllers/subsystems/liquids.dm b/code/controllers/subsystems/liquids.dm
new file mode 100644
index 00000000000..468026606be
--- /dev/null
+++ b/code/controllers/subsystems/liquids.dm
@@ -0,0 +1,145 @@
+SUBSYSTEM_DEF(liquids)
+ name = "Liquid Turfs"
+ wait = 1 SECONDS
+ flags = SS_KEEP_TIMING | SS_NO_INIT
+ runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
+
+ var/list/active_turfs = list()
+ var/list/currentrun_active_turfs = list()
+
+ var/list/active_groups = list()
+
+ var/list/active_immutables = list()
+
+ var/list/evaporation_queue = list()
+ var/evaporation_counter = 0 //Only process evaporation on intervals
+
+ var/list/processing_fire = list()
+ var/fire_counter = 0 //Only process fires on intervals
+
+ var/list/singleton_immutables = list()
+
+ var/run_type = SSLIQUIDS_RUN_TYPE_TURFS
+
+/datum/controller/subsystem/liquids/proc/get_immutable(type)
+ if(!singleton_immutables[type])
+ var/obj/effect/abstract/liquid_turf/immutable/new_one = new type()
+ singleton_immutables[type] = new_one
+ return singleton_immutables[type]
+
+/datum/controller/subsystem/liquids/Initialize()
+ fire(FALSE, TRUE)
+ . = ..()
+
+/datum/controller/subsystem/liquids/stat_entry(msg)
+ msg += "AT:[active_turfs.len]|AG:[active_groups.len]|AIM:[active_immutables.len]|EQ:[evaporation_queue.len]|PF:[processing_fire.len]"
+ return ..()
+
+
+/datum/controller/subsystem/liquids/fire(resumed = FALSE, no_mc_tick = FALSE)
+ if(run_type == SSLIQUIDS_RUN_TYPE_TURFS)
+ if(!currentrun_active_turfs.len && active_turfs.len)
+ currentrun_active_turfs = active_turfs.Copy()
+ for(var/tur in currentrun_active_turfs)
+ if (no_mc_tick)
+ CHECK_TICK
+ else if (MC_TICK_CHECK)
+ return
+ var/turf/T = tur
+ T.process_liquid_cell()
+ currentrun_active_turfs -= T //work off of index later
+ if(!currentrun_active_turfs.len)
+ run_type = SSLIQUIDS_RUN_TYPE_GROUPS
+ if (run_type == SSLIQUIDS_RUN_TYPE_GROUPS)
+ for(var/g in active_groups)
+ var/datum/liquid_group/LG = g
+ if(LG.dirty)
+ LG.share()
+ LG.dirty = FALSE
+ else if(!LG.amount_of_active_turfs)
+ LG.decay_counter++
+ if(LG.decay_counter >= LIQUID_GROUP_DECAY_TIME)
+ //Perhaps check if any turfs in here can spread before removing it? It's not unlikely they would
+ LG.break_group()
+ if(MC_TICK_CHECK)
+ run_type = SSLIQUIDS_RUN_TYPE_IMMUTABLES //No currentrun here for now
+ return
+ run_type = SSLIQUIDS_RUN_TYPE_IMMUTABLES
+ if(run_type == SSLIQUIDS_RUN_TYPE_IMMUTABLES)
+ for(var/t in active_immutables)
+ var/turf/T = t
+ T.process_immutable_liquid()
+ /*
+ if (no_mc_tick)
+ CHECK_TICK
+ else if (MC_TICK_CHECK)
+ return
+ */
+ run_type = SSLIQUIDS_RUN_TYPE_EVAPORATION
+
+ if(run_type == SSLIQUIDS_RUN_TYPE_EVAPORATION)
+ evaporation_counter++
+ if(evaporation_counter >= REQUIRED_EVAPORATION_PROCESSES)
+ for(var/t in evaporation_queue)
+ var/turf/T = t
+ if(prob(EVAPORATION_CHANCE))
+ T.liquids.process_evaporation()
+ if(MC_TICK_CHECK)
+ return
+ evaporation_counter = 0
+ run_type = SSLIQUIDS_RUN_TYPE_FIRE
+
+ if(run_type == SSLIQUIDS_RUN_TYPE_FIRE)
+ fire_counter++
+ if(fire_counter >= REQUIRED_FIRE_PROCESSES)
+ for(var/t in processing_fire)
+ var/turf/T = t
+ T.liquids.process_fire()
+ if (no_mc_tick)
+ CHECK_TICK
+ else if (MC_TICK_CHECK)
+ return
+ fire_counter = 0
+ run_type = SSLIQUIDS_RUN_TYPE_TURFS
+
+/datum/controller/subsystem/liquids/proc/add_active_turf(turf/T)
+ if(!active_turfs[T])
+ active_turfs[T] = TRUE
+ if(T.lgroup)
+ T.lgroup.amount_of_active_turfs++
+
+/datum/controller/subsystem/liquids/proc/remove_active_turf(turf/T)
+ if(active_turfs[T])
+ active_turfs -= T
+ if(T.lgroup)
+ T.lgroup.amount_of_active_turfs--
+
+/client/proc/toggle_liquid_debug()
+ set category = "Debug"
+ set name = "Liquid Groups Color Debug"
+ set desc = "Liquid Groups Color Debug."
+ if(!holder)
+ return
+ GLOB.liquid_debug_colors = !GLOB.liquid_debug_colors
+
+/proc/mix_color_from_reagent_list(list/reagent_list)
+ var/mixcolor
+ var/vol_counter = 0
+ var/vol_temp
+ var/cached_color
+ var/datum/reagent/raw_reagent
+
+ for(var/reagent_type in reagent_list)
+ vol_temp = reagent_list[reagent_type]
+ vol_counter += vol_temp
+ raw_reagent = reagent_type // Not initialized
+ cached_color = initial(raw_reagent.color)
+
+ if(!mixcolor)
+ mixcolor = cached_color
+ else if (length(mixcolor) >= length(cached_color))
+ mixcolor = BlendRGB(mixcolor, cached_color, vol_temp/vol_counter)
+ else
+ mixcolor = BlendRGB(cached_color, mixcolor, vol_temp/vol_counter)
+
+ return mixcolor
diff --git a/code/controllers/subsystems/mapping.dm b/code/controllers/subsystems/mapping.dm
index ed427bfbc3f..7ae27d6b4b3 100644
--- a/code/controllers/subsystems/mapping.dm
+++ b/code/controllers/subsystems/mapping.dm
@@ -6,6 +6,9 @@ SUBSYSTEM_DEF(mapping)
var/list/map_templates = list()
var/list/holodeck_templates = list()
+ ///All possible biomes in assoc list as type || instance
+ var/list/biomes = list()
+
/datum/controller/subsystem/mapping/Initialize(timeofday)
preloadTemplates()
preloadHolodeckTemplates()
diff --git a/code/datums/mapgen/_MapGenerator.dm b/code/datums/mapgen/_MapGenerator.dm
new file mode 100644
index 00000000000..a3c5fb299fc
--- /dev/null
+++ b/code/datums/mapgen/_MapGenerator.dm
@@ -0,0 +1,8 @@
+///This type is responsible for any map generation behavior that is done in areas, override this to allow for area-specific map generation. This generation is ran by areas in initialize.
+/datum/map_generator
+
+///This proc will be ran by areas on Initialize, and provides the areas turfs as argument to allow for generation.
+/datum/map_generator/proc/generate_terrain(list/turfs, area/generate_in)
+ return
+
+/proc/perlin_noise(seed, x, y)
diff --git a/code/datums/mapgen/biomes/_biome.dm b/code/datums/mapgen/biomes/_biome.dm
new file mode 100644
index 00000000000..cf2d68de9e1
--- /dev/null
+++ b/code/datums/mapgen/biomes/_biome.dm
@@ -0,0 +1,26 @@
+///This datum handles the transitioning from a turf to a specific biome, and handles spawning decorative structures and mobs.
+/datum/biome
+ ///Type of turf this biome creates
+ var/turf_type
+ ///Chance of having a structure from the flora types list spawn
+ var/flora_density = 0
+ ///Chance of having a mob from the fauna types list spawn
+ var/fauna_density = 0
+ ///list of type paths of objects that can be spawned when the turf spawns flora
+ var/list/flora_types = list(/turf/simulated/floor/natural/jungle/sand)
+ ///list of type paths of mobs that can be spawned when the turf spawns fauna
+ var/list/fauna_types = list()
+
+///This proc handles the creation of a turf of a specific biome type
+/datum/biome/proc/generate_turf(turf/gen_turf)
+ gen_turf.ChangeTurf(turf_type)
+ if(length(fauna_types) && prob(fauna_density))
+ var/mob/fauna = pick(fauna_types)
+ new fauna(gen_turf)
+
+ if(length(flora_types) && prob(flora_density))
+ var/obj/structure/flora = pick(flora_types)
+ new flora(gen_turf)
+
+/datum/biome/water
+ turf_type = /turf/simulated/floor/natural/jungle/water
diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm
index 26705fb48bb..704198bb7d0 100644
--- a/code/game/area/areas.dm
+++ b/code/game/area/areas.dm
@@ -20,10 +20,13 @@
var/importance = 1
var/loyalty = 0
+ ///This datum, if set, allows terrain generation behavior to be ran on Initialize()
+ var/datum/map_generator/map_generator
+
/area/New()
icon_state = ""
uid = ++global_uid
-
+ GLOB.areas += src
if(!requires_power)
power_light = 0
power_equip = 0
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 43dcf6f72ac..ab0d2562915 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -323,7 +323,6 @@ its easier to just keep the beam vertical.
/atom/proc/examine(...)
SHOULD_NOT_OVERRIDE(TRUE)
-
var/content = "
"
content += _examine_text(arglist(args))
diff --git a/code/game/objects/items/storage/lockbox.dm b/code/game/objects/items/storage/lockbox.dm
index dfa7a134902..847d6d5b239 100644
--- a/code/game/objects/items/storage/lockbox.dm
+++ b/code/game/objects/items/storage/lockbox.dm
@@ -142,4 +142,3 @@
broken = !broken
update_icon()
return
-
diff --git a/code/game/objects/items/weapons/mop.dm b/code/game/objects/items/weapons/mop.dm
index 5b05d354eb7..665aeeded22 100644
--- a/code/game/objects/items/weapons/mop.dm
+++ b/code/game/objects/items/weapons/mop.dm
@@ -14,10 +14,19 @@
/obj/item/mop/New()
create_reagents(30)
+ AddComponent(/datum/component/liquids_interaction, /obj/item/mop/proc/attack_on_liquids_turf)
/obj/item/mop/afterattack(atom/A, mob/user, proximity)
if(!proximity)
return
+
+ var/turf/turf_to_clean = A
+
+ // Disable normal cleaning if there are liquids.
+ if(isturf(A) && turf_to_clean.liquids)
+ SEND_SIGNAL(src, SIGNAL_CLEAN_LIQUIDS, turf_to_clean, user)
+ return FALSE
+
if(istype(A, /turf) || istype(A, /obj/effect/decal/cleanable) || istype(A, /obj/effect/overlay) || istype(A, /obj/effect/rune))
if(reagents.total_volume < 1)
to_chat(user, "Your mop is dry!")
@@ -38,3 +47,27 @@
if(istype(I, /obj/item/mop) || istype(I, /obj/item/soap))
return
..()
+
+/**
+ * Proc to remove liquids from a turf using a mop.
+ *
+ * Arguments:
+ * * tile - On which tile we're trying to absorb liquids
+ * * user - Who tries to absorb liquids with this?
+ * * liquids - Liquids we're trying to absorb.
+ */
+/obj/item/mop/proc/attack_on_liquids_turf(turf/tile, mob/user, obj/effect/abstract/liquid_turf/liquids)
+ if(!in_range(user, tile))
+ return FALSE
+
+ var/free_space = reagents.maximum_volume - reagents.total_volume
+ if(free_space <= 0)
+ to_chat(user, SPAN_WARNING("Your [src] can't absorb any more liquid!"))
+ return TRUE
+
+ var/datum/reagents/tempr = liquids.take_reagents_flat(free_space)
+ tempr.trans_to_obj(src, tempr.total_volume)
+ to_chat(user, SPAN_NOTICE("You soak \the [src] with some liquids."))
+ qdel(tempr)
+ user.setClickCooldown(DEFAULT_ATTACK_COOLDOWN)
+ return TRUE
diff --git a/code/game/objects/items/weapons/towels.dm b/code/game/objects/items/weapons/towels.dm
index 199715e4b4b..29ee323d01e 100644
--- a/code/game/objects/items/weapons/towels.dm
+++ b/code/game/objects/items/weapons/towels.dm
@@ -15,10 +15,25 @@
drop_sound = SFX_DROP_CLOTH
pickup_sound = SFX_PICKUP_CLOTH
+/obj/item/towel/Initialize()
+ . = ..()
+ AddComponent(/datum/component/liquids_interaction, /obj/item/towel/proc/attack_on_liquids_turf)
+
/obj/item/towel/attack_self(mob/living/user as mob)
user.visible_message(text("[] uses [] to towel themselves off.", user, src))
playsound(user, 'sound/weapons/towelwipe.ogg', 25, 1)
+/obj/item/towel/afterattack(atom/A, mob/user, proximity)
+ if(!proximity)
+ return
+
+ var/turf/turf_to_clean = A
+
+ // Disable normal cleaning if there are liquids.
+ if(isturf(A) && turf_to_clean.liquids)
+ SEND_SIGNAL(src, SIGNAL_CLEAN_LIQUIDS, turf_to_clean, user)
+ return
+
/obj/item/towel/random/New()
..()
color = get_random_colour()
@@ -29,3 +44,33 @@
color = "#ffd700"
force = 1
attack_verb = list("smote")
+
+/**
+ * The procedure for remove liquids from turf
+ *
+ * The object is called from liquid_interaction element.
+ * The procedure check range of mop owner and tile, then check reagents in mop, if reagents volume < mop capacity - liquids absorbs from tile
+ * In another way, input a chat about mop capacity
+ * Arguments:
+ * * towel - Towel used to absorb liquids
+ * * tile - On which tile the towel will try to absorb liquids
+ * * user - Who tries to absorb liquids with the towel
+ * * liquids - Liquids that user tries to absorb with the towel
+ */
+/obj/item/towel/proc/attack_on_liquids_turf(turf/tile, mob/user, obj/effect/abstract/liquid_turf/liquids)
+ if(!in_range(user, tile))
+ return FALSE
+
+ var/free_space = reagents.maximum_volume - reagents.total_volume
+ if(free_space <= 0)
+ to_chat(user, SPAN_WARNING("Your [src] can't absorb any more liquid!"))
+ return TRUE
+
+ var/datum/reagents/temp_holder = liquids.take_reagents_flat(free_space)
+ temp_holder.trans_to_obj(src, temp_holder.total_volume)
+
+ to_chat(user, SPAN_NOTICE("You soak \the [src] with some liquids."))
+
+ qdel(temp_holder)
+ user.setClickCooldown(DEFAULT_ATTACK_COOLDOWN)
+ return TRUE
diff --git a/code/game/objects/structures/mop_bucket.dm b/code/game/objects/structures/mop_bucket.dm
index f209cc1da73..8f1fb5c7861 100644
--- a/code/game/objects/structures/mop_bucket.dm
+++ b/code/game/objects/structures/mop_bucket.dm
@@ -21,6 +21,20 @@
if(get_dist(src, user) <= 1)
. += "\n[src] \icon[src] contains [reagents.total_volume] unit\s of water!"
+/obj/structure/mopbucket/ShiftClick(mob/user)
+ . = ..()
+ var/obj/O = user.get_active_hand()
+ if(istype(O, /obj/item/mop))
+ if(O.reagents.total_volume == 0)
+ to_chat(user, "[O] is dry, you can't squeeze anything out!")
+ return
+ if(reagents.total_volume == reagents.maximum_volume)
+ to_chat(user, "[src] is full!")
+ return
+ O.reagents.remove_any(O.reagents.total_volume * SQUEEZING_DISPERSAL_RATIO)
+ O.reagents.trans_to(src, O.reagents.total_volume)
+ to_chat(user, "You squeeze the liquids from [O] to [src].")
+
/obj/structure/mopbucket/attackby(obj/item/I, mob/user)
if(istype(I, /obj/item/mop))
if(reagents.total_volume < 1)
diff --git a/code/game/objects/structures/rock.dm b/code/game/objects/structures/rock.dm
index 9b35450356d..2a73ff4da32 100644
--- a/code/game/objects/structures/rock.dm
+++ b/code/game/objects/structures/rock.dm
@@ -9,18 +9,20 @@
var/list/iconlist = list("asteroid_bigstone1","asteroid_bigstone2","asteroid_bigstone3","asteroid_bigstone4")
var/health = 40
var/last_act = 0
+ var/harvest_amount_low = 2
+ var/harvest_amount_high = 6
+ var/mineralSpawnChanceList = list(uranium = 10, osmium = 10, iron = 20, coal = 20, diamond = 2, gold = 10, silver = 10, plasma = 20)
/obj/structure/rock/New()
..()
icon_state = pick(iconlist)
/obj/structure/rock/Destroy()
- var/mineralSpawnChanceList = list(uranium = 10, osmium = 10, iron = 20, coal = 20, diamond = 2, gold = 10, silver = 10, plasma = 20)
if(prob(20))
var/mineral_name = util_pick_weight(mineralSpawnChanceList) //temp mineral name
mineral_name = lowertext(mineral_name)
var/ore = text2path("/obj/item/ore/[mineral_name]")
- for(var/i=1,i <= rand(2,6),i++)
+ for(var/i=1,i <= rand(harvest_amount_low, harvest_amount_high),i++)
new ore(get_turf(src))
return ..()
diff --git a/code/game/objects/structures/watercloset.dm b/code/game/objects/structures/watercloset.dm
index 59e2a7d8f2b..d491c9a1c29 100644
--- a/code/game/objects/structures/watercloset.dm
+++ b/code/game/objects/structures/watercloset.dm
@@ -425,6 +425,19 @@
for(var/mob/V in viewers(src, null))
V.show_message("[user] washes their hands using \the [src].")
+/obj/structure/sink/ShiftClick(mob/user)
+ . = ..()
+ var/obj/O = user.get_active_hand()
+ if(istype(O, /obj/item/mop))
+ if(O.reagents.total_volume == 0)
+ to_chat(user, "[O] is dry, you can't squeeze anything out!")
+ return
+ if(reagents.total_volume == reagents.maximum_volume)
+ to_chat(user, "[src] is full!")
+ return
+ O.reagents.remove_any(O.reagents.total_volume * SQUEEZING_DISPERSAL_RATIO)
+ O.reagents.trans_to(src, O.reagents.total_volume)
+ to_chat(user, "You squeeze the liquids from [O] to [src].")
/obj/structure/sink/attackby(obj/item/O as obj, mob/living/user as mob)
if(busy)
diff --git a/code/game/turfs/simulated/wall_attacks.dm b/code/game/turfs/simulated/wall_attacks.dm
index bba402077fe..ea052fe1f87 100644
--- a/code/game/turfs/simulated/wall_attacks.dm
+++ b/code/game/turfs/simulated/wall_attacks.dm
@@ -18,6 +18,7 @@
set_opacity(0)
for(var/turf/simulated/turf in loc)
SSair.mark_for_update(turf)
+ turf.liquid_update_turf()
else
can_open = WALL_OPENING
//flick("[material.icon_base]fwall_closing", src)
@@ -33,6 +34,7 @@
shove_everything()
for(var/turf/simulated/turf in loc)
SSair.mark_for_update(turf)
+ turf.liquid_update_turf()
can_open = WALL_CAN_OPEN
update_icon()
@@ -44,6 +46,7 @@
for(var/turf/simulated/turf in loc)
update_thermal(turf)
SSair.mark_for_update(turf)
+ turf.liquid_update_turf()
/turf/simulated/wall/proc/update_thermal(turf/simulated/source)
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index 818e23b2943..5d9277dc1ec 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -38,6 +38,8 @@
var/changing_turf
var/footstep_sound = SFX_FOOTSTEP_PLATING
+ ///list of turfs adjacent to us that air can flow onto
+ var/list/atmos_adjacent_turfs
var/turf_height = 0 // "Vertical" offset. Mostly used for mobs and dropped items.
@@ -57,6 +59,7 @@
luminosity = 1
RecalculateOpacity()
+ get_atmos_adjacent_turfs()
/turf/Destroy()
if(!changing_turf)
diff --git a/code/game/turfs/turf_changing.dm b/code/game/turfs/turf_changing.dm
index 19c708cec11..14ac4727c30 100644
--- a/code/game/turfs/turf_changing.dm
+++ b/code/game/turfs/turf_changing.dm
@@ -94,7 +94,7 @@
GLOB.universe.OnTurfChange(W)
SSair.mark_for_update(src) //handle the addition of the new turf.
-
+ liquid_update_turf()
for(var/turf/space/S in range(W,1))
S.update_starlight()
diff --git a/code/modules/ZAS/Atom.dm b/code/modules/ZAS/Atom.dm
index 70d4847aacc..07036b95620 100644
--- a/code/modules/ZAS/Atom.dm
+++ b/code/modules/ZAS/Atom.dm
@@ -50,6 +50,7 @@
/atom/movable/proc/update_nearby_tiles(need_rebuild)
for(var/turf/simulated/turf in locs)
SSair.mark_for_update(turf)
+ turf.liquid_update_turf()
return 1
diff --git a/code/modules/ZAS/Fire.dm b/code/modules/ZAS/Fire.dm
index 94347104ec5..dafc6e36024 100644
--- a/code/modules/ZAS/Fire.dm
+++ b/code/modules/ZAS/Fire.dm
@@ -34,6 +34,7 @@ If it gains pressure too slowly, it may leak or just rupture instead of explodin
igniting = 1
create_fire(exposed_temperature)
+ liquids?.fire_act()
return igniting
/zone/proc/process_fire()
diff --git a/code/modules/ZAS/Zone.dm b/code/modules/ZAS/Zone.dm
index 67fe6e15abb..9a3da0d9c0b 100644
--- a/code/modules/ZAS/Zone.dm
+++ b/code/modules/ZAS/Zone.dm
@@ -135,6 +135,7 @@ Class Procs:
continue //don't need to rebuild this edge
for(var/turf/T in E.connecting_turfs)
SSair.mark_for_update(T)
+ T.liquid_update_turf()
/zone/proc/c_invalidate()
invalid = 1
@@ -152,6 +153,7 @@ Class Procs:
//T.dbg(invalid_zone)
T.needs_air_update = 0 //Reset the marker so that it will be added to the list.
SSair.mark_for_update(T)
+ T.liquid_update_turf()
/zone/proc/add_tile_air(datum/gas_mixture/tile_air)
//air.volume += CELL_VOLUME
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index d5833b80190..085e96d2fda 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -143,7 +143,9 @@ var/list/admin_verbs_fun = list(
/client/proc/create_dungeon,
/datum/admins/proc/ai_hologram_set,
/client/proc/projectile_basketball,
- /client/proc/toggle_possess_mode
+ /client/proc/toggle_possess_mode,
+ /client/proc/spawn_liquid,
+ /client/proc/remove_liquid,
)
var/list/admin_verbs_spawn = list(
diff --git a/code/modules/liquids/drains.dm b/code/modules/liquids/drains.dm
new file mode 100644
index 00000000000..f1ff4d4327a
--- /dev/null
+++ b/code/modules/liquids/drains.dm
@@ -0,0 +1,85 @@
+//Structure as this doesn't need any power to work
+/obj/structure/drain
+ name = "drain"
+ icon = 'icons/obj/liquids/structures/drains.dmi'
+ icon_state = "drain"
+ desc = "Drainage inlet embedded in the floor to prevent flooding."
+ density = FALSE
+ plane = TURF_PLANE
+ layer = EXPOSED_PIPE_LAYER
+ anchored = TRUE
+ var/processing = FALSE
+ var/drain_flat = 5
+ var/drain_percent = 0.1
+ var/welded = FALSE
+ var/turf/my_turf //need to keep track of it for the signal, if in any bizarre cases something would be moving the drain
+
+/obj/structure/drain/update_icon()
+ . = ..()
+ if(welded)
+ icon_state = "[initial(icon_state)]_welded"
+ else
+ icon_state = "[initial(icon_state)]"
+
+/obj/structure/drain/attackby(obj/item/O, mob/user)
+ . = ..()
+ if(isWelder(O))
+ welder_act(user, O)
+
+/obj/structure/drain/proc/welder_act(mob/living/user, obj/item/weldingtool/WT)
+ if(!WT.use_tool(src, user, delay= 4 SECONDS, amount = 5))
+ return
+
+ playsound(src, 'sound/items/welder2.ogg', 50, TRUE)
+ to_chat(user, SPAN_NOTICE("You start [welded ? "unwelding" : "welding"] [src]..."))
+ if(do_after(user, 40, src))
+ if(!src || !user || !WT.remove_fuel(5, user)) return
+ to_chat(user, SPAN_NOTICE("You [welded ? "unweld" : "weld"] [src]."))
+ welded = !welded
+ update_icon()
+ if(welded)
+ if(processing)
+ processing = FALSE
+ set_next_think(0)
+ else if (my_turf.liquids)
+ set_next_think(world.time+1)
+ processing = TRUE
+ return TRUE
+
+/obj/structure/drain/think()
+ if(!my_turf.liquids || my_turf.liquids.immutable)
+ set_next_think(0)
+ processing = FALSE
+ return
+ my_turf.liquids.liquid_simple_delete_flat(drain_flat + (drain_percent * my_turf.liquids.total_reagents))
+ set_next_think(world.time+1)
+
+/obj/structure/drain/Initialize(mapload)
+ . = ..()
+ if(!isturf(loc))
+ CRASH("Drain structure initialized not on a turf")
+ my_turf = loc
+ register_signal(my_turf, SIGNAL_TURF_LIQUIDS_CREATION, .proc/liquids_signal)
+ if(my_turf.liquids)
+ set_next_think(world.time+1)
+ processing = TRUE
+
+/obj/structure/drain/proc/liquids_signal()
+ SIGNAL_HANDLER
+ if(processing || welded)
+ return
+ set_next_think(world.time+1)
+ processing = TRUE
+
+/obj/structure/drain/Destroy()
+ if(processing)
+ set_next_think(0)
+ unregister_signal(my_turf, SIGNAL_TURF_LIQUIDS_CREATION)
+ my_turf = null
+ return ..()
+
+/obj/structure/drain/big
+ desc = "Drainage inlet embedded in the floor to prevent flooding. This one seems large."
+ icon_state = "bigdrain"
+ drain_percent = 0.3
+ drain_flat = 15
diff --git a/code/modules/liquids/height_floors.dm b/code/modules/liquids/height_floors.dm
new file mode 100644
index 00000000000..825616954ac
--- /dev/null
+++ b/code/modules/liquids/height_floors.dm
@@ -0,0 +1,27 @@
+
+/turf/simulated/floor/smoothable/iron/pool
+ name = "pool floor"
+ //tile_type = /obj/item/stack/tile/iron/pool
+ icon = 'icons/turf/pool_tile.dmi'
+ base_icon_state = "pool_tile"
+ icon_state = "pool_tile"
+ liquid_height = -30
+ turf_height = -30
+
+/turf/simulated/floor/smoothable/iron/elevated
+ name = "elevated floor"
+ //tile_type = /obj/item/stack/tile/iron/elevated
+ icon = 'icons/turf/elevated_plasteel.dmi'
+ icon_state = "elevated_plasteel-0"
+ base_icon_state = "elevated_plasteel"
+ liquid_height = 30
+ turf_height = 30
+
+/turf/simulated/floor/smoothable/iron/lowered
+ name = "lowered floor"
+ //tile_type = /obj/item/stack/tile/iron/lowered
+ icon = 'icons/turf/lowered_plasteel.dmi'
+ icon_state = "lowered_plasteel-0"
+ base_icon_state = "lowered_plasteel"
+ liquid_height = -30
+ turf_height = -30
diff --git a/code/modules/liquids/liquid_systems/liquid_effect.dm b/code/modules/liquids/liquid_systems/liquid_effect.dm
new file mode 100644
index 00000000000..2d852a89093
--- /dev/null
+++ b/code/modules/liquids/liquid_systems/liquid_effect.dm
@@ -0,0 +1,621 @@
+/obj/effect/abstract/liquid_turf
+ name = "liquid"
+ icon = 'icons/effects/liquid.dmi'
+ icon_state = "water-0"
+ anchored = TRUE
+ plane = TURF_PLANE
+ layer = SHALLOW_FLUID_LAYER
+ color = "#DDF"
+
+ //For being on fire
+ var/light_range = 0
+ var/light_power = 1
+ light_color = "#FAA019"
+
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ var/height = 1
+ var/only_big_diffs = 1
+ var/turf/my_turf
+ var/liquid_state = LIQUID_STATE_PUDDLE
+ var/has_cached_share = FALSE
+
+ var/attrition = 0
+
+ var/immutable = FALSE
+
+ var/list/reagent_list = list()
+ var/total_reagents = 0
+ var/temp = 20 CELSIUS
+
+ var/fire_state = LIQUID_FIRE_STATE_NONE
+
+ var/no_effects = FALSE
+
+
+/obj/effect/abstract/liquid_turf/proc/check_fire(hotspotted = FALSE)
+ var/my_burn_power = get_burn_power(hotspotted)
+ if(!my_burn_power)
+ if(fire_state)
+ //Set state to 0
+ set_fire_state(LIQUID_FIRE_STATE_NONE)
+ return FALSE
+ //Calculate appropriate state
+ var/new_state = LIQUID_FIRE_STATE_SMALL
+ switch(my_burn_power)
+ if(0 to 7)
+ new_state = LIQUID_FIRE_STATE_SMALL
+ if(7 to 8)
+ new_state = LIQUID_FIRE_STATE_MILD
+ if(8 to 9)
+ new_state = LIQUID_FIRE_STATE_MEDIUM
+ if(9 to 10)
+ new_state = LIQUID_FIRE_STATE_HUGE
+ if(10 to INFINITY)
+ new_state = LIQUID_FIRE_STATE_INFERNO
+
+ if(fire_state != new_state)
+ set_fire_state(new_state)
+
+ return TRUE
+
+/obj/effect/abstract/liquid_turf/proc/set_fire_state(new_state)
+ fire_state = new_state
+ switch(fire_state)
+ if(LIQUID_FIRE_STATE_NONE)
+ light_outer_range = 0
+ if(LIQUID_FIRE_STATE_SMALL)
+ light_outer_range = LIGHT_RANGE_FIRE
+ if(LIQUID_FIRE_STATE_MILD)
+ light_outer_range = LIGHT_RANGE_FIRE
+ if(LIQUID_FIRE_STATE_MEDIUM)
+ light_outer_range = LIGHT_RANGE_FIRE
+ if(LIQUID_FIRE_STATE_HUGE)
+ light_outer_range = LIGHT_RANGE_FIRE
+ if(LIQUID_FIRE_STATE_INFERNO)
+ light_outer_range = LIGHT_RANGE_FIRE
+ update_light()
+ update_overlays()
+
+/obj/effect/abstract/liquid_turf/proc/get_burn_power(hotspotted = FALSE)
+ //We are not on fire and werent ignited by a hotspot exposure, no fire pls
+ if(!hotspotted && !fire_state)
+ return FALSE
+ var/total_burn_power = 0
+ var/datum/reagent/R //Faster declaration
+ for(var/reagent_type in reagent_list)
+ R = reagent_type
+ var/burn_power = initial(R.liquid_fire_power)
+ if(burn_power)
+ total_burn_power += burn_power * reagent_list[reagent_type]
+ if(!total_burn_power)
+ return FALSE
+ total_burn_power /= total_reagents //We get burn power per unit.
+ if(total_burn_power <= REQUIRED_FIRE_POWER_PER_UNIT)
+ return FALSE
+ //Finally, we burn
+ return total_burn_power
+
+/obj/effect/abstract/liquid_turf/proc/process_fire()
+ if(!fire_state)
+ SSliquids.processing_fire -= my_turf
+ set_fire_state(LIQUID_FIRE_STATE_NONE)
+ var/old_state = fire_state
+ if(!check_fire())
+ SSliquids.processing_fire -= my_turf
+ //Try spreading
+ if(fire_state == old_state) //If an extinguisher made our fire smaller, dont spread, else it's too hard to put out
+ for(var/turf/T in my_turf.atmos_adjacent_turfs)
+ if(T.liquids && !T.liquids.fire_state && T.liquids.check_fire(TRUE))
+ SSliquids.processing_fire[T] = TRUE
+ //Burn our resources
+ var/datum/reagent/R //Faster declaration
+ var/burn_rate
+ for(var/reagent_type in reagent_list)
+ R = reagent_type
+ burn_rate = initial(R.liquid_fire_burnrate)
+ if(burn_rate)
+ var/amt = reagent_list[reagent_type]
+ if(burn_rate >= amt)
+ reagent_list -= reagent_type
+ total_reagents -= amt
+ else
+ reagent_list[reagent_type] -= burn_rate
+ total_reagents -= burn_rate
+
+ my_turf.hotspot_expose((20 CELSIUS+50) + (50*fire_state), 125)
+ for(var/A in my_turf.contents)
+ var/atom/AT = A
+ if(!QDELETED(AT))
+ AT.fire_act((20 CELSIUS+50) + (50*fire_state), 125)
+
+ if(reagent_list.len == 0)
+ qdel(src, TRUE)
+ else
+ has_cached_share = FALSE
+ if(!my_turf.lgroup)
+ calculate_height()
+ set_reagent_color_for_liquid()
+
+/obj/effect/abstract/liquid_turf/proc/process_evaporation()
+ if(immutable)
+ SSliquids.evaporation_queue -= my_turf
+ return
+ //We're in a group. dont try and evaporate
+ if(my_turf.lgroup)
+ SSliquids.evaporation_queue -= my_turf
+ return
+ if(liquid_state != LIQUID_STATE_PUDDLE)
+ SSliquids.evaporation_queue -= my_turf
+ return
+ //See if any of our reagents evaporates
+ var/any_change = FALSE
+ var/datum/reagent/R //Faster declaration
+ for(var/reagent_type in reagent_list)
+ R = reagent_type
+ //We evaporate. bye bye
+ if(initial(R.evaporates))
+ total_reagents -= reagent_list[reagent_type]
+ reagent_list -= reagent_type
+ any_change = TRUE
+ if(!any_change)
+ SSliquids.evaporation_queue -= my_turf
+ return
+ //No total reagents. Commit death
+ if(reagent_list.len == 0)
+ qdel(src, TRUE)
+ //Reagents still left. Recalculte height and color and remove us from the queue
+ else
+ has_cached_share = FALSE
+ SSliquids.evaporation_queue -= my_turf
+ calculate_height()
+ set_reagent_color_for_liquid()
+
+/**
+ * Makes and returns the liquid effect overlay.
+ *
+ * Arguments:
+ * * overlay_state - the icon state of the new overlay
+ * * overlay_layer - the layer
+ * * overlay_plane - the plane
+ */
+/obj/effect/abstract/liquid_turf/proc/make_liquid_overlay(overlay_state, overlay_layer, overlay_plane)
+ PRIVATE_PROC(TRUE)
+
+ return mutable_appearance(
+ 'icons/effects/liquid_overlays.dmi',
+ overlay_state
+ )
+
+/**
+ * Returns a list of over and underlays for different liquid states.
+ *
+ * Arguments:
+ * * state - the stage number.
+ * * has_top - if this stage has a top.
+ */
+/obj/effect/abstract/liquid_turf/proc/make_state_layer(state, has_top)
+ PRIVATE_PROC(TRUE)
+
+ . = list(make_liquid_overlay("stage[state]_bottom", DEEP_FLUID_LAYER, DEFAULT_PLANE))
+
+ if(!has_top)
+ return
+
+ . += make_liquid_overlay("stage[state]_top", BELOW_OBJ_LAYER, DEFAULT_PLANE)
+
+/obj/effect/abstract/liquid_turf/proc/set_new_liquid_state(new_state)
+ liquid_state = new_state
+ update_overlays()
+
+/obj/effect/abstract/liquid_turf/proc/update_overlays()
+
+ if(no_effects)
+ return
+ src.overlays.Cut()
+ var/mutable_appearance/liquid_state_overlay
+ switch(liquid_state)
+ if(LIQUID_STATE_ANKLES)
+ liquid_state_overlay+= make_state_layer(1, has_top = TRUE)
+ if(LIQUID_STATE_WAIST)
+ liquid_state_overlay += make_state_layer(2, has_top = TRUE)
+ if(LIQUID_STATE_SHOULDERS)
+ liquid_state_overlay += make_state_layer(3, has_top = TRUE)
+ if(LIQUID_STATE_FULLTILE)
+ liquid_state_overlay += make_state_layer(4, has_top = FALSE)
+
+ src.overlays += liquid_state_overlay
+
+ var/mutable_appearance/shine = mutable_appearance(icon, "shine", flags = RESET_COLOR|RESET_ALPHA)
+ shine.alpha = 32
+ src.overlays += shine
+
+ //Add a fire overlay too
+
+ if(fire_state == LIQUID_FIRE_STATE_NONE)
+ return
+
+ var/fire_icon_state
+ switch(fire_state)
+ if(LIQUID_FIRE_STATE_SMALL)
+ fire_icon_state = "fire_small"
+ if(LIQUID_FIRE_STATE_MILD)
+ fire_icon_state = "fire_small"
+ if(LIQUID_FIRE_STATE_MEDIUM)
+ fire_icon_state = "fire_medium"
+ if(LIQUID_FIRE_STATE_HUGE)
+ fire_icon_state = "fire_big"
+ if(LIQUID_FIRE_STATE_INFERNO)
+ fire_icon_state = "fire_big"
+
+ src.overlays += mutable_appearance(icon, fire_icon_state, flags = RESET_COLOR|RESET_ALPHA)
+
+//Takes a flat of our reagents and returns it, possibly qdeling our liquids
+/obj/effect/abstract/liquid_turf/proc/take_reagents_flat(flat_amount)
+ var/datum/reagents/tempr = new /datum/reagents(10000, GLOB.temp_reagents_holder)
+ if(flat_amount >= total_reagents)
+ tempr.add_noreact_reagent_list(reagent_list)
+ qdel(src, TRUE)
+ else
+ var/fraction = flat_amount/total_reagents
+ var/passed_list = list()
+ for(var/reagent_type in reagent_list)
+ var/amount = fraction * reagent_list[reagent_type]
+ reagent_list[reagent_type] -= amount
+ total_reagents -= amount
+ passed_list[reagent_type] = amount
+ tempr.add_noreact_reagent_list(passed_list)
+ has_cached_share = FALSE
+
+ return tempr
+
+/obj/effect/abstract/liquid_turf/immutable/take_reagents_flat(flat_amount)
+ return simulate_reagents_flat(flat_amount)
+
+//Returns a reagents holder with all the reagents with a higher volume than the threshold
+/obj/effect/abstract/liquid_turf/proc/simulate_reagents_threshold(amount_threshold)
+ var/datum/reagents/tempr = new /datum/reagents(10000, GLOB.temp_reagents_holder)
+ var/passed_list = list()
+ for(var/reagent_type in reagent_list)
+ var/amount = reagent_list[reagent_type]
+ if(amount_threshold && amount < amount_threshold)
+ continue
+ passed_list[reagent_type] = amount
+ tempr.add_noreact_reagent_list(passed_list)
+
+ return tempr
+
+//Returns a flat of our reagents without any effects on the liquids
+/obj/effect/abstract/liquid_turf/proc/simulate_reagents_flat(flat_amount)
+ var/datum/reagents/tempr = new /datum/reagents(10000, GLOB.temp_reagents_holder)
+ if(flat_amount >= total_reagents)
+ tempr.add_noreact_reagent_list(reagent_list)
+ else
+ var/fraction = flat_amount/total_reagents
+ var/passed_list = list()
+ for(var/reagent_type in reagent_list)
+ var/amount = fraction * reagent_list[reagent_type]
+ passed_list[reagent_type] = amount
+ tempr.add_noreact_reagent_list(passed_list)
+
+ return tempr
+
+/obj/effect/abstract/liquid_turf/fire_act(temperature, volume)
+ if(!fire_state)
+ if(check_fire(TRUE))
+ SSliquids.processing_fire[my_turf] = TRUE
+
+/obj/effect/abstract/liquid_turf/proc/set_reagent_color_for_liquid()
+ color = mix_color_from_reagent_list(reagent_list)
+
+/obj/effect/abstract/liquid_turf/proc/calculate_height()
+ var/new_height = CEILING(total_reagents, 1)/LIQUID_HEIGHT_DIVISOR
+ set_height(new_height)
+ var/determined_new_state
+ //We add the turf height if it's positive to state calculations
+ if(my_turf.turf_height > 0)
+ new_height += my_turf.turf_height
+ switch(new_height)
+ if(0 to LIQUID_ANKLES_LEVEL_HEIGHT-1)
+ determined_new_state = LIQUID_STATE_PUDDLE
+ if(LIQUID_ANKLES_LEVEL_HEIGHT to LIQUID_WAIST_LEVEL_HEIGHT-1)
+ determined_new_state = LIQUID_STATE_ANKLES
+ if(LIQUID_WAIST_LEVEL_HEIGHT to LIQUID_SHOULDERS_LEVEL_HEIGHT-1)
+ determined_new_state = LIQUID_STATE_WAIST
+ if(LIQUID_SHOULDERS_LEVEL_HEIGHT to LIQUID_FULLTILE_LEVEL_HEIGHT-1)
+ determined_new_state = LIQUID_STATE_SHOULDERS
+ if(LIQUID_FULLTILE_LEVEL_HEIGHT to INFINITY)
+ determined_new_state = LIQUID_STATE_FULLTILE
+ if(determined_new_state != liquid_state)
+ set_new_liquid_state(determined_new_state)
+
+/obj/effect/abstract/liquid_turf/immutable/calculate_height()
+ var/new_height = CEILING(total_reagents, 1)/LIQUID_HEIGHT_DIVISOR
+ set_height(new_height)
+ var/determined_new_state
+ switch(new_height)
+ if(0 to LIQUID_ANKLES_LEVEL_HEIGHT-1)
+ determined_new_state = LIQUID_STATE_PUDDLE
+ if(LIQUID_ANKLES_LEVEL_HEIGHT to LIQUID_WAIST_LEVEL_HEIGHT-1)
+ determined_new_state = LIQUID_STATE_ANKLES
+ if(LIQUID_WAIST_LEVEL_HEIGHT to LIQUID_SHOULDERS_LEVEL_HEIGHT-1)
+ determined_new_state = LIQUID_STATE_WAIST
+ if(LIQUID_SHOULDERS_LEVEL_HEIGHT to LIQUID_FULLTILE_LEVEL_HEIGHT-1)
+ determined_new_state = LIQUID_STATE_SHOULDERS
+ if(LIQUID_FULLTILE_LEVEL_HEIGHT to INFINITY)
+ determined_new_state = LIQUID_STATE_FULLTILE
+ if(determined_new_state != liquid_state)
+ set_new_liquid_state(determined_new_state)
+
+/obj/effect/abstract/liquid_turf/proc/set_height(new_height)
+ var/prev_height = height
+ height = new_height
+ if(abs(height - prev_height) > WATER_HEIGH_DIFFERENCE_DELTA_SPLASH)
+ //Splash
+ if(prob(WATER_HEIGH_DIFFERENCE_SOUND_CHANCE))
+ var/sound_to_play = pick(list(
+ 'sound/effects/water_wade1.ogg',
+ 'sound/effects/water_wade2.ogg',
+ 'sound/effects/water_wade3.ogg',
+ 'sound/effects/water_wade4.ogg'
+ ))
+ playsound(my_turf, sound_to_play, 60, 0)
+ var/obj/splashy = new /obj/effect/liquid_splash(my_turf)
+ splashy.color = color
+ if(height >= LIQUID_WAIST_LEVEL_HEIGHT)
+ //Push things into some direction, like space wind
+ var/turf/dest_turf
+ var/last_height = height
+ for(var/turf in my_turf.atmos_adjacent_turfs)
+ var/turf/T = turf
+ if(T.z != my_turf.z)
+ continue
+ if(!T.liquids) //Automatic winner
+ dest_turf = T
+ break
+ if(T.liquids.height < last_height)
+ dest_turf = T
+ last_height = T.liquids.height
+ if(dest_turf)
+ var/dir = get_dir(my_turf, dest_turf)
+ var/atom/movable/AM
+ for(var/thing in my_turf)
+ AM = thing
+ if(!AM.anchored && !AM.pulledby && !isobserver(AM))
+ if(ishuman(AM))
+ var/mob/living/carbon/human/H = AM
+ if(!(H.shoes))
+ step(H, dir)
+ if(prob(60) && !H.lying)
+ to_chat(H, SPAN_DANGER("The current knocks you down!"))
+ H.Paralyse(60)
+ else
+ step(AM, dir)
+
+/obj/effect/abstract/liquid_turf/immutable/set_height(new_height)
+ height = new_height
+
+/obj/effect/abstract/liquid_turf/proc/movable_entered(datum/source, atom/movable/AM)
+ var/turf/T = source
+ if(isobserver(AM))
+ return //ghosts, camera eyes, etc. don't make water splashy splashy
+ if(liquid_state >= LIQUID_STATE_ANKLES)
+ if(prob(30))
+ var/sound_to_play = pick(list(
+ 'sound/effects/water_wade1.ogg',
+ 'sound/effects/water_wade2.ogg',
+ 'sound/effects/water_wade3.ogg',
+ 'sound/effects/water_wade4.ogg'
+ ))
+ playsound(T, sound_to_play, 50, 0)
+ if(iscarbon(AM))
+ var/mob/living/carbon/C = AM
+ C.add_modifier(/datum/modifier/status_effect/water_affected)
+ else if (isliving(AM))
+ var/mob/living/L = AM
+ if(prob(7))
+ L.slip(T, 60)
+ if(fire_state)
+ AM.fire_act((20 CELSIUS+50) + (50*fire_state), 125)
+
+/obj/effect/abstract/liquid_turf/proc/mob_fall(datum/source, mob/M)
+ var/turf/T = source
+ if(liquid_state >= LIQUID_STATE_ANKLES && has_gravity(T))
+ playsound(T, 'sound/effects/splash.ogg', 50, 0)
+ if(iscarbon(M))
+ var/mob/living/carbon/falling_carbon = M
+
+ // No point in giving reagents to the deceased. It can cause some runtimes.
+ if(falling_carbon.stat >= DEAD)
+ return
+
+ if(falling_carbon.wear_mask && falling_carbon.wear_mask.body_parts_covered & FACE)
+ to_chat(falling_carbon, SPAN_DANGER("You fall in the [reagents_to_text()]!"))
+ else
+ var/datum/reagents/tempr = take_reagents_flat(CHOKE_REAGENTS_INGEST_ON_FALL_AMOUNT)
+ tempr.trans_to_mob(falling_carbon, tempr.total_volume, type = CHEM_INGEST)
+
+ qdel(tempr)
+ falling_carbon.adjustOxyLoss(5)
+ falling_carbon.emote("cough")
+ to_chat(falling_carbon, SPAN_DANGER("You fall in and swallow some [reagents_to_text()]!"))
+ else
+ to_chat(M, SPAN_DANGER("You fall in the [reagents_to_text()]!"))
+
+/obj/effect/abstract/liquid_turf/Initialize(mapload)
+ . = ..()
+ if(!SSliquids)
+ CRASH("Liquid Turf created with the liquids sybsystem not yet initialized!")
+ if(!immutable)
+ my_turf = get_turf(src)
+ register_signal(my_turf, SIGNAL_ENTERED, .proc/movable_entered)
+ register_signal(my_turf, SIGNAL_ATOM_FALL, .proc/mob_fall)
+ SSliquids.add_active_turf(my_turf)
+
+ SEND_SIGNAL(my_turf, SIGNAL_TURF_LIQUIDS_CREATION, src)
+
+ update_overlays()
+ if(z)
+ src.smooth(get_turf(src))
+ src.smooth_neighbors(get_turf(src))
+
+/obj/effect/abstract/liquid_turf/Destroy(force)
+ ..()
+ if(force)
+ unregister_signal(my_turf, list(SIGNAL_ENTERED, SIGNAL_ATOM_FALL))
+ if(my_turf.lgroup)
+ my_turf.lgroup.remove_from_group(my_turf)
+ if(SSliquids.evaporation_queue[my_turf])
+ SSliquids.evaporation_queue -= my_turf
+ if(SSliquids.processing_fire[my_turf])
+ SSliquids.processing_fire -= my_turf
+ //Is added because it could invoke a change to neighboring liquids
+ SSliquids.add_active_turf(my_turf)
+ my_turf.liquids = null
+ src.smooth_neighbors(my_turf)
+ my_turf = null
+ else
+ return QDEL_HINT_LETMELIVE
+ return
+
+/obj/effect/abstract/liquid_turf/immutable/Destroy(force)
+ if(force)
+ CRASH("Something tried to hard destroy an immutable liquid.")
+ return ..()
+
+//Exposes my turf with simulated reagents
+/obj/effect/abstract/liquid_turf/proc/ExposeMyTurf()
+ var/datum/reagents/tempr = simulate_reagents_threshold(LIQUID_REAGENT_THRESHOLD_TURF_EXPOSURE)
+ tempr.touch_turf(my_turf)
+ qdel(tempr)
+
+/obj/effect/abstract/liquid_turf/proc/ChangeToNewTurf(turf/NewT)
+ if(NewT.liquids)
+ CRASH("Liquids tried to change to a new turf, that already had liquids on it!")
+
+ unregister_signal(my_turf, list(SIGNAL_ENTERED, SIGNAL_ATOM_FALL))
+ if(SSliquids.active_turfs[my_turf])
+ SSliquids.active_turfs -= my_turf
+ SSliquids.active_turfs[NewT] = TRUE
+ if(SSliquids.evaporation_queue[my_turf])
+ SSliquids.evaporation_queue -= my_turf
+ SSliquids.evaporation_queue[NewT] = TRUE
+ if(SSliquids.processing_fire[my_turf])
+ SSliquids.processing_fire -= my_turf
+ SSliquids.processing_fire[NewT] = TRUE
+ my_turf.liquids = null
+ my_turf = NewT
+ NewT.liquids = src
+ loc = NewT
+ register_signal(my_turf, SIGNAL_ENTERED, .proc/movable_entered)
+ register_signal(my_turf, SIGNAL_ATOM_FALL, .proc/mob_fall)
+
+
+/**
+ * Creates a string of the reagents that make up this liquid.
+ *
+ * Puts the reagent(s) that make up the liquid into string form e.g. "plasma" or "plasma and water", or 'plasma, milk, and water' depending on how many reagents there are.
+ *
+ * Returns the reagents list string or a generic "liquid" if there are no reagents somehow
+ * */
+/obj/effect/abstract/liquid_turf/proc/reagents_to_text()
+ /// the total amount of different types of reagents in the liquid
+ var/total_reagents = length(reagent_list)
+ /// the amount of different types of reagents that have not been listed yet
+ var/reagents_remaining = total_reagents
+ /// the final string to be returned
+ var/reagents_string = ""
+ if(!reagents_remaining)
+ return reagents_string += "liquid"
+
+ do
+ for(var/datum/reagent/reagent_type as anything in reagent_list)
+ reagents_string += "[initial(reagent_type.name)]"
+ reagents_remaining--
+ if(!reagents_remaining)
+ break
+ // if we are at the last reagent in the list, preface its name with 'and'.
+ // do not use a comma if there were only two reagents in the list
+ if(total_reagents == 2)
+ reagents_string += " and "
+ else
+ reagents_string += ", "
+ if(reagents_remaining == 1)
+ reagents_string += "and "
+ while(reagents_remaining)
+
+ return lowertext(reagents_string)
+
+/obj/effect/liquid_splash
+ icon = 'icons/effects/splash.dmi'
+ icon_state = "splash"
+ layer = FLY_LAYER
+
+/obj/effect/liquid_splash/Initialize()
+ . = ..()
+ QDEL_IN(src, 5 SECONDS)
+
+/obj/effect/abstract/liquid_turf/immutable
+ immutable = TRUE
+ var/list/starting_mixture = list(/datum/reagent/water = 600)
+ var/starting_temp = 20 CELSIUS
+
+//STRICTLY FOR IMMUTABLES DESPITE NOT BEING /immutable
+/obj/effect/abstract/liquid_turf/proc/add_turf(turf/T)
+ T.liquids = src
+ T.vis_contents += src
+ SSliquids.active_immutables[T] = TRUE
+ register_signal(T, SIGNAL_ENTERED, .proc/movable_entered)
+ register_signal(T, SIGNAL_ATOM_FALL, .proc/mob_fall)
+
+/obj/effect/abstract/liquid_turf/proc/remove_turf(turf/T)
+ SSliquids.active_immutables -= T
+ T.liquids = null
+ T.vis_contents -= src
+ unregister_signal(T, list(SIGNAL_ENTERED, SIGNAL_ATOM_FALL))
+
+/obj/effect/abstract/liquid_turf/immutable/ocean
+ icon_state = "ocean"
+ plane = DEFAULT_PLANE //Same as weather, etc.
+ layer = OBJ_LAYER
+ starting_temp = 20 CELSIUS-150
+ no_effects = TRUE
+ vis_flags = 0
+
+/obj/effect/abstract/liquid_turf/immutable/ocean/warm
+ starting_temp = 20 CELSIUS+20
+
+/obj/effect/abstract/liquid_turf/immutable/Initialize(mapload)
+ . = ..()
+ reagent_list = starting_mixture.Copy()
+ total_reagents = 0
+ for(var/key in reagent_list)
+ total_reagents += reagent_list[key]
+ temp = starting_temp
+ calculate_height()
+ set_reagent_color_for_liquid()
+
+/obj/effect/abstract/liquid_turf/proc/smooth_neighbors(smoothing_turf)
+ for(var/direction in GLOB.alldirs)
+ var/obj/effect/abstract/liquid_turf/L = locate() in get_step(smoothing_turf, direction)
+ if(L)
+ L.smooth(L.loc) //so siding get updated properly
+
+
+/obj/effect/abstract/liquid_turf/proc/smooth(smoothing_turf)
+ ASSERT(!isnull(smoothing_turf))
+ var/connectdir = 0
+ for(var/direction in GLOB.cardinal)
+ if(locate(/obj/effect/abstract/liquid_turf) in get_step(smoothing_turf, direction))
+ connectdir |= direction
+
+ //Check the diagonal connections for corners, where you have, for example, connections both north and east. In this case it checks for a north-east connection to determine whether to add a corner marker or not.
+ var/diagonalconnect = 0 //1 = NE; 2 = SE; 4 = NW; 8 = SW
+ var/dirs = list(1,2,4,8)
+ var/i = 1
+ for(var/diag in list(NORTHEAST, SOUTHEAST,NORTHWEST,SOUTHWEST))
+ if((connectdir & diag) == diag)
+ if(locate(/obj/effect/abstract/liquid_turf) in get_step(smoothing_turf, diag))
+ diagonalconnect |= dirs[i]
+ i += 1
+
+ src.icon_state = "water-[connectdir]-[diagonalconnect]"
diff --git a/code/modules/liquids/liquid_systems/liquid_groups.dm b/code/modules/liquids/liquid_systems/liquid_groups.dm
new file mode 100644
index 00000000000..534dfbb6603
--- /dev/null
+++ b/code/modules/liquids/liquid_systems/liquid_groups.dm
@@ -0,0 +1,271 @@
+/***************************************************/
+/********************PROPER GROUPING**************/
+
+//Whenever you add a liquid cell add its contents to the group, have the group hold the reference to total reagents for processing sake
+//Have the liquid turfs point to a partial liquids reference in the group for any interactions
+//Have the liquid group handle the total reagents datum, and reactions too (apply fraction?)
+
+GLOBAL_VAR_INIT(liquid_debug_colors, FALSE)
+
+/datum/liquid_group
+ var/list/members = list()
+ var/color
+ var/next_share = 0
+ var/dirty = TRUE
+ var/amount_of_active_turfs = 0
+ var/decay_counter = 0
+ var/expected_turf_height = 0
+ var/cached_color
+ var/list/last_cached_fraction_share
+ var/last_cached_total_volume = 0
+ var/last_cached_thermal = 0
+ var/last_cached_overlay_state = LIQUID_STATE_PUDDLE
+
+/datum/liquid_group/proc/add_to_group(turf/T)
+ members[T] = TRUE
+ T.lgroup = src
+ if(SSliquids.active_turfs[T])
+ amount_of_active_turfs++
+ if(T.liquids)
+ T.liquids.has_cached_share = FALSE
+
+/datum/liquid_group/proc/remove_from_group(turf/T)
+ members -= T
+ T.lgroup = null
+ if(SSliquids.active_turfs[T])
+ amount_of_active_turfs--
+ if(!members.len)
+ qdel(src)
+
+/datum/liquid_group/New(height)
+ SSliquids.active_groups[src] = TRUE
+ color = "#[rand_hex_color()]"
+ expected_turf_height = height
+
+/datum/liquid_group/proc/can_merge_group(datum/liquid_group/otherg)
+ if(expected_turf_height == otherg.expected_turf_height)
+ return TRUE
+ return FALSE
+
+/datum/liquid_group/proc/merge_group(datum/liquid_group/otherg)
+ amount_of_active_turfs += otherg.amount_of_active_turfs
+ for(var/t in otherg.members)
+ var/turf/T = t
+ T.lgroup = src
+ members[T] = TRUE
+ if(T.liquids)
+ T.liquids.has_cached_share = FALSE
+ otherg.members = list()
+ qdel(otherg)
+ share()
+
+/datum/liquid_group/proc/break_group()
+ //Flag puddles to the evaporation queue
+ for(var/t in members)
+ var/turf/T = t
+ if(T.liquids && T.liquids.liquid_state >= LIQUID_STATE_PUDDLE)
+ SSliquids.evaporation_queue[T] = TRUE
+
+ share(TRUE)
+ qdel(src)
+
+/datum/liquid_group/Destroy()
+ SSliquids.active_groups -= src
+ for(var/t in members)
+ var/turf/T = t
+ T.lgroup = null
+ members = null
+ return ..()
+
+/datum/liquid_group/proc/check_adjacency(turf/T)
+ var/list/recursive_adjacent = list()
+ var/list/current_adjacent = list()
+ current_adjacent[T] = TRUE
+ recursive_adjacent[T] = TRUE
+ var/getting_new_turfs = TRUE
+ var/indef_loop_safety = 0
+ while(getting_new_turfs && indef_loop_safety < LIQUID_RECURSIVE_LOOP_SAFETY)
+ indef_loop_safety++
+ getting_new_turfs = FALSE
+ var/list/new_adjacent = list()
+ for(var/t in current_adjacent)
+ var/turf/T2 = t
+ for(var/y in T2.get_atmos_adjacent_turfs())
+ if(!recursive_adjacent[y])
+ new_adjacent[y] = TRUE
+ recursive_adjacent[y] = TRUE
+ getting_new_turfs = TRUE
+ current_adjacent = new_adjacent
+ //All adjacent, somehow
+ if(recursive_adjacent.len == members.len)
+ return
+ var/datum/liquid_group/new_group = new(expected_turf_height)
+ for(var/t in members)
+ if(!recursive_adjacent[t])
+ remove_from_group(t)
+ new_group.add_to_group(t)
+
+/datum/liquid_group/proc/share(use_liquids_color = FALSE)
+ var/any_share = FALSE
+ var/cached_shares = 0
+ var/list/cached_add = list()
+ var/cached_volume = 0
+ var/cached_thermal = 0
+
+ var/turf/T
+ var/obj/effect/abstract/liquid_turf/cached_liquids
+ for(var/t in members)
+ T = t
+ if(T.liquids)
+ any_share = TRUE
+ cached_liquids = T.liquids
+
+ if(cached_liquids.has_cached_share && last_cached_fraction_share)
+ cached_shares++
+ continue
+
+ for(var/r_type in cached_liquids.reagent_list)
+ if(!cached_add[r_type])
+ cached_add[r_type] = 0
+ cached_add[r_type] += cached_liquids.reagent_list[r_type]
+ cached_volume += cached_liquids.total_reagents
+ cached_thermal += cached_liquids.total_reagents * cached_liquids.temp
+ if(!any_share)
+ return
+
+ decay_counter = 0
+
+ if(cached_shares)
+ for(var/reagent_type in last_cached_fraction_share)
+ if(!cached_add[reagent_type])
+ cached_add[reagent_type] = 0
+ cached_add[reagent_type] += last_cached_fraction_share[reagent_type] * cached_shares
+ cached_volume += last_cached_total_volume * cached_shares
+ cached_thermal += cached_shares * last_cached_thermal
+
+ for(var/reagent_type in cached_add)
+ cached_add[reagent_type] = cached_add[reagent_type] / members.len
+ cached_volume = cached_volume / members.len
+ cached_thermal = cached_thermal / members.len
+ var/temp_to_set = cached_thermal / cached_volume
+ last_cached_thermal = cached_thermal
+ last_cached_fraction_share = cached_add
+ last_cached_total_volume = cached_volume
+ var/mixed_color = use_liquids_color ? mix_color_from_reagent_list(cached_add) : color
+ if(use_liquids_color)
+ mixed_color = mix_color_from_reagent_list(cached_add)
+ else if (GLOB.liquid_debug_colors)
+ mixed_color = color
+ else
+ if(!cached_color)
+ cached_color = mix_color_from_reagent_list(cached_add)
+ mixed_color = cached_color
+
+ var/height = CEILING(cached_volume/LIQUID_HEIGHT_DIVISOR, 1)
+
+ var/determined_new_state
+ var/state_height = height
+ if(expected_turf_height > 0)
+ state_height += expected_turf_height
+ switch(state_height)
+ if(0 to LIQUID_ANKLES_LEVEL_HEIGHT-1)
+ determined_new_state = LIQUID_STATE_PUDDLE
+ if(LIQUID_ANKLES_LEVEL_HEIGHT to LIQUID_WAIST_LEVEL_HEIGHT-1)
+ determined_new_state = LIQUID_STATE_ANKLES
+ if(LIQUID_WAIST_LEVEL_HEIGHT to LIQUID_SHOULDERS_LEVEL_HEIGHT-1)
+ determined_new_state = LIQUID_STATE_WAIST
+ if(LIQUID_SHOULDERS_LEVEL_HEIGHT to LIQUID_FULLTILE_LEVEL_HEIGHT-1)
+ determined_new_state = LIQUID_STATE_SHOULDERS
+ if(LIQUID_FULLTILE_LEVEL_HEIGHT to INFINITY)
+ determined_new_state = LIQUID_STATE_FULLTILE
+
+ var/new_liquids = FALSE
+ for(var/t in members)
+ T = t
+ new_liquids = FALSE
+ if(!T.liquids)
+ new_liquids = TRUE
+ T.liquids = new(T)
+ cached_liquids = T.liquids
+
+ cached_liquids.reagent_list = cached_add.Copy()
+ cached_liquids.total_reagents = cached_volume
+ cached_liquids.temp = temp_to_set
+
+ cached_liquids.has_cached_share = TRUE
+ cached_liquids.attrition = 0
+
+ cached_liquids.color = mixed_color
+ cached_liquids.set_height(height)
+
+ if(determined_new_state != cached_liquids.liquid_state)
+ cached_liquids.set_new_liquid_state(determined_new_state)
+
+ //Only simulate a turf exposure when we had to create a new liquid tile
+ if(new_liquids)
+ cached_liquids.ExposeMyTurf()
+
+/datum/liquid_group/proc/process_cell(turf/T)
+ if(T.liquids.height <= 1) //Causes a bug when the liquid hangs in the air and is supposed to fall down a level
+ return FALSE
+ for(var/tur in T.get_atmos_adjacent_turfs())
+ var/turf/T2 = tur
+ //Immutable check thing
+ if(T2.liquids && T2.liquids.immutable)
+ if(T.z != T2.z)
+ var/turf/Z_turf_below = GetBelow(T)
+ if(T2 == Z_turf_below)
+ qdel(T.liquids, TRUE)
+ return
+ else
+ continue
+
+ //CHECK DIFFERENT TURF HEIGHT THING
+ if(T.liquid_height != T2.liquid_height)
+ var/my_liquid_height = T.liquid_height + T.liquids.height
+ var/target_liquid_height = T2.liquid_height + T2.liquids.height
+ if(my_liquid_height > target_liquid_height+2)
+ var/coeff = (T.liquids.height / (T.liquids.height + abs(T.liquid_height)))
+ var/height_diff = min(0.4,abs((target_liquid_height / my_liquid_height)-1)*coeff)
+ T.liquid_fraction_delete(height_diff)
+ . = TRUE
+ continue
+
+ if(T2.liquids.height > T.liquids.height + 1)
+ SSliquids.active_immutables[T2] = TRUE
+ . = TRUE
+ continue
+ //END OF IMMUTABLE MADNESS
+
+ if(T.z != T2.z)
+ var/turf/Z_turf_below = GetBelow(T)
+ if(T2 == Z_turf_below)
+ if(!(T2.liquids && T2.liquids.height + T2.liquid_height >= LIQUID_HEIGHT_CONSIDER_FULL_TILE))
+ T.liquid_fraction_share(T2, 1)
+ qdel(T.liquids, TRUE)
+ . = TRUE
+ continue
+ //CHECK DIFFERENT TURF HEIGHT THING
+ if(T.liquid_height != T2.liquid_height)
+ var/my_liquid_height = T.liquid_height + T.liquids.height
+ var/target_liquid_height = T2.liquid_height + (T2.liquids ? T2.liquids.height : 0)
+ if(my_liquid_height > target_liquid_height+1)
+ var/coeff = (T.liquids.height / (T.liquids.height + abs(T.liquid_height)))
+ var/height_diff = min(0.4,abs((target_liquid_height / my_liquid_height)-1)*coeff)
+ T.liquid_fraction_share(T2, height_diff)
+ . = TRUE
+ continue
+ //END OF TURF HEIGHT
+ if(!T.can_share_liquids_with(T2))
+ continue
+ if(!T2.lgroup)
+ add_to_group(T2)
+ //Try merge groups if possible
+ else if(T2.lgroup != T.lgroup && T.lgroup.can_merge_group(T2.lgroup))
+ T.lgroup.merge_group(T2.lgroup)
+ . = TRUE
+ SSliquids.add_active_turf(T2)
+ if(.)
+ dirty = TRUE
+ //return //Do we want it to spread once per process or many times?
+ //Make sure to handle up/down z levels on adjacency properly
diff --git a/code/modules/liquids/liquid_systems/liquid_height.dm b/code/modules/liquids/liquid_systems/liquid_height.dm
new file mode 100644
index 00000000000..1e5147976fa
--- /dev/null
+++ b/code/modules/liquids/liquid_systems/liquid_height.dm
@@ -0,0 +1,47 @@
+/**
+ * Liquid Height element; for dynamically applying liquid blockages.
+ *
+ * Used for reinforced tables, sandbags, and the likes.
+ */
+/datum/element/liquids_height
+ element_flags = ELEMENT_BESPOKE | ELEMENT_DETACH
+ id_arg_index = 2
+
+ ///Height applied by this element
+ var/height_applied
+
+/datum/element/liquids_height/attach(datum/target, height_applied)
+ . = ..()
+
+ if(!ismovable(target))
+ return ELEMENT_INCOMPATIBLE
+
+ src.height_applied = height_applied
+
+ register_signal(target, SIGNAL_MOVED, .proc/on_target_move)
+ var/atom/movable/movable_target = target
+ if(isturf(movable_target.loc))
+ var/turf/turf_loc = movable_target.loc
+ turf_loc.liquid_height += height_applied
+ turf_loc.reasses_liquids()
+
+/datum/element/liquids_height/detach(atom/movable/target)
+ . = ..()
+
+ unregister_signal(target, list(SIGNAL_MOVED))
+ var/atom/movable/movable_target = target
+ if(isturf(movable_target.loc))
+ var/turf/turf_loc = movable_target.loc
+ turf_loc.liquid_height -= height_applied
+ turf_loc.reasses_liquids()
+
+/datum/element/liquids_height/proc/on_target_move(atom/movable/source, atom/OldLoc, Dir, Forced = FALSE)
+
+ if(isturf(OldLoc))
+ var/turf/old_turf = OldLoc
+ old_turf.liquid_height += height_applied
+ old_turf.reasses_liquids()
+ if(isturf(source.loc))
+ var/turf/new_turf = source.loc
+ new_turf.liquid_height -= height_applied
+ new_turf.reasses_liquids()
diff --git a/code/modules/liquids/liquid_systems/liquid_interaction.dm b/code/modules/liquids/liquid_systems/liquid_interaction.dm
new file mode 100644
index 00000000000..7f046f22e1a
--- /dev/null
+++ b/code/modules/liquids/liquid_systems/liquid_interaction.dm
@@ -0,0 +1,27 @@
+///This element allows for items to interact with liquids on turfs.
+/datum/component/liquids_interaction
+ ///Callback interaction called when the turf has some liquids on it
+ var/datum/callback/interaction_callback
+
+/datum/component/liquids_interaction/Initialize(on_interaction_callback)
+ . = ..()
+
+ if(!istype(parent, /obj/item))
+ return COMPONENT_INCOMPATIBLE
+
+ interaction_callback = CALLBACK(parent, on_interaction_callback)
+
+/datum/component/liquids_interaction/register_with_parent()
+ register_signal(parent, SIGNAL_CLEAN_LIQUIDS, .proc/AfterAttack) //The only signal allowing item -> turf interaction
+
+/datum/component/liquids_interaction/unregister_from_parent()
+ unregister_signal(parent, SIGNAL_CLEAN_LIQUIDS)
+
+/datum/component/liquids_interaction/proc/AfterAttack(turf/turf_target, mob/user)
+ SIGNAL_HANDLER
+
+ if(!isturf(turf_target) || !turf_target.liquids)
+ return
+
+ if(interaction_callback.Invoke(turf_target, user, turf_target.liquids))
+ return
diff --git a/code/modules/liquids/liquid_systems/liquid_plumbers.dm b/code/modules/liquids/liquid_systems/liquid_plumbers.dm
new file mode 100644
index 00000000000..9f7d5afb346
--- /dev/null
+++ b/code/modules/liquids/liquid_systems/liquid_plumbers.dm
@@ -0,0 +1,329 @@
+/*
+/**
+ * Base class for underfloor plumbing machines that mess with floor liquids.
+ */
+/obj/machinery/plumbing/floor_pump
+ icon = 'icons/obj/liquids/structures/drains.dmi'
+ icon_state = "active_input"
+ anchored = FALSE
+ density = FALSE
+ idle_power_usage = 10
+ active_power_usage = 1000
+ buffer = 300
+
+ /// Pump is turned on by engineer, etc.
+ var/turned_on = FALSE
+ /// Only pump to this liquid level height. 0 means pump the most possible.
+ var/height_regulator = 0
+
+ /// The default duct layer for mapping
+ var/duct_layer = 0
+
+ /// Base amount to drain
+ var/drain_flat = 20
+ /// Additional ratio of liquid volume to drain
+ var/drain_percent = 0.4
+
+ /// Currently pumping.
+ var/is_pumping = FALSE
+ /// Floor tile is placed down
+ var/tile_placed = FALSE
+
+ ///category for plumbing RCD
+ category = "Liquids"
+
+/obj/machinery/plumbing/floor_pump/Initialize(mapload, bolt, layer)
+ . = ..()
+ register_signal(src, COMSIG_OBJ_HIDE, PROC_REF(on_hide))
+
+/obj/machinery/plumbing/floor_pump/examine(mob/user)
+ . = ..()
+ . += SPAN_NOTICE("It's currently turned [turned_on ? "ON" : "OFF"].")
+ . += SPAN_NOTICE("Its height regulator [height_regulator ? "points at [height_regulator]" : "is disabled"]. Click while unanchored to change.")
+
+/obj/machinery/plumbing/floor_pump/update_appearance(updates)
+ . = ..()
+ layer = tile_placed ? GAS_SCRUBBER_LAYER : BELOW_OBJ_LAYER
+ plane = tile_placed ? FLOOR_PLANE : GAME_PLANE
+
+/obj/machinery/plumbing/floor_pump/update_icon_state()
+ . = ..()
+ if(panel_open)
+ icon_state = "[base_icon_state]-open"
+ else if(is_pumping)
+ icon_state = "[base_icon_state]-pumping"
+ else if(is_operational && turned_on)
+ icon_state = "[base_icon_state]-idle"
+ else
+ icon_state = base_icon_state
+
+/obj/machinery/plumbing/floor_pump/default_unfasten_wrench(mob/user, obj/item/I, time = 20)
+ . = ..()
+ if(. == SUCCESSFUL_UNFASTEN)
+ turned_on = FALSE
+ update_icon_state()
+
+/obj/machinery/plumbing/floor_pump/attack_hand(mob/user)
+ if(!anchored)
+ set_regulator(user)
+ return
+ balloon_alert(user, "turned [turned_on ? "off" : "on"]")
+ turned_on = !turned_on
+ update_icon_state()
+
+/**
+ * Change regulator level -- ie. what liquid depth we are OK with, like a thermostat.
+ */
+/obj/machinery/plumbing/floor_pump/proc/set_regulator(mob/living/user)
+ if(!user.can_perform_action(src, NEED_DEXTERITY))
+ return
+ var/new_height = tgui_input_number(user,
+ "At what water level should the pump stop pumping from 0 to [LIQUID_HEIGHT_CONSIDER_FULL_TILE]? 0 disables.",
+ "[src]",
+ default = height_regulator,
+ min_value = 0,
+ max_value = LIQUID_HEIGHT_CONSIDER_FULL_TILE)
+ if(QDELETED(src) || new_height == null)
+ return
+ height_regulator = new_height
+
+/**
+ * Handle COMSIG_OBJ_HIDE to toggle whether we're on the floor
+ */
+/obj/machinery/plumbing/floor_pump/proc/on_hide(atom/movable/AM, should_hide)
+ SIGNAL_HANDLER
+
+ tile_placed = should_hide
+ update_appearance()
+
+/**
+ * Can the pump actually run at all?
+ */
+/obj/machinery/plumbing/floor_pump/proc/can_run()
+ return is_operational \
+ && turned_on \
+ && anchored \
+ && !panel_open \
+ && isturf(loc) \
+ && are_reagents_ready()
+
+/**
+ * Is the internal reagents container able to give or take liquid as appropriate?
+ */
+/obj/machinery/plumbing/floor_pump/proc/are_reagents_ready()
+ CRASH("are_reagents_ready() must be overriden.")
+
+/**
+ * Should we actually be pumping this tile right now?
+ * Arguments:
+ * * affected_turf - the turf to check.
+ */
+/obj/machinery/plumbing/floor_pump/proc/should_pump(turf/affected_turf)
+ return isturf(affected_turf) \
+ && should_regulator_permit(affected_turf)
+
+/**
+ * Should the liquid height regulator allow water to be pumped here?
+ */
+/obj/machinery/plumbing/floor_pump/proc/should_regulator_permit(turf/affected_turf)
+ CRASH("should_regulator_permit() must be overriden.")
+
+/obj/machinery/plumbing/floor_pump/process(seconds_per_tick)
+ var/was_pumping = is_pumping
+
+ if(!can_run())
+ is_pumping = FALSE
+ if(was_pumping)
+ update_icon_state()
+ return
+
+ // Determine what tiles should be pumped. We grab from a 3x3 area,
+ // but overall try to pump the same volume regardless of number of affected tiles
+ var/turf/local_turf = get_turf(src)
+ var/list/turf/candidate_turfs = local_turf.get_atmos_adjacent_turfs()
+ candidate_turfs += local_turf
+
+ var/list/turf/affected_turfs = list()
+
+ for(var/turf/candidate as anything in candidate_turfs)
+ if(should_pump(candidate))
+ affected_turfs += candidate
+
+ // Update state
+ is_pumping = length(affected_turfs) > 0
+ if(is_pumping != was_pumping)
+ update_icon_state()
+ if(!is_pumping)
+ return
+
+ // note that the length was verified to be > 0 directly above and is a local var.
+ var/multiplier = 1 / length(affected_turfs)
+
+ // We're good, actually pump.
+ for(var/turf/affected_turf as anything in affected_turfs)
+ pump_turf(affected_turf, seconds_per_tick, multiplier)
+
+/**
+ * Pump out the liquids on a turf.
+ *
+ * Arguments:
+ * * affected_turf - the turf to pump liquids out of.
+ * * seconds_per_tick - machine process delta time
+ * * multiplier - Multiplier to apply to final volume we want to pump.
+ */
+/obj/machinery/plumbing/floor_pump/proc/pump_turf(turf/affected_turf, seconds_per_tick, multiplier)
+ CRASH("pump_turf() must be overriden.")
+
+
+
+/obj/machinery/plumbing/floor_pump/input
+ name = "liquid input pump"
+ desc = "Pump used to siphon liquids from a location into the plumbing pipenet."
+ icon_state = "active_input"
+ base_icon_state = "active_input"
+
+/obj/machinery/plumbing/floor_pump/input/Initialize(mapload, bolt, layer)
+ . = ..()
+ AddComponent(/datum/component/plumbing/simple_supply, bolt, layer || duct_layer)
+
+/obj/machinery/plumbing/floor_pump/input/are_reagents_ready()
+ return reagents.total_volume < reagents.maximum_volume
+
+/obj/machinery/plumbing/floor_pump/input/should_regulator_permit(turf/affected_turf)
+ return affected_turf.liquids && affected_turf.liquids.height > height_regulator
+
+/obj/machinery/plumbing/floor_pump/input/pump_turf(turf/affected_turf, seconds_per_tick, multiplier)
+ var/target_value = seconds_per_tick * (drain_flat + (affected_turf.liquids.total_reagents * drain_percent)) * multiplier
+ //Free space handling
+ var/free_space = reagents.maximum_volume - reagents.total_volume
+ if(target_value > free_space)
+ target_value = free_space
+
+ var/datum/reagents/tempr = affected_turf.liquids.take_reagents_flat(target_value)
+ tempr.trans_to(src, tempr.total_volume)
+ qdel(tempr)
+
+// So user can intuitively touch-pour liquids down the drain.
+/obj/machinery/plumbing/floor_pump/input/expose_reagents(list/reagents, datum/reagents/source, methods, volume_modifier, show_message)
+ . = ..()
+
+ if(methods == TOUCH)
+ source.trans_to(src.reagents, min(source.total_volume * volume_modifier, src.reagents.maximum_volume - src.reagents.total_volume))
+
+/obj/machinery/plumbing/floor_pump/input/on
+ icon_state = "active_input-mapping"
+ anchored = TRUE
+ turned_on = TRUE
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/plumbing/floor_pump/input/on, 0)
+
+/obj/machinery/plumbing/floor_pump/input/on/waste
+ icon_state = "active_input-mapping2"
+ duct_layer = SECOND_DUCT_LAYER
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/plumbing/floor_pump/input/on/waste, 0)
+
+/obj/machinery/plumbing/floor_pump/output
+ name = "liquid output pump"
+ desc = "Pump used to dump liquids out from a plumbing pipenet into a location."
+ icon_state = "active_output"
+ base_icon_state = "active_output"
+
+ /// Is the turf too full to pump more?
+ var/over_volume = FALSE
+ /// Max liquid volume on the turf before we stop pumping.
+ var/max_ext_volume = LIQUID_HEIGHT_CONSIDER_FULL_TILE
+
+ /// Is the turf too high-pressured to pump more?
+ var/over_pressure = FALSE
+ /// Max pressure on the turf before we stop pumping.
+ var/max_ext_kpa = WARNING_HIGH_PRESSURE
+
+/obj/machinery/plumbing/floor_pump/output/Initialize(mapload, bolt, layer)
+ . = ..()
+ AddComponent(/datum/component/plumbing/simple_demand, bolt, layer || duct_layer)
+
+/obj/machinery/plumbing/floor_pump/output/examine(mob/user)
+ . = ..()
+ if(over_pressure)
+ . += SPAN_WARNING("The gas regulator light is blinking.")
+ if(over_volume)
+ . += SPAN_WARNING("The liquid volume regulator light is blinking.")
+
+/obj/machinery/plumbing/floor_pump/output/are_reagents_ready()
+ return reagents.total_volume > 0
+
+/obj/machinery/plumbing/floor_pump/output/should_regulator_permit(turf/affected_turf)
+ // 0 means keep pumping forever.
+ return !height_regulator || affected_turf.liquids.height < height_regulator
+
+/obj/machinery/plumbing/floor_pump/output/process()
+ over_pressure = FALSE
+ return ..()
+
+/obj/machinery/plumbing/floor_pump/output/should_pump(turf/affected_turf)
+ . = ..()
+ if(!.)
+ return FALSE
+
+ if(affected_turf.liquids?.height >= max_ext_volume)
+ return FALSE
+ var/turf/simulated/open_turf = affected_turf
+ var/datum/gas_mixture/gas_mix = open_turf?.return_air()
+ if(gas_mix?.return_pressure() > max_ext_kpa)
+ over_pressure = TRUE
+ return FALSE
+ return TRUE
+
+/obj/machinery/plumbing/floor_pump/output/pump_turf(turf/affected_turf, seconds_per_tick, multiplier)
+ var/target_value = seconds_per_tick * (drain_flat + (reagents.total_volume * drain_percent)) * multiplier
+ if(target_value > reagents.total_volume)
+ target_value = reagents.total_volume
+
+ var/datum/reagents/tempr = new(10000)
+ reagents.trans_to(tempr, target_value, no_react = TRUE)
+ affected_turf.add_liquid_from_reagents(tempr)
+ qdel(tempr)
+
+/obj/machinery/plumbing/floor_pump/output/on
+ icon_state = "active_output-mapping"
+ anchored = TRUE
+ turned_on = TRUE
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/plumbing/floor_pump/output/on, 0)
+
+/obj/machinery/plumbing/floor_pump/output/on/supply
+ icon_state = "active_output-mapping2"
+ duct_layer = FOURTH_DUCT_LAYER
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/plumbing/floor_pump/output/on/supply, 0)
+
+/obj/item/construction/plumbing/engineering
+ name = "engineering plumbing constructor"
+ desc = "A type of plumbing constructor designed to rapidly deploy the machines needed for logistics regarding fluids."
+ icon_state = "plumberer2"
+
+/obj/item/construction/plumbing/engineering/set_plumbing_designs()
+ plumbing_design_types = list(
+ /obj/machinery/duct = 1,
+ /obj/machinery/plumbing/input = 5,
+ /obj/machinery/plumbing/output = 5,
+ /obj/machinery/plumbing/tank = 20,
+ /obj/machinery/plumbing/acclimator = 10,
+ /obj/machinery/plumbing/filter = 5,
+ /obj/machinery/plumbing/splitter = 5,
+ /obj/machinery/plumbing/disposer = 10,
+ /obj/machinery/plumbing/floor_pump/input = 20,
+ /obj/machinery/plumbing/floor_pump/output = 20,
+ /obj/structure/drain = 5,
+ )
+
+// Helpers for maps
+/obj/machinery/duct/supply
+ duct_color = COLOR_CYAN
+ duct_layer = FOURTH_DUCT_LAYER
+
+/obj/machinery/duct/waste
+ duct_color = COLOR_BROWN
+ duct_layer = SECOND_DUCT_LAYER
+*/
diff --git a/code/modules/liquids/liquid_systems/liquid_pump.dm b/code/modules/liquids/liquid_systems/liquid_pump.dm
new file mode 100644
index 00000000000..f83f180ac1c
--- /dev/null
+++ b/code/modules/liquids/liquid_systems/liquid_pump.dm
@@ -0,0 +1,112 @@
+//Right now it's a structure that works off of magic, as it'd require an internal power source for what its supposed to do
+/obj/structure/liquid_pump
+ name = "portable liquid pump"
+ desc = "An industrial grade pump, capable of either siphoning or spewing liquids. Needs to be anchored first to work. Has a limited capacity internal storage."
+ icon = 'icons/obj/liquids/structures/liquid_pump.dmi'
+ icon_state = "liquid_pump"
+ density = TRUE
+ anchored = FALSE
+ /// How many reagents at maximum can it hold
+ var/max_volume = 10000
+ /// Whether spewing reagents out, instead of siphoning them
+ var/spewing_mode = FALSE
+ /// Whether its turned on and processing
+ var/turned_on = FALSE
+ /// How fast does the pump work, in percentages relative to the volume we're working with
+ var/pump_speed_percentage = 0.4
+ /// How fast does the pump work, in flat values. Flat values on top of percentages to help processing
+ var/pump_speed_flat = 20
+
+/obj/structure/liquid_pump/attackby(obj/item/O, mob/user)
+ if(!isWrench(O))
+ return ..()
+ playsound(src.loc, 'sound/items/Ratchet.ogg', 50, 1)
+ to_chat(user, "You begin to unfasten \the [src]...")
+ if (do_after(user, 40, src))
+ user.visible_message( \
+ "\The [user] unfastens \the [src].", \
+ "You have unfastened \the [src].", \
+ "You hear ratchet.")
+ if(!anchored && turned_on)
+ toggle_working()
+ return TRUE
+
+/obj/structure/liquid_pump/attack_hand(mob/user)
+ if(!anchored)
+ to_chat(user, SPAN_WARNING("[src] needs to be anchored first!"))
+ return
+ to_chat(user, SPAN_NOTICE("You turn [src] [turned_on ? "off" : "on"]."))
+ toggle_working()
+
+/obj/structure/liquid_pump/AltClick(mob/living/user)
+ if(!Adjacent(user, src))
+ return ..()
+
+ to_chat(user, SPAN_NOTICE("You flick [src]'s spewing mode [spewing_mode ? "off" : "on"]."))
+ spewing_mode = !spewing_mode
+ update_icon()
+
+/obj/structure/liquid_pump/_examine_text(mob/user, infix, suffix)
+ . = ..()
+ . += SPAN_NOTICE("It's anchor bolts are [anchored ? "down and secured" : "up"].")
+ . += SPAN_NOTICE("It's currently [turned_on ? "ON" : "OFF"].")
+ . += SPAN_NOTICE("It's mode currently is set to [spewing_mode ? "SPEWING" : "SIPHONING"].")
+ . += SPAN_NOTICE("The pressure gauge shows [reagents.total_volume]/[reagents.maximum_volume].")
+
+/obj/structure/liquid_pump/think()
+ if(!isturf(loc))
+ set_next_think(world.time+1)
+ return
+ var/turf/T = loc
+ if(spewing_mode)
+ if(!reagents.total_volume)
+ set_next_think(world.time+1)
+ return
+ var/datum/reagents/tempr = new(10000)
+ reagents.trans_to(tempr, (reagents.total_volume * pump_speed_percentage) + pump_speed_flat)
+ T.add_liquid_from_reagents(tempr)
+ qdel(tempr)
+ else
+ if(!T.liquids)
+ set_next_think(world.time+1)
+ return
+ var/free_space = reagents.maximum_volume - reagents.total_volume
+ if(!free_space)
+ set_next_think(world.time+1)
+ return
+ var/target_siphon_amt = (T.liquids.total_reagents * pump_speed_percentage) + pump_speed_flat
+ if(target_siphon_amt > free_space)
+ target_siphon_amt = free_space
+ var/datum/reagents/tempr = T.liquids.take_reagents_flat(target_siphon_amt)
+ tempr.trans_to(reagents, tempr.total_volume)
+ qdel(tempr)
+ set_next_think(world.time+1)
+ return
+
+/obj/structure/liquid_pump/update_icon()
+ . = ..()
+ if(turned_on)
+ if(spewing_mode)
+ icon_state = "[initial(icon_state)]_spewing"
+ else
+ icon_state = "[initial(icon_state)]_siphoning"
+ else
+ icon_state = "[initial(icon_state)]"
+
+/obj/structure/liquid_pump/proc/toggle_working()
+ if(turned_on)
+ set_next_think(0)
+ else
+ set_next_think(world.time+1)
+ turned_on = !turned_on
+ update_icon()
+
+/obj/structure/liquid_pump/Initialize(mapload)
+ . = ..()
+ create_reagents(max_volume)
+
+/obj/structure/liquid_pump/Destroy()
+ if(turned_on)
+ set_next_think(0)
+ qdel(reagents)
+ return ..()
diff --git a/code/modules/liquids/liquid_systems/liquid_status_effect.dm b/code/modules/liquids/liquid_systems/liquid_status_effect.dm
new file mode 100644
index 00000000000..46b8d8b11ab
--- /dev/null
+++ b/code/modules/liquids/liquid_systems/liquid_status_effect.dm
@@ -0,0 +1,30 @@
+/datum/modifier/status_effect/water_affected
+ name = "wateraffected"
+
+/datum/modifier/status_effect/water_affected/on_applied()
+ //We should be inside a liquid turf if this is applied
+ calculate_water_slow()
+ return TRUE
+
+/datum/modifier/status_effect/water_affected/proc/calculate_water_slow()
+ //Factor in swimming skill here?
+ var/turf/T = get_turf(holder)
+ var/slowdown_amount = T.liquids.liquid_state * 0.5
+ holder.setMoveCooldown(slowdown_amount)
+
+/datum/modifier/status_effect/water_affected/tick()
+ var/turf/T = get_turf(holder)
+ if(!T || !T.liquids || T.liquids.liquid_state == LIQUID_STATE_PUDDLE)
+ qdel(src)
+ return
+ calculate_water_slow()
+ //Make the reagents touch the person
+ var/fraction = SUBMERGEMENT_PERCENT(holder, T.liquids)
+ var/datum/reagents/tempr = T.liquids.simulate_reagents_flat(SUBMERGEMENT_REAGENTS_TOUCH_AMOUNT*fraction)
+ tempr.touch_mob(holder)
+ tempr.trans_to(holder)
+ qdel(tempr)
+ return ..()
+
+/datum/modifier/status_effect/water_affected/on_expire()
+ holder.setMoveCooldown(holder.movement_delay())
diff --git a/code/modules/liquids/liquid_systems/liquid_turf.dm b/code/modules/liquids/liquid_systems/liquid_turf.dm
new file mode 100644
index 00000000000..502d3a025db
--- /dev/null
+++ b/code/modules/liquids/liquid_systems/liquid_turf.dm
@@ -0,0 +1,283 @@
+/turf
+ var/datum/liquid_group/lgroup
+ var/obj/effect/abstract/liquid_turf/liquids
+ var/liquid_height = 0
+
+/turf/proc/convert_immutable_liquids()
+ if(!liquids || !liquids.immutable)
+ return
+ var/datum/reagents/tempr = liquids.take_reagents_flat(liquids.total_reagents)
+ var/cached_height = liquids.height
+ liquids.remove_turf(src)
+ liquids = new(src)
+ liquids.height = cached_height //Prevent height effects
+ add_liquid_from_reagents(tempr)
+ qdel(tempr)
+
+/turf/proc/reasses_liquids()
+ if(!liquids)
+ return
+ if(lgroup)
+ lgroup.remove_from_group(src)
+ SSliquids.add_active_turf(src)
+
+/obj/effect/abstract/liquid_turf/proc/liquid_simple_delete_flat(flat_amount)
+ if(flat_amount >= total_reagents)
+ qdel(src, TRUE)
+ return
+ var/fraction = flat_amount/total_reagents
+ for(var/reagent_type in reagent_list)
+ var/amount = fraction * reagent_list[reagent_type]
+ reagent_list[reagent_type] -= amount
+ total_reagents -= amount
+ has_cached_share = FALSE
+ if(!my_turf.lgroup)
+ calculate_height()
+
+/turf/proc/liquid_fraction_delete(fraction)
+ for(var/r_type in liquids.reagent_list)
+ var/volume_change = liquids.reagent_list[r_type] * fraction
+ liquids.reagent_list[r_type] -= volume_change
+ liquids.total_reagents -= volume_change
+
+/turf/proc/liquid_fraction_share(turf/T, fraction)
+ if(!liquids)
+ return
+ if(fraction > 1)
+ CRASH("Fraction share more than 100%")
+ for(var/r_type in liquids.reagent_list)
+ var/volume_change = liquids.reagent_list[r_type] * fraction
+ liquids.reagent_list[r_type] -= volume_change
+ liquids.total_reagents -= volume_change
+ T.add_liquid(r_type, volume_change, TRUE, liquids.temp)
+ liquids.has_cached_share = FALSE
+
+/turf/proc/liquid_update_turf()
+ if(!liquids)
+ return
+
+ if(liquids && liquids.immutable)
+ SSliquids.active_immutables[src] = TRUE
+ return
+ //Check atmos adjacency to cut off any disconnected groups
+ if(lgroup)
+ var/assoc_atmos_turfs = list()
+ for(var/tur in get_atmos_adjacent_turfs())
+ assoc_atmos_turfs[tur] = TRUE
+ //Check any cardinal that may have a matching group
+ for(var/direction in GLOB.cardinal)
+ var/turf/T = get_step(src, direction)
+ //Same group of which we do not share atmos adjacency
+ if(!assoc_atmos_turfs[T] && T.lgroup && T.lgroup == lgroup)
+ T.lgroup.check_adjacency(T)
+
+ SSliquids.add_active_turf(src)
+
+/turf/proc/add_liquid_from_reagents(datum/reagents/giver, no_react = FALSE, reagent_multiplier = 1)
+ var/list/compiled_list = list()
+ for(var/r in giver.reagent_list)
+ var/datum/reagent/R = r
+ compiled_list[R.type] = R.volume * reagent_multiplier
+ if(!compiled_list.len) //No reagents to add, don't bother going further
+ return
+ add_liquid_list(compiled_list, no_react)
+
+//More efficient than add_liquid for multiples
+/turf/proc/add_liquid_list(reagent_list, no_react = FALSE)
+ if(!liquids)
+ liquids = new(src)
+ if(liquids.immutable)
+ return
+
+ var/prev_total_reagents = liquids.total_reagents
+ var/prev_thermal_energy = prev_total_reagents * liquids.temp
+
+ for(var/reagent in reagent_list)
+ if(!liquids.reagent_list[reagent])
+ liquids.reagent_list[reagent] = 0
+ liquids.reagent_list[reagent] += reagent_list[reagent]
+ liquids.total_reagents += reagent_list[reagent]
+
+ var/recieved_thermal_energy = (liquids.total_reagents - prev_total_reagents)
+ liquids.temp = (recieved_thermal_energy + prev_thermal_energy) / liquids.total_reagents
+
+ if(!no_react)
+ //We do react so, make a simulation
+ create_reagents(10000) //Reagents are on turf level, should they be on liquids instead?
+ reagents.add_noreact_reagent_list(liquids.reagent_list)
+ if(reagents.process_reactions())//Any reactions happened, so re-calculate our reagents
+ liquids.reagent_list = list()
+ liquids.total_reagents = 0
+ for(var/r in reagents.reagent_list)
+ var/datum/reagent/R = r
+ liquids.reagent_list[R.type] = R.volume
+ liquids.total_reagents += R.volume
+
+ if(!liquids.total_reagents) //Our reaction exerted all of our reagents, remove self
+ qdel(reagents)
+ qdel(liquids)
+ return
+ qdel(reagents)
+ //Expose turf
+ liquids.ExposeMyTurf()
+
+ liquids.calculate_height()
+ liquids.set_reagent_color_for_liquid()
+ liquids.has_cached_share = FALSE
+ SSliquids.add_active_turf(src)
+ if(lgroup)
+ lgroup.dirty = TRUE
+
+/turf/proc/add_liquid(reagent, amount, no_react = FALSE)
+ if(!liquids)
+ liquids = new(src)
+ if(liquids.immutable)
+ return
+
+ var/prev_thermal_energy = liquids.total_reagents * liquids.temp
+
+ if(!liquids.reagent_list[reagent])
+ liquids.reagent_list[reagent] = 0
+ liquids.reagent_list[reagent] += amount
+ liquids.total_reagents += amount
+
+ liquids.temp = (amount + prev_thermal_energy) / liquids.total_reagents
+
+ if(!no_react)
+ //We do react so, make a simulation
+ create_reagents(10000)
+ reagents.add_noreact_reagent_list(liquids.reagent_list)
+ if(reagents.process_reactions())//Any reactions happened, so re-calculate our reagents
+ liquids.reagent_list = list()
+ liquids.total_reagents = 0
+ for(var/r in reagents.reagent_list)
+ var/datum/reagent/R = r
+ liquids.reagent_list[R.type] = R.volume
+ liquids.total_reagents += R.volume
+ qdel(reagents)
+ //Expose turf
+ liquids.ExposeMyTurf()
+
+ liquids.calculate_height()
+ liquids.set_reagent_color_for_liquid()
+ liquids.has_cached_share = FALSE
+ SSliquids.add_active_turf(src)
+ if(lgroup)
+ lgroup.dirty = TRUE
+
+/turf/proc/can_share_liquids_with(turf/T)
+ if(T.z != z) //No Z here handling currently
+ return FALSE
+
+ if(T.liquids && T.liquids.immutable)
+ return FALSE
+
+ if(istype(T, /turf/space)) //No space liquids - Maybe add an ice system later
+ return FALSE
+
+ var/my_liquid_height = liquids ? liquids.height : 0
+ if(my_liquid_height < 1)
+ return FALSE
+ var/target_height = T.liquids ? T.liquids.height : 0
+
+ //Varied heights handling:
+ if(liquid_height != T.liquid_height)
+ if(my_liquid_height+liquid_height < target_height + T.liquid_height + 1)
+ return FALSE
+ else
+ return TRUE
+
+ var/difference = abs(target_height - my_liquid_height)
+ //The: sand effect or "piling" Very good for performance
+ if(difference > 1) //SHOULD BE >= 1 or > 1? '>= 1' can lead into a lot of unnessecary processes, while ' > 1' will lead to a "piling" phenomena
+ return TRUE
+ return FALSE
+
+/turf/proc/process_liquid_cell()
+ if(!liquids)
+ if(!lgroup)
+ for(var/tur in get_atmos_adjacent_turfs())
+ var/turf/T2 = tur
+ if(T2.liquids)
+ if(T2.liquids.immutable)
+ SSliquids.active_immutables[T2] = TRUE
+ else if (T2.can_share_liquids_with(src))
+ if(T2.lgroup)
+ lgroup = new(liquid_height)
+ lgroup.add_to_group(src)
+ SSliquids.add_active_turf(T2)
+ SSliquids.remove_active_turf(src)
+ break
+ SSliquids.remove_active_turf(src)
+ return
+ if(!lgroup)
+ lgroup = new(liquid_height)
+ lgroup.add_to_group(src)
+ var/shared = lgroup.process_cell(src)
+ if(QDELETED(liquids)) //Liquids may be deleted in process cell
+ SSliquids.remove_active_turf(src)
+ return
+ if(!shared)
+ liquids.attrition++
+ if(liquids.attrition >= LIQUID_ATTRITION_TO_STOP_ACTIVITY)
+ SSliquids.remove_active_turf(src)
+
+/turf/proc/process_immutable_liquid()
+ var/any_share = FALSE
+ for(var/tur in get_atmos_adjacent_turfs())
+ var/turf/T = tur
+ if(can_share_liquids_with(T))
+ //Move this elsewhere sometime later?
+ if(T.liquids && T.liquids.height > liquids.height)
+ continue
+
+ any_share = TRUE
+ T.add_liquid_list(liquids.reagent_list, TRUE, liquids.temp)
+ if(!any_share)
+ SSliquids.active_immutables -= src
+
+//Consider making all of these behaviours a smart component/element? Something that's only applied wherever it needs to be
+//Could probably have the variables on the turf level, and the behaviours being activated/deactived on the component level as the vars are updated
+/turf/simulated/CanPass(atom/movable/mover, turf/location)
+ if(isliving(mover))
+ var/turf/current_turf = get_turf(mover)
+ if(current_turf && current_turf.turf_height - turf_height <= -TURF_HEIGHT_BLOCK_THRESHOLD)
+ return FALSE
+ return ..()
+
+/turf/simulated/Exit(atom/movable/mover, atom/newloc)
+ . = ..()
+ if(. && isliving(mover) && isturf(newloc))
+ var/mob/living/moving_mob = mover
+ if(moving_mob.mob_has_gravity())
+ var/turf/new_turf = get_turf(newloc)
+ if(new_turf && new_turf.turf_height - turf_height <= -TURF_HEIGHT_BLOCK_THRESHOLD)
+ moving_mob.handle_fall()
+
+// Handles climbing up and down between turfs with height differences, as well as manipulating others to do the same.
+/turf/simulated/MouseDrop_T(mob/dropped_mob, mob/user)
+ if(!isliving(dropped_mob) || !isliving(user) || !dropped_mob.mob_has_gravity() || !Adjacent(user) || !dropped_mob.Adjacent(user) || !(user.stat == CONSCIOUS) || user.lying)
+ return
+ if(!dropped_mob.mob_has_gravity())
+ return
+ var/turf/mob_turf = get_turf(dropped_mob)
+ if(!mob_turf)
+ return
+ if(mob_turf.turf_height - turf_height <= -TURF_HEIGHT_BLOCK_THRESHOLD)
+ //Climb up
+ if(user == dropped_mob)
+ user.visible_message("climbing...")
+ else
+ dropped_mob.visible_message("being pulled up...")
+ if(do_after(user, 2 SECONDS, dropped_mob))
+ dropped_mob.forceMove(src)
+ return
+ if(turf_height - mob_turf.turf_height <= -TURF_HEIGHT_BLOCK_THRESHOLD)
+ //Climb down
+ if(user == dropped_mob)
+ user.visible_message("climbing down...")
+ else
+ dropped_mob.visible_message("being lowered...")
+ if(do_after(user, 2 SECONDS, dropped_mob))
+ dropped_mob.forceMove(src)
+ return
diff --git a/code/modules/liquids/reagents/chemistry/holder.dm b/code/modules/liquids/reagents/chemistry/holder.dm
new file mode 100644
index 00000000000..ac219588959
--- /dev/null
+++ b/code/modules/liquids/reagents/chemistry/holder.dm
@@ -0,0 +1,5 @@
+/// Like add_reagent but you can enter a list. Adds them with no_react = TRUE. Format it like this: list(/datum/reagent/toxin = 10, "beer" = 15)
+/datum/reagents/proc/add_noreact_reagent_list(list/list_reagents, list/data=null)
+ for(var/r_id in list_reagents)
+ var/amt = list_reagents[r_id]
+ add_reagent(r_id, amt, data, safety = TRUE)
diff --git a/code/modules/liquids/reagents/chemistry/reagents.dm b/code/modules/liquids/reagents/chemistry/reagents.dm
new file mode 100644
index 00000000000..476dc5d7200
--- /dev/null
+++ b/code/modules/liquids/reagents/chemistry/reagents.dm
@@ -0,0 +1,219 @@
+/datum/reagent
+ ///Whether it will evaporate if left untouched on a liquids simulated puddle
+ var/evaporates = FALSE
+
+ ///How much fire power does the liquid have, for burning on simulated liquids. Not enough fire power/unit of entire mixture may result in no fire
+ var/liquid_fire_power = 0
+
+ ///How fast does the liquid burn on simulated turfs, if it does
+ var/liquid_fire_burnrate = 0.1
+
+ ///Whether a fire from this requires oxygen in the atmosphere
+ var/fire_needs_oxygen = TRUE
+
+/*
+* ALCOHOL REAGENTS
+*/
+/datum/reagent/ethanol
+ liquid_fire_power = 10
+ liquid_fire_burnrate = 0.1
+
+// 0 fire power
+/datum/reagent/ethanol/beer/light
+ liquid_fire_power = 0
+
+/datum/reagent/ethanol/threemileisland
+ liquid_fire_power = 0
+
+/datum/reagent/ethanol/grog
+ liquid_fire_power = 0
+
+/datum/reagent/ethanol/fetching_fizz
+ liquid_fire_power = 0
+
+/datum/reagent/ethanol/sugar_rush
+ liquid_fire_power = 0
+
+/datum/reagent/ethanol/crevice_spike
+ liquid_fire_power = 0
+
+/datum/reagent/ethanol/fanciulli
+ liquid_fire_power = 0
+
+// 2 fire power
+/datum/reagent/ethanol/beer
+ liquid_fire_power = 2
+
+/datum/reagent/ethanol/wine
+ liquid_fire_power = 2
+
+/datum/reagent/ethanol/lizardwine
+ liquid_fire_power = 2
+
+/datum/reagent/ethanol/amaretto
+ liquid_fire_power = 2
+
+/datum/reagent/ethanol/goldschlager
+ liquid_fire_power = 2
+
+/datum/reagent/ethanol/gintonic
+ liquid_fire_power = 2
+
+/datum/reagent/ethanol/iced_beer
+ liquid_fire_power = 2
+
+/datum/reagent/ethanol/irishcarbomb
+ liquid_fire_power = 2
+
+/datum/reagent/ethanol/hcider
+ liquid_fire_power = 2
+
+/datum/reagent/ethanol/narsour
+ liquid_fire_power = 2
+
+/datum/reagent/ethanol/peppermint_patty
+ liquid_fire_power = 2
+
+/datum/reagent/ethanol/blank_paper
+ liquid_fire_power = 2
+
+/datum/reagent/ethanol/applejack
+ liquid_fire_power = 2
+
+/datum/reagent/ethanol/jack_rose
+ liquid_fire_power = 2
+
+/datum/reagent/ethanol/old_timer
+ liquid_fire_power = 2
+
+/datum/reagent/ethanol/duplex
+ liquid_fire_power = 2
+
+/datum/reagent/ethanol/painkiller
+ liquid_fire_power = 2
+
+// 3 fire power
+/datum/reagent/ethanol/longislandicedtea
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/irishcoffee
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/margarita
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/manhattan
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/snowwhite
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/bahama_mama
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/singulo
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/red_mead
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/mead
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/aloe
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/andalusia
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/alliescocktail
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/amasec
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/erikasurprise
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/whiskey_sour
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/triple_sec
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/creme_de_menthe
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/creme_de_cacao
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/creme_de_coconut
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/quadruple_sec
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/grasshopper
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/stinger
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/bastion_bourbon
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/squirt_cider
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/amaretto_alexander
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/sidecar
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/mojito
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/moscow_mule
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/fruit_wine
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/champagne
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/pina_colada
+ liquid_fire_power = 3
+
+/datum/reagent/ethanol/ginger_amaretto
+ liquid_fire_power = 3
+
+// 4 fire power
+/datum/reagent/ethanol/rum_coke
+ liquid_fire_power = 4
+
+/datum/reagent/ethanol/booger
+ liquid_fire_power = 4
+
+/datum/reagent/ethanol/tequila_sunrise
+ liquid_fire_power = 4
+
+/*
+* PYROTECHNIC REAGENTS
+*/
+/datum/reagent/thermite
+ liquid_fire_power = 20
+ liquid_fire_burnrate = 0.3
+
+/*
+* OTHER
+*/
+
+/datum/reagent/fuel
+ liquid_fire_power = 10
+ liquid_fire_burnrate = 0.2
+
+/datum/reagent/plasma
+ liquid_fire_power = 15
+ liquid_fire_burnrate = 0.2
diff --git a/code/modules/liquids/reagents/reagent_containers.dm b/code/modules/liquids/reagents/reagent_containers.dm
new file mode 100644
index 00000000000..28449198ed5
--- /dev/null
+++ b/code/modules/liquids/reagents/reagent_containers.dm
@@ -0,0 +1,46 @@
+/obj/item/reagent_containers/Initialize(mapload, vol)
+ . = ..()
+
+ AddComponent(/datum/component/liquids_interaction, /obj/item/reagent_containers/vessel/proc/attack_on_liquids_turf)
+
+/**
+ * Proc to remove liquids from a turf using a reagent container.
+ *
+ * Arguments:
+ * * tile - On which tile we're trying to absorb liquids
+ * * user - Who tries to absorb liquids with this?
+ * * liquids - Liquids we're trying to absorb.
+ */
+/obj/item/reagent_containers/vessel/proc/attack_on_liquids_turf(turf/target_turf, mob/living/user, obj/effect/abstract/liquid_turf/liquids)
+ if(user.a_intent == I_HURT)
+ return FALSE
+
+ if(!can_be_splashed)
+ return FALSE
+
+ if(!user.Adjacent(target_turf))
+ return FALSE
+
+ if(liquids.fire_state) //Use an extinguisher first
+ to_chat(user, SPAN_WARNING("You can't scoop up anything while it's on fire!"))
+ return TRUE
+
+ if(liquids.height == 1)
+ to_chat(user, SPAN_WARNING("The puddle is too shallow to scoop anything up!"))
+ return TRUE
+
+ var/free_space = reagents.maximum_volume - reagents.total_volume
+ if(free_space <= 0)
+ to_chat(user, SPAN_WARNING("You can't fit any more liquids inside [src]!"))
+ return TRUE
+
+ var/desired_transfer = amount_per_transfer_from_this
+ if(desired_transfer > free_space)
+ desired_transfer = free_space
+
+ var/datum/reagents/tempr = liquids.take_reagents_flat(desired_transfer)
+ tempr.trans_to(reagents, tempr.total_volume)
+ to_chat(user, SPAN_NOTICE("You scoop up around [amount_per_transfer_from_this] units of liquids with [src]."))
+ qdel(tempr)
+ user.setClickCooldown(DEFAULT_ATTACK_COOLDOWN)
+ return TRUE
diff --git a/code/modules/liquids/tools.dm b/code/modules/liquids/tools.dm
new file mode 100644
index 00000000000..12617dcb09d
--- /dev/null
+++ b/code/modules/liquids/tools.dm
@@ -0,0 +1,41 @@
+/client/proc/spawn_liquid()
+ set category = "Fun"
+ set name = "Spawn Liquid"
+ set desc = "Spawns an amount of chosen liquid at your current location."
+
+ var/choice
+ var/valid_id
+ while(!valid_id)
+ choice = tgui_input_text(usr, "Enter the ID of the reagent you want to add.", "Search reagents")
+ if(isnull(choice)) //Get me out of here!
+ break
+ if (!ispath(text2path(choice)))
+ choice = pick_closest_path(choice, make_types_fancy(subtypesof(/datum/reagent)))
+ if (ispath(choice))
+ valid_id = TRUE
+ else
+ valid_id = TRUE
+ if(!valid_id)
+ to_chat(usr, SPAN_WARNING("A reagent with that ID doesn't exist!"))
+ if(!choice)
+ return
+ var/volume = tgui_input_number(usr, "Volume:", "Choose volume")
+ if(!volume)
+ return
+ var/turf/epicenter = get_turf(mob)
+ epicenter.add_liquid(choice, volume)
+ message_admins("[usr] ([usr.ckey]) spawned liquid at [epicenter.loc] ([choice] - [volume]).")
+ log_admin("[key_name(usr)] spawned liquid at [epicenter.loc] ([choice] - [volume]).")
+
+/client/proc/remove_liquid(turf/epicenter in world)
+ set name = "Remove Liquids"
+ set category = "Fun"
+ set desc = "Remove liquids in a range."
+
+ var/range = input(usr, "Enter range:", "Range selection", 2) as num
+
+ for(var/obj/effect/abstract/liquid_turf/liquid in range(range, epicenter))
+ qdel(liquid, TRUE)
+
+ message_admins("[key_name_admin(usr)] removed liquids with range [range] in [epicenter.loc.name]")
+ log_game("[key_name_admin(usr)] removed liquids with range [range] in [epicenter.loc.name]")
diff --git a/code/modules/maps/map_template.dm b/code/modules/maps/map_template.dm
index cdc9ef95f07..d3cd7db4ab1 100644
--- a/code/modules/maps/map_template.dm
+++ b/code/modules/maps/map_template.dm
@@ -73,7 +73,7 @@
for (var/i in turfs)
var/turf/T = i
T.post_change()
- if(template_flags & TEMPLATE_FLAG_NO_RUINS)
+ if(template_flags & TEMPLATE_FLAG_TURF_FLAG_NORUINS)
T.turf_flags |= TURF_FLAG_NORUINS
/datum/map_template/proc/init_shuttles()
diff --git a/code/modules/modifier/status_effect.dm b/code/modules/modifier/status_effect.dm
new file mode 100644
index 00000000000..caacf4d7bfc
--- /dev/null
+++ b/code/modules/modifier/status_effect.dm
@@ -0,0 +1,16 @@
+/datum/modifier/status_effect
+ var/duration = 0
+ var/alert_type = null
+ var/obj/screen/movable/alert/linked_alert = null
+
+/datum/modifier/status_effect/New(new_holder, new_origin)
+ ..()
+ if(duration)
+ expire_at = world.time + duration
+
+ if(alert_type)
+ linked_alert = holder.throw_alert("\ref[src]",alert_type)
+
+/datum/modifier/status_effect/on_expire()
+ . = ..()
+ holder.clear_alert("\ref[src]")
diff --git a/code/modules/multiz/movement.dm b/code/modules/multiz/movement.dm
index 3516ee09550..f6c0c56e191 100644
--- a/code/modules/multiz/movement.dm
+++ b/code/modules/multiz/movement.dm
@@ -170,6 +170,7 @@
return species.can_fall(src)
/atom/movable/proc/handle_fall(turf/landing)
+ SEND_SIGNAL(src, SIGNAL_ATOM_FALL)
forceMove(landing)
if(locate(/obj/structure/stairs) in landing)
diff --git a/code/modules/reagents/Chemistry-Holder.dm b/code/modules/reagents/Chemistry-Holder.dm
index 1b00363a6fd..3a383188d95 100644
--- a/code/modules/reagents/Chemistry-Holder.dm
+++ b/code/modules/reagents/Chemistry-Holder.dm
@@ -348,6 +348,9 @@ GLOBAL_DATUM_INIT(temp_reagents_holder, /obj, new)
amount -= spill
if(spill)
splash(target.loc, spill, multiplier, copy, min_spill, max_spill)
+ if(isturf(target))
+ var/turf/T = target
+ T.add_liquid_from_reagents(src)
trans_to(target, amount, multiplier, copy)
diff --git a/code/modules/reagents/Chemistry-Reagents/basic.dm b/code/modules/reagents/Chemistry-Reagents/basic.dm
index 3047422656e..3a4861a21fd 100644
--- a/code/modules/reagents/Chemistry-Reagents/basic.dm
+++ b/code/modules/reagents/Chemistry-Reagents/basic.dm
@@ -9,6 +9,7 @@
glass_name = "water"
glass_desc = "The father of all refreshments."
var/slippery = 1
+ evaporates = TRUE
/datum/reagent/water/affect_blood(mob/living/carbon/M, alien, removed)
if(!istype(M, /mob/living/carbon/metroid) && alien != IS_METROID)
diff --git a/code/modules/reagents/reagent_containers.dm b/code/modules/reagents/reagent_containers.dm
index 3a1573f90ea..f624dc579a5 100644
--- a/code/modules/reagents/reagent_containers.dm
+++ b/code/modules/reagents/reagent_containers.dm
@@ -34,13 +34,23 @@
/obj/item/reagent_containers/attack_self(mob/user)
return
-/obj/item/reagent_containers/afterattack(obj/target, mob/user, flag)
+/obj/item/reagent_containers/afterattack(atom/A, mob/user, proximity)
+ if(!proximity)
+ return
+
+ var/turf/turf_to_clean = A
+
+ // Disable normal cleaning if there are liquids.
+ if(isturf(A) && turf_to_clean.liquids)
+ SEND_SIGNAL(src, SIGNAL_CLEAN_LIQUIDS, turf_to_clean, user)
+ return FALSE
+
if(can_be_splashed && user.a_intent != I_HELP)
- if(standard_splash_mob(user,target))
+ if(standard_splash_mob(user,A))
return
if(reagents && reagents.total_volume)
- to_chat(user, SPAN_NOTICE("You splash the contents of \the [src] onto [target].")) // They are not on help intent, aka wanting to spill it.
- reagents.splash(target, reagents.total_volume)
+ to_chat(user, SPAN_NOTICE("You splash the contents of \the [src] onto [A].")) // They are not on help intent, aka wanting to spill it.
+ reagents.splash(A, reagents.total_volume)
return
/obj/item/reagent_containers/proc/reagentlist() // For attack logs
diff --git a/code/modules/reagents/reagent_containers/vessel/unsorted.dm b/code/modules/reagents/reagent_containers/vessel/unsorted.dm
index 08a7778d5af..9ecb0d04d16 100644
--- a/code/modules/reagents/reagent_containers/vessel/unsorted.dm
+++ b/code/modules/reagents/reagent_containers/vessel/unsorted.dm
@@ -40,7 +40,21 @@
/obj/item/reagent_containers/vessel/bucket/full
startswith = list(/datum/reagent/water)
-/obj/item/reagent_containers/vessel/bucket/attackby(obj/D, mob/user)
+/obj/item/reagent_containers/vessel/bucket/ShiftClick(mob/user)
+ . = ..()
+ var/obj/O = user.get_active_hand()
+ if(istype(O, /obj/item/mop))
+ if(O.reagents.total_volume == 0)
+ to_chat(user, "[O] is dry, you can't squeeze anything out!")
+ return
+ if(reagents.total_volume == reagents.maximum_volume)
+ to_chat(user, "[src] is full!")
+ return
+ O.reagents.remove_any(O.reagents.total_volume * SQUEEZING_DISPERSAL_RATIO)
+ O.reagents.trans_to(src, O.reagents.total_volume)
+ to_chat(user, "You squeeze the liquids from [O] to [src].")
+
+/obj/item/reagent_containers/vessel/bucket/attackby(obj/D, mob/user, click_params)
if(isprox(D))
to_chat(user, "You add [D] to [src].")
qdel(D)
diff --git a/icons/effects/liquid.dmi b/icons/effects/liquid.dmi
index 258a0fb2e62..71c82c7566c 100644
Binary files a/icons/effects/liquid.dmi and b/icons/effects/liquid.dmi differ
diff --git a/icons/effects/liquid_overlays.dmi b/icons/effects/liquid_overlays.dmi
new file mode 100644
index 00000000000..cf6ff903337
Binary files /dev/null and b/icons/effects/liquid_overlays.dmi differ
diff --git a/icons/effects/splash.dmi b/icons/effects/splash.dmi
new file mode 100644
index 00000000000..f2cb774f59e
Binary files /dev/null and b/icons/effects/splash.dmi differ
diff --git a/icons/obj/flora/rocks.dmi b/icons/obj/flora/rocks.dmi
deleted file mode 100644
index a1f6a0df0a9..00000000000
Binary files a/icons/obj/flora/rocks.dmi and /dev/null differ
diff --git a/icons/obj/liquids/structures/drains.dmi b/icons/obj/liquids/structures/drains.dmi
new file mode 100644
index 00000000000..5c305b7d237
Binary files /dev/null and b/icons/obj/liquids/structures/drains.dmi differ
diff --git a/icons/obj/liquids/structures/liquid_pump.dmi b/icons/obj/liquids/structures/liquid_pump.dmi
new file mode 100644
index 00000000000..012dda8bb5e
Binary files /dev/null and b/icons/obj/liquids/structures/liquid_pump.dmi differ
diff --git a/icons/turf/elevated_plasteel.dmi b/icons/turf/elevated_plasteel.dmi
new file mode 100644
index 00000000000..43c82bff75a
Binary files /dev/null and b/icons/turf/elevated_plasteel.dmi differ
diff --git a/icons/turf/lowered_plasteel.dmi b/icons/turf/lowered_plasteel.dmi
new file mode 100644
index 00000000000..c64af2d9b68
Binary files /dev/null and b/icons/turf/lowered_plasteel.dmi differ
diff --git a/icons/turf/pool_tile.dmi b/icons/turf/pool_tile.dmi
new file mode 100644
index 00000000000..b5ccf9e54c3
Binary files /dev/null and b/icons/turf/pool_tile.dmi differ
diff --git a/sound/effects/heart_beat_loop3.ogg b/sound/effects/heart_beat_loop3.ogg
new file mode 100644
index 00000000000..ca647ff78b9
Binary files /dev/null and b/sound/effects/heart_beat_loop3.ogg differ
diff --git a/sound/effects/heart_beat_once.ogg b/sound/effects/heart_beat_once.ogg
new file mode 100644
index 00000000000..d8f6f9343b9
Binary files /dev/null and b/sound/effects/heart_beat_once.ogg differ
diff --git a/sound/effects/splash.ogg b/sound/effects/splash.ogg
new file mode 100644
index 00000000000..22f17c07901
Binary files /dev/null and b/sound/effects/splash.ogg differ
diff --git a/sound/effects/water_wade1.ogg b/sound/effects/water_wade1.ogg
new file mode 100644
index 00000000000..27527f9d8b8
Binary files /dev/null and b/sound/effects/water_wade1.ogg differ
diff --git a/sound/effects/water_wade2.ogg b/sound/effects/water_wade2.ogg
new file mode 100644
index 00000000000..f9c15cab64a
Binary files /dev/null and b/sound/effects/water_wade2.ogg differ
diff --git a/sound/effects/water_wade3.ogg b/sound/effects/water_wade3.ogg
new file mode 100644
index 00000000000..3daccae01a1
Binary files /dev/null and b/sound/effects/water_wade3.ogg differ
diff --git a/sound/effects/water_wade4.ogg b/sound/effects/water_wade4.ogg
new file mode 100644
index 00000000000..7ba705f4991
Binary files /dev/null and b/sound/effects/water_wade4.ogg differ
diff --git a/sound/effects/watersplash.ogg b/sound/effects/watersplash.ogg
new file mode 100644
index 00000000000..a9a6e486087
Binary files /dev/null and b/sound/effects/watersplash.ogg differ