// Defines are in code\__DEFINES\emotes.dm
/// Sentinel for emote stats.
/// If this is set for max stat, then its value will be ignored.
#define DEFAULT_MAX_STAT_ALLOWED "defaultstat"
/**
* # Emote
*
* Most of the text that's not someone talking is based off of this.
*
*/
/datum/emote
/// What calls the emote.
var/key = ""
/// This will also call the emote.
var/key_third_person = ""
/// 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.
var/message_simple = ""
/// Message to display if the user is a spooky observer ghost.
var/message_observer = ""
/// Sounds emitted when the user is muzzled. Generally used like "[user] makes a pick(muzzled_noises) noise!"
var/muzzled_noises = list("strong", "weak")
/// Message with %t at the end to allow adding params to the message, like for mobs doing an emote relatively to something else.
/// Set this to EMOTE_PARAM_USE_POSTFIX to just use the postfix.
var/message_param = ""
/// Message postfix with %t used when we don't want to use message_param for our targeting. Used for things like message_monkey or message_mime.
/// Punctuation from the message will be stripped when this is applied, so make sure it's punctuated as well.
var/message_postfix = ""
/// Description appended to the emote name describing what the target should be, like for help commands.
var/param_desc = "target"
/// Whether the emote is visible or audible.
var/emote_type = EMOTE_VISIBLE
/// Checks if the mob can use its hands before performing the emote.
var/hands_use_check = FALSE
/// If the emote type is EMOTE_MOUTH but should still bypass a muzzle.
var/muzzle_ignore = 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
/// Species types which the emote will be exclusively available to. Should be subclasses of /datum/species
var/list/species_type_whitelist_typecache
/// Species types which the emote will be exclusively not available to. Should be subclasses of /datum/species
var/list/species_type_blacklist_typecache
/// If we get a target, how do we want to treat it?
var/target_behavior = EMOTE_TARGET_BHVR_USE_PARAMS_ANYWAY
/// If our target behavior isn't to ignore, what should we look for with targets?
var/emote_target_type = EMOTE_TARGET_ANY
// Stat_allowed is the "lower threshold" for stat, and basically represents how alive you have to be to use it.
// on the other hand, max_stat_allowed is the "upper threshold" representing how 'dead' you can be while still using the emote.
// see stat.dm for the stats that you can actually use here.
// Typical use case might be setting unintentional_stat_allowed to UNCONSCIOUS to allow a mob to gasp whether or not they're conscious.
// A use case for max_stat alone would be in case you'd want someone to be able to do something while unconscious or dead. Unlikely, but the option will still be there.
// A use case for both at once would be fixing it somewhere in the middle, like only allowing mobs to snore while they're unconscious.
// (worth noting: this is flexible on purpose in case we ever increase the amount of life stats).
/// How conscious do you need to be to use this emote intentionally?
var/stat_allowed = CONSCIOUS
/// How unconscious/dead can you be while still being able to use this emote intentionally?
/// If this is set to DEFAULT_STAT_ALLOWED, it'll behave as if it isn't set.
var/max_stat_allowed = DEFAULT_MAX_STAT_ALLOWED
/// How conscious do you need to be to have this emote forced out of you?
var/unintentional_stat_allowed = CONSCIOUS
/// Same as above, how unconscious/dead do you need to be to have this emote forced out of you?
/// If this is set to DEFAULT_STAT_ALLOWED, it'll behave as if it isn't set.
var/max_unintentional_stat_allowed = DEFAULT_MAX_STAT_ALLOWED
/// Sound to play when emote is called. If you want to adjust this dynamically, see get_sound().
var/sound
/// Whether or not to vary the sound of the emote.
var/vary = FALSE
/// Whether or not to adjust the frequency of the emote sound based on age.
var/age_based = FALSE
/// If true, this emote will only make a sound effect when called unintentionally.
var/only_forced_audio = FALSE
/// Whether or not the emote can even be called at all if it's not intentional
var/only_unintentional = FALSE
/// The cooldown between the uses of the emote.
var/cooldown = DEFAULT_EMOTE_COOLDOWN
/// How long is the cooldown on the audio of the emote, if it has one?
var/audio_cooldown = AUDIO_EMOTE_COOLDOWN
/// If the emote is triggered unintentionally, how long would that cooldown be?
var/unintentional_audio_cooldown = AUDIO_EMOTE_UNINTENTIONAL_COOLDOWN
/// If true, an emote will completely bypass any cooldown when called unintentionally. Necessary for things like deathgasp.
var/bypass_unintentional_cooldown = FALSE
/// How loud is the audio emote?
var/volume = 50
/datum/emote/New()
if(message_param && !param_desc)
CRASH("emote [src] was given a message parameter without a description.")
if(ispath(mob_type_allowed_typecache))
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)
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)
species_type_whitelist_typecache = typecacheof(species_type_whitelist_typecache)
species_type_blacklist_typecache = typecacheof(species_type_blacklist_typecache)
/datum/emote/Destroy(force)
if(force)
return ..()
else
// if you're deleting an emote something has gone wrong
return QDEL_HINT_LETMELIVE
/**
* Handles the modifications and execution of emotes.
*
* In general, what this does:
* - Checks if the user can run the emote at all
* - Checks and applies the message parameter, if it exists
* - Replaces pronouns with a mob's specific pronouns
* - Checks for and plays sound if the emote supports it
* - Sends the emote to users
* - Runechats the emote
*
* You most likely want to use try_run_emote() anywhere you would otherwise call this directly,
* as that will incorporate can_run_emote() checking as well.
*
* 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).
*
* Returns TRUE if it was able to run the emote, FALSE otherwise.
*/
/datum/emote/proc/run_emote(mob/user, params, type_override, intentional = FALSE)
. = TRUE
var/msg = select_message_type(user, message, intentional)
if(params && message_param)
// In this case, we did make some changes to the message that will be used, and we want to add the postfix on with the new parameters.
// This is applicable to things like mimes, who this lets have a target on their canned emote responses.
// Note that we only do this if we would otherwise have a message param, meaning there should be some target by default.
// If we're using EMOTE_PARAM_USE_POSTFIX, we don't want to bother specifying a message_param and just want to use the postfix for everything.
if(message_param == EMOTE_PARAM_USE_POSTFIX || (msg != message && message_postfix))
if(!message_postfix)
CRASH("Emote was specified to use postfix but message_postfix is empty.")
msg = select_param(user, params, "[remove_ending_punctuation(msg)] [message_postfix]", msg)
else if(msg == message)
// In this case, we're not making any substitutions in select_message_type, but we do have some params we want to sub in.
msg = select_param(user, params, message_param, message)
// If this got propogated up, jump out.
if(msg == EMOTE_ACT_STOP_EXECUTION)
return TRUE
if(isnull(msg))
to_chat(user, "'[params]' isn't a valid parameter for [key].")
return TRUE
msg = replace_pronoun(user, msg)
var/suppressed = FALSE
// Keep em quiet if they can't speak
if(!can_vocalize_emotes(user) && (emote_type & (EMOTE_MOUTH | EMOTE_AUDIBLE)))
var/noise_emitted = pick(muzzled_noises)
suppressed = TRUE
msg = "makes \a [noise_emitted] noise."
var/tmp_sound = get_sound(user)
var/sound_volume = get_volume(user)
// If our sound emote is forced by code, don't worry about cooldowns at all.
if(tmp_sound && should_play_sound(user, intentional) && sound_volume > 0)
if(bypass_unintentional_cooldown || user.start_audio_emote_cooldown(intentional, intentional ? audio_cooldown : unintentional_audio_cooldown))
play_sound_effect(user, intentional, tmp_sound, sound_volume)
if(msg)
user.create_log(EMOTE_LOG, msg)
if(isobserver(user))
log_ghostemote(msg, user)
else
log_emote(msg, user)
var/displayed_msg = "[user] [msg]"
var/user_turf = get_turf(user)
if(user.client && !isobserver(user))
for(var/mob/ghost as anything in GLOB.dead_mob_list)
if(!ghost.client)
continue
if((ghost.client.prefs.toggles & PREFTOGGLE_CHAT_GHOSTSIGHT) && !(ghost in viewers(user_turf, null)))
ghost.show_message("[user] ([ghost_follow_link(user, ghost)]) [msg]")
if(isobserver(user))
for(var/mob/dead/observer/ghost in viewers(user))
ghost.show_message("[displayed_msg]", EMOTE_VISIBLE)
else if((emote_type & EMOTE_AUDIBLE) && !user.mind?.miming)
user.audible_message(displayed_msg, deaf_message = "You see how [user] [msg]")
else
user.visible_message(displayed_msg, blind_message = "You hear how someone [msg]")
if(!((emote_type & EMOTE_FORCE_NO_RUNECHAT) || suppressed) && !isobserver(user))
runechat_emote(user, msg)
SEND_SIGNAL(user, COMSIG_MOB_EMOTED(key), src, key, emote_type, message, intentional)
SEND_SIGNAL(user, COMSIG_MOB_EMOTE, key, intentional)
/**
* Try to run an emote, checking can_run_emote once before executing the emote itself.
*
* * user - User of the emote
* * params - Params of the emote to be passed to run_emote
* * type_override - emote type to override the existing one with, if given.
* * intentional - Whether or not the emote was triggered intentionally (if false, the emote was forced by code).
*
* Returns TRUE if the emote was able to be run (or failed successfully), or FALSE if the emote is unusable.
*/
/datum/emote/proc/try_run_emote(mob/user, params, type_override, intentional = FALSE)
// You can use this signal to block execution of emotes from components/other sources.
var/sig_res = SEND_SIGNAL(user, COMSIG_MOB_PREEMOTE, key, intentional)
switch(sig_res)
if(COMPONENT_BLOCK_EMOTE_UNUSABLE)
return FALSE
if(COMPONENT_BLOCK_EMOTE_SILENT)
return TRUE
. = run_emote(user, params, type_override, intentional)
// safeguard in case these get modified
message = initial(message)
message_param = initial(message_param)
/**
* Play the sound effect in an emote.
* If you want to change the way the playsound call works, override this.
* Note! If you want age_based to work, you need to force vary to TRUE.
* * user - The user of the emote.
* * intentional - Whether or not the emote was triggered intentionally.
* * sound_path - Filesystem path to the audio clip to play.
* * sound_volume - Volume at which to play the audio clip.
*/
/datum/emote/proc/play_sound_effect(mob/user, intentional, sound_path, sound_volume)
if(age_based && ishuman(user))
var/mob/living/carbon/human/H = user
// Vary needs to be true as otherwise frequency changes get ignored deep within playsound_local :(
playsound(user.loc, sound_path, sound_volume, TRUE, frequency = H.get_age_pitch(H.dna.species.max_age))
else
playsound(user.loc, sound_path, sound_volume, vary)
/**
* Send an emote to runechat for all (listening) users in the vicinity.
*
* * user - The user of the emote.
* * text - The text of the emote.
*/
/datum/emote/proc/runechat_emote(mob/user, text)
var/runechat_text = text
if(length(text) > 100)
runechat_text = "[copytext(text, 1, 101)]..."
var/list/can_see = get_mobs_in_view(1, user) //Allows silicon & mmi mobs carried around to see the emotes of the person carrying them around.
can_see |= viewers(user, null)
for(var/mob/O in can_see)
if(O.status_flags & PASSEMOTES)
for(var/obj/item/holder/H in O.contents)
H.show_message(text, EMOTE_VISIBLE)
for(var/mob/living/M in O.contents)
M.show_message(text, EMOTE_VISIBLE)
if(O.client?.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT)
O.create_chat_message(user, runechat_text, symbol = RUNECHAT_SYMBOL_EMOTE)
/**
* Check whether or not an emote can be used due to a cooldown.
* This applies to per-emote cooldowns, preventing individual emotes from being used (intentionally) too frequently.
* This also checks audio cooldowns, so that intentional uses of audio emotes across the mob are time-constrained.
*
* 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(!intentional)
return TRUE
// if our emote would play sound but another audio emote is on cooldown, prevent this emote from being used.
// Note that this only applies to intentional emotes
if(get_sound(user) && should_play_sound(user, intentional) && !user.can_use_audio_emote(intentional))
return FALSE
var/cooldown_in_use
if(!isnull(user.emote_cooldown_override))
// if the user has a a cooldown override in place, apply that instead.
cooldown_in_use = user.emote_cooldown_override
else
cooldown_in_use = cooldown
// Check cooldown on a per-emote basis.
if(user.emotes_used && user.emotes_used[src] + cooldown_in_use > world.time)
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.
/**
* Get the volume of the audio emote to play.
*
* Override this if you want to dynamically change the volume of an emote.
*
* Arguments:
* * user - Person that is trying to send the emote.
*
* Returns the volume level for an emote's audio component.
*/
/datum/emote/proc/get_volume(mob/living/user)
return volume
/**
* Replace pronouns in the inputed string with the user's proper pronouns.
*
* Specifically replaces they/them/their pronouns with the user's pronouns, as well as %s (like theirs)
*
* 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, "themselves"))
msg = replacetext(msg, "themselves", user.p_themselves())
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)
. = msg
if(user.mind && user.mind.miming && message_mime)
. = message_mime
if(isalienadult(user) && message_alien)
. = message_alien
else if(islarva(user) && message_larva)
. = message_larva
else if(issilicon(user) && message_robot)
. = message_robot
else if(isAI(user) && message_AI)
. = message_AI
else if(ismonkeybasic(user) && message_monkey)
. = message_monkey
else if(isanimal(user) && message_simple)
. = message_simple
else if(isobserver(user) && message_observer)
. = message_observer
/**
* Replaces the %t in the message in message_param by params.
*
* The behavior of this proc is particularly dependent on `target_behavior` and `emote_target_type`.
* If target_behavior is EMOTE_TARGET_BHVR_RAW, we ignore any sort of target searching.
* Otherwise, we try to find a target in view to call this emote on based on emote_target_type.
*
*
* If you want to call something on the target object itself while it's still in scope, override act_on_target().
*
*
* Arguments:
* * user - Person that is trying to send the emote.
* * params - Parameters added after the emote.
* * substitution_str - String to substitute the target into.
* * base_message - If passed, the original message before any sort of modification occurred. Useful when dealing with non-standard message types.
*
* Returns the modified string, or null if the given parameter is invalid. May also return EMOTE_ACT_STOP_EXECUTION if acting on the target should stop emote execution.
*/
/datum/emote/proc/select_param(mob/user, params, substitution, base_message)
if(target_behavior == EMOTE_TARGET_BHVR_RAW)
return replacetext(substitution, "%t", params)
if(target_behavior == EMOTE_TARGET_BHVR_NUM)
if(!isnum(text2num(params)))
return null
act_on_target(user, text2num(params))
return replacetext(substitution, "%t", params)
var/full_target = find_target(user, params, emote_target_type)
if(full_target)
// If we find an actual target obj/item/whatever, see if we'd want to perform some action on it and jump out
// Fire off a signal first to see if our interaction should be stopped for some reason
if(!(SEND_SIGNAL(user, COMSIG_MOB_EMOTE_AT, full_target, key) & COMPONENT_BLOCK_EMOTE_ACTION))
if(act_on_target(user, full_target) == EMOTE_ACT_STOP_EXECUTION)
return EMOTE_ACT_STOP_EXECUTION
return replacetext(substitution, "%t", full_target)
// no target found, contingency plans
switch(target_behavior)
if(EMOTE_TARGET_BHVR_MUST_MATCH)
return null
if(EMOTE_TARGET_BHVR_DEFAULT_TO_BASE)
return base_message
if(EMOTE_TARGET_BHVR_USE_PARAMS_ANYWAY)
return replacetext(substitution, "%t", params)
CRASH("Emote tried to select_param with invalid target behavior.")
/**
* Perform an action on the target of an emote, if one was found.
*
* This gets called in select_param if a valid object target was found, and should let you interact with the
* object being targeted while it's still in scope.
*
* * user - Person who is triggering the emote.
* * Target - The target of the emote itself.
*/
/datum/emote/proc/act_on_target(mob/user, target)
return
/**
* 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).
*
* 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)
. = TRUE
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(ishuman(user))
var/mob/living/carbon/human/H = user
if(H.dna)
// Since the typecaches might be null as a valid option, it looks like we do need to check that these exist first.
if(species_type_whitelist_typecache && !is_type_in_typecache(H.dna.species, species_type_whitelist_typecache))
return FALSE
if(species_type_blacklist_typecache && is_type_in_typecache(H.dna.species, species_type_blacklist_typecache))
return FALSE
if(intentional && only_unintentional)
return FALSE
if(check_mute(user.client?.ckey, MUTE_EMOTE))
to_chat(user, "You cannot send emotes (muted).")
return FALSE
if(status_check && !is_type_in_typecache(user, mob_type_ignore_stat_typecache))
var/intentional_stat_check = (intentional && (user.stat <= stat_allowed && (max_stat_allowed == DEFAULT_MAX_STAT_ALLOWED || user.stat >= max_stat_allowed)))
var/unintentional_stat_check = (!intentional && (user.stat <= unintentional_stat_allowed && (max_unintentional_stat_allowed == DEFAULT_MAX_STAT_ALLOWED || user.stat >= max_unintentional_stat_allowed)))
if(!intentional_stat_check && !unintentional_stat_check)
var/stat = stat_to_text(user.stat)
if(!intentional)
return FALSE
if(stat)
to_chat(user, "You cannot [key] while [stat]!")
return FALSE
if(HAS_TRAIT(user, TRAIT_FAKEDEATH))
// Don't let people blow their cover by mistake
return FALSE
if(hands_use_check && !user.can_use_hands() && (iscarbon(user)))
if(!intentional)
return FALSE
to_chat(user, "You cannot use your hands to [key] right now!")
return FALSE
if(isliving(user))
var/mob/living/sender = user
if(HAS_TRAIT(sender, TRAIT_EMOTE_MUTE) && intentional)
return FALSE
else
// deadchat handling
if(check_mute(user.client?.ckey, MUTE_DEADCHAT))
to_chat(user, "You cannot send deadchat emotes (muted).")
return FALSE
if(!(user.client?.prefs.toggles & PREFTOGGLE_CHAT_DEAD))
to_chat(user, "You have deadchat muted.")
return FALSE
if(!check_rights(R_ADMIN, FALSE, user))
if(!GLOB.dsay_enabled)
to_chat(user, "Deadchat is globally muted.")
return FALSE
/**
* Find a target for the emote based on the message parameter fragment passed in.
*
* * user - The user looking for a target.
* * fragment - The mesage parameter or fragment of text they're using to try to find a target.
* * emote_target_type - Define denoting the type of target to use when searching.
*
* Returns a matched target, or null if a specific match couldn't be made.
*/
/datum/emote/proc/find_target(mob/user, fragment, emote_target_type)
var/target = null
fragment = lowertext(fragment)
if(emote_target_type & EMOTE_TARGET_MOB)
for(var/mob/living/M in view(user.client))
if(findtext(lowertext(M.name), fragment))
target = M
break
if(!target && emote_target_type & EMOTE_TARGET_OBJ)
for(var/obj/thing in view(user.client))
if(findtext(lowertext(thing.name), fragment))
target = thing
break
return target
/**
* Return whether a user should be able to vocalize emotes or not, due to a mask or inability to speak.
* If this returns false, any mouth emotes will be replaced with muzzled noises.
*/
/datum/emote/proc/can_vocalize_emotes(mob/user)
if(user.mind?.miming)
// mimes get special treatment; though they can't really "vocalize" we don't want to replace their message.
return TRUE
if(!muzzle_ignore && !user.can_speak())
return FALSE
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(only_forced_audio && intentional)
return FALSE
if((emote_type & EMOTE_MOUTH) && !can_vocalize_emotes(user))
return FALSE
if(isliving(user))
var/mob/living/liveuser = user
if(liveuser.has_status_effect(STATUS_EFFECT_ABSSILENCED))
return FALSE
return TRUE
/datum/emote/proc/remove_ending_punctuation(msg)
var/static/list/end_punctuation = list(".", "?", "!")
if(copytext(msg, -1) in end_punctuation)
msg = copytext(msg, 1, -1)
return msg
/**
* 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.
*/
/mob/proc/manual_emote(text) //Just override the song and dance
. = TRUE
if(stat != CONSCIOUS)
return FALSE
if(!text)
CRASH("Someone passed nothing to manual_emote(), fix it")
log_emote(text, src)
create_log(EMOTE_LOG, text)
var/ghost_text = "[src] [text]"
var/origin_turf = get_turf(src)
if(client)
for(var/mob/ghost as anything in GLOB.dead_mob_list)
if(!ghost.client)
continue
if(ghost.client.prefs.toggles & PREFTOGGLE_CHAT_GHOSTSIGHT && !(ghost in viewers(origin_turf, null)))
ghost.show_message("[ghost_follow_link(src, ghost)] [ghost_text]")
visible_message(text)
#undef DEFAULT_MAX_STAT_ALLOWED