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.
426 lines
18 KiB
Rust
426 lines
18 KiB
Rust
use crate::prelude::*;
|
|
|
|
pub(crate) struct TutorialPlugin;
|
|
|
|
impl Plugin for TutorialPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.init_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,
|
|
(
|
|
// Evaluate if a piece is selected
|
|
step.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(
|
|
just_pressed(KeyCode::Enter)
|
|
.or_else(just_pressed(MouseButton::Left)),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
// Manage visible/hidden states
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
manage_state_entities::<TutorialState>(),
|
|
activate_tutorial_step,
|
|
)
|
|
.run_if(not(in_state(GameState::Loading)))
|
|
.run_if(state_changed::<TutorialState>),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[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<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();
|
|
let button_handle = tweak.get_handle::<Image>("buttons_image_resting").unwrap();
|
|
let font_handle = tweak.get_handle::<Font>("buttons_font").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::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: 12.0,
|
|
color: Color::hex(&text_visible_hex).unwrap(),
|
|
font: ui_font.handle.clone(),
|
|
},
|
|
})
|
|
.collect(),
|
|
justify: JustifyText::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: 10.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: 8.0,
|
|
font: font_handle.clone(),
|
|
},
|
|
}],
|
|
..default()
|
|
},
|
|
style: Style {
|
|
margin: UiRect::all(Val::Px(20.0)),
|
|
..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);
|
|
|
|
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::Promotions
|
|
| 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 promotions_done = seen.0.contains(&TutorialState::Promotions);
|
|
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 && promotions_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
|
|
}
|
|
// We have not touched on field promotions yet
|
|
else if all_moves_done && !promotions_done {
|
|
TutorialState::Promotions
|
|
}
|
|
// 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!"),
|
|
}
|
|
}
|
|
|
|
fn activate_tutorial_step(
|
|
state: Res<State<TutorialState>>,
|
|
mut query: Query<(&mut Visibility, &TutorialState), Without<GameState>>,
|
|
) {
|
|
info!("Activating tutorial 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;
|
|
}
|
|
});
|
|
} |