initiative_core/world/npc/species/
mod.rs

1mod dragonborn;
2mod dwarf;
3mod elf;
4mod gnome;
5mod half_elf;
6mod half_orc;
7mod halfling;
8mod human;
9mod tiefling;
10
11use super::{Age, Ethnicity, Gender, NpcData, Size};
12use initiative_macros::WordList;
13use rand::prelude::*;
14use rand_distr::{Distribution, Normal};
15use serde::{Deserialize, Serialize};
16use std::fmt;
17use std::ops::RangeInclusive;
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, WordList, Serialize, Deserialize)]
20#[serde(into = "&'static str", try_from = "&str")]
21pub enum Species {
22    Dragonborn,
23    Dwarf,
24    Elf,
25    Gnome,
26
27    #[alias = "half elf"]
28    HalfElf,
29
30    #[alias = "half orc"]
31    HalfOrc,
32    Halfling,
33    Human,
34    Tiefling,
35}
36
37trait Generate {
38    fn regenerate(rng: &mut impl Rng, npc: &mut NpcData) {
39        npc.gender.replace_with(|_| Self::gen_gender(rng));
40
41        match (npc.age.is_locked(), npc.age_years.is_locked()) {
42            (false, false) => {
43                let age_years = Self::gen_age_years(rng);
44                npc.age_years.replace(age_years);
45                npc.age.replace_with(|_| Self::age_from_years(age_years));
46            }
47            (false, true) => {
48                npc.age
49                    .replace(Self::age_from_years(*npc.age_years.value().unwrap()));
50            }
51            (true, false) => {
52                npc.age_years
53                    .replace(Self::gen_years_from_age(rng, npc.age.value().unwrap()));
54            }
55            (true, true) => {}
56        }
57
58        if let Some(years) = npc.age_years.value() {
59            npc.age.replace_with(|_| Self::age_from_years(*years));
60        } else {
61            npc.age.clear();
62        }
63
64        if let (Some(gender), Some(age_years)) = (npc.gender.value(), npc.age_years.value()) {
65            npc.size
66                .replace_with(|_| Self::gen_size(rng, *age_years, gender));
67        }
68    }
69
70    fn gen_gender(rng: &mut impl Rng) -> Gender;
71
72    fn gen_age_years(rng: &mut impl Rng) -> u16;
73
74    fn gen_years_from_age(rng: &mut impl Rng, age: &Age) -> u16;
75
76    fn age_from_years(years: u16) -> Age;
77
78    fn gen_size(rng: &mut impl Rng, age_years: u16, gender: &Gender) -> Size;
79}
80
81pub fn regenerate(rng: &mut impl Rng, npc: &mut NpcData) {
82    if let Some(species) = npc.species.value() {
83        match species {
84            Species::Dragonborn => dragonborn::Species::regenerate(rng, npc),
85            Species::Dwarf => dwarf::Species::regenerate(rng, npc),
86            Species::Elf => elf::Species::regenerate(rng, npc),
87            Species::Gnome => gnome::Species::regenerate(rng, npc),
88            Species::HalfElf => half_elf::Species::regenerate(rng, npc),
89            Species::HalfOrc => half_orc::Species::regenerate(rng, npc),
90            Species::Halfling => halfling::Species::regenerate(rng, npc),
91            Species::Human => human::Species::regenerate(rng, npc),
92            Species::Tiefling => tiefling::Species::regenerate(rng, npc),
93        }
94    }
95}
96
97fn gen_height_weight(
98    rng: &mut impl Rng,
99    height_range: RangeInclusive<f32>,
100    bmi_range: RangeInclusive<f32>,
101) -> (u16, u16) {
102    let height = {
103        let mean = (height_range.end() + height_range.start()) / 2.;
104        let std_dev = mean - height_range.start();
105        Normal::new(mean, std_dev).unwrap().sample(rng)
106    };
107
108    let bmi = {
109        let mean = (bmi_range.end() + bmi_range.start()) / 2.;
110        let std_dev = mean - bmi_range.start();
111        Normal::new(mean, std_dev).unwrap().sample(rng)
112    };
113
114    let weight = bmi * height * height / 703.;
115
116    (height as u16, weight as u16)
117}
118
119impl Species {
120    pub fn default_ethnicity(&self) -> Ethnicity {
121        match self {
122            Self::Dragonborn => Ethnicity::Dragonborn,
123            Self::Dwarf => Ethnicity::Dwarvish,
124            Self::Elf => Ethnicity::Elvish,
125            Self::Gnome => Ethnicity::Gnomish,
126            Self::HalfElf => Ethnicity::Human,
127            Self::HalfOrc => Ethnicity::Orcish,
128            Self::Halfling => Ethnicity::Halfling,
129            Self::Human => Ethnicity::Human,
130            Self::Tiefling => Ethnicity::Tiefling,
131        }
132    }
133}
134
135impl fmt::Display for Species {
136    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
137        match self {
138            Self::Dragonborn => write!(f, "dragonborn"),
139            Self::Dwarf => write!(f, "dwarf"),
140            Self::Elf => write!(f, "elf"),
141            Self::Gnome => write!(f, "gnome"),
142            Self::HalfElf => write!(f, "half-elf"),
143            Self::HalfOrc => write!(f, "half-orc"),
144            Self::Halfling => write!(f, "halfling"),
145            Self::Human => write!(f, "human"),
146            Self::Tiefling => write!(f, "tiefling"),
147        }
148    }
149}
150
151#[cfg(test)]
152mod test {
153    use super::*;
154    use crate::world::Field;
155
156    #[test]
157    fn regenerate_test_default() {
158        let mut npc = NpcData {
159            species: Field::new_generated(Species::Human),
160            ..Default::default()
161        };
162        let mut rng = SmallRng::seed_from_u64(0);
163
164        regenerate(&mut rng, &mut npc);
165
166        assert!(npc.age.is_some());
167        assert!(npc.age_years.is_some());
168        assert!(npc.gender.is_some());
169        assert!(npc.size.is_some());
170    }
171
172    #[test]
173    fn regenerate_test_locked() {
174        let mut npc = NpcData {
175            species: Species::Human.into(),
176            age: Age::Adult.into(),
177            age_years: u16::MAX.into(),
178            gender: Gender::Neuter.into(),
179            size: Size::Tiny {
180                height: u16::MAX,
181                weight: u16::MAX,
182            }
183            .into(),
184            ..Default::default()
185        };
186
187        let mut rng = SmallRng::seed_from_u64(0);
188
189        regenerate(&mut rng, &mut npc);
190
191        assert_eq!(Some(&Age::Adult), npc.age.value());
192        assert_eq!(Some(&u16::MAX), npc.age_years.value());
193        assert_eq!(Some(&Gender::Neuter), npc.gender.value());
194        assert_eq!(
195            Some(&Size::Tiny {
196                height: u16::MAX,
197                weight: u16::MAX
198            }),
199            npc.size.value(),
200        );
201    }
202
203    #[test]
204    fn regenerate_test_age_years_provided() {
205        let mut npc = NpcData {
206            species: Species::Human.into(),
207            age_years: u16::MAX.into(),
208            ..Default::default()
209        };
210
211        let mut rng = SmallRng::seed_from_u64(0);
212
213        regenerate(&mut rng, &mut npc);
214
215        assert_eq!(Some(&Age::Geriatric), npc.age.value());
216    }
217
218    #[test]
219    fn gen_height_weight_test() {
220        let mut rng = SmallRng::seed_from_u64(0);
221
222        assert_eq!(
223            (72, 147),
224            gen_height_weight(&mut rng, 72.0..=72.0, 20.0..=20.0),
225        );
226
227        assert_eq!(
228            vec![
229                (71, 153),
230                (69, 180),
231                (66, 133),
232                (65, 146),
233                (64, 154),
234                (67, 115),
235                (68, 125),
236                (65, 119),
237                (67, 162),
238                (66, 118),
239            ],
240            (0..10)
241                .map(|_| gen_height_weight(&mut rng, 64.0..=68.0, 18.5..=25.0))
242                .collect::<Vec<(u16, u16)>>(),
243        );
244    }
245
246    #[test]
247    fn default_ethnicity_test() {
248        assert_eq!(
249            Ethnicity::Dragonborn,
250            Species::Dragonborn.default_ethnicity(),
251        );
252        assert_eq!(Ethnicity::Dwarvish, Species::Dwarf.default_ethnicity());
253        assert_eq!(Ethnicity::Elvish, Species::Elf.default_ethnicity());
254        assert_eq!(Ethnicity::Gnomish, Species::Gnome.default_ethnicity());
255        assert_eq!(Ethnicity::Human, Species::HalfElf.default_ethnicity());
256        assert_eq!(Ethnicity::Orcish, Species::HalfOrc.default_ethnicity());
257        assert_eq!(Ethnicity::Halfling, Species::Halfling.default_ethnicity());
258        assert_eq!(Ethnicity::Human, Species::Human.default_ethnicity());
259        assert_eq!(Ethnicity::Tiefling, Species::Tiefling.default_ethnicity());
260    }
261
262    #[test]
263    fn try_from_test() {
264        assert_eq!(Ok(Species::Dragonborn), "dragonborn".parse());
265        assert_eq!(Ok(Species::HalfElf), "half elf".parse());
266        assert_eq!(Ok(Species::HalfElf), "half-elf".parse());
267        assert_eq!(Err(()), "potato".parse::<Species>());
268    }
269
270    #[test]
271    fn fmt_test() {
272        assert_eq!("dragonborn", format!("{}", Species::Dragonborn));
273        assert_eq!("dwarf", format!("{}", Species::Dwarf));
274        assert_eq!("elf", format!("{}", Species::Elf));
275        assert_eq!("gnome", format!("{}", Species::Gnome));
276        assert_eq!("halfling", format!("{}", Species::Halfling));
277        assert_eq!("human", format!("{}", Species::Human));
278        assert_eq!("tiefling", format!("{}", Species::Tiefling));
279    }
280
281    #[test]
282    fn serialize_deserialize_test() {
283        assert_eq!("\"human\"", serde_json::to_string(&Species::Human).unwrap());
284
285        let value: Species = serde_json::from_str("\"human\"").unwrap();
286        assert_eq!(Species::Human, value);
287    }
288}