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::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/// 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.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() // Unwrap safe since is_some is true
116                .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
124/// Randomly generates a body associated with this NPC kind.
125pub 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
146/// A combination of an NpcKind (representing an outer species to generate), and
147/// a function that generates a fresh Body of a species that is part of that
148/// NpcKind each time it's called.  The reason things are done this way is that
149/// when parsing spawn strings, we'd like to be able to randomize features that
150/// haven't already been specified; for instance, if no species is specified we
151/// should randomize species, while if a species is specified we can still
152/// randomize other attributes like gender or clothing.
153///
154/// TODO: Now that we return a closure, consider having the closure accept a
155/// source of randomness explicitly, rather than always using ThreadRng.
156pub struct NpcBody(pub NpcKind, pub Box<dyn FnMut() -> Body>);
157
158impl FromStr for NpcBody {
159    type Err = ();
160
161    /// Get an NPC kind from a string.  If a body kind is matched without an
162    /// associated species, generate the species randomly within it; if an
163    /// explicit species is found, generate a random member of the species;
164    /// otherwise, return Err(()).
165    fn from_str(s: &str) -> Result<Self, Self::Err> { Self::from_str_with(s, kind_to_body) }
166}
167
168impl NpcBody {
169    /// If there is an exact name match for a body kind, call kind_to_body on
170    /// it. Otherwise, if an explicit species is found, generate a random
171    /// member of the species; otherwise, return Err(()).
172    #[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            // NOTE: Should be cheap in all cases, but if it weren't we should revamp the indexing
178            // method to take references instead of owned values.
179            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        // First, parse npc kind names.
205        NpcKind::from_str(s)
206            .map(|kind| NpcBody(kind, Box::new(move || kind_to_body(kind))))
207            .ok()
208            // Otherwise, npc kind names aren't sufficient; we parse species names instead.
209            .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}