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