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, };