1use std::marker::PhantomData;
15
16use bevy::{dev_tools::states::*, ecs::schedule::ScheduleLabel, prelude::*};
17
18use custom_transitions::*;
19
20#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
21enum AppState {
22 #[default]
23 Menu,
24 InGame,
25}
26
27fn main() {
28 App::new()
29 .add_plugins((
31 DefaultPlugins,
32 IdentityTransitionsPlugin::<AppState>::default(),
33 ))
34 .init_state::<AppState>()
35 .add_systems(Startup, setup)
36 .add_systems(OnEnter(AppState::Menu), setup_menu)
37 .add_systems(Update, menu.run_if(in_state(AppState::Menu)))
38 .add_systems(OnExit(AppState::Menu), cleanup_menu)
39 .add_systems(OnReenter(AppState::InGame), setup_game)
41 .add_systems(OnReexit(AppState::InGame), teardown_game)
42 .add_systems(
44 Update,
45 ((movement, change_color, trigger_game_restart).run_if(in_state(AppState::InGame)),),
46 )
47 .add_systems(Update, log_transitions::<AppState>)
48 .run();
49}
50
51mod custom_transitions {
53 use crate::*;
54
55 #[derive(Default)]
61 pub struct IdentityTransitionsPlugin<S: States>(PhantomData<S>);
62
63 impl<S: States> Plugin for IdentityTransitionsPlugin<S> {
64 fn build(&self, app: &mut App) {
65 app.add_systems(
66 StateTransition,
67 last_transition::<S>
70 .pipe(run_reenter::<S>)
72 .in_set(EnterSchedules::<S>::default()),
79 )
80 .add_systems(
81 StateTransition,
82 last_transition::<S>
83 .pipe(run_reexit::<S>)
84 .in_set(ExitSchedules::<S>::default()),
85 );
86 }
87 }
88
89 #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
91 pub struct OnReenter<S: States>(pub S);
92
93 fn run_reenter<S: States>(transition: In<Option<StateTransitionEvent<S>>>, world: &mut World) {
96 let Some(transition) = transition.0 else {
98 return;
99 };
100
101 let Some(entered) = transition.entered else {
109 return;
110 };
111
112 let _ = world.try_run_schedule(OnReenter(entered));
114
115 }
125
126 #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
128 pub struct OnReexit<S: States>(pub S);
129
130 fn run_reexit<S: States>(transition: In<Option<StateTransitionEvent<S>>>, world: &mut World) {
131 let Some(transition) = transition.0 else {
132 return;
133 };
134 let Some(exited) = transition.exited else {
135 return;
136 };
137
138 let _ = world.try_run_schedule(OnReexit(exited));
139 }
140}
141
142fn menu(
143 mut next_state: ResMut<NextState<AppState>>,
144 mut interaction_query: Query<
145 (&Interaction, &mut BackgroundColor),
146 (Changed<Interaction>, With<Button>),
147 >,
148) {
149 for (interaction, mut color) in &mut interaction_query {
150 match *interaction {
151 Interaction::Pressed => {
152 *color = PRESSED_BUTTON.into();
153 next_state.set(AppState::InGame);
154 }
155 Interaction::Hovered => {
156 *color = HOVERED_BUTTON.into();
157 }
158 Interaction::None => {
159 *color = NORMAL_BUTTON.into();
160 }
161 }
162 }
163}
164
165fn cleanup_menu(mut commands: Commands, menu_data: Res<MenuData>) {
166 commands.entity(menu_data.button_entity).despawn();
167}
168
169const SPEED: f32 = 100.0;
170fn movement(
171 time: Res<Time>,
172 input: Res<ButtonInput<KeyCode>>,
173 mut query: Query<&mut Transform, With<Sprite>>,
174) {
175 for mut transform in &mut query {
176 let mut direction = Vec3::ZERO;
177 if input.pressed(KeyCode::ArrowLeft) {
178 direction.x -= 1.0;
179 }
180 if input.pressed(KeyCode::ArrowRight) {
181 direction.x += 1.0;
182 }
183 if input.pressed(KeyCode::ArrowUp) {
184 direction.y += 1.0;
185 }
186 if input.pressed(KeyCode::ArrowDown) {
187 direction.y -= 1.0;
188 }
189
190 if direction != Vec3::ZERO {
191 transform.translation += direction.normalize() * SPEED * time.delta_secs();
192 }
193 }
194}
195
196fn change_color(time: Res<Time>, mut query: Query<&mut Sprite>) {
197 for mut sprite in &mut query {
198 let new_color = LinearRgba {
199 blue: ops::sin(time.elapsed_secs() * 0.5) + 2.0,
200 ..LinearRgba::from(sprite.color)
201 };
202
203 sprite.color = new_color.into();
204 }
205}
206
207fn trigger_game_restart(
211 input: Res<ButtonInput<KeyCode>>,
212 mut next_state: ResMut<NextState<AppState>>,
213) {
214 if input.just_pressed(KeyCode::KeyR) {
215 next_state.set(AppState::InGame);
218 }
219}
220
221fn setup(mut commands: Commands) {
222 commands.spawn(Camera2d);
223}
224
225fn setup_game(mut commands: Commands, asset_server: Res<AssetServer>) {
226 commands.spawn(Sprite::from_image(asset_server.load("branding/icon.png")));
227 info!("Setup game");
228}
229
230fn teardown_game(mut commands: Commands, player: Single<Entity, With<Sprite>>) {
231 commands.entity(*player).despawn();
232 info!("Teardown game");
233}
234
235#[derive(Resource)]
236struct MenuData {
237 pub button_entity: Entity,
238}
239
240const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
241const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
242const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
243
244fn setup_menu(mut commands: Commands) {
245 let button_entity = commands
246 .spawn((
247 Node {
248 width: Val::Percent(100.),
250 height: Val::Percent(100.),
251 justify_content: JustifyContent::Center,
252 align_items: AlignItems::Center,
253 ..default()
254 },
255 children![(
256 Button,
257 Node {
258 width: Val::Px(150.),
259 height: Val::Px(65.),
260 justify_content: JustifyContent::Center,
262 align_items: AlignItems::Center,
264 ..default()
265 },
266 BackgroundColor(NORMAL_BUTTON),
267 children![(
268 Text::new("Play"),
269 TextFont {
270 font_size: 33.0,
271 ..default()
272 },
273 TextColor(Color::srgb(0.9, 0.9, 0.9)),
274 )]
275 )],
276 ))
277 .id();
278 commands.insert_resource(MenuData { button_entity });
279}