From 4962a86b9594fbaabbef47606dd7224006c67d0f Mon Sep 17 00:00:00 2001
From: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
Date: Sat, 11 Jul 2026 10:52:28 -0500
Subject: [PATCH] CSL / Mutual understanding changes (#96774)
## About The Pull Request
1. Redid the formula for calculating mutual language understanding
Old: `base + (10 * (1 - (min(commonness, 1250) / 500)))`, or in other
words `base + 10% to -15%`
New: `base + max(20 - (commonness / 1000) * 20, -5)`, or in other words
`base + 20% to -5%`
2. CSL strength preference has been changed
Old: `"90%", "75%", "50%", "33%", "25%", "10%"`
New: `"75%", "50%", "25%"`
3. If you understand a language partially, the icon in chat is
differentiated slightly
(Icon subject to change, it's just a `?` spliced with the icon rn)
4. CSL can now be taken by humans, doing so grants you gal-uncommon
5. Fixed a bug where contractions, due to being encoded, would be
treated as lower frequency / rarer and thus be more likely to be
translated
## Why It's Good For The Game
1. The current formula was a bit too harsh and resulted in more words
being translated than you'd expect. The altered formula should make it
less persistent.
2. These values were tweaked according to the new formula. If I left the
90% value it'd mean like every word would get translated and that
defeats the point.
3. Makes things a bit clearer when it comes to partial understanding.
4. Requested, sounds like fun so why not.
5. Bugfix.
## Changelog
:cl: Melbert
fix: Fixed a bug where contractions were treated as rarer words than
they should be for the sake of partial language understanding
balance: Tweaked the formula of partial language understanding: Less
common words are penalized less, more common words are rewarded more. In
other words more words will be understood on average.
balance: Common Second Language numbers were tweaked according to the
new formula. Now "75%", "50%", "25%".
balance: Humans can now take Common Second Language. Doing so also
grants Galactic Uncommon.
add: Partially understood sentences now get an altered language icon in
chat and runechat, indicating that only some of the words were
translated.
/:cl:
---
code/__DEFINES/language.dm | 21 +++++++--
code/datums/chatmessage.dm | 45 +++++++++++--------
code/datums/quirks/negative_quirks/csl.dm | 15 ++++---
code/game/atoms_movable.dm | 8 ++++
code/game/say.dm | 7 ++-
code/modules/asset_cache/assets/chat.dm | 16 ++++---
code/modules/client/preferences/language.dm | 4 +-
code/modules/language/_language.dm | 30 ++++++-------
code/modules/language/_language_holder.dm | 8 ++++
code/modules/language/carptongue.dm | 2 +-
code/modules/language/codespeak.dm | 2 +-
code/modules/language/common.dm | 2 +-
code/modules/language/draconic.dm | 2 +-
code/modules/language/drone.dm | 2 +-
code/modules/language/machine.dm | 2 +-
code/modules/language/spinwarder.dm | 2 +-
code/modules/language/uncommon.dm | 2 +-
code/modules/mob/living/carbon/carbon_say.dm | 2 +-
icons/ui/chat/language.dmi | Bin 5437 -> 5763 bytes
19 files changed, 110 insertions(+), 62 deletions(-)
diff --git a/code/__DEFINES/language.dm b/code/__DEFINES/language.dm
index 937fc987aa6..1ae7d898203 100644
--- a/code/__DEFINES/language.dm
+++ b/code/__DEFINES/language.dm
@@ -1,7 +1,20 @@
-#define NO_STUTTER 1
-#define TONGUELESS_SPEECH 2
-#define LANGUAGE_HIDE_ICON_IF_UNDERSTOOD 4
-#define LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD 8
+// Language flags
+/// Language is never stuttered
+#define LANGUAGE_NO_STUTTER (1<<0)
+/// Language is speakable without a tongue
+#define LANGUAGE_TONGUELESS_SPEECH (1<<1)
+/// Language icon is hidden if the language is understood
+#define LANGUAGE_HIDE_ICON_IF_UNDERSTOOD (1<<2)
+/// Language icon is hidden if the language is not understood
+#define LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD (1<<3)
+
+// Returns from display_icon_type
+/// Display no icon for this language
+#define DISPLAY_LANGUAGE_ICON_NONE 0
+/// Display the full icon for this language
+#define DISPLAY_LANGUAGE_ICON_FULL 1
+/// Display a blended icon for this language, indicating partial understanding
+#define DISPLAY_LANGUAGE_ICON_PARTIAL 2
// LANGUAGE SOURCE DEFINES
/// For use in full removal only.
diff --git a/code/datums/chatmessage.dm b/code/datums/chatmessage.dm
index cf555170b68..dce1ca6791d 100644
--- a/code/datums/chatmessage.dm
+++ b/code/datums/chatmessage.dm
@@ -64,7 +64,7 @@
* * extra_classes - Extra classes to apply to the span that holds the text
* * lifespan - The lifespan of the message in deciseconds
*/
-/datum/chatmessage/New(text, atom/target, mob/owner, datum/language/language, list/extra_classes = list(), lifespan = CHAT_MESSAGE_LIFESPAN)
+/datum/chatmessage/New(text, atom/target, mob/owner, datum/language/language, list/extra_classes = list(), lifespan = CHAT_MESSAGE_LIFESPAN, list/message_mods)
. = ..()
if (!istype(target))
CRASH("Invalid target given for chatmessage")
@@ -72,7 +72,7 @@
stack_trace("/datum/chatmessage created with [isnull(owner) ? "null" : "invalid"] mob owner")
qdel(src)
return
- INVOKE_ASYNC(src, PROC_REF(generate_image), text, target, owner, language, extra_classes, lifespan)
+ INVOKE_ASYNC(src, PROC_REF(generate_image), text, target, owner, language, extra_classes, lifespan, message_mods)
/datum/chatmessage/Destroy()
if (!QDELING(owned_by))
@@ -109,8 +109,9 @@
* * language - The language this message was spoken in
* * extra_classes - Extra classes to apply to the span that holds the text
* * lifespan - The lifespan of the message in deciseconds
+ * * message_mods - All mods from whatever message triggered this (if applicable)
*/
-/datum/chatmessage/proc/generate_image(text, atom/target, mob/owner, datum/language/language, list/extra_classes, lifespan)
+/datum/chatmessage/proc/generate_image(text, atom/target, mob/owner, datum/language/language, list/extra_classes, lifespan, list/message_mods)
/// Cached icons to show what language the user is speaking
var/static/list/language_icons
@@ -171,12 +172,16 @@
// Append language icon if the language uses one
var/datum/language/language_instance = GLOB.language_datum_instances[language]
- if (language_instance?.display_icon(owner))
- var/icon/language_icon = LAZYACCESS(language_icons, language)
+ var/language_icon_type = language_instance?.display_icon_type(owner, message_mods) || DISPLAY_LANGUAGE_ICON_NONE
+ if(language_icon_type != DISPLAY_LANGUAGE_ICON_NONE)
+ var/language_icon_key = "[language][language_icon_type == DISPLAY_LANGUAGE_ICON_PARTIAL ? "-partial" : ""]"
+ var/icon/language_icon = LAZYACCESS(language_icons, language_icon_key)
if (isnull(language_icon))
language_icon = icon(language_instance.icon, icon_state = language_instance.icon_state)
+ if(language_icon_type == DISPLAY_LANGUAGE_ICON_PARTIAL)
+ language_icon.Blend(icon('icons/ui/chat/language.dmi', "unknown"), ICON_OVERLAY)
language_icon.Scale(CHAT_MESSAGE_ICON_SIZE, CHAT_MESSAGE_ICON_SIZE)
- LAZYSET(language_icons, language, language_icon)
+ LAZYSET(language_icons, language_icon_key, language_icon)
LAZYADD(prefixes, "\icon[language_icon]")
text = "[prefixes?.Join(" ")][text]"
@@ -319,30 +324,34 @@
* * raw_message - The text content of the message
* * spans - Additional classes to be added to the message
*/
-/mob/proc/create_chat_message(atom/movable/speaker, datum/language/message_language, raw_message, list/spans, runechat_flags = NONE)
+/mob/proc/create_chat_message(atom/movable/speaker, datum/language/message_language, raw_message, list/spans, runechat_flags = NONE, list/message_mods)
if(SSlag_switch.measures[DISABLE_RUNECHAT] && !HAS_TRAIT(speaker, TRAIT_BYPASS_MEASURES))
return
if(HAS_TRAIT(speaker, TRAIT_RUNECHAT_HIDDEN))
return
// Ensure the list we are using, if present, is a copy so we don't modify the list provided to us
- spans = spans ? spans.Copy() : list()
+ var/list/classes = (runechat_flags & EMOTE_MESSAGE) ? list("emote", "italics") : LAZYCOPY(spans)
// Check for virtual speakers (aka hearing a message through a radio)
- var/atom/movable/originalSpeaker = speaker
- if (istype(speaker, /atom/movable/virtualspeaker))
- var/atom/movable/virtualspeaker/v = speaker
- speaker = v.source
- spans |= "virtual-speaker"
+ var/atom/movable/original_speaker = speaker
+ if (!(runechat_flags & EMOTE_MESSAGE) && istype(speaker, /atom/movable/virtualspeaker))
+ var/atom/movable/virtualspeaker/virtual_speaker = speaker
+ speaker = virtual_speaker.source
+ classes |= "virtual-speaker"
// Ignore virtual speaker (most often radio messages) from ourselves
- if (originalSpeaker != src && speaker == src)
+ if (original_speaker != src && speaker == src)
return
// Display visual above source
- if(runechat_flags & EMOTE_MESSAGE)
- new /datum/chatmessage(raw_message, speaker, src, message_language, list("emote", "italics"))
- else
- new /datum/chatmessage(raw_message, speaker, src, message_language, spans)
+ new /datum/chatmessage(
+ text = raw_message,
+ target = speaker,
+ owner = src,
+ language = message_language,
+ extra_classes = classes,
+ message_mods = message_mods,
+ )
#undef CHAT_LAYER_MAX_Z
#undef CHAT_LAYER_Z_STEP
diff --git a/code/datums/quirks/negative_quirks/csl.dm b/code/datums/quirks/negative_quirks/csl.dm
index 68a8f58be68..752a7d2dc31 100644
--- a/code/datums/quirks/negative_quirks/csl.dm
+++ b/code/datums/quirks/negative_quirks/csl.dm
@@ -10,13 +10,14 @@
lose_text = span_notice("Common starts to click for you.")
medical_record_text = "Patient is CSL."
/// What language typepath is our primary language?
- var/native_language
+ VAR_PRIVATE/native_language
/datum/quirk/csl/add(client/client_source)
- if(iscarbon(quirk_holder))
- quirk_holder.remove_language(/datum/language/common, UNDERSTOOD_LANGUAGE, LANGUAGE_SPECIES)
- else
- quirk_holder.remove_language(/datum/language/common, UNDERSTOOD_LANGUAGE, LANGUAGE_ATOM)
+ quirk_holder.remove_language(/datum/language/common, UNDERSTOOD_LANGUAGE, LANGUAGE_SPECIES)
+ quirk_holder.remove_language(/datum/language/common, UNDERSTOOD_LANGUAGE, LANGUAGE_ATOM)
+ if(length(quirk_holder.get_understood_languages()) < 1)
+ quirk_holder.grant_language(/datum/language/uncommon, SPOKEN_LANGUAGE|UNDERSTOOD_LANGUAGE, LANGUAGE_QUIRK)
+
quirk_holder.grant_partial_language(/datum/language/common, text2num(client_source?.prefs?.read_preference(/datum/preference/choiced/csl_strength)) || 90, type)
RegisterSignal(quirk_holder, COMSIG_SPECIES_GAIN, PROC_REF(reremove_common))
RegisterSignal(quirk_holder, COMSIG_MOB_SAY, PROC_REF(translate_parts))
@@ -30,6 +31,7 @@
return
quirk_holder.remove_partial_language(/datum/language/common, type)
+ quirk_holder.remove_language(/datum/language/uncommon, SPOKEN_LANGUAGE|UNDERSTOOD_LANGUAGE, LANGUAGE_QUIRK)
var/mob/living/carbon/carbon_quirk_holder = quirk_holder
if(istype(carbon_quirk_holder) && carbon_quirk_holder.dna.species)
// only give back common if they're a species that should speak it
@@ -78,10 +80,9 @@
if(say_args[SPEECH_FORCED] || isnull(native_language) || quirk_holder.mob_mood?.sanity > 75)
return
- // init this list if nothing else has
- LAZYINITLIST(say_args[SPEECH_MODS][LANGUAGE_MUTUAL_BONUS])
// force speak language, add mutual bonuses so everyone else can understand
say_args[SPEECH_LANGUAGE] = native_language
+ say_args[SPEECH_MODS][LANGUAGE_MUTUAL_BONUS] ||= list()
say_args[SPEECH_MODS][LANGUAGE_MUTUAL_BONUS][native_language] = max(round(8 * sqrt(quirk_holder.mob_mood?.sanity), 5), say_args[SPEECH_MODS][LANGUAGE_MUTUAL_BONUS][native_language])
/datum/quirk_constant_data/csl
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index 5475913e1c7..2ffa7e29ac9 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -1598,6 +1598,10 @@
/atom/movable/proc/has_language(language, flags_to_check)
return get_language_holder().has_language(language, flags_to_check)
+/// Checks if atom has the language. If spoken is true, only checks if atom can speak the language.
+/atom/movable/proc/has_partial_language(language)
+ return get_language_holder().has_partial_language(language)
+
/// Checks if atom can speak the language.
/atom/movable/proc/can_speak_language(language)
return get_language_holder().can_speak_language(language)
@@ -1623,6 +1627,10 @@
/atom/movable/proc/get_random_spoken_language()
return get_language_holder().get_random_spoken_language()
+/// Gets a list of all understood languages, excluding any blocked languages
+/atom/movable/proc/get_understood_languages() as /list
+ return get_language_holder().get_understood_languages() || list()
+
/// Copies all languages into the supplied atom/language holder. Source should be overridden when you
/// do not want the language overwritten by later atom updates or want to avoid blocked languages.
/atom/movable/proc/copy_languages(datum/language_holder/from_holder, source_override)
diff --git a/code/game/say.dm b/code/game/say.dm
index c707f90f9c9..51f48cc8bca 100644
--- a/code/game/say.dm
+++ b/code/game/say.dm
@@ -155,6 +155,7 @@ GLOBAL_LIST_INIT(freqtospan, list(
listened += hearing_movable
do_tts_message(tts_message_to_use, message_language, message_mods, tts_filter, listened)
+
/atom/movable/proc/compose_message(atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, list/spans, list/message_mods = list(), visible_name = FALSE)
//This proc uses [] because it is faster than continually appending strings. Thanks BYOND.
//Basic span
@@ -174,8 +175,10 @@ GLOBAL_LIST_INIT(freqtospan, list(
var/languageicon = ""
if(!message_mods[MODE_CUSTOM_SAY_ERASE_INPUT])
var/datum/language/dialect = GLOB.language_datum_instances[message_language]
- if(istype(dialect) && dialect.display_icon(src))
- languageicon = "[dialect.get_icon()] "
+ var/dialect_icon_type = dialect?.display_icon_type(src, message_mods) || DISPLAY_LANGUAGE_ICON_NONE
+ if(dialect_icon_type != DISPLAY_LANGUAGE_ICON_NONE)
+ var/datum/asset/spritesheet_batched/sheet = get_asset_datum(/datum/asset/spritesheet_batched/chat)
+ languageicon = sheet.icon_tag("language-[dialect.icon_state][dialect_icon_type == DISPLAY_LANGUAGE_ICON_PARTIAL ? "-partial" : ""]") + " "
// The actual message part.
var/messagepart = speaker.generate_messagepart(raw_message, spans, message_mods)
diff --git a/code/modules/asset_cache/assets/chat.dm b/code/modules/asset_cache/assets/chat.dm
index 890f05afe39..b5b0e596bca 100644
--- a/code/modules/asset_cache/assets/chat.dm
+++ b/code/modules/asset_cache/assets/chat.dm
@@ -1,17 +1,23 @@
/datum/asset/spritesheet_batched/chat
name = "chat"
+ /// Default file for language icons
+ var/default_language_file = 'icons/ui/chat/language.dmi'
/datum/asset/spritesheet_batched/chat/create_spritesheets()
insert_all_icons("emoji", EMOJI_SET)
// pre-loading all lanugage icons also helps to avoid meta
- insert_all_icons("language", 'icons/ui/chat/language.dmi')
+ insert_all_icons("language", default_language_file)
// catch languages which are pulling icons from another file
- for(var/datum/language/L as anything in subtypesof(/datum/language))
- var/icon = initial(L.icon)
- if (icon != 'icons/ui/chat/language.dmi')
- var/icon_state = initial(L.icon_state)
+ for(var/datum/language/lang_type as anything in subtypesof(/datum/language))
+ var/icon = initial(lang_type.icon)
+ var/icon_state = initial(lang_type.icon_state)
+ if (icon != default_language_file)
insert_icon("language-[icon_state]", uni_icon(icon, icon_state))
+ var/datum/universal_icon/partial_icon = uni_icon(icon, icon_state)
+ partial_icon.blend_icon(uni_icon(default_language_file, "unknown"), ICON_OVERLAY)
+ insert_icon("language-[icon_state]-partial", partial_icon)
+
var/datum/universal_icon/byond_member = uni_icon('icons/ui/chat/member_content.dmi', "blag")
byond_member.scale(16, 16)
insert_icon("byond_member", byond_member)
diff --git a/code/modules/client/preferences/language.dm b/code/modules/client/preferences/language.dm
index c9f69c6e205..f822bdf323d 100644
--- a/code/modules/client/preferences/language.dm
+++ b/code/modules/client/preferences/language.dm
@@ -94,13 +94,13 @@
should_update_preview = FALSE
/datum/preference/choiced/csl_strength/create_default_value()
- return "90%"
+ return init_possible_values()[1]
/datum/preference/choiced/csl_strength/is_accessible(datum/preferences/preferences)
return ..() && (/datum/quirk/csl::name in preferences.all_quirks)
/datum/preference/choiced/csl_strength/init_possible_values()
- return list("90%", "75%", "50%", "33%", "25%", "10%")
+ return list("75%", "50%", "25%")
/datum/preference/choiced/csl_strength/apply_to_human(mob/living/carbon/human/target, value)
return
diff --git a/code/modules/language/_language.dm b/code/modules/language/_language.dm
index 65b107ee598..2c558e33f33 100644
--- a/code/modules/language/_language.dm
+++ b/code/modules/language/_language.dm
@@ -110,19 +110,19 @@
most_common_cache.Cut()
last_sentence_cache.Cut()
-/// Checks whether we should display the language icon to the passed hearer.
-/datum/language/proc/display_icon(atom/movable/hearer)
- var/understands = hearer.has_language(src.type)
- if((flags & LANGUAGE_HIDE_ICON_IF_UNDERSTOOD) && understands)
- return FALSE
- if((flags & LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD) && !understands)
- return FALSE
- return TRUE
+/// Returns what icon to display when speaking this language
+/datum/language/proc/display_icon_type(atom/movable/hearer, list/message_mods)
+ if(hearer.has_language(type))
+ if(flags & LANGUAGE_HIDE_ICON_IF_UNDERSTOOD)
+ return DISPLAY_LANGUAGE_ICON_NONE
-/// Returns the icon to display in the chat window when speaking this language.
-/datum/language/proc/get_icon()
- var/datum/asset/spritesheet_batched/sheet = get_asset_datum(/datum/asset/spritesheet_batched/chat)
- return sheet.icon_tag("language-[icon_state]")
+ else
+ if(hearer.has_partial_language(type) || LAZYACCESSASSOC(message_mods, LANGUAGE_MUTUAL_BONUS, type))
+ return DISPLAY_LANGUAGE_ICON_PARTIAL
+ if(flags & LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD)
+ return DISPLAY_LANGUAGE_ICON_NONE
+
+ return DISPLAY_LANGUAGE_ICON_FULL
/// Simple helper for getting a default firstname lastname
/datum/language/proc/default_name(gender = NEUTER)
@@ -259,12 +259,12 @@
var/list/translated_index = list()
for(var/word in splittext(input, " "))
var/translate_prob = mutual_languages?[type] || 0
- var/base_word = strip_outer_punctuation(word)
+ var/base_word = strip_outer_punctuation(html_decode(word))
if(translate_prob > 0)
- // the probability of managing to understand a word is based on how common it is (+10%, -15%)
+ // the probability of managing to understand a word is based on how common it is (up to +20%, though -5% for very uncommon words)
// 1000 words in the list, so words outside the list are just treated as "the 1250th most common word"
var/commonness = GLOB.most_common_words_frequency[LOWER_TEXT(base_word)] || 1250
- translate_prob += (10 * (1 - (min(commonness, 1250) / 500)))
+ translate_prob += max(20 - (commonness / 1000) * 20, -5)
if(prob(translate_prob))
scrambled_words += word
translated_index += FALSE
diff --git a/code/modules/language/_language_holder.dm b/code/modules/language/_language_holder.dm
index c4207f40f07..c72e830d135 100644
--- a/code/modules/language/_language_holder.dm
+++ b/code/modules/language/_language_holder.dm
@@ -232,6 +232,10 @@ Key procs
return language in langs_to_check
+/// Checks if you have partial understanding of the language passed.
+/datum/language_holder/proc/has_partial_language(language)
+ return LAZYACCESS(best_mutual_languages, language)
+
/// Checks if you can speak the language. Tongue limitations should be supplied as an argument.
/datum/language_holder/proc/can_speak_language(language)
var/can_speak_language_path = omnitongue || owner.could_speak_language(language)
@@ -272,6 +276,10 @@ Key procs
else
return /datum/language/common
+/// Returns a list of languages we understand
+/datum/language_holder/proc/get_understood_languages()
+ return assoc_to_keys(understood_languages) - assoc_to_keys(blocked_understanding)
+
/// Opens a language menu reading from the language holder.
/datum/language_holder/proc/open_language_menu(mob/user)
if(!language_menu)
diff --git a/code/modules/language/carptongue.dm b/code/modules/language/carptongue.dm
index f3a376287ec..6215c618863 100644
--- a/code/modules/language/carptongue.dm
+++ b/code/modules/language/carptongue.dm
@@ -3,7 +3,7 @@
desc = "Various fishy sounds that might make sense to space carps."
key = "c"
icon_state = "fish"
- flags = NO_STUTTER|TONGUELESS_SPEECH
+ flags = LANGUAGE_NO_STUTTER|LANGUAGE_TONGUELESS_SPEECH
sentence_chance = 0
space_chance = 0
between_word_sentence_chance = 0
diff --git a/code/modules/language/codespeak.dm b/code/modules/language/codespeak.dm
index 8cc8c1c460a..55af1b060a7 100644
--- a/code/modules/language/codespeak.dm
+++ b/code/modules/language/codespeak.dm
@@ -3,7 +3,7 @@
desc = "Syndicate operatives can use a series of codewords to convey complex information, while sounding like random concepts and drinks to anyone listening in."
key = "t"
default_priority = 0
- flags = TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD
+ flags = LANGUAGE_TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD
icon_state = "codespeak"
always_use_default_namelist = TRUE // No syllables anyways
diff --git a/code/modules/language/common.dm b/code/modules/language/common.dm
index bf015a04e2f..7cd79359c44 100644
--- a/code/modules/language/common.dm
+++ b/code/modules/language/common.dm
@@ -3,7 +3,7 @@
name = "Galactic Common"
desc = "The common galactic tongue."
key = "0"
- flags = TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_UNDERSTOOD
+ flags = LANGUAGE_TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_UNDERSTOOD
default_priority = 100
space_chance = 20
sentence_chance = 0
diff --git a/code/modules/language/draconic.dm b/code/modules/language/draconic.dm
index 76cc7350f60..628587b7db9 100644
--- a/code/modules/language/draconic.dm
+++ b/code/modules/language/draconic.dm
@@ -2,7 +2,7 @@
name = "Draconic"
desc = "The common language of lizard-people, composed of sibilant hisses and rattles."
key = "o"
- flags = TONGUELESS_SPEECH
+ flags = LANGUAGE_TONGUELESS_SPEECH
space_chance = 12
sentence_chance = 0
between_word_sentence_chance = 10
diff --git a/code/modules/language/drone.dm b/code/modules/language/drone.dm
index 373f504a135..3b220119e76 100644
--- a/code/modules/language/drone.dm
+++ b/code/modules/language/drone.dm
@@ -3,7 +3,7 @@
desc = "A heavily encoded damage control coordination stream, with special flags for hats."
spans = list(SPAN_ROBOT)
key = "d"
- flags = NO_STUTTER
+ flags = LANGUAGE_NO_STUTTER
syllables = list(".", "|")
// ...|..||.||||.|.||.|.|.|||.|||
space_chance = 0
diff --git a/code/modules/language/machine.dm b/code/modules/language/machine.dm
index 63bec30c6f8..ab08d141237 100644
--- a/code/modules/language/machine.dm
+++ b/code/modules/language/machine.dm
@@ -3,7 +3,7 @@
desc = "An efficient language of encoded tones developed by synthetics and cyborgs."
spans = list(SPAN_ROBOT)
key = "6"
- flags = NO_STUTTER
+ flags = LANGUAGE_NO_STUTTER
syllables = list(
"beep", "beep", "beep", "beep", "beep", "boop", "boop", "boop",
"bop", "bop", "dee", "dee", "doo", "doo", "hiss", "hss", "buzz",
diff --git a/code/modules/language/spinwarder.dm b/code/modules/language/spinwarder.dm
index 8c3877f38c0..7e4f12f738c 100644
--- a/code/modules/language/spinwarder.dm
+++ b/code/modules/language/spinwarder.dm
@@ -10,7 +10,7 @@
between_word_space_chance = 75
additional_syllable_low = 0
additional_syllable_high = 0
- flags = TONGUELESS_SPEECH
+ flags = LANGUAGE_TONGUELESS_SPEECH
syllables = list(
"v", "od", "noy", "ned", "ele", "dn", "ey", "da", "ny", "et", "mes", "yat",
"che", "bu", "pol", "ye", "eli", "nyy", "se", "god", "ya", "dom", "ash", "zav",
diff --git a/code/modules/language/uncommon.dm b/code/modules/language/uncommon.dm
index 43b878e8341..060f6c02cb1 100644
--- a/code/modules/language/uncommon.dm
+++ b/code/modules/language/uncommon.dm
@@ -2,7 +2,7 @@
name = "Galactic Uncommon"
desc = "The second-most spoken Human language."
key = "!"
- flags = TONGUELESS_SPEECH
+ flags = LANGUAGE_TONGUELESS_SPEECH
space_chance = 20
sentence_chance = 0
between_word_sentence_chance = 10
diff --git a/code/modules/mob/living/carbon/carbon_say.dm b/code/modules/mob/living/carbon/carbon_say.dm
index e1dc3d27992..35ed43d8fde 100644
--- a/code/modules/mob/living/carbon/carbon_say.dm
+++ b/code/modules/mob/living/carbon/carbon_say.dm
@@ -12,4 +12,4 @@
// the tower of babel needs to bypass the tongue language restrictions without giving omnitongue
return HAS_MIND_TRAIT(src, TRAIT_TOWER_OF_BABEL) || spoken_with.could_speak_language(language_path)
- return initial(language_path.flags) & TONGUELESS_SPEECH
+ return initial(language_path.flags) & LANGUAGE_TONGUELESS_SPEECH
diff --git a/icons/ui/chat/language.dmi b/icons/ui/chat/language.dmi
index 7cb84ee3af9464502ef542b3b83986129dc35985..3a8a6148f44aefbe249735e89a72aff1bab6ae63 100644
GIT binary patch
literal 5763
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
BNMHZ}
literal 5437
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