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