📄 src/confetti.rs
use std::ops::Range;

use bevy::{
    app::{App, Update},
    asset::Assets,
    camera::Camera3d,
    color::Color,
    ecs::{
        component::Component,
        entity::Entity,
        event::Event,
        observer::On,
        query::With,
        system::{Commands, Query, Res, ResMut, Single},
    },
    math::{Dir3, FloatPow, Quat, Vec3, primitives::Plane3d},
    mesh::{Mesh, Mesh3d, Meshable},
    pbr::{MeshMaterial3d, StandardMaterial},
    time::Time,
    transform::components::Transform,
};
use rand::RngExt;

use crate::Rng;

pub trait ConfettiSystems {
    fn add_confetti_systems(&mut self) -> &mut Self;
}

impl ConfettiSystems for App {
    fn add_confetti_systems(&mut self) -> &mut Self {
        self.add_observer(spawn_confetti)
            .add_systems(Update, update)
    }
}

#[derive(Event)]
pub struct SpawnConfetti {
    pub origin: Vec3,
    pub direction: Dir3,
    pub count: usize,
}

#[derive(Component)]
struct Confetti {
    velocity: Vec3,
    rotation: Quat,
}

const CONE_ANGLE: f32 = std::f32::consts::FRAC_PI_8;
const INITIAL_VELOCITY: Range<f32> = 7.5..12.5;
const ROTATION_VELOCITY: Range<f32> = std::f32::consts::FRAC_PI_8..std::f32::consts::PI;

fn spawn_confetti(
    confetti: On<SpawnConfetti>,
    camera: Single<&Transform, With<Camera3d>>,
    mut rng: ResMut<Rng>,
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    (0..confetti.count).for_each(|_| {
        let velocity = sample_cone(&mut rng, confetti.direction, CONE_ANGLE)
            * rng.random_range(INITIAL_VELOCITY);
        let rotation = Quat::from_axis_angle(
            (camera.translation - confetti.origin).normalize(),
            rng.random_range(ROTATION_VELOCITY),
        );
        commands.spawn((
            Confetti { velocity, rotation },
            Transform::from_translation(confetti.origin).with_rotation(rotation),
            Mesh3d(meshes.add(Plane3d::default().mesh().size(0.1, 0.2))),
            MeshMaterial3d(materials.add(StandardMaterial::from_color(Color::hsl(
                rng.random_range(0.0..360.0),
                1.0,
                0.5,
            )))),
        ));
    });
}

fn sample_cone(rng: &mut Rng, direction: Dir3, angle: f32) -> Dir3 {
    let rotation = Quat::from_rotation_arc(Vec3::Z, *direction);
    let z = rng.random_range(angle.cos()..=1.0);
    let theta = rng.random_range(0.0..std::f32::consts::TAU);
    rotation
        * Dir3::new_unchecked(Vec3::new(
            (1.0 - z.squared()).sqrt() * theta.cos(),
            (1.0 - z.squared()).sqrt() * theta.sin(),
            z,
        ))
}

fn update(
    mut commands: Commands,
    time: Res<Time>,
    mut confetti: Query<(Entity, &mut Transform, &mut Confetti)>,
) {
    confetti
        .iter_mut()
        .for_each(|(entity, mut transform, mut confetti)| {
            if transform.translation.y < 0.0 {
                commands.entity(entity).despawn();
                return;
            }

            transform.translation += confetti.velocity * time.delta_secs();
            transform.rotation *= confetti.rotation;

            confetti.velocity.y -= 9.82 * time.delta_secs();
        });
}