lifecycler 0.2.7

Bevy Game Jam #5 submission. Terminal aquarium.
Documentation
use bevy::prelude::*;
use rand::seq::SliceRandom;
use rand_chacha::{
    rand_core::{RngCore, SeedableRng},
    ChaCha8Rng,
};

use crate::{general::play_sfx, Flags};

pub(super) fn plugin(app: &mut App) {
    app.add_systems(Startup, (setup_pellets_system, setup_sfx_system))
        .add_systems(
            Update,
            (
                create_pellets_system,
                move_pellets_system,
                perish_perishables_system,
            ),
        )
        .init_resource::<PelletThreshold>()
        .add_event::<PelletEvent>();
}

#[derive(Component)]
pub struct Pellet;

#[derive(Component, Deref)]
pub struct PelletFalling(Vec3);

#[derive(Resource, Deref, DerefMut)]
pub struct PelletRng(ChaCha8Rng);

#[derive(Resource, Deref)]
pub struct PelletMesh(Handle<Mesh>);

#[derive(Resource, Deref)]
pub struct PelletMaterials(Vec<Handle<StandardMaterial>>);

#[derive(Resource, Default, Deref, DerefMut)]
pub struct PelletThreshold(u32);

#[derive(Component, Deref, DerefMut)]
pub struct Perishable(Timer);

#[derive(Event, Deref)]
pub struct PelletEvent(pub Transform);

#[derive(Resource, Deref)]
pub struct PelletSound(Handle<AudioSource>);

fn setup_pellets_system(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    let mesh = meshes.add(Cuboid::from_size(Vec3::new(0.03, 0.03, 0.03)));
    commands.insert_resource(PelletMesh(mesh));

    let mut seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);
    let pellet_materials = (0..36)
        .map(|_| {
            let base_color = Color::hsl((seeded_rng.next_u32() % 360) as f32, 0.8, 0.8);
            let emissive = base_color.to_linear() * 5.0;

            materials.add(StandardMaterial {
                base_color,
                emissive,
                ..default()
            })
        })
        .collect();
    commands.insert_resource(PelletMaterials(pellet_materials));
    commands.insert_resource(PelletRng(seeded_rng));
}

fn setup_sfx_system(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.insert_resource(PelletSound(
        asset_server.load("embedded://lifecycler/../assets/bubble.ogg"),
    ));
}

fn create_pellets_system(
    mut commands: Commands,
    flags: Res<Flags>,
    mut pellet_events: EventReader<PelletEvent>,
    mut pellet_rng: ResMut<PelletRng>,
    pellet_mesh: Res<PelletMesh>,
    pellet_materials: Res<PelletMaterials>,
    pellet_sound: Res<PelletSound>,
) {
    for mouse_position in pellet_events.read() {
        let fall_target = Vec3::new(
            mouse_position.translation.x.clamp(-1.75, 1.75),
            -1.7,
            pellet_rng.next_u32() as f32 / u32::MAX as f32 * 0.75 - 0.25,
        );

        play_sfx(&mut commands, &pellet_sound, &flags);

        commands.spawn((
            Pellet,
            PelletFalling(fall_target),
            PbrBundle {
                transform: **mouse_position,
                mesh: pellet_mesh.clone(),
                material: pellet_materials.choose(&mut pellet_rng.0).unwrap().clone(),
                ..default()
            },
        ));
    }
}

fn move_pellets_system(
    mut commands: Commands,
    mut pellets: Query<(Entity, &mut Transform, &PelletFalling)>,
    time: Res<Time>,
) {
    for (id, mut pellet_transform, PelletFalling(fall_target)) in &mut pellets {
        pellet_transform.translation = pellet_transform
            .translation
            .move_towards(*fall_target, time.delta_seconds() * 0.3);

        pellet_transform.translation.x +=
            (time.elapsed_seconds() + (fall_target.x * 16.) % 3.).sin() / 800.;
        pellet_transform.translation.x = pellet_transform.translation.x.clamp(-1.8, 1.8);

        if pellet_transform.translation.distance(*fall_target) < 0.003 {
            let mut entity = commands.entity(id);
            entity.remove::<PelletFalling>();
            entity.insert(Perishable(Timer::from_seconds(20., TimerMode::Once)));
        }
    }
}

fn perish_perishables_system(
    mut commands: Commands,
    time: Res<Time>,
    mut perishables: Query<(Entity, &mut Perishable)>,
) {
    let delta = time.delta();
    for (id, mut timer) in &mut perishables {
        timer.tick(delta);
        if timer.finished() {
            commands.entity(id).despawn();
        }
    }
}