2020-01-29 16:56:28 +00:00
|
|
|
use crate::{
|
2020-12-12 22:14:24 +00:00
|
|
|
assets::{AssetExt, AssetHandle},
|
2021-01-20 11:20:06 +00:00
|
|
|
comp::{self, body, AllBodies, Body},
|
2020-01-29 16:56:28 +00:00
|
|
|
};
|
2019-06-03 22:11:27 +00:00
|
|
|
use lazy_static::lazy_static;
|
2019-06-01 19:24:17 +00:00
|
|
|
use rand::seq::SliceRandom;
|
2020-07-06 14:23:08 +00:00
|
|
|
use serde::Deserialize;
|
2020-12-12 22:14:24 +00:00
|
|
|
use std::str::FromStr;
|
2019-06-01 19:24:17 +00:00
|
|
|
|
2019-11-23 08:26:39 +00:00
|
|
|
#[derive(Clone, Copy, PartialEq)]
|
2019-06-01 19:24:17 +00:00
|
|
|
pub enum NpcKind {
|
2019-06-03 22:38:44 +00:00
|
|
|
Humanoid,
|
2019-06-01 19:24:17 +00:00
|
|
|
Wolf,
|
2019-06-01 19:49:34 +00:00
|
|
|
Pig,
|
2020-01-26 00:22:48 +00:00
|
|
|
Duck,
|
2020-11-29 16:22:13 +00:00
|
|
|
Clownfish,
|
2020-11-26 21:57:38 +00:00
|
|
|
Marlin,
|
2020-04-26 01:09:03 +00:00
|
|
|
Ogre,
|
2020-12-23 06:24:44 +00:00
|
|
|
Gremlin,
|
2020-08-29 08:23:12 +00:00
|
|
|
Archaeos,
|
2020-04-26 01:09:03 +00:00
|
|
|
StoneGolem,
|
2020-04-25 13:20:37 +00:00
|
|
|
Reddragon,
|
2020-06-01 23:56:50 +00:00
|
|
|
Crocodile,
|
2019-06-01 19:24:17 +00:00
|
|
|
}
|
|
|
|
|
2020-12-23 06:24:44 +00:00
|
|
|
pub const ALL_NPCS: [NpcKind; 12] = [
|
2020-01-29 03:38:45 +00:00
|
|
|
NpcKind::Humanoid,
|
|
|
|
NpcKind::Wolf,
|
|
|
|
NpcKind::Pig,
|
|
|
|
NpcKind::Duck,
|
2020-11-29 16:22:13 +00:00
|
|
|
NpcKind::Clownfish,
|
2020-11-26 21:57:38 +00:00
|
|
|
NpcKind::Marlin,
|
2020-04-26 01:09:03 +00:00
|
|
|
NpcKind::Ogre,
|
2020-12-23 06:24:44 +00:00
|
|
|
NpcKind::Gremlin,
|
2020-08-29 08:23:12 +00:00
|
|
|
NpcKind::Archaeos,
|
2020-04-26 01:09:03 +00:00
|
|
|
NpcKind::StoneGolem,
|
2020-04-25 13:20:37 +00:00
|
|
|
NpcKind::Reddragon,
|
2020-06-01 23:56:50 +00:00
|
|
|
NpcKind::Crocodile,
|
2020-01-29 03:38:45 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
/// Body-specific NPC name metadata.
|
|
|
|
///
|
|
|
|
/// NOTE: Deliberately don't (yet?) implement serialize.
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
|
|
pub struct BodyNames {
|
2020-02-01 20:39:39 +00:00
|
|
|
/// The keyword used to refer to this body type (e.g. via the command
|
|
|
|
/// console). Should be unique per body type.
|
2020-01-29 03:38:45 +00:00
|
|
|
pub keyword: String,
|
2020-02-01 20:39:39 +00:00
|
|
|
/// A list of canonical names for NPCs with this body types (currently used
|
|
|
|
/// when spawning this kind of NPC from the console). Going forward,
|
|
|
|
/// these names will likely be split up by species.
|
2021-01-20 11:20:06 +00:00
|
|
|
pub names_0: Vec<String>,
|
|
|
|
pub names_1: Option<Vec<String>>,
|
2020-01-29 03:38:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Species-specific NPC name metadata.
|
|
|
|
///
|
|
|
|
/// NOTE: Deliberately don't (yet?) implement serialize.
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
|
|
pub struct SpeciesNames {
|
2020-01-29 16:56:28 +00:00
|
|
|
/// The keyword used to refer to this species (e.g. via the command
|
|
|
|
/// console). Should be unique per species and distinct from all body
|
|
|
|
/// types (maybe in the future, it will just be unique per body type).
|
|
|
|
pub keyword: String,
|
2020-01-29 03:38:45 +00:00
|
|
|
/// The generic name for NPCs of this species.
|
|
|
|
pub generic: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Type holding configuration data for NPC names.
|
|
|
|
pub type NpcNames = AllBodies<BodyNames, SpeciesNames>;
|
|
|
|
|
|
|
|
lazy_static! {
|
2020-12-12 22:14:24 +00:00
|
|
|
pub static ref NPC_NAMES: AssetHandle<NpcNames> = NpcNames::load_expect("common.npc_names");
|
2019-06-01 19:24:17 +00:00
|
|
|
}
|
|
|
|
|
2019-06-15 07:54:47 +00:00
|
|
|
impl FromStr for NpcKind {
|
|
|
|
type Err = ();
|
|
|
|
|
2020-12-12 22:14:24 +00:00
|
|
|
fn from_str(s: &str) -> Result<Self, ()> {
|
|
|
|
let npc_names = &*NPC_NAMES.read();
|
2020-01-29 03:38:45 +00:00
|
|
|
ALL_NPCS
|
|
|
|
.iter()
|
|
|
|
.copied()
|
2020-01-29 16:56:28 +00:00
|
|
|
.find(|&npc| npc_names[npc].keyword == s)
|
2020-01-29 03:38:45 +00:00
|
|
|
.ok_or(())
|
2019-06-15 07:54:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-20 11:20:06 +00:00
|
|
|
pub fn get_npc_name(npc_type: NpcKind, body_type: Option<BodyType>) -> String {
|
2020-12-12 22:14:24 +00:00
|
|
|
let npc_names = NPC_NAMES.read();
|
2021-01-20 11:20:06 +00:00
|
|
|
let BodyNames {
|
|
|
|
keyword,
|
|
|
|
names_0,
|
|
|
|
names_1,
|
|
|
|
} = &npc_names[npc_type];
|
2019-06-01 19:49:34 +00:00
|
|
|
|
2020-01-29 03:38:45 +00:00
|
|
|
// If no pretty name is found, fall back to the keyword.
|
2021-01-20 11:20:06 +00:00
|
|
|
match body_type {
|
|
|
|
Some(BodyType::Male) => names_0
|
|
|
|
.choose(&mut rand::thread_rng())
|
|
|
|
.unwrap_or(keyword)
|
|
|
|
.clone(),
|
|
|
|
Some(BodyType::Female) if names_1.is_some() => {
|
|
|
|
names_1
|
|
|
|
.as_ref()
|
|
|
|
.unwrap() // Unwrap safe since is_some is true
|
|
|
|
.choose(&mut rand::thread_rng())
|
|
|
|
.unwrap_or(keyword)
|
|
|
|
.clone()
|
|
|
|
},
|
|
|
|
_ => names_0
|
|
|
|
.choose(&mut rand::thread_rng())
|
|
|
|
.unwrap_or(keyword)
|
|
|
|
.clone(),
|
|
|
|
}
|
2019-06-01 19:24:17 +00:00
|
|
|
}
|
2020-01-29 16:56:28 +00:00
|
|
|
|
|
|
|
/// Randomly generates a body associated with this NPC kind.
|
|
|
|
pub fn kind_to_body(kind: NpcKind) -> Body {
|
|
|
|
match kind {
|
|
|
|
NpcKind::Humanoid => comp::humanoid::Body::random().into(),
|
|
|
|
NpcKind::Pig => comp::quadruped_small::Body::random().into(),
|
|
|
|
NpcKind::Wolf => comp::quadruped_medium::Body::random().into(),
|
|
|
|
NpcKind::Duck => comp::bird_medium::Body::random().into(),
|
2020-11-29 16:22:13 +00:00
|
|
|
NpcKind::Clownfish => comp::fish_small::Body::random().into(),
|
2020-11-26 21:57:38 +00:00
|
|
|
NpcKind::Marlin => comp::fish_medium::Body::random().into(),
|
2020-04-26 01:09:03 +00:00
|
|
|
NpcKind::Ogre => comp::biped_large::Body::random().into(),
|
2020-12-23 06:24:44 +00:00
|
|
|
NpcKind::Gremlin => comp::biped_small::Body::random().into(),
|
2020-08-29 08:23:12 +00:00
|
|
|
NpcKind::Archaeos => comp::theropod::Body::random().into(),
|
2020-04-26 01:09:03 +00:00
|
|
|
NpcKind::StoneGolem => comp::golem::Body::random().into(),
|
2020-05-09 22:01:49 +00:00
|
|
|
NpcKind::Reddragon => comp::dragon::Body::random().into(),
|
2020-06-01 23:56:50 +00:00
|
|
|
NpcKind::Crocodile => comp::quadruped_low::Body::random().into(),
|
2020-01-29 16:56:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A combination of an NpcKind (representing an outer species to generate), and
|
|
|
|
/// a function that generates a fresh Body of a species that is part of that
|
|
|
|
/// NpcKind each time it's called. The reason things are done this way is that
|
|
|
|
/// when parsing spawn strings, we'd like to be able to randomize features that
|
|
|
|
/// haven't already been specified; for instance, if no species is specified we
|
|
|
|
/// should randomize species, while if a species is specified we can still
|
|
|
|
/// randomize other attributes like gender or clothing.
|
|
|
|
///
|
|
|
|
/// TODO: Now that we return a closure, consider having the closure accept a
|
|
|
|
/// source of randomness explicitly, rather than always using ThreadRng.
|
|
|
|
pub struct NpcBody(pub NpcKind, pub Box<dyn FnMut() -> Body>);
|
|
|
|
|
|
|
|
impl FromStr for NpcBody {
|
|
|
|
type Err = ();
|
|
|
|
|
|
|
|
/// Get an NPC kind from a string. If a body kind is matched without an
|
2020-08-25 12:21:25 +00:00
|
|
|
/// associated species, generate the species randomly within it; if an
|
2020-01-29 16:56:28 +00:00
|
|
|
/// explicit species is found, generate a random member of the species;
|
|
|
|
/// otherwise, return Err(()).
|
2020-12-10 11:50:48 +00:00
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> { Self::from_str_with(s, kind_to_body) }
|
2020-01-29 16:56:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl NpcBody {
|
|
|
|
/// If there is an exact name match for a body kind, call kind_to_body on
|
|
|
|
/// it. Otherwise, if an explicit species is found, generate a random
|
|
|
|
/// member of the species; otherwise, return Err(()).
|
2020-12-10 11:50:48 +00:00
|
|
|
#[allow(clippy::result_unit_err)]
|
2020-01-29 16:56:28 +00:00
|
|
|
pub fn from_str_with(s: &str, kind_to_body: fn(NpcKind) -> Body) -> Result<Self, ()> {
|
|
|
|
fn parse<
|
|
|
|
'a,
|
|
|
|
B: Into<Body> + 'static,
|
|
|
|
// NOTE: Should be cheap in all cases, but if it weren't we should revamp the indexing
|
|
|
|
// method to take references instead of owned values.
|
|
|
|
Species: 'static,
|
|
|
|
BodyMeta,
|
|
|
|
SpeciesData: for<'b> core::ops::Index<&'b Species, Output = SpeciesNames>,
|
|
|
|
>(
|
|
|
|
s: &str,
|
|
|
|
npc_kind: NpcKind,
|
|
|
|
body_data: &'a comp::BodyData<BodyMeta, SpeciesData>,
|
|
|
|
conv_func: for<'d> fn(&mut rand::rngs::ThreadRng, &'d Species) -> B,
|
|
|
|
) -> Option<NpcBody>
|
|
|
|
where
|
|
|
|
&'a SpeciesData: IntoIterator<Item = Species>,
|
|
|
|
{
|
|
|
|
let npc_names = &body_data.species;
|
|
|
|
body_data
|
|
|
|
.species
|
|
|
|
.into_iter()
|
|
|
|
.find(|species| npc_names[species].keyword == s)
|
|
|
|
.map(|species| {
|
|
|
|
NpcBody(
|
|
|
|
npc_kind,
|
|
|
|
Box::new(move || conv_func(&mut rand::thread_rng(), &species).into()),
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
2020-12-12 22:14:24 +00:00
|
|
|
let npc_names = &*NPC_NAMES.read();
|
2020-01-29 16:56:28 +00:00
|
|
|
// First, parse npc kind names.
|
|
|
|
NpcKind::from_str(s)
|
|
|
|
.map(|kind| NpcBody(kind, Box::new(move || kind_to_body(kind))))
|
|
|
|
.ok()
|
|
|
|
// Otherwise, npc kind names aren't sufficient; we parse species names instead.
|
|
|
|
.or_else(|| {
|
|
|
|
parse(
|
|
|
|
s,
|
|
|
|
NpcKind::Humanoid,
|
|
|
|
&npc_names.humanoid,
|
|
|
|
comp::humanoid::Body::random_with,
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.or_else(|| {
|
|
|
|
parse(
|
|
|
|
s,
|
|
|
|
NpcKind::Pig,
|
|
|
|
&npc_names.quadruped_small,
|
|
|
|
comp::quadruped_small::Body::random_with,
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.or_else(|| {
|
|
|
|
parse(
|
|
|
|
s,
|
|
|
|
NpcKind::Wolf,
|
|
|
|
&npc_names.quadruped_medium,
|
|
|
|
comp::quadruped_medium::Body::random_with,
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.or_else(|| {
|
|
|
|
parse(
|
|
|
|
s,
|
|
|
|
NpcKind::Duck,
|
|
|
|
&npc_names.bird_medium,
|
|
|
|
comp::bird_medium::Body::random_with,
|
|
|
|
)
|
|
|
|
})
|
2020-11-26 21:57:38 +00:00
|
|
|
.or_else(|| {
|
|
|
|
parse(
|
|
|
|
s,
|
2020-11-29 16:22:13 +00:00
|
|
|
NpcKind::Clownfish,
|
2020-11-26 21:57:38 +00:00
|
|
|
&npc_names.fish_small,
|
|
|
|
comp::fish_small::Body::random_with,
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.or_else(|| {
|
|
|
|
parse(
|
|
|
|
s,
|
|
|
|
NpcKind::Marlin,
|
|
|
|
&npc_names.fish_medium,
|
|
|
|
comp::fish_medium::Body::random_with,
|
|
|
|
)
|
|
|
|
})
|
2020-01-29 16:56:28 +00:00
|
|
|
.or_else(|| {
|
|
|
|
parse(
|
|
|
|
s,
|
2020-04-26 01:09:03 +00:00
|
|
|
NpcKind::Ogre,
|
2020-01-29 16:56:28 +00:00
|
|
|
&npc_names.biped_large,
|
|
|
|
comp::biped_large::Body::random_with,
|
|
|
|
)
|
|
|
|
})
|
2020-12-23 06:24:44 +00:00
|
|
|
.or_else(|| {
|
|
|
|
parse(
|
|
|
|
s,
|
|
|
|
NpcKind::Gremlin,
|
|
|
|
&npc_names.biped_small,
|
|
|
|
comp::biped_small::Body::random_with,
|
|
|
|
)
|
|
|
|
})
|
2020-01-29 16:56:28 +00:00
|
|
|
.or_else(|| {
|
|
|
|
parse(
|
|
|
|
s,
|
2020-08-29 08:23:12 +00:00
|
|
|
NpcKind::Archaeos,
|
|
|
|
&npc_names.theropod,
|
|
|
|
comp::theropod::Body::random_with,
|
2020-01-29 16:56:28 +00:00
|
|
|
)
|
|
|
|
})
|
2020-04-26 01:09:03 +00:00
|
|
|
.or_else(|| {
|
|
|
|
parse(
|
|
|
|
s,
|
|
|
|
NpcKind::StoneGolem,
|
|
|
|
&npc_names.golem,
|
|
|
|
comp::golem::Body::random_with,
|
|
|
|
)
|
|
|
|
})
|
2020-04-25 13:20:37 +00:00
|
|
|
.or_else(|| {
|
|
|
|
parse(
|
|
|
|
s,
|
|
|
|
NpcKind::Reddragon,
|
|
|
|
&npc_names.dragon,
|
|
|
|
comp::dragon::Body::random_with,
|
|
|
|
)
|
|
|
|
})
|
2020-06-01 23:56:50 +00:00
|
|
|
.or_else(|| {
|
|
|
|
parse(
|
|
|
|
s,
|
|
|
|
NpcKind::Crocodile,
|
|
|
|
&npc_names.quadruped_low,
|
|
|
|
comp::quadruped_low::Body::random_with,
|
|
|
|
)
|
|
|
|
})
|
2020-01-29 16:56:28 +00:00
|
|
|
.ok_or(())
|
|
|
|
}
|
|
|
|
}
|
2021-01-20 11:20:06 +00:00
|
|
|
|
|
|
|
pub enum BodyType {
|
|
|
|
Male,
|
|
|
|
Female,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl BodyType {
|
|
|
|
pub fn from_body(body: Body) -> Option<BodyType> {
|
|
|
|
match body {
|
|
|
|
Body::Humanoid(humanoid) => match humanoid.body_type {
|
|
|
|
body::humanoid::BodyType::Male => Some(BodyType::Male),
|
|
|
|
body::humanoid::BodyType::Female => Some(BodyType::Female),
|
|
|
|
},
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|