From 4be51f846d074136aaf7e264b6febf3961c29abe Mon Sep 17 00:00:00 2001 From: Emmett Gaines Date: Mon, 9 Oct 2023 01:39:04 -0400 Subject: [PATCH] Pipe gas visuals (#78217) ## About The Pull Request Primarily this pr is all about getting pipes to have a visual display for the gas within them. A couple other things of note have been done to make that easier though: - A sprite generator that outputs to a dmi has been made for all pipe variants and the layers. This is because I didn't want to work with hundreds of pipe sprites just to do minor changes. In the future I would like to generate things like this during a github action but for the moment this is just a helper you can use for generating the pipe dmi files. - Some minor reorganization of the code in the pipe dm file. - Some doc comments on things that bothered me ## Why It's Good For The Game Gives a bit of visual feedback when working with pipes that should hopefully make people more comfortable around them if they can easily see if the pipes are empty or are filled with an unknown gas mixture. Can also serve as a warning to those keeping an eye out for things like plasma floods. :cl: ninjanomnom add: Pipes now have a colored visual display that shows their contents at a glance. /:cl: ~~The colors for gases could stand to be better, if anyone wants to suggest alternatives to what I've used please do as I put only a bare minimum of thought into each choice.~~ I've switched to using the gas colors defined in tgui constants. ![dreamseeker_2023-09-26_07-32-56](https://github.com/tgstation/tgstation/assets/1234602/0a3a5636-c9b9-4046-83b9-0d116f8232d1) --- code/controllers/subsystem/air.dm | 2 +- code/modules/admin/admin_verbs.dm | 1 + .../atmospherics/gasmixtures/gas_types.dm | 22 +++ .../atmospherics/machinery/atmosmachinery.dm | 4 +- .../atmospherics/machinery/datum_pipeline.dm | 91 ++++++++++- .../machinery/pipes/bridge_pipe.dm | 2 + .../machinery/pipes/color_adapter.dm | 4 +- .../machinery/pipes/heat_exchange/he_pipes.dm | 2 + .../machinery/pipes/heat_exchange/junction.dm | 2 + .../machinery/pipes/heat_exchange/manifold.dm | 2 + .../pipes/heat_exchange/manifold4w.dm | 2 + .../machinery/pipes/layermanifold.dm | 1 + .../atmospherics/machinery/pipes/multiz.dm | 2 + .../pipes/pipe_spritesheet_helper.dm | 148 ++++++++++++++++++ .../atmospherics/machinery/pipes/pipes.dm | 72 ++++++--- .../atmospherics/machinery/pipes/smart.dm | 2 +- .../obj/pipes_n_cables/!pipe_gas_overlays.dmi | Bin 0 -> 3745 bytes icons/obj/pipes_n_cables/!pipes_bitmask.dmi | Bin 0 -> 11754 bytes .../pipes_n_cables/pipe_template_pieces.dmi | Bin 0 -> 2625 bytes icons/obj/pipes_n_cables/pipes_bitmask.dmi | Bin 14027 -> 0 bytes tgstation.dme | 1 + tgui/packages/tgui/constants.ts | 2 +- 22 files changed, 324 insertions(+), 38 deletions(-) create mode 100644 code/modules/atmospherics/machinery/pipes/pipe_spritesheet_helper.dm create mode 100644 icons/obj/pipes_n_cables/!pipe_gas_overlays.dmi create mode 100644 icons/obj/pipes_n_cables/!pipes_bitmask.dmi create mode 100644 icons/obj/pipes_n_cables/pipe_template_pieces.dmi delete mode 100644 icons/obj/pipes_n_cables/pipes_bitmask.dmi diff --git a/code/controllers/subsystem/air.dm b/code/controllers/subsystem/air.dm index 488debb48e5..1de3f900c8c 100644 --- a/code/controllers/subsystem/air.dm +++ b/code/controllers/subsystem/air.dm @@ -452,7 +452,7 @@ SUBSYSTEM_DEF(air) border += item net.air.volume += item.volume - item.parent = net + item.replace_pipenet(item.parent, net) if(item.air_temporary) net.air.merge(item.air_temporary) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index c5b9813c554..600bc75a83d 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -230,6 +230,7 @@ GLOBAL_PROTECT(admin_verbs_debug) /client/proc/unload_ctf, /client/proc/validate_cards, /client/proc/validate_puzzgrids, + /client/proc/GeneratePipeSpritesheet, /client/proc/view_runtimes, ) GLOBAL_LIST_INIT(admin_verbs_possess, list(/proc/possess, /proc/release)) diff --git a/code/modules/atmospherics/gasmixtures/gas_types.dm b/code/modules/atmospherics/gasmixtures/gas_types.dm index ae3367ace59..060b5a12ae9 100644 --- a/code/modules/atmospherics/gasmixtures/gas_types.dm +++ b/code/modules/atmospherics/gasmixtures/gas_types.dm @@ -64,6 +64,8 @@ ///How does a single mole of this gas sell for? Formula to calculate maximum value is in code\modules\cargo\exports\large_objects.dm. Doesn't matter for roundstart gasses. var/base_value = 0 var/desc + ///RGB code for use when a generic color representing the gas is needed. Colors taken from contants.ts + var/primary_color /datum/gas/oxygen @@ -74,6 +76,7 @@ purchaseable = TRUE base_value = 0.2 desc = "The gas most life forms need to be able to survive. Also an oxidizer." + primary_color = "#0000ff" /datum/gas/nitrogen id = GAS_N2 @@ -83,6 +86,7 @@ purchaseable = TRUE base_value = 0.1 desc = "A very common gas that used to pad artifical atmospheres to habitable pressure." + primary_color = "#ffff00" /datum/gas/carbon_dioxide //what the fuck is this? id = GAS_CO2 @@ -93,6 +97,7 @@ purchaseable = TRUE base_value = 0.2 desc = "What the fuck is carbon dioxide?" + primary_color = "#808080" /datum/gas/plasma id = GAS_PLASMA @@ -104,6 +109,7 @@ rarity = 800 base_value = 1.5 desc = "A flammable gas with many other curious properties. It's research is one of NT's primary objective." + primary_color = "#ffc0cb" /datum/gas/water_vapor id = GAS_WATER_VAPOR @@ -116,6 +122,7 @@ purchaseable = TRUE base_value = 0.5 desc = "Water, in gas form. Makes things slippery." + primary_color = "#b0c4de" /datum/gas/hypernoblium id = GAS_HYPER_NOBLIUM @@ -127,6 +134,7 @@ rarity = 50 base_value = 2.5 desc = "The most noble gas of them all. High quantities of hyper-noblium actively prevents reactions from occuring." + primary_color = "#008080" /datum/gas/nitrous_oxide id = GAS_N2O @@ -140,6 +148,7 @@ purchaseable = TRUE base_value = 1.5 desc = "Causes drowsiness, euphoria, and eventually unconsciousness." + primary_color = "#ffe4c4" /datum/gas/nitrium id = GAS_NITRIUM @@ -152,6 +161,7 @@ rarity = 1 base_value = 6 desc = "An experimental performance enhancing gas. Nitrium can have amplified effects as more of it gets into your bloodstream." + primary_color = "#a52a2a" /datum/gas/tritium id = GAS_TRITIUM @@ -164,6 +174,7 @@ rarity = 300 base_value = 2.5 desc = "A highly flammable and radioctive gas." + primary_color = "#32cd32" /datum/gas/bz id = GAS_BZ @@ -175,6 +186,7 @@ purchaseable = TRUE base_value = 1.5 desc = "A powerful hallucinogenic nerve agent able to induce cognitive damage." + primary_color = "#9370db" /datum/gas/pluoxium id = GAS_PLUOXIUM @@ -184,6 +196,7 @@ rarity = 200 base_value = 2.5 desc = "A gas that could supply even more oxygen to the bloodstream when inhaled, without being an oxidizer." + primary_color = "#7b68ee" /datum/gas/miasma id = GAS_MIASMA @@ -195,6 +208,7 @@ rarity = 250 base_value = 1 desc = "Not necessarily a gas, miasma refers to biological pollutants found in the atmosphere." + primary_color = "#808000" /datum/gas/freon id = GAS_FREON @@ -207,6 +221,7 @@ rarity = 10 base_value = 5 desc = "A coolant gas. Mainly used for it's endothermic reaction with oxygen." + primary_color = "#afeeee" /datum/gas/hydrogen id = GAS_HYDROGEN @@ -217,6 +232,7 @@ rarity = 600 base_value = 1 desc = "A highly flammable gas." + primary_color = "#ffffff" /datum/gas/healium id = GAS_HEALIUM @@ -228,6 +244,7 @@ rarity = 300 base_value = 5.5 desc = "Causes deep, regenerative sleep." + primary_color = "#fa8072" /datum/gas/proto_nitrate id = GAS_PROTO_NITRATE @@ -239,6 +256,7 @@ rarity = 200 base_value = 2.5 desc = "A very volatile gas that reacts differently with various gases." + primary_color = "#adff2f" /datum/gas/zauker id = GAS_ZAUKER @@ -250,6 +268,7 @@ rarity = 1 base_value = 7 desc = "A highly toxic gas, it's production is highly regulated on top of being difficult. It also breaks down when in contact with nitrogen." + primary_color = "#006400" /datum/gas/halon id = GAS_HALON @@ -261,6 +280,7 @@ rarity = 300 base_value = 4 desc = "A potent fire supressant. Removes oxygen from high temperature fires and cools down the area" + primary_color = "#800080" /datum/gas/helium id = GAS_HELIUM @@ -270,6 +290,7 @@ rarity = 50 base_value = 3.5 desc = "A very inert gas produced by the fusion of hydrogen and it's derivatives." + primary_color = "#f0f8ff" /datum/gas/antinoblium id = GAS_ANTINOBLIUM @@ -282,6 +303,7 @@ rarity = 1 base_value = 10 desc = "We still don't know what it does, but it sells for a lot." + primary_color = "#800000" /obj/effect/overlay/gas icon = 'icons/effects/atmospherics.dmi' diff --git a/code/modules/atmospherics/machinery/atmosmachinery.dm b/code/modules/atmospherics/machinery/atmosmachinery.dm index 417e5055de7..62c2b562d51 100644 --- a/code/modules/atmospherics/machinery/atmosmachinery.dm +++ b/code/modules/atmospherics/machinery/atmosmachinery.dm @@ -352,9 +352,9 @@ return /** - * Similar to set_pipenet() but instead of setting a network to a pipeline, it replaces the old pipeline with a new one, called by Merge() in datum_pipeline.dm + * Replaces the connection to the old_pipenet with the new_pipenet */ -/obj/machinery/atmospherics/proc/replace_pipenet() +/obj/machinery/atmospherics/proc/replace_pipenet(datum/pipeline/old_pipenet, datum/pipeline/new_pipenet) return /** diff --git a/code/modules/atmospherics/machinery/datum_pipeline.dm b/code/modules/atmospherics/machinery/datum_pipeline.dm index b8ad06e25ab..77a29b5461d 100644 --- a/code/modules/atmospherics/machinery/datum_pipeline.dm +++ b/code/modules/atmospherics/machinery/datum_pipeline.dm @@ -1,5 +1,7 @@ /datum/pipeline + /// The gases contained within this pipeline var/datum/gas_mixture/air + /// The gas_mixtures of objects directly connected to this pipeline var/list/datum/gas_mixture/other_airs var/list/obj/machinery/atmospherics/pipe/members @@ -8,6 +10,10 @@ /// We're essentially caching this to avoid needing to filter over it when processing our machines var/list/obj/machinery/atmospherics/components/require_custom_reconcilation + /// The weighted color blend of the gas mixture in this pipeline + var/gasmix_color + /// A named list of icon_file:overlay_object that gets automatically colored when the gasmix_color updates + var/list/gas_visuals ///Should we equalize air amoung all our members? var/update = TRUE @@ -19,6 +25,7 @@ members = list() other_atmos_machines = list() require_custom_reconcilation = list() + gas_visuals = list() SSair.networks += src /datum/pipeline/Destroy() @@ -28,7 +35,7 @@ if(air?.volume) temporarily_store_air() for(var/obj/machinery/atmospherics/pipe/considered_pipe in members) - considered_pipe.parent = null + considered_pipe.replace_pipenet(considered_pipe.parent, null) if(QDELETED(considered_pipe)) continue SSair.add_to_rebuild_queue(considered_pipe) @@ -42,6 +49,13 @@ reconcile_air() //Only react if the mix has changed, and don't keep updating if it hasn't update = air.react(src) + CalculateGasmixColor(air) + +/datum/pipeline/proc/set_air(datum/gas_mixture/new_air) + if(new_air == air) + return + air = new_air + CalculateGasmixColor(air) ///Preps a pipeline for rebuilding, insterts it into the rebuild queue /datum/pipeline/proc/build_pipeline(obj/machinery/atmospherics/base) @@ -52,13 +66,13 @@ volume = considered_pipe.volume members += considered_pipe if(considered_pipe.air_temporary) - air = considered_pipe.air_temporary + set_air(considered_pipe.air_temporary) considered_pipe.air_temporary = null else add_machinery_member(base) if(!air) - air = new + set_air(new /datum/gas_mixture) air.volume = volume SSair.add_to_expansion(src, base) @@ -71,13 +85,13 @@ volume = considered_pipe.volume members += considered_pipe if(considered_pipe.air_temporary) - air = considered_pipe.air_temporary + set_air(considered_pipe.air_temporary) considered_pipe.air_temporary = null else add_machinery_member(base) if(!air) - air = new + set_air(new /datum/gas_mixture) var/list/possible_expansions = list(base) while(possible_expansions.len) for(var/obj/machinery/atmospherics/borderline in possible_expansions) @@ -105,7 +119,7 @@ possible_expansions += item volume += item.volume - item.parent = src + item.replace_pipenet(item.parent, src) if(item.air_temporary) air.merge(item.air_temporary) @@ -142,7 +156,7 @@ var/obj/machinery/atmospherics/pipe/reference_pipe = reference_device if(reference_pipe.parent) merge(reference_pipe.parent) - reference_pipe.parent = src + reference_pipe.replace_pipenet(reference_pipe.parent, src) var/list/adjacent = reference_pipe.pipeline_expansion() for(var/obj/machinery/atmospherics/pipe/adjacent_pipe in adjacent) if(adjacent_pipe.parent == src) @@ -159,7 +173,7 @@ air.volume += parent_pipeline.air.volume members.Add(parent_pipeline.members) for(var/obj/machinery/atmospherics/pipe/reference_pipe in parent_pipeline.members) - reference_pipe.parent = src + reference_pipe.replace_pipenet(reference_pipe.parent, src) air.merge(parent_pipeline.air) for(var/obj/machinery/atmospherics/components/reference_component in parent_pipeline.other_atmos_machines) reference_component.replace_pipenet(parent_pipeline, src) @@ -280,3 +294,64 @@ //Update individual gas_mixtures by volume ratio for(var/datum/gas_mixture/gas_mixture as anything in gas_mixture_list) gas_mixture.copy_from_ratio(total_gas_mixture, gas_mixture.volume / volume_sum) + +//-------------------- +// GAS VISUALS STUFF +// +// If I could have gotten layer filters to obey the RESET_COLOR appearance flag I would have used that here +// so that only a single overlay object needs to exist for all pipelines per icon file. It shouldn't be too +// hard to switch over to that if it becomes possible in the future or some other equivalent feature is added. + +/** + * Used to create and/or get the gas visual overlay created using the given icon file. + * The color is automatically kept up to date and expected to be used as a vis_contents object. + */ +/datum/pipeline/proc/GetGasVisual(icon/icon_file) + if(gas_visuals[icon_file]) + return gas_visuals[icon_file] + + var/obj/effect/abstract/new_overlay = new + new_overlay.icon = icon_file + new_overlay.appearance_flags = RESET_COLOR | KEEP_APART + new_overlay.vis_flags = VIS_INHERIT_ICON_STATE | VIS_INHERIT_LAYER | VIS_INHERIT_PLANE | VIS_INHERIT_ID + new_overlay.color = gasmix_color + + gas_visuals[icon_file] = new_overlay + return new_overlay + +/// Called when the gasmix color has changed and the gas visuals need to be updated. +/datum/pipeline/proc/UpdateGasVisuals() + for(var/icon/source as anything in gas_visuals) + var/obj/effect/abstract/overlay = gas_visuals[source] + animate(overlay, time=5, color=gasmix_color) + +/// After updating, this proc handles looking at the new gas mixture and blends the colors together according to percentage of the gas mix. +/datum/pipeline/proc/CalculateGasmixColor(datum/gas_mixture/source) + SIGNAL_HANDLER + + var/current_weight = 0 + var/current_color + for(var/datum/gas/gas_path as anything in air.gases) + var/gas_weight = air.gases[gas_path][MOLES] + if(!gas_weight) + continue + var/gas_color = RGBtoHSV(initial(gas_path.primary_color)) + current_weight += gas_weight + if(!current_color) + current_color = gas_color + else + current_color = BlendHSV(current_color, gas_color, gas_weight / current_weight) + + if(!current_color) + current_color = "#000000" + else + // Empty weight is prety much arbitrary, just tuned to make the color change from black reasonably quickly without hitting max color immediately + var/empty_weight = (air.volume * 1.5 - current_weight) / 10 + if(empty_weight > 0) + current_color = BlendHSV("#000000", current_color, current_weight / (empty_weight + current_weight)) + + current_color = HSVtoRGB(current_color) + + if(gasmix_color != current_color) + gasmix_color = current_color + UpdateGasVisuals() diff --git a/code/modules/atmospherics/machinery/pipes/bridge_pipe.dm b/code/modules/atmospherics/machinery/pipes/bridge_pipe.dm index 9cda298ccd4..472cb3ed203 100644 --- a/code/modules/atmospherics/machinery/pipes/bridge_pipe.dm +++ b/code/modules/atmospherics/machinery/pipes/bridge_pipe.dm @@ -13,6 +13,8 @@ construction_type = /obj/item/pipe/binary pipe_state = "bridge_center" + has_gas_visuals = FALSE + /obj/machinery/atmospherics/pipe/bridge_pipe/set_init_directions() switch(dir) if(NORTH, SOUTH) diff --git a/code/modules/atmospherics/machinery/pipes/color_adapter.dm b/code/modules/atmospherics/machinery/pipes/color_adapter.dm index 02c550fd558..06812aaa649 100644 --- a/code/modules/atmospherics/machinery/pipes/color_adapter.dm +++ b/code/modules/atmospherics/machinery/pipes/color_adapter.dm @@ -16,6 +16,8 @@ paintable = FALSE hide = FALSE + has_gas_visuals = FALSE + ///cache for the icons var/static/list/mutable_appearance/center_cache = list() @@ -34,7 +36,7 @@ . = ..() var/mutable_appearance/center = center_cache["[piping_layer]"] if(!center) - center = mutable_appearance(icon, "adapter_center") + center = mutable_appearance(initial(icon), "adapter_center") PIPING_LAYER_DOUBLE_SHIFT(center, piping_layer) center_cache["[piping_layer]"] = center . += center diff --git a/code/modules/atmospherics/machinery/pipes/heat_exchange/he_pipes.dm b/code/modules/atmospherics/machinery/pipes/heat_exchange/he_pipes.dm index fd43e315527..72c9c742781 100644 --- a/code/modules/atmospherics/machinery/pipes/heat_exchange/he_pipes.dm +++ b/code/modules/atmospherics/machinery/pipes/heat_exchange/he_pipes.dm @@ -8,6 +8,8 @@ hide = FALSE + has_gas_visuals = FALSE + /obj/machinery/atmospherics/pipe/heat_exchanging/Initialize(mapload) . = ..() diff --git a/code/modules/atmospherics/machinery/pipes/heat_exchange/junction.dm b/code/modules/atmospherics/machinery/pipes/heat_exchange/junction.dm index 00ef058333a..a6bfcc533d2 100644 --- a/code/modules/atmospherics/machinery/pipes/heat_exchange/junction.dm +++ b/code/modules/atmospherics/machinery/pipes/heat_exchange/junction.dm @@ -15,6 +15,8 @@ construction_type = /obj/item/pipe/directional pipe_state = "junction" + has_gas_visuals = FALSE + /obj/machinery/atmospherics/pipe/heat_exchanging/junction/set_init_directions() switch(dir) if(NORTH, SOUTH) diff --git a/code/modules/atmospherics/machinery/pipes/heat_exchange/manifold.dm b/code/modules/atmospherics/machinery/pipes/heat_exchange/manifold.dm index e340d7f54cc..5841486ba9b 100644 --- a/code/modules/atmospherics/machinery/pipes/heat_exchange/manifold.dm +++ b/code/modules/atmospherics/machinery/pipes/heat_exchange/manifold.dm @@ -16,6 +16,8 @@ construction_type = /obj/item/pipe/trinary pipe_state = "he_manifold" + has_gas_visuals = FALSE + /obj/machinery/atmospherics/pipe/heat_exchanging/manifold/set_init_directions() initialize_directions = ALL_CARDINALS initialize_directions &= ~dir diff --git a/code/modules/atmospherics/machinery/pipes/heat_exchange/manifold4w.dm b/code/modules/atmospherics/machinery/pipes/heat_exchange/manifold4w.dm index 03ef32b4354..83870ee210b 100644 --- a/code/modules/atmospherics/machinery/pipes/heat_exchange/manifold4w.dm +++ b/code/modules/atmospherics/machinery/pipes/heat_exchange/manifold4w.dm @@ -15,6 +15,8 @@ construction_type = /obj/item/pipe/quaternary pipe_state = "he_manifold4w" + has_gas_visuals = FALSE + /obj/machinery/atmospherics/pipe/heat_exchanging/manifold4w/set_init_directions() initialize_directions = initial(initialize_directions) diff --git a/code/modules/atmospherics/machinery/pipes/layermanifold.dm b/code/modules/atmospherics/machinery/pipes/layermanifold.dm index bdfbda9ba6c..09c8a3a9367 100644 --- a/code/modules/atmospherics/machinery/pipes/layermanifold.dm +++ b/code/modules/atmospherics/machinery/pipes/layermanifold.dm @@ -12,6 +12,7 @@ construction_type = /obj/item/pipe/binary pipe_state = "manifoldlayer" paintable = TRUE + has_gas_visuals = FALSE ///Reference to all the nodes in the front var/list/front_nodes diff --git a/code/modules/atmospherics/machinery/pipes/multiz.dm b/code/modules/atmospherics/machinery/pipes/multiz.dm index cfc24ab8291..7e14b8a9806 100644 --- a/code/modules/atmospherics/machinery/pipes/multiz.dm +++ b/code/modules/atmospherics/machinery/pipes/multiz.dm @@ -15,6 +15,8 @@ construction_type = /obj/item/pipe/directional pipe_state = "multiz" + has_gas_visuals = FALSE + ///Our central icon var/mutable_appearance/center = null ///The pipe icon diff --git a/code/modules/atmospherics/machinery/pipes/pipe_spritesheet_helper.dm b/code/modules/atmospherics/machinery/pipes/pipe_spritesheet_helper.dm new file mode 100644 index 00000000000..9642442a973 --- /dev/null +++ b/code/modules/atmospherics/machinery/pipes/pipe_spritesheet_helper.dm @@ -0,0 +1,148 @@ +/client/proc/GeneratePipeSpritesheet() + set name = "Generate Pipe Spritesheet" + set category = "Debug" + + var/datum/pipe_icon_generator/generator = new + generator.Start() + fcopy(generator.generated_icons, "icons/obj/pipes_n_cables/!pipes_bitmask.dmi") + + generator.Start("-gas") + fcopy(generator.generated_icons, "icons/obj/pipes_n_cables/!pipe_gas_overlays.dmi") + +/datum/pipe_icon_generator + var/static/icon/template_pieces = icon('icons/obj/pipes_n_cables/pipe_template_pieces.dmi') + var/static/list/icon/damage_masks = list( + "[NORTH]"=icon('icons/obj/pipes_n_cables/pipe_template_pieces.dmi', "damage_mask", NORTH), + "[EAST]"=icon('icons/obj/pipes_n_cables/pipe_template_pieces.dmi', "damage_mask", EAST), + "[SOUTH]"=icon('icons/obj/pipes_n_cables/pipe_template_pieces.dmi', "damage_mask", SOUTH), + "[WEST]"=icon('icons/obj/pipes_n_cables/pipe_template_pieces.dmi', "damage_mask", WEST), + ) + + var/icon/generated_icons + +/datum/pipe_icon_generator/proc/Start(icon_state_suffix="") + var/list/outputs = list() + for(var/layer in 1 to 5) + // Since dirs are bitflags, if we want to iterate over every possible direction + // combination we just need to iterate over every number that can be contained in 4 bits. + // + // I wrote this all the hard way originally >.> + for(var/combined_dirs in 1 to 15) + switch(combined_dirs) + if(NORTH, EAST, SOUTH, WEST) + continue + + outputs += GeneratePipeDir(icon_state_suffix, layer, combined_dirs) + + generated_icons = icon('icons/testing/greyscale_error.dmi') + for(var/icon/generated_icon as anything in outputs) + var/pending_icon_state = outputs[generated_icon] + generated_icons.Insert(generated_icon, pending_icon_state) + +/datum/pipe_icon_generator/proc/GeneratePipeDir(icon_state_suffix, layer, combined_dirs) + var/list/output + + switch(combined_dirs) + if(NORTH | SOUTH, EAST | WEST) + output = GeneratePipeStraight(icon_state_suffix, layer, combined_dirs) + if(NORTH | EAST, SOUTH | EAST, SOUTH | WEST, NORTH | WEST) + output = GeneratePipeElbow(icon_state_suffix, layer, combined_dirs) + if(NORTH | EAST | SOUTH, EAST | SOUTH | WEST, SOUTH | WEST | NORTH, WEST | NORTH | EAST) + output = GeneratePipeTJunction(icon_state_suffix, layer, combined_dirs) + if(NORTH | EAST | SOUTH | WEST) + output = GeneratePipeCross(icon_state_suffix, layer, combined_dirs) + + var/shift_amount = (layer - 1) * 5 + for(var/icon/sprite as anything in output) + if(shift_amount > 0) + sprite.Shift(EAST, shift_amount) + sprite.Shift(NORTH, shift_amount) + sprite.Crop(33, 33, 64, 64) + + return output + +/// Generates all variants of damaged pipe from a given icon and the dirs that can be broken +/datum/pipe_icon_generator/proc/GenerateDamaged(icon/working, layer, dirs, x_offset=1, y_offset=1) + var/outputs = list() + var/completed = list() + for(var/combined_dirs in 1 to 15) + combined_dirs &= dirs + + var/completion_key = "[combined_dirs]" + if(completed[completion_key] || (combined_dirs == NONE)) + continue + completed[completion_key] = TRUE + + var/icon/damaged = icon(working) + for(var/i in 0 to 3) + var/dir = 1 << i + if(!(combined_dirs & dir)) + continue + var/icon/damage_mask = damage_masks["[dir]"] + damaged.Blend(damage_mask, ICON_MULTIPLY, x_offset, y_offset) + + var/icon_state_dirs = (dirs & ~combined_dirs) | CARDINAL_TO_SHORTPIPES(combined_dirs) + outputs[damaged] = "[icon_state_dirs]_[layer]" + return outputs + + +/datum/pipe_icon_generator/proc/GeneratePipeStraight(icon_state_suffix, layer, combined_dirs) + var/list/output = list() + var/north_or_east = combined_dirs & (NORTH | EAST) + var/icon/working = icon(template_pieces, "straight[icon_state_suffix]", north_or_east) + + output[working] = "[combined_dirs]_[layer]" + + var/offset = 1 + (5-layer) * 2 + switch(combined_dirs) + if(NORTH | SOUTH) + output += GenerateDamaged(working, layer, combined_dirs, y_offset=offset) + if(EAST | WEST) + output += GenerateDamaged(working, layer, combined_dirs, x_offset=offset) + + return output + +/datum/pipe_icon_generator/proc/GeneratePipeElbow(icon_state_suffix, layer, combined_dirs) + var/list/output = list() + var/icon/working + switch(combined_dirs) + if(NORTH | EAST) + working = icon(template_pieces, "elbow[icon_state_suffix]", NORTH) + if(NORTH | WEST) + working = icon(template_pieces, "elbow[icon_state_suffix]", WEST) + if(SOUTH | EAST) + working = icon(template_pieces, "elbow[icon_state_suffix]", EAST) + if(SOUTH | WEST) + working = icon(template_pieces, "elbow[icon_state_suffix]", SOUTH) + + output[working] = "[combined_dirs]_[layer]" + output += GenerateDamaged(working, layer, combined_dirs) + + return output + +/datum/pipe_icon_generator/proc/GeneratePipeTJunction(icon_state_suffix, layer, combined_dirs) + var/list/output = list() + var/icon/working + switch(combined_dirs) + if(WEST | NORTH | EAST) + working = icon(template_pieces, "tee[icon_state_suffix]", NORTH) + if(NORTH | EAST | SOUTH) + working = icon(template_pieces, "tee[icon_state_suffix]", EAST) + if(EAST | SOUTH | WEST) + working = icon(template_pieces, "tee[icon_state_suffix]", SOUTH) + if(SOUTH | WEST | NORTH) + working = icon(template_pieces, "tee[icon_state_suffix]", WEST) + + output[working] = "[combined_dirs]_[layer]" + output += GenerateDamaged(working, layer, combined_dirs) + + return output + +/datum/pipe_icon_generator/proc/GeneratePipeCross(icon_state_suffix, layer, combined_dirs) + var/list/output = list() + var/icon/working = icon(template_pieces, "cross[icon_state_suffix]") + + output[working] = "[combined_dirs]_[layer]" + output += GenerateDamaged(working, layer, combined_dirs) + + return output diff --git a/code/modules/atmospherics/machinery/pipes/pipes.dm b/code/modules/atmospherics/machinery/pipes/pipes.dm index c1c128c2e80..c94caf31174 100644 --- a/code/modules/atmospherics/machinery/pipes/pipes.dm +++ b/code/modules/atmospherics/machinery/pipes/pipes.dm @@ -1,15 +1,22 @@ /obj/machinery/atmospherics/pipe - icon = 'icons/obj/pipes_n_cables/pipes_bitmask.dmi' + icon = 'icons/obj/pipes_n_cables/!pipes_bitmask.dmi' damage_deflection = 12 - var/datum/gas_mixture/air_temporary //used when reconstructing a pipeline that broke + /// Temporary holder for gases in the absence of a pipeline + var/datum/gas_mixture/air_temporary + + /// The gas capacity this pipe contributes to a pipeline var/volume = 0 use_power = NO_POWER_USE can_unwrench = 1 + /// The pipeline this pipe is a member of var/datum/pipeline/parent = null paintable = TRUE + /// Determines if this pipe will be given gas visuals + var/has_gas_visuals = TRUE + //Buckling can_buckle = TRUE buckle_requires_restraints = TRUE @@ -27,6 +34,23 @@ if(hide) AddElement(/datum/element/undertile, TRAIT_T_RAY_VISIBLE) //if changing this, change the subtypes RemoveElements too, because thats how bespoke works +/obj/machinery/atmospherics/pipe/Destroy() + QDEL_NULL(parent) + + releaseAirToTurf() + + var/turf/local_turf = loc + for(var/obj/machinery/meter/meter in local_turf) + if(meter.target != src) + continue + var/obj/item/pipe_meter/meter_object = new (local_turf) + meter.transfer_fingerprints_to(meter_object) + qdel(meter) + return ..() + +//----------------- +// PIPENET STUFF + /obj/machinery/atmospherics/pipe/nullify_node(i) var/obj/machinery/atmospherics/old_node = nodes[i] . = ..() @@ -39,7 +63,7 @@ /obj/machinery/atmospherics/pipe/get_rebuild_targets() if(!QDELETED(parent)) return - parent = new + replace_pipenet(parent, new /datum/pipeline) return list(parent) /obj/machinery/atmospherics/pipe/proc/releaseAirToTurf() @@ -73,25 +97,33 @@ /obj/machinery/atmospherics/pipe/return_pipenet() return parent -/obj/machinery/atmospherics/pipe/set_pipenet(datum/pipeline/pipenet_to_set) - parent = pipenet_to_set +/obj/machinery/atmospherics/pipe/replace_pipenet(datum/pipeline/old_pipenet, datum/pipeline/new_pipenet) + if(parent && has_gas_visuals) + vis_contents -= parent.GetGasVisual('icons/obj/pipes_n_cables/!pipe_gas_overlays.dmi') -/obj/machinery/atmospherics/pipe/Destroy() - QDEL_NULL(parent) + parent = new_pipenet - releaseAirToTurf() + if(parent && has_gas_visuals) // null is a valid argument here + vis_contents += parent.GetGasVisual('icons/obj/pipes_n_cables/!pipe_gas_overlays.dmi') - var/turf/local_turf = loc - for(var/obj/machinery/meter/meter in local_turf) - if(meter.target != src) - continue - var/obj/item/pipe_meter/meter_object = new (local_turf) - meter.transfer_fingerprints_to(meter_object) - qdel(meter) +/obj/machinery/atmospherics/pipe/return_pipenets() + . = list(parent) + +//-------------------- +// APPEARANCE STUFF + +/obj/machinery/atmospherics/pipe/update_icon() + update_pipe_icon() + update_layer() return ..() /obj/machinery/atmospherics/pipe/proc/update_pipe_icon() - icon = 'icons/obj/pipes_n_cables/pipes_bitmask.dmi' + switch(initialize_directions) + if(NORTH, EAST, SOUTH, WEST) // Pipes with only a single connection aren't handled by this system + icon = null + return + else + icon = 'icons/obj/pipes_n_cables/!pipes_bitmask.dmi' var/connections = NONE var/bitfield = NONE for(var/i in 1 to device_type) @@ -104,11 +136,6 @@ bitfield |= CARDINAL_TO_SHORTPIPES(initialize_directions & ~connections) icon_state = "[bitfield]_[piping_layer]" -/obj/machinery/atmospherics/pipe/update_icon() - update_pipe_icon() - update_layer() - return ..() - /obj/machinery/atmospherics/proc/update_node_icon() for(var/i in 1 to device_type) if(!nodes[i]) @@ -116,9 +143,6 @@ var/obj/machinery/atmospherics/current_node = nodes[i] current_node.update_icon() -/obj/machinery/atmospherics/pipe/return_pipenets() - . = list(parent) - /obj/machinery/atmospherics/pipe/paint(paint_color) if(paintable) add_atom_colour(paint_color, FIXED_COLOUR_PRIORITY) diff --git a/code/modules/atmospherics/machinery/pipes/smart.dm b/code/modules/atmospherics/machinery/pipes/smart.dm index 95e5839469f..7c530bace5f 100644 --- a/code/modules/atmospherics/machinery/pipes/smart.dm +++ b/code/modules/atmospherics/machinery/pipes/smart.dm @@ -14,7 +14,7 @@ GLOBAL_LIST_INIT(atmos_components, typecacheof(list(/obj/machinery/atmospherics) var/connections = NONE /obj/machinery/atmospherics/pipe/smart/update_pipe_icon() - icon = 'icons/obj/pipes_n_cables/pipes_bitmask.dmi' + icon = 'icons/obj/pipes_n_cables/!pipes_bitmask.dmi' //find all directions this pipe is connected with other nodes connections = NONE diff --git a/icons/obj/pipes_n_cables/!pipe_gas_overlays.dmi b/icons/obj/pipes_n_cables/!pipe_gas_overlays.dmi new file mode 100644 index 0000000000000000000000000000000000000000..0262adcaeb2419380c299ac30f82006eb35c1d18 GIT binary patch literal 3745 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 N0 z#PYY_zUf4yyvBMDKqZF(sN#WY&AB)F@$4IJ@B?m~^(gou7sKR+Rr3|(=L%E1jYYp7 zL)zkIyBxd`riUJ>Ur|7-9F3fm$+P^Hp{YLvDo~G5v1K0H;HJOPp;{-IuK@TI0&>`EdpyJ~-Es2a7xdNGbm~ zU_;8m5dLtm|2m|p#*fSNbapt+4?}A#dz=J_5|scWJ|+53)9i4W)4*`Ri5fHNN&iIX zhV2OMdw}LV1Vj?HgbmPXH_S$}*!}jPA?LxPt`j}lI^4(wd^O7VoNu?{m~bueTPp29 zj|NlHr`c$pGwDlPet@-_#IIb5Aid>hmP;I?AV7HZ&ZFj^7K_Z)_gfEw)gFX8As{90 zS7~?8N75jEn@1eHYbkMnPT!KmG{ZOQ6mB3U?ehkNmogf8}c%ef(V;uhc0;e zs_5}PX7;u(fwNbRr@>BO-25vuF4aI6%IWv93KdAH{<*1H`sm$|t*CzMTpRQr3U9!& z_6R4g{r-Utt~}3PP=pQkT;OF2qa)WnOwV}Tl!n!6 z@j@{%@tXnYpaMXfj?8=8Z|+NDSsq--G^gpVY-9o&yr1etd>0u)#}#ypChDI_ny)w6 zQ$1vZSd;UVno`{LY-sfgAWvf^U4p>8hTY@)AB_ggts#`v9Z0CQHMiRzQnaj_z}g`y zf)RYa$x+Awa)Wg((b7Nmg^5QoRK**beqa43y=ZN(f(7M+`lU?zQHi@!;EohFWq>&} zP4=+G2J4&L!PD@&a*{qWTE%u-Y^4lnx`Cs@Dbp_?4c)>iS#yBpejc*QpugA1oy@f# za~w*r4gK}*Mb(P9`bcT6hMTp4f0H5cW&1Bw9j7nA<7O} z?_Ke|+LPk8VcP5G5rJvM{6%y7jO=XuJPSeD0TU%N(4}^bIFb2mDSwVhn&lqaC?AI| z=At=_0GR%@uo|I)=#R-?JE2+)@N3~?H3S{vk=J3tshZT8CHjHhm8t>S_wwiqrsFAB zH_kjc%9HifZba$?c+@V%oghTgPFM!D{0+!kF15?V2^_zVND_%-Q~^iDUL0C5DAOfG zYo%xT&R?wjGC^IOpI1b4il~-hoTLSym z!A|_$>)?U2z!8wAXglqmwo@rl-lG)w$nF8#YB&1P$f(@7=VQEMu~Dba0LC%nyAb>(w97Nn)8(%{i&r0l zFa2GqEWF@g_0E%OtSY|HXT7n(FL3NnwUX;)ZGNA4zE8SIF@Dryy|gO$f`5!I`qV?M zsM{qH`=~;deQ4n;S)V~ej1F3@H7RAuYHaVTBsP73QQ|L>lqI~LO;iCx?%p)Z?Kd|C zL%KhFtsNJDofcV-h&|Z!6LKIz=~jl6mA*-9QlY>c%V|!{Z0WyB{jlvA;X%(ZAp4mY zCCusgF99`^0xRd0#S1L^VRsxX+kuLLB%r4f?UEIZ3r4Eq7}0X=R`e0cWJ1y)&W9~{ z&v?aH#YD;m3(8I@BvEDsB9+dZle7B&{JU?-!ruP5AsS+I)EfACGYDqoU|Dn8=hpuL DllBOP literal 0 HcmV?d00001 diff --git a/icons/obj/pipes_n_cables/pipe_template_pieces.dmi b/icons/obj/pipes_n_cables/pipe_template_pieces.dmi new file mode 100644 index 0000000000000000000000000000000000000000..d0d2f7ff7bb809e4ca62c04b0a78ac5618be4bf3 GIT binary patch literal 2625 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 Xq;ISuW%&OO)7s&