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