From 0f3d4596f309415c2a161a0e0b2bfdd7952be7f9 Mon Sep 17 00:00:00 2001 From: FlufflesTheDog Date: Tue, 28 Oct 2025 16:20:58 -0700 Subject: [PATCH] Dullahan fixes (#93365) ## About The Pull Request Fixes up some of the blaring problems with the woefully unloved dullahans, making them at least playable in time for halloween. List of individual fixes in the changelog, but notable code changes include that visible/audible messages can (currently exists for dullahan heads and holopad projections) now be proxied, so that detached cameras can optionally relay TTS, emotes, and other messages to the proper client. ## Why It's Good For The Game Its spooky month! ## Changelog :cl: fix: Dullahans spawn with their intended organs fix: Dullahans can hear/see emotes fix: Dullahan heads properly have preferences applied to them fix: Dullahan heads no longer appear eyeless fix: Dullahans can hear things from their head regardless of if their body is nearby fix: Dullahans can hear TTS messages fix: Dullahans have a TTS voice qol: Dullahans have a head in the character preview, for the sake of easier customization /:cl: --- code/__HELPERS/spatial_info.dm | 10 +++ code/controllers/subsystem/tts.dm | 9 +- .../quirks/negative_quirks/blindness.dm | 5 ++ .../quirks/negative_quirks/brain_problems.dm | 5 ++ .../quirks/negative_quirks/fluoride_stare.dm | 5 ++ .../quirks/negative_quirks/nearsighted.dm | 5 ++ code/modules/mob/living/carbon/carbon.dm | 8 +- .../carbon/human/species_types/dullahan.dm | 83 +++++++++++-------- code/modules/mob/mob.dm | 55 +++++++----- .../surgery/bodyparts/head_hair_and_lips.dm | 45 +++++++--- 10 files changed, 159 insertions(+), 71 deletions(-) diff --git a/code/__HELPERS/spatial_info.dm b/code/__HELPERS/spatial_info.dm index 4cc71bb091b..2f3ae753327 100644 --- a/code/__HELPERS/spatial_info.dm +++ b/code/__HELPERS/spatial_info.dm @@ -214,6 +214,16 @@ for(var/obj/item/radio/radio as anything in radios) . |= get_hearers_in_LOS(radio.canhear_range, radio) +/// A filter to be applied to get_hearers_in_x, that removes any non-mob hearers, converting them to their relevant mob if one exists (such as dullahan heads). +/// Modifies input list. +/proc/mob_only_listeners(list/atom/movable/hearers) + RETURN_TYPE(/list/mob) + for(var/hearer_index in 1 to hearers.len) + var/atom/movable/hearer = hearers[hearer_index] + hearers[hearer_index] = hearer.get_listening_mob() + list_clear_nulls(hearers) + return hearers + //Used when converting pixels to tiles to make them accurate #define OFFSET_X (0.5 / ICON_SIZE_X) #define OFFSET_Y (0.5 / ICON_SIZE_Y) diff --git a/code/controllers/subsystem/tts.dm b/code/controllers/subsystem/tts.dm index 1106318e021..5bd65b63e19 100644 --- a/code/controllers/subsystem/tts.dm +++ b/code/controllers/subsystem/tts.dm @@ -105,7 +105,10 @@ SUBSYSTEM_DEF(tts) return var/channel = SSsounds.random_available_channel() - for(var/mob/listening_mob in listeners | SSmobs.dead_players_by_zlevel[turf_source.z])//observers always hear through walls + for(var/atom/movable/hearer in listeners | SSmobs.dead_players_by_zlevel[turf_source.z])//observers always hear through walls + var/mob/listening_mob = hearer.get_listening_mob() + if(isnull(listening_mob)) + continue if(QDELING(listening_mob)) stack_trace("TTS tried to play a sound to a deleted mob.") continue @@ -115,13 +118,13 @@ SUBSYSTEM_DEF(tts) if(volume_modifier == 0 || (tts_pref == TTS_SOUND_OFF)) continue - var/sound_volume = ((listening_mob == target)? 60 : 85) + volume_offset + var/sound_volume = ((hearer == target)? 60 : 85) + volume_offset sound_volume = sound_volume*volume_modifier var/datum/language_holder/holder = listening_mob.get_language_holder() var/audio_to_use = (tts_pref == TTS_SOUND_BLIPS) ? audio_blips : audio if(!holder.has_language(language)) continue - if(get_dist(listening_mob, turf_source) <= range) + if(get_dist(hearer, turf_source) <= range) listening_mob.playsound_local( turf_source, vol = sound_volume, diff --git a/code/datums/quirks/negative_quirks/blindness.dm b/code/datums/quirks/negative_quirks/blindness.dm index 8dafede1fe1..42517e05344 100644 --- a/code/datums/quirks/negative_quirks/blindness.dm +++ b/code/datums/quirks/negative_quirks/blindness.dm @@ -20,6 +20,11 @@ blindfold.colored_before = TRUE give_item_to_holder(blindfold, list(LOCATION_EYES, LOCATION_HANDS)) +/datum/quirk/item_quirk/blindness/is_species_appropriate(datum/species/mob_species) + if(ispath(mob_species, /datum/species/dullahan)) + return FALSE + return ..() + /datum/quirk/item_quirk/blindness/add(client/client_source) quirk_holder.become_blind(QUIRK_TRAIT) diff --git a/code/datums/quirks/negative_quirks/brain_problems.dm b/code/datums/quirks/negative_quirks/brain_problems.dm index 8bafa07b9dd..4ecc9c0713a 100644 --- a/code/datums/quirks/negative_quirks/brain_problems.dm +++ b/code/datums/quirks/negative_quirks/brain_problems.dm @@ -29,5 +29,10 @@ notify_player = TRUE, ) +/datum/quirk/item_quirk/brainproblems/is_species_appropriate(datum/species/mob_species) + if(ispath(mob_species, /datum/species/dullahan)) + return FALSE + return ..() + /datum/quirk/item_quirk/brainproblems/process(seconds_per_tick) quirk_holder.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.2 * seconds_per_tick) diff --git a/code/datums/quirks/negative_quirks/fluoride_stare.dm b/code/datums/quirks/negative_quirks/fluoride_stare.dm index 97b9c75f3e9..c14efee3014 100644 --- a/code/datums/quirks/negative_quirks/fluoride_stare.dm +++ b/code/datums/quirks/negative_quirks/fluoride_stare.dm @@ -10,6 +10,11 @@ quirk_flags = QUIRK_HUMAN_ONLY mail_goodies = list(/obj/item/reagent_containers/cup/bottle/salglu_solution, /obj/item/light/bulb) +/datum/quirk/item_quirk/fluoride_stare/is_species_appropriate(datum/species/mob_species) + if(ispath(mob_species, /datum/species/dullahan)) + return FALSE + return ..() + /datum/quirk/item_quirk/fluoride_stare/add_unique(client/client_source) var/obj/item/reagent_containers/cup/bottle/salglu_solution/saline = new(get_turf(quirk_holder)) give_item_to_holder(saline, list( diff --git a/code/datums/quirks/negative_quirks/nearsighted.dm b/code/datums/quirks/negative_quirks/nearsighted.dm index a9e945861d7..3c0b8f78c53 100644 --- a/code/datums/quirks/negative_quirks/nearsighted.dm +++ b/code/datums/quirks/negative_quirks/nearsighted.dm @@ -14,6 +14,11 @@ associated_typepath = /datum/quirk/item_quirk/nearsighted customization_options = list(/datum/preference/choiced/glasses) +/datum/quirk/item_quirk/nearsighted/is_species_appropriate(datum/species/mob_species) + if(ispath(mob_species, /datum/species/dullahan)) + return FALSE + return ..() + /datum/quirk/item_quirk/nearsighted/add_unique(client/client_source) var/glasses_name = client_source?.prefs.read_preference(/datum/preference/choiced/glasses) || "Regular" var/obj/item/clothing/glasses/glasses_type diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index 18943c0b6ee..c4eff9208cc 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -814,7 +814,10 @@ return ..() /mob/living/carbon/can_be_revived() - if(!get_organ_by_type(/obj/item/organ/brain) && (!IS_CHANGELING(src)) || HAS_TRAIT(src, TRAIT_HUSK)) + if(HAS_TRAIT(src, TRAIT_HUSK)) + return FALSE + var/brainless_creature = IS_CHANGELING(src) || isdullahan(src) + if(!brainless_creature && !get_organ_by_type(/obj/item/organ/brain)) return FALSE return ..() @@ -842,6 +845,9 @@ return DEFIB_FAIL_FAILING_HEART var/obj/item/organ/brain/current_brain = get_organ_by_type(/obj/item/organ/brain) + if(isdullahan(src)) + var/datum/species/dullahan/dullahan_species = src.dna.species + current_brain = locate() in dullahan_species.my_head.loc if (QDELETED(current_brain)) return DEFIB_FAIL_NO_BRAIN diff --git a/code/modules/mob/living/carbon/human/species_types/dullahan.dm b/code/modules/mob/living/carbon/human/species_types/dullahan.dm index 7375c259243..e499d768eff 100644 --- a/code/modules/mob/living/carbon/human/species_types/dullahan.dm +++ b/code/modules/mob/living/carbon/human/species_types/dullahan.dm @@ -44,6 +44,9 @@ RegisterSignal(human, COMSIG_CARBON_ATTACH_LIMB, PROC_REF(on_gained_part)) var/obj/item/bodypart/head/head = human.get_bodypart(BODY_ZONE_HEAD) + head.speech_span = null + if(isnull(human.drop_location())) + return head?.drop_limb() if(QDELETED(head)) //drop_limb() deletes the limb if no drop location exists and character setup dummies are located in nullspace. return @@ -54,16 +57,20 @@ var/obj/item/organ/eyes/eyes = new /obj/item/organ/eyes(head) eyes.eye_color_left = human.eye_color_left eyes.eye_color_right = human.eye_color_right - eyes.bodypart_insert(my_head) + eyes.bodypart_insert(head) human.update_body() + head.update_limb() head.update_icon_dropped() RegisterSignal(head, COMSIG_QDELETING, PROC_REF(on_head_destroyed)) + RegisterSignal(my_head, COMSIG_MOVABLE_MOVED, PROC_REF(on_relay_move)) /// If we gained a new body part, it had better not be a head /datum/species/dullahan/proc/on_gained_part(mob/living/carbon/human/dullahan, obj/item/bodypart/part) SIGNAL_HANDLER - if (part.body_zone != BODY_ZONE_HEAD) + if(part.body_zone != BODY_ZONE_HEAD) return + if(isnull(dullahan.drop_location())) + return // don't gib nullspace my_head = null dullahan.investigate_log("has been gibbed by having an illegal head put on [dullahan.p_their()] shoulders.", INVESTIGATE_DEATHS) dullahan.gib(DROP_ALL_REMAINS) // Yeah so giving them a head on their body is really not a good idea, so their original head will remain but uh, good luck fixing it after that. @@ -72,12 +79,20 @@ /datum/species/dullahan/proc/on_head_destroyed() SIGNAL_HANDLER var/mob/living/human = my_head?.owner - if (QDELETED(human)) + if(QDELETED(human)) return // guess we already died my_head = null human.investigate_log("has been gibbed by the loss of [human.p_their()] head.", INVESTIGATE_DEATHS) human.gib(DROP_ALL_REMAINS) +/// Head was butchered? No more dullahan +/datum/species/dullahan/proc/on_relay_move() + SIGNAL_HANDLER + if(QDELETED(my_head?.owner) || !isdullahan(my_head?.owner)) + return + my_head.owner.gib(DROP_ALL_REMAINS) + QDEL_NULL(my_head) + /datum/species/dullahan/on_species_loss(mob/living/carbon/human/human) . = ..() if(my_head) @@ -169,7 +184,8 @@ organ_flags = ORGAN_ORGANIC //not vital /obj/item/organ/tongue/dullahan - zone = "abstract" + zone = BODY_ZONE_CHEST + organ_flags = parent_type::organ_flags | ORGAN_UNREMOVABLE modifies_speech = TRUE /obj/item/organ/tongue/dullahan/handle_speech(datum/source, list/speech_args) @@ -179,17 +195,23 @@ var/datum/species/dullahan/dullahan_species = human.dna.species if(isobj(dullahan_species.my_head.loc)) var/obj/head = dullahan_species.my_head.loc - head.say(speech_args[SPEECH_MESSAGE], spans = speech_args[SPEECH_SPANS], sanitize = FALSE, language = speech_args[SPEECH_LANGUAGE], message_range = speech_args[SPEECH_RANGE]) + if(speech_args[SPEECH_MODS][WHISPER_MODE]) // whisper away + speech_args[SPEECH_SPANS] |= SPAN_ITALICS + head.say(speech_args[SPEECH_MESSAGE], spans = speech_args[SPEECH_SPANS], sanitize = FALSE, language = speech_args[SPEECH_LANGUAGE], message_range = speech_args[SPEECH_RANGE], message_mods = speech_args[SPEECH_MODS]) speech_args[SPEECH_MESSAGE] = "" /obj/item/organ/ears/dullahan - zone = "abstract" + zone = BODY_ZONE_CHEST + organ_flags = parent_type::organ_flags | ORGAN_UNREMOVABLE + decay_factor = 0 /obj/item/organ/eyes/dullahan name = "head vision" desc = "An abstraction." actions_types = list(/datum/action/item_action/organ_action/dullahan) - zone = "abstract" + zone = BODY_ZONE_CHEST + organ_flags = parent_type::organ_flags | ORGAN_UNREMOVABLE + decay_factor = 0 tint = INFINITY // to switch the vision perspective to the head on species_gain() without issue. /datum/action/item_action/organ_action/dullahan @@ -223,7 +245,7 @@ START_PROCESSING(SSobj, src) RegisterSignal(owner, COMSIG_CARBON_REGENERATE_LIMBS, PROC_REF(unlist_head)) RegisterSignal(owner, COMSIG_LIVING_REVIVE, PROC_REF(retrieve_head)) - RegisterSignal(owner, COMSIG_HUMAN_PREFS_APPLIED, PROC_REF(update_prefs_name)) + RegisterSignal(owner, COMSIG_HUMAN_PREFS_APPLIED, PROC_REF(on_prefs_loaded)) become_hearing_sensitive(ROUNDSTART_TRAIT) /obj/item/dullahan_relay/Destroy() @@ -231,32 +253,33 @@ owner = null return ..() -/obj/item/dullahan_relay/process() - if(istype(loc, /obj/item/bodypart/head) && !QDELETED(owner)) - return - qdel(src) - return PROCESS_KILL - /// Updates our names after applying name prefs -/obj/item/dullahan_relay/proc/update_prefs_name(mob/living/carbon/human/wearer) +/obj/item/dullahan_relay/proc/on_prefs_loaded(mob/living/carbon/human/headless) SIGNAL_HANDLER var/obj/item/bodypart/head/detached_head = loc if (!istype(detached_head)) return // It's so over - detached_head.real_name = wearer.real_name - detached_head.name = wearer.real_name + detached_head.real_name = headless.real_name + detached_head.name = headless.real_name + name = headless.real_name + detached_head.voice = headless.voice + detached_head.pitch = pitch var/obj/item/organ/brain/brain = locate(/obj/item/organ/brain) in detached_head - brain.name = "[wearer.name]'s brain" + brain.name = "[headless.name]'s brain" + + detached_head.copy_appearance_from(headless, overwrite_eyes = TRUE) + detached_head.update_icon_dropped() /obj/item/dullahan_relay/Hear(atom/movable/speaker, message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, list/spans, list/message_mods = list(), message_range) . = ..() - owner?.Hear(speaker, message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, spans, message_mods, message_range) - -///Adds the owner to the list of hearers in hearers_in_view(), for visible/hearable on top of say messages -/obj/item/dullahan_relay/proc/include_owner(datum/source, list/hearers) - SIGNAL_HANDLER - if(!QDELETED(owner)) - hearers += owner + var/dist = get_dist(speaker, src) - message_range + if(dist > 0 && dist <= EAVESDROP_EXTRA_RANGE) + raw_message = stars(raw_message) + if(message_range != INFINITY && dist > EAVESDROP_EXTRA_RANGE) + return FALSE + if(!owner) + return FALSE + return owner.Hear(speaker, message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, spans, message_mods, message_range = INFINITY) ///Stops dullahans from gibbing when regenerating limbs /obj/item/dullahan_relay/proc/unlist_head(datum/source, list/excluded_zones) @@ -273,13 +296,3 @@ var/turf/body_turf = get_turf(owner) if(head && istype(head) && body_turf && !(head in owner.get_all_contents())) head.forceMove(body_turf) - -/obj/item/dullahan_relay/Destroy() - if(!QDELETED(owner)) - var/mob/living/carbon/human/human = owner - if(isdullahan(human)) - var/datum/species/dullahan/dullahan_species = human.dna.species - dullahan_species.my_head = null - owner.gib(DROP_ALL_REMAINS) - owner = null - return ..() diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index cc57babea8a..9e9455da1cf 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -286,44 +286,42 @@ if(!islist(ignored_mobs)) ignored_mobs = list(ignored_mobs) - var/list/hearers = get_hearers_in_view(vision_distance, src) //caches the hearers and then removes ignored mobs. + var/list/hearers = mob_only_listeners(get_hearers_in_view(vision_distance, src)) //caches the hearers and then removes ignored mobs. hearers -= ignored_mobs - if(self_message) - hearers -= src - var/raw_msg = message if(visible_message_flags & WITH_EMPHASIS_MESSAGE) message = apply_message_emphasis(message) if(visible_message_flags & EMOTE_MESSAGE) message = span_emote("[src] [message]") - for(var/mob/M in hearers) - if(!M.client) + for(var/mob/hearing_mob as anything in hearers) + if(!hearing_mob?.client) + continue + if(self_message && hearing_mob == src) continue //This entire if/else chain could be in two lines but isn't for readibilties sake. var/msg = message var/msg_type = MSG_VISUAL - if(M.see_invisible < invisibility)//if src is invisible to M + if(hearing_mob.see_invisible < invisibility)//if src is invisible to M msg = blind_message msg_type = MSG_AUDIBLE else if(T != loc && T != src) //if src is inside something and not a turf. - if(M != loc) // Only give the blind message to hearers that aren't the location + if(hearing_mob != loc) // Only give the blind message to hearers that aren't the location msg = blind_message msg_type = MSG_AUDIBLE - else if(!HAS_TRAIT(M, TRAIT_HEAR_THROUGH_DARKNESS) && M.lighting_cutoff < LIGHTING_CUTOFF_HIGH && T.is_softly_lit() && !in_range(T,M)) //if it is too dark, unless we're right next to them. + else if(!HAS_TRAIT(hearing_mob, TRAIT_HEAR_THROUGH_DARKNESS) && hearing_mob.lighting_cutoff < LIGHTING_CUTOFF_HIGH && T.is_softly_lit() && !in_range(T,hearing_mob)) //if it is too dark, unless we're right next to them. msg = blind_message msg_type = MSG_AUDIBLE if(!msg) continue - if(visible_message_flags & EMOTE_MESSAGE && runechat_prefs_check(M, visible_message_flags) && !M.is_blind()) - M.create_chat_message(src, raw_message = raw_msg, runechat_flags = visible_message_flags) - - M.show_message(msg, msg_type, blind_message, MSG_AUDIBLE) + if(visible_message_flags & EMOTE_MESSAGE && runechat_prefs_check(hearing_mob, visible_message_flags) && !hearing_mob.is_blind()) + hearing_mob.create_chat_message(src, raw_message = raw_msg, runechat_flags = visible_message_flags) + hearing_mob.show_message(msg, msg_type, blind_message, MSG_AUDIBLE) ///Adds the functionality to self_message. /mob/visible_message(message, self_message, blind_message, vision_distance = DEFAULT_MESSAGE_RANGE, list/ignored_mobs, visible_message_flags = NONE) @@ -360,18 +358,20 @@ * * audible_message_flags (optional) is the type of message being sent. */ /atom/proc/audible_message(message, deaf_message, hearing_distance = DEFAULT_MESSAGE_RANGE, self_message, audible_message_flags = NONE) - var/list/hearers = get_hearers_in_view(hearing_distance, src) - if(self_message) - hearers -= src + var/list/hearers = mob_only_listeners(get_hearers_in_view(hearing_distance, src)) var/raw_msg = message if(audible_message_flags & WITH_EMPHASIS_MESSAGE) message = apply_message_emphasis(message) if(audible_message_flags & EMOTE_MESSAGE) message = span_emote("[src] [message]") - for(var/mob/M in hearers) - if(audible_message_flags & EMOTE_MESSAGE && runechat_prefs_check(M, audible_message_flags) && M.can_hear()) - M.create_chat_message(src, raw_message = raw_msg, runechat_flags = audible_message_flags) - M.show_message(message, MSG_AUDIBLE, deaf_message, MSG_VISUAL) + for(var/mob/hearing_mob as anything in hearers) + if(!hearing_mob?.client) + continue + if(self_message && hearing_mob == src) + continue + if(audible_message_flags & EMOTE_MESSAGE && runechat_prefs_check(hearing_mob, audible_message_flags) && hearing_mob.can_hear()) + hearing_mob.create_chat_message(src, raw_message = raw_msg, runechat_flags = audible_message_flags) + hearing_mob.show_message(message, MSG_AUDIBLE, deaf_message, MSG_VISUAL) /** * Show a message to all mobs in earshot of this one @@ -405,6 +405,21 @@ if(self_runechat && (audible_message_flags & EMOTE_MESSAGE) && runechat_prefs_check(src, audible_message_flags)) create_chat_message(src, raw_message = raw_self_message, runechat_flags = audible_message_flags) +/// Gets a linked mob, letting atoms act as proxies for actions that rely on hearing sensitivity. +/// For example, AIs hearing around their holopads, and dullahans hearing around their heads. +/// Normal say messages are handled by Hear(), this is for other visible/audible messages +/atom/movable/proc/get_listening_mob() + return + +/obj/effect/overlay/holo_pad_hologram/get_listening_mob() + return Impersonation + +/obj/item/dullahan_relay/get_listening_mob() + return owner + +/mob/get_listening_mob() + return src + ///Returns the client runechat visible messages preference according to the message type. /atom/proc/runechat_prefs_check(mob/target, visible_message_flags = NONE) if(!target.client?.prefs.read_preference(/datum/preference/toggle/enable_runechat)) diff --git a/code/modules/surgery/bodyparts/head_hair_and_lips.dm b/code/modules/surgery/bodyparts/head_hair_and_lips.dm index 9348551ab02..3f7cb5228e7 100644 --- a/code/modules/surgery/bodyparts/head_hair_and_lips.dm +++ b/code/modules/surgery/bodyparts/head_hair_and_lips.dm @@ -3,7 +3,6 @@ /// Part of `update_limb()`, basically does all the head specific icon stuff. /obj/item/bodypart/head/proc/update_hair_and_lips(dropping_limb, is_creating) var/mob/living/carbon/human/human_head_owner = owner - var/datum/species/owner_species = human_head_owner?.dna.species //HIDDEN CHECKS START hair_hidden = FALSE @@ -43,18 +42,40 @@ if(!is_creating || !owner) return + copy_appearance_from(human_head_owner) - lip_style = human_head_owner.lip_style - lip_color = human_head_owner.lip_color - hairstyle = human_head_owner.hairstyle - hair_alpha = owner_species.hair_alpha - hair_color = human_head_owner.hair_color - facial_hairstyle = human_head_owner.facial_hairstyle - facial_hair_alpha = owner_species.facial_hair_alpha - facial_hair_color = human_head_owner.facial_hair_color - fixed_hair_color = owner_species.get_fixed_hair_color(human_head_owner) //Can be null - gradient_styles = human_head_owner.grad_style.Copy() - gradient_colors = human_head_owner.grad_color.Copy() +/obj/item/bodypart/head/proc/copy_appearance_from(mob/living/carbon/human/target, overwrite_eyes = FALSE) + var/datum/species/target_species = target.dna.species + + lip_style = target.lip_style + lip_color = target.lip_color + hairstyle = target.hairstyle + hair_alpha = target_species.hair_alpha + hair_color = target.hair_color + facial_hairstyle = target.facial_hairstyle + facial_hair_alpha = target_species.facial_hair_alpha + facial_hair_color = target.facial_hair_color + fixed_hair_color = target_species.get_fixed_hair_color(target) //Can be null + gradient_styles = target.grad_style.Copy() + gradient_colors = target.grad_color.Copy() + var/obj/item/organ/eyes/peepers = locate() in src + if(peepers) + if(overwrite_eyes || isnull(initial(peepers.eye_color_left))) + peepers.eye_color_left = target.eye_color_left + if(overwrite_eyes || isnull(initial(peepers.eye_color_right))) + peepers.eye_color_right = target.eye_color_right + + if(HAS_TRAIT(target, TRAIT_USES_SKINTONES)) + skin_tone = target.skin_tone + else if(HAS_TRAIT(target, TRAIT_MUTANT_COLORS)) + skin_tone = "" + if(target_species.fixed_mut_color) + species_color = target_species.fixed_mut_color + else + species_color = target.dna.features["mcolor"] + else + skin_tone = "" + species_color = "" /obj/item/bodypart/head/proc/get_hair_and_lips_icon(dropped) SHOULD_CALL_PARENT(TRUE)