diff --git a/code/__HELPERS/text.dm b/code/__HELPERS/text.dm
index 69e187a9d67..4afc24c3965 100644
--- a/code/__HELPERS/text.dm
+++ b/code/__HELPERS/text.dm
@@ -1028,7 +1028,11 @@ GLOBAL_LIST_INIT(binary, list("0","1"))
return corrupted_text
-#define is_alpha(X) ((text2ascii(X) <= 122) && (text2ascii(X) >= 97))
+/// Checks if the char is lowercase
+#define is_lowercase_character(X) ((text2ascii(X) <= 122) && (text2ascii(X) >= 97))
+/// Checks if the char is uppercase
+#define is_uppercase_character(X) ((text2ascii(X) <= 90) && (text2ascii(X) >= 65))
+/// Checks if the char is a digit
#define is_digit(X) ((length(X) == 1) && (length(text2num(X)) == 1))
//json decode that will return null on parse error instead of runtiming.
@@ -1233,3 +1237,38 @@ GLOBAL_LIST_INIT(binary, list("0","1"))
for(var/iteration in 1 to length_char(text))
grawlix += pick("@", "$", "?", "!", "#", "§", "*", "£", "%", "☠", "★", "☆", "¿", "⚡")
return grawlix
+
+/// All punctuation that can be stripped in strip_outer_punctuation()
+#define STRIPPED_PUNCTUATION (REGEX_QUOTE("!?.~;:,|+_`-"))
+
+/// Removes all punctuation sequences from the beginning and the end of the input string.
+/// Includes emphasis (|, +, _) and whitespace.
+/// Anything punctuation in the middle of the input will be maintained.
+/proc/strip_outer_punctuation(input)
+ var/static/regex/pre_word_regex = new("^(?:\[[STRIPPED_PUNCTUATION]\]{0,3})(.*)", "m")
+ if(pre_word_regex.Find(input))
+ input = pre_word_regex.group[1]
+
+ var/static/regex/post_word_regex = new("^(.*?)(?:\[[STRIPPED_PUNCTUATION]\]{0,3})$", "m")
+ if(post_word_regex.Find(input))
+ return trim(post_word_regex.group[1])
+
+ return trim(input)
+
+#undef STRIPPED_PUNCTUATION
+
+/// Find what punctuation is at the end of the input, returns it.
+/// Ignores emphasis (|, +, _)
+/proc/find_last_punctuation(input)
+ var/static/regex/punctuation_regex = new(@"([!?.~;:,-]{1,3})[|+_\s]*$", "m")
+ if(punctuation_regex.Find(input))
+ return punctuation_regex.group[1]
+
+ return ""
+
+/// Checks if the passed string is all uppercase, ignoring punctuation and numbers and symbols
+/proc/is_uppercase(input)
+ var/static/regex/lowercase_regex = new(@"[a-z]", "g")
+ if(lowercase_regex.Find(input))
+ return FALSE
+ return TRUE
diff --git a/code/_globalvars/lists/flavor_misc.dm b/code/_globalvars/lists/flavor_misc.dm
index eca55a94b41..13fbd4a80a0 100644
--- a/code/_globalvars/lists/flavor_misc.dm
+++ b/code/_globalvars/lists/flavor_misc.dm
@@ -271,3 +271,14 @@ GLOBAL_LIST_INIT(status_display_state_pictures, list(
))
GLOBAL_LIST_INIT(fishing_tips, world.file2list("strings/fishing_tips.txt"))
+
+/// 1000 element long list containing the 1000 most common words in the English language.
+/// Indexed by word, value is the rank of the word in the list. So accessing it is fasta.
+GLOBAL_LIST_INIT(most_common_words, init_common_words())
+
+/proc/init_common_words()
+ . = list()
+ var/i = 1
+ for(var/word in world.file2list("strings/1000_most_common.txt"))
+ .[word] = i
+ i += 1
diff --git a/code/controllers/subsystem/discord.dm b/code/controllers/subsystem/discord.dm
index 802b95b2341..2e7936f17e3 100644
--- a/code/controllers/subsystem/discord.dm
+++ b/code/controllers/subsystem/discord.dm
@@ -43,9 +43,6 @@ SUBSYSTEM_DEF(discord)
/// People who have tried to verify this round already
var/list/reverify_cache
- /// Common words list, used to generate one time tokens
- var/list/common_words
-
/// The file where notification status is saved
var/notify_file = file("data/notify.json")
@@ -53,7 +50,6 @@ SUBSYSTEM_DEF(discord)
var/enabled = FALSE
/datum/controller/subsystem/discord/Initialize()
- common_words = world.file2list("strings/1000_most_common.txt")
reverify_cache = list()
// Check for if we are using TGS, otherwise return and disables firing
if(world.TgsAvailable())
@@ -158,7 +154,7 @@ SUBSYSTEM_DEF(discord)
// While there's a collision in the token, generate a new one (should rarely happen)
while(not_unique)
//Column is varchar 100, so we trim just in case someone does us the dirty later
- one_time_token = trim("[pick(common_words)]-[pick(common_words)]-[pick(common_words)]-[pick(common_words)]-[pick(common_words)]-[pick(common_words)]", 100)
+ one_time_token = trim("[pick(GLOB.most_common_words)]-[pick(GLOB.most_common_words)]-[pick(GLOB.most_common_words)]-[pick(GLOB.most_common_words)]-[pick(GLOB.most_common_words)]-[pick(GLOB.most_common_words)]", 100)
not_unique = find_discord_link_by_token(one_time_token, timebound = TRUE)
@@ -300,4 +296,3 @@ SUBSYSTEM_DEF(discord)
if (length(discord_mention_extraction_regex.group) == 1)
return discord_mention_extraction_regex.group[1]
return null
-
diff --git a/code/datums/brain_damage/mild.dm b/code/datums/brain_damage/mild.dm
index f4dc1f60cc4..ffdabc0adf7 100644
--- a/code/datums/brain_damage/mild.dm
+++ b/code/datums/brain_damage/mild.dm
@@ -198,8 +198,6 @@
gain_text = span_warning("You lose your grasp on complex words.")
lose_text = span_notice("You feel your vocabulary returning to normal again.")
- var/static/list/common_words = world.file2list("strings/1000_most_common.txt")
-
/datum/brain_trauma/mild/expressive_aphasia/handle_speech(datum/source, list/speech_args)
var/message = speech_args[SPEECH_MESSAGE]
if(message)
@@ -219,7 +217,7 @@
word = copytext(word, 1, suffix_foundon)
word = html_decode(word)
- if(LOWER_TEXT(word) in common_words)
+ if(GLOB.most_common_words[LOWER_TEXT(word)])
new_message += word + suffix
else
if(prob(30) && message_split.len > 2)
diff --git a/code/datums/quirks/positive_quirks/bilingual.dm b/code/datums/quirks/positive_quirks/bilingual.dm
index 20123dbe87a..b5fc8a75f37 100644
--- a/code/datums/quirks/positive_quirks/bilingual.dm
+++ b/code/datums/quirks/positive_quirks/bilingual.dm
@@ -10,19 +10,37 @@
/datum/quirk_constant_data/bilingual
associated_typepath = /datum/quirk/bilingual
- customization_options = list(/datum/preference/choiced/language)
+ customization_options = list(
+ /datum/preference/choiced/language,
+ /datum/preference/toggle/language_speakable,
+ /datum/preference/choiced/language_skill,
+ )
/datum/quirk/bilingual/add(client/client_source)
var/wanted_language = client_source?.prefs.read_preference(/datum/preference/choiced/language)
var/datum/language/language_type
if(wanted_language == "Random")
language_type = pick(GLOB.uncommon_roundstart_languages)
- else
+ else if(wanted_language)
language_type = GLOB.language_types_by_name[wanted_language]
- if(quirk_holder.has_language(language_type))
+ if(!language_type || quirk_holder.has_language(language_type))
language_type = /datum/language/uncommon
if(quirk_holder.has_language(language_type))
to_chat(quirk_holder, span_boldnotice("You are already familiar with the quirk in your preferences, so you did not learn one."))
return
to_chat(quirk_holder, span_boldnotice("You are already familiar with the quirk in your preferences, so you learned Galactic Uncommon instead."))
- quirk_holder.grant_language(language_type, source = LANGUAGE_QUIRK)
+
+ var/speakable = client_source?.prefs.read_preference(/datum/preference/toggle/language_speakable)
+ var/language_skill = client_source?.prefs.read_preference(/datum/preference/choiced/language_skill) || "100%"
+ if(isnull(speakable) || speakable)
+ quirk_holder.grant_language(language_type, SPOKEN_LANGUAGE|UNDERSTOOD_LANGUAGE, source = LANGUAGE_QUIRK)
+ else if(language_skill == "100%")
+ quirk_holder.grant_language(language_type, UNDERSTOOD_LANGUAGE, source = LANGUAGE_QUIRK)
+ else
+ quirk_holder.grant_partial_language(language_type, text2num(language_skill), source = LANGUAGE_QUIRK)
+
+/datum/quirk/bilingual/remove()
+ if(QDELING(quirk_holder))
+ return
+ quirk_holder.remove_all_languages(source = LANGUAGE_QUIRK)
+ quirk_holder.remove_all_partial_languages(source = LANGUAGE_QUIRK)
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index 6c490e422c6..18d00e9966d 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -1570,6 +1570,10 @@
/atom/movable/proc/grant_all_languages(language_flags = ALL, grant_omnitongue = TRUE, source = LANGUAGE_MIND)
return get_language_holder().grant_all_languages(language_flags, grant_omnitongue, source)
+/// Grants partial understanding of a language.
+/atom/movable/proc/grant_partial_language(language, amount = 50, source = LANGUAGE_ATOM)
+ return get_language_holder().grant_partial_language(language, amount, source)
+
/// Removes a single language.
/atom/movable/proc/remove_language(language, language_flags = ALL, source = LANGUAGE_ALL)
return get_language_holder().remove_language(language, language_flags, source)
@@ -1578,6 +1582,14 @@
/atom/movable/proc/remove_all_languages(source = LANGUAGE_ALL, remove_omnitongue = FALSE)
return get_language_holder().remove_all_languages(source, remove_omnitongue)
+/// Removes partial understanding of a language.
+/atom/movable/proc/remove_partial_language(language, source = LANGUAGE_ALL)
+ return get_language_holder().remove_partial_language(language, source)
+
+/// Removes all partial languages.
+/atom/movable/proc/remove_all_partial_languages(source = LANGUAGE_ALL)
+ return get_language_holder().remove_all_partial_languages(source)
+
/// Adds a language to the blocked language list. Use this over remove_language in cases where you will give languages back later.
/atom/movable/proc/add_blocked_language(language, source = LANGUAGE_ATOM)
return get_language_holder().add_blocked_language(language, source)
@@ -1606,6 +1618,10 @@
/atom/movable/proc/get_random_understood_language()
return get_language_holder().get_random_understood_language()
+/// Gets a lazylist of all mutually understood languages.
+/atom/movable/proc/get_partially_understood_languages()
+ return get_language_holder().best_mutual_languages
+
/// Gets a random spoken language, useful for forced speech and such.
/atom/movable/proc/get_random_spoken_language()
return get_language_holder().get_random_spoken_language()
diff --git a/code/game/machinery/telecomms/computers/logbrowser.dm b/code/game/machinery/telecomms/computers/logbrowser.dm
index 22a41a6ada6..5a9c3d6daac 100644
--- a/code/game/machinery/telecomms/computers/logbrowser.dm
+++ b/code/game/machinery/telecomms/computers/logbrowser.dm
@@ -59,7 +59,7 @@
message_out = "\"[message_in]\""
else if(!user.has_language(language))
// Language unknown: scramble
- message_out = "\"[language_instance.scramble(message_in)]\""
+ message_out = "\"[language_instance.scramble_sentence(message_in, user.get_partially_understood_languages())]\""
else
message_out = "(Unintelligible)"
packet_out["message"] = message_out
diff --git a/code/game/objects/items/crayons.dm b/code/game/objects/items/crayons.dm
index 7396fbb59ca..ecc06ee8e25 100644
--- a/code/game/objects/items/crayons.dm
+++ b/code/game/objects/items/crayons.dm
@@ -483,7 +483,7 @@
var/temp = "rune"
var/ascii = (length(drawing) == 1)
- if(ascii && is_alpha(drawing))
+ if(ascii && is_lowercase_character(drawing))
temp = "letter"
else if(ascii && is_digit(drawing))
temp = "number"
diff --git a/code/game/objects/items/devices/radio/encryptionkey.dm b/code/game/objects/items/devices/radio/encryptionkey.dm
index 41ff2d379ff..d8d51697954 100644
--- a/code/game/objects/items/devices/radio/encryptionkey.dm
+++ b/code/game/objects/items/devices/radio/encryptionkey.dm
@@ -10,23 +10,42 @@
var/list/channels = list()
/// Flags for which "special" radio networks should be accessible
var/special_channels = NONE
- var/datum/language/translated_language
+ /// Assoc list of language to how well understood it is. 0 is invalid, 100 is perfect.
+ var/list/language_data
+
greyscale_config = /datum/greyscale_config/encryptionkey_basic
greyscale_colors = "#820a16#3758c4"
/obj/item/encryptionkey/examine(mob/user)
. = ..()
- if(LAZYLEN(channels) || special_channels & RADIO_SPECIAL_BINARY)
- var/list/examine_text_list = list()
- for(var/i in channels)
- examine_text_list += "[GLOB.channel_tokens[i]] - [LOWER_TEXT(i)]"
-
- if(special_channels & RADIO_SPECIAL_BINARY)
- examine_text_list += "[GLOB.channel_tokens[MODE_BINARY]] - [MODE_BINARY]"
-
- . += span_notice("It can access the following channels; [jointext(examine_text_list, ", ")].")
- else
+ if(!LAZYLEN(channels) && !(special_channels & RADIO_SPECIAL_BINARY) && !LAZYLEN(language_data))
. += span_warning("Has no special codes in it. You should probably tell a coder!")
+ return
+
+ var/list/examine_text_list = list()
+ for(var/i in channels)
+ examine_text_list += "[GLOB.channel_tokens[i]] - [LOWER_TEXT(i)]"
+
+ if(special_channels & RADIO_SPECIAL_BINARY)
+ examine_text_list += "[GLOB.channel_tokens[MODE_BINARY]] - [MODE_BINARY]"
+
+ if(length(examine_text_list))
+ . += span_notice("It can access the following channels; [jointext(examine_text_list, ", ")].")
+
+ var/list/language_text_list = list()
+ for(var/lang in language_data)
+ var/langstring = "[GLOB.language_datum_instances[lang].name]"
+ switch(language_data[lang])
+ if(25 to 50)
+ langstring += " (poor)"
+ if(50 to 75)
+ langstring += " (average)"
+ if(75 to 100)
+ langstring += " (good)"
+ language_text_list += langstring
+
+ if(length(language_text_list))
+ . += span_notice("It can translate the following languages; [jointext(language_text_list, ", ")].")
/obj/item/encryptionkey/syndicate
name = "syndicate encryption key"
@@ -40,7 +59,9 @@
name = "binary translator key"
icon_state = "cypherkey_basic"
special_channels = RADIO_SPECIAL_BINARY
- translated_language = /datum/language/machine
+ language_data = list(
+ /datum/language/machine = 100,
+ )
greyscale_config = /datum/greyscale_config/encryptionkey_basic
greyscale_colors = "#24a157#3758c4"
@@ -219,7 +240,9 @@
RADIO_CHANNEL_ENTERTAINMENT = 1,
)
special_channels = RADIO_SPECIAL_BINARY
- translated_language = /datum/language/machine
+ language_data = list(
+ /datum/language/machine = 100,
+ )
/obj/item/encryptionkey/ai/evil //ported from NT, this goes 'inside' the AI.
name = "syndicate binary encryption key"
diff --git a/code/game/objects/items/devices/radio/headset.dm b/code/game/objects/items/devices/radio/headset.dm
index d8ee05d26b5..65f58d603a0 100644
--- a/code/game/objects/items/devices/radio/headset.dm
+++ b/code/game/objects/items/devices/radio/headset.dm
@@ -41,8 +41,6 @@ GLOBAL_LIST_INIT(channel_tokens, list(
drop_sound = 'sound/items/handling/headset/headset_drop1.ogg'
sound_vary = TRUE
var/obj/item/encryptionkey/keyslot2 = null
- /// A list of all languages that this headset allows the user to understand. Populated by language encryption keys.
- var/list/language_list
// headset is too small to display overlays
overlay_speaker_idle = null
@@ -117,8 +115,32 @@ GLOBAL_LIST_INIT(channel_tokens, list(
/// Grants all the languages this headset allows the mob to understand via installed chips.
/obj/item/radio/headset/proc/grant_headset_languages(mob/grant_to)
+ var/list/language_list = keyslot?.language_data?.Copy()
+
+ if(keyslot2)
+ if(length(language_list))
+ for(var/language in keyslot2.language_data)
+ if(language_list[language] < keyslot2.language_data[language])
+ language_list[language] = keyslot2.language_data[language]
+ continue
+ language_list[language] = keyslot2.language_data[language]
+
+ else
+ language_list = keyslot2.language_data?.Copy()
+
for(var/language in language_list)
- grant_to.grant_language(language, language_flags = UNDERSTOOD_LANGUAGE, source = LANGUAGE_RADIOKEY)
+ var/amount_understood = language_list[language]
+ if(amount_understood >= 100)
+ grant_to.grant_language(language, language_flags = UNDERSTOOD_LANGUAGE, source = LANGUAGE_RADIOKEY)
+ else
+ grant_to.grant_partial_language(language, amount = amount_understood, source = LANGUAGE_RADIOKEY)
+
+/// Clears all radio related languages from the mob.
+/obj/item/radio/headset/proc/remove_headset_languages(mob/remove_from)
+ if(QDELETED(remove_from)) //This can be called as a part of destroy
+ return
+ remove_from.remove_all_languages(source = LANGUAGE_RADIOKEY)
+ remove_from.remove_all_partial_languages(source = LANGUAGE_RADIOKEY)
/obj/item/radio/headset/equipped(mob/user, slot, initial)
. = ..()
@@ -129,10 +151,7 @@ GLOBAL_LIST_INIT(channel_tokens, list(
/obj/item/radio/headset/dropped(mob/user, silent)
. = ..()
- if(QDELETED(src)) //This can be called as a part of destroy
- return
- for(var/language in language_list)
- user.remove_language(language, language_flags = UNDERSTOOD_LANGUAGE, source = LANGUAGE_RADIOKEY)
+ remove_headset_languages(user)
// Headsets do not become hearing sensitive as broadcasting instead controls their talk_into capabilities
/obj/item/radio/headset/set_broadcasting(new_broadcasting, actual_setting = TRUE)
@@ -488,22 +507,10 @@ GLOBAL_LIST_INIT(channel_tokens, list(
for(var/ch_name in channels)
secure_radio_connections[ch_name] = add_radio(src, GLOB.radiochannels[ch_name])
- var/list/old_language_list = language_list?.Copy()
- language_list = list()
- if(keyslot?.translated_language)
- language_list += keyslot.translated_language
- if(keyslot2?.translated_language)
- language_list += keyslot2.translated_language
-
- // If we're equipped on a mob, we should make sure all the languages
- // learned from our installed key chips are all still accurate
+ // Updates radio languages entirely for the mob wearing the headset
var/mob/mob_loc = loc
if(istype(mob_loc) && mob_loc.get_item_by_slot(slot_flags) == src)
- // Remove all the languages we may not be able to know anymore
- for(var/language in old_language_list)
- mob_loc.remove_language(language, language_flags = UNDERSTOOD_LANGUAGE, source = LANGUAGE_RADIOKEY)
-
- // And grant all the languages we definitely should know now
+ remove_headset_languages(mob_loc)
grant_headset_languages(mob_loc)
/obj/item/radio/headset/click_alt(mob/living/user)
diff --git a/code/game/objects/structures/lavaland/ore_vent.dm b/code/game/objects/structures/lavaland/ore_vent.dm
index dc9318ca740..8a2e30d95a7 100644
--- a/code/game/objects/structures/lavaland/ore_vent.dm
+++ b/code/game/objects/structures/lavaland/ore_vent.dm
@@ -587,7 +587,7 @@
RegisterSignal(boss, COMSIG_LIVING_DEATH, PROC_REF(handle_wave_conclusion))
SSblackbox.record_feedback("tally", "ore_vent_mobs_spawned", 1, summoned_boss)
COOLDOWN_START(src, wave_cooldown, INFINITY) //Basically forever
- boss.say(boss.summon_line) //Pull their specific summon line to say. Default is meme text so make sure that they have theirs set already.
+ boss.say(boss.summon_line, language = /datum/language/common, forced = "summon line") //Pull their specific summon line to say. Default is meme text so make sure that they have theirs set already.
/obj/structure/ore_vent/boss/handle_wave_conclusion()
node = new /mob/living/basic/node_drone(loc) //We're spawning the vent after the boss dies, so the player can just focus on the boss.
diff --git a/code/game/say.dm b/code/game/say.dm
index 380ed4accca..cae3fa31e4c 100644
--- a/code/game/say.dm
+++ b/code/game/say.dm
@@ -66,8 +66,7 @@ GLOBAL_LIST_INIT(freqtospan, list(
if(!message || message == "")
return
spans |= speech_span
- if(!language)
- language = get_selected_language()
+ language ||= get_selected_language()
message_mods[SAY_MOD_VERB] = say_mod(message, message_mods)
send_speech(message, message_range, src, bubble_type, spans, language, message_mods, forced = forced)
@@ -274,7 +273,7 @@ GLOBAL_LIST_INIT(freqtospan, list(
if(!has_language(language))
var/datum/language/dialect = GLOB.language_datum_instances[language]
- raw_message = dialect.scramble(raw_message)
+ raw_message = dialect.scramble_sentence(raw_message, get_partially_understood_languages())
return raw_message
diff --git a/code/modules/atmospherics/machinery/components/fusion/hfr_procs.dm b/code/modules/atmospherics/machinery/components/fusion/hfr_procs.dm
index 2c77ac829b7..16d21bf3c43 100644
--- a/code/modules/atmospherics/machinery/components/fusion/hfr_procs.dm
+++ b/code/modules/atmospherics/machinery/components/fusion/hfr_procs.dm
@@ -415,7 +415,7 @@
priority_announce("WARNING - The explosion will likely cover a big part of the station and the coming EMP will wipe out most of the electronics. \
Get as far away as possible from the reactor or find a way to shut it down.", "Alert", 'sound/announcer/notice/notice3.ogg')
var/speaking = "[emergency_alert] The Hypertorus fusion reactor has reached critical integrity failure. Emergency magnetic dampeners online."
- radio.talk_into(src, speaking, common_channel, language = get_selected_language())
+ radio.talk_into(src, speaking, common_channel)
notify_ghosts(
"The [src] has begun melting down!",
diff --git a/code/modules/client/preferences/language.dm b/code/modules/client/preferences/language.dm
index dbe4c700a31..4e142f1be54 100644
--- a/code/modules/client/preferences/language.dm
+++ b/code/modules/client/preferences/language.dm
@@ -2,15 +2,27 @@
category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
savefile_key = "language"
savefile_identifier = PREFERENCE_CHARACTER
+ should_generate_icons = TRUE
/datum/preference/choiced/language/create_default_value()
return "Random"
/datum/preference/choiced/language/is_accessible(datum/preferences/preferences)
- if (!..(preferences))
+ if (!..())
return FALSE
- return "Bilingual" in preferences.all_quirks
+ return /datum/quirk/bilingual::name in preferences.all_quirks
+
+/datum/preference/choiced/language/icon_for(value)
+ var/datum/language/lang = GLOB.language_types_by_name[value]
+ if(lang)
+ var/datum/universal_icon/lang_icon = uni_icon(lang.icon, lang.icon_state)
+ lang_icon.scale(32, 32)
+ return lang_icon
+
+ var/datum/universal_icon/unknown = uni_icon('icons/ui/chat/language.dmi', "unknown")
+ unknown.scale(32, 32)
+ return unknown
/datum/preference/choiced/language/init_possible_values()
var/list/values = list()
@@ -19,11 +31,9 @@
generate_selectable_species_and_languages()
values += "Random"
-
//we add uncommon as it's foreigner-only.
- var/datum/language/uncommon/uncommon_language = /datum/language/uncommon
- values += initial(uncommon_language.name)
values += /datum/language/common::name // SKYRAT EDIT ADDITION START - Let's you select common
+ values += /datum/language/uncommon::name
for(var/datum/language/language_type as anything in GLOB.uncommon_roundstart_languages)
if(initial(language_type.name) in values)
@@ -34,3 +44,42 @@
/datum/preference/choiced/language/apply_to_human(mob/living/carbon/human/target, value)
return
+
+/datum/preference/toggle/language_speakable
+ category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
+ savefile_key = "language_speakable"
+ savefile_identifier = PREFERENCE_CHARACTER
+ default_value = TRUE
+ can_randomize = FALSE
+
+/datum/preference/toggle/language_speakable/is_accessible(datum/preferences/preferences)
+ if(!..())
+ return FALSE
+
+ return /datum/quirk/bilingual::name in preferences.all_quirks
+
+/datum/preference/toggle/language_speakable/apply_to_human(mob/living/carbon/human/target, value)
+ return
+
+/datum/preference/choiced/language_skill
+ category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
+ savefile_key = "language_skill"
+ savefile_identifier = PREFERENCE_CHARACTER
+ can_randomize = FALSE
+
+/datum/preference/choiced/language_skill/create_default_value()
+ return "100%"
+
+/datum/preference/choiced/language_skill/is_accessible(datum/preferences/preferences)
+ if(!..())
+ return FALSE
+ if(preferences.read_preference(/datum/preference/toggle/language_speakable))
+ return FALSE
+
+ return /datum/quirk/bilingual::name in preferences.all_quirks
+
+/datum/preference/choiced/language_skill/init_possible_values()
+ return list("100%", "75%", "50%", "33%", "25%", "10%")
+
+/datum/preference/choiced/language_skill/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 53b4cbdb44e..4dd2e58f98e 100644
--- a/code/modules/language/_language.dm
+++ b/code/modules/language/_language.dm
@@ -1,5 +1,7 @@
-/// maximum of 50 specific scrambled lines per language
+/// Last 50 spoken (uncommon) words will be cached before we start cycling them out (re-randomizing them)
#define SCRAMBLE_CACHE_LEN 50
+/// Last 20 spoken sentences will be cached before we start cycling them out (re-randomizing them)
+#define SENTENCE_CACHE_LEN 20
/// Datum based languages. Easily editable and modular.
/datum/language
@@ -16,15 +18,52 @@
var/list/syllables
/// List of characters that will randomly be inserted between syllables.
var/list/special_characters
+
+ // These modify how syllables are combined.
/// Likelihood of making a new sentence after each syllable.
- var/sentence_chance = 5
- /// Likelihood of getting a space in the random scramble string
- var/space_chance = 55
+ var/sentence_chance = 2
+ /// Likelihood of making a new sentence after each word.
+ var/between_word_sentence_chance = 0
+ /// Likelihood of adding a space between syllables.
+ var/space_chance = 20
+ /// Likelyhood of adding a space between words.
+ var/between_word_space_chance = 100
+ /// Scramble word interprets the word as this much longer than it really is (low end)
+ /// You can set this to an arbitarily large negative number to make all words only one syllable.
+ var/additional_syllable_low = -1
+ /// Scramble word interprets the word as this much longer than it really is (high end)
+ /// You can set this to an arbitarily large negative number to make all words only one syllable.
+ var/additional_syllable_high = 3
+
/// Spans to apply from this language
var/list/spans
- /// Cache of recently scrambled text
- /// This allows commonly reused words to not require a full re-scramble every time.
- var/list/scramble_cache = list()
+ /**
+ * Cache of recently scrambled text
+ * This allows commonly reused words to not require a full re-scramble every time.
+ * Is limited to the last SCRAMBLE_CACHE_LEN words spoken. After surpassing this limit,
+ * the oldest word will be removed from the cache and rescrambled if spoken again.
+ *
+ * Case insensitive, punctuation insensitive.
+ */
+ VAR_PRIVATE/list/scramble_cache = list()
+ /**
+ * Scramble cache, but for the 1000 most common words in the English language.
+ * These are never rescrambled, so they will consistently be the same thing.
+ *
+ * Case insensitive, punctuation insensitive.
+ */
+ VAR_PRIVATE/list/most_common_cache = list()
+ /**
+ * Cache of recently spoken sentences
+ * So if one person speaks over the radio, everyone hears the same thing.
+ *
+ * This is an assoc list [sentence] = [key, scrambled_text]
+ * Where key is a string that is used to determine context about the listener (like what languages they know)
+ *
+ * Case sensitive, punctuation sensitive.
+ */
+ VAR_PRIVATE/list/last_sentence_cache = list()
+
/// The language that an atom knows with the highest "default_priority" is selected by default.
var/default_priority = 0
/// If TRUE, when generating names, we will always use the default human namelist, even if we have syllables set.
@@ -34,7 +73,7 @@
/// if you are seeing someone speak popcorn language, then something is wrong.
var/icon = 'icons/ui/chat/language.dmi'
/// Icon state displayed in the chat window when speaking this language.
- var/icon_state = "popcorn"
+ var/icon_state = "unknown"
/// By default, random names picks this many names
var/default_name_count = 2
@@ -45,6 +84,32 @@
/// What char to place in between randomly generated names
var/random_name_spacer = " "
+ /**
+ * Assoc Lazylist of other language types that would have a degree of mutual understanding with this language.
+ * For example, you could do `list(/datum/language/common = 50)` to say that this language has a 50% chance to understand common words
+ * And yeah if you give a 100% chance, they can basically just understand the language.
+ * Not sure why you would do that though.
+ */
+ var/list/mutual_understanding
+
+// Primarily for debugging, allows for easy iteration and testing of languages.
+/datum/language/vv_edit_var(var_name, var_value)
+ . = ..()
+ var/list/delete_cache = list(
+ NAMEOF(src, additional_syllable_high),
+ NAMEOF(src, additional_syllable_low),
+ NAMEOF(src, between_word_sentence_chance),
+ NAMEOF(src, between_word_space_chance),
+ NAMEOF(src, sentence_chance),
+ NAMEOF(src, space_chance),
+ NAMEOF(src, special_characters),
+ NAMEOF(src, syllables),
+ )
+ if(var_name in delete_cache)
+ scramble_cache.Cut()
+ 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)
@@ -109,56 +174,138 @@
return result
-/datum/language/proc/check_cache(input)
- var/lookup = scramble_cache[input]
- if(lookup)
- scramble_cache -= input
- scramble_cache[input] = lookup
- . = lookup
+/// Checks the word cache for a word
+/datum/language/proc/read_word_cache(input)
+ SHOULD_NOT_OVERRIDE(TRUE)
+ // we generally want "The" and "the" to translate to the same thing.
+ // so we lowercase everything, making it case insensitive.
+ var/lowertext_input = LOWER_TEXT(input)
+ if(most_common_cache[lowertext_input])
+ return most_common_cache[lowertext_input]
-/datum/language/proc/add_to_cache(input, scrambled_text)
+ . = scramble_cache[lowertext_input]
+ if(. && scramble_cache[1] != lowertext_input)
+ // bumps it to the top of the cache
+ scramble_cache -= lowertext_input
+ scramble_cache[lowertext_input] = .
+ return .
+
+/// Adds a word to the cache
+/datum/language/proc/write_word_cache(input, scrambled_text)
+ SHOULD_NOT_OVERRIDE(TRUE)
+ var/lowertext_input = LOWER_TEXT(input)
+ // The most common words are always cached
+ if(GLOB.most_common_words[lowertext_input])
+ most_common_cache[lowertext_input] = scrambled_text
+ return
// Add it to cache, cutting old entries if the list is too long
- scramble_cache[input] = scrambled_text
- if(scramble_cache.len > SCRAMBLE_CACHE_LEN)
- scramble_cache.Cut(1, 2)
+ scramble_cache[lowertext_input] = scrambled_text
+ if(length(scramble_cache) > SCRAMBLE_CACHE_LEN)
+ scramble_cache.Cut(1, scramble_cache.len - SCRAMBLE_CACHE_LEN + 1)
-/datum/language/proc/scramble(input)
+/// Checks the sentence cache for a sentence
+/datum/language/proc/read_sentence_cache(input)
+ SHOULD_NOT_OVERRIDE(TRUE)
+ // the only handling we do is capitalizing the first word, as say auto-capitalizes the first word anyway
+ // the actual structure of the sentence is otherwise case sensitive so it's preserved
+ var/input_capitalized = capitalize(input)
+ . = last_sentence_cache[input_capitalized]
+ if(. && last_sentence_cache[1] != input_capitalized)
+ // bumps it to the top of the cache (don't anticipate this happening often)
+ last_sentence_cache -= input_capitalized
+ last_sentence_cache[input_capitalized] = .
+ return .
+
+/// Adds a sentence to the cache, though the sentence should be modified with a key
+/datum/language/proc/write_sentence_cache(input, key, result_scramble)
+ SHOULD_NOT_OVERRIDE(TRUE)
+ var/input_capitalized = capitalize(input)
+ // Add to the cache (the cache being an assoc list of assoc lists), cutting old entries if the list is too long
+ LAZYSET(last_sentence_cache[input_capitalized], key, result_scramble)
+ if(length(last_sentence_cache) > SENTENCE_CACHE_LEN)
+ last_sentence_cache.Cut(1, last_sentence_cache.len - SENTENCE_CACHE_LEN + 1)
+
+/**
+ * Scrambles a sentence in this language.
+ *
+ * Takes into account any languages the hearer knows that has mutual understanding with this language.
+ */
+/datum/language/proc/scramble_sentence(input, list/mutual_languages)
+ var/cache_key = "[mutual_languages?[type] || 0]-understanding"
+ var/list/cache = read_sentence_cache(cache_key)
+ if(cache?[cache_key])
+ return cache[cache_key]
+
+ var/list/real_words = splittext(input, " ")
+ var/list/scrambled_words = list()
+ for(var/word in real_words)
+ var/translate_prob = mutual_languages?[type] || 0
+ var/base_word = strip_outer_punctuation(word)
+ if(translate_prob > 0)
+ // the probability of managing to understand a word is based on how common it is
+ // 1000 words in the list, so words outside the list are just treated as "the 1500th most common word"
+ var/commonness = GLOB.most_common_words[LOWER_TEXT(base_word)] || 1500
+ translate_prob += (translate_prob * 0.2 * (1 - (min(commonness, 1500) / 500)))
+ if(prob(translate_prob))
+ scrambled_words += base_word
+ continue
+
+ scrambled_words += scramble_word(base_word)
+
+ // start building the new sentence. first word is capitalized and otherwise untouched
+ var/sentence = capitalize(popleft(scrambled_words))
+ for(var/word in scrambled_words)
+ if(prob(between_word_sentence_chance))
+ sentence += ". "
+ word = capitalize(word)
+ else if(prob(between_word_space_chance))
+ sentence += " "
+
+ sentence += word
+
+ // scrambling the words will drop punctuation, so re-add it at the end
+ sentence += find_last_punctuation(input)
+
+ write_sentence_cache(input, cache_key, sentence)
+
+ return sentence
+
+/**
+ * Scrambles a single word in this language.
+ */
+/datum/language/proc/scramble_word(input)
+ // If the input is cached already, move it to the end of the cache and return it
+ var/word = read_word_cache(input)
+ if(word)
+ return (is_uppercase(input) && length_char(input) >= 2) ? uppertext(word) : word
if(!length(syllables))
- return stars(input)
+ word = stars(input)
- // If the input is cached already, move it to the end of the cache and return it
- var/lookup = check_cache(input)
- if(lookup)
- return lookup
+ else
+ var/input_size = max(length_char(input) + rand(additional_syllable_low, additional_syllable_high), 1)
+ var/add_space = FALSE
+ var/add_period = FALSE
+ word = ""
+ while(length_char(word) < input_size)
+ // add in the last syllable's period or space first
+ if(add_period)
+ word += ". "
+ else if(add_space)
+ word += " "
+ // insert special chars if we're not at the start of the word
+ else if(word && prob(1) && length(special_characters))
+ word += pick(special_characters)
+ // generate the next syllable (capitalize if we just added a period)
+ var/next = pick_weight_recursive(syllables)
+ word += add_period ? capitalize(next) : next
+ // determine if the next syllable gets a period or space
+ add_period = prob(sentence_chance)
+ add_space = prob(space_chance)
- var/input_size = length_char(input)
- var/scrambled_text = ""
- var/capitalize = TRUE
+ write_word_cache(input, word)
- while(length_char(scrambled_text) < input_size)
- var/next = (length(scrambled_text) && length(special_characters) && prob(1)) ? pick(special_characters) : pick_weight_recursive(syllables)
- if(capitalize)
- next = capitalize(next)
- capitalize = FALSE
- scrambled_text += next
- var/chance = rand(100)
- if(chance <= sentence_chance)
- scrambled_text += ". "
- capitalize = TRUE
- else if(chance > sentence_chance && chance <= space_chance)
- scrambled_text += " "
-
- scrambled_text = trim(scrambled_text)
- var/ending = copytext_char(scrambled_text, -1)
- if(ending == ".")
- scrambled_text = copytext_char(scrambled_text, 1, -2)
- var/input_ending = copytext_char(input, -1)
- if(input_ending in list("!","?","."))
- scrambled_text += input_ending
-
- add_to_cache(input, scrambled_text)
-
- return scrambled_text
+ // If they're shouting, we're shouting
+ return (is_uppercase(input) && length_char(input) >= 2) ? uppertext(word) : word
#undef SCRAMBLE_CACHE_LEN
diff --git a/code/modules/language/_language_holder.dm b/code/modules/language/_language_holder.dm
index b43684747b6..cb8cf2b39a1 100644
--- a/code/modules/language/_language_holder.dm
+++ b/code/modules/language/_language_holder.dm
@@ -47,11 +47,17 @@ Key procs
/// If true, overrides tongue aforementioned limitations.
var/omnitongue = FALSE
/// Handles displaying the language menu UI.
- var/datum/language_menu/language_menu
+ VAR_FINAL/datum/language_menu/language_menu
/// Currently spoken language
var/selected_language
/// Tracks the entity that owns the holder.
- var/atom/movable/owner
+ VAR_FINAL/atom/movable/owner
+ /// Lazyassoclist of all mutual understanding this holder has
+ /// You generally don't want to access this, you want [best_mutual_languages] instead
+ /// Format: list(language_type = list(source = % of understanding))
+ VAR_PROTECTED/list/mutual_understanding
+ /// Cached form of the mutual language list which only contains the best understanding available to each language
+ VAR_FINAL/list/best_mutual_languages
/// Initializes, and copies in the languages from the current atom if available.
/datum/language_holder/New(atom/new_owner)
@@ -66,16 +72,48 @@ Key procs
// If we have an owner, we'll set a default selected language
if(owner)
get_selected_language()
+ // Normally this is applied in grant_language, which we bypass
+ for(var/language in understood_languages)
+ gain_partial_understanding_from_language(language)
/datum/language_holder/Destroy()
QDEL_NULL(language_menu)
owner = null
return ..()
+/// Helper to get all the partial understanding from the passed language
+/// Does effectively nothing if given a language already understood
+/datum/language_holder/proc/gain_partial_understanding_from_language(language)
+ PRIVATE_PROC(TRUE)
+
+ var/datum/language/prototype = GLOB.language_datum_instances[language]
+ for(var/other_language in prototype.mutual_understanding)
+ grant_partial_language(other_language, prototype.mutual_understanding[other_language], language)
+
+/// Helper to remove all the partial understanding from the passed language
+/datum/language_holder/proc/lose_partial_understanding_from_language(language)
+ PRIVATE_PROC(TRUE)
+
+ var/datum/language/prototype = GLOB.language_datum_instances[language]
+ for(var/other_language in prototype.mutual_understanding)
+ remove_partial_language(other_language, language)
+
+/// Calculates the "best mutual language list"
+/datum/language_holder/proc/calculate_best_mutual_language()
+ best_mutual_languages = list()
+ for(var/language in mutual_understanding)
+ for(var/source in mutual_understanding[language])
+ // if this mutual understanding comes from a language, and that language is blocked, skip it
+ if(LAZYACCESS(blocked_languages, source))
+ continue
+ if(!best_mutual_languages[language] || best_mutual_languages[language] < mutual_understanding[language][source])
+ best_mutual_languages[language] = mutual_understanding[language][source]
+
/// Grants the supplied language.
/datum/language_holder/proc/grant_language(language, language_flags = ALL, source = LANGUAGE_MIND)
if(language_flags & UNDERSTOOD_LANGUAGE)
LAZYORASSOCLIST(understood_languages, language, source)
+ gain_partial_understanding_from_language(language)
. = TRUE
if(language_flags & SPOKEN_LANGUAGE)
LAZYORASSOCLIST(spoken_languages, language, source)
@@ -91,6 +129,14 @@ Key procs
omnitongue = TRUE
return TRUE
+/// Grants partial understanding of the passed language.
+/// Giving 100 understanding is basically equivalent to knowning the language, just with butchered punctuation.
+/datum/language_holder/proc/grant_partial_language(language, amount = 50, source = LANGUAGE_MIND)
+ LAZYINITLIST(mutual_understanding)
+ LAZYSET(mutual_understanding[language], source, amount)
+ calculate_best_mutual_language()
+ return TRUE
+
/// Removes a single language or source, removing all sources returns the pre-removal state of the language.
/datum/language_holder/proc/remove_language(language, language_flags = ALL, source = LANGUAGE_ALL)
if(language_flags & UNDERSTOOD_LANGUAGE)
@@ -98,6 +144,8 @@ Key procs
LAZYREMOVE(understood_languages, language)
else
LAZYREMOVEASSOC(understood_languages, language, source)
+ if(!LAZYACCESS(understood_languages, language))
+ lose_partial_understanding_from_language(language)
. = TRUE
if(language_flags & SPOKEN_LANGUAGE)
@@ -117,6 +165,30 @@ Key procs
omnitongue = FALSE
return TRUE
+/// Removes partial understanding of the passed language.
+/datum/language_holder/proc/remove_partial_language(language, source = LANGUAGE_MIND)
+ . = FALSE
+ if(source == LANGUAGE_ALL)
+ for(var/other_source in mutual_understanding[language])
+ if(ispath(other_source, /datum/language))
+ continue
+ . = remove_partial_language(language, other_source) || .
+ else if(LAZYACCESSASSOC(mutual_understanding, language, source))
+ LAZYREMOVE(mutual_understanding[language], source)
+ ASSOC_UNSETEMPTY(mutual_understanding, language)
+ UNSETEMPTY(mutual_understanding)
+ . = TRUE
+
+ if(.)
+ calculate_best_mutual_language()
+ return .
+
+/// Removes all partial understandings of all languages.
+/datum/language_holder/proc/remove_all_partial_languages(source = LANGUAGE_MIND)
+ for(var/language in mutual_understanding)
+ remove_partial_language(language, source)
+ return TRUE
+
/// Adds a single language or list of languages to the blocked language list.
/datum/language_holder/proc/add_blocked_language(languages, source = LANGUAGE_MIND)
if(!islist(languages))
@@ -124,6 +196,7 @@ Key procs
for(var/language in languages)
LAZYORASSOCLIST(blocked_languages, language, source)
+ calculate_best_mutual_language()
return TRUE
/// Removes a single language or list of languages from the blocked language list.
@@ -136,7 +209,7 @@ Key procs
LAZYREMOVE(blocked_languages, language)
else
LAZYREMOVEASSOC(blocked_languages, language, source)
-
+ calculate_best_mutual_language()
return TRUE
/// Checks if you have the language passed.
@@ -229,6 +302,11 @@ Key procs
if(LANGUAGE_MIND in blocked_languages[language])
remove_blocked_language(language, LANGUAGE_MIND)
to_holder.add_blocked_language(language, LANGUAGE_MIND)
+ for(var/language in mutual_understanding)
+ var/mind_understanding = mutual_understanding[language][LANGUAGE_MIND]
+ if(mind_understanding > 0)
+ remove_partial_language(language, LANGUAGE_MIND)
+ to_holder.grant_partial_language(language, mind_understanding, LANGUAGE_MIND)
if(owner)
get_selected_language()
diff --git a/code/modules/language/_language_menu.dm b/code/modules/language/_language_menu.dm
index 487c0e67be1..b9ffa4fab58 100644
--- a/code/modules/language/_language_menu.dm
+++ b/code/modules/language/_language_menu.dm
@@ -24,6 +24,7 @@
var/list/data = list()
var/atom/movable/speaker = language_holder.owner
+ var/list/partial_languages = speaker?.get_partially_understood_languages()
data["languages"] = list()
for(var/datum/language/language as anything in GLOB.all_languages)
var/list/lang_data = list()
@@ -38,6 +39,7 @@
lang_data["can_speak"] = !!speaker.has_language(language, SPOKEN_LANGUAGE)
lang_data["could_speak"] = !!(language_holder.omnitongue || speaker.could_speak_language(language))
lang_data["can_understand"] = !!speaker.has_language(language, UNDERSTOOD_LANGUAGE)
+ lang_data["partial_understanding"] = partial_languages?[language] || 0
UNTYPED_LIST_ADD(data["languages"], lang_data)
diff --git a/code/modules/language/aphasia.dm b/code/modules/language/aphasia.dm
index 2d82b79892e..4ee2b9b5eae 100644
--- a/code/modules/language/aphasia.dm
+++ b/code/modules/language/aphasia.dm
@@ -4,7 +4,12 @@
flags = LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD
key = "i"
syllables = list("m","n","gh","h","l","s","r","a","e","i","o","u")
- space_chance = 20
+ space_chance = 0
+ sentence_chance = 0
+ between_word_sentence_chance = 10
+ between_word_space_chance = 75
+ additional_syllable_low = 0
+ additional_syllable_high = 0
default_priority = 10
icon_state = "aphasia"
always_use_default_namelist = TRUE // Shouldn't generate names for this anyways
diff --git a/code/modules/language/beachbum.dm b/code/modules/language/beachbum.dm
index bd319e717ff..213aa096891 100644
--- a/code/modules/language/beachbum.dm
+++ b/code/modules/language/beachbum.dm
@@ -2,7 +2,12 @@
name = "Beachtongue"
desc = "An ancient language from the distant Beach Planet. People magically learn to speak it under the influence of space drugs."
key = "u"
- space_chance = 85
+ space_chance = 80
+ sentence_chance = 5
+ between_word_sentence_chance = 0
+ between_word_space_chance = 100
+ additional_syllable_low = -2
+ additional_syllable_high = -1
default_priority = 90
syllables = list(
"cowabunga", "rad", "radical", "dudes", "bogus", "weeed", "every",
@@ -19,3 +24,8 @@
)
icon_state = "beach"
always_use_default_namelist = TRUE
+
+ mutual_understanding = list(
+ /datum/language/common = 50,
+ /datum/language/uncommon = 33,
+ )
diff --git a/code/modules/language/buzzwords.dm b/code/modules/language/buzzwords.dm
index 2ed033bca34..0a9a1828c44 100644
--- a/code/modules/language/buzzwords.dm
+++ b/code/modules/language/buzzwords.dm
@@ -3,6 +3,11 @@
desc = "A common language to all insects, made by the rhythmic beating of wings."
key = "z"
space_chance = 0
+ sentence_chance = 0
+ between_word_sentence_chance = 5
+ between_word_space_chance = 0
+ additional_syllable_low = 0
+ additional_syllable_high = 0
syllables = list(
"bzz","zzz","z","bz","bzzz","zzzz", "bzzzz", "b", "zz", "zzzzz"
)
diff --git a/code/modules/language/calcic.dm b/code/modules/language/calcic.dm
index 477e442203b..2aaf57a83ff 100644
--- a/code/modules/language/calcic.dm
+++ b/code/modules/language/calcic.dm
@@ -3,6 +3,11 @@
desc = "The disjointed and staccato language of plasmamen. Also understood by skeletons."
key = "b"
space_chance = 10
+ sentence_chance = 2
+ between_word_sentence_chance = 10
+ between_word_space_chance = 75
+ additional_syllable_low = 0
+ additional_syllable_high = 1
syllables = list(
"k", "ck", "ack", "ick", "cl", "tk", "sk", "isk", "tak",
"kl", "hs", "ss", "ks", "lk", "dk", "gk", "ka", "ska", "la", "pk",
diff --git a/code/modules/language/carptongue.dm b/code/modules/language/carptongue.dm
index 34ef4c4b5cd..f3a376287ec 100644
--- a/code/modules/language/carptongue.dm
+++ b/code/modules/language/carptongue.dm
@@ -5,7 +5,11 @@
icon_state = "fish"
flags = NO_STUTTER|TONGUELESS_SPEECH
sentence_chance = 0
- space_chance = 75
+ space_chance = 0
+ between_word_sentence_chance = 0
+ between_word_space_chance = 75
+ additional_syllable_low = -2
+ additional_syllable_high = -1
syllables = list("glub", "blub", "bloop")
default_priority = 25
default_name_syllable_min = 1
diff --git a/code/modules/language/codespeak.dm b/code/modules/language/codespeak.dm
index 242095b3bb7..8cc8c1c460a 100644
--- a/code/modules/language/codespeak.dm
+++ b/code/modules/language/codespeak.dm
@@ -7,26 +7,21 @@
icon_state = "codespeak"
always_use_default_namelist = TRUE // No syllables anyways
-/datum/language/codespeak/scramble(input)
- var/lookup = check_cache(input)
- if(lookup)
- return lookup
+/datum/language/codespeak/scramble_sentence(input, list/mutual_languages)
+ var/sentence = read_word_cache(input)
+ if(sentence)
+ return sentence
- . = ""
+ sentence = ""
var/list/words = list()
- while(length_char(.) < length_char(input))
+ while(length_char(sentence) < length_char(input))
words += generate_code_phrase(return_list=TRUE)
- . = jointext(words, ", ")
+ sentence = jointext(words, ", ")
- . = capitalize(.)
+ sentence = capitalize(sentence)
- var/input_ending = copytext_char(input, -1)
+ sentence += find_last_punctuation(input)
- var/static/list/endings
- if(!endings)
- endings = list("!", "?", ".")
+ write_word_cache(input, sentence)
- if(input_ending in endings)
- . += input_ending
-
- add_to_cache(input, .)
+ return sentence
diff --git a/code/modules/language/common.dm b/code/modules/language/common.dm
index 6bad808fef2..bf015a04e2f 100644
--- a/code/modules/language/common.dm
+++ b/code/modules/language/common.dm
@@ -5,6 +5,12 @@
key = "0"
flags = TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_UNDERSTOOD
default_priority = 100
+ space_chance = 20
+ sentence_chance = 0
+ between_word_sentence_chance = 10
+ between_word_space_chance = 75
+ additional_syllable_low = 0
+ additional_syllable_high = 0
icon_state = "galcom"
// Default namelist is the human namelist, and common is the human language, so might as well.
@@ -55,3 +61,8 @@
"his", "ing", "ion", "ith", "not", "ome", "oul", "our", "sho", "ted", "ter", "tha", "the", "thi",
),
)
+
+ mutual_understanding = list(
+ /datum/language/beachbum = 33,
+ /datum/language/uncommon = 20,
+ )
diff --git a/code/modules/language/draconic.dm b/code/modules/language/draconic.dm
index f61bbef009e..76cc7350f60 100644
--- a/code/modules/language/draconic.dm
+++ b/code/modules/language/draconic.dm
@@ -3,7 +3,12 @@
desc = "The common language of lizard-people, composed of sibilant hisses and rattles."
key = "o"
flags = TONGUELESS_SPEECH
- space_chance = 40
+ space_chance = 12
+ sentence_chance = 0
+ between_word_sentence_chance = 10
+ between_word_space_chance = 75
+ additional_syllable_low = 0
+ additional_syllable_high = 3
syllables = list(
"za", "az", "ze", "ez", "zi", "iz", "zo", "oz", "zu", "uz", "zs", "sz",
"ha", "ah", "he", "eh", "hi", "ih", "ho", "oh", "hu", "uh", "hs", "sh",
diff --git a/code/modules/language/drone.dm b/code/modules/language/drone.dm
index 09fb6546e4a..373f504a135 100644
--- a/code/modules/language/drone.dm
+++ b/code/modules/language/drone.dm
@@ -8,6 +8,10 @@
// ...|..||.||||.|.||.|.|.|||.|||
space_chance = 0
sentence_chance = 0
+ between_word_sentence_chance = 0
+ between_word_space_chance = 0
+ additional_syllable_low = 0
+ additional_syllable_high = 0
default_priority = 20
icon_state = "drone"
diff --git a/code/modules/language/machine.dm b/code/modules/language/machine.dm
index 4be282a5e28..63bec30c6f8 100644
--- a/code/modules/language/machine.dm
+++ b/code/modules/language/machine.dm
@@ -9,7 +9,12 @@
"bop", "bop", "dee", "dee", "doo", "doo", "hiss", "hss", "buzz",
"buzz", "bzz", "ksssh", "keey", "wurr", "wahh", "tzzz",
)
- space_chance = 10
+ space_chance = 0
+ sentence_chance = 0
+ between_word_sentence_chance = 10
+ between_word_space_chance = 10
+ additional_syllable_low = 0
+ additional_syllable_high = 0
default_priority = 90
icon_state = "eal"
diff --git a/code/modules/language/moffic.dm b/code/modules/language/moffic.dm
index fb8dea63dcc..2e90b5cd168 100644
--- a/code/modules/language/moffic.dm
+++ b/code/modules/language/moffic.dm
@@ -2,7 +2,12 @@
name = "Moffic"
desc = "The language of the Mothpeople borders on complete unintelligibility."
key = "m"
- space_chance = 10
+ space_chance = 5
+ sentence_chance = 0
+ between_word_sentence_chance = 10
+ between_word_space_chance = 25
+ additional_syllable_low = 0
+ additional_syllable_high = 0
syllables = list(
"år", "i", "går", "sek", "mo", "ff", "ok", "gj", "ø", "gå", "la", "le",
"lit", "ygg", "van", "dår", "næ", "møt", "idd", "hvo", "ja", "på", "han",
diff --git a/code/modules/language/monkey.dm b/code/modules/language/monkey.dm
index 423e94f22bd..bb6b4ac1a71 100644
--- a/code/modules/language/monkey.dm
+++ b/code/modules/language/monkey.dm
@@ -2,7 +2,12 @@
name = "Chimpanzee"
desc = "Ook ook ook."
key = "1"
- space_chance = 100
+ space_chance = 0
+ sentence_chance = 0
+ between_word_sentence_chance = 10
+ between_word_space_chance = 75
+ additional_syllable_low = 0
+ additional_syllable_high = 0
syllables = list("oop", "aak", "chee", "eek")
default_priority = 80
diff --git a/code/modules/language/mushroom.dm b/code/modules/language/mushroom.dm
index 910489fd6dd..95562600d21 100644
--- a/code/modules/language/mushroom.dm
+++ b/code/modules/language/mushroom.dm
@@ -3,6 +3,11 @@
desc = "A language that consists of the sound of periodic gusts of spore-filled air being released."
key = "y"
sentence_chance = 0
+ sentence_chance = 0
+ between_word_sentence_chance = 10
+ between_word_space_chance = 75
+ additional_syllable_low = -4
+ additional_syllable_high = -2
default_priority = 80
syllables = list("poof", "pff", "pFfF", "piff", "puff", "pooof", "pfffff", "piffpiff", "puffpuff", "poofpoof", "pifpafpofpuf")
default_name_syllable_min = 1
diff --git a/code/modules/language/narsian.dm b/code/modules/language/narsian.dm
index 4e8402128a0..0fc96da674d 100644
--- a/code/modules/language/narsian.dm
+++ b/code/modules/language/narsian.dm
@@ -2,8 +2,12 @@
name = "Nar'Sian"
desc = "The ancient, blood-soaked, impossibly complex language of Nar'Sian cultists."
key = "n"
- sentence_chance = 8
- space_chance = 95 //very high due to the potential length of each syllable
+ space_chance = 75 //very high due to the potential length of each syllable
+ sentence_chance = 10
+ between_word_sentence_chance = 5
+ between_word_space_chance = 95
+ additional_syllable_low = -1
+ additional_syllable_high = 0
var/static/list/base_syllables = list(
"h", "v", "c", "e", "g", "d", "r", "n", "h", "o", "p",
"ra", "so", "at", "il", "ta", "gh", "sh", "ya", "te", "sh", "ol", "ma", "om", "ig", "ni", "in",
diff --git a/code/modules/language/nekomimetic.dm b/code/modules/language/nekomimetic.dm
index c947c185f23..2551b15eceb 100644
--- a/code/modules/language/nekomimetic.dm
+++ b/code/modules/language/nekomimetic.dm
@@ -2,7 +2,12 @@
name = "Nekomimetic"
desc = "To the casual observer, this language is an incomprehensible mess of broken Japanese. To the felinids, it's somehow comprehensible."
key = "f"
- space_chance = 70
+ space_chance = 15
+ sentence_chance = 0
+ between_word_sentence_chance = 10
+ between_word_space_chance = 75
+ additional_syllable_low = -1
+ additional_syllable_high = 1
syllables = list(
"neko", "nyan", "mimi", "moe", "mofu", "fuwa", "kyaa", "kawaii", "poka", "munya",
"puni", "munyu", "ufufu", "uhuhu", "icha", "doki", "kyun", "kusu", "nya", "nyaa",
diff --git a/code/modules/language/piratespeak.dm b/code/modules/language/piratespeak.dm
index a2faddb544f..d4db6b082a2 100644
--- a/code/modules/language/piratespeak.dm
+++ b/code/modules/language/piratespeak.dm
@@ -3,6 +3,11 @@
desc = "The language of space pirates."
key = "p"
space_chance = 100
+ sentence_chance = 10
+ between_word_sentence_chance = 10
+ between_word_space_chance = 75
+ additional_syllable_low = -2
+ additional_syllable_high = -1
default_priority = 90
syllables = list(
"arr", "ahoy", "rum", "aye", "blimey", "booty", "bucko", "grog", "treasure",
diff --git a/code/modules/language/shadowtongue.dm b/code/modules/language/shadowtongue.dm
index 35158939385..f0519f6590c 100644
--- a/code/modules/language/shadowtongue.dm
+++ b/code/modules/language/shadowtongue.dm
@@ -4,7 +4,12 @@
name = "Shadowtongue"
desc = "What a grand and intoxicating innocence."
key = "x"
- space_chance = 50
+ space_chance = 40
+ sentence_chance = 0
+ between_word_sentence_chance = 10
+ between_word_space_chance = 75
+ additional_syllable_low = 0
+ additional_syllable_high = 1
syllables = list(
"er", "sint", "en", "et", "nor", "bahr", "sint", "un", "ku'elm", "lakor", "eri",
"noj", "dashilu", "as", "ot", "lih", "morh", "ghinu", "kin", "sha", "marik", "jibu",
diff --git a/code/modules/language/spinwarder.dm b/code/modules/language/spinwarder.dm
index 22d4546f1b9..8c3877f38c0 100644
--- a/code/modules/language/spinwarder.dm
+++ b/code/modules/language/spinwarder.dm
@@ -4,6 +4,12 @@
name = "Spinwarder"
desc = "The official language of the Spinward Stellar Coalition, as inherited from the Third Soviet Union."
key = "s"
+ space_chance = 20
+ sentence_chance = 0
+ between_word_sentence_chance = 10
+ between_word_space_chance = 75
+ additional_syllable_low = 0
+ additional_syllable_high = 0
flags = TONGUELESS_SPEECH
syllables = list(
"v", "od", "noy", "ned", "ele", "dn", "ey", "da", "ny", "et", "mes", "yat",
diff --git a/code/modules/language/sylvan.dm b/code/modules/language/sylvan.dm
index 4f66fb5931c..e2d9facf4f4 100644
--- a/code/modules/language/sylvan.dm
+++ b/code/modules/language/sylvan.dm
@@ -3,7 +3,12 @@
name = "Sylvan"
desc = "A complicated, ancient language spoken by sentient plants."
key = "h"
- space_chance = 20
+ space_chance = 10
+ sentence_chance = 0
+ between_word_sentence_chance = 10
+ between_word_space_chance = 50
+ additional_syllable_low = 1
+ additional_syllable_high = 2
syllables = list(
"fii", "sii", "rii", "rel", "maa", "ala", "san", "tol", "tok", "dia", "eres",
"fal", "tis", "bis", "qel", "aras", "losk", "rasa", "eob", "hil", "tanl", "aere",
diff --git a/code/modules/language/terrum.dm b/code/modules/language/terrum.dm
index 63b527202f4..8715a44f209 100644
--- a/code/modules/language/terrum.dm
+++ b/code/modules/language/terrum.dm
@@ -2,7 +2,12 @@
name = "Terrum"
desc = "The language of the golems. Sounds similar to old-earth Hebrew."
key = "g"
- space_chance = 40
+ space_chance = 20
+ sentence_chance = 0
+ between_word_sentence_chance = 10
+ between_word_space_chance = 75
+ additional_syllable_low = 1
+ additional_syllable_high = 2
syllables = list(
"sha", "vu", "nah", "ha", "yom", "ma", "cha", "ar", "et", "mol", "lua",
"ch", "na", "sh", "ni", "yah", "bes", "ol", "hish", "ev", "la", "ot", "la",
diff --git a/code/modules/language/uncommon.dm b/code/modules/language/uncommon.dm
index 117ed1c76fd..43b878e8341 100644
--- a/code/modules/language/uncommon.dm
+++ b/code/modules/language/uncommon.dm
@@ -3,7 +3,12 @@
desc = "The second-most spoken Human language."
key = "!"
flags = TONGUELESS_SPEECH
- space_chance = 50
+ space_chance = 20
+ sentence_chance = 0
+ between_word_sentence_chance = 10
+ between_word_space_chance = 75
+ additional_syllable_low = 0
+ additional_syllable_high = 0
syllables = list(
"ba", "be", "bo", "ca", "ce", "co", "da", "de", "do",
"fa", "fe", "fo", "ga", "ge", "go", "ha", "he", "ho",
@@ -14,3 +19,8 @@
)
icon_state = "galuncom"
default_priority = 90
+
+ mutual_understanding = list(
+ /datum/language/common = 20,
+ /datum/language/beachbum = 20,
+ )
diff --git a/code/modules/language/voltaic.dm b/code/modules/language/voltaic.dm
index 90ab90dbe48..97c52da23f6 100644
--- a/code/modules/language/voltaic.dm
+++ b/code/modules/language/voltaic.dm
@@ -4,6 +4,11 @@
desc = "A sparky language made by manipulating electrical discharge."
key = "v"
space_chance = 20
+ sentence_chance = 0
+ between_word_sentence_chance = 10
+ between_word_space_chance = 75
+ additional_syllable_low = 0
+ additional_syllable_high = 1
syllables = list(
"bzzt", "skrrt", "zzp", "mmm", "hzz", "tk", "shz", "k", "z",
"bzt", "zzt", "skzt", "skzz", "hmmt", "zrrt", "hzzt", "hz",
diff --git a/code/modules/language/xenocommon.dm b/code/modules/language/xenocommon.dm
index f4949b7d73c..b8ce823268f 100644
--- a/code/modules/language/xenocommon.dm
+++ b/code/modules/language/xenocommon.dm
@@ -4,6 +4,11 @@
key = "4"
syllables = list("sss","sSs","SSS")
default_priority = 50
-
+ space_chance = 0
+ sentence_chance = 0
+ between_word_sentence_chance = 0
+ between_word_space_chance = 50
+ additional_syllable_low = 0
+ additional_syllable_high = 0
icon_state = "xeno"
always_use_default_namelist = TRUE // Sssss Ssss?
diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm
index eb8143ad4b9..9b3f6544890 100644
--- a/code/modules/mob/living/living_say.dm
+++ b/code/modules/mob/living/living_say.dm
@@ -172,7 +172,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
if(!try_speak(original_message, ignore_spam, forced, filterproof))
return
- language = message_mods[LANGUAGE_EXTENSION] || get_selected_language()
+ language ||= message_mods[LANGUAGE_EXTENSION] || get_selected_language()
var/succumbed = FALSE
diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm
index d0d502b42c0..0d48bdc699b 100644
--- a/code/modules/unit_tests/_unit_tests.dm
+++ b/code/modules/unit_tests/_unit_tests.dm
@@ -302,6 +302,7 @@
#include "syringe_gun.dm"
#include "tail_wag.dm"
#include "teleporters.dm"
+#include "text.dm"
#include "tgui_create_message.dm"
#include "timer_sanity.dm"
#include "trait_addition_and_removal.dm"
diff --git a/code/modules/unit_tests/text.dm b/code/modules/unit_tests/text.dm
new file mode 100644
index 00000000000..80965391cd2
--- /dev/null
+++ b/code/modules/unit_tests/text.dm
@@ -0,0 +1,30 @@
+/// Tests strip_outer_punctuation()
+/datum/unit_test/strip_outer_punctuation
+
+/datum/unit_test/strip_outer_punctuation/Run()
+ var/list/sample_to_expected = list(
+ "Hello, world!" = "Hello, world",
+ "|Who are you?|" = "Who are you",
+ "Oh wow..." = "Oh wow",
+ "My name is E.T, the alien!" = "My name is E.T, the alien",
+ "I'm +YELLING+ at you!" = "I'm +YELLING+ at you",
+ "+I'm REALLY yelling!+" = "I'm REALLY yelling",
+ )
+
+ for(var/sample in sample_to_expected)
+ TEST_ASSERT_EQUAL(strip_outer_punctuation(sample), sample_to_expected[sample], "Strip punctuation failed for sample text: [sample]")
+
+/// Tests find_last_punctuation()
+/datum/unit_test/find_last_punctuation
+
+/datum/unit_test/find_last_punctuation/Run()
+ var/list/sample_to_expected = list(
+ "Hello, world!" = "!",
+ "|Who are you?|" = "?",
+ "Really, |WHO are you|?" = "?",
+ "Oh wow..." = "...",
+ "My name is E.T, the alien!" = "!",
+ )
+
+ for(var/sample in sample_to_expected)
+ TEST_ASSERT_EQUAL(find_last_punctuation(sample), sample_to_expected[sample], "Find punctuation failed for sample text: [sample]")
diff --git a/icons/ui/chat/language.dmi b/icons/ui/chat/language.dmi
index 7250a26e085..c45b61b1041 100644
Binary files a/icons/ui/chat/language.dmi and b/icons/ui/chat/language.dmi differ
diff --git a/tgui/packages/tgui/interfaces/LanguageMenu.tsx b/tgui/packages/tgui/interfaces/LanguageMenu.tsx
index d134000ddd5..8a119f61aae 100644
--- a/tgui/packages/tgui/interfaces/LanguageMenu.tsx
+++ b/tgui/packages/tgui/interfaces/LanguageMenu.tsx
@@ -21,6 +21,7 @@ type Language = {
can_speak: BooleanLike; // mentally, we know how to speak it
could_speak: BooleanLike; // physically, we are capable of speaking it
can_understand: BooleanLike; // we can understand it regardless
+ partial_understanding: number; // how much of the language we understand
icon: string;
icon_state: string;
};
@@ -61,6 +62,20 @@ const LangSpeakIcon = (props: LanguagePropsPassRest) => {
const LangUnderstandIcon = (props: LanguageProps) => {
const { language } = props;
+ if (!language.can_understand && language.partial_understanding > 0) {
+ return (
+
+
+ {language.partial_understanding}%
+
+
+ );
+ }
return ;
};
@@ -202,7 +217,11 @@ export const LanguageMenu = (props) => {
// also, push all languages we can speak to the top, then all languagse we can only understand, then alphabetize
const shown_languages = languages
.filter(
- (language) => admin_mode || language.can_speak || language.can_understand,
+ (language) =>
+ admin_mode ||
+ language.can_speak ||
+ language.can_understand ||
+ language.partial_understanding > 0,
)
.sort(
(a, b) =>
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/language.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/language.tsx
index 39c48ae40d6..836a600a724 100644
--- a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/language.tsx
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/language.tsx
@@ -1,7 +1,23 @@
-import { FeatureChoiced } from '../base';
-import { FeatureDropdownInput } from '../dropdowns';
+import { CheckboxInput, FeatureChoiced, FeatureToggle } from '../base';
+import {
+ FeatureDropdownInput,
+ FeatureIconnedDropdownInput,
+} from '../dropdowns';
export const language: FeatureChoiced = {
name: 'Language',
+ component: FeatureIconnedDropdownInput,
+};
+
+export const language_speakable: FeatureToggle = {
+ name: 'Language Speakable',
+ description: `If unchecked, you'll only be able to understand the language,
+ but not speak it.`,
+ component: CheckboxInput,
+};
+
+export const language_skill: FeatureChoiced = {
+ name: 'Language Skill',
+ description: 'The percentage of the language you can understand.',
component: FeatureDropdownInput,
};