diff --git a/code/_globalvars/lists/flavor_misc.dm b/code/_globalvars/lists/flavor_misc.dm index 38072aaec01e..cf803d1c4519 100644 --- a/code/_globalvars/lists/flavor_misc.dm +++ b/code/_globalvars/lists/flavor_misc.dm @@ -307,3 +307,12 @@ GLOBAL_LIST_INIT(status_display_state_pictures, list( "blank", "shuttle", )) + +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/_globalvars/lists/mobs.dm b/code/_globalvars/lists/mobs.dm index 8d35f2192062..3f1ffe94f02e 100644 --- a/code/_globalvars/lists/mobs.dm +++ b/code/_globalvars/lists/mobs.dm @@ -90,7 +90,7 @@ GLOBAL_LIST_INIT_TYPED(language_datum_instances, /datum/language, init_language_ /// List if all language typepaths learnable, IE, those with keys GLOBAL_LIST_INIT(all_languages, init_all_languages()) // /List of language prototypes to reference, assoc "name" = typepath -GLOBAL_LIST_INIT(language_types_by_name, init_language_types_by_name()) +GLOBAL_LIST_INIT_TYPED(language_types_by_name, /datum/language, init_language_types_by_name()) /proc/init_language_prototypes() var/list/lang_list = list() diff --git a/code/controllers/subsystem/discord.dm b/code/controllers/subsystem/discord.dm index 7efdbfcda6a5..ccfa60e09c5f 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()) @@ -156,7 +152,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) @@ -298,4 +294,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/controllers/subsystem/processing/quirks.dm b/code/controllers/subsystem/processing/quirks.dm index 79f03a1a8506..bd9785551edf 100644 --- a/code/controllers/subsystem/processing/quirks.dm +++ b/code/controllers/subsystem/processing/quirks.dm @@ -19,7 +19,7 @@ GLOBAL_LIST_INIT_TYPED(quirk_blacklist, /list/datum/quirk, list( list(/datum/quirk/social_anxiety, /datum/quirk/mute), list(/datum/quirk/mute, /datum/quirk/softspoken), list(/datum/quirk/poor_aim, /datum/quirk/bighands), - list(/datum/quirk/bilingual, /datum/quirk/foreigner), + // list(/datum/quirk/bilingual, /datum/quirk/foreigner), list(/datum/quirk/spacer_born, /datum/quirk/item_quirk/settler), list(/datum/quirk/photophobia, /datum/quirk/nyctophobia), list(/datum/quirk/item_quirk/settler, /datum/quirk/freerunning), diff --git a/code/datums/brain_damage/mild.dm b/code/datums/brain_damage/mild.dm index 513f56840b56..97001f177f19 100644 --- a/code/datums/brain_damage/mild.dm +++ b/code/datums/brain_damage/mild.dm @@ -191,8 +191,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) @@ -212,7 +210,7 @@ word = copytext(word, 1, suffix_foundon) word = html_decode(word) - if(lowertext(word) in common_words) + if(GLOB.most_common_words[lowertext(word)]) new_message += word + suffix else if(prob(30) && message_split.len > 2) diff --git a/code/datums/quirks/neutral_quirks/foreigner.dm b/code/datums/quirks/neutral_quirks/foreigner.dm index da317a7e66a4..1d252efc920e 100644 --- a/code/datums/quirks/neutral_quirks/foreigner.dm +++ b/code/datums/quirks/neutral_quirks/foreigner.dm @@ -1,3 +1,4 @@ +/* /datum/quirk/foreigner name = "Foreigner" desc = "You're not from around here. You don't know Galactic Common!" @@ -19,3 +20,4 @@ human_holder.remove_blocked_language(/datum/language/common) if(ishumanbasic(human_holder)) human_holder.remove_language(/datum/language/uncommon) +*/ diff --git a/code/datums/quirks/positive_quirks/bilingual.dm b/code/datums/quirks/positive_quirks/bilingual.dm index 408a952cfe18..13c75d57bdec 100644 --- a/code/datums/quirks/positive_quirks/bilingual.dm +++ b/code/datums/quirks/positive_quirks/bilingual.dm @@ -1,3 +1,4 @@ +/* /datum/quirk/bilingual name = "Bilingual" desc = "Over the years you've picked up an extra language!" @@ -26,3 +27,4 @@ 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) +*/ diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 71bfdfcab4df..dfc72301fff0 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -1467,7 +1467,7 @@ */ /// Gets or creates the relevant language holder. For mindless atoms, gets the local one. For atom with mind, gets the mind one. -/atom/movable/proc/get_language_holder() +/atom/movable/proc/get_language_holder() as /datum/language_holder RETURN_TYPE(/datum/language_holder) if(QDELING(src)) CRASH("get_language_holder() called on a QDELing atom, \ @@ -1521,6 +1521,10 @@ /atom/movable/proc/get_random_understood_language() return get_language_holder().get_random_understood_language() +/// Gets a list of all mutually understood languages. +/atom/movable/proc/get_mutually_understood_languages() + return get_language_holder().get_mutually_understood_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 e202a508ecf0..546262b044a7 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_mutually_understood_languages())]\"" else message_out = "(Unintelligible)" packet_out["message"] = message_out diff --git a/code/game/say.dm b/code/game/say.dm index 3a8eb748b6b1..0075e0d2a801 100644 --- a/code/game/say.dm +++ b/code/game/say.dm @@ -213,7 +213,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_mutually_understood_languages()) return raw_message diff --git a/code/modules/client/preferences/language.dm b/code/modules/client/preferences/language.dm index 637c4542da27..9879f4c73de9 100644 --- a/code/modules/client/preferences/language.dm +++ b/code/modules/client/preferences/language.dm @@ -1,3 +1,4 @@ +/* /datum/preference/choiced/language category = PREFERENCE_CATEGORY_MANUALLY_RENDERED savefile_key = "language" @@ -33,3 +34,4 @@ /datum/preference/choiced/language/apply_to_human(mob/living/carbon/human/target, value) return +*/ diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index c97a26c5396f..6a7dcaa119e1 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -5,7 +5,7 @@ // You do not need to raise this if you are adding new values that have sane defaults. // Only raise this value when changing the meaning/format/name/layout of an existing value // where you would want the updater procs below to run -#define SAVEFILE_VERSION_MAX 45 +#define SAVEFILE_VERSION_MAX 45.1 /* SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Carn @@ -111,6 +111,9 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car data_to_migrate = list(INFO_RESKIN = save_data?["pride_pin"]), ) + if (current_version < 45.1) + migrate_quirks_to_language_menu(save_data) + /// checks through keybindings for outdated unbound keys and updates them /datum/preferences/proc/check_keybindings() if(!parent) diff --git a/code/modules/language/_language.dm b/code/modules/language/_language.dm index 3876720cbd44..f68405920c24 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 @@ -18,13 +20,23 @@ var/list/special_characters /// Likelihood of making a new sentence after each syllable. var/sentence_chance = 5 + /// Likelihood of making a new sentence after each word. + var/between_word_sentence_chance = 0 /// Likelihood of getting a space in the random scramble string var/space_chance = 55 + /// Likelyhood of getting a space between words + var/between_word_space_chance = 100 /// 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 spoken sentences + /// So if one person speaks over the radio, everyone hears the same thing. + var/list/last_sentence_cache = list() + /// The 1000 most common words get permanently cached + var/list/most_common_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. @@ -45,6 +57,11 @@ /// 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 + var/list/mutual_understanding + /// 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 +126,144 @@ return result -/datum/language/proc/check_cache(input) - var/lookup = scramble_cache[input] - if(lookup) +/// Checks the word cache for a word +/datum/language/proc/read_word_cache(input) + SHOULD_NOT_OVERRIDE(TRUE) + if(most_common_cache[input]) + return most_common_cache[input] + + . = scramble_cache[input] + if(. && scramble_cache[1] != input) + // bumps it to the top of the cache scramble_cache -= input - scramble_cache[input] = lookup - . = lookup + scramble_cache[input] = . + return . -/datum/language/proc/add_to_cache(input, scrambled_text) +/// Adds a word to the cache +/datum/language/proc/write_word_cache(input, scrambled_text) + SHOULD_NOT_OVERRIDE(TRUE) + if(GLOB.most_common_words[lowertext(input)]) + most_common_cache[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, scramble_cache.len-SCRAMBLE_CACHE_LEN-1) + 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) + . = last_sentence_cache[input] + if(. && last_sentence_cache[1] != input) + // bumps it to the top of the cache (don't anticipate this happening often) + last_sentence_cache -= input + last_sentence_cache[input] = . + return . - if(!length(syllables)) - return stars(input) +/// 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) + // 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], key, result_scramble) + if(last_sentence_cache.len > SENTENCE_CACHE_LEN) + last_sentence_cache.Cut(1, last_sentence_cache.len - SENTENCE_CACHE_LEN + 1) + +/// Goes through the input and removes any punctuation from the end of the string. +/proc/strip_punctuation(input) + var/static/list/bad_punctuation = list("!", "?", ".", "~", ";", ":", "-") + var/last_char = copytext_char(input, -1) + while(last_char in bad_punctuation) + input = copytext(input, 1, -1) + last_char = copytext_char(input, -1) + + return trim_right(input) + +/// Find what punctuation is at the end of the input, returns it. +/proc/find_last_punctuation(input) + . = copytext_char(input, -3) + if(. == "...") + return . + . = copytext_char(input, -2) + if(. in list("!!", "??", "..", "?!", "!?")) + return . + . = copytext_char(input, -1) + if(. in list("!", "?" ,".", "~", ";", ":", "-")) + return . + return "" + +/// 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 + if(translate_prob > 0) + var/base_word = lowertext(strip_punctuation(word)) + // 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[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(word) + // start building the word. first word is capitalized and otherwise untouched + . = capitalize(popleft(scrambled_words)) + for(var/word in scrambled_words) + if(prob(between_word_sentence_chance)) + . += ". " + else if(prob(between_word_space_chance)) + . += " " + + . += word + + // scrambling the words will drop punctuation, so re-add it at the end + . += find_last_punctuation(trim_right(input)) + + write_sentence_cache(input, cache_key, .) + + return . + +/// 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/lookup = check_cache(input) - if(lookup) - return lookup - - var/input_size = length_char(input) - var/scrambled_text = "" - var/capitalize = TRUE - - 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 + . = read_word_cache(input) + if(.) + return . + + if(!length(syllables)) + . = stars(input) + + else + var/input_size = length_char(input) + var/add_space = FALSE + var/add_period = FALSE + . = "" + while(length_char(.) < input_size) + // add in the last syllable's period or space first + if(add_period) + . += ". " + else if(add_space) + . += " " + // generate the next syllable (capitalize if we just added a period) + var/next = (. && length(special_characters) && prob(1)) ? pick(special_characters) : pick_weight_recursive(syllables) + if(add_period) + next = capitalize(next) + . += next + // determine if the next syllable gets a period or space + add_period = prob(sentence_chance) + add_space = prob(space_chance) + + write_word_cache(input, .) + + return . #undef SCRAMBLE_CACHE_LEN diff --git a/code/modules/language/_language_holder.dm b/code/modules/language/_language_holder.dm index b48a1ab1530a..f061ed2bab35 100644 --- a/code/modules/language/_language_holder.dm +++ b/code/modules/language/_language_holder.dm @@ -176,6 +176,18 @@ Key procs /datum/language_holder/proc/get_random_understood_language() return pick(understood_languages) +/// Gets a list of all mutually understood languages. +/datum/language_holder/proc/get_mutually_understood_languages() + var/list/mutual_languages = list() + for(var/language_type in understood_languages) + var/datum/language/language_instance = GLOB.language_datum_instances[language_type] + for(var/mutual_language_type in language_instance.mutual_understanding) + // add it to the list OR override it if it's a stronger mutual understanding + if(!mutual_languages[mutual_language_type] || mutual_languages[mutual_language_type] < language_instance.mutual_understanding[mutual_language_type]) + mutual_languages[mutual_language_type] = language_instance.mutual_understanding[mutual_language_type] + + return mutual_languages + /// Gets a random spoken language, useful for forced speech and such. /datum/language_holder/proc/get_random_spoken_language() return pick(spoken_languages) diff --git a/code/modules/language/beachbum.dm b/code/modules/language/beachbum.dm index bd319e717ffd..eb2447ded187 100644 --- a/code/modules/language/beachbum.dm +++ b/code/modules/language/beachbum.dm @@ -19,3 +19,8 @@ ) icon_state = "beach" always_use_default_namelist = TRUE + + mutual_understanding = list( + /datum/language/common = 50, + /datum/language/uncommon = 30, + ) diff --git a/code/modules/language/codespeak.dm b/code/modules/language/codespeak.dm index 242095b3bb7f..7c2657c7b285 100644 --- a/code/modules/language/codespeak.dm +++ b/code/modules/language/codespeak.dm @@ -7,10 +7,10 @@ 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) + . = read_word_cache(input) + if(.) + return . . = "" var/list/words = list() @@ -29,4 +29,4 @@ if(input_ending in endings) . += input_ending - add_to_cache(input, .) + write_word_cache(input, .) diff --git a/code/modules/language/common.dm b/code/modules/language/common.dm index 6bad808fef26..764375c4a0d3 100644 --- a/code/modules/language/common.dm +++ b/code/modules/language/common.dm @@ -55,3 +55,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/uncommon.dm b/code/modules/language/uncommon.dm index 117ed1c76fd1..58c1d5bba2cb 100644 --- a/code/modules/language/uncommon.dm +++ b/code/modules/language/uncommon.dm @@ -14,3 +14,8 @@ ) icon_state = "galuncom" default_priority = 90 + + mutual_understanding = list( + /datum/language/common = 33, + /datum/language/beachbum = 20, + ) diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm index 747d2154c7ae..f5f67a39b620 100644 --- a/code/modules/mob/living/living_say.dm +++ b/code/modules/mob/living/living_say.dm @@ -158,6 +158,9 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( return language = message_mods[LANGUAGE_EXTENSION] || get_selected_language() + if(!language) + message_mods[MODE_CUSTOM_SAY_EMOTE] ||= "makes \a [pick("strange", "weird", "bizarre", "peculiar", "odd", "unusual", "curious")] sound" + message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] = TRUE var/succumbed = FALSE diff --git a/maplestation_modules/code/datums/quirks/good.dm b/maplestation_modules/code/datums/quirks/good.dm index a1c65f47beb2..a205c49834fd 100644 --- a/maplestation_modules/code/datums/quirks/good.dm +++ b/maplestation_modules/code/datums/quirks/good.dm @@ -4,14 +4,8 @@ /datum/quirk/jolly value = 3 -/datum/quirk/bilingual - icon = FA_ICON_GLOBE_EUROPE - value = 0 - desc = "Over the years you've picked up an extra language! \ - (Made redundant by the Language Picker - use it instead.)" - medical_record_text = "Patient is bilingual speaks multiple languages." - // New quirks +/* /// Trilingual quirk - Gives the owner a language. /datum/quirk/trilingual name = "Trilingual" @@ -59,6 +53,7 @@ /datum/quirk/trilingual/remove() if(added_language && !QDELETED(quirk_holder)) quirk_holder.get_language_holder().remove_language(added_language, ALL, LANGUAGE_QUIRK) +*/ /datum/quirk/no_appendix name = "Appendicitis Survivor" diff --git a/maplestation_modules/code/modules/antagonists/advanced_cult/clock_cult/clock_language.dm b/maplestation_modules/code/modules/antagonists/advanced_cult/clock_cult/clock_language.dm index 1b14aed539cd..68a1c4bcf9da 100644 --- a/maplestation_modules/code/modules/antagonists/advanced_cult/clock_cult/clock_language.dm +++ b/maplestation_modules/code/modules/antagonists/advanced_cult/clock_cult/clock_language.dm @@ -11,7 +11,7 @@ spans = list(SPAN_ROBOT) icon_state = "ratvar" -/datum/language/ratvarian/scramble(input) +/datum/language/ratvarian/scramble_sentence(input, list/mutual_languages) return text2ratvar(input) /// Regexes used to add ratvarian styling to rot13 english diff --git a/maplestation_modules/code/modules/client/preferences/languages.dm b/maplestation_modules/code/modules/client/preferences/languages.dm index 1254cc2e0452..b14600bdb78d 100644 --- a/maplestation_modules/code/modules/client/preferences/languages.dm +++ b/maplestation_modules/code/modules/client/preferences/languages.dm @@ -1,191 +1,247 @@ // -- Language preference and UI. -/// Simple define to denote no language. -#define NO_LANGUAGE "No Language" - -/datum/preference/choiced/language - savefile_key = "bilingual_language" - -// Stores a typepath of a language, or "No language" when passed a null / invalid language. -/datum/preference/additional_language +// These defines are used in the UI be careful updating them. +#define ADD_SPOKEN_LANGUAGE "Add spoken language" +#define ADD_UNDERSTOOD_LANGUAGE "Add understood language" +#define REMOVE_SPOKEN_LANGUAGE "Remove spoken language" +#define REMOVE_UNDERSTOOD_LANGUAGE "Remove understood language" + +/datum/preferences/proc/migrate_quirks_to_language_menu(list/save_data) + var/datum/preference_middleware/language/update = locate() in middleware + var/datum/preference/languages/language_pref = GLOB.preference_entries[/datum/preference/languages] + + // random quirks + if("Bilingual" in all_quirks) + var/picked_lang = GLOB.language_types_by_name[save_data["bilingual_language"]]?.type + if(picked_lang && (picked_lang in language_pref.selectable_languages)) + update.add_language_to_user(picked_lang, ADD_SPOKEN_LANGUAGE) + update.add_language_to_user(picked_lang, ADD_UNDERSTOOD_LANGUAGE) + + if("Trilingual" in all_quirks) + pass() // nothing to do about this + + if("Foreigner" in all_quirks) + update.add_language_to_user(/datum/language/uncommon, ADD_SPOKEN_LANGUAGE) + update.add_language_to_user(/datum/language/uncommon, ADD_UNDERSTOOD_LANGUAGE) + update.add_language_to_user(/datum/language/common, REMOVE_SPOKEN_LANGUAGE) + update.add_language_to_user(/datum/language/common, REMOVE_UNDERSTOOD_LANGUAGE) + + // the old prefs + var/other_lang = text2path(save_data["language"]) + if(other_lang && (other_lang in language_pref.selectable_languages)) + update.add_language_to_user(other_lang, ADD_SPOKEN_LANGUAGE) + update.add_language_to_user(other_lang, ADD_UNDERSTOOD_LANGUAGE) + +/datum/preference/languages savefile_key = "language" savefile_identifier = PREFERENCE_CHARACTER priority = PREFERENCE_PRIORITY_NAMES // needs to happen after species, so name works can_randomize = FALSE -/datum/preference/additional_language/deserialize(input, datum/preferences/preferences) - if(input == NO_LANGUAGE) - return NO_LANGUAGE - if("Trilingual" in preferences.all_quirks) - return NO_LANGUAGE - if("Bilingual" in preferences.all_quirks) - return NO_LANGUAGE - - var/datum/language/lang_to_add = check_input_path(input) - if(!ispath(lang_to_add, /datum/language)) - return NO_LANGUAGE - - var/datum/species/species = preferences.read_preference(/datum/preference/choiced/species) - var/banned = initial(lang_to_add.banned_from_species) - var/req = initial(lang_to_add.required_species) - if((banned && ispath(species, banned)) || (req && !ispath(species, req))) - return NO_LANGUAGE - - return lang_to_add - -/datum/preference/additional_language/serialize(input) - return check_input_path(input) || NO_LANGUAGE - -/datum/preference/additional_language/create_default_value() - return NO_LANGUAGE - -/datum/preference/additional_language/is_valid(value) - return !!check_input_path(value) - -/// Checks if our passed input is valid -/// Returns NO LANGUAGE if passed NO LANGUAGE (truthy value) -/// Returns null if the input was invalid (falsy value) -/// Returns a language typepath if the input was a valid path (truthy value) -/datum/preference/additional_language/proc/check_input_path(input) - if(input == NO_LANGUAGE) - return NO_LANGUAGE - - var/path_form = istext(input) ? text2path(input) : input - // sometimes we deserialize with a text string that is a path, as they're saved as string in our json save - // other times we recieve a full typepath, likely from write preference - // we need to support either case just to be inclusive, so here we are var/path_form = istext(input) ? text2path(input) : input - if(!ispath(path_form, /datum/language)) - return null + /// List of languages you can pick. + /// You only need to add languagues here that are not spoken by selectable roundstart species. + var/list/selectable_languages = list( + /datum/language/common, + /datum/language/impdraconic, + /datum/language/isatoa, + /datum/language/piratespeak, + /datum/language/shadowtongue, + /datum/language/uncommon, + /datum/language/yangyu, + // these should be auto filled + /datum/language/moffic, + /datum/language/nekomimetic, + /datum/language/draconic, + /datum/language/skrell, + // these are iffy + /datum/language/voltaic, + /datum/language/calcic, + ) + /// Languages not rendered in the UI under any circumstances. + var/list/dont_show_languages = list( + /datum/language/aphasia, + /datum/language/codespeak, + /datum/language/drone, + /datum/language/xenocommon, + ) + /// Max # of languages you can add to your character. + var/max_spoken_languages = 1 + /// Max # of languages you can understand. + var/max_understood_languages = 2 - var/datum/language/lang_instance = GLOB.language_datum_instances[path_form] - // MAYBE accessed when language datums aren't created so this is just a sanity check - if(istype(lang_instance) && !lang_instance.available_as_pref) +/datum/preference/languages/create_default_value() + return null + +/datum/preference/languages/deserialize(list/input, datum/preferences/preferences) + if(!islist(input)) return null - return path_form + var/datum/species/species = GLOB.species_prototypes[preferences.read_preference(/datum/preference/choiced/species)] + var/datum/language_holder/species_holder = GLOB.prototype_language_holders[species.species_language_holder] + var/list/sanitized_input = list() + for(var/key in input) + for(var/lang_text in input[key]) + var/lang = istext(lang_text) ? text2path(lang_text) : lang_text + if(!ispath(lang, /datum/language)) + continue + switch(key) + if(ADD_SPOKEN_LANGUAGE) + if(!(lang in selectable_languages)) + continue + if(lang in species_holder.spoken_languages) + continue + if(length(sanitized_input[key]) >= max_spoken_languages) + continue + + if(ADD_UNDERSTOOD_LANGUAGE) + if(!(lang in selectable_languages)) + continue + if(lang in species_holder.understood_languages) + continue + if(length(sanitized_input[key]) >= max_understood_languages) + continue + + if(REMOVE_SPOKEN_LANGUAGE) + if(!(lang in species_holder.spoken_languages)) + continue + + if(REMOVE_UNDERSTOOD_LANGUAGE) + if(!(lang in species_holder.understood_languages)) + continue + + LAZYADD(sanitized_input[key], lang) + + return sanitized_input + +/datum/preference/languages/is_valid(value) + return islist(value) || isnull(value) + +/datum/preference/languages/apply_to_human(mob/living/carbon/human/target, value) + if(isdummy(target) || !islist(value)) + return + + // this needs to be delayed because it's tied to the mind (and we probably don't have that created yet) + if(target.mind) + add_mind_languages(target, value) + else + RegisterSignals(target, list(COMSIG_MOB_MIND_TRANSFERRED_INTO, COMSIG_MOB_MIND_INITIALIZED), PROC_REF(comsig_add_mind_languages), TRUE) + // this is fine to do now, though + remove_species_languages(target, value) -/datum/preference/additional_language/apply_to_human(mob/living/carbon/human/target, value) - if(value == NO_LANGUAGE) +/datum/preference/languages/proc/comsig_add_mind_languages(mob/living/carbon/human/target) + SIGNAL_HANDLER + + if(!target.client) return - target.grant_language(value, ALL, LANGUAGE_PREF) + UnregisterSignal(target, list(COMSIG_MOB_MIND_TRANSFERRED_INTO, COMSIG_MOB_MIND_INITIALIZED)) + add_mind_languages(target, target.client.prefs.read_preference(/datum/preference/languages)) -/datum/language - // Vars used in determining valid languages for the language preferences. - /// Whether this language is available as a pref. - var/available_as_pref = FALSE - /// The 'base species' of the language, the lizard to the draconic. - /// Players cannot select this language in the preferences menu if they already have this species set. - var/datum/species/banned_from_species - /// The 'required species' of the language, languages that require you be a certain species to know. - /// Players cannot select this language in the preferences menu if they do not have this species set. - var/datum/species/required_species +/datum/preference/languages/proc/add_mind_languages(mob/living/carbon/human/target, list/value = list()) + if(QDELETED(target)) + return + for(var/lang in value[ADD_SPOKEN_LANGUAGE]) + target.grant_language(lang, SPOKEN_LANGUAGE, LANGUAGE_MIND) + for(var/lang in value[ADD_UNDERSTOOD_LANGUAGE]) + target.grant_language(lang, UNDERSTOOD_LANGUAGE, LANGUAGE_MIND) -/datum/language/skrell - available_as_pref = TRUE - banned_from_species = /datum/species/skrell +/datum/preference/languages/proc/remove_species_languages(mob/living/carbon/human/target, list/value = list()) + if(QDELETED(target)) + return + for(var/lang in value[REMOVE_SPOKEN_LANGUAGE]) + target.remove_language(lang, SPOKEN_LANGUAGE, LANGUAGE_SPECIES) + for(var/lang in value[REMOVE_UNDERSTOOD_LANGUAGE]) + target.remove_language(lang, UNDERSTOOD_LANGUAGE, LANGUAGE_SPECIES) -/datum/language/draconic - available_as_pref = TRUE - banned_from_species = /datum/species/lizard +/datum/preference_middleware/language + action_delegations = list( + "set_language" = PROC_REF(set_language), + ) -/datum/language/impdraconic - available_as_pref = TRUE - banned_from_species = /datum/species/lizard/silverscale // already know it (though this check should be deharcoded) - required_species = /datum/species/lizard +/datum/preference_middleware/language/proc/add_language_to_user(lang_type, lang_key) + var/datum/preference/languages/language_pref = GLOB.preference_entries[/datum/preference/languages] + var/list/existing = preferences.read_preference(/datum/preference/languages) || list() -/datum/language/nekomimetic - available_as_pref = TRUE - banned_from_species = /datum/species/human/felinid + if(lang_key == ADD_SPOKEN_LANGUAGE && length(existing[ADD_SPOKEN_LANGUAGE]) >= language_pref.max_spoken_languages) + return FALSE + if(lang_key == ADD_UNDERSTOOD_LANGUAGE && length(existing[ADD_UNDERSTOOD_LANGUAGE]) >= language_pref.max_understood_languages) + return FALSE -/datum/language/moffic - available_as_pref = TRUE - banned_from_species = /datum/species/moth + if((lang_key == ADD_SPOKEN_LANGUAGE || lang_key == ADD_UNDERSTOOD_LANGUAGE) && !(lang_type in language_pref.selectable_languages)) + return FALSE -/datum/language/uncommon - available_as_pref = TRUE + LAZYADD(existing[lang_key], lang_type) -/datum/language/piratespeak - available_as_pref = TRUE + preferences.write_preference(GLOB.preference_entries[/datum/preference/languages], existing) -/datum/language/yangyu - available_as_pref = TRUE - banned_from_species = /datum/species/ornithid + return TRUE -/datum/language/shadowtongue - available_as_pref = TRUE +/datum/preference_middleware/language/proc/remove_language_from_user(lang_type, lang_key) + var/list/existing = preferences.read_preference(/datum/preference/languages) || list() -/datum/preference_middleware/language - action_delegations = list( - "set_language" = PROC_REF(set_language), - ) + LAZYREMOVE(existing[lang_key], lang_type) + + preferences.write_preference(GLOB.preference_entries[/datum/preference/languages], existing) + + return TRUE /datum/preference_middleware/language/proc/set_language(list/params, mob/user) - var/datum/preference/additional_language/language_pref = GLOB.preference_entries[/datum/preference/additional_language] if(params["deselecting"]) - preferences.update_preference(language_pref, NO_LANGUAGE) + remove_language_from_user(text2path(params["lang_type"]), params["lang_key"]) return TRUE var/lang_path = text2path(params["lang_type"]) - var/datum/species/current_species = preferences.read_preference(/datum/preference/choiced/species) - var/datum/language/lang_to_add = GLOB.language_datum_instances[lang_path] - if(!istype(lang_to_add)) - to_chat(user, span_warning("Invalid language.")) - return TRUE - if(!lang_to_add.available_as_pref) - to_chat(user, span_warning("That language is not available.")) - return TRUE - // Sanity checking - Buttons are disabled in UI but you can never rely on that - if(lang_to_add.banned_from_species && ispath(current_species, lang_to_add.banned_from_species)) - to_chat(user, span_warning("Invalid language for current species.")) - return TRUE - if(lang_to_add.required_species && !ispath(current_species, lang_to_add.required_species)) - to_chat(user, span_warning("Language requires another species.")) - return TRUE - - preferences.update_preference(language_pref, lang_path) + if(GLOB.language_datum_instances[lang_path]) + add_language_to_user(lang_path, params["lang_key"]) return TRUE +/datum/preference_middleware/language/on_new_character(mob/user) + preferences.update_static_data(user) + +/datum/preference_middleware/language/get_ui_static_data(mob/user) + var/list/data = list() + + var/datum/species/species = GLOB.species_prototypes[preferences.read_preference(/datum/preference/choiced/species)] + var/datum/language_holder/species_holder = GLOB.prototype_language_holders[species.species_language_holder] + data["spoken_languages"] = assoc_to_keys(species_holder.spoken_languages) + data["understood_languages"] = assoc_to_keys(species_holder.understood_languages) + data["partial_languages"] = species_holder.get_mutually_understood_languages() + + return data + /datum/preference_middleware/language/get_ui_data(mob/user) var/list/data = list() - data["selected_lang"] = preferences.read_preference(/datum/preference/additional_language) - data["selected_species"] = preferences.read_preference(/datum/preference/choiced/species) - data["pref_name"] = preferences.read_preference(/datum/preference/name/real_name) - data["trilingual"] = ("Trilingual" in preferences.all_quirks) - data["bilingual"] = ("Bilingual" in preferences.all_quirks) + var/list/selected_languages = preferences.read_preference(/datum/preference/languages) + data["pref_spoken_languages"] = selected_languages?[ADD_SPOKEN_LANGUAGE] || list() + data["pref_understood_languages"] = selected_languages?[ADD_UNDERSTOOD_LANGUAGE] || list() + data["pref_unspoken_languages"] = selected_languages?[REMOVE_SPOKEN_LANGUAGE] || list() + data["pref_ununderstood_languages"] = selected_languages?[REMOVE_UNDERSTOOD_LANGUAGE] || list() return data /datum/preference_middleware/language/get_constant_data() var/list/data = list() - var/list/base_languages = list() - var/list/bonus_languages = list() + + var/datum/preference/languages/language_pref = GLOB.preference_entries[/datum/preference/languages] + + data["base_languages"] = list() for(var/found_language in GLOB.language_datum_instances) - var/datum/language/found_instance = GLOB.language_datum_instances[found_language] - if(!found_instance.available_as_pref) + if(found_language in language_pref.dont_show_languages) continue - var/list/lang_data = list() - lang_data["name"] = found_instance.name lang_data["type"] = found_language + lang_data["unlocked"] = (found_language in language_pref.selectable_languages) + lang_data["name"] = GLOB.language_datum_instances[found_language].name + lang_data["desc"] = GLOB.language_datum_instances[found_language].desc + UNTYPED_LIST_ADD(data["base_languages"], lang_data) + + data["max_spoken_languages"] = language_pref.max_spoken_languages + data["max_understood_languages"] = language_pref.max_understood_languages - var/datum/species/banned_species = found_instance.banned_from_species - if(banned_species) - lang_data["incompatible_with"] = list("name" = initial(banned_species.name), "type" = banned_species) - var/datum/species/required_species = found_instance.required_species - if(required_species) - lang_data["requires"] = list("name" = initial(required_species.name), "type" = required_species) - - // Having a required species makes it a bonus language, otherwise it's a base language - if(found_instance.required_species) - UNTYPED_LIST_ADD(bonus_languages, lang_data) - else - UNTYPED_LIST_ADD(base_languages, lang_data) - - data["base_languages"] = base_languages - data["bonus_languages"] = bonus_languages - data["blacklisted_species"] = list() return data -#undef NO_LANGUAGE +#undef ADD_SPOKEN_LANGUAGE +#undef ADD_UNDERSTOOD_LANGUAGE +#undef REMOVE_SPOKEN_LANGUAGE +#undef REMOVE_UNDERSTOOD_LANGUAGE diff --git a/maplestation_modules/code/modules/language/highdraconic.dm b/maplestation_modules/code/modules/language/highdraconic.dm index b671914981b3..6c1e2a6cb57a 100644 --- a/maplestation_modules/code/modules/language/highdraconic.dm +++ b/maplestation_modules/code/modules/language/highdraconic.dm @@ -17,18 +17,18 @@ icon_state = "lizardred" default_priority = 85 -// So I wrote a few unit tests for /tg/ that rely on Lizards not knowing what high draconic is. -// And since rewriting them is out of the questions, Lizards don't know high draconic in unit tests. -#ifndef UNIT_TESTS + mutual_understanding = list( + /datum/language/draconic = 66, + ) -// Edit to the base lizard language holder - lizards can understand high draconic. -/datum/language_holder/lizard - understood_languages = list( - /datum/language/common = list(LANGUAGE_ATOM), - /datum/language/draconic = list(LANGUAGE_ATOM), - /datum/language/impdraconic = list(LANGUAGE_ATOM), +/datum/language/draconic + mutual_understanding = list( + /datum/language/impdraconic = 66, ) +// TG unit test compliance (out of laziness) +#ifndef UNIT_TESTS + // Edit to the silverscale language holder - silverscales can speak high draconic. /datum/language_holder/lizard/silver understood_languages = list( diff --git a/maplestation_modules/code/modules/language/isatoan.dm b/maplestation_modules/code/modules/language/isatoan.dm index 5d55fde3b55c..63e57ae91d7e 100644 --- a/maplestation_modules/code/modules/language/isatoan.dm +++ b/maplestation_modules/code/modules/language/isatoan.dm @@ -15,7 +15,6 @@ icon_state = "mu" icon = 'maplestation_modules/icons/misc/language.dmi' default_priority = 80 - available_as_pref = TRUE /datum/language_holder/isatoa understood_languages = list( diff --git a/maplestation_modules/code/modules/language/japanese.dm b/maplestation_modules/code/modules/language/japanese.dm index bb633e4660ec..b47a0d3baaa7 100644 --- a/maplestation_modules/code/modules/language/japanese.dm +++ b/maplestation_modules/code/modules/language/japanese.dm @@ -13,7 +13,7 @@ ) icon_state = "torii" icon = 'maplestation_modules/icons/misc/language.dmi' - default_priority = 94 + default_priority = 90 /datum/language_holder/yangyu understood_languages = list( diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/_LanguagePicker.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/_LanguagePicker.tsx index bdfdca673d21..c8e34294f3a8 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/_LanguagePicker.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/_LanguagePicker.tsx @@ -4,164 +4,396 @@ import { useBackend } from '../../backend'; import { Box, Button, - Dimmer, + Flex, NoticeBox, Section, Stack, + Tooltip, } from '../../components'; import { ServerPreferencesFetcher } from './ServerPreferencesFetcher'; type typePath = string; +type languagePath = typePath; + type Data = { pref_name: string; - selected_species: typePath; - selected_lang: typePath | string; - trilingual: BooleanLike; - bilingual: BooleanLike; + spoken_languages: languagePath[]; + understood_languages: languagePath[]; + partial_languages: Record[]; + pref_spoken_languages: languagePath[]; + pref_understood_languages: languagePath[]; + pref_unspoken_languages: languagePath[]; + pref_ununderstood_languages: languagePath[]; }; -type Species = { +export type Language = { name: string; - type: typePath; + desc: string; + type: languagePath; + unlocked: BooleanLike; }; -export type Language = { - name: string; - type: typePath; - incompatible_with: Species | null; - requires: Species | null; +enum LanguageState { + DEFAULT, + DISABLED, + ENABLED, + NONE, +} + +const StateToIcon = { + [LanguageState.DEFAULT]: 'square-check-o', + [LanguageState.DISABLED]: 'square-minus-o', + [LanguageState.ENABLED]: 'square-plus-o', + [LanguageState.NONE]: 'square-o', +}; + +const StateToColor = { + [LanguageState.DEFAULT]: 'good', + [LanguageState.DISABLED]: 'bad', + [LanguageState.ENABLED]: 'average', + [LanguageState.NONE]: 'default', +}; + +const StateToTooltipSpeech = { + [LanguageState.DEFAULT]: 'You innately speak this.', + [LanguageState.DISABLED]: 'You have opted out of speaking this.', + [LanguageState.ENABLED]: 'You have chosen to speak this.', + [LanguageState.NONE]: 'You do not speak this.', +}; + +const StateToTooltipUnderstanding = { + [LanguageState.DEFAULT]: 'You innately understand this.', + [LanguageState.DISABLED]: 'You have opted out of understanding this.', + [LanguageState.ENABLED]: 'You have chosen to understand this.', + [LanguageState.NONE]: 'You do not understand this.', +}; + +const get_spoken_language_state = ( + langtype: languagePath, + data: Data, +): LanguageState => { + if (data.pref_unspoken_languages.includes(langtype)) { + return LanguageState.DISABLED; + } + if (data.pref_spoken_languages.includes(langtype)) { + return LanguageState.ENABLED; + } + if (data.spoken_languages.includes(langtype)) { + return LanguageState.DEFAULT; + } + return LanguageState.NONE; }; -// Fake an ispath() check to determine if this species can learn this language -const isPickable = (lang: Language, species: typePath): boolean => { - if (lang.incompatible_with && species.includes(lang.incompatible_with.type)) { - return false; +const get_understood_language_state = ( + langtype: languagePath, + data: Data, +): LanguageState => { + if (data.pref_ununderstood_languages.includes(langtype)) { + return LanguageState.DISABLED; + } + if (data.pref_understood_languages.includes(langtype)) { + return LanguageState.ENABLED; + } + if (data.understood_languages.includes(langtype)) { + return LanguageState.DEFAULT; } - if (lang.requires && !species.includes(lang.requires.type)) { - return false; + return LanguageState.NONE; +}; + +// Returns the keys for the spoken language button action based on the given language and data +const get_spoken_button_keys = (langtype: languagePath, data: Data) => { + if (data.spoken_languages.includes(langtype)) { + return { + lang_key: 'Remove spoken language', // Corresponds to DM defines + deselecting: data.pref_unspoken_languages.includes(langtype), + }; } - return true; + return { + lang_key: 'Add spoken language', // Corresponds to DM defines + deselecting: data.pref_spoken_languages.includes(langtype), + }; }; -const getLanguageTooltip = (lang: Language): string => { - if (lang.incompatible_with && lang.requires) { - return `This language cannot be selected by - the "${lang.incompatible_with.name}" species and requires - the "${lang.requires.name}" species.`; +// Returns the keys for the understood language button action based on the given language and data +const get_understood_button_keys = (langtype: languagePath, data: Data) => { + if (data.understood_languages.includes(langtype)) { + return { + lang_key: 'Remove understood language', // Corresponds to DM defines + deselecting: data.pref_ununderstood_languages.includes(langtype), + }; } - if (lang.incompatible_with) { - return `This language cannot be selected by - the "${lang.incompatible_with.name}" species.`; + return { + lang_key: 'Add understood language', // Corresponds to DM defines + deselecting: data.pref_understood_languages.includes(langtype), + }; +}; + +const partial_understanding_percent = (langtype: languagePath, data: Data) => { + if (!data.partial_languages[langtype]) { + return null; } - if (lang.requires) { - return `This language requires - the "${lang.requires.name}" species.`; + const all_understood_combined = data.understood_languages + .concat(data.pref_understood_languages) + .filter((item) => !data.pref_ununderstood_languages.includes(item)); + if (all_understood_combined.includes(langtype)) { + return ( + + + {`${data.partial_languages[langtype]}%`} + + + ); } - return ''; + return {data.partial_languages[langtype]}%; }; -const LanguageStack = (props: { - language: Language; - selected_lang: typePath; - selected_species: typePath; +const LanguageRow = (props: { + displayed_language: Language; + spoken_cap: number; + understood_cap: number; }) => { - const { act } = useBackend(); - const { language, selected_species } = props; - const { name, type } = language; - const pickable = isPickable(language, selected_species); + const { act, data } = useBackend(); + const { displayed_language, spoken_cap, understood_cap } = props; + const { + spoken_languages, + pref_spoken_languages, + pref_unspoken_languages, + understood_languages, + pref_understood_languages, + pref_ununderstood_languages, + } = data; + + const lang_type = displayed_language.type; + + const spoken_state = get_spoken_language_state(lang_type, data); + const understood_state = get_understood_language_state(lang_type, data); + const ignore_spoken_cap = spoken_languages + .concat(pref_spoken_languages) + .concat(pref_unspoken_languages) + .includes(lang_type); + + const ignore_undersood_cap = understood_languages + .concat(pref_understood_languages) + .concat(pref_ununderstood_languages) + .includes(lang_type); + + // name - spoken - understood - partial understanding percent return ( - - - {name} - - - + + {displayed_language.desc ? ( + + + {displayed_language.name} + + + ) : ( + {displayed_language.name} + )} + + +