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