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 =
3270 smoke_properties
3271 .choose_multiple_weighted(&mut rng, particle_count, |smoker| smoker.strength);
3272 if let Ok(chosen) = chosen {
3273 self.particles.extend(chosen.map(|smoker| {
3274 Particle::new(
3275 Duration::from_secs_f32(lifetime),
3276 time,
3277 if rng.random::<f32>() > smoker.dry_chance {
3278 ParticleMode::BlackSmoke
3279 } else {
3280 ParticleMode::CampfireSmoke
3281 },
3282 smoker.position.map(|e: i32| e as f32 + rng.random::<f32>()),
3283 scene_data,
3284 )
3285 }));
3286 }
3287 }
3288 }
3289
3290 fn maintain_shockwave_particles(&mut self, scene_data: &SceneData) {
3291 let state = scene_data.state;
3292 let ecs = state.ecs();
3293 let time = state.get_time();
3294 let dt = scene_data.state.ecs().fetch::<DeltaTime>().0;
3295 let terrain = scene_data.state.ecs().fetch::<TerrainGrid>();
3296
3297 for (_entity, interp, pos, ori, shockwave) in (
3298 &ecs.entities(),
3299 ecs.read_storage::<Interpolated>().maybe(),
3300 &ecs.read_storage::<Pos>(),
3301 &ecs.read_storage::<Ori>(),
3302 &ecs.read_storage::<Shockwave>(),
3303 )
3304 .join()
3305 {
3306 let pos = interp.map_or(pos.0, |i| i.pos);
3307 let ori = interp.map_or(*ori, |i| i.ori);
3308
3309 let elapsed = time - shockwave.creation.unwrap_or(time);
3310 let speed = shockwave.properties.speed;
3311
3312 let percent = elapsed as f32 / shockwave.properties.duration.as_secs_f32();
3313
3314 let distance = speed * elapsed as f32;
3315
3316 let radians = shockwave.properties.angle.to_radians();
3317
3318 let ori_vec = ori.look_vec();
3319 let theta = ori_vec.y.atan2(ori_vec.x) - radians / 2.0;
3320 let dtheta = radians / distance;
3321
3322 let arc_length = distance * radians;
3325
3326 use shockwave::FrontendSpecifier;
3327 match shockwave.properties.specifier {
3328 FrontendSpecifier::Ground => {
3329 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(2));
3330 for heartbeat in 0..heartbeats {
3331 let scale = 1.0 / 3.0;
3333
3334 let scaled_speed = speed * scale;
3335
3336 let sub_tick_interpolation = scaled_speed * 1000.0 * heartbeat as f32;
3337
3338 let distance = speed * (elapsed as f32 - sub_tick_interpolation);
3339
3340 let particle_count_factor = radians / (3.0 * scale);
3341 let new_particle_count = distance * particle_count_factor;
3342 self.particles.reserve(new_particle_count as usize);
3343
3344 for d in 0..(new_particle_count as i32) {
3345 let arc_position = theta + dtheta * d as f32 / particle_count_factor;
3346
3347 let position = pos
3348 + distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0);
3349
3350 let half_ray_length = 10.0;
3354 let mut last_air = false;
3355 let _ = terrain
3363 .ray(
3364 position + Vec3::unit_z() * half_ray_length,
3365 position - Vec3::unit_z() * half_ray_length,
3366 )
3367 .for_each(|block: &Block, pos: Vec3<i32>| {
3368 if block.is_solid() && block.get_sprite().is_none() {
3369 if last_air {
3370 let position = position.xy().with_z(pos.z as f32 + 1.0);
3371
3372 let position_snapped =
3373 ((position / scale).floor() + 0.5) * scale;
3374
3375 self.particles.push(Particle::new(
3376 Duration::from_millis(250),
3377 time,
3378 ParticleMode::GroundShockwave,
3379 position_snapped,
3380 scene_data,
3381 ));
3382 last_air = false;
3383 }
3384 } else {
3385 last_air = true;
3386 }
3387 })
3388 .cast();
3389 }
3390 }
3391 },
3392 FrontendSpecifier::Fire => {
3393 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(2));
3394 for _ in 0..heartbeats {
3395 for d in 0..3 * distance as i32 {
3396 let arc_position = theta + dtheta * d as f32 / 3.0;
3397
3398 let position = pos
3399 + distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0);
3400
3401 self.particles.push(Particle::new(
3402 Duration::from_secs_f32((distance + 10.0) / 50.0),
3403 time,
3404 ParticleMode::FireShockwave,
3405 position,
3406 scene_data,
3407 ));
3408 }
3409 }
3410 },
3411 FrontendSpecifier::FireLow => {
3412 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(2));
3413 for heartbeat in 0..heartbeats {
3414 let scale = 1.0 / 3.0;
3416
3417 let scaled_speed = speed * scale;
3418
3419 let sub_tick_interpolation = scaled_speed * 1000.0 * heartbeat as f32;
3420
3421 let distance = speed * (elapsed as f32 - sub_tick_interpolation);
3422
3423 let particle_count_factor = radians / (3.0 * scale);
3424 let new_particle_count = distance * particle_count_factor;
3425 self.particles.reserve(new_particle_count as usize);
3426
3427 for d in 0..(new_particle_count as i32) {
3428 let arc_position = theta + dtheta * d as f32 / particle_count_factor;
3429
3430 let position = pos
3431 + distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0);
3432
3433 let half_ray_length = 10.0;
3437 let mut last_air = false;
3438 let _ = terrain
3446 .ray(
3447 position + Vec3::unit_z() * half_ray_length,
3448 position - Vec3::unit_z() * half_ray_length,
3449 )
3450 .for_each(|block: &Block, pos: Vec3<i32>| {
3451 if block.is_solid() && block.get_sprite().is_none() {
3452 if last_air {
3453 let position = position.xy().with_z(pos.z as f32 + 1.0);
3454
3455 let position_snapped =
3456 ((position / scale).floor() + 0.5) * scale;
3457
3458 self.particles.push(Particle::new(
3459 Duration::from_millis(250),
3460 time,
3461 ParticleMode::FireLowShockwave,
3462 position_snapped,
3463 scene_data,
3464 ));
3465 last_air = false;
3466 }
3467 } else {
3468 last_air = true;
3469 }
3470 })
3471 .cast();
3472 }
3473 }
3474 },
3475 FrontendSpecifier::Water => {
3476 let particles_per_length = arc_length as usize;
3478 let dtheta = radians / particles_per_length as f32;
3479 let heartbeats = self
3482 .scheduler
3483 .heartbeats(Duration::from_secs_f32(1.0 / speed));
3484
3485 let new_particle_count = particles_per_length * heartbeats as usize;
3487 self.particles.reserve(new_particle_count);
3488
3489 for i in 0..particles_per_length {
3490 let angle = dtheta * i as f32;
3491 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3492 for j in 0..heartbeats {
3493 let dt = (j as f32 / heartbeats as f32) * dt;
3495 let distance = distance + speed * dt;
3496 let pos1 = pos + distance * direction - Vec3::unit_z();
3497 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
3498 let time = time + dt as f64;
3499
3500 self.particles.push(Particle::new_directed(
3501 Duration::from_secs_f32(0.5),
3502 time,
3503 ParticleMode::Water,
3504 pos1,
3505 pos2,
3506 scene_data,
3507 ));
3508 }
3509 }
3510 },
3511 FrontendSpecifier::Lightning => {
3512 let particles_per_length = arc_length as usize;
3514 let dtheta = radians / particles_per_length as f32;
3515 let heartbeats = self
3518 .scheduler
3519 .heartbeats(Duration::from_secs_f32(1.0 / speed));
3520
3521 let new_particle_count = particles_per_length * heartbeats as usize;
3523 self.particles.reserve(new_particle_count);
3524
3525 for i in 0..particles_per_length {
3526 let angle = dtheta * i as f32;
3527 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3528 for j in 0..heartbeats {
3529 let dt = (j as f32 / heartbeats as f32) * dt;
3531 let distance = distance + speed * dt;
3532 let pos1 = pos + distance * direction - Vec3::unit_z();
3533 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
3534 let time = time + dt as f64;
3535
3536 self.particles.push(Particle::new_directed(
3537 Duration::from_secs_f32(0.5),
3538 time,
3539 ParticleMode::Lightning,
3540 pos1,
3541 pos2,
3542 scene_data,
3543 ));
3544 }
3545 }
3546 },
3547 FrontendSpecifier::Steam => {
3548 let particles_per_length = arc_length as usize;
3550 let dtheta = radians / particles_per_length as f32;
3551 let heartbeats = self
3554 .scheduler
3555 .heartbeats(Duration::from_secs_f32(1.0 / speed));
3556
3557 let new_particle_count = particles_per_length * heartbeats as usize;
3559 self.particles.reserve(new_particle_count);
3560
3561 for i in 0..particles_per_length {
3562 let angle = dtheta * i as f32;
3563 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3564 for j in 0..heartbeats {
3565 let dt = (j as f32 / heartbeats as f32) * dt;
3567 let distance = distance + speed * dt;
3568 let pos1 = pos + distance * direction - Vec3::unit_z();
3569 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
3570 let time = time + dt as f64;
3571
3572 self.particles.push(Particle::new_directed(
3573 Duration::from_secs_f32(0.5),
3574 time,
3575 ParticleMode::Steam,
3576 pos1,
3577 pos2,
3578 scene_data,
3579 ));
3580 }
3581 }
3582 },
3583 FrontendSpecifier::Poison => {
3584 let particles_per_length = arc_length as usize;
3586 let dtheta = radians / particles_per_length as f32;
3587 let heartbeats = self
3590 .scheduler
3591 .heartbeats(Duration::from_secs_f32(1.0 / speed));
3592
3593 let new_particle_count = particles_per_length * heartbeats as usize;
3595 self.particles.reserve(new_particle_count);
3596
3597 for i in 0..particles_per_length {
3598 let angle = theta + dtheta * i as f32;
3599 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3600 for j in 0..heartbeats {
3601 let dt = (j as f32 / heartbeats as f32) * dt;
3603 let distance = distance + speed * dt;
3604 let pos1 = pos + distance * direction - Vec3::unit_z();
3605 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
3606 let time = time + dt as f64;
3607
3608 self.particles.push(Particle::new_directed(
3609 Duration::from_secs_f32(0.5),
3610 time,
3611 ParticleMode::Poison,
3612 pos1,
3613 pos2,
3614 scene_data,
3615 ));
3616 }
3617 }
3618 },
3619 FrontendSpecifier::AcidCloud => {
3620 let particles_per_height = 5;
3621 let particles_per_length = arc_length as usize;
3623 let dtheta = radians / particles_per_length as f32;
3624 let heartbeats = self
3627 .scheduler
3628 .heartbeats(Duration::from_secs_f32(1.0 / speed));
3629
3630 let new_particle_count =
3632 particles_per_length * heartbeats as usize * particles_per_height;
3633 self.particles.reserve(new_particle_count);
3634
3635 for i in 0..particles_per_height {
3636 let height = (i as f32 / (particles_per_height as f32 - 1.0)) * 4.0;
3637 for j in 0..particles_per_length {
3638 let angle = theta + dtheta * j as f32;
3639 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3640 for k in 0..heartbeats {
3641 let dt = (k as f32 / heartbeats as f32) * dt;
3643 let distance = distance + speed * dt;
3644 let pos1 = pos + distance * direction - Vec3::unit_z()
3645 + Vec3::unit_z() * height;
3646 let pos2 = pos1 + direction;
3647 let time = time + dt as f64;
3648
3649 self.particles.push(Particle::new_directed(
3650 Duration::from_secs_f32(0.5),
3651 time,
3652 ParticleMode::Poison,
3653 pos1,
3654 pos2,
3655 scene_data,
3656 ));
3657 }
3658 }
3659 }
3660 },
3661 FrontendSpecifier::Ink => {
3662 let particles_per_length = arc_length as usize;
3664 let dtheta = radians / particles_per_length as f32;
3665 let heartbeats = self
3668 .scheduler
3669 .heartbeats(Duration::from_secs_f32(1.0 / speed));
3670
3671 let new_particle_count = particles_per_length * heartbeats as usize;
3673 self.particles.reserve(new_particle_count);
3674
3675 for i in 0..particles_per_length {
3676 let angle = theta + dtheta * i as f32;
3677 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3678 for j in 0..heartbeats {
3679 let dt = (j as f32 / heartbeats as f32) * dt;
3681 let distance = distance + speed * dt;
3682 let pos1 = pos + distance * direction - Vec3::unit_z();
3683 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
3684 let time = time + dt as f64;
3685
3686 self.particles.push(Particle::new_directed(
3687 Duration::from_secs_f32(0.5),
3688 time,
3689 ParticleMode::Ink,
3690 pos1,
3691 pos2,
3692 scene_data,
3693 ));
3694 }
3695 }
3696 },
3697 FrontendSpecifier::IceSpikes | FrontendSpecifier::Ice => {
3698 let scale = 1.0 / 3.0;
3700 let scaled_distance = distance / scale;
3701 let scaled_speed = speed / scale;
3702
3703 let particles_per_length = (0.25 * arc_length / scale) as usize;
3705 let dtheta = radians / particles_per_length as f32;
3706 let heartbeats = self
3709 .scheduler
3710 .heartbeats(Duration::from_secs_f32(3.0 / scaled_speed));
3711
3712 let new_particle_count = particles_per_length * heartbeats as usize;
3714 self.particles.reserve(new_particle_count);
3715 let wave = if matches!(shockwave.properties.dodgeable, Dodgeable::Jump) {
3717 0.5
3718 } else {
3719 8.0
3720 };
3721 let height_scale = wave + 1.5 * percent;
3723 for i in 0..particles_per_length {
3724 let angle = theta + dtheta * i as f32;
3725 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3726 for j in 0..heartbeats {
3727 let dt = (j as f32 / heartbeats as f32) * dt;
3729 let scaled_distance = scaled_distance + scaled_speed * dt;
3730 let mut pos1 = pos + (scaled_distance * direction).floor() * scale;
3731 let time = time + dt as f64;
3732
3733 let half_ray_length = 10.0;
3737 let mut last_air = false;
3738 let _ = terrain
3746 .ray(
3747 pos1 + Vec3::unit_z() * half_ray_length,
3748 pos1 - Vec3::unit_z() * half_ray_length,
3749 )
3750 .for_each(|block: &Block, pos: Vec3<i32>| {
3751 if block.is_solid() && block.get_sprite().is_none() {
3752 if last_air {
3753 pos1 = pos1.xy().with_z(pos.z as f32 + 1.0);
3754 last_air = false;
3755 }
3756 } else {
3757 last_air = true;
3758 }
3759 })
3760 .cast();
3761
3762 let get_positions = |a| {
3763 let pos1 = match a {
3764 2 => pos1 + Vec3::unit_x() * scale,
3765 3 => pos1 - Vec3::unit_x() * scale,
3766 4 => pos1 + Vec3::unit_y() * scale,
3767 5 => pos1 - Vec3::unit_y() * scale,
3768 _ => pos1,
3769 };
3770 let pos2 = if a == 1 {
3771 pos1 + Vec3::unit_z() * 5.0 * height_scale
3772 } else {
3773 pos1 + Vec3::unit_z() * 1.0 * height_scale
3774 };
3775 (pos1, pos2)
3776 };
3777
3778 for a in 1..=5 {
3779 let (pos1, pos2) = get_positions(a);
3780 self.particles.push(Particle::new_directed(
3781 Duration::from_secs_f32(0.5),
3782 time,
3783 ParticleMode::IceSpikes,
3784 pos1,
3785 pos2,
3786 scene_data,
3787 ));
3788 }
3789 }
3790 }
3791 },
3792 }
3793 }
3794 }
3795
3796 fn maintain_stance_particles(&mut self, scene_data: &SceneData) {
3797 let state = scene_data.state;
3798 let ecs = state.ecs();
3799 let time = state.get_time();
3800 let mut rng = rand::rng();
3801
3802 for (interp, pos, stance, body, ori) in (
3803 ecs.read_storage::<Interpolated>().maybe(),
3804 &ecs.read_storage::<Pos>(),
3805 &ecs.read_storage::<comp::Stance>(),
3806 &ecs.read_storage::<Body>(),
3807 &ecs.read_storage::<Ori>(),
3808 )
3809 .join()
3810 {
3811 let pos = interp.map_or(pos.0, |i| i.pos);
3812
3813 use comp::ability::{BowStance, Stance};
3814 match stance {
3815 Stance::Bow(BowStance::IgniteArrow) => {
3816 self.particles.resize_with(
3817 self.particles.len()
3818 + usize::from(self.scheduler.heartbeats(Duration::from_millis(150))),
3819 || {
3820 let start_pos = pos
3821 + Vec3::unit_z() * body.height() * 0.45
3822 + ori.look_dir().xy().rotated_z(0.6) * body.front_radius() * 2.5
3823 + Vec3::<f32>::zero()
3824 .map(|_| rng.random_range(-1.0..1.0))
3825 .normalized()
3826 * 0.05;
3827 let end_pos = start_pos
3828 + Vec3::unit_z() * 0.7
3829 + Vec3::<f32>::zero()
3830 .map(|_| rng.random_range(-1.0..1.0))
3831 .normalized()
3832 * 0.05;
3833 Particle::new_directed(
3834 Duration::from_secs(1),
3835 time,
3836 ParticleMode::FlameThrower,
3837 start_pos,
3838 end_pos,
3839 scene_data,
3840 )
3841 },
3842 );
3843 },
3844 Stance::Bow(BowStance::DrenchArrow) => {
3845 self.particles.resize_with(
3846 self.particles.len()
3847 + usize::from(self.scheduler.heartbeats(Duration::from_millis(500))),
3848 || {
3849 let start_pos = pos
3850 + Vec3::unit_z() * body.height() * 0.45
3851 + ori.look_dir().xy().rotated_z(0.6) * body.front_radius() * 2.5
3852 + Vec3::<f32>::zero()
3853 .map(|_| rng.random_range(-1.0..1.0))
3854 .normalized()
3855 * 0.05;
3856 let end_pos = start_pos - Vec3::unit_z() * 0.7
3857 + Vec3::<f32>::zero()
3858 .map(|_| rng.random_range(-1.0..1.0))
3859 .normalized()
3860 * 0.05;
3861 Particle::new_directed(
3862 Duration::from_secs(1),
3863 time,
3864 ParticleMode::CultistFlame,
3865 start_pos,
3866 end_pos,
3867 scene_data,
3868 )
3869 },
3870 );
3871 },
3872 Stance::Bow(BowStance::FreezeArrow) => {
3873 self.particles.resize_with(
3874 self.particles.len()
3875 + usize::from(self.scheduler.heartbeats(Duration::from_millis(400))),
3876 || {
3877 let start_pos = pos
3878 + Vec3::unit_z() * body.height() * 0.45
3879 + ori.look_dir().xy().rotated_z(0.6) * body.front_radius() * 2.5
3880 + Vec3::<f32>::zero()
3881 .map(|_| rng.random_range(-1.0..1.0))
3882 .normalized()
3883 * 0.05;
3884 let end_pos = start_pos
3885 + Vec3::unit_z() * 1.0
3886 + Vec3::<f32>::zero()
3887 .map(|_| rng.random_range(-1.0..1.0))
3888 .normalized()
3889 * 0.05;
3890 Particle::new_directed(
3891 Duration::from_millis(500),
3892 time,
3893 ParticleMode::Ice,
3894 start_pos,
3895 end_pos,
3896 scene_data,
3897 )
3898 },
3899 );
3900 },
3901 Stance::Bow(BowStance::JoltArrow) => {
3902 self.particles.resize_with(
3903 self.particles.len()
3904 + usize::from(self.scheduler.heartbeats(Duration::from_millis(20))),
3905 || {
3906 let start_pos = pos
3907 + Vec3::unit_z() * body.height() * 0.45
3908 + ori.look_dir().xy().rotated_z(0.6) * body.front_radius() * 2.5
3909 + Vec3::<f32>::zero()
3910 .map(|_| rng.random_range(-1.0..1.0))
3911 .normalized()
3912 * 0.2;
3913 let end_pos = start_pos
3914 + Vec3::<f32>::zero()
3915 .map(|_| rng.random_range(-1.0..1.0))
3916 .normalized()
3917 * 0.5;
3918 Particle::new_directed(
3919 Duration::from_millis(150),
3920 time,
3921 ParticleMode::ElectricSparks,
3922 start_pos,
3923 end_pos,
3924 scene_data,
3925 )
3926 },
3927 );
3928 },
3929 _ => {},
3930 }
3931 }
3932 }
3933
3934 fn maintain_marker_particles(&mut self, scene_data: &SceneData) {
3935 let state = scene_data.state;
3936 let ecs = state.ecs();
3937 let time = state.get_time();
3938 let mut rng = rand::rng();
3939
3940 for (interp, pos, marker) in (
3941 ecs.read_storage::<Interpolated>().maybe(),
3942 &ecs.read_storage::<Pos>(),
3943 &ecs.read_storage::<comp::FrontendMarker>(),
3944 )
3945 .join()
3946 {
3947 let pos = interp.map_or(pos.0, |i| i.pos);
3948
3949 use comp::FrontendMarker;
3950 match marker {
3951 FrontendMarker::JoltArrow => {
3952 self.particles.resize_with(
3953 self.particles.len()
3954 + usize::from(self.scheduler.heartbeats(Duration::from_millis(20))),
3955 || {
3956 let start_pos = pos
3957 + Vec3::<f32>::zero()
3958 .map(|_| rng.random_range(-1.0..1.0))
3959 .normalized()
3960 * 0.2;
3961 let end_pos = start_pos
3962 + Vec3::<f32>::zero()
3963 .map(|_| rng.random_range(-1.0..1.0))
3964 .normalized()
3965 * 0.5;
3966 Particle::new_directed(
3967 Duration::from_millis(150),
3968 time,
3969 ParticleMode::ElectricSparks,
3970 start_pos,
3971 end_pos,
3972 scene_data,
3973 )
3974 },
3975 );
3976 },
3977 }
3978 }
3979 }
3980
3981 fn maintain_arcing_particles(&mut self, scene_data: &SceneData) {
3982 let state = scene_data.state;
3983 let ecs = state.ecs();
3984 let time = state.get_time();
3985 let mut rng = rand::rng();
3986 let id_maps = ecs.read_resource::<IdMaps>();
3987
3988 for (interp, pos, arcing) in (
3989 ecs.read_storage::<Interpolated>().maybe(),
3990 &ecs.read_storage::<Pos>(),
3991 &ecs.read_storage::<comp::Arcing>(),
3992 )
3993 .join()
3994 {
3995 let pos = interp.map_or(pos.0, |i| i.pos);
3996 let body = arcing
3997 .hit_entities
3998 .last()
3999 .and_then(|uid| id_maps.uid_entity(*uid))
4000 .and_then(|e| ecs.read_storage::<Body>().get(e).copied());
4001 let height = body.map_or(2.0, |b| b.height());
4002 let radius = body.map_or(1.0, |b| b.max_radius());
4003 let pos = pos + Vec3::unit_z() * height / 2.0;
4004 self.particles.resize_with(
4005 self.particles.len()
4006 + usize::from(self.scheduler.heartbeats(Duration::from_millis(5))),
4007 || {
4008 let start_pos = pos
4009 + Vec3::<f32>::zero()
4010 .map(|_| rng.random_range(-1.0..1.0))
4011 .normalized()
4012 * radius;
4013 let end_pos = start_pos
4014 + Vec3::<f32>::zero()
4015 .map(|_| rng.random_range(-1.0..1.0))
4016 .normalized()
4017 * (radius + 0.5);
4018 Particle::new_directed(
4019 Duration::from_millis(200),
4020 time,
4021 ParticleMode::ElectricSparks,
4022 start_pos,
4023 end_pos,
4024 scene_data,
4025 )
4026 },
4027 );
4028
4029 let num = arcing.hit_entities.len();
4030 if num > 1 && (time - arcing.last_arc_time.0 < arcing.properties.min_delay.0) {
4031 let last_pos = {
4032 let last_hit = arcing
4033 .hit_entities
4034 .get(num - 2)
4035 .and_then(|uid| id_maps.uid_entity(*uid));
4036 let pos = last_hit.and_then(|e| ecs.read_storage::<Pos>().get(e).map(|p| p.0));
4037 let height = last_hit
4038 .and_then(|e| ecs.read_storage::<Body>().get(e).map(|b| b.height()))
4039 .unwrap_or(2.0);
4040 pos.map(|p| p + Vec3::unit_z() * height / 2.0)
4041 };
4042
4043 if let Some(last_pos) = last_pos {
4044 let vector = last_pos - pos;
4045 let dist = vector.magnitude();
4046 let ctrl = pos + vector / 2.0 + Vec3::unit_z() * dist;
4047 let bezier = QuadraticBezier3 {
4048 start: last_pos,
4049 ctrl,
4050 end: pos,
4051 };
4052 let segments = (dist * 1.0).ceil() as i32 + 2;
4053 for segment in 0..(segments - 1) {
4054 let t_0 = segment as f32 / segments as f32;
4055 let t_1 = (segment + 2) as f32 / segments as f32;
4056 self.particles.resize_with(
4057 self.particles.len()
4058 + usize::from(self.scheduler.heartbeats(Duration::from_millis(30))),
4059 || {
4060 let start_pos = bezier.evaluate(t_0)
4061 + Vec3::<f32>::zero()
4062 .map(|_| rng.random_range(-1.0..1.0))
4063 .normalized()
4064 * 0.2;
4065 let end_pos = bezier.evaluate(t_1)
4066 + Vec3::<f32>::zero()
4067 .map(|_| rng.random_range(-1.0..1.0))
4068 .normalized()
4069 * 0.2;
4070 Particle::new_directed(
4071 Duration::from_millis(150),
4072 time,
4073 ParticleMode::ElectricSparks,
4074 start_pos,
4075 end_pos,
4076 scene_data,
4077 )
4078 },
4079 );
4080 }
4081 }
4082 }
4083 }
4084 }
4085
4086 fn upload_particles(&mut self, renderer: &mut Renderer) {
4087 prof_span!("ParticleMgr::upload_particles");
4088 let all_cpu_instances = self
4089 .particles
4090 .iter()
4091 .map(|p| p.instance)
4092 .collect::<Vec<ParticleInstance>>();
4093
4094 let gpu_instances = renderer.create_instances(&all_cpu_instances);
4096
4097 self.instances = gpu_instances;
4098 }
4099
4100 pub fn render<'a>(&'a self, drawer: &mut ParticleDrawer<'_, 'a>, scene_data: &SceneData) {
4101 prof_span!("ParticleMgr::render");
4102 if scene_data.particles_enabled {
4103 let model = &self
4104 .model_cache
4105 .get(DEFAULT_MODEL_KEY)
4106 .expect("Expected particle model in cache");
4107
4108 drawer.draw(model, &self.instances);
4109 }
4110 }
4111
4112 pub fn particle_count(&self) -> usize { self.instances.count() }
4113
4114 pub fn particle_count_visible(&self) -> usize { self.instances.count() }
4115}
4116
4117fn default_instances(renderer: &mut Renderer) -> Instances<ParticleInstance> {
4118 let empty_vec = Vec::new();
4119
4120 renderer.create_instances(&empty_vec)
4121}
4122
4123const DEFAULT_MODEL_KEY: &str = "voxygen.voxel.particle";
4124
4125fn default_cache(renderer: &mut Renderer) -> HashMap<&'static str, Model<ParticleVertex>> {
4126 let mut model_cache = HashMap::new();
4127
4128 model_cache.entry(DEFAULT_MODEL_KEY).or_insert_with(|| {
4129 let vox = DotVox::load_expect(DEFAULT_MODEL_KEY);
4130
4131 let max_texture_size = renderer.max_texture_size();
4134 let max_size = Vec2::from(u16::try_from(max_texture_size).unwrap_or(u16::MAX));
4135 let mut greedy = GreedyMesh::new(max_size, crate::mesh::greedy::general_config());
4136
4137 let segment = Segment::from_vox_model_index(&vox.read().0, 0, None);
4138 let segment_size = segment.size();
4139 let mut mesh = generate_mesh_base_vol_particle(segment, &mut greedy).0;
4140 for vert in mesh.vertices_mut() {
4142 vert.pos[0] -= segment_size.x as f32 / 2.0;
4143 vert.pos[1] -= segment_size.y as f32 / 2.0;
4144 vert.pos[2] -= segment_size.z as f32 / 2.0;
4145 }
4146
4147 drop(greedy);
4149
4150 renderer
4151 .create_model(&mesh)
4152 .expect("Failed to create particle model")
4153 });
4154
4155 model_cache
4156}
4157
4158struct HeartbeatScheduler {
4160 timers: HashMap<Duration, (f64, u8)>,
4168
4169 last_known_time: f64,
4170}
4171
4172impl HeartbeatScheduler {
4173 pub fn new() -> Self {
4174 HeartbeatScheduler {
4175 timers: HashMap::new(),
4176 last_known_time: 0.0,
4177 }
4178 }
4179
4180 pub fn maintain(&mut self, now: f64) {
4183 prof_span!("HeartbeatScheduler::maintain");
4184 self.last_known_time = now;
4185
4186 for (frequency, (last_update, heartbeats)) in self.timers.iter_mut() {
4187 let total_heartbeats = (now - *last_update) / frequency.as_secs_f64();
4189
4190 let full_heartbeats = total_heartbeats.floor();
4192
4193 *heartbeats = full_heartbeats as u8;
4194
4195 let partial_heartbeat = total_heartbeats - full_heartbeats;
4197
4198 let partial_heartbeat_as_time = frequency.mul_f64(partial_heartbeat).as_secs_f64();
4200
4201 *last_update = now - partial_heartbeat_as_time;
4205 }
4206 }
4207
4208 pub fn heartbeats(&mut self, frequency: Duration) -> u8 {
4215 prof_span!("HeartbeatScheduler::heartbeats");
4216 let last_known_time = self.last_known_time;
4217
4218 self.timers
4219 .entry(frequency)
4220 .or_insert_with(|| (last_known_time, 0))
4221 .1
4222 }
4223
4224 pub fn clear(&mut self) { self.timers.clear() }
4225}
4226
4227#[derive(Clone, Copy)]
4228struct Particle {
4229 alive_until: f64, instance: ParticleInstance,
4231}
4232
4233impl Particle {
4234 fn new(
4235 lifespan: Duration,
4236 time: f64,
4237 mode: ParticleMode,
4238 pos: Vec3<f32>,
4239 scene_data: &SceneData,
4240 ) -> Self {
4241 Particle {
4242 alive_until: time + lifespan.as_secs_f64(),
4243 instance: ParticleInstance::new(
4244 time,
4245 lifespan.as_secs_f32(),
4246 mode,
4247 pos,
4248 scene_data.wind_vel,
4249 ),
4250 }
4251 }
4252
4253 fn new_directed(
4254 lifespan: Duration,
4255 time: f64,
4256 mode: ParticleMode,
4257 pos1: Vec3<f32>,
4258 pos2: Vec3<f32>,
4259 scene_data: &SceneData,
4260 ) -> Self {
4261 Particle {
4262 alive_until: time + lifespan.as_secs_f64(),
4263 instance: ParticleInstance::new_directed(
4264 time,
4265 lifespan.as_secs_f32(),
4266 mode,
4267 pos1,
4268 pos2,
4269 scene_data.wind_vel,
4270 ),
4271 }
4272 }
4273
4274 fn new_directed_with_collision(
4275 lifespan: Duration,
4276 time: f64,
4277 mode: ParticleMode,
4278 pos1: Vec3<f32>,
4279 pos2: Vec3<f32>,
4280 scene_data: &SceneData,
4281 distance: impl Fn(Vec3<f32>, Vec3<f32>) -> f32,
4282 ) -> Self {
4283 let dir = pos2 - pos1;
4284 let end_distance = pos1.distance(pos2);
4285 let (end_pos, lifespawn) = if end_distance > 0.1 {
4286 let ratio = distance(pos1, pos2) / end_distance;
4287 (pos1 + ratio * dir, lifespan.mul_f32(ratio))
4288 } else {
4289 (pos2, lifespan)
4290 };
4291
4292 Self::new_directed(lifespawn, time, mode, pos1, end_pos, scene_data)
4293 }
4294}