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"