1use std::cell::RefCell;
12use std::rc::Rc;
13
14use deno_core::OpState;
15
16#[derive(Debug, Clone)]
18struct Particle {
19 x: f32,
20 y: f32,
21 vx: f32,
22 vy: f32,
23 angle: f32,
24 angular_vel: f32,
25 scale: f32,
26 alpha: f32,
27 lifetime: f32,
28 max_lifetime: f32,
29 texture_id: u32,
30}
31
32#[derive(Debug, Clone)]
34struct EmitterConfig {
35 spawn_rate: f32,
36 lifetime_min: f32,
37 lifetime_max: f32,
38 speed_min: f32,
39 speed_max: f32,
40 direction: f32,
41 spread: f32,
42 scale_min: f32,
43 scale_max: f32,
44 alpha_start: f32,
45 alpha_end: f32,
46 gravity_x: f32,
47 gravity_y: f32,
48 texture_id: u32,
49}
50
51impl Default for EmitterConfig {
52 fn default() -> Self {
53 Self {
54 spawn_rate: 10.0,
55 lifetime_min: 0.5,
56 lifetime_max: 1.5,
57 speed_min: 20.0,
58 speed_max: 80.0,
59 direction: -std::f32::consts::FRAC_PI_2, spread: std::f32::consts::PI,
61 scale_min: 1.0,
62 scale_max: 1.0,
63 alpha_start: 1.0,
64 alpha_end: 0.0,
65 gravity_x: 0.0,
66 gravity_y: 0.0,
67 texture_id: 0,
68 }
69 }
70}
71
72#[derive(Debug)]
74pub struct ParticleEmitter {
75 pub id: u32,
76 config: EmitterConfig,
77 particles: Vec<Particle>,
78 time_accumulator: f32,
79 rng_state: u32,
81}
82
83impl ParticleEmitter {
84 fn new(id: u32, config: EmitterConfig) -> Self {
85 let rng_state = id.wrapping_mul(2654435761).max(1);
87 Self {
88 id,
89 config,
90 particles: Vec::new(),
91 time_accumulator: 0.0,
92 rng_state,
93 }
94 }
95
96 fn rand(&mut self) -> f32 {
98 let mut s = self.rng_state;
99 s ^= s << 13;
100 s ^= s >> 17;
101 s ^= s << 5;
102 self.rng_state = s;
103 (s as f32) / (u32::MAX as f32)
104 }
105
106 fn rand_range(&mut self, min: f32, max: f32) -> f32 {
107 min + self.rand() * (max - min)
108 }
109
110 fn spawn_particle(&mut self, cx: f32, cy: f32) {
111 let lifetime_min = self.config.lifetime_min;
113 let lifetime_max = self.config.lifetime_max;
114 let speed_min = self.config.speed_min;
115 let speed_max = self.config.speed_max;
116 let direction = self.config.direction;
117 let half_spread = self.config.spread * 0.5;
118 let scale_min = self.config.scale_min;
119 let scale_max = self.config.scale_max;
120 let alpha_start = self.config.alpha_start;
121 let texture_id = self.config.texture_id;
122
123 let lifetime = self.rand_range(lifetime_min, lifetime_max);
124 let speed = self.rand_range(speed_min, speed_max);
125 let angle = direction + self.rand_range(-half_spread, half_spread);
126 let scale = self.rand_range(scale_min, scale_max);
127
128 self.particles.push(Particle {
129 x: cx,
130 y: cy,
131 vx: angle.cos() * speed,
132 vy: angle.sin() * speed,
133 angle: 0.0,
134 angular_vel: 0.0,
135 scale,
136 alpha: alpha_start,
137 lifetime: 0.0,
138 max_lifetime: lifetime,
139 texture_id,
140 });
141 }
142
143 fn update(&mut self, dt: f32, cx: f32, cy: f32) {
144 let spawn_rate = self.config.spawn_rate;
146 let gx = self.config.gravity_x;
147 let gy = self.config.gravity_y;
148 let alpha_start = self.config.alpha_start;
149 let alpha_end = self.config.alpha_end;
150
151 self.time_accumulator += dt * spawn_rate;
153 while self.time_accumulator >= 1.0 {
154 self.spawn_particle(cx, cy);
155 self.time_accumulator -= 1.0;
156 }
157
158 self.particles.retain_mut(|p| {
159 p.lifetime += dt;
160 if p.lifetime >= p.max_lifetime {
161 return false;
162 }
163
164 p.vx += gx * dt;
165 p.vy += gy * dt;
166 p.x += p.vx * dt;
167 p.y += p.vy * dt;
168 p.angle += p.angular_vel * dt;
169
170 let t = p.lifetime / p.max_lifetime;
172 p.alpha = alpha_start + (alpha_end - alpha_start) * t;
173
174 true
175 });
176 }
177}
178
179pub struct ParticleState {
181 pub emitters: Vec<ParticleEmitter>,
182 pub next_id: u32,
183}
184
185impl ParticleState {
186 pub fn new() -> Self {
187 Self {
188 emitters: Vec::new(),
189 next_id: 1,
190 }
191 }
192
193 fn find(&self, id: u32) -> Option<usize> {
194 self.emitters.iter().position(|e| e.id == id)
195 }
196}
197
198#[deno_core::op2(fast)]
206fn op_create_emitter(state: &mut OpState, #[string] config_json: &str) -> u32 {
207 let ps = state.borrow_mut::<Rc<RefCell<ParticleState>>>();
208 let mut ps = ps.borrow_mut();
209
210 let mut cfg = EmitterConfig::default();
211
212 fn extract_f32(json: &str, key: &str) -> Option<f32> {
215 let needle = format!("\"{}\"", key);
216 let start = json.find(&needle)?;
217 let after_key = &json[start + needle.len()..];
218 let colon = after_key.find(':')?;
219 let val_start = &after_key[colon + 1..];
220 let val_start = val_start.trim_start();
222 let end = val_start.find(|c: char| c == ',' || c == '}').unwrap_or(val_start.len());
224 val_start[..end].trim().parse().ok()
225 }
226
227 fn extract_u32(json: &str, key: &str) -> Option<u32> {
228 extract_f32(json, key).map(|f| f as u32)
229 }
230
231 if let Some(v) = extract_f32(config_json, "spawnRate") { cfg.spawn_rate = v; }
232 if let Some(v) = extract_f32(config_json, "lifetimeMin") { cfg.lifetime_min = v; }
233 if let Some(v) = extract_f32(config_json, "lifetimeMax") { cfg.lifetime_max = v; }
234 if let Some(v) = extract_f32(config_json, "speedMin") { cfg.speed_min = v; }
235 if let Some(v) = extract_f32(config_json, "speedMax") { cfg.speed_max = v; }
236 if let Some(v) = extract_f32(config_json, "direction") { cfg.direction = v; }
237 if let Some(v) = extract_f32(config_json, "spread") { cfg.spread = v; }
238 if let Some(v) = extract_f32(config_json, "scaleMin") { cfg.scale_min = v; }
239 if let Some(v) = extract_f32(config_json, "scaleMax") { cfg.scale_max = v; }
240 if let Some(v) = extract_f32(config_json, "alphaStart") { cfg.alpha_start = v; }
241 if let Some(v) = extract_f32(config_json, "alphaEnd") { cfg.alpha_end = v; }
242 if let Some(v) = extract_f32(config_json, "gravityX") { cfg.gravity_x = v; }
243 if let Some(v) = extract_f32(config_json, "gravityY") { cfg.gravity_y = v; }
244 if let Some(v) = extract_u32(config_json, "textureId") { cfg.texture_id = v; }
245
246 let id = ps.next_id;
247 ps.next_id += 1;
248 ps.emitters.push(ParticleEmitter::new(id, cfg));
249 id
250}
251
252#[deno_core::op2(fast)]
255fn op_update_emitter(state: &mut OpState, id: u32, dt: f64, cx: f64, cy: f64) {
256 let ps = state.borrow_mut::<Rc<RefCell<ParticleState>>>();
257 let mut ps = ps.borrow_mut();
258 if let Some(idx) = ps.find(id) {
259 ps.emitters[idx].update(dt as f32, cx as f32, cy as f32);
260 }
261}
262
263#[deno_core::op2(fast)]
265fn op_destroy_emitter(state: &mut OpState, id: u32) {
266 let ps = state.borrow_mut::<Rc<RefCell<ParticleState>>>();
267 let mut ps = ps.borrow_mut();
268 if let Some(idx) = ps.find(id) {
269 ps.emitters.swap_remove(idx);
270 }
271}
272
273#[deno_core::op2(fast)]
276fn op_set_emitter_spawn_rate(state: &mut OpState, id: u32, spawn_rate: f64) {
277 let ps = state.borrow_mut::<Rc<RefCell<ParticleState>>>();
278 let mut ps = ps.borrow_mut();
279 if let Some(idx) = ps.find(id) {
280 ps.emitters[idx].config.spawn_rate = spawn_rate as f32;
281 }
282}
283
284#[deno_core::op2(fast)]
286fn op_get_emitter_particle_count(state: &mut OpState, id: u32) -> u32 {
287 let ps = state.borrow_mut::<Rc<RefCell<ParticleState>>>();
288 let ps = ps.borrow();
289 match ps.find(id) {
290 Some(idx) => ps.emitters[idx].particles.len() as u32,
291 None => 0,
292 }
293}
294
295#[deno_core::op2]
299#[buffer]
300fn op_get_emitter_sprite_data(state: &mut OpState, id: u32) -> Vec<u8> {
301 let ps = state.borrow_mut::<Rc<RefCell<ParticleState>>>();
302 let ps = ps.borrow();
303 let idx = match ps.find(id) {
304 Some(i) => i,
305 None => return Vec::new(),
306 };
307
308 let emitter = &ps.emitters[idx];
309 let count = emitter.particles.len();
310 let mut floats = Vec::with_capacity(count * 6);
311
312 for p in &emitter.particles {
313 floats.push(p.x);
314 floats.push(p.y);
315 floats.push(p.angle);
316 floats.push(p.scale);
317 floats.push(p.alpha);
318 floats.push(f32::from_bits(p.texture_id));
319 }
320
321 bytemuck::cast_slice(&floats).to_vec()
322}
323
324deno_core::extension!(
325 particle_ext,
326 ops = [
327 op_create_emitter,
328 op_update_emitter,
329 op_destroy_emitter,
330 op_set_emitter_spawn_rate,
331 op_get_emitter_particle_count,
332 op_get_emitter_sprite_data,
333 ],
334);
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
341 fn test_emitter_config_default() {
342 let cfg = EmitterConfig::default();
343 assert_eq!(cfg.spawn_rate, 10.0);
344 assert!(cfg.lifetime_min > 0.0);
345 assert!(cfg.lifetime_max >= cfg.lifetime_min);
346 assert_eq!(cfg.alpha_start, 1.0);
347 assert_eq!(cfg.alpha_end, 0.0);
348 assert_eq!(cfg.texture_id, 0);
349 }
350
351 #[test]
352 fn test_particle_emitter_new() {
353 let cfg = EmitterConfig::default();
354 let emitter = ParticleEmitter::new(1, cfg);
355
356 assert_eq!(emitter.id, 1);
357 assert!(emitter.particles.is_empty());
358 assert_eq!(emitter.time_accumulator, 0.0);
359 }
360
361 #[test]
362 fn test_emitter_deterministic_rng() {
363 let cfg = EmitterConfig::default();
364 let mut e1 = ParticleEmitter::new(42, cfg.clone());
365 let mut e2 = ParticleEmitter::new(42, cfg);
366
367 let r1_a = e1.rand();
369 let r1_b = e1.rand();
370 let r2_a = e2.rand();
371 let r2_b = e2.rand();
372
373 assert_eq!(r1_a, r2_a);
374 assert_eq!(r1_b, r2_b);
375 }
376
377 #[test]
378 fn test_emitter_different_seeds_different_rng() {
379 let cfg = EmitterConfig::default();
380 let mut e1 = ParticleEmitter::new(1, cfg.clone());
381 let mut e2 = ParticleEmitter::new(2, cfg);
382
383 let r1 = e1.rand();
385 let r2 = e2.rand();
386
387 assert_ne!(r1, r2);
388 }
389
390 #[test]
391 fn test_emitter_rand_in_range() {
392 let cfg = EmitterConfig::default();
393 let mut emitter = ParticleEmitter::new(123, cfg);
394
395 for _ in 0..100 {
396 let v = emitter.rand();
397 assert!(v >= 0.0 && v < 1.0, "rand() should be in [0, 1), got {}", v);
398 }
399 }
400
401 #[test]
402 fn test_emitter_rand_range() {
403 let cfg = EmitterConfig::default();
404 let mut emitter = ParticleEmitter::new(456, cfg);
405
406 for _ in 0..100 {
407 let v = emitter.rand_range(5.0, 10.0);
408 assert!(v >= 5.0 && v <= 10.0, "rand_range(5, 10) should be in [5, 10], got {}", v);
409 }
410 }
411
412 #[test]
413 fn test_emitter_spawns_particles() {
414 let cfg = EmitterConfig {
415 spawn_rate: 100.0, speed_min: 0.0, speed_max: 0.0,
418 ..EmitterConfig::default()
419 };
420 let mut emitter = ParticleEmitter::new(1, cfg);
421
422 assert!(emitter.particles.is_empty());
423
424 emitter.update(0.1, 100.0, 200.0);
426
427 assert!(!emitter.particles.is_empty());
428 for p in &emitter.particles {
430 assert_eq!(p.x, 100.0);
431 assert_eq!(p.y, 200.0);
432 }
433 }
434
435 #[test]
436 fn test_particle_lifetime_removal() {
437 let cfg = EmitterConfig {
438 spawn_rate: 0.0, lifetime_min: 0.1,
440 lifetime_max: 0.1,
441 ..EmitterConfig::default()
442 };
443 let mut emitter = ParticleEmitter::new(1, cfg);
444
445 emitter.spawn_particle(0.0, 0.0);
447 assert_eq!(emitter.particles.len(), 1);
448
449 emitter.update(0.2, 0.0, 0.0);
451
452 assert!(emitter.particles.is_empty(), "Particle should be removed after lifetime expires");
453 }
454
455 #[test]
456 fn test_particle_state_new() {
457 let state = ParticleState::new();
458 assert!(state.emitters.is_empty());
459 assert_eq!(state.next_id, 1);
460 }
461
462 #[test]
463 fn test_particle_state_find() {
464 let mut state = ParticleState::new();
465 state.emitters.push(ParticleEmitter::new(5, EmitterConfig::default()));
466 state.emitters.push(ParticleEmitter::new(10, EmitterConfig::default()));
467
468 assert_eq!(state.find(5), Some(0));
469 assert_eq!(state.find(10), Some(1));
470 assert_eq!(state.find(999), None);
471 }
472
473 #[test]
474 fn test_sprite_data_packing() {
475 let cfg = EmitterConfig {
476 spawn_rate: 0.0,
477 texture_id: 42,
478 ..EmitterConfig::default()
479 };
480 let mut emitter = ParticleEmitter::new(1, cfg);
481 emitter.spawn_particle(100.0, 200.0);
482
483 let p = &emitter.particles[0];
485 let mut floats = Vec::new();
486 floats.push(p.x);
487 floats.push(p.y);
488 floats.push(p.angle);
489 floats.push(p.scale);
490 floats.push(p.alpha);
491 floats.push(f32::from_bits(p.texture_id));
492
493 assert_eq!(floats.len(), 6);
494 assert_eq!(floats[0], 100.0); assert_eq!(floats[1], 200.0); assert_eq!(f32::to_bits(floats[5]), 42); }
498}