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