/datum/emote - emote refactor, adds vocal cues (#6725)

datumizes and standardizes all emotes because that file is in the
fucking way of my saycode rewrite
adds vocal cues
This commit is contained in:
silicons
2025-11-14 17:54:43 -05:00
committed by GitHub
parent a6310604f0
commit 21883078ce
163 changed files with 2285 additions and 1308 deletions

View File

@@ -183,6 +183,7 @@
#include "code\__DEFINES\controllers\timer.dm"
#include "code\__DEFINES\datums\beam.dm"
#include "code\__DEFINES\datums\design.dm"
#include "code\__DEFINES\datums\emote.dm"
#include "code\__DEFINES\dcs\flags.dm"
#include "code\__DEFINES\dcs\helpers.dm"
#include "code\__DEFINES\dcs\components\riding.dm"
@@ -229,6 +230,7 @@
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob-clickchain.dm"
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob-inventory.dm"
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob-perspective.dm"
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob-say.dm"
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_combat.dm"
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_main.dm"
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_mobility.dm"
@@ -913,6 +915,24 @@
#include "code\datums\elements\clothing\dynamic_recolor.dm"
#include "code\datums\elements\items\darksight_granter.dm"
#include "code\datums\elements\items\hud_granter.dm"
#include "code\datums\emote\emote.dm"
#include "code\datums\emote\types\standard.dm"
#include "code\datums\emote\types\standard\basic.dm"
#include "code\datums\emote\types\standard\basic\animal_noise.dm"
#include "code\datums\emote\types\standard\basic\general.dm"
#include "code\datums\emote\types\standard\basic\machine_noise.dm"
#include "code\datums\emote\types\standard\basic\species.dm"
#include "code\datums\emote\types\standard\basic\synth_sfx.dm"
#include "code\datums\emote\types\standard\basic\general\blink.dm"
#include "code\datums\emote\types\standard\basic\general\clap.dm"
#include "code\datums\emote\types\standard\basic\general\cough.dm"
#include "code\datums\emote\types\standard\basic\general\salute.dm"
#include "code\datums\emote\types\standard\basic\general\sneeze.dm"
#include "code\datums\emote\types\standard\basic\general\twitch.dm"
#include "code\datums\emote\types\standard\basic\species\adherent.dm"
#include "code\datums\emote\types\standard\basic\species\promethean.dm"
#include "code\datums\emote\types\standard\basic\species\vox.dm"
#include "code\datums\emote\types\standard\basic\species\xenomorph.dm"
#include "code\datums\event_args\_event_args.dm"
#include "code\datums\event_args\actor.dm"
#include "code\datums\event_args\clickchain.dm"
@@ -994,6 +1014,7 @@
#include "code\datums\soundbytes\defs\explosion.dm"
#include "code\datums\soundbytes\defs\sparks.dm"
#include "code\datums\soundbytes\defs\spray.dm"
#include "code\datums\soundbytes\defs\talksound.dm"
#include "code\datums\soundbytes\defs\terminal_keyboard.dm"
#include "code\datums\soundbytes\effects\combat.dm"
#include "code\datums\soundbytes\guns\_guns.dm"
@@ -3770,6 +3791,8 @@
#include "code\modules\mob\mob-client.dm"
#include "code\modules\mob\mob-damage.dm"
#include "code\modules\mob\mob-defense.dm"
#include "code\modules\mob\mob-emote.dm"
#include "code\modules\mob\mob-emote_custom.dm"
#include "code\modules\mob\mob-examine.dm"
#include "code\modules\mob\mob-hands.dm"
#include "code\modules\mob\mob-iff.dm"
@@ -3780,6 +3803,9 @@
#include "code\modules\mob\mob-inventory-stripping.dm"
#include "code\modules\mob\mob-inventory.dm"
#include "code\modules\mob\mob-keybind-triggers.dm"
#include "code\modules\mob\mob-say-legacy.dm"
#include "code\modules\mob\mob-say-triggers.dm"
#include "code\modules\mob\mob-say.dm"
#include "code\modules\mob\mob-login.dm"
#include "code\modules\mob\mob-logout.dm"
#include "code\modules\mob\mob-melee_attack_chain.dm"
@@ -3795,7 +3821,6 @@
#include "code\modules\mob\perspective.dm"
#include "code\modules\mob\physiology.dm"
#include "code\modules\mob\pulling.dm"
#include "code\modules\mob\say.dm"
#include "code\modules\mob\say_vr.dm"
#include "code\modules\mob\ssd.dm"
#include "code\modules\mob\status_procs.dm"
@@ -3868,6 +3893,7 @@
#include "code\modules\mob\living\living-damage.dm"
#include "code\modules\mob\living\living-defense-legacy.dm"
#include "code\modules\mob\living\living-defense.dm"
#include "code\modules\mob\living\living-emote.dm"
#include "code\modules\mob\living\living-interaction.dm"
#include "code\modules\mob\living\living-inventory.dm"
#include "code\modules\mob\living\living-melee_attack_chain.dm"
@@ -3905,6 +3931,7 @@
#include "code\modules\mob\living\carbon\carbon-cpr.dm"
#include "code\modules\mob\living\carbon\carbon-damage.dm"
#include "code\modules\mob\living\carbon\carbon-defense.dm"
#include "code\modules\mob\living\carbon\carbon-emote.dm"
#include "code\modules\mob\living\carbon\carbon-hands.dm"
#include "code\modules\mob\living\carbon\carbon-interaction.dm"
#include "code\modules\mob\living\carbon\carbon-movement.dm"
@@ -3945,9 +3972,9 @@
#include "code\modules\mob\living\carbon\alien\larva\larva.dm"
#include "code\modules\mob\living\carbon\alien\larva\life.dm"
#include "code\modules\mob\living\carbon\alien\larva\progression.dm"
#include "code\modules\mob\living\carbon\brain\brain-emote.dm"
#include "code\modules\mob\living\carbon\brain\brain.dm"
#include "code\modules\mob\living\carbon\brain\death.dm"
#include "code\modules\mob\living\carbon\brain\emote.dm"
#include "code\modules\mob\living\carbon\brain\life.dm"
#include "code\modules\mob\living\carbon\brain\login.dm"
#include "code\modules\mob\living\carbon\brain\MMI.dm"
@@ -4019,6 +4046,7 @@
#include "code\modules\mob\living\silicon\silicon.dm"
#include "code\modules\mob\living\silicon\subystems.dm"
#include "code\modules\mob\living\silicon\translation.dm"
#include "code\modules\mob\living\silicon\ai\ai-custom_emote.dm"
#include "code\modules\mob\living\silicon\ai\ai-examine.dm"
#include "code\modules\mob\living\silicon\ai\ai.dm"
#include "code\modules\mob\living\silicon\ai\ai_movement.dm"
@@ -4349,6 +4377,8 @@
#include "code\modules\mob\observer\mobility.dm"
#include "code\modules\mob\observer\observer.dm"
#include "code\modules\mob\observer\perspective.dm"
#include "code\modules\mob\observer\dead\dead-emote.dm"
#include "code\modules\mob\observer\dead\dead-emote_custom.dm"
#include "code\modules\mob\observer\dead\dead.dm"
#include "code\modules\mob\observer\dead\free_vr.dm"
#include "code\modules\mob\observer\dead\logout.dm"

View File

@@ -12,7 +12,7 @@
/// NAME: must be a string
/// VALUE: the actual enum value, whatever it is
#define BITFIELD_NEW(NAME, VALUE) #NAME = ##VALUE
#define BITFIELD_NEW(NAME, VALUE) NAME = ##VALUE
/**
* * `ID` the same in `DECLARE_BITFIELD`

View File

@@ -0,0 +1,64 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
//* emote invocation output *//
/// no emote found
#define EMOTE_INVOKE_INVALID 0
/// the emote finished
#define EMOTE_INVOKE_FINISHED 1
/// the emote errored
#define EMOTE_INVOKE_ERRORED 2
/// the emote is running
#define EMOTE_INVOKE_SLEEPING 3
//* emote_class's ; determines who can do what emote *//
//* -- This is broadphase, effectively. *//
// todo: DEFINE_BITFIELD_NEW
/// require body
/// * so no brains
#define EMOTE_CLASS_IS_BODY (1<<0)
/// requires humanoid body
/// * so no four legged animals
#define EMOTE_CLASS_IS_HUMANOID (1<<1)
GLOBAL_REAL_LIST(emote_class_bit_descriptors) = list(
"requires body",
"requires humanoid",
)
//* emote_require's ; more freeform classes that should invoke procs *//
//* -- This is narrowphase and doesn't exclude emotes from listings, *//
//* generally. *//
// todo: DEFINE_BITFIELD_NEW
/// has a speaker of some kind
/// * implies `EMOTE_REQUIRE_VOCALIZATION`
#define EMOTE_REQUIRE_SYNTHETIC_SPEAKER (1<<0)
/// can talk coherent words
/// * implies `EMOTE_REQUIRE_VOCALIZATION`
#define EMOTE_REQUIRE_COHERENT_SPEECH (1<<1)
/// has a free hand
/// * does not imply the actor isn't stunned
#define EMOTE_REQUIRE_FREE_HAND (1<<2)
/// require being able to make sounds at all
#define EMOTE_REQUIRE_VOCALIZATION (1<<3)
GLOBAL_REAL_LIST(emote_require_bit_descriptors) = list(
"requires synthetic speaker",
"requires coherent speech",
"requires free hand",
"requires vocalization",
)
//* emote arbitrary key-value store key's *//
/// the original parameter string passed in.
#define EMOTE_PARAMETER_KEY_ORIGINAL "original"
/// the target. hard reference. don't hold onto it for too long.
#define EMOTE_PARAMETER_KEY_TARGET "target"
/// for basic emotes; custom parameter, as tokens
#define EMOTE_PARAMETER_KEY_CUSTOM_TOKENS "custom-tokens"
/// resolved target as /atom ref
#define EMOTE_PARAMETER_KEY_TARGET_RESOLVED "target-resolved"

View File

@@ -0,0 +1,7 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
// TODO: maybe this shouldn't be on mob?
// TODO: on saycode refactor this'll be rewritten to atom level saycode receive.
// called with (mob/from_mob, raw html, subtle, anti_ghost, saycode_type)
#define COMSIG_MOB_ON_RECEIVE_CUSTOM_EMOTE "mob-on_receive_custom_emote"

View File

@@ -22,10 +22,6 @@
//// From /mob/living/say(): ()
#define COMSIG_MOB_SAY "mob_say"
/// Sent on custom emote. (mob, raw_emote)
#define COMSIG_MOB_CUSTOM_EMOTE "custom_emote"
/// Sent on subtle/subtler. (mob, raw_emote)
#define COMSIG_MOB_SUBTLE_EMOTE "subtle_emote"
/// Sent when a mob/login() finishes: (client)
#define COMSIG_MOB_CLIENT_LOGIN "comsig_mob_client_login"
/// Sent when a mob/logout() begins: (client)

View File

@@ -1,3 +1,6 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/// works as long as can see
#define SAYCODE_TYPE_VISIBLE 1
/// works as long as can hear
@@ -8,3 +11,8 @@
#define SAYCODE_TYPE_LIVING 4
/// it just works
#define SAYCODE_TYPE_ALWAYS 5
/// special
///
/// * runtime / stack traces if used in actual saycode
/// * used in emotes for automatically generating both visible and audible messages
#define SAYCODE_TYPE_AUTO 6

View File

@@ -16,6 +16,7 @@
} \
} \
} while (0)
#define ADD_TRAIT_IN(target, trait, source, time) addtimer(CALLBACK(target, TYPE_PROC_REF(/datum, ___callbackaddtrait), trait, source), time);
#define REMOVE_TRAIT(target, trait, sources) \
do { \
var/list/_L = target.status_traits; \
@@ -39,21 +40,7 @@
}; \
} \
} while (0)
#define REMOVE_TRAITS_NOT_IN(target, sources) \
do { \
var/list/_L = target.status_traits; \
var/list/_S = sources; \
if (_L) { \
for (var/_T in _L) { \
_L[_T] &= _S;\
if (!length(_L[_T])) { \
_L -= _T } \
};\
if (!length(_L)) { \
target.status_traits = null\
};\
}\
} while (0)
#define REMOVE_TRAIT_IN(target, trait, source, time) addtimer(CALLBACK(target, TYPE_PROC_REF(/datum, ___callbackdeltrait), trait, source), time);
#define HAS_TRAIT(target, trait) (target.status_traits ? (target.status_traits[trait] ? TRUE : FALSE) : FALSE)
#define HAS_TRAIT_FROM(target, trait, source) (target.status_traits ? (target.status_traits[trait] ? (source in target.status_traits[trait]) : FALSE) : FALSE)
#define HAS_TRAIT_FROM_ONLY(target, trait, source) (\
@@ -64,6 +51,12 @@
: FALSE)
#define HAS_TRAIT_NOT_FROM(target, trait, source) (target.status_traits ? (target.status_traits[trait] ? (length(target.status_traits[trait] - source) > 0) : FALSE) : FALSE)
/datum/proc/___callbackaddtrait(trait, source)
ADD_TRAIT(src, trait, source)
/datum/proc/___callbackdeltrait(trait, source)
REMOVE_TRAIT(src, trait, source)
/// trait registration defines
/// due to how defines work, this goes AFTER the trait define!
#define DATUM_TRAIT(TYPE, TRAIT)

View File

@@ -52,8 +52,12 @@ DATUM_TRAIT(/mob, TRAIT_MOB_SLEEPING)
#define TRAIT_MOB_FORCED_STANDING "mob_forced_standing"
DATUM_TRAIT(/mob, TRAIT_MOB_FORCED_STANDING)
//? misc
//* Misc *//
/// Emote cooldown trait
#define TRAIT_EMOTE_COOLDOWN(KEY) "emote-cd-[KEY]"
/// Emote cooldown trait
#define TRAIT_EMOTE_GLOBAL_COOLDOWN "emote-cd"
/// Tracks whether you're a mime or not.
#define TRAIT_MIMING "miming"
DATUM_TRAIT(/mob, TRAIT_MIMING)

View File

@@ -582,7 +582,7 @@
if(isnewplayer(player_mob)) // exclude people in the lobby
continue
if(isobserver(player_mob)) // Ghosts are fine if they were playing once (didn't start as observers)
// var/mob/dead/observer/ghost_player = player_mob
// var/mob/observer/dead/ghost_player = player_mob
// if(ghost_player.started_as_observer) // Exclude people who started as observers
continue
active_players++

View File

@@ -6,6 +6,7 @@
var/list/viewrangelist = splittext(view,"x")
return list(text2num(viewrangelist[1]), text2num(viewrangelist[2]))
// TODO: optimize
/proc/world_view_max_number()
if(isnum(world.view))
return world.view

View File

@@ -2,11 +2,11 @@
//* Copyright (c) 2025 Citadel Station Developers *//
/**
* Replaces all instances of "$key" in a text string with the given variable.
* Replaces all instances of "%%key%%" in a text string with the given variable.
*
* Keys are provided as a key list associated to replacements.
*
* Example: string_format("$user hits $target with $weapon", list("user" = src, "target" = target, "weapon" = get_active_held_item()))
* Example: string_format("%%user%% hits $target with %%weapon%%", list("user" = src, "target" = target, "weapon" = get_active_held_item()))
*/
/proc/string_format(str, list/keys)
. = str
@@ -15,4 +15,4 @@
return
// todo: optimize this if needed, maybe dynamic regex?
for(var/key in keys)
. = replacetext_char(., "$[key]", keys[key])
. = replacetext_char(., "%%[key]%%", keys[key])

View File

@@ -5,7 +5,8 @@ SUBSYSTEM_DEF(early_init)
subsystem_flags = SS_NO_FIRE
/datum/controller/subsystem/early_init/Initialize()
init_inventory_slot_meta()
init_crayon_decal_meta()
init_bitfield_meta()
init_emote_meta()
init_crayon_decal_meta()
init_inventory_slot_meta()
return SS_INIT_SUCCESS

View File

