Compare commits

...

4 Commits

Author SHA1 Message Date
Elijah Voigt 8c892a50aa prep to draw outline gizmos 2 months ago
Elijah Voigt 62d24bec0e Rotation works! 2 months ago
Elijah Voigt 92ee8c7598 Rotation exists, but needs polish 2 months ago
Elijah Voigt 467f81bee7 Hey basic movement works!
Some stuff does not but it's a hard panic so we WILL find it.
2 months ago

@ -22,7 +22,7 @@ pub use bevy::{
},
camera::{primitives::*, visibility::*, *},
color::palettes::css::*,
feathers::controls::*,
feathers::{controls::*, theme::*, dark_theme::*},
feathers::*,
gizmos::{aabb::AabbGizmoPlugin, light::LightGizmoPlugin},
input::{
@ -40,6 +40,7 @@ pub use bevy::{
render::render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages},
sprite_render::*,
time::common_conditions::*,
ui::*,
ui_widgets::{Button, *},
window::{WindowResized, WindowResolution},

@ -3,6 +3,9 @@ VERSION := `git rev-parse --short HEAD`
check:
cargo check -p tetris
clippy:
cargo clippy -p tetris
run:
cargo run -p tetris

@ -1,12 +1,9 @@
#![allow(clippy::type_complexity)]
use super::*;
// TODO:
// - When shape asset is updated, shape layout update in real time
// - Rotating shape when arrow up pressed
// - Shape moves left/right/down when respective keys pressed
// - Add Orientation component
// - Add functionality to ShapeLayout
// - Write tests before functionality
// - Debug: gizmo outline entities
// - conditional enable/disable
// - entities without mesh should draw empty cross
@ -33,15 +30,25 @@ impl Plugin for BlocksPlugin {
fn build(&self, app: &mut App) {
app.init_asset::<ShapeAsset>()
.init_asset_loader::<ShapeAssetLoader>()
.add_message::<Movement>()
.add_systems(OnEnter(Loading(true)), load_assets.run_if(run_once))
.add_systems(OnEnter(GameState::Setup), (setup_camera, setup_blocks))
.add_systems(
Update,
updated_shape_asset.run_if(on_message::<AssetEvent<ShapeAsset>>),
(
updated_shape_asset.run_if(on_message::<AssetEvent<ShapeAsset>>),
update_grid_position_transform.run_if(
any_component_added::<GridPosition>
.or(any_component_changed::<GridPosition>),
),
propogate_orientation.run_if(any_component_changed::<Orientation>),
propogate_grid_position.run_if(any_component_changed::<GridPosition>),
handle_kb_input.run_if(on_message::<KeyboardInput>),
handle_movement.run_if(on_message::<Movement>),
),
)
.add_observer(add_shape)
.add_observer(add_relative_position)
.add_observer(add_grid_position);
.add_observer(add_relative_position);
}
}
@ -53,7 +60,7 @@ const Y_MAX: usize = 20;
/// Stores shape data in an asset file, likely toml
#[derive(Asset, TypePath, Debug, Deserialize)]
struct ShapeAsset {
layout: ShapeLayout,
layout: Vec<Vec<u8>>,
#[serde(default = "default_tint")]
tint: String,
#[serde(skip)]
@ -69,22 +76,25 @@ fn default_tint() -> String {
impl ShapeAsset {
fn as_bundle(&self) -> impl Bundle {
// Get shape block's relative positions
let [a, b, c, d] = self.layout.positions();
let layout = ShapeLayout::new(self.layout.clone());
let [a, b, c, d] = layout.positions(Orientation::Up);
(
GridPosition { x: 5, y: 5 }, // TEMP
layout.clone(),
Orientation::Up,
related!(ShapeBlocks[
(self.mesh.clone(), self.material.clone(), a),
(self.mesh.clone(), self.material.clone(), b),
(self.mesh.clone(), self.material.clone(), c),
(self.mesh.clone(), self.material.clone(), d),
])
]),
)
}
}
/// The main play area grid coordinates
#[derive(Component)]
#[derive(Component, Debug, Clone)]
#[require(Transform)]
struct GridPosition {
pub x: usize,
@ -108,6 +118,31 @@ impl Into<Vec3> for &GridPosition {
}
}
impl std::ops::AddAssign<RelativePosition> for GridPosition {
fn add_assign(&mut self, rhs: RelativePosition) {
self.x = self.x.strict_add_signed(rhs.x);
self.y = self.y.strict_add_signed(rhs.y);
}
}
impl std::ops::SubAssign<RelativePosition> for GridPosition {
fn sub_assign(&mut self, rhs: RelativePosition) {
self.x = self.x.strict_sub_signed(rhs.x);
self.y = self.y.strict_sub_signed(rhs.y);
}
}
impl std::ops::Add<RelativePosition> for GridPosition {
type Output = GridPosition;
fn add(self, rhs: RelativePosition) -> GridPosition {
GridPosition {
x: self.x.strict_add_signed(rhs.x),
y: self.y.strict_add_signed(rhs.y),
}
}
}
/// Block positions relative to the shape's center
#[derive(Component, PartialEq, Debug, Clone, Copy)]
pub(crate) struct RelativePosition {
@ -115,37 +150,72 @@ pub(crate) struct RelativePosition {
pub y: isize,
}
impl RelativePosition {
fn new(x: isize, y: isize) -> Self {
RelativePosition { x, y }
}
}
impl From<(isize, isize)> for RelativePosition {
fn from((x, y): (isize, isize)) -> Self {
RelativePosition { x, y }
RelativePosition::new(x, y)
}
}
/// Layout for a given shape
#[derive(Debug, Deserialize)]
pub(crate) struct ShapeLayout(pub Vec<Vec<u8>>);
#[derive(Debug, Component, Deserialize, PartialEq, Clone)]
pub(crate) struct ShapeLayout {
pub matrix: Vec<Vec<u8>>,
#[serde(skip)]
symmetrical: bool,
}
impl ShapeLayout {
pub(crate) fn positions(&self) -> [RelativePosition; 4] {
pub(crate) fn new(matrix: Vec<Vec<u8>>) -> Self {
let mut s = Self {
matrix,
symmetrical: true,
};
// Check if symmetrical
s.symmetrical = s.matrix == s.rotated_matrix(Orientation::Down);
s
}
pub(crate) fn positions(&self, o: Orientation) -> [RelativePosition; 4] {
let mut c: Vec<RelativePosition> = Vec::with_capacity(4);
let layout = self.rotated_matrix(o);
let center: (isize, isize) = {
// This could be less lines of code but it would be confusing af
let y = if self.0.len() % 2 == 1 {
self.0.len() / 2
let y = if layout.len() % 2 == 1 {
layout.len() / 2
} else {
self.0.len() / 2 - 1
layout.len() / 2 - 1
};
let x = if self.0[0].len() % 2 == 1 {
self.0[0].len() / 2
let x = if layout[0].len() % 2 == 1 {
layout[0].len() / 2
} else {
self.0[0].len() / 2 - 1
layout[0].len() / 2 - 1
};
let (x_off, y_off) = match self.symmetrical {
true => (0, 0),
false => match o {
Orientation::Up => (0, 0),
Orientation::Right => (0, 0),
Orientation::Down => (0, 1),
Orientation::Left => (1, 0),
},
};
(x as isize, y as isize)
(x_off + x as isize, y_off + y as isize)
};
for (y, nested) in self.0.iter().rev().enumerate() {
for (y, nested) in layout.iter().rev().enumerate() {
for (x, val) in nested.iter().enumerate() {
if *val == 1 {
c.push(RelativePosition {
@ -158,6 +228,56 @@ impl ShapeLayout {
[c[0], c[1], c[2], c[3]]
}
pub(crate) fn rotated_matrix(&self, o: Orientation) -> Vec<Vec<u8>> {
// Now we have the base layout, rotate it based on orientation
match o {
Orientation::Up => {
// The identity
self.matrix.clone()
}
Orientation::Right => {
// The main algorithm
let cols = self.matrix[0].len();
let rows = self.matrix.len();
let mut rotated = vec![vec![0; rows]; cols];
// (i, j) -> (j,rows-1-i)
for i in 0..rows {
for j in 0..cols {
rotated[j][rows - 1 - i] = self.matrix[i][j];
}
}
rotated
}
Orientation::Down => {
// HACK: Implemented as repeated turns right
// TODO: Just hard-code this
ShapeLayout {
matrix: self.rotated_matrix(Orientation::Right),
symmetrical: false
}.rotated_matrix(Orientation::Right)
}
Orientation::Left => {
// The main algorithm
let cols = self.matrix[0].len();
let rows = self.matrix.len();
let mut rotated = vec![vec![0; rows]; cols];
// (i, j) -> (cols-1-j,i)
for i in 0..rows {
for j in 0..cols {
rotated[cols - 1 - j][i] = self.matrix[i][j];
}
}
rotated
}
}
}
}
#[derive(Default)]
@ -294,13 +414,139 @@ fn add_relative_position(
let parent_shape = blocks.get(event.entity).unwrap();
let rp = relative_positions.get(event.entity).unwrap();
let gp = grid_positions.get(parent_shape.0).unwrap();
commands.entity(event.entity).insert(gp.with_relative_offset(rp));
commands
.entity(event.entity)
.insert(gp.with_relative_offset(rp));
}
fn add_grid_position(
event: On<Add, GridPosition>,
mut query: Query<(&GridPosition, &mut Transform)>,
/// Populate Transform when GridPosition is added to an entity
fn update_grid_position_transform(
mut query: Query<
(&GridPosition, &mut Transform),
Or<(Added<GridPosition>, Changed<GridPosition>)>,
>,
) {
let (gp, mut t) = query.get_mut(event.entity).unwrap();
t.translation = gp.into();
query.iter_mut().for_each(|(gp, mut t)| {
t.translation = gp.into();
});
}
/// When grid position is updated on a shape, apply that to blocks
fn propogate_grid_position(
parent: Query<(&GridPosition, &ShapeBlocks), Changed<GridPosition>>,
mut children: Query<(&mut GridPosition, &RelativePosition), Without<ShapeBlocks>>,
) {
parent.iter().for_each(|(parent_gp, sbs)| {
sbs.iter().for_each(|e| {
let (mut gp, rp) = children.get_mut(e).unwrap();
*gp = parent_gp.clone() + *rp;
});
});
}
/// When a shape's orientation changes, the blocks need to move
fn propogate_orientation(
parent: Query<(&GridPosition, &Orientation, &ShapeLayout, &ShapeBlocks), Changed<Orientation>>,
mut children: Query<&mut GridPosition, Without<ShapeBlocks>>,
) {
parent.iter().for_each(|(parent_gp, parent_o, sl, sbs)| {
let new_layout = sl.positions(*parent_o);
sbs.iter().zip(new_layout).for_each(|(e, rp)| {
let mut gp = children.get_mut(e).unwrap();
*gp = parent_gp.clone() + rp;
});
});
}
/// Movement message used to propose/plan a move
#[derive(Debug, Message)]
enum Movement {
Rotate,
Left,
Down,
Right,
}
/// Handle KeyBoard input
/// Nothing is handled directly in this method,
/// All key presses result in either a message, event, or state change
fn handle_kb_input(mut input: MessageReader<KeyboardInput>, mut moves: MessageWriter<Movement>) {
input.read().for_each(
|KeyboardInput {
key_code, state, ..
}| {
if *state == ButtonState::Pressed {
match key_code {
KeyCode::ArrowLeft => {
moves.write(Movement::Left);
}
KeyCode::ArrowRight => {
moves.write(Movement::Right);
}
KeyCode::ArrowDown => {
moves.write(Movement::Down);
}
KeyCode::ArrowUp => {
moves.write(Movement::Rotate);
}
KeyCode::Escape => todo!("Pause Toggle"),
_ => (), // Do nothing
};
}
},
);
}
#[derive(Component, Default, Debug, Clone, Copy)]
pub(crate) enum Orientation {
#[default]
Up,
Right,
Down,
Left,
}
impl Orientation {
fn rotated(&self) -> Orientation {
match self {
Orientation::Up => Orientation::Right,
Orientation::Right => Orientation::Down,
Orientation::Down => Orientation::Left,
Orientation::Left => Orientation::Up,
}
}
}
fn handle_movement(
mut moves: MessageReader<Movement>,
mut grid_positions: Query<&mut GridPosition, With<ShapeBlocks>>,
mut orientations: Query<&mut Orientation, With<ShapeBlocks>>,
) {
moves.read().for_each(|m| match m {
Movement::Left => {
grid_positions.iter_mut().for_each(|mut gp| {
debug!("moving shape left");
*gp -= RelativePosition::new(1, 0);
});
}
Movement::Right => {
grid_positions.iter_mut().for_each(|mut gp| {
debug!("moving shape right");
*gp += RelativePosition::new(1, 0);
});
}
Movement::Down => {
grid_positions.iter_mut().for_each(|mut gp| {
debug!("moving shape down");
*gp -= RelativePosition::new(0, 1);
});
}
Movement::Rotate => {
orientations.iter_mut().for_each(|mut o| {
debug!("rotating shape");
*o = o.rotated();
});
}
})
}

@ -1,4 +1,3 @@
use bevy::input_focus::tab_navigation::TabGroup;
use engine::theme::ThemedText;
use super::*;
@ -17,6 +16,7 @@ pub struct DebugPlugin;
impl Plugin for DebugPlugin {
fn build(&self, app: &mut App) {
app.init_state::<DebugState>()
.init_state::<DebugOutlines>()
.add_systems(Startup, setup_ui)
.add_systems(
Update,
@ -25,6 +25,7 @@ impl Plugin for DebugPlugin {
log_transition::<DebugState>.run_if(state_changed::<DebugState>),
log_transition::<Loading>.run_if(state_changed::<Loading>),
log_transition::<GameState>.run_if(state_changed::<GameState>),
log_transition::<DebugOutlines>.run_if(state_changed::<DebugOutlines>)
),
)
.add_systems(
@ -34,10 +35,21 @@ impl Plugin for DebugPlugin {
.add_systems(
Update,
(
toggle_state_visibility::<DebugState>,
sync_state_to_ui::<DebugState>,
(
toggle_state_visibility::<DebugState>,
sync_state_to_ui::<DebugState>,
)
.run_if(state_changed::<DebugState>),
(
toggle_state_visibility::<DebugOutlines>,
sync_state_to_ui::<DebugOutlines>,
)
.run_if(state_changed::<DebugOutlines>),
)
.run_if(state_changed::<DebugState>),
)
.add_systems(
Update,
draw_outline_gizmos.run_if(in_state(DebugOutlines(true)))
);
}
}
@ -79,6 +91,30 @@ fn setup_ui(mut commands: Commands) {
],
));
commands.spawn((
Node {
justify_self: JustifySelf::Center,
align_self: AlignSelf::Center,
..default()
},
DebugState(true),
children![
(
Node {
..default()
},
children![
Text::new("Outlines"),
(
toggle_switch((),),
observe(checkbox_self_update),
observe(toggle_outline_state),
),
]
),
]
));
commands.spawn((
Node {
bottom: px(0.0),
@ -93,6 +129,10 @@ fn setup_ui(mut commands: Commands) {
Text::new("DebugState State"),
SyncState::<DebugState>::default()
),
(
Text::new("DebugOutlines State"),
SyncState::<DebugOutlines>::default()
),
(Text::new("Loading State"), SyncState::<Loading>::default()),
(Text::new("Game State"), SyncState::<GameState>::default()),
],
@ -117,3 +157,25 @@ fn debug_toggle(event: On<ValueChange<bool>>, mut next_state: ResMut<NextState<D
info!("Debug State Toggled: {:?}", event.event().value);
next_state.set(event.event().value.into());
}
#[derive(States, Default, Clone, Eq, Debug, PartialEq, Hash, Component)]
struct DebugOutlines(bool);
impl fmt::Display for DebugOutlines {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Debug Outlines {}", self.0)
}
}
fn toggle_outline_state(
event: On<ValueChange<bool>>,
mut next: ResMut<NextState<DebugOutlines>>,
) {
next.set(DebugOutlines(event.event().value));
}
fn draw_debug_outlines(
gizmos: Gizmos
) {
todo!("Draw outlines here")
}

@ -1,6 +1,8 @@
mod blocks;
mod debug;
mod fighter;
#[cfg(test)]
mod test;
use engine::*;
@ -16,6 +18,7 @@ fn main() {
.add_plugins(DefaultPlugins)
.add_plugins(FeathersPlugins)
.add_plugins((BlocksPlugin, FighterPlugin, DebugPlugin))
.insert_resource(UiTheme(create_dark_theme()))
.init_state::<Loading>()
.init_state::<GameState>()
.init_resource::<AllAssets>()

@ -2,7 +2,7 @@ use super::*;
#[test]
fn test_shape_layout_01a() {
let actual = ShapeLayout(vec![vec![0, 1, 0], vec![1, 1, 1]]).positions();
let actual = ShapeLayout::new(vec![vec![0, 1, 0], vec![1, 1, 1]]).positions(Orientation::Up);
let expected: [RelativePosition; 4] =
[(-1, 0).into(), (0, 0).into(), (1, 0).into(), (0, 1).into()];
@ -12,7 +12,8 @@ fn test_shape_layout_01a() {
#[test]
fn test_shape_layout_01b() {
let actual = ShapeLayout(vec![vec![1, 0], vec![1, 1], vec![1, 0]]).positions();
let actual =
ShapeLayout::new(vec![vec![1, 0], vec![1, 1], vec![1, 0]]).positions(Orientation::Up);
let expected: [RelativePosition; 4] =
[(0, -1).into(), (0, 0).into(), (1, 0).into(), (0, 1).into()];
@ -20,9 +21,20 @@ fn test_shape_layout_01b() {
debug_assert_eq!(expected, actual);
}
#[test]
fn test_shape_layout_01c() {
let actual = ShapeLayout::new(vec![vec![0, 1, 0], vec![1, 1, 1]]).positions(Orientation::Down);
let expected: [RelativePosition; 4] =
[(0, -1).into(), (-1, 0).into(), (0, 0).into(), (1, 0).into(),];
debug_assert_eq!(expected, actual);
}
#[test]
fn test_shape_layout_02a() {
let actual = ShapeLayout(vec![vec![1], vec![1], vec![1], vec![1]]).positions();
let actual =
ShapeLayout::new(vec![vec![1], vec![1], vec![1], vec![1]]).positions(Orientation::Up);
let expected: [RelativePosition; 4] =
[(0, -1).into(), (0, 0).into(), (0, 1).into(), (0, 2).into()];
@ -32,7 +44,7 @@ fn test_shape_layout_02a() {
#[test]
fn test_shape_layout_02b() {
let actual = ShapeLayout(vec![vec![1, 1, 1, 1]]).positions();
let actual = ShapeLayout::new(vec![vec![1, 1, 1, 1]]).positions(Orientation::Up);
let expected: [RelativePosition; 4] =
[(-1, 0).into(), (0, 0).into(), (1, 0).into(), (2, 0).into()];
@ -42,10 +54,52 @@ fn test_shape_layout_02b() {
#[test]
fn test_shape_layout_03() {
let actual = ShapeLayout(vec![vec![1, 1, 0], vec![0, 1, 1]]).positions();
let actual = ShapeLayout::new(vec![vec![1, 1, 0], vec![0, 1, 1]]).positions(Orientation::Up);
let expected: [RelativePosition; 4] =
[(0, 0).into(), (1, 0).into(), (-1, 1).into(), (0, 1).into()];
debug_assert_eq!(expected, actual);
}
#[test]
fn test_rotation_01() {
let actual =
ShapeLayout::new(vec![vec![1], vec![1], vec![1], vec![1]]).positions(Orientation::Left);
let expected = ShapeLayout::new(vec![vec![1, 1, 1, 1]]).positions(Orientation::Up);
debug_assert_eq!(expected, actual);
}
#[test]
fn test_rotation_02() {
let base = ShapeLayout::new(vec![vec![0, 1, 0], vec![1, 1, 1]]);
let expected_up = base.matrix.clone();
let actual_up = base.rotated_matrix(Orientation::Up);
let expected_down = ShapeLayout::new(vec![vec![1, 1, 1], vec![0, 1, 0]]).matrix;
let actual_down = base.rotated_matrix(Orientation::Down);
let expected_right = ShapeLayout::new(vec![vec![1, 0], vec![1, 1], vec![1, 0]]).matrix;
let actual_right = base.rotated_matrix(Orientation::Right);
let expected_left = ShapeLayout::new(vec![vec![0, 1], vec![1, 1], vec![0, 1]]).matrix;
let actual_left = base.rotated_matrix(Orientation::Left);
debug_assert_eq!(expected_up, actual_up);
debug_assert_eq!(expected_down, actual_down);
debug_assert_eq!(expected_right, actual_right);
debug_assert_eq!(expected_left, actual_left);
}
#[test]
fn test_rotation_03() {
let base = ShapeLayout::new(vec![vec![0, 1, 0], vec![1, 1, 1]]);
let actual_right = base.rotated_matrix(Orientation::Right);
let expected_right = ShapeLayout::new(vec![vec![1, 0], vec![1, 1], vec![1, 0]]).matrix;
debug_assert_eq!(expected_right, actual_right);
}

Loading…
Cancel
Save