From 8b9da265bab1070fbcae77b94db2743d29dfd3e0 Mon Sep 17 00:00:00 2001 From: Runi-c <5150427+Runi-c@users.noreply.github.com> Date: Thu, 28 Nov 2024 08:17:18 -0700 Subject: [PATCH] Digitigrade code refactor and shoes + explorer suit (Using GAGS and zero sprite bloat) (#88096) ## About The Pull Request Refactors and extends the existing [digitigrade clothing system](https://github.com/tgstation/tgstation/pull/85406) to also support oversuits and shoes (only enabled for sneakers, specific boots, and shaft miner's explorer suit). All digitigrade-specific clothes are generated by GAGS by color sampling the base sprite and applying it to a greyscale template. ![image](https://github.com/user-attachments/assets/c4ce5996-9373-4d76-a0e5-a6b094dd10b4) More up to date boots: ![image](https://github.com/user-attachments/assets/8ead8056-25a3-42dd-9fde-e838e2a810b9) ![image](https://github.com/user-attachments/assets/783a1201-8a77-45a0-af85-8a63b41b63a8) Credit to MrMelbert for prototype code and Junkgle for new sprites ## Why It's Good For The Game Looks good, doesn't introduce maintainability issues or sprite bloat ## Changelog :cl: add: digitigrade lizards can wear certain shoes and suits image: added digitigrade shoes & oversuit templates refactor: improved digi clothing generator code /:cl: --------- Co-authored-by: MrMelbert Co-authored-by: Roryl-c <5150427+Roryl-c@users.noreply.github.com> --- code/__DEFINES/inventory.dm | 1 - .../greyscale_configs/greyscale_clothes.dm | 13 ++- .../greyscale/json_configs/digitigrade.json | 39 +++++++ .../json_configs/jumpsuit_worn_digilegs.json | 10 -- code/game/objects/items.dm | 6 - code/modules/clothing/shoes/_shoes.dm | 3 + code/modules/clothing/shoes/boots.dm | 6 + code/modules/clothing/shoes/sneakers.dm | 8 ++ code/modules/clothing/suits/_suits.dm | 4 + code/modules/clothing/under/_under.dm | 5 +- code/modules/clothing/under/color.dm | 4 +- code/modules/clothing/under/miscellaneous.dm | 4 +- .../modules/mining/equipment/explorer_gear.dm | 4 + .../living/carbon/human/human_update_icons.dm | 109 ++++++++++++------ .../screenshots/screenshot_digi_leg_test.png | Bin 1692 -> 1758 bytes ...anoids__datum_species_lizard_ashwalker.png | Bin 1223 -> 1246 bytes icons/mob/clothing/digi_template.dmi | Bin 0 -> 706 bytes icons/mob/clothing/under/digi_template.dmi | Bin 586 -> 0 bytes icons/mob/clothing/under/masking_helpers.dmi | Bin 454 -> 433 bytes 19 files changed, 156 insertions(+), 60 deletions(-) create mode 100644 code/datums/greyscale/json_configs/digitigrade.json delete mode 100644 code/datums/greyscale/json_configs/jumpsuit_worn_digilegs.json create mode 100644 icons/mob/clothing/digi_template.dmi delete mode 100644 icons/mob/clothing/under/digi_template.dmi diff --git a/code/__DEFINES/inventory.dm b/code/__DEFINES/inventory.dm index 87f7ba50f75..3e9b0bcc33c 100644 --- a/code/__DEFINES/inventory.dm +++ b/code/__DEFINES/inventory.dm @@ -163,7 +163,6 @@ DEFINE_BITFIELD(no_equip_flags, list( /// The sprite works fine for digitigrade legs as-is. #define CLOTHING_DIGITIGRADE_VARIATION_NO_NEW_ICON (1<<2) /// Auto-generates the leg portion of the sprite with GAGS -/// Suggested that you set [/obj/item/var/digitigrade_greyscale_config_worn] when using this flag #define CLOTHING_DIGITIGRADE_MASK (1<<3) /// All variation flags which render "correctly" on a digitigrade leg setup diff --git a/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm b/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm index bdc2a7d2928..54ea9f41857 100644 --- a/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm +++ b/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm @@ -276,11 +276,6 @@ icon_file = 'icons/mob/inhands/clothing/suits_righthand.dmi' json_config = 'code/datums/greyscale/json_configs/jumpsuit_prison_inhand.json' -/datum/greyscale_config/jumpsuit/worn_digi - name = "Jumpsuit Worn (Digitigrate)" - icon_file = 'icons/mob/clothing/under/digi_template.dmi' - json_config = 'code/datums/greyscale/json_configs/jumpsuit_worn_digilegs.json' - /datum/greyscale_config/eth_tunic name = "Ethereal Tunic" icon_file = 'icons/obj/clothing/under/ethereal.dmi' @@ -627,6 +622,14 @@ name = "Waistcoat (Worn)" icon_file = 'icons/mob/clothing/accessories.dmi' +// Digi Stuff + +/datum/greyscale_config/digitigrade + name = "Digitigrade Clothes" + icon_file = 'icons/mob/clothing/digi_template.dmi' + json_config = 'code/datums/greyscale/json_configs/digitigrade.json' + + // // SUIT + HEAD // (Specifically for toggleable suits with hats, i.e. winter coats) diff --git a/code/datums/greyscale/json_configs/digitigrade.json b/code/datums/greyscale/json_configs/digitigrade.json new file mode 100644 index 00000000000..899dff334e4 --- /dev/null +++ b/code/datums/greyscale/json_configs/digitigrade.json @@ -0,0 +1,39 @@ +{ + "jumpsuit_worn": [ + { + "type": "icon_state", + "icon_state": "jumpsuit", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ], + "oversuit_worn": [ + { + "type": "icon_state", + "icon_state": "oversuit", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ], + "sneakers_worn": [ + { + "type": "icon_state", + "icon_state": "shoes_colored", + "blend_mode": "overlay", + "color_ids": [ 1 ] + }, + { + "type": "icon_state", + "icon_state": "shoes_uncolored", + "blend_mode": "overlay" + } + ], + "boots_worn": [ + { + "type": "icon_state", + "icon_state": "boots", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ] +} diff --git a/code/datums/greyscale/json_configs/jumpsuit_worn_digilegs.json b/code/datums/greyscale/json_configs/jumpsuit_worn_digilegs.json deleted file mode 100644 index 9aa201cece3..00000000000 --- a/code/datums/greyscale/json_configs/jumpsuit_worn_digilegs.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "": [ - { - "type": "icon_state", - "icon_state": "jumpsuit", - "blend_mode": "overlay", - "color_ids": [ 1 ] - } - ] -} diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index f32690f334c..b8aec816391 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -39,12 +39,6 @@ ///The config type to use for greyscaled belt overlays. Both this and greyscale_colors must be assigned to work. var/greyscale_config_belt - /// Greyscale config used when generating digitigrade versions of the sprite. - var/digitigrade_greyscale_config_worn - /// Greyscale colors used when generating digitigrade versions of the sprite. - /// Optional - If not set it will default to normal greyscale colors, or approximate them if those are unset as well - var/digitigrade_greyscale_colors - /* !!!!!!!!!!!!!!! IMPORTANT !!!!!!!!!!!!!! IF YOU ADD MORE ICON CRAP TO THIS diff --git a/code/modules/clothing/shoes/_shoes.dm b/code/modules/clothing/shoes/_shoes.dm index 3ecec813a95..1793a212ae3 100644 --- a/code/modules/clothing/shoes/_shoes.dm +++ b/code/modules/clothing/shoes/_shoes.dm @@ -110,6 +110,9 @@ var/mob/M = loc M.update_worn_shoes() +/obj/item/clothing/shoes/generate_digitigrade_icons(icon/base_icon, greyscale_colors) + return icon(SSgreyscale.GetColoredIconByType(/datum/greyscale_config/digitigrade, greyscale_colors), "boots_worn") + /** * adjust_laces adjusts whether our shoes (assuming they can_be_tied) and tied, untied, or knotted * diff --git a/code/modules/clothing/shoes/boots.dm b/code/modules/clothing/shoes/boots.dm index 77e7b2ff369..2dba6ed24fc 100644 --- a/code/modules/clothing/shoes/boots.dm +++ b/code/modules/clothing/shoes/boots.dm @@ -3,6 +3,7 @@ desc = "High speed, low drag combat boots." icon_state = "jackboots" inhand_icon_state = "jackboots" + supports_variations_flags = CLOTHING_DIGITIGRADE_MASK body_parts_covered = FEET|LEGS armor_type = /datum/armor/shoes_combat strip_delay = 40 @@ -45,6 +46,7 @@ desc = "Nanotrasen-issue Security combat boots for combat scenarios or combat situations. All combat, all the time." icon_state = "jackboots" inhand_icon_state = "jackboots" + supports_variations_flags = CLOTHING_DIGITIGRADE_MASK strip_delay = 30 equip_delay_other = 50 resistance_flags = NONE @@ -71,6 +73,7 @@ desc = "Is it just me or is there a pair of jackboots on the floor?" icon_state = "ftc_boots" inhand_icon_state = null + supports_variations_flags = NONE /obj/item/clothing/shoes/jackboots/floortile/Initialize(mapload) . = ..() @@ -81,6 +84,7 @@ desc = "Boots lined with 'synthetic' animal fur." icon_state = "winterboots" inhand_icon_state = null + supports_variations_flags = CLOTHING_DIGITIGRADE_MASK armor_type = /datum/armor/shoes_winterboots cold_protection = FEET|LEGS min_cold_protection_temperature = SHOES_MIN_TEMP_PROTECT @@ -131,6 +135,7 @@ icon_state = "workboots" inhand_icon_state = "jackboots" armor_type = /datum/armor/shoes_workboots + supports_variations_flags = CLOTHING_DIGITIGRADE_MASK strip_delay = 20 equip_delay_other = 40 lace_time = 8 SECONDS @@ -155,6 +160,7 @@ icon_state = "rus_shoes" inhand_icon_state = null lace_time = 8 SECONDS + supports_variations_flags = CLOTHING_DIGITIGRADE_MASK /obj/item/clothing/shoes/russian/Initialize(mapload) . = ..() diff --git a/code/modules/clothing/shoes/sneakers.dm b/code/modules/clothing/shoes/sneakers.dm index 0ae1e6e9caa..1fc47accd35 100644 --- a/code/modules/clothing/shoes/sneakers.dm +++ b/code/modules/clothing/shoes/sneakers.dm @@ -9,9 +9,17 @@ greyscale_config_worn = /datum/greyscale_config/sneakers/worn greyscale_config_inhand_left = /datum/greyscale_config/sneakers/inhand_left greyscale_config_inhand_right = /datum/greyscale_config/sneakers/inhand_right + supports_variations_flags = CLOTHING_DIGITIGRADE_MASK flags_1 = IS_PLAYER_COLORABLE_1 interaction_flags_mouse_drop = NEED_HANDS +/obj/item/clothing/shoes/sneakers/get_general_color(icon/base_icon) + var/colors = SSgreyscale.ParseColorString(greyscale_colors) + return colors ? colors[1] : ..() + +/obj/item/clothing/shoes/sneakers/generate_digitigrade_icons(icon/base_icon, greyscale_colors) + return icon(SSgreyscale.GetColoredIconByType(/datum/greyscale_config/digitigrade, greyscale_colors), "sneakers_worn") + /obj/item/clothing/shoes/sneakers/random/Initialize(mapload) . = ..() greyscale_colors = "#" + random_color() + "#" + random_color() diff --git a/code/modules/clothing/suits/_suits.dm b/code/modules/clothing/suits/_suits.dm index ecae343f685..78ea1d2f3b7 100644 --- a/code/modules/clothing/suits/_suits.dm +++ b/code/modules/clothing/suits/_suits.dm @@ -43,3 +43,7 @@ if(ismob(loc)) var/mob/M = loc M.update_worn_oversuit() + +/obj/item/clothing/suit/generate_digitigrade_icons(icon/base_icon, greyscale_colors) + var/icon/legs = icon(SSgreyscale.GetColoredIconByType(/datum/greyscale_config/digitigrade, greyscale_colors), "oversuit_worn") + return replace_icon_legs(base_icon, legs) diff --git a/code/modules/clothing/under/_under.dm b/code/modules/clothing/under/_under.dm index 85fff72052b..3fa11d2882c 100644 --- a/code/modules/clothing/under/_under.dm +++ b/code/modules/clothing/under/_under.dm @@ -9,7 +9,6 @@ interaction_flags_click = NEED_DEXTERITY armor_type = /datum/armor/clothing_under supports_variations_flags = CLOTHING_DIGITIGRADE_MASK - digitigrade_greyscale_config_worn = /datum/greyscale_config/jumpsuit/worn_digi equip_sound = 'sound/items/equip/jumpsuit_equip.ogg' drop_sound = 'sound/items/handling/cloth_drop.ogg' pickup_sound = 'sound/items/handling/cloth_pickup.ogg' @@ -142,6 +141,10 @@ adjusted = DIGITIGRADE_STYLE update_appearance() +/obj/item/clothing/under/generate_digitigrade_icons(icon/base_icon, greyscale_colors) + var/icon/legs = icon(SSgreyscale.GetColoredIconByType(/datum/greyscale_config/digitigrade, greyscale_colors), "jumpsuit_worn") + return replace_icon_legs(base_icon, legs) + /obj/item/clothing/under/equipped(mob/living/user, slot) ..() if((slot & ITEM_SLOT_ICLOTHING) && freshly_laundered) diff --git a/code/modules/clothing/under/color.dm b/code/modules/clothing/under/color.dm index 9ae1c7d63e3..af64ca4e999 100644 --- a/code/modules/clothing/under/color.dm +++ b/code/modules/clothing/under/color.dm @@ -224,10 +224,12 @@ greyscale_config_inhand_left = null greyscale_config_inhand_right = null greyscale_config_worn = null - digitigrade_greyscale_colors = "#3f3f3f" can_adjust = FALSE flags_1 = NONE +/obj/item/clothing/under/color/rainbow/get_general_color(icon/base_icon) + return "#3f3f3f" + /obj/item/clothing/under/color/jumpskirt/rainbow name = "rainbow jumpskirt" desc = "A multi-colored jumpskirt!" diff --git a/code/modules/clothing/under/miscellaneous.dm b/code/modules/clothing/under/miscellaneous.dm index 70d2a5eeadb..2c428f1c35c 100644 --- a/code/modules/clothing/under/miscellaneous.dm +++ b/code/modules/clothing/under/miscellaneous.dm @@ -33,7 +33,9 @@ desc = "Groovy!" icon_state = "psyche" inhand_icon_state = "p_suit" - digitigrade_greyscale_colors = "#3f3f3f" + +/obj/item/clothing/under/misc/psyche/get_general_color(icon/base_icon) + return "#3f3f3f" /obj/item/clothing/under/misc/vice_officer name = "vice officer's jumpsuit" diff --git a/code/modules/mining/equipment/explorer_gear.dm b/code/modules/mining/equipment/explorer_gear.dm index 216275a2390..a6b2f075b8f 100644 --- a/code/modules/mining/equipment/explorer_gear.dm +++ b/code/modules/mining/equipment/explorer_gear.dm @@ -6,6 +6,7 @@ icon = 'icons/obj/clothing/suits/utility.dmi' worn_icon = 'icons/mob/clothing/suits/utility.dmi' inhand_icon_state = null + supports_variations_flags = CLOTHING_DIGITIGRADE_MASK body_parts_covered = CHEST|GROIN|LEGS|ARMS cold_protection = CHEST|GROIN|LEGS|ARMS min_cold_protection_temperature = FIRE_SUIT_MIN_TEMP_PROTECT @@ -15,6 +16,9 @@ armor_type = /datum/armor/hooded_explorer resistance_flags = FIRE_PROOF +/obj/item/clothing/suit/hooded/explorer/get_general_color(icon/base_icon) + return "#796755" + /datum/armor/hooded_explorer melee = 30 bullet = 10 diff --git a/code/modules/mob/living/carbon/human/human_update_icons.dm b/code/modules/mob/living/carbon/human/human_update_icons.dm index 73ce6808339..6227e9558db 100644 --- a/code/modules/mob/living/carbon/human/human_update_icons.dm +++ b/code/modules/mob/living/carbon/human/human_update_icons.dm @@ -573,20 +573,70 @@ There are several things that need to be remembered: return icon(female_clothing_icon) -// These coordonates point to roughly somewhere in the middle of the left leg -// Used in approximating what color the pants of clothing should be +/// Modifies a sprite to conform to digitigrade body shapes +/proc/wear_digi_version(icon/base_icon, obj/item/item, key, greyscale_colors) + ASSERT(istype(item), "wear_digi_version: no item passed") + ASSERT(istext(key), "wear_digi_version: no key passed") + if(isnull(greyscale_colors) || length(SSgreyscale.ParseColorString(greyscale_colors)) > 1) + greyscale_colors = item.get_general_color(base_icon) + + var/index = "[key]-[item.type]-[greyscale_colors]" + var/static/list/digitigrade_clothing_cache = list() + var/icon/resulting_icon = digitigrade_clothing_cache[index] + if(!resulting_icon) + resulting_icon = item.generate_digitigrade_icons(base_icon, greyscale_colors) + if(!resulting_icon) + stack_trace("[item.type] is set to generate a masked digitigrade icon, but generate_digitigrade_icons was not implemented (or error'd).") + return base_icon + digitigrade_clothing_cache[index] = fcopy_rsc(resulting_icon) + + return icon(resulting_icon) + +/// Modifies a sprite to replace the legs with a new version +/proc/replace_icon_legs(icon/base_icon, icon/new_legs) + var/static/icon/leg_mask + if(!leg_mask) + leg_mask = icon('icons/mob/clothing/under/masking_helpers.dmi', "digi_leg_mask") + + // cuts the legs off + base_icon.Blend(leg_mask, ICON_SUBTRACT) + // staples the new legs on + base_icon.Blend(new_legs, ICON_OVERLAY) + return base_icon + +/** + * Generates a digitigrade version of this item's worn icon + * + * Arguments: + * * base_icon: The icon to generate the digitigrade icon from + * * greyscale_colors: The greyscale colors to use for the digitigrade icon + * + * Returns an icon that is the digitigrade version of the item's worn icon + * Returns null if the item has no support for digitigrade variations via this method + */ +/obj/item/proc/generate_digitigrade_icons(icon/base_icon, greyscale_colors) + return null + +/** + * Get what color the item is on "average" + * Can be used to approximate what color this item is/should be + * + * Arguments: + * * base_icon: The icon to get the color from + */ +/obj/item/proc/get_general_color(icon/base_icon) + if(greyscale_colors && length(SSgreyscale.ParseColorString(greyscale_colors)) == 1) + return greyscale_colors + return color + +// These coordinates point to the middle of the left leg #define LEG_SAMPLE_X_LOWER 13 #define LEG_SAMPLE_X_UPPER 14 - #define LEG_SAMPLE_Y_LOWER 8 #define LEG_SAMPLE_Y_UPPER 9 -/// Modifies a sprite to conform to digitigrade body shapes -/proc/wear_digi_version(icon/base_icon, key, greyscale_config = /datum/greyscale_config/jumpsuit/worn_digi, greyscale_colors) - ASSERT(key, "wear_digi_version: no key passed") - ASSERT(ispath(greyscale_config, /datum/greyscale_config), "wear_digi_version: greyscale_config is not a valid path (got: [greyscale_config])") - // items with greyscale colors containing multiple colors are invalid - if(isnull(greyscale_colors) || length(SSgreyscale.ParseColorString(greyscale_colors)) > 1) +/obj/item/clothing/get_general_color(icon/base_icon) + if(slot_flags & (ITEM_SLOT_ICLOTHING|ITEM_SLOT_OCLOTHING)) var/pant_color // approximates the color of the pants by sampling a few pixels in the middle of the left leg for(var/x in LEG_SAMPLE_X_LOWER to LEG_SAMPLE_X_UPPER) @@ -594,37 +644,26 @@ There are several things that need to be remembered: var/xy_color = base_icon.GetPixel(x, y) pant_color = pant_color ? BlendRGB(pant_color, xy_color, 0.5) : xy_color - greyscale_colors = pant_color || "#1d1d1d" // black pants always look good + return pant_color || "#1d1d1d" // black pants always look good - var/index = "[key]-[greyscale_config]-[greyscale_colors]" - var/static/list/digitigrade_clothing_icons = list() - var/icon/digitigrade_clothing_icon = digitigrade_clothing_icons[index] - if(!digitigrade_clothing_icon) - var/static/icon/torso_mask - if(!torso_mask) - torso_mask = icon('icons/mob/clothing/under/masking_helpers.dmi', "digi_torso_mask") - var/static/icon/leg_mask - if(!leg_mask) - leg_mask = icon('icons/mob/clothing/under/masking_helpers.dmi', "digi_leg_mask") - - base_icon.Blend(leg_mask, ICON_SUBTRACT) // cuts the legs off - - var/icon/leg_icon = SSgreyscale.GetColoredIconByType(greyscale_config, greyscale_colors) - leg_icon.Blend(torso_mask, ICON_SUBTRACT) // cuts the torso off - - base_icon.Blend(leg_icon, ICON_OVERLAY) // puts the new legs on - - digitigrade_clothing_icon = fcopy_rsc(base_icon) - digitigrade_clothing_icons[index] = digitigrade_clothing_icon - - return icon(digitigrade_clothing_icon) + return ..() #undef LEG_SAMPLE_X_LOWER #undef LEG_SAMPLE_X_UPPER - #undef LEG_SAMPLE_Y_LOWER #undef LEG_SAMPLE_Y_UPPER +// Points to the tip of the left foot +#define SHOE_SAMPLE_X 11 +#define SHOE_SAMPLE_Y 2 + +/obj/item/clothing/shoes/get_general_color(icon/base_icon) + // just grabs the color of the middle of the left foot + return base_icon.GetPixel(SHOE_SAMPLE_X, SHOE_SAMPLE_Y) || "#1d1d1d" + +#undef SHOE_SAMPLE_X +#undef SHOE_SAMPLE_Y + /mob/living/carbon/human/proc/get_overlays_copy(list/unwantedLayers) var/list/out = new for(var/i in 1 to TOTAL_LAYERS) @@ -774,9 +813,9 @@ generate/load female uniform sprites matching all previously decided variables if(!isinhands && is_digi && (supports_variations_flags & CLOTHING_DIGITIGRADE_MASK)) building_icon = wear_digi_version( base_icon = building_icon || icon(file2use, t_state), + item = src, key = "[t_state]-[file2use]-[female_uniform]", - greyscale_config = digitigrade_greyscale_config_worn || greyscale_config_worn, - greyscale_colors = digitigrade_greyscale_colors || greyscale_colors || color, + greyscale_colors = greyscale_colors, ) if(building_icon) standing = mutable_appearance(building_icon, layer = -layer2use) diff --git a/code/modules/unit_tests/screenshots/screenshot_digi_leg_test.png b/code/modules/unit_tests/screenshots/screenshot_digi_leg_test.png index 1ca452de089462485c7e4d6204dd14452995206d..3907b46b8709b4db3a6a2aeaa4b68a3dba36c500 100644 GIT binary patch delta 1314 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 KMNUMnLSTX}=85S5 diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_lizard_ashwalker.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_lizard_ashwalker.png index 3437280e851ba96f3e6d4c02b45ea11513f36f14..9c05ab07e5ffed325061ff64cfa8dbb26caf9096 100644 GIT binary patch delta 1018 zcmV$Y|nm7!W@Uv4JNLB*f#}@BrR} z0Tz1;cVm(PP8r}J$skA)``GvRiw!&jNF$Pnx%Mdd&?5nq!oZQ?7{`@B&nkZcTv?s~ z&){m@!{oIkpO#?_J`x^+t1@+PxXEUCGC$#L?4g9YXWqcNkpbxjz-JzoO4P6yDjC+bv$gs z<10Z*lLb4JhzfyJ6-@nJ#@9je&4L1A#pKn4S0ZRDvCOpPJ~O2D--(qrSpEWz5D zPp7jxpH6dwV1|P`op_<3PtlB4NPM|oXAsQS>&1+}DD2b$6d!5u-Q0is>F9o9{ZJT^ol+=t5)gsZr=`@o>jUUyn8XS6oqV>ytHs zCsvJMza6}Nh=+!Lxo5CQ1beqE@?bLqeo=myO|U?0ZKV%}MMX8xMPxPO(XsQ0nX~m4 z0FN2X-HgZNM%Okc@7;fNME5<0yupBY%uGy)klK?bGJt~lias@sw-`Gbs8#_ zqGJnK9&w8e0_n%BgC6d624(EaR~g8n6sinjArMVqlK?45A$Tl;4*v|3uLwlFhFZz# z@XuH%tWJ%UK#F0rP*#obh@vdov9VNSY_tTg7}`JQx`u^fkI8?>hr{bzD}%S!!vX7= z_YJL@g;~`>YqDVG=zQhIc)zjNxhPX}AxHu77CqlAoP(~iX`4XNY;JcXPzY4WGzp+? z3W1pd`=V|@u(1wuE<&j82{-pAfvFkqbpGE{a1S{k9yOG00#^v&o#(~lH0uE2m12^>{t&Z#WIo o0rF|iFUp{)4_1M&%7lC1Z`C_T088_ep8x;=07*qoM6N<$g4iDF*#H0l delta 995 zcmV<9104L`3C9VLBmr8HB~=Xo2><{D004RbGwu^(k(Pmfs!2paR9J=0SK)fwFbI^1 zM7K`Z(n!+IP2T@CcgRVbFGAk-Z}e;I=HQHx1lJJ$E1MoZ{~ad^hbhK59qjhs@sJYp zA*FO+PRYX~Qv@D~qlXj7DMkFWy8&+I;~^2hr1%X#xTF3N?c2>>Jls}Uh$59ganD%Eh2Mch|I5U5kq(lZ03U-N&1lpegn%QL?n_2_$=8>=285TOHH-l z>c=nnr&(TnhAK-bQl*qds*C4kLHx4h0bW2*5Ra?l0el7nEYBVuYLNv_3UHC)5ERLC zo@e~#0j>Z_MG{$ikA^QZ5&92wSJZv=MN5#XDD?G5k_p~r((4W9(?4B2{B_2q;>AdxA$M?(~X?Nu7aD{Ce&4J zJGwZ3sBxabApQdy!Ak26fe1(RixI(!H$XSRgM0@NtSto|65#Dvhcz%a_5$4~&pR~me`aG^_cz||$VY!P^W+~pza|FUVU4F1yZKr~J6LAzU=CZfX_ zhenu&s~Bt{Ctp0=akAy{hW`yWH2V7FmcR>}M(|e$s}J!o&@cB4Hi_V$mQ5b)X27q; z5Az8&h`poq#jvTU0lJB-VLUpvR?M8eRRCNS&E1X1CM9(vZyu*Na%=GaaX#~$ju;H&T`4fSd*U~FF8~%!o!sayC31k=!3#n;@XB1_zj*YD%bY}2JdgD6ZSKI9|u}hh1t}>XtH4D*nH)|c)xPAxhU&!BS-_W zijl7#&dGM!x=&!Jy2l*}Gy)ScLjtIqM&PEvv8fvn?Cb;8CWQK)a1Eacti$+V^Z%Yg zc*+6ssG;l=ghl|T_|%uK*6(ie0H0$5cx(zHTHm~Wg*SPCJq}v+YrLgt0wF*~Fscm@ zzEN!Q!1Mw)WqjMY`n>lb|`ybANY=At>dA{()h6DThAY9l+0-PR&#cxI7K$KB= RUG4w?002ovPDHLkV1kcS)dK(k diff --git a/icons/mob/clothing/digi_template.dmi b/icons/mob/clothing/digi_template.dmi new file mode 100644 index 0000000000000000000000000000000000000000..319bd875041c7dec5e746d36613068004eaae547 GIT binary patch literal 706 zcmV;z0zLhSP)004jl0{{R3eocQU0000FP)t-sz`($a zlBLVd(R6Ww|NsAb@^TFT0004WQchCV=-0C=2J zR&a84_w-Y6@%7{?OD!tS%+FJ>RWQ*r;NmRLOex6#a*U0*I5Sc+(=$pSoZ^zil2jm5 zDXTQMptv-%M2U+tC9|j)q>qa;ttc@!6~s0~D9i_$OpL;${QQz)f~tx$@>7fBlk;=( zi&9gFR9%`!K)teptDg%v7yzThPjGvb(h~px0mn&1K~!jg?U+fD;~)@4Q{V#_&;caO z0T^@uo96y!>QqEU`J)P3LAw^c$QSbB1DQg@;#ow3AP9o+Ut_(q{`$w222vkeAvD_eyE9$3TjjH=ppds9!LrL94MeWX-zL9V86XaY9$A#EP!x#4y0 z8v1L03Aq-WSFLmnkM%UKGzDm2Ib*{qJW7T26v|xRtmk_WGv0%_E*`TEw}cxY>oVTs zV?9l4Oe^NLga>5p4-J*e_>_{`p7PTNy+Pko9LE|C`rvuPK_5J2)CW%)^+95N>o)9S>GG%#UUp-} z^RaOHZi|CHsBFv@&zE9K^a0@!6TSsA?=Xx!v?JD$w)iiAFCc%?P!I$`5Cq{jF^gF+ zE2GRO{c_Xcjsq-!W7>agXsPo(T5tRDjcRd2=T_m~P%uNKUg}x)x-IesRYRp!F?Tz- zkzOEbUV(X+z0UWhHS+QLiUdIr1VIplkH~d*ykBIuJJ7e?74iFE;NJ!EK8ab~*-78^ ow=9(S)%ch>bb0b|`uaxu7jtMy3bm4fO#lD@07*qoM6N<$g5au2-~a#s literal 0 HcmV?d00001 diff --git a/icons/mob/clothing/under/digi_template.dmi b/icons/mob/clothing/under/digi_template.dmi deleted file mode 100644 index 0c9db80eb1c880eebac6627e659877a5bc2e65a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 586 zcmV-Q0=4~#P)(^b0d!JMQvg8b*k%9#0Bw3ySad{Xb7OL8aCB*J zZU6vyoKseCa&`CgQ*iP1fTLt$1vDPdqJhZ$X|kxj>nvWw!rgoljvxW41IgN1pqx$r!%44`#L)8eF%(gfg_2EK6u5K$kG$%)ii!&v&s2HS` zi!-e#F*g;&HpEa{l3zfq(!Bimq@vWs;*w$#3@1fbN@jXyd`@b5d~RZKHZIl53a)-G z;Lrg8F5_4YYuIUF0002BNklxtI9$N2ooFk#;&sIYHc1hZTD@@NGW zln6pE!iva?5JW10U@f}(5qlB(v2hse#XCJp1P`|+v_}OMd=adAk@6-1pcg5>f;E*; zUe@)T|0l0jtb392vZhv07C1AZ;0Vw+g08iq{lnB9Kpi`@N5h4JLcs+IstW2@1p~7H O0000J delta 441 zcmV;q0Y?6@1I7c87k{7v0{{R3_Mwly0000CP)t-s{{R60|Nj600RI30Kw2_`00001 zbW%=J06^y0W&i*Hn|f4ObVOxyV{&P5bZKvH004NLm5@6Q!Y~j-Yx@*d(j$H?4N4TH zAXl)AytY@^G4d{OeG({X>}Vx`1^@s6!AV3xR9J=WmeCEvAPhuVf{`)+5?QiSR!U+e zr58-nQm)e74}S>17~4pvgt<{c;COJdP4Y6P;)7E#uS;}6q%L6IYO=MgaXtT}nY?cg zDA`+|g2hyRd9^@+j3D$ym=XCR1d)|Mu%@p5hv_^pfzX(=+ z(c<9%Kwq@@0@kd=;#}i-|DU{?vF?i&=jvJ@7x-`_a54m#8o{YGWBiA%JAiR$jlq*Z jR#1|pPI3)YAUXI1mJ#%->3NZH00000NkvXXu0mjfp0C6Z