Per-speces NPC names!

Is also able to refactor some of the uglier code and introduces a
framework that (suitably extended) could be useful in removing
boilerplate elsewhere.
This commit is contained in:
Joshua Yanovski 2020-01-29 04:38:45 +01:00
parent 2d9e60e566
commit 3fa21b3dc7
19 changed files with 816 additions and 464 deletions

View File

@ -91,6 +91,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Various performance improvements to world generation. - Various performance improvements to world generation.
- Nametags now a fixed size and shown in a limited range - Nametags now a fixed size and shown in a limited range
- Non-humanoid skeletons now utilize configs for hotloading, and skeletal attributes. - Non-humanoid skeletons now utilize configs for hotloading, and skeletal attributes.
- Names of NPCs spawned in the wild now include their species.
### Removed ### Removed

View File

@ -1,322 +1,475 @@
{ {
"humanoid": [ "humanoid": {
"Adon", "body": {
"Agro", "keyword": "humanoid",
"Arlo", "names": [
"Azamarr", "Adon",
"Baashar", "Agro",
"Barak", "Arlo",
"Barton", "Azamarr",
"Baske", "Baashar",
"Baxar", "Barak",
"Blaiz", "Barton",
"Caelan", "Baske",
"Cassian", "Baxar",
"Clawsen", "Blaiz",
"Colborn", "Caelan",
"Dagfinn", "Cassian",
"Dagrod", "Clawsen",
"Dimian", "Colborn",
"Domnhar", "Dagfinn",
"Ebraheim", "Dagrod",
"Eldermar", "Dimian",
"Embre", "Domnhar",
"Esdel", "Ebraheim",
"Eune", "Eldermar",
"Fangar", "Embre",
"Favroe", "Esdel",
"Feron", "Eune",
"Feston", "Fangar",
"Fintis", "Favroe",
"Gatlen", "Feron",
"Gatlin", "Feston",
"Gentar", "Fintis",
"Gethrod", "Gatlen",
"Graff", "Gatlin",
"Gunnar", "Gentar",
"Hagalbar", "Gethrod",
"Hawke", "Graff",
"Hemm", "Gunnar",
"Henndar", "Hagalbar",
"Hezra", "Hawke",
"Hodus", "Hemm",
"Ishmael", "Henndar",
"Jakrin", "Hezra",
"Jareth", "Hodus",
"Jaris", "Ishmael",
"Jather", "Jakrin",
"Jerrick", "Jareth",
"Jessop", "Jaris",
"Jinto", "Jather",
"Joz", "Jerrick",
"Kadric", "Jessop",
"Kagran", "Jinto",
"Kent", "Joz",
"Khron", "Kadric",
"Kontas", "Kagran",
"Krinn", "Kent",
"Lassrin", "Khron",
"Lenox", "Kontas",
"Lothe", "Krinn",
"Lustros", "Lassrin",
"Lydan", "Lenox",
"Mavrek", "Lothe",
"Moki", "Lustros",
"Monty", "Lydan",
"Nazim", "Mavrek",
"Nesso", "Moki",
"Ophni", "Monty",
"Pakker", "Nazim",
"Paquin", "Nesso",
"Paskel", "Ophni",
"Pike", "Pakker",
"Ptorik", "Paquin",
"Quintis", "Paskel",
"Rankar", "Pike",
"Renham", "Ptorik",
"Revvyn", "Quintis",
"Riordan", "Rankar",
"Rivik", "Renham",
"Rourke", "Revvyn",
"Roux", "Riordan",
"Ryven", "Rivik",
"Sarkin", "Rourke",
"Sturp", "Roux",
"Straus", "Ryven",
"Syrin", "Sarkin",
"Talon", "Sturp",
"Tekren", "Straus",
"Tez", "Syrin",
"Turrek", "Talon",
"Tyvrik", "Tekren",
"Vadim", "Tez",
"Vale", "Turrek",
"Varog", "Tyvrik",
"Verssek", "Vadim",
"Weston", "Vale",
"Whit", "Varog",
"Wulfe", "Verssek",
"Yorjan", "Weston",
"Zaden", "Whit",
"Zagaroth", "Wulfe",
"Zenner" "Yorjan",
], "Zaden",
"wolf": [ "Zagaroth",
"Achak", "Zenner"
"Adalwolf", ]
"Akela", },
"Alaska", "species": {
"Aleu", "danari": {
"Amarok", "generic": "Danari"
"Apisi", },
"Archer", "dwarf": {
"Ares", "generic": "Dwarf"
"Arrax", },
"Artic", "elf": {
"Aspen", "generic": "Elf"
"Aura", },
"Axel", "human": {
"Balto", "generic": "Human"
"Barwolf", },
"Basil", "orc": {
"Beja", "generic": "Orc"
"Beowulf", },
"Borris", "undead": {
"Brassa", "generic": "Undead"
"Bruno", }
"Chronos", }
"Colt", },
"Comet", "quadruped_medium": {
"Cronus", "body": {
"Czar", "keyword": "wolf",
"Dakota", "names": [
"Dash", "Achak",
"Diego", "Adalwolf",
"Dire", "Akela",
"Duke", "Alaska",
"Echo", "Aleu",
"Elda", "Amarok",
"Eskimo", "Apisi",
"Essos", "Archer",
"Frey", "Ares",
"Gabu", "Arrax",
"Ghost", "Artic",
"Giro", "Aspen",
"Grey Wind", "Aura",
"Gunner", "Axel",
"Harou", "Balto",
"Havoc", "Barwolf",
"Hera", "Basil",
"Hunter", "Beja",
"Inuit", "Beowulf",
"Jacob", "Borris",
"Jenna", "Brassa",
"Juno", "Bruno",
"Kar", "Chronos",
"Khal", "Colt",
"Kiba", "Comet",
"Kimbra", "Cronus",
"Kodi", "Czar",
"Lady", "Dakota",
"Lakota", "Dash",
"Larka", "Diego",
"Leah", "Dire",
"Leto", "Duke",
"Lobo", "Echo",
"Loki", "Elda",
"Lotus", "Eskimo",
"Louve", "Essos",
"Lupa", "Frey",
"Major", "Gabu",
"Mathias", "Ghost",
"Moro", "Giro",
"Murdock", "Grey Wind",
"Nomad", "Gunner",
"Okami", "Harou",
"Orbit", "Havoc",
"Palla", "Hera",
"Pyro", "Hunter",
"Radolf", "Inuit",
"Raven", "Jacob",
"Rhea", "Jenna",
"Rider", "Juno",
"Rollo", "Kar",
"Rune", "Khal",
"Sable", "Kiba",
"Saga", "Kimbra",
"Sarge", "Kodi",
"Shiro", "Lady",
"Siku", "Lakota",
"Sky", "Larka",
"Stark", "Leah",
"Storm", "Leto",
"Suki", "Lobo",
"Tala", "Loki",
"Thor", "Lotus",
"Tiva", "Louve",
"Tyr", "Lupa",
"Ubba", "Major",
"Ulva", "Mathias",
"Valor", "Moro",
"Vechro", "Murdock",
"Wolf", "Nomad",
"Wolfgang", "Okami",
"Yara", "Orbit",
"Zeus", "Palla",
"Ziva", "Pyro",
"Zylo" "Radolf",
], "Raven",
"pig": [ "Rhea",
"Acorn", "Rider",
"Adeline", "Rollo",
"Ajna", "Rune",
"Athena", "Sable",
"Avacado", "Saga",
"Babe", "Sarge",
"Bella", "Shiro",
"Buddy", "Siku",
"Buttons", "Sky",
"Charlie", "Stark",
"Charlotte", "Storm",
"Chubbs", "Suki",
"Cinnamon", "Tala",
"Clarence", "Thor",
"Clover", "Tiva",
"Cookie", "Tyr",
"Corky", "Ubba",
"Cupcake", "Ulva",
"Daisy", "Valor",
"Dani", "Vechro",
"Delilah", "Wolf",
"Dexter", "Wolfgang",
"Dolly", "Yara",
"Dottie", "Zeus",
"Dudley", "Ziva",
"Ellie", "Zylo"
"Erwin", ]
"Evie", },
"Gertrude", "species": {
"Gilly", "wolf": {
"Ginger", "generic": "Wolf"
"Gizmo", },
"Gwenivere", "saber": {
"Hogrid", "generic": "Sabertooth Tiger"
"Hazel", },
"Hector", "viper": {
"Herman", "generic": "Lizard"
"Hermione", },
"Hoover", "tuskram": {
"Huck", "generic": "Tusk Ram"
"Iggy", },
"Jake", "alligator": {
"Josie", "generic": "Alligator"
"Leonardo", },
"Lily", "monitor": {
"Lola", "generic": "Monitor Lizard"
"Lottie", },
"Lucy", "lion": {
"Lulu", "generic": "Lion"
"Mabel", },
"Madeline", "tarasque": {
"Maisie", "generic": "Tarasque"
"Millie", }
"Mimzy", }
"Nooch", },
"Nutmeg", "quadruped_small": {
"Oinkers", "body": {
"Okja", "keyword": "pig",
"Oliver", "names": [
"Olivia", "Acorn",
"Panda", "Adeline",
"Pasley", "Ajna",
"Peanut", "Athena",
"Penelope", "Avacado",
"Peppa", "Babe",
"Petunia", "Bella",
"Phoebe", "Buddy",
"Piggie Smalls", "Buttons",
"Piggles", "Charlie",
"Piglet", "Charlotte",
"Pinto Bean", "Chubbs",
"Piper", "Cinnamon",
"Poly", "Clarence",
"Popcorn", "Clover",
"Poppy", "Cookie",
"Punky", "Corky",
"Rey", "Cupcake",
"Rooter", "Daisy",
"Rosie", "Dani",
"Ruby", "Delilah",
"Sadie", "Dexter",
"Scouter", "Dolly",
"Skittles", "Dottie",
"Snowball", "Dudley",
"Snuffles", "Ellie",
"Sonny", "Erwin",
"Sprout", "Evie",
"Squiggles", "Gertrude",
"Sweetie Pie", "Gilly",
"Theo", "Ginger",
"Toffuti", "Gizmo",
"Trixie", "Gwenivere",
"Violet", "Hogrid",
"Vishnu", "Hazel",
"Wee Wee", "Hector",
"Wilbur", "Herman",
"Willow", "Hermione",
"Winnie", "Hoover",
"Wrinkles", "Huck",
"Ziggy", "Iggy",
"Zoe", "Jake",
"Zoinks" "Josie",
], "Leonardo",
"duck": [ "Lily",
"ducky" "Lola",
], "Lottie",
"giant": [ "Lucy",
"leroybrown" "Lulu",
], "Mabel",
"rat": [ "Madeline",
"rat" "Maisie",
] "Millie",
} "Mimzy",
"Nooch",
"Nutmeg",
"Oinkers",
"Okja",
"Oliver",
"Olivia",
"Panda",
"Pasley",
"Peanut",
"Penelope",
"Peppa",
"Petunia",
"Phoebe",
"Piggie Smalls",
"Piggles",
"Piglet",
"Pinto Bean",
"Piper",
"Poly",
"Popcorn",
"Poppy",
"Punky",
"Rey",
"Rooter",
"Rosie",
"Ruby",
"Sadie",
"Scouter",
"Skittles",
"Snowball",
"Snuffles",
"Sonny",
"Sprout",
"Squiggles",
"Sweetie Pie",
"Theo",
"Toffuti",
"Trixie",
"Violet",
"Vishnu",
"Wee Wee",
"Wilbur",
"Willow",
"Winnie",
"Wrinkles",
"Ziggy",
"Zoe",
"Zoinks"
]
},
"species": {
"pig": {
"generic": "Pig"
},
"fox": {
"generic": "Fox"
},
"sheep": {
"generic": "Sheep"
},
"boar": {
"generic": "Boar"
},
"jackalope": {
"generic": "Jackalope"
},
"skunk": {
"generic": "Skunk"
},
"cat": {
"generic": "Cat"
},
"batfox": {
"generic": "Bat Fox"
},
"raccoon": {
"generic": "Raccoon"
},
"quokka": {
"generic": "Quokka"
},
"dodarock": {
"generic": "Dodarock"
},
"holladon": {
"generic": "Holladon"
}
}
},
"bird_medium": {
"body": {
"keyword": "duck",
"names": [
"Donald"
]
},
"species": {
"duck": {
"generic": "Duck"
},
"chicken": {
"generic": "Chicken"
},
"goose": {
"generic": "Goose"
},
"peacock": {
"generic": "Peacock"
}
}
},
"biped_large": {
"body": {
"keyword": "giant",
"names": [
"Leroy Brown"
]
},
"species": {
"giant": {
"generic": "Giant"
}
}
},
"critter": {
"body": {
"keyword": "rat",
"names": [
"Remy"
]
},
"species": {
"rat": {
"generic": "Rat"
},
"axolotl": {
"generic": "Axolotl"
},
"gecko": {
"generic": "Gecko"
},
"turtle": {
"generic": "Turtle"
},
"squirrel": {
"generic": "Squirrel"
},
"fungome": {
"generic": "Fungome"
}
}
}
}

