diff --git a/code/datums/greyscale/_greyscale_config.dm b/code/datums/greyscale/_greyscale_config.dm index 71bd56fbcb1..caf27af0139 100644 --- a/code/datums/greyscale/_greyscale_config.dm +++ b/code/datums/greyscale/_greyscale_config.dm @@ -1,4 +1,7 @@ /datum/greyscale_config + /// User friendly name used in the debug menu + var/name + /// Reference to the json config file var/json_config @@ -115,18 +118,31 @@ expected_colors = length(color_groups) /// Actually create the icon and color it in, handles caching -/datum/greyscale_config/proc/Generate(color_string, list/render_steps) +/datum/greyscale_config/proc/Generate(color_string) var/key = color_string var/icon/new_icon = icon_cache[key] - if(new_icon && !render_steps) - var/icon/output = icon(new_icon) - output.Scale(width, height) - return output - var/list/colors = ParseColorString(color_string) + if(new_icon) + return icon(new_icon) + + var/icon/icon_bundle = GenerateBundle(color_string) + + // This block is done like this because generated icons are unable to be scaled before getting added to the rsc + icon_bundle = fcopy_rsc(icon_bundle) + icon_bundle = icon(icon_bundle) + icon_bundle.Scale(width, height) + icon_bundle = fcopy_rsc(icon_bundle) + + icon_cache[key] = icon_bundle + var/icon/output = icon(icon_bundle) + return output + +/// Handles the actual icon manipulation to create the spritesheet +/datum/greyscale_config/proc/GenerateBundle(list/colors, list/render_steps) + if(!istype(colors)) + colors = ParseColorString(colors) if(length(colors) != expected_colors) CRASH("[DebugName()] expected [expected_colors] color arguments but only received [length(colors)]") - // We could do this all in one loop but it's easier to debug this way var/list/generated_icons = list() for(var/icon_state in icon_states) var/icon/generated_icon = GenerateLayerGroup(colors, icon_states[icon_state], render_steps) @@ -135,15 +151,11 @@ generated_icon.GetPixel(1, 1) generated_icons[icon_state] = generated_icon - var/icon/icon_bundle = new + var/icon/icon_bundle = icon('icons/Testing/greyscale_error.dmi') for(var/icon_state in generated_icons) icon_bundle.Insert(generated_icons[icon_state], icon_state) - icon_bundle = fcopy_rsc(icon_bundle) - icon_cache[key] = icon_bundle - var/icon/output = icon(icon_bundle) - output.Scale(width, height) - return output + return icon_bundle /// Internal recursive proc to handle nested layer groups /datum/greyscale_config/proc/GenerateLayerGroup(list/colors, list/group, list/render_steps) @@ -171,7 +183,7 @@ var/list/debug_steps = list() output["steps"] = debug_steps - output["icon"] = Generate(colors, debug_steps) + output["icon"] = GenerateBundle(colors, debug_steps) return output /datum/greyscale_config/proc/ParseColorString(color_string) diff --git a/code/datums/greyscale/greyscale_configs.dm b/code/datums/greyscale/greyscale_configs.dm index 45280f210ab..02604792b4f 100644 --- a/code/datums/greyscale/greyscale_configs.dm +++ b/code/datums/greyscale/greyscale_configs.dm @@ -1,159 +1,204 @@ /datum/greyscale_config/canister + name = "Default Canister" icon_file = 'icons/obj/atmospherics/canisters.dmi' json_config = 'code/datums/greyscale/json_configs/canister_default.json' /datum/greyscale_config/canister/base + name = "Base Canister Style" json_config = 'code/datums/greyscale/json_configs/canister_base.json' /datum/greyscale_config/canister/post_effects + name = "Canister Post-Effects" json_config = 'code/datums/greyscale/json_configs/canister_post_effects.json' /datum/greyscale_config/canister/stripe + name = "Single Striped Canister" json_config = 'code/datums/greyscale/json_configs/canister_stripe.json' /datum/greyscale_config/canister/double_stripe + name = "Double Striped Canister" json_config = 'code/datums/greyscale/json_configs/canister_double_stripe.json' /datum/greyscale_config/canister/hazard + name = "Hazard Striped Canister" json_config = 'code/datums/greyscale/json_configs/canister_hazard.json' /datum/greyscale_config/prototype_canister + name = "Prototype Canister" icon_file = 'icons/obj/atmospherics/prototype_canister.dmi' json_config = 'code/datums/greyscale/json_configs/canister_proto.json' /datum/greyscale_config/carp + name = "Space Carp" icon_file = 'icons/mob/carp.dmi' json_config = 'code/datums/greyscale/json_configs/carp.json' /datum/greyscale_config/carp/disk_mouth + name = "Space Carp, Disk in Mouth" json_config = 'code/datums/greyscale/json_configs/carp_disk_mouth.json' /datum/greyscale_config/wirecutters + name = "Wirecutters" icon_file = 'icons/obj/tools.dmi' json_config = 'code/datums/greyscale/json_configs/wirecutters.json' /datum/greyscale_config/screwdriver + name = "Screwdriver" icon_file = 'icons/obj/tools.dmi' json_config = 'code/datums/greyscale/json_configs/screwdriver.json' /datum/greyscale_config/screwdriver_inhand_left + name = "Held Screwdriver, Left" icon_file = 'icons/mob/inhands/equipment/tools_lefthand.dmi' json_config = 'code/datums/greyscale/json_configs/screwdriver_worn.json' /datum/greyscale_config/screwdriver_inhand_right + name = "Held Screwdriver, Right" icon_file = 'icons/mob/inhands/equipment/tools_righthand.dmi' json_config = 'code/datums/greyscale/json_configs/screwdriver_worn.json' /datum/greyscale_config/screwdriver_belt + name = "Belt Worn Screwdriver" icon_file = 'icons/obj/clothing/belt_overlays.dmi' json_config = 'code/datums/greyscale/json_configs/screwdriver_worn.json' /datum/greyscale_config/jumpsuit + name = "Jumpsuit" icon_file = 'icons/obj/clothing/under/color.dmi' json_config = 'code/datums/greyscale/json_configs/jumpsuit.json' /datum/greyscale_config/jumpsuit_worn + name = "Worn Jumpsuit" icon_file = 'icons/mob/clothing/under/color.dmi' json_config = 'code/datums/greyscale/json_configs/jumpsuit_worn.json' /datum/greyscale_config/jumpsuit_inhand_left + name = "Held Jumpsuit, Left" icon_file = 'icons/mob/inhands/clothing_lefthand.dmi' json_config = 'code/datums/greyscale/json_configs/jumpsuit_inhand.json' /datum/greyscale_config/jumpsuit_inhand_right + name = "Held Jumpsuit, Right" icon_file = 'icons/mob/inhands/clothing_righthand.dmi' json_config = 'code/datums/greyscale/json_configs/jumpsuit_inhand.json' /datum/greyscale_config/jumpsuit_prison + name = "Prison Jumpsuit" icon_file = 'icons/obj/clothing/under/color.dmi' json_config = 'code/datums/greyscale/json_configs/jumpsuit_prison.json' /datum/greyscale_config/jumpsuit_prison_worn + name = "Worn Prison Jumpsuit" icon_file = 'icons/mob/clothing/under/color.dmi' json_config = 'code/datums/greyscale/json_configs/jumpsuit_prison_worn.json' /datum/greyscale_config/jumpsuit_prison_inhand_left + name = "Held Prison Jumpsuit, Left" icon_file = 'icons/mob/inhands/clothing_lefthand.dmi' json_config = 'code/datums/greyscale/json_configs/jumpsuit_prison_inhand.json' /datum/greyscale_config/jumpsuit_prison_inhand_right + name = "Held Prison Jumpsuit, Right" icon_file = 'icons/mob/inhands/clothing_righthand.dmi' json_config = 'code/datums/greyscale/json_configs/jumpsuit_prison_inhand.json' /datum/greyscale_config/pda + name = "PDA" icon_file = 'icons/obj/pda.dmi' json_config = 'code/datums/greyscale/json_configs/pda.json' /datum/greyscale_config/pda/chaplain + name = "Chaplain PDA" json_config = 'code/datums/greyscale/json_configs/pda_chaplain.json' /datum/greyscale_config/pda/clown + name = "Clown PDA" json_config = 'code/datums/greyscale/json_configs/pda_clown.json' /datum/greyscale_config/pda/head + name = "Head PDA" json_config = 'code/datums/greyscale/json_configs/pda_head.json' /datum/greyscale_config/pda/mime + name = "Mime PDA" json_config = 'code/datums/greyscale/json_configs/pda_mime.json' /datum/greyscale_config/pda/stripe_split + name = "Split Stripe PDA" json_config = 'code/datums/greyscale/json_configs/pda_stripe_split.json' /datum/greyscale_config/pda/stripe_thick + name = "Thick Stripe PDA" json_config = 'code/datums/greyscale/json_configs/pda_stripe_thick.json' /datum/greyscale_config/pda/stripe_thick/head + name = "Head Thick Stripe PDA" json_config = 'code/datums/greyscale/json_configs/pda_stripe_thick_head.json' /datum/greyscale_config/sneakers + name = "Sneakers" icon_file = 'icons/obj/clothing/shoes.dmi' json_config = 'code/datums/greyscale/json_configs/sneakers.json' /datum/greyscale_config/sneakers_worn + name = "Worn Sneakers" icon_file = 'icons/mob/clothing/feet.dmi' json_config = 'code/datums/greyscale/json_configs/sneakers_worn.json' /datum/greyscale_config/sneakers_orange + name = "Orange Sneakers" icon_file = 'icons/obj/clothing/shoes.dmi' json_config = 'code/datums/greyscale/json_configs/sneakers_orange.json' /datum/greyscale_config/sneakers_orange_worn + name = "Worn Orange Sneakers" icon_file = 'icons/mob/clothing/feet.dmi' json_config = 'code/datums/greyscale/json_configs/sneakers_orange_worn.json' /datum/greyscale_config/sneakers_wheelys + name = "Wheeled Sneakers" icon_file = 'icons/obj/clothing/shoes.dmi' json_config = 'code/datums/greyscale/json_configs/sneakers_wheelys.json' /datum/greyscale_config/sneakers_marisa + name = "Marisa Sneakers" icon_file = 'icons/obj/clothing/shoes.dmi' json_config = 'code/datums/greyscale/json_configs/sneakers_marisa.json' /datum/greyscale_config/circuit + name = "Circuit Board" icon_file = 'icons/obj/module.dmi' json_config = 'code/datums/greyscale/json_configs/circuit.json' /datum/greyscale_config/sombrero + name = "Sombrero" icon_file = 'icons/obj/clothing/head/sombrero.dmi' json_config = 'code/datums/greyscale/json_configs/sombrero.json' /datum/greyscale_config/sombrero/base + name = "Base Sombrero Style" json_config = 'code/datums/greyscale/json_configs/sombrero_base.json' /datum/greyscale_config/sombrero/lefthand + name = "Held Sombrero, Left" json_config = 'code/datums/greyscale/json_configs/sombrero_lefthand.json' /datum/greyscale_config/sombrero/base_lefthand + name = "Base Held Sombrero Style, Left" json_config = 'code/datums/greyscale/json_configs/sombrero_base_lefthand.json' /datum/greyscale_config/sombrero/righthand + name = "Held Sombrero, Right" json_config = 'code/datums/greyscale/json_configs/sombrero_righthand.json' /datum/greyscale_config/sombrero/base_righthand + name = "Base Held Sombrero Style, Right" json_config = 'code/datums/greyscale/json_configs/sombrero_base_righthand.json' /datum/greyscale_config/sombrero/worn + name = "Worn Sombrero" json_config = 'code/datums/greyscale/json_configs/sombrero_worn.json' /datum/greyscale_config/sombrero/base_worn + name = "Base Worn Sombrero Style" json_config = 'code/datums/greyscale/json_configs/sombrero_base_worn.json' diff --git a/code/game/atoms.dm b/code/game/atoms.dm index 4c0d3f13608..c255ca562f5 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -223,7 +223,8 @@ if(loc) SEND_SIGNAL(loc, COMSIG_ATOM_CREATED, src) /// Sends a signal that the new atom `src`, has been created at `loc` - update_greyscale() + if(greyscale_config && greyscale_colors) + update_greyscale() //atom color stuff if(color) @@ -737,7 +738,7 @@ greyscale_colors = colors if(!greyscale_config) return - if(update) + if(update && greyscale_config && greyscale_colors) update_greyscale() /// Checks if the greyscale config given is different and if so causes a greyscale icon update @@ -745,13 +746,12 @@ if(greyscale_config == new_config) return greyscale_config = new_config - if(update) + if(update && greyscale_config && greyscale_colors) update_greyscale() /// Checks if this atom uses the GAS system and if so updates the icon /atom/proc/update_greyscale() - if(greyscale_config && greyscale_colors) - icon = SSgreyscale.GetColoredIconByType(greyscale_config, greyscale_colors) + icon = SSgreyscale.GetColoredIconByType(greyscale_config, greyscale_colors) /** * An atom we are buckled or is contained within us has tried to move diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index e5c0c4a0018..76a3eabf9a2 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -207,7 +207,6 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e for(var/path in actions_types) new path(src) actions_types = null - update_item_greyscale() if(force_string) item_flags |= FORCE_STRING_OVERRIDE @@ -255,7 +254,8 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e return /// Checks if this atom uses the GAS system and if so updates the worn and inhand icons -/obj/item/proc/update_item_greyscale() +/obj/item/update_greyscale() + . = ..() if(!greyscale_colors) return if(greyscale_config_worn) @@ -264,7 +264,6 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e lefthand_file = SSgreyscale.GetColoredIconByType(greyscale_config_inhand_left, greyscale_colors) if(greyscale_config_inhand_right) righthand_file = SSgreyscale.GetColoredIconByType(greyscale_config_inhand_right, greyscale_colors) - return /obj/item/verb/move_to_top() set name = "Move To Top" diff --git a/code/game/objects/items/tools/screwdriver.dm b/code/game/objects/items/tools/screwdriver.dm index 51ca8958907..e813a6f2bfd 100644 --- a/code/game/objects/items/tools/screwdriver.dm +++ b/code/game/objects/items/tools/screwdriver.dm @@ -25,6 +25,9 @@ drop_sound = 'sound/items/handling/screwdriver_drop.ogg' pickup_sound = 'sound/items/handling/screwdriver_pickup.ogg' sharpness = SHARP_POINTY + greyscale_config = /datum/greyscale_config/screwdriver + greyscale_config_inhand_left = /datum/greyscale_config/screwdriver_inhand_left + greyscale_config_inhand_right = /datum/greyscale_config/screwdriver_inhand_right /// If the item should be assigned a random color var/random_color = TRUE /// List of possible random colors @@ -46,12 +49,9 @@ /obj/item/screwdriver/Initialize() if(random_color) - set_greyscale_config(/datum/greyscale_config/screwdriver) var/our_color = pick(screwdriver_colors) set_greyscale_colors(list(screwdriver_colors[our_color])) inhand_icon_state = null - lefthand_file = SSgreyscale.GetColoredIconByType(/datum/greyscale_config/screwdriver_inhand_left, greyscale_colors) - righthand_file = SSgreyscale.GetColoredIconByType(/datum/greyscale_config/screwdriver_inhand_right, greyscale_colors) colored_belt_appearance = mutable_appearance(SSgreyscale.GetColoredIconByType(/datum/greyscale_config/screwdriver_belt, greyscale_colors)) . = ..() AddElement(/datum/element/eyestab) diff --git a/code/modules/admin/greyscale_modify_menu.dm b/code/modules/admin/greyscale_modify_menu.dm index 182493e9216..2f7d4df7894 100644 --- a/code/modules/admin/greyscale_modify_menu.dm +++ b/code/modules/admin/greyscale_modify_menu.dm @@ -1,31 +1,68 @@ +/// The controller for the ui in charge of all runtime greyscale configuration/debug. +/// If `Unlock()` is not called the menu is safe for players to use. /datum/greyscale_modify_menu + /// The "owner" object of this menu, is usually the greyscale object being edited but that can be changed for specific uses of this menu var/atom/target + /// The client that opened this menu var/client/user + /// A keyed list of allowed configs in the form niceName:typepath + var/list/allowed_configs + + /// A callback to control what happens when the user presses apply. Used mainly for if you want the menu to be used outside of vv. + var/datum/callback/apply_callback + + /// The current config being previewed var/datum/greyscale_config/config + /// A list of colors currently selected var/list/split_colors + /// Collection of data for tgui to use in displaying everything var/list/sprite_data + /// The sprite dir currently being shown var/sprite_dir = SOUTH + /// The sprite icon state currently being shown + var/icon_state + /// Whether the full preview should be generated, with this FALSE only the final sprite is shown instead of all steps. + var/generate_full_preview = FALSE -/datum/greyscale_modify_menu/New(atom/target, client/user) + /// Whether the menu is in the middle of refreshing the preview + var/refreshing = TRUE + + /// Whether the menu is currently locked down to prevent abuse from players. Currently is only unlocked when opened from vv. + var/unlocked = FALSE + +/datum/greyscale_modify_menu/New(atom/target, client/user, list/allowed_configs, datum/callback/apply_callback, starting_icon_state="", starting_config, starting_colors) src.target = target src.user = user + src.apply_callback = apply_callback || CALLBACK(src, .proc/DefaultApply) + icon_state = starting_icon_state - config = SSgreyscale.configurations["[target.greyscale_config]"] - ReadColorsFromString(target.greyscale_colors) + var/current_config = "[starting_config]" || "[target?.greyscale_config]" + config = SSgreyscale.configurations[current_config] + if(!(current_config in allowed_configs)) + config = SSgreyscale.configurations["[allowed_configs[pick(allowed_configs)]]"] + + var/list/config_choices = list() + for(var/config_string in allowed_configs) + var/datum/greyscale_config/config = text2path("[config_string]") + config_choices[initial(config.name)] = config_string + src.allowed_configs = config_choices + + ReadColorsFromString(starting_colors || target?.greyscale_colors) + + if(target) + RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/ui_close) refresh_preview() - RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/ui_close) - /datum/greyscale_modify_menu/Destroy() target = null user = null return ..() /datum/greyscale_modify_menu/ui_state(mob/user) - return GLOB.admin_state + return GLOB.always_state /datum/greyscale_modify_menu/ui_close() qdel(src) @@ -38,7 +75,7 @@ /datum/greyscale_modify_menu/ui_data(mob/user) var/list/data = list() - data["greyscale_config"] = "[config.type]" + data["greyscale_config"] = "[config.name]" var/list/color_data = list() data["colors"] = color_data @@ -48,7 +85,11 @@ "value" = split_colors[i] )) + data["generate_full_preview"] = generate_full_preview + data["unlocked"] = unlocked + data["refreshing"] = refreshing data["sprites_dir"] = dir2text(sprite_dir) + data["icon_state"] = icon_state data["sprites"] = sprite_data return data @@ -63,26 +104,35 @@ "Choose a new greyscale configuration to use", "Greyscale Modification Menu", "[config.type]" - ) as anything in SSgreyscale.configurations + ) as anything in allowed_configs + new_config = allowed_configs[new_config] new_config = SSgreyscale.configurations[new_config] if(!isnull(new_config) && config != new_config) config = new_config - refresh_preview() + queue_refresh() if("load_config_from_string") + if(!(params["config_string"] in allowed_configs)) + return var/datum/greyscale_config/new_config = SSgreyscale.configurations[params["config_string"]] if(!isnull(new_config) && config != new_config) config = new_config - refresh_preview() + queue_refresh() + + if("toggle_full_preview") + if(!generate_full_preview && !unlocked) + return + generate_full_preview = !generate_full_preview + queue_refresh() if("recolor") var/index = text2num(params["color_index"]) split_colors[index] = lowertext(params["new_color"]) - refresh_preview() + queue_refresh() if("recolor_from_string") ReadColorsFromString(lowertext(params["color_string"])) - refresh_preview() + queue_refresh() if("pick_color") var/group = params["color_index"] @@ -94,20 +144,38 @@ ) as color|null if(new_color) split_colors[group] = new_color - refresh_preview() + queue_refresh() + + if("select_icon_state") + var/new_icon_state = params["new_icon_state"] + if(!config.icon_states[new_icon_state]) + return + icon_state = new_icon_state + queue_refresh() if("apply") - target.set_greyscale_config(config.type, update=FALSE) - target.greyscale_colors = "" // We do this to force an update, in some cases it will think nothing changed when it should be refreshing - target.set_greyscale_colors(split_colors) + apply_callback.Invoke(src) if("refresh_file") + if(!unlocked) + return + if(length(GLOB.player_list) > 1) + var/check = alert( + user, +{"Other players are connected to the server, are you sure you want to refresh all greyscale configurations?\n +This is highly likely to cause a lag spike for a few seconds."}, + "Refresh Greyscale Configurations", + "Yes", + "Cancel" + ) + if(check != "Yes") + return SSgreyscale.RefreshConfigsFromFile() - refresh_preview() + queue_refresh() if("change_dir") sprite_dir = text2dir(params["new_sprite_dir"]) - refresh_preview() + queue_refresh() /datum/greyscale_modify_menu/proc/ReadColorsFromString(colorString) var/list/raw_colors = splittext(colorString, "#") @@ -115,22 +183,54 @@ for(var/i in 2 to length(raw_colors)) split_colors += "#[raw_colors[i]]" +/datum/greyscale_modify_menu/proc/queue_refresh() + refreshing = TRUE + addtimer(CALLBACK(src, .proc/refresh_preview), 1 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE) + /datum/greyscale_modify_menu/proc/refresh_preview() for(var/i in length(split_colors) + 1 to config.expected_colors) split_colors += rgb(100, 100, 100) var/list/used_colors = split_colors.Copy(1, config.expected_colors+1) - var/list/data = config.GenerateDebug(used_colors.Join()) sprite_data = list() - var/list/steps = list() - sprite_data["steps"] = steps - for(var/step in data["steps"]) - CHECK_TICK - var/image/layer = image(data["steps"][step]) - var/image/result = image(step) - steps += list( - list( - "layer"=icon2html(layer, user, dir=sprite_dir, sourceonly=TRUE), - "result"=icon2html(result, user, dir=sprite_dir, sourceonly=TRUE) + + var/list/generated_icon_states = list() + for(var/state in config.icon_states) + generated_icon_states += state // We don't want the values from this keyed list + sprite_data["icon_states"] = generated_icon_states + + if(!(icon_state in generated_icon_states)) + icon_state = target.icon_state + if(!(icon_state in generated_icon_states)) + icon_state = pick(generated_icon_states) + + var/image/finished + if(!generate_full_preview) + finished = image(config.GenerateBundle(used_colors), icon_state=icon_state) + else + var/list/data = config.GenerateDebug(used_colors.Join()) + finished = image(data["icon"], icon_state=icon_state) + var/list/steps = list() + sprite_data["steps"] = steps + for(var/step in data["steps"]) + CHECK_TICK + var/image/layer = image(data["steps"][step]) + var/image/result = image(step) + steps += list( + list( + "layer"=icon2html(layer, user, dir=sprite_dir, sourceonly=TRUE), + "result"=icon2html(result, user, dir=sprite_dir, sourceonly=TRUE) + ) ) - ) + + sprite_data["finished"] = icon2html(finished, user, dir=sprite_dir, sourceonly=TRUE) + refreshing = FALSE + +/datum/greyscale_modify_menu/proc/Unlock() + allowed_configs = SSgreyscale.configurations + unlocked = TRUE + +/datum/greyscale_modify_menu/proc/DefaultApply() + target.set_greyscale_config(config.type, update=FALSE) + target.greyscale_colors = "" // We do this to force an update, in some cases it will think nothing changed when it should be refreshing + target.set_greyscale_colors(split_colors) diff --git a/code/modules/admin/view_variables/topic_basic.dm b/code/modules/admin/view_variables/topic_basic.dm index 0d323e8cf07..4baa36c05ab 100644 --- a/code/modules/admin/view_variables/topic_basic.dm +++ b/code/modules/admin/view_variables/topic_basic.dm @@ -81,7 +81,8 @@ if(href_list[VV_HK_MODIFY_GREYSCALE]) if(!check_rights(NONE)) return - var/datum/greyscale_modify_menu/menu = new(target, usr) + var/datum/greyscale_modify_menu/menu = new(target, usr, SSgreyscale.configurations) + menu.Unlock() menu.ui_interact(usr) if(href_list[VV_HK_CALLPROC]) usr.client.callproc_datum(target) diff --git a/code/modules/vending/_vending.dm b/code/modules/vending/_vending.dm index ffda3d4fa0c..418883c2af6 100644 --- a/code/modules/vending/_vending.dm +++ b/code/modules/vending/_vending.dm @@ -36,6 +36,8 @@ IF YOU MODIFY THE PRODUCTS LIST OF A MACHINE, MAKE SURE TO UPDATE ITS RESUPPLY C var/custom_premium_price ///Whether spessmen with an ID with an age below AGE_MINOR (20 by default) can buy this item var/age_restricted = FALSE + ///Whether the product can be recolored by the GAGS system + var/colorable /** * # vending machines @@ -323,6 +325,7 @@ GLOBAL_LIST_EMPTY(vending_products) R.custom_price = round(initial(temp.custom_price) * SSeconomy.inflation_value()) R.custom_premium_price = round(initial(temp.custom_premium_price) * SSeconomy.inflation_value()) R.age_restricted = initial(temp.age_restricted) + R.colorable = !!(initial(temp.greyscale_config) && initial(temp.greyscale_colors)) recordlist += R /** @@ -791,7 +794,12 @@ GLOBAL_LIST_EMPTY(vending_products) .["user"]["department"] = "No Department" .["stock"] = list() for (var/datum/data/vending_product/R in product_records + coin_records + hidden_records) - .["stock"][R.name] = R.amount + var/list/product_data = list( + name = R.name, + amount = R.amount, + colorable = R.colorable, + ) + .["stock"][R.name] = product_data .["extended_inventory"] = extended_inventory /obj/machinery/vending/ui_act(action, params) @@ -800,96 +808,145 @@ GLOBAL_LIST_EMPTY(vending_products) return switch(action) if("vend") - . = TRUE - if(!vend_ready) - return - if(panel_open) - to_chat(usr, "The vending machine cannot dispense products while its service panel is open!") - return - vend_ready = FALSE //One thing at a time!! - var/datum/data/vending_product/R = locate(params["ref"]) - var/list/record_to_check = product_records + coin_records - if(extended_inventory) - record_to_check = product_records + coin_records + hidden_records - if(!R || !istype(R) || !R.product_path) - vend_ready = TRUE - return - var/price_to_use = default_price - if(R.custom_price) - price_to_use = R.custom_price - if(R in hidden_records) - if(!extended_inventory) - vend_ready = TRUE - return - else if (!(R in record_to_check)) - vend_ready = TRUE - message_admins("Vending machine exploit attempted by [ADMIN_LOOKUPFLW(usr)]!") - return - if (R.amount <= 0) - say("Sold out of [R.name].") - flick(icon_deny,src) - vend_ready = TRUE - return - if(onstation) - var/obj/item/card/id/C - if(isliving(usr)) - var/mob/living/L = usr - C = L.get_idcard(TRUE) - if(!C) - say("No card found.") - flick(icon_deny,src) - vend_ready = TRUE - return - else if (!C.registered_account) - say("No account found.") - flick(icon_deny,src) - vend_ready = TRUE - return - else if(!C.registered_account.account_job) - say("Departmental accounts have been blacklisted from personal expenses due to embezzlement.") - flick(icon_deny, src) - vend_ready = TRUE - return - else if(age_restrictions && R.age_restricted && (!C.registered_age || C.registered_age < AGE_MINOR)) - say("You are not of legal age to purchase [R.name].") - if(!(usr in GLOB.narcd_underages)) - Radio.set_frequency(FREQ_SECURITY) - Radio.talk_into(src, "SECURITY ALERT: Underaged crewmember [usr] recorded attempting to purchase [R.name] in [get_area(src)]. Please watch for substance abuse.", FREQ_SECURITY) - GLOB.narcd_underages += usr - flick(icon_deny,src) - vend_ready = TRUE - return - var/datum/bank_account/account = C.registered_account - if(account.account_job && account.account_job.paycheck_department == payment_department) - price_to_use = max(round(price_to_use * VENDING_DISCOUNT), 1) //No longer free, but signifigantly cheaper. - if(coin_records.Find(R) || hidden_records.Find(R)) - price_to_use = R.custom_premium_price ? R.custom_premium_price : extra_price - if(price_to_use && !account.adjust_money(-price_to_use)) - say("You do not possess the funds to purchase [R.name].") - flick(icon_deny,src) - vend_ready = TRUE - return - var/datum/bank_account/D = SSeconomy.get_dep_account(payment_department) - if(D) - D.adjust_money(price_to_use) - SSblackbox.record_feedback("amount", "vending_spent", price_to_use) - log_econ("[price_to_use] credits were inserted into [src] by [D.account_holder] to buy [R].") - if(last_shopper != usr || purchase_message_cooldown < world.time) - say("Thank you for shopping with [src]!") - purchase_message_cooldown = world.time + 5 SECONDS - last_shopper = usr - use_power(5) - if(icon_vend) //Show the vending animation if needed - flick(icon_vend,src) - playsound(src, 'sound/machines/machine_vend.ogg', 50, TRUE, extrarange = -3) - var/obj/item/vended_item = new R.product_path(get_turf(src)) - R.amount-- - if(usr.CanReach(src) && usr.put_in_hands(vended_item)) - to_chat(usr, "You take [R.name] out of the slot.") - else - to_chat(usr, "[capitalize(R.name)] falls onto the floor!") - SSblackbox.record_feedback("nested tally", "vending_machine_usage", 1, list("[type]", "[R.product_path]")) + . = vend(params) + if("select_colors") + . = select_colors(params) + +/obj/machinery/vending/proc/can_vend(user, silent=FALSE) + . = FALSE + if(!vend_ready) + return + if(panel_open) + to_chat(user, "The vending machine cannot dispense products while its service panel is open!") + return + return TRUE + +/obj/machinery/vending/proc/select_colors(list/params) + . = TRUE + if(!can_vend(usr)) + return + var/datum/data/vending_product/product = locate(params["ref"]) + var/atom/fake_atom = product.product_path + + var/list/allowed_configs = list() + var/config = initial(fake_atom.greyscale_config) + if(!config) + return + allowed_configs += "[config]" + if(ispath(fake_atom, /obj/item)) + var/obj/item/item = fake_atom + if(initial(item.greyscale_config_worn)) + allowed_configs += "[initial(item.greyscale_config_worn)]" + if(initial(item.greyscale_config_inhand_left)) + allowed_configs += "[initial(item.greyscale_config_inhand_left)]" + if(initial(item.greyscale_config_inhand_right)) + allowed_configs += "[initial(item.greyscale_config_inhand_right)]" + + var/datum/greyscale_modify_menu/menu = new( + src, usr, allowed_configs, CALLBACK(src, .proc/vend_greyscale, params), + starting_icon_state=initial(fake_atom.icon_state), + starting_config=initial(fake_atom.greyscale_config), + starting_colors=initial(fake_atom.greyscale_colors) + ) + menu.ui_interact(usr) + +/obj/machinery/vending/proc/vend_greyscale(list/params, datum/greyscale_modify_menu/menu) + if(usr != menu.user) + return + if(!menu.target.can_interact(usr)) + return + vend(params, menu.split_colors) + +/obj/machinery/vending/proc/vend(list/params, list/greyscale_colors) + . = TRUE + if(!can_vend(usr)) + return + vend_ready = FALSE //One thing at a time!! + var/datum/data/vending_product/R = locate(params["ref"]) + var/list/record_to_check = product_records + coin_records + if(extended_inventory) + record_to_check = product_records + coin_records + hidden_records + if(!R || !istype(R) || !R.product_path) + vend_ready = TRUE + return + var/price_to_use = default_price + if(R.custom_price) + price_to_use = R.custom_price + if(R in hidden_records) + if(!extended_inventory) vend_ready = TRUE + return + else if (!(R in record_to_check)) + vend_ready = TRUE + message_admins("Vending machine exploit attempted by [ADMIN_LOOKUPFLW(usr)]!") + return + if (R.amount <= 0) + say("Sold out of [R.name].") + flick(icon_deny,src) + vend_ready = TRUE + return + if(onstation) + var/obj/item/card/id/C + if(isliving(usr)) + var/mob/living/L = usr + C = L.get_idcard(TRUE) + if(!C) + say("No card found.") + flick(icon_deny,src) + vend_ready = TRUE + return + else if (!C.registered_account) + say("No account found.") + flick(icon_deny,src) + vend_ready = TRUE + return + else if(!C.registered_account.account_job) + say("Departmental accounts have been blacklisted from personal expenses due to embezzlement.") + flick(icon_deny, src) + vend_ready = TRUE + return + else if(age_restrictions && R.age_restricted && (!C.registered_age || C.registered_age < AGE_MINOR)) + say("You are not of legal age to purchase [R.name].") + if(!(usr in GLOB.narcd_underages)) + Radio.set_frequency(FREQ_SECURITY) + Radio.talk_into(src, "SECURITY ALERT: Underaged crewmember [usr] recorded attempting to purchase [R.name] in [get_area(src)]. Please watch for substance abuse.", FREQ_SECURITY) + GLOB.narcd_underages += usr + flick(icon_deny,src) + vend_ready = TRUE + return + var/datum/bank_account/account = C.registered_account + if(account.account_job && account.account_job.paycheck_department == payment_department) + price_to_use = max(round(price_to_use * VENDING_DISCOUNT), 1) //No longer free, but signifigantly cheaper. + if(coin_records.Find(R) || hidden_records.Find(R)) + price_to_use = R.custom_premium_price ? R.custom_premium_price : extra_price + if(price_to_use && !account.adjust_money(-price_to_use)) + say("You do not possess the funds to purchase [R.name].") + flick(icon_deny,src) + vend_ready = TRUE + return + var/datum/bank_account/D = SSeconomy.get_dep_account(payment_department) + if(D) + D.adjust_money(price_to_use) + SSblackbox.record_feedback("amount", "vending_spent", price_to_use) + log_econ("[price_to_use] credits were inserted into [src] by [D.account_holder] to buy [R].") + if(last_shopper != usr || purchase_message_cooldown < world.time) + say("Thank you for shopping with [src]!") + purchase_message_cooldown = world.time + 5 SECONDS + last_shopper = usr + use_power(5) + if(icon_vend) //Show the vending animation if needed + flick(icon_vend,src) + playsound(src, 'sound/machines/machine_vend.ogg', 50, TRUE, extrarange = -3) + var/obj/item/vended_item = new R.product_path(get_turf(src)) + if(greyscale_colors) + vended_item.set_greyscale_colors(greyscale_colors) + R.amount-- + if(usr.CanReach(src) && usr.put_in_hands(vended_item)) + to_chat(usr, "You take [R.name] out of the slot.") + else + to_chat(usr, "[capitalize(R.name)] falls onto the floor!") + SSblackbox.record_feedback("nested tally", "vending_machine_usage", 1, list("[type]", "[R.product_path]")) + vend_ready = TRUE /obj/machinery/vending/process(delta_time) if(machine_stat & (BROKEN|NOPOWER)) diff --git a/icons/Testing/greyscale_error.dmi b/icons/Testing/greyscale_error.dmi new file mode 100644 index 00000000000..aacf6106821 Binary files /dev/null and b/icons/Testing/greyscale_error.dmi differ diff --git a/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx b/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx index 4615e9a7185..f12e2197eeb 100644 --- a/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx +++ b/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx @@ -1,5 +1,5 @@ import { useBackend } from '../backend'; -import { Box, Button, ColorBox, Flex, Icon, Input, LabeledList, Section, Table } from '../components'; +import { Box, Button, ColorBox, Flex, Stack, Icon, Input, LabeledList, Section, Table } from '../components'; import { Window } from '../layouts'; type ColorEntry = { @@ -8,20 +8,25 @@ type ColorEntry = { } type SpriteData = { - finished: SpriteEntry - steps: Array + icon_states: string[]; + finished: string; + steps: SpriteEntry[]; } type SpriteEntry = { - layer: string - result: string + layer: string; + result: string; } type GreyscaleMenuData = { greyscale_config: string; - colors: Array; + colors: ColorEntry[]; sprites: SpriteData; + generate_full_preview: boolean; + unlocked: boolean; sprites_dir: string; + icon_state: string; + refreshing: boolean; } enum Direction { @@ -49,9 +54,9 @@ const DirectionAbbreviation : Record = { const ConfigDisplay = (props, context) => { const { act, data } = useBackend(context); return ( -
+
- + - - - - - - - - -
+ + + + + + + + + + + + + + + + + + + + + ); }; @@ -149,23 +156,79 @@ const SingleDirection = (props, context) => { ); }; +const IconStatesDisplay = (props, context) => { + const { data, act } = useBackend(context); + return ( +
+ + { + data.sprites.icon_states.map(item => ( + +
+ ); +}; + const PreviewDisplay = (props, context) => { const { data } = useBackend(context); return (
- - - Step Layer - Step Result + + + + + { + data.sprites?.finished + ? ( + + + + ) + : ( + + + + + + ) + } - {data.sprites.steps.map(item => ( - - - - - ))}
+ { + !data.refreshing + && ( + + { + !!data.generate_full_preview && data.sprites.steps !== null + && ( + + Step Layer + Step Result + + ) + } + { + !!data.generate_full_preview && data.sprites.steps !== null + && data.sprites.steps.map(item => ( + + + + + )) + } +
+ ) + }
); }; @@ -183,26 +246,44 @@ const SingleSprite = (props) => { ); }; +const LoadingAnimation = () => { + return ( + + + + ); +}; + export const GreyscaleModifyMenu = (props, context) => { const { act, data } = useBackend(context); return ( -