veloren_voxygen/scene/
particle.rs

1use super::{FigureMgr, SceneData, Terrain, terrain::BlocksOfInterest};
2use crate::{
3    ecs::comp::Interpolated,
4    mesh::{greedy::GreedyMesh, segment::generate_mesh_base_vol_particle},
5    render::{
6        Instances, Light, Model, ParticleDrawer, ParticleInstance, ParticleVertex, Renderer,
7        pipelines::particle::ParticleMode,
8    },
9    scene::terrain::FireplaceType,
10};
11use common::{
12    assets::{AssetExt, DotVoxAsset},
13    comp::{
14        self, Beam, Body, CharacterActivity, CharacterState, Fluid, Inventory, Ori, PhysicsState,
15        Pos, Scale, Shockwave, Vel,
16        ability::Dodgeable,
17        aura, beam, biped_large, body, buff,
18        item::{ItemDefinitionId, Reagent},
19        object, shockwave,
20    },
21    figure::Segment,
22    outcome::Outcome,
23    resources::{DeltaTime, Time},
24    spiral::Spiral2d,
25    states::{self, utils::StageSection},
26    terrain::{Block, SpriteKind, TerrainChunk, TerrainGrid},
27    uid::IdMaps,
28    vol::{ReadVol, RectRasterableVol, SizedVol},
29};
30use common_base::prof_span;
31use hashbrown::HashMap;
32use rand::prelude::*;
33use specs::{Entity, Join, LendJoin, WorldExt};
34use std::{
35    f32::consts::{PI, TAU},
36    time::Duration,
37};
38use vek::*;
39
40pub struct ParticleMgr {
41    /// keep track of lifespans
42    particles: Vec<Particle>,
43
44    /// keep track of timings
45    scheduler: HeartbeatScheduler,
46
47    /// GPU Instance Buffer
48    instances: Instances<ParticleInstance>,
49
50    /// GPU Vertex Buffers
51    model_cache: HashMap<&'static str, Model<ParticleVertex>>,
52}
53
54impl ParticleMgr {
55    pub fn new(renderer: &mut Renderer) -> Self {
56        Self {
57            particles: Vec::new(),
58            scheduler: HeartbeatScheduler::new(),
59            instances: default_instances(renderer),
60            model_cache: default_cache(renderer),
61        }
62    }
63
64    pub fn handle_outcome(
65        &mut self,
66        outcome: &Outcome,
67        scene_data: &SceneData,
68        figure_mgr: &FigureMgr,
69    ) {
70        prof_span!("ParticleMgr::handle_outcome");
71        let time = scene_data.state.get_time();
72        let mut rng = thread_rng();
73
74        match outcome {
75            Outcome::Lightning { pos } => {
76                self.particles.resize_with(self.particles.len() + 800, || {
77                    Particle::new_directed(
78                        Duration::from_secs_f32(rng.gen_range(0.5..1.0)),
79                        time,
80                        ParticleMode::Lightning,
81                        *pos + Vec3::new(0.0, 0.0, rng.gen_range(0.0..600.0)),
82                        *pos,
83                    )
84                });
85            },
86            Outcome::SpriteDelete { pos, sprite } => match sprite {
87                SpriteKind::SeaUrchin => {
88                    let pos = pos.map(|e| e as f32 + 0.5);
89                    self.particles.resize_with(self.particles.len() + 10, || {
90                        Particle::new_directed(
91                            Duration::from_secs_f32(rng.gen_range(0.1..0.5)),
92                            time,
93                            ParticleMode::Steam,
94                            pos + Vec3::new(0.0, 0.0, rng.gen_range(0.0..1.5)),
95                            pos,
96                        )
97                    });
98                },
99                SpriteKind::EnsnaringVines => {},
100                _ => {},
101            },
102            Outcome::Explosion {
103                pos,
104                power,
105                radius,
106                is_attack,
107                reagent,
108            } => {
109                if *is_attack {
110                    match reagent {
111                        Some(Reagent::Green) => {
112                            self.particles.resize_with(
113                                self.particles.len() + (60.0 * power.abs()) as usize,
114                                || {
115                                    Particle::new_directed(
116                                        Duration::from_secs_f32(rng.gen_range(0.2..3.0)),
117                                        time,
118                                        ParticleMode::EnergyNature,
119                                        *pos,
120                                        *pos + Vec3::<f32>::zero()
121                                            .map(|_| rng.gen_range(-1.0..1.0))
122                                            .normalized()
123                                            * rng.gen_range(1.0..*radius),
124                                    )
125                                },
126                            );
127                        },
128                        Some(Reagent::Red) => {
129                            self.particles.resize_with(
130                                self.particles.len() + (75.0 * power.abs()) as usize,
131                                || {
132                                    Particle::new_directed(
133                                        Duration::from_millis(500),
134                                        time,
135                                        ParticleMode::Explosion,
136                                        *pos,
137                                        *pos + Vec3::<f32>::zero()
138                                            .map(|_| rng.gen_range(-1.0..1.0))
139                                            .normalized()
140                                            * *radius,
141                                    )
142                                },
143                            );
144                        },
145                        Some(Reagent::White) => {
146                            self.particles.resize_with(
147                                self.particles.len() + (75.0 * power.abs()) as usize,
148                                || {
149                                    Particle::new_directed(
150                                        Duration::from_millis(500),
151                                        time,
152                                        ParticleMode::Ice,
153                                        *pos,
154                                        *pos + Vec3::<f32>::zero()
155                                            .map(|_| rng.gen_range(-1.0..1.0))
156                                            .normalized()
157                                            * *radius,
158                                    )
159                                },
160                            );
161                        },
162                        Some(Reagent::Purple) => {
163                            self.particles.resize_with(
164                                self.particles.len() + (75.0 * power.abs()) as usize,
165                                || {
166                                    Particle::new_directed(
167                                        Duration::from_millis(500),
168                                        time,
169                                        ParticleMode::CultistFlame,
170                                        *pos,
171                                        *pos + Vec3::<f32>::zero()
172                                            .map(|_| rng.gen_range(-1.0..1.0))
173                                            .normalized()
174                                            * *radius,
175                                    )
176                                },
177                            );
178                        },
179                        Some(Reagent::FireRain) => {
180                            self.particles.resize_with(
181                                self.particles.len() + (5.0 * power.abs()) as usize,
182                                || {
183                                    Particle::new_directed(
184                                        Duration::from_millis(300),
185                                        time,
186                                        ParticleMode::Explosion,
187                                        *pos,
188                                        *pos + Vec3::<f32>::zero()
189                                            .map(|_| rng.gen_range(-1.0..1.0))
190                                            .normalized()
191                                            * *radius,
192                                    )
193                                },
194                            );
195                        },
196                        Some(Reagent::FireGigas) => {
197                            self.particles.resize_with(
198                                self.particles.len() + (4.0 * radius.powi(2)) as usize,
199                                || {
200                                    Particle::new_directed(
201                                        Duration::from_millis(500),
202                                        time,
203                                        ParticleMode::FireGigasExplosion,
204                                        *pos,
205                                        *pos + Vec3::<f32>::zero()
206                                            .map(|_| rng.gen_range(-1.0..1.0))
207                                            .normalized()
208                                            * *radius,
209                                    )
210                                },
211                            );
212                        },
213                        _ => {},
214                    }
215                } else {
216                    self.particles.resize_with(
217                        self.particles.len() + if reagent.is_some() { 300 } else { 150 },
218                        || {
219                            Particle::new(
220                                Duration::from_millis(if reagent.is_some() { 1000 } else { 250 }),
221                                time,
222                                match reagent {
223                                    Some(Reagent::Blue) => ParticleMode::FireworkBlue,
224                                    Some(Reagent::Green) => ParticleMode::FireworkGreen,
225                                    Some(Reagent::Purple) => ParticleMode::FireworkPurple,
226                                    Some(Reagent::Red) => ParticleMode::FireworkRed,
227                                    Some(Reagent::White) => ParticleMode::FireworkWhite,
228                                    Some(Reagent::Yellow) => ParticleMode::FireworkYellow,
229                                    Some(Reagent::FireRain) => ParticleMode::FireworkYellow,
230                                    Some(Reagent::FireGigas) => ParticleMode::FireGigasExplosion,
231                                    None => ParticleMode::Shrapnel,
232                                },
233                                *pos,
234                            )
235                        },
236                    );
237
238                    self.particles.resize_with(
239                        self.particles.len() + if reagent.is_some() { 100 } else { 200 },
240                        || {
241                            Particle::new(
242                                Duration::from_secs(4),
243                                time,
244                                ParticleMode::CampfireSmoke,
245                                *pos + Vec3::<f32>::zero()
246                                    .map(|_| rng.gen_range(-1.0..1.0))
247                                    .normalized()
248                                    * *radius,
249                            )
250                        },
251                    );
252                }
253            },
254            Outcome::BreakBlock { pos, .. } => {
255                // TODO: Use color field when particle colors are a thing
256                self.particles.resize_with(self.particles.len() + 30, || {
257                    Particle::new(
258                        Duration::from_millis(200),
259                        time,
260                        ParticleMode::Shrapnel,
261                        pos.map(|e| e as f32 + 0.5),
262                    )
263                });
264            },
265            Outcome::DamagedBlock {
266                pos, stage_changed, ..
267            } => {
268                self.particles.resize_with(
269                    self.particles.len() + if *stage_changed { 30 } else { 10 },
270                    || {
271                        Particle::new(
272                            Duration::from_millis(if *stage_changed { 200 } else { 100 }),
273                            time,
274                            ParticleMode::Shrapnel,
275                            pos.map(|e| e as f32 + 0.5),
276                        )
277                    },
278                );
279            },
280            Outcome::SpriteUnlocked { .. } => {},
281            Outcome::FailedSpriteUnlock { pos } => {
282                // TODO: Use color field when particle colors are a thing
283                self.particles.resize_with(self.particles.len() + 10, || {
284                    Particle::new(
285                        Duration::from_millis(50),
286                        time,
287                        ParticleMode::Shrapnel,
288                        pos.map(|e| e as f32 + 0.5),
289                    )
290                });
291            },
292            Outcome::SummonedCreature { pos, body } => match body {
293                Body::BipedSmall(b) if matches!(b.species, body::biped_small::Species::Husk) => {
294                    self.particles.resize_with(
295                        self.particles.len()
296                            + 2 * usize::from(self.scheduler.heartbeats(Duration::from_millis(1))),
297                        || {
298                            let start_pos = pos + Vec3::unit_z() * body.height() / 2.0;
299                            let end_pos = pos
300                                + Vec3::new(
301                                    2.0 * rng.gen::<f32>() - 1.0,
302                                    2.0 * rng.gen::<f32>() - 1.0,
303                                    0.0,
304                                )
305                                .normalized()
306                                    * (body.max_radius() + 4.0)
307                                + Vec3::unit_z() * (body.height() + 2.0) * rng.gen::<f32>();
308
309                            Particle::new_directed(
310                                Duration::from_secs_f32(0.5),
311                                time,
312                                ParticleMode::CultistFlame,
313                                start_pos,
314                                end_pos,
315                            )
316                        },
317                    );
318                },
319                Body::BipedSmall(b) if matches!(b.species, body::biped_small::Species::Boreal) => {
320                    self.particles.resize_with(
321                        self.particles.len()
322                            + 2 * usize::from(self.scheduler.heartbeats(Duration::from_millis(1))),
323                        || {
324                            let start_pos = pos + Vec3::unit_z() * body.height() / 2.0;
325                            let end_pos = pos
326                                + Vec3::new(
327                                    2.0 * rng.gen::<f32>() - 1.0,
328                                    2.0 * rng.gen::<f32>() - 1.0,
329                                    0.0,
330                                )
331                                .normalized()
332                                    * (body.max_radius() + 4.0)
333                                + Vec3::unit_z() * (body.height() + 20.0) * rng.gen::<f32>();
334
335                            Particle::new_directed(
336                                Duration::from_secs_f32(0.5),
337                                time,
338                                ParticleMode::GigaSnow,
339                                start_pos,
340                                end_pos,
341                            )
342                        },
343                    );
344                },
345                Body::BipedSmall(b) if matches!(b.species, body::biped_small::Species::Ashen) => {
346                    self.particles.resize_with(
347                        self.particles.len()
348                            + 2 * usize::from(self.scheduler.heartbeats(Duration::from_millis(1))),
349                        || {
350                            let start_pos = pos + Vec3::unit_z() * body.height() / 2.0;
351                            let end_pos = pos
352                                + Vec3::new(
353                                    2.0 * rng.gen::<f32>() - 1.0,
354                                    2.0 * rng.gen::<f32>() - 1.0,
355                                    0.0,
356                                )
357                                .normalized()
358                                    * (body.max_radius() + 4.0)
359                                + Vec3::unit_z() * (body.height() + 20.0) * rng.gen::<f32>();
360
361                            Particle::new_directed(
362                                Duration::from_secs_f32(0.5),
363                                time,
364                                ParticleMode::FlameThrower,
365                                start_pos,
366                                end_pos,
367                            )
368                        },
369                    );
370                },
371                _ => {},
372            },
373            Outcome::ProjectileHit { pos, target, .. } => {
374                if target.is_some() {
375                    let ecs = scene_data.state.ecs();
376                    if target
377                        .and_then(|target| ecs.read_resource::<IdMaps>().uid_entity(target))
378                        .and_then(|entity| {
379                            ecs.read_storage::<Body>()
380                                .get(entity)
381                                .map(|body| body.bleeds())
382                        })
383                        .unwrap_or(false)
384                    {
385                        self.particles.resize_with(self.particles.len() + 30, || {
386                            Particle::new(
387                                Duration::from_millis(250),
388                                time,
389                                ParticleMode::Blood,
390                                *pos,
391                            )
392                        })
393                    };
394                };
395            },
396            Outcome::Block { pos, parry, .. } => {
397                if *parry {
398                    self.particles.resize_with(self.particles.len() + 10, || {
399                        Particle::new(
400                            Duration::from_millis(200),
401                            time,
402                            ParticleMode::GunPowderSpark,
403                            *pos + Vec3::unit_z(),
404                        )
405                    });
406                }
407            },
408            Outcome::GroundSlam { pos, .. } => {
409                self.particles.resize_with(self.particles.len() + 100, || {
410                    Particle::new(
411                        Duration::from_millis(1000),
412                        time,
413                        ParticleMode::BigShrapnel,
414                        *pos,
415                    )
416                });
417            },
418            Outcome::FireLowShockwave { pos, .. } => {
419                self.particles.resize_with(self.particles.len() + 100, || {
420                    Particle::new(
421                        Duration::from_millis(1000),
422                        time,
423                        ParticleMode::FireLowShockwave,
424                        *pos,
425                    )
426                });
427            },
428            Outcome::SurpriseEgg { pos, .. } => {
429                self.particles.resize_with(self.particles.len() + 50, || {
430                    Particle::new(
431                        Duration::from_millis(1000),
432                        time,
433                        ParticleMode::SurpriseEgg,
434                        *pos,
435                    )
436                });
437            },
438            Outcome::FlashFreeze { pos, .. } => {
439                self.particles.resize_with(
440                    self.particles.len()
441                        + 2 * usize::from(self.scheduler.heartbeats(Duration::from_millis(1))),
442                    || {
443                        let start_pos = pos + Vec3::unit_z() - 1.0;
444                        let end_pos = pos
445                            + Vec3::new(
446                                4.0 * rng.gen::<f32>() - 1.0,
447                                4.0 * rng.gen::<f32>() - 1.0,
448                                0.0,
449                            )
450                            .normalized()
451                                * 1.5
452                            + Vec3::unit_z()
453                            + 5.0 * rng.gen::<f32>();
454
455                        Particle::new_directed(
456                            Duration::from_secs_f32(0.5),
457                            time,
458                            ParticleMode::GigaSnow,
459                            start_pos,
460                            end_pos,
461                        )
462                    },
463                );
464            },
465            Outcome::CyclopsCharge { pos } => {
466                self.particles.push(Particle::new_directed(
467                    Duration::from_secs_f32(rng.gen_range(0.1..0.2)),
468                    time,
469                    ParticleMode::CyclopsCharge,
470                    *pos + Vec3::new(0.0, 0.0, 5.3),
471                    *pos + Vec3::new(0.0, 0.0, 5.6 + 0.5 * rng.gen_range(0.0..0.2)),
472                ));
473            },
474            Outcome::FlamethrowerCharge { pos } | Outcome::FuseCharge { pos } => {
475                self.particles.push(Particle::new_directed(
476                    Duration::from_secs_f32(rng.gen_range(0.1..0.2)),
477                    time,
478                    ParticleMode::CampfireFire,
479                    *pos + Vec3::new(0.0, 0.0, 1.2),
480                    *pos + Vec3::new(0.0, 0.0, 1.5 + 0.5 * rng.gen_range(0.0..0.2)),
481                ));
482            },
483            Outcome::TerracottaStatueCharge { pos } => {
484                self.particles.push(Particle::new_directed(
485                    Duration::from_secs_f32(rng.gen_range(0.1..0.2)),
486                    time,
487                    ParticleMode::FireworkYellow,
488                    *pos + Vec3::new(0.0, 0.0, 4.0),
489                    *pos + Vec3::new(0.0, 0.0, 5.0 + 0.5 * rng.gen_range(0.3..0.8)),
490                ));
491            },
492            Outcome::Death { pos, .. } => {
493                self.particles.resize_with(self.particles.len() + 40, || {
494                    Particle::new(
495                        Duration::from_millis(400 + rng.gen_range(0..100)),
496                        time,
497                        ParticleMode::Death,
498                        *pos + Vec3::unit_z()
499                            + Vec3::<f32>::zero()
500                                .map(|_| rng.gen_range(-0.1..0.1))
501                                .normalized(),
502                    )
503                });
504            },
505            Outcome::GroundDig { pos, .. } => {
506                self.particles.resize_with(self.particles.len() + 12, || {
507                    Particle::new(
508                        Duration::from_millis(200),
509                        time,
510                        ParticleMode::BigShrapnel,
511                        *pos,
512                    )
513                });
514            },
515            Outcome::TeleportedByPortal { pos, .. } => {
516                self.particles.resize_with(self.particles.len() + 80, || {
517                    Particle::new_directed(
518                        Duration::from_millis(500),
519                        time,
520                        ParticleMode::CultistFlame,
521                        *pos,
522                        pos + Vec3::unit_z()
523                            + Vec3::zero()
524                                .map(|_: f32| rng.gen_range(-0.1..0.1))
525                                .normalized()
526                                * 2.0,
527                    )
528                });
529            },
530            Outcome::ClayGolemDash { pos, .. } => {
531                self.particles.resize_with(self.particles.len() + 100, || {
532                    Particle::new(
533                        Duration::from_millis(1000),
534                        time,
535                        ParticleMode::ClayShrapnel,
536                        *pos,
537                    )
538                });
539            },
540            Outcome::HeadLost { uid, head } => {
541                if let Some(entity) = scene_data
542                    .state
543                    .ecs()
544                    .read_resource::<IdMaps>()
545                    .uid_entity(*uid)
546                {
547                    if let Some(pos) = scene_data.state.read_component_copied::<Pos>(entity) {
548                        let heads = figure_mgr.get_heads(scene_data, entity);
549                        let head_pos = pos.0 + heads.get(*head).copied().unwrap_or_default();
550
551                        self.particles.resize_with(self.particles.len() + 40, || {
552                            Particle::new(
553                                Duration::from_millis(1000),
554                                time,
555                                ParticleMode::Death,
556                                head_pos
557                                    + Vec3::<f32>::zero()
558                                        .map(|_| rng.gen_range(-0.1..0.1))
559                                        .normalized(),
560                            )
561                        });
562                    }
563                };
564            },
565            Outcome::Splash {
566                vel,
567                pos,
568                mass,
569                kind,
570            } => {
571                let mode = match kind {
572                    comp::fluid_dynamics::LiquidKind::Water => ParticleMode::WaterFoam,
573                    comp::fluid_dynamics::LiquidKind::Lava => ParticleMode::CampfireFire,
574                };
575                let magnitude = (-vel.z).max(0.0);
576                let energy = mass * magnitude;
577                if energy > 0.0 {
578                    let count = ((0.6 * energy.sqrt()).ceil() as usize).min(500);
579                    let mut i = 0;
580                    let r = 0.5 / count as f32;
581                    self.particles
582                        .resize_with(self.particles.len() + count, || {
583                            let t = i as f32 / count as f32 + rng.gen_range(-r..=r);
584                            i += 1;
585                            let angle = t * TAU;
586                            let s = angle.sin();
587                            let c = angle.cos();
588                            let energy = energy
589                                * f32::abs(rng.gen_range(0.0..1.0) + rng.gen_range(0.0..1.0) - 0.5);
590
591                            let axis = -Vec3::unit_z();
592                            let plane = Vec3::new(c, s, 0.0);
593
594                            let pos = *pos + plane * rng.gen_range(0.0..0.5);
595
596                            let energy = energy.sqrt() * 0.5;
597
598                            let dir = plane * (1.0 + energy) - axis * energy;
599
600                            Particle::new_directed(
601                                Duration::from_millis(4000),
602                                time,
603                                mode,
604                                pos,
605                                pos + dir,
606                            )
607                        });
608                }
609            },
610            Outcome::Transformation { pos } => {
611                self.particles.resize_with(self.particles.len() + 100, || {
612                    Particle::new(
613                        Duration::from_millis(1400),
614                        time,
615                        ParticleMode::Transformation,
616                        *pos,
617                    )
618                });
619            },
620            Outcome::FirePillarIndicator { pos, radius } => {
621                self.particles.resize_with(
622                    self.particles.len() + radius.powi(2) as usize / 2,
623                    || {
624                        Particle::new_directed(
625                            Duration::from_millis(500),
626                            time,
627                            ParticleMode::FirePillarIndicator,
628                            *pos + 0.2 * Vec3::<f32>::unit_z(),
629                            // unit_x choosen arbitrarily, particle shader only uses the distance
630                            // between pos1 and pos2, not the actual position
631                            *pos + 0.2 * Vec3::<f32>::unit_z() + *radius * Vec3::unit_x(),
632                        )
633                    },
634                );
635            },
636            Outcome::ProjectileShot { .. }
637            | Outcome::Beam { .. }
638            | Outcome::ExpChange { .. }
639            | Outcome::SkillPointGain { .. }
640            | Outcome::ComboChange { .. }
641            | Outcome::HealthChange { .. }
642            | Outcome::PoiseChange { .. }
643            | Outcome::Utterance { .. }
644            | Outcome::IceSpikes { .. }
645            | Outcome::IceCrack { .. }
646            | Outcome::Glider { .. }
647            | Outcome::Whoosh { .. }
648            | Outcome::Swoosh { .. }
649            | Outcome::Slash { .. }
650            | Outcome::Bleep { .. }
651            | Outcome::Charge { .. }
652            | Outcome::Steam { .. }
653            | Outcome::FireShockwave { .. }
654            | Outcome::PortalActivated { .. }
655            | Outcome::FromTheAshes { .. }
656            | Outcome::LaserBeam { .. } => {},
657        }
658    }
659
660    pub fn maintain(
661        &mut self,
662        renderer: &mut Renderer,
663        scene_data: &SceneData,
664        terrain: &Terrain<TerrainChunk>,
665        figure_mgr: &FigureMgr,
666        lights: &mut Vec<Light>,
667    ) {
668        prof_span!("ParticleMgr::maintain");
669        if scene_data.particles_enabled {
670            // update timings
671            self.scheduler.maintain(scene_data.state.get_time());
672
673            // remove dead Particle
674            self.particles
675                .retain(|p| p.alive_until > scene_data.state.get_time());
676
677            // add new Particle
678            self.maintain_armor_particles(scene_data, figure_mgr);
679            self.maintain_body_particles(scene_data);
680            self.maintain_char_state_particles(scene_data, figure_mgr);
681            self.maintain_beam_particles(scene_data, lights);
682            self.maintain_block_particles(scene_data, terrain, figure_mgr);
683            self.maintain_shockwave_particles(scene_data);
684            self.maintain_aura_particles(scene_data);
685            self.maintain_buff_particles(scene_data);
686
687            self.upload_particles(renderer);
688        } else {
689            // remove all particle lifespans
690            if !self.particles.is_empty() {
691                self.particles.clear();
692                self.upload_particles(renderer);
693            }
694
695            // remove all timings
696            self.scheduler.clear();
697        }
698    }
699
700    fn maintain_armor_particles(&mut self, scene_data: &SceneData, figure_mgr: &FigureMgr) {
701        prof_span!("ParticleMgr::maintain_armor_particles");
702        let ecs = scene_data.state.ecs();
703
704        for (entity, interpolated, inv) in (
705            &ecs.entities(),
706            &ecs.read_storage::<Interpolated>(),
707            &ecs.read_storage::<Inventory>(),
708        )
709            .join()
710        {
711            for item in inv.equipped_items() {
712                if let ItemDefinitionId::Simple(str) = item.item_definition_id() {
713                    if &*str == "common.items.armor.misc.head.pipe" {
714                        self.maintain_pipe_particles(
715                            scene_data,
716                            figure_mgr,
717                            entity,
718                            interpolated.pos,
719                        )
720                    }
721                }
722            }
723        }
724    }
725
726    fn maintain_pipe_particles(
727        &mut self,
728        scene_data: &SceneData,
729        figure_mgr: &FigureMgr,
730        entity: Entity,
731        pos: Vec3<f32>,
732    ) {
733        prof_span!("ParticleMgr::maintain_pipe_particles");
734        let Some((species, body_type)) = scene_data
735            .state
736            .ecs()
737            .read_storage::<Body>()
738            .get(entity)
739            .and_then(|body| {
740                if let Body::Humanoid(body) = body {
741                    Some((body.species, body.body_type))
742                } else {
743                    None
744                }
745            })
746        else {
747            return;
748        };
749        let Some(skeleton) = figure_mgr
750            .states
751            .character_states
752            .get(&entity)
753            .map(|state| &state.computed_skeleton)
754        else {
755            return;
756        };
757        let time = scene_data.state.get_time();
758
759        // TODO: compute offsets instead of hardcoding
760        use body::humanoid::{BodyType::*, Species::*};
761        let pipe_offset = match (species, body_type) {
762            (Orc, Male) => Vec3::new(5.5, 10.5, 0.0),
763            (Orc, Female) => Vec3::new(4.5, 10.0, -2.5),
764            (Human, Male) => Vec3::new(4.5, 12.0, -3.0),
765            (Human, Female) => Vec3::new(4.5, 11.5, -3.0),
766            (Elf, Male) => Vec3::new(4.5, 12.0, -3.0),
767            (Elf, Female) => Vec3::new(4.5, 9.5, -3.0),
768            (Dwarf, Male) => Vec3::new(4.5, 11.0, -4.0),
769            (Dwarf, Female) => Vec3::new(4.5, 11.0, -3.0),
770            (Draugr, Male) => Vec3::new(4.5, 9.5, -0.75),
771            (Draugr, Female) => Vec3::new(4.5, 9.5, -2.0),
772            (Danari, Male) => Vec3::new(4.5, 10.5, -1.25),
773            (Danari, Female) => Vec3::new(4.5, 10.5, -1.25),
774        };
775
776        for _ in 0..self.scheduler.heartbeats(Duration::from_secs(6)) {
777            self.particles.resize_with(self.particles.len() + 10, || {
778                Particle::new(
779                    Duration::from_millis(1500),
780                    time,
781                    ParticleMode::PipeSmoke,
782                    pos + skeleton.head.mul_point(pipe_offset),
783                )
784            });
785        }
786    }
787
788    fn maintain_body_particles(&mut self, scene_data: &SceneData) {
789        prof_span!("ParticleMgr::maintain_body_particles");
790        let ecs = scene_data.state.ecs();
791        for (body, interpolated, vel) in (
792            &ecs.read_storage::<Body>(),
793            &ecs.read_storage::<Interpolated>(),
794            ecs.read_storage::<Vel>().maybe(),
795        )
796            .join()
797        {
798            match body {
799                Body::Object(object::Body::CampfireLit) => {
800                    self.maintain_campfirelit_particles(scene_data, interpolated.pos, vel)
801                },
802                Body::Object(object::Body::BarrelOrgan) => {
803                    self.maintain_barrel_organ_particles(scene_data, interpolated.pos, vel)
804                },
805                Body::Object(object::Body::BoltFire) => {
806                    self.maintain_boltfire_particles(scene_data, interpolated.pos, vel)
807                },
808                Body::Object(object::Body::BoltFireBig) => {
809                    self.maintain_boltfirebig_particles(scene_data, interpolated.pos, vel)
810                },
811                Body::Object(object::Body::FireRainDrop) => {
812                    self.maintain_fireraindrop_particles(scene_data, interpolated.pos, vel)
813                },
814                Body::Object(object::Body::BoltNature) => {
815                    self.maintain_boltnature_particles(scene_data, interpolated.pos, vel)
816                },
817                Body::Object(object::Body::Tornado) => {
818                    self.maintain_tornado_particles(scene_data, interpolated.pos)
819                },
820                Body::Object(object::Body::FieryTornado) => {
821                    self.maintain_fiery_tornado_particles(scene_data, interpolated.pos)
822                },
823                Body::Object(object::Body::Mine) => {
824                    self.maintain_mine_particles(scene_data, interpolated.pos)
825                },
826                Body::Object(
827                    object::Body::Bomb
828                    | object::Body::FireworkBlue
829                    | object::Body::FireworkGreen
830                    | object::Body::FireworkPurple
831                    | object::Body::FireworkRed
832                    | object::Body::FireworkWhite
833                    | object::Body::FireworkYellow
834                    | object::Body::IronPikeBomb,
835                ) => self.maintain_bomb_particles(scene_data, interpolated.pos, vel),
836                Body::Object(object::Body::PortalActive) => {
837                    self.maintain_active_portal_particles(scene_data, interpolated.pos)
838                },
839                Body::Object(object::Body::Portal) => {
840                    self.maintain_portal_particles(scene_data, interpolated.pos)
841                },
842                Body::BipedLarge(biped_large::Body {
843                    species: biped_large::Species::Gigasfire,
844                    ..
845                }) => self.maintain_fire_gigas_particles(scene_data, interpolated.pos),
846                _ => {},
847            }
848        }
849    }
850
851    fn maintain_fire_gigas_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
852        let time = scene_data.state.get_time();
853        let mut rng = rand::thread_rng();
854
855        if rng.gen_bool(0.05) {
856            self.particles.resize_with(self.particles.len() + 1, || {
857                let rand_offset = Vec3::new(
858                    rng.gen_range(-5.0..5.0),
859                    rng.gen_range(-5.0..5.0),
860                    rng.gen_range(7.0..15.0),
861                );
862
863                Particle::new(
864                    Duration::from_secs_f32(30.0),
865                    time,
866                    ParticleMode::FireGigasAsh,
867                    pos + rand_offset,
868                )
869            });
870        }
871    }
872
873    fn maintain_hydra_tail_swipe_particles(
874        &mut self,
875        scene_data: &SceneData,
876        figure_mgr: &FigureMgr,
877        entity: Entity,
878        pos: Vec3<f32>,
879        body: &Body,
880        state: &CharacterState,
881        inventory: Option<&Inventory>,
882    ) {
883        let Some(ability_id) = state
884            .ability_info()
885            .and_then(|info| info.ability.map(|a| a.ability_id(Some(state), inventory)))
886        else {
887            return;
888        };
889
890        if ability_id != Some("common.abilities.custom.hydra.tail_swipe") {
891            return;
892        }
893
894        let Some(stage_section) = state.stage_section() else {
895            return;
896        };
897
898        let particle_count = match stage_section {
899            StageSection::Charge => 1,
900            StageSection::Action => 10,
901            _ => return,
902        };
903
904        let Some(skeleton) = figure_mgr
905            .states
906            .quadruped_low_states
907            .get(&entity)
908            .map(|state| &state.computed_skeleton)
909        else {
910            return;
911        };
912        let Some(attr) = anim::quadruped_low::SkeletonAttr::try_from(body).ok() else {
913            return;
914        };
915
916        let start = (skeleton.tail_front * Vec4::unit_w()).xyz();
917        let end = (skeleton.tail_rear * Vec4::new(0.0, -attr.tail_rear_length, 0.0, 1.0)).xyz();
918
919        let start = pos + start;
920        let end = pos + end;
921
922        let time = scene_data.state.get_time();
923        let mut rng = thread_rng();
924
925        self.particles.resize_with(
926            self.particles.len()
927                + particle_count * self.scheduler.heartbeats(Duration::from_millis(33)) as usize,
928            || {
929                let t = rng.gen_range(0.0..1.0);
930                let p = start * t + end * (1.0 - t) - Vec3::new(0.0, 0.0, 0.5);
931
932                Particle::new(
933                    Duration::from_millis(500),
934                    time,
935                    ParticleMode::GroundShockwave,
936                    p,
937                )
938            },
939        );
940    }
941
942    fn maintain_campfirelit_particles(
943        &mut self,
944        scene_data: &SceneData,
945        pos: Vec3<f32>,
946        vel: Option<&Vel>,
947    ) {
948        prof_span!("ParticleMgr::maintain_campfirelit_particles");
949        let time = scene_data.state.get_time();
950        let dt = scene_data.state.get_delta_time();
951        let mut rng = thread_rng();
952
953        for _ in 0..self.scheduler.heartbeats(Duration::from_millis(50)) {
954            self.particles.push(Particle::new(
955                Duration::from_millis(250),
956                time,
957                ParticleMode::CampfireFire,
958                pos,
959            ));
960
961            self.particles.push(Particle::new(
962                Duration::from_secs(10),
963                time,
964                ParticleMode::CampfireSmoke,
965                pos.map(|e| e + thread_rng().gen_range(-0.25..0.25))
966                    + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
967            ));
968        }
969    }
970
971    fn maintain_barrel_organ_particles(
972        &mut self,
973        scene_data: &SceneData,
974        pos: Vec3<f32>,
975        vel: Option<&Vel>,
976    ) {
977        prof_span!("ParticleMgr::maintain_barrel_organ_particles");
978        let time = scene_data.state.get_time();
979        let dt = scene_data.state.get_delta_time();
980        let mut rng = thread_rng();
981
982        for _ in 0..self.scheduler.heartbeats(Duration::from_millis(20)) {
983            self.particles.push(Particle::new(
984                Duration::from_millis(250),
985                time,
986                ParticleMode::BarrelOrgan,
987                pos,
988            ));
989
990            self.particles.push(Particle::new(
991                Duration::from_secs(10),
992                time,
993                ParticleMode::BarrelOrgan,
994                pos.map(|e| e + thread_rng().gen_range(-0.25..0.25))
995                    + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
996            ));
997        }
998    }
999
1000    fn maintain_boltfire_particles(
1001        &mut self,
1002        scene_data: &SceneData,
1003        pos: Vec3<f32>,
1004        vel: Option<&Vel>,
1005    ) {
1006        prof_span!("ParticleMgr::maintain_boltfire_particles");
1007        let time = scene_data.state.get_time();
1008        let dt = scene_data.state.get_delta_time();
1009        let mut rng = thread_rng();
1010
1011        for _ in 0..self.scheduler.heartbeats(Duration::from_millis(4)) {
1012            self.particles.push(Particle::new(
1013                Duration::from_millis(500),
1014                time,
1015                ParticleMode::CampfireFire,
1016                pos,
1017            ));
1018            self.particles.push(Particle::new(
1019                Duration::from_secs(1),
1020                time,
1021                ParticleMode::CampfireSmoke,
1022                pos.map(|e| e + rng.gen_range(-0.25..0.25))
1023                    + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
1024            ));
1025        }
1026    }
1027
1028    fn maintain_boltfirebig_particles(
1029        &mut self,
1030        scene_data: &SceneData,
1031        pos: Vec3<f32>,
1032        vel: Option<&Vel>,
1033    ) {
1034        prof_span!("ParticleMgr::maintain_boltfirebig_particles");
1035        let time = scene_data.state.get_time();
1036        let dt = scene_data.state.get_delta_time();
1037        let mut rng = thread_rng();
1038
1039        // fire
1040        self.particles.resize_with(
1041            self.particles.len() + usize::from(self.scheduler.heartbeats(Duration::from_millis(2))),
1042            || {
1043                Particle::new(
1044                    Duration::from_millis(500),
1045                    time,
1046                    ParticleMode::CampfireFire,
1047                    pos.map(|e| e + rng.gen_range(-0.25..0.25))
1048                        + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
1049                )
1050            },
1051        );
1052
1053        // smoke
1054        self.particles.resize_with(
1055            self.particles.len() + usize::from(self.scheduler.heartbeats(Duration::from_millis(5))),
1056            || {
1057                Particle::new(
1058                    Duration::from_secs(2),
1059                    time,
1060                    ParticleMode::CampfireSmoke,
1061                    pos.map(|e| e + rng.gen_range(-0.25..0.25))
1062                        + vel.map_or(Vec3::zero(), |v| -v.0 * dt),
1063                )
1064            },
1065        );
1066    }
1067
1068    fn maintain_fireraindrop_particles(
1069        &mut self,
1070        scene_data: &SceneData,
1071        pos: Vec3<f32>,
1072        vel: Option<&Vel>,
1073    ) {
1074        prof_span!("ParticleMgr::maintain_fireraindrop_particles");
1075        let time = scene_data.state.get_time();
1076        let dt = scene_data.state.get_delta_time();
1077        let mut rng = thread_rng();
1078
1079        // trace
1080        self.particles.resize_with(
1081            self.particles.len()
1082                + usize::from(self.scheduler.heartbeats(Duration::from_millis(100))),
1083            || {
1084                Particle::new(
1085                    Duration::from_millis(300),
1086                    time,
1087                    ParticleMode::FieryDropletTrace,
1088                    pos.map(|e| e + rng.gen_range(-0.25..0.25))
1089                        + Vec3::new(0.0, 0.0, 0.5)
1090                        + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
1091                )
1092            },
1093        );
1094    }
1095
1096    fn maintain_boltnature_particles(
1097        &mut self,
1098        scene_data: &SceneData,
1099        pos: Vec3<f32>,
1100        vel: Option<&Vel>,
1101    ) {
1102        let time = scene_data.state.get_time();
1103        let dt = scene_data.state.get_delta_time();
1104        let mut rng = thread_rng();
1105
1106        // nature
1107        self.particles.resize_with(
1108            self.particles.len() + usize::from(self.scheduler.heartbeats(Duration::from_millis(2))),
1109            || {
1110                Particle::new(
1111                    Duration::from_millis(500),
1112                    time,
1113                    ParticleMode::CampfireSmoke,
1114                    pos.map(|e| e + rng.gen_range(-0.25..0.25))
1115                        + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
1116                )
1117            },
1118        );
1119    }
1120
1121    fn maintain_tornado_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1122        let time = scene_data.state.get_time();
1123        let mut rng = thread_rng();
1124
1125        // air particles
1126        self.particles.resize_with(
1127            self.particles.len() + usize::from(self.scheduler.heartbeats(Duration::from_millis(5))),
1128            || {
1129                Particle::new(
1130                    Duration::from_millis(1000),
1131                    time,
1132                    ParticleMode::Tornado,
1133                    pos.map(|e| e + rng.gen_range(-0.25..0.25)),
1134                )
1135            },
1136        );
1137    }
1138
1139    fn maintain_fiery_tornado_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1140        let time = scene_data.state.get_time();
1141        let mut rng = thread_rng();
1142
1143        // air particles
1144        self.particles.resize_with(
1145            self.particles.len() + usize::from(self.scheduler.heartbeats(Duration::from_millis(5))),
1146            || {
1147                Particle::new(
1148                    Duration::from_millis(1000),
1149                    time,
1150                    ParticleMode::FieryTornado,
1151                    pos.map(|e| e + rng.gen_range(-0.25..0.25)),
1152                )
1153            },
1154        );
1155    }
1156
1157    fn maintain_bomb_particles(
1158        &mut self,
1159        scene_data: &SceneData,
1160        pos: Vec3<f32>,
1161        vel: Option<&Vel>,
1162    ) {
1163        prof_span!("ParticleMgr::maintain_bomb_particles");
1164        let time = scene_data.state.get_time();
1165        let dt = scene_data.state.get_delta_time();
1166        let mut rng = thread_rng();
1167
1168        for _ in 0..self.scheduler.heartbeats(Duration::from_millis(10)) {
1169            // sparks
1170            self.particles.push(Particle::new(
1171                Duration::from_millis(1500),
1172                time,
1173                ParticleMode::GunPowderSpark,
1174                pos,
1175            ));
1176
1177            // smoke
1178            self.particles.push(Particle::new(
1179                Duration::from_secs(2),
1180                time,
1181                ParticleMode::CampfireSmoke,
1182                pos + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
1183            ));
1184        }
1185    }
1186
1187    fn maintain_active_portal_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1188        prof_span!("ParticleMgr::maintain_active_portal_particles");
1189
1190        let time = scene_data.state.get_time();
1191        let mut rng = thread_rng();
1192
1193        for _ in 0..self.scheduler.heartbeats(Duration::from_millis(5)) {
1194            let outer_pos =
1195                pos + (Vec2::unit_x().rotated_z(rng.gen_range((0.)..PI * 2.)) * 2.7).with_z(0.);
1196
1197            self.particles.push(Particle::new_directed(
1198                Duration::from_secs_f32(rng.gen_range(0.4..0.8)),
1199                time,
1200                ParticleMode::CultistFlame,
1201                outer_pos,
1202                outer_pos + Vec3::unit_z() * rng.gen_range(5.0..7.0),
1203            ));
1204        }
1205    }
1206
1207    fn maintain_portal_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1208        prof_span!("ParticleMgr::maintain_portal_particles");
1209
1210        let time = scene_data.state.get_time();
1211        let mut rng = thread_rng();
1212
1213        for _ in 0..self.scheduler.heartbeats(Duration::from_millis(150)) {
1214            let outer_pos = pos
1215                + (Vec2::unit_x().rotated_z(rng.gen_range((0.)..PI * 2.))
1216                    * rng.gen_range(1.0..2.9))
1217                .with_z(0.);
1218
1219            self.particles.push(Particle::new_directed(
1220                Duration::from_secs_f32(rng.gen_range(0.5..3.0)),
1221                time,
1222                ParticleMode::CultistFlame,
1223                outer_pos,
1224                outer_pos + Vec3::unit_z() * rng.gen_range(3.0..4.0),
1225            ));
1226        }
1227    }
1228
1229    fn maintain_mine_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1230        prof_span!("ParticleMgr::maintain_mine_particles");
1231        let time = scene_data.state.get_time();
1232
1233        for _ in 0..self.scheduler.heartbeats(Duration::from_millis(1)) {
1234            // sparks
1235            self.particles.push(Particle::new(
1236                Duration::from_millis(25),
1237                time,
1238                ParticleMode::GunPowderSpark,
1239                pos,
1240            ));
1241        }
1242    }
1243
1244    fn maintain_char_state_particles(&mut self, scene_data: &SceneData, figure_mgr: &FigureMgr) {
1245        prof_span!("ParticleMgr::maintain_char_state_particles");
1246        let state = scene_data.state;
1247        let ecs = state.ecs();
1248        let time = state.get_time();
1249        let dt = scene_data.state.get_delta_time();
1250        let mut rng = thread_rng();
1251
1252        for (
1253            entity,
1254            interpolated,
1255            vel,
1256            character_state,
1257            body,
1258            ori,
1259            character_activity,
1260            physics,
1261            inventory,
1262        ) in (
1263            &ecs.entities(),
1264            &ecs.read_storage::<Interpolated>(),
1265            ecs.read_storage::<Vel>().maybe(),
1266            &ecs.read_storage::<CharacterState>(),
1267            &ecs.read_storage::<Body>(),
1268            &ecs.read_storage::<Ori>(),
1269            &ecs.read_storage::<CharacterActivity>(),
1270            &ecs.read_storage::<PhysicsState>(),
1271            ecs.read_storage::<Inventory>().maybe(),
1272        )
1273            .join()
1274        {
1275            match character_state {
1276                CharacterState::Boost(_) => {
1277                    self.particles.resize_with(
1278                        self.particles.len()
1279                            + usize::from(self.scheduler.heartbeats(Duration::from_millis(10))),
1280                        || {
1281                            Particle::new(
1282                                Duration::from_millis(250),
1283                                time,
1284                                ParticleMode::PortalFizz,
1285                                // Output particles from broom, not from character ass
1286                                interpolated.pos
1287                                    - ori.to_horizontal().look_dir().to_vec()
1288                                    - vel.map_or(Vec3::zero(), |v| v.0 * dt * rng.gen::<f32>()),
1289                            )
1290                        },
1291                    );
1292                },
1293                CharacterState::BasicMelee(c) => {
1294                    if let Some(specifier) = c.static_data.frontend_specifier {
1295                        match specifier {
1296                            states::basic_melee::FrontendSpecifier::FlameTornado => {
1297                                if matches!(c.stage_section, StageSection::Action) {
1298                                    let time = scene_data.state.get_time();
1299                                    let mut rng = thread_rng();
1300                                    self.particles.resize_with(
1301                                        self.particles.len()
1302                                            + 10
1303                                            + usize::from(
1304                                                self.scheduler.heartbeats(Duration::from_millis(5)),
1305                                            ),
1306                                        || {
1307                                            Particle::new(
1308                                                Duration::from_millis(1000),
1309                                                time,
1310                                                ParticleMode::FlameTornado,
1311                                                interpolated
1312                                                    .pos
1313                                                    .map(|e| e + rng.gen_range(-0.25..0.25)),
1314                                            )
1315                                        },
1316                                    );
1317                                }
1318                            },
1319                            states::basic_melee::FrontendSpecifier::FireGigasWhirlwind => {
1320                                if matches!(c.stage_section, StageSection::Action) {
1321                                    let time = scene_data.state.get_time();
1322                                    let mut rng = thread_rng();
1323                                    self.particles.resize_with(
1324                                        self.particles.len()
1325                                            + 3
1326                                            + usize::from(
1327                                                self.scheduler.heartbeats(Duration::from_millis(5)),
1328                                            ),
1329                                        || {
1330                                            Particle::new(
1331                                                Duration::from_millis(600),
1332                                                time,
1333                                                ParticleMode::FireGigasWhirlwind,
1334                                                interpolated
1335                                                    .pos
1336                                                    .map(|e| e + rng.gen_range(-0.25..0.25))
1337                                                    + 3.0 * Vec3::<f32>::unit_z(),
1338                                            )
1339                                        },
1340                                    );
1341                                }
1342                            },
1343                        }
1344                    }
1345                },
1346                CharacterState::RapidMelee(c) => {
1347                    if let Some(specifier) = c.static_data.frontend_specifier {
1348                        match specifier {
1349                            states::rapid_melee::FrontendSpecifier::CultistVortex => {
1350                                if matches!(c.stage_section, StageSection::Action) {
1351                                    let range = c.static_data.melee_constructor.range;
1352                                    // Particles for vortex
1353                                    let heartbeats =
1354                                        self.scheduler.heartbeats(Duration::from_millis(3));
1355                                    self.particles.resize_with(
1356                                        self.particles.len()
1357                                            + range.powi(2) as usize * usize::from(heartbeats)
1358                                                / 150,
1359                                        || {
1360                                            let rand_dist =
1361                                                range * (1.0 - rng.gen::<f32>().powi(10));
1362                                            let init_pos = Vec3::new(
1363                                                2.0 * rng.gen::<f32>() - 1.0,
1364                                                2.0 * rng.gen::<f32>() - 1.0,
1365                                                0.0,
1366                                            )
1367                                            .normalized()
1368                                                * rand_dist
1369                                                + interpolated.pos
1370                                                + Vec3::unit_z() * 0.05;
1371                                            Particle::new_directed(
1372                                                Duration::from_millis(900),
1373                                                time,
1374                                                ParticleMode::CultistFlame,
1375                                                init_pos,
1376                                                interpolated.pos,
1377                                            )
1378                                        },
1379                                    );
1380                                    // Particles for lifesteal effect
1381                                    for (_entity_b, interpolated_b, body_b, _health_b) in (
1382                                        &ecs.entities(),
1383                                        &ecs.read_storage::<Interpolated>(),
1384                                        &ecs.read_storage::<Body>(),
1385                                        &ecs.read_storage::<comp::Health>(),
1386                                    )
1387                                        .join()
1388                                        .filter(|(e, _, _, h)| !h.is_dead && entity != *e)
1389                                    {
1390                                        if interpolated.pos.distance_squared(interpolated_b.pos)
1391                                            < range.powi(2)
1392                                        {
1393                                            let heartbeats = self
1394                                                .scheduler
1395                                                .heartbeats(Duration::from_millis(20));
1396                                            self.particles.resize_with(
1397                                                self.particles.len()
1398                                                    + range.powi(2) as usize
1399                                                        * usize::from(heartbeats)
1400                                                        / 150,
1401                                                || {
1402                                                    let start_pos = interpolated_b.pos
1403                                                        + Vec3::unit_z() * body_b.height() * 0.5
1404                                                        + Vec3::<f32>::zero()
1405                                                            .map(|_| rng.gen_range(-1.0..1.0))
1406                                                            .normalized()
1407                                                            * 1.0;
1408                                                    Particle::new_directed(
1409                                                        Duration::from_millis(900),
1410                                                        time,
1411                                                        ParticleMode::CultistFlame,
1412                                                        start_pos,
1413                                                        interpolated.pos
1414                                                            + Vec3::unit_z() * body.height() * 0.5,
1415                                                    )
1416                                                },
1417                                            );
1418                                        }
1419                                    }
1420                                }
1421                            },
1422                            states::rapid_melee::FrontendSpecifier::IceWhirlwind => {
1423                                if matches!(c.stage_section, StageSection::Action) {
1424                                    let time = scene_data.state.get_time();
1425                                    let mut rng = thread_rng();
1426                                    self.particles.resize_with(
1427                                        self.particles.len()
1428                                            + 3
1429                                            + usize::from(
1430                                                self.scheduler.heartbeats(Duration::from_millis(5)),
1431                                            ),
1432                                        || {
1433                                            Particle::new(
1434                                                Duration::from_millis(1000),
1435                                                time,
1436                                                ParticleMode::IceWhirlwind,
1437                                                interpolated
1438                                                    .pos
1439                                                    .map(|e| e + rng.gen_range(-0.25..0.25)),
1440                                            )
1441                                        },
1442                                    );
1443                                }
1444                            },
1445                        }
1446                    }
1447                },
1448                CharacterState::RepeaterRanged(repeater) => {
1449                    if let Some(specifier) = repeater.static_data.specifier {
1450                        match specifier {
1451                            states::repeater_ranged::FrontendSpecifier::FireRainPhoenix => {
1452                                // base, dark clouds
1453                                self.particles.resize_with(
1454                                    self.particles.len()
1455                                        + 2 * usize::from(
1456                                            self.scheduler.heartbeats(Duration::from_millis(25)),
1457                                        ),
1458                                    || {
1459                                        let rand_pos = {
1460                                            let theta = rng.gen::<f32>() * TAU;
1461                                            let radius = repeater
1462                                                .static_data
1463                                                .properties_of_aoe
1464                                                .map(|aoe| aoe.radius)
1465                                                .unwrap_or_default()
1466                                                * rng.gen::<f32>().sqrt();
1467                                            let x = radius * theta.sin();
1468                                            let y = radius * theta.cos();
1469                                            Vec2::new(x, y) + interpolated.pos.xy()
1470                                        };
1471                                        let pos1 = rand_pos.with_z(
1472                                            repeater
1473                                                .static_data
1474                                                .properties_of_aoe
1475                                                .map(|aoe| aoe.height)
1476                                                .unwrap_or_default()
1477                                                + interpolated.pos.z
1478                                                + 2.0 * rng.gen::<f32>(),
1479                                        );
1480                                        Particle::new_directed(
1481                                            Duration::from_secs_f32(3.0),
1482                                            time,
1483                                            ParticleMode::PhoenixCloud,
1484                                            pos1,
1485                                            pos1 + Vec3::new(7.09, 4.09, 18.09),
1486                                        )
1487                                    },
1488                                );
1489                                self.particles.resize_with(
1490                                    self.particles.len()
1491                                        + 2 * usize::from(
1492                                            self.scheduler.heartbeats(Duration::from_millis(25)),
1493                                        ),
1494                                    || {
1495                                        let rand_pos = {
1496                                            let theta = rng.gen::<f32>() * TAU;
1497                                            let radius = repeater
1498                                                .static_data
1499                                                .properties_of_aoe
1500                                                .map(|aoe| aoe.radius)
1501                                                .unwrap_or_default()
1502                                                * rng.gen::<f32>().sqrt();
1503                                            let x = radius * theta.sin();
1504                                            let y = radius * theta.cos();
1505                                            Vec2::new(x, y) + interpolated.pos.xy()
1506                                        };
1507                                        let pos1 = rand_pos.with_z(
1508                                            repeater
1509                                                .static_data
1510                                                .properties_of_aoe
1511                                                .map(|aoe| aoe.height)
1512                                                .unwrap_or_default()
1513                                                + interpolated.pos.z
1514                                                + 1.5 * rng.gen::<f32>(),
1515                                        );
1516                                        Particle::new_directed(
1517                                            Duration::from_secs_f32(2.5),
1518                                            time,
1519                                            ParticleMode::PhoenixCloud,
1520                                            pos1,
1521                                            pos1 + Vec3::new(10.025, 4.025, 17.025),
1522                                        )
1523                                    },
1524                                );
1525                            },
1526                        }
1527                    }
1528                },
1529                CharacterState::Blink(c) => {
1530                    if let Some(specifier) = c.static_data.frontend_specifier {
1531                        match specifier {
1532                            states::blink::FrontendSpecifier::CultistFlame => {
1533                                self.particles.resize_with(
1534                                    self.particles.len()
1535                                        + usize::from(
1536                                            self.scheduler.heartbeats(Duration::from_millis(10)),
1537                                        ),
1538                                    || {
1539                                        let center_pos =
1540                                            interpolated.pos + Vec3::unit_z() * body.height() / 2.0;
1541                                        let outer_pos = interpolated.pos
1542                                            + Vec3::new(
1543                                                2.0 * rng.gen::<f32>() - 1.0,
1544                                                2.0 * rng.gen::<f32>() - 1.0,
1545                                                0.0,
1546                                            )
1547                                            .normalized()
1548                                                * (body.max_radius() + 2.0)
1549                                            + Vec3::unit_z() * body.height() * rng.gen::<f32>();
1550
1551                                        let (start_pos, end_pos) =
1552                                            if matches!(c.stage_section, StageSection::Buildup) {
1553                                                (outer_pos, center_pos)
1554                                            } else {
1555                                                (center_pos, outer_pos)
1556                                            };
1557
1558                                        Particle::new_directed(
1559                                            Duration::from_secs_f32(0.5),
1560                                            time,
1561                                            ParticleMode::CultistFlame,
1562                                            start_pos,
1563                                            end_pos,
1564                                        )
1565                                    },
1566                                );
1567                            },
1568                            states::blink::FrontendSpecifier::FlameThrower => {
1569                                self.particles.resize_with(
1570                                    self.particles.len()
1571                                        + usize::from(
1572                                            self.scheduler.heartbeats(Duration::from_millis(10)),
1573                                        ),
1574                                    || {
1575                                        let center_pos =
1576                                            interpolated.pos + Vec3::unit_z() * body.height() / 2.0;
1577                                        let outer_pos = interpolated.pos
1578                                            + Vec3::new(
1579                                                2.0 * rng.gen::<f32>() - 1.0,
1580                                                2.0 * rng.gen::<f32>() - 1.0,
1581                                                0.0,
1582                                            )
1583                                            .normalized()
1584                                                * (body.max_radius() + 2.0)
1585                                            + Vec3::unit_z() * body.height() * rng.gen::<f32>();
1586
1587                                        let (start_pos, end_pos) =
1588                                            if matches!(c.stage_section, StageSection::Buildup) {
1589                                                (outer_pos, center_pos)
1590                                            } else {
1591                                                (center_pos, outer_pos)
1592                                            };
1593
1594                                        Particle::new_directed(
1595                                            Duration::from_secs_f32(0.5),
1596                                            time,
1597                                            ParticleMode::FlameThrower,
1598                                            start_pos,
1599                                            end_pos,
1600                                        )
1601                                    },
1602                                );
1603                            },
1604                        }
1605                    }
1606                },
1607                CharacterState::SelfBuff(c) => {
1608                    if let Some(specifier) = c.static_data.specifier {
1609                        match specifier {
1610                            states::self_buff::FrontendSpecifier::FromTheAshes => {
1611                                if matches!(c.stage_section, StageSection::Action) {
1612                                    let pos = interpolated.pos;
1613                                    self.particles.resize_with(
1614                                        self.particles.len()
1615                                            + 2 * usize::from(
1616                                                self.scheduler.heartbeats(Duration::from_millis(1)),
1617                                            ),
1618                                        || {
1619                                            let start_pos = pos + Vec3::unit_z() - 1.0;
1620                                            let end_pos = pos
1621                                                + Vec3::new(
1622                                                    4.0 * rng.gen::<f32>() - 1.0,
1623                                                    4.0 * rng.gen::<f32>() - 1.0,
1624                                                    0.0,
1625                                                )
1626                                                .normalized()
1627                                                    * 1.5
1628                                                + Vec3::unit_z()
1629                                                + 5.0 * rng.gen::<f32>();
1630
1631                                            Particle::new_directed(
1632                                                Duration::from_secs_f32(0.5),
1633                                                time,
1634                                                ParticleMode::FieryBurst,
1635                                                start_pos,
1636                                                end_pos,
1637                                            )
1638                                        },
1639                                    );
1640                                    self.particles.resize_with(
1641                                        self.particles.len()
1642                                            + usize::from(
1643                                                self.scheduler
1644                                                    .heartbeats(Duration::from_millis(10)),
1645                                            ),
1646                                        || {
1647                                            Particle::new(
1648                                                Duration::from_millis(650),
1649                                                time,
1650                                                ParticleMode::FieryBurstVortex,
1651                                                pos.map(|e| e + rng.gen_range(-0.25..0.25))
1652                                                    + Vec3::new(0.0, 0.0, 1.0),
1653                                            )
1654                                        },
1655                                    );
1656                                    self.particles.resize_with(
1657                                        self.particles.len()
1658                                            + usize::from(
1659                                                self.scheduler
1660                                                    .heartbeats(Duration::from_millis(40)),
1661                                            ),
1662                                        || {
1663                                            Particle::new(
1664                                                Duration::from_millis(1000),
1665                                                time,
1666                                                ParticleMode::FieryBurstSparks,
1667                                                pos.map(|e| e + rng.gen_range(-0.25..0.25)),
1668                                            )
1669                                        },
1670                                    );
1671                                    self.particles.resize_with(
1672                                        self.particles.len()
1673                                            + usize::from(
1674                                                self.scheduler
1675                                                    .heartbeats(Duration::from_millis(14)),
1676                                            ),
1677                                        || {
1678                                            let pos1 = pos.map(|e| e + rng.gen_range(-0.25..0.25));
1679                                            Particle::new_directed(
1680                                                Duration::from_millis(1000),
1681                                                time,
1682                                                ParticleMode::FieryBurstAsh,
1683                                                pos1,
1684                                                Vec3::new(
1685                                                    4.5,    // radius of rand spawn
1686                                                    20.4,   // integer part - radius of the curve part, fractional part - relative time of setting particle on fire
1687                                                    8.58)   // height of the flight
1688                                                    + pos1,
1689                                            )
1690                                        },
1691                                    );
1692                                }
1693                            },
1694                        }
1695                    }
1696                    use buff::BuffKind;
1697                    if c.static_data
1698                        .buffs
1699                        .iter()
1700                        .any(|buff_desc| matches!(buff_desc.kind, BuffKind::Frenzied))
1701                        && matches!(c.stage_section, StageSection::Action)
1702                    {
1703                        self.particles.resize_with(
1704                            self.particles.len()
1705                                + usize::from(self.scheduler.heartbeats(Duration::from_millis(5))),
1706                            || {
1707                                let start_pos = interpolated.pos
1708                                    + Vec3::new(
1709                                        body.max_radius(),
1710                                        body.max_radius(),
1711                                        body.height() / 2.0,
1712                                    )
1713                                    .map(|d| d * rng.gen_range(-1.0..1.0));
1714                                let end_pos =
1715                                    interpolated.pos + (start_pos - interpolated.pos) * 6.0;
1716                                Particle::new_directed(
1717                                    Duration::from_secs(1),
1718                                    time,
1719                                    ParticleMode::Enraged,
1720                                    start_pos,
1721                                    end_pos,
1722                                )
1723                            },
1724                        );
1725                    }
1726                },
1727                CharacterState::BasicBeam(beam) => {
1728                    let ori = *ori;
1729                    let _look_dir = *character_activity.look_dir.unwrap_or(ori.look_dir());
1730                    let dir = ori.look_dir(); //.with_z(look_dir.z);
1731                    let specifier = beam.static_data.specifier;
1732                    if specifier == beam::FrontendSpecifier::PhoenixLaser
1733                        && matches!(beam.stage_section, StageSection::Buildup)
1734                    {
1735                        self.particles.resize_with(
1736                            self.particles.len()
1737                                + 2 * usize::from(
1738                                    self.scheduler.heartbeats(Duration::from_millis(2)),
1739                                ),
1740                            || {
1741                                let mut left_right_alignment =
1742                                    dir.cross(Vec3::new(0.0, 0.0, 1.0)).normalized();
1743                                if rng.gen_bool(0.5) {
1744                                    left_right_alignment *= -1.0;
1745                                }
1746                                let start = interpolated.pos
1747                                    + left_right_alignment * 4.0
1748                                    + dir.normalized() * 6.0;
1749                                let lifespan = Duration::from_secs_f32(0.5);
1750                                Particle::new_directed(
1751                                    lifespan,
1752                                    time,
1753                                    ParticleMode::PhoenixBuildUpAim,
1754                                    start,
1755                                    interpolated.pos
1756                                        + dir.normalized() * 3.0
1757                                        + left_right_alignment * 0.4
1758                                        + vel
1759                                            .map_or(Vec3::zero(), |v| v.0 * lifespan.as_secs_f32()),
1760                                )
1761                            },
1762                        );
1763                    }
1764                },
1765                CharacterState::Glide(glide) => {
1766                    if let Some(Fluid::Air {
1767                        vel: air_vel,
1768                        elevation: _,
1769                    }) = physics.in_fluid
1770                    {
1771                        // Empirical observation is that air_vel is somewhere
1772                        // between 0.0 and 13.0, but we are extending to be sure
1773                        const MAX_AIR_VEL: f32 = 15.0;
1774                        const MIN_AIR_VEL: f32 = -2.0;
1775
1776                        let minmax_norm = |val, min, max| (val - min) / (max - min);
1777
1778                        let wind_speed = air_vel.0.magnitude();
1779
1780                        // Less means more frequent particles
1781                        let heartbeat = 200
1782                            - Lerp::lerp(
1783                                50u64,
1784                                150,
1785                                minmax_norm(wind_speed, MIN_AIR_VEL, MAX_AIR_VEL),
1786                            );
1787
1788                        let new_count = self.particles.len()
1789                            + usize::from(
1790                                self.scheduler.heartbeats(Duration::from_millis(heartbeat)),
1791                            );
1792
1793                        // More number, longer particles
1794                        let duration = Lerp::lerp(
1795                            0u64,
1796                            1000,
1797                            minmax_norm(wind_speed, MIN_AIR_VEL, MAX_AIR_VEL),
1798                        );
1799                        let duration = Duration::from_millis(duration);
1800
1801                        self.particles.resize_with(new_count, || {
1802                            let start_pos = interpolated.pos
1803                                + Vec3::new(
1804                                    body.max_radius(),
1805                                    body.max_radius(),
1806                                    body.height() / 2.0,
1807                                )
1808                                .map(|d| d * rng.gen_range(-10.0..10.0));
1809
1810                            Particle::new_directed(
1811                                duration,
1812                                time,
1813                                ParticleMode::Airflow,
1814                                start_pos,
1815                                start_pos + air_vel.0,
1816                            )
1817                        });
1818
1819                        // When using the glide boost, emit particles
1820                        if let Some(states::glide::Boost::Forward(_)) = &glide.booster
1821                            && let Some(figure_state) =
1822                                figure_mgr.states.character_states.get(&entity)
1823                            && let Some(tp0) = figure_state.primary_abs_trail_points
1824                            && let Some(tp1) = figure_state.secondary_abs_trail_points
1825                        {
1826                            for _ in 0..self.scheduler.heartbeats(Duration::from_millis(5)) {
1827                                self.particles.push(Particle::new(
1828                                    Duration::from_secs(2),
1829                                    time,
1830                                    ParticleMode::EngineJet,
1831                                    ((tp0.0 + tp1.1) * 0.5)
1832                                        // TODO: This offset is used to position the particles at the engine outlet. Ideally, we'd have a way to configure this per-glider
1833                                        + Vec3::unit_z() * 0.5
1834                                        + Vec3::<f32>::zero().map(|_| rng.gen_range(-0.25..0.25))
1835                                        + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
1836                                ));
1837                            }
1838                        }
1839                    }
1840                },
1841                CharacterState::Transform(data) => {
1842                    if matches!(data.stage_section, StageSection::Buildup)
1843                        && let Some(specifier) = data.static_data.specifier
1844                    {
1845                        match specifier {
1846                            states::transform::FrontendSpecifier::Evolve => {
1847                                self.particles.resize_with(
1848                                    self.particles.len()
1849                                        + usize::from(
1850                                            self.scheduler.heartbeats(Duration::from_millis(10)),
1851                                        ),
1852                                    || {
1853                                        let start_pos = interpolated.pos
1854                                            + (Vec2::unit_y()
1855                                                * rng.gen::<f32>()
1856                                                * body.max_radius())
1857                                            .rotated_z(rng.gen_range(0.0..(PI * 2.0)))
1858                                            .with_z(body.height() * rng.gen::<f32>());
1859
1860                                        Particle::new_directed(
1861                                            Duration::from_millis(100),
1862                                            time,
1863                                            ParticleMode::BarrelOrgan,
1864                                            start_pos,
1865                                            start_pos + Vec3::unit_z() * 2.0,
1866                                        )
1867                                    },
1868                                )
1869                            },
1870                            states::transform::FrontendSpecifier::Cursekeeper => {
1871                                self.particles.resize_with(
1872                                    self.particles.len()
1873                                        + usize::from(
1874                                            self.scheduler.heartbeats(Duration::from_millis(10)),
1875                                        ),
1876                                    || {
1877                                        let start_pos = interpolated.pos
1878                                            + (Vec2::unit_y()
1879                                                * rng.gen::<f32>()
1880                                                * body.max_radius())
1881                                            .rotated_z(rng.gen_range(0.0..(PI * 2.0)))
1882                                            .with_z(body.height() * rng.gen::<f32>());
1883
1884                                        Particle::new_directed(
1885                                            Duration::from_millis(100),
1886                                            time,
1887                                            ParticleMode::FireworkPurple,
1888                                            start_pos,
1889                                            start_pos + Vec3::unit_z() * 2.0,
1890                                        )
1891                                    },
1892                                )
1893                            },
1894                        }
1895                    }
1896                },
1897                CharacterState::ChargedMelee(_melee) => {
1898                    self.maintain_hydra_tail_swipe_particles(
1899                        scene_data,
1900                        figure_mgr,
1901                        entity,
1902                        interpolated.pos,
1903                        body,
1904                        character_state,
1905                        inventory,
1906                    );
1907                },
1908                _ => {},
1909            }
1910        }
1911    }
1912
1913    fn maintain_beam_particles(&mut self, scene_data: &SceneData, lights: &mut Vec<Light>) {
1914        let state = scene_data.state;
1915        let ecs = state.ecs();
1916        let time = state.get_time();
1917        let terrain = state.terrain();
1918        // Limit to 100 per tick, so at less than 10 FPS particle generation
1919        // work doesn't increase frame cost further.
1920        let tick_elapse = u32::from(self.scheduler.heartbeats(Duration::from_millis(1)).min(100));
1921        let mut rng = thread_rng();
1922
1923        for (beam, ori) in (&ecs.read_storage::<Beam>(), &ecs.read_storage::<Ori>()).join() {
1924            let particles_per_sec = (match beam.specifier {
1925                beam::FrontendSpecifier::Flamethrower
1926                | beam::FrontendSpecifier::Bubbles
1927                | beam::FrontendSpecifier::Steam
1928                | beam::FrontendSpecifier::Frost
1929                | beam::FrontendSpecifier::Poison
1930                | beam::FrontendSpecifier::Ink
1931                | beam::FrontendSpecifier::PhoenixLaser
1932                | beam::FrontendSpecifier::Gravewarden => 300.0,
1933                beam::FrontendSpecifier::FirePillar | beam::FrontendSpecifier::FlameWallPillar => {
1934                    40.0 * beam.end_radius.powi(2)
1935                },
1936                beam::FrontendSpecifier::LifestealBeam => 420.0,
1937                beam::FrontendSpecifier::Cultist => 960.0,
1938                beam::FrontendSpecifier::WebStrand => 180.0,
1939                beam::FrontendSpecifier::Lightning => 120.0,
1940                beam::FrontendSpecifier::FireGigasOverheat => 1600.0,
1941            }) / 1000.0;
1942
1943            let beam_tick_count = tick_elapse as f32 * particles_per_sec;
1944            let beam_tick_count = if rng.gen_bool(f64::from(beam_tick_count.fract())) {
1945                beam_tick_count.ceil() as u32
1946            } else {
1947                beam_tick_count.floor() as u32
1948            };
1949
1950            if beam_tick_count == 0 {
1951                continue;
1952            }
1953
1954            let distributed_time = tick_elapse as f64 / (beam_tick_count * 1000) as f64;
1955            let angle = (beam.end_radius / beam.range).atan();
1956            let beam_dir = (beam.bezier.ctrl - beam.bezier.start)
1957                .try_normalized()
1958                .unwrap_or(*ori.look_dir());
1959            let raycast_distance = |from, to| terrain.ray(from, to).until(Block::is_solid).cast().0;
1960
1961            self.particles.reserve(beam_tick_count as usize);
1962            match beam.specifier {
1963                beam::FrontendSpecifier::Flamethrower => {
1964                    let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
1965                    let m = Mat3::<f32>::rotation_from_to_3d(from, to);
1966                    // Emit a light when using flames
1967                    if scene_data.flashing_lights_enabled {
1968                        lights.push(Light::new(
1969                            beam.bezier.start,
1970                            Rgb::new(1.0, 0.25, 0.05).map(|e| e * rng.gen_range(0.8..1.2)),
1971                            2.0,
1972                        ));
1973                    }
1974
1975                    for i in 0..beam_tick_count {
1976                        let phi: f32 = rng.gen_range(0.0..angle);
1977                        let theta: f32 = rng.gen_range(0.0..2.0 * PI);
1978                        let offset_z =
1979                            Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
1980                        let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
1981                        self.particles.push(Particle::new_directed_with_collision(
1982                            Duration::from_secs_f64(beam.duration.0),
1983                            time + distributed_time * i as f64,
1984                            ParticleMode::FlameThrower,
1985                            beam.bezier.start,
1986                            beam.bezier.start + random_ori * beam.range,
1987                            raycast_distance,
1988                        ));
1989                    }
1990                },
1991                beam::FrontendSpecifier::FireGigasOverheat => {
1992                    let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
1993                    let m = Mat3::<f32>::rotation_from_to_3d(from, to);
1994                    // Emit a light when using flames
1995                    if scene_data.flashing_lights_enabled {
1996                        lights.push(Light::new(
1997                            beam.bezier.start,
1998                            Rgb::new(1.0, 0.25, 0.05).map(|e| e * rng.gen_range(0.8..1.2)),
1999                            2.0,
2000                        ));
2001                    }
2002
2003                    for i in 0..beam_tick_count {
2004                        let phi: f32 = rng.gen_range(0.0..angle);
2005                        let theta: f32 = rng.gen_range(0.0..2.0 * PI);
2006                        let offset_z =
2007                            Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2008                        let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2009                        self.particles.push(Particle::new_directed_with_collision(
2010                            Duration::from_secs_f64(beam.duration.0),
2011                            time + distributed_time * i as f64,
2012                            ParticleMode::FireGigasOverheat,
2013                            beam.bezier.start,
2014                            beam.bezier.start + random_ori * beam.range,
2015                            raycast_distance,
2016                        ));
2017                    }
2018                },
2019                beam::FrontendSpecifier::FirePillar | beam::FrontendSpecifier::FlameWallPillar => {
2020                    // Emit a light when using flames
2021                    if scene_data.flashing_lights_enabled {
2022                        lights.push(Light::new(
2023                            beam.bezier.start,
2024                            Rgb::new(1.0, 0.25, 0.05).map(|e| e * rng.gen_range(0.8..1.2)),
2025                            2.0,
2026                        ));
2027                    }
2028
2029                    for i in 0..beam_tick_count {
2030                        let theta: f32 = rng.gen_range(0.0..2.0 * PI);
2031                        let radius = beam.start_radius * (1.0 - rng.gen::<f32>().powi(8));
2032                        let offset = Vec3::new(radius * theta.cos(), radius * theta.sin(), 0.0);
2033                        self.particles.push(Particle::new_directed_with_collision(
2034                            Duration::from_secs_f64(beam.duration.0),
2035                            time + distributed_time * i as f64,
2036                            ParticleMode::FirePillar,
2037                            beam.bezier.start + offset,
2038                            beam.bezier.start + offset + beam.range * Vec3::unit_z(),
2039                            raycast_distance,
2040                        ));
2041                    }
2042                },
2043                beam::FrontendSpecifier::Cultist => {
2044                    let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2045                    let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2046                    // Emit a light when using flames
2047                    if scene_data.flashing_lights_enabled {
2048                        lights.push(Light::new(
2049                            beam.bezier.start,
2050                            Rgb::new(1.0, 0.0, 1.0).map(|e| e * rng.gen_range(0.5..1.0)),
2051                            2.0,
2052                        ));
2053                    }
2054                    for i in 0..beam_tick_count {
2055                        let phi: f32 = rng.gen_range(0.0..angle);
2056                        let theta: f32 = rng.gen_range(0.0..2.0 * PI);
2057                        let offset_z =
2058                            Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2059                        let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2060                        self.particles.push(Particle::new_directed_with_collision(
2061                            Duration::from_secs_f64(beam.duration.0),
2062                            time + distributed_time * i as f64,
2063                            ParticleMode::CultistFlame,
2064                            beam.bezier.start,
2065                            beam.bezier.start + random_ori * beam.range,
2066                            raycast_distance,
2067                        ));
2068                    }
2069                },
2070                beam::FrontendSpecifier::LifestealBeam => {
2071                    // Emit a light when using lifesteal beam
2072                    if scene_data.flashing_lights_enabled {
2073                        lights.push(Light::new(beam.bezier.start, Rgb::new(0.8, 1.0, 0.5), 1.0));
2074                    }
2075
2076                    // If the beam is one straight line, we can run raycast one time.
2077                    let bezier_end = beam.bezier.start + beam_dir * beam.range;
2078                    let distance = raycast_distance(beam.bezier.start, bezier_end);
2079                    for i in 0..beam_tick_count {
2080                        self.particles.push(Particle::new_directed_with_collision(
2081                            Duration::from_secs_f64(beam.duration.0),
2082                            time + distributed_time * i as f64,
2083                            ParticleMode::LifestealBeam,
2084                            beam.bezier.start,
2085                            bezier_end,
2086                            |_from, _to| distance,
2087                        ));
2088                    }
2089                },
2090                beam::FrontendSpecifier::Gravewarden => {
2091                    for i in 0..beam_tick_count {
2092                        let mut offset = 0.5;
2093                        let side = Vec2::new(-beam_dir.y, beam_dir.x);
2094                        self.particles.resize_with(self.particles.len() + 2, || {
2095                            offset = -offset;
2096                            Particle::new_directed_with_collision(
2097                                Duration::from_secs_f64(beam.duration.0),
2098                                time + distributed_time * i as f64,
2099                                ParticleMode::Laser,
2100                                beam.bezier.start + beam_dir * 1.5 + side * offset,
2101                                beam.bezier.start + beam_dir * beam.range + side * offset,
2102                                raycast_distance,
2103                            )
2104                        });
2105                    }
2106                },
2107                beam::FrontendSpecifier::WebStrand => {
2108                    let bezier_end = beam.bezier.start + beam_dir * beam.range;
2109                    let distance = raycast_distance(beam.bezier.start, bezier_end);
2110                    for i in 0..beam_tick_count {
2111                        self.particles.push(Particle::new_directed_with_collision(
2112                            Duration::from_secs_f64(beam.duration.0),
2113                            time + distributed_time * i as f64,
2114                            ParticleMode::WebStrand,
2115                            beam.bezier.start,
2116                            bezier_end,
2117                            |_from, _to| distance,
2118                        ));
2119                    }
2120                },
2121                beam::FrontendSpecifier::Bubbles => {
2122                    let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2123                    let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2124                    for i in 0..beam_tick_count {
2125                        let phi: f32 = rng.gen_range(0.0..angle);
2126                        let theta: f32 = rng.gen_range(0.0..2.0 * PI);
2127                        let offset_z =
2128                            Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2129                        let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2130                        self.particles.push(Particle::new_directed_with_collision(
2131                            Duration::from_secs_f64(beam.duration.0),
2132                            time + distributed_time * i as f64,
2133                            ParticleMode::Bubbles,
2134                            beam.bezier.start,
2135                            beam.bezier.start + random_ori * beam.range,
2136                            raycast_distance,
2137                        ));
2138                    }
2139                },
2140                beam::FrontendSpecifier::Poison => {
2141                    let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2142                    let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2143                    for i in 0..beam_tick_count {
2144                        let phi: f32 = rng.gen_range(0.0..angle);
2145                        let theta: f32 = rng.gen_range(0.0..2.0 * PI);
2146                        let offset_z =
2147                            Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2148                        let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2149                        self.particles.push(Particle::new_directed_with_collision(
2150                            Duration::from_secs_f64(beam.duration.0),
2151                            time + distributed_time * i as f64,
2152                            ParticleMode::Poison,
2153                            beam.bezier.start,
2154                            beam.bezier.start + random_ori * beam.range,
2155                            raycast_distance,
2156                        ));
2157                    }
2158                },
2159                beam::FrontendSpecifier::Ink => {
2160                    let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2161                    let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2162                    for i in 0..beam_tick_count {
2163                        let phi: f32 = rng.gen_range(0.0..angle);
2164                        let theta: f32 = rng.gen_range(0.0..2.0 * PI);
2165                        let offset_z =
2166                            Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2167                        let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2168                        self.particles.push(Particle::new_directed_with_collision(
2169                            Duration::from_secs_f64(beam.duration.0),
2170                            time + distributed_time * i as f64,
2171                            ParticleMode::Bubbles,
2172                            beam.bezier.start,
2173                            beam.bezier.start + random_ori * beam.range,
2174                            raycast_distance,
2175                        ));
2176                    }
2177                },
2178                beam::FrontendSpecifier::Steam => {
2179                    let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2180                    let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2181                    for i in 0..beam_tick_count {
2182                        let phi: f32 = rng.gen_range(0.0..angle);
2183                        let theta: f32 = rng.gen_range(0.0..2.0 * PI);
2184                        let offset_z =
2185                            Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2186                        let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2187                        self.particles.push(Particle::new_directed_with_collision(
2188                            Duration::from_secs_f64(beam.duration.0),
2189                            time + distributed_time * i as f64,
2190                            ParticleMode::Steam,
2191                            beam.bezier.start,
2192                            beam.bezier.start + random_ori * beam.range,
2193                            raycast_distance,
2194                        ));
2195                    }
2196                },
2197                beam::FrontendSpecifier::Lightning => {
2198                    let bezier_end = beam.bezier.start + beam_dir * beam.range;
2199                    let distance = raycast_distance(beam.bezier.start, bezier_end);
2200                    for i in 0..beam_tick_count {
2201                        self.particles.push(Particle::new_directed_with_collision(
2202                            Duration::from_secs_f64(beam.duration.0),
2203                            time + distributed_time * i as f64,
2204                            ParticleMode::Lightning,
2205                            beam.bezier.start,
2206                            bezier_end,
2207                            |_from, _to| distance,
2208                        ));
2209                    }
2210                },
2211                beam::FrontendSpecifier::Frost => {
2212                    let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2213                    let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2214                    for i in 0..beam_tick_count {
2215                        let phi: f32 = rng.gen_range(0.0..angle);
2216                        let theta: f32 = rng.gen_range(0.0..2.0 * PI);
2217                        let offset_z =
2218                            Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2219                        let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2220                        self.particles.push(Particle::new_directed_with_collision(
2221                            Duration::from_secs_f64(beam.duration.0),
2222                            time + distributed_time * i as f64,
2223                            ParticleMode::Ice,
2224                            beam.bezier.start,
2225                            beam.bezier.start + random_ori * beam.range,
2226                            raycast_distance,
2227                        ));
2228                    }
2229                },
2230                beam::FrontendSpecifier::PhoenixLaser => {
2231                    let bezier_end = beam.bezier.start + beam_dir * beam.range;
2232                    let distance = raycast_distance(beam.bezier.start, bezier_end);
2233                    for i in 0..beam_tick_count {
2234                        self.particles.push(Particle::new_directed_with_collision(
2235                            Duration::from_secs_f64(beam.duration.0),
2236                            time + distributed_time * i as f64,
2237                            ParticleMode::PhoenixBeam,
2238                            beam.bezier.start,
2239                            bezier_end,
2240                            |_from, _to| distance,
2241                        ));
2242                    }
2243                },
2244            }
2245        }
2246    }
2247
2248    fn maintain_aura_particles(&mut self, scene_data: &SceneData) {
2249        let state = scene_data.state;
2250        let ecs = state.ecs();
2251        let time = state.get_time();
2252        let mut rng = thread_rng();
2253        let dt = scene_data.state.get_delta_time();
2254
2255        for (interp, pos, auras, body_maybe) in (
2256            ecs.read_storage::<Interpolated>().maybe(),
2257            &ecs.read_storage::<Pos>(),
2258            &ecs.read_storage::<comp::Auras>(),
2259            ecs.read_storage::<comp::Body>().maybe(),
2260        )
2261            .join()
2262        {
2263            let pos = interp.map_or(pos.0, |i| i.pos);
2264
2265            for (_, aura) in auras.auras.iter() {
2266                match aura.aura_kind {
2267                    aura::AuraKind::Buff {
2268                        kind: buff::BuffKind::ProtectingWard,
2269                        ..
2270                    } => {
2271                        let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
2272                        self.particles.resize_with(
2273                            self.particles.len()
2274                                + aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
2275                            || {
2276                                let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100));
2277                                let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
2278                                let duration = Duration::from_secs_f64(
2279                                    aura.end_time
2280                                        .map_or(1.0, |end| end.0 - time)
2281                                        .clamp(0.0, 1.0),
2282                                );
2283                                Particle::new_directed(
2284                                    duration,
2285                                    time,
2286                                    ParticleMode::EnergyNature,
2287                                    pos,
2288                                    pos + init_pos,
2289                                )
2290                            },
2291                        );
2292                    },
2293                    aura::AuraKind::Buff {
2294                        kind: buff::BuffKind::Regeneration,
2295                        ..
2296                    } => {
2297                        if auras.auras.iter().any(|(_, aura)| {
2298                            matches!(aura.aura_kind, aura::AuraKind::Buff {
2299                                kind: buff::BuffKind::ProtectingWard,
2300                                ..
2301                            })
2302                        }) {
2303                            // If same entity has both protecting ward and regeneration auras, skip
2304                            // particles for regeneration
2305                            continue;
2306                        }
2307                        let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
2308                        self.particles.resize_with(
2309                            self.particles.len()
2310                                + aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
2311                            || {
2312                                let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100));
2313                                let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
2314                                let duration = Duration::from_secs_f64(
2315                                    aura.end_time
2316                                        .map_or(1.0, |end| end.0 - time)
2317                                        .clamp(0.0, 1.0),
2318                                );
2319                                Particle::new_directed(
2320                                    duration,
2321                                    time,
2322                                    ParticleMode::EnergyHealing,
2323                                    pos,
2324                                    pos + init_pos,
2325                                )
2326                            },
2327                        );
2328                    },
2329                    aura::AuraKind::Buff {
2330                        kind: buff::BuffKind::Burning,
2331                        ..
2332                    } => {
2333                        let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
2334                        self.particles.resize_with(
2335                            self.particles.len()
2336                                + aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
2337                            || {
2338                                let rand_pos = {
2339                                    let theta = rng.gen::<f32>() * TAU;
2340                                    let radius = aura.radius * rng.gen::<f32>().sqrt();
2341                                    let x = radius * theta.sin();
2342                                    let y = radius * theta.cos();
2343                                    Vec2::new(x, y) + pos.xy()
2344                                };
2345                                let duration = Duration::from_secs_f64(
2346                                    aura.end_time
2347                                        .map_or(1.0, |end| end.0 - time)
2348                                        .clamp(0.0, 1.0),
2349                                );
2350                                Particle::new_directed(
2351                                    duration,
2352                                    time,
2353                                    ParticleMode::FlameThrower,
2354                                    rand_pos.with_z(pos.z),
2355                                    rand_pos.with_z(pos.z + 1.0),
2356                                )
2357                            },
2358                        );
2359                    },
2360                    aura::AuraKind::Buff {
2361                        kind: buff::BuffKind::Hastened,
2362                        ..
2363                    } => {
2364                        let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
2365                        self.particles.resize_with(
2366                            self.particles.len()
2367                                + aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
2368                            || {
2369                                let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100));
2370                                let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
2371                                let duration = Duration::from_secs_f64(
2372                                    aura.end_time
2373                                        .map_or(1.0, |end| end.0 - time)
2374                                        .clamp(0.0, 1.0),
2375                                );
2376                                Particle::new_directed(
2377                                    duration,
2378                                    time,
2379                                    ParticleMode::EnergyBuffing,
2380                                    pos,
2381                                    pos + init_pos,
2382                                )
2383                            },
2384                        );
2385                    },
2386                    aura::AuraKind::Buff {
2387                        kind: buff::BuffKind::Frozen,
2388                        ..
2389                    } => {
2390                        let is_new_aura = aura.data.duration.is_none_or(|max_dur| {
2391                            let rem_dur = aura.end_time.map_or(time, |e| e.0) - time;
2392                            rem_dur > max_dur.0 * 0.9
2393                        });
2394                        if is_new_aura {
2395                            let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
2396                            self.particles.resize_with(
2397                                self.particles.len()
2398                                    + aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
2399                                || {
2400                                    let rand_angle = rng.gen_range(0.0..TAU);
2401                                    let offset =
2402                                        Vec2::new(rand_angle.cos(), rand_angle.sin()) * aura.radius;
2403                                    let z_start = body_maybe
2404                                        .map_or(0.0, |b| rng.gen_range(0.5..0.75) * b.height());
2405                                    let z_end = body_maybe
2406                                        .map_or(0.0, |b| rng.gen_range(0.0..3.0) * b.height());
2407                                    Particle::new_directed(
2408                                        Duration::from_secs(3),
2409                                        time,
2410                                        ParticleMode::Ice,
2411                                        pos + Vec3::unit_z() * z_start,
2412                                        pos + offset.with_z(z_end),
2413                                    )
2414                                },
2415                            );
2416                        }
2417                    },
2418                    aura::AuraKind::Buff {
2419                        kind: buff::BuffKind::Heatstroke,
2420                        ..
2421                    } => {
2422                        let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
2423                        self.particles.resize_with(
2424                            self.particles.len()
2425                                + aura.radius.powi(2) as usize * usize::from(heartbeats) / 900,
2426                            || {
2427                                let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100));
2428                                let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
2429                                let duration = Duration::from_secs_f64(
2430                                    aura.end_time
2431                                        .map_or(1.0, |end| end.0 - time)
2432                                        .clamp(0.0, 1.0),
2433                                );
2434                                Particle::new_directed(
2435                                    duration,
2436                                    time,
2437                                    ParticleMode::EnergyPhoenix,
2438                                    pos,
2439                                    pos + init_pos,
2440                                )
2441                            },
2442                        );
2443
2444                        let num_particles = aura.radius.powi(2) * dt / 50.0;
2445                        let num_particles = num_particles.floor() as usize
2446                            + usize::from(rng.gen_bool(f64::from(num_particles % 1.0)));
2447                        self.particles
2448                            .resize_with(self.particles.len() + num_particles, || {
2449                                let rand_pos = {
2450                                    let theta = rng.gen::<f32>() * TAU;
2451                                    let radius = aura.radius * rng.gen::<f32>().sqrt();
2452                                    let x = radius * theta.sin();
2453                                    let y = radius * theta.cos();
2454                                    Vec2::new(x, y) + pos.xy()
2455                                };
2456                                let duration = Duration::from_secs_f64(
2457                                    aura.end_time
2458                                        .map_or(1.0, |end| end.0 - time)
2459                                        .clamp(0.0, 1.0),
2460                                );
2461                                Particle::new_directed(
2462                                    duration,
2463                                    time,
2464                                    ParticleMode::FieryBurstAsh,
2465                                    pos,
2466                                    Vec3::new(
2467                                                    0.0,    // radius of rand spawn
2468                                                    20.0,   // integer part - radius of the curve part, fractional part - relative time of setting particle on fire
2469                                                    5.5)    // height of the flight
2470                                                    + rand_pos.with_z(pos.z),
2471                                )
2472                            });
2473                    },
2474                    _ => {},
2475                }
2476            }
2477        }
2478    }
2479
2480    fn maintain_buff_particles(&mut self, scene_data: &SceneData) {
2481        let state = scene_data.state;
2482        let ecs = state.ecs();
2483        let time = state.get_time();
2484        let mut rng = thread_rng();
2485
2486        for (interp, pos, buffs, body, ori, scale) in (
2487            ecs.read_storage::<Interpolated>().maybe(),
2488            &ecs.read_storage::<Pos>(),
2489            &ecs.read_storage::<comp::Buffs>(),
2490            &ecs.read_storage::<Body>(),
2491            &ecs.read_storage::<Ori>(),
2492            ecs.read_storage::<Scale>().maybe(),
2493        )
2494            .join()
2495        {
2496            let pos = interp.map_or(pos.0, |i| i.pos);
2497
2498            for (buff_kind, buff_keys) in buffs
2499                .kinds
2500                .iter()
2501                .filter_map(|(kind, keys)| keys.as_ref().map(|keys| (kind, keys)))
2502            {
2503                use buff::BuffKind;
2504                match buff_kind {
2505                    BuffKind::Cursed | BuffKind::Burning => {
2506                        self.particles.resize_with(
2507                            self.particles.len()
2508                                + usize::from(self.scheduler.heartbeats(Duration::from_millis(15))),
2509                            || {
2510                                let start_pos = pos
2511                                    + Vec3::unit_z() * body.height() * 0.25
2512                                    + Vec3::<f32>::zero()
2513                                        .map(|_| rng.gen_range(-1.0..1.0))
2514                                        .normalized()
2515                                        * 0.25;
2516                                let end_pos = start_pos
2517                                    + Vec3::unit_z() * body.height()
2518                                    + Vec3::<f32>::zero()
2519                                        .map(|_| rng.gen_range(-1.0..1.0))
2520                                        .normalized();
2521                                Particle::new_directed(
2522                                    Duration::from_secs(1),
2523                                    time,
2524                                    if matches!(buff_kind, BuffKind::Cursed) {
2525                                        ParticleMode::CultistFlame
2526                                    } else {
2527                                        ParticleMode::FlameThrower
2528                                    },
2529                                    start_pos,
2530                                    end_pos,
2531                                )
2532                            },
2533                        );
2534                    },
2535                    BuffKind::PotionSickness => {
2536                        let mut multiplicity = 0;
2537                        // Only show particles for potion sickness at the beginning, after the
2538                        // drinking animation finishes
2539                        if buff_keys.0
2540                            .iter()
2541                            .filter_map(|key| buffs.buffs.get(*key))
2542                            .any(|buff| {
2543                                matches!(buff.elapsed(Time(time)), dur if (1.0..=1.5).contains(&dur.0))
2544                            })
2545                        {
2546                            multiplicity = 1;
2547                        }
2548                        self.particles.resize_with(
2549                            self.particles.len()
2550                                + multiplicity
2551                                    * usize::from(
2552                                        self.scheduler.heartbeats(Duration::from_millis(25)),
2553                                    ),
2554                            || {
2555                                let start_pos = pos
2556                                    + Vec3::unit_z() * body.eye_height(scale.map_or(1.0, |s| s.0));
2557                                let (radius, theta) =
2558                                    (rng.gen_range(0.0f32..1.0).sqrt(), rng.gen_range(0.0..TAU));
2559                                let end_pos = pos
2560                                    + *ori.look_dir()
2561                                    + Vec3::<f32>::new(
2562                                        radius * theta.cos(),
2563                                        radius * theta.sin(),
2564                                        0.0,
2565                                    ) * 0.25;
2566                                Particle::new_directed(
2567                                    Duration::from_secs(1),
2568                                    time,
2569                                    ParticleMode::PotionSickness,
2570                                    start_pos,
2571                                    end_pos,
2572                                )
2573                            },
2574                        );
2575                    },
2576                    BuffKind::Frenzied => {
2577                        self.particles.resize_with(
2578                            self.particles.len()
2579                                + usize::from(self.scheduler.heartbeats(Duration::from_millis(15))),
2580                            || {
2581                                let start_pos = pos
2582                                    + Vec3::new(
2583                                        body.max_radius(),
2584                                        body.max_radius(),
2585                                        body.height() / 2.0,
2586                                    )
2587                                    .map(|d| d * rng.gen_range(-1.0..1.0));
2588                                let end_pos = start_pos
2589                                    + Vec3::unit_z() * body.height()
2590                                    + Vec3::<f32>::zero()
2591                                        .map(|_| rng.gen_range(-1.0..1.0))
2592                                        .normalized();
2593                                Particle::new_directed(
2594                                    Duration::from_secs(1),
2595                                    time,
2596                                    ParticleMode::Enraged,
2597                                    start_pos,
2598                                    end_pos,
2599                                )
2600                            },
2601                        );
2602                    },
2603                    BuffKind::Polymorphed => {
2604                        let mut multiplicity = 0;
2605                        // Only show particles for polymorph at the beginning, after the
2606                        // drinking animation finishes
2607                        if buff_keys.0
2608                            .iter()
2609                            .filter_map(|key| buffs.buffs.get(*key))
2610                            .any(|buff| {
2611                                matches!(buff.elapsed(Time(time)), dur if (0.1..=0.3).contains(&dur.0))
2612                            })
2613                        {
2614                            multiplicity = 1;
2615                        }
2616                        self.particles.resize_with(
2617                            self.particles.len()
2618                                + multiplicity
2619                                    * self.scheduler.heartbeats(Duration::from_millis(3)) as usize,
2620                            || {
2621                                let start_pos = pos
2622                                    + Vec3::unit_z() * body.eye_height(scale.map_or(1.0, |s| s.0))
2623                                        / 2.0;
2624                                let end_pos = start_pos
2625                                    + Vec3::<f32>::zero()
2626                                        .map(|_| rng.gen_range(-1.0..1.0))
2627                                        .normalized()
2628                                        * 5.0;
2629
2630                                Particle::new_directed(
2631                                    Duration::from_secs(2),
2632                                    time,
2633                                    ParticleMode::Explosion,
2634                                    start_pos,
2635                                    end_pos,
2636                                )
2637                            },
2638                        )
2639                    },
2640                    _ => {},
2641                }
2642            }
2643        }
2644    }
2645
2646    fn maintain_block_particles(
2647        &mut self,
2648        scene_data: &SceneData,
2649        terrain: &Terrain<TerrainChunk>,
2650        figure_mgr: &FigureMgr,
2651    ) {
2652        prof_span!("ParticleMgr::maintain_block_particles");
2653        let dt = scene_data.state.ecs().fetch::<DeltaTime>().0;
2654        let time = scene_data.state.get_time();
2655        let player_pos = scene_data
2656            .state
2657            .read_component_copied::<Interpolated>(scene_data.viewpoint_entity)
2658            .map(|i| i.pos)
2659            .unwrap_or_default();
2660        let player_chunk = player_pos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| {
2661            (e.floor() as i32).div_euclid(sz as i32)
2662        });
2663
2664        struct BlockParticles<'a> {
2665            // The function to select the blocks of interest that we should emit from
2666            blocks: fn(&'a BlocksOfInterest) -> BlockParticleSlice<'a>,
2667            // The range, in chunks, that the particles should be generated in from the player
2668            range: usize,
2669            // The emission rate, per block per second, of the generated particles
2670            rate: f32,
2671            // The number of seconds that each particle should live for
2672            lifetime: f32,
2673            // The visual mode of the generated particle
2674            mode: ParticleMode,
2675            // Condition that must be true
2676            cond: fn(&SceneData) -> bool,
2677        }
2678
2679        enum BlockParticleSlice<'a> {
2680            Positions(&'a [Vec3<i32>]),
2681            PositionsAndDirs(&'a [(Vec3<i32>, Vec3<f32>)]),
2682        }
2683
2684        impl BlockParticleSlice<'_> {
2685            fn len(&self) -> usize {
2686                match self {
2687                    Self::Positions(blocks) => blocks.len(),
2688                    Self::PositionsAndDirs(blocks) => blocks.len(),
2689                }
2690            }
2691        }
2692
2693        let particles: &[BlockParticles] = &[
2694            BlockParticles {
2695                blocks: |boi| BlockParticleSlice::Positions(&boi.leaves),
2696                range: 4,
2697                rate: 0.0125,
2698                lifetime: 30.0,
2699                mode: ParticleMode::Leaf,
2700                cond: |_| true,
2701            },
2702            BlockParticles {
2703                blocks: |boi| BlockParticleSlice::Positions(&boi.drip),
2704                range: 4,
2705                rate: 0.004,
2706                lifetime: 20.0,
2707                mode: ParticleMode::Drip,
2708                cond: |_| true,
2709            },
2710            BlockParticles {
2711                blocks: |boi| BlockParticleSlice::Positions(&boi.fires),
2712                range: 2,
2713                rate: 20.0,
2714                lifetime: 0.25,
2715                mode: ParticleMode::CampfireFire,
2716                cond: |_| true,
2717            },
2718            BlockParticles {
2719                blocks: |boi| BlockParticleSlice::Positions(&boi.fire_bowls),
2720                range: 2,
2721                rate: 20.0,
2722                lifetime: 0.25,
2723                mode: ParticleMode::FireBowl,
2724                cond: |_| true,
2725            },
2726            BlockParticles {
2727                blocks: |boi| BlockParticleSlice::Positions(&boi.fireflies),
2728                range: 6,
2729                rate: 0.004,
2730                lifetime: 40.0,
2731                mode: ParticleMode::Firefly,
2732                cond: |sd| sd.state.get_day_period().is_dark(),
2733            },
2734            BlockParticles {
2735                blocks: |boi| BlockParticleSlice::Positions(&boi.flowers),
2736                range: 5,
2737                rate: 0.002,
2738                lifetime: 40.0,
2739                mode: ParticleMode::Firefly,
2740                cond: |sd| sd.state.get_day_period().is_dark(),
2741            },
2742            BlockParticles {
2743                blocks: |boi| BlockParticleSlice::Positions(&boi.beehives),
2744                range: 3,
2745                rate: 0.5,
2746                lifetime: 30.0,
2747                mode: ParticleMode::Bee,
2748                cond: |sd| sd.state.get_day_period().is_light(),
2749            },
2750            BlockParticles {
2751                blocks: |boi| BlockParticleSlice::Positions(&boi.snow),
2752                range: 4,
2753                rate: 0.025,
2754                lifetime: 15.0,
2755                mode: ParticleMode::Snow,
2756                cond: |_| true,
2757            },
2758            BlockParticles {
2759                blocks: |boi| BlockParticleSlice::PositionsAndDirs(&boi.one_way_walls),
2760                range: 2,
2761                rate: 12.0,
2762                lifetime: 1.5,
2763                mode: ParticleMode::PortalFizz,
2764                cond: |_| true,
2765            },
2766            BlockParticles {
2767                blocks: |boi| BlockParticleSlice::Positions(&boi.spores),
2768                range: 4,
2769                rate: 0.055,
2770                lifetime: 20.0,
2771                mode: ParticleMode::Spore,
2772                cond: |_| true,
2773            },
2774            BlockParticles {
2775                blocks: |boi| BlockParticleSlice::PositionsAndDirs(&boi.waterfall),
2776                range: 2,
2777                rate: 4.0,
2778                lifetime: 5.0,
2779                mode: ParticleMode::WaterFoam,
2780                cond: |_| true,
2781            },
2782        ];
2783
2784        let ecs = scene_data.state.ecs();
2785        let mut rng = thread_rng();
2786        // Hard cap for performance reasons; Assuming that 25% of a chunk is covered in
2787        // lava or 32*32*0.25 = 256 TODO: Make this a setting?
2788        let cap = 512;
2789        for particles in particles.iter() {
2790            if !(particles.cond)(scene_data) {
2791                continue;
2792            }
2793
2794            for offset in Spiral2d::new().take((particles.range * 2 + 1).pow(2)) {
2795                let chunk_pos = player_chunk + offset;
2796
2797                terrain.get(chunk_pos).map(|chunk_data| {
2798                    let blocks = (particles.blocks)(&chunk_data.blocks_of_interest);
2799
2800                    let avg_particles = dt * (blocks.len() as f32 * particles.rate).min(cap as f32);
2801                    let particle_count = avg_particles.trunc() as usize
2802                        + (rng.gen::<f32>() < avg_particles.fract()) as usize;
2803
2804                    self.particles
2805                        .resize_with(self.particles.len() + particle_count, || {
2806                            match blocks {
2807                                BlockParticleSlice::Positions(blocks) => {
2808                                    // Can't fail, resize only occurs if blocks > 0
2809                                    let block_pos = Vec3::from(
2810                                        chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32),
2811                                    ) + blocks.choose(&mut rng).copied().unwrap();
2812                                    Particle::new(
2813                                        Duration::from_secs_f32(particles.lifetime),
2814                                        time,
2815                                        particles.mode,
2816                                        block_pos.map(|e: i32| e as f32 + rng.gen::<f32>()),
2817                                    )
2818                                },
2819                                BlockParticleSlice::PositionsAndDirs(blocks) => {
2820                                    // Can't fail, resize only occurs if blocks > 0
2821                                    let (block_offset, particle_dir) =
2822                                        blocks.choose(&mut rng).copied().unwrap();
2823                                    let block_pos = Vec3::from(
2824                                        chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32),
2825                                    ) + block_offset;
2826                                    let particle_pos =
2827                                        block_pos.map(|e: i32| e as f32 + rng.gen::<f32>());
2828                                    Particle::new_directed(
2829                                        Duration::from_secs_f32(particles.lifetime),
2830                                        time,
2831                                        particles.mode,
2832                                        particle_pos,
2833                                        particle_pos + particle_dir,
2834                                    )
2835                                },
2836                            }
2837                        })
2838                });
2839            }
2840
2841            for (entity, body, interpolated, collider) in (
2842                &ecs.entities(),
2843                &ecs.read_storage::<comp::Body>(),
2844                &ecs.read_storage::<crate::ecs::comp::Interpolated>(),
2845                ecs.read_storage::<comp::Collider>().maybe(),
2846            )
2847                .join()
2848            {
2849                if let Some((blocks_of_interest, offset)) =
2850                    figure_mgr.get_blocks_of_interest(entity, body, collider)
2851                {
2852                    let mat = Mat4::from(interpolated.ori.to_quat())
2853                        .translated_3d(interpolated.pos)
2854                        * Mat4::translation_3d(offset);
2855
2856                    let blocks = (particles.blocks)(blocks_of_interest);
2857
2858                    let avg_particles = dt * blocks.len() as f32 * particles.rate;
2859                    let particle_count = avg_particles.trunc() as usize
2860                        + (rng.gen::<f32>() < avg_particles.fract()) as usize;
2861
2862                    self.particles
2863                        .resize_with(self.particles.len() + particle_count, || {
2864                            match blocks {
2865                                BlockParticleSlice::Positions(blocks) => {
2866                                    let rel_pos = blocks
2867                                        .choose(&mut rng)
2868                                        .copied()
2869                                        // Can't fail, resize only occurs if blocks > 0
2870                                        .unwrap()
2871                                        .map(|e: i32| e as f32 + rng.gen::<f32>());
2872                                    let wpos = mat.mul_point(rel_pos);
2873
2874                                    Particle::new(
2875                                        Duration::from_secs_f32(particles.lifetime),
2876                                        time,
2877                                        particles.mode,
2878                                        wpos,
2879                                    )
2880                                },
2881                                BlockParticleSlice::PositionsAndDirs(blocks) => {
2882                                    // Can't fail, resize only occurs if blocks > 0
2883                                    let (block_offset, particle_dir) =
2884                                        blocks.choose(&mut rng).copied().unwrap();
2885                                    let particle_pos =
2886                                        block_offset.map(|e: i32| e as f32 + rng.gen::<f32>());
2887                                    let wpos = mat.mul_point(particle_pos);
2888                                    Particle::new_directed(
2889                                        Duration::from_secs_f32(particles.lifetime),
2890                                        time,
2891                                        particles.mode,
2892                                        wpos,
2893                                        wpos + mat.mul_direction(particle_dir),
2894                                    )
2895                                },
2896                            }
2897                        })
2898                }
2899            }
2900        }
2901        // smoke is more complex as it comes with varying rate and color
2902        {
2903            struct SmokeProperties {
2904                position: Vec3<i32>,
2905                strength: f32,
2906                dry_chance: f32,
2907            }
2908
2909            let range = 8_usize;
2910            let rate = 3.0 / 128.0;
2911            let lifetime = 40.0;
2912            let time_of_day = scene_data
2913                .state
2914                .get_time_of_day()
2915                .rem_euclid(24.0 * 60.0 * 60.0) as f32;
2916
2917            for offset in Spiral2d::new().take((range * 2 + 1).pow(2)) {
2918                let chunk_pos = player_chunk + offset;
2919
2920                terrain.get(chunk_pos).map(|chunk_data| {
2921                    let blocks = &chunk_data.blocks_of_interest.smokers;
2922                    let mut smoke_properties: Vec<SmokeProperties> = Vec::new();
2923                    let block_pos =
2924                        Vec3::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32));
2925                    let mut sum = 0.0_f32;
2926                    for smoker in blocks.iter() {
2927                        let position = block_pos + smoker.position;
2928                        let (strength, dry_chance) = {
2929                            match smoker.kind {
2930                                FireplaceType::House => {
2931                                    let prop = crate::scene::smoke_cycle::smoke_at_time(
2932                                        position,
2933                                        chunk_data.blocks_of_interest.temperature,
2934                                        time_of_day,
2935                                    );
2936                                    (
2937                                        prop.0,
2938                                        if prop.1 {
2939                                            // fire started, dark smoke
2940                                            0.8 - chunk_data.blocks_of_interest.humidity
2941                                        } else {
2942                                            // fire continues, light smoke
2943                                            1.2 - chunk_data.blocks_of_interest.humidity
2944                                        },
2945                                    )
2946                                },
2947                                FireplaceType::Workshop => (128.0, 1.0),
2948                            }
2949                        };
2950                        sum += strength;
2951                        smoke_properties.push(SmokeProperties {
2952                            position,
2953                            strength,
2954                            dry_chance,
2955                        });
2956                    }
2957                    let avg_particles = dt * sum * rate;
2958
2959                    let particle_count = avg_particles.trunc() as usize
2960                        + (rng.gen::<f32>() < avg_particles.fract()) as usize;
2961                    let chosen = smoke_properties.choose_multiple_weighted(
2962                        &mut rng,
2963                        particle_count,
2964                        |smoker| smoker.strength,
2965                    );
2966                    if let Ok(chosen) = chosen {
2967                        self.particles.extend(chosen.map(|smoker| {
2968                            Particle::new(
2969                                Duration::from_secs_f32(lifetime),
2970                                time,
2971                                if rng.gen::<f32>() > smoker.dry_chance {
2972                                    ParticleMode::BlackSmoke
2973                                } else {
2974                                    ParticleMode::CampfireSmoke
2975                                },
2976                                smoker.position.map(|e: i32| e as f32 + rng.gen::<f32>()),
2977                            )
2978                        }));
2979                    }
2980                });
2981            }
2982        }
2983    }
2984
2985    fn maintain_shockwave_particles(&mut self, scene_data: &SceneData) {
2986        let state = scene_data.state;
2987        let ecs = state.ecs();
2988        let time = state.get_time();
2989        let dt = scene_data.state.ecs().fetch::<DeltaTime>().0;
2990        let terrain = scene_data.state.ecs().fetch::<TerrainGrid>();
2991
2992        for (_entity, interp, pos, ori, shockwave) in (
2993            &ecs.entities(),
2994            ecs.read_storage::<Interpolated>().maybe(),
2995            &ecs.read_storage::<Pos>(),
2996            &ecs.read_storage::<Ori>(),
2997            &ecs.read_storage::<Shockwave>(),
2998        )
2999            .join()
3000        {
3001            let pos = interp.map_or(pos.0, |i| i.pos);
3002            let ori = interp.map_or(*ori, |i| i.ori);
3003
3004            let elapsed = time - shockwave.creation.unwrap_or(time);
3005            let speed = shockwave.properties.speed;
3006
3007            let percent = elapsed as f32 / shockwave.properties.duration.as_secs_f32();
3008
3009            let distance = speed * elapsed as f32;
3010
3011            let radians = shockwave.properties.angle.to_radians();
3012
3013            let ori_vec = ori.look_vec();
3014            let theta = ori_vec.y.atan2(ori_vec.x) - radians / 2.0;
3015            let dtheta = radians / distance;
3016
3017            // Number of particles derived from arc length (for new particles at least, old
3018            // can be converted later)
3019            let arc_length = distance * radians;
3020
3021            use shockwave::FrontendSpecifier;
3022            match shockwave.properties.specifier {
3023                FrontendSpecifier::Ground => {
3024                    let heartbeats = self.scheduler.heartbeats(Duration::from_millis(2));
3025                    for heartbeat in 0..heartbeats {
3026                        // 1 / 3 the size of terrain voxel
3027                        let scale = 1.0 / 3.0;
3028
3029                        let scaled_speed = speed * scale;
3030
3031                        let sub_tick_interpolation = scaled_speed * 1000.0 * heartbeat as f32;
3032
3033                        let distance = speed * (elapsed as f32 - sub_tick_interpolation);
3034
3035                        let particle_count_factor = radians / (3.0 * scale);
3036                        let new_particle_count = distance * particle_count_factor;
3037                        self.particles.reserve(new_particle_count as usize);
3038
3039                        for d in 0..(new_particle_count as i32) {
3040                            let arc_position = theta + dtheta * d as f32 / particle_count_factor;
3041
3042                            let position = pos
3043                                + distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0);
3044
3045                            // Arbitrary number chosen that is large enough to be able to accurately
3046                            // place particles most of the time, but also not too big as to make ray
3047                            // be too large (for performance reasons)
3048                            let half_ray_length = 10.0;
3049                            let mut last_air = false;
3050                            // TODO: Optimize ray to only be cast at most once per block per tick if
3051                            // it becomes an issue.
3052                            // From imbris:
3053                            //      each ray is ~2 us
3054                            //      at 30 FPS, it peaked at 113 rays in a tick
3055                            //      total time was 240 us (although potentially half that is
3056                            //          overhead from the profiling of each ray)
3057                            let _ = terrain
3058                                .ray(
3059                                    position + Vec3::unit_z() * half_ray_length,
3060                                    position - Vec3::unit_z() * half_ray_length,
3061                                )
3062                                .for_each(|block: &Block, pos: Vec3<i32>| {
3063                                    if block.is_solid() && block.get_sprite().is_none() {
3064                                        if last_air {
3065                                            let position = position.xy().with_z(pos.z as f32 + 1.0);
3066
3067                                            let position_snapped =
3068                                                ((position / scale).floor() + 0.5) * scale;
3069
3070                                            self.particles.push(Particle::new(
3071                                                Duration::from_millis(250),
3072                                                time,
3073                                                ParticleMode::GroundShockwave,
3074                                                position_snapped,
3075                                            ));
3076                                            last_air = false;
3077                                        }
3078                                    } else {
3079                                        last_air = true;
3080                                    }
3081                                })
3082                                .cast();
3083                        }
3084                    }
3085                },
3086                FrontendSpecifier::Fire => {
3087                    let heartbeats = self.scheduler.heartbeats(Duration::from_millis(2));
3088                    for _ in 0..heartbeats {
3089                        for d in 0..3 * distance as i32 {
3090                            let arc_position = theta + dtheta * d as f32 / 3.0;
3091
3092                            let position = pos
3093                                + distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0);
3094
3095                            self.particles.push(Particle::new(
3096                                Duration::from_secs_f32((distance + 10.0) / 50.0),
3097                                time,
3098                                ParticleMode::FireShockwave,
3099                                position,
3100                            ));
3101                        }
3102                    }
3103                },
3104                FrontendSpecifier::FireLow => {
3105                    let heartbeats = self.scheduler.heartbeats(Duration::from_millis(2));
3106                    for heartbeat in 0..heartbeats {
3107                        // 1 / 3 the size of terrain voxel
3108                        let scale = 1.0 / 3.0;
3109
3110                        let scaled_speed = speed * scale;
3111
3112                        let sub_tick_interpolation = scaled_speed * 1000.0 * heartbeat as f32;
3113
3114                        let distance = speed * (elapsed as f32 - sub_tick_interpolation);
3115
3116                        let particle_count_factor = radians / (3.0 * scale);
3117                        let new_particle_count = distance * particle_count_factor;
3118                        self.particles.reserve(new_particle_count as usize);
3119
3120                        for d in 0..(new_particle_count as i32) {
3121                            let arc_position = theta + dtheta * d as f32 / particle_count_factor;
3122
3123                            let position = pos
3124                                + distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0);
3125
3126                            // Arbitrary number chosen that is large enough to be able to accurately
3127                            // place particles most of the time, but also not too big as to make ray
3128                            // be too large (for performance reasons)
3129                            let half_ray_length = 10.0;
3130                            let mut last_air = false;
3131                            // TODO: Optimize ray to only be cast at most once per block per tick if
3132                            // it becomes an issue.
3133                            // From imbris:
3134                            //      each ray is ~2 us
3135                            //      at 30 FPS, it peaked at 113 rays in a tick
3136                            //      total time was 240 us (although potentially half that is
3137                            //          overhead from the profiling of each ray)
3138                            let _ = terrain
3139                                .ray(
3140                                    position + Vec3::unit_z() * half_ray_length,
3141                                    position - Vec3::unit_z() * half_ray_length,
3142                                )
3143                                .for_each(|block: &Block, pos: Vec3<i32>| {
3144                                    if block.is_solid() && block.get_sprite().is_none() {
3145                                        if last_air {
3146                                            let position = position.xy().with_z(pos.z as f32 + 1.0);
3147
3148                                            let position_snapped =
3149                                                ((position / scale).floor() + 0.5) * scale;
3150
3151                                            self.particles.push(Particle::new(
3152                                                Duration::from_millis(250),
3153                                                time,
3154                                                ParticleMode::FireLowShockwave,
3155                                                position_snapped,
3156                                            ));
3157                                            last_air = false;
3158                                        }
3159                                    } else {
3160                                        last_air = true;
3161                                    }
3162                                })
3163                                .cast();
3164                        }
3165                    }
3166                },
3167                FrontendSpecifier::Water => {
3168                    // 1 particle per unit length of arc
3169                    let particles_per_length = arc_length as usize;
3170                    let dtheta = radians / particles_per_length as f32;
3171                    // Scales number of desired heartbeats from speed - thicker arc = higher speed =
3172                    // lower duration = more particles
3173                    let heartbeats = self
3174                        .scheduler
3175                        .heartbeats(Duration::from_secs_f32(1.0 / speed));
3176
3177                    // Reserves capacity for new particles
3178                    let new_particle_count = particles_per_length * heartbeats as usize;
3179                    self.particles.reserve(new_particle_count);
3180
3181                    for i in 0..particles_per_length {
3182                        let angle = dtheta * i as f32;
3183                        let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3184                        for j in 0..heartbeats {
3185                            // Sub tick dt
3186                            let dt = (j as f32 / heartbeats as f32) * dt;
3187                            let distance = distance + speed * dt;
3188                            let pos1 = pos + distance * direction - Vec3::unit_z();
3189                            let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
3190                            let time = time + dt as f64;
3191
3192                            self.particles.push(Particle::new_directed(
3193                                Duration::from_secs_f32(0.5),
3194                                time,
3195                                ParticleMode::Water,
3196                                pos1,
3197                                pos2,
3198                            ));
3199                        }
3200                    }
3201                },
3202                FrontendSpecifier::Lightning => {
3203                    // 1 particle per unit length of arc
3204                    let particles_per_length = arc_length as usize;
3205                    let dtheta = radians / particles_per_length as f32;
3206                    // Scales number of desired heartbeats from speed - thicker arc = higher speed =
3207                    // lower duration = more particles
3208                    let heartbeats = self
3209                        .scheduler
3210                        .heartbeats(Duration::from_secs_f32(1.0 / speed));
3211
3212                    // Reserves capacity for new particles
3213                    let new_particle_count = particles_per_length * heartbeats as usize;
3214                    self.particles.reserve(new_particle_count);
3215
3216                    for i in 0..particles_per_length {
3217                        let angle = dtheta * i as f32;
3218                        let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3219                        for j in 0..heartbeats {
3220                            // Sub tick dt
3221                            let dt = (j as f32 / heartbeats as f32) * dt;
3222                            let distance = distance + speed * dt;
3223                            let pos1 = pos + distance * direction - Vec3::unit_z();
3224                            let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
3225                            let time = time + dt as f64;
3226
3227                            self.particles.push(Particle::new_directed(
3228                                Duration::from_secs_f32(0.5),
3229                                time,
3230                                ParticleMode::Lightning,
3231                                pos1,
3232                                pos2,
3233                            ));
3234                        }
3235                    }
3236                },
3237                FrontendSpecifier::Steam => {
3238                    // 1 particle per unit length of arc
3239                    let particles_per_length = arc_length as usize;
3240                    let dtheta = radians / particles_per_length as f32;
3241                    // Scales number of desired heartbeats from speed - thicker arc = higher speed =
3242                    // lower duration = more particles
3243                    let heartbeats = self
3244                        .scheduler
3245                        .heartbeats(Duration::from_secs_f32(1.0 / speed));
3246
3247                    // Reserves capacity for new particles
3248                    let new_particle_count = particles_per_length * heartbeats as usize;
3249                    self.particles.reserve(new_particle_count);
3250
3251                    for i in 0..particles_per_length {
3252                        let angle = dtheta * i as f32;
3253                        let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3254                        for j in 0..heartbeats {
3255                            // Sub tick dt
3256                            let dt = (j as f32 / heartbeats as f32) * dt;
3257                            let distance = distance + speed * dt;
3258                            let pos1 = pos + distance * direction - Vec3::unit_z();
3259                            let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
3260                            let time = time + dt as f64;
3261
3262                            self.particles.push(Particle::new_directed(
3263                                Duration::from_secs_f32(0.5),
3264                                time,
3265                                ParticleMode::Steam,
3266                                pos1,
3267                                pos2,
3268                            ));
3269                        }
3270                    }
3271                },
3272                FrontendSpecifier::Poison => {
3273                    // 1 particle per unit length of arc
3274                    let particles_per_length = arc_length as usize;
3275                    let dtheta = radians / particles_per_length as f32;
3276                    // Scales number of desired heartbeats from speed - thicker arc = higher speed =
3277                    // lower duration = more particles
3278                    let heartbeats = self
3279                        .scheduler
3280                        .heartbeats(Duration::from_secs_f32(1.0 / speed));
3281
3282                    // Reserves capacity for new particles
3283                    let new_particle_count = particles_per_length * heartbeats as usize;
3284                    self.particles.reserve(new_particle_count);
3285
3286                    for i in 0..particles_per_length {
3287                        let angle = theta + dtheta * i as f32;
3288                        let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3289                        for j in 0..heartbeats {
3290                            // Sub tick dt
3291                            let dt = (j as f32 / heartbeats as f32) * dt;
3292                            let distance = distance + speed * dt;
3293                            let pos1 = pos + distance * direction - Vec3::unit_z();
3294                            let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
3295                            let time = time + dt as f64;
3296
3297                            self.particles.push(Particle::new_directed(
3298                                Duration::from_secs_f32(0.5),
3299                                time,
3300                                ParticleMode::Poison,
3301                                pos1,
3302                                pos2,
3303                            ));
3304                        }
3305                    }
3306                },
3307                FrontendSpecifier::AcidCloud => {
3308                    let particles_per_height = 5;
3309                    // 1 particle per unit length of arc
3310                    let particles_per_length = arc_length as usize;
3311                    let dtheta = radians / particles_per_length as f32;
3312                    // Scales number of desired heartbeats from speed - thicker arc = higher speed =
3313                    // lower duration = more particles
3314                    let heartbeats = self
3315                        .scheduler
3316                        .heartbeats(Duration::from_secs_f32(1.0 / speed));
3317
3318                    // Reserves capacity for new particles
3319                    let new_particle_count =
3320                        particles_per_length * heartbeats as usize * particles_per_height;
3321                    self.particles.reserve(new_particle_count);
3322
3323                    for i in 0..particles_per_height {
3324                        let height = (i as f32 / (particles_per_height as f32 - 1.0)) * 4.0;
3325                        for j in 0..particles_per_length {
3326                            let angle = theta + dtheta * j as f32;
3327                            let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3328                            for k in 0..heartbeats {
3329                                // Sub tick dt
3330                                let dt = (k as f32 / heartbeats as f32) * dt;
3331                                let distance = distance + speed * dt;
3332                                let pos1 = pos + distance * direction - Vec3::unit_z()
3333                                    + Vec3::unit_z() * height;
3334                                let pos2 = pos1 + direction;
3335                                let time = time + dt as f64;
3336
3337                                self.particles.push(Particle::new_directed(
3338                                    Duration::from_secs_f32(0.5),
3339                                    time,
3340                                    ParticleMode::Poison,
3341                                    pos1,
3342                                    pos2,
3343                                ));
3344                            }
3345                        }
3346                    }
3347                },
3348                FrontendSpecifier::Ink => {
3349                    // 1 particle per unit length of arc
3350                    let particles_per_length = arc_length as usize;
3351                    let dtheta = radians / particles_per_length as f32;
3352                    // Scales number of desired heartbeats from speed - thicker arc = higher speed =
3353                    // lower duration = more particles
3354                    let heartbeats = self
3355                        .scheduler
3356                        .heartbeats(Duration::from_secs_f32(1.0 / speed));
3357
3358                    // Reserves capacity for new particles
3359                    let new_particle_count = particles_per_length * heartbeats as usize;
3360                    self.particles.reserve(new_particle_count);
3361
3362                    for i in 0..particles_per_length {
3363                        let angle = theta + dtheta * i as f32;
3364                        let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3365                        for j in 0..heartbeats {
3366                            // Sub tick dt
3367                            let dt = (j as f32 / heartbeats as f32) * dt;
3368                            let distance = distance + speed * dt;
3369                            let pos1 = pos + distance * direction - Vec3::unit_z();
3370                            let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
3371                            let time = time + dt as f64;
3372
3373                            self.particles.push(Particle::new_directed(
3374                                Duration::from_secs_f32(0.5),
3375                                time,
3376                                ParticleMode::Ink,
3377                                pos1,
3378                                pos2,
3379                            ));
3380                        }
3381                    }
3382                },
3383                FrontendSpecifier::IceSpikes | FrontendSpecifier::Ice => {
3384                    // 1 / 3 the size of terrain voxel
3385                    let scale = 1.0 / 3.0;
3386                    let scaled_distance = distance / scale;
3387                    let scaled_speed = speed / scale;
3388
3389                    // 1 particle per scaled unit length of arc
3390                    let particles_per_length = (0.25 * arc_length / scale) as usize;
3391                    let dtheta = radians / particles_per_length as f32;
3392                    // Scales number of desired heartbeats from speed - thicker arc = higher speed =
3393                    // lower duration = more particles
3394                    let heartbeats = self
3395                        .scheduler
3396                        .heartbeats(Duration::from_secs_f32(3.0 / scaled_speed));
3397
3398                    // Reserves capacity for new particles
3399                    let new_particle_count = particles_per_length * heartbeats as usize;
3400                    self.particles.reserve(new_particle_count);
3401                    // higher wave when wave doesn't require ground
3402                    let wave = if matches!(shockwave.properties.dodgeable, Dodgeable::Jump) {
3403                        0.5
3404                    } else {
3405                        8.0
3406                    };
3407                    // Used to make taller the further out spikes are
3408                    let height_scale = wave + 1.5 * percent;
3409                    for i in 0..particles_per_length {
3410                        let angle = theta + dtheta * i as f32;
3411                        let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3412                        for j in 0..heartbeats {
3413                            // Sub tick dt
3414                            let dt = (j as f32 / heartbeats as f32) * dt;
3415                            let scaled_distance = scaled_distance + scaled_speed * dt;
3416                            let mut pos1 = pos + (scaled_distance * direction).floor() * scale;
3417                            let time = time + dt as f64;
3418
3419                            // Arbitrary number chosen that is large enough to be able to accurately
3420                            // place particles most of the time, but also not too big as to make ray
3421                            // be too large (for performance reasons)
3422                            let half_ray_length = 10.0;
3423                            let mut last_air = false;
3424                            // TODO: Optimize ray to only be cast at most once per block per tick if
3425                            // it becomes an issue.
3426                            // From imbris:
3427                            //      each ray is ~2 us
3428                            //      at 30 FPS, it peaked at 113 rays in a tick
3429                            //      total time was 240 us (although potentially half that is
3430                            //          overhead from the profiling of each ray)
3431                            let _ = terrain
3432                                .ray(
3433                                    pos1 + Vec3::unit_z() * half_ray_length,
3434                                    pos1 - Vec3::unit_z() * half_ray_length,
3435                                )
3436                                .for_each(|block: &Block, pos: Vec3<i32>| {
3437                                    if block.is_solid() && block.get_sprite().is_none() {
3438                                        if last_air {
3439                                            pos1 = pos1.xy().with_z(pos.z as f32 + 1.0);
3440                                            last_air = false;
3441                                        }
3442                                    } else {
3443                                        last_air = true;
3444                                    }
3445                                })
3446                                .cast();
3447
3448                            let get_positions = |a| {
3449                                let pos1 = match a {
3450                                    2 => pos1 + Vec3::unit_x() * scale,
3451                                    3 => pos1 - Vec3::unit_x() * scale,
3452                                    4 => pos1 + Vec3::unit_y() * scale,
3453                                    5 => pos1 - Vec3::unit_y() * scale,
3454                                    _ => pos1,
3455                                };
3456                                let pos2 = if a == 1 {
3457                                    pos1 + Vec3::unit_z() * 5.0 * height_scale
3458                                } else {
3459                                    pos1 + Vec3::unit_z() * 1.0 * height_scale
3460                                };
3461                                (pos1, pos2)
3462                            };
3463
3464                            for a in 1..=5 {
3465                                let (pos1, pos2) = get_positions(a);
3466                                self.particles.push(Particle::new_directed(
3467                                    Duration::from_secs_f32(0.5),
3468                                    time,
3469                                    ParticleMode::IceSpikes,
3470                                    pos1,
3471                                    pos2,
3472                                ));
3473                            }
3474                        }
3475                    }
3476                },
3477            }
3478        }
3479    }
3480
3481    fn upload_particles(&mut self, renderer: &mut Renderer) {
3482        prof_span!("ParticleMgr::upload_particles");
3483        let all_cpu_instances = self
3484            .particles
3485            .iter()
3486            .map(|p| p.instance)
3487            .collect::<Vec<ParticleInstance>>();
3488
3489        // TODO: optimise buffer writes
3490        let gpu_instances = renderer.create_instances(&all_cpu_instances);
3491
3492        self.instances = gpu_instances;
3493    }
3494
3495    pub fn render<'a>(&'a self, drawer: &mut ParticleDrawer<'_, 'a>, scene_data: &SceneData) {
3496        prof_span!("ParticleMgr::render");
3497        if scene_data.particles_enabled {
3498            let model = &self
3499                .model_cache
3500                .get(DEFAULT_MODEL_KEY)
3501                .expect("Expected particle model in cache");
3502
3503            drawer.draw(model, &self.instances);
3504        }
3505    }
3506
3507    pub fn particle_count(&self) -> usize { self.instances.count() }
3508
3509    pub fn particle_count_visible(&self) -> usize { self.instances.count() }
3510}
3511
3512fn default_instances(renderer: &mut Renderer) -> Instances<ParticleInstance> {
3513    let empty_vec = Vec::new();
3514
3515    renderer.create_instances(&empty_vec)
3516}
3517
3518const DEFAULT_MODEL_KEY: &str = "voxygen.voxel.particle";
3519
3520fn default_cache(renderer: &mut Renderer) -> HashMap<&'static str, Model<ParticleVertex>> {
3521    let mut model_cache = HashMap::new();
3522
3523    model_cache.entry(DEFAULT_MODEL_KEY).or_insert_with(|| {
3524        let vox = DotVoxAsset::load_expect(DEFAULT_MODEL_KEY);
3525
3526        // NOTE: If we add texturing we may eventually try to share it among all
3527        // particles in a single atlas.
3528        let max_texture_size = renderer.max_texture_size();
3529        let max_size = Vec2::from(u16::try_from(max_texture_size).unwrap_or(u16::MAX));
3530        let mut greedy = GreedyMesh::new(max_size, crate::mesh::greedy::general_config());
3531
3532        let segment = Segment::from_vox_model_index(&vox.read().0, 0);
3533        let segment_size = segment.size();
3534        let mut mesh = generate_mesh_base_vol_particle(segment, &mut greedy).0;
3535        // Center particle vertices around origin
3536        for vert in mesh.vertices_mut() {
3537            vert.pos[0] -= segment_size.x as f32 / 2.0;
3538            vert.pos[1] -= segment_size.y as f32 / 2.0;
3539            vert.pos[2] -= segment_size.z as f32 / 2.0;
3540        }
3541
3542        // NOTE: Ignoring coloring / lighting for now.
3543        drop(greedy);
3544
3545        renderer
3546            .create_model(&mesh)
3547            .expect("Failed to create particle model")
3548    });
3549
3550    model_cache
3551}
3552
3553/// Accumulates heartbeats to be consumed on the next tick.
3554struct HeartbeatScheduler {
3555    /// Duration = Heartbeat Frequency/Intervals
3556    /// f64 = Last update time
3557    /// u8 = number of heartbeats since last update
3558    /// - if it's more frequent then tick rate, it could be 1 or more.
3559    /// - if it's less frequent then tick rate, it could be 1 or 0.
3560    /// - if it's equal to the tick rate, it could be between 2 and 0, due to
3561    ///   delta time variance etc.
3562    timers: HashMap<Duration, (f64, u8)>,
3563
3564    last_known_time: f64,
3565}
3566
3567impl HeartbeatScheduler {
3568    pub fn new() -> Self {
3569        HeartbeatScheduler {
3570            timers: HashMap::new(),
3571            last_known_time: 0.0,
3572        }
3573    }
3574
3575    /// updates the last elapsed times and elapsed counts
3576    /// this should be called once, and only once per tick.
3577    pub fn maintain(&mut self, now: f64) {
3578        prof_span!("HeartbeatScheduler::maintain");
3579        self.last_known_time = now;
3580
3581        for (frequency, (last_update, heartbeats)) in self.timers.iter_mut() {
3582            // the number of frequency cycles that have occurred.
3583            let total_heartbeats = (now - *last_update) / frequency.as_secs_f64();
3584
3585            // exclude partial frequency cycles
3586            let full_heartbeats = total_heartbeats.floor();
3587
3588            *heartbeats = full_heartbeats as u8;
3589
3590            // the remaining partial frequency cycle, as a decimal.
3591            let partial_heartbeat = total_heartbeats - full_heartbeats;
3592
3593            // the remaining partial frequency cycle, as a unit of time(f64).
3594            let partial_heartbeat_as_time = frequency.mul_f64(partial_heartbeat).as_secs_f64();
3595
3596            // now minus the left over heart beat count precision as seconds,
3597            // Note: we want to preserve incomplete heartbeats, and roll them
3598            // over into the next update.
3599            *last_update = now - partial_heartbeat_as_time;
3600        }
3601    }
3602
3603    /// returns the number of times this duration has elapsed since the last
3604    /// tick:
3605    ///   - if it's more frequent then tick rate, it could be 1 or more.
3606    ///   - if it's less frequent then tick rate, it could be 1 or 0.
3607    ///   - if it's equal to the tick rate, it could be between 2 and 0, due to
3608    ///     delta time variance.
3609    pub fn heartbeats(&mut self, frequency: Duration) -> u8 {
3610        prof_span!("HeartbeatScheduler::heartbeats");
3611        let last_known_time = self.last_known_time;
3612
3613        self.timers
3614            .entry(frequency)
3615            .or_insert_with(|| (last_known_time, 0))
3616            .1
3617    }
3618
3619    pub fn clear(&mut self) { self.timers.clear() }
3620}
3621
3622#[derive(Clone, Copy)]
3623struct Particle {
3624    alive_until: f64, // created_at + lifespan
3625    instance: ParticleInstance,
3626}
3627
3628impl Particle {
3629    fn new(lifespan: Duration, time: f64, mode: ParticleMode, pos: Vec3<f32>) -> Self {
3630        Particle {
3631            alive_until: time + lifespan.as_secs_f64(),
3632            instance: ParticleInstance::new(time, lifespan.as_secs_f32(), mode, pos),
3633        }
3634    }
3635
3636    fn new_directed(
3637        lifespan: Duration,
3638        time: f64,
3639        mode: ParticleMode,
3640        pos1: Vec3<f32>,
3641        pos2: Vec3<f32>,
3642    ) -> Self {
3643        Particle {
3644            alive_until: time + lifespan.as_secs_f64(),
3645            instance: ParticleInstance::new_directed(
3646                time,
3647                lifespan.as_secs_f32(),
3648                mode,
3649                pos1,
3650                pos2,
3651            ),
3652        }
3653    }
3654
3655    fn new_directed_with_collision(
3656        lifespan: Duration,
3657        time: f64,
3658        mode: ParticleMode,
3659        pos1: Vec3<f32>,
3660        pos2: Vec3<f32>,
3661        distance: impl Fn(Vec3<f32>, Vec3<f32>) -> f32,
3662    ) -> Self {
3663        let dir = pos2 - pos1;
3664        let end_distance = pos1.distance(pos2);
3665        let (end_pos, lifespawn) = if end_distance > 0.1 {
3666            let ratio = distance(pos1, pos2) / end_distance;
3667            (pos1 + ratio * dir, lifespan.mul_f32(ratio))
3668        } else {
3669            (pos2, lifespan)
3670        };
3671
3672        Self::new_directed(lifespawn, time, mode, pos1, end_pos)
3673    }
3674}