Skip to main content

proof_engine/
integration.rs

1//! ProofGame integration trait — the contract between proof-engine and chaos-rpg-core.
2//!
3//! Any game that wants to drive the Proof Engine implements `ProofGame`.
4//! The engine calls `update()` each frame with mutable access to the engine state,
5//! allowing game logic to spawn entities, emit particles, trigger audio, and more.
6//!
7//! # Example
8//!
9//! ```rust,no_run
10//! use proof_engine::prelude::*;
11//! use proof_engine::integration::ProofGame;
12//!
13//! struct MyChaosRpg { tick: u64 }
14//!
15//! impl ProofGame for MyChaosRpg {
16//!     fn title(&self) -> &str { "CHAOS RPG" }
17//!     fn update(&mut self, engine: &mut ProofEngine, dt: f32) {
18//!         self.tick += 1;
19//!     }
20//! }
21//! ```
22
23use crate::{ProofEngine, EngineConfig};
24
25/// The integration contract between a game and the Proof Engine.
26///
27/// Implement this trait on your game state struct. Pass it to
28/// `ProofEngine::run_game()` to start the game loop.
29pub trait ProofGame {
30    /// The window title shown for this game.
31    fn title(&self) -> &str;
32
33    /// Called once before the game loop starts. Use this to spawn initial
34    /// entities, set up the scene, and configure the camera.
35    fn on_start(&mut self, _engine: &mut ProofEngine) {}
36
37    /// Called every frame. `dt` is the time in seconds since the last frame.
38    /// Apply game logic, spawn entities, react to input here.
39    fn update(&mut self, engine: &mut ProofEngine, dt: f32);
40
41    /// Called when the window is resized. Override to reposition UI elements.
42    fn on_resize(&mut self, _engine: &mut ProofEngine, _width: u32, _height: u32) {}
43
44    /// Called once when the game loop exits cleanly (window closed or
45    /// `engine.request_quit()` called). Use for save/cleanup.
46    fn on_stop(&mut self, _engine: &mut ProofEngine) {}
47
48    /// Engine configuration. Override to customize window size, title, etc.
49    /// Called before `on_start()`.
50    fn config(&self) -> EngineConfig {
51        EngineConfig {
52            window_title: self.title().to_string(),
53            ..EngineConfig::default()
54        }
55    }
56}
57
58impl ProofEngine {
59    /// Run the engine with a `ProofGame` implementation.
60    ///
61    /// This is the preferred entry point for games that implement [`ProofGame`].
62    /// It calls `on_start()`, runs the game loop calling `update()` each frame,
63    /// then calls `on_stop()` on clean exit.
64    ///
65    /// ```rust,no_run
66    /// use proof_engine::prelude::*;
67    /// use proof_engine::integration::ProofGame;
68    ///
69    /// struct MyGame;
70    /// impl ProofGame for MyGame {
71    ///     fn title(&self) -> &str { "My Game" }
72    ///     fn update(&mut self, _engine: &mut ProofEngine, _dt: f32) {}
73    /// }
74    ///
75    /// ProofEngine::run_game(MyGame);
76    /// ```
77    pub fn run_game<G: ProofGame>(mut game: G) {
78        let config = game.config();
79        let mut engine = ProofEngine::new(config);
80
81        game.on_start(&mut engine);
82
83        engine.run(|eng, dt| {
84            // Handle resize events from the pipeline
85            if let Some((w, h)) = eng.input.window_resized {
86                game.on_resize(eng, w, h);
87            }
88            game.update(eng, dt);
89        });
90
91        game.on_stop(&mut engine);
92    }
93}
94
95
96// ── CHAOS RPG event bridge ─────────────────────────────────────────────────────
97
98/// Events that chaos-rpg-core can send to the proof-engine renderer.
99///
100/// These map 1:1 to proof-engine API calls, allowing the game to be
101/// decoupled from the rendering details.
102#[derive(Clone, Debug)]
103pub enum GameEvent {
104    /// Spawn a damage number at a world position.
105    DamageNumber {
106        amount: f32,
107        position: glam::Vec3,
108        critical: bool,
109    },
110    /// Flash the screen (trauma/shake).
111    ScreenShake { intensity: f32 },
112    /// Trigger a death explosion at a position.
113    EntityDeath { position: glam::Vec3 },
114    /// Trigger a spell impact effect.
115    SpellImpact { position: glam::Vec3, color: glam::Vec4, radius: f32 },
116    /// Change the ambient music vibe.
117    MusicVibe(crate::audio::MusicVibe),
118    /// Play a named sound effect.
119    PlaySfx { name: String, position: glam::Vec3, volume: f32 },
120}
121
122impl ProofEngine {
123    /// Dispatch a `GameEvent` to the appropriate engine subsystem.
124    ///
125    /// This is the primary integration point — chaos-rpg-core can queue events
126    /// each frame and the engine handles the visual/audio response.
127    pub fn dispatch(&mut self, event: GameEvent) {
128        match event {
129            GameEvent::DamageNumber { amount, position, critical } => {
130                use crate::{Glyph, RenderLayer, MathFunction};
131                let color = if critical {
132                    glam::Vec4::new(1.0, 0.2, 0.0, 1.0) // orange-red crit
133                } else {
134                    glam::Vec4::new(1.0, 1.0, 0.4, 1.0) // yellow normal
135                };
136                // Format as text glyphs
137                let text = format!("{:.0}", amount);
138                let len = text.len() as f32;
139                for (i, ch) in text.chars().enumerate() {
140                    let x_off = (i as f32 - len * 0.5) * 0.6;
141                    self.spawn_glyph(Glyph {
142                        character: ch,
143                        position: position + glam::Vec3::new(x_off, 1.0, 0.0),
144                        color,
145                        emission: if critical { 1.5 } else { 0.8 },
146                        glow_color: glam::Vec3::new(color.x, color.y, color.z),
147                        glow_radius: if critical { 2.0 } else { 0.8 },
148                        life_function: Some(MathFunction::Breathing {
149                            rate: 2.0,
150                            depth: 0.3,
151                        }),
152                        layer: RenderLayer::UI,
153                        ..Default::default()
154                    });
155                }
156            }
157
158            GameEvent::ScreenShake { intensity } => {
159                self.add_trauma(intensity);
160            }
161
162            GameEvent::EntityDeath { position } => {
163                use crate::particle::EmitterPreset;
164                self.emit_particles(EmitterPreset::DeathExplosion {
165                    color: glam::Vec4::new(1.0, 0.3, 0.1, 1.0),
166                }, position);
167                self.add_trauma(0.4);
168            }
169
170            GameEvent::SpellImpact { position, color, radius } => {
171                use crate::{Glyph, RenderLayer};
172                // Ring of impact glyphs
173                let n = (radius * 8.0) as usize;
174                for i in 0..n {
175                    let angle = (i as f32 / n as f32) * std::f32::consts::TAU;
176                    let pos = position + glam::Vec3::new(
177                        angle.cos() * radius,
178                        angle.sin() * radius,
179                        0.0,
180                    );
181                    self.spawn_glyph(Glyph {
182                        character: '✦',
183                        position: pos,
184                        color,
185                        emission: 1.2,
186                        glow_color: glam::Vec3::new(color.x, color.y, color.z),
187                        glow_radius: 1.5,
188                        layer: RenderLayer::Particle,
189                        ..Default::default()
190                    });
191                }
192                self.add_trauma(0.2);
193            }
194
195            GameEvent::MusicVibe(vibe) => {
196                if let Some(ref audio) = self.audio {
197                    audio.emit(crate::AudioEvent::SetMusicVibe(vibe));
198                }
199            }
200
201            GameEvent::PlaySfx { name, position, volume } => {
202                if let Some(ref audio) = self.audio {
203                    audio.emit(crate::AudioEvent::PlaySfx { name, position, volume });
204                }
205            }
206        }
207    }
208}