1use crate::choice_enum;
6use crate::components::{Component, MeshRenderer, RigidBody, Transform};
7use crate::hierarchy;
8use crate::reflect::Field;
9use glam::Vec3;
10use kvas_ecs::{Entity, World};
11use serde::{Deserialize, Serialize};
12
13choice_enum!(
14 SwitchKind { Button, Toggle, Plate } default Button);
16
17#[derive(Clone, Serialize, Deserialize)]
20#[serde(default)]
21pub struct Switch {
22 pub kind: SwitchKind,
24 pub channel: String,
26 pub hold_time: f32,
28 pub prompt: String,
30 pub color_on: [f32; 4],
32 pub color_off: [f32; 4],
34 #[serde(skip)]
35 #[doc(hidden)]
36 pub active: bool,
37 #[serde(skip)]
38 #[doc(hidden)]
39 pub timer: f32,
40}
41
42impl Default for Switch {
43 fn default() -> Self {
44 Self {
45 kind: SwitchKind::Button,
46 channel: "door".into(),
47 hold_time: 4.0,
48 prompt: "Press E".into(),
49 color_on: [0.2, 1.0, 0.35, 1.0],
50 color_off: [1.0, 0.18, 0.12, 1.0],
51 active: false,
52 timer: 0.0,
53 }
54 }
55}
56
57impl Component for Switch {
58 fn fields(&mut self) -> Vec<Field<'_>> {
59 vec![
60 Field::choice("kind", &mut self.kind),
61 Field::text("channel", &mut self.channel),
62 Field::f32("hold_time", &mut self.hold_time),
63 Field::text("prompt", &mut self.prompt),
64 Field::color("color_on", &mut self.color_on),
65 Field::color("color_off", &mut self.color_off),
66 ]
67 }
68}
69
70#[derive(Clone, Serialize, Deserialize)]
73#[serde(default)]
74pub struct Slider {
75 pub channel: String,
77 pub open_offset: Vec3,
79 pub speed: f32,
81 #[serde(skip)]
82 #[doc(hidden)]
83 pub origin: Option<Vec3>,
84 #[serde(skip)]
86 #[doc(hidden)]
87 pub amount: f32,
88}
89
90impl Default for Slider {
91 fn default() -> Self {
92 Self { channel: "door".into(), open_offset: Vec3::new(0.0, 3.0, 0.0), speed: 2.0, origin: None, amount: 0.0 }
93 }
94}
95
96impl Component for Slider {
97 fn fields(&mut self) -> Vec<Field<'_>> {
98 vec![
99 Field::text("channel", &mut self.channel),
100 Field::vec3("open_offset", &mut self.open_offset),
101 Field::f32("speed", &mut self.speed),
102 ]
103 }
104}
105
106pub fn press(world: &mut World, e: Entity) {
108 if let Some(s) = world.get_mut::<Switch>(e) {
109 match s.kind {
110 SwitchKind::Button => s.timer = s.hold_time,
111 SwitchKind::Toggle => s.active = !s.active,
112 SwitchKind::Plate => {}
113 }
114 }
115}
116
117fn plate_pressed(world: &World, plate: Entity, bodies: &[(Entity, Vec3)]) -> bool {
119 let m = hierarchy::global_matrix(world, plate);
120 let inv = m.inverse();
121 let top = m.w_axis.y + m.y_axis.length() * 0.5;
122 bodies.iter().any(|&(e, p)| {
123 let local = inv.transform_point3(p);
124 e != plate && local.x.abs() <= 0.5 && local.z.abs() <= 0.5 && p.y > top - 0.2 && p.y < top + 1.5
125 })
126}
127
128pub fn update(world: &mut World, dt: f32) {
131 if !dt.is_finite() || dt < 0.0 { return; }
132 let switches: Vec<Entity> = world.iter::<Switch>().map(|(e, _)| e).collect();
133 if switches.is_empty() && world.iter::<Slider>().next().is_none() {
134 return;
135 }
136 let bodies: Vec<(Entity, Vec3)> = world
137 .iter::<RigidBody>()
138 .filter(|(_, rb)| rb.kind == crate::components::BodyKind::Dynamic)
139 .map(|(e, _)| (e, hierarchy::global_matrix(world, e).w_axis.truncate()))
140 .collect();
141 let mut active_channels: Vec<String> = Vec::new();
142 for e in switches {
143 let pressed_plate = world.get::<Switch>(e).is_some_and(|s| s.kind == SwitchKind::Plate) && plate_pressed(world, e, &bodies);
144 let Some(s) = world.get_mut::<Switch>(e) else { continue };
145 match s.kind {
146 SwitchKind::Button => {
147 s.timer = (s.timer - dt).max(0.0);
148 s.active = s.timer > 0.0;
149 }
150 SwitchKind::Plate => s.active = pressed_plate,
151 SwitchKind::Toggle => {}
152 }
153 if s.active && !s.channel.is_empty() {
154 active_channels.push(s.channel.clone());
155 }
156 let color = if s.active { s.color_on } else { s.color_off };
157 if let Some(m) = world.get_mut::<MeshRenderer>(e) {
158 m.color = color;
159 }
160 }
161
162 let sliders: Vec<Entity> = world.iter::<Slider>().map(|(e, _)| e).collect();
163 for e in sliders {
164 let Some(base) = world.get::<Transform>(e).map(|t| t.position) else { continue };
165 let Some(s) = world.get_mut::<Slider>(e) else { continue };
166 let origin = *s.origin.get_or_insert(base);
167 let target = if active_channels.contains(&s.channel) { 1.0 } else { 0.0 };
168 let length = s.open_offset.length().max(0.001);
169 let step = s.speed * dt / length;
170 s.amount += (target - s.amount).clamp(-step, step);
171 let eased = s.amount * s.amount * (3.0 - 2.0 * s.amount);
172 let position = origin + s.open_offset * eased;
173 if let Some(t) = world.get_mut::<Transform>(e) {
174 t.position = position;
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::components::{register_all, Collider};
183
184 fn world() -> World {
185 let mut w = World::default();
186 register_all(&mut w);
187 w
188 }
189
190 fn spawn(w: &mut World, name: &str, position: Vec3, scale: Vec3) -> Entity {
191 let e = hierarchy::spawn_named(w, name);
192 w.insert(e, Transform { position, scale, ..Default::default() });
193 e
194 }
195
196 #[test]
197 fn button_opens_a_door_and_closes_it_again() {
198 let mut w = world();
199 let button = spawn(&mut w, "Button", Vec3::ZERO, Vec3::ONE);
200 w.insert(button, Switch { hold_time: 1.0, ..Default::default() });
201 let door = spawn(&mut w, "Door", Vec3::new(5.0, 1.0, 0.0), Vec3::ONE);
202 w.insert(door, Slider { open_offset: Vec3::new(0.0, 3.0, 0.0), speed: 3.0, ..Default::default() });
203
204 update(&mut w, 0.1);
205 assert_eq!(w.get::<Transform>(door).unwrap().position.y, 1.0, "closed until pressed");
206 press(&mut w, button);
208 let mut open_y = 0.0f32;
209 for _ in 0..15 {
210 update(&mut w, 0.1);
211 open_y = open_y.max(w.get::<Transform>(door).unwrap().position.y);
212 }
213 assert!(open_y > 3.5, "door rose to {open_y}");
214 for _ in 0..80 {
215 update(&mut w, 0.1);
216 }
217 assert!((w.get::<Transform>(door).unwrap().position.y - 1.0).abs() < 0.01, "door closes after the hold time");
218 }
219
220 #[test]
221 fn plate_is_pressed_by_a_crate_and_toggle_stays() {
222 let mut w = world();
223 let plate = spawn(&mut w, "Plate", Vec3::new(0.0, 0.05, 0.0), Vec3::new(2.0, 0.1, 2.0));
224 w.insert(plate, Switch { kind: SwitchKind::Plate, channel: "gate".into(), ..Default::default() });
225 let gate = spawn(&mut w, "Gate", Vec3::ZERO, Vec3::ONE);
226 w.insert(gate, Slider { channel: "gate".into(), open_offset: Vec3::X * 2.0, speed: 100.0, ..Default::default() });
227 let crate_ = spawn(&mut w, "Crate", Vec3::new(5.0, 0.6, 0.0), Vec3::ONE);
228 w.insert(crate_, RigidBody::default());
229 w.insert(crate_, Collider::default());
230
231 update(&mut w, 0.1);
232 assert!(!w.get::<Switch>(plate).unwrap().active);
233 w.get_mut::<Transform>(crate_).unwrap().position = Vec3::new(0.4, 0.6, -0.3);
234 update(&mut w, 0.1);
235 assert!(w.get::<Switch>(plate).unwrap().active, "a crate on the plate presses it");
236 update(&mut w, 0.1);
237 assert!((w.get::<Transform>(gate).unwrap().position.x - 2.0).abs() < 0.01);
238 w.get_mut::<Transform>(crate_).unwrap().position = Vec3::new(3.0, 0.6, 0.0);
239 update(&mut w, 0.1);
240 assert!(!w.get::<Switch>(plate).unwrap().active, "stepping off releases it");
241
242 let lever = spawn(&mut w, "Lever", Vec3::new(9.0, 0.0, 0.0), Vec3::ONE);
243 w.insert(lever, Switch { kind: SwitchKind::Toggle, channel: "gate".into(), ..Default::default() });
244 press(&mut w, lever);
245 update(&mut w, 0.1);
246 update(&mut w, 5.0);
247 assert!(w.get::<Switch>(lever).unwrap().active, "toggles do not time out");
248 press(&mut w, lever);
249 assert!(!w.get::<Switch>(lever).unwrap().active);
250 }
251}