1use std::f32::consts::TAU;
2use jengine::engine::{Color, Game, jEngine, KeyCode};
3use jengine::ui::{Padding, BorderStyle};
4use jengine::ui::widgets::Widget;
5use jengine::ecs::{Entity, World};
6use jengine::{DEFAULT_TILESET, DEFAULT_FONT_METADATA, DEFAULT_TILE_W, DEFAULT_TILE_H};
7
8struct Position { x: f32, y: f32 }
11struct Velocity { vx: f32, vy: f32 }
12struct Life { current: f32, max: f32 }
13struct Particle {
14 color_start: [f32; 4],
15 color_end: [f32; 4],
16 size_start: f32,
17 size_end: f32,
18 drag: f32,
19}
20
21fn pseudo_rand(seed: u64) -> f32 {
24 let x = seed
25 .wrapping_mul(6364136223846793005)
26 .wrapping_add(1442695040888963407);
27 (x >> 33) as f32 / u32::MAX as f32
28}
29
30struct PostProcessState {
33 scanlines: bool,
34 vignette: bool,
35 chromatic: bool,
36 bloom: bool,
37}
38
39struct ParticleDemo {
40 world: World,
41 font_loaded: bool,
42 tick: u64,
43 pp: PostProcessState,
44}
45
46impl ParticleDemo {
47 fn new() -> Self {
48 Self {
49 world: World::new(),
50 font_loaded: false,
51 tick: 0,
52 pp: PostProcessState {
53 scanlines: false,
54 vignette: false,
55 chromatic: false,
56 bloom: false,
57 },
58 }
59 }
60
61 fn spawn_particle(&mut self, x: f32, y: f32, vx: f32, vy: f32, life: f32,
62 color_start: [f32; 4], color_end: [f32; 4],
63 size_start: f32, size_end: f32, drag: f32) {
64 let e = self.world.spawn();
65 self.world.insert(e, Position { x, y });
66 self.world.insert(e, Velocity { vx, vy });
67 self.world.insert(e, Life { current: life, max: life });
68 self.world.insert(e, Particle { color_start, color_end, size_start, size_end, drag });
69 }
70
71 fn spawn_fire_and_smoke(&mut self, x: f32, y: f32) {
73 for i in 0..2 {
75 let seed = self.tick.wrapping_add(i as u64 * 137);
76 let vx = (pseudo_rand(seed) - 0.5) * 60.0;
77 let vy = -100.0 - pseudo_rand(seed.wrapping_add(1)) * 50.0;
78 let life = 0.2 + pseudo_rand(seed.wrapping_add(2)) * 0.3;
79
80 let color_start = [1.0, 0.9, 0.2, 1.0]; let color_end = [1.0, 0.2, 0.0, 0.0]; self.spawn_particle(x, y, vx, vy, life, color_start, color_end, 8.0, 2.0, 1.5);
84 }
85
86 if self.tick % 4 == 0 {
88 let seed = self.tick.wrapping_add(999);
89 let vx = (pseudo_rand(seed) - 0.5) * 40.0;
90 let vy = -60.0 - pseudo_rand(seed.wrapping_add(1)) * 30.0;
91 let life = 1.5 + pseudo_rand(seed.wrapping_add(2)) * 1.0;
92
93 let g = 0.4 + pseudo_rand(seed.wrapping_add(3)) * 0.2;
94 let color_start = [g, g, g, 0.5];
95 let color_end = [g * 0.5, g * 0.5, g * 0.5, 0.0];
96
97 self.spawn_particle(x, y - 10.0, vx, vy, life, color_start, color_end, 4.0, 12.0, 0.8);
98 }
99 }
100
101 fn spawn_explosion(&mut self, engine: &mut jEngine, x: f32, y: f32) {
103 engine.camera_shake(15.0);
105
106 for i in 0..30 {
108 let seed = self.tick.wrapping_add(i as u64 * 71);
109 let angle = pseudo_rand(seed) * TAU;
110 let speed = 200.0 + pseudo_rand(seed.wrapping_add(1)) * 400.0;
111 let vx = angle.cos() * speed;
112 let vy = angle.sin() * speed;
113 let life = 0.1 + pseudo_rand(seed.wrapping_add(2)) * 0.15;
114 self.spawn_particle(x, y, vx, vy, life, [1.0, 1.0, 1.0, 1.0], [1.0, 0.9, 0.5, 0.0], 12.0, 4.0, 5.0);
115 }
116
117 for i in 0..80 {
119 let seed = self.tick.wrapping_add(i as u64 * 113 + 500);
120 let angle = pseudo_rand(seed) * TAU;
121 let speed = 100.0 + pseudo_rand(seed.wrapping_add(1)) * 600.0;
122 let vx = angle.cos() * speed;
123 let vy = angle.sin() * speed;
124 let life = 0.3 + pseudo_rand(seed.wrapping_add(2)) * 0.5;
125 self.spawn_particle(x, y, vx, vy, life, [1.0, 0.6, 0.1, 1.0], [0.8, 0.1, 0.0, 0.0], 4.0, 1.0, 3.0);
126 }
127
128 for i in 0..40 {
130 let seed = self.tick.wrapping_add(i as u64 * 197 + 1000);
131 let angle = pseudo_rand(seed) * TAU;
132 let speed = 20.0 + pseudo_rand(seed.wrapping_add(1)) * 80.0;
133 let vx = angle.cos() * speed;
134 let vy = angle.sin() * speed - 20.0; let life = 0.8 + pseudo_rand(seed.wrapping_add(2)) * 1.2;
136 let grey = 0.3 + pseudo_rand(seed.wrapping_add(3)) * 0.2;
137 self.spawn_particle(x, y, vx, vy, life, [1.0, 0.3, 0.0, 0.8], [grey, grey, grey, 0.0], 6.0, 16.0, 1.0);
138 }
139 }
140
141 fn spawn_glitch(&mut self, x: f32, y: f32) {
143 let count = 12;
144 for i in 0..count {
145 let seed = self.tick.wrapping_add(i as u64 * 31);
146 let vx = (pseudo_rand(seed) - 0.5) * 600.0;
147 let vy = (pseudo_rand(seed.wrapping_add(1)) - 0.5) * 10.0;
148 let life = 0.05 + pseudo_rand(seed.wrapping_add(2)) * 0.15;
149
150 let r = pseudo_rand(seed.wrapping_add(3));
151 let color = if r < 0.33 {
152 [0.0, 1.0, 1.0, 1.0] } else if r < 0.66 {
154 [1.0, 0.0, 1.0, 1.0] } else {
156 [1.0, 1.0, 1.0, 1.0] };
158
159 self.spawn_particle(x, y + (pseudo_rand(seed.wrapping_add(4)) - 0.5) * 40.0,
161 vx, vy, life, color, color, 12.0, 2.0, 0.0);
162 }
163 }
164
165 fn spawn_slash(&mut self, x: f32, y: f32) {
167 let count = 20;
168 let base_angle = pseudo_rand(self.tick) * TAU;
169 for i in 0..count {
170 let seed = self.tick.wrapping_add(i as u64 * 17);
171 let angle = base_angle + (i as f32 / count as f32 - 0.5) * 0.8;
172 let speed = 300.0 + pseudo_rand(seed) * 150.0;
173
174 let vx = angle.cos() * speed;
175 let vy = angle.sin() * speed;
176 let life = 0.1 + pseudo_rand(seed.wrapping_add(1)) * 0.1;
177
178 let color_start = [0.8, 0.9, 1.0, 1.0]; let color_end = [1.0, 1.0, 1.0, 0.0];
180
181 self.spawn_particle(x, y, vx, vy, life, color_start, color_end, 2.0, 6.0, 8.0);
182 }
183 }
184}
185
186impl Game for ParticleDemo {
187 fn on_enter(&mut self, engine: &mut jEngine) {
188 let sw = engine.renderer.window.inner_size().width as f32;
189 let sh = engine.renderer.window.inner_size().height as f32;
190 engine.set_camera_pos(sw * 0.5, sh * 0.5);
192 }
193
194 fn update(&mut self, engine: &mut jEngine) {
195 self.tick += 1;
196 let dt = engine.dt();
197
198 if engine.is_key_pressed(KeyCode::Escape) {
199 engine.request_quit();
200 }
201
202 let sw = engine.renderer.window.inner_size().width as f32;
204 let sh = engine.renderer.window.inner_size().height as f32;
205 self.spawn_fire_and_smoke(sw * 0.5, sh * 0.85);
206
207 if engine.input.is_mouse_pressed(jengine::input::MouseButton::Left) && !engine.input.mouse_consumed {
209 let [mx, my] = engine.input.mouse_pos;
210 self.spawn_explosion(engine, mx, my);
211 }
212
213 if engine.is_key_pressed(KeyCode::KeyG) {
214 let [mx, my] = engine.input.mouse_pos;
215 self.spawn_glitch(mx, my);
216 }
217
218 if engine.is_key_pressed(KeyCode::KeyS) {
219 let [mx, my] = engine.input.mouse_pos;
220 self.spawn_slash(mx, my);
221 }
222
223 let mut pp_changed = false;
225 if engine.is_key_pressed(KeyCode::Digit1) { self.pp.scanlines = !self.pp.scanlines; pp_changed = true; }
226 if engine.is_key_pressed(KeyCode::Digit2) { self.pp.vignette = !self.pp.vignette; pp_changed = true; }
227 if engine.is_key_pressed(KeyCode::Digit3) { self.pp.chromatic = !self.pp.chromatic; pp_changed = true; }
228 if engine.is_key_pressed(KeyCode::Digit4) { self.pp.bloom = !self.pp.bloom; pp_changed = true; }
229
230 if pp_changed {
231 engine.renderer.post_process.clear_effects();
232 if self.pp.scanlines { engine.set_scanlines(true); }
233 if self.pp.vignette { engine.set_vignette(true); }
234 if self.pp.chromatic { engine.set_chromatic_aberration(true); }
235 if self.pp.bloom { engine.set_bloom(true); }
236 }
237
238 let dead: Vec<Entity> = self.world.query_multi_mut::<(Position, Velocity, Life, Particle)>()
240 .filter_map(|(e, (pos, vel, life, p))| {
241 life.current -= dt;
242 if life.current <= 0.0 {
243 return Some(e);
244 }
245
246 let drag_factor = (1.0 - p.drag * dt).max(0.0);
248 vel.vx *= drag_factor;
249 vel.vy *= drag_factor;
250
251 pos.x += vel.vx * dt;
252 pos.y += vel.vy * dt;
253
254 None
255 })
256 .collect();
257
258 for e in dead {
259 self.world.despawn(e);
260 }
261 }
262
263 fn render(&mut self, engine: &mut jEngine) {
264 if !self.font_loaded {
265 if let Ok(font) = jengine::renderer::text::Font::from_mtsdf_json(DEFAULT_FONT_METADATA) {
266 engine.renderer.set_mtsdf_distance_range(font.distance_range);
267 engine.ui.text.set_font(font);
268 }
269 self.font_loaded = true;
270 }
271
272 engine.clear();
273
274 let sw = engine.renderer.window.inner_size().width as f32;
275 let sh = engine.renderer.window.inner_size().height as f32;
276
277 for y in 0..engine.grid_height() {
280 for x in 0..engine.grid_width() {
281 engine.set_background(x, y, Color([0.01, 0.01, 0.02, 1.0]));
282 }
283 }
284
285 for (_e, (pos, life, p)) in self.world.query_multi::<(Position, Life, Particle)>() {
287 let t = (life.current / life.max).clamp(0.0, 1.0);
288
289 let mut c = [0.0; 4];
291 for i in 0..4 {
292 c[i] = p.color_end[i] + (p.color_start[i] - p.color_end[i]) * t;
293 }
294
295 let size = p.size_end + (p.size_start - p.size_end) * t;
297
298 engine.draw_particle(pos.x, pos.y, Color(c), size);
299 }
300
301 let tw = engine.tile_width() as f32;
303 let th = engine.tile_height() as f32;
304
305 engine.ui.ui_text(tw, th, "PARTICLE SHOWCASE", Color::WHITE, Color::TRANSPARENT, Some(48.0));
306
307 let mut y = th * 4.0;
308 let help = [
309 "MOUSE LCLICK : Spawn Explosion",
310 "[S] KEY : Spawn Slash (at mouse)",
311 "[G] KEY : Spawn Glitch (at mouse)",
312 "CONTINUOUS : Fire & Smoke (bottom)",
313 "[ESC] : Quit demo"
314 ];
315
316 for line in help {
317 engine.ui.ui_text(tw, y, line, Color([0.6, 0.7, 0.7, 1.0]), Color::TRANSPARENT, None);
318 y += th * 1.2;
319 }
320
321 y += th;
322 let pp_help = [
323 format!("[1] Scanlines: {}", if self.pp.scanlines { "ON" } else { "OFF" }),
324 format!("[2] Vignette: {}", if self.pp.vignette { "ON" } else { "OFF" }),
325 format!("[3] Chromatic: {}", if self.pp.chromatic { "ON" } else { "OFF" }),
326 format!("[4] Bloom: {}", if self.pp.bloom { "ON" } else { "OFF" }),
327 ];
328 for line in pp_help {
329 engine.ui.ui_text(tw, y, &line, Color([0.4, 0.9, 0.6, 1.0]), Color::TRANSPARENT, None);
330 y += th * 1.2;
331 }
332
333 let count = self.world.entity_count();
334 engine.ui.ui_text(sw - tw * 18.0, sh - th * 2.5, &format!("Active Entities: {}", count), Color::CYAN, Color::TRANSPARENT, Some(20.0));
335 }
336
337 fn debug_render(&mut self, engine: &mut jEngine) -> Option<Box<dyn jengine::ui::widgets::Widget>> {
338 use jengine::ui::widgets::{VStack, TextWidget};
339 use jengine::ui::Alignment;
340
341 let fs = 12.0;
342 let tw = engine.tile_width() as f32;
343 let th = engine.tile_height() as f32;
344
345 let [mx, my] = engine.input.mouse_pos;
347 let [wx, wy] = engine.screen_to_world(mx, my);
348 let gx = (wx / tw).floor();
349 let gy = (wy / th).floor();
350
351 let [s_x, s_y] = engine.world_to_screen(gx * tw, gy * th);
353 let z = engine.camera_zoom();
354 engine.ui.debug_box(s_x, s_y, tw * z, th * z, Color::CYAN);
355
356 let mut hovered_entities = Vec::new();
358 for (entity, pos) in self.world.query::<Position>() {
359 if pos.x >= gx * tw && pos.x < (gx + 1.0) * tw
360 && pos.y >= gy * th && pos.y < (gy + 1.0) * th {
361 let components = self.world.components_for_entity(entity);
362 let short_names: Vec<String> = components.iter()
363 .map(|&full_name| full_name.split("::").last().unwrap_or(full_name).to_string())
364 .collect();
365 hovered_entities.push((entity, short_names));
366 }
367 }
368
369 if !hovered_entities.is_empty() {
371 let h_x = mx + 15.0;
372 let h_y = my + 15.0;
373 let panel_w = 220.0;
374
375 let mut popup = VStack::new(Alignment::Start)
376 .with_padding(Padding::all(5.0))
377 .with_bg(Color([0.05, 0.05, 0.1, 0.8]))
378 .with_border(BorderStyle::Thin, Color::CYAN);
379
380 for (entity, comps) in hovered_entities {
381 popup = popup.add(TextWidget {
382 text: format!("E{}: {}", entity.id(), comps.join(", ")),
383 size: Some(fs),
384 color: Some(Color::WHITE),
385 });
386 }
387 Widget::draw(&mut popup, engine, h_x, h_y, panel_w, None);
388 }
389
390 let total_entities = self.world.entity_count();
392 let mut stack = VStack::new(Alignment::Start).with_spacing(2.0);
393
394 stack = stack.add(TextWidget {
395 text: format!("Entities (Total): {}", total_entities),
396 size: Some(fs),
397 color: Some(Color::DARK_GRAY),
398 });
399
400 stack = stack.add(TextWidget {
401 text: "--- ENTITY LIST ---".to_string(),
402 size: Some(fs),
403 color: Some(Color::CYAN),
404 });
405
406 let entities = self.world.entities_debug_info_paginated(0, 100);
408 for (entity, components) in entities {
409 let mut short_comps = Vec::new();
410 for c in components {
411 short_comps.push(c.split("::").last().unwrap_or(c));
412 }
413 stack = stack.add(TextWidget {
414 text: format!("E{}: {}", entity.id(), short_comps.join(", ")),
415 size: Some(fs),
416 color: Some(Color::BLACK),
417 });
418 }
419
420 if total_entities > 10 {
421 stack = stack.add(TextWidget {
422 text: "... and more".to_string(),
423 size: Some(fs),
424 color: Some(Color([0.4, 0.4, 0.4, 1.0])),
425 });
426 }
427
428 Some(Box::new(stack))
429 }
430}
431
432fn main() {
433 jEngine::builder()
434 .with_title("jengine — Particle Showcase")
435 .with_size(1280, 720)
436 .with_tileset(DEFAULT_TILESET, DEFAULT_TILE_W, DEFAULT_TILE_H)
437 .run(ParticleDemo::new());
438}