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)]
275fn op_get_emitter_particle_count(state: &mut OpState, id: u32) -> u32 {
276 let ps = state.borrow_mut::<Rc<RefCell<ParticleState>>>();
277 let ps = ps.borrow();
278 match ps.find(id) {
279 Some(idx) => ps.emitters[idx].particles.len() as u32,
280 None => 0,
281 }
282}
283
284#[deno_core::op2]
288#[buffer]
289fn op_get_emitter_sprite_data(state: &mut OpState, id: u32) -> Vec<u8> {
290 let ps = state.borrow_mut::<Rc<RefCell<ParticleState>>>();
291 let ps = ps.borrow();
292 let idx = match ps.find(id) {
293 Some(i) => i,
294 None => return Vec::new(),
295 };
296
297 let emitter = &ps.emitters[idx];
298 let count = emitter.particles.len();
299 let mut floats = Vec::with_capacity(count * 6);
300
301 for p in &emitter.particles {
302 floats.push(p.x);
303 floats.push(p.y);
304 floats.push(p.angle);
305 floats.push(p.scale);
306 floats.push(p.alpha);
307 floats.push(f32::from_bits(p.texture_id));
308 }
309
310 bytemuck::cast_slice(&floats).to_vec()
311}
312
313deno_core::extension!(
314 particle_ext,
315 ops = [
316 op_create_emitter,
317 op_update_emitter,
318 op_destroy_emitter,
319 op_get_emitter_particle_count,
320 op_get_emitter_sprite_data,
321 ],
322);
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn test_emitter_config_default() {
330 let cfg = EmitterConfig::default();
331 assert_eq!(cfg.spawn_rate, 10.0);
332 assert!(cfg.lifetime_min > 0.0);
333 assert!(cfg.lifetime_max >= cfg.lifetime_min);
334 assert_eq!(cfg.alpha_start, 1.0);
335 assert_eq!(cfg.alpha_end, 0.0);
336 assert_eq!(cfg.texture_id, 0);
337 }
338
339 #[test]
340 fn test_particle_emitter_new() {
341 let cfg = EmitterConfig::default();
342 let emitter = ParticleEmitter::new(1, cfg);
343
344 assert_eq!(emitter.id, 1);
345 assert!(emitter.particles.is_empty());
346 assert_eq!(emitter.time_accumulator, 0.0);
347 }
348
349 #[test]
350 fn test_emitter_deterministic_rng() {
351 let cfg = EmitterConfig::default();
352 let mut e1 = ParticleEmitter::new(42, cfg.clone());
353 let mut e2 = ParticleEmitter::new(42, cfg);
354
355 let r1_a = e1.rand();
357 let r1_b = e1.rand();
358 let r2_a = e2.rand();
359 let r2_b = e2.rand();
360
361 assert_eq!(r1_a, r2_a);
362 assert_eq!(r1_b, r2_b);
363 }
364
365 #[test]
366 fn test_emitter_different_seeds_different_rng() {
367 let cfg = EmitterConfig::default();
368 let mut e1 = ParticleEmitter::new(1, cfg.clone());
369 let mut e2 = ParticleEmitter::new(2, cfg);
370
371 let r1 = e1.rand();
373 let r2 = e2.rand();
374
375 assert_ne!(r1, r2);
376 }
377
378 #[test]
379 fn test_emitter_rand_in_range() {
380 let cfg = EmitterConfig::default();
381 let mut emitter = ParticleEmitter::new(123, cfg);
382
383 for _ in 0..100 {
384 let v = emitter.rand();
385 assert!(v >= 0.0 && v < 1.0, "rand() should be in [0, 1), got {}", v);
386 }
387 }
388
389 #[test]
390 fn test_emitter_rand_range() {
391 let cfg = EmitterConfig::default();
392 let mut emitter = ParticleEmitter::new(456, cfg);
393
394 for _ in 0..100 {
395 let v = emitter.rand_range(5.0, 10.0);
396 assert!(v >= 5.0 && v <= 10.0, "rand_range(5, 10) should be in [5, 10], got {}", v);
397 }
398 }
399
400 #[test]
401 fn test_emitter_spawns_particles() {
402 let cfg = EmitterConfig {
403 spawn_rate: 100.0, speed_min: 0.0, speed_max: 0.0,
406 ..EmitterConfig::default()
407 };
408 let mut emitter = ParticleEmitter::new(1, cfg);
409
410 assert!(emitter.particles.is_empty());
411
412 emitter.update(0.1, 100.0, 200.0);
414
415 assert!(!emitter.particles.is_empty());
416 for p in &emitter.particles {
418 assert_eq!(p.x, 100.0);
419 assert_eq!(p.y, 200.0);
420 }
421 }
422
423 #[test]
424 fn test_particle_lifetime_removal() {
425 let cfg = EmitterConfig {
426 spawn_rate: 0.0, lifetime_min: 0.1,
428 lifetime_max: 0.1,
429 ..EmitterConfig::default()
430 };
431 let mut emitter = ParticleEmitter::new(1, cfg);
432
433 emitter.spawn_particle(0.0, 0.0);
435 assert_eq!(emitter.particles.len(), 1);
436
437 emitter.update(0.2, 0.0, 0.0);
439
440 assert!(emitter.particles.is_empty(), "Particle should be removed after lifetime expires");
441 }
442
443 #[test]
444 fn test_particle_state_new() {
445 let state = ParticleState::new();
446 assert!(state.emitters.is_empty());
447 assert_eq!(state.next_id, 1);
448 }
449
450 #[test]
451 fn test_particle_state_find() {
452 let mut state = ParticleState::new();
453 state.emitters.push(ParticleEmitter::new(5, EmitterConfig::default()));
454 state.emitters.push(ParticleEmitter::new(10, EmitterConfig::default()));
455
456 assert_eq!(state.find(5), Some(0));
457 assert_eq!(state.find(10), Some(1));
458 assert_eq!(state.find(999), None);
459 }
460
461 #[test]
462 fn test_sprite_data_packing() {
463 let cfg = EmitterConfig {
464 spawn_rate: 0.0,
465 texture_id: 42,
466 ..EmitterConfig::default()
467 };
468 let mut emitter = ParticleEmitter::new(1, cfg);
469 emitter.spawn_particle(100.0, 200.0);
470
471 let p = &emitter.particles[0];
473 let mut floats = Vec::new();
474 floats.push(p.x);
475 floats.push(p.y);
476 floats.push(p.angle);
477 floats.push(p.scale);
478 floats.push(p.alpha);
479 floats.push(f32::from_bits(p.texture_id));
480
481 assert_eq!(floats.len(), 6);
482 assert_eq!(floats[0], 100.0); assert_eq!(floats[1], 200.0); assert_eq!(f32::to_bits(floats[5]), 42); }
486}