veloren_common/
npc.rs

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