# Objective Use the same text positioning as other examples that have instruction text. See https://bevyengine.org/learn/contribute/helping-out/creating-examples/#visual-guidelines
		
			
				
	
	
		
			180 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			180 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
//! Demonstrates implementing a cooldown in UI.
 | 
						|
//!
 | 
						|
//! You might want a system like this for abilities, buffs or consumables.
 | 
						|
//! We create four food buttons to eat with 2, 1, 10, and 4 seconds cooldown.
 | 
						|
 | 
						|
use bevy::{color::palettes::tailwind, ecs::spawn::SpawnIter, prelude::*};
 | 
						|
 | 
						|
fn main() {
 | 
						|
    App::new()
 | 
						|
        .add_plugins(DefaultPlugins)
 | 
						|
        .add_systems(Startup, setup)
 | 
						|
        .add_systems(
 | 
						|
            Update,
 | 
						|
            (
 | 
						|
                activate_ability,
 | 
						|
                animate_cooldowns.run_if(any_with_component::<ActiveCooldown>),
 | 
						|
            ),
 | 
						|
        )
 | 
						|
        .run();
 | 
						|
}
 | 
						|
 | 
						|
fn setup(
 | 
						|
    mut commands: Commands,
 | 
						|
    mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
 | 
						|
    asset_server: Res<AssetServer>,
 | 
						|
) {
 | 
						|
    commands.spawn(Camera2d);
 | 
						|
    let texture = asset_server.load("textures/food_kenney.png");
 | 
						|
    let layout = TextureAtlasLayout::from_grid(UVec2::splat(64), 7, 7, None, None);
 | 
						|
    let texture_atlas_layout = texture_atlas_layouts.add(layout);
 | 
						|
    commands.spawn((
 | 
						|
        Node {
 | 
						|
            width: Val::Percent(100.),
 | 
						|
            height: Val::Percent(100.),
 | 
						|
            align_items: AlignItems::Center,
 | 
						|
            justify_content: JustifyContent::Center,
 | 
						|
            column_gap: Val::Px(15.),
 | 
						|
            ..default()
 | 
						|
        },
 | 
						|
        Children::spawn(SpawnIter(
 | 
						|
            [
 | 
						|
                FoodItem {
 | 
						|
                    name: "an apple",
 | 
						|
                    cooldown: 2.,
 | 
						|
                    index: 2,
 | 
						|
                },
 | 
						|
                FoodItem {
 | 
						|
                    name: "a burger",
 | 
						|
                    cooldown: 1.,
 | 
						|
                    index: 23,
 | 
						|
                },
 | 
						|
                FoodItem {
 | 
						|
                    name: "chocolate",
 | 
						|
                    cooldown: 10.,
 | 
						|
                    index: 32,
 | 
						|
                },
 | 
						|
                FoodItem {
 | 
						|
                    name: "cherries",
 | 
						|
                    cooldown: 4.,
 | 
						|
                    index: 41,
 | 
						|
                },
 | 
						|
            ]
 | 
						|
            .into_iter()
 | 
						|
            .map(move |food| build_ability(food, texture.clone(), texture_atlas_layout.clone())),
 | 
						|
        )),
 | 
						|
    ));
 | 
						|
    commands.spawn((
 | 
						|
        Text::new("*Click some food to eat it*"),
 | 
						|
        Node {
 | 
						|
            position_type: PositionType::Absolute,
 | 
						|
            top: Val::Px(12.0),
 | 
						|
            left: Val::Px(12.0),
 | 
						|
            ..default()
 | 
						|
        },
 | 
						|
    ));
 | 
						|
}
 | 
						|
 | 
						|
struct FoodItem {
 | 
						|
    name: &'static str,
 | 
						|
    cooldown: f32,
 | 
						|
    index: usize,
 | 
						|
}
 | 
						|
 | 
						|
