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}