Skip to main content

oxihuman_morph/
child_morph.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Child body proportion morph (ages 2–12).
6
7/// Configuration for the child morph.
8#[derive(Debug, Clone)]
9pub struct ChildMorphConfig {
10    pub limb_elongation: f32,
11    pub torso_narrowness: f32,
12    pub head_ratio: f32,
13}
14
15impl Default for ChildMorphConfig {
16    fn default() -> Self {
17        ChildMorphConfig {
18            limb_elongation: 0.7,
19            torso_narrowness: 0.85,
20            head_ratio: 0.85,
21        }
22    }
23}
24
25/// State for the child body morph.
26#[derive(Debug, Clone)]
27pub struct ChildMorph {
28    /// Age in years (2–12).
29    pub age_years: f32,
30    pub config: ChildMorphConfig,
31    pub enabled: bool,
32}
33
34/// Create a new child morph at age 2.
35pub fn new_child_morph() -> ChildMorph {
36    ChildMorph {
37        age_years: 2.0,
38        config: ChildMorphConfig::default(),
39        enabled: true,
40    }
41}
42
43/// Set age in years (clamped to 2–12).
44pub fn cm_set_age(m: &mut ChildMorph, years: f32) {
45    m.age_years = years.clamp(2.0, 12.0);
46}
47
48/// Normalised progress (0 = age 2, 1 = age 12).
49pub fn cm_progress(m: &ChildMorph) -> f32 {
50    (m.age_years - 2.0) / 10.0
51}
52
53/// Body height scale factor relative to adult norm.
54pub fn cm_height_scale(m: &ChildMorph) -> f32 {
55    0.45 + 0.35 * cm_progress(m)
56}
57
58/// Limb length scale.
59pub fn cm_limb_scale(m: &ChildMorph) -> f32 {
60    m.config.limb_elongation + 0.15 * cm_progress(m)
61}
62
63/// Serialise to JSON.
64pub fn cm_to_json(m: &ChildMorph) -> String {
65    format!(
66        r#"{{"age_years":{:.1},"height_scale":{:.3},"enabled":{}}}"#,
67        m.age_years,
68        cm_height_scale(m),
69        m.enabled
70    )
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn default_age_is_two() {
79        let m = new_child_morph();
80        assert!((m.age_years - 2.0).abs() < 1e-6 /* default age 2 */);
81    }
82
83    #[test]
84    fn set_age_clamps_bounds() {
85        let mut m = new_child_morph();
86        cm_set_age(&mut m, 0.0);
87        assert!((m.age_years - 2.0).abs() < 1e-6 /* clamped to 2 */);
88        cm_set_age(&mut m, 99.0);
89        assert!((m.age_years - 12.0).abs() < 1e-6 /* clamped to 12 */);
90    }
91
92    #[test]
93    fn progress_at_age_12_is_one() {
94        let mut m = new_child_morph();
95        cm_set_age(&mut m, 12.0);
96        assert!((cm_progress(&m) - 1.0).abs() < 1e-6 /* full progress */);
97    }
98
99    #[test]
100    fn height_increases_with_age() {
101        let mut m = new_child_morph();
102        cm_set_age(&mut m, 2.0);
103        let h2 = cm_height_scale(&m);
104        cm_set_age(&mut m, 12.0);
105        let h12 = cm_height_scale(&m);
106        assert!(h12 > h2 /* taller at 12 */);
107    }
108
109    #[test]
110    fn limb_scale_increases() {
111        let mut m = new_child_morph();
112        cm_set_age(&mut m, 2.0);
113        let l2 = cm_limb_scale(&m);
114        cm_set_age(&mut m, 12.0);
115        let l12 = cm_limb_scale(&m);
116        assert!(l12 > l2 /* longer limbs at 12 */);
117    }
118
119    #[test]
120    fn json_contains_age() {
121        let mut m = new_child_morph();
122        cm_set_age(&mut m, 8.0);
123        assert!(cm_to_json(&m).contains("8.0") /* age in json */);
124    }
125
126    #[test]
127    fn enabled_flag_works() {
128        let mut m = new_child_morph();
129        m.enabled = false;
130        assert!(!m.enabled /* disabled */);
131    }
132
133    #[test]
134    fn torso_narrowness_positive() {
135        let m = new_child_morph();
136        assert!(m.config.torso_narrowness > 0.0 /* valid config */);
137    }
138}