1use crate::{
2 assets::{AssetExt, AssetHandle},
3 comp::{self, AllBodies, Body, body},
4};
5use lazy_static::lazy_static;
6use rand::seq::SliceRandom;
7use serde::Deserialize;
8use std::str::FromStr;
9
10#[derive(Clone, Copy, PartialEq, Eq)]
11pub enum NpcKind {
12 Humanoid,
13 Wolf,
14 Pig,
15 Duck,
16 Phoenix,
17 Clownfish,
18 Marlin,
19 Ogre,
20 Gnome,
21 Archaeos,
22 StoneGolem,
23 Reddragon,
24 Crocodile,
25 Tarantula,
26 Crab,
27 Plugin,
28}
29
30pub const ALL_NPCS: [NpcKind; 16] = [
31 NpcKind::Humanoid,
32 NpcKind::Wolf,
33 NpcKind::Pig,
34 NpcKind::Duck,
35 NpcKind::Phoenix,
36 NpcKind::Clownfish,
37 NpcKind::Marlin,
38 NpcKind::Ogre,
39 NpcKind::Gnome,
40 NpcKind::Archaeos,
41 NpcKind::StoneGolem,
42 NpcKind::Reddragon,
43 NpcKind::Crocodile,
44 NpcKind::Tarantula,
45 NpcKind::Crab,
46 NpcKind::Plugin,
47];
48
49#[derive(Clone, Debug, Deserialize)]
53pub struct BodyNames {
54 pub keyword: String,
57 pub names_0: Vec<String>,
61 pub names_1: Option<Vec<String>>,
62}
63
64#[derive(Clone, Debug, Deserialize)]
68pub struct SpeciesNames {
69 pub keyword: String,
73 pub generic: String,
75}
76
77pub type NpcNames = AllBodies<BodyNames, SpeciesNames>;
79
80lazy_static! {
81 pub static ref NPC_NAMES: AssetHandle<NpcNames> = NpcNames::load_expect("common.npc_names");
82}
83
84impl FromStr for NpcKind {
85 type Err = ();
86
87 fn from_str(s: &str) -> Result<Self, ()> {
88 let npc_names = &*NPC_NAMES.read();
89 ALL_NPCS
90 .iter()
91 .copied()
92 .find(|&npc| npc_names[npc].keyword == s)
93 .ok_or(())
94 }
95}
96
97pub fn get_npc_name(npc_type: NpcKind, body_type: Option<BodyType>) -> String {
98 let npc_names = NPC_NAMES.read();
99 let BodyNames {
100 keyword,
101 names_0,
102 names_1,
103 } = &npc_names[npc_type];
104
105 match body_type {
107 Some(BodyType::Male) => names_0
108 .choose(&mut rand::thread_rng())
109 .unwrap_or(keyword)
110 .clone(),
111 Some(BodyType::Female) if names_1.is_some() => {
112 names_1
113 .as_ref()
114 .unwrap() .choose(&mut rand::thread_rng())
116 .unwrap_or(keyword)
117 .clone()
118 },
119 _ => names_0
120 .choose(&mut rand::thread_rng())
121 .unwrap_or(keyword)
122 .clone(),
123 }
124}
125
126pub fn kind_to_body(kind: NpcKind) -> Body {
128 match kind {
129 NpcKind::Humanoid => comp::humanoid::Body::random().into(),
130 NpcKind::Pig => comp::quadruped_small::Body::random().into(),
131 NpcKind::Wolf => comp::quadruped_medium::Body::random().into(),
132 NpcKind::Duck => comp::bird_medium::Body::random().into(),
133 NpcKind::Phoenix => comp::bird_large::Body::random().into(),
134 NpcKind::Clownfish => comp::fish_small::Body::random().into(),
135 NpcKind::Marlin => comp::fish_medium::Body::random().into(),
136 NpcKind::Ogre => comp::biped_large::Body::random().into(),
137 NpcKind::Gnome => comp::biped_small::Body::random().into(),
138 NpcKind::Archaeos => comp::theropod::Body::random().into(),
139 NpcKind::StoneGolem => comp::golem::Body::random().into(),
140 NpcKind::Reddragon => comp::dragon::Body::random().into(),
141 NpcKind::Crocodile => comp::quadruped_low::Body::random().into(),
142 NpcKind::Tarantula => comp::arthropod::Body::random().into(),
143 NpcKind::Crab => comp::crustacean::Body::random().into(),
144 NpcKind::Plugin => comp::plugin::Body::random().into(),
145 }
146}
147
148pub struct NpcBody(pub NpcKind, pub Box<dyn FnMut() -> Body>);
159
160impl FromStr for NpcBody {
161 type Err = ();
162
163 fn from_str(s: &str) -> Result<Self, Self::Err> { Self::from_str_with(s, kind_to_body) }
168}
169
170impl NpcBody {
171 #[expect(clippy::result_unit_err)]
175 pub fn from_str_with(s: &str, kind_to_body: fn(NpcKind) -> Body) -> Result<Self, ()> {
176 fn parse<
177 'a,
178 B: Into<Body> + 'static,
179 Species: 'static,
182 BodyMeta,
183 SpeciesData: for<'b> core::ops::Index<&'b Species, Output = SpeciesNames>,
184 >(
185 s: &str,
186 npc_kind: NpcKind,
187 body_data: &'a comp::BodyData<BodyMeta, SpeciesData>,
188 conv_func: for<'d> fn(&mut rand::rngs::ThreadRng, &'d Species) -> B,
189 ) -> Option<NpcBody>
190 where
191 &'a SpeciesData: IntoIterator<Item = Species>,
192 {
193 let npc_names = &body_data.species;
194 body_data
195 .species
196 .into_iter()
197 .find(|species| npc_names[species].keyword == s)
198 .map(|species| {
199 NpcBody(
200 npc_kind,
201 Box::new(move || conv_func(&mut rand::thread_rng(), &species).into()),
202 )
203 })
204 }
205 let npc_names = &*NPC_NAMES.read();
206 NpcKind::from_str(s)
208 .map(|kind| NpcBody(kind, Box::new(move || kind_to_body(kind))))
209 .ok()
210 .or_else(|| {
212 parse(
213 s,
214 NpcKind::Humanoid,
215 &npc_names.humanoid,
216 comp::humanoid::Body::random_with,
217 )
218 })
219 .or_else(|| {
220 parse(
221 s,
222 NpcKind::Pig,
223 &npc_names.quadruped_small,
224 comp::quadruped_small::Body::random_with,
225 )
226 })
227 .or_else(|| {
228 parse(
229 s,
230 NpcKind::Wolf,
231 &npc_names.quadruped_medium,
232 comp::quadruped_medium::Body::random_with,
233 )
234 })
235 .or_else(|| {
236 parse(
237 s,
238 NpcKind::Duck,
239 &npc_names.bird_medium,
240 comp::bird_medium::Body::random_with,
241 )
242 })
243 .or_else(|| {
244 parse(
245 s,
246 NpcKind::Phoenix,
247 &npc_names.bird_large,
248 comp::bird_large::Body::random_with,
249 )
250 })
251 .or_else(|| {
252 parse(
253 s,
254 NpcKind::Clownfish,
255 &npc_names.fish_small,
256 comp::fish_small::Body::random_with,
257 )
258 })
259 .or_else(|| {
260 parse(
261 s,
262 NpcKind::Marlin,
263 &npc_names.fish_medium,
264 comp::fish_medium::Body::random_with,
265 )
266 })
267 .or_else(|| {
268 parse(
269 s,
270 NpcKind::Ogre,
271 &npc_names.biped_large,
272 comp::biped_large::Body::random_with,
273 )
274 })
275 .or_else(|| {
276 parse(
277 s,
278 NpcKind::Gnome,
279 &npc_names.biped_small,
280 comp::biped_small::Body::random_with,
281 )
282 })
283 .or_else(|| {
284 parse(
285 s,
286 NpcKind::Archaeos,
287 &npc_names.theropod,
288 comp::theropod::Body::random_with,
289 )
290 })
291 .or_else(|| {
292 parse(
293 s,
294 NpcKind::StoneGolem,
295 &npc_names.golem,
296 comp::golem::Body::random_with,
297 )
298 })
299 .or_else(|| {
300 parse(
301 s,
302 NpcKind::Reddragon,
303 &npc_names.dragon,
304 comp::dragon::Body::random_with,
305 )
306 })
307 .or_else(|| {
308 parse(
309 s,
310 NpcKind::Crocodile,
311 &npc_names.quadruped_low,
312 comp::quadruped_low::Body::random_with,
313 )
314 })
315 .or_else(|| {
316 parse(
317 s,
318 NpcKind::Tarantula,
319 &npc_names.arthropod,
320 comp::arthropod::Body::random_with,
321 )
322 })
323 .or_else(|| {
324 parse(
325 s,
326 NpcKind::Crab,
327 &npc_names.crustacean,
328 comp::crustacean::Body::random_with,
329 )
330 })
331 .or_else(|| {
332 parse(
333 s,
334 NpcKind::Plugin,
335 &npc_names.plugin,
336 comp::plugin::Body::random_with,
337 )
338 })
339 .or_else(|| crate::comp::body::plugin::parse_name(s))
340 .ok_or(())
341 }
342}
343
344pub enum BodyType {
345 Male,
346 Female,
347}
348
349impl BodyType {
350 pub fn from_body(body: Body) -> Option<BodyType> {
351 match body {
352 Body::Humanoid(humanoid) => match humanoid.body_type {
353 body::humanoid::BodyType::Male => Some(BodyType::Male),
354 body::humanoid::BodyType::Female => Some(BodyType::Female),
355 },
356 _ => None,
357 }
358 }
359}