Skip to main content

pfx_play/
characters.rs

1use pfx_load::scene::{Character, CharacterPlane, Value};
2use pfx_physics::rigid::{self, CharacterDesc, Plane, TICKS};
3
4use crate::math::number;
5
6pub const KEYS: [&str; 13] = [
7    "kind",
8    "radius",
9    "height",
10    "max_climb",
11    "max_step",
12    "snap",
13    "coyote",
14    "jump_buffer",
15    "jump_speed",
16    "plane",
17    "variable_jump",
18    "wall_slide",
19    "wall_jump",
20];
21
22pub const FLAT_KEYS: [&str; 4] = ["plane", "variable_jump", "wall_slide", "wall_jump"];
23
24#[derive(Clone, Debug, PartialEq)]
25pub enum Setting {
26    Number(f32),
27    Toggle(bool),
28    Pair([f32; 2]),
29    Word(String),
30}
31
32impl Setting {
33    pub fn of_value(value: &Value) -> Option<Setting> {
34        Some(match value {
35            Value::Bool(on) => Setting::Toggle(*on),
36            Value::Text(word) => Setting::Word(word.clone()),
37            Value::Array(items) if items.len() == 2 => {
38                Setting::Pair([number(&items[0])?, number(&items[1])?])
39            }
40            value => Setting::Number(number(value)?),
41        })
42    }
43
44    pub fn value(&self) -> Value {
45        match self {
46            Setting::Number(value) => Value::Float(*value),
47            Setting::Toggle(on) => Value::Bool(*on),
48            Setting::Pair(pair) => {
49                Value::Array(pair.iter().map(|part| Value::Float(*part)).collect())
50            }
51            Setting::Word(word) => Value::Text(word.clone()),
52        }
53    }
54}
55
56fn ticks(value: f32) -> Option<u32> {
57    (value.is_finite() && value >= 0.0 && value.fract() == 0.0 && value <= TICKS as f32)
58        .then_some(value as u32)
59}
60
61pub fn set(desc: &mut CharacterDesc, key: &str, setting: &Setting) -> Result<(), String> {
62    let mut next = *desc;
63    let flat = next.plane.is_some();
64    if FLAT_KEYS.contains(&key) && !flat {
65        return Err("a 3d character takes no such key".to_string());
66    }
67    match (key, setting) {
68        ("kind", Setting::Word(word)) => match (word.as_str(), flat) {
69            ("3d", false) | ("2d", true) => {}
70            ("2d", false) => next.plane = Some(Plane::Xy),
71            ("3d", true) => {
72                next.plane = None;
73                next.variable_jump = 0.0;
74                next.wall_slide = None;
75                next.wall_jump = None;
76            }
77            _ => return Err(format!("{word:?} is not 3d or 2d")),
78        },
79        ("radius", Setting::Number(value)) => next.radius = *value,
80        ("height", Setting::Number(value)) => next.height = *value,
81        ("max_climb", Setting::Number(value)) => next.max_climb = *value,
82        ("max_step", Setting::Number(value)) => next.max_step = *value,
83        ("snap", Setting::Number(value)) => next.snap = *value,
84        ("coyote", Setting::Number(value)) => next.coyote = ticks(*value).ok_or(TICKS_NEEDED)?,
85        ("jump_buffer", Setting::Number(value)) => {
86            next.jump_buffer = ticks(*value).ok_or(TICKS_NEEDED)?
87        }
88        ("jump_speed", Setting::Number(value)) => next.jump_speed = *value,
89        ("plane", Setting::Word(word)) => {
90            next.plane = Some(match word.as_str() {
91                "xy" => Plane::Xy,
92                "yz" => Plane::Yz,
93                _ => return Err(format!("{word:?} is not xy or yz")),
94            })
95        }
96        ("variable_jump", Setting::Number(value)) => next.variable_jump = *value,
97        ("wall_slide", Setting::Number(value)) => next.wall_slide = Some(*value),
98        ("wall_jump", Setting::Pair(kick)) => next.wall_jump = Some(*kick),
99        (key, _) if KEYS.contains(&key) => {
100            return Err(match key {
101                "kind" => "needs \"3d\" or \"2d\"".to_string(),
102                "plane" => "needs \"xy\" or \"yz\"".to_string(),
103                "wall_jump" => "needs two numbers, [away, up]".to_string(),
104                _ => "needs a number".to_string(),
105            });
106        }
107        _ => return Err("unknown key".to_string()),
108    }
109    next.check()?;
110    *desc = next;
111    Ok(())
112}
113
114const TICKS_NEEDED: &str = "needs a whole number of ticks from 0 to 1000";
115
116pub fn get(desc: &CharacterDesc, key: &str) -> Option<Setting> {
117    let flat = desc.plane.is_some();
118    Some(match key {
119        "kind" => Setting::Word(if flat { "2d" } else { "3d" }.to_string()),
120        "radius" => Setting::Number(desc.radius),
121        "height" => Setting::Number(desc.height),
122        "max_climb" => Setting::Number(desc.max_climb),
123        "max_step" => Setting::Number(desc.max_step),
124        "snap" => Setting::Number(desc.snap),
125        "coyote" => Setting::Number(desc.coyote as f32),
126        "jump_buffer" => Setting::Number(desc.jump_buffer as f32),
127        "jump_speed" => Setting::Number(desc.jump_speed),
128        "plane" => Setting::Word(
129            match desc.plane? {
130                Plane::Xy => "xy",
131                Plane::Yz => "yz",
132            }
133            .to_string(),
134        ),
135        "variable_jump" if flat => Setting::Number(desc.variable_jump),
136        "wall_slide" => Setting::Number(desc.wall_slide?),
137        "wall_jump" => Setting::Pair(desc.wall_jump?),
138        _ => return None,
139    })
140}
141
142pub fn settings(character: &Character) -> Vec<(&'static str, Setting)> {
143    let desc = desc_of(character, rigid::Layers::ALL);
144    KEYS.iter()
145        .filter_map(|key| get(&desc, key).map(|setting| (*key, setting)))
146        .collect()
147}
148
149pub fn desc_of(character: &Character, layers: rigid::Layers) -> CharacterDesc {
150    CharacterDesc {
151        radius: character.radius,
152        height: character.height,
153        max_climb: character.max_climb,
154        max_step: character.max_step,
155        snap: character.snap,
156        coyote: character.coyote,
157        jump_buffer: character.jump_buffer,
158        jump_speed: character.jump_speed,
159        variable_jump: character.variable_jump,
160        wall_slide: character.wall_slide,
161        wall_jump: character.wall_jump,
162        plane: character.plane.map(|plane| match plane {
163            CharacterPlane::Xy => Plane::Xy,
164            CharacterPlane::Yz => Plane::Yz,
165        }),
166        layers,
167        ..CharacterDesc::default()
168    }
169}