1use super::{FigureMgr, SceneData, Terrain, terrain::BlocksOfInterest};
2use crate::{
3 ecs::comp::{Footsteps, 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::{RAIN_THRESHOLD, terrain::FireplaceType, trail::TOOL_TRAIL_MANIFEST},
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.add_particles(scene_data.particles_chance, 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.add_particles(scene_data.particles_chance, 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.add_particles(
115 scene_data.particles_chance,
116 (60.0 * power.abs()) as usize,
117 || {
118 Particle::new_directed(
119 Duration::from_secs_f32(rng.random_range(0.2..3.0)),
120 time,
121 ParticleMode::EnergyNature,
122 *pos,
123 *pos + Vec3::<f32>::zero()
124 .map(|_| rng.random_range(-1.0..1.0))
125 .normalized()
126 * rng.random_range(1.0..*radius),
127 scene_data,
128 )
129 },
130 );
131 },
132 Some(Reagent::Red) => {
133 self.add_particles(
134 scene_data.particles_chance,
135 (75.0 * power.abs()) as usize,
136 || {
137 Particle::new_directed(
138 Duration::from_millis(500),
139 time,
140 ParticleMode::Explosion,
141 *pos,
142 *pos + Vec3::<f32>::zero()
143 .map(|_| rng.random_range(-1.0..1.0))
144 .normalized()
145 * *radius,
146 scene_data,
147 )
148 },
149 );
150 },
151 Some(Reagent::White) => {
152 self.add_particles(
153 scene_data.particles_chance,
154 (75.0 * power.abs()) as usize,
155 || {
156 Particle::new_directed(
157 Duration::from_millis(500),
158 time,
159 ParticleMode::Ice,
160 *pos,
161 *pos + Vec3::<f32>::zero()
162 .map(|_| rng.random_range(-1.0..1.0))
163 .normalized()
164 * *radius,
165 scene_data,
166 )
167 },
168 );
169 },
170 Some(Reagent::Purple) => {
171 self.add_particles(
172 scene_data.particles_chance,
173 (75.0 * power.abs()) as usize,
174 || {
175 Particle::new_directed(
176 Duration::from_millis(500),
177 time,
178 ParticleMode::CultistFlame,
179 *pos,
180 *pos + Vec3::<f32>::zero()
181 .map(|_| rng.random_range(-1.0..1.0))
182 .normalized()
183 * *radius,
184 scene_data,
185 )
186 },
187 );
188 },
189 Some(Reagent::FireRain) => {
190 self.add_particles(
191 scene_data.particles_chance,
192 (5.0 * power.abs()) as usize,
193 || {
194 Particle::new_directed(
195 Duration::from_millis(300),
196 time,
197 ParticleMode::Explosion,
198 *pos,
199 *pos + Vec3::<f32>::zero()
200 .map(|_| rng.random_range(-1.0..1.0))
201 .normalized()
202 * *radius,
203 scene_data,
204 )
205 },
206 );
207 },
208 Some(Reagent::FireGigas) => {
209 self.add_particles(
210 scene_data.particles_chance,
211 (4.0 * radius.powi(2)) as usize,
212 || {
213 Particle::new_directed(
214 Duration::from_millis(500),
215 time,
216 ParticleMode::FireGigasExplosion,
217 *pos,
218 *pos + Vec3::<f32>::zero()
219 .map(|_| rng.random_range(-1.0..1.0))
220 .normalized()
221 * *radius,
222 scene_data,
223 )
224 },
225 );
226 },
227 Some(Reagent::Earth) => {
228 self.add_particles(scene_data.particles_chance, 150, || {
229 Particle::new(
230 Duration::from_millis(250),
231 time,
232 ParticleMode::BigShrapnel,
233 *pos,
234 scene_data,
235 )
236 })
237 },
238 _ => {},
239 }
240 } else {
241 self.add_particles(
242 scene_data.particles_chance,
243 if reagent.is_some() { 300 } else { 150 },
244 || {
245 Particle::new(
246 Duration::from_millis(if reagent.is_some() {
247 rng.random_range(3000..5000)
248 } else {
249 rng.random_range(1000..2500)
250 }),
251 time,
252 match reagent {
253 Some(Reagent::Blue) => ParticleMode::FireworkBlue,
254 Some(Reagent::Green) => ParticleMode::FireworkGreen,
255 Some(Reagent::Purple) => ParticleMode::FireworkPurple,
256 Some(Reagent::Red) => ParticleMode::FireworkRed,
257 Some(Reagent::White) => ParticleMode::FireworkWhite,
258 Some(Reagent::Yellow) => ParticleMode::FireworkYellow,
259 Some(Reagent::FireRain) => ParticleMode::FireworkYellow,
260 Some(Reagent::FireGigas) => ParticleMode::FireGigasExplosion,
261 Some(Reagent::Earth) => ParticleMode::BigShrapnel,
262 None => ParticleMode::Shrapnel,
263 },
264 *pos,
265 scene_data,
266 )
267 },
268 );
269
270 self.add_particles(
271 scene_data.particles_chance,
272 if reagent.is_some() { 100 } else { 200 },
273 || {
274 Particle::new(
275 Duration::from_secs(4),
276 time,
277 ParticleMode::CampfireSmoke,
278 *pos + Vec3::<f32>::zero()
279 .map(|_| rng.random_range(-1.0..1.0))
280 .normalized()
281 * *radius,
282 scene_data,
283 )
284 },
285 );
286 }
287 },
288 Outcome::BreakBlock { pos, .. } => {
289 self.add_particles(scene_data.particles_chance, 30, || {
291 Particle::new(
292 Duration::from_millis(rng.random_range(1500..2000)),
293 time,
294 ParticleMode::Shrapnel,
295 pos.map(|e| e as f32 + 0.5),
296 scene_data,
297 )
298 });
299 },
300 Outcome::DamagedBlock {
301 pos, stage_changed, ..
302 } => {
303 self.add_particles(
304 scene_data.particles_chance,
305 if *stage_changed { 30 } else { 10 },
306 || {
307 Particle::new(
308 Duration::from_millis(rng.random_range(1000..1500)),
309 time,
310 ParticleMode::Shrapnel,
311 pos.map(|e| e as f32 + 0.5),
312 scene_data,
313 )
314 },
315 );
316 },
317 Outcome::SpriteUnlocked { .. } => {},
318 Outcome::FailedSpriteUnlock { pos } => {
319 self.add_particles(scene_data.particles_chance, 10, || {
321 Particle::new(
322 Duration::from_millis(50),
323 time,
324 ParticleMode::Shrapnel,
325 pos.map(|e| e as f32 + 0.5),
326 scene_data,
327 )
328 });
329 },
330 Outcome::SummonedCreature { pos, body } => match body {
331 Body::BipedSmall(b) if matches!(b.species, body::biped_small::Species::Husk) => {
332 let final_amount =
333 2 * usize::from(self.scheduler.heartbeats(Duration::from_millis(1)));
334 self.add_particles(scene_data.particles_chance, final_amount, || {
335 let start_pos = pos + Vec3::unit_z() * body.height() / 2.0;
336 let end_pos = pos
337 + Vec3::new(
338 2.0 * rng.random::<f32>() - 1.0,
339 2.0 * rng.random::<f32>() - 1.0,
340 0.0,
341 )
342 .normalized()
343 * (body.max_radius() + 4.0)
344 + Vec3::unit_z() * (body.height() + 2.0) * rng.random::<f32>();
345
346 Particle::new_directed(
347 Duration::from_secs_f32(0.5),
348 time,
349 ParticleMode::CultistFlame,
350 start_pos,
351 end_pos,
352 scene_data,
353 )
354 });
355 },
356 Body::BipedSmall(b) if matches!(b.species, body::biped_small::Species::Boreal) => {
357 let final_amount =
358 2 * usize::from(self.scheduler.heartbeats(Duration::from_millis(1)));
359 self.add_particles(scene_data.particles_chance, final_amount, || {
360 let start_pos = pos + Vec3::unit_z() * body.height() / 2.0;
361 let end_pos = pos
362 + Vec3::new(
363 2.0 * rng.random::<f32>() - 1.0,
364 2.0 * rng.random::<f32>() - 1.0,
365 0.0,
366 )
367 .normalized()
368 * (body.max_radius() + 4.0)
369 + Vec3::unit_z() * (body.height() + 20.0) * rng.random::<f32>();
370
371 Particle::new_directed(
372 Duration::from_secs_f32(0.5),
373 time,
374 ParticleMode::GigaSnow,
375 start_pos,
376 end_pos,
377 scene_data,
378 )
379 });
380 },
381 Body::BipedSmall(b) if matches!(b.species, body::biped_small::Species::Ashen) => {
382 let final_amount =
383 2 * usize::from(self.scheduler.heartbeats(Duration::from_millis(1)));
384 self.add_particles(scene_data.particles_chance, final_amount, || {
385 let start_pos = pos + Vec3::unit_z() * body.height() / 2.0;
386 let end_pos = pos
387 + Vec3::new(
388 2.0 * rng.random::<f32>() - 1.0,
389 2.0 * rng.random::<f32>() - 1.0,
390 0.0,
391 )
392 .normalized()
393 * (body.max_radius() + 4.0)
394 + Vec3::unit_z() * (body.height() + 20.0) * rng.random::<f32>();
395
396 Particle::new_directed(
397 Duration::from_secs_f32(0.5),
398 time,
399 ParticleMode::FlameThrower,
400 start_pos,
401 end_pos,
402 scene_data,
403 )
404 });
405 },
406 _ => {},
407 },
408 Outcome::ProjectileHit { pos, target, .. } => {
409 if target.is_some() {
410 let ecs = scene_data.state.ecs();
411 if target
412 .and_then(|target| ecs.read_resource::<IdMaps>().uid_entity(target))
413 .and_then(|entity| {
414 ecs.read_storage::<Body>()
415 .get(entity)
416 .map(|body| body.bleeds())
417 })
418 .unwrap_or(false)
419 {
420 self.add_particles(scene_data.particles_chance, 30, || {
421 Particle::new(
422 Duration::from_millis(250),
423 time,
424 ParticleMode::Blood,
425 *pos,
426 scene_data,
427 )
428 })
429 };
430 };
431 },
432 Outcome::Block { pos, parry, .. } => {
433 if *parry {
434 self.add_particles(scene_data.particles_chance, 10, || {
435 Particle::new(
436 Duration::from_millis(200),
437 time,
438 ParticleMode::GunPowderSpark,
439 *pos + Vec3::unit_z(),
440 scene_data,
441 )
442 });
443 }
444 },
445 Outcome::GroundSlam { pos, .. } => {
446 self.add_particles(scene_data.particles_chance, 100, || {
447 Particle::new(
448 Duration::from_millis(1000),
449 time,
450 ParticleMode::BigShrapnel,
451 *pos,
452 scene_data,
453 )
454 });
455 },
456 Outcome::FireLowShockwave { pos, .. } => {
457 self.add_particles(scene_data.particles_chance, 100, || {
458 Particle::new(
459 Duration::from_millis(1000),
460 time,
461 ParticleMode::FireLowShockwave,
462 *pos,
463 scene_data,
464 )
465 });
466 },
467 Outcome::SurpriseEgg { pos, .. } => {
468 self.add_particles(scene_data.particles_chance, 50, || {
469 Particle::new(
470 Duration::from_millis(1000),
471 time,
472 ParticleMode::SurpriseEgg,
473 *pos,
474 scene_data,
475 )
476 });
477 },
478 Outcome::FlashFreeze { pos, .. } => {
479 let final_amount =
480 2 * usize::from(self.scheduler.heartbeats(Duration::from_millis(1)));
481 self.add_particles(scene_data.particles_chance, final_amount, || {
482 let start_pos = pos + Vec3::unit_z() - 1.0;
483 let end_pos = pos
484 + Vec3::new(
485 4.0 * rng.random::<f32>() - 1.0,
486 4.0 * rng.random::<f32>() - 1.0,
487 0.0,
488 )
489 .normalized()
490 * 1.5
491 + Vec3::unit_z()
492 + 5.0 * rng.random::<f32>();
493
494 Particle::new_directed(
495 Duration::from_secs_f32(0.5),
496 time,
497 ParticleMode::GigaSnow,
498 start_pos,
499 end_pos,
500 scene_data,
501 )
502 });
503 },
504 Outcome::CyclopsCharge { pos } => {
505 self.push_particle(
506 scene_data.particles_chance,
507 Particle::new_directed(
508 Duration::from_secs_f32(rng.random_range(0.1..0.2)),
509 time,
510 ParticleMode::CyclopsCharge,
511 *pos + Vec3::new(0.0, 0.0, 5.3),
512 *pos + Vec3::new(0.0, 0.0, 5.6 + 0.5 * rng.random_range(0.0..0.2)),
513 scene_data,
514 ),
515 );
516 },
517 Outcome::PyroclasmCharge { .. } => {},
518 Outcome::FlamethrowerCharge { pos }
519 | Outcome::FuseCharge { pos }
520 | Outcome::FireBreathCharge { pos } => {
521 self.push_particle(
522 scene_data.particles_chance,
523 Particle::new_directed(
524 Duration::from_secs_f32(rng.random_range(0.1..0.2)),
525 time,
526 ParticleMode::CampfireFire,
527 *pos + Vec3::new(0.0, 0.0, 1.2),
528 *pos + Vec3::new(0.0, 0.0, 1.5 + 0.5 * rng.random_range(0.0..0.2)),
529 scene_data,
530 ),
531 );
532 },
533 Outcome::TerracottaStatueCharge { pos } => {
534 self.push_particle(
535 scene_data.particles_chance,
536 Particle::new_directed(
537 Duration::from_secs_f32(rng.random_range(0.1..0.2)),
538 time,
539 ParticleMode::FireworkYellow,
540 *pos + Vec3::new(0.0, 0.0, 4.0),
541 *pos + Vec3::new(0.0, 0.0, 5.0 + 0.5 * rng.random_range(0.3..0.8)),
542 scene_data,
543 ),
544 );
545 },
546 Outcome::Death { pos, .. } => {
547 self.add_particles(scene_data.particles_chance, 40, || {
548 Particle::new(
549 Duration::from_millis(400 + rng.random_range(0..100)),
550 time,
551 ParticleMode::Death,
552 *pos + Vec3::unit_z()
553 + Vec3::<f32>::zero()
554 .map(|_| rng.random_range(-0.1..0.1))
555 .normalized(),
556 scene_data,
557 )
558 });
559 },
560 Outcome::GroundDig { pos, .. } => {
561 self.add_particles(scene_data.particles_chance, 12, || {
562 Particle::new(
563 Duration::from_millis(200),
564 time,
565 ParticleMode::BigShrapnel,
566 *pos,
567 scene_data,
568 )
569 });
570 },
571 Outcome::TeleportedByPortal { pos, .. } => {
572 self.add_particles(scene_data.particles_chance, 80, || {
573 Particle::new_directed(
574 Duration::from_millis(500),
575 time,
576 ParticleMode::CultistFlame,
577 *pos,
578 pos + Vec3::unit_z()
579 + Vec3::zero()
580 .map(|_: f32| rng.random_range(-0.1..0.1))
581 .normalized()
582 * 2.0,
583 scene_data,
584 )
585 });
586 },
587 Outcome::ClayGolemDash { pos, .. } => {
588 self.add_particles(scene_data.particles_chance, 100, || {
589 Particle::new(
590 Duration::from_millis(1000),
591 time,
592 ParticleMode::ClayShrapnel,
593 *pos,
594 scene_data,
595 )
596 });
597 },
598 Outcome::HeadLost { uid, head } => {
599 if let Some(entity) = scene_data
600 .state
601 .ecs()
602 .read_resource::<IdMaps>()
603 .uid_entity(*uid)
604 && let Some(pos) = scene_data.state.read_component_copied::<Pos>(entity)
605 {
606 let heads = figure_mgr.get_heads(scene_data, entity);
607 let head_pos = pos.0 + heads.get(*head).copied().unwrap_or_default();
608
609 self.add_particles(scene_data.particles_chance, 40, || {
610 Particle::new(
611 Duration::from_millis(1000),
612 time,
613 ParticleMode::Death,
614 head_pos
615 + Vec3::<f32>::zero()
616 .map(|_| rng.random_range(-0.1..0.1))
617 .normalized(),
618 scene_data,
619 )
620 });
621 };
622 },
623 Outcome::Splash {
624 vel,
625 pos,
626 mass,
627 kind,
628 } => {
629 let mode = match kind {
630 comp::fluid_dynamics::LiquidKind::Water => ParticleMode::WaterFoam,
631 comp::fluid_dynamics::LiquidKind::Lava => ParticleMode::CampfireFire,
632 };
633 let magnitude = (-vel.z).max(0.0);
634 let energy = mass * magnitude;
635 if energy > 0.0 {
636 let count = ((2.5 * energy.sqrt()).ceil() as usize).min(500);
637 let mut i = 0;
638 let r = 0.5 / count as f32;
639 self.add_particles(scene_data.particles_chance, count, || {
640 let t = i as f32 / count as f32 + rng.random_range(-r..=r);
641 i += 1;
642 let angle = t * TAU;
643 let s = angle.sin();
644 let c = angle.cos();
645 let energy = energy
646 * f32::abs(
647 rng.random_range(0.0..1.0) + rng.random_range(0.0..1.0) - 0.5,
648 );
649
650 let axis = -Vec3::unit_z();
651 let plane = Vec3::new(c, s, 0.0);
652
653 let pos = *pos + plane * rng.random_range(0.0..0.5);
654
655 let energy = energy.sqrt() * 0.5;
656
657 let dir = plane * (1.0 + energy) - axis * energy * 1.5;
658
659 Particle::new_directed(
660 Duration::from_millis(4000),
661 time,
662 mode,
663 pos,
664 pos + dir,
665 scene_data,
666 )
667 });
668 }
669 },
670 Outcome::Transformation { pos } => {
671 self.add_particles(scene_data.particles_chance, 100, || {
672 Particle::new(
673 Duration::from_millis(1400),
674 time,
675 ParticleMode::Transformation,
676 *pos,
677 scene_data,
678 )
679 });
680 },
681 Outcome::FirePillarIndicator { pos, radius } => {
682 self.add_particles(
683 scene_data.particles_chance,
684 radius.powi(2) as usize / 2,
685 || {
686 Particle::new_directed(
687 Duration::from_millis(500),
688 time,
689 ParticleMode::FirePillarIndicator,
690 *pos + 0.2 * Vec3::<f32>::unit_z(),
691 *pos + 0.2 * Vec3::<f32>::unit_z() + *radius * Vec3::unit_x(),
694 scene_data,
695 )
696 },
697 );
698 },
699 Outcome::ProjectileShot { .. }
700 | Outcome::Beam { .. }
701 | Outcome::ExpChange { .. }
702 | Outcome::SkillPointGain { .. }
703 | Outcome::ComboChange { .. }
704 | Outcome::HealthChange { .. }
705 | Outcome::PoiseChange { .. }
706 | Outcome::Utterance { .. }
707 | Outcome::IceSpikes { .. }
708 | Outcome::IceCrack { .. }
709 | Outcome::Glider { .. }
710 | Outcome::Whoosh { .. }
711 | Outcome::Swoosh { .. }
712 | Outcome::Slash { .. }
713 | Outcome::Bleep { .. }
714 | Outcome::Charge { .. }
715 | Outcome::Steam { .. }
716 | Outcome::FireShockwave { .. }
717 | Outcome::PortalActivated { .. }
718 | Outcome::FromTheAshes { .. }
719 | Outcome::LaserBeam { .. } => {},
720 }
721 }
722
723 pub fn maintain(
724 &mut self,
725 renderer: &mut Renderer,
726 scene_data: &SceneData,
727 terrain: &Terrain<TerrainChunk>,
728 figure_mgr: &FigureMgr,
729 lights: &mut Vec<Light>,
730 ) {
731 prof_span!("ParticleMgr::maintain");
732 if scene_data.particles_enabled {
733 self.scheduler.maintain(scene_data.state.get_time());
735
736 self.particles
738 .retain(|p| p.alive_until > scene_data.state.get_time());
739
740 self.maintain_equipment_particles(scene_data, figure_mgr);
742 self.maintain_body_particles(scene_data);
743 self.maintain_char_state_particles(scene_data, figure_mgr);
744 self.maintain_beam_particles(scene_data, lights);
745 self.maintain_block_particles(scene_data, terrain, figure_mgr);
746 self.maintain_shockwave_particles(scene_data);
747 self.maintain_aura_particles(scene_data);
748 self.maintain_buff_particles(scene_data);
749 self.maintain_fluid_particles(scene_data);
750 self.maintain_marker_particles(scene_data);
751 self.maintain_arcing_particles(scene_data);
752 self.maintain_pool_particles(scene_data);
753
754 self.upload_particles(renderer);
755 } else {
756 if !self.particles.is_empty() {
758 self.particles.clear();
759 self.upload_particles(renderer);
760 }
761
762 self.scheduler.clear();
764 }
765 }
766
767 fn push_particle(&mut self, chance: f32, particle: Particle) {
768 let mut rng = rand::rng();
769 if rng.random_bool(chance.into()) {
770 self.particles.push(particle);
771 }
772 }
773
774 fn add_particles<F>(&mut self, chance: f32, amount: usize, f: F)
775 where
776 F: FnMut() -> Particle,
777 {
778 self.particles
779 .resize_with(self.particles.len() + (amount as f32 * chance) as usize, f);
780 }
781
782 fn maintain_equipment_particles(&mut self, scene_data: &SceneData, figure_mgr: &FigureMgr) {
783 prof_span!("ParticleMgr::maintain_armor_particles");
784 let ecs = scene_data.state.ecs();
785
786 for (entity, body, scale, inv, physics) in (
787 &ecs.entities(),
788 &ecs.read_storage::<Body>(),
789 ecs.read_storage::<Scale>().maybe(),
790 &ecs.read_storage::<Inventory>(),
791 &ecs.read_storage::<PhysicsState>(),
792 )
793 .join()
794 {
795 for item in inv.equipped_items() {
796 if let ItemDefinitionId::Simple(str) = item.item_definition_id() {
797 match &*str {
798 "common.items.armor.misc.head.pipe" => self.maintain_pipe_particles(
799 scene_data, figure_mgr, entity, body, scale, physics,
800 ),
801 "common.items.npc_weapons.sword.gigas_fire_sword" => {
802 if let Some(trail_points) = TOOL_TRAIL_MANIFEST.get(item) {
803 self.maintain_gigas_fire_sword_particles(
804 scene_data,
805 figure_mgr,
806 trail_points,
807 entity,
808 )
809 }
810 },
811 _ => {},
812 }
813 }
814 }
815 }
816 }
817
818 fn maintain_pipe_particles(
819 &mut self,
820 scene_data: &SceneData,
821 figure_mgr: &FigureMgr,
822 entity: Entity,
823 body: &Body,
824 scale: Option<&Scale>,
825 physics: &PhysicsState,
826 ) {
827 prof_span!("ParticleMgr::maintain_pipe_particles");
828 if physics
829 .in_liquid()
830 .is_none_or(|depth| body.eye_height(scale.map_or(1.0, |scale| scale.0)) > depth)
831 {
832 let Body::Humanoid(body) = body else {
833 return;
834 };
835 let Some(state) = figure_mgr.states.character_states.get(&entity) else {
836 return;
837 };
838
839 use body::humanoid::{BodyType::*, Species::*};
841 let pipe_offset = match (body.species, body.body_type) {
842 (Orc, Male) => Vec3::new(5.5, 10.5, 0.0),
843 (Orc, Female) => Vec3::new(4.5, 10.0, -2.5),
844 (Human, Male) => Vec3::new(4.5, 12.0, -3.0),
845 (Human, Female) => Vec3::new(4.5, 11.5, -3.0),
846 (Elf, Male) => Vec3::new(4.5, 12.0, -3.0),
847 (Elf, Female) => Vec3::new(4.5, 9.5, -3.0),
848 (Dwarf, Male) => Vec3::new(4.5, 11.0, -4.0),
849 (Dwarf, Female) => Vec3::new(4.5, 11.0, -3.0),
850 (Draugr, Male) => Vec3::new(4.5, 9.5, -0.75),
851 (Draugr, Female) => Vec3::new(4.5, 9.5, -2.0),
852 (Danari, Male) => Vec3::new(4.5, 10.5, -1.25),
853 (Danari, Female) => Vec3::new(4.5, 10.5, -1.25),
854 };
855
856 let mut rng = rand::rng();
857 let dt = scene_data.state.get_delta_time();
858 if rng.random_bool((0.25 * dt as f64).min(1.0)) {
859 let time = scene_data.state.get_time();
860 self.add_particles(scene_data.particles_chance, 10, || {
861 Particle::new(
862 Duration::from_millis(1500),
863 time,
864 ParticleMode::PipeSmoke,
865 state.wpos_of(state.computed_skeleton.head.mul_point(pipe_offset)),
866 scene_data,
867 )
868 });
869 }
870 }
871 }
872
873 fn maintain_gigas_fire_sword_particles(
874 &mut self,
875 scene_data: &SceneData,
876 figure_mgr: &FigureMgr,
877 trail_points: (Vec3<f32>, Vec3<f32>),
878 entity: Entity,
879 ) {
880 prof_span!("ParticleMgr::maintain_gigas_fire_sword_particles");
881 let Some(state) = figure_mgr.states.biped_large_states.get(&entity) else {
882 return;
883 };
884
885 let mut rng = rand::rng();
886 let time = scene_data.state.get_time();
887 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(10)) {
888 let blade_offset = trail_points.0
889 + rng.random_range(0.0..1.0_f32) * (trail_points.1 - trail_points.0)
890 + rng.random_range(-5.0..5.0) * Vec3::<f32>::unit_y()
891 + rng.random_range(-1.0..1.0) * Vec3::<f32>::unit_x();
892
893 let start_pos = state.wpos_of(state.computed_skeleton.main.mul_point(blade_offset));
894 let end_pos = start_pos + rng.random_range(1.0..2.0) * Vec3::<f32>::unit_z();
895
896 self.push_particle(
897 scene_data.particles_chance,
898 Particle::new_directed(
899 Duration::from_millis(500),
900 time,
901 ParticleMode::FlameThrower,
902 start_pos,
903 end_pos,
904 scene_data,
905 ),
906 );
907 }
908 }
909
910 fn maintain_fluid_particles(&mut self, scene_data: &SceneData) {
911 prof_span!("ParticleMgr::maintain_fluid_particles");
912 let ecs = scene_data.state.ecs();
913 for (pos, vel, collider) in (
914 &ecs.read_storage::<Pos>(),
915 &ecs.read_storage::<Vel>(),
916 &ecs.read_storage::<comp::Collider>(),
917 )
918 .join()
919 {
920 const CAVITATION_SPEED: f32 = 20.0;
923 if matches!(collider, comp::Collider::Point)
924 && let speed = vel.0.magnitude()
925 && speed > CAVITATION_SPEED
926 && scene_data
927 .state
928 .terrain()
929 .get((pos.0 + Vec3::unit_z()).as_())
931 .is_ok_and(|b| b.kind() == BlockKind::Water)
932 {
933 let mut rng = rand::rng();
934 let time = scene_data.state.get_time();
935 let dt = scene_data.state.get_delta_time();
936 for _ in 0..self
937 .scheduler
938 .heartbeats(Duration::from_millis(1000 / speed.min(500.0) as u64))
939 {
940 self.push_particle(
941 scene_data.particles_chance,
942 Particle::new(
943 Duration::from_secs(1),
944 time,
945 ParticleMode::Bubble,
946 pos.0.map(|e| e + rng.random_range(-0.1..0.1))
947 - vel.0 * dt * rng.random::<f32>(),
948 scene_data,
949 ),
950 );
951 }
952 }
953 }
954 }
955
956 fn maintain_body_particles(&mut self, scene_data: &SceneData) {
957 prof_span!("ParticleMgr::maintain_body_particles");
958 let ecs = scene_data.state.ecs();
959 for (body, interpolated, vel) in (
960 &ecs.read_storage::<Body>(),
961 &ecs.read_storage::<Interpolated>(),
962 ecs.read_storage::<Vel>().maybe(),
963 )
964 .join()
965 {
966 match body {
967 Body::Object(object::Body::CampfireLit) => {
968 self.maintain_campfirelit_particles(scene_data, interpolated.pos, vel)
969 },
970 Body::Object(object::Body::BarrelOrgan) => {
971 self.maintain_barrel_organ_particles(scene_data, interpolated.pos, vel)
972 },
973 Body::Object(object::Body::BoltFire) => {
974 self.maintain_boltfire_particles(scene_data, interpolated.pos, vel)
975 },
976 Body::Object(object::Body::NapalmShot) => {
977 self.maintain_napalmshot_particles(scene_data, interpolated.pos, vel)
978 },
979 Body::Object(object::Body::BoltFireBig) => {
980 self.maintain_boltfirebig_particles(scene_data, interpolated.pos, vel)
981 },
982 Body::Object(object::Body::FireRainDrop) => {
983 self.maintain_fireraindrop_particles(scene_data, interpolated.pos, vel)
984 },
985 Body::Object(object::Body::BoltNature) => {
986 self.maintain_boltnature_particles(scene_data, interpolated.pos, vel)
987 },
988 Body::Object(object::Body::Tornado) => {
989 self.maintain_tornado_particles(scene_data, interpolated.pos)
990 },
991 Body::Object(object::Body::FieryTornado) => {
992 self.maintain_fiery_tornado_particles(scene_data, interpolated.pos)
993 },
994 Body::Object(object::Body::Mine) => {
995 self.maintain_mine_particles(scene_data, interpolated.pos)
996 },
997 Body::Object(
998 object::Body::Bomb
999 | object::Body::FireworkBlue
1000 | object::Body::FireworkGreen
1001 | object::Body::FireworkPurple
1002 | object::Body::FireworkRed
1003 | object::Body::FireworkWhite
1004 | object::Body::FireworkYellow
1005 | object::Body::IronPikeBomb,
1006 ) => self.maintain_bomb_particles(scene_data, interpolated.pos, vel),
1007 Body::Object(object::Body::PortalActive) => {
1008 self.maintain_active_portal_particles(scene_data, interpolated.pos)
1009 },
1010 Body::Object(object::Body::Portal) => {
1011 self.maintain_portal_particles(scene_data, interpolated.pos)
1012 },
1013 Body::Object(object::Body::NapalmPool) => {
1014 self.maintain_napalmpool_particles(scene_data, interpolated.pos)
1015 },
1016 Body::Object(object::Body::PyroclasmBolt) => {
1017 self.maintain_pyroclasm_bolt_particles(scene_data, interpolated.pos, vel)
1018 },
1019 Body::BipedLarge(biped_large::Body {
1020 species: biped_large::Species::Gigasfire,
1021 ..
1022 }) => self.maintain_fire_gigas_particles(scene_data, interpolated.pos),
1023 _ => {},
1024 }
1025 }
1026 }
1027
1028 fn maintain_pyroclasm_bolt_particles(
1029 &mut self,
1030 scene_data: &SceneData,
1031 pos: Vec3<f32>,
1032 vel: Option<&Vel>,
1033 ) {
1034 let time = scene_data.state.get_time();
1035 let mut rng = rand::rng();
1036 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
1037
1038 let fwd = vel
1039 .map(|v| v.0)
1040 .unwrap_or(Vec3::unit_y())
1041 .try_normalized()
1042 .unwrap_or(Vec3::unit_y());
1043
1044 self.add_particles(
1045 scene_data.particles_chance,
1046 usize::from(heartbeats) * 6,
1047 || {
1048 let spread = Vec3::new(
1049 rng.random_range(-0.2..0.2_f32),
1050 rng.random_range(-0.2..0.2_f32),
1051 rng.random_range(-0.2..0.2_f32),
1052 );
1053 let spawn = pos + spread;
1054 let trail_dir = (fwd - Vec3::unit_z() * 0.4).try_normalized().unwrap_or(fwd);
1055 let tail = spawn - trail_dir * rng.random_range(0.5..2.5_f32);
1056
1057 Particle::new_directed(
1058 Duration::from_millis(150),
1059 time,
1060 ParticleMode::FlameThrower,
1061 spawn,
1062 tail,
1063 scene_data,
1064 )
1065 },
1066 );
1067 }
1068
1069 fn maintain_pyroclasm_charge_particles(
1070 &mut self,
1071 scene_data: &SceneData,
1072 pos: Vec3<f32>,
1073 progress: f32,
1074 height: f32,
1075 radius: f32,
1076 ) {
1077 let time = scene_data.state.get_time();
1078 let mut rng = rand::rng();
1079 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(8));
1080
1081 const LATITUDE_MODIFIERS: [(f32, f32, f32); 5] = [
1084 (-0.85, 0.50, 1.0),
1085 (-0.50, 0.80, -1.0),
1086 (0.00, 0.95, 1.0),
1087 (0.50, 0.80, -1.0),
1088 (0.85, 0.50, 1.0),
1089 ];
1090
1091 let scale = 1.0 - progress;
1092 let center_z = height;
1093
1094 for &(sin_lat, cos_lat, dir) in &LATITUDE_MODIFIERS {
1095 let r = radius * cos_lat * scale;
1097 let z = center_z + radius * sin_lat * (1.0 - progress);
1098
1099 let particle_density_hb: usize = (radius * 4.0).floor() as usize;
1101 let particle_density_lifespan: u64 = (radius * 75.0).floor() as u64;
1102
1103 self.add_particles(
1104 scene_data.particles_chance,
1105 usize::from(heartbeats) * particle_density_hb,
1106 || {
1107 let theta = rng.random_range(0.0..TAU);
1108 let spawn = pos + Vec3::new(theta.cos() * r, theta.sin() * r, z);
1109 let tangent = Vec3::new(-theta.sin() * dir, theta.cos() * dir, 0.06 * dir);
1110 let end_pos = spawn + tangent * 0.55;
1111
1112 let blue_prob = (progress / 0.9).clamp(0.0, 1.0) as f64;
1113
1114 let mode = if rng.random_bool(1.0 - blue_prob) {
1115 ParticleMode::FlameThrower
1116 } else {
1117 ParticleMode::FlamethrowerBlue
1118 };
1119
1120 let lifespan: Duration = Duration::from_millis(particle_density_lifespan);
1122 Particle {
1123 alive_until: time + lifespan.as_secs_f64(),
1124 instance: ParticleInstance::new_directed(
1125 time,
1126 lifespan.as_secs_f32(),
1127 mode,
1128 spawn,
1129 end_pos,
1130 Vec2::zero(),
1131 ),
1132 }
1133 },
1134 );
1135 }
1136 }
1137
1138 fn maintain_napalmshot_particles(
1139 &mut self,
1140 scene_data: &SceneData,
1141 pos: Vec3<f32>,
1142 vel: Option<&Vel>,
1143 ) {
1144 prof_span!("ParticleMgr::maintain_napalmshot_particles");
1145 let time = scene_data.state.get_time();
1146 let dt = scene_data.state.get_delta_time();
1147 let mut rng = rand::rng();
1148
1149 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(20)) {
1150 self.push_particle(
1151 scene_data.particles_chance,
1152 Particle::new(
1153 Duration::from_millis(150),
1154 time,
1155 ParticleMode::GunPowderSpark,
1156 pos.map(|e| e + rng.random_range(-0.2..0.2))
1157 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.random::<f32>()),
1158 scene_data,
1159 ),
1160 );
1161 }
1162 }
1163
1164 fn maintain_napalmpool_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1165 prof_span!("ParticleMgr::maintain_napalmpool_particles");
1166 let time = scene_data.state.get_time();
1167 let mut rng = rand::rng();
1168
1169 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(50)) {
1171 self.push_particle(
1172 scene_data.particles_chance,
1173 Particle::new(
1174 Duration::from_millis(1500),
1175 time,
1176 ParticleMode::GunPowderSpark,
1177 pos.map(|e| e + rng.random_range(-0.4..0.4)),
1178 scene_data,
1179 ),
1180 );
1181 }
1182
1183 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(50)) {
1185 self.push_particle(
1186 scene_data.particles_chance,
1187 Particle::new(
1188 Duration::from_millis(1500),
1189 time,
1190 ParticleMode::CampfireSmoke,
1191 pos.map(|e| e + rng.random_range(-0.4..0.4)),
1192 scene_data,
1193 ),
1194 );
1195 }
1196 }
1197
1198 fn maintain_fire_gigas_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1199 let time = scene_data.state.get_time();
1200 let mut rng = rand::rng();
1201
1202 if rng.random_bool(0.05) {
1203 self.add_particles(scene_data.particles_chance, 1, || {
1204 let rand_offset = Vec3::new(
1205 rng.random_range(-5.0..5.0),
1206 rng.random_range(-5.0..5.0),
1207 rng.random_range(7.0..15.0),
1208 );
1209
1210 Particle::new(
1211 Duration::from_secs_f32(30.0),
1212 time,
1213 ParticleMode::FireGigasAsh,
1214 pos + rand_offset,
1215 scene_data,
1216 )
1217 });
1218 }
1219 }
1220
1221 fn maintain_hydra_tail_swipe_particles(
1222 &mut self,
1223 scene_data: &SceneData,
1224 figure_mgr: &FigureMgr,
1225 entity: Entity,
1226 pos: Vec3<f32>,
1227 body: &Body,
1228 state: &CharacterState,
1229 inventory: Option<&Inventory>,
1230 ) {
1231 let Some(ability_id) = state
1232 .ability_info()
1233 .and_then(|info| info.ability.map(|a| a.ability_id(Some(state), inventory)))
1234 else {
1235 return;
1236 };
1237
1238 if ability_id != Some("common.abilities.custom.hydra.tail_swipe") {
1239 return;
1240 }
1241
1242 let Some(stage_section) = state.stage_section() else {
1243 return;
1244 };
1245
1246 let particle_count = match stage_section {
1247 StageSection::Charge => 1,
1248 StageSection::Action => 10,
1249 _ => return,
1250 };
1251
1252 let Some(skeleton) = figure_mgr
1253 .states
1254 .quadruped_low_states
1255 .get(&entity)
1256 .map(|state| &state.computed_skeleton)
1257 else {
1258 return;
1259 };
1260 let Some(attr) = anim::quadruped_low::SkeletonAttr::try_from(body).ok() else {
1261 return;
1262 };
1263
1264 let start = (skeleton.tail_front * Vec4::unit_w()).xyz();
1265 let end = (skeleton.tail_rear * Vec4::new(0.0, -attr.tail_rear_length, 0.0, 1.0)).xyz();
1266
1267 let start = pos + start;
1268 let end = pos + end;
1269
1270 let time = scene_data.state.get_time();
1271 let mut rng = rand::rng();
1272
1273 let final_amount =
1274 particle_count * self.scheduler.heartbeats(Duration::from_millis(33)) as usize;
1275 self.add_particles(scene_data.particles_chance, final_amount, || {
1276 let t = rng.random_range(0.0..1.0);
1277 let p = start * t + end * (1.0 - t) - Vec3::new(0.0, 0.0, 0.5);
1278
1279 Particle::new(
1280 Duration::from_millis(500),
1281 time,
1282 ParticleMode::GroundShockwave,
1283 p,
1284 scene_data,
1285 )
1286 });
1287 }
1288
1289 fn maintain_campfirelit_particles(
1290 &mut self,
1291 scene_data: &SceneData,
1292 pos: Vec3<f32>,
1293 vel: Option<&Vel>,
1294 ) {
1295 prof_span!("ParticleMgr::maintain_campfirelit_particles");
1296 let time = scene_data.state.get_time();
1297 let dt = scene_data.state.get_delta_time();
1298 let mut rng = rand::rng();
1299
1300 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(25)) {
1301 self.push_particle(
1302 scene_data.particles_chance,
1303 Particle::new(
1304 Duration::from_millis(800),
1305 time,
1306 ParticleMode::CampfireFire,
1307 pos + Vec2::broadcast(())
1308 .map(|_| rand::rng().random_range(-0.3..0.3))
1309 .with_z(0.1),
1310 scene_data,
1311 ),
1312 );
1313 }
1314
1315 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(50)) {
1316 self.push_particle(
1317 scene_data.particles_chance,
1318 Particle::new(
1319 Duration::from_secs(10),
1320 time,
1321 ParticleMode::CampfireSmoke,
1322 pos.map(|e| e + rand::rng().random_range(-0.25..0.25))
1323 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.random::<f32>()),
1324 scene_data,
1325 ),
1326 );
1327 }
1328 }
1329
1330 fn maintain_barrel_organ_particles(
1331 &mut self,
1332 scene_data: &SceneData,
1333 pos: Vec3<f32>,
1334 vel: Option<&Vel>,
1335 ) {
1336 prof_span!("ParticleMgr::maintain_barrel_organ_particles");
1337 let time = scene_data.state.get_time();
1338 let dt = scene_data.state.get_delta_time();
1339 let mut rng = rand::rng();
1340
1341 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(20)) {
1342 self.push_particle(
1343 scene_data.particles_chance,
1344 Particle::new(
1345 Duration::from_millis(250),
1346 time,
1347 ParticleMode::BarrelOrgan,
1348 pos,
1349 scene_data,
1350 ),
1351 );
1352
1353 self.push_particle(
1354 scene_data.particles_chance,
1355 Particle::new(
1356 Duration::from_secs(10),
1357 time,
1358 ParticleMode::BarrelOrgan,
1359 pos.map(|e| e + rand::rng().random_range(-0.25..0.25))
1360 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.random::<f32>()),
1361 scene_data,
1362 ),
1363 );
1364 }
1365 }
1366
1367 fn maintain_boltfire_particles(
1368 &mut self,
1369 scene_data: &SceneData,
1370 pos: Vec3<f32>,
1371 vel: Option<&Vel>,
1372 ) {
1373 prof_span!("ParticleMgr::maintain_boltfire_particles");
1374 let time = scene_data.state.get_time();
1375 let dt = scene_data.state.get_delta_time();
1376 let mut rng = rand::rng();
1377
1378 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(4)) {
1379 self.push_particle(
1380 scene_data.particles_chance,
1381 Particle::new(
1382 Duration::from_millis(500),
1383 time,
1384 ParticleMode::CampfireFire,
1385 pos.map(|e| e + rng.random_range(-0.25..0.25))
1386 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.random::<f32>()),
1387 scene_data,
1388 ),
1389 );
1390 self.push_particle(
1391 scene_data.particles_chance,
1392 Particle::new(
1393 Duration::from_secs(1),
1394 time,
1395 ParticleMode::CampfireSmoke,
1396 pos.map(|e| e + rng.random_range(-0.25..0.25))
1397 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.random::<f32>()),
1398 scene_data,
1399 ),
1400 );
1401 }
1402 }
1403
1404 fn maintain_boltfirebig_particles(
1405 &mut self,
1406 scene_data: &SceneData,
1407 pos: Vec3<f32>,
1408 vel: Option<&Vel>,
1409 ) {
1410 prof_span!("ParticleMgr::maintain_boltfirebig_particles");
1411 let time = scene_data.state.get_time();
1412 let dt = scene_data.state.get_delta_time();
1413 let mut rng = rand::rng();
1414
1415 let final_amount = usize::from(self.scheduler.heartbeats(Duration::from_millis(2)));
1417 self.add_particles(scene_data.particles_chance, final_amount, || {
1418 Particle::new(
1419 Duration::from_millis(500),
1420 time,
1421 ParticleMode::CampfireFire,
1422 pos.map(|e| e + rng.random_range(-0.25..0.25))
1423 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.random::<f32>()),
1424 scene_data,
1425 )
1426 });
1427
1428 let final_amount = usize::from(self.scheduler.heartbeats(Duration::from_millis(5)));
1430 self.add_particles(scene_data.particles_chance, final_amount, || {
1431 Particle::new(
1432 Duration::from_secs(2),
1433 time,
1434 ParticleMode::CampfireSmoke,
1435 pos.map(|e| e + rng.random_range(-0.25..0.25))
1436 + vel.map_or(Vec3::zero(), |v| -v.0 * dt),
1437 scene_data,
1438 )
1439 });
1440 }
1441
1442 fn maintain_fireraindrop_particles(
1443 &mut self,
1444 scene_data: &SceneData,
1445 pos: Vec3<f32>,
1446 vel: Option<&Vel>,
1447 ) {
1448 prof_span!("ParticleMgr::maintain_fireraindrop_particles");
1449 let time = scene_data.state.get_time();
1450 let dt = scene_data.state.get_delta_time();
1451 let mut rng = rand::rng();
1452
1453 let final_amount = usize::from(self.scheduler.heartbeats(Duration::from_millis(100)));
1455 self.add_particles(scene_data.particles_chance, final_amount, || {
1456 Particle::new(
1457 Duration::from_millis(300),
1458 time,
1459 ParticleMode::FieryDropletTrace,
1460 pos.map(|e| e + rng.random_range(-0.25..0.25))
1461 + Vec3::new(0.0, 0.0, 0.5)
1462 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.random::<f32>()),
1463 scene_data,
1464 )
1465 });
1466 }
1467
1468 fn maintain_boltnature_particles(
1469 &mut self,
1470 scene_data: &SceneData,
1471 pos: Vec3<f32>,
1472 vel: Option<&Vel>,
1473 ) {
1474 let time = scene_data.state.get_time();
1475 let dt = scene_data.state.get_delta_time();
1476 let mut rng = rand::rng();
1477
1478 let final_amount = usize::from(self.scheduler.heartbeats(Duration::from_millis(2)));
1480 self.add_particles(scene_data.particles_chance, final_amount, || {
1481 Particle::new(
1482 Duration::from_millis(500),
1483 time,
1484 ParticleMode::CampfireSmoke,
1485 pos.map(|e| e + rng.random_range(-0.25..0.25))
1486 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.random::<f32>()),
1487 scene_data,
1488 )
1489 });
1490 }
1491
1492 fn maintain_tornado_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1493 let time = scene_data.state.get_time();
1494 let mut rng = rand::rng();
1495
1496 let final_amount = usize::from(self.scheduler.heartbeats(Duration::from_millis(5)));
1498 self.add_particles(scene_data.particles_chance, final_amount, || {
1499 Particle::new(
1500 Duration::from_millis(1000),
1501 time,
1502 ParticleMode::Tornado,
1503 pos.map(|e| e + rng.random_range(-0.25..0.25)),
1504 scene_data,
1505 )
1506 });
1507 }
1508
1509 fn maintain_fiery_tornado_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1510 let time = scene_data.state.get_time();
1511 let mut rng = rand::rng();
1512
1513 let final_amount = usize::from(self.scheduler.heartbeats(Duration::from_millis(5)));
1515 self.add_particles(scene_data.particles_chance, final_amount, || {
1516 Particle::new(
1517 Duration::from_millis(1000),
1518 time,
1519 ParticleMode::FieryTornado,
1520 pos.map(|e| e + rng.random_range(-0.25..0.25)),
1521 scene_data,
1522 )
1523 });
1524 }
1525
1526 fn maintain_bomb_particles(
1527 &mut self,
1528 scene_data: &SceneData,
1529 pos: Vec3<f32>,
1530 vel: Option<&Vel>,
1531 ) {
1532 prof_span!("ParticleMgr::maintain_bomb_particles");
1533 let time = scene_data.state.get_time();
1534 let dt = scene_data.state.get_delta_time();
1535 let mut rng = rand::rng();
1536
1537 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(10)) {
1538 self.push_particle(
1540 scene_data.particles_chance,
1541 Particle::new(
1542 Duration::from_millis(1500),
1543 time,
1544 ParticleMode::GunPowderSpark,
1545 pos,
1546 scene_data,
1547 ),
1548 );
1549
1550 self.push_particle(
1552 scene_data.particles_chance,
1553 Particle::new(
1554 Duration::from_secs(2),
1555 time,
1556 ParticleMode::CampfireSmoke,
1557 pos + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.random::<f32>()),
1558 scene_data,
1559 ),
1560 );
1561 }
1562 }
1563
1564 fn maintain_active_portal_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1565 prof_span!("ParticleMgr::maintain_active_portal_particles");
1566
1567 let time = scene_data.state.get_time();
1568 let mut rng = rand::rng();
1569
1570 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(5)) {
1571 let outer_pos =
1572 pos + (Vec2::unit_x().rotated_z(rng.random_range((0.)..PI * 2.)) * 2.7).with_z(0.);
1573
1574 self.push_particle(
1575 scene_data.particles_chance,
1576 Particle::new_directed(
1577 Duration::from_secs_f32(rng.random_range(0.4..0.8)),
1578 time,
1579 ParticleMode::CultistFlame,
1580 outer_pos,
1581 outer_pos + Vec3::unit_z() * rng.random_range(5.0..7.0),
1582 scene_data,
1583 ),
1584 );
1585 }
1586 }
1587
1588 fn maintain_portal_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1589 prof_span!("ParticleMgr::maintain_portal_particles");
1590
1591 let time = scene_data.state.get_time();
1592 let mut rng = rand::rng();
1593
1594 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(150)) {
1595 let outer_pos = pos
1596 + (Vec2::unit_x().rotated_z(rng.random_range((0.)..PI * 2.))
1597 * rng.random_range(1.0..2.9))
1598 .with_z(0.);
1599
1600 self.push_particle(
1601 scene_data.particles_chance,
1602 Particle::new_directed(
1603 Duration::from_secs_f32(rng.random_range(0.5..3.0)),
1604 time,
1605 ParticleMode::CultistFlame,
1606 outer_pos,
1607 outer_pos + Vec3::unit_z() * rng.random_range(3.0..4.0),
1608 scene_data,
1609 ),
1610 );
1611 }
1612 }
1613
1614 fn maintain_mine_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1615 prof_span!("ParticleMgr::maintain_mine_particles");
1616 let time = scene_data.state.get_time();
1617
1618 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(1)) {
1619 self.push_particle(
1621 scene_data.particles_chance,
1622 Particle::new(
1623 Duration::from_millis(25),
1624 time,
1625 ParticleMode::GunPowderSpark,
1626 pos,
1627 scene_data,
1628 ),
1629 );
1630 }
1631 }
1632
1633 fn maintain_char_state_particles(&mut self, scene_data: &SceneData, figure_mgr: &FigureMgr) {
1634 prof_span!("ParticleMgr::maintain_char_state_particles");
1635 let state = scene_data.state;
1636 let ecs = state.ecs();
1637 let time = state.get_time();
1638 let dt = scene_data.state.get_delta_time();
1639 let mut rng = rand::rng();
1640
1641 for (
1642 entity,
1643 interpolated,
1644 vel,
1645 character_state,
1646 body,
1647 ori,
1648 character_activity,
1649 physics,
1650 inventory,
1651 footsteps,
1652 scale,
1653 ) in (
1654 &ecs.entities(),
1655 &ecs.read_storage::<Interpolated>(),
1656 ecs.read_storage::<Vel>().maybe(),
1657 &ecs.read_storage::<CharacterState>(),
1658 &ecs.read_storage::<Body>(),
1659 &ecs.read_storage::<Ori>(),
1660 &ecs.read_storage::<CharacterActivity>(),
1661 &ecs.read_storage::<PhysicsState>(),
1662 ecs.read_storage::<Inventory>().maybe(),
1663 ecs.read_storage::<Footsteps>().maybe(),
1664 ecs.read_storage::<Scale>().maybe(),
1665 )
1666 .join()
1667 {
1668 if let Some(block) = physics.on_ground
1670 && let Some(color) = block.get_color()
1671 && let Some(vel) = vel
1672 && (vel.0 - physics.ground_vel).xy().magnitude_squared() > 30.0
1673 && matches!(body, Body::Humanoid(_))
1674 && let Some(char_state) = figure_mgr.states.character_states.get(&entity)
1675 && let Some(footsteps) = footsteps
1676 {
1677 let feet = [
1678 char_state.computed_skeleton.foot_l,
1679 char_state.computed_skeleton.foot_r,
1680 ];
1681
1682 let feet_pos = feet.map(|f| {
1683 char_state
1684 .wpos_of(f.mul_point(Vec3::zero()))
1685 .with_z(interpolated.pos.z)
1686 });
1687
1688 for (i, foot_pos) in feet_pos.into_iter().enumerate() {
1689 if footsteps.is_stepping(i) {
1690 let scale = scale.map_or(1.0, |s| s.0);
1691 let dust_particles = (scale * 4.0).ceil() as usize;
1693 self.add_particles(scene_data.particles_chance, dust_particles, || {
1694 Particle::new_colored(
1695 Duration::from_millis(750),
1696 time,
1697 ParticleMode::Dust,
1698 foot_pos
1700 + Vec2::broadcast(scale)
1701 .map(|s| rng.random_range(-0.1..0.1) * s),
1702 color.as_() * (1.0 / 255.0),
1706 scene_data,
1707 )
1708 .with_light(char_state.meta.last_light, char_state.meta.last_glow.1)
1709 });
1710 if char_state.meta.last_light > 0.9 {
1712 let splash_particles =
1714 ((state.weather_at(interpolated.pos.xy()).rain - RAIN_THRESHOLD)
1715 .max(0.0)
1716 * scale
1717 * 100.0)
1718 .ceil()
1719 .min(16.0) as usize;
1720 self.add_particles(
1721 scene_data.particles_chance,
1722 splash_particles,
1723 || {
1724 Particle::new_directed(
1725 Duration::from_millis(2500),
1726 time,
1727 ParticleMode::WaterFoam,
1728 foot_pos,
1729 foot_pos
1730 + (Vec2::broadcast(())
1731 .map(|()| rng.random_range(-1.0..1.0))
1732 .try_normalized()
1733 .unwrap_or_default()
1734 * rng.random_range(9.0..12.0))
1735 .with_z(13.0),
1736 scene_data,
1737 )
1738 .with_light(
1739 char_state.meta.last_light,
1740 char_state.meta.last_glow.1,
1741 )
1742 },
1743 );
1744 }
1745 }
1746 }
1747 }
1748
1749 if let Some(fluid) = physics.in_fluid
1751 && fluid.is_water()
1752 && let Some(vel) = vel
1753 && fluid.relative_flow(vel).0.magnitude_squared() > 10.0
1754 && matches!(body, Body::Humanoid(_))
1755 && let Some(state) = figure_mgr.states.character_states.get(&entity)
1756 {
1757 for hand in [
1758 state.computed_skeleton.hand_l,
1759 state.computed_skeleton.hand_r,
1760 ] {
1761 if hand.mul_direction(Vec3::unit_z()).z < 0.0 {
1765 let final_amount =
1766 usize::from(self.scheduler.heartbeats(Duration::from_millis(90)));
1767 self.add_particles(scene_data.particles_chance, final_amount, || {
1768 Particle::new(
1769 Duration::from_secs(1),
1770 time,
1771 ParticleMode::Bubble,
1772 state.wpos_of(hand.mul_point(Vec3::zero()))
1773 - vel.0 * dt * rng.random::<f32>(),
1774 scene_data,
1775 )
1776 });
1777 }
1778 }
1779 }
1780
1781 match character_state {
1782 CharacterState::Boost(_) => {
1783 let final_amount =
1784 usize::from(self.scheduler.heartbeats(Duration::from_millis(10)));
1785 self.add_particles(scene_data.particles_chance, final_amount, || {
1786 Particle::new(
1787 Duration::from_millis(250),
1788 time,
1789 ParticleMode::PortalFizz,
1790 interpolated.pos
1792 - ori.to_horizontal().look_dir().to_vec()
1793 - vel.map_or(Vec3::zero(), |v| v.0 * dt * rng.random::<f32>()),
1794 scene_data,
1795 )
1796 });
1797 },
1798 CharacterState::BasicMelee(c) => {
1799 if let Some(specifier) = c.static_data.frontend_specifier {
1800 match specifier {
1801 states::basic_melee::FrontendSpecifier::FlameTornado => {
1802 if matches!(c.stage_section, StageSection::Action) {
1803 let time = scene_data.state.get_time();
1804 let mut rng = rand::rng();
1805 let final_amount = 10
1806 + usize::from(
1807 self.scheduler.heartbeats(Duration::from_millis(5)),
1808 );
1809 self.add_particles(
1810 scene_data.particles_chance,
1811 final_amount,
1812 || {
1813 Particle::new(
1814 Duration::from_millis(1000),
1815 time,
1816 ParticleMode::FlameTornado,
1817 interpolated
1818 .pos
1819 .map(|e| e + rng.random_range(-0.25..0.25)),
1820 scene_data,
1821 )
1822 },
1823 );
1824 }
1825 },
1826 states::basic_melee::FrontendSpecifier::FireGigasWhirlwind => {
1827 if matches!(c.stage_section, StageSection::Action) {
1828 let time = scene_data.state.get_time();
1829 let mut rng = rand::rng();
1830 let final_amount = 3 + usize::from(
1831 self.scheduler.heartbeats(Duration::from_millis(5)),
1832 );
1833 self.add_particles(
1834 scene_data.particles_chance,
1835 final_amount,
1836 || {
1837 Particle::new(
1838 Duration::from_millis(600),
1839 time,
1840 ParticleMode::FireGigasWhirlwind,
1841 interpolated
1842 .pos
1843 .map(|e| e + rng.random_range(-0.25..0.25))
1844 + 3.0 * Vec3::<f32>::unit_z(),
1845 scene_data,
1846 )
1847 },
1848 );
1849 }
1850 },
1851 }
1852 }
1853 },
1854 CharacterState::RapidMelee(c) => {
1855 if let Some(specifier) = c.static_data.frontend_specifier {
1856 match specifier {
1857 states::rapid_melee::FrontendSpecifier::CultistVortex => {
1858 if matches!(c.stage_section, StageSection::Action) {
1859 let range = c.static_data.melee_constructor.range;
1860 let heartbeats =
1862 self.scheduler.heartbeats(Duration::from_millis(3));
1863 self.add_particles(
1864 scene_data.particles_chance,
1865 range.powi(2) as usize * usize::from(heartbeats) / 150,
1866 || {
1867 let rand_dist =
1868 range * (1.0 - rng.random::<f32>().powi(10));
1869 let init_pos = Vec3::new(
1870 2.0 * rng.random::<f32>() - 1.0,
1871 2.0 * rng.random::<f32>() - 1.0,
1872 0.0,
1873 )
1874 .normalized()
1875 * rand_dist
1876 + interpolated.pos
1877 + Vec3::unit_z() * 0.05;
1878 Particle::new_directed(
1879 Duration::from_millis(900),
1880 time,
1881 ParticleMode::CultistFlame,
1882 init_pos,
1883 interpolated.pos,
1884 scene_data,
1885 )
1886 },
1887 );
1888 for (_entity_b, interpolated_b, body_b, _health_b) in (
1890 &ecs.entities(),
1891 &ecs.read_storage::<Interpolated>(),
1892 &ecs.read_storage::<Body>(),
1893 &ecs.read_storage::<comp::Health>(),
1894 )
1895 .join()
1896 .filter(|(e, _, _, h)| !h.is_dead && entity != *e)
1897 {
1898 if interpolated.pos.distance_squared(interpolated_b.pos)
1899 < range.powi(2)
1900 {
1901 let heartbeats = self
1902 .scheduler
1903 .heartbeats(Duration::from_millis(20));
1904 self.add_particles(
1905 scene_data.particles_chance,
1906 range.powi(2) as usize * usize::from(heartbeats)
1907 / 150,
1908 || {
1909 let start_pos = interpolated_b.pos
1910 + Vec3::unit_z() * body_b.height() * 0.5
1911 + Vec3::<f32>::zero()
1912 .map(|_| rng.random_range(-1.0..1.0))
1913 .normalized()
1914 * 1.0;
1915 Particle::new_directed(
1916 Duration::from_millis(900),
1917 time,
1918 ParticleMode::CultistFlame,
1919 start_pos,
1920 interpolated.pos
1921 + Vec3::unit_z() * body.height() * 0.5,
1922 scene_data,
1923 )
1924 },
1925 );
1926 }
1927 }
1928 }
1929 },
1930 states::rapid_melee::FrontendSpecifier::IceWhirlwind => {
1931 if matches!(c.stage_section, StageSection::Action) {
1932 let time = scene_data.state.get_time();
1933 let mut rng = rand::rng();
1934 let final_amount = 3 + usize::from(
1935 self.scheduler.heartbeats(Duration::from_millis(5)),
1936 );
1937 self.add_particles(
1938 scene_data.particles_chance,
1939 final_amount,
1940 || {
1941 Particle::new(
1942 Duration::from_millis(1000),
1943 time,
1944 ParticleMode::IceWhirlwind,
1945 interpolated
1946 .pos
1947 .map(|e| e + rng.random_range(-0.25..0.25)),
1948 scene_data,
1949 )
1950 },
1951 );
1952 }
1953 },
1954 states::rapid_melee::FrontendSpecifier::ElephantVacuum => {
1955 if matches!(c.stage_section, StageSection::Action) {
1956 let time = scene_data.state.get_time();
1957 let mut rng = rand::rng();
1958
1959 let (end_radius, max_range) =
1960 if let CharacterState::RapidMelee(data) = character_state {
1961 let max_range =
1962 data.static_data.melee_constructor.range;
1963 (
1964 max_range
1965 * (data.static_data.melee_constructor.angle
1966 / 2.0
1967 * PI
1968 / 180.0)
1969 .tan(),
1970 max_range,
1971 )
1972 } else {
1973 (0.0, 0.0)
1974 };
1975 let ori = ori.look_vec();
1976 let body_radius = body.max_radius() * 1.4;
1977 let body_offsets_z = body.height() * 0.4;
1978 let beam_offsets = Vec3::new(
1979 body_radius * ori.x * 1.1,
1980 body_radius * ori.y * 1.1,
1981 body_offsets_z,
1982 );
1983
1984 let (from, to) = (Vec3::<f32>::unit_z(), ori);
1985 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
1986
1987 let final_amount = 5 + usize::from(
1988 self.scheduler.heartbeats(Duration::from_millis(5)),
1989 );
1990 self.add_particles(
1991 scene_data.particles_chance,
1992 final_amount,
1993 || {
1994 let trunk_pos = interpolated.pos + beam_offsets;
1995
1996 let range = rng.random_range(0.05..=max_range);
1997 let radius = rng
1998 .random_range(0.0..=end_radius * range / max_range);
1999 let theta = rng.random_range(0.0..2.0 * PI);
2000
2001 Particle::new_directed(
2002 Duration::from_millis(300),
2003 time,
2004 ParticleMode::ElephantVacuum,
2005 trunk_pos
2006 + m * Vec3::new(
2007 radius * theta.cos(),
2008 radius * theta.sin(),
2009 range,
2010 ),
2011 trunk_pos,
2012 scene_data,
2013 )
2014 },
2015 );
2016 }
2017 },
2018 }
2019 }
2020 },
2021 CharacterState::RapidRanged(repeater) => {
2022 if let Some(specifier) = repeater.static_data.specifier {
2023 match specifier {
2024 states::rapid_ranged::FrontendSpecifier::FireRainPhoenix => {
2025 let final_amount = 2 * usize::from(
2027 self.scheduler.heartbeats(Duration::from_millis(25)),
2028 );
2029 self.add_particles(
2030 scene_data.particles_chance,
2031 final_amount,
2032 || {
2033 let rand_pos = {
2034 let theta = rng.random::<f32>() * TAU;
2035 let radius = repeater
2036 .static_data
2037 .options
2038 .offset
2039 .map(|offset| offset.radius)
2040 .unwrap_or_default()
2041 * rng.random::<f32>().sqrt();
2042 let x = radius * theta.sin();
2043 let y = radius * theta.cos();
2044 Vec2::new(x, y) + interpolated.pos.xy()
2045 };
2046 let pos1 = rand_pos.with_z(
2047 repeater
2048 .static_data
2049 .options
2050 .offset
2051 .map(|offset| offset.height)
2052 .unwrap_or_default()
2053 + interpolated.pos.z
2054 + 2.0 * rng.random::<f32>(),
2055 );
2056 Particle::new_directed(
2057 Duration::from_secs_f32(3.0),
2058 time,
2059 ParticleMode::PhoenixCloud,
2060 pos1,
2061 pos1 + Vec3::new(7.09, 4.09, 18.09),
2062 scene_data,
2063 )
2064 },
2065 );
2066 let final_amount = 2 * usize::from(
2067 self.scheduler.heartbeats(Duration::from_millis(25)),
2068 );
2069 self.add_particles(
2070 scene_data.particles_chance,
2071 final_amount,
2072 || {
2073 let rand_pos = {
2074 let theta = rng.random::<f32>() * TAU;
2075 let radius = repeater
2076 .static_data
2077 .options
2078 .offset
2079 .map(|offset| offset.radius)
2080 .unwrap_or_default()
2081 * rng.random::<f32>().sqrt();
2082 let x = radius * theta.sin();
2083 let y = radius * theta.cos();
2084 Vec2::new(x, y) + interpolated.pos.xy()
2085 };
2086 let pos1 = rand_pos.with_z(
2087 repeater
2088 .static_data
2089 .options
2090 .offset
2091 .map(|offset| offset.height)
2092 .unwrap_or_default()
2093 + interpolated.pos.z
2094 + 1.5 * rng.random::<f32>(),
2095 );
2096 Particle::new_directed(
2097 Duration::from_secs_f32(2.5),
2098 time,
2099 ParticleMode::PhoenixCloud,
2100 pos1,
2101 pos1 + Vec3::new(10.025, 4.025, 17.025),
2102 scene_data,
2103 )
2104 },
2105 );
2106 },
2107 states::rapid_ranged::FrontendSpecifier::PyroclasmCharge {
2108 height: z,
2109 radius: r,
2110 } => {
2111 const TAIL_SECS: f32 = 1.0;
2112 match repeater.stage_section {
2113 StageSection::Buildup => {
2114 let progress = (repeater.timer.as_secs_f32()
2115 / repeater.static_data.buildup_duration.as_secs_f32())
2116 .clamp(0.0, 1.0)
2117 * 0.9;
2118 self.maintain_pyroclasm_charge_particles(
2119 scene_data,
2120 interpolated.pos,
2121 progress,
2122 z,
2123 r,
2124 );
2125 },
2126 StageSection::Action
2127 if repeater.timer.as_secs_f32() < TAIL_SECS =>
2128 {
2129 self.maintain_pyroclasm_charge_particles(
2130 scene_data,
2131 interpolated.pos,
2132 0.9,
2133 z,
2134 r,
2135 );
2136 },
2137 _ => {},
2138 }
2139 },
2140 }
2141 }
2142 },
2143 CharacterState::Blink(c) => {
2144 if let Some(specifier) = c.static_data.frontend_specifier {
2145 match specifier {
2146 states::blink::FrontendSpecifier::CultistFlame => {
2147 let final_amount = usize::from(
2148 self.scheduler.heartbeats(Duration::from_millis(10)),
2149 );
2150 self.add_particles(
2151 scene_data.particles_chance,
2152 final_amount,
2153 || {
2154 let center_pos =
2155 interpolated.pos + Vec3::unit_z() * body.height() / 2.0;
2156 let outer_pos = interpolated.pos
2157 + Vec3::new(
2158 2.0 * rng.random::<f32>() - 1.0,
2159 2.0 * rng.random::<f32>() - 1.0,
2160 0.0,
2161 )
2162 .normalized()
2163 * (body.max_radius() + 2.0)
2164 + Vec3::unit_z() * body.height() * rng.random::<f32>();
2165
2166 let (start_pos, end_pos) =
2167 if matches!(c.stage_section, StageSection::Buildup) {
2168 (outer_pos, center_pos)
2169 } else {
2170 (center_pos, outer_pos)
2171 };
2172
2173 Particle::new_directed(
2174 Duration::from_secs_f32(0.5),
2175 time,
2176 ParticleMode::CultistFlame,
2177 start_pos,
2178 end_pos,
2179 scene_data,
2180 )
2181 },
2182 );
2183 },
2184 states::blink::FrontendSpecifier::FlameThrower => {
2185 let final_amount = usize::from(
2186 self.scheduler.heartbeats(Duration::from_millis(10)),
2187 );
2188 self.add_particles(
2189 scene_data.particles_chance,
2190 final_amount,
2191 || {
2192 let center_pos =
2193 interpolated.pos + Vec3::unit_z() * body.height() / 2.0;
2194 let outer_pos = interpolated.pos
2195 + Vec3::new(
2196 2.0 * rng.random::<f32>() - 1.0,
2197 2.0 * rng.random::<f32>() - 1.0,
2198 0.0,
2199 )
2200 .normalized()
2201 * (body.max_radius() + 2.0)
2202 + Vec3::unit_z() * body.height() * rng.random::<f32>();
2203
2204 let (start_pos, end_pos) =
2205 if matches!(c.stage_section, StageSection::Buildup) {
2206 (outer_pos, center_pos)
2207 } else {
2208 (center_pos, outer_pos)
2209 };
2210
2211 Particle::new_directed(
2212 Duration::from_secs_f32(0.5),
2213 time,
2214 ParticleMode::FlameThrower,
2215 start_pos,
2216 end_pos,
2217 scene_data,
2218 )
2219 },
2220 );
2221 },
2222 }
2223 }
2224 },
2225 CharacterState::SelfBuff(c) => {
2226 if let Some(specifier) = c.static_data.specifier {
2227 match specifier {
2228 states::self_buff::FrontendSpecifier::FromTheAshes => {
2229 if matches!(c.stage_section, StageSection::Action) {
2230 let pos = interpolated.pos;
2231 let final_amount = 2 * usize::from(
2232 self.scheduler.heartbeats(Duration::from_millis(1)),
2233 );
2234 self.add_particles(
2235 scene_data.particles_chance,
2236 final_amount,
2237 || {
2238 let start_pos = pos + Vec3::unit_z() - 1.0;
2239 let end_pos = pos
2240 + Vec3::new(
2241 4.0 * rng.random::<f32>() - 1.0,
2242 4.0 * rng.random::<f32>() - 1.0,
2243 0.0,
2244 )
2245 .normalized()
2246 * 1.5
2247 + Vec3::unit_z()
2248 + 5.0 * rng.random::<f32>();
2249
2250 Particle::new_directed(
2251 Duration::from_secs_f32(0.5),
2252 time,
2253 ParticleMode::FieryBurst,
2254 start_pos,
2255 end_pos,
2256 scene_data,
2257 )
2258 },
2259 );
2260 let final_amount = usize::from(
2261 self.scheduler.heartbeats(Duration::from_millis(10)),
2262 );
2263 self.add_particles(
2264 scene_data.particles_chance,
2265 final_amount,
2266 || {
2267 Particle::new(
2268 Duration::from_millis(650),
2269 time,
2270 ParticleMode::FieryBurstVortex,
2271 pos.map(|e| e + rng.random_range(-0.25..0.25))
2272 + Vec3::new(0.0, 0.0, 1.0),
2273 scene_data,
2274 )
2275 },
2276 );
2277 let final_amount = usize::from(
2278 self.scheduler.heartbeats(Duration::from_millis(40)),
2279 );
2280 self.add_particles(
2281 scene_data.particles_chance,
2282 final_amount,
2283 || {
2284 Particle::new(
2285 Duration::from_millis(1000),
2286 time,
2287 ParticleMode::FieryBurstSparks,
2288 pos.map(|e| e + rng.random_range(-0.25..0.25)),
2289 scene_data,
2290 )
2291 },
2292 );
2293 let final_amount = usize::from(
2294 self.scheduler.heartbeats(Duration::from_millis(14)),
2295 );
2296 self.add_particles(
2297 scene_data.particles_chance,
2298 final_amount,
2299 || {
2300 let pos1 =
2301 pos.map(|e| e + rng.random_range(-0.25..0.25));
2302 Particle::new_directed(
2303 Duration::from_millis(1000),
2304 time,
2305 ParticleMode::FieryBurstAsh,
2306 pos1,
2307 Vec3::new(
2308 4.5, 20.4, 8.58) + pos1,
2312 scene_data,
2313 )
2314 },
2315 );
2316 }
2317 },
2318 }
2319 }
2320 use buff::BuffKind;
2321 if c.static_data
2322 .buffs
2323 .iter()
2324 .any(|buff_desc| matches!(buff_desc.kind, BuffKind::Frenzied))
2325 && matches!(c.stage_section, StageSection::Action)
2326 {
2327 let final_amount =
2328 usize::from(self.scheduler.heartbeats(Duration::from_millis(5)));
2329 self.add_particles(scene_data.particles_chance, final_amount, || {
2330 let start_pos = interpolated.pos
2331 + Vec3::new(
2332 body.max_radius(),
2333 body.max_radius(),
2334 body.height() / 2.0,
2335 )
2336 .map(|d| d * rng.random_range(-1.0..1.0));
2337 let end_pos = interpolated.pos + (start_pos - interpolated.pos) * 6.0;
2338 Particle::new_directed(
2339 Duration::from_secs(1),
2340 time,
2341 ParticleMode::Enraged,
2342 start_pos,
2343 end_pos,
2344 scene_data,
2345 )
2346 });
2347 }
2348 },
2349 CharacterState::BasicBeam(beam) => {
2350 let ori = *ori;
2351 let _look_dir = *character_activity.look_dir.unwrap_or(ori.look_dir());
2352 let dir = ori.look_dir(); let specifier = beam.static_data.specifier;
2354 if specifier == beam::FrontendSpecifier::PhoenixLaser
2355 && matches!(beam.stage_section, StageSection::Buildup)
2356 {
2357 let final_amount =
2358 2 * usize::from(self.scheduler.heartbeats(Duration::from_millis(2)));
2359 self.add_particles(scene_data.particles_chance, final_amount, || {
2360 let mut left_right_alignment =
2361 dir.cross(Vec3::new(0.0, 0.0, 1.0)).normalized();
2362 if rng.random_bool(0.5) {
2363 left_right_alignment *= -1.0;
2364 }
2365 let start = interpolated.pos
2366 + left_right_alignment * 4.0
2367 + dir.normalized() * 6.0;
2368 let lifespan = Duration::from_secs_f32(0.5);
2369 Particle::new_directed(
2370 lifespan,
2371 time,
2372 ParticleMode::PhoenixBuildUpAim,
2373 start,
2374 interpolated.pos
2375 + dir.normalized() * 3.0
2376 + left_right_alignment * 0.4
2377 + vel.map_or(Vec3::zero(), |v| v.0 * lifespan.as_secs_f32()),
2378 scene_data,
2379 )
2380 });
2381 }
2382 },
2383 CharacterState::Glide(glide) => {
2384 if let Some(Fluid::Air {
2385 vel: air_vel,
2386 elevation: _,
2387 }) = physics.in_fluid
2388 {
2389 const MAX_AIR_VEL: f32 = 15.0;
2392 const MIN_AIR_VEL: f32 = -2.0;
2393
2394 let minmax_norm = |val, min, max| (val - min) / (max - min);
2395
2396 let wind_speed = air_vel.0.magnitude();
2397
2398 let heartbeat = 200
2400 - Lerp::lerp(
2401 50u64,
2402 150,
2403 minmax_norm(wind_speed, MIN_AIR_VEL, MAX_AIR_VEL),
2404 );
2405
2406 let new_count = usize::from(
2407 self.scheduler.heartbeats(Duration::from_millis(heartbeat)),
2408 );
2409
2410 let duration = Lerp::lerp(
2412 0u64,
2413 1000,
2414 minmax_norm(wind_speed, MIN_AIR_VEL, MAX_AIR_VEL),
2415 );
2416 let duration = Duration::from_millis(duration);
2417
2418 self.add_particles(scene_data.particles_chance, new_count, || {
2419 let start_pos = interpolated.pos
2420 + Vec3::new(
2421 body.max_radius(),
2422 body.max_radius(),
2423 body.height() / 2.0,
2424 )
2425 .map(|d| d * rng.random_range(-10.0..10.0));
2426
2427 Particle::new_directed(
2428 duration,
2429 time,
2430 ParticleMode::Airflow,
2431 start_pos,
2432 start_pos + air_vel.0,
2433 scene_data,
2434 )
2435 });
2436
2437 if let Some(states::glide::Boost::Forward(_)) = &glide.booster
2439 && let Some(figure_state) =
2440 figure_mgr.states.character_states.get(&entity)
2441 && let Some(tp0) = figure_state.primary_abs_trail_points
2442 && let Some(tp1) = figure_state.secondary_abs_trail_points
2443 {
2444 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(5)) {
2445 self.push_particle(scene_data.particles_chance, Particle::new(
2446 Duration::from_secs(2),
2447 time,
2448 ParticleMode::EngineJet,
2449 ((tp0.0 + tp1.1) * 0.5)
2450 + Vec3::unit_z() * 0.5
2452 + Vec3::<f32>::zero().map(|_| rng.random_range(-0.25..0.25))
2453 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.random::<f32>()),
2454 scene_data,
2455 ));
2456 }
2457 }
2458 }
2459 },
2460 CharacterState::Transform(data) => {
2461 if matches!(data.stage_section, StageSection::Buildup)
2462 && let Some(specifier) = data.static_data.specifier
2463 {
2464 match specifier {
2465 states::transform::FrontendSpecifier::Evolve => {
2466 let final_amount = usize::from(
2467 self.scheduler.heartbeats(Duration::from_millis(10)),
2468 );
2469 self.add_particles(
2470 scene_data.particles_chance,
2471 final_amount,
2472 || {
2473 let start_pos = interpolated.pos
2474 + (Vec2::unit_y()
2475 * rng.random::<f32>()
2476 * body.max_radius())
2477 .rotated_z(rng.random_range(0.0..(PI * 2.0)))
2478 .with_z(body.height() * rng.random::<f32>());
2479
2480 Particle::new_directed(
2481 Duration::from_millis(100),
2482 time,
2483 ParticleMode::BarrelOrgan,
2484 start_pos,
2485 start_pos + Vec3::unit_z() * 2.0,
2486 scene_data,
2487 )
2488 },
2489 )
2490 },
2491 states::transform::FrontendSpecifier::Cursekeeper => {
2492 let final_amount = usize::from(
2493 self.scheduler.heartbeats(Duration::from_millis(10)),
2494 );
2495 self.add_particles(
2496 scene_data.particles_chance,
2497 final_amount,
2498 || {
2499 let start_pos = interpolated.pos
2500 + (Vec2::unit_y()
2501 * rng.random::<f32>()
2502 * body.max_radius())
2503 .rotated_z(rng.random_range(0.0..(PI * 2.0)))
2504 .with_z(body.height() * rng.random::<f32>());
2505
2506 Particle::new_directed(
2507 Duration::from_millis(100),
2508 time,
2509 ParticleMode::FireworkPurple,
2510 start_pos,
2511 start_pos + Vec3::unit_z() * 2.0,
2512 scene_data,
2513 )
2514 },
2515 )
2516 },
2517 }
2518 }
2519 },
2520 CharacterState::ChargedMelee(_melee) => {
2521 self.maintain_hydra_tail_swipe_particles(
2522 scene_data,
2523 figure_mgr,
2524 entity,
2525 interpolated.pos,
2526 body,
2527 character_state,
2528 inventory,
2529 );
2530 },
2531 CharacterState::DashMelee(s) => {
2532 if matches!(s.stage_section, StageSection::Charge) {
2533 match s.static_data.frontend_specifier {
2534 Some(states::dash_melee::FrontendSpecifier::FireDash) => {
2535 let look_dir = ori.to_horizontal().look_dir().to_vec();
2536 let back_dir = -look_dir;
2537 let time = scene_data.state.get_time();
2538 let mut rng = rand::rng();
2539 let heartbeats =
2540 self.scheduler.heartbeats(Duration::from_millis(5));
2541 let pos = interpolated.pos + Vec3::unit_z() * 0.9;
2542
2543 let m = Mat3::<f32>::rotation_from_to_3d(
2545 Vec3::<f32>::unit_z(),
2546 back_dir,
2547 );
2548
2549 let tail_angle: f32 = 0.4;
2550 let tail_range = 3.5_f32;
2551 self.add_particles(
2552 scene_data.particles_chance,
2553 usize::from(heartbeats) * 3,
2554 || {
2555 let phi = rng.random_range(0.0..tail_angle);
2556 let theta = rng.random_range(0.0..TAU);
2557 let offset_z = Vec3::new(
2558 phi.sin() * theta.cos(),
2559 phi.sin() * theta.sin(),
2560 phi.cos(),
2561 );
2562 let dir = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2563 let mode = if rng.random_bool(0.5) {
2564 ParticleMode::FlamethrowerBlue
2565 } else {
2566 ParticleMode::FlameThrower
2567 };
2568 Particle::new_directed(
2569 Duration::from_millis(300),
2570 time,
2571 mode,
2572 pos,
2573 pos + dir * tail_range,
2574 scene_data,
2575 )
2576 },
2577 );
2578
2579 let nose_pos = pos + look_dir * 3.0;
2580 let nose_angle: f32 = 2.8; let nose_range = 3.0_f32;
2582 self.add_particles(
2583 scene_data.particles_chance,
2584 usize::from(heartbeats) * 6,
2585 || {
2586 let phi = rng.random_range(0.0..nose_angle);
2587 let theta = rng.random_range(0.0..TAU);
2588 let offset_z = Vec3::new(
2589 phi.sin() * theta.cos(),
2590 phi.sin() * theta.sin(),
2591 phi.cos(),
2592 );
2593 let dir = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2594 Particle::new_directed(
2595 Duration::from_millis(180),
2596 time,
2597 ParticleMode::FlameThrower,
2598 nose_pos,
2599 nose_pos + dir * nose_range,
2600 scene_data,
2601 )
2602 },
2603 );
2604 },
2605 None => (),
2606 }
2607 }
2608 },
2609 _ => {},
2610 }
2611 }
2612 }
2613
2614 fn maintain_beam_particles(&mut self, scene_data: &SceneData, lights: &mut Vec<Light>) {
2615 let state = scene_data.state;
2616 let ecs = state.ecs();
2617 let time = state.get_time();
2618 let terrain = state.terrain();
2619 let tick_elapse = u32::from(self.scheduler.heartbeats(Duration::from_millis(1)).min(100));
2622 let mut rng = rand::rng();
2623
2624 for (beam, ori) in (&ecs.read_storage::<Beam>(), &ecs.read_storage::<Ori>()).join() {
2625 let particles_per_sec = (match beam.specifier {
2626 beam::FrontendSpecifier::Flamethrower
2627 | beam::FrontendSpecifier::Bubbles
2628 | beam::FrontendSpecifier::Steam
2629 | beam::FrontendSpecifier::Frost
2630 | beam::FrontendSpecifier::Poison
2631 | beam::FrontendSpecifier::Ink
2632 | beam::FrontendSpecifier::PhoenixLaser
2633 | beam::FrontendSpecifier::Gravewarden => 300.0,
2634 beam::FrontendSpecifier::FirePillar | beam::FrontendSpecifier::FlameWallPillar => {
2635 40.0 * beam.end_radius.powi(2)
2636 },
2637 beam::FrontendSpecifier::LifestealBeam => 420.0,
2638 beam::FrontendSpecifier::Cultist => 960.0,
2639 beam::FrontendSpecifier::WebStrand => 180.0,
2640 beam::FrontendSpecifier::Lightning => 120.0,
2641 beam::FrontendSpecifier::FireGigasOverheat => 1600.0,
2642 }) / 1000.0;
2643
2644 let beam_tick_count = tick_elapse as f32 * particles_per_sec;
2645 let beam_tick_count = if rng.random_bool(f64::from(beam_tick_count.fract())) {
2646 beam_tick_count.ceil() as u32
2647 } else {
2648 beam_tick_count.floor() as u32
2649 };
2650
2651 if beam_tick_count == 0 {
2652 continue;
2653 }
2654
2655 let distributed_time = tick_elapse as f64 / (beam_tick_count * 1000) as f64;
2656 let angle = (beam.end_radius / beam.range).atan();
2657 let beam_dir = (beam.bezier.ctrl - beam.bezier.start)
2658 .try_normalized()
2659 .unwrap_or(*ori.look_dir());
2660 let raycast_distance = |from, to| terrain.ray(from, to).until(Block::is_solid).cast().0;
2661
2662 match beam.specifier {
2663 beam::FrontendSpecifier::Flamethrower => {
2664 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2665 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2666 if scene_data.flashing_lights_enabled {
2668 lights.push(Light::new(
2669 beam.bezier.start,
2670 Rgb::new(1.0, 0.25, 0.05).map(|e| e * rng.random_range(0.8..1.2)),
2671 2.0,
2672 ));
2673 }
2674
2675 for i in 0..beam_tick_count {
2676 let phi: f32 = rng.random_range(0.0..angle);
2677 let theta: f32 = rng.random_range(0.0..2.0 * PI);
2678 let offset_z =
2679 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2680 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2681 self.push_particle(
2682 scene_data.particles_chance,
2683 Particle::new_directed_with_collision(
2684 Duration::from_secs_f64(beam.duration.0),
2685 time + distributed_time * i as f64,
2686 ParticleMode::FlameThrower,
2687 beam.bezier.start,
2688 beam.bezier.start + random_ori * beam.range,
2689 scene_data,
2690 raycast_distance,
2691 ),
2692 );
2693 }
2694 },
2695 beam::FrontendSpecifier::FireGigasOverheat => {
2696 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2697 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2698 if scene_data.flashing_lights_enabled {
2700 lights.push(Light::new(
2701 beam.bezier.start,
2702 Rgb::new(1.0, 0.25, 0.05).map(|e| e * rng.random_range(0.8..1.2)),
2703 2.0,
2704 ));
2705 }
2706
2707 for i in 0..beam_tick_count {
2708 let phi: f32 = rng.random_range(0.0..angle);
2709 let theta: f32 = rng.random_range(0.0..2.0 * PI);
2710 let offset_z =
2711 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2712 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2713 self.push_particle(
2714 scene_data.particles_chance,
2715 Particle::new_directed_with_collision(
2716 Duration::from_secs_f64(beam.duration.0),
2717 time + distributed_time * i as f64,
2718 ParticleMode::FireGigasOverheat,
2719 beam.bezier.start,
2720 beam.bezier.start + random_ori * beam.range,
2721 scene_data,
2722 raycast_distance,
2723 ),
2724 );
2725 }
2726 },
2727 beam::FrontendSpecifier::FirePillar | beam::FrontendSpecifier::FlameWallPillar => {
2728 if scene_data.flashing_lights_enabled {
2730 lights.push(Light::new(
2731 beam.bezier.start,
2732 Rgb::new(1.0, 0.25, 0.05).map(|e| e * rng.random_range(0.8..1.2)),
2733 2.0,
2734 ));
2735 }
2736
2737 for i in 0..beam_tick_count {
2738 let theta: f32 = rng.random_range(0.0..2.0 * PI);
2739 let radius = beam.start_radius * (1.0 - rng.random::<f32>().powi(8));
2740 let offset = Vec3::new(radius * theta.cos(), radius * theta.sin(), 0.0);
2741 self.push_particle(
2742 scene_data.particles_chance,
2743 Particle::new_directed_with_collision(
2744 Duration::from_secs_f64(beam.duration.0),
2745 time + distributed_time * i as f64,
2746 ParticleMode::FirePillar,
2747 beam.bezier.start + offset,
2748 beam.bezier.start + offset + beam.range * Vec3::unit_z(),
2749 scene_data,
2750 raycast_distance,
2751 ),
2752 );
2753 }
2754 },
2755 beam::FrontendSpecifier::Cultist => {
2756 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2757 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2758 if scene_data.flashing_lights_enabled {
2760 lights.push(Light::new(
2761 beam.bezier.start,
2762 Rgb::new(1.0, 0.0, 1.0).map(|e| e * rng.random_range(0.5..1.0)),
2763 2.0,
2764 ));
2765 }
2766 for i in 0..beam_tick_count {
2767 let phi: f32 = rng.random_range(0.0..angle);
2768 let theta: f32 = rng.random_range(0.0..2.0 * PI);
2769 let offset_z =
2770 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2771 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2772 self.push_particle(
2773 scene_data.particles_chance,
2774 Particle::new_directed_with_collision(
2775 Duration::from_secs_f64(beam.duration.0),
2776 time + distributed_time * i as f64,
2777 ParticleMode::CultistFlame,
2778 beam.bezier.start,
2779 beam.bezier.start + random_ori * beam.range,
2780 scene_data,
2781 raycast_distance,
2782 ),
2783 );
2784 }
2785 },
2786 beam::FrontendSpecifier::LifestealBeam => {
2787 if scene_data.flashing_lights_enabled {
2789 lights.push(Light::new(beam.bezier.start, Rgb::new(0.8, 1.0, 0.5), 1.0));
2790 }
2791
2792 let bezier_end = beam.bezier.start + beam_dir * beam.range;
2794 let distance = raycast_distance(beam.bezier.start, bezier_end);
2795 for i in 0..beam_tick_count {
2796 self.push_particle(
2797 scene_data.particles_chance,
2798 Particle::new_directed_with_collision(
2799 Duration::from_secs_f64(beam.duration.0),
2800 time + distributed_time * i as f64,
2801 ParticleMode::LifestealBeam,
2802 beam.bezier.start,
2803 bezier_end,
2804 scene_data,
2805 |_from, _to| distance,
2806 ),
2807 );
2808 }
2809 },
2810 beam::FrontendSpecifier::Gravewarden => {
2811 for i in 0..beam_tick_count {
2812 let mut offset = 0.5;
2813 let side = Vec2::new(-beam_dir.y, beam_dir.x);
2814 self.add_particles(scene_data.particles_chance, 2, || {
2815 offset = -offset;
2816 Particle::new_directed_with_collision(
2817 Duration::from_secs_f64(beam.duration.0),
2818 time + distributed_time * i as f64,
2819 ParticleMode::Laser,
2820 beam.bezier.start + beam_dir * 1.5 + side * offset,
2821 beam.bezier.start + beam_dir * beam.range + side * offset,
2822 scene_data,
2823 raycast_distance,
2824 )
2825 });
2826 }
2827 },
2828 beam::FrontendSpecifier::WebStrand => {
2829 let bezier_end = beam.bezier.start + beam_dir * beam.range;
2830 let distance = raycast_distance(beam.bezier.start, bezier_end);
2831 for i in 0..beam_tick_count {
2832 self.push_particle(
2833 scene_data.particles_chance,
2834 Particle::new_directed_with_collision(
2835 Duration::from_secs_f64(beam.duration.0),
2836 time + distributed_time * i as f64,
2837 ParticleMode::WebStrand,
2838 beam.bezier.start,
2839 bezier_end,
2840 scene_data,
2841 |_from, _to| distance,
2842 ),
2843 );
2844 }
2845 },
2846 beam::FrontendSpecifier::Bubbles => {
2847 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2848 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2849 for i in 0..beam_tick_count {
2850 let phi: f32 = rng.random_range(0.0..angle);
2851 let theta: f32 = rng.random_range(0.0..2.0 * PI);
2852 let offset_z =
2853 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2854 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2855 self.push_particle(
2856 scene_data.particles_chance,
2857 Particle::new_directed_with_collision(
2858 Duration::from_secs_f64(beam.duration.0),
2859 time + distributed_time * i as f64,
2860 ParticleMode::Bubbles,
2861 beam.bezier.start,
2862 beam.bezier.start + random_ori * beam.range,
2863 scene_data,
2864 raycast_distance,
2865 ),
2866 );
2867 }
2868 },
2869 beam::FrontendSpecifier::Poison => {
2870 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2871 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2872 for i in 0..beam_tick_count {
2873 let phi: f32 = rng.random_range(0.0..angle);
2874 let theta: f32 = rng.random_range(0.0..2.0 * PI);
2875 let offset_z =
2876 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2877 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2878 self.push_particle(
2879 scene_data.particles_chance,
2880 Particle::new_directed_with_collision(
2881 Duration::from_secs_f64(beam.duration.0),
2882 time + distributed_time * i as f64,
2883 ParticleMode::Poison,
2884 beam.bezier.start,
2885 beam.bezier.start + random_ori * beam.range,
2886 scene_data,
2887 raycast_distance,
2888 ),
2889 );
2890 }
2891 },
2892 beam::FrontendSpecifier::Ink => {
2893 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2894 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2895 for i in 0..beam_tick_count {
2896 let phi: f32 = rng.random_range(0.0..angle);
2897 let theta: f32 = rng.random_range(0.0..2.0 * PI);
2898 let offset_z =
2899 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2900 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2901 self.push_particle(
2902 scene_data.particles_chance,
2903 Particle::new_directed_with_collision(
2904 Duration::from_secs_f64(beam.duration.0),
2905 time + distributed_time * i as f64,
2906 ParticleMode::Bubbles,
2907 beam.bezier.start,
2908 beam.bezier.start + random_ori * beam.range,
2909 scene_data,
2910 raycast_distance,
2911 ),
2912 );
2913 }
2914 },
2915 beam::FrontendSpecifier::Steam => {
2916 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2917 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2918 for i in 0..beam_tick_count {
2919 let phi: f32 = rng.random_range(0.0..angle);
2920 let theta: f32 = rng.random_range(0.0..2.0 * PI);
2921 let offset_z =
2922 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2923 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2924 self.push_particle(
2925 scene_data.particles_chance,
2926 Particle::new_directed_with_collision(
2927 Duration::from_secs_f64(beam.duration.0),
2928 time + distributed_time * i as f64,
2929 ParticleMode::Steam,
2930 beam.bezier.start,
2931 beam.bezier.start + random_ori * beam.range,
2932 scene_data,
2933 raycast_distance,
2934 ),
2935 );
2936 }
2937 },
2938 beam::FrontendSpecifier::Lightning => {
2939 let bezier_end = beam.bezier.start + beam_dir * beam.range;
2940 let distance = raycast_distance(beam.bezier.start, bezier_end);
2941 for i in 0..beam_tick_count {
2942 self.push_particle(
2943 scene_data.particles_chance,
2944 Particle::new_directed_with_collision(
2945 Duration::from_secs_f64(beam.duration.0),
2946 time + distributed_time * i as f64,
2947 ParticleMode::Lightning,
2948 beam.bezier.start,
2949 bezier_end,
2950 scene_data,
2951 |_from, _to| distance,
2952 ),
2953 );
2954 }
2955 },
2956 beam::FrontendSpecifier::Frost => {
2957 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2958 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2959 for i in 0..beam_tick_count {
2960 let phi: f32 = rng.random_range(0.0..angle);
2961 let theta: f32 = rng.random_range(0.0..2.0 * PI);
2962 let offset_z =
2963 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2964 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2965 self.push_particle(
2966 scene_data.particles_chance,
2967 Particle::new_directed_with_collision(
2968 Duration::from_secs_f64(beam.duration.0),
2969 time + distributed_time * i as f64,
2970 ParticleMode::Ice,
2971 beam.bezier.start,
2972 beam.bezier.start + random_ori * beam.range,
2973 scene_data,
2974 raycast_distance,
2975 ),
2976 );
2977 }
2978 },
2979 beam::FrontendSpecifier::PhoenixLaser => {
2980 let bezier_end = beam.bezier.start + beam_dir * beam.range;
2981 let distance = raycast_distance(beam.bezier.start, bezier_end);
2982 for i in 0..beam_tick_count {
2983 self.push_particle(
2984 scene_data.particles_chance,
2985 Particle::new_directed_with_collision(
2986 Duration::from_secs_f64(beam.duration.0),
2987 time + distributed_time * i as f64,
2988 ParticleMode::PhoenixBeam,
2989 beam.bezier.start,
2990 bezier_end,
2991 scene_data,
2992 |_from, _to| distance,
2993 ),
2994 );
2995 }
2996 },
2997 }
2998 }
2999 }
3000
3001 fn maintain_aura_particles(&mut self, scene_data: &SceneData) {
3002 let state = scene_data.state;
3003 let ecs = state.ecs();
3004 let time = state.get_time();
3005 let mut rng = rand::rng();
3006 let dt = scene_data.state.get_delta_time();
3007
3008 for (interp, pos, auras, body_maybe) in (
3009 ecs.read_storage::<Interpolated>().maybe(),
3010 &ecs.read_storage::<Pos>(),
3011 &ecs.read_storage::<comp::Auras>(),
3012 ecs.read_storage::<comp::Body>().maybe(),
3013 )
3014 .join()
3015 {
3016 let pos = interp.map_or(pos.0, |i| i.pos);
3017
3018 for (_, aura) in auras.auras.iter() {
3019 match aura.aura_kind {
3020 aura::AuraKind::Buff {
3021 kind: buff::BuffKind::ProtectingWard,
3022 ..
3023 } => {
3024 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
3025 self.add_particles(
3026 scene_data.particles_chance,
3027 aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
3028 || {
3029 let rand_dist = aura.radius * (1.0 - rng.random::<f32>().powi(100));
3030 let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
3031 let duration = Duration::from_secs_f64(
3032 aura.end_time
3033 .map_or(1.0, |end| end.0 - time)
3034 .clamp(0.0, 1.0),
3035 );
3036 Particle::new_directed(
3037 duration,
3038 time,
3039 ParticleMode::EnergyNature,
3040 pos,
3041 pos + init_pos,
3042 scene_data,
3043 )
3044 },
3045 );
3046 },
3047 aura::AuraKind::Buff {
3048 kind: buff::BuffKind::Regeneration,
3049 ..
3050 } => {
3051 if auras.auras.iter().any(|(_, aura)| {
3052 matches!(aura.aura_kind, aura::AuraKind::Buff {
3053 kind: buff::BuffKind::ProtectingWard,
3054 ..
3055 })
3056 }) {
3057 continue;
3060 }
3061 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
3062 self.add_particles(
3063 scene_data.particles_chance,
3064 aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
3065 || {
3066 let rand_dist = aura.radius * (1.0 - rng.random::<f32>().powi(100));
3067 let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
3068 let duration = Duration::from_secs_f64(
3069 aura.end_time
3070 .map_or(1.0, |end| end.0 - time)
3071 .clamp(0.0, 1.0),
3072 );
3073 Particle::new_directed(
3074 duration,
3075 time,
3076 ParticleMode::EnergyHealing,
3077 pos,
3078 pos + init_pos,
3079 scene_data,
3080 )
3081 },
3082 );
3083 },
3084 aura::AuraKind::Buff {
3085 kind: buff::BuffKind::Burning,
3086 ..
3087 } => {
3088 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
3089 match aura.frontend_specifier {
3090 Some(aura::Specifier::FieryAura) => {
3091 self.add_particles(
3093 scene_data.particles_chance,
3094 aura.radius.powi(2) as usize * usize::from(heartbeats) / 3,
3095 || {
3096 let orbit_speed = 1.0_f32;
3097 let theta = time as f32 * orbit_speed
3098 + rng.random::<f32>() * std::f32::consts::TAU;
3099 let r = aura.radius * (0.25 + rng.random::<f32>() * 0.2);
3100 let spawn_pos =
3101 (Vec2::new(r * theta.sin(), r * theta.cos())
3102 + pos.xy())
3103 .with_z(pos.z + rng.random::<f32>() * 0.5);
3104 let duration = Duration::from_secs_f64(
3105 aura.end_time
3106 .map_or(0.3, |end| (end.0 - time).clamp(0.0, 0.3)),
3107 );
3108 Particle::new(
3109 duration,
3110 time,
3111 ParticleMode::FlameCloakOrbit,
3112 spawn_pos,
3113 scene_data,
3114 )
3115 },
3116 );
3117 },
3118 None => {
3119 self.add_particles(
3121 scene_data.particles_chance,
3122 aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
3123 || {
3124 let rand_pos = {
3125 let theta = rng.random::<f32>() * TAU;
3126 let radius = aura.radius * rng.random::<f32>().sqrt();
3127 let x = radius * theta.sin();
3128 let y = radius * theta.cos();
3129 Vec2::new(x, y) + pos.xy()
3130 };
3131 let duration = Duration::from_secs_f64(
3132 aura.end_time
3133 .map_or(1.0, |end| end.0 - time)
3134 .clamp(0.0, 1.0),
3135 );
3136 Particle::new_directed(
3137 duration,
3138 time,
3139 ParticleMode::FlameThrower,
3140 rand_pos.with_z(pos.z),
3141 rand_pos.with_z(pos.z + 1.0),
3142 scene_data,
3143 )
3144 },
3145 );
3146 },
3147 _ => {},
3148 }
3149 },
3150 aura::AuraKind::Buff {
3151 kind: buff::BuffKind::Hastened,
3152 ..
3153 } => {
3154 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
3155 self.add_particles(
3156 scene_data.particles_chance,
3157 aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
3158 || {
3159 let rand_dist = aura.radius * (1.0 - rng.random::<f32>().powi(100));
3160 let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
3161 let duration = Duration::from_secs_f64(
3162 aura.end_time
3163 .map_or(1.0, |end| end.0 - time)
3164 .clamp(0.0, 1.0),
3165 );
3166 Particle::new_directed(
3167 duration,
3168 time,
3169 ParticleMode::EnergyBuffing,
3170 pos,
3171 pos + init_pos,
3172 scene_data,
3173 )
3174 },
3175 );
3176 },
3177 aura::AuraKind::Buff {
3178 kind: buff::BuffKind::Frozen,
3179 ..
3180 } => {
3181 let is_new_aura = aura.data.duration.is_none_or(|max_dur| {
3182 let rem_dur = aura.end_time.map_or(time, |e| e.0) - time;
3183 rem_dur > max_dur.0 * 0.9
3184 });
3185 if is_new_aura {
3186 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
3187 self.add_particles(
3188 scene_data.particles_chance,
3189 aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
3190 || {
3191 let rand_angle = rng.random_range(0.0..TAU);
3192 let offset =
3193 Vec2::new(rand_angle.cos(), rand_angle.sin()) * aura.radius;
3194 let z_start = body_maybe
3195 .map_or(0.0, |b| rng.random_range(0.5..0.75) * b.height());
3196 let z_end = body_maybe
3197 .map_or(0.0, |b| rng.random_range(0.0..3.0) * b.height());
3198 Particle::new_directed(
3199 Duration::from_secs(3),
3200 time,
3201 ParticleMode::Ice,
3202 pos + Vec3::unit_z() * z_start,
3203 pos + offset.with_z(z_end),
3204 scene_data,
3205 )
3206 },
3207 );
3208 }
3209 },
3210 aura::AuraKind::Buff {
3211 kind: buff::BuffKind::Heatstroke,
3212 ..
3213 } => {
3214 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
3215 self.add_particles(
3216 scene_data.particles_chance,
3217 aura.radius.powi(2) as usize * usize::from(heartbeats) / 900,
3218 || {
3219 let rand_dist = aura.radius * (1.0 - rng.random::<f32>().powi(100));
3220 let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
3221 let duration = Duration::from_secs_f64(
3222 aura.end_time
3223 .map_or(1.0, |end| end.0 - time)
3224 .clamp(0.0, 1.0),
3225 );
3226 Particle::new_directed(
3227 duration,
3228 time,
3229 ParticleMode::EnergyPhoenix,
3230 pos,
3231 pos + init_pos,
3232 scene_data,
3233 )
3234 },
3235 );
3236
3237 let num_particles = aura.radius.powi(2) * dt / 50.0;
3238 let num_particles = num_particles.floor() as usize
3239 + usize::from(rng.random_bool(f64::from(num_particles % 1.0)));
3240 self.add_particles(scene_data.particles_chance, num_particles, || {
3241 let rand_pos = {
3242 let theta = rng.random::<f32>() * TAU;
3243 let radius = aura.radius * rng.random::<f32>().sqrt();
3244 let x = radius * theta.sin();
3245 let y = radius * theta.cos();
3246 Vec2::new(x, y) + pos.xy()
3247 };
3248 let duration = Duration::from_secs_f64(
3249 aura.end_time
3250 .map_or(1.0, |end| end.0 - time)
3251 .clamp(0.0, 1.0),
3252 );
3253 Particle::new_directed(
3254 duration,
3255 time,
3256 ParticleMode::FieryBurstAsh,
3257 pos,
3258 Vec3::new(
3259 0.0, 20.0, 5.5) + rand_pos.with_z(pos.z),
3263 scene_data,
3264 )
3265 });
3266 },
3267 _ => {},
3268 }
3269 }
3270 }
3271 }
3272
3273 fn maintain_buff_particles(&mut self, scene_data: &SceneData) {
3274 let state = scene_data.state;
3275 let ecs = state.ecs();
3276 let time = state.get_time();
3277 let mut rng = rand::rng();
3278
3279 for (interp, pos, buffs, body, ori, scale) in (
3280 ecs.read_storage::<Interpolated>().maybe(),
3281 &ecs.read_storage::<Pos>(),
3282 &ecs.read_storage::<comp::Buffs>(),
3283 &ecs.read_storage::<Body>(),
3284 &ecs.read_storage::<Ori>(),
3285 ecs.read_storage::<Scale>().maybe(),
3286 )
3287 .join()
3288 {
3289 let pos = interp.map_or(pos.0, |i| i.pos);
3290
3291 for (buff_kind, buff_keys) in buffs
3292 .kinds
3293 .iter()
3294 .filter_map(|(kind, keys)| keys.as_ref().map(|keys| (kind, keys)))
3295 {
3296 use buff::BuffKind;
3297 match buff_kind {
3298 BuffKind::Cursed | BuffKind::Burning => {
3299 let final_amount =
3300 usize::from(self.scheduler.heartbeats(Duration::from_millis(15)));
3301 self.add_particles(scene_data.particles_chance, final_amount, || {
3302 let start_pos = pos
3303 + Vec3::unit_z() * body.height() * 0.25
3304 + Vec3::<f32>::zero()
3305 .map(|_| rng.random_range(-1.0..1.0))
3306 .normalized()
3307 * 0.25;
3308 let end_pos = start_pos
3309 + Vec3::unit_z() * body.height()
3310 + Vec3::<f32>::zero()
3311 .map(|_| rng.random_range(-1.0..1.0))
3312 .normalized();
3313 Particle::new_directed(
3314 Duration::from_secs(1),
3315 time,
3316 if matches!(buff_kind, BuffKind::Cursed) {
3317 ParticleMode::CultistFlame
3318 } else {
3319 ParticleMode::FlameThrower
3320 },
3321 start_pos,
3322 end_pos,
3323 scene_data,
3324 )
3325 });
3326 },
3327 BuffKind::PotionSickness => {
3328 let mut multiplicity = 0;
3329 if buff_keys.0
3332 .iter()
3333 .filter_map(|key| buffs.buffs.get(*key))
3334 .any(|buff| {
3335 matches!(buff.elapsed(Time(time)), dur if (1.0..=1.5).contains(&dur.0))
3336 })
3337 {
3338 multiplicity = 1;
3339 }
3340 let final_amount = multiplicity
3341 * usize::from(self.scheduler.heartbeats(Duration::from_millis(25)));
3342 self.add_particles(scene_data.particles_chance, final_amount, || {
3343 let start_pos =
3344 pos + Vec3::unit_z() * body.eye_height(scale.map_or(1.0, |s| s.0));
3345 let (radius, theta) = (
3346 rng.random_range(0.0f32..1.0).sqrt(),
3347 rng.random_range(0.0..TAU),
3348 );
3349 let end_pos = pos
3350 + *ori.look_dir()
3351 + Vec3::<f32>::new(radius * theta.cos(), radius * theta.sin(), 0.0)
3352 * 0.25;
3353 Particle::new_directed(
3354 Duration::from_secs(1),
3355 time,
3356 ParticleMode::PotionSickness,
3357 start_pos,
3358 end_pos,
3359 scene_data,
3360 )
3361 });
3362 },
3363 BuffKind::Frenzied => {
3364 let final_amount =
3365 usize::from(self.scheduler.heartbeats(Duration::from_millis(15)));
3366 self.add_particles(scene_data.particles_chance, final_amount, || {
3367 let start_pos = pos
3368 + Vec3::new(
3369 body.max_radius(),
3370 body.max_radius(),
3371 body.height() / 2.0,
3372 )
3373 .map(|d| d * rng.random_range(-1.0..1.0));
3374 let end_pos = start_pos
3375 + Vec3::unit_z() * body.height()
3376 + Vec3::<f32>::zero()
3377 .map(|_| rng.random_range(-1.0..1.0))
3378 .normalized();
3379 Particle::new_directed(
3380 Duration::from_secs(1),
3381 time,
3382 ParticleMode::Enraged,
3383 start_pos,
3384 end_pos,
3385 scene_data,
3386 )
3387 });
3388 },
3389 BuffKind::Polymorphed => {
3390 let mut multiplicity = 0;
3391 if buff_keys.0
3394 .iter()
3395 .filter_map(|key| buffs.buffs.get(*key))
3396 .any(|buff| {
3397 matches!(buff.elapsed(Time(time)), dur if (0.1..=0.3).contains(&dur.0))
3398 })
3399 {
3400 multiplicity = 1;
3401 }
3402 let final_amount = multiplicity
3403 * self.scheduler.heartbeats(Duration::from_millis(3)) as usize;
3404 self.add_particles(scene_data.particles_chance, final_amount, || {
3405 let start_pos = pos
3406 + Vec3::unit_z() * body.eye_height(scale.map_or(1.0, |s| s.0))
3407 / 2.0;
3408 let end_pos = start_pos
3409 + Vec3::<f32>::zero()
3410 .map(|_| rng.random_range(-1.0..1.0))
3411 .normalized()
3412 * 5.0;
3413
3414 Particle::new_directed(
3415 Duration::from_secs(2),
3416 time,
3417 ParticleMode::Explosion,
3418 start_pos,
3419 end_pos,
3420 scene_data,
3421 )
3422 })
3423 },
3424 BuffKind::IgniteArrow => {
3425 let final_amount =
3426 usize::from(self.scheduler.heartbeats(Duration::from_millis(150)));
3427 self.add_particles(scene_data.particles_chance, final_amount, || {
3428 let start_pos = pos
3429 + Vec3::unit_z() * body.height() * 0.45
3430 + ori.look_dir().xy().rotated_z(0.6) * body.front_radius() * 2.5
3431 + Vec3::<f32>::zero()
3432 .map(|_| rng.random_range(-1.0..1.0))
3433 .normalized()
3434 * 0.05;
3435 let end_pos = start_pos
3436 + Vec3::unit_z() * 0.7
3437 + Vec3::<f32>::zero()
3438 .map(|_| rng.random_range(-1.0..1.0))
3439 .normalized()
3440 * 0.05;
3441 Particle::new_directed(
3442 Duration::from_secs(1),
3443 time,
3444 ParticleMode::FlameThrower,
3445 start_pos,
3446 end_pos,
3447 scene_data,
3448 )
3449 });
3450 },
3451 BuffKind::FreezeArrow => {
3452 let final_amount =
3453 usize::from(self.scheduler.heartbeats(Duration::from_millis(400)));
3454 self.add_particles(scene_data.particles_chance, final_amount, || {
3455 let start_pos = pos
3456 + Vec3::unit_z() * body.height() * 0.45
3457 + ori.look_dir().xy().rotated_z(0.6) * body.front_radius() * 2.5
3458 + Vec3::<f32>::zero()
3459 .map(|_| rng.random_range(-1.0..1.0))
3460 .normalized()
3461 * 0.05;
3462 let end_pos = start_pos
3463 + Vec3::unit_z() * 1.0
3464 + Vec3::<f32>::zero()
3465 .map(|_| rng.random_range(-1.0..1.0))
3466 .normalized()
3467 * 0.05;
3468 Particle::new_directed(
3469 Duration::from_millis(500),
3470 time,
3471 ParticleMode::Ice,
3472 start_pos,
3473 end_pos,
3474 scene_data,
3475 )
3476 });
3477 },
3478 BuffKind::DrenchArrow => {
3479 let final_amount =
3480 usize::from(self.scheduler.heartbeats(Duration::from_millis(500)));
3481 self.add_particles(scene_data.particles_chance, final_amount, || {
3482 let start_pos = pos
3483 + Vec3::unit_z() * body.height() * 0.45
3484 + ori.look_dir().xy().rotated_z(0.6) * body.front_radius() * 2.5
3485 + Vec3::<f32>::zero()
3486 .map(|_| rng.random_range(-1.0..1.0))
3487 .normalized()
3488 * 0.05;
3489 let end_pos = start_pos - Vec3::unit_z() * 0.7
3490 + Vec3::<f32>::zero()
3491 .map(|_| rng.random_range(-1.0..1.0))
3492 .normalized()
3493 * 0.05;
3494 Particle::new_directed(
3495 Duration::from_secs(1),
3496 time,
3497 ParticleMode::CultistFlame,
3498 start_pos,
3499 end_pos,
3500 scene_data,
3501 )
3502 });
3503 },
3504 BuffKind::JoltArrow => {
3505 let final_amount =
3506 usize::from(self.scheduler.heartbeats(Duration::from_millis(20)));
3507 self.add_particles(scene_data.particles_chance, final_amount, || {
3508 let start_pos = pos
3509 + Vec3::unit_z() * body.height() * 0.45
3510 + ori.look_dir().xy().rotated_z(0.6) * body.front_radius() * 2.5
3511 + Vec3::<f32>::zero()
3512 .map(|_| rng.random_range(-1.0..1.0))
3513 .normalized()
3514 * 0.2;
3515 let end_pos = start_pos
3516 + Vec3::<f32>::zero()
3517 .map(|_| rng.random_range(-1.0..1.0))
3518 .normalized()
3519 * 0.5;
3520 Particle::new_directed(
3521 Duration::from_millis(150),
3522 time,
3523 ParticleMode::ElectricSparks,
3524 start_pos,
3525 end_pos,
3526 scene_data,
3527 )
3528 });
3529 },
3530 _ => {},
3531 }
3532 }
3533 }
3534 }
3535
3536 fn maintain_block_particles(
3537 &mut self,
3538 scene_data: &SceneData,
3539 terrain: &Terrain<TerrainChunk>,
3540 figure_mgr: &FigureMgr,
3541 ) {
3542 prof_span!("ParticleMgr::maintain_block_particles");
3543 let dt = scene_data.state.ecs().fetch::<DeltaTime>().0;
3544 let time = scene_data.state.get_time();
3545 let player_pos = scene_data
3546 .state
3547 .read_component_copied::<Interpolated>(scene_data.viewpoint_entity)
3548 .map(|i| i.pos)
3549 .unwrap_or_default();
3550 let player_chunk = player_pos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| {
3551 (e.floor() as i32).div_euclid(sz as i32)
3552 });
3553
3554 struct BlockParticles<'a> {
3555 blocks: fn(&'a BlocksOfInterest) -> BlockParticleSlice<'a>,
3557 range: usize,
3559 rate: f32,
3561 lifetime: f32,
3563 mode: ParticleMode,
3565 cond: fn(&SceneData) -> bool,
3567 sunlight_level: f32,
3570 }
3571
3572 enum BlockParticleSlice<'a> {
3573 Positions(&'a [Vec3<i32>]),
3574 PositionsAndDirs(&'a [(Vec3<i32>, Vec3<f32>)]),
3575 }
3576
3577 impl BlockParticleSlice<'_> {
3578 fn len(&self) -> usize {
3579 match self {
3580 Self::Positions(blocks) => blocks.len(),
3581 Self::PositionsAndDirs(blocks) => blocks.len(),
3582 }
3583 }
3584 }
3585
3586 let particles: &[BlockParticles] = &[
3587 BlockParticles {
3588 blocks: |boi| BlockParticleSlice::Positions(&boi.leaves),
3589 range: 4,
3590 rate: 0.0125,
3591 lifetime: 30.0,
3592 mode: ParticleMode::Leaf,
3593 cond: |_| true,
3594 sunlight_level: 1.0,
3595 },
3596 BlockParticles {
3597 blocks: |boi| BlockParticleSlice::Positions(&boi.water),
3598 range: 4,
3599 rate: 0.003,
3600 lifetime: 30.0,
3601 mode: ParticleMode::BubbleAmbient,
3603 cond: |_| true,
3604 sunlight_level: 1.0,
3605 },
3606 BlockParticles {
3607 blocks: |boi| BlockParticleSlice::Positions(&boi.cave_roof),
3608 range: 4,
3609 rate: 0.015,
3610 lifetime: 30.0,
3611 mode: ParticleMode::CaveDust,
3612 cond: |_| true,
3613 sunlight_level: 0.0,
3614 },
3615 BlockParticles {
3616 blocks: |boi| BlockParticleSlice::Positions(&boi.drip),
3617 range: 4,
3618 rate: 0.004,
3619 lifetime: 20.0,
3620 mode: ParticleMode::Drip,
3621 cond: |_| true,
3622 sunlight_level: 0.0,
3623 },
3624 BlockParticles {
3625 blocks: |boi| BlockParticleSlice::Positions(&boi.fires),
3626 range: 2,
3627 rate: 50.0,
3628 lifetime: 0.5,
3629 mode: ParticleMode::CampfireFire,
3630 cond: |_| true,
3631 sunlight_level: 1.0,
3632 },
3633 BlockParticles {
3634 blocks: |boi| BlockParticleSlice::Positions(&boi.fire_bowls),
3635 range: 2,
3636 rate: 20.0,
3637 lifetime: 0.25,
3638 mode: ParticleMode::FireBowl,
3639 cond: |_| true,
3640 sunlight_level: 1.0,
3641 },
3642 BlockParticles {
3643 blocks: |boi| BlockParticleSlice::Positions(&boi.fireflies),
3644 range: 6,
3645 rate: 0.004,
3646 lifetime: 40.0,
3647 mode: ParticleMode::Firefly,
3648 cond: |sd| sd.state.get_day_period().is_dark(),
3649 sunlight_level: 1.0,
3650 },
3651 BlockParticles {
3652 blocks: |boi| BlockParticleSlice::Positions(&boi.flowers),
3653 range: 5,
3654 rate: 0.002,
3655 lifetime: 40.0,
3656 mode: ParticleMode::Firefly,
3657 cond: |sd| sd.state.get_day_period().is_dark(),
3658 sunlight_level: 1.0,
3659 },
3660 BlockParticles {
3661 blocks: |boi| BlockParticleSlice::Positions(&boi.beehives),
3662 range: 3,
3663 rate: 0.5,
3664 lifetime: 30.0,
3665 mode: ParticleMode::Bee,
3666 cond: |sd| sd.state.get_day_period().is_light(),
3667 sunlight_level: 1.0,
3668 },
3669 BlockParticles {
3670 blocks: |boi| BlockParticleSlice::Positions(&boi.snow),
3671 range: 4,
3672 rate: 0.025,
3673 lifetime: 15.0,
3674 mode: ParticleMode::Snow,
3675 cond: |_| true,
3676 sunlight_level: 1.0,
3677 },
3678 BlockParticles {
3679 blocks: |boi| BlockParticleSlice::PositionsAndDirs(&boi.one_way_walls),
3680 range: 2,
3681 rate: 12.0,
3682 lifetime: 1.5,
3683 mode: ParticleMode::PortalFizz,
3684 cond: |_| true,
3685 sunlight_level: 1.0,
3686 },
3687 BlockParticles {
3688 blocks: |boi| BlockParticleSlice::Positions(&boi.spores),
3689 range: 4,
3690 rate: 0.055,
3691 lifetime: 20.0,
3692 mode: ParticleMode::Spore,
3693 cond: |_| true,
3694 sunlight_level: 1.0,
3695 },
3696 BlockParticles {
3697 blocks: |boi| BlockParticleSlice::PositionsAndDirs(&boi.waterfall),
3698 range: 2,
3699 rate: 4.0,
3700 lifetime: 5.0,
3701 mode: ParticleMode::WaterFoam,
3702 cond: |_| true,
3703 sunlight_level: 1.0,
3704 },
3705 BlockParticles {
3706 blocks: |boi| BlockParticleSlice::Positions(&boi.train_smokes),
3707 range: 2,
3708 rate: 50.0,
3709 lifetime: 8.0,
3710 mode: ParticleMode::TrainSmoke,
3711 cond: |_| true,
3712 sunlight_level: 1.0,
3713 },
3714 ];
3715
3716 let ecs = scene_data.state.ecs();
3717 let mut rng = rand::rng();
3718 let cap = 512;
3721 for particles in particles.iter() {
3722 if !(particles.cond)(scene_data) {
3723 continue;
3724 }
3725
3726 for offset in Spiral2d::new().take((particles.range * 2 + 1).pow(2)) {
3727 let chunk_pos = player_chunk + offset;
3728
3729 terrain.get(chunk_pos).map(|chunk_data| {
3730 let blocks = (particles.blocks)(&chunk_data.blocks_of_interest);
3731
3732 let avg_particles = dt * (blocks.len() as f32 * particles.rate).min(cap as f32);
3733 let particle_count = avg_particles.trunc() as usize
3734 + (rng.random::<f32>() < avg_particles.fract()) as usize;
3735
3736 self.add_particles(scene_data.particles_chance, particle_count, || {
3737 match blocks {
3738 BlockParticleSlice::Positions(blocks) => {
3739 let block_pos = Vec3::from(
3741 chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32),
3742 ) + blocks.choose(&mut rng).copied().unwrap();
3743 Particle::new(
3744 Duration::from_secs_f32(particles.lifetime),
3745 time,
3746 particles.mode,
3747 block_pos.map(|e: i32| e as f32 + rng.random::<f32>()),
3748 scene_data,
3749 )
3750 .with_light(particles.sunlight_level, 0.0)
3751 },
3752 BlockParticleSlice::PositionsAndDirs(blocks) => {
3753 let (block_offset, particle_dir) =
3755 blocks.choose(&mut rng).copied().unwrap();
3756 let block_pos = Vec3::from(
3757 chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32),
3758 ) + block_offset;
3759 let particle_pos =
3760 block_pos.map(|e: i32| e as f32 + rng.random::<f32>());
3761 Particle::new_directed(
3762 Duration::from_secs_f32(particles.lifetime),
3763 time,
3764 particles.mode,
3765 particle_pos,
3766 particle_pos + particle_dir,
3767 scene_data,
3768 )
3769 .with_light(particles.sunlight_level, 0.0)
3770 },
3771 }
3772 })
3773 });
3774 }
3775
3776 for (entity, body, interpolated, collider) in (
3777 &ecs.entities(),
3778 &ecs.read_storage::<comp::Body>(),
3779 &ecs.read_storage::<crate::ecs::comp::Interpolated>(),
3780 ecs.read_storage::<comp::Collider>().maybe(),
3781 )
3782 .join()
3783 {
3784 if let Some((blocks_of_interest, offset)) =
3785 figure_mgr.get_blocks_of_interest(entity, body, collider)
3786 {
3787 let mat = Mat4::from(interpolated.ori.to_quat())
3788 .translated_3d(interpolated.pos)
3789 * Mat4::translation_3d(offset);
3790
3791 let blocks = (particles.blocks)(blocks_of_interest);
3792
3793 let avg_particles = dt * blocks.len() as f32 * particles.rate;
3794 let particle_count = avg_particles.trunc() as usize
3795 + (rng.random::<f32>() < avg_particles.fract()) as usize;
3796
3797 self.add_particles(scene_data.particles_chance, particle_count, || {
3798 match blocks {
3799 BlockParticleSlice::Positions(blocks) => {
3800 let rel_pos = blocks
3801 .choose(&mut rng)
3802 .copied()
3803 .unwrap()
3805 .map(|e: i32| e as f32 + rng.random::<f32>());
3806 let wpos = mat.mul_point(rel_pos);
3807
3808 Particle::new(
3809 Duration::from_secs_f32(particles.lifetime),
3810 time,
3811 particles.mode,
3812 wpos,
3813 scene_data,
3814 )
3815 },
3816 BlockParticleSlice::PositionsAndDirs(blocks) => {
3817 let (block_offset, particle_dir) =
3819 blocks.choose(&mut rng).copied().unwrap();
3820 let particle_pos =
3821 block_offset.map(|e: i32| e as f32 + rng.random::<f32>());
3822 let wpos = mat.mul_point(particle_pos);
3823 Particle::new_directed(
3824 Duration::from_secs_f32(particles.lifetime),
3825 time,
3826 particles.mode,
3827 wpos,
3828 wpos + mat.mul_direction(particle_dir),
3829 scene_data,
3830 )
3831 },
3832 }
3833 })
3834 }
3835 }
3836 }
3837 {
3839 struct SmokeProperties {
3840 position: Vec3<i32>,
3841 strength: f32,
3842 dry_chance: f32,
3843 }
3844
3845 let range = 8_usize;
3846 let rate = 3.0 / 128.0;
3847 let lifetime = 40.0;
3848 let time_of_day = scene_data
3849 .state
3850 .get_time_of_day()
3851 .rem_euclid(24.0 * 60.0 * 60.0) as f32;
3852
3853 let smokers = Spiral2d::new()
3854 .take((range * 2 + 1).pow(2))
3855 .flat_map(|offset| {
3856 let chunk_pos = player_chunk + offset;
3857 let block_pos =
3858 Vec3::<i32>::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32));
3859 terrain.get(chunk_pos).into_iter().flat_map(move |chunk| {
3860 chunk.blocks_of_interest.smokers.iter().map(move |smoker| {
3861 (
3862 block_pos.as_::<f32>() + smoker.position.as_(),
3863 smoker.kind,
3864 chunk.blocks_of_interest.temperature,
3865 chunk.blocks_of_interest.humidity,
3866 )
3867 })
3868 })
3869 })
3870 .chain(
3871 (
3872 &ecs.entities(),
3873 &ecs.read_storage::<comp::Body>(),
3874 &ecs.read_storage::<crate::ecs::comp::Interpolated>(),
3875 ecs.read_storage::<comp::Collider>().maybe(),
3876 )
3877 .join()
3878 .flat_map(|(entity, body, interpolated, collider)| {
3879 figure_mgr
3880 .get_blocks_of_interest(entity, body, collider)
3881 .into_iter()
3882 .flat_map(|(boi, offset)| {
3883 let mat = Mat4::from(interpolated.ori.to_quat())
3884 .translated_3d(interpolated.pos)
3885 * Mat4::translation_3d(offset);
3886 boi.smokers.iter().map(move |smoker| {
3887 (
3888 mat.mul_point(smoker.position.as_::<f32>() + 0.5),
3889 smoker.kind,
3890 0.0, 0.5,
3892 )
3893 })
3894 })
3895 })
3896 .collect::<Vec<_>>(),
3897 );
3898
3899 let mut smoke_properties: Vec<SmokeProperties> = Vec::new();
3900 let mut sum = 0.0_f32;
3901 for (pos, kind, temperature, humidity) in smokers {
3902 let (strength, dry_chance) = {
3903 match kind {
3904 FireplaceType::House => {
3905 let prop = crate::scene::smoke_cycle::smoke_at_time(
3906 pos.round().as_(),
3907 temperature,
3908 time_of_day,
3909 );
3910 (
3911 prop.0,
3912 if prop.1 {
3913 0.8 - humidity
3915 } else {
3916 1.2 - humidity
3918 },
3919 )
3920 },
3921 FireplaceType::Workshop => (128.0, 1.0),
3922 }
3923 };
3924 sum += strength;
3925 smoke_properties.push(SmokeProperties {
3926 position: pos.round().as_(),
3927 strength,
3928 dry_chance,
3929 });
3930 }
3931 let avg_particles = dt * sum * rate;
3932
3933 let particle_count = avg_particles.trunc() as usize
3934 + (rng.random::<f32>() < avg_particles.fract()) as usize;
3935 let chosen = smoke_properties
3936 .sample_weighted(&mut rng, particle_count, |smoker| smoker.strength);
3937 if let Ok(chosen) = chosen {
3938 self.particles.extend(chosen.map(|smoker| {
3939 Particle::new(
3940 Duration::from_secs_f32(lifetime),
3941 time,
3942 if rng.random::<f32>() > smoker.dry_chance {
3943 ParticleMode::BlackSmoke
3944 } else {
3945 ParticleMode::CampfireSmoke
3946 },
3947 smoker.position.map(|e: i32| e as f32 + rng.random::<f32>()),
3948 scene_data,
3949 )
3950 }));
3951 }
3952 }
3953 }
3954
3955 fn maintain_shockwave_particles(&mut self, scene_data: &SceneData) {
3956 let state = scene_data.state;
3957 let ecs = state.ecs();
3958 let time = state.get_time();
3959 let dt = scene_data.state.ecs().fetch::<DeltaTime>().0;
3960 let terrain = scene_data.state.ecs().fetch::<TerrainGrid>();
3961
3962 for (_entity, interp, pos, ori, shockwave) in (
3963 &ecs.entities(),
3964 ecs.read_storage::<Interpolated>().maybe(),
3965 &ecs.read_storage::<Pos>(),
3966 &ecs.read_storage::<Ori>(),
3967 &ecs.read_storage::<Shockwave>(),
3968 )
3969 .join()
3970 {
3971 let pos = interp.map_or(pos.0, |i| i.pos);
3972 let ori = interp.map_or(*ori, |i| i.ori);
3973
3974 let elapsed = time - shockwave.creation.unwrap_or(time);
3975 let speed = shockwave.properties.speed;
3976
3977 let percent = elapsed as f32 / shockwave.properties.duration.as_secs_f32();
3978
3979 let distance = speed * elapsed as f32;
3980
3981 let radians = shockwave.properties.angle.to_radians();
3982
3983 let ori_vec = ori.look_vec();
3984 let theta = ori_vec.y.atan2(ori_vec.x) - radians / 2.0;
3985 let dtheta = radians / distance;
3986
3987 let arc_length = distance * radians;
3990
3991 use shockwave::FrontendSpecifier;
3992 match shockwave.properties.specifier {
3993 FrontendSpecifier::Ground => {
3994 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(2));
3995 for heartbeat in 0..heartbeats {
3996 let scale = 1.0 / 3.0;
3998
3999 let scaled_speed = speed * scale;
4000
4001 let sub_tick_interpolation = scaled_speed * 1000.0 * heartbeat as f32;
4002
4003 let distance = speed * (elapsed as f32 - sub_tick_interpolation);
4004
4005 let particle_count_factor = radians / (3.0 * scale);
4006 let new_particle_count = distance * particle_count_factor;
4007
4008 for d in 0..(new_particle_count as i32) {
4009 let arc_position = theta + dtheta * d as f32 / particle_count_factor;
4010
4011 let position = pos
4012 + distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0);
4013
4014 let half_ray_length = 10.0;
4018 let mut last_air = false;
4019 let _ = terrain
4027 .ray(
4028 position + Vec3::unit_z() * half_ray_length,
4029 position - Vec3::unit_z() * half_ray_length,
4030 )
4031 .for_each(|block: &Block, pos: Vec3<i32>| {
4032 if block.is_solid() && block.get_sprite().is_none() {
4033 if last_air {
4034 let position = position.xy().with_z(pos.z as f32 + 1.0);
4035
4036 let position_snapped =
4037 ((position / scale).floor() + 0.5) * scale;
4038
4039 self.push_particle(
4040 scene_data.particles_chance,
4041 Particle::new(
4042 Duration::from_millis(250),
4043 time,
4044 ParticleMode::GroundShockwave,
4045 position_snapped,
4046 scene_data,
4047 ),
4048 );
4049 last_air = false;
4050 }
4051 } else {
4052 last_air = true;
4053 }
4054 })
4055 .cast();
4056 }
4057 }
4058 },
4059 FrontendSpecifier::Fire => {
4060 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(2));
4061 for _ in 0..heartbeats {
4062 for d in 0..3 * distance as i32 {
4063 let arc_position = theta + dtheta * d as f32 / 3.0;
4064
4065 let position = pos
4066 + distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0);
4067
4068 self.push_particle(
4069 scene_data.particles_chance,
4070 Particle::new(
4071 Duration::from_secs_f32((distance + 10.0) / 50.0),
4072 time,
4073 ParticleMode::FireShockwave,
4074 position,
4075 scene_data,
4076 ),
4077 );
4078 }
4079 }
4080 },
4081 FrontendSpecifier::FireLow => {
4082 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(2));
4083 for heartbeat in 0..heartbeats {
4084 let scale = 1.0 / 3.0;
4086
4087 let scaled_speed = speed * scale;
4088
4089 let sub_tick_interpolation = scaled_speed * 1000.0 * heartbeat as f32;
4090
4091 let distance = speed * (elapsed as f32 - sub_tick_interpolation);
4092
4093 let particle_count_factor = radians / (3.0 * scale);
4094 let new_particle_count = distance * particle_count_factor;
4095
4096 for d in 0..(new_particle_count as i32) {
4097 let arc_position = theta + dtheta * d as f32 / particle_count_factor;
4098
4099 let position = pos
4100 + distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0);
4101
4102 let half_ray_length = 10.0;
4106 let mut last_air = false;
4107 let _ = terrain
4115 .ray(
4116 position + Vec3::unit_z() * half_ray_length,
4117 position - Vec3::unit_z() * half_ray_length,
4118 )
4119 .for_each(|block: &Block, pos: Vec3<i32>| {
4120 if block.is_solid() && block.get_sprite().is_none() {
4121 if last_air {
4122 let position = position.xy().with_z(pos.z as f32 + 1.0);
4123
4124 let position_snapped =
4125 ((position / scale).floor() + 0.5) * scale;
4126
4127 self.push_particle(
4128 scene_data.particles_chance,
4129 Particle::new(
4130 Duration::from_millis(250),
4131 time,
4132 ParticleMode::FireLowShockwave,
4133 position_snapped,
4134 scene_data,
4135 ),
4136 );
4137 last_air = false;
4138 }
4139 } else {
4140 last_air = true;
4141 }
4142 })
4143 .cast();
4144 }
4145 }
4146 },
4147 FrontendSpecifier::Water => {
4148 let particles_per_length = arc_length as usize;
4150 let dtheta = radians / particles_per_length as f32;
4151 let heartbeats = self
4154 .scheduler
4155 .heartbeats(Duration::from_secs_f32(1.0 / speed));
4156
4157 for i in 0..particles_per_length {
4158 let angle = dtheta * i as f32;
4159 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
4160 for j in 0..heartbeats {
4161 let dt = (j as f32 / heartbeats as f32) * dt;
4163 let distance = distance + speed * dt;
4164 let pos1 = pos + distance * direction - Vec3::unit_z();
4165 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
4166 let time = time + dt as f64;
4167
4168 self.push_particle(
4169 scene_data.particles_chance,
4170 Particle::new_directed(
4171 Duration::from_secs_f32(0.5),
4172 time,
4173 ParticleMode::Water,
4174 pos1,
4175 pos2,
4176 scene_data,
4177 ),
4178 );
4179 }
4180 }
4181 },
4182 FrontendSpecifier::Lightning => {
4183 let particles_per_length = arc_length as usize;
4185 let dtheta = radians / particles_per_length as f32;
4186 let heartbeats = self
4189 .scheduler
4190 .heartbeats(Duration::from_secs_f32(1.0 / speed));
4191
4192 for i in 0..particles_per_length {
4193 let angle = dtheta * i as f32;
4194 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
4195 for j in 0..heartbeats {
4196 let dt = (j as f32 / heartbeats as f32) * dt;
4198 let distance = distance + speed * dt;
4199 let pos1 = pos + distance * direction - Vec3::unit_z();
4200 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
4201 let time = time + dt as f64;
4202
4203 self.push_particle(
4204 scene_data.particles_chance,
4205 Particle::new_directed(
4206 Duration::from_secs_f32(0.5),
4207 time,
4208 ParticleMode::Lightning,
4209 pos1,
4210 pos2,
4211 scene_data,
4212 ),
4213 );
4214 }
4215 }
4216 },
4217 FrontendSpecifier::Steam => {
4218 let particles_per_length = arc_length as usize;
4220 let dtheta = radians / particles_per_length as f32;
4221 let heartbeats = self
4224 .scheduler
4225 .heartbeats(Duration::from_secs_f32(1.0 / speed));
4226
4227 for i in 0..particles_per_length {
4228 let angle = dtheta * i as f32;
4229 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
4230 for j in 0..heartbeats {
4231 let dt = (j as f32 / heartbeats as f32) * dt;
4233 let distance = distance + speed * dt;
4234 let pos1 = pos + distance * direction - Vec3::unit_z();
4235 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
4236 let time = time + dt as f64;
4237
4238 self.push_particle(
4239 scene_data.particles_chance,
4240 Particle::new_directed(
4241 Duration::from_secs_f32(0.5),
4242 time,
4243 ParticleMode::Steam,
4244 pos1,
4245 pos2,
4246 scene_data,
4247 ),
4248 );
4249 }
4250 }
4251 },
4252 FrontendSpecifier::Poison => {
4253 let particles_per_length = arc_length as usize;
4255 let dtheta = radians / particles_per_length as f32;
4256 let heartbeats = self
4259 .scheduler
4260 .heartbeats(Duration::from_secs_f32(1.0 / speed));
4261
4262 for i in 0..particles_per_length {
4263 let angle = theta + dtheta * i as f32;
4264 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
4265 for j in 0..heartbeats {
4266 let dt = (j as f32 / heartbeats as f32) * dt;
4268 let distance = distance + speed * dt;
4269 let pos1 = pos + distance * direction - Vec3::unit_z();
4270 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
4271 let time = time + dt as f64;
4272
4273 self.push_particle(
4274 scene_data.particles_chance,
4275 Particle::new_directed(
4276 Duration::from_secs_f32(0.5),
4277 time,
4278 ParticleMode::Poison,
4279 pos1,
4280 pos2,
4281 scene_data,
4282 ),
4283 );
4284 }
4285 }
4286 },
4287 FrontendSpecifier::AcidCloud => {
4288 let particles_per_height = 5;
4289 let particles_per_length = arc_length as usize;
4291 let dtheta = radians / particles_per_length as f32;
4292 let heartbeats = self
4295 .scheduler
4296 .heartbeats(Duration::from_secs_f32(1.0 / speed));
4297 for i in 0..particles_per_height {
4298 let height = (i as f32 / (particles_per_height as f32 - 1.0)) * 4.0;
4299 for j in 0..particles_per_length {
4300 let angle = theta + dtheta * j as f32;
4301 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
4302 for k in 0..heartbeats {
4303 let dt = (k as f32 / heartbeats as f32) * dt;
4305 let distance = distance + speed * dt;
4306 let pos1 = pos + distance * direction - Vec3::unit_z()
4307 + Vec3::unit_z() * height;
4308 let pos2 = pos1 + direction;
4309 let time = time + dt as f64;
4310
4311 self.push_particle(
4312 scene_data.particles_chance,
4313 Particle::new_directed(
4314 Duration::from_secs_f32(0.5),
4315 time,
4316 ParticleMode::Poison,
4317 pos1,
4318 pos2,
4319 scene_data,
4320 ),
4321 );
4322 }
4323 }
4324 }
4325 },
4326 FrontendSpecifier::Ink => {
4327 let particles_per_length = arc_length as usize;
4329 let dtheta = radians / particles_per_length as f32;
4330 let heartbeats = self
4333 .scheduler
4334 .heartbeats(Duration::from_secs_f32(1.0 / speed));
4335
4336 for i in 0..particles_per_length {
4337 let angle = theta + dtheta * i as f32;
4338 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
4339 for j in 0..heartbeats {
4340 let dt = (j as f32 / heartbeats as f32) * dt;
4342 let distance = distance + speed * dt;
4343 let pos1 = pos + distance * direction - Vec3::unit_z();
4344 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
4345 let time = time + dt as f64;
4346
4347 self.push_particle(
4348 scene_data.particles_chance,
4349 Particle::new_directed(
4350 Duration::from_secs_f32(0.5),
4351 time,
4352 ParticleMode::Ink,
4353 pos1,
4354 pos2,
4355 scene_data,
4356 ),
4357 );
4358 }
4359 }
4360 },
4361 FrontendSpecifier::IceSpikes | FrontendSpecifier::Ice => {
4362 let scale = 1.0 / 3.0;
4364 let scaled_distance = distance / scale;
4365 let scaled_speed = speed / scale;
4366
4367 let particles_per_length = (0.25 * arc_length / scale) as usize;
4369 let dtheta = radians / particles_per_length as f32;
4370 let heartbeats = self
4373 .scheduler
4374 .heartbeats(Duration::from_secs_f32(3.0 / scaled_speed));
4375
4376 let wave = if matches!(shockwave.properties.dodgeable, Dodgeable::Jump) {
4378 0.5
4379 } else {
4380 8.0
4381 };
4382 let height_scale = wave + 1.5 * percent;
4384 for i in 0..particles_per_length {
4385 let angle = theta + dtheta * i as f32;
4386 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
4387 for j in 0..heartbeats {
4388 let dt = (j as f32 / heartbeats as f32) * dt;
4390 let scaled_distance = scaled_distance + scaled_speed * dt;
4391 let mut pos1 = pos + (scaled_distance * direction).floor() * scale;
4392 let time = time + dt as f64;
4393
4394 let half_ray_length = 10.0;
4398 let mut last_air = false;
4399 let _ = terrain
4407 .ray(
4408 pos1 + Vec3::unit_z() * half_ray_length,
4409 pos1 - Vec3::unit_z() * half_ray_length,
4410 )
4411 .for_each(|block: &Block, pos: Vec3<i32>| {
4412 if block.is_solid() && block.get_sprite().is_none() {
4413 if last_air {
4414 pos1 = pos1.xy().with_z(pos.z as f32 + 1.0);
4415 last_air = false;
4416 }
4417 } else {
4418 last_air = true;
4419 }
4420 })
4421 .cast();
4422
4423 let get_positions = |a| {
4424 let pos1 = match a {
4425 2 => pos1 + Vec3::unit_x() * scale,
4426 3 => pos1 - Vec3::unit_x() * scale,
4427 4 => pos1 + Vec3::unit_y() * scale,
4428 5 => pos1 - Vec3::unit_y() * scale,
4429 _ => pos1,
4430 };
4431 let pos2 = if a == 1 {
4432 pos1 + Vec3::unit_z() * 5.0 * height_scale
4433 } else {
4434 pos1 + Vec3::unit_z() * 1.0 * height_scale
4435 };
4436 (pos1, pos2)
4437 };
4438
4439 for a in 1..=5 {
4440 let (pos1, pos2) = get_positions(a);
4441 self.push_particle(
4442 scene_data.particles_chance,
4443 Particle::new_directed(
4444 Duration::from_secs_f32(0.5),
4445 time,
4446 ParticleMode::IceSpikes,
4447 pos1,
4448 pos2,
4449 scene_data,
4450 ),
4451 );
4452 }
4453 }
4454 }
4455 },
4456 }
4457 }
4458 }
4459
4460 fn maintain_marker_particles(&mut self, scene_data: &SceneData) {
4461 let state = scene_data.state;
4462 let ecs = state.ecs();
4463 let time = state.get_time();
4464 let mut rng = rand::rng();
4465
4466 for (interp, pos, vel, marker) in (
4467 ecs.read_storage::<Interpolated>().maybe(),
4468 &ecs.read_storage::<Pos>(),
4469 ecs.read_storage::<Vel>().maybe(),
4470 &ecs.read_storage::<comp::FrontendMarker>(),
4471 )
4472 .join()
4473 {
4474 let pos = interp.map_or(pos.0, |i| i.pos);
4475
4476 use comp::{FrontendMarker, visual::TorusMode};
4477 match marker {
4478 FrontendMarker::IgniteArrow => {
4479 let final_amount =
4480 usize::from(self.scheduler.heartbeats(Duration::from_millis(150)));
4481 self.add_particles(scene_data.particles_chance, final_amount, || {
4482 let start_pos = pos
4483 + Vec3::<f32>::zero()
4484 .map(|_| rng.random_range(-1.0..1.0))
4485 .normalized()
4486 * 0.05;
4487 let end_pos = start_pos
4488 + Vec3::unit_z() * 0.7
4489 + Vec3::<f32>::zero()
4490 .map(|_| rng.random_range(-1.0..1.0))
4491 .normalized()
4492 * 0.05;
4493 Particle::new_directed(
4494 Duration::from_secs(1),
4495 time,
4496 ParticleMode::FlameThrower,
4497 start_pos,
4498 end_pos,
4499 scene_data,
4500 )
4501 });
4502 },
4503 FrontendMarker::FreezeArrow => {
4504 let final_amount =
4505 usize::from(self.scheduler.heartbeats(Duration::from_millis(400)));
4506 self.add_particles(scene_data.particles_chance, final_amount, || {
4507 let start_pos = pos
4508 + Vec3::<f32>::zero()
4509 .map(|_| rng.random_range(-1.0..1.0))
4510 .normalized()
4511 * 0.05;
4512 let end_pos = start_pos
4513 + Vec3::unit_z() * 1.0
4514 + Vec3::<f32>::zero()
4515 .map(|_| rng.random_range(-1.0..1.0))
4516 .normalized()
4517 * 0.05;
4518 Particle::new_directed(
4519 Duration::from_millis(500),
4520 time,
4521 ParticleMode::Ice,
4522 start_pos,
4523 end_pos,
4524 scene_data,
4525 )
4526 });
4527 },
4528 FrontendMarker::DrenchArrow => {
4529 let final_amount =
4530 usize::from(self.scheduler.heartbeats(Duration::from_millis(500)));
4531 self.add_particles(scene_data.particles_chance, final_amount, || {
4532 let start_pos = pos
4533 + Vec3::<f32>::zero()
4534 .map(|_| rng.random_range(-1.0..1.0))
4535 .normalized()
4536 * 0.05;
4537 let end_pos = start_pos - Vec3::unit_z() * 0.7
4538 + Vec3::<f32>::zero()
4539 .map(|_| rng.random_range(-1.0..1.0))
4540 .normalized()
4541 * 0.05;
4542 Particle::new_directed(
4543 Duration::from_secs(1),
4544 time,
4545 ParticleMode::CultistFlame,
4546 start_pos,
4547 end_pos,
4548 scene_data,
4549 )
4550 });
4551 },
4552 FrontendMarker::JoltArrow => {
4553 let final_amount =
4554 usize::from(self.scheduler.heartbeats(Duration::from_millis(20)));
4555 self.add_particles(scene_data.particles_chance, final_amount, || {
4556 let start_pos = pos
4557 + Vec3::<f32>::zero()
4558 .map(|_| rng.random_range(-1.0..1.0))
4559 .normalized()
4560 * 0.2;
4561 let end_pos = start_pos
4562 + Vec3::<f32>::zero()
4563 .map(|_| rng.random_range(-1.0..1.0))
4564 .normalized()
4565 * 0.5;
4566 Particle::new_directed(
4567 Duration::from_millis(150),
4568 time,
4569 ParticleMode::ElectricSparks,
4570 start_pos,
4571 end_pos,
4572 scene_data,
4573 )
4574 });
4575 },
4576 FrontendMarker::Torus(major_r, torus_mode) => {
4577 let time = scene_data.state.get_time();
4578 let mut rng = rand::rng();
4579 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
4580
4581 let fwd = vel
4583 .map(|v| v.0)
4584 .unwrap_or(Vec3::unit_y())
4585 .try_normalized()
4586 .unwrap_or(Vec3::unit_y());
4587 let right = fwd
4588 .cross(Vec3::unit_z())
4589 .try_normalized()
4590 .unwrap_or(Vec3::unit_x());
4591 let up = right.cross(fwd);
4592
4593 let major_r = *major_r;
4594 let minor_r: f32 = major_r / 1.5;
4595 let flame_reach: f32 = fwd.magnitude();
4596
4597 self.add_particles(
4598 scene_data.particles_chance,
4599 usize::from(heartbeats) * 8,
4600 || {
4601 let u = rng.random_range(0.0..TAU); let v = rng.random_range(0.0..TAU); let radial = u.cos() * right + u.sin() * up;
4605 let ring_center = pos + major_r * radial;
4606 let tube_pos =
4607 ring_center + minor_r * v.cos() * radial + minor_r * v.sin() * fwd;
4608
4609 let mode = match torus_mode {
4610 TorusMode::RedBlueFire => {
4611 if rng.random_bool(0.4) {
4612 ParticleMode::FlamethrowerBlue
4613 } else {
4614 ParticleMode::FlameThrower
4615 }
4616 },
4617 };
4618
4619 let lifespan: Duration = Duration::from_millis(220);
4621 Particle {
4622 alive_until: time + lifespan.as_secs_f64(),
4623 instance: ParticleInstance::new_directed(
4624 time,
4625 lifespan.as_secs_f32(),
4626 mode,
4627 tube_pos,
4628 tube_pos + radial * flame_reach,
4629 Vec2::zero(),
4630 ),
4631 }
4632 },
4633 );
4634 },
4635 }
4636 }
4637 }
4638
4639 fn maintain_arcing_particles(&mut self, scene_data: &SceneData) {
4640 let state = scene_data.state;
4641 let ecs = state.ecs();
4642 let time = state.get_time();
4643 let mut rng = rand::rng();
4644 let id_maps = ecs.read_resource::<IdMaps>();
4645
4646 for (interp, pos, arcing) in (
4647 ecs.read_storage::<Interpolated>().maybe(),
4648 &ecs.read_storage::<Pos>(),
4649 &ecs.read_storage::<comp::Arcing>(),
4650 )
4651 .join()
4652 {
4653 let pos = interp.map_or(pos.0, |i| i.pos);
4654 let body = arcing
4655 .hit_entities
4656 .last()
4657 .and_then(|uid| id_maps.uid_entity(*uid))
4658 .and_then(|e| ecs.read_storage::<Body>().get(e).copied());
4659 let height = body.map_or(2.0, |b| b.height());
4660 let radius = body.map_or(1.0, |b| b.max_radius());
4661 let pos = pos + Vec3::unit_z() * height / 2.0;
4662 let final_amount = usize::from(self.scheduler.heartbeats(Duration::from_millis(5)));
4663 self.add_particles(scene_data.particles_chance, final_amount, || {
4664 let start_pos = pos
4665 + Vec3::<f32>::zero()
4666 .map(|_| rng.random_range(-1.0..1.0))
4667 .normalized()
4668 * radius;
4669 let end_pos = start_pos
4670 + Vec3::<f32>::zero()
4671 .map(|_| rng.random_range(-1.0..1.0))
4672 .normalized()
4673 * (radius + 0.5);
4674 Particle::new_directed(
4675 Duration::from_millis(200),
4676 time,
4677 ParticleMode::ElectricSparks,
4678 start_pos,
4679 end_pos,
4680 scene_data,
4681 )
4682 });
4683
4684 let num = arcing.hit_entities.len();
4685 if num > 1 && (time - arcing.last_arc_time.0 < arcing.properties.min_delay.0) {
4686 let last_pos = {
4687 let last_hit = arcing
4688 .hit_entities
4689 .get(num - 2)
4690 .and_then(|uid| id_maps.uid_entity(*uid));
4691 let pos = last_hit.and_then(|e| ecs.read_storage::<Pos>().get(e).map(|p| p.0));
4692 let height = last_hit
4693 .and_then(|e| ecs.read_storage::<Body>().get(e).map(|b| b.height()))
4694 .unwrap_or(2.0);
4695 pos.map(|p| p + Vec3::unit_z() * height / 2.0)
4696 };
4697
4698 if let Some(last_pos) = last_pos {
4699 let vector = last_pos - pos;
4700 let dist = vector.magnitude();
4701 let ctrl = pos + vector / 2.0 + Vec3::unit_z() * dist;
4702 let bezier = QuadraticBezier3 {
4703 start: last_pos,
4704 ctrl,
4705 end: pos,
4706 };
4707 let segments = (dist * 1.0).ceil() as i32 + 2;
4708 for segment in 0..(segments - 1) {
4709 let t_0 = segment as f32 / segments as f32;
4710 let t_1 = (segment + 2) as f32 / segments as f32;
4711 let final_amount =
4712 usize::from(self.scheduler.heartbeats(Duration::from_millis(30)));
4713 self.add_particles(scene_data.particles_chance, final_amount, || {
4714 let start_pos = bezier.evaluate(t_0)
4715 + Vec3::<f32>::zero()
4716 .map(|_| rng.random_range(-1.0..1.0))
4717 .normalized()
4718 * 0.2;
4719 let end_pos = bezier.evaluate(t_1)
4720 + Vec3::<f32>::zero()
4721 .map(|_| rng.random_range(-1.0..1.0))
4722 .normalized()
4723 * 0.2;
4724 Particle::new_directed(
4725 Duration::from_millis(150),
4726 time,
4727 ParticleMode::ElectricSparks,
4728 start_pos,
4729 end_pos,
4730 scene_data,
4731 )
4732 });
4733 }
4734 }
4735 }
4736 }
4737 }
4738
4739 fn maintain_pool_particles(&mut self, scene_data: &SceneData) {
4740 prof_span!("ParticleMgr::maintain_pool_particles");
4741 let state = scene_data.state;
4742 let ecs = state.ecs();
4743 let time = state.get_time();
4744 let mut rng = rand::rng();
4745
4746 for (interp, pos, pool) in (
4747 ecs.read_storage::<Interpolated>().maybe(),
4748 &ecs.read_storage::<Pos>(),
4749 &ecs.read_storage::<comp::Pool>(),
4750 )
4751 .join()
4752 {
4753 let pos = interp.map_or(pos.0, |i| i.pos);
4754 let radius = pool.properties.radius;
4755
4756 let final_amount = usize::from(self.scheduler.heartbeats(Duration::from_millis(20)));
4758 self.add_particles(scene_data.particles_chance, final_amount, || {
4759 Particle::new(
4760 Duration::from_millis(700),
4761 time,
4762 ParticleMode::CampfireFire,
4763 pos + Vec3::new(
4764 rng.random_range(-radius..radius),
4765 rng.random_range(-radius..radius),
4766 0.1,
4767 ),
4768 scene_data,
4769 )
4770 });
4771
4772 let final_amount = usize::from(self.scheduler.heartbeats(Duration::from_millis(60)));
4774 self.add_particles(scene_data.particles_chance, final_amount, || {
4775 Particle::new(
4776 Duration::from_secs(6),
4777 time,
4778 ParticleMode::CampfireSmoke,
4779 pos + Vec3::new(
4780 rng.random_range(-radius * 0.6..radius * 0.6),
4781 rng.random_range(-radius * 0.6..radius * 0.6),
4782 rng.random_range(0.5..1.5),
4783 ),
4784 scene_data,
4785 )
4786 });
4787 }
4788 }
4789
4790 fn upload_particles(&mut self, renderer: &mut Renderer) {
4791 prof_span!("ParticleMgr::upload_particles");
4792 let all_cpu_instances = self
4793 .particles
4794 .iter()
4795 .map(|p| p.instance)
4796 .collect::<Vec<ParticleInstance>>();
4797
4798 let gpu_instances = renderer.create_instances(&all_cpu_instances);
4800
4801 self.instances = gpu_instances;
4802 }
4803
4804 pub fn render<'a>(&'a self, drawer: &mut ParticleDrawer<'_, 'a>, scene_data: &SceneData) {
4805 prof_span!("ParticleMgr::render");
4806 if scene_data.particles_enabled {
4807 let model = &self
4808 .model_cache
4809 .get(DEFAULT_MODEL_KEY)
4810 .expect("Expected particle model in cache");
4811
4812 drawer.draw(model, &self.instances);
4813 }
4814 }
4815
4816 pub fn particle_count(&self) -> usize { self.instances.count() }
4817
4818 pub fn particle_count_visible(&self) -> usize { self.instances.count() }
4819}
4820
4821fn default_instances(renderer: &mut Renderer) -> Instances<ParticleInstance> {
4822 let empty_vec = Vec::new();
4823
4824 renderer.create_instances(&empty_vec)
4825}
4826
4827const DEFAULT_MODEL_KEY: &str = "voxygen.voxel.particle";
4828
4829fn default_cache(renderer: &mut Renderer) -> HashMap<&'static str, Model<ParticleVertex>> {
4830 let mut model_cache = HashMap::new();
4831
4832 model_cache.entry(DEFAULT_MODEL_KEY).or_insert_with(|| {
4833 let vox = DotVox::load_expect(DEFAULT_MODEL_KEY);
4834
4835 let max_texture_size = renderer.max_texture_size();
4838 let max_size = Vec2::from(u16::try_from(max_texture_size).unwrap_or(u16::MAX));
4839 let mut greedy = GreedyMesh::new(max_size, crate::mesh::greedy::general_config());
4840
4841 let segment = Segment::from_vox_model_index(&vox.read().0, 0, None);
4842 let segment_size = segment.size();
4843 let mut mesh = generate_mesh_base_vol_particle(segment, &mut greedy).0;
4844 for vert in mesh.vertices_mut() {
4846 vert.pos[0] -= segment_size.x as f32 / 2.0;
4847 vert.pos[1] -= segment_size.y as f32 / 2.0;
4848 vert.pos[2] -= segment_size.z as f32 / 2.0;
4849 }
4850
4851 drop(greedy);
4853
4854 renderer
4855 .create_model(&mesh)
4856 .expect("Failed to create particle model")
4857 });
4858
4859 model_cache
4860}
4861
4862struct HeartbeatScheduler {
4864 timers: HashMap<Duration, (f64, u8)>,
4872
4873 last_known_time: f64,
4874}
4875
4876impl HeartbeatScheduler {
4877 pub fn new() -> Self {
4878 HeartbeatScheduler {
4879 timers: HashMap::new(),
4880 last_known_time: 0.0,
4881 }
4882 }
4883
4884 pub fn maintain(&mut self, now: f64) {
4887 prof_span!("HeartbeatScheduler::maintain");
4888 self.last_known_time = now;
4889
4890 for (frequency, (last_update, heartbeats)) in self.timers.iter_mut() {
4891 let total_heartbeats = (now - *last_update) / frequency.as_secs_f64();
4893
4894 let full_heartbeats = total_heartbeats.floor();
4896
4897 *heartbeats = full_heartbeats as u8;
4898
4899 let partial_heartbeat = total_heartbeats - full_heartbeats;
4901
4902 let partial_heartbeat_as_time = frequency.mul_f64(partial_heartbeat).as_secs_f64();
4904
4905 *last_update = now - partial_heartbeat_as_time;
4909 }
4910 }
4911
4912 pub fn heartbeats(&mut self, frequency: Duration) -> u8 {
4919 prof_span!("HeartbeatScheduler::heartbeats");
4920 let last_known_time = self.last_known_time;
4921
4922 self.timers
4923 .entry(frequency)
4924 .or_insert_with(|| (last_known_time, 0))
4925 .1
4926 }
4927
4928 pub fn clear(&mut self) { self.timers.clear() }
4929}
4930
4931#[derive(Clone, Copy)]
4932struct Particle {
4933 alive_until: f64, instance: ParticleInstance,
4935}
4936
4937impl Particle {
4938 fn new(
4939 lifespan: Duration,
4940 time: f64,
4941 mode: ParticleMode,
4942 pos: Vec3<f32>,
4943 scene_data: &SceneData,
4944 ) -> Self {
4945 Particle {
4946 alive_until: time + lifespan.as_secs_f64(),
4947 instance: ParticleInstance::new(
4948 time,
4949 lifespan.as_secs_f32(),
4950 mode,
4951 pos,
4952 scene_data.wind_vel,
4953 ),
4954 }
4955 }
4956
4957 fn new_directed(
4958 lifespan: Duration,
4959 time: f64,
4960 mode: ParticleMode,
4961 pos1: Vec3<f32>,
4962 pos2: Vec3<f32>,
4963 scene_data: &SceneData,
4964 ) -> Self {
4965 Particle {
4966 alive_until: time + lifespan.as_secs_f64(),
4967 instance: ParticleInstance::new_directed(
4968 time,
4969 lifespan.as_secs_f32(),
4970 mode,
4971 pos1,
4972 pos2,
4973 scene_data.wind_vel,
4974 ),
4975 }
4976 }
4977
4978 pub fn with_light(self, sun_light: f32, glow_light: f32) -> Self {
4979 Self {
4980 instance: self.instance.with_light(sun_light, glow_light),
4981 ..self
4982 }
4983 }
4984
4985 fn new_colored(
4986 lifespan: Duration,
4987 time: f64,
4988 mode: ParticleMode,
4989 pos: Vec3<f32>,
4990 col: Rgb<f32>,
4991 scene_data: &SceneData,
4992 ) -> Self {
4993 Particle {
4994 alive_until: time + lifespan.as_secs_f64(),
4995 instance: ParticleInstance::new_colored(
4996 time,
4997 lifespan.as_secs_f32(),
4998 mode,
4999 pos,
5000 col,
5001 scene_data.wind_vel,
5002 ),
5003 }
5004 }
5005
5006 fn new_directed_with_collision(
5007 lifespan: Duration,
5008 time: f64,
5009 mode: ParticleMode,
5010 pos1: Vec3<f32>,
5011 pos2: Vec3<f32>,
5012 scene_data: &SceneData,
5013 distance: impl Fn(Vec3<f32>, Vec3<f32>) -> f32,
5014 ) -> Self {
5015 let dir = pos2 - pos1;
5016 let end_distance = pos1.distance(pos2);
5017 let (end_pos, lifespawn) = if end_distance > 0.1 {
5018 let ratio = distance(pos1, pos2) / end_distance;
5019 (pos1 + ratio * dir, lifespan.mul_f32(ratio))
5020 } else {
5021 (pos2, lifespan)
5022 };
5023
5024 Self::new_directed(lifespawn, time, mode, pos1, end_pos, scene_data)
5025 }
5026}