use crate::{AvatarCatalog, AvatarPart, AvatarRaceMode, AvatarSlot};
use thread_id::StructuredId;
pub const CAT_AVATAR_PART: u8 = 50;
pub const CAT_AVATAR_RACE: u8 = 51;
mod ids {
use super::CAT_AVATAR_PART;
const fn part(sub: u32, num: u32) -> u32 {
CAT_AVATAR_PART as u32 * 1_000_000 + sub * 10_000 + num
}
pub const SKIN_SMOOTH: u32 = part(1, 1);
pub const SKIN_SCALE: u32 = part(1, 2);
pub const SKIN_FUR: u32 = part(1, 3);
pub const SKIN_FEATHER: u32 = part(1, 4);
pub const TAIL_REPTILE: u32 = part(2, 1);
pub const TAIL_BEAST: u32 = part(2, 2);
pub const CLAWS_BASIC: u32 = part(3, 1);
pub const COAT_FUR: u32 = part(4, 1);
pub const WINGS_FEATHER: u32 = part(5, 1);
pub const HORNS_BASIC: u32 = part(6, 1);
pub const SNOUT_BEAST: u32 = part(7, 1);
pub const SNOUT_BEAK: u32 = part(7, 2);
pub const EARS_BEAST: u32 = part(8, 1);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CanonicalRace {
Humen,
Hedningr,
Arra,
Draumander,
}
impl CanonicalRace {
pub const ALL: [CanonicalRace; 4] = [
CanonicalRace::Humen,
CanonicalRace::Hedningr,
CanonicalRace::Arra,
CanonicalRace::Draumander,
];
pub fn slug(self) -> &'static str {
match self {
CanonicalRace::Humen => "humen",
CanonicalRace::Hedningr => "reptillian",
CanonicalRace::Arra => "arra",
CanonicalRace::Draumander => "skyfolk",
}
}
pub fn name(self) -> &'static str {
match self {
CanonicalRace::Humen => "Humen",
CanonicalRace::Hedningr => "Hedningr",
CanonicalRace::Arra => "Arra",
CanonicalRace::Draumander => "Draumander",
}
}
fn race_number(self) -> u16 {
match self {
CanonicalRace::Humen => 1,
CanonicalRace::Hedningr => 2,
CanonicalRace::Arra => 3,
CanonicalRace::Draumander => 4,
}
}
pub fn race_mode(self) -> AvatarRaceMode {
let id = StructuredId::new(CAT_AVATAR_RACE, 0, self.race_number());
let mut race = AvatarRaceMode::new(id, self.name());
match self {
CanonicalRace::Humen => {
race = race.with_default(AvatarSlot::SkinType, ids::SKIN_SMOOTH);
}
CanonicalRace::Hedningr => {
race = race
.with_default(AvatarSlot::SkinType, ids::SKIN_SCALE)
.with_default(AvatarSlot::Tail, ids::TAIL_REPTILE)
.with_default(AvatarSlot::Claws, ids::CLAWS_BASIC)
.with_default(AvatarSlot::Horns, ids::HORNS_BASIC);
}
CanonicalRace::Arra => {
race = race
.with_default(AvatarSlot::SkinType, ids::SKIN_FUR)
.with_default(AvatarSlot::Coat, ids::COAT_FUR)
.with_default(AvatarSlot::Claws, ids::CLAWS_BASIC)
.with_default(AvatarSlot::Snout, ids::SNOUT_BEAST)
.with_default(AvatarSlot::Ears, ids::EARS_BEAST)
.with_default(AvatarSlot::Tail, ids::TAIL_BEAST);
}
CanonicalRace::Draumander => {
race = race
.with_default(AvatarSlot::SkinType, ids::SKIN_FEATHER)
.with_default(AvatarSlot::Wings, ids::WINGS_FEATHER)
.with_default(AvatarSlot::Snout, ids::SNOUT_BEAK);
}
}
race
}
}
pub fn canonical_races() -> Vec<AvatarRaceMode> {
CanonicalRace::ALL.iter().map(|r| r.race_mode()).collect()
}
pub fn biology_catalog() -> AvatarCatalog {
let mut cat = AvatarCatalog::new();
let entries: &[(u32, AvatarSlot, &str)] = &[
(ids::SKIN_SMOOTH, AvatarSlot::SkinType, "Smooth Skin"),
(ids::SKIN_SCALE, AvatarSlot::SkinType, "Scale Skin"),
(ids::SKIN_FUR, AvatarSlot::SkinType, "Fur Skin"),
(ids::SKIN_FEATHER, AvatarSlot::SkinType, "Feather Skin"),
(ids::TAIL_REPTILE, AvatarSlot::Tail, "Reptile Tail"),
(ids::TAIL_BEAST, AvatarSlot::Tail, "Beast Tail"),
(ids::CLAWS_BASIC, AvatarSlot::Claws, "Claws"),
(ids::COAT_FUR, AvatarSlot::Coat, "Fur Coat"),
(ids::WINGS_FEATHER, AvatarSlot::Wings, "Feathered Wings"),
(ids::HORNS_BASIC, AvatarSlot::Horns, "Horns"),
(ids::SNOUT_BEAST, AvatarSlot::Snout, "Beast Snout"),
(ids::SNOUT_BEAK, AvatarSlot::Snout, "Beak"),
(ids::EARS_BEAST, AvatarSlot::Ears, "Beast Ears"),
];
for &(id, slot, name) in entries {
cat.add(AvatarPart::mesh_part(
StructuredId(id),
slot,
name,
"placeholder",
));
}
cat
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{AvatarSpec, SlotCategory};
#[test]
fn four_canonical_races() {
assert_eq!(CanonicalRace::ALL.len(), 4);
assert_eq!(canonical_races().len(), 4);
assert_eq!(CanonicalRace::Hedningr.name(), "Hedningr");
assert_eq!(CanonicalRace::Hedningr.slug(), "reptillian");
assert_eq!(CanonicalRace::Draumander.slug(), "skyfolk");
}
#[test]
fn hedningr_is_a_reptile_body() {
let mut spec = AvatarSpec::new();
CanonicalRace::Hedningr.race_mode().apply_to(&mut spec);
assert_eq!(spec.get(AvatarSlot::SkinType), ids::SKIN_SCALE);
assert!(spec.has(AvatarSlot::Tail));
assert!(spec.has(AvatarSlot::Claws));
assert!(spec.has(AvatarSlot::Horns));
assert!(!spec.has(AvatarSlot::Wings));
}
#[test]
fn draumander_has_wings() {
let mut spec = AvatarSpec::new();
CanonicalRace::Draumander.race_mode().apply_to(&mut spec);
assert!(spec.has(AvatarSlot::Wings));
assert_eq!(spec.get(AvatarSlot::Snout), ids::SNOUT_BEAK);
}
#[test]
fn humen_is_baseline() {
let mut spec = AvatarSpec::new();
CanonicalRace::Humen.race_mode().apply_to(&mut spec);
assert_eq!(spec.get(AvatarSlot::SkinType), ids::SKIN_SMOOTH);
for s in [
AvatarSlot::Tail,
AvatarSlot::Claws,
AvatarSlot::Wings,
AvatarSlot::Horns,
AvatarSlot::Coat,
] {
assert!(!spec.has(s), "Humen should not have {s:?}");
}
}
#[test]
fn all_race_defaults_are_biology_and_resolve_in_catalog() {
let cat = biology_catalog();
for race in canonical_races() {
let mut spec = AvatarSpec::new();
race.apply_to(&mut spec);
for (slot, id) in spec.iter() {
assert_eq!(
slot.category(),
SlotCategory::Biology,
"race default in non-biology slot"
);
assert!(
cat.get(StructuredId(id)).is_some(),
"part {id} not in biology catalog"
);
}
}
}
}