use bevy::utils::HashSet; use crate::prelude::*; pub(crate) struct TutorialPlugin; impl Plugin for TutorialPlugin { fn build(&self, app: &mut App) { app.add_state::() .init_resource::() .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, ( // Evaluate if a piece is selected step.run_if(state_exists::()) .run_if(not(in_state(TutorialState::None))) .run_if( // A piece changes sides any_component_changed:: // When a piece is selected, we .or_else(any_component_added::) // A piece is de-selected .or_else(any_component_removed::()) // TEMP: The user hits 'enter' .or_else(|keys: Res>| -> bool { keys.just_pressed(KeyCode::Return) }), ), ), ) .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( ui_font: Res, tweaks_file: Res, tweaks: Res>, mut commands: Commands, ) { let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweaks"); info!("Initializing tutorial entities"); let background_hex = tweak.get::("tutorial_rgba_background").unwrap(); let text_visible_hex = tweak.get::("tutorial_rgba_visible").unwrap(); // 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::hex(&background_hex).unwrap().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::hex(&text_visible_hex).unwrap(), font: ui_font.handle.clone(), }, }) .collect(), alignment: TextAlignment::Left, ..default() }, ..default() }, )); }); }); }) } #[derive(Debug, Resource, Default)] struct SeenStates(HashSet); fn step( pieces: Query<&game::Piece, Added>, transitions: Query>, curr_state: Res>, mut next_state: ResMut>, mut seen: ResMut, ) { info!("Evalute tutorial state"); // Store the current state // Used to avoid doing a prevoius state again seen.0.insert(curr_state.get().clone()); info!("Curr: {:?}, Seen: {:?}", curr_state.get(), seen); 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 => { // PERF: Doing all of this work up front results in unnecessary work let piece_selected = !pieces.is_empty(); let all_moves_done = seen.0.contains(&TutorialState::PieceIntro) && seen.0.contains(&TutorialState::PieceQueen) && seen.0.contains(&TutorialState::PieceDrone) && seen.0.contains(&TutorialState::PiecePawn); let ownership_done = seen.0.contains(&TutorialState::Ownership); let ownership_check = !ownership_done && transitions.iter().count() > 0; let jump_done = seen.0.contains(&TutorialState::PieceJump); let queen_selected = pieces.iter().filter(|&p| *p == game::Piece::Queen).count() > 0; let drone_selected = pieces.iter().filter(|&p| *p == game::Piece::Drone).count() > 0; let pawn_selected = pieces.iter().filter(|&p| *p == game::Piece::Pawn).count() > 0; // A piece is selected, so talk about it if piece_selected { // When a queen is selected for the first time... if queen_selected { TutorialState::PieceQueen // When a drone is selected for the first time... } else if drone_selected { TutorialState::PieceDrone // When a pawn is selected for the first time... } else if pawn_selected { TutorialState::PiecePawn } else { panic!("This shouldn't be possible...!") } // There are no pieces selected } else { // All move, jump, and ownership tutorials done, say goodbye if all_moves_done && jump_done && ownership_done { TutorialState::Outro } // A piece moves sides, so talk about ownership else if ownership_check { TutorialState::Ownership } // We have not touched on jumping yet else if all_moves_done && !jump_done { TutorialState::PieceJump // All pieces tutorialized, so prompt user to move pieces for more tutorial } else if all_moves_done { TutorialState::PieceEnd // 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