1mod dragonborn;
2mod dwarvish;
3mod elvish;
4mod gnomish;
5mod halfling;
6mod human;
7mod orcish;
8mod tiefling;
9
10use super::{Age, Gender, NpcData, Species};
11use crate::world::weighted_index_from_tuple;
12use initiative_macros::WordList;
13use rand::Rng;
14use serde::{Deserialize, Serialize};
15use std::fmt;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, WordList, Serialize, Deserialize)]
18#[serde(into = "&'static str", try_from = "&str")]
19pub enum Ethnicity {
20 Dragonborn,
21 Dwarvish,
22 Elvish,
23 Gnomish,
24 Orcish,
25 Halfling,
26 Human,
27 Tiefling,
28}
29
30impl Ethnicity {
31 pub fn default_species(&self) -> Species {
32 match self {
33 Self::Human => Species::Human,
34 Self::Dragonborn => Species::Dragonborn,
35 Self::Dwarvish => Species::Dwarf,
36 Self::Elvish => Species::Elf,
37 Self::Gnomish => Species::Gnome,
38 Self::Orcish => Species::HalfOrc,
39 Self::Halfling => Species::Halfling,
40 Self::Tiefling => Species::Tiefling,
41 }
42 }
43}
44
45trait Generate {
46 fn regenerate(rng: &mut impl Rng, npc: &mut NpcData) {
47 if let (Some(gender), Some(age)) = (npc.gender.value(), npc.age.value()) {
48 npc.name.replace_with(|_| Self::gen_name(rng, age, gender));
49 }
50 }
51
52 fn gen_name(rng: &mut impl Rng, age: &Age, gender: &Gender) -> String;
53}
54
55trait GenerateSimple {
56 fn gen_fname_simple(rng: &mut impl Rng, gender: &Gender) -> String {
57 gen_name(
58 rng,
59 match gender {
60 Gender::Feminine => Self::syllable_fname_count_f(),
61 Gender::Masculine => Self::syllable_fname_count_m(),
62 _ => Self::syllable_fname_count(),
63 },
64 match gender {
65 Gender::Feminine => Self::syllable_fname_first_f(),
66 Gender::Masculine => Self::syllable_fname_first_m(),
67 _ => Self::syllable_fname_first(),
68 },
69 Self::syllable_fname_middle(),
70 match gender {
71 Gender::Feminine => Self::syllable_fname_last_f(),
72 Gender::Masculine => Self::syllable_fname_last_m(),
73 _ => Self::syllable_fname_last(),
74 },
75 )
76 }
77
78 fn gen_lname_simple(rng: &mut impl Rng) -> String {
79 if rng.gen_bool(Self::compound_word_probability()) {
80 format!(
81 "{}{}",
82 weighted_index_from_tuple(rng, Self::word_lname_first()),
83 weighted_index_from_tuple(rng, Self::word_lname_last())
84 )
85 } else {
86 gen_name(
87 rng,
88 Self::syllable_lname_count(),
89 Self::syllable_lname_first(),
90 Self::syllable_lname_middle(),
91 Self::syllable_lname_last(),
92 )
93 }
94 }
95
96 fn syllable_fname_count_f() -> &'static [(u8, usize)];
97 fn syllable_fname_first_f() -> &'static [(&'static str, usize)];
98 fn syllable_fname_last_f() -> &'static [(&'static str, usize)];
99 fn syllable_fname_count_m() -> &'static [(u8, usize)];
100 fn syllable_fname_first_m() -> &'static [(&'static str, usize)];
101 fn syllable_fname_last_m() -> &'static [(&'static str, usize)];
102 fn syllable_fname_count() -> &'static [(u8, usize)];
103 fn syllable_fname_first() -> &'static [(&'static str, usize)];
104 fn syllable_fname_last() -> &'static [(&'static str, usize)];
105 fn syllable_fname_middle() -> &'static [(&'static str, usize)];
106 fn syllable_lname_count() -> &'static [(u8, usize)];
107 fn syllable_lname_first() -> &'static [(&'static str, usize)];
108 fn syllable_lname_middle() -> &'static [(&'static str, usize)];
109 fn syllable_lname_last() -> &'static [(&'static str, usize)];
110 fn compound_word_probability() -> f64;
111 fn word_lname_first() -> &'static [(&'static str, usize)];
112 fn word_lname_last() -> &'static [(&'static str, usize)];
113}
114
115pub fn regenerate(rng: &mut impl Rng, npc: &mut NpcData) {
116 if let Some(ethnicity) = npc.ethnicity.value() {
117 match ethnicity {
118 Ethnicity::Dragonborn => dragonborn::Ethnicity::regenerate(rng, npc),
119 Ethnicity::Dwarvish => dwarvish::Ethnicity::regenerate(rng, npc),
120 Ethnicity::Elvish => elvish::Ethnicity::regenerate(rng, npc),
121 Ethnicity::Gnomish => gnomish::Ethnicity::regenerate(rng, npc),
122 Ethnicity::Orcish => orcish::Ethnicity::regenerate(rng, npc),
123 Ethnicity::Halfling => halfling::Ethnicity::regenerate(rng, npc),
124 Ethnicity::Human => human::Ethnicity::regenerate(rng, npc),
125 Ethnicity::Tiefling => tiefling::Ethnicity::regenerate(rng, npc),
126 }
127 }
128}
129
130impl fmt::Display for Ethnicity {
131 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
132 match self {
133 Self::Dragonborn => write!(f, "dragonborn"),
134 Self::Dwarvish => write!(f, "dwarvish"),
135 Self::Elvish => write!(f, "elvish"),
136 Self::Gnomish => write!(f, "gnomish"),
137 Self::Orcish => write!(f, "orcish"),
138 Self::Halfling => write!(f, "halfling"),
139 Self::Human => write!(f, "human"),
140 Self::Tiefling => write!(f, "tiefling"),
141 }
142 }
143}
144
145fn gen_name(
146 rng: &mut impl Rng,
147 syllable_count_dist: &[(u8, usize)],
148 start_dist: &[(&str, usize)],
149 mid_dist: &[(&str, usize)],
150 end_dist: &[(&str, usize)],
151) -> String {
152 let syllable_count = *weighted_index_from_tuple(rng, syllable_count_dist);
153 if syllable_count < 2 {
154 panic!("Expected at least two syllables.");
155 }
156
157 let mut result = weighted_index_from_tuple(rng, start_dist).to_string();
158 for _ in 2..syllable_count {
159 #[expect(clippy::explicit_auto_deref)]
161 result.push_str(*weighted_index_from_tuple(rng, mid_dist));
162 }
163 #[expect(clippy::explicit_auto_deref)]
164 result.push_str(*weighted_index_from_tuple(rng, end_dist));
165 result
166}
167
168#[cfg(test)]
169pub mod test_utils {
170 use super::*;
171 use crate::test_utils as test;
172
173 pub fn gen_name(rng: &mut impl Rng, ethnicity: Ethnicity, age: Age, gender: Gender) -> String {
174 let mut npc = test::npc()
175 .age(age)
176 .ethnicity(ethnicity)
177 .gender(gender)
178 .build();
179 regenerate(rng, &mut npc);
180 format!("{}", npc.name)
181 }
182}
183
184#[cfg(test)]
185mod test {
186 use super::*;
187 use rand::prelude::*;
188
189 #[test]
190 fn default_species_test() {
191 assert_eq!(Species::Dragonborn, Ethnicity::Dragonborn.default_species());
192 assert_eq!(Species::Dwarf, Ethnicity::Dwarvish.default_species());
193 assert_eq!(Species::Elf, Ethnicity::Elvish.default_species());
194 assert_eq!(Species::Gnome, Ethnicity::Gnomish.default_species());
195 assert_eq!(Species::HalfOrc, Ethnicity::Orcish.default_species());
196 assert_eq!(Species::Halfling, Ethnicity::Halfling.default_species());
197 assert_eq!(Species::Human, Ethnicity::Human.default_species());
198 assert_eq!(Species::Tiefling, Ethnicity::Tiefling.default_species());
199 }
200
201 #[test]
202 fn serialize_deserialize_test() {
203 assert_eq!(
204 "\"elvish\"",
205 serde_json::to_string(&Ethnicity::Elvish).unwrap()
206 );
207
208 let value: Ethnicity = serde_json::from_str("\"elvish\"").unwrap();
209 assert_eq!(Ethnicity::Elvish, value);
210 }
211
212 #[test]
213 fn generate_name_test() {
214 let mut rng = SmallRng::seed_from_u64(0);
215 let syllable_count_dist = &[(2, 2), (3, 3), (4, 1)][..];
216 let start_dist = &[("Ta", 1), ("Te", 2), ("To", 3)][..];
217 let mid_dist = &[("la", 1), ("le", 2), ("lo", 3)][..];
218 let end_dist = &[("ra", 1), ("ro", 2), ("ri", 3)][..];
219
220 assert_eq!(
221 [
222 "Telori", "Telero", "Toro", "Tori", "Teleri", "Tolaro", "Taleloro", "Toro", "Tori",
223 "Teloro", "Talari", "Tori", "Teri", "Tolara", "Taloro", "Tolori", "Tololoro",
224 "Teleri", "Tolelero", "Tori",
225 ]
226 .iter()
227 .map(|s| s.to_string())
228 .collect::<Vec<_>>(),
229 (0..20)
230 .map(|_| gen_name(
231 &mut rng,
232 syllable_count_dist,
233 start_dist,
234 mid_dist,
235 end_dist
236 ))
237 .collect::<Vec<_>>(),
238 );
239 }
240}