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}