diff --git a/Cargo.toml b/Cargo.toml index 939ba28..914f76e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ tracing = { version = "0.1", features = [ "max_level_debug", "release_max_level_warn", ] } -hexx = { version = "0.18", features = ["bevy_reflect"] } +hexx = { version = "0.18", features = ["bevy_reflect", "grid"] } bevy_prototype_lyon = "0.12" [features] diff --git a/src/hexgrid/direction.rs b/src/hexgrid/direction.rs deleted file mode 100644 index 7522a5f..0000000 --- a/src/hexgrid/direction.rs +++ /dev/null @@ -1,36 +0,0 @@ -use bevy::prelude::*; -use hexx::EdgeDirection; - -pub(super) fn plugin(_app: &mut App) {} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)] -pub enum Direction { - Top, - TopRight, - BottomRight, - Bottom, - BottomLeft, - TopLeft, -} - -impl Direction { - pub const ALL: [Direction; 6] = [ - Self::Top, - Self::TopRight, - Self::BottomRight, - Self::Bottom, - Self::BottomLeft, - Self::TopLeft, - ]; - - pub fn opposite(&self) -> Self { - match self { - Self::Top => Self::Bottom, - Self::TopRight => Self::BottomLeft, - Self::BottomRight => Self::TopLeft, - Self::Bottom => Self::Top, - Self::BottomLeft => Self::TopRight, - Self::TopLeft => Self::BottomRight, - } - } -} diff --git a/src/hexgrid/grid.rs b/src/hexgrid/grid.rs deleted file mode 100644 index 757e829..0000000 --- a/src/hexgrid/grid.rs +++ /dev/null @@ -1,194 +0,0 @@ -use bevy::{ - color::palettes::css::{BLACK, GREEN, RED}, - prelude::*, - utils::hashbrown::HashMap, -}; - -use bevy_prototype_lyon::{ - draw::{Fill, Stroke}, - entity::ShapeBundle, - path::PathBuilder, - plugin::ShapePlugin, -}; -use rand::{prelude::SliceRandom, rngs::ThreadRng, thread_rng}; - -use super::tile::{AxialCoord, Tile}; - -const DIRECTIONS: [AxialCoord; 6] = [ - AxialCoord { q: 1, r: 0 }, // Right - AxialCoord { q: 1, r: -1 }, // Top-right - AxialCoord { q: 0, r: -1 }, // Top-left - AxialCoord { q: -1, r: 0 }, // Left - AxialCoord { q: -1, r: 1 }, // Bottom-left - AxialCoord { q: 0, r: 1 }, // Bottom-right -]; - -pub struct HexGrid; - -impl Plugin for HexGrid { - fn build(&self, app: &mut App) { - app.add_plugins(ShapePlugin); - app.add_systems(Startup, setup_system); - } -} - -pub(super) fn setup_system(mut commands: Commands) { - let radius = 7; - let mut grid = generate_hex_grix(radius); - - let start_coord = AxialCoord::new(-radius, 0); - let end_coord = AxialCoord::new(radius, 0); - - let mut rng = thread_rng(); - generate_maze(&mut grid, start_coord, &mut rng); - - render_maze(&mut commands, &mut grid, radius, start_coord, end_coord); -} - -fn generate_hex_grix(radius: i32) -> HashMap { - let mut grid = HashMap::new(); - - for q in -radius..=radius { - let r1 = (-radius).max(-q - radius); - let r2 = radius.min(-q + radius); - - for r in r1..=r2 { - let coord = AxialCoord::new(q, r); - let tile = Tile { - position: coord, - walls: [true; 6], - visited: false, - }; - grid.insert(coord, tile); - } - } - grid -} - -fn add_hex_tile( - commands: &mut Commands, - position: Vec2, - size: f32, - walls: [bool; 6], - fill_color: Color, -) { - let hex_points = (0..6) - .map(|i| { - let angle_deg = 60. * i as f32 - 30.; - let angle_rad = angle_deg.to_radians(); - Vec2::new(size * angle_rad.cos(), size * angle_rad.sin()) - }) - .collect::>(); - - let mut path_builder = PathBuilder::new(); - path_builder.move_to(hex_points[0]); - for point in &hex_points[1..] { - path_builder.line_to(*point); - } - path_builder.close(); - let hexagon = path_builder.build(); - - // Create the hexagon fill - commands.spawn(( - ShapeBundle { - path: hexagon, - spatial: SpatialBundle { - transform: Transform::from_xyz(position.x, position.y, 0.), - ..default() - }, - ..default() - }, - Fill::color(fill_color), - )); - - // Draw walls - for i in 0..6 { - if walls[i] { - let start = hex_points[i]; - let end = hex_points[(i + 1) % 6]; - let mut line_builder = PathBuilder::new(); - line_builder.move_to(start); - line_builder.line_to(end); - let line = line_builder.build(); - - commands.spawn(( - ShapeBundle { - path: line, - spatial: SpatialBundle { - transform: Transform::from_xyz(position.x, position.y, 1.), - ..default() - }, - ..default() - }, - Stroke::new(BLACK, 2.), - )); - } - } -} - -fn generate_maze( - grid: &mut HashMap, - current_coord: AxialCoord, - rng: &mut ThreadRng, -) { - { - let current_tile = grid.get_mut(¤t_coord).unwrap(); - current_tile.visit(); - } - - let mut directions = DIRECTIONS.clone(); - directions.shuffle(rng); - - for (i, direction) in directions.iter().enumerate() { - let neightbor_coord = AxialCoord { - q: current_coord.q + direction.q, - r: current_coord.r + direction.r, - }; - - if let Some(neightbor_tile) = grid.get(&neightbor_coord) { - if !neightbor_tile.visited { - // Remove the wall between current_tile and neighbor_tile - { - let current_tile = grid.get_mut(¤t_coord).unwrap(); - current_tile.walls[i] = false; - } - { - let neightbor_tile = grid.get_mut(&neightbor_coord).unwrap(); - neightbor_tile.walls[opposite_wall(i)] = false; - } - // Recurse with the neighbor tile - generate_maze(grid, neightbor_coord, rng); - } - } - } -} - -fn render_maze( - commands: &mut Commands, - grid: &mut HashMap, - radius: i32, - start_coord: AxialCoord, - end_coord: AxialCoord, -) { - let hex_size = 30.; - let hex_height = hex_size * 2.; - let hex_width = (3.0_f32).sqrt() * hex_size; - - for tile in grid.values() { - let (q, r) = (tile.position.q, tile.position.r); - let x = hex_width * (q as f32 + r as f32 / 2.); - let y = hex_height * (r as f32 * 3. / 4.); - let mut fill_color = Color::srgb(0.8, 0.8, 0.8); - if tile.position == start_coord { - fill_color = GREEN.into(); - } else if tile.position == end_coord { - fill_color = RED.into(); - } - - add_hex_tile(commands, Vec2::new(x, y), hex_size, tile.walls, fill_color); - } -} - -fn opposite_wall(index: usize) -> usize { - (index + 3) % 6 -} diff --git a/src/hexgrid/mod.rs b/src/hexgrid/mod.rs deleted file mode 100644 index 0dd34ee..0000000 --- a/src/hexgrid/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -use bevy::{ - ecs::{system::RunSystemOnce, world::Command}, - prelude::*, -}; -use bevy_prototype_lyon::plugin::ShapePlugin; -use grid::setup_system; -pub mod direction; -pub mod grid; -pub mod tile; - -pub struct HexGrid; - -impl Plugin for HexGrid { - fn build(&self, app: &mut App) { - app.add_plugins(ShapePlugin); - } -} - -impl Command for HexGrid { - fn apply(self, world: &mut World) { - world.run_system_once(setup_system); - } -} - -pub fn spawn_grid(world: &mut World) { - HexGrid.apply(world); -} diff --git a/src/hexgrid/tile.rs b/src/hexgrid/tile.rs deleted file mode 100644 index ad13baf..0000000 --- a/src/hexgrid/tile.rs +++ /dev/null @@ -1,24 +0,0 @@ -#[derive(Debug, Clone)] -pub struct Tile { - pub position: AxialCoord, - pub walls: [bool; 6], - pub visited: bool, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct AxialCoord { - pub q: i32, - pub r: i32, -} - -impl Tile { - pub fn visit(&mut self) { - self.visited = true - } -} - -impl AxialCoord { - pub fn new(q: i32, r: i32) -> Self { - Self { q, r } - } -} diff --git a/src/lib.rs b/src/lib.rs index 2edc68d..c0ad671 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ mod demo; #[cfg(feature = "dev")] mod dev_tools; #[cfg(not(feature = "demo"))] -mod hexgrid; +mod maze; mod screens; mod theme; @@ -63,7 +63,7 @@ impl Plugin for AppPlugin { #[cfg(feature = "demo")] demo::plugin, #[cfg(not(feature = "demo"))] - hexgrid::HexGrid, + maze::MazePlugin, screens::plugin, theme::plugin, )); diff --git a/src/maze/grid.rs b/src/maze/grid.rs new file mode 100644 index 0000000..9247675 --- /dev/null +++ b/src/maze/grid.rs @@ -0,0 +1,218 @@ +use std::usize; + +use bevy::{ + color::palettes::css::{BLACK, GREEN, RED}, + prelude::*, + utils::hashbrown::HashMap, +}; + +use bevy_prototype_lyon::{ + draw::{Fill, Stroke}, + entity::ShapeBundle, + path::PathBuilder, + plugin::ShapePlugin, +}; +use hexx::{EdgeDirection, Hex}; +use log::info; +use rand::{prelude::SliceRandom, rngs::ThreadRng, thread_rng}; + +use super::{ + resource::{Layout, MazeConfig}, + tile::{Tile, TileBundle, Walls}, +}; + +pub(super) fn plugin(app: &mut App) { + app.add_plugins(ShapePlugin); + app.init_resource::(); + app.init_resource::(); +} + +pub(super) fn spawn_hex_grid(mut commands: Commands, config: Res) { + let radius = config.radius as i32; + + for q in -radius..=radius { + let r1 = (-radius).max(-q - radius); + let r2 = radius.min(-q + radius); + for r in r1..=r2 { + let tile = Tile::new(q, r); + commands.spawn(( + Name::new(format!("Tile {}", &tile.to_string())), + TileBundle { + hex: tile, + ..default() + }, + )); + } + } +} + +pub(super) fn generate_maze( + mut commands: Commands, + query: Query<(Entity, &Tile, &Walls)>, + config: Res, +) { + let mut tiles = query + .into_iter() + .map(|(entity, tile, walls)| (tile.hex, (entity, tile.clone(), walls.clone()))) + .collect(); + + let mut rng = thread_rng(); + recursive_maze(&mut tiles, config.start_pos, &mut rng); + + for (entity, tile, walls) in tiles.values() { + commands + .entity(*entity) + .insert(tile.clone()) + .insert(walls.clone()); + } +} + +fn recursive_maze( + tiles: &mut HashMap, + current_hex: Hex, + rng: &mut ThreadRng, +) { + { + let (_, tile, _) = tiles.get_mut(¤t_hex).unwrap(); + tile.visit(); + } + + let mut directions = EdgeDirection::ALL_DIRECTIONS; + directions.shuffle(rng); + + for direction in directions.into_iter() { + let neighbor_hex = current_hex + direction; + if let Some((_, neighbor_tile, _)) = tiles.get(&neighbor_hex) { + if !neighbor_tile.visited { + remove_wall_between(tiles, current_hex, neighbor_hex, direction); + recursive_maze(tiles, neighbor_hex, rng); + } + } + } +} + +fn remove_wall_between( + tiles: &mut HashMap, + current_hex: Hex, + neighbor_hex: Hex, + direction: EdgeDirection, +) { + { + let (_, _, walls) = tiles.get_mut(¤t_hex).unwrap(); + walls.0[direction.index() as usize] = false; + } + { + let (_, _, walls) = tiles.get_mut(&neighbor_hex).unwrap(); + walls.0[direction.const_neg().index() as usize] = false; + } +} + +fn add_hex_tile( + commands: &mut Commands, + position: Vec2, + size: f32, + tile: &Tile, + walls: &Walls, + fill_color: Color, + layout: &Layout, +) { + let hex_points = tile + .hex + .all_vertices() + .into_iter() + .map(|v| { + let mut layout = layout.clone(); + layout.origin = position; + layout.hex_size = Vec2::splat(size); + layout.hex_to_world_pos(v.origin + v.direction) + }) + .collect::>(); + + let mut path_builder = PathBuilder::new(); + path_builder.move_to(hex_points[0]); + for point in &hex_points[1..] { + path_builder.line_to(*point); + } + path_builder.close(); + let hexagon = path_builder.build(); + + // Create the hexagon fill + commands.spawn(( + ShapeBundle { + path: hexagon, + spatial: SpatialBundle { + transform: Transform::from_xyz(position.x, position.y, 0.), + ..default() + }, + ..default() + }, + Fill::color(fill_color), + )); + // .with_children(|p| { + // p.spawn(Text2dBundle { + // text: Text { + // sections: vec![TextSection { + // value: tile.to_string(), + // style: TextStyle { + // font_size: 16., + // color: Color::BLACK, + // ..default() + // }, + // }], + // ..default() + // }, + // transform: Transform::from_xyz(position.x * 2., position.y * 2., 1.), + // ..default() + // }); + // }); + + // Draw walls + for direction in EdgeDirection::iter() { + let idx = direction.index() as usize; + if walls[idx] { + let start = hex_points[idx]; + let end = hex_points[(idx + 1) % 6]; + let mut line_builder = PathBuilder::new(); + line_builder.move_to(start); + line_builder.line_to(end); + let line = line_builder.build(); + + commands.spawn(( + ShapeBundle { + path: line, + spatial: SpatialBundle { + transform: Transform::from_xyz(position.x, position.y, 1.), + ..default() + }, + ..default() + }, + Stroke::new(BLACK, 2.), + )); + } + } +} + +pub(super) fn render_maze( + mut commands: Commands, + query: Query<(&Tile, &mut Walls)>, + layout: Res, + config: Res, +) { + for (tile, walls) in query.iter() { + let world_pos = layout.hex_to_world_pos(tile.hex); + let fill_color = match tile.hex { + pos if pos == config.start_pos => GREEN.into(), + pos if pos == config.end_pos => RED.into(), + _ => Color::srgb(0.8, 0.8, 0.8), + }; + add_hex_tile( + &mut commands, + world_pos, + config.size, + tile, + walls, + fill_color, + &layout, + ); + } +} diff --git a/src/maze/mod.rs b/src/maze/mod.rs new file mode 100644 index 0000000..8a347d3 --- /dev/null +++ b/src/maze/mod.rs @@ -0,0 +1,28 @@ +use bevy::{ + ecs::{system::RunSystemOnce, world::Command}, + prelude::*, +}; +use grid::{generate_maze, plugin, render_maze, spawn_hex_grid}; +pub mod grid; +pub mod resource; +pub mod tile; + +pub struct MazePlugin; + +impl Plugin for MazePlugin { + fn build(&self, app: &mut App) { + app.add_plugins(plugin); + } +} + +impl Command for MazePlugin { + fn apply(self, world: &mut World) { + world.run_system_once(spawn_hex_grid); + world.run_system_once(generate_maze); + world.run_system_once(render_maze); + } +} + +pub fn spawn_grid(world: &mut World) { + MazePlugin.apply(world); +} diff --git a/src/maze/resource.rs b/src/maze/resource.rs new file mode 100644 index 0000000..7ecb034 --- /dev/null +++ b/src/maze/resource.rs @@ -0,0 +1,53 @@ +use bevy::prelude::*; +use hexx::{Hex, HexLayout, HexOrientation}; +use rand::{thread_rng, Rng}; + +#[derive(Debug, Reflect, Resource)] +#[reflect(Resource)] +pub struct MazeConfig { + pub radius: u32, + pub size: f32, + pub start_pos: Hex, + pub end_pos: Hex, +} + +impl Default for MazeConfig { + fn default() -> Self { + let mut rng = thread_rng(); + let radius = 5; + let start_pos = Hex::new( + rng.gen_range(-radius..radius), + rng.gen_range(-radius..radius), + ); + let end_pos = Hex::new( + rng.gen_range(-radius..radius), + rng.gen_range(-radius..radius), + ); + debug!("Start pos: ({},{})", start_pos.x, start_pos.y); + debug!("End pos: ({},{})", end_pos.x, end_pos.y); + Self { + radius: radius as u32, + size: 10., + start_pos, + end_pos, + } + } +} + +#[derive(Debug, Reflect, Resource, Deref, DerefMut, Clone)] +#[reflect(Resource)] +pub struct Layout(pub HexLayout); + +impl FromWorld for Layout { + fn from_world(world: &mut World) -> Self { + let size = world + .get_resource::() + .unwrap_or(&MazeConfig::default()) + .size; + Self(HexLayout { + orientation: HexOrientation::Pointy, + hex_size: Vec2::splat(size), + ..default() + }) + } +} diff --git a/src/maze/tile.rs b/src/maze/tile.rs new file mode 100644 index 0000000..bda8302 --- /dev/null +++ b/src/maze/tile.rs @@ -0,0 +1,46 @@ +use std::fmt::Display; + +use bevy::prelude::*; +use hexx::Hex; + +#[derive(Debug, Reflect, Component, Default, PartialEq, Eq, Hash, Clone)] +#[reflect(Component)] +pub struct Tile { + pub hex: Hex, + pub visited: bool, +} + +#[derive(Debug, Reflect, Component, Deref, DerefMut, Clone)] +#[reflect(Component)] +pub struct Walls(pub [bool; 6]); + +#[derive(Debug, Reflect, Bundle, Default)] +pub struct TileBundle { + pub hex: Tile, + pub walls: Walls, +} + +impl Tile { + pub fn new(q: i32, r: i32) -> Self { + Self { + hex: Hex::new(q, r), + visited: false, + } + } + + pub fn visit(&mut self) { + self.visited = true; + } +} + +impl Display for Tile { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "({},{})", self.hex.x, self.hex.y) + } +} + +impl Default for Walls { + fn default() -> Self { + Self([true; 6]) + } +} diff --git a/src/screens/gameplay.rs b/src/screens/gameplay.rs index 638ec9d..f932d17 100644 --- a/src/screens/gameplay.rs +++ b/src/screens/gameplay.rs @@ -5,7 +5,7 @@ use bevy::{input::common_conditions::input_just_pressed, prelude::*}; #[cfg(feature = "demo")] use crate::demo::level::spawn_level as spawn_level_command; #[cfg(not(feature = "demo"))] -use crate::hexgrid::spawn_grid as spawn_level_command; +use crate::maze::spawn_grid as spawn_level_command; use crate::{asset_tracking::LoadResource, audio::Music, screens::Screen}; pub(super) fn plugin(app: &mut App) {