Recognize an "end state" for the game

Introduce some sort of "Goal" in a level.
When that goal is is "Achieved" and the level.
If the player chooses, go to the next level.

Requires:
* A goal tracker
* A level counter
* A state enum:
    * "InPlay": Playing the game
    * "LevelComplete": Completed the objective
        * Can progress to the next level
    * "GameOver": Failed the objective
    * "Paused": Gameplay is temporarily paused

Once a level is complete, there should be:
* A new goal generated for the next level.
* A button to progress to the next level.
main
Elijah Voigt 4 weeks ago
parent 6fd9550975
commit 1074555bab

@ -0,0 +1,15 @@
# Tetris Battle
- Recognize finishing a level successfully
- Recognize failing a level
- Art pass
- Nicer preview for "Next" and "Swap" Shapes
- Use a timer resource for next step
## Bugs
- Two blocks in the same position
- Skipping doesn't work 100% of the time
- A little delay after skipping
## Nice to haves
- Fix tests after Shape -> ShapeLayout refactor

@ -27,6 +27,7 @@ fn main() {
.init_resource::<ShapeStore>()
.init_resource::<Score>()
.init_resource::<OutputImages>()
.init_resource::<LevelGoal>()
.add_systems(
Startup,
(
@ -37,6 +38,10 @@ fn main() {
init_battler,
),
)
.add_systems(
Update,
game_state_machine.run_if(state_changed::<GameState>)
)
// Input and basic systems
.add_systems(
Update,
@ -52,9 +57,10 @@ fn main() {
.run_if(resource_changed::<ShapesBuffer>.or(resource_added::<ShapesBuffer>)),
add_piece
.run_if(not(any_with_component::<Shape>))
.run_if(in_state(GameState::Play))
.after(update_next_shapes),
falling
.run_if(in_state(GameState::Falling))
.run_if(in_state(GameState::Play))
.run_if(on_timer(Duration::from_secs(1))),
update_position.run_if(any_component_changed::<GridPosition>),
update_shape_blocks
@ -74,6 +80,7 @@ fn main() {
damage_on_place_shape.run_if(any_component_removed::<Shape>),
damage_on_clear_line.run_if(any_component_removed::<LineBlock>),
damage_over_time.run_if(on_timer(Duration::from_secs(5))),
check_level_goal.run_if(resource_changed::<Score>),
),
)
// UI systems
@ -83,6 +90,7 @@ fn main() {
sync_resource_to_ui::<ShapesBuffer>.run_if(resource_changed::<ShapesBuffer>),
sync_resource_to_ui::<ShapeStore>.run_if(resource_changed::<ShapeStore>),
sync_resource_to_ui::<Score>.run_if(resource_changed::<Score>),
sync_resource_to_ui::<LevelGoal>.run_if(resource_changed::<LevelGoal>),
sync_singleton_to_ui::<Shape>.run_if(any_component_changed::<Shape>),
),
)
@ -221,9 +229,87 @@ impl std::ops::AddAssign<&GridPosition> for GridPosition {
#[derive(States, Clone, Eq, PartialEq, Debug, Hash, Default, Component)]
enum GameState {
// Sets up a new game
#[default]
Falling,
NewGame,
// Actively playing the game
Play,
// Temporarily paused gameplay
Pause,
// Reached level goal
LevelComplete,
// Sets up next level
NextLevel,
// Failed level
GameOver,
}
// The core of the Game state machine, coordinates other stuff happening
fn game_state_machine(
curr: Res<State<GameState>>,
mut next: ResMut<NextState<GameState>>,
mut goal: ResMut<LevelGoal>,
mut score: ResMut<Score>,
items: Query<Entity, (With<GridPosition>, Or<(With<Block>, With<Shape>)>)>,
mut commands: Commands,
) {
match curr.get() {
GameState::NewGame => {
// Set new game goal
*goal = LevelGoal::Score(3);
// Reset score
*score = Score(0);
// Clear board
items.iter().for_each(|i| {
commands.entity(i).despawn();
});
// Set goal based on current goal
next.set(GameState::Play);
},
GameState::Play => (), // handled by user input
GameState::Pause => (), // handled by user input
GameState::LevelComplete => (), // hanled by user input
GameState::NextLevel => {
// Increase goal
*goal = match *goal {
LevelGoal::Score(n) => LevelGoal::Score(n + 5),
};
// Reset score
*score = Score(0);
// Clear board
items.iter().for_each(|i| {
commands.entity(i).despawn();
});
// Progress to play
next.set(GameState::Play)
}
GameState::GameOver => (),// handled by user input
}
}
#[derive(Resource)]
enum LevelGoal {
Score(usize),
}
impl Default for LevelGoal {
fn default() -> Self {
Self::Score(0)
}
}
impl Display for LevelGoal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LevelGoal::Score(n) => write!(f, "{n}")
}
}
}
#[derive(Resource, Debug)]
@ -516,8 +602,54 @@ fn init_ui(mut commands: Commands, output_images: Res<OutputImages>, images: Res
parent.spawn(Text::new("Score:"));
parent.spawn((Text::new("???"), SyncResource::<Score>::default()));
});
parent
.spawn((Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
..default()
},))
.with_children(|parent| {
parent.spawn(Text::new("Goal:"));
parent.spawn((Text::new("???"), SyncResource::<LevelGoal>::default()));
});
});
commands
.spawn((
Node {
align_self: AlignSelf::Center,
justify_self: JustifySelf::Center,
..default()
},
ZIndex(1),
GameState::Pause,
children![(Text::new("paused"), TextColor(WHITE.into()), GameState::Pause)],
));
commands
.spawn((
Node {
align_self: AlignSelf::Center,
justify_self: JustifySelf::Center,
..default()
},
ZIndex(1),
GameState::LevelComplete,
children![(Text::new("you did it!"), TextColor(WHITE.into()), GameState::LevelComplete)],
));
commands
.spawn((
Node {
align_self: AlignSelf::Center,
justify_self: JustifySelf::Center,
..default()
},
ZIndex(1),
GameState::GameOver,
children![(Text::new("aww, you suck. i'm sorry."), TextColor(WHITE.into()), GameState::GameOver)],
));
commands
.spawn((
Node {
@ -569,19 +701,6 @@ fn init_ui(mut commands: Commands, output_images: Res<OutputImages>, images: Res
},
));
});
commands
.spawn((
Node {
align_self: AlignSelf::Center,
justify_self: JustifySelf::Center,
..default()
},
GameState::Pause,
))
.with_children(|parent| {
parent.spawn(Text::new("Paused"));
});
}
fn init_debug_ui(mut commands: Commands) {
@ -772,7 +891,6 @@ impl ShapeLayout {
}
}
// TODO: move to event
fn update_position(
mut changed: Query<
(Entity, &GridPosition, &mut Transform),
@ -791,7 +909,6 @@ fn update_position(
});
}
// TODO: Move to event
fn update_shape_blocks(
query: Query<
(Entity, &ShapeLayout, &GridPosition),
@ -809,7 +926,13 @@ fn update_shape_blocks(
let mut p = sl.coordinates_at(center);
blocks.iter_mut().for_each(|mut gp| {
*gp = p.next().unwrap().unwrap();
match p.next() {
Some(next) => match next {
Ok(next_gp) => *gp = next_gp,
Err(_) => warn!("Next coordinate was an Err?"),
},
None => warn!("Next coordinate was a None?"),
}
});
});
}
@ -844,7 +967,7 @@ fn on_add_shape_layout(
fn kb_input(
mut events: MessageReader<KeyboardInput>,
mut query: Query<Entity, With<Shape>>,
mut query: Query<Entity, With<Shape>>, // Single?
curr: Res<State<GameState>>,
mut next: ResMut<NextState<GameState>>,
mut commands: Commands,
@ -855,47 +978,61 @@ fn kb_input(
}| {
if let ButtonState::Pressed = state {
query.iter_mut().for_each(|e| {
match key_code {
// Up arrow should rotate if in falling mode
// Only move up if in falling::off mode
KeyCode::ArrowUp => {
commands.entity(e).trigger(|entity| Movement {
entity,
direction: MovementDirection::Rotate,
});
}
KeyCode::ArrowDown => {
commands.entity(e).trigger(|entity| Movement {
entity,
direction: MovementDirection::Down,
});
}
KeyCode::ArrowLeft => {
commands.entity(e).trigger(|entity| Movement {
entity,
direction: MovementDirection::Left,
});
}
KeyCode::ArrowRight => {
commands.entity(e).trigger(|entity| Movement {
entity,
direction: MovementDirection::Right,
});
}
KeyCode::Enter => {
commands.entity(e).trigger(|entity| Movement {
entity,
direction: MovementDirection::Skip,
});
}
KeyCode::Space => {
commands.entity(e).trigger(|entity| Swap { entity });
match curr.get() {
GameState::Play => match key_code {
// Up arrow should rotate if in falling mode
// Only move up if in falling::off mode
KeyCode::ArrowUp => {
commands.entity(e).trigger(|entity| Movement {
entity,
direction: MovementDirection::Rotate,
});
}
KeyCode::ArrowDown => {
commands.entity(e).trigger(|entity| Movement {
entity,
direction: MovementDirection::Down,
});
}
KeyCode::ArrowLeft => {
commands.entity(e).trigger(|entity| Movement {
entity,
direction: MovementDirection::Left,
});
}
KeyCode::ArrowRight => {
commands.entity(e).trigger(|entity| Movement {
entity,
direction: MovementDirection::Right,
});
}
KeyCode::Enter => {
commands.entity(e).trigger(|entity| Movement {
entity,
direction: MovementDirection::Skip,
});
}
KeyCode::Space => {
commands.entity(e).trigger(|entity| Swap { entity });
}
KeyCode::Escape => next.set(GameState::Pause),
_ => (),
},
GameState::Pause => match key_code {
KeyCode::Escape | KeyCode::Enter => next.set(GameState::Play),
_ => ()
}
KeyCode::Escape => next.set(match curr.get() {
GameState::Falling => GameState::Pause,
GameState::Pause => GameState::Falling,
}),
_ => (),
GameState::GameOver => match key_code {
KeyCode::Escape => todo!("Quit the game"),
KeyCode::Enter => next.set(GameState::NewGame),
_ => ()
},
GameState::LevelComplete => match key_code {
KeyCode::Escape => todo!("Quit the game"),
KeyCode::Enter => next.set(GameState::NextLevel),
_ => ()
},
GameState::NewGame | GameState::NextLevel => (),
}
});
}
@ -1189,10 +1326,17 @@ fn update_next_shapes(mut buffer: ResMut<ShapesBuffer>) {
}
fn assert_grid_position_uniqueness(
grid_positions: Query<&GridPosition, (Without<GridBackground>, Without<Shape>)>,
grid_positions: Query<(Entity, &GridPosition, Option<&LineBlock>, Option<&ShapeBlock>), With<Block>>,
mut next: ResMut<NextState<GameState>>,
) {
grid_positions.iter_combinations().for_each(|[a, b]| {
assert_ne!(a, b, "Two entities are in the same grid position!");
grid_positions.iter_combinations().for_each(|[(e1, a, lb1, sb1), (e2, b, lb2, sb2)]| {
if a == b {
error!("Two entities are in the same grid position: {a:?} <-> {b:?}!");
error!("\tE1: {:?} (Parent Block: {:?}| Parent Line: {:?})!", e1, lb1, sb1);
error!("\tE2: {:?} (Parent Block: {:?}| Parent Line: {:?})!", e2, lb2, sb2);
next.set(GameState::Pause);
}
// assert_ne!(a, b, "Two entities are in the same grid position!");
});
}
@ -1279,3 +1423,17 @@ fn damage_over_time(protagonist: Single<Entity, With<Protagonist>>, mut commands
quantity: 1.0,
});
}
fn check_level_goal(
goal: Res<LevelGoal>,
score: Res<Score>,
mut next: ResMut<NextState<GameState>>,
) {
match *goal {
LevelGoal::Score(g) => {
if score.0 >= g {
next.set(GameState::LevelComplete);
}
}
}
}

Loading…
Cancel
Save