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