You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
martian-chess/src/tutorial.rs

354 lines
13 KiB
Rust

use bevy::utils::HashSet;
use crate::prelude::*;
pub(crate) struct TutorialPlugin;
impl Plugin for TutorialPlugin {
fn build(&self, app: &mut App) {
app.add_state::<TutorialState>()
.init_resource::<SeenStates>()
.add_systems(
OnExit(GameState::Loading),
initialize_tutorial
.run_if(resource_exists::<tweak::GameTweaks>())
.run_if(run_once()),
)
.add_systems(
Update,
menu::handle_menu_button::<TutorialState>.run_if(in_state(GameState::Menu)),
)
.add_systems(
Update,
(
// Evaluate if a piece is selected
step.run_if(state_exists::<TutorialState>())
.run_if(not(in_state(TutorialState::None)))
.run_if(
// A piece changes sides
any_component_changed::<game::Side>
// When a piece is selected, we
.or_else(any_component_added::<game::Selected>)
// A piece is de-selected
.or_else(any_component_removed::<game::Selected>())
// TEMP: The user hits 'enter'
.or_else(|keys: Res<Input<KeyCode>>| -> bool {
keys.just_pressed(KeyCode::Return)
}),
),
),
)
.add_systems(OnExit(GameState::Play), deactivate::<TutorialState>)
.add_systems(
OnEnter(GameState::Play),
activate_tutorial_step.run_if(state_exists::<TutorialState>()),
)
.add_systems(
Update,
activate_tutorial_step.run_if(state_changed::<TutorialState>()),
);
}
}
#[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<ui::UiFont>,
tweaks_file: Res<tweak::GameTweaks>,
tweaks: Res<Assets<tweak::Tweaks>>,
mut commands: Commands,
) {
let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweaks");
info!("Initializing tutorial entities");
let background_hex = tweak.get::<String>("tutorial_rgba_background").unwrap();
let text_visible_hex = tweak.get::<String>("tutorial_rgba_visible").unwrap();
// List of (state, lines) for tutorial steps
[
(
TutorialState::Intro,
tweak
.get::<Vec<String>>("tutorial_intro")
.expect("Tutorial intro"),
),
(
TutorialState::Objective,
tweak
.get::<Vec<String>>("tutorial_objective")
.expect("Tutorial objective"),
),
(
TutorialState::Ownership,
tweak
.get::<Vec<String>>("tutorial_ownership")
.expect("Tutorial ownership"),
),
(
TutorialState::_Promotions,
tweak
.get::<Vec<String>>("tutorial_promotions")
.expect("Tutorial promotions"),
),
(
TutorialState::Outro,
tweak
.get::<Vec<String>>("tutorial_outro")
.expect("Tutorial pieces end"),
),
(
TutorialState::PieceIntro,
tweak
.get::<Vec<String>>("tutorial_pieces_prompt")
.expect("Tutorial pieces prompt"),
),
(
TutorialState::PieceJump,
tweak
.get::<Vec<String>>("tutorial_pieces_jumping")
.expect("Tutorial pieces jumping"),
),
(
TutorialState::PieceEnd,
tweak
.get::<Vec<String>>("tutorial_pieces_end")
.expect("Tutorial pieces end"),
),
(
TutorialState::PieceQueen,
tweak
.get::<Vec<String>>("tutorial_pieces_queen")
.expect("Tutorial pieces queen"),
),
(
TutorialState::PieceDrone,
tweak
.get::<Vec<String>>("tutorial_pieces_drone")
.expect("Tutorial pieces drone"),
),
(
TutorialState::PiecePawn,
tweak
.get::<Vec<String>>("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<TutorialState>);
fn step(
pieces: Query<&game::Piece, Added<game::Selected>>,
transitions: Query<Entity, Changed<game::Side>>,
curr_state: Res<State<TutorialState>>,
mut next_state: ResMut<NextState<TutorialState>>,
mut seen: ResMut<SeenStates>,
) {
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<State<TutorialState>>,
mut query: Query<(&mut Visibility, &TutorialState), Without<GameState>>,
) {
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