use bevy::core::FrameCount; use crate::prelude::*; pub(crate) struct IntroPlugin; impl Plugin for IntroPlugin { fn build(&self, app: &mut App) { app.add_systems( OnExit(GameState::Loading), init_intro_text.run_if(resource_exists::()), ) .init_resource::() .add_systems(OnEnter(GameState::Intro), activate::) .add_systems(OnExit(GameState::Intro), deactivate::) .add_systems(Update, manage_intro_progress .run_if(in_state(GameState::Intro)) .run_if(not(resource_exists::())) ) .add_systems(Update, play_intro .run_if(in_state(GameState::Intro)) .run_if(resource_changed::()) ) // Continue to play state if the intro is done playing out .add_systems( Update, continue_to_play .run_if(|keys: Res>| -> bool { keys.just_pressed(KeyCode::Return) }) .run_if(resource_exists::()), ); } } #[derive(Debug, Component)] struct Intro; #[derive(Debug, Resource)] struct IntroPlayed; #[derive(Debug, Resource, Default)] struct IntroProgress(usize); // Draw the intro text (invisible) on startup // Requires the Tweakfile to be loaded fn init_intro_text( tweaks_file: Res, tweaks: Res>, mut commands: Commands, ) { let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweaks"); let text = tweak.get::("intro_text").expect("Intro text"); commands .spawn(( Intro, NodeBundle { style: Style { width: Val::Percent(100.0), height: Val::Percent(100.0), justify_content: JustifyContent::Center, align_items: AlignItems::Center, flex_direction: FlexDirection::Column, position_type: PositionType::Absolute, padding: UiRect::all(Val::Px(50.0)), ..default() }, background_color: Color::NONE.into(), visibility: Visibility::Hidden, ..default() }, )) .with_children(|parent| { parent.spawn(( Intro, TextBundle::from_sections(text.chars().into_iter().map(|c| TextSection { value: c.to_string(), style: TextStyle { font_size: 16.0, color: Color::WHITE.with_a(0.0), ..default() }, })) .with_text_alignment(TextAlignment::Center), )); }); } fn manage_intro_progress( keys: Res>, mut start: Local, mut progress: ResMut, framecount: Res, mut commands: Commands, tweaks_file: Res, tweaks: Res>, ) { // If this is the first run, initialize the starting point if *start == 0 { *start = framecount.0; } // If the user hits 'return' set this to the end of the animation if keys.just_pressed(KeyCode::Return) { progress.0 = usize::MAX; commands.insert_resource(IntroPlayed); // Otherwise progress by N characters (1) } else { let tweak = tweaks .get(tweaks_file.handle.clone()) .expect("Load tweaks"); let rate = tweak.get::("intro_rate").expect("[intro] rate = #"); progress.0 = ((framecount.0 - *start) / rate) as usize; } } // Upon entering the Intro state, start the intro "animation" fn play_intro( mut text: Query<(&mut Text, &mut Visibility), With>, progress: Res, mut commands: Commands, ) { // Iterate over all (one) text text.iter_mut().for_each(|(mut t, mut v)| { // If this is the first frame of the animation, make the object visibility if progress.0 == 0 { *v = Visibility::Inherited; } // Iterate over all characters in the intro text t.sections .iter_mut() .enumerate() // Only operate on sections up to this point .filter_map(|(i, s)| { (i <= progress.0).then_some(s) }) // Set the alpha to 1.0 making it visible .for_each(|s| { s.style.color.set_a(1.0); }); if t.sections.iter().all(|s| s.style.color.a() == 1.0) { commands.insert_resource(IntroPlayed); } }); } // Intro animation reveals one character every nth frame // Hit enter to skip the "animation" // Hit enter once the animation is complete to start the game fn continue_to_play(mut next_state: ResMut>) { next_state.set(GameState::Play) }