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