mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-10 17:52:36 +00:00
## About The Pull Request - Tweaks partial understanding. Paragraphs are now split into sentences first creating more natural breaks between sentences. - Adds "Common Second Language" quirk This quirk changes your default understanding of common (up to) 90% (your choice), meaning you drop the occasional word.   Additionally, when your sanity drops below a threshold, you become forced to speak your native language, albeit with a partial understanding applied for everyone else. Incompatible with similar language quirks + can't be taken by humans (yet?) ## Why It's Good For The Game Just a fun way to play around with the new "partial understanding" system. ## Changelog 🆑 Melbert add: "Common Second Language" quirk qol: Language translations chunk sentences together better, making partial understanding a bit easier to parse. /🆑
343 lines
14 KiB
Plaintext
343 lines
14 KiB
Plaintext
/// 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
|
|
/// Fluff name of language if any.
|
|
var/name = "an unknown language"
|
|
/// Short description for 'Check Languages'.
|
|
var/desc = "A language."
|
|
/// Character used to speak in language
|
|
/// If key is null, then the language isn't real or learnable.
|
|
var/key
|
|
/// Various language flags.
|
|
var/flags = NONE
|
|
/// Used when scrambling text for a non-speaker.
|
|
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 = 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.
|
|
* 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.
|
|
/// This is to be used for languages with very outlandish syllable lists (like pirates).
|
|
var/always_use_default_namelist = FALSE
|
|
/// Icon displayed in the chat window when speaking this language.
|
|
/// 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 = "unknown"
|
|
|
|
/// By default, random names picks this many names
|
|
var/default_name_count = 2
|
|
/// By default, random names picks this many syllables (min)
|
|
var/default_name_syllable_min = 2
|
|
/// By default, random names picks this many syllables (max)
|
|
var/default_name_syllable_max = 4
|
|
/// 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)
|
|
if((flags & LANGUAGE_HIDE_ICON_IF_UNDERSTOOD) && understands)
|
|
return FALSE
|
|
if((flags & LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD) && !understands)
|
|
return FALSE
|
|
return TRUE
|
|
|
|
/// Returns the icon to display in the chat window when speaking this language.
|
|
/datum/language/proc/get_icon()
|
|
var/datum/asset/spritesheet_batched/sheet = get_asset_datum(/datum/asset/spritesheet_batched/chat)
|
|
return sheet.icon_tag("language-[icon_state]")
|
|
|
|
/// Simple helper for getting a default firstname lastname
|
|
/datum/language/proc/default_name(gender = NEUTER)
|
|
if(gender != MALE && gender != FEMALE)
|
|
gender = pick(MALE, FEMALE)
|
|
if(gender == FEMALE)
|
|
return capitalize(pick(GLOB.first_names_female)) + " " + capitalize(pick(GLOB.last_names))
|
|
return capitalize(pick(GLOB.first_names_male)) + " " + capitalize(pick(GLOB.last_names))
|
|
|
|
|
|
/**
|
|
* Generates a random name this language would use.
|
|
*
|
|
* * gender: What gender to generate from, if neuter / plural coin flips between male and female
|
|
* * name_count: How many names to generate in, by default 2, for firstname lastname
|
|
* * syllable_count: How many syllables to generate in each name, min
|
|
* * syllable_max: How many syllables to generate in each name, max
|
|
* * force_use_syllables: If the name should be generated from the syllables list.
|
|
* Only used for subtypes which implement custom name lists. Also requires the language has syllables set.
|
|
*/
|
|
/datum/language/proc/get_random_name(
|
|
gender = NEUTER,
|
|
name_count = default_name_count,
|
|
syllable_min = default_name_syllable_min,
|
|
syllable_max = default_name_syllable_max,
|
|
force_use_syllables = FALSE,
|
|
)
|
|
if(gender != MALE && gender != FEMALE)
|
|
gender = pick(MALE, FEMALE)
|
|
if(!length(syllables) || always_use_default_namelist)
|
|
return default_name(gender)
|
|
|
|
var/list/full_name = list()
|
|
for(var/i in 1 to name_count)
|
|
var/new_name = ""
|
|
for(var/j in 1 to rand(default_name_syllable_min, default_name_syllable_max))
|
|
new_name += pick_weight_recursive(syllables)
|
|
full_name += capitalize(LOWER_TEXT(new_name))
|
|
|
|
return jointext(full_name, random_name_spacer)
|
|
|
|
/// Generates a random name, and attempts to ensure it is unique (IE, no other mob in the world has it)
|
|
/datum/language/proc/get_random_unique_name(...)
|
|
var/result = get_random_name(arglist(args))
|
|
for(var/i in 1 to 10)
|
|
if(!findname(result))
|
|
break
|
|
result = get_random_name(arglist(args))
|
|
|
|
return result
|
|
|
|
/// 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]
|
|
|
|
. = 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[lowertext_input] = scrambled_text
|
|
if(length(scramble_cache) > SCRAMBLE_CACHE_LEN)
|
|
scramble_cache.Cut(1, scramble_cache.len - SCRAMBLE_CACHE_LEN + 1)
|
|
|
|
/// 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)
|
|
|
|
/**
|
|
* Scramble a paragraph in this language.
|
|
*
|
|
* Takes into account any languages the hearer knows that has mutual understanding with this language.
|
|
*/
|
|
/datum/language/proc/scramble_paragraph(input, list/mutual_languages)
|
|
// perfect understanding, no need to scramble
|
|
if(mutual_languages?[type] >= 100)
|
|
return input
|
|
|
|
var/static/regex/first_sentence = regex(@"(.+?(?:[\.!\?]|$))", "g")
|
|
var/list/new_paragraph = list()
|
|
while(first_sentence.Find(input))
|
|
new_paragraph += scramble_sentence(trim(first_sentence.group[1]), mutual_languages)
|
|
return jointext(new_paragraph, " ")
|
|
|
|
/**
|
|
* 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(input)
|
|
if(cache?[cache_key])
|
|
return cache[cache_key]
|
|
|
|
// List of words that will be recombined into a sentence
|
|
var/list/scrambled_words = list()
|
|
// List which indexes correspond to words in scrambled_words, records whether the word was translated
|
|
// Can't be a single assoc list because duplicates are expected
|
|
var/list/translated_index = list()
|
|
for(var/word in splittext(input, " "))
|
|
var/translate_prob = mutual_languages?[type] || 0
|
|
var/base_word = strip_outer_punctuation(word)
|
|
if(translate_prob > 0)
|
|
// the probability of managing to understand a word is based on how common it is (+10%, -15%)
|
|
// 1000 words in the list, so words outside the list are just treated as "the 1250th most common word"
|
|
var/commonness = GLOB.most_common_words[LOWER_TEXT(base_word)] || 1250
|
|
translate_prob += (10 * (1 - (min(commonness, 1250) / 500)))
|
|
if(prob(translate_prob))
|
|
scrambled_words += word
|
|
translated_index += FALSE
|
|
continue
|
|
|
|
var/scrambled_word = scramble_word(base_word)
|
|
scrambled_words += scrambled_word
|
|
translated_index += (scrambled_word != base_word)
|
|
|
|
// start building the new sentence. first word is capitalized and otherwise untouched
|
|
var/sentence = capitalize(scrambled_words[1])
|
|
for(var/i in 2 to length(scrambled_words))
|
|
var/word = scrambled_words[i]
|
|
// this was not translated so just throw it in
|
|
if(!translated_index[i])
|
|
sentence += " [word]"
|
|
continue
|
|
// if the last word was scrambled, always include a space
|
|
if(translated_index[i - 1] || prob(between_word_space_chance))
|
|
sentence += " "
|
|
// lastly try inserting a new sentence
|
|
else if(prob(between_word_sentence_chance))
|
|
sentence += ". "
|
|
word = capitalize(word)
|
|
|
|
sentence += word
|
|
|
|
// scrambling the word will drop punctuation, so we need to re-add it at the end
|
|
// (however we don't need to do anything if the last word was not translated)
|
|
if(translated_index[length(scrambled_words)])
|
|
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))
|
|
word = stars(input)
|
|
|
|
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)
|
|
|
|
write_word_cache(input, word)
|
|
|
|
// If they're shouting, we're shouting
|
|
return (is_uppercase(input) && length_char(input) >= 2) ? uppertext(word) : word
|
|
|
|
#undef SCRAMBLE_CACHE_LEN
|