From b576de94e46b761eb7e1a3f6b1b04ac924e178ac Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Thu, 4 Dec 2025 22:27:27 -0800 Subject: [PATCH] restarting on tetris --- Cargo.lock | 50 ++ Cargo.toml | 2 +- src/bin/tetris/DESIGN.md | 45 -- src/bin/tetris/README.md | 7 - src/bin/tetris/TODO.md | 15 - src/bin/tetris/main.rs | 1566 +------------------------------------- src/bin/tetris/test.rs | 209 ----- src/lib.rs | 5 +- 8 files changed, 72 insertions(+), 1827 deletions(-) delete mode 100644 src/bin/tetris/DESIGN.md delete mode 100644 src/bin/tetris/README.md delete mode 100644 src/bin/tetris/TODO.md delete mode 100644 src/bin/tetris/test.rs diff --git a/Cargo.lock b/Cargo.lock index 852fcf9..34e8cbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -741,6 +741,35 @@ dependencies = [ "encase_derive_impl", ] +[[package]] +name = "bevy_feathers" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6a75a7f32e355d9232a92b3bca9f90af18f3c81232d26d1baed77f1326863ec" +dependencies = [ + "accesskit", + "bevy_a11y", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_ecs", + "bevy_input_focus", + "bevy_log", + "bevy_math", + "bevy_picking", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_text", + "bevy_ui", + "bevy_ui_render", + "bevy_ui_widgets", + "bevy_window", + "smol_str", +] + [[package]] name = "bevy_gilrs" version = "0.17.2" @@ -925,6 +954,7 @@ dependencies = [ "bevy_derive", "bevy_diagnostic", "bevy_ecs", + "bevy_feathers", "bevy_gilrs", "bevy_gizmos", "bevy_gltf", @@ -953,6 +983,7 @@ dependencies = [ "bevy_transform", "bevy_ui", "bevy_ui_render", + "bevy_ui_widgets", "bevy_utils", "bevy_window", "bevy_winit", @@ -1554,6 +1585,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "bevy_ui_widgets" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f131e13aa2ea9f8fa9af92aadfd6cf7f47d561d21510cf76854b46808c9b9bf" +dependencies = [ + "accesskit", + "bevy_a11y", + "bevy_app", + "bevy_ecs", + "bevy_input", + "bevy_input_focus", + "bevy_log", + "bevy_math", + "bevy_picking", + "bevy_reflect", + "bevy_ui", +] + [[package]] name = "bevy_utils" version = "0.17.2" diff --git a/Cargo.toml b/Cargo.toml index fd0c422..26b4d3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ version = "0.4.1" [dependencies.bevy] version = "0.17.2" -features = ["wayland", "dynamic_linking", "track_location"] +features = ["wayland", "dynamic_linking", "track_location", "experimental_bevy_feathers", "experimental_bevy_ui_widgets"] [dev-dependencies] lipsum = "*" diff --git a/src/bin/tetris/DESIGN.md b/src/bin/tetris/DESIGN.md deleted file mode 100644 index a72590e..0000000 --- a/src/bin/tetris/DESIGN.md +++ /dev/null @@ -1,45 +0,0 @@ -# Design - -## Tetris - -Matrix Multiplication for rotating pieces. - -Each piece (shape) contains a Mat4 containing a representation of it's shape. -For example: - -``` -0 1 0 0 -0 1 0 0 -0 1 0 0 -0 1 0 0 -``` - -This is the classic `line` piece. - -And here it is on it's `up` side - -``` -0 0 0 0 -1 1 1 1 -0 0 0 0 -0 0 0 0 -``` - -And here is the `t` piece - -``` -0 1 0 -1 1 1 -0 0 0 -``` - -A matrix multiplication is applied to this Mat6 to achieve a piece rotation. - -When that matrix is updated, the 4 blocks parented to the shape are moved to reflect this new shape. - -This matrix also allows us to do checks to see if any of the blocks in the shape would intersect with another piece on the board. -We can also check if a piece would go out of bounds during a move or rotation. - -We can use this to "plan -> validate -> commit" changes based on user input. - -Question: How the fuck do matrix multiplications work?? diff --git a/src/bin/tetris/README.md b/src/bin/tetris/README.md deleted file mode 100644 index d9368cf..0000000 --- a/src/bin/tetris/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Falling Blocks RPG - -This game is inspired by both Tetris and Peglin. - -Your goal is to play Tetris (or a similar falling block game) but while that is happening you are carrying out a 2d real-time combat RPG battle. - -Between battles/levels you choose a different level to go to in an overworld, choose upgrades, perks, and maybe some other stuff! diff --git a/src/bin/tetris/TODO.md b/src/bin/tetris/TODO.md deleted file mode 100644 index 48ca17b..0000000 --- a/src/bin/tetris/TODO.md +++ /dev/null @@ -1,15 +0,0 @@ -# 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 diff --git a/src/bin/tetris/main.rs b/src/bin/tetris/main.rs index 1cd2607..0ee5166 100644 --- a/src/bin/tetris/main.rs +++ b/src/bin/tetris/main.rs @@ -1,1558 +1,26 @@ -#![feature(try_blocks)] -// Bevy basically forces "complex types" with Querys -#![allow(clippy::type_complexity)] - use games::*; -#[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, 480).into(), - game_type: GameType::Two, - ..default() - }) - .init_state::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .add_message::() - .add_systems( - Startup, - ( - init_tetris, - init_debug_ui, - init_cameras, - init_ui.after(init_cameras), - init_battler, - ), - ) - .add_systems(OnEnter(DebuggingState::On), toggle_art) - .add_systems(OnExit(DebuggingState::On), toggle_art) - .add_systems( - Update, - game_state_machine.run_if(state_changed::), - ) - // Input and basic systems - .add_systems( - Update, - ( - kb_input.run_if(on_message::), - toggle_state_visibility::.run_if(state_changed::), - ), - ) - .add_systems( - Update, - ( - update_next_shapes - .run_if(resource_changed::.or(resource_added::)), - add_piece - .run_if(not(any_with_component::)) - .run_if(in_state(GameState::Play)) - .after(update_next_shapes), - falling - .run_if(in_state(GameState::Play)) - .run_if(on_timer(Duration::from_secs(1))), - movement.run_if(on_message::), - update_line_position.run_if(any_component_changed::), - update_block_position.run_if(any_component_changed::), - update_shape_blocks - .run_if( - any_component_changed:: - .or(any_component_changed::), - ) - .after(update_block_position), - deactivate_shape - .run_if(any_component_removed::) - .after(game_state_machine) - .after(update_shape_blocks), - // Clearing lines systems - clear_line.run_if(any_component_changed::), - adjust_block_lines - .run_if(any_component_changed::) - .after(clear_line), - sync_health - .run_if(any_component_changed::.or(any_component_added::)), - damage_on_place_shape.run_if(any_component_removed::), - damage_on_clear_line.run_if(any_component_removed::), - damage_over_time.run_if(on_timer(Duration::from_secs(5))), - check_level_goal.run_if(resource_changed::), - assert_grid_position_uniqueness.run_if(any_component_changed::), - toggle_art.run_if(any_component_added::), - ), - ) - // UI systems - .add_systems( - Update, - ( - sync_resource_to_ui::.run_if(resource_changed::), - sync_resource_to_ui::.run_if(resource_changed::), - sync_resource_to_ui::.run_if(resource_changed::), - sync_resource_to_ui::.run_if(resource_changed::), - sync_singleton_to_ui::.run_if(any_component_changed::), - ), - ) - .add_observer(deal_damage) - .add_observer(on_add_shape_layout) - .add_observer(on_add_health) - .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); - -/// 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); - -/// 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); - -impl From<&Line> for Vec3 { - fn from(Line(y): &Line) -> Vec3 { - let y_0 = -SCALE * 10.0 + (0.5 * SCALE); - let y = y_0 + ((*y as f32) * SCALE); - Vec3::new(0.0, y, 0.0) - } -} - -// 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 { - let x = self.x as isize + other_x; - let y = self.y as isize + other_y; - - if x >= X_MAX as isize { - Err(OutOfBoundsError::Left) - } else if x < 0 { - Err(OutOfBoundsError::Right) - } else if y < 0 { - Err(OutOfBoundsError::Down) - } 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, Clone, Copy)] -enum OutOfBoundsError { - #[error("Coordinates are out of bounds: Left")] - Left, - #[error("Coordinates are out of bounds: Right")] - Right, - #[error("Coordinates are out of bounds: Down")] - Down, -} - -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 { - // Sets up a new game - #[default] - 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>, - mut next: ResMut>, - mut goal: ResMut, - mut score: ResMut, - items: Query, Or<(With, With)>)>, - 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)] -struct Visuals { - art: Art, - mesh: Handle, - material_o: Handle, - material_t: Handle, - material_l: Handle, - material_j: Handle, - material_s: Handle, - material_z: Handle, - material_i: Handle, -} - -/// Stores the art assets that may be toggled on and off for development -#[derive(Component, Debug, Clone)] -struct Art { - texture: Handle, -} - -#[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, -} - -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); - -impl Display for ShapeStore { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.0 { - Some(s) => write!(f, "{}", ShapeLayout::from_shape(s).as_ascii()), - None => write!(f, "---"), - } - } -} - -#[derive(Component)] -struct GridBackground; - -fn init_tetris( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, - server: Res, -) { - let mesh = meshes.add(Rectangle::new(SCALE, SCALE)); - commands.insert_resource(Visuals { - mesh: mesh.clone(), - art: Art { - texture: server.load("tetris/shape_block.png"), - }, - material_o: materials.add(ColorMaterial { - color: YELLOW.into(), - ..default() - }), - material_t: materials.add(ColorMaterial { - color: PURPLE.into(), - ..default() - }), - material_l: materials.add(ColorMaterial { - color: ORANGE.into(), - ..default() - }), - material_j: materials.add(ColorMaterial { - color: BLUE.into(), - ..default() - }), - material_s: materials.add(ColorMaterial { - color: LIME.into(), - ..default() - }), - material_z: materials.add(ColorMaterial { - color: RED.into(), - ..default() - }), - material_i: materials.add(ColorMaterial { - color: AQUA.into(), - ..default() - }), - }); - - (0..Y_MAX).for_each(|y| { - // Spawn tiles for this line - (0..X_MAX).for_each(|x| { - commands.spawn(( - Mesh2d(mesh.clone()), - MeshMaterial2d(materials.add(ColorMaterial { - color: BLACK.into(), - ..default() - })), - Art { - texture: server.load("tetris/bg_block.png"), - }, - 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, - Transform::default(), - Visibility::default(), - )) - .with_children(|parent| { - // Spawn a line number next to the line so we can track it - parent.spawn(( - Text2d(format!("{}", y)), - TETRIS, - Transform::from_xyz(0.0, 0.0, 99.0), - Visibility::default(), - )); - }); - }); -} - -#[derive(Component, Debug)] -struct Protagonist; - -#[derive(Component, Debug)] -struct Enemy; - -fn init_battler( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, - server: Res, -) { - { - let mat = materials.add(ColorMaterial { - color: BLUE.into(), - ..default() - }); - let mesh = meshes.add(Ellipse::new(SCALE, SCALE * 2.0)); - let art = Art { - texture: server.load("tetris/placeholder_protagonist.png"), - }; - let t = Transform::from_xyz(-3.0 * SCALE, 0.0, 0.0); - commands.spawn(( - Mesh2d(mesh), - MeshMaterial2d(mat), - art, - t, - BATTLER, - Health(100.0), - Name::new("Protagonist"), - Protagonist, - )); - } - - { - let mat = materials.add(ColorMaterial { - color: RED.into(), - ..default() - }); - let art = Art { - texture: server.load("tetris/placeholder_enemy.png"), - }; - 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), - art, - t, - BATTLER, - Health(100.0), - Name::new("ENEMY"), - Enemy, - )); - } -} - -#[derive(Resource, Default)] -struct OutputImages { - tetris: Handle, - battler: Handle, -} - -#[derive(Component)] -struct TetrisCamera; - -#[derive(Component)] -struct BattlerCamera; - -fn init_cameras( - mut commands: Commands, - mut images: ResMut>, - mut output_images: ResMut, -) { - 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, images: Res>) { - 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::::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::::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::::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::::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 { - align_self: AlignSelf::End, - justify_self: JustifySelf::Center, - border: UiRect::all(Val::Px(5.0)), - ..default() - }, - BorderColor::all(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::all(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() - }, - )); - }); -} - -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::::default()),], - )); - }); -} - -/// Enum describing a shape option -#[derive(Component, Debug, Clone)] -enum Shape { - O, - T, - L, - J, - S, - Z, - I, -} - -impl Default for Shape { - fn default() -> Self { - Self::T - } -} - -impl Display for Shape { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{self:?}") - } -} - -impl Shape { - fn reposition( - (x_offset, y_offset): (isize, isize), - center: &GridPosition, - ) -> Result { - center.with_offset(x_offset, y_offset) - } -} - -#[derive(Component, Debug, Clone, PartialEq, Eq)] -struct ShapeLayout { - inner: Vec>, -} - -impl From>> for ShapeLayout { - fn from(inner: Vec>) -> Self { - Self { inner } - } -} - -impl ShapeLayout { - fn from_shape(s: &Shape) -> Self { - match s { - Shape::O => Self::new_o(), - Shape::T => Self::new_t(), - Shape::L => Self::new_l(), - Shape::J => Self::new_j(), - Shape::S => Self::new_s(), - Shape::Z => Self::new_z(), - Shape::I => Self::new_i(), - } - } - - fn new_o() -> Self { - vec![vec![1, 1], vec![1, 1]].into() - } - - fn new_t() -> Self { - vec![vec![0, 1, 0], vec![1, 1, 1], vec![0, 0, 0]].into() - } - - fn new_l() -> Self { - vec![vec![1, 0], vec![1, 0], vec![1, 1]].into() - } - - fn new_j() -> Self { - vec![vec![0, 1], vec![0, 1], vec![1, 1]].into() - } - - fn new_s() -> Self { - vec![vec![0, 1, 1], vec![1, 1, 0]].into() - } - - fn new_z() -> Self { - vec![vec![1, 1, 0], vec![0, 1, 1]].into() - } - - fn new_i() -> Self { - vec![vec![1], vec![1], vec![1], vec![1]].into() - } - - fn rotated(&self) -> Self { - let mut inner = vec![]; - for _ in 0..self.inner[0].len() { - inner.push(vec![]); - } - for y in self.inner.iter() { - for (j, x) in y.iter().enumerate() { - inner[j].insert(0, *x); - } - } - ShapeLayout { inner } - } - - fn center(&self) -> (usize, usize) { - let mid_x = match self.inner[0].len() % 2 { - 0 => (self.inner[0].len() - 1) / 2, - 1 => self.inner[0].len() / 2, - - _ => panic!("That's not how mod works!"), - }; - - let mid_y = match self.inner.len() % 2 { - // If we have an even number of elements - 0 => self.inner.len().div_ceil(2), - // If we have an odd number of elements - 1 => self.inner.len() / 2, - - _ => panic!("That's not how mod works!"), - }; - - (mid_x, mid_y) - } - - fn coordinates_at( - &self, - center: &GridPosition, - ) -> impl Iterator> { - self.coordinates() - .map(|(x, y)| Shape::reposition((x, y), center)) - } - - fn coordinates(&self) -> impl Iterator { - let (mid_x, mid_y) = self.center(); - // Loop over outer vec (i) - self.inner - .iter() - .rev() - .enumerate() - .flat_map(move |(i, ys)| { - ys.iter().enumerate().filter_map(move |(j, val)| { - // this_x = j - mid_x - let x = j as isize - mid_x as isize; - // this_y = i - mid_y - let y = i as isize - mid_y as isize; - - (*val > 0).then_some((x, y)) - }) - }) - } - - // TODO: Make fnctional with chaining - fn as_ascii(&self) -> String { - let mut s = String::new(); - for y in self.inner.iter() { - for x in y { - match x { - 0 => s.push('0'), - 1 => s.push('1'), - _ => panic!("aur nau"), - } - } - s.push('\n'); - } - s - } - - fn height(&self) -> usize { - self.inner.len() - } -} - -fn update_block_position( - mut changed: Query< - (Entity, &GridPosition, &mut Transform), - Or<(Added, Changed)>, - >, -) { - 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; - }); -} - -fn update_line_position( - mut changed: Query<(Entity, &Line, &mut Transform), Or<(Added, Changed)>>, -) { - 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; - }); -} - -fn update_shape_blocks( - query: Query< - (Entity, &ShapeLayout, &GridPosition), - Or<(Changed, Changed)>, - >, - mut blocks: Query<&mut GridPosition, (With, Without)>, -) { - query.iter().for_each(|(e, sl, center)| { - debug!("Setting piece: {e:?} {center:?}\n{}", sl.as_ascii()); - - debug_assert!(!blocks.is_empty()); - - let mut p = sl.coordinates_at(center); - blocks.iter_mut().for_each(|mut gp| 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?"), - }); - }); -} - -fn on_add_shape_layout( - trigger: On, - visuals: Res, - query: Query<(Entity, &Shape, &ShapeLayout, &GridPosition)>, - mut commands: Commands, -) { - let (e, s, sl, center) = query.get(trigger.entity).unwrap(); - let mesh = Mesh2d(visuals.mesh.clone()); - let mat = match s { - Shape::O => &visuals.material_o, - Shape::T => &visuals.material_t, - Shape::L => &visuals.material_l, - Shape::J => &visuals.material_j, - Shape::S => &visuals.material_s, - Shape::Z => &visuals.material_z, - Shape::I => &visuals.material_i, - }; - let art = visuals.art.clone(); - commands - .entity(e) - .with_related_entities::(|parent| { - sl.coordinates_at(center).for_each(|gp| { - parent.spawn(( - mesh.clone(), - MeshMaterial2d(mat.clone()), - art.clone(), - gp.unwrap(), - Block, - TETRIS, - )); - }); - }); -} - -fn kb_input( - mut events: MessageReader, - mut query: Query>, // Single? - curr: Res>, - mut next: ResMut>, - mut messages: MessageWriter, - mut commands: Commands, -) { - events.read().for_each( - |KeyboardInput { - key_code, state, .. - }| { - if let ButtonState::Pressed = state { - 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 => { - messages.write(Movement { - entity, - direction: MovementDirection::Rotate, - }); - } - KeyCode::ArrowDown => { - messages.write(Movement { - entity, - direction: MovementDirection::Down, - }); - } - KeyCode::ArrowLeft => { - messages.write(Movement { - entity, - direction: MovementDirection::Left, - }); - } - KeyCode::ArrowRight => { - messages.write(Movement { - entity, - direction: MovementDirection::Right, - }); - } - KeyCode::Enter => { - messages.write(Movement { - entity, - direction: MovementDirection::Skip, - }); - } - KeyCode::Space => { - commands.entity(entity).trigger(|entity| Swap { entity }); - } - KeyCode::Escape => next.set(GameState::Pause), - _ => (), - }, - GameState::Pause => match key_code { - KeyCode::Escape | KeyCode::Enter => next.set(GameState::Play), - _ => (), - }, - 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 => (), - } - }); - } - }, - ); -} - -fn falling(mut shape: Query>, mut messages: MessageWriter) { - shape.iter_mut().for_each(|entity| { - debug!("Making {:?} fall", entity); - messages.write(Movement { - entity, - direction: MovementDirection::Down, - }); - }); -} - -fn add_piece(mut commands: Commands, mut shapes: ResMut) { - let shape = shapes.next.pop_front().unwrap(); - let shape_layout = ShapeLayout::from_shape(&shape); - commands - .spawn(( - GridPosition { - y: Y_MAX, - ..default() + .add_plugins(( + BaseGamePlugin { + title: "Battle Tetris".into(), + name: "battle tetris".into(), + game_type: GameType::Two, + target_resolution: (360, 640).into(), }, - shape_layout, - shape, - TETRIS, )) - .observe(swap); -} - -/// When a line reaches 10 blocks, clear it -fn clear_line( - changed_lines: Query>, - mut lines: Query<&mut Line>, - line_blocks: Query<&LineBlocks>, - mut score: ResMut, - mut commands: Commands, -) { - let mut cleared_lines: Vec = 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::() - .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>, - 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(EntityEvent, Message, Copy, Clone, PartialEq)] -struct Swap { - entity: Entity, -} - -/// Movement events evented on the piece -#[derive(Message, EntityEvent, Copy, Clone, PartialEq)] -struct Movement { - entity: Entity, - direction: MovementDirection, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -enum MovementDirection { - Down, - Left, - Right, - Rotate, - Skip, -} - -// TODO: When out of bounds left/right, try to move piece away from wall -fn movement( - mut messages: MessageReader, - mut grid_positions: Query< - &mut GridPosition, - Or<(With, With, Without)>, - >, - mut shape_layouts: Query<&mut ShapeLayout>, - inactive: Query<&GridPosition, (Without, Without, With)>, - mut commands: Commands, - mut next_state: ResMut>, -) { - 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> = - 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::(); - 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::(); - } - // Regardless, cancel the move - return; - } - } - } - } - - debug!("Checks passed for {position:?}, committing change"); - - // Update center - let mut gp = grid_positions.get_mut(message.entity).unwrap(); - *gp = new_center; - - // Update shape/rotation - let mut sl = shape_layouts.get_mut(message.entity).unwrap(); - *sl = new_shape_layout.clone(); - } - } - } else { - warn!("Oned movement on non-shape entity"); - } - }); -} - -fn swap( - event: On, - mut shapes: Query<&mut Shape>, - mut store: ResMut, - 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().clone()); - commands - .entity(event.entity) - .despawn_related::() - .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, - grid_positions: Query<&GridPosition, With>, - parent: Query<&ShapeBlocks>, - lines: Query<(Entity, &Line), With>, - 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::(block); - } else { - error!("wtf?"); - } - }); - commands.entity(target).despawn(); - }); -} - -fn update_next_shapes(mut buffer: ResMut) { - // 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::O, - Shape::T, - Shape::L, - Shape::J, - Shape::S, - Shape::Z, - Shape::I, - ]); - } -} - -fn assert_grid_position_uniqueness( - grid_positions: Query< - ( - Entity, - &GridPosition, - Option<&LineBlock>, - Option<&ShapeBlock>, - ), - With, - >, -) { - grid_positions - .iter_combinations() - .for_each(|[(e1, a, lb1, sb1), (e2, b, lb2, sb2)]| { - if a == b { - 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!"); - }); -} - -fn sync_health( - query: Query<(Entity, &Health), Changed>, - parent: Query<&Children>, - mut texts: Query<&mut Text2d>, -) { - query.iter().for_each(|(e, h)| { - let child = parent - .iter_descendants(e) - .find(|child| texts.contains(*child)) - .unwrap(); - debug!("Updating health"); - let mut t = texts.get_mut(child).unwrap(); - t.0 = format!("{}", h.0); - }); -} - -fn on_add_health( - event: On, - query: Query<(&Health, &Mesh2d)>, - meshes: Res>, - mut commands: Commands, -) { - let (h, m) = query.get(event.entity).unwrap(); - info!("Creating health display"); - commands.entity(event.entity).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, EntityEvent)] -struct Damage { - entity: Entity, - quantity: f32, -} - -fn deal_damage(event: On, mut healths: Query<&mut Health>) { - healths.get_mut(event.entity).unwrap().0 -= event.event().quantity -} - -fn damage_on_place_shape( - mut events: RemovedComponents, - enemies: Query>, - mut commands: Commands, -) { - events.read().for_each(|_| { - enemies.iter().for_each(|e| { - commands.entity(e).trigger(|entity| Damage { - entity, - quantity: 1.0, - }); - }); - }); -} - -fn damage_on_clear_line( - mut events: RemovedComponents, - enemies: Query>, - mut commands: Commands, -) { - events.read().for_each(|_| { - enemies.iter().for_each(|e| { - commands.entity(e).trigger(|entity| Damage { - entity, - quantity: 1.0, - }); - }); - }); -} - -fn damage_over_time(protagonist: Single>, mut commands: Commands) { - commands.entity(*protagonist).trigger(|entity| Damage { - entity, - quantity: 1.0, - }); -} - -fn check_level_goal( - goal: Res, - score: Res, - mut next: ResMut>, -) { - match *goal { - LevelGoal::Score(g) => { - if score.0 >= g { - next.set(GameState::LevelComplete); - } - } - } + .run(); } -fn toggle_art( - state: Res>, - // TODO: Correctness: Query for added or changed entities - query: Query<(&MeshMaterial2d, &Art)>, - mut color_materials: ResMut>, -) { - query.iter().for_each(|(mm, a)| { - let texture: Option> = match state.get() { - DebuggingState::Off => Some(a.texture.clone()), - DebuggingState::On => None, - }; +// Create tetris game with camera that renders to subset of viewport +// +// Focus on a single piece and making it really tight mechanically +// A single piece with movement, rotation, jump-to-end, line clearing, etc. +// +// Once done, make pieces a data input so we can add arbitrary metadata to them - let cm = color_materials.get_mut(mm.0.id()).unwrap(); - cm.texture = texture; - }); -} +// Create figher game that renders to another subset of the viewport +// +// Focus on a single fighter class with a single enemy (blob) to start with +// Once damage both ways is tight, add multiple enemies as assets diff --git a/src/bin/tetris/test.rs b/src/bin/tetris/test.rs deleted file mode 100644 index 2c58d7c..0000000 --- a/src/bin/tetris/test.rs +++ /dev/null @@ -1,209 +0,0 @@ -use super::*; - -#[test] -fn test_shape_t() { - let mut shape = Shape::new_t(); - - let expected_up = "010\n\ - 111\n\ - 000\n"; - - let expected_right = "010\n\ - 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 = shape.rotated(); - assert_eq!(shape.as_ascii(), expected_right); - shape = shape.rotated(); - assert_eq!(shape.as_ascii(), expected_down); - shape = shape.rotated(); - assert_eq!(shape.as_ascii(), expected_left); - shape = shape.rotated(); - assert_eq!(shape.as_ascii(), expected_up); -} - -#[test] -fn test_shape_i() { - let mut shape = Shape::new_i(); - - let expected_up = "1\n\ - 1\n\ - 1\n\ - 1\n"; - - let expected_right = "1111\n"; - - let expected_down = "1\n\ - 1\n\ - 1\n\ - 1\n"; - - let expected_left = "1111\n"; - - assert_eq!(shape.as_ascii(), expected_up); - shape = shape.rotated(); - assert_eq!(shape.as_ascii(), expected_right); - shape = shape.rotated(); - assert_eq!(shape.as_ascii(), expected_down); - shape = shape.rotated(); - assert_eq!(shape.as_ascii(), expected_left); - shape = shape.rotated(); - assert_eq!(shape.as_ascii(), expected_up); -} - -#[test] -fn test_coordinates() { - let shape = Shape::new_t(); - - let center = GridPosition { x: 5, y: 5 }; - - let actual: Vec> = shape.coordinates(¢er).collect(); - - let expected: Vec> = vec![ - Ok((4, 5).into()), - Ok((5, 5).into()), - Ok((6, 5).into()), - Ok((5, 6).into()), - ]; - - assert_eq!(shape.layout.center(), (1, 1)); - - assert_eq!(actual, expected); -} - -#[test] -fn test_height() { - // todo: should be 2 for t piece - assert_eq!(Shape::new_t().height(), 3); - assert_eq!(Shape::new_i().height(), 4); - assert_eq!(Shape::new_l().height(), 3); -} - -#[test] -fn test_shape_block_layout_rotation() { - { - let actual = ShapeBlockLayout { - inner: vec![vec![0, 0, 1]], - } - .rotated(); - let expected = ShapeBlockLayout { - inner: vec![vec![0], vec![0], vec![1]], - }; - assert_eq!(expected, actual); - } - - { - let actual = ShapeBlockLayout { - inner: vec![vec![1, 2, 3], vec![4, 5, 6]], - } - .rotated(); - let expected = ShapeBlockLayout { - inner: vec![vec![4, 1], vec![5, 2], vec![6, 3]], - }; - assert_eq!(expected, actual); - } - - { - let actual = ShapeBlockLayout { - inner: vec![vec![1, 2, 3], vec![4, 5, 6]], - } - .rotated(); - let expected = ShapeBlockLayout { - inner: vec![vec![4, 1], vec![5, 2], vec![6, 3]], - }; - assert_eq!(expected, actual); - } - - { - let actual = ShapeBlockLayout { - inner: vec![ - vec![1, 2, 3, 4], - vec![5, 6, 7, 8], - vec![9, 10, 11, 12], - vec![13, 14, 15, 16], - ], - } - .rotated(); - let expected = ShapeBlockLayout { - inner: vec![ - vec![13, 9, 5, 1], - vec![14, 10, 6, 2], - vec![15, 11, 7, 3], - vec![16, 12, 8, 4], - ], - }; - assert_eq!(expected, actual); - } -} - -#[test] -fn test_shape_block_center() { - { - let actual = ShapeBlockLayout { - inner: vec![vec![0], vec![0], vec![0]], - } - .center(); - - let expected = (0, 1); - - assert_eq!(actual, expected); - } - - { - let actual = ShapeBlockLayout { - inner: vec![vec![0], vec![0], vec![0], vec![0]], - } - .center(); - - let expected = (0, 2); - - assert_eq!(actual, expected); - } - - { - let actual = ShapeBlockLayout { - inner: vec![vec![0, 0], vec![0, 0]], - } - .center(); - - let expected = (0, 1); - - assert_eq!(actual, expected); - } - - { - let actual = ShapeBlockLayout { - inner: vec![vec![0, 0, 0], vec![0, 0, 0], vec![0, 0, 0]], - } - .center(); - - let expected = (1, 1); - - assert_eq!(actual, expected); - } - - { - let actual = ShapeBlockLayout { - inner: vec![ - vec![0, 0, 0, 0], - vec![0, 0, 0, 0], - vec![0, 0, 0, 0], - vec![0, 0, 0, 0], - ], - } - .center(); - - let expected = (1, 2); - - assert_eq!(actual, expected); - } -} diff --git a/src/lib.rs b/src/lib.rs index f6f917c..3453b11 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,12 +35,15 @@ pub use bevy::{ math::FloatOrd, pbr::wireframe::{WireframeConfig, WireframePlugin}, platform::{collections::HashMap, hash::RandomState}, - prelude::*, + prelude::{State, *}, reflect::TypePath, render::render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages}, sprite_render::*, time::common_conditions::*, window::{WindowResized, WindowResolution}, + ui_widgets::{Button, *}, + ui::*, + feathers::controls::*, }; pub use itertools::Itertools; pub use serde::Deserialize;