| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
use bevy::{ |
| 7 |
color::palettes::tailwind::{SLATE_400, SLATE_50}, |
| 8 |
image::TextureAtlasTemplate, |
| 9 |
prelude::*, |
| 10 |
ui_widgets::{Activate, Button}, |
| 11 |
}; |
| 12 |
|
| 13 |
fn 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 |
|
| 25 |
fn 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 |
|
| 32 |
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 |
|
| 46 |
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 |
|
| 58 |
|
| 59 |
fn 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 |
|
| 95 |
struct FoodItem { |
| 96 |
name: &'static str, |
| 97 |
cooldown: f32, |
| 98 |
index: usize, |
| 99 |
} |
| 100 |
|
| 101 |
|
| 102 |
fn 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 |
|
| 114 |
|
| 115 |
|
| 116 |
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 |
|
| 140 |
|
| 141 |
fn 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)] |
| 152 |
struct Cooldown(Timer); |
| 153 |
|
| 154 |
#[derive(Component)] |
| 155 |
#[component(storage = "SparseSet")] |
| 156 |
struct ActiveCooldown; |
| 157 |
|
| 158 |
|
| 159 |
fn 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 |
|
| 184 |
fn 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 |
} |