@@ -11,16 +11,19 @@
var/div_slider = slidecolor
if(!i["allowed_edit"])
div_slider = "locked"
var/entry_name = html_encode(i["name"])
var/entry_checked = i["checked"]
output += {"<li>
<label class="switch">
<input type="[inputtype]" value="1" name="[i["name"]]"[i["checked"] ? " checked" : ""][i["allowed_edit"] ? "" : " onclick='return false' onkeydown='return false'"]>
<input type="[inputtype]" value="1" name="[entry_name]"[entry_checked ? " checked" : ""][i["allowed_edit"] ? "" : " onclick='return false' onkeydown='return false'"] />
<div class="slider [div_slider ? "[div_slider]" : ""]"></div>
<span>[i["name"]]</span>
</label>
</li>"}
else
for (var/i in values)
output += {"<li><input id="name="[i["name"]]"" style="width: 50px" type="[type]" name="[i["name"]]" value="[i["value"]]">
var/entry_name = html_encode(i["name"])
output += {"<li><input id="name="[i["name"]]"" style="width: 50px" type="[type]" name="[entry_name]" value="[i["value"]]" />
<label for="[i["name"]]">[i["name"]]</label></li>"}
output += {"</ul><div style="text-align:center">
<button type="submit" name="button" value="1" style="font-size:large;float:[( Button2 ? "left" : "right" )]">[Button1]</button>"}

270
code/datums/emote/emote.dm Normal file
View File

@@ -0,0 +1,270 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
// init'd by early init
GLOBAL_LIST(emotes)
// init'd by early init
GLOBAL_LIST(emote_lookup)
/proc/init_emote_meta()
GLOB.emotes = list()
GLOB.emote_lookup = list()
for(var/datum/emote/path as anything in subtypesof(/datum/emote))
if(initial(path.abstract_type) == path)
continue
var/datum/emote/instance = new path
GLOB.emotes += instance
for(var/binding in islist(instance.bindings) ? instance.bindings : list(instance.bindings))
if(GLOB.emote_lookup[binding])
var/datum/emote/existing = GLOB.emote_lookup[binding]
stack_trace("collision between [existing.type] and [instance.type] on [binding]")
break
GLOB.emote_lookup[binding] = instance
tim_sort(GLOB.emotes, /proc/cmp_name_asc)
/proc/fetch_emote(key) as /datum/emote
return GLOB.emote_lookup[key]
/**
* Emotes!
*
* * Custom /me emotes are not part of this. They're way too weird / expressive.
*/
/datum/emote
abstract_type = /datum/emote
/// emote name
var/name = "Unnamed Emote (Yell at Coders)"
/// emote desc, if any
var/desc = "An action you can perform with your character. Yell at coders to set descriptions if you see this."
/// our bindings key(s)
///
/// * can be a string or a list
/// * if a list, the first string in the list is the primary; rest are alises
/// * for now, emote bindings must be unique to a single emote. in the future, this may change.
var/bindings
/// a prefix to use before our binding
/// * applies to all bindings in bindings list if specified
var/binding_prefix
/// parameter help string rendered as "[binding] [parameter_description]"
var/parameter_description
/// our emote classes
/// * A mob must have all of these to use us.
/// * Missing a class will stop us from appearing in the mob's emote-help panel at all.
var/emote_class = NONE
/// our emote require's
/// * A mob must have all of these to use us.
/// * Unlike `emote_class`, not having this doesn't exclude the emote from the 'help' list,
/// as these are assumed to be more temporary.
var/emote_require = NONE
/// mobility flags needed to invoke (all of them are if set)
var/required_mobility_flags = MOBILITY_IS_CONSCIOUS
/// cannot invoke this again for x time
var/self_cooldown = 0 SECONDS
/datum/emote/New()
// preprocess bindings
if(binding_prefix)
if(islist(bindings))
for(var/i in 1 to length(bindings))
bindings[i] = "[binding_prefix]-[bindings[i]]"
else
bindings = "[binding_prefix]-[bindings]"
//* Checks *//
/**
* Fast check. Emits no errors; can_use() should have all checks in here as this is only used
* to do the menu check.
*/
/datum/emote/proc/can_potentially_use(datum/event_args/actor/actor, use_emote_class)
return (emote_class & use_emote_class) == emote_class
/**
* @params
* * actor - actor data
* * arbitrary - arbitrary processed params
* * out_reasons_fail - out reasons we can't be used right now
*/
/datum/emote/proc/can_use(datum/event_args/actor/actor, list/arbitrary, list/out_reasons_fail)
SHOULD_NOT_OVERRIDE(TRUE)
var/special_check = can_use_special(actor, arbitrary, out_reasons_fail)
if(!isnull(special_check))
return special_check
var/their_class = actor.performer.get_usable_emote_class()
var/their_require = actor.performer.get_usable_emote_require()
if((their_class & emote_class) != emote_class)
var/missing_class = emote_class & ~their_class
if(out_reasons_fail)
for(var/i in 1 to length(global.emote_class_bit_descriptors))
if((1<<(i - 1)) & missing_class)
out_reasons_fail?.Add(global.emote_class_bit_descriptors[i])
return FALSE
if((their_require & emote_require) != emote_require)
var/missing_require = emote_require & ~their_require
if(out_reasons_fail)
for(var/i in 1 to length(global.emote_require_bit_descriptors))
if((1<<(i - 1)) & missing_require)
out_reasons_fail?.Add(global.emote_require_bit_descriptors[i])
return FALSE
return TRUE
/**
* @params
* * actor - (optional) the provided actor
* * arbitrary - (optional) arbitrary processed params
* * out_reasons_fail - out reasons we can't be used right now
*
* @return non-null TRUE / FALSE to override [can_use()]
*/
/datum/emote/proc/can_use_special(datum/event_args/actor/actor, list/arbitrary, list/out_reasons_fail)
return
//* Execution *//
/**
* Process passed in string (so anything after a **single** space from the emote key) to an 'arbitrary',
* which is just how we process params
*
* @return the 'arbitrary' param passed into the rest of the emote call chain, null on fail
*/
/datum/emote/proc/process_parameters(parameter_string, datum/event_args/actor/actor, silent)
return list(EMOTE_PARAMETER_KEY_ORIGINAL = parameter_string)
/**
* Standard parameter tokenization. Allows double-quoting, single-quoting, and just space-ing
* @return list, or null on fail
*/
/datum/emote/proc/tokenize_parameters(parameter_string, datum/event_args/actor/actor, silent)
// incase they try something fishy
// notice the length(); curse of RA can be a problem.
if(length(parameter_string) >= MAX_MESSAGE_LEN)
loudly_reject_failure("---", actor, silent, "Parameters were too large; skipping emote.")
return null
if(!parameter_string)
return list()
var/len = length_char(parameter_string)
var/in_space = TRUE
var/active_border_char
var/active_token_pos
. = list()
// TODO: faster regex tokenizer?
for(var/pos in 1 to len)
var/char = parameter_string[pos]
var/is_border = FALSE
var/ignore_one
switch(char)
if("\"", "'")
if(in_space)
is_border = TRUE
ignore_one = TRUE
else if(char == active_border_char)
is_border = TRUE
ignore_one = TRUE
else
// being inside a token = ignored
continue
// no unicode spaces too bad
if(" ")
if(in_space)
// still in space just keep going
continue
else
if(active_border_char)
// special border char; keep going until finding another
continue
else
// we were in a word and aren't bounded by active border char;
// this is a border
is_border = TRUE
ignore_one = TRUE
// another character
else
// if we're in space and we find a non-special non-space
// it's the immediate start of a token
if(in_space)
is_border = TRUE
ignore_one = FALSE
if(is_border)
// we're at the start or an end of a token
if(in_space)
// start token
in_space = FALSE
active_token_pos = ignore_one ? pos + 1 : pos
switch(char)
if("\"", "'")
active_border_char = char
else
// end token
var/token = copytext_char(parameter_string, active_token_pos, pos + (ignore_one ? 0 : 1))
active_border_char = null
in_space = TRUE
. += token
// if we're in a token,
if(!in_space)
if(active_border_char)
// mismatched somewhere
loudly_reject_failure(parameter_string, actor, silent, "Mismatched '[active_border_char]' in parameters.")
return null
else
// was using space so just insert last
var/last_token = copytext_char(parameter_string, active_token_pos, len + 1)
. += last_token
/**
* Tries to run an emote, if someone's allowed to.
* * Blocking proc.
* @params
* * actor - person doing it
* * arbitrary - parsed arbitrary params from tokenized parameter string
* * silent - don't emit errors to actor
* * used_binding - binding used by user, if any
*/
/datum/emote/proc/try_run_emote(datum/event_args/actor/actor, list/arbitrary, silent, used_binding)
var/list/why_not = list()
var/can_run = can_use(actor, arbitrary, why_not)
if(!can_run)
if(!silent)
actor?.chat_feedback(SPAN_WARNING("You can't '[used_binding || name]' right now; ([english_list(why_not, "unknown reason")])"))
return FALSE
run_emote(actor, arbitrary, silent, used_binding)
return TRUE
/**
* Blocking proc.
*
* Runs an emote.
* * This can still fail if things fail to be parsed from arbitrary list.
*
* @params
* * actor - actor data
* * arbitrary - arbitrary processed params
* * silent - suppress errors
*
* @return pass / fail
*/
/datum/emote/proc/run_emote(datum/event_args/actor/actor, list/arbitrary, silent, used_binding)
return TRUE
/datum/emote/proc/get_mob_context(mob/invoking)
return invoking.emotes_running?[src]
/datum/emote/proc/set_mob_context(mob/invoking, ctx)
if(!invoking.emotes_running)
invoking.emotes_running = list()
invoking.emotes_running[src] = ctx
/datum/emote/proc/loudly_reject_failure(parameter_string, datum/event_args/actor/actor, silent, reason)
if(!silent)
actor?.chat_feedback(
SPAN_WARNING("Failed emote parse on {[parameter_string]} - [reason]"),
)

View File

@@ -0,0 +1,39 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/**
* General handling for SFX / VFX / a lot of other stuff
*/
/datum/emote/standard
abstract_type = /datum/emote/standard
//* SFX *//
/// sound to play, or a list of sounds to pick from (equal weight)
var/sfx
/// volume of sound to play
var/sfx_volume = 75
/// vary the sound?
var/sfx_vary = TRUE
/// extra range
///
/// todo: legacy; playsound should allow specifying range directly.
var/sfx_extra_range = 0
//* VFX *//
// todo: vfx support
/datum/emote/standard/run_emote(datum/event_args/actor/actor, list/arbitrary, silent, used_binding)
. = ..()
if(!.)
return
var/sfx = get_sfx(actor, arbitrary)
play_sfx(actor, arbitrary, sfx)
/**
* Gets a SFX to play.
*/
/datum/emote/standard/proc/get_sfx(datum/event_args/actor/actor, list/arbitrary)
return islist(sfx) ? (length(sfx) ? pick(sfx) : null) : sfx
/datum/emote/standard/proc/play_sfx(datum/event_args/actor/actor, list/arbitrary, sfx)
playsound(actor.performer, sfx, sfx_volume, sfx_vary)

View File

@@ -0,0 +1,224 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/**
* Basic emotes that just emit a simple message or something.
*
* * This is in reality just a generic emote subtype that handles most cases.
* * 'arbitrary' will be a list of key-values, with EMOTE_PARAMETER_KEY_*.
* * parameters are a list of strings; you can do strings with spaces with "string here". \" will escape doublequotes, \\ will escape a \
* * named parameters are not yet supported. it's too expensive to process these.
* * you are allowed one custom parameter in addition to default parameters generated by this type (like 'target').
* * the custom parameter will be under EMOTE_PARAMETER_KEY_CUSTOM.
*
* Params
*/
/datum/emote/standard/basic
abstract_type = /datum/emote/standard/basic
//* Feedback *//
/// outgoing saycode type flags
///
/// If set to AUTO, the following happens:
/// * If the hearer can see, normal / visible text is used
/// * If the hearer can't see an actor, 'someone / something' replaces that actor
/// * If the hearer cannot see, audible is used with no replacement.
///
/// If set to anything that isn't AUTO, the following happens:
/// * ALWAYS, LIVING, CONSCIOUS will always use visible string, never audible
/// * VISIBLE will always use visible string, never audible
/// * AUDIBLE will always use audible string, falling back to visible if it exists
var/feedback_saycode_type = SAYCODE_TYPE_AUTO
/**
* Override order as follows:
*
* 1. Miming, if miming
* 2. Muzzled, if muzzled
* 3. Targeted, if there is a target
* 4. Default
*/
/// if existing, and we're miming, we immediately switch saycode mode to visible if we're on auto
///
/// * %%USER%% is replaced with the user.
/// * If the replacement is at the start of the string, it'll be capitalized as needed.
/// * HTML is valid
var/feedback_special_miming
/// targeted version of miming
///
/// * %%USER%% is replaced with the user.
/// * %%TARGET%% is replaced with a target.
/// * If the replacement is at the start of the string, it'll be capitalized as needed.
/// * HTML is valid
var/feedback_special_miming_targeted
/// if existing, and we're muzzled, we immediately switch to this
///
/// * %%USER%% is replaced with the user.
/// * If the replacement is at the start of the string, it'll be capitalized as needed.
/// * HTML is valid
var/feedback_special_muzzled
/// targeted version of muzzled
///
/// * %%USER%% is replaced with the user.
/// * %%TARGET%% is replaced with a target.
/// * If the replacement is at the start of the string, it'll be capitalized as needed.
/// * HTML is valid
var/feedback_special_muzzled_targeted
/// audible descriptor for muzzled
///
/// * HTML is valid
var/feedback_special_muzzled_audible
/// the default feedback string
///
/// * If heard, audible has %%USER%% replaced with "someone" if the viewer is blind.
/// * If the replacement is at the start of the string, it'll be capitalized as needed.
/// * HTML is valid
var/feedback_default = "<b>%%USER%%</b> does something that isn't implemented by the coders. (Yell at coders, someone forgot a default string.)"
/// the default targeted feedback sting
///
/// * %%USER%% is replaced with the user.
/// * %%TARGET%% is replaced with a target.
/// * If the replacement is at the start of the string, it'll be capitalized as needed.
/// * HTML is valid
var/feedback_default_targeted
/// the default audible descriptor
///
/// * HTML is valid
var/feedback_default_audible = "You hear something that isn't implemented by the coders. (Yell at coders, someone forgot a default string.)"
//* Parameter *//
/// has a custom parameter
/// * if enabled, it goes *before* target
/// * this means that it must be specified to specify a target at all
var/parameter_custom = FALSE
/// your custom parameter help string
var/parameter_custom_description
//* Targeting *//
/// Allows targeting?
/// * overrides [target_required]
var/target_allowed = FALSE
/// requires a target?
var/target_required = FALSE
/// maximum distance target can be from us
var/target_range = 7
/// allow target self
var/target_allow_self = FALSE
/datum/emote/standard/basic/New()
if(isnull(parameter_description))
parameter_description = generate_parameter_description()
else
stack_trace("[NAMEOF(src, parameter_description)] was set, but it should be autogenerated!")
..()
/datum/emote/standard/basic/proc/generate_parameter_description()
return "[parameter_custom ? "\[[(parameter_custom_description || "Custom Parameter (Yell at coders!)")]\]" : ""]\
[target_allowed ? " \[target[target_required ? "" : "?"]\]" : ""]"
/datum/emote/standard/basic/process_parameters(parameter_string, datum/event_args/actor/actor, silent)
var/list/tokenized = tokenize_parameters(parameter_string, actor, silent)
if(tokenized == null)
return null
. = ..()
if(parameter_custom)
var/got_target
if(target_allowed)
if(target_required)
if(length(tokenized) <= 2)
loudly_reject_failure(parameter_string, actor, silent, "A target is required.")
return null
if(length(tokenized) >= 2)
.[EMOTE_PARAMETER_KEY_TARGET] = tokenized[2]
got_target = TRUE
if(length(tokenized) >= 1)
.[EMOTE_PARAMETER_KEY_CUSTOM_TOKENS] = tokenized.Copy(1, length(tokenized) + got_target ? 0 : 1)
else
if(target_allowed)
if(target_required)
if(length(tokenized) <= 1)
loudly_reject_failure(parameter_string, actor, silent, "A target is required.")
return null
if(length(tokenized) >= 1)
.[EMOTE_PARAMETER_KEY_TARGET] = tokenized[1]
/datum/emote/standard/basic/run_emote(datum/event_args/actor/actor, list/arbitrary, silent, used_binding)
var/target_string = arbitrary[EMOTE_PARAMETER_KEY_TARGET]
if(target_string)
var/mob/resolved
for(var/mob/maybe_target in hearers())
if(maybe_target == actor.performer && !target_allow_self)
continue
if(findtext(maybe_target.name, target_string))
resolved = maybe_target
break
if(!resolved)
if(!silent)
actor?.chat_feedback(SPAN_WARNING("No one around you's named '[target_string]'!"))
return FALSE
else
arbitrary[EMOTE_PARAMETER_KEY_TARGET_RESOLVED] = resolved
. = ..()
if(!.)
return
on_run_emote(
actor,
arbitrary,
arbitrary[EMOTE_PARAMETER_KEY_CUSTOM_TOKENS],
arbitrary[EMOTE_PARAMETER_KEY_TARGET_RESOLVED],
)
/datum/emote/standard/basic/proc/on_run_emote(datum/event_args/actor/actor, list/arbitrary, list/maybe_custom_param_tokens, atom/maybe_target)
var/template_string_visible = get_template_string(actor, arbitrary, FALSE)
var/template_string_audible = get_template_string(actor, arbitrary, TRUE)
var/list/template_parameters = get_template_parameters(actor, arbitrary)
var/out_visible = string_format(template_string_visible, template_parameters)
var/out_audible = string_format(template_string_audible, template_parameters)
var/list/mob/hearing_mobs = actor.performer.saycode_view_query(null, TRUE, FALSE)
// TODO: centralized observer pref check in saycode_view_query
var/optimize_this_later_max_number = world_view_max_number() + 2
// TODO: proper runechat stuff.
actor.performer.say_overhead(out_visible)
for(var/mob/hearing as anything in hearing_mobs)
if(isobserver(hearing))
if((get_dist(hearing, src) > optimize_this_later_max_number) && !hearing.get_preference_toggle(/datum/game_preference_toggle/observer/ghost_sight))
continue
if(hearing.is_blind())
if(hearing.is_deaf())
else
hearing.show_message(out_audible, SAYCODE_TYPE_ALWAYS)
else
hearing.show_message(out_visible, SAYCODE_TYPE_ALWAYS)
/**
* Get template parameters for 'feedback_' vars.
*/
/datum/emote/standard/basic/proc/get_template_parameters(datum/event_args/actor/actor, list/arbitrary)
. = list("USER" = actor.performer)
if(target_allowed)
var/atom/maybe_target = arbitrary[EMOTE_PARAMETER_KEY_TARGET_RESOLVED]
.["TARGET"] = maybe_target ? "[maybe_target]" : "something"
/datum/emote/standard/basic/proc/get_template_string(datum/event_args/actor/actor, list/arbitrary, audible)
// TODO: miming
// TODO: muzzled
if(audible)
return feedback_default_audible
if(target_allowed && arbitrary[EMOTE_PARAMETER_KEY_TARGET])
return feedback_default_targeted
return feedback_default
/**
* Processes a custom parameter
*/
/datum/emote/standard/basic/proc/process_custom_parameter(datum/event_args/actor/actor, string)
// default to just spitting the string back out
return string

View File

@@ -0,0 +1,73 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/datum/emote/standard/basic/animal_noise
abstract_type = /datum/emote/standard/basic/animal_noise
emote_require = EMOTE_REQUIRE_VOCALIZATION
// TODO: parameter to enable this / set frequency
sfx_vary = FALSE
/datum/emote/standard/basic/animal_noise/awoo
name = "Awoo"
desc = "Let out an awoo."
bindings = "awoo"
feedback_special_miming = "<b>%%USER%%</b> acts out an awoo."
feedback_default = "<b>%%USER%%</b> awoos!"
feedback_default_audible = "You hear an awoo."
sfx = 'sound/voice/awoo.ogg'
sfx_volume = 50
/datum/emote/standard/basic/animal_noise/bird_beep
name = "Bird Peep"
desc = "Peep like a bird."
bindings = list(
"peep",
"bird-peep",
)
feedback_special_miming = "<b>%%USER%%</b> bobs as if they're peeping like a bird."
feedback_default = "<b>%%USER%%</b> peeps like a bird!"
feedback_default_audible = "You hear a peep."
sfx = 'sound/voice/peep.ogg'
sfx_volume = 50
/datum/emote/standard/basic/animal_noise/clak
name = "Clak"
desc = "... Clak. What?."
bindings = "clak"
feedback_default = "<b>%%USER%%</b> <font color='grey' size='2'>CLAKS!</font>!"
feedback_default_audible = "You hear a <font color='grey' size='2'>CLAK</font>."
sfx = 'sound/spooky/boneclak.ogg'
sfx_volume = 50
/datum/emote/standard/basic/animal_noise/meow
name = "Meow"
desc = "Let out a meow."
bindings = "meow"
feedback_special_miming = "<b>%%USER%%</b> acts out a soft mrowl."
feedback_default = "<b>%%USER%%</b> mrowls!"
feedback_default_audible = "You hear a mrowl."
sfx = 'sound/voice/meow1.ogg'
sfx_volume = 50
/datum/emote/standard/basic/animal_noise/nya
name = "Nya"
desc = "Let out a nya."
bindings = "nya"
feedback_special_miming = "<b>%%USER%%</b> acts out a cartoony cat noise, whatever that means."
feedback_default = "<b>%%USER%%</b> lets out a nya!"
feedback_default_audible = "You hear an unrealistically cartoony cat noise."
sfx = 'sound/voice/nya.ogg'
sfx_volume = 50
/datum/emote/standard/basic/animal_noise/squeak_mouse
name = "Squeak (Mouse)"
desc = "Squeak like a mouse."
bindings = list(
"squeak",
"mouse-squeak",
)
feedback_special_miming = "<b>%%USER%%</b> acts out a soft squeak."
feedback_default = "<b>%%USER%%</b> squeaks!"
feedback_default_audible = "You hear a squeak."
sfx = 'sound/effects/mouse_squeak.ogg'
sfx_volume = 50

View File

@@ -0,0 +1,110 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/datum/emote/standard/basic/general
abstract_type = /datum/emote/standard/basic/general
/datum/emote/standard/basic/general/bow
name = "Bow"
desc = "Bow, or bow to someone."
bindings = "bow"
required_mobility_flags = MOBILITY_CAN_STAND
emote_class = EMOTE_CLASS_IS_HUMANOID
feedback_default = "<b>%%USER%%</b> bows."
feedback_default_targeted = "<b>%%USER%%</b> bows to %%TARGET%%."
target_allowed = TRUE
// todo: you need a face to do this
/datum/emote/standard/basic/general/blush
name = "Blush"
desc = "Blush, or blush at someone."
bindings = "blush"
emote_class = EMOTE_CLASS_IS_HUMANOID
feedback_default = "<b>%%USER%%</b> blushes."
feedback_default_targeted = "<b>%%USER%%</b> blushes at %%TARGET%%."
target_allowed = TRUE
// todo: you need a face to do this
/datum/emote/standard/basic/general/frown
name = "Frown"
desc = "Frown, or frown at someone."
bindings = "frown"
emote_class = EMOTE_CLASS_IS_HUMANOID
feedback_default = "<b>%%USER%%</b> frowns."
feedback_default_targeted = "<b>%%USER%%</b> frowns at %%TARGET%%."
target_allowed = TRUE
// todo: you need a head to do this
/datum/emote/standard/basic/general/nod
name = "Nod"
desc = "Nod, or nod at someone."
bindings = "nod"
emote_class = EMOTE_CLASS_IS_HUMANOID
feedback_default = "<b>%%USER%%</b> nods."
feedback_default_targeted = "<b>%%USER%%</b> nods at %%TARGET%%."
target_allowed = TRUE
/datum/emote/standard/basic/general/raise_hand
name = "Raise Hand"
desc = "Raise your hand."
bindings = "raise"
emote_class = EMOTE_CLASS_IS_HUMANOID
feedback_default = "<b>%%USER%%</b> raises a hand."
emote_require = EMOTE_REQUIRE_FREE_HAND
/datum/emote/standard/basic/general/shiver
name = "Shiver"
desc = "Shiver."
bindings = "shiver"
emote_class = EMOTE_CLASS_IS_HUMANOID
feedback_default = "<b>%%USER%%</b> shivers."
/datum/emote/standard/basic/general/shrug
name = "Shrug"
desc = "Shrug, or shrug at someone."
bindings = "shrug"
emote_class = EMOTE_CLASS_IS_HUMANOID
feedback_default = "<b>%%USER%%</b> shrugs."
feedback_default_targeted = "<b>%%USER%%</b> shrugs at %%TARGET%%."
target_allowed = TRUE
// todo: check has mouth
/datum/emote/standard/basic/general/smile
name = "Smile"
desc = "Smile, or smile at someone."
bindings = "smile"
emote_class = EMOTE_CLASS_IS_HUMANOID
feedback_default = "<b>%%USER%%</b> smiles."
feedback_default_targeted = "<b>%%USER%%</b> smiles at %%TARGET%%."
target_allowed = TRUE
// todo: check has mouth
/datum/emote/standard/basic/general/smooch
name = "Smooch"
desc = "Smooch someone standing next to you."
bindings = "smooch"
emote_class = EMOTE_CLASS_IS_HUMANOID
target_allowed = TRUE
target_required = TRUE
target_range = 1
feedback_default_targeted = "<b>%%USER%%</b> smooches %%TARGET%%."
/datum/emote/standard/basic/general/wave
name = "Wave"
desc = "Wave, or wave at someone."
bindings = "wave"
emote_class = EMOTE_CLASS_IS_HUMANOID
feedback_default = "<b>%%USER%%</b> waves."
feedback_default_targeted = "<b>%%USER%%</b> waves at %%TARGET%%."
target_allowed = TRUE
emote_require = EMOTE_REQUIRE_FREE_HAND
// todo: check has eyes
/datum/emote/standard/basic/general/wink
name = "Wink"
desc = "Wink at someone."
bindings = "wink"
emote_class = EMOTE_CLASS_IS_HUMANOID
feedback_default = "<b>%%USER%%</b> winks."
feedback_default_targeted = "<b>%%USER%%</b> winks at %TARGET."
target_allowed = TRUE

View File

@@ -0,0 +1,21 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
// todo: check has eyes
/datum/emote/standard/basic/general/blink
name = "Blink"
desc = "Blink your eyes."
bindings = "blink"
emote_class = EMOTE_CLASS_IS_HUMANOID
feedback_default = "<b>%%USER%%</b> blinks."
// todo: check has eyes
/datum/emote/standard/basic/general/blink_fast
name = "Blink Rapidly"
desc = "Rapidly blink your eyes."
bindings = list(
"blink_r",
"blink-fast",
)
emote_class = EMOTE_CLASS_IS_HUMANOID
feedback_default = "<b>%%USER%%</b> blinks rapidly."

View File

@@ -0,0 +1,34 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/datum/emote/standard/basic/general/clap
name = "Clap"
desc = "Clap."
bindings = "clap"
emote_class = EMOTE_CLASS_IS_HUMANOID
sfx = list(
'sound/misc/clapping.ogg',
'sound/voice/clap2.ogg',
'sound/voice/clap3.ogg',
'sound/voice/clap4.ogg',
)
sfx_volume = 75
feedback_default = "<b>%%USER%%</b> claps."
feedback_default_targeted = "<b>%%USER%%</b> claps for %%TARGET%%."
feedback_default_audible = "You hear clapping."
target_allowed = TRUE
/datum/emote/standard/basic/general/golfclap
name = "Clap Slowly"
desc = "Clap slowly, perhaps sarcastically."
bindings = list(
"golfclap",
"clap-slow",
)
emote_class = EMOTE_CLASS_IS_HUMANOID
sfx = 'sound/voice/golfclap.ogg'
sfx_volume = 75
feedback_default = "<b>%%USER%%</b> slowly claps."
feedback_default_targeted = "<b>%%USER%%</b> slowly claps for %%TARGET%%."
feedback_default_audible = "You hear sarcastic clapping."
target_allowed = TRUE

View File

@@ -0,0 +1,40 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/datum/emote/standard/basic/general/cough
emote_class = EMOTE_CLASS_IS_HUMANOID
name = "Cough"
desc = "Cough."
bindings = "cough"
emote_require = EMOTE_REQUIRE_VOCALIZATION
feedback_default = "<b>%%USER%%</b> coughs."
feedback_default_audible = "You hear a cough."
feedback_special_miming = "<b>%%USER%%</b> appears to cough."
feedback_special_muzzled = "<b>%%USER%%</b> makes a strong noise."
feedback_special_muzzled_audible = "You hear a strong noise."
/datum/emote/standard/basic/general/cough/get_sfx(datum/event_args/actor/actor, list/arbitrary)
var/robotic = FALSE
var/mob/casted_mob = actor.performer
if(iscarbon(casted_mob))
var/mob/living/carbon/casted_carbon = casted_mob
var/obj/item/organ/internal/maybe_lungs = casted_carbon.organs_by_name[O_LUNGS]
if(maybe_lungs.robotic >= ORGAN_ROBOT)
robotic = TRUE
else
if(casted_mob.isSynthetic())
robotic = TRUE
// yes another check because this will be modularized out later to a gender/robotic lookup,
// then a sound lookup.
if(iscarbon(casted_mob))
var/mob/living/carbon/casted_carbon = casted_mob
if(robotic)
if(casted_carbon.get_gender() == FEMALE)
return 'sound/effects/mob_effects/m_machine_cougha.ogg'
else
return 'sound/effects/mob_effects/f_machine_cougha.ogg'
else
if(casted_carbon.get_gender() == FEMALE)
return casted_carbon.species?.female_cough_sounds
else
return casted_carbon.species?.male_cough_sounds

View File

@@ -0,0 +1,13 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/datum/emote/standard/basic/general/salute
name = "Salute"
desc = "Salute at someone."
bindings = "salute"
emote_class = EMOTE_CLASS_IS_HUMANOID
sfx = 'sound/misc/salute.ogg'
sfx_volume = 60
target_allowed = TRUE
feedback_default = "<b>%%USER%%</b> salutes."
feedback_default_targeted = "<b>%%USER%%</b> salutes to %%TARGET%%."

View File

@@ -0,0 +1,40 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/datum/emote/standard/basic/general/sneeze
emote_class = EMOTE_CLASS_IS_HUMANOID
name = "Sneeze"
desc = "Sneeze."
bindings = "sneeze"
emote_require = EMOTE_REQUIRE_VOCALIZATION
feedback_default = "<b>%%USER%%</b> sneezes."
feedback_default_audible = "You hear a sneeze."
feedback_special_miming = "<b>%%USER%%</b> appears to sneeze."
feedback_special_muzzled = "<b>%%USER%%</b> makes a strange noise."
feedback_special_muzzled_audible = "You hear a strange noise."
/datum/emote/standard/basic/general/sneeze/get_sfx(datum/event_args/actor/actor, list/arbitrary)
var/robotic = FALSE
var/mob/casted_mob = actor.performer
if(iscarbon(casted_mob))
var/mob/living/carbon/casted_carbon = casted_mob
var/obj/item/organ/internal/maybe_lungs = casted_carbon.organs_by_name[O_LUNGS]
if(maybe_lungs.robotic >= ORGAN_ROBOT)
robotic = TRUE
else
if(casted_mob.isSynthetic())
robotic = TRUE
// yes another check because this will be modularized out later to a gender/robotic lookup,
// then a sound lookup.
if(iscarbon(casted_mob))
var/mob/living/carbon/casted_carbon = casted_mob
if(robotic)
if(casted_carbon.get_gender() == FEMALE)
return 'sound/effects/mob_effects/machine_sneeze.ogg'
else
return 'sound/effects/mob_effects/f_machine_sneeze.ogg'
else
if(casted_carbon.get_gender() == FEMALE)
return casted_carbon.species?.female_sneeze_sound
else
return casted_carbon.species?.male_sneeze_sound

View File

@@ -0,0 +1,21 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/datum/emote/standard/basic/general/twitch
name = "Twitch"
desc = "Twitch."
bindings = "twitch"
emote_class = EMOTE_CLASS_IS_HUMANOID
feedback_default = "<b>%%USER%%</b> twitches."
feedback_saycode_type = SAYCODE_TYPE_VISIBLE
/datum/emote/standard/basic/general/twitch/strong
name = "Twitch Violently"
desc = "Violently twitch."
bindings = list(
"twitch_v",
"twitch-violently",
)
emote_class = EMOTE_CLASS_IS_HUMANOID
feedback_default = "<b>%%USER%%</b> twitches violently."
feedback_saycode_type = SAYCODE_TYPE_VISIBLE

View File

@@ -0,0 +1,111 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/datum/emote/standard/basic/machine_noise
abstract_type = /datum/emote/standard/basic/machine_noise
emote_require = EMOTE_REQUIRE_SYNTHETIC_SPEAKER
target_allowed = TRUE
// TODO: parameter to enable this / set frequency
sfx_vary = FALSE
/datum/emote/standard/basic/machine_noise/beep
name = "Machine - Beep"
desc = "Emit a beep."
bindings = list(
"beep",
"synth-beep",
)
feedback_default = "<b>%%USER%%</b> beeps."
feedback_default_targeted = "<b>%%USER%%</b> beeps at %%TARGET%%."
sfx = 'sound/machines/twobeep.ogg'
// todo: name, targeted
/datum/emote/standard/basic/machine_noise/buzz
bindings = list(
"buzz",
"synth-buzz",
)
feedback_default = "<b>%%USER%%</b> buzzes."
sfx = 'sound/machines/buzz-sigh.ogg'
// todo: name, targeted
/datum/emote/standard/basic/machine_noise/buzz2
bindings = list(
"buzz2",
"synth-buzz2",
)
feedback_default = "<b>%%USER%%</b> buzzes twice."
sfx = 'sound/machines/buzz-two.ogg'
// todo: name, targeted
/datum/emote/standard/basic/machine_noise/chime
bindings = list(
"chime",
"synth-chime",
)
feedback_default = "<b>%%USER%%</b> chimes."
sfx = 'sound/machines/chime.ogg'
// todo: name, targeted
/datum/emote/standard/basic/machine_noise/dwoop
bindings = list(
"dwoop",
"synth-dwoop",
)
feedback_default = "<b>%%USER%%</b> chirps happily."
sfx = 'sound/machines/dwoop.ogg'
// todo: name, targeted
/datum/emote/standard/basic/machine_noise/warn
bindings = list(
"warn",
"synth-warn",
)
feedback_default = "<b>%%USER%%</b> blares an alarm."
sfx = 'sound/machines/warning-buzzer.ogg'
// todo: name, targeted
/datum/emote/standard/basic/machine_noise/ping
bindings = list(
"ping",
"synth-ping",
)
feedback_default = "<b>%%USER%%</b> pings."
sfx = 'sound/machines/ping.ogg'
// todo: name, targeted
/datum/emote/standard/basic/machine_noise/yes
bindings = list(
"yes",
"synth-yes",
)
feedback_default = "<b>%%USER%%</b> emits an affirmative blip."
sfx = 'sound/machines/synth_yes.ogg'
// todo: name, targeted
/datum/emote/standard/basic/machine_noise/no
bindings = list(
"no",
"synth-no",
)
feedback_default = "<b>%%USER%%</b> emits a negative blip."
sfx = 'sound/machines/synth_no.ogg'
// todo: name, targeted
/datum/emote/standard/basic/machine_noise/scary
bindings = list(
"scary",
"synth-scary",
)
feedback_default = "<b>%%USER%%</b> emits a disconcerting tone."
sfx = 'sound/machines/synth_scary.ogg'
// todo: name, targeted
/datum/emote/standard/basic/machine_noise/steam
bindings = list(
"steam",
"synth-steam",
)
feedback_default = "<b>%%USER%%</b> lets off some steam."
sfx = 'sound/machines/clockcult/steam_whoosh.ogg'
sfx_volume = 30

View File

@@ -0,0 +1,37 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/datum/emote/standard/basic/species
abstract_type = /datum/emote/standard/basic/species
emote_class = EMOTE_CLASS_IS_HUMANOID
/// required species id or list of ids
///
/// * these are ids, not uids.
var/required_species_id
/datum/emote/standard/basic/species/can_potentially_use(datum/event_args/actor/actor, use_emote_class)
if(!check_species(actor.performer))
return FALSE
return ..()
/datum/emote/standard/basic/species/can_use_special(datum/event_args/actor/actor, list/arbitrary, list/out_reasons_fail)
if(!required_species_id)
return ..()
if(!actor?.performer)
return ..()
if(!check_species(actor.performer))
out_reasons_fail?.Add("incorrect species")
return FALSE
return ..()
/datum/emote/standard/basic/species/proc/check_species(mob/actor)
var/mob/living/carbon/human/human = actor
if(!istype(human))
return FALSE
if(islist(required_species_id))
if(!(human.species.id in required_species_id))
return FALSE
else
if(!(human.species.id == required_species_id))
return FALSE
return TRUE

View File

@@ -0,0 +1,15 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/datum/emote/standard/basic/species/adherent
abstract_type = /datum/emote/standard/basic/species/adherent
required_species_id = /datum/species/adherent::id
/datum/emote/standard/basic/species/adherent/chime
bindings = list(
"achime",
"adherent-chime",
)
sfx = 'sound/machines/achime.ogg'
sfx_volume = 50
feedback_default = "<b>%%USER%%</b> chimes!"

View File

@@ -0,0 +1,21 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/datum/emote/standard/basic/species/promethean
abstract_type = /datum/emote/standard/basic/species/promethean
required_species_id = /datum/species/shapeshifter/promethean::id
/datum/emote/standard/basic/species/promethean/check_species(mob/actor)
if(istype(actor, /mob/living/simple_mob/slime))
return TRUE
return ..()
/datum/emote/standard/basic/species/promethean/squish
bindings = list(
"squish",
"slime-squish",
)
feedback_saycode_type = SAYCODE_TYPE_VISIBLE
feedback_default = "<b>%%USER%%</b> squishes."
sfx = 'sound/effects/slime_squish.ogg'
sfx_volume = 50

View File

@@ -0,0 +1,26 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/datum/emote/standard/basic/species/vox
abstract_type = /datum/emote/standard/basic/species/vox
required_species_id = /datum/species/vox::id
/datum/emote/standard/basic/species/vox/shriek_loud
bindings = list(
"shriekloud",
"vox-shriek-loud",
)
sfx = 'sound/voice/shrieksneeze.ogg'
sfx_volume = 50
feedback_default = "<b>%%USER%%</b> gives a short sharp shriek!"
/datum/emote/standard/basic/species/vox/shriek_quiet
bindings = list(
"shrieksoft",
"vox-shriek-soft",
)
sfx = 'sound/voice/shriekcough.ogg'
sfx_volume = 50
feedback_default = "<b>%%USER%%</b> gives a short, quieter shriek!"

View File

@@ -0,0 +1,149 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/datum/emote/standard/basic/species/xenomorph
abstract_type = /datum/emote/standard/basic/species/xenomorph
required_species_id = list(
/datum/species/xenos::id,
/datum/species/xenohybrid::id,
)
/datum/emote/standard/basic/species/promethean/check_species(mob/actor)
if(istype(actor, /mob/living/simple_mob/animal/space/xenomorph))
return TRUE
return ..()
/datum/emote/standard/basic/species/xenomorph/xhiss
feedback_default = "<b>%%USER%%</b> hisses!"
sfx = 'sound/voice/xenos/alien_hiss3.ogg'
sfx_volume = 50
bindings = list(
"xhiss",
"xeno-hiss",
)
/datum/emote/standard/basic/species/xenomorph/xroar
feedback_default = "<b>%%USER%%</b> roars!"
sfx = 'sound/voice/xenos/alien_roar1.ogg'
sfx_volume = 50
bindings = list(
"xroar",
"xeno-roar",
)
/datum/emote/standard/basic/species/xenomorph/xhiss2
feedback_default = "<b>%%USER%%</b> hisses!"
sfx = 'sound/voice/xenos/xhiss2.ogg'
sfx_volume = 50
bindings = list(
"xhiss2",
"xeno-hiss2",
)
/datum/emote/standard/basic/species/xenomorph/xhiss3
feedback_default = "<b>%%USER%%</b> hisses!"
sfx = 'sound/voice/xenos/xhiss3.ogg'
sfx_volume = 50
bindings = list(
"xhiss3",
"xeno-hiss3",
)
/datum/emote/standard/basic/species/xenomorph/xhiss4
feedback_default = "<b>%%USER%%</b> hisses!"
sfx = 'sound/voice/xenos/xhiss4.ogg'
sfx_volume = 50
bindings = list(
"xhiss4",
"xeno-hiss4",
)
/datum/emote/standard/basic/species/xenomorph/xhiss5
feedback_default = "<b>%%USER%%</b> hisses!"
sfx = 'sound/voice/xenos/xhiss5.ogg'
sfx_volume = 50
bindings = list(
"xhiss5",
"xeno-hiss5",
)
/datum/emote/standard/basic/species/xenomorph/xhiss6
feedback_default = "<b>%%USER%%</b> hisses!"
sfx = 'sound/voice/xenos/xhiss6.ogg'
sfx_volume = 50
bindings = list(
"xhiss6",
"xeno-hiss6",
)
/datum/emote/standard/basic/species/xenomorph/xroar1
feedback_default = "<b>%%USER%%</b> hisses!"
sfx = 'sound/voice/xenos/xroar1.ogg'
sfx_volume = 50
bindings = list(
"xroar1",
"xeno-roar1",
)
/datum/emote/standard/basic/species/xenomorph/xroar2
feedback_default = "<b>%%USER%%</b> hisses!"
sfx = 'sound/voice/xenos/xroar2.ogg'
sfx_volume = 50
bindings = list(
"xroar2",
"xeno-roar2",
)
/datum/emote/standard/basic/species/xenomorph/xroar3
feedback_default = "<b>%%USER%%</b> hisses!"
sfx = 'sound/voice/xenos/xroar3.ogg'
sfx_volume = 50
bindings = list(
"xroar3",
"xeno-roar3",
)
/datum/emote/standard/basic/species/xenomorph/xtalk1
feedback_default = "<b>%%USER%%</b> hisses!"
sfx = 'sound/voice/xenos/xtalk1.ogg'
sfx_volume = 50
bindings = list(
"xtalk1",
"xeno-talk1",
)
/datum/emote/standard/basic/species/xenomorph/xtalk2
feedback_default = "<b>%%USER%%</b> hisses!"
sfx = 'sound/voice/xenos/xtalk2.ogg'
sfx_volume = 50
bindings = list(
"xtalk2",
"xeno-talk2",
)
/datum/emote/standard/basic/species/xenomorph/xtalk3
feedback_default = "<b>%%USER%%</b> hisses!"
sfx = 'sound/voice/xenos/xtalk3.ogg'
sfx_volume = 50
bindings = list(
"xtalk3",
"xeno-talk3",
)
/datum/emote/standard/basic/species/xenomorph/xroar
feedback_default = "<b>%%USER%%</b> roars!"
sfx = 'sound/voice/xenos/alien_roar1.ogg'
sfx_volume = 50
bindings = list(
"xroar",
"xeno-roar",
)
/datum/emote/standard/basic/species/xenomorph/xgrowl
feedback_default = "<b>%%USER%%</b> growls!"
sfx = 'sound/voice/xenos/alien_growl1.ogg'
sfx_volume = 50
bindings = list(
"xgrowl",
"xeno-growl",
)

View File

@@ -0,0 +1,62 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/datum/emote/standard/basic/synth_sfx
abstract_type = /datum/emote/standard/basic/synth_sfx
emote_require = EMOTE_REQUIRE_SYNTHETIC_SPEAKER
required_mobility_flags = MOBILITY_IS_CONSCIOUS
// TODO: parameter to enable this / set frequency
sfx_vary = FALSE
/datum/emote/standard/basic/synth_sfx/honk
name = "Machine - Bike Horn"
desc = "Emit a bike horn's honk."
bindings = list(
"honk",
"synth-honk",
)
feedback_default = "<b>%%USER%%</b> honks."
feedback_default_targeted = "<b>%%USER%%</b> honks at %%TARGET%%."
sfx = 'sound/items/bikehorn.ogg'
target_allowed = TRUE
// todo; name
/datum/emote/standard/basic/synth_sfx/gameover
name = "Machine - Game Over"
bindings = list(
"gameover",
"synth-gameover",
)
feedback_default = "<b>%%USER%%</b> crumples, their chassis colder and more lifeless than usual."
sfx = 'sound/machines/synth_gameover.ogg'
// todo; name
/datum/emote/standard/basic/synth_sfx/xp_startup
name = "Machine - Startup (XP)"
bindings = list(
"startup",
"synth-startup",
)
feedback_default = "<b>%%USER%%</b> chimes to life."
sfx = 'sound/machines/synth_startup.ogg'
// todo; name
/datum/emote/standard/basic/synth_sfx/xp_shutdown
name = "Machine - Shutdown (XP)"
bindings = list(
"shutdown",
"synth-shutdown",
)
feedback_default = "<b>%%USER%%</b> emits a nostalgic tone as they fall silent."
sfx = 'sound/machines/synth_shutdown.ogg'
// todo; name, targeting
/datum/emote/standard/basic/synth_sfx/xp_error
name = "Machine - Error (XP)"
bindings = list(
"error",
"synth-error",
)
feedback_default = "<b>%%USER%%</b> experiences a system error."
sfx = 'sound/machines/synth_error.ogg'

View File

@@ -1,3 +1,6 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
/datum/soundbyte/clicker
name = "Clicker A"
id = "clicker-1"

View File

@@ -1,3 +1,6 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
/datum/soundbyte/explosion
name = "Explosion A"
id = "explosion-1"

View File

@@ -1,3 +1,6 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
/datum/soundbyte/sparks
name = "Sparks A"
id = "sparks-1"

View File

@@ -1,3 +1,6 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
/datum/soundbyte/spray
abstract_type = /datum/soundbyte/spray
is_sfx = TRUE

View File

@@ -0,0 +1,55 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
/datum/soundbyte/talksound
abstract_type = /datum/soundbyte/talksound
is_sfx = TRUE
/datum/soundbyte/talksound/goon_say
name = "Talksound - Goon Say"
id = "talksound-goon-say"
path = list(
'sound/soundbytes/talksounds/goon/speak_1.ogg',
'sound/soundbytes/talksounds/goon/speak_2.ogg',
'sound/soundbytes/talksounds/goon/speak_3.ogg',
'sound/soundbytes/talksounds/goon/speak_4.ogg',
)
/datum/soundbyte/talksound/goon_ask
name = "Talksound - Goon Ask"
id = "talksound-goon-ask"
path = list(
'sound/soundbytes/talksounds/goon/speak_1_ask.ogg',
'sound/soundbytes/talksounds/goon/speak_2_ask.ogg',
'sound/soundbytes/talksounds/goon/speak_3_ask.ogg',
'sound/soundbytes/talksounds/goon/speak_4_ask.ogg',
)
/datum/soundbyte/talksound/goon_exclaim
name = "Talksound - Goon Exclaim"
id = "talksound-goon-exclaim"
path = list(
'sound/soundbytes/talksounds/goon/speak_1_exclaim.ogg',
'sound/soundbytes/talksounds/goon/speak_2_exclaim.ogg',
'sound/soundbytes/talksounds/goon/speak_3_exclaim.ogg',
'sound/soundbytes/talksounds/goon/speak_4_exclaim.ogg',
)
/datum/soundbyte/talksound/generic_emote_1
name = "Talksound - Generic Emote 1"
id = "talksound-generic-emote-1"
path = list(
'sound/soundbytes/talksounds/generic/me_a.ogg',
'sound/soundbytes/talksounds/generic/me_b.ogg',
'sound/soundbytes/talksounds/generic/me_c.ogg',
'sound/soundbytes/talksounds/generic/me_d.ogg',
'sound/soundbytes/talksounds/generic/me_e.ogg',
'sound/soundbytes/talksounds/generic/me_f.ogg',
)
/datum/soundbyte/talksound/generic_subtle_emote_1
name = "Talksound - Generic Subtle Emote 1"
id = "talksound-generic-subtle-emote-1"
path = list(
'sound/soundbytes/talksounds/generic/subtle_sound.ogg',
)

View File

@@ -1,3 +1,6 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
/datum/soundbyte/terminal_keyboard
name = "Terminal Click"
id = "terminal-1"

View File

@@ -24,13 +24,17 @@
INVOKE_ASYNC(src, TYPE_PROC_REF(/atom/movable, animate_chat), message, null, FALSE, speech_bubble_hearers, 3 SECONDS)
/atom/proc/say_overhead(var/message, whispering, message_range = 7, var/datum/prototype/language/speaking = null, var/list/passed_hearing_list)
var/list/speech_bubble_hearers = list()
var/italics
if(whispering)
italics = TRUE
for(var/mob/M in get_mobs_in_view(message_range, src))
if(M.client)
speech_bubble_hearers += M.client
var/list/speech_bubble_hearers
if(passed_hearing_list)
speech_bubble_hearers = passed_hearing_list
else
speech_bubble_hearers = list()
for(var/mob/M in get_mobs_in_view(message_range, src))
if(M.client)
speech_bubble_hearers += M.client
if(length(speech_bubble_hearers))
INVOKE_ASYNC(src, TYPE_PROC_REF(/atom/movable, animate_chat), message, speaking, italics, speech_bubble_hearers, 30)

View File

@@ -24,23 +24,23 @@
/// * automatic message emit will not happen if this isn't set, even if self/audible are.
///
/// accepted vars:
/// * "$ATTACKER" - the person attacking
/// * "$ATTACKER_P_THEIR" - pronoun for 'their' for attacker
/// * "$TARGET" - the target
/// * "%%ATTACKER%%" - the person attacking
/// * "%%ATTACKER_P_THEIR%%" - pronoun for 'their' for attacker
/// * "%%TARGET%%" - the target
var/default_feedback_message
/// templateable message
///
/// accepted vars:
/// * "$ATTACKER" - the person attacking
/// * "$ATTACKER_P_THEIR" - pronoun for 'their' for attacker
/// * "$TARGET" - the target
/// * "%%ATTACKER%%" - the person attacking
/// * "%%ATTACKER_P_THEIR%%" - pronoun for 'their' for attacker
/// * "%%TARGET%%" - the target
var/default_feedback_message_self
/// templateable message
///
/// accepted vars:
/// * "$ATTACKER" - the person attacking
/// * "$ATTACKER_P_THEIR" - pronoun for 'their' for attacker
/// * "$TARGET" - the target
/// * "%%ATTACKER%%" - the person attacking
/// * "%%ATTACKER_P_THEIR%%" - pronoun for 'their' for attacker
/// * "%%TARGET%%" - the target
var/default_feedback_message_audible
/datum/combo/melee/New()

View File

@@ -71,7 +71,7 @@
else
item = "<a href='?_src_=vars;[HrefToken()];Vars=[REF(value)]'>[VV_HTML_ENCODE(name)] = /list ([L.len])</a>"
else if ((maybe_bitfield = fetch_bitfield(D?.type, name)))
else if (istype(D) && (maybe_bitfield = fetch_bitfield(D.type, name)))
var/list/flags = list()
for(var/i in 1 to maybe_bitfield.get_declared_count())
var/bit = maybe_bitfield.bits[i]

View File

@@ -302,7 +302,7 @@ datumrefresh=[refid];[HrefToken()]'>Refresh</a>
</body>
</html>
"}
src << browse(html, "window=variables[refid];size=475x650")
src << browse(html, "window=variables[refid];size=525x725")
/client/proc/vv_update_display(datum/D, span, content)
src << output("[span]:[content]", "variables[REF(D)].browser:replace_span")

View File

@@ -106,6 +106,11 @@
key = "mob_tooltips"
legacy_key = "MOB_TOOLTIPS"
/datum/game_preference_toggle/game/vocal_cues
name = "Vocal Cues on Chat"
key = "vocal_cues"
description = "Enables playing a sound on hearing someone speak / act."
/datum/game_preference_toggle/game/overhead_chat
name = "Overhead Chat"
key = "runechat"

View File

@@ -53,6 +53,9 @@ GLOBAL_VAR_INIT(game_view_y, 15)
// minimum size viewports can be
GLOBAL_VAR_INIT(min_client_view_x, 15)
GLOBAL_VAR_INIT(min_client_view_y, 15)
// the radius that things like saycode operates on
// todo: update this as needed
GLOBAL_VAR_INIT(game_view_radius, 9)
// these two variables, if set, lock all clients to a certain viewsize no matter what
GLOBAL_VAR(lock_client_view_x)
GLOBAL_VAR(lock_client_view_y)

View File

@@ -160,7 +160,7 @@
description = "Does a subtle emote that's invisible to ghosts."
/datum/keybinding/mob/subtler/down(client/user)
user.mob.subtler_wrapper()
user.mob.subtle_wrapper()
return TRUE
/datum/keybinding/mob/drop_item

View File

@@ -31,7 +31,7 @@ GLOBAL_DATUM_INIT(kinetic_gauntlet_melee_combo, /datum/combo_set/melee, new /dat
INTENT_DISARM,
INTENT_HARM,
)
default_feedback_message = SPAN_DANGER_CONST("$ATTACKER rapidly strikes $TARGET and sends them flying!")
default_feedback_message = SPAN_DANGER_CONST("%%ATTACKER%% rapidly strikes %%TARGET%% and sends them flying!")
damage_force = 36
/datum/combo/melee/intent_based/kinetic_gauntlets/slam/inflict_on(atom/target, target_zone, mob/attacker, datum/event_args/actor/clickchain/clickchain)
@@ -65,7 +65,7 @@ GLOBAL_DATUM_INIT(kinetic_gauntlet_melee_combo, /datum/combo_set/melee, new /dat
damage_force = 36
default_feedback_sfx = 'sound/weapons/resonator_blast.ogg'
default_feedback_message = SPAN_DANGER_CONST("$ATTACKER places three precision strikes $TARGET, causing their destabilization field to collapse into a concussive blast!")
default_feedback_message = SPAN_DANGER_CONST("%%ATTACKER%% places three precision strikes %%TARGET%%, causing their destabilization field to collapse into a concussive blast!")
/datum/combo/melee/intent_based/kinetic_gauntlets/concuss/inflict_on(atom/target, target_zone, mob/attacker, datum/event_args/actor/clickchain/clickchain)
. = ..()
@@ -89,4 +89,4 @@ GLOBAL_DATUM_INIT(kinetic_gauntlet_melee_combo, /datum/combo_set/melee, new /dat
)
damage_force = 72
default_feedback_message = SPAN_DANGER_CONST("$ATTACKER slams $ATTACKER_P_THEIR gauntlet into $TARGET, violently detonating their destabilization field!")
default_feedback_message = SPAN_DANGER_CONST("%%ATTACKER%% slams %%ATTACKER_P_THEIR%% gauntlet into %%TARGET%%, violently detonating their destabilization field!")

View File

@@ -1,99 +1,38 @@
// All mobs should have custom emote, really..
//m_type == 1 --> visual.
//m_type == 2 --> audible
/mob/proc/custom_emote(var/m_type=1,var/message = null,var/range=world.view)
if(stat || !use_me && usr == src)
to_chat(src, "You are unable to emote.")
return
var/muzzled = is_muzzled()
if(m_type == 2 && muzzled)
return
/**
* Legacy below
*/
var/input
if(!message)
input = sanitize_or_reflect(input(src,"Choose an emote to display.") as text|null, src) // Reflect too long messages, within reason.
/mob/proc/emote(var/act, var/type, var/message)
act = trim(act)
var/spacer = findtext_char(act, " ")
var/raw_key
var/raw_params
if(spacer != 0)
raw_key = copytext_char(act, 1, spacer)
raw_params = copytext_char(act, spacer + 1)
else
input = message
if(input)
log_emote(message,src) //Log before we add junk
//If the message starts with a comma or apostrophe, no space after the name.
//We have to account for sanitization so we take the whole first word to see if it's a character code.
var/nospace = GLOB.valid_starting_punctuation.Find(input)
message = "<span class='emote'><B>[src]</B>[nospace ? "" : " "][input]</span>"
else
return
if (message)
message = say_emphasis(message)
var/overhead_message = ("** [message] **")
say_overhead(overhead_message, FALSE, range)
SEND_SIGNAL(src, COMSIG_MOB_CUSTOM_EMOTE, src, message)
// Hearing gasp and such every five seconds is not good emotes were not global for a reason.
// Maybe some people are okay with that.
var/turf/T = get_turf(src)
if(!T) return
var/list/in_range = get_mobs_and_objs_in_view_fast(T,range,2,remote_ghosts = client ? TRUE : FALSE)
var/list/m_viewers = in_range["mobs"]
var/list/o_viewers = in_range["objs"]
for(var/mob in m_viewers)
var/mob/M = mob
spawn(0) // It's possible that it could be deleted in the meantime, or that it runtimes.
if(M)
if(istype(M, /mob/observer/dead/))
var/mob/observer/dead/D = M
if(ckey || (src in view(D)))
M.show_message(message, m_type)
else
M.show_message(message, m_type)
for(var/obj in o_viewers)
var/obj/O = obj
spawn(0)
if(O)
O.see_emote(src, message, m_type)
// Shortcuts for above proc
/mob/proc/visible_emote(var/act_desc)
custom_emote(1, act_desc)
/mob/proc/audible_emote(var/act_desc)
custom_emote(2, act_desc)
/mob/proc/emote_dead(var/message)
if(client.prefs.muted & MUTE_DEADCHAT)
to_chat(src, "<span class='danger'>You cannot send deadchat emotes (muted).</span>")
return
if(!get_preference_toggle(/datum/game_preference_toggle/chat/dsay))
to_chat(src, "<span class='danger'>You have deadchat muted.</span>")
return
if(!src.client.holder)
if(!config_legacy.dsay_allowed)
to_chat(src, "<span class='danger'>Deadchat is globally muted.</span>")
raw_key = act
raw_params = ""
var/results = invoke_emote(raw_key, raw_params, new /datum/event_args/actor(src))
switch(results)
if(EMOTE_INVOKE_INVALID)
return
if(SSbans.t_is_role_banned_ckey(ckey, role = BAN_ROLE_OOC))
to_chat(src, SPAN_WARNING("You are banned from OOC and deadchat."))
return
var/input
if(!message)
input = sanitize_or_reflect(input(src, "Choose an emote to display.") as text|null, src) // Reflect too long messages, within reason
else
input = message
input = emoji_parse(say_emphasis(input))
if(input)
log_ghostemote(input, src)
if(!invisibility) //If the ghost is made visible by admins or cult. And to see if the ghost has toggled its own visibility, as well. -Mech
visible_message(SPAN_DEADSAY("<B>[src]</B> [input]"))
else
say_dead_direct(input, src)
return "stop"
/// Deprecated.
/mob/proc/visible_emote(var/act_desc)
run_custom_emote(act_desc, saycode_type = SAYCODE_TYPE_VISIBLE)
/// Deprecated.
/mob/proc/audible_emote(var/act_desc)
run_custom_emote(act_desc, saycode_type = SAYCODE_TYPE_AUDIBLE)
/// Deprecated
/mob/proc/custom_emote(m_type, message)
switch(m_type)
if(2)
audible_emote(message)
else
visible_emote(message)

View File

@@ -1,5 +1,7 @@
/mob/living/carbon/alien/emote(var/act,var/m_type=1,var/message = null)
. = ..()
if(. == "stop")
return
var/param = null
if (findtext(act, "-", 1, null))
var/t1 = findtext(act, "-", 1, null)
@@ -11,21 +13,6 @@
var/muzzled = is_muzzled()
switch(act)
if ("me")
if(silent)
return
if (src.client)
if (client.prefs.muted & MUTE_IC)
to_chat(src, "<font color='red'>You cannot send IC messages (muted).</font>")
return
if (stat)
return
if(!(message))
return
return custom_emote(m_type, message)
if ("custom")
return custom_emote(m_type, message)
if("sign")
if (!src.restrained())
message = "<B>The alien</B> signs[(text2num(param) ? " the number [text2num(param)]" : null)]."
@@ -70,21 +57,12 @@
if("nod")
message = "<B>The [src.name]</B> nods its head."
m_type = 1
// if("sit")
// message = "<B>The [src.name]</B> sits down." //Larvan can't sit down, /N
// m_type = 1
if("sway")
message = "<B>The [src.name]</B> sways around dizzily."
m_type = 1
if("sulk")
message = "<B>The [src.name]</B> sulks down sadly."
m_type = 1
if("twitch")
message = "<B>The [src.name]</B> twitches."
m_type = 1
if("twitch_v")
message = "<B>The [src.name]</B> twitches violently."
m_type = 1
if("dance")
if (!src.restrained())
message = "<B>The [src.name]</B> dances around happily."
@@ -103,9 +81,6 @@
if("jump")
message = "<B>The [src.name]</B> jumps!"
m_type = 1
if("hiss_")
message = "<B>The [src.name]</B> hisses softly."
m_type = 1
if("collapse")
afflict_unconscious(20 * 2)
message = "<B>[src]</B> collapses!"
@@ -115,7 +90,7 @@
playsound(src.loc, 'sound/misc/nymphchirp.ogg', 50, 0)
m_type = 2
if("help")
to_chat(src, "burp, chirp, choke, collapse, dance, drool, gasp, shiver, gnarl, jump, moan, nod, roll, scratch,\nscretch, shake, sign-#, sulk, sway, tail, twitch, whimper")
to_chat(src, "burp, chirp, choke, collapse, dance, drool, gasp, shiver, gnarl, jump, moan, nod, roll, scratch,\nscretch, shake, sign-#, sulk, sway, tail, whimper")
else
to_chat(src, "Invalid Emote: [act]")
if ((message && src.stat == 0))
@@ -128,4 +103,3 @@
for(var/mob/O in hearers(src, null))
O.show_message(message, m_type)
//Foreach goto(746)
return

View File

@@ -0,0 +1,7 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/mob/living/carbon/brain/get_usable_emote_require()
. = ..()
// if we can use emotes at all we probably can do these.
. |= EMOTE_REQUIRE_COHERENT_SPEECH | EMOTE_REQUIRE_SYNTHETIC_SPEAKER | EMOTE_REQUIRE_VOCALIZATION

View File

@@ -39,10 +39,10 @@
var/timeofhostdeath = 0
var/emp_damage = 0//Handles a type of MMI damage
var/alert = null
use_me = 0 //Can't use the me verb, it's a freaking immobile brain
icon = 'icons/obj/surgery.dmi'
icon_state = "brain1"
no_vore = TRUE
emote_class = NONE
catalogue_data = list(/datum/category_item/catalogue/fauna/brain/organic)
/mob/living/carbon/brain/Initialize(mapload)
@@ -66,7 +66,6 @@
. = ..(blocked, forced)
else
. = ..(MOBILITY_FLAGS_REAL, forced)
use_me = !!(. & MOBILITY_IS_CONSCIOUS)
/mob/living/carbon/brain/isSynthetic()
return istype(loc, /obj/item/mmi)

View File

@@ -1,80 +0,0 @@
/mob/living/carbon/brain/emote(var/act,var/m_type=1,var/message = null)
if(!(container && istype(container, /obj/item/mmi)))//No MMI, no emotes
return
if (findtext(act, "-", 1, null))
var/t1 = findtext(act, "-", 1, null)
act = copytext(act, 1, t1)
if(findtext(act,"s",-1) && !findtext(act,"_",-2))//Removes ending s's unless they are prefixed with a '_'
act = copytext(act,1,length(act))
if(src.stat == DEAD)
return
switch(act)
if ("me")
if(silent)
return
if (src.client)
if (client.prefs.muted & MUTE_IC)
to_chat(src, "<font color='red'>You cannot send IC messages (muted).</font>")
return
if (stat)
return
if(!(message))
return
return custom_emote(m_type, message)
if ("custom")
return custom_emote(m_type, message)
if ("alarm")
to_chat(src, "You sound an alarm.")
message = "<B>[src]</B> sounds an alarm."
m_type = 2
if ("alert")
to_chat(src, "You let out a distressed noise.")
message = "<B>[src]</B> lets out a distressed noise."
m_type = 2
if ("notice")
to_chat(src, "You play a loud tone.")
message = "<B>[src]</B> plays a loud tone."
m_type = 2
if ("flash")
message = "The lights on <B>[src]</B> flash quickly."
m_type = 1
if ("blink")
message = "<B>[src]</B> blinks."
m_type = 1
if ("whistle")
to_chat(src, "You whistle.")
message = "<B>[src]</B> whistles."
m_type = 2
if ("beep")
to_chat(src, "You beep.")
message = "<B>[src]</B> beeps."
m_type = 2
if ("boop")
to_chat(src, "You boop.")
message = "<B>[src]</B> boops."
m_type = 2
if ("help")
to_chat(src, "alarm,alert,notice,flash,blink,whistle,beep,boop")
else
to_chat(src, "<font color=#4F49AF>Unusable emote '[act]'. Say *help for a list.</font>")
if (message)
log_emote(message, src)
for(var/mob/M in dead_mob_list)
if (!M.client || istype(M, /mob/new_player))
continue //skip monkeys, leavers, and new_players
if(M.stat == DEAD && M.get_preference_toggle(/datum/game_preference_toggle/observer/ghost_sight) && !(M in viewers(src,null)))
M.show_message(message)
if (m_type & 1)
for (var/mob/O in viewers(src, null))
O.show_message(message, m_type)
else if (m_type & 2)
for (var/mob/O in hearers(src.loc, null))
O.show_message(message, m_type)

View File

@@ -0,0 +1,8 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
/mob/living/carbon/get_usable_emote_require()
. = ..()
var/obj/item/organ/internal/maybe_voicebox = organs_by_name[O_VOICE]
if(maybe_voicebox?.robotic >= ORGAN_ROBOT)
. |= EMOTE_REQUIRE_SYNTHETIC_SPEAKER

View File

@@ -1,6 +1,7 @@
/mob/living/carbon
//* Organs, Reagents, Biologies *//
emote_class = EMOTE_CLASS_IS_BODY | EMOTE_CLASS_IS_HUMANOID
//* Organs, Reagents, Biologies *//
/// Our blood holder.
var/datum/blood_holder/blood_holder

View File

@@ -3,7 +3,11 @@
set waitfor = FALSE
emote(arglist(args))
// New emotes in here are no longer allowed.
/mob/living/carbon/human/emote(var/act,var/m_type=1,var/message = null)
. = ..()
if(. == "stop")
return
var/param = null
var/datum/gender/T = GLOB.gender_datums[get_visible_gender()]
if(istype(src, /mob/living/carbon/human/dummy))
@@ -13,269 +17,20 @@
param = copytext(act, t1 + 1, length(act) + 1)
act = copytext(act, 1, t1)
//if(findtext(act,"s",-1) && !findtext(act,"_",-2))//Removes ending s's unless they are prefixed with a '_'
// act = copytext(act,1,length(act))
var/muzzled = is_muzzled()
//var/m_type = 1
var/dangerous_pass_through_without_sanitizing = FALSE
for(var/obj/item/organ/O in src.organs)
for (var/obj/item/implant/I in O)
if (I.implanted)
I.trigger(act, src)
if(src.stat == 2.0 && (act != "deathgasp"))
if(src.stat == DEAD && (act != "deathgasp"))
return
if(attempt_vr(src,"handle_emote_vr",list(act,m_type,message)))
return // Custom Emote Handler
switch(act)
if ("airguitar")
if (!src.restrained())
message = "is strumming the air and headbanging like a safari chimp."
m_type = 1
//Machine-only emotes
if("beep", "buzz", "buzz2", "chime", "die", "dwoop", "error", "honk", "no", "ping", "rcough", "rsneeze", "scary", "shutdown","startup", "warn", "ye", "yes")
var/obj/item/organ/o = internal_organs_by_name[O_VOICE]
if(!isSynthetic() && (!o || !(o.robotic >= ORGAN_ASSISTED)))
to_chat(src, "<span class='warning'>You are not a synthetic.</span>")
return
var/M = null
if(param)
for (var/mob/A in view(null, null))
if (param == A.name)
M = A
break
if(!M)
param = null
var/display_msg = "beeps"
var/use_sound = 'sound/machines/twobeep.ogg'
if(act == "buzz")
display_msg = "buzzes"
use_sound = 'sound/machines/buzz-sigh.ogg'
else if(act == "chime")
display_msg = "chimes"
use_sound = 'sound/machines/chime.ogg'
else if(act == "buzz2")
display_msg = "buzzes twice"
use_sound = 'sound/machines/buzz-two.ogg'
else if(act == "warn")
display_msg = "blares an alarm"
use_sound = 'sound/machines/warning-buzzer.ogg'
else if(act == "honk")
display_msg = "honks"
use_sound = 'sound/items/bikehorn.ogg'
else if(act == "ping")
display_msg = "pings"
use_sound = 'sound/machines/ping.ogg'
else if(act == "yes" || act == "ye")
display_msg = "emits an affirmative blip"
use_sound = 'sound/machines/synth_yes.ogg'
else if(act == "no")
display_msg = "emits a negative blip"
use_sound = 'sound/machines/synth_no.ogg'
else if(act == "dwoop")
display_msg = "chirps happily"
use_sound = 'sound/machines/dwoop.ogg'
else if(act == "scary")
display_msg = "emits a disconcerting tone"
use_sound = 'sound/machines/synth_scary.ogg'
else if(act == "startup")
display_msg = "chimes to life"
use_sound = 'sound/machines/synth_startup.ogg'
else if(act == "shutdown")
display_msg = "emits a nostalgic tone as they fall silent"
use_sound = 'sound/machines/synth_shutdown.ogg'
else if(act == "error")
display_msg = "experiences a system error"
use_sound = 'sound/machines/synth_error.ogg'
else if(act == "die")
display_msg = "crumples, their chassis colder and more lifeless than usual"
use_sound = 'sound/machines/synth_gameover.ogg'
else if(act == "rcough")
display_msg = "emits a robotic cough"
if(get_gender() == FEMALE)
use_sound = pick('sound/effects/mob_effects/f_machine_cougha.ogg','sound/effects/mob_effects/f_machine_coughb.ogg')
else
use_sound = pick('sound/effects/mob_effects/m_machine_cougha.ogg','sound/effects/mob_effects/m_machine_coughb.ogg', 'sound/effects/mob_effects/m_machine_coughc.ogg')
else if(act == "rsneeze")
display_msg = "emits a robotic sneeze"
if(get_gender() == FEMALE)
use_sound = 'sound/effects/mob_effects/machine_sneeze.ogg'
else
use_sound = 'sound/effects/mob_effects/f_machine_sneeze.ogg'
if (param)
message = "[display_msg] at [param]."
else
message = "[display_msg]."
playsound(src.loc, use_sound, 50, 0)
m_type = 1
//Promethean-only emotes
if("squish")
if(species.bump_flag != SLIME) //This should definitely do it.
to_chat(src, "<span class='warning'>You are not a slime thing!</span>")
return
playsound(src.loc, 'sound/effects/slime_squish.ogg', 50, 0) //Credit to DrMinky (freesound.org) for the sound.
message = "squishes."
m_type = 1
// SHRIEK VOXXY ONLY
if ("shriekloud")
if(src.species.get_species_id() != SPECIES_ID_VOX)
to_chat(src, "<span class='warning'>You aren't ear piercingly vocal enough!</span>")
return
playsound(src.loc, 'sound/voice/shrieksneeze.ogg', 50, 0)
message = "gives a short sharp shriek!"
m_type = 1
if ("shriekshort")
if(src.species.get_species_id() != SPECIES_ID_VOX)
to_chat(src, "<span class='warning'>You aren't noisy enough!</span>")
return
playsound(src.loc, 'sound/voice/shriekcough.ogg', 50, 0)
message = "gives a short, quieter shriek!"
m_type = 1
// SQUID GAMES
if ("achime")
if(src.species.get_species_id() != SPECIES_ID_ADHERENT)
to_chat(src, "<span class='warning'>You aren't floaty enough!</span>")
return
playsound(src.loc, 'sound/machines/achime.ogg', 50, 0)
message = "chimes!"
m_type = 1
//Xenomorph Hybrid
if("xhiss")
if(src.species.get_species_id() != SPECIES_ID_XENOHYBRID)
to_chat(src, "<span class='warning'>You aren't alien enough!</span>")
return
playsound(src.loc, 'sound/voice/xenos/alien_hiss3.ogg', 50, 0)
message = "hisses!"
m_type = 2
if("xroar")
if(src.species.get_species_id() != SPECIES_ID_XENOHYBRID)
to_chat(src, "<span class='warning'>You aren't alien enough!</span>")
return
playsound(src.loc, 'sound/voice/xenos/alien_roar1.ogg', 50, 0)
message = "roars!"
m_type = 2
if("xhiss2")
if(src.species.get_species_id() != SPECIES_ID_XENOHYBRID)
to_chat(src, "<span class='warning'>You aren't alien enough!</span>")
return
playsound(src.loc, 'sound/voice/xenos/xhiss2.ogg', 50, 0)
message = "hisses!"
m_type = 2
if("xhiss3")
if(src.species.get_species_id() != SPECIES_ID_XENOHYBRID)
to_chat(src, "<span class='warning'>You aren't alien enough!</span>")
return
playsound(src.loc, 'sound/voice/xenos/xhiss3.ogg', 50, 0)
message = "hisses!"
m_type = 2
if("xhiss4")
if(src.species.get_species_id() != SPECIES_ID_XENOHYBRID)
to_chat(src, "<span class='warning'>You aren't alien enough!</span>")
return
playsound(src.loc, 'sound/voice/xenos/xhiss4.ogg', 50, 0)
message = "hisses!"
m_type = 2
if("xhiss5")
if(src.species.get_species_id() != SPECIES_ID_XENOHYBRID)
to_chat(src, "<span class='warning'>You aren't alien enough!</span>")
return
playsound(src.loc, 'sound/voice/xenos/xhiss5.ogg', 50, 0)
message = "hisses!"
m_type = 2
if("xhiss6")
if(src.species.get_species_id() != SPECIES_ID_XENOHYBRID)
to_chat(src, "<span class='warning'>You aren't alien enough!</span>")
return
playsound(src.loc, 'sound/voice/xenos/xhiss6.ogg', 50, 0)
message = "hisses!"
m_type = 2
if("xroar1")
if(src.species.get_species_id() != SPECIES_ID_XENOHYBRID)
to_chat(src, "<span class='warning'>You aren't alien enough!</span>")
return
playsound(src.loc, 'sound/voice/xenos/xroar1.ogg', 50, 0)
message = "hisses!"
m_type = 2
if("xroar2")
if(src.species.get_species_id() != SPECIES_ID_XENOHYBRID)
to_chat(src, "<span class='warning'>You aren't alien enough!</span>")
return
playsound(src.loc, 'sound/voice/xenos/xroar2.ogg', 50, 0)
message = "hisses!"
m_type = 2
if("xroar3")
if(src.species.get_species_id() != SPECIES_ID_XENOHYBRID)
to_chat(src, "<span class='warning'>You aren't alien enough!</span>")
return
playsound(src.loc, 'sound/voice/xenos/xroar3.ogg', 50, 0)
message = "hisses!"
m_type = 2
if("xtalk1")
if(src.species.get_species_id() != SPECIES_ID_XENOHYBRID)
to_chat(src, "<span class='warning'>You aren't alien enough!</span>")
return
playsound(src.loc, 'sound/voice/xenos/xtalk1.ogg', 50, 0)
message = "hisses!"
m_type = 2
if("xtalk2")
if(src.species.get_species_id() != SPECIES_ID_XENOHYBRID)
to_chat(src, "<span class='warning'>You aren't alien enough!</span>")
return
playsound(src.loc, 'sound/voice/xenos/xtalk2.ogg', 50, 0)
message = "hisses!"
m_type = 2
if("xtalk3")
if(src.species.get_species_id() != SPECIES_ID_XENOHYBRID)
to_chat(src, "<span class='warning'>You aren't alien enough!</span>")
return
playsound(src.loc, 'sound/voice/xenos/xtalk3.ogg', 50, 0)
message = "hisses!"
m_type = 2
if("xroar")
if(src.species.get_species_id() != SPECIES_ID_XENOHYBRID)
to_chat(src, "<span class='warning'>You aren't alien enough!</span>")
return
playsound(src.loc, 'sound/voice/xenos/alien_roar1.ogg', 50, 0)
message = "roars!"
m_type = 2
if("xgrowl")
if(src.species.get_species_id() != SPECIES_ID_XENOHYBRID)
to_chat(src, "<span class='warning'>You aren't alien enough!</span>")
return
playsound(src.loc, 'sound/voice/xenos/alien_growl1.ogg', 50, 0)
message = "growls!"
m_type = 2
if("xkiss")
if(src.species.get_species_id() != SPECIES_ID_XENOHYBRID)
to_chat(src, "<span class='warning'>You aren't alien enough!</span>")
return
var/M = null
if (param)
for (var/mob/A in view(1,src.loc))
@@ -309,113 +64,8 @@
message = "makes a kissing mouth."
m_type = 1
if("smooch")
if (param)
var/M = null
for (var/mob/A in view(1,src.loc))
if (param == A.name)
M = A
break
if (!M)
param = null
if (param)
message = "smooches [param]."
m_type = 1
if ("blink")
message = "blinks."
m_type = 1
if ("blink_r")
message = "blinks rapidly."
m_type = 1
if ("bow")
if (!src.buckled)
var/M = null
if (param)
for (var/mob/A in view(null, null))
if (param == A.name)
M = A
break
if (!M)
param = null
if (param)
message = "bows to [param]."
else
message = "bows."
m_type = 1
if ("custom")
var/input = sanitize(input("Choose an emote to display.") as text|null)
if (!input)
return
var/input2 = input("Is this a visible or hearable emote?") in list("Visible","Hearable")
if (input2 == "Visible")
m_type = 1
else if (input2 == "Hearable")
if HAS_TRAIT_FROM(src, TRAIT_MUTE, MIME_TRAIT)
return
m_type = 2
else
alert("Unable to use this emote, must be either hearable or visible.")
return
return custom_emote(m_type, message)
if ("me")
//if(silent && silent > 0 && findtext(message,"\"",1, null) > 0)
// return //This check does not work and I have no idea why, I'm leaving it in for reference.
if (src.client)
if (client.prefs.muted & MUTE_IC)
to_chat(src, "<font color='red'>You cannot send IC messages (muted).</font>")
return
if (stat)
return
if(!(message))
return
return custom_emote(m_type, message)
if ("salute")
if (!src.buckled)
var/M = null
if (param)
for (var/mob/A in view(null, null))
if (param == A.name)
M = A
break
if (!M)
param = null
if (param)
message = "salutes to [param]."
else
message = "salutes."
m_type = 1
if ("fullsalute")
if (!src.buckled)
var/M = null
if (param)
for (var/mob/A in view(null, null))
if (param == A.name)
M = A
break
if (!M)
param = null
if (param)
message = "salutes to [param]."
else
message = "salutes."
playsound(src.loc, 'sound/misc/salute.ogg', 30, 0)
m_type = 1
if ("choke")
if HAS_TRAIT_FROM(src, TRAIT_MUTE, MIME_TRAIT)
message = "clutches [T.his] throat desperately!"
@@ -428,18 +78,6 @@
message = "makes a strong noise."
m_type = 2
if ("clap")
if (!src.restrained())
message = "claps."
var/use_sound
use_sound = pick('sound/misc/clapping.ogg','sound/voice/clap2.ogg','sound/voice/clap3.ogg','sound/voice/clap4.ogg')
playsound(src.loc, use_sound, 50, 0)
if("golfclap")
if (!src.restrained())
message = "claps very slowly."
playsound(src.loc, 'sound/voice/golfclap.ogg', 50, 0)
if ("flap")
if (!src.restrained())
message = "flaps [T.his] wings."
@@ -454,16 +92,6 @@
if(HAS_TRAIT_FROM(src, TRAIT_MUTE, MIME_TRAIT))
m_type = 1
if("ara")
message = "aras."
var/use_sound
use_sound = pick('sound/voice/ara_ara1.ogg','sound/voice/ara_ara2.ogg')
playsound(src.loc, use_sound, 50, 0)
if("uwu")
message = "lets out a devious noise."
playsound(src.loc, 'sound/voice/uwu.ogg', 50, 0)
if ("drool")
message = "drools."
m_type = 1
@@ -484,14 +112,6 @@
message = "makes a noise."
m_type = 2
if ("twitch")
message = "twitches."
m_type = 1
if ("twitch_v")
message = "twitches violently."
m_type = 1
if ("faint")
message = "faints."
if(!IS_CONSCIOUS(src))
@@ -499,38 +119,6 @@
afflict_sleeping(20 * 10) //Short-short nap
m_type = 1
if("cough", "coughs")
if HAS_TRAIT_FROM(src, TRAIT_MUTE, MIME_TRAIT)
message = "appears to cough!"
m_type = 1
else
if(!muzzled)
var/robotic = 0
m_type = 2
if(should_have_organ(O_LUNGS))
var/obj/item/organ/internal/lungs/L = internal_organs_by_name[O_LUNGS]
if(L && L.robotic == 2) //Hard-coded to 2, incase we add lifelike robotic lungs
robotic = 1
if(!robotic)
message = "coughs!"
if(get_gender() == FEMALE)
if(species.female_cough_sounds)
playsound(src, pick(species.female_cough_sounds), 120)
else
if(species.male_cough_sounds)
playsound(src, pick(species.male_cough_sounds), 120)
else
message = "emits a robotic cough"
var/use_sound
if(get_gender() == FEMALE)
use_sound = pick('sound/effects/mob_effects/f_machine_cougha.ogg','sound/effects/mob_effects/f_machine_coughb.ogg')
else
use_sound = pick('sound/effects/mob_effects/m_machine_cougha.ogg','sound/effects/mob_effects/m_machine_coughb.ogg', 'sound/effects/mob_effects/m_machine_coughc.ogg')
playsound(src.loc, use_sound, 50, 0)
else
message = "makes a strong noise."
m_type = 2
if("bcough")
if HAS_TRAIT_FROM(src, TRAIT_MUTE, MIME_TRAIT)
message = "appears to cough up blood!"
@@ -554,22 +142,6 @@
if(species.male_cough_sounds)
playsound(src, pick(species.male_cough_sounds), 120)
if ("frown")
message = "frowns."
m_type = 1
if ("nod")
message = "nods."
m_type = 1
if ("blush")
message = "blushes."
m_type = 1
if ("wave")
message = "waves."
m_type = 1
if ("gasp")
if HAS_TRAIT_FROM(src, TRAIT_MUTE, MIME_TRAIT)
message = "appears to be gasping!"
@@ -747,44 +319,16 @@
message = "says, \"[M], please. He had a family.\" [src.name] takes a drag from a cigarette and blows his name out in smoke."
m_type = 2
if ("point")
if (!src.restrained())
var/mob/M = null
if (param)
for (var/atom/A as mob|obj|turf|area in view(null, null))
if (param == A.name)
M = A
break
if (!M)
message = "points."
else
pointed(M)
if (M)
message = "points to [M]."
else
m_type = 1
if("crack")
if(!restrained())
message = "cracks [T.his] knuckles."
playsound(src, 'sound/voice/knuckles.ogg', 50, 1,)
m_type = 1
if ("raise")
if (!src.restrained())
message = "raises a hand."
m_type = 1
if("shake")
message = "shakes [T.his] head."
m_type = 1
if ("shrug")
message = "shrugs."
m_type = 1
if ("signal")
if (!src.restrained())
var/t1 = round(text2num(param))
@@ -792,16 +336,6 @@
message = "raises [t1] finger\s."
m_type = 1
if ("smile")
message = "smiles."
m_type = 1
if ("shiver")
message = "shivers."
m_type = 2
if HAS_TRAIT_FROM(src, TRAIT_MUTE, MIME_TRAIT)
m_type = 1
if ("pale")
message = "goes pale for a second."
m_type = 1
@@ -810,37 +344,6 @@
message = "trembles in fear!"
m_type = 1
if("sneeze", "sneezes")
if HAS_TRAIT_FROM(src, TRAIT_MUTE, MIME_TRAIT)
message = "sneezes."
m_type = 1
else
if(!muzzled)
var/robotic = 0
m_type = 2
if(should_have_organ(O_LUNGS))
var/obj/item/organ/internal/lungs/L = internal_organs_by_name[O_LUNGS]
if(L && L.robotic == 2) //Hard-coded to 2, incase we add lifelike robotic lungs
robotic = 1
if(!robotic)
message = "sneezes."
if(get_gender() == FEMALE)
playsound(src, species.female_sneeze_sound, 70)
else
playsound(src, species.male_sneeze_sound, 70)
m_type = 2
else
message = "emits a robotic sneeze"
var/use_sound
if(get_gender() == FEMALE)
use_sound = 'sound/effects/mob_effects/machine_sneeze.ogg'
else
use_sound = 'sound/effects/mob_effects/f_machine_sneeze.ogg'
playsound(src.loc, use_sound, 50, 0)
else
message = "makes a strange noise."
m_type = 2
if ("sniff")
message = "sniffs."
m_type = 2
@@ -871,10 +374,6 @@
message = "makes a weak noise."
m_type = 2
if ("wink")
message = "winks."
m_type = 1
if ("yawn")
if (!muzzled)
message = "yawns."
@@ -965,6 +464,7 @@
if("aslap", "aslaps")
m_type = 1
dangerous_pass_through_without_sanitizing = TRUE
var/mob/living/carbon/human/H = src
var/obj/item/organ/external/L = H.get_organ("l_hand")
var/obj/item/organ/external/R = H.get_organ("r_hand")
@@ -1017,25 +517,6 @@
else
message = "makes a very loud noise."
m_type = 2
if("squeak","squeaks")
if HAS_TRAIT_FROM(src, TRAIT_MUTE, MIME_TRAIT)
message = "acts out a soft squeak."
m_type = 1
else
if(!muzzled)
message = "squeaks!"
m_type = 2
playsound(loc, "sound/effects/mouse_squeak.ogg", 50, 1)
if("meow", "meows")
if HAS_TRAIT_FROM(src, TRAIT_MUTE, MIME_TRAIT)
message = "acts out a soft mrowl."
m_type = 1
else
if(!muzzled)
message = "mrowls!"
m_type = 2
playsound(loc, 'sound/voice/meow1.ogg', 50, 1)
if("snap", "snaps")
m_type = 2
@@ -1096,6 +577,7 @@
if(!param)
to_chat(src, usage)
return
dangerous_pass_through_without_sanitizing = TRUE
var/t1 = findtext(param, "+", 1, null)
var/t2 = findtext(param, "-", 1, null)
@@ -1136,20 +618,22 @@
return
if ("help")
to_chat(src, "ara, awoo, bark, bleat, blink, blink_r, blush, bow-(none)/mob, burp, chirp, choke, chuckle, clap, collapse, cough, cry, custom, deathgasp, drool, echoping, eyebrow, fastsway/qwag, \
flip, frown, gasp, giggle, glare-(none)/mob, grin, groan, grumble, growl, handshake, hiss, howl, hug-(none)/mob, laugh, look-(none)/mob, mar, merp, moan, mrrp, mumble, nod, nya, nyaha, pale, peep, point-atom, prbt, \
raise, roll, salute, fullsalute, scream, sneeze, shake, shiver, shrug, sigh, signal-#1-10, slap-(none)/mob, smile, sneeze, sniff, snore, stare-(none)/mob, stopsway/swag, squeak, sway/wag, swish, tremble, twitch, \
twitch_v, uwu, vomit, weh, whimper, wink, yawn, ycackle. Moth: mchitter, mlaugh, mscream, msqueak. Synthetics: airhorn, beep, buzz, buzz2, chime, die, dwoop, error, honk, no, ping, rcough, rsneeze, scary, \
shutdown, startup, steam, warn, ye, yes. Vox: shriekshort, shriekloud")
to_chat(src, "bark, bleat, blush, bow-(none)/mob, burp, chirp, choke, chuckle, clap, collapse, cough, cry, custom, deathgasp, drool, echoping, eyebrow, fastsway/qwag, \
flip, frown, gasp, giggle, glare-(none)/mob, grin, groan, grumble, growl, handshake, hiss, howl, hug-(none)/mob, laugh, look-(none)/mob, mar, merp, moan, mrrp, mumble, nod, nya, nyaha, pale, prbt, \
raise, roll, scream, sneeze, shake, shiver, shrug, sigh, signal-#1-10, slap-(none)/mob, smile, sneeze, sniff, snore, stare-(none)/mob, stopsway/swag, squeak, sway/wag, swish, tremble, \
vomit, weh, whimper, wink, yawn, ycackle. Moth: mchitter, mlaugh, mscream, msqueak. Synthetics: airhorn, die, error, rcough, rsneeze, \
ye.")
else
to_chat(src, "<font color=#4F49AF>Unusable emote '[act]'. Say *help for a list.</font>")
return
if (message)
custom_emote(m_type,message)
// RAAAAGH I HATE YOU *ROLL
if(!dangerous_pass_through_without_sanitizing)
message = html_encode(message)
for(var/mob/nearby in saycode_view_query(world_view_max_number(), TRUE))
nearby.show_message("<b>[src]</b> [message]", SAYCODE_TYPE_ALWAYS)
/mob/living/carbon/human/proc/set_pose(new_pose)
pose = sanitize(new_pose)
@@ -1241,6 +725,7 @@
HTML += "<tt>"
src << browse(HTML, "window=flavor_changes;size=430x300")
// New emotes in here are no longer allowed.
/mob/living/carbon/human/proc/handle_emote_vr(var/act,var/m_type=1,var/message = null)
switch(act)
@@ -1265,17 +750,6 @@
if ("mlem")
message = "mlems [get_visible_gender() == MALE ? "his" : get_visible_gender() == FEMALE ? "her" : "their"] tongue up over [get_visible_gender() == MALE ? "his" : get_visible_gender() == FEMALE ? "her" : "their"] nose. Mlem."
m_type = 1
///////////////////////// EMOTES PORTED FROM MAIN START
if ("awoo")
m_type = 2
message = "lets out an awoo."
playsound(loc, 'sound/voice/awoo.ogg', 50, 1, -1)
if ("nya")
message = "lets out a nya."
m_type = 2
var/use_sound
use_sound = pick('sound/voice/nya.ogg')
playsound(src.loc, use_sound, 50, 0)
if ("nyaha")
if(!spam_flag)
var/list/catlaugh = list('sound/voice/catpeople/nyaha.ogg', 'sound/voice/catpeople/nyahaha1.ogg', 'sound/voice/catpeople/nyahaha2.ogg', 'sound/voice/catpeople/nyahehe.ogg')
@@ -1285,10 +759,6 @@
var/list/laughs = list("laughs deviously.", "lets out a catty laugh.", "nya ha ha's.")
message = "[pick(laughs)]"
m_type = 2
if ("peep")
message = "peeps like a bird."
m_type = 2
playsound(loc, 'sound/voice/peep.ogg', 50, 1, -1)
if("chirp")
message = "chirps!"
playsound(src.loc, 'sound/misc/nymphchirp.ogg', 50, 0)
@@ -1321,10 +791,6 @@
message = "lets out a hiss."
m_type = 2
playsound(loc, 'sound/voice/hiss.ogg', 50, 1, -1)
if ("squeak")
message = "lets out a squeak."
m_type = 2
playsound(loc, 'sound/effects/mouse_squeak.ogg', 50, 1, -1)
if("mar")
message = "lets out a mar."
m_type = 2
@@ -1383,14 +849,6 @@
message = "purrs softly."
m_type = 2
playsound(loc, 'sound/voice/purr.ogg', 50, 1, -1)
if ("clak")
if(!spam_flag)
var/msg = list("<font color='grey' size='2'>CLAKS!</font>", "claks!")
message = "[pick(msg)]"
playsound(loc, 'sound/spooky/boneclak.ogg', 50, 1, 1)
spam_flag = TRUE
addtimer(CALLBACK(src, PROC_REF(spam_flag_false)), 18)
m_type = 2
if ("ycackle")
message = "cackles maniacally!"
m_type = 2
@@ -1421,16 +879,6 @@
playsound(loc, 'sound/items/airhorn2.ogg', 20, 1, 1)
spam_flag = TRUE
addtimer(CALLBACK(src, PROC_REF(spam_flag_false)), 18)
if ("steam")
var/obj/item/organ/o = internal_organs_by_name[O_VOICE]
if(!isSynthetic() && (!o || !(o.robotic >= ORGAN_ASSISTED)))
to_chat(src, "<span class='warning'>You are not a synthetic.</span>")
return
message = "lets off some steam."
m_type = 2
playsound(loc, 'sound/machines/clockcult/steam_whoosh.ogg', 30, 1, 1)
spam_flag = TRUE
addtimer(CALLBACK(src, PROC_REF(spam_flag_false)), 18)
if (message)
custom_emote(m_type,message)

View File

@@ -1,10 +1,9 @@
/mob/living/carbon/human/say(var/message,var/whispering=0)
var/alt_name = ""
/mob/living/carbon/human/say(var/message, var/datum/prototype/language/speaking = null, var/verb="says", var/alt_name="", var/whispering = 0)
if(name != GetVoice())
alt_name = "(as [get_id_name("Unknown")])"
message = sanitize_or_reflect(message,src) // Reflect too-long messages, within reason
..(message, alt_name = alt_name, whispering = whispering)
return ..()
/mob/living/carbon/human/speech_bubble_appearance()
if(isSynthetic())

View File

@@ -0,0 +1,12 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
/mob/living/get_usable_emote_require()
. = NONE
if(silent || is_muzzled())
else
. |= EMOTE_REQUIRE_COHERENT_SPEECH | EMOTE_REQUIRE_VOCALIZATION
if(isSynthetic())
. |= EMOTE_REQUIRE_SYNTHETIC_SPEAKER
if(inventory.get_empty_hand_indices())
. |= EMOTE_REQUIRE_FREE_HAND

View File

@@ -1,3 +1,6 @@
/mob/living
emote_class = EMOTE_CLASS_IS_BODY
TYPE_REGISTER_SPATIAL_GRID(/mob/living, SSspatial_grids.living)
/mob/living/Initialize(mapload)
. = ..()

View File

@@ -168,8 +168,12 @@ var/list/channel_to_radio_key = new
//Maybe they are using say/whisper to do a quick emote, so do those
switch(copytext_char(message,1,2))
if("*") return emote(copytext_char(message,2))
if("^") return custom_emote(1, copytext_char(message,2))
// TODO: hey see this decode?
// this is because emote system handles its own sanitization
// this is to trample any *potential* sanitiziation that happened,
// as otherwise quotes won't be properly parsed by the tokenizer.
if("*") return emote(html_decode(copytext_char(message,2)))
if("^") return custom_emote(1, html_decode(copytext_char(message,2)))
//Parse the radio code and consume it
if (message_mode)
@@ -244,7 +248,7 @@ var/list/channel_to_radio_key = new
message = message_args["message"]
if(HAS_TRAIT(src, TRAIT_MUTE))
if(HAS_TRAIT(src, TRAIT_MUTE) && !(speaking && (speaking.language_flags & LANGUAGE_SIGNLANG)))
to_chat(src, "<span class='danger'>You are not capable of speech!</span>")
return
@@ -399,6 +403,9 @@ var/list/channel_to_radio_key = new
//Main 'say' and 'whisper' message delivery
var/turf/our_turf = get_turf(src)
// todo: cache this or maybe just have a distinction between regular hear and 'observer heard us from far away'?
var/max_vocal_cue_dist = world_view_max_number()
for(var/mob/M in listening)
spawn(0) //Using spawns to queue all the messages for AFTER this proc is done, and stop runtimes
@@ -410,6 +417,8 @@ var/list/channel_to_radio_key = new
var/image/I1 = listening[M] || speech_bubble
images_to_clients[I1] |= M.client
SEND_IMAGE(M, I1)
if(M.get_preference_toggle(/datum/game_preference_toggle/game/vocal_cues) && get_dist(M, src) <= max_vocal_cue_dist)
M.playsound_local(our_turf, /datum/soundbyte/talksound/goon_say, 75, TRUE)
M.hear_say(message, verb, speaking, alt_name, italics, src, speech_sound, sound_vol)
else if(whispering) //Don't even bother with these unless whispering
if(dst > message_range && dst <= w_scramble_range) //Inside whisper scramble range

View File

@@ -0,0 +1,8 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/mob/living/silicon/ai/emit_custom_emote(emote_text)
if(holopad)
holopad_emote(emote_text)
else
..()

View File

@@ -45,9 +45,3 @@
to_chat(src, "No holopad connected.")
return FALSE
return TRUE
/mob/living/silicon/ai/emote(var/act, var/type, var/message)
if(holopad)
holopad_emote(message)
else //Emote normally, then.
..()

View File

@@ -1,4 +1,7 @@
/mob/living/silicon/emote(var/act,var/m_type=1,var/message = null)
. = ..()
if(. == "stop")
return
var/param = null
if (findtext(act, "-", 1, null))
var/t1 = findtext(act, "-", 1, null)
@@ -20,40 +23,6 @@
break
switch(act)
if ("me")
if (src.client)
if(client.prefs.muted & MUTE_IC)
to_chat(src, "You cannot send IC messages (muted).")
return
if (stat)
return
if(!(message))
return
else
return custom_emote(m_type, message)
if ("custom")
return custom_emote(m_type, message)
if ("salute")
if (!src.buckled)
if (emote_target)
message = "salutes to [emote_target]."
else
message = "salutes."
m_type = 1
if ("bow")
if (!src.buckled)
if (emote_target)
message = "bows to [emote_target]."
else
message = "bows."
m_type = 1
if ("clap")
if (!src.restrained())
message = "claps."
m_type = 2
if ("flap")
if (!src.restrained())
message = "flaps its wings."
@@ -64,65 +33,10 @@
message = "flaps its wings ANGRILY!"
m_type = 2
if ("twitch")
message = "twitches."
m_type = 1
if ("twitch_v")
message = "twitches violently."
m_type = 1
if ("nod")
message = "nods."
m_type = 1
if ("deathgasp")
message = "shudders violently for a moment, then becomes motionless, its eyes slowly darkening."
m_type = 1
if ("glare")
if (emote_target)
message = "glares at [emote_target]."
else
message = "glares."
if ("stare")
if (emote_target)
message = "stares at [emote_target]."
else
message = "stares."
if ("look")
if (emote_target)
message = "looks at [emote_target]."
else
message = "looks."
m_type = 1
if("beep")
if (emote_target)
message = "beeps at [emote_target]."
else
message = "beeps."
playsound(src.loc, 'sound/machines/twobeep.ogg', 50, 0)
m_type = 1
if("ping")
if (emote_target)
message = "pings at [emote_target]."
else
message = "pings."
playsound(src.loc, 'sound/machines/ping.ogg', 50, 0)
m_type = 1
if("buzz")
if (emote_target)
message = "buzzes at [emote_target]."
else
message = "buzzes."
playsound(src.loc, 'sound/machines/buzz-sigh.ogg', 50, 0)
m_type = 1
if("spin")
message = "spins!"
m_type = 1
@@ -132,59 +46,6 @@
unbuckle_mob(L, BUCKLE_OP_FORCE)
L.throw_at(get_edge_target_turf(get_turf(src), dir), 7, 1, THROW_AT_IS_GENTLE, src)
spin(15, 1)
if("yes", "ye")
if (emote_target)
message = "emits an affirmative blip at [emote_target]."
else
message = "emits an affirmative blip."
playsound(src, 'sound/machines/synth_yes.ogg', 50, 0)
m_type = 1
if("no")
if (emote_target)
message = "emits a negative blip at [emote_target]."
else
message = "emits a negative blip."
playsound(src.loc, 'sound/machines/synth_no.ogg', 50, 0)
m_type = 1
if("scary")
if (emote_target)
message = "emits a disconcerting tone at [emote_target]."
else
message = "emits a disconcerting tone."
playsound(src.loc, 'sound/machines/synth_scary.ogg', 50, 0)
m_type = 1
if("dwoop")
if (emote_target)
message = "chirps happily at [emote_target]."
else
message = "chirps happily."
playsound(src.loc, 'sound/machines/dwoop.ogg', 50, 0)
m_type = 1
if("startup")
message = "chimes to life."
playsound(src.loc, 'sound/machines/synth_startup.ogg', 50)
m_type = 1
if("shutdown")
message = "emits a nostalgic tone as they fall silent."
playsound(src.loc, 'sound/machines/synth_shutdown.ogg', 50)
m_type = 1
if("error")
message = "experiences a system error."
playsound(src.loc, 'sound/machines/synth_error.ogg', 50)
m_type = 1
if("die")
message = "crumples, their chassis colder and more lifeless than usual."
playsound(src.loc, 'sound/machines/synth_gameover.ogg', 50)
m_type = 1
if("flip")
if(!CHECK_ALL_MOBILITY(src, MOBILITY_CAN_MOVE | MOBILITY_CAN_USE))
to_chat(src, "<span class='warning'>You can't *flip in your current state!</span>")
@@ -243,16 +104,6 @@
playsound(src.loc, 'sound/machines/gonk.ogg', 50, 0)
m_type = 1
if("nya")
var/mob/living/silicon/robot/R = src
if (istype(R) && R.module.is_cat())
message = "lets out a nya."
playsound(loc, 'sound/voice/nya.ogg', 50, 1, -1)
m_type = 2
else
to_chat(src, "You're not a cat!")
if("mrrp")
var/mob/living/silicon/robot/R = src
if (istype(R) && R.module.is_cat())
@@ -273,16 +124,6 @@
else
to_chat(src, "You're not a cat!")
if("meow", "meows")
var/mob/living/silicon/robot/R = src
if (istype(R) && R.module.is_cat())
message = "prbts."
playsound(loc, 'sound/voice/meow1.ogg', 50, 1, -1)
m_type = 2
else
to_chat(src, "You're not a cat!")
if("hiss")
var/mob/living/silicon/robot/R = src
if (istype(R) && R.module.is_cat())
@@ -307,8 +148,3 @@
to_chat(src, "salute, bow-(none)/mob, clap, flap, aflap, twitch, twitch_s, nod, deathgasp, glare-(none)/mob, stare-(none)/mob, look, beep, ping, \nbuzz, law, halt, yes, no")
else
to_chat(src, "<font color=#4F49AF>Unusable emote '[act]'. Say *help for a list.</font>")
if ((message && src.stat == 0))
custom_emote(m_type,message)
return

View File

@@ -324,18 +324,11 @@
update_icon()
..()
//Client attached
/mob/living/simple_mob/Login()
. = ..()
to_chat(src,"<b>You are \the [src].</b> [player_msg]")
/mob/living/simple_mob/emote(var/act, var/type, var/desc)
if(act)
..(act, type, desc)
/mob/living/simple_mob/SelfMove(turf/n, direct)
var/turf/old_turf = get_turf(src)
var/old_dir = dir

View File

@@ -31,8 +31,8 @@
else if(M.stat == DEAD && M.get_preference_toggle(/datum/game_preference_toggle/observer/ghost_ears))
to_chat(M, "The captive mind of [src] whispers, \"[message]\"")
/mob/living/captive_brain/emote(var/message)
return
/mob/living/captive_brain/process_emote(datum/emote/emote, raw_parameter_string, datum/event_args/actor/actor, used_binding)
return FALSE
/mob/living/captive_brain/process_resist()
//Resisting control by an alien mind.

View File

@@ -136,7 +136,3 @@
if (istype(other, /mob/living/carbon/brain))
return 1
return ..()
/mob/living/voice/custom_emote(var/m_type=1,var/message = null,var/range=world.view)
if(!comm) return
..(m_type,message,comm.video_range)

View File

@@ -0,0 +1,130 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
/**
* Specifically for invoking /datum/emote
*
* 'custom emotes' aka 'say'd actions' are not handled by this system.
*/
/**
* Description WIP
* * Blocking.
* @return TRUE if invoked, FALSE otherwise
*/
/mob/proc/invoke_emote(key, parameter_string, datum/event_args/actor/actor)
SHOULD_NOT_OVERRIDE(TRUE)
if(isnull(actor))
actor = new(src)
ASSERT(actor.performer == src)
// always apply anti-spam
if(HAS_TRAIT(src, TRAIT_EMOTE_GLOBAL_COOLDOWN))
return EMOTE_INVOKE_ERRORED
ADD_TRAIT(src, TRAIT_EMOTE_GLOBAL_COOLDOWN, "__EMOTE__")
REMOVE_TRAIT_IN(src, TRAIT_EMOTE_GLOBAL_COOLDOWN, "__EMOTE__", 0.25 SECONDS)
var/special_result = process_emote_special(key, parameter_string, actor)
if(!isnull(special_result))
return special_result
var/datum/emote/resolved = fetch_emote(key)
if(!resolved)
return EMOTE_INVOKE_INVALID
if(HAS_TRAIT(src, TRAIT_EMOTE_COOLDOWN(resolved.type)))
return EMOTE_INVOKE_ERRORED
var/result = process_emote(resolved, parameter_string, actor, used_binding = key)
if(result)
return EMOTE_INVOKE_FINISHED
else
return EMOTE_INVOKE_ERRORED
/**
* Description WIP
*/
/mob/proc/invoke_emote_async(key, parameter_string, datum/event_args/actor/actor)
set waitfor = FALSE
. = EMOTE_INVOKE_SLEEPING
return invoke_emote(key, parameter_string, actor)
/**
* Runs an emote.
* * Blocking
* * `raw_params` may be a list or a text string.
* @return TRUE if successful
*/
/mob/proc/process_emote(datum/emote/emote, parameter_string, datum/event_args/actor/actor, used_binding)
var/list/arbitrary = emote.process_parameters(parameter_string, actor)
// only continue if parameter parsing worked
if(arbitrary == null)
return
// apply specific emote cooldown but only after it's done executing; checked in invoke_emote()
ADD_TRAIT(src, TRAIT_EMOTE_COOLDOWN(emote.type), "__EMOTE__")
. = emote.try_run_emote(actor, arbitrary, used_binding = used_binding)
REMOVE_TRAIT_IN(src, TRAIT_EMOTE_COOLDOWN(emote.type), "__EMOTE__", emote.self_cooldown)
/**
* Process special overrides
* * Blocking
* * `raw_params` may be a list or a text string.
* @return EMOTE_INVOKE_* or null if not handled
*/
/mob/proc/process_emote_special(key, raw_params, datum/event_args/actor/actor)
switch(key)
if("me")
if(!length(raw_params))
actor.chat_feedback(
SPAN_WARNING("You can't custom emote an empty string!"),
target = src,
)
return EMOTE_INVOKE_ERRORED
run_custom_emote(raw_params, actor = actor, with_overhead = TRUE)
return EMOTE_INVOKE_FINISHED
if("help")
var/list/datum/emote/can_run_emotes = query_emote()
var/list/assembled_html = list()
for(var/datum/emote/emote as anything in can_run_emotes)
if((islist(emote.bindings) && !length(emote.bindings)) || !emote.bindings)
continue
var/rendered_bindings = islist(emote.bindings) ? jointext(emote.bindings, "/") : emote.bindings
var/rendered_first_binding = islist(emote.bindings) ? emote.bindings[1] : emote.bindings
assembled_html += SPAN_TOOLTIP_DANGEROUS_HTML("[rendered_bindings][emote.parameter_description ? " [emote.parameter_description]" : ""]", rendered_first_binding)
to_chat(src, "Usable emotes: [english_list(assembled_html)]")
// TODO: should be FINISHED but we need to route to legacy *help too!
return null
return null
/**
* Return a list of /datum/emote's we can **potentially** use
*/
/mob/proc/query_emote()
RETURN_TYPE(/list)
. = list()
var/our_emote_class = get_usable_emote_class()
var/datum/event_args/actor/actor = new(src)
for(var/datum/emote/emote as anything in GLOB.emotes)
if(!emote.can_potentially_use(actor, our_emote_class))
continue
. += emote
/**
* Return a list of legacy emotes associated to descriptions or null
*/
/mob/proc/query_emote_special()
RETURN_TYPE(/list)
return list(
"me" = "Input a custom emote for your character to perform.",
"help" = "Get a list of usable emotes.",
)
/**
* Return emote classes we support.
*/
/mob/proc/get_usable_emote_class()
return emote_class
/**
* Return remote require flags we support
*/
/mob/proc/get_usable_emote_require()
. = NONE

View File

@@ -0,0 +1,97 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/**
* This module is used for processing custom emotes.
*
* Eventually, they'll be used with a proper saycode call-chain and tokenzier
* to enable language fragment support and more.
*
* * Unlike saycode, there's no custom_emote() default wrapper, because custom emotes have so many things to customize.
*/
/**
* Relay a raw incoming custom emote
*
* * Logging happens here.
* * THE INPUT EMOTE TEXT IS NOT SANITIZED AT THIS STAGE.
*/
/mob/proc/run_custom_emote(emote_text, subtle, anti_ghost, saycode_type = SAYCODE_TYPE_VISIBLE, datum/event_args/actor/actor, with_overhead)
if(stat)
// TODO: tooltip with copy link.
to_chat(src, SPAN_WARNING("You are unable to emote."))
return
if(isnull(actor))
actor = new(src)
// raw preprocessed text is used
// var/log_string = "[key_name(src)] ([AREACOORD(src)])[actor?.initiator != src ? " (initiated by [key_name(actor.initiator)] at [AREACOORD(actor.initiator)])" : ""]: [emote_text]"
var/log_string = "[actor?.initiator != src ? " (initiated by [key_name(actor.initiator)] at [AREACOORD(actor.initiator)]) " : ""][emote_text]"
if(anti_ghost)
// implicit
subtle = TRUE
if(subtle)
if(anti_ghost)
log_subtle_anti_ghost(log_string, src)
else
log_subtle(log_string, src)
else
log_emote(log_string, src)
var/raw_html = process_custom_emote(emote_text, subtle, anti_ghost, saycode_type, with_overhead)
if(!raw_html)
return
emit_custom_emote(raw_html, subtle, anti_ghost, saycode_type, with_overhead)
/**
* Perform special preprocessing on an incoming custom emote
* * The base level will html_encode().
* * THE INPUT EMOTE TEXT IS NOT SANITIZED AT THIS STAGE.
*
* @return raw HTML
*/
/mob/proc/process_custom_emote(emote_text, subtle, anti_ghost, saycode_type, with_overhead)
. = emote_text
. = html_encode(.)
. = say_emphasis(.)
. = "<b>[src]</b> " + .
if(subtle)
if(anti_ghost)
. = SPAN_SINGING(.)
else
. = "<i>[.]</i>"
/**
* Emit a custom emote
*/
/mob/proc/emit_custom_emote(raw_html, subtle, anti_ghost, saycode_type, with_overhead)
var/list/atom/movable/heard = saycode_view_query(subtle ? 1 : GLOB.game_view_radius, TRUE, anti_ghost)
// TODO: centralized observer pref check in saycode_view_query
var/optimize_this_later_max_number = world_view_max_number() + 2
var/mob/filtered_mobs = list()
// todo: legacy code
for(var/atom/movable/hearing in heard)
if(ismob(hearing))
var/mob/hearing_mob = hearing
if(isobserver(hearing_mob))
if((get_dist(hearing_mob, src) > optimize_this_later_max_number) && !hearing_mob.get_preference_toggle(/datum/game_preference_toggle/observer/ghost_sight))
continue
if(subtle && !hearing_mob.get_preference_toggle(/datum/game_preference_toggle/observer/ghost_subtle))
continue
SEND_SIGNAL(hearing_mob, COMSIG_MOB_ON_RECEIVE_CUSTOM_EMOTE, src, raw_html, subtle, anti_ghost, saycode_type)
hearing_mob.show_message(raw_html, saycode_type)
filtered_mobs += hearing
else if(isobj(hearing))
var/obj/hearing_obj = hearing
hearing_obj.see_emote(src, raw_html, 2)
var/turf/our_loc = get_turf(src)
var/use_sfx = subtle ? /datum/soundbyte/talksound/generic_subtle_emote_1 : /datum/soundbyte/talksound/generic_emote_1
// todo: cache this or maybe just have a distinction between regular hear and 'observer heard us from far away'?
var/max_vocal_cue_dist = world_view_max_number()
for(var/mob/M as anything in filtered_mobs)
if(M.get_preference_toggle(/datum/game_preference_toggle/game/vocal_cues) && get_dist(M, src) <= max_vocal_cue_dist)
M.playsound_local(our_loc, use_sfx, 50, TRUE)
if(with_overhead)
say_overhead(raw_html, FALSE, GLOB.game_view_radius, passed_hearing_list = filtered_mobs)

View File

@@ -1,48 +1,8 @@
/mob/proc/say(var/message, var/datum/prototype/language/speaking = null, var/verb="says", var/alt_name="", var/whispering = 0)
return
/mob/proc/whisper_wrapper()
var/message = input("","whisper (text)") as text|null
if(message)
whisper(message)
/mob/proc/subtle_wrapper()
var/message = input("","subtle (text)") as message|null
if(message)
me_verb_subtle(message)
/mob/proc/subtler_wrapper()
var/message = input("","subtler (text)") as message|null
if(message)
subtler_anti_ghost(message)
/mob/verb/whisper(message as text)
set name = "Whisper"
set category = VERB_CATEGORY_IC
usr.say(message,whispering=1)
/mob/verb/say_verb(message as text)
set name = "Say"
set category = VERB_CATEGORY_IC
set_typing_indicator(FALSE)
usr.say(message)
/mob/verb/me_verb(message as message)
set name = "Me"
set category = VERB_CATEGORY_IC
if(muffled)
return me_verb_subtle(message)
message = sanitize_or_reflect(message,src) // Reflect too-long messages (within reason)
set_typing_indicator(FALSE)
if(use_me)
usr.emote("me",SAYCODE_TYPE_ALWAYS,message)
else
usr.emote(message)
/mob/proc/whisper(var/message, var/datum/prototype/language/speaking = null, var/verb="says", var/alt_name="")
say(message, speaking, verb, alt_name, TRUE)
/mob/proc/say_dead(var/message)
if(!client)
@@ -119,10 +79,6 @@
verb="asks"
return verb
/mob/proc/emote(var/act, var/type, var/message)
if(act == "me")
return custom_emote(type, message)
/mob/proc/get_ear()
// returns an atom representing a location on the map from which this
// mob can hear things

View File

@@ -0,0 +1,156 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
//* Say *//
/**
* Prompts for a say
*/
/mob/verb/say_wrapper()
set name = "say-indicator"
set category = null
set hidden = TRUE
set_typing_indicator(TRUE)
var/message = input(src, "", "say (text)") as text|null
set_typing_indicator(FALSE)
if(!message)
return
say_verb(message)
/mob/verb/say_verb(message as text)
set name = "Say"
set desc = "Say something to people who you can see."
set category = VERB_CATEGORY_IC
// ensure length isn't too high
message = sanitize_or_reflect(message, src, encode = FALSE)
if(!message)
return
// clear typing indicator
set_typing_indicator(FALSE)
// perform say
say(message)
//* Whisper *//
/**
* Prompts for a whisper
*/
/mob/verb/whisper_wrapper()
set name = "whisper-indicator"
set category = null
set hidden = TRUE
var/message = input(src, "", "whisper (text)") as text|null
if(!message)
return
whisper_verb(message)
/mob/verb/whisper_verb(message as text)
set name = "Whisper"
set desc = "Whisper something to people next to you."
set category = VERB_CATEGORY_IC
// ensure length isn't too high
message = sanitize_or_reflect(message, src, encode = FALSE)
if(!message)
return
// clear typing indicator
set_typing_indicator(FALSE)
// perform say
whisper(message)
//* Emote *//
/**
* Prompts for a custom emote; uses typing indicator
*/
/mob/verb/me_wrapper()
set name = "me-indicator"
set category = null
set hidden = TRUE
set_typing_indicator(TRUE)
var/message = input(src, "", "me (text)") as message|null
set_typing_indicator(FALSE)
if(!message)
return
me_verb(message)
/mob/verb/me_verb(message as message)
set name = "Me"
set desc = "Emote to people in view."
set category = VERB_CATEGORY_IC
// ensure length isn't too high
message = sanitize_or_reflect(message, src, encode = FALSE)
if(!message)
return
// clear typing indicator
set_typing_indicator(FALSE)
// perform emote
run_custom_emote(message, FALSE, FALSE, SAYCODE_TYPE_ALWAYS, with_overhead = TRUE)
//* Subtle Emote *//
/**
* Prompts for a subtle
*/
/mob/verb/subtle_wrapper()
set name = "subtle-wrapper"
set category = null
set hidden = TRUE
var/message = input(src, "", "subtle (text)") as message|null
if(!message)
return
subtle_verb(message)
/mob/verb/subtle_verb(message as message)
set name = "Subtle"
set desc = "Emote to people within 1 tile (3x3) of (or inside) yourself."
set category = VERB_CATEGORY_IC
// ensure length isn't too high
message = sanitize_or_reflect(message, src, encode = FALSE)
if(!message)
return
// clear typing indicator
set_typing_indicator(FALSE)
// perform emote
run_custom_emote(message, TRUE, FALSE, SAYCODE_TYPE_ALWAYS)
//* Subtler Anti Ghost Emote *//
/**
* Prompts for a subtle
*/
/mob/verb/subtler_anti_ghost_wrapper()
set name = "subtle-anti-ghost-wrapper"
set category = null
set hidden = TRUE
var/message = input(src, "", "subtler-anti-ghost (text)") as message|null
if(!message)
return
subtler_anti_ghost_verb(message)
/mob/verb/subtler_anti_ghost_verb(message as message)
set name = "Subtler Anti-Ghost"
set desc = "Emote to people within 1 tile (3x3) of (or inside) yourself. Ghosts cannot see this."
set category = VERB_CATEGORY_IC
// ensure length isn't too high
message = sanitize_or_reflect(message, src, encode = FALSE)
if(!message)
return
// clear typing indicator
set_typing_indicator(FALSE)
// perform emote
run_custom_emote(message, TRUE, TRUE, SAYCODE_TYPE_ALWAYS)

View File

@@ -0,0 +1,33 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/**
* The transmission end of saycode.
*/
/**
* Gets everyone (and everything) in a given range that can hear our say / emote.
*
* * Does not check if they are blind / deaf / whatever
* * Not specifying global or exclude observers means observers can only hear if they're in range.
* * This does **not** check ghost ears/sight prefs.
*
* @params
* * range - tile radius; 0 is 1x1 on yourself, 1 is 3x3, ...
* * global_observers - all observers are included
* * exclude_observers - prevent **any** observer from being included. overrides `global_observers`.
*/
/mob/proc/saycode_view_query(range, global_observers, exclude_observers)
. = get_hearers_in_view(range, src)
if(exclude_observers)
for(var/mob/observer/observer in .)
. -= observer
else if(global_observers)
var/list/already_gotten = list()
for(var/mob/observer/observer in .)
already_gotten[observer] = TRUE
for(var/mob/observer/observer in GLOB.ghost_list)
if(already_gotten[observer])
continue
. += observer

View File

@@ -7,6 +7,23 @@
/// * Lazy list, see mob_movespeed.dm
var/list/actionspeed_modifier_immunities
//* HUD (Atom) *//
/// HUDs to initialize, typepaths
var/list/atom_huds_to_initialize
//* Buckling *//
/// Atom we're buckled to
var/atom/movable/buckled
/// Atom we're buckl**ing** to. Used to stop stuff like lava from incinerating those who are mid buckle.
// todo: can this be put in an existing bitfield somewhere else?
var/atom/movable/buckling
//* Emotes *//
/// running emotes, associated to context the emote can set
var/list/datum/emote/emotes_running
/// our default emote classes
var/emote_class = EMOTE_CLASS_IS_BODY
//* Impairments *//
/// active feign_impairment types
/// * lazy list

View File

@@ -58,17 +58,6 @@
/// current datum that's entirely intercepting our movements. only can have one - this is usually used with perspective.
var/datum/movement_intercept
//* Buckling *//
/// Atom we're buckled to
var/atom/movable/buckled
/// Atom we're buckl**ing** to. Used to stop stuff like lava from incinerating those who are mid buckle.
// todo: can this be put in an existing bitfield somewhere else?
var/atom/movable/buckling
//* HUD (Atom) *//
/// HUDs to initialize, typepaths
var/list/atom_huds_to_initialize
//* HUD *//
/// active, opened storage
// todo: doesn't clear from clients properly on logout, relies on login clearing screne.
@@ -201,8 +190,6 @@
*/
var/atom/movable/screen/zone_sel/zone_sel = null
/// Allows all mobs to use the me verb by default, will have to manually specify they cannot.
var/use_me = 1
var/damageoverlaytemp = 0
var/computer_id = null
var/obj/machinery/machine = null

View File

@@ -0,0 +1,9 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/mob/observer/dead/process_emote(datum/emote/emote, raw_parameter_string, datum/event_args/actor/actor, used_binding)
actor.chat_feedback(
SPAN_WARNING("Ghosts can't send normal emotes!"),
target = src,
)
return FALSE

View File

@@ -0,0 +1,54 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/mob/observer/dead/saycode_view_query(range, global_observers, exclude_observers)
if(global_observers)
return GLOB.ghost_list.Copy()
. = list()
for(var/mob/observer/observer as anything in GLOB.ghost_list)
if(get_dist(src, observer) <= range)
. += observer
/mob/observer/dead/run_custom_emote(emote_text, subtle, anti_ghost, saycode_type, datum/event_args/actor/actor, with_overhead)
// Now watch, as I violate a codebase best practice for shits and giggles.
if(!usr)
return FALSE
if(usr != src)
to_chat(usr, SPAN_DANGER("How did you just try to emote as another player's ghost?"))
return FALSE
// Validated to be 'self' at this point.
if(!client)
return FALSE
if(client.prefs.muted & MUTE_DEADCHAT)
to_chat(src, SPAN_DANGER("You cannot send deadchat emotes (muted)."))
return FALSE
if(!get_preference_toggle(/datum/game_preference_toggle/chat/dsay))
to_chat(src, SPAN_DANGER("You have deadchat muted."))
return FALSE
if(!config_legacy.dsay_allowed && !check_rights(show_msg = FALSE, C = client))
to_chat(src, SPAN_DANGER("Deadchat is globally muted."))
return FALSE
if(SSbans.t_is_role_banned_ckey(ckey, role = BAN_ROLE_OOC))
to_chat(src, SPAN_DANGER("You are banned from OOC and deadchat."))
return FALSE
if(subtle)
to_chat(src, SPAN_DANGER("Why are you trying to subtle emote as a ghost?"))
return FALSE
if(anti_ghost)
to_chat(src, SPAN_DANGER("Why are you trying to anti-ghost emote as a ghost?"))
return FALSE
log_ghostemote(emote_text, src)
var/raw_html = process_custom_emote(emote_text, subtle, anti_ghost, saycode_type, with_overhead)
if(!raw_html)
return
emit_custom_emote(raw_html, subtle, anti_ghost, saycode_type, with_overhead)
/mob/observer/dead/process_custom_emote(emote_text, subtle, anti_ghost, saycode_type, with_overhead)
. = emote_text
. = html_encode(emote_text)
. = say_emphasis(.)
. = SPAN_DEADSAY(emoji_parse(.))
/mob/observer/dead/emit_custom_emote(raw_html, subtle, anti_ghost, saycode_type, with_overhead)
say_dead_direct(raw_html, src)

View File

@@ -1,5 +1,5 @@
/// all player ghosts
GLOBAL_LIST_EMPTY(observer_list)
GLOBAL_LIST_EMPTY(ghost_list)
/mob/observer/dead
name = "ghost"
@@ -101,15 +101,15 @@ GLOBAL_LIST_EMPTY(observer_list)
//For a better follow selection:
/mob/observer/dead/Initialize(mapload)
GLOB.observer_list += src
GLOB.ghost_list += src
var/mob/body = loc
see_invisible = SEE_INVISIBLE_OBSERVER
see_in_dark = world.view //I mean. I don't even know if byond has occlusion culling... but...
plane = OBSERVER_PLANE //Why doesn't the var above work...???
add_verb(src, /mob/observer/dead/proc/dead_tele)
var/turf/T
if(ismob(body))
var/turf/T
T = get_turf(body) //Where is the body located?
attack_log = body.attack_log //preserve our attack logs by copying them to our ghost
@@ -142,11 +142,11 @@ GLOBAL_LIST_EMPTY(observer_list)
mind = body.mind //we don't transfer the mind but we keep a reference to it.
if(!T)
T = SSjob.get_latejoin_spawnpoint()
if(!T)
T = locate(1,1,1)
forceMove(T)
if(!T)
T = SSjob.get_latejoin_spawnpoint()
if(!T)
T = locate(1,1,1)
forceMove(T)
if(!name) //To prevent nameless ghosts
name = capitalize(pick(GLOB.first_names_male)) + " " + capitalize(pick(GLOB.last_names))
@@ -154,7 +154,7 @@ GLOBAL_LIST_EMPTY(observer_list)
return ..()
/mob/observer/dead/Destroy()
GLOB.observer_list -= src
GLOB.ghost_list -= src
return ..()
/mob/observer/dead/Topic(href, href_list)
@@ -790,7 +790,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
if(sound)
SEND_SOUND(src, sound(sound))
/mob/dead/observer/canUseTopic(atom/movable/M, be_close=FALSE, no_dexterity=FALSE, no_tk=FALSE)
/mob/observer/dead/canUseTopic(atom/movable/M, be_close=FALSE, no_dexterity=FALSE, no_tk=FALSE)
return isAdminGhostAI(usr)
/mob/observer/dead/verb/nifjoin()

View File

@@ -14,50 +14,3 @@
return
. = src.say_dead(message)
/mob/observer/dead/emote(var/act, var/type, var/message)
//message = sanitize(message) - already sanitized in verb/me_verb()
if(!message)
return
if(act != "me")
return
log_ghostemote(message, src)
if(src.client)
if(message)
client.handle_spam_prevention(MUTE_DEADCHAT)
if(src.client.prefs.muted & MUTE_DEADCHAT)
to_chat(src, "<font color='red'>You cannot emote in deadchat (muted).</font>")
return
. = src.emote_dead(message)
/*
for (var/mob/M in hearers(null, null))
if (!M.stat)
if(M.job == "Chaplain")
if (prob (49))
M.show_message("<span class='game'><i>You hear muffled speech... but nothing is there...</i></span>", 2)
if(prob(20))
playsound(src.loc, pick('sound/effects/ghost.ogg','sound/effects/ghost2.ogg'), 10, 1)
else
M.show_message("<span class='game'><i>You hear muffled speech... you can almost make out some words...</i></span>", 2)
// M.show_message("<span class='game'><i>[stutter(message)]</i></span>", 2)
if(prob(30))
playsound(src.loc, pick('sound/effects/ghost.ogg','sound/effects/ghost2.ogg'), 10, 1)
else
if (prob(50))
return
else if (prob (95))
M.show_message("<span class='game'><i>You hear muffled speech... but nothing is there...</i></span>", 2)
if(prob(20))
playsound(src.loc, pick('sound/effects/ghost.ogg','sound/effects/ghost2.ogg'), 10, 1)
else
M.show_message("<span class='game'><i>You hear muffled speech... you can almost make out some words...</i></span>", 2)
// M.show_message("<span class='game'><i>[stutter(message)]</i></span>", 2)
playsound(src.loc, pick('sound/effects/ghost.ogg','sound/effects/ghost2.ogg'), 10, 1)
*/

View File

@@ -252,4 +252,4 @@
* TODO: this doesn't work on client global planes like admin plane.
*/
/mob/proc/can_see_plane(val)
return val <= BYOND_PLANE || val >= HUD_PLANE || self_perspective.is_plane_visible(val)
return val <= BYOND_PLANE || val >= HUD_PLANE || self_perspective?.is_plane_visible(val)

View File

@@ -1,134 +1,3 @@
//////////////////////////////////////////////////////
////////////////////SUBTLE COMMAND////////////////////
//////////////////////////////////////////////////////
/mob/verb/me_verb_subtle(message as message) //This would normally go in say.dm
set name = "Subtle"
set category = VERB_CATEGORY_IC
set desc = "Emote to nearby people (and your pred/prey)"
message = sanitize_or_reflect(message,src) // Reflect too-long messages (within reason)
if(!message)
return
set_typing_indicator(FALSE)
if(use_me)
usr.emote_vr("me",4,message)
else
usr.emote_vr(message)
/mob/proc/custom_emote_vr(var/m_type=1,var/message = null) //This would normally go in emote.dm
if(stat || !use_me && usr == src)
to_chat(src, "You are unable to emote.")
return
var/muzzled = is_muzzled()
if(m_type == 2 && muzzled) return
var/input
if(!message)
input = sanitize_or_reflect(input(src,"Choose an emote to display.") as text|null, src)
else
input = message
if(input)
log_subtle(message,src)
message = "<B>[src]</B> <I>[input]</I>"
else
return
if (message)
message = say_emphasis(message)
SEND_SIGNAL(src, COMSIG_MOB_SUBTLE_EMOTE, src, message)
var/list/vis = get_mobs_and_objs_in_view_fast(get_turf(src),1,2) //Turf, Range, and type 2 is emote
var/list/vis_mobs = vis["mobs"]
var/list/vis_objs = vis["objs"]
for(var/vismob in vis_mobs)
var/mob/M = vismob
if(istype(vismob, /mob/observer))
if(M.client && !M.client.get_preference_toggle(/datum/game_preference_toggle/observer/ghost_subtle))
continue
spawn(0)
M.show_message(message, SAYCODE_TYPE_ALWAYS)
for(var/visobj in vis_objs)
var/obj/O = visobj
spawn(0)
O.see_emote(src, message, 2)
var/list/other_viewers = get_hearers_in_view(3, source = src)
for(var/mob/M in other_viewers - vis_mobs)
M.show_message(SPAN_SMALL("<i>[src] does something [pick("subtly", "discreetly", "hidden", "obscured")].</i>"), SAYCODE_TYPE_VISIBLE)
/mob/proc/emote_vr(var/act, var/type, var/message) //This would normally go in say.dm
if(act == "me")
return custom_emote_vr(type, message)
//////// SHIT COPYPASTE CODE FOR SUBTLER ANTI GHOST
/mob/verb/subtler_anti_ghost(message as message) //This would normally go in say.dm
set name = "Subtler Anti Ghost"
set category = VERB_CATEGORY_IC
set desc = "Emote to nearby people (and your pred/prey), but ghosts can't see it."
message = sanitize_or_reflect(message,src) // Reflect too-long messages (within reason)
if(!message)
return
set_typing_indicator(FALSE)
run_subtler(message)
/mob/proc/run_subtler(message)
if(stat || !use_me && usr == src)
to_chat(src, "You are unable to emote.")
return
var/input
if(!message)
input = sanitize_or_reflect(input(src,"Choose an emote to display.") as text|null, src)
else
input = message
if(input)
log_subtle_anti_ghost(message,src)
message = "<B>[src]</B> " + SPAN_SINGING(input)
else
return
if (message)
message = say_emphasis(message)
SEND_SIGNAL(src, COMSIG_MOB_SUBTLE_EMOTE, src, message)
var/turf_origin = get_turf(src)
var/list/vis = get_mobs_and_objs_in_view_fast(turf_origin,1,2) //Turf, Range, and type 2 is emote
var/list/vis_mobs = vis["mobs"]
var/list/vis_objs = vis["objs"]
for(var/vismob in vis_mobs)
var/mob/M = vismob
if(istype(vismob, /mob/observer))
continue
if(M.stat == DEAD)
continue // get mobs and objs in view fast is shitty; say refactor will deal with that
if(M != src)
M.playsound_local(turf_origin, 'sound/effects/subtle_emote.ogg', 100)
M.show_message(message, SAYCODE_TYPE_ALWAYS)
for(var/visobj in vis_objs)
var/obj/O = visobj
O.see_emote(src, message, 2)
var/list/other_viewers = get_hearers_in_view(3, source = src)
for(var/mob/M in (other_viewers - vis_mobs))
if(istype(M, /mob/observer))
continue
M.show_message(SPAN_SMALL("<i>[src] does something [pick("subtly", "discreetly", "hidden", "obscured")].</i>"), SAYCODE_TYPE_VISIBLE)
/////// END
//////// SHITTIER COPYPASTE CODE FOR VORE SUBTLE
/mob/living/verb/subtle_vore(message as message)
set name = "Subtle Vore"
set category = "Vore"
@@ -142,7 +11,7 @@
run_subtle_vore(message)
/mob/living/proc/run_subtle_vore(message)
if(stat || !use_me && usr == src)
if(stat)
to_chat(src, "You are unable to emote.")
return
@@ -160,7 +29,6 @@
if (message)
message = say_emphasis(message)
SEND_SIGNAL(src, COMSIG_MOB_SUBTLE_EMOTE, src, message)
var/list/vis_mobs = new()
var/list/vis_objs = new()
@@ -199,17 +67,16 @@
var/obj/O = visobj
O.see_emote(src, message, 2)
//////// END
#define MAX_HUGE_MESSAGE_LEN 8192
#define POST_DELIMITER_STR "\<\>"
/proc/sanitize_or_reflect(message,user)
// TODO: NUKE THIS PROC!!
/proc/sanitize_or_reflect(message, user, encode)
//Way too long to send
if(length_char(message) > MAX_HUGE_MESSAGE_LEN)
fail_to_chat(user, null)
return
message = sanitize(message, max_length = MAX_HUGE_MESSAGE_LEN)
message = sanitize(message, max_length = MAX_HUGE_MESSAGE_LEN, encode = encode)
//Came back still too long to send
if(length_char(message) > MAX_MESSAGE_LEN)

View File

@@ -20,25 +20,3 @@
init_typing_indicator()
add_overlay(typing_indicator, TRUE)
typing = TRUE
/mob/verb/say_wrapper()
set name = ".Say"
set hidden = 1
set_typing_indicator(TRUE)
var/message = input("","say (text)") as text|null
set_typing_indicator(FALSE)
if(message)
say_verb(message)
/mob/verb/me_wrapper()
set name = ".Me"
set hidden = 1
set_typing_indicator(TRUE)
var/message = input("","me (text)") as message|null
set_typing_indicator(FALSE)
if(message)
me_verb(message)

View File

@@ -129,12 +129,13 @@
log_nsay(message,nif.human.real_name,sender)
/datum/nifsoft/soulcatcher/proc/emote_into(var/message, var/mob/living/sender, var/mob/eyeobj)
/datum/nifsoft/soulcatcher/proc/emote_into(var/message, var/mob/living/sender, var/mob/eyeobj, skip_name)
message = trim(message)
if(!length(message))
return
message = say_emphasis(message)
var/sender_name = eyeobj ? eyeobj.name : sender.name
var/maybe_sender_name = skip_name ? "" : "<b>[sender_name]</b> "
//AR Projecting
if(eyeobj)
@@ -142,9 +143,9 @@
//Not AR Projecting
else
to_chat(nif.human,"<span class='emote nif'><b>\[[icon2html(thing = nif.big_icon, target = nif.human)]NIF\]</b> <b>[sender_name]</b> [message]</span>")
to_chat(nif.human,"<span class='emote nif'><b>\[[icon2html(thing = nif.big_icon, target = nif.human)]NIF\]</b> [maybe_sender_name][message]</span>")
for(var/mob/living/carbon/brain/caught_soul/CS as anything in brainmobs)
to_chat(CS,"<span class='emote nif'><b>\[[icon2html(thing = nif.big_icon, target = CS)]NIF\]</b> <b>[sender_name]</b> [message]</span>")
to_chat(CS,"<span class='emote nif'><b>\[[icon2html(thing = nif.big_icon, target = CS)]NIF\]</b> [maybe_sender_name][message]</span>")
log_nme(message,nif.human.real_name,sender)
@@ -346,7 +347,6 @@
/mob/living/carbon/brain/caught_soul
name = "recorded mind"
desc = "A mind recorded and being played on digital hardware."
use_me = 1
var/ext_deaf = FALSE //Forbidden from 'ear' access on host
var/ext_blind = FALSE //Forbidden from 'eye' access on host
var/parent_mob = FALSE //If we've captured our owner
@@ -431,14 +431,12 @@
return FALSE
..()
/mob/living/carbon/brain/caught_soul/me_verb_subtle()
/mob/living/carbon/brain/caught_soul/subtle_verb()
set hidden = TRUE
return FALSE
/mob/living/carbon/brain/caught_soul/whisper()
set hidden = TRUE
return FALSE
/mob/living/carbon/brain/caught_soul/face_atom(var/atom/A)
@@ -474,6 +472,9 @@
return TRUE
/mob/living/carbon/brain/caught_soul/emote(var/act,var/m_type=1,var/message = null)
. = ..()
if(. == "stop")
return
if(silent)
return FALSE
if (act == "me")
@@ -491,10 +492,11 @@
else
return FALSE
/mob/living/carbon/brain/caught_soul/custom_emote(var/m_type, var/message)
if(silent)
return FALSE
soulcatcher.emote_into(message,src,eyeobj)
/mob/living/carbon/brain/caught_soul/emit_custom_emote(raw_html, subtle, anti_ghost, saycode_type, with_overhead)
if(subtle || anti_ghost)
to_chat(src, SPAN_BOLDANNOUNCE("Your [SPAN_TOOLTIP(raw_html, "message")] was not sent. Soulcatchers do not currently support subtle or subtler-anti-ghost."))
return
soulcatcher.emote_into(raw_html, src, eyeobj, skip_name = TRUE)
/mob/living/carbon/brain/caught_soul/resist()
set name = "Resist"

View File

@@ -70,6 +70,9 @@
buckle_allowed = TRUE
buckle_flags = BUCKLING_GROUND_HOIST //blobsurfing
/mob/living/simple_mob/protean_blob/isSynthetic()
return TRUE
/datum/say_list/protean_blob
speak = list("Blrb?","Sqrsh.","Glrsh!")
emote_hear = list("squishes softly","spluts quietly","makes wet noises")
@@ -432,7 +435,7 @@
if(back)
H.equip_to_slot_if_possible(prig,SLOT_ID_BACK, INV_OP_FORCE | INV_OP_DIRECTLY_EQUIPPING | INV_OP_SHOULD_NOT_INTERCEPT | INV_OP_SILENT)
return
if(isturf(loc))
var/obj/item/hardsuit/protean/prig

View File

@@ -1,3 +1,4 @@
#include "characteristics.dm"
#include "inventory.dm"
#include "sprite_accessories.dm"
#include "subtler_anti_ghost.dm"

View File

@@ -0,0 +1,93 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
/datum/unit_test/subtler_anti_ghost_still_works
var/intended_recipient_heard_subtler = FALSE
var/unintended_recipient_heard_subtler = FALSE
var/intended_recipient_heard_normal = FALSE
var/unintended_recipient_heard_normal = FALSE
var/send_str_subtler = "isnt-there-someone-you-forgot-to-ask"
var/send_str_normal = "kynde-did-nothing-wrong"
var/mob/speaker
/datum/unit_test/subtler_anti_ghost_still_works/Destroy()
if(!QDELETED(speaker))
qdel(speaker)
speaker = null
return ..()
/datum/unit_test/subtler_anti_ghost_still_works/Run()
var/mob/living/carbon/human/speaker = allocate(/mob/living/carbon/human)
var/mob/living/carbon/human/listener = allocate(/mob/living/carbon/human)
var/mob/observer/dead/the_myth_of_consensual_handholding = allocate(/mob/observer/dead)
src.speaker = speaker
RegisterSignal(listener, COMSIG_MOB_ON_RECEIVE_CUSTOM_EMOTE, PROC_REF(on_intended_hear))
RegisterSignal(the_myth_of_consensual_handholding, COMSIG_MOB_ON_RECEIVE_CUSTOM_EMOTE, PROC_REF(on_unintended_hear))
speaker.forceMove(run_loc_floor_bottom_left)
the_myth_of_consensual_handholding.forceMove(run_loc_floor_bottom_left)
intended_recipient_heard_normal = intended_recipient_heard_subtler = FALSE
unintended_recipient_heard_normal = unintended_recipient_heard_subtler = FALSE
speaker.subtler_anti_ghost_verb(send_str_subtler)
speaker.me_verb(send_str_normal)
if(!intended_recipient_heard_subtler)
Fail("A person next to the couldn't hear the subtler emote")
if(!intended_recipient_heard_normal)
Fail("A person next to the speaker couldn't hear the normal emote")
if(unintended_recipient_heard_subtler)
Fail("A ghost next to the speaker could hear the subtler emote")
if(!unintended_recipient_heard_normal)
Fail("A ghost next to the speaker couldn't hear the normal emote")
var/distance = world_view_max_number() + 2
var/turf/some_distance_away = locate(the_myth_of_consensual_handholding.x + distance, the_myth_of_consensual_handholding.y + distance, the_myth_of_consensual_handholding.z)
if(!some_distance_away)
Fail("some_distance_away wasn't found")
the_myth_of_consensual_handholding.forceMove(some_distance_away)
intended_recipient_heard_normal = intended_recipient_heard_subtler = FALSE
unintended_recipient_heard_normal = unintended_recipient_heard_subtler = FALSE
speaker.me_verb(send_str_normal)
speaker.subtler_anti_ghost_verb(send_str_subtler)
if(unintended_recipient_heard_subtler)
Fail("A ghost [distance] tiles away from the speaker could hear the subtler emote")
if(!unintended_recipient_heard_normal)
Fail("A ghost [distance] tiles away from the speaker couldn't hear the normal emote")
var/turf/one_z_away = locate(the_myth_of_consensual_handholding.x, the_myth_of_consensual_handholding.y, the_myth_of_consensual_handholding.z - 1)
if(!one_z_away)
Fail("one_z_away wasn't found")
the_myth_of_consensual_handholding.forceMove(one_z_away)
intended_recipient_heard_normal = intended_recipient_heard_subtler = FALSE
unintended_recipient_heard_normal = unintended_recipient_heard_subtler = FALSE
speaker.me_verb(send_str_normal)
speaker.subtler_anti_ghost_verb(send_str_subtler)
if(unintended_recipient_heard_subtler)
Fail("A ghost a z-level from the speaker could hear the subtler emote")
if(!unintended_recipient_heard_normal)
Fail("A ghost a z-level from the speaker couldn't hear the normal emote")
/datum/unit_test/subtler_anti_ghost_still_works/proc/on_intended_hear(datum/source, mob/from_mob, raw_html, subtle, anti_ghost, saycode_type)
if(from_mob != speaker)
return
if(findtext_char(raw_html, send_str_normal))
intended_recipient_heard_normal = TRUE
else if(findtext_char(raw_html, send_str_subtler))
intended_recipient_heard_subtler = TRUE
else
Fail("Intended recipient heard something other than the predicted lines: [raw_html].")
/datum/unit_test/subtler_anti_ghost_still_works/proc/on_unintended_hear(datum/source, mob/from_mob, raw_html, subtle, anti_ghost, saycode_type)
if(from_mob != speaker)
return
if(findtext_char(raw_html, send_str_normal))
unintended_recipient_heard_normal = TRUE
else if(findtext_char(raw_html, send_str_subtler))
unintended_recipient_heard_subtler = TRUE
else
Fail("Unintended recipient heard something other than the predicted lines: [raw_html].")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More