1use crate::{AvatarCatalog, AvatarPart, AvatarRaceMode, AvatarSlot};
16use thread_id::StructuredId;
17
18pub const CAT_AVATAR_PART: u8 = 50;
20pub const CAT_AVATAR_RACE: u8 = 51;
22
23mod ids {
26 use super::CAT_AVATAR_PART;
27 const fn part(sub: u32, num: u32) -> u32 {
28 CAT_AVATAR_PART as u32 * 1_000_000 + sub * 10_000 + num
29 }
30 pub const SKIN_SMOOTH: u32 = part(1, 1);
31 pub const SKIN_SCALE: u32 = part(1, 2);
32 pub const SKIN_FUR: u32 = part(1, 3);
33 pub const SKIN_FEATHER: u32 = part(1, 4);
34 pub const TAIL_REPTILE: u32 = part(2, 1);
35 pub const TAIL_BEAST: u32 = part(2, 2);
36 pub const CLAWS_BASIC: u32 = part(3, 1);
37 pub const COAT_FUR: u32 = part(4, 1);
38 pub const WINGS_FEATHER: u32 = part(5, 1);
39 pub const HORNS_BASIC: u32 = part(6, 1);
40 pub const SNOUT_BEAST: u32 = part(7, 1);
41 pub const SNOUT_BEAK: u32 = part(7, 2);
42 pub const EARS_BEAST: u32 = part(8, 1);
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum CanonicalRace {
48 Humen,
49 Hedningr,
50 Arra,
51 Draumander,
52}
53
54impl CanonicalRace {
55 pub const ALL: [CanonicalRace; 4] = [
56 CanonicalRace::Humen,
57 CanonicalRace::Hedningr,
58 CanonicalRace::Arra,
59 CanonicalRace::Draumander,
60 ];
61
62 pub fn slug(self) -> &'static str {
64 match self {
65 CanonicalRace::Humen => "humen",
66 CanonicalRace::Hedningr => "reptillian",
67 CanonicalRace::Arra => "arra",
68 CanonicalRace::Draumander => "skyfolk",
69 }
70 }
71
72 pub fn name(self) -> &'static str {
74 match self {
75 CanonicalRace::Humen => "Humen",
76 CanonicalRace::Hedningr => "Hedningr",
77 CanonicalRace::Arra => "Arra",
78 CanonicalRace::Draumander => "Draumander",
79 }
80 }
81
82 fn race_number(self) -> u16 {
83 match self {
84 CanonicalRace::Humen => 1,
85 CanonicalRace::Hedningr => 2,
86 CanonicalRace::Arra => 3,
87 CanonicalRace::Draumander => 4,
88 }
89 }
90
91 pub fn race_mode(self) -> AvatarRaceMode {
93 let id = StructuredId::new(CAT_AVATAR_RACE, 0, self.race_number());
94 let mut race = AvatarRaceMode::new(id, self.name());
95 match self {
96 CanonicalRace::Humen => {
97 race = race.with_default(AvatarSlot::SkinType, ids::SKIN_SMOOTH);
98 }
99 CanonicalRace::Hedningr => {
100 race = race
101 .with_default(AvatarSlot::SkinType, ids::SKIN_SCALE)
102 .with_default(AvatarSlot::Tail, ids::TAIL_REPTILE)
103 .with_default(AvatarSlot::Claws, ids::CLAWS_BASIC)
104 .with_default(AvatarSlot::Horns, ids::HORNS_BASIC);
105 }
106 CanonicalRace::Arra => {
107 race = race
108 .with_default(AvatarSlot::SkinType, ids::SKIN_FUR)
109 .with_default(AvatarSlot::Coat, ids::COAT_FUR)
110 .with_default(AvatarSlot::Claws, ids::CLAWS_BASIC)
111 .with_default(AvatarSlot::Snout, ids::SNOUT_BEAST)
112 .with_default(AvatarSlot::Ears, ids::EARS_BEAST)
113 .with_default(AvatarSlot::Tail, ids::TAIL_BEAST);
114 }
115 CanonicalRace::Draumander => {
116 race = race
117 .with_default(AvatarSlot::SkinType, ids::SKIN_FEATHER)
118 .with_default(AvatarSlot::Wings, ids::WINGS_FEATHER)
119 .with_default(AvatarSlot::Snout, ids::SNOUT_BEAK);
120 }
121 }
122 race
123 }
124}
125
126pub fn canonical_races() -> Vec<AvatarRaceMode> {
128 CanonicalRace::ALL.iter().map(|r| r.race_mode()).collect()
129}
130
131pub fn biology_catalog() -> AvatarCatalog {
134 let mut cat = AvatarCatalog::new();
135 let entries: &[(u32, AvatarSlot, &str)] = &[
136 (ids::SKIN_SMOOTH, AvatarSlot::SkinType, "Smooth Skin"),
137 (ids::SKIN_SCALE, AvatarSlot::SkinType, "Scale Skin"),
138 (ids::SKIN_FUR, AvatarSlot::SkinType, "Fur Skin"),
139 (ids::SKIN_FEATHER, AvatarSlot::SkinType, "Feather Skin"),
140 (ids::TAIL_REPTILE, AvatarSlot::Tail, "Reptile Tail"),
141 (ids::TAIL_BEAST, AvatarSlot::Tail, "Beast Tail"),
142 (ids::CLAWS_BASIC, AvatarSlot::Claws, "Claws"),
143 (ids::COAT_FUR, AvatarSlot::Coat, "Fur Coat"),
144 (ids::WINGS_FEATHER, AvatarSlot::Wings, "Feathered Wings"),
145 (ids::HORNS_BASIC, AvatarSlot::Horns, "Horns"),
146 (ids::SNOUT_BEAST, AvatarSlot::Snout, "Beast Snout"),
147 (ids::SNOUT_BEAK, AvatarSlot::Snout, "Beak"),
148 (ids::EARS_BEAST, AvatarSlot::Ears, "Beast Ears"),
149 ];
150 for &(id, slot, name) in entries {
151 cat.add(AvatarPart::mesh_part(
152 StructuredId(id),
153 slot,
154 name,
155 "placeholder",
156 ));
157 }
158 cat
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use crate::{AvatarSpec, SlotCategory};
165
166 #[test]
167 fn four_canonical_races() {
168 assert_eq!(CanonicalRace::ALL.len(), 4);
169 assert_eq!(canonical_races().len(), 4);
170 assert_eq!(CanonicalRace::Hedningr.name(), "Hedningr");
171 assert_eq!(CanonicalRace::Hedningr.slug(), "reptillian");
172 assert_eq!(CanonicalRace::Draumander.slug(), "skyfolk");
173 }
174
175 #[test]
176 fn hedningr_is_a_reptile_body() {
177 let mut spec = AvatarSpec::new();
178 CanonicalRace::Hedningr.race_mode().apply_to(&mut spec);
179 assert_eq!(spec.get(AvatarSlot::SkinType), ids::SKIN_SCALE);
180 assert!(spec.has(AvatarSlot::Tail));
181 assert!(spec.has(AvatarSlot::Claws));
182 assert!(spec.has(AvatarSlot::Horns));
183 assert!(!spec.has(AvatarSlot::Wings));
185 }
186
187 #[test]
188 fn draumander_has_wings() {
189 let mut spec = AvatarSpec::new();
190 CanonicalRace::Draumander.race_mode().apply_to(&mut spec);
191 assert!(spec.has(AvatarSlot::Wings));
192 assert_eq!(spec.get(AvatarSlot::Snout), ids::SNOUT_BEAK);
193 }
194
195 #[test]
196 fn humen_is_baseline() {
197 let mut spec = AvatarSpec::new();
198 CanonicalRace::Humen.race_mode().apply_to(&mut spec);
199 assert_eq!(spec.get(AvatarSlot::SkinType), ids::SKIN_SMOOTH);
201 for s in [
202 AvatarSlot::Tail,
203 AvatarSlot::Claws,
204 AvatarSlot::Wings,
205 AvatarSlot::Horns,
206 AvatarSlot::Coat,
207 ] {
208 assert!(!spec.has(s), "Humen should not have {s:?}");
209 }
210 }
211
212 #[test]
213 fn all_race_defaults_are_biology_and_resolve_in_catalog() {
214 let cat = biology_catalog();
215 for race in canonical_races() {
216 let mut spec = AvatarSpec::new();
217 race.apply_to(&mut spec);
218 for (slot, id) in spec.iter() {
219 assert_eq!(
220 slot.category(),
221 SlotCategory::Biology,
222 "race default in non-biology slot"
223 );
224 assert!(
225 cat.get(StructuredId(id)).is_some(),
226 "part {id} not in biology catalog"
227 );
228 }
229 }
230 }
231}