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_ui.after(init_cameras),
init_battler,
init_deck,
),
)
// Input and basic systems
@ -56,10 +55,10 @@ fn main() {
.after(update_next_shapes),
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_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),
deactivate_shape
.run_if(any_component_removed::<Shape>)
@ -74,7 +73,7 @@ fn main() {
.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)),
damage_over_time.run_if(on_timer(Duration::from_secs(5))),
),
)
// UI systems
@ -88,6 +87,8 @@ fn main() {
),
)
.add_observer(deal_damage)
.add_observer(on_add_shape_layout)
.add_observer(on_add_health)
.run();
}
@ -227,8 +228,14 @@ enum GameState {
#[derive(Resource, Debug)]
struct Visuals {
material: Handle<ColorMaterial>,
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)]
@ -266,7 +273,7 @@ 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()),
Some(s) => write!(f, "{}", ShapeLayout::from_shape(s).as_ascii()),
None => write!(f, "---"),
}
}
@ -281,17 +288,36 @@ fn init_tetris(
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(),
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| {
@ -299,7 +325,10 @@ fn init_tetris(
(0..X_MAX).for_each(|x| {
commands.spawn((
Mesh2d(mesh.clone()),
MeshMaterial2d(grid_material.clone()),
MeshMaterial2d(materials.add(ColorMaterial {
color: BLACK.into(),
..default()
})),
GridPosition { x, y },
Transform::from_xyz(0.0, 0.0, -1.0),
GridBackground,
@ -312,12 +341,6 @@ fn init_tetris(
});
}
fn init_deck(
mut commands: Commands,
) {
todo!()
}
#[derive(Component, Debug)]
struct Protagonist;
@ -579,32 +602,63 @@ fn init_debug_ui(mut commands: Commands) {
});
}
/// Enum describing a shape option
#[derive(Component, Debug, Clone)]
struct Shape {
layout: ShapeBlockLayout,
enum Shape {
O,
T,
L,
J,
S,
Z,
I,
}
impl Default for Shape {
fn default() -> Self {
Self::new_t()
Self::T
}
}
impl Display for Shape {
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 {
fn from(inner: Vec<Vec<u8>>) -> Shape {
Shape {
layout: ShapeBlockLayout { inner },
}
impl Shape {
fn reposition(
(x_offset, y_offset): (isize, isize),
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 {
vec![vec![1, 1], vec![1, 1]].into()
}
@ -637,45 +691,6 @@ impl Shape {
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 {
let mut inner = vec![];
for _ in 0..self.inner[0].len() {
@ -686,7 +701,7 @@ impl ShapeBlockLayout {
inner[j].insert(0, *x);
}
}
ShapeBlockLayout { inner }
ShapeLayout { inner }
}
fn center(&self) -> (usize, usize) {
@ -709,6 +724,14 @@ impl ShapeBlockLayout {
(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)> {
let (mid_x, mid_y) = self.center();
// Loop over outer vec (i)
@ -771,45 +794,57 @@ fn update_position(
// TODO: Move to event
fn update_shape_blocks(
query: Query<
(Entity, &Shape, &GridPosition),
(Entity, &ShapeLayout, &GridPosition),
Or<(
Added<Shape>,
Changed<Shape>,
Added<GridPosition>,
Changed<ShapeLayout>,
Changed<GridPosition>,
)>,
>,
mut blocks: Query<&mut GridPosition, (With<ShapeBlock>, Without<Shape>)>,
mut commands: Commands,
visuals: Res<Visuals>,
mut blocks: Query<&mut GridPosition, (With<ShapeBlock>, Without<ShapeLayout>)>,
) {
query.iter().for_each(|(e, s, center)| {
debug!("Setting piece: {e:?} {center:?}\n{}", s.as_ascii());
query.iter().for_each(|(e, sl, center)| {
debug!("Setting piece: {e:?} {center:?}\n{}", sl.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();
});
}
debug_assert!(!blocks.is_empty());
let mut p = sl.coordinates_at(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(
mut events: MessageReader<KeyboardInput>,
mut query: Query<(Entity, &mut Shape)>,
mut query: Query<Entity, With<Shape>>,
curr: Res<State<GameState>>,
mut next: ResMut<NextState<GameState>>,
mut commands: Commands,
@ -819,7 +854,7 @@ fn kb_input(
key_code, state, ..
}| {
if let ButtonState::Pressed = state {
query.iter_mut().for_each(|(e, mut s)| {
query.iter_mut().for_each(|e| {
match key_code {
// Up arrow should rotate if in falling mode
// Only move up if in falling::off mode
@ -860,13 +895,6 @@ fn kb_input(
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(),
_ => (),
}
});
@ -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>) {
let this_shape = shapes.next.pop_front().unwrap();
let shape = shapes.next.pop_front().unwrap();
let shape_layout = ShapeLayout::from_shape(&shape);
commands
.spawn((
GridPosition {
y: Y_MAX - this_shape.height(),
y: Y_MAX - shape_layout.height(),
..default()
},
this_shape,
shape_layout,
shape,
TETRIS,
))
.observe(movement)
@ -1025,12 +1041,12 @@ fn movement(
&mut GridPosition,
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>)>,
mut commands: Commands,
) {
if let (Ok(this_shape), Ok(center)) = (
shape.get_mut(event.entity),
if let (Ok(this_shape_layout), Ok(center)) = (
shape_layouts.get_mut(event.entity),
grid_positions.get(event.entity),
) {
let new_positions = match event.event().direction {
@ -1042,17 +1058,17 @@ fn movement(
.map(|i| center.with_offset(0, -(i as isize)))
.collect(),
};
let new_shape = match event.event().direction {
let new_shape_layout = match event.event().direction {
MovementDirection::Down
| MovementDirection::Left
| MovementDirection::Right
| MovementDirection::Skip => this_shape.clone(),
MovementDirection::Rotate => this_shape.rotated(),
| MovementDirection::Skip => this_shape_layout.clone(),
MovementDirection::Rotate => this_shape_layout.rotated(),
};
debug!(
"Proposed change: {:?}\n{}",
new_positions,
new_shape.as_ascii()
new_shape_layout.as_ascii()
);
for position in new_positions {
match position {
@ -1061,7 +1077,7 @@ fn movement(
commands.entity(event.entity).remove::<Shape>();
}
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 {
match block_gp {
Err(OutOfBoundsError::Left) | Err(OutOfBoundsError::Right) => {
@ -1094,8 +1110,8 @@ fn movement(
*gp = new_center;
// Update shape/rotation
let mut s = shape.get_mut(event.entity).unwrap();
*s = new_shape.clone();
let mut sl = shape_layouts.get_mut(event.entity).unwrap();
*sl = new_shape_layout.clone();
}
}
}
@ -1161,13 +1177,13 @@ fn update_next_shapes(mut buffer: ResMut<ShapesBuffer>) {
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(),
Shape::O,
Shape::T,
Shape::L,
Shape::J,
Shape::S,
Shape::Z,
Shape::I,
]);
}
}
@ -1181,35 +1197,40 @@ fn assert_grid_position_uniqueness(
}
fn sync_health(
query: Query<(Entity, &Health, &Mesh2d), Or<(Changed<Health>, Added<Health>)>>,
query: Query<(Entity, &Health), Changed<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
query.iter().for_each(|(e, h)| {
let 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,
));
});
}
})
.unwrap();
debug!("Updating health");
let mut t = texts.get_mut(child).unwrap();
t.0 = format!("{}", h.0);
});
}
fn on_add_health(
event: On<Add, Health>,
query: Query<(&Health, &Mesh2d)>,
meshes: Res<Assets<Mesh>>,
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)]

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

Loading…
Cancel
Save