Files
MrMelbertandGitHub 62ad8d3520 Allows arbitrary/custom/"unlimited" bodypart overlay layers (#96684)
## About The Pull Request

`EXTERNAL_FRONT`, `EXTERNAL_ADJACENT`, and `EXTERNAL_BEHIND` are no
longer bitflags
Instead they are strings, that correspond to the sprite's icon state's
postfix ie `wings_FRONT` / `wings_ADJ`

Bodypart overlays now define their layers in terms of `postfix` to
`rendering layer`
For example wings are defined as:
```dm
	layers = list(
		EXTERNAL_FRONT = BODY_FRONT_LAYER,
		EXTERNAL_ADJACENT = BODY_ADJ_LAYER,
		EXTERNAL_BEHIND = BODY_BEHIND_LAYER,
	)
```
Which translates to
```dm
	layers = list(
		"FRONT" = 2.5,
		"ADJ" = 22.9,
		"BEHIND" = 32.2,
	)
```
## Why It's Good For The Game

What does this mean?

One, you are no longer constricted to the three existing layers when
adding a bodypart overlay. You can decide to put it on whatever layer
you need...
```dm
	layers = list(
		"FRONT" = 2.4, // I specifically need this to render above other front overlays!
	)
```

Two, you can easily add a new layer without needing to mess with core
bodypart code at all
```dm
	layers = list(
		"FRONT_HIGHER" = 2.4, // I need a special layer
		"FRONT" = 2.3, 
	)
```

Three, you don't have to use the `EXTERNAL_FRONT` `EXTERNAL_ADJACENT`
etc. at all if you don't want to. You can organize your layers however
you want...
```dm
	layers = list(
		// I can make my icon states `wings_top` and `wings_bottom` instead of `wings_FRONT` and `wings_BEHIND` to make it easier to parse
		"top" = BODY_ADJ_LAYER, 
		"bottom" = BODY_BEHIND_LAYER, 
	)
```

Ultimately, this makes it a ton easier to work with bodypart overlays,
as you no longer need to learn what FRONT/ADJ/BEHIND means. You can just
add your sprites and define your layers and you're done

## Changelog

🆑 Melbert
refactor: Surprise, a follow up refactor to species part rendering,
report any oddities with them (wings/snouts/tails/etc)
/🆑
2026-07-02 11:41:31 +02:00

148 lines
4.6 KiB
Plaintext

/**
* Face decal component
*
* For when you have some dirt on your face
*/
/datum/component/face_decal
dupe_mode = COMPONENT_DUPE_HIGHLANDER
/// Overlay we use for non-carbon mobs
var/mutable_appearance/normal_overlay
/// Bodypart overlay we use for carbon mobs
var/datum/bodypart_overlay/simple/bodypart_overlay
/// Cached head for carbons, to ensure proper removal of our overlay
var/obj/item/bodypart/my_head
/// Base icon state we use for the effect
var/icon_state
/// Layers for the bodypart_overlay to draw on
var/layers
/// Color that the overlay is modified by
var/color
/datum/component/face_decal/Initialize(icon_state, layers, color)
src.icon_state = icon_state
src.layers = layers
src.color = color
/datum/component/face_decal/Destroy(force)
. = ..()
normal_overlay = null
my_head = null
QDEL_NULL(bodypart_overlay)
/datum/component/face_decal/RegisterWithParent()
if(iscarbon(parent))
var/mob/living/carbon/human/carbon_parent = parent
my_head = carbon_parent.get_bodypart(BODY_ZONE_HEAD)
if(!my_head) //just to be sure
qdel(src)
return
bodypart_overlay = new()
bodypart_overlay.set_layers(layers)
if(carbon_parent.bodyshape & BODYSHAPE_SNOUTED) //stupid, but external organ bodytypes are not stored on the limb
bodypart_overlay.icon_state = "[icon_state]_lizard"
else if(my_head.bodyshape & BODYSHAPE_MONKEY)
bodypart_overlay.icon_state = "[icon_state]_monkey"
else
bodypart_overlay.icon_state = "[icon_state]_human"
if (!isnull(color))
bodypart_overlay.draw_color = color
my_head.add_bodypart_overlay(bodypart_overlay)
RegisterSignals(my_head, list(COMSIG_BODYPART_REMOVED, COMSIG_QDELETING), PROC_REF(lost_head))
else
normal_overlay = get_normal_overlay()
normal_overlay.color = color
RegisterSignals(parent, list(
COMSIG_COMPONENT_CLEAN_ACT,
COMSIG_COMPONENT_CLEAN_FACE_ACT),
PROC_REF(clean_up)
)
if (!isnull(normal_overlay))
if (!isnull(color))
normal_overlay.color = color
var/atom/atom_parent = parent
RegisterSignal(atom_parent, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(update_overlays))
atom_parent.update_appearance()
/datum/component/face_decal/proc/get_normal_overlay()
return
/datum/component/face_decal/UnregisterFromParent()
UnregisterSignal(parent, list(
COMSIG_COMPONENT_CLEAN_ACT,
COMSIG_COMPONENT_CLEAN_FACE_ACT))
if(my_head)
if(bodypart_overlay)
my_head.remove_bodypart_overlay(bodypart_overlay)
QDEL_NULL(bodypart_overlay)
UnregisterSignal(my_head, list(COMSIG_BODYPART_REMOVED, COMSIG_QDELETING))
my_head = null
if(normal_overlay)
var/atom/atom_parent = parent
UnregisterSignal(atom_parent, COMSIG_ATOM_UPDATE_OVERLAYS)
atom_parent.update_appearance()
normal_overlay = null
///Callback to remove our decal
/datum/component/face_decal/proc/clean_up(datum/source, clean_types)
SIGNAL_HANDLER
if(!(clean_types & CLEAN_TYPE_BLOOD))
return NONE
qdel(src)
return COMPONENT_CLEANED|COMPONENT_CLEANED_GAIN_XP
/// Ensures normal_overlay overlay in case the mob is not a carbon
/datum/component/face_decal/proc/update_overlays(atom/parent_atom, list/overlays)
SIGNAL_HANDLER
if(normal_overlay)
overlays += normal_overlay
/// Removes the decal when the head gets dismembered
/datum/component/face_decal/proc/lost_head(obj/item/bodypart/source, mob/living/carbon/owner, dismembered)
SIGNAL_HANDLER
qdel(src)
/// splat subtype, handling signals and mood logic
GLOBAL_LIST_INIT(splattable, zebra_typecacheof(list(
/mob/living/carbon/human = "human",
/mob/living/basic/pet/dog/corgi = "corgi",
/mob/living/silicon/ai = "ai",
)))
/datum/component/face_decal/splat
///The mood_event that we add
var/mood_event_type
/datum/component/face_decal/splat/Initialize(icon_state, layers, color, memory_type = /datum/memory/witnessed_creampie, mood_event_type = /datum/mood_event/creampie)
if(!is_type_in_typecache(parent, GLOB.splattable))
return COMPONENT_INCOMPATIBLE
. = ..()
SEND_SIGNAL(parent, COMSIG_MOB_HIT_BY_SPLAT, src)
add_memory_in_range(parent, 7, memory_type, protagonist = parent)
src.mood_event_type = mood_event_type
/datum/component/face_decal/splat/get_normal_overlay()
return mutable_appearance('icons/mob/effects/face_decal.dmi', "[icon_state]_[is_type_in_list(parent, GLOB.splattable, zebra = TRUE)]")
/datum/component/face_decal/splat/RegisterWithParent()
. = ..()
if(iscarbon(parent))
var/mob/living/carbon/human/carbon_parent = parent
carbon_parent.add_mood_event("splat", mood_event_type)
/datum/component/face_decal/splat/UnregisterFromParent()
. = ..()
if(iscarbon(parent))
var/mob/living/carbon/carbon_parent = parent
carbon_parent.clear_mood_event("splat")