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, body, scale, inv, physics) in (
705 &ecs.entities(),
706 &ecs.read_storage::<Interpolated>(),
707 &ecs.read_storage::<Body>(),
708 ecs.read_storage::<Scale>().maybe(),
709 &ecs.read_storage::<Inventory>(),
710 &ecs.read_storage::<PhysicsState>(),
711 )
712 .join()
713 {
714 for item in inv.equipped_items() {
715 if let ItemDefinitionId::Simple(str) = item.item_definition_id() {
716 if &*str == "common.items.armor.misc.head.pipe" {
717 self.maintain_pipe_particles(
718 scene_data,
719 figure_mgr,
720 entity,
721 interpolated.pos,
722 body,
723 scale,
724 physics,
725 )
726 }
727 }
728 }
729 }
730 }
731
732 fn maintain_pipe_particles(
733 &mut self,
734 scene_data: &SceneData,
735 figure_mgr: &FigureMgr,
736 entity: Entity,
737 pos: Vec3<f32>,
738 body: &Body,
739 scale: Option<&Scale>,
740 physics: &PhysicsState,
741 ) {
742 prof_span!("ParticleMgr::maintain_pipe_particles");
743 if physics
744 .in_liquid()
745 .is_none_or(|depth| body.eye_height(scale.map_or(1.0, |scale| scale.0)) > depth)
746 {
747 let Body::Humanoid(body) = body else {
748 return;
749 };
750 let Some(skeleton) = figure_mgr
751 .states
752 .character_states
753 .get(&entity)
754 .map(|state| &state.computed_skeleton)
755 else {
756 return;
757 };
758 let time = scene_data.state.get_time();
759
760 use body::humanoid::{BodyType::*, Species::*};
762 let pipe_offset = match (body.species, body.body_type) {
763 (Orc, Male) => Vec3::new(5.5, 10.5, 0.0),
764 (Orc, Female) => Vec3::new(4.5, 10.0, -2.5),
765 (Human, Male) => Vec3::new(4.5, 12.0, -3.0),
766 (Human, Female) => Vec3::new(4.5, 11.5, -3.0),
767 (Elf, Male) => Vec3::new(4.5, 12.0, -3.0),
768 (Elf, Female) => Vec3::new(4.5, 9.5, -3.0),
769 (Dwarf, Male) => Vec3::new(4.5, 11.0, -4.0),
770 (Dwarf, Female) => Vec3::new(4.5, 11.0, -3.0),
771 (Draugr, Male) => Vec3::new(4.5, 9.5, -0.75),
772 (Draugr, Female) => Vec3::new(4.5, 9.5, -2.0),
773 (Danari, Male) => Vec3::new(4.5, 10.5, -1.25),
774 (Danari, Female) => Vec3::new(4.5, 10.5, -1.25),
775 };
776
777 let mut rng = rand::thread_rng();
778 let dt = scene_data.state.get_delta_time();
779 if rng.gen_bool((0.16 * dt as f64).min(1.0)) {
780 self.particles.resize_with(self.particles.len() + 10, || {
781 Particle::new(
782 Duration::from_millis(1500),
783 time,
784 ParticleMode::PipeSmoke,
785 pos + skeleton.head.mul_point(pipe_offset),
786 )
787 });
788 }
789 }
790 }
791
792 fn maintain_body_particles(&mut self, scene_data: &SceneData) {
793 prof_span!("ParticleMgr::maintain_body_particles");
794 let ecs = scene_data.state.ecs();
795 for (body, interpolated, vel) in (
796 &ecs.read_storage::<Body>(),
797 &ecs.read_storage::<Interpolated>(),
798 ecs.read_storage::<Vel>().maybe(),
799 )
800 .join()
801 {
802 match body {
803 Body::Object(object::Body::CampfireLit) => {
804 self.maintain_campfirelit_particles(scene_data, interpolated.pos, vel)
805 },
806 Body::Object(object::Body::BarrelOrgan) => {
807 self.maintain_barrel_organ_particles(scene_data, interpolated.pos, vel)
808 },
809 Body::Object(object::Body::BoltFire) => {
810 self.maintain_boltfire_particles(scene_data, interpolated.pos, vel)
811 },
812 Body::Object(object::Body::BoltFireBig) => {
813 self.maintain_boltfirebig_particles(scene_data, interpolated.pos, vel)
814 },
815 Body::Object(object::Body::FireRainDrop) => {
816 self.maintain_fireraindrop_particles(scene_data, interpolated.pos, vel)
817 },
818 Body::Object(object::Body::BoltNature) => {
819 self.maintain_boltnature_particles(scene_data, interpolated.pos, vel)
820 },
821 Body::Object(object::Body::Tornado) => {
822 self.maintain_tornado_particles(scene_data, interpolated.pos)
823 },
824 Body::Object(object::Body::FieryTornado) => {
825 self.maintain_fiery_tornado_particles(scene_data, interpolated.pos)
826 },
827 Body::Object(object::Body::Mine) => {
828 self.maintain_mine_particles(scene_data, interpolated.pos)
829 },
830 Body::Object(
831 object::Body::Bomb
832 | object::Body::FireworkBlue
833 | object::Body::FireworkGreen
834 | object::Body::FireworkPurple
835 | object::Body::FireworkRed
836 | object::Body::FireworkWhite
837 | object::Body::FireworkYellow
838 | object::Body::IronPikeBomb,
839 ) => self.maintain_bomb_particles(scene_data, interpolated.pos, vel),
840 Body::Object(object::Body::PortalActive) => {
841 self.maintain_active_portal_particles(scene_data, interpolated.pos)
842 },
843 Body::Object(object::Body::Portal) => {
844 self.maintain_portal_particles(scene_data, interpolated.pos)
845 },
846 Body::BipedLarge(biped_large::Body {
847 species: biped_large::Species::Gigasfire,
848 ..
849 }) => self.maintain_fire_gigas_particles(scene_data, interpolated.pos),
850 _ => {},
851 }
852 }
853 }
854
855 fn maintain_fire_gigas_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
856 let time = scene_data.state.get_time();
857 let mut rng = rand::thread_rng();
858
859 if rng.gen_bool(0.05) {
860 self.particles.resize_with(self.particles.len() + 1, || {
861 let rand_offset = Vec3::new(
862 rng.gen_range(-5.0..5.0),
863 rng.gen_range(-5.0..5.0),
864 rng.gen_range(7.0..15.0),
865 );
866
867 Particle::new(
868 Duration::from_secs_f32(30.0),
869 time,
870 ParticleMode::FireGigasAsh,
871 pos + rand_offset,
872 )
873 });
874 }
875 }
876
877 fn maintain_hydra_tail_swipe_particles(
878 &mut self,
879 scene_data: &SceneData,
880 figure_mgr: &FigureMgr,
881 entity: Entity,
882 pos: Vec3<f32>,
883 body: &Body,
884 state: &CharacterState,
885 inventory: Option<&Inventory>,
886 ) {
887 let Some(ability_id) = state
888 .ability_info()
889 .and_then(|info| info.ability.map(|a| a.ability_id(Some(state), inventory)))
890 else {
891 return;
892 };
893
894 if ability_id != Some("common.abilities.custom.hydra.tail_swipe") {
895 return;
896 }
897
898 let Some(stage_section) = state.stage_section() else {
899 return;
900 };
901
902 let particle_count = match stage_section {
903 StageSection::Charge => 1,
904 StageSection::Action => 10,
905 _ => return,
906 };
907
908 let Some(skeleton) = figure_mgr
909 .states
910 .quadruped_low_states
911 .get(&entity)
912 .map(|state| &state.computed_skeleton)
913 else {
914 return;
915 };
916 let Some(attr) = anim::quadruped_low::SkeletonAttr::try_from(body).ok() else {
917 return;
918 };
919
920 let start = (skeleton.tail_front * Vec4::unit_w()).xyz();
921 let end = (skeleton.tail_rear * Vec4::new(0.0, -attr.tail_rear_length, 0.0, 1.0)).xyz();
922
923 let start = pos + start;
924 let end = pos + end;
925
926 let time = scene_data.state.get_time();
927 let mut rng = thread_rng();
928
929 self.particles.resize_with(
930 self.particles.len()
931 + particle_count * self.scheduler.heartbeats(Duration::from_millis(33)) as usize,
932 || {
933 let t = rng.gen_range(0.0..1.0);
934 let p = start * t + end * (1.0 - t) - Vec3::new(0.0, 0.0, 0.5);
935
936 Particle::new(
937 Duration::from_millis(500),
938 time,
939 ParticleMode::GroundShockwave,
940 p,
941 )
942 },
943 );
944 }
945
946 fn maintain_campfirelit_particles(
947 &mut self,
948 scene_data: &SceneData,
949 pos: Vec3<f32>,
950 vel: Option<&Vel>,
951 ) {
952 prof_span!("ParticleMgr::maintain_campfirelit_particles");
953 let time = scene_data.state.get_time();
954 let dt = scene_data.state.get_delta_time();
955 let mut rng = thread_rng();
956
957 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(50)) {
958 self.particles.push(Particle::new(
959 Duration::from_millis(250),
960 time,
961 ParticleMode::CampfireFire,
962 pos,
963 ));
964
965 self.particles.push(Particle::new(
966 Duration::from_secs(10),
967 time,
968 ParticleMode::CampfireSmoke,
969 pos.map(|e| e + thread_rng().gen_range(-0.25..0.25))
970 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
971 ));
972 }
973 }
974
975 fn maintain_barrel_organ_particles(
976 &mut self,
977 scene_data: &SceneData,
978 pos: Vec3<f32>,
979 vel: Option<&Vel>,
980 ) {
981 prof_span!("ParticleMgr::maintain_barrel_organ_particles");
982 let time = scene_data.state.get_time();
983 let dt = scene_data.state.get_delta_time();
984 let mut rng = thread_rng();
985
986 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(20)) {
987 self.particles.push(Particle::new(
988 Duration::from_millis(250),
989 time,
990 ParticleMode::BarrelOrgan,
991 pos,
992 ));
993
994 self.particles.push(Particle::new(
995 Duration::from_secs(10),
996 time,
997 ParticleMode::BarrelOrgan,
998 pos.map(|e| e + thread_rng().gen_range(-0.25..0.25))
999 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
1000 ));
1001 }
1002 }
1003
1004 fn maintain_boltfire_particles(
1005 &mut self,
1006 scene_data: &SceneData,
1007 pos: Vec3<f32>,
1008 vel: Option<&Vel>,
1009 ) {
1010 prof_span!("ParticleMgr::maintain_boltfire_particles");
1011 let time = scene_data.state.get_time();
1012 let dt = scene_data.state.get_delta_time();
1013 let mut rng = thread_rng();
1014
1015 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(4)) {
1016 self.particles.push(Particle::new(
1017 Duration::from_millis(500),
1018 time,
1019 ParticleMode::CampfireFire,
1020 pos,
1021 ));
1022 self.particles.push(Particle::new(
1023 Duration::from_secs(1),
1024 time,
1025 ParticleMode::CampfireSmoke,
1026 pos.map(|e| e + rng.gen_range(-0.25..0.25))
1027 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
1028 ));
1029 }
1030 }
1031
1032 fn maintain_boltfirebig_particles(
1033 &mut self,
1034 scene_data: &SceneData,
1035 pos: Vec3<f32>,
1036 vel: Option<&Vel>,
1037 ) {
1038 prof_span!("ParticleMgr::maintain_boltfirebig_particles");
1039 let time = scene_data.state.get_time();
1040 let dt = scene_data.state.get_delta_time();
1041 let mut rng = thread_rng();
1042
1043 self.particles.resize_with(
1045 self.particles.len() + usize::from(self.scheduler.heartbeats(Duration::from_millis(2))),
1046 || {
1047 Particle::new(
1048 Duration::from_millis(500),
1049 time,
1050 ParticleMode::CampfireFire,
1051 pos.map(|e| e + rng.gen_range(-0.25..0.25))
1052 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
1053 )
1054 },
1055 );
1056
1057 self.particles.resize_with(
1059 self.particles.len() + usize::from(self.scheduler.heartbeats(Duration::from_millis(5))),
1060 || {
1061 Particle::new(
1062 Duration::from_secs(2),
1063 time,
1064 ParticleMode::CampfireSmoke,
1065 pos.map(|e| e + rng.gen_range(-0.25..0.25))
1066 + vel.map_or(Vec3::zero(), |v| -v.0 * dt),
1067 )
1068 },
1069 );
1070 }
1071
1072 fn maintain_fireraindrop_particles(
1073 &mut self,
1074 scene_data: &SceneData,
1075 pos: Vec3<f32>,
1076 vel: Option<&Vel>,
1077 ) {
1078 prof_span!("ParticleMgr::maintain_fireraindrop_particles");
1079 let time = scene_data.state.get_time();
1080 let dt = scene_data.state.get_delta_time();
1081 let mut rng = thread_rng();
1082
1083 self.particles.resize_with(
1085 self.particles.len()
1086 + usize::from(self.scheduler.heartbeats(Duration::from_millis(100))),
1087 || {
1088 Particle::new(
1089 Duration::from_millis(300),
1090 time,
1091 ParticleMode::FieryDropletTrace,
1092 pos.map(|e| e + rng.gen_range(-0.25..0.25))
1093 + Vec3::new(0.0, 0.0, 0.5)
1094 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
1095 )
1096 },
1097 );
1098 }
1099
1100 fn maintain_boltnature_particles(
1101 &mut self,
1102 scene_data: &SceneData,
1103 pos: Vec3<f32>,
1104 vel: Option<&Vel>,
1105 ) {
1106 let time = scene_data.state.get_time();
1107 let dt = scene_data.state.get_delta_time();
1108 let mut rng = thread_rng();
1109
1110 self.particles.resize_with(
1112 self.particles.len() + usize::from(self.scheduler.heartbeats(Duration::from_millis(2))),
1113 || {
1114 Particle::new(
1115 Duration::from_millis(500),
1116 time,
1117 ParticleMode::CampfireSmoke,
1118 pos.map(|e| e + rng.gen_range(-0.25..0.25))
1119 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
1120 )
1121 },
1122 );
1123 }
1124
1125 fn maintain_tornado_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1126 let time = scene_data.state.get_time();
1127 let mut rng = thread_rng();
1128
1129 self.particles.resize_with(
1131 self.particles.len() + usize::from(self.scheduler.heartbeats(Duration::from_millis(5))),
1132 || {
1133 Particle::new(
1134 Duration::from_millis(1000),
1135 time,
1136 ParticleMode::Tornado,
1137 pos.map(|e| e + rng.gen_range(-0.25..0.25)),
1138 )
1139 },
1140 );
1141 }
1142
1143 fn maintain_fiery_tornado_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1144 let time = scene_data.state.get_time();
1145 let mut rng = thread_rng();
1146
1147 self.particles.resize_with(
1149 self.particles.len() + usize::from(self.scheduler.heartbeats(Duration::from_millis(5))),
1150 || {
1151 Particle::new(
1152 Duration::from_millis(1000),
1153 time,
1154 ParticleMode::FieryTornado,
1155 pos.map(|e| e + rng.gen_range(-0.25..0.25)),
1156 )
1157 },
1158 );
1159 }
1160
1161 fn maintain_bomb_particles(
1162 &mut self,
1163 scene_data: &SceneData,
1164 pos: Vec3<f32>,
1165 vel: Option<&Vel>,
1166 ) {
1167 prof_span!("ParticleMgr::maintain_bomb_particles");
1168 let time = scene_data.state.get_time();
1169 let dt = scene_data.state.get_delta_time();
1170 let mut rng = thread_rng();
1171
1172 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(10)) {
1173 self.particles.push(Particle::new(
1175 Duration::from_millis(1500),
1176 time,
1177 ParticleMode::GunPowderSpark,
1178 pos,
1179 ));
1180
1181 self.particles.push(Particle::new(
1183 Duration::from_secs(2),
1184 time,
1185 ParticleMode::CampfireSmoke,
1186 pos + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
1187 ));
1188 }
1189 }
1190
1191 fn maintain_active_portal_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1192 prof_span!("ParticleMgr::maintain_active_portal_particles");
1193
1194 let time = scene_data.state.get_time();
1195 let mut rng = thread_rng();
1196
1197 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(5)) {
1198 let outer_pos =
1199 pos + (Vec2::unit_x().rotated_z(rng.gen_range((0.)..PI * 2.)) * 2.7).with_z(0.);
1200
1201 self.particles.push(Particle::new_directed(
1202 Duration::from_secs_f32(rng.gen_range(0.4..0.8)),
1203 time,
1204 ParticleMode::CultistFlame,
1205 outer_pos,
1206 outer_pos + Vec3::unit_z() * rng.gen_range(5.0..7.0),
1207 ));
1208 }
1209 }
1210
1211 fn maintain_portal_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1212 prof_span!("ParticleMgr::maintain_portal_particles");
1213
1214 let time = scene_data.state.get_time();
1215 let mut rng = thread_rng();
1216
1217 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(150)) {
1218 let outer_pos = pos
1219 + (Vec2::unit_x().rotated_z(rng.gen_range((0.)..PI * 2.))
1220 * rng.gen_range(1.0..2.9))
1221 .with_z(0.);
1222
1223 self.particles.push(Particle::new_directed(
1224 Duration::from_secs_f32(rng.gen_range(0.5..3.0)),
1225 time,
1226 ParticleMode::CultistFlame,
1227 outer_pos,
1228 outer_pos + Vec3::unit_z() * rng.gen_range(3.0..4.0),
1229 ));
1230 }
1231 }
1232
1233 fn maintain_mine_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1234 prof_span!("ParticleMgr::maintain_mine_particles");
1235 let time = scene_data.state.get_time();
1236
1237 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(1)) {
1238 self.particles.push(Particle::new(
1240 Duration::from_millis(25),
1241 time,
1242 ParticleMode::GunPowderSpark,
1243 pos,
1244 ));
1245 }
1246 }
1247
1248 fn maintain_char_state_particles(&mut self, scene_data: &SceneData, figure_mgr: &FigureMgr) {
1249 prof_span!("ParticleMgr::maintain_char_state_particles");
1250 let state = scene_data.state;
1251 let ecs = state.ecs();
1252 let time = state.get_time();
1253 let dt = scene_data.state.get_delta_time();
1254 let mut rng = thread_rng();
1255
1256 for (
1257 entity,
1258 interpolated,
1259 vel,
1260 character_state,
1261 body,
1262 ori,
1263 character_activity,
1264 physics,
1265 inventory,
1266 ) in (
1267 &ecs.entities(),
1268 &ecs.read_storage::<Interpolated>(),
1269 ecs.read_storage::<Vel>().maybe(),
1270 &ecs.read_storage::<CharacterState>(),
1271 &ecs.read_storage::<Body>(),
1272 &ecs.read_storage::<Ori>(),
1273 &ecs.read_storage::<CharacterActivity>(),
1274 &ecs.read_storage::<PhysicsState>(),
1275 ecs.read_storage::<Inventory>().maybe(),
1276 )
1277 .join()
1278 {
1279 match character_state {
1280 CharacterState::Boost(_) => {
1281 self.particles.resize_with(
1282 self.particles.len()
1283 + usize::from(self.scheduler.heartbeats(Duration::from_millis(10))),
1284 || {
1285 Particle::new(
1286 Duration::from_millis(250),
1287 time,
1288 ParticleMode::PortalFizz,
1289 interpolated.pos
1291 - ori.to_horizontal().look_dir().to_vec()
1292 - vel.map_or(Vec3::zero(), |v| v.0 * dt * rng.gen::<f32>()),
1293 )
1294 },
1295 );
1296 },
1297 CharacterState::BasicMelee(c) => {
1298 if let Some(specifier) = c.static_data.frontend_specifier {
1299 match specifier {
1300 states::basic_melee::FrontendSpecifier::FlameTornado => {
1301 if matches!(c.stage_section, StageSection::Action) {
1302 let time = scene_data.state.get_time();
1303 let mut rng = thread_rng();
1304 self.particles.resize_with(
1305 self.particles.len()
1306 + 10
1307 + usize::from(
1308 self.scheduler.heartbeats(Duration::from_millis(5)),
1309 ),
1310 || {
1311 Particle::new(
1312 Duration::from_millis(1000),
1313 time,
1314 ParticleMode::FlameTornado,
1315 interpolated
1316 .pos
1317 .map(|e| e + rng.gen_range(-0.25..0.25)),
1318 )
1319 },
1320 );
1321 }
1322 },
1323 states::basic_melee::FrontendSpecifier::FireGigasWhirlwind => {
1324 if matches!(c.stage_section, StageSection::Action) {
1325 let time = scene_data.state.get_time();
1326 let mut rng = thread_rng();
1327 self.particles.resize_with(
1328 self.particles.len()
1329 + 3
1330 + usize::from(
1331 self.scheduler.heartbeats(Duration::from_millis(5)),
1332 ),
1333 || {
1334 Particle::new(
1335 Duration::from_millis(600),
1336 time,
1337 ParticleMode::FireGigasWhirlwind,
1338 interpolated
1339 .pos
1340 .map(|e| e + rng.gen_range(-0.25..0.25))
1341 + 3.0 * Vec3::<f32>::unit_z(),
1342 )
1343 },
1344 );
1345 }
1346 },
1347 }
1348 }
1349 },
1350 CharacterState::RapidMelee(c) => {
1351 if let Some(specifier) = c.static_data.frontend_specifier {
1352 match specifier {
1353 states::rapid_melee::FrontendSpecifier::CultistVortex => {
1354 if matches!(c.stage_section, StageSection::Action) {
1355 let range = c.static_data.melee_constructor.range;
1356 let heartbeats =
1358 self.scheduler.heartbeats(Duration::from_millis(3));
1359 self.particles.resize_with(
1360 self.particles.len()
1361 + range.powi(2) as usize * usize::from(heartbeats)
1362 / 150,
1363 || {
1364 let rand_dist =
1365 range * (1.0 - rng.gen::<f32>().powi(10));
1366 let init_pos = Vec3::new(
1367 2.0 * rng.gen::<f32>() - 1.0,
1368 2.0 * rng.gen::<f32>() - 1.0,
1369 0.0,
1370 )
1371 .normalized()
1372 * rand_dist
1373 + interpolated.pos
1374 + Vec3::unit_z() * 0.05;
1375 Particle::new_directed(
1376 Duration::from_millis(900),
1377 time,
1378 ParticleMode::CultistFlame,
1379 init_pos,
1380 interpolated.pos,
1381 )
1382 },
1383 );
1384 for (_entity_b, interpolated_b, body_b, _health_b) in (
1386 &ecs.entities(),
1387 &ecs.read_storage::<Interpolated>(),
1388 &ecs.read_storage::<Body>(),
1389 &ecs.read_storage::<comp::Health>(),
1390 )
1391 .join()
1392 .filter(|(e, _, _, h)| !h.is_dead && entity != *e)
1393 {
1394 if interpolated.pos.distance_squared(interpolated_b.pos)
1395 < range.powi(2)
1396 {
1397 let heartbeats = self
1398 .scheduler
1399 .heartbeats(Duration::from_millis(20));
1400 self.particles.resize_with(
1401 self.particles.len()
1402 + range.powi(2) as usize
1403 * usize::from(heartbeats)
1404 / 150,
1405 || {
1406 let start_pos = interpolated_b.pos
1407 + Vec3::unit_z() * body_b.height() * 0.5
1408 + Vec3::<f32>::zero()
1409 .map(|_| rng.gen_range(-1.0..1.0))
1410 .normalized()
1411 * 1.0;
1412 Particle::new_directed(
1413 Duration::from_millis(900),
1414 time,
1415 ParticleMode::CultistFlame,
1416 start_pos,
1417 interpolated.pos
1418 + Vec3::unit_z() * body.height() * 0.5,
1419 )
1420 },
1421 );
1422 }
1423 }
1424 }
1425 },
1426 states::rapid_melee::FrontendSpecifier::IceWhirlwind => {
1427 if matches!(c.stage_section, StageSection::Action) {
1428 let time = scene_data.state.get_time();
1429 let mut rng = thread_rng();
1430 self.particles.resize_with(
1431 self.particles.len()
1432 + 3
1433 + usize::from(
1434 self.scheduler.heartbeats(Duration::from_millis(5)),
1435 ),
1436 || {
1437 Particle::new(
1438 Duration::from_millis(1000),
1439 time,
1440 ParticleMode::IceWhirlwind,
1441 interpolated
1442 .pos
1443 .map(|e| e + rng.gen_range(-0.25..0.25)),
1444 )
1445 },
1446 );
1447 }
1448 },
1449 }
1450 }
1451 },
1452 CharacterState::RepeaterRanged(repeater) => {
1453 if let Some(specifier) = repeater.static_data.specifier {
1454 match specifier {
1455 states::repeater_ranged::FrontendSpecifier::FireRainPhoenix => {
1456 self.particles.resize_with(
1458 self.particles.len()
1459 + 2 * usize::from(
1460 self.scheduler.heartbeats(Duration::from_millis(25)),
1461 ),
1462 || {
1463 let rand_pos = {
1464 let theta = rng.gen::<f32>() * TAU;
1465 let radius = repeater
1466 .static_data
1467 .properties_of_aoe
1468 .map(|aoe| aoe.radius)
1469 .unwrap_or_default()
1470 * rng.gen::<f32>().sqrt();
1471 let x = radius * theta.sin();
1472 let y = radius * theta.cos();
1473 Vec2::new(x, y) + interpolated.pos.xy()
1474 };
1475 let pos1 = rand_pos.with_z(
1476 repeater
1477 .static_data
1478 .properties_of_aoe
1479 .map(|aoe| aoe.height)
1480 .unwrap_or_default()
1481 + interpolated.pos.z
1482 + 2.0 * rng.gen::<f32>(),
1483 );
1484 Particle::new_directed(
1485 Duration::from_secs_f32(3.0),
1486 time,
1487 ParticleMode::PhoenixCloud,
1488 pos1,
1489 pos1 + Vec3::new(7.09, 4.09, 18.09),
1490 )
1491 },
1492 );
1493 self.particles.resize_with(
1494 self.particles.len()
1495 + 2 * usize::from(
1496 self.scheduler.heartbeats(Duration::from_millis(25)),
1497 ),
1498 || {
1499 let rand_pos = {
1500 let theta = rng.gen::<f32>() * TAU;
1501 let radius = repeater
1502 .static_data
1503 .properties_of_aoe
1504 .map(|aoe| aoe.radius)
1505 .unwrap_or_default()
1506 * rng.gen::<f32>().sqrt();
1507 let x = radius * theta.sin();
1508 let y = radius * theta.cos();
1509 Vec2::new(x, y) + interpolated.pos.xy()
1510 };
1511 let pos1 = rand_pos.with_z(
1512 repeater
1513 .static_data
1514 .properties_of_aoe
1515 .map(|aoe| aoe.height)
1516 .unwrap_or_default()
1517 + interpolated.pos.z
1518 + 1.5 * rng.gen::<f32>(),
1519 );
1520 Particle::new_directed(
1521 Duration::from_secs_f32(2.5),
1522 time,
1523 ParticleMode::PhoenixCloud,
1524 pos1,
1525 pos1 + Vec3::new(10.025, 4.025, 17.025),
1526 )
1527 },
1528 );
1529 },
1530 }
1531 }
1532 },
1533 CharacterState::Blink(c) => {
1534 if let Some(specifier) = c.static_data.frontend_specifier {
1535 match specifier {
1536 states::blink::FrontendSpecifier::CultistFlame => {
1537 self.particles.resize_with(
1538 self.particles.len()
1539 + usize::from(
1540 self.scheduler.heartbeats(Duration::from_millis(10)),
1541 ),
1542 || {
1543 let center_pos =
1544 interpolated.pos + Vec3::unit_z() * body.height() / 2.0;
1545 let outer_pos = interpolated.pos
1546 + Vec3::new(
1547 2.0 * rng.gen::<f32>() - 1.0,
1548 2.0 * rng.gen::<f32>() - 1.0,
1549 0.0,
1550 )
1551 .normalized()
1552 * (body.max_radius() + 2.0)
1553 + Vec3::unit_z() * body.height() * rng.gen::<f32>();
1554
1555 let (start_pos, end_pos) =
1556 if matches!(c.stage_section, StageSection::Buildup) {
1557 (outer_pos, center_pos)
1558 } else {
1559 (center_pos, outer_pos)
1560 };
1561
1562 Particle::new_directed(
1563 Duration::from_secs_f32(0.5),
1564 time,
1565 ParticleMode::CultistFlame,
1566 start_pos,
1567 end_pos,
1568 )
1569 },
1570 );
1571 },
1572 states::blink::FrontendSpecifier::FlameThrower => {
1573 self.particles.resize_with(
1574 self.particles.len()
1575 + usize::from(
1576 self.scheduler.heartbeats(Duration::from_millis(10)),
1577 ),
1578 || {
1579 let center_pos =
1580 interpolated.pos + Vec3::unit_z() * body.height() / 2.0;
1581 let outer_pos = interpolated.pos
1582 + Vec3::new(
1583 2.0 * rng.gen::<f32>() - 1.0,
1584 2.0 * rng.gen::<f32>() - 1.0,
1585 0.0,
1586 )
1587 .normalized()
1588 * (body.max_radius() + 2.0)
1589 + Vec3::unit_z() * body.height() * rng.gen::<f32>();
1590
1591 let (start_pos, end_pos) =
1592 if matches!(c.stage_section, StageSection::Buildup) {
1593 (outer_pos, center_pos)
1594 } else {
1595 (center_pos, outer_pos)
1596 };
1597
1598 Particle::new_directed(
1599 Duration::from_secs_f32(0.5),
1600 time,
1601 ParticleMode::FlameThrower,
1602 start_pos,
1603 end_pos,
1604 )
1605 },
1606 );
1607 },
1608 }
1609 }
1610 },
1611 CharacterState::SelfBuff(c) => {
1612 if let Some(specifier) = c.static_data.specifier {
1613 match specifier {
1614 states::self_buff::FrontendSpecifier::FromTheAshes => {
1615 if matches!(c.stage_section, StageSection::Action) {
1616 let pos = interpolated.pos;
1617 self.particles.resize_with(
1618 self.particles.len()
1619 + 2 * usize::from(
1620 self.scheduler.heartbeats(Duration::from_millis(1)),
1621 ),
1622 || {
1623 let start_pos = pos + Vec3::unit_z() - 1.0;
1624 let end_pos = pos
1625 + Vec3::new(
1626 4.0 * rng.gen::<f32>() - 1.0,
1627 4.0 * rng.gen::<f32>() - 1.0,
1628 0.0,
1629 )
1630 .normalized()
1631 * 1.5
1632 + Vec3::unit_z()
1633 + 5.0 * rng.gen::<f32>();
1634
1635 Particle::new_directed(
1636 Duration::from_secs_f32(0.5),
1637 time,
1638 ParticleMode::FieryBurst,
1639 start_pos,
1640 end_pos,
1641 )
1642 },
1643 );
1644 self.particles.resize_with(
1645 self.particles.len()
1646 + usize::from(
1647 self.scheduler
1648 .heartbeats(Duration::from_millis(10)),
1649 ),
1650 || {
1651 Particle::new(
1652 Duration::from_millis(650),
1653 time,
1654 ParticleMode::FieryBurstVortex,
1655 pos.map(|e| e + rng.gen_range(-0.25..0.25))
1656 + Vec3::new(0.0, 0.0, 1.0),
1657 )
1658 },
1659 );
1660 self.particles.resize_with(
1661 self.particles.len()
1662 + usize::from(
1663 self.scheduler
1664 .heartbeats(Duration::from_millis(40)),
1665 ),
1666 || {
1667 Particle::new(
1668 Duration::from_millis(1000),
1669 time,
1670 ParticleMode::FieryBurstSparks,
1671 pos.map(|e| e + rng.gen_range(-0.25..0.25)),
1672 )
1673 },
1674 );
1675 self.particles.resize_with(
1676 self.particles.len()
1677 + usize::from(
1678 self.scheduler
1679 .heartbeats(Duration::from_millis(14)),
1680 ),
1681 || {
1682 let pos1 = pos.map(|e| e + rng.gen_range(-0.25..0.25));
1683 Particle::new_directed(
1684 Duration::from_millis(1000),
1685 time,
1686 ParticleMode::FieryBurstAsh,
1687 pos1,
1688 Vec3::new(
1689 4.5, 20.4, 8.58) + pos1,
1693 )
1694 },
1695 );
1696 }
1697 },
1698 }
1699 }
1700 use buff::BuffKind;
1701 if c.static_data
1702 .buffs
1703 .iter()
1704 .any(|buff_desc| matches!(buff_desc.kind, BuffKind::Frenzied))
1705 && matches!(c.stage_section, StageSection::Action)
1706 {
1707 self.particles.resize_with(
1708 self.particles.len()
1709 + usize::from(self.scheduler.heartbeats(Duration::from_millis(5))),
1710 || {
1711 let start_pos = interpolated.pos
1712 + Vec3::new(
1713 body.max_radius(),
1714 body.max_radius(),
1715 body.height() / 2.0,
1716 )
1717 .map(|d| d * rng.gen_range(-1.0..1.0));
1718 let end_pos =
1719 interpolated.pos + (start_pos - interpolated.pos) * 6.0;
1720 Particle::new_directed(
1721 Duration::from_secs(1),
1722 time,
1723 ParticleMode::Enraged,
1724 start_pos,
1725 end_pos,
1726 )
1727 },
1728 );
1729 }
1730 },
1731 CharacterState::BasicBeam(beam) => {
1732 let ori = *ori;
1733 let _look_dir = *character_activity.look_dir.unwrap_or(ori.look_dir());
1734 let dir = ori.look_dir(); let specifier = beam.static_data.specifier;
1736 if specifier == beam::FrontendSpecifier::PhoenixLaser
1737 && matches!(beam.stage_section, StageSection::Buildup)
1738 {
1739 self.particles.resize_with(
1740 self.particles.len()
1741 + 2 * usize::from(
1742 self.scheduler.heartbeats(Duration::from_millis(2)),
1743 ),
1744 || {
1745 let mut left_right_alignment =
1746 dir.cross(Vec3::new(0.0, 0.0, 1.0)).normalized();
1747 if rng.gen_bool(0.5) {
1748 left_right_alignment *= -1.0;
1749 }
1750 let start = interpolated.pos
1751 + left_right_alignment * 4.0
1752 + dir.normalized() * 6.0;
1753 let lifespan = Duration::from_secs_f32(0.5);
1754 Particle::new_directed(
1755 lifespan,
1756 time,
1757 ParticleMode::PhoenixBuildUpAim,
1758 start,
1759 interpolated.pos
1760 + dir.normalized() * 3.0
1761 + left_right_alignment * 0.4
1762 + vel
1763 .map_or(Vec3::zero(), |v| v.0 * lifespan.as_secs_f32()),
1764 )
1765 },
1766 );
1767 }
1768 },
1769 CharacterState::Glide(glide) => {
1770 if let Some(Fluid::Air {
1771 vel: air_vel,
1772 elevation: _,
1773 }) = physics.in_fluid
1774 {
1775 const MAX_AIR_VEL: f32 = 15.0;
1778 const MIN_AIR_VEL: f32 = -2.0;
1779
1780 let minmax_norm = |val, min, max| (val - min) / (max - min);
1781
1782 let wind_speed = air_vel.0.magnitude();
1783
1784 let heartbeat = 200
1786 - Lerp::lerp(
1787 50u64,
1788 150,
1789 minmax_norm(wind_speed, MIN_AIR_VEL, MAX_AIR_VEL),
1790 );
1791
1792 let new_count = self.particles.len()
1793 + usize::from(
1794 self.scheduler.heartbeats(Duration::from_millis(heartbeat)),
1795 );
1796
1797 let duration = Lerp::lerp(
1799 0u64,
1800 1000,
1801 minmax_norm(wind_speed, MIN_AIR_VEL, MAX_AIR_VEL),
1802 );
1803 let duration = Duration::from_millis(duration);
1804
1805 self.particles.resize_with(new_count, || {
1806 let start_pos = interpolated.pos
1807 + Vec3::new(
1808 body.max_radius(),
1809 body.max_radius(),
1810 body.height() / 2.0,
1811 )
1812 .map(|d| d * rng.gen_range(-10.0..10.0));
1813
1814 Particle::new_directed(
1815 duration,
1816 time,
1817 ParticleMode::Airflow,
1818 start_pos,
1819 start_pos + air_vel.0,
1820 )
1821 });
1822
1823 if let Some(states::glide::Boost::Forward(_)) = &glide.booster
1825 && let Some(figure_state) =
1826 figure_mgr.states.character_states.get(&entity)
1827 && let Some(tp0) = figure_state.primary_abs_trail_points
1828 && let Some(tp1) = figure_state.secondary_abs_trail_points
1829 {
1830 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(5)) {
1831 self.particles.push(Particle::new(
1832 Duration::from_secs(2),
1833 time,
1834 ParticleMode::EngineJet,
1835 ((tp0.0 + tp1.1) * 0.5)
1836 + Vec3::unit_z() * 0.5
1838 + Vec3::<f32>::zero().map(|_| rng.gen_range(-0.25..0.25))
1839 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
1840 ));
1841 }
1842 }
1843 }
1844 },
1845 CharacterState::Transform(data) => {
1846 if matches!(data.stage_section, StageSection::Buildup)
1847 && let Some(specifier) = data.static_data.specifier
1848 {
1849 match specifier {
1850 states::transform::FrontendSpecifier::Evolve => {
1851 self.particles.resize_with(
1852 self.particles.len()
1853 + usize::from(
1854 self.scheduler.heartbeats(Duration::from_millis(10)),
1855 ),
1856 || {
1857 let start_pos = interpolated.pos
1858 + (Vec2::unit_y()
1859 * rng.gen::<f32>()
1860 * body.max_radius())
1861 .rotated_z(rng.gen_range(0.0..(PI * 2.0)))
1862 .with_z(body.height() * rng.gen::<f32>());
1863
1864 Particle::new_directed(
1865 Duration::from_millis(100),
1866 time,
1867 ParticleMode::BarrelOrgan,
1868 start_pos,
1869 start_pos + Vec3::unit_z() * 2.0,
1870 )
1871 },
1872 )
1873 },
1874 states::transform::FrontendSpecifier::Cursekeeper => {
1875 self.particles.resize_with(
1876 self.particles.len()
1877 + usize::from(
1878 self.scheduler.heartbeats(Duration::from_millis(10)),
1879 ),
1880 || {
1881 let start_pos = interpolated.pos
1882 + (Vec2::unit_y()
1883 * rng.gen::<f32>()
1884 * body.max_radius())
1885 .rotated_z(rng.gen_range(0.0..(PI * 2.0)))
1886 .with_z(body.height() * rng.gen::<f32>());
1887
1888 Particle::new_directed(
1889 Duration::from_millis(100),
1890 time,
1891 ParticleMode::FireworkPurple,
1892 start_pos,
1893 start_pos + Vec3::unit_z() * 2.0,
1894 )
1895 },
1896 )
1897 },
1898 }
1899 }
1900 },
1901 CharacterState::ChargedMelee(_melee) => {
1902 self.maintain_hydra_tail_swipe_particles(
1903 scene_data,
1904 figure_mgr,
1905 entity,
1906 interpolated.pos,
1907 body,
1908 character_state,
1909 inventory,
1910 );
1911 },
1912 _ => {},
1913 }
1914 }
1915 }
1916
1917 fn maintain_beam_particles(&mut self, scene_data: &SceneData, lights: &mut Vec<Light>) {
1918 let state = scene_data.state;
1919 let ecs = state.ecs();
1920 let time = state.get_time();
1921 let terrain = state.terrain();
1922 let tick_elapse = u32::from(self.scheduler.heartbeats(Duration::from_millis(1)).min(100));
1925 let mut rng = thread_rng();
1926
1927 for (beam, ori) in (&ecs.read_storage::<Beam>(), &ecs.read_storage::<Ori>()).join() {
1928 let particles_per_sec = (match beam.specifier {
1929 beam::FrontendSpecifier::Flamethrower
1930 | beam::FrontendSpecifier::Bubbles
1931 | beam::FrontendSpecifier::Steam
1932 | beam::FrontendSpecifier::Frost
1933 | beam::FrontendSpecifier::Poison
1934 | beam::FrontendSpecifier::Ink
1935 | beam::FrontendSpecifier::PhoenixLaser
1936 | beam::FrontendSpecifier::Gravewarden => 300.0,
1937 beam::FrontendSpecifier::FirePillar | beam::FrontendSpecifier::FlameWallPillar => {
1938 40.0 * beam.end_radius.powi(2)
1939 },
1940 beam::FrontendSpecifier::LifestealBeam => 420.0,
1941 beam::FrontendSpecifier::Cultist => 960.0,
1942 beam::FrontendSpecifier::WebStrand => 180.0,
1943 beam::FrontendSpecifier::Lightning => 120.0,
1944 beam::FrontendSpecifier::FireGigasOverheat => 1600.0,
1945 }) / 1000.0;
1946
1947 let beam_tick_count = tick_elapse as f32 * particles_per_sec;
1948 let beam_tick_count = if rng.gen_bool(f64::from(beam_tick_count.fract())) {
1949 beam_tick_count.ceil() as u32
1950 } else {
1951 beam_tick_count.floor() as u32
1952 };
1953
1954 if beam_tick_count == 0 {
1955 continue;
1956 }
1957
1958 let distributed_time = tick_elapse as f64 / (beam_tick_count * 1000) as f64;
1959 let angle = (beam.end_radius / beam.range).atan();
1960 let beam_dir = (beam.bezier.ctrl - beam.bezier.start)
1961 .try_normalized()
1962 .unwrap_or(*ori.look_dir());
1963 let raycast_distance = |from, to| terrain.ray(from, to).until(Block::is_solid).cast().0;
1964
1965 self.particles.reserve(beam_tick_count as usize);
1966 match beam.specifier {
1967 beam::FrontendSpecifier::Flamethrower => {
1968 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
1969 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
1970 if scene_data.flashing_lights_enabled {
1972 lights.push(Light::new(
1973 beam.bezier.start,
1974 Rgb::new(1.0, 0.25, 0.05).map(|e| e * rng.gen_range(0.8..1.2)),
1975 2.0,
1976 ));
1977 }
1978
1979 for i in 0..beam_tick_count {
1980 let phi: f32 = rng.gen_range(0.0..angle);
1981 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
1982 let offset_z =
1983 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
1984 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
1985 self.particles.push(Particle::new_directed_with_collision(
1986 Duration::from_secs_f64(beam.duration.0),
1987 time + distributed_time * i as f64,
1988 ParticleMode::FlameThrower,
1989 beam.bezier.start,
1990 beam.bezier.start + random_ori * beam.range,
1991 raycast_distance,
1992 ));
1993 }
1994 },
1995 beam::FrontendSpecifier::FireGigasOverheat => {
1996 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
1997 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
1998 if scene_data.flashing_lights_enabled {
2000 lights.push(Light::new(
2001 beam.bezier.start,
2002 Rgb::new(1.0, 0.25, 0.05).map(|e| e * rng.gen_range(0.8..1.2)),
2003 2.0,
2004 ));
2005 }
2006
2007 for i in 0..beam_tick_count {
2008 let phi: f32 = rng.gen_range(0.0..angle);
2009 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
2010 let offset_z =
2011 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2012 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2013 self.particles.push(Particle::new_directed_with_collision(
2014 Duration::from_secs_f64(beam.duration.0),
2015 time + distributed_time * i as f64,
2016 ParticleMode::FireGigasOverheat,
2017 beam.bezier.start,
2018 beam.bezier.start + random_ori * beam.range,
2019 raycast_distance,
2020 ));
2021 }
2022 },
2023 beam::FrontendSpecifier::FirePillar | beam::FrontendSpecifier::FlameWallPillar => {
2024 if scene_data.flashing_lights_enabled {
2026 lights.push(Light::new(
2027 beam.bezier.start,
2028 Rgb::new(1.0, 0.25, 0.05).map(|e| e * rng.gen_range(0.8..1.2)),
2029 2.0,
2030 ));
2031 }
2032
2033 for i in 0..beam_tick_count {
2034 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
2035 let radius = beam.start_radius * (1.0 - rng.gen::<f32>().powi(8));
2036 let offset = Vec3::new(radius * theta.cos(), radius * theta.sin(), 0.0);
2037 self.particles.push(Particle::new_directed_with_collision(
2038 Duration::from_secs_f64(beam.duration.0),
2039 time + distributed_time * i as f64,
2040 ParticleMode::FirePillar,
2041 beam.bezier.start + offset,
2042 beam.bezier.start + offset + beam.range * Vec3::unit_z(),
2043 raycast_distance,
2044 ));
2045 }
2046 },
2047 beam::FrontendSpecifier::Cultist => {
2048 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2049 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2050 if scene_data.flashing_lights_enabled {
2052 lights.push(Light::new(
2053 beam.bezier.start,
2054 Rgb::new(1.0, 0.0, 1.0).map(|e| e * rng.gen_range(0.5..1.0)),
2055 2.0,
2056 ));
2057 }
2058 for i in 0..beam_tick_count {
2059 let phi: f32 = rng.gen_range(0.0..angle);
2060 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
2061 let offset_z =
2062 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2063 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2064 self.particles.push(Particle::new_directed_with_collision(
2065 Duration::from_secs_f64(beam.duration.0),
2066 time + distributed_time * i as f64,
2067 ParticleMode::CultistFlame,
2068 beam.bezier.start,
2069 beam.bezier.start + random_ori * beam.range,
2070 raycast_distance,
2071 ));
2072 }
2073 },
2074 beam::FrontendSpecifier::LifestealBeam => {
2075 if scene_data.flashing_lights_enabled {
2077 lights.push(Light::new(beam.bezier.start, Rgb::new(0.8, 1.0, 0.5), 1.0));
2078 }
2079
2080 let bezier_end = beam.bezier.start + beam_dir * beam.range;
2082 let distance = raycast_distance(beam.bezier.start, bezier_end);
2083 for i in 0..beam_tick_count {
2084 self.particles.push(Particle::new_directed_with_collision(
2085 Duration::from_secs_f64(beam.duration.0),
2086 time + distributed_time * i as f64,
2087 ParticleMode::LifestealBeam,
2088 beam.bezier.start,
2089 bezier_end,
2090 |_from, _to| distance,
2091 ));
2092 }
2093 },
2094 beam::FrontendSpecifier::Gravewarden => {
2095 for i in 0..beam_tick_count {
2096 let mut offset = 0.5;
2097 let side = Vec2::new(-beam_dir.y, beam_dir.x);
2098 self.particles.resize_with(self.particles.len() + 2, || {
2099 offset = -offset;
2100 Particle::new_directed_with_collision(
2101 Duration::from_secs_f64(beam.duration.0),
2102 time + distributed_time * i as f64,
2103 ParticleMode::Laser,
2104 beam.bezier.start + beam_dir * 1.5 + side * offset,
2105 beam.bezier.start + beam_dir * beam.range + side * offset,
2106 raycast_distance,
2107 )
2108 });
2109 }
2110 },
2111 beam::FrontendSpecifier::WebStrand => {
2112 let bezier_end = beam.bezier.start + beam_dir * beam.range;
2113 let distance = raycast_distance(beam.bezier.start, bezier_end);
2114 for i in 0..beam_tick_count {
2115 self.particles.push(Particle::new_directed_with_collision(
2116 Duration::from_secs_f64(beam.duration.0),
2117 time + distributed_time * i as f64,
2118 ParticleMode::WebStrand,
2119 beam.bezier.start,
2120 bezier_end,
2121 |_from, _to| distance,
2122 ));
2123 }
2124 },
2125 beam::FrontendSpecifier::Bubbles => {
2126 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2127 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2128 for i in 0..beam_tick_count {
2129 let phi: f32 = rng.gen_range(0.0..angle);
2130 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
2131 let offset_z =
2132 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2133 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2134 self.particles.push(Particle::new_directed_with_collision(
2135 Duration::from_secs_f64(beam.duration.0),
2136 time + distributed_time * i as f64,
2137 ParticleMode::Bubbles,
2138 beam.bezier.start,
2139 beam.bezier.start + random_ori * beam.range,
2140 raycast_distance,
2141 ));
2142 }
2143 },
2144 beam::FrontendSpecifier::Poison => {
2145 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2146 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2147 for i in 0..beam_tick_count {
2148 let phi: f32 = rng.gen_range(0.0..angle);
2149 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
2150 let offset_z =
2151 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2152 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2153 self.particles.push(Particle::new_directed_with_collision(
2154 Duration::from_secs_f64(beam.duration.0),
2155 time + distributed_time * i as f64,
2156 ParticleMode::Poison,
2157 beam.bezier.start,
2158 beam.bezier.start + random_ori * beam.range,
2159 raycast_distance,
2160 ));
2161 }
2162 },
2163 beam::FrontendSpecifier::Ink => {
2164 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2165 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2166 for i in 0..beam_tick_count {
2167 let phi: f32 = rng.gen_range(0.0..angle);
2168 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
2169 let offset_z =
2170 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2171 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2172 self.particles.push(Particle::new_directed_with_collision(
2173 Duration::from_secs_f64(beam.duration.0),
2174 time + distributed_time * i as f64,
2175 ParticleMode::Bubbles,
2176 beam.bezier.start,
2177 beam.bezier.start + random_ori * beam.range,
2178 raycast_distance,
2179 ));
2180 }
2181 },
2182 beam::FrontendSpecifier::Steam => {
2183 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2184 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2185 for i in 0..beam_tick_count {
2186 let phi: f32 = rng.gen_range(0.0..angle);
2187 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
2188 let offset_z =
2189 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2190 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2191 self.particles.push(Particle::new_directed_with_collision(
2192 Duration::from_secs_f64(beam.duration.0),
2193 time + distributed_time * i as f64,
2194 ParticleMode::Steam,
2195 beam.bezier.start,
2196 beam.bezier.start + random_ori * beam.range,
2197 raycast_distance,
2198 ));
2199 }
2200 },
2201 beam::FrontendSpecifier::Lightning => {
2202 let bezier_end = beam.bezier.start + beam_dir * beam.range;
2203 let distance = raycast_distance(beam.bezier.start, bezier_end);
2204 for i in 0..beam_tick_count {
2205 self.particles.push(Particle::new_directed_with_collision(
2206 Duration::from_secs_f64(beam.duration.0),
2207 time + distributed_time * i as f64,
2208 ParticleMode::Lightning,
2209 beam.bezier.start,
2210 bezier_end,
2211 |_from, _to| distance,
2212 ));
2213 }
2214 },
2215 beam::FrontendSpecifier::Frost => {
2216 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
2217 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
2218 for i in 0..beam_tick_count {
2219 let phi: f32 = rng.gen_range(0.0..angle);
2220 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
2221 let offset_z =
2222 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
2223 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
2224 self.particles.push(Particle::new_directed_with_collision(
2225 Duration::from_secs_f64(beam.duration.0),
2226 time + distributed_time * i as f64,
2227 ParticleMode::Ice,
2228 beam.bezier.start,
2229 beam.bezier.start + random_ori * beam.range,
2230 raycast_distance,
2231 ));
2232 }
2233 },
2234 beam::FrontendSpecifier::PhoenixLaser => {
2235 let bezier_end = beam.bezier.start + beam_dir * beam.range;
2236 let distance = raycast_distance(beam.bezier.start, bezier_end);
2237 for i in 0..beam_tick_count {
2238 self.particles.push(Particle::new_directed_with_collision(
2239 Duration::from_secs_f64(beam.duration.0),
2240 time + distributed_time * i as f64,
2241 ParticleMode::PhoenixBeam,
2242 beam.bezier.start,
2243 bezier_end,
2244 |_from, _to| distance,
2245 ));
2246 }
2247 },
2248 }
2249 }
2250 }
2251
2252 fn maintain_aura_particles(&mut self, scene_data: &SceneData) {
2253 let state = scene_data.state;
2254 let ecs = state.ecs();
2255 let time = state.get_time();
2256 let mut rng = thread_rng();
2257 let dt = scene_data.state.get_delta_time();
2258
2259 for (interp, pos, auras, body_maybe) in (
2260 ecs.read_storage::<Interpolated>().maybe(),
2261 &ecs.read_storage::<Pos>(),
2262 &ecs.read_storage::<comp::Auras>(),
2263 ecs.read_storage::<comp::Body>().maybe(),
2264 )
2265 .join()
2266 {
2267 let pos = interp.map_or(pos.0, |i| i.pos);
2268
2269 for (_, aura) in auras.auras.iter() {
2270 match aura.aura_kind {
2271 aura::AuraKind::Buff {
2272 kind: buff::BuffKind::ProtectingWard,
2273 ..
2274 } => {
2275 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
2276 self.particles.resize_with(
2277 self.particles.len()
2278 + aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
2279 || {
2280 let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100));
2281 let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
2282 let duration = Duration::from_secs_f64(
2283 aura.end_time
2284 .map_or(1.0, |end| end.0 - time)
2285 .clamp(0.0, 1.0),
2286 );
2287 Particle::new_directed(
2288 duration,
2289 time,
2290 ParticleMode::EnergyNature,
2291 pos,
2292 pos + init_pos,
2293 )
2294 },
2295 );
2296 },
2297 aura::AuraKind::Buff {
2298 kind: buff::BuffKind::Regeneration,
2299 ..
2300 } => {
2301 if auras.auras.iter().any(|(_, aura)| {
2302 matches!(aura.aura_kind, aura::AuraKind::Buff {
2303 kind: buff::BuffKind::ProtectingWard,
2304 ..
2305 })
2306 }) {
2307 continue;
2310 }
2311 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
2312 self.particles.resize_with(
2313 self.particles.len()
2314 + aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
2315 || {
2316 let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100));
2317 let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
2318 let duration = Duration::from_secs_f64(
2319 aura.end_time
2320 .map_or(1.0, |end| end.0 - time)
2321 .clamp(0.0, 1.0),
2322 );
2323 Particle::new_directed(
2324 duration,
2325 time,
2326 ParticleMode::EnergyHealing,
2327 pos,
2328 pos + init_pos,
2329 )
2330 },
2331 );
2332 },
2333 aura::AuraKind::Buff {
2334 kind: buff::BuffKind::Burning,
2335 ..
2336 } => {
2337 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
2338 self.particles.resize_with(
2339 self.particles.len()
2340 + aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
2341 || {
2342 let rand_pos = {
2343 let theta = rng.gen::<f32>() * TAU;
2344 let radius = aura.radius * rng.gen::<f32>().sqrt();
2345 let x = radius * theta.sin();
2346 let y = radius * theta.cos();
2347 Vec2::new(x, y) + pos.xy()
2348 };
2349 let duration = Duration::from_secs_f64(
2350 aura.end_time
2351 .map_or(1.0, |end| end.0 - time)
2352 .clamp(0.0, 1.0),
2353 );
2354 Particle::new_directed(
2355 duration,
2356 time,
2357 ParticleMode::FlameThrower,
2358 rand_pos.with_z(pos.z),
2359 rand_pos.with_z(pos.z + 1.0),
2360 )
2361 },
2362 );
2363 },
2364 aura::AuraKind::Buff {
2365 kind: buff::BuffKind::Hastened,
2366 ..
2367 } => {
2368 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
2369 self.particles.resize_with(
2370 self.particles.len()
2371 + aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
2372 || {
2373 let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100));
2374 let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
2375 let duration = Duration::from_secs_f64(
2376 aura.end_time
2377 .map_or(1.0, |end| end.0 - time)
2378 .clamp(0.0, 1.0),
2379 );
2380 Particle::new_directed(
2381 duration,
2382 time,
2383 ParticleMode::EnergyBuffing,
2384 pos,
2385 pos + init_pos,
2386 )
2387 },
2388 );
2389 },
2390 aura::AuraKind::Buff {
2391 kind: buff::BuffKind::Frozen,
2392 ..
2393 } => {
2394 let is_new_aura = aura.data.duration.is_none_or(|max_dur| {
2395 let rem_dur = aura.end_time.map_or(time, |e| e.0) - time;
2396 rem_dur > max_dur.0 * 0.9
2397 });
2398 if is_new_aura {
2399 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
2400 self.particles.resize_with(
2401 self.particles.len()
2402 + aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
2403 || {
2404 let rand_angle = rng.gen_range(0.0..TAU);
2405 let offset =
2406 Vec2::new(rand_angle.cos(), rand_angle.sin()) * aura.radius;
2407 let z_start = body_maybe
2408 .map_or(0.0, |b| rng.gen_range(0.5..0.75) * b.height());
2409 let z_end = body_maybe
2410 .map_or(0.0, |b| rng.gen_range(0.0..3.0) * b.height());
2411 Particle::new_directed(
2412 Duration::from_secs(3),
2413 time,
2414 ParticleMode::Ice,
2415 pos + Vec3::unit_z() * z_start,
2416 pos + offset.with_z(z_end),
2417 )
2418 },
2419 );
2420 }
2421 },
2422 aura::AuraKind::Buff {
2423 kind: buff::BuffKind::Heatstroke,
2424 ..
2425 } => {
2426 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
2427 self.particles.resize_with(
2428 self.particles.len()
2429 + aura.radius.powi(2) as usize * usize::from(heartbeats) / 900,
2430 || {
2431 let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100));
2432 let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
2433 let duration = Duration::from_secs_f64(
2434 aura.end_time
2435 .map_or(1.0, |end| end.0 - time)
2436 .clamp(0.0, 1.0),
2437 );
2438 Particle::new_directed(
2439 duration,
2440 time,
2441 ParticleMode::EnergyPhoenix,
2442 pos,
2443 pos + init_pos,
2444 )
2445 },
2446 );
2447
2448 let num_particles = aura.radius.powi(2) * dt / 50.0;
2449 let num_particles = num_particles.floor() as usize
2450 + usize::from(rng.gen_bool(f64::from(num_particles % 1.0)));
2451 self.particles
2452 .resize_with(self.particles.len() + num_particles, || {
2453 let rand_pos = {
2454 let theta = rng.gen::<f32>() * TAU;
2455 let radius = aura.radius * rng.gen::<f32>().sqrt();
2456 let x = radius * theta.sin();
2457 let y = radius * theta.cos();
2458 Vec2::new(x, y) + pos.xy()
2459 };
2460 let duration = Duration::from_secs_f64(
2461 aura.end_time
2462 .map_or(1.0, |end| end.0 - time)
2463 .clamp(0.0, 1.0),
2464 );
2465 Particle::new_directed(
2466 duration,
2467 time,
2468 ParticleMode::FieryBurstAsh,
2469 pos,
2470 Vec3::new(
2471 0.0, 20.0, 5.5) + rand_pos.with_z(pos.z),
2475 )
2476 });
2477 },
2478 _ => {},
2479 }
2480 }
2481 }
2482 }
2483
2484 fn maintain_buff_particles(&mut self, scene_data: &SceneData) {
2485 let state = scene_data.state;
2486 let ecs = state.ecs();
2487 let time = state.get_time();
2488 let mut rng = thread_rng();
2489
2490 for (interp, pos, buffs, body, ori, scale) in (
2491 ecs.read_storage::<Interpolated>().maybe(),
2492 &ecs.read_storage::<Pos>(),
2493 &ecs.read_storage::<comp::Buffs>(),
2494 &ecs.read_storage::<Body>(),
2495 &ecs.read_storage::<Ori>(),
2496 ecs.read_storage::<Scale>().maybe(),
2497 )
2498 .join()
2499 {
2500 let pos = interp.map_or(pos.0, |i| i.pos);
2501
2502 for (buff_kind, buff_keys) in buffs
2503 .kinds
2504 .iter()
2505 .filter_map(|(kind, keys)| keys.as_ref().map(|keys| (kind, keys)))
2506 {
2507 use buff::BuffKind;
2508 match buff_kind {
2509 BuffKind::Cursed | BuffKind::Burning => {
2510 self.particles.resize_with(
2511 self.particles.len()
2512 + usize::from(self.scheduler.heartbeats(Duration::from_millis(15))),
2513 || {
2514 let start_pos = pos
2515 + Vec3::unit_z() * body.height() * 0.25
2516 + Vec3::<f32>::zero()
2517 .map(|_| rng.gen_range(-1.0..1.0))
2518 .normalized()
2519 * 0.25;
2520 let end_pos = start_pos
2521 + Vec3::unit_z() * body.height()
2522 + Vec3::<f32>::zero()
2523 .map(|_| rng.gen_range(-1.0..1.0))
2524 .normalized();
2525 Particle::new_directed(
2526 Duration::from_secs(1),
2527 time,
2528 if matches!(buff_kind, BuffKind::Cursed) {
2529 ParticleMode::CultistFlame
2530 } else {
2531 ParticleMode::FlameThrower
2532 },
2533 start_pos,
2534 end_pos,
2535 )
2536 },
2537 );
2538 },
2539 BuffKind::PotionSickness => {
2540 let mut multiplicity = 0;
2541 if buff_keys.0
2544 .iter()
2545 .filter_map(|key| buffs.buffs.get(*key))
2546 .any(|buff| {
2547 matches!(buff.elapsed(Time(time)), dur if (1.0..=1.5).contains(&dur.0))
2548 })
2549 {
2550 multiplicity = 1;
2551 }
2552 self.particles.resize_with(
2553 self.particles.len()
2554 + multiplicity
2555 * usize::from(
2556 self.scheduler.heartbeats(Duration::from_millis(25)),
2557 ),
2558 || {
2559 let start_pos = pos
2560 + Vec3::unit_z() * body.eye_height(scale.map_or(1.0, |s| s.0));
2561 let (radius, theta) =
2562 (rng.gen_range(0.0f32..1.0).sqrt(), rng.gen_range(0.0..TAU));
2563 let end_pos = pos
2564 + *ori.look_dir()
2565 + Vec3::<f32>::new(
2566 radius * theta.cos(),
2567 radius * theta.sin(),
2568 0.0,
2569 ) * 0.25;
2570 Particle::new_directed(
2571 Duration::from_secs(1),
2572 time,
2573 ParticleMode::PotionSickness,
2574 start_pos,
2575 end_pos,
2576 )
2577 },
2578 );
2579 },
2580 BuffKind::Frenzied => {
2581 self.particles.resize_with(
2582 self.particles.len()
2583 + usize::from(self.scheduler.heartbeats(Duration::from_millis(15))),
2584 || {
2585 let start_pos = pos
2586 + Vec3::new(
2587 body.max_radius(),
2588 body.max_radius(),
2589 body.height() / 2.0,
2590 )
2591 .map(|d| d * rng.gen_range(-1.0..1.0));
2592 let end_pos = start_pos
2593 + Vec3::unit_z() * body.height()
2594 + Vec3::<f32>::zero()
2595 .map(|_| rng.gen_range(-1.0..1.0))
2596 .normalized();
2597 Particle::new_directed(
2598 Duration::from_secs(1),
2599 time,
2600 ParticleMode::Enraged,
2601 start_pos,
2602 end_pos,
2603 )
2604 },
2605 );
2606 },
2607 BuffKind::Polymorphed => {
2608 let mut multiplicity = 0;
2609 if buff_keys.0
2612 .iter()
2613 .filter_map(|key| buffs.buffs.get(*key))
2614 .any(|buff| {
2615 matches!(buff.elapsed(Time(time)), dur if (0.1..=0.3).contains(&dur.0))
2616 })
2617 {
2618 multiplicity = 1;
2619 }
2620 self.particles.resize_with(
2621 self.particles.len()
2622 + multiplicity
2623 * self.scheduler.heartbeats(Duration::from_millis(3)) as usize,
2624 || {
2625 let start_pos = pos
2626 + Vec3::unit_z() * body.eye_height(scale.map_or(1.0, |s| s.0))
2627 / 2.0;
2628 let end_pos = start_pos
2629 + Vec3::<f32>::zero()
2630 .map(|_| rng.gen_range(-1.0..1.0))
2631 .normalized()
2632 * 5.0;
2633
2634 Particle::new_directed(
2635 Duration::from_secs(2),
2636 time,
2637 ParticleMode::Explosion,
2638 start_pos,
2639 end_pos,
2640 )
2641 },
2642 )
2643 },
2644 _ => {},
2645 }
2646 }
2647 }
2648 }
2649
2650 fn maintain_block_particles(
2651 &mut self,
2652 scene_data: &SceneData,
2653 terrain: &Terrain<TerrainChunk>,
2654 figure_mgr: &FigureMgr,
2655 ) {
2656 prof_span!("ParticleMgr::maintain_block_particles");
2657 let dt = scene_data.state.ecs().fetch::<DeltaTime>().0;
2658 let time = scene_data.state.get_time();
2659 let player_pos = scene_data
2660 .state
2661 .read_component_copied::<Interpolated>(scene_data.viewpoint_entity)
2662 .map(|i| i.pos)
2663 .unwrap_or_default();
2664 let player_chunk = player_pos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| {
2665 (e.floor() as i32).div_euclid(sz as i32)
2666 });
2667
2668 struct BlockParticles<'a> {
2669 blocks: fn(&'a BlocksOfInterest) -> BlockParticleSlice<'a>,
2671 range: usize,
2673 rate: f32,
2675 lifetime: f32,
2677 mode: ParticleMode,
2679 cond: fn(&SceneData) -> bool,
2681 }
2682
2683 enum BlockParticleSlice<'a> {
2684 Positions(&'a [Vec3<i32>]),
2685 PositionsAndDirs(&'a [(Vec3<i32>, Vec3<f32>)]),
2686 }
2687
2688 impl BlockParticleSlice<'_> {
2689 fn len(&self) -> usize {
2690 match self {
2691 Self::Positions(blocks) => blocks.len(),
2692 Self::PositionsAndDirs(blocks) => blocks.len(),
2693 }
2694 }
2695 }
2696
2697 let particles: &[BlockParticles] = &[
2698 BlockParticles {
2699 blocks: |boi| BlockParticleSlice::Positions(&boi.leaves),
2700 range: 4,
2701 rate: 0.0125,
2702 lifetime: 30.0,
2703 mode: ParticleMode::Leaf,
2704 cond: |_| true,
2705 },
2706 BlockParticles {
2707 blocks: |boi| BlockParticleSlice::Positions(&boi.drip),
2708 range: 4,
2709 rate: 0.004,
2710 lifetime: 20.0,
2711 mode: ParticleMode::Drip,
2712 cond: |_| true,
2713 },
2714 BlockParticles {
2715 blocks: |boi| BlockParticleSlice::Positions(&boi.fires),
2716 range: 2,
2717 rate: 20.0,
2718 lifetime: 0.25,
2719 mode: ParticleMode::CampfireFire,
2720 cond: |_| true,
2721 },
2722 BlockParticles {
2723 blocks: |boi| BlockParticleSlice::Positions(&boi.fire_bowls),
2724 range: 2,
2725 rate: 20.0,
2726 lifetime: 0.25,
2727 mode: ParticleMode::FireBowl,
2728 cond: |_| true,
2729 },
2730 BlockParticles {
2731 blocks: |boi| BlockParticleSlice::Positions(&boi.fireflies),
2732 range: 6,
2733 rate: 0.004,
2734 lifetime: 40.0,
2735 mode: ParticleMode::Firefly,
2736 cond: |sd| sd.state.get_day_period().is_dark(),
2737 },
2738 BlockParticles {
2739 blocks: |boi| BlockParticleSlice::Positions(&boi.flowers),
2740 range: 5,
2741 rate: 0.002,
2742 lifetime: 40.0,
2743 mode: ParticleMode::Firefly,
2744 cond: |sd| sd.state.get_day_period().is_dark(),
2745 },
2746 BlockParticles {
2747 blocks: |boi| BlockParticleSlice::Positions(&boi.beehives),
2748 range: 3,
2749 rate: 0.5,
2750 lifetime: 30.0,
2751 mode: ParticleMode::Bee,
2752 cond: |sd| sd.state.get_day_period().is_light(),
2753 },
2754 BlockParticles {
2755 blocks: |boi| BlockParticleSlice::Positions(&boi.snow),
2756 range: 4,
2757 rate: 0.025,
2758 lifetime: 15.0,
2759 mode: ParticleMode::Snow,
2760 cond: |_| true,
2761 },
2762 BlockParticles {
2763 blocks: |boi| BlockParticleSlice::PositionsAndDirs(&boi.one_way_walls),
2764 range: 2,
2765 rate: 12.0,
2766 lifetime: 1.5,
2767 mode: ParticleMode::PortalFizz,
2768 cond: |_| true,
2769 },
2770 BlockParticles {
2771 blocks: |boi| BlockParticleSlice::Positions(&boi.spores),
2772 range: 4,
2773 rate: 0.055,
2774 lifetime: 20.0,
2775 mode: ParticleMode::Spore,
2776 cond: |_| true,
2777 },
2778 BlockParticles {
2779 blocks: |boi| BlockParticleSlice::PositionsAndDirs(&boi.waterfall),
2780 range: 2,
2781 rate: 4.0,
2782 lifetime: 5.0,
2783 mode: ParticleMode::WaterFoam,
2784 cond: |_| true,
2785 },
2786 BlockParticles {
2787 blocks: |boi| BlockParticleSlice::Positions(&boi.train_smokes),
2788 range: 2,
2789 rate: 50.0,
2790 lifetime: 8.0,
2791 mode: ParticleMode::TrainSmoke,
2792 cond: |_| true,
2793 },
2794 ];
2795
2796 let ecs = scene_data.state.ecs();
2797 let mut rng = thread_rng();
2798 let cap = 512;
2801 for particles in particles.iter() {
2802 if !(particles.cond)(scene_data) {
2803 continue;
2804 }
2805
2806 for offset in Spiral2d::new().take((particles.range * 2 + 1).pow(2)) {
2807 let chunk_pos = player_chunk + offset;
2808
2809 terrain.get(chunk_pos).map(|chunk_data| {
2810 let blocks = (particles.blocks)(&chunk_data.blocks_of_interest);
2811
2812 let avg_particles = dt * (blocks.len() as f32 * particles.rate).min(cap as f32);
2813 let particle_count = avg_particles.trunc() as usize
2814 + (rng.gen::<f32>() < avg_particles.fract()) as usize;
2815
2816 self.particles
2817 .resize_with(self.particles.len() + particle_count, || {
2818 match blocks {
2819 BlockParticleSlice::Positions(blocks) => {
2820 let block_pos = Vec3::from(
2822 chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32),
2823 ) + blocks.choose(&mut rng).copied().unwrap();
2824 Particle::new(
2825 Duration::from_secs_f32(particles.lifetime),
2826 time,
2827 particles.mode,
2828 block_pos.map(|e: i32| e as f32 + rng.gen::<f32>()),
2829 )
2830 },
2831 BlockParticleSlice::PositionsAndDirs(blocks) => {
2832 let (block_offset, particle_dir) =
2834 blocks.choose(&mut rng).copied().unwrap();
2835 let block_pos = Vec3::from(
2836 chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32),
2837 ) + block_offset;
2838 let particle_pos =
2839 block_pos.map(|e: i32| e as f32 + rng.gen::<f32>());
2840 Particle::new_directed(
2841 Duration::from_secs_f32(particles.lifetime),
2842 time,
2843 particles.mode,
2844 particle_pos,
2845 particle_pos + particle_dir,
2846 )
2847 },
2848 }
2849 })
2850 });
2851 }
2852
2853 for (entity, body, interpolated, collider) in (
2854 &ecs.entities(),
2855 &ecs.read_storage::<comp::Body>(),
2856 &ecs.read_storage::<crate::ecs::comp::Interpolated>(),
2857 ecs.read_storage::<comp::Collider>().maybe(),
2858 )
2859 .join()
2860 {
2861 if let Some((blocks_of_interest, offset)) =
2862 figure_mgr.get_blocks_of_interest(entity, body, collider)
2863 {
2864 let mat = Mat4::from(interpolated.ori.to_quat())
2865 .translated_3d(interpolated.pos)
2866 * Mat4::translation_3d(offset);
2867
2868 let blocks = (particles.blocks)(blocks_of_interest);
2869
2870 let avg_particles = dt * blocks.len() as f32 * particles.rate;
2871 let particle_count = avg_particles.trunc() as usize
2872 + (rng.gen::<f32>() < avg_particles.fract()) as usize;
2873
2874 self.particles
2875 .resize_with(self.particles.len() + particle_count, || {
2876 match blocks {
2877 BlockParticleSlice::Positions(blocks) => {
2878 let rel_pos = blocks
2879 .choose(&mut rng)
2880 .copied()
2881 .unwrap()
2883 .map(|e: i32| e as f32 + rng.gen::<f32>());
2884 let wpos = mat.mul_point(rel_pos);
2885
2886 Particle::new(
2887 Duration::from_secs_f32(particles.lifetime),
2888 time,
2889 particles.mode,
2890 wpos,
2891 )
2892 },
2893 BlockParticleSlice::PositionsAndDirs(blocks) => {
2894 let (block_offset, particle_dir) =
2896 blocks.choose(&mut rng).copied().unwrap();
2897 let particle_pos =
2898 block_offset.map(|e: i32| e as f32 + rng.gen::<f32>());
2899 let wpos = mat.mul_point(particle_pos);
2900 Particle::new_directed(
2901 Duration::from_secs_f32(particles.lifetime),
2902 time,
2903 particles.mode,
2904 wpos,
2905 wpos + mat.mul_direction(particle_dir),
2906 )
2907 },
2908 }
2909 })
2910 }
2911 }
2912 }
2913 {
2915 struct SmokeProperties {
2916 position: Vec3<i32>,
2917 strength: f32,
2918 dry_chance: f32,
2919 }
2920
2921 let range = 8_usize;
2922 let rate = 3.0 / 128.0;
2923 let lifetime = 40.0;
2924 let time_of_day = scene_data
2925 .state
2926 .get_time_of_day()
2927 .rem_euclid(24.0 * 60.0 * 60.0) as f32;
2928
2929 let smokers = Spiral2d::new()
2930 .take((range * 2 + 1).pow(2))
2931 .flat_map(|offset| {
2932 let chunk_pos = player_chunk + offset;
2933 let block_pos =
2934 Vec3::<i32>::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32));
2935 terrain.get(chunk_pos).into_iter().flat_map(move |chunk| {
2936 chunk.blocks_of_interest.smokers.iter().map(move |smoker| {
2937 (
2938 block_pos.as_::<f32>() + smoker.position.as_(),
2939 smoker.kind,
2940 chunk.blocks_of_interest.temperature,
2941 chunk.blocks_of_interest.humidity,
2942 )
2943 })
2944 })
2945 })
2946 .chain(
2947 (
2948 &ecs.entities(),
2949 &ecs.read_storage::<comp::Body>(),
2950 &ecs.read_storage::<crate::ecs::comp::Interpolated>(),
2951 ecs.read_storage::<comp::Collider>().maybe(),
2952 )
2953 .join()
2954 .flat_map(|(entity, body, interpolated, collider)| {
2955 figure_mgr
2956 .get_blocks_of_interest(entity, body, collider)
2957 .into_iter()
2958 .flat_map(|(boi, offset)| {
2959 let mat = Mat4::from(interpolated.ori.to_quat())
2960 .translated_3d(interpolated.pos)
2961 * Mat4::translation_3d(offset);
2962 boi.smokers.iter().map(move |smoker| {
2963 (
2964 mat.mul_point(smoker.position.as_::<f32>() + 0.5),
2965 smoker.kind,
2966 0.0, 0.5,
2968 )
2969 })
2970 })
2971 })
2972 .collect::<Vec<_>>(),
2973 );
2974
2975 let mut smoke_properties: Vec<SmokeProperties> = Vec::new();
2976 let mut sum = 0.0_f32;
2977 for (pos, kind, temperature, humidity) in smokers {
2978 let (strength, dry_chance) = {
2979 match kind {
2980 FireplaceType::House => {
2981 let prop = crate::scene::smoke_cycle::smoke_at_time(
2982 pos.round().as_(),
2983 temperature,
2984 time_of_day,
2985 );
2986 (
2987 prop.0,
2988 if prop.1 {
2989 0.8 - humidity
2991 } else {
2992 1.2 - humidity
2994 },
2995 )
2996 },
2997 FireplaceType::Workshop => (128.0, 1.0),
2998 }
2999 };
3000 sum += strength;
3001 smoke_properties.push(SmokeProperties {
3002 position: pos.round().as_(),
3003 strength,
3004 dry_chance,
3005 });
3006 }
3007 let avg_particles = dt * sum * rate;
3008
3009 let particle_count = avg_particles.trunc() as usize
3010 + (rng.gen::<f32>() < avg_particles.fract()) as usize;
3011 let chosen =
3012 smoke_properties
3013 .choose_multiple_weighted(&mut rng, particle_count, |smoker| smoker.strength);
3014 if let Ok(chosen) = chosen {
3015 self.particles.extend(chosen.map(|smoker| {
3016 Particle::new(
3017 Duration::from_secs_f32(lifetime),
3018 time,
3019 if rng.gen::<f32>() > smoker.dry_chance {
3020 ParticleMode::BlackSmoke
3021 } else {
3022 ParticleMode::CampfireSmoke
3023 },
3024 smoker.position.map(|e: i32| e as f32 + rng.gen::<f32>()),
3025 )
3026 }));
3027 }
3028 }
3029 }
3030
3031 fn maintain_shockwave_particles(&mut self, scene_data: &SceneData) {
3032 let state = scene_data.state;
3033 let ecs = state.ecs();
3034 let time = state.get_time();
3035 let dt = scene_data.state.ecs().fetch::<DeltaTime>().0;
3036 let terrain = scene_data.state.ecs().fetch::<TerrainGrid>();
3037
3038 for (_entity, interp, pos, ori, shockwave) in (
3039 &ecs.entities(),
3040 ecs.read_storage::<Interpolated>().maybe(),
3041 &ecs.read_storage::<Pos>(),
3042 &ecs.read_storage::<Ori>(),
3043 &ecs.read_storage::<Shockwave>(),
3044 )
3045 .join()
3046 {
3047 let pos = interp.map_or(pos.0, |i| i.pos);
3048 let ori = interp.map_or(*ori, |i| i.ori);
3049
3050 let elapsed = time - shockwave.creation.unwrap_or(time);
3051 let speed = shockwave.properties.speed;
3052
3053 let percent = elapsed as f32 / shockwave.properties.duration.as_secs_f32();
3054
3055 let distance = speed * elapsed as f32;
3056
3057 let radians = shockwave.properties.angle.to_radians();
3058
3059 let ori_vec = ori.look_vec();
3060 let theta = ori_vec.y.atan2(ori_vec.x) - radians / 2.0;
3061 let dtheta = radians / distance;
3062
3063 let arc_length = distance * radians;
3066
3067 use shockwave::FrontendSpecifier;
3068 match shockwave.properties.specifier {
3069 FrontendSpecifier::Ground => {
3070 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(2));
3071 for heartbeat in 0..heartbeats {
3072 let scale = 1.0 / 3.0;
3074
3075 let scaled_speed = speed * scale;
3076
3077 let sub_tick_interpolation = scaled_speed * 1000.0 * heartbeat as f32;
3078
3079 let distance = speed * (elapsed as f32 - sub_tick_interpolation);
3080
3081 let particle_count_factor = radians / (3.0 * scale);
3082 let new_particle_count = distance * particle_count_factor;
3083 self.particles.reserve(new_particle_count as usize);
3084
3085 for d in 0..(new_particle_count as i32) {
3086 let arc_position = theta + dtheta * d as f32 / particle_count_factor;
3087
3088 let position = pos
3089 + distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0);
3090
3091 let half_ray_length = 10.0;
3095 let mut last_air = false;
3096 let _ = terrain
3104 .ray(
3105 position + Vec3::unit_z() * half_ray_length,
3106 position - Vec3::unit_z() * half_ray_length,
3107 )
3108 .for_each(|block: &Block, pos: Vec3<i32>| {
3109 if block.is_solid() && block.get_sprite().is_none() {
3110 if last_air {
3111 let position = position.xy().with_z(pos.z as f32 + 1.0);
3112
3113 let position_snapped =
3114 ((position / scale).floor() + 0.5) * scale;
3115
3116 self.particles.push(Particle::new(
3117 Duration::from_millis(250),
3118 time,
3119 ParticleMode::GroundShockwave,
3120 position_snapped,
3121 ));
3122 last_air = false;
3123 }
3124 } else {
3125 last_air = true;
3126 }
3127 })
3128 .cast();
3129 }
3130 }
3131 },
3132 FrontendSpecifier::Fire => {
3133 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(2));
3134 for _ in 0..heartbeats {
3135 for d in 0..3 * distance as i32 {
3136 let arc_position = theta + dtheta * d as f32 / 3.0;
3137
3138 let position = pos
3139 + distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0);
3140
3141 self.particles.push(Particle::new(
3142 Duration::from_secs_f32((distance + 10.0) / 50.0),
3143 time,
3144 ParticleMode::FireShockwave,
3145 position,
3146 ));
3147 }
3148 }
3149 },
3150 FrontendSpecifier::FireLow => {
3151 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(2));
3152 for heartbeat in 0..heartbeats {
3153 let scale = 1.0 / 3.0;
3155
3156 let scaled_speed = speed * scale;
3157
3158 let sub_tick_interpolation = scaled_speed * 1000.0 * heartbeat as f32;
3159
3160 let distance = speed * (elapsed as f32 - sub_tick_interpolation);
3161
3162 let particle_count_factor = radians / (3.0 * scale);
3163 let new_particle_count = distance * particle_count_factor;
3164 self.particles.reserve(new_particle_count as usize);
3165
3166 for d in 0..(new_particle_count as i32) {
3167 let arc_position = theta + dtheta * d as f32 / particle_count_factor;
3168
3169 let position = pos
3170 + distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0);
3171
3172 let half_ray_length = 10.0;
3176 let mut last_air = false;
3177 let _ = terrain
3185 .ray(
3186 position + Vec3::unit_z() * half_ray_length,
3187 position - Vec3::unit_z() * half_ray_length,
3188 )
3189 .for_each(|block: &Block, pos: Vec3<i32>| {
3190 if block.is_solid() && block.get_sprite().is_none() {
3191 if last_air {
3192 let position = position.xy().with_z(pos.z as f32 + 1.0);
3193
3194 let position_snapped =
3195 ((position / scale).floor() + 0.5) * scale;
3196
3197 self.particles.push(Particle::new(
3198 Duration::from_millis(250),
3199 time,
3200 ParticleMode::FireLowShockwave,
3201 position_snapped,
3202 ));
3203 last_air = false;
3204 }
3205 } else {
3206 last_air = true;
3207 }
3208 })
3209 .cast();
3210 }
3211 }
3212 },
3213 FrontendSpecifier::Water => {
3214 let particles_per_length = arc_length as usize;
3216 let dtheta = radians / particles_per_length as f32;
3217 let heartbeats = self
3220 .scheduler
3221 .heartbeats(Duration::from_secs_f32(1.0 / speed));
3222
3223 let new_particle_count = particles_per_length * heartbeats as usize;
3225 self.particles.reserve(new_particle_count);
3226
3227 for i in 0..particles_per_length {
3228 let angle = dtheta * i as f32;
3229 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3230 for j in 0..heartbeats {
3231 let dt = (j as f32 / heartbeats as f32) * dt;
3233 let distance = distance + speed * dt;
3234 let pos1 = pos + distance * direction - Vec3::unit_z();
3235 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
3236 let time = time + dt as f64;
3237
3238 self.particles.push(Particle::new_directed(
3239 Duration::from_secs_f32(0.5),
3240 time,
3241 ParticleMode::Water,
3242 pos1,
3243 pos2,
3244 ));
3245 }
3246 }
3247 },
3248 FrontendSpecifier::Lightning => {
3249 let particles_per_length = arc_length as usize;
3251 let dtheta = radians / particles_per_length as f32;
3252 let heartbeats = self
3255 .scheduler
3256 .heartbeats(Duration::from_secs_f32(1.0 / speed));
3257
3258 let new_particle_count = particles_per_length * heartbeats as usize;
3260 self.particles.reserve(new_particle_count);
3261
3262 for i in 0..particles_per_length {
3263 let angle = dtheta * i as f32;
3264 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3265 for j in 0..heartbeats {
3266 let dt = (j as f32 / heartbeats as f32) * dt;
3268 let distance = distance + speed * dt;
3269 let pos1 = pos + distance * direction - Vec3::unit_z();
3270 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
3271 let time = time + dt as f64;
3272
3273 self.particles.push(Particle::new_directed(
3274 Duration::from_secs_f32(0.5),
3275 time,
3276 ParticleMode::Lightning,
3277 pos1,
3278 pos2,
3279 ));
3280 }
3281 }
3282 },
3283 FrontendSpecifier::Steam => {
3284 let particles_per_length = arc_length as usize;
3286 let dtheta = radians / particles_per_length as f32;
3287 let heartbeats = self
3290 .scheduler
3291 .heartbeats(Duration::from_secs_f32(1.0 / speed));
3292
3293 let new_particle_count = particles_per_length * heartbeats as usize;
3295 self.particles.reserve(new_particle_count);
3296
3297 for i in 0..particles_per_length {
3298 let angle = dtheta * i as f32;
3299 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3300 for j in 0..heartbeats {
3301 let dt = (j as f32 / heartbeats as f32) * dt;
3303 let distance = distance + speed * dt;
3304 let pos1 = pos + distance * direction - Vec3::unit_z();
3305 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
3306 let time = time + dt as f64;
3307
3308 self.particles.push(Particle::new_directed(
3309 Duration::from_secs_f32(0.5),
3310 time,
3311 ParticleMode::Steam,
3312 pos1,
3313 pos2,
3314 ));
3315 }
3316 }
3317 },
3318 FrontendSpecifier::Poison => {
3319 let particles_per_length = arc_length as usize;
3321 let dtheta = radians / particles_per_length as f32;
3322 let heartbeats = self
3325 .scheduler
3326 .heartbeats(Duration::from_secs_f32(1.0 / speed));
3327
3328 let new_particle_count = particles_per_length * heartbeats as usize;
3330 self.particles.reserve(new_particle_count);
3331
3332 for i in 0..particles_per_length {
3333 let angle = theta + dtheta * i as f32;
3334 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3335 for j in 0..heartbeats {
3336 let dt = (j as f32 / heartbeats as f32) * dt;
3338 let distance = distance + speed * dt;
3339 let pos1 = pos + distance * direction - Vec3::unit_z();
3340 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
3341 let time = time + dt as f64;
3342
3343 self.particles.push(Particle::new_directed(
3344 Duration::from_secs_f32(0.5),
3345 time,
3346 ParticleMode::Poison,
3347 pos1,
3348 pos2,
3349 ));
3350 }
3351 }
3352 },
3353 FrontendSpecifier::AcidCloud => {
3354 let particles_per_height = 5;
3355 let particles_per_length = arc_length as usize;
3357 let dtheta = radians / particles_per_length as f32;
3358 let heartbeats = self
3361 .scheduler
3362 .heartbeats(Duration::from_secs_f32(1.0 / speed));
3363
3364 let new_particle_count =
3366 particles_per_length * heartbeats as usize * particles_per_height;
3367 self.particles.reserve(new_particle_count);
3368
3369 for i in 0..particles_per_height {
3370 let height = (i as f32 / (particles_per_height as f32 - 1.0)) * 4.0;
3371 for j in 0..particles_per_length {
3372 let angle = theta + dtheta * j as f32;
3373 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3374 for k in 0..heartbeats {
3375 let dt = (k as f32 / heartbeats as f32) * dt;
3377 let distance = distance + speed * dt;
3378 let pos1 = pos + distance * direction - Vec3::unit_z()
3379 + Vec3::unit_z() * height;
3380 let pos2 = pos1 + direction;
3381 let time = time + dt as f64;
3382
3383 self.particles.push(Particle::new_directed(
3384 Duration::from_secs_f32(0.5),
3385 time,
3386 ParticleMode::Poison,
3387 pos1,
3388 pos2,
3389 ));
3390 }
3391 }
3392 }
3393 },
3394 FrontendSpecifier::Ink => {
3395 let particles_per_length = arc_length as usize;
3397 let dtheta = radians / particles_per_length as f32;
3398 let heartbeats = self
3401 .scheduler
3402 .heartbeats(Duration::from_secs_f32(1.0 / speed));
3403
3404 let new_particle_count = particles_per_length * heartbeats as usize;
3406 self.particles.reserve(new_particle_count);
3407
3408 for i in 0..particles_per_length {
3409 let angle = theta + dtheta * i as f32;
3410 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3411 for j in 0..heartbeats {
3412 let dt = (j as f32 / heartbeats as f32) * dt;
3414 let distance = distance + speed * dt;
3415 let pos1 = pos + distance * direction - Vec3::unit_z();
3416 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
3417 let time = time + dt as f64;
3418
3419 self.particles.push(Particle::new_directed(
3420 Duration::from_secs_f32(0.5),
3421 time,
3422 ParticleMode::Ink,
3423 pos1,
3424 pos2,
3425 ));
3426 }
3427 }
3428 },
3429 FrontendSpecifier::IceSpikes | FrontendSpecifier::Ice => {
3430 let scale = 1.0 / 3.0;
3432 let scaled_distance = distance / scale;
3433 let scaled_speed = speed / scale;
3434
3435 let particles_per_length = (0.25 * arc_length / scale) as usize;
3437 let dtheta = radians / particles_per_length as f32;
3438 let heartbeats = self
3441 .scheduler
3442 .heartbeats(Duration::from_secs_f32(3.0 / scaled_speed));
3443
3444 let new_particle_count = particles_per_length * heartbeats as usize;
3446 self.particles.reserve(new_particle_count);
3447 let wave = if matches!(shockwave.properties.dodgeable, Dodgeable::Jump) {
3449 0.5
3450 } else {
3451 8.0
3452 };
3453 let height_scale = wave + 1.5 * percent;
3455 for i in 0..particles_per_length {
3456 let angle = theta + dtheta * i as f32;
3457 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3458 for j in 0..heartbeats {
3459 let dt = (j as f32 / heartbeats as f32) * dt;
3461 let scaled_distance = scaled_distance + scaled_speed * dt;
3462 let mut pos1 = pos + (scaled_distance * direction).floor() * scale;
3463 let time = time + dt as f64;
3464
3465 let half_ray_length = 10.0;
3469 let mut last_air = false;
3470 let _ = terrain
3478 .ray(
3479 pos1 + Vec3::unit_z() * half_ray_length,
3480 pos1 - Vec3::unit_z() * half_ray_length,
3481 )
3482 .for_each(|block: &Block, pos: Vec3<i32>| {
3483 if block.is_solid() && block.get_sprite().is_none() {
3484 if last_air {
3485 pos1 = pos1.xy().with_z(pos.z as f32 + 1.0);
3486 last_air = false;
3487 }
3488 } else {
3489 last_air = true;
3490 }
3491 })
3492 .cast();
3493
3494 let get_positions = |a| {
3495 let pos1 = match a {
3496 2 => pos1 + Vec3::unit_x() * scale,
3497 3 => pos1 - Vec3::unit_x() * scale,
3498 4 => pos1 + Vec3::unit_y() * scale,
3499 5 => pos1 - Vec3::unit_y() * scale,
3500 _ => pos1,
3501 };
3502 let pos2 = if a == 1 {
3503 pos1 + Vec3::unit_z() * 5.0 * height_scale
3504 } else {
3505 pos1 + Vec3::unit_z() * 1.0 * height_scale
3506 };
3507 (pos1, pos2)
3508 };
3509
3510 for a in 1..=5 {
3511 let (pos1, pos2) = get_positions(a);
3512 self.particles.push(Particle::new_directed(
3513 Duration::from_secs_f32(0.5),
3514 time,
3515 ParticleMode::IceSpikes,
3516 pos1,
3517 pos2,
3518 ));
3519 }
3520 }
3521 }
3522 },
3523 }
3524 }
3525 }
3526
3527 fn upload_particles(&mut self, renderer: &mut Renderer) {
3528 prof_span!("ParticleMgr::upload_particles");
3529 let all_cpu_instances = self
3530 .particles
3531 .iter()
3532 .map(|p| p.instance)
3533 .collect::<Vec<ParticleInstance>>();
3534
3535 let gpu_instances = renderer.create_instances(&all_cpu_instances);
3537
3538 self.instances = gpu_instances;
3539 }
3540
3541 pub fn render<'a>(&'a self, drawer: &mut ParticleDrawer<'_, 'a>, scene_data: &SceneData) {
3542 prof_span!("ParticleMgr::render");
3543 if scene_data.particles_enabled {
3544 let model = &self
3545 .model_cache
3546 .get(DEFAULT_MODEL_KEY)
3547 .expect("Expected particle model in cache");
3548
3549 drawer.draw(model, &self.instances);
3550 }
3551 }
3552
3553 pub fn particle_count(&self) -> usize { self.instances.count() }
3554
3555 pub fn particle_count_visible(&self) -> usize { self.instances.count() }
3556}
3557
3558fn default_instances(renderer: &mut Renderer) -> Instances<ParticleInstance> {
3559 let empty_vec = Vec::new();
3560
3561 renderer.create_instances(&empty_vec)
3562}
3563
3564const DEFAULT_MODEL_KEY: &str = "voxygen.voxel.particle";
3565
3566fn default_cache(renderer: &mut Renderer) -> HashMap<&'static str, Model<ParticleVertex>> {
3567 let mut model_cache = HashMap::new();
3568
3569 model_cache.entry(DEFAULT_MODEL_KEY).or_insert_with(|| {
3570 let vox = DotVoxAsset::load_expect(DEFAULT_MODEL_KEY);
3571
3572 let max_texture_size = renderer.max_texture_size();
3575 let max_size = Vec2::from(u16::try_from(max_texture_size).unwrap_or(u16::MAX));
3576 let mut greedy = GreedyMesh::new(max_size, crate::mesh::greedy::general_config());
3577
3578 let segment = Segment::from_vox_model_index(&vox.read().0, 0);
3579 let segment_size = segment.size();
3580 let mut mesh = generate_mesh_base_vol_particle(segment, &mut greedy).0;
3581 for vert in mesh.vertices_mut() {
3583 vert.pos[0] -= segment_size.x as f32 / 2.0;
3584 vert.pos[1] -= segment_size.y as f32 / 2.0;
3585 vert.pos[2] -= segment_size.z as f32 / 2.0;
3586 }
3587
3588 drop(greedy);
3590
3591 renderer
3592 .create_model(&mesh)
3593 .expect("Failed to create particle model")
3594 });
3595
3596 model_cache
3597}
3598
3599struct HeartbeatScheduler {
3601 timers: HashMap<Duration, (f64, u8)>,
3609
3610 last_known_time: f64,
3611}
3612
3613impl HeartbeatScheduler {
3614 pub fn new() -> Self {
3615 HeartbeatScheduler {
3616 timers: HashMap::new(),
3617 last_known_time: 0.0,
3618 }
3619 }
3620
3621 pub fn maintain(&mut self, now: f64) {
3624 prof_span!("HeartbeatScheduler::maintain");
3625 self.last_known_time = now;
3626
3627 for (frequency, (last_update, heartbeats)) in self.timers.iter_mut() {
3628 let total_heartbeats = (now - *last_update) / frequency.as_secs_f64();
3630
3631 let full_heartbeats = total_heartbeats.floor();
3633
3634 *heartbeats = full_heartbeats as u8;
3635
3636 let partial_heartbeat = total_heartbeats - full_heartbeats;
3638
3639 let partial_heartbeat_as_time = frequency.mul_f64(partial_heartbeat).as_secs_f64();
3641
3642 *last_update = now - partial_heartbeat_as_time;
3646 }
3647 }
3648
3649 pub fn heartbeats(&mut self, frequency: Duration) -> u8 {
3656 prof_span!("HeartbeatScheduler::heartbeats");
3657 let last_known_time = self.last_known_time;
3658
3659 self.timers
3660 .entry(frequency)
3661 .or_insert_with(|| (last_known_time, 0))
3662 .1
3663 }
3664
3665 pub fn clear(&mut self) { self.timers.clear() }
3666}
3667
3668#[derive(Clone, Copy)]
3669struct Particle {
3670 alive_until: f64, instance: ParticleInstance,
3672}
3673
3674impl Particle {
3675 fn new(lifespan: Duration, time: f64, mode: ParticleMode, pos: Vec3<f32>) -> Self {
3676 Particle {
3677 alive_until: time + lifespan.as_secs_f64(),
3678 instance: ParticleInstance::new(time, lifespan.as_secs_f32(), mode, pos),
3679 }
3680 }
3681
3682 fn new_directed(
3683 lifespan: Duration,
3684 time: f64,
3685 mode: ParticleMode,
3686 pos1: Vec3<f32>,
3687 pos2: Vec3<f32>,
3688 ) -> Self {
3689 Particle {
3690 alive_until: time + lifespan.as_secs_f64(),
3691 instance: ParticleInstance::new_directed(
3692 time,
3693 lifespan.as_secs_f32(),
3694 mode,
3695 pos1,
3696 pos2,
3697 ),
3698 }
3699 }
3700
3701 fn new_directed_with_collision(
3702 lifespan: Duration,
3703 time: f64,
3704 mode: ParticleMode,
3705 pos1: Vec3<f32>,
3706 pos2: Vec3<f32>,
3707 distance: impl Fn(Vec3<f32>, Vec3<f32>) -> f32,
3708 ) -> Self {
3709 let dir = pos2 - pos1;
3710 let end_distance = pos1.distance(pos2);
3711 let (end_pos, lifespawn) = if end_distance > 0.1 {
3712 let ratio = distance(pos1, pos2) / end_distance;
3713 (pos1 + ratio * dir, lifespan.mul_f32(ratio))
3714 } else {
3715 (pos2, lifespan)
3716 };
3717
3718 Self::new_directed(lifespawn, time, mode, pos1, end_pos)
3719 }
3720}