Improved archery with feedback sfx and particles

This commit is contained in:
Joshua Barretto 2021-03-29 00:29:48 +01:00
parent 88f99986af
commit b0acbda236
16 changed files with 236 additions and 47 deletions

View File

@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Generated a net world map - Generated a net world map
- Overhauled clouds for more verticality and performance - Overhauled clouds for more verticality and performance
- New tooltip for items with stats comparison - New tooltip for items with stats comparison
- Improved bow feedback, added arrow particles
### Removed ### Removed

BIN
assets/voxygen/audio/sfx/arrow_hit.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/arrow_miss.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -62,6 +62,7 @@ const int EXPLOSION = 20;
const int ICE = 21; const int ICE = 21;
const int LIFESTEAL_BEAM = 22; const int LIFESTEAL_BEAM = 22;
const int CULTIST_FLAME = 23; const int CULTIST_FLAME = 23;
const int STATIC_SMOKE = 24;
// meters per second squared (acceleration) // meters per second squared (acceleration)
const float earth_gravity = 9.807; const float earth_gravity = 9.807;
@ -402,6 +403,13 @@ void main() {
vec4(purp_color, 0.0, purp_color, 1), vec4(purp_color, 0.0, purp_color, 1),
spin_in_axis(vec3(rand6, rand7, rand8), percent() * 10 + 3 * rand9) spin_in_axis(vec3(rand6, rand7, rand8), percent() * 10 + 3 * rand9)
); );
} else if (inst_mode == STATIC_SMOKE) {
attr = Attr(
vec3(0),
vec3((0.5 * (1 - slow_start(0.8)))),
vec4(1.0),
spin_in_axis(vec3(rand6, rand7, rand8), rand9)
);
} else { } else {
attr = Attr( attr = Attr(
linear_motion( linear_motion(
@ -424,7 +432,7 @@ void main() {
vec4 normals[6] = vec4[](vec4(-1,0,0,0), vec4(1,0,0,0), vec4(0,-1,0,0), vec4(0,1,0,0), vec4(0,0,-1,0), vec4(0,0,1,0)); vec4 normals[6] = vec4[](vec4(-1,0,0,0), vec4(1,0,0,0), vec4(0,-1,0,0), vec4(0,1,0,0), vec4(0,0,-1,0), vec4(0,0,1,0));
f_norm = f_norm =
// inst_pos * // inst_pos *
((normals[(v_norm_ao >> 0) & 0x7u]) * attr.rot).xyz; normalize(((normals[(v_norm_ao >> 0) & 0x7u]) * attr.rot).xyz);
//vec3 col = vec3((uvec3(v_col) >> uvec3(0, 8, 16)) & uvec3(0xFFu)) / 255.0; //vec3 col = vec3((uvec3(v_col) >> uvec3(0, 8, 16)) & uvec3(0xFFu)) / 255.0;
f_col = vec4(attr.col.rgb, attr.col.a); f_col = vec4(attr.col.rgb, attr.col.a);

View File

@ -1033,14 +1033,15 @@ impl Client {
pub fn loaded_distance(&self) -> f32 { self.loaded_distance } pub fn loaded_distance(&self) -> f32 { self.loaded_distance }
pub fn current_chunk(&self) -> Option<Arc<TerrainChunk>> { pub fn position(&self) -> Option<Vec3<f32>> {
let chunk_pos = Vec2::from(
self.state self.state
.read_storage::<comp::Pos>() .read_storage::<comp::Pos>()
.get(self.entity()) .get(self.entity())
.cloned()? .map(|v| v.0)
.0, }
)
pub fn current_chunk(&self) -> Option<Arc<TerrainChunk>> {
let chunk_pos = Vec2::from(self.position()?)
.map2(TerrainChunkSize::RECT_SIZE, |e: f32, sz| { .map2(TerrainChunkSize::RECT_SIZE, |e: f32, sz| {
(e as u32).div_euclid(sz) as i32 (e as u32).div_euclid(sz) as i32
}); });

View File

@ -8,13 +8,25 @@ use specs_idvs::IdvStorage;
use tracing::warn; use tracing::warn;
use vek::ops::{Lerp, Slerp}; use vek::ops::{Lerp, Slerp};
#[derive(Debug, Default)] #[derive(Debug)]
pub struct InterpBuffer<T> { pub struct InterpBuffer<T> {
pub buf: [(f64, T); 4], pub buf: [(f64, T); 4],
pub i: usize, pub i: usize,
} }
impl<T: Clone> InterpBuffer<T> { impl<T: Clone> InterpBuffer<T> {
pub fn new(x: T) -> Self {
Self {
buf: [
(0.0, x.clone()),
(0.0, x.clone()),
(0.0, x.clone()),
(0.0, x),
],
i: 0,
}
}
fn push(&mut self, time: f64, x: T) { fn push(&mut self, time: f64, x: T) {
let InterpBuffer { let InterpBuffer {
ref mut buf, ref mut buf,
@ -54,6 +66,8 @@ impl InterpolatableComponent for Pos {
type InterpData = InterpBuffer<Pos>; type InterpData = InterpBuffer<Pos>;
type ReadData = InterpBuffer<Vel>; type ReadData = InterpBuffer<Vel>;
fn new_data(x: Self) -> Self::InterpData { InterpBuffer::new(x) }
fn update_component(&self, interp_data: &mut Self::InterpData, time: f64, force_update: bool) { fn update_component(&self, interp_data: &mut Self::InterpData, time: f64, force_update: bool) {
interp_data.update(time, *self, force_update); interp_data.update(time, *self, force_update);
} }
@ -108,6 +122,8 @@ impl InterpolatableComponent for Vel {
type InterpData = InterpBuffer<Vel>; type InterpData = InterpBuffer<Vel>;
type ReadData = (); type ReadData = ();
fn new_data(x: Self) -> Self::InterpData { InterpBuffer::new(x) }
fn update_component(&self, interp_data: &mut Self::InterpData, time: f64, force_update: bool) { fn update_component(&self, interp_data: &mut Self::InterpData, time: f64, force_update: bool) {
interp_data.update(time, *self, force_update); interp_data.update(time, *self, force_update);
} }
@ -143,6 +159,8 @@ impl InterpolatableComponent for Ori {
type InterpData = InterpBuffer<Ori>; type InterpData = InterpBuffer<Ori>;
type ReadData = (); type ReadData = ();
fn new_data(x: Self) -> Self::InterpData { InterpBuffer::new(x) }
fn update_component(&self, interp_data: &mut Self::InterpData, time: f64, force_update: bool) { fn update_component(&self, interp_data: &mut Self::InterpData, time: f64, force_update: bool) {
interp_data.update(time, *self, force_update); interp_data.update(time, *self, force_update);
} }

View File

@ -47,20 +47,21 @@ pub fn handle_remove<C: Component>(entity: Entity, world: &World) {
} }
pub trait InterpolatableComponent: Component { pub trait InterpolatableComponent: Component {
type InterpData: Component + Default; type InterpData: Component;
type ReadData; type ReadData;
fn new_data(x: Self) -> Self::InterpData;
fn update_component(&self, data: &mut Self::InterpData, time: f64, force_update: bool); fn update_component(&self, data: &mut Self::InterpData, time: f64, force_update: bool);
fn interpolate(self, data: &Self::InterpData, time: f64, read_data: &Self::ReadData) -> Self; fn interpolate(self, data: &Self::InterpData, time: f64, read_data: &Self::ReadData) -> Self;
} }
pub fn handle_interp_insert<C: InterpolatableComponent>( pub fn handle_interp_insert<C: InterpolatableComponent + Clone>(
comp: C, comp: C,
entity: Entity, entity: Entity,
world: &World, world: &World,
force_update: bool, force_update: bool,
) { ) {
let mut interp_data = C::InterpData::default(); let mut interp_data = C::new_data(comp.clone());
let time = world.read_resource::<Time>().0; let time = world.read_resource::<Time>().0;
comp.update_component(&mut interp_data, time, force_update); comp.update_component(&mut interp_data, time, force_update);
handle_insert(comp, entity, world); handle_insert(comp, entity, world);

View File

@ -22,6 +22,13 @@ pub enum Outcome {
body: comp::Body, body: comp::Body,
vel: Vec3<f32>, vel: Vec3<f32>,
}, },
ProjectileHit {
pos: Vec3<f32>,
body: comp::Body,
vel: Vec3<f32>,
source: Option<Uid>,
target: Option<Uid>,
},
Beam { Beam {
pos: Vec3<f32>, pos: Vec3<f32>,
specifier: beam::FrontendSpecifier, specifier: beam::FrontendSpecifier,
@ -56,6 +63,7 @@ impl Outcome {
match self { match self {
Outcome::Explosion { pos, .. } Outcome::Explosion { pos, .. }
| Outcome::ProjectileShot { pos, .. } | Outcome::ProjectileShot { pos, .. }
| Outcome::ProjectileHit { pos, .. }
| Outcome::Beam { pos, .. } | Outcome::Beam { pos, .. }
| Outcome::SkillPointGain { pos, .. } | Outcome::SkillPointGain { pos, .. }
| Outcome::SummonedCreature { pos, .. } => Some(*pos), | Outcome::SummonedCreature { pos, .. } => Some(*pos),

View File

@ -10,6 +10,7 @@ use common::{
}, },
consts::{FRIC_GROUND, GRAVITY}, consts::{FRIC_GROUND, GRAVITY},
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
outcome::Outcome,
resources::DeltaTime, resources::DeltaTime,
terrain::{Block, TerrainGrid}, terrain::{Block, TerrainGrid},
uid::Uid, uid::Uid,
@ -20,7 +21,7 @@ use common_ecs::{Job, Origin, ParMode, Phase, PhysicsMetrics, System};
use rayon::iter::ParallelIterator; use rayon::iter::ParallelIterator;
use specs::{ use specs::{
shred::{ResourceId, World}, shred::{ResourceId, World},
Entities, Entity, Join, ParJoin, Read, ReadExpect, ReadStorage, SystemData, WriteExpect, Entities, Entity, Join, ParJoin, Read, ReadExpect, ReadStorage, SystemData, Write, WriteExpect,
WriteStorage, WriteStorage,
}; };
use std::ops::Range; use std::ops::Range;
@ -106,6 +107,7 @@ pub struct PhysicsWrite<'a> {
pos_vel_defers: WriteStorage<'a, PosVelDefer>, pos_vel_defers: WriteStorage<'a, PosVelDefer>,
orientations: WriteStorage<'a, Ori>, orientations: WriteStorage<'a, Ori>,
previous_phys_cache: WriteStorage<'a, PreviousPhysCache>, previous_phys_cache: WriteStorage<'a, PreviousPhysCache>,
outcomes: Write<'a, Vec<Outcome>>,
} }
#[derive(SystemData)] #[derive(SystemData)]
@ -590,7 +592,7 @@ impl<'a> PhysicsData<'a> {
let velocities = &write.velocities; let velocities = &write.velocities;
// Second pass: resolve collisions // Second pass: resolve collisions
let land_on_grounds = ( let (land_on_grounds, mut outcomes) = (
&read.entities, &read.entities,
read.scales.maybe(), read.scales.maybe(),
read.stickies.maybe(), read.stickies.maybe(),
@ -628,16 +630,12 @@ impl<'a> PhysicsData<'a> {
_, _,
)| { )| {
let mut land_on_ground = None; let mut land_on_ground = None;
let mut outcomes = Vec::new();
// Defer the writes of positions and velocities to allow an inner loop over // Defer the writes of positions and velocities to allow an inner loop over
// terrain-like entities // terrain-like entities
let old_vel = *vel; let old_vel = *vel;
let mut vel = *vel; let mut vel = *vel;
if sticky.is_some() && physics_state.on_surface().is_some() {
vel.0 = physics_state.ground_vel;
return land_on_ground;
}
let scale = if let Collider::Voxel { .. } = collider { let scale = if let Collider::Voxel { .. } = collider {
scale.map(|s| s.0).unwrap_or(1.0) scale.map(|s| s.0).unwrap_or(1.0)
} else { } else {
@ -738,17 +736,45 @@ impl<'a> PhysicsData<'a> {
Collider::Point => { Collider::Point => {
let mut pos = *pos; let mut pos = *pos;
let (dist, block) = if let Some(block) = read
.terrain
.get(pos.0.map(|e| e.floor() as i32))
.ok()
.filter(|b| b.is_filled())
// TODO: `is_solid`, when arrows are special-cased
{
(0.0, Some(block))
} else {
let (dist, block) = read let (dist, block) = read
.terrain .terrain
.ray(pos.0, pos.0 + pos_delta) .ray(pos.0, pos.0 + pos_delta)
.until(|block: &Block| block.is_filled()) .until(|block: &Block| block.is_filled())
.ignore_error() .ignore_error()
.cast(); .cast();
(dist, block.unwrap()) // Can't fail since we do ignore_error above
};
pos.0 += pos_delta.try_normalized().unwrap_or_else(Vec3::zero) * dist; pos.0 += pos_delta.try_normalized().unwrap_or_else(Vec3::zero) * dist;
// Can't fail since we do ignore_error above // TODO: Not all projectiles should count as sticky!
if block.unwrap().is_some() { if sticky.is_some() {
if let Some((projectile, body)) = read
.projectiles
.get(entity)
.filter(|_| vel.0.magnitude_squared() > 1.0 && block.is_some())
.zip(read.bodies.get(entity).copied())
{
outcomes.push(Outcome::ProjectileHit {
pos: pos.0,
body,
vel: vel.0,
source: projectile.owner,
target: None,
});
}
}
if block.is_some() {
let block_center = pos.0.map(|e| e.floor()) + 0.5; let block_center = pos.0.map(|e| e.floor()) + 0.5;
let block_rpos = (pos.0 - block_center) let block_rpos = (pos.0 - block_center)
.try_normalized() .try_normalized()
@ -774,6 +800,11 @@ impl<'a> PhysicsData<'a> {
Vec3::unit_y() * -block_rpos.y.signum() Vec3::unit_y() * -block_rpos.y.signum()
}); });
} }
// Sticky things shouldn't move
if sticky.is_some() {
vel.0 = Vec3::zero();
}
} }
physics_state.in_liquid = read physics_state.in_liquid = read
@ -978,20 +1009,31 @@ impl<'a> PhysicsData<'a> {
pos_vel_defer.vel = None; pos_vel_defer.vel = None;
} }
land_on_ground (land_on_ground, outcomes)
}, },
) )
.fold(Vec::new, |mut land_on_grounds, land_on_ground| { .fold(
|| (Vec::new(), Vec::new()),
|(mut land_on_grounds, mut all_outcomes), (land_on_ground, mut outcomes)| {
land_on_ground.map(|log| land_on_grounds.push(log)); land_on_ground.map(|log| land_on_grounds.push(log));
land_on_grounds all_outcomes.append(&mut outcomes);
}) (land_on_grounds, all_outcomes)
.reduce(Vec::new, |mut land_on_grounds_a, mut land_on_grounds_b| { },
)
.reduce(
|| (Vec::new(), Vec::new()),
|(mut land_on_grounds_a, mut outcomes_a),
(mut land_on_grounds_b, mut outcomes_b)| {
land_on_grounds_a.append(&mut land_on_grounds_b); land_on_grounds_a.append(&mut land_on_grounds_b);
land_on_grounds_a outcomes_a.append(&mut outcomes_b);
}); (land_on_grounds_a, outcomes_a)
},
);
drop(guard); drop(guard);
job.cpu_stats.measure(ParMode::Single); job.cpu_stats.measure(ParMode::Single);
write.outcomes.append(&mut outcomes);
prof_span!(guard, "write deferred pos and vel"); prof_span!(guard, "write deferred pos and vel");
for (_, pos, vel, pos_vel_defer) in ( for (_, pos, vel, pos_vel_defer) in (
&read.entities, &read.entities,
@ -1279,6 +1321,7 @@ fn box_voxel_collision<'a, T: BaseVol<Vox = Block> + ReadVol>(
if d * e.signum() < 0.0 { 0.0 } else { e } if d * e.signum() < 0.0 { 0.0 } else { e }
}, },
); );
pos_delta *= resolve_dir.map(|e| if e != 0.0 { 0.0 } else { 1.0 }); pos_delta *= resolve_dir.map(|e| if e != 0.0 { 0.0 } else { 1.0 });
} }

View File

@ -1,21 +1,23 @@
use common::{ use common::{
combat::{AttackerInfo, TargetInfo}, combat::{AttackerInfo, TargetInfo},
comp::{ comp::{
projectile, Combo, Energy, Group, Health, HealthSource, Inventory, Ori, PhysicsState, Pos, projectile, Body, Combo, Energy, Group, Health, HealthSource, Inventory, Ori, PhysicsState,
Projectile, Stats, Vel, Pos, Projectile, Stats, Vel,
}, },
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
outcome::Outcome,
resources::DeltaTime, resources::DeltaTime,
uid::UidAllocator, uid::{Uid, UidAllocator},
util::Dir, util::Dir,
GroupTarget, GroupTarget,
}; };
use common_ecs::{Job, Origin, Phase, System}; use common_ecs::{Job, Origin, Phase, System};
use specs::{ use specs::{
saveload::MarkerAllocator, shred::ResourceId, Entities, Join, Read, ReadStorage, SystemData, saveload::MarkerAllocator, shred::ResourceId, Entities, Join, Read, ReadStorage, SystemData,
World, WriteStorage, World, Write, WriteStorage,
}; };
use std::time::Duration; use std::time::Duration;
use vek::*;
#[derive(SystemData)] #[derive(SystemData)]
pub struct ReadData<'a> { pub struct ReadData<'a> {
@ -23,6 +25,7 @@ pub struct ReadData<'a> {
dt: Read<'a, DeltaTime>, dt: Read<'a, DeltaTime>,
uid_allocator: Read<'a, UidAllocator>, uid_allocator: Read<'a, UidAllocator>,
server_bus: Read<'a, EventBus<ServerEvent>>, server_bus: Read<'a, EventBus<ServerEvent>>,
uids: ReadStorage<'a, Uid>,
positions: ReadStorage<'a, Pos>, positions: ReadStorage<'a, Pos>,
physics_states: ReadStorage<'a, PhysicsState>, physics_states: ReadStorage<'a, PhysicsState>,
velocities: ReadStorage<'a, Vel>, velocities: ReadStorage<'a, Vel>,
@ -32,6 +35,7 @@ pub struct ReadData<'a> {
stats: ReadStorage<'a, Stats>, stats: ReadStorage<'a, Stats>,
combos: ReadStorage<'a, Combo>, combos: ReadStorage<'a, Combo>,
healths: ReadStorage<'a, Health>, healths: ReadStorage<'a, Health>,
bodies: ReadStorage<'a, Body>,
} }
/// This system is responsible for handling projectile effect triggers /// This system is responsible for handling projectile effect triggers
@ -42,13 +46,17 @@ impl<'a> System<'a> for Sys {
ReadData<'a>, ReadData<'a>,
WriteStorage<'a, Ori>, WriteStorage<'a, Ori>,
WriteStorage<'a, Projectile>, WriteStorage<'a, Projectile>,
Write<'a, Vec<Outcome>>,
); );
const NAME: &'static str = "projectile"; const NAME: &'static str = "projectile";
const ORIGIN: Origin = Origin::Common; const ORIGIN: Origin = Origin::Common;
const PHASE: Phase = Phase::Create; const PHASE: Phase = Phase::Create;
fn run(_job: &mut Job<Self>, (read_data, mut orientations, mut projectiles): Self::SystemData) { fn run(
_job: &mut Job<Self>,
(read_data, mut orientations, mut projectiles, mut outcomes): Self::SystemData,
) {
let mut server_emitter = read_data.server_bus.emitter(); let mut server_emitter = read_data.server_bus.emitter();
// Attacks // Attacks
@ -123,6 +131,19 @@ impl<'a> System<'a> for Sys {
health: read_data.healths.get(target), health: read_data.healths.get(target),
}; };
if let Some(&body) = read_data.bodies.get(entity) {
outcomes.push(Outcome::ProjectileHit {
pos: pos.0,
body,
vel: read_data
.velocities
.get(entity)
.map_or(Vec3::zero(), |v| v.0),
source: projectile.owner,
target: read_data.uids.get(target).copied(),
});
}
attack.apply_attack( attack.apply_attack(
target_group, target_group,
attacker_info, attacker_info,

View File

@ -289,7 +289,12 @@ impl SfxMgr {
); );
} }
pub fn handle_outcome(&mut self, outcome: &Outcome, audio: &mut AudioFrontend) { pub fn handle_outcome(
&mut self,
outcome: &Outcome,
audio: &mut AudioFrontend,
client: &Client,
) {
if !audio.sfx_enabled() { if !audio.sfx_enabled() {
return; return;
} }
@ -350,6 +355,33 @@ impl SfxMgr {
}, },
} }
}, },
Outcome::ProjectileHit {
pos,
body,
source,
target,
..
} => match body {
Body::Object(
object::Body::Arrow
| object::Body::MultiArrow
| object::Body::ArrowSnake
| object::Body::ArrowTurret,
) => {
if target.is_none() {
audio.play_sfx("voxygen.audio.sfx.arrow_miss", *pos, Some(2.0));
} else if *source == client.uid() {
audio.play_sfx(
"voxygen.audio.sfx.arrow_hit",
client.position().unwrap_or(*pos),
Some(2.0),
);
} else {
audio.play_sfx("voxygen.audio.sfx.arrow_hit", *pos, Some(2.0));
}
},
_ => {},
},
Outcome::SkillPointGain { pos, .. } => { Outcome::SkillPointGain { pos, .. } => {
let file_ref = "voxygen.audio.sfx.character.level_up_sound_-_shorter_wind_up"; let file_ref = "voxygen.audio.sfx.character.level_up_sound_-_shorter_wind_up";
audio.play_sfx(file_ref, *pos, None); audio.play_sfx(file_ref, *pos, None);

View File

@ -41,8 +41,8 @@ impl<'a> System<'a> for Sys {
) )
.join() .join()
{ {
// Update interpolation values // Update interpolation values, but don't interpolate far things or objects
if i.pos.distance_squared(pos.0) < 64.0 * 64.0 { if i.pos.distance_squared(pos.0) < 64.0 * 64.0 && !matches!(body, Body::Object(_)) {
i.pos = Lerp::lerp(i.pos, pos.0 + vel.0 * 0.03, 10.0 * dt.0); i.pos = Lerp::lerp(i.pos, pos.0 + vel.0 * 0.03, 10.0 * dt.0);
i.ori = Ori::slerp(i.ori, *ori, base_ori_interp(body) * dt.0); i.ori = Ori::slerp(i.ori, *ori, base_ori_interp(body) * dt.0);
} else { } else {

View File

@ -120,6 +120,7 @@ pub enum ParticleMode {
Ice = 21, Ice = 21,
LifestealBeam = 22, LifestealBeam = 22,
CultistFlame = 23, CultistFlame = 23,
StaticSmoke = 24,
} }
impl ParticleMode { impl ParticleMode {

View File

@ -108,6 +108,7 @@ pub struct Scene {
} }
pub struct SceneData<'a> { pub struct SceneData<'a> {
pub client: &'a Client,
pub state: &'a State, pub state: &'a State,
pub player_entity: specs::Entity, pub player_entity: specs::Entity,
pub target_entity: Option<specs::Entity>, pub target_entity: Option<specs::Entity>,
@ -398,7 +399,8 @@ impl Scene {
) { ) {
span!(_guard, "handle_outcome", "Scene::handle_outcome"); span!(_guard, "handle_outcome", "Scene::handle_outcome");
self.particle_mgr.handle_outcome(&outcome, &scene_data); self.particle_mgr.handle_outcome(&outcome, &scene_data);
self.sfx_mgr.handle_outcome(&outcome, audio); self.sfx_mgr
.handle_outcome(&outcome, audio, scene_data.client);
match outcome { match outcome {
Outcome::Explosion { Outcome::Explosion {

View File

@ -196,6 +196,18 @@ impl ParticleMgr {
}, },
_ => {}, _ => {},
}, },
Outcome::ProjectileHit { pos, target, .. } => {
if target.is_some() {
self.particles.resize_with(self.particles.len() + 30, || {
Particle::new(
Duration::from_millis(100),
time,
ParticleMode::Shrapnel,
*pos,
)
});
}
},
Outcome::ProjectileShot { .. } Outcome::ProjectileShot { .. }
| Outcome::Beam { .. } | Outcome::Beam { .. }
| Outcome::ExpChange { .. } | Outcome::ExpChange { .. }
@ -257,6 +269,12 @@ impl ParticleMgr {
Body::Object(object::Body::CampfireLit) => { Body::Object(object::Body::CampfireLit) => {
self.maintain_campfirelit_particles(scene_data, pos, vel) 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) => { Body::Object(object::Body::BoltFire) => {
self.maintain_boltfire_particles(scene_data, pos, vel) self.maintain_boltfire_particles(scene_data, pos, vel)
}, },
@ -313,6 +331,33 @@ impl ParticleMgr {
} }
} }
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( fn maintain_boltfire_particles(
&mut self, &mut self,
scene_data: &SceneData, scene_data: &SceneData,

View File

@ -1510,6 +1510,7 @@ impl PlayState for SessionState {
{ {
let client = self.client.borrow(); let client = self.client.borrow();
let scene_data = SceneData { let scene_data = SceneData {
client: &client,
state: client.state(), state: client.state(),
player_entity: client.entity(), player_entity: client.entity(),
// Only highlight if interactable // Only highlight if interactable
@ -1577,6 +1578,7 @@ impl PlayState for SessionState {
let client = self.client.borrow(); let client = self.client.borrow();
let scene_data = SceneData { let scene_data = SceneData {
client: &client,
state: client.state(), state: client.state(),
player_entity: client.entity(), player_entity: client.entity(),
// Only highlight if interactable // Only highlight if interactable