veloren_common/
npc.rs

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/// Body-specific NPC name metadata.
50///
51/// NOTE: Deliberately don't (yet?) implement serialize.
52#[derive(Clone, Debug, Deserialize)]
53pub struct BodyNames {
54    /// The keyword used to refer to this body type (e.g. via the command
55    /// console).  Should be unique per body type.
56    pub keyword: String,
57    /// A list of canonical names for NPCs with this body types (currently used
58    /// when spawning this kind of NPC from the console).  Going forward,
59    /// these names will likely be split up by species.
60    pub names_0: Vec<String>,
61    pub names_1: Option<Vec<String>>,
62}
63
64/// Species-specific NPC name metadata.
65///
66/// NOTE: Deliberately don't (yet?) implement serialize.
67#[derive(Clone, Debug, Deserialize)]
68pub struct SpeciesNames {
69    /// The keyword used to refer to this species (e.g. via the command
70    /// console).  Should be unique per species and distinct from all body
71    /// types (maybe in the future, it will just be unique per body type).
72    pub keyword: String,
73    /// The generic name for NPCs of this species.
74    pub generic: String,
75}
76
77/// Type holding configuration data for NPC names.
78pub 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    // If no pretty name is found, fall back to the keyword.
106    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() // Unwrap safe since is_some is true
115                .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
126/// Randomly generates a body associated with this NPC kind.
127pub 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
148/// A combination of an NpcKind (representing an outer species to generate), and
149/// a function that generates a fresh Body of a species that is part of that
150/// NpcKind each time it's called.  The reason things are done this way is that
151/// when parsing spawn strings, we'd like to be able to randomize features that
152/// haven't already been specified; for instance, if no species is specified we
153/// should randomize species, while if a species is specified we can still
154/// randomize other attributes like gender or clothing.
155///
156/// TODO: Now that we return a closure, consider having the closure accept a
157/// source of randomness explicitly, rather than always using ThreadRng.
158pub struct NpcBody(pub NpcKind, pub Box<dyn FnMut() -> Body>);
159
160impl FromStr for NpcBody {
161    type Err = ();
162
163    /// Get an NPC kind from a string.  If a body kind is matched without an
164    /// associated species, generate the species randomly within it; if an
165    /// explicit species is found, generate a random member of the species;
166    /// otherwise, return Err(()).
167    fn from_str(s: &str) -> Result<Self, Self::Err> { Self::from_str_with(s, kind_to_body) }
168}
169
170impl NpcBody {
171    /// If there is an exact name match for a body kind, call kind_to_body on
172    /// it. Otherwise, if an explicit species is found, generate a random
173    /// member of the species; otherwise, return Err(()).
174    #[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            // NOTE: Should be cheap in all cases, but if it weren't we should revamp the indexing
180            // method to take references instead of owned values.
181            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        // First, parse npc kind names.
207        NpcKind::from_str(s)
208            .map(|kind| NpcBody(kind, Box::new(move || kind_to_body(kind))))
209            .ok()
210            // Otherwise, npc kind names aren't sufficient; we parse species names instead.
211            .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}