diff --git a/simulation_parameters/SimulationParameters.cs b/simulation_parameters/SimulationParameters.cs index 0d293702881..e69c76b62f5 100644 --- a/simulation_parameters/SimulationParameters.cs +++ b/simulation_parameters/SimulationParameters.cs @@ -73,6 +73,7 @@ public partial class SimulationParameters : Node public IAutoEvoConfiguration AutoEvoConfiguration => autoEvoConfiguration; + public SpeciesNameConfig SpeciesNameConfig { get; private set; } = null!; public NameGenerator NameGenerator { get; private set; } = null!; public PatchMapNameGenerator PatchMapNameGenerator { get; private set; } = null!; @@ -147,7 +148,9 @@ public override void _Ready() bioProcesses = LoadRegistry("res://simulation_parameters/microbe_stage/bio_processes.json"); meteors = LoadRegistry("res://simulation_parameters/microbe_stage/meteors.json"); - NameGenerator = LoadDirectObject("res://simulation_parameters/microbe_stage/species_names.json"); + SpeciesNameConfig = LoadDirectObject( + "res://simulation_parameters/microbe_stage/species_names.json"); + NameGenerator = new NameGenerator(SpeciesNameConfig); musicCategories = LoadRegistry("res://simulation_parameters/common/music_tracks.json"); @@ -826,7 +829,7 @@ private void CheckForInvalidValues() CheckRegistryType(visualResources); CheckRegistryType(stageResources); - NameGenerator.Check(string.Empty); + SpeciesNameConfig.Check(string.Empty); PatchMapNameGenerator.Check(string.Empty); autoEvoConfiguration.Check(string.Empty); autoEvoConfiguration.InternalName = AUTO_EVO_CONFIGURATION_NAME; @@ -908,7 +911,7 @@ private void ResolveValueRelationships() entry.Value.Resolve(this); } - NameGenerator.Resolve(this); + SpeciesNameConfig.Resolve(this); visualResourceByIdentifier = visualResources.ToDictionary(t => t.Value.VisualIdentifier, t => t.Value); stageResourcesByEnum = stageResources.ToDictionary(t => t.Value.Stage, t => t.Value); diff --git a/simulation_parameters/microbe_stage/species_names.json b/simulation_parameters/microbe_stage/species_names.json index 2b4f06ed546..d9859b1e7b2 100644 --- a/simulation_parameters/microbe_stage/species_names.json +++ b/simulation_parameters/microbe_stage/species_names.json @@ -1,40 +1,1325 @@ { "prefixCofix": [ - "Untrust", - "Kines", - "Atrox", - "Hhyyrylain", - "Whal", - "Crodn", - "Kraken", - "Creat", - "Narotiz", - "Minegam", - "Monti", - "Nick", - "Sporef", - "Dickinson", - "Wigglesworth", - "Glossin", - "Oliver", - "Irritat", - "Quantum", - "Crab", - "Oliv", - "River", - "Prim", - "Prem", - "Kapp", - "Vip", - "Viper", - "Spik", - "Vuper", - "Fuj", - "Thim", - "Maxonov", - "Kemik", - "Tak" + "untrust", + "kines", + "atrox", + "hhyyrylain", + "whal", + "crodn", + "kraken", + "creat", + "narotiz", + "minegam", + "monti", + "nick", + "sporef", + "dickinson", + "wigglesworth", + "glossin", + "oliver", + "irritat", + "quantum", + "crab", + "oliv", + "river", + "prim", + "prem", + "kapp", + "vip", + "viper", + "spik", + "vuper", + "fuj", + "thim", + "maxonov", + "kemik", + "tak" ], + "quantity": { + "1": [ + "mono", + "uni", + "eka" + ], + "2": [ + "bi", + "di" + ], + "3": [ + "tri" + ], + "4": [ + "tetra", + "quadri", + "quater", + "quadra", + "catur" + ], + "5": [ + "penta", + "quinti", + "quinque", + "panca" + ], + "6": [ + "hexa", + "sat", + "sexa" + ], + "7": [ + "septa", + "septi", + "septem", + "hepta" + ], + "8": [ + "octo" + ], + "9": [ + "nono", + "enna" + ], + "10": [ + "deca" + ], + "multiple": [ + "multi", + "pluri", + "poli", + "poly" + ] + }, + "qualityRoots": { + "new": [ + "neo" + ], + "first": [ + "proto", + "primi", + "archi" + ], + "old": [ + "paleo", + "archaeo" + ], + "slow": [ + "brady", + "tardi", + "manda" + ], + "fast": [ + "celeri", + "veloci", + "tachy", + "tachia", + "sighra", + "asu", + "tivra" + ], + "hot": [ + "calori", + "fervi", + "calidi", + "thermo", + "pyro", + "usna", + "tapa" + ], + "cold": [ + "frigi", + "gel", + "cryo", + "psychro", + "sita", + "hima" + ], + "big": [ + "magni", + "maxi", + "grand", + "macro", + "mega", + "giga", + "megalo", + "maha", + "brhat" + ], + "small": [ + "mini", + "parvi", + "micro", + "nano", + "alpa", + "suksma" + ], + "very": [ + "per", + "hyper", + "poly", + "ati", + "su" + ], + "little": [ + "pauci", + "sub", + "hypo", + "oligo", + "isat", + "kincit" + ], + "liking": [ + "ama", + "amo", + "philo", + "kama", + "priya", + "raga" + ], + "disliking": [ + "odi", + "miso" + ], + "mutation": [ + "trans", + "muta", + "meta", + "allo", + "pari" + ], + "consuming": [ + "voro", + "vora", + "phago", + "bhaksa", + "grasa" + ], + "producing": [ + "geno", + "creo", + "poio", + "jana", + "srsti" + ], + "rigid": [ + "rigidi", + "duri", + "stricto", + "duro", + "sclero", + "ankylo", + "drdha", + "kathina", + "stabdha" + ], + "fluid": [ + "fluidi", + "liqui", + "fluvi", + "rheo", + "hygro", + "chymo", + "drava", + "srava", + "slattha", + "aqui" + ], + "tolerant": [ + "tolero", + "pati", + "duro", + "anexi", + "saha", + "ksam", + "xam", + "titix" + ] + }, + "qualitySuffixes": { + "small": [ + "ulus", + "culus", + "olus", + "ellus", + "ion", + "iskos", + "ka", + "la" + ], + "very": [ + "issimus", + "istos", + "tama" + ], + "little": [ + "ulus", + "culus", + "olus", + "ellus", + "ion", + "iskos", + "ka", + "la" + ], + "liking": [ + "amans", + "phila", + "kama", + "priya" + ], + "disliking": [ + "phobo", + "miso", + "dviso" + ], + "mutation": [ + "ficans", + "morpho" + ], + "consuming": [ + "vore", + "vorus", + "phagus", + "phago", + "ada" + ], + "producing": [ + "parous", + "fic", + "geno", + "poio", + "ja", + "kara" + ], + "rigid": [ + "rigid", + "durus", + "strictus", + "sclerosus", + "ankylosus" + ], + "fluid": [ + "fluido", + "liquidum", + "fluvium", + "rheus", + "hygrum", + "chymus", + "drava", + "srava", + "slattha", + "aqueous" + ], + "tolerant": [ + "patiens", + "saha" + ] + }, + "colours": { + "red": [ + "rubri", + "rufo", + "erythro", + "rhodo", + "rakta", + "lohita" + ], + "blue": [ + "caeruleo", + "livid", + "cyano", + "glauco", + "nila" + ], + "yellow": [ + "flavi", + "luteo", + "xantho", + "xantho", + "chryso", + "pita" + ], + "green": [ + "viridi", + "chloro", + "harita" + ], + "black": [ + "atri", + "atro", + "melano", + "opaco", + "krsna", + "syama" + ], + "white": [ + "albi", + "candi", + "leuco", + "sveta", + "sukla" + ] + }, + "organelles": { + "binding_agent": { + "prefixes": [ + "bandha", + "coeno", + "collo", + "desmo", + "gano", + "glutino", + "gregi", + "nexo", + "sango", + "symplo", + "syndi", + "copulo", + "fascio", + "haesi", + "harmo", + "jalo", + "linio", + "nodi", + "tacto", + "vinculo", + "yuji", + "con", + "dictyum", + "gregia", + "koinia", + "pagus", + "plexus", + "samaja", + "sorus", + "yuga" + ] + }, + "bioluminescent_vacuole": { + "prefixes": [ + "luci", + "luxo", + "jyoti", + "fulguri", + "lampo", + "photo", + "fulgo" + ], + "suffixes": [ + "vacuo", + "cysto" + ] + }, + "chemoplast": { + "prefixes": [ + "thio", + "gandhako", + "litho", + "capno", + "sulpho", + "sulfo", + "chemo", + "tantalo", + "prometheo", + "niobo", + "helio", + "seleno", + "tello", + "phlogo", + "anthro", + "ioeido", + "sulbari", + "sphati", + "alkimi", + "alkemo", + "aether" + ], + "suffixes": [ + "glyca", + "sacchara", + "madhua", + "dulcia", + "muta" + ] + }, + "chloroplast": { + "prefixes": [ + "chloro", + "photo", + "helio", + "harito", + "luci", + "thylako", + "prasio", + "beryllo", + "smaragdo", + "virido", + "malvo", + "herbo", + "marakato", + "kamsyo", + "krypto", + "leovirido", + "gronno", + "sun", + "star" + ], + "suffixes": [ + "capto", + "prana", + "plasto", + "karo", + "grano" + ] + }, + "cilia": { + "prefixes": [ + "cilio", + "kineto", + "mastigo", + "roma", + "tricho", + "pecto", + "pectino", + "saeta", + "penicillo", + "kteis", + "cteis", + "cto", + "cteno", + "stlengo", + "ortho", + "kankato", + "kurco", + "circe", + "sampo", + "bifrosto", + "comb" + ], + "suffixes": [ + "pulso", + "remo", + "spando", + "tubulo", + "gati", + "phora", + "scopa" + ] + }, + "cytoplasm": { + "prefixes": [ + "cyto", + "keno", + "hyalo", + "proto", + "vacuo", + "hydro", + "shunya", + "cysto", + "nudo", + "choro", + "puro", + "vasculo", + "cell" + ], + "suffixes": [ + "angea", + "cava", + "kosha", + "sacca", + "simplex" + ] + }, + "ferroplast": { + "prefixes": [ + "ferroplasto", + "rusti", + "rusticyano", + "ferrocyano", + "sidero", + "hemato", + "erythro", + "magneto", + "ayas", + "mjolnir", + "robigo", + "rubigo", + "aerugo", + "chalybs", + "sara", + "ukku", + "wootz", + "ferro", + "cupro", + "iron" + ] + }, + "flagellum": { + "prefixes": [ + "caudo", + "dino", + "mastigo", + "kasha", + "uro", + "scutica", + "februa", + "mastix", + "kenthro", + "kasa", + "whip", + "frusta" + ], + "suffixes": [ + "pulso", + "remo", + "spando", + "tubulo", + "gati", + "kinesis", + "natrico", + "phora", + "scopa", + "rotato", + "helico", + "chalano", + "droma", + "nator", + "pello", + "plania", + "yana" + ] + }, + "hydrogenosome": { + "prefixes": [ + "physo", + "anoxo", + "hydro", + "vata", + "pneumo", + "aquo", + "hydor", + "apas", + "abzu", + "fervo", + "zymo", + "kinva", + "soma", + "fermento", + "protio", + "phlogisto", + "protono", + "acido", + "amalo", + "vitriol", + "dagda", + "hydra", + "amrita", + "aceto" + ], + "suffixes": [ + "somo", + "zymo", + "schizo", + "fumo", + "paco", + "bullio", + "ergo", + "flato", + "mukto", + "bato" + ] + }, + "lysosome": { + "prefixes": [ + "lyso", + "digesto", + "solvo", + "amla", + "oxy", + "purgo", + "pepto", + "pepso", + "coctio", + "paka", + "xaro", + "ksaro", + "puter", + "mastico", + "dialyso", + "agni", + "cerridwen", + "trito" + ], + "suffixes": [ + "phago", + "pepto", + "bhanja", + "scindo", + "voro", + "clasto", + "hara", + "soma", + "lytes", + "macero" + ] + }, + "melanosome": { + "prefixes": [ + "aura", + "melano", + "radio", + "urano", + "ray", + "fluoro", + "pho", + "akto", + "acto", + "aethero", + "kirana", + "arcis", + "prabha", + "oganess" + ] + }, + "mitochondrion": { + "prefixes": [ + "mito", + "chondro", + "aero", + "oxy", + "shvaso", + "foco", + "prana", + "vayu", + "spiro", + "respiro", + "anapno", + "kyano", + "energia", + "ergo", + "calor", + "ojas", + "eitipi", + "draco" + ], + "suffixes": [ + "cremato", + "tejo", + "ergo", + "chromo", + "dynamia", + "motrix", + "shakti", + "dota", + "furnax" + ] + }, + "nitroplast": { + "prefixes": [ + "ammono", + "ammo", + "ammino", + "amino", + "ammonio", + "amido", + "azoto", + "nitro", + "fertili", + "urvara", + "saphala", + "navasadara" + ], + "suffixes": [ + "nitro", + "fixo", + "azo", + "azoto", + "ammono", + "amino", + "ferax", + "posha" + ] + }, + "nucleus": { + "prefixes": [ + "karyo", + "nucleo", + "bijo", + "centro", + "mulo", + "eukaryo", + "neokaryo", + "kernel", + "codex", + "gnosis", + "liber", + "biblo", + "pustaka", + "grapho", + "sutra", + "recipe", + "formula", + "kalpa", + "grimoire", + "nux", + "cor", + "coro", + "cordo", + "kokko", + "kentro", + "bija", + "garbha", + "mono", + "monax", + "silmaril", + "linux", + "core" + ] + }, + "toxisome": { + "prefixes": [ + "toxi", + "oxytoxi", + "veneno", + "veno", + "visha", + "viro", + "noxo", + "noxio", + "mors", + "thanato", + "kala", + "caedo", + "phono", + "marana", + "basilisk", + "cyano", + "morso", + "venom" + ], + "suffixes": [ + "cysto", + "fundo", + "noxo", + "noxio", + "ghata", + "plaga", + "bolia", + "cido" + ] + }, + "pilus": { + "prefixes": [ + "pilo", + "belono", + "shulo", + "perono", + "hasto", + "cnido", + "trypo", + "foro", + "puncto", + "viddho", + "tubulo", + "syringo", + "bhedia", + "aculeo", + "fuso", + "aco", + "rhaphi", + "belemno", + "atrakto", + "suci", + "tarku", + "vejaka", + "spindl", + "needl" + ] + }, + "signaling_agent": { + "prefixes": [ + "sema", + "hormo", + "nuntio", + "duta", + "para", + "signo", + "commo", + "indico", + "phero", + "phanero", + "pheromo", + "angello", + "angelo", + "tele", + "sema", + "samketa", + "dhvani", + "sandesa", + "qualia", + "senso", + "beacon", + "hermeto", + "hermes", + "iris", + "iridi", + "gondor" + ], + "suffixes": [ + "crino", + "mitto", + "prero", + "fundo", + "cysto", + "pompo", + "vocans", + "tropia", + "naya", + "kinesis", + "tacto" + ] + }, + "slime_jet": { + "prefixes": [ + "muco", + "myxo", + "blenno", + "sleshmo", + "phlegmo", + "gluto", + "glutino", + "visco", + "limo", + "kolla", + "colla", + "guggulu", + "plasmo", + "tar", + "polymero" + ], + "suffixes": [ + "ejacto", + "blyso", + "truso", + "sravo", + "jaculo", + "rhoea", + "dhara", + "klysta", + "tracta", + "fusio" + ] + }, + "thermoplast": { + "prefixes": [ + "thermo", + "calori", + "pyro", + "tapo", + "thalpo", + "fervo", + "ebullio", + "bullo", + "zein", + "kvatha", + "ignis", + "pur", + "agni", + "fornax", + "camino", + "crucible", + "crucibo", + "hearth", + "hestia", + "prometheo", + "torch", + "phoenix", + "phlego", + "magma" + ], + "suffixes": [ + "plasto", + "cysto", + "kauma", + "tapa", + "calens", + "therma", + "furna" + ] + }, + "chemoreceptor": { + "prefixes": [ + "osmo", + "capto", + "olfacto", + "chemo", + "philo", + "amo", + "noso", + "nosophor", + "nosofer", + "odor", + "nasus", + "naso", + "fragro", + "aistheto", + "rho", + "rhino", + "rhinophor", + "rhinofer", + "ghrana", + "gandha", + "smell" + ] + }, + "vacuole": { + "prefixes": [ + "vacuo", + "asco", + "kumbo", + "kysto", + "cysto", + "vesico", + "cisterna", + "sacca", + "otre", + "cella", + "cista", + "theke", + "angos", + "kosa", + "kumba", + "asaya", + "box", + "chest", + "storo" + ], + "suffixes": [ + "turgo", + "dharo", + "choro", + "pieso", + "recepto", + "vaha" + ] + } + }, + "organelleMap": { + "chemosynthesizing_proteins": "chemoplast", + "thylakoids": "chloroplast", + "nitrogenase": "nitroplast", + "protoplasm": "cytoplasm", + "hydrogenase": "hydrogenosome", + "rusticyanin": "ferroplast", + "thermosynthase": "thermoplast", + "toxin_vacuole": "vacuole", + "metabolosomes": "mitochondrion", + "perforator_pilus": "pilus" + }, + "processes": { + "generic": [ + "chymo", + "chemo", + "nido", + "osmo", + "rasa", + "sapio" + ], + "glucose": [ + "glyco", + "saccharo", + "madhua", + "dulci" + ], + "sulphur": [ + "thio", + "gandhako", + "litho", + "sulfo", + "sulpho", + "mercapto", + "thiono", + "tio" + ], + "iron": [ + "ferro", + "rusti", + "rusticyano", + "ferrocyano", + "sidero", + "siderocyano", + "hemato", + "erythros", + "magneto" + ], + "phosphate": [ + "phospho", + "fosfo", + "phosphoro", + "fosforo" + ], + "nitrogen": [ + "nitro", + "aero", + "osmo", + "azoto" + ], + "ammonia": [ + "ammono", + "ammo", + "ammino", + "amino", + "ammonio", + "amido", + "azoto", + "nitro", + "fertili", + "urvara", + "saphala", + "navasadara" + ], + "temperature": [ + "calori", + "fervi", + "calidi", + "thermo", + "pyro", + "usna", + "tapa" + ], + "sunlight": [ + "calori", + "calidi", + "photo", + "helio", + "harito", + "luci" + ], + "oxytoxy": [ + "toxi", + "oxytoxi", + "veneno", + "veno", + "visha", + "viro", + "noxo", + "noxio" + ], + "radiation": [ + "melano", + "radio", + "urano", + "thor", + "ergo", + "pyro", + "hephaisto" + ], + "carbondioxide": [ + "capno", + "carboxy", + "fumo", + "fumi", + "dhuma", + "dhumaka" + ], + "oxygen": [ + "oxy", + "oxido", + "aero", + "oxo", + "vayu", + "amla", + "hydro", + "osmo", + "eolo" + ], + "luciferase": [ + "luci", + "luxo", + "jyoti", + "fulguri", + "lampo", + "photo", + "fulgo", + "lucifer" + ] + }, + "tolerances": { + "hot": [ + "calori", + "fervi", + "calidi", + "thermo", + "pyro", + "usna", + "tapa" + ], + "cold": [ + "frigi", + "gel", + "cryo", + "psychro", + "sita", + "hima" + ], + "sunlight": [ + "calori", + "calidi", + "photo", + "helio", + "harito", + "luci" + ], + "oxygen": [ + "oxy", + "oxido", + "aero", + "oxo", + "vayu", + "amla", + "hydro", + "osmo", + "eolo" + ], + "pressure": [ + "presso", + "grave", + "baro", + "piezo", + "pida", + "bhara" + ] + }, + "membranes": { + "double": [ + "bimembrano", + "bipelli", + "diplolemmo", + "diplohymeno", + "dvyavarana" + ], + "cellulose": [ + "ligni", + "xylo", + "valkala" + ], + "chitin": [ + "tunic", + "loric", + "chitino", + "chito", + "varman" + ], + "calciumCarbonate": [ + "calci", + "creta", + "titano", + "sudha" + ], + "silica": [ + "silici", + "silico", + "psammo", + "chalico", + "sikata", + "upala" + ] + }, + "bacteriaShapes": { + "coccum": { + "masculine": [ + "bacter", + "coccus" + ], + "feminine": [ + "sphaera", + "monas" + ], + "neuter": [ + "coccum", + "sphaerium" + ] + }, + "bacillum": { + "masculine": [ + "bacillus", + "bacter" + ], + "feminine": [ + "bacilla", + "monas" + ], + "neuter": [ + "bacterium", + "baculum", + "bactrum" + ] + }, + "vibrion": { + "masculine": [ + "bacter", + "vibrio", + "virgula" + ] + }, + "fusiform": { + "masculine": [ + "fusum", + "clostridium" + ] + } + }, + "suffixes": { + "masculine": [ + "us", + "olus", + "rius", + "canus", + "tus", + "tor", + "ster", + "ili", + "o" + ], + "feminine": [ + "olera", + "ilia", + "idia", + "ila", + "ola", + "ala", + "ela", + "ula", + "ia", + "sia", + "sia", + "ella", + "nea", + "ra", + "dae", + "mia", + "a" + ], + "neuter": [ + "ium", + "um", + "trum", + "alia", + "data", + "en", + "os", + "on" + ], + "ambiguous": [ + "is", + "as", + "es", + "ex", + "ax", + "ix", + "ox", + "ux" + ] + }, "prefixesV": [ "Ni", "Pi", @@ -284,85 +1569,5 @@ "mon", "yt", "yn" - ], - "suffixesV": [ - "olera", - "ilia", - "idia", - "ium", - "um", - "un", - "an", - "on", - "en", - "in", - "ion", - "ien", - "ian", - "iem", - "iom", - "is", - "as", - "es", - "os", - "us", - "olus", - "ila", - "ola", - "ala", - "ela", - "ula", - "ex", - "ax", - "ix", - "ox", - "ux", - "ia", - "sia", - "an", - "alia", - "ite", - "ili", - "ian", - "ales", - "ella", - "ens", - "opsis", - "upsis", - "epsis", - "apsis", - "ipsis", - "ansis", - "ensis", - "insis", - "onsis", - "unsis" - ], - "suffixesC": [ - "pien", - "pion", - "pian", - "piun", - "rius", - "nien", - "ster", - "stir", - "canus", - "tus", - "cys", - "trum", - "nea", - "ceae", - "ra", - "tor", - "sia", - "data", - "dae", - "mia", - "quito", - "hin", - "guar", - "yes", - "ko" ] } diff --git a/src/auto-evo/mutations/CommonMutationFunctions.cs b/src/auto-evo/mutations/CommonMutationFunctions.cs index f706185b8e8..cd70c81ba9b 100644 --- a/src/auto-evo/mutations/CommonMutationFunctions.cs +++ b/src/auto-evo/mutations/CommonMutationFunctions.cs @@ -94,8 +94,8 @@ public static MicrobeSpecies GenerateRandomSpecies(MicrobeSpecies mutated, Patch // Override the default species starting name to have more variability in the names var nameGenerator = SimulationParameters.Instance.NameGenerator; - mutated.Epithet = nameGenerator.GenerateNameSection(random, true); - mutated.Genus = nameGenerator.GenerateNameSection(random); + mutated.Genus = nameGenerator.GenerateGenusName(random, null, mutated); + mutated.Epithet = nameGenerator.GenerateEpithetName(random, null, mutated, forPatch); mutated.OnEdited(); diff --git a/src/auto-evo/mutations/MutationLogicFunctions.cs b/src/auto-evo/mutations/MutationLogicFunctions.cs index a651c6aecff..ff66ab9fbae 100644 --- a/src/auto-evo/mutations/MutationLogicFunctions.cs +++ b/src/auto-evo/mutations/MutationLogicFunctions.cs @@ -6,7 +6,7 @@ public class MutationLogicFunctions { - public static void NameNewMicrobeSpecies(MicrobeSpecies newSpecies, MicrobeSpecies parentSpecies) + public static void NameNewMicrobeSpecies(MicrobeSpecies newSpecies, MicrobeSpecies parentSpecies, Patch? forPatch) { // If for some silly reason the species are the same don't rename if (newSpecies == parentSpecies) @@ -14,16 +14,11 @@ public static void NameNewMicrobeSpecies(MicrobeSpecies newSpecies, MicrobeSpeci return; } - if (MicrobeSpeciesIsNewGenus(newSpecies, parentSpecies)) - { - newSpecies.Genus = SimulationParameters.Instance.NameGenerator.GenerateNameSection(); - } - else - { - newSpecies.Genus = parentSpecies.Genus; - } - - newSpecies.Epithet = SimulationParameters.Instance.NameGenerator.GenerateNameSection(null, true); + // Genus check is now handled inside GenerateGenusName + newSpecies.Genus = SimulationParameters.Instance.NameGenerator.GenerateGenusName(null, parentSpecies, + newSpecies); + newSpecies.Epithet = SimulationParameters.Instance.NameGenerator.GenerateEpithetName(null, parentSpecies, + newSpecies, forPatch); } public static void ColourNewMicrobeSpecies(Random random, MicrobeSpecies newSpecies, diff --git a/src/auto-evo/steps/ModifyExistingSpecies.cs b/src/auto-evo/steps/ModifyExistingSpecies.cs index 2b56f429b41..002d4cbdeaf 100644 --- a/src/auto-evo/steps/ModifyExistingSpecies.cs +++ b/src/auto-evo/steps/ModifyExistingSpecies.cs @@ -228,9 +228,12 @@ public bool RunStep(RunResults results) if (newPopulation > Constants.AUTO_EVO_MINIMUM_VIABLE_POPULATION) { // Only apply a new name and colour to results that are actually kept - MutationLogicFunctions.NameNewMicrobeSpecies(mutation.MutatedSpecies, mutation.ParentSpecies); + // Since the new name generator, colouring must come before naming as the epithet could be + // influenced by the new colour of the species. MutationLogicFunctions.ColourNewMicrobeSpecies(random, mutation.MutatedSpecies, mutation.ParentSpecies); + MutationLogicFunctions.NameNewMicrobeSpecies(mutation.MutatedSpecies, mutation.ParentSpecies, + patch); results.AddPossibleMutation(mutation.MutatedSpecies, new KeyValuePair(patch, newPopulation), mutation.AddType, diff --git a/src/benchmark/microbe/MicrobeBenchmark.cs b/src/benchmark/microbe/MicrobeBenchmark.cs index a238972bd94..a0bd806ba70 100644 --- a/src/benchmark/microbe/MicrobeBenchmark.cs +++ b/src/benchmark/microbe/MicrobeBenchmark.cs @@ -477,11 +477,13 @@ private void GenerateWorldAndSpecies() for (int i = 0; i < SPECIES_COUNT; ++i) { - var species = CommonMutationFunctions.GenerateRandomSpecies(world.NewMicrobeSpecies( - nameGenerator.GenerateNameSection(random), - nameGenerator.GenerateNameSection(random, true)), dummyPatch, workMemory, random, + var species = CommonMutationFunctions.GenerateRandomSpecies( + world.NewMicrobeSpecies(string.Empty, string.Empty), dummyPatch, workMemory, random, random.Next(200, 500)); + species.Genus = nameGenerator.GenerateGenusName(random, null, species); + species.Epithet = nameGenerator.GenerateEpithetName(random, null, species, dummyPatch); + generatedSpecies.Add(species); } } diff --git a/src/general/Species.cs b/src/general/Species.cs index ff3bb56fd5d..7850304aafd 100644 --- a/src/general/Species.cs +++ b/src/general/Species.cs @@ -143,6 +143,11 @@ public Color GUIColour /// public string FormattedNameBbCodeUnstyled => $"[url=species:{ID}]{FormattedName}[/url]"; + /// + /// A property used by the NameGenerator to keep trace of the naming state. + /// + public NameGenerator.NamingState? NamingState { get; set; } + public string FormattedIdentifier => FormattedName + $" ({ID:n0})"; public bool IsExtinct => Population <= 0; @@ -274,6 +279,8 @@ public virtual void ApplyMutation(Species mutation) cachedBaseReproductionCost = null; + NamingState = mutation.NamingState; + // These don't mutate for a species // genus; // epithet; diff --git a/src/general/mutation_points/IReadOnlyEnvironmentalTolerances.cs b/src/general/mutation_points/IReadOnlyEnvironmentalTolerances.cs index 4bf69c2fa77..343e23a27fa 100644 --- a/src/general/mutation_points/IReadOnlyEnvironmentalTolerances.cs +++ b/src/general/mutation_points/IReadOnlyEnvironmentalTolerances.cs @@ -1,10 +1,37 @@ public interface IReadOnlyEnvironmentalTolerances { + /// + /// Temperature (in C) that this species likes to be in + /// public float PreferredTemperature { get; } + + /// + /// How wide a temperature range this species can stay in effectively. The range of temperatures is + /// PreferredTemperature - TemperatureTolerance to PreferredTemperature + TemperatureTolerance + /// public float TemperatureTolerance { get; } + + /// + /// Minimum pressure this species likes. The value is in Pa (pascals). This is not just a single range as + /// the range needs to be lopsided towards surviving higher pressures. + /// + /// + /// + /// The difference between the defaults may not be over Constants.TOLERANCE_PRESSURE_RANGE_MAX, otherwise the + /// GUI will break when this data is fed in. + /// + /// public float PressureMinimum { get; } public float PressureTolerance { get; } + + /// + /// UV Resistance ranged in the unit interval. + /// public float UVResistance { get; } + + /// + /// Oxygen Resistance ranged in the unit interval. + /// public float OxygenResistance { get; } public EnvironmentalTolerances Clone() diff --git a/src/general/utils/ListUtils.cs b/src/general/utils/ListUtils.cs index 49dd47d6b9c..86c40fed923 100644 --- a/src/general/utils/ListUtils.cs +++ b/src/general/utils/ListUtils.cs @@ -64,9 +64,18 @@ public static int RandomElementIndexByProbability(this IReadOnlyList cha /// /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Shuffle(this IList list, Random random) { - int length = list.Count; + list.Shuffle(random, list.Count); + } + + /// + /// + /// + public static void Shuffle(this IList list, Random random, int size) + { + int length = size; for (int i = 0; i < length - 1; ++i) { int j = random.Next(i, length); diff --git a/src/microbe_stage/BioProcess.cs b/src/microbe_stage/BioProcess.cs index c96ab7ea6cb..7a6c1bba9b7 100644 --- a/src/microbe_stage/BioProcess.cs +++ b/src/microbe_stage/BioProcess.cs @@ -6,7 +6,7 @@ /// /// Definition of a bio process that cells can do in the form of a TweakedProcess. /// -public class BioProcess : RegistryType +public class BioProcess : RegistryType, INameGenerationTarget { /// /// User visible pretty name diff --git a/src/microbe_stage/BiomeCompoundProperties.cs b/src/microbe_stage/BiomeCompoundProperties.cs index 234733c0765..11a2ca3e0b3 100644 --- a/src/microbe_stage/BiomeCompoundProperties.cs +++ b/src/microbe_stage/BiomeCompoundProperties.cs @@ -103,7 +103,10 @@ public override int GetHashCode() public bool Equals(BiomeCompoundProperties other) { + // ReSharper disable CompareOfFloatsByEqualityOperator return Amount == other.Amount && Density == other.Density && Ambient == other.Ambient; + + // ReSharper enable CompareOfFloatsByEqualityOperator } public override string ToString() diff --git a/src/microbe_stage/INameGenerationTarget.cs b/src/microbe_stage/INameGenerationTarget.cs new file mode 100644 index 00000000000..4f835c3164d --- /dev/null +++ b/src/microbe_stage/INameGenerationTarget.cs @@ -0,0 +1 @@ +public interface INameGenerationTarget; diff --git a/src/microbe_stage/NameGenerator.Epithet.cs b/src/microbe_stage/NameGenerator.Epithet.cs new file mode 100644 index 00000000000..d416947411b --- /dev/null +++ b/src/microbe_stage/NameGenerator.Epithet.cs @@ -0,0 +1,174 @@ +using System; +using System.Buffers; +using System.Text; +using Xoshiro.PRNG64; + +/// +/// The microbe species name generator. +/// +public partial class NameGenerator +{ + public string GenerateEpithetName(Random? random, MicrobeSpecies? speciesOld, MicrobeSpecies speciesNew, + Patch? patch) + { + var stringBuilder = GetBuffer(); + random ??= new XoShiRo256starstar(); + + if (speciesNew.NamingState is null) + throw new Exception("GenerateEpithetName should be called after GenerateGenusName."); + + var gender = speciesNew.NamingState.Gender; + + GenerateEpithetRoot(random, stringBuilder, speciesNew, patch); + GenerateGenderedSuffix(random, stringBuilder, gender, false, speciesNew); + + stringBuilder[0] = char.ToLowerInvariant(stringBuilder[0]); + + return stringBuilder.ToString(); + } + + private void GenerateEpithetRoot(Random random, StringBuilder stringBuilder, MicrobeSpecies species, Patch? patch) + { + double ruleSelector = random.NextDouble() * 100.0; + + if (ruleSelector > 98) + { + // Easter egg naming. + PhonotacticsFriendlyAppend(stringBuilder, config.PrefixCofix.Random(random)); + return; + } + + const int numberOfRules = 5; + + int[] rules = ArrayPool.Shared.Rent(numberOfRules); + for (int i = 0; i < numberOfRules; ++i) + rules[i] = i; + + rules.Shuffle(random, numberOfRules); + + string root = string.Empty; + for (int i = 0; i < numberOfRules && root == string.Empty; ++i) + { + int rule = rules[i]; + + switch (rule) + { + case 0: + // Rule 1: quality-based naming + GenerateQualityBasedRoot(random, species, out root); + break; + case 1: + // Rule 2: tolerance-based naming + GenerateToleranceBasedRoot(random, species, out root); + break; + case 2: + // Rule 3: Membrane-based naming + GenerateMembraneBasedRoot(random, species, out root); + break; + case 3: + // Rile 4: colour-based naming + GenerateColourBasedRoot(random, species, out root); + break; + case 4: + // Rule 5: patch-based naming + GeneratePatchBasedRoot(random, patch, out root); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + if (root == string.Empty) + { + // If everything failed, fall back to the old generator. + ModifiedLegacyGenerateNameRoot(random, stringBuilder); + } + else + { + PhonotacticsFriendlyAppend(stringBuilder, root); + } + + ArrayPool.Shared.Return(rules); + } + + private void GenerateQualityBasedRoot(Random random, MicrobeSpecies species, out string adjective) + { + var dictionary = CalculateRelevantQualities(species); + var key = ExtractWeightedKey(random, dictionary); + + if (key == string.Empty) + { + adjective = string.Empty; + return; + } + + if (!config.QualityRoots.TryGetValue(key, out var roots)) + { + throw new Exception($"Quality adjectives not mapped in species_names.json: {key}"); + } + + adjective = roots.Random(random); + } + + private void GenerateToleranceBasedRoot(Random random, MicrobeSpecies species, out string adjective) + { + var dictionary = CalculateRelevantTolerances(species); + var key = ExtractWeightedKey(random, dictionary); + + if (key == string.Empty) + { + adjective = string.Empty; + return; + } + + if (!config.Tolerances.TryGetValue(key, out var roots)) + { + throw new Exception($"Process adjectives not mapped in species_names.json: {key}"); + } + + adjective = roots.Random(random); + } + + private void GenerateMembraneBasedRoot(Random random, MicrobeSpecies species, out string adjective) + { + var key = species.MembraneType.InternalName; + + if (key == "single") + { + adjective = string.Empty; + return; + } + + if (!config.Membranes.TryGetValue(key, out var roots)) + { + throw new Exception($"Membrane adjectives not mapped in species_names.json: {key}"); + } + + adjective = roots.Random(random); + } + + private void GenerateColourBasedRoot(Random random, MicrobeSpecies species, out string adjective) + { + var colour = CalculateColourAdjective(species); + + if (colour == string.Empty) + { + adjective = string.Empty; + return; + } + + if (!config.Colours.TryGetValue(colour, out var colourRoots)) + { + throw new Exception($"Colour adjectives not mapped in species_names.json: {colour}"); + } + + adjective = colourRoots.Random(random); + } + + private void GeneratePatchBasedRoot(Random random, Patch? patch, out string adjective) + { + adjective = patch is null ? + SimulationParameters.Instance.PatchMapNameGenerator.Next(random).ContinentName : + patch.Region.Name; + } +} diff --git a/src/microbe_stage/NameGenerator.Genus.cs b/src/microbe_stage/NameGenerator.Genus.cs new file mode 100644 index 00000000000..88e181ec5bc --- /dev/null +++ b/src/microbe_stage/NameGenerator.Genus.cs @@ -0,0 +1,241 @@ +using System; +using System.Linq; +using System.Text; +using Xoshiro.PRNG64; + +/// +/// The microbe species name generator. +/// +public partial class NameGenerator +{ + public string GenerateGenusName(Random? random, MicrobeSpecies? speciesOld, MicrobeSpecies speciesNew) + { + var stringBuilder = GetBuffer(); + random ??= new XoShiRo256starstar(); + + GenerateGenusNameInternal(random, stringBuilder, speciesOld, speciesNew, out var isNumbered, out var isProto, + out var newRoot, out var newGender, out var target); + + speciesNew.NamingState = new NamingState(isNumbered, isProto, newRoot, newGender, target); + + stringBuilder[0] = char.ToUpperInvariant(stringBuilder[0]); + + var name = stringBuilder.ToString(); + + return name; + } + + public void GenerateGenusNameInternal(Random random, StringBuilder stringBuilder, + MicrobeSpecies? speciesOld, MicrobeSpecies speciesNew, out bool isNumbered, out bool isProto, + out string newRoot, out GrammaticalGender newGender, out INameGenerationTarget? target) + { + target = null; + + if (speciesOld is null) + { + GenerateFreshGenusName(random, stringBuilder, speciesNew, out target, out newRoot, out newGender, + out isProto); + + isNumbered = false; + + return; + } + + var species1UniqueOrganelles = speciesOld.Organelles.Select(o => o.Definition).ToHashSet(); + var species2UniqueOrganelles = speciesNew.Organelles.Select(o => o.Definition).ToHashSet(); + var newOrganelles = species2UniqueOrganelles.Except(species1UniqueOrganelles).ToHashSet(); + var lostOrganelles = species1UniqueOrganelles.Except(species2UniqueOrganelles).ToHashSet(); + + var randomOrganelle = newOrganelles.Count == 0 ? + species2UniqueOrganelles.Random(random) : + newOrganelles.Random(random); + + int organelleCount = speciesNew.Organelles.Count(organelle => organelle.Definition == randomOrganelle); + + bool useBacteriaSuffix = random.Next(100) < 10 && speciesNew.IsBacteria; + + // Without too many headaches, we regenerate the genus root when we lose at least one organelle. + // This ensures having a new unique name that is unrelated to the lost organelle or processes without comparing + // all the new processes to the old target. + bool regenerateRoot = lostOrganelles.Count > 0; + + if (speciesOld.NamingState is not null) + { + var namingState = speciesOld.NamingState!; + + if (namingState.Target == null || !regenerateRoot) + { + if (newOrganelles.Count == 0 && speciesOld.NamingState is { GenusIsNumbered: false }) + { + // We don't need to create a new genus name, so we return the old one. + if (!namingState.GenusIsProto) + { + stringBuilder.Append(speciesOld.Genus); + + isNumbered = namingState.GenusIsNumbered; + isProto = namingState.GenusIsProto; + newRoot = namingState.GenusRoot; + newGender = namingState.Gender; + + return; + } + + PhonotacticsFriendlyAppend(stringBuilder, "Eu"); + PhonotacticsFriendlyAppend(stringBuilder, namingState.GenusRoot); + + isNumbered = false; + isProto = false; + newRoot = namingState.GenusRoot; + newGender = GenerateGenderedSuffix(random, stringBuilder, namingState.Gender, useBacteriaSuffix, + speciesNew); + + return; + } + + if (namingState.GenusIsProto && random.Next(3) == 0) + { + PhonotacticsFriendlyAppend(stringBuilder, config.QualityRoots["new"].Random(random)); + PhonotacticsFriendlyAppend(stringBuilder, namingState.GenusRoot); + + isNumbered = namingState.GenusIsNumbered; + isProto = false; + newRoot = namingState.GenusRoot; + newGender = GenerateGenderedSuffix(random, stringBuilder, namingState.Gender, useBacteriaSuffix, + speciesNew); + + // Force genus change on the old species to acquire the "Eu-" prefix. + speciesOld.Genus = GenerateGenusName(random, speciesOld, speciesOld); + speciesOld.Epithet = GenerateEpithetName(random, speciesOld, speciesOld, null); + + return; + } + + target = null; + } + + if (namingState.GenusIsNumbered && randomOrganelle == namingState.Target) + { + // Override other rules: we increment the organelle count to maintain consistency in the naming system. + if (!config.Quantity.TryGetValue(organelleCount.ToString(), out var quantity)) + { + quantity = config.Quantity["multiple"]; + isNumbered = false; + } + else + { + isNumbered = true; + } + + PhonotacticsFriendlyAppend(stringBuilder, quantity.Random(random)); + PhonotacticsFriendlyAppend(stringBuilder, namingState.GenusRoot); + + isProto = false; + newRoot = namingState.GenusRoot; + newGender = GenerateGenderedSuffix(random, stringBuilder, namingState.Gender, useBacteriaSuffix, + speciesNew); + + return; + } + } + + isProto = false; + + GenerateGenusRoot(random, stringBuilder, randomOrganelle, organelleCount, out target, out isNumbered, + out var root); + var gender = GenerateGenderedSuffix(random, stringBuilder, null, useBacteriaSuffix, speciesNew); + + newRoot = root; + newGender = gender; + } + + private void GenerateFreshGenusName(Random random, StringBuilder stringBuilder, MicrobeSpecies species, + out INameGenerationTarget? target, out string root, out GrammaticalGender gender, out bool isProto) + { + target = null; + + var speciesUniqueOrganelles = species.Organelles.Select(o => o.Definition).ToHashSet(); + + var randomOrganelle = speciesUniqueOrganelles.Random(random); + + int organelleCount = species.Organelles.Count(organelle => organelle.Definition == randomOrganelle); + + isProto = false; + + if (random.Next(100) < 15) + { + PhonotacticsFriendlyAppend(stringBuilder, config.QualityRoots["first"].Random(random)); + + isProto = true; + } + + int ruleSelector = random.Next(100); + + root = string.Empty; + + switch (ruleSelector) + { + case < 10: + // Rule 1: cell root is "Cyto". + PhonotacticsFriendlyAppend(stringBuilder, "cyto"); + break; + case < 25: + // Rule 2: cell root is "Prim". + PhonotacticsFriendlyAppend(stringBuilder, "prim"); + break; + default: + GenerateGenusRoot(random, stringBuilder, randomOrganelle, organelleCount, out target, out _, out root); + break; + } + + if (root == string.Empty) + root = stringBuilder.ToString(); + + gender = GenerateGenderedSuffix(random, stringBuilder, null, true, species); + } + + private void GenerateGenusRoot(Random random, StringBuilder stringBuilder, OrganelleDefinition randomOrganelle, + int organelleCount, out INameGenerationTarget target, out bool isNumbered, out string root) + { + isNumbered = false; + target = randomOrganelle; + + var randomOrganelleProcesses = randomOrganelle.RunnableProcesses; + + int ruleSelector = random.Next(100); + + const int ruleThreshold = 48; + + switch (ruleSelector) + { + case < ruleThreshold: + if (randomOrganelleProcesses.Count == 0) + { + // There are no processes for the selected organelle. + // Rule 1: use the organelle definition naming system + isNumbered = GenerateOrganelleBasedName(random, stringBuilder, randomOrganelle, organelleCount, + out root); + } + else + { + // Rule 2: use the process definition naming system + var process = randomOrganelleProcesses.Random(random).Process; + + isNumbered = GenerateProcessBasedName(random, stringBuilder, process, out root); + + target = process; + } + + break; + case < 2 * ruleThreshold: + // Rule 1. + isNumbered = GenerateOrganelleBasedName(random, stringBuilder, randomOrganelle, organelleCount, + out root); + break; + default: + // Rule 3: easter egg naming + PhonotacticsFriendlyAppend(stringBuilder, config.PrefixCofix.Random(random)); + root = stringBuilder.ToString(); + break; + } + } +} diff --git a/src/microbe_stage/NameGenerator.Utils.cs b/src/microbe_stage/NameGenerator.Utils.cs new file mode 100644 index 00000000000..22c8b34d6c6 --- /dev/null +++ b/src/microbe_stage/NameGenerator.Utils.cs @@ -0,0 +1,498 @@ +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using Godot; + +/// +/// The microbe species name generator. +/// +public partial class NameGenerator +{ + private static readonly HashSet Vocals = ['a', 'e', 'i', 'o', 'u']; + private static readonly HashSet Bilabials = ['p', 'b', 'm']; + + private static readonly FrozenDictionary<(char, char), char> VocalicTransitions = new Dictionary<(char, char), char> + { + { ('o', 'a'), 'o' }, + { ('e', 'a'), 'e' }, + { ('e', 'u'), 'u' }, + { ('o', 'u'), 'u' }, + }.ToFrozenDictionary(); + + private static StringBuilder? stringBuffer; + + public enum GrammaticalGender + { + Masculine, + Feminine, + Neuter, + Ambiguous, + } + + private enum BacteriaShape + { + Coccum, + Bacillum, + Fusiform, + Vibrion, + Unknown, + } + + private static StringBuilder GetBuffer() + { + stringBuffer ??= new StringBuilder(64); + + stringBuffer.Length = 0; + return stringBuffer; + } + + private static T GetRandomElement(Random random, T[] array) + { + return array[random.Next(array.Length)]; + } + + private static void PhonotacticsFriendlyAppend(StringBuilder stringBuilder, string data) + { + if (stringBuilder.Length == 0) + { + stringBuilder.Append(data); + return; + } + + var next = data[0]; + var previous = stringBuilder[^1]; + + if (Vocals.Contains(next) && Vocals.Contains(previous)) + { + // Both are vocals + + if (next == previous) + { + --stringBuilder.Length; + } + else if (VocalicTransitions.TryGetValue((previous, next), out var vocal)) + { + --stringBuilder.Length; + stringBuilder.Append(vocal); + stringBuilder.Append(data, 1, data.Length - 1); + + return; + } + } + else if (!Vocals.Contains(next) && !Vocals.Contains(previous)) + { + switch (previous) + { + // Both are consonants + + case 'n' when Bilabials.Contains(next): + // n before bilabial becomes m + --stringBuilder.Length; + stringBuilder.Append('m'); + break; + case 'n' when next is 'r' or 'l': + // n before liquid gets assimilated + --stringBuilder.Length; + stringBuilder.Append(next); + break; + } + } + + stringBuilder.Append(data); + } + + private static Dictionary CalculateRelevantQualities(MicrobeSpecies species) + { + Dictionary relevantQualities = new(); + + // Speed + var baseSpeed = species.BaseSpeed; + var speedWeight = Math.Abs(Clamp((baseSpeed - 30) * 0.05, -1, 1)); + switch (baseSpeed) + { + case > 40: + relevantQualities.Add("fast", speedWeight); + break; + case < 15: + relevantQualities.Add("slow", speedWeight); + break; + } + + // Preferred temperature + var preferredTemperature = species.Tolerances.PreferredTemperature; + var temperatureWeight = Math.Abs(Clamp((preferredTemperature - 50) * 0.02, -1, 1)); + switch (preferredTemperature) + { + case > 80: + relevantQualities.Add("hot", temperatureWeight); + break; + case < 0: + relevantQualities.Add("cold", temperatureWeight); + break; + } + + // Size + var size = species.BaseHexSize; + var logSize = Math.Log2(size); + var sizeWeight = Math.Abs(Clamp(1.0 / 6.0 * logSize - 1 + 5.0 / 42.0 * logSize, -1, 1)); + switch (sizeWeight) + { + case > 50: + relevantQualities.Add("big", sizeWeight); + break; + case <= 1: + relevantQualities.Add("small", sizeWeight); + break; + } + + // Membrane rigidity + var rigidity = 0.5 * (species.MembraneRigidity + 1.0); + rigidity = rigidity * rigidity * rigidity * rigidity * Math.Sign(rigidity); + switch (rigidity) + { + case > 0.5: + relevantQualities.Add("rigid", rigidity); + break; + case < 0.5: + relevantQualities.Add("fluid", rigidity); + break; + } + + return relevantQualities; + } + + private static Dictionary CalculateRelevantTolerances(MicrobeSpecies species) + { + Dictionary relevantTolerances = new(); + + var tolerances = species.Tolerances; + + // Oxygen resistance + var oxygenResistance = tolerances.OxygenResistance; + switch (oxygenResistance) + { + case > 0.1f: + relevantTolerances.Add("oxygen", oxygenResistance); + break; + } + + // Preferred temperature + var preferredTemperature = tolerances.PreferredTemperature; + var temperatureWeight = Math.Abs(Clamp((preferredTemperature - 50) * 0.02, -1, 1)); + switch (preferredTemperature) + { + case > 80: + relevantTolerances.Add("hot", temperatureWeight); + break; + case < 0: + relevantTolerances.Add("cold", temperatureWeight); + break; + } + + // Pressure + var pressure = tolerances.PressureMinimum + tolerances.PressureTolerance; + var logPressure = Math.Log10(pressure); + switch (logPressure) + { + case > 7: + var logPressureWeight = Clamp(logPressure / 8.0, -1.0, 1.0); + relevantTolerances.Add("pressure", logPressureWeight); + break; + } + + // Membrane rigidity + var uvResistance = tolerances.UVResistance; + switch (uvResistance) + { + case > 0.5f: + relevantTolerances.Add("sunlight", uvResistance); + break; + } + + return relevantTolerances; + } + + private static string CalculateColourAdjective(MicrobeSpecies species) + { + var colour = species.Colour; + var whiteness = (double)Math.Min(colour.R, Math.Min(colour.G, colour.B)); + var r = colour.R - whiteness; + var g = colour.G - whiteness; + var b = colour.B - whiteness; + var yellowness = Math.Min(r, g); + var redness = Math.Max(0, r - yellowness); + var greenness = Math.Max(0, g - yellowness); + + var wantedAdjective = whiteness switch + { + > 0.9 => "white", + < 0.1 => "black", + _ => string.Empty, + }; + + if (yellowness > 0.8) + { + wantedAdjective = "yellow"; + } + else if (redness > 0.8) + { + wantedAdjective = "red"; + } + else if (greenness > 0.8) + { + wantedAdjective = "green"; + } + else if (b > 0.8) + { + wantedAdjective = "blue"; + } + + return wantedAdjective; + } + + /// + /// Transforms GrammaticalGender to a string without allocating. + /// + private static string GenderToString(GrammaticalGender gender) + { + return gender switch + { + GrammaticalGender.Masculine => "masculine", + GrammaticalGender.Feminine => "feminine", + GrammaticalGender.Neuter => "neuter", + GrammaticalGender.Ambiguous => "ambiguous", + _ => throw new ArgumentOutOfRangeException(nameof(gender), gender, null), + }; + } + + private static GrammaticalGender GenderFromString(string gender) + { + return gender switch + { + "masculine" => GrammaticalGender.Masculine, + "feminine" => GrammaticalGender.Feminine, + "neuter" => GrammaticalGender.Neuter, + "ambiguous" => GrammaticalGender.Ambiguous, + _ => throw new ArgumentOutOfRangeException(nameof(gender), gender, null), + }; + } + + private static string ExtractWeightedKey(Random random, Dictionary dictionary) + { + if (dictionary.Count == 0) + return string.Empty; + + double maxRandom = 0.0; + foreach (var weight in dictionary.Values) + { + maxRandom += Math.Abs(weight); + } + + var randomValue = random.NextDouble() * maxRandom; + + double cumulative = 0.0; + string key = string.Empty; + foreach (var entry in dictionary) + { + cumulative += Math.Abs(entry.Value); + + if (cumulative < randomValue) + continue; + + key = entry.Key; + break; + } + + return key; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double Clamp(double value, double min, double max) + { + return Math.Min(Math.Max(value, min), max); + } + + private T GetRandomElement(Random random, IList list) + { + return list[random.Next(list.Count)]; + } + + private GrammaticalGender GenerateGenderedSuffix(Random random, StringBuilder stringBuilder, + GrammaticalGender? parent, bool useBacteria, MicrobeSpecies? bacterium) + { + GrammaticalGender gender; + + // If the parent gender is ambiguous, we can consistently reroll the wanted grammatical gender for the new + // generation. + if (parent == GrammaticalGender.Ambiguous) + { + gender = GetRandomElement(random, (GrammaticalGender[])Enum.GetValuesAsUnderlyingType()); + } + else + { + gender = parent ?? GetRandomElement(random, + (GrammaticalGender[])Enum.GetValuesAsUnderlyingType()); + } + + var genderName = GenderToString(gender); + + string genderedSuffix; + if (useBacteria) + { + // We want to generate a bacterium suffix. + + if (bacterium is null) + { + genderedSuffix = config.BacteriaShapes.Random(random)!.TryGetValue(genderName, out var suffixes) ? + suffixes.Random(random) : + config.Suffixes[genderName].Random(random); + } + else + { + // If a species is provided, we generate the bacterium suffix based on the shape. + + gender = GenerateGenderedBacteriumSuffix(random, gender, bacterium, out var suffixes); + + genderedSuffix = suffixes.Random(random); + } + } + else + { + genderedSuffix = config.Suffixes[genderName].Random(random); + } + + PhonotacticsFriendlyAppend(stringBuilder, genderedSuffix); + + return gender; + } + + private GrammaticalGender GenerateGenderedBacteriumSuffix(Random random, GrammaticalGender parent, + MicrobeSpecies bacterium, out List suffixes) + { + var @override = parent; + + // We do some principal component analysis on the organelle layout to see what shape this is. + var centroid = Hex.AxialToCartesian(bacterium.Organelles.CenterOfMass); + + int count = bacterium.Organelles.Count; + + // Calculate the covariance matrix of the layout and the minimum squared distance to centroid. + float varX = 0f; + float varY = 0f; + float cov = 0f; + + float minSquaredDistanceToCentroid = float.MaxValue; + for (int i = 0; i < count; ++i) + { + var organelle = bacterium.Organelles[i]; + + Vector3 pos = Hex.AxialToCartesian(organelle.Position); + + float dx = pos.X - centroid.X; + float dy = pos.Z - centroid.Z; + + varX += dx * dx; + varY += dy * dy; + cov += dx * dy; + + float distanceSquared = dx * dx + dy * dy; + + if (distanceSquared < minSquaredDistanceToCentroid) + minSquaredDistanceToCentroid = distanceSquared; + } + + varX /= count; + varY /= count; + cov /= count; + + // Compute the eigenvalues of the covariance matrix + float trace = varX + varY; + float determinant = varX * varY - cov * cov; + float gap = (float)Math.Sqrt(Math.Max(0, trace * trace - 4 * determinant)); + float lambda1 = (trace + gap) / 2.0f; + float lambda2 = (trace - gap) / 2.0f; + float absLambda1 = MathF.Abs(lambda1); + float absLambda2 = MathF.Abs(lambda2); + float signLambda1 = (absLambda1 < 0.0001f) ? +1.0f : MathF.Sign(lambda1); + float signLambda2 = (absLambda2 < 0.0001f) ? +1.0f : MathF.Sign(lambda1); + float adjustedLambda1 = MathF.Max(absLambda1, Constants.DEFAULT_HEX_SIZE) * signLambda1; + float adjustedLambda2 = MathF.Max(absLambda2, Constants.DEFAULT_HEX_SIZE) * signLambda2; + float aspectRatio = (float)Math.Sqrt(adjustedLambda1 / adjustedLambda2); + + bool isCentroidOutside = minSquaredDistanceToCentroid > 0.75f * Constants.DEFAULT_HEX_SIZE * + Constants.DEFAULT_HEX_SIZE; + + var shape = count switch + { + // Comma-shaped bacterium, aka vibrions. + < 10 when isCentroidOutside && aspectRatio > 1.2f => BacteriaShape.Vibrion, + + // Spherical bacterium, aka a coccum. The count condition is based on the fourth hexagonal number. + // Huge prokaryotes aren't usually considered cocci. + < 37 when aspectRatio < 1.2f => BacteriaShape.Coccum, + + // Spindle-shaped bacterium, aka a fusiform bacterium. It's long and thin. + < 40 when aspectRatio > 3f => BacteriaShape.Fusiform, + + // Standard bacilli. These are rod-shaped bacteria that are not too long nor too large, and aren't cocci. + < 10 => BacteriaShape.Bacillum, + + // This is likely a large and amorphous prokaryote, so we avoid using standard nomenclature and fall back + // to a simple suffix. + _ => BacteriaShape.Unknown, + }; + + var lookupKey = shape switch + { + BacteriaShape.Vibrion => "vibrion", + BacteriaShape.Coccum => "coccum", + BacteriaShape.Bacillum => "bacillum", + BacteriaShape.Fusiform => "fusiform", + BacteriaShape.Unknown => string.Empty, + _ => throw new ArgumentOutOfRangeException(), + }; + + var bacteriaShapes = config.BacteriaShapes; + var genderName = GenderToString(parent); + if (!bacteriaShapes.TryGetValue(lookupKey, out var genderedSuffixes)) + { + if (shape != BacteriaShape.Unknown) + { + throw new Exception($"Bacteria shapes not mapped for key {lookupKey} in species_names.json!"); + } + + // We don't know what shape this is, so fall back to regular suffixes. + suffixes = config.Suffixes[genderName]; + + return parent; + } + + if (!genderedSuffixes.TryGetValue(genderName, out var suffixesNullable)) + { + // If the specific gender isn't mapped, we switch to a new available one. + var newKey = genderedSuffixes.Keys.ToHashSet().Random(random); + suffixes = genderedSuffixes[newKey]; + + @override = GenderFromString(newKey); + } + else + { + suffixes = suffixesNullable; + } + + return @override; + } + + public record NamingState(bool GenusIsNumbered = false, bool GenusIsProto = false, string GenusRoot = "", + GrammaticalGender Gender = GrammaticalGender.Neuter, INameGenerationTarget? Target = null) + { + public bool GenusIsNumbered = GenusIsNumbered; + public bool GenusIsProto = GenusIsProto; + public string GenusRoot = GenusRoot; + public GrammaticalGender Gender = Gender; + public INameGenerationTarget? Target = Target; + } +} diff --git a/src/microbe_stage/NameGenerator.cs b/src/microbe_stage/NameGenerator.cs index 32f234d1874..22caae8052c 100644 --- a/src/microbe_stage/NameGenerator.cs +++ b/src/microbe_stage/NameGenerator.cs @@ -1,70 +1,201 @@ using System; using System.Collections.Generic; -using System.Globalization; -using Newtonsoft.Json; -using ThriveScriptsShared; -using Xoshiro.PRNG64; +using System.Linq; +using System.Text; -public class NameGenerator : IRegistryType +/// +/// The microbe species name generator. +/// +public partial class NameGenerator(SpeciesNameConfig config) { - [JsonRequired] - public List PrefixCofix = null!; + private SpeciesNameConfig config = config; - [JsonRequired] - public List PrefixesV = null!; + private bool GenerateOrganelleBasedName(Random random, StringBuilder stringBuilder, OrganelleDefinition organelle, + int count, out string root) + { + var name = organelle.UntranslatedName.Replace(" ", "_").ToLowerInvariant().Replace("organelle_", string.Empty); - [JsonRequired] - public List PrefixesC = null!; + // Check for aliases + if (config.OrganelleMap.TryGetValue(name, out var alias)) + name = alias; - [JsonRequired] - public List CofixesV = null!; + // Generate prefix + if (!config.Organelles.TryGetValue(name, out var namingData) || + !namingData.TryGetValue("prefixes", out var prefixes)) + throw new Exception($"Invalid data for name {name}"); - [JsonRequired] - public List CofixesC = null!; + PhonotacticsFriendlyAppend(stringBuilder, prefixes.Random(random)); - [JsonRequired] - public List SuffixesV = null!; + // Generate optional root suffix + if (random.Next(3) == 1 && namingData.TryGetValue("suffixes", out var suffixes)) + PhonotacticsFriendlyAppend(stringBuilder, suffixes.Random(random)); - [JsonRequired] - public List SuffixesC = null!; + // Generate optional quantity prefix + int ruleSelector = random.Next(100); + bool isNumbered; + string quantityPrefix; + if (ruleSelector < 5) + { + if (count < 1) + { + quantityPrefix = string.Empty; + isNumbered = false; + } + else + { + // Rule 1: use explicit quantity prefix. + quantityPrefix = config.Quantity[count.ToString()].Random(random); + isNumbered = true; + } + } + else if (ruleSelector < 10 + 2 * count) + { + // Rule 2: use approximate quantifier for multiple organelles. + bool single = count == 1; + quantityPrefix = config.Quantity[single ? "1" : "multiple"].Random(random); + isNumbered = single; + } + else + { + // Do not use any quantifier. + quantityPrefix = string.Empty; + isNumbered = false; + } - /// - /// List of all suffixes - /// - [JsonIgnore] - public List Suffixes = null!; + root = stringBuilder.ToString(); - /// - /// Unused - /// - public string InternalName { get; set; } = null!; + stringBuilder.Insert(0, quantityPrefix); - /// - /// Generates a single name section - /// - public string GenerateNameSection(Random? random = null, bool lowercase = false) + return isNumbered; + } + + private bool GenerateProcessBasedName(Random random, StringBuilder stringBuilder, BioProcess process, + out string root) + { + var inputs = process.Inputs.ToArray(); + var outputs = process.Outputs.ToArray(); + var selectedInput = inputs[random.Next(inputs.Length)]; + var selectedOutput = outputs[random.Next(outputs.Length)]; + + List? outputNames = null; + if (!config.Processes.TryGetValue(selectedInput.Key.InternalName, out var inputNames) && + !config.Processes.TryGetValue(selectedOutput.Key.InternalName, out outputNames)) + { + // Both input and output are not mapped in the config. This shouldn't ever happen. + throw new Exception($"Invalid data: both input and output elements are unmapped." + + $"{selectedInput.Key.InternalName} {selectedOutput.Key.InternalName}"); + } + + var inputName = inputNames is not null ? inputNames.Random(random) : string.Empty; + var outputName = outputNames is not null ? outputNames.Random(random) : string.Empty; + + bool oneIsEmpty = inputName == string.Empty || outputName == string.Empty; + + int ruleSelector = oneIsEmpty ? 0 : random.Next(100); + int affixRule = random.Next(100); + + const int ruleOneThreshold = 80; + + if (affixRule < 5) + { + PhonotacticsFriendlyAppend(stringBuilder, config.QualityRoots["liking"].Random(random)); + } + + switch (ruleSelector) + { + case < ruleOneThreshold: + // Rule 1: consider input or output (exclusive) + GenerateProcessRuleOne(random, stringBuilder, affixRule, ruleSelector, ruleOneThreshold, inputName, + outputName); + + break; + default: + if (oneIsEmpty) + { + // Rule 1. + GenerateProcessRuleOne(random, stringBuilder, affixRule, ruleSelector, ruleOneThreshold, inputName, + outputName); + } + else + { + // Rule 2: consider both input and output + if (affixRule is >= 5 and < 20) + { + PhonotacticsFriendlyAppend(stringBuilder, config.QualityRoots["mutation"].Random(random)); + } + + PhonotacticsFriendlyAppend(stringBuilder, inputName); + PhonotacticsFriendlyAppend(stringBuilder, outputName); + + if (affixRule is >= 20 and < 25) + { + PhonotacticsFriendlyAppend(stringBuilder, config.QualitySuffixes["mutation"].Random(random)); + } + } + + break; + } + + if (affixRule > 79) + PhonotacticsFriendlyAppend(stringBuilder, config.QualitySuffixes["liking"].Random(random)); + + root = stringBuilder.ToString(); + + // Process naming is never numbered + return false; + } + + private void GenerateProcessRuleOne(Random random, StringBuilder stringBuilder, int affixRule, int ruleSelector, + int ruleOneThreshold, string inputName, string outputName) { - random ??= new XoShiRo256starstar(); + bool useInput = (inputName != string.Empty && ruleSelector < ruleOneThreshold / 2) || + outputName == string.Empty; + + if (useInput) + { + if (affixRule is >= 5 and < 10) + PhonotacticsFriendlyAppend(stringBuilder, config.QualityRoots["consuming"].Random(random)); + + PhonotacticsFriendlyAppend(stringBuilder, inputName); + + if (affixRule is >= 10 and < 25) + PhonotacticsFriendlyAppend(stringBuilder, config.QualitySuffixes["consuming"].Random(random)); + } + else + { + if (affixRule is >= 5 and < 10) + PhonotacticsFriendlyAppend(stringBuilder, config.QualityRoots["producing"].Random(random)); - string newName; + PhonotacticsFriendlyAppend(stringBuilder, outputName); + + if (affixRule is >= 10 and < 25) + PhonotacticsFriendlyAppend(stringBuilder, config.QualitySuffixes["producing"].Random(random)); + } + } + /// + /// A tweaked version of the original name generator that generates the root without the suffix to take gender + /// into account. + /// + private void ModifiedLegacyGenerateNameRoot(Random random, StringBuilder stringBuilder) + { if (random.Next(0, 100) >= 10) { switch (random.Next(0, 4)) { case 0: - newName = PrefixesC.Random(random) + SuffixesV.Random(random); + PhonotacticsFriendlyAppend(stringBuilder, config.PrefixesC.Random(random)); break; case 1: - newName = PrefixesV.Random(random) + SuffixesC.Random(random); + PhonotacticsFriendlyAppend(stringBuilder, config.PrefixesV.Random(random)); break; case 2: - newName = PrefixesV.Random(random) + CofixesC.Random(random) + - SuffixesV.Random(random); + PhonotacticsFriendlyAppend(stringBuilder, config.PrefixesV.Random(random)); + PhonotacticsFriendlyAppend(stringBuilder, config.CofixesC.Random(random)); break; case 3: - newName = PrefixesC.Random(random) + CofixesV.Random(random) + - SuffixesC.Random(random); + PhonotacticsFriendlyAppend(stringBuilder, config.PrefixesC.Random(random)); + PhonotacticsFriendlyAppend(stringBuilder, config.CofixesV.Random(random)); break; default: throw new Exception("unreachable"); @@ -74,93 +205,24 @@ public string GenerateNameSection(Random? random = null, bool lowercase = false) { // ReSharper disable once CommentTypo // Developer Easter Eggs and really silly long names here - // Our own version of wigglesoworthia for example + // Our own version of wigglesworthia for example switch (random.Next(0, 4)) { case 0: case 1: - newName = PrefixCofix.Random(random) + Suffixes.Random(random); + stringBuilder.Append(config.PrefixCofix.Random(random)); break; case 2: - newName = PrefixesV.Random(random) + CofixesC.Random(random) + - Suffixes.Random(random); + stringBuilder.Append(config.PrefixesV.Random(random)); + stringBuilder.Append(config.CofixesC.Random(random)); break; case 3: - newName = PrefixesC.Random(random) + CofixesV.Random(random) + - Suffixes.Random(random); + stringBuilder.Append(config.PrefixesC.Random(random)); + stringBuilder.Append(config.CofixesV.Random(random)); break; default: throw new Exception("unreachable"); } } - - // TODO: DO more stuff here to improve names (remove double - // letters when the prefix ends with and the cofix starts with - // the same letter Remove weird things that come up like "rc" - // (Implemented through vowels and consonants) - - // Convert first letter to lowercase - if (lowercase) - newName = char.ToLower(newName[0], CultureInfo.InvariantCulture) + newName.Substring(1); - - return newName; - } - - public void Check(string name) - { - if (PrefixCofix.Count < 1) - { - throw new InvalidRegistryDataException("NameGenerator", GetType().Name, - "PrefixCofix is empty"); - } - - if (PrefixesV.Count < 1) - { - throw new InvalidRegistryDataException("NameGenerator", GetType().Name, - "PrefixesV is empty"); - } - - if (PrefixesC.Count < 1) - { - throw new InvalidRegistryDataException("NameGenerator", GetType().Name, - "PrefixesC is empty"); - } - - if (CofixesV.Count < 1) - { - throw new InvalidRegistryDataException("NameGenerator", GetType().Name, - "CofixesV is empty"); - } - - if (CofixesC.Count < 1) - { - throw new InvalidRegistryDataException("NameGenerator", GetType().Name, - "CofixesC is empty"); - } - - if (SuffixesV.Count < 1) - { - throw new InvalidRegistryDataException("NameGenerator", GetType().Name, - "SuffixesV is empty"); - } - - if (SuffixesC.Count < 1) - { - throw new InvalidRegistryDataException("NameGenerator", GetType().Name, - "SuffixesC is empty"); - } - } - - public void Resolve(SimulationParameters parameters) - { - _ = parameters; - - Suffixes = new List(); - Suffixes.AddRange(SuffixesC); - Suffixes.AddRange(SuffixesV); - } - - public void ApplyTranslations() - { } } diff --git a/src/microbe_stage/OrganelleDefinition.cs b/src/microbe_stage/OrganelleDefinition.cs index 3839f9806b3..e6d0e58775d 100644 --- a/src/microbe_stage/OrganelleDefinition.cs +++ b/src/microbe_stage/OrganelleDefinition.cs @@ -19,7 +19,7 @@ /// /// #pragma warning disable CA1001 // Owns Godot resource that is fine to stay for the program lifetime -public class OrganelleDefinition : RegistryType, IPlayerReadableName +public class OrganelleDefinition : RegistryType, INameGenerationTarget, IPlayerReadableName #pragma warning restore CA1001 { /// diff --git a/src/microbe_stage/SpeciesNameConfig.cs b/src/microbe_stage/SpeciesNameConfig.cs new file mode 100644 index 00000000000..967cb9bda49 --- /dev/null +++ b/src/microbe_stage/SpeciesNameConfig.cs @@ -0,0 +1,168 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using ThriveScriptsShared; + +public class SpeciesNameConfig : IRegistryType +{ + [JsonRequired] + public List PrefixCofix = null!; + + [JsonRequired] + public Dictionary> Suffixes = null!; + + [JsonRequired] + public Dictionary>> BacteriaShapes = null!; + + [JsonRequired] + public Dictionary> Quantity = null!; + + [JsonRequired] + public Dictionary>> Organelles = null!; + + [JsonRequired] + public Dictionary OrganelleMap = null!; + + [JsonRequired] + public Dictionary> Processes = null!; + + [JsonRequired] + public Dictionary> QualityRoots = null!; + + [JsonRequired] + public Dictionary> QualitySuffixes = null!; + + [JsonRequired] + public Dictionary> Colours = null!; + + [JsonRequired] + public Dictionary> Membranes = null!; + + [JsonRequired] + public Dictionary> Tolerances = null!; + + // Legacy name generator species_names data + [JsonRequired] + public List PrefixesV = null!; + + [JsonRequired] + public List PrefixesC = null!; + + [JsonRequired] + public List CofixesV = null!; + + [JsonRequired] + public List CofixesC = null!; + + /// + /// Unused + /// + public string InternalName { get; set; } = null!; + + public void Check(string name) + { + if (PrefixCofix.Count < 1) + { + throw new InvalidRegistryDataException("SpeciesNameConfig", GetType().Name, + "PrefixCofix is empty"); + } + + if (Suffixes.Count < 1) + { + throw new InvalidRegistryDataException("SpeciesNameConfig", GetType().Name, + "Suffixes is empty"); + } + + if (BacteriaShapes.Count < 1) + { + throw new InvalidRegistryDataException("SpeciesNameConfig", GetType().Name, + "BacteriaShapes is empty"); + } + + if (Quantity.Count < 1) + { + throw new InvalidRegistryDataException("SpeciesNameConfig", GetType().Name, + "Quantity is empty"); + } + + if (Organelles.Count < 1) + { + throw new InvalidRegistryDataException("SpeciesNameConfig", GetType().Name, + "Organelles is empty"); + } + + if (OrganelleMap.Count < 1) + { + throw new InvalidRegistryDataException("SpeciesNameConfig", GetType().Name, + "OrganelleMap is empty"); + } + + if (Processes.Count < 1) + { + throw new InvalidRegistryDataException("SpeciesNameConfig", GetType().Name, + "Processes is empty"); + } + + if (QualityRoots.Count < 1) + { + throw new InvalidRegistryDataException("SpeciesNameConfig", GetType().Name, + "QualityRoots is empty"); + } + + if (QualitySuffixes.Count < 1) + { + throw new InvalidRegistryDataException("SpeciesNameConfig", GetType().Name, + "QualitySuffixes is empty"); + } + + if (PrefixesV.Count < 1) + { + throw new InvalidRegistryDataException("SpeciesNameConfig", GetType().Name, + "PrefixesV is empty"); + } + + if (PrefixesC.Count < 1) + { + throw new InvalidRegistryDataException("SpeciesNameConfig", GetType().Name, + "PrefixesC is empty"); + } + + if (CofixesV.Count < 1) + { + throw new InvalidRegistryDataException("SpeciesNameConfig", GetType().Name, + "CofixesV is empty"); + } + + if (CofixesC.Count < 1) + { + throw new InvalidRegistryDataException("SpeciesNameConfig", GetType().Name, + "CofixesC is empty"); + } + + if (Membranes.Count < 1) + { + throw new InvalidRegistryDataException("SpeciesNameConfig", GetType().Name, + "Membranes is empty"); + } + + if (Colours.Count < 1) + { + throw new InvalidRegistryDataException("SpeciesNameConfig", GetType().Name, + "Colours is empty"); + } + + if (Tolerances.Count < 1) + { + throw new InvalidRegistryDataException("SpeciesNameConfig", GetType().Name, + "Tolerances is empty"); + } + } + + public void Resolve(SimulationParameters simulationParameters) + { + _ = simulationParameters; + } + + public void ApplyTranslations() + { + } +} diff --git a/src/microbe_stage/editor/CellEditorComponent.cs b/src/microbe_stage/editor/CellEditorComponent.cs index ad5e2714497..1b9e1a628ff 100644 --- a/src/microbe_stage/editor/CellEditorComponent.cs +++ b/src/microbe_stage/editor/CellEditorComponent.cs @@ -1234,6 +1234,21 @@ public void ShowAutoEvoPredictionPanel(bool animate) tutorialAnimationPlayer.Play("ShowAutoEvoPrediction"); } + public string GenerateRandomName() + { + var nameGenerator = SimulationParameters.Instance.NameGenerator; + + if (previewMicrobeSpecies is not null) + { + CopyEditedPropertiesToSpecies(previewMicrobeSpecies); + + return nameGenerator.GenerateGenusName(null, null, previewMicrobeSpecies) + " " + + nameGenerator.GenerateEpithetName(null, null, previewMicrobeSpecies, null); + } + + return string.Empty; + } + /// /// Allows access to the latest edited organelles by this component. Shouldn't be modified but just read. /// diff --git a/src/microbe_stage/editor/EditorComponentBottomLeftButtons.cs b/src/microbe_stage/editor/EditorComponentBottomLeftButtons.cs index 2f6edd33f8b..ec1345d15b0 100644 --- a/src/microbe_stage/editor/EditorComponentBottomLeftButtons.cs +++ b/src/microbe_stage/editor/EditorComponentBottomLeftButtons.cs @@ -272,9 +272,8 @@ private void OnRandomizeNamePressed() if (HandleRandomSpeciesName) { - var nameGenerator = SimulationParameters.Instance.NameGenerator; - var randomizedName = nameGenerator.GenerateNameSection() + " " + - nameGenerator.GenerateNameSection(null, true); + var editor = GetParent(); + var randomizedName = editor.GenerateRandomName(); speciesNameEdit.Text = randomizedName; OnNameTextChanged(randomizedName); diff --git a/src/thriveopedia/fossilisation/FossilisationDialog.cs b/src/thriveopedia/fossilisation/FossilisationDialog.cs index a9f068c0c19..a9ba274b4b6 100644 --- a/src/thriveopedia/fossilisation/FossilisationDialog.cs +++ b/src/thriveopedia/fossilisation/FossilisationDialog.cs @@ -137,11 +137,19 @@ private void OnRandomizeNamePressed() GUICommon.Instance.PlayButtonPressSound(); var nameGenerator = SimulationParameters.Instance.NameGenerator; - var randomizedName = nameGenerator.GenerateNameSection() + " " + - nameGenerator.GenerateNameSection(null, true); - speciesNameEdit.Text = randomizedName; - OnNameTextChanged(randomizedName); + if (selectedSpecies is MicrobeSpecies microbeSpecies) + { + var randomizedName = nameGenerator.GenerateGenusName(null, null, microbeSpecies) + + nameGenerator.GenerateEpithetName(null, null, microbeSpecies, null); + + speciesNameEdit.Text = randomizedName; + OnNameTextChanged(randomizedName); + } + else + { + throw new Exception("Name generation for the selected species has not been implemented yet."); + } } private void OnCancelPressed()