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