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!
///
use crate::{
game::{ActiveTile, Board, BoardIndex, Piece},
game::{ActiveTile, Board, BoardIndex, Piece, Tile},
prelude::*,
};
@ -42,15 +42,17 @@ impl Plugin for Display2dPlugin {
.run_if(any_with_component::<game::Selected>()),
cancel_place
.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>()),
draw_board
.run_if(resource_exists::<SpriteSheet>())
.run_if(any_component_removed::<game::Selected>()) // trigger if item was de-selected
.run_if(any_with_component::<BoardIndex>()),
// PERF: Only need to run when piece2d or tile2d added
set_sprite,
set_transform,
),
)
.add_systems(OnEnter(GameState::Display2d), (activate, draw_board))
.add_systems(OnEnter(GameState::Display2d), activate)
.add_systems(OnExit(GameState::Display2d), deactivate);
}
}
@ -67,7 +69,10 @@ struct Board2d;
/// Marker for 2d piece entities
#[derive(Debug, Component)]
struct Piece2d;
pub(crate) struct Piece2d;
#[derive(Debug, Component)]
struct Tile2d;
#[derive(Debug, Component)]
struct Display2dCamera;
@ -151,16 +156,11 @@ fn update_background(
}
/// STARTUP: Initialize the board for representation
fn initialize_board(sprite_sheet: Option<Res<SpriteSheet>>, mut commands: Commands) {
if let Some(sprite_sheet) = sprite_sheet {
fn initialize_board(board: Option<Res<Board>>, mut commands: Commands) {
if let Some(board) = board {
commands
.spawn((
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,
..default()
},
@ -170,82 +170,78 @@ fn initialize_board(sprite_sheet: Option<Res<SpriteSheet>>, mut commands: Comman
for i in 0..32 {
let x = 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 };
// Rectangle
parent.spawn((
SpriteSheetBundle {
texture_atlas,
sprite,
transform,
..default()
},
index,
));
let s = (x % 2) ^ (y % 2);
let tile = if s == 0 { Tile::Dark } else { Tile::Light };
parent.spawn((tile, index, Tile2d, SpriteSheetBundle { ..default() }));
}
});
}
}
/// Update the location of pieces on the board
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);
board.pieces().iter().for_each(|(piece, index)| {
// TODO: transform is slightly different, set sprite
parent.spawn((
piece.clone(),
SpriteSheetBundle {
texture_atlas,
sprite,
transform: Transform {
..transform.clone()
},
..default()
},
Piece2d,
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(
mut cameras: Query<&mut Camera, With<Display2dCamera>>,
mut boards: Query<&mut Visibility, With<Board2d>>,
@ -296,7 +292,7 @@ fn active_tile(
let size = {
let sprite_size = atlases
.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")
.size();
let (transform_scale, _, _) = transform.to_scale_rotation_translation();
@ -372,7 +368,8 @@ fn place_piece(
active: Res<ActiveTile>,
mut board: ResMut<Board>,
mut commands: Commands,
mut writer: EventWriter<game::GameEvent>,
mut game_events: EventWriter<game::GameEvent>,
mut move_events: EventWriter<game::Move>,
) {
events
.iter()
@ -391,11 +388,14 @@ fn place_piece(
))
})
.for_each(|(entity, from, to)| match board.move_piece(from, to) {
Ok(()) => {
Ok(moves) => {
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!"),
})
}
@ -417,3 +417,19 @@ fn cancel_place(
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,
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(
Update,
set_piece_position.run_if(in_state(GameState::Display3d)),
@ -36,7 +30,7 @@ impl Plugin for Display3dPlugin {
struct Board3d;
#[derive(Debug, Component)]
struct Piece3d;
pub(crate) struct Piece3d;
fn initialize_camera(mut commands: Commands) {
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
fn set_piece_position(
mut events: Query<(&mut Transform, &BoardIndex), (With<Piece3d>, Changed<BoardIndex>)>,

@ -5,8 +5,17 @@ pub(crate) struct GamePlugin;
impl Plugin for GamePlugin {
fn build(&self, app: &mut App) {
app.add_event::<GameEvent>()
.add_event::<Move>()
.init_resource::<ActiveTile>()
.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(
PostUpdate,
(
@ -26,6 +35,12 @@ pub(crate) enum Piece {
Queen,
}
#[derive(Debug, Component, Clone, PartialEq)]
pub(crate) enum Tile {
Dark,
Light,
}
// manually for the type.
impl std::fmt::Display for Piece {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
@ -65,9 +80,17 @@ pub(crate) enum GameEvent {
#[derive(Debug, Resource)]
pub(crate) struct Board {
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 x: usize,
pub y: usize,
@ -96,15 +119,35 @@ impl Board {
&mut self,
from: &BoardIndex,
to: &BoardIndex,
) -> Result<(), GameError> {
) -> Result<Vec<Move>, GameError> {
if from == to {
Err(GameError::NullMove)
} else if self.at(to).is_none() {
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
// Update board to reflect move
self.inner[to.y][to.x] = Some(from_val);
self.inner[from.y][from.x] = None;
Ok(())
Ok(moves)
})
} else {
Err(GameError::InvalidMove)
@ -141,6 +184,7 @@ pub(crate) struct Selected;
fn setup_board(mut commands: Commands) {
use Piece::*;
commands.insert_resource(Board {
moves: vec![],
inner: vec![
vec![
Some(Queen),
@ -204,3 +248,24 @@ fn debug_hovering(
fn debug_board(board: Res<Board>, mut debug_info: ResMut<debug::DebugInfo>) {
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