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.
1254 lines
37 KiB
Rust
1254 lines
37 KiB
Rust
#![feature(try_blocks)]
|
|
|
|
// Bevy basically forces "complex types" with Querys
|
|
#![allow(clippy::type_complexity)]
|
|
|
|
use bevy::{
|
|
asset::RenderAssetUsages,
|
|
math::{FloatOrd},
|
|
render::{
|
|
mesh::MeshAabb,
|
|
camera::{ImageRenderTarget, RenderTarget},
|
|
render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages},
|
|
view::RenderLayers,
|
|
},
|
|
};
|
|
use games::*;
|
|
use itertools::Itertools;
|
|
|
|
#[cfg(test)]
|
|
mod test;
|
|
|
|
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
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(BaseGamePlugin {
|
|
name: "falling-block-adventure".into(),
|
|
target_resolution: (640.0, 480.0).into(),
|
|
game_type: GameType::Two,
|
|
..default()
|
|
})
|
|
.init_state::<GameState>()
|
|
.init_resource::<ShapesBuffer>()
|
|
.init_resource::<ShapeStore>()
|
|
.init_resource::<Score>()
|
|
.init_resource::<OutputImages>()
|
|
.add_systems(
|
|
Startup,
|
|
(
|
|
init_tetris,
|
|
init_debug_ui,
|
|
init_cameras,
|
|
init_ui.after(init_cameras),
|
|
init_battler,
|
|
),
|
|
)
|
|
// Input and basic systems
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
kb_input.run_if(on_message::<KeyboardInput>),
|
|
toggle_state_visibility::<GameState>.run_if(state_changed::<GameState>),
|
|
),
|
|
)
|
|
.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),
|
|
falling
|
|
.run_if(in_state(GameState::Falling))
|
|
.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),
|
|
// Clearing lines systems
|
|
clear_line.run_if(any_component_changed::<LineBlocks>),
|
|
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(clock_cycle(5.0))
|
|
),
|
|
)
|
|
// UI systems
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
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_singleton_to_ui::<Shape>.run_if(any_component_changed::<Shape>),
|
|
),
|
|
)
|
|
.add_observer(deal_damage)
|
|
.run();
|
|
}
|
|
|
|
const SCALE: f32 = 30.0;
|
|
|
|
// Declare the size of the play area
|
|
const X_MAX: usize = 10;
|
|
const Y_MAX: usize = 20;
|
|
|
|
// The blocks making up this shape
|
|
#[derive(Component)]
|
|
#[relationship_target(relationship = ShapeBlock)]
|
|
struct ShapeBlocks(Vec<Entity>);
|
|
|
|
/// A part of a piece, i.e., a single square of a piece
|
|
#[derive(Component, Debug)]
|
|
#[relationship(relationship_target = ShapeBlocks)]
|
|
struct ShapeBlock {
|
|
#[relationship]
|
|
shape: Entity,
|
|
}
|
|
|
|
// The blocks making up this shape
|
|
#[derive(Component, Default, Debug)]
|
|
#[relationship_target(relationship = LineBlock)]
|
|
struct LineBlocks(Vec<Entity>);
|
|
|
|
/// A part of a piece, i.e., a single square of a piece
|
|
#[derive(Component, Debug)]
|
|
#[require(Transform, Visibility)]
|
|
#[relationship(relationship_target = LineBlocks)]
|
|
struct LineBlock {
|
|
#[relationship]
|
|
line: Entity,
|
|
}
|
|
|
|
// A line holds up to 10 blocks before being cleared
|
|
#[derive(Component, Debug, Clone, Copy)]
|
|
struct Line(usize);
|
|
|
|
// Just marks a block either of a shape or line
|
|
#[derive(Component, Debug)]
|
|
struct Block;
|
|
|
|
#[derive(Component, Debug, Clone, Copy, PartialEq)]
|
|
#[require(Transform, Visibility)]
|
|
struct GridPosition {
|
|
x: usize,
|
|
y: usize,
|
|
}
|
|
|
|
impl GridPosition {
|
|
fn with_offset(self, other_x: isize, other_y: isize) -> Result<GridPosition, GameError> {
|
|
let x = self.x as isize + other_x;
|
|
let y = self.y as isize + other_y;
|
|
|
|
if x >= X_MAX as isize {
|
|
Err(GameError::OutOfBoundsLeft)
|
|
} else if x < 0 {
|
|
Err(GameError::OutOfBoundsRight)
|
|
} else if y < 0 {
|
|
Err(GameError::OutOfBoundsDown)
|
|
} else {
|
|
Ok(GridPosition {
|
|
x: x as usize,
|
|
y: y as usize,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for GridPosition {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "({},{})", self.x, self.y)
|
|
}
|
|
}
|
|
|
|
#[derive(Error, Debug, PartialEq)]
|
|
enum GameError {
|
|
#[error("Coordinates are out of bounds: Left")]
|
|
OutOfBoundsLeft,
|
|
#[error("Coordinates are out of bounds: Right")]
|
|
OutOfBoundsRight,
|
|
#[error("Coordinates are out of bounds: Down")]
|
|
OutOfBoundsDown,
|
|
#[error("Coordiante collision")]
|
|
Collision,
|
|
}
|
|
|
|
impl Default for GridPosition {
|
|
fn default() -> Self {
|
|
GridPosition { x: 5, y: Y_MAX }
|
|
}
|
|
}
|
|
|
|
impl From<&GridPosition> for Vec3 {
|
|
fn from(GridPosition { x, y }: &GridPosition) -> Vec3 {
|
|
// Grid Positions start in the bottom left of the area
|
|
// So (0, 0) is the bottom left, (0, 9) is the bottom right, etc
|
|
|
|
let x_0 = -SCALE * 5.0 + (0.5 * SCALE);
|
|
let x = x_0 + ((*x as f32) * SCALE);
|
|
|
|
let y_0 = -SCALE * 10.0 + (0.5 * SCALE);
|
|
let y = y_0 + ((*y as f32) * SCALE);
|
|
Vec3::new(x, y, 0.0)
|
|
}
|
|
}
|
|
|
|
impl From<(usize, usize)> for GridPosition {
|
|
fn from((x, y): (usize, usize)) -> GridPosition {
|
|
GridPosition { x, y }
|
|
}
|
|
}
|
|
|
|
impl std::ops::Add for GridPosition {
|
|
type Output = Self;
|
|
|
|
fn add(self, GridPosition { x: x2, y: y2 }: Self) -> Self {
|
|
GridPosition {
|
|
x: self.x + x2,
|
|
y: self.y + y2,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::ops::AddAssign<&GridPosition> for GridPosition {
|
|
fn add_assign(&mut self, rhs: &GridPosition) {
|
|
*self = *self + *rhs;
|
|
}
|
|
}
|
|
|
|
#[derive(States, Clone, Eq, PartialEq, Debug, Hash, Default, Component)]
|
|
enum GameState {
|
|
#[default]
|
|
Falling,
|
|
Pause,
|
|
}
|
|
|
|
#[derive(Resource, Debug)]
|
|
struct Visuals {
|
|
material: Handle<ColorMaterial>,
|
|
mesh: Handle<Mesh>,
|
|
}
|
|
|
|
#[derive(Resource, Debug, Default)]
|
|
struct Score(usize);
|
|
|
|
impl Display for Score {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}", self.0)
|
|
}
|
|
}
|
|
|
|
#[derive(Component, Debug)]
|
|
struct Health(f32);
|
|
|
|
/// ShapesBuffer resource stores non-active shapes
|
|
#[derive(Resource, Debug, Default)]
|
|
struct ShapesBuffer {
|
|
/// Next stores a vector of 2N shapes that will come up in play
|
|
next: VecDeque<Shape>,
|
|
}
|
|
|
|
impl Display for ShapesBuffer {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
if let Some(shape) = self.next.front() {
|
|
write!(f, "{shape}")
|
|
} else {
|
|
write!(f, "ERR")
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Resource, Debug, Default)]
|
|
struct ShapeStore(Option<Shape>);
|
|
|
|
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, "---"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct GridBackground;
|
|
|
|
fn init_tetris(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<ColorMaterial>>,
|
|
) {
|
|
let mesh = meshes.add(Rectangle::new(SCALE, SCALE));
|
|
let block_material = materials.add(ColorMaterial {
|
|
color: WHITE.into(),
|
|
..default()
|
|
});
|
|
let grid_material = materials.add(ColorMaterial {
|
|
color: BLACK.into(),
|
|
..default()
|
|
});
|
|
commands.insert_resource(Visuals {
|
|
material: block_material.clone(),
|
|
mesh: mesh.clone(),
|
|
});
|
|
|
|
(0..Y_MAX).for_each(|y| {
|
|
// Spawn tiles for this line
|
|
(0..X_MAX).for_each(|x| {
|
|
commands.spawn((
|
|
Mesh2d(mesh.clone()),
|
|
MeshMaterial2d(grid_material.clone()),
|
|
GridPosition { x, y },
|
|
Transform::from_xyz(0.0, 0.0, -1.0),
|
|
GridBackground,
|
|
TETRIS,
|
|
));
|
|
});
|
|
|
|
// Spawn line for holding blocks
|
|
commands.spawn((Line(y), LineBlocks::default(), TETRIS));
|
|
});
|
|
}
|
|
|
|
#[derive(Component, Debug)]
|
|
struct Protagonist;
|
|
|
|
#[derive(Component, Debug)]
|
|
struct Enemy;
|
|
|
|
fn init_battler(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<ColorMaterial>>,
|
|
) {
|
|
{
|
|
let mat = materials.add(ColorMaterial {
|
|
color: BLUE.into(),
|
|
..default()
|
|
});
|
|
let mesh = meshes.add(Ellipse::new(SCALE, SCALE * 2.0));
|
|
let t = Transform::from_xyz(-3.0 * SCALE, 0.0, 0.0);
|
|
commands.spawn((
|
|
Mesh2d(mesh),
|
|
MeshMaterial2d(mat),
|
|
t,
|
|
BATTLER,
|
|
Health(100.0),
|
|
Name::new("Protagonist"),
|
|
Protagonist,
|
|
));
|
|
}
|
|
|
|
{
|
|
let mat = materials.add(ColorMaterial {
|
|
color: RED.into(),
|
|
..default()
|
|
});
|
|
let mesh = meshes.add(Ellipse::new(SCALE, SCALE));
|
|
let t = Transform::from_xyz(3.0 * SCALE, 0.0, 0.0);
|
|
commands.spawn((
|
|
Mesh2d(mesh),
|
|
MeshMaterial2d(mat),
|
|
t,
|
|
BATTLER,
|
|
Health(100.0),
|
|
Name::new("ENEMY"),
|
|
Enemy,
|
|
));
|
|
}
|
|
}
|
|
|
|
#[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((width, height): (u32, u32)) -> Image {
|
|
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 (width, height) = (SCALE as u32 * X_MAX as u32, SCALE as u32 * Y_MAX as u32);
|
|
let img = render_target_image((width, height));
|
|
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,
|
|
clear_color: ClearColorConfig::Custom(WHITE.into()),
|
|
..default()
|
|
},
|
|
TETRIS,
|
|
TetrisCamera,
|
|
));
|
|
}
|
|
|
|
{
|
|
let (width, height) = (SCALE as u32 * Y_MAX as u32, SCALE as u32 * X_MAX as u32);
|
|
let img = render_target_image((width, height));
|
|
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,
|
|
clear_color: ClearColorConfig::Custom(WHITE.into()),
|
|
..default()
|
|
},
|
|
BATTLER,
|
|
BattlerCamera,
|
|
));
|
|
}
|
|
}
|
|
|
|
fn init_ui(mut commands: Commands, output_images: Res<OutputImages>, images: Res<Assets<Image>>) {
|
|
commands
|
|
.spawn((Node {
|
|
align_self: AlignSelf::Center,
|
|
justify_self: JustifySelf::End,
|
|
flex_direction: FlexDirection::Column,
|
|
..default()
|
|
},))
|
|
.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("Swap:"));
|
|
parent.spawn((Text::new("???"), SyncResource::<ShapeStore>::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::End,
|
|
justify_self: JustifySelf::Center,
|
|
border: UiRect::all(Val::Px(5.0)),
|
|
..default()
|
|
},
|
|
BorderColor(WHITE),
|
|
))
|
|
.with_children(|parent| {
|
|
let img = images.get(&output_images.tetris).unwrap();
|
|
parent.spawn((
|
|
Node {
|
|
width: Val::Px(img.size_f32().x * 0.75),
|
|
height: Val::Px(img.size_f32().y * 0.75),
|
|
..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(WHITE),
|
|
))
|
|
.with_children(|parent| {
|
|
let img = images.get(&output_images.battler).unwrap();
|
|
parent.spawn((
|
|
Node {
|
|
width: Val::Px(img.size_f32().x * 0.75),
|
|
height: Val::Px(img.size_f32().y * 0.75),
|
|
..default()
|
|
},
|
|
ImageNode {
|
|
image: output_images.battler.clone(),
|
|
image_mode: NodeImageMode::Stretch,
|
|
..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) {
|
|
commands
|
|
.spawn((
|
|
Node {
|
|
top: Val::Px(0.0),
|
|
left: Val::Px(0.0),
|
|
..default()
|
|
},
|
|
DebuggingState::On,
|
|
))
|
|
.with_children(|parent| {
|
|
parent.spawn((
|
|
Node::default(),
|
|
children![(Text::new("SHAPE"), SyncSingleton::<Shape>::default()),],
|
|
));
|
|
});
|
|
}
|
|
|
|
#[derive(Component, Debug, Clone, Copy)]
|
|
enum Shape {
|
|
M4(Mat4),
|
|
M3(Mat3),
|
|
}
|
|
|
|
impl Default for Shape {
|
|
fn default() -> Self {
|
|
Self::new_t()
|
|
}
|
|
}
|
|
|
|
impl Display for Shape {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}", self.as_ascii())
|
|
}
|
|
}
|
|
|
|
impl Shape {
|
|
fn from_mat4(input: Mat4) -> Self {
|
|
Self::M4(input)
|
|
}
|
|
|
|
fn from_mat3(input: Mat3) -> Self {
|
|
Self::M3(input)
|
|
}
|
|
|
|
fn new_o() -> Self {
|
|
Self::from_mat4(Mat4::from_cols_array_2d(&[
|
|
[0., 0., 0., 0.],
|
|
[0., 1., 1., 0.],
|
|
[0., 1., 1., 0.],
|
|
[0., 0., 0., 0.],
|
|
]))
|
|
}
|
|
|
|
fn new_t() -> Self {
|
|
Self::from_mat3(Mat3::from_cols_array_2d(&[
|
|
[0., 1., 0.],
|
|
[1., 1., 1.],
|
|
[0., 0., 0.],
|
|
]))
|
|
}
|
|
|
|
fn new_l() -> Self {
|
|
Self::from_mat4(Mat4::from_cols_array_2d(&[
|
|
[0., 0., 0., 0.],
|
|
[0., 1., 0., 0.],
|
|
[0., 1., 0., 0.],
|
|
[0., 1., 1., 0.],
|
|
]))
|
|
}
|
|
|
|
fn new_j() -> Self {
|
|
Self::from_mat4(Mat4::from_cols_array_2d(&[
|
|
[0., 0., 0., 0.],
|
|
[0., 0., 1., 0.],
|
|
[0., 0., 1., 0.],
|
|
[0., 1., 1., 0.],
|
|
]))
|
|
}
|
|
|
|
fn new_s() -> Self {
|
|
Self::from_mat4(Mat4::from_cols_array_2d(&[
|
|
[0., 0., 0., 0.],
|
|
[0., 1., 1., 0.],
|
|
[1., 1., 0., 0.],
|
|
[0., 0., 0., 0.],
|
|
]))
|
|
}
|
|
|
|
fn new_z() -> Self {
|
|
Self::from_mat4(Mat4::from_cols_array_2d(&[
|
|
[0., 0., 0., 0.],
|
|
[1., 1., 0., 0.],
|
|
[0., 1., 1., 0.],
|
|
[0., 0., 0., 0.],
|
|
]))
|
|
}
|
|
|
|
fn new_i() -> Self {
|
|
Self::from_mat4(Mat4::from_cols_array_2d(&[
|
|
[0., 0., 1., 0.],
|
|
[0., 0., 1., 0.],
|
|
[0., 0., 1., 0.],
|
|
[0., 0., 1., 0.],
|
|
]))
|
|
}
|
|
|
|
// Rotates 90 degrees to the right
|
|
// https://stackoverflow.com/a/8664879
|
|
fn rotated(&self) -> Self {
|
|
match self {
|
|
Self::M4(inner) => {
|
|
let mut new_self = inner.transpose();
|
|
for i in 0..4 {
|
|
let col = new_self.col_mut(i);
|
|
*col = Vec4::new(col[3], col[2], col[1], col[0]);
|
|
}
|
|
Self::M4(new_self)
|
|
}
|
|
Self::M3(inner) => {
|
|
let mut new_self = inner.transpose();
|
|
for i in 0..3 {
|
|
let col = new_self.col_mut(i);
|
|
*col = Vec3::new(col[2], col[1], col[0]);
|
|
}
|
|
Self::M3(new_self)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn coordinates(
|
|
&self,
|
|
center: &GridPosition,
|
|
) -> impl Iterator<Item = Result<GridPosition, GameError>> {
|
|
let mut v: Vec<Result<GridPosition, GameError>> = Vec::new();
|
|
match self {
|
|
Self::M4(inner) => {
|
|
for (i, y) in (-1..3).rev().enumerate() {
|
|
let c = inner.col(i);
|
|
for (j, x) in (-1..3).enumerate() {
|
|
if c[j] == 1.0 {
|
|
v.push(center.with_offset(x, y));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Self::M3(inner) => {
|
|
for (i, y) in (-1..2).rev().enumerate() {
|
|
let c = inner.col(i);
|
|
for (j, x) in (-1..2).enumerate() {
|
|
if c[j] == 1.0 {
|
|
v.push(center.with_offset(x, y));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
v.into_iter()
|
|
}
|
|
|
|
fn as_ascii(&self) -> String {
|
|
let mut output = String::default();
|
|
|
|
match self {
|
|
Self::M4(this) => {
|
|
for i in 0..4 {
|
|
let col = this.col(i).to_array();
|
|
output += format!("{}{}{}{}\n", col[0], col[1], col[2], col[3]).as_str();
|
|
}
|
|
}
|
|
Self::M3(this) => {
|
|
for i in 0..3 {
|
|
let col = this.col(i).to_array();
|
|
output += format!("{}{}{}\n", col[0], col[1], col[2]).as_str();
|
|
}
|
|
}
|
|
};
|
|
|
|
output
|
|
}
|
|
|
|
fn height(&self) -> usize {
|
|
let mut x = 0;
|
|
|
|
match self {
|
|
Self::M4(this) => {
|
|
for i in 0..4 {
|
|
if this.col(i).to_array().contains(&1.0) {
|
|
x += 1
|
|
}
|
|
}
|
|
}
|
|
Self::M3(this) => {
|
|
for i in 0..3 {
|
|
if this.col(i).to_array().contains(&1.0) {
|
|
x += 1
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
x
|
|
}
|
|
}
|
|
|
|
// TODO: move to event
|
|
fn update_position(
|
|
mut changed: Query<
|
|
(Entity, &GridPosition, &mut Transform),
|
|
Or<(Added<GridPosition>, Changed<GridPosition>)>,
|
|
>,
|
|
) {
|
|
changed.iter_mut().for_each(|(e, gp, mut t)| {
|
|
let v3: Vec3 = gp.into();
|
|
debug!(
|
|
"Updating {e} with grid position {:?} to coordinates {:?}",
|
|
gp, v3
|
|
);
|
|
|
|
t.translation.x = v3.x;
|
|
t.translation.y = v3.y;
|
|
});
|
|
}
|
|
|
|
// TODO: Move to event
|
|
fn update_shape_blocks(
|
|
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>,
|
|
) {
|
|
query.iter().for_each(|(e, s, center)| {
|
|
debug!("Setting piece: {e:?} {center:?}\n{}", s.as_ascii());
|
|
|
|
if blocks.is_empty() {
|
|
let mesh = Mesh2d(visuals.mesh.clone());
|
|
let mat = MeshMaterial2d(visuals.material.clone());
|
|
commands
|
|
.entity(e)
|
|
.with_related_entities::<ShapeBlock>(|parent| {
|
|
s.coordinates(center).for_each(|gp| {
|
|
parent
|
|
.spawn((mesh.clone(), mat.clone(), gp.unwrap(), Block, TETRIS))
|
|
.observe(movement);
|
|
});
|
|
});
|
|
} else {
|
|
let mut p = s.coordinates(center);
|
|
blocks.iter_mut().for_each(|mut gp| {
|
|
*gp = p.next().unwrap().unwrap();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
fn kb_input(
|
|
mut events: MessageReader<KeyboardInput>,
|
|
mut query: Query<(Entity, &mut Shape)>,
|
|
curr: Res<State<GameState>>,
|
|
mut next: ResMut<NextState<GameState>>,
|
|
mut commands: Commands,
|
|
) {
|
|
events.read().for_each(
|
|
|KeyboardInput {
|
|
key_code, state, ..
|
|
}| {
|
|
if let ButtonState::Pressed = state {
|
|
query.iter_mut().for_each(|(e, mut s)| {
|
|
match key_code {
|
|
// Up arrow should rotate if in falling mode
|
|
// Only move up if in falling::off mode
|
|
KeyCode::ArrowUp => {
|
|
commands.entity(e).event(Movement::Rotate);
|
|
}
|
|
KeyCode::ArrowDown => {
|
|
commands.entity(e).event(Movement::Down);
|
|
}
|
|
KeyCode::ArrowLeft => {
|
|
commands.entity(e).event(Movement::Left);
|
|
}
|
|
KeyCode::ArrowRight => {
|
|
commands.entity(e).event(Movement::Right);
|
|
}
|
|
KeyCode::Enter => {
|
|
commands.entity(e).event(Movement::Skip);
|
|
}
|
|
KeyCode::Space => {
|
|
commands.entity(e).event(Swap);
|
|
}
|
|
KeyCode::Escape => next.set(match curr.get() {
|
|
GameState::Falling => GameState::Pause,
|
|
GameState::Pause => GameState::Falling,
|
|
}),
|
|
KeyCode::Digit1 => *s = Shape::new_t(),
|
|
KeyCode::Digit2 => *s = Shape::new_o(),
|
|
KeyCode::Digit3 => *s = Shape::new_l(),
|
|
KeyCode::Digit4 => *s = Shape::new_j(),
|
|
KeyCode::Digit5 => *s = Shape::new_s(),
|
|
KeyCode::Digit6 => *s = Shape::new_z(),
|
|
KeyCode::Digit7 => *s = Shape::new_i(),
|
|
_ => (),
|
|
}
|
|
});
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
fn falling(mut shape: Query<Entity, With<Shape>>, mut commands: Commands) {
|
|
shape.iter_mut().for_each(|e| {
|
|
debug!("Making {:?} fall", e);
|
|
commands.entity(e).event(Movement::Down);
|
|
});
|
|
}
|
|
|
|
// Run condition that returns `true` every `n` seconds
|
|
// TODO: Update a resource with the current tick
|
|
fn clock_cycle(n: f32) -> impl FnMut(Res<Time>, Local<f32>) -> bool {
|
|
move |t: Res<Time>, mut buf: Local<f32>| -> bool {
|
|
*buf += t.delta_secs();
|
|
if *buf > n {
|
|
*buf = 0.0;
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
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()
|
|
},
|
|
this_shape,
|
|
TETRIS,
|
|
))
|
|
.observe(movement)
|
|
.observe(swap);
|
|
}
|
|
|
|
/// When a line reaches 10 blocks, clear it
|
|
fn clear_line(
|
|
changed_lines: Query<Entity, Changed<LineBlocks>>,
|
|
mut lines: Query<&mut Line>,
|
|
line_blocks: Query<&LineBlocks>,
|
|
mut score: ResMut<Score>,
|
|
mut commands: Commands,
|
|
) {
|
|
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, lb, Line(i))| {
|
|
if lb.0.len() == 10 {
|
|
commands
|
|
.entity(e)
|
|
.despawn_related::<LineBlocks>()
|
|
.insert(LineBlocks::default());
|
|
score.0 += 1;
|
|
info!("New score: {:?}", score.0);
|
|
Some(*i)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.sorted()
|
|
.collect();
|
|
|
|
if !cleared_lines.is_empty() {
|
|
info!("Cleared lines: {:?}", cleared_lines);
|
|
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
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");
|
|
});
|
|
}
|
|
|
|
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();
|
|
debug!("Moving line {:?}->{:?}", l.0, Y_MAX - 1 - offset);
|
|
l.0 = Y_MAX - 1 - offset;
|
|
cleared_lines.pop();
|
|
} else {
|
|
debug!("Moving line {:?}->{:?}", l.0, l.0 - cleared_lines.len());
|
|
l.0 -= cleared_lines.len();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
fn adjust_block_lines(
|
|
query: Query<(Entity, &Line), Changed<Line>>,
|
|
parent: Query<&LineBlocks>,
|
|
mut blocks: Query<&mut GridPosition>,
|
|
) {
|
|
query.iter().for_each(|(e, Line(i))| {
|
|
parent.iter_descendants(e).for_each(|block| {
|
|
if let Ok(mut gp) = blocks.get_mut(block) {
|
|
gp.y = *i;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/// Swap the current piece out
|
|
#[derive(Message, Copy, Clone, PartialEq)]
|
|
struct Swap;
|
|
|
|
/// Movement events evented on the piece
|
|
#[derive(Message, Copy, Clone, PartialEq)]
|
|
enum Movement {
|
|
Down,
|
|
Left,
|
|
Right,
|
|
Rotate,
|
|
Skip,
|
|
}
|
|
|
|
// TODO: When out of bounds left/right, try to move piece away from wall
|
|
fn movement(
|
|
event: On<Movement>,
|
|
mut grid_positions: Query<
|
|
&mut GridPosition,
|
|
Or<(With<ShapeBlock>, With<ShapeBlocks>, Without<LineBlock>)>,
|
|
>,
|
|
mut shape: Query<&mut Shape>,
|
|
inactive: Query<&GridPosition, (Without<ShapeBlock>, Without<ShapeBlocks>, With<LineBlock>)>,
|
|
mut commands: Commands,
|
|
) {
|
|
if let (Ok(this_shape), Ok(center)) = (
|
|
shape.get_mut(event.entity),
|
|
grid_positions.get(event.entity),
|
|
) {
|
|
let new_positions = match event.event() {
|
|
Movement::Down => vec![center.with_offset(0, -1)],
|
|
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(),
|
|
};
|
|
let new_shape = match event.event() {
|
|
Movement::Down | Movement::Left | Movement::Right | Movement::Skip => *this_shape,
|
|
Movement::Rotate => this_shape.rotated(),
|
|
};
|
|
debug!(
|
|
"Proposed change: {:?}\n{}",
|
|
new_positions,
|
|
new_shape.as_ascii()
|
|
);
|
|
for position in new_positions {
|
|
match position {
|
|
Err(GameError::OutOfBoundsLeft) | Err(GameError::OutOfBoundsRight) => (), // Do nothing
|
|
Err(GameError::OutOfBoundsDown) => {
|
|
commands.entity(event.entity).remove::<Shape>();
|
|
}
|
|
Err(GameError::Collision) => panic!("This shouldn't happen!"),
|
|
Ok(new_center) => {
|
|
let new_blocks = new_shape.coordinates(&new_center);
|
|
for block_gp in new_blocks {
|
|
match block_gp {
|
|
Err(GameError::OutOfBoundsLeft) | Err(GameError::OutOfBoundsRight) => {
|
|
return;
|
|
} // Do nothing
|
|
Err(GameError::OutOfBoundsDown) => {
|
|
commands.entity(event.entity).remove::<Shape>();
|
|
return;
|
|
}
|
|
Err(GameError::Collision) => panic!("This shouldn't happen!"),
|
|
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() == Movement::Down {
|
|
// De-activate this piece
|
|
commands.entity(event.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();
|
|
*gp = new_center;
|
|
|
|
// Update shape/rotation
|
|
let mut s = shape.get_mut(event.entity).unwrap();
|
|
*s = new_shape;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
warn!("Oned movement on non-shape entity");
|
|
}
|
|
}
|
|
|
|
fn swap(
|
|
event: On<Swap>,
|
|
mut shapes: Query<&mut Shape>,
|
|
mut store: ResMut<ShapeStore>,
|
|
mut commands: Commands,
|
|
) {
|
|
info!("Swap called");
|
|
match store.0.as_mut() {
|
|
None => {
|
|
// Copy current shape into store
|
|
// De-activate entity
|
|
store.0 = Some(*shapes.get(event.entity).unwrap());
|
|
commands
|
|
.entity(event.entity)
|
|
.despawn_related::<ShapeBlocks>()
|
|
.despawn();
|
|
}
|
|
Some(inner) => {
|
|
// Copy current shape into store
|
|
// Copy old shape into entity
|
|
let mut curr = shapes.get_mut(event.entity).unwrap();
|
|
std::mem::swap(&mut (*inner), &mut (*curr));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn deactivate_shape(
|
|
mut events: RemovedComponents<Shape>,
|
|
grid_positions: Query<&GridPosition, With<ShapeBlock>>,
|
|
parent: Query<&ShapeBlocks>,
|
|
lines: Query<(Entity, &Line), With<LineBlocks>>,
|
|
mut commands: Commands,
|
|
) {
|
|
events.read().for_each(|target| {
|
|
parent.iter_descendants(target).for_each(|block| {
|
|
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))
|
|
{
|
|
commands
|
|
.entity(parent_line)
|
|
.add_one_related::<LineBlock>(block);
|
|
} else {
|
|
error!("wtf?");
|
|
}
|
|
});
|
|
commands.entity(target).despawn();
|
|
});
|
|
}
|
|
|
|
fn update_next_shapes(mut buffer: ResMut<ShapesBuffer>) {
|
|
// If the buffer contains less than n+1 shapes (where n is the number of possible shapes)
|
|
// Ideally we have between 1n and 2n shapes in the `next` buffer
|
|
while buffer.next.len() < 8 {
|
|
// TODO: Shuffle these!
|
|
buffer.next.extend([
|
|
Shape::new_o(),
|
|
Shape::new_t(),
|
|
Shape::new_l(),
|
|
Shape::new_j(),
|
|
Shape::new_s(),
|
|
Shape::new_z(),
|
|
Shape::new_i(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
fn assert_grid_position_uniqueness(
|
|
grid_positions: Query<&GridPosition, (Without<GridBackground>, Without<Shape>)>,
|
|
) {
|
|
grid_positions.iter_combinations().for_each(|[a, b]| {
|
|
assert_ne!(a, b, "Two entities are in the same grid position!");
|
|
});
|
|
}
|
|
|
|
fn sync_health(
|
|
query: Query<(Entity, &Health, &Mesh2d), Or<(Changed<Health>, Added<Health>)>>,
|
|
parent: Query<&Children>,
|
|
meshes: Res<Assets<Mesh>>,
|
|
mut texts: Query<&mut Text2d>,
|
|
mut commands: Commands,
|
|
) {
|
|
query.iter().for_each(|(e, h, m)| {
|
|
if let Some(child) = parent
|
|
.iter_descendants(e)
|
|
.find(|child| texts.contains(*child))
|
|
{
|
|
info!("Updating health");
|
|
let mut t = texts.get_mut(child).unwrap();
|
|
t.0 = format!("{}", h.0);
|
|
} else {
|
|
info!("Creating health display");
|
|
commands.entity(e).with_children(|parent| {
|
|
let mesh = meshes.get(&m.0).unwrap();
|
|
let aabb = mesh.compute_aabb().unwrap();
|
|
let offset = Vec3::new(0.0, aabb.half_extents.y + 10.0, 0.0);
|
|
parent.spawn((
|
|
Text2d(format!("{}", h.0)),
|
|
TextColor(BLACK.into()),
|
|
Transform::from_translation(offset),
|
|
BATTLER,
|
|
));
|
|
});
|
|
}
|
|
})
|
|
}
|
|
|
|
#[derive(Message)]
|
|
struct Damage {
|
|
quantity: f32
|
|
}
|
|
|
|
fn deal_damage(
|
|
event: On<Damage>,
|
|
mut healths: Query<&mut Health>
|
|
) {
|
|
healths.get_mut(event.entity).unwrap().0 -= event.event().quantity
|
|
}
|
|
|
|
fn damage_on_place_shape(
|
|
mut events: RemovedComponents<Shape>,
|
|
enemies: Query<Entity, With<Enemy>>,
|
|
mut commands: Commands,
|
|
) {
|
|
events.read().for_each(|_| {
|
|
enemies.iter().for_each(|e| {
|
|
commands.entity(e).event(Damage { quantity: 1.0 });
|
|
});
|
|
});
|
|
}
|
|
|
|
fn damage_on_clear_line(
|
|
mut events: RemovedComponents<LineBlock>,
|
|
enemies: Query<Entity, With<Enemy>>,
|
|
mut commands: Commands,
|
|
) {
|
|
events.read().for_each(|_| {
|
|
enemies.iter().for_each(|e| {
|
|
commands.entity(e).event(Damage { quantity: 1.0 });
|
|
});
|
|
});
|
|
}
|
|
|
|
fn damage_over_time(
|
|
protagonist: Single<Entity, With<Protagonist>>,
|
|
mut commands: Commands,
|
|
) {
|
|
commands.entity(*protagonist).event(Damage { quantity: 1.0 });
|
|
}
|