diff --git a/code/__DEFINES/dcs/signals/signals_painting.dm b/code/__DEFINES/dcs/signals/signals_painting.dm new file mode 100644 index 0000000000..4f9201ec8f --- /dev/null +++ b/code/__DEFINES/dcs/signals/signals_painting.dm @@ -0,0 +1,7 @@ +// signals for painting canvases, tools and the /datum/component/palette component + +///from base of /item/proc/set_painting_tool_color(): (chosen_color) +#define COMSIG_PAINTING_TOOL_SET_COLOR "painting_tool_set_color" + +/// from base of /item/canvas/ui_data(): (data) +#define COMSIG_PAINTING_TOOL_GET_ADDITIONAL_DATA "painting_tool_get_data" diff --git a/code/_globalvars/lists/maintenance_loot.dm b/code/_globalvars/lists/maintenance_loot.dm index b41b7356a8..cec25c0047 100644 --- a/code/_globalvars/lists/maintenance_loot.dm +++ b/code/_globalvars/lists/maintenance_loot.dm @@ -49,6 +49,8 @@ GLOBAL_LIST_INIT(maintenance_loot, list( /obj/item/radio/off = 2, /obj/item/t_scanner = 5, /obj/item/airlock_painter = 1, + /obj/item/airlock_painter/decal = 1, + /obj/item/airlock_painter/decal/tile = 1, /obj/item/stack/cable_coil/random = 4, /obj/item/stack/cable_coil/random/five = 6, /obj/item/stack/medical/suture = 1, diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 1e41b726df..6beb8319ec 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -604,6 +604,16 @@ animate(time = 1) animate(alpha = 0, time = 3, easing = CIRCULAR_EASING|EASE_OUT) +/// Common proc used by painting tools like spraycans and palettes that can access the entire 24 bits color space. +/obj/item/proc/pick_painting_tool_color(mob/user, default_color) + var/chosen_color = input(user,"Pick new color", "[src]", default_color) as color|null + if(!chosen_color || QDELETED(src) || IS_DEAD_OR_INCAP(user) || !user.is_holding(src)) + return + set_painting_tool_color(chosen_color) + +/obj/item/proc/set_painting_tool_color(chosen_color) + SEND_SIGNAL(src, COMSIG_PAINTING_TOOL_SET_COLOR, chosen_color) + /atom/movable/vv_get_dropdown() . = ..() . += "" diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm index aa888fe67e..0b31ecdf9f 100644 --- a/code/game/machinery/doors/airlock.dm +++ b/code/game/machinery/doors/airlock.dm @@ -1262,81 +1262,29 @@ locked = TRUE return - -/obj/machinery/door/airlock/proc/change_paintjob(obj/item/airlock_painter/W, mob/user) - if(!W.can_use(user)) +// gets called when a player uses an airlock painter on this airlock +/obj/machinery/door/airlock/proc/change_paintjob(obj/item/airlock_painter/painter, mob/user) + if((!in_range(src, user) && loc != user) || !painter.can_use(user)) // user should be adjacent to the airlock, and the painter should have a toner cartridge that isn't empty return - var/list/optionlist - if(airlock_material == "glass") - optionlist = list("Standard", "Public", "Engineering", "Atmospherics", "Security", "Command", "Medical", "Research", "Science", "Virology", "Mining", "Maintenance", "External", "External Maintenance") - else - optionlist = list("Standard", "Public", "Engineering", "Atmospherics", "Security", "Command", "Medical", "Research", "Freezer", "Science", "Virology", "Mining", "Maintenance", "External", "External Maintenance") - - var/paintjob = input(user, "Please select a paintjob for this airlock.") in optionlist - if((!in_range(src, usr) && src.loc != usr) || !W.use_paint(user)) + // reads from the airlock painter's `available paintjob` list. lets the player choose a paint option, or cancel painting + var/current_paintjob = tgui_input_list(user, "Paintjob for this airlock", "Customize", sortList(painter.available_paint_jobs)) + if(isnull(current_paintjob)) // if the user clicked cancel on the popup, return return - switch(paintjob) - if("Standard") - icon = 'icons/obj/doors/airlocks/station/public.dmi' - overlays_file = 'icons/obj/doors/airlocks/station/overlays.dmi' - assemblytype = /obj/structure/door_assembly - if("Public") - icon = 'icons/obj/doors/airlocks/station2/glass.dmi' - overlays_file = 'icons/obj/doors/airlocks/station2/overlays.dmi' - assemblytype = /obj/structure/door_assembly/door_assembly_public - if("Engineering") - icon = 'icons/obj/doors/airlocks/station/engineering.dmi' - overlays_file = 'icons/obj/doors/airlocks/station/overlays.dmi' - assemblytype = /obj/structure/door_assembly/door_assembly_eng - if("Atmospherics") - icon = 'icons/obj/doors/airlocks/station/atmos.dmi' - overlays_file = 'icons/obj/doors/airlocks/station/overlays.dmi' - assemblytype = /obj/structure/door_assembly/door_assembly_atmo - if("Security") - icon = 'icons/obj/doors/airlocks/station/security.dmi' - overlays_file = 'icons/obj/doors/airlocks/station/overlays.dmi' - assemblytype = /obj/structure/door_assembly/door_assembly_sec - if("Command") - icon = 'icons/obj/doors/airlocks/station/command.dmi' - overlays_file = 'icons/obj/doors/airlocks/station/overlays.dmi' - assemblytype = /obj/structure/door_assembly/door_assembly_com - if("Medical") - icon = 'icons/obj/doors/airlocks/station/medical.dmi' - overlays_file = 'icons/obj/doors/airlocks/station/overlays.dmi' - assemblytype = /obj/structure/door_assembly/door_assembly_med - if("Research") - icon = 'icons/obj/doors/airlocks/station/research.dmi' - overlays_file = 'icons/obj/doors/airlocks/station/overlays.dmi' - assemblytype = /obj/structure/door_assembly/door_assembly_research - if("Freezer") - icon = 'icons/obj/doors/airlocks/station/freezer.dmi' - overlays_file = 'icons/obj/doors/airlocks/station/overlays.dmi' - assemblytype = /obj/structure/door_assembly/door_assembly_fre - if("Science") - icon = 'icons/obj/doors/airlocks/station/science.dmi' - overlays_file = 'icons/obj/doors/airlocks/station/overlays.dmi' - assemblytype = /obj/structure/door_assembly/door_assembly_science - if("Virology") - icon = 'icons/obj/doors/airlocks/station/virology.dmi' - overlays_file = 'icons/obj/doors/airlocks/station/overlays.dmi' - assemblytype = /obj/structure/door_assembly/door_assembly_viro - if("Mining") - icon = 'icons/obj/doors/airlocks/station/mining.dmi' - overlays_file = 'icons/obj/doors/airlocks/station/overlays.dmi' - assemblytype = /obj/structure/door_assembly/door_assembly_min - if("Maintenance") - icon = 'icons/obj/doors/airlocks/station/maintenance.dmi' - overlays_file = 'icons/obj/doors/airlocks/station/overlays.dmi' - assemblytype = /obj/structure/door_assembly/door_assembly_mai - if("External") - icon = 'icons/obj/doors/airlocks/external/external.dmi' - overlays_file = 'icons/obj/doors/airlocks/external/overlays.dmi' - assemblytype = /obj/structure/door_assembly/door_assembly_ext - if("External Maintenance") - icon = 'icons/obj/doors/airlocks/station/maintenanceexternal.dmi' - overlays_file = 'icons/obj/doors/airlocks/station/overlays.dmi' - assemblytype = /obj/structure/door_assembly/door_assembly_extmai + + var/airlock_type = painter.available_paint_jobs["[current_paintjob]"] // get the airlock type path associated with the airlock name the user just chose + var/obj/machinery/door/airlock/airlock = airlock_type // we need to create a new instance of the airlock and assembly to read vars from them + var/obj/structure/door_assembly/assembly = initial(airlock.assemblytype) + + if(airlock_material == "glass" && initial(assembly.noglass)) // prevents painting glass airlocks with a paint job that doesn't have a glass version, such as the freezer + to_chat(user, span_warning("This paint job can only be applied to non-glass airlocks.")) + return + + // applies the user-chosen airlock's icon, overlays and assemblytype to the src airlock + painter.use_paint(user) + icon = initial(airlock.icon) + overlays_file = initial(airlock.overlays_file) + assemblytype = initial(airlock.assemblytype) update_icon() /obj/machinery/door/airlock/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) diff --git a/code/game/objects/items/airlock_painter.dm b/code/game/objects/items/airlock_painter.dm index 048a62fea6..b239033dca 100644 --- a/code/game/objects/items/airlock_painter.dm +++ b/code/game/objects/items/airlock_painter.dm @@ -1,10 +1,11 @@ /obj/item/airlock_painter name = "airlock painter" desc = "An advanced autopainter preprogrammed with several paintjobs for airlocks. Use it on an airlock during or after construction to change the paintjob." +// desc_controls = "Alt-Click to remove the ink cartridge." icon = 'icons/obj/objects.dmi' - icon_state = "paint sprayer" - item_state = "paint sprayer" - + icon_state = "paint_sprayer" + item_state = "paint_sprayer" // inhand_icon_state = "paint_sprayer" + // worn_icon_state = "painter" w_class = WEIGHT_CLASS_SMALL custom_materials = list(/datum/material/iron=50, /datum/material/glass=50) @@ -14,40 +15,66 @@ slot_flags = ITEM_SLOT_BELT usesound = 'sound/effects/spray2.ogg' + /// The ink cartridge to pull charges from. var/obj/item/toner/ink = null + /// The type path to instantiate for the ink cartridge the device initially comes with, eg. /obj/item/toner + var/initial_ink_type = /obj/item/toner + /// Associate list of all paint jobs the airlock painter can apply. The key is the name of the airlock the user will see. The value is the type path of the airlock + var/list/available_paint_jobs = list( + "Public" = /obj/machinery/door/airlock/public, + "Engineering" = /obj/machinery/door/airlock/engineering, + "Atmospherics" = /obj/machinery/door/airlock/atmos, + "Security" = /obj/machinery/door/airlock/security, + "Command" = /obj/machinery/door/airlock/command, + "Medical" = /obj/machinery/door/airlock/medical, + "Virology" = /obj/machinery/door/airlock/virology, + "Research" = /obj/machinery/door/airlock/research, +// "Hydroponics" = /obj/machinery/door/airlock/hydroponics, + "Freezer" = /obj/machinery/door/airlock/freezer, + "Science" = /obj/machinery/door/airlock/science, + "Mining" = /obj/machinery/door/airlock/mining, + "Maintenance" = /obj/machinery/door/airlock/maintenance, + "External" = /obj/machinery/door/airlock/external, + "External Maintenance"= /obj/machinery/door/airlock/maintenance/external, + "Standard" = /obj/machinery/door/airlock + ) /obj/item/airlock_painter/Initialize(mapload) . = ..() - ink = new /obj/item/toner(src) + ink = new initial_ink_type(src) + +/obj/item/airlock_painter/examine(mob/user) + . = ..() + . += span_notice("Alt-Click to remove the ink cartridge.") //This proc doesn't just check if the painter can be used, but also uses it. //Only call this if you are certain that the painter will be used right after this check! /obj/item/airlock_painter/proc/use_paint(mob/user) if(can_use(user)) ink.charges-- - playsound(src.loc, 'sound/effects/spray2.ogg', 50, 1) - return 1 + playsound(src.loc, 'sound/effects/spray2.ogg', 50, TRUE) + return TRUE else - return 0 + return FALSE //This proc only checks if the painter can be used. //Call this if you don't want the painter to be used right after this check, for example //because you're expecting user input. /obj/item/airlock_painter/proc/can_use(mob/user) if(!ink) - to_chat(user, "There is no toner cartridge installed in [src]!") - return 0 + balloon_alert(user, "no cartridge!") + return FALSE else if(ink.charges < 1) - to_chat(user, "[src] is out of ink!") - return 0 + balloon_alert(user, "out of ink!") + return FALSE else - return 1 + return TRUE -/obj/item/airlock_painter/suicide_act(mob/user) +/obj/item/airlock_painter/suicide_act(mob/living/user) var/obj/item/organ/lungs/L = user.getorganslot(ORGAN_SLOT_LUNGS) if(can_use(user) && L) - user.visible_message("[user] is inhaling toner from [src]! It looks like [user.p_theyre()] trying to commit suicide!") + user.visible_message(span_suicide("[user] is inhaling toner from [src]! It looks like [user.p_theyre()] trying to commit suicide!")) use(user) // Once you've inhaled the toner, you throw up your lungs @@ -64,7 +91,7 @@ if(!L) return OXYLOSS - L.Remove() + L.Remove(user) // make some colorful reagent, and apply it to the lungs L.create_reagents(10, NONE, NO_REAGENTS_VALUE) @@ -73,27 +100,27 @@ // TODO maybe add some colorful vomit? - user.visible_message("[user] vomits out [user.p_their()] [L]!") - playsound(user.loc, 'sound/effects/splat.ogg', 50, 1) + user.visible_message(span_suicide("[user] vomits out [user.p_their()] [L]!")) + playsound(user.loc, 'sound/effects/splat.ogg', 50, TRUE) L.forceMove(T) return (TOXLOSS|OXYLOSS) else if(can_use(user) && !L) - user.visible_message("[user] is spraying toner on [user.p_them()]self from [src]! It looks like [user.p_theyre()] trying to commit suicide.") + user.visible_message(span_suicide("[user] is spraying toner on [user.p_them()]self from [src]! It looks like [user.p_theyre()] trying to commit suicide.")) user.reagents.add_reagent(/datum/reagent/colorful_reagent, 1) user.reagents.reaction(user, TOUCH, 1) return TOXLOSS else - user.visible_message("[user] is trying to inhale toner from [src]! It might be a suicide attempt if [src] had any toner.") + user.visible_message(span_suicide("[user] is trying to inhale toner from [src]! It might be a suicide attempt if [src] had any toner.")) return SHAME /obj/item/airlock_painter/examine(mob/user) . = ..() if(!ink) - . += "It doesn't have a toner cartridge installed." + . += span_notice("It doesn't have a toner cartridge installed.") return var/ink_level = "high" if(ink.charges < 1) @@ -102,88 +129,129 @@ ink_level = "low" else if((ink.charges/ink.max_charges) > 1) //Over 100% (admin var edit) ink_level = "dangerously high" - . += "Its ink levels look [ink_level]." + . += span_notice("Its ink levels look [ink_level].") /obj/item/airlock_painter/attackby(obj/item/W, mob/user, params) if(istype(W, /obj/item/toner)) if(ink) - to_chat(user, "[src] already contains \a [ink].") + to_chat(user, span_warning("[src] already contains \a [ink]!")) return if(!user.transferItemToLoc(W, src)) return - to_chat(user, "You install [W] into [src].") + to_chat(user, span_notice("You install [W] into [src].")) ink = W - playsound(src.loc, 'sound/machines/click.ogg', 50, 1) + playsound(src.loc, 'sound/machines/click.ogg', 50, TRUE) else return ..() -/obj/item/airlock_painter/attack_self(mob/user) +/obj/item/airlock_painter/AltClick(mob/user) + . = ..() if(ink) - playsound(src.loc, 'sound/machines/click.ogg', 50, 1) + playsound(src.loc, 'sound/machines/click.ogg', 50, TRUE) ink.forceMove(user.drop_location()) user.put_in_hands(ink) - to_chat(user, "You remove [ink] from [src].") + to_chat(user, span_notice("You remove [ink] from [src].")) ink = null - /obj/item/airlock_painter/decal name = "decal painter" - desc = "An airlock painter, reprogramed to use a different style of paint in order to apply decals for floor tiles as well, in addition to repainting doors. Decals break when the floor tiles are removed. Alt-Click to change design." + desc = "An airlock painter, reprogramed to use a different style of paint in order to apply decals for floor tiles as well, in addition to repainting doors. Decals break when the floor tiles are removed." +// desc_controls = "Alt-Click to remove the ink cartridge." icon = 'icons/obj/objects.dmi' icon_state = "decal_sprayer" - item_state = "decalsprayer" - custom_materials = list(/datum/material/iron=2000, /datum/material/glass=500) + item_state = "decal_sprayer" // inhand_icon_state = "decal_sprayer" + custom_materials = list(/datum/material/iron=50, /datum/material/glass=50) + initial_ink_type = /obj/item/toner/large + /// The current direction of the decal being printed var/stored_dir = 2 - var/stored_color = "" + /// The current color of the decal being printed. + var/stored_color = "yellow" + /// The current base icon state of the decal being printed. var/stored_decal = "warningline" + /// The full icon state of the decal being printed. var/stored_decal_total = "warningline" - var/color_list = list("","red","white") - var/dir_list = list(1,2,4,8) - var/decal_list = list(list("Warning Line","warningline"), - list("Warning Line Corner","warninglinecorner"), - list("Caution Label","caution"), - list("Directional Arrows","arrows"), - list("Stand Clear Label","stand_clear"), - list("Box","box"), - list("Box Corner","box_corners"), - list("Delivery Marker","delivery"), - list("Warning Box","warn_full")) - -/obj/item/airlock_painter/decal/afterattack(atom/target, mob/user, proximity) - . = ..() - var/turf/open/floor/F = target - if(!proximity) - to_chat(user, "You need to get closer!") - return - if(use_paint(user) && isturf(F)) - F.AddElement(/datum/element/decal, 'icons/turf/decals.dmi', stored_decal_total, stored_dir, CLEAN_STRONG, color, null, null, alpha) - -/obj/item/airlock_painter/decal/attack_self(mob/user) - if((ink) && (ink.charges >= 1)) - to_chat(user, "[src] beeps to prevent you from removing the toner until out of charges.") - return - . = ..() - -/obj/item/airlock_painter/decal/AltClick(mob/user) - . = ..() - ui_interact(user) + /// The type path of the spritesheet being used for the frontend. + var/spritesheet_type = /datum/asset/spritesheet/decals // spritesheet containing previews + /// Does this printer implementation support custom colors? + var/supports_custom_color = FALSE + /// Current custom color + var/stored_custom_color + /// List of color options as list(user-friendly label, color value to return) + var/color_list = list( + list("Yellow", "yellow"), + list("Red", "red"), + list("White", "white"), + ) + /// List of direction options as list(user-friendly label, dir value to return) + var/dir_list = list( + list("North", NORTH), + list("South", SOUTH), + list("East", EAST), + list("West", WEST), + ) + /// List of decal options as list(user-friendly label, icon state base value to return) + var/decal_list = list( + list("Warning Line", "warningline"), + list("Warning Line Corner", "warninglinecorner"), + list("Caution Label", "caution"), + list("Directional Arrows", "arrows"), + list("Stand Clear Label", "stand_clear"), + list("Bot", "bot"), + list("Box", "box"), + list("Box Corner", "box_corners"), + list("Delivery Marker", "delivery"), + list("Warning Box", "warn_full"), + ) + // These decals only have a south sprite. + var/nondirectional_decals = list( + "bot", + "box", + "delivery", + "warn_full", + ) /obj/item/airlock_painter/decal/Initialize(mapload) . = ..() - ink = new /obj/item/toner/large(src) + stored_custom_color = stored_color + +/obj/item/airlock_painter/decal/afterattack(atom/target, mob/user, proximity) + . = ..() + if(!proximity) + balloon_alert(user, "get closer!") + return + + if(isfloorturf(target) && use_paint(user)) + paint_floor(target) + +/** + * Actually add current decal to the floor. + * + * Responsible for actually adding the element to the turf for maximum flexibility.area + * Can be overriden for different decal behaviors. + * Arguments: + * * target - The turf being painted to +*/ +/obj/item/airlock_painter/decal/proc/paint_floor(turf/open/floor/target) + target.AddElement(/datum/element/decal, 'icons/turf/decals.dmi', stored_decal_total, stored_dir, CLEAN_STRONG, color, null, null, alpha) + +/** + * Return the final icon_state for the given decal options + * + * Arguments: + * * decal - the selected decal base icon state + * * color - the selected color + * * dir - the selected dir + */ +/obj/item/airlock_painter/decal/proc/get_decal_path(decal, color, dir) + // Special case due to icon_state names + if(color == "yellow") + color = "" + + return "[decal][color ? "_" : ""][color]" /obj/item/airlock_painter/decal/proc/update_decal_path() - var/yellow_fix = "" //This will have to do until someone refactor's markings.dm - if (stored_color) - yellow_fix = "_" - stored_decal_total = "[stored_decal][yellow_fix][stored_color]" - return - -/obj/item/airlock_painter/decal/ui_assets(mob/user) - return list( - get_asset_datum(/datum/asset/spritesheet/decals) - ) + stored_decal_total = get_decal_path(stored_decal, stored_color, stored_dir) /obj/item/airlock_painter/decal/ui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) @@ -191,52 +259,215 @@ ui = new(user, src, "DecalPainter", name) ui.open() +/obj/item/airlock_painter/decal/ui_assets(mob/user) + . = ..() + . += get_asset_datum(spritesheet_type) + +/obj/item/airlock_painter/decal/ui_static_data(mob/user) + . = ..() + var/datum/asset/spritesheet/icon_assets = get_asset_datum(spritesheet_type) + + .["icon_prefix"] = "[icon_assets.name]32x32" + .["supports_custom_color"] = supports_custom_color + .["decal_list"] = list() + .["color_list"] = list() + .["dir_list"] = list() + .["nondirectional_decals"] = nondirectional_decals + + for(var/decal in decal_list) + .["decal_list"] += list(list( + "name" = decal[1], + "decal" = decal[2], + )) + for(var/color in color_list) + .["color_list"] += list(list( + "name" = color[1], + "color" = color[2], + )) + for(var/dir in dir_list) + .["dir_list"] += list(list( + "name" = dir[1], + "dir" = dir[2], + )) + /obj/item/airlock_painter/decal/ui_data(mob/user) - var/list/data = list() - data["decal_direction"] = stored_dir - data["decal_dir_text"] = dir2text(stored_dir) - data["decal_color"] = stored_color - data["decal_style"] = stored_decal - data["decal_list"] = list() - data["color_list"] = list() - data["dir_list"] = list() + . = ..() + .["current_decal"] = stored_decal + .["current_color"] = stored_color + .["current_dir"] = stored_dir + .["current_custom_color"] = stored_custom_color - for(var/i in decal_list) - data["decal_list"] += list(list( - "name" = i[1], - "decal" = i[2] - )) - for(var/j in color_list) - data["color_list"] += list(list( - "colors" = j - )) - for(var/k in dir_list) - data["dir_list"] += list(list( - "dirs" = k - )) - return data - -/obj/item/airlock_painter/decal/ui_act(action,list/params) - if(..()) +/obj/item/airlock_painter/decal/ui_act(action, list/params) + . = ..() + if(.) return + switch(action) //Lists of decals and designs if("select decal") - var/selected_decal = params["decals"] + var/selected_decal = params["decal"] + var/selected_dir = text2num(params["dir"]) stored_decal = selected_decal + stored_dir = selected_dir if("select color") - var/selected_color = params["colors"] + var/selected_color = params["color"] stored_color = selected_color - if("selected direction") - var/selected_direction = text2num(params["dirs"]) - stored_dir = selected_direction + if("pick custom color") + if(supports_custom_color) + pick_painting_tool_color(usr, stored_custom_color) update_decal_path() . = TRUE +/obj/item/airlock_painter/decal/set_painting_tool_color(chosen_color) + . = ..() + stored_custom_color = chosen_color + stored_color = chosen_color + +/datum/asset/spritesheet/decals + name = "floor_decals" +// cross_round_cachable = TRUE + + /// The floor icon used for blend_preview_floor() + var/preview_floor_icon = 'icons/turf/floors.dmi' + /// The floor icon state used for blend_preview_floor() + var/preview_floor_state = "floor" + /// The associated decal painter type to grab decals, colors, etc from. + var/obj/item/airlock_painter/decal/painter_type = /obj/item/airlock_painter/decal + +/** + * Underlay an example floor for preview purposes, and return the new icon. + * + * Arguments: + * * decal - the decal to place over the example floor tile + */ +/datum/asset/spritesheet/decals/proc/blend_preview_floor(icon/decal) + var/icon/final = icon(preview_floor_icon, preview_floor_state) + final.Blend(decal, ICON_OVERLAY) + return final + +/** + * Insert a specific state into the spritesheet. + * + * Arguments: + * * decal - the given decal base state. + * * dir - the given direction. + * * color - the given color. + */ +/datum/asset/spritesheet/decals/proc/insert_state(decal, dir, color) + // Special case due to icon_state names + var/icon_state_color = color == "yellow" ? "" : color + + var/icon/final = blend_preview_floor(icon('icons/turf/decals.dmi', "[decal][icon_state_color ? "_" : ""][icon_state_color]", dir)) + Insert("[decal]_[dir]_[color]", final) + +/datum/asset/spritesheet/decals/register() + // Must actually create because initial(type) doesn't work for /lists for some reason. + var/obj/item/airlock_painter/decal/painter = new painter_type() + + for(var/list/decal in painter.decal_list) + for(var/list/dir in painter.dir_list) + for(var/list/color in painter.color_list) + insert_state(decal[2], dir[2], color[2]) + if(painter.supports_custom_color) + insert_state(decal[2], dir[2], "custom") + + qdel(painter) + . = ..() + /obj/item/airlock_painter/decal/debug name = "extreme decal painter" icon_state = "decal_sprayer_ex" + initial_ink_type = /obj/item/toner/extreme -/obj/item/airlock_painter/decal/debug/Initialize(mapload) - . = ..() - ink = new /obj/item/toner/extreme(src) +/obj/item/airlock_painter/decal/tile + name = "tile sprayer" + desc = "An airlock painter, reprogramed to use a different style of paint in order to spray colors on floor tiles as well, in addition to repainting doors. Decals break when the floor tiles are removed." +// desc_controls = "Alt-Click to remove the ink cartridge." + icon_state = "tile_sprayer" + stored_dir = 2 + stored_color = "#D4D4D432" + stored_decal = "tile_corner" + spritesheet_type = /datum/asset/spritesheet/decals/tiles + supports_custom_color = TRUE + // Colors can have a an alpha component as RGBA, or just be RGB and use default alpha + color_list = list( + list("Neutral", "#D4D4D432"), + list("Dark", "#0e0f0f"), + list("Bar Burgundy", "#79150082"), + list("Sec Red", "#DE3A3A"), + list("Cargo Brown", "#A46106"), + list("Engi Yellow", "#EFB341"), + list("Service Green", "#9FED58"), + list("Med Blue", "#52B4E9"), + list("R&D Purple", "#D381C9"), + ) + decal_list = list( + list("Corner", "tile_corner"), + list("Half", "tile_half_contrasted"), + list("Opposing Corners", "tile_opposing_corners"), + list("3 Corners", "tile_anticorner_contrasted"), + list("4 Corners", "tile_fourcorners"), + list("Trimline Corner", "trimline_corner_fill"), + list("Trimline Fill", "trimline_fill"), + list("Trimline Fill L", "trimline_fill__8"), // This is a hack that lives in the spritesheet builder and paint_floor + list("Trimline End", "trimline_end_fill"), + list("Trimline Box", "trimline_box_fill"), + ) + nondirectional_decals = list( + "tile_fourcorners", + "trimline_box_fill", + ) + + /// Regex to split alpha out. + var/static/regex/rgba_regex = new(@"(#[0-9a-fA-F]{6})([0-9a-fA-F]{2})") + + /// Default alpha for /obj/effect/turf_decal/tile + var/default_alpha = 110 + +/obj/item/airlock_painter/decal/tile/paint_floor(turf/open/floor/target) + // Account for 8-sided decals. + var/source_decal = stored_decal + var/source_dir = stored_dir + if(copytext(stored_decal, -3) == "__8") + source_decal = splicetext(stored_decal, -3, 0, "") + source_dir = turn(stored_dir, 45) + + var/decal_color = stored_color + var/decal_alpha = default_alpha + // Handle the RGBA case. + if(rgba_regex.Find(decal_color)) + decal_color = rgba_regex.group[1] + decal_alpha = text2num(rgba_regex.group[2], 16) + + target.AddElement(/datum/element/decal, 'icons/turf/decals.dmi', source_decal, source_dir, CLEAN_STRONG, decal_color, null, null, decal_alpha) + +/datum/asset/spritesheet/decals/tiles + name = "floor_tile_decals" + painter_type = /obj/item/airlock_painter/decal/tile + +/datum/asset/spritesheet/decals/tiles/insert_state(decal, dir, color) + // Account for 8-sided decals. + var/source_decal = decal + var/source_dir = dir + if(copytext(decal, -3) == "__8") + source_decal = splicetext(decal, -3, 0, "") + source_dir = turn(dir, 45) + + // Handle the RGBA case. + var/obj/item/airlock_painter/decal/tile/tile_type = painter_type + var/render_color = color + var/render_alpha = initial(tile_type.default_alpha) + if(tile_type.rgba_regex.Find(color)) + render_color = tile_type.rgba_regex.group[1] + render_alpha = text2num(tile_type.rgba_regex.group[2], 16) + + var/icon/colored_icon = icon('icons/turf/decals.dmi', source_decal, dir=source_dir) + colored_icon.ChangeOpacity(render_alpha * 0.008) + if(color == "custom") + // Do a fun rainbow pattern to stand out while still being static. + colored_icon.Blend(icon('icons/effects/random_spawners.dmi', "rainbow"), ICON_MULTIPLY) + else + colored_icon.Blend(render_color, ICON_MULTIPLY) + + colored_icon = blend_preview_floor(colored_icon) + Insert("[decal]_[dir]_[replacetext(color, "#", "")]", colored_icon) diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm index 5ad334bb76..631f0c3e4f 100644 --- a/code/modules/asset_cache/asset_list_items.dm +++ b/code/modules/asset_cache/asset_list_items.dm @@ -311,14 +311,6 @@ InsertAll("", each, GLOB.alldirs) ..() -/datum/asset/spritesheet/decals - name = "decals" - -/datum/asset/spritesheet/decals/register() - for(var/each in list('icons/turf/decals.dmi')) - InsertAll("", each, GLOB.alldirs) - ..() - /datum/asset/spritesheet/supplypods name = "supplypods" diff --git a/code/modules/research/designs/autolathe_desings/autolathe_designs_tcomms_and_misc.dm b/code/modules/research/designs/autolathe_desings/autolathe_designs_tcomms_and_misc.dm index 1b1c710e8c..0ac303a3a0 100644 --- a/code/modules/research/designs/autolathe_desings/autolathe_designs_tcomms_and_misc.dm +++ b/code/modules/research/designs/autolathe_desings/autolathe_designs_tcomms_and_misc.dm @@ -92,6 +92,11 @@ category = list("initial","Tools","Tool Designs") departmental_flags = DEPARTMENTAL_FLAG_ENGINEERING | DEPARTMENTAL_FLAG_SERVICE +/datum/design/airlock_painter/decal/tile + name = "Tile Sprayer" + id = "tile_sprayer" + build_path = /obj/item/airlock_painter/decal/tile + /datum/design/cultivator name = "Cultivator" id = "cultivator" diff --git a/code/modules/research/techweb/nodes/tools_nodes.dm b/code/modules/research/techweb/nodes/tools_nodes.dm index 86a48ab8de..004795113b 100644 --- a/code/modules/research/techweb/nodes/tools_nodes.dm +++ b/code/modules/research/techweb/nodes/tools_nodes.dm @@ -5,7 +5,7 @@ display_name = "Basic Tools" description = "Basic mechanical, electronic, surgical and botanical tools." prereq_ids = list("base") - design_ids = list("screwdriver", "wrench", "wirecutters", "crowbar", "multitool", "welding_tool", "tscanner", "analyzer", "cable_coil", "pipe_painter", "airlock_painter", "decal_painter", "scalpel", "circular_saw", "surgicaldrill", "retractor", "cautery", "hemostat", "cultivator", "plant_analyzer", "shovel", "spade", "hatchet", "mop", "broom", "normtrash", "spraycan") + design_ids = list("screwdriver", "wrench", "wirecutters", "crowbar", "multitool", "welding_tool", "tscanner", "analyzer", "cable_coil", "pipe_painter", "airlock_painter", "decal_painter", "tile_sprayer", "scalpel", "circular_saw", "surgicaldrill", "retractor", "cautery", "hemostat", "cultivator", "plant_analyzer", "shovel", "spade", "hatchet", "mop", "broom", "normtrash", "spraycan") research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 500) /datum/techweb_node/basic_mining diff --git a/icons/effects/random_spawners.dmi b/icons/effects/random_spawners.dmi new file mode 100644 index 0000000000..144287ced0 Binary files /dev/null and b/icons/effects/random_spawners.dmi differ diff --git a/icons/mob/clothing/belt.dmi b/icons/mob/clothing/belt.dmi index 9746e61a3d..f7062b9f4e 100644 Binary files a/icons/mob/clothing/belt.dmi and b/icons/mob/clothing/belt.dmi differ diff --git a/icons/mob/inhands/items_lefthand.dmi b/icons/mob/inhands/items_lefthand.dmi index d0ebbb4501..b694d0ba35 100644 Binary files a/icons/mob/inhands/items_lefthand.dmi and b/icons/mob/inhands/items_lefthand.dmi differ diff --git a/icons/mob/inhands/items_righthand.dmi b/icons/mob/inhands/items_righthand.dmi index 0d87e7871c..3836f4c713 100644 Binary files a/icons/mob/inhands/items_righthand.dmi and b/icons/mob/inhands/items_righthand.dmi differ diff --git a/icons/obj/objects.dmi b/icons/obj/objects.dmi index 8eb3b619a1..cc28f4e346 100644 Binary files a/icons/obj/objects.dmi and b/icons/obj/objects.dmi differ diff --git a/icons/turf/decals.dmi b/icons/turf/decals.dmi index 4ed8c8db9a..6b42ad12c7 100644 Binary files a/icons/turf/decals.dmi and b/icons/turf/decals.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 41118ab4f3..5949ee79a7 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -157,6 +157,7 @@ #include "code\__DEFINES\dcs\flags.dm" #include "code\__DEFINES\dcs\helpers.dm" #include "code\__DEFINES\dcs\signals.dm" +#include "code\__DEFINES\dcs\signals\signals_painting.dm" #include "code\__DEFINES\dcs\signals\signals_screentips.dm" #include "code\__DEFINES\dcs\signals\signals_subsystem.dm" #include "code\__DEFINES\dcs\signals\signals_atom\signals_atom_movement.dm" diff --git a/tgui/packages/tgui/interfaces/DecalPainter.js b/tgui/packages/tgui/interfaces/DecalPainter.js deleted file mode 100644 index f116d09c18..0000000000 --- a/tgui/packages/tgui/interfaces/DecalPainter.js +++ /dev/null @@ -1,114 +0,0 @@ -import { useBackend } from '../backend'; -import { Box, Button, Section } from '../components'; -import { Window } from '../layouts'; -import { classes } from 'common/react'; - -export const DecalPainter = (props, context) => { - const { act, data } = useBackend(context); - const decal_list = data.decal_list || []; - const color_list = data.color_list || []; - const dir_list = data.dir_list || []; - return ( - - -
- {decal_list.map(decal => ( - - ))} -
-
- {color_list.map(color => { - return ( - - ); - })} -
-
- {dir_list.map(dir => { - return ( - - ); - })} -
-
-
- ); -}; diff --git a/tgui/packages/tgui/interfaces/DecalPainter.tsx b/tgui/packages/tgui/interfaces/DecalPainter.tsx new file mode 100644 index 0000000000..1a211cfba4 --- /dev/null +++ b/tgui/packages/tgui/interfaces/DecalPainter.tsx @@ -0,0 +1,162 @@ +import { useBackend } from '../backend'; +import { Button, ColorBox, Flex, Section } from '../components'; +import { Window } from '../layouts'; + +type DecalInfo = { + name: string; + decal: string; +}; + +type ColorInfo = { + name: string; + color: string; +}; + +type DirInfo = { + name: string; + dir: number; +}; + +type DecalPainterData = { + icon_prefix: string; + decal_list: DecalInfo[]; + color_list: ColorInfo[]; + dir_list: DirInfo[]; + nondirectional_decals: string[]; + supports_custom_color: number; + current_decal: string; + current_color: string; + current_dir: number; + current_custom_color: string; +}; + +const filterBoxColor = (color: string) => { + if (!color.startsWith('#')) { + return color; + } + + // cut alpha + return color.substring(0, 7); +}; + +export const DecalPainter = (props, context) => { + const { act, data } = useBackend(context); + + const custom_color_selected = !data.color_list.some( + (color) => color.color === data.current_color + ); + const supports_custom_color = !!data.supports_custom_color; + + // Handle custom color icon correctly + const preview_color = custom_color_selected ? 'custom' : data.current_color; + + return ( + + +
+ {data.color_list.map((color) => { + return ( + + ); + })} + {supports_custom_color && ( + + )} +
+
+ + {data.decal_list.map((decal) => { + const nondirectional = data.nondirectional_decals.includes( + decal.decal + ); + + return nondirectional ? ( + // Tallll button for nondirectional + + ) : ( + // 4 buttons for directional + + {data.dir_list.map((dir) => { + const selected + = decal.decal === data.current_decal + && dir.dir === data.current_dir; + + return ( + + ); + })} + + ); + })} + +
+
+
+ ); +}; + +type IconButtonParams = { + decal: string; + dir: number; + color: string; + label: string; + selected: boolean; +}; + +const IconButton = (props: IconButtonParams, context) => { + const { act, data } = useBackend(context); + + const generateIconKey = (decal: string, dir: number, color: string) => + `${data.icon_prefix} ${decal}_${dir}_${color.replace('#', '')}`; + + const icon = generateIconKey(props.decal, props.dir, props.color); + + return ( + + ); +};