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::Transformation { pos } => {
554 self.particles.resize_with(self.particles.len() + 100, || {
555 Particle::new(
556 Duration::from_millis(1400),
557 time,
558 ParticleMode::Transformation,
559 *pos,
560 )
561 });
562 },
563 Outcome::ProjectileShot { .. }
564 | Outcome::Beam { .. }
565 | Outcome::ExpChange { .. }
566 | Outcome::SkillPointGain { .. }
567 | Outcome::ComboChange { .. }
568 | Outcome::HealthChange { .. }
569 | Outcome::PoiseChange { .. }
570 | Outcome::Utterance { .. }
571 | Outcome::IceSpikes { .. }
572 | Outcome::IceCrack { .. }
573 | Outcome::Glider { .. }
574 | Outcome::Whoosh { .. }
575 | Outcome::Swoosh { .. }
576 | Outcome::Slash { .. }
577 | Outcome::Bleep { .. }
578 | Outcome::Charge { .. }
579 | Outcome::Steam { .. }
580 | Outcome::FireShockwave { .. }
581 | Outcome::PortalActivated { .. }
582 | Outcome::FromTheAshes { .. }
583 | Outcome::LaserBeam { .. } => {},
584 }
585 }
586
587 pub fn maintain(
588 &mut self,
589 renderer: &mut Renderer,
590 scene_data: &SceneData,
591 terrain: &Terrain<TerrainChunk>,
592 figure_mgr: &FigureMgr,
593 lights: &mut Vec<Light>,
594 ) {
595 prof_span!("ParticleMgr::maintain");
596 if scene_data.particles_enabled {
597 self.scheduler.maintain(scene_data.state.get_time());
599
600 self.particles
602 .retain(|p| p.alive_until > scene_data.state.get_time());
603
604 self.maintain_body_particles(scene_data);
606 self.maintain_char_state_particles(scene_data, figure_mgr);
607 self.maintain_beam_particles(scene_data, lights);
608 self.maintain_block_particles(scene_data, terrain, figure_mgr);
609 self.maintain_shockwave_particles(scene_data);
610 self.maintain_aura_particles(scene_data);
611 self.maintain_buff_particles(scene_data);
612
613 self.upload_particles(renderer);
614 } else {
615 if !self.particles.is_empty() {
617 self.particles.clear();
618 self.upload_particles(renderer);
619 }
620
621 self.scheduler.clear();
623 }
624 }
625
626 fn maintain_body_particles(&mut self, scene_data: &SceneData) {
627 prof_span!("ParticleMgr::maintain_body_particles");
628 let ecs = scene_data.state.ecs();
629 for (body, interpolated, vel) in (
630 &ecs.read_storage::<Body>(),
631 &ecs.read_storage::<Interpolated>(),
632 ecs.read_storage::<Vel>().maybe(),
633 )
634 .join()
635 {
636 match body {
637 Body::Object(object::Body::CampfireLit) => {
638 self.maintain_campfirelit_particles(scene_data, interpolated.pos, vel)
639 },
640 Body::Object(object::Body::BarrelOrgan) => {
641 self.maintain_barrel_organ_particles(scene_data, interpolated.pos, vel)
642 },
643 Body::Object(object::Body::BoltFire) => {
644 self.maintain_boltfire_particles(scene_data, interpolated.pos, vel)
645 },
646 Body::Object(object::Body::BoltFireBig) => {
647 self.maintain_boltfirebig_particles(scene_data, interpolated.pos, vel)
648 },
649 Body::Object(object::Body::FireRainDrop) => {
650 self.maintain_fireraindrop_particles(scene_data, interpolated.pos, vel)
651 },
652 Body::Object(object::Body::BoltNature) => {
653 self.maintain_boltnature_particles(scene_data, interpolated.pos, vel)
654 },
655 Body::Object(object::Body::Tornado) => {
656 self.maintain_tornado_particles(scene_data, interpolated.pos)
657 },
658 Body::Object(object::Body::FieryTornado) => {
659 self.maintain_fiery_tornado_particles(scene_data, interpolated.pos)
660 },
661 Body::Object(object::Body::Mine) => {
662 self.maintain_mine_particles(scene_data, interpolated.pos)
663 },
664 Body::Object(
665 object::Body::Bomb
666 | object::Body::FireworkBlue
667 | object::Body::FireworkGreen
668 | object::Body::FireworkPurple
669 | object::Body::FireworkRed
670 | object::Body::FireworkWhite
671 | object::Body::FireworkYellow
672 | object::Body::IronPikeBomb,
673 ) => self.maintain_bomb_particles(scene_data, interpolated.pos, vel),
674 Body::Object(object::Body::PortalActive) => {
675 self.maintain_active_portal_particles(scene_data, interpolated.pos)
676 },
677 Body::Object(object::Body::Portal) => {
678 self.maintain_portal_particles(scene_data, interpolated.pos)
679 },
680 _ => {},
681 }
682 }
683 }
684
685 fn maintain_hydra_tail_swipe_particles(
686 &mut self,
687 scene_data: &SceneData,
688 figure_mgr: &FigureMgr,
689 entity: Entity,
690 pos: Vec3<f32>,
691 state: &CharacterState,
692 inventory: Option<&Inventory>,
693 ) {
694 let Some(ability_id) = state
695 .ability_info()
696 .and_then(|info| info.ability.map(|a| a.ability_id(Some(state), inventory)))
697 else {
698 return;
699 };
700
701 if ability_id != Some("common.abilities.custom.hydra.tail_swipe") {
702 return;
703 }
704
705 let Some(stage_section) = state.stage_section() else {
706 return;
707 };
708
709 let particle_count = match stage_section {
710 StageSection::Charge => 1,
711 StageSection::Action => 10,
712 _ => return,
713 };
714
715 let Some((start, end)) = figure_mgr.get_tail(scene_data, entity) else {
716 return;
717 };
718
719 let start = pos + start;
720 let end = pos + end;
721
722 let time = scene_data.state.get_time();
723 let mut rng = thread_rng();
724
725 self.particles.resize_with(
726 self.particles.len()
727 + particle_count * self.scheduler.heartbeats(Duration::from_millis(33)) as usize,
728 || {
729 let t = rng.gen_range(0.0..1.0);
730 let p = start * t + end * (1.0 - t) - Vec3::new(0.0, 0.0, 0.5);
731
732 Particle::new(
733 Duration::from_millis(500),
734 time,
735 ParticleMode::GroundShockwave,
736 p,
737 )
738 },
739 );
740 }
741
742 fn maintain_campfirelit_particles(
743 &mut self,
744 scene_data: &SceneData,
745 pos: Vec3<f32>,
746 vel: Option<&Vel>,
747 ) {
748 prof_span!("ParticleMgr::maintain_campfirelit_particles");
749 let time = scene_data.state.get_time();
750 let dt = scene_data.state.get_delta_time();
751 let mut rng = thread_rng();
752
753 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(50)) {
754 self.particles.push(Particle::new(
755 Duration::from_millis(250),
756 time,
757 ParticleMode::CampfireFire,
758 pos,
759 ));
760
761 self.particles.push(Particle::new(
762 Duration::from_secs(10),
763 time,
764 ParticleMode::CampfireSmoke,
765 pos.map(|e| e + thread_rng().gen_range(-0.25..0.25))
766 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
767 ));
768 }
769 }
770
771 fn maintain_barrel_organ_particles(
772 &mut self,
773 scene_data: &SceneData,
774 pos: Vec3<f32>,
775 vel: Option<&Vel>,
776 ) {
777 prof_span!("ParticleMgr::maintain_barrel_organ_particles");
778 let time = scene_data.state.get_time();
779 let dt = scene_data.state.get_delta_time();
780 let mut rng = thread_rng();
781
782 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(20)) {
783 self.particles.push(Particle::new(
784 Duration::from_millis(250),
785 time,
786 ParticleMode::BarrelOrgan,
787 pos,
788 ));
789
790 self.particles.push(Particle::new(
791 Duration::from_secs(10),
792 time,
793 ParticleMode::BarrelOrgan,
794 pos.map(|e| e + thread_rng().gen_range(-0.25..0.25))
795 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
796 ));
797 }
798 }
799
800 fn maintain_boltfire_particles(
801 &mut self,
802 scene_data: &SceneData,
803 pos: Vec3<f32>,
804 vel: Option<&Vel>,
805 ) {
806 prof_span!("ParticleMgr::maintain_boltfire_particles");
807 let time = scene_data.state.get_time();
808 let dt = scene_data.state.get_delta_time();
809 let mut rng = thread_rng();
810
811 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(4)) {
812 self.particles.push(Particle::new(
813 Duration::from_millis(500),
814 time,
815 ParticleMode::CampfireFire,
816 pos,
817 ));
818 self.particles.push(Particle::new(
819 Duration::from_secs(1),
820 time,
821 ParticleMode::CampfireSmoke,
822 pos.map(|e| e + rng.gen_range(-0.25..0.25))
823 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
824 ));
825 }
826 }
827
828 fn maintain_boltfirebig_particles(
829 &mut self,
830 scene_data: &SceneData,
831 pos: Vec3<f32>,
832 vel: Option<&Vel>,
833 ) {
834 prof_span!("ParticleMgr::maintain_boltfirebig_particles");
835 let time = scene_data.state.get_time();
836 let dt = scene_data.state.get_delta_time();
837 let mut rng = thread_rng();
838
839 self.particles.resize_with(
841 self.particles.len() + usize::from(self.scheduler.heartbeats(Duration::from_millis(2))),
842 || {
843 Particle::new(
844 Duration::from_millis(500),
845 time,
846 ParticleMode::CampfireFire,
847 pos.map(|e| e + rng.gen_range(-0.25..0.25))
848 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
849 )
850 },
851 );
852
853 self.particles.resize_with(
855 self.particles.len() + usize::from(self.scheduler.heartbeats(Duration::from_millis(5))),
856 || {
857 Particle::new(
858 Duration::from_secs(2),
859 time,
860 ParticleMode::CampfireSmoke,
861 pos.map(|e| e + rng.gen_range(-0.25..0.25))
862 + vel.map_or(Vec3::zero(), |v| -v.0 * dt),
863 )
864 },
865 );
866 }
867
868 fn maintain_fireraindrop_particles(
869 &mut self,
870 scene_data: &SceneData,
871 pos: Vec3<f32>,
872 vel: Option<&Vel>,
873 ) {
874 prof_span!("ParticleMgr::maintain_fireraindrop_particles");
875 let time = scene_data.state.get_time();
876 let dt = scene_data.state.get_delta_time();
877 let mut rng = thread_rng();
878
879 self.particles.resize_with(
881 self.particles.len()
882 + usize::from(self.scheduler.heartbeats(Duration::from_millis(100))),
883 || {
884 Particle::new(
885 Duration::from_millis(300),
886 time,
887 ParticleMode::FieryDropletTrace,
888 pos.map(|e| e + rng.gen_range(-0.25..0.25))
889 + Vec3::new(0.0, 0.0, 0.5)
890 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
891 )
892 },
893 );
894 }
895
896 fn maintain_boltnature_particles(
897 &mut self,
898 scene_data: &SceneData,
899 pos: Vec3<f32>,
900 vel: Option<&Vel>,
901 ) {
902 let time = scene_data.state.get_time();
903 let dt = scene_data.state.get_delta_time();
904 let mut rng = thread_rng();
905
906 self.particles.resize_with(
908 self.particles.len() + usize::from(self.scheduler.heartbeats(Duration::from_millis(2))),
909 || {
910 Particle::new(
911 Duration::from_millis(500),
912 time,
913 ParticleMode::CampfireSmoke,
914 pos.map(|e| e + rng.gen_range(-0.25..0.25))
915 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
916 )
917 },
918 );
919 }
920
921 fn maintain_tornado_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
922 let time = scene_data.state.get_time();
923 let mut rng = thread_rng();
924
925 self.particles.resize_with(
927 self.particles.len() + usize::from(self.scheduler.heartbeats(Duration::from_millis(5))),
928 || {
929 Particle::new(
930 Duration::from_millis(1000),
931 time,
932 ParticleMode::Tornado,
933 pos.map(|e| e + rng.gen_range(-0.25..0.25)),
934 )
935 },
936 );
937 }
938
939 fn maintain_fiery_tornado_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
940 let time = scene_data.state.get_time();
941 let mut rng = thread_rng();
942
943 self.particles.resize_with(
945 self.particles.len() + usize::from(self.scheduler.heartbeats(Duration::from_millis(5))),
946 || {
947 Particle::new(
948 Duration::from_millis(1000),
949 time,
950 ParticleMode::FieryTornado,
951 pos.map(|e| e + rng.gen_range(-0.25..0.25)),
952 )
953 },
954 );
955 }
956
957 fn maintain_bomb_particles(
958 &mut self,
959 scene_data: &SceneData,
960 pos: Vec3<f32>,
961 vel: Option<&Vel>,
962 ) {
963 prof_span!("ParticleMgr::maintain_bomb_particles");
964 let time = scene_data.state.get_time();
965 let dt = scene_data.state.get_delta_time();
966 let mut rng = thread_rng();
967
968 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(10)) {
969 self.particles.push(Particle::new(
971 Duration::from_millis(1500),
972 time,
973 ParticleMode::GunPowderSpark,
974 pos,
975 ));
976
977 self.particles.push(Particle::new(
979 Duration::from_secs(2),
980 time,
981 ParticleMode::CampfireSmoke,
982 pos + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
983 ));
984 }
985 }
986
987 fn maintain_active_portal_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
988 prof_span!("ParticleMgr::maintain_active_portal_particles");
989
990 let time = scene_data.state.get_time();
991 let mut rng = thread_rng();
992
993 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(5)) {
994 let outer_pos =
995 pos + (Vec2::unit_x().rotated_z(rng.gen_range((0.)..PI * 2.)) * 2.7).with_z(0.);
996
997 self.particles.push(Particle::new_directed(
998 Duration::from_secs_f32(rng.gen_range(0.4..0.8)),
999 time,
1000 ParticleMode::CultistFlame,
1001 outer_pos,
1002 outer_pos + Vec3::unit_z() * rng.gen_range(5.0..7.0),
1003 ));
1004 }
1005 }
1006
1007 fn maintain_portal_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1008 prof_span!("ParticleMgr::maintain_portal_particles");
1009
1010 let time = scene_data.state.get_time();
1011 let mut rng = thread_rng();
1012
1013 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(150)) {
1014 let outer_pos = pos
1015 + (Vec2::unit_x().rotated_z(rng.gen_range((0.)..PI * 2.))
1016 * rng.gen_range(1.0..2.9))
1017 .with_z(0.);
1018
1019 self.particles.push(Particle::new_directed(
1020 Duration::from_secs_f32(rng.gen_range(0.5..3.0)),
1021 time,
1022 ParticleMode::CultistFlame,
1023 outer_pos,
1024 outer_pos + Vec3::unit_z() * rng.gen_range(3.0..4.0),
1025 ));
1026 }
1027 }
1028
1029 fn maintain_mine_particles(&mut self, scene_data: &SceneData, pos: Vec3<f32>) {
1030 prof_span!("ParticleMgr::maintain_mine_particles");
1031 let time = scene_data.state.get_time();
1032
1033 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(1)) {
1034 self.particles.push(Particle::new(
1036 Duration::from_millis(25),
1037 time,
1038 ParticleMode::GunPowderSpark,
1039 pos,
1040 ));
1041 }
1042 }
1043
1044 fn maintain_char_state_particles(&mut self, scene_data: &SceneData, figure_mgr: &FigureMgr) {
1045 prof_span!("ParticleMgr::maintain_char_state_particles");
1046 let state = scene_data.state;
1047 let ecs = state.ecs();
1048 let time = state.get_time();
1049 let dt = scene_data.state.get_delta_time();
1050 let mut rng = thread_rng();
1051
1052 for (
1053 entity,
1054 interpolated,
1055 vel,
1056 character_state,
1057 body,
1058 ori,
1059 character_activity,
1060 physics,
1061 inventory,
1062 ) in (
1063 &ecs.entities(),
1064 &ecs.read_storage::<Interpolated>(),
1065 ecs.read_storage::<Vel>().maybe(),
1066 &ecs.read_storage::<CharacterState>(),
1067 &ecs.read_storage::<Body>(),
1068 &ecs.read_storage::<Ori>(),
1069 &ecs.read_storage::<CharacterActivity>(),
1070 &ecs.read_storage::<PhysicsState>(),
1071 ecs.read_storage::<Inventory>().maybe(),
1072 )
1073 .join()
1074 {
1075 match character_state {
1076 CharacterState::Boost(_) => {
1077 self.particles.resize_with(
1078 self.particles.len()
1079 + usize::from(self.scheduler.heartbeats(Duration::from_millis(10))),
1080 || {
1081 Particle::new(
1082 Duration::from_millis(250),
1083 time,
1084 ParticleMode::PortalFizz,
1085 interpolated.pos
1087 - ori.to_horizontal().look_dir().to_vec()
1088 - vel.map_or(Vec3::zero(), |v| v.0 * dt * rng.gen::<f32>()),
1089 )
1090 },
1091 );
1092 },
1093 CharacterState::BasicMelee(c) => {
1094 if let Some(specifier) = c.static_data.frontend_specifier {
1095 match specifier {
1096 states::basic_melee::FrontendSpecifier::FlameTornado => {
1097 if matches!(c.stage_section, StageSection::Action) {
1098 let time = scene_data.state.get_time();
1099 let mut rng = thread_rng();
1100 self.particles.resize_with(
1101 self.particles.len()
1102 + 10
1103 + usize::from(
1104 self.scheduler.heartbeats(Duration::from_millis(5)),
1105 ),
1106 || {
1107 Particle::new(
1108 Duration::from_millis(1000),
1109 time,
1110 ParticleMode::FlameTornado,
1111 interpolated
1112 .pos
1113 .map(|e| e + rng.gen_range(-0.25..0.25)),
1114 )
1115 },
1116 );
1117 }
1118 },
1119 }
1120 }
1121 },
1122 CharacterState::RapidMelee(c) => {
1123 if let Some(specifier) = c.static_data.frontend_specifier {
1124 match specifier {
1125 states::rapid_melee::FrontendSpecifier::CultistVortex => {
1126 if matches!(c.stage_section, StageSection::Action) {
1127 let range = c.static_data.melee_constructor.range;
1128 let heartbeats =
1130 self.scheduler.heartbeats(Duration::from_millis(3));
1131 self.particles.resize_with(
1132 self.particles.len()
1133 + range.powi(2) as usize * usize::from(heartbeats)
1134 / 150,
1135 || {
1136 let rand_dist =
1137 range * (1.0 - rng.gen::<f32>().powi(10));
1138 let init_pos = Vec3::new(
1139 2.0 * rng.gen::<f32>() - 1.0,
1140 2.0 * rng.gen::<f32>() - 1.0,
1141 0.0,
1142 )
1143 .normalized()
1144 * rand_dist
1145 + interpolated.pos
1146 + Vec3::unit_z() * 0.05;
1147 Particle::new_directed(
1148 Duration::from_millis(900),
1149 time,
1150 ParticleMode::CultistFlame,
1151 init_pos,
1152 interpolated.pos,
1153 )
1154 },
1155 );
1156 for (_entity_b, interpolated_b, body_b, _health_b) in (
1158 &ecs.entities(),
1159 &ecs.read_storage::<Interpolated>(),
1160 &ecs.read_storage::<Body>(),
1161 &ecs.read_storage::<comp::Health>(),
1162 )
1163 .join()
1164 .filter(|(e, _, _, h)| !h.is_dead && entity != *e)
1165 {
1166 if interpolated.pos.distance_squared(interpolated_b.pos)
1167 < range.powi(2)
1168 {
1169 let heartbeats = self
1170 .scheduler
1171 .heartbeats(Duration::from_millis(20));
1172 self.particles.resize_with(
1173 self.particles.len()
1174 + range.powi(2) as usize
1175 * usize::from(heartbeats)
1176 / 150,
1177 || {
1178 let start_pos = interpolated_b.pos
1179 + Vec3::unit_z() * body_b.height() * 0.5
1180 + Vec3::<f32>::zero()
1181 .map(|_| rng.gen_range(-1.0..1.0))
1182 .normalized()
1183 * 1.0;
1184 Particle::new_directed(
1185 Duration::from_millis(900),
1186 time,
1187 ParticleMode::CultistFlame,
1188 start_pos,
1189 interpolated.pos
1190 + Vec3::unit_z() * body.height() * 0.5,
1191 )
1192 },
1193 );
1194 }
1195 }
1196 }
1197 },
1198 states::rapid_melee::FrontendSpecifier::Whirlwind => {
1199 if matches!(c.stage_section, StageSection::Action) {
1200 let time = scene_data.state.get_time();
1201 let mut rng = thread_rng();
1202 self.particles.resize_with(
1203 self.particles.len()
1204 + 3
1205 + usize::from(
1206 self.scheduler.heartbeats(Duration::from_millis(5)),
1207 ),
1208 || {
1209 Particle::new(
1210 Duration::from_millis(1000),
1211 time,
1212 ParticleMode::Whirlwind,
1213 interpolated
1214 .pos
1215 .map(|e| e + rng.gen_range(-0.25..0.25)),
1216 )
1217 },
1218 );
1219 }
1220 },
1221 }
1222 }
1223 },
1224 CharacterState::RepeaterRanged(repeater) => {
1225 if let Some(specifier) = repeater.static_data.specifier {
1226 match specifier {
1227 states::repeater_ranged::FrontendSpecifier::FireRain => {
1228 self.particles.resize_with(
1230 self.particles.len()
1231 + 2 * usize::from(
1232 self.scheduler.heartbeats(Duration::from_millis(25)),
1233 ),
1234 || {
1235 let rand_pos = {
1236 let theta = rng.gen::<f32>() * TAU;
1237 let radius = repeater
1238 .static_data
1239 .properties_of_aoe
1240 .map(|aoe| aoe.radius)
1241 .unwrap_or_default()
1242 * rng.gen::<f32>().sqrt();
1243 let x = radius * theta.sin();
1244 let y = radius * theta.cos();
1245 Vec2::new(x, y) + interpolated.pos.xy()
1246 };
1247 let pos1 = rand_pos.with_z(
1248 repeater
1249 .static_data
1250 .properties_of_aoe
1251 .map(|aoe| aoe.height)
1252 .unwrap_or_default()
1253 + interpolated.pos.z
1254 + 2.0 * rng.gen::<f32>(),
1255 );
1256 Particle::new_directed(
1257 Duration::from_secs_f32(3.0),
1258 time,
1259 ParticleMode::PhoenixCloud,
1260 pos1,
1261 pos1 + Vec3::new(7.09, 4.09, 18.09),
1262 )
1263 },
1264 );
1265 self.particles.resize_with(
1266 self.particles.len()
1267 + 2 * usize::from(
1268 self.scheduler.heartbeats(Duration::from_millis(25)),
1269 ),
1270 || {
1271 let rand_pos = {
1272 let theta = rng.gen::<f32>() * TAU;
1273 let radius = repeater
1274 .static_data
1275 .properties_of_aoe
1276 .map(|aoe| aoe.radius)
1277 .unwrap_or_default()
1278 * rng.gen::<f32>().sqrt();
1279 let x = radius * theta.sin();
1280 let y = radius * theta.cos();
1281 Vec2::new(x, y) + interpolated.pos.xy()
1282 };
1283 let pos1 = rand_pos.with_z(
1284 repeater
1285 .static_data
1286 .properties_of_aoe
1287 .map(|aoe| aoe.height)
1288 .unwrap_or_default()
1289 + interpolated.pos.z
1290 + 1.5 * rng.gen::<f32>(),
1291 );
1292 Particle::new_directed(
1293 Duration::from_secs_f32(2.5),
1294 time,
1295 ParticleMode::PhoenixCloud,
1296 pos1,
1297 pos1 + Vec3::new(10.025, 4.025, 17.025),
1298 )
1299 },
1300 );
1301 },
1302 }
1303 }
1304 },
1305 CharacterState::Blink(c) => {
1306 if let Some(specifier) = c.static_data.frontend_specifier {
1307 match specifier {
1308 states::blink::FrontendSpecifier::CultistFlame => {
1309 self.particles.resize_with(
1310 self.particles.len()
1311 + usize::from(
1312 self.scheduler.heartbeats(Duration::from_millis(10)),
1313 ),
1314 || {
1315 let center_pos =
1316 interpolated.pos + Vec3::unit_z() * body.height() / 2.0;
1317 let outer_pos = interpolated.pos
1318 + Vec3::new(
1319 2.0 * rng.gen::<f32>() - 1.0,
1320 2.0 * rng.gen::<f32>() - 1.0,
1321 0.0,
1322 )
1323 .normalized()
1324 * (body.max_radius() + 2.0)
1325 + Vec3::unit_z() * body.height() * rng.gen::<f32>();
1326
1327 let (start_pos, end_pos) =
1328 if matches!(c.stage_section, StageSection::Buildup) {
1329 (outer_pos, center_pos)
1330 } else {
1331 (center_pos, outer_pos)
1332 };
1333
1334 Particle::new_directed(
1335 Duration::from_secs_f32(0.5),
1336 time,
1337 ParticleMode::CultistFlame,
1338 start_pos,
1339 end_pos,
1340 )
1341 },
1342 );
1343 },
1344 states::blink::FrontendSpecifier::FlameThrower => {
1345 self.particles.resize_with(
1346 self.particles.len()
1347 + usize::from(
1348 self.scheduler.heartbeats(Duration::from_millis(10)),
1349 ),
1350 || {
1351 let center_pos =
1352 interpolated.pos + Vec3::unit_z() * body.height() / 2.0;
1353 let outer_pos = interpolated.pos
1354 + Vec3::new(
1355 2.0 * rng.gen::<f32>() - 1.0,
1356 2.0 * rng.gen::<f32>() - 1.0,
1357 0.0,
1358 )
1359 .normalized()
1360 * (body.max_radius() + 2.0)
1361 + Vec3::unit_z() * body.height() * rng.gen::<f32>();
1362
1363 let (start_pos, end_pos) =
1364 if matches!(c.stage_section, StageSection::Buildup) {
1365 (outer_pos, center_pos)
1366 } else {
1367 (center_pos, outer_pos)
1368 };
1369
1370 Particle::new_directed(
1371 Duration::from_secs_f32(0.5),
1372 time,
1373 ParticleMode::FlameThrower,
1374 start_pos,
1375 end_pos,
1376 )
1377 },
1378 );
1379 },
1380 }
1381 }
1382 },
1383 CharacterState::SelfBuff(c) => {
1384 if let Some(specifier) = c.static_data.specifier {
1385 match specifier {
1386 states::self_buff::FrontendSpecifier::FromTheAshes => {
1387 if matches!(c.stage_section, StageSection::Action) {
1388 let pos = interpolated.pos;
1389 self.particles.resize_with(
1390 self.particles.len()
1391 + 2 * usize::from(
1392 self.scheduler.heartbeats(Duration::from_millis(1)),
1393 ),
1394 || {
1395 let start_pos = pos + Vec3::unit_z() - 1.0;
1396 let end_pos = pos
1397 + Vec3::new(
1398 4.0 * rng.gen::<f32>() - 1.0,
1399 4.0 * rng.gen::<f32>() - 1.0,
1400 0.0,
1401 )
1402 .normalized()
1403 * 1.5
1404 + Vec3::unit_z()
1405 + 5.0 * rng.gen::<f32>();
1406
1407 Particle::new_directed(
1408 Duration::from_secs_f32(0.5),
1409 time,
1410 ParticleMode::FieryBurst,
1411 start_pos,
1412 end_pos,
1413 )
1414 },
1415 );
1416 self.particles.resize_with(
1417 self.particles.len()
1418 + usize::from(
1419 self.scheduler
1420 .heartbeats(Duration::from_millis(10)),
1421 ),
1422 || {
1423 Particle::new(
1424 Duration::from_millis(650),
1425 time,
1426 ParticleMode::FieryBurstVortex,
1427 pos.map(|e| e + rng.gen_range(-0.25..0.25))
1428 + Vec3::new(0.0, 0.0, 1.0),
1429 )
1430 },
1431 );
1432 self.particles.resize_with(
1433 self.particles.len()
1434 + usize::from(
1435 self.scheduler
1436 .heartbeats(Duration::from_millis(40)),
1437 ),
1438 || {
1439 Particle::new(
1440 Duration::from_millis(1000),
1441 time,
1442 ParticleMode::FieryBurstSparks,
1443 pos.map(|e| e + rng.gen_range(-0.25..0.25)),
1444 )
1445 },
1446 );
1447 self.particles.resize_with(
1448 self.particles.len()
1449 + usize::from(
1450 self.scheduler
1451 .heartbeats(Duration::from_millis(14)),
1452 ),
1453 || {
1454 let pos1 = pos.map(|e| e + rng.gen_range(-0.25..0.25));
1455 Particle::new_directed(
1456 Duration::from_millis(1000),
1457 time,
1458 ParticleMode::FieryBurstAsh,
1459 pos1,
1460 Vec3::new(
1461 4.5, 20.4, 8.58) + pos1,
1465 )
1466 },
1467 );
1468 }
1469 },
1470 }
1471 }
1472 use buff::BuffKind;
1473 if let BuffKind::Frenzied = c.static_data.buff_kind {
1474 if matches!(c.stage_section, StageSection::Action) {
1475 self.particles.resize_with(
1476 self.particles.len()
1477 + usize::from(
1478 self.scheduler.heartbeats(Duration::from_millis(5)),
1479 ),
1480 || {
1481 let start_pos = interpolated.pos
1482 + Vec3::new(
1483 body.max_radius(),
1484 body.max_radius(),
1485 body.height() / 2.0,
1486 )
1487 .map(|d| d * rng.gen_range(-1.0..1.0));
1488 let end_pos =
1489 interpolated.pos + (start_pos - interpolated.pos) * 6.0;
1490 Particle::new_directed(
1491 Duration::from_secs(1),
1492 time,
1493 ParticleMode::Enraged,
1494 start_pos,
1495 end_pos,
1496 )
1497 },
1498 );
1499 }
1500 }
1501 },
1502 CharacterState::BasicBeam(beam) => {
1503 let ori = *ori;
1504 let _look_dir = *character_activity.look_dir.unwrap_or(ori.look_dir());
1505 let dir = ori.look_dir(); let specifier = beam.static_data.specifier;
1507 if specifier == beam::FrontendSpecifier::PhoenixLaser
1508 && matches!(beam.stage_section, StageSection::Buildup)
1509 {
1510 self.particles.resize_with(
1511 self.particles.len()
1512 + 2 * usize::from(
1513 self.scheduler.heartbeats(Duration::from_millis(2)),
1514 ),
1515 || {
1516 let mut left_right_alignment =
1517 dir.cross(Vec3::new(0.0, 0.0, 1.0)).normalized();
1518 if rng.gen_bool(0.5) {
1519 left_right_alignment *= -1.0;
1520 }
1521 let start = interpolated.pos
1522 + left_right_alignment * 4.0
1523 + dir.normalized() * 6.0;
1524 let lifespan = Duration::from_secs_f32(0.5);
1525 Particle::new_directed(
1526 lifespan,
1527 time,
1528 ParticleMode::PhoenixBuildUpAim,
1529 start,
1530 interpolated.pos
1531 + dir.normalized() * 3.0
1532 + left_right_alignment * 0.4
1533 + vel
1534 .map_or(Vec3::zero(), |v| v.0 * lifespan.as_secs_f32()),
1535 )
1536 },
1537 );
1538 }
1539 },
1540 CharacterState::Glide(glide) => {
1541 if let Some(Fluid::Air {
1542 vel: air_vel,
1543 elevation: _,
1544 }) = physics.in_fluid
1545 {
1546 const MAX_AIR_VEL: f32 = 15.0;
1549 const MIN_AIR_VEL: f32 = -2.0;
1550
1551 let minmax_norm = |val, min, max| (val - min) / (max - min);
1552
1553 let wind_speed = air_vel.0.magnitude();
1554
1555 let heartbeat = 200
1557 - Lerp::lerp(
1558 50u64,
1559 150,
1560 minmax_norm(wind_speed, MIN_AIR_VEL, MAX_AIR_VEL),
1561 );
1562
1563 let new_count = self.particles.len()
1564 + usize::from(
1565 self.scheduler.heartbeats(Duration::from_millis(heartbeat)),
1566 );
1567
1568 let duration = Lerp::lerp(
1570 0u64,
1571 1000,
1572 minmax_norm(wind_speed, MIN_AIR_VEL, MAX_AIR_VEL),
1573 );
1574 let duration = Duration::from_millis(duration);
1575
1576 self.particles.resize_with(new_count, || {
1577 let start_pos = interpolated.pos
1578 + Vec3::new(
1579 body.max_radius(),
1580 body.max_radius(),
1581 body.height() / 2.0,
1582 )
1583 .map(|d| d * rng.gen_range(-10.0..10.0));
1584
1585 Particle::new_directed(
1586 duration,
1587 time,
1588 ParticleMode::Airflow,
1589 start_pos,
1590 start_pos + air_vel.0,
1591 )
1592 });
1593
1594 if let Some(states::glide::Boost::Forward(_)) = &glide.booster
1596 && let Some(figure_state) =
1597 figure_mgr.states.character_states.get(&entity)
1598 && let Some(tp0) = figure_state.main_abs_trail_points
1599 && let Some(tp1) = figure_state.off_abs_trail_points
1600 {
1601 for _ in 0..self.scheduler.heartbeats(Duration::from_millis(5)) {
1602 self.particles.push(Particle::new(
1603 Duration::from_secs(2),
1604 time,
1605 ParticleMode::EngineJet,
1606 ((tp0.0 + tp1.1) * 0.5)
1607 + Vec3::unit_z() * 0.5
1609 + Vec3::<f32>::zero().map(|_| rng.gen_range(-0.25..0.25))
1610 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
1611 ));
1612 }
1613 }
1614 }
1615 },
1616 CharacterState::Transform(data) => {
1617 if matches!(data.stage_section, StageSection::Buildup)
1618 && let Some(specifier) = data.static_data.specifier
1619 {
1620 match specifier {
1621 states::transform::FrontendSpecifier::Evolve => {
1622 self.particles.resize_with(
1623 self.particles.len()
1624 + usize::from(
1625 self.scheduler.heartbeats(Duration::from_millis(10)),
1626 ),
1627 || {
1628 let start_pos = interpolated.pos
1629 + (Vec2::unit_y()
1630 * rng.gen::<f32>()
1631 * body.max_radius())
1632 .rotated_z(rng.gen_range(0.0..(PI * 2.0)))
1633 .with_z(body.height() * rng.gen::<f32>());
1634
1635 Particle::new_directed(
1636 Duration::from_millis(100),
1637 time,
1638 ParticleMode::BarrelOrgan,
1639 start_pos,
1640 start_pos + Vec3::unit_z() * 2.0,
1641 )
1642 },
1643 )
1644 },
1645 states::transform::FrontendSpecifier::Cursekeeper => {
1646 self.particles.resize_with(
1647 self.particles.len()
1648 + usize::from(
1649 self.scheduler.heartbeats(Duration::from_millis(10)),
1650 ),
1651 || {
1652 let start_pos = interpolated.pos
1653 + (Vec2::unit_y()
1654 * rng.gen::<f32>()
1655 * body.max_radius())
1656 .rotated_z(rng.gen_range(0.0..(PI * 2.0)))
1657 .with_z(body.height() * rng.gen::<f32>());
1658
1659 Particle::new_directed(
1660 Duration::from_millis(100),
1661 time,
1662 ParticleMode::FireworkPurple,
1663 start_pos,
1664 start_pos + Vec3::unit_z() * 2.0,
1665 )
1666 },
1667 )
1668 },
1669 }
1670 }
1671 },
1672 CharacterState::ChargedMelee(_melee) => {
1673 self.maintain_hydra_tail_swipe_particles(
1674 scene_data,
1675 figure_mgr,
1676 entity,
1677 interpolated.pos,
1678 character_state,
1679 inventory,
1680 );
1681 },
1682 _ => {},
1683 }
1684 }
1685 }
1686
1687 fn maintain_beam_particles(&mut self, scene_data: &SceneData, lights: &mut Vec<Light>) {
1688 let state = scene_data.state;
1689 let ecs = state.ecs();
1690 let time = state.get_time();
1691 let terrain = state.terrain();
1692 let tick_elapse = u32::from(self.scheduler.heartbeats(Duration::from_millis(1)).min(100));
1695 let mut rng = thread_rng();
1696
1697 for (beam, ori) in (&ecs.read_storage::<Beam>(), &ecs.read_storage::<Ori>()).join() {
1698 let beam_tick_count = tick_elapse as f32 * beam.specifier.particles_per_sec();
1699 let beam_tick_count = if rng.gen_bool(f64::from(beam_tick_count.fract())) {
1700 beam_tick_count.ceil() as u32
1701 } else {
1702 beam_tick_count.floor() as u32
1703 };
1704
1705 if beam_tick_count == 0 {
1706 continue;
1707 }
1708
1709 let distributed_time = tick_elapse as f64 / (beam_tick_count * 1000) as f64;
1710 let angle = (beam.end_radius / beam.range).atan();
1711 let beam_dir = (beam.bezier.ctrl - beam.bezier.start)
1712 .try_normalized()
1713 .unwrap_or(*ori.look_dir());
1714 let raycast_distance = |from, to| terrain.ray(from, to).until(Block::is_solid).cast().0;
1715
1716 self.particles.reserve(beam_tick_count as usize);
1717 match beam.specifier {
1718 beam::FrontendSpecifier::Flamethrower => {
1719 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
1720 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
1721 if scene_data.flashing_lights_enabled {
1723 lights.push(Light::new(
1724 beam.bezier.start,
1725 Rgb::new(1.0, 0.25, 0.05).map(|e| e * rng.gen_range(0.8..1.2)),
1726 2.0,
1727 ));
1728 }
1729
1730 for i in 0..beam_tick_count {
1731 let phi: f32 = rng.gen_range(0.0..angle);
1732 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
1733 let offset_z =
1734 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
1735 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
1736 self.particles.push(Particle::new_directed_with_collision(
1737 Duration::from_secs_f64(beam.duration.0),
1738 time + distributed_time * i as f64,
1739 ParticleMode::FlameThrower,
1740 beam.bezier.start,
1741 beam.bezier.start + random_ori * beam.range,
1742 raycast_distance,
1743 ));
1744 }
1745 },
1746 beam::FrontendSpecifier::Cultist => {
1747 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
1748 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
1749 if scene_data.flashing_lights_enabled {
1751 lights.push(Light::new(
1752 beam.bezier.start,
1753 Rgb::new(1.0, 0.0, 1.0).map(|e| e * rng.gen_range(0.5..1.0)),
1754 2.0,
1755 ));
1756 }
1757 for i in 0..beam_tick_count {
1758 let phi: f32 = rng.gen_range(0.0..angle);
1759 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
1760 let offset_z =
1761 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
1762 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
1763 self.particles.push(Particle::new_directed_with_collision(
1764 Duration::from_secs_f64(beam.duration.0),
1765 time + distributed_time * i as f64,
1766 ParticleMode::CultistFlame,
1767 beam.bezier.start,
1768 beam.bezier.start + random_ori * beam.range,
1769 raycast_distance,
1770 ));
1771 }
1772 },
1773 beam::FrontendSpecifier::LifestealBeam => {
1774 if scene_data.flashing_lights_enabled {
1776 lights.push(Light::new(beam.bezier.start, Rgb::new(0.8, 1.0, 0.5), 1.0));
1777 }
1778
1779 let bezier_end = beam.bezier.start + beam_dir * beam.range;
1781 let distance = raycast_distance(beam.bezier.start, bezier_end);
1782 for i in 0..beam_tick_count {
1783 self.particles.push(Particle::new_directed_with_collision(
1784 Duration::from_secs_f64(beam.duration.0),
1785 time + distributed_time * i as f64,
1786 ParticleMode::LifestealBeam,
1787 beam.bezier.start,
1788 bezier_end,
1789 |_from, _to| distance,
1790 ));
1791 }
1792 },
1793 beam::FrontendSpecifier::Gravewarden => {
1794 for i in 0..beam_tick_count {
1795 let mut offset = 0.5;
1796 let side = Vec2::new(-beam_dir.y, beam_dir.x);
1797 self.particles.resize_with(self.particles.len() + 2, || {
1798 offset = -offset;
1799 Particle::new_directed_with_collision(
1800 Duration::from_secs_f64(beam.duration.0),
1801 time + distributed_time * i as f64,
1802 ParticleMode::Laser,
1803 beam.bezier.start + beam_dir * 1.5 + side * offset,
1804 beam.bezier.start + beam_dir * beam.range + side * offset,
1805 raycast_distance,
1806 )
1807 });
1808 }
1809 },
1810 beam::FrontendSpecifier::WebStrand => {
1811 let bezier_end = beam.bezier.start + beam_dir * beam.range;
1812 let distance = raycast_distance(beam.bezier.start, bezier_end);
1813 for i in 0..beam_tick_count {
1814 self.particles.push(Particle::new_directed_with_collision(
1815 Duration::from_secs_f64(beam.duration.0),
1816 time + distributed_time * i as f64,
1817 ParticleMode::WebStrand,
1818 beam.bezier.start,
1819 bezier_end,
1820 |_from, _to| distance,
1821 ));
1822 }
1823 },
1824 beam::FrontendSpecifier::Bubbles => {
1825 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
1826 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
1827 for i in 0..beam_tick_count {
1828 let phi: f32 = rng.gen_range(0.0..angle);
1829 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
1830 let offset_z =
1831 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
1832 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
1833 self.particles.push(Particle::new_directed_with_collision(
1834 Duration::from_secs_f64(beam.duration.0),
1835 time + distributed_time * i as f64,
1836 ParticleMode::Bubbles,
1837 beam.bezier.start,
1838 beam.bezier.start + random_ori * beam.range,
1839 raycast_distance,
1840 ));
1841 }
1842 },
1843 beam::FrontendSpecifier::Poison => {
1844 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
1845 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
1846 for i in 0..beam_tick_count {
1847 let phi: f32 = rng.gen_range(0.0..angle);
1848 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
1849 let offset_z =
1850 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
1851 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
1852 self.particles.push(Particle::new_directed_with_collision(
1853 Duration::from_secs_f64(beam.duration.0),
1854 time + distributed_time * i as f64,
1855 ParticleMode::Poison,
1856 beam.bezier.start,
1857 beam.bezier.start + random_ori * beam.range,
1858 raycast_distance,
1859 ));
1860 }
1861 },
1862 beam::FrontendSpecifier::Ink => {
1863 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
1864 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
1865 for i in 0..beam_tick_count {
1866 let phi: f32 = rng.gen_range(0.0..angle);
1867 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
1868 let offset_z =
1869 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
1870 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
1871 self.particles.push(Particle::new_directed_with_collision(
1872 Duration::from_secs_f64(beam.duration.0),
1873 time + distributed_time * i as f64,
1874 ParticleMode::Bubbles,
1875 beam.bezier.start,
1876 beam.bezier.start + random_ori * beam.range,
1877 raycast_distance,
1878 ));
1879 }
1880 },
1881 beam::FrontendSpecifier::Steam => {
1882 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
1883 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
1884 for i in 0..beam_tick_count {
1885 let phi: f32 = rng.gen_range(0.0..angle);
1886 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
1887 let offset_z =
1888 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
1889 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
1890 self.particles.push(Particle::new_directed_with_collision(
1891 Duration::from_secs_f64(beam.duration.0),
1892 time + distributed_time * i as f64,
1893 ParticleMode::Steam,
1894 beam.bezier.start,
1895 beam.bezier.start + random_ori * beam.range,
1896 raycast_distance,
1897 ));
1898 }
1899 },
1900 beam::FrontendSpecifier::Lightning => {
1901 let bezier_end = beam.bezier.start + beam_dir * beam.range;
1902 let distance = raycast_distance(beam.bezier.start, bezier_end);
1903 for i in 0..beam_tick_count {
1904 self.particles.push(Particle::new_directed_with_collision(
1905 Duration::from_secs_f64(beam.duration.0),
1906 time + distributed_time * i as f64,
1907 ParticleMode::Lightning,
1908 beam.bezier.start,
1909 bezier_end,
1910 |_from, _to| distance,
1911 ));
1912 }
1913 },
1914 beam::FrontendSpecifier::Frost => {
1915 let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
1916 let m = Mat3::<f32>::rotation_from_to_3d(from, to);
1917 for i in 0..beam_tick_count {
1918 let phi: f32 = rng.gen_range(0.0..angle);
1919 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
1920 let offset_z =
1921 Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
1922 let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
1923 self.particles.push(Particle::new_directed_with_collision(
1924 Duration::from_secs_f64(beam.duration.0),
1925 time + distributed_time * i as f64,
1926 ParticleMode::Ice,
1927 beam.bezier.start,
1928 beam.bezier.start + random_ori * beam.range,
1929 raycast_distance,
1930 ));
1931 }
1932 },
1933 beam::FrontendSpecifier::PhoenixLaser => {
1934 let bezier_end = beam.bezier.start + beam_dir * beam.range;
1935 let distance = raycast_distance(beam.bezier.start, bezier_end);
1936 for i in 0..beam_tick_count {
1937 self.particles.push(Particle::new_directed_with_collision(
1938 Duration::from_secs_f64(beam.duration.0),
1939 time + distributed_time * i as f64,
1940 ParticleMode::PhoenixBeam,
1941 beam.bezier.start,
1942 bezier_end,
1943 |_from, _to| distance,
1944 ));
1945 }
1946 },
1947 }
1948 }
1949 }
1950
1951 fn maintain_aura_particles(&mut self, scene_data: &SceneData) {
1952 let state = scene_data.state;
1953 let ecs = state.ecs();
1954 let time = state.get_time();
1955 let mut rng = thread_rng();
1956 let dt = scene_data.state.get_delta_time();
1957
1958 for (interp, pos, auras, body_maybe) in (
1959 ecs.read_storage::<Interpolated>().maybe(),
1960 &ecs.read_storage::<Pos>(),
1961 &ecs.read_storage::<comp::Auras>(),
1962 ecs.read_storage::<comp::Body>().maybe(),
1963 )
1964 .join()
1965 {
1966 let pos = interp.map_or(pos.0, |i| i.pos);
1967
1968 for (_, aura) in auras.auras.iter() {
1969 match aura.aura_kind {
1970 aura::AuraKind::Buff {
1971 kind: buff::BuffKind::ProtectingWard,
1972 ..
1973 } => {
1974 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
1975 self.particles.resize_with(
1976 self.particles.len()
1977 + aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
1978 || {
1979 let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100));
1980 let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
1981 let duration = Duration::from_secs_f64(
1982 aura.end_time
1983 .map_or(1.0, |end| end.0 - time)
1984 .clamp(0.0, 1.0),
1985 );
1986 Particle::new_directed(
1987 duration,
1988 time,
1989 ParticleMode::EnergyNature,
1990 pos,
1991 pos + init_pos,
1992 )
1993 },
1994 );
1995 },
1996 aura::AuraKind::Buff {
1997 kind: buff::BuffKind::Regeneration,
1998 ..
1999 } => {
2000 if auras.auras.iter().any(|(_, aura)| {
2001 matches!(aura.aura_kind, aura::AuraKind::Buff {
2002 kind: buff::BuffKind::ProtectingWard,
2003 ..
2004 })
2005 }) {
2006 continue;
2009 }
2010 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
2011 self.particles.resize_with(
2012 self.particles.len()
2013 + aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
2014 || {
2015 let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100));
2016 let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
2017 let duration = Duration::from_secs_f64(
2018 aura.end_time
2019 .map_or(1.0, |end| end.0 - time)
2020 .clamp(0.0, 1.0),
2021 );
2022 Particle::new_directed(
2023 duration,
2024 time,
2025 ParticleMode::EnergyHealing,
2026 pos,
2027 pos + init_pos,
2028 )
2029 },
2030 );
2031 },
2032 aura::AuraKind::Buff {
2033 kind: buff::BuffKind::Burning,
2034 ..
2035 } => {
2036 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
2037 self.particles.resize_with(
2038 self.particles.len()
2039 + aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
2040 || {
2041 let rand_pos = {
2042 let theta = rng.gen::<f32>() * TAU;
2043 let radius = aura.radius * rng.gen::<f32>().sqrt();
2044 let x = radius * theta.sin();
2045 let y = radius * theta.cos();
2046 Vec2::new(x, y) + pos.xy()
2047 };
2048 let duration = Duration::from_secs_f64(
2049 aura.end_time
2050 .map_or(1.0, |end| end.0 - time)
2051 .clamp(0.0, 1.0),
2052 );
2053 Particle::new_directed(
2054 duration,
2055 time,
2056 ParticleMode::FlameThrower,
2057 rand_pos.with_z(pos.z),
2058 rand_pos.with_z(pos.z + 1.0),
2059 )
2060 },
2061 );
2062 },
2063 aura::AuraKind::Buff {
2064 kind: buff::BuffKind::Hastened,
2065 ..
2066 } => {
2067 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
2068 self.particles.resize_with(
2069 self.particles.len()
2070 + aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
2071 || {
2072 let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100));
2073 let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
2074 let duration = Duration::from_secs_f64(
2075 aura.end_time
2076 .map_or(1.0, |end| end.0 - time)
2077 .clamp(0.0, 1.0),
2078 );
2079 Particle::new_directed(
2080 duration,
2081 time,
2082 ParticleMode::EnergyBuffing,
2083 pos,
2084 pos + init_pos,
2085 )
2086 },
2087 );
2088 },
2089 aura::AuraKind::Buff {
2090 kind: buff::BuffKind::Frozen,
2091 ..
2092 } => {
2093 let is_new_aura = aura.data.duration.is_none_or(|max_dur| {
2094 let rem_dur = aura.end_time.map_or(time, |e| e.0) - time;
2095 rem_dur > max_dur.0 * 0.9
2096 });
2097 if is_new_aura {
2098 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
2099 self.particles.resize_with(
2100 self.particles.len()
2101 + aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
2102 || {
2103 let rand_angle = rng.gen_range(0.0..TAU);
2104 let offset =
2105 Vec2::new(rand_angle.cos(), rand_angle.sin()) * aura.radius;
2106 let z_start = body_maybe
2107 .map_or(0.0, |b| rng.gen_range(0.5..0.75) * b.height());
2108 let z_end = body_maybe
2109 .map_or(0.0, |b| rng.gen_range(0.0..3.0) * b.height());
2110 Particle::new_directed(
2111 Duration::from_secs(3),
2112 time,
2113 ParticleMode::Ice,
2114 pos + Vec3::unit_z() * z_start,
2115 pos + offset.with_z(z_end),
2116 )
2117 },
2118 );
2119 }
2120 },
2121 aura::AuraKind::Buff {
2122 kind: buff::BuffKind::Heatstroke,
2123 ..
2124 } => {
2125 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
2126 self.particles.resize_with(
2127 self.particles.len()
2128 + aura.radius.powi(2) as usize * usize::from(heartbeats) / 900,
2129 || {
2130 let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100));
2131 let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
2132 let duration = Duration::from_secs_f64(
2133 aura.end_time
2134 .map_or(1.0, |end| end.0 - time)
2135 .clamp(0.0, 1.0),
2136 );
2137 Particle::new_directed(
2138 duration,
2139 time,
2140 ParticleMode::EnergyPhoenix,
2141 pos,
2142 pos + init_pos,
2143 )
2144 },
2145 );
2146
2147 let num_particles = aura.radius.powi(2) * dt / 50.0;
2148 let num_particles = num_particles.floor() as usize
2149 + usize::from(rng.gen_bool(f64::from(num_particles % 1.0)));
2150 self.particles
2151 .resize_with(self.particles.len() + num_particles, || {
2152 let rand_pos = {
2153 let theta = rng.gen::<f32>() * TAU;
2154 let radius = aura.radius * rng.gen::<f32>().sqrt();
2155 let x = radius * theta.sin();
2156 let y = radius * theta.cos();
2157 Vec2::new(x, y) + pos.xy()
2158 };
2159 let duration = Duration::from_secs_f64(
2160 aura.end_time
2161 .map_or(1.0, |end| end.0 - time)
2162 .clamp(0.0, 1.0),
2163 );
2164 Particle::new_directed(
2165 duration,
2166 time,
2167 ParticleMode::FieryBurstAsh,
2168 pos,
2169 Vec3::new(
2170 0.0, 20.0, 5.5) + rand_pos.with_z(pos.z),
2174 )
2175 });
2176 },
2177 _ => {},
2178 }
2179 }
2180 }
2181 }
2182
2183 fn maintain_buff_particles(&mut self, scene_data: &SceneData) {
2184 let state = scene_data.state;
2185 let ecs = state.ecs();
2186 let time = state.get_time();
2187 let mut rng = thread_rng();
2188
2189 for (interp, pos, buffs, body, ori, scale) in (
2190 ecs.read_storage::<Interpolated>().maybe(),
2191 &ecs.read_storage::<Pos>(),
2192 &ecs.read_storage::<comp::Buffs>(),
2193 &ecs.read_storage::<Body>(),
2194 &ecs.read_storage::<Ori>(),
2195 ecs.read_storage::<Scale>().maybe(),
2196 )
2197 .join()
2198 {
2199 let pos = interp.map_or(pos.0, |i| i.pos);
2200
2201 for (buff_kind, buff_keys) in buffs
2202 .kinds
2203 .iter()
2204 .filter_map(|(kind, keys)| keys.as_ref().map(|keys| (kind, keys)))
2205 {
2206 use buff::BuffKind;
2207 match buff_kind {
2208 BuffKind::Cursed | BuffKind::Burning => {
2209 self.particles.resize_with(
2210 self.particles.len()
2211 + usize::from(self.scheduler.heartbeats(Duration::from_millis(15))),
2212 || {
2213 let start_pos = pos
2214 + Vec3::unit_z() * body.height() * 0.25
2215 + Vec3::<f32>::zero()
2216 .map(|_| rng.gen_range(-1.0..1.0))
2217 .normalized()
2218 * 0.25;
2219 let end_pos = start_pos
2220 + Vec3::unit_z() * body.height()
2221 + Vec3::<f32>::zero()
2222 .map(|_| rng.gen_range(-1.0..1.0))
2223 .normalized();
2224 Particle::new_directed(
2225 Duration::from_secs(1),
2226 time,
2227 if matches!(buff_kind, BuffKind::Cursed) {
2228 ParticleMode::CultistFlame
2229 } else {
2230 ParticleMode::FlameThrower
2231 },
2232 start_pos,
2233 end_pos,
2234 )
2235 },
2236 );
2237 },
2238 BuffKind::PotionSickness => {
2239 let mut multiplicity = 0;
2240 if buff_keys.0
2243 .iter()
2244 .filter_map(|key| buffs.buffs.get(*key))
2245 .any(|buff| {
2246 matches!(buff.elapsed(Time(time)), dur if (1.0..=1.5).contains(&dur.0))
2247 })
2248 {
2249 multiplicity = 1;
2250 }
2251 self.particles.resize_with(
2252 self.particles.len()
2253 + multiplicity
2254 * usize::from(
2255 self.scheduler.heartbeats(Duration::from_millis(25)),
2256 ),
2257 || {
2258 let start_pos = pos
2259 + Vec3::unit_z() * body.eye_height(scale.map_or(1.0, |s| s.0));
2260 let (radius, theta) =
2261 (rng.gen_range(0.0f32..1.0).sqrt(), rng.gen_range(0.0..TAU));
2262 let end_pos = pos
2263 + *ori.look_dir()
2264 + Vec3::<f32>::new(
2265 radius * theta.cos(),
2266 radius * theta.sin(),
2267 0.0,
2268 ) * 0.25;
2269 Particle::new_directed(
2270 Duration::from_secs(1),
2271 time,
2272 ParticleMode::PotionSickness,
2273 start_pos,
2274 end_pos,
2275 )
2276 },
2277 );
2278 },
2279 BuffKind::Frenzied => {
2280 self.particles.resize_with(
2281 self.particles.len()
2282 + usize::from(self.scheduler.heartbeats(Duration::from_millis(15))),
2283 || {
2284 let start_pos = pos
2285 + Vec3::new(
2286 body.max_radius(),
2287 body.max_radius(),
2288 body.height() / 2.0,
2289 )
2290 .map(|d| d * rng.gen_range(-1.0..1.0));
2291 let end_pos = start_pos
2292 + Vec3::unit_z() * body.height()
2293 + Vec3::<f32>::zero()
2294 .map(|_| rng.gen_range(-1.0..1.0))
2295 .normalized();
2296 Particle::new_directed(
2297 Duration::from_secs(1),
2298 time,
2299 ParticleMode::Enraged,
2300 start_pos,
2301 end_pos,
2302 )
2303 },
2304 );
2305 },
2306 BuffKind::Polymorphed => {
2307 let mut multiplicity = 0;
2308 if buff_keys.0
2311 .iter()
2312 .filter_map(|key| buffs.buffs.get(*key))
2313 .any(|buff| {
2314 matches!(buff.elapsed(Time(time)), dur if (0.1..=0.3).contains(&dur.0))
2315 })
2316 {
2317 multiplicity = 1;
2318 }
2319 self.particles.resize_with(
2320 self.particles.len()
2321 + multiplicity
2322 * self.scheduler.heartbeats(Duration::from_millis(3)) as usize,
2323 || {
2324 let start_pos = pos
2325 + Vec3::unit_z() * body.eye_height(scale.map_or(1.0, |s| s.0))
2326 / 2.0;
2327 let end_pos = start_pos
2328 + Vec3::<f32>::zero()
2329 .map(|_| rng.gen_range(-1.0..1.0))
2330 .normalized()
2331 * 5.0;
2332
2333 Particle::new_directed(
2334 Duration::from_secs(2),
2335 time,
2336 ParticleMode::Explosion,
2337 start_pos,
2338 end_pos,
2339 )
2340 },
2341 )
2342 },
2343 _ => {},
2344 }
2345 }
2346 }
2347 }
2348
2349 fn maintain_block_particles(
2350 &mut self,
2351 scene_data: &SceneData,
2352 terrain: &Terrain<TerrainChunk>,
2353 figure_mgr: &FigureMgr,
2354 ) {
2355 prof_span!("ParticleMgr::maintain_block_particles");
2356 let dt = scene_data.state.ecs().fetch::<DeltaTime>().0;
2357 let time = scene_data.state.get_time();
2358 let player_pos = scene_data
2359 .state
2360 .read_component_copied::<Interpolated>(scene_data.viewpoint_entity)
2361 .map(|i| i.pos)
2362 .unwrap_or_default();
2363 let player_chunk = player_pos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| {
2364 (e.floor() as i32).div_euclid(sz as i32)
2365 });
2366
2367 struct BlockParticles<'a> {
2368 blocks: fn(&'a BlocksOfInterest) -> BlockParticleSlice<'a>,
2370 range: usize,
2372 rate: f32,
2374 lifetime: f32,
2376 mode: ParticleMode,
2378 cond: fn(&SceneData) -> bool,
2380 }
2381
2382 enum BlockParticleSlice<'a> {
2383 Positions(&'a [Vec3<i32>]),
2384 PositionsAndDirs(&'a [(Vec3<i32>, Vec3<f32>)]),
2385 }
2386
2387 impl BlockParticleSlice<'_> {
2388 fn len(&self) -> usize {
2389 match self {
2390 Self::Positions(blocks) => blocks.len(),
2391 Self::PositionsAndDirs(blocks) => blocks.len(),
2392 }
2393 }
2394 }
2395
2396 let particles: &[BlockParticles] = &[
2397 BlockParticles {
2398 blocks: |boi| BlockParticleSlice::Positions(&boi.leaves),
2399 range: 4,
2400 rate: 0.0125,
2401 lifetime: 30.0,
2402 mode: ParticleMode::Leaf,
2403 cond: |_| true,
2404 },
2405 BlockParticles {
2406 blocks: |boi| BlockParticleSlice::Positions(&boi.drip),
2407 range: 4,
2408 rate: 0.004,
2409 lifetime: 20.0,
2410 mode: ParticleMode::Drip,
2411 cond: |_| true,
2412 },
2413 BlockParticles {
2414 blocks: |boi| BlockParticleSlice::Positions(&boi.fires),
2415 range: 2,
2416 rate: 20.0,
2417 lifetime: 0.25,
2418 mode: ParticleMode::CampfireFire,
2419 cond: |_| true,
2420 },
2421 BlockParticles {
2422 blocks: |boi| BlockParticleSlice::Positions(&boi.fire_bowls),
2423 range: 2,
2424 rate: 20.0,
2425 lifetime: 0.25,
2426 mode: ParticleMode::FireBowl,
2427 cond: |_| true,
2428 },
2429 BlockParticles {
2430 blocks: |boi| BlockParticleSlice::Positions(&boi.fireflies),
2431 range: 6,
2432 rate: 0.004,
2433 lifetime: 40.0,
2434 mode: ParticleMode::Firefly,
2435 cond: |sd| sd.state.get_day_period().is_dark(),
2436 },
2437 BlockParticles {
2438 blocks: |boi| BlockParticleSlice::Positions(&boi.flowers),
2439 range: 5,
2440 rate: 0.002,
2441 lifetime: 40.0,
2442 mode: ParticleMode::Firefly,
2443 cond: |sd| sd.state.get_day_period().is_dark(),
2444 },
2445 BlockParticles {
2446 blocks: |boi| BlockParticleSlice::Positions(&boi.beehives),
2447 range: 3,
2448 rate: 0.5,
2449 lifetime: 30.0,
2450 mode: ParticleMode::Bee,
2451 cond: |sd| sd.state.get_day_period().is_light(),
2452 },
2453 BlockParticles {
2454 blocks: |boi| BlockParticleSlice::Positions(&boi.snow),
2455 range: 4,
2456 rate: 0.025,
2457 lifetime: 15.0,
2458 mode: ParticleMode::Snow,
2459 cond: |_| true,
2460 },
2461 BlockParticles {
2462 blocks: |boi| BlockParticleSlice::PositionsAndDirs(&boi.one_way_walls),
2463 range: 2,
2464 rate: 12.0,
2465 lifetime: 1.5,
2466 mode: ParticleMode::PortalFizz,
2467 cond: |_| true,
2468 },
2469 BlockParticles {
2470 blocks: |boi| BlockParticleSlice::Positions(&boi.spores),
2471 range: 4,
2472 rate: 0.055,
2473 lifetime: 20.0,
2474 mode: ParticleMode::Spore,
2475 cond: |_| true,
2476 },
2477 BlockParticles {
2478 blocks: |boi| BlockParticleSlice::PositionsAndDirs(&boi.waterfall),
2479 range: 2,
2480 rate: 4.0,
2481 lifetime: 5.0,
2482 mode: ParticleMode::WaterFoam,
2483 cond: |_| true,
2484 },
2485 ];
2486
2487 let ecs = scene_data.state.ecs();
2488 let mut rng = thread_rng();
2489 let cap = 512;
2492 for particles in particles.iter() {
2493 if !(particles.cond)(scene_data) {
2494 continue;
2495 }
2496
2497 for offset in Spiral2d::new().take((particles.range * 2 + 1).pow(2)) {
2498 let chunk_pos = player_chunk + offset;
2499
2500 terrain.get(chunk_pos).map(|chunk_data| {
2501 let blocks = (particles.blocks)(&chunk_data.blocks_of_interest);
2502
2503 let avg_particles = dt * (blocks.len() as f32 * particles.rate).min(cap as f32);
2504 let particle_count = avg_particles.trunc() as usize
2505 + (rng.gen::<f32>() < avg_particles.fract()) as usize;
2506
2507 self.particles
2508 .resize_with(self.particles.len() + particle_count, || {
2509 match blocks {
2510 BlockParticleSlice::Positions(blocks) => {
2511 let block_pos = Vec3::from(
2513 chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32),
2514 ) + blocks.choose(&mut rng).copied().unwrap();
2515 Particle::new(
2516 Duration::from_secs_f32(particles.lifetime),
2517 time,
2518 particles.mode,
2519 block_pos.map(|e: i32| e as f32 + rng.gen::<f32>()),
2520 )
2521 },
2522 BlockParticleSlice::PositionsAndDirs(blocks) => {
2523 let (block_offset, particle_dir) =
2525 blocks.choose(&mut rng).copied().unwrap();
2526 let block_pos = Vec3::from(
2527 chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32),
2528 ) + block_offset;
2529 let particle_pos =
2530 block_pos.map(|e: i32| e as f32 + rng.gen::<f32>());
2531 Particle::new_directed(
2532 Duration::from_secs_f32(particles.lifetime),
2533 time,
2534 particles.mode,
2535 particle_pos,
2536 particle_pos + particle_dir,
2537 )
2538 },
2539 }
2540 })
2541 });
2542 }
2543
2544 for (entity, body, interpolated, collider) in (
2545 &ecs.entities(),
2546 &ecs.read_storage::<comp::Body>(),
2547 &ecs.read_storage::<crate::ecs::comp::Interpolated>(),
2548 ecs.read_storage::<comp::Collider>().maybe(),
2549 )
2550 .join()
2551 {
2552 if let Some((blocks_of_interest, offset)) =
2553 figure_mgr.get_blocks_of_interest(entity, body, collider)
2554 {
2555 let mat = Mat4::from(interpolated.ori.to_quat())
2556 .translated_3d(interpolated.pos)
2557 * Mat4::translation_3d(offset);
2558
2559 let blocks = (particles.blocks)(blocks_of_interest);
2560
2561 let avg_particles = dt * blocks.len() as f32 * particles.rate;
2562 let particle_count = avg_particles.trunc() as usize
2563 + (rng.gen::<f32>() < avg_particles.fract()) as usize;
2564
2565 self.particles
2566 .resize_with(self.particles.len() + particle_count, || {
2567 match blocks {
2568 BlockParticleSlice::Positions(blocks) => {
2569 let rel_pos = blocks
2570 .choose(&mut rng)
2571 .copied()
2572 .unwrap()
2574 .map(|e: i32| e as f32 + rng.gen::<f32>());
2575 let wpos = mat.mul_point(rel_pos);
2576
2577 Particle::new(
2578 Duration::from_secs_f32(particles.lifetime),
2579 time,
2580 particles.mode,
2581 wpos,
2582 )
2583 },
2584 BlockParticleSlice::PositionsAndDirs(blocks) => {
2585 let (block_offset, particle_dir) =
2587 blocks.choose(&mut rng).copied().unwrap();
2588 let particle_pos =
2589 block_offset.map(|e: i32| e as f32 + rng.gen::<f32>());
2590 let wpos = mat.mul_point(particle_pos);
2591 Particle::new_directed(
2592 Duration::from_secs_f32(particles.lifetime),
2593 time,
2594 particles.mode,
2595 wpos,
2596 wpos + mat.mul_direction(particle_dir),
2597 )
2598 },
2599 }
2600 })
2601 }
2602 }
2603 }
2604 {
2606 struct SmokeProperties {
2607 position: Vec3<i32>,
2608 strength: f32,
2609 dry_chance: f32,
2610 }
2611
2612 let range = 8_usize;
2613 let rate = 3.0 / 128.0;
2614 let lifetime = 40.0;
2615 let time_of_day = scene_data
2616 .state
2617 .get_time_of_day()
2618 .rem_euclid(24.0 * 60.0 * 60.0) as f32;
2619
2620 for offset in Spiral2d::new().take((range * 2 + 1).pow(2)) {
2621 let chunk_pos = player_chunk + offset;
2622
2623 terrain.get(chunk_pos).map(|chunk_data| {
2624 let blocks = &chunk_data.blocks_of_interest.smokers;
2625 let mut smoke_properties: Vec<SmokeProperties> = Vec::new();
2626 let block_pos =
2627 Vec3::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32));
2628 let mut sum = 0.0_f32;
2629 for smoker in blocks.iter() {
2630 let position = block_pos + smoker.position;
2631 let (strength, dry_chance) = {
2632 match smoker.kind {
2633 FireplaceType::House => {
2634 let prop = crate::scene::smoke_cycle::smoke_at_time(
2635 position,
2636 chunk_data.blocks_of_interest.temperature,
2637 time_of_day,
2638 );
2639 (
2640 prop.0,
2641 if prop.1 {
2642 0.8 - chunk_data.blocks_of_interest.humidity
2644 } else {
2645 1.2 - chunk_data.blocks_of_interest.humidity
2647 },
2648 )
2649 },
2650 FireplaceType::Workshop => (128.0, 1.0),
2651 }
2652 };
2653 sum += strength;
2654 smoke_properties.push(SmokeProperties {
2655 position,
2656 strength,
2657 dry_chance,
2658 });
2659 }
2660 let avg_particles = dt * sum * rate;
2661
2662 let particle_count = avg_particles.trunc() as usize
2663 + (rng.gen::<f32>() < avg_particles.fract()) as usize;
2664 let chosen = smoke_properties.choose_multiple_weighted(
2665 &mut rng,
2666 particle_count,
2667 |smoker| smoker.strength,
2668 );
2669 if let Ok(chosen) = chosen {
2670 self.particles.extend(chosen.map(|smoker| {
2671 Particle::new(
2672 Duration::from_secs_f32(lifetime),
2673 time,
2674 if rng.gen::<f32>() > smoker.dry_chance {
2675 ParticleMode::BlackSmoke
2676 } else {
2677 ParticleMode::CampfireSmoke
2678 },
2679 smoker.position.map(|e: i32| e as f32 + rng.gen::<f32>()),
2680 )
2681 }));
2682 }
2683 });
2684 }
2685 }
2686 }
2687
2688 fn maintain_shockwave_particles(&mut self, scene_data: &SceneData) {
2689 let state = scene_data.state;
2690 let ecs = state.ecs();
2691 let time = state.get_time();
2692 let dt = scene_data.state.ecs().fetch::<DeltaTime>().0;
2693 let terrain = scene_data.state.ecs().fetch::<TerrainGrid>();
2694
2695 for (_entity, interp, pos, ori, shockwave) in (
2696 &ecs.entities(),
2697 ecs.read_storage::<Interpolated>().maybe(),
2698 &ecs.read_storage::<Pos>(),
2699 &ecs.read_storage::<Ori>(),
2700 &ecs.read_storage::<Shockwave>(),
2701 )
2702 .join()
2703 {
2704 let pos = interp.map_or(pos.0, |i| i.pos);
2705 let ori = interp.map_or(*ori, |i| i.ori);
2706
2707 let elapsed = time - shockwave.creation.unwrap_or(time);
2708 let speed = shockwave.properties.speed;
2709
2710 let percent = elapsed as f32 / shockwave.properties.duration.as_secs_f32();
2711
2712 let distance = speed * elapsed as f32;
2713
2714 let radians = shockwave.properties.angle.to_radians();
2715
2716 let ori_vec = ori.look_vec();
2717 let theta = ori_vec.y.atan2(ori_vec.x) - radians / 2.0;
2718 let dtheta = radians / distance;
2719
2720 let arc_length = distance * radians;
2723
2724 use shockwave::FrontendSpecifier;
2725 match shockwave.properties.specifier {
2726 FrontendSpecifier::Ground => {
2727 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(2));
2728 for heartbeat in 0..heartbeats {
2729 let scale = 1.0 / 3.0;
2731
2732 let scaled_speed = speed * scale;
2733
2734 let sub_tick_interpolation = scaled_speed * 1000.0 * heartbeat as f32;
2735
2736 let distance = speed * (elapsed as f32 - sub_tick_interpolation);
2737
2738 let particle_count_factor = radians / (3.0 * scale);
2739 let new_particle_count = distance * particle_count_factor;
2740 self.particles.reserve(new_particle_count as usize);
2741
2742 for d in 0..(new_particle_count as i32) {
2743 let arc_position = theta + dtheta * d as f32 / particle_count_factor;
2744
2745 let position = pos
2746 + distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0);
2747
2748 let half_ray_length = 10.0;
2752 let mut last_air = false;
2753 let _ = terrain
2761 .ray(
2762 position + Vec3::unit_z() * half_ray_length,
2763 position - Vec3::unit_z() * half_ray_length,
2764 )
2765 .for_each(|block: &Block, pos: Vec3<i32>| {
2766 if block.is_solid() && block.get_sprite().is_none() {
2767 if last_air {
2768 let position = position.xy().with_z(pos.z as f32 + 1.0);
2769
2770 let position_snapped =
2771 ((position / scale).floor() + 0.5) * scale;
2772
2773 self.particles.push(Particle::new(
2774 Duration::from_millis(250),
2775 time,
2776 ParticleMode::GroundShockwave,
2777 position_snapped,
2778 ));
2779 last_air = false;
2780 }
2781 } else {
2782 last_air = true;
2783 }
2784 })
2785 .cast();
2786 }
2787 }
2788 },
2789 FrontendSpecifier::Fire => {
2790 let heartbeats = self.scheduler.heartbeats(Duration::from_millis(2));
2791 for _ in 0..heartbeats {
2792 for d in 0..3 * distance as i32 {
2793 let arc_position = theta + dtheta * d as f32 / 3.0;
2794
2795 let position = pos
2796 + distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0);
2797
2798 self.particles.push(Particle::new(
2799 Duration::from_secs_f32((distance + 10.0) / 50.0),
2800 time,
2801 ParticleMode::FireShockwave,
2802 position,
2803 ));
2804 }
2805 }
2806 },
2807 FrontendSpecifier::Water => {
2808 let particles_per_length = arc_length as usize;
2810 let dtheta = radians / particles_per_length as f32;
2811 let heartbeats = self
2814 .scheduler
2815 .heartbeats(Duration::from_secs_f32(1.0 / speed));
2816
2817 let new_particle_count = particles_per_length * heartbeats as usize;
2819 self.particles.reserve(new_particle_count);
2820
2821 for i in 0..particles_per_length {
2822 let angle = dtheta * i as f32;
2823 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
2824 for j in 0..heartbeats {
2825 let dt = (j as f32 / heartbeats as f32) * dt;
2827 let distance = distance + speed * dt;
2828 let pos1 = pos + distance * direction - Vec3::unit_z();
2829 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
2830 let time = time + dt as f64;
2831
2832 self.particles.push(Particle::new_directed(
2833 Duration::from_secs_f32(0.5),
2834 time,
2835 ParticleMode::Water,
2836 pos1,
2837 pos2,
2838 ));
2839 }
2840 }
2841 },
2842 FrontendSpecifier::Lightning => {
2843 let particles_per_length = arc_length as usize;
2845 let dtheta = radians / particles_per_length as f32;
2846 let heartbeats = self
2849 .scheduler
2850 .heartbeats(Duration::from_secs_f32(1.0 / speed));
2851
2852 let new_particle_count = particles_per_length * heartbeats as usize;
2854 self.particles.reserve(new_particle_count);
2855
2856 for i in 0..particles_per_length {
2857 let angle = dtheta * i as f32;
2858 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
2859 for j in 0..heartbeats {
2860 let dt = (j as f32 / heartbeats as f32) * dt;
2862 let distance = distance + speed * dt;
2863 let pos1 = pos + distance * direction - Vec3::unit_z();
2864 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
2865 let time = time + dt as f64;
2866
2867 self.particles.push(Particle::new_directed(
2868 Duration::from_secs_f32(0.5),
2869 time,
2870 ParticleMode::Lightning,
2871 pos1,
2872 pos2,
2873 ));
2874 }
2875 }
2876 },
2877 FrontendSpecifier::Steam => {
2878 let particles_per_length = arc_length as usize;
2880 let dtheta = radians / particles_per_length as f32;
2881 let heartbeats = self
2884 .scheduler
2885 .heartbeats(Duration::from_secs_f32(1.0 / speed));
2886
2887 let new_particle_count = particles_per_length * heartbeats as usize;
2889 self.particles.reserve(new_particle_count);
2890
2891 for i in 0..particles_per_length {
2892 let angle = dtheta * i as f32;
2893 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
2894 for j in 0..heartbeats {
2895 let dt = (j as f32 / heartbeats as f32) * dt;
2897 let distance = distance + speed * dt;
2898 let pos1 = pos + distance * direction - Vec3::unit_z();
2899 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
2900 let time = time + dt as f64;
2901
2902 self.particles.push(Particle::new_directed(
2903 Duration::from_secs_f32(0.5),
2904 time,
2905 ParticleMode::Steam,
2906 pos1,
2907 pos2,
2908 ));
2909 }
2910 }
2911 },
2912 FrontendSpecifier::Poison => {
2913 let particles_per_length = arc_length as usize;
2915 let dtheta = radians / particles_per_length as f32;
2916 let heartbeats = self
2919 .scheduler
2920 .heartbeats(Duration::from_secs_f32(1.0 / speed));
2921
2922 let new_particle_count = particles_per_length * heartbeats as usize;
2924 self.particles.reserve(new_particle_count);
2925
2926 for i in 0..particles_per_length {
2927 let angle = theta + dtheta * i as f32;
2928 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
2929 for j in 0..heartbeats {
2930 let dt = (j as f32 / heartbeats as f32) * dt;
2932 let distance = distance + speed * dt;
2933 let pos1 = pos + distance * direction - Vec3::unit_z();
2934 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
2935 let time = time + dt as f64;
2936
2937 self.particles.push(Particle::new_directed(
2938 Duration::from_secs_f32(0.5),
2939 time,
2940 ParticleMode::Poison,
2941 pos1,
2942 pos2,
2943 ));
2944 }
2945 }
2946 },
2947 FrontendSpecifier::AcidCloud => {
2948 let particles_per_height = 5;
2949 let particles_per_length = arc_length as usize;
2951 let dtheta = radians / particles_per_length as f32;
2952 let heartbeats = self
2955 .scheduler
2956 .heartbeats(Duration::from_secs_f32(1.0 / speed));
2957
2958 let new_particle_count =
2960 particles_per_length * heartbeats as usize * particles_per_height;
2961 self.particles.reserve(new_particle_count);
2962
2963 for i in 0..particles_per_height {
2964 let height = (i as f32 / (particles_per_height as f32 - 1.0)) * 4.0;
2965 for j in 0..particles_per_length {
2966 let angle = theta + dtheta * j as f32;
2967 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
2968 for k in 0..heartbeats {
2969 let dt = (k as f32 / heartbeats as f32) * dt;
2971 let distance = distance + speed * dt;
2972 let pos1 = pos + distance * direction - Vec3::unit_z()
2973 + Vec3::unit_z() * height;
2974 let pos2 = pos1 + direction;
2975 let time = time + dt as f64;
2976
2977 self.particles.push(Particle::new_directed(
2978 Duration::from_secs_f32(0.5),
2979 time,
2980 ParticleMode::Poison,
2981 pos1,
2982 pos2,
2983 ));
2984 }
2985 }
2986 }
2987 },
2988 FrontendSpecifier::Ink => {
2989 let particles_per_length = arc_length as usize;
2991 let dtheta = radians / particles_per_length as f32;
2992 let heartbeats = self
2995 .scheduler
2996 .heartbeats(Duration::from_secs_f32(1.0 / speed));
2997
2998 let new_particle_count = particles_per_length * heartbeats as usize;
3000 self.particles.reserve(new_particle_count);
3001
3002 for i in 0..particles_per_length {
3003 let angle = theta + dtheta * i as f32;
3004 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3005 for j in 0..heartbeats {
3006 let dt = (j as f32 / heartbeats as f32) * dt;
3008 let distance = distance + speed * dt;
3009 let pos1 = pos + distance * direction - Vec3::unit_z();
3010 let pos2 = pos1 + (Vec3::unit_z() + direction) * 3.0;
3011 let time = time + dt as f64;
3012
3013 self.particles.push(Particle::new_directed(
3014 Duration::from_secs_f32(0.5),
3015 time,
3016 ParticleMode::Ink,
3017 pos1,
3018 pos2,
3019 ));
3020 }
3021 }
3022 },
3023 FrontendSpecifier::IceSpikes | FrontendSpecifier::Ice => {
3024 let scale = 1.0 / 3.0;
3026 let scaled_distance = distance / scale;
3027 let scaled_speed = speed / scale;
3028
3029 let particles_per_length = (0.25 * arc_length / scale) as usize;
3031 let dtheta = radians / particles_per_length as f32;
3032 let heartbeats = self
3035 .scheduler
3036 .heartbeats(Duration::from_secs_f32(3.0 / scaled_speed));
3037
3038 let new_particle_count = particles_per_length * heartbeats as usize;
3040 self.particles.reserve(new_particle_count);
3041 let wave = if matches!(shockwave.properties.dodgeable, Dodgeable::Jump) {
3043 0.5
3044 } else {
3045 8.0
3046 };
3047 let height_scale = wave + 1.5 * percent;
3049 for i in 0..particles_per_length {
3050 let angle = theta + dtheta * i as f32;
3051 let direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
3052 for j in 0..heartbeats {
3053 let dt = (j as f32 / heartbeats as f32) * dt;
3055 let scaled_distance = scaled_distance + scaled_speed * dt;
3056 let mut pos1 = pos + (scaled_distance * direction).floor() * scale;
3057 let time = time + dt as f64;
3058
3059 let half_ray_length = 10.0;
3063 let mut last_air = false;
3064 let _ = terrain
3072 .ray(
3073 pos1 + Vec3::unit_z() * half_ray_length,
3074 pos1 - Vec3::unit_z() * half_ray_length,
3075 )
3076 .for_each(|block: &Block, pos: Vec3<i32>| {
3077 if block.is_solid() && block.get_sprite().is_none() {
3078 if last_air {
3079 pos1 = pos1.xy().with_z(pos.z as f32 + 1.0);
3080 last_air = false;
3081 }
3082 } else {
3083 last_air = true;
3084 }
3085 })
3086 .cast();
3087
3088 let get_positions = |a| {
3089 let pos1 = match a {
3090 2 => pos1 + Vec3::unit_x() * scale,
3091 3 => pos1 - Vec3::unit_x() * scale,
3092 4 => pos1 + Vec3::unit_y() * scale,
3093 5 => pos1 - Vec3::unit_y() * scale,
3094 _ => pos1,
3095 };
3096 let pos2 = if a == 1 {
3097 pos1 + Vec3::unit_z() * 5.0 * height_scale
3098 } else {
3099 pos1 + Vec3::unit_z() * 1.0 * height_scale
3100 };
3101 (pos1, pos2)
3102 };
3103
3104 for a in 1..=5 {
3105 let (pos1, pos2) = get_positions(a);
3106 self.particles.push(Particle::new_directed(
3107 Duration::from_secs_f32(0.5),
3108 time,
3109 ParticleMode::IceSpikes,
3110 pos1,
3111 pos2,
3112 ));
3113 }
3114 }
3115 }
3116 },
3117 }
3118 }
3119 }
3120
3121 fn upload_particles(&mut self, renderer: &mut Renderer) {
3122 prof_span!("ParticleMgr::upload_particles");
3123 let all_cpu_instances = self
3124 .particles
3125 .iter()
3126 .map(|p| p.instance)
3127 .collect::<Vec<ParticleInstance>>();
3128
3129 let gpu_instances = renderer.create_instances(&all_cpu_instances);
3131
3132 self.instances = gpu_instances;
3133 }
3134
3135 pub fn render<'a>(&'a self, drawer: &mut ParticleDrawer<'_, 'a>, scene_data: &SceneData) {
3136 prof_span!("ParticleMgr::render");
3137 if scene_data.particles_enabled {
3138 let model = &self
3139 .model_cache
3140 .get(DEFAULT_MODEL_KEY)
3141 .expect("Expected particle model in cache");
3142
3143 drawer.draw(model, &self.instances);
3144 }
3145 }
3146
3147 pub fn particle_count(&self) -> usize { self.instances.count() }
3148
3149 pub fn particle_count_visible(&self) -> usize { self.instances.count() }
3150}
3151
3152fn default_instances(renderer: &mut Renderer) -> Instances<ParticleInstance> {
3153 let empty_vec = Vec::new();
3154
3155 renderer.create_instances(&empty_vec)
3156}
3157
3158const DEFAULT_MODEL_KEY: &str = "voxygen.voxel.particle";
3159
3160fn default_cache(renderer: &mut Renderer) -> HashMap<&'static str, Model<ParticleVertex>> {
3161 let mut model_cache = HashMap::new();
3162
3163 model_cache.entry(DEFAULT_MODEL_KEY).or_insert_with(|| {
3164 let vox = DotVoxAsset::load_expect(DEFAULT_MODEL_KEY);
3165
3166 let max_texture_size = renderer.max_texture_size();
3169 let max_size = Vec2::from(u16::try_from(max_texture_size).unwrap_or(u16::MAX));
3170 let mut greedy = GreedyMesh::new(max_size, crate::mesh::greedy::general_config());
3171
3172 let segment = Segment::from_vox_model_index(&vox.read().0, 0);
3173 let segment_size = segment.size();
3174 let mut mesh = generate_mesh_base_vol_particle(segment, &mut greedy).0;
3175 for vert in mesh.vertices_mut() {
3177 vert.pos[0] -= segment_size.x as f32 / 2.0;
3178 vert.pos[1] -= segment_size.y as f32 / 2.0;
3179 vert.pos[2] -= segment_size.z as f32 / 2.0;
3180 }
3181
3182 drop(greedy);
3184
3185 renderer
3186 .create_model(&mesh)
3187 .expect("Failed to create particle model")
3188 });
3189
3190 model_cache
3191}
3192
3193struct HeartbeatScheduler {
3195 timers: HashMap<Duration, (f64, u8)>,
3203
3204 last_known_time: f64,
3205}
3206
3207impl HeartbeatScheduler {
3208 pub fn new() -> Self {
3209 HeartbeatScheduler {
3210 timers: HashMap::new(),
3211 last_known_time: 0.0,
3212 }
3213 }
3214
3215 pub fn maintain(&mut self, now: f64) {
3218 prof_span!("HeartbeatScheduler::maintain");
3219 self.last_known_time = now;
3220
3221 for (frequency, (last_update, heartbeats)) in self.timers.iter_mut() {
3222 let total_heartbeats = (now - *last_update) / frequency.as_secs_f64();
3224
3225 let full_heartbeats = total_heartbeats.floor();
3227
3228 *heartbeats = full_heartbeats as u8;
3229
3230 let partial_heartbeat = total_heartbeats - full_heartbeats;
3232
3233 let partial_heartbeat_as_time = frequency.mul_f64(partial_heartbeat).as_secs_f64();
3235
3236 *last_update = now - partial_heartbeat_as_time;
3240 }
3241 }
3242
3243 pub fn heartbeats(&mut self, frequency: Duration) -> u8 {
3250 prof_span!("HeartbeatScheduler::heartbeats");
3251 let last_known_time = self.last_known_time;
3252
3253 self.timers
3254 .entry(frequency)
3255 .or_insert_with(|| (last_known_time, 0))
3256 .1
3257 }
3258
3259 pub fn clear(&mut self) { self.timers.clear() }
3260}
3261
3262#[derive(Clone, Copy)]
3263struct Particle {
3264 alive_until: f64, instance: ParticleInstance,
3266}
3267
3268impl Particle {
3269 fn new(lifespan: Duration, time: f64, mode: ParticleMode, pos: Vec3<f32>) -> Self {
3270 Particle {
3271 alive_until: time + lifespan.as_secs_f64(),
3272 instance: ParticleInstance::new(time, lifespan.as_secs_f32(), mode, pos),
3273 }
3274 }
3275
3276 fn new_directed(
3277 lifespan: Duration,
3278 time: f64,
3279 mode: ParticleMode,
3280 pos1: Vec3<f32>,
3281 pos2: Vec3<f32>,
3282 ) -> Self {
3283 Particle {
3284 alive_until: time + lifespan.as_secs_f64(),
3285 instance: ParticleInstance::new_directed(
3286 time,
3287 lifespan.as_secs_f32(),
3288 mode,
3289 pos1,
3290 pos2,
3291 ),
3292 }
3293 }
3294
3295 fn new_directed_with_collision(
3296 lifespan: Duration,
3297 time: f64,
3298 mode: ParticleMode,
3299 pos1: Vec3<f32>,
3300 pos2: Vec3<f32>,
3301 distance: impl Fn(Vec3<f32>, Vec3<f32>) -> f32,
3302 ) -> Self {
3303 let dir = pos2 - pos1;
3304 let end_distance = pos1.distance(pos2);
3305 let (end_pos, lifespawn) = if end_distance > 0.1 {
3306 let ratio = distance(pos1, pos2) / end_distance;
3307 (pos1 + ratio * dir, lifespan.mul_f32(ratio))
3308 } else {
3309 (pos2, lifespan)
3310 };
3311
3312 Self::new_directed(lifespawn, time, mode, pos1, end_pos)
3313 }
3314}