Skip to main content

dreamwell_engine/input/
frame.rs

1// InputFrame — per-frame input snapshot consumed by game logic.
2// The exchange currency between platform adapters and game systems.
3// Produced once per frame by the input system, consumed by movement, combat, UI, etc.
4
5use super::action::InputAction;
6
7/// Per-frame input snapshot. Immutable once built. Consumed by game systems.
8///
9/// Separates input collection (platform-specific) from input consumption (game logic).
10/// Adapters write to InputFrameBuilder, then finalize to InputFrame.
11#[derive(Debug, Clone)]
12pub struct InputFrame {
13    /// Actions active this frame (just pressed or held).
14    pub actions: Vec<InputAction>,
15
16    /// Movement vector: [x, y] in [-1, 1]. Composite of WASD/stick/dpad.
17    /// x: negative=left, positive=right. y: negative=backward, positive=forward.
18    pub movement: [f32; 2],
19
20    /// Camera look delta: [dx, dy] in pixels or normalized units.
21    /// dx: negative=look left, positive=look right.
22    /// dy: negative=look up, positive=look down.
23    pub look_delta: [f32; 2],
24
25    /// Scroll wheel delta (positive=zoom in / scroll up).
26    pub scroll_delta: f32,
27
28    /// Cursor position in window coordinates [x, y].
29    pub cursor_position: [f32; 2],
30
31    /// Cursor delta since last frame [dx, dy].
32    pub cursor_delta: [f32; 2],
33
34    /// Frame timestamp (monotonic, seconds since start).
35    pub timestamp: f32,
36}
37
38impl InputFrame {
39    /// Whether a specific action is active this frame.
40    pub fn has_action(&self, action: InputAction) -> bool {
41        self.actions.contains(&action)
42    }
43
44    /// Whether any movement input is present.
45    pub fn has_movement(&self) -> bool {
46        self.movement[0] != 0.0 || self.movement[1] != 0.0
47    }
48
49    /// Whether any look/rotation input is present.
50    pub fn has_look(&self) -> bool {
51        self.look_delta[0] != 0.0 || self.look_delta[1] != 0.0
52    }
53
54    /// Whether any scroll input is present.
55    pub fn has_scroll(&self) -> bool {
56        self.scroll_delta != 0.0
57    }
58}
59
60impl Default for InputFrame {
61    fn default() -> Self {
62        Self {
63            actions: Vec::new(),
64            movement: [0.0; 2],
65            look_delta: [0.0; 2],
66            scroll_delta: 0.0,
67            cursor_position: [0.0; 2],
68            cursor_delta: [0.0; 2],
69            timestamp: 0.0,
70        }
71    }
72}
73
74/// Builder for constructing an InputFrame. Used by platform adapters.
75pub struct InputFrameBuilder {
76    actions: Vec<InputAction>,
77    movement: [f32; 2],
78    look_delta: [f32; 2],
79    scroll_delta: f32,
80    cursor_position: [f32; 2],
81    cursor_delta: [f32; 2],
82    timestamp: f32,
83}
84
85impl InputFrameBuilder {
86    pub fn new(timestamp: f32) -> Self {
87        Self {
88            actions: Vec::new(),
89            movement: [0.0; 2],
90            look_delta: [0.0; 2],
91            scroll_delta: 0.0,
92            cursor_position: [0.0; 2],
93            cursor_delta: [0.0; 2],
94            timestamp,
95        }
96    }
97
98    /// Add an action to the frame.
99    pub fn action(mut self, action: InputAction) -> Self {
100        if !self.actions.contains(&action) {
101            self.actions.push(action);
102        }
103        self
104    }
105
106    /// Set movement vector.
107    pub fn movement(mut self, x: f32, y: f32) -> Self {
108        self.movement = [x, y];
109        self
110    }
111
112    /// Set look delta.
113    pub fn look(mut self, dx: f32, dy: f32) -> Self {
114        self.look_delta = [dx, dy];
115        self
116    }
117
118    /// Set scroll delta.
119    pub fn scroll(mut self, delta: f32) -> Self {
120        self.scroll_delta = delta;
121        self
122    }
123
124    /// Set cursor position.
125    pub fn cursor(mut self, x: f32, y: f32) -> Self {
126        self.cursor_position = [x, y];
127        self
128    }
129
130    /// Set cursor delta.
131    pub fn cursor_delta(mut self, dx: f32, dy: f32) -> Self {
132        self.cursor_delta = [dx, dy];
133        self
134    }
135
136    /// Finalize into an immutable InputFrame.
137    pub fn build(self) -> InputFrame {
138        InputFrame {
139            actions: self.actions,
140            movement: self.movement,
141            look_delta: self.look_delta,
142            scroll_delta: self.scroll_delta,
143            cursor_position: self.cursor_position,
144            cursor_delta: self.cursor_delta,
145            timestamp: self.timestamp,
146        }
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn empty_frame() {
156        let frame = InputFrame::default();
157        assert!(!frame.has_movement());
158        assert!(!frame.has_look());
159        assert!(!frame.has_scroll());
160        assert!(frame.actions.is_empty());
161    }
162
163    #[test]
164    fn builder_actions() {
165        let frame = InputFrameBuilder::new(0.0)
166            .action(InputAction::MoveForward)
167            .action(InputAction::Sprint)
168            .build();
169
170        assert!(frame.has_action(InputAction::MoveForward));
171        assert!(frame.has_action(InputAction::Sprint));
172        assert!(!frame.has_action(InputAction::Jump));
173    }
174
175    #[test]
176    fn builder_no_duplicate_actions() {
177        let frame = InputFrameBuilder::new(0.0)
178            .action(InputAction::Attack)
179            .action(InputAction::Attack)
180            .build();
181
182        assert_eq!(frame.actions.len(), 1);
183    }
184
185    #[test]
186    fn builder_movement() {
187        let frame = InputFrameBuilder::new(0.0).movement(-1.0, 1.0).build();
188
189        assert!(frame.has_movement());
190        assert_eq!(frame.movement, [-1.0, 1.0]);
191    }
192
193    #[test]
194    fn builder_look_and_scroll() {
195        let frame = InputFrameBuilder::new(0.0).look(5.0, -3.0).scroll(1.5).build();
196
197        assert!(frame.has_look());
198        assert!(frame.has_scroll());
199        assert_eq!(frame.look_delta, [5.0, -3.0]);
200        assert_eq!(frame.scroll_delta, 1.5);
201    }
202
203    #[test]
204    fn builder_cursor() {
205        let frame = InputFrameBuilder::new(1.0)
206            .cursor(100.0, 200.0)
207            .cursor_delta(2.0, -1.0)
208            .build();
209
210        assert_eq!(frame.cursor_position, [100.0, 200.0]);
211        assert_eq!(frame.cursor_delta, [2.0, -1.0]);
212        assert_eq!(frame.timestamp, 1.0);
213    }
214
215    #[test]
216    fn full_frame_build() {
217        let frame = InputFrameBuilder::new(0.016)
218            .action(InputAction::MoveForward)
219            .action(InputAction::Sprint)
220            .movement(0.0, 1.0)
221            .look(0.0, 0.0)
222            .scroll(-0.5)
223            .cursor(640.0, 480.0)
224            .cursor_delta(0.0, 0.0)
225            .build();
226
227        assert!(frame.has_action(InputAction::MoveForward));
228        assert!(frame.has_action(InputAction::Sprint));
229        assert!(frame.has_movement());
230        assert!(!frame.has_look());
231        assert!(frame.has_scroll());
232        assert_eq!(frame.timestamp, 0.016);
233    }
234}
235
236// ── InputPacket ───────────────────────────────────────────────────────
237//
238// Tick-stamped, simulation-ready input contract. The canonical exchange type
239// between the platform input layer and the CausalComputeKernel. Produced
240// once per tick from an InputFrame (binding-aware, normalized).
241
242/// Simulation-ready input packet. Single canonical type consumed by
243/// CausalComputeKernel, CausalEngineEncoder, and all downstream systems.
244#[derive(Debug, Clone)]
245pub struct InputPacket {
246    // ── Movement (from InputFrame.movement, already binding-aware + normalized) ──
247    /// Camera-relative movement in [-1,1]. x=right, y=forward.
248    pub movement: [f32; 2],
249    /// Camera yaw in radians, for world-space direction derivation.
250    pub camera_yaw: f32,
251
252    // ── Edge-triggered (fire once per tick) ──
253    pub jump: bool,
254    pub interact: bool,
255
256    // ── Level-triggered (held state) ──
257    pub sprint: bool,
258    pub gather: bool,
259    /// Analog primary action strength [0,1].
260    pub emit_strength: f32,
261    /// Form coherence target [0,1]. 1.0=Cohere, 0.0=Wave.
262    pub coherence_target: f32,
263
264    // ── Timing ──
265    pub dt: f32,
266    /// Monotonic seconds since session start.
267    pub timestamp: f64,
268    pub tick: u64,
269
270    // ── Physics context (from previous frame) ──
271    pub grounded: bool,
272}
273
274impl InputPacket {
275    /// Idle packet with all zeros. For tests and no-input frames.
276    pub fn idle(dt: f32, tick: u64) -> Self {
277        Self {
278            movement: [0.0; 2],
279            camera_yaw: 0.0,
280            jump: false,
281            interact: false,
282            sprint: false,
283            gather: false,
284            emit_strength: 0.0,
285            coherence_target: 1.0,
286            dt,
287            timestamp: 0.0,
288            tick,
289            grounded: true,
290        }
291    }
292
293    /// Bridge an InputFrame to an InputPacket with additional simulation context.
294    pub fn from_frame(
295        frame: &InputFrame,
296        camera_yaw: f32,
297        dt: f32,
298        timestamp: f64,
299        tick: u64,
300        grounded: bool,
301        coherence_target: f32,
302        gather: bool,
303        emit_strength: f32,
304    ) -> Self {
305        Self {
306            movement: frame.movement,
307            camera_yaw,
308            jump: frame.has_action(InputAction::Jump),
309            interact: frame.has_action(InputAction::Interact),
310            sprint: frame.has_action(InputAction::Sprint),
311            gather,
312            emit_strength,
313            coherence_target,
314            dt,
315            timestamp,
316            tick,
317            grounded,
318        }
319    }
320
321    /// Encode active actions as u8 discriminants for Weave serialization.
322    pub fn actions_as_u8(&self) -> Vec<u8> {
323        let mut out = Vec::with_capacity(4);
324        if self.movement[1] > 0.0 {
325            out.push(0);
326        } // MoveForward
327        if self.movement[1] < 0.0 {
328            out.push(1);
329        } // MoveBackward
330        if self.movement[0] < 0.0 {
331            out.push(2);
332        } // MoveLeft
333        if self.movement[0] > 0.0 {
334            out.push(3);
335        } // MoveRight
336        if self.sprint {
337            out.push(6);
338        } // Sprint
339        if self.jump {
340            out.push(8);
341        } // Jump
342        if self.interact {
343            out.push(16);
344        } // Interact
345        if self.gather {
346            out.push(100);
347        } // Gather (extended)
348        out
349    }
350
351    /// BLAKE3 digest of the packet for attestation chains.
352    pub fn digest(&self) -> [u8; 32] {
353        let mut hasher = blake3::Hasher::new();
354        hasher.update(&self.movement[0].to_le_bytes());
355        hasher.update(&self.movement[1].to_le_bytes());
356        hasher.update(&self.camera_yaw.to_le_bytes());
357        hasher.update(&[
358            self.jump as u8,
359            self.interact as u8,
360            self.sprint as u8,
361            self.gather as u8,
362        ]);
363        hasher.update(&self.emit_strength.to_le_bytes());
364        hasher.update(&self.coherence_target.to_le_bytes());
365        hasher.update(&self.dt.to_le_bytes());
366        hasher.update(&self.timestamp.to_le_bytes());
367        hasher.update(&self.tick.to_le_bytes());
368        hasher.update(&[self.grounded as u8]);
369        *hasher.finalize().as_bytes()
370    }
371
372    /// Whether any movement input is present.
373    pub fn has_movement(&self) -> bool {
374        self.movement[0] != 0.0 || self.movement[1] != 0.0
375    }
376}
377
378#[cfg(test)]
379mod input_packet_tests {
380    use super::*;
381
382    #[test]
383    fn idle_packet_is_zero() {
384        let p = InputPacket::idle(1.0 / 60.0, 0);
385        assert!(!p.has_movement());
386        assert!(!p.jump);
387        assert!(!p.sprint);
388        assert!(p.grounded);
389        assert_eq!(p.coherence_target, 1.0);
390    }
391
392    #[test]
393    fn from_frame_maps_actions() {
394        let frame = InputFrameBuilder::new(0.0)
395            .action(InputAction::Jump)
396            .action(InputAction::Sprint)
397            .movement(0.5, 1.0)
398            .build();
399        let p = InputPacket::from_frame(&frame, 0.0, 0.016, 1.0, 1, true, 0.8, false, 0.0);
400        assert!(p.jump);
401        assert!(p.sprint);
402        assert!(!p.interact);
403        assert_eq!(p.movement, [0.5, 1.0]);
404        assert_eq!(p.coherence_target, 0.8);
405    }
406
407    #[test]
408    fn actions_as_u8_encodes_movement() {
409        let mut p = InputPacket::idle(0.016, 0);
410        p.movement = [0.0, 1.0]; // forward
411        p.sprint = true;
412        let actions = p.actions_as_u8();
413        assert!(actions.contains(&0)); // MoveForward
414        assert!(actions.contains(&6)); // Sprint
415        assert!(!actions.contains(&8)); // no Jump
416    }
417
418    #[test]
419    fn digest_is_nonzero() {
420        let p = InputPacket::idle(0.016, 0);
421        let d = p.digest();
422        assert_ne!(d, [0u8; 32]);
423    }
424
425    #[test]
426    fn digest_changes_with_input() {
427        let p1 = InputPacket::idle(0.016, 0);
428        let mut p2 = InputPacket::idle(0.016, 0);
429        p2.jump = true;
430        assert_ne!(p1.digest(), p2.digest());
431    }
432}