Shape colors

This breaks shapes out into having separate colors.

This requires breaking shapes out into an enum describing the shape and
the shape layout, then we can add the mesh and material based on the
shape enum variant.
main
Elijah Voigt 4 weeks ago
parent 2efa01bb89
commit 6fd9550975

@ -35,7 +35,6 @@ fn main() {
init_cameras, init_cameras,
init_ui.after(init_cameras), init_ui.after(init_cameras),
init_battler, init_battler,
init_deck,
), ),
) )
// Input and basic systems // Input and basic systems
@ -56,10 +55,10 @@ fn main() {
.after(update_next_shapes), .after(update_next_shapes),
falling falling
.run_if(in_state(GameState::Falling)) .run_if(in_state(GameState::Falling))
.run_if(clock_cycle(1.0)), .run_if(on_timer(Duration::from_secs(1))),
update_position.run_if(any_component_changed::<GridPosition>), update_position.run_if(any_component_changed::<GridPosition>),
update_shape_blocks update_shape_blocks
.run_if(any_component_added::<Shape>.or(any_component_changed::<Shape>)) .run_if(any_component_changed::<ShapeLayout>.or(any_component_changed::<GridPosition>))
.after(update_position), .after(update_position),
deactivate_shape deactivate_shape
.run_if(any_component_removed::<Shape>) .run_if(any_component_removed::<Shape>)
@ -74,7 +73,7 @@ fn main() {
.run_if(any_component_changed::<Health>.or(any_component_added::<Health>)), .run_if(any_component_changed::<Health>.or(any_component_added::<Health>)),
damage_on_place_shape.run_if(any_component_removed::<Shape>), damage_on_place_shape.run_if(any_component_removed::<Shape>),
damage_on_clear_line.run_if(any_component_removed::<LineBlock>), damage_on_clear_line.run_if(any_component_removed::<LineBlock>),
damage_over_time.run_if(clock_cycle(5.0)), damage_over_time.run_if(on_timer(Duration::from_secs(5))),
), ),
) )
// UI systems // UI systems
@ -88,6 +87,8 @@ fn main() {
), ),
) )
.add_observer(deal_damage) .add_observer(deal_damage)
.add_observer(on_add_shape_layout)
.add_observer(on_add_health)
.run(); .run();
} }
@ -227,8 +228,14 @@ enum GameState {
#[derive(Resource, Debug)] #[derive(Resource, Debug)]
struct Visuals { struct Visuals {
material: Handle<ColorMaterial>,
mesh: Handle<Mesh>, mesh: Handle<Mesh>,
material_o: Handle<ColorMaterial>,
material_t: Handle<ColorMaterial>,
material_l: Handle<ColorMaterial>,
material_j: Handle<ColorMaterial>,
material_s: Handle<ColorMaterial>,
material_z: Handle<ColorMaterial>,
material_i: Handle<ColorMaterial>,
} }
#[derive(Resource, Debug, Default)] #[derive(Resource, Debug, Default)]
@ -266,7 +273,7 @@ struct ShapeStore(Option<Shape>);
impl Display for ShapeStore { impl Display for ShapeStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.0 { match &self.0 {
Some(inner) => write!(f, "{}", inner.as_ascii()), Some(s) => write!(f, "{}", ShapeLayout::from_shape(s).as_ascii()),
None => write!(f, "---"), None => write!(f, "---"),
} }
} }
@ -281,17 +288,36 @@ fn init_tetris(
mut materials: ResMut<Assets<ColorMaterial>>, mut materials: ResMut<Assets<ColorMaterial>>,
) { ) {
let mesh = meshes.add(Rectangle::new(SCALE, SCALE)); 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 { commands.insert_resource(Visuals {
material: block_material.clone(),
mesh: mesh.clone(), mesh: mesh.clone(),
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| { (0..Y_MAX).for_each(|y| {
@ -299,7 +325,10 @@ fn init_tetris(
(0..X_MAX).for_each(|x| { (0..X_MAX).for_each(|x| {
commands.spawn(( commands.spawn((
Mesh2d(mesh.clone()), Mesh2d(mesh.clone()),
MeshMaterial2d(grid_material.clone()), MeshMaterial2d(materials.add(ColorMaterial {
color: BLACK.into(),
..default()
})),
GridPosition { x, y }, GridPosition { x, y },
Transform::from_xyz(0.0, 0.0, -1.0), Transform::from_xyz(0.0, 0.0, -1.0),
GridBackground, GridBackground,
@ -312,12 +341,6 @@ fn init_tetris(
}); });
} }
fn init_deck(
mut commands: Commands,
) {
todo!()
}
#[derive(Component, Debug)] #[derive(Component, Debug)]
struct Protagonist; struct Protagonist;
@ -579,32 +602,63 @@ fn init_debug_ui(mut commands: Commands) {
}); });
} }
/// Enum describing a shape option
#[derive(Component, Debug, Clone)] #[derive(Component, Debug, Clone)]
struct Shape { enum Shape {
layout: ShapeBlockLayout, O,
T,
L,
J,
S,
Z,
I,
} }
impl Default for Shape { impl Default for Shape {
fn default() -> Self { fn default() -> Self {
Self::new_t() Self::T
} }
} }
impl Display for Shape { impl Display for Shape {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.layout.as_ascii()) write!(f, "{self:?}")
} }
} }
impl From<Vec<Vec<u8>>> for Shape { impl Shape {
fn from(inner: Vec<Vec<u8>>) -> Shape { fn reposition(
Shape { (x_offset, y_offset): (isize, isize),
layout: ShapeBlockLayout { inner }, center: &GridPosition,
} ) -> Result<GridPosition, OutOfBoundsError> {
center.with_offset(x_offset, y_offset)
} }
} }
impl Shape { #[derive(Component, Debug, Clone, PartialEq, Eq)]
struct ShapeLayout {
inner: Vec<Vec<u8>>,
}
impl From<Vec<Vec<u8>>> for ShapeLayout {
fn from(inner: Vec<Vec<u8>>) -> 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 { fn new_o() -> Self {
vec![vec![1, 1], vec![1, 1]].into() vec![vec![1, 1], vec![1, 1]].into()
} }
@ -637,45 +691,6 @@ impl Shape {
vec![vec![1], vec![1], vec![1], vec![1]].into() vec![vec![1], vec![1], vec![1], vec![1]].into()
} }
// Rotates 90 degrees to the right
// https://stackoverflow.com/a/8664879
fn rotated(&self) -> Self {
Self {
layout: self.layout.rotated(),
}
}
fn reposition(
(x_offset, y_offset): (isize, isize),
center: &GridPosition,
) -> Result<GridPosition, OutOfBoundsError> {
center.with_offset(x_offset, y_offset)
}
fn coordinates(
&self,
center: &GridPosition,
) -> impl Iterator<Item = Result<GridPosition, OutOfBoundsError>> {
self.layout
.coordinates()
.map(|(x, y)| Shape::reposition((x, y), center))
}
fn as_ascii(&self) -> String {
self.layout.as_ascii()
}
fn height(&self) -> usize {
self.layout.height()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ShapeBlockLayout {
inner: Vec<Vec<u8>>,
}
impl ShapeBlockLayout {
fn rotated(&self) -> Self { fn rotated(&self) -> Self {
let mut inner = vec![]; let mut inner = vec![];
for _ in 0..self.inner[0].len() { for _ in 0..self.inner[0].len() {
@ -686,7 +701,7 @@ impl ShapeBlockLayout {
inner[j].insert(0, *x); inner[j].insert(0, *x);
} }
} }
ShapeBlockLayout { inner } ShapeLayout { inner }
} }
fn center(&self) -> (usize, usize) { fn center(&self) -> (usize, usize) {
@ -709,6 +724,14 @@ impl ShapeBlockLayout {
(mid_x, mid_y) (mid_x, mid_y)
} }
fn coordinates_at(
&self,
center: &GridPosition,
) -> impl Iterator<Item = Result<GridPosition, OutOfBoundsError>> {
self.coordinates()
.map(|(x, y)| Shape::reposition((x, y), center))
}
fn coordinates(&self) -> impl Iterator<Item = (isize, isize)> { fn coordinates(&self) -> impl Iterator<Item = (isize, isize)> {
let (mid_x, mid_y) = self.center(); let (mid_x, mid_y) = self.center();
// Loop over outer vec (i) // Loop over outer vec (i)
@ -771,45 +794,57 @@ fn update_position(
// TODO: Move to event // TODO: Move to event
fn update_shape_blocks( fn update_shape_blocks(
query: Query< query: Query<
(Entity, &Shape, &GridPosition), (Entity, &ShapeLayout, &GridPosition),
Or<( Or<(
Added<Shape>, Changed<ShapeLayout>,
Changed<Shape>,
Added<GridPosition>,
Changed<GridPosition>, Changed<GridPosition>,
)>, )>,
>, >,
mut blocks: Query<&mut GridPosition, (With<ShapeBlock>, Without<Shape>)>, mut blocks: Query<&mut GridPosition, (With<ShapeBlock>, Without<ShapeLayout>)>,
mut commands: Commands,
visuals: Res<Visuals>,
) { ) {
query.iter().for_each(|(e, s, center)| { query.iter().for_each(|(e, sl, center)| {
debug!("Setting piece: {e:?} {center:?}\n{}", s.as_ascii()); debug!("Setting piece: {e:?} {center:?}\n{}", sl.as_ascii());
if blocks.is_empty() { debug_assert!(!blocks.is_empty());
let mesh = Mesh2d(visuals.mesh.clone());
let mat = MeshMaterial2d(visuals.material.clone()); let mut p = sl.coordinates_at(center);
commands blocks.iter_mut().for_each(|mut gp| {
.entity(e) *gp = p.next().unwrap().unwrap();
.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 on_add_shape_layout(
trigger: On<Add, ShapeLayout>,
visuals: Res<Visuals>,
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,
};
commands
.entity(e)
.with_related_entities::<ShapeBlock>(|parent| {
sl.coordinates_at(center).for_each(|gp| {
parent
.spawn((mesh.clone(), MeshMaterial2d(mat.clone()), gp.unwrap(), Block, TETRIS))
.observe(movement);
});
});
}
fn kb_input( fn kb_input(
mut events: MessageReader<KeyboardInput>, mut events: MessageReader<KeyboardInput>,
mut query: Query<(Entity, &mut Shape)>, mut query: Query<Entity, With<Shape>>,
curr: Res<State<GameState>>, curr: Res<State<GameState>>,
mut next: ResMut<NextState<GameState>>, mut next: ResMut<NextState<GameState>>,
mut commands: Commands, mut commands: Commands,
@ -819,7 +854,7 @@ fn kb_input(
key_code, state, .. key_code, state, ..
}| { }| {
if let ButtonState::Pressed = state { if let ButtonState::Pressed = state {
query.iter_mut().for_each(|(e, mut s)| { query.iter_mut().for_each(|e| {
match key_code { match key_code {
// Up arrow should rotate if in falling mode // Up arrow should rotate if in falling mode
// Only move up if in falling::off mode // Only move up if in falling::off mode
@ -860,13 +895,6 @@ fn kb_input(
GameState::Falling => GameState::Pause, GameState::Falling => GameState::Pause,
GameState::Pause => GameState::Falling, 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(),
_ => (), _ => (),
} }
}); });
@ -885,29 +913,17 @@ fn falling(mut shape: Query<Entity, With<Shape>>, mut commands: Commands) {
}); });
} }
// 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>) { fn add_piece(mut commands: Commands, mut shapes: ResMut<ShapesBuffer>) {
let this_shape = shapes.next.pop_front().unwrap(); let shape = shapes.next.pop_front().unwrap();
let shape_layout = ShapeLayout::from_shape(&shape);
commands commands
.spawn(( .spawn((
GridPosition { GridPosition {
y: Y_MAX - this_shape.height(), y: Y_MAX - shape_layout.height(),
..default() ..default()
}, },
this_shape, shape_layout,
shape,
TETRIS, TETRIS,
)) ))
.observe(movement) .observe(movement)
@ -1025,12 +1041,12 @@ fn movement(
&mut GridPosition, &mut GridPosition,
Or<(With<ShapeBlock>, With<ShapeBlocks>, Without<LineBlock>)>, Or<(With<ShapeBlock>, With<ShapeBlocks>, Without<LineBlock>)>,
>, >,
mut shape: Query<&mut Shape>, mut shape_layouts: Query<&mut ShapeLayout>,
inactive: Query<&GridPosition, (Without<ShapeBlock>, Without<ShapeBlocks>, With<LineBlock>)>, inactive: Query<&GridPosition, (Without<ShapeBlock>, Without<ShapeBlocks>, With<LineBlock>)>,
mut commands: Commands, mut commands: Commands,
) { ) {
if let (Ok(this_shape), Ok(center)) = ( if let (Ok(this_shape_layout), Ok(center)) = (
shape.get_mut(event.entity), shape_layouts.get_mut(event.entity),
grid_positions.get(event.entity), grid_positions.get(event.entity),
) { ) {
let new_positions = match event.event().direction { let new_positions = match event.event().direction {
@ -1042,17 +1058,17 @@ fn movement(
.map(|i| center.with_offset(0, -(i as isize))) .map(|i| center.with_offset(0, -(i as isize)))
.collect(), .collect(),
}; };
let new_shape = match event.event().direction { let new_shape_layout = match event.event().direction {
MovementDirection::Down MovementDirection::Down
| MovementDirection::Left | MovementDirection::Left
| MovementDirection::Right | MovementDirection::Right
| MovementDirection::Skip => this_shape.clone(), | MovementDirection::Skip => this_shape_layout.clone(),
MovementDirection::Rotate => this_shape.rotated(), MovementDirection::Rotate => this_shape_layout.rotated(),
}; };
debug!( debug!(
"Proposed change: {:?}\n{}", "Proposed change: {:?}\n{}",
new_positions, new_positions,
new_shape.as_ascii() new_shape_layout.as_ascii()
); );
for position in new_positions { for position in new_positions {
match position { match position {
@ -1061,7 +1077,7 @@ fn movement(
commands.entity(event.entity).remove::<Shape>(); commands.entity(event.entity).remove::<Shape>();
} }
Ok(new_center) => { Ok(new_center) => {
let new_blocks = new_shape.coordinates(&new_center); let new_blocks = new_shape_layout.coordinates_at(&new_center);
for block_gp in new_blocks { for block_gp in new_blocks {
match block_gp { match block_gp {
Err(OutOfBoundsError::Left) | Err(OutOfBoundsError::Right) => { Err(OutOfBoundsError::Left) | Err(OutOfBoundsError::Right) => {
@ -1094,8 +1110,8 @@ fn movement(
*gp = new_center; *gp = new_center;
// Update shape/rotation // Update shape/rotation
let mut s = shape.get_mut(event.entity).unwrap(); let mut sl = shape_layouts.get_mut(event.entity).unwrap();
*s = new_shape.clone(); *sl = new_shape_layout.clone();
} }
} }
} }
@ -1161,13 +1177,13 @@ fn update_next_shapes(mut buffer: ResMut<ShapesBuffer>) {
while buffer.next.len() < 8 { while buffer.next.len() < 8 {
// TODO: Shuffle these! // TODO: Shuffle these!
buffer.next.extend([ buffer.next.extend([
Shape::new_o(), Shape::O,
Shape::new_t(), Shape::T,
Shape::new_l(), Shape::L,
Shape::new_j(), Shape::J,
Shape::new_s(), Shape::S,
Shape::new_z(), Shape::Z,
Shape::new_i(), Shape::I,
]); ]);
} }
} }
@ -1181,35 +1197,40 @@ fn assert_grid_position_uniqueness(
} }
fn sync_health( fn sync_health(
query: Query<(Entity, &Health, &Mesh2d), Or<(Changed<Health>, Added<Health>)>>, query: Query<(Entity, &Health), Changed<Health>>,
parent: Query<&Children>, parent: Query<&Children>,
meshes: Res<Assets<Mesh>>,
mut texts: Query<&mut Text2d>, mut texts: Query<&mut Text2d>,
mut commands: Commands,
) { ) {
query.iter().for_each(|(e, h, m)| { query.iter().for_each(|(e, h)| {
if let Some(child) = parent let child = parent
.iter_descendants(e) .iter_descendants(e)
.find(|child| texts.contains(*child)) .find(|child| texts.contains(*child))
{ .unwrap();
info!("Updating health"); debug!("Updating health");
let mut t = texts.get_mut(child).unwrap(); let mut t = texts.get_mut(child).unwrap();
t.0 = format!("{}", h.0); t.0 = format!("{}", h.0);
} else { });
info!("Creating health display"); }
commands.entity(e).with_children(|parent| {
let mesh = meshes.get(&m.0).unwrap(); fn on_add_health(
let aabb = mesh.compute_aabb().unwrap(); event: On<Add, Health>,
let offset = Vec3::new(0.0, aabb.half_extents.y + 10.0, 0.0); query: Query<(&Health, &Mesh2d)>,
parent.spawn(( meshes: Res<Assets<Mesh>>,
Text2d(format!("{}", h.0)), mut commands: Commands,
TextColor(BLACK.into()), ) {
Transform::from_translation(offset), let (h, m) = query.get(event.entity).unwrap();
BATTLER, 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)] #[derive(Message, EntityEvent)]

@ -13,12 +13,16 @@ mod ui;
mod version; mod version;
// Rust stdlib // Rust stdlib
pub use std::collections::VecDeque; pub use std::{
pub use std::f32::consts::PI; collections::VecDeque,
pub use std::fmt::Display; f32::consts::PI,
fmt::Display,
};
pub use core::time::Duration;
// Community libraries // Community libraries
pub use bevy::{ pub use bevy::{
time::common_conditions::*,
asset::{ asset::{
AssetLoader, AssetMetaCheck, LoadContext, LoadState, LoadedFolder, RenderAssetUsages, AssetLoader, AssetMetaCheck, LoadContext, LoadState, LoadedFolder, RenderAssetUsages,
io::Reader, uuid_handle, io::Reader, uuid_handle,

Loading…
Cancel
Save