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::IndexedRandom;
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.choose(&mut rand::rng()).unwrap_or(keyword).clone(),
112 Some(BodyType::Female) if names_1.is_some() => {
113 names_1
114 .as_ref()
115 .unwrap() .choose(&mut rand::rng())
117 .unwrap_or(keyword)
118 .clone()
119 },
120 _ => names_0.choose(&mut rand::rng()).unwrap_or(keyword).clone(),
121 }
122}
123
124pub fn kind_to_body(kind: NpcKind) -> Body {
126 match kind {
127 NpcKind::Humanoid => comp::humanoid::Body::random().into(),
128 NpcKind::Pig => comp::quadruped_small::Body::random().into(),
129 NpcKind::Wolf => comp::quadruped_medium::Body::random().into(),
130 NpcKind::Duck => comp::bird_medium::Body::random().into(),
131 NpcKind::Phoenix => comp::bird_large::Body::random().into(),
132 NpcKind::Clownfish => comp::fish_small::Body::random().into(),
133 NpcKind::Marlin => comp::fish_medium::Body::random().into(),
134 NpcKind::Ogre => comp::biped_large::Body::random().into(),
135 NpcKind::Gnome => comp::biped_small::Body::random().into(),
136 NpcKind::Archaeos => comp::theropod::Body::random().into(),
137 NpcKind::StoneGolem => comp::golem::Body::random().into(),
138 NpcKind::Reddragon => comp::dragon::Body::random().into(),
139 NpcKind::Crocodile => comp::quadruped_low::Body::random().into(),
140 NpcKind::Tarantula => comp::arthropod::Body::random().into(),
141 NpcKind::Crab => comp::crustacean::Body::random().into(),
142 NpcKind::Plugin => comp::plugin::Body::random().into(),
143 }
144}
145
146pub struct NpcBody(pub NpcKind, pub Box<dyn FnMut() -> Body>);
157
158impl FromStr for NpcBody {
159 type Err = ();
160
161 fn from_str(s: &str) -> Result<Self, Self::Err> { Self::from_str_with(s, kind_to_body) }
166}
167
168impl NpcBody {
169 #[expect(clippy::result_unit_err)]
173 pub fn from_str_with(s: &str, kind_to_body: fn(NpcKind) -> Body) -> Result<Self, ()> {
174 fn parse<
175 'a,
176 B: Into<Body> + 'static,
177 Species: 'static,
180 BodyMeta,
181 SpeciesData: for<'b> core::ops::Index<&'b Species, Output = SpeciesNames>,
182 >(
183 s: &str,
184 npc_kind: NpcKind,
185 body_data: &'a comp::BodyData<BodyMeta, SpeciesData>,
186 conv_func: for<'d> fn(&mut rand::rngs::ThreadRng, &'d Species) -> B,
187 ) -> Option<NpcBody>
188 where
189 &'a SpeciesData: IntoIterator<Item = Species>,
190 {
191 let npc_names = &body_data.species;
192 body_data
193 .species
194 .into_iter()
195 .find(|species| npc_names[species].keyword == s)
196 .map(|species| {
197 NpcBody(
198 npc_kind,
199 Box::new(move || conv_func(&mut rand::rng(), &species).into()),
200 )
201 })
202 }
203 let npc_names = &*NPC_NAMES.read();
204 NpcKind::from_str(s)
206 .map(|kind| NpcBody(kind, Box::new(move || kind_to_body(kind))))
207 .ok()
208 .or_else(|| {
210 parse(
211 s,
212 NpcKind::Humanoid,
213 &npc_names.humanoid,
214 comp::humanoid::Body::random_with,
215 )
216 })
217 .or_else(|| {
218 parse(
219 s,
220 NpcKind::Pig,
221 &npc_names.quadruped_small,
222 comp::quadruped_small::Body::random_with,
223 )
224 })
225 .or_else(|| {
226 parse(
227 s,
228 NpcKind::Wolf,
229 &npc_names.quadruped_medium,
230 comp::quadruped_medium::Body::random_with,
231 )
232 })
233 .or_else(|| {
234 parse(
235 s,
236 NpcKind::Duck,
237 &npc_names.bird_medium,
238 comp::bird_medium::Body::random_with,
239 )
240 })
241 .or_else(|| {
242 parse(
243 s,
244 NpcKind::Phoenix,
245 &npc_names.bird_large,
246 comp::bird_large::Body::random_with,
247 )
248 })
249 .or_else(|| {
250 parse(
251 s,
252 NpcKind::Clownfish,
253 &npc_names.fish_small,
254 comp::fish_small::Body::random_with,
255 )
256 })
257 .or_else(|| {
258 parse(
259 s,
260 NpcKind::Marlin,
261 &npc_names.fish_medium,
262 comp::fish_medium::Body::random_with,
263 )
264 })
265 .or_else(|| {
266 parse(
267 s,
268 NpcKind::Ogre,
269 &npc_names.biped_large,
270 comp::biped_large::Body::random_with,
271 )
272 })
273 .or_else(|| {
274 parse(
275 s,
276 NpcKind::Gnome,
277 &npc_names.biped_small,
278 comp::biped_small::Body::random_with,
279 )
280 })
281 .or_else(|| {
282 parse(
283 s,
284 NpcKind::Archaeos,
285 &npc_names.theropod,
286 comp::theropod::Body::random_with,
287 )
288 })
289 .or_else(|| {
290 parse(
291 s,
292 NpcKind::StoneGolem,
293 &npc_names.golem,
294 comp::golem::Body::random_with,
295 )
296 })
297 .or_else(|| {
298 parse(
299 s,
300 NpcKind::Reddragon,
301 &npc_names.dragon,
302 comp::dragon::Body::random_with,
303 )
304 })
305 .or_else(|| {
306 parse(
307 s,
308 NpcKind::Crocodile,
309 &npc_names.quadruped_low,
310 comp::quadruped_low::Body::random_with,
311 )
312 })
313 .or_else(|| {
314 parse(
315 s,
316 NpcKind::Tarantula,
317 &npc_names.arthropod,
318 comp::arthropod::Body::random_with,
319 )
320 })
321 .or_else(|| {
322 parse(
323 s,
324 NpcKind::Crab,
325 &npc_names.crustacean,
326 comp::crustacean::Body::random_with,
327 )
328 })
329 .or_else(|| {
330 parse(
331 s,
332 NpcKind::Plugin,
333 &npc_names.plugin,
334 comp::plugin::Body::random_with,
335 )
336 })
337 .or_else(|| crate::comp::body::plugin::parse_name(s))
338 .ok_or(())
339 }
340}
341
342pub enum BodyType {
343 Male,
344 Female,
345}
346
347impl BodyType {
348 pub fn from_body(body: Body) -> Option<BodyType> {
349 match body {
350 Body::Humanoid(humanoid) => match humanoid.body_type {
351 body::humanoid::BodyType::Male => Some(BodyType::Male),
352 body::humanoid::BodyType::Female => Some(BodyType::Female),
353 },
354 _ => None,
355 }
356 }
357}