mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
288a6f3a82
Added temp sfx for blocking and parrying. Added temp particles for successful parry. Tweaked values of default block ability.
1255 lines
50 KiB
Rust
1255 lines
50 KiB
Rust
use super::{terrain::BlocksOfInterest, SceneData, Terrain};
|
|
use crate::{
|
|
mesh::{greedy::GreedyMesh, Meshable},
|
|
render::{
|
|
pipelines::particle::ParticleMode, GlobalModel, Instances, Light, LodData, Model,
|
|
ParticleInstance, ParticlePipeline, Renderer,
|
|
},
|
|
};
|
|
use common::{
|
|
assets::{AssetExt, DotVoxAsset},
|
|
comp::{
|
|
self, aura, beam, body, buff, item::Reagent, object, BeamSegment, Body, CharacterState,
|
|
Ori, Pos, Shockwave, Vel,
|
|
},
|
|
figure::Segment,
|
|
outcome::Outcome,
|
|
resources::DeltaTime,
|
|
spiral::Spiral2d,
|
|
states::{self, utils::StageSection},
|
|
terrain::TerrainChunk,
|
|
vol::{RectRasterableVol, SizedVol},
|
|
};
|
|
use common_base::span;
|
|
use hashbrown::HashMap;
|
|
use rand::prelude::*;
|
|
use specs::{Join, WorldExt};
|
|
use std::{f32::consts::PI, time::Duration};
|
|
use vek::*;
|
|
|
|
pub struct ParticleMgr {
|
|
/// keep track of lifespans
|
|
particles: Vec<Particle>,
|
|
|
|
/// keep track of timings
|
|
scheduler: HeartbeatScheduler,
|
|
|
|
/// GPU Instance Buffer
|
|
instances: Instances<ParticleInstance>,
|
|
|
|
/// GPU Vertex Buffers
|
|
model_cache: HashMap<&'static str, Model<ParticlePipeline>>,
|
|
}
|
|
|
|
impl ParticleMgr {
|
|
pub fn new(renderer: &mut Renderer) -> Self {
|
|
Self {
|
|
particles: Vec::new(),
|
|
scheduler: HeartbeatScheduler::new(),
|
|
instances: default_instances(renderer),
|
|
model_cache: default_cache(renderer),
|
|
}
|
|
}
|
|
|
|
pub fn handle_outcome(&mut self, outcome: &Outcome, scene_data: &SceneData) {
|
|
span!(_guard, "handle_outcome", "ParticleMgr::handle_outcome");
|
|
let time = scene_data.state.get_time();
|
|
let mut rng = rand::thread_rng();
|
|
|
|
match outcome {
|
|
Outcome::Explosion {
|
|
pos,
|
|
power,
|
|
radius,
|
|
is_attack,
|
|
reagent,
|
|
} => {
|
|
if *is_attack {
|
|
match reagent {
|
|
Some(Reagent::Green) => {
|
|
self.particles.resize_with(
|
|
self.particles.len() + (60.0 * power.abs()) as usize,
|
|
|| {
|
|
Particle::new_directed(
|
|
Duration::from_secs_f32(rng.gen_range(0.2..3.0)),
|
|
time,
|
|
ParticleMode::EnergyNature,
|
|
*pos,
|
|
*pos + Vec3::<f32>::zero()
|
|
.map(|_| rng.gen_range(-1.0..1.0))
|
|
.normalized()
|
|
* rng.gen_range(1.0..*radius),
|
|
)
|
|
},
|
|
);
|
|
},
|
|
Some(Reagent::Red) => {
|
|
self.particles.resize_with(
|
|
self.particles.len() + (75.0 * power.abs()) as usize,
|
|
|| {
|
|
Particle::new_directed(
|
|
Duration::from_millis(500),
|
|
time,
|
|
ParticleMode::Explosion,
|
|
*pos,
|
|
*pos + Vec3::<f32>::zero()
|
|
.map(|_| rng.gen_range(-1.0..1.0))
|
|
.normalized()
|
|
* *radius,
|
|
)
|
|
},
|
|
);
|
|
},
|
|
Some(Reagent::Blue) => {
|
|
self.particles.resize_with(
|
|
self.particles.len() + (75.0 * power.abs()) as usize,
|
|
|| {
|
|
Particle::new_directed(
|
|
Duration::from_millis(500),
|
|
time,
|
|
ParticleMode::Ice,
|
|
*pos,
|
|
*pos + Vec3::<f32>::zero()
|
|
.map(|_| rng.gen_range(-1.0..1.0))
|
|
.normalized()
|
|
* *radius,
|
|
)
|
|
},
|
|
);
|
|
},
|
|
_ => {},
|
|
}
|
|
} else {
|
|
self.particles.resize_with(
|
|
self.particles.len() + if reagent.is_some() { 300 } else { 150 },
|
|
|| {
|
|
Particle::new(
|
|
Duration::from_millis(if reagent.is_some() { 1000 } else { 250 }),
|
|
time,
|
|
match reagent {
|
|
Some(Reagent::Blue) => ParticleMode::FireworkBlue,
|
|
Some(Reagent::Green) => ParticleMode::FireworkGreen,
|
|
Some(Reagent::Purple) => ParticleMode::FireworkPurple,
|
|
Some(Reagent::Red) => ParticleMode::FireworkRed,
|
|
Some(Reagent::White) => ParticleMode::FireworkWhite,
|
|
Some(Reagent::Yellow) => ParticleMode::FireworkYellow,
|
|
None => ParticleMode::Shrapnel,
|
|
},
|
|
*pos,
|
|
)
|
|
},
|
|
);
|
|
|
|
self.particles.resize_with(
|
|
self.particles.len() + if reagent.is_some() { 100 } else { 200 },
|
|
|| {
|
|
Particle::new(
|
|
Duration::from_secs(4),
|
|
time,
|
|
ParticleMode::CampfireSmoke,
|
|
*pos + Vec3::<f32>::zero()
|
|
.map(|_| rng.gen_range(-1.0..1.0))
|
|
.normalized()
|
|
* *radius,
|
|
)
|
|
},
|
|
);
|
|
}
|
|
},
|
|
Outcome::BreakBlock { pos, .. } => {
|
|
// TODO: Use color field when particle colors are a thing
|
|
self.particles.resize_with(self.particles.len() + 30, || {
|
|
Particle::new(
|
|
Duration::from_millis(100),
|
|
time,
|
|
ParticleMode::Shrapnel,
|
|
pos.map(|e| e as f32 + 0.5),
|
|
)
|
|
});
|
|
},
|
|
Outcome::SummonedCreature { pos, body } => match body {
|
|
Body::BipedSmall(b) if matches!(b.species, body::biped_small::Species::Husk) => {
|
|
self.particles.resize_with(
|
|
self.particles.len()
|
|
+ 2 * usize::from(self.scheduler.heartbeats(Duration::from_millis(1))),
|
|
|| {
|
|
let start_pos = pos + Vec3::unit_z() * body.height() / 2.0;
|
|
let end_pos = pos
|
|
+ Vec3::new(
|
|
2.0 * rng.gen::<f32>() - 1.0,
|
|
2.0 * rng.gen::<f32>() - 1.0,
|
|
0.0,
|
|
)
|
|
.normalized()
|
|
* (body.radius() + 4.0)
|
|
+ Vec3::unit_z() * (body.height() + 2.0) * rng.gen::<f32>();
|
|
|
|
Particle::new_directed(
|
|
Duration::from_secs_f32(0.5),
|
|
time,
|
|
ParticleMode::CultistFlame,
|
|
start_pos,
|
|
end_pos,
|
|
)
|
|
},
|
|
);
|
|
},
|
|
_ => {},
|
|
},
|
|
Outcome::ProjectileHit { pos, target, .. } => {
|
|
if target.is_some() {
|
|
self.particles.resize_with(self.particles.len() + 30, || {
|
|
Particle::new(Duration::from_millis(250), time, ParticleMode::Blood, *pos)
|
|
});
|
|
}
|
|
},
|
|
Outcome::Block { pos, parry } => {
|
|
if *parry {
|
|
self.particles.resize_with(self.particles.len() + 20, || {
|
|
Particle::new(
|
|
Duration::from_millis(200),
|
|
time,
|
|
ParticleMode::GunPowderSpark,
|
|
*pos + Vec3::unit_z(),
|
|
)
|
|
});
|
|
}
|
|
},
|
|
Outcome::ProjectileShot { .. }
|
|
| Outcome::Beam { .. }
|
|
| Outcome::ExpChange { .. }
|
|
| Outcome::SkillPointGain { .. }
|
|
| Outcome::ComboChange { .. } => {},
|
|
Outcome::Damage { .. } => {},
|
|
}
|
|
}
|
|
|
|
pub fn maintain(
|
|
&mut self,
|
|
renderer: &mut Renderer,
|
|
scene_data: &SceneData,
|
|
terrain: &Terrain<TerrainChunk>,
|
|
lights: &mut Vec<Light>,
|
|
) {
|
|
span!(_guard, "maintain", "ParticleMgr::maintain");
|
|
if scene_data.particles_enabled {
|
|
// update timings
|
|
self.scheduler.maintain(scene_data.state.get_time());
|
|
|
|
// remove dead Particle
|
|
self.particles
|
|
.retain(|p| p.alive_until > scene_data.state.get_time());
|
|
|
|
// add new Particle
|
|
self.maintain_body_particles(scene_data);
|
|
self.maintain_char_state_particles(scene_data);
|
|
self.maintain_beam_particles(scene_data, lights);
|
|
self.maintain_block_particles(scene_data, terrain);
|
|
self.maintain_shockwave_particles(scene_data);
|
|
self.maintain_aura_particles(scene_data);
|
|
self.maintain_buff_particles(scene_data);
|
|
} else {
|
|
// remove all particle lifespans
|
|
self.particles.clear();
|
|
|
|
// remove all timings
|
|
self.scheduler.clear();
|
|
}
|
|
|
|
self.upload_particles(renderer);
|
|
}
|
|
|
|
fn maintain_body_particles(&mut self, scene_data: &SceneData) {
|
|
span!(
|
|
_guard,
|
|
"body_particles",
|
|
"ParticleMgr::maintain_body_particles"
|
|
);
|
|
let ecs = scene_data.state.ecs();
|
|
for (body, pos, vel) in (
|
|
&ecs.read_storage::<Body>(),
|
|
&ecs.read_storage::<Pos>(),
|
|
ecs.read_storage::<Vel>().maybe(),
|
|
)
|
|
.join()
|
|
{
|
|
match body {
|
|
Body::Object(object::Body::CampfireLit) => {
|
|
self.maintain_campfirelit_particles(scene_data, pos, vel)
|
|
},
|
|
Body::Object(
|
|
object::Body::Arrow
|
|
| object::Body::MultiArrow
|
|
| object::Body::ArrowSnake
|
|
| object::Body::ArrowTurret,
|
|
) => self.maintain_arrow_particles(scene_data, pos, vel),
|
|
Body::Object(object::Body::BoltFire) => {
|
|
self.maintain_boltfire_particles(scene_data, pos, vel)
|
|
},
|
|
Body::Object(object::Body::BoltFireBig) => {
|
|
self.maintain_boltfirebig_particles(scene_data, pos, vel)
|
|
},
|
|
Body::Object(object::Body::BoltNature) => {
|
|
self.maintain_boltnature_particles(scene_data, pos, vel)
|
|
},
|
|
Body::Object(
|
|
object::Body::Bomb
|
|
| object::Body::FireworkBlue
|
|
| object::Body::FireworkGreen
|
|
| object::Body::FireworkPurple
|
|
| object::Body::FireworkRed
|
|
| object::Body::FireworkWhite
|
|
| object::Body::FireworkYellow,
|
|
) => self.maintain_bomb_particles(scene_data, pos, vel),
|
|
_ => {},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn maintain_campfirelit_particles(
|
|
&mut self,
|
|
scene_data: &SceneData,
|
|
pos: &Pos,
|
|
vel: Option<&Vel>,
|
|
) {
|
|
span!(
|
|
_guard,
|
|
"campfirelit_particles",
|
|
"ParticleMgr::maintain_campfirelit_particles"
|
|
);
|
|
let time = scene_data.state.get_time();
|
|
let dt = scene_data.state.get_delta_time();
|
|
let mut rng = thread_rng();
|
|
|
|
for _ in 0..self.scheduler.heartbeats(Duration::from_millis(50)) {
|
|
self.particles.push(Particle::new(
|
|
Duration::from_millis(250),
|
|
time,
|
|
ParticleMode::CampfireFire,
|
|
pos.0,
|
|
));
|
|
|
|
self.particles.push(Particle::new(
|
|
Duration::from_secs(10),
|
|
time,
|
|
ParticleMode::CampfireSmoke,
|
|
pos.0.map(|e| e + thread_rng().gen_range(-0.25..0.25))
|
|
+ vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
|
|
));
|
|
}
|
|
}
|
|
|
|
fn maintain_arrow_particles(&mut self, scene_data: &SceneData, pos: &Pos, vel: Option<&Vel>) {
|
|
const MIN_SPEED: f32 = 15.0;
|
|
// Don't emit particles for immobile arrows
|
|
if vel.map_or(true, |v| v.0.magnitude_squared() < MIN_SPEED.powi(2)) {
|
|
return;
|
|
}
|
|
|
|
span!(
|
|
_guard,
|
|
"arrow_particles",
|
|
"ParticleMgr::maintain_arrow_particles"
|
|
);
|
|
let time = scene_data.state.get_time();
|
|
let dt = scene_data.state.get_delta_time();
|
|
|
|
let count = self.scheduler.heartbeats(Duration::from_millis(2));
|
|
for i in 0..count {
|
|
let proportion = i as f32 / count as f32;
|
|
self.particles.push(Particle::new(
|
|
Duration::from_millis(200),
|
|
time,
|
|
ParticleMode::StaticSmoke,
|
|
pos.0 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * proportion),
|
|
));
|
|
}
|
|
}
|
|
|
|
fn maintain_boltfire_particles(
|
|
&mut self,
|
|
scene_data: &SceneData,
|
|
pos: &Pos,
|
|
vel: Option<&Vel>,
|
|
) {
|
|
span!(
|
|
_guard,
|
|
"boltfire_particles",
|
|
"ParticleMgr::maintain_boltfire_particles"
|
|
);
|
|
let time = scene_data.state.get_time();
|
|
let dt = scene_data.state.get_delta_time();
|
|
let mut rng = thread_rng();
|
|
|
|
for _ in 0..self.scheduler.heartbeats(Duration::from_millis(4)) {
|
|
self.particles.push(Particle::new(
|
|
Duration::from_millis(500),
|
|
time,
|
|
ParticleMode::CampfireFire,
|
|
pos.0,
|
|
));
|
|
self.particles.push(Particle::new(
|
|
Duration::from_secs(1),
|
|
time,
|
|
ParticleMode::CampfireSmoke,
|
|
pos.0.map(|e| e + rng.gen_range(-0.25..0.25))
|
|
+ vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
|
|
));
|
|
}
|
|
}
|
|
|
|
fn maintain_boltfirebig_particles(
|
|
&mut self,
|
|
scene_data: &SceneData,
|
|
pos: &Pos,
|
|
vel: Option<&Vel>,
|
|
) {
|
|
span!(
|
|
_guard,
|
|
"boltfirebig_particles",
|
|
"ParticleMgr::maintain_boltfirebig_particles"
|
|
);
|
|
let time = scene_data.state.get_time();
|
|
let dt = scene_data.state.get_delta_time();
|
|
let mut rng = thread_rng();
|
|
|
|
// fire
|
|
self.particles.resize_with(
|
|
self.particles.len() + usize::from(self.scheduler.heartbeats(Duration::from_millis(2))),
|
|
|| {
|
|
Particle::new(
|
|
Duration::from_millis(500),
|
|
time,
|
|
ParticleMode::CampfireFire,
|
|
pos.0.map(|e| e + rng.gen_range(-0.25..0.25))
|
|
+ vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
|
|
)
|
|
},
|
|
);
|
|
|
|
// smoke
|
|
self.particles.resize_with(
|
|
self.particles.len() + usize::from(self.scheduler.heartbeats(Duration::from_millis(5))),
|
|
|| {
|
|
Particle::new(
|
|
Duration::from_secs(2),
|
|
time,
|
|
ParticleMode::CampfireSmoke,
|
|
pos.0.map(|e| e + rng.gen_range(-0.25..0.25))
|
|
+ vel.map_or(Vec3::zero(), |v| -v.0 * dt),
|
|
)
|
|
},
|
|
);
|
|
}
|
|
|
|
fn maintain_boltnature_particles(
|
|
&mut self,
|
|
scene_data: &SceneData,
|
|
pos: &Pos,
|
|
vel: Option<&Vel>,
|
|
) {
|
|
let time = scene_data.state.get_time();
|
|
let dt = scene_data.state.get_delta_time();
|
|
let mut rng = thread_rng();
|
|
|
|
// nature
|
|
self.particles.resize_with(
|
|
self.particles.len() + usize::from(self.scheduler.heartbeats(Duration::from_millis(2))),
|
|
|| {
|
|
Particle::new(
|
|
Duration::from_millis(500),
|
|
time,
|
|
ParticleMode::CampfireSmoke,
|
|
pos.0.map(|e| e + rng.gen_range(-0.25..0.25))
|
|
+ vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
|
|
)
|
|
},
|
|
);
|
|
}
|
|
|
|
fn maintain_bomb_particles(&mut self, scene_data: &SceneData, pos: &Pos, vel: Option<&Vel>) {
|
|
span!(
|
|
_guard,
|
|
"bomb_particles",
|
|
"ParticleMgr::maintain_bomb_particles"
|
|
);
|
|
let time = scene_data.state.get_time();
|
|
let dt = scene_data.state.get_delta_time();
|
|
let mut rng = thread_rng();
|
|
|
|
for _ in 0..self.scheduler.heartbeats(Duration::from_millis(10)) {
|
|
// sparks
|
|
self.particles.push(Particle::new(
|
|
Duration::from_millis(1500),
|
|
time,
|
|
ParticleMode::GunPowderSpark,
|
|
pos.0,
|
|
));
|
|
|
|
// smoke
|
|
self.particles.push(Particle::new(
|
|
Duration::from_secs(2),
|
|
time,
|
|
ParticleMode::CampfireSmoke,
|
|
pos.0 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
|
|
));
|
|
}
|
|
}
|
|
|
|
fn maintain_char_state_particles(&mut self, scene_data: &SceneData) {
|
|
span!(
|
|
_guard,
|
|
"char_state_particles",
|
|
"ParticleMgr::maintain_char_state_particles"
|
|
);
|
|
let state = scene_data.state;
|
|
let ecs = state.ecs();
|
|
let time = state.get_time();
|
|
let dt = scene_data.state.get_delta_time();
|
|
let mut rng = thread_rng();
|
|
|
|
for (entity, pos, vel, character_state, body) in (
|
|
&ecs.entities(),
|
|
&ecs.read_storage::<Pos>(),
|
|
ecs.read_storage::<Vel>().maybe(),
|
|
&ecs.read_storage::<CharacterState>(),
|
|
&ecs.read_storage::<Body>(),
|
|
)
|
|
.join()
|
|
{
|
|
match character_state {
|
|
CharacterState::Boost(_) => {
|
|
self.particles.resize_with(
|
|
self.particles.len()
|
|
+ usize::from(self.scheduler.heartbeats(Duration::from_millis(10))),
|
|
|| {
|
|
Particle::new(
|
|
Duration::from_secs(15),
|
|
time,
|
|
ParticleMode::CampfireSmoke,
|
|
pos.0 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * rng.gen::<f32>()),
|
|
)
|
|
},
|
|
);
|
|
},
|
|
CharacterState::SpinMelee(spin) => {
|
|
if let Some(specifier) = spin.static_data.specifier {
|
|
match specifier {
|
|
states::spin_melee::FrontendSpecifier::CultistVortex => {
|
|
if matches!(spin.stage_section, StageSection::Swing) {
|
|
let range = spin.static_data.range;
|
|
// Particles for vortex
|
|
let heartbeats =
|
|
self.scheduler.heartbeats(Duration::from_millis(3));
|
|
self.particles.resize_with(
|
|
self.particles.len()
|
|
+ range.powi(2) as usize * usize::from(heartbeats)
|
|
/ 150,
|
|
|| {
|
|
let rand_dist =
|
|
range * (1.0 - rng.gen::<f32>().powi(10));
|
|
let init_pos = Vec3::new(
|
|
2.0 * rng.gen::<f32>() - 1.0,
|
|
2.0 * rng.gen::<f32>() - 1.0,
|
|
0.0,
|
|
)
|
|
.normalized()
|
|
* rand_dist
|
|
+ pos.0
|
|
+ Vec3::unit_z() * 0.05;
|
|
Particle::new_directed(
|
|
Duration::from_millis(900),
|
|
time,
|
|
ParticleMode::CultistFlame,
|
|
init_pos,
|
|
pos.0,
|
|
)
|
|
},
|
|
);
|
|
// Particles for lifesteal effect
|
|
for (_entity_b, pos_b, body_b, _health_b) in (
|
|
&ecs.entities(),
|
|
&ecs.read_storage::<Pos>(),
|
|
&ecs.read_storage::<Body>(),
|
|
&ecs.read_storage::<comp::Health>(),
|
|
)
|
|
.join()
|
|
.filter(|(e, _, _, h)| !h.is_dead && entity != *e)
|
|
{
|
|
if pos.0.distance_squared(pos_b.0) < range.powi(2) {
|
|
let heartbeats = self
|
|
.scheduler
|
|
.heartbeats(Duration::from_millis(20));
|
|
self.particles.resize_with(
|
|
self.particles.len()
|
|
+ range.powi(2) as usize
|
|
* usize::from(heartbeats)
|
|
/ 150,
|
|
|| {
|
|
let start_pos = pos_b.0
|
|
+ Vec3::unit_z() * body_b.height() * 0.5
|
|
+ Vec3::<f32>::zero()
|
|
.map(|_| rng.gen_range(-1.0..1.0))
|
|
.normalized()
|
|
* 1.0;
|
|
Particle::new_directed(
|
|
Duration::from_millis(900),
|
|
time,
|
|
ParticleMode::CultistFlame,
|
|
start_pos,
|
|
pos.0
|
|
+ Vec3::unit_z() * body.height() * 0.5,
|
|
)
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
},
|
|
CharacterState::Blink(c) => {
|
|
self.particles.resize_with(
|
|
self.particles.len()
|
|
+ usize::from(self.scheduler.heartbeats(Duration::from_millis(10))),
|
|
|| {
|
|
let center_pos = pos.0 + Vec3::unit_z() * body.height() / 2.0;
|
|
let outer_pos = pos.0
|
|
+ Vec3::new(
|
|
2.0 * rng.gen::<f32>() - 1.0,
|
|
2.0 * rng.gen::<f32>() - 1.0,
|
|
0.0,
|
|
)
|
|
.normalized()
|
|
* (body.radius() + 2.0)
|
|
+ Vec3::unit_z() * body.height() * rng.gen::<f32>();
|
|
|
|
let (start_pos, end_pos) =
|
|
if matches!(c.stage_section, StageSection::Buildup) {
|
|
(outer_pos, center_pos)
|
|
} else {
|
|
(center_pos, outer_pos)
|
|
};
|
|
|
|
Particle::new_directed(
|
|
Duration::from_secs_f32(0.5),
|
|
time,
|
|
ParticleMode::CultistFlame,
|
|
start_pos,
|
|
end_pos,
|
|
)
|
|
},
|
|
);
|
|
},
|
|
_ => {},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn maintain_beam_particles(&mut self, scene_data: &SceneData, lights: &mut Vec<Light>) {
|
|
let state = scene_data.state;
|
|
let ecs = state.ecs();
|
|
let time = state.get_time();
|
|
let dt = scene_data.state.ecs().fetch::<DeltaTime>().0;
|
|
|
|
for (pos, ori, beam) in (
|
|
&ecs.read_storage::<Pos>(),
|
|
&ecs.read_storage::<Ori>(),
|
|
&ecs.read_storage::<BeamSegment>(),
|
|
)
|
|
.join()
|
|
.filter(|(_, _, b)| b.creation.map_or(true, |c| (c + dt as f64) >= time))
|
|
{
|
|
// TODO: Handle this less hackily. Done this way as beam segments are created
|
|
// every server tick, which is approximately 33 ms. Heartbeat scheduler used to
|
|
// account for clients with less than 30 fps because they start the creation
|
|
// time when the segments are received and could receive 2 at once
|
|
let beam_tick_count = 33.max(self.scheduler.heartbeats(Duration::from_millis(1)));
|
|
let range = beam.properties.speed * beam.properties.duration.as_secs_f32();
|
|
match beam.properties.specifier {
|
|
beam::FrontendSpecifier::Flamethrower => {
|
|
let mut rng = thread_rng();
|
|
let (from, to) = (Vec3::<f32>::unit_z(), *ori.look_dir());
|
|
let m = Mat3::<f32>::rotation_from_to_3d(from, to);
|
|
// Emit a light when using flames
|
|
lights.push(Light::new(
|
|
pos.0,
|
|
Rgb::new(1.0, 0.25, 0.05).map(|e| e * rng.gen_range(0.8..1.2)),
|
|
2.0,
|
|
));
|
|
self.particles.resize_with(
|
|
self.particles.len() + usize::from(beam_tick_count) / 2,
|
|
|| {
|
|
let phi: f32 = rng.gen_range(0.0..beam.properties.angle);
|
|
let theta: f32 = rng.gen_range(0.0..2.0 * PI);
|
|
let offset_z = Vec3::new(
|
|
phi.sin() * theta.cos(),
|
|
phi.sin() * theta.sin(),
|
|
phi.cos(),
|
|
);
|
|
let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
|
|
Particle::new_directed(
|
|
beam.properties.duration,
|
|
time,
|
|
ParticleMode::FlameThrower,
|
|
pos.0,
|
|
pos.0 + random_ori * range,
|
|
)
|
|
},
|
|
);
|
|
},
|
|
beam::FrontendSpecifier::Cultist => {
|
|
let mut rng = thread_rng();
|
|
let (from, to) = (Vec3::<f32>::unit_z(), *ori.look_dir());
|
|
let m = Mat3::<f32>::rotation_from_to_3d(from, to);
|
|
// Emit a light when using flames
|
|
lights.push(Light::new(
|
|
pos.0,
|
|
Rgb::new(1.0, 0.0, 1.0).map(|e| e * rng.gen_range(0.5..1.0)),
|
|
2.0,
|
|
));
|
|
self.particles.resize_with(
|
|
self.particles.len() + usize::from(beam_tick_count) / 2,
|
|
|| {
|
|
let phi: f32 = rng.gen_range(0.0..beam.properties.angle);
|
|
let theta: f32 = rng.gen_range(0.0..2.0 * PI);
|
|
let offset_z = Vec3::new(
|
|
phi.sin() * theta.cos(),
|
|
phi.sin() * theta.sin(),
|
|
phi.cos(),
|
|
);
|
|
let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
|
|
Particle::new_directed(
|
|
beam.properties.duration,
|
|
time,
|
|
ParticleMode::CultistFlame,
|
|
pos.0,
|
|
pos.0 + random_ori * range,
|
|
)
|
|
},
|
|
);
|
|
},
|
|
beam::FrontendSpecifier::HealingBeam => {
|
|
// Emit a light when using healing
|
|
lights.push(Light::new(pos.0, Rgb::new(0.1, 1.0, 0.15), 1.0));
|
|
for i in 0..beam_tick_count {
|
|
self.particles.push(Particle::new_directed(
|
|
beam.properties.duration,
|
|
time + i as f64 / 1000.0,
|
|
ParticleMode::HealingBeam,
|
|
pos.0,
|
|
pos.0 + *ori.look_dir() * range,
|
|
));
|
|
}
|
|
},
|
|
beam::FrontendSpecifier::LifestealBeam => {
|
|
// Emit a light when using lifesteal beam
|
|
lights.push(Light::new(pos.0, Rgb::new(0.8, 1.0, 0.5), 1.0));
|
|
for i in 0..beam_tick_count {
|
|
self.particles.push(Particle::new_directed(
|
|
beam.properties.duration,
|
|
time + i as f64 / 1000.0,
|
|
ParticleMode::LifestealBeam,
|
|
pos.0,
|
|
pos.0 + *ori.look_dir() * range,
|
|
));
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn maintain_aura_particles(&mut self, scene_data: &SceneData) {
|
|
let state = scene_data.state;
|
|
let ecs = state.ecs();
|
|
let time = state.get_time();
|
|
let mut rng = thread_rng();
|
|
|
|
for (pos, auras) in (
|
|
&ecs.read_storage::<Pos>(),
|
|
&ecs.read_storage::<comp::Auras>(),
|
|
)
|
|
.join()
|
|
{
|
|
for (_, aura) in auras.auras.iter() {
|
|
#[allow(clippy::single_match)]
|
|
match aura.aura_kind {
|
|
aura::AuraKind::Buff {
|
|
kind: buff::BuffKind::ProtectingWard,
|
|
..
|
|
} => {
|
|
let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
|
|
self.particles.resize_with(
|
|
self.particles.len()
|
|
+ aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
|
|
|| {
|
|
let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100));
|
|
let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
|
|
let max_dur = Duration::from_secs(1);
|
|
Particle::new_directed(
|
|
aura.duration.map_or(max_dur, |dur| dur.min(max_dur)),
|
|
time,
|
|
ParticleMode::EnergyNature,
|
|
pos.0,
|
|
pos.0 + init_pos,
|
|
)
|
|
},
|
|
);
|
|
},
|
|
_ => {},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn maintain_buff_particles(&mut self, scene_data: &SceneData) {
|
|
let state = scene_data.state;
|
|
let ecs = state.ecs();
|
|
let time = state.get_time();
|
|
let mut rng = rand::thread_rng();
|
|
|
|
for (pos, buffs, body) in (
|
|
&ecs.read_storage::<Pos>(),
|
|
&ecs.read_storage::<comp::Buffs>(),
|
|
&ecs.read_storage::<comp::Body>(),
|
|
)
|
|
.join()
|
|
{
|
|
for (buff_kind, _) in buffs.kinds.iter() {
|
|
#[allow(clippy::single_match)]
|
|
match buff_kind {
|
|
buff::BuffKind::Cursed | buff::BuffKind::Burning => {
|
|
self.particles.resize_with(
|
|
self.particles.len()
|
|
+ usize::from(self.scheduler.heartbeats(Duration::from_millis(15))),
|
|
|| {
|
|
let start_pos = pos.0
|
|
+ Vec3::unit_z() * body.height() * 0.25
|
|
+ Vec3::<f32>::zero()
|
|
.map(|_| rng.gen_range(-1.0..1.0))
|
|
.normalized()
|
|
* 0.25;
|
|
let end_pos = start_pos
|
|
+ Vec3::unit_z() * body.height()
|
|
+ Vec3::<f32>::zero()
|
|
.map(|_| rng.gen_range(-1.0..1.0))
|
|
.normalized();
|
|
Particle::new_directed(
|
|
Duration::from_secs(1),
|
|
time,
|
|
if matches!(buff_kind, buff::BuffKind::Cursed) {
|
|
ParticleMode::CultistFlame
|
|
} else {
|
|
ParticleMode::FlameThrower
|
|
},
|
|
start_pos,
|
|
end_pos,
|
|
)
|
|
},
|
|
);
|
|
},
|
|
_ => {},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::same_item_push)] // TODO: Pending review in #587
|
|
fn maintain_block_particles(
|
|
&mut self,
|
|
scene_data: &SceneData,
|
|
terrain: &Terrain<TerrainChunk>,
|
|
) {
|
|
span!(
|
|
_guard,
|
|
"block_particles",
|
|
"ParticleMgr::maintain_block_particles"
|
|
);
|
|
let dt = scene_data.state.ecs().fetch::<DeltaTime>().0;
|
|
let time = scene_data.state.get_time();
|
|
let player_pos = scene_data
|
|
.state
|
|
.read_component_copied::<Pos>(scene_data.player_entity)
|
|
.unwrap_or_default();
|
|
let player_chunk = player_pos.0.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| {
|
|
(e.floor() as i32).div_euclid(sz as i32)
|
|
});
|
|
|
|
struct BlockParticles<'a> {
|
|
// The function to select the blocks of interest that we should emit from
|
|
blocks: fn(&'a BlocksOfInterest) -> &'a [Vec3<i32>],
|
|
// The range, in chunks, that the particles should be generated in from the player
|
|
range: usize,
|
|
// The emission rate, per block per second, of the generated particles
|
|
rate: f32,
|
|
// The number of seconds that each particle should live for
|
|
lifetime: f32,
|
|
// The visual mode of the generated particle
|
|
mode: ParticleMode,
|
|
// Condition that must be true
|
|
cond: fn(&SceneData) -> bool,
|
|
}
|
|
|
|
let particles: &[BlockParticles] = &[
|
|
BlockParticles {
|
|
blocks: |boi| &boi.leaves,
|
|
range: 4,
|
|
rate: 0.001,
|
|
lifetime: 30.0,
|
|
mode: ParticleMode::Leaf,
|
|
cond: |_| true,
|
|
},
|
|
BlockParticles {
|
|
blocks: |boi| &boi.fires,
|
|
range: 2,
|
|
rate: 20.0,
|
|
lifetime: 0.25,
|
|
mode: ParticleMode::CampfireFire,
|
|
cond: |_| true,
|
|
},
|
|
BlockParticles {
|
|
blocks: |boi| &boi.fire_bowls,
|
|
range: 2,
|
|
rate: 20.0,
|
|
lifetime: 0.25,
|
|
mode: ParticleMode::FireBowl,
|
|
cond: |_| true,
|
|
},
|
|
BlockParticles {
|
|
blocks: |boi| &boi.smokers,
|
|
range: 8,
|
|
rate: 3.0,
|
|
lifetime: 40.0,
|
|
mode: ParticleMode::CampfireSmoke,
|
|
cond: |_| true,
|
|
},
|
|
BlockParticles {
|
|
blocks: |boi| &boi.fireflies,
|
|
range: 6,
|
|
rate: 0.004,
|
|
lifetime: 40.0,
|
|
mode: ParticleMode::Firefly,
|
|
cond: |sd| sd.state.get_day_period().is_dark(),
|
|
},
|
|
BlockParticles {
|
|
blocks: |boi| &boi.flowers,
|
|
range: 5,
|
|
rate: 0.002,
|
|
lifetime: 40.0,
|
|
mode: ParticleMode::Firefly,
|
|
cond: |sd| sd.state.get_day_period().is_dark(),
|
|
},
|
|
BlockParticles {
|
|
blocks: |boi| &boi.beehives,
|
|
range: 3,
|
|
rate: 0.5,
|
|
lifetime: 30.0,
|
|
mode: ParticleMode::Bee,
|
|
cond: |sd| sd.state.get_day_period().is_light(),
|
|
},
|
|
BlockParticles {
|
|
blocks: |boi| &boi.snow,
|
|
range: 4,
|
|
rate: 0.025,
|
|
lifetime: 15.0,
|
|
mode: ParticleMode::Snow,
|
|
cond: |_| true,
|
|
},
|
|
];
|
|
|
|
let mut rng = thread_rng();
|
|
for particles in particles.iter() {
|
|
if !(particles.cond)(scene_data) {
|
|
continue;
|
|
}
|
|
|
|
for offset in Spiral2d::new().take((particles.range * 2 + 1).pow(2)) {
|
|
let chunk_pos = player_chunk + offset;
|
|
|
|
terrain.get(chunk_pos).map(|chunk_data| {
|
|
let blocks = (particles.blocks)(&chunk_data.blocks_of_interest);
|
|
|
|
let avg_particles = dt * blocks.len() as f32 * particles.rate;
|
|
let particle_count = avg_particles.trunc() as usize
|
|
+ (rng.gen::<f32>() < avg_particles.fract()) as usize;
|
|
|
|
self.particles
|
|
.resize_with(self.particles.len() + particle_count, || {
|
|
let block_pos =
|
|
Vec3::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32))
|
|
+ blocks.choose(&mut rng).copied().unwrap(); // Can't fail
|
|
|
|
Particle::new(
|
|
Duration::from_secs_f32(particles.lifetime),
|
|
time,
|
|
particles.mode,
|
|
block_pos.map(|e: i32| e as f32 + rng.gen::<f32>()),
|
|
)
|
|
})
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
fn maintain_shockwave_particles(&mut self, scene_data: &SceneData) {
|
|
let state = scene_data.state;
|
|
let ecs = state.ecs();
|
|
let time = state.get_time();
|
|
|
|
for (_entity, pos, ori, shockwave) in (
|
|
&ecs.entities(),
|
|
&ecs.read_storage::<Pos>(),
|
|
&ecs.read_storage::<Ori>(),
|
|
&ecs.read_storage::<Shockwave>(),
|
|
)
|
|
.join()
|
|
{
|
|
let elapsed = time - shockwave.creation.unwrap_or_default();
|
|
|
|
let distance = shockwave.properties.speed * elapsed as f32;
|
|
|
|
let radians = shockwave.properties.angle.to_radians();
|
|
|
|
let ori_vec = ori.look_vec();
|
|
let theta = ori_vec.y.atan2(ori_vec.x);
|
|
let dtheta = radians / distance;
|
|
|
|
let heartbeats = self.scheduler.heartbeats(Duration::from_millis(2));
|
|
|
|
for heartbeat in 0..heartbeats {
|
|
if shockwave.properties.requires_ground {
|
|
// 1 / 3 the size of terrain voxel
|
|
let scale = 1.0 / 3.0;
|
|
|
|
let scaled_speed = shockwave.properties.speed * scale;
|
|
|
|
let sub_tick_interpolation = scaled_speed * 1000.0 * heartbeat as f32;
|
|
|
|
let distance =
|
|
shockwave.properties.speed * (elapsed as f32 - sub_tick_interpolation);
|
|
|
|
let new_particle_count = distance / scale as f32;
|
|
self.particles.reserve(new_particle_count as usize);
|
|
|
|
for d in 0..((distance / scale) as i32) {
|
|
let arc_position = theta - radians / 2.0 + dtheta * d as f32 * scale;
|
|
|
|
let position = pos.0
|
|
+ distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0);
|
|
|
|
let position_snapped = ((position / scale).floor() + 0.5) * scale;
|
|
|
|
self.particles.push(Particle::new(
|
|
Duration::from_millis(250),
|
|
time,
|
|
ParticleMode::GroundShockwave,
|
|
position_snapped,
|
|
));
|
|
}
|
|
} else {
|
|
for d in 0..3 * distance as i32 {
|
|
let arc_position = theta - radians / 2.0 + dtheta * d as f32 / 3.0;
|
|
|
|
let position = pos.0
|
|
+ distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0);
|
|
|
|
self.particles.push(Particle::new(
|
|
Duration::from_secs_f32((distance + 10.0) / 50.0),
|
|
time,
|
|
ParticleMode::FireShockwave,
|
|
position,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn upload_particles(&mut self, renderer: &mut Renderer) {
|
|
span!(_guard, "upload_particles", "ParticleMgr::upload_particles");
|
|
let all_cpu_instances = self
|
|
.particles
|
|
.iter()
|
|
.map(|p| p.instance)
|
|
.collect::<Vec<ParticleInstance>>();
|
|
|
|
// TODO: optimise buffer writes
|
|
let gpu_instances = renderer
|
|
.create_instances(&all_cpu_instances)
|
|
.expect("Failed to upload particle instances to the GPU!");
|
|
|
|
self.instances = gpu_instances;
|
|
}
|
|
|
|
pub fn render(
|
|
&self,
|
|
renderer: &mut Renderer,
|
|
scene_data: &SceneData,
|
|
global: &GlobalModel,
|
|
lod: &LodData,
|
|
) {
|
|
span!(_guard, "render", "ParticleMgr::render");
|
|
if scene_data.particles_enabled {
|
|
let model = &self
|
|
.model_cache
|
|
.get(DEFAULT_MODEL_KEY)
|
|
.expect("Expected particle model in cache");
|
|
|
|
renderer.render_particles(model, global, &self.instances, lod);
|
|
}
|
|
}
|
|
|
|
pub fn particle_count(&self) -> usize { self.instances.count() }
|
|
|
|
pub fn particle_count_visible(&self) -> usize { self.instances.count() }
|
|
}
|
|
|
|
fn default_instances(renderer: &mut Renderer) -> Instances<ParticleInstance> {
|
|
let empty_vec = Vec::new();
|
|
|
|
renderer
|
|
.create_instances(&empty_vec)
|
|
.expect("Failed to upload particle instances to the GPU!")
|
|
}
|
|
|
|
const DEFAULT_MODEL_KEY: &str = "voxygen.voxel.particle";
|
|
|
|
fn default_cache(renderer: &mut Renderer) -> HashMap<&'static str, Model<ParticlePipeline>> {
|
|
let mut model_cache = HashMap::new();
|
|
|
|
model_cache.entry(DEFAULT_MODEL_KEY).or_insert_with(|| {
|
|
let vox = DotVoxAsset::load_expect(DEFAULT_MODEL_KEY);
|
|
|
|
// NOTE: If we add texturing we may eventually try to share it among all
|
|
// particles in a single atlas.
|
|
let max_texture_size = renderer.max_texture_size();
|
|
let max_size =
|
|
guillotiere::Size::new(i32::from(max_texture_size), i32::from(max_texture_size));
|
|
let mut greedy = GreedyMesh::new(max_size);
|
|
|
|
let segment = Segment::from(&vox.read().0);
|
|
let segment_size = segment.size();
|
|
let mut mesh =
|
|
Meshable::<ParticlePipeline, &mut GreedyMesh>::generate_mesh(segment, &mut greedy).0;
|
|
// Center particle vertices around origin
|
|
for vert in mesh.vertices_mut() {
|
|
vert.pos[0] -= segment_size.x as f32 / 2.0;
|
|
vert.pos[1] -= segment_size.y as f32 / 2.0;
|
|
vert.pos[2] -= segment_size.z as f32 / 2.0;
|
|
}
|
|
|
|
// NOTE: Ignoring coloring / lighting for now.
|
|
drop(greedy);
|
|
|
|
renderer
|
|
.create_model(&mesh)
|
|
.expect("Failed to create particle model")
|
|
});
|
|
|
|
model_cache
|
|
}
|
|
|
|
/// Accumulates heartbeats to be consumed on the next tick.
|
|
struct HeartbeatScheduler {
|
|
/// Duration = Heartbeat Frequency/Intervals
|
|
/// f64 = Last update time
|
|
/// u8 = number of heartbeats since last update
|
|
/// - if it's more frequent then tick rate, it could be 1 or more.
|
|
/// - if it's less frequent then tick rate, it could be 1 or 0.
|
|
/// - if it's equal to the tick rate, it could be between 2 and 0, due to
|
|
/// delta time variance etc.
|
|
timers: HashMap<Duration, (f64, u8)>,
|
|
|
|
last_known_time: f64,
|
|
}
|
|
|
|
impl HeartbeatScheduler {
|
|
pub fn new() -> Self {
|
|
HeartbeatScheduler {
|
|
timers: HashMap::new(),
|
|
last_known_time: 0.0,
|
|
}
|
|
}
|
|
|
|
/// updates the last elapsed times and elapsed counts
|
|
/// this should be called once, and only once per tick.
|
|
pub fn maintain(&mut self, now: f64) {
|
|
span!(_guard, "maintain", "HeartbeatScheduler::maintain");
|
|
self.last_known_time = now;
|
|
|
|
for (frequency, (last_update, heartbeats)) in self.timers.iter_mut() {
|
|
// the number of frequency cycles that have occurred.
|
|
let total_heartbeats = (now - *last_update) / frequency.as_secs_f64();
|
|
|
|
// exclude partial frequency cycles
|
|
let full_heartbeats = total_heartbeats.floor();
|
|
|
|
*heartbeats = full_heartbeats as u8;
|
|
|
|
// the remaining partial frequency cycle, as a decimal.
|
|
let partial_heartbeat = total_heartbeats - full_heartbeats;
|
|
|
|
// the remaining partial frequency cycle, as a unit of time(f64).
|
|
let partial_heartbeat_as_time = frequency.mul_f64(partial_heartbeat).as_secs_f64();
|
|
|
|
// now minus the left over heart beat count precision as seconds,
|
|
// Note: we want to preserve incomplete heartbeats, and roll them
|
|
// over into the next update.
|
|
*last_update = now - partial_heartbeat_as_time;
|
|
}
|
|
}
|
|
|
|
/// returns the number of times this duration has elapsed since the last
|
|
/// tick:
|
|
/// - if it's more frequent then tick rate, it could be 1 or more.
|
|
/// - if it's less frequent then tick rate, it could be 1 or 0.
|
|
/// - if it's equal to the tick rate, it could be between 2 and 0, due to
|
|
/// delta time variance.
|
|
pub fn heartbeats(&mut self, frequency: Duration) -> u8 {
|
|
span!(_guard, "HeartbeatScheduler::heartbeats");
|
|
let last_known_time = self.last_known_time;
|
|
|
|
self.timers
|
|
.entry(frequency)
|
|
.or_insert_with(|| (last_known_time, 0))
|
|
.1
|
|
}
|
|
|
|
pub fn clear(&mut self) { self.timers.clear() }
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct Particle {
|
|
alive_until: f64, // created_at + lifespan
|
|
instance: ParticleInstance,
|
|
}
|
|
|
|
impl Particle {
|
|
fn new(lifespan: Duration, time: f64, mode: ParticleMode, pos: Vec3<f32>) -> Self {
|
|
Particle {
|
|
alive_until: time + lifespan.as_secs_f64(),
|
|
instance: ParticleInstance::new(time, lifespan.as_secs_f32(), mode, pos),
|
|
}
|
|
}
|
|
|
|
fn new_directed(
|
|
lifespan: Duration,
|
|
time: f64,
|
|
mode: ParticleMode,
|
|
pos1: Vec3<f32>,
|
|
pos2: Vec3<f32>,
|
|
) -> Self {
|
|
Particle {
|
|
alive_until: time + lifespan.as_secs_f64(),
|
|
instance: ParticleInstance::new_directed(
|
|
time,
|
|
lifespan.as_secs_f32(),
|
|
mode,
|
|
pos1,
|
|
pos2,
|
|
),
|
|
}
|
|
}
|
|
}
|