mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-12 02:32:10 +00:00
296 lines
13 KiB
Plaintext
296 lines
13 KiB
Plaintext
/// 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)
|
|
|
|
/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
|
|
var/obj/item/organ/internal/tongue/tongue = carbon_parent.get_organ_slot(ORGAN_SLOT_TONGUE)
|
|
if(tongue)
|
|
tongue.temp_say_mod = "signs"
|
|
//this speech relies on hands, which we have our own way of garbling speech when they're occupied, so we can have this always on
|
|
ADD_TRAIT(carbon_parent, TRAIT_SPEAKS_CLEARLY, SPEAKING_FROM_HANDS)
|
|
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_CARBON_GAIN_ORGAN, PROC_REF(on_added_organ))
|
|
RegisterSignal(carbon_parent, COMSIG_MOB_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_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))
|
|
RegisterSignal(carbon_parent, COMSIG_MOB_TRY_INVOKE_SPELL, PROC_REF(can_cast_spell))
|
|
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
|
|
var/obj/item/organ/internal/tongue/tongue = carbon_parent.get_organ_slot(ORGAN_SLOT_TONGUE)
|
|
if(tongue)
|
|
tongue.temp_say_mod = ""
|
|
REMOVE_TRAIT(carbon_parent, TRAIT_SPEAKS_CLEARLY, SPEAKING_FROM_HANDS)
|
|
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_CARBON_GAIN_ORGAN,
|
|
COMSIG_MOB_TRY_SPEECH,
|
|
COMSIG_LIVING_TREAT_MESSAGE,
|
|
COMSIG_MOVABLE_USING_RADIO,
|
|
COMSIG_MOVABLE_SAY_QUOTE,
|
|
COMSIG_MOB_SAY,
|
|
COMSIG_MOB_TRY_INVOKE_SPELL,
|
|
))
|
|
return TRUE
|
|
|
|
///Signal proc for [COMSIG_CARBON_GAIN_ORGAN]
|
|
///Applies the new say mod to any tongues that have appeared!
|
|
/datum/component/sign_language/proc/on_added_organ(mob/living/source, obj/item/organ/new_organ)
|
|
SIGNAL_HANDLER
|
|
|
|
if(!istype(new_organ, /obj/item/organ/internal/tongue))
|
|
return
|
|
var/obj/item/organ/internal/tongue/new_tongue = new_organ
|
|
new_tongue.temp_say_mod = "signs"
|
|
|
|
/// Signal proc for [COMSIG_MOB_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(HAS_MIND_TRAIT(carbon_parent, TRAIT_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_IGNORE_CAN_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(carbon_parent, TRAIT_RESTRAINED))
|
|
return SIGN_CUFFED
|
|
// Some other trait preventing us from using our hands now
|
|
else if(HAS_TRAIT(carbon_parent, TRAIT_HANDS_BLOCKED) || HAS_TRAIT(carbon_parent, 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
|
|
|
|
/**
|
|
* Check if we can sign the given spell
|
|
*
|
|
* Checks to make sure the spell is not a mime spell, and that we are able to physically cast the spell.
|
|
* Arguments:
|
|
* * mob/living/carbon/source - the caster of the spell
|
|
* * datum/action/cooldown/spell/spell - the spell we are trying to cast
|
|
* * feedback - whether or not a message should be displayed in chat
|
|
* *
|
|
* * returns SPELL_INVOCATION_FAIL or SPELL_INVOCATION_SUCCESS
|
|
*/
|
|
/datum/component/sign_language/proc/can_cast_spell(mob/living/carbon/source, datum/action/cooldown/spell/spell, feedback)
|
|
SIGNAL_HANDLER
|
|
var/mob/living/carbon/carbon_parent = parent
|
|
if(spell.invocation_type == INVOCATION_EMOTE) // Mime spells are not cast with signs
|
|
return NONE // Run normal checks
|
|
else if(check_signables_state() != SIGN_OKAY || HAS_MIND_TRAIT(carbon_parent, TRAIT_MIMING)) // Cannot cast if miming or not SIGN_OKAY
|
|
if(feedback)
|
|
to_chat(carbon_parent, span_warning("You can't sign the words to invoke [spell]!"))
|
|
return SPELL_INVOCATION_FAIL
|
|
|
|
return SPELL_INVOCATION_ALWAYS_SUCCEED
|
|
|
|
/// 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_ARG] = stars(message_args[TREAT_MESSAGE_ARG])
|
|
|
|
message_args[TREAT_TTS_MESSAGE_ARG] = ""
|
|
|
|
/// 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_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
|
|
|
|
// remove the ! and ? symbols from message at the end
|
|
message = sanitize_message(message)
|
|
speech_args[SPEECH_MESSAGE] = message
|
|
|
|
/// 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
|