|
|
|
|
@ -28,6 +28,7 @@ fn main() {
|
|
|
|
|
.init_resource::<Score>()
|
|
|
|
|
.init_resource::<OutputImages>()
|
|
|
|
|
.init_resource::<LevelGoal>()
|
|
|
|
|
.add_message::<Movement>()
|
|
|
|
|
.add_systems(
|
|
|
|
|
Startup,
|
|
|
|
|
(
|
|
|
|
|
@ -68,6 +69,7 @@ fn main() {
|
|
|
|
|
falling
|
|
|
|
|
.run_if(in_state(GameState::Play))
|
|
|
|
|
.run_if(on_timer(Duration::from_secs(1))),
|
|
|
|
|
movement.run_if(on_message::<Movement>),
|
|
|
|
|
update_position.run_if(any_component_changed::<GridPosition>),
|
|
|
|
|
update_shape_blocks
|
|
|
|
|
.run_if(any_component_changed::<ShapeLayout>.or(any_component_changed::<GridPosition>))
|
|
|
|
|
@ -80,14 +82,13 @@ fn main() {
|
|
|
|
|
adjust_block_lines
|
|
|
|
|
.run_if(any_component_changed::<Line>)
|
|
|
|
|
.after(clear_line),
|
|
|
|
|
assert_grid_position_uniqueness.run_if(any_component_changed::<GridPosition>),
|
|
|
|
|
sync_health
|
|
|
|
|
.run_if(any_component_changed::<Health>.or(any_component_added::<Health>)),
|
|
|
|
|
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>),
|
|
|
|
|
check_level_fail.run_if(any_component_removed::<ShapeBlock>),
|
|
|
|
|
assert_grid_position_uniqueness.run_if(any_component_changed::<GridPosition>),
|
|
|
|
|
toggle_art.run_if(any_component_added::<Art>),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
@ -182,7 +183,7 @@ impl Display for GridPosition {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Error, Debug, PartialEq)]
|
|
|
|
|
#[derive(Error, Debug, PartialEq, Clone, Copy)]
|
|
|
|
|
enum OutOfBoundsError {
|
|
|
|
|
#[error("Coordinates are out of bounds: Left")]
|
|
|
|
|
Left,
|
|
|
|
|
@ -990,9 +991,7 @@ fn on_add_shape_layout(
|
|
|
|
|
.entity(e)
|
|
|
|
|
.with_related_entities::<ShapeBlock>(|parent| {
|
|
|
|
|
sl.coordinates_at(center).for_each(|gp| {
|
|
|
|
|
parent
|
|
|
|
|
.spawn((mesh.clone(), MeshMaterial2d(mat.clone()), art.clone(), gp.unwrap(), Block, TETRIS))
|
|
|
|
|
.observe(movement);
|
|
|
|
|
parent.spawn((mesh.clone(), MeshMaterial2d(mat.clone()), art.clone(), gp.unwrap(), Block, TETRIS));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
@ -1002,6 +1001,7 @@ fn kb_input(
|
|
|
|
|
mut query: Query<Entity, With<Shape>>, // Single?
|
|
|
|
|
curr: Res<State<GameState>>,
|
|
|
|
|
mut next: ResMut<NextState<GameState>>,
|
|
|
|
|
mut messages: MessageWriter<Movement>,
|
|
|
|
|
mut commands: Commands,
|
|
|
|
|
) {
|
|
|
|
|
events.read().for_each(
|
|
|
|
|
@ -1009,43 +1009,43 @@ fn kb_input(
|
|
|
|
|
key_code, state, ..
|
|
|
|
|
}| {
|
|
|
|
|
if let ButtonState::Pressed = state {
|
|
|
|
|
query.iter_mut().for_each(|e| {
|
|
|
|
|
query.iter_mut().for_each(|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 {
|
|
|
|
|
messages.write(Movement {
|
|
|
|
|
entity,
|
|
|
|
|
direction: MovementDirection::Rotate,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
KeyCode::ArrowDown => {
|
|
|
|
|
commands.entity(e).trigger(|entity| Movement {
|
|
|
|
|
messages.write(Movement {
|
|
|
|
|
entity,
|
|
|
|
|
direction: MovementDirection::Down,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
KeyCode::ArrowLeft => {
|
|
|
|
|
commands.entity(e).trigger(|entity| Movement {
|
|
|
|
|
messages.write(Movement {
|
|
|
|
|
entity,
|
|
|
|
|
direction: MovementDirection::Left,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
KeyCode::ArrowRight => {
|
|
|
|
|
commands.entity(e).trigger(|entity| Movement {
|
|
|
|
|
messages.write(Movement {
|
|
|
|
|
entity,
|
|
|
|
|
direction: MovementDirection::Right,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Enter => {
|
|
|
|
|
commands.entity(e).trigger(|entity| Movement {
|
|
|
|
|
messages.write(Movement {
|
|
|
|
|
entity,
|
|
|
|
|
direction: MovementDirection::Skip,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Space => {
|
|
|
|
|
commands.entity(e).trigger(|entity| Swap { entity });
|
|
|
|
|
commands.entity(entity).trigger(|entity| Swap { entity });
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Escape => next.set(GameState::Pause),
|
|
|
|
|
_ => (),
|
|
|
|
|
@ -1072,10 +1072,10 @@ fn kb_input(
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn falling(mut shape: Query<Entity, With<Shape>>, mut commands: Commands) {
|
|
|
|
|
shape.iter_mut().for_each(|e| {
|
|
|
|
|
debug!("Making {:?} fall", e);
|
|
|
|
|
commands.entity(e).trigger(|entity| Movement {
|
|
|
|
|
fn falling(mut shape: Query<Entity, With<Shape>>, mut messages: MessageWriter<Movement>) {
|
|
|
|
|
shape.iter_mut().for_each(|entity| {
|
|
|
|
|
debug!("Making {:?} fall", entity);
|
|
|
|
|
messages.write(Movement {
|
|
|
|
|
entity,
|
|
|
|
|
direction: MovementDirection::Down,
|
|
|
|
|
});
|
|
|
|
|
@ -1095,7 +1095,6 @@ fn add_piece(mut commands: Commands, mut shapes: ResMut<ShapesBuffer>) {
|
|
|
|
|
shape,
|
|
|
|
|
TETRIS,
|
|
|
|
|
))
|
|
|
|
|
.observe(movement)
|
|
|
|
|
.observe(swap);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -1205,7 +1204,7 @@ enum MovementDirection {
|
|
|
|
|
|
|
|
|
|
// TODO: When out of bounds left/right, try to move piece away from wall
|
|
|
|
|
fn movement(
|
|
|
|
|
event: On<Movement>,
|
|
|
|
|
mut messages: MessageReader<Movement>,
|
|
|
|
|
mut grid_positions: Query<
|
|
|
|
|
&mut GridPosition,
|
|
|
|
|
Or<(With<ShapeBlock>, With<ShapeBlocks>, Without<LineBlock>)>,
|
|
|
|
|
@ -1213,80 +1212,97 @@ fn movement(
|
|
|
|
|
mut shape_layouts: Query<&mut ShapeLayout>,
|
|
|
|
|
inactive: Query<&GridPosition, (Without<ShapeBlock>, Without<ShapeBlocks>, With<LineBlock>)>,
|
|
|
|
|
mut commands: Commands,
|
|
|
|
|
mut next_state: ResMut<NextState<GameState>>,
|
|
|
|
|
) {
|
|
|
|
|
if let (Ok(this_shape_layout), Ok(center)) = (
|
|
|
|
|
shape_layouts.get_mut(event.entity),
|
|
|
|
|
grid_positions.get(event.entity),
|
|
|
|
|
) {
|
|
|
|
|
let new_positions = match event.event().direction {
|
|
|
|
|
MovementDirection::Down => vec![center.with_offset(0, -1)],
|
|
|
|
|
MovementDirection::Left => vec![center.with_offset(-1, 0)],
|
|
|
|
|
MovementDirection::Right => vec![center.with_offset(1, 0)],
|
|
|
|
|
MovementDirection::Rotate => vec![Ok(*center)],
|
|
|
|
|
MovementDirection::Skip => (1..=center.y)
|
|
|
|
|
.map(|i| center.with_offset(0, -(i as isize)))
|
|
|
|
|
.collect(),
|
|
|
|
|
};
|
|
|
|
|
let new_shape_layout = match event.event().direction {
|
|
|
|
|
MovementDirection::Down
|
|
|
|
|
| MovementDirection::Left
|
|
|
|
|
| MovementDirection::Right
|
|
|
|
|
| MovementDirection::Skip => this_shape_layout.clone(),
|
|
|
|
|
MovementDirection::Rotate => this_shape_layout.rotated(),
|
|
|
|
|
};
|
|
|
|
|
debug!(
|
|
|
|
|
"Proposed change: {:?}\n{}",
|
|
|
|
|
new_positions,
|
|
|
|
|
new_shape_layout.as_ascii()
|
|
|
|
|
);
|
|
|
|
|
for position in new_positions {
|
|
|
|
|
match position {
|
|
|
|
|
Err(OutOfBoundsError::Left) | Err(OutOfBoundsError::Right) => (), // Do nothing
|
|
|
|
|
Err(OutOfBoundsError::Down) => {
|
|
|
|
|
commands.entity(event.entity).remove::<Shape>();
|
|
|
|
|
}
|
|
|
|
|
Ok(new_center) => {
|
|
|
|
|
let new_blocks = new_shape_layout.coordinates_at(&new_center);
|
|
|
|
|
for block_gp in new_blocks {
|
|
|
|
|
match block_gp {
|
|
|
|
|
Err(OutOfBoundsError::Left) | Err(OutOfBoundsError::Right) => {
|
|
|
|
|
return;
|
|
|
|
|
} // Do nothing
|
|
|
|
|
Err(OutOfBoundsError::Down) => {
|
|
|
|
|
commands.entity(event.entity).remove::<Shape>();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
Ok(gp) => {
|
|
|
|
|
for other_gp in inactive.iter() {
|
|
|
|
|
// If there would be a collision between blocks
|
|
|
|
|
if gp == *other_gp {
|
|
|
|
|
// And we are moving down
|
|
|
|
|
if event.event().direction == MovementDirection::Down {
|
|
|
|
|
// De-activate this piece
|
|
|
|
|
commands.entity(event.entity).remove::<Shape>();
|
|
|
|
|
}
|
|
|
|
|
// Regardless, cancel the move
|
|
|
|
|
return;
|
|
|
|
|
messages.read().for_each(|message| {
|
|
|
|
|
if let (Ok(this_shape_layout), Ok(center)) = (
|
|
|
|
|
shape_layouts.get_mut(message.entity),
|
|
|
|
|
grid_positions.get(message.entity),
|
|
|
|
|
) {
|
|
|
|
|
let new_positions = match message.direction {
|
|
|
|
|
MovementDirection::Down => vec![center.with_offset(0, -1)],
|
|
|
|
|
MovementDirection::Left => vec![center.with_offset(-1, 0)],
|
|
|
|
|
MovementDirection::Right => vec![center.with_offset(1, 0)],
|
|
|
|
|
MovementDirection::Rotate => vec![Ok(*center)],
|
|
|
|
|
MovementDirection::Skip => (1..=center.y)
|
|
|
|
|
.map(|i| center.with_offset(0, -(i as isize)))
|
|
|
|
|
.collect(),
|
|
|
|
|
};
|
|
|
|
|
let new_shape_layout = match message.direction {
|
|
|
|
|
MovementDirection::Down
|
|
|
|
|
| MovementDirection::Left
|
|
|
|
|
| MovementDirection::Right
|
|
|
|
|
| MovementDirection::Skip => this_shape_layout.clone(),
|
|
|
|
|
MovementDirection::Rotate => this_shape_layout.rotated(),
|
|
|
|
|
};
|
|
|
|
|
debug!(
|
|
|
|
|
"Proposed change: {:?}\n{}",
|
|
|
|
|
new_positions,
|
|
|
|
|
new_shape_layout.as_ascii()
|
|
|
|
|
);
|
|
|
|
|
// For each of the proposed positions
|
|
|
|
|
for position in new_positions {
|
|
|
|
|
if let Ok(new_center) = position {
|
|
|
|
|
let new_blocks: Vec<Result<GridPosition, OutOfBoundsError>> = new_shape_layout
|
|
|
|
|
.coordinates_at(&new_center)
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
// If the move would cause one block to be out of bounds,
|
|
|
|
|
// Not a valid move so return.
|
|
|
|
|
if new_blocks.contains(&Err(OutOfBoundsError::Left))
|
|
|
|
|
|| new_blocks.contains(&Err(OutOfBoundsError::Right)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If would be out of bounds at the bottom of play
|
|
|
|
|
// Deactivate shape as we have hit the floor
|
|
|
|
|
if new_blocks.contains(&Err(OutOfBoundsError::Down)) {
|
|
|
|
|
commands.entity(message.entity).remove::<Shape>();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If all positions are within bounds, check for collisions
|
|
|
|
|
if new_blocks.iter().all(|b| b.is_ok()) {
|
|
|
|
|
for block_gp in new_blocks.iter().map(|b| b.unwrap()) {
|
|
|
|
|
for other_gp in inactive.iter() {
|
|
|
|
|
// If there would be a collision between blocks
|
|
|
|
|
if block_gp == *other_gp {
|
|
|
|
|
// Check if this pieces is out of bounds at the top
|
|
|
|
|
let out_of_bounds_top = block_gp.y >= Y_MAX;
|
|
|
|
|
// If so, transition to the game over state
|
|
|
|
|
if out_of_bounds_top {
|
|
|
|
|
next_state.set(GameState::GameOver);
|
|
|
|
|
}
|
|
|
|
|
// We are moving down
|
|
|
|
|
if [
|
|
|
|
|
MovementDirection::Down,
|
|
|
|
|
MovementDirection::Skip,
|
|
|
|
|
].contains(&message.direction) {
|
|
|
|
|
// De-activate this piece
|
|
|
|
|
commands.entity(message.entity).remove::<Shape>();
|
|
|
|
|
}
|
|
|
|
|
// Regardless, cancel the move
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
debug!("Checks passed for {position:?}, committing change");
|
|
|
|
|
|
|
|
|
|
// Update center
|
|
|
|
|
let mut gp = grid_positions.get_mut(event.entity).unwrap();
|
|
|
|
|
let mut gp = grid_positions.get_mut(message.entity).unwrap();
|
|
|
|
|
*gp = new_center;
|
|
|
|
|
|
|
|
|
|
// Update shape/rotation
|
|
|
|
|
let mut sl = shape_layouts.get_mut(event.entity).unwrap();
|
|
|
|
|
let mut sl = shape_layouts.get_mut(message.entity).unwrap();
|
|
|
|
|
*sl = new_shape_layout.clone();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
warn!("Oned movement on non-shape entity");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
warn!("Oned movement on non-shape entity");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn swap(
|
|
|
|
|
@ -1359,16 +1375,14 @@ fn update_next_shapes(mut buffer: ResMut<ShapesBuffer>) {
|
|
|
|
|
|
|
|
|
|
fn assert_grid_position_uniqueness(
|
|
|
|
|
grid_positions: Query<(Entity, &GridPosition, Option<&LineBlock>, Option<&ShapeBlock>), With<Block>>,
|
|
|
|
|
mut next: ResMut<NextState<GameState>>,
|
|
|
|
|
) {
|
|
|
|
|
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);
|
|
|
|
|
error!("Entities {e1:?} and {e2:?} @ GP {a:?}/{b:?}!");
|
|
|
|
|
error!("\t{:?}: (Block: {:?}| Line: {:?})!", e1, sb1, lb1);
|
|
|
|
|
error!("\t{:?}: (Block: {:?}| Line: {:?})!", e2, sb2, lb2);
|
|
|
|
|
}
|
|
|
|
|
// assert_ne!(a, b, "Two entities are in the same grid position!");
|
|
|
|
|
assert_ne!(a, b, "Two entities are in the same grid position!");
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -1470,19 +1484,6 @@ fn check_level_goal(
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn check_level_fail(
|
|
|
|
|
query: Query<&GridPosition, (With<Block>, Without<ShapeBlock>, Without<LineBlock>)>,
|
|
|
|
|
mut next: ResMut<NextState<GameState>>,
|
|
|
|
|
) {
|
|
|
|
|
// Check all blocks that are unassigned
|
|
|
|
|
query.iter().for_each(|gp| {
|
|
|
|
|
// If any block is above the top line, the game is over
|
|
|
|
|
if gp.y >= Y_MAX {
|
|
|
|
|
next.set(GameState::GameOver);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn toggle_art(
|
|
|
|
|
state: Res<State<DebuggingState>>,
|
|
|
|
|
// TODO: Correctness: Query for added or changed <Art> entities
|
|
|
|
|
|