Skip to main content

oxihuman_morph/
body_preset.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Named body type presets for character creation.
5
6#![allow(dead_code)]
7
8use std::collections::HashMap;
9
10/// Parameter map: param name → value in [0.0, 1.0].
11pub type BodyParams = HashMap<String, f32>;
12
13// ---------------------------------------------------------------------------
14// BodyCategory
15// ---------------------------------------------------------------------------
16
17/// Body type categories used to classify presets.
18#[derive(Clone, Debug, PartialEq, Eq, Hash)]
19pub enum BodyCategory {
20    /// Slim, low body fat.
21    Ectomorph,
22    /// Muscular, athletic.
23    Mesomorph,
24    /// Rounder, higher body fat.
25    Endomorph,
26    /// Balanced average.
27    Average,
28    /// Short stature.
29    Petite,
30    /// Tall stature.
31    Tall,
32    /// Child proportions.
33    Child,
34    /// Elderly proportions.
35    Elder,
36    /// Custom (user-defined).
37    Custom(String),
38}
39
40impl BodyCategory {
41    /// Short name for the category.
42    pub fn name(&self) -> &str {
43        match self {
44            BodyCategory::Ectomorph => "Ectomorph",
45            BodyCategory::Mesomorph => "Mesomorph",
46            BodyCategory::Endomorph => "Endomorph",
47            BodyCategory::Average => "Average",
48            BodyCategory::Petite => "Petite",
49            BodyCategory::Tall => "Tall",
50            BodyCategory::Child => "Child",
51            BodyCategory::Elder => "Elder",
52            BodyCategory::Custom(s) => s.as_str(),
53        }
54    }
55
56    /// Human-readable description of the category.
57    pub fn description(&self) -> &str {
58        match self {
59            BodyCategory::Ectomorph => "Slim body type with low body fat and lean muscle.",
60            BodyCategory::Mesomorph => "Muscular and athletic body type.",
61            BodyCategory::Endomorph => "Rounder body type with higher body fat.",
62            BodyCategory::Average => "Balanced, average body proportions.",
63            BodyCategory::Petite => "Short stature with proportionally smaller frame.",
64            BodyCategory::Tall => "Tall stature with elongated proportions.",
65            BodyCategory::Child => "Child body proportions with low muscle mass.",
66            BodyCategory::Elder => "Elderly body proportions with reduced muscle mass.",
67            BodyCategory::Custom(_) => "User-defined custom body category.",
68        }
69    }
70
71    /// All named (non-Custom) categories.
72    pub fn all_named() -> Vec<BodyCategory> {
73        vec![
74            BodyCategory::Ectomorph,
75            BodyCategory::Mesomorph,
76            BodyCategory::Endomorph,
77            BodyCategory::Average,
78            BodyCategory::Petite,
79            BodyCategory::Tall,
80            BodyCategory::Child,
81            BodyCategory::Elder,
82        ]
83    }
84}
85
86// ---------------------------------------------------------------------------
87// BodyPreset
88// ---------------------------------------------------------------------------
89
90/// A named body preset with a full parameter set.
91pub struct BodyPreset {
92    /// Unique preset name.
93    pub name: String,
94    /// Body type category.
95    pub category: BodyCategory,
96    /// Human-readable description.
97    pub description: String,
98    /// Parameter map.
99    pub params: BodyParams,
100    /// Tags for filtering and search.
101    pub tags: Vec<String>,
102}
103
104impl BodyPreset {
105    /// Create a new preset with default empty params.
106    pub fn new(name: impl Into<String>, category: BodyCategory) -> Self {
107        Self {
108            name: name.into(),
109            category,
110            description: String::new(),
111            params: HashMap::new(),
112            tags: Vec::new(),
113        }
114    }
115
116    /// Builder: set a parameter value.
117    pub fn with_param(mut self, key: impl Into<String>, value: f32) -> Self {
118        self.params.insert(key.into(), value);
119        self
120    }
121
122    /// Builder: set description.
123    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
124        self.description = desc.into();
125        self
126    }
127
128    /// Builder: add a tag.
129    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
130        self.tags.push(tag.into());
131        self
132    }
133
134    /// Get a parameter value; returns 0.5 if the key is missing.
135    pub fn get_param(&self, key: &str) -> f32 {
136        *self.params.get(key).unwrap_or(&0.5)
137    }
138
139    /// Number of parameters stored in this preset.
140    pub fn param_count(&self) -> usize {
141        self.params.len()
142    }
143}
144
145// ---------------------------------------------------------------------------
146// PresetLibrary
147// ---------------------------------------------------------------------------
148
149/// A searchable library of named body presets.
150pub struct PresetLibrary {
151    presets: Vec<BodyPreset>,
152}
153
154impl PresetLibrary {
155    /// Create an empty library.
156    pub fn new() -> Self {
157        Self {
158            presets: Vec::new(),
159        }
160    }
161
162    /// Add a preset to the library.
163    pub fn add(&mut self, preset: BodyPreset) {
164        self.presets.push(preset);
165    }
166
167    /// Look up a preset by name (case-sensitive).
168    pub fn get(&self, name: &str) -> Option<&BodyPreset> {
169        self.presets.iter().find(|p| p.name == name)
170    }
171
172    /// Number of presets in the library.
173    pub fn preset_count(&self) -> usize {
174        self.presets.len()
175    }
176
177    /// All presets belonging to a given category.
178    pub fn by_category(&self, cat: &BodyCategory) -> Vec<&BodyPreset> {
179        self.presets.iter().filter(|p| &p.category == cat).collect()
180    }
181
182    /// All presets that have the given tag.
183    pub fn with_tag(&self, tag: &str) -> Vec<&BodyPreset> {
184        self.presets
185            .iter()
186            .filter(|p| p.tags.iter().any(|t| t == tag))
187            .collect()
188    }
189
190    /// Names of all presets in the library.
191    pub fn names(&self) -> Vec<&str> {
192        self.presets.iter().map(|p| p.name.as_str()).collect()
193    }
194
195    /// Blend two presets at weight `t` (0 = fully a, 1 = fully b).
196    ///
197    /// Missing params default to 0.5.  Returns `None` if either name is unknown.
198    pub fn blend(&self, name_a: &str, name_b: &str, t: f32) -> Option<BodyParams> {
199        let a = self.get(name_a)?;
200        let b = self.get(name_b)?;
201
202        // Collect union of all keys
203        let mut keys: std::collections::HashSet<&str> = std::collections::HashSet::new();
204        for k in a.params.keys() {
205            keys.insert(k.as_str());
206        }
207        for k in b.params.keys() {
208            keys.insert(k.as_str());
209        }
210
211        let mut result = HashMap::new();
212        for key in keys {
213            let va = a.get_param(key);
214            let vb = b.get_param(key);
215            result.insert(key.to_string(), va + (vb - va) * t);
216        }
217        Some(result)
218    }
219
220    /// Find the nearest preset to the given params by L2 distance in param space.
221    ///
222    /// Missing params in either side default to 0.5.
223    pub fn nearest(&self, params: &BodyParams) -> Option<&BodyPreset> {
224        // Collect all keys appearing in `params`
225        let keys: Vec<&str> = params.keys().map(|k| k.as_str()).collect();
226
227        self.presets.iter().min_by(|a, b| {
228            let dist_a = l2_distance(a, params, &keys);
229            let dist_b = l2_distance(b, params, &keys);
230            dist_a
231                .partial_cmp(&dist_b)
232                .unwrap_or(std::cmp::Ordering::Equal)
233        })
234    }
235}
236
237impl Default for PresetLibrary {
238    fn default() -> Self {
239        Self::new()
240    }
241}
242
243/// Compute L2 distance between a preset and a param map over the given keys.
244fn l2_distance(preset: &BodyPreset, params: &BodyParams, keys: &[&str]) -> f32 {
245    keys.iter()
246        .map(|k| {
247            let va = preset.get_param(k);
248            let vb = *params.get(*k).unwrap_or(&0.5);
249            (va - vb) * (va - vb)
250        })
251        .sum::<f32>()
252        .sqrt()
253}
254
255// ---------------------------------------------------------------------------
256// Individual preset constructors
257// ---------------------------------------------------------------------------
258
259/// Average adult — all params at 0.5 (neutral starting point).
260pub fn preset_average() -> BodyPreset {
261    BodyPreset::new("average", BodyCategory::Average)
262        .with_description("Average adult with balanced proportions.")
263        .with_param("height", 0.5)
264        .with_param("weight", 0.5)
265        .with_param("muscle", 0.5)
266        .with_param("age", 0.5)
267        .with_param("bmi_factor", 0.5)
268        .with_param("shoulder_width", 0.5)
269        .with_param("hip_width", 0.5)
270        .with_param("leg_length", 0.5)
271        .with_param("torso_length", 0.5)
272        .with_param("arm_length", 0.5)
273        .with_tag("neutral")
274        .with_tag("adult")
275}
276
277/// Athletic build — high muscle, moderate weight, young adult.
278pub fn preset_athletic() -> BodyPreset {
279    BodyPreset::new("athletic", BodyCategory::Mesomorph)
280        .with_description("Athletic build with high muscle tone and low body fat.")
281        .with_param("height", 0.55)
282        .with_param("weight", 0.45)
283        .with_param("muscle", 0.75)
284        .with_param("age", 0.3)
285        .with_param("bmi_factor", 0.35)
286        .with_param("shoulder_width", 0.65)
287        .with_param("hip_width", 0.5)
288        .with_param("leg_length", 0.5)
289        .with_param("torso_length", 0.5)
290        .with_param("arm_length", 0.5)
291        .with_tag("fit")
292        .with_tag("adult")
293        .with_tag("sport")
294}
295
296/// Slender build — low weight and muscle, ectomorphic.
297pub fn preset_slender() -> BodyPreset {
298    BodyPreset::new("slender", BodyCategory::Ectomorph)
299        .with_description("Slender build with low body fat and lean muscle.")
300        .with_param("height", 0.55)
301        .with_param("weight", 0.2)
302        .with_param("muscle", 0.3)
303        .with_param("age", 0.3)
304        .with_param("bmi_factor", 0.15)
305        .with_param("shoulder_width", 0.45)
306        .with_param("hip_width", 0.5)
307        .with_param("leg_length", 0.5)
308        .with_param("torso_length", 0.5)
309        .with_param("arm_length", 0.5)
310        .with_tag("slim")
311        .with_tag("adult")
312}
313
314/// Heavy build — high weight, endomorphic.
315pub fn preset_heavy() -> BodyPreset {
316    BodyPreset::new("heavy", BodyCategory::Endomorph)
317        .with_description("Heavy build with higher body fat and rounded proportions.")
318        .with_param("height", 0.45)
319        .with_param("weight", 0.85)
320        .with_param("muscle", 0.3)
321        .with_param("age", 0.45)
322        .with_param("bmi_factor", 0.85)
323        .with_param("shoulder_width", 0.5)
324        .with_param("hip_width", 0.5)
325        .with_param("leg_length", 0.5)
326        .with_param("torso_length", 0.5)
327        .with_param("arm_length", 0.5)
328        .with_tag("heavy")
329        .with_tag("adult")
330}
331
332/// Muscular build — high muscle and broad shoulders.
333pub fn preset_muscular() -> BodyPreset {
334    BodyPreset::new("muscular", BodyCategory::Mesomorph)
335        .with_description("Highly muscular build with broad shoulders.")
336        .with_param("height", 0.6)
337        .with_param("weight", 0.6)
338        .with_param("muscle", 0.9)
339        .with_param("age", 0.35)
340        .with_param("bmi_factor", 0.5)
341        .with_param("shoulder_width", 0.8)
342        .with_param("hip_width", 0.5)
343        .with_param("leg_length", 0.5)
344        .with_param("torso_length", 0.5)
345        .with_param("arm_length", 0.5)
346        .with_tag("muscle")
347        .with_tag("adult")
348        .with_tag("sport")
349}
350
351/// Petite build — short stature and small frame.
352pub fn preset_petite() -> BodyPreset {
353    BodyPreset::new("petite", BodyCategory::Petite)
354        .with_description("Petite build with short stature and small proportions.")
355        .with_param("height", 0.2)
356        .with_param("weight", 0.35)
357        .with_param("muscle", 0.4)
358        .with_param("age", 0.5)
359        .with_param("bmi_factor", 0.5)
360        .with_param("shoulder_width", 0.5)
361        .with_param("hip_width", 0.5)
362        .with_param("leg_length", 0.3)
363        .with_param("torso_length", 0.35)
364        .with_param("arm_length", 0.5)
365        .with_tag("short")
366        .with_tag("adult")
367}
368
369/// Tall build — elongated legs and arms.
370pub fn preset_tall() -> BodyPreset {
371    BodyPreset::new("tall", BodyCategory::Tall)
372        .with_description("Tall build with elongated limbs and proportions.")
373        .with_param("height", 0.85)
374        .with_param("weight", 0.5)
375        .with_param("muscle", 0.5)
376        .with_param("age", 0.5)
377        .with_param("bmi_factor", 0.5)
378        .with_param("shoulder_width", 0.5)
379        .with_param("hip_width", 0.5)
380        .with_param("leg_length", 0.75)
381        .with_param("torso_length", 0.5)
382        .with_param("arm_length", 0.7)
383        .with_tag("tall")
384        .with_tag("adult")
385}
386
387/// Child proportions — young, low muscle, short.
388pub fn preset_child() -> BodyPreset {
389    BodyPreset::new("child", BodyCategory::Child)
390        .with_description("Child body proportions with low muscle mass and short stature.")
391        .with_param("height", 0.1)
392        .with_param("weight", 0.2)
393        .with_param("muscle", 0.2)
394        .with_param("age", 0.05)
395        .with_param("bmi_factor", 0.5)
396        .with_param("shoulder_width", 0.5)
397        .with_param("hip_width", 0.5)
398        .with_param("leg_length", 0.5)
399        .with_param("torso_length", 0.5)
400        .with_param("arm_length", 0.5)
401        .with_tag("child")
402        .with_tag("young")
403}
404
405/// Elder proportions — high age, reduced muscle.
406pub fn preset_elder() -> BodyPreset {
407    BodyPreset::new("elder", BodyCategory::Elder)
408        .with_description("Elderly body proportions with reduced muscle mass.")
409        .with_param("height", 0.45)
410        .with_param("weight", 0.55)
411        .with_param("muscle", 0.2)
412        .with_param("age", 0.9)
413        .with_param("bmi_factor", 0.5)
414        .with_param("shoulder_width", 0.5)
415        .with_param("hip_width", 0.5)
416        .with_param("leg_length", 0.5)
417        .with_param("torso_length", 0.5)
418        .with_param("arm_length", 0.5)
419        .with_tag("elder")
420        .with_tag("senior")
421}
422
423// ---------------------------------------------------------------------------
424// Standard library
425// ---------------------------------------------------------------------------
426
427/// Build a standard preset library containing ~12 named presets.
428pub fn standard_preset_library() -> PresetLibrary {
429    let mut lib = PresetLibrary::new();
430    lib.add(preset_average());
431    lib.add(preset_athletic());
432    lib.add(preset_slender());
433    lib.add(preset_heavy());
434    lib.add(preset_muscular());
435    lib.add(preset_petite());
436    lib.add(preset_tall());
437    lib.add(preset_child());
438    lib.add(preset_elder());
439
440    // Additional presets to reach ~12
441    lib.add(
442        BodyPreset::new("bodybuilder", BodyCategory::Mesomorph)
443            .with_description("Extreme bodybuilder physique.")
444            .with_param("height", 0.6)
445            .with_param("weight", 0.7)
446            .with_param("muscle", 1.0)
447            .with_param("age", 0.35)
448            .with_param("bmi_factor", 0.5)
449            .with_param("shoulder_width", 0.9)
450            .with_param("hip_width", 0.55)
451            .with_param("leg_length", 0.5)
452            .with_param("torso_length", 0.5)
453            .with_param("arm_length", 0.5)
454            .with_tag("muscle")
455            .with_tag("adult")
456            .with_tag("sport"),
457    );
458    lib.add(
459        BodyPreset::new("runner", BodyCategory::Ectomorph)
460            .with_description("Long-distance runner with lean build and long legs.")
461            .with_param("height", 0.6)
462            .with_param("weight", 0.25)
463            .with_param("muscle", 0.45)
464            .with_param("age", 0.3)
465            .with_param("bmi_factor", 0.2)
466            .with_param("shoulder_width", 0.4)
467            .with_param("hip_width", 0.45)
468            .with_param("leg_length", 0.7)
469            .with_param("torso_length", 0.45)
470            .with_param("arm_length", 0.6)
471            .with_tag("slim")
472            .with_tag("sport")
473            .with_tag("adult"),
474    );
475    lib.add(
476        BodyPreset::new("stocky", BodyCategory::Endomorph)
477            .with_description("Short and broad with dense musculature.")
478            .with_param("height", 0.3)
479            .with_param("weight", 0.65)
480            .with_param("muscle", 0.55)
481            .with_param("age", 0.4)
482            .with_param("bmi_factor", 0.7)
483            .with_param("shoulder_width", 0.6)
484            .with_param("hip_width", 0.55)
485            .with_param("leg_length", 0.35)
486            .with_param("torso_length", 0.4)
487            .with_param("arm_length", 0.4)
488            .with_tag("heavy")
489            .with_tag("adult"),
490    );
491
492    lib
493}
494
495// ---------------------------------------------------------------------------
496// Tests
497// ---------------------------------------------------------------------------
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502
503    #[test]
504    fn test_body_category_name() {
505        assert_eq!(BodyCategory::Ectomorph.name(), "Ectomorph");
506        assert_eq!(BodyCategory::Mesomorph.name(), "Mesomorph");
507        assert_eq!(BodyCategory::Endomorph.name(), "Endomorph");
508        assert_eq!(BodyCategory::Average.name(), "Average");
509        assert_eq!(BodyCategory::Petite.name(), "Petite");
510        assert_eq!(BodyCategory::Tall.name(), "Tall");
511        assert_eq!(BodyCategory::Child.name(), "Child");
512        assert_eq!(BodyCategory::Elder.name(), "Elder");
513        assert_eq!(BodyCategory::Custom("MyType".to_string()).name(), "MyType");
514    }
515
516    #[test]
517    fn test_body_category_all_named() {
518        let named = BodyCategory::all_named();
519        assert_eq!(named.len(), 8);
520        // Custom should not appear
521        for cat in &named {
522            assert!(!matches!(cat, BodyCategory::Custom(_)));
523        }
524        assert!(named.contains(&BodyCategory::Ectomorph));
525        assert!(named.contains(&BodyCategory::Elder));
526    }
527
528    #[test]
529    fn test_preset_new() {
530        let p = BodyPreset::new("test_preset", BodyCategory::Average);
531        assert_eq!(p.name, "test_preset");
532        assert_eq!(p.category, BodyCategory::Average);
533        assert!(p.params.is_empty());
534        assert!(p.tags.is_empty());
535        assert!(p.description.is_empty());
536    }
537
538    #[test]
539    fn test_preset_with_param() {
540        let p = BodyPreset::new("x", BodyCategory::Average)
541            .with_param("height", 0.7)
542            .with_param("muscle", 0.3);
543        assert_eq!(p.param_count(), 2);
544        assert!((p.get_param("height") - 0.7).abs() < 1e-6);
545        assert!((p.get_param("muscle") - 0.3).abs() < 1e-6);
546    }
547
548    #[test]
549    fn test_preset_get_param_missing() {
550        let p = BodyPreset::new("x", BodyCategory::Average);
551        // Missing key defaults to 0.5
552        assert!((p.get_param("nonexistent") - 0.5).abs() < 1e-6);
553    }
554
555    #[test]
556    fn test_library_add_and_get() {
557        let mut lib = PresetLibrary::new();
558        assert_eq!(lib.preset_count(), 0);
559        lib.add(preset_average());
560        lib.add(preset_athletic());
561        assert_eq!(lib.preset_count(), 2);
562        assert!(lib.get("average").is_some());
563        assert!(lib.get("athletic").is_some());
564        assert!(lib.get("nonexistent").is_none());
565    }
566
567    #[test]
568    fn test_library_by_category() {
569        let lib = standard_preset_library();
570        let mesomorphs = lib.by_category(&BodyCategory::Mesomorph);
571        // athletic, muscular, bodybuilder
572        assert!(mesomorphs.len() >= 2);
573        for p in &mesomorphs {
574            assert_eq!(p.category, BodyCategory::Mesomorph);
575        }
576    }
577
578    #[test]
579    fn test_library_with_tag() {
580        let lib = standard_preset_library();
581        let sport = lib.with_tag("sport");
582        assert!(!sport.is_empty());
583        for p in &sport {
584            assert!(p.tags.contains(&"sport".to_string()));
585        }
586
587        let adults = lib.with_tag("adult");
588        assert!(adults.len() > 3);
589    }
590
591    #[test]
592    fn test_library_blend() {
593        let lib = standard_preset_library();
594
595        // blend at t=0 should give preset_a values
596        let blended_0 = lib.blend("average", "athletic", 0.0).expect("blend failed");
597        let avg = lib.get("average").expect("should succeed");
598        assert!((blended_0["height"] - avg.get_param("height")).abs() < 1e-5);
599
600        // blend at t=1 should give preset_b values
601        let blended_1 = lib.blend("average", "athletic", 1.0).expect("blend failed");
602        let ath = lib.get("athletic").expect("should succeed");
603        assert!((blended_1["height"] - ath.get_param("height")).abs() < 1e-5);
604
605        // blend at t=0.5 should be midpoint
606        let blended_half = lib.blend("average", "athletic", 0.5).expect("blend failed");
607        let expected_height = (avg.get_param("height") + ath.get_param("height")) / 2.0;
608        assert!((blended_half["height"] - expected_height).abs() < 1e-5);
609
610        // unknown name returns None
611        assert!(lib.blend("average", "nonexistent", 0.5).is_none());
612    }
613
614    #[test]
615    fn test_library_nearest() {
616        let lib = standard_preset_library();
617
618        // params matching "average" exactly
619        let mut params: BodyParams = HashMap::new();
620        params.insert("height".to_string(), 0.5);
621        params.insert("weight".to_string(), 0.5);
622        params.insert("muscle".to_string(), 0.5);
623        params.insert("age".to_string(), 0.5);
624        let nearest = lib.nearest(&params).expect("should find nearest");
625        assert_eq!(nearest.name, "average");
626
627        // params matching "child" closely
628        let mut child_params: BodyParams = HashMap::new();
629        child_params.insert("height".to_string(), 0.1);
630        child_params.insert("muscle".to_string(), 0.2);
631        child_params.insert("age".to_string(), 0.05);
632        let nearest_child = lib.nearest(&child_params).expect("should find nearest");
633        assert_eq!(nearest_child.name, "child");
634    }
635
636    #[test]
637    fn test_preset_average() {
638        let p = preset_average();
639        assert_eq!(p.name, "average");
640        assert_eq!(p.category, BodyCategory::Average);
641        // All standard params at 0.5
642        for key in &[
643            "height",
644            "weight",
645            "muscle",
646            "age",
647            "bmi_factor",
648            "shoulder_width",
649            "hip_width",
650            "leg_length",
651            "torso_length",
652            "arm_length",
653        ] {
654            assert!(
655                (p.get_param(key) - 0.5).abs() < 1e-6,
656                "param '{}' expected 0.5, got {}",
657                key,
658                p.get_param(key)
659            );
660        }
661    }
662
663    #[test]
664    fn test_preset_athletic() {
665        let p = preset_athletic();
666        assert_eq!(p.name, "athletic");
667        assert_eq!(p.category, BodyCategory::Mesomorph);
668        assert!((p.get_param("height") - 0.55).abs() < 1e-6);
669        assert!((p.get_param("weight") - 0.45).abs() < 1e-6);
670        assert!((p.get_param("muscle") - 0.75).abs() < 1e-6);
671        assert!((p.get_param("age") - 0.3).abs() < 1e-6);
672        assert!((p.get_param("bmi_factor") - 0.35).abs() < 1e-6);
673        assert!((p.get_param("shoulder_width") - 0.65).abs() < 1e-6);
674        assert!(p.tags.contains(&"sport".to_string()));
675    }
676
677    #[test]
678    fn test_standard_preset_library() {
679        let lib = standard_preset_library();
680        assert!(
681            lib.preset_count() >= 12,
682            "expected at least 12 presets, got {}",
683            lib.preset_count()
684        );
685        let names = lib.names();
686        assert!(names.contains(&"average"));
687        assert!(names.contains(&"athletic"));
688        assert!(names.contains(&"slender"));
689        assert!(names.contains(&"heavy"));
690        assert!(names.contains(&"muscular"));
691        assert!(names.contains(&"petite"));
692        assert!(names.contains(&"tall"));
693        assert!(names.contains(&"child"));
694        assert!(names.contains(&"elder"));
695    }
696
697    #[test]
698    fn test_preset_child_age() {
699        let p = preset_child();
700        assert_eq!(p.name, "child");
701        assert_eq!(p.category, BodyCategory::Child);
702        // age should be very low (0.05)
703        assert!(p.get_param("age") < 0.1, "child age param should be < 0.1");
704        // height should be very low
705        assert!(
706            p.get_param("height") < 0.2,
707            "child height param should be < 0.2"
708        );
709        // muscle should be low
710        assert!(
711            p.get_param("muscle") < 0.3,
712            "child muscle param should be < 0.3"
713        );
714        assert!(p.tags.contains(&"child".to_string()));
715    }
716}