fn build_ability(
 | 
						|
    food: FoodItem,
 | 
						|
    texture: Handle<Image>,
 | 
						|
    layout: Handle<TextureAtlasLayout>,
 | 
						|
) -> impl Bundle {
 | 
						|
    let FoodItem {
 | 
						|
        name,
 | 
						|
        cooldown,
 | 
						|
        index,
 | 
						|
    } = food;
 | 
						|
    let name = Name::new(name);
 | 
						|
 | 
						|
    // Every food item is a button with a child node.
 | 
						|
    // The child node's height will be animated to be at 100% at the beginning
 | 
						|
    // of a cooldown, effectively graying out the whole button, and then getting smaller over time.
 | 
						|
    (
 | 
						|
        Node {
 | 
						|
            width: Val::Px(80.0),
 | 
						|
            height: Val::Px(80.0),
 | 
						|
            flex_direction: FlexDirection::ColumnReverse,
 | 
						|
            ..default()
 | 
						|
        },
 | 
						|
        BackgroundColor(tailwind::SLATE_400.into()),
 | 
						|
        Button,
 | 
						|
        ImageNode::from_atlas_image(texture, TextureAtlas { layout, index }),
 | 
						|
        Cooldown(Timer::from_seconds(cooldown, TimerMode::Once)),
 | 
						|
        name,
 | 
						|
        children![(
 | 
						|
            Node {
 | 
						|
                width: Val::Percent(100.),
 | 
						|
                height: Val::Percent(0.),
 | 
						|
                ..default()
 | 
						|
            },
 | 
						|
            BackgroundColor(tailwind::SLATE_50.with_alpha(0.5).into()),
 | 
						|
        )],
 | 
						|
    )
 | 
						|
}
 | 
						|
 | 
						|
#[derive(Component)]
 | 
						|
struct Cooldown(Timer);
 | 
						|
 | 
						|
#[derive(Component)]
 | 
						|
#[component(storage = "SparseSet")]
 | 
						|
struct ActiveCooldown;
 | 
						|
 | 
						|
fn activate_ability(
 | 
						|
    mut commands: Commands,
 | 
						|
    mut interaction_query: Query<
 | 
						|
        (
 | 
						|
            Entity,
 | 
						|
            &Interaction,
 | 
						|
            &mut Cooldown,
 | 
						|
            &Name,
 | 
						|
            Option<&ActiveCooldown>,
 | 
						|
        ),
 | 
						|
        (Changed<Interaction>, With<Button>),
 | 
						|
    >,
 | 
						|
    mut text: Query<&mut Text>,
 | 
						|
) -> Result {
 | 
						|
    for (entity, interaction, mut cooldown, name, on_cooldown) in &mut interaction_query {
 | 
						|
        if *interaction == Interaction::Pressed {
 | 
						|
            if on_cooldown.is_none() {
 | 
						|
                cooldown.0.reset();
 | 
						|
                commands.entity(entity).insert(ActiveCooldown);
 | 
						|
                **text.single_mut()? = format!("You ate {name}");
 | 
						|
            } else {
 | 
						|
                **text.single_mut()? = format!(
 | 
						|
                    "You can eat {name} again in {} seconds.",
 | 
						|
                    cooldown.0.remaining_secs().ceil()
 | 
						|
                );
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    Ok(())
 | 
						|
}
 | 
						|
 | 
						|
fn animate_cooldowns(
 | 
						|
    time: Res<Time>,
 | 
						|
    mut commands: Commands,
 | 
						|
    buttons: Query<(Entity, &mut Cooldown, &Children), With<ActiveCooldown>>,
 | 
						|
    mut nodes: Query<&mut Node>,
 | 
						|
) -> Result {
 | 
						|
    for (entity, mut timer, children) in buttons {
 | 
						|
        timer.0.tick(time.delta());
 | 
						|
        let cooldown = children.first().ok_or("No child")?;
 | 
						|
        if timer.0.just_finished() {
 | 
						|
            commands.entity(entity).remove::<ActiveCooldown>();
 | 
						|
            nodes.get_mut(*cooldown)?.height = Val::Percent(0.);
 | 
						|
        } else {
 | 
						|
            nodes.get_mut(*cooldown)?.height = Val::Percent((1. - timer.0.fraction()) * 100.);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    Ok(())
 | 
						|
}
 |