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, ( // Evaluate if a piece is selected step.run_if( state_exists::().and_then( // 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( just_pressed(KeyCode::Return) .or_else(just_pressed(MouseButton::Left)), ), ), ), ), ) // Manage visible/hidden states .add_systems( Update, manage_state_entities::().run_if(state_changed::()), ) .add_systems( Update, activate_tutorial_step.run_if( state_exists::().and_then(state_changed::()), ), ); } } #[derive(Debug, States, Hash, Default, PartialEq, Eq, Clone, Component)] pub(crate) enum TutorialState { #[default] None, Intro, Objective, Empty, 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(); let button_handle = tweak.get_handle::("buttons_image_resting").unwrap(); let font_handle = tweak.get_handle::("buttons_font").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::Center, align_items: AlignItems::FlexEnd, flex_direction: FlexDirection::Column, position_type: PositionType::Absolute, padding: UiRect::all(Val::Px(25.0)), ..default() }, background_color: Color::NONE.into(), visibility: Visibility::Hidden, ..default() }, )) .with_children(|parent| { parent .spawn(( step.clone(), NodeBundle { style: Style { width: Val::Percent(33.0), max_height: Val::Percent(100.0), padding: UiRect::all(Val::Px(25.0)), flex_direction: FlexDirection::Column, ..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: 8.0, color: Color::hex(&text_visible_hex).unwrap(), font: ui_font.handle.clone(), }, }) .collect(), alignment: TextAlignment::Left, ..default() }, ..default() }, )); if *step == TutorialState::Outro { parent .spawn(NodeBundle { style: Style { flex_direction: FlexDirection::Row, ..default() }, ..default() }) .with_children(|parent| { parent .spawn(( menu::ButtonAction(tutorial::TutorialState::None), menu::ButtonAction(GameState::Restart), menu::ButtonAction(menu::MenuState::Off), ButtonBundle { style: Style { padding: UiRect::all(Val::Px(5.0)), margin: UiRect::all(Val::Px(5.0)), ..default() }, image: UiImage { texture: button_handle.clone(), ..default() }, ..default() }, )) .with_children(|parent| { parent.spawn(TextBundle { text: Text { sections: vec![TextSection { value: "R e s t a r t".into(), style: TextStyle { color: Color::WHITE, font_size: 8.0, font: font_handle.clone(), }, }], ..default() }, style: Style { margin: UiRect::all(Val::Px(20.0)), ..default() }, ..default() }); }); parent .spawn(( menu::ButtonAction(tutorial::TutorialState::None), menu::ButtonAction(GameState::Play), menu::ButtonAction(menu::MenuState::Off), ButtonBundle { style: Style { padding: UiRect::all(Val::Px(5.0)), margin: UiRect::all(Val::Px(5.0)), ..default() }, image: UiImage { texture: button_handle.clone(), ..default() }, ..default() }, )) .with_children(|parent| { parent.spawn(TextBundle { text: Text { sections: vec![TextSection { value: "C o n t i n u e".into(), style: TextStyle { color: Color::WHITE, font_size: 12.0, font: font_handle.clone(), }, }], ..default() }, style: Style { margin: UiRect::all(Val::Px(20.0)), ..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); match curr_state.get() { // None does not go to Intro, that is controlled by the button handler TutorialState::None => next_state.set(TutorialState::None), // From Intro we always go to Objective TutorialState::Intro => next_state.set(TutorialState::Objective), // From objective we go to the Pieces flow intro TutorialState::Objective => next_state.set(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 queen_seen = seen.0.contains(&TutorialState::PieceQueen); let drone_selected = pieces.iter().filter(|&p| *p == game::Piece::Drone).count() > 0; let drone_seen = seen.0.contains(&TutorialState::PieceDrone); let pawn_selected = pieces.iter().filter(|&p| *p == game::Piece::Pawn).count() > 0; let pawn_seen = seen.0.contains(&TutorialState::PiecePawn); // A piece is selected, so talk about it let next = if piece_selected { // When a queen is selected for the first time... if queen_selected && !queen_seen { TutorialState::PieceQueen // When a drone is selected for the first time... } else if drone_selected && !drone_seen { TutorialState::PieceDrone // When a pawn is selected for the first time... } else if pawn_selected && !pawn_seen { TutorialState::PiecePawn } else { TutorialState::Empty } // 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::PieceIntro } }; next_state.set(next); } // After the outro, we exit the tutorial TutorialState::Outro => error!("Press a button!"), 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; } }); }