Drastic rework of movement

* Added `moves` ledger to Board
  * This should help with future work like undo, automated tests,
    net-play.
* Refactored 2d and 3d displays to use BoardIndex -> Transform
  * This means we can have a generic system set the position of Entities
    based on moves on the Board.
* Refactored 2d view to have centered board
  * (instead of shifting root entity which had poor behavior
    characteristics)
selection-refactor
Elijah Voigt 2 years ago
parent 03fa207297
commit 8a9b47be6b

@ -12,7 +12,7 @@ use bevy::{
/// TODO: Handle Cursor! /// TODO: Handle Cursor!
/// ///
use crate::{ use crate::{
game::{ActiveTile, Board, BoardIndex, Piece}, game::{ActiveTile, Board, BoardIndex, Piece, Tile},
prelude::*, prelude::*,
}; };
@ -42,15 +42,17 @@ impl Plugin for Display2dPlugin {
.run_if(any_with_component::<game::Selected>()), .run_if(any_with_component::<game::Selected>()),
cancel_place cancel_place
.run_if(in_state(GameState::Display2d)) .run_if(in_state(GameState::Display2d))
.run_if(any_with_component::<game::Selected>()), .run_if(on_event::<MouseButtonInput>()),
snap_back_cancel
.run_if(in_state(GameState::Display2d))
.run_if(any_component_removed::<game::Selected>()),
update_background.run_if(on_event::<WindowResized>()), update_background.run_if(on_event::<WindowResized>()),
draw_board // PERF: Only need to run when piece2d or tile2d added
.run_if(resource_exists::<SpriteSheet>()) set_sprite,
.run_if(any_component_removed::<game::Selected>()) // trigger if item was de-selected set_transform,
.run_if(any_with_component::<BoardIndex>()),
), ),
) )
.add_systems(OnEnter(GameState::Display2d), (activate, draw_board)) .add_systems(OnEnter(GameState::Display2d), activate)
.add_systems(OnExit(GameState::Display2d), deactivate); .add_systems(OnExit(GameState::Display2d), deactivate);
} }
} }
@ -67,7 +69,10 @@ struct Board2d;
/// Marker for 2d piece entities /// Marker for 2d piece entities
#[derive(Debug, Component)] #[derive(Debug, Component)]
struct Piece2d; pub(crate) struct Piece2d;
#[derive(Debug, Component)]
struct Tile2d;
#[derive(Debug, Component)] #[derive(Debug, Component)]
struct Display2dCamera; struct Display2dCamera;
@ -151,16 +156,11 @@ fn update_background(
} }
/// STARTUP: Initialize the board for representation /// STARTUP: Initialize the board for representation
fn initialize_board(sprite_sheet: Option<Res<SpriteSheet>>, mut commands: Commands) { fn initialize_board(board: Option<Res<Board>>, mut commands: Commands) {
if let Some(sprite_sheet) = sprite_sheet { if let Some(board) = board {
commands commands
.spawn(( .spawn((
SpatialBundle { SpatialBundle {
transform: Transform::from_xyz(
-SCALE * TILE_SIZE * 7.0 / 2.0, // TODO: WHY???
-SCALE * TILE_SIZE * 3.0 / 2.0, // Why 7 and 3??
0.0,
),
visibility: Visibility::Hidden, visibility: Visibility::Hidden,
..default() ..default()
}, },
@ -170,82 +170,78 @@ fn initialize_board(sprite_sheet: Option<Res<SpriteSheet>>, mut commands: Comman
for i in 0..32 { for i in 0..32 {
let x = i % 8; let x = i % 8;
let y = i / 8; let y = i / 8;
let s = (x % 2) ^ (y % 2);
let transform = Transform::from_scale(Vec3::splat(SCALE)).with_translation(
Vec3::new(SCALE * 16.0 * x as f32, SCALE * 16.0 * y as f32, 0.0),
);
let sprite = TextureAtlasSprite::new(s);
let texture_atlas = sprite_sheet.handle.clone();
let index = BoardIndex { x, y }; let index = BoardIndex { x, y };
// Rectangle let s = (x % 2) ^ (y % 2);
parent.spawn(( let tile = if s == 0 { Tile::Dark } else { Tile::Light };
SpriteSheetBundle {
texture_atlas, parent.spawn((tile, index, Tile2d, SpriteSheetBundle { ..default() }));
sprite,
transform,
..default()
},
index,
));
} }
});
}
}
/// Update the location of pieces on the board board.pieces().iter().for_each(|(piece, index)| {
fn draw_board(
board: Option<Res<Board>>,
sprite_sheet: Option<Res<SpriteSheet>>,
root: Query<Entity, With<Board2d>>,
tiles: Query<(&Transform, &BoardIndex)>,
pieces: Query<Entity, With<Piece2d>>,
mut commands: Commands,
) {
if let (Some(board), Some(sprite_sheet)) = (board, sprite_sheet) {
pieces
.iter()
.for_each(|entity| commands.entity(entity).despawn_recursive());
commands.entity(root.single()).with_children(|parent| {
board
.pieces()
.iter()
.filter_map(|(board_index, piece)| {
tiles.iter().find_map(|(transform, this_index)| {
(*this_index == *board_index).then(|| (piece, transform, this_index))
})
})
.for_each(|(piece, transform, index)| {
let texture_atlas = sprite_sheet.handle.clone();
let s = match piece {
Piece::Queen => 2,
Piece::Drone => 3,
Piece::Pawn => 4,
};
let sprite = TextureAtlasSprite::new(s);
// TODO: transform is slightly different, set sprite // TODO: transform is slightly different, set sprite
parent.spawn(( parent.spawn((
piece.clone(), piece.clone(),
SpriteSheetBundle {
texture_atlas,
sprite,
transform: Transform {
..transform.clone()
},
..default()
},
Piece2d, Piece2d,
index.clone(), index.clone(),
SpriteSheetBundle { ..default() },
)); ));
}); });
}); });
} }
} }
fn set_sprite(
mut events: Query<
(
&mut TextureAtlasSprite,
&mut Handle<TextureAtlas>,
Option<&Piece>,
Option<&Tile>,
),
Or<(Added<Piece>, Added<Tile2d>)>,
>,
sprite_sheet: Option<Res<SpriteSheet>>,
) {
if let Some(sprite_sheet) = sprite_sheet {
events
.iter_mut()
.for_each(|(mut sprite, mut texture_atlas, piece, tile)| {
*texture_atlas = sprite_sheet.handle.clone();
let s = match piece {
Some(Piece::Queen) => 2,
Some(Piece::Drone) => 3,
Some(Piece::Pawn) => 4,
None => match tile {
Some(Tile::Dark) => 0,
Some(Tile::Light) => 1,
None => 99,
},
};
*sprite = TextureAtlasSprite::new(s);
debug!("Setting sprite for {:?} {:?}", piece, tile);
});
}
}
/// Sets a piece location given it's board index
fn set_transform(
mut events: Query<
(&mut Transform, &BoardIndex),
(
Or<(With<Piece2d>, With<Tile2d>)>,
Or<(Changed<BoardIndex>, Added<BoardIndex>)>,
),
>,
) {
events.iter_mut().for_each(|(mut t, i)| {
let x = SCALE * 16.0 * ((i.x as f32) - 3.5);
let y = SCALE * 16.0 * ((i.y as f32) - 1.5);
*t = Transform::from_scale(Vec3::splat(SCALE)).with_translation(Vec3::new(x, y, 0.0));
info!("setting position of {:?} to {:?}", i, t);
});
}
fn activate( fn activate(
mut cameras: Query<&mut Camera, With<Display2dCamera>>, mut cameras: Query<&mut Camera, With<Display2dCamera>>,
mut boards: Query<&mut Visibility, With<Board2d>>, mut boards: Query<&mut Visibility, With<Board2d>>,
@ -296,7 +292,7 @@ fn active_tile(
let size = { let size = {
let sprite_size = atlases let sprite_size = atlases
.get(handle) .get(handle)
.map(|atlas| atlas.textures.get(*index).expect("Get this rect")) .map(|atlas| atlas.textures.get(*index).expect("Get this rect texture"))
.expect("get this rect") .expect("get this rect")
.size(); .size();
let (transform_scale, _, _) = transform.to_scale_rotation_translation(); let (transform_scale, _, _) = transform.to_scale_rotation_translation();
@ -372,7 +368,8 @@ fn place_piece(
active: Res<ActiveTile>, active: Res<ActiveTile>,
mut board: ResMut<Board>, mut board: ResMut<Board>,
mut commands: Commands, mut commands: Commands,
mut writer: EventWriter<game::GameEvent>, mut game_events: EventWriter<game::GameEvent>,
mut move_events: EventWriter<game::Move>,
) { ) {
events events
.iter() .iter()
@ -391,11 +388,14 @@ fn place_piece(
)) ))
}) })
.for_each(|(entity, from, to)| match board.move_piece(from, to) { .for_each(|(entity, from, to)| match board.move_piece(from, to) {
Ok(()) => { Ok(moves) => {
commands.entity(entity).remove::<game::Selected>(); commands.entity(entity).remove::<game::Selected>();
writer.send(game::GameEvent::PlacePiece); moves.iter().for_each(|m| {
move_events.send(m.clone());
});
game_events.send(game::GameEvent::PlacePiece);
} }
Err(game::GameError::NullMove) => writer.send(game::GameEvent::PlacePiece), Err(game::GameError::NullMove) => game_events.send(game::GameEvent::PlacePiece),
Err(game::GameError::InvalidMove) => warn!("Invalid move!"), Err(game::GameError::InvalidMove) => warn!("Invalid move!"),
}) })
} }
@ -417,3 +417,19 @@ fn cancel_place(
writer.send(game::GameEvent::PlacePiece); writer.send(game::GameEvent::PlacePiece);
}) })
} }
fn snap_back_cancel(
mut events: RemovedComponents<game::Selected>,
query: Query<&game::BoardIndex, With<Piece2d>>,
mut move_events: EventWriter<game::Move>,
) {
events.iter().for_each(|entity| {
if let Ok(idx) = query.get(entity) {
move_events.send(game::Move {
epoch: 0,
from: idx.clone(),
to: Some(idx.clone()),
});
}
})
}

