1use 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 let _ = count;
82 }
83 }
84}
85
86fn 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 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 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 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 { 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 { 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 { 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 { 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 { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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; 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 => { 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 _ => { 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
415fn 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}