use bevy::time::Stopwatch; use crate::prelude::*; pub(crate) struct IntroPlugin; impl Plugin for IntroPlugin { fn build(&self, app: &mut App) { app.init_resource::() .init_resource::() .add_systems( OnExit(GameState::Loading), init_intro_text .run_if(resource_exists::) .run_if(run_once()), ) .add_systems(OnEnter(GameState::Intro), manage_intro) .add_systems(OnExit(GameState::Intro), cleanup_intro) // All of these run during GameState::Intro .add_systems( Update, ( manage_intro.run_if(any_component_removed::()), // Started when the TextScrollAnimation component is added to the parent entity // Updated for as long as there is scrolling text manage_scroll_text_animation.run_if( any_component_added::() .or_else(just_pressed(KeyCode::Enter)) .or_else(just_pressed(MouseButton::Left)), ), // Play intro manages playing the intro of each individual paragraph // Runs every time the TextScroll component (managed by manage_scroll_text_animation) is updated scroll_text.run_if(any_with_component::), set_text_animation_speed .run_if( just_pressed(KeyCode::Enter) .or_else(just_pressed(MouseButton::Left)) .or_else(just_released(KeyCode::Enter)) .or_else(just_released(MouseButton::Left)) ) ) .run_if(in_state(GameState::Intro)), ); } } #[derive(Debug, Component)] struct IntroUi; #[derive(Debug, Resource)] struct TextAnimationSpeed(f32); impl Default for TextAnimationSpeed { fn default() -> Self { TextAnimationSpeed(5.0) } } fn set_text_animation_speed( mut animation_speed: ResMut, tweaks: Res>, tweaks_file: Res, keys: Res>, mouse: Res>, ) { *animation_speed = if keys.just_pressed(KeyCode::Enter) || mouse.just_pressed(MouseButton::Left) { let tweak = tweaks .get(tweaks_file.handle.clone()) .expect("Load tweakfile"); let speed = tweak.get::("animation_fast_text_speed").unwrap(); TextAnimationSpeed(speed) } else { TextAnimationSpeed::default() }; debug!("Set animation speeds {:?}", *animation_speed); } // Draw the intro text (invisible) on startup // Requires the Tweakfile to be loaded fn init_intro_text( tweaks_file: Res, tweaks: Res>, ui_font: Res, mut commands: Commands, ) { let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweaks"); let texts = tweak.get::>("intro_text").expect("Intro text"); let background_hex = tweak.get::("intro_rgba_background").unwrap(); let text_hidden_hex = tweak.get::("intro_rgba_hidden").unwrap(); commands .spawn(( IntroUi, NodeBundle { style: Style { width: Val::Percent(100.0), height: Val::Percent(100.0), justify_content: JustifyContent::Center, align_items: AlignItems::Center, position_type: PositionType::Absolute, padding: UiRect::all(Val::Px(50.0)), ..default() }, background_color: Color::NONE.into(), visibility: Visibility::Hidden, ..default() }, )) .with_children(|parent| { texts.iter().for_each(|text| { parent .spawn(( IntroUi, NodeBundle { style: Style { position_type: PositionType::Absolute, padding: UiRect::all(Val::Px(25.0)), ..default() }, visibility: Visibility::Hidden, background_color: Color::hex(&background_hex).unwrap().into(), ..default() }, )) .with_children(|parent| { parent.spawn(( IntroUi, TextBundle { text: Text { sections: text .chars() .map(|c| TextSection { value: c.to_string(), style: TextStyle { font_size: 16.0, color: Color::hex(&text_hidden_hex).unwrap(), font: ui_font.handle.clone(), }, }) .collect(), justify: JustifyText::Center, ..default() }, ..default() }, )); }); }); }); } fn manage_intro( // Hack, this way of "finding" the root Node containing our paragraphs is precarious query: Query, With, Without)>, mut commands: Commands, ) { debug!("Managing intro"); query.iter().for_each(|e| { commands .entity(e) .insert(ui::TextScrollAnimation) .insert(Visibility::Visible); }); } fn cleanup_intro( query: Query>, mut texts: Query<&mut Text, With>, mut curr: ResMut, tweaks_file: Res, tweaks: Res>, mut commands: Commands, ) { debug!("Cleaning up intro"); query.iter().for_each(|e| { commands .entity(e) .remove::() .remove::() .insert(Visibility::Hidden); }); { // Reset text colors let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweaks"); let text_hidden_hex = tweak.get::("intro_rgba_hidden").unwrap(); let text_hidden_color = Color::hex(text_hidden_hex).unwrap(); texts.iter_mut().for_each(|mut text| { text.sections.iter_mut().for_each(|s| { s.style.color = text_hidden_color; }); }); } curr.0 = None; } #[derive(Debug, Resource, Default)] struct CurrentIntroParagraph(Option); fn manage_scroll_text_animation( roots: Query< Entity, Or<( With, Added, )>, >, texts: Query, With)>, animated_texts: Query<&ui::TextScroll>, parents: Query<&Parent>, children: Query<&Children>, framecount: Res, mut next_state: ResMut>, mut curr: ResMut, mut commands: Commands, mut speed: ResMut, mut keys: ResMut>, mut mouse: ResMut>, ) { debug!("Managing scroll text animation"); roots.iter().for_each(|r| { debug!("Processing {:?} at frame {:?}", r, framecount.0); // Skip to next paragraph only the current paragraph's animation is complete if animated_texts.is_empty() { debug!( "No animations playing, moving on to next paragraph {:?}", curr.0 ); // Create an iterator over all paragraphs let mut paragraphs = texts.iter_many(children.iter_descendants(r)); if curr.0.is_some() { // Hide the last active entity if let Some(e) = curr.0 { let p = parents.get(e).unwrap(); commands.entity(p.get()).insert(Visibility::Hidden); commands.entity(e).insert(Visibility::Hidden); *speed = TextAnimationSpeed::default(); keys.clear(); mouse.clear(); } // Locate the last entity we were operating with for entity in paragraphs.by_ref() { if curr.0 == Some(entity) { break; } } } // Operate on the next entity in the list curr.0 = paragraphs.next(); debug!("Curr: {:?}", curr.0); // Progress to the next paragraph if let Some(e) = curr.0 { commands.entity(e).insert(ui::TextScroll { progress: 0, stopwatch: Stopwatch::new(), }); // Continue to the title } else { commands.entity(r).remove::(); next_state.set(GameState::Title); } } }); } fn scroll_text( mut texts: Query<(Entity, &mut Visibility, &mut Text, &mut ui::TextScroll)>, tweaks_file: Res, tweaks: Res>, time: Res