@ -11,12 +11,6 @@ impl Plugin for Display3dPlugin {
Update, Update,
menu::exit_to_menu.run_if(in_state(GameState::Display3d)), menu::exit_to_menu.run_if(in_state(GameState::Display3d)),
) )
.add_systems(
Update,
update_board
// .run_if(in_state(GameState::Display3d))
.run_if(resource_changed::<game::Board>()),
)
.add_systems( .add_systems(
Update, Update,
set_piece_position.run_if(in_state(GameState::Display3d)), set_piece_position.run_if(in_state(GameState::Display3d)),
@ -36,7 +30,7 @@ impl Plugin for Display3dPlugin {
struct Board3d; struct Board3d;
#[derive(Debug, Component)] #[derive(Debug, Component)]
struct Piece3d; pub(crate) struct Piece3d;
fn initialize_camera(mut commands: Commands) { fn initialize_camera(mut commands: Commands) {
commands.spawn(( commands.spawn((
@ -157,86 +151,6 @@ fn initialize_board(
} }
} }
/// Update the location of pieces on the 3d board
fn update_board(
board: Option<Res<game::Board>>,
mut pieces: Query<(Entity, &mut BoardIndex, &game::Piece), With<Piece3d>>,
mut commands: Commands,
) {
if let Some(board) = board {
let board_pieces = board.pieces();
{
// Get a list of all Queen piece entities
let mut queen_entities = pieces
.iter_mut()
.filter_map(|(entity, idx, p)| (*p == game::Piece::Queen).then_some((entity, idx)));
// Compare each board piece with an entity piece
// Set the board index for this particular queen
board_pieces
.iter()
.filter_map(|(index, p)| (*p == game::Piece::Queen).then_some(index))
.for_each(|index| {
if let Some((_, mut idx)) = queen_entities.next() {
*idx = index.clone();
}
});
// If we have one left that means this queen has been "destroyed" so we remove the
// BoardIndex component from the entitiy, removing it from play.
if let Some((entity, _)) = queen_entities.next() {
commands.entity(entity).remove::<BoardIndex>();
}
}
{
// Get a list of all drone piece entities
let mut drone_entities = pieces
.iter_mut()
.filter_map(|(entity, idx, p)| (*p == game::Piece::Drone).then_some((entity, idx)));
// Compare each board piece with an entity piece
// Set the board index for this particular drone
board_pieces
.iter()
.filter_map(|(index, p)| (*p == game::Piece::Drone).then_some(index))
.for_each(|index| {
if let Some((_, mut idx)) = drone_entities.next() {
*idx = index.clone();
}
});
// If we have one left that means this drone has been "destroyed" so we remove the
// BoardIndex component from the entitiy, removing it from play.
if let Some((entity, _)) = drone_entities.next() {
commands.entity(entity).remove::<BoardIndex>();
}
}
{
// Get a list of all pawn piece entities
let mut pawn_entities = pieces
.iter_mut()
.filter_map(|(entity, idx, p)| (*p == game::Piece::Pawn).then_some((entity, idx)));
// Compare each board piece with an entity piece
// Set the board index for this particular pawn
board_pieces
.iter()
.filter_map(|(index, p)| (*p == game::Piece::Pawn).then_some(index))
.for_each(|index| {
if let Some((_, mut idx)) = pawn_entities.next() {
*idx = index.clone();
}
});
// If we have one left that means this pawn has been "destroyed" so we remove the
// BoardIndex component from the entitiy, removing it from play.
if let Some((entity, _)) = pawn_entities.next() {
commands.entity(entity).remove::<BoardIndex>();
}
}
}
}
/// Sets a piece location given it's board index /// Sets a piece location given it's board index
fn set_piece_position( fn set_piece_position(
mut events: Query<(&mut Transform, &BoardIndex), (With<Piece3d>, Changed<BoardIndex>)>, mut events: Query<(&mut Transform, &BoardIndex), (With<Piece3d>, Changed<BoardIndex>)>,

@ -5,8 +5,17 @@ pub(crate) struct GamePlugin;
impl Plugin for GamePlugin { impl Plugin for GamePlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_event::<GameEvent>() app.add_event::<GameEvent>()
.add_event::<Move>()
.init_resource::<ActiveTile>() .init_resource::<ActiveTile>()
.add_systems(Startup, setup_board) .add_systems(Startup, setup_board)
.add_systems(
Update,
update_board::<display2d::Piece2d>.run_if(on_event::<Move>()),
)
.add_systems(
Update,
update_board::<display3d::Piece3d>.run_if(on_event::<Move>()),
)
.add_systems( .add_systems(
PostUpdate, PostUpdate,
( (
@ -26,6 +35,12 @@ pub(crate) enum Piece {
Queen, Queen,
} }
#[derive(Debug, Component, Clone, PartialEq)]
pub(crate) enum Tile {
Dark,
Light,
}
// manually for the type. // manually for the type.
impl std::fmt::Display for Piece { impl std::fmt::Display for Piece {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
@ -65,9 +80,17 @@ pub(crate) enum GameEvent {
#[derive(Debug, Resource)] #[derive(Debug, Resource)]
pub(crate) struct Board { pub(crate) struct Board {
inner: Vec<Vec<Option<Piece>>>, inner: Vec<Vec<Option<Piece>>>,
moves: Vec<Move>,
} }
#[derive(Debug, Component, PartialEq, Clone)] #[derive(Debug, Default, Event, Clone)]
pub(crate) struct Move {
pub epoch: usize,
pub from: BoardIndex,
pub to: Option<BoardIndex>,
}
#[derive(Debug, Component, PartialEq, Clone, Default)]
pub(crate) struct BoardIndex { pub(crate) struct BoardIndex {
pub x: usize, pub x: usize,
pub y: usize, pub y: usize,
@ -96,15 +119,35 @@ impl Board {
&mut self, &mut self,
from: &BoardIndex, from: &BoardIndex,
to: &BoardIndex, to: &BoardIndex,
) -> Result<(), GameError> { ) -> Result<Vec<Move>, GameError> {
if from == to { if from == to {
Err(GameError::NullMove) Err(GameError::NullMove)
} else if self.at(to).is_none() { } else if self.at(to).is_none() {
self.at(from).map_or(Err(GameError::NullMove), |from_val| { self.at(from).map_or(Err(GameError::NullMove), |from_val| {
// The current epoch is the last epoch + 1
let epoch = self.moves.last().unwrap_or(&Move { ..default() }).epoch + 1;
// Local moves vec we can return
let mut moves = vec![];
// If the position we are moving to is occupied, capture the removal in the ledger
if self.inner[to.y][to.x].is_some() {
moves.push(Move {
epoch,
from: to.clone(),
to: None,
});
}
// Capture the intened move in the moves ledger
moves.push(Move {
epoch,
from: from.clone(),
to: Some(to.clone()),
});
self.moves.extend(moves.clone());
// TODO: We can self.inner.swap(to, from) if board is single vec // TODO: We can self.inner.swap(to, from) if board is single vec
// Update board to reflect move
self.inner[to.y][to.x] = Some(from_val); self.inner[to.y][to.x] = Some(from_val);
self.inner[from.y][from.x] = None; self.inner[from.y][from.x] = None;
Ok(()) Ok(moves)
}) })
} else { } else {
Err(GameError::InvalidMove) Err(GameError::InvalidMove)
@ -141,6 +184,7 @@ pub(crate) struct Selected;
fn setup_board(mut commands: Commands) { fn setup_board(mut commands: Commands) {
use Piece::*; use Piece::*;
commands.insert_resource(Board { commands.insert_resource(Board {
moves: vec![],
inner: vec![ inner: vec![
vec![ vec![
Some(Queen), Some(Queen),
@ -204,3 +248,24 @@ fn debug_hovering(
fn debug_board(board: Res<Board>, mut debug_info: ResMut<debug::DebugInfo>) { fn debug_board(board: Res<Board>, mut debug_info: ResMut<debug::DebugInfo>) {
debug_info.set("board".into(), format!("\n{}", *board)); debug_info.set("board".into(), format!("\n{}", *board));
} }
/// Update this method to use a diff between the board and the state of the 2d/3d worlds
/// Only update the tiles without a corresponding board piece
fn update_board<T: Component>(
mut events: EventReader<Move>,
mut pieces: Query<(Entity, &mut BoardIndex), With<T>>,
mut commands: Commands,
) {
events.iter().for_each(|Move { from, to, .. }| {
pieces
.iter_mut()
.filter(|(_, index)| **index == *from)
.for_each(|(entity, mut index)| {
if let Some(idx) = to {
*index = idx.clone();
} else {
commands.entity(entity).remove::<BoardIndex>();
}
})
})
}

Loading…
Cancel
Save