Skip to main content

kvas_engine/
interaction.rs

1//! Puzzle building blocks that need no code: `Switch` (button, lever, pressure plate) and `Slider`
2//! (door, lift, gate). They talk through a shared `channel` name: a slider opens while any switch
3//! on its channel is active. Scripts press buttons with `press()`.
4
5use 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    /// Momentary Button, persistent Toggle, or body-detecting pressure Plate.
15    SwitchKind { Button, Toggle, Plate } default Button);
16
17/// Button: active for `hold_time` seconds after being pressed. Toggle: flips on every press.
18/// Plate: active while a physics object (a crate, the player) stands on it.
19#[derive(Clone, Serialize, Deserialize)]
20#[serde(default)]
21pub struct Switch {
22    /// Momentary button, persistent toggle or proximity pressure plate.
23    pub kind: SwitchKind,
24    /// Shared channel name matched by sliders; empty emits no channel.
25    pub channel: String,
26    /// Duration in simulation seconds for a momentary button.
27    pub hold_time: f32,
28    /// Text shown on screen when the player looks at it (empty = not interactive by looking).
29    pub prompt: String,
30    /// RGBA MeshRenderer tint while active.
31    pub color_on: [f32; 4],
32    /// RGBA MeshRenderer tint while inactive.
33    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/// Moves its entity between its start position and `start + open_offset` while its channel is active.
71/// A door with collision needs a Kinematic RigidBody so physics follows it.
72#[derive(Clone, Serialize, Deserialize)]
73#[serde(default)]
74pub struct Slider {
75    /// Channel opened while any matching switch is active.
76    pub channel: String,
77    /// Parent-local displacement from the initial position when fully open.
78    pub open_offset: Vec3,
79    /// Metres per second.
80    pub speed: f32,
81    #[serde(skip)]
82    #[doc(hidden)]
83    pub origin: Option<Vec3>,
84    /// 0 = closed, 1 = fully open.
85    #[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
106/// Presses a button / flips a toggle (what the player does with the use key).
107pub 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
117/// Is any physics object resting on the plate (its footprint, from just under to 1.5 m above its top)?
118fn 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
128/// Advances every switch and slider by `dt` seconds.
129/// Invalid or negative deltas are ignored.
130pub 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        // Held for 1 s, speed 3 m/s: open for about a second, then closing again.
207        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}