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}