|
|
|
@ -1,10 +1,18 @@
|
|
|
|
|
|
|
|
#![feature(try_blocks)]
|
|
|
|
// Bevy basically forces "complex types" with Querys
|
|
|
|
// Bevy basically forces "complex types" with Querys
|
|
|
|
#![allow(clippy::type_complexity)]
|
|
|
|
#![allow(clippy::type_complexity)]
|
|
|
|
|
|
|
|
|
|
|
|
use itertools::Itertools;
|
|
|
|
|
|
|
|
use games::*;
|
|
|
|
use games::*;
|
|
|
|
|
|
|
|
use itertools::Itertools;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
|
|
mod test;
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: When line is "full" (has 10 children) clear line and add to score
|
|
|
|
// TODO: Space key: skip to end
|
|
|
|
|
|
|
|
// TODO: When piece is near wall and rotates, move it over if it fits
|
|
|
|
|
|
|
|
// TODO: Make falling based on a timer resource ticking
|
|
|
|
|
|
|
|
// This allows us to tune the falling rate over time
|
|
|
|
|
|
|
|
// TODO: Preview next batch of pieces that will drop
|
|
|
|
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
fn main() {
|
|
|
|
App::new()
|
|
|
|
App::new()
|
|
|
|
@ -14,35 +22,59 @@ fn main() {
|
|
|
|
game_type: GameType::Two,
|
|
|
|
game_type: GameType::Two,
|
|
|
|
..default()
|
|
|
|
..default()
|
|
|
|
})
|
|
|
|
})
|
|
|
|
.init_state::<Falling>()
|
|
|
|
.init_state::<GameState>()
|
|
|
|
.add_systems(Startup, (init_world, init_debug_ui))
|
|
|
|
.init_resource::<ShapesBuffer>()
|
|
|
|
|
|
|
|
.init_resource::<Score>()
|
|
|
|
|
|
|
|
.add_systems(Startup, (init_world, init_debug_ui, init_ui))
|
|
|
|
|
|
|
|
// Input and basic systems
|
|
|
|
.add_systems(
|
|
|
|
.add_systems(
|
|
|
|
Update,
|
|
|
|
Update,
|
|
|
|
(
|
|
|
|
(
|
|
|
|
kb_input.run_if(on_event::<KeyboardInput>),
|
|
|
|
kb_input.run_if(on_event::<KeyboardInput>),
|
|
|
|
falling
|
|
|
|
toggle_state_visibility::<GameState>.run_if(state_changed::<GameState>),
|
|
|
|
.run_if(in_state(Falling::On))
|
|
|
|
),
|
|
|
|
.run_if(clock_cycle(1.0)),
|
|
|
|
)
|
|
|
|
|
|
|
|
.add_systems(
|
|
|
|
|
|
|
|
Update,
|
|
|
|
|
|
|
|
(
|
|
|
|
|
|
|
|
update_next_shapes
|
|
|
|
|
|
|
|
.run_if(resource_changed::<ShapesBuffer>.or(resource_added::<ShapesBuffer>)),
|
|
|
|
|
|
|
|
add_piece
|
|
|
|
|
|
|
|
.run_if(not(any_with_component::<Shape>))
|
|
|
|
|
|
|
|
.after(update_next_shapes),
|
|
|
|
update_shape_blocks
|
|
|
|
update_shape_blocks
|
|
|
|
.run_if(any_component_added::<Shape>.or(any_component_changed::<Shape>)),
|
|
|
|
.run_if(any_component_added::<Shape>.or(any_component_changed::<Shape>)),
|
|
|
|
sync_singleton_to_ui::<Shape>.run_if(any_component_changed::<Shape>),
|
|
|
|
falling
|
|
|
|
sync_singleton_to_ui::<Orientation>.run_if(any_component_changed::<Orientation>),
|
|
|
|
.run_if(in_state(GameState::Falling))
|
|
|
|
update_position,
|
|
|
|
.run_if(clock_cycle(1.0)),
|
|
|
|
add_piece.run_if(not(any_with_component::<Shape>)),
|
|
|
|
update_position.run_if(any_component_changed::<GridPosition>),
|
|
|
|
|
|
|
|
deactivate_shape.run_if(any_component_removed::<Shape>),
|
|
|
|
|
|
|
|
check_line_removal,
|
|
|
|
|
|
|
|
// Clearing lines systems
|
|
|
|
clear_line.run_if(any_component_changed::<LineBlocks>),
|
|
|
|
clear_line.run_if(any_component_changed::<LineBlocks>),
|
|
|
|
adjust_block_lines.run_if(any_component_changed::<Line>),
|
|
|
|
adjust_block_lines
|
|
|
|
|
|
|
|
.run_if(any_component_changed::<Line>)
|
|
|
|
|
|
|
|
.after(clear_line),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
// UI systems
|
|
|
|
|
|
|
|
.add_systems(
|
|
|
|
|
|
|
|
Update,
|
|
|
|
|
|
|
|
(
|
|
|
|
|
|
|
|
sync_resource_to_ui::<ShapesBuffer>.run_if(resource_changed::<ShapesBuffer>),
|
|
|
|
|
|
|
|
sync_resource_to_ui::<Score>.run_if(resource_changed::<Score>),
|
|
|
|
|
|
|
|
sync_singleton_to_ui::<Shape>.run_if(any_component_changed::<Shape>),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.add_systems(Update, draw_grid)
|
|
|
|
.add_systems(Update, draw_grid)
|
|
|
|
.add_observer(deactive_shape)
|
|
|
|
|
|
|
|
.run();
|
|
|
|
.run();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const SCALE: f32 = 30.0;
|
|
|
|
const SCALE: f32 = 30.0;
|
|
|
|
|
|
|
|
|
|
|
|
// Declare the size of the play area
|
|
|
|
// Declare the size of the play area
|
|
|
|
const X_MAX: u32 = 10;
|
|
|
|
const X_MAX: usize = 10;
|
|
|
|
const Y_MAX: u32 = 20;
|
|
|
|
const Y_MAX: usize = 20;
|
|
|
|
|
|
|
|
|
|
|
|
// The blocks making up this shape
|
|
|
|
// The blocks making up this shape
|
|
|
|
#[derive(Component)]
|
|
|
|
#[derive(Component)]
|
|
|
|
@ -82,8 +114,8 @@ struct Block;
|
|
|
|
#[derive(Component, Debug, Clone, Copy, PartialEq)]
|
|
|
|
#[derive(Component, Debug, Clone, Copy, PartialEq)]
|
|
|
|
#[require(Transform, Visibility)]
|
|
|
|
#[require(Transform, Visibility)]
|
|
|
|
struct GridPosition {
|
|
|
|
struct GridPosition {
|
|
|
|
x: u32,
|
|
|
|
x: usize,
|
|
|
|
y: u32,
|
|
|
|
y: usize,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl GridPosition {
|
|
|
|
impl GridPosition {
|
|
|
|
@ -99,8 +131,8 @@ impl GridPosition {
|
|
|
|
Err(GameError::OutOfBoundsDown)
|
|
|
|
Err(GameError::OutOfBoundsDown)
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
Ok(GridPosition {
|
|
|
|
Ok(GridPosition {
|
|
|
|
x: x as u32,
|
|
|
|
x: x as usize,
|
|
|
|
y: y as u32,
|
|
|
|
y: y as usize,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -144,8 +176,8 @@ impl From<&GridPosition> for Vec3 {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<(u32, u32)> for GridPosition {
|
|
|
|
impl From<(usize, usize)> for GridPosition {
|
|
|
|
fn from((x, y): (u32, u32)) -> GridPosition {
|
|
|
|
fn from((x, y): (usize, usize)) -> GridPosition {
|
|
|
|
GridPosition { x, y }
|
|
|
|
GridPosition { x, y }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -167,57 +199,43 @@ impl std::ops::AddAssign<&GridPosition> for GridPosition {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Component, Default, Event, Clone, Debug)]
|
|
|
|
#[derive(States, Clone, Eq, PartialEq, Debug, Hash, Default, Component)]
|
|
|
|
enum Orientation {
|
|
|
|
enum GameState {
|
|
|
|
#[default]
|
|
|
|
#[default]
|
|
|
|
Up,
|
|
|
|
Falling,
|
|
|
|
Left,
|
|
|
|
Pause,
|
|
|
|
Down,
|
|
|
|
|
|
|
|
Right,
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Orientation {
|
|
|
|
#[derive(Resource, Debug)]
|
|
|
|
fn next(&self) -> Self {
|
|
|
|
struct Visuals {
|
|
|
|
match self {
|
|
|
|
material: Handle<ColorMaterial>,
|
|
|
|
Self::Up => Self::Left,
|
|
|
|
mesh: Handle<Mesh>,
|
|
|
|
Self::Left => Self::Down,
|
|
|
|
|
|
|
|
Self::Down => Self::Right,
|
|
|
|
|
|
|
|
Self::Right => Self::Up,
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn prev(&self) -> Self {
|
|
|
|
|
|
|
|
match self {
|
|
|
|
|
|
|
|
Self::Up => Self::Right,
|
|
|
|
|
|
|
|
Self::Right => Self::Down,
|
|
|
|
|
|
|
|
Self::Down => Self::Left,
|
|
|
|
|
|
|
|
Self::Left => Self::Up,
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Display for Orientation {
|
|
|
|
#[derive(Resource, Debug, Default)]
|
|
|
|
|
|
|
|
struct Score(usize);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl Display for Score {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
match self {
|
|
|
|
write!(f, "{}", self.0)
|
|
|
|
Orientation::Up => write!(f, "up"),
|
|
|
|
|
|
|
|
Orientation::Down => write!(f, "down"),
|
|
|
|
|
|
|
|
Orientation::Left => write!(f, "<-"),
|
|
|
|
|
|
|
|
Orientation::Right => write!(f, "->"),
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(States, Clone, Eq, PartialEq, Debug, Hash, Default, Component)]
|
|
|
|
/// ShapesBuffer resource stores non-active shapes
|
|
|
|
enum Falling {
|
|
|
|
#[derive(Resource, Debug, Default)]
|
|
|
|
#[default]
|
|
|
|
struct ShapesBuffer {
|
|
|
|
On,
|
|
|
|
/// Next stores a vector of 2N shapes that will come up in play
|
|
|
|
Off,
|
|
|
|
next: VecDeque<Shape>,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Resource, Debug)]
|
|
|
|
impl Display for ShapesBuffer {
|
|
|
|
struct Visuals {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
material: Handle<ColorMaterial>,
|
|
|
|
if let Some(shape) = self.next.front() {
|
|
|
|
mesh: Handle<Mesh>,
|
|
|
|
write!(f, "{shape}")
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
write!(f, "ERR")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn init_world(
|
|
|
|
fn init_world(
|
|
|
|
@ -233,11 +251,60 @@ fn init_world(
|
|
|
|
mesh: meshes.add(Rectangle::new(SCALE, SCALE)),
|
|
|
|
mesh: meshes.add(Rectangle::new(SCALE, SCALE)),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
(0..20).for_each(|i| {
|
|
|
|
(0..Y_MAX).for_each(|i| {
|
|
|
|
|
|
|
|
info!("Spawning line {i}");
|
|
|
|
commands.spawn((Line(i), LineBlocks::default()));
|
|
|
|
commands.spawn((Line(i), LineBlocks::default()));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn init_ui(mut commands: Commands) {
|
|
|
|
|
|
|
|
commands
|
|
|
|
|
|
|
|
.spawn((
|
|
|
|
|
|
|
|
Node {
|
|
|
|
|
|
|
|
align_self: AlignSelf::Center,
|
|
|
|
|
|
|
|
justify_self: JustifySelf::End,
|
|
|
|
|
|
|
|
flex_direction: FlexDirection::Column,
|
|
|
|
|
|
|
|
..default()
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
BackgroundColor(BLACK.into()),
|
|
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
.with_children(|parent| {
|
|
|
|
|
|
|
|
parent
|
|
|
|
|
|
|
|
.spawn((Node {
|
|
|
|
|
|
|
|
flex_direction: FlexDirection::Column,
|
|
|
|
|
|
|
|
align_items: AlignItems::Center,
|
|
|
|
|
|
|
|
..default()
|
|
|
|
|
|
|
|
},))
|
|
|
|
|
|
|
|
.with_children(|parent| {
|
|
|
|
|
|
|
|
parent.spawn(Text::new("Next:"));
|
|
|
|
|
|
|
|
parent.spawn((Text::new("???"), SyncResource::<ShapesBuffer>::default()));
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
parent
|
|
|
|
|
|
|
|
.spawn((Node {
|
|
|
|
|
|
|
|
flex_direction: FlexDirection::Column,
|
|
|
|
|
|
|
|
align_items: AlignItems::Center,
|
|
|
|
|
|
|
|
..default()
|
|
|
|
|
|
|
|
},))
|
|
|
|
|
|
|
|
.with_children(|parent| {
|
|
|
|
|
|
|
|
parent.spawn(Text::new("Score:"));
|
|
|
|
|
|
|
|
parent.spawn((Text::new("???"), SyncResource::<Score>::default()));
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
fn init_debug_ui(mut commands: Commands) {
|
|
|
|
commands
|
|
|
|
commands
|
|
|
|
.spawn((
|
|
|
|
.spawn((
|
|
|
|
@ -253,10 +320,6 @@ fn init_debug_ui(mut commands: Commands) {
|
|
|
|
Node::default(),
|
|
|
|
Node::default(),
|
|
|
|
children![
|
|
|
|
children![
|
|
|
|
(Text::new("SHAPE"), SyncSingleton::<Shape>::default()),
|
|
|
|
(Text::new("SHAPE"), SyncSingleton::<Shape>::default()),
|
|
|
|
(
|
|
|
|
|
|
|
|
Text::new("ORIENTATION"),
|
|
|
|
|
|
|
|
SyncSingleton::<Orientation>::default()
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
],
|
|
|
|
],
|
|
|
|
));
|
|
|
|
));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
@ -374,10 +437,6 @@ impl Shape {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn rotate(&mut self) {
|
|
|
|
|
|
|
|
*self = self.rotated();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn coordinates(
|
|
|
|
fn coordinates(
|
|
|
|
&self,
|
|
|
|
&self,
|
|
|
|
center: &GridPosition,
|
|
|
|
center: &GridPosition,
|
|
|
|
@ -449,13 +508,13 @@ fn update_position(
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: Inline this to when movement occurs
|
|
|
|
// TODO: Inline this to when movement occurs
|
|
|
|
fn update_shape_blocks(
|
|
|
|
fn update_shape_blocks(
|
|
|
|
query: Query<(Entity, &Shape, &Orientation, &GridPosition), Or<(Added<Shape>, Changed<Shape>)>>,
|
|
|
|
query: Query<(Entity, &Shape, &GridPosition), Or<(Added<Shape>, Changed<Shape>)>>,
|
|
|
|
mut blocks: Query<&mut GridPosition, (With<ShapeBlock>, Without<Shape>)>,
|
|
|
|
mut blocks: Query<&mut GridPosition, (With<ShapeBlock>, Without<Shape>)>,
|
|
|
|
mut commands: Commands,
|
|
|
|
mut commands: Commands,
|
|
|
|
visuals: Res<Visuals>,
|
|
|
|
visuals: Res<Visuals>,
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
query.iter().for_each(|(e, s, o, center)| {
|
|
|
|
query.iter().for_each(|(e, s, center)| {
|
|
|
|
info!("Setting piece: {e:?} {o:?} {center:?}\n{}", s.as_ascii());
|
|
|
|
debug!("Setting piece: {e:?} {center:?}\n{}", s.as_ascii());
|
|
|
|
|
|
|
|
|
|
|
|
if blocks.is_empty() {
|
|
|
|
if blocks.is_empty() {
|
|
|
|
let mesh = Mesh2d(visuals.mesh.clone());
|
|
|
|
let mesh = Mesh2d(visuals.mesh.clone());
|
|
|
|
@ -480,9 +539,9 @@ fn update_shape_blocks(
|
|
|
|
|
|
|
|
|
|
|
|
fn kb_input(
|
|
|
|
fn kb_input(
|
|
|
|
mut events: EventReader<KeyboardInput>,
|
|
|
|
mut events: EventReader<KeyboardInput>,
|
|
|
|
mut query: Query<(Entity, &Orientation, &mut Shape)>,
|
|
|
|
mut query: Query<(Entity, &mut Shape)>,
|
|
|
|
curr: Res<State<Falling>>,
|
|
|
|
curr: Res<State<GameState>>,
|
|
|
|
mut next: ResMut<NextState<Falling>>,
|
|
|
|
mut next: ResMut<NextState<GameState>>,
|
|
|
|
mut commands: Commands,
|
|
|
|
mut commands: Commands,
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
events.read().for_each(
|
|
|
|
events.read().for_each(
|
|
|
|
@ -490,7 +549,7 @@ fn kb_input(
|
|
|
|
key_code, state, ..
|
|
|
|
key_code, state, ..
|
|
|
|
}| {
|
|
|
|
}| {
|
|
|
|
if let ButtonState::Pressed = state {
|
|
|
|
if let ButtonState::Pressed = state {
|
|
|
|
query.iter_mut().for_each(|(e, o, mut s)| {
|
|
|
|
query.iter_mut().for_each(|(e, mut s)| {
|
|
|
|
match key_code {
|
|
|
|
match key_code {
|
|
|
|
// Up arrow should rotate if in falling mode
|
|
|
|
// Up arrow should rotate if in falling mode
|
|
|
|
// Only move up if in falling::off mode
|
|
|
|
// Only move up if in falling::off mode
|
|
|
|
@ -506,9 +565,9 @@ fn kb_input(
|
|
|
|
KeyCode::ArrowRight => {
|
|
|
|
KeyCode::ArrowRight => {
|
|
|
|
commands.entity(e).trigger(Movement::Right);
|
|
|
|
commands.entity(e).trigger(Movement::Right);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
KeyCode::Space => next.set(match curr.get() {
|
|
|
|
KeyCode::Escape => next.set(match curr.get() {
|
|
|
|
Falling::On => Falling::Off,
|
|
|
|
GameState::Falling => GameState::Pause,
|
|
|
|
Falling::Off => Falling::On,
|
|
|
|
GameState::Pause => GameState::Falling,
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
KeyCode::Digit1 => *s = Shape::new_t(),
|
|
|
|
KeyCode::Digit1 => *s = Shape::new_t(),
|
|
|
|
KeyCode::Digit2 => *s = Shape::new_o(),
|
|
|
|
KeyCode::Digit2 => *s = Shape::new_o(),
|
|
|
|
@ -529,7 +588,7 @@ fn draw_grid(mut gizmos: Gizmos) {
|
|
|
|
gizmos
|
|
|
|
gizmos
|
|
|
|
.grid_2d(
|
|
|
|
.grid_2d(
|
|
|
|
Isometry2d::IDENTITY,
|
|
|
|
Isometry2d::IDENTITY,
|
|
|
|
UVec2::new(X_MAX, Y_MAX),
|
|
|
|
UVec2::new(X_MAX as u32, Y_MAX as u32),
|
|
|
|
Vec2::new(SCALE, SCALE),
|
|
|
|
Vec2::new(SCALE, SCALE),
|
|
|
|
GREEN,
|
|
|
|
GREEN,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
@ -557,13 +616,11 @@ fn clock_cycle(n: f32) -> impl FnMut(Res<Time>, Local<f32>) -> bool {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn add_piece(mut commands: Commands) {
|
|
|
|
fn add_piece(mut commands: Commands, mut shapes: ResMut<ShapesBuffer>) {
|
|
|
|
// TODO: Choose a different piece
|
|
|
|
|
|
|
|
commands
|
|
|
|
commands
|
|
|
|
.spawn((
|
|
|
|
.spawn((
|
|
|
|
Orientation::default(),
|
|
|
|
|
|
|
|
GridPosition::default(),
|
|
|
|
GridPosition::default(),
|
|
|
|
Shape::default(),
|
|
|
|
shapes.next.pop_front().unwrap(),
|
|
|
|
))
|
|
|
|
))
|
|
|
|
.observe(movement);
|
|
|
|
.observe(movement);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -571,37 +628,59 @@ fn add_piece(mut commands: Commands) {
|
|
|
|
/// When a line reaches 10 blocks, clear it
|
|
|
|
/// When a line reaches 10 blocks, clear it
|
|
|
|
fn clear_line(
|
|
|
|
fn clear_line(
|
|
|
|
changed_lines: Query<Entity, Changed<LineBlocks>>,
|
|
|
|
changed_lines: Query<Entity, Changed<LineBlocks>>,
|
|
|
|
mut lines: Query<(Entity, &LineBlocks, &mut Line)>,
|
|
|
|
mut lines: Query<&mut Line>,
|
|
|
|
|
|
|
|
line_blocks: Query<&LineBlocks>,
|
|
|
|
|
|
|
|
mut score: ResMut<Score>,
|
|
|
|
mut commands: Commands,
|
|
|
|
mut commands: Commands,
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
let cleared_lines: Vec<usize> = changed_lines
|
|
|
|
let mut cleared_lines: Vec<usize> = changed_lines
|
|
|
|
.iter()
|
|
|
|
.iter()
|
|
|
|
.filter_map(|e| lines.get(e).ok())
|
|
|
|
.filter_map(|e| try { (Some(e)?, line_blocks.get(e).ok()?, lines.get(e).ok()?) } )
|
|
|
|
.filter_map(|(e, lb, Line(i))| {
|
|
|
|
.filter_map(|(e, lb, Line(i))| {
|
|
|
|
if lb.0.len() == 10 {
|
|
|
|
if lb.0.len() == 10 {
|
|
|
|
commands.entity(e).despawn_related::<LineBlocks>();
|
|
|
|
commands.entity(e).despawn_related::<LineBlocks>().insert(LineBlocks::default());
|
|
|
|
|
|
|
|
score.0 += 1;
|
|
|
|
|
|
|
|
info!("New score: {:?}", score.0);
|
|
|
|
Some(*i)
|
|
|
|
Some(*i)
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
.sorted()
|
|
|
|
.collect();
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if !cleared_lines.is_empty() {
|
|
|
|
info!("Cleared lines: {:?}", cleared_lines);
|
|
|
|
info!("Cleared lines: {:?}", cleared_lines);
|
|
|
|
|
|
|
|
|
|
|
|
for (idx, cleared_line_number) in cleared_lines.into_iter().sorted().enumerate() {
|
|
|
|
#[cfg(debug_assertions)]
|
|
|
|
info!("Processing line {cleared_line_number} ({idx})");
|
|
|
|
{
|
|
|
|
let cleared_line_number = cleared_line_number - idx;
|
|
|
|
debug_assert_eq!(lines.iter().count(), 20, "There should be 20 lines");
|
|
|
|
lines.iter_mut().for_each(|(_, _, mut l)| {
|
|
|
|
// Check that all line numbers are present
|
|
|
|
let dest = if l.0 > cleared_line_number {
|
|
|
|
lines.iter().map(|Line(i)| i).sorted().enumerate().for_each(|(i, line_num)| {
|
|
|
|
l.0 - 1
|
|
|
|
debug_assert_eq!(i, *line_num, "Line numbers should match their sorted index");
|
|
|
|
} else if l.0 == cleared_line_number {
|
|
|
|
});
|
|
|
|
(Y_MAX - (idx + 1) as u32) as usize
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let original_cleared_lines_len = cleared_lines.len();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Iterate over all lines in reverse sorted order (largest to smallest)
|
|
|
|
|
|
|
|
lines
|
|
|
|
|
|
|
|
.iter_mut()
|
|
|
|
|
|
|
|
.sorted_by(|i, j| i.0.cmp(&j.0))
|
|
|
|
|
|
|
|
.rev()
|
|
|
|
|
|
|
|
.for_each(|mut l| {
|
|
|
|
|
|
|
|
// If the current index is in the set of cleared lines, move it to the top
|
|
|
|
|
|
|
|
// Otherwise, move it down by the number of cleared lines
|
|
|
|
|
|
|
|
if cleared_lines.contains(&l.0) {
|
|
|
|
|
|
|
|
// Move to the N-offset line number (top, top-1, etc)
|
|
|
|
|
|
|
|
let offset = original_cleared_lines_len - cleared_lines.len();
|
|
|
|
|
|
|
|
info!("Moving line {:?}->{:?}", l.0, Y_MAX - 1 - offset);
|
|
|
|
|
|
|
|
l.0 = Y_MAX - 1 - offset;
|
|
|
|
|
|
|
|
cleared_lines.pop();
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
l.0
|
|
|
|
info!("Moving line {:?}->{:?}", l.0, l.0 - cleared_lines.len());
|
|
|
|
};
|
|
|
|
l.0 -= cleared_lines.len();
|
|
|
|
info!("Moving line {:?} to {:?}", l, dest);
|
|
|
|
}
|
|
|
|
l.0 = dest;
|
|
|
|
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -614,7 +693,7 @@ fn adjust_block_lines(
|
|
|
|
query.iter().for_each(|(e, Line(i))| {
|
|
|
|
query.iter().for_each(|(e, Line(i))| {
|
|
|
|
parent.iter_descendants(e).for_each(|block| {
|
|
|
|
parent.iter_descendants(e).for_each(|block| {
|
|
|
|
if let Ok(mut gp) = blocks.get_mut(block) {
|
|
|
|
if let Ok(mut gp) = blocks.get_mut(block) {
|
|
|
|
gp.y = *i as u32;
|
|
|
|
gp.y = *i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
@ -646,7 +725,7 @@ fn movement(
|
|
|
|
Movement::Right => (center.with_offset(1, 0), *this_shape),
|
|
|
|
Movement::Right => (center.with_offset(1, 0), *this_shape),
|
|
|
|
Movement::Rotate => (Ok(*center), this_shape.rotated()),
|
|
|
|
Movement::Rotate => (Ok(*center), this_shape.rotated()),
|
|
|
|
};
|
|
|
|
};
|
|
|
|
info!(
|
|
|
|
debug!(
|
|
|
|
"Proposed change: {:?}\n{}",
|
|
|
|
"Proposed change: {:?}\n{}",
|
|
|
|
new_center,
|
|
|
|
new_center,
|
|
|
|
new_shape.as_ascii()
|
|
|
|
new_shape.as_ascii()
|
|
|
|
@ -698,112 +777,50 @@ fn movement(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn check_line_removal(
|
|
|
|
|
|
|
|
mut events: RemovedComponents<Line>,
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
events.read().for_each(|e| {
|
|
|
|
|
|
|
|
info!("Line entity {:?} removed", e);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: Just despawn?
|
|
|
|
// TODO: Just despawn?
|
|
|
|
fn deactive_shape(
|
|
|
|
fn deactivate_shape(
|
|
|
|
trigger: Trigger<OnRemove, Shape>,
|
|
|
|
mut events: RemovedComponents<Shape>,
|
|
|
|
grid_positions: Query<&GridPosition>,
|
|
|
|
grid_positions: Query<&GridPosition>,
|
|
|
|
parent: Query<&ShapeBlocks>,
|
|
|
|
parent: Query<&ShapeBlocks>,
|
|
|
|
lines: Query<(Entity, &Line), With<LineBlocks>>,
|
|
|
|
lines: Query<(Entity, &Line), With<LineBlocks>>,
|
|
|
|
mut commands: Commands,
|
|
|
|
mut commands: Commands,
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
parent.iter_descendants(trigger.target()).for_each(|block| {
|
|
|
|
events.read().for_each(|target| {
|
|
|
|
|
|
|
|
parent.iter_descendants(target).for_each(|block| {
|
|
|
|
let GridPosition { y, .. } = grid_positions.get(block).unwrap();
|
|
|
|
let GridPosition { y, .. } = grid_positions.get(block).unwrap();
|
|
|
|
let parent_line = lines
|
|
|
|
let parent_line = lines
|
|
|
|
.iter()
|
|
|
|
.iter()
|
|
|
|
.find_map(|(e, Line(i))| (*y == *i as u32).then_some(e))
|
|
|
|
.find_map(|(e, Line(i))| (*y == *i).then_some(e))
|
|
|
|
.unwrap();
|
|
|
|
.unwrap(); // TODO: This crashed once kinda late in a game... why?
|
|
|
|
commands
|
|
|
|
commands
|
|
|
|
.entity(parent_line)
|
|
|
|
.entity(parent_line)
|
|
|
|
.add_one_related::<LineBlock>(block);
|
|
|
|
.add_one_related::<LineBlock>(block);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
commands.entity(trigger.target()).despawn();
|
|
|
|
commands.entity(target).despawn();
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
fn update_next_shapes(mut buffer: ResMut<ShapesBuffer>) {
|
|
|
|
mod test {
|
|
|
|
// If the buffer contains less than n+1 shapes (where n is the number of possible shapes)
|
|
|
|
use super::*;
|
|
|
|
// Ideally we have between 1n and 2n shapes in the `next` buffer
|
|
|
|
|
|
|
|
while buffer.next.len() < 8 {
|
|
|
|
#[test]
|
|
|
|
// TODO: Shuffle these!
|
|
|
|
fn test_shape_t() {
|
|
|
|
buffer.next.extend([
|
|
|
|
let mut shape = Shape::new_t();
|
|
|
|
Shape::new_o(),
|
|
|
|
|
|
|
|
Shape::new_t(),
|
|
|
|
let expected_up = "010\n\
|
|
|
|
Shape::new_l(),
|
|
|
|
111\n\
|
|
|
|
Shape::new_j(),
|
|
|
|
000\n";
|
|
|
|
Shape::new_s(),
|
|
|
|
|
|
|
|
Shape::new_z(),
|
|
|
|
let expected_right = "010\n\
|
|
|
|
Shape::new_i(),
|
|
|
|
011\n\
|
|
|
|
]);
|
|
|
|
010\n";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let expected_down = "000\n\
|
|
|
|
|
|
|
|
111\n\
|
|
|
|
|
|
|
|
010\n";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let expected_left = "010\n\
|
|
|
|
|
|
|
|
110\n\
|
|
|
|
|
|
|
|
010\n";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(shape.as_ascii(), expected_up);
|
|
|
|
|
|
|
|
shape.rotate();
|
|
|
|
|
|
|
|
assert_eq!(shape.as_ascii(), expected_right);
|
|
|
|
|
|
|
|
shape.rotate();
|
|
|
|
|
|
|
|
assert_eq!(shape.as_ascii(), expected_down);
|
|
|
|
|
|
|
|
shape.rotate();
|
|
|
|
|
|
|
|
assert_eq!(shape.as_ascii(), expected_left);
|
|
|
|
|
|
|
|
shape.rotate();
|
|
|
|
|
|
|
|
assert_eq!(shape.as_ascii(), expected_up);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
|
|
fn test_shape_i() {
|
|
|
|
|
|
|
|
let mut shape = Shape::new_i();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let expected_up = "0010\n\
|
|
|
|
|
|
|
|
0010\n\
|
|
|
|
|
|
|
|
0010\n\
|
|
|
|
|
|
|
|
0010\n";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let expected_right = "0000\n\
|
|
|
|
|
|
|
|
0000\n\
|
|
|
|
|
|
|
|
1111\n\
|
|
|
|
|
|
|
|
0000\n";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let expected_down = "0100\n\
|
|
|
|
|
|
|
|
0100\n\
|
|
|
|
|
|
|
|
0100\n\
|
|
|
|
|
|
|
|
0100\n";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let expected_left = "0000\n\
|
|
|
|
|
|
|
|
1111\n\
|
|
|
|
|
|
|
|
0000\n\
|
|
|
|
|
|
|
|
0000\n";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(shape.as_ascii(), expected_up);
|
|
|
|
|
|
|
|
shape.rotate();
|
|
|
|
|
|
|
|
assert_eq!(shape.as_ascii(), expected_right);
|
|
|
|
|
|
|
|
shape.rotate();
|
|
|
|
|
|
|
|
assert_eq!(shape.as_ascii(), expected_down);
|
|
|
|
|
|
|
|
shape.rotate();
|
|
|
|
|
|
|
|
assert_eq!(shape.as_ascii(), expected_left);
|
|
|
|
|
|
|
|
shape.rotate();
|
|
|
|
|
|
|
|
assert_eq!(shape.as_ascii(), expected_up);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
|
|
fn test_coordinates() {
|
|
|
|
|
|
|
|
let shape = Shape::new_t();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let center = GridPosition { x: 5, y: 5 };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let expected: Vec<Result<GridPosition, GameError>> = vec![
|
|
|
|
|
|
|
|
Ok((5, 6).into()),
|
|
|
|
|
|
|
|
Ok((4, 5).into()),
|
|
|
|
|
|
|
|
Ok((5, 5).into()),
|
|
|
|
|
|
|
|
Ok((6, 5).into()),
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let actual: Vec<Result<GridPosition, GameError>> = shape.coordinates(¢er).collect();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(actual, expected);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|