📄 src/bunnies/heart.rs
use std::time::Duration;

use bevy::{
    animation::{
        AnimatedBy, AnimationClip, AnimationPlayer, AnimationTargetId, animated_field,
        animation_curves::{AnimatableCurve, AnimatedField},
        graph::{AnimationGraph, AnimationGraphHandle},
    },
    app::{App, Update},
    asset::{AssetServer, Assets},
    color::Color,
    ecs::{
        component::Component,
        entity::Entity,
        hierarchy::Children,
        name::Name,
        observer::On,
        query::With,
        system::{Commands, In, Query, Res, ResMut},
    },
    gltf::GltfAssetLabel,
    math::{Quat, Vec3, curve::UnevenSampleAutoCurve},
    pbr::{MeshMaterial3d, StandardMaterial},
    scene::{SceneInstanceReady, SceneRoot},
    time::Time,
    transform::components::Transform,
};

pub(super) fn add_heart_system(app: &mut App) -> &mut App {
    app.add_observer(heart_ready);
    app.add_systems(Update, destroy_heart)
}

pub(super) fn spawn_heart(
    In((mid_point, time_to_live)): In<(Vec3, Duration)>,
    mut commands: Commands,
    time: Res<Time>,
    asset_server: Res<AssetServer>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    mut animations: ResMut<Assets<AnimationClip>>,
    mut graphs: ResMut<Assets<AnimationGraph>>,
) {
    let model =
        asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/3D Leap Land/glb/heart.glb"));

    let name = Name::new("heart");
    let mut animation = AnimationClip::default();
    let animation_target_id = AnimationTargetId::from_name(&name);
    animation.add_curve_to_target(
        animation_target_id,
        AnimatableCurve::new(
            animated_field!(Transform::translation),
            UnevenSampleAutoCurve::new([
                (0.0, mid_point.with_y(2.0)),
                (0.5, mid_point.with_y(3.0)),
                (1.0, mid_point.with_y(2.0)),
            ])
            .unwrap(),
        ),
    );
    animation.add_curve_to_target(
        animation_target_id,
        AnimatableCurve::new(
            animated_field!(Transform::rotation),
            UnevenSampleAutoCurve::new([
                (0.0, Quat::IDENTITY),
                (
                    0.25,
                    Quat::from_axis_angle(Vec3::Y, std::f32::consts::PI * 0.5),
                ),
                (
                    0.50,
                    Quat::from_axis_angle(Vec3::Y, std::f32::consts::PI * 1.0),
                ),
                (
                    0.75,
                    Quat::from_axis_angle(Vec3::Y, std::f32::consts::PI * 1.5),
                ),
                (1.0, Quat::IDENTITY),
            ])
            .unwrap(),
        ),
    );
    let (graph, animation_index) = AnimationGraph::from_clip(animations.add(animation));
    let mut player = AnimationPlayer::default();
    player.play(animation_index).repeat().set_speed(0.5);

    let mut entity = commands.spawn_empty();
    entity.insert((
        SceneRoot(model),
        MeshMaterial3d(materials.add(Color::linear_rgb(1.0, 0.0, 0.0))),
        Transform::from_scale(Vec3::splat(0.01)),
        name,
        AnimationGraphHandle(graphs.add(graph)),
        player,
        animation_target_id,
        AnimatedBy(entity.id()),
        Heart {
            destroy_time: time.elapsed() + time_to_live,
        },
    ));
}

#[derive(Component)]
struct Heart {
    destroy_time: Duration,
}

fn heart_ready(
    scene_ready: On<SceneInstanceReady>,
    children: Query<&Children>,
    hearts: Query<(), With<Heart>>,
    mesh_materials: Query<&MeshMaterial3d<StandardMaterial>>,
    mut asset_materials: ResMut<Assets<StandardMaterial>>,
) {
    let Ok(()) = hearts.get(scene_ready.entity) else {
        return;
    };

    for descendant in children.iter_descendants(scene_ready.entity) {
        let Ok(id) = mesh_materials.get(descendant) else {
            continue;
        };
        let Some(material) = asset_materials.get_mut(id.id()) else {
            continue;
        };

        material.base_color = Color::linear_rgb(1.0, 0.0, 0.0);
    }
}

fn destroy_heart(mut commands: Commands, time: Res<Time>, hearts: Query<(Entity, &Heart)>) {
    hearts.iter().for_each(|(heart, Heart { destroy_time })| {
        if time.elapsed() < *destroy_time {
            return;
        }

        commands.entity(heart).despawn();
    });
}