1use bevy::{color::palettes::tailwind, ecs::spawn::SpawnIter, prelude::*};
7
8fn main() {
9 App::new()
10 .add_plugins(DefaultPlugins)
11 .add_systems(Startup, setup)
12 .add_systems(
13 Update,
14 (
15 activate_ability,
16 animate_cooldowns.run_if(any_with_component::<ActiveCooldown>),
17 ),
18 )
19 .run();
20}
21
22fn setup(
23 mut commands: Commands,
24 mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
25 asset_server: Res<AssetServer>,
26) {
27 commands.spawn(Camera2d);
28 let texture = asset_server.load("textures/food_kenney.png");
29 let layout = TextureAtlasLayout::from_grid(UVec2::splat(64), 7, 7, None, None);
30 let texture_atlas_layout = texture_atlas_layouts.add(layout);
31 commands.spawn((
32 Node {
33 width: percent(100),
34 height: percent(100),
35 align_items: AlignItems::Center,
36 justify_content: JustifyContent::Center,
37 column_gap: px(15),
38 ..default()
39 },
40 Children::spawn(SpawnIter(
41 [
42 FoodItem {
43 name: "an apple",
44 cooldown: 2.,
45 index: 2,
46 },
47 FoodItem {
48 name: "a burger",
49 cooldown: 1.,
50 index: 23,
51 },
52 FoodItem {
53 name: "chocolate",
54 cooldown: 10.,
55 index: 32,
56 },
57 FoodItem {
58 name: "cherries",
59 cooldown: 4.,
60 index: 41,
61 },
62 ]
63 .into_iter()
64 .map(move |food| build_ability(food, texture.clone(), texture_atlas_layout.clone())),
65 )),
66 ));
67 commands.spawn((
68 Text::new("*Click some food to eat it*"),
69 Node {
70 position_type: PositionType::Absolute,
71 top: px(12),
72 left: px(12),
73 ..default()
74 },
75 ));
76}
77
78struct FoodItem {
79 name: &'static str,
80 cooldown: f32,
81 index: usize,
82}
83
84fn build_ability(
85 food: FoodItem,
86 texture: Handle<Image>,
87 layout: Handle<TextureAtlasLayout>,
88) -> impl Bundle {
89 let FoodItem {
90 name,
91 cooldown,
92 index,
93 } = food;
94 let name = Name::new(name);
95
96 (
100 Node {
101 width: px(80),
102 height: px(80),
103 flex_direction: FlexDirection::ColumnReverse,
104 ..default()
105 },
106 BackgroundColor(tailwind::SLATE_400.into()),
107 Button,
108 ImageNode::from_atlas_image(texture, TextureAtlas { layout, index }),
109 Cooldown(Timer::from_seconds(cooldown, TimerMode::Once)),
110 name,
111 children![(
112 Node {
113 width: percent(100),
114 height: percent(0),
115 ..default()
116 },
117 BackgroundColor(tailwind::SLATE_50.with_alpha(0.5).into()),
118 )],
119 )
120}
121
122#[derive(Component)]
123struct Cooldown(Timer);
124
125#[derive(Component)]
126#[component(storage = "SparseSet")]
127struct ActiveCooldown;
128
129fn activate_ability(
130 mut commands: Commands,
131 mut interaction_query: Query<
132 (
133 Entity,
134 &Interaction,
135 &mut Cooldown,
136 &Name,
137 Option<&ActiveCooldown>,
138 ),
139 (Changed<Interaction>, With<Button>),
140 >,
141 mut text: Query<&mut Text>,
142) -> Result {
143 for (entity, interaction, mut cooldown, name, on_cooldown) in &mut interaction_query {
144 if *interaction == Interaction::Pressed {
145 if on_cooldown.is_none() {
146 cooldown.0.reset();
147 commands.entity(entity).insert(ActiveCooldown);
148 **text.single_mut()? = format!("You ate {name}");
149 } else {
150 **text.single_mut()? = format!(
151 "You can eat {name} again in {} seconds.",
152 cooldown.0.remaining_secs().ceil()
153 );
154 }
155 }
156 }
157
158 Ok(())
159}
160
161fn animate_cooldowns(
162 time: Res<Time>,
163 mut commands: Commands,
164 buttons: Query<(Entity, &mut Cooldown, &Children), With<ActiveCooldown>>,
165 mut nodes: Query<&mut Node>,
166) -> Result {
167 for (entity, mut timer, children) in buttons {
168 timer.0.tick(time.delta());
169 let cooldown = children.first().ok_or("No child")?;
170 if timer.0.just_finished() {
171 commands.entity(entity).remove::<ActiveCooldown>();
172 nodes.get_mut(*cooldown)?.height = percent(0);
173 } else {
174 nodes.get_mut(*cooldown)?.height = percent((1. - timer.0.fraction()) * 100.);
175 }
176 }
177
178 Ok(())
179}