From a27949d5f5a80e50c2e3745e2b1b1c117d73145a Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:12:14 +0300 Subject: [PATCH] Advanced Color Shifting: Spraypaint Edition (#88201) ## About The Pull Request "If GAGS is such a good system, why isn't there GAGS 2?" - Sun Tzu GAGS is very neat but it has one glaring issue: it needs sprites to be greyscaled in advance to be used. On the other hand we have color matrices, but they're hard to use and even harder to get good results from. The logical solution grew out of a discord argument about colors this morning after @LemonInTheDark decided to toy around with HSL matrices using filters on live servers. This PR implements Color Transition Filters as an additional option for atom colors - passing a transition filter matrix into ``add_atom_colour`` will "recolor" the atom into the passed color by using an HSL filter (since color only supports RGB values and matrices). Normal color matrices are now also supported in atom colors, in case anyone needs to use them there. ``color_transition_filter`` has 2 modes: ``SATURATION_MULTIPLY`` which only changes the hue and shifts saturation of the original icon, and ``SATURATION_OVERRIDE`` which changes saturation and light values to more correctly fit the passed color. Multiply mode does a far better job at recoloring clothing or objects with obvious highlights, but fails to color pale or white objects, while Override mode is closer to what we have right now (just doesn't produce rancid blobs of color nearly as much) Here are some examples of colored clothes, mechs, items and tiles using the new system. Green RD? Sure. ![image](https://github.com/user-attachments/assets/6d79cac3-15a5-4850-abae-19219e1d4bdb) Atmos MODsuit colored with a speed potion ![4cTKpeu](https://github.com/user-attachments/assets/9106e74c-8d60-489a-9ef7-4d154ddbbdf9) Why override mode exists in the first place ![dreamseeker_fAKn811LXT](https://github.com/user-attachments/assets/3d3bea8c-5e27-4390-a924-0c243265fa6a) Aftermath of a colorful reagent grenade. ![image](https://github.com/user-attachments/assets/ba4c78c5-cba5-42da-ac4d-7861bb329b68) As you can see, the colors are far brighter and significantly less acidic, since they're no longer just used as multipliers for existing colors but instead shift the palette of the sprite towards themselves. In order to bypass the main downside of "default" Multiply mode, spraycans have received a new right click function "coat with paint", which will color the item using the Override mode. Left Click mode lost its coloring restrictions (RMB still has them), and color sampling/prosthetic recoloring has been moved to Ctrl Click instead. Here's the full list of all systems/items that now use color transition filters: * Drying items * Deep frying items * Slime blueprints/potions/coloring crossbreeds * Colorful reagent * Spraycans * Paint buckets ## Why It's Good For The Game Our coloring system is ***really*** bad, to the point where we're preventing players from using any dark colors because item icons become unintelligible when colored into them. ## Changelog :cl: SmArtKar, LemonInTheDark add: Changed how spraycans color items - "old" mode is still availible via right click. refactor: Refactored how some items and effects color things so that they look prettier. /:cl: --- code/__DEFINES/colors.dm | 13 ++ code/__DEFINES/traits/sources.dm | 3 + code/__HELPERS/colors.dm | 113 +++++++++++ code/__HELPERS/filters.dm | 5 + code/datums/elements/dryable.dm | 5 +- code/datums/elements/food/fried_item.dm | 15 +- code/game/atom/atom_color.dm | 87 +++++++-- code/game/objects/items/blueprints.dm | 3 +- code/game/objects/items/crayons.dm | 182 ++++++++++-------- code/game/objects/items/paint.dm | 5 +- .../structures/crates_lockers/closets.dm | 3 +- .../gas_recipe_machines/crystallizer_items.dm | 2 +- .../living/carbon/human/human_update_icons.dm | 2 +- code/modules/mod/mod_control.dm | 2 +- code/modules/power/lighting/light.dm | 2 + .../chemistry/reagents/other_reagents.dm | 4 +- .../xenobiology/crossbreeding/_potions.dm | 4 +- .../xenobiology/crossbreeding/prismatic.dm | 2 +- .../xenobiology/crossbreeding/regenerative.dm | 16 +- .../research/xenobiology/xenobiology.dm | 4 +- 20 files changed, 352 insertions(+), 120 deletions(-) diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm index d1fff3b20ed..6e9af2cdb99 100644 --- a/code/__DEFINES/colors.dm +++ b/code/__DEFINES/colors.dm @@ -11,6 +11,19 @@ #define FIXED_COLOUR_PRIORITY 4 ///how many colour priority levels there are. #define COLOUR_PRIORITY_AMOUNT 4 +/// If this is a plain atom color +#define ATOM_COLOR_TYPE_NORMAL "normal" +/// If this is a color filter +#define ATOM_COLOR_TYPE_FILTER "filter" +// Indexes for color arrays +#define ATOM_COLOR_VALUE_INDEX 1 +#define ATOM_COLOR_TYPE_INDEX 2 +#define ATOM_PRIORITY_COLOR_FILTER "atom_priority_color" +#define ATOM_PRIORITY_COLOR_FILTER_PRIORITY -1 +/// Multiply pixel's saturation by color's saturation. Paints accents while keeping dim areas dim. +#define SATURATION_MULTIPLY "multiply" +/// Always affects the original pixel's saturation and lightness. +#define SATURATION_OVERRIDE "always" #define COLOR_DARKMODE_BACKGROUND "#202020" #define COLOR_DARKMODE_DARKBACKGROUND "#171717" diff --git a/code/__DEFINES/traits/sources.dm b/code/__DEFINES/traits/sources.dm index 4deb3925141..c4e952ed77b 100644 --- a/code/__DEFINES/traits/sources.dm +++ b/code/__DEFINES/traits/sources.dm @@ -312,3 +312,6 @@ /// From the aquarium component #define AQUARIUM_TRAIT "aquarium" + +/// Trait aquired from being painted a certain color +#define ATOM_COLOR_TRAIT "atom_color" diff --git a/code/__HELPERS/colors.dm b/code/__HELPERS/colors.dm index 3a20e5ad60c..28677669768 100644 --- a/code/__HELPERS/colors.dm +++ b/code/__HELPERS/colors.dm @@ -148,3 +148,116 @@ return "#[num2hex(c, 2)][num2hex(m, 2)][num2hex(x, 2)]" #define RANDOM_COLOUR (rgb(rand(0,255),rand(0,255),rand(0,255))) + +/* Generates an HSL color transition matrix filter which nicely paints an object + * without making it a deep fried blob of color + * saturation_behavior determines how we handle color saturation: + * SATURATION_MULTIPLY - Multiply pixel's saturation by color's saturation. Paints accents while keeping dim areas dim. + * SATURATION_OVERRIDE- Affects original lightness/saturation to ensure that pale objects still get doused in color + */ +/proc/color_transition_filter(new_color, saturation_behavior = SATURATION_MULTIPLY) + if (islist(new_color)) + new_color = rgb(new_color[1], new_color[2], new_color[3]) + new_color = rgb2num(new_color, COLORSPACE_HSL) + var/hue = new_color[1] / 360 + var/saturation = new_color[2] / 100 + var/added_saturation = 0 + var/deducted_light = 0 + if (saturation_behavior == SATURATION_OVERRIDE) + added_saturation = saturation * 0.75 + deducted_light = saturation * 0.5 + saturation = min(saturation, 1 - added_saturation) + + var/list/new_matrix = list( + 0, 0, 0, 0, // Ignore original hue + 0, saturation, 0, 0, // Multiply the saturation by ours + 0, 0, 1 - deducted_light, 0, // If we're highly saturated then remove a bit of lightness to keep some color in + 0, 0, 0, 1, // Preserve alpha + hue, added_saturation, 0, 0, // And apply our preferred hue and some saturation if we're oversaturated + ) + return color_matrix_filter(new_matrix, FILTER_COLOR_HSL) + +/// Applies a color filter to a hex/RGB list color +/proc/apply_matrix_to_color(color, list/matrix, colorspace = COLORSPACE_HSL) + if (islist(color)) + color = rgb(color[1], color[2], color[3], color[4]) + color = rgb2num(color, colorspace) + // Pad alpha if we're lacking it + if (length(color) < 4) + color += 255 + + // Do we have a constants row? + var/has_constants = FALSE + // Do we have an alpha row/parameters? + var/has_alpha = FALSE + + switch (length(matrix)) + if (9) + has_constants = FALSE + has_alpha = FALSE + if (12) + has_constants = TRUE + has_alpha = FALSE + if (16) + has_constants = FALSE + has_alpha = TRUE + if (20) + has_constants = TRUE + has_alpha = TRUE + else + CRASH("Matrix of invalid length [length(matrix)] was passed into apply_matrix_to_color!") + + var/list/new_color = list(0, 0, 0, 0) + var/row_length = 3 + if (has_alpha) + row_length = 4 + else + new_color[4] = 255 + + for (var/row_index in 1 to length(matrix) / row_length) + for (var/row_elem in 1 to row_length) + var/elem = matrix[(row_index - 1) * row_length + row_elem] + if (!has_constants || row_index != (length(matrix) / row_length)) + new_color[row_index] += color[row_elem] * elem + continue + + // Constant values at the end of the list (if we have such) + if (colorspace != COLORSPACE_HSV && colorspace != COLORSPACE_HCY && colorspace != COLORSPACE_HSL) + new_color[row_elem] += elem * 255 + continue + + // HSV/HSL/HCY have non-255 maximums for their values + var/multiplier = 255 + switch (row_elem) + // Hue goes from 0 to 360 + if (1) + multiplier = 360 + // Value, luminance, chroma, etc go from 0 to 100 + if (2 to 3) + multiplier = 100 + // Alpha still goes from 0 to 255 + if (4) + multiplier = 255 + new_color[row_elem] += elem * multiplier + + var/rgbcolor = rgb(new_color[1], new_color[2], new_color[3], new_color[4], space = colorspace) + return rgbcolor + +/// Recursively applies a filter to a passed in static appearance, returns the modified appearance +/proc/filter_appearance_recursive(mutable_appearance/filter, filter_to_apply) + var/mutable_appearance/modify = new(filter) + var/list/existing_filters = modify.filters.Copy() + modify.filters = list(filter_to_apply) + existing_filters + + // Ideally this should be recursive to check for KEEP_APART elements that need this applied to it + // and RESET_COLOR flags but this is much simpler, and hopefully we don't have that point of layering here + if(modify.appearance_flags & KEEP_TOGETHER) + return modify + + for(var/overlay_index in 1 to length(modify.overlays)) + modify.overlays[overlay_index] = filter_appearance_recursive(modify.overlays[overlay_index], filter_to_apply) + + for(var/underlay_index in 1 to length(modify.underlays)) + modify.underlays[underlay_index] = filter_appearance_recursive(modify.underlays[underlay_index], filter_to_apply) + + return modify diff --git a/code/__HELPERS/filters.dm b/code/__HELPERS/filters.dm index 930c889b09c..f84c812e0be 100644 --- a/code/__HELPERS/filters.dm +++ b/code/__HELPERS/filters.dm @@ -322,3 +322,8 @@ GLOBAL_LIST_INIT(master_filter_info, list( continue animate(filter, x = 0, y = 0, size = 0, offset = 0, time = remove_duration) addtimer(CALLBACK(in_atom, TYPE_PROC_REF(/datum, remove_filter), "wibbly-[i]"), remove_duration) + +/proc/convert_list_to_filter(list/list_filter) + var/list/arguments = list_filter.Copy() + arguments -= "priority" + return filter(arglist(arguments)) diff --git a/code/datums/elements/dryable.dm b/code/datums/elements/dryable.dm index d66ea96d8c1..583c74764ee 100644 --- a/code/datums/elements/dryable.dm +++ b/code/datums/elements/dryable.dm @@ -22,10 +22,13 @@ /datum/element/dryable/proc/finish_drying(atom/source, datum/weakref/drying_user) SIGNAL_HANDLER + var/static/list/dried_color + if (isnull(dried_color)) + dried_color = color_transition_filter(COLOR_DRIED_TAN) var/atom/dried_atom = source if(dry_result == dried_atom.type)//if the dried type is the same as our currrent state, don't bother creating a whole new item, just re-color it. var/atom/movable/resulting_atom = dried_atom - resulting_atom.add_atom_colour(COLOR_DRIED_TAN, FIXED_COLOUR_PRIORITY) + resulting_atom.add_atom_colour(dried_color, FIXED_COLOUR_PRIORITY) apply_dried_status(resulting_atom, drying_user) return else if(isstack(source)) //Check if its a sheet diff --git a/code/datums/elements/food/fried_item.dm b/code/datums/elements/food/fried_item.dm index bc21e51f24c..7f8613a537f 100644 --- a/code/datums/elements/food/fried_item.dm +++ b/code/datums/elements/food/fried_item.dm @@ -2,18 +2,21 @@ /datum/element/fried_item /// List of colors to apply the element target. /// Each index corresponds to a different level. - var/static/list/fried_colors = list( - COLOR_LIGHT_BROWN, - COLOR_BROWNER_BROWN, - COLOR_DARKER_BROWN, - COLOR_BLACK, - ) + var/static/list/fried_colors /datum/element/fried_item/Attach(datum/target, fry_time) . = ..() if(!isatom(target)) return ELEMENT_INCOMPATIBLE + if (isnull(fried_colors)) + fried_colors = list( + color_transition_filter(COLOR_LIGHT_BROWN, SATURATION_OVERRIDE), + color_transition_filter(COLOR_BROWNER_BROWN, SATURATION_OVERRIDE), + color_transition_filter(COLOR_DARKER_BROWN, SATURATION_OVERRIDE), + color_transition_filter(COLOR_BLACK, SATURATION_OVERRIDE), + ) + var/atom/this_food = target switch(fry_time) diff --git a/code/game/atom/atom_color.dm b/code/game/atom/atom_color.dm index 2508e86f44d..4b20a004228 100644 --- a/code/game/atom/atom_color.dm +++ b/code/game/atom/atom_color.dm @@ -3,6 +3,9 @@ A System that gives finer control over which atom colour to colour the atom with. The "highest priority" one is always displayed as opposed to the default of "whichever was set last is displayed" + + It can also be used for color filters, since some effects (using non-RGB space matrices) + are impossible to achieve with just the color variable */ /atom @@ -12,6 +15,8 @@ * its inherent color, the colored paint applied on it, special color effect etc... */ var/list/atom_colours + /// Currently used color filter - cached because its applied to all of our overlays because BYOND is horrific + var/cached_color_filter ///Adds an instance of colour_type to the atom's atom_colours list /atom/proc/add_atom_colour(coloration, colour_priority) @@ -22,7 +27,12 @@ return if(colour_priority > atom_colours.len) return - atom_colours[colour_priority] = coloration + var/color_type = ATOM_COLOR_TYPE_NORMAL + if (islist(coloration)) + var/list/color_matrix = coloration + if (color_matrix["type"] == "color") + color_type = ATOM_COLOR_TYPE_FILTER + atom_colours[colour_priority] = list(coloration, color_type) update_atom_colour() @@ -32,8 +42,13 @@ return if(colour_priority > atom_colours.len) return - if(coloration && atom_colours[colour_priority] != coloration) - return //if we don't have the expected color (for a specific priority) to remove, do nothing + if(coloration && atom_colours[colour_priority]) + if (atom_colours[colour_priority][ATOM_COLOR_TYPE_INDEX] == ATOM_COLOR_TYPE_NORMAL) + if (atom_colours[colour_priority][ATOM_COLOR_VALUE_INDEX] != coloration) + return //if we don't have the expected color (for a specific priority) to remove, do nothing + else + if (!islist(coloration) || !compare_list(coloration, atom_colours[colour_priority][ATOM_COLOR_VALUE_INDEX]["color"])) + return atom_colours[colour_priority] = null update_atom_colour() @@ -43,29 +58,69 @@ */ /atom/proc/is_atom_colour(looking_for_color, min_priority_index = 1, max_priority_index = COLOUR_PRIORITY_AMOUNT) // make sure uppertext hex strings don't mess with LOWER_TEXT hex strings - looking_for_color = LOWER_TEXT(looking_for_color) + if (!islist(looking_for_color)) + looking_for_color = LOWER_TEXT(looking_for_color) if(!LAZYLEN(atom_colours)) // no atom colors list has been set up, just check the color var - return LOWER_TEXT(color) == looking_for_color + if (!islist(color)) + return LOWER_TEXT(color) == looking_for_color + if (!islist(looking_for_color)) + return FALSE + return compare_list(color, looking_for_color) for(var/i in min_priority_index to max_priority_index) - if(LOWER_TEXT(atom_colours[i]) == looking_for_color) + if (!atom_colours[i]) + continue + + if (!islist(looking_for_color)) + if (islist(atom_colours[i][ATOM_COLOR_VALUE_INDEX])) + continue + + if (LOWER_TEXT(atom_colours[i][ATOM_COLOR_VALUE_INDEX]) == looking_for_color) + return TRUE + + continue + + var/compared_matrix = atom_colours[i][ATOM_COLOR_VALUE_INDEX] + if (atom_colours[i][ATOM_COLOR_TYPE_INDEX] == ATOM_COLOR_TYPE_FILTER) + compared_matrix = atom_colours[i][ATOM_COLOR_VALUE_INDEX]["color"] + + if (compare_list(looking_for_color, compared_matrix)) return TRUE return FALSE ///Resets the atom's color to null, and then sets it to the highest priority colour available /atom/proc/update_atom_colour() + var/old_filter = cached_color_filter color = null - if(!atom_colours) + cached_color_filter = null + remove_filter(ATOM_PRIORITY_COLOR_FILTER) + REMOVE_KEEP_TOGETHER(src, ATOM_COLOR_TRAIT) + + if (!atom_colours) + if (old_filter) + update_appearance() return - for(var/checked_color in atom_colours) - if(islist(checked_color)) - var/list/color_list = checked_color - if(color_list.len) - color = color_list - return - else if(checked_color) - color = checked_color - return + + for (var/list/checked_color in atom_colours) + if (checked_color[ATOM_COLOR_TYPE_INDEX] == ATOM_COLOR_TYPE_FILTER) + add_filter(ATOM_PRIORITY_COLOR_FILTER, ATOM_PRIORITY_COLOR_FILTER_PRIORITY, checked_color[ATOM_COLOR_VALUE_INDEX]) + cached_color_filter = checked_color[ATOM_COLOR_VALUE_INDEX] + break + + if (length(checked_color[ATOM_COLOR_VALUE_INDEX])) + color = checked_color[ATOM_COLOR_VALUE_INDEX] + break + + ADD_KEEP_TOGETHER(src, ATOM_COLOR_TRAIT) + if (cached_color_filter != old_filter) + update_appearance() + +/// Same as update_atom_color, but simplifies overlay coloring +/atom/proc/color_atom_overlay(mutable_appearance/overlay) + overlay.color = color + if (!cached_color_filter) + return overlay + return filter_appearance_recursive(overlay, cached_color_filter) diff --git a/code/game/objects/items/blueprints.dm b/code/game/objects/items/blueprints.dm index d11c6e21d69..a1f0001f651 100644 --- a/code/game/objects/items/blueprints.dm +++ b/code/game/objects/items/blueprints.dm @@ -239,10 +239,11 @@ /obj/item/blueprints/slime/edit_area(mob/user) . = ..() var/area/area = get_area(src) + var/list/turf_matrix = color_transition_filter("#2956B2") for(var/list/zlevel_turfs as anything in area.get_zlevel_turf_lists()) for(var/turf/area_turf as anything in zlevel_turfs) area_turf.remove_atom_colour(WASHABLE_COLOUR_PRIORITY) - area_turf.add_atom_colour("#2956B2", FIXED_COLOUR_PRIORITY) + area_turf.add_atom_colour(turf_matrix, FIXED_COLOUR_PRIORITY) area.area_flags |= XENOBIOLOGY_COMPATIBLE qdel(src) diff --git a/code/game/objects/items/crayons.dm b/code/game/objects/items/crayons.dm index 1412ad41a25..18da163bce4 100644 --- a/code/game/objects/items/crayons.dm +++ b/code/game/objects/items/crayons.dm @@ -202,7 +202,7 @@ /obj/item/toy/crayon/suicide_act(mob/living/user) user.visible_message(span_suicide("[user] is jamming [src] up [user.p_their()] nose and into [user.p_their()] brain. It looks like [user.p_theyre()] trying to commit suicide!")) - user.add_atom_colour(paint_color, ADMIN_COLOUR_PRIORITY) + user.add_atom_colour(color_transition_filter(paint_color, SATURATION_OVERRIDE), ADMIN_COLOUR_PRIORITY) return (BRUTELOSS|OXYLOSS) /obj/item/toy/crayon/Initialize(mapload) @@ -435,7 +435,7 @@ if(!isValidSurface(target)) target.balloon_alert(user, "can't use there!") - return + return ITEM_INTERACT_BLOCKING var/drawing = drawtype switch(drawtype) @@ -471,7 +471,7 @@ if (istagger) cost *= 0.5 if(check_empty(user, cost)) - return + return ITEM_INTERACT_BLOCKING var/temp = "rune" var/ascii = (length(drawing) == 1) @@ -521,10 +521,10 @@ wait_time *= 0.5 if(!instant && !do_after(user, wait_time, target = target, max_interact_count = 4)) - return + return ITEM_INTERACT_BLOCKING if(!use_charges(user, cost)) - return + return ITEM_INTERACT_BLOCKING if(length(text_buffer)) drawing = text_buffer[1] @@ -548,7 +548,7 @@ affected_turfs += right else balloon_alert(user, "no room!") - return + return ITEM_INTERACT_BLOCKING created_art.add_hiddenprint(user) if(istagger) created_art.AddElement(/datum/element/art, GOOD_ART) @@ -575,15 +575,14 @@ for(var/turf/draw_turf as anything in affected_turfs) reagents.expose(draw_turf, methods = TOUCH, volume_modifier = volume_multiplier) check_empty(user) - return + return ITEM_INTERACT_SUCCESS /obj/item/toy/crayon/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers) if (!check_allowed_items(interacting_with)) return NONE if(can_use_on(interacting_with, user, modifiers)) - use_on(interacting_with, user, modifiers) - return ITEM_INTERACT_BLOCKING + return use_on(interacting_with, user, modifiers) return NONE /obj/item/toy/crayon/get_writing_implement_details() @@ -790,6 +789,12 @@ post_noise = FALSE interaction_flags_click = NEED_DEXTERITY|NEED_HANDS|ALLOW_RESTING + /// Types which use their color var for additional logic, so we need to avoid using transition filters on them. + var/static/list/direct_color_types = typecacheof(list( + /obj/item/paper, // Uses color for TGUI backgrounds, doesn't look very good either + /obj/item/fish, // Used for aquarium sprites + )) + /obj/item/toy/crayon/spraycan/Initialize(mapload) . = ..() var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/improvised_coolant) @@ -819,13 +824,14 @@ return . context[SCREENTIP_CONTEXT_LMB] = "Paint" + context[SCREENTIP_CONTEXT_RMB] = "Coat with paint" if(isbodypart(target)) var/obj/item/bodypart/limb = target if(IS_ROBOTIC_LIMB(limb)) - context[SCREENTIP_CONTEXT_RMB] = "Restyle robotic limb" + context[SCREENTIP_CONTEXT_CTRL_LMB] = "Restyle robotic limb" else - context[SCREENTIP_CONTEXT_RMB] = "Copy color" + context[SCREENTIP_CONTEXT_CTRL_LMB] = "Copy color" return CONTEXTUAL_SCREENTIP_SET @@ -881,12 +887,15 @@ return ..() /obj/item/toy/crayon/spraycan/use_on(atom/target, mob/user, list/modifiers) + if (LAZYACCESS(modifiers, CTRL_CLICK)) + return ctrl_interact(target, user) + if(is_capped) balloon_alert(user, "take the cap off first!") - return + return ITEM_INTERACT_BLOCKING if(check_empty(user)) - return + return ITEM_INTERACT_BLOCKING if(iscarbon(target)) if(pre_noise || post_noise) @@ -912,11 +921,15 @@ else if(actually_paints && target.is_atom_colour(paint_color, min_priority_index = WASHABLE_COLOUR_PRIORITY)) balloon_alert(user, "[target.p_theyre()] already that color!") - return + return ITEM_INTERACT_BLOCKING + + var/saturation_mode = SATURATION_MULTIPLY + if (LAZYACCESS(modifiers, RIGHT_CLICK)) + saturation_mode = SATURATION_OVERRIDE if(ismob(target) && (HAS_TRAIT(target, TRAIT_SPRAY_PAINTABLE))) if(actually_paints) - target.add_atom_colour(paint_color, WASHABLE_COLOUR_PRIORITY) + target.add_atom_colour(color_transition_filter(paint_color, saturation_mode), WASHABLE_COLOUR_PRIORITY) SEND_SIGNAL(target, COMSIG_LIVING_MOB_PAINTED) use_charges(user, 2, requires_full = FALSE) reagents.trans_to(target, ., volume_multiplier, transferred_by = user, methods = VAPOR) @@ -924,45 +937,13 @@ if(pre_noise || post_noise) playsound(user.loc, 'sound/effects/spray.ogg', 5, TRUE, 5) user.visible_message(span_notice("[user] coats [target] with spray paint!"), span_notice("You coat [target] with spray paint.")) - return + return ITEM_INTERACT_SUCCESS - if(isobj(target) && !(target.flags_1 & UNPAINTABLE_1)) - var/color_is_dark = FALSE - if(actually_paints) - color_is_dark = is_color_dark(paint_color) - - if (color_is_dark && !(target.flags_1 & ALLOW_DARK_PAINTS_1)) - to_chat(user, span_warning("A color that dark on an object like this? Surely not...")) - return - - if(istype(target, /obj/item/pipe)) - if(GLOB.pipe_color_name.Find(paint_color)) - var/obj/item/pipe/target_pipe = target - target_pipe.pipe_color = paint_color - target.add_atom_colour(paint_color, FIXED_COLOUR_PRIORITY) - balloon_alert(user, "painted in [GLOB.pipe_color_name[paint_color]] color") - else - balloon_alert(user, "invalid pipe color!") - return - else if(istype(target, /obj/machinery/atmospherics)) - if(GLOB.pipe_color_name.Find(paint_color)) - var/obj/machinery/atmospherics/target_pipe = target - target_pipe.paint(paint_color) - balloon_alert(user, "painted in [GLOB.pipe_color_name[paint_color]] color") - else - balloon_alert(user, "invalid pipe color!") - return - else - target.add_atom_colour(paint_color, WASHABLE_COLOUR_PRIORITY) - - if(isitem(target) && isliving(target.loc)) - var/obj/item/target_item = target - var/mob/living/holder = target.loc - if(holder.is_holding(target_item)) - holder.update_held_items() - else - holder.update_clothing(target_item.slot_flags) + if(!isobj(target) || (target.flags_1 & UNPAINTABLE_1)) + return ..() + var/color_is_dark = is_color_dark(paint_color) + if(!actually_paints) if(!(SEND_SIGNAL(target, COMSIG_OBJ_PAINTED, user, src, color_is_dark) & DONT_USE_SPRAYCAN_CHARGES)) use_charges(user, 2, requires_full = FALSE) reagents.trans_to(target, ., volume_multiplier, transferred_by = user, methods = VAPOR) @@ -970,11 +951,50 @@ if(pre_noise || post_noise) playsound(user.loc, 'sound/effects/spray.ogg', 5, TRUE, 5) user.visible_message(span_notice("[user] coats [target] with spray paint!"), span_notice("You coat [target] with spray paint.")) - return + return ITEM_INTERACT_SUCCESS - return ..() + if (color_is_dark && saturation_mode == SATURATION_OVERRIDE && !(target.flags_1 & ALLOW_DARK_PAINTS_1)) + to_chat(user, span_warning("A color that dark on an object like this? Surely not...")) + return ITEM_INTERACT_BLOCKING -/obj/item/toy/crayon/spraycan/interact_with_atom_secondary(atom/interacting_with, mob/living/user, list/modifiers) + if(istype(target, /obj/item/pipe)) + if(!GLOB.pipe_color_name.Find(paint_color)) + balloon_alert(user, "invalid pipe color!") + return ITEM_INTERACT_BLOCKING + var/obj/item/pipe/target_pipe = target + target_pipe.pipe_color = paint_color + target.add_atom_colour(paint_color, FIXED_COLOUR_PRIORITY) + balloon_alert(user, "painted in [GLOB.pipe_color_name[paint_color]] color") + else if(istype(target, /obj/machinery/atmospherics)) + if(!GLOB.pipe_color_name.Find(paint_color)) + balloon_alert(user, "invalid pipe color!") + return ITEM_INTERACT_BLOCKING + var/obj/machinery/atmospherics/target_pipe = target + target_pipe.paint(paint_color) + balloon_alert(user, "painted in [GLOB.pipe_color_name[paint_color]] color") + else if (is_type_in_typecache(target, direct_color_types)) + target.add_atom_colour(paint_color, WASHABLE_COLOUR_PRIORITY) + else + target.add_atom_colour(color_transition_filter(paint_color, saturation_mode), WASHABLE_COLOUR_PRIORITY) + + if(isitem(target) && isliving(target.loc)) + var/obj/item/target_item = target + var/mob/living/holder = target.loc + if(holder.is_holding(target_item)) + holder.update_held_items() + else + holder.update_clothing(target_item.slot_flags) + + if(!(SEND_SIGNAL(target, COMSIG_OBJ_PAINTED, user, src, color_is_dark) & DONT_USE_SPRAYCAN_CHARGES)) + use_charges(user, 2, requires_full = FALSE) + reagents.trans_to(target, ., volume_multiplier, transferred_by = user, methods = VAPOR) + + if(pre_noise || post_noise) + playsound(user.loc, 'sound/effects/spray.ogg', 5, TRUE, 5) + user.visible_message(span_notice("[user] coats [target] with spray paint!"), span_notice("You coat [target] with spray paint.")) + return ITEM_INTERACT_SUCCESS + +/obj/item/toy/crayon/spraycan/proc/ctrl_interact(atom/interacting_with, mob/living/user) if(is_capped) if(!interacting_with.color) // let's be generous and assume if they're trying to match something with no color, while capped, @@ -982,31 +1002,41 @@ return NONE balloon_alert(user, "take the cap off first!") return ITEM_INTERACT_BLOCKING + if(check_empty(user)) return ITEM_INTERACT_BLOCKING - if(isbodypart(interacting_with) && actually_paints) - var/obj/item/bodypart/limb = interacting_with - if(IS_ROBOTIC_LIMB(limb)) - var/list/skins = list() - var/static/list/style_list_icons = list("standard" = 'icons/mob/augmentation/augments.dmi', "engineer" = 'icons/mob/augmentation/augments_engineer.dmi', "security" = 'icons/mob/augmentation/augments_security.dmi', "mining" = 'icons/mob/augmentation/augments_mining.dmi') - for(var/skin_option in style_list_icons) - var/image/part_image = image(icon = style_list_icons[skin_option], icon_state = "[limb.limb_id]_[limb.body_zone]") - if(limb.aux_zone) //Hands - part_image.overlays += image(icon = style_list_icons[skin_option], icon_state = "[limb.limb_id]_[limb.aux_zone]") - skins += list("[skin_option]" = part_image) - var/choice = show_radial_menu(user, src, skins, require_near = TRUE) - if(choice && (use_charges(user, 5, requires_full = FALSE))) - playsound(user.loc, 'sound/effects/spray.ogg', 5, TRUE, 5) - limb.change_appearance(style_list_icons[choice], greyscale = FALSE) - return ITEM_INTERACT_SUCCESS - if(interacting_with.color) - paint_color = interacting_with.color - balloon_alert(user, "matched colour of target") - update_appearance() + if(!isbodypart(interacting_with) || !actually_paints) + if(interacting_with.color) + paint_color = interacting_with.color + balloon_alert(user, "matched colour of target") + update_appearance() + return ITEM_INTERACT_BLOCKING + balloon_alert(user, "can't match those colours!") return ITEM_INTERACT_BLOCKING - balloon_alert(user, "can't match those colours!") - return ITEM_INTERACT_BLOCKING + + var/obj/item/bodypart/limb = interacting_with + if(!IS_ROBOTIC_LIMB(limb)) + return ITEM_INTERACT_BLOCKING + + var/list/skins = list() + var/static/list/style_list_icons = list( + "standard" = 'icons/mob/augmentation/augments.dmi', + "engineer" = 'icons/mob/augmentation/augments_engineer.dmi', + "security" = 'icons/mob/augmentation/augments_security.dmi', + "mining" = 'icons/mob/augmentation/augments_mining.dmi', + ) + + for(var/skin_option in style_list_icons) + var/image/part_image = image(icon = style_list_icons[skin_option], icon_state = "[limb.limb_id]_[limb.body_zone]") + if(limb.aux_zone) //Hands + part_image.overlays += image(icon = style_list_icons[skin_option], icon_state = "[limb.limb_id]_[limb.aux_zone]") + skins += list("[skin_option]" = part_image) + var/choice = show_radial_menu(user, src, skins, require_near = TRUE) + if(choice && (use_charges(user, 5, requires_full = FALSE))) + playsound(user.loc, 'sound/effects/spray.ogg', 5, TRUE, 5) + limb.change_appearance(style_list_icons[choice], greyscale = FALSE) + return ITEM_INTERACT_SUCCESS /obj/item/toy/crayon/spraycan/click_alt(mob/user) if(!has_cap) diff --git a/code/game/objects/items/paint.dm b/code/game/objects/items/paint.dm index 66e0b15e99f..7d5cc7eabe8 100644 --- a/code/game/objects/items/paint.dm +++ b/code/game/objects/items/paint.dm @@ -118,7 +118,10 @@ if(paintleft <= 0) return NONE paintleft-- - interacting_with.add_atom_colour(paint_color, WASHABLE_COLOUR_PRIORITY) + var/color_type = SATURATION_MULTIPLY + if (LAZYACCESS(modifiers, RIGHT_CLICK)) + color_type = SATURATION_OVERRIDE + interacting_with.add_atom_colour(color_transition_filter(paint_color, color_type), WASHABLE_COLOUR_PRIORITY) if(paintleft <= 0) icon_state = "paint_empty" return ITEM_INTERACT_SUCCESS diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm index 6046d356892..207b85122dd 100644 --- a/code/game/objects/structures/crates_lockers/closets.dm +++ b/code/game/objects/structures/crates_lockers/closets.dm @@ -236,7 +236,8 @@ GLOBAL_LIST_EMPTY(roundstart_station_closets) . += door_overlay door_overlay.overlays += emissive_blocker(door_overlay.icon, door_overlay.icon_state, src, alpha = door_overlay.alpha) // If we don't do this the door doesn't block emissives and it looks weird. else if(has_closed_overlay) - . += "[icon_door || overlay_state]_door" + var/mutable_appearance/door_overlay = mutable_appearance(icon, "[icon_door || overlay_state]_door", alpha = src.alpha) + . += door_overlay if(opened) return diff --git a/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer_items.dm b/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer_items.dm index 6308e1eee61..a7ad6f4664f 100644 --- a/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer_items.dm +++ b/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer_items.dm @@ -29,7 +29,7 @@ to_chat(user, span_notice("You see how the [worn_item] changes color, it's now pressure proof.")) worn_item.name = "pressure-resistant [worn_item.name]" worn_item.remove_atom_colour(WASHABLE_COLOUR_PRIORITY) - worn_item.add_atom_colour("#00fff7", FIXED_COLOUR_PRIORITY) + worn_item.add_atom_colour(color_transition_filter("#00fff7", SATURATION_OVERRIDE), FIXED_COLOUR_PRIORITY) worn_item.min_cold_protection_temperature = SPACE_SUIT_MIN_TEMP_PROTECT worn_item.cold_protection = worn_item.body_parts_covered worn_item.clothing_flags |= STOPSPRESSUREDAMAGE diff --git a/code/modules/mob/living/carbon/human/human_update_icons.dm b/code/modules/mob/living/carbon/human/human_update_icons.dm index 6227e9558db..a48a1c95482 100644 --- a/code/modules/mob/living/carbon/human/human_update_icons.dm +++ b/code/modules/mob/living/carbon/human/human_update_icons.dm @@ -837,7 +837,7 @@ generate/load female uniform sprites matching all previously decided variables standing.pixel_y += offsets[2] standing.alpha = alpha - standing.color = color + standing = color_atom_overlay(standing) return standing diff --git a/code/modules/mod/mod_control.dm b/code/modules/mod/mod_control.dm index c658cfb0dcc..8d5c5c20954 100644 --- a/code/modules/mod/mod_control.dm +++ b/code/modules/mod/mod_control.dm @@ -757,7 +757,7 @@ to_chat(user, span_warning("It's too dangerous to smear [speed_potion] on [src] while it's active!")) return SPEED_POTION_STOP to_chat(user, span_notice("You slather the red gunk over [src], making it faster.")) - set_mod_color(COLOR_RED) + set_mod_color(color_transition_filter(COLOR_RED)) slowdown_inactive = 0 slowdown_active = 0 update_speed() diff --git a/code/modules/power/lighting/light.dm b/code/modules/power/lighting/light.dm index 6529fcff965..4d38d6944fe 100644 --- a/code/modules/power/lighting/light.dm +++ b/code/modules/power/lighting/light.dm @@ -222,6 +222,8 @@ var/color_set = bulb_colour if(color) color_set = color + if (cached_color_filter) + color_set = apply_matrix_to_color(color_set, cached_color_filter["color"], cached_color_filter["space"] || COLORSPACE_RGB) if(reagents) START_PROCESSING(SSmachines, src) var/area/local_area = get_room_area() diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm index 3137453d2e9..4444ef843c8 100644 --- a/code/modules/reagents/chemistry/reagents/other_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm @@ -2227,13 +2227,13 @@ /datum/reagent/colorful_reagent/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired) . = ..() if(can_colour_mobs) - affected_mob.add_atom_colour(pick(random_color_list), WASHABLE_COLOUR_PRIORITY) + affected_mob.add_atom_colour(color_transition_filter(pick(random_color_list), SATURATION_OVERRIDE), WASHABLE_COLOUR_PRIORITY) /// Colors anything it touches a random color. /datum/reagent/colorful_reagent/expose_atom(atom/exposed_atom, reac_volume) . = ..() if(!isliving(exposed_atom) || can_colour_mobs) - exposed_atom.add_atom_colour(pick(random_color_list), WASHABLE_COLOUR_PRIORITY) + exposed_atom.add_atom_colour(color_transition_filter(pick(random_color_list), SATURATION_OVERRIDE), WASHABLE_COLOUR_PRIORITY) /datum/reagent/hair_dye name = "Quantum Hair Dye" diff --git a/code/modules/research/xenobiology/crossbreeding/_potions.dm b/code/modules/research/xenobiology/crossbreeding/_potions.dm index f3477963f43..2f82396168e 100644 --- a/code/modules/research/xenobiology/crossbreeding/_potions.dm +++ b/code/modules/research/xenobiology/crossbreeding/_potions.dm @@ -125,7 +125,7 @@ Slimecrossing Potions to_chat(user, span_notice("You slather the blue gunk over the [clothing], making it airtight.")) clothing.name = "pressure-resistant [clothing.name]" clothing.remove_atom_colour(WASHABLE_COLOUR_PRIORITY) - clothing.add_atom_colour(COLOR_NAVY, FIXED_COLOUR_PRIORITY) + clothing.add_atom_colour(color_transition_filter(COLOR_NAVY, SATURATION_OVERRIDE), FIXED_COLOUR_PRIORITY) clothing.min_cold_protection_temperature = SPACE_SUIT_MIN_TEMP_PROTECT clothing.cold_protection = clothing.body_parts_covered clothing.clothing_flags |= STOPSPRESSUREDAMAGE @@ -165,7 +165,7 @@ Slimecrossing Potions to_chat(user, span_notice("You slather the red gunk over the [clothing], making it lavaproof.")) clothing.name = "lavaproof [clothing.name]" clothing.remove_atom_colour(WASHABLE_COLOUR_PRIORITY) - clothing.add_atom_colour(COLOR_MAROON, FIXED_COLOUR_PRIORITY) + clothing.add_atom_colour(color_transition_filter(COLOR_MAROON, SATURATION_OVERRIDE), FIXED_COLOUR_PRIORITY) clothing.resistance_flags |= LAVA_PROOF if (isclothing(clothing)) var/obj/item/clothing/clothing_real = clothing diff --git a/code/modules/research/xenobiology/crossbreeding/prismatic.dm b/code/modules/research/xenobiology/crossbreeding/prismatic.dm index 947323b0e47..647711192af 100644 --- a/code/modules/research/xenobiology/crossbreeding/prismatic.dm +++ b/code/modules/research/xenobiology/crossbreeding/prismatic.dm @@ -14,7 +14,7 @@ Prismatic extracts: if(!isturf(interacting_with) || isspaceturf(interacting_with)) return NONE user.do_attack_animation(interacting_with) - interacting_with.add_atom_colour(paintcolor, WASHABLE_COLOUR_PRIORITY) + interacting_with.add_atom_colour(color_transition_filter(paintcolor, SATURATION_OVERRIDE), WASHABLE_COLOUR_PRIORITY) playsound(interacting_with, 'sound/effects/slosh.ogg', 20, TRUE) return ITEM_INTERACT_SUCCESS diff --git a/code/modules/research/xenobiology/crossbreeding/regenerative.dm b/code/modules/research/xenobiology/crossbreeding/regenerative.dm index 99ac6b3c1af..a0d9a1e89ef 100644 --- a/code/modules/research/xenobiology/crossbreeding/regenerative.dm +++ b/code/modules/research/xenobiology/crossbreeding/regenerative.dm @@ -128,13 +128,13 @@ Regenerative extracts: if(fireproofed) target.visible_message(span_notice("Some of [target]'s clothing gets coated in the goo, and turns blue!")) -/obj/item/slimecross/regenerative/darkblue/proc/fireproof(obj/item/clothing/C) - C.name = "fireproofed [C.name]" - C.remove_atom_colour(WASHABLE_COLOUR_PRIORITY) - C.add_atom_colour(COLOR_NAVY, FIXED_COLOUR_PRIORITY) - C.max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT - C.heat_protection = C.body_parts_covered - C.resistance_flags |= FIRE_PROOF +/obj/item/slimecross/regenerative/darkblue/proc/fireproof(obj/item/clothing/clothing_piece) + clothing_piece.name = "fireproofed [clothing_piece.name]" + clothing_piece.remove_atom_colour(WASHABLE_COLOUR_PRIORITY) + clothing_piece.add_atom_colour(color_transition_filter(COLOR_NAVY, SATURATION_OVERRIDE), FIXED_COLOUR_PRIORITY) + clothing_piece.max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT + clothing_piece.heat_protection = clothing_piece.body_parts_covered + clothing_piece.resistance_flags |= FIRE_PROOF /obj/item/slimecross/regenerative/silver colour = SLIME_TYPE_SILVER @@ -188,7 +188,7 @@ Regenerative extracts: /obj/item/slimecross/regenerative/pyrite/core_effect(mob/living/target, mob/user) target.visible_message(span_warning("The milky goo coating [target] leaves [target.p_them()] a different color!")) - target.add_atom_colour(rgb(rand(0,255),rand(0,255),rand(0,255)),WASHABLE_COLOUR_PRIORITY) + target.add_atom_colour(color_transition_filter(rgb(rand(0,255), rand(0,255), rand(0,255)), SATURATION_OVERRIDE), WASHABLE_COLOUR_PRIORITY) /obj/item/slimecross/regenerative/red colour = SLIME_TYPE_RED diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm index a3e760104c7..f63ee0f0880 100644 --- a/code/modules/research/xenobiology/xenobiology.dm +++ b/code/modules/research/xenobiology/xenobiology.dm @@ -921,7 +921,7 @@ to_chat(user, span_notice("You slather the red gunk over the [interacting_with], making it faster.")) interacting_with.remove_atom_colour(WASHABLE_COLOUR_PRIORITY) - interacting_with.add_atom_colour(COLOR_RED, FIXED_COLOUR_PRIORITY) + interacting_with.add_atom_colour(color_transition_filter(COLOR_RED, SATURATION_OVERRIDE), FIXED_COLOUR_PRIORITY) interacting_with.drag_slowdown = 0 ADD_TRAIT(interacting_with, TRAIT_SPEED_POTIONED, SLIME_POTION_TRAIT) qdel(src) @@ -952,7 +952,7 @@ to_chat(user, span_notice("You slather the blue gunk over the [clothing], fireproofing it.")) clothing.name = "fireproofed [clothing.name]" clothing.remove_atom_colour(WASHABLE_COLOUR_PRIORITY) - clothing.add_atom_colour(COLOR_NAVY, FIXED_COLOUR_PRIORITY) + clothing.add_atom_colour(color_transition_filter(COLOR_NAVY, SATURATION_OVERRIDE), FIXED_COLOUR_PRIORITY) clothing.max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT clothing.heat_protection = clothing.body_parts_covered clothing.resistance_flags |= FIRE_PROOF