diff --git a/_maps/map_files/Blueshift/BlueShift_middle.dmm b/_maps/map_files/Blueshift/BlueShift_middle.dmm index 85e8846419a..e9d98475627 100644 --- a/_maps/map_files/Blueshift/BlueShift_middle.dmm +++ b/_maps/map_files/Blueshift/BlueShift_middle.dmm @@ -20855,7 +20855,6 @@ /obj/structure/closet/crate/freezer{ name = "organ storage" }, -/obj/item/organ/internal/tongue/tied, /obj/item/organ/internal/lungs/toxin, /obj/item/organ/external/genital/penis, /obj/item/organ/internal/heart, diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm index 524878b415b..c563ac056bc 100644 --- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm +++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm @@ -93,3 +93,23 @@ #define COMSIG_MOVABLE_USING_RADIO "movable_radio" /// Return to prevent the movable from talking into the radio. #define COMPONENT_CANNOT_USE_RADIO (1<<0) + +/// Sent from /atom/movable/proc/say_quote() after say verb is chosen and before spans are applied. +#define COMSIG_MOVABLE_SAY_QUOTE "movable_say_quote" + // Used to access COMSIG_MOVABLE_SAY_QUOTE argslist + /// The index of args that corresponds to the actual message + #define MOVABLE_SAY_QUOTE_MESSAGE 1 + #define MOVABLE_SAY_QUOTE_MESSAGE_SPANS 2 + #define MOVABLE_SAY_QUOTE_MESSAGE_MODS 3 +/// Sent from /atom/movable/proc/lang_treat() before it runs. +#define COMSIG_MOVABLE_TREAT_MESSAGE "movable_treat_message" + // Used to access COMSIG_MOVABLE_TREAT_MESSAGE argslist + /// The index of args that corresponds to the mob speaking + #define MOVABLE_TREAT_MESSAGE_SPEAKER 1 + /// The index of args that corresponds to the spoken language + #define MOVABLE_TREAT_MESSAGE_LANGUAGE 2 + /// The index of args that corresponds to the actual message + #define MOVABLE_TREAT_MESSAGE_MESSAGE 3 + #define MOVABLE_TREAT_MESSAGE_SPANS 4 + #define MOVABLE_TREAT_MESSAGE_MODS 5 + #define MOVABLE_TREAT_MESSAGE_NOQUOTE 6 diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 00dea832719..6f9496711f6 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -325,7 +325,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_RDS_SUPPRESSED "rds_suppressed" /// overrides the update_fire proc to always add fire (for lava) #define TRAIT_PERMANENTLY_ONFIRE "permanently_onfire" -/// Galactic Common Sign Language +/// Indicates if the mob is currently speaking with sign language #define TRAIT_SIGN_LANG "sign_language" /// This mob is able to use sign language over the radio. #define TRAIT_CAN_SIGN_ON_COMMS "can_sign_on_comms" diff --git a/code/__DEFINES/~skyrat_defines/jobs.dm b/code/__DEFINES/~skyrat_defines/jobs.dm index 96a67d65993..207d459a0d4 100644 --- a/code/__DEFINES/~skyrat_defines/jobs.dm +++ b/code/__DEFINES/~skyrat_defines/jobs.dm @@ -10,4 +10,6 @@ #define TECH_RESTRICTED_QUIRKS "Chunky Fingers" = TRUE #define GUARD_RESTRICTED_QUIRKS "Blind" = TRUE, "Deaf" = TRUE, "Foreigner" = TRUE, "Pacifist" = TRUE, "Nerve Stapled" = TRUE +#define RESTRICTED_QUIRKS_EXCEPTIONS list("Mute" = "Signer") + #define FLAVOR_TEXT_CHAR_REQUIREMENT 150 diff --git a/code/_globalvars/lists/maintenance_loot.dm b/code/_globalvars/lists/maintenance_loot.dm index 33367a432db..5f82b22d2b8 100644 --- a/code/_globalvars/lists/maintenance_loot.dm +++ b/code/_globalvars/lists/maintenance_loot.dm @@ -332,6 +332,7 @@ GLOBAL_LIST_INIT(rarity_loot, list(//rare: really good items list(//misc /obj/item/book/granter/crafting_recipe/pipegun_prime = 1, /obj/item/book/granter/crafting_recipe/trash_cannon = 1, + /obj/item/book/granter/sign_language = 1, /obj/item/disk/nuclear/fake = 1, /obj/item/skillchip/brainwashing = 1, /obj/item/tattoo_kit = 1, diff --git a/code/datums/actions/mobs/sign_language.dm b/code/datums/actions/mobs/sign_language.dm new file mode 100644 index 00000000000..1a8446ffea4 --- /dev/null +++ b/code/datums/actions/mobs/sign_language.dm @@ -0,0 +1,92 @@ +/** + * Allows a Carbon to toggle sign language on/off. The button is invisible for mute Carbons. + * Theory of Operation: + * A. If TRAIT_SIGN_LANG is added/removed, and the button is visible, then update the button. + * B. React to presence of trait TRAIT_MUTE for quality/convenience purposes: + * C. If TRAIT_MUTE is added, then activate and hide the Action. + * D. If TRAIT_MUTE is then removed, then show the Action. + * + * * Credits: + * - Action sprite created by @Wallemations (icons/hud/actions.dmi:sign_language) +*/ +/datum/action/innate/sign_language + name = "Sign Language" + icon_icon = 'icons/hud/actions.dmi' + button_icon_state = "sign_language" + desc = "Allows you to communicate via sign language." + owner_has_control = FALSE + +/datum/action/innate/sign_language/UpdateButton(atom/movable/screen/movable/action_button/button, status_only = FALSE, force) + . = ..() + if(!. || !button) + return + if(HAS_TRAIT(owner, TRAIT_SIGN_LANG)) + button.icon_state = "template_active" + else + button.icon_state = "template" + +/datum/action/innate/sign_language/Grant(mob/living/carbon/grant_to) + ..() + if (HAS_TRAIT(grant_to, TRAIT_MUTE)) + RegisterSignal(grant_to, SIGNAL_REMOVETRAIT(TRAIT_MUTE), PROC_REF(on_unmuted)) + // Convenience. Mute Carbons can only speak with sign language. + if (!active) + Activate() + else + RegisterSignal(grant_to, SIGNAL_ADDTRAIT(TRAIT_MUTE), PROC_REF(on_muted)) + // Convenience. Only display action if the Carbon isn't mute. + show_action() + +/datum/action/innate/sign_language/Remove(mob/living/carbon/grant_to) + ..() + UnregisterSignal(grant_to, list( + SIGNAL_ADDTRAIT(TRAIT_SIGN_LANG), + SIGNAL_REMOVETRAIT(TRAIT_SIGN_LANG), + SIGNAL_ADDTRAIT(TRAIT_MUTE), + SIGNAL_REMOVETRAIT(TRAIT_MUTE) + )) + REMOVE_TRAIT(grant_to, TRAIT_SIGN_LANG, TRAIT_GENERIC) + +/datum/action/innate/sign_language/Activate() + active = TRUE + ADD_TRAIT(owner, TRAIT_SIGN_LANG, TRAIT_GENERIC) + to_chat(owner, span_green("You are now communicating with sign language.")) + +/datum/action/innate/sign_language/Deactivate() + active = FALSE + REMOVE_TRAIT(owner, TRAIT_SIGN_LANG, TRAIT_GENERIC) + to_chat(owner, span_green("You have stopped using sign language.")) + +/// Shows the linked action to the owner Carbon. +/datum/action/innate/sign_language/proc/show_action() + owner_has_control = TRUE + RegisterSignal(owner, SIGNAL_ADDTRAIT(TRAIT_SIGN_LANG), PROC_REF(update_icon_on_signal)) + RegisterSignal(owner, SIGNAL_REMOVETRAIT(TRAIT_SIGN_LANG), PROC_REF(update_icon_on_signal)) + GiveAction(owner) + +/// Hides the linked action from the owner Carbon. +/datum/action/innate/sign_language/proc/hide_action() + owner_has_control = FALSE + UnregisterSignal(owner, list( + SIGNAL_ADDTRAIT(TRAIT_SIGN_LANG), + SIGNAL_REMOVETRAIT(TRAIT_SIGN_LANG) + )) + HideFrom(owner) + +/// Signal handler for SIGNAL_ADDTRAIT(TRAIT_MUTE) +/// Hides the action if the signing Carbon gains TRAIT_MUTE. +/datum/action/innate/sign_language/proc/on_muted() + SIGNAL_HANDLER + + RegisterSignal(owner, SIGNAL_REMOVETRAIT(TRAIT_MUTE), PROC_REF(on_unmuted)) + hide_action() + // Enable sign language if the Carbon knows it and just gained TRAIT_MUTE + if (!HAS_TRAIT(owner, TRAIT_SIGN_LANG)) + Activate() + +/// Signal handler for SIGNAL_REMOVETRAIT(TRAIT_MUTE) +/// Re-shows the action if the signing Carbon loses TRAIT_MUTE. +/datum/action/innate/sign_language/proc/on_unmuted() + SIGNAL_HANDLER + + show_action() diff --git a/code/datums/components/sign_language.dm b/code/datums/components/sign_language.dm new file mode 100644 index 00000000000..eaffb160f54 --- /dev/null +++ b/code/datums/components/sign_language.dm @@ -0,0 +1,256 @@ +/// Defines used to determine whether a sign language user can sign or not, and if not, why they cannot. +#define SIGN_OKAY 0 +#define SIGN_ONE_HAND 1 +#define SIGN_HANDS_FULL 2 +#define SIGN_ARMLESS 3 +#define SIGN_ARMS_DISABLED 4 +#define SIGN_TRAIT_BLOCKED 5 +#define SIGN_CUFFED 6 + +/** +* Reactive Sign Language Component for Carbons. Allows Carbons to speak with sign language if they have the relevant traits. +* Implements sign language by incrementally overriding several critical functions, variables, and argument lists. +* +* High-Level Theory of Operation: +* 1. Component is added to a Carbon via AddComponent. +* 2. Component grants sign language action to its parent, which adds and removes TRAIT_SIGN_LANG. +* 3. Component reacts to addition and removal of TRAIT_SIGN_LANG in parent: +* 4. If TRAIT_SIGN_LANG is added, then enable sign language. Listen for speech signals and modify the mob's speech, say_mod verbs, and typing indicator. +* 5. If TRAIT_SIGN_LANG is removed, then disable sign language. Unregister from speech signals and reset the mob's speech, say_mob verbs, and typing indicator. +* +* * Credits: +* - Original Tongue Tied created by @Wallemations (https://github.com/tgstation/tgstation/pull/52907) +* - Action sprite created by @Wallemations (icons/hud/actions.dmi:sign_language) +*/ +/datum/component/sign_language + /// The tonal indicator shown when sign language users finish sending a message. If it's empty, none appears. + var/tonal_indicator = null + /// The timerid for our sign language tonal indicator. + var/tonal_timerid + /// Any symbols to sanitize from signed messages. + var/regex/omissions = new ("\[!?\]", "g") + /// The action for toggling sign language. + var/datum/action/innate/sign_language/linked_action + +/// Replace specific characters in the input string with periods. +/datum/component/sign_language/proc/sanitize_message(input) + return replacetext(input, omissions, ".") + +/datum/component/sign_language/Initialize() + // Non-Carbon mobs can't use sign language. + if (!iscarbon(parent)) + stack_trace("Sign Language component added to [parent] ([parent?.type]) which is not a /mob/living/carbon subtype.") + return COMPONENT_INCOMPATIBLE + linked_action = new(src) + +/datum/component/sign_language/Destroy() + QDEL_NULL(linked_action) + return ..() + +/datum/component/sign_language/RegisterWithParent() + // Sign language is toggled on/off via adding/removing TRAIT_SIGN_LANG. + RegisterSignal(parent, SIGNAL_ADDTRAIT(TRAIT_SIGN_LANG), PROC_REF(enable_sign_language)) + RegisterSignal(parent, SIGNAL_REMOVETRAIT(TRAIT_SIGN_LANG), PROC_REF(disable_sign_language)) + linked_action.Grant(parent) + linked_action.UpdateButtons() + +/datum/component/sign_language/UnregisterFromParent() + disable_sign_language() + UnregisterSignal(parent, list( + SIGNAL_ADDTRAIT(TRAIT_SIGN_LANG), + SIGNAL_REMOVETRAIT(TRAIT_SIGN_LANG) + )) + +/// Signal handler for [COMSIG_SIGNLANGUAGE_ENABLE] +/// Enables signing for the parent Carbon, stopping them from speaking vocally. +/// This proc is only called directly after TRAIT_SIGN_LANG is added to the Carbon. +/datum/component/sign_language/proc/enable_sign_language() + SIGNAL_HANDLER + + var/mob/living/carbon/carbon_parent = parent + carbon_parent.dna?.species.say_mod = "signs" + carbon_parent.verb_ask = "signs" + carbon_parent.verb_exclaim = "signs" + carbon_parent.verb_whisper = "subtly signs" + carbon_parent.verb_sing = "rythmically signs" + carbon_parent.verb_yell = "emphatically signs" + carbon_parent.bubble_icon = "signlang" + RegisterSignal(carbon_parent, COMSIG_LIVING_TRY_SPEECH, PROC_REF(on_try_speech)) + RegisterSignal(carbon_parent, COMSIG_LIVING_TREAT_MESSAGE, PROC_REF(on_treat_living_message)) + RegisterSignal(carbon_parent, COMSIG_MOVABLE_TREAT_MESSAGE, PROC_REF(on_treat_message)) + RegisterSignal(carbon_parent, COMSIG_MOVABLE_USING_RADIO, PROC_REF(on_using_radio)) + RegisterSignal(carbon_parent, COMSIG_MOVABLE_SAY_QUOTE, PROC_REF(on_say_quote)) + RegisterSignal(carbon_parent, COMSIG_MOB_SAY, PROC_REF(on_say)) + return TRUE + +/// Signal handler for [COMSIG_SIGNLANGUAGE_DISABLE] +/// Disables signing for the parent Carbon, allowing them to speak vocally. +/// This proc is only called directly after TRAIT_SIGN_LANG is removed from the Carbon. +/datum/component/sign_language/proc/disable_sign_language() + SIGNAL_HANDLER + + var/mob/living/carbon/carbon_parent = parent + carbon_parent.dna?.species.say_mod = initial(carbon_parent.dna.species.say_mod) + carbon_parent.verb_ask = initial(carbon_parent.verb_ask) + carbon_parent.verb_exclaim = initial(carbon_parent.verb_exclaim) + carbon_parent.verb_whisper = initial(carbon_parent.verb_whisper) + carbon_parent.verb_sing = initial(carbon_parent.verb_sing) + carbon_parent.verb_yell = initial(carbon_parent.verb_yell) + carbon_parent.bubble_icon = initial(carbon_parent.bubble_icon) + UnregisterSignal(carbon_parent, list( + COMSIG_LIVING_TRY_SPEECH, + COMSIG_LIVING_TREAT_MESSAGE, + COMSIG_MOVABLE_TREAT_MESSAGE, + COMSIG_MOVABLE_USING_RADIO, + COMSIG_MOVABLE_SAY_QUOTE, + COMSIG_MOB_SAY + )) + return TRUE + +/// Signal proc for [COMSIG_LIVING_TRY_SPEECH] +/// Sign languagers can always speak regardless of they're mute (as long as they're not mimes) +/datum/component/sign_language/proc/on_try_speech(mob/living/source, message, ignore_spam, forced) + SIGNAL_HANDLER + + var/mob/living/carbon/carbon_parent = parent + if(carbon_parent.mind?.miming) + to_chat(carbon_parent, span_green("You stop yourself from signing in favor of the artform of mimery!")) + return COMPONENT_CANNOT_SPEAK + + switch(check_signables_state()) + if(SIGN_HANDS_FULL) // Full hands + carbon_parent.visible_message("tries to sign, but can't with [carbon_parent.p_their()] hands full!", visible_message_flags = EMOTE_MESSAGE) + return COMPONENT_CANNOT_SPEAK + + if(SIGN_CUFFED) // Restrained + carbon_parent.visible_message("tries to sign, but can't with [carbon_parent.p_their()] hands bound!", visible_message_flags = EMOTE_MESSAGE) + return COMPONENT_CANNOT_SPEAK + + if(SIGN_ARMLESS) // No arms + to_chat(carbon_parent, span_warning("You can't sign with no hands!")) + return COMPONENT_CANNOT_SPEAK + + if(SIGN_ARMS_DISABLED) // Arms but they're disabled + to_chat(carbon_parent, span_warning("You can't sign with your hands right now!")) + return COMPONENT_CANNOT_SPEAK + + if(SIGN_TRAIT_BLOCKED) // Hands blocked or emote mute + to_chat(carbon_parent, span_warning("You can't sign at the moment!")) + return COMPONENT_CANNOT_SPEAK + + // Assuming none of the above fail, sign language users can speak + // regardless of being muzzled or mute toxin'd or whatever. + return COMPONENT_CAN_ALWAYS_SPEAK + +/// Checks to see what state this person is in and if they are able to sign or not. +/datum/component/sign_language/proc/check_signables_state() + var/mob/living/carbon/carbon_parent = parent + // See how many hands we can actually use (this counts disabled / missing limbs for us) + var/total_hands = carbon_parent.usable_hands + // Look ma, no hands! + if(total_hands <= 0) + // Either our hands are still attached (just disabled) or they're gone entirely + return carbon_parent.num_hands > 0 ? SIGN_ARMS_DISABLED : SIGN_ARMLESS + + // Now let's see how many of our hands is holding something + var/busy_hands = 0 + // Yes held_items can contain null values, which represents empty hands, + // I'm just saving myself a variable cast by using as anything + for(var/obj/item/held_item as anything in carbon_parent.held_items) + // items like slappers/zombie claws/etc. should be ignored + if(isnull(held_item) || held_item.item_flags & HAND_ITEM) + continue + + busy_hands++ + + // Handcuffed or otherwise restrained - can't talk + if(HAS_TRAIT(src, TRAIT_RESTRAINED)) + return SIGN_CUFFED + // Some other trait preventing us from using our hands now + else if(HAS_TRAIT(src, TRAIT_HANDS_BLOCKED) || HAS_TRAIT(src, TRAIT_EMOTEMUTE)) + return SIGN_TRAIT_BLOCKED + + // Okay let's compare the total hands to the number of busy hands + // to see how many we have left to use for signing right now + var/actually_usable_hands = total_hands - busy_hands + if(actually_usable_hands <= 0) + return SIGN_HANDS_FULL + if(actually_usable_hands == 1) + return SIGN_ONE_HAND + + return SIGN_OKAY + +/// Signal proc for [COMSIG_LIVING_TREAT_MESSAGE] +/// Stars out our message if we only have 1 hand free. +/datum/component/sign_language/proc/on_treat_living_message(atom/movable/source, list/message_args) + SIGNAL_HANDLER + + if(check_signables_state() == SIGN_ONE_HAND) + message_args[TREAT_MESSAGE_MESSAGE] = stars(message_args[TREAT_MESSAGE_MESSAGE]) + +/// Signal proc for [COMSIG_MOVABLE_SAY_QUOTE] +/// Removes exclamation/question marks. +/datum/component/sign_language/proc/on_say_quote(atom/movable/source, list/message_args) + SIGNAL_HANDLER + + message_args[MOVABLE_SAY_QUOTE_MESSAGE] = sanitize_message(message_args[MOVABLE_SAY_QUOTE_MESSAGE]) + +/// Signal proc for [COMSIG_MOVABLE_TREAT_MESSAGE] +/// Removes exclamation/question marks only if /atom/movable/proc/say_quote() isn't going to run. +/datum/component/sign_language/proc/on_treat_message(atom/movable/source, list/message_args) + SIGNAL_HANDLER + + if (message_args[MOVABLE_TREAT_MESSAGE_NOQUOTE]) + message_args[MOVABLE_TREAT_MESSAGE_MESSAGE] = sanitize_message(message_args[MOVABLE_TREAT_MESSAGE_MESSAGE]) + +/// Signal proc for [COMSIG_MOVABLE_USING_RADIO] +/// Disallows us from speaking on comms if we don't have the special trait. +/datum/component/sign_language/proc/on_using_radio(atom/movable/source, obj/item/radio/radio) + SIGNAL_HANDLER + + return HAS_TRAIT(source, TRAIT_CAN_SIGN_ON_COMMS) ? NONE : COMPONENT_CANNOT_USE_RADIO + +/// Replaces emphatic punctuation with periods. Changes tonal indicator and emotes eyebrow movement based on what is typed. +/datum/component/sign_language/proc/on_say(mob/living/carbon/carbon_parent, list/speech_args) + SIGNAL_HANDLER + + // The original message + var/message = speech_args[SPEECH_MESSAGE] + // Is there a ! + var/exclamation_found = findtext(message, "!") + // Is there a ? + var/question_found = findtext(message, "?") + + // Cut our last overlay before we replace it + if(timeleft(tonal_timerid) > 0) + remove_tonal_indicator() + deltimer(tonal_timerid) + // Prioritize questions + if(question_found) + tonal_indicator = mutable_appearance('icons/mob/effects/talk.dmi', "signlang1", TYPING_LAYER) + carbon_parent.visible_message(span_notice("[carbon_parent] lowers [carbon_parent.p_their()] eyebrows.")) + else if(exclamation_found) + tonal_indicator = mutable_appearance('icons/mob/effects/talk.dmi', "signlang2", TYPING_LAYER) + carbon_parent.visible_message(span_notice("[carbon_parent] raises [carbon_parent.p_their()] eyebrows.")) + // If either an exclamation or question are found + if(!isnull(tonal_indicator) && carbon_parent.client?.typing_indicators) + carbon_parent.add_overlay(tonal_indicator) + tonal_timerid = addtimer(CALLBACK(src, PROC_REF(remove_tonal_indicator)), 2.5 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE | TIMER_STOPPABLE | TIMER_DELETE_ME) + else // If we're not gonna use it, just be sure we get rid of it + tonal_indicator = null + +/// Removes the tonal indicator overlay completely +/datum/component/sign_language/proc/remove_tonal_indicator() + if(isnull(tonal_indicator)) + return + var/mob/living/carbon/carbon_parent = parent + carbon_parent.cut_overlay(tonal_indicator) + tonal_indicator = null + +#undef SIGN_OKAY +#undef SIGN_ONE_HAND +#undef SIGN_HANDS_FULL +#undef SIGN_ARMLESS +#undef SIGN_ARMS_DISABLED +#undef SIGN_TRAIT_BLOCKED +#undef SIGN_CUFFED diff --git a/code/datums/quirks/good.dm b/code/datums/quirks/good.dm index 9cf312b82e0..df54a116a61 100644 --- a/code/datums/quirks/good.dm +++ b/code/datums/quirks/good.dm @@ -276,3 +276,20 @@ gain_text = "You feel HONGRY." lose_text = "You no longer feel HONGRY." mail_goodies = list(/obj/effect/spawner/random/food_or_drink/dinner) + +/datum/quirk/item_quirk/signer + name = "Signer" + desc = "You possess excellent communication skills in sign language." + icon = "hands" + value = 4 + mail_goodies = list(/obj/item/clothing/gloves/radio) + +/datum/quirk/item_quirk/signer/add_unique() + quirk_holder.AddComponent(/datum/component/sign_language) + var/obj/item/clothing/gloves/gloves_type = /obj/item/clothing/gloves/radio + if(isplasmaman(quirk_holder)) + gloves_type = /obj/item/clothing/gloves/color/plasmaman/radio + give_item_to_holder(gloves_type, list(LOCATION_GLOVES = ITEM_SLOT_GLOVES, LOCATION_BACKPACK = ITEM_SLOT_BACKPACK, LOCATION_HANDS = ITEM_SLOT_HANDS)) + +/datum/quirk/item_quirk/signer/remove() + qdel(quirk_holder.GetComponent(/datum/component/sign_language)) diff --git a/code/datums/quirks/negative.dm b/code/datums/quirks/negative.dm index f1d458b8fff..dee743b916a 100644 --- a/code/datums/quirks/negative.dm +++ b/code/datums/quirks/negative.dm @@ -992,3 +992,14 @@ medical_record_text = "Patient is not literate." hardcore_value = 8 mail_goodies = list(/obj/item/pai_card) // can read things for you + +/datum/quirk/mute + name = "Mute" + desc = "For some reason you are completely unable to speak." + icon = "volume-xmark" + value = -4 + mob_trait = TRAIT_MUTE + gain_text = span_danger("You find yourself unable to speak!") + lose_text = span_notice("You feel a growing strength in your vocal chords.") + medical_record_text = "The patient is unable to use their voice in any capacity." + hardcore_value = 4 diff --git a/code/datums/quirks/neutral.dm b/code/datums/quirks/neutral.dm index 25d0da4d505..d9337e6c04f 100644 --- a/code/datums/quirks/neutral.dm +++ b/code/datums/quirks/neutral.dm @@ -358,50 +358,6 @@ quirk_holder.add_mood_event("bad_hair_day", /datum/mood_event/bald) -/datum/quirk/item_quirk/tongue_tied - name = "Tongue Tied" - desc = "Due to a past incident, your ability to communicate has been relegated to your hands." - icon = "sign-language" - value = 0 - medical_record_text = "During physical examination, patient's tongue was found to be uniquely damaged." - mail_goodies = list(/obj/item/clothing/gloves/radio) - -/datum/quirk/item_quirk/tongue_tied/add_unique() - var/mob/living/carbon/human/human_holder = quirk_holder - var/obj/item/organ/internal/tongue/old_tongue = human_holder.getorganslot(ORGAN_SLOT_TONGUE) - old_tongue.Remove(human_holder) - qdel(old_tongue) - - var/obj/item/organ/internal/tongue/tied/new_tongue = new(get_turf(human_holder)) - new_tongue.Insert(human_holder) - // Only tongues of people with this quirk can't be removed. Manually spawned or found tongues can be. - new_tongue.organ_flags |= ORGAN_UNREMOVABLE - - var/obj/item/clothing/gloves/gloves_type = /obj/item/clothing/gloves/radio - if(isplasmaman(human_holder)) - gloves_type = /obj/item/clothing/gloves/color/plasmaman/radio - give_item_to_holder(gloves_type, list(LOCATION_GLOVES = ITEM_SLOT_GLOVES, LOCATION_BACKPACK = ITEM_SLOT_BACKPACK, LOCATION_HANDS = ITEM_SLOT_HANDS)) - -/datum/quirk/item_quirk/tongue_tied/post_add() - to_chat(quirk_holder, span_boldannounce("Because you speak with your hands, having them full hinders your ability to communicate!")) - -/datum/quirk/item_quirk/tongue_tied/remove() - var/obj/item/organ/internal/tongue/tied/quirk_tongue = quirk_holder.getorganslot(ORGAN_SLOT_TONGUE) - if(!istype(quirk_tongue)) - return - - var/obj/item/organ/internal/tongue/new_tongue_type = /obj/item/organ/internal/tongue - if(iscarbon(quirk_holder)) - var/mob/living/carbon/carbon_quirky = quirk_holder - new_tongue_type = carbon_quirky.dna?.species?.mutanttongue - if(!new_tongue_type) - return - - var/obj/item/organ/internal/tongue/new_tongue = new new_tongue_type() - quirk_tongue.Remove(quirk_holder, TRUE) - new_tongue.Insert(quirk_holder, TRUE) - qdel(quirk_tongue) - /datum/quirk/item_quirk/photographer name = "Photographer" desc = "You carry your camera and personal photo album everywhere you go, and your scrapbooks are legendary among your coworkers." diff --git a/code/game/objects/effects/spawners/random/exotic.dm b/code/game/objects/effects/spawners/random/exotic.dm index 80e9b103ba8..a4356b5ec3a 100644 --- a/code/game/objects/effects/spawners/random/exotic.dm +++ b/code/game/objects/effects/spawners/random/exotic.dm @@ -17,6 +17,7 @@ icon_state = "book" loot = list( // A single roundstart species language book. /obj/item/language_manual/roundstart_species = 100, + /obj/item/book/granter/sign_language = 10, /obj/item/language_manual/roundstart_species/five = 3, /obj/item/language_manual/roundstart_species/unlimited = 1, ) diff --git a/code/game/objects/items/granters/sign_language.dm b/code/game/objects/items/granters/sign_language.dm new file mode 100644 index 00000000000..97c5285096a --- /dev/null +++ b/code/game/objects/items/granters/sign_language.dm @@ -0,0 +1,29 @@ +/// Sign language book adds the sign language component to the reading Human. +/// Grants reader the ability to toggle sign language using a HUD button. +/obj/item/book/granter/sign_language + name = "Galactic Standard Sign Language" + desc = "A comprehensive guide to learning sign language and finger-spelling." + remarks = list( + "Signing comprises a range of techniques...", + "Words can be spelled out through sequences of signs...", + "The way your palm faces?", + "With questions, the eyebrows are lowered...", + "Cool! Extensive coverage of common phrases!", + "Communicate just about anything through signing...", + ) + +/obj/item/book/granter/sign_language/can_learn(mob/living/user) + if (!iscarbon(user)) + return + if (user.GetComponent(/datum/component/sign_language)) + to_chat(user, span_warning("You already know all about sign language!")) + return + return TRUE + +/obj/item/book/granter/sign_language/recoil(mob/living/user) + to_chat(user, span_warning("You can't read it, the pages are too faded and smudged!")) + +/// Called when the reading is completely finished. This is where the actual granting should happen. +/obj/item/book/granter/sign_language/on_reading_finished(mob/living/user) + ..() + user.AddComponent(/datum/component/sign_language) diff --git a/code/game/say.dm b/code/game/say.dm index 15b81ff5803..3983ff2323d 100644 --- a/code/game/say.dm +++ b/code/game/say.dm @@ -148,6 +148,8 @@ GLOBAL_LIST_INIT(freqtospan, list( if (!say_mod) say_mod = say_mod(input, message_mods) + SEND_SIGNAL(src, COMSIG_MOVABLE_SAY_QUOTE, args) + if(copytext_char(input, -2) == "!!") spans |= SPAN_YELL @@ -171,6 +173,7 @@ GLOBAL_LIST_INIT(freqtospan, list( #undef ENCODE_HTML_EMPHASIS /atom/movable/proc/lang_treat(atom/movable/speaker, datum/language/language, raw_message, list/spans, list/message_mods = list(), no_quote = FALSE) + SEND_SIGNAL(src, COMSIG_MOVABLE_TREAT_MESSAGE, args) var/atom/movable/source = speaker.GetSource() || speaker //is the speaker virtual if(has_language(language)) return no_quote ? raw_message : source.say_quote(raw_message, spans, message_mods) diff --git a/code/modules/mapfluff/ruins/lavalandruin_code/syndicate_base.dm b/code/modules/mapfluff/ruins/lavalandruin_code/syndicate_base.dm index 1b936458837..805c7d23457 100644 --- a/code/modules/mapfluff/ruins/lavalandruin_code/syndicate_base.dm +++ b/code/modules/mapfluff/ruins/lavalandruin_code/syndicate_base.dm @@ -37,8 +37,8 @@ new /obj/item/organ/internal/tongue/bone(src) new /obj/item/organ/internal/tongue/robot(src) //DANGER! CRYSTAL HYPERSTRUCTURE- new /obj/item/organ/internal/tongue/ethereal(src) - new /obj/item/organ/internal/tongue/tied(src) new /obj/item/autosurgeon/syndicate/commsagent(src) + new /obj/item/book/granter/sign_language(src) new /obj/item/clothing/gloves/radio(src) /obj/machinery/power/supermatter_crystal/shard/syndicate diff --git a/code/modules/mob/living/carbon/human/human_say.dm b/code/modules/mob/living/carbon/human/human_say.dm index b104a1da485..e85dfe1b8d2 100644 --- a/code/modules/mob/living/carbon/human/human_say.dm +++ b/code/modules/mob/living/carbon/human/human_say.dm @@ -1,12 +1,5 @@ /mob/living/carbon/human/say_mod(input, list/message_mods = list()) verb_say = dna.species.say_mod - // Any subtype of slurring in our status effects make us "slur" - if(locate(/datum/status_effect/speech/slurring) in status_effects) - if (HAS_TRAIT(src, TRAIT_SIGN_LANG)) - return "loosely signs" - else - return "slurs" - return ..() /mob/living/carbon/human/GetVoice() diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm index dca1515b59e..d170b98be82 100644 --- a/code/modules/mob/living/living_say.dm +++ b/code/modules/mob/living/living_say.dm @@ -460,6 +460,12 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( . = "[verb_whisper] in [p_their()] last breath" else if(message_mods[MODE_SING]) . = verb_sing + // Any subtype of slurring in our status effects make us "slur" + else if(locate(/datum/status_effect/speech/slurring) in status_effects) + if (HAS_TRAIT(src, TRAIT_SIGN_LANG)) + . = "loosely signs" + else + . = "slurs" else if(has_status_effect(/datum/status_effect/speech/stutter)) if(HAS_TRAIT(src, TRAIT_SIGN_LANG)) . = "shakily signs" diff --git a/code/modules/surgery/organs/tongue.dm b/code/modules/surgery/organs/tongue.dm index 0cd42c978f6..93fc97d5240 100644 --- a/code/modules/surgery/organs/tongue.dm +++ b/code/modules/surgery/organs/tongue.dm @@ -504,185 +504,3 @@ GLOBAL_LIST_INIT(english_to_zombie, list()) /obj/item/organ/internal/tongue/ethereal/Initialize(mapload) . = ..() languages_possible = languages_possible_ethereal - -/// Defines used to determine whether a sign language user can sign or not, and if not, why they cannot. -#define SIGN_OKAY 0 -#define SIGN_ONE_HAND 1 -#define SIGN_HANDS_FULL 2 -#define SIGN_ARMLESS 3 -#define SIGN_ARMS_DISABLED 4 -#define SIGN_TRAIT_BLOCKED 5 -#define SIGN_CUFFED 6 - -//Sign Language Tongue - yep, that's how you speak sign language. -/obj/item/organ/internal/tongue/tied - name = "tied tongue" - desc = "If only one had a sword so we may finally untie this knot." - say_mod = "signs" - icon_state = "tonguetied" - modifies_speech = TRUE - // The tonal indicator shown when we finish sending a message. If it's empty, none appears. - var/tonal_indicator = null - // The timerid for our tonal indicator - var/tonal_timerid - -/obj/item/organ/internal/tongue/tied/Insert(mob/living/carbon/signer, special = FALSE, drop_if_replaced = TRUE) - . = ..() - signer.verb_ask = "signs" - signer.verb_exclaim = "signs" - signer.verb_whisper = "subtly signs" - signer.verb_sing = "rythmically signs" - signer.verb_yell = "emphatically signs" - signer.bubble_icon = "signlang" - ADD_TRAIT(signer, TRAIT_SIGN_LANG, ORGAN_TRAIT) - RegisterSignal(signer, COMSIG_LIVING_TRY_SPEECH, PROC_REF(on_speech_check)) - RegisterSignal(signer, COMSIG_LIVING_TREAT_MESSAGE, PROC_REF(on_treat_message)) - RegisterSignal(signer, COMSIG_MOVABLE_USING_RADIO, PROC_REF(on_use_radio)) - -/obj/item/organ/internal/tongue/tied/Remove(mob/living/carbon/speaker, special = FALSE) - . = ..() - speaker.verb_ask = initial(speaker.verb_ask) - speaker.verb_exclaim = initial(speaker.verb_exclaim) - speaker.verb_whisper = initial(speaker.verb_whisper) - speaker.verb_sing = initial(speaker.verb_sing) - speaker.verb_yell = initial(speaker.verb_yell) - speaker.bubble_icon = initial(speaker.bubble_icon) - REMOVE_TRAIT(speaker, TRAIT_SIGN_LANG, ORGAN_TRAIT) - UnregisterSignal(speaker, list(COMSIG_LIVING_TRY_SPEECH, COMSIG_LIVING_TREAT_MESSAGE, COMSIG_MOVABLE_USING_RADIO)) - -/// Signal proc for [COMSIG_LIVING_TRY_SPEECH] -/// Sign languagers can always speak regardless of they're mute (as long as they're not mimes) -/obj/item/organ/internal/tongue/tied/proc/on_speech_check(mob/living/source, message, ignore_spam, forced) - SIGNAL_HANDLER - - if(source.mind?.miming) - to_chat(source, span_green("You stop yourself from signing in favor of the artform of mimery!")) - return COMPONENT_CANNOT_SPEAK - - switch(check_signables_state()) - if(SIGN_HANDS_FULL) // Full hands - source.visible_message("tries to sign, but can't with [source.p_their()] hands full!", visible_message_flags = EMOTE_MESSAGE) - return COMPONENT_CANNOT_SPEAK - - if(SIGN_CUFFED) // Restrained - source.visible_message("tries to sign, but can't with [source.p_their()] hands bound!", visible_message_flags = EMOTE_MESSAGE) - return COMPONENT_CANNOT_SPEAK - - if(SIGN_ARMLESS) // No arms - to_chat(source, span_warning("You can't sign with no hands!")) - return COMPONENT_CANNOT_SPEAK - - if(SIGN_ARMS_DISABLED) // Arms but they're disabled - to_chat(source, span_warning("Your can't sign with your hands right now!")) - return COMPONENT_CANNOT_SPEAK - - if(SIGN_TRAIT_BLOCKED) // Hands blocked or emote mute - to_chat(source, span_warning("You can't sign at the moment!")) - return COMPONENT_CANNOT_SPEAK - - // Assuming none of the above fail, sign language users can speak - // regardless of being muzzled or mute toxin'd or whatever. - return COMPONENT_CAN_ALWAYS_SPEAK - -/// Signal proc for [COMSIG_LIVING_TREAT_MESSAGE] that stars out our message if we only have 1 hand free -/obj/item/organ/internal/tongue/tied/proc/on_treat_message(mob/living/source, list/message_args) - SIGNAL_HANDLER - - if(check_signables_state() == SIGN_ONE_HAND) - message_args[TREAT_MESSAGE_MESSAGE] = stars(message_args[TREAT_MESSAGE_MESSAGE]) - -/// Signal proc for [COMSIG_MOVABLE_USING_RADIO] that disallows us from speaking on comms if we don't have the special trait -/// Being unable to sign, or having our message be starred out, is handled by the above two signal procs. -/obj/item/organ/internal/tongue/tied/proc/on_use_radio(atom/movable/source, obj/item/radio/radio) - SIGNAL_HANDLER - - return HAS_TRAIT(source, TRAIT_CAN_SIGN_ON_COMMS) ? NONE : COMPONENT_CANNOT_USE_RADIO - -/// Checks to see what state this person is in and if they are able to sign or not. -/obj/item/organ/internal/tongue/tied/proc/check_signables_state() - if(!owner) - CRASH("[type] called check_signables_state without an owner.") - - // See how many hands we can actually use (this counts disabled / missing limbs for us) - var/total_hands = owner.usable_hands - // Look ma, no hands! - if(total_hands <= 0) - // Either our hands are still attached (just disabled) or they're gone entirely - return owner.num_hands > 0 ? SIGN_ARMS_DISABLED : SIGN_ARMLESS - - // Now let's see how many of our hands is holding something - var/busy_hands = 0 - // Yes held_items can contain null values, which represents empty hands, - // I'm just saving myself a variable cast by using as anything - for(var/obj/item/held_item as anything in owner.held_items) - // items like slappers/zombie claws/etc. should be ignored - if(isnull(held_item) || held_item.item_flags & HAND_ITEM) - continue - - busy_hands++ - - // Handcuffed or otherwise restrained - can't talk - if(HAS_TRAIT(owner, TRAIT_RESTRAINED)) - return SIGN_CUFFED - // Some other trait preventing us from using our hands now - else if(HAS_TRAIT(owner, TRAIT_HANDS_BLOCKED) || HAS_TRAIT(owner, TRAIT_EMOTEMUTE)) - return SIGN_TRAIT_BLOCKED - - // Okay let's compare the total hands to the number of busy hands - // to see how many we have left to use for signing right now - var/actually_usable_hands = total_hands - busy_hands - if(actually_usable_hands <= 0) - return SIGN_HANDS_FULL - if(actually_usable_hands == 1) - return SIGN_ONE_HAND - - return SIGN_OKAY - -//Thank you Jwapplephobia for helping me with the literal hellcode below //Shoutout to Jwapplephobia -/obj/item/organ/internal/tongue/tied/modify_speech(datum/source, list/speech_args) - // The message we send instead of our normal one - var/new_message - // The original message - var/message = speech_args[SPEECH_MESSAGE] - // Is there a ! - var/exclamation_found = findtext(message, "!") - // Is there a ? - var/question_found = findtext(message, "?") - new_message = message - if(exclamation_found) - new_message = replacetext(new_message, "!", ".") - if(question_found) - new_message = replacetext(new_message, "?", ".") - speech_args[SPEECH_MESSAGE] = new_message - - // Cut our last overlay before we replace it - if(timeleft(tonal_timerid) > 0) - remove_tonal_indicator() - deltimer(tonal_timerid) - // Prioritize questions - if(question_found) - tonal_indicator = mutable_appearance('icons/mob/effects/talk.dmi', "signlang1", TYPING_LAYER) - owner.visible_message(span_notice("[owner] lowers [owner.p_their()] eyebrows.")) - else if(exclamation_found) - tonal_indicator = mutable_appearance('icons/mob/effects/talk.dmi', "signlang2", TYPING_LAYER) - owner.visible_message(span_notice("[owner] raises [owner.p_their()] eyebrows.")) - // If either an exclamation or question are found - if(!isnull(tonal_indicator) && owner.client?.typing_indicators) - owner.add_overlay(tonal_indicator) - tonal_timerid = addtimer(CALLBACK(src, PROC_REF(remove_tonal_indicator)), 2.5 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE | TIMER_STOPPABLE | TIMER_DELETE_ME) - else // If we're not gonna use it, just be sure we get rid of it - tonal_indicator = null - -/obj/item/organ/internal/tongue/tied/proc/remove_tonal_indicator() - if(isnull(tonal_indicator)) - return - owner.cut_overlay(tonal_indicator) - tonal_indicator = null - -#undef SIGN_OKAY -#undef SIGN_ONE_HAND -#undef SIGN_HANDS_FULL -#undef SIGN_ARMLESS -#undef SIGN_ARMS_DISABLED -#undef SIGN_TRAIT_BLOCKED -#undef SIGN_CUFFED diff --git a/icons/hud/actions.dmi b/icons/hud/actions.dmi index 569a9766708..d6c46d880f2 100644 Binary files a/icons/hud/actions.dmi and b/icons/hud/actions.dmi differ diff --git a/modular_skyrat/master_files/code/datums/traits/negative.dm b/modular_skyrat/master_files/code/datums/traits/negative.dm index bf8d96fce5a..2f6a6378249 100644 --- a/modular_skyrat/master_files/code/datums/traits/negative.dm +++ b/modular_skyrat/master_files/code/datums/traits/negative.dm @@ -75,19 +75,3 @@ value = -4 mob_trait = TRAIT_NOGUNS icon = "none" - -/datum/quirk/mute - name = "Mute" - desc = "Due to some accident, medical condition, or simply by choice, you are completely unable to speak." - value = -2 // HALP MAINTS - gain_text = span_danger("You find yourself unable to speak!") - lose_text = span_notice("You feel a growing strength in your vocal chords.") - medical_record_text = "Functionally mute, patient is unable to use their voice in any capacity." - -/datum/quirk/mute/add() - var/mob/living/carbon/human/user = quirk_holder - user.gain_trauma(new /datum/brain_trauma/severe/mute, TRAUMA_RESILIENCE_ABSOLUTE) - -/datum/quirk/mute/remove() - var/mob/living/carbon/human/user = quirk_holder - user?.cure_trauma_type(/datum/brain_trauma/severe/mute, TRAUMA_RESILIENCE_ABSOLUTE) diff --git a/modular_skyrat/modules/customization/modules/jobs/_job.dm b/modular_skyrat/modules/customization/modules/jobs/_job.dm index f11c36bee91..bce13cad92a 100644 --- a/modular_skyrat/modules/customization/modules/jobs/_job.dm +++ b/modular_skyrat/modules/customization/modules/jobs/_job.dm @@ -24,7 +24,9 @@ if(banned_quirks) for(var/Q in pref.all_quirks) if(banned_quirks[Q]) - return TRUE + var/exception = RESTRICTED_QUIRKS_EXCEPTIONS[Q] + if (!exception || !pref.all_quirks.Find(exception)) + return TRUE return FALSE /datum/job/proc/has_banned_species(datum/preferences/pref) diff --git a/tgstation.dme b/tgstation.dme index ef74f3bbdea..7c556868a15 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -759,6 +759,7 @@ #include "code\datums\actions\mobs\meteors.dm" #include "code\datums\actions\mobs\mobcooldown.dm" #include "code\datums\actions\mobs\projectileattack.dm" +#include "code\datums\actions\mobs\sign_language.dm" #include "code\datums\actions\mobs\small_sprite.dm" #include "code\datums\actions\mobs\transform_weapon.dm" #include "code\datums\actions\mobs\sequences\dash_attack.dm" @@ -953,6 +954,7 @@ #include "code\datums\components\shrink.dm" #include "code\datums\components\shy.dm" #include "code\datums\components\shy_in_room.dm" +#include "code\datums\components\sign_language.dm" #include "code\datums\components\simple_access.dm" #include "code\datums\components\singularity.dm" #include "code\datums\components\sitcomlaughter.dm" @@ -1829,6 +1831,7 @@ #include "code\game\objects\items\food\spaghetti.dm" #include "code\game\objects\items\granters\_granters.dm" #include "code\game\objects\items\granters\oragami.dm" +#include "code\game\objects\items\granters\sign_language.dm" #include "code\game\objects\items\granters\crafting\_crafting_granter.dm" #include "code\game\objects\items\granters\crafting\bone_notes.dm" #include "code\game\objects\items\granters\crafting\cannon.dm"