use std::hash::{Hash, Hasher};
use bevy::{ecs::resource::Resource, log::trace, math::FloatOrd, prelude::*, reflect::Reflect};
use rand::{
distr::{uniform::SampleUniform, Distribution, Uniform},
SeedableRng,
};
use rand_pcg::Pcg32;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{
CompiledParticleEffect, EffectAsset, EffectSimulation, ParticleEffect, SimulationCondition,
};
pub(crate) fn new_rng() -> Pcg32 {
let mut rng = rand::rng();
let mut seed = [0u8; 16];
seed.copy_from_slice(
&Uniform::new_inclusive(0, u128::MAX)
.unwrap()
.sample(&mut rng)
.to_le_bytes(),
);
Pcg32::from_seed(seed)
}
#[derive(Resource)]
pub struct Random(pub Pcg32);
pub trait FloatHash: PartialEq {
fn hash_f32<H: Hasher>(&self, state: &mut H);
}
impl FloatHash for f32 {
fn hash_f32<H: Hasher>(&self, state: &mut H) {
FloatOrd(*self).hash(state);
}
}
impl FloatHash for Vec2 {
fn hash_f32<H: Hasher>(&self, state: &mut H) {
FloatOrd(self.x).hash(state);
FloatOrd(self.y).hash(state);
}
}
impl FloatHash for Vec3 {
fn hash_f32<H: Hasher>(&self, state: &mut H) {
FloatOrd(self.x).hash(state);
FloatOrd(self.y).hash(state);
FloatOrd(self.z).hash(state);
}
}
impl FloatHash for Vec4 {
fn hash_f32<H: Hasher>(&self, state: &mut H) {
FloatOrd(self.x).hash(state);
FloatOrd(self.y).hash(state);
FloatOrd(self.z).hash(state);
FloatOrd(self.w).hash(state);
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Reflect)]
#[non_exhaustive]
pub enum CpuValue<T: Copy + FromReflect> {
Single(T),
Uniform((T, T)),
}
impl<T: Copy + FromReflect + Default> Default for CpuValue<T> {
fn default() -> Self {
Self::Single(T::default())
}
}
impl<T: Copy + FromReflect + SampleUniform> CpuValue<T> {
pub fn sample(&self, rng: &mut Pcg32) -> T {
match self {
Self::Single(x) => *x,
Self::Uniform((a, b)) => Uniform::new_inclusive(*a, *b).unwrap().sample(rng),
}
}
}
impl<T: Copy + FromReflect + PartialOrd> CpuValue<T> {
pub fn range(&self) -> [T; 2] {
match self {
Self::Single(x) => [*x; 2],
Self::Uniform((a, b)) => {
if a <= b {
[*a, *b]
} else {
[*b, *a]
}
}
}
}
}
impl<T: Copy + FromReflect> From<T> for CpuValue<T> {
fn from(t: T) -> Self {
Self::Single(t)
}
}
impl<T: Copy + FromReflect> From<[T; 2]> for CpuValue<T> {
fn from(t: [T; 2]) -> Self {
Self::Uniform((t[0], t[1]))
}
}
impl<T: Copy + FromReflect> From<(T, T)> for CpuValue<T> {
fn from(t: (T, T)) -> Self {
Self::Uniform(t)
}
}
impl<T: Copy + FromReflect + FloatHash> Eq for CpuValue<T> {}
impl<T: Copy + FromReflect + FloatHash> Hash for CpuValue<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
CpuValue::Single(f) => {
1_u8.hash(state);
f.hash_f32(state);
}
CpuValue::Uniform((a, b)) => {
2_u8.hash(state);
a.hash_f32(state);
b.hash_f32(state);
}
}
}
}
#[derive(Debug, Clone, Copy, Error)]
pub enum SpawnerSettingsError {
#[error("Spawn period [{min}:{max}] is invalid")]
InvalidPeriod { min: f32, max: f32 },
#[error("Spawn period is infinite")]
InfinitePeriod,
}
#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
#[reflect(Default)]
pub struct SpawnerSettings {
count: CpuValue<f32>,
spawn_duration: CpuValue<f32>,
period: CpuValue<f32>,
cycle_count: u32,
starts_active: bool,
emit_on_start: bool,
}
impl Default for SpawnerSettings {
fn default() -> Self {
Self::once(1.0f32.into())
}
}
impl SpawnerSettings {
pub fn new(
count: CpuValue<f32>,
spawn_duration: CpuValue<f32>,
period: CpuValue<f32>,
cycle_count: u32,
) -> Self {
match Self::try_new(count, spawn_duration, period, cycle_count) {
Ok(s) => s,
Err(err) => match err {
SpawnerSettingsError::InvalidPeriod { min, max } => {
if min < 0. {
panic!("`period` must not generate negative numbers (period.min was {min}, expected >= 0).");
} else {
panic!("`period` must be able to generate a positive number (period.max was {max}, expected > 0).");
}
}
SpawnerSettingsError::InfinitePeriod => panic!("`period` {period:?} has an infinite bound. If upgrading from a previous version, use `cycle_count = 1` instead for a single-cycle burst.",),
},
}
}
pub fn try_new(
count: CpuValue<f32>,
spawn_duration: CpuValue<f32>,
period: CpuValue<f32>,
cycle_count: u32,
) -> Result<Self, SpawnerSettingsError> {
let range = period.range();
if (cycle_count != 1) && (range[0] < 0. || range[1] <= 0.) {
return Err(SpawnerSettingsError::InvalidPeriod {
min: range[0],
max: range[1],
});
}
if !range[0].is_finite() || !range[1].is_finite() {
return Err(SpawnerSettingsError::InfinitePeriod);
}
Ok(Self {
count,
spawn_duration,
period,
cycle_count,
starts_active: true,
emit_on_start: true,
})
}
pub fn with_emit_on_start(mut self, emit_on_start: bool) -> Self {
self.emit_on_start = emit_on_start;
self
}
pub fn set_emit_on_start(&mut self, emit_on_start: bool) {
self.emit_on_start = emit_on_start;
}
pub fn emits_on_start(&self) -> bool {
self.emit_on_start
}
pub fn once(count: CpuValue<f32>) -> Self {
Self::new(count, 0.0.into(), 0.0.into(), 1)
}
pub fn is_once(&self) -> bool {
self.cycle_count == 1
}
pub fn is_forever(&self) -> bool {
self.cycle_count == 0
}
pub fn rate(rate: CpuValue<f32>) -> Self {
Self::new(rate, 1.0.into(), 1.0.into(), 0)
}
pub fn burst(count: CpuValue<f32>, period: CpuValue<f32>) -> Self {
Self::new(count, 0.0.into(), period, 0)
}
pub fn with_count(mut self, count: CpuValue<f32>) -> Self {
self.count = count;
self
}
pub fn set_count(&mut self, count: CpuValue<f32>) {
self.count = count;
}
pub fn count(&self) -> CpuValue<f32> {
self.count
}
pub fn with_spawn_duration(mut self, spawn_duration: CpuValue<f32>) -> Self {
self.spawn_duration = spawn_duration;
self
}
pub fn set_spawn_duration(&mut self, spawn_duration: CpuValue<f32>) {
self.spawn_duration = spawn_duration;
}
pub fn spawn_duration(&self) -> CpuValue<f32> {
self.spawn_duration
}
pub fn with_period(mut self, period: CpuValue<f32>) -> Self {
assert!(
period.range()[0].is_finite() && period.range()[1].is_finite(),
"`period` {:?} has an infinite bound. If upgrading from a previous version, use `cycle_count = 1` instead for a single-cycle burst.",
period
);
self.period = period;
self
}
pub fn set_period(&mut self, period: CpuValue<f32>) {
assert!(
period.range()[0].is_finite() && period.range()[1].is_finite(),
"`period` {:?} has an infinite bound. If upgrading from a previous version, use `cycle_count = 1` instead for a single-cycle burst.",
period
);
self.period = period;
}
pub fn period(&self) -> CpuValue<f32> {
self.period
}
pub fn with_cycle_count(mut self, cycle_count: u32) -> Self {
self.cycle_count = cycle_count;
self
}
pub fn set_cycle_count(&mut self, cycle_count: u32) {
self.cycle_count = cycle_count;
}
pub fn cycle_count(&self) -> u32 {
self.cycle_count
}
pub fn with_starts_active(mut self, starts_active: bool) -> Self {
self.starts_active = starts_active;
self
}
pub fn set_starts_active(&mut self, starts_active: bool) {
self.starts_active = starts_active;
}
pub fn starts_active(&self) -> bool {
self.starts_active
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Component, Reflect)]
#[reflect(Component)]
pub struct EffectSpawner {
pub settings: SpawnerSettings,
cycle_time: f32,
completed_cycle_count: u32,
sampled_spawn_duration: f32,
sampled_period: f32,
sampled_count: f32,
pub spawn_count: u32,
spawn_remainder: f32,
pub active: bool,
}
impl EffectSpawner {
pub fn new(settings: &SpawnerSettings) -> Self {
Self {
settings: *settings,
cycle_time: 0.,
completed_cycle_count: if settings.emit_on_start || settings.is_forever() {
0
} else {
settings.cycle_count()
},
sampled_spawn_duration: 0.,
sampled_period: 0.,
sampled_count: 0.,
spawn_count: 0,
spawn_remainder: 0.,
active: settings.starts_active(),
}
}
pub fn with_active(mut self, active: bool) -> Self {
self.active = active;
self
}
#[inline]
pub fn cycle_time(&self) -> f32 {
self.cycle_time
}
#[inline]
pub fn cycle_spawn_duration(&self) -> f32 {
self.sampled_spawn_duration
}
#[inline]
pub fn cycle_period(&self) -> f32 {
if self.settings.is_once() {
0.
} else {
self.sampled_period
}
}
#[inline]
pub fn cycle_ratio(&self) -> f32 {
if self.settings.is_once() {
0.
} else {
self.cycle_time / self.sampled_period
}
}
#[inline]
pub fn cycle_spawn_count(&self) -> f32 {
self.sampled_count
}
#[inline]
pub fn completed_cycle_count(&self) -> u32 {
self.completed_cycle_count
}
#[inline]
pub fn has_completed(&self) -> bool {
!self.settings.is_forever() && (self.completed_cycle_count >= self.settings.cycle_count())
}
pub fn reset(&mut self) {
self.cycle_time = 0.;
self.completed_cycle_count = 0;
self.sampled_spawn_duration = 0.;
self.sampled_period = 0.;
self.sampled_count = 0.;
self.spawn_count = 0;
self.spawn_remainder = 0.;
}
pub fn tick(&mut self, mut dt: f32, rng: &mut Pcg32) -> u32 {
if !self.active
|| (!self.settings.is_forever()
&& (self.completed_cycle_count >= self.settings.cycle_count()))
{
self.spawn_count = 0;
return 0;
}
loop {
if self.sampled_period == 0.0 {
if self.settings.is_once() {
self.sampled_spawn_duration = self.settings.spawn_duration.sample(rng);
self.sampled_period = self.sampled_spawn_duration.max(1e-12);
} else {
self.sampled_period = self.settings.period.sample(rng);
assert!(self.sampled_period > 0.);
self.sampled_spawn_duration = self
.settings
.spawn_duration
.sample(rng)
.clamp(0., self.sampled_period);
}
self.sampled_spawn_duration = self.settings.spawn_duration.sample(rng);
self.sampled_count = self.settings.count.sample(rng).max(0.);
}
let new_time = self.cycle_time + dt;
if self.cycle_time <= self.sampled_spawn_duration {
self.spawn_remainder += if self.sampled_spawn_duration < 1e-5f32.max(dt / 100.0) {
self.sampled_count
} else {
let ratio = ((new_time.min(self.sampled_spawn_duration) - self.cycle_time)
/ self.sampled_spawn_duration)
.clamp(0., 1.);
self.sampled_count * ratio
};
}
self.cycle_time = new_time;
if self.cycle_time >= self.sampled_period {
dt = self.cycle_time - self.sampled_period;
self.cycle_time = 0.0;
self.completed_cycle_count += 1;
self.sampled_period = 0.0;
if !self.settings.is_forever()
&& (self.completed_cycle_count >= self.settings.cycle_count())
{
break;
}
} else {
break;
}
}
let count = self.spawn_remainder.floor();
self.spawn_remainder -= count;
self.spawn_count = count as u32;
self.spawn_count
}
}
pub fn tick_spawners(
mut commands: Commands,
time: Res<Time<EffectSimulation>>,
effects: Res<Assets<EffectAsset>>,
mut rng: ResMut<Random>,
mut query: Query<(
Entity,
&ParticleEffect,
&CompiledParticleEffect,
&InheritedVisibility,
Option<&mut EffectSpawner>,
)>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("tick_spawners").entered();
trace!("tick_spawners()");
let dt = time.delta_secs();
for (entity, effect, compiled_effect, inherited_visibility, maybe_spawner) in query.iter_mut() {
let mut can_tick = if compiled_effect.is_ready() {
true
} else {
trace!("[Effect {entity:?}] Not ready; skipped spawner tick.");
false
};
let Some(asset) = effects.get(&effect.handle) else {
trace!(
"Effect asset with handle {:?} is not available; skipped initializers tick.",
effect.handle
);
continue;
};
if asset.simulation_condition == SimulationCondition::WhenVisible
&& !inherited_visibility.get()
{
trace!(
"Effect asset with handle {:?} is not visible, and simulates only WhenVisible; skipped initializers tick.",
effect.handle
);
can_tick = false;
}
if let Some(mut effect_spawner) = maybe_spawner {
if can_tick {
effect_spawner.tick(dt, &mut rng.0);
}
} else {
let mut effect_spawner = EffectSpawner::new(&asset.spawner);
if can_tick {
effect_spawner.tick(dt, &mut rng.0);
}
commands.entity(entity).insert(effect_spawner);
}
}
}
#[cfg(test)]
mod test {
use std::time::Duration;
use bevy::{
asset::{
io::{
memory::{Dir, MemoryAssetReader},
AssetSourceBuilder, AssetSourceBuilders, AssetSourceId,
},
AssetServerMode, UnapprovedPathMode,
},
camera::visibility::{VisibilityPlugin, VisibilitySystems},
tasks::{IoTaskPool, TaskPoolBuilder},
};
use super::*;
use crate::Module;
#[test]
fn test_range_single() {
let value = CpuValue::Single(1.0);
assert_eq!(value.range(), [1.0, 1.0]);
}
#[test]
fn test_range_uniform() {
let value = CpuValue::Uniform((1.0, 3.0));
assert_eq!(value.range(), [1.0, 3.0]);
}
#[test]
fn test_range_uniform_reverse() {
let value = CpuValue::Uniform((3.0, 1.0));
assert_eq!(value.range(), [1.0, 3.0]);
}
#[test]
fn test_new() {
let rng = &mut new_rng();
let spawner = SpawnerSettings::new(3.0.into(), 3.0.into(), 10.0.into(), 2);
let mut spawner = EffectSpawner::new(&spawner);
let count = spawner.tick(2., rng); assert_eq!(count, 2);
assert!(spawner.active);
assert_eq!(spawner.cycle_time(), 2.);
assert_eq!(spawner.cycle_spawn_duration(), 3.);
assert_eq!(spawner.cycle_period(), 10.);
assert_eq!(spawner.cycle_ratio(), 0.2); assert_eq!(spawner.cycle_spawn_count(), 3.);
assert_eq!(spawner.completed_cycle_count(), 0);
let count = spawner.tick(5., rng); assert_eq!(count, 1);
assert!(spawner.active);
assert_eq!(spawner.cycle_time(), 7.);
assert_eq!(spawner.cycle_spawn_duration(), 3.);
assert_eq!(spawner.cycle_period(), 10.);
assert_eq!(spawner.cycle_ratio(), 0.7); assert_eq!(spawner.cycle_spawn_count(), 3.);
assert_eq!(spawner.completed_cycle_count(), 0);
let count = spawner.tick(8., rng); assert_eq!(count, 3);
assert!(spawner.active);
assert_eq!(spawner.cycle_time(), 5.); assert_eq!(spawner.cycle_spawn_duration(), 3.);
assert_eq!(spawner.cycle_period(), 10.);
assert_eq!(spawner.cycle_ratio(), 0.5); assert_eq!(spawner.cycle_spawn_count(), 3.);
assert_eq!(spawner.completed_cycle_count(), 1);
let count = spawner.tick(10., rng); assert_eq!(count, 0);
assert!(spawner.active);
assert_eq!(spawner.completed_cycle_count(), 2);
let count = spawner.tick(0.1, rng); assert_eq!(count, 0);
assert!(spawner.active);
assert_eq!(spawner.completed_cycle_count(), 2);
}
#[test]
#[should_panic]
fn test_new_panic_negative_period() {
let _ = SpawnerSettings::new(3.0.into(), 1.0.into(), CpuValue::Uniform((-1., 1.)), 0);
}
#[test]
#[should_panic]
fn test_new_panic_zero_period() {
let _ = SpawnerSettings::new(3.0.into(), 1.0.into(), CpuValue::Uniform((0., 0.)), 0);
}
#[test]
#[should_panic]
fn test_new_panic_infinite_period() {
let _ = SpawnerSettings::new(
3.0.into(),
1.0.into(),
CpuValue::Uniform((0., f32::INFINITY)),
0,
);
}
#[test]
fn test_try_new_negative_period() {
assert!(matches!(
SpawnerSettings::try_new(3.0.into(), 1.0.into(), CpuValue::Uniform((-1., 1.)), 0),
Err(SpawnerSettingsError::InvalidPeriod { min: -1., max: 1. })
));
}
#[test]
fn test_try_new_zero_period() {
assert!(matches!(
SpawnerSettings::try_new(3.0.into(), 1.0.into(), CpuValue::Uniform((0., 0.)), 0),
Err(SpawnerSettingsError::InvalidPeriod { min: 0., max: 0. })
));
}
#[test]
fn test_try_new_infitie_period() {
assert!(matches!(
SpawnerSettings::try_new(
3.0.into(),
1.0.into(),
CpuValue::Uniform((0., f32::INFINITY)),
0
),
Err(SpawnerSettingsError::InfinitePeriod)
));
assert!(matches!(
SpawnerSettings::try_new(
3.0.into(),
1.0.into(),
CpuValue::Uniform((f32::INFINITY, f32::INFINITY)),
0
),
Err(SpawnerSettingsError::InfinitePeriod)
));
}
#[test]
fn test_once() {
let rng = &mut new_rng();
let spawner = SpawnerSettings::once(5.0.into());
assert!(spawner.is_once());
let mut spawner = EffectSpawner::new(&spawner);
assert!(spawner.active);
let count = spawner.tick(0.001, rng);
assert_eq!(count, 5);
let count = spawner.tick(100.0, rng);
assert_eq!(count, 0);
}
#[test]
fn test_once_reset() {
let rng = &mut new_rng();
let spawner = SpawnerSettings::once(5.0.into());
assert!(spawner.is_once());
assert!(spawner.starts_active());
let mut spawner = EffectSpawner::new(&spawner);
spawner.tick(1.0, rng);
spawner.reset();
let count = spawner.tick(1.0, rng);
assert_eq!(count, 5);
}
#[test]
fn test_once_start_inactive() {
let rng = &mut new_rng();
let spawner = SpawnerSettings::once(5.0.into()).with_starts_active(false);
assert!(spawner.is_once());
assert!(!spawner.starts_active());
let mut spawner = EffectSpawner::new(&spawner);
assert!(!spawner.has_completed());
let count = spawner.tick(1.0, rng);
assert_eq!(count, 0);
assert!(!spawner.has_completed());
spawner.active = true;
let count = spawner.tick(1.0, rng);
assert_eq!(count, 5);
assert!(spawner.active);
assert!(spawner.has_completed());
let count = spawner.tick(1.0, rng);
assert_eq!(count, 0);
assert!(spawner.active);
assert!(spawner.has_completed());
spawner.reset();
assert!(spawner.active);
assert!(!spawner.has_completed());
let count = spawner.tick(1.0, rng);
assert_eq!(count, 5);
assert!(spawner.active);
assert!(spawner.has_completed());
}
#[test]
fn test_rate() {
let rng = &mut new_rng();
let spawner = SpawnerSettings::rate(5.0.into());
assert!(!spawner.is_once());
assert!(spawner.is_forever());
let mut spawner = EffectSpawner::new(&spawner);
let count = spawner.tick(1.01, rng);
assert_eq!(count, 5);
let count = spawner.tick(0.4, rng);
assert_eq!(count, 2);
}
#[test]
fn test_rate_active() {
let rng = &mut new_rng();
let spawner = SpawnerSettings::rate(5.0.into());
assert!(!spawner.is_once());
let mut spawner = EffectSpawner::new(&spawner);
spawner.tick(1.01, rng);
spawner.active = false;
assert!(!spawner.active);
let count = spawner.tick(0.4, rng);
assert_eq!(count, 0);
spawner.active = true;
assert!(spawner.active);
let count = spawner.tick(0.4, rng);
assert_eq!(count, 2);
}
#[test]
fn test_rate_accumulate() {
let rng = &mut new_rng();
let spawner = SpawnerSettings::rate(5.0.into());
assert!(!spawner.is_once());
let mut spawner = EffectSpawner::new(&spawner);
let count = (0..13).map(|_| spawner.tick(1.0 / 60.0, rng)).sum::<u32>();
assert_eq!(count, 1);
}
#[test]
fn test_burst() {
let rng = &mut new_rng();
let spawner = SpawnerSettings::burst(5.0.into(), 2.0.into());
assert!(!spawner.is_once());
assert!(spawner.is_forever());
let mut spawner = EffectSpawner::new(&spawner);
let count = spawner.tick(1.0, rng);
assert_eq!(count, 5);
let count = spawner.tick(4.0, rng);
assert_eq!(count, 10);
let count = spawner.tick(0.1, rng);
assert_eq!(count, 0);
}
#[test]
fn test_with_active() {
let rng = &mut new_rng();
let spawner = SpawnerSettings::rate(5.0.into()).with_starts_active(false);
let mut spawner = EffectSpawner::new(&spawner);
assert!(!spawner.active);
let count = spawner.tick(1., rng);
assert_eq!(count, 0);
spawner.active = false; let count = spawner.tick(1., rng);
assert_eq!(count, 0);
spawner.active = true;
assert!(spawner.active);
let count = spawner.tick(1., rng);
assert_eq!(count, 5);
}
fn make_test_app() -> App {
IoTaskPool::get_or_init(|| {
TaskPoolBuilder::default()
.num_threads(1)
.thread_name("Hanabi test IO Task Pool".to_string())
.build()
});
let mut app = App::new();
let watch_for_changes = false;
let mut builders = app
.world_mut()
.get_resource_or_insert_with::<AssetSourceBuilders>(Default::default);
let dir = Dir::default();
let dummy_builder =
AssetSourceBuilder::new(move || Box::new(MemoryAssetReader { root: dir.clone() }));
builders.insert(AssetSourceId::Default, dummy_builder);
let sources = builders.build_sources(watch_for_changes, false);
let asset_server = AssetServer::new(
sources.into(),
AssetServerMode::Unprocessed,
watch_for_changes,
UnapprovedPathMode::Forbid,
);
app.insert_resource(asset_server);
app.init_asset::<Mesh>();
app.init_asset::<bevy::mesh::skinning::SkinnedMeshInverseBindposes>();
app.add_plugins(VisibilityPlugin);
app.init_resource::<Time<EffectSimulation>>();
app.insert_resource(Random(new_rng()));
app.init_asset::<EffectAsset>();
app.add_systems(
PostUpdate,
tick_spawners.after(VisibilitySystems::CheckVisibility),
);
app
}
struct TestCase {
visibility: Option<Visibility>,
asset_spawner: SpawnerSettings,
}
impl TestCase {
fn new(visibility: Option<Visibility>, asset_spawner: SpawnerSettings) -> Self {
Self {
visibility,
asset_spawner,
}
}
}
#[test]
fn test_tick_spawners() {
let asset_spawner = SpawnerSettings::once(32.0.into());
for test_case in &[
TestCase::new(None, asset_spawner),
TestCase::new(Some(Visibility::Hidden), asset_spawner),
TestCase::new(Some(Visibility::Visible), asset_spawner),
] {
let mut app = make_test_app();
let (effect_entity, handle) = {
let world = app.world_mut();
let mut assets = world.resource_mut::<Assets<EffectAsset>>();
let mut asset = EffectAsset::new(64, test_case.asset_spawner, Module::default());
asset.simulation_condition = if test_case.visibility.is_some() {
SimulationCondition::WhenVisible
} else {
SimulationCondition::Always
};
let handle = assets.add(asset);
let entity = if let Some(visibility) = test_case.visibility {
world
.spawn((
visibility,
InheritedVisibility::default(),
ParticleEffect {
handle: handle.clone(),
..default()
},
CompiledParticleEffect::default().with_ready_for_tests(),
))
.id()
} else {
world
.spawn((
ParticleEffect {
handle: handle.clone(),
..default()
},
CompiledParticleEffect::default().with_ready_for_tests(),
))
.id()
};
world.spawn(Camera3d::default());
(entity, handle)
};
let _cur_time = {
let mut time = app.world_mut().resource_mut::<Time<EffectSimulation>>();
time.advance_by(Duration::from_millis(16));
time.elapsed()
};
app.update();
let world = app.world_mut();
if let Some(test_visibility) = test_case.visibility {
let (entity, visibility, inherited_visibility, particle_effect, effect_spawner) =
world
.query::<(
Entity,
&Visibility,
&InheritedVisibility,
&ParticleEffect,
Option<&EffectSpawner>,
)>()
.iter(world)
.next()
.unwrap();
assert_eq!(entity, effect_entity);
assert_eq!(visibility, test_visibility);
assert_eq!(
inherited_visibility.get(),
test_visibility == Visibility::Visible
);
assert_eq!(particle_effect.handle, handle);
assert!(effect_spawner.is_some());
let effect_spawner = effect_spawner.unwrap();
let actual_spawner = effect_spawner.settings;
assert_eq!(actual_spawner, test_case.asset_spawner);
assert!(effect_spawner.active);
assert_eq!(effect_spawner.spawn_remainder, 0.);
assert_eq!(effect_spawner.cycle_time, 0.);
if inherited_visibility.get() {
assert_eq!(effect_spawner.completed_cycle_count, 1);
assert_eq!(effect_spawner.spawn_count, 32);
} else {
assert_eq!(effect_spawner.completed_cycle_count, 0);
assert_eq!(effect_spawner.spawn_count, 0);
}
} else {
let (entity, particle_effect, effect_spawners) = world
.query::<(Entity, &ParticleEffect, Option<&EffectSpawner>)>()
.iter(world)
.next()
.unwrap();
assert_eq!(entity, effect_entity);
assert_eq!(particle_effect.handle, handle);
assert!(effect_spawners.is_some());
let effect_spawner = effect_spawners.unwrap();
let actual_spawner = effect_spawner.settings;
assert!(effect_spawner.active); assert_eq!(effect_spawner.spawn_remainder, 0.);
assert_eq!(effect_spawner.cycle_time, 0.);
assert_eq!(effect_spawner.completed_cycle_count, 1);
assert_eq!(effect_spawner.spawn_count, 32);
assert_eq!(actual_spawner, test_case.asset_spawner);
}
}
}
}