diff --git a/code/__defines/hoses.dm b/code/__defines/hoses.dm
new file mode 100644
index 0000000000..2a7bd4d803
--- /dev/null
+++ b/code/__defines/hoses.dm
@@ -0,0 +1,3 @@
+#define HOSE_INPUT "Input"// Only pull liquid.
+#define HOSE_OUTPUT "Output"// Only push liquid.
+#define HOSE_NEUTRAL "Neutral"// Equalize liquids, not super efficient, can waste reagents due to rounding.
\ No newline at end of file
diff --git a/code/datums/autolathe/tools.dm b/code/datums/autolathe/tools.dm
index d0aa2aa196..9e9a87ea71 100644
--- a/code/datums/autolathe/tools.dm
+++ b/code/datums/autolathe/tools.dm
@@ -42,3 +42,15 @@
/datum/category_item/autolathe/tools/welder_industrial
name = "industrial welding tool"
path =/obj/item/weapon/weldingtool/largetank
+
+/datum/category_item/autolathe/tools/spraybottle
+ name = "spray bottle"
+ path = /obj/item/weapon/reagent_containers/spray
+ resources = list(MAT_PLASTIC = 2000)
+
+/datum/category_item/autolathe/tools/spraynozzle
+ name = "spray nozzle"
+ path = /obj/item/weapon/reagent_containers/spray
+ resources = list(MAT_PLASTIC = 5000, DEFAULT_WALL_MATERIAL = 2000)
+ hidden = 1
+ man_rating = 2
diff --git a/code/datums/beam.dm b/code/datums/beam.dm
index a35abf8c37..fc4e696702 100644
--- a/code/datums/beam.dm
+++ b/code/datums/beam.dm
@@ -6,6 +6,7 @@
var/icon/base_icon = null
var/icon
var/icon_state = "" //icon state of the main segments of the beam
+ var/beam_color = null // Color of the beam segments
var/max_distance = 0
var/endtime = 0
var/sleep_time = 3
@@ -15,7 +16,7 @@
var/static_beam = 0
var/beam_type = /obj/effect/ebeam //must be subtype
-/datum/beam/New(beam_origin,beam_target,beam_icon='icons/effects/beam.dmi',beam_icon_state="b_beam",time=50,maxdistance=10,btype = /obj/effect/ebeam,beam_sleep_time=3)
+/datum/beam/New(beam_origin,beam_target,beam_icon='icons/effects/beam.dmi',beam_icon_state="b_beam",time=50,maxdistance=10,btype = /obj/effect/ebeam,beam_sleep_time=3,new_beam_color = null)
endtime = world.time+time
origin = beam_origin
origin_oldloc = get_turf(origin)
@@ -28,6 +29,8 @@
base_icon = new(beam_icon,beam_icon_state)
icon = beam_icon
icon_state = beam_icon_state
+ if(new_beam_color)
+ beam_color = new_beam_color
beam_type = btype
/datum/beam/proc/Start()
@@ -68,9 +71,12 @@
var/matrix/rot_matrix = matrix()
rot_matrix.Turn(Angle)
+ var/turf/T_target = get_turf(target) //Turfs are referenced instead of the objects directly so that beams will link between 2 objects inside other objects.
+ var/turf/T_origin = get_turf(origin)
+
//Translation vector for origin and target
- var/DX = (32*target.x+target.pixel_x)-(32*origin.x+origin.pixel_x)
- var/DY = (32*target.y+target.pixel_y)-(32*origin.y+origin.pixel_y)
+ var/DX = (32*T_target.x+target.pixel_x)-(32*T_origin.x+origin.pixel_x)
+ var/DY = (32*T_target.y+target.pixel_y)-(32*T_origin.y+origin.pixel_y)
var/N = 0
var/length = round(sqrt((DX)**2+(DY)**2)) //hypotenuse of the triangle formed by target and origin's displacement
@@ -78,6 +84,10 @@
if(QDELETED(src) || finished)
break
var/obj/effect/ebeam/X = new beam_type(origin_oldloc)
+
+ if(beam_color)
+ X.color = beam_color
+
X.owner = src
elements |= X
@@ -184,8 +194,8 @@
-/atom/proc/Beam(atom/BeamTarget,icon_state="b_beam",icon='icons/effects/beam.dmi',time=50, maxdistance=10,beam_type=/obj/effect/ebeam,beam_sleep_time=3)
- var/datum/beam/newbeam = new(src,BeamTarget,icon,icon_state,time,maxdistance,beam_type,beam_sleep_time)
+/atom/proc/Beam(atom/BeamTarget,icon_state="b_beam",icon='icons/effects/beam.dmi',time=50, maxdistance=10,beam_type=/obj/effect/ebeam,beam_sleep_time=3,beam_color = null)
+ var/datum/beam/newbeam = new(src,BeamTarget,icon,icon_state,time,maxdistance,beam_type,beam_sleep_time,beam_color)
spawn(0)
newbeam.Start()
return newbeam
diff --git a/code/datums/supplypacks/misc.dm b/code/datums/supplypacks/misc.dm
index 40b8c33670..72d20b0686 100644
--- a/code/datums/supplypacks/misc.dm
+++ b/code/datums/supplypacks/misc.dm
@@ -179,3 +179,12 @@
cost = 40
containertype = /obj/structure/closet/crate/zenghu
containername = "emergency rations"
+
+/datum/supply_pack/misc/reagentpump
+ name = "Machine - Pump"
+ contains = list(
+ /obj/machinery/pump = 1
+ )
+ cost = 60
+ containertype = /obj/structure/closet/crate/large/xion
+ containername = "pump crate"
diff --git a/code/game/machinery/reagents/pump.dm b/code/game/machinery/reagents/pump.dm
new file mode 100644
index 0000000000..04916d4d9b
--- /dev/null
+++ b/code/game/machinery/reagents/pump.dm
@@ -0,0 +1,270 @@
+
+/obj/machinery/pump
+ name = "fluid pump"
+ desc = "A fluid pumping machine."
+
+ description_info = "A machine that can pump fluid from certain turfs.
\
+ Water can be pumped from any body of water. Certain locations or environmental\
+ conditions can cause different byproducts to be produced.
\
+ Magma or Lava can be pumped to produce mineralized fluid."
+
+ anchored = 0
+ density = 1
+
+ icon = 'icons/obj/machines/reagent.dmi'
+ icon_state = "pump"
+
+ circuit = /obj/item/weapon/circuitboard/fluidpump
+
+ var/on = 0
+ var/obj/item/weapon/cell/cell = null
+ var/use = 200
+ var/efficiency = 1
+ var/reagents_per_cycle = 40
+ var/unlocked = 0
+ var/open = 0
+
+ var/obj/item/hose_connector/output/Output
+
+// Overlay cache vars.
+ var/icon/liquid
+ var/icon/tank
+ var/icon/powerlow
+ var/icon/glass
+ var/icon/open_overlay
+ var/icon/cell_overlay
+
+/obj/machinery/pump/Initialize()
+ create_reagents(200)
+ . = ..()
+ default_apply_parts()
+
+ if(ispath(cell))
+ cell = new cell(src)
+
+ Output = new(src)
+
+ RefreshParts()
+ update_icon()
+
+/obj/machinery/pump/update_icon()
+ ..()
+ if(!tank)
+ tank = new/icon(icon, "[icon_state]-volume")
+
+ if(!powerlow)
+ powerlow = new/icon(icon, "[icon_state]-lowpower")
+
+ if(!liquid)
+ var/icon/cutter = new/icon(icon, "[icon_state]-volume")
+
+ cutter.Blend(rgb(0, 0, 0,), ICON_MULTIPLY)
+
+ liquid = new/icon(icon, "[icon_state]-cutting")
+
+ liquid.Blend(cutter,ICON_AND)
+
+ if(!glass)
+ glass = new/icon(icon, "[icon_state]-glass")
+
+ glass.Blend(rgb(1,1,1,0.5), ICON_MULTIPLY)
+
+ if(!open_overlay)
+ open_overlay = new/icon(icon, "[icon_state]-open")
+
+ if(!cell_overlay)
+ cell_overlay = new/icon(icon, "[icon_state]-cell")
+
+ cut_overlays()
+ add_overlay(tank)
+
+ if(cell && cell.charge < (use * CELLRATE / efficiency))
+ add_overlay(powerlow)
+
+ if(reagents.total_volume >= 1)
+ var/list/hextorgb = hex2rgb(reagents.get_color())
+ liquid.GrayScale()
+
+ liquid.Blend(rgb(hextorgb[1],hextorgb[2],hextorgb[3]),ICON_MULTIPLY)
+
+ add_overlay(liquid)
+
+ add_overlay(glass)
+
+ if(open)
+ add_overlay(open_overlay)
+
+ if(cell)
+ add_overlay(cell_overlay)
+
+ if(on)
+ icon_state = "[initial(icon_state)]-running"
+
+ else
+ icon_state = "[initial(icon_state)]"
+
+/obj/machinery/pump/process()
+ if(Output.get_pairing())
+ reagents.trans_to_holder(Output.reagents, Output.reagents.maximum_volume)
+ if(prob(5))
+ visible_message("\The [src] gurgles as it exports fluid.")
+
+ if(!on)
+ return
+
+ if(!cell || (cell.charge < (use * CELLRATE / efficiency)))
+ turn_off(TRUE)
+ return
+
+ handle_pumping()
+ update_icon()
+
+/obj/machinery/pump/RefreshParts()
+ for(var/obj/item/weapon/stock_parts/manipulator/SM in component_parts)
+ efficiency = SM.rating
+
+ var/total_bin_rating = 0
+ for(var/obj/item/weapon/stock_parts/matter_bin/SB in component_parts)
+ total_bin_rating += SB.rating
+
+ reagents.maximum_volume = round(initial(reagents.maximum_volume) + 100 * total_bin_rating)
+
+ return
+
+/obj/machinery/pump/power_change()
+ if(!cell || cell.charge < (use * CELLRATE) || !anchored)
+ return turn_off(TRUE)
+
+ return FALSE
+
+
+// Returns 0 on failure and 1 on success
+/obj/machinery/pump/proc/turn_on(var/loud = 0)
+ if(!cell)
+ return 0
+ if(cell.charge < (use * CELLRATE))
+ return 0
+
+ on = 1
+ update_icon()
+ if(loud)
+ visible_message("\The [src] turns on.")
+ return 1
+
+/obj/machinery/pump/proc/turn_off(var/loud = 0)
+ on = 0
+ set_light(0, 0)
+ update_icon()
+ if(loud)
+ visible_message("\The [src] shuts down.")
+
+ if(!on)
+ return TRUE
+
+ return FALSE
+
+/obj/machinery/pump/attack_ai(mob/user as mob)
+ if(istype(user, /mob/living/silicon/robot) && Adjacent(user))
+ return attack_hand(user)
+
+ if(on)
+ turn_off(TRUE)
+ else
+ if(!turn_on(1))
+ to_chat(user, "You try to turn on \the [src] but it does not work.")
+
+/obj/machinery/pump/attack_hand(mob/user as mob)
+ if(open && cell)
+ if(ishuman(user))
+ if(!user.get_active_hand())
+ user.put_in_hands(cell)
+ cell.loc = user.loc
+ else
+ cell.loc = src.loc
+
+ cell.add_fingerprint(user)
+ cell.update_icon()
+
+ cell = null
+ on = 0
+ set_light(0)
+ to_chat(user, "You remove the power cell.")
+ update_icon()
+ return
+
+ if(on)
+ turn_off(TRUE)
+ else if(anchored)
+ if(!turn_on(1))
+ to_chat(user, "You try to turn on \the [src] but it does not work.")
+
+ update_icon()
+
+/obj/machinery/pump/attackby(obj/item/weapon/W as obj, mob/user as mob)
+ if(W.is_screwdriver())
+ if(!open)
+ if(unlocked)
+ unlocked = 0
+ to_chat(user, "You screw the battery panel in place.")
+ else
+ unlocked = 1
+ to_chat(user, "You unscrew the battery panel.")
+
+ else if(W.is_crowbar())
+ if(unlocked)
+ if(open)
+ open = 0
+ overlays = null
+ to_chat(user, "You crowbar the battery panel in place.")
+ else
+ if(unlocked)
+ open = 1
+ to_chat(user, "You remove the battery panel.")
+
+ else if(W.is_wrench())
+ if(on)
+ to_chat(user, "\The [src] is active. Turn it off before trying to move it!")
+ return
+ default_unfasten_wrench(user, W, 2 SECONDS)
+
+ else if(istype(W, /obj/item/weapon/cell))
+ if(open)
+ if(cell)
+ to_chat(user, "There is a power cell already installed.")
+ else
+ user.drop_item()
+ W.loc = src
+ cell = W
+ to_chat(user, "You insert the power cell.")
+ else
+ ..()
+ RefreshParts()
+ update_icon()
+
+/obj/machinery/pump/proc/handle_pumping()
+ var/turf/T = get_turf(src)
+
+ if(istype(T, /turf/simulated/floor/water))
+ cell.use(use * CELLRATE / efficiency)
+ reagents.add_reagent("water", reagents_per_cycle)
+
+ if(T.temperature <= T0C)
+ reagents.add_reagent("ice", round(reagents_per_cycle / 2, 0.1))
+
+ if((istype(T,/turf/simulated/floor/water/pool) || istype(T,/turf/simulated/floor/water/deep/pool)))
+ reagents.add_reagent("chlorine", round(reagents_per_cycle / 10 * efficiency, 0.1))
+
+ else if(istype(T,/turf/simulated/floor/water/contaminated))
+ reagents.add_reagent("vatstabilizer", round(reagents_per_cycle / 2))
+
+ if(T.loc.name == "Sea") // Saltwater.
+ reagents.add_reagent("sodiumchloride", round(reagents_per_cycle / 10 * efficiency, 0.1))
+
+ for(var/turf/simulated/mineral/MT in range(5))
+ if(MT.mineral)
+ var/obj/effect/mineral/OR = MT.mineral
+ reagents.add_reagent(OR.ore_reagent, round(reagents_per_cycle / 20 * efficiency, 0.1))
+
+ else if(istype(T, /turf/simulated/floor/lava))
+ cell.use(use * CELLRATE / efficiency * 4)
+ reagents.add_reagent("mineralizedfluid", round(reagents_per_cycle * efficiency / 2, 0.1))
diff --git a/code/game/objects/items/weapons/circuitboards/machinery/fluidpump.dm b/code/game/objects/items/weapons/circuitboards/machinery/fluidpump.dm
new file mode 100644
index 0000000000..3646d7311a
--- /dev/null
+++ b/code/game/objects/items/weapons/circuitboards/machinery/fluidpump.dm
@@ -0,0 +1,14 @@
+
+#ifndef T_BOARD
+#error T_BOARD macro is not defined but we need it!
+#endif
+
+/obj/item/weapon/circuitboard/fluidpump
+ name = T_BOARD("fluid pump")
+ build_path = /obj/machinery/pump
+ board_type = new /datum/frame/frame_types/machine
+ origin_tech = list(TECH_DATA = 1)
+ req_components = list(
+ /obj/item/weapon/stock_parts/matter_bin = 2,
+ /obj/item/weapon/stock_parts/motor = 2,
+ /obj/item/weapon/stock_parts/manipulator = 1)
diff --git a/code/modules/materials/material_recipes.dm b/code/modules/materials/material_recipes.dm
index b5d2863f4a..e981c1130c 100644
--- a/code/modules/materials/material_recipes.dm
+++ b/code/modules/materials/material_recipes.dm
@@ -138,6 +138,7 @@
recipes += new/datum/stack_recipe("lampshade", /obj/item/weapon/lampshade, 1, time = 1, pass_stack_color = TRUE)
recipes += new/datum/stack_recipe("plastic net", /obj/item/weapon/material/fishing_net, 25, time = 1 MINUTE, pass_stack_color = TRUE)
recipes += new/datum/stack_recipe("plastic fishtank", /obj/item/glass_jar/fish/plastic, 2, time = 30 SECONDS)
+ recipes += new/datum/stack_recipe("reagent tubing", /obj/item/stack/hose, 1, 4, 20, pass_stack_color = TRUE)
/material/wood/generate_recipes()
..()
diff --git a/code/modules/mining/mineral_effect.dm b/code/modules/mining/mineral_effect.dm
index 5f8daed8d1..12781a7ea4 100644
--- a/code/modules/mining/mineral_effect.dm
+++ b/code/modules/mining/mineral_effect.dm
@@ -7,11 +7,14 @@
anchored = 1
var/ore_key
var/image/scanner_image
+ var/ore_reagent // Reagent from pumping water near this ore.
/obj/effect/mineral/New(var/newloc, var/ore/M)
..(newloc)
name = "[M.display_name] deposit"
ore_key = M.name
+ if(M.reagent)
+ ore_reagent = M.reagent
icon_state = "rock_[ore_key]"
var/turf/T = get_turf(src)
layer = T.layer+0.1
diff --git a/code/modules/mining/ore_datum.dm b/code/modules/mining/ore_datum.dm
index f888ae3bb6..0bca24db1f 100644
--- a/code/modules/mining/ore_datum.dm
+++ b/code/modules/mining/ore_datum.dm
@@ -17,6 +17,7 @@ var/global/list/ore_data = list()
"million" = 999
)
var/xarch_source_mineral = "iron"
+ var/reagent = "silicate"
/ore/New()
. = ..()
@@ -36,6 +37,7 @@ var/global/list/ore_data = list()
"million" = 704
)
xarch_source_mineral = "potassium"
+ reagent = "uranium"
/ore/hematite
name = "hematite"
@@ -46,6 +48,7 @@ var/global/list/ore_data = list()
spread_chance = 25
ore = /obj/item/weapon/ore/iron
scan_icon = "mineral_common"
+ reagent = "iron"
/ore/coal
name = "carbon"
@@ -57,6 +60,7 @@ var/global/list/ore_data = list()
spread_chance = 25
ore = /obj/item/weapon/ore/coal
scan_icon = "mineral_common"
+ reagent = "carbon"
/ore/glass
name = "sand"
@@ -81,6 +85,7 @@ var/global/list/ore_data = list()
"billion_lower" = 10
)
xarch_source_mineral = "phoron"
+ reagent = "phoron"
/ore/silver
name = "silver"
@@ -90,6 +95,7 @@ var/global/list/ore_data = list()
spread_chance = 10
ore = /obj/item/weapon/ore/silver
scan_icon = "mineral_uncommon"
+ reagent = "silver"
/ore/gold
smelts_to = "gold"
@@ -105,6 +111,7 @@ var/global/list/ore_data = list()
"billion" = 4,
"billion_lower" = 3
)
+ reagent = "gold"
/ore/diamond
name = "diamond"
@@ -116,6 +123,7 @@ var/global/list/ore_data = list()
ore = /obj/item/weapon/ore/diamond
scan_icon = "mineral_rare"
xarch_source_mineral = "nitrogen"
+ reagent = "carbon"
/ore/platinum
name = "platinum"
@@ -127,6 +135,7 @@ var/global/list/ore_data = list()
spread_chance = 10
ore = /obj/item/weapon/ore/osmium
scan_icon = "mineral_rare"
+ reagent = "platinum"
/ore/hydrogen
name = "mhydrogen"
@@ -134,6 +143,7 @@ var/global/list/ore_data = list()
smelts_to = "tritium"
compresses_to = "mhydrogen"
scan_icon = "mineral_rare"
+ reagent = "hydrogen"
/ore/verdantium
name = MAT_VERDANTIUM
@@ -156,6 +166,7 @@ var/global/list/ore_data = list()
spread_chance = 10
ore = /obj/item/weapon/ore/marble
scan_icon = "mineral_common"
+ reagent = "calciumcarbonate"
/ore/lead
name = MAT_LEAD
@@ -165,3 +176,4 @@ var/global/list/ore_data = list()
spread_chance = 20
ore = /obj/item/weapon/ore/lead
scan_icon = "mineral_rare"
+ reagent = "lead"
diff --git a/code/modules/mob/_modifiers/medical.dm b/code/modules/mob/_modifiers/medical.dm
index 7e150ceb93..3d17071903 100644
--- a/code/modules/mob/_modifiers/medical.dm
+++ b/code/modules/mob/_modifiers/medical.dm
@@ -50,3 +50,13 @@
evasion = -5
attack_speed_percent = 1.1
disable_duration_percent = 1.05
+
+/datum/modifier/clone_stabilizer
+ name = "clone stabilized"
+ desc = "Your body's regeneration is highly restricted."
+
+ on_created_text = "You feel nauseous."
+ on_expired_text = "You feel healthier."
+ stacks = MODIFIER_STACK_EXTEND
+
+ incoming_healing_percent = 0.1
diff --git a/code/modules/reagents/Chemistry-Reagents/Chemistry-Reagents-Modifiers.dm b/code/modules/reagents/Chemistry-Reagents/Chemistry-Reagents-Modifiers.dm
index cd2e6767b4..bed5da5f47 100644
--- a/code/modules/reagents/Chemistry-Reagents/Chemistry-Reagents-Modifiers.dm
+++ b/code/modules/reagents/Chemistry-Reagents/Chemistry-Reagents-Modifiers.dm
@@ -57,3 +57,14 @@
var/turf/simulated/S = T
S.freeze_floor()
return
+
+/datum/reagent/modapplying/vatstabilizer
+ name = "clone growth inhibitor"
+ id = "vatstabilizer"
+ description = "A compound produced by NanoTrasen using a secret blend of phoron and toxins to stop the rampant growth of a clone beyond intended states."
+ taste_description = "sour glue"
+ color = "#060501"
+ metabolism = REM * 0.2
+
+ modifier_to_add = /datum/modifier/clone_stabilizer
+ modifier_duration = 30 SECONDS
diff --git a/code/modules/reagents/Chemistry-Reagents/Chemistry-Reagents-Other.dm b/code/modules/reagents/Chemistry-Reagents/Chemistry-Reagents-Other.dm
index ba7dc93d63..18a281783a 100644
--- a/code/modules/reagents/Chemistry-Reagents/Chemistry-Reagents-Other.dm
+++ b/code/modules/reagents/Chemistry-Reagents/Chemistry-Reagents-Other.dm
@@ -524,6 +524,14 @@
reagent_state = LIQUID
color = "#DF9FBF"
+/datum/reagent/mineralfluid
+ name = "Mineral-Rich Fluid"
+ id = "mineralizedfluid"
+ description = "A warm, mineral-rich fluid."
+ taste_description = "salt"
+ reagent_state = LIQUID
+ color = "#ff205255"
+
// The opposite to healing nanites, exists to make unidentified hypos implied to have nanites not be 100% safe.
/datum/reagent/defective_nanites
name = "Defective Nanites"
diff --git a/code/modules/reagents/Chemistry-Reagents/Chemistry-Reagents-Toxins.dm b/code/modules/reagents/Chemistry-Reagents/Chemistry-Reagents-Toxins.dm
index e18c2e56d9..74abe0b4bf 100644
--- a/code/modules/reagents/Chemistry-Reagents/Chemistry-Reagents-Toxins.dm
+++ b/code/modules/reagents/Chemistry-Reagents/Chemistry-Reagents-Toxins.dm
@@ -120,6 +120,13 @@
spawn(rand(30, 60))
M.IgniteMob()
+/datum/reagent/toxin/lead
+ name = "lead"
+ id = "lead"
+ description = "Elemental Lead."
+ color = "#273956"
+ strength = 4
+
/datum/reagent/toxin/spidertoxin
name = "Spidertoxin"
id = "spidertoxin"
diff --git a/code/modules/reagents/hoses/connector.dm b/code/modules/reagents/hoses/connector.dm
new file mode 100644
index 0000000000..bac7fc7c6a
--- /dev/null
+++ b/code/modules/reagents/hoses/connector.dm
@@ -0,0 +1,138 @@
+
+/obj/attackby(var/obj/item/O, var/mob/user)
+ . = ..()
+
+ if(locate(/obj/item/hose_connector) in src)
+ if(O.is_wirecutter())
+ var/list/available_sockets = list()
+
+ for(var/obj/item/hose_connector/HC in src)
+ if(HC.my_hose)
+ available_sockets |= HC
+
+ if(LAZYLEN(available_sockets))
+ if(available_sockets.len == 1)
+ var/obj/item/hose_connector/AC = available_sockets[1]
+ var/choice = alert("Are you sure you want to disconnect [AC]?", "Confirm", "Yes", "No")
+
+ if(choice == "Yes" && Adjacent(user))
+ visible_message("[user] disconnects \the hose from \the [src].")
+ AC.my_hose.disconnect()
+ return
+
+ else
+
+ var/choice = input("Select a target hose connector.", "Socket Disconnect", null) as null|anything in available_sockets
+
+ if(choice)
+ var/obj/item/hose_connector/AC = choice
+ var/confirm = alert("Are you sure you want to disconnect [AC]?", "Confirm", "Yes", "No")
+
+ if(confirm == "Yes" && Adjacent(user))
+ visible_message("[user] disconnects \the hose from \the [src].")
+ AC.my_hose.disconnect()
+
+ return
+
+/obj/item/hose_connector
+ name = "hose connector"
+ desc = "A socket for a hose. It.. doesn't do anything on its own."
+
+ var/obj/carrier = null
+
+ var/flow_direction = HOSE_NEUTRAL
+
+ var/datum/hose/my_hose = null
+
+/obj/item/hose_connector/Destroy()
+ if(my_hose)
+ my_hose.disconnect()
+ my_hose = null
+ if(carrier)
+ carrier = null
+ ..()
+
+/obj/item/hose_connector/Initialize()
+ ..()
+
+ create_reagents(100)
+
+ if(!istype(loc, /turf))
+ name = "[flow_direction] hose connector ([loc])"
+
+/obj/item/hose_connector/proc/valid_connection(var/obj/item/hose_connector/C)
+ if(istype(C))
+ if(C.my_hose)
+ return FALSE
+
+ if(C.flow_direction in list(HOSE_INPUT, HOSE_OUTPUT) - flow_direction)
+ return TRUE
+
+ return FALSE
+
+/obj/item/hose_connector/proc/disconnect()
+ my_hose = null
+
+/obj/item/hose_connector/proc/connect(var/datum/hose/H = null)
+ if(istype(H))
+ my_hose = H
+
+/obj/item/hose_connector/proc/setup_hoses(var/obj/item/hose_connector/target)
+ if(target)
+ var/datum/hose/H = new()
+
+ H.set_hose(src, target)
+
+/obj/item/hose_connector/proc/get_pairing()
+ if(my_hose)
+ return my_hose.get_pairing(src)
+
+ return
+
+/*
+ * Subtypes
+ */
+
+/obj/item/hose_connector/input
+ name = "hose input"
+ flow_direction = HOSE_INPUT
+
+/obj/item/hose_connector/input/active
+ name = "active hose"
+
+/obj/item/hose_connector/input/active/Destroy()
+ STOP_PROCESSING(SSobj, src)
+ ..()
+
+/obj/item/hose_connector/input/active/Initialize()
+ ..()
+ START_PROCESSING(SSobj, src)
+
+ if(!isturf(loc))
+ carrier = loc
+
+/obj/item/hose_connector/input/active/process()
+ if(carrier)
+ reagents.trans_to_obj(carrier, reagents.maximum_volume)
+
+/obj/item/hose_connector/output
+ name = "hose output"
+ flow_direction = HOSE_OUTPUT
+
+/obj/item/hose_connector/output/active
+ name = "active hose"
+
+/obj/item/hose_connector/output/active/Destroy()
+ STOP_PROCESSING(SSobj, src)
+ ..()
+
+/obj/item/hose_connector/output/active/Initialize()
+ ..()
+ START_PROCESSING(SSobj, src)
+
+ if(!isturf(loc))
+ carrier = loc
+
+/obj/item/hose_connector/output/active/process()
+ if(carrier)
+ carrier.reagents.trans_to_holder(reagents, reagents.maximum_volume)
diff --git a/code/modules/reagents/hoses/hose.dm b/code/modules/reagents/hoses/hose.dm
new file mode 100644
index 0000000000..dfe0eade79
--- /dev/null
+++ b/code/modules/reagents/hoses/hose.dm
@@ -0,0 +1,113 @@
+
+GLOBAL_LIST_EMPTY(hoses)
+
+/obj/effect/ebeam/hose
+ plane = OBJ_PLANE
+ layer = STAIRS_LAYER
+
+/datum/hose
+ var/name = "hose"
+
+ var/obj/item/hose_connector/node1 = null
+ var/obj/item/hose_connector/node2 = null
+
+ var/hose_color = "#ffffff"
+
+ var/initial_distance = 7
+
+ var/datum/beam/hose = null
+
+/datum/hose/proc/get_pairing(var/obj/item/hose_connector/target)
+ if(target)
+ if(target == node1)
+ return node2
+ else if(target == node2)
+ return node1
+
+ return
+
+/datum/hose/proc/disconnect()
+ if(node1)
+ node1.disconnect()
+ node1 = null
+ if(node2)
+ node2.disconnect()
+ node2 = null
+
+/datum/hose/proc/set_hose(var/obj/item/hose_connector/target1, var/obj/item/hose_connector/target2)
+ if(target1 && target2)
+ node1 = target1
+ node2 = target2
+
+ node1.connect(src)
+ node2.connect(src)
+
+ name = "[name] ([node1],[node2])"
+
+ initial_distance = get_dist(get_turf(node1), get_turf(node2))
+
+ GLOB.hoses |= src
+ START_PROCESSING(SSobj, src)
+
+/datum/hose/process()
+ if(node1 && node2)
+ if(get_dist(get_turf(node1), get_turf(node2)) > 0)
+ hose = node1.loc.Beam(node2.loc, icon_state = "hose", beam_color = hose_color, maxdistance = world.view, beam_type = /obj/effect/ebeam/hose)
+
+ if(!hose || get_dist(get_turf(node1), get_turf(node2)) > initial_distance) // The hose didn't form. Something's fucky.
+ disconnect()
+ return
+
+ var/datum/reagents/reagent_node1 = node1.reagents
+ var/datum/reagents/reagent_node2 = node2.reagents
+
+ switch(node1.flow_direction) // Node 1 is the default 'master', interactions are considered in all current possible states in regards to it, however.
+ if(HOSE_INPUT)
+ if(node2.flow_direction == HOSE_NEUTRAL) // We're input, they're neutral. Take half of our volume.
+ reagent_node2.trans_to_holder(reagent_node1, reagent_node1.maximum_volume / 2)
+ else if(node2.flow_direction == HOSE_OUTPUT) // We're input, they're output. Take all of our volume.
+ reagent_node2.trans_to_holder(reagent_node1, reagent_node1.maximum_volume)
+
+ if(HOSE_OUTPUT) // We're output, give all of their maximum volume.
+ reagent_node1.trans_to_holder(reagent_node2, reagent_node2.maximum_volume)
+
+ if(HOSE_NEUTRAL)
+ switch(node2.flow_direction)
+ if(HOSE_INPUT) // We're neutral, they're input. Give them half of their volume.
+ reagent_node1.trans_to_holder(reagent_node2, reagent_node2.maximum_volume / 2)
+
+ if(HOSE_NEUTRAL) // We're neutral, they're neutral. Balance our values.
+ var/volume_difference_perc = (reagent_node1.total_volume / reagent_node1.maximum_volume) - (reagent_node2.total_volume / reagent_node2.maximum_volume)
+ var/volume_difference = 0
+
+ var/pulling = FALSE
+ if(volume_difference_perc > 0) // They are smaller, so they determine the transfer amount. Half of the difference will equalize.
+ volume_difference = reagent_node2.maximum_volume * volume_difference_perc / 2
+
+ else if(volume_difference_perc < 0) // We're smaller, so we determine the transfer amount. Half of the difference will equalize.
+ volume_difference_perc *= -1
+
+ pulling = TRUE
+
+ volume_difference = reagent_node1.maximum_volume * volume_difference_perc / 2
+
+ if(volume_difference)
+ if(pulling)
+ reagent_node2.trans_to_holder(reagent_node1, volume_difference)
+ else
+ reagent_node1.trans_to_holder(reagent_node2, volume_difference)
+
+ if(HOSE_OUTPUT)
+ reagent_node2.trans_to_holder(reagent_node1, reagent_node2.maximum_volume)
+
+ else
+ if(node1)
+ node1.disconnect()
+ node1 = null
+ if(node2)
+ node2.disconnect()
+ node2 = null
+
+ STOP_PROCESSING(SSobj, src)
+ GLOB.hoses -= src
+ qdel(src)
diff --git a/code/modules/reagents/hoses/hose_connector.dm b/code/modules/reagents/hoses/hose_connector.dm
new file mode 100644
index 0000000000..4c442cc712
--- /dev/null
+++ b/code/modules/reagents/hoses/hose_connector.dm
@@ -0,0 +1,92 @@
+
+/obj/item/stack/hose
+ name = "plastic tubing"
+ singular_name = "plastic tube"
+ desc = "A non-reusable plastic tube for moving reagents to and fro. It looks flimsy."
+
+ description_info = "This tubing may be used to join two hose sockets, if able.
\
+ Clicking on an object with a connector, such as a water tank, will display a list of possible sockets.
\
+ Neutral can link to all socket types, and Input/Output sockets can link to all but their own type.
\
+ This hose does not stretch. The maximum distance you can move two objects from eachother\
+ without snapping the tube is determined by distance upon connection."
+
+ icon = 'icons/obj/machines/reagent.dmi'
+ icon_state = "hose"
+ origin_tech = list(TECH_MATERIAL = 2, TECH_ENGINEERING = 1)
+ amount = 1
+ w_class = ITEMSIZE_SMALL
+ no_variants = TRUE
+
+ var/obj/item/hose_connector/remembered = null
+
+/obj/item/stack/hose/Destroy()
+ remembered = null
+ ..()
+
+/obj/item/stack/hose/CtrlClick(mob/user)
+ if(remembered)
+ to_chat(user, "You wind \the [src] back up.")
+ remembered = null
+ return
+
+/obj/item/stack/hose/afterattack(var/atom/target, var/mob/living/user, proximity, params)
+ if(!proximity)
+ return
+
+ var/list/available_sockets = list()
+
+ for(var/obj/item/hose_connector/HC in target.contents)
+ if(!HC.my_hose)
+ if(remembered)
+ if(HC.flow_direction == HOSE_NEUTRAL || HC.flow_direction != remembered.flow_direction)
+ available_sockets |= HC
+
+ else
+ available_sockets |= HC
+
+ if(LAZYLEN(available_sockets))
+ if(available_sockets.len == 1)
+ var/obj/item/hose_connector/AC = available_sockets[1]
+ if(remembered && remembered.valid_connection(AC))
+ var/distancetonode = get_dist(remembered,AC)
+ if(distancetonode > world.view)
+ to_chat(user, "\The [src] would probably burst if it were this long.")
+ else if(distancetonode <= amount)
+ to_chat(user, "You join \the [remembered] to \the [AC]")
+ remembered.setup_hoses(AC)
+ use(distancetonode)
+ remembered = null
+ else
+ to_chat(user, "You do not have enough tubing to connect the sockets.")
+
+ else
+ remembered = AC
+ to_chat(user, "You connect one end of tubing to \the [AC].")
+
+ else
+ var/choice = input("Select a target hose connector.", "Socket Selection", null) as null|anything in available_sockets
+
+ if(choice)
+ var/obj/item/hose_connector/CC = choice
+ if(remembered)
+ if(remembered.valid_connection(CC))
+ var/distancetonode = get_dist(remembered, CC)
+ if(distancetonode > world.view)
+ to_chat(user, "\The [src] would probably burst if it were this long.")
+ else if(distancetonode <= amount)
+ to_chat(user, "You join \the [remembered] to \the [CC]")
+ remembered.setup_hoses(CC)
+ use(distancetonode)
+ remembered = null
+
+ else
+ to_chat(user, "You do not have enough tubing to connect the sockets.")
+
+ else
+ remembered = CC
+ to_chat(user, "You connect one end of tubing to \the [CC].")
+
+ return
+
+ else
+ ..()
diff --git a/code/modules/reagents/reagent_containers/spray.dm b/code/modules/reagents/reagent_containers/spray.dm
index 2a89067e8a..a83c961546 100644
--- a/code/modules/reagents/reagent_containers/spray.dm
+++ b/code/modules/reagents/reagent_containers/spray.dm
@@ -204,4 +204,97 @@
/obj/item/weapon/reagent_containers/spray/plantbgone/Initialize()
. = ..()
- reagents.add_reagent("plantbgone", 100)
\ No newline at end of file
+ reagents.add_reagent("plantbgone", 100)
+
+/obj/item/weapon/reagent_containers/spray/chemsprayer/hosed
+ name = "hose nozzle"
+ desc = "A heavy spray nozzle that must be attached to a hose."
+ icon = 'icons/obj/janitor.dmi'
+ icon_state = "cleaner-industrial"
+ item_state = "cleaner"
+ center_of_mass = list("x" = 16,"y" = 10)
+
+ possible_transfer_amounts = list(5,10,20)
+
+ var/heavy_spray = FALSE
+ var/spray_particles = 3
+
+ var/icon/hose_overlay
+
+ var/obj/item/hose_connector/input/active/InputSocket
+
+/obj/item/weapon/reagent_containers/spray/chemsprayer/hosed/Initialize()
+ ..()
+
+ InputSocket = new(src)
+
+/obj/item/weapon/reagent_containers/spray/chemsprayer/hosed/update_icon()
+ ..()
+
+ overlays.Cut()
+
+ if(!hose_overlay)
+ hose_overlay = new icon(icon, "[icon_state]+hose")
+
+ if(InputSocket.get_pairing())
+ add_overlay(hose_overlay)
+
+/obj/item/weapon/reagent_containers/spray/chemsprayer/hosed/AltClick(mob/living/carbon/user)
+ if(++spray_particles > 3) spray_particles = 1
+
+ to_chat(user, "You turn the dial on \the [src] to [spray_particles].")
+ return
+
+/obj/item/weapon/reagent_containers/spray/chemsprayer/hosed/CtrlClick(var/mob/user)
+ if(loc != get_turf(src))
+ heavy_spray = !heavy_spray
+ else
+ . = ..()
+
+/obj/item/weapon/reagent_containers/spray/chemsprayer/hosed/Spray_at(atom/A as mob|obj)
+ update_icon()
+
+ var/direction = get_dir(src, A)
+ var/turf/T = get_turf(A)
+ var/turf/T1 = get_step(T,turn(direction, 90))
+ var/turf/T2 = get_step(T,turn(direction, -90))
+ var/list/the_targets = list(T, T1, T2)
+
+ if(src.reagents.total_volume < 1)
+ to_chat(usr, "\The [src] is empty.")
+ return
+
+ if(!heavy_spray)
+ for(var/a = 1 to 3)
+ spawn(0)
+ if(reagents.total_volume < 1) break
+ playsound(src, 'sound/effects/spray2.ogg', 50, 1, -6)
+ var/obj/effect/effect/water/chempuff/D = new/obj/effect/effect/water/chempuff(get_turf(src))
+ var/turf/my_target = the_targets[a]
+ D.create_reagents(amount_per_transfer_from_this)
+ if(!src)
+ return
+ reagents.trans_to_obj(D, amount_per_transfer_from_this)
+ D.set_color()
+ D.set_up(my_target, rand(6, 8), 2)
+ return
+
+ else
+ playsound(src, 'sound/effects/extinguish.ogg', 75, 1, -3)
+
+ for(var/a = 1 to spray_particles)
+ spawn(0)
+ if(!src || !reagents.total_volume) return
+
+ var/obj/effect/effect/water/W = new /obj/effect/effect/water(get_turf(src))
+ var/turf/my_target
+ if(a <= the_targets.len)
+ my_target = the_targets[a]
+ else
+ my_target = pick(the_targets)
+ W.create_reagents(amount_per_transfer_from_this)
+ reagents.trans_to_obj(W, amount_per_transfer_from_this)
+ W.set_color()
+ W.set_up(my_target)
+
+ return
diff --git a/code/modules/reagents/reagent_dispenser.dm b/code/modules/reagents/reagent_dispenser.dm
index 129af30fe9..ae6ecf8505 100644
--- a/code/modules/reagents/reagent_dispenser.dm
+++ b/code/modules/reagents/reagent_dispenser.dm
@@ -10,18 +10,33 @@
anchored = 0
pressure_resistance = 2*ONE_ATMOSPHERE
+ var/obj/item/hose_connector/input/active/InputSocket
+ var/obj/item/hose_connector/output/active/OutputSocket
+
var/amount_per_transfer_from_this = 10
var/possible_transfer_amounts = list(10,25,50,100)
- attackby(obj/item/weapon/W as obj, mob/user as mob)
+/obj/structure/reagent_dispensers/attackby(obj/item/weapon/W as obj, mob/user as mob)
return
+/obj/structure/reagent_dispensers/Destroy()
+ QDEL_NULL(InputSocket)
+ QDEL_NULL(OutputSocket)
+
+ ..()
+
/obj/structure/reagent_dispensers/Initialize()
var/datum/reagents/R = new/datum/reagents(5000)
reagents = R
R.my_atom = src
if (!possible_transfer_amounts)
src.verbs -= /obj/structure/reagent_dispensers/verb/set_APTFT
+
+ InputSocket = new(src)
+ InputSocket.carrier = src
+ OutputSocket = new(src)
+ OutputSocket.carrier = src
+
. = ..()
/obj/structure/reagent_dispensers/examine(mob/user)
diff --git a/html/changelogs/mechoid - reagenthoses.yml b/html/changelogs/mechoid - reagenthoses.yml
new file mode 100644
index 0000000000..6cf9cfe9a9
--- /dev/null
+++ b/html/changelogs/mechoid - reagenthoses.yml
@@ -0,0 +1,37 @@
+################################
+# Example Changelog File
+#
+# Note: This file, and files beginning with ".", and files that don't end in ".yml" will not be read. If you change this file, you will look really dumb.
+#
+# Your changelog will be merged with a master changelog. (New stuff added only, and only on the date entry for the day it was merged.)
+# When it is, any changes listed below will disappear.
+#
+# Valid Prefixes:
+# bugfix
+# wip (For works in progress)
+# tweak
+# soundadd
+# sounddel
+# rscadd (general adding of nice things)
+# rscdel (general deleting of nice things)
+# imageadd
+# imagedel
+# maptweak
+# spellcheck (typo fixes)
+# experiment
+#################################
+
+# Your name.
+author: Mechoid
+
+# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again.
+delete-after: True
+
+# Any changes you've made. See valid prefix list above.
+# INDENT WITH TWO SPACES. NOT TABS. SPACES.
+# SCREW THIS UP AND IT WON'T WORK.
+# Also, all entries are changed into a single [] after a master changelog generation. Just remove the brackets when you add new entries.
+# Please surround your changes in double quotes ("), as certain characters otherwise screws up compiling. The quotes will not show up in the changelog.
+changes:
+ - rscadd: "Reagent pumps added. You can refill water tanks planetside!"
+ - rscadd: "Reagent hoses added. Only used player-side by tanks, pumps, and spray nozzles."
diff --git a/icons/effects/beam.dmi b/icons/effects/beam.dmi
index 4115b01ef2..4d0710885f 100644
Binary files a/icons/effects/beam.dmi and b/icons/effects/beam.dmi differ
diff --git a/icons/obj/janitor.dmi b/icons/obj/janitor.dmi
index 607fef4bc7..47974032e0 100644
Binary files a/icons/obj/janitor.dmi and b/icons/obj/janitor.dmi differ
diff --git a/icons/obj/machines/reagent.dmi b/icons/obj/machines/reagent.dmi
index d11f34270d..7495451380 100644
Binary files a/icons/obj/machines/reagent.dmi and b/icons/obj/machines/reagent.dmi differ
diff --git a/polaris.dme b/polaris.dme
index 1d4cfa1bbd..98a7ac077c 100644
--- a/polaris.dme
+++ b/polaris.dme
@@ -38,6 +38,7 @@
#include "code\__defines\flags.dm"
#include "code\__defines\gamemode.dm"
#include "code\__defines\holomap.dm"
+#include "code\__defines\hoses.dm"
#include "code\__defines\integrated_circuits.dm"
#include "code\__defines\inventory_sizes.dm"
#include "code\__defines\is_helpers.dm"
@@ -800,6 +801,7 @@
#include "code\game\machinery\pipe\pipe_dispenser.dm"
#include "code\game\machinery\pipe\pipe_recipes.dm"
#include "code\game\machinery\pipe\pipelayer.dm"
+#include "code\game\machinery\reagents\pump.dm"
#include "code\game\machinery\telecomms\broadcaster.dm"
#include "code\game\machinery\telecomms\logbrowser.dm"
#include "code\game\machinery\telecomms\machine_interactions.dm"
@@ -1085,6 +1087,7 @@
#include "code\game\objects\items\weapons\circuitboards\machinery\biogenerator.dm"
#include "code\game\objects\items\weapons\circuitboards\machinery\cloning.dm"
#include "code\game\objects\items\weapons\circuitboards\machinery\engineering.dm"
+#include "code\game\objects\items\weapons\circuitboards\machinery\fluidpump.dm"
#include "code\game\objects\items\weapons\circuitboards\machinery\jukebox.dm"
#include "code\game\objects\items\weapons\circuitboards\machinery\kitchen_appliances.dm"
#include "code\game\objects\items\weapons\circuitboards\machinery\mech_recharger.dm"
@@ -2718,6 +2721,9 @@
#include "code\modules\reagents\dispenser\supply.dm"
#include "code\modules\reagents\distilling\Distilling-Recipes.dm"
#include "code\modules\reagents\distilling\distilling.dm"
+#include "code\modules\reagents\hoses\connector.dm"
+#include "code\modules\reagents\hoses\hose.dm"
+#include "code\modules\reagents\hoses\hose_connector.dm"
#include "code\modules\reagents\reagent_containers\blood_pack.dm"
#include "code\modules\reagents\reagent_containers\borghydro.dm"
#include "code\modules\reagents\reagent_containers\dropper.dm"