Files
Bubberstation/code/datums/emotes.dm
nevimer b348b617a3 Merge branch 'master' of https://github.com/tgstation/tgstation into pupstream-2025-09-07
# Conflicts:
#	README.md
#	code/__DEFINES/admin.dm
#	code/__DEFINES/melee.dm
#	code/_globalvars/traits/_traits.dm
#	code/controllers/subsystem/economy.dm
#	code/datums/components/crafting/crafting.dm
#	code/datums/elements/crusher_loot.dm
#	code/modules/antagonists/pirate/pirate_shuttle_equipment.dm
#	code/modules/clothing/suits/_suits.dm
#	code/modules/escape_menu/leave_body.dm
#	code/modules/jobs/job_types/_job.dm
#	code/modules/mining/equipment/mineral_scanner.dm
#	code/modules/mob/living/living.dm
#	code/modules/plumbing/plumbers/pill_press.dm
#	tgui/packages/tgui/interfaces/Vending.tsx
2025-09-07 00:37:52 -04:00

481 lines
17 KiB
Plaintext

/**
* # Emote
*
* Most of the text that's not someone talking is based off of this.
*
* Yes, the displayed message is stored on the datum, it would cause problems
* for emotes with a message that can vary, but that's handled differently in
* run_emote(), so be sure to use can_message_change if you plan to have
* different displayed messages from player to player.
*
*/
/datum/emote
/// What calls the emote.
var/key = ""
/// This will also call the emote.
var/key_third_person = ""
/// Needed for more user-friendly emote names, so emotes with keys like "aflap" will show as "flap angry". Defaulted to key.
var/name = ""
/// Message displayed when emote is used.
var/message = ""
/// Message displayed if the user is a mime.
var/message_mime = ""
/// Message displayed if the user is a grown alien.
var/message_alien = ""
/// Message displayed if the user is an alien larva.
var/message_larva = ""
/// Message displayed if the user is a robot.
var/message_robot = ""
/// Message displayed if the user is an AI.
var/message_AI = ""
/// Message displayed if the user is a monkey.
var/message_monkey = ""
/// Message to display if the user is a simple_animal or basic mob.
var/message_animal_or_basic = ""
/// Message with %t at the end to allow adding params to the message, like for mobs doing an emote relatively to something else.
var/message_param = ""
/// Whether the emote is visible and/or audible bitflag
var/emote_type = EMOTE_VISIBLE
/// Checks if the mob can use its hands before performing the emote.
var/hands_use_check = FALSE
/// Types that are allowed to use that emote.
var/list/mob_type_allowed_typecache = /mob
/// Types that are NOT allowed to use that emote.
var/list/mob_type_blacklist_typecache
/// Types that can use this emote regardless of their state.
var/list/mob_type_ignore_stat_typecache
/// Trait that is required to use this emote.
var/trait_required
/// In which state can you use this emote? (Check stat.dm for a full list of them)
var/stat_allowed = CONSCIOUS
/// Sound to play when emote is called.
var/sound
/// Does this emote vary in pitch?
var/vary = FALSE
/// If this emote's sound is affected by TTS pitch
var/affected_by_pitch = TRUE
/// Can only code call this event instead of the player.
var/only_forced_audio = FALSE
/// The cooldown between the uses of the emote.
var/cooldown = 0.8 SECONDS
/// Does this message have a message that can be modified by the user?
var/can_message_change = FALSE
/// How long is the shared emote cooldown triggered by this emote?
var/general_emote_audio_cooldown = 2 SECONDS
/// How long is the specific emote cooldown triggered by this emote?
var/specific_emote_audio_cooldown = 5 SECONDS
/// Does this emote's sound ignore walls?
var/sound_wall_ignore = FALSE
/datum/emote/New()
switch(mob_type_allowed_typecache)
if(/mob)
mob_type_allowed_typecache = GLOB.typecache_mob
if(/mob/living)
mob_type_allowed_typecache = GLOB.typecache_living
else
mob_type_allowed_typecache = typecacheof(mob_type_allowed_typecache)
mob_type_blacklist_typecache = typecacheof(mob_type_blacklist_typecache)
mob_type_ignore_stat_typecache = typecacheof(mob_type_ignore_stat_typecache)
if(!name)
name = key
/**
* Handles the modifications and execution of emotes.
*
* Arguments:
* * user - Person that is trying to send the emote.
* * params - Parameters added after the emote.
* * type_override - Override to the current emote_type.
* * intentional - Bool that says whether the emote was forced (FALSE) or not (TRUE).
*
*/
/datum/emote/proc/run_emote(mob/user, params, type_override, intentional = FALSE)
var/msg = select_message_type(user, message, intentional)
if(params && message_param)
msg = select_param(user, params)
msg = replace_pronoun(user, msg)
if(!msg)
return
if(user.client)
user.log_message(msg, LOG_EMOTE)
var/tmp_sound = get_sound(user)
if(tmp_sound && should_play_sound(user, intentional) && TIMER_COOLDOWN_FINISHED(user, "general_emote_audio_cooldown") && TIMER_COOLDOWN_FINISHED(user, type))
TIMER_COOLDOWN_START(user, type, specific_emote_audio_cooldown)
TIMER_COOLDOWN_START(user, "general_emote_audio_cooldown", general_emote_audio_cooldown)
var/frequency = null
if (affected_by_pitch && SStts.tts_enabled && SStts.pitch_enabled)
frequency = rand(MIN_EMOTE_PITCH, MAX_EMOTE_PITCH) * (1 + sqrt(abs(user.pitch)) * SIGN(user.pitch) * EMOTE_TTS_PITCH_MULTIPLIER)
else if(vary)
frequency = rand(MIN_EMOTE_PITCH, MAX_EMOTE_PITCH)
playsound(source = user,soundin = tmp_sound,vol = 50, vary = FALSE, ignore_walls = sound_wall_ignore, frequency = frequency, volume_preference = /datum/preference/numeric/volume/sound_emote) // BUBBER EDIT CHANGE - Add volume pref - ORIGINAL: playsound(source = user,soundin = tmp_sound,vol = 50, vary = FALSE, ignore_walls = sound_wall_ignore, frequency = frequency)
var/is_important = emote_type & EMOTE_IMPORTANT
var/is_visual = emote_type & EMOTE_VISIBLE
var/is_audible = emote_type & EMOTE_AUDIBLE
var/additional_message_flags = get_message_flags(intentional)
var/space = should_have_space_before_emote(html_decode(msg)[1]) ? " " : "" // SKYRAT EDIT ADDITION
// Emote doesn't get printed to chat, runechat only
if(emote_type & EMOTE_RUNECHAT)
for(var/mob/viewer as anything in viewers(user))
if(isnull(viewer.client))
continue
if(!is_important && viewer != user && (!is_visual || !is_audible))
if(is_audible && !viewer.can_hear())
continue
if(is_visual && viewer.is_blind())
continue
// SKYRAT EDIT BEGIN - Pref checked emotes
if(!pref_check_emote(viewer))
continue
// SKYRAT EDIT END
if(user.runechat_prefs_check(viewer, EMOTE_MESSAGE))
viewer.create_chat_message(
speaker = user,
raw_message = msg,
runechat_flags = EMOTE_MESSAGE,
)
else if(is_important)
to_chat(viewer, span_emote("<b>[user]</b> [msg]"))
else if(is_audible && is_visual)
viewer.show_message(
span_emote("<b>[user]</b> [msg]"), MSG_AUDIBLE,
span_emote("You see how <b>[user]</b> [msg]"), MSG_VISUAL,
)
else if(is_audible)
viewer.show_message(span_emote("<b>[user]</b> [msg]"), MSG_AUDIBLE)
else if(is_visual)
viewer.show_message(span_emote("<b>[user]</b> [msg]"), MSG_VISUAL)
return // Early exit so no dchat message
// The emote has some important information, and should always be shown to the user
else if(is_important)
for(var/mob/viewer as anything in viewers(user))
// SKYRAT EDIT BEGIN - Pref checked emotes
if(!pref_check_emote(viewer))
continue
// SKYRAT EDIT END
to_chat(viewer, span_emote("<b>[user]</b> [msg]"))
if(user.runechat_prefs_check(viewer, EMOTE_MESSAGE))
viewer.create_chat_message(
speaker = user,
raw_message = msg,
runechat_flags = EMOTE_MESSAGE,
)
// Emotes has both an audible and visible component
// Prioritize audible, and provide a visible message if the user is deaf
else if(is_visual && is_audible)
user.audible_message(
message = msg,
deaf_message = span_emote("You see how <b>[user]</b> [msg]"),
self_message = msg,
audible_message_flags = EMOTE_MESSAGE|ALWAYS_SHOW_SELF_MESSAGE|additional_message_flags,
separation = space, // SKYRAT EDIT ADDITION
pref_to_check = pref_to_check // SKYRAT EDIT ADDITION - Pref checked emotes
)
// Emote is entirely audible, no visible component
else if(is_audible)
user.audible_message(
message = msg,
self_message = msg,
audible_message_flags = EMOTE_MESSAGE|additional_message_flags,
separation = space, // SKYRAT EDIT ADDITION
pref_to_check = pref_to_check // SKYRAT EDIT ADDITION - Pref checked emotes
)
// Emote is entirely visible, no audible component
else if(is_visual)
user.visible_message(
message = msg,
self_message = msg,
visible_message_flags = EMOTE_MESSAGE|ALWAYS_SHOW_SELF_MESSAGE|additional_message_flags,
separation = space, // SKYRAT EDIT ADDITION
pref_to_check = pref_to_check // SKYRAT EDIT ADDITION - Pref checked emotes
)
else
CRASH("Emote [type] has no valid emote type set!")
// SKYRAT EDIT -- BEGIN -- ADDITION -- AI QOL - RELAY EMOTES OVER HOLOPADS
var/obj/effect/overlay/holo_pad_hologram/hologram = GLOB.hologram_impersonators[user]
if(hologram)
if(is_important)
for(var/mob/living/viewer in viewers(world.view, hologram))
if(!pref_check_emote(viewer))
continue
to_chat(viewer, msg)
else if(is_visual && is_audible)
hologram.audible_message(
message = msg,
deaf_message = "<span class='emote'>You see how <b>[user]</b> [msg]</span>",
self_message = msg,
audible_message_flags = EMOTE_MESSAGE|ALWAYS_SHOW_SELF_MESSAGE,
separation = space,
pref_to_check = pref_to_check,
)
else if(is_audible)
hologram.audible_message(
message = msg,
self_message = msg,
audible_message_flags = EMOTE_MESSAGE,
separation = space,
pref_to_check = pref_to_check,
)
else if(is_visual)
hologram.visible_message(
message = msg,
self_message = msg,
visible_message_flags = EMOTE_MESSAGE|ALWAYS_SHOW_SELF_MESSAGE,
separation = space,
pref_to_check = pref_to_check,
)
// SKYRAT EDIT -- END
if(!isnull(user.client))
var/dchatmsg = "<b>[user]</b>[space][msg]" // SKYRAT EDIT - Better emotes - Original: var/dchatmsg = "<b>[user]</b> [msg]"
for(var/mob/ghost as anything in GLOB.dead_mob_list - viewers(get_turf(user)))
if(isnull(ghost.client) || isnewplayer(ghost))
continue
if(!(get_chat_toggles(ghost.client) & CHAT_GHOSTSIGHT))
continue
// SKYRAT EDIT BEGIN - Pref checked emotes
if(!pref_check_emote(ghost))
continue
// SKYRAT EDIT END
to_chat(ghost, span_emote("[FOLLOW_LINK(ghost, user)] [dchatmsg]"))
return
/**
* For handling emote cooldown, return true to allow the emote to happen.
*
* Arguments:
* * user - Person that is trying to send the emote.
* * intentional - Bool that says whether the emote was forced (FALSE) or not (TRUE).
*
* Returns FALSE if the cooldown is not over, TRUE if the cooldown is over.
*/
/datum/emote/proc/check_cooldown(mob/user, intentional)
if(SEND_SIGNAL(user, COMSIG_MOB_EMOTE_COOLDOWN_CHECK, src.key, intentional) & COMPONENT_EMOTE_COOLDOWN_BYPASS)
intentional = FALSE
if(!intentional)
return TRUE
if(user.emotes_used && user.emotes_used[src] + cooldown > world.time)
var/datum/emote/default_emote = /datum/emote
if(cooldown > initial(default_emote.cooldown)) // only worry about longer-than-normal emotes
// Original: to_chat(user, span_danger("You must wait another [DisplayTimeText(user.emotes_used[src] - world.time + cooldown)] before using that emote."))
user.balloon_alert(user, "on cooldown!") // BUBBER EDIT CHANGE
return FALSE
if(!user.emotes_used)
user.emotes_used = list()
user.emotes_used[src] = world.time
return TRUE
/**
* To get the sound that the emote plays, for special sound interactions depending on the mob.
*
* Arguments:
* * user - Person that is trying to send the emote.
*
* Returns the sound that will be made while sending the emote.
*/
/datum/emote/proc/get_sound(mob/living/user)
return sound //by default just return this var.
/**
* To get the flags visible/audible messages for ran by the emote.
*
* Arguments:
* * intentional - Bool that says whether the emote was forced (FALSE) or not (TRUE).
*
* Returns the additional message flags we should be using, if any.
*/
/datum/emote/proc/get_message_flags(intentional)
// If we did it, we most often already know what's in it, so we try to avoid highlight clutter.
return intentional ? BLOCK_SELF_HIGHLIGHT_MESSAGE : NONE
/**
* To replace pronouns in the inputed string with the user's proper pronouns.
*
* Arguments:
* * user - Person that is trying to send the emote.
* * msg - The string to modify.
*
* Returns the modified msg string.
*/
/datum/emote/proc/replace_pronoun(mob/user, msg)
if(findtext(msg, "their"))
msg = replacetext(msg, "their", user.p_their())
if(findtext(msg, "them"))
msg = replacetext(msg, "them", user.p_them())
if(findtext(msg, "they"))
msg = replacetext(msg, "they", user.p_they())
if(findtext(msg, "%s"))
msg = replacetext(msg, "%s", user.p_s())
return msg
/**
* Selects the message type to override the message with.
*
* Arguments:
* * user - Person that is trying to send the emote.
* * msg - The string to modify.
* * intentional - Bool that says whether the emote was forced (FALSE) or not (TRUE).
*
* Returns the new message, or msg directly, if no change was needed.
*/
/datum/emote/proc/select_message_type(mob/user, msg, intentional)
// Basically, we don't care that the others can use datum variables, because they're never going to change.
. = msg
if(!isliving(user))
return .
var/mob/living/living_user = user
if(HAS_MIND_TRAIT(user, TRAIT_MIMING) && message_mime)
. = message_mime
if(isalienadult(user) && message_alien)
. = message_alien
else if(islarva(user) && message_larva)
. = message_larva
else if(isAI(user) && message_AI)
. = message_AI
else if(ismonkey(user) && message_monkey)
. = message_monkey
else if((iscyborg(user) || (living_user.mob_biotypes & MOB_ROBOTIC)) && message_robot)
. = message_robot
else if(isanimal_or_basicmob(user) && message_animal_or_basic)
. = message_animal_or_basic
return .
/**
* Replaces the %t in the message in message_param by params.
*
* Arguments:
* * user - Person that is trying to send the emote.
* * params - Parameters added after the emote.
*
* Returns the modified string.
*/
/datum/emote/proc/select_param(mob/user, params)
return replacetext(message_param, "%t", params)
/**
* Check to see if the user is allowed to run the emote.
*
* Arguments:
* * user - Person that is trying to send the emote.
* * status_check - Bool that says whether we should check their stat or not.
* * intentional - Bool that says whether the emote was forced (FALSE) or not (TRUE).
* * params - Parameters added after the emote.
*
* Returns a bool about whether or not the user can run the emote.
*/
/datum/emote/proc/can_run_emote(mob/user, status_check = TRUE, intentional = FALSE, params)
if(trait_required && !HAS_TRAIT(user, trait_required))
return FALSE
if(!is_type_in_typecache(user, mob_type_allowed_typecache))
return FALSE
if(is_type_in_typecache(user, mob_type_blacklist_typecache))
return FALSE
if(status_check && !is_type_in_typecache(user, mob_type_ignore_stat_typecache))
if(user.stat > stat_allowed)
if(!intentional)
return FALSE
switch(user.stat)
if(SOFT_CRIT)
to_chat(user, span_warning("You cannot [key] while in a critical condition!"))
if(UNCONSCIOUS, HARD_CRIT)
to_chat(user, span_warning("You cannot [key] while unconscious!"))
if(DEAD)
to_chat(user, span_warning("You cannot [key] while dead!"))
return FALSE
if(hands_use_check && HAS_TRAIT(user, TRAIT_HANDS_BLOCKED))
if(!intentional)
return FALSE
to_chat(user, span_warning("You cannot use your hands to [key] right now!"))
return FALSE
if(HAS_TRAIT(user, TRAIT_EMOTEMUTE))
return FALSE
//SKYRAT EDIT BEGIN
if(allowed_species && ishuman(user))
var/mob/living/carbon/human/sender = user
if(sender.dna.species.type in allowed_species)
return TRUE
else
return FALSE
//SKYRAT EDIT END
return TRUE
/**
* Check to see if the user should play a sound when performing the emote.
*
* Arguments:
* * user - Person that is doing the emote.
* * intentional - Bool that says whether the emote was forced (FALSE) or not (TRUE).
*
* Returns a bool about whether or not the user should play a sound when performing the emote.
*/
/datum/emote/proc/should_play_sound(mob/user, intentional = FALSE)
if(emote_type & EMOTE_AUDIBLE && !hands_use_check)
if(HAS_TRAIT(user, TRAIT_MUTE))
return FALSE
if(ishuman(user))
var/mob/living/carbon/human/loud_mouth = user
if(HAS_MIND_TRAIT(loud_mouth, TRAIT_MIMING)) // vow of silence prevents outloud noises
return FALSE
if(!loud_mouth.get_organ_slot(ORGAN_SLOT_TONGUE))
return FALSE
if(only_forced_audio && intentional)
return FALSE
return TRUE
/**
* Allows the intrepid coder to send a basic emote
* Takes text as input, sends it out to those who need to know after some light parsing
* If you need something more complex, make it into a datum emote
* Arguments:
* * text - The text to send out
*
* Returns TRUE if it was able to run the emote, FALSE otherwise.
*/
/atom/proc/manual_emote(text, log_emote = TRUE)
if (!text)
CRASH("Someone passed nothing to manual_emote(), fix it")
if (log_emote)
log_message(text, LOG_EMOTE)
visible_message(text, visible_message_flags = EMOTE_MESSAGE)
return TRUE
/mob/manual_emote(text, log_emote = null)
if (stat != CONSCIOUS)
return FALSE
if (isnull(log_emote))
log_emote = !isnull(client)
. = ..(text, log_emote)
if (!.)
return FALSE
if (!client)
return TRUE
var/ghost_text = "<b>[src]</b> [text]"
var/origin_turf = get_turf(src)
for(var/mob/ghost as anything in GLOB.dead_mob_list)
if(!ghost.client || isnewplayer(ghost))
continue
if(get_chat_toggles(ghost.client) & CHAT_GHOSTSIGHT && !(ghost in viewers(origin_turf, null)))
ghost.show_message("[FOLLOW_LINK(ghost, src)] [ghost_text]")
return TRUE