diff --git a/Cargo.lock b/Cargo.lock index 3dbaade..aa2a063 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2137,6 +2137,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "hexx" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c40cfb11c06c0b7051c2c0df030c57c65921db962ee2b8e89de218bb749f173" +dependencies = [ + "bevy_reflect", + "glam", + "serde", +] + [[package]] name = "image" version = "0.25.2" @@ -3600,6 +3611,7 @@ name = "the-labyrinth-of-echoes" version = "0.0.3" dependencies = [ "bevy", + "hexx", "log", "rand", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 92fd13f..322c41b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ tracing = { version = "0.1", features = [ "max_level_debug", "release_max_level_warn", ] } +hexx = { version = "0.18", features = ["bevy_reflect"] } [features] default = [ @@ -35,6 +36,7 @@ dev_native = [ # Enable embedded asset hot reloading for native dev builds. "bevy/embedded_watcher", ] +demo = [] # Idiomatic Bevy code often triggers these lints, and the CI workflow treats them as errors. diff --git a/src/lib.rs b/src/lib.rs index 8da505d..f673718 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,13 @@ mod asset_tracking; pub mod audio; +#[cfg(feature = "demo")] mod demo; #[cfg(feature = "dev")] mod dev_tools; mod screens; mod theme; +#[cfg(not(feature = "demo"))] +mod tiles; use bevy::{ asset::AssetMetaCheck, @@ -57,7 +60,10 @@ impl Plugin for AppPlugin { // Add other plugins. app.add_plugins(( asset_tracking::plugin, + #[cfg(feature = "demo")] demo::plugin, + #[cfg(not(feature = "demo"))] + tiles::plugin, screens::plugin, theme::plugin, )); diff --git a/src/screens/gameplay.rs b/src/screens/gameplay.rs index 0640462..da60175 100644 --- a/src/screens/gameplay.rs +++ b/src/screens/gameplay.rs @@ -2,10 +2,11 @@ use bevy::{input::common_conditions::input_just_pressed, prelude::*}; -use crate::{ - asset_tracking::LoadResource, audio::Music, demo::level::spawn_level as spawn_level_command, - screens::Screen, -}; +#[cfg(feature = "demo")] +use crate::demo::level::spawn_level as spawn_level_command; +#[cfg(not(feature = "demo"))] +use crate::tiles::level::spawn_level as spawn_level_command; +use crate::{asset_tracking::LoadResource, audio::Music, screens::Screen}; pub(super) fn plugin(app: &mut App) { app.add_systems(OnEnter(Screen::Gameplay), spawn_level); diff --git a/src/screens/loading.rs b/src/screens/loading.rs index e6eb5ca..1f2b3aa 100644 --- a/src/screens/loading.rs +++ b/src/screens/loading.rs @@ -4,7 +4,6 @@ use bevy::prelude::*; use crate::{ - demo::player::PlayerAssets, screens::{credits::CreditsMusic, gameplay::GameplayMusic, Screen}, theme::{interaction::InteractionAssets, prelude::*}, }; @@ -35,13 +34,9 @@ fn continue_to_title_screen(mut next_screen: ResMut>) { } fn all_assets_loaded( - player_assets: Option>, interaction_assets: Option>, credits_music: Option>, gameplay_music: Option>, ) -> bool { - player_assets.is_some() - && interaction_assets.is_some() - && credits_music.is_some() - && gameplay_music.is_some() + interaction_assets.is_some() && credits_music.is_some() && gameplay_music.is_some() } diff --git a/src/tiles/level.rs b/src/tiles/level.rs new file mode 100644 index 0000000..2290946 --- /dev/null +++ b/src/tiles/level.rs @@ -0,0 +1,20 @@ +//! Spawn the main level. + +use bevy::{ecs::world::Command, prelude::*}; + +use crate::tiles::player::SpawnPlayer; + +use super::tile::{self, GridSettings, SpawnGrid}; + +pub(super) fn plugin(app: &mut App) { + app.insert_resource(GridSettings::default()); + app.add_plugins(tile::plugin); +} + +/// A [`Command`] to spawn the level. +/// Functions that accept only `&mut World` as their parameter implement [`Command`]. +/// We use this style when a command requires no configuration. +pub fn spawn_level(world: &mut World) { + SpawnGrid.apply(world); + SpawnPlayer.apply(world); +} diff --git a/src/tiles/mod.rs b/src/tiles/mod.rs new file mode 100644 index 0000000..841f267 --- /dev/null +++ b/src/tiles/mod.rs @@ -0,0 +1,8 @@ +use bevy::prelude::*; +pub mod level; +pub mod player; +pub mod tile; + +pub(super) fn plugin(app: &mut App) { + app.add_plugins((player::plugin, level::plugin)); +} diff --git a/src/tiles/player.rs b/src/tiles/player.rs new file mode 100644 index 0000000..db13397 --- /dev/null +++ b/src/tiles/player.rs @@ -0,0 +1,112 @@ +use bevy::{ + color::palettes::css::BLUE, + ecs::{system::RunSystemOnce, world::Command}, + prelude::*, +}; +use hexx::{Hex, HexLayout, HexOrientation}; + +use crate::screens::Screen; + +use super::tile::{GridSettings, HexDirection, Tile}; + +pub(super) fn plugin(app: &mut App) { + app.register_type::(); + // app.add_systems(Update, move_player); +} + +#[derive(Debug)] +pub struct SpawnPlayer; + +impl Command for SpawnPlayer { + fn apply(self, world: &mut World) { + world.run_system_once(spawn_player); + } +} + +#[derive(Debug, Reflect, Component)] +#[reflect(Component)] +pub struct Player { + position: Hex, +} + +fn spawn_player(mut commands: Commands, grid_settings: Res) { + let starting_hex = Hex::ZERO; + let layout = HexLayout { + orientation: HexOrientation::Pointy, + origin: Vec2::ZERO, + hex_size: grid_settings.hex_size, + ..default() + }; + + let world_pos = layout.hex_to_world_pos(starting_hex); + + commands.spawn(( + Name::new("Player"), + SpriteBundle { + sprite: Sprite { + color: BLUE.into(), + custom_size: Some(grid_settings.hex_size * 0.8), + ..default() + }, + transform: Transform::from_translation(world_pos.extend(1.)), + ..default() + }, + Player { + position: starting_hex, + }, + StateScoped(Screen::Gameplay), + )); +} + +fn move_player( + input: Res>, + grid_settings: Res, + mut player_query: Query<&mut Player>, + tile_query: Query<&Tile>, +) { + let mut player = player_query.single_mut(); + + if let Some(direction) = get_move_direction(&input) { + let current_tile = tile_query + .iter() + .find(|tile| tile.position == player.position) + .unwrap(); + if current_tile.has_wall(&direction) { + return; + } + + let hexx_direction = direction.to_hexx_direction(); + player.position = player.position + hexx_direction; + + let layout = HexLayout { + orientation: HexOrientation::Pointy, + origin: Vec2::ZERO, + hex_size: grid_settings.hex_size, + ..default() + }; + + let world_pos = layout.hex_to_world_pos(player.position); + } +} + +fn get_move_direction(input: &Res>) -> Option { + if input.just_pressed(KeyCode::KeyW) { + return Some(HexDirection::Top); + } + if input.just_pressed(KeyCode::KeyE) { + return Some(HexDirection::TopRight); + } + if input.just_pressed(KeyCode::KeyD) { + return Some(HexDirection::BottomRight); + } + if input.just_pressed(KeyCode::KeyS) { + return Some(HexDirection::Bottom); + } + if input.just_pressed(KeyCode::KeyA) { + return Some(HexDirection::BottomLeft); + } + if input.just_pressed(KeyCode::KeyQ) { + return Some(HexDirection::TopLeft); + } + None +} diff --git a/src/tiles/tile.rs b/src/tiles/tile.rs new file mode 100644 index 0000000..98b067d --- /dev/null +++ b/src/tiles/tile.rs @@ -0,0 +1,183 @@ +use bevy::{ + color::palettes::css::GRAY, + ecs::{system::RunSystemOnce, world::Command}, + prelude::*, + utils::hashbrown::{HashMap, HashSet}, +}; +use hexx::{EdgeDirection, Hex, HexLayout, HexOrientation}; +use rand::{seq::IteratorRandom, thread_rng}; + +pub(super) fn plugin(app: &mut App) { + app.add_systems(Startup, generate_maze); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)] +pub enum HexDirection { + Top, + TopRight, + BottomRight, + Bottom, + BottomLeft, + TopLeft, +} + +#[derive(Debug)] +pub struct SpawnGrid; + +impl Command for SpawnGrid { + fn apply(self, world: &mut World) { + world.run_system_once(setup_hex_grid); + } +} + +impl HexDirection { + pub fn to_hexx_direction(self) -> EdgeDirection { + self.into() + } + + pub const ALL: [HexDirection; 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, + } + } +} + +impl From for EdgeDirection { + fn from(value: HexDirection) -> Self { + match value { + HexDirection::Top => Self::FLAT_NORTH, + HexDirection::TopRight => Self::FLAT_NORTH_EAST, + HexDirection::BottomRight => Self::FLAT_SOUTH_EAST, + HexDirection::Bottom => Self::FLAT_SOUTH, + HexDirection::BottomLeft => Self::FLAT_SOUTH_WEST, + HexDirection::TopLeft => Self::FLAT_NORTH_WEST, + } + } +} + +#[derive(Debug, Reflect, Resource)] +#[reflect(Resource)] +pub struct GridSettings { + pub radius: u32, + pub hex_size: Vec2, +} + +#[derive(Debug, Clone, Reflect, Component)] +#[reflect(Component)] +pub struct Tile { + pub position: Hex, + pub walls: HashMap, +} + +impl Tile { + pub fn new(position: Hex) -> Self { + let mut walls = HashMap::new(); + for direction in HexDirection::ALL { + walls.insert(direction, true); + } + Self { position, walls } + } + + pub fn has_wall(&self, direction: &HexDirection) -> bool { + *self.walls.get(direction).unwrap_or(&false) + } + + pub fn remove_wall(&mut self, direction: HexDirection) { + self.walls.insert(direction, false); + } +} + +impl Default for GridSettings { + fn default() -> Self { + Self { + radius: 5, + hex_size: Vec2::splat(32.), + } + } +} + +pub fn setup_hex_grid(mut commands: Commands, grid_settings: Res) { + let GridSettings { radius, hex_size } = *grid_settings; + let layout = HexLayout { + orientation: HexOrientation::Pointy, + origin: Vec2::ZERO, + hex_size, + ..default() + }; + + let hexes = Hex::ZERO.range(radius); + + for hex in hexes { + let world_pos = layout.hex_to_world_pos(hex); + commands.spawn(( + SpriteBundle { + sprite: Sprite { + color: GRAY.into(), + custom_size: Some(hex_size), + ..default() + }, + transform: Transform::from_translation(world_pos.extend(0.)), + ..default() + }, + Tile::new(hex), + )); + } +} + +pub fn generate_maze(mut tile_query: Query<&mut Tile>, grid_settings: Res) { + let radius = grid_settings.radius; + let mut tiles = tile_query + .iter_mut() + .map(|tile| (tile.position, tile.clone())) + .collect::>(); + + let mut rng = thread_rng(); + let mut visited = HashSet::new(); + let mut stack = Vec::new(); + + let start_hex = Hex::ZERO; + visited.insert(start_hex); + stack.push(start_hex); + + while let Some(current_hex) = stack.pop() { + let mut unvisited_neighbors = Vec::new(); + for direction in HexDirection::ALL { + let neighbor_hex = current_hex + direction.to_hexx_direction(); + if neighbor_hex.distance_to(Hex::ZERO) > radius as i32 { + continue; + } + if !visited.contains(&neighbor_hex) { + unvisited_neighbors.push((neighbor_hex, direction)); + } + } + if !unvisited_neighbors.is_empty() { + stack.push(current_hex); + let &(neighbor_hex, direction) = unvisited_neighbors.iter().choose(&mut rng).unwrap(); + + if let Some(current_tile) = tiles.get_mut(¤t_hex) { + current_tile.remove_wall(direction); + } + + if let Some(neighbor_tile) = tiles.get_mut(&neighbor_hex) { + neighbor_tile.remove_wall(direction.opposite()); + } + + visited.insert(neighbor_hex); + stack.push(neighbor_hex); + } + } +}