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