Skip to main content

oxihuman_morph/
athletic_build_morph.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Athletic/muscular build morph.
6
7/// Configuration for the athletic build morph.
8#[derive(Debug, Clone)]
9pub struct AthleticBuildConfig {
10    pub muscle_definition: f32,
11    pub shoulder_width: f32,
12    pub waist_taper: f32,
13}
14
15impl Default for AthleticBuildConfig {
16    fn default() -> Self {
17        AthleticBuildConfig {
18            muscle_definition: 0.8,
19            shoulder_width: 0.7,
20            waist_taper: 0.6,
21        }
22    }
23}
24
25/// State for the athletic build morph.
26#[derive(Debug, Clone)]
27pub struct AthleticBuildMorph {
28    /// Overall intensity [0, 1].
29    pub intensity: f32,
30    pub config: AthleticBuildConfig,
31    pub enabled: bool,
32}
33
34/// Create a new athletic build morph at zero intensity.
35pub fn new_athletic_build_morph() -> AthleticBuildMorph {
36    AthleticBuildMorph {
37        intensity: 0.0,
38        config: AthleticBuildConfig::default(),
39        enabled: true,
40    }
41}
42
43/// Set overall intensity (clamped 0–1).
44pub fn ab_set_intensity(m: &mut AthleticBuildMorph, v: f32) {
45    m.intensity = v.clamp(0.0, 1.0);
46}
47
48/// Muscle definition weight.
49pub fn ab_muscle_weight(m: &AthleticBuildMorph) -> f32 {
50    m.intensity * m.config.muscle_definition
51}
52
53/// Shoulder width delta.
54pub fn ab_shoulder_delta(m: &AthleticBuildMorph) -> f32 {
55    m.intensity * m.config.shoulder_width
56}
57
58/// Waist taper weight.
59pub fn ab_waist_taper(m: &AthleticBuildMorph) -> f32 {
60    m.intensity * m.config.waist_taper
61}
62
63/// Serialise to JSON.
64pub fn ab_to_json(m: &AthleticBuildMorph) -> String {
65    format!(
66        r#"{{"intensity":{:.3},"muscle":{:.3},"enabled":{}}}"#,
67        m.intensity,
68        ab_muscle_weight(m),
69        m.enabled
70    )
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn default_intensity_zero() {
79        let m = new_athletic_build_morph();
80        assert!((m.intensity - 0.0).abs() < 1e-6 /* zero intensity */);
81    }
82
83    #[test]
84    fn clamp_intensity() {
85        let mut m = new_athletic_build_morph();
86        ab_set_intensity(&mut m, 5.0);
87        assert!((m.intensity - 1.0).abs() < 1e-6 /* clamped to 1 */);
88        ab_set_intensity(&mut m, -1.0);
89        assert!((m.intensity - 0.0).abs() < 1e-6 /* clamped to 0 */);
90    }
91
92    #[test]
93    fn muscle_weight_scales_with_intensity() {
94        let mut m = new_athletic_build_morph();
95        ab_set_intensity(&mut m, 0.5);
96        let w = ab_muscle_weight(&m);
97        assert!(w > 0.0 && w < 1.0 /* partial weight */);
98    }
99
100    #[test]
101    fn shoulder_delta_nonzero_at_full() {
102        let mut m = new_athletic_build_morph();
103        ab_set_intensity(&mut m, 1.0);
104        assert!(ab_shoulder_delta(&m) > 0.0 /* nonzero delta */);
105    }
106
107    #[test]
108    fn waist_taper_zero_at_zero_intensity() {
109        let m = new_athletic_build_morph();
110        assert!((ab_waist_taper(&m) - 0.0).abs() < 1e-6 /* no taper at zero */);
111    }
112
113    #[test]
114    fn json_contains_intensity() {
115        let mut m = new_athletic_build_morph();
116        ab_set_intensity(&mut m, 0.75);
117        assert!(ab_to_json(&m).contains("0.750") /* json has intensity */);
118    }
119
120    #[test]
121    fn enabled_default() {
122        let m = new_athletic_build_morph();
123        assert!(m.enabled /* enabled by default */);
124    }
125
126    #[test]
127    fn config_values_valid() {
128        let m = new_athletic_build_morph();
129        assert!(m.config.muscle_definition > 0.0 /* positive muscle def */);
130    }
131}