diff --git a/game_core/src/assets/startup.rs b/game_core/src/assets/startup.rs index e848c5fff0f0bd6adce82aada13ccc0799812bde..50b235cbebbe25b17a141fd8c5075dbffb027e64 100644 --- a/game_core/src/assets/startup.rs +++ b/game_core/src/assets/startup.rs @@ -11,11 +11,16 @@ pub fn start_preload_resources(mut commands: Commands) { commands.insert_resource(NextState(AppState::Setup)) } +/// All sheets have been adjusted to be 64 x 64 sprites +pub const SHEET_WIDTH: usize = 64; +/// All sheets have been adjusted to be 64 x 64 sprites +pub const SHEET_HEIGHT: usize = 64; + pub fn start_load_resources(mut loader: AssetTypeLoader) { loader.load_images(&[("splash.png", "splash")]); loader.load_audio(&[("splash_sting.mp3", "splash_sting")]); - let sheet_config = SpriteSheetConfig::squares(16, 64, 64); + let sheet_config = SpriteSheetConfig::squares(16, SHEET_WIDTH, SHEET_HEIGHT); loader.load_spritesheet( &sheet_config, &[ diff --git a/game_core/src/control/mod.rs b/game_core/src/control/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..f28d7c205ea9a9b4c0a1c6bf1404f8a0f7421669 --- /dev/null +++ b/game_core/src/control/mod.rs @@ -0,0 +1 @@ +pub mod player; diff --git a/game_core/src/debug.rs b/game_core/src/debug.rs index 0b87c5f02f21ca9524bccaf402d200c86a5b2291..928add3d7a5eacb15566f4c082ac034ae662e8cb 100644 --- a/game_core/src/debug.rs +++ b/game_core/src/debug.rs @@ -5,10 +5,15 @@ use iyes_loopless::state::NextState; use crate::entities::spawner::EntitySpawner; use crate::system::flow::AppState; +use crate::world::generation::generate_map; +use crate::world::level_map::LevelMapBundle; pub fn spawn_player(mut spawner: EntitySpawner) { log::info!("Spawning player"); - spawner.spawn_player(uvec2(2, 2)); + spawner.spawn_player(uvec2(25, 25)); + spawner + .commands + .spawn_bundle(LevelMapBundle::generate(50, 50)); } pub fn skip_menu(mut commands: Commands) { diff --git a/game_core/src/entities/lifecycle.rs b/game_core/src/entities/lifecycle.rs index 4d105b24723296807d15e7946b9c19f9827031e9..8007a1dbfe41240fc35766f76eacb0646b92bf90 100644 --- a/game_core/src/entities/lifecycle.rs +++ b/game_core/src/entities/lifecycle.rs @@ -1,4 +1,7 @@ use bevy::prelude::*; +use iyes_loopless::prelude::AppLooplessStateExt; + +use crate::system::flow::AppState; #[derive(Debug, Clone, Copy, Component)] pub struct GameEntity; @@ -8,3 +11,10 @@ pub fn remove_game_entities(mut commands: Commands, query: Query<Entity, With<Ga commands.entity(entity).despawn_recursive(); } } + +pub struct EntityLifecyclePlugin; +impl Plugin for EntityLifecyclePlugin { + fn build(&self, app: &mut App) { + app.add_exit_system(AppState::InGame, remove_game_entities); + } +} diff --git a/game_core/src/entities/spawner.rs b/game_core/src/entities/spawner.rs index ab539eaeff6c77f6fa5ec52ab0a03095d35b4cde..7d78cbd43f5cae102ad802866f953aa051128e1d 100644 --- a/game_core/src/entities/spawner.rs +++ b/game_core/src/entities/spawner.rs @@ -5,6 +5,7 @@ use crate::assets::AssetHandles; use crate::entities::lifecycle::GameEntity; use crate::system::camera::ChaseCam; use crate::system::graphics::LAYER_CREATURE; +use crate::world::level_map::GridPosition; const PLAYER_SPRITE: usize = 2; @@ -18,7 +19,10 @@ impl<'w, 's> EntitySpawner<'w, 's> { pub fn spawn_player(&mut self, grid_position: UVec2) { let mut entity = self.commands.spawn(); - entity.insert(ChaseCam).insert(GameEntity); + entity + .insert(ChaseCam) + .insert(GameEntity) + .insert(GridPosition(grid_position)); entity.insert_bundle(SpriteSheetBundle { texture_atlas: self.handles.atlas("creatures"), diff --git a/game_core/src/lib.rs b/game_core/src/lib.rs index aff889092a604f6530bdc59a3d05060465c8e162..9bf52a28ce185a0c746a0e95195eb59eeffd45ba 100644 --- a/game_core/src/lib.rs +++ b/game_core/src/lib.rs @@ -1,4 +1,5 @@ pub mod assets; +pub mod control; pub mod debug; pub mod entities; pub mod multiplayer; diff --git a/game_core/src/main.rs b/game_core/src/main.rs index 879c0037d6db3da1d399500c0de73ac2016df47f..bbe51c5948b1f5cb9158439cb9fecbce79d7c115 100644 --- a/game_core/src/main.rs +++ b/game_core/src/main.rs @@ -20,5 +20,6 @@ fn main() { game_core::multiplayer::IncomingEvent, >::new()) .add_plugin(game_core::debug::DebugPlugin) + .add_plugin(game_core::world::WorldPlugin) .run(); } diff --git a/game_core/src/world/adjacency.rs b/game_core/src/world/adjacency.rs new file mode 100644 index 0000000000000000000000000000000000000000..f663ac7e25c20198f03a4f8a1693e6e72254f2a9 --- /dev/null +++ b/game_core/src/world/adjacency.rs @@ -0,0 +1,57 @@ +pub const NONE: u8 = 0b0000; + +pub const TOP: u8 = 0b0001; +pub const BOTTOM: u8 = 0b0010; +pub const LEFT: u8 = 0b0100; +pub const RIGHT: u8 = 0b1000; + +pub const BOTTOM_RIGHT: u8 = BOTTOM | RIGHT; +pub const BOTTOM_LEFT_RIGHT: u8 = BOTTOM | LEFT | RIGHT; +pub const BOTTOM_LEFT: u8 = BOTTOM | LEFT; +pub const TOP_BOTTOM_RIGHT: u8 = TOP | BOTTOM | RIGHT; +pub const TOP_BOTTOM_LEFT_RIGHT: u8 = TOP | BOTTOM | LEFT | RIGHT; +pub const TOP_BOTTOM_LEFT: u8 = TOP | BOTTOM | LEFT; +pub const TOP_RIGHT: u8 = TOP | RIGHT; +pub const TOP_LEFT_RIGHT: u8 = TOP | LEFT | RIGHT; +pub const TOP_LEFT: u8 = TOP | LEFT; +pub const TOP_BOTTOM: u8 = TOP | BOTTOM; +pub const LEFT_RIGHT: u8 = LEFT | RIGHT; + +pub fn get_floor_sprite_offset(adjacency: u8) -> usize { + match adjacency { + BOTTOM_RIGHT => 0, + BOTTOM_LEFT_RIGHT => 1, + BOTTOM_LEFT => 2, + BOTTOM => 3, + TOP_BOTTOM_RIGHT => 64, + TOP_BOTTOM_LEFT_RIGHT => 64 + 1, + TOP_BOTTOM_LEFT => 64 + 2, + TOP_BOTTOM => 64 + 3, + RIGHT => 64 + 4, + LEFT_RIGHT => 64 + 5, + LEFT => 64 + 6, + TOP_RIGHT => 128, + TOP_LEFT_RIGHT => 128 + 1, + TOP_LEFT => 128 + 2, + TOP => 128 + 3, + NONE | _ => 5, + } +} + +pub fn get_wall_sprite_offset(adjacency: u8) -> usize { + match adjacency { + BOTTOM_RIGHT => 0, + LEFT_RIGHT => 1, + BOTTOM_LEFT => 2, + BOTTOM_LEFT_RIGHT => 4, + TOP_BOTTOM | BOTTOM => 64, + NONE | TOP => 64 + 1, + TOP_BOTTOM_RIGHT => 64 + 3, + TOP_BOTTOM_LEFT_RIGHT => 64 + 4, + TOP_BOTTOM_LEFT => 64 + 5, + TOP_RIGHT | RIGHT => 128, + TOP_LEFT | LEFT => 128 + 2, + TOP_LEFT_RIGHT => 128 + 4, + _ => 3, + } +} diff --git a/game_core/src/world/generation.rs b/game_core/src/world/generation.rs new file mode 100644 index 0000000000000000000000000000000000000000..4f39d6b74238c9d647ebc1c9554132504f24445b --- /dev/null +++ b/game_core/src/world/generation.rs @@ -0,0 +1,262 @@ +use fastrand::Rng; + +use crate::world::level_map::{Indexer, LevelMap, MapLayer, MapTile}; + +const TMP_FLOOR_GROUP: usize = 6 * 64; +const TMP_WALL_GROUP: usize = 3 * 64 + 28; + +#[derive(Copy, Clone, Debug, Default)] +enum GenerationDirection { + Top, + Bottom, + Left, + Right, + #[default] + None, +} + +pub fn generate_map(width: usize, height: usize) -> LevelMap { + generate_map_with_seed(fastrand::u64(0..u64::MAX), width, height) +} + +pub fn generate_map_with_seed(seed: u64, width: usize, height: usize) -> LevelMap { + let rng = fastrand::Rng::with_seed(seed); + generate_map_with_rng(&rng, width, height) +} + +pub fn generate_map_with_rng(rng: &Rng, width: usize, height: usize) -> LevelMap { + let indexer = Indexer::new(width, height); + + let mut floor_layer = vec![0; width * height]; + let mut wall_layer = vec![0; width * height]; + let mut decoration_layer = vec![0; width * height]; + + let initial_x = width / 2; + let initial_y = width / 2; + let initial_width = rng.usize(3..=5); + let initial_height = rng.usize(3..=5); + + draw_box( + initial_x - initial_width / 2, + initial_y - initial_height / 2, + initial_width, + initial_height, + TMP_FLOOR_GROUP, + &mut floor_layer, + &indexer, + ); + + for _ in 0..1000 { + let initial = find_next_door(1000, &floor_layer, &rng, &indexer); + if let Some((next_x, next_y, direction)) = initial { + let size_selection = rng.usize(1..=10); + let (room_width, room_height) = if size_selection < 8 { + (rng.usize(3..=6), rng.usize(3..=6)) + } else { + (rng.usize(7..=12), rng.usize(7..=12)) + }; + + // Align room drawing to top left corner of new box + match direction { + GenerationDirection::Top => { + let start_x = next_x - (room_width / 4); + let start_y = next_y + room_height; + draw_box( + start_x, + start_y, + room_width, + room_height, + TMP_FLOOR_GROUP, + &mut floor_layer, + &indexer, + ); + } + GenerationDirection::Bottom => { + let start_x = next_x - (room_width / 4); + let start_y = next_y; + draw_box( + start_x, + start_y, + room_width, + room_height, + TMP_FLOOR_GROUP, + &mut floor_layer, + &indexer, + ); + } + GenerationDirection::Left => { + let start_x = next_x - room_width; + let start_y = next_y; + draw_box( + start_x, + start_y, + room_width, + room_height, + TMP_FLOOR_GROUP, + &mut floor_layer, + &indexer, + ); + } + GenerationDirection::Right => { + let start_x = next_x; + let start_y = next_y; + draw_box( + start_x, + start_y, + room_width, + room_height, + TMP_FLOOR_GROUP, + &mut floor_layer, + &indexer, + ); + } + GenerationDirection::None => {} + } + } + } + + LevelMap { + width, + height, + layers: vec![ + MapLayer::from_sized_list( + width, + height, + floor_layer + .into_iter() + .map(|tile| match tile { + 0 => None, + rest => Some(MapTile::new_floor(rest)), + }) + .collect(), + ), + MapLayer::from_sized_list( + width, + height, + wall_layer + .into_iter() + .map(|tile| match tile { + 0 => None, + rest => Some(MapTile::new_wall(rest)), + }) + .collect(), + ), + MapLayer::from_sized_list( + width, + height, + decoration_layer + .into_iter() + .map(|tile| match tile { + 0 => None, + rest => Some(MapTile::new_obstacle(rest)), + }) + .collect(), + ), + ], + } +} + +fn find_next_door( + max_attempts: usize, + floor: &Vec<usize>, + rng: &Rng, + idx: &Indexer, +) -> Option<(usize, usize, GenerationDirection)> { + let mut remaining = max_attempts; + let mut found = None; + let mut last_direction = GenerationDirection::None; + + while remaining > 0 && found.is_none() { + remaining = remaining.saturating_sub(1); + + let target_index = rng.usize(0..floor.len()); + let (x, y) = idx.reverse(target_index); + + // Edges of the map are unsuitable for new rooms + if x == 0 || x == idx.width() - 1 || y == 0 || y == idx.height() - 1 { + continue; + } + + let mut adjacents = 0; + + // Check Left + if floor[idx.index(x - 1, y)] != 0 { + adjacents += 1; + last_direction = GenerationDirection::Right; + } + // Check Right + if floor[idx.index(x + 1, y)] != 0 { + adjacents += 1; + last_direction = GenerationDirection::Left; + } + // Check Top + if floor[idx.index(x, y + 1)] != 0 { + adjacents += 1; + last_direction = GenerationDirection::Bottom; + } + // Check Bottom + if floor[idx.index(x, y - 1)] != 0 { + adjacents += 1; + last_direction = GenerationDirection::Top; + } + + if adjacents == 1 { + found = Some((x, y, last_direction)); + break; + } + } + + found +} + +fn draw_box( + start_x: usize, + start_y: usize, + width: usize, + height: usize, + value: usize, + layer: &mut Vec<usize>, + indexer: &Indexer, +) { + if !validate_box(start_x, start_y, width, height, layer, indexer) { + return; + } + + for x in start_x..(start_x + width) { + for y in start_y..(start_y + height) { + let idx = indexer.index(x, y); + layer[idx] = value; + } + } +} + +fn validate_box( + start_x: usize, + start_y: usize, + width: usize, + height: usize, + layer: &Vec<usize>, + indexer: &Indexer, +) -> bool { + let map_width = indexer.width(); + let map_height = indexer.height(); + + if start_y + height >= map_height { + return false; + } + + if start_x + width >= map_width { + return false; + } + + for x in start_x..(start_x + width) { + for y in start_y..(start_y + height) { + let idx = indexer.index(x, y); + if layer[idx] != 0 { + return false; + } + } + } + + true +} diff --git a/game_core/src/world/handlers.rs b/game_core/src/world/handlers.rs new file mode 100644 index 0000000000000000000000000000000000000000..14d0d582c86cd923e3f635a2b3b49c6685623149 --- /dev/null +++ b/game_core/src/world/handlers.rs @@ -0,0 +1,45 @@ +use bevy::math::uvec2; +use bevy::prelude::*; + +use crate::assets::AssetHandles; +use crate::entities::lifecycle::GameEntity; +use crate::system::graphics::LAYER_TILE; +use crate::world::adjacency::get_floor_sprite_offset; +use crate::world::level_map::{GridPosition, Indexer, LevelMap, Tile, WORLD_TILE_SIZE}; + +pub fn spawn_new_world( + mut commands: Commands, + assets: Res<AssetHandles>, + query: Query<(Entity, &LevelMap), Added<LevelMap>>, +) { + for (entity, map) in &query { + let mut cmds = commands.entity(entity); + for (idx, layer) in map.layers.iter().enumerate() { + cmds.with_children(|layer_cmds| { + let indexer = Indexer::new(layer.width, layer.height); + for (idx, tile) in layer.tiles.iter().enumerate() { + if let Some(data) = tile { + let (x, y) = indexer.reverse(idx); + layer_cmds + .spawn_bundle(SpriteSheetBundle { + texture_atlas: assets.atlas("environs"), + sprite: TextureAtlasSprite::new( + data.tile_group + + get_floor_sprite_offset(layer.get_tile_adjacency(x, y)), + ), + transform: Transform::from_xyz( + x as f32 * WORLD_TILE_SIZE, + y as f32 * WORLD_TILE_SIZE, + LAYER_TILE + (idx as f32 / 100.0), + ), + ..Default::default() + }) + .insert(GameEntity) + .insert(Tile) + .insert(GridPosition(uvec2(x as u32, y as u32))); + } + } + }); + } + } +} diff --git a/game_core/src/world/level_map.rs b/game_core/src/world/level_map.rs index b6e2ed7be7c06a9cd0cddae1967c7097f11f93ba..b674c1dd3496a4f722f7622256dda706a9259f8d 100644 --- a/game_core/src/world/level_map.rs +++ b/game_core/src/world/level_map.rs @@ -1,10 +1,19 @@ +use std::fmt::{Debug, Formatter}; use std::ops::Deref; use bevy::math::UVec2; use bevy::prelude::*; +use fastrand::Rng; + +use crate::world::adjacency::{BOTTOM, LEFT, NONE, RIGHT, TOP}; +use crate::world::generation::{generate_map, generate_map_with_rng, generate_map_with_seed}; pub const WORLD_TILE_SIZE: f32 = 16.0; +/// Marker component for any spawned tile entity +#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Component)] +pub struct Tile; + /// Track the location of an entity within a grid #[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Component)] #[repr(transparent)] @@ -24,3 +33,171 @@ pub fn sync_grid_to_transform(mut query: Query<(&GridPosition, &mut Transform)>) .extend(transform.translation.z); } } + +/// Store accessibility of a tile and the sprite group that the tile belongs to. +/// Used for pathfinding, and for selecting which sprites should be considered as +/// a group +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub struct MapTile { + pub can_walk: bool, + pub can_fly: bool, + pub can_see: bool, + pub tile_group: usize, +} + +impl MapTile { + /// Create a tile that can be passed by all entities + pub fn new_floor(tile_group: usize) -> MapTile { + MapTile { + can_walk: true, + can_fly: true, + can_see: true, + tile_group, + } + } + + /// Create a tile that can be passed by no entities + pub fn new_wall(tile_group: usize) -> MapTile { + MapTile { + can_walk: false, + can_fly: false, + can_see: false, + tile_group, + } + } + /// Create a tile that can be passed by flying entities + /// and seen through by any entity + pub fn new_obstacle(tile_group: usize) -> MapTile { + MapTile { + can_walk: false, + can_fly: true, + can_see: true, + tile_group, + } + } +} + +#[derive(Clone, Eq, PartialEq)] +pub struct MapLayer { + pub width: usize, + pub height: usize, + pub offset_horizontal: usize, + pub offset_vertical: usize, + pub tiles: Vec<Option<MapTile>>, +} + +impl MapLayer { + pub fn from_sized_list(width: usize, height: usize, tiles: Vec<Option<MapTile>>) -> Self { + MapLayer { + width, + height, + offset_horizontal: 0, + offset_vertical: 0, + tiles, + } + } + + pub fn get_tile_adjacency(&self, x: usize, y: usize) -> u8 { + let indexer = Indexer::new(self.width, self.height); + let mut adjacency = NONE; + + if x > 0 && self.tiles[indexer.index(x - 1, y)].is_some() { + adjacency = adjacency | LEFT; + } + + if x < self.width - 1 && self.tiles[indexer.index(x + 1, y)].is_some() { + adjacency = adjacency | RIGHT; + } + + if y > 0 && self.tiles[indexer.index(x, y - 1)].is_some() { + adjacency = adjacency | BOTTOM; + } + + if y < self.height - 1 && self.tiles[indexer.index(x, y + 1)].is_some() { + adjacency = adjacency | TOP; + } + + adjacency + } +} + +impl Debug for MapLayer { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "---\nMap Layer: {}x{}", self.width, self.height)?; + let idx = Indexer::new(self.width, self.height); + for y in (0..self.height).rev() { + for x in 0..self.width { + write!( + f, + "{}", + self.tiles[idx.index(x, y)] + .map(|t| t.tile_group.to_string()) + .unwrap_or(" ".to_string()) + )?; + } + write!(f, "\n")?; + } + + writeln!(f, "---") + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Component)] +pub struct LevelMap { + pub width: usize, + pub height: usize, + pub layers: Vec<MapLayer>, +} + +#[derive(Bundle)] +pub struct LevelMapBundle { + level_map: LevelMap, + #[bundle] + transform: TransformBundle, + #[bundle] + visibility: VisibilityBundle, +} + +impl LevelMapBundle { + pub fn new(level_map: LevelMap) -> Self { + Self { + level_map, + transform: TransformBundle::default(), + visibility: VisibilityBundle::default(), + } + } + + pub fn generate(width: usize, height: usize) -> Self { + Self::new(generate_map(width, height)) + } + pub fn generate_with_seed(seed: u64, width: usize, height: usize) -> Self { + Self::new(generate_map_with_seed(seed, width, height)) + } + pub fn generate_with_rng(rng: &Rng, width: usize, height: usize) -> Self { + Self::new(generate_map_with_rng(rng, width, height)) + } +} + +#[derive(Debug, Copy, Clone)] +pub struct Indexer { + width: usize, + height: usize, +} + +impl Indexer { + pub fn new(width: usize, height: usize) -> Self { + Indexer { width, height } + } + pub fn index(&self, x: usize, y: usize) -> usize { + (y * self.width) + x + } + pub fn reverse(&self, index: usize) -> (usize, usize) { + (index / self.width, index % self.width) + } + pub fn width(&self) -> usize { + self.width + } + pub fn height(&self) -> usize { + self.height + } +} diff --git a/game_core/src/world/mod.rs b/game_core/src/world/mod.rs index c379ed7d24b0e81501eff93d8ae2a4db3351a1ec..0997770ae74867bef99c1e8ee6cb3457d44ea0a5 100644 --- a/game_core/src/world/mod.rs +++ b/game_core/src/world/mod.rs @@ -1 +1,27 @@ +pub mod adjacency; +pub mod generation; +pub mod handlers; pub mod level_map; + +mod __internal { + use bevy::app::{App, CoreStage, Plugin}; + use iyes_loopless::condition::ConditionSet; + + use crate::system::flow::AppState; + + pub struct WorldPlugin; + impl Plugin for WorldPlugin { + fn build(&self, app: &mut App) { + app.add_system_set_to_stage( + CoreStage::PostUpdate, + ConditionSet::new() + .run_in_state(AppState::InGame) + .with_system(super::handlers::spawn_new_world) + .with_system(super::level_map::sync_grid_to_transform) + .into(), + ); + } + } +} + +pub use __internal::WorldPlugin;