From 01f686474846fa8b08ed23526bdcc8068b0f705c Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:56:14 +0200 Subject: [PATCH] Refactors atom HUD positioning on mobs, fixes some inlay overlays on wide humans (#96682) ## About The Pull Request This PR moves out hud offsets into a separate proc on atom level, which lets mobs override it as to control their atom HUD positioning. This fixes weird hud offsets on mobs who are located in files larger than their own icon, or have a very offset but small/detached detail on their sprite (blood-drunk miner, raptors, megacarps) This also lets humans adjust their HUDs based on their height, which should prevent hud clipping for spacers. dreamseeker_iBapCH9y5a Main reason behind this change is to allow humans to override their sprite width/height according to their bodypart overlays, which fixes immerse and brimdust sac inlaid overlays being shorter/thinner than expected. Before & after: dreamseeker_5l78R30uLI dreamseeker_9hX6FXLvK9 These values are cached and only updated on ``update_body_parts`` as to save on a bit of performance. ## Why It's Good For The Game Fixes jank visuals ## Changelog :cl: fix: Brimdust sac and fluid immersion overlays no longer look weird on humans with extra-wide or tall bodyparts or organs fix: Some mobs now have more sensible health HUD positions fix: Health and security HUD now scales with player height, no more huds clipping into spacer hair /:cl: --- code/datums/elements/immerse.dm | 13 ++++- code/game/data_huds.dm | 24 +++++++-- .../equipment/monster_organs/brimdust_sac.dm | 6 +++ .../blood_drunk_miner/_blood_drunk_miner.dm | 3 ++ .../basic/lavaland/goldgrub/goldgrub.dm | 3 ++ .../living/basic/lavaland/goliath/goliath.dm | 3 ++ .../living/basic/lavaland/raptor/_raptor.dm | 3 ++ .../living/basic/space_fauna/carp/megacarp.dm | 3 ++ .../mob/living/carbon/human/human_defines.dm | 9 ++++ .../mob/living/carbon/human/human_helpers.dm | 1 + .../living/carbon/human/human_update_icons.dm | 49 +++++++++++++++++++ .../modules/mob/living/living_update_icons.dm | 9 ++-- 12 files changed, 119 insertions(+), 7 deletions(-) diff --git a/code/datums/elements/immerse.dm b/code/datums/elements/immerse.dm index b23623d51b0..f0fcd06a6d5 100644 --- a/code/datums/elements/immerse.dm +++ b/code/datums/elements/immerse.dm @@ -245,8 +245,17 @@ GLOBAL_LIST_INIT(immerse_ignored_movable, typecacheof(list( // This determines if the overlay should cover the entire surface of the object or not var/layer_to_check = IS_TOPDOWN_PLANE(movable.plane) ? TOPDOWN_WATER_LEVEL_LAYER : WATER_LEVEL_LAYER var/is_below_water = (movable.layer < layer_to_check) ? "underwater-" : "" + var/x_offset = 0 + var/y_offset = 0 + var/movable_width = movable.get_cached_width() + if (ishuman(movable)) + var/mob/living/carbon/human/as_human = movable + if (as_human.cached_body_min_x_offset && movable_width > ICON_SIZE_X) + x_offset = as_human.cached_body_min_x_offset + if (as_human.cached_body_min_y_offset && movable.get_cached_height() > ICON_SIZE_Y) + y_offset = as_human.cached_body_min_y_offset // Tall mobs still only get covered to their feet, unless they're offset down - var/mutable_appearance/immerse_mask = generate_immerse_mask(movable.get_cached_width(), max(ICON_SIZE_Y - movable.pixel_z, ICON_SIZE_Y), is_below_water) + var/mutable_appearance/immerse_mask = generate_immerse_mask(movable_width, max(ICON_SIZE_Y - movable.pixel_z - y_offset, ICON_SIZE_Y), is_below_water) if (!immerse_mask) return var/atom/movable/immerse_mask/effect_relay = generated_visual_overlays[movable] @@ -256,6 +265,8 @@ GLOBAL_LIST_INIT(immerse_ignored_movable, typecacheof(list( generated_visual_overlays[movable] = effect_relay var/mutable_appearance/mask_copy = new(immerse_mask) effect_relay.appearance = mask_copy + effect_relay.pixel_w += x_offset + effect_relay.pixel_z += y_offset effect_relay.render_target = "*immerse_[REF(movable)]" SEND_SIGNAL(movable, COMSIG_MOVABLE_EDIT_UNIQUE_IMMERSE_OVERLAY, effect_relay) // Should always render above any other filters that could be adding visuals diff --git a/code/game/data_huds.dm b/code/game/data_huds.dm index a38ba684758..4e0bdb2873a 100644 --- a/code/game/data_huds.dm +++ b/code/game/data_huds.dm @@ -497,6 +497,18 @@ Diagnostic HUDs! var/list/dimensions = get_icon_dimensions(icon) return dimensions[CACHED_HEIGHT_INDEX] +/image/proc/get_cached_width() + if (isnull(icon)) + return 0 + var/list/dimensions = get_icon_dimensions(icon) + return dimensions[CACHED_WIDTH_INDEX] + +/image/proc/get_cached_height() + if (isnull(icon)) + return 0 + var/list/dimensions = get_icon_dimensions(icon) + return dimensions[CACHED_HEIGHT_INDEX] + #undef CACHED_WIDTH_INDEX #undef CACHED_HEIGHT_INDEX @@ -522,12 +534,18 @@ Diagnostic HUDs! ) return max(scale_list) - min(scale_list) +/atom/proc/get_hud_x_offset() + return -(get_cached_width() - ICON_SIZE_X) / 2 + +/atom/proc/get_hud_y_offset() + return get_cached_height() - ICON_SIZE_Y + /atom/proc/adjust_hud_position(image/holder, animate_time = null) if (animate_time) - animate(holder, pixel_w = -(get_cached_width() - ICON_SIZE_X) / 2, pixel_z = get_cached_height() - ICON_SIZE_Y, time = animate_time) + animate(holder, pixel_w = get_hud_x_offset(), pixel_z = get_hud_y_offset(), time = animate_time) return - holder.pixel_w = -(get_cached_width() - ICON_SIZE_X) / 2 - holder.pixel_z = get_cached_height() - ICON_SIZE_Y + holder.pixel_w = get_hud_x_offset() + holder.pixel_z = get_hud_y_offset() /atom/proc/set_hud_image_state(hud_type, hud_state, x_offset = 0, y_offset = 0) if (!hud_list) // Still initializing diff --git a/code/modules/mining/equipment/monster_organs/brimdust_sac.dm b/code/modules/mining/equipment/monster_organs/brimdust_sac.dm index 9c741ae0680..2c047792c8e 100644 --- a/code/modules/mining/equipment/monster_organs/brimdust_sac.dm +++ b/code/modules/mining/equipment/monster_organs/brimdust_sac.dm @@ -169,6 +169,12 @@ dust_overlay.alpha = stacks * BRIMDUST_ALPHA_PER_STACK dust_overlay.color = COLOR_RED_LIGHT dust_overlay.blend_mode = BLEND_INSET_OVERLAY + if (ishuman(owner)) + var/mob/living/carbon/human/as_human = owner + if (as_human.cached_body_min_x_offset && target_width > ICON_SIZE_X) + dust_overlay.pixel_w = as_human.cached_body_min_x_offset + if (as_human.cached_body_min_y_offset && target_height > ICON_SIZE_Y) + dust_overlay.pixel_z = as_human.cached_body_min_y_offset ADD_KEEP_TOGETHER(owner, REF(src)) owner.add_overlay(dust_overlay) var/obj/effect/holder = owner.add_shared_particles(/particles/brimdust, "brimdust_coating-[owner.base_pixel_w]") diff --git a/code/modules/mob/living/basic/boss/blood_drunk_miner/_blood_drunk_miner.dm b/code/modules/mob/living/basic/boss/blood_drunk_miner/_blood_drunk_miner.dm index 0e9e734a21d..a251db94200 100644 --- a/code/modules/mob/living/basic/boss/blood_drunk_miner/_blood_drunk_miner.dm +++ b/code/modules/mob/living/basic/boss/blood_drunk_miner/_blood_drunk_miner.dm @@ -86,6 +86,9 @@ Difficulty: Medium QDEL_NULL(miner_saw) return ..() +/mob/living/basic/boss/blood_drunk_miner/get_hud_x_offset() + return 0 + /// Returns a list of innate actions for the blood-drunk miner. /mob/living/basic/boss/blood_drunk_miner/proc/get_innate_actions() var/list/innate_abilities = list( diff --git a/code/modules/mob/living/basic/lavaland/goldgrub/goldgrub.dm b/code/modules/mob/living/basic/lavaland/goldgrub/goldgrub.dm index b43f2149893..7422e705104 100644 --- a/code/modules/mob/living/basic/lavaland/goldgrub/goldgrub.dm +++ b/code/modules/mob/living/basic/lavaland/goldgrub/goldgrub.dm @@ -87,6 +87,9 @@ if(has_emissive) update_appearance(UPDATE_OVERLAYS) +/mob/living/basic/mining/goldgrub/get_hud_x_offset() + return -4 + /mob/living/basic/mining/goldgrub/proc/block_bullets(datum/source, obj/projectile/hitting_projectile) SIGNAL_HANDLER diff --git a/code/modules/mob/living/basic/lavaland/goliath/goliath.dm b/code/modules/mob/living/basic/lavaland/goliath/goliath.dm index 82eb6de7a04..972127977ed 100644 --- a/code/modules/mob/living/basic/lavaland/goliath/goliath.dm +++ b/code/modules/mob/living/basic/lavaland/goliath/goliath.dm @@ -87,6 +87,9 @@ QDEL_NULL(tentacle_line) return ..() +/mob/living/basic/mining/goliath/get_hud_x_offset() + return -4 + /mob/living/basic/mining/goliath/examine(mob/user) . = ..() if (saddled) diff --git a/code/modules/mob/living/basic/lavaland/raptor/_raptor.dm b/code/modules/mob/living/basic/lavaland/raptor/_raptor.dm index 610682fd7a3..59a3727c38f 100644 --- a/code/modules/mob/living/basic/lavaland/raptor/_raptor.dm +++ b/code/modules/mob/living/basic/lavaland/raptor/_raptor.dm @@ -139,6 +139,9 @@ GLOBAL_LIST_EMPTY(raptor_population) return return ..() +/mob/living/basic/raptor/get_hud_x_offset() + return -4 + /mob/living/basic/raptor/examine(mob/user) . = ..() if (stat == DEAD) diff --git a/code/modules/mob/living/basic/space_fauna/carp/megacarp.dm b/code/modules/mob/living/basic/space_fauna/carp/megacarp.dm index d82120a37e0..c86250c5f90 100644 --- a/code/modules/mob/living/basic/space_fauna/carp/megacarp.dm +++ b/code/modules/mob/living/basic/space_fauna/carp/megacarp.dm @@ -33,3 +33,6 @@ melee_damage_upper += rand(10,20) maxHealth += rand(30,60) health = maxHealth + +/mob/living/basic/carp/mega/get_hud_x_offset() + return -5 diff --git a/code/modules/mob/living/carbon/human/human_defines.dm b/code/modules/mob/living/carbon/human/human_defines.dm index 67e6185d28e..091ac22cf80 100644 --- a/code/modules/mob/living/carbon/human/human_defines.dm +++ b/code/modules/mob/living/carbon/human/human_defines.dm @@ -93,3 +93,12 @@ /// Tracks how long in seconds we've been in a low pressure environment VAR_FINAL/seconds_in_low_pressure = 0 + + /// Combined width of our body sprite + VAR_PRIVATE/cached_body_width = ICON_SIZE_X + /// Combined height of our body sprite + VAR_PRIVATE/cached_body_height = ICON_SIZE_Y + /// Leftmost offset of our overlays + var/cached_body_min_x_offset = 0 + /// Rightmost offset of our overlays + var/cached_body_min_y_offset = 0 diff --git a/code/modules/mob/living/carbon/human/human_helpers.dm b/code/modules/mob/living/carbon/human/human_helpers.dm index 523c3656802..c0e5b21b233 100644 --- a/code/modules/mob/living/carbon/human/human_helpers.dm +++ b/code/modules/mob/living/carbon/human/human_helpers.dm @@ -286,6 +286,7 @@ mob_height = dna?.species?.update_species_heights(src) || base_mob_height if(old_height != mob_height) regenerate_icons() + readjust_atom_huds() SEND_SIGNAL(src, COMSIG_HUMAN_HEIGHT_UPDATED, old_height) /** 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 55fbb4cec72..39506b1fb4c 100644 --- a/code/modules/mob/living/carbon/human/human_update_icons.dm +++ b/code/modules/mob/living/carbon/human/human_update_icons.dm @@ -946,3 +946,52 @@ generate/load female uniform sprites matching all previously decided variables return appearance #undef RESOLVE_ICON_STATE + +// Wide organs or bodyparts shouldn't offset human HUD directly +/mob/living/carbon/human/get_hud_x_offset() + return 0 + +// But they are affected by height +/mob/living/carbon/human/get_hud_y_offset() + return GLOB.human_heights_to_offsets[mob_height]["[UPPER_BODY]"] + +/mob/living/carbon/human/get_cached_width() + return cached_body_width + +/mob/living/carbon/human/get_cached_height() + return cached_body_height + +#define SUB_OVERLAY_X_INDEX 1 +#define SUB_OVERLAY_Y_INDEX 1 + +/mob/living/carbon/human/update_body_parts(update_limb_data) + . = ..() + if (!.) + return + cached_body_width = ICON_SIZE_X + cached_body_height = ICON_SIZE_Y + var/list/bodypart_overlays = overlays_standing[BODYPARTS_LAYER] + if (!length(bodypart_overlays)) + return + var/list/parsed_overlays = bodypart_overlays.Copy() + var/i = 1 + while (i <= length(parsed_overlays)) + var/mutable_appearance/overlay = parsed_overlays[i] + if (!isimage(overlay)) // Malformed overlays, etc + i += 1 + continue + var/overlay_x = overlay.pixel_x + overlay.pixel_w + var/overlay_y = overlay.pixel_y + overlay.pixel_z + if (!isnull(parsed_overlays[overlay])) // Nested overlay + overlay_x += parsed_overlays[overlay][SUB_OVERLAY_X_INDEX] + overlay_y += parsed_overlays[overlay][SUB_OVERLAY_Y_INDEX] + cached_body_width = max(cached_body_width, overlay.get_cached_width()) + cached_body_height = max(cached_body_height, overlay.get_cached_height()) + cached_body_min_x_offset = min(cached_body_min_x_offset, overlay_x) + cached_body_min_y_offset = min(cached_body_min_y_offset, overlay_y) + for (var/sub_overlay in overlay.overlays) + parsed_overlays[sub_overlay] = list(overlay_x, overlay_y) + i += 1 + +#undef SUB_OVERLAY_X_INDEX +#undef SUB_OVERLAY_Y_INDEX diff --git a/code/modules/mob/living/living_update_icons.dm b/code/modules/mob/living/living_update_icons.dm index 274a5a4f101..cd328b3a801 100644 --- a/code/modules/mob/living/living_update_icons.dm +++ b/code/modules/mob/living/living_update_icons.dm @@ -52,14 +52,17 @@ var/is_opposite_angle = REVERSE_ANGLE(lying_angle) == lying_prev var/animate_time = is_opposite_angle ? 0 : UPDATE_TRANSFORM_ANIMATION_TIME animate(src, transform = ntransform, time = animate_time, dir = final_dir, easing = SINE_EASING) + readjust_atom_huds(animate_time) + + SEND_SIGNAL(src, COMSIG_LIVING_POST_UPDATE_TRANSFORM, resize, lying_angle, is_opposite_angle) + return TRUE + +/mob/living/proc/readjust_atom_huds(animate_time = null) for (var/hud_key in hud_list) var/image/hud_image = hud_list[hud_key] if (istype(hud_image)) adjust_hud_position(hud_image, animate_time = animate_time) - SEND_SIGNAL(src, COMSIG_LIVING_POST_UPDATE_TRANSFORM, resize, lying_angle, is_opposite_angle) - return TRUE - /// Calculates how far vertically the mob's transform should translate according to its size (1 being "default") /mob/living/proc/get_transform_translation_size(value) return (value - 1) * 16