Skip to main content

proof_engine/particle/
emitters.rs

1//! Preset emitter implementations.
2//!
3//! Each emitter translates a game event into a burst of MathParticles.
4//! The killing math determines how the death animation looks.
5//! The damage number determines the gravitational warp radius.
6
7use glam::{Vec3, Vec4};
8use std::f32::consts::TAU;
9use super::{EmitterPreset, MathParticle, ParticlePool, ParticleInteraction};
10use crate::glyph::{Glyph, BlendMode, RenderLayer};
11use crate::math::eval::MathFunction;
12use crate::math::attractors::AttractorType;
13
14pub fn emit_preset(pool: &mut ParticlePool, preset: EmitterPreset, origin: Vec3) {
15    match preset {
16        EmitterPreset::DeathExplosion { color } => emit_death(pool, origin, color),
17        EmitterPreset::LevelUpFountain => emit_level_up(pool, origin),
18        EmitterPreset::CritBurst => emit_crit_burst(pool, origin),
19        EmitterPreset::HitSparks { color, count } => emit_hit_sparks(pool, origin, color, count),
20        EmitterPreset::LootSparkle { color } => emit_loot_sparkle(pool, origin, color),
21        EmitterPreset::StunOrbit => emit_stun_orbit(pool, origin),
22        EmitterPreset::GravitationalCollapse { color, attractor } => {
23            emit_gravitational_collapse(pool, origin, color, attractor)
24        }
25        EmitterPreset::SpellStream { element_color } => {
26            emit_spell_stream(pool, origin, element_color)
27        }
28        EmitterPreset::HealSpiral => emit_heal_spiral(pool, origin),
29        EmitterPreset::EntropyCascade => emit_entropy_cascade(pool, origin),
30        EmitterPreset::StatusAmbient { effect_mask } => {
31            emit_status_ambient(pool, origin, effect_mask)
32        }
33        EmitterPreset::RoomAmbient { room_type_id } => {
34            emit_room_ambient(pool, origin, room_type_id)
35        }
36        EmitterPreset::BossEntrance { boss_id } => emit_boss_entrance(pool, origin, boss_id),
37        EmitterPreset::FireBurst { intensity } => {
38            emit_death(pool, origin, Vec4::new(1.0, 0.5 * intensity, 0.0, 1.0));
39        }
40        EmitterPreset::SmokePuff => {
41            emit_loot_sparkle(pool, origin, Vec4::new(0.4, 0.4, 0.4, 0.6));
42        }
43        EmitterPreset::ElectricDischarge { color } => {
44            emit_crit_burst(pool, origin);
45            emit_hit_sparks(pool, origin, color, 12);
46        }
47        EmitterPreset::BloodSplatter { color, count } => {
48            emit_hit_sparks(pool, origin, color, count);
49        }
50        EmitterPreset::IceShatter => {
51            emit_hit_sparks(pool, origin, Vec4::new(0.5, 0.8, 1.0, 1.0), 16);
52        }
53        EmitterPreset::PoisonCloud => {
54            emit_loot_sparkle(pool, origin, Vec4::new(0.2, 0.8, 0.1, 0.7));
55        }
56        EmitterPreset::TeleportFlash { color } => {
57            emit_death(pool, origin, color);
58            emit_crit_burst(pool, origin);
59        }
60        EmitterPreset::ShieldHit { shield_color } => {
61            emit_crit_burst(pool, origin);
62            emit_hit_sparks(pool, origin, shield_color, 8);
63        }
64        EmitterPreset::CoinScatter { count } => {
65            emit_hit_sparks(pool, origin, Vec4::new(1.0, 0.85, 0.0, 1.0), count);
66        }
67        EmitterPreset::RubbleDebris { count } => {
68            emit_hit_sparks(pool, origin, Vec4::new(0.5, 0.4, 0.3, 1.0), count);
69        }
70        EmitterPreset::RainShower => {
71            emit_level_up(pool, origin);
72        }
73        EmitterPreset::SnowFall => {
74            emit_level_up(pool, origin);
75        }
76        EmitterPreset::ConfettiBurst => {
77            emit_death(pool, origin, Vec4::new(1.0, 0.2, 0.8, 1.0));
78        }
79        EmitterPreset::Custom { template: _, count, shape: _ } => {
80            // Custom emitters are handled by ParticleSystem::burst directly
81            let _ = count;
82        }
83    }
84}
85
86// ── Emitter implementations ───────────────────────────────────────────────────
87
88fn emit_death(pool: &mut ParticlePool, origin: Vec3, color: Vec4) {
89    const CHARS: &[char] = &['☠', '×', '+', '·', '*', '#', '!', '▓', '▒', '░'];
90    for i in 0..40usize {
91        let angle = i as f32 * TAU / 40.0;
92        let speed = 0.3 + (i % 5) as f32 * 0.15;
93        pool.spawn(make_particle(
94            origin,
95            CHARS[i % CHARS.len()],
96            color,
97            MathFunction::Exponential { start: speed, target: 0.0, rate: 2.0 },
98            0.8 + (i % 25) as f32 * 0.04,
99            Vec3::new(angle.cos() * speed, angle.sin() * speed * 0.6, 0.0),
100        ));
101    }
102}
103
104fn emit_level_up(pool: &mut ParticlePool, origin: Vec3) {
105    const CHARS: &[char] = &['★', '✦', '+', '·', '↑', '▲'];
106    let gold = Vec4::new(1.0, 0.84, 0.0, 1.0);
107    for i in 0..30usize {
108        let spread = (i as f32 - 15.0) * 0.1;
109        let vy = -(0.5 + (i % 5) as f32 * 0.1);
110        pool.spawn(make_particle(
111            origin,
112            CHARS[i % CHARS.len()],
113            gold,
114            MathFunction::Breathing { rate: 2.0, depth: 0.3 },
115            0.6 + i as f32 * 0.02,
116            Vec3::new(spread * 0.3, vy, 0.0),
117        ));
118    }
119}
120
121fn emit_crit_burst(pool: &mut ParticlePool, origin: Vec3) {
122    let gold = Vec4::new(1.0, 0.84, 0.12, 1.0);
123    for i in 0..16usize {
124        let angle = i as f32 * TAU / 16.0;
125        let speed = 0.2 + (i % 4) as f32 * 0.07;
126        pool.spawn(make_particle(
127            origin, '✦', gold,
128            MathFunction::Exponential { start: speed, target: 0.0, rate: 4.0 },
129            0.4,
130            Vec3::new(angle.cos() * speed, angle.sin() * speed, 0.0),
131        ));
132    }
133}
134
135fn emit_hit_sparks(pool: &mut ParticlePool, origin: Vec3, color: Vec4, count: u8) {
136    let n = count.max(4) as usize;
137    for i in 0..n {
138        let angle = i as f32 * TAU / n as f32;
139        let speed = 0.15 + (i % 3) as f32 * 0.08;
140        pool.spawn(make_particle(
141            origin, '·', color,
142            MathFunction::Exponential { start: speed, target: 0.0, rate: 5.0 },
143            0.25,
144            Vec3::new(angle.cos() * speed, angle.sin() * speed, 0.0),
145        ));
146    }
147}
148
149fn emit_loot_sparkle(pool: &mut ParticlePool, origin: Vec3, color: Vec4) {
150    const CHARS: &[char] = &['✦', '·', '*', '+'];
151    for i in 0..12usize {
152        let angle = i as f32 * TAU / 12.0;
153        let r = 1.5 + (i % 3) as f32 * 0.4;
154        pool.spawn(make_particle(
155            origin + Vec3::new(angle.cos() * r, angle.sin() * r, 0.0),
156            CHARS[i % CHARS.len()],
157            color,
158            MathFunction::Sine { amplitude: 0.05, frequency: 0.5, phase: i as f32 },
159            2.5,
160            Vec3::ZERO,
161        ));
162    }
163}
164
165fn emit_stun_orbit(pool: &mut ParticlePool, origin: Vec3) {
166    let gold = Vec4::new(1.0, 0.84, 0.0, 1.0);
167    for i in 0..4usize {
168        let angle = i as f32 * TAU / 4.0;
169        pool.spawn(make_particle(
170            origin + Vec3::new(angle.cos() * 2.5, angle.sin() * 1.5, 0.0),
171            '★',
172            gold,
173            MathFunction::Orbit { center: origin, radius: 2.5, speed: 1.5, eccentricity: 0.3 },
174            1.5,
175            Vec3::ZERO,
176        ));
177    }
178}
179
180fn emit_gravitational_collapse(
181    pool: &mut ParticlePool, origin: Vec3, color: Vec4, attractor: AttractorType,
182) {
183    // Phase 1: orbit inward (Lorenz), Phase 2: scatter outward
184    for i in 0..40usize {
185        let angle = i as f32 * TAU / 40.0;
186        let r = 8.0 + (i % 8) as f32;
187        let start = origin + Vec3::new(angle.cos() * r, angle.sin() * r * 0.6, 0.0);
188        pool.spawn(MathParticle {
189            glyph: Glyph {
190                character: ['·', '✦', '*', '#'][i % 4],
191                position: start,
192                color,
193                emission: 1.5,
194                layer: RenderLayer::Particle,
195                blend_mode: BlendMode::Additive,
196                mass: 0.1,
197                lifetime: 1.2,
198                ..Default::default()
199            },
200            behavior: MathFunction::StrangeAttractor {
201                attractor_type: attractor,
202                scale: 0.1,
203                strength: 0.5,
204            },
205            origin: start,
206            age: 0.0,
207            lifetime: 1.2,
208            trail: true,
209            trail_length: 4,
210            trail_decay: 0.6,
211            interaction: ParticleInteraction::None,
212            ..Default::default()
213        });
214    }
215}
216
217fn emit_spell_stream(pool: &mut ParticlePool, origin: Vec3, color: Vec4) {
218    // Flock of particles self-organize toward the target
219    for i in 0..30usize {
220        let spread = (i as f32 - 15.0) * 0.3;
221        let start = origin + Vec3::new(spread, spread * 0.5, 0.0);
222        pool.spawn(MathParticle {
223            glyph: Glyph {
224                character: ['·', '*', '✦'][i % 3],
225                position: start,
226                color,
227                emission: 0.8,
228                layer: RenderLayer::Particle,
229                blend_mode: BlendMode::Additive,
230                mass: 0.05,
231                lifetime: 0.8,
232                ..Default::default()
233            },
234            behavior: MathFunction::CriticallyDamped { target: 0.5, speed: 4.0 },
235            origin: start,
236            age: 0.0,
237            lifetime: 0.8,
238            trail: false,
239            trail_length: 0,
240            trail_decay: 0.5,
241            interaction: ParticleInteraction::Flock {
242                alignment: 0.8, cohesion: 0.5, separation: 0.3, radius: 2.0,
243            },
244            ..Default::default()
245        });
246    }
247}
248
249fn emit_heal_spiral(pool: &mut ParticlePool, origin: Vec3) {
250    let green = Vec4::new(0.2, 0.95, 0.4, 1.0);
251    for i in 0..20usize {
252        pool.spawn(MathParticle {
253            glyph: Glyph {
254                character: ['·', '+', '✦'][i % 3],
255                position: origin + Vec3::new(0.0, -(i as f32 * 0.1), 0.0),
256                color: green,
257                emission: 0.5,
258                layer: RenderLayer::Particle,
259                blend_mode: BlendMode::Additive,
260                mass: 0.05,
261                lifetime: 1.5,
262                ..Default::default()
263            },
264            behavior: MathFunction::GoldenSpiral {
265                center: origin, scale: 0.3, speed: 2.0,
266            },
267            origin,
268            age: i as f32 * 0.05,
269            lifetime: 1.5,
270            trail: true,
271            trail_length: 6,
272            trail_decay: 0.4,
273            interaction: ParticleInteraction::Chain(1.5),
274            ..Default::default()
275        });
276    }
277}
278
279fn emit_entropy_cascade(pool: &mut ParticlePool, _origin: Vec3) {
280    // Spread 200 particles across the screen
281    for i in 0..200usize {
282        let x = ((i * 73 + 17) % 160) as f32 - 80.0;
283        let y = ((i * 37 + 11) % 80) as f32 - 40.0;
284        let pos = Vec3::new(x, y, 0.0);
285        let hue = (i as f32 / 200.0) * 360.0;
286        let (r, g, b) = crate::math::hsv_to_rgb(hue, 0.9, 0.9);
287        let color = Vec4::new(r, g, b, 1.0);
288        pool.spawn(make_particle(
289            pos, ['·', '*', '#', '∞'][i % 4], color,
290            MathFunction::LogisticMap { r: 3.8, x0: 0.2 + (i as f32 % 8.0) * 0.1 },
291            2.0 + (i % 10) as f32 * 0.1,
292            Vec3::ZERO,
293        ));
294    }
295}
296
297fn emit_status_ambient(pool: &mut ParticlePool, origin: Vec3, mask: u8) {
298    if mask & 1 != 0 { // Burn: orange sparks
299        let col = Vec4::new(1.0, 0.43, 0.08, 1.0);
300        pool.spawn(make_particle(origin, '·', col,
301            MathFunction::Exponential { start: 0.12, target: 0.0, rate: 3.0 }, 0.3,
302            Vec3::new(0.0, -0.12, 0.0)));
303    }
304    if mask & 2 != 0 { // Freeze: blue flakes
305        let col = Vec4::new(0.31, 0.63, 1.0, 1.0);
306        pool.spawn(make_particle(origin + Vec3::new(0.0, -2.0, 0.0), '❄', col,
307            MathFunction::Sine { amplitude: 0.02, frequency: 0.5, phase: 0.0 }, 0.35,
308            Vec3::new(0.0, 0.07, 0.0)));
309    }
310    if mask & 4 != 0 { // Poison: green bubbles
311        let col = Vec4::new(0.16, 0.82, 0.27, 1.0);
312        pool.spawn(make_particle(origin + Vec3::new(0.0, 2.0, 0.0), 'o', col,
313            MathFunction::Exponential { start: 0.09, target: 0.0, rate: 2.0 }, 0.4,
314            Vec3::new(0.0, -0.09, 0.0)));
315    }
316    if mask & 8 != 0 { // Bleed: red drips
317        let col = Vec4::new(0.78, 0.08, 0.08, 1.0);
318        pool.spawn(make_particle(origin, '▪', col,
319            MathFunction::Linear { slope: 0.08, offset: 0.0 }, 0.28,
320            Vec3::new(0.0, 0.08, 0.0)));
321    }
322    if mask & 32 != 0 { // Regen: green plus
323        let col = Vec4::new(0.20, 0.94, 0.39, 1.0);
324        pool.spawn(make_particle(origin, '+', col,
325            MathFunction::Exponential { start: 0.10, target: 0.0, rate: 3.0 }, 0.33,
326            Vec3::new(0.0, -0.10, 0.0)));
327    }
328}
329
330fn emit_room_ambient(pool: &mut ParticlePool, origin: Vec3, room_type_id: u8) {
331    match room_type_id {
332        1 => { // Combat: faint red haze
333            let col = Vec4::new(0.31, 0.04, 0.04, 0.6);
334            pool.spawn(make_particle(origin, '·', col,
335                MathFunction::Exponential { start: 0.06, target: 0.0, rate: 1.5 }, 0.7,
336                Vec3::new(0.0, -0.06, 0.0)));
337        }
338        2 => { // Treasure: gold sparkles
339            let col = Vec4::new(1.0, 0.78, 0.12, 1.0);
340            pool.spawn(make_particle(origin, '✦', col,
341                MathFunction::Breathing { rate: 1.0, depth: 0.1 }, 1.2, Vec3::ZERO));
342        }
343        3 => { // Shrine: blue upward
344            let col = Vec4::new(0.24, 0.39, 1.0, 0.9);
345            pool.spawn(make_particle(origin, '·', col,
346                MathFunction::Exponential { start: 0.10, target: 0.0, rate: 2.0 }, 0.6,
347                Vec3::new(0.0, -0.10, 0.0)));
348        }
349        4 => { // Chaos Rift: glitching
350            let col = Vec4::new(0.9, 0.4, 1.0, 1.0);
351            pool.spawn(make_particle(origin, '∞', col,
352                MathFunction::LogisticMap { r: 3.9, x0: 0.5 }, 0.4, Vec3::ZERO));
353        }
354        5 => { // Boss: pulsing red/purple
355            let col = Vec4::new(0.78, 0.08, 0.08, 1.0);
356            pool.spawn(make_particle(origin, '▪', col,
357                MathFunction::HeartBeat { bpm: 60.0, intensity: 0.15 }, 0.8,
358                Vec3::new(0.0, -0.08, 0.0)));
359        }
360        _ => {}
361    }
362}
363
364fn emit_boss_entrance(pool: &mut ParticlePool, origin: Vec3, boss_id: u8) {
365    match boss_id {
366        1 => { // Mirror: symmetric split
367            let col = Vec4::new(0.78, 0.78, 1.0, 1.0);
368            for side in [-1.0f32, 1.0] {
369                for i in 0..8usize {
370                    let angle = i as f32 * TAU / 8.0;
371                    pool.spawn(make_particle(
372                        origin + Vec3::new(side * 20.0, 0.0, 0.0), '◈', col,
373                        MathFunction::Exponential { start: 0.2, target: 0.0, rate: 2.5 }, 0.4,
374                        Vec3::new(side * 0.2, angle.sin() * 0.1, 0.0)));
375                }
376            }
377        }
378        3 => { // Fibonacci Hydra: golden spiral
379            let col = Vec4::new(1.0, 0.78, 0.12, 1.0);
380            for i in 0..30usize {
381                let angle = i as f32 * 2.399; // golden angle
382                let r = i as f32 * 0.6;
383                pool.spawn(make_particle(
384                    origin + Vec3::new(angle.cos() * r * 0.3, angle.sin() * r * 0.2, 0.0),
385                    ['·', '✦', '*'][i % 3], col,
386                    MathFunction::GoldenSpiral { center: origin, scale: 0.1, speed: 1.5 },
387                    0.5, Vec3::ZERO));
388            }
389        }
390        12 => { // Algorithm Reborn: ring explosion
391            for i in 0..60usize {
392                let angle = i as f32 * TAU / 60.0;
393                let hue = (i as f32 / 60.0) * 360.0;
394                let (r, g, b) = crate::math::hsv_to_rgb(hue, 1.0, 1.0);
395                let col = Vec4::new(r, g, b, 1.0);
396                let speed = 0.4 + (i % 5) as f32 * 0.08;
397                pool.spawn(make_particle(origin, ['*','#','@','!'][i%4], col,
398                    MathFunction::Exponential { start: speed, target: 0.0, rate: 1.5 }, 0.7,
399                    Vec3::new(angle.cos() * speed, angle.sin() * speed * 0.5, 0.0)));
400            }
401        }
402        _ => { // Generic boss entrance
403            let col = Vec4::new(0.86, 0.16, 0.16, 1.0);
404            for i in 0..20usize {
405                let angle = i as f32 * TAU / 20.0;
406                let speed = 0.25 + (i % 4) as f32 * 0.07;
407                pool.spawn(make_particle(origin, '☠', col,
408                    MathFunction::Exponential { start: speed, target: 0.0, rate: 2.0 }, 0.5,
409                    Vec3::new(angle.cos() * speed, angle.sin() * speed * 0.6, 0.0)));
410            }
411        }
412    }
413}
414
415// ── Helpers ───────────────────────────────────────────────────────────────────
416
417fn make_particle(
418    origin: Vec3,
419    ch: char,
420    color: Vec4,
421    behavior: MathFunction,
422    lifetime: f32,
423    initial_velocity: Vec3,
424) -> MathParticle {
425    MathParticle {
426        glyph: Glyph {
427            character: ch,
428            position: origin,
429            color,
430            emission: 0.5,
431            layer: RenderLayer::Particle,
432            blend_mode: BlendMode::Additive,
433            mass: 0.1,
434            velocity: initial_velocity,
435            lifetime,
436            ..Default::default()
437        },
438        behavior,
439        origin,
440        age: 0.0,
441        lifetime,
442        trail: false,
443        trail_length: 0,
444        trail_decay: 0.5,
445        interaction: ParticleInteraction::None,
446        ..Default::default()
447    }
448}