Skip to main content

oxihuman_morph/
adolescent_morph.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Adolescent body proportion morph (ages 12–18).
6
7/// Sex used to pick secondary sexual characteristic curves.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum AdolSex {
10    Male,
11    Female,
12}
13
14/// Configuration for the adolescent morph.
15#[derive(Debug, Clone)]
16pub struct AdolescentMorphConfig {
17    pub sex: AdolSex,
18    pub growth_spurt_peak_years: f32,
19}
20
21impl Default for AdolescentMorphConfig {
22    fn default() -> Self {
23        AdolescentMorphConfig {
24            sex: AdolSex::Female,
25            growth_spurt_peak_years: 14.0,
26        }
27    }
28}
29
30/// State for the adolescent body morph.
31#[derive(Debug, Clone)]
32pub struct AdolescentMorph {
33    /// Age in years (12–18).
34    pub age_years: f32,
35    pub config: AdolescentMorphConfig,
36    pub enabled: bool,
37}
38
39/// Create a new adolescent morph at age 12.
40pub fn new_adolescent_morph() -> AdolescentMorph {
41    AdolescentMorph {
42        age_years: 12.0,
43        config: AdolescentMorphConfig::default(),
44        enabled: true,
45    }
46}
47
48/// Set age in years (clamped 12–18).
49pub fn adol_set_age(m: &mut AdolescentMorph, years: f32) {
50    m.age_years = years.clamp(12.0, 18.0);
51}
52
53/// Normalised progress (0 = 12, 1 = 18).
54pub fn adol_progress(m: &AdolescentMorph) -> f32 {
55    (m.age_years - 12.0) / 6.0
56}
57
58/// Hip-width delta for the given sex.
59pub fn adol_hip_delta(m: &AdolescentMorph) -> f32 {
60    let t = adol_progress(m);
61    match m.config.sex {
62        AdolSex::Female => 0.2 * t,
63        AdolSex::Male => 0.05 * t,
64    }
65}
66
67/// Shoulder-width delta.
68pub fn adol_shoulder_delta(m: &AdolescentMorph) -> f32 {
69    let t = adol_progress(m);
70    match m.config.sex {
71        AdolSex::Male => 0.2 * t,
72        AdolSex::Female => 0.08 * t,
73    }
74}
75
76/// Serialise to JSON.
77pub fn adol_to_json(m: &AdolescentMorph) -> String {
78    format!(
79        r#"{{"age_years":{:.1},"enabled":{},"hip_delta":{:.3}}}"#,
80        m.age_years,
81        m.enabled,
82        adol_hip_delta(m)
83    )
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn default_age_is_twelve() {
92        let m = new_adolescent_morph();
93        assert!((m.age_years - 12.0).abs() < 1e-6 /* default 12 */);
94    }
95
96    #[test]
97    fn clamp_upper() {
98        let mut m = new_adolescent_morph();
99        adol_set_age(&mut m, 99.0);
100        assert!((m.age_years - 18.0).abs() < 1e-6 /* clamped to 18 */);
101    }
102
103    #[test]
104    fn progress_at_18() {
105        let mut m = new_adolescent_morph();
106        adol_set_age(&mut m, 18.0);
107        assert!((adol_progress(&m) - 1.0).abs() < 1e-6 /* full progress */);
108    }
109
110    #[test]
111    fn female_hip_delta_larger_than_male() {
112        let mut mf = new_adolescent_morph();
113        adol_set_age(&mut mf, 18.0);
114        let mut mm = new_adolescent_morph();
115        mm.config.sex = AdolSex::Male;
116        adol_set_age(&mut mm, 18.0);
117        assert!(adol_hip_delta(&mf) > adol_hip_delta(&mm) /* female hips wider */);
118    }
119
120    #[test]
121    fn male_shoulder_delta_larger() {
122        let mut mm = new_adolescent_morph();
123        mm.config.sex = AdolSex::Male;
124        adol_set_age(&mut mm, 18.0);
125        let mut mf = new_adolescent_morph();
126        adol_set_age(&mut mf, 18.0);
127        assert!(adol_shoulder_delta(&mm) > adol_shoulder_delta(&mf) /* male shoulders wider */);
128    }
129
130    #[test]
131    fn json_contains_age() {
132        let mut m = new_adolescent_morph();
133        adol_set_age(&mut m, 15.0);
134        assert!(adol_to_json(&m).contains("15.0") /* age in json */);
135    }
136
137    #[test]
138    fn enabled_default_true() {
139        let m = new_adolescent_morph();
140        assert!(m.enabled /* enabled by default */);
141    }
142
143    #[test]
144    fn hip_delta_zero_at_start() {
145        let mut m = new_adolescent_morph();
146        adol_set_age(&mut m, 12.0);
147        assert!((adol_hip_delta(&m) - 0.0).abs() < 1e-6 /* no delta at 12 */);
148    }
149}