diff --git a/code/__DEFINES/atmospherics.dm b/code/__DEFINES/atmospherics.dm
index aaa6a71d86d7..90deadaa9ea4 100644
--- a/code/__DEFINES/atmospherics.dm
+++ b/code/__DEFINES/atmospherics.dm
@@ -303,6 +303,7 @@ GLOBAL_LIST_INIT(atmos_adjacent_savings, list(0,0))
#define LAVALAND_DEFAULT_ATMOS "o2=14;n2=23;TEMP=300"
#define ICEMOON_DEFAULT_ATMOS "o2=14;n2=23;TEMP=180"
#define JUNGLELAND_DEFAULT_ATMOS "o2=44;n2=164;TEMP=300" //yogs edit
+#define OCEAN_DEFAULT_ATMOS "o2=10;co2=10;TEMP=293.15"
//ATMOSIA GAS MONITOR TAGS
#define ATMOS_GAS_MONITOR_INPUT_O2 "o2_in"
diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_x_act.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_x_act.dm
index 4d3eea4929aa..90e401bdb248 100644
--- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_x_act.dm
+++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_x_act.dm
@@ -44,3 +44,4 @@
#define COMSIG_ATOM_SECONDARY_TOOL_ACT(tooltype) "tool_secondary_act_[tooltype]"
// We have the same returns here as COMSIG_ATOM_TOOL_ACT
// #define COMPONENT_BLOCK_TOOL_ATTACK (1<<0)
+#define COMSIG_ATOM_DOOR_OPEN "atom_door_open"
diff --git a/code/__DEFINES/dcs/signals/signals_turf.dm b/code/__DEFINES/dcs/signals/signals_turf.dm
index a821d243c956..1f873257c0a6 100644
--- a/code/__DEFINES/dcs/signals/signals_turf.dm
+++ b/code/__DEFINES/dcs/signals/signals_turf.dm
@@ -47,3 +47,13 @@
#define COMSIG_TURF_IGNITED "turf_ignited"
///Prevents hotspots and turf fires
#define SUPPRESS_FIRE (1<<0)
+
+///called on liquid creation
+#define COMSIG_TURF_LIQUIDS_CREATION "turf_liquids_creation"
+
+#define COMSIG_TURF_MOB_FALL "turf_mob_fall"
+
+///this is called whenever a turf is destroyed
+#define COMSIG_TURF_DESTROY "turf_destroy"
+///this is called whenever a turfs air is updated
+#define COMSIG_TURF_UPDATE_AIR "turf_air_change"
diff --git a/code/__DEFINES/icon_smoothing.dm b/code/__DEFINES/icon_smoothing.dm
index 1275dd4b7180..6b5b77537d60 100644
--- a/code/__DEFINES/icon_smoothing.dm
+++ b/code/__DEFINES/icon_smoothing.dm
@@ -160,6 +160,8 @@ DEFINE_BITFIELD(smoothing_junction, list(
#define SMOOTH_GROUP_BAMBOO_WALLS S_TURF(17) //![/turf/closed/wall/mineral/bamboo, /obj/structure/falsewall/bamboo]
#define SMOOTH_GROUP_PLASTINUM_WALLS S_TURF(18) //![turf/closed/indestructible/riveted/plastinum]
#define SMOOTH_GROUP_CLOCKWORK_WALLS S_TURF(19) //![/turf/closed/wall/clockwork, /obj/structure/falsewall/brass]
+#define SMOOTH_GROUP_ELEVATED_PLASTEEL S_TURF(20)
+#define SMOOTH_GROUP_LOWERED_PLASTEEL S_TURF(21)
#define SMOOTH_GROUP_PAPERFRAME S_OBJ(21) ///obj/structure/window/paperframe, /obj/structure/mineral_door/paperframe
@@ -203,6 +205,8 @@ DEFINE_BITFIELD(smoothing_junction, list(
#define SMOOTH_GROUP_GAS_TANK S_OBJ(72)
+#define SMOOTH_GROUP_WATER S_OBJ(73) ///obj/effect/abstract/liquid_turf
+
/// Performs the work to set smoothing_groups and canSmoothWith.
/// An inlined function used in both turf/Initialize and atom/Initialize.
diff --git a/code/__DEFINES/movespeed_modification.dm b/code/__DEFINES/movespeed_modification.dm
index e8254f615d5e..bcc230185046 100644
--- a/code/__DEFINES/movespeed_modification.dm
+++ b/code/__DEFINES/movespeed_modification.dm
@@ -83,3 +83,5 @@
#define MOVESPEED_ID_RESIN_FOAM "RESIN_FOAM"
#define MOVESPEED_ID_SYNTH_SUSPICION "SYNTH_SUSPICION"
+
+#define MOVESPEED_ID_LIQUID "LIQUID"
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index 678052fbebc1..34efaa265b22 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -332,3 +332,13 @@
//Wardrobe callback master list indexes
#define WARDROBE_CALLBACK_INSERT 1
#define WARDROBE_CALLBACK_REMOVE 2
+
+///liquid defines
+#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 SSLIQUIDS_RUN_TYPE_OCEAN 6
+#define SSLIQUIDS_RUN_TYPE_TEMPERATURE 7
+#define SSLIQUIDS_RUN_TYPE_CACHED_EDGES 8
diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index 212d327e7ace..c237895dbeea 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -285,3 +285,6 @@
///reduces the cooldown of all used /datum/action/cooldown by 25%
#define TRAIT_FAST_COOLDOWNS "short_spell_cooldowns"
+
+/// One can breath under water, you get me?
+#define TRAIT_WATER_BREATHING "water_breathing"
diff --git a/code/__DEFINES/turfs.dm b/code/__DEFINES/turfs.dm
index 7b972abc20cb..6ff0d233ac0c 100644
--- a/code/__DEFINES/turfs.dm
+++ b/code/__DEFINES/turfs.dm
@@ -1,7 +1,7 @@
#define CHANGETURF_DEFER_CHANGE (1<<0)
#define CHANGETURF_IGNORE_AIR (1<<1) // This flag prevents changeturf from gathering air from nearby turfs to fill the new turf with an approximation of local air
#define CHANGETURF_FORCEOP (1<<2)
-#define CHANGETURF_SKIP (1<<3) // A flag for PlaceOnTop to just instance the new turf instead of calling ChangeTurf. Used for uninitialized turfs NOTHING ELSE
+#define CHANGETURF_SKIP (1<<3) // A flag for place_on_top to just instance the new turf instead of calling ChangeTurf. Used for uninitialized turfs NOTHING ELSE
#define CHANGETURF_INHERIT_AIR (1<<4) // Inherit air from previous turf. Implies CHANGETURF_IGNORE_AIR
#define CHANGETURF_RECALC_ADJACENT (1<<5) //Immediately recalc adjacent atmos turfs instead of queuing.
#define CHANGETURF_TRAPDOOR_INDUCED (1<<6) // Caused by a trapdoor, for trapdoor to know that this changeturf was caused by itself
diff --git a/code/__DEFINES/{yogs_defines}/liquids.dm b/code/__DEFINES/{yogs_defines}/liquids.dm
new file mode 100644
index 000000000000..362543279aec
--- /dev/null
+++ b/code/__DEFINES/{yogs_defines}/liquids.dm
@@ -0,0 +1,86 @@
+#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_MEMBER_PROCESSES 10
+
+#define REQUIRED_EVAPORATION_PROCESSES 20
+#define EVAPORATION_CHANCE 50
+
+#define REQUIRED_FIRE_PROCESSES 10
+#define REQUIRED_FIRE_POWER_PER_UNIT 5
+#define FIRE_BURN_PERCENT 10
+
+#define REQUIRED_OCEAN_PROCESSES 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 255
+
+//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 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,(!MOBILITY_STAND ? liquids.liquid_group.group_overlay_state+LYING_DOWN_SUBMERGEMENT_STATE_BONUS : liquids.liquid_group.group_overlay_state)/TOTAL_LIQUID_STATES)
+
+#define LIQUID_PROTECTION "liquid_protection"
+
+GLOBAL_LIST_INIT(liquid_blacklist, list(
+ /datum/reagent/sorium,
+ /datum/reagent/liquid_dark_matter
+))
diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm
index b41a91e0ea0b..c65816553c63 100644
--- a/code/game/area/areas.dm
+++ b/code/game/area/areas.dm
@@ -878,7 +878,7 @@ GLOBAL_LIST_EMPTY(teleportlocs)
CRASH("Bad op: area/drop_location() called")
/// A hook so areas can modify the incoming args (of what??)
-/area/proc/PlaceOnTopReact(list/new_baseturfs, turf/fake_turf_type, flags)
+/area/proc/place_on_topReact(list/new_baseturfs, turf/fake_turf_type, flags)
return flags
/// Called when a living mob that spawned here, joining the round, receives the player client.
diff --git a/code/game/area/areas/shuttles.dm b/code/game/area/areas/shuttles.dm
index 37c511734e47..71183599bf9d 100644
--- a/code/game/area/areas/shuttles.dm
+++ b/code/game/area/areas/shuttles.dm
@@ -20,7 +20,7 @@
///list of miners & their mining points from gems to be given once all exports are processed, used by supply shuttles
var/list/gem_payout = list()
-/area/shuttle/PlaceOnTopReact(list/new_baseturfs, turf/fake_turf_type, flags)
+/area/shuttle/place_on_topReact(list/new_baseturfs, turf/fake_turf_type, flags)
. = ..()
if(length(new_baseturfs) > 1 || fake_turf_type)
return // More complicated larger changes indicate this isn't a player
diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm
index 444e9d64a047..2129b7745685 100644
--- a/code/game/machinery/doors/airlock.dm
+++ b/code/game/machinery/doors/airlock.dm
@@ -1373,6 +1373,7 @@
locked = !locked
if(welded)
welded = !welded
+ SEND_SIGNAL(src, COMSIG_AIRLOCK_OPEN, forced)
operating = TRUE
update_icon(state = AIRLOCK_OPENING, override = TRUE)
sleep(0.1 SECONDS)
@@ -1389,6 +1390,7 @@
if(delayed_close_requested)
delayed_close_requested = FALSE
addtimer(CALLBACK(src, PROC_REF(close)), 1)
+ SEND_SIGNAL(src, COMSIG_ATOM_DOOR_OPEN) /// this is different because we need one that covers all doors
return TRUE
@@ -1418,6 +1420,20 @@
if(killthis)
SSexplosions.med_mov_atom += killthis
+ SEND_SIGNAL(src, COMSIG_AIRLOCK_CLOSE, forced)
+
+ var/turf/open/open_turf = get_turf(src)
+ if(open_turf.liquids)
+ var/datum/liquid_group/turfs_group = open_turf.liquids.liquid_group
+ turfs_group.remove_from_group(open_turf)
+ qdel(open_turf.liquids)
+ turfs_group.try_split(open_turf)
+ for(var/dir in GLOB.cardinals)
+ var/turf/open/direction_turf = get_step(open_turf, dir)
+ if(!isopenturf(direction_turf) || !direction_turf.liquids)
+ continue
+ turfs_group.check_edges(direction_turf)
+
operating = TRUE
update_icon(state = AIRLOCK_CLOSING, override = TRUE)
layer = CLOSED_DOOR_LAYER
diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm
index 2993c9b8d9e2..efe8c3782256 100644
--- a/code/game/machinery/doors/door.dm
+++ b/code/game/machinery/doors/door.dm
@@ -379,6 +379,7 @@
operating = FALSE
air_update_turf()
update_freelook_sight()
+ SEND_SIGNAL(src, COMSIG_ATOM_DOOR_OPEN)
if(autoclose)
spawn(autoclose)
close()
@@ -399,6 +400,19 @@
operating = TRUE
do_animate("closing")
+
+ var/turf/open/open_turf = get_turf(src)
+ if(open_turf.liquids)
+ var/datum/liquid_group/turfs_group = open_turf.liquids.liquid_group
+ turfs_group.remove_from_group(open_turf)
+ qdel(open_turf.liquids)
+ turfs_group.try_split(open_turf)
+ for(var/dir in GLOB.cardinals)
+ var/turf/open/direction_turf = get_step(open_turf, dir)
+ if(!isopenturf(direction_turf) || !direction_turf.liquids)
+ continue
+ turfs_group.check_edges(direction_turf)
+
layer = closingLayer
if(air_tight)
density = TRUE
diff --git a/code/game/objects/items/miscellaneous.dm b/code/game/objects/items/miscellaneous.dm
index c70dfbe84aea..83895c94fb15 100644
--- a/code/game/objects/items/miscellaneous.dm
+++ b/code/game/objects/items/miscellaneous.dm
@@ -163,3 +163,14 @@
user.gib()
playsound(src, 'sound/items/eatfood.ogg', 50, 1, -1)
return MANUAL_SUICIDE
+
+/obj/item/choice_beacon/liquids
+ name = "Free Liquid Pump Kit"
+ desc = "Kit containing a free liquid pump from SPAAAACE."
+ icon = 'icons/obj/device.dmi'
+ icon_state = "gangtool-blue"
+ item_state = "radio"
+
+// LIQUIDS TM REMOVE THIS
+/obj/item/choice_beacon/liquids/generate_display_names()
+ return list("Liquid Pump" = /obj/structure/liquid_pump)
diff --git a/code/game/objects/items/mop.dm b/code/game/objects/items/mop.dm
index 982116fe9651..e0870f3ebebb 100644
--- a/code/game/objects/items/mop.dm
+++ b/code/game/objects/items/mop.dm
@@ -14,7 +14,7 @@
resistance_flags = FLAMMABLE
var/mopping = 0
var/mopcount = 0
- var/mopcap = 15
+ var/mopcap = 45
var/mopspeed = 15
force_string = "robust... against germs"
var/insertable = TRUE
@@ -22,6 +22,7 @@
/obj/item/mop/Initialize(mapload)
. = ..()
create_reagents(mopcap, REFILLABLE)
+ AddComponent(/datum/component/liquids_interaction, TYPE_PROC_REF(/obj/item/mop, attack_on_liquids_turf))
/obj/item/mop/proc/clean(turf/A)
@@ -46,6 +47,11 @@
return
if(T)
+ // Disable normal cleaning if there are liquids.
+ if(T.liquids)
+ to_chat(user, span_warning("It would be quite difficult to clean this with a pool of liquids on top!"))
+ return
+
user.visible_message("[user] begins to clean \the [T] with [src].", span_notice("You begin to clean \the [T] with [src]..."))
var/realspeed = mopspeed
@@ -73,6 +79,33 @@
to_chat(user, span_warning("You are unable to fit your [name] into the [J.name]."))
return
+/obj/item/mop/proc/attack_on_liquids_turf(obj/item/mop/the_mop, turf/target, mob/user, obj/effect/abstract/liquid_turf/liquids)
+ if(!user.Adjacent(target))
+ return FALSE
+ var/free_space = mopcap - reagents.total_volume
+ var/speed_mult = 1
+ var/datum/liquid_group/targeted_group = target?.liquids?.liquid_group
+ while(!QDELETED(targeted_group))
+ if(speed_mult >= 0.2)
+ speed_mult -= 0.05
+ if(free_space <= 0)
+ to_chat(user, span_warning("You cant absorb any more liquid with \the [src]!"))
+ return TRUE
+ if(!do_after(user, src.mopspeed * speed_mult, target = target))
+ break
+ if(the_mop.reagents.total_volume == the_mop.mopcap)
+ to_chat(user, span_warning("You cant absorb any more liquid with \the [src]!"))
+ break
+ if(targeted_group?.reagents_per_turf)
+ targeted_group?.trans_to_seperate_group(the_mop.reagents, min(targeted_group?.reagents_per_turf, 5))
+ to_chat(user, span_notice("You soak up some liquids with \the [src]."))
+ else if(!QDELETED(target?.liquids?.liquid_group))
+ targeted_group = target.liquids.liquid_group
+ else
+ break
+ user.changeNext_move(CLICK_CD_MELEE)
+ return TRUE
+
/obj/item/mop/cyborg
insertable = FALSE
diff --git a/code/game/objects/structures/mop_bucket.dm b/code/game/objects/structures/mop_bucket.dm
index 6ba389ed11e7..fdc79fbdc9f8 100644
--- a/code/game/objects/structures/mop_bucket.dm
+++ b/code/game/objects/structures/mop_bucket.dm
@@ -39,6 +39,30 @@
return
return ..()
+/obj/structure/mop_bucket/attackby_secondary(obj/item/weapon, mob/user, params)
+ if(istype(weapon, /obj/item/mop))
+ if(!weapon.reagents.total_volume)
+ if(weapon.reagents.total_volume >= weapon.reagents.maximum_volume)
+ balloon_alert(user, "mop is already soaked!")
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+ if(!reagents.total_volume < 1)
+ balloon_alert(user, "mop bucket is empty!")
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+ reagents.trans_to(weapon, weapon.reagents.maximum_volume, transfered_by = user)
+ balloon_alert(user, "wet mop")
+ playsound(src, 'sound/effects/slosh.ogg', 25, vary = TRUE)
+ else
+ var/obj/item/mop/attacked_mop = weapon
+ to_chat(user, "You completly wring out the [attacked_mop.name] into the waste bucket of the cart.")
+ attacked_mop.reagents.remove_all(attacked_mop.mopcap)
+
+ if(istype(weapon, /obj/item/reagent_containers) || istype(weapon, /obj/item/mop))
+ update_appearance(UPDATE_OVERLAYS)
+ return SECONDARY_ATTACK_CONTINUE_CHAIN // skip attack animations when refilling cart
+
+ return SECONDARY_ATTACK_CONTINUE_CHAIN
+
+
/obj/structure/mopbucket/update_overlays()
. = ..()
if(reagents.total_volume > 0)
diff --git a/code/game/turfs/baseturfs.dm b/code/game/turfs/baseturfs.dm
index 6a0fdfd04be5..d5a85213fb4a 100644
--- a/code/game/turfs/baseturfs.dm
+++ b/code/game/turfs/baseturfs.dm
@@ -40,11 +40,11 @@
/// Places a turf on top - for map loading
/turf/proc/load_on_top(turf/added_layer, flags)
var/area/our_area = get_area(src)
- flags = our_area.PlaceOnTopReact(list(baseturfs), added_layer, flags)
+ flags = our_area.place_on_topReact(list(baseturfs), added_layer, flags)
if(flags & CHANGETURF_SKIP) // We haven't been initialized
if(flags_1 & INITIALIZED_1)
- stack_trace("CHANGETURF_SKIP was used in a PlaceOnTop call for a turf that's initialized. This is a mistake. [src]([type])")
+ stack_trace("CHANGETURF_SKIP was used in a place_on_top call for a turf that's initialized. This is a mistake. [src]([type])")
assemble_baseturfs()
var/turf/new_turf
diff --git a/code/game/turfs/change_turf.dm b/code/game/turfs/change_turf.dm
index 5f17e2711660..0e5a71f500d3 100644
--- a/code/game/turfs/change_turf.dm
+++ b/code/game/turfs/change_turf.dm
@@ -82,6 +82,7 @@ GLOBAL_LIST_INIT(blacklisted_automated_baseturfs, typecacheof(list(
var/old_lighting_corner_NW = lighting_corner_NW
var/old_directional_opacity = directional_opacity
var/old_dynamic_lumcount = dynamic_lumcount
+
var/old_rcd_memory = rcd_memory
var/old_explosion_throw_details = explosion_throw_details
var/old_opacity = opacity
@@ -206,6 +207,14 @@ GLOBAL_LIST_INIT(blacklisted_automated_baseturfs, typecacheof(list(
//don't
if(!SSair.initialized)
return ..()
+ var/obj/effect/abstract/liquid_turf/old_liquids = liquids
+ var/datum/liquid_group/old_group = liquids?.liquid_group
+ var/evaporating = FALSE
+ if(old_group)
+ old_group.remove_from_group(liquids.my_turf)
+ if(SSliquids.evaporation_queue[src])
+ evaporating = TRUE
+ SSliquids.evaporation_queue -= src
if ((flags & CHANGETURF_INHERIT_AIR) && ispath(path, /turf/open))
var/datum/gas_mixture/stashed_air = new()
stashed_air.copy_from(air)
@@ -223,9 +232,17 @@ GLOBAL_LIST_INIT(blacklisted_automated_baseturfs, typecacheof(list(
QDEL_NULL(newTurf.air)
newTurf.air = stashed_air
update_air_ref(planetary_atmos ? 1 : 2)
+ if(old_liquids)
+ old_liquids.my_turf = newTurf
+ newTurf.liquids = old_liquids
+ old_group.add_to_group(newTurf)
+ if(evaporating)
+ SSliquids.evaporation_queue[newTurf] = TRUE
else
if(turf_fire)
qdel(turf_fire)
+ if(old_liquids)
+ qdel(old_liquids)
if(ispath(path, /turf/closed) || ispath(path, /turf/cordon))
flags |= CHANGETURF_RECALC_ADJACENT
update_air_ref(-1)
diff --git a/code/game/turfs/open/_open.dm b/code/game/turfs/open/_open.dm
index 8cd4221d8228..5a056c3ba632 100644
--- a/code/game/turfs/open/_open.dm
+++ b/code/game/turfs/open/_open.dm
@@ -93,7 +93,7 @@
* This replaces the current turf if it is plating and is passed plating, is tile and is passed tile.
* It places the new turf on top of itself if it is plating and is passed a tile.
* It also replaces the turf if it is tile and is passed plating, essentially destroying the over turf.
- * Flags argument is passed directly to ChangeTurf or PlaceOnTop
+ * Flags argument is passed directly to ChangeTurf or place_on_top
*/
/turf/open/proc/replace_floor(turf/open/new_floor_path, flags)
if (!overfloor_placed && initial(new_floor_path.overfloor_placed))
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index a195e07f1a49..fac9984035c4 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -215,6 +215,7 @@ GLOBAL_LIST_EMPTY(station_turfs)
if(length(vis_contents))
vis_contents.Cut()
+ SEND_SIGNAL(src, COMSIG_TURF_DESTROY)
/// WARNING WARNING
/// Turfs DO NOT lose their signals when they get replaced, REMEMBER THIS
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 21ade81d0b85..35298b6d1fe7 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -132,7 +132,9 @@ GLOBAL_LIST_INIT(admin_verbs_fun, list(
/client/proc/admin_away,
/client/proc/centcom_podlauncher,/*Open a window to launch a Supplypod and configure it or it's contents*/
/client/proc/load_json_admin_event,
- /client/proc/event_role_manager
+ /client/proc/event_role_manager,
+ /client/proc/spawn_liquid,
+ /client/proc/remove_liquid
))
GLOBAL_PROTECT(admin_verbs_fun)
GLOBAL_LIST_INIT(admin_verbs_spawn, list(/datum/admins/proc/spawn_atom, /datum/admins/proc/podspawn_atom, /datum/admins/proc/spawn_cargo, /datum/admins/proc/spawn_objasmob, /client/proc/respawn_character, /datum/admins/proc/beaker_panel))
diff --git a/code/modules/atmospherics/environmental/LINDA_fire.dm b/code/modules/atmospherics/environmental/LINDA_fire.dm
index 3da1ba1bc1db..1100f18ad563 100644
--- a/code/modules/atmospherics/environmental/LINDA_fire.dm
+++ b/code/modules/atmospherics/environmental/LINDA_fire.dm
@@ -17,6 +17,9 @@
if(!air)
return
+ if(liquids && liquids.liquid_group && !liquids.fire_state)
+ liquids.liquid_group.ignite_turf(src)
+
if (air.get_moles(GAS_O2) < 0.5 || air.get_moles(GAS_HYPERNOB) > REACTION_OPPRESSION_THRESHOLD)
return
diff --git a/code/modules/cargo/packs.dm b/code/modules/cargo/packs.dm
index 8e1c8ede005e..c668b32a3ad3 100644
--- a/code/modules/cargo/packs.dm
+++ b/code/modules/cargo/packs.dm
@@ -3245,3 +3245,11 @@
small_item = TRUE
contains = list(/obj/item/wallframe/telescreen/preset)
crate_name = "telescreen crate"
+
+// LIQUIDS TM REMOVE THIS
+/datum/supply_pack/service/janitor/pump
+ name = "Liquids Pump Crate"
+ desc = "A crate containing a portable liquid pump, for stations that lack proper liquid infrastructure."
+ cost = 2000
+ crate_name = "liquid pump crate"
+ contains = list(/obj/structure/liquid_pump)
diff --git a/code/modules/jobs/job_types/janitor.dm b/code/modules/jobs/job_types/janitor.dm
index 1f1230523a4b..82f14a9bc79b 100644
--- a/code/modules/jobs/job_types/janitor.dm
+++ b/code/modules/jobs/job_types/janitor.dm
@@ -46,3 +46,5 @@
ears = /obj/item/radio/headset/headset_srv
uniform = /obj/item/clothing/under/rank/civilian/janitor
uniform_skirt = /obj/item/clothing/under/rank/civilian/janitor/skirt
+ // LIQUIDS TM REMOVE THIS
+ backpack_contents = list(/obj/item/choice_beacon/liquids)
diff --git a/code/modules/mapping/reader.dm b/code/modules/mapping/reader.dm
index cf3d2c8e56a8..56f6c8252d62 100644
--- a/code/modules/mapping/reader.dm
+++ b/code/modules/mapping/reader.dm
@@ -149,7 +149,7 @@
* - y_upper: The maximum y coordinate to load
* - z_lower: The minimum z coordinate to load
* - z_upper: The maximum z coordinate to load
- * - place_on_top: Whether to use /turf/proc/PlaceOnTop rather than /turf/proc/ChangeTurf
+ * - place_on_top: Whether to use /turf/proc/place_on_top rather than /turf/proc/ChangeTurf
* - new_z: If true, a new z level will be created for the map
*/
/proc/load_map(
diff --git a/code/modules/mob/living/blood.dm b/code/modules/mob/living/blood.dm
index 5e2e2f1ec143..311660758874 100644
--- a/code/modules/mob/living/blood.dm
+++ b/code/modules/mob/living/blood.dm
@@ -345,6 +345,10 @@
T = get_turf(src)
var/list/temp_blood_DNA
if(small_drip)
+ if(!QDELETED(T.liquids)) //just add it to our liquids
+ var/list/blood_drop = list(get_blood_id() = 0.1)
+ T.add_liquid_list(blood_drop, FALSE, 300)
+ return
// Only a certain number of drips (or one large splatter) can be on a given turf.
var/obj/effect/decal/cleanable/blood/drip/drop = locate() in T
if(drop)
diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm
index 7e860d1e31af..0973a78159ac 100644
--- a/code/modules/mob/living/carbon/life.dm
+++ b/code/modules/mob/living/carbon/life.dm
@@ -113,6 +113,21 @@
breath = loc_as_obj.handle_internal_lifeform(src, BREATH_VOLUME)
else if(isturf(loc)) //Breathe from loc as turf
+ var/turf/our_turf = loc
+ if(our_turf.liquids && !HAS_TRAIT(src, TRAIT_NOBREATH) && ((body_position == LYING_DOWN && our_turf.liquids.liquid_state >= LIQUID_STATE_WAIST) || (body_position == STANDING_UP && our_turf.liquids.liquid_state >= LIQUID_STATE_FULLTILE)))
+ //Officially trying to breathe underwater
+ if(HAS_TRAIT(src, TRAIT_WATER_BREATHING))
+ failed_last_breath = FALSE
+ clear_alert("not_enough_oxy")
+ return FALSE
+ adjustOxyLoss(3)
+ failed_last_breath = TRUE
+ if(oxyloss <= OXYGEN_DAMAGE_CHOKING_THRESHOLD && stat == CONSCIOUS)
+ to_chat(src, span_userdanger("You hold in your breath!"))
+ else
+ //Try and drink water
+ our_turf.liquids.liquid_group.transfer_to_atom(src, CHOKE_REAGENTS_INGEST_ON_BREATH_AMOUNT)
+ visible_message(span_warning("[src] chokes on water!"), span_userdanger("You're choking on water!"))
var/breath_ratio = 0
if(environment)
breath_ratio = BREATH_VOLUME/environment.return_volume()
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index 6ef239024c5d..6ab406a9349c 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -42,6 +42,7 @@
return ..()
/mob/living/proc/ZImpactDamage(turf/T, levels)
+ SEND_SIGNAL(T, COMSIG_TURF_MOB_FALL, src)
visible_message(span_danger("[src] crashes into [T] with a sickening noise!"))
adjustBruteLoss((levels * 5) ** 1.5)
Knockdown(levels * 50)
@@ -633,6 +634,21 @@
cure_fakedeath()
SEND_SIGNAL(src, COMSIG_LIVING_POST_FULLY_HEAL)
+/mob/living/proc/do_strange_reagent_revival()
+ if(iscarbon(src))
+ var/mob/living/carbon/C = src
+ for(var/organ in C.internal_organs)
+ var/obj/item/organ/O = organ
+ O.setOrganDamage(0)
+ adjustBruteLoss(-100)
+ adjustFireLoss(-100)
+ adjustOxyLoss(-200, 0)
+ adjustToxLoss(-200, 0, TRUE)
+ updatehealth()
+ if(revive())
+ emote("gasp")
+ log_combat(src, src, "revived", src)
+
//proc called by revive(), to check if we can actually ressuscitate the mob (we don't want to revive him and have him instantly die again)
/mob/living/proc/can_be_revived()
. = 1
diff --git a/code/modules/reagents/chemistry/holder.dm b/code/modules/reagents/chemistry/holder.dm
index 99f9db8cfb51..f49e7975118e 100644
--- a/code/modules/reagents/chemistry/holder.dm
+++ b/code/modules/reagents/chemistry/holder.dm
@@ -742,10 +742,10 @@
return TRUE
/// Like add_reagent but you can enter a list. Format it like this: list(/datum/reagent/toxin = 10, "beer" = 15)
-/datum/reagents/proc/add_reagent_list(list/list_reagents, list/data=null)
+/datum/reagents/proc/add_reagent_list(list/list_reagents, list/data=null, _no_react = FALSE)
for(var/r_id in list_reagents)
var/amt = list_reagents[r_id]
- add_reagent(r_id, amt, data)
+ add_reagent(r_id, amt, data, no_react = _no_react)
/// Remove a specific reagent
/datum/reagents/proc/remove_reagent(reagent, amount, safety)//Added a safety check for the trans_id_to
diff --git a/code/modules/reagents/chemistry/reagents.dm b/code/modules/reagents/chemistry/reagents.dm
index 5f0850414914..7cd4a70b42f0 100644
--- a/code/modules/reagents/chemistry/reagents.dm
+++ b/code/modules/reagents/chemistry/reagents.dm
@@ -1,5 +1,7 @@
#define REM REAGENTS_EFFECT_MULTIPLIER
+GLOBAL_VAR_INIT(global_evaporation_rate, 1)
+
GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
/proc/build_name2reagent()
@@ -62,8 +64,7 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
var/addiction_name = null
/// What biotypes can process this? We'll assume by default that it affects organics (and undead, for plasmemes)
var/compatible_biotypes = ALL_NON_ROBOTIC
- /// How flammable is this material?
- var/accelerant_quality = 0
+
/// You fucked up and this is now triggering its overdose effects, purge that shit quick.
var/overdosed = 0
///if false stops metab in liverless mobs
@@ -76,8 +77,23 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
var/harmful = FALSE
/// The default reagent container for the reagent. Currently only used for crafting icon/displays.
var/obj/item/reagent_containers/default_container = /obj/item/reagent_containers/glass/bottle
-
- /// Are we from a material? We might wanna know that for special stuff. Like metalgen. Is replaced with a ref of the material on New()
+
+ ///Whether it will evaporate if left untouched on a liquids simulated puddle
+ var/evaporates = TRUE
+ /// How flammable is this material? For liquid spills and molotov cocktails
+ var/accelerant_quality = 0
+ ///Whether a fire from this requires oxygen in the atmosphere
+ var/fire_needs_oxygen = TRUE
+ ///The opacity of the chems used to determine the alpha of liquid turfs
+ var/opacity = 175
+ ///The rate of evaporation in units per call
+ var/evaporation_rate = 1
+ ///The rate of evaporation for the entire GROUP per call, for special things like drying agent
+ var/group_evaporation_rate = 0
+ /// do we have a turf exposure (used to prevent liquids doing un-needed processes)
+ var/turf_exposure = FALSE
+ /// are we slippery?
+ var/slippery = TRUE
/datum/reagent/Destroy() // This should only be called by the holder, so it's already handled clearing its references
. = ..()
@@ -149,6 +165,9 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
/datum/reagent/proc/on_ex_act(severity)
return
+/datum/reagent/proc/evaporate(turf/exposed_turf, reac_volume)
+ return
+
/// Called if the reagent has passed the overdose threshold and is set to be triggering overdose effects
/datum/reagent/proc/overdose_process(mob/living/M)
return
diff --git a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
index 2b0566b84596..2ac80bc826bd 100644
--- a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
@@ -935,20 +935,7 @@
M.do_jitter_animation(10)
addtimer(CALLBACK(M, TYPE_PROC_REF(/mob/living/carbon, do_jitter_animation), 10), 40) //jitter immediately, then again after 4 and 8 seconds
addtimer(CALLBACK(M, TYPE_PROC_REF(/mob/living/carbon, do_jitter_animation), 10), 80)
- sleep(10 SECONDS) //so the ghost has time to re-enter
- if(iscarbon(M))
- var/mob/living/carbon/C = M
- for(var/organ in C.internal_organs)
- var/obj/item/organ/O = organ
- O.setOrganDamage(0)
- M.adjustBruteLoss(-100)
- M.adjustFireLoss(-100)
- M.adjustOxyLoss(-200, 0)
- M.adjustToxLoss(-200, 0, TRUE)
- M.updatehealth()
- if(M.revive())
- M.emote("gasp")
- log_combat(M, M, "revived", src)
+ addtimer(CALLBACK(M, TYPE_PROC_REF(/mob/living, do_strange_reagent_revival)), 10 SECONDS)
..()
/datum/reagent/medicine/strange_reagent/on_mob_life(mob/living/carbon/M)
diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm
index 4086435c7925..9e6e20e4d0d7 100644
--- a/code/modules/reagents/chemistry/reagents/other_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm
@@ -141,8 +141,9 @@
/datum/reagent/water
name = "Water"
description = "An ubiquitous chemical substance that is composed of hydrogen and oxygen."
- color = "#609bdf77" // rgb: 96, 155, 223, 77 (alpha)
+ color = "#00B8FF" // rgb: 170, 170, 170, 77 (alpha)
taste_description = "water"
+ evaporation_rate = 4 // water goes fast
glass_icon_state = "glass_clear"
glass_name = "glass of water"
glass_desc = "The father of all refreshments."
@@ -1133,7 +1134,7 @@
glass_icon_state = "dr_gibb_glass"
glass_name = "glass of Dr. Gibb"
glass_desc = "Dr. Gibb. Not as dangerous as the glass_name might imply."
- accelerant_quality = 10
+ accelerant_quality = 15
compatible_biotypes = ALL_BIOTYPES
/datum/reagent/fuel/reaction_mob(mob/living/M, methods=TOUCH, reac_volume, show_message = TRUE, permeability = 1)//Splashing people with welding fuel to make them easy to ignite!
@@ -1454,6 +1455,7 @@
color = "#C8A5DC"
taste_description = "oil"
compatible_biotypes = ALL_BIOTYPES
+ accelerant_quality = 8
/datum/reagent/oil/on_mob_life(mob/living/carbon/M)
M.adjustFireLoss(-2*REM, FALSE, FALSE, BODYPART_ROBOTIC)
@@ -1635,6 +1637,8 @@
reagent_state = LIQUID
color = "#A70FFF"
taste_description = "dryness"
+ group_evaporation_rate = 16
+ evaporation_rate = 0 //will never evaporate on it's own
/datum/reagent/drying_agent/reaction_turf(turf/open/T, reac_volume)
if(istype(T))
diff --git a/code/modules/reagents/chemistry/reagents/pyrotechnic_reagents.dm b/code/modules/reagents/chemistry/reagents/pyrotechnic_reagents.dm
index f5b637bc4b6e..11975f1d816b 100644
--- a/code/modules/reagents/chemistry/reagents/pyrotechnic_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/pyrotechnic_reagents.dm
@@ -38,6 +38,7 @@
taste_description = "burning"
accelerant_quality = 20
compatible_biotypes = ALL_BIOTYPES
+ evaporation_rate = 100
/datum/reagent/clf3/on_mob_life(mob/living/carbon/M)
M.adjust_fire_stacks(2)
diff --git a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
index 1b4423d540ca..0ab725c99e05 100644
--- a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
@@ -7,6 +7,7 @@
color = "#CF3600" // rgb: 207, 54, 0
taste_description = "bitterness"
taste_mult = 1.2
+ evaporation_rate = 3 //6x faster than normal chems
var/toxpwr = 1.5
var/silent_toxin = FALSE //won't produce a pain message when processed by liver/Life(seconds_per_tick = SSMOBS_DT, times_fired) if there isn't another non-silent toxin present.
@@ -59,7 +60,7 @@
taste_mult = 1.5
color = "#8228A0"
toxpwr = 3
- accelerant_quality = 10
+ accelerant_quality = 50 //OWWW
compatible_biotypes = ALL_BIOTYPES
/datum/reagent/toxin/plasma/on_mob_life(mob/living/carbon/C)
diff --git a/code/modules/reagents/reagent_containers.dm b/code/modules/reagents/reagent_containers.dm
index d51b807d5ba4..f006350cd06b 100644
--- a/code/modules/reagents/reagent_containers.dm
+++ b/code/modules/reagents/reagent_containers.dm
@@ -11,7 +11,6 @@
var/list/list_reagents = null
var/spawned_disease = null
var/disease_amount = 20
- var/spillable = FALSE
/obj/item/reagent_containers/Initialize(mapload, vol)
. = ..()
@@ -25,6 +24,37 @@
add_initial_reagents()
+ AddComponent(/datum/component/liquids_interaction, TYPE_PROC_REF(/obj/item/reagent_containers, attack_on_liquids_turf))
+
+/obj/item/reagent_containers/proc/attack_on_liquids_turf(obj/item/reagent_containers/my_beaker, turf/T, mob/living/user, obj/effect/abstract/liquid_turf/liquids)
+ if(!user.Adjacent(T))
+ return FALSE
+ if(!my_beaker.is_open_container())
+ return FALSE
+ if(!user.Adjacent(T))
+ return FALSE
+ if(user.combat_mode)
+ return FALSE
+ if(liquids.fire_state) //Use an extinguisher first
+ to_chat(user, "You can't scoop up anything while it's on fire!")
+ return TRUE
+ if(liquids.liquid_group.expected_turf_height == 1)
+ to_chat(user, "The puddle is too shallow to scoop anything up!")
+ return TRUE
+ var/free_space = my_beaker.reagents.maximum_volume - my_beaker.reagents.total_volume
+ if(free_space <= 0)
+ to_chat(user, "You can't fit any more liquids inside [my_beaker]!")
+ return TRUE
+ var/desired_transfer = my_beaker.amount_per_transfer_from_this
+ if(desired_transfer > free_space)
+ desired_transfer = free_space
+ if(desired_transfer > liquids.liquid_group.reagents_per_turf)
+ desired_transfer = liquids.liquid_group.reagents_per_turf
+ liquids.liquid_group.trans_to_seperate_group(my_beaker.reagents, desired_transfer, liquids)
+ to_chat(user, "You scoop up around [round(desired_transfer)] units of liquids with [my_beaker].")
+ user.changeNext_move(CLICK_CD_MELEE)
+ return TRUE
+
/obj/item/reagent_containers/proc/add_initial_reagents()
if(list_reagents)
reagents.add_reagent_list(list_reagents)
@@ -118,17 +148,34 @@
return
else
- if(isturf(target) && reagents.reagent_list.len && thrownby)
- log_combat(thrownby, target, "splashed (thrown) [english_list(reagents.reagent_list)]", "in [AREACOORD(target)]")
- log_game("[key_name(thrownby)] splashed (thrown) [english_list(reagents.reagent_list)] on [target] in [AREACOORD(target)].")
- message_admins("[ADMIN_LOOKUPFLW(thrownby)] splashed (thrown) [english_list(reagents.reagent_list)] on [target] in [ADMIN_VERBOSEJMP(target)].")
+ if(isturf(target))
+ var/turf/T = target
+ if(istype(T, /turf/open))
+ T.add_liquid_from_reagents(reagents, FALSE, reagents.chem_temp)
+ if(reagents.reagent_list.len && thrownby)
+ log_combat(thrownby, target, "splashed (thrown) [english_list(reagents.reagent_list)]", "in [AREACOORD(target)]")
+ log_game("[key_name(thrownby)] splashed (thrown) [english_list(reagents.reagent_list)] on [target] in [AREACOORD(target)].")
+ message_admins("[ADMIN_LOOKUPFLW(thrownby)] splashed (thrown) [english_list(reagents.reagent_list)] on [target] in [ADMIN_VERBOSEJMP(target)].")
+ else
+ reagents.reaction(target, TOUCH)
+ var/turf/targets_loc = target.loc
+ if(istype(targets_loc, /turf/open) && !target.density)
+ targets_loc.add_liquid_from_reagents(reagents)
+ else
+ targets_loc = get_step_towards(targets_loc, thrownby)
+ targets_loc.add_liquid_from_reagents(reagents) //not perfect but i can't figure out how to move something to the nearest visible turf from throw_target
visible_message(span_notice("[src] spills its contents all over [target]."))
reagents.reaction(target, TOUCH)
if(QDELETED(src))
return
- if(!isturf(target)) // it all ends up on the floor because gravity exists
- reagents.reaction(get_turf(target), TOUCH)
+ playsound(target, 'sound/effects/slosh.ogg', 25, TRUE)
+ var/image/splash_animation = image('icons/effects/effects.dmi', target, "splash")
+ if(isturf(target))
+ splash_animation = image('icons/effects/effects.dmi', target, "splash_floor")
+ splash_animation.color = mix_color_from_reagents(reagents.reagent_list)
+ flick_overlay_global(splash_animation, GLOB.clients, 1.0 SECONDS)
+
reagents.clear_reagents()
/obj/item/reagent_containers/microwave_act(obj/machinery/microwave/M)
diff --git a/code/modules/reagents/reagent_containers/bottle.dm b/code/modules/reagents/reagent_containers/bottle.dm
index c5d3fa6de44b..75a5a5a8aa27 100644
--- a/code/modules/reagents/reagent_containers/bottle.dm
+++ b/code/modules/reagents/reagent_containers/bottle.dm
@@ -460,7 +460,7 @@
base_icon_state = "coffee_cup"
possible_transfer_amounts = list(10)
volume = 30
- spillable = TRUE
+ reagent_flags = OPENCONTAINER
/obj/item/reagent_containers/glass/coffee_cup/update_icon_state()
icon_state = reagents.total_volume ? base_icon_state : "[base_icon_state]_e"
@@ -715,7 +715,7 @@
fill_icon_thresholds = list(0, 20, 40, 60, 80, 100)
possible_transfer_amounts = list(5, 10)
amount_per_transfer_from_this = 5
- spillable = FALSE
+ reagent_flags = OPENCONTAINER_NOSPILL
///variable to tell if the bottle can be refilled
var/cap_on = TRUE
obj_flags = UNIQUE_RENAME | UNIQUE_REDESC
diff --git a/code/modules/reagents/reagent_containers/glass.dm b/code/modules/reagents/reagent_containers/glass.dm
index a07364c63eaa..ad24f1fed791 100755
--- a/code/modules/reagents/reagent_containers/glass.dm
+++ b/code/modules/reagents/reagent_containers/glass.dm
@@ -274,6 +274,24 @@
ITEM_SLOT_DEX_STORAGE
)
+/obj/item/reagent_containers/glass/bucket/attackby_secondary(obj/item/weapon, mob/user, params)
+ . = ..()
+ if(istype(weapon, /obj/item/mop))
+ if(reagents.total_volume == volume)
+ to_chat(user, "The [src.name] can't hold anymore liquids")
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ var/obj/item/mop/attacked_mop = weapon
+
+ if(attacked_mop.reagents.total_volume < 0.1)
+ to_chat(user, span_warning("Your [attacked_mop.name] is already dry!"))
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ to_chat(user, "You wring out the [attacked_mop.name] into the [src.name].")
+ attacked_mop.reagents.trans_to(src, attacked_mop.mopcap * 0.25)
+ attacked_mop.reagents.remove_all(attacked_mop.mopcap)
+ return SECONDARY_ATTACK_CONTINUE_CHAIN
+
/obj/item/reagent_containers/glass/bucket/wooden
name = "wooden bucket"
icon_state = "woodbucket"
diff --git a/goon/icons/turfs/outdoors.dmi b/goon/icons/turfs/outdoors.dmi
new file mode 100644
index 000000000000..e8fa45135f1d
Binary files /dev/null and b/goon/icons/turfs/outdoors.dmi differ
diff --git a/icons/effects/effects.dmi b/icons/effects/effects.dmi
index 9ac330d0690d..c7fe80096652 100644
Binary files a/icons/effects/effects.dmi and b/icons/effects/effects.dmi differ
diff --git a/yogstation.dme b/yogstation.dme
index aa50f1893e29..a15df60c6e85 100644
--- a/yogstation.dme
+++ b/yogstation.dme
@@ -236,6 +236,7 @@
#include "code\__DEFINES\{yogs_defines}\is_helpers.dm"
#include "code\__DEFINES\{yogs_defines}\jungle.dm"
#include "code\__DEFINES\{yogs_defines}\layers.dm"
+#include "code\__DEFINES\{yogs_defines}\liquids.dm"
#include "code\__DEFINES\{yogs_defines}\logging.dm"
#include "code\__DEFINES\{yogs_defines}\mapping.dm"
#include "code\__DEFINES\{yogs_defines}\maps.dm"
@@ -4427,6 +4428,19 @@
#include "yogstation\code\modules\jungleland\kinetic_javelin.dm"
#include "yogstation\code\modules\language\darkspeak.dm"
#include "yogstation\code\modules\language\japanese.dm"
+#include "yogstation\code\modules\liquids\drains.dm"
+#include "yogstation\code\modules\liquids\height_floors.dm"
+#include "yogstation\code\modules\liquids\liquid_controller.dm"
+#include "yogstation\code\modules\liquids\liquid_effect.dm"
+#include "yogstation\code\modules\liquids\liquid_groups.dm"
+#include "yogstation\code\modules\liquids\liquid_height.dm"
+#include "yogstation\code\modules\liquids\liquid_interaction.dm"
+#include "yogstation\code\modules\liquids\liquid_ocean.dm"
+#include "yogstation\code\modules\liquids\liquid_plumbers.dm"
+#include "yogstation\code\modules\liquids\liquid_pump.dm"
+#include "yogstation\code\modules\liquids\liquid_status_effect.dm"
+#include "yogstation\code\modules\liquids\liquid_turf.dm"
+#include "yogstation\code\modules\liquids\tools.dm"
#include "yogstation\code\modules\language\language_holder.dm"
#include "yogstation\code\modules\language\voxpidgin.dm"
#include "yogstation\code\modules\mentor\follow.dm"
diff --git a/yogstation/code/modules/liquids/drains.dm b/yogstation/code/modules/liquids/drains.dm
new file mode 100644
index 000000000000..563fec37264e
--- /dev/null
+++ b/yogstation/code/modules/liquids/drains.dm
@@ -0,0 +1,80 @@
+//Structure as this doesn't need any power to work
+/obj/structure/drain
+ name = "drain"
+ icon = 'yogstation/icons/obj/structures/drains.dmi'
+ icon_state = "drain"
+ desc = "Drainage inlet embedded in the floor to prevent flooding."
+ resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
+ density = FALSE
+ plane = FLOOR_PLANE
+ layer = GAS_SCRUBBER_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/welder_act(mob/living/user, obj/item/I)
+ ..()
+ if(!I.tool_start_check(user, amount=0))
+ return TRUE
+
+ playsound(src, 'sound/items/welder2.ogg', 50, TRUE)
+ to_chat(user, span_notice("You start [welded ? "unwelding" : "welding"] [src]..."))
+ if(I.use_tool(src, user, 20))
+ to_chat(user, span_notice("You [welded ? "unweld" : "weld"] [src]."))
+ welded = !welded
+ update_icon()
+ if(welded)
+ if(processing)
+ STOP_PROCESSING(SSobj, src)
+ processing = FALSE
+ else if (my_turf.liquids)
+ START_PROCESSING(SSobj, src)
+ processing = TRUE
+ return TRUE
+
+/obj/structure/drain/process()
+ if(!my_turf.liquids)
+ STOP_PROCESSING(SSobj, src)
+ processing = FALSE
+ return
+ my_turf.liquids.liquid_group.remove_any(my_turf.liquids, drain_flat + (drain_percent * my_turf.liquids.liquid_group.total_reagent_volume))
+
+/obj/structure/drain/Initialize()
+ . = ..()
+ if(!isturf(loc))
+ stack_trace("Drain structure initialized not on a turf")
+ my_turf = loc
+ RegisterSignal(my_turf, COMSIG_TURF_LIQUIDS_CREATION, PROC_REF(liquids_signal))
+ if(my_turf.liquids)
+ START_PROCESSING(SSobj, src)
+ processing = TRUE
+
+/obj/structure/drain/proc/liquids_signal()
+ SIGNAL_HANDLER
+ if(processing || welded)
+ return
+ START_PROCESSING(SSobj, src)
+ processing = TRUE
+
+/obj/structure/drain/Destroy()
+ if(processing)
+ STOP_PROCESSING(SSobj, src)
+ UnregisterSignal(my_turf, COMSIG_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/yogstation/code/modules/liquids/height_floors.dm b/yogstation/code/modules/liquids/height_floors.dm
new file mode 100644
index 000000000000..61b677892d8d
--- /dev/null
+++ b/yogstation/code/modules/liquids/height_floors.dm
@@ -0,0 +1,57 @@
+/obj/item/stack/tile/elevated
+ name = "elevated floor tile"
+ singular_name = "elevated floor tile"
+ turf_type = /turf/open/floor/elevated
+ merge_type = /obj/item/stack/tile/elevated
+ icon = 'yogstation/icons/obj/items/tiles.dmi'
+ icon_state = "elevated"
+
+/obj/item/stack/tile/lowered
+ name = "lowered floor tile"
+ singular_name = "lowered floor tile"
+ turf_type = /turf/open/floor/lowered
+ merge_type = /obj/item/stack/tile/lowered
+ icon = 'yogstation/icons/obj/items/tiles.dmi'
+ icon_state = "lowered"
+
+/obj/item/stack/tile/lowered/iron
+ name = "lowered floor tile"
+ singular_name = "lowered floor tile"
+ turf_type = /turf/open/floor/lowered
+ merge_type = /obj/item/stack/tile/lowered
+ icon = 'yogstation/icons/obj/items/tiles.dmi'
+ icon_state = "lowered"
+
+/turf/open/floor/iron/pool/rust_heretic_act()
+ return
+
+/turf/open/floor/elevated
+ name = "elevated floor"
+ floor_tile = /obj/item/stack/tile/elevated
+ icon = 'yogstation/icons/turf/floors/elevated_iron.dmi'
+ icon_state = "elevated_plasteel-0"
+ base_icon_state = "elevated_plasteel-0"
+ smoothing_flags = SMOOTH_CORNERS
+ smoothing_groups = SMOOTH_GROUP_ELEVATED_PLASTEEL
+ canSmoothWith = SMOOTH_GROUP_WALLS + SMOOTH_GROUP_ELEVATED_PLASTEEL
+ liquid_height = 30
+ turf_height = 30
+
+/turf/open/floor/elevated/rust_heretic_act()
+ return
+
+/turf/open/floor/lowered
+ name = "lowered floor"
+ floor_tile = /obj/item/stack/tile/lowered
+ icon = 'yogstation/icons/turf/floors/lowered_iron.dmi'
+ icon_state = "lowered_plasteel-0"
+ base_icon_state = "lowered_plasteel-0"
+ smoothing_flags = SMOOTH_CORNERS
+ smoothing_groups = SMOOTH_GROUP_LOWERED_PLASTEEL
+ canSmoothWith = SMOOTH_GROUP_WALLS + SMOOTH_GROUP_LOWERED_PLASTEEL
+ liquid_height = -30
+ turf_height = -30
+
+
+/turf/open/floor/lowered/rust_heretic_act()
+ return
diff --git a/yogstation/code/modules/liquids/liquid_controller.dm b/yogstation/code/modules/liquids/liquid_controller.dm
new file mode 100644
index 000000000000..394efd71d933
--- /dev/null
+++ b/yogstation/code/modules/liquids/liquid_controller.dm
@@ -0,0 +1,233 @@
+SUBSYSTEM_DEF(liquids)
+ name = "Liquid Turfs"
+ wait = 0.5 SECONDS
+ flags = SS_KEEP_TIMING | SS_NO_INIT
+ runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
+ var/list/active_groups = list()
+
+ var/list/evaporation_queue = list()
+ var/evaporation_counter = 0 //Only process evaporation on intervals
+
+ var/list/temperature_queue = list()
+
+ var/list/active_ocean_turfs = list()
+ var/list/ocean_turfs = list()
+ var/list/currentrun_active_ocean_turfs = list()
+ var/list/unvalidated_oceans = list()
+ var/ocean_counter = 0
+
+ var/run_type = SSLIQUIDS_RUN_TYPE_GROUPS
+
+ ///debug variable to toggle evaporation from running
+ var/debug_evaporation = FALSE
+
+ var/list/burning_turfs = list()
+ var/fire_counter = 0
+
+ var/member_counter = 0
+
+ var/list/arrayed_groups = list()
+
+ ///list of groups to work on for cached edges
+ var/list/cached_edge_work_queue = list()
+ ///list of groups we are going to work on in group process
+ var/list/group_process_work_queue = list()
+ ///list of all work queue for turf processing
+ var/list/active_turf_group_queue = list()
+
+
+/datum/controller/subsystem/liquids/stat_entry(msg)
+ msg += "AG:[length(active_groups)]|BT:[length(burning_turfs)]|EQ:[length(evaporation_queue)]|AO:[length(active_ocean_turfs)]|UO:[length(unvalidated_oceans)]"
+ return ..()
+
+/datum/controller/subsystem/liquids/fire(resumed)
+ if(!length(active_groups) && !length(evaporation_queue) && !length(active_ocean_turfs) && !length(burning_turfs) && !length(unvalidated_oceans))
+ return
+
+ listclearnulls(active_groups)
+
+ if(length(unvalidated_oceans))
+ for(var/turf/open/floor/plating/ocean/unvalidated_turf in unvalidated_oceans)
+ if(MC_TICK_CHECK)
+ return
+ unvalidated_turf.assume_self()
+
+ if(length(arrayed_groups))
+ listclearnulls(arrayed_groups)
+ for(var/datum/liquid_group/liquid_group as anything in arrayed_groups)
+ if(QDELETED(liquid_group))
+ arrayed_groups -= liquid_group
+ continue
+ while(!MC_TICK_CHECK && length(liquid_group?.splitting_array)) // three at a time until we either finish or over-run, this should be done before anything else
+ liquid_group.work_on_split_queue()
+ liquid_group.cleanse_members()
+
+ if(!length(temperature_queue))
+ for(var/datum/liquid_group/liquid_group as anything in active_groups)
+ if(MC_TICK_CHECK)
+ return
+ if(QDELETED(liquid_group))
+ temperature_queue -= active_groups
+ continue
+ var/list/turfs = liquid_group.fetch_temperature_queue()
+ temperature_queue += turfs
+
+ if(run_type == SSLIQUIDS_RUN_TYPE_GROUPS)
+ if(!length(group_process_work_queue))
+ group_process_work_queue |= active_groups
+ listclearnulls(group_process_work_queue)
+ if(length(group_process_work_queue))
+ var/populate_evaporation = FALSE
+ if(!length(evaporation_queue))
+ populate_evaporation = TRUE
+ for(var/datum/liquid_group/liquid_group as anything in group_process_work_queue)
+ if(MC_TICK_CHECK)
+ return
+ if(QDELETED(liquid_group))
+ group_process_work_queue -= liquid_group
+ continue
+ liquid_group.process_group(TRUE)
+ if(populate_evaporation && (liquid_group.expected_turf_height < LIQUID_ANKLES_LEVEL_HEIGHT) && liquid_group.evaporates)
+ for(var/turf/listed_turf as anything in liquid_group.members)
+ if(QDELETED(listed_turf))
+ continue
+ evaporation_queue |= listed_turf
+ group_process_work_queue -= liquid_group
+
+
+ run_type = SSLIQUIDS_RUN_TYPE_TEMPERATURE
+
+ if(run_type == SSLIQUIDS_RUN_TYPE_TEMPERATURE)
+ listclearnulls(temperature_queue)
+ if(length(temperature_queue))
+ for(var/turf/open/temperature_turf as anything in temperature_queue)
+ if(MC_TICK_CHECK)
+ return
+ temperature_queue -= temperature_turf
+ if(QDELETED(temperature_turf.liquids))
+ continue
+ if(QDELETED(temperature_turf.liquids.liquid_group))
+ QDEL_NULL(temperature_turf.liquids)
+ continue
+ temperature_turf.liquids.liquid_group.act_on_queue(temperature_turf)
+ run_type = SSLIQUIDS_RUN_TYPE_EVAPORATION
+
+ if(run_type == SSLIQUIDS_RUN_TYPE_EVAPORATION && !debug_evaporation)
+ listclearnulls(evaporation_queue)
+ evaporation_counter++
+ if(evaporation_counter >= REQUIRED_EVAPORATION_PROCESSES)
+ evaporation_counter = 0
+ for(var/datum/liquid_group/liquid_group as anything in active_groups)
+ if(MC_TICK_CHECK)
+ return
+ if(QDELETED(liquid_group))
+ active_groups -= liquid_group
+ continue
+ liquid_group.check_dead()
+ if(!length(liquid_group?.splitting_array))
+ liquid_group.process_turf_disperse()
+ for(var/turf/liquid_turf as anything in evaporation_queue)
+ if(MC_TICK_CHECK)
+ return
+ if(!prob(EVAPORATION_CHANCE) || QDELETED(liquid_turf))
+ evaporation_queue -= liquid_turf
+ continue
+ liquid_turf?.liquids?.process_evaporation()
+ run_type = SSLIQUIDS_RUN_TYPE_FIRE
+
+ if(run_type == SSLIQUIDS_RUN_TYPE_FIRE)
+ fire_counter++
+ for(var/datum/liquid_group/liquid_group as anything in active_groups)
+ if(MC_TICK_CHECK)
+ return
+ if(length(liquid_group?.burning_members))
+ for(var/turf/burning_turf as anything in liquid_group.burning_members)
+ if(MC_TICK_CHECK)
+ return
+ if(!istype(burning_turf) || QDELING(burning_turf))
+ liquid_group.burning_members -= burning_turf
+ continue
+ liquid_group.process_spread(burning_turf)
+
+ if(fire_counter > REQUIRED_FIRE_PROCESSES)
+ for(var/datum/liquid_group/liquid_group as anything in active_groups)
+ if(MC_TICK_CHECK)
+ return
+ if(QDELETED(liquid_group))
+ active_groups -= liquid_group
+ continue
+ if(length(liquid_group.burning_members))
+ liquid_group.process_fire()
+ fire_counter = 0
+ run_type = SSLIQUIDS_RUN_TYPE_OCEAN
+
+ if(!length(currentrun_active_ocean_turfs))
+ currentrun_active_ocean_turfs = active_ocean_turfs
+
+ if(run_type == SSLIQUIDS_RUN_TYPE_OCEAN)
+ listclearnulls(currentrun_active_ocean_turfs)
+ ocean_counter++
+ if(ocean_counter >= REQUIRED_OCEAN_PROCESSES)
+ for(var/turf/open/floor/plating/ocean/active_ocean in currentrun_active_ocean_turfs)
+ if(MC_TICK_CHECK)
+ return
+ active_ocean.process_turf()
+ ocean_counter = 0
+ run_type = SSLIQUIDS_RUN_TYPE_TURFS
+
+ if(run_type == SSLIQUIDS_RUN_TYPE_TURFS)
+ member_counter++
+ if(!length(active_turf_group_queue))
+ active_turf_group_queue += active_groups
+ listclearnulls(active_turf_group_queue)
+
+ if(member_counter > REQUIRED_MEMBER_PROCESSES)
+ for(var/datum/liquid_group/liquid_group as anything in active_turf_group_queue)
+ if(MC_TICK_CHECK)
+ return
+ if(QDELETED(liquid_group))
+ active_turf_group_queue -= liquid_group
+ continue
+ liquid_group.build_turf_reagent()
+ active_turf_group_queue -= liquid_group
+ if(!liquid_group.exposure)
+ continue
+ for(var/turf/member as anything in liquid_group.members)
+ if(MC_TICK_CHECK)
+ return
+ if(!istype(member) || QDELING(member))
+ liquid_group.members -= member
+ continue
+ liquid_group.process_member(member)
+ member_counter = 0
+ run_type = SSLIQUIDS_RUN_TYPE_CACHED_EDGES
+
+ if(run_type == SSLIQUIDS_RUN_TYPE_CACHED_EDGES)
+ if(!length(cached_edge_work_queue))
+ cached_edge_work_queue |= active_groups
+ listclearnulls(cached_edge_work_queue)
+
+ if(length(cached_edge_work_queue))
+ for(var/datum/liquid_group/liquid_group as anything in cached_edge_work_queue)
+ if(MC_TICK_CHECK)
+ return
+ if(QDELETED(liquid_group))
+ cached_edge_work_queue -= liquid_group
+ continue
+
+ liquid_group.build_turf_reagent()
+ if(liquid_group.reagents_per_turf > LIQUID_HEIGHT_DIVISOR)
+ liquid_group.process_cached_edges()
+ cached_edge_work_queue -= liquid_group
+
+
+ run_type = SSLIQUIDS_RUN_TYPE_GROUPS
+
+
+/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
diff --git a/yogstation/code/modules/liquids/liquid_effect.dm b/yogstation/code/modules/liquids/liquid_effect.dm
new file mode 100644
index 000000000000..7ff9affcf2c5
--- /dev/null
+++ b/yogstation/code/modules/liquids/liquid_effect.dm
@@ -0,0 +1,312 @@
+/obj/effect/abstract/liquid_turf
+ name = "liquid"
+ icon = 'yogstation/icons/obj/effects/liquid.dmi'
+ icon_state = "water-0"
+ base_icon_state = "water"
+ anchored = TRUE
+ plane = FLOOR_PLANE
+ color = "#DDF"
+ alpha = 175
+ //For being on fire
+ light_power = 1
+ light_color = LIGHT_COLOR_FIRE
+
+ smoothing_flags = SMOOTH_BITMASK | SMOOTH_OBJ
+ smoothing_groups = SMOOTH_GROUP_WATER
+ canSmoothWith = SMOOTH_GROUP_WATER + SMOOTH_GROUP_WINDOW_FULLTILE + SMOOTH_GROUP_WALLS
+
+ mouse_opacity = FALSE
+
+ var/datum/liquid_group/liquid_group
+ var/turf/my_turf
+
+ var/fire_state = LIQUID_FIRE_STATE_NONE
+ var/liquid_state = LIQUID_STATE_PUDDLE
+ var/no_effects = FALSE
+
+
+ var/static/obj/effect/abstract/fire/small_fire/small_fire
+ var/static/obj/effect/abstract/fire/medium_fire/medium_fire
+ var/static/obj/effect/abstract/fire/big_fire/big_fire
+
+ var/mutable_appearance/displayed_content
+ /// State-specific message chunks for examine_turf()
+ var/static/list/liquid_state_messages = list(
+ "[LIQUID_STATE_PUDDLE]" = "a puddle of $",
+ "[LIQUID_STATE_ANKLES]" = "$ going [span_warning("up to your ankles")]",
+ "[LIQUID_STATE_WAIST]" = "$ going [span_warning("up to your waist")]",
+ "[LIQUID_STATE_SHOULDERS]" = "$ going [span_warning("up to your shoulders")]",
+ "[LIQUID_STATE_FULLTILE]" = "$ going [span_danger("over your head")]",
+ )
+
+ var/temporary_split_key
+
+
+/obj/effect/abstract/liquid_turf/proc/process_evaporation()
+ if(liquid_group.expected_turf_height > LIQUID_ANKLES_LEVEL_HEIGHT)
+ 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 liquid_group.reagents.reagent_list)
+ R = reagent_type
+ //We evaporate. bye bye
+ if(initial(R.evaporates))
+ var/remove_amount = min((initial(R.evaporation_rate) * GLOB.global_evaporation_rate), R.volume, (liquid_group.reagents_per_turf / length(liquid_group.reagents.reagent_list)))
+ liquid_group.remove_specific(src, remove_amount, R, TRUE)
+ any_change = TRUE
+ R.evaporate(src.loc, remove_amount)
+ if(initial(R.group_evaporation_rate))
+ var/remove_amount = min((initial(R.group_evaporation_rate) * GLOB.global_evaporation_rate), liquid_group.total_reagent_volume, (liquid_group.reagents_per_turf / length(liquid_group.reagents.reagent_list)))
+ liquid_group.remove_any(src, remove_amount)
+ any_change = TRUE
+ R.evaporate(src.loc, remove_amount)
+ if(!any_change)
+ SSliquids.evaporation_queue -= my_turf
+ return
+
+/obj/effect/abstract/liquid_turf/forceMove(atom/destination, no_tp=FALSE, harderforce = FALSE)
+ if(harderforce)
+ . = ..()
+
+/obj/effect/abstract/liquid_turf/proc/set_new_liquid_state(new_state)
+ if(no_effects)
+ return
+ liquid_state = new_state
+
+ var/number = new_state - 1
+ if(number != 0)
+ icon_state = null
+ base_icon_state = null
+ update_appearance()
+
+ else
+ icon_state = initial(icon_state)
+ base_icon_state = initial(base_icon_state)
+ QUEUE_SMOOTH(src)
+ QUEUE_SMOOTH_NEIGHBORS(src)
+
+/obj/effect/abstract/liquid_turf/update_overlays()
+ . = ..()
+ var/number = liquid_state - 1
+ if(number != 0)
+ . += mutable_appearance('yogstation/icons/obj/effects/liquid_overlays.dmi', "stage[number]_bottom", offset_spokesman = my_turf, plane = ABOVE_GAME_PLANE, layer = ABOVE_MOB_LAYER)
+ . += mutable_appearance('yogstation/icons/obj/effects/liquid_overlays.dmi', "stage[number]_top", offset_spokesman = my_turf, plane =GAME_PLANE, layer = GATEWAY_UNDERLAY_LAYER)
+
+/obj/effect/abstract/liquid_turf/proc/set_fire_effect()
+ if(displayed_content)
+ vis_contents -= displayed_content
+
+ if(!liquid_group)
+ return
+
+ switch(liquid_group.group_fire_state)
+ if(LIQUID_FIRE_STATE_SMALL)
+ displayed_content = small_fire
+ if(LIQUID_FIRE_STATE_MILD)
+ displayed_content = small_fire
+ if(LIQUID_FIRE_STATE_MEDIUM)
+ displayed_content = medium_fire
+ if(LIQUID_FIRE_STATE_HUGE)
+ displayed_content = big_fire
+ if(LIQUID_FIRE_STATE_INFERNO)
+ displayed_content = big_fire
+ else
+ displayed_content = null
+
+ if(displayed_content)
+ vis_contents |= displayed_content
+
+//Takes a flat of our reagents and returns it, possibly qdeling our liquids
+/obj/effect/abstract/liquid_turf/proc/take_reagents_flat(flat_amount)
+ liquid_group.remove_any(src, flat_amount)
+
+/obj/effect/abstract/liquid_turf/proc/movable_entered(datum/source, atom/movable/AM)
+ SIGNAL_HANDLER
+ if(!liquid_group)
+ qdel(src)
+ return
+
+ var/turf/T = source
+ if(isobserver(AM))
+ return //ghosts, camera eyes, etc. don't make water splashy splashy
+ if(liquid_group.group_overlay_state >= LIQUID_STATE_ANKLES)
+ if(prob(30))
+ var/sound_to_play = pick(list(
+ 'yogstation/sound/effects/water_wade1.ogg',
+ 'yogstation/sound/effects/water_wade2.ogg',
+ 'yogstation/sound/effects/water_wade3.ogg',
+ 'yogstation/sound/effects/water_wade4.ogg'
+ ))
+ playsound(T, sound_to_play, 50, 0)
+ if(iscarbon(AM))
+ var/mob/living/carbon/C = AM
+ C.apply_status_effect(/datum/status_effect/water_affected)
+ if(isliving(AM))
+ var/mob/living/carbon/human/stepped_human = AM
+ liquid_group.expose_atom(stepped_human, 1, TOUCH)
+ else if (isliving(AM))
+ var/mob/living/L = AM
+ if(liquid_group.slippery)
+ if(prob(7) && !(L.movement_type & FLYING) && L.body_position == STANDING_UP)
+ L.slip(30, T, NO_SLIP_WHEN_WALKING, 0, TRUE)
+
+ if(fire_state)
+ AM.fire_act((T20C+50) + (50*fire_state), 125)
+
+/obj/effect/abstract/liquid_turf/proc/mob_fall(datum/source, mob/M)
+ SIGNAL_HANDLER
+ var/turf/T = source
+ if(liquid_group.group_overlay_state >= LIQUID_STATE_ANKLES && T.has_gravity(T))
+ playsound(T, 'yogstation/sound/effects/splash.ogg', 50, 0)
+ if(iscarbon(M))
+ var/mob/living/carbon/C = M
+ if(C.wear_mask && C.wear_mask.flags_cover & MASKCOVERSMOUTH)
+ to_chat(C, span_userdanger("You fall in the water!"))
+ else
+ liquid_group.transfer_to_atom(src, CHOKE_REAGENTS_INGEST_ON_FALL_AMOUNT, C)
+ C.adjustOxyLoss(5)
+ //C.emote("cough")
+ INVOKE_ASYNC(C, TYPE_PROC_REF(/mob, emote), "cough")
+ to_chat(C, span_userdanger("You fall in and swallow some water!"))
+ else
+ to_chat(M, span_userdanger("You fall in the water!"))
+
+/obj/effect/abstract/liquid_turf/Initialize(mapload, datum/liquid_group/group_to_add)
+ . = ..()
+ if(!small_fire)
+ small_fire = new
+ if(!medium_fire)
+ medium_fire = new
+ if(!big_fire)
+ big_fire = new
+
+ if(!my_turf)
+ my_turf = loc
+
+ if(!my_turf.liquids)
+ my_turf.liquids = src
+
+ if(group_to_add)
+ group_to_add.add_to_group(my_turf)
+ set_new_liquid_state(liquid_group.group_overlay_state)
+
+ if(!liquid_group && !group_to_add)
+ liquid_group = new(1, src)
+
+ if(!SSliquids)
+ CRASH("Liquid Turf created with the liquids sybsystem not yet initialized!")
+ my_turf = loc
+ RegisterSignal(my_turf, COMSIG_ATOM_ENTERED, PROC_REF(movable_entered))
+ RegisterSignal(my_turf, COMSIG_TURF_MOB_FALL, PROC_REF(mob_fall))
+ RegisterSignal(my_turf, COMSIG_ATOM_EXAMINE, PROC_REF(examine_turf))
+
+ SEND_SIGNAL(my_turf, COMSIG_TURF_LIQUIDS_CREATION, src)
+
+ if(z)
+ QUEUE_SMOOTH(src)
+ QUEUE_SMOOTH_NEIGHBORS(src)
+
+
+/obj/effect/abstract/liquid_turf/Destroy(force)
+ UnregisterSignal(my_turf, list(COMSIG_ATOM_ENTERED, COMSIG_TURF_MOB_FALL, COMSIG_ATOM_EXAMINE))
+ if(liquid_group)
+ liquid_group.remove_from_group(my_turf)
+ if(my_turf in SSliquids.evaporation_queue)
+ SSliquids.evaporation_queue -= my_turf
+ if(my_turf in SSliquids.burning_turfs)
+ SSliquids.burning_turfs -= my_turf
+ my_turf.liquids = null
+ my_turf = null
+ QUEUE_SMOOTH_NEIGHBORS(src)
+ return ..()
+
+/obj/effect/abstract/liquid_turf/proc/ChangeToNewTurf(turf/NewT)
+ if(NewT.liquids)
+ stack_trace("Liquids tried to change to a new turf, that already had liquids on it!")
+
+ UnregisterSignal(my_turf, list(COMSIG_ATOM_ENTERED, COMSIG_TURF_MOB_FALL))
+ if(SSliquids.evaporation_queue[my_turf])
+ SSliquids.evaporation_queue -= my_turf
+ SSliquids.evaporation_queue[NewT] = TRUE
+ my_turf.liquids = null
+ my_turf = NewT
+ liquid_group.move_liquid_group(src)
+ NewT.liquids = src
+ loc = NewT
+ RegisterSignal(my_turf, COMSIG_ATOM_ENTERED, PROC_REF(movable_entered))
+ RegisterSignal(my_turf, COMSIG_TURF_MOB_FALL, PROC_REF(mob_fall))
+
+/**
+ * Handles COMSIG_ATOM_EXAMINE for the turf.
+ *
+ * Adds reagent info to examine text.
+ * Arguments:
+ * * source - the turf we're peekin at
+ * * examiner - the user
+ * * examine_text - the examine list
+ * */
+/obj/effect/abstract/liquid_turf/proc/examine_turf(turf/source, mob/examiner, list/examine_list)
+ SIGNAL_HANDLER
+
+ if(!liquid_group)
+ qdel(src)
+ return
+
+ // This should always have reagents if this effect object exists, but as a sanity check...
+ if(!length(liquid_group.reagents.reagent_list))
+ return
+
+ var/liquid_state_template = liquid_state_messages["[liquid_group.group_overlay_state]"]
+
+ examine_list += "
"
+
+ if(examiner.can_see_reagents())
+ examine_list += "
"
+
+ if(length(liquid_group.reagents.reagent_list) == 1)
+ // Single reagent text.
+ var/datum/reagent/reagent_type = liquid_group.reagents.reagent_list[1]
+ var/reagent_name = initial(reagent_type.name)
+ var/volume = round(reagent_type.volume / length(liquid_group.members), 0.01)
+
+ examine_list += span_notice("There is [replacetext(liquid_state_template, "$", "[volume] units of [reagent_name]")] here.")
+ else
+ // Show each individual reagent
+ examine_list += "There is [replacetext(liquid_state_template, "$", "the following")] here:"
+
+ for(var/datum/reagent/reagent_type as anything in liquid_group.reagents.reagent_list)
+ var/reagent_name = initial(reagent_type.name)
+ var/volume = round(reagent_type.volume / length(liquid_group.members), 0.01)
+ examine_list += "• [volume] units of [reagent_name]"
+
+ examine_list += span_notice("The solution has a temperature of [liquid_group.group_temperature]K.")
+ examine_list += "
"
+ return
+
+ // Otherwise, just show the total volume
+ examine_list += span_notice("There is [replacetext(liquid_state_template, "$", "liquid")] here.")
+
+/obj/effect/temp_visual/liquid_splash
+ icon = 'yogstation/icons/obj/effects/splash.dmi'
+ icon_state = "splash"
+ layer = FLY_LAYER
+ randomdir = FALSE
+
+/obj/effect/abstract/fire
+ icon = 'yogstation/icons/obj/effects/liquid.dmi'
+ plane = FLOOR_PLANE
+ layer = BELOW_MOB_LAYER
+ mouse_opacity = FALSE
+ appearance_flags = RESET_COLOR | RESET_ALPHA
+
+/obj/effect/abstract/fire/small_fire
+ icon_state = "fire_small"
+
+/obj/effect/abstract/fire/medium_fire
+ icon_state = "fire_medium"
+
+/obj/effect/abstract/fire/big_fire
+ icon_state = "fire_big"
diff --git a/yogstation/code/modules/liquids/liquid_groups.dm b/yogstation/code/modules/liquids/liquid_groups.dm
new file mode 100644
index 000000000000..f0efd914332b
--- /dev/null
+++ b/yogstation/code/modules/liquids/liquid_groups.dm
@@ -0,0 +1,1028 @@
+/***************************************************/
+/********************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
+ ///the generated color given to the group on creation for debugging
+ var/color
+ ///list of all current members of the group saved in true/false format
+ var/list/members = list()
+ ///list of all current burning members of our group
+ var/list/burning_members = list()
+ ///our reagent holder, where the entire liquid groups reagents are stored
+ var/datum/reagents/reagents
+ ///our reagent holder for a single turf
+ var/datum/reagents/turf_reagents
+ ///the expected height of all the collective turfs
+ var/expected_turf_height = 1
+ ///A saved variable of the total reagent volumes to avoid calling reagents.total_volume constantly
+ var/total_reagent_volume = 0
+ ///a cached value of our reagents per turf, used to determine liquid height and state
+ var/reagents_per_turf = 0
+ ///the icon state our group currently uses
+ var/group_overlay_state = LIQUID_STATE_PUDDLE
+ ///the calculated alpha cache for our group
+ var/group_alpha = 0
+ ///the calculated temperature cache for our group
+ var/group_temperature = 300
+ ///the generated color used to apply coloring to all the members
+ var/group_color
+ ///have we failed a process? if so we are added to a death check so it will gracefully die on its own
+ var/failed_death_check = FALSE
+ ///the burn power of our group, used to determine how strong we burn each process_fire()
+ var/group_burn_power = 0
+ ///the icon state of our fire
+ var/group_fire_state = LIQUID_FIRE_STATE_NONE
+ ///the amount of reagents we attempt to burn each process_fire()
+ var/group_burn_rate = 0
+ ///the viscosity of our group, determines how much we can spread with our total reagent pool, higher means less turfs per reagent
+ var/group_viscosity = 1
+ ///are we currently attempting a merge? if so don't process groups
+ var/merging = FALSE
+ ///list of cached edge turfs with a sublist of directions stored
+ var/list/cached_edge_turfs = list()
+ ///list of cached spreadable turfs for each burning member
+ var/list/cached_fire_spreads = list()
+ ///list of old reagents
+ var/list/cached_reagent_list = list()
+ ///cached temperature between turfs recalculated on group_process
+ var/cached_temperature_shift = 0
+ ///does temperature need action
+ var/temperature_shift_needs_action = FALSE
+ ///this groups list of currently running turfs, we iterate over this to stop redundancy
+ var/list/current_temperature_queue = list()
+ ///do we evaporate
+ var/evaporates = TRUE
+ ///can we merge?
+ var/can_merge = TRUE
+ ///number in decimal value that acts as a multiplier to the amount of liquids lost in applications
+ var/loss_precent = 1
+ ///do we have any containing expose turf chemicals with volume to look for?
+ var/exposure = FALSE
+ ///array generated by bulk splitting
+ var/list/splitting_array = list()
+ ///are we slippery
+ var/slippery = TRUE
+
+///NEW/DESTROY
+/datum/liquid_group/New(height, obj/effect/abstract/liquid_turf/created_liquid)
+ color = "#[random_short_color()]"
+ expected_turf_height = height
+ reagents = new(100000) // this is a random number used on creation it expands based on the turfs in the group
+ if(!QDELETED(created_liquid))
+ add_to_group(created_liquid.my_turf)
+ cached_edge_turfs[created_liquid.my_turf] = list(NORTH, SOUTH, EAST, WEST)
+ SSliquids.active_groups |= src
+
+/datum/liquid_group/Destroy()
+ SSliquids.active_groups -= src
+
+ if(src in SSliquids.arrayed_groups)
+ SSliquids.arrayed_groups -= src /// Someone made a massive fucky wucky if this is happening
+
+ for(var/turf/member_turf as anything in members)
+ member_turf?.liquids?.liquid_group = null
+ members = list()
+ burning_members = null
+ return ..()
+
+
+///GROUP CONTROLLING
+/datum/liquid_group/proc/add_to_group(turf/T)
+ if(QDELETED(T))
+ return
+ if(QDELETED(T.liquids))
+ T.liquids = new(T, src)
+ cached_edge_turfs[T] = list(NORTH, SOUTH, EAST, WEST)
+
+ if(!members)
+ QDEL_NULL(T.liquids)
+ return
+
+ members[T] = TRUE
+ T.liquids.liquid_group = src
+
+ reagents.maximum_volume += 1000 /// each turf will hold 1000 units plus the base amount spread across the group
+ if(group_color)
+ T.liquids.color = group_color
+ process_group()
+
+/datum/liquid_group/proc/remove_from_group(turf/T, should_reprocess = TRUE)
+
+ if(T in burning_members)
+ burning_members -= T
+
+ if(T in SSliquids.burning_turfs)
+ SSliquids.burning_turfs -= T
+
+ members -= T
+ T.liquids?.liquid_group = null
+
+ if(!length(members))
+ qdel(src)
+ return
+ if(should_reprocess)
+ process_group()
+
+/datum/liquid_group/proc/remove_all()
+ for(var/turf/member in members)
+ QDEL_NULL(member.liquids)
+
+/datum/liquid_group/proc/merge_group(datum/liquid_group/otherg)
+ if(otherg == src)
+ return
+ if(!length(members) || !total_reagent_volume)
+ return
+
+ otherg.merging = TRUE
+ var/list/created_reagent_list = list()
+ for(var/datum/reagent/reagent in otherg.reagents.reagent_list)
+ created_reagent_list |= reagent.type
+ created_reagent_list[reagent.type] = reagent.volume
+
+ add_reagents(reagent_list = created_reagent_list, chem_temp = otherg.group_temperature)
+ cached_edge_turfs |= otherg.cached_edge_turfs
+
+ for(var/turf/liquid_turf as anything in otherg.members)
+ otherg.remove_from_group(liquid_turf)
+ add_to_group(liquid_turf)
+
+
+ total_reagent_volume = reagents.total_volume
+ reagents_per_turf = total_reagent_volume / length(members)
+
+ qdel(otherg)
+ process_group()
+
+/datum/liquid_group/proc/break_group()
+ qdel(src)
+
+/datum/liquid_group/proc/check_dead()
+ if(!members && !total_reagent_volume)
+ if(failed_death_check)
+ qdel(src)
+ return
+ failed_death_check = TRUE
+
+///PROCESSING
+/datum/liquid_group/proc/process_group(from_SS = FALSE)
+ if(merging)
+ return
+ if(!members || !length(members)) // this ideally shouldn't exist, ideally groups would die before they got to this point but alas here we are
+ check_dead()
+ return
+
+ if(group_temperature != reagents.chem_temp)
+ reagents.chem_temp = group_temperature
+
+ handle_visual_changes()
+ reagents.my_atom = pick(members) /// change the location of explosions and sounds every group process
+
+ for(var/turf/turf as anything in members)
+ if(isopenturf(turf))
+ continue
+ remove_from_group(turf)
+
+ var/turf/open/open_turf = pick(members)
+ var/datum/gas_mixture/math_cache = open_turf.air
+
+ if(math_cache && total_reagent_volume)
+ if(!(group_temperature <= math_cache.return_temperature() + 5 && math_cache.return_temperature() - 5 <= group_temperature) && !temperature_shift_needs_action)
+ cached_temperature_shift =((math_cache.return_temperature() * max(1, math_cache.total_moles())) + ((group_temperature * max(1, (total_reagent_volume * 0.025))))) / (max(1, (total_reagent_volume * 0.025)) + max(1, math_cache.total_moles()))
+ temperature_shift_needs_action = TRUE
+
+ if(from_SS)
+ total_reagent_volume = reagents.total_volume
+ reagents.handle_reactions()
+
+ if(!total_reagent_volume || !members)
+ return
+
+ reagents_per_turf = total_reagent_volume / length(members)
+
+ expected_turf_height = CEILING(reagents_per_turf, 1) / LIQUID_HEIGHT_DIVISOR
+ var/old_overlay = group_overlay_state
+ switch(expected_turf_height)
+ if(0 to LIQUID_ANKLES_LEVEL_HEIGHT-1)
+ group_overlay_state = LIQUID_STATE_PUDDLE
+ if(LIQUID_ANKLES_LEVEL_HEIGHT to LIQUID_WAIST_LEVEL_HEIGHT-1)
+ group_overlay_state = LIQUID_STATE_ANKLES
+ if(LIQUID_WAIST_LEVEL_HEIGHT to LIQUID_SHOULDERS_LEVEL_HEIGHT-1)
+ group_overlay_state = LIQUID_STATE_WAIST
+ if(LIQUID_SHOULDERS_LEVEL_HEIGHT to LIQUID_FULLTILE_LEVEL_HEIGHT-1)
+ group_overlay_state = LIQUID_STATE_SHOULDERS
+ if(LIQUID_FULLTILE_LEVEL_HEIGHT to INFINITY)
+ group_overlay_state = LIQUID_STATE_FULLTILE
+
+ if(old_overlay != group_overlay_state)
+ for(var/turf/member in members)
+ if(QDELETED(member.liquids))
+ remove_from_group(member)
+ continue
+
+ member.liquids.set_new_liquid_state(group_overlay_state)
+ member.liquid_height = expected_turf_height + member.turf_height
+
+/datum/liquid_group/proc/cleanse_members()
+ for(var/turf/listed_turf as anything in members)
+ if(isclosedturf(listed_turf))
+ remove_from_group(listed_turf)
+ qdel(listed_turf.liquids)
+
+/datum/liquid_group/proc/process_member(turf/member)
+ if(isspaceturf(member))
+ remove_any(member.liquids, reagents_per_turf)
+
+ if(!(member in members))
+ return
+ if(QDELETED(turf_reagents))
+ return
+ turf_reagents.reaction(member, TOUCH)
+
+/datum/liquid_group/proc/build_turf_reagent()
+ if(!length(members))
+ return
+ if(!turf_reagents)
+ turf_reagents = new(100000)
+
+ exposure = FALSE
+ slippery = FALSE
+ for(var/reagent_type in reagents.reagent_list)
+ var/datum/reagent/pulled_reagent = reagent_type
+ var/amount = pulled_reagent.volume / length(members)
+ if(!amount)
+ continue
+ turf_reagents.add_reagent(pulled_reagent.type, amount)
+ if(pulled_reagent.turf_exposure && amount > 10)
+ exposure = TRUE
+ if(pulled_reagent.slippery)
+ slippery = TRUE
+
+
+/datum/liquid_group/proc/process_turf_disperse()
+ if(!total_reagent_volume)
+ for(var/turf/member in members)
+ remove_from_group(member)
+ QDEL_NULL(member.liquids)
+ return
+
+ var/list/removed_turf = list()
+ if(reagents_per_turf < 5)
+ var/turfs_to_remove = round(length(members) - (total_reagent_volume / 6))
+ if(turfs_to_remove <= 0)
+ return
+ while(turfs_to_remove > 0)
+ turfs_to_remove--
+ if(members && length(members))
+ var/turf/picked_turf = pick(members)
+ if(!QDELETED(picked_turf.liquids))
+ remove_from_group(picked_turf)
+ QDEL_NULL(picked_turf.liquids)
+ removed_turf |= picked_turf
+ if(!total_reagent_volume)
+ reagents_per_turf = 0
+ else
+ reagents_per_turf = total_reagent_volume / length(members)
+ else
+ members -= picked_turf
+
+ if(!length(removed_turf))
+ return
+ try_bulk_split(removed_turf)
+
+///REAGENT ADD/REMOVAL HANDLING
+/datum/liquid_group/proc/check_liquid_removal(obj/effect/abstract/liquid_turf/remover, amount)
+ if(amount >= reagents_per_turf)
+ remove_from_group(remover.my_turf)
+ var/turf/remover_turf = remover.my_turf
+ qdel(remover)
+ try_split(remover_turf)
+
+ for(var/dir in GLOB.cardinals)
+ var/turf/open/open_turf = get_step(remover_turf, dir)
+ if(!isopenturf(open_turf) || QDELETED(open_turf.liquids))
+ continue
+ check_edges(open_turf)
+ process_group()
+
+/datum/liquid_group/proc/remove_edge_liquids()
+ var/list/plucked_turfs = list()
+ var/list/edge_queue = list()
+
+ total_reagent_volume = reagents.total_volume
+ var/minimum_turfs = total_reagent_volume * 0.2
+ var/turfs_till_fine = length(members) - minimum_turfs
+
+ while(turfs_till_fine > 0)
+ turfs_till_fine--
+ if(!length(edge_queue))
+ edge_queue |= cached_edge_turfs
+ var/turf/plucked = pick_n_take(edge_queue)
+ remove_from_group(plucked, FALSE)
+
+ if(QDELETED(src))
+ return
+
+ qdel(plucked.liquids)
+ plucked_turfs |= plucked
+
+ if(!length(members))
+ qdel(src)
+ return
+
+ for(var/dir in GLOB.cardinals)
+ var/turf/open/open_turf = get_step(plucked, dir)
+ if(!isopenturf(open_turf) || QDELETED(open_turf.liquids))
+ continue
+ check_edges(open_turf)
+
+ if(!length(plucked_turfs))
+ return
+
+ reagents_per_turf = total_reagent_volume / length(members)
+ try_bulk_split(plucked_turfs)
+ process_group()
+
+/datum/liquid_group/proc/remove_any(obj/effect/abstract/liquid_turf/remover, amount)
+ reagents.remove_any(amount, TRUE)
+ if(!QDELETED(remover))
+ check_liquid_removal(remover, amount)
+ total_reagent_volume = reagents.total_volume
+ if(length(members))
+ reagents_per_turf = total_reagent_volume / length(members)
+ else
+ reagents_per_turf = 1
+ expected_turf_height = CEILING(reagents_per_turf, 1) / LIQUID_HEIGHT_DIVISOR
+ if(!total_reagent_volume && !reagents.total_volume)
+ remove_all()
+ qdel(src)
+
+/datum/liquid_group/proc/remove_specific(obj/effect/abstract/liquid_turf/remover, amount, datum/reagent/reagent_type)
+ reagents.remove_reagent(reagent_type.type, amount)
+ if(!QDELETED(remover))
+ check_liquid_removal(remover, amount)
+ total_reagent_volume = reagents.total_volume
+
+/datum/liquid_group/proc/transfer_to_atom(obj/effect/abstract/liquid_turf/remover, amount, atom/transfer_target, transfer_method = INGEST)
+ reagents.trans_to(transfer_target, amount)
+ if(!QDELETED(remover))
+ check_liquid_removal(remover, amount)
+ total_reagent_volume = reagents.total_volume
+
+/datum/liquid_group/proc/move_liquid_group(obj/effect/abstract/liquid_turf/member)
+ remove_from_group(member.my_turf)
+ member.liquid_group = new(1, member)
+ var/remove_amount = reagents_per_turf / length(reagents.reagent_list)
+ for(var/datum/reagent/reagent_type in reagents.reagent_list)
+ member.liquid_group.reagents.add_reagent(reagent_type, remove_amount, no_react = TRUE)
+ remove_specific(amount = remove_amount, reagent_type = reagent_type)
+
+/datum/liquid_group/proc/add_reagents(obj/effect/abstract/liquid_turf/member, reagent_list, chem_temp)
+ reagents.add_reagent_list(reagent_list, _no_react = TRUE)
+
+ var/amount = 0
+ for(var/list_item in reagent_list)
+ amount += reagent_list[list_item]
+ handle_temperature(amount, chem_temp)
+ handle_visual_changes()
+ process_group()
+
+/datum/liquid_group/proc/add_reagent(obj/effect/abstract/liquid_turf/member, datum/reagent/reagent, amount, temperature)
+ reagents.add_reagent(reagent, amount, temperature, no_react = TRUE)
+
+ handle_temperature(amount, temperature)
+ handle_visual_changes()
+ process_group()
+
+/datum/liquid_group/proc/transfer_reagents_to_secondary_group(obj/effect/abstract/liquid_turf/member, obj/effect/abstract/liquid_turf/transfer)
+ if(!total_reagent_volume && !reagents.total_volume)
+ return
+ else if(!total_reagent_volume)
+ total_reagent_volume = reagents.total_volume
+
+ var/total_removed = length(members) + 1 / total_reagent_volume
+ var/remove_amount = total_removed / length(reagents.reagent_list)
+ if(QDELETED(transfer))
+ transfer = new()
+ if(QDELETED(transfer.liquid_group))
+ transfer.liquid_group = new(1, transfer)
+ for(var/datum/reagent/reagent_type in reagents.reagent_list)
+ transfer.liquid_group.reagents.add_reagent(reagent_type.type, remove_amount, no_react = TRUE)
+ remove_specific(amount = remove_amount, reagent_type = reagent_type)
+ total_removed += remove_amount
+ check_liquid_removal(member, total_removed)
+ handle_visual_changes()
+ process_group()
+
+/datum/liquid_group/proc/trans_to_seperate_group(datum/reagents/secondary_reagent, amount, obj/effect/abstract/liquid_turf/remover, merge = FALSE)
+ reagents.trans_to(secondary_reagent, amount)
+ if(remover)
+ check_liquid_removal(remover, amount)
+ else if(!merge)
+ process_removal(amount)
+
+ handle_visual_changes()
+
+/datum/liquid_group/proc/transfer_specific_reagents(datum/reagents/secondary_reagent, amount, list/reagents_to_check, obj/effect/abstract/liquid_turf/remover, merge = FALSE)
+ if(!length(reagents_to_check))
+ return
+ var/total_hits = 0
+ var/total_volume = 0
+ for(var/datum/reagent/reagent_type in reagents.reagent_list)
+ if(!(reagent_type.type in reagents_to_check))
+ continue
+ total_hits++
+ total_volume += reagent_type.volume
+ if(!total_hits)
+ return
+
+ var/precent = (amount / total_volume)
+ for(var/datum/reagent/reagent_type in reagents.reagent_list)
+ if(!(reagent_type.type in reagents_to_check))
+ continue
+ secondary_reagent.add_reagent(reagent_type.type, reagent_type.volume * precent, no_react = TRUE)
+ if(remover)
+ remove_specific(remover, amount = reagent_type.volume * precent, reagent_type = reagent_type.type)
+ else
+ remove_specific(amount = reagent_type.volume * precent, reagent_type = reagent_type.type)
+
+ process_removal()
+ handle_visual_changes()
+
+
+/datum/liquid_group/proc/process_removal(amount)
+
+ total_reagent_volume = reagents.total_volume
+ if(total_reagent_volume && length(members)) //Otherwise we are probably just sending the last of things
+ reagents_per_turf = total_reagent_volume / length(members)
+ else
+ reagents_per_turf = 0
+ process_turf_disperse()
+ process_group()
+
+/datum/liquid_group/proc/handle_temperature(previous_reagents, temp)
+ var/baseline_temperature = ((total_reagent_volume * group_temperature) + (previous_reagents * temp)) / (total_reagent_volume + previous_reagents)
+ group_temperature = baseline_temperature
+ reagents.chem_temp = group_temperature
+
+/datum/liquid_group/proc/handle_visual_changes()
+ var/new_color
+ var/old_color = group_color
+
+ if(GLOB.liquid_debug_colors)
+ new_color = color
+ else if(length(cached_reagent_list) != length(reagents.reagent_list))
+ new_color = mix_color_from_reagents(reagents.reagent_list)
+ cached_reagent_list = list()
+ cached_reagent_list |= reagents.reagent_list
+
+ var/alpha_setting = 1
+ var/alpha_divisor = 1
+
+ for(var/r in reagents.reagent_list)
+ var/datum/reagent/R = r
+ alpha_setting += max((R.opacity * R.volume), 1)
+ alpha_divisor += max((1 * R.volume), 1)
+
+ var/old_alpha = group_alpha
+ if(new_color == old_color && group_alpha == old_alpha || !new_color)
+ return
+ group_alpha = clamp(round(alpha_setting / alpha_divisor, 1), 120, 255)
+ group_color = new_color
+ for(var/turf/member in members)
+ if(QDELETED(member.liquids))
+ continue
+ member.liquids.alpha = group_alpha
+ member.liquids.color = group_color
+
+///Fire Related Procs / Handling
+
+/datum/liquid_group/proc/get_group_burn()
+ var/total_burn_power = 0
+ var/total_burn_rate = 0
+ for(var/datum/reagent/reagent_type in reagents.reagent_list)
+ var/burn_power = initial(reagent_type.accelerant_quality)
+ if(burn_power)
+ total_burn_power += burn_power * reagent_type.volume
+ total_burn_rate += burn_power
+ group_burn_rate = total_burn_rate * 0.5 //half power because reasons
+ if(!total_burn_power)
+ if(length(burning_members))
+ extinguish_all()
+ group_burn_power = 0
+ return
+
+ total_burn_power /= reagents.total_volume //We get burn power per unit.
+ if(total_burn_power <= REQUIRED_FIRE_POWER_PER_UNIT)
+ return FALSE
+ //Finally, we burn
+ var/old_burn = group_burn_power
+
+ group_burn_power = total_burn_power
+
+ if(old_burn == group_burn_power)
+ return
+ switch(group_burn_power)
+ if(0 to 7)
+ group_fire_state = LIQUID_FIRE_STATE_SMALL
+ if(7 to 8)
+ group_fire_state = LIQUID_FIRE_STATE_MILD
+ if(8 to 9)
+ group_fire_state = LIQUID_FIRE_STATE_MEDIUM
+ if(9 to 10)
+ group_fire_state = LIQUID_FIRE_STATE_HUGE
+ if(10 to INFINITY)
+ group_fire_state = LIQUID_FIRE_STATE_INFERNO
+
+/datum/liquid_group/proc/process_fire()
+ get_group_burn()
+
+ var/reagents_to_remove = group_burn_rate * (length(burning_members))
+
+ if(!group_burn_power)
+ extinguish_all()
+ return
+
+ remove_any(amount = reagents_to_remove)
+
+ if(!reagents_per_turf)
+ return
+
+ if(group_burn_rate >= reagents_per_turf)
+ var/list/removed_turf = list()
+ var/number = round(group_burn_rate / reagents_per_turf)
+ for(var/num in 1 to number)
+ if(!length(burning_members))
+ break
+ var/turf/picked_turf = burning_members[1]
+ extinguish(picked_turf)
+ remove_from_group(picked_turf)
+ QDEL_NULL(picked_turf.liquids)
+ removed_turf |= picked_turf
+
+
+ for(var/turf/remover in removed_turf)
+ for(var/dir in GLOB.cardinals)
+ var/turf/open/open_turf = get_step(remover, dir)
+ if(!isopenturf(open_turf) || QDELETED(open_turf.liquids))
+ continue
+ check_edges(open_turf)
+
+ while(length(removed_turf))
+ var/turf/picked_turf = pick(removed_turf)
+ var/list/output = try_split(picked_turf, TRUE)
+ removed_turf -= picked_turf
+ for(var/turf/outputted_turf in output)
+ if(outputted_turf in removed_turf)
+ removed_turf -= outputted_turf
+
+
+/datum/liquid_group/proc/ignite_turf(turf/member)
+ get_group_burn()
+ if(!group_burn_power)
+ return
+
+ member.liquids.fire_state = group_fire_state
+ member.liquids.set_fire_effect()
+ burning_members |= member
+ SSliquids.burning_turfs |= member
+
+/datum/liquid_group/proc/build_fire_cache(turf/burning_member)
+ cached_fire_spreads |= burning_member
+ var/list/directions = list(NORTH, SOUTH, EAST, WEST)
+ var/list/spreading_turfs = list()
+ for(var/dir in directions)
+ var/turf/open/open_adjacent = get_step(burning_member, dir)
+ if(QDELETED(open_adjacent) || QDELETED(open_adjacent.liquids))
+ continue
+ spreading_turfs |= open_adjacent
+
+ cached_fire_spreads[burning_member] = spreading_turfs
+
+/datum/liquid_group/proc/process_spread(turf/member)
+ if(member.liquids.fire_state <= LIQUID_FIRE_STATE_MEDIUM) // fires to small to worth spreading
+ return
+
+ if(!cached_fire_spreads[member])
+ build_fire_cache(member)
+
+ for(var/turf/open/adjacent_turf in cached_fire_spreads[member])
+ if(!QDELETED(adjacent_turf.liquids) && adjacent_turf.liquids.liquid_group == src && adjacent_turf.liquids.fire_state < member.liquids.fire_state)
+ adjacent_turf.liquids.fire_state = group_fire_state
+ member.liquids.set_fire_effect()
+ burning_members |= adjacent_turf
+ SSliquids.burning_turfs |= adjacent_turf
+ for(var/atom/movable/movable in adjacent_turf)
+ movable.fire_act((T20C+50) + (50*adjacent_turf.liquids.fire_state), 125)
+
+/datum/liquid_group/proc/extinguish_all()
+ group_burn_power = 0
+ group_fire_state = LIQUID_FIRE_STATE_NONE
+ for(var/turf/member in burning_members)
+ member.liquids.fire_state = LIQUID_FIRE_STATE_NONE
+ member.liquids.set_fire_effect()
+ if(burning_members[member])
+ burning_members -= member
+ if(SSliquids.burning_turfs[member])
+ SSliquids.burning_turfs -= member
+
+/datum/liquid_group/proc/extinguish(turf/member)
+ if(SSliquids.burning_turfs[member])
+ SSliquids.burning_turfs -= member
+ burning_members -= member
+ if(QDELETED(member.liquids))
+ return
+ member.liquids.fire_state = LIQUID_FIRE_STATE_NONE
+ member.liquids.set_fire_effect()
+
+///EDGE COLLECTION AND PROCESSING
+
+/datum/liquid_group/proc/check_adjacency(turf/member)
+ var/adjacent_liquid = 0
+ for(var/tur in member.get_atmos_adjacent_turfs())
+ var/turf/adjacent_turf = tur
+ if(!QDELETED(adjacent_turf.liquids))
+ if(adjacent_turf.liquids.liquid_group == member.liquids.liquid_group)
+ adjacent_liquid++
+ if(adjacent_liquid < 2)
+ return FALSE
+ return TRUE
+
+/datum/liquid_group/proc/process_cached_edges()
+ for(var/turf/cached_turf in cached_edge_turfs)
+ for(var/direction in cached_edge_turfs[cached_turf])
+ var/turf/directional_turf = get_step(cached_turf, direction)
+ if(isclosedturf(directional_turf))
+ continue
+ if(!(directional_turf in cached_turf.atmos_adjacent_turfs)) //i hate that this is needed
+ continue
+ if(!cached_turf.atmos_adjacent_turfs[directional_turf])
+ continue
+ if(spread_liquid(directional_turf, cached_turf))
+ cached_edge_turfs[cached_turf] -= direction
+ if(!length(cached_edge_turfs[cached_turf]))
+ cached_edge_turfs -= cached_turf
+
+/datum/liquid_group/proc/check_edges(turf/checker)
+ var/list/passed_directions = list()
+ for(var/direction in GLOB.cardinals)
+ var/turf/directional_turf = get_step(checker, direction)
+ if(!QDELETED(directional_turf.liquids))
+ continue
+ passed_directions.Add(direction)
+
+ if(length(passed_directions))
+ cached_edge_turfs |= checker
+ cached_edge_turfs[checker] = passed_directions
+
+
+
+///SPLITING PROCS AND RETURNING CONNECTED PROCS
+
+/*okay for large groups we need some way to iterate over it without grinding the server to a halt to split them
+* A breadth-first search or depth first search, are the most efficent but still cause issues with larger groups
+* the easist way around this would be using an index of visted turfs and comparing it for changes to save cycles
+* this has the draw back of being multiple times slower on small groups, but massively faster on large groups
+* For a unique key the easist way to do so would be either to retrive its member number, or better its position
+* key as that will be totally unique. this can be used for things aside from splitting by sucking up large groups
+*/
+
+/datum/liquid_group/proc/return_connected_liquids(obj/effect/abstract/liquid_turf/source, adjacent_checks = 0)
+ var/temporary_split_key = source.temporary_split_key
+ var/turf/first_turf = source.my_turf
+ var/list/connected_liquids = list()
+ ///the current queue
+ var/list/queued_liquids = list(source)
+ ///the turfs that we have previously visited with unique ids
+ var/list/previously_visited = list()
+ ///the turf object the liquid resides on
+ var/turf/queued_turf
+
+ var/obj/effect/abstract/liquid_turf/current_head
+ ///compares after each iteration to see if we even need to continue
+ var/visited_length = 0
+ while(length(queued_liquids))
+ current_head = queued_liquids[1]
+ queued_turf = current_head.my_turf
+ queued_liquids -= current_head
+
+ for(var/turf/adjacent_turf in get_adjacent_open_turfs(queued_turf))
+ if(QDELETED(adjacent_turf.liquids) || !members[adjacent_turf])
+ continue
+ if(!(adjacent_turf in queued_turf.atmos_adjacent_turfs)) //i hate that this is needed
+ continue
+ visited_length = length(previously_visited)
+ previously_visited["[adjacent_turf.liquids.x]_[adjacent_turf.liquids.y]"] = adjacent_turf.liquids
+ if(length(previously_visited) != visited_length)
+ queued_liquids |= adjacent_turf.liquids
+ connected_liquids |= adjacent_turf
+ if(adjacent_checks)
+ if(temporary_split_key == adjacent_turf.liquids.temporary_split_key && adjacent_turf != first_turf)
+ adjacent_checks--
+ if(adjacent_checks <= 0)
+ return FALSE
+
+ return connected_liquids
+
+/datum/liquid_group/proc/return_connected_liquids_in_range(obj/effect/abstract/liquid_turf/source, total_turfs = 0)
+ var/list/connected_liquids = list()
+ ///the current queue
+ var/list/queued_liquids = list(source)
+ ///the turfs that we have previously visited with unique ids
+ var/list/previously_visited = list()
+ ///the turf object the liquid resides on
+ var/turf/queued_turf
+
+ var/obj/effect/abstract/liquid_turf/current_head
+ ///compares after each iteration to see if we even need to continue
+ var/visited_length = 0
+ while(length(queued_liquids))
+ current_head = queued_liquids[1]
+ queued_turf = current_head.my_turf
+ queued_liquids -= current_head
+
+ for(var/turf/adjacent_turf in get_adjacent_open_turfs(queued_turf))
+ if(QDELETED(adjacent_turf.liquids) || !members[adjacent_turf])
+ continue
+
+ visited_length = length(previously_visited)
+ previously_visited["[adjacent_turf.liquids.x]_[adjacent_turf.liquids.y]"] = adjacent_turf.liquids
+ if(length(previously_visited) != visited_length)
+ queued_liquids |= adjacent_turf.liquids
+ connected_liquids |= adjacent_turf
+ if(total_turfs > 0 && length(connected_liquids) >= total_turfs)
+ return connected_liquids
+
+
+/datum/liquid_group/proc/try_split(turf/source, return_list = FALSE)
+ if(!length(members))
+ return
+ var/list/connected_liquids = list()
+
+ var/turf/head_turf = source
+ var/obj/effect/abstract/liquid_turf/current_head
+
+ var/generated_key = "[world.time]_activemembers[length(members)]"
+ var/adjacent_liquid_count = -1
+ for(var/turf/adjacent_turf in get_adjacent_open_turfs(head_turf))
+ if(QDELETED(adjacent_turf.liquids) || !members[adjacent_turf]) //empty turf or not our group just skip this
+ continue
+ ///the section is a little funky, as if say a cross shaped liquid removal occurs this will leave 3 of them in the same group, not a big deal as this only affects turfs that are like 5 tiles total
+ current_head = adjacent_turf.liquids
+ current_head.temporary_split_key = generated_key
+ adjacent_liquid_count++
+
+ if(adjacent_liquid_count > 0) ///if there is only 1 adjacent liquid it physically can't split
+ return FALSE
+
+ if(current_head)
+ connected_liquids = return_connected_liquids(current_head, adjacent_liquid_count)
+
+ if(!length(connected_liquids) || length(connected_liquids) == length(members)) //yes yes i know if two groups are identical in size this will break but fixing this would add to much processing
+ if(return_list)
+ return connected_liquids
+ return FALSE
+
+ var/amount_to_transfer = length(connected_liquids) * reagents_per_turf
+
+ var/datum/liquid_group/new_group = new(1)
+
+ for(var/turf/connected_liquid in connected_liquids)
+ new_group.check_edges(connected_liquid)
+
+ if(connected_liquid in burning_members)
+ new_group.burning_members |= connected_liquid
+ remove_from_group(connected_liquid, TRUE)
+ new_group.add_to_group(connected_liquid)
+
+ trans_to_seperate_group(new_group.reagents, amount_to_transfer)
+ new_group.total_reagent_volume = new_group.reagents.total_volume
+ new_group.reagents_per_turf = new_group.total_reagent_volume / length(new_group.members)
+
+ ///asses the group to see if it should exist
+ var/new_group_length = length(new_group.members)
+ if(new_group.total_reagent_volume == 0 || new_group.reagents_per_turf == 0 || !new_group_length)
+ qdel(new_group)
+ return FALSE
+ for(var/turf/new_turf in new_group.members)
+ if(new_turf in members)
+ new_group.members -= new_turf
+
+ if(!length(new_group.members))
+ qdel(new_group)
+ return FALSE
+
+ if(return_list)
+ return connected_liquids
+
+ return TRUE
+
+/datum/liquid_group/proc/try_bulk_split(list/input_turfs)
+ var/list/connected_array = list()
+ for(var/turf/listed_input in input_turfs)
+ for(var/turf/cardinal in listed_input.get_atmos_adjacent_turfs())
+ var/exists_already = FALSE
+ for(var/list/arrayed_item in connected_array)
+ if(cardinal in arrayed_item)
+ exists_already = TRUE
+ break
+ if(!exists_already)
+ if(!QDELETED(cardinal.liquids))
+ var/list/temp = return_connected_liquids(cardinal.liquids)
+ if(isnull(temp) || !length(temp) || (temp in connected_array))
+ continue
+ connected_array |= list(temp)
+
+ if(!length(connected_array))
+ return
+
+ splitting_array = connected_array
+ SSliquids.arrayed_groups += src
+
+///we try and work on the split queue from this point on
+/datum/liquid_group/proc/work_on_split_queue()
+ if(!length(splitting_array))
+ SSliquids.arrayed_groups -= src
+ return
+
+ var/list/plucked_array = list()
+ var/pick_count = 3
+ while(pick_count > 0 && length(splitting_array))
+ var/list/temp = pick(splitting_array)
+ plucked_array += list(temp)
+ splitting_array -= list(temp)
+
+ if(!length(splitting_array))
+ SSliquids.arrayed_groups -= src
+
+ for(var/list/connected_liquids in plucked_array)
+ if(isnull(connected_liquids) || !length(connected_liquids))
+ continue
+
+ var/amount_to_transfer = length(connected_liquids) * reagents_per_turf
+
+ var/datum/liquid_group/new_group = new(1)
+ if(!members)
+ members = list()
+ trans_to_seperate_group(new_group.reagents, amount_to_transfer)
+ for(var/turf/connected_liquid in connected_liquids)
+
+ new_group.check_edges(connected_liquid)
+
+ if(connected_liquid in burning_members)
+ new_group.burning_members |= connected_liquid
+ remove_from_group(connected_liquid, TRUE)
+ new_group.add_to_group(connected_liquid)
+
+ new_group.total_reagent_volume = new_group.reagents.total_volume
+ new_group.reagents_per_turf = new_group.total_reagent_volume / length(new_group.members)
+
+ ///asses the group to see if it should exist
+ var/new_group_length = length(new_group.members)
+ if(new_group.total_reagent_volume == 0 || new_group.reagents_per_turf == 0 || !new_group_length)
+ qdel(new_group)
+ return
+
+ for(var/turf/new_turf in new_group.members)
+ if(new_turf in members)
+ new_group.members -= new_turf
+
+ if(!length(new_group.members))
+ qdel(new_group)
+ return
+
+
+
+///EXPOSURE AND SPREADING
+/datum/liquid_group/proc/expose_members_turf(obj/effect/abstract/liquid_turf/member)
+ if(!turf_reagents)
+ return
+ var/turf/members_turf = member.my_turf
+ for(var/atom/movable/target_atom in members_turf)
+ turf_reagents.reaction(target_atom, TOUCH)
+
+/datum/liquid_group/proc/expose_atom(atom/target, modifier = 0, method)
+ if(!turf_reagents)
+ return
+ if(HAS_TRAIT(target, LIQUID_PROTECTION))
+ return
+ turf_reagents.reaction(target, method)
+
+/datum/liquid_group/proc/spread_liquid(turf/new_turf, turf/source_turf)
+ if(isclosedturf(new_turf) || !source_turf.atmos_adjacent_turfs)
+ return
+ if(!(new_turf in source_turf.atmos_adjacent_turfs)) //i hate that this is needed
+ return
+ if(!source_turf.atmos_adjacent_turfs[new_turf])
+ return
+
+ if(isopenspaceturf(new_turf))
+ var/turf/Z_turf_below = GET_TURF_BELOW(new_turf)
+ if(!Z_turf_below)
+ return
+ if(isspaceturf(Z_turf_below))
+ return FALSE
+ if(QDELETED(Z_turf_below.liquids))
+ Z_turf_below.liquids = new(Z_turf_below)
+ if(QDELETED(source_turf.liquids))
+ remove_from_group(source_turf)
+ if(source_turf in cached_edge_turfs)
+ cached_edge_turfs -= source_turf
+ return FALSE
+ source_turf.liquids.liquid_group.transfer_reagents_to_secondary_group(source_turf.liquids, Z_turf_below.liquids)
+
+ var/obj/splashy = new /obj/effect/temp_visual/liquid_splash(Z_turf_below)
+ if(!QDELETED(Z_turf_below.liquids?.liquid_group))
+ splashy.color = Z_turf_below.liquids.liquid_group.group_color
+ return FALSE
+
+ if(QDELETED(new_turf.liquids) && !istype(new_turf, /turf/open/openspace) && !isspaceturf(new_turf) && !istype(new_turf, /turf/open/floor/plating/ocean) && source_turf.turf_height == new_turf.turf_height) // no space turfs, or oceans turfs, also don't attempt to spread onto a turf that already has liquids wastes processing time
+ if(reagents_per_turf < LIQUID_HEIGHT_DIVISOR)
+ return FALSE
+ if(!length(members))
+ return FALSE
+ reagents_per_turf = total_reagent_volume / length(members)
+ expected_turf_height = CEILING(reagents_per_turf, 1) / LIQUID_HEIGHT_DIVISOR
+ new_turf.liquids = new(new_turf, src)
+ new_turf.liquids.alpha = group_alpha
+ check_edges(new_turf)
+
+ var/obj/splashy = new /obj/effect/temp_visual/liquid_splash(new_turf)
+ if(!QDELETED(new_turf.liquids?.liquid_group))
+ splashy.color = new_turf.liquids.liquid_group.group_color
+
+ water_rush(new_turf, source_turf)
+
+ else if(!QDELETED(source_turf?.liquids?.liquid_group) && !QDELETED(new_turf?.liquids?.liquid_group) && new_turf.liquids.liquid_group != source_turf.liquids.liquid_group && source_turf.turf_height == new_turf.turf_height && new_turf.liquids.liquid_group.can_merge)
+ merge_group(new_turf.liquids.liquid_group)
+ return FALSE
+ else if(source_turf.turf_height != new_turf.turf_height)
+ if(new_turf.turf_height < source_turf.turf_height) // your going down
+ if(QDELETED(new_turf.liquids))
+ new_turf.liquids = new(new_turf)
+ if(new_turf.turf_height + new_turf.liquids.liquid_group.expected_turf_height < source_turf.turf_height)
+ var/obj/effect/abstract/liquid_turf/turf_liquids = new_turf.liquids
+ trans_to_seperate_group(turf_liquids.liquid_group.reagents, reagents_per_turf, source_turf)
+ turf_liquids.liquid_group.process_group()
+ else if(source_turf.turf_height < new_turf.turf_height)
+ if(source_turf.turf_height + expected_turf_height < new_turf.turf_height)
+ return
+ if(QDELETED(new_turf.liquids))
+ new_turf.liquids = new(new_turf)
+ var/obj/effect/abstract/liquid_turf/turf_liquids = new_turf.liquids
+ trans_to_seperate_group(turf_liquids.liquid_group.reagents, 10, source_turf) //overflows out
+ turf_liquids.liquid_group.process_group()
+
+ return TRUE
+
+/datum/liquid_group/proc/water_rush(turf/new_turf, turf/source_turf)
+ var/direction = get_dir(source_turf, new_turf)
+ for(var/atom/movable/target_atom in new_turf)
+ reagents.reaction(target_atom, TOUCH, (reagents_per_turf * 0.5))
+ if(!target_atom.anchored && !target_atom.pulledby && (target_atom.move_resist < INFINITY))
+ if(expected_turf_height < LIQUID_ANKLES_LEVEL_HEIGHT)
+ return
+ if(isliving(target_atom))
+ var/mob/living/target_living = target_atom
+ if(!target_living.mob_has_heavy_gravity()) //push everything EXCEPT living things that have heavy gravity
+ step(target_living, direction)
+ if(prob(60))
+ if(target_living.slip(6 SECONDS, null, FALSE, 6 SECONDS, TRUE))
+ to_chat(target_living, span_danger("You are knocked down by the currents!"))
+ else
+ step(target_atom, direction)
+
+/datum/liquid_group/proc/fetch_temperature_queue()
+ if(!cached_temperature_shift)
+ return list()
+
+ var/list/returned = list()
+ for(var/tur in members)
+ var/turf/open/member = tur
+ returned |= member
+
+ current_temperature_queue = returned
+ return returned
+
+/datum/liquid_group/proc/act_on_queue(turf/member)
+ if(!temperature_shift_needs_action)
+ return
+
+ var/turf/open/member_open = member
+ var/datum/gas_mixture/gas = member_open.air
+ if(!gas)
+ return
+
+ gas.set_temperature(cached_temperature_shift)
+ if(group_temperature != cached_temperature_shift)
+ group_temperature = cached_temperature_shift
+ reagents.chem_temp = cached_temperature_shift
+
+ current_temperature_queue -= member
+ if(!length(current_temperature_queue))
+ temperature_shift_needs_action = FALSE
diff --git a/yogstation/code/modules/liquids/liquid_height.dm b/yogstation/code/modules/liquids/liquid_height.dm
new file mode 100644
index 000000000000..291b4df68273
--- /dev/null
+++ b/yogstation/code/modules/liquids/liquid_height.dm
@@ -0,0 +1,45 @@
+/**
+ * Liquid Height element; for dynamically applying liquid blockages.
+ *
+ * Used for reinforced tables, sandbags, and the likes.
+ */
+/datum/element/liquids_height
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 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
+
+ RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(on_target_move))
+ var/atom/movable/movable_target = target
+ if(isturf(movable_target.loc))
+ var/turf/turf_loc = movable_target.loc
+ turf_loc.turf_height += height_applied
+ turf_loc.reasses_liquids()
+
+/datum/element/liquids_height/Detach(atom/movable/target)
+ . = ..()
+ UnregisterSignal(target, list(COMSIG_MOVABLE_MOVED))
+ var/atom/movable/movable_target = target
+ if(isturf(movable_target.loc))
+ var/turf/turf_loc = movable_target.loc
+ turf_loc.turf_height -= height_applied
+ turf_loc.reasses_liquids()
+
+/datum/element/liquids_height/proc/on_target_move(atom/movable/source, atom/OldLoc, Dir, Forced = FALSE)
+ SIGNAL_HANDLER
+ if(isturf(OldLoc))
+ var/turf/old_turf = OldLoc
+ old_turf.turf_height += height_applied
+ old_turf.reasses_liquids()
+ if(isturf(source.loc))
+ var/turf/new_turf = source.loc
+ new_turf.turf_height -= height_applied
+ new_turf.reasses_liquids()
diff --git a/yogstation/code/modules/liquids/liquid_interaction.dm b/yogstation/code/modules/liquids/liquid_interaction.dm
new file mode 100644
index 000000000000..0a43b0c9f512
--- /dev/null
+++ b/yogstation/code/modules/liquids/liquid_interaction.dm
@@ -0,0 +1,28 @@
+///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/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_ITEM_AFTERATTACK, PROC_REF(AfterAttack)) //The only signal allowing item -> turf interaction
+
+/datum/component/liquids_interaction/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_ITEM_AFTERATTACK)
+
+/datum/component/liquids_interaction/proc/AfterAttack(datum/source, atom/victim, mob/caster, proximity_flag, click_parameters)
+ SIGNAL_HANDLER
+ var/turf/turf_target = victim
+
+ if(!isturf(turf_target) || !turf_target.liquids)
+ return NONE
+
+ if(interaction_callback.Invoke(parent, victim, caster, turf_target.liquids))
+ return COMPONENT_CANCEL_ATTACK_CHAIN
diff --git a/yogstation/code/modules/liquids/liquid_ocean.dm b/yogstation/code/modules/liquids/liquid_ocean.dm
new file mode 100644
index 000000000000..02cabe4a5801
--- /dev/null
+++ b/yogstation/code/modules/liquids/liquid_ocean.dm
@@ -0,0 +1,332 @@
+GLOBAL_LIST_INIT(initalized_ocean_areas, list())
+/area/ocean
+ name = "Ocean"
+
+ icon = 'yogstation/icons/obj/effects/liquid.dmi'
+ base_icon_state = "ocean"
+ icon_state = "ocean"
+ alpha = 120
+
+ requires_power = TRUE
+ always_unpowered = TRUE
+
+ power_light = FALSE
+ power_equip = FALSE
+ power_environ = FALSE
+
+ outdoors = TRUE
+ ambience_index = AMBIENCE_SPACE
+
+ flags_1 = CAN_BE_DIRTY_1
+ sound_environment = SOUND_AREA_SPACE
+
+/area/ocean/Initialize(mapload)
+ . = ..()
+ GLOB.initalized_ocean_areas += src
+
+/area/ocean/dark
+ base_lighting_alpha = 0
+
+/area/ruin/ocean
+ has_gravity = TRUE
+
+/area/ruin/ocean/listening_outpost
+ unique = TRUE
+
+/area/ruin/ocean/bunker
+ unique = TRUE
+
+/area/ruin/ocean/bioweapon_research
+ unique = TRUE
+
+/area/ruin/ocean/mining_site
+ unique = TRUE
+
+/area/ocean/near_station_powered
+ requires_power = FALSE
+
+/turf/open/openspace/ocean
+ name = "ocean"
+ planetary_atmos = TRUE
+ baseturfs = /turf/open/openspace/ocean
+ var/replacement_turf = /turf/open/floor/plating/ocean
+
+/turf/open/openspace/ocean/Initialize()
+ . = ..()
+ ChangeTurf(replacement_turf, null, CHANGETURF_IGNORE_AIR)
+
+/turf/open/floor/plating
+ ///do we still call parent but dont want other stuff?
+ var/overwrites_attack_by = FALSE
+
+/turf/open/floor/plating/ocean
+ plane = FLOOR_PLANE
+ layer = TURF_LAYER
+ force_no_gravity = FALSE
+ gender = PLURAL
+ name = "ocean sand"
+ baseturfs = /turf/open/floor/plating/ocean
+ icon = 'yogstation/icons/turf/floors/seafloor.dmi'
+ icon_state = "seafloor"
+ base_icon_state = "seafloor"
+ footstep = FOOTSTEP_SAND
+ barefootstep = FOOTSTEP_SAND
+ clawfootstep = FOOTSTEP_SAND
+ heavyfootstep = FOOTSTEP_GENERIC_HEAVY
+ planetary_atmos = TRUE
+ initial_gas_mix = OCEAN_DEFAULT_ATMOS
+
+ upgradable = FALSE
+ attachment_holes = FALSE
+
+ resistance_flags = INDESTRUCTIBLE
+
+ overwrites_attack_by = TRUE
+
+ var/static/obj/effect/abstract/ocean_overlay/static_overlay
+ var/static/list/ocean_reagents = list(/datum/reagent/water = 10)
+ var/ocean_temp = T20C
+ var/list/ocean_turfs = list()
+ var/list/open_turfs = list()
+
+ ///are we captured, this is easier than having to run checks on turfs for vents
+ var/captured = FALSE
+
+ var/rand_variants = 0
+ var/rand_chance = 30
+
+ /// Itemstack to drop when dug by a shovel
+ var/obj/item/stack/dig_result = /obj/item/stack/ore/glass
+ /// Whether the turf has been dug or not
+ var/dug = FALSE
+
+ /// do we build a catwalk or plating with rods
+ var/catwalk = FALSE
+
+/turf/open/floor/plating/ocean/Initialize()
+ . = ..()
+ RegisterSignal(src, COMSIG_ATOM_ENTERED, PROC_REF(movable_entered))
+ RegisterSignal(src, COMSIG_TURF_MOB_FALL, PROC_REF(mob_fall))
+ if(!static_overlay)
+ static_overlay = new(null, ocean_reagents)
+
+ vis_contents += static_overlay
+ light_color = static_overlay.color
+ SSliquids.unvalidated_oceans |= src
+ SSliquids.ocean_turfs |= src
+
+ if(rand_variants && prob(rand_chance))
+ var/random = rand(1,rand_variants)
+ icon_state = "[base_icon_state][random]"
+ base_icon_state = "[base_icon_state][random]"
+
+
+/turf/open/floor/plating/ocean/Destroy()
+ . = ..()
+ UnregisterSignal(src, list(COMSIG_ATOM_ENTERED, COMSIG_TURF_MOB_FALL))
+ SSliquids.active_ocean_turfs -= src
+ SSliquids.ocean_turfs -= src
+ for(var/turf/open/floor/plating/ocean/listed_ocean as anything in ocean_turfs)
+ listed_ocean.rebuild_adjacent()
+
+/turf/open/floor/plating/ocean/attackby(obj/item/C, mob/user, params)
+ if(..())
+ return
+ if(istype(C, /obj/item/stack/rods))
+ var/obj/item/stack/rods/R = C
+ if (R.get_amount() < 2)
+ to_chat(user, span_warning("You need two rods to make a [catwalk ? "catwalk" : "plating"]!"))
+ return
+ else
+ to_chat(user, span_notice("You begin constructing a [catwalk ? "catwalk" : "plating"]..."))
+ if(do_after(user, 30, target = src))
+ if (R.get_amount() >= 2 && !catwalk)
+ place_on_top(/turf/open/floor/plating, flags = CHANGETURF_INHERIT_AIR)
+ playsound(src, 'sound/items/deconstruct.ogg', 80, TRUE)
+ R.use(2)
+ to_chat(user, span_notice("You reinforce the [src]."))
+ else if(R.get_amount() >= 2 && catwalk)
+ new /obj/structure/lattice/catwalk(src)
+ playsound(src, 'sound/items/deconstruct.ogg', 80, TRUE)
+ R.use(2)
+ to_chat(user, span_notice("You build a catwalk over the [src]."))
+
+/// Drops itemstack when dug and changes icon
+/turf/open/floor/plating/ocean/proc/getDug()
+ dug = TRUE
+ new dig_result(src, 5)
+
+/// If the user can dig the turf
+/turf/open/floor/plating/ocean/proc/can_dig(mob/user)
+ if(!dug)
+ return TRUE
+ if(user)
+ to_chat(user, span_warning("Looks like someone has dug here already!"))
+
+
+/turf/open/floor/plating/ocean/proc/assume_self()
+ if(!atmos_adjacent_turfs)
+ immediate_calculate_adjacent_turfs()
+ for(var/direction in GLOB.cardinals)
+ var/turf/directional_turf = get_step(src, direction)
+ if(istype(directional_turf, /turf/open/floor/plating/ocean))
+ ocean_turfs |= directional_turf
+ else
+ if(isclosedturf(directional_turf))
+ RegisterSignal(directional_turf, COMSIG_TURF_DESTROY, PROC_REF(add_turf_direction), TRUE)
+ continue
+ else if(!(directional_turf in atmos_adjacent_turfs))
+ var/obj/machinery/door/found_door = locate(/obj/machinery/door) in directional_turf
+ if(found_door)
+ RegisterSignal(found_door, COMSIG_ATOM_DOOR_OPEN, TYPE_PROC_REF(/turf/open/floor/plating/ocean, door_opened))
+ RegisterSignal(directional_turf, COMSIG_TURF_UPDATE_AIR, PROC_REF(add_turf_direction_non_closed), TRUE)
+ continue
+ else
+ open_turfs.Add(direction)
+
+ if(open_turfs.len)
+ SSliquids.active_ocean_turfs |= src
+ SSliquids.unvalidated_oceans -= src
+
+/turf/open/floor/plating/ocean/proc/door_opened(datum/source)
+ SIGNAL_HANDLER
+
+ var/obj/machinery/door/found_door = source
+ var/turf/turf = get_turf(found_door)
+
+ if(turf.can_atmos_pass())
+ turf.add_liquid_list(ocean_reagents, FALSE, ocean_temp)
+
+/turf/open/floor/plating/ocean/proc/process_turf()
+ for(var/direction in open_turfs)
+ var/turf/directional_turf = get_step(src, direction)
+ if(isspaceturf(directional_turf) || istype(directional_turf, /turf/open/floor/plating/ocean))
+ RegisterSignal(directional_turf, COMSIG_TURF_DESTROY, PROC_REF(add_turf_direction), TRUE)
+ open_turfs -= direction
+ if(!open_turfs.len)
+ SSliquids.active_ocean_turfs -= src
+ return
+ else if(!(directional_turf in atmos_adjacent_turfs))
+ RegisterSignal(directional_turf, COMSIG_TURF_UPDATE_AIR, PROC_REF(add_turf_direction_non_closed), TRUE)
+ open_turfs -= direction
+ if(!open_turfs.len)
+ SSliquids.active_ocean_turfs -= src
+ return
+
+ directional_turf.add_liquid_list(ocean_reagents, FALSE, ocean_temp)
+
+/turf/open/floor/plating/ocean/proc/rebuild_adjacent()
+ ocean_turfs = list()
+ open_turfs = list()
+ for(var/direction in GLOB.cardinals)
+ var/turf/directional_turf = get_step(src, direction)
+ if(istype(directional_turf, /turf/open/floor/plating/ocean))
+ ocean_turfs |= directional_turf
+ else
+ open_turfs.Add(direction)
+
+ if(open_turfs.len)
+ SSliquids.active_ocean_turfs |= src
+ else if(src in SSliquids.active_ocean_turfs)
+ SSliquids.active_ocean_turfs -= src
+
+/turf/open/floor/plating/ocean/attackby(obj/item/C, mob/user, params)
+ . = ..()
+
+ if(C.tool_behaviour == TOOL_SHOVEL || C.tool_behaviour == TOOL_MINING)
+ if(!can_dig(user))
+ return TRUE
+
+ if(!isturf(user.loc))
+ return
+
+ balloon_alert(user, "digging...")
+
+ if(C.use_tool(src, user, 40, volume=50))
+ if(!can_dig(user))
+ return TRUE
+ getDug()
+ SSblackbox.record_feedback("tally", "pick_used_mining", 1, C.type)
+ return TRUE
+
+/obj/effect/abstract/ocean_overlay
+ icon = 'yogstation/icons/obj/effects/liquid.dmi'
+ icon_state = "ocean"
+ base_icon_state = "ocean"
+ plane = AREA_PLANE //Same as weather, etc.
+ layer = ABOVE_MOB_LAYER
+ vis_flags = NONE
+ mouse_opacity = FALSE
+ alpha = 120
+
+/obj/effect/abstract/ocean_overlay/Initialize(mapload, list/ocean_contents)
+ . = ..()
+ var/datum/reagents/fake_reagents = new
+ fake_reagents.add_reagent_list(ocean_contents)
+ color = mix_color_from_reagents(fake_reagents.reagent_list)
+ qdel(fake_reagents)
+ if(istype(loc, /area/ocean))
+ var/area/area_loc = loc
+ area_loc.base_lighting_color = color
+
+/obj/effect/abstract/ocean_overlay/proc/mix_colors(list/ocean_contents)
+ var/datum/reagents/fake_reagents = new
+ fake_reagents.add_reagent_list(ocean_contents)
+ color = mix_color_from_reagents(fake_reagents.reagent_list)
+ qdel(fake_reagents)
+ if(istype(loc, /area/ocean))
+ var/area/area_loc = loc
+ area_loc.base_lighting_color = color
+
+/turf/open/floor/plating/ocean/proc/mob_fall(datum/source, mob/M)
+ SIGNAL_HANDLER
+ var/turf/T = source
+ playsound(T, 'yogstation/sound/effects/splash.ogg', 50, 0)
+ if(iscarbon(M))
+ var/mob/living/carbon/C = M
+ to_chat(C, span_userdanger("You fall in the water!"))
+
+/turf/open/floor/plating/ocean/proc/movable_entered(datum/source, atom/movable/AM)
+ SIGNAL_HANDLER
+
+ var/turf/T = source
+ if(isobserver(AM))
+ return //ghosts, camera eyes, etc. don't make water splashy splashy
+ if(prob(30))
+ var/sound_to_play = pick(list(
+ 'yogstation/sound/effects/water_wade1.ogg',
+ 'yogstation/sound/effects/water_wade2.ogg',
+ 'yogstation/sound/effects/water_wade3.ogg',
+ 'yogstation/sound/effects/water_wade4.ogg'
+ ))
+ playsound(T, sound_to_play, 50, 0)
+ if(isliving(AM))
+ var/mob/living/arrived = AM
+ if(!arrived.has_status_effect(/datum/status_effect/ocean_affected))
+ arrived.apply_status_effect(/datum/status_effect/ocean_affected)
+
+ SEND_SIGNAL(AM, COMSIG_COMPONENT_CLEAN_ACT, CLEAN_WASH)
+
+/turf/open/floor/plating/ocean/proc/add_turf_direction(datum/source)
+ SIGNAL_HANDLER
+ var/turf/direction_turf = source
+
+ if(istype(direction_turf, /turf/open/floor/plating/ocean))
+ return
+
+ open_turfs.Add(get_dir(src, direction_turf))
+
+ if(!(src in SSliquids.active_ocean_turfs))
+ SSliquids.active_ocean_turfs |= src
+
+/turf/open/floor/plating/ocean/proc/add_turf_direction_non_closed(datum/source)
+ SIGNAL_HANDLER
+ var/turf/direction_turf = source
+
+ if(!(direction_turf in atmos_adjacent_turfs))
+ return
+
+ open_turfs.Add(get_dir(src, direction_turf))
+
+ if(!(src in SSliquids.active_ocean_turfs))
+ SSliquids.active_ocean_turfs |= src
diff --git a/yogstation/code/modules/liquids/liquid_plumbers.dm b/yogstation/code/modules/liquids/liquid_plumbers.dm
new file mode 100644
index 000000000000..c324ad9b9a82
--- /dev/null
+++ b/yogstation/code/modules/liquids/liquid_plumbers.dm
@@ -0,0 +1,336 @@
+/**
+ * Base class for underfloor plumbing machines that mess with floor liquids.
+ */
+/obj/machinery/plumbing/floor_pump
+ icon = 'yogstation/icons/obj/structures/drains.dmi'
+ base_icon_state = "active_input"
+ icon_state = "active_input"
+ anchored = FALSE
+ density = FALSE
+ idle_power_usage = 10
+ active_power_usage = 1000
+ //buffer = 300
+ //category="Distribution"
+ //reagent_flags = NO_REACT
+
+ /// 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 = 1
+
+ /// Currently pumping.
+ var/is_pumping = FALSE
+ /// Floor tile is placed down
+ var/tile_placed = FALSE
+
+ var/processes = 0
+ var/processes_required = 25
+
+/obj/machinery/plumbing/floor_pump/Initialize(mapload, bolt, layer)
+ . = ..()
+ RegisterSignal(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.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
+ 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)
+ 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(alldir = TRUE)
+ 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.liquid_group.expected_turf_height > height_regulator
+
+/obj/machinery/plumbing/floor_pump/input/pump_turf(turf/affected_turf, seconds_per_tick, multiplier)
+ if(processes < processes_required)
+ processes++
+ return
+ processes = 0
+ if(!affected_turf.liquids || !affected_turf.liquids.liquid_group || reagents.total_volume)
+ return
+ var/target_value = seconds_per_tick * (drain_flat + (affected_turf.liquids.liquid_group.total_reagent_volume * 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/liquid_group/targeted_group = affected_turf.liquids.liquid_group
+ if(!targeted_group.reagents_per_turf)
+ return
+ var/turfs_to_pull = round(target_value / targeted_group.reagents_per_turf,1)
+
+ var/list/removed_turfs = targeted_group.return_connected_liquids_in_range(affected_turf.liquids, turfs_to_pull)
+ targeted_group.trans_to_seperate_group(reagents, target_value, merge = TRUE)
+ for(var/turf/listed_turf in removed_turfs)
+ var/datum/liquid_group/listed_group = listed_turf.liquids.liquid_group
+ targeted_group.remove_from_group(listed_turf)
+ qdel(listed_turf.liquids)
+ for(var/dir in GLOB.cardinals)
+ var/turf/open/direction_turf = get_step(listed_turf, dir)
+ if(!isopenturf(direction_turf) || !direction_turf.liquids)
+ continue
+ if(!listed_group)
+ continue
+ listed_group.check_edges(direction_turf)
+
+ ///recalculate the values here because processing
+ targeted_group.total_reagent_volume = targeted_group.reagents.total_volume
+ targeted_group.reagents_per_turf = targeted_group.total_reagent_volume / length(targeted_group.members)
+
+ if(!removed_turfs.len)
+ return
+ while(removed_turfs.len)
+ var/turf/picked_turf = pick(removed_turfs)
+ var/list/output = targeted_group.try_split(picked_turf, TRUE)
+ removed_turfs -= picked_turf
+ for(var/turf/outputted_turf in output)
+ if(outputted_turf in removed_turfs)
+ removed_turfs -= outputted_turf
+
+/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.liquid_group.expected_turf_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?.liquid_group.expected_turf_height >= max_ext_volume)
+ return FALSE
+ var/turf/open/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)
+
+// 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/yogstation/code/modules/liquids/liquid_pump.dm b/yogstation/code/modules/liquids/liquid_pump.dm
new file mode 100644
index 000000000000..c33cade103b3
--- /dev/null
+++ b/yogstation/code/modules/liquids/liquid_pump.dm
@@ -0,0 +1,99 @@
+//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 = 'yogstation/icons/obj/structures/liquid_pump.dmi'
+ icon_state = "liquid_pump"
+ density = TRUE
+ max_integrity = 500
+ anchored = FALSE
+ resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
+ /// 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/wrench_act(mob/living/user, obj/item/I)
+ . = ..()
+ default_unfasten_wrench(user, I, 40)
+ 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(!user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
+ 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(mob/user)
+ . = ..()
+ . += 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"]. (Alt-click to switch)")
+ . += span_notice("The pressure gauge shows [reagents.total_volume]/[reagents.maximum_volume].")
+
+/obj/structure/liquid_pump/process()
+ if(!isturf(loc))
+ return
+ var/turf/T = loc
+ if(spewing_mode)
+ if(!reagents.total_volume)
+ return
+ var/datum/reagents/tempr = new(10000)
+ reagents.trans_to(tempr, (reagents.total_volume * pump_speed_percentage) + pump_speed_flat, no_react = TRUE)
+ T.add_liquid_from_reagents(tempr)
+ qdel(tempr)
+ else
+ if(!T.liquids)
+ return
+ var/free_space = reagents.maximum_volume - reagents.total_volume
+ if(!free_space)
+ return
+ var/target_siphon_amt = (T.liquids.liquid_group.total_reagent_volume * pump_speed_percentage) + pump_speed_flat
+ if(target_siphon_amt > free_space)
+ target_siphon_amt = free_space
+ T.liquids.liquid_group.transfer_to_atom(T.liquids, target_siphon_amt, src)
+ 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)
+ STOP_PROCESSING(SSobj, src)
+ else
+ START_PROCESSING(SSobj, src)
+ turned_on = !turned_on
+ update_icon()
+
+/obj/structure/liquid_pump/Initialize()
+ . = ..()
+ create_reagents(max_volume)
+
+/obj/structure/liquid_pump/Destroy()
+ if(turned_on)
+ STOP_PROCESSING(SSobj, src)
+ qdel(reagents)
+ return ..()
diff --git a/yogstation/code/modules/liquids/liquid_status_effect.dm b/yogstation/code/modules/liquids/liquid_status_effect.dm
new file mode 100644
index 000000000000..69d0a931c60e
--- /dev/null
+++ b/yogstation/code/modules/liquids/liquid_status_effect.dm
@@ -0,0 +1,46 @@
+/datum/status_effect/water_affected
+ id = "wateraffected"
+ alert_type = null
+ duration = -1
+
+/datum/status_effect/water_affected/on_apply()
+ //We should be inside a liquid turf if this is applied
+ calculate_water_slow()
+ return TRUE
+
+/datum/status_effect/water_affected/proc/calculate_water_slow()
+ //Factor in swimming skill here?
+ var/turf/T = get_turf(owner)
+ var/slowdown_amount = T.liquids.liquid_group.group_overlay_state * 0.5
+ owner.add_movespeed_modifier(MOVESPEED_ID_LIQUID, multiplicative_slowdown = slowdown_amount)
+
+/datum/status_effect/water_affected/tick()
+ var/turf/owner_turf = get_turf(owner)
+ if(QDELETED(owner_turf) || QDELETED(owner_turf.liquids) || owner_turf.liquids.liquid_group.group_overlay_state == LIQUID_STATE_PUDDLE)
+ qdel(src)
+ return
+ calculate_water_slow()
+ //Make the reagents touch the person
+
+ var/fraction = SUBMERGEMENT_PERCENT(owner, owner_turf.liquids)
+ owner_turf.liquids.liquid_group.expose_members_turf(owner_turf.liquids)
+ owner_turf.liquids.liquid_group.transfer_to_atom(owner_turf.liquids, ((SUBMERGEMENT_REAGENTS_TOUCH_AMOUNT * fraction / 20)), owner)
+
+ return ..()
+
+/datum/status_effect/water_affected/on_remove()
+ owner.remove_movespeed_modifier(MOVESPEED_ID_LIQUID)
+
+/datum/status_effect/ocean_affected
+ alert_type = null
+ duration = -1
+
+/datum/status_effect/ocean_affected/tick()
+ var/turf/ocean_turf = get_turf(owner)
+ if(!istype(ocean_turf, /turf/open/floor/plating/ocean))
+ qdel(src)
+
+ if(ishuman(owner))
+ var/mob/living/carbon/human/arrived = owner
+ if(is_species(owner, /datum/species/ipc) && !(arrived.wear_suit?.clothing_flags & STOPSPRESSUREDAMAGE))
+ arrived.adjustFireLoss(5)
diff --git a/yogstation/code/modules/liquids/liquid_turf.dm b/yogstation/code/modules/liquids/liquid_turf.dm
new file mode 100644
index 000000000000..226d9afd6ecd
--- /dev/null
+++ b/yogstation/code/modules/liquids/liquid_turf.dm
@@ -0,0 +1,58 @@
+/turf
+ var/obj/effect/abstract/liquid_turf/liquids
+ var/liquid_height = 0
+ var/turf_height = 0
+
+/turf/proc/reasses_liquids()
+ if(!liquids)
+ return
+ if(!liquids.liquid_group)
+ liquids.liquid_group = new(1, liquids)
+
+/turf/proc/liquid_update_turf()
+ if(!liquids)
+ return
+ //Check atmos adjacency to cut off any disconnected groups
+ if(liquids.liquid_group)
+ var/assoc_atmos_turfs = list()
+ for(var/tur in get_atmos_adjacent_turfs())
+ assoc_atmos_turfs[tur] = TRUE
+ //Check any cardinals that may have a matching group
+ for(var/direction in GLOB.cardinals)
+ var/turf/T = get_step(src, direction)
+ if(!T.liquids)
+ return
+
+/turf/proc/add_liquid_from_reagents(datum/reagents/giver, no_react = FALSE, chem_temp)
+ var/list/compiled_list = list()
+ for(var/r in giver.reagent_list)
+ var/datum/reagent/R = r
+ if(!(R.type in GLOB.liquid_blacklist))
+ compiled_list[R.type] = R.volume
+ if(!compiled_list.len) //No reagents to add, don't bother going further
+ return
+ if(!liquids)
+ liquids = new(src)
+ liquids.liquid_group.add_reagents(liquids, compiled_list, chem_temp)
+
+//More efficient than add_liquid for multiples
+/turf/proc/add_liquid_list(reagent_list, no_react = FALSE, chem_temp)
+ if(liquids && !liquids.liquid_group)
+ qdel(liquids)
+ return
+
+ if(!liquids)
+ liquids = new(src)
+ liquids.liquid_group.add_reagents(liquids, reagent_list, chem_temp)
+ //Expose turf
+ liquids.liquid_group.expose_members_turf(liquids)
+
+/turf/proc/add_liquid(reagent, amount, no_react = FALSE, chem_temp = 300)
+ if(reagent in GLOB.liquid_blacklist)
+ return
+ if(!liquids)
+ liquids = new(src)
+
+ liquids.liquid_group.add_reagent(liquids, reagent, amount, chem_temp)
+ //Expose turf
+ liquids.liquid_group.expose_members_turf(liquids)
diff --git a/yogstation/code/modules/liquids/tools.dm b/yogstation/code/modules/liquids/tools.dm
new file mode 100644
index 000000000000..f0f21461561e
--- /dev/null
+++ b/yogstation/code/modules/liquids/tools.dm
@@ -0,0 +1,75 @@
+/client/proc/spawn_liquid()
+ set category = "Misc.Unused"
+ 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 = stripped_input(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 = input(usr, "Volume:", "Choose volume") as num
+ if(!volume)
+ return
+ if(volume >= 100000)
+ to_chat(usr, span_warning("Please limit the volume to below 100000 units!"))
+ return
+ var/turf/epicenter = get_turf(mob)
+ epicenter.add_liquid(choice, volume, FALSE, 300)
+ message_admins("[ADMIN_LOOKUPFLW(usr)] spawned liquid at [epicenter.loc] ([choice] - [volume]).")
+ log_admin("[key_name(usr)] spawned liquid at [epicenter.loc] ([choice] - [volume]).")
+
+/client/proc/remove_liquid()
+ set name = "Remove Liquids"
+ set category = "Misc.Unused"
+ set desc = "Removes liquids in a specified radius."
+ var/turf/epicenter = get_turf(mob)
+
+ var/range = input(usr, "Enter range:", "Range selection", 2) as num
+
+ for(var/obj/effect/abstract/liquid_turf/liquid in range(range, epicenter))
+ if(QDELETED(liquid))
+ continue
+ if(!liquid)
+ continue
+ if(!liquid.liquid_group)
+ continue
+ liquid.liquid_group.remove_any(liquid, liquid.liquid_group.reagents_per_turf)
+ qdel(liquid)
+
+ 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]")
+
+
+
+/client/proc/change_ocean()
+ set category = "Admin.Fun"
+ set name = "Change Ocean Liquid"
+ set desc = "Changes the reagent of the ocean."
+
+
+ var/choice = tgui_input_list(usr, "Choose a reagent", "Ocean Reagent", subtypesof(/datum/reagent))
+ if(!choice)
+ return
+ var/datum/reagent/chosen_reagent = choice
+ var/rebuilt = FALSE
+ for(var/turf/open/floor/plating/ocean/listed_ocean as anything in SSliquids.ocean_turfs)
+ if(!rebuilt)
+ listed_ocean.ocean_reagents = list()
+ listed_ocean.ocean_reagents[chosen_reagent] = 10
+ listed_ocean.static_overlay.mix_colors(listed_ocean.ocean_reagents)
+ for(var/area/ocean/ocean_types in GLOB.initalized_ocean_areas)
+ ocean_types.base_lighting_color = listed_ocean.static_overlay.color
+ ocean_types.update_base_lighting()
+ rebuilt = TRUE
diff --git a/yogstation/icons/obj/effects/liquid.dmi b/yogstation/icons/obj/effects/liquid.dmi
new file mode 100644
index 000000000000..70f93f53b2cc
Binary files /dev/null and b/yogstation/icons/obj/effects/liquid.dmi differ
diff --git a/yogstation/icons/obj/effects/liquid_overlays.dmi b/yogstation/icons/obj/effects/liquid_overlays.dmi
new file mode 100644
index 000000000000..bf1a1a4c40a3
Binary files /dev/null and b/yogstation/icons/obj/effects/liquid_overlays.dmi differ
diff --git a/yogstation/icons/obj/effects/splash.dmi b/yogstation/icons/obj/effects/splash.dmi
new file mode 100644
index 000000000000..9a5f4fbaf1fa
Binary files /dev/null and b/yogstation/icons/obj/effects/splash.dmi differ
diff --git a/yogstation/icons/obj/items/tiles.dmi b/yogstation/icons/obj/items/tiles.dmi
new file mode 100644
index 000000000000..2f9c4e81f00b
Binary files /dev/null and b/yogstation/icons/obj/items/tiles.dmi differ
diff --git a/yogstation/icons/obj/structures/drains.dmi b/yogstation/icons/obj/structures/drains.dmi
new file mode 100644
index 000000000000..5c305b7d2376
Binary files /dev/null and b/yogstation/icons/obj/structures/drains.dmi differ
diff --git a/yogstation/icons/obj/structures/liquid_pump.dmi b/yogstation/icons/obj/structures/liquid_pump.dmi
new file mode 100644
index 000000000000..012dda8bb5e1
Binary files /dev/null and b/yogstation/icons/obj/structures/liquid_pump.dmi differ
diff --git a/yogstation/icons/turf/floors/elevated_iron.dmi b/yogstation/icons/turf/floors/elevated_iron.dmi
new file mode 100644
index 000000000000..422ebaddd9a4
Binary files /dev/null and b/yogstation/icons/turf/floors/elevated_iron.dmi differ
diff --git a/yogstation/icons/turf/floors/lowered_iron.dmi b/yogstation/icons/turf/floors/lowered_iron.dmi
new file mode 100644
index 000000000000..8171815033be
Binary files /dev/null and b/yogstation/icons/turf/floors/lowered_iron.dmi differ
diff --git a/yogstation/icons/turf/floors/seafloor.dmi b/yogstation/icons/turf/floors/seafloor.dmi
new file mode 100644
index 000000000000..f78eed47f7dd
Binary files /dev/null and b/yogstation/icons/turf/floors/seafloor.dmi differ
diff --git a/yogstation/sound/effects/splash.ogg b/yogstation/sound/effects/splash.ogg
new file mode 100644
index 000000000000..22f17c07901c
Binary files /dev/null and b/yogstation/sound/effects/splash.ogg differ
diff --git a/yogstation/sound/effects/water_wade1.ogg b/yogstation/sound/effects/water_wade1.ogg
new file mode 100644
index 000000000000..27527f9d8b8e
Binary files /dev/null and b/yogstation/sound/effects/water_wade1.ogg differ
diff --git a/yogstation/sound/effects/water_wade2.ogg b/yogstation/sound/effects/water_wade2.ogg
new file mode 100644
index 000000000000..f9c15cab64a9
Binary files /dev/null and b/yogstation/sound/effects/water_wade2.ogg differ
diff --git a/yogstation/sound/effects/water_wade3.ogg b/yogstation/sound/effects/water_wade3.ogg
new file mode 100644
index 000000000000..3daccae01a1d
Binary files /dev/null and b/yogstation/sound/effects/water_wade3.ogg differ
diff --git a/yogstation/sound/effects/water_wade4.ogg b/yogstation/sound/effects/water_wade4.ogg
new file mode 100644
index 000000000000..7ba705f4991f
Binary files /dev/null and b/yogstation/sound/effects/water_wade4.ogg differ
diff --git a/yogstation/sound/effects/watersplash.ogg b/yogstation/sound/effects/watersplash.ogg
new file mode 100644
index 000000000000..a9a6e4860878
Binary files /dev/null and b/yogstation/sound/effects/watersplash.ogg differ