initiative_core/world/npc/ethnicity/
mod.rs

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        // following Clippy's advice leads to a compile error as of 1.65
160        #[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}