|
|
|
|
@ -2,17 +2,27 @@
|
|
|
|
|
// Bevy basically forces "complex types" with Querys
|
|
|
|
|
#![allow(clippy::type_complexity)]
|
|
|
|
|
|
|
|
|
|
use bevy::{
|
|
|
|
|
asset::RenderAssetUsages,
|
|
|
|
|
math::FloatOrd,
|
|
|
|
|
render::{
|
|
|
|
|
camera::{ImageRenderTarget, RenderTarget},
|
|
|
|
|
render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages},
|
|
|
|
|
view::RenderLayers,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
use games::*;
|
|
|
|
|
use itertools::Itertools;
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod test;
|
|
|
|
|
|
|
|
|
|
// TODO: Space key: skip to end
|
|
|
|
|
const TETRIS: RenderLayers = RenderLayers::layer(1);
|
|
|
|
|
const BATTLER: RenderLayers = RenderLayers::layer(2);
|
|
|
|
|
|
|
|
|
|
// 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() {
|
|
|
|
|
App::new()
|
|
|
|
|
@ -26,7 +36,16 @@ fn main() {
|
|
|
|
|
.init_resource::<ShapesBuffer>()
|
|
|
|
|
.init_resource::<ShapeStore>()
|
|
|
|
|
.init_resource::<Score>()
|
|
|
|
|
.add_systems(Startup, (init_world, init_debug_ui, init_ui))
|
|
|
|
|
.init_resource::<OutputImages>()
|
|
|
|
|
.add_systems(
|
|
|
|
|
Startup,
|
|
|
|
|
(
|
|
|
|
|
init_world,
|
|
|
|
|
init_debug_ui,
|
|
|
|
|
init_cameras,
|
|
|
|
|
init_ui.after(init_cameras),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
// Input and basic systems
|
|
|
|
|
.add_systems(
|
|
|
|
|
Update,
|
|
|
|
|
@ -48,8 +67,11 @@ fn main() {
|
|
|
|
|
.run_if(clock_cycle(1.0)),
|
|
|
|
|
update_position.run_if(any_component_changed::<GridPosition>),
|
|
|
|
|
update_shape_blocks
|
|
|
|
|
.run_if(any_component_added::<Shape>.or(any_component_changed::<Shape>)).after(update_position),
|
|
|
|
|
deactivate_shape.run_if(any_component_removed::<Shape>).after(update_shape_blocks),
|
|
|
|
|
.run_if(any_component_added::<Shape>.or(any_component_changed::<Shape>))
|
|
|
|
|
.after(update_position),
|
|
|
|
|
deactivate_shape
|
|
|
|
|
.run_if(any_component_removed::<Shape>)
|
|
|
|
|
.after(update_shape_blocks),
|
|
|
|
|
// Clearing lines systems
|
|
|
|
|
clear_line.run_if(any_component_changed::<LineBlocks>),
|
|
|
|
|
adjust_block_lines
|
|
|
|
|
@ -246,7 +268,7 @@ impl Display for ShapeStore {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
match self.0 {
|
|
|
|
|
Some(inner) => write!(f, "{}", inner.as_ascii()),
|
|
|
|
|
None => write!(f, "---")
|
|
|
|
|
None => write!(f, "---"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -266,11 +288,96 @@ fn init_world(
|
|
|
|
|
|
|
|
|
|
(0..Y_MAX).for_each(|i| {
|
|
|
|
|
info!("Spawning line {i}");
|
|
|
|
|
commands.spawn((Line(i), LineBlocks::default()));
|
|
|
|
|
commands.spawn((Line(i), LineBlocks::default(), TETRIS));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn init_ui(mut commands: Commands) {
|
|
|
|
|
#[derive(Resource, Default)]
|
|
|
|
|
struct OutputImages {
|
|
|
|
|
tetris: Handle<Image>,
|
|
|
|
|
battler: Handle<Image>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Component)]
|
|
|
|
|
struct TetrisCamera;
|
|
|
|
|
|
|
|
|
|
#[derive(Component)]
|
|
|
|
|
struct BattlerCamera;
|
|
|
|
|
|
|
|
|
|
fn init_cameras(
|
|
|
|
|
mut commands: Commands,
|
|
|
|
|
mut images: ResMut<Assets<Image>>,
|
|
|
|
|
mut output_images: ResMut<OutputImages>,
|
|
|
|
|
) {
|
|
|
|
|
fn render_target_image() -> Image {
|
|
|
|
|
let (width, height) = (1028, 1028);
|
|
|
|
|
let size = Extent3d {
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
..default()
|
|
|
|
|
};
|
|
|
|
|
let dimension = TextureDimension::D2;
|
|
|
|
|
let format = TextureFormat::Bgra8UnormSrgb;
|
|
|
|
|
let asset_usage = RenderAssetUsages::all();
|
|
|
|
|
let mut image = Image::new_uninit(size, dimension, format, asset_usage);
|
|
|
|
|
|
|
|
|
|
image.texture_descriptor.usage = TextureUsages::TEXTURE_BINDING
|
|
|
|
|
| TextureUsages::COPY_DST
|
|
|
|
|
| TextureUsages::RENDER_ATTACHMENT;
|
|
|
|
|
|
|
|
|
|
image
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let img = render_target_image();
|
|
|
|
|
let target = {
|
|
|
|
|
let handle = images.add(img);
|
|
|
|
|
output_images.tetris = handle.clone();
|
|
|
|
|
let scale_factor = FloatOrd(1.0);
|
|
|
|
|
|
|
|
|
|
RenderTarget::Image(ImageRenderTarget {
|
|
|
|
|
handle,
|
|
|
|
|
scale_factor,
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
commands.spawn((
|
|
|
|
|
Camera2d,
|
|
|
|
|
Camera {
|
|
|
|
|
order: 1,
|
|
|
|
|
target,
|
|
|
|
|
..default()
|
|
|
|
|
},
|
|
|
|
|
TETRIS,
|
|
|
|
|
TetrisCamera,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let img = render_target_image();
|
|
|
|
|
let target = {
|
|
|
|
|
let handle = images.add(img);
|
|
|
|
|
output_images.battler = handle.clone();
|
|
|
|
|
let scale_factor = FloatOrd(1.0);
|
|
|
|
|
|
|
|
|
|
RenderTarget::Image(ImageRenderTarget {
|
|
|
|
|
handle,
|
|
|
|
|
scale_factor,
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
commands.spawn((
|
|
|
|
|
Camera2d,
|
|
|
|
|
Camera {
|
|
|
|
|
order: 2,
|
|
|
|
|
target,
|
|
|
|
|
..default()
|
|
|
|
|
},
|
|
|
|
|
BATTLER,
|
|
|
|
|
BattlerCamera,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn init_ui(mut commands: Commands, output_images: Res<OutputImages>) {
|
|
|
|
|
commands
|
|
|
|
|
.spawn((
|
|
|
|
|
Node {
|
|
|
|
|
@ -279,7 +386,6 @@ fn init_ui(mut commands: Commands) {
|
|
|
|
|
flex_direction: FlexDirection::Column,
|
|
|
|
|
..default()
|
|
|
|
|
},
|
|
|
|
|
BackgroundColor(BLACK.into()),
|
|
|
|
|
))
|
|
|
|
|
.with_children(|parent| {
|
|
|
|
|
parent
|
|
|
|
|
@ -314,6 +420,50 @@ fn init_ui(mut commands: Commands) {
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
commands.spawn((
|
|
|
|
|
Node {
|
|
|
|
|
align_self: AlignSelf::End,
|
|
|
|
|
justify_self: JustifySelf::Center,
|
|
|
|
|
border: UiRect::all(Val::Px(5.0)),
|
|
|
|
|
..default()
|
|
|
|
|
},
|
|
|
|
|
BorderColor(BLACK.into()),
|
|
|
|
|
children![(
|
|
|
|
|
Node {
|
|
|
|
|
width: Val::Px(256.0),
|
|
|
|
|
height: Val::Px(256.0),
|
|
|
|
|
..default()
|
|
|
|
|
},
|
|
|
|
|
ImageNode {
|
|
|
|
|
image: output_images.tetris.clone(),
|
|
|
|
|
image_mode: NodeImageMode::Stretch,
|
|
|
|
|
..default()
|
|
|
|
|
},
|
|
|
|
|
)]
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
commands.spawn((
|
|
|
|
|
Node {
|
|
|
|
|
align_self: AlignSelf::Start,
|
|
|
|
|
justify_self: JustifySelf::Center,
|
|
|
|
|
border: UiRect::all(Val::Px(5.0)),
|
|
|
|
|
..default()
|
|
|
|
|
},
|
|
|
|
|
BorderColor(BLACK.into()),
|
|
|
|
|
children![(
|
|
|
|
|
Node {
|
|
|
|
|
width: Val::Px(256.0),
|
|
|
|
|
height: Val::Px(256.0),
|
|
|
|
|
..default()
|
|
|
|
|
},
|
|
|
|
|
ImageNode {
|
|
|
|
|
image: output_images.battler.clone(),
|
|
|
|
|
image_mode: NodeImageMode::Stretch,
|
|
|
|
|
..default()
|
|
|
|
|
},
|
|
|
|
|
)]
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
commands
|
|
|
|
|
.spawn((
|
|
|
|
|
Node {
|
|
|
|
|
@ -341,9 +491,7 @@ fn init_debug_ui(mut commands: Commands) {
|
|
|
|
|
.with_children(|parent| {
|
|
|
|
|
parent.spawn((
|
|
|
|
|
Node::default(),
|
|
|
|
|
children![
|
|
|
|
|
(Text::new("SHAPE"), SyncSingleton::<Shape>::default()),
|
|
|
|
|
],
|
|
|
|
|
children![(Text::new("SHAPE"), SyncSingleton::<Shape>::default()),],
|
|
|
|
|
));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
@ -555,7 +703,15 @@ fn update_position(
|
|
|
|
|
|
|
|
|
|
// TODO: Move to trigger
|
|
|
|
|
fn update_shape_blocks(
|
|
|
|
|
query: Query<(Entity, &Shape, &GridPosition), Or<(Added<Shape>, Changed<Shape>, Added<GridPosition>, Changed<GridPosition>)>>,
|
|
|
|
|
query: Query<
|
|
|
|
|
(Entity, &Shape, &GridPosition),
|
|
|
|
|
Or<(
|
|
|
|
|
Added<Shape>,
|
|
|
|
|
Changed<Shape>,
|
|
|
|
|
Added<GridPosition>,
|
|
|
|
|
Changed<GridPosition>,
|
|
|
|
|
)>,
|
|
|
|
|
>,
|
|
|
|
|
mut blocks: Query<&mut GridPosition, (With<ShapeBlock>, Without<Shape>)>,
|
|
|
|
|
mut commands: Commands,
|
|
|
|
|
visuals: Res<Visuals>,
|
|
|
|
|
@ -571,7 +727,7 @@ fn update_shape_blocks(
|
|
|
|
|
.with_related_entities::<ShapeBlock>(|parent| {
|
|
|
|
|
s.coordinates(center).for_each(|gp| {
|
|
|
|
|
parent
|
|
|
|
|
.spawn((mesh.clone(), mat.clone(), gp.unwrap(), Block))
|
|
|
|
|
.spawn((mesh.clone(), mat.clone(), gp.unwrap(), Block, TETRIS))
|
|
|
|
|
.observe(movement);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
@ -673,8 +829,12 @@ fn add_piece(mut commands: Commands, mut shapes: ResMut<ShapesBuffer>) {
|
|
|
|
|
let this_shape = shapes.next.pop_front().unwrap();
|
|
|
|
|
commands
|
|
|
|
|
.spawn((
|
|
|
|
|
GridPosition { y: Y_MAX - this_shape.height(), ..default() },
|
|
|
|
|
GridPosition {
|
|
|
|
|
y: Y_MAX - this_shape.height(),
|
|
|
|
|
..default()
|
|
|
|
|
},
|
|
|
|
|
this_shape,
|
|
|
|
|
TETRIS,
|
|
|
|
|
))
|
|
|
|
|
.observe(movement)
|
|
|
|
|
.observe(swap);
|
|
|
|
|
@ -690,10 +850,13 @@ fn clear_line(
|
|
|
|
|
) {
|
|
|
|
|
let mut cleared_lines: Vec<usize> = changed_lines
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|e| try { (Some(e)?, line_blocks.get(e).ok()?, 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))| {
|
|
|
|
|
if lb.0.len() == 10 {
|
|
|
|
|
commands.entity(e).despawn_related::<LineBlocks>().insert(LineBlocks::default());
|
|
|
|
|
commands
|
|
|
|
|
.entity(e)
|
|
|
|
|
.despawn_related::<LineBlocks>()
|
|
|
|
|
.insert(LineBlocks::default());
|
|
|
|
|
score.0 += 1;
|
|
|
|
|
info!("New score: {:?}", score.0);
|
|
|
|
|
Some(*i)
|
|
|
|
|
@ -711,9 +874,14 @@ fn clear_line(
|
|
|
|
|
{
|
|
|
|
|
debug_assert_eq!(lines.iter().count(), 20, "There should be 20 lines");
|
|
|
|
|
// Check that all line numbers are present
|
|
|
|
|
lines.iter().map(|Line(i)| i).sorted().enumerate().for_each(|(i, line_num)| {
|
|
|
|
|
debug_assert_eq!(i, *line_num, "Line numbers should match their sorted index");
|
|
|
|
|
});
|
|
|
|
|
lines
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|Line(i)| i)
|
|
|
|
|
.sorted()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.for_each(|(i, line_num)| {
|
|
|
|
|
debug_assert_eq!(i, *line_num, "Line numbers should match their sorted index");
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let original_cleared_lines_len = cleared_lines.len();
|
|
|
|
|
@ -785,7 +953,9 @@ fn movement(
|
|
|
|
|
Movement::Left => vec![center.with_offset(-1, 0)],
|
|
|
|
|
Movement::Right => vec![center.with_offset(1, 0)],
|
|
|
|
|
Movement::Rotate => vec![Ok(*center)],
|
|
|
|
|
Movement::Skip => (1..=center.y).map(|i| center.with_offset(0, -(i as isize))).collect(),
|
|
|
|
|
Movement::Skip => (1..=center.y)
|
|
|
|
|
.map(|i| center.with_offset(0, -(i as isize)))
|
|
|
|
|
.collect(),
|
|
|
|
|
};
|
|
|
|
|
let new_shape = match trigger.event() {
|
|
|
|
|
Movement::Down | Movement::Left | Movement::Right | Movement::Skip => *this_shape,
|
|
|
|
|
@ -860,7 +1030,10 @@ fn swap(
|
|
|
|
|
// Copy current shape into store
|
|
|
|
|
// De-activate entity
|
|
|
|
|
store.0 = Some(*shapes.get(trigger.target()).unwrap());
|
|
|
|
|
commands.entity(trigger.target()).despawn_related::<ShapeBlocks>().despawn();
|
|
|
|
|
commands
|
|
|
|
|
.entity(trigger.target())
|
|
|
|
|
.despawn_related::<ShapeBlocks>()
|
|
|
|
|
.despawn();
|
|
|
|
|
}
|
|
|
|
|
Some(inner) => {
|
|
|
|
|
// Copy current shape into store
|
|
|
|
|
@ -883,7 +1056,8 @@ fn deactivate_shape(
|
|
|
|
|
let GridPosition { y, .. } = grid_positions.get(block).unwrap();
|
|
|
|
|
if let Some(parent_line) = lines
|
|
|
|
|
.iter()
|
|
|
|
|
.find_map(|(e, Line(i))| (*y == *i).then_some(e)) {
|
|
|
|
|
.find_map(|(e, Line(i))| (*y == *i).then_some(e))
|
|
|
|
|
{
|
|
|
|
|
commands
|
|
|
|
|
.entity(parent_line)
|
|
|
|
|
.add_one_related::<LineBlock>(block);
|
|
|
|
|
|