use bevy::utils::HashSet; use crate::prelude::*; pub(crate) struct TutorialPlugin; impl Plugin for TutorialPlugin { fn build(&self, app: &mut App) { app.add_state::() .add_systems( OnExit(GameState::Loading), initialize_tutorial .run_if(resource_exists::()) .run_if(run_once()), ) .add_systems( Update, menu::handle_menu_button::.run_if(in_state(GameState::Menu)), ) .add_systems( Update, ( // Run if we have a TutorialState *and* it is not TutorialState::None *and* we get a return keypress transition .run_if(state_exists::()) .run_if(not(in_state(TutorialState::None))), transition .run_if(state_exists::()) .run_if(not(in_state(TutorialState::None))) .run_if(any_component_added::), transition .run_if(state_exists::()) .run_if(not(in_state(TutorialState::None))) .run_if(any_component_changed::), ), ) .add_systems(OnExit(GameState::Play), deactivate::) .add_systems( OnEnter(GameState::Play), activate_tutorial_step.run_if(state_exists::()), ) .add_systems( Update, activate_tutorial_step.run_if(state_changed::()), ); } } #[derive(Debug, States, Hash, Default, PartialEq, Eq, Clone, Component)] pub(crate) enum TutorialState { #[default] None, Empty, Intro, Objective, PieceIntro, PieceQueen, PieceDrone, PiecePawn, PieceJump, PieceEnd, Ownership, _Promotions, Outro, } // Assertion: "Tutorial" just starts a regular game with TutorialState set to Intro // This plays out a usual game, but with tutorial text overlayed // Once tutorial is done, the game can continue as usual // Init animations, initialize all text windows but make invisible // This should be run when TutorialState::Intro fn initialize_tutorial( tweaks_file: Res, tweaks: Res>, mut commands: Commands, ) { let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweaks"); info!("Initializing tutorial entities"); // List of (state, lines) for tutorial steps [ ( TutorialState::Intro, tweak .get::>("tutorial_intro") .expect("Tutorial intro"), ), ( TutorialState::Objective, tweak .get::>("tutorial_objective") .expect("Tutorial objective"), ), ( TutorialState::Ownership, tweak .get::>("tutorial_ownership") .expect("Tutorial ownership"), ), ( TutorialState::_Promotions, tweak .get::>("tutorial_promotions") .expect("Tutorial promotions"), ), ( TutorialState::Outro, tweak .get::>("tutorial_outro") .expect("Tutorial pieces end"), ), ( TutorialState::PieceIntro, tweak .get::>("tutorial_pieces_prompt") .expect("Tutorial pieces prompt"), ), ( TutorialState::PieceJump, tweak .get::>("tutorial_pieces_jumping") .expect("Tutorial pieces jumping"), ), ( TutorialState::PieceEnd, tweak .get::>("tutorial_pieces_end") .expect("Tutorial pieces end"), ), ( TutorialState::PieceQueen, tweak .get::>("tutorial_pieces_queen") .expect("Tutorial pieces queen"), ), ( TutorialState::PieceDrone, tweak .get::>("tutorial_pieces_drone") .expect("Tutorial pieces drone"), ), ( TutorialState::PiecePawn, tweak .get::>("tutorial_pieces_pawn") .expect("Tutorial pieces pawn"), ), ] .iter() .for_each(|(step, lines)| { commands .spawn(( step.clone(), NodeBundle { style: Style { width: Val::Percent(100.0), height: Val::Percent(100.0), justify_content: JustifyContent::FlexStart, 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(( step.clone(), NodeBundle { style: Style { padding: UiRect::all(Val::Px(25.0)), ..default() }, background_color: Color::BLACK.with_a(0.9).into(), ..default() }, )) .with_children(|parent| { parent.spawn(( step.clone(), TextBundle { text: Text { sections: lines .iter() .map(|line| TextSection { value: line.clone(), style: TextStyle { font_size: 16.0, color: Color::WHITE.with_a(1.0), ..default() }, }) .collect(), alignment: TextAlignment::Left, ..default() }, ..default() }, )); }); }); }) } fn transition( pieces: Query<&game::Piece, With>, transitions: Query>, curr_state: Res>, mut next_state: ResMut>, keys: Res>, mut seen: Local>, ) { // TEMP: Early out, only handle Return keys if !keys.just_pressed(KeyCode::Return) { return; } info!("Transitioning tutorial state"); // Store the current state // Used to avoid doing a prevoius state again (*seen).insert(curr_state.get().clone()); next_state.set(match curr_state.get() { // This transition is implicit. Menu transitions from None to Intro TutorialState::None => TutorialState::Intro, // From Intro we always go to Objective TutorialState::Intro => TutorialState::Objective, // From objective we go to the Pieces flow intro TutorialState::Objective => TutorialState::PieceIntro, // From PiecesIntro we can go to any other Pieces if a piece is selected // Each pieces tutorial can transition to any other, as well as Ownership TutorialState::PieceIntro | TutorialState::PieceQueen | TutorialState::PieceDrone | TutorialState::PiecePawn | TutorialState::PieceEnd | TutorialState::PieceJump | TutorialState::Empty | TutorialState::Ownership => { // When a queen is selected for the first time... if pieces.iter().filter(|&p| *p == game::Piece::Queen).count() > 0 && !(*seen).contains(&TutorialState::PieceQueen) { TutorialState::PieceQueen // When a drone is selected for the first time... } else if pieces.iter().filter(|&p| *p == game::Piece::Drone).count() > 0 && !(*seen).contains(&TutorialState::PieceDrone) { TutorialState::PieceDrone // When a pawn is selected for the first time... } else if pieces.iter().filter(|&p| *p == game::Piece::Pawn).count() > 0 && !(*seen).contains(&TutorialState::PiecePawn) { TutorialState::PiecePawn // All pieces have been selected } else if (*seen).contains(&TutorialState::PieceIntro) && (*seen).contains(&TutorialState::PieceQueen) && (*seen).contains(&TutorialState::PieceDrone) && (*seen).contains(&TutorialState::PiecePawn) { // And we have touched on jumping if (*seen).contains(&TutorialState::PieceJump) { TutorialState::PieceEnd // We have not touched on jumping } else { TutorialState::PieceJump } // A piece moves sides, so talk about ownership } else if !(*seen).contains(&TutorialState::Ownership) && transitions.iter().count() > 0 { TutorialState::Ownership // We have visited all relevant tutorial points, say goodbye } else if (*seen).contains(&TutorialState::PieceIntro) && (*seen).contains(&TutorialState::PieceQueen) && (*seen).contains(&TutorialState::PieceDrone) && (*seen).contains(&TutorialState::PiecePawn) && (*seen).contains(&TutorialState::PieceJump) && (*seen).contains(&TutorialState::Ownership) { TutorialState::Outro // Default, empty (tutorial doesn't always need to show something) } else { TutorialState::Empty } } // After the outro, we exit the tutorial TutorialState::Outro => TutorialState::None, TutorialState::_Promotions => todo!("Not implemented yet!"), }); } fn activate_tutorial_step( state: Res>, mut query: Query<(&mut Visibility, &TutorialState), Without>, ) { info!("Activating step {:?}", state.get()); // Iterate over all entities with TutorialState components query.iter_mut().for_each(|(mut v, s)| { // If their component matches the current state, make visible if s == state.get() { *v = Visibility::Inherited; // Otherwise hide it (again) } else { *v = Visibility::Hidden; } }); } // Start by loading the same game as display3d does // Show the intro text when entering Intro TutorialState // Continue // Show objective when entering Objective TutorialState // Continue // Prompt user to click some pieces // When each piece type is clicked, show relevant text // Once all pieces are described say jumping text // Finally end with final text // Continue // When a piece crosses the canal, show ownership text // Continue // A few moves in, show field promotions text // Continue // End with outro text // Continue // Play game out as usual // If player early exits, set TutorialState to None