diff --git a/code/__HELPERS/cmp.dm b/code/__HELPERS/cmp.dm index 8545b04d6f3..467f60e8195 100644 --- a/code/__HELPERS/cmp.dm +++ b/code/__HELPERS/cmp.dm @@ -140,6 +140,10 @@ GLOBAL_VAR_INIT(cmp_field, "name") /proc/cmp_mob_realname_dsc(mob/A,mob/B) return sorttext(A.real_name,B.real_name) +/// Orders bodyparts by their body_part value, ascending. +/proc/cmp_bodypart_by_body_part_asc(obj/item/bodypart/limb_one, obj/item/bodypart/limb_two) + return limb_one.body_part - limb_two.body_part + /// Orders by integrated circuit weight /proc/cmp_port_order_asc(datum/port/compare1, datum/port/compare2) return compare1.order - compare2.order diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm index 5f9cef0db4a..2d41fc6f1c1 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -897,6 +897,37 @@ world alpha_mask.Blend(image_overlay,ICON_OR)//OR so they are lumped together in a nice overlay. return alpha_mask//And now return the mask. +/** + * Helper proc to generate a cutout alpha mask out of an icon. + * + * Why is it a helper if it's so simple? + * + * Because BYOND's documentation is hot garbage and I don't trust anyone to actually + * figure this out on their own without sinking countless hours into it. Yes, it's that + * simple, now enjoy. + * + * But why not use filters? + * + * Filters do not allow for masks that are not the exact same on every dir. An example of a + * need for that can be found in [/proc/generate_left_leg_mask()]. + * + * Arguments: + * * icon_to_mask - The icon file you want to generate an alpha mask out of. + * * icon_state_to_mask - The specific icon_state you want to generate an alpha mask out of. + * + * Returns an `/icon` that is the alpha mask of the provided icon and icon_state. + */ +/proc/generate_icon_alpha_mask(icon_to_mask, icon_state_to_mask) + var/icon/mask_icon = icon(icon_to_mask, icon_state_to_mask) + // I hate the MapColors documentation, so I'll explain what happens here. + // Basically, what we do here is that we invert the mask by using none of the original + // colors, and then the fourth group of number arguments is actually the alpha values of + // each of the original colors, which we multiply by 255 and subtract a value of 255 to the + // result for the matching pixels, while starting with a base color of white everywhere. + mask_icon.MapColors(0,0,0,0, 0,0,0,0, 0,0,0,0, 255,255,255,-255, 1,1,1,1) + return mask_icon + + /mob/proc/AddCamoOverlay(atom/A)//A is the atom which we are using as the overlay. var/icon/opacity_icon = new(A.icon, A.icon_state)//Don't really care for overlays/underlays. //Now we need to culculate overlays+underlays and add them together to form an image for a mask. diff --git a/code/modules/mob/living/carbon/carbon_update_icons.dm b/code/modules/mob/living/carbon/carbon_update_icons.dm index 17f0ba53f33..39867c4a3a2 100644 --- a/code/modules/mob/living/carbon/carbon_update_icons.dm +++ b/code/modules/mob/living/carbon/carbon_update_icons.dm @@ -248,24 +248,40 @@ update_wound_overlays() var/list/needs_update = list() var/limb_count_update = FALSE + var/obj/item/bodypart/l_leg/left_leg + var/obj/item/bodypart/r_leg/right_leg + var/old_left_leg_key for(var/obj/item/bodypart/limb as anything in bodyparts) limb.update_limb(is_creating = update_limb_data) //Update limb actually doesn't do much, get_limb_icon is the cpu eater. + + if(limb.body_zone == BODY_ZONE_R_LEG) + right_leg = limb + continue // Legs are handled separately + var/old_key = icon_render_keys?[limb.body_zone] //Checks the mob's icon render key list for the bodypart icon_render_keys[limb.body_zone] = (limb.is_husked) ? limb.generate_husk_key().Join() : limb.generate_icon_key().Join() //Generates a key for the current bodypart - if(!(icon_render_keys[limb.body_zone] == old_key)) //If the keys match, that means the limb doesn't need to be redrawn + + if(limb.body_zone == BODY_ZONE_L_LEG) + left_leg = limb + old_left_leg_key = old_key + continue // Legs are handled separately + + if(icon_render_keys[limb.body_zone] != old_key) //If the keys match, that means the limb doesn't need to be redrawn needs_update += limb + // Here we handle legs differently, because legs are a mess due to layering code. So we got to process the left leg first. Thanks BYOND. + var/legs_need_redrawn = update_legs(right_leg, left_leg, old_left_leg_key) + var/list/missing_bodyparts = get_missing_limbs() if(((dna ? dna.species.max_bodypart_count : BODYPARTS_DEFAULT_MAXIMUM) - icon_render_keys.len) != missing_bodyparts.len) //Checks to see if the target gained or lost any limbs. limb_count_update = TRUE for(var/missing_limb in missing_bodyparts) icon_render_keys -= missing_limb //Removes dismembered limbs from the key list - if(!needs_update.len && !limb_count_update) + if(!needs_update.len && !limb_count_update && !legs_need_redrawn) return - remove_overlay(BODYPARTS_LAYER) //GENERATE NEW LIMBS var/list/new_limbs = list() @@ -277,12 +293,51 @@ else new_limbs += limb_icon_cache[icon_render_keys[limb.body_zone]] //Pulls existing sprites from the cache + remove_overlay(BODYPARTS_LAYER) + if(new_limbs.len) overlays_standing[BODYPARTS_LAYER] = new_limbs apply_overlay(BODYPARTS_LAYER) +/** + * Here we update the legs separately from the other bodyparts. Thanks BYOND for so little support for dir layering. + * + * Arguments: + * * right_leg - Right leg that we might need to update. Can be null. + * * left_leg - Left leg that we might need to update. Can be null. + * * old_left_leg_key - The icon_key of the left_leg, passed here to avoid having to re-generate it in this proc. + * + * Returns a boolean, TRUE if the legs need to be redrawn, FALSE if they do not need to be redrawn. + * Necessary so that we can ensure that modifications of legs cause overlay updates. + */ +/mob/living/carbon/proc/update_legs(obj/item/bodypart/r_leg/right_leg, obj/item/bodypart/l_leg/left_leg, old_left_leg_key) + var/list/left_leg_icons // yes it's actually a list, bet you didn't expect that, now did you? + var/legs_need_redrawn = FALSE + if(left_leg) + // We regenerate the look of the left leg if it isn't already cached, we don't if not. + if(icon_render_keys[left_leg.body_zone] != old_left_leg_key) + limb_icon_cache[icon_render_keys[left_leg.body_zone]] = left_leg.get_limb_icon() + legs_need_redrawn = TRUE + + left_leg_icons = limb_icon_cache[icon_render_keys[left_leg.body_zone]] + + if(right_leg) + var/old_right_leg_key = icon_render_keys?[right_leg.body_zone] + right_leg.left_leg_mask_key = left_leg?.generate_mask_key().Join() // We generate a new mask key, to see if it changed. + // We regenerate the left_leg_mask in case that it doesn't exist yet. + if(right_leg.left_leg_mask_key && !right_leg.left_leg_mask_cache[right_leg.left_leg_mask_key] && left_leg_icons) + right_leg.left_leg_mask_cache[right_leg.left_leg_mask_key] = generate_left_leg_mask(left_leg_icons[1], right_leg.left_leg_mask_key) + // We generate a new icon_render_key, which also takes into account the left_leg_mask_key so we cache the masked versions of the limbs too. + icon_render_keys[right_leg.body_zone] = right_leg.is_husked ? right_leg.generate_husk_key().Join("-") : right_leg.generate_icon_key().Join() + + if(icon_render_keys[right_leg.body_zone] != old_right_leg_key) + limb_icon_cache[icon_render_keys[right_leg.body_zone]] = right_leg.get_limb_icon() + legs_need_redrawn = TRUE + + return legs_need_redrawn + ///////////////////////// // Limb Icon Cache 2.0 // @@ -313,6 +368,29 @@ return . +/** + * Generates a cache key for masks (mainly only used for right legs now, but perhaps in the future...). + * + * This is exactly like generate_icon_key(), except that it doesn't add `"-[draw_color]"` + * to the returned list under any circumstance. Why? Because it (generate_icon_key()) is + * a proc that gets called a ton and I don't want this to affect its performance. + * + * Returns a list of strings. + */ +/obj/item/bodypart/proc/generate_mask_key() + RETURN_TYPE(/list) + . = list() + if(is_dimorphic) + . += "[limb_gender]" + . += "[limb_id]" + . += "[body_zone]" + for(var/obj/item/organ/external/external_organ as anything in external_organs) + if(!external_organ.can_draw_on_bodypart(owner)) + continue + . += "[external_organ.generate_icon_cache()]" + + return . + ///Generates a cache key specifically for husks /obj/item/bodypart/proc/generate_husk_key() RETURN_TYPE(/list) @@ -346,3 +424,61 @@ . += "-HAIR_HIDDEN" return . + +/obj/item/bodypart/r_leg/generate_icon_key() + RETURN_TYPE(/list) + . = ..() + if(left_leg_mask_key) // We do this so we can cache the versions with and without a mask, for when there's no left leg. + . += "-[left_leg_mask_key]" + + return . + +/** + * This proc serves as a way to ensure that right legs don't overlap above left legs when their dir is WEST on a mob. + * + * It's using the `left_leg_mask_cache` to avoid generating a new mask when unnecessary, which means that there needs to be one + * for the proc to return anything. + * + * Arguments: + * * right_leg_icon_file - The icon file of the right leg overlay we're trying to apply a mask to. + * * right_leg_icon_state - The icon_state of the right leg overlay we're trying to apply a mask to. + * * image_dir - The direction applied to the icon, only meant for when the leg is dropped, so it remains + * facing SOUTH all the time. + * + * Returns the `/image` of the right leg that was masked, or `null` if the mask didn't exist. + */ +/obj/item/bodypart/r_leg/proc/generate_masked_right_leg(right_leg_icon_file, right_leg_icon_state, image_dir) + RETURN_TYPE(/image) + if(!left_leg_mask_cache[left_leg_mask_key] || !right_leg_icon_file || !right_leg_icon_state) + return + + var/icon/right_leg_icon = icon(right_leg_icon_file, right_leg_icon_state) + right_leg_icon.Blend(left_leg_mask_cache[left_leg_mask_key], ICON_MULTIPLY) + return image(right_leg_icon, right_leg_icon_state, layer = -BODYPARTS_LAYER, dir = image_dir) + + +/** + * The proc that handles generating left leg masks at runtime. + * It basically creates an icon that are all white on all dirs except WEST, where there's a cutout + * of the left leg that needed to be masked. + * + * It does /not/ cache the mask itself, and as such, the caching must be done manually (which it is, look up in update_body_parts()). + * + * Arguments: + * * image/left_leg_image - `image` of the left leg that we need to create a mask out of. + * + * Returns the generated left leg mask as an `/icon`, or `null` if no left_leg_image is provided. + */ +/proc/generate_left_leg_mask(image/left_leg_image) + RETURN_TYPE(/icon) + if(!left_leg_image) + return + var/icon/left_leg_alpha_mask = generate_icon_alpha_mask(left_leg_image.icon, left_leg_image.icon_state) + // Right here, we use the crop_mask_icon to single out the WEST sprite of the mask we just created above. + var/icon/crop_mask_icon = icon(icon = 'icons/mob/left_leg_mask_base.dmi', icon_state = "mask_base") + crop_mask_icon.Blend(left_leg_alpha_mask, ICON_MULTIPLY) + // Then, we add (with ICON_OR) that singled-out WEST mask to a template mask that has the NORTH, + // SOUTH and EAST dirs as full white squares, to finish our WEST-directional mask. + var/icon/new_mask_icon = icon(icon = 'icons/mob/left_leg_mask_base.dmi', icon_state = "mask_rest") + new_mask_icon.Blend(crop_mask_icon, ICON_OR) + return new_mask_icon diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm index 459c2a843a4..a1b5ea3b6bd 100644 --- a/code/modules/surgery/bodyparts/_bodyparts.dm +++ b/code/modules/surgery/bodyparts/_bodyparts.dm @@ -714,7 +714,6 @@ var/image/limb = image(layer = -BODYPARTS_LAYER, dir = image_dir) var/image/aux - . += limb if(animal_origin) if(IS_ORGANIC_LIMB(src)) @@ -731,6 +730,7 @@ var/mutable_appearance/limb_em_block = emissive_blocker(limb.icon, limb.icon_state, alpha = limb.alpha) limb_em_block.dir = image_dir limb.overlays += limb_em_block + . += limb return //HUSK SHIIIIT @@ -738,6 +738,7 @@ limb.icon = icon_husk limb.icon_state = "[husk_type]_husk_[body_zone]" icon_exists(limb.icon, limb.icon_state, scream = TRUE) //Prints a stack trace on the first failure of a given iconstate. + . += limb if(aux_zone) //Hand shit aux = image(limb.icon, "[husk_type]_husk_[aux_zone]", -aux_layer, image_dir) . += aux @@ -756,6 +757,16 @@ icon_exists(limb.icon, limb.icon_state, TRUE) //Prints a stack trace on the first failure of a given iconstate. + if(body_zone == BODY_ZONE_R_LEG) + var/obj/item/bodypart/r_leg/leg = src + var/limb_overlays = limb.overlays + var/image/new_limb = leg.generate_masked_right_leg(limb.icon, limb.icon_state, image_dir) + if(new_limb) + limb = new_limb + limb.overlays = limb_overlays + + . += limb + if(aux_zone) //Hand shit aux = image(limb.icon, "[limb_id]_[aux_zone]", -aux_layer, image_dir) . += aux diff --git a/code/modules/surgery/bodyparts/dismemberment.dm b/code/modules/surgery/bodyparts/dismemberment.dm index 9a1814cacb0..b66c8561d92 100644 --- a/code/modules/surgery/bodyparts/dismemberment.dm +++ b/code/modules/surgery/bodyparts/dismemberment.dm @@ -365,6 +365,9 @@ if(can_be_disabled) update_disabled() + // Bodyparts need to be sorted for leg masking to be done properly. It also will allow for some predictable + // behavior within said bodyparts list. We sort it here, as it's the only place we make changes to bodyparts. + new_limb_owner.bodyparts = sort_list(new_limb_owner.bodyparts, /proc/cmp_bodypart_by_body_part_asc) synchronize_bodytypes(new_limb_owner) new_limb_owner.updatehealth() new_limb_owner.update_body() diff --git a/code/modules/surgery/bodyparts/parts.dm b/code/modules/surgery/bodyparts/parts.dm index 181de8e5c5c..cf355f7cfc7 100644 --- a/code/modules/surgery/bodyparts/parts.dm +++ b/code/modules/surgery/bodyparts/parts.dm @@ -366,6 +366,13 @@ px_y = 12 max_stamina_damage = 50 can_be_disabled = TRUE + /// We store this here to generate our icon key more easily. + var/left_leg_mask_key + /// The associated list of all the left leg mask keys associated to their cached left leg masks. + /// It's static, so it's shared between all the left legs there is. Be careful. + /// Why? Both legs share the same layer for rendering, and since we don't want to do redraws on + /// each dir changes, we're doing it with a mask instead, which we cache for efficiency reasons. + var/static/list/left_leg_mask_cache = list() /obj/item/bodypart/r_leg/set_owner(new_owner) diff --git a/icons/mob/left_leg_mask_base.dmi b/icons/mob/left_leg_mask_base.dmi new file mode 100644 index 00000000000..f4d95816363 Binary files /dev/null and b/icons/mob/left_leg_mask_base.dmi differ