📄 src/goal.rs
use std::{
    ops::{Add, DerefMut, RangeInclusive},
    time::Duration,
};

use bevy::{
    app::{App, Startup, Update},
    asset::Assets,
    color::Color,
    ecs::{
        children,
        component::Component,
        hierarchy::Children,
        query::{With, Without},
        system::{Commands, Query, Res, ResMut, Single},
    },
    log::warn,
    math::{Dir3, Quat, Vec3, primitives::Circle},
    mesh::{Mesh, Mesh3d},
    pbr::{MeshMaterial3d, StandardMaterial},
    text::{TextFont, TextSpan},
    time::Time,
    transform::components::Transform,
    ui::widget::Text,
};
use rand::RngExt;

use crate::{
    Rng,
    bunnies::{Bunny, BunnyLocator},
    confetti::SpawnConfetti,
};

pub trait GoalSystems {
    fn add_goal_systems(&mut self) -> &mut Self;
}

impl GoalSystems for App {
    fn add_goal_systems(&mut self) -> &mut Self {
        self.add_systems(Startup, setup).add_systems(Update, update)
    }
}

const GOAL_AREA_WIDTH: RangeInclusive<f32> = -10.0..=10.0;
const GOAL_AREA_HEIGHT: RangeInclusive<f32> = -10.0..=10.0;
const GOAL_RADIUS: f32 = 5.0;
const GOAL_DURATION: f32 = 1.0;
const CELEBRATION_DURATION: f32 = 3.0;

#[derive(Component, Default)]
enum Goal {
    #[default]
    NotAchieved,
    InProgress {
        start: Duration,
    },
    Achieved {
        start: Duration,
    },
}

#[derive(Component)]
struct GoalText;

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    commands.spawn((
        Mesh3d(meshes.add(Circle::new(GOAL_RADIUS))),
        MeshMaterial3d(materials.add(Color::linear_rgb(0.0, 1.0, 0.0))),
        Transform::from_xyz(10.0, 0.0, 10.0)
            .with_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
        Goal::default(),
        children![(
            Mesh3d(meshes.add(Circle::new(GOAL_RADIUS))),
            MeshMaterial3d(materials.add(Color::linear_rgb(0.0, 0.0, 1.0))),
            Transform::from_xyz(0.0, 0.0, 0.1).with_scale(Vec3::new(0.0, 0.0, 1.0)),
        )],
    ));
    commands
        .spawn((
            Text::new("Bunnies in goal: "),
            TextFont {
                font_size: 42.0,
                ..Default::default()
            },
        ))
        .with_child((
            TextSpan::default(),
            TextFont {
                font_size: 33.0,
                ..Default::default()
            },
            GoalText,
        ));
}

fn update(
    mut commands: Commands,
    locator: Res<BunnyLocator>,
    bunnies: Query<(), With<Bunny>>,
    time: Res<Time>,
    mut rng: ResMut<Rng>,
    mut goal: Single<(&mut Transform, &mut Goal, &Children)>,
    mut goal_text: Single<&mut TextSpan, With<GoalText>>,
    mut transforms: Query<&mut Transform, Without<Goal>>,
) {
    let (transform, goal, children) = goal.deref_mut();

    let count = bunnies.count();
    let in_goal = locator.get_nearby_count(transform.translation, GOAL_RADIUS);
    **goal_text = format!("{in_goal}/{count}").into();

    match (in_goal == count, goal.as_ref()) {
        (_, Goal::Achieved { start }) => {
            if start.add(Duration::from_secs_f32(CELEBRATION_DURATION)) < time.elapsed() {
                **goal = Goal::NotAchieved;
                transform.translation = Vec3::new(
                    rng.random_range(GOAL_AREA_WIDTH),
                    0.0,
                    rng.random_range(GOAL_AREA_HEIGHT),
                );
            }
        }
        (true, Goal::NotAchieved) => {
            **goal = Goal::InProgress {
                start: time.elapsed(),
            }
        }
        (true, Goal::InProgress { start }) => {
            let Some(Ok(mut child)) = children.first().map(|c| transforms.get_mut(*c)) else {
                warn!("Cannot get child of goal");
                return;
            };
            if start.add(Duration::from_secs_f32(GOAL_DURATION)) < time.elapsed() {
                commands.trigger(SpawnConfetti {
                    origin: transform.translation,
                    direction: Dir3::Y,
                    count: 1000,
                });

                **goal = Goal::Achieved {
                    start: time.elapsed(),
                };
                child.scale = Vec3::ZERO;
            } else {
                let scale = 1.0
                    - (start.add(Duration::from_secs_f32(GOAL_DURATION)) - time.elapsed())
                        .as_secs_f32()
                        / GOAL_DURATION;
                child.scale = Vec3::new(scale, scale, 1.0);
            }
        }
        (false, Goal::NotAchieved) => {}
        (false, Goal::InProgress { .. }) => {
            let Some(Ok(mut child)) = children.first().map(|c| transforms.get_mut(*c)) else {
                warn!("Cannot get child of goal");
                return;
            };
            **goal = Goal::NotAchieved;
            child.scale = Vec3::new(0.0, 0.0, 1.0);
        }
    }
}