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)