From 4b3c8aa60262be2c397e057e6b2f95632a5633fa Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Wed, 23 Nov 2022 18:07:11 +0100 Subject: [PATCH] [MIRROR] Sign Language Refactor & Bugfixes [MDB IGNORE] (#17591) * Sign Language Refactor & Bugfixes * Hotfix for Upstream Merge 71265 (#17594) * Remove Body Purist which was added upstream by PR #71229 * Remove Tongue Tied tongue from BlueShift * Remove Skyrat's Mute quirk * Add job-quirk-quirk exceptions list * Add fix from upstream Co-authored-by: Dani Glore --- .../map_files/Blueshift/BlueShift_middle.dmm | 1 - .../signals_atom/signals_atom_movable.dm | 20 ++ code/__DEFINES/traits.dm | 2 +- code/__DEFINES/~skyrat_defines/jobs.dm | 2 + code/_globalvars/lists/maintenance_loot.dm | 1 + code/datums/actions/mobs/sign_language.dm | 92 +++++++ code/datums/components/sign_language.dm | 256 ++++++++++++++++++ code/datums/quirks/good.dm | 17 ++ code/datums/quirks/negative.dm | 11 + code/datums/quirks/neutral.dm | 44 --- .../objects/effects/spawners/random/exotic.dm | 1 + .../objects/items/granters/sign_language.dm | 29 ++ code/game/say.dm | 3 + .../ruins/lavalandruin_code/syndicate_base.dm | 2 +- .../mob/living/carbon/human/human_say.dm | 7 - code/modules/mob/living/living_say.dm | 6 + code/modules/surgery/organs/tongue.dm | 182 ------------- icons/hud/actions.dmi | Bin 8396 -> 8581 bytes .../code/datums/traits/negative.dm | 16 -- .../customization/modules/jobs/_job.dm | 4 +- tgstation.dme | 3 + 21 files changed, 446 insertions(+), 253 deletions(-) create mode 100644 code/datums/actions/mobs/sign_language.dm create mode 100644 code/datums/components/sign_language.dm create mode 100644 code/game/objects/items/granters/sign_language.dm 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 569a976670823b53272fbfd290da322c67b37b79..d6c46d880f24c2e4b6b8512460ff20f24e74f8be 100644 GIT binary patch literal 8581 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 iuGRL4hll@{