owned_orbit/
lib.rs

1//! Orbit Breaker: original gameplay, original CPU raster, and only first-party Ferravex crates.
2pub mod game;
3mod raster;
4use ferravex_core::FixedClock;
5use ferravex_engine::input::{ActionMap, Binding, InputCode};
6use game::{Game, HEIGHT, Input, WIDTH};
7use raster::Canvas;
8use std::sync::Mutex;
9
10pub struct Orbit {
11    pub game: Game,
12    pub canvas: Canvas,
13    clock: FixedClock,
14    actions: ActionMap,
15    axis: usize,
16    launch: usize,
17    restart: usize,
18}
19impl Default for Orbit {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24impl Orbit {
25    pub fn new() -> Self {
26        let mut actions = ActionMap::default();
27        let axis = actions.action(vec![Binding {
28            code: InputCode::PadAxis(0),
29            scale: 1.0,
30            deadzone: 0.02,
31        }]);
32        let launch = actions.action(vec![Binding {
33            code: InputCode::Key(32),
34            scale: 1.0,
35            deadzone: 0.0,
36        }]);
37        let restart = actions.action(vec![Binding {
38            code: InputCode::Key(82),
39            scale: 1.0,
40            deadzone: 0.0,
41        }]);
42        let mut app = Self {
43            game: Game::new(),
44            canvas: Canvas::new(),
45            clock: FixedClock::new(120.0, 12),
46            actions,
47            axis,
48            launch,
49            restart,
50        };
51        app.render();
52        app
53    }
54    pub fn advance(&mut self, dt: f64, axis: f32, buttons: u32) {
55        self.actions.feed(InputCode::PadAxis(0), axis);
56        self.actions
57            .feed(InputCode::Key(32), f32::from(buttons & 1 != 0));
58        self.actions
59            .feed(InputCode::Key(82), f32::from(buttons & 2 != 0));
60        let step = self.clock.advance(dt);
61        for _ in 0..step.steps {
62            self.game.tick(
63                Input {
64                    axis: self.actions.consume_tick(self.axis).value,
65                    launch: self.actions.consume_tick(self.launch).pressed,
66                    restart: self.actions.consume_tick(self.restart).pressed,
67                },
68                step.dt,
69            );
70        }
71    }
72    pub fn render(&mut self) {
73        self.canvas.render(&self.game);
74    }
75}
76static APP: Mutex<Option<Orbit>> = Mutex::new(None);
77#[unsafe(no_mangle)]
78pub extern "C" fn init() {
79    *APP.lock().expect("app lock") = Some(Orbit::new());
80}
81/// buttons: bit0 launch/retry, bit1 reset. Input edges survive frames with no simulation tick.
82#[unsafe(no_mangle)]
83pub extern "C" fn step(dt: f64, axis: f32, buttons: u32) {
84    if let Some(app) = APP.lock().expect("app lock").as_mut() {
85        app.advance(dt, axis, buttons);
86        app.render();
87    }
88}
89#[unsafe(no_mangle)]
90pub extern "C" fn pixels() -> *const u8 {
91    APP.lock()
92        .expect("app lock")
93        .as_ref()
94        .map_or(std::ptr::null(), |app| {
95            app.canvas.frame.pixels().as_ptr().cast()
96        })
97}
98#[unsafe(no_mangle)]
99pub extern "C" fn width() -> u32 {
100    WIDTH
101}
102#[unsafe(no_mangle)]
103pub extern "C" fn height() -> u32 {
104    HEIGHT
105}
106#[unsafe(no_mangle)]
107pub extern "C" fn status() -> u32 {
108    APP.lock()
109        .expect("app lock")
110        .as_ref()
111        .map_or(0, |app| app.game.phase as u32)
112}
113#[unsafe(no_mangle)]
114pub extern "C" fn score() -> u32 {
115    APP.lock()
116        .expect("app lock")
117        .as_ref()
118        .map_or(0, |app| app.game.score)
119}
120#[unsafe(no_mangle)]
121pub extern "C" fn lives() -> u32 {
122    APP.lock()
123        .expect("app lock")
124        .as_ref()
125        .map_or(3, |app| app.game.lives)
126}
127#[unsafe(no_mangle)]
128pub extern "C" fn sector() -> u32 {
129    APP.lock()
130        .expect("app lock")
131        .as_ref()
132        .map_or(1, |app| app.game.sector)
133}
134#[unsafe(no_mangle)]
135pub extern "C" fn paddle() -> f32 {
136    APP.lock()
137        .expect("app lock")
138        .as_ref()
139        .map_or(320.0, |app| app.game.paddle_x)
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    #[test]
146    fn quick_input_tap_survives_zero_tick_and_frame_is_rgba() {
147        let mut app = Orbit::new();
148        app.advance(0.0, 0.0, 1);
149        app.advance(0.0, 0.0, 0);
150        app.advance(1.0 / 120.0, 0.0, 0);
151        assert_eq!(app.game.phase, game::Phase::Playing);
152        app.render();
153        assert_eq!(app.canvas.frame.pixels().len(), (WIDTH * HEIGHT) as usize);
154        assert!(app.canvas.frame.pixels().iter().all(|p| p[3] == 255));
155        assert!(
156            app.canvas
157                .frame
158                .pixels()
159                .iter()
160                .filter(|p| p[0] > 100)
161                .count()
162                > 5000
163        );
164    }
165}