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/objects/effects/decals/cleanable.dm b/code/game/objects/effects/decals/cleanable.dm index c00ad525b181..19d437443c7a 100644 --- a/code/game/objects/effects/decals/cleanable.dm +++ b/code/game/objects/effects/decals/cleanable.dm @@ -78,23 +78,12 @@ return FALSE bloodiness = clamp((bloodiness + by_amount), 0, BLOOD_POOL_MAX) - update_appearance() return TRUE /// Called before attempting to scoop up reagents from this decal to only load reagents when necessary /obj/effect/decal/cleanable/proc/lazy_init_reagents() return -#ifdef TESTING -/obj/effect/decal/cleanable/update_overlays() - . = ..() - if(bloodiness) - var/mutable_appearance/blah_text = new() - blah_text.maptext = MAPTEXT_TINY_UNICODE("[bloodiness]") - blah_text.appearance_flags |= (KEEP_APART|RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM) - . += blah_text -#endif - /obj/effect/decal/cleanable/attackby(obj/item/W, mob/user, params) if((istype(W, /obj/item/reagent_containers/cup) && !istype(W, /obj/item/reagent_containers/cup/rag)) || istype(W, /obj/item/reagent_containers/cup/glass)) if(src.reagents && W.reagents) diff --git a/code/game/objects/effects/decals/cleanable/humans.dm b/code/game/objects/effects/decals/cleanable/humans.dm index 79144056e6ad..1375c8400e60 100644 --- a/code/game/objects/effects/decals/cleanable/humans.dm +++ b/code/game/objects/effects/decals/cleanable/humans.dm @@ -11,6 +11,7 @@ decal_reagent = /datum/reagent/blood bloodiness = BLOOD_AMOUNT_PER_DECAL color = COLOR_BLOOD + flags_1 = UNPAINTABLE_1 /// Can this blood dry out? var/can_dry = TRUE /// Is this blood dried out? @@ -29,21 +30,17 @@ /// How long it takes to dry out var/drying_time = 5 MINUTES /// The process to drying out, recorded in deciseconds - var/drying_progress = 0 - /// Color matrix applied to dried blood via filter to make it look dried - var/static/list/blood_dry_filter_matrix = list( - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1, - -0.5, -0.75, -0.75, 0, - ) + VAR_FINAL/drying_progress = 0 /obj/effect/decal/cleanable/blood/Initialize(mapload) . = ..() - START_PROCESSING(SSblood_drying, src) - if(color && can_dry && !dried) - update_blood_drying_effect() + if(mapload) + add_blood_DNA(list("UNKNOWN DNA" = random_human_blood_type())) + if(dried) + dry() + else if(can_dry) + START_PROCESSING(SSblood_drying, src) + // update_atom_colour() // this is already called by parent via add_atom_colour /obj/effect/decal/cleanable/blood/Destroy() STOP_PROCESSING(SSblood_drying, src) @@ -54,29 +51,56 @@ return return ..() -#define DRY_FILTER_KEY "dry_effect" - -/obj/effect/decal/cleanable/blood/proc/update_blood_drying_effect() - if(!can_dry) - remove_filter(DRY_FILTER_KEY) // I GUESS - return - - var/existing_filter = get_filter(DRY_FILTER_KEY) - if(dried) - if(existing_filter) - animate(existing_filter) // just stop existing animations and force it to the end state - return - add_filter(DRY_FILTER_KEY, 2, color_matrix_filter(blood_dry_filter_matrix)) +// When color changes we need to update the drying animation +/obj/effect/decal/cleanable/blood/update_atom_colour() + . = ..() + // get a default color based on DNA if it ends up unset somehow + color ||= (GET_ATOM_BLOOD_DNA_LENGTH(src) ? get_blood_dna_color() : COLOR_BLOOD) + // stop existing drying animations + animate(src) + // ok let's make the dry color now + // we will manually calculate what the resulting color should be when it dries + // we do this instead of using something like a color matrix because byond moment + // (at any given moment, there may be like... 200 blood decals on your screen at once + // byond is, apparently, pretty bad at handling that many color matrix operations, + // especially in a filter or while animating) + var/list/starting_color_rgb = ReadRGB(color) || list(255, 255, 255, alpha) + // we want a fixed offset for a fixed drop in color intensity, plus a scaling offset based on our strongest color + // the scaling offset helps keep dark colors from turning black, while also ensurse bright colors don't stay super bright + var/max_color = max(starting_color_rgb[1], starting_color_rgb[2], starting_color_rgb[3]) + var/red_offset = 50 + (75 * (starting_color_rgb[1] / max_color)) + var/green_offset = 50 + (75 * (starting_color_rgb[2] / max_color)) + var/blue_offset = 50 + (75 * (starting_color_rgb[3] / max_color)) + // if the color is already decently dark, we should reduce the offsets even further + // this is intended to prevent already dark blood (mixed blood in particular) from becoming full black + var/strength = starting_color_rgb[1] + starting_color_rgb[2] + starting_color_rgb[3] + if(strength <= 192) + red_offset *= 0.5 + green_offset *= 0.5 + blue_offset *= 0.5 + // finally, get this show on the road + var/dried_color = rgb( + clamp(starting_color_rgb[1] - red_offset, 0, 255), + clamp(starting_color_rgb[2] - green_offset, 0, 255), + clamp(starting_color_rgb[3] - blue_offset, 0, 255), + length(starting_color_rgb) >= 4 ? starting_color_rgb[4] : alpha, // maintain alpha! (if it has it) + ) + // if it's dried (or about to dry) we can just set color directly + if(dried || drying_progress >= drying_time) + color = dried_color return - - if(existing_filter) - remove_filter(DRY_FILTER_KEY) - - add_filter(DRY_FILTER_KEY, 2, color_matrix_filter()) - transition_filter(DRY_FILTER_KEY, color_matrix_filter(blood_dry_filter_matrix), drying_time - drying_progress) - -#undef DRY_FILTER_KEY - + // otherwise set the color to what it should be at the current drying progress, then animate down to the dried color if we can + color = gradient(0, color, 1, dried_color, round(drying_progress / drying_time, 0.01)) + if(can_dry) + animate(src, time = drying_time - drying_progress, color = dried_color) + +/// Slows down the drying time by a given amount, +/// then updates the effect, meaning the animation will slow down +/obj/effect/decal/cleanable/blood/proc/slow_dry(by_amount) + drying_progress -= by_amount + update_atom_colour() + +/// Returns a string of all the blood reagents in the blood /obj/effect/decal/cleanable/blood/proc/get_blood_string() var/list/all_dna = GET_ATOM_BLOOD_DNA(src) var/list/all_blood_names = list() @@ -91,7 +115,8 @@ adjust_bloodiness(-0.4 * BLOOD_PER_UNIT_MODIFIER * seconds_per_tick) drying_progress += (seconds_per_tick * 1 SECONDS) - if(drying_progress >= drying_time + SSblood_drying.wait) // Do it next tick when we're done + // Finish it next tick when we're all done + if(drying_progress >= drying_time + SSblood_drying.wait) dry() /obj/effect/decal/cleanable/blood/update_name(updates) @@ -121,7 +146,7 @@ dried = TRUE reagents?.clear_reagents() update_appearance() - update_blood_drying_effect() + update_atom_colour() STOP_PROCESSING(SSblood_drying, src) return TRUE @@ -145,17 +170,12 @@ . = ..() merger.add_blood_DNA(GET_ATOM_BLOOD_DNA(src)) merger.adjust_bloodiness(bloodiness) - merger.drying_progress -= (bloodiness * BLOOD_PER_UNIT_MODIFIER) // goes negative = takes longer to dry - merger.update_blood_drying_effect() + merger.slow_dry(1 SECONDS * bloodiness * BLOOD_PER_UNIT_MODIFIER) /obj/effect/decal/cleanable/blood/old bloodiness = 0 - icon_state = "floor1-old" - -/obj/effect/decal/cleanable/blood/old/Initialize(mapload, list/datum/disease/diseases) - add_blood_DNA(list("UNKNOWN DNA" = random_human_blood_type())) - . = ..() - dry() + dried = TRUE + icon_state = "floor1-old" // just for mappers. overrided in init /obj/effect/decal/cleanable/blood/splatter icon_state = "gibbl1" @@ -297,17 +317,16 @@ /obj/effect/decal/cleanable/blood/gibs/old name = "old rotting gibs" desc = "Space Jesus, why didn't anyone clean this up? They smell terrible." - icon_state = "gib1-old" + icon_state = "gib1-old" // just for mappers. overrided in init bloodiness = 0 + dried = TRUE dry_prefix = "" dry_desc = "" /obj/effect/decal/cleanable/blood/gibs/old/Initialize(mapload, list/datum/disease/diseases) - add_blood_DNA(list("UNKNOWN DNA" = random_human_blood_type())) . = ..() setDir(pick(GLOB.cardinals)) AddElement(/datum/element/swabable, CELL_LINE_TABLE_SLUDGE, CELL_VIRUS_TABLE_GENERIC, rand(2,4), 10) - dry() /obj/effect/decal/cleanable/blood/drip name = "drips of blood" @@ -538,3 +557,21 @@ the_window.vis_contents += final_splatter the_window.bloodied = TRUE expire() + +/// Subtype which has random DNA baked in OUTSIDE of mapload. +/// For testing, mapping, or badmins +/obj/effect/decal/cleanable/blood/pre_dna + var/list/dna_types = list("UNKNOWN DNA A" = /datum/blood_type/crew/human/a_minus) + +/obj/effect/decal/cleanable/blood/pre_dna/Initialize(mapload) + . = ..() + add_blood_DNA(dna_types) + +/obj/effect/decal/cleanable/blood/pre_dna/lizard + dna_types = list("UNKNOWN DNA A" = /datum/blood_type/crew/lizard) + +/obj/effect/decal/cleanable/blood/pre_dna/lizhuman + dna_types = list("UNKNOWN DNA A" = /datum/blood_type/crew/human/a_minus, "UNKNOWN DNA B" = /datum/blood_type/crew/lizard) + +/obj/effect/decal/cleanable/blood/pre_dna/ethereal + dna_types = list("UNKNOWN DNA A" = /datum/blood_type/crew/ethereal) 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/forensics/forensics_helpers.dm b/code/modules/forensics/forensics_helpers.dm index f11ddea8c4f2..c4008e9f21f3 100644 --- a/code/modules/forensics/forensics_helpers.dm +++ b/code/modules/forensics/forensics_helpers.dm @@ -77,6 +77,7 @@ /// Cached mixed color of all blood DNA on us VAR_PROTECTED/cached_blood_dna_color +/// Gets what color all the blood coating this atom mixed together would be /atom/proc/get_blood_dna_color() if(cached_blood_dna_color) return cached_blood_dna_color @@ -92,6 +93,9 @@ cached_blood_dna_color = final_color return final_color +/obj/effect/decal/cleanable/blood/get_blood_dna_color() + return ..() || COLOR_BLOOD + /obj/effect/decal/cleanable/blood/drip/get_blood_dna_color() var/list/all_dna = GET_ATOM_BLOOD_DNA(src) var/blood_type_to_use = all_dna[all_dna[1]] @@ -117,13 +121,14 @@ var/first_dna = GET_ATOM_BLOOD_DNA_LENGTH(src) if(!..()) return FALSE - - color = get_blood_dna_color() + if(dried) + return TRUE // Imperfect, ends up with some blood types being double-set-up, but harmless (for now) for(var/new_blood in blood_DNA_to_add) var/datum/blood_type/blood = find_blood_type(blood_DNA_to_add[new_blood]) blood.set_up_blood(src, first_dna == 0) update_appearance() + add_atom_colour(get_blood_dna_color(), FIXED_COLOUR_PRIORITY) return TRUE /obj/item/add_blood_DNA(list/blood_DNA_to_add) 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/maplestation_modules/code/modules/mob/living/blood.dm b/maplestation_modules/code/modules/mob/living/blood.dm index 62b31fb0cc4a..cef4c18ad402 100644 --- a/maplestation_modules/code/modules/mob/living/blood.dm +++ b/maplestation_modules/code/modules/mob/living/blood.dm @@ -49,12 +49,13 @@ * Blood Drying SS * * Used as a low priority backround system to handling the drying of blood on the ground + * (basically just handles reducing their bloodiness value over time) */ PROCESSING_SUBSYSTEM_DEF(blood_drying) name = "Blood Drying" flags = SS_NO_INIT | SS_BACKGROUND priority = 10 - wait = 10 SECONDS + wait = 4 SECONDS /** * Blood Types @@ -101,6 +102,9 @@ PROCESSING_SUBSYSTEM_DEF(blood_drying) /** * Used to handle any unique facets of blood spawned of this blood type * + * You don't need to worry about updating the icon of the decal, + * it will be handled automatically after setup is finished + * * Arguments * * blood - the blood being set up * * new_splat - whether this is a newly instantiated blood decal, or an existing one this blood is being added to @@ -132,10 +136,9 @@ PROCESSING_SUBSYSTEM_DEF(blood_drying) if(isnull(drop)) var/obj/effect/decal/cleanable/blood/splatter = locate() in blood_turf if(!QDELETED(splatter)) - splatter.adjust_bloodiness(new_blood) - splatter.drying_progress -= (new_blood * BLOOD_PER_UNIT_MODIFIER) - splatter.update_blood_drying_effect() splatter.add_mob_blood(bleeding) + splatter.adjust_bloodiness(new_blood) + splatter.slow_dry(1 SECONDS * new_blood * BLOOD_PER_UNIT_MODIFIER) return splatter drop = new(blood_turf, bleeding.get_static_viruses()) @@ -170,8 +173,7 @@ PROCESSING_SUBSYSTEM_DEF(blood_drying) return null else splatter.adjust_bloodiness(BLOOD_AMOUNT_PER_DECAL) - splatter.drying_progress -= (BLOOD_AMOUNT_PER_DECAL * BLOOD_PER_UNIT_MODIFIER) - splatter.update_blood_drying_effect() + splatter.slow_dry(1 SECONDS * BLOOD_AMOUNT_PER_DECAL * BLOOD_PER_UNIT_MODIFIER) splatter.add_mob_blood(bleeding) //give blood info to the blood decal. if(LAZYLEN(temp_blood_DNA)) splatter.add_blood_DNA(temp_blood_DNA) @@ -277,16 +279,20 @@ PROCESSING_SUBSYSTEM_DEF(blood_drying) /datum/blood_type/crew/lizard name = "L" color = "#047200" // Some species of lizards have mutated green blood due to biliverdin build up - compatible_types = list(/datum/blood_type/crew/lizard/silver) + compatible_types = list(/datum/blood_type/silver/lizard) -/datum/blood_type/crew/lizard/silver - color = "#ffffff9c" - compatible_types = list(/datum/blood_type/crew/lizard) +/datum/blood_type/silver + name = "Ag" + color = "#c9c9c99c" + reagent_type = /datum/reagent/silver -/datum/blood_type/crew/lizard/silver/set_up_blood(obj/effect/decal/cleanable/blood/blood, new_splat) - blood.add_filter("silver_glint", 3, list("type" = "outline", "color" = "#c9c9c99c", "size" = 1.5)) +/datum/blood_type/silver/set_up_blood(obj/effect/decal/cleanable/blood/blood, new_splat) + blood.can_dry = FALSE blood.emissive_alpha = max(blood.emissive_alpha, new_splat ? 125 : 63) - blood.update_appearance(UPDATE_OVERLAYS) + +/datum/blood_type/silver/lizard + name = "sL" + compatible_types = list(/datum/blood_type/crew/lizard) /datum/blood_type/crew/skrell name = "S" @@ -300,11 +306,9 @@ PROCESSING_SUBSYSTEM_DEF(blood_drying) /datum/blood_type/crew/ethereal/set_up_blood(obj/effect/decal/cleanable/blood/blood, new_splat) blood.emissive_alpha = max(blood.emissive_alpha, new_splat ? 188 : 125) - blood.update_appearance(UPDATE_OVERLAYS) if(!new_splat) return blood.can_dry = FALSE - blood.update_blood_drying_effect() RegisterSignals(blood, list(COMSIG_ATOM_ITEM_INTERACTION, COMSIG_ATOM_ITEM_INTERACTION_SECONDARY), PROC_REF(on_cleaned)) /datum/blood_type/crew/ethereal/proc/on_cleaned(obj/effect/decal/cleanable/source, mob/living/user, obj/item/tool, ...) @@ -337,7 +341,6 @@ PROCESSING_SUBSYSTEM_DEF(blood_drying) return // Oil blood will never dry and can be ignited with fire blood.can_dry = FALSE - blood.update_blood_drying_effect() blood.AddElement(/datum/element/easy_ignite) /// A universal blood type which accepts everything diff --git a/maplestation_modules/code/modules/mob/living/carbon/human/species_types/silverscale.dm b/maplestation_modules/code/modules/mob/living/carbon/human/species_types/silverscale.dm index a8d40a4fc539..16bb77f22cfe 100644 --- a/maplestation_modules/code/modules/mob/living/carbon/human/species_types/silverscale.dm +++ b/maplestation_modules/code/modules/mob/living/carbon/human/species_types/silverscale.dm @@ -82,7 +82,7 @@ he_who_was_blessed_with_silver.add_filter("silver_glint", 2, list("type" = "outline", "color" = "#ffffff63", "size" = 2)) he_who_was_blessed_with_silver.physiology?.damage_resistance += 10 - he_who_was_blessed_with_silver.dna.species.exotic_bloodtype = /datum/blood_type/crew/lizard/silver + he_who_was_blessed_with_silver.dna.species.exotic_bloodtype = /datum/blood_type/silver/lizard organ_owner.update_body(TRUE) /obj/item/organ/internal/tongue/lizard/silver/on_mob_remove(mob/living/carbon/organ_owner, special) diff --git a/maplestation_modules/story_content/localnode_equipment/localnode_core.dm b/maplestation_modules/story_content/localnode_equipment/localnode_core.dm index 206bbfd19e02..34f4d9d19818 100644 --- a/maplestation_modules/story_content/localnode_equipment/localnode_core.dm +++ b/maplestation_modules/story_content/localnode_equipment/localnode_core.dm @@ -6,7 +6,7 @@ /obj/item/localnode name = "LocalNode#4248" - desc = "It looks like a basketball sized blue orb, however it looks like it had bits broken off with a hammer a few times before covering it with superglue and rolling it in a box of computer parts" + desc = "A strange blue orb, humming with alien power and smelling strongly of ozone" icon = 'maplestation_modules/story_content/localnode_equipment/sprites/localnode.dmi' icon_state = "localnode" lefthand_file = 'maplestation_modules/story_content/localnode_equipment/sprites/localnode_inhand_lh.dmi' diff --git a/maplestation_modules/story_content/localnode_equipment/sprites/localnode.dmi b/maplestation_modules/story_content/localnode_equipment/sprites/localnode.dmi index 9702c29c532f..d576678b9131 100644 Binary files a/maplestation_modules/story_content/localnode_equipment/sprites/localnode.dmi and b/maplestation_modules/story_content/localnode_equipment/sprites/localnode.dmi differ diff --git a/maplestation_modules/story_content/localnode_equipment/sprites/localnode_inhand_lh.dmi b/maplestation_modules/story_content/localnode_equipment/sprites/localnode_inhand_lh.dmi index 572c930528a9..8be450991cde 100644 Binary files a/maplestation_modules/story_content/localnode_equipment/sprites/localnode_inhand_lh.dmi and b/maplestation_modules/story_content/localnode_equipment/sprites/localnode_inhand_lh.dmi differ diff --git a/maplestation_modules/story_content/localnode_equipment/sprites/localnode_inhand_rh.dmi b/maplestation_modules/story_content/localnode_equipment/sprites/localnode_inhand_rh.dmi index 6267b73a943d..0cf45c0f36fc 100644 Binary files a/maplestation_modules/story_content/localnode_equipment/sprites/localnode_inhand_rh.dmi and b/maplestation_modules/story_content/localnode_equipment/sprites/localnode_inhand_rh.dmi differ 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} + )} + + +