View File

@ -9,6 +9,7 @@ use log::error;
use serde_json::Value; use serde_json::Value;
use std::{ use std::{
any::Any, any::Any,
fmt,
fs::{self, File, ReadDir}, fs::{self, File, ReadDir},
io::{BufReader, Read}, io::{BufReader, Read},
path::PathBuf, path::PathBuf,
@ -18,12 +19,27 @@ use std::{
/// The error returned by asset loading functions /// The error returned by asset loading functions
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Error { pub enum Error {
/// An internal error occurred.
Internal(Arc<dyn std::error::Error>),
/// An asset of a different type has already been loaded with this specifier. /// An asset of a different type has already been loaded with this specifier.
InvalidType, InvalidType,
/// Asset does not exist. /// Asset does not exist.
NotFound(String), NotFound(String),
} }
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Internal(err) => err.fmt(f),
Error::InvalidType => write!(
f,
"an asset of a different type has already been loaded with this specifier."
),
Error::NotFound(s) => write!(f, "{}", s),
}
}
}
impl From<Arc<dyn Any + 'static + Sync + Send>> for Error { impl From<Arc<dyn Any + 'static + Sync + Send>> for Error {
fn from(_: Arc<dyn Any + 'static + Sync + Send>) -> Self { fn from(_: Arc<dyn Any + 'static + Sync + Send>) -> Self {
Error::InvalidType Error::InvalidType
@ -32,7 +48,7 @@ impl From<Arc<dyn Any + 'static + Sync + Send>> for Error {
impl From<std::io::Error> for Error { impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self { fn from(err: std::io::Error) -> Self {
Error::NotFound(format!("{:?}", err)) Error::NotFound(format!("{}", err))
} }
} }
@ -135,7 +151,12 @@ pub fn load_cloned<A: Asset + Clone + 'static>(specifier: &str) -> Result<A, Err
/// let my_image = assets::load_expect::<DynamicImage>("core.ui.backgrounds.city"); /// let my_image = assets::load_expect::<DynamicImage>("core.ui.backgrounds.city");
/// ``` /// ```
pub fn load_expect<A: Asset + 'static>(specifier: &str) -> Arc<A> { pub fn load_expect<A: Asset + 'static>(specifier: &str) -> Arc<A> {
load(specifier).unwrap_or_else(|_| panic!("Failed loading essential asset: {}", specifier)) load(specifier).unwrap_or_else(|err| {
panic!(
"Failed loading essential asset: {} (error={})",
specifier, err
)
})
} }
/// Function used to load essential assets from the filesystem or the cache and return a clone. It will panic if the asset is not found. /// Function used to load essential assets from the filesystem or the cache and return a clone. It will panic if the asset is not found.

View File

@ -10,8 +10,13 @@ pub mod object;
pub mod quadruped_medium; pub mod quadruped_medium;
pub mod quadruped_small; pub mod quadruped_small;
use crate::{
assets::{self, Asset},
npc::NpcKind,
};
use specs::{Component, FlaggedStorage}; use specs::{Component, FlaggedStorage};
use specs_idvs::IDVStorage; use specs_idvs::IDVStorage;
use std::{fs::File, io::BufReader, sync::Arc};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u32)] #[repr(u32)]
@ -29,6 +34,58 @@ pub enum Body {
Critter(critter::Body) = 10, Critter(critter::Body) = 10,
} }
/// Data representing data generic to the body together with per-species data.
///
/// NOTE: Deliberately don't (yet?) implement serialize.
#[derive(Clone, Debug, Deserialize)]
pub struct BodyData<BodyMeta, SpeciesData> {
/// Shared metadata for this whole body type.
pub body: BodyMeta,
/// All the metadata for species with this body type.
pub species: SpeciesData,
}
/// Metadata intended to be stored per-body, together with data intended to be stored for each
/// species for each body.
///
/// NOTE: Deliberately don't (yet?) implement serialize.
#[derive(Clone, Debug, Deserialize)]
pub struct AllBodies<BodyMeta, SpeciesMeta> {
pub humanoid: BodyData<BodyMeta, humanoid::AllSpecies<SpeciesMeta>>,
pub quadruped_small: BodyData<BodyMeta, quadruped_small::AllSpecies<SpeciesMeta>>,
pub quadruped_medium: BodyData<BodyMeta, quadruped_medium::AllSpecies<SpeciesMeta>>,
pub bird_medium: BodyData<BodyMeta, bird_medium::AllSpecies<SpeciesMeta>>,
pub biped_large: BodyData<BodyMeta, biped_large::AllSpecies<SpeciesMeta>>,
pub critter: BodyData<BodyMeta, critter::AllSpecies<SpeciesMeta>>,
}
/// Can only retrieve body metadata by direct index.
impl<BodyMeta, SpeciesMeta> core::ops::Index<NpcKind> for AllBodies<BodyMeta, SpeciesMeta> {
type Output = BodyMeta;
fn index(&self, index: NpcKind) -> &Self::Output {
match index {
NpcKind::Humanoid => &self.humanoid.body,
NpcKind::Pig => &self.quadruped_small.body,
NpcKind::Wolf => &self.quadruped_medium.body,
NpcKind::Duck => &self.bird_medium.body,
NpcKind::Giant => &self.biped_large.body,
NpcKind::Rat => &self.critter.body,
}
}
}
impl<
BodyMeta: Send + Sync + for<'de> serde::Deserialize<'de>,
SpeciesMeta: Send + Sync + for<'de> serde::Deserialize<'de>,
> Asset for AllBodies<BodyMeta, SpeciesMeta>
{
const ENDINGS: &'static [&'static str] = &["json"];
fn parse(buf_reader: BufReader<File>) -> Result<Self, assets::Error> {
serde_json::de::from_reader(buf_reader).map_err(|e| assets::Error::Internal(Arc::new(e)))
}
}
impl Body { impl Body {
pub fn is_humanoid(&self) -> bool { pub fn is_humanoid(&self) -> bool {
match self { match self {

View File

@ -1,7 +1,6 @@
use rand::{seq::SliceRandom, thread_rng}; use rand::{seq::SliceRandom, thread_rng};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(C)]
pub struct Body { pub struct Body {
pub species: Species, pub species: Species,
pub body_type: BodyType, pub body_type: BodyType,
@ -21,6 +20,25 @@ impl Body {
pub enum Species { pub enum Species {
Giant = 0, Giant = 0,
} }
/// Data representing per-species generic data.
///
/// NOTE: Deliberately don't (yet?) implement serialize.
#[derive(Clone, Debug, Deserialize)]
pub struct AllSpecies<SpeciesMeta> {
pub giant: SpeciesMeta,
}
impl<SpeciesMeta> core::ops::Index<Species> for AllSpecies<SpeciesMeta> {
type Output = SpeciesMeta;
fn index(&self, index: Species) -> &Self::Output {
match index {
Species::Giant => &self.giant,
}
}
}
pub const ALL_SPECIES: [Species; 1] = [Species::Giant]; pub const ALL_SPECIES: [Species; 1] = [Species::Giant];
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]

View File

@ -1,7 +1,6 @@
use rand::{seq::SliceRandom, thread_rng}; use rand::{seq::SliceRandom, thread_rng};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(C)]
pub struct Body { pub struct Body {
pub species: Species, pub species: Species,
pub body_type: BodyType, pub body_type: BodyType,
@ -24,6 +23,31 @@ pub enum Species {
Goose = 2, Goose = 2,
Peacock = 3, Peacock = 3,
} }
/// Data representing per-species generic data.
///
/// NOTE: Deliberately don't (yet?) implement serialize.
#[derive(Clone, Debug, Deserialize)]
pub struct AllSpecies<SpeciesMeta> {
pub duck: SpeciesMeta,
pub chicken: SpeciesMeta,
pub goose: SpeciesMeta,
pub peacock: SpeciesMeta,
}
impl<SpeciesMeta> core::ops::Index<Species> for AllSpecies<SpeciesMeta> {
type Output = SpeciesMeta;
fn index(&self, index: Species) -> &Self::Output {
match index {
Species::Duck => &self.duck,
Species::Chicken => &self.chicken,
Species::Goose => &self.goose,
Species::Peacock => &self.peacock,
}
}
}
pub const ALL_SPECIES: [Species; 4] = [ pub const ALL_SPECIES: [Species; 4] = [
Species::Duck, Species::Duck,
Species::Chicken, Species::Chicken,

View File

@ -1,7 +1,6 @@
use rand::{seq::SliceRandom, thread_rng}; use rand::{seq::SliceRandom, thread_rng};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(C)]
pub struct Body { pub struct Body {
pub head: Head, pub head: Head,
pub torso: Torso, pub torso: Torso,

View File

@ -1,7 +1,6 @@
use rand::{seq::SliceRandom, thread_rng}; use rand::{seq::SliceRandom, thread_rng};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(C)]
pub struct Body { pub struct Body {
pub species: Species, pub species: Species,
pub body_type: BodyType, pub body_type: BodyType,
@ -26,6 +25,33 @@ pub enum Species {
Squirrel = 4, Squirrel = 4,
Fungome = 5, Fungome = 5,
} }
/// Data representing per-species generic data.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AllSpecies<SpeciesMeta> {
pub rat: SpeciesMeta,
pub axolotl: SpeciesMeta,
pub gecko: SpeciesMeta,
pub turtle: SpeciesMeta,
pub squirrel: SpeciesMeta,
pub fungome: SpeciesMeta,
}
impl<SpeciesMeta> core::ops::Index<Species> for AllSpecies<SpeciesMeta> {
type Output = SpeciesMeta;
fn index(&self, index: Species) -> &Self::Output {
match index {
Species::Rat => &self.rat,
Species::Axolotl => &self.axolotl,
Species::Gecko => &self.gecko,
Species::Turtle => &self.turtle,
Species::Squirrel => &self.squirrel,
Species::Fungome => &self.fungome,
}
}
}
pub const ALL_SPECIES: [Species; 6] = [ pub const ALL_SPECIES: [Species; 6] = [
Species::Rat, Species::Rat,
Species::Axolotl, Species::Axolotl,

View File

@ -1,7 +1,6 @@
use rand::{seq::SliceRandom, thread_rng}; use rand::{seq::SliceRandom, thread_rng};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(C)]
pub struct Body { pub struct Body {
pub head: Head, pub head: Head,
pub chest_front: ChestFront, pub chest_front: ChestFront,

View File

@ -1,7 +1,6 @@
use rand::{seq::SliceRandom, thread_rng}; use rand::{seq::SliceRandom, thread_rng};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(C)]
pub struct Body { pub struct Body {
pub head: Head, pub head: Head,
pub torso: Torso, pub torso: Torso,

View File

@ -1,7 +1,6 @@
use rand::{seq::SliceRandom, thread_rng}; use rand::{seq::SliceRandom, thread_rng};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(C)]
pub struct Body { pub struct Body {
pub torso: Torso, pub torso: Torso,
pub tail: Tail, pub tail: Tail,

View File

@ -1,77 +0,0 @@
use rand::{seq::SliceRandom, thread_rng};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(C)]
pub struct Body {
pub head: Head,
pub shoulder: Shoulder,
pub chest: Chest,
pub hand: Hand,
pub belt: Belt,
pub pants: Pants,
pub foot: Foot,
}
impl Body {
pub fn random() -> Self {
let mut rng = thread_rng();
Self {
head: *(&ALL_HEADS).choose(&mut rng).unwrap(),
shoulder: *(&ALL_SHOULDERS).choose(&mut rng).unwrap(),
chest: *(&ALL_CHESTS).choose(&mut rng).unwrap(),
hand: *(&ALL_HANDS).choose(&mut rng).unwrap(),
belt: *(&ALL_BELTS).choose(&mut rng).unwrap(),
pants: *(&ALL_PANTS).choose(&mut rng).unwrap(),
foot: *(&ALL_FEET).choose(&mut rng).unwrap(),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u32)]
pub enum Head {
Default,
}
const ALL_HEADS: [Head; 1] = [Head::Default];
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u32)]
pub enum Shoulder {
Default,
}
const ALL_SHOULDERS: [Shoulder; 1] = [Shoulder::Default];
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u32)]
pub enum Chest {
Default,
}
const ALL_CHESTS: [Chest; 1] = [Chest::Default];
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u32)]
pub enum Hand {
Default,
}
const ALL_HANDS: [Hand; 1] = [Hand::Default];
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u32)]
pub enum Belt {
Default,
}
const ALL_BELTS: [Belt; 1] = [Belt::Default];
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u32)]
pub enum Pants {
Default,
}
const ALL_FEET: [Foot; 1] = [Foot::Default];
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u32)]
pub enum Foot {
Default,
}

View File

@ -2,7 +2,6 @@ use rand::{seq::SliceRandom, thread_rng, Rng};
use vek::Rgb; use vek::Rgb;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(C)]
pub struct Body { pub struct Body {
pub race: Race, pub race: Race,
pub body_type: BodyType, pub body_type: BodyType,
@ -69,6 +68,35 @@ pub enum Race {
Orc = 4, Orc = 4,
Undead = 5, Undead = 5,
} }
/// Data representing per-species generic data.
///
/// NOTE: Deliberately don't (yet?) implement serialize.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AllSpecies<SpeciesMeta> {
pub danari: SpeciesMeta,
pub dwarf: SpeciesMeta,
pub elf: SpeciesMeta,
pub human: SpeciesMeta,
pub orc: SpeciesMeta,
pub undead: SpeciesMeta,
}
impl<SpeciesMeta> core::ops::Index<Race> for AllSpecies<SpeciesMeta> {
type Output = SpeciesMeta;
fn index(&self, index: Race) -> &Self::Output {
match index {
Race::Danari => &self.danari,
Race::Dwarf => &self.dwarf,
Race::Elf => &self.elf,
Race::Human => &self.human,
Race::Orc => &self.orc,
Race::Undead => &self.undead,
}
}
}
pub const ALL_RACES: [Race; 6] = [ pub const ALL_RACES: [Race; 6] = [
Race::Danari, Race::Danari,
Race::Dwarf, Race::Dwarf,

View File

@ -1,7 +1,6 @@
use rand::{seq::SliceRandom, thread_rng}; use rand::{seq::SliceRandom, thread_rng};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(C)]
pub struct Body { pub struct Body {
pub species: Species, pub species: Species,
pub body_type: BodyType, pub body_type: BodyType,
@ -28,6 +27,39 @@ pub enum Species {
Lion = 6, Lion = 6,
Tarasque = 7, Tarasque = 7,
} }
/// Data representing per-species generic data.
///
/// NOTE: Deliberately don't (yet?) implement serialize.
#[derive(Clone, Debug, Deserialize)]
pub struct AllSpecies<SpeciesMeta> {
pub wolf: SpeciesMeta,
pub saber: SpeciesMeta,
pub viper: SpeciesMeta,
pub tuskram: SpeciesMeta,
pub alligator: SpeciesMeta,
pub monitor: SpeciesMeta,
pub lion: SpeciesMeta,
pub tarasque: SpeciesMeta,
}
impl<SpeciesMeta> core::ops::Index<Species> for AllSpecies<SpeciesMeta> {
type Output = SpeciesMeta;
fn index(&self, index: Species) -> &Self::Output {
match index {
Species::Wolf => &self.wolf,
Species::Saber => &self.saber,
Species::Viper => &self.viper,
Species::Tuskram => &self.tuskram,
Species::Alligator => &self.alligator,
Species::Monitor => &self.monitor,
Species::Lion => &self.lion,
Species::Tarasque => &self.tarasque,
}
}
}
pub const ALL_SPECIES: [Species; 8] = [ pub const ALL_SPECIES: [Species; 8] = [
Species::Wolf, Species::Wolf,
Species::Saber, Species::Saber,

View File

@ -1,7 +1,6 @@
use rand::{seq::SliceRandom, thread_rng}; use rand::{seq::SliceRandom, thread_rng};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(C)]
pub struct Body { pub struct Body {
pub species: Species, pub species: Species,
pub body_type: BodyType, pub body_type: BodyType,
@ -32,6 +31,47 @@ pub enum Species {
Dodarock = 10, Dodarock = 10,
Holladon = 11, Holladon = 11,
} }
/// Data representing per-species generic data.
///
/// NOTE: Deliberately don't (yet?) implement serialize.
#[derive(Clone, Debug, Deserialize)]
pub struct AllSpecies<SpeciesMeta> {
pub pig: SpeciesMeta,
pub fox: SpeciesMeta,
pub sheep: SpeciesMeta,
pub boar: SpeciesMeta,
pub jackalope: SpeciesMeta,
pub skunk: SpeciesMeta,
pub cat: SpeciesMeta,
pub batfox: SpeciesMeta,
pub raccoon: SpeciesMeta,
pub quokka: SpeciesMeta,
pub dodarock: SpeciesMeta,
pub holladon: SpeciesMeta,
}
impl<SpeciesMeta> core::ops::Index<Species> for AllSpecies<SpeciesMeta> {
type Output = SpeciesMeta;
fn index(&self, index: Species) -> &Self::Output {
match index {
Species::Pig => &self.pig,
Species::Fox => &self.fox,
Species::Sheep => &self.sheep,
Species::Boar => &self.boar,
Species::Jackalope => &self.jackalope,
Species::Skunk => &self.skunk,
Species::Cat => &self.cat,
Species::Batfox => &self.batfox,
Species::Raccoon => &self.raccoon,
Species::Quokka => &self.quokka,
Species::Dodarock => &self.dodarock,
Species::Holladon => &self.holladon,
}
}
}
pub const ALL_SPECIES: [Species; 12] = [ pub const ALL_SPECIES: [Species; 12] = [
Species::Pig, Species::Pig,
Species::Fox, Species::Fox,

View File

@ -19,7 +19,7 @@ pub use admin::Admin;
pub use agent::{Agent, Alignment}; pub use agent::{Agent, Alignment};
pub use body::{ pub use body::{
biped_large, bird_medium, bird_small, critter, dragon, fish_medium, fish_small, humanoid, biped_large, bird_medium, bird_small, critter, dragon, fish_medium, fish_small, humanoid,
object, quadruped_medium, quadruped_small, Body, object, quadruped_medium, quadruped_small, AllBodies, Body, BodyData,
}; };
pub use character_state::{ActionState, CharacterState, MovementState}; pub use character_state::{ActionState, CharacterState, MovementState};
pub use controller::{ pub use controller::{

View File

@ -1,7 +1,6 @@
use crate::assets; use crate::{assets, comp::AllBodies};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use serde_json;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
@ -15,50 +14,61 @@ pub enum NpcKind {
Rat, Rat,
} }
impl NpcKind { pub const ALL_NPCS: [NpcKind; 6] = [
fn as_str(self) -> &'static str { NpcKind::Humanoid,
match self { NpcKind::Wolf,
NpcKind::Humanoid => "humanoid", NpcKind::Pig,
NpcKind::Wolf => "wolf", NpcKind::Duck,
NpcKind::Pig => "pig", NpcKind::Giant,
NpcKind::Duck => "duck", NpcKind::Rat,
NpcKind::Giant => "giant", ];
NpcKind::Rat => "rat",
} /// Body-specific NPC name metadata.
} ///
/// NOTE: Deliberately don't (yet?) implement serialize.
#[derive(Clone, Debug, Deserialize)]
pub struct BodyNames {
/// The keyword used to refer to this body type (e.g. via the command console). Should be
/// unique per body type.
pub keyword: String,
/// 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.
pub names: Vec<String>,
}
/// Species-specific NPC name metadata.
///
/// NOTE: Deliberately don't (yet?) implement serialize.
#[derive(Clone, Debug, Deserialize)]
pub struct SpeciesNames {
/// 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! {
pub static ref NPC_NAMES: Arc<NpcNames> = assets::load_expect("common.npc_names");
} }
impl FromStr for NpcKind { impl FromStr for NpcKind {
type Err = (); type Err = ();
fn from_str(s: &str) -> Result<Self, ()> { fn from_str(s: &str) -> Result<Self, ()> {
match s { let npc_names_json = &*NPC_NAMES;
"humanoid" => Ok(NpcKind::Humanoid), ALL_NPCS
"wolf" => Ok(NpcKind::Wolf), .iter()
"pig" => Ok(NpcKind::Pig), .copied()
"duck" => Ok(NpcKind::Duck), .find(|&npc| npc_names_json[npc].keyword == s)
"giant" => Ok(NpcKind::Giant), .ok_or(())
"rat" => Ok(NpcKind::Rat),
_ => Err(()),
}
} }
} }
lazy_static! { pub fn get_npc_name(npc_type: NpcKind) -> &'static str {
static ref NPC_NAMES_JSON: Arc<serde_json::Value> = assets::load_expect("common.npc_names"); let BodyNames { keyword, names } = &NPC_NAMES[npc_type];
}
pub fn get_npc_name(npc_type: NpcKind) -> String { // If no pretty name is found, fall back to the keyword.
let npc_names = NPC_NAMES_JSON names.choose(&mut rand::thread_rng()).unwrap_or(keyword)
.get(npc_type.as_str())
.expect("accessing json using NPC type provided as key")
.as_array()
.expect("parsing accessed json value into an array");
let npc_name = npc_names
.choose(&mut rand::thread_rng())
.expect("getting a random NPC name")
.as_str()
.expect("parsing NPC name json value into a &str");
String::from(npc_name)
} }

View File

@ -487,7 +487,7 @@ fn handle_spawn(server: &mut Server, entity: EcsEntity, args: String, action: &C
.state .state
.create_npc( .create_npc(
pos, pos,
comp::Stats::new(get_npc_name(id), body, None), comp::Stats::new(get_npc_name(id).into(), body, None),
body, body,
) )
.with(comp::Vel(vel)) .with(comp::Vel(vel))

View File

@ -6,6 +6,7 @@ use common::{
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
generation::EntityKind, generation::EntityKind,
msg::ServerMsg, msg::ServerMsg,
npc::{self, NPC_NAMES},
state::TerrainChanges, state::TerrainChanges,
terrain::TerrainGrid, terrain::TerrainGrid,
}; };
@ -100,6 +101,15 @@ impl<'a> System<'a> for Sys {
if let EntityKind::Waypoint = entity.kind { if let EntityKind::Waypoint = entity.kind {
server_emitter.emit(ServerEvent::CreateWaypoint(entity.pos)); server_emitter.emit(ServerEvent::CreateWaypoint(entity.pos));
} else { } else {
fn get_npc_name<
Species,
SpeciesData: core::ops::Index<Species, Output = npc::SpeciesNames>,
>(
body_data: &comp::BodyData<npc::BodyNames, SpeciesData>,
species: Species,
) -> &str {
&body_data.species[species].generic
}
const SPAWN_NPCS: &'static [fn() -> ( const SPAWN_NPCS: &'static [fn() -> (
String, String,
comp::Body, comp::Body,
@ -107,49 +117,58 @@ impl<'a> System<'a> for Sys {
comp::Alignment, comp::Alignment,
)] = &[ )] = &[
(|| { (|| {
let body = comp::humanoid::Body::random();
( (
"Traveler".into(), format!(
comp::Body::Humanoid(comp::humanoid::Body::random()), "{} Traveler",
get_npc_name(&NPC_NAMES.humanoid, body.race)
),
comp::Body::Humanoid(body),
Some(assets::load_expect_cloned("common.items.weapons.staff_1")), Some(assets::load_expect_cloned("common.items.weapons.staff_1")),
comp::Alignment::Npc, comp::Alignment::Npc,
) )
}) as _, }) as _,
(|| { (|| {
let body = comp::humanoid::Body::random();
( (
"Bandit".into(), format!("{} Bandit", get_npc_name(&NPC_NAMES.humanoid, body.race)),
comp::Body::Humanoid(comp::humanoid::Body::random()), comp::Body::Humanoid(body),
Some(assets::load_expect_cloned("common.items.weapons.staff_1")), Some(assets::load_expect_cloned("common.items.weapons.staff_1")),
comp::Alignment::Enemy, comp::Alignment::Enemy,
) )
}) as _, }) as _,
(|| { (|| {
let body = comp::quadruped_medium::Body::random();
( (
"Wolf".into(), get_npc_name(&NPC_NAMES.quadruped_medium, body.species).into(),
comp::Body::QuadrupedMedium(comp::quadruped_medium::Body::random()), comp::Body::QuadrupedMedium(body),
None, None,
comp::Alignment::Enemy, comp::Alignment::Enemy,
) )
}) as _, }) as _,
(|| { (|| {
let body = comp::bird_medium::Body::random();
( (
"Duck".into(), get_npc_name(&NPC_NAMES.bird_medium, body.species).into(),
comp::Body::BirdMedium(comp::bird_medium::Body::random()), comp::Body::BirdMedium(body),
None, None,
comp::Alignment::Wild, comp::Alignment::Wild,
) )
}) as _, }) as _,
(|| { (|| {
let body = comp::critter::Body::random();
( (
"Rat".into(), get_npc_name(&NPC_NAMES.critter, body.species).into(),
comp::Body::Critter(comp::critter::Body::random()), comp::Body::Critter(body),
None, None,
comp::Alignment::Wild, comp::Alignment::Wild,
) )
}) as _, }) as _,
(|| { (|| {
let body = comp::quadruped_small::Body::random();
( (
"Pig".into(), get_npc_name(&NPC_NAMES.quadruped_small, body.species).into(),
comp::Body::QuadrupedSmall(comp::quadruped_small::Body::random()), comp::Body::QuadrupedSmall(body),
None, None,
comp::Alignment::Wild, comp::Alignment::Wild,
) )
@ -168,10 +187,14 @@ impl<'a> System<'a> for Sys {
if let EntityKind::Boss = entity.kind { if let EntityKind::Boss = entity.kind {
if rand::random::<f32>() < 0.65 { if rand::random::<f32>() < 0.65 {
body = comp::Body::Humanoid(comp::humanoid::Body::random()); let body_new = comp::humanoid::Body::random();
body = comp::Body::Humanoid(body_new);
alignment = comp::Alignment::Npc; alignment = comp::Alignment::Npc;
stats = comp::Stats::new( stats = comp::Stats::new(
"Fearless Giant".to_string(), format!(
"Fearless Giant {}",
get_npc_name(&NPC_NAMES.humanoid, body_new.race)
),
body, body,
Some(assets::load_expect_cloned("common.items.weapons.hammer_1")), Some(assets::load_expect_cloned("common.items.weapons.hammer_1")),
); );