From 0f1aa3c0cad76c049c0b67fc4d3cd54b8dfbd7a3 Mon Sep 17 00:00:00 2001 From: lolman360 Date: Wed, 22 Jul 2020 10:49:56 +1000 Subject: [PATCH] 'lolman? did you test your code?' no i did not ms. kevinz --- code/__DEFINES/dcs/signals.dm | 1 + code/__DEFINES/plumbing.dm | 9 + code/__DEFINES/reagents.dm | 4 + code/controllers/subsystem/fluid.dm | 5 + code/datums/components/plumbing/_plumbing.dm | 215 ++++++++++ .../plumbing/chemical_acclimator.dm | 21 + code/datums/components/plumbing/filter.dm | 59 +++ .../components/plumbing/reaction_chamber.dm | 44 +++ code/datums/components/plumbing/splitter.dm | 45 +++ code/datums/ductnet.dm | 60 +++ code/game/machinery/_machinery.dm | 2 + code/game/objects/items/RCD.dm | 75 ++++ code/game/objects/items/RPD.dm | 69 +++- .../items/stacks/sheets/sheet_types.dm | 1 + code/game/objects/objs.dm | 3 + code/game/objects/structures/geyser.dm | 74 ++++ code/modules/asset_cache/asset_list_items.dm | 3 +- code/modules/plumbing/ducts.dm | 371 ++++++++++++++++++ .../plumbing/plumbers/_plumb_machinery.dm | 97 +++++ code/modules/plumbing/plumbers/acclimator.dm | 105 +++++ code/modules/plumbing/plumbers/bottler.dm | 79 ++++ code/modules/plumbing/plumbers/destroyer.dm | 21 + code/modules/plumbing/plumbers/fermenter.dm | 52 +++ code/modules/plumbing/plumbers/filter.dm | 74 ++++ .../plumbing/plumbers/grinder_chemical.dm | 58 +++ code/modules/plumbing/plumbers/pill_press.dm | 107 +++++ code/modules/plumbing/plumbers/pumps.dm | 83 ++++ .../plumbing/plumbers/reaction_chamber.dm | 63 +++ code/modules/plumbing/plumbers/splitters.dm | 50 +++ code/modules/plumbing/plumbers/synthesizer.dm | 111 ++++++ code/modules/reagents/chemistry/holder.dm | 6 + code/modules/reagents/chemistry/reagents.dm | 2 +- code/modules/reagents/reagent_dispenser.dm | 37 +- .../research/designs/medical_designs.dm | 155 ++++++++ .../research/techweb/nodes/medical_nodes.dm | 17 + icons/obj/watercloset.dmi | Bin 23715 -> 16012 bytes 36 files changed, 2171 insertions(+), 7 deletions(-) create mode 100644 code/__DEFINES/plumbing.dm create mode 100644 code/controllers/subsystem/fluid.dm create mode 100644 code/datums/components/plumbing/_plumbing.dm create mode 100644 code/datums/components/plumbing/chemical_acclimator.dm create mode 100644 code/datums/components/plumbing/filter.dm create mode 100644 code/datums/components/plumbing/reaction_chamber.dm create mode 100644 code/datums/components/plumbing/splitter.dm create mode 100644 code/datums/ductnet.dm create mode 100644 code/game/objects/structures/geyser.dm create mode 100644 code/modules/plumbing/ducts.dm create mode 100644 code/modules/plumbing/plumbers/_plumb_machinery.dm create mode 100644 code/modules/plumbing/plumbers/acclimator.dm create mode 100644 code/modules/plumbing/plumbers/bottler.dm create mode 100644 code/modules/plumbing/plumbers/destroyer.dm create mode 100644 code/modules/plumbing/plumbers/fermenter.dm create mode 100644 code/modules/plumbing/plumbers/filter.dm create mode 100644 code/modules/plumbing/plumbers/grinder_chemical.dm create mode 100644 code/modules/plumbing/plumbers/pill_press.dm create mode 100644 code/modules/plumbing/plumbers/pumps.dm create mode 100644 code/modules/plumbing/plumbers/reaction_chamber.dm create mode 100644 code/modules/plumbing/plumbers/splitters.dm create mode 100644 code/modules/plumbing/plumbers/synthesizer.dm diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm index 1882effe38..cfd1906e82 100644 --- a/code/__DEFINES/dcs/signals.dm +++ b/code/__DEFINES/dcs/signals.dm @@ -270,6 +270,7 @@ #define COMSIG_OBJ_DECONSTRUCT "obj_deconstruct" //from base of obj/deconstruct(): (disassembled) #define COMSIG_OBJ_BREAK "obj_break" //from base of /obj/obj_break(): (damage_flag) #define COMSIG_OBJ_SETANCHORED "obj_setanchored" //called in /obj/structure/setAnchored(): (value) +#define COMSIG_OBJ_DEFAULT_UNFASTEN_WRENCH "obj_default_unfasten_wrench" //called exclusively in plumbing, for now #define COMSIG_OBJ_ATTACK_GENERIC "obj_attack_generic" //from base of atom/animal_attack(): (/mob/user) #define COMPONENT_STOP_GENERIC_ATTACK 1 diff --git a/code/__DEFINES/plumbing.dm b/code/__DEFINES/plumbing.dm new file mode 100644 index 0000000000..5ea7c46b42 --- /dev/null +++ b/code/__DEFINES/plumbing.dm @@ -0,0 +1,9 @@ +#define FIRST_DUCT_LAYER 1 +#define SECOND_DUCT_LAYER 2 +#define THIRD_DUCT_LAYER 4 +#define FOURTH_DUCT_LAYER 8 +#define FIFTH_DUCT_LAYER 16 + +#define DUCT_LAYER_DEFAULT THIRD_DUCT_LAYER + +#define MACHINE_REAGENT_TRANSFER 10 diff --git a/code/__DEFINES/reagents.dm b/code/__DEFINES/reagents.dm index f4beef7ee8..44e97ef345 100644 --- a/code/__DEFINES/reagents.dm +++ b/code/__DEFINES/reagents.dm @@ -57,6 +57,10 @@ #define ADD_REAGENT 2 // reagent added #define REM_REAGENT 3 // reagent removed (may still exist) + +#define PILL_STYLE_COUNT 22 //Update this if you add more pill icons or you die (literally, we'll toss a nuke at whever your ip turns up) +#define RANDOM_PILL_STYLE 22 //Dont change this one though + #define THRESHOLD_UNHUSK 50 // health threshold for synthflesh/rezadone to unhusk someone //reagent bitflags, used for altering how they works diff --git a/code/controllers/subsystem/fluid.dm b/code/controllers/subsystem/fluid.dm new file mode 100644 index 0000000000..c4fa13d693 --- /dev/null +++ b/code/controllers/subsystem/fluid.dm @@ -0,0 +1,5 @@ +PROCESSING_SUBSYSTEM_DEF(fluids) + name = "Fluids" + wait = 20 + stat_tag = "FD" //its actually Fluid Ducts + flags = SS_NO_INIT | SS_TICKER diff --git a/code/datums/components/plumbing/_plumbing.dm b/code/datums/components/plumbing/_plumbing.dm new file mode 100644 index 0000000000..22b8bafde5 --- /dev/null +++ b/code/datums/components/plumbing/_plumbing.dm @@ -0,0 +1,215 @@ +/datum/component/plumbing + ///Index with "1" = /datum/ductnet/theductpointingnorth etc. "1" being the num2text from NORTH define + var/list/datum/ductnet/ducts = list() + ///shortcut to our parents' reagent holder + var/datum/reagents/reagents + ///TRUE if we wanna add proper pipe outless under our parent object. this is pretty good if i may so so myself + var/use_overlays = TRUE + ///We can't just cut all of the parents' overlays, so we'll track them here + var/list/image/ducterlays + ///directions in wich we act as a supplier + var/supply_connects + ///direction in wich we act as a demander + var/demand_connects + ///FALSE to pretty much just not exist in the plumbing world so we can be moved, TRUE to go plumbo mode + var/active = FALSE + ///if TRUE connects will spin with the parent object visually and codually, so you can have it work in any direction. FALSE if you want it to be static + var/turn_connects = TRUE + +/datum/component/plumbing/Initialize(start=TRUE, _turn_connects=TRUE) //turn_connects for wheter or not we spin with the object to change our pipes + if(parent && !ismovableatom(parent)) + return COMPONENT_INCOMPATIBLE + var/atom/movable/AM = parent + if(!AM.reagents) + return COMPONENT_INCOMPATIBLE + reagents = AM.reagents + turn_connects = _turn_connects + + RegisterSignal(parent, list(COMSIG_MOVABLE_MOVED,COMSIG_PARENT_PREQDELETED), .proc/disable) + RegisterSignal(parent, list(COMSIG_OBJ_DEFAULT_UNFASTEN_WRENCH), .proc/toggle_active) + + if(start) + enable() + + if(use_overlays) + create_overlays() + +/datum/component/plumbing/process() + if(!demand_connects || !reagents) + STOP_PROCESSING(SSfluids, src) + return + if(reagents.total_volume < reagents.maximum_volume) + for(var/D in GLOB.cardinals) + if(D & demand_connects) + send_request(D) +///Can we be added to the ductnet? +/datum/component/plumbing/proc/can_add(datum/ductnet/D, dir) + if(!active) + return + if(!dir || !D) + return FALSE + if(num2text(dir) in ducts) + return FALSE + + return TRUE +///called from in process(). only calls process_request(), but can be overwritten for children with special behaviour +/datum/component/plumbing/proc/send_request(dir) + process_request(amount = MACHINE_REAGENT_TRANSFER, reagent = null, dir = dir) +///check who can give us what we want, and how many each of them will give us +/datum/component/plumbing/proc/process_request(amount, reagent, dir) + var/list/valid_suppliers = list() + var/datum/ductnet/net + if(!ducts.Find(num2text(dir))) + return + net = ducts[num2text(dir)] + for(var/A in net.suppliers) + var/datum/component/plumbing/supplier = A + if(supplier.can_give(amount, reagent, net)) + valid_suppliers += supplier + for(var/A in valid_suppliers) + var/datum/component/plumbing/give = A + give.transfer_to(src, amount / valid_suppliers.len, reagent, net) +///returns TRUE when they can give the specified amount and reagent. called by process request +/datum/component/plumbing/proc/can_give(amount, reagent, datum/ductnet/net) + if(amount <= 0) + return + + if(reagent) //only asked for one type of reagent + for(var/A in reagents.reagent_list) + var/datum/reagent/R = A + if(R.id == reagent) + return TRUE + else if(reagents.total_volume > 0) //take whatever + return TRUE +///this is where the reagent is actually transferred and is thus the finish point of our process() +/datum/component/plumbing/proc/transfer_to(datum/component/plumbing/target, amount, reagent, datum/ductnet/net) + if(!reagents || !target || !target.reagents) + return FALSE + if(reagent) + reagents.trans_id_to(target.parent, reagent, amount) + else + reagents.trans_to(target.parent, amount) +///We create our luxurious piping overlays/underlays, to indicate where we do what. only called once if use_overlays = TRUE in Initialize() +/datum/component/plumbing/proc/create_overlays() + var/atom/movable/AM = parent + for(var/image/I in ducterlays) + AM.overlays.Remove(I) + qdel(I) + ducterlays = list() + for(var/D in GLOB.cardinals) + var/color + var/direction + if(D & demand_connects) + color = "red" //red because red is mean and it takes + else if(D & supply_connects) + color = "blue" //blue is nice and gives + else + continue + var/image/I + if(turn_connects) + switch(D) + if(NORTH) + direction = "north" + if(SOUTH) + direction = "south" + if(EAST) + direction = "east" + if(WEST) + direction = "west" + I = image('icons/obj/plumbing/plumbers.dmi', "[direction]-[color]", layer = AM.layer - 1) + else + I = image('icons/obj/plumbing/plumbers.dmi', color, layer = AM.layer - 1) //color is not color as in the var, it's just the name + I.dir = D + AM.add_overlay(I) + ducterlays += I +///we stop acting like a plumbing thing and disconnect if we are, so we can safely be moved and stuff +/datum/component/plumbing/proc/disable() + if(!active) + return + STOP_PROCESSING(SSfluids, src) + for(var/A in ducts) + var/datum/ductnet/D = ducts[A] + D.remove_plumber(src) + active = FALSE + for(var/D in GLOB.cardinals) + if(D & (demand_connects | supply_connects)) + for(var/obj/machinery/duct/duct in get_step(parent, D)) + duct.attempt_connect() + +///settle wherever we are, and start behaving like a piece of plumbing +/datum/component/plumbing/proc/enable() + if(active) + return + update_dir() + active = TRUE + var/atom/movable/AM = parent + for(var/obj/machinery/duct/D in AM.loc) //Destroy any ducts under us. Ducts also self destruct if placed under a plumbing machine. machines disable when they get moved + if(D.anchored) //that should cover everything + D.disconnect_duct() + + if(demand_connects) + START_PROCESSING(SSfluids, src) + + for(var/D in GLOB.cardinals) + if(D & (demand_connects | supply_connects)) + for(var/atom/movable/A in get_step(parent, D)) + if(istype(A, /obj/machinery/duct)) + var/obj/machinery/duct/duct = A + duct.attempt_connect() + else + var/datum/component/plumbing/P = A.GetComponent(/datum/component/plumbing) + if(P) + direct_connect(P, D) + +/// Toggle our machinery on or off. This is called by a hook from default_unfasten_wrench with anchored as only param, so we dont have to copypaste this on every object that can move +/datum/component/plumbing/proc/toggle_active(obj/O, new_state) + if(new_state) + enable() + else + disable() +/** We update our connects only when we settle down by taking our current and original direction to find our new connects +* If someone wants it to fucking spin while connected to something go actually knock yourself out +*/ +/datum/component/plumbing/proc/update_dir() + if(!turn_connects) + return + var/atom/movable/AM = parent + var/new_demand_connects + var/new_supply_connects + var/new_dir = AM.dir + var/angle = 180 - dir2angle(new_dir) + if(new_dir == SOUTH) + demand_connects = initial(demand_connects) + supply_connects = initial(supply_connects) + else + for(var/D in GLOB.cardinals) + if(D & initial(demand_connects)) + new_demand_connects += turn(D, angle) + if(D & initial(supply_connects)) + new_supply_connects += turn(D, angle) + demand_connects = new_demand_connects + supply_connects = new_supply_connects +///Give the direction of a pipe, and it'll return wich direction it originally was when it's object pointed SOUTH +/datum/component/plumbing/proc/get_original_direction(dir) + var/atom/movable/AM = parent + return turn(dir, dir2angle(AM.dir) - 180) +//special case in-case we want to connect directly with another machine without a duct +/datum/component/plumbing/proc/direct_connect(datum/component/plumbing/P, dir) + if(!P.active) + return + var/opposite_dir = turn(dir, 180) + if(P.demand_connects & opposite_dir && supply_connects & dir || P.supply_connects & opposite_dir && demand_connects & dir) //make sure we arent connecting two supplies or demands + var/datum/ductnet/net = new() + net.add_plumber(src, dir) + net.add_plumber(P, opposite_dir) + +///has one pipe input that only takes, example is manual output pipe +/datum/component/plumbing/simple_demand + demand_connects = NORTH +///has one pipe output that only supplies. example is liquid pump and manual input pipe +/datum/component/plumbing/simple_supply + supply_connects = NORTH +///input and output, like a holding tank +/datum/component/plumbing/tank + demand_connects = WEST + supply_connects = EAST diff --git a/code/datums/components/plumbing/chemical_acclimator.dm b/code/datums/components/plumbing/chemical_acclimator.dm new file mode 100644 index 0000000000..44a1396c6d --- /dev/null +++ b/code/datums/components/plumbing/chemical_acclimator.dm @@ -0,0 +1,21 @@ +v/datum/component/plumbing/acclimator + demand_connects = WEST + supply_connects = EAST + var/obj/machinery/plumbing/acclimator/AC + +/datum/component/plumbing/acclimator/Initialize(start=TRUE, _turn_connects=TRUE) + . = ..() + if(!istype(parent, /obj/machinery/plumbing/acclimator)) + return COMPONENT_INCOMPATIBLE + AC = parent + +/datum/component/plumbing/acclimator/can_give(amount, reagent) + . = ..() + if(. && AC.emptying) + return TRUE + return FALSE +///We're overriding process and not send_request, because all process does is do the requests, so we might aswell cut out the middle man and save some code from running +/datum/component/plumbing/acclimator/process() + if(AC.emptying) + return + . = ..() diff --git a/code/datums/components/plumbing/filter.dm b/code/datums/components/plumbing/filter.dm new file mode 100644 index 0000000000..cfbfb641da --- /dev/null +++ b/code/datums/components/plumbing/filter.dm @@ -0,0 +1,59 @@ +///The magical plumbing component used by the chemical filters. The different supply connects behave differently depending on the filters set on the chemical filter +/datum/component/plumbing/filter + demand_connects = NORTH + supply_connects = SOUTH | EAST | WEST //SOUTH is straight, EAST is left and WEST is right. We look from the perspective of the insert + +/datum/component/plumbing/filter/Initialize() + . = ..() + if(!istype(parent, /obj/machinery/plumbing/filter)) + return COMPONENT_INCOMPATIBLE + +/datum/component/plumbing/filter/can_give(amount, reagent, datum/ductnet/net) + . = ..() + if(.) + var/direction + for(var/A in ducts) + if(ducts[A] == net) + direction = get_original_direction(text2num(A)) //we need it relative to the direction, so filters don't change when we turn the filter + break + if(!direction) + return FALSE + if(reagent) + if(!can_give_in_direction(direction, reagent)) + return FALSE + +/datum/component/plumbing/filter/transfer_to(datum/component/plumbing/target, amount, reagent, datum/ductnet/net) + if(!reagents || !target || !target.reagents) + return FALSE + var/direction + for(var/A in ducts) + if(ducts[A] == net) + direction = get_original_direction(text2num(A)) + break + if(reagent) + reagents.trans_id_to(target.parent, reagent, amount) + else + for(var/A in reagents.reagent_list) + var/datum/reagent/R = A + if(!can_give_in_direction(direction, R.id)) + continue + var/new_amount + if(R.volume < amount) + new_amount = amount - R.volume + reagents.trans_id_to(target.parent, R.id, amount) + amount = new_amount + if(amount <= 0) + break +///We check if the direction and reagent are valid to give. Needed for filters since different outputs have different behaviours +/datum/component/plumbing/filter/proc/can_give_in_direction(dir, reagent) + var/obj/machinery/plumbing/filter/F = parent + switch(dir) + if(SOUTH) //straight + if(!F.left.Find(reagent) && !F.right.Find(reagent)) + return TRUE + if(WEST) //right + if(F.right.Find(reagent)) + return TRUE + if(EAST) //left + if(F.left.Find(reagent)) + return TRUE diff --git a/code/datums/components/plumbing/reaction_chamber.dm b/code/datums/components/plumbing/reaction_chamber.dm new file mode 100644 index 0000000000..d0a9416e34 --- /dev/null +++ b/code/datums/components/plumbing/reaction_chamber.dm @@ -0,0 +1,44 @@ +/datum/component/plumbing/reaction_chamber + demand_connects = WEST + supply_connects = EAST + +/datum/component/plumbing/reaction_chamber/Initialize(start=TRUE, _turn_connects=TRUE) + . = ..() + if(!istype(parent, /obj/machinery/plumbing/reaction_chamber)) + return COMPONENT_INCOMPATIBLE + +/datum/component/plumbing/reaction_chamber/can_give(amount, reagent) + . = ..() + var/obj/machinery/plumbing/reaction_chamber/RC = parent + if(!. || !RC.emptying) + return FALSE + +/datum/component/plumbing/reaction_chamber/send_request(dir) + var/obj/machinery/plumbing/reaction_chamber/RC = parent + if(RC.emptying || !LAZYLEN(RC.required_reagents)) + return + for(var/RTid in RC.required_reagents) + var/has_reagent = FALSE + for(var/A in reagents.reagent_list) + var/datum/reagent/RD = A + if(RTid == RD.id) + has_reagent = TRUE + if(RD.volume < RC.required_reagents[RTid]) + process_request(min(RC.required_reagents[RTid] - RD.volume, MACHINE_REAGENT_TRANSFER) , RTid, dir) + return + if(!has_reagent) + process_request(min(RC.required_reagents[RTid], MACHINE_REAGENT_TRANSFER), RTid, dir) + return + + RC.reagent_flags &= ~NO_REACT + reagents.handle_reactions() + Add when everything works: + if(reagents.fermiIsReacting) + return + RC.emptying = TRUE + +/datum/component/plumbing/reaction_chamber/can_give(amount, reagent, datum/ductnet/net) + . = ..() + var/obj/machinery/plumbing/reaction_chamber/RC = parent + if(!. || !RC.emptying) + return FALSE diff --git a/code/datums/components/plumbing/splitter.dm b/code/datums/components/plumbing/splitter.dm new file mode 100644 index 0000000000..7194e8803b --- /dev/null +++ b/code/datums/components/plumbing/splitter.dm @@ -0,0 +1,45 @@ +/datum/component/plumbing/splitter + demand_connects = NORTH + supply_connects = SOUTH | EAST + +/datum/component/plumbing/splitter/Initialize() + . = ..() + if(. && !istype(parent, /obj/machinery/plumbing/splitter)) + return FALSE + +/datum/component/plumbing/splitter/can_give(amount, reagent, datum/ductnet/net) + . = ..() + if(!.) + return + . = FALSE + var/direction + for(var/A in ducts) + if(ducts[A] == net) + direction = get_original_direction(text2num(A)) + break + var/obj/machinery/plumbing/splitter/S = parent + switch(direction) + if(SOUTH) + if(S.turn_straight && S.transfer_straight <= amount) + S.turn_straight = FALSE + return TRUE + if(EAST) + if(!S.turn_straight && S.transfer_side <= amount) + S.turn_straight = TRUE + return TRUE + +/datum/component/plumbing/splitter/transfer_to(datum/component/plumbing/target, amount, reagent, datum/ductnet/net) + var/direction + for(var/A in ducts) + if(ducts[A] == net) + direction = get_original_direction(text2num(A)) + break + var/obj/machinery/plumbing/splitter/S = parent + switch(direction) + if(SOUTH) + if(amount >= S.transfer_straight) + amount = S.transfer_straight + if(EAST) + if(amount >= S.transfer_side) + amount = S.transfer_side + . = ..() diff --git a/code/datums/ductnet.dm b/code/datums/ductnet.dm new file mode 100644 index 0000000000..f81e9e608c --- /dev/null +++ b/code/datums/ductnet.dm @@ -0,0 +1,60 @@ +/datum/ductnet + var/list/suppliers = list() + var/list/demanders = list() + var/list/obj/machinery/duct/ducts = list() + + var/capacity + +/datum/ductnet/proc/add_duct(obj/machinery/duct/D) + if(!D || D in ducts) + return + ducts += D + D.duct = src + +/datum/ductnet/proc/remove_duct(obj/machinery/duct/ducting) + destroy_network(FALSE) + for(var/A in ducting.neighbours) + var/obj/machinery/duct/D = A + D.attempt_connect() //we destroyed the network, so now we tell the disconnected ducts neighbours they can start making a new ductnet + qdel(src) + +/datum/ductnet/proc/add_plumber(datum/component/plumbing/P, dir) + if(!P.can_add(src, dir)) + return + P.ducts[num2text(dir)] = src + if(dir & P.supply_connects) + suppliers += P + else if(dir & P.demand_connects) + demanders += P + +/datum/ductnet/proc/remove_plumber(datum/component/plumbing/P) + suppliers.Remove(P) //we're probably only in one of these, but Remove() is inherently sane so this is fine + demanders.Remove(P) + + for(var/dir in P.ducts) + if(P.ducts[dir] == src) + P.ducts -= dir + +/datum/ductnet/proc/assimilate(datum/ductnet/D) + ducts.Add(D.ducts) + suppliers.Add(D.suppliers) + demanders.Add(D.demanders) + for(var/A in D.suppliers + D.demanders) + var/datum/component/plumbing/P = A + for(var/s in P.ducts) + if(P.ducts[s] != D) + continue + P.ducts[s] = src //all your ducts are belong to us + for(var/A in D.ducts) + var/obj/machinery/duct/M = A + M.duct = src //forget your old master + qdel(D) + +/datum/ductnet/proc/destroy_network(delete=TRUE) + for(var/A in suppliers + demanders) + remove_plumber(A) + for(var/A in ducts) + var/obj/machinery/duct/D = A + D.duct = null + if(delete) //I don't want code to run with qdeleted objects because that can never be good, so keep this in-case the ductnet has some business left to attend to before commiting suicide + qdel(src) diff --git a/code/game/machinery/_machinery.dm b/code/game/machinery/_machinery.dm index 6ef13ac36f..2ed39a21a8 100644 --- a/code/game/machinery/_machinery.dm +++ b/code/game/machinery/_machinery.dm @@ -116,6 +116,7 @@ Class Procs: var/new_occupant_dir = SOUTH //The direction the occupant will be set to look at when entering the machine. var/speed_process = FALSE // Process as fast as possible? var/obj/item/circuitboard/circuit // Circuit to be created and inserted when the machinery is created + var/wire_compatible = FALSE // For storing and overriding ui id and dimensions var/tgui_id // ID of TGUI interface var/ui_style // ID of custom TGUI style (optional) @@ -432,6 +433,7 @@ Class Procs: to_chat(user, "You [anchored ? "un" : ""]secure [src].") setAnchored(!anchored) playsound(src, 'sound/items/deconstruct.ogg', 50, 1) + SEND_SIGNAL(src, COMSIG_OBJ_DEFAULT_UNFASTEN_WRENCH, anchored) return SUCCESSFUL_UNFASTEN return FAILED_UNFASTEN return CANT_UNFASTEN diff --git a/code/game/objects/items/RCD.dm b/code/game/objects/items/RCD.dm index 2fbb45173b..29a04237ea 100644 --- a/code/game/objects/items/RCD.dm +++ b/code/game/objects/items/RCD.dm @@ -856,6 +856,81 @@ RLD desc = "It contains the design for firelock, air alarm, fire alarm, apc circuits and crap power cells." upgrade = RCD_UPGRADE_SIMPLE_CIRCUITS +/obj/item/construction/plumbing + name = "Plumbing Constructor" + desc = "An expertly modified RCD outfitted to construct plumbing machinery. Reload with compressed matter cartridges." + icon_state = "arcd" + item_state = "oldrcd" + has_ammobar = FALSE + matter = 200 + max_matter = 200 + + ///type of the plumbing machine + var/blueprint = null + ///index, used in the attack self to get the type. stored here since it doesnt change + var/list/choices = list() + ///index, used in the attack self to get the type. stored here since it doesnt change + var/list/name_to_type = list() + /// + var/list/machinery_data = list("cost" = list(), "delay" = list()) + +/obj/item/construction/plumbing/attack_self(mob/user) + ..() + if(!choices.len) + for(var/A in subtypesof(/obj/machinery/plumbing)) + var/obj/machinery/plumbing/M = A + if(initial(M.rcd_constructable)) + choices += list(initial(M.name) = image(icon = initial(M.icon), icon_state = initial(M.icon_state))) + name_to_type[initial(M.name)] = M + machinery_data["cost"][A] = initial(M.rcd_cost) + machinery_data["delay"][A] = initial(M.rcd_delay) + + var/choice = show_radial_menu(user, src, choices, custom_check = CALLBACK(src, .proc/check_menu, user), require_near = TRUE, tooltips = TRUE) + if(!check_menu(user)) + return + + blueprint = name_to_type[choice] + playsound(src, 'sound/effects/pop.ogg', 50, FALSE) + to_chat(user, "You change [name]s blueprint to '[choice]'.") + +///pretty much rcd_create, but named differently to make myself feel less bad for copypasting from a sibling-type +/obj/item/construction/plumbing/proc/create_machine(atom/A, mob/user) + if(!machinery_data || !isopenturf(A)) + return FALSE + + if(checkResource(machinery_data["cost"][blueprint], user) && blueprint) + if(do_after(user, machinery_data["delay"][blueprint], target = A)) + if(checkResource(machinery_data["cost"][blueprint], user) && canPlace(A)) + useResource(machinery_data["cost"][blueprint], user) + activate() + playsound(src.loc, 'sound/machines/click.ogg', 50, TRUE) + new blueprint (A, FALSE, FALSE) + return TRUE + +/obj/item/construction/plumbing/proc/canPlace(turf/T) + if(!isopenturf(T)) + return FALSE + . = TRUE + for(var/obj/O in T.contents) + if(O.density) //let's not built ontop of dense stuff, like big machines and other obstacles, it kills my immershion + return FALSE + +/obj/item/construction/plumbing/afterattack(atom/A, mob/user, proximity) + . = ..() + if(!prox_check(proximity)) + return + if(istype(A, /obj/machinery/plumbing)) + var/obj/machinery/plumbing/P = A + if(P.anchored) + to_chat(user, "The [P.name] needs to be unanchored!") + return + if(do_after(user, 20, target = P)) + P.deconstruct() //Let's not substract matter + playsound(get_turf(src), 'sound/machines/click.ogg', 50, TRUE) //this is just such a great sound effect + else + create_machine(A, user) + + #undef GLOW_MODE #undef LIGHT_MODE #undef REMOVE_MODE diff --git a/code/game/objects/items/RPD.dm b/code/game/objects/items/RPD.dm index 93d73b2b5f..2c8acf5a28 100644 --- a/code/game/objects/items/RPD.dm +++ b/code/game/objects/items/RPD.dm @@ -6,6 +6,7 @@ RPD #define ATMOS_CATEGORY 0 #define DISPOSALS_CATEGORY 1 #define TRANSIT_CATEGORY 2 +#define PLUMBING_CATEGORY 3 #define BUILD_MODE (1<<0) #define WRENCH_MODE (1<<1) @@ -75,6 +76,13 @@ GLOBAL_LIST_INIT(transit_tube_recipes, list( ) )) +GLOBAL_LIST_INIT(fluid_duct_recipes, list( + "Fluid Ducts" = list( + new /datum/pipe_info/plumbing("Duct", /obj/machinery/duct, PIPE_ONEDIR), + new /datum/pipe_info/plumbing/multilayer("Duct Layer-Manifold",/obj/machinery/duct/multilayered, PIPE_STRAIGHT) + ) +)) + /datum/pipe_info var/name var/icon_state @@ -175,6 +183,15 @@ GLOBAL_LIST_INIT(transit_tube_recipes, list( if(dt == PIPE_UNARY_FLIPPABLE) icon_state = "[icon_state]_preview" +/datum/pipe_info/plumbing/New(label, obj/path, dt=PIPE_UNARY) + name = label + id = path + icon_state = initial(path.icon_state) + dirtype = dt + +/datum/pipe_info/plumbing/multilayer //exists as identifier so we can see the difference between multi_layer and just ducts properly later on + + /obj/item/pipe_dispenser name = "Rapid Piping Device (RPD)" desc = "A device used to rapidly pipe things." @@ -200,15 +217,19 @@ GLOBAL_LIST_INIT(transit_tube_recipes, list( var/atmos_build_speed = 5 //deciseconds (500ms) var/disposal_build_speed = 5 var/transit_build_speed = 5 + var/plumbing_build_speed = 5 var/destroy_speed = 5 var/paint_speed = 5 var/category = ATMOS_CATEGORY var/piping_layer = PIPING_LAYER_DEFAULT + var/ducting_layer = DUCT_LAYER_DEFAULT var/datum/pipe_info/recipe var/static/datum/pipe_info/first_atmos var/static/datum/pipe_info/first_disposal var/static/datum/pipe_info/first_transit var/mode = BUILD_MODE | DESTROY_MODE | WRENCH_MODE + var/static/datum/pipe_info/first_plumbing + var/locked = FALSE //wheter we can change categories. Useful for the plumber /obj/item/pipe_dispenser/New() . = ..() @@ -256,11 +277,13 @@ GLOBAL_LIST_INIT(transit_tube_recipes, list( var/list/data = list( "category" = category, "piping_layer" = piping_layer, + "ducting_layer" = ducting_layer, "preview_rows" = recipe.get_preview(p_dir), "categories" = list(), "selected_color" = paint_color, "paint_colors" = GLOB.pipe_paint_colors, "mode" = mode + "locked" = locked ) var/list/recipes @@ -271,6 +294,8 @@ GLOBAL_LIST_INIT(transit_tube_recipes, list( recipes = GLOB.disposal_pipe_recipes if(TRANSIT_CATEGORY) recipes = GLOB.transit_tube_recipes + if(PLUMBING_CATEGORY) + recipes = GLOB.fluid_duct_recipes for(var/c in recipes) var/list/cat = recipes[c] var/list/r = list() @@ -299,6 +324,8 @@ GLOBAL_LIST_INIT(transit_tube_recipes, list( recipe = first_atmos if(TRANSIT_CATEGORY) recipe = first_transit + if(PLUMBING_CATEGORY) + recipe = first_plumbing p_dir = NORTH playeffect = FALSE if("piping_layer") @@ -469,16 +496,56 @@ GLOBAL_LIST_INIT(transit_tube_recipes, list( if(mode & WRENCH_MODE) tube.wrench_act(user, src) return - + if(PLUMBING_CATEGORY) //Plumbing. + if(!can_make_pipe) + return ..() + A = get_turf(A) + if(isclosedturf(A)) + to_chat(user, "[src]'s error light flickers; there's something in the way!") + return + to_chat(user, "You start building a fluid duct...") + playsound(get_turf(src), 'sound/machines/click.ogg', 50, 1) + if(do_after(user, plumbing_build_speed, target = A)) + var/obj/machinery/duct/D + if(recipe.type == /datum/pipe_info/plumbing/multilayer) + var/temp_connects = NORTH + SOUTH + if(queued_p_dir == EAST) + temp_connects = EAST + WEST + D = new queued_p_type (A, TRUE, GLOB.pipe_paint_colors[paint_color], ducting_layer, temp_connects) + else + D = new queued_p_type (A, TRUE, GLOB.pipe_paint_colors[paint_color], ducting_layer) + D.add_fingerprint(usr) + if(mode & WRENCH_MODE) + D.wrench_act(user, src) else return ..() /obj/item/pipe_dispenser/proc/activate() playsound(get_turf(src), 'sound/items/deconstruct.ogg', 50, 1) +/obj/item/pipe_dispenser/plumbing + name = "Plumberinator" + desc = "A crude device to rapidly plumb things." + icon_state = "plumberer" + category = PLUMBING_CATEGORY + locked = TRUE + +/obj/item/pipe_dispenser/plumbing/Initialize() + . = ..() + spark_system = new + spark_system.set_up(5, 0, src) + spark_system.attach(src) + if(!first_plumbing) + first_plumbing = GLOB.fluid_duct_recipes[GLOB.fluid_duct_recipes[1]][1] + + recipe = first_plumbing + + + #undef ATMOS_CATEGORY #undef DISPOSALS_CATEGORY #undef TRANSIT_CATEGORY +#undef PLUMBING_CATEGORY #undef BUILD_MODE #undef DESTROY_MODE diff --git a/code/game/objects/items/stacks/sheets/sheet_types.dm b/code/game/objects/items/stacks/sheets/sheet_types.dm index 0383c3f1ee..4e5ffab1e0 100644 --- a/code/game/objects/items/stacks/sheets/sheet_types.dm +++ b/code/game/objects/items/stacks/sheets/sheet_types.dm @@ -773,6 +773,7 @@ GLOBAL_LIST_INIT(plastic_recipes, list( new /datum/stack_recipe("water bottle", /obj/item/reagent_containers/glass/beaker/waterbottle/empty), \ new /datum/stack_recipe("large water bottle", /obj/item/reagent_containers/glass/beaker/waterbottle/large/empty,3), \ new /datum/stack_recipe("shower curtain", /obj/structure/curtain, 10, time = 10, one_per_turf = 1, on_floor = 1), \ + new /datum/stack_recipe("duct", /obj/item/stack/ducts,1), \ new /datum/stack_recipe("laser pointer case", /obj/item/glasswork/glass_base/laserpointer_shell, 30), \ new /datum/stack_recipe("wet floor sign", /obj/item/caution, 2))) diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm index fb3b1535ed..85f0066e3f 100644 --- a/code/game/objects/objs.dm +++ b/code/game/objects/objs.dm @@ -324,3 +324,6 @@ . = ..() if(. && ricochet_damage_mod) take_damage(P.damage * ricochet_damage_mod, P.damage_type, P.flag, 0, turn(P.dir, 180), P.armour_penetration) // pass along ricochet_damage_mod damage to the structure for the ricochet + +/obj/proc/plunger_act(obj/item/plunger/P, mob/living/user, reinforced) + return diff --git a/code/game/objects/structures/geyser.dm b/code/game/objects/structures/geyser.dm new file mode 100644 index 0000000000..1d77a3c4f8 --- /dev/null +++ b/code/game/objects/structures/geyser.dm @@ -0,0 +1,74 @@ +//If you look at the "geyser_soup" overlay icon_state, you'll see that the first frame has 25 ticks. +//That's because the first 18~ ticks are completely skipped for some ungodly weird fucking byond reason + +/obj/structure/geyser + name = "geyser" + icon = 'icons/obj/lavaland/terrain.dmi' + icon_state = "geyser" + anchored = TRUE + var/decay = 0 //reagents/tick removed + var/erupting_state = null //set to null to get it greyscaled from "[icon_state]_soup". Not very usable with the whole random thing, but more types can be added if you change the spawn prob + var/activated = FALSE //whether we are active and generating chems + var/reagent_id = "oil" + var/potency = 2 //how much reagents we add every process (2 seconds) + var/max_volume = 500 + var/start_volume = 50 + +/obj/structure/geyser/proc/start_chemming() + activated = TRUE + create_reagents(max_volume, DRAINABLE) + reagents.add_reagent(reagent_id, start_volume) + START_PROCESSING(SSfluids, src) //It's main function is to be plumbed, so use SSfluids + if(erupting_state) + icon_state = erupting_state + else + var/mutable_appearance/I = mutable_appearance('icons/obj/lavaland/terrain.dmi', "[icon_state]_soup") + I.color = mix_color_from_reagents(reagents.reagent_list) + add_overlay(I) + +/obj/structure/geyser/process() + if(activated && reagents.total_volume <= reagents.maximum_volume) //this is also evaluated in add_reagent, but from my understanding proc calls are expensive and should be avoided in continous + reagents.add_reagent(reagent_id, potency) //processes + if(potency > 0) + potency =- decay //decaying! + +/obj/structure/geyser/plunger_act(obj/item/plunger/P, mob/living/user, _reinforced) + if(!_reinforced) + to_chat(user, "The [P.name] isn't strong enough!") + return + if(activated) + to_chat(user, "The [name] is already active!") + return + + to_chat(user, "You start vigorously plunging [src]!") + if(do_after(user, 50*P.plunge_mod, target = src) && !activated) + start_chemming() + +/obj/structure/geyser/random + erupting_state = null + var/list/options = list("oil" = 2, "clf3" = 1) //formerly crudeoil, 2,1 +/obj/structure/geyser/random/Initialize() + . = ..() + reagent_id = pickweight(options) + +/obj/item/plunger + name = "plunger" + desc = "It's a plunger for plunging." + icon = 'icons/obj/watercloset.dmi' + icon_state = "plunger" + slot_flags = ITEM_SLOT_MASK + + var/plunge_mod = 1 //time*plunge_mod = total time we take to plunge an object + var/reinforced = FALSE //whether we do heavy duty stuff like geysers + +/obj/item/plunger/attack_obj(obj/O, mob/living/user) + if(!O.plunger_act(src, user, reinforced)) + return ..() + +/obj/item/plunger/reinforced + name = "reinforced plunger" + desc = " It's an M. 7 Reinforced Plunger for heavy duty plunging." + icon_state = "reinforced_plunger" + + reinforced = TRUE + plunge_mod = 0.8 diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm index 57d8e55e07..e44c5e2a3f 100644 --- a/code/modules/asset_cache/asset_list_items.dm +++ b/code/modules/asset_cache/asset_list_items.dm @@ -266,7 +266,7 @@ name = "pipes" /datum/asset/spritesheet/pipes/register() - for (var/each in list('icons/obj/atmospherics/pipes/pipe_item.dmi', 'icons/obj/atmospherics/pipes/disposal.dmi')) + for (var/each in list('icons/obj/atmospherics/pipes/pipe_item.dmi', 'icons/obj/atmospherics/pipes/disposal.dmi', 'icons/obj/atmospherics/pipes/transit_tube.dmi', 'icons/obj/plumbing/fluid_ducts.dmi')) InsertAll("", each, GLOB.alldirs) ..() @@ -378,4 +378,3 @@ assets = list( "ghost.png" = 'html/ghost.png' ) - diff --git a/code/modules/plumbing/ducts.dm b/code/modules/plumbing/ducts.dm new file mode 100644 index 0000000000..735526511f --- /dev/null +++ b/code/modules/plumbing/ducts.dm @@ -0,0 +1,371 @@ +/* +All the important duct code: +/code/datums/components/plumbing/plumbing.dm +/code/datums/ductnet.dm +*/ +/obj/machinery/duct + name = "fluid duct" + icon = 'icons/obj/plumbing/fluid_ducts.dmi' + icon_state = "nduct" + level = 1 + ///bitfield with the directions we're connected in + var/connects + ///set to TRUE to disable smart cable behaviour + var/dumb = FALSE + ///wheter we allow our connects to be changed after initialization or not + var/lock_connects = FALSE + ///our ductnet, wich tracks what we're connected to + var/datum/ductnet/duct + ///amount we can transfer per process. note that the ductnet can carry as much as the lowest capacity duct + var/capacity = 10 + + ///the color of our duct + var/duct_color = null + ///TRUE to ignore colors, so yeah we also connect with other colors without issue + var/ignore_colors = FALSE + ///1,2,4,8,16 + var/duct_layer = DUCT_LAYER_DEFAULT + ///whether we allow our layers to be altered + var/lock_layers = FALSE + ///TRUE to let colors connect when forced with a wrench, false to just not do that at all + var/color_to_color_support = TRUE + ///wheter to even bother with plumbing code or not + var/active = TRUE + ///track ducts we're connected to. Mainly for ducts we connect to that we normally wouldn't, like different layers and colors, for when we regenerate the ducts + var/list/neighbours = list() + ///wheter we just unanchored or drop whatever is in the variable. either is safe + var/drop_on_wrench = /obj/item/stack/ducts + +/obj/machinery/duct/Initialize(mapload, no_anchor, color_of_duct, layer_of_duct = DUCT_LAYER_DEFAULT, force_connects) + . = ..() + if(no_anchor) + active = FALSE + anchored = FALSE + else if(!can_anchor()) + CRASH("Overlapping ducts detected") + qdel(src) + if(force_connects) + connects = force_connects //skip change_connects() because we're still initializing and we need to set our connects at one point + if(!lock_layers) + duct_layer = layer_of_duct + if(!ignore_colors) + duct_color = color_of_duct + if(duct_color) + add_atom_colour(duct_color, FIXED_COLOUR_PRIORITY) + handle_layer() + for(var/obj/machinery/duct/D in loc) + if(D == src) + continue + if(D.duct_layer & duct_layer) + disconnect_duct() + if(active) + attempt_connect() +///start looking around us for stuff to connect to +/obj/machinery/duct/proc/attempt_connect() + reset_connects() //All connects are gathered here again eitherway, we might aswell reset it so they properly update when reconnecting + + for(var/atom/movable/AM in loc) + var/datum/component/plumbing/P = AM.GetComponent(/datum/component/plumbing) + if(P?.active) + disconnect_duct() //let's not built under plumbing machinery + return + for(var/D in GLOB.cardinals) + if(dumb && !(D & connects)) + continue + for(var/atom/movable/AM in get_step(src, D)) + if(connect_network(AM, D)) + add_connects(D) + update_icon() +///see if whatever we found can be connected to +/obj/machinery/duct/proc/connect_network(atom/movable/AM, direction, ignore_color) + if(istype(AM, /obj/machinery/duct)) + return connect_duct(AM, direction, ignore_color) + + var/plumber = AM.GetComponent(/datum/component/plumbing) + if(!plumber) + return + return connect_plumber(plumber, direction) +///connect to a duct +/obj/machinery/duct/proc/connect_duct(obj/machinery/duct/D, direction, ignore_color) + var/opposite_dir = turn(direction, 180) + if(!active || !D.active) + return + + if(!dumb && D.dumb && !(opposite_dir & D.connects)) + return + if(dumb && D.dumb && !(connects & D.connects)) //we eliminated a few more scenario in attempt connect + return + + if((duct == D.duct) && duct)//check if we're not just comparing two null values + add_neighbour(D) + + D.add_connects(opposite_dir) + D.update_icon() + return TRUE //tell the current pipe to also update it's sprite + if(!(D in neighbours)) //we cool + if((duct_color != D.duct_color) && !(ignore_colors || D.ignore_colors)) + return + if(!(duct_layer & D.duct_layer)) + return + + if(D.duct) + if(duct) + duct.assimilate(D.duct) + else + D.duct.add_duct(src) + else + if(duct) + duct.add_duct(D) + else + create_duct() + duct.add_duct(D) + add_neighbour(D) + D.attempt_connect()//tell our buddy its time to pass on the torch of connecting to pipes. This shouldn't ever infinitely loop since it only works on pipes that havent been inductrinated + return TRUE +///connect to a plumbing object +/obj/machinery/duct/proc/connect_plumber(datum/component/plumbing/P, direction) + var/opposite_dir = turn(direction, 180) + if(duct_layer != DUCT_LAYER_DEFAULT) //plumbing devices don't support multilayering. 3 is the default layer so we only use that. We can change this later + return FALSE + + if(!P.active) + return + + var/comp_directions = P.supply_connects + P.demand_connects //they should never, ever have supply and demand connects overlap or catastrophic failure + if(opposite_dir & comp_directions) + if(!duct) + create_duct() + duct.add_plumber(P, opposite_dir) + return TRUE +///we disconnect ourself from our neighbours. we also destroy our ductnet and tell our neighbours to make a new one +/obj/machinery/duct/proc/disconnect_duct() + anchored = FALSE + active = FALSE + if(duct) + duct.remove_duct(src) + lose_neighbours() + reset_connects(0) + update_icon() + if(ispath(drop_on_wrench) && !QDELING(src)) + new drop_on_wrench(drop_location()) + qdel(src) + +///create a new duct datum +/obj/machinery/duct/proc/create_duct() + duct = new() + duct.add_duct(src) +///add a duct as neighbour. this means we're connected and will connect again if we ever regenerate +/obj/machinery/duct/proc/add_neighbour(obj/machinery/duct/D) + if(!(D in neighbours)) + neighbours += D + if(!(src in D.neighbours)) + D.neighbours += src +///remove all our neighbours, and remove us from our neighbours aswell +/obj/machinery/duct/proc/lose_neighbours() + for(var/A in neighbours) + var/obj/machinery/duct/D = A + D.neighbours.Remove(src) + neighbours = list() +///add a connect direction +/obj/machinery/duct/proc/add_connects(new_connects) //make this a define to cut proc calls? + if(!lock_connects) + connects |= new_connects +///remove our connects +/obj/machinery/duct/proc/reset_connects() + if(!lock_connects) + connects = 0 +///get a list of the ducts we can connect to if we are dumb +/obj/machinery/duct/proc/get_adjacent_ducts() + var/list/adjacents = list() + for(var/A in GLOB.cardinals) + if(A & connects) + for(var/obj/machinery/duct/D in get_step(src, A)) + if((turn(A, 180) & D.connects) && D.active) + adjacents += D + return adjacents + +/obj/machinery/duct/update_icon() //setting connects isnt a parameter because sometimes we make more than one change, overwrite it completely or just add it to the bitfield + var/temp_icon = initial(icon_state) + for(var/D in GLOB.cardinals) + if(D & connects) + if(D == NORTH) + temp_icon += "_n" + if(D == SOUTH) + temp_icon += "_s" + if(D == EAST) + temp_icon += "_e" + if(D == WEST) + temp_icon += "_w" + icon_state = temp_icon +///update the layer we are on +/obj/machinery/duct/proc/handle_layer() + var/offset + switch(duct_layer)//it's a bitfield, but it's fine because it only works when there's one layer, and multiple layers should be handled differently + if(FIRST_DUCT_LAYER) + offset = -10 + if(SECOND_DUCT_LAYER) + offset = -5 + if(THIRD_DUCT_LAYER) + offset = 0 + if(FOURTH_DUCT_LAYER) + offset = 5 + if(FIFTH_DUCT_LAYER) + offset = 10 + pixel_x = offset + pixel_y = offset + + +/obj/machinery/duct/wrench_act(mob/living/user, obj/item/I) //I can also be the RPD + ..() + add_fingerprint(user) + I.play_tool_sound(src) + if(anchored) + user.visible_message( \ + "[user] unfastens \the [src].", \ + "You unfasten \the [src].", \ + "You hear ratcheting.") + disconnect_duct() + else if(can_anchor()) + anchored = TRUE + active = TRUE + user.visible_message( \ + "[user] fastens \the [src].", \ + "You fasten \the [src].", \ + "You hear ratcheting.") + attempt_connect() + return TRUE +///collection of all the sanity checks to prevent us from stacking ducts that shouldnt be stacked +/obj/machinery/duct/proc/can_anchor(turf/T) + if(!T) + T = get_turf(src) + for(var/obj/machinery/duct/D in T) + if(!anchored) + continue + for(var/A in GLOB.cardinals) + if(A & connects && A & D.connects) + return FALSE + return TRUE + +/obj/machinery/duct/doMove(destination) + . = ..() + disconnect_duct() + anchored = FALSE + +/obj/machinery/duct/Destroy() + disconnect_duct() + return ..() + +/obj/machinery/duct/MouseDrop_T(atom/A, mob/living/user) + if(!istype(A, /obj/machinery/duct)) + return + var/obj/machinery/duct/D = A + var/obj/item/I = user.get_active_held_item() + if(I?.tool_behaviour != TOOL_WRENCH) + to_chat(user, "You need to be holding a wrench in your active hand to do that!") + return + if(get_dist(src, D) != 1) + return + var/direction = get_dir(src, D) + if(!(direction in GLOB.cardinals)) + return + if(duct_layer != D.duct_layer) + return + + add_connects(direction) //the connect of the other duct is handled in connect_network, but do this here for the parent duct because it's not necessary in normal cases + add_neighbour(D) + connect_network(D, direction, TRUE) + update_icon() +///has a total of 5 layers and doesnt give a shit about color. its also dumb so doesnt autoconnect. +/obj/machinery/duct/multilayered + name = "duct layer-manifold" + icon = 'icons/obj/2x2.dmi' + icon_state = "multiduct" + pixel_x = -15 + pixel_y = -15 + + color_to_color_support = FALSE + duct_layer = FIRST_DUCT_LAYER | SECOND_DUCT_LAYER | THIRD_DUCT_LAYER | FOURTH_DUCT_LAYER | FIFTH_DUCT_LAYER + drop_on_wrench = null + + lock_connects = TRUE + lock_layers = TRUE + ignore_colors = TRUE + dumb = TRUE + + active = FALSE + anchored = FALSE + +/obj/machinery/duct/multilayered/Initialize(mapload, no_anchor, color_of_duct, layer_of_duct = DUCT_LAYER_DEFAULT, force_connects) + . = ..() + update_connects() + +/obj/machinery/duct/multilayered/update_icon() + return + +/obj/machinery/duct/multilayered/wrench_act(mob/living/user, obj/item/I) + . = ..() + update_connects() + +/obj/machinery/duct/multilayered/proc/update_connects() + if(dir & NORTH || dir & SOUTH) + connects = NORTH | SOUTH + else + connects = EAST | WEST + +///don't connect to other multilayered stuff because honestly it shouldnt be done and I dont wanna deal with it +/obj/machinery/duct/multilayered/connect_duct(obj/machinery/duct/D, direction, ignore_color) + if(istype(D, /obj/machinery/duct/multilayered)) + return + return ..() + +/obj/machinery/duct/multilayered/handle_layer() + return + +/obj/item/stack/ducts + name = "stack of duct" + desc = "A stack of fluid ducts." + singular_name = "duct" + icon = 'icons/obj/plumbing/fluid_ducts.dmi' + icon_state = "ducts" + w_class = WEIGHT_CLASS_TINY + novariants = FALSE + max_amount = 50 + item_flags = NOBLUDGEON + merge_type = /obj/item/stack/ducts + ///Color of our duct + var/duct_color = "grey" + ///Default layer of our duct + var/duct_layer = "Default Layer" + ///Assoc index with all the available layers. yes five might be a bit much. Colors uses a global by the way + var/list/layers = list("First Layer" = FIRST_DUCT_LAYER, "Second Layer" = SECOND_DUCT_LAYER, "Default Layer" = DUCT_LAYER_DEFAULT, + "Fourth Layer" = FOURTH_DUCT_LAYER, "Fifth Layer" = FIFTH_DUCT_LAYER) + +/obj/item/stack/ducts/examine(mob/user) + . = ..() + . += "It's current color and layer are [duct_color] and [duct_layer]. Use in-hand to change." + +/obj/item/stack/ducts/attack_self(mob/user) + var/new_layer = input("Select a layer", "Layer") as null|anything in layers + if(new_layer) + duct_layer = new_layer + var/new_color = input("Select a color", "Color") as null|anything in GLOB.pipe_paint_colors + if(new_color) + duct_color = new_color + add_atom_colour(GLOB.pipe_paint_colors[new_color], FIXED_COLOUR_PRIORITY) + +/obj/item/stack/ducts/afterattack(atom/A, user, proximity) + . = ..() + if(!proximity) + return + if(istype(A, /obj/machinery/duct)) + var/obj/machinery/duct/D = A + if(!D.anchored) + add(1) + qdel(D) + if(istype(A, /turf/open) && use(1)) + var/turf/open/OT = A + new /obj/machinery/duct(OT, FALSE, GLOB.pipe_paint_colors[duct_color], layers[duct_layer]) + playsound(get_turf(src), 'sound/machines/click.ogg', 50, TRUE) + +/obj/item/stack/ducts/fifty + amount = 50 diff --git a/code/modules/plumbing/plumbers/_plumb_machinery.dm b/code/modules/plumbing/plumbers/_plumb_machinery.dm new file mode 100644 index 0000000000..1ea7ab56b1 --- /dev/null +++ b/code/modules/plumbing/plumbers/_plumb_machinery.dm @@ -0,0 +1,97 @@ +/**Basic plumbing object. +* It doesn't really hold anything special, YET. +* Objects that are plumbing but not a subtype are as of writing liquid pumps and the reagent_dispenser tank +* Also please note that the plumbing component is toggled on and off by the component using a signal from default_unfasten_wrench, so dont worry about it +*/ +/obj/machinery/plumbing + name = "pipe thing" + icon = 'icons/obj/plumbing/plumbers.dmi' + icon_state = "pump" + density = TRUE + active_power_usage = 30 + use_power = ACTIVE_POWER_USE + resistance_flags = FIRE_PROOF | UNACIDABLE | ACID_PROOF + ///Plumbing machinery is always gonna need reagents, so we might aswell put it here + var/buffer = 50 + ///Flags for reagents, like INJECTABLE, TRANSPARENT bla bla everything thats in DEFINES/reagents.dm + var/reagent_flags = TRANSPARENT + ///wheter we partake in rcd construction or not + var/rcd_constructable = TRUE + ///cost of the plumbing rcd construction + var/rcd_cost = 15 + ///delay of constructing it throught the plumbing rcd + var/rcd_delay = 10 + +/obj/machinery/plumbing/Initialize(mapload, bolt = TRUE) + . = ..() + anchored = bolt + create_reagents(buffer, reagent_flags) + AddComponent(/datum/component/simple_rotation, ROTATION_ALTCLICK | ROTATION_CLOCKWISE | ROTATION_COUNTERCLOCKWISE | ROTATION_VERBS ) + +/obj/machinery/plumbing/proc/can_be_rotated(mob/user,rotation_type) + return TRUE + +/obj/machinery/plumbing/examine(mob/user) + . = ..() + . += "The maximum volume display reads: [reagents.maximum_volume] units." + +/obj/machinery/plumbing/wrench_act(mob/living/user, obj/item/I) + ..() + default_unfasten_wrench(user, I) + return TRUE + +/obj/machinery/plumbing/plunger_act(obj/item/plunger/P, mob/living/user, reinforced) + to_chat(user, "You start furiously plunging [name].") + if(do_after(user, 30, target = src)) + to_chat(user, "You finish plunging the [name].") + reagents.reaction(get_turf(src), TOUCH) //splash on the floor + reagents.clear_reagents() + +/obj/machinery/plumbing/welder_act(mob/living/user, obj/item/I) + . = ..() + if(anchored) + to_chat(user, "The [name] needs to be unbolted to do that!You start slicing the [name] apart.You slice the [name] apart. target_temperature && acclimate_state != COOLING) + acclimate_state = COOLING + update_icon() + if(!emptying) + if(reagents.chem_temp >= target_temperature && target_temperature + allowed_temperature_difference >= reagents.chem_temp) //cooling here + emptying = TRUE + if(reagents.chem_temp <= target_temperature && target_temperature - allowed_temperature_difference <= reagents.chem_temp) //heating here + emptying = TRUE + + reagents.adjust_thermal_energy((target_temperature - reagents.chem_temp) * heater_coefficient * SPECIFIC_HEAT_DEFAULT * reagents.total_volume) //keep constant with chem heater + reagents.handle_reactions() + +/obj/machinery/plumbing/acclimator/update_icon() + icon_state = initial(icon_state) + switch(acclimate_state) + if(COOLING) + icon_state += "_cold" + if(HEATING) + icon_state += "_hot" + +/obj/machinery/plumbing/acclimator/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ChemAcclimator", name) + ui.open() + +/obj/machinery/plumbing/acclimator/ui_data(mob/user) + var/list/data = list() + + data["enabled"] = enabled + data["chem_temp"] = reagents.chem_temp + data["target_temperature"] = target_temperature + data["allowed_temperature_difference"] = allowed_temperature_difference + data["acclimate_state"] = acclimate_state + data["max_volume"] = reagents.maximum_volume + data["reagent_volume"] = reagents.total_volume + data["emptying"] = emptying + return data + +/obj/machinery/plumbing/acclimator/ui_act(action, params) + if(..()) + return + . = TRUE + switch(action) + if("set_target_temperature") + var/target = text2num(params["temperature"]) + target_temperature = clamp(target, 0, 1000) + if("set_allowed_temperature_difference") + var/target = text2num(params["temperature"]) + allowed_temperature_difference = clamp(target, 0, 1000) + if("toggle_power") + enabled = !enabled + if("change_volume") + var/target = text2num(params["volume"]) + reagents.maximum_volume = clamp(round(target), 1, buffer) + +#undef COOLING +#undef HEATING +#undef NEUTRAL diff --git a/code/modules/plumbing/plumbers/bottler.dm b/code/modules/plumbing/plumbers/bottler.dm new file mode 100644 index 0000000000..c6595fc397 --- /dev/null +++ b/code/modules/plumbing/plumbers/bottler.dm @@ -0,0 +1,79 @@ +/obj/machinery/plumbing/bottler + name = "chemical bottler" + desc = "Puts reagents into containers, like bottles and beakers." + icon_state = "bottler" + layer = ABOVE_ALL_MOB_LAYER + reagent_flags = TRANSPARENT | DRAINABLE + rcd_cost = 50 + rcd_delay = 50 + buffer = 100 + ///how much do we fill + var/wanted_amount = 10 + ///where things are sent + var/turf/goodspot = null + ///where things are taken + var/turf/inputspot = null + ///where beakers that are already full will be sent + var/turf/badspot = null + +/obj/machinery/plumbing/bottler/Initialize(mapload, bolt) + . = ..() + AddComponent(/datum/component/plumbing/simple_demand, bolt) + setDir(dir) + +/obj/machinery/plumbing/bottler/can_be_rotated(mob/user, rotation_type) + if(anchored) + to_chat(user, "It is fastened to the floor!") + return FALSE + return TRUE + +///changes the tile array +/obj/machinery/plumbing/bottler/setDir(newdir) + . = ..() + switch(dir) + if(NORTH) + goodspot = get_step(get_turf(src), NORTH) + inputspot = get_step(get_turf(src), SOUTH) + badspot = get_step(get_turf(src), EAST) + if(SOUTH) + goodspot = get_step(get_turf(src), SOUTH) + inputspot = get_step(get_turf(src), NORTH) + badspot = get_step(get_turf(src), WEST) + if(WEST) + goodspot = get_step(get_turf(src), WEST) + inputspot = get_step(get_turf(src), EAST) + badspot = get_step(get_turf(src), NORTH) + if(EAST) + goodspot = get_step(get_turf(src), EAST) + inputspot = get_step(get_turf(src), WEST) + badspot = get_step(get_turf(src), SOUTH) + +///changing input ammount with a window +/obj/machinery/plumbing/bottler/interact(mob/user) + . = ..() + wanted_amount = clamp(round(input(user,"maximum is 100u","set ammount to fill with") as num|null, 1), 1, 100) + reagents.clear_reagents() + to_chat(user, " The [src] will now fill for [wanted_amount]u.") + +/obj/machinery/plumbing/bottler/process() + if(machine_stat & NOPOWER) + return + ///see if machine has enough to fill + if(reagents.total_volume >= wanted_amount && anchored) + var/obj/AM = pick(inputspot.contents)///pick a reagent_container that could be used + if(istype(AM, /obj/item/reagent_containers) && (!istype(AM, /obj/item/reagent_containers/hypospray/medipen))) + var/obj/item/reagent_containers/B = AM + ///see if it would overflow else inject + if((B.reagents.total_volume + wanted_amount) <= B.reagents.maximum_volume) + reagents.trans_to(B, wanted_amount, transfered_by = src) + B.forceMove(goodspot) + return + ///glass was full so we move it away + AM.forceMove(badspot) + if(istype(AM, /obj/item/slime_extract)) ///slime extracts need inject + AM.forceMove(goodspot) + reagents.trans_to(AM, wanted_amount, transfered_by = src, method = INJECT) + return + if(istype(AM, /obj/item/slimecross/industrial)) ///no need to move slimecross industrial things + reagents.trans_to(AM, wanted_amount, transfered_by = src, method = INJECT) + return diff --git a/code/modules/plumbing/plumbers/destroyer.dm b/code/modules/plumbing/plumbers/destroyer.dm new file mode 100644 index 0000000000..b61383ea4a --- /dev/null +++ b/code/modules/plumbing/plumbers/destroyer.dm @@ -0,0 +1,21 @@ +/obj/machinery/plumbing/disposer + name = "chemical disposer" + desc = "Breaks down chemicals and annihilates them." + icon_state = "disposal" + ///we remove 10 reagents per second + var/disposal_rate = 10 + +/obj/machinery/plumbing/disposer/Initialize(mapload, bolt) + . = ..() + AddComponent(/datum/component/plumbing/simple_demand, bolt) + +/obj/machinery/plumbing/disposer/process() + if(stat & NOPOWER) + return + if(reagents.total_volume) + if(icon_state != initial(icon_state) + "_working") //threw it here instead of update icon since it only has two states + icon_state = initial(icon_state) + "_working" + reagents.remove_any(disposal_rate) + else + if(icon_state != initial(icon_state)) + icon_state = initial(icon_state) diff --git a/code/modules/plumbing/plumbers/fermenter.dm b/code/modules/plumbing/plumbers/fermenter.dm new file mode 100644 index 0000000000..e13ae3feff --- /dev/null +++ b/code/modules/plumbing/plumbers/fermenter.dm @@ -0,0 +1,52 @@ +/obj/machinery/plumbing/fermenter //FULLY AUTOMATIC BEER BREWING. TRULY, THE FUTURE. + name = "chemical fermenter" + desc = "Turns plants into various types of booze." + icon_state = "fermenter" + layer = ABOVE_ALL_MOB_LAYER + reagent_flags = TRANSPARENT | DRAINABLE + rcd_cost = 30 + rcd_delay = 30 + buffer = 400 + ///input dir + var/eat_dir = SOUTH + +/obj/machinery/plumbing/fermenter/Initialize(mapload, bolt) + . = ..() + AddComponent(/datum/component/plumbing/simple_supply, bolt) + +/obj/machinery/plumbing/grinder_chemical/can_be_rotated(mob/user, rotation_type) + if(anchored) + to_chat(user, "It is fastened to the floor!") + return FALSE + return TRUE + +/obj/machinery/plumbing/fermenter/setDir(newdir) + . = ..() + eat_dir = newdir + +/obj/machinery/plumbing/fermenter/CanAllowThrough(atom/movable/AM) + . = ..() + if(!anchored) + return + var/move_dir = get_dir(loc, AM.loc) + if(move_dir == eat_dir) + return TRUE + +/obj/machinery/plumbing/fermenter/Crossed(atom/movable/AM) + . = ..() + ferment(AM) + +/// uses fermentation proc similar to fermentation barrels +/obj/machinery/plumbing/fermenter/proc/ferment(atom/AM) + if(machine_stat & NOPOWER) + return + if(reagents.holder_full()) + return + if(!isitem(AM)) + return + if(istype(AM, /obj/item/reagent_containers/food/snacks/grown)) + var/obj/item/reagent_containers/food/snacks/grown/G = AM + if(G.distill_reagent) + var/amount = G.seed.potency * 0.25 + reagents.add_reagent(G.distill_reagent, amount) + qdel(G) diff --git a/code/modules/plumbing/plumbers/filter.dm b/code/modules/plumbing/plumbers/filter.dm new file mode 100644 index 0000000000..3058120b9d --- /dev/null +++ b/code/modules/plumbing/plumbers/filter.dm @@ -0,0 +1,74 @@ +///chemical plumbing filter. If it's not filtered by left and right, it goes straight. +/obj/machinery/plumbing/filter + name = "chemical filter" + desc = "A chemical filter for filtering chemicals. The left and right outputs appear to be from the perspective of the input port." + icon_state = "filter" + density = FALSE + ///whitelist of chems id's that go to the left side. Empty to disable port + var/list/left = list() + ///whitelist of chem id's that go to the right side. Empty to disable port + var/list/right = list() + ///whitelist of chems but their name instead of path + var/list/english_left = list() + ///whitelist of chems but their name instead of path + var/list/english_right = list() + var/ui_x = 320 + var/ui_y = 310 + + +/obj/machinery/plumbing/filter/Initialize(mapload, bolt) + . = ..() + AddComponent(/datum/component/plumbing/filter, bolt) + +/obj/machinery/plumbing/filter/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.default_state) + ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open) + + if(!ui) + ui = new(user, src, ui_key, "chemical_filter", name, ui_x, ui_y, master_ui, state) + ui.open() + +/obj/machinery/plumbing/filter/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ChemFilter", name) + ui.open() + +/obj/machinery/plumbing/filter/ui_data(mob/user) + var/list/data = list() + data["left"] = english_left + data["right"] = english_right + return data + +/obj/machinery/plumbing/filter/ui_act(action, params) + if(..()) + return + . = TRUE + switch(action) + if("add") + var/new_chem_name = params["name"] + var/chem_id = get_chem_id(new_chem_name) + if(chem_id) + switch(params["which"]) + if("left") + if(!left.Find(chem_id)) + english_left += new_chem_name + left += chem_id + if("right") + if(!right.Find(chem_id)) + english_right += new_chem_name + right += chem_id + else + to_chat(usr, "No such known reagent exists!") + + if("remove") + var/chem_name = params["reagent"] + var/chem_id = get_chem_id(chem_name) + switch(params["which"]) + if("left") + if(english_left.Find(chem_name)) + english_left -= chem_name + left -= chem_id + if("right") + if(english_right.Find(chem_name)) + english_right -= chem_name + right -= chem_id diff --git a/code/modules/plumbing/plumbers/grinder_chemical.dm b/code/modules/plumbing/plumbers/grinder_chemical.dm new file mode 100644 index 0000000000..4fa0d9e2cc --- /dev/null +++ b/code/modules/plumbing/plumbers/grinder_chemical.dm @@ -0,0 +1,58 @@ +/obj/machinery/plumbing/grinder_chemical + name = "chemical grinder" + desc = "chemical grinder." + icon_state = "grinder_chemical" + layer = ABOVE_ALL_MOB_LAYER + reagent_flags = TRANSPARENT | DRAINABLE + rcd_cost = 30 + rcd_delay = 30 + buffer = 400 + var/eat_dir = SOUTH + +/obj/machinery/plumbing/grinder_chemical/Initialize(mapload, bolt) + . = ..() + AddComponent(/datum/component/plumbing/simple_supply, bolt) + +/obj/machinery/plumbing/grinder_chemical/can_be_rotated(mob/user, rotation_type) + if(anchored) + to_chat(user, "It is fastened to the floor!") + return FALSE + return TRUE + +/obj/machinery/plumbing/grinder_chemical/setDir(newdir) + . = ..() + eat_dir = newdir + +/obj/machinery/plumbing/grinder_chemical/CanAllowThrough(atom/movable/AM) + . = ..() + if(!anchored) + return + var/move_dir = get_dir(loc, AM.loc) + if(move_dir == eat_dir) + return TRUE + +/obj/machinery/plumbing/grinder_chemical/Crossed(atom/movable/AM) + . = ..() + grind(AM) + +/obj/machinery/plumbing/grinder_chemical/proc/grind(atom/AM) + if(machine_stat & NOPOWER) + return + if(reagents.holder_full()) + return + if(!isitem(AM)) + return + var/obj/item/I = AM + if(I.juice_results || I.grind_results) + if(I.juice_results) + I.on_juice() + reagents.add_reagent_list(I.juice_results) + if(I.reagents) + I.reagents.trans_to(src, I.reagents.total_volume, transfered_by = src) + qdel(I) + return + I.on_grind() + reagents.add_reagent_list(I.grind_results) + if(I.reagents) + I.reagents.trans_to(src, I.reagents.total_volume, transfered_by = src) + qdel(I) diff --git a/code/modules/plumbing/plumbers/pill_press.dm b/code/modules/plumbing/plumbers/pill_press.dm new file mode 100644 index 0000000000..9c2505661c --- /dev/null +++ b/code/modules/plumbing/plumbers/pill_press.dm @@ -0,0 +1,107 @@ +///We take a constant input of reagents, and produce a pill once a set volume is reached +/obj/machinery/plumbing/pill_press + name = "pill press" + desc = "A press that presses pills." + icon_state = "pill_press" + ///the minimum size a pill can be + var/minimum_pill = 5 + ///the maximum size a pill can be + var/maximum_pill = 50 + ///the size of the pill + var/pill_size = 10 + ///pill name + var/pill_name = "factory pill" + ///the icon_state number for the pill. + var/pill_number = RANDOM_PILL_STYLE + ///list of id's and icons for the pill selection of the ui + var/list/pill_styles + ///list of pills stored in the machine, so we dont have 610 pills on one tile + var/list/stored_pills = list() + ///max amount of pills allowed on our tile before we start storing them instead + var/max_floor_pills = 10 + +/obj/machinery/plumbing/pill_press/examine(mob/user) + . = ..() + . += "The [name] currently has [stored_pills.len] stored. There needs to be less than [max_floor_pills] on the floor to continue dispensing." + +/obj/machinery/plumbing/pill_press/Initialize(mapload, bolt) + . = ..() + AddComponent(/datum/component/plumbing/simple_demand, bolt) + + //expertly copypasted from chemmasters + var/datum/asset/spritesheet/simple/assets = get_asset_datum(/datum/asset/spritesheet/simple/pills) + pill_styles = list() + for (var/x in 1 to PILL_STYLE_COUNT) + var/list/SL = list() + SL["id"] = x + SL["htmltag"] = assets.icon_tag("pill[x]") + pill_styles += list(SL) + +/obj/machinery/plumbing/pill_press/process() + if(stat & NOPOWER) + return + if(reagents.total_volume >= pill_size) + var/obj/item/reagent_containers/pill/P = new(src) + reagents.trans_to(P, pill_size) + P.name = pill_name + stored_pills += P + if(pill_number == RANDOM_PILL_STYLE) + P.icon_state = "pill[rand(1,21)]" + else + P.icon_state = "pill[pill_number]" + if(P.icon_state == "pill4") //mirrored from chem masters + P.desc = "A tablet or capsule, but not just any, a red one, one taken by the ones not scared of knowledge, freedom, uncertainty and the brutal truths of reality." + if(stored_pills.len) + var/pill_amount = 0 + for(var/obj/item/reagent_containers/pill/P in loc) + pill_amount++ + if(pill_amount >= max_floor_pills) //too much so just stop + break + if(pill_amount < max_floor_pills) + var/atom/movable/AM = stored_pills[1] //AM because forceMove is all we need + stored_pills -= AM + AM.forceMove(drop_location()) + + +/obj/machinery/plumbing/pill_press/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/spritesheet/simple/pills), + ) + +/obj/machinery/plumbing/pill_press/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ChemPress", name) + ui.open() + +/obj/machinery/plumbing/pill_press/ui_data(mob/user) + var/list/data = list() + data["pill_style"] = pill_number + data["current_volume"] = current_volume + data["product_name"] = product_name + data["pill_styles"] = pill_styles + data["product"] = product + data["min_volume"] = min_volume + data["max_volume"] = max_volume + return data + +/obj/machinery/plumbing/pill_press/ui_act(action, params) + if(..()) + return + . = TRUE + switch(action) + if("change_pill_style") + pill_number = clamp(text2num(params["id"]), 1 , PILL_STYLE_COUNT) + if("change_current_volume") + current_volume = clamp(text2num(params["volume"]), min_volume, max_volume) + if("change_product_name") + product_name = html_encode(params["name"]) + if("change_product") + product = params["product"] + if (product == "pill") + max_volume = max_pill_volume + else if (product == "patch") + max_volume = max_patch_volume + else if (product == "bottle") + max_volume = max_bottle_volume + current_volume = clamp(current_volume, min_volume, max_volume) diff --git a/code/modules/plumbing/plumbers/pumps.dm b/code/modules/plumbing/plumbers/pumps.dm new file mode 100644 index 0000000000..aeec8c9994 --- /dev/null +++ b/code/modules/plumbing/plumbers/pumps.dm @@ -0,0 +1,83 @@ +///We pump liquids from activated(plungerated) geysers to a plumbing outlet. We need to be wired. +/obj/machinery/power/liquid_pump + name = "liquid pump" + desc = "Pump up those sweet liquids from under the surface." + icon = 'icons/obj/plumbing/plumbers.dmi' + icon_state = "pump" + anchored = FALSE + density = TRUE + idle_power_usage = 10 + active_power_usage = 1000 + ///Are we powered? + var/powered = FALSE + ///units we pump per process (2 seconds) + var/pump_power = 2 + ///set to true if the loop couldnt find a geyser in process, so it remembers and stops checking every loop until moved. more accurate name would be absolutely_no_geyser_under_me_so_dont_try + var/geyserless = FALSE + ///The geyser object + var/obj/structure/geyser/geyser + ///volume of our internal buffer + var/volume = 200 + +/obj/machinery/power/liquid_pump/Initialize() + . = ..() + create_reagents(volume) + AddComponent(/datum/component/plumbing/simple_supply, TRUE) + +/obj/machinery/power/liquid_pump/attackby(obj/item/W, mob/user, params) + if(!powered) + if(!anchored) + if(default_deconstruction_screwdriver(user, "[initial(icon_state)]_open", "[initial(icon_state)]",W)) + return + if(default_deconstruction_crowbar(W)) + return + return ..() + +/obj/machinery/power/liquid_pump/wrench_act(mob/living/user, obj/item/I) + ..() + default_unfasten_wrench(user, I) + return TRUE +///please note that the component has a hook in the parent call, wich handles activating and deactivating +/obj/machinery/power/liquid_pump/default_unfasten_wrench(mob/user, obj/item/I, time = 20) + . = ..() + if(. == SUCCESSFUL_UNFASTEN) + geyser = null + update_icon() + powered = FALSE + geyserless = FALSE //we switched state, so lets just set this back aswell + +/obj/machinery/power/liquid_pump/process() + if(!anchored || panel_open) + return + if(!geyser && !geyserless) + for(var/obj/structure/geyser/G in loc.contents) + geyser = G + if(!geyser) //we didnt find one, abort + anchored = FALSE + geyserless = TRUE + visible_message("The [name] makes a sad beep!") + playsound(src, 'sound/machines/buzz-sigh.ogg', 50) + return + + if(avail(active_power_usage)) + if(!powered) //we werent powered before this tick so update our sprite + powered = TRUE + update_icon() + add_load(active_power_usage) + pump() + else if(powered) //we were powered, but now we arent + powered = FALSE + update_icon() +///pump up that sweet geyser nectar +/obj/machinery/power/liquid_pump/proc/pump() + if(!geyser || !geyser.reagents) + return + geyser.reagents.trans_to(src, pump_power) + +/obj/machinery/power/liquid_pump/update_icon() + if(powered) + icon_state = initial(icon_state) + "-on" + else if(panel_open) + icon_state = initial(icon_state) + "-open" + else + icon_state = initial(icon_state) diff --git a/code/modules/plumbing/plumbers/reaction_chamber.dm b/code/modules/plumbing/plumbers/reaction_chamber.dm new file mode 100644 index 0000000000..33fc5c8f0c --- /dev/null +++ b/code/modules/plumbing/plumbers/reaction_chamber.dm @@ -0,0 +1,63 @@ +///a reaction chamber for plumbing. pretty much everything can react, but this one keeps the reagents seperated and only reacts under your given terms +/obj/machinery/plumbing/reaction_chamber + name = "reaction chamber" + desc = "Keeps chemicals seperated until given conditions are met." + icon_state = "reaction_chamber" + + buffer = 200 + reagent_flags = TRANSPARENT | NO_REACT + /**list of set reagents that the reaction_chamber allows in, and must all be present before mixing is enabled. + * example: list(/datum/reagent/water = 20, /datum/reagent/fuel/oil = 50) + */ + var/list/required_reagents = list() + ///our reagent goal has been reached, so now we lock our inputs and start emptying + var/emptying = FALSE + +/obj/machinery/plumbing/reaction_chamber/Initialize(mapload, bolt) + . = ..() + AddComponent(/datum/component/plumbing/reaction_chamber, bolt) + +/obj/machinery/plumbing/reaction_chamber/on_reagent_change() + if(reagents.total_volume == 0 && emptying) //we were emptying, but now we aren't + emptying = FALSE + reagent_flags |= NO_REACT + +/obj/machinery/plumbing/reaction_chamber/power_change() + . = ..() + if(use_power != NO_POWER_USE) + icon_state = initial(icon_state) + "_on" + else + icon_state = initial(icon_state) + +/obj/machinery/plumbing/reaction_chamber/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ChemReactionChamber", name) + ui.open() + +/obj/machinery/plumbing/reaction_chamber/ui_data(mob/user) + var/list/data = list() + var/list/text_reagents = list() + for(var/A in required_reagents) //make a list where the key is text, because that looks alot better in the ui than a typepath + var/datum/reagent/R = A + text_reagents[initial(R.name)] = required_reagents[R] + + data["reagents"] = text_reagents + data["emptying"] = emptying + return data + +/obj/machinery/plumbing/reaction_chamber/ui_act(action, params) + if(..()) + return + . = TRUE + switch(action) + if("remove") + var/reagent = get_chem_id(params["chem"]) + if(reagent) + required_reagents.Remove(reagent) + if("add") + var/input_reagent = get_chem_id(params["chem"]) + if(input_reagent && !required_reagents.Find(input_reagent)) + var/input_amount = text2num(params["amount"]) + if(input_amount) + required_reagents[input_reagent] = input_amount diff --git a/code/modules/plumbing/plumbers/splitters.dm b/code/modules/plumbing/plumbers/splitters.dm new file mode 100644 index 0000000000..a26813486c --- /dev/null +++ b/code/modules/plumbing/plumbers/splitters.dm @@ -0,0 +1,50 @@ +///it splits the reagents however you want. So you can "every 60 units, 45 goes left and 15 goes straight". The side direction is EAST, you can change this in the component +/obj/machinery/plumbing/splitter + name = "Chemical Splitter" + desc = "A chemical splitter for smart chemical factorization. Waits till a set of conditions is met and then stops all input and splits the buffer evenly or other in two ducts." + icon_state = "splitter" + buffer = 100 + density = FALSE + + ///constantly switches between TRUE and FALSE. TRUE means the batch tick goes straight, FALSE means the next batch goes in the side duct. + var/turn_straight = TRUE + ///how much we must transfer straight. note input can be as high as 10 reagents per process, usually + var/transfer_straight = 5 + ///how much we must transfer to the side + var/transfer_side = 5 + //the maximum you can set the transfer to + var/max_transfer = 9 + + +/obj/machinery/plumbing/splitter/Initialize(mapload, bolt) + . = ..() + AddComponent(/datum/component/plumbing/splitter, bolt) + +/obj/machinery/plumbing/splitter/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ChemSplitter", name) + ui.open() + +/obj/machinery/plumbing/splitter/ui_data(mob/user) + var/list/data = list() + data["straight"] = transfer_straight + data["side"] = transfer_side + data["max_transfer"] = max_transfer + return data + +/obj/machinery/plumbing/splitter/ui_act(action, params) + if(..()) + return + . = TRUE + switch(action) + if("set_amount") + var/direction = params["target"] + var/value = clamp(text2num(params["amount"]), 1, max_transfer) + switch(direction) + if("straight") + transfer_straight = value + if("side") + transfer_side = value + else + return FALSE diff --git a/code/modules/plumbing/plumbers/synthesizer.dm b/code/modules/plumbing/plumbers/synthesizer.dm new file mode 100644 index 0000000000..2d782e9d3e --- /dev/null +++ b/code/modules/plumbing/plumbers/synthesizer.dm @@ -0,0 +1,111 @@ +///A single machine that produces a single chem. Can be placed in unison with others through plumbing to create chemical factories +/obj/machinery/plumbing/synthesizer + name = "chemical synthesizer" + desc = "Produces a single chemical at a given volume. Must be plumbed. Most effective when working in unison with other chemical synthesizers, heaters and filters." + + icon_state = "synthesizer" + icon = 'icons/obj/plumbing/plumbers.dmi' + rcd_cost = 25 + rcd_delay = 15 + + ///Amount we produce for every process. Ideally keep under 5 since thats currently the standard duct capacity + var/amount = 1 + ///The maximum we can produce for every process + buffer = 5 + ///I track them here because I have no idea how I'd make tgui loop like that + var/static/list/possible_amounts = list(0,1,2,3,4,5) + ///The reagent we are producing. We are a typepath, but are also typecast because there's several occations where we need to use initial. + var/datum/reagent/reagent_id = null + ///straight up copied from chem dispenser. Being a subtype would be extremely tedious and making it global would restrict potential subtypes using different dispensable_reagents + var/list/dispensable_reagents = list( + /datum/reagent/aluminium, + /datum/reagent/bromine, + /datum/reagent/carbon, + /datum/reagent/chlorine, + /datum/reagent/copper, + /datum/reagent/consumable/ethanol, + /datum/reagent/fluorine, + /datum/reagent/hydrogen, + /datum/reagent/iodine, + /datum/reagent/iron, + /datum/reagent/lithium, + /datum/reagent/mercury, + /datum/reagent/nitrogen, + /datum/reagent/oxygen, + /datum/reagent/phosphorus, + /datum/reagent/potassium, + /datum/reagent/uranium/radium, + /datum/reagent/silicon, + /datum/reagent/silver, + /datum/reagent/sodium, + /datum/reagent/stable_plasma, + /datum/reagent/consumable/sugar, + /datum/reagent/sulfur, + /datum/reagent/toxin/acid, + /datum/reagent/water, + /datum/reagent/fuel, + ) + +/obj/machinery/plumbing/synthesizer/Initialize(mapload, bolt) + . = ..() + AddComponent(/datum/component/plumbing/simple_supply, bolt) + +/obj/machinery/plumbing/synthesizer/process() + if(machine_stat & NOPOWER || !reagent_id || !amount) + return + if(reagents.total_volume >= amount) //otherwise we get leftovers, and we need this to be precise + return + reagents.add_reagent(reagent_id, amount) + +/obj/machinery/plumbing/synthesizer/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ChemSynthesizer", name) + ui.open() + +/obj/machinery/plumbing/synthesizer/ui_data(mob/user) + var/list/data = list() + + var/is_hallucinating = user.hallucinating() + var/list/chemicals = list() + + for(var/A in dispensable_reagents) + var/datum/reagent/R = GLOB.chemical_reagents_list[A] + if(R) + var/chemname = R.name + if(is_hallucinating && prob(5)) + chemname = "[pick_list_replacements("hallucination.json", "chemicals")]" + chemicals.Add(list(list("title" = chemname, "id" = ckey(R.name)))) + data["chemicals"] = chemicals + data["amount"] = amount + data["possible_amounts"] = possible_amounts + + data["current_reagent"] = ckey(initial(reagent_id.name)) + return data + +/obj/machinery/plumbing/synthesizer/ui_act(action, params) + if(..()) + return + . = TRUE + switch(action) + if("amount") + var/new_amount = text2num(params["target"]) + if(new_amount in possible_amounts) + amount = new_amount + . = TRUE + if("select") + var/new_reagent = GLOB.name2reagent[params["reagent"]] + if(new_reagent in dispensable_reagents) + reagent_id = new_reagent + . = TRUE + update_icon() + reagents.clear_reagents() + +/obj/machinery/plumbing/synthesizer/update_overlays() + . = ..() + var/mutable_appearance/r_overlay = mutable_appearance(icon, "[icon_state]_overlay") + if(reagent_id) + r_overlay.color = initial(reagent_id.color) + else + r_overlay.color = "#FFFFFF" + . += r_overlay diff --git a/code/modules/reagents/chemistry/holder.dm b/code/modules/reagents/chemistry/holder.dm index c2662a8342..c489edf88e 100644 --- a/code/modules/reagents/chemistry/holder.dm +++ b/code/modules/reagents/chemistry/holder.dm @@ -1165,3 +1165,9 @@ random_reagents += R var/picked_reagent = pick(random_reagents) return picked_reagent + +/proc/get_chem_id(chem_name) + for(var/X in GLOB.chemical_reagents_list) + var/datum/reagent/R = GLOB.chemical_reagents_list[X] + if(ckey(chem_name) == ckey(lowertext(R.name))) + return X diff --git a/code/modules/reagents/chemistry/reagents.dm b/code/modules/reagents/chemistry/reagents.dm index a85ac8b085..cdda9d0469 100644 --- a/code/modules/reagents/chemistry/reagents.dm +++ b/code/modules/reagents/chemistry/reagents.dm @@ -8,6 +8,7 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent()) if (length(initial(R.name))) .[ckey(initial(R.name))] = t + //Various reagents //Toxin & acid reagents //Hydroponics stuff @@ -220,4 +221,3 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent()) bloodsuckerdatum.handle_eat_human_food(disgust, blood_puke, force) if(blood_change) bloodsuckerdatum.AddBloodVolume(blood_change) - diff --git a/code/modules/reagents/reagent_dispenser.dm b/code/modules/reagents/reagent_dispenser.dm index 0131e6d028..077c8331ae 100644 --- a/code/modules/reagents/reagent_dispenser.dm +++ b/code/modules/reagents/reagent_dispenser.dm @@ -24,7 +24,8 @@ /obj/structure/reagent_dispensers/Initialize() create_reagents(tank_volume, DRAINABLE | AMOUNT_VISIBLE) - reagents.add_reagent(reagent_id, tank_volume) + if(reagent_id) + reagents.add_reagent(reagent_id, tank_volume) . = ..() /obj/structure/reagent_dispensers/proc/boom() @@ -91,6 +92,38 @@ user.put_in_hands(S) paper_cups-- +/obj/structure/reagent_dispensers/plumbed + name = "stationairy water tank" + anchored = TRUE + icon_state = "water_stationairy" + desc = "A stationairy, plumbed, water tank." + +/obj/structure/reagent_dispensers/plumbed/wrench_act(mob/living/user, obj/item/I) + default_unfasten_wrench(user, I) + return TRUE + +/obj/structure/reagent_dispensers/plumbed/default_unfasten_wrench(mob/user, obj/item/I, time = 20) + . = ..() + if(. == SUCCESSFUL_UNFASTEN) + user.visible_message("[user.name] [anchored ? "fasten" : "unfasten"] [src]", \ + "You [anchored ? "fasten" : "unfasten"] [src]") + var/datum/component/plumbing/CP = GetComponent(/datum/component/plumbing) + if(anchored) + CP.enable() + else + CP.disable() + +/obj/structure/reagent_dispensers/plumbed/ComponentInitialize() + AddComponent(/datum/component/plumbing/simple_supply) + +/obj/structure/reagent_dispensers/plumbed/storage + name = "stationairy storage tank" + icon_state = "tank_stationairy" + reagent_id = null //start empty + +/obj/structure/reagent_dispensers/plumbed/storage/ComponentInitialize() + AddComponent(/datum/component/plumbing/tank) + ////////////// //Fuel Tanks// ////////////// @@ -284,5 +317,3 @@ icon_state = "bluekeg" reagent_id = /datum/reagent/consumable/ethanol/neurotoxin tank_volume = 100 //2.5x less than the other kegs because it's harder to get - - diff --git a/code/modules/research/designs/medical_designs.dm b/code/modules/research/designs/medical_designs.dm index e1e55f3476..0598ebf9ab 100644 --- a/code/modules/research/designs/medical_designs.dm +++ b/code/modules/research/designs/medical_designs.dm @@ -961,3 +961,158 @@ build_path = /obj/item/bodypart/r_arm/robot/surplus_upgraded category = list("Medical Designs") departmental_flags = DEPARTMENTAL_FLAG_MEDICAL + + +/datum/design/acclimator + name = "Plumbing Acclimator" + desc = "A heating and cooling device for pipes!" + id = "acclimator" + build_type = PROTOLATHE + materials = list(MAT_METAL = 1000, MAT_GLASS = 500) + construction_time = 75 + build_path = /obj/machinery/plumbing/acclimator + category = list("Misc","Medical Designs") + departmental_flags = DEPARTMENTAL_FLAG_MEDICAL | DEPARTMENTAL_FLAG_SCIENCE + +/datum/design/disposer + name = "Plumbing Disposer" + desc = "Using the power of SCIENCE, dissolves reagents into nothing (almost)." + id = "disposer" + build_type = PROTOLATHE + materials = list(MAT_METAL = 500, MAT_GLASS = 100) + construction_time = 75 + build_path = /obj/machinery/plumbing/disposer + category = list("Misc","Medical Designs") + departmental_flags = DEPARTMENTAL_FLAG_MEDICAL | DEPARTMENTAL_FLAG_SCIENCE + +/datum/design/plumb_filter + name = "Plumbing Filter" + desc = "Filters out chemicals by their NTDB ID." + id = "plumb_filter" + build_type = PROTOLATHE + materials = list(MAT_METAL = 1000, MAT_GLASS = 500) + construction_time = 75 + build_path = /obj/machinery/plumbing/filter + category = list("Misc","Medical Designs") + departmental_flags = DEPARTMENTAL_FLAG_MEDICAL | DEPARTMENTAL_FLAG_SCIENCE + +/datum/design/plumb_synth + name = "Plumbing Synthesizer" + desc = "Using standard mass-energy dynamic autoconverters, generates reagents from power and puts them in a pipe." + id = "plumb_synth" + build_type = PROTOLATHE + materials = list(MAT_METAL = 5000, MAT_GLASS = 1000, MAT_PLASTIC = 1000) + construction_time = 75 + build_path = /obj/machinery/plumbing/synthesizer + category = list("Misc","Medical Designs") + departmental_flags = DEPARTMENTAL_FLAG_MEDICAL | DEPARTMENTAL_FLAG_SCIENCE + +/datum/design/plumb_grinder + name = "Plumbing-Linked Autogrinder" + desc = "Automatically extracts reagents from an item by grinding it. Think of the possibilities! Note: does not grind people." + id = "plumb_grinder" + build_type = PROTOLATHE + materials = list(MAT_METAL = 2000, MAT_GLASS = 1500) + construction_time = 75 + build_path = /obj/machinery/plumbing/grinder_chemical + category = list("Misc","Medical Designs") + departmental_flags = DEPARTMENTAL_FLAG_MEDICAL | DEPARTMENTAL_FLAG_SCIENCE + +/datum/design/reaction_chamber + name = "Plumbing Reaction Chamber" + desc = "You can set a list of allowed reagents and amounts. Once the chamber has these reagents, will let the products through." + id = "reaction_chamber" + build_type = PROTOLATHE + materials = list(MAT_METAL = 1000, MAT_GLASS = 500) + construction_time = 75 + build_path = /obj/machinery/plumbing/reaction_chamber + category = list("Misc","Medical Designs") + departmental_flags = DEPARTMENTAL_FLAG_MEDICAL | DEPARTMENTAL_FLAG_SCIENCE + +/datum/design/duct_print + name = "Plumbing Ducts" + desc = "Ducts for plumbing! Now lathed for efficiency." + id = "duct_print" + build_type = PROTOLATHE + materials = list(MAT_PLASTIC = 400) + construction_time = 5 + build_path = /obj/item/stack/ducts + category = list("Misc","Medical Designs") + departmental_flags = DEPARTMENTAL_FLAG_MEDICAL | DEPARTMENTAL_FLAG_SCIENCE + +/datum/design/plumb_splitter + name = "Plumbing Chemical Splitter" + desc = "A splitter. Has 2 outputs. Can be configured to allow a certain amount through each side." + id = "plumb_splitter" + build_type = PROTOLATHE + materials = list(MAT_METAL = 750, MAT_GLASS = 250) + construction_time = 75 + build_path = /obj/machinery/plumbing/splitter + category = list("Misc","Medical Designs") + departmental_flags = DEPARTMENTAL_FLAG_MEDICAL | DEPARTMENTAL_FLAG_SCIENCE + +/datum/design/pill_press + name = "Plumbing Automatic Pill Former" + desc = "Automatically forms pills to the required parameters with piped reagents! A good replacement for those lazy, useless chemists." + id = "pill_press" + build_type = PROTOLATHE + materials = list(MAT_METAL = 1000, MAT_GLASS = 500) + construction_time = 75 + build_path = /obj/machinery/plumbing/pill_press + category = list("Misc","Medical Designs") + departmental_flags = DEPARTMENTAL_FLAG_MEDICAL | DEPARTMENTAL_FLAG_SCIENCE + +/datum/design/plumb_pump + name = "Liquid Extraction Pump" + desc = "Use it for extracting liquids from lavaland's geysers!" + id = "plumb_pump" + build_type = PROTOLATHE + materials = list(MAT_METAL = 1000, MAT_GLASS = 500) + construction_time = 75 + build_path = /obj/machinery/power/liquid_pump + category = list("Misc","Medical Designs") + departmental_flags = DEPARTMENTAL_FLAG_MEDICAL | DEPARTMENTAL_FLAG_SCIENCE + +/datum/design/plumb_in + name = "Plumbing Input Device" + desc = "A big piped funnel for putting stuff in the pipe network." + id = "plumb_in" + build_type = PROTOLATHE + materials = list(MAT_METAL = 400, MAT_GLASS = 400) + construction_time = 75 + build_path = /obj/machinery/plumbing/input + category = list("Misc","Medical Designs") + departmental_flags = DEPARTMENTAL_FLAG_MEDICAL | DEPARTMENTAL_FLAG_SCIENCE + +/datum/design/plumb_out + name = "Plumbing Output Device" + desc = "A big piped funnel for taking stuff out of the pipe network." + id = "plumb_out" + build_type = PROTOLATHE + materials = list(MAT_METAL = 400, MAT_GLASS = 400) + construction_time = 75 + build_path = /obj/machinery/plumbing/output + category = list("Misc","Medical Designs") + departmental_flags = DEPARTMENTAL_FLAG_MEDICAL | DEPARTMENTAL_FLAG_SCIENCE + +/datum/design/plumb_tank + name = "Plumbed Storage Tank" + desc = "A tank for storing plumbed reagents." + id = "plumb_tank" + build_type = PROTOLATHE + materials = list(MAT_METAL = 10000, MAT_GLASS = 10000, MAT_PLASTIC = 4000) + construction_time = 75 + build_path = /obj/machinery/plumbing/tank + category = list("Misc","Medical Designs") + departmental_flags = DEPARTMENTAL_FLAG_MEDICAL | DEPARTMENTAL_FLAG_SCIENCE + +/datum/design/plumb_rcd + name = "Plumbed Autoconstruction Device" + desc = "A RCD for plumbing machines! Cannot make ducts." + id = "plumb_rcd" + build_type = PROTOLATHE + materials = list(MAT_METAL = 20000, MAT_GLASS = 10000, MAT_PLASTIC = 20000, MAT_PLASMA = 6000, MAT_DIAMOND = 5000, MAT_BLUESPACE = 5000, MAT_GOLD = 5000, MAT_SILVER = 5000) + construction_time = 150 + build_path = /obj/item/construction/plumbing + category = list("Misc","Medical Designs") + departmental_flags = DEPARTMENTAL_FLAG_MEDICAL | DEPARTMENTAL_FLAG_SCIENCEs diff --git a/code/modules/research/techweb/nodes/medical_nodes.dm b/code/modules/research/techweb/nodes/medical_nodes.dm index 71dd7c943c..43fe941ab2 100644 --- a/code/modules/research/techweb/nodes/medical_nodes.dm +++ b/code/modules/research/techweb/nodes/medical_nodes.dm @@ -24,6 +24,23 @@ design_ids = list("defib_decay", "defib_shock", "defib_heal", "defib_speed") research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500) +/datum/techweb_node/plumbing + id = "plumbing" + display_name = "Reagent Plumbing Technology" + description = "Plastic tubes, and machinery used for manipulating things in them." + prereq_ids = list("base") + design_ids = list("acclimator", "disposer", "plumb_filter", "plumb_synth", "plumb_grinder", "reaction_chamber", "duct_print", "plumb_splitter", "pill_press", "plumb_pump", "plumb_in", "plumb_out", "plumb_tank") + research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500) + + +/datum/techweb_node/advplumbing + id = "advplumbing" + display_name = "Advanced Plumbing Technology" + description = "Plumbing RCD." + prereq_ids = list("plumbing", "adv_engi", "adv_biotech") + design_ids = list("plumb_rcd") + research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 10000) + //////////////////////Cybernetics///////////////////// /datum/techweb_node/surplus_limbs diff --git a/icons/obj/watercloset.dmi b/icons/obj/watercloset.dmi index 4a299f29ddb16c596e251c00890f4f016c630ece..94ea415ed7cf406fb49befddd17426bccf1fd3c2 100644 GIT binary patch literal 16012 zcmYj&byyYO_w`&ZB^`n^my(ih5x5{CA&rzYhzQbr=(sdUOC!>ybazR23P^Y7rTaI2 z|9Ri%nTI1?J!dj=P4o zlk`VNQ-{xXPM>XU0KhdhBBngjpPNA1>W-u^=llovTltibZRwah6kb;-X}64(Ba4p@ z8rsu(z72U>bLUSZcFkT|_$)^xx01+N9TwQ*JDmy(5_gtmc2l{ihP{rLS1_m)BtN+- zezTJ(x9>GXL?Mmw?U@QmoANh7tp%w}SP`C*5I=0r zDHxru`=eerf=K0dmrk8m_4aKRleM{`}Z#<@m`|ZPrYSu9E=$Jri@FO-wvP zh9~&4QH&nChN-kXLp<^v!zA)}RhVYv2@V&AC}isETS07HE92wt@Z@(&IRr!|(%mqW zVH81zo~Tael;xTi;bzTCmU7iT8cVx#xx0tIc7HXzU$}HsN#hLFDsX&088Gz-VM*WzIcF zzMgJbTk2~X?U;b=$B((jf%acLi@*5%(wV6{d@eT@-~X=W!0krli=IpDI;)+qdDZ~* z>S3x!Z}0FrMa5(8M?q0hXQWv0=yAY*)m&EBmNC;e<>4fMkIvm!&fTrIaHIEh^i-Lw zV+|+Tr^U+@-9*aAHXsB|23!*qN!522ujc9$%BPlEylzuXYsHW%Gvf6^|607c*mjYh z68L8zOcku;*uA-+wDhf)SF>3W8#}wIj!s|mv*64DC`?0B^Y_+iVEwx0Z(VQ38gJ9W zyhg**5a+S2iX5YCceUJuf+htd?SzIQUW2xMekSdgf?hX{mPi zV|wq+hYysydwb=FX{9UGK0dE+nn$)e3XfX)Co!qKZ_f+RVFZ`}!GQbK!F0L9hJ4}I z?oj^%bOdTCaAIt8EZBD73sriOESuYVc1$P!Rg(rL_F?*lzN7{eu6Jl1^(x@|FWdI!%f+yW z?=C2q|8)^s`7HXNhq3?4NeO2$dsUWD8V|Ms5)d9;a7PIQ#9;sw6cn2*@~>Y*YiyTj z^2|hQtfRvL)tB`CXimfkL10}s3+<)Ih4g!Z=(?(}j-G`c>^ zK{ug5q^45+o4LB5H>C4EkN1w2A}Rg-^6TsCWV-Mv0nuIkzbH)a<;DSAyK+odL%^HK`8Z0cDx3D(MC@P2rGMVJ`7wLXc#VmIN>7+ zzMig{lrEC@JQ(_J%?4GqwHB6YiBDuHr9V-#rVpzB}uccrA55R+{VgkVu5*3w?z}A5+>;6=qTd9 zOHb=zo-vv#*;`r13=q_2QyG5G-CvDiHQm_(?zo{XJ287{WJb(7kioA6*~&~ zY3Uk{l7inLrJ=r;Y#Sd}i%Ur8-e2X}Q#R!=?^mHuafb*we%5T1mdOyK0EMhqEUQx6o8gjaiJ@9gh~lVdEG`Cb^U zi@}s&YEf}ie;gt6HbUxFy}B)`y$lsG7k^2LY2GyzL_AEOVrcH{;$~zXVTeuY9sVuf zb&qoiV-*?st}-i_e@V|FV3 zW~W^c70{}{IBqrT8IUH$^vuK287~v#{56&M#PWiIg39_n$U|ON4!~7D;8hPjS44+R zwNX_1I~8s0`LalOsW&usiO=lBW(t0AaS@VNP{<9HG_%KTewTpq(LLAfO4F7zBK5z^hN{^i3FFplxDu3;k=>5Fpj6Aub_{x zRYgXx6F~3p!XhkOS9!Gb*il%2q%sw4yp<}slX$!yfrKnd&SLRooSmjW>ha>p#|CO6 z?~9}Ao%cl;$$himMy+<>OW=6KU<~Y?n}Eg}R6IU^RVg!l^Iz*wZ%@g&Yhb&TE@LW;aoy%g;eR0e*k->qF}ct>plbOB#nmTHGex?t9$DR||Und9TZb zZ-lVvyqvot85BXV*6RD8=5*sP-T-PwKTke&bB)ga8wZdz0;%T&2V<1;lx*jl)GLw1 z77T!q5(ni$3VzFluM?hFMRkI@p5qA=^!@6c?efTqSmZ=M{`NK+j5+(0UrfsM)@f!# zgYF(HHIpHilXVF{Qxc1KC}bvgGe23}4LyFZ1s5&?yw}kQbusjsp5&Fb3hu$SMqQVb zfs4%i!|=c->~Ru|=4D)JDh;fs{qeM;#(?Yrqf2dLVpg7FniD7?uvee_$7kF%qa6r0w056_8k0BY%U*x}*mnSAvEk8?sPX?;72l)g9kP9CU+biH$PlY(V`^c4nd(Tu&6 znW#wf`E!5g_5QbUyS@-825D3&qp%MMOl@a{i4KR3%^73HSD(r}N@s!DWd94IUdW zi>)^b>cY0sF41vK4dXcmd^v?t_AYXR+e@J`P{Qb>B_jWJ>!awn2En3KK>_pM&+j5c zJYv92roPXf`-;}%bzw*k9zK5Xhty(1{P(z4*Sd85N_hm(rY|MvuY=Nqhbw&AbV2w zhS*qsuh>;(_XE9F4;CHt5F1pYcv-8h31K(tdo$^YFKrfUZN8(jtV*kNc%mF>l~kl+ z1A|+{>WMJ`NzX%43q1id&;R()^8os}uDA&!XtAW~hOj5if)}v75gR(Bj7Xr3+zRB2 z>acB%+=V+PlT=Uhp`x-b3oE_((WcY; zlY66@IBKNWm>Dn`?#*izprN1;mJv4(U~=?5O=WM3K*;U8yFEjUK2m?)Ag94_zzbqV z=Xrho&yYtQA2vqknhZ*R@sGO?7;gcIe>YCuo5#cXa;tZ;ILP|TnSY4^6%?#G?sB1^ z2s7!a!Oi;vSPt)$C>IIYdc)8{O1!vdP9r zjaXU(4zbs{+WNN~`!h9?FKNXDzB>_yU8WzbEx7v4qE^{uhibS@~Q=NrbM zXUWOe-@Fn{KS&IpR9~%* z{u2E&JRSnOUh%CjEFTT1#>Soe11ic)9*L&B)51P{T8_+MP!p75q@-kZD^$Hme{<+l zbrlJ!at{v=i%=WrQEMDwKHbGVhO2EfsYVabn{w>5%aj7Xql35S`0jXLwc&Y1#CovS z2ZdOyjOJ#sRUdT&1J3t=QvIXlD{N+W63bGBX<;CD9pkOCK2Nb%(K))6n1O<~D<4X@ z3|%YNS$|CovcoFy%`XAIqer*ax^a!F+tRA9mBg^=T#d|_+fq@!(X&22<5Fn`9b|`N zoLs_fX&)3;-8-fpOn)fUKs&>Ez4GJh*ffC^Z_?Pz#=`{mO7Gj36_zx;J&i1&+PE~V zhGZmC8#FhO9i`zcwYs|%B*F+!J4A8a=@b}HlE$V|wO&ri?%O?P>wBoH$L55XQ$lrj zvKO=*@J$wc^pkx-(LK6}+uCOy0aPk@U;PvAV#*T1{v#4J&K(k1WP)r5W4 zwFfc5Wp!C>CK2!49kiJhRsHo3eRA&Mu1Hco57?ypYbT;ni(8K%`9I`9?%lCGR{Hk! zN@%#O+moT~$2_+h`^10o;MdRT+YXb9&iR=O3G$bQq0+67M5h2 z(i1VRrWReVoYb(DH>gthD<46tHiEU-6Xzd6U0zjHW#sPu@{5nHZK>0PS<9n2B_#W( zl?{6Q&%OT(ZVSk_l)-U#tIHxB#ODGHDA6R5^k>Yr4P`NF*3RAWncL?qw7P8)`$4C| z&n^PG$ywE5eUCZ0sdo>Bt&ir;#I$1np~c5{FZ%qU0Y>m8p4}*)U#BL6xW0QBVIn+i z&1(Y8^y@6}5y?B>vP)1-$y=XtM!kOJb-57j|t`xBT&hh(lG04@AXW^3fvIcv(*sYJp`Lh-cR%60ZF3d zZ}5~cQ>ao!U6kDMN?IRo0vfhLovhb&Q6<~HF)6I>p{aoEx}xJtobI?a`~(cI#xI{4 zda4$H(lx}x`*2=lZFJIo4dtN)Fig{>WCHh=H+cEgb3m{X{(_(V?MMEil zJM&6P@H{RLyIfjt^>-HfsD$m{b&n6Xxkp^hXGDjMN8(==8c1+)aU+ZFAx4*n3nj%L zYFArs4?DuRS}*~-dcCV*mlpp-LvI~`KM$Q@LXtsJM&6z%aj&#>@m$E~RLH{03VNT0 zsfZXJRsv)(H<TkNoRJh9pz!&> z>f6O2e2aezGnv2;``AIB=(e0KuQ*`F>z+~W}o56=r&hJp?P0;VTY z`OrnI-;jWb27*C+e0%-6;-JK2a>k>~9}@whnc-9(nf-vCmm?3UR=||sT)Fk@PE$GW ztJC^!@c?UU>t6}q8~(d54(dmfvRV-d1#@De-srF%PYrV58!b&zBL%kNOaUC)11&Z% zz=^nCi6Ewlvo*`Yt^5f(2db2@1XGR_Vt;~F2*X^a4iOlyt^adLf)zw8b*N`m#!{$PjQ~$jKwp3?^X#q z6bnBX<6w2c#_vFE5U>QOl8-9Opse|A5Gqr#9z5Mq@3I&eA!v41cjwRFzoB8?6653JR6pUDOFk0Ye-WXB6(0xeP9~J?PdRFVQEq~eVWE=s z-3fBj!CrjZPG=4<%cU^=nl#Xf=u9fX9F3c}>%SJ)6M9aArrrj!gm0@UXc-LXDxgLO z`&i}H5kFt_-}u2dT5nJu8jnnj{0mgFK@Rw}vpxi^)O!7`w6d&@zG|2K|M?5c*#g&u zvBUlSxXnajHa61LA1>ndX7H0Cp`|!+E}e=1W4Xb6FbyE;a@cm{?ef&xnhh7B=6xl5 z{kOLxJ_Ph3IBpU0i7TKc$m0mT0F!^27d4_J6XIhV1z`S7y^$l{eDKd?)%WKk?}+}p zFl78^L;?awChim6~_~S-&^=FS`nbxEX&mfp)RV&w+q{jyO2rZ6?47n0R-e`N4bEzVN>apXj-eZ#*auUrBhbZUno~gGT>pI91ZxngfqkJp9+M zPFX}h%Fr6%>V4JOkAK7uvaP$;hY#OXKyhKsP`JSRBEfV_fIMU?xRy&7^yM)rDHPKn zTu{exmitR-|3CIua#$n#*Z8;3)4)dNR#pj7_cw6;jOD zI_z0IVv~ig>6s;7x7VQ-HS`LRQ05#YB2CW)CFZvk~h* z$hJ;wKt}zvS%Uu}cO+uH2~tt4t3r7AkauU-{f-qcY|(Y;d@_o7vsP-qBcU+^(rmu6 ztzqowZMr;z-?+3W^4$O@x1o|^?MET=zI=87?m49`ab%e>Ag6^A6Dd^m4DsJwuf=uI zT-}LOzje=S|C8P$QESYZI2OSeJ@1KE2s7i1A~icHTzJ1l&Db|elEP-7aQ!VWinB}@ z5^7HYO!J5 zrc(^|XPAF%&x~Pia}eXB$h33Hmpsc)T#B5jPtMK%G7oG^_hFa0--+$brdRWpCD>v> zDQ(F$C=q5mH=$?;mgUaPpRdgua^>-p<88RDu0wGy0Vku{;1SM^@>ofJV!mJJ$`JdFt|FMX=^1hQVmphRf8QN7sMJK-EWlO1Hi2P@)sxX20f28spUt6sV3ga=jc z4S*^4UNIvbaeC3Tttd($(pwqbsbaYFh-DHq7r8@vqRIV zPO6BGs75mFk+~Eny*J1`)pb+W9#EFXu|+AX2`}1U7A;yZ$=(nhx|%3p6^()vbh6_6 zsrT=o616vSe-Sj|&EAwO>pxXtU?!x|W6qc)HtVW#a^q5YwY459Nkz{`;moc~pQ)Ky zjD>3T1f`Hw6l}T4v)oDr5#eF!Y6uLUq7Wmp+Ow@i!M5DsS9mpH1-)2m@|F;^`eURe z&?}e4Bo}f)DbO>g(~>Hy5cr){J%Xw*0ho?;drr7MaMVc6#^BfVLN`4yX{UXsT}b`_ zKi66YZw9m`UALldpP8>6wDI3n$hX`*e)L-4M^;$4k(yBaGs(JR7%Vup(+)}i5v@83 zhpCqMo8mR#0@NWUgiHb-yo-u{DZv$v!SrneHaN8iPAJ?Dr)X391D(&}HxiURfu&W+ z2ijzgfiL8m1E0JUWlSs`RHl65bdaVTcfB_aPh$Gb-zdGF3NzKETk10iN~zf61&Vr7 zKKxlg4|oO1e=}Q}Y|J4grl8bnZf9(GP&t3N~B6^d=vGfxkL9$^^%~CBduFA;l z3du21@pyC-GD4l4Vkt^_{i46eT~3gF%lmShp z<)c~1{pjdd)8O|A)68{T%9aM%q%{cMsGpVjjzMTWxXz}O5EX^y~ysRaS| zc%SwVW~Ht;t`$R@ly}wSmlxes+Dz%y?Gt_p70BlZrF|rPCYLHS2G zn4aiV|1-rFV)`j1MqR$PU%X_0?+3&XEW*v`cJ9{)L249&3Go?f$68joW}rprpK8;k zS8p-&*lfrNrZ#bNTmmNW>8zrN4=>`-Wyx`^O8q`+We~mi?ptnl+6kjlD0h6vNV9Kq z70+#^s-pBodl=1#PUx@E!AYk-of|}3!UD`&{^mTeX^dk?HeR_VV^UAVFgkY_b&gdY z_{bwCElw1E?q!4qPCHPEo63aJ08cDI><)H(l)Zb}It&7+IJvA3`8Uayi4}B2$5;q; zMC2l`J+y5sU}2TU8OHh?`les8w2D5A+y~R@x}5t%jlRS9gu2q7d1B@Pt(UFiFMZFD zb20s9W+L0~eHe-#^J}P5#{CO*qDBUrcEA?+^$NN#NWC0N{4-6ur#onnJ$8tUKMW=R zsP^(x(Uzp52qU!2d?>{U$OU*IIp~JzP%@Ndlw~{{g|~HE+?4ZMDBMtlHkH=#=~G1c zvP;U(iwozudPgcH-W4L$b~IIyEhvkmtbIP@9ZC|wji=hj2cv*w4YIZ$K|+Gyi#Zu1 z0ytx21i9&G+C$kHQ=rPJh=lF4%OfL4DyfF1bsyisGu~kWBcJuyo)so(*O2_Dw$h`a?dd z&+FMc)(S^+^`0E5GlG!cpHK}M>v~saNV5Xm;^>EZpGLsh+l^BDZR0)FKEy8NOKCWx zE};Qhl1{fhu&JpD0xO5r#eDQ7^@26M_JhiH$LO_OD@>a;yDt%Q2Qo@FG)U~Q-!*Ui zCVyA9udP>CEUf3FEy*b=HUaQ6l;;GyL}ze~L-zK2MuMcE=tOwKy&ByV@^ibX9%k1Q z**ipBY)83(^ve3z#N-(A3ojD~`qgsh`1GJ?Dkh#hl&^iH z6B`rGj|vI#g$qf)=7o-74x$@&0;2}L*+0Y;BmjV#>)&1gVW;hA01y)s0}w~vmyHR6 zpR@-O6T5yn>@PHG0f=wkh`!8}wVzozH%<^@zLUdZ(t1fzVQb>xcVpc|gQ!PNz}Q?U z&XXtaItWNSgkAUfrL*k>q?wpy{K=kZ*-?I`uOr-*PxZCn)&p(`#l_oH3A*O0KC91M z&^{aoqZqZIlqJxHf;qAI|EBd5`rMa&FsBihCk|5pUlcUAvyuPJImr|7vAF$#42rxMJBQXFZ ze_rJ3TdU2Po12+9IZYBGkC9Dkn}sKRGN zBjxY0DOq$~`SaNh{TXcXQd!TNmxusB(oq0H2$PUC+!B;BU^UghNx*7KxSL>9K%bw+ z>rD0Mj}%#?2>pNmT`ot8?y6{Lgu`J7@DM#Lh^-+fzpIoIr-mlIwPr5y5|nlkC(i7H z|HOI*9hyG(c5%fNi>&?k zX_?w{`{D%~A%pwc^o)WtY~jcJiK{-}^kt!7!=6(ve&dtzkJ|?iPm{@$H59*uV;JRm zHZ+UV%x?x2+;tMevac=B7=jR}cc>vU{;5{znVixlM(e)6JwwoC5+TuXKJh@}0NToo zZl>2d^>oT>tH2A~O>+OWo+yQ*v)3$-X$45g^8Vk--(dRY_k*b#C%a06W-c$eB#ax} zLeSWHn^09;IT6)0{i5x?mOd_UHG5#NzOj+n)TH@x^R4nxw71o#Ycm$kpVj&V-=R8~ z|4pO8*myqgK{GkUPw7|81X6@3emL-TJ45ZszXuw8YgB?XOtCQvA~~m zYa5&HJjL&JcUSgQ;-1>>n0+q5xCebdhV&b#R5Ri2<&n^P1OAJnr6>#ILeK+0*Q z0jhLz#q+qTsy&2QcQHJ-Im+oy8;UA(a>^*G&O%bep&`DoW*k;YordkkUFzvWf2%;B zd9wM@7cI%Ew-X0Po93!2a&n<($(|CPSKP93v%MC=3fD2KPol(cERrA{i0jKAGbUmvhYp-UkwWKl5B zZ)0TxcPVZ=rmCe5wBB@KAbz#!X71u;(%fO* zcLvSZ1A5h#B;azx=01arB>O+dwHNrzfFJ~U-{aHt#K^|yu7}nK6<`^@92CL09x8mC z{kkCW;F~7^DT+HHLN#XXWVy#BU^rhO__RkT^rK{FrvQZqHOE?l=2he%xvc-TY8=K= z-{?w9M))ZpZe?rxn4KaKh<9~<24zBXj1sy2bQ%uNVQCug@VPoR?!Z_ti&Jg_OfZY$ zIJhuQ>@3!ULRq|DZ4@+ZDj3I4Yz(A-z7hMJPVDJ(?Vnen&9uaX`CAHLr>nVrCb>U= zA-%yY;LIkq2aR@xghG0QiIiddIIiNI&f+45g3`98Puqe4@1?~>Z=E4DLMS{gT@Yox zG8rAfq(xHT@?oHq$Cc)JjQz@NGf&K{-p~r_j@>I~0$liWX{KRm z7I;#i%5fy#YVP=!WG~Agx=$iTdAD-#HYA6ks;(Cmp$s&E^&F}@LIM+~v9w46*<0_t zE+v<{U!H!41mjT?trK3QrO^xotvqkMYxcOrVq#))be9aLBGEQ{#P_>wT?k^=l0tW} zWPAF4WMBYa)Z5Muev_I;;}91XrZ9RjHD_UYQ=V6$!<0Ta%#K>dc$F4B`Vvx4aJ#RM ziHpjHMHB0*@&bW#i3{_D5R6yC-3f!}oNv$JUwEJXx)tqAxo<0#vi81Nkl$b~qxmm! zANhHZML~TcO5fVHIbwB{$n}-Zv1>(iVj@>FItIpXgEMi5CWDPyP$qP))KAm#Z(|V5 z?U<>K0-a-s9n1(4m_MSI3PB!V4oQ%rdOeCU#vPp;*wMZeyNEXQBS&bedF>?u4*EFN zReW{7vUas){3Nrr^Ce}XgqGmF4Z5PDg!7;2#||p3yUjmB!#{l?%lFx|Y-t_CpwB}G z`cm8Xm$wQ5J->qGznDY&46(to%Usx*OzjGU6soRFbwBpg1IVbT{0puaMn#B^yBl1< z{9nzEi27ZTZZ%e+W^o1M&B(?`AnKc;{XiA9j31(pI@`Ou(mFaK0Kzl!?O&Zf=Cwx; zIyUZ~UFyJuOxfeno+t!=7$w`lCc(G^K=pbj65N(O)?ff(kMVtj_1a6I499 zD)WprO|(@zBDC@8M{;Q{MAZ%-q3&Kf&N1Dnx^JHguc;kQmedyg2VjFa<0inW?zBjy6ar$+0rHbANsD?rc+o*$zmyoj z3B6J{K7}FJ4-O6@1pp+^j%I21z(!?!tK*i00ul(T~ zJNMYSBec!_DYH^4K?28zpoWHeb+~e>FkO*e{kR5G+<;-79UWir`R_3nu`8hL;{Ykp25xA4r!GbBv+_G|rq(u{ zkrh!yn{91Ay>Ar>KaH`7YS}#bp$F&IpYVH;u~8e<6Nfgev$J z$bMkLL?5-9PXE2)@uYl_s=! zydo!~P4DcSd>ZX7z{}TpVb`V}nRz1j?w!)Ae7B)o&wn`3KfY1ipyCz#xlKzU>AX2l zby&X6)l!49C2{hBEuD(qkD&jB8$&vRKrXx{>XHux6^Ue$tbUY6|?bE z`faWF^^fF%=yXwhY%!X+#G#(`^{0{EPAGtiMpD#b3&izRvR!W7=(!tyMdhnwGxH1w zRgt{>{FsDG|;XSZuzNygM?e=$% z1Z>&8BZk+t+jskpPHE?vWw2*|!gMo4 z2ZoUMpN6yVcCYK^C&HF!Z=vWxK{3f`kwstdu}&R%#K~`H-ET2?KsX%Ij9qye8Qa}!rrbYF^fmVqN466HWRQFr-(K#VR=ug%&e7sDsVa@mc zWE3=AC&Zt)Uy9SkGqDf&b9tS-R75`WJU2*v?2O`i&mrTSfUzLvB`c>G6X)~Ai3)tN z=@%zGcM{TdxyPZ=V;1q|nLuQrJOt3XLAR^Ve|F9OsHxgQs?93lOXkb*p~mQM#Yd1T zY5(ysco}exk`y>LoqmRnsB6`_3C^*u!cPCj_VC6uJCo4PAuzvIA^l@3ZF*8%3O*P| zy!yJclbnS7%*42*=V=|t$lk@|J{E}PMn*=_4OR{!#}0C}i)$$5qD(7(VmQvYv%M3W|Ll00W0eVVfcEhp4<4#7r?I-JGIA%T2h3aKG&Eg)rHf&alch$k zUrZuVdN;U|4Cz_vitm^o&$dSE(ICk?J6VPL(ZS&4cpj(c=iGM!UOz3%pI>iiC1$Nn z>YSb9)ANZ@*DfGQn(ot*qQu$f{97I*`v@WtlmKtrQMPhsf;>ihQt z_N$#eJW-{Mdw9k4zb&p#qBN*oHp8z*^SjCakWo{KlYBDGAE- zbW%7uYqpKfkiAL^4?!|RAy=GEG8|TnCHfk8OMyyq-p&bcEALWMzx@)9wjK&~l+s$Bz zl@(Q-)T`XOp$#c}@5NM845d3Ft@x*Mhwd9$4rX-=-=y?!nlRWC41$0Ay?VN_MJK8- zZ=fB`%(~)t%>)qS#Q_=vBX*6sYflozkDiD|CMfr7YDWj3riYMke8oPEIdslGb#T#E zq*d@{+}7ou?XYz^C}afb)}(+dgcru%j$kF(IR&sm%$Qk;Z+8~a6pD)9r^DT@t2`TU zIuG(A{Qn~V&=jH%h57YQkR-;(XBS$Pe78Jf`zinVTfCW7{OykICAPlUJxfwz=0$U1;v~*qU|;$M z02nzVm`K#rs@cw=D@CXdxXYzV*bmvK(K!JUw;A zn`r>-Yt-iHQmgmNpVAgNSWQ(`d0HxW=H6|h-KB3P1}-kLk6#AUUcc4fxLVtb_`NL+ zs88^b{YdQ!jN5~0Ji0h}Ux=VT?S>Ti+K8`j4ZptrbGov7)vC>Z`)S=gFQX2oOhHVm zAa36W$++89bMl^wSk1ayzPuh62hG9h@{EpygX9gGTauw#fcht|hK=;cPFi!%8;qdn zI5L?G2Gkv<1gbqs6yIf=t`TlnF~w-EgKJSG(K<>>CAm7P8(>`6+t=51e5|tX zpPj}`R+a+fnX8wv9Dj03y<*ygBn{s3nt%F~D_GCW$5131@x0?8;es$%QBJPC=@6fL z6juX-_F;1JxX40x-34qR=qJ)$E`NicuAqzxaWmFWpDJMSwB}%@U!Hw^9n8n;H1JkQ zASn1Ph^rTX(YcnUiL03gFPtebGilC?R1@(iR33Y~6{oJ|b+f$6W~{w)q0{dL31 zMaVS@j5d1qU*sQQ*!uIw5k5}2`Gdc~FL2JoXEx%09JJ4Vw}s0Yiz6Mk+SmFJ%nZ=$ z`cqIhD9;+GB}J|N_8c4@{2d$PyZdoE+pG@y#-BFZeUm|B#4{ij=h^17F!|Th-tOxo z-xCAFafI?vAcF%J8K6f8I{?~d$K_{ckcen%??CLWubY`33<<%6m??iGHjasMhs4D_ z4Uddin;7QQYnYEv0B>IZ-Liv#xHJ59&eyoule_fDpXZ4u>f`~udDA76z`*(S>sLu3 zw6o-mM23*THN)mpG#J<=8)Y>TloC#&MOiy zPpfz1WAbb#G2aJYInN^JVc;fMpZ0E!Ps+Z1e!JG-njw3QjFJ|g{WT#%p;gF0T%&&k z)s9v8!LZm9kJ70J2!VjPfa9TvOWyL$QmVV3+~S-UOqUl1W7N>-HIGWUq@<*_UTJ`@ zvXblvPI?AI@Z=Q#U-B%+_uO2>ps`ua_|q0%C^%nJ?JdXpJ}m&FiRt{-1uir5VG|P* zl%K37x3`;r*T#9)+DYtRCGWs>vF2G>F$ceXm7;NX*#B4b>E=d(_i?Pgc+bl^NC?mj zZ0<}yh!kP-GIa%3d_#GsgvKpOtjiJXy{+5=E*EMudrwy;nO6*KX+v#M$WW6tpk;2$ z;Nx#;?L7d^%{N-#I7VWJv8M37z%tvf6CriWt>M@Y13)zaN6?R7W85yTp(E%b_t9L-9)qam=L}hd^Xe9co0k4!0|f@EzdXJZwwAJE zd6lvg#aEpN@I<{`=-=UXt18;qm-*(HF#(imv}NowGwfGZ;5d#lh?dq`M_QE_e>Wk9 z4~Y1x5F)T6WpbE`ZzGA zNT!Is-2Oo6{A$@}M^0K+qi*nVJQjadTs;oK@(6uMBzPl!p*8<0h0w_DKDE9GWpr5O zVP_)Wgx1VagAza<_w*}Adfnn^q3B+}?-lC8ENk+{>v^G}i1FOUHs>DcX;FJ;J{KD^ z+XsFJ;f;%gw2ktXHF%Sb+M|f3#}Z3``G#C&pBG^l$FWs%GBNu|kqZVw6a-?Jsa>1d@*RlsNL33#posRtEYVhQ>26bq z+RY|XoSz(+!uP2dsQnrliT5`3)OG*$yN`b>Ho_GMBz}2-i@JAxoJ;Q-8X88&#(cpo zJnD^m;Dw61y6IP*TLmCs+5?rbOSN+J7{0!y`p44q{8?|%aNLmrHxVvCB9o__F*Vi!q`v>@W8P`*) zxs$~M4{yMt@OrV)YXpCl8lU#=9v?i%ck0CeUiY`{-sqI?263%DD=sA$bdNG1XHk_6 z0ALxJPmJ>K_WM}UJ9wb)(GewRf2!Ofitg795c1auNVSAMy5_VkNnl{08a~|EIFoe? zqcaWQ8y*=8-nD6ah820>N^l7&<`R1%J*?c)x+>m5`|!WJWsm6lrFBPgRV@KzNC-4u zq6jCvWa*3SS^5hstPnBC$uJrqb?Ecof9I)hB-d7AX=tF_D2mvA1WYO^RK$8QJypSA zZdX3m-wz2Pj{D!!>84ynnb-uQ<%~1fXg$uaTyN9rC-m6|4T+=4d@pU5PIJC_CKcQn ze>8Hsr-~fg)_oec;jaWHr@b`e+p$EbZY#e0MFt$BP}RQ>)`B~-ym-+`Y1`zxyK4z< z$^_tg_Q3c9^OpNsgb)rc;5`mC3N_B3ct`Jq_HrDs8w5XWp?DifC8?v6yyA!g(t;hh zd$o^j`{UwlH461@Bf3=}4Zv`uUHQ_^@igprgwSHhk>@93AR~JX|93F0cZlNmLzng4 zwuUL_G9%52%tmpvfNgQz(>hO7yC(=MzuUo_`s+6kK>Y5wqwGLv(Usxl%ZKIOk0W)E zWnn+JIKQ{M_a98N2x0yBAy}o-Fzy3khPLlOxQO?@X&*-(vBR(V3k=}0`^01PwbPJx zqcWb@6r7_xuGR0R$@~B`ujNhniw(n?BKaFC%y-x}E-9ud(0O16vG7Sxk}i=l^7}v6+6vGB literal 23715 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