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