diff --git a/code/modules/admin/greyscale_modify_menu.dm b/code/modules/admin/greyscale_modify_menu.dm index f309d79a6b6..cf7cedefc78 100644 --- a/code/modules/admin/greyscale_modify_menu.dm +++ b/code/modules/admin/greyscale_modify_menu.dm @@ -31,6 +31,8 @@ /// Whether the menu is in the middle of refreshing the preview var/refreshing = TRUE + ///BUBBER VAR: Optional display labels for color groups, keyed by color index. + var/list/color_labels /** * Whether the menu is currently locked down to prevent abuse from players. @@ -102,12 +104,14 @@ /datum/greyscale_modify_menu/ui_data(mob/user) var/list/data = list() data["greyscale_config"] = "[config.name]" + data["full_color_string"] = split_colors?.Join("") //BUBBER ADDITION var/list/color_data = list() data["colors"] = color_data for(var/i in 1 to config.expected_colors) color_data += list(list( "index" = i, + "label" = color_labels?["[i]"] || color_labels?[i], //BUBBER ADDITION "value" = split_colors[i] )) diff --git a/code/modules/loadout/loadout_items.dm b/code/modules/loadout/loadout_items.dm index ecab82a1abc..bf2068a8cdf 100644 --- a/code/modules/loadout/loadout_items.dm +++ b/code/modules/loadout/loadout_items.dm @@ -407,7 +407,11 @@ GLOBAL_LIST_INIT(all_loadout_categories, init_loadout_categories()) formatted_item["reskins"] = get_reskin_options() formatted_item["icon"] = ui_icon formatted_item["icon_state"] = ui_icon_state - formatted_item["ckey_whitelist"] = ckeywhitelist // BUBBER EDIT ADDITION: Filter ckey-locked items + //BUBBER EDIT ADDITION START - Dynamic Uniforms + ckey lock filter + if(initial(item_path.greyscale_component_style_type)) + add_component_style_preview_to_ui_data(formatted_item) + formatted_item["ckey_whitelist"] = ckeywhitelist + //BUBBER EDIT ADDITION END return formatted_item diff --git a/code/modules/vending/vendor/inventory.dm b/code/modules/vending/vendor/inventory.dm index 26a9337581a..807b8594a2c 100644 --- a/code/modules/vending/vendor/inventory.dm +++ b/code/modules/vending/vendor/inventory.dm @@ -204,6 +204,11 @@ if(greyscale_colors) vended_item.set_greyscale(colors=greyscale_colors) + //BUBBER ADDITION START - dynamic uniforms + var/list/component_style_details = greyscale_component_style_vend_details?[REF(user)] + if(component_style_details && vended_item.greyscale_component_style_type) + vended_item.apply_greyscale_component_style(component_style_details) + //BUBBER ADDITION END if(IsReachableBy(user) && user.put_in_hands(vended_item)) to_chat(user, span_notice("You take [item_record.name] out of the slot.")) vended_item.do_pickup_animation(user, src) diff --git a/code/modules/vending/vendor/ui_data.dm b/code/modules/vending/vendor/ui_data.dm index 14ab20660d4..e77d04a8f66 100644 --- a/code/modules/vending/vendor/ui_data.dm +++ b/code/modules/vending/vendor/ui_data.dm @@ -44,11 +44,22 @@ ) var/atom/printed = record.product_path + //BUBBER ADDITION START - Dynamic uniforms + var/component_preview_applied = FALSE + if(ispath(printed, /obj/item)) + var/obj/item/printed_item = printed + if(initial(printed_item.greyscale_component_style_type)) + static_record["image"] = get_component_style_vending_preview_image(printed_item) + component_preview_applied = TRUE + //BUBBER ADDITION END // If it's not GAGS and has no innate colors we have to care about, we use DMIcon - if(ispath(printed, /atom) \ + //BUBBER EDIT START - dynamic uniform + if(!component_preview_applied \ + && ispath(printed, /atom) \ && (!initial(printed.greyscale_config) || !initial(printed.greyscale_colors)) \ && !initial(printed.color) \ ) + //BUBBER EDIT END static_record["icon"] = initial(printed.icon) static_record["icon_state"] = initial(printed.icon_state) @@ -147,6 +158,12 @@ if(!istype(product)) return FALSE var/atom/fake_atom = product.product_path + //BUBBER ADDITION START - Dynamic uniforms + if(ispath(fake_atom, /obj/item)) + var/obj/item/fake_item = fake_atom + if(initial(fake_item.greyscale_component_style_type)) + return open_component_style_vending_menu(product, params, ui.user) + //BUBBER ADDITION END var/config = initial(fake_atom.greyscale_config) if(!config) return FALSE diff --git a/icons/map_icons/clothing/head/_head.dmi b/icons/map_icons/clothing/head/_head.dmi index 04dc00e5696..17e5ce120d0 100644 Binary files a/icons/map_icons/clothing/head/_head.dmi and b/icons/map_icons/clothing/head/_head.dmi differ diff --git a/icons/map_icons/clothing/under/_under.dmi b/icons/map_icons/clothing/under/_under.dmi index 70fa35b4137..f80d98aaaf4 100644 Binary files a/icons/map_icons/clothing/under/_under.dmi and b/icons/map_icons/clothing/under/_under.dmi differ diff --git a/modular_zubbers/code/__DEFINES/loadout.dm b/modular_zubbers/code/__DEFINES/loadout.dm new file mode 100644 index 00000000000..07655d14420 --- /dev/null +++ b/modular_zubbers/code/__DEFINES/loadout.dm @@ -0,0 +1,13 @@ +/// Tracks selected core components for component-style GAGS loadout items. +#define INFO_GREYSCALE_COMPONENT_CORES "greyscale_component_cores" +/// Tracks selected optional components for component-style GAGS loadout items. +#define INFO_GREYSCALE_COMPONENT_ACCESSORIES "greyscale_component_accessories" + +/// Accessory applies to every option in a core component slot. +#define COMPONENT_CORE_ALL "all" +/// Accessory applies only to the named core option. +#define COMPONENT_CORE_EXACT "exact" +/// Accessory applies to the named core option and any option descended from it. +#define COMPONENT_CORE_BRANCH "branch" +/// Accessory applies to a named group defined on the core component. +#define COMPONENT_CORE_GROUP "group" diff --git a/modular_zubbers/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm b/modular_zubbers/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm index d4d2bca7df1..eddbfb0d36f 100644 --- a/modular_zubbers/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm +++ b/modular_zubbers/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm @@ -8,6 +8,47 @@ icon_file = 'modular_zubbers/icons/mob/clothing/under/captain.dmi' json_config = 'modular_zubbers/code/datums/greyscale/json_configs/cso.json' +/datum/greyscale_config/security_uniform + name = "Security Uniform" + icon_file = 'modular_zubbers/icons/mob/clothing/under/security.dmi' + greyscale_component_style_type = /datum/greyscale_component_style/security_uniform + greyscale_component_config_states = list("recolorable_security_uniform", "rsecurity") + +/datum/greyscale_config/security_uniform/New() + InitializeGreyscaleComponentStyleConfig() + +/datum/greyscale_config/security_uniform/Refresh(loadFromDisk = FALSE) + return RefreshGreyscaleComponentStyleConfig(loadFromDisk) + +/datum/greyscale_config/security_uniform/worn + name = "Security Uniform (Worn)" + icon_file = 'modular_zubbers/icons/mob/clothing/under/security.dmi' + +/datum/greyscale_config/security_uniform/worn/digi + name = "Security Uniform (Worn, Digi)" + icon_file = 'modular_zubbers/icons/mob/clothing/under/security.dmi' + +/datum/greyscale_config/security_cap + name = "Security Cap" + icon_file = 'modular_zubbers/icons/obj/clothing/head/hats.dmi' + greyscale_component_style_type = /datum/greyscale_component_style/security_cap + greyscale_component_config_states = list("security_cap") + +/datum/greyscale_config/security_cap/New() + InitializeGreyscaleComponentStyleConfig() + +/datum/greyscale_config/security_cap/Refresh(loadFromDisk = FALSE) + return RefreshGreyscaleComponentStyleConfig(loadFromDisk) + +/datum/greyscale_config/security_cap/worn + name = "Security Cap (Worn)" + icon_file = 'modular_zubbers/icons/mob/clothing/head/hats.dmi' + greyscale_component_config_state_overrides = list( + "security_cap" = "security_capg", + "security_cap_accs_front" = "security_capg_accs_backing", + "security_cap_accs_logo" = "security_capg_accs_logo", + ) + /datum/greyscale_config/gi/worn/digi name = "Gi (Worn, Digi)" icon_file = 'modular_skyrat/master_files/icons/mob/clothing/under/costume_digi.dmi' diff --git a/modular_zubbers/code/modules/clothing/head/hats.dm b/modular_zubbers/code/modules/clothing/head/hats.dm index 8601a822518..54498c60e7f 100644 --- a/modular_zubbers/code/modules/clothing/head/hats.dm +++ b/modular_zubbers/code/modules/clothing/head/hats.dm @@ -46,6 +46,75 @@ icon_state = "secsoft_v" armor_type = /datum/armor/cosmetic_sec +/datum/greyscale_component_style/security_cap + output_icon_state = "security_cap" + fallback_icon_state = "security_cap" + default_accessories = list("security_cap_accs_front", "security_cap_accs_logo") + core_components = list( + list( + "name" = "Cap", + "key" = "cap", + "default" = "security", + "options" = list( + "security" = list( + "name" = "Security cap", + "state" = "security_cap", + "color_id" = 1, + "color_label" = "Main", + "default_color" = "#A53429", + ), + ), + ), + ) + accessories = list( + "Backing" = list( + "state" = "security_cap_accs_front", + "color_id" = 2, + "color_label" = "Backing", + "default_color" = "#3F6E9E", + ), + "Logo" = list( + "state" = "security_cap_accs_logo", + "color_id" = 3, + "color_label" = "Logo", + "default_color" = "#FFFFFF", + ), + ) + +/obj/item/clothing/head/soft/sec/recolorable + name = "security cap" + desc = "It's a robust baseball hat, this one seems to have been custom ordered." + icon = 'modular_zubbers/icons/obj/clothing/head/hats.dmi' + worn_icon = 'modular_zubbers/icons/mob/clothing/head/hats.dmi' + icon_state = "security_cap" + post_init_icon_state = "security_cap" + soft_type = "security_cap" + soft_suffix = "" + greyscale_config = /datum/greyscale_config/security_cap + greyscale_config_worn = /datum/greyscale_config/security_cap/worn + greyscale_colors = "#A53429#3F6E9E#FFFFFF" + flags_1 = parent_type::flags_1 | IS_PLAYER_COLORABLE_1 + greyscale_component_style_type = /datum/greyscale_component_style/security_cap + greyscale_component_icon_file = 'modular_zubbers/icons/obj/clothing/head/hats.dmi' + greyscale_component_worn_icon_file = 'modular_zubbers/icons/mob/clothing/head/hats.dmi' + greyscale_component_worn_state_overrides = list( + "security_cap" = "security_capg", + "security_cap_accs_front" = "security_capg_accs_backing", + "security_cap_accs_logo" = "security_capg_accs_logo", + ) + greyscale_component_accessories = list("security_cap_accs_front", "security_cap_accs_logo") + +/obj/item/clothing/head/soft/sec/recolorable/Initialize(mapload) + initialize_greyscale_component_style() + return ..() + +/obj/item/clothing/head/soft/sec/recolorable/update_greyscale() + . = ..() + update_greyscale_component_icons() + +/obj/item/clothing/head/soft/sec/recolorable/flip(mob/user) + balloon_alert(user, "can't be flipped!") + /obj/item/clothing/head/sec/viro/beanie name = "security beanie" desc = "A beanie for security purposes" diff --git a/modular_zubbers/code/modules/clothing/under/security.dm b/modular_zubbers/code/modules/clothing/under/security.dm index 422af0607ba..177bc0d29ee 100644 --- a/modular_zubbers/code/modules/clothing/under/security.dm +++ b/modular_zubbers/code/modules/clothing/under/security.dm @@ -92,6 +92,196 @@ can_adjust = TRUE supports_variations_flags = CLOTHING_DIGITIGRADE_VARIATION +/datum/greyscale_component_style/security_uniform + output_icon_state = "rsecurity" + fallback_icon_state = "security_shirt" + default_accessories = list("security_shirt_accs_tie") + core_components = list( + list( + "name" = "Shirt", + "key" = "shirt", + "default" = "security", + "groups" = list( + "tie_compatible" = list("security", "viro", "suit"), + "arm_trim_compatible" = list("security", "viro", "turtleneck"), + ), + "options" = list( + "security" = list( + "name" = "Security shirt", + "state" = "security_shirt", + "color_id" = 1, + "color_label" = "Shirt", + "default_color" = "#BA3B2E", + ), + "viro" = list( + "name" = "Virosec shirt", + "state" = "security_shirt_viro", + "parent" = "security", + "color_id" = 1, + "color_label" = "Shirt", + "default_color" = "#BA3B2E", + ), + "suit" = list( + "name" = "Security suit", + "state" = "security_shirt_suit", + "parent" = "security", + "color_id" = 1, + "color_label" = "Shirt", + "default_color" = "#BA3B2E", + ), + "turtleneck" = list( + "name" = "Security turtleneck", + "state" = "security_shirt_turtleneck", + "parent" = "security", + "color_id" = 1, + "color_label" = "Shirt", + "default_color" = "#BA3B2E", + ), + ), + ), + list( + "name" = "Pants", + "key" = "pants", + "default" = "security", + "options" = list( + "security" = list( + "name" = "Security pants", + "state" = "security_pants", + "color_id" = 2, + "color_label" = "Pants", + "default_color" = "#4B4C51", + ), + "skirt" = list( + "name" = "Security skirt", + "state" = "security_pants_skirt", + "parent" = "security", + "color_id" = 2, + "color_label" = "Pants", + "default_color" = "#4B4C51", + ), + "viro" = list( + "name" = "Virosec pants", + "state" = "security_pants_viro", + "parent" = "security", + "color_id" = 2, + "color_label" = "Pants", + "default_color" = "#4B4C51", + ), + ), + ), + ) + accessories = list( + "Tie" = list( + "state" = "security_shirt_accs_tie", + "applies_to" = list("shirt" = list(COMPONENT_CORE_GROUP = "tie_compatible")), + "color_id" = 3, + "color_label" = "Tie", + "default_color" = "#2D2D33", + ), + "Arm trim one" = list( + "state" = "security_shirt_accs_arm_trim_one", + "applies_to" = list("shirt" = list(COMPONENT_CORE_EXACT = list("security", "viro"))), + "color_id" = 4, + "color_label" = "Arm trim one", + "default_color" = "#39393F", + ), + "Arm trim two" = list( + "state" = "security_shirt_accs_arm_trim_two", + "applies_to" = list("shirt" = list(COMPONENT_CORE_GROUP = "arm_trim_compatible")), + "color_id" = 13, + "color_label" = "Arm trim two", + "default_color" = "#46464D", + ), + "Arm trim three" = list( + "state" = "security_shirt_accs_arm_trim_three", + "applies_to" = list("shirt" = list(COMPONENT_CORE_GROUP = "arm_trim_compatible")), + "color_id" = 12, + "color_label" = "Arm trim three", + "default_color" = "#D0D0D0", + ), + "Epaulettes" = list( + "state" = "security_shirt_accss_epaulettes", + "applies_to" = list("shirt" = list(COMPONENT_CORE_BRANCH = "security")), + "color_id" = 11, + "color_label" = "Epaulettes", + "default_color" = "#39393F", + ), + "Viro shirt trim" = list( + "state" = "security_shirt_accs_trim_viro", + "applies_to" = list("shirt" = list("viro" = COMPONENT_CORE_EXACT)), + "color_id" = 8, + "color_label" = "Viro shirt trim", + "default_color" = "#46464D", + ), + "Suit inner shirt" = list( + "state" = "security_shirt_suit_accs_inner", + "applies_to" = list("shirt" = list("suit" = COMPONENT_CORE_EXACT)), + "color_id" = 9, + "color_label" = "Suit inner shirt", + "default_color" = "#EBEBEB", + ), + "Buckle" = list( + "state" = "security_pants_accs_buckle", + "applies_to" = list("pants" = COMPONENT_CORE_ALL), + "color_id" = 10, + "color_label" = "Buckle", + "default_color" = "#B5BBCF", + ), + "Skirt outer trim" = list( + "state" = "security_pants_skirt_accs_outer_trim", + "applies_to" = list("pants" = list("skirt" = COMPONENT_CORE_EXACT)), + "color_id" = 5, + "color_label" = "Skirt outer trim", + "default_color" = "#46464D", + ), + "Skirt inner trim" = list( + "state" = "security_pants_skirt_accs_inner_trim", + "applies_to" = list("pants" = list("skirt" = COMPONENT_CORE_EXACT)), + "color_id" = 6, + "color_label" = "Skirt inner trim", + "default_color" = "#46464D", + ), + "Viro pants trim" = list( + "state" = "security_pants_viro_accs_trim", + "applies_to" = list("pants" = list("viro" = COMPONENT_CORE_EXACT)), + "color_id" = 7, + "color_label" = "Viro pants trim", + "default_color" = "#7D2A25", + ), + ) + +/obj/item/clothing/under/rank/security/officer/recolorable + name = "security uniform" + desc = "A tactical security jumpsuit for officers. This one seems to have a few custom modifications." + icon = 'icons/map_icons/clothing/under/_under.dmi' + worn_icon = 'modular_zubbers/icons/mob/clothing/under/security.dmi' + worn_icon_digi = 'modular_zubbers/icons/mob/clothing/under/security_digi.dmi' + icon_state = "/obj/item/clothing/under/rank/security/officer/recolorable" + post_init_icon_state = "recolorable_security_uniform" + greyscale_config = /datum/greyscale_config/security_uniform + greyscale_config_worn = /datum/greyscale_config/security_uniform/worn + greyscale_config_worn_digi = /datum/greyscale_config/security_uniform/worn/digi + greyscale_colors = "#BA3B2E#4B4C51#2D2D33#39393F#46464D#46464D#7D2A25#46464D#EBEBEB#B5BBCF#39393F#D0D0D0#46464D" + flags_1 = parent_type::flags_1 | IS_PLAYER_COLORABLE_1 + can_adjust = TRUE + supports_variations_flags = CLOTHING_DIGITIGRADE_VARIATION + greyscale_component_style_type = /datum/greyscale_component_style/security_uniform + greyscale_component_worn_icon_file = 'modular_zubbers/icons/mob/clothing/under/security.dmi' + greyscale_component_worn_digi_icon_file = 'modular_zubbers/icons/mob/clothing/under/security_digi.dmi' + greyscale_component_digi_fallback_icon_file = 'modular_zubbers/icons/mob/clothing/under/security.dmi' + greyscale_component_accessories = list("security_shirt_accs_tie") + +/obj/item/clothing/under/rank/security/officer/recolorable/Initialize(mapload) + initialize_greyscale_component_style() + return ..() + +/obj/item/clothing/under/rank/security/officer/recolorable/update_greyscale() + . = ..() + update_greyscale_component_icons() + icon = 'icons/obj/clothing/under/security.dmi' + icon_state = "rsecurity" + inhand_icon_state = "r_suit" + // Virosec, sprites by axietheaxolotl /obj/item/clothing/under/rank/security/viro/officer/ diff --git a/modular_zubbers/code/modules/greyscale/component_style.dm b/modular_zubbers/code/modules/greyscale/component_style.dm new file mode 100644 index 00000000000..dd6e812ef15 --- /dev/null +++ b/modular_zubbers/code/modules/greyscale/component_style.dm @@ -0,0 +1,866 @@ +/datum/greyscale_component_style + /// Marker used when loadout storage needs to preserve an intentionally empty accessory selection. + var/static/no_accessories_marker = "__component_style_no_accessories" + +/datum/greyscale_component_style + /// Icon state inserted into generated icon files. + var/output_icon_state + /// Fallback component state to use if every selected component is missing. + var/fallback_icon_state + /// Suffix checked first for digitigrade component states. + var/digi_suffix = "_digi" + /// Ordered list of core component definitions. + var/list/core_components = list() + /// Optional component definitions, keyed by display name. + var/list/accessories = list() + /// Optional accessory component states enabled by default. + var/list/default_accessories = list() + /// Suffix checked first for adjusted/casual component states. + var/adjusted_suffix = "_d" + +/datum/greyscale_component_style/proc/default_core_selection() as /list + var/list/selection = list() + for(var/list/core_component as anything in core_components) + selection[core_component["key"]] = core_component["default"] + return selection + +/datum/greyscale_component_style/proc/sanitize_core_selection(list/selected_cores) as /list + var/list/sanitized = list() + for(var/list/core_component as anything in core_components) + var/core_key = core_component["key"] + var/selected_state = selected_cores?[core_key] || core_component["default"] + if(!core_option_exists(core_component, selected_state)) + selected_state = core_component["default"] + sanitized[core_key] = selected_state + return sanitized + +/datum/greyscale_component_style/proc/sanitize_accessories(list/selected_accessories, list/selected_cores) as /list + var/list/sanitized = list() + if(!islist(selected_accessories) || (no_accessories_marker in selected_accessories)) + return sanitized + for(var/accessory in selected_accessories) + if(!accessory_allowed(accessory, selected_cores)) + continue + sanitized += accessory + return sanitized + +/datum/greyscale_component_style/proc/core_option_exists(list/core_component, option_state) + var/list/options = core_component["options"] + return islist(options?[option_state]) + +/datum/greyscale_component_style/proc/core_component_for_key(core_key) as /list + for(var/list/core_component as anything in core_components) + if(core_component["key"] == core_key) + return core_component + return null + +/datum/greyscale_component_style/proc/core_option_data(list/core_component, option_id) as /list + var/list/options = core_component?["options"] + return options?[option_id] + +/datum/greyscale_component_style/proc/core_option_state(list/core_component, option_id) + var/list/option_data = core_option_data(core_component, option_id) + return option_data?["state"] + +/datum/greyscale_component_style/proc/core_group_options(list/core_component, group_id) as /list + var/list/groups = core_component?["groups"] + var/list/group_options = groups?[group_id] + return islist(group_options) ? group_options : null + +/datum/greyscale_component_style/proc/core_option_matches(list/core_component, selected_option, target_option, match_mode) + if(match_mode == COMPONENT_CORE_ALL) + return TRUE + if(match_mode == COMPONENT_CORE_EXACT) + return selected_option == target_option + if(match_mode != COMPONENT_CORE_BRANCH) + return FALSE + + var/current_option = selected_option + while(current_option) + if(current_option == target_option) + return TRUE + var/list/option_data = core_option_data(core_component, current_option) + current_option = option_data?["parent"] + return FALSE + +/datum/greyscale_component_style/proc/core_rule_targets(list/core_component, rule_value, use_groups = FALSE) as /list + if(islist(rule_value)) + return rule_value + if(use_groups) + return core_group_options(core_component, rule_value) || list() + return list(rule_value) + +/datum/greyscale_component_style/proc/core_apply_rule_matches(list/core_component, selected_option, rule_mode, rule_value) + var/list/targets = core_rule_targets(core_component, rule_value, rule_mode == COMPONENT_CORE_GROUP) + var/match_mode = (rule_mode == COMPONENT_CORE_GROUP) ? COMPONENT_CORE_EXACT : rule_mode + for(var/target_option in targets) + if(core_option_matches(core_component, selected_option, target_option, match_mode)) + return TRUE + return FALSE + +/datum/greyscale_component_style/proc/core_ui_data(list/selected_cores) as /list + var/list/core_data = list() + for(var/list/core_component as anything in core_components) + var/core_key = core_component["key"] + core_data += list(list( + "name" = core_component["name"], + "key" = core_key, + "options" = option_ui_data(core_component["options"], selected_cores[core_key]), + )) + return core_data + +/datum/greyscale_component_style/proc/option_ui_data(list/options, selected_value) as /list + var/list/style_options = list() + for(var/option_id in options) + var/list/option_data = options[option_id] + style_options += list(list( + "name" = option_data["name"] || option_id, + "value" = option_id, + "selected" = (option_id == selected_value), + )) + return style_options + +/datum/greyscale_component_style/proc/accessory_ui_data(list/selected_accessories, list/selected_cores) as /list + var/list/style_options = list() + for(var/accessory_name in accessories) + var/list/accessory_rules = accessories[accessory_name] + var/accessory_state = accessory_rules["state"] + if(!accessory_allowed(accessory_state, selected_cores)) + continue + style_options += list(list( + "name" = accessory_name, + "value" = accessory_state, + "selected" = (accessory_state in selected_accessories), + )) + return style_options + +/datum/greyscale_component_style/proc/accessory_allowed(accessory_state, list/selected_cores) + var/list/accessory_rules = accessory_rules_for_state(accessory_state) + if(isnull(accessory_rules)) + return FALSE + + var/list/apply_rules = accessory_rules["applies_to"] + if(!length(apply_rules)) + return TRUE + + for(var/core_key in apply_rules) + var/list/core_component = core_component_for_key(core_key) + if(!core_component) + return FALSE + + var/selected_option = selected_cores?[core_key] + if(!selected_option) + return FALSE + + var/apply_rule = apply_rules[core_key] + if(apply_rule == COMPONENT_CORE_ALL) + continue + + var/matched_core = FALSE + if(islist(apply_rule)) + for(var/rule_key in apply_rule) + if(rule_key in list(COMPONENT_CORE_EXACT, COMPONENT_CORE_BRANCH, COMPONENT_CORE_GROUP)) + if(core_apply_rule_matches(core_component, selected_option, rule_key, apply_rule[rule_key])) + matched_core = TRUE + break + continue + + if(core_apply_rule_matches(core_component, selected_option, apply_rule[rule_key] || COMPONENT_CORE_EXACT, rule_key)) + matched_core = TRUE + break + else + matched_core = core_option_matches(core_component, selected_option, apply_rule, COMPONENT_CORE_EXACT) + + if(!matched_core) + return FALSE + return TRUE + +/datum/greyscale_component_style/proc/accessory_rules_for_state(accessory_state) as /list + for(var/accessory_name in accessories) + var/list/accessory_rules = accessories[accessory_name] + if(accessory_rules["state"] == accessory_state) + return accessory_rules + return null + +/datum/greyscale_component_style/proc/active_components(list/selected_cores, list/selected_accessories) as /list + var/list/components = list() + for(var/list/core_component as anything in core_components) + var/component_state = core_option_state(core_component, selected_cores?[core_component["key"]]) + if(component_state) + components += component_state + if(islist(selected_accessories)) + for(var/accessory_state in selected_accessories) + if(!accessory_state) + continue + components += accessory_state + return components + +/datum/greyscale_component_style/proc/color_id_for_component(component_state) + var/list/component_rules = component_rules_for_state(component_state) + return component_rules?["color_id"] + +/datum/greyscale_component_style/proc/component_rules_for_state(component_state) as /list + for(var/list/core_component as anything in core_components) + var/list/options = core_component["options"] + for(var/option_id in options) + var/list/option_data = options[option_id] + if(option_data["state"] == component_state) + return option_data + for(var/accessory_name in accessories) + var/list/accessory_rules = accessories[accessory_name] + if(accessory_rules["state"] == component_state) + return accessory_rules + return null + +/datum/greyscale_component_style/proc/active_color_ids(list/selected_cores, list/selected_accessories) as /list + var/list/active_color_ids = list() + for(var/component in active_components(selected_cores, selected_accessories)) + var/color_id = color_id_for_component(component) + if(!color_id || (color_id in active_color_ids)) + continue + active_color_ids += color_id + return active_color_ids + +/datum/greyscale_component_style/proc/active_color_labels(list/selected_cores, list/selected_accessories) as /list + var/list/labels = list() + for(var/component_state in active_components(selected_cores, selected_accessories)) + var/list/component_rules = component_rules_for_state(component_state) + var/color_id = component_rules?["color_id"] + var/color_label = component_rules?["color_label"] + if(!color_id || !color_label) + continue + labels["[color_id]"] = color_label + return labels + +/datum/greyscale_component_style/proc/default_color_list(list/fallback_colors, expected_colors = 0) as /list + var/list/colors = islist(fallback_colors) ? fallback_colors.Copy() : list() + while(length(colors) < expected_colors) + colors += null + + for(var/list/core_component as anything in core_components) + var/list/options = core_component["options"] + for(var/option_id in options) + apply_component_default_color(colors, options[option_id]) + for(var/accessory_name in accessories) + apply_component_default_color(colors, accessories[accessory_name]) + + for(var/i in 1 to length(colors)) + if(!colors[i]) + colors[i] = rgb(100, 100, 100) + return colors + +/datum/greyscale_component_style/proc/apply_component_default_color(list/colors, list/component_rules) + var/color_id = component_rules?["color_id"] + var/default_color = component_rules?["default_color"] + if(!color_id || !default_color) + return + while(length(colors) < color_id) + colors += null + colors[color_id] = default_color + +/datum/greyscale_component_style/proc/greyscale_config_data(list/output_states, list/state_overrides) as /list + var/list/layers = greyscale_config_layers(state_overrides) + var/list/config_data = list() + if(!length(output_states)) + output_states = list(output_icon_state) + for(var/output_state in output_states) + if(isnull(output_state)) + continue + config_data[output_state] = layers + return config_data + +/datum/greyscale_component_style/proc/greyscale_config_layers(list/state_overrides) as /list + var/list/layers = list() + var/list/seen_states = list() + for(var/component_state in greyscale_config_component_states()) + if(!component_state || seen_states[component_state]) + continue + seen_states[component_state] = TRUE + + var/list/component_rules = component_rules_for_state(component_state) + var/layer_state = islist(state_overrides) ? (state_overrides[component_state] || component_state) : component_state + var/list/layer_data = list( + "type" = "icon_state", + "icon_state" = layer_state, + "blend_mode" = "overlay", + ) + var/color_id = component_rules?["color_id"] + if(color_id) + layer_data["color_ids"] = list(color_id) + layers += list(layer_data) + return layers + +/datum/greyscale_component_style/proc/greyscale_config_component_states() as /list + var/list/component_states = list() + for(var/list/core_component as anything in core_components) + var/list/options = core_component["options"] + for(var/option_id in options) + component_states += options[option_id]["state"] + for(var/accessory_name in accessories) + component_states += accessories[accessory_name]["state"] + return component_states + +/datum/greyscale_config + /// Optional dynamic component-style definition used to generate this config without a hand-written JSON file. + var/greyscale_component_style_type + /// Icon states this generated config should expose in its output bundle. + var/list/greyscale_component_config_states + /// Optional logical component state to real icon state mapping for this config's icon file. + var/list/greyscale_component_config_state_overrides + +/datum/greyscale_config/proc/InitializeGreyscaleComponentStyleConfig() + if(!icon_file) + stack_trace("Greyscale component-style config object [DebugName()] is missing an icon file, make sure `icon_file` has been assigned a value.") + string_icon_file = "[icon_file]" + if(!name) + stack_trace("Greyscale component-style config object [DebugName()] is missing a name, make sure `name` has been assigned a value.") + if(!greyscale_component_style_type) + stack_trace("Greyscale component-style config object [DebugName()] is missing a component style type.") + +/datum/greyscale_config/proc/RefreshGreyscaleComponentStyleConfig(loadFromDisk = FALSE) + if(loadFromDisk) + var/changed = FALSE + + icon_file = file(string_icon_file) + var/icon_hash = rustg_hash_file("md5", string_icon_file) + if(icon_file_hash != icon_hash) + icon_file_hash = icon_hash + changed = TRUE + + for(var/datum/greyscale_layer/layer as anything in flat_all_layers) + if(layer.DiskRefresh()) + changed = TRUE + + if(!changed) + return FALSE + + var/style_type = greyscale_component_style_type + var/datum/greyscale_component_style/component_style = new style_type + var/list/raw = component_style.greyscale_config_data(greyscale_component_config_states, greyscale_component_config_state_overrides) + raw_json_string = json_encode(raw) + qdel(component_style) + + ReadIconStateConfiguration(raw) + if(!length(icon_states)) + CRASH("The component-style greyscale configuration [DebugName()] doesn't have any icon states.") + + icon_cache = list() + ReadMetadata() + + SEND_SIGNAL(src, COMSIG_GREYSCALE_CONFIG_REFRESHED) + return TRUE + +/datum/greyscale_component_style/proc/build_icon(icon_file, list/colors, list/selected_cores, list/selected_accessories, use_digi = FALSE, digi_fallback_icon_file = null, only_dir = null, list/state_overrides = null, use_adjusted = FALSE) + var/icon/final_icon = icon('icons/effects/effects.dmi', "nothing") + final_icon.Scale(32, 32) + if(!icon_file) + return final_icon + + var/icon/built_icon + for(var/component in active_components(selected_cores, selected_accessories)) + if(!component) + continue + var/layer_file = icon_file + var/state_to_use = component + if(islist(state_overrides)) + state_to_use = state_overrides[component] || component + + var/list/candidate_states = list() + if(use_digi && use_adjusted) + candidate_states += "[state_to_use][digi_suffix][adjusted_suffix]" + if(use_adjusted) + candidate_states += "[state_to_use][adjusted_suffix]" + if(use_digi) + candidate_states += "[state_to_use][digi_suffix]" + candidate_states += state_to_use + + var/list/candidate_files = list(layer_file) + if(use_digi && digi_fallback_icon_file) + candidate_files += digi_fallback_icon_file + + var/found_component_state = FALSE + for(var/candidate_state in candidate_states) + for(var/candidate_file in candidate_files) + if(!icon_exists(candidate_file, candidate_state)) + continue + layer_file = candidate_file + state_to_use = candidate_state + found_component_state = TRUE + break + if(found_component_state) + break + + if(!found_component_state) + continue + + var/icon/component_icon = isnull(only_dir) ? icon(layer_file, state_to_use) : icon(layer_file, state_to_use, only_dir) + var/color_id = color_id_for_component(component) + if(color_id && length(colors) >= color_id && colors[color_id]) + component_icon.Blend(colors[color_id], ICON_MULTIPLY) + + if(isnull(built_icon)) + built_icon = component_icon + else + built_icon.Blend(component_icon, ICON_OVERLAY) + + if(isnull(built_icon) && fallback_icon_state) + built_icon = isnull(only_dir) ? icon(icon_file, fallback_icon_state) : icon(icon_file, fallback_icon_state, only_dir) + + if(!isnull(built_icon)) + var/generated_icon_state = use_adjusted ? "[output_icon_state][adjusted_suffix]" : output_icon_state + final_icon.Insert(built_icon, generated_icon_state) + return final_icon + +/obj/item + /// Optional component-style GAGS definition used for layered, selectable components. + var/greyscale_component_style_type + /// Selected core option ids, keyed by core component id. + var/list/greyscale_component_cores + /// Selected optional component states. + var/list/greyscale_component_accessories + /// Object icon file used for component-style GAGS composition. + var/greyscale_component_icon_file + /// Worn icon file used for component-style GAGS composition. + var/greyscale_component_worn_icon_file + /// Digitigrade worn icon file used for component-style GAGS composition. + var/greyscale_component_worn_digi_icon_file + /// Optional non-digi worn icon fallback for missing digitigrade component states. + var/greyscale_component_digi_fallback_icon_file + /// Whether component-style worn icons should use adjusted/casual component states. + var/greyscale_component_use_adjusted = FALSE + /// Optional logical component state to worn icon state mapping. + var/list/greyscale_component_worn_state_overrides + /// Optional logical component state to digitigrade worn icon state mapping. + var/list/greyscale_component_worn_digi_state_overrides + +/obj/item/proc/get_greyscale_component_style() as /datum/greyscale_component_style + if(!greyscale_component_style_type) + return null + return new greyscale_component_style_type + +/obj/item/proc/initialize_greyscale_component_style() + var/datum/greyscale_component_style/component_style = get_greyscale_component_style() + if(!component_style) + return + greyscale_component_cores = component_style.sanitize_core_selection(greyscale_component_cores) + var/list/accessories_to_apply = isnull(greyscale_component_accessories) ? component_style.default_accessories : greyscale_component_accessories + greyscale_component_accessories = component_style.sanitize_accessories(accessories_to_apply, greyscale_component_cores) + qdel(component_style) + +/obj/item/proc/apply_greyscale_component_style(list/item_details) + var/datum/greyscale_component_style/component_style = get_greyscale_component_style() + if(!component_style) + return + greyscale_component_cores = component_style.sanitize_core_selection(item_details?[INFO_GREYSCALE_COMPONENT_CORES] || greyscale_component_cores) + var/list/accessories_to_apply = item_details?[INFO_GREYSCALE_COMPONENT_ACCESSORIES] + if(isnull(accessories_to_apply)) + accessories_to_apply = isnull(greyscale_component_accessories) ? component_style.default_accessories : greyscale_component_accessories + greyscale_component_accessories = component_style.sanitize_accessories(accessories_to_apply, greyscale_component_cores) + qdel(component_style) + update_greyscale() + +/obj/item/proc/update_greyscale_component_icons() + var/datum/greyscale_component_style/component_style = get_greyscale_component_style() + if(!component_style || !greyscale_colors) + qdel(component_style) + return + + var/list/colors = SSgreyscale.ParseColorString(greyscale_colors) + icon_state = component_style.output_icon_state + worn_icon_state = greyscale_component_use_adjusted ? "[component_style.output_icon_state][component_style.adjusted_suffix]" : component_style.output_icon_state + greyscale_config_worn = null + greyscale_config_worn_digi = null + if(greyscale_component_icon_file) + icon = component_style.build_icon(greyscale_component_icon_file, colors, greyscale_component_cores, greyscale_component_accessories) + if(greyscale_component_worn_icon_file) + worn_icon = component_style.build_icon(greyscale_component_worn_icon_file, colors, greyscale_component_cores, greyscale_component_accessories, state_overrides = greyscale_component_worn_state_overrides, use_adjusted = greyscale_component_use_adjusted) + if(greyscale_component_worn_digi_icon_file) + worn_icon_digi = component_style.build_icon(greyscale_component_worn_digi_icon_file, colors, greyscale_component_cores, greyscale_component_accessories, use_digi = TRUE, digi_fallback_icon_file = greyscale_component_digi_fallback_icon_file, state_overrides = greyscale_component_worn_digi_state_overrides || greyscale_component_worn_state_overrides, use_adjusted = greyscale_component_use_adjusted) + qdel(component_style) + + var/obj/item/clothing/worn_clothing = src + if(!istype(worn_clothing)) + return + + var/mob/living/carbon/human/wearer = worn_clothing.loc + if(!istype(wearer)) + return + wearer.update_clothing(worn_clothing.slot_flags) + +/obj/item/clothing/under/adjust_to_normal() + . = ..() + if(!greyscale_component_style_type) + return + greyscale_component_use_adjusted = FALSE + update_greyscale_component_icons() + +/obj/item/clothing/under/adjust_to_alt() + . = ..() + if(!greyscale_component_style_type) + return + greyscale_component_use_adjusted = TRUE + update_greyscale_component_icons() + +/datum/loadout_item/proc/uses_greyscale_component_style() + return !!initial(item_path.greyscale_component_style_type) + +/datum/loadout_item + /// Optional icon file used for component-style color previews instead of the item's worn preview. + var/greyscale_component_preview_icon_file + /// Optional logical component state to preview icon state mapping. + var/list/greyscale_component_preview_state_overrides + +/// Opens a component-style GAGS editing menu when the loadout item supports it. +/datum/loadout_item/proc/set_component_style_item_color(datum/preference_middleware/loadout/manager, mob/user) + if(manager.menu) + return FALSE + + var/list/loadout = manager.get_current_loadout() + var/list/allowed_configs = list() + if(initial(item_path.greyscale_config_worn)) + allowed_configs += "[initial(item_path.greyscale_config_worn)]" + else if(initial(item_path.greyscale_config)) + allowed_configs += "[initial(item_path.greyscale_config)]" + + var/list/selected_cores = loadout?[item_path]?[INFO_GREYSCALE_COMPONENT_CORES] + if(isnull(selected_cores)) + selected_cores = initial(item_path.greyscale_component_cores) + + var/list/selected_accessories = loadout?[item_path]?[INFO_GREYSCALE_COMPONENT_ACCESSORIES] + if(islist(selected_accessories) && (/datum/greyscale_component_style::no_accessories_marker in selected_accessories)) + selected_accessories = list() + + var/starting_config = initial(item_path.greyscale_config_worn) || initial(item_path.greyscale_config) + var/starting_colors = loadout?[item_path]?[INFO_GREYSCALE] || initial(item_path.greyscale_colors) + var/datum/greyscale_config/selected_config = SSgreyscale.configurations["[starting_config]"] + if(selected_config && length(SSgreyscale.ParseColorString(starting_colors)) != selected_config.expected_colors) + starting_colors = initial(item_path.greyscale_colors) + + var/preview_icon_file = greyscale_component_preview_icon_file || initial(item_path.greyscale_component_worn_icon_file) || initial(item_path.greyscale_component_icon_file) + var/list/preview_state_overrides = greyscale_component_preview_state_overrides + if(!preview_state_overrides && preview_icon_file == initial(item_path.greyscale_component_worn_icon_file)) + preview_state_overrides = initial(item_path.greyscale_component_worn_state_overrides) + + var/datum/greyscale_modify_menu/component_style/menu = new( + manager, + user, + allowed_configs, + CALLBACK(src, PROC_REF(set_slot_greyscale), manager), + starting_icon_state = initial(item_path.icon_state), + starting_config = starting_config, + starting_colors = starting_colors, + component_style_type = initial(item_path.greyscale_component_style_type), + selected_cores = selected_cores, + selected_accessories = selected_accessories, + preview_icon_file = preview_icon_file, + preview_digi_fallback_icon_file = initial(item_path.greyscale_component_digi_fallback_icon_file), + preview_state_overrides = preview_state_overrides, + default_colors = initial(item_path.greyscale_colors), + default_cores = initial(item_path.greyscale_component_cores), + default_accessories = null, + ) + + manager.register_greyscale_menu(menu) + menu.ui_interact(user) + return TRUE + +/// Saves component-style GAGS color and component state. +/datum/loadout_item/proc/set_component_style_slot_greyscale(datum/preference_middleware/loadout/manager, datum/greyscale_modify_menu/open_menu) + var/datum/greyscale_modify_menu/component_style/component_menu = open_menu + if(!istype(component_menu)) + return FALSE + + var/list/loadout = manager.get_current_loadout() + if(!loadout?[item_path]) + return FALSE + + var/list/colors = component_menu.split_colors + if(!colors) + return FALSE + + loadout[item_path][INFO_GREYSCALE] = colors.Join("") + loadout[item_path][INFO_GREYSCALE_COMPONENT_CORES] = component_menu.selected_cores.Copy() + loadout[item_path][INFO_GREYSCALE_COMPONENT_ACCESSORIES] = length(component_menu.selected_accessories) ? component_menu.selected_accessories.Copy() : list(/datum/greyscale_component_style::no_accessories_marker) + manager.save_current_loadout(loadout) + return TRUE + +/datum/loadout_item/proc/apply_component_style_to_equipped_item(obj/item/equipped_item, list/item_details) + if(equipped_item?.greyscale_component_style_type) + equipped_item.apply_greyscale_component_style(item_details) + +/proc/get_component_style_item_preview_image(obj/item/item_path) + if(item_path == /obj/item/clothing/head/soft/sec/recolorable) + var/icon/preview_icon = icon('icons/obj/clothing/head/hats.dmi', "secsoft") + return icon2base64(preview_icon) + + var/component_style_type = initial(item_path.greyscale_component_style_type) + if(!component_style_type) + return null + var/datum/greyscale_component_style/component_style = new component_style_type + var/list/default_cores = component_style.sanitize_core_selection(initial(item_path.greyscale_component_cores)) + var/list/default_accessories = component_style.default_accessories + default_accessories = component_style.sanitize_accessories(default_accessories, default_cores) + var/list/default_colors = component_style.default_color_list(SSgreyscale.ParseColorString(initial(item_path.greyscale_colors))) + var/preview_icon_file = initial(item_path.greyscale_component_worn_icon_file) || initial(item_path.greyscale_component_icon_file) + var/list/preview_state_overrides + if(preview_icon_file == initial(item_path.greyscale_component_worn_icon_file)) + preview_state_overrides = initial(item_path.greyscale_component_worn_state_overrides) + var/icon/south_preview_icon = component_style.build_icon(preview_icon_file, default_colors, default_cores, default_accessories, only_dir = SOUTH, state_overrides = preview_state_overrides) + var/icon/single_frame_preview = icon(south_preview_icon, component_style.output_icon_state, SOUTH, 1) + qdel(component_style) + return icon2base64(single_frame_preview) + +/datum/loadout_item/proc/get_component_style_preview_image() + return get_component_style_item_preview_image(item_path) + +/datum/loadout_item/proc/add_component_style_preview_to_ui_data(list/formatted_item) + if(!uses_greyscale_component_style()) + return + formatted_item["image"] = get_component_style_preview_image() + +/obj/machinery/vending/proc/get_component_style_vending_preview_image(obj/item/item_path) + return get_component_style_item_preview_image(item_path) + +/obj/machinery/vending + /// Temporary component-style selections keyed by vending user ref while the normal vend path creates the item. + var/list/greyscale_component_style_vend_details + +/obj/machinery/vending/proc/open_component_style_vending_menu(datum/data/vending_product/product, list/params, mob/user) + var/obj/item/item_path = product.product_path + var/list/allowed_configs = list() + if(initial(item_path.greyscale_config_worn)) + allowed_configs += "[initial(item_path.greyscale_config_worn)]" + else if(initial(item_path.greyscale_config)) + allowed_configs += "[initial(item_path.greyscale_config)]" + + var/preview_icon_file = initial(item_path.greyscale_component_worn_icon_file) || initial(item_path.greyscale_component_icon_file) + var/list/preview_state_overrides + if(preview_icon_file == initial(item_path.greyscale_component_worn_icon_file)) + preview_state_overrides = initial(item_path.greyscale_component_worn_state_overrides) + + var/datum/greyscale_modify_menu/component_style/menu = new( + src, + user, + allowed_configs, + CALLBACK(src, PROC_REF(_vend_component_style), params, user), + starting_icon_state = initial(item_path.icon_state), + starting_config = initial(item_path.greyscale_config_worn) || initial(item_path.greyscale_config), + starting_colors = initial(item_path.greyscale_colors), + component_style_type = initial(item_path.greyscale_component_style_type), + selected_cores = initial(item_path.greyscale_component_cores), + selected_accessories = initial(item_path.greyscale_component_accessories), + preview_icon_file = preview_icon_file, + preview_digi_fallback_icon_file = initial(item_path.greyscale_component_digi_fallback_icon_file), + preview_state_overrides = preview_state_overrides, + default_colors = initial(item_path.greyscale_colors), + default_cores = initial(item_path.greyscale_component_cores), + default_accessories = initial(item_path.greyscale_component_accessories), + ) + menu.ui_interact(user) + return TRUE + +/obj/machinery/vending/proc/_vend_component_style(list/params, mob/user, datum/greyscale_modify_menu/menu) + PRIVATE_PROC(TRUE) + + var/datum/greyscale_modify_menu/component_style/component_menu = menu + if(user != menu.user || !istype(component_menu)) + return + + var/list/component_details = list( + INFO_GREYSCALE_COMPONENT_CORES = component_menu.selected_cores.Copy(), + INFO_GREYSCALE_COMPONENT_ACCESSORIES = length(component_menu.selected_accessories) ? component_menu.selected_accessories.Copy() : list(/datum/greyscale_component_style::no_accessories_marker), + ) + if(!greyscale_component_style_vend_details) + greyscale_component_style_vend_details = list() + var/user_ref = REF(user) + greyscale_component_style_vend_details[user_ref] = component_details + vend(params, user, component_menu.split_colors) + greyscale_component_style_vend_details -= user_ref + if(!length(greyscale_component_style_vend_details)) + greyscale_component_style_vend_details = null + +/datum/greyscale_modify_menu/component_style + var/datum/greyscale_component_style/component_style + var/component_style_type + var/list/selected_cores + var/list/selected_accessories + var/preview_icon_file + var/preview_use_digi = FALSE + var/preview_digi_fallback_icon_file + var/list/preview_state_overrides + var/list/default_colors + var/list/default_cores + var/list/default_accessories + +/datum/greyscale_modify_menu/component_style/New(datum/target, client/user, list/allowed_configs, datum/callback/apply_callback, starting_icon_state = "", starting_config, starting_colors, unlocked = FALSE, component_style_type = null, list/selected_cores = null, list/selected_accessories = null, preview_icon_file = null, preview_use_digi = FALSE, preview_digi_fallback_icon_file = null, list/preview_state_overrides = null, default_colors = null, list/default_cores = null, list/default_accessories = null) + src.component_style_type = component_style_type || /datum/greyscale_component_style + var/style_type = src.component_style_type + src.component_style = new style_type + src.selected_cores = component_style.sanitize_core_selection(selected_cores) + if(isnull(selected_accessories)) + selected_accessories = component_style.default_accessories + src.selected_accessories = component_style.sanitize_accessories(selected_accessories, src.selected_cores) + src.preview_icon_file = preview_icon_file + src.preview_use_digi = preview_use_digi + src.preview_digi_fallback_icon_file = preview_digi_fallback_icon_file + src.preview_state_overrides = preview_state_overrides + src.default_colors = component_style.default_color_list(SSgreyscale.ParseColorString(default_colors || starting_colors)) + src.default_cores = component_style.sanitize_core_selection(default_cores) + if(isnull(default_accessories)) + default_accessories = component_style.default_accessories + src.default_accessories = component_style.sanitize_accessories(default_accessories, src.default_cores) + sprite_data = list( + "icon_states" = list(component_style.output_icon_state), + "finished" = null, + "steps" = list(), + "time_spent" = 0, + ) + update_component_color_labels() + return ..() + +/datum/greyscale_modify_menu/component_style/Destroy() + QDEL_NULL(component_style) + selected_cores = null + selected_accessories = null + preview_state_overrides = null + default_colors = null + default_cores = null + default_accessories = null + return ..() + +/datum/greyscale_modify_menu/component_style/ReadColorsFromString(colorString) + if(..()) + return TRUE + normalize_split_colors() + return TRUE + +/datum/greyscale_modify_menu/component_style/ui_data(mob/user) + normalize_split_colors() + var/list/data = list() + data["greyscale_config"] = "[config.name]" + data["full_color_string"] = split_colors.Join("") + + var/list/active_color_ids = component_style.active_color_ids(selected_cores, selected_accessories) + var/list/active_colors = list() + for(var/i in 1 to config.expected_colors) + if(!(i in active_color_ids)) + continue + active_colors += list(list( + "index" = i, + "label" = color_labels?["[i]"], + "value" = (length(split_colors) >= i) ? split_colors[i] : rgb(100, 100, 100), + )) + data["colors"] = active_colors + + data["generate_full_preview"] = generate_full_preview + data["unlocked"] = unlocked + data["refreshing"] = refreshing + data["monitoring_files"] = !!(config.datum_flags & DF_ISPROCESSING) + data["sprites_dir"] = dir2text(sprite_dir) + data["icon_state"] = icon_state + data["sprites"] = sprite_data || list( + "icon_states" = list(component_style.output_icon_state), + "finished" = null, + "steps" = list(), + "time_spent" = 0, + ) + data["hide_full_color_string"] = FALSE + data["component_style"] = list( + "core_components" = component_style.core_ui_data(selected_cores), + "accessories" = component_style.accessory_ui_data(selected_accessories, selected_cores), + ) + return data + +/datum/greyscale_modify_menu/component_style/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + switch(action) + if("component_style_set_core") + var/core_component = params["component"] + var/new_style = params["value"] + if(!set_core_component(core_component, new_style)) + return TRUE + queue_refresh() + return TRUE + + if("component_style_toggle_accessory") + var/accessory = params["accessory"] + if(!component_style.accessory_allowed(accessory, selected_cores)) + return TRUE + if(accessory in selected_accessories) + selected_accessories -= accessory + else + selected_accessories += accessory + update_component_color_labels() + queue_refresh() + return TRUE + + if("random_all_colors") + for(var/color_id in component_style.active_color_ids(selected_cores, selected_accessories)) + randomize_color(color_id) + queue_refresh() + return TRUE + + if("component_style_reset_color") + var/color_id = text2num(params["color_index"]) + if(color_id && length(default_colors) >= color_id) + normalize_split_colors() + split_colors[color_id] = default_colors[color_id] + queue_refresh() + return TRUE + + if("component_style_reset_all") + selected_cores = default_cores.Copy() + selected_accessories = default_accessories.Copy() + split_colors = islist(default_colors) ? default_colors.Copy() : list() + normalize_split_colors() + update_component_color_labels() + queue_refresh() + return TRUE + + return ..() + +/datum/greyscale_modify_menu/component_style/proc/set_core_component(core_key, new_style) + for(var/list/core_component as anything in component_style.core_components) + if(core_component["key"] != core_key) + continue + if(!component_style.core_option_exists(core_component, new_style)) + return FALSE + selected_cores[core_key] = new_style + prune_invalid_accessories() + update_component_color_labels() + return TRUE + return FALSE + +/datum/greyscale_modify_menu/component_style/proc/prune_invalid_accessories() + selected_accessories = component_style.sanitize_accessories(selected_accessories, selected_cores) + +/datum/greyscale_modify_menu/component_style/proc/update_component_color_labels() + color_labels = component_style.active_color_labels(selected_cores, selected_accessories) + +/datum/greyscale_modify_menu/component_style/proc/normalize_split_colors() + if(!config) + return + if(!islist(split_colors)) + split_colors = list() + if(length(split_colors) > config.expected_colors) + split_colors.Cut(config.expected_colors + 1) + while(length(split_colors) < config.expected_colors) + var/next_color_index = length(split_colors) + 1 + split_colors += (islist(default_colors) && length(default_colors) >= next_color_index) ? default_colors[next_color_index] : rgb(100, 100, 100) + +/datum/greyscale_modify_menu/component_style/refresh_preview() + normalize_split_colors() + var/list/used_colors = split_colors.Copy(1, config.expected_colors + 1) + + sprite_data = list() + sprite_data["icon_states"] = list(component_style.output_icon_state) + icon_state = component_style.output_icon_state + + var/time_spent = TICK_USAGE + var/icon/preview_icon = component_style.build_icon(preview_icon_file, used_colors, selected_cores, selected_accessories, preview_use_digi, preview_digi_fallback_icon_file, only_dir = sprite_dir, state_overrides = preview_state_overrides) + var/image/finished = image(preview_icon, icon_state = icon_state) + time_spent = TICK_USAGE - time_spent + + sprite_data["time_spent"] = TICK_DELTA_TO_MS(time_spent) + sprite_data["finished"] = icon2html(finished, user, dir = sprite_dir, sourceonly = TRUE) + sprite_data["steps"] = list() + refreshing = FALSE diff --git a/modular_zubbers/code/modules/loadout/categories/heads.dm b/modular_zubbers/code/modules/loadout/categories/heads.dm index aad20c2fbca..95d7ddc1b68 100644 --- a/modular_zubbers/code/modules/loadout/categories/heads.dm +++ b/modular_zubbers/code/modules/loadout/categories/heads.dm @@ -76,6 +76,22 @@ item_path = /obj/item/clothing/head/soft/sec restricted_roles = list(ALL_JOBS_SEC) +/datum/loadout_item/head/security_cap/recolorable + name = "Recolorable Security Cap" + item_path = /obj/item/clothing/head/soft/sec/recolorable + restricted_roles = list(ALL_JOBS_SEC) + greyscale_component_preview_icon_file = 'modular_zubbers/icons/obj/clothing/head/hats.dmi' + +/datum/loadout_item/head/security_cap/recolorable/set_item_color(datum/preference_middleware/loadout/manager, mob/user) + return set_component_style_item_color(manager, user) + +/datum/loadout_item/head/security_cap/recolorable/set_slot_greyscale(datum/preference_middleware/loadout/manager, datum/greyscale_modify_menu/open_menu) + return set_component_style_slot_greyscale(manager, open_menu) + +/datum/loadout_item/head/security_cap/recolorable/on_equip_item(obj/item/equipped_item, list/item_details, mob/living/carbon/human/equipper, datum/outfit/outfit, visuals_only = FALSE) + . = ..() + apply_component_style_to_equipped_item(equipped_item, item_details) + /datum/loadout_item/head/security_cap/veteran name = "Veteran's Soft Security Cap" item_path = /obj/item/clothing/head/soft/veteran diff --git a/modular_zubbers/code/modules/loadout/categories/under.dm b/modular_zubbers/code/modules/loadout/categories/under.dm index eb1ff3a1f67..c6318597eb6 100644 --- a/modular_zubbers/code/modules/loadout/categories/under.dm +++ b/modular_zubbers/code/modules/loadout/categories/under.dm @@ -80,6 +80,21 @@ item_path = /obj/item/clothing/under/rank/security/officer/redsec restricted_roles = list(ALL_JOBS_SEC) +/datum/loadout_item/uniform/security/officer/recolorable + name = "Recolorable Security Uniform" + item_path = /obj/item/clothing/under/rank/security/officer/recolorable + restricted_roles = list(ALL_JOBS_SEC) + +/datum/loadout_item/uniform/security/officer/recolorable/set_item_color(datum/preference_middleware/loadout/manager, mob/user) + return set_component_style_item_color(manager, user) + +/datum/loadout_item/uniform/security/officer/recolorable/set_slot_greyscale(datum/preference_middleware/loadout/manager, datum/greyscale_modify_menu/open_menu) + return set_component_style_slot_greyscale(manager, open_menu) + +/datum/loadout_item/uniform/security/officer/recolorable/on_equip_item(obj/item/equipped_item, list/item_details, mob/living/carbon/human/equipper, datum/outfit/outfit, visuals_only = FALSE) + . = ..() + apply_component_style_to_equipped_item(equipped_item, item_details) + /datum/loadout_item/uniform/security/peacekeeper/armadyne name = "Armadyne Corporate Uniform" item_path =/obj/item/clothing/under/rank/security/peacekeeper/armadyne diff --git a/modular_zubbers/code/modules/security/sec_clothing_overrides.dm b/modular_zubbers/code/modules/security/sec_clothing_overrides.dm index f45003c85b3..a1afa4cf950 100644 --- a/modular_zubbers/code/modules/security/sec_clothing_overrides.dm +++ b/modular_zubbers/code/modules/security/sec_clothing_overrides.dm @@ -13,6 +13,21 @@ icon_state = "security" worn_icon_state = "security" +/obj/item/storage/belt/security/setup_reskins() + AddComponent(/datum/component/reskinable_item, /datum/atom_skin/sec_belt, infinite = FALSE) + +/datum/atom_skin/sec_belt + abstract_type = /datum/atom_skin/sec_belt + +/datum/atom_skin/sec_belt/standard + preview_name = "Standard" + +/datum/atom_skin/sec_belt/black + preview_name = "Black" + new_icon = 'modular_zubbers/icons/obj/clothing/belts/belts.dmi' + new_worn_icon = 'modular_zubbers/icons/mob/clothing/belt.dmi' + new_icon_state = "security_black" + ///Enables you to quickdraw weapons from security holsters /datum/storage/security/open_storage(datum/source, mob/user) var/atom/resolve_parent = parent @@ -51,6 +66,21 @@ icon_state = "sunhudsec" glass_colour_type = /datum/client_colour/glass_colour/red +/obj/item/clothing/glasses/hud/security/sunglasses/setup_reskins() + AddComponent(/datum/component/reskinable_item, /datum/atom_skin/sec_hud_sunglasses, infinite = FALSE) + +/datum/atom_skin/sec_hud_sunglasses + abstract_type = /datum/atom_skin/sec_hud_sunglasses + +/datum/atom_skin/sec_hud_sunglasses/standard + preview_name = "Standard" + +/datum/atom_skin/sec_hud_sunglasses/sunglasses + preview_name = "Sunglasses" + new_icon = 'icons/obj/clothing/glasses.dmi' + new_worn_icon = 'icons/mob/clothing/eyes.dmi' + new_icon_state = "sun" + /obj/item/clothing/glasses/hud/security/sunglasses/eyepatch icon_state = "hudpatch" base_icon_state = "hudpatch" @@ -203,16 +233,37 @@ worn_icon = 'icons/mob/clothing/hands.dmi' icon_state = "sec" +/obj/item/clothing/gloves/color/black/security/setup_reskins() + AddComponent(/datum/component/reskinable_item, /datum/atom_skin/sec_gloves, infinite = FALSE) + /obj/item/clothing/gloves/tackler/security //Can't just overwrite tackler, as there's a ton of subtypes that we'd then need to account for. This is easier. MUCH easier. icon = 'icons/obj/clothing/gloves.dmi' worn_icon = 'icons/mob/clothing/hands.dmi' icon_state = "gorilla" +/obj/item/clothing/gloves/tackler/security/setup_reskins() + AddComponent(/datum/component/reskinable_item, /datum/atom_skin/sec_gloves, infinite = FALSE) + /obj/item/clothing/gloves/tackler/combat icon = 'icons/obj/clothing/gloves.dmi' worn_icon = 'icons/mob/clothing/hands.dmi' icon_state = "gorilla" +/obj/item/clothing/gloves/tackler/combat/setup_reskins() + AddComponent(/datum/component/reskinable_item, /datum/atom_skin/sec_gloves, infinite = FALSE) + +/datum/atom_skin/sec_gloves + abstract_type = /datum/atom_skin/sec_gloves + +/datum/atom_skin/sec_gloves/standard + preview_name = "Standard" + +/datum/atom_skin/sec_gloves/plain_black + preview_name = "Plain Black" + new_icon = 'modular_skyrat/master_files/icons/obj/clothing/gloves.dmi' + new_worn_icon = 'modular_skyrat/master_files/icons/mob/clothing/hands.dmi' + new_icon_state = "combat" + /obj/item/clothing/gloves/kaza_ruk/sec icon = 'modular_skyrat/master_files/icons/obj/clothing/gloves.dmi' worn_icon = 'modular_skyrat/master_files/icons/mob/clothing/hands.dmi' @@ -646,12 +697,6 @@ inhand_icon_state = "security" worn_icon_state = "security" -/obj/item/storage/belt/security/redsec/Initialize(mapload) - . = ..() - var/list/reskin_components = GetComponents(/datum/component/reskinable_item) - for(var/datum/component/reskinable_item/reskin_component as anything in reskin_components) - qdel(reskin_component) - /obj/item/storage/belt/holster desc = "A rather plain but still cool looking holster that can hold a handgun, and some ammo." diff --git a/modular_zubbers/code/modules/vending/multisec.dm b/modular_zubbers/code/modules/vending/multisec.dm index e2b94a8d070..9246567b7fb 100644 --- a/modular_zubbers/code/modules/vending/multisec.dm +++ b/modular_zubbers/code/modules/vending/multisec.dm @@ -29,6 +29,7 @@ /obj/item/clothing/head/security_kepi = 6, /obj/item/clothing/head/beret/sec/peacekeeper/armadyne = 6, /obj/item/clothing/head/soft/sec = 6, + /obj/item/clothing/head/soft/sec/recolorable = 6, /obj/item/clothing/head/security_beanie = 6, /obj/item/clothing/head/costume/ushanka/sec = 10, /obj/item/clothing/head/playbunnyears/security = 6, @@ -59,6 +60,7 @@ /obj/item/clothing/neck/security_cape = 6, /obj/item/clothing/neck/security_cape/armplate = 6, /obj/item/clothing/under/rank/security/officer = 6, + /obj/item/clothing/under/rank/security/officer/recolorable = 6, /obj/item/clothing/under/rank/security/peacekeeper/skirt = 6, /obj/item/clothing/under/rank/security/peacekeeper/miniskirt = 6, /obj/item/clothing/under/rank/security/officer/blueshirt = 6, diff --git a/modular_zubbers/icons/mob/clothing/belt.dmi b/modular_zubbers/icons/mob/clothing/belt.dmi index c1baa4724ac..7a738f20144 100644 Binary files a/modular_zubbers/icons/mob/clothing/belt.dmi and b/modular_zubbers/icons/mob/clothing/belt.dmi differ diff --git a/modular_zubbers/icons/mob/clothing/head/hats.dmi b/modular_zubbers/icons/mob/clothing/head/hats.dmi index 3132c691b66..cb911e5ac9b 100644 Binary files a/modular_zubbers/icons/mob/clothing/head/hats.dmi and b/modular_zubbers/icons/mob/clothing/head/hats.dmi differ diff --git a/modular_zubbers/icons/mob/clothing/under/security.dmi b/modular_zubbers/icons/mob/clothing/under/security.dmi index 4be4b12d062..d067248da07 100644 Binary files a/modular_zubbers/icons/mob/clothing/under/security.dmi and b/modular_zubbers/icons/mob/clothing/under/security.dmi differ diff --git a/modular_zubbers/icons/obj/clothing/belts/belts.dmi b/modular_zubbers/icons/obj/clothing/belts/belts.dmi index cf1e9180c0f..3f19c07adce 100644 Binary files a/modular_zubbers/icons/obj/clothing/belts/belts.dmi and b/modular_zubbers/icons/obj/clothing/belts/belts.dmi differ diff --git a/modular_zubbers/icons/obj/clothing/head/hats.dmi b/modular_zubbers/icons/obj/clothing/head/hats.dmi index 5d38bf52758..a337c183cfd 100644 Binary files a/modular_zubbers/icons/obj/clothing/head/hats.dmi and b/modular_zubbers/icons/obj/clothing/head/hats.dmi differ diff --git a/modular_zubbers/icons/obj/clothing/under/security.dmi b/modular_zubbers/icons/obj/clothing/under/security.dmi index dd2b6a8121c..cef38ede37e 100644 Binary files a/modular_zubbers/icons/obj/clothing/under/security.dmi and b/modular_zubbers/icons/obj/clothing/under/security.dmi differ diff --git a/tgstation.dme b/tgstation.dme index f9912fa9574..18b06af1870 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -9022,6 +9022,7 @@ #include "modular_skyrat\modules\xenos_skyrat_redo\code\xeno_types\warrior.dm" #include "modular_zubbers\code\__DEFINES\bloodsucker_defines.dm" #include "modular_zubbers\code\__DEFINES\guardian_defines.dm" +#include "modular_zubbers\code\__DEFINES\loadout.dm" #include "modular_zubbers\code\__DEFINES\maps.dm" #include "modular_zubbers\code\__DEFINES\moonstation_defines.dm" #include "modular_zubbers\code\__DEFINES\projectiles.dm" @@ -9759,6 +9760,7 @@ #include "modular_zubbers\code\modules\general_sprite_overrides\turfs\walls.dm" #include "modular_zubbers\code\modules\ghost_wavedefence\wave_def.dm" #include "modular_zubbers\code\modules\gladiator\code\game\objects\items\gladiator_items.dm" +#include "modular_zubbers\code\modules\greyscale\component_style.dm" #include "modular_zubbers\code\modules\hydroponics\gene_modder.dm" #include "modular_zubbers\code\modules\hydroponics\plant_genes.dm" #include "modular_zubbers\code\modules\hydroponics\grown\rocks.dm" diff --git a/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx b/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx index 84746728c77..e92f10ca311 100644 --- a/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx +++ b/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx @@ -18,6 +18,7 @@ import { Window } from '../layouts'; type ColorEntry = { index: number; + label?: string; //BUBBER ADDITION - dynamic uniforms value: string; }; @@ -37,6 +38,11 @@ type SpriteEntry = { type GreyscaleMenuData = { greyscale_config: string; colors: ColorEntry[]; + //BUBBER ADDITION START - dynamic uniforms + full_color_string?: string; + hide_full_color_string?: boolean; + component_style?: ComponentStyleData; + //BUBBER ADDITION END - dynamic uniforms sprites: SpriteData; generate_full_preview: boolean; unlocked: boolean; @@ -46,6 +52,25 @@ type GreyscaleMenuData = { refreshing: boolean; }; +//BUBBER ADDITION START - dynamic uniforms +type ComponentStyleOption = { + name: string; + value: string; + selected: boolean; +}; + +type ComponentStyleCore = { + name: string; + key: string; + options: ComponentStyleOption[]; +}; + +type ComponentStyleData = { + core_components: ComponentStyleCore[]; + accessories: ComponentStyleOption[]; +}; +//BUBBER ADDITION END - dynamic uniforms + enum Direction { North = 'north', NorthEast = 'northeast', @@ -86,6 +111,68 @@ const ConfigDisplay = (props) => { ); }; +//BUBBER ADDITION START - dynamic uniforms +const ComponentStyleDisplay = (props) => { + const { act, data } = useBackend(); + const style = data.component_style; + + if (!style) { + return null; + } + + return ( +
+ + {style.core_components.map((component) => ( + + + {component.options.map((item) => ( + + + act('component_style_set_core', { + component: component.key, + value: item.value, + }) + } + > + {item.name} + + + ))} + + + ))} + {!!style.accessories.length && ( + + + {style.accessories.map((item) => ( + + + act('component_style_toggle_accessory', { + accessory: item.value, + }) + } + > + {item.name} + + + ))} + + + )} + +
+ ); +}; +//BUBBER ADDITION END - dynamic uniforms const ColorDisplay = (props) => { const { act, data } = useBackend(); @@ -93,23 +180,37 @@ const ColorDisplay = (props) => { return (
- -