diff --git a/CHANGELOG.md b/CHANGELOG.md index aa87ccf4aa..18baf2e358 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Various performance improvements to world generation. - Nametags now a fixed size and shown in a limited range - Non-humanoid skeletons now utilize configs for hotloading, and skeletal attributes. +- Names of NPCs spawned in the wild now include their species. ### Removed diff --git a/assets/common/npc_names.json b/assets/common/npc_names.json index 7f70e2d8b5..4fec15bf01 100644 --- a/assets/common/npc_names.json +++ b/assets/common/npc_names.json @@ -1,322 +1,475 @@ { - "humanoid": [ - "Adon", - "Agro", - "Arlo", - "Azamarr", - "Baashar", - "Barak", - "Barton", - "Baske", - "Baxar", - "Blaiz", - "Caelan", - "Cassian", - "Clawsen", - "Colborn", - "Dagfinn", - "Dagrod", - "Dimian", - "Domnhar", - "Ebraheim", - "Eldermar", - "Embre", - "Esdel", - "Eune", - "Fangar", - "Favroe", - "Feron", - "Feston", - "Fintis", - "Gatlen", - "Gatlin", - "Gentar", - "Gethrod", - "Graff", - "Gunnar", - "Hagalbar", - "Hawke", - "Hemm", - "Henndar", - "Hezra", - "Hodus", - "Ishmael", - "Jakrin", - "Jareth", - "Jaris", - "Jather", - "Jerrick", - "Jessop", - "Jinto", - "Joz", - "Kadric", - "Kagran", - "Kent", - "Khron", - "Kontas", - "Krinn", - "Lassrin", - "Lenox", - "Lothe", - "Lustros", - "Lydan", - "Mavrek", - "Moki", - "Monty", - "Nazim", - "Nesso", - "Ophni", - "Pakker", - "Paquin", - "Paskel", - "Pike", - "Ptorik", - "Quintis", - "Rankar", - "Renham", - "Revvyn", - "Riordan", - "Rivik", - "Rourke", - "Roux", - "Ryven", - "Sarkin", - "Sturp", - "Straus", - "Syrin", - "Talon", - "Tekren", - "Tez", - "Turrek", - "Tyvrik", - "Vadim", - "Vale", - "Varog", - "Verssek", - "Weston", - "Whit", - "Wulfe", - "Yorjan", - "Zaden", - "Zagaroth", - "Zenner" - ], - "wolf": [ - "Achak", - "Adalwolf", - "Akela", - "Alaska", - "Aleu", - "Amarok", - "Apisi", - "Archer", - "Ares", - "Arrax", - "Artic", - "Aspen", - "Aura", - "Axel", - "Balto", - "Barwolf", - "Basil", - "Beja", - "Beowulf", - "Borris", - "Brassa", - "Bruno", - "Chronos", - "Colt", - "Comet", - "Cronus", - "Czar", - "Dakota", - "Dash", - "Diego", - "Dire", - "Duke", - "Echo", - "Elda", - "Eskimo", - "Essos", - "Frey", - "Gabu", - "Ghost", - "Giro", - "Grey Wind", - "Gunner", - "Harou", - "Havoc", - "Hera", - "Hunter", - "Inuit", - "Jacob", - "Jenna", - "Juno", - "Kar", - "Khal", - "Kiba", - "Kimbra", - "Kodi", - "Lady", - "Lakota", - "Larka", - "Leah", - "Leto", - "Lobo", - "Loki", - "Lotus", - "Louve", - "Lupa", - "Major", - "Mathias", - "Moro", - "Murdock", - "Nomad", - "Okami", - "Orbit", - "Palla", - "Pyro", - "Radolf", - "Raven", - "Rhea", - "Rider", - "Rollo", - "Rune", - "Sable", - "Saga", - "Sarge", - "Shiro", - "Siku", - "Sky", - "Stark", - "Storm", - "Suki", - "Tala", - "Thor", - "Tiva", - "Tyr", - "Ubba", - "Ulva", - "Valor", - "Vechro", - "Wolf", - "Wolfgang", - "Yara", - "Zeus", - "Ziva", - "Zylo" - ], - "pig": [ - "Acorn", - "Adeline", - "Ajna", - "Athena", - "Avacado", - "Babe", - "Bella", - "Buddy", - "Buttons", - "Charlie", - "Charlotte", - "Chubbs", - "Cinnamon", - "Clarence", - "Clover", - "Cookie", - "Corky", - "Cupcake", - "Daisy", - "Dani", - "Delilah", - "Dexter", - "Dolly", - "Dottie", - "Dudley", - "Ellie", - "Erwin", - "Evie", - "Gertrude", - "Gilly", - "Ginger", - "Gizmo", - "Gwenivere", - "Hogrid", - "Hazel", - "Hector", - "Herman", - "Hermione", - "Hoover", - "Huck", - "Iggy", - "Jake", - "Josie", - "Leonardo", - "Lily", - "Lola", - "Lottie", - "Lucy", - "Lulu", - "Mabel", - "Madeline", - "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" - ], - "duck": [ - "ducky" - ], - "giant": [ - "leroybrown" - ], - "rat": [ - "rat" - ] -} \ No newline at end of file + "humanoid": { + "body": { + "keyword": "humanoid", + "names": [ + "Adon", + "Agro", + "Arlo", + "Azamarr", + "Baashar", + "Barak", + "Barton", + "Baske", + "Baxar", + "Blaiz", + "Caelan", + "Cassian", + "Clawsen", + "Colborn", + "Dagfinn", + "Dagrod", + "Dimian", + "Domnhar", + "Ebraheim", + "Eldermar", + "Embre", + "Esdel", + "Eune", + "Fangar", + "Favroe", + "Feron", + "Feston", + "Fintis", + "Gatlen", + "Gatlin", + "Gentar", + "Gethrod", + "Graff", + "Gunnar", + "Hagalbar", + "Hawke", + "Hemm", + "Henndar", + "Hezra", + "Hodus", + "Ishmael", + "Jakrin", + "Jareth", + "Jaris", + "Jather", + "Jerrick", + "Jessop", + "Jinto", + "Joz", + "Kadric", + "Kagran", + "Kent", + "Khron", + "Kontas", + "Krinn", + "Lassrin", + "Lenox", + "Lothe", + "Lustros", + "Lydan", + "Mavrek", + "Moki", + "Monty", + "Nazim", + "Nesso", + "Ophni", + "Pakker", + "Paquin", + "Paskel", + "Pike", + "Ptorik", + "Quintis", + "Rankar", + "Renham", + "Revvyn", + "Riordan", + "Rivik", + "Rourke", + "Roux", + "Ryven", + "Sarkin", + "Sturp", + "Straus", + "Syrin", + "Talon", + "Tekren", + "Tez", + "Turrek", + "Tyvrik", + "Vadim", + "Vale", + "Varog", + "Verssek", + "Weston", + "Whit", + "Wulfe", + "Yorjan", + "Zaden", + "Zagaroth", + "Zenner" + ] + }, + "species": { + "danari": { + "generic": "Danari" + }, + "dwarf": { + "generic": "Dwarf" + }, + "elf": { + "generic": "Elf" + }, + "human": { + "generic": "Human" + }, + "orc": { + "generic": "Orc" + }, + "undead": { + "generic": "Undead" + } + } + }, + "quadruped_medium": { + "body": { + "keyword": "wolf", + "names": [ + "Achak", + "Adalwolf", + "Akela", + "Alaska", + "Aleu", + "Amarok", + "Apisi", + "Archer", + "Ares", + "Arrax", + "Artic", + "Aspen", + "Aura", + "Axel", + "Balto", + "Barwolf", + "Basil", + "Beja", + "Beowulf", + "Borris", + "Brassa", + "Bruno", + "Chronos", + "Colt", + "Comet", + "Cronus", + "Czar", + "Dakota", + "Dash", + "Diego", + "Dire", + "Duke", + "Echo", + "Elda", + "Eskimo", + "Essos", + "Frey", + "Gabu", + "Ghost", + "Giro", + "Grey Wind", + "Gunner", + "Harou", + "Havoc", + "Hera", + "Hunter", + "Inuit", + "Jacob", + "Jenna", + "Juno", + "Kar", + "Khal", + "Kiba", + "Kimbra", + "Kodi", + "Lady", + "Lakota", + "Larka", + "Leah", + "Leto", + "Lobo", + "Loki", + "Lotus", + "Louve", + "Lupa", + "Major", + "Mathias", + "Moro", + "Murdock", + "Nomad", + "Okami", + "Orbit", + "Palla", + "Pyro", + "Radolf", + "Raven", + "Rhea", + "Rider", + "Rollo", + "Rune", + "Sable", + "Saga", + "Sarge", + "Shiro", + "Siku", + "Sky", + "Stark", + "Storm", + "Suki", + "Tala", + "Thor", + "Tiva", + "Tyr", + "Ubba", + "Ulva", + "Valor", + "Vechro", + "Wolf", + "Wolfgang", + "Yara", + "Zeus", + "Ziva", + "Zylo" + ] + }, + "species": { + "wolf": { + "generic": "Wolf" + }, + "saber": { + "generic": "Sabertooth Tiger" + }, + "viper": { + "generic": "Lizard" + }, + "tuskram": { + "generic": "Tusk Ram" + }, + "alligator": { + "generic": "Alligator" + }, + "monitor": { + "generic": "Monitor Lizard" + }, + "lion": { + "generic": "Lion" + }, + "tarasque": { + "generic": "Tarasque" + } + } + }, + "quadruped_small": { + "body": { + "keyword": "pig", + "names": [ + "Acorn", + "Adeline", + "Ajna", + "Athena", + "Avacado", + "Babe", + "Bella", + "Buddy", + "Buttons", + "Charlie", + "Charlotte", + "Chubbs", + "Cinnamon", + "Clarence", + "Clover", + "Cookie", + "Corky", + "Cupcake", + "Daisy", + "Dani", + "Delilah", + "Dexter", + "Dolly", + "Dottie", + "Dudley", + "Ellie", + "Erwin", + "Evie", + "Gertrude", + "Gilly", + "Ginger", + "Gizmo", + "Gwenivere", + "Hogrid", + "Hazel", + "Hector", + "Herman", + "Hermione", + "Hoover", + "Huck", + "Iggy", + "Jake", + "Josie", + "Leonardo", + "Lily", + "Lola", + "Lottie", + "Lucy", + "Lulu", + "Mabel", + "Madeline", + "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" + } + } + } +} diff --git a/common/src/assets/mod.rs b/common/src/assets/mod.rs index 43ba77fae9..8198aa657f 100644 --- a/common/src/assets/mod.rs +++ b/common/src/assets/mod.rs @@ -9,6 +9,7 @@ use log::error; use serde_json::Value; use std::{ any::Any, + fmt, fs::{self, File, ReadDir}, io::{BufReader, Read}, path::PathBuf, @@ -18,12 +19,27 @@ use std::{ /// The error returned by asset loading functions #[derive(Debug, Clone)] pub enum Error { + /// An internal error occurred. + Internal(Arc), /// An asset of a different type has already been loaded with this specifier. InvalidType, /// Asset does not exist. 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> for Error { fn from(_: Arc) -> Self { Error::InvalidType @@ -32,7 +48,7 @@ impl From> for Error { impl From for Error { fn from(err: std::io::Error) -> Self { - Error::NotFound(format!("{:?}", err)) + Error::NotFound(format!("{}", err)) } } @@ -135,7 +151,12 @@ pub fn load_cloned(specifier: &str) -> Result("core.ui.backgrounds.city"); /// ``` pub fn load_expect(specifier: &str) -> Arc { - 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. diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index ec25d4ae2e..b1cc8dd44b 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -10,8 +10,13 @@ pub mod object; pub mod quadruped_medium; pub mod quadruped_small; +use crate::{ + assets::{self, Asset}, + npc::NpcKind, +}; use specs::{Component, FlaggedStorage}; use specs_idvs::IDVStorage; +use std::{fs::File, io::BufReader, sync::Arc}; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[repr(u32)] @@ -29,6 +34,58 @@ pub enum Body { 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 { + /// 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 { + pub humanoid: BodyData>, + pub quadruped_small: BodyData>, + pub quadruped_medium: BodyData>, + pub bird_medium: BodyData>, + pub biped_large: BodyData>, + pub critter: BodyData>, +} + +/// Can only retrieve body metadata by direct index. +impl core::ops::Index for AllBodies { + 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 +{ + const ENDINGS: &'static [&'static str] = &["json"]; + fn parse(buf_reader: BufReader) -> Result { + serde_json::de::from_reader(buf_reader).map_err(|e| assets::Error::Internal(Arc::new(e))) + } +} + impl Body { pub fn is_humanoid(&self) -> bool { match self { diff --git a/common/src/comp/body/biped_large.rs b/common/src/comp/body/biped_large.rs index f1c78f26ed..2b70d721ff 100644 --- a/common/src/comp/body/biped_large.rs +++ b/common/src/comp/body/biped_large.rs @@ -1,7 +1,6 @@ use rand::{seq::SliceRandom, thread_rng}; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[repr(C)] pub struct Body { pub species: Species, pub body_type: BodyType, @@ -21,6 +20,25 @@ impl Body { pub enum Species { Giant = 0, } + +/// Data representing per-species generic data. +/// +/// NOTE: Deliberately don't (yet?) implement serialize. +#[derive(Clone, Debug, Deserialize)] +pub struct AllSpecies { + pub giant: SpeciesMeta, +} + +impl core::ops::Index for AllSpecies { + type Output = SpeciesMeta; + + fn index(&self, index: Species) -> &Self::Output { + match index { + Species::Giant => &self.giant, + } + } +} + pub const ALL_SPECIES: [Species; 1] = [Species::Giant]; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] diff --git a/common/src/comp/body/bird_medium.rs b/common/src/comp/body/bird_medium.rs index dbf27b56f3..b09d8cc562 100644 --- a/common/src/comp/body/bird_medium.rs +++ b/common/src/comp/body/bird_medium.rs @@ -1,7 +1,6 @@ use rand::{seq::SliceRandom, thread_rng}; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[repr(C)] pub struct Body { pub species: Species, pub body_type: BodyType, @@ -24,6 +23,31 @@ pub enum Species { Goose = 2, Peacock = 3, } + +/// Data representing per-species generic data. +/// +/// NOTE: Deliberately don't (yet?) implement serialize. +#[derive(Clone, Debug, Deserialize)] +pub struct AllSpecies { + pub duck: SpeciesMeta, + pub chicken: SpeciesMeta, + pub goose: SpeciesMeta, + pub peacock: SpeciesMeta, +} + +impl core::ops::Index for AllSpecies { + 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] = [ Species::Duck, Species::Chicken, diff --git a/common/src/comp/body/bird_small.rs b/common/src/comp/body/bird_small.rs index 4b84df734d..92584d1aa0 100644 --- a/common/src/comp/body/bird_small.rs +++ b/common/src/comp/body/bird_small.rs @@ -1,7 +1,6 @@ use rand::{seq::SliceRandom, thread_rng}; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[repr(C)] pub struct Body { pub head: Head, pub torso: Torso, diff --git a/common/src/comp/body/critter.rs b/common/src/comp/body/critter.rs index 622ac03167..6849db47e7 100644 --- a/common/src/comp/body/critter.rs +++ b/common/src/comp/body/critter.rs @@ -1,7 +1,6 @@ use rand::{seq::SliceRandom, thread_rng}; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[repr(C)] pub struct Body { pub species: Species, pub body_type: BodyType, @@ -26,6 +25,33 @@ pub enum Species { Squirrel = 4, Fungome = 5, } + +/// Data representing per-species generic data. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AllSpecies { + pub rat: SpeciesMeta, + pub axolotl: SpeciesMeta, + pub gecko: SpeciesMeta, + pub turtle: SpeciesMeta, + pub squirrel: SpeciesMeta, + pub fungome: SpeciesMeta, +} + +impl core::ops::Index for AllSpecies { + 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] = [ Species::Rat, Species::Axolotl, diff --git a/common/src/comp/body/dragon.rs b/common/src/comp/body/dragon.rs index 20b26ba192..94c23ce9b9 100644 --- a/common/src/comp/body/dragon.rs +++ b/common/src/comp/body/dragon.rs @@ -1,7 +1,6 @@ use rand::{seq::SliceRandom, thread_rng}; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[repr(C)] pub struct Body { pub head: Head, pub chest_front: ChestFront, diff --git a/common/src/comp/body/fish_medium.rs b/common/src/comp/body/fish_medium.rs index 52a7a1af18..47d715fd55 100644 --- a/common/src/comp/body/fish_medium.rs +++ b/common/src/comp/body/fish_medium.rs @@ -1,7 +1,6 @@ use rand::{seq::SliceRandom, thread_rng}; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[repr(C)] pub struct Body { pub head: Head, pub torso: Torso, diff --git a/common/src/comp/body/fish_small.rs b/common/src/comp/body/fish_small.rs index 57f2b5a495..18d3c5f5e2 100644 --- a/common/src/comp/body/fish_small.rs +++ b/common/src/comp/body/fish_small.rs @@ -1,7 +1,6 @@ use rand::{seq::SliceRandom, thread_rng}; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[repr(C)] pub struct Body { pub torso: Torso, pub tail: Tail, diff --git a/common/src/comp/body/giant.rs b/common/src/comp/body/giant.rs deleted file mode 100644 index 2980f6651c..0000000000 --- a/common/src/comp/body/giant.rs +++ /dev/null @@ -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, -} - - - diff --git a/common/src/comp/body/humanoid.rs b/common/src/comp/body/humanoid.rs index bb80eb375b..c75d899de2 100644 --- a/common/src/comp/body/humanoid.rs +++ b/common/src/comp/body/humanoid.rs @@ -2,7 +2,6 @@ use rand::{seq::SliceRandom, thread_rng, Rng}; use vek::Rgb; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[repr(C)] pub struct Body { pub race: Race, pub body_type: BodyType, @@ -69,6 +68,35 @@ pub enum Race { Orc = 4, Undead = 5, } + +/// Data representing per-species generic data. +/// +/// NOTE: Deliberately don't (yet?) implement serialize. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AllSpecies { + pub danari: SpeciesMeta, + pub dwarf: SpeciesMeta, + pub elf: SpeciesMeta, + pub human: SpeciesMeta, + pub orc: SpeciesMeta, + pub undead: SpeciesMeta, +} + +impl core::ops::Index for AllSpecies { + 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] = [ Race::Danari, Race::Dwarf, diff --git a/common/src/comp/body/quadruped_medium.rs b/common/src/comp/body/quadruped_medium.rs index 71362d7e42..94c42ebe1e 100644 --- a/common/src/comp/body/quadruped_medium.rs +++ b/common/src/comp/body/quadruped_medium.rs @@ -1,7 +1,6 @@ use rand::{seq::SliceRandom, thread_rng}; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[repr(C)] pub struct Body { pub species: Species, pub body_type: BodyType, @@ -28,6 +27,39 @@ pub enum Species { Lion = 6, Tarasque = 7, } + +/// Data representing per-species generic data. +/// +/// NOTE: Deliberately don't (yet?) implement serialize. +#[derive(Clone, Debug, Deserialize)] +pub struct AllSpecies { + 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 core::ops::Index for AllSpecies { + 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] = [ Species::Wolf, Species::Saber, diff --git a/common/src/comp/body/quadruped_small.rs b/common/src/comp/body/quadruped_small.rs index 7a24087a26..e3b86612d9 100644 --- a/common/src/comp/body/quadruped_small.rs +++ b/common/src/comp/body/quadruped_small.rs @@ -1,7 +1,6 @@ use rand::{seq::SliceRandom, thread_rng}; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[repr(C)] pub struct Body { pub species: Species, pub body_type: BodyType, @@ -32,6 +31,47 @@ pub enum Species { Dodarock = 10, Holladon = 11, } + +/// Data representing per-species generic data. +/// +/// NOTE: Deliberately don't (yet?) implement serialize. +#[derive(Clone, Debug, Deserialize)] +pub struct AllSpecies { + 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 core::ops::Index for AllSpecies { + 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] = [ Species::Pig, Species::Fox, diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 16d9bf7a72..24b1be9efb 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -19,7 +19,7 @@ pub use admin::Admin; pub use agent::{Agent, Alignment}; pub use body::{ 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 controller::{ diff --git a/common/src/npc.rs b/common/src/npc.rs index c2412f341e..eb578fe649 100644 --- a/common/src/npc.rs +++ b/common/src/npc.rs @@ -1,7 +1,6 @@ -use crate::assets; +use crate::{assets, comp::AllBodies}; use lazy_static::lazy_static; use rand::seq::SliceRandom; -use serde_json; use std::str::FromStr; use std::sync::Arc; @@ -15,50 +14,61 @@ pub enum NpcKind { Rat, } -impl NpcKind { - fn as_str(self) -> &'static str { - match self { - NpcKind::Humanoid => "humanoid", - NpcKind::Wolf => "wolf", - NpcKind::Pig => "pig", - NpcKind::Duck => "duck", - NpcKind::Giant => "giant", - NpcKind::Rat => "rat", - } - } +pub const ALL_NPCS: [NpcKind; 6] = [ + NpcKind::Humanoid, + NpcKind::Wolf, + NpcKind::Pig, + NpcKind::Duck, + NpcKind::Giant, + NpcKind::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, +} + +/// 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; + +lazy_static! { + pub static ref NPC_NAMES: Arc = assets::load_expect("common.npc_names"); } impl FromStr for NpcKind { type Err = (); fn from_str(s: &str) -> Result { - match s { - "humanoid" => Ok(NpcKind::Humanoid), - "wolf" => Ok(NpcKind::Wolf), - "pig" => Ok(NpcKind::Pig), - "duck" => Ok(NpcKind::Duck), - "giant" => Ok(NpcKind::Giant), - "rat" => Ok(NpcKind::Rat), - - _ => Err(()), - } + let npc_names_json = &*NPC_NAMES; + ALL_NPCS + .iter() + .copied() + .find(|&npc| npc_names_json[npc].keyword == s) + .ok_or(()) } } -lazy_static! { - static ref NPC_NAMES_JSON: Arc = assets::load_expect("common.npc_names"); -} +pub fn get_npc_name(npc_type: NpcKind) -> &'static str { + let BodyNames { keyword, names } = &NPC_NAMES[npc_type]; -pub fn get_npc_name(npc_type: NpcKind) -> String { - let npc_names = NPC_NAMES_JSON - .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) + // If no pretty name is found, fall back to the keyword. + names.choose(&mut rand::thread_rng()).unwrap_or(keyword) } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 33e0eb6ca1..c6662d5c81 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -487,7 +487,7 @@ fn handle_spawn(server: &mut Server, entity: EcsEntity, args: String, action: &C .state .create_npc( pos, - comp::Stats::new(get_npc_name(id), body, None), + comp::Stats::new(get_npc_name(id).into(), body, None), body, ) .with(comp::Vel(vel)) diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index b243a7b445..c0c5e1b559 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -6,6 +6,7 @@ use common::{ event::{EventBus, ServerEvent}, generation::EntityKind, msg::ServerMsg, + npc::{self, NPC_NAMES}, state::TerrainChanges, terrain::TerrainGrid, }; @@ -100,6 +101,15 @@ impl<'a> System<'a> for Sys { if let EntityKind::Waypoint = entity.kind { server_emitter.emit(ServerEvent::CreateWaypoint(entity.pos)); } else { + fn get_npc_name< + Species, + SpeciesData: core::ops::Index, + >( + body_data: &comp::BodyData, + species: Species, + ) -> &str { + &body_data.species[species].generic + } const SPAWN_NPCS: &'static [fn() -> ( String, comp::Body, @@ -107,49 +117,58 @@ impl<'a> System<'a> for Sys { comp::Alignment, )] = &[ (|| { + let body = comp::humanoid::Body::random(); ( - "Traveler".into(), - comp::Body::Humanoid(comp::humanoid::Body::random()), + format!( + "{} Traveler", + get_npc_name(&NPC_NAMES.humanoid, body.race) + ), + comp::Body::Humanoid(body), Some(assets::load_expect_cloned("common.items.weapons.staff_1")), comp::Alignment::Npc, ) }) as _, (|| { + let body = comp::humanoid::Body::random(); ( - "Bandit".into(), - comp::Body::Humanoid(comp::humanoid::Body::random()), + format!("{} Bandit", get_npc_name(&NPC_NAMES.humanoid, body.race)), + comp::Body::Humanoid(body), Some(assets::load_expect_cloned("common.items.weapons.staff_1")), comp::Alignment::Enemy, ) }) as _, (|| { + let body = comp::quadruped_medium::Body::random(); ( - "Wolf".into(), - comp::Body::QuadrupedMedium(comp::quadruped_medium::Body::random()), + get_npc_name(&NPC_NAMES.quadruped_medium, body.species).into(), + comp::Body::QuadrupedMedium(body), None, comp::Alignment::Enemy, ) }) as _, (|| { + let body = comp::bird_medium::Body::random(); ( - "Duck".into(), - comp::Body::BirdMedium(comp::bird_medium::Body::random()), + get_npc_name(&NPC_NAMES.bird_medium, body.species).into(), + comp::Body::BirdMedium(body), None, comp::Alignment::Wild, ) }) as _, (|| { + let body = comp::critter::Body::random(); ( - "Rat".into(), - comp::Body::Critter(comp::critter::Body::random()), + get_npc_name(&NPC_NAMES.critter, body.species).into(), + comp::Body::Critter(body), None, comp::Alignment::Wild, ) }) as _, (|| { + let body = comp::quadruped_small::Body::random(); ( - "Pig".into(), - comp::Body::QuadrupedSmall(comp::quadruped_small::Body::random()), + get_npc_name(&NPC_NAMES.quadruped_small, body.species).into(), + comp::Body::QuadrupedSmall(body), None, comp::Alignment::Wild, ) @@ -168,10 +187,14 @@ impl<'a> System<'a> for Sys { if let EntityKind::Boss = entity.kind { if rand::random::() < 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; stats = comp::Stats::new( - "Fearless Giant".to_string(), + format!( + "Fearless Giant {}", + get_npc_name(&NPC_NAMES.humanoid, body_new.race) + ), body, Some(assets::load_expect_cloned("common.items.weapons.hammer_1")), );