From ee7ac39ad5198bc4cd7018781499e8946a1fa11c Mon Sep 17 00:00:00 2001 From: popertots Date: Thu, 11 Sep 2025 18:23:53 +0100 Subject: [PATCH] refactor file locations for readability --- assets/character.png | Bin 1222 -> 0 bytes src/camera.rs | 11 +- src/entities/livestock/pig.rs | 2 +- src/entities/livestock/rabbit.rs | 2 +- src/entities/sentient/dorf.rs | 2 +- src/entities/shared_systems/pathfinding.rs | 9 +- src/main.rs | 14 +- src/tile.rs | 75 --- src/tilemap.rs | 678 --------------------- src/world/chunks/management.rs | 118 ++++ src/world/chunks/mod.rs | 3 + src/world/generation/fauna.rs | 11 + src/world/generation/foliage.rs | 11 + src/world/generation/forestry.rs | 176 ++++++ src/world/generation/mod.rs | 9 + src/world/generation/terrain.rs | 174 ++++++ src/world/mod.rs | 78 +++ src/world/textures/management.rs | 85 +++ src/world/textures/mod.rs | 3 + src/world/tiles/components.rs | 44 ++ src/world/tiles/mod.rs | 11 + src/world/tiles/prefabs.rs | 166 +++++ src/{tiles.rs => world/tiles/rendering.rs} | 284 +-------- src/world/tiles/tilemap.rs | 8 + src/world/tiles/visibility.rs | 181 ++++++ 25 files changed, 1103 insertions(+), 1052 deletions(-) delete mode 100644 assets/character.png delete mode 100644 src/tile.rs delete mode 100644 src/tilemap.rs create mode 100644 src/world/chunks/management.rs create mode 100644 src/world/chunks/mod.rs create mode 100644 src/world/generation/fauna.rs create mode 100644 src/world/generation/foliage.rs create mode 100644 src/world/generation/forestry.rs create mode 100644 src/world/generation/mod.rs create mode 100644 src/world/generation/terrain.rs create mode 100644 src/world/mod.rs create mode 100644 src/world/textures/management.rs create mode 100644 src/world/textures/mod.rs create mode 100644 src/world/tiles/components.rs create mode 100644 src/world/tiles/mod.rs create mode 100644 src/world/tiles/prefabs.rs rename src/{tiles.rs => world/tiles/rendering.rs} (53%) create mode 100644 src/world/tiles/tilemap.rs create mode 100644 src/world/tiles/visibility.rs diff --git a/assets/character.png b/assets/character.png deleted file mode 100644 index 55f1070f8e0a965deeb55285b405ad0dce31d1e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1222 zcmV;%1UdVOP)EX>4Tx04R}tkv&MmKpe$iQ$;BihjvhL$WWc^qEZ}76^me@v=v%)FuC*#nlvOS zE{=k0!NHHks)LKOt`4q(Aou~|?BJy6A|?JWDYS_3;J6>}?mh0_0YaNu&AwMxlVH&Nh~3SG(^Z~po$tSL}}GXF_EGDgol63@u$fpldB3w zjs?`ALUR1zfAG6ovotg9CIu5f|BG#ZOaKGBK)Y$%-^aGyJ^}pCz?IhZ*IU5cC+W?u z7C8cjw}Ff6t|spRmpj17lP(#OBl&3xr4sOdM&DEbLbpI*&F!tTkJASrM_n!700)P_ zWSO$pJ>EUo+uOfqI{p0s>8NtnoCzyT00006VoOIv00000008+zyMF)x010qNS#tmY zE+YT{E+YYWr9XB6000McNliru=nM)J0x{%CS3Up$02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00N##L_t(I%iWYuNYh~$$3O3HQIpEmTH-R?=E~`oi1cSc zskMkG#5AZKq)a+^2%;jZL#GI85$PcbgANu^LV@j4QBYx6(&&&a0_Q?UZB&f1LU(Xa zhqaY|GVRz4&*A;P&-?Ivzt11|ACAn#S)1xnDJ4zbN|aL54yyi?Rof9S_j-cZau=eM zVtKxq#&aF&ZvkhG2TUfD?DaUgbi0eTi#|jIKqj9#lT{NES0Knxc z!EUz$;P-b@?<}DGby$rFn3a_!$z+m5B9Tg6)2LbJL@9+O$3W!MSNu=kplKQ-BO^GS zE3n(`7#0}$_#sAj_Y;POhJI0AwWXLuB7wzX;pnjwB$G)7VsQW*);vZ>M>*p=&+zat z8<#A~X!hlEoJb@>dwa(i4P_|)J>z6Hn>pWn0i{v|?ml}(;rbmMI=G*}gR4A>#t8;{ z357z0LLp9_I!kTs)-jBwXpKdFiYqpoO`2O;Gj~s(N$Q;inY&U-np;{$uh+|j(Ns!F zX@x_I)>x#p!jbuuwoOYHPr!b&$=fE|IG5RT=8$ci3&8XHSLhFRf} zl`8i#na(O3pSB#RT}fG?6;L$zZUS(;t~}K~OLJ)O-I{7kO;`@ Self { - Self { - id: 0, - opaque: true, - walkable: true, - astar_weight: 0, - visible_range: [0; 8], - } - } -} - -#[derive(Component, Clone)] -pub struct FixtureTile { - pub id: u32, - pub solid: bool, - pub visible_range: [u32; 8], -} - -impl Default for FixtureTile { - fn default() -> Self { - Self { - id: 0, - solid: true, - visible_range: [0; 8], - } - } -} - -#[derive(Resource, Default, Clone)] -pub struct TileMap { - pub floor_tiles: HashMap, //id, canStandIn, canStandOn, visiblyTransparent, astar_weight, visible_range - pub fixture_tiles: HashMap, // id, canStandIn, canStandOn, visible_range -} - -pub fn update_tile_visibility( - z_index: Res, - mut query: Query<(&tiles::TerrainSprite, &mut Visibility)>, - mut cwss: ResMut, -) { - let now = Instant::now(); - - for (terrain_sprite, mut visibility) in query.iter_mut() { - *visibility = if terrain_sprite.z_index == ((z_index.0 + tilemap::Z_BELOW) as usize) { - Visibility::Visible - } else { - Visibility::Hidden - }; - } - cwss.state = TerrainSpriteState::Inactive; - println!("Visibility update: {:.2?}", now.elapsed()); -} - -#[derive(Component)] -pub struct TileState { - pub timer: Timer, -} diff --git a/src/tilemap.rs b/src/tilemap.rs deleted file mode 100644 index 70a611d..0000000 --- a/src/tilemap.rs +++ /dev/null @@ -1,678 +0,0 @@ -use std::sync::Mutex; -use std::time::Instant; - -use crate::constants::{self, ITILE_SIZE, TILE_SIZE}; -use crate::tile::{update_tile_visibility, FloorTile, TileMap}; -use crate::tiles::{ - self, build_quilted_terrain_sprites, CurrentWorldSpriteState, FixtureTilePrefab, - FloorTilePrefab, TerrainSpriteState, TextureIDs, Textures, -}; -use crate::{camera, game}; -use bevy::prelude::*; -use bevy_platform::collections::hash_map::HashMap; -use bevy_platform::collections::HashSet; -use bevy_rand::prelude::*; -use noise::{NoiseFn, Perlin}; -use rand::{Rng, SeedableRng}; -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; - -pub const CHUNK_SIZE: i32 = 8; - -pub const Z_BELOW: f32 = 45.0; -pub const Z_ABOVE: f32 = 15.0; -pub const Z_TOTAL: f32 = Z_ABOVE + Z_BELOW; - -const _: () = assert!(Z_TOTAL <= 255.0); - -#[derive(Resource)] -pub struct ChunkMap { - pub loaded_chunks: HashMap, -} - -impl Default for ChunkMap { - fn default() -> Self { - Self { - loaded_chunks: HashMap::new(), - } - } -} - -#[derive(Event)] -pub struct GenerateChunkEvent { - pub chunk_position: IVec2, -} -#[derive(Event)] -pub struct ChunkTerrainEvent { - pub chunk_position: IVec2, -} -#[derive(Event)] -// CAlculate the weather effecrs and rivers/lakes of this chunk -pub struct ChunkWeatheringAndPrecipitationEvent { - pub chunk_position: IVec2, -} -#[derive(Event)] -pub struct ChunkForrestryEvent { - pub chunk_position: IVec2, - pub floor_tiles: Vec<(Vec3, String)>, -} -#[derive(Event)] -pub struct ChunkFoliageEvent { - pub chunk_position: IVec2, -} -#[derive(Event)] -pub struct ChunkFaunaEvent { - pub chunk_position: IVec2, -} - -fn setup_chunk_system(mut commands: Commands) { - commands.insert_resource(TileMap::default()); - commands.insert_resource(ChunkMap::default()); -} - -#[derive(Event)] -pub struct TileOcclusionEvent { - pub tile_position: IVec3, -} - -pub fn handle_tile_occlusion_updates( - mut tilemap: ResMut, - mut floor_tiles: Query<(Entity, &mut FloorTile, &Transform)>, - mut cwss: ResMut, - mut events: EventReader, -) { - let start = Instant::now(); - - let count = events.len(); - - // Process events in parallel - let updates: Vec<(IVec3, [u32; 8])> = events - .par_read() - .into_iter() - .map(|event| { - let pos = event.0.tile_position; - let visibility = calculate_visibility(pos, &tilemap); - (pos, visibility) - }) - .collect(); - - // Map updates for efficient lookup - let update_map: HashMap = updates.into_iter().collect(); - - // Update only the relevant FloorTile components - for (_, mut tile, pos) in floor_tiles.iter_mut() { - if let Some(visibility) = update_map.get(&pos.translation.as_ivec3()) { - tile.visible_range = *visibility; - if let Some(tile_data) = tilemap.floor_tiles.get_mut(&pos.translation.as_ivec3()) { - tile_data.5 = *visibility; - } - } - } - if count > 0 { - cwss.state = TerrainSpriteState::WaitingForRender; - println!( - "Tile occlusion calculated for {} tiles in {:.2?}", - count, - start.elapsed() - ); - } -} - -pub fn calculate_visibility(pos: IVec3, tilemap: &TileMap) -> [u32; 8] { - let mut visible_range = [0u32; 8]; - - for mut camera_z in -Z_BELOW as i32..=Z_ABOVE as i32 { - camera_z *= ITILE_SIZE; - let mut is_visible = false; - - if pos.z > camera_z { - continue; - } - - let mut is_occluded = false; - - let v_check_height: i32 = Z_TOTAL as i32 - (camera_z / ITILE_SIZE) + Z_BELOW as i32 + 1; - 'vertical_check: for z_offset in 1..v_check_height { - let above_pos = IVec3::new(pos.x, pos.y, pos.z + (z_offset * ITILE_SIZE)); - if above_pos.z <= camera_z { - if let Some(&(_, _, _, visibly_transparent, _, _)) = - tilemap.floor_tiles.get(&above_pos) - { - if !visibly_transparent { - is_occluded = true; - break 'vertical_check; - } - } - } else { - break 'vertical_check; - } - } - if !is_occluded { - 'neighbor_check: for x_offset in -1..=1 { - for y_offset in -1..=1 { - for z_offset in 0..=1 { - if x_offset == 0 && y_offset == 0 && z_offset == 0 { - continue; - } - let neighbor_pos = IVec3::new( - pos.x + x_offset * ITILE_SIZE, - pos.y + y_offset * ITILE_SIZE, - pos.z + z_offset * ITILE_SIZE, - ); - - if let Some(&(id, _, _, _, _, _)) = tilemap.floor_tiles.get(&neighbor_pos) { - if id == 0 { - // id 0 = air tile - is_visible = true; - break 'neighbor_check; - } - } - } - } - } - } - - if is_visible { - let z2 = ((camera_z / ITILE_SIZE) + Z_BELOW as i32) as usize; - visible_range[z2 / 32] |= 1 << ((z2 % 32) as u32); - } - } - - visible_range -} - -pub fn generate_surface_terrain(x: i32, y: i32) -> f32 { - let noise = Perlin::new(constants::SEED); - let mut noise_value = 0.0; - let mut amplitude = 1.0; - let mut frequency = 0.008; - - for _ in 0..6 { - noise_value += noise.get([x as f64 * frequency, y as f64 * frequency]) * amplitude; - amplitude *= 0.6; - frequency *= 1.8; - } - (noise_value * 2.5) as f32 -} - -fn chunkmap_despawn_timer_system( - mut chunk_map: ResMut, - mut cwss: ResMut, -) { - for (_, (is_loaded, timer)) in chunk_map.loaded_chunks.iter_mut() { - if !*is_loaded { - continue; - } - if *timer > 0 { - *timer -= 1; - } else { - //TODO - remove chunk from chunkmap - - *is_loaded = false; - cwss.state = TerrainSpriteState::WaitingForRender; - } - } -} - -fn handle_chunk_events( - mut chunk_events: EventReader, - mut chunk_map: ResMut, - mut terrain_event_writer: EventWriter, - mut weathering_event_writer: EventWriter, - mut foliage_event_writer: EventWriter, - mut fauna_event_writer: EventWriter, - mut cwss: ResMut, -) { - let mut any = false; - for event in chunk_events.par_read() { - let chunk_pos = event.0.chunk_position; - if let Some((true, _)) = chunk_map.loaded_chunks.get(&chunk_pos) { - continue; - } - any = true; - let chunk_map_updates: Mutex> = Mutex::new(HashMap::new()); - // Mark chunk as loaded in our thread-safe collection - chunk_map_updates.lock().unwrap().insert(chunk_pos, true); - terrain_event_writer.write(ChunkTerrainEvent { - chunk_position: chunk_pos, - }); - weathering_event_writer.write(ChunkWeatheringAndPrecipitationEvent { - chunk_position: chunk_pos, - }); - foliage_event_writer.write(ChunkFoliageEvent { - chunk_position: chunk_pos, - }); - fauna_event_writer.write(ChunkFaunaEvent { - chunk_position: chunk_pos, - }); - // Apply all collected updates to the actual resources - for (chunk_pos, value) in chunk_map_updates.into_inner().unwrap() { - chunk_map.loaded_chunks.insert(chunk_pos, (value, 1800)); - } - } - if any { - cwss.state = TerrainSpriteState::WaitingForRender; - } -} - -fn generate_chunk_terrain( - commands: ParallelCommands<'_, '_>, // Use ParallelCommands for parallel spawning - mut events: EventReader, - mut tilemap: ResMut, - mut forrestry_event_writer: EventWriter, - mut occlusion_event_writer: EventWriter, -) { - let is_empty = events.is_empty(); - let start = Instant::now(); - let count: usize = events.len(); - - let cave_noise = Perlin::new(constants::SEED); - - // Create mutexes for our shared resources - let tilemap_updates = Mutex::new(HashMap::new()); - let forrestry_events = Mutex::new(Vec::new()); - - events.par_read().for_each(|event| { - let chunk_pos = event.chunk_position; - - let start_x = chunk_pos.x * CHUNK_SIZE; - let start_y = chunk_pos.y * CHUNK_SIZE; - - let mut surface_positions: Vec<(Vec3, String)> = Vec::new(); - let mut local_tilemap_updates: HashMap = - HashMap::new(); - - // Generate tiles for this chunk - for local_y in 0..CHUNK_SIZE { - for local_x in 0..CHUNK_SIZE { - let world_x = start_x + local_x; - let world_y = start_y + local_y; - - let noise_position = Vec3::new( - (world_x as f32 * TILE_SIZE).round(), - (world_y as f32 * TILE_SIZE).round(), - (generate_surface_terrain(world_x, world_y) * TILE_SIZE).round(), - ); - - // Spawn tiles and add them to tilemap - for z in -Z_BELOW as isize..=Z_ABOVE as isize { - let position = Vec3::new( - (world_x as f32 * TILE_SIZE).round(), - (world_y as f32 * TILE_SIZE).round(), - (z as f32 * TILE_SIZE).round(), - ); - let pos_ivec = position.as_ivec3(); - - if z < -5 { - let cave_value = cave_noise.get([ - world_x as f64 * 0.05, - world_y as f64 * 0.05, - z as f64 * 0.05, - ]); - if cave_value < -0.75 { - commands.command_scope(|mut cmd| { - FloorTilePrefab::air(position).spawn(&mut cmd); - }); - local_tilemap_updates - .insert(pos_ivec, (0, true, false, true, 0, [0; 8])); - // Air tile - } else if cave_value < 0.8 { - commands.command_scope(|mut cmd| { - FloorTilePrefab::rock(position).spawn(&mut cmd); - }); - local_tilemap_updates - .insert(pos_ivec, (2, false, true, false, 50, [0; 8])); - // Rock tile - } else { - commands.command_scope(|mut cmd| { - FloorTilePrefab::dirt(position).spawn(&mut cmd); - }); - local_tilemap_updates - .insert(pos_ivec, (1, false, true, false, 85, [0; 8])); - // Dirt tile - } - } else if noise_position.z > position.z { - if (generate_surface_terrain(world_x, world_y) * TILE_SIZE).round() - <= position.z + TILE_SIZE - { - commands.command_scope(|mut cmd| { - FloorTilePrefab::grass(position).spawn(&mut cmd); - }); - local_tilemap_updates - .insert(pos_ivec, (1, false, true, false, 100, [0; 8])); // Dirt tile (grass) - surface_positions.push((position, ("grass").to_string())); - } else { - commands.command_scope(|mut cmd| { - FloorTilePrefab::dirt(position).spawn(&mut cmd); - }); - local_tilemap_updates - .insert(pos_ivec, (1, false, true, false, 85, [0; 8])); - // Dirt tile - } - } else { - commands.command_scope(|mut cmd| { - FloorTilePrefab::air(position).spawn(&mut cmd); - }); - local_tilemap_updates.insert(pos_ivec, (0, true, false, true, 0, [0; 8])); - // Air tile - } - } - } - } - - // Add our local updates to the global mutexes - { - let mut tilemap_guard = tilemap_updates.lock().unwrap(); - for (pos, data) in local_tilemap_updates { - tilemap_guard.insert(pos, data); - } - } - - // Store forrestry event for this chunk - forrestry_events.lock().unwrap().push(ChunkForrestryEvent { - chunk_position: chunk_pos, - floor_tiles: surface_positions, - }); - }); - - for (pos, data) in tilemap_updates.into_inner().unwrap() { - tilemap.floor_tiles.insert(pos, data); - occlusion_event_writer.write(TileOcclusionEvent { tile_position: pos }); - } - - // Send all forrestry events - for event in forrestry_events.into_inner().unwrap() { - forrestry_event_writer.write(event); - } - - if !is_empty { - println!("{} terrain chunks loaded in {:.2?}", count, start.elapsed()); - } -} - -fn generate_chunk_weathering_and_precipitation(// mut commands: Commands, - // mut events: EventReader, - // mut chunk_map: ResMut, - // mut tilemap: ResMut, -) { - // TODO: Generate weathering and precipitation - // Temperature and humidity - // Erosion - // Hadley lines/cells etc - // Biomes -} - -fn generate_chunk_forrestry( - commands: ParallelCommands<'_, '_>, - mut events: EventReader, - mut tilemap: ResMut, - texture_ids: Res, - textures: Res, -) { - let start = Instant::now(); - let count = events.len(); - - let collected_tilemap_updates: Mutex> = - Mutex::new(Vec::<(IVec3, (i32, bool, bool, [u32; 8]))>::new()); - - events.par_read().for_each(|event| { - let floor_positions = &event.floor_tiles; - let mut tree_positions: Vec = Vec::new(); - let min_distance = 7.0 * TILE_SIZE; - - let mut hasher = DefaultHasher::new(); - constants::SEED.hash(&mut hasher); - event.chunk_position.x.hash(&mut hasher); - event.chunk_position.y.hash(&mut hasher); - let seed = hasher.finish(); - let mut rng = WyRand::seed_from_u64(seed); - - for (position, floor_type) in floor_positions.iter() { - let above_pos = *position + Vec3::new(0.0, 0.0, TILE_SIZE); - - match floor_type.as_str() { - "grass" => { - commands.command_scope(|mut commands| { - // Track log positions to avoid leaf overlap - let mut log_positions = HashSet::new(); - - // Check if position is far enough from existing trees - let is_far_enough = tree_positions - .iter() - .all(|&tree_pos| above_pos.distance(tree_pos) > min_distance); - - // 1 in 100 chance if far enough from other trees - if is_far_enough && rng.random::() % 100 == 0 { - // Generate trunk - let trunk_height = 4 + rng.random::() % 5; - for i in 0..trunk_height { - let trunk_pos = - above_pos + Vec3::new(0.0, 0.0, i as f32 * TILE_SIZE); - let trunk_ivec = trunk_pos.as_ivec3(); - - // Skip if position already has a tile - if tilemap.fixture_tiles.get(&trunk_ivec).is_some() { - continue; - } - - let trunk_entity = - FixtureTilePrefab::log(trunk_pos).spawn(&mut commands); - tree_positions.push(trunk_pos); - // Add sprite component to the same entity if texture exists - if let Some(texture_id) = texture_ids.refs.get(&500004) { - if let Some(texture) = textures.handles.get(texture_id) { - let sprite = Sprite { - image: texture.clone(), - ..Default::default() - }; - - commands - .entity(trunk_entity) - .insert((sprite, VisibleGameEntity)); - // .insert(Visibility::Visible); // Override the hidden visibility - } - } - - collected_tilemap_updates - .lock() - .unwrap() - .push((trunk_ivec, (1, false, true, [0; 8]))); - - log_positions.insert(trunk_ivec); - } - - // Generate leaves - 3D spherical canopy with random shape - let base_leaf_radius = 2.25; - let leaf_center = - above_pos + Vec3::new(0.0, 0.0, trunk_height as f32 * TILE_SIZE); - - for x in -base_leaf_radius as i32..=base_leaf_radius as i32 { - for y in -base_leaf_radius as i32..=base_leaf_radius as i32 { - for z in -base_leaf_radius as i32..=base_leaf_radius as i32 { - let pos = Vec3::new( - x as f32 * TILE_SIZE, - y as f32 * TILE_SIZE, - z as f32 * TILE_SIZE, - ) + leaf_center; - let ivec = pos.as_ivec3(); - - // Skip if position already has a tile or is a log - if tilemap.fixture_tiles.get(&ivec).is_some() - || log_positions.contains(&ivec) - { - continue; - } - - // Apply random radius variation for natural shape - let x_f = x as f32; - let y_f = y as f32; - let z_f = z as f32; - let radius = base_leaf_radius - * (1.0 + (rng.random::() * 0.35 - 0.1)); - - if x_f * x_f + y_f * y_f + z_f * z_f <= radius * radius { - FixtureTilePrefab::leaves(pos).spawn(&mut commands); - if let Some(texture_id) = texture_ids.refs.get(&500005) - { - if let Some(texture) = - textures.handles.get(texture_id) - { - let sprite = Sprite { - image: texture.clone(), - ..Default::default() - }; - let leaf = commands - .spawn(( - sprite, - Transform::from_xyz( - pos.x, pos.y, pos.z, - ), - )) - .id(); - commands.entity(leaf).insert(VisibleGameEntity); - collected_tilemap_updates - .lock() - .unwrap() - .push((ivec, (5, false, true, [0; 8]))); - } - } - } - } - } - } - } - }); - } - _ => {} - } - } - }); - - let collected_updates = collected_tilemap_updates.into_inner().unwrap(); - for (ivec, data) in collected_updates { - tilemap.fixture_tiles.insert(ivec, data); - } - if count > 0 { - println!( - "Forrestry update for {:?} chunks in {:.2?}", - count, - start.elapsed() - ); - } -} - -fn generate_chunk_foliage( - mut commands: Commands, - mut events: EventReader, - mut chunk_map: ResMut, - mut tilemap: ResMut, -) { -} - -fn generate_chunk_fauna( - mut commands: Commands, - mut events: EventReader, - mut chunk_map: ResMut, - mut tilemap: ResMut, -) { -} - -fn setup_initial_chunks(mut event_writer: EventWriter) { - for x in -5..=5 { - for y in -5..=5 { - event_writer.write(GenerateChunkEvent { - chunk_position: IVec2::new(x, y), - }); - } - } -} - -#[derive(Component)] -pub struct VisibleGameEntity; - -pub fn compute_visibility_of_game_entities( - mut query: Query<(&Transform, &mut Visibility, &mut Sprite), With>, - z_index: Res, -) { - query - .par_iter_mut() - .for_each(|(transform, mut visibility, mut sprite)| { - let entity_z = (transform.translation.z / TILE_SIZE) - 1.0; - // if same z-level, always visible - if z_index.0 == entity_z { - sprite.color = Color::WHITE; - sprite.color.set_alpha(1.0); - *visibility = Visibility::Visible; - return; - }; - - // if entity too low, saturation does not matter - if z_index.0 - entity_z > 8.0 { - *visibility = Visibility::Hidden; - return; - } - // if entity too high, saturation does not matter - if entity_z > z_index.0 { - *visibility = Visibility::Hidden; - return; - } - - // if visible, calculate saturation - *visibility = Visibility::Visible; - let saturation = - ((z_index.0 - (transform.translation.z / TILE_SIZE) + 1.) / 8.0).clamp(0.0, 1.0); - - sprite.color = Color::hsv(194.7, saturation, 1.0 - (saturation / 2.0)); - - sprite.color.set_alpha(1.0 - saturation); - }); -} - -pub struct TilemapPlugin; - -impl Plugin for TilemapPlugin { - fn build(&self, app: &mut App) { - app.init_resource::() - .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_systems(Startup, (setup_chunk_system, setup_initial_chunks).chain()) - .add_systems( - FixedUpdate, - ( - ( - handle_chunk_events, - generate_chunk_terrain, - generate_chunk_weathering_and_precipitation, - generate_chunk_forrestry, - generate_chunk_foliage, - generate_chunk_fauna, - ) - .chain(), - chunkmap_despawn_timer_system, - ), - ) - .add_systems( - Update, - ( - compute_visibility_of_game_entities, - ( - handle_tile_occlusion_updates, - build_quilted_terrain_sprites, - update_tile_visibility.run_if( - |cwss: Res, - camera_moved: Res| { - cwss.state == tiles::TerrainSpriteState::RenderReady - || camera_moved.0 - }, - ), - ) - .chain(), - ), - ); - } -} diff --git a/src/world/chunks/management.rs b/src/world/chunks/management.rs new file mode 100644 index 0000000..96ec224 --- /dev/null +++ b/src/world/chunks/management.rs @@ -0,0 +1,118 @@ +use bevy::prelude::*; +use bevy_platform::collections::HashMap; +use bevy_platform::sync::Mutex; + +use crate::world::{tiles::TileMap, CurrentWorldSpriteState, TerrainSpriteState}; + +pub const CHUNK_SIZE: i32 = 8; + +pub const Z_BELOW: f32 = 45.0; +pub const Z_ABOVE: f32 = 15.0; +pub const Z_TOTAL: f32 = Z_ABOVE + Z_BELOW; + +const _: () = assert!(Z_TOTAL <= 255.0); + +#[derive(Resource)] +pub struct ChunkMap { + pub loaded_chunks: HashMap, +} + +impl Default for ChunkMap { + fn default() -> Self { + Self { + loaded_chunks: HashMap::new(), + } + } +} + +#[derive(Event)] +pub struct GenerateChunkEvent { + pub chunk_position: IVec2, +} +#[derive(Event)] +pub struct ChunkTerrainEvent { + pub chunk_position: IVec2, +} +#[derive(Event)] +// CAlculate the weather effecrs and rivers/lakes of this chunk +pub struct ChunkWeatheringAndPrecipitationEvent { + pub chunk_position: IVec2, +} +#[derive(Event)] +pub struct ChunkForrestryEvent { + pub chunk_position: IVec2, + pub floor_tiles: Vec<(Vec3, String)>, +} +#[derive(Event)] +pub struct ChunkFoliageEvent { + pub chunk_position: IVec2, +} +#[derive(Event)] +pub struct ChunkFaunaEvent { + pub chunk_position: IVec2, +} + +pub fn setup_chunk_system(mut commands: Commands) { + commands.insert_resource(TileMap::default()); + commands.insert_resource(ChunkMap::default()); +} + +pub fn handle_chunk_events( + mut chunk_events: EventReader, + mut chunk_map: ResMut, + mut terrain_event_writer: EventWriter, + mut weathering_event_writer: EventWriter, + mut foliage_event_writer: EventWriter, + mut fauna_event_writer: EventWriter, + mut cwss: ResMut, +) { + let mut any = false; + for event in chunk_events.par_read() { + let chunk_pos = event.0.chunk_position; + if let Some((true, _)) = chunk_map.loaded_chunks.get(&chunk_pos) { + continue; + } + any = true; + let chunk_map_updates: Mutex> = Mutex::new(HashMap::new()); + // Mark chunk as loaded in our thread-safe collection + chunk_map_updates.lock().unwrap().insert(chunk_pos, true); + terrain_event_writer.write(ChunkTerrainEvent { + chunk_position: chunk_pos, + }); + weathering_event_writer.write(ChunkWeatheringAndPrecipitationEvent { + chunk_position: chunk_pos, + }); + foliage_event_writer.write(ChunkFoliageEvent { + chunk_position: chunk_pos, + }); + fauna_event_writer.write(ChunkFaunaEvent { + chunk_position: chunk_pos, + }); + // Apply all collected updates to the actual resources + for (chunk_pos, value) in chunk_map_updates.into_inner().unwrap() { + chunk_map.loaded_chunks.insert(chunk_pos, (value, 1800)); + } + } + if any { + cwss.state = TerrainSpriteState::WaitingForRender; + } +} + +pub fn chunkmap_despawn_timer_system( + mut chunk_map: ResMut, + mut cwss: ResMut, +) { + for (_, (is_loaded, timer)) in chunk_map.loaded_chunks.iter_mut() { + if !*is_loaded { + continue; + } + if *timer > 0 { + *timer -= 1; + } else { + //TODO - remove chunk from chunkmap + + *is_loaded = false; + cwss.state = TerrainSpriteState::WaitingForRender; + } + } +} diff --git a/src/world/chunks/mod.rs b/src/world/chunks/mod.rs new file mode 100644 index 0000000..5679e09 --- /dev/null +++ b/src/world/chunks/mod.rs @@ -0,0 +1,3 @@ +pub mod management; + +pub use management::*; diff --git a/src/world/generation/fauna.rs b/src/world/generation/fauna.rs new file mode 100644 index 0000000..46bed98 --- /dev/null +++ b/src/world/generation/fauna.rs @@ -0,0 +1,11 @@ +use bevy::prelude::*; + +use crate::world::{tiles::TileMap, ChunkMap, GenerateChunkEvent}; + +pub fn generate_chunk_fauna( + mut commands: Commands, + mut events: EventReader, + mut chunk_map: ResMut, + mut tilemap: ResMut, +) { +} diff --git a/src/world/generation/foliage.rs b/src/world/generation/foliage.rs new file mode 100644 index 0000000..a7c1695 --- /dev/null +++ b/src/world/generation/foliage.rs @@ -0,0 +1,11 @@ +use bevy::prelude::*; + +use crate::world::{tiles::TileMap, ChunkMap, GenerateChunkEvent}; + +pub fn generate_chunk_foliage( + mut commands: Commands, + mut events: EventReader, + mut chunk_map: ResMut, + mut tilemap: ResMut, +) { +} diff --git a/src/world/generation/forestry.rs b/src/world/generation/forestry.rs new file mode 100644 index 0000000..d49d60e --- /dev/null +++ b/src/world/generation/forestry.rs @@ -0,0 +1,176 @@ +use bevy::prelude::*; +use bevy_platform::collections::HashSet; +use bevy_platform::sync::Mutex; +use bevy_platform::time::Instant; +use bevy_rand::prelude::*; + +use rand::{Rng, SeedableRng}; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + +use crate::{ + constants::{SEED, TILE_SIZE}, + world::{ + tiles::TileMap, ChunkForrestryEvent, FixtureTilePrefab, TextureIDs, Textures, + VisibleGameEntity, + }, +}; + +pub fn generate_chunk_forrestry( + commands: ParallelCommands<'_, '_>, + mut events: EventReader, + mut tilemap: ResMut, + texture_ids: Res, + textures: Res, +) { + let start = Instant::now(); + let count = events.len(); + + let collected_tilemap_updates: Mutex> = + Mutex::new(Vec::<(IVec3, (i32, bool, bool, [u32; 8]))>::new()); + + events.par_read().for_each(|event| { + let floor_positions = &event.floor_tiles; + let mut tree_positions: Vec = Vec::new(); + let min_distance = 7.0 * TILE_SIZE; + + let mut hasher = DefaultHasher::new(); + SEED.hash(&mut hasher); + event.chunk_position.x.hash(&mut hasher); + event.chunk_position.y.hash(&mut hasher); + let seed = hasher.finish(); + let mut rng = WyRand::seed_from_u64(seed); + + for (position, floor_type) in floor_positions.iter() { + let above_pos = *position + Vec3::new(0.0, 0.0, TILE_SIZE); + + match floor_type.as_str() { + "grass" => { + commands.command_scope(|mut commands| { + // Track log positions to avoid leaf overlap + let mut log_positions = HashSet::new(); + + // Check if position is far enough from existing trees + let is_far_enough = tree_positions + .iter() + .all(|&tree_pos| above_pos.distance(tree_pos) > min_distance); + + // 1 in 100 chance if far enough from other trees + if is_far_enough && rng.random::() % 100 == 0 { + // Generate trunk + let trunk_height = 4 + rng.random::() % 5; + for i in 0..trunk_height { + let trunk_pos = + above_pos + Vec3::new(0.0, 0.0, i as f32 * TILE_SIZE); + let trunk_ivec = trunk_pos.as_ivec3(); + + // Skip if position already has a tile + if tilemap.fixture_tiles.get(&trunk_ivec).is_some() { + continue; + } + + let trunk_entity = + FixtureTilePrefab::log(trunk_pos).spawn(&mut commands); + tree_positions.push(trunk_pos); + // Add sprite component to the same entity if texture exists + if let Some(texture_id) = texture_ids.refs.get(&500004) { + if let Some(texture) = textures.handles.get(texture_id) { + let sprite = Sprite { + image: texture.clone(), + ..Default::default() + }; + + commands + .entity(trunk_entity) + .insert((sprite, VisibleGameEntity)); + // .insert(Visibility::Visible); // Override the hidden visibility + } + } + + collected_tilemap_updates + .lock() + .unwrap() + .push((trunk_ivec, (1, false, true, [0; 8]))); + + log_positions.insert(trunk_ivec); + } + + // Generate leaves - 3D spherical canopy with random shape + let base_leaf_radius = 2.25; + let leaf_center = + above_pos + Vec3::new(0.0, 0.0, trunk_height as f32 * TILE_SIZE); + + for x in -base_leaf_radius as i32..=base_leaf_radius as i32 { + for y in -base_leaf_radius as i32..=base_leaf_radius as i32 { + for z in -base_leaf_radius as i32..=base_leaf_radius as i32 { + let pos = Vec3::new( + x as f32 * TILE_SIZE, + y as f32 * TILE_SIZE, + z as f32 * TILE_SIZE, + ) + leaf_center; + let ivec = pos.as_ivec3(); + + // Skip if position already has a tile or is a log + if tilemap.fixture_tiles.get(&ivec).is_some() + || log_positions.contains(&ivec) + { + continue; + } + + // Apply random radius variation for natural shape + let x_f = x as f32; + let y_f = y as f32; + let z_f = z as f32; + let radius = base_leaf_radius + * (1.0 + (rng.random::() * 0.35 - 0.1)); + + if x_f * x_f + y_f * y_f + z_f * z_f <= radius * radius { + FixtureTilePrefab::leaves(pos).spawn(&mut commands); + if let Some(texture_id) = texture_ids.refs.get(&500005) + { + if let Some(texture) = + textures.handles.get(texture_id) + { + let sprite = Sprite { + image: texture.clone(), + ..Default::default() + }; + let leaf = commands + .spawn(( + sprite, + Transform::from_xyz( + pos.x, pos.y, pos.z, + ), + )) + .id(); + commands.entity(leaf).insert(VisibleGameEntity); + collected_tilemap_updates + .lock() + .unwrap() + .push((ivec, (5, false, true, [0; 8]))); + } + } + } + } + } + } + } + }); + } + _ => {} + } + } + }); + + let collected_updates = collected_tilemap_updates.into_inner().unwrap(); + for (ivec, data) in collected_updates { + tilemap.fixture_tiles.insert(ivec, data); + } + if count > 0 { + println!( + "Forrestry update for {:?} chunks in {:.2?}", + count, + start.elapsed() + ); + } +} diff --git a/src/world/generation/mod.rs b/src/world/generation/mod.rs new file mode 100644 index 0000000..0049bb1 --- /dev/null +++ b/src/world/generation/mod.rs @@ -0,0 +1,9 @@ +pub mod fauna; +pub mod foliage; +pub mod forestry; +pub mod terrain; + +pub use fauna::*; +pub use foliage::*; +pub use forestry::*; +pub use terrain::*; diff --git a/src/world/generation/terrain.rs b/src/world/generation/terrain.rs new file mode 100644 index 0000000..e6be0c1 --- /dev/null +++ b/src/world/generation/terrain.rs @@ -0,0 +1,174 @@ +use bevy::prelude::*; +use bevy_platform::collections::HashMap; +use bevy_platform::sync::Mutex; +use bevy_platform::time::Instant; +use noise::{NoiseFn, Perlin}; + +use crate::{ + constants::{SEED, TILE_SIZE}, + world::{ + tiles::TileMap, ChunkForrestryEvent, ChunkTerrainEvent, FloorTilePrefab, + TileOcclusionEvent, CHUNK_SIZE, Z_ABOVE, Z_BELOW, + }, +}; + +pub fn generate_surface_terrain(x: i32, y: i32) -> f32 { + let noise = Perlin::new(SEED); + let mut noise_value = 0.0; + let mut amplitude = 1.0; + let mut frequency = 0.008; + + for _ in 0..6 { + noise_value += noise.get([x as f64 * frequency, y as f64 * frequency]) * amplitude; + amplitude *= 0.6; + frequency *= 1.8; + } + (noise_value * 2.5) as f32 +} + +pub fn generate_chunk_terrain( + commands: ParallelCommands<'_, '_>, // Use ParallelCommands for parallel spawning + mut events: EventReader, + mut tilemap: ResMut, + mut forrestry_event_writer: EventWriter, + mut occlusion_event_writer: EventWriter, +) { + let is_empty = events.is_empty(); + let start = Instant::now(); + let count: usize = events.len(); + + let cave_noise = Perlin::new(SEED); + + // Create mutexes for our shared resources + let tilemap_updates = Mutex::new(HashMap::new()); + let forrestry_events = Mutex::new(Vec::new()); + + events.par_read().for_each(|event| { + let chunk_pos = event.chunk_position; + + let start_x = chunk_pos.x * CHUNK_SIZE; + let start_y = chunk_pos.y * CHUNK_SIZE; + + let mut surface_positions: Vec<(Vec3, String)> = Vec::new(); + let mut local_tilemap_updates: HashMap = + HashMap::new(); + + // Generate tiles for this chunk + for local_y in 0..CHUNK_SIZE { + for local_x in 0..CHUNK_SIZE { + let world_x = start_x + local_x; + let world_y = start_y + local_y; + + let noise_position = Vec3::new( + (world_x as f32 * TILE_SIZE).round(), + (world_y as f32 * TILE_SIZE).round(), + (generate_surface_terrain(world_x, world_y) * TILE_SIZE).round(), + ); + + // Spawn tiles and add them to tilemap + for z in -Z_BELOW as isize..=Z_ABOVE as isize { + let position = Vec3::new( + (world_x as f32 * TILE_SIZE).round(), + (world_y as f32 * TILE_SIZE).round(), + (z as f32 * TILE_SIZE).round(), + ); + let pos_ivec = position.as_ivec3(); + + if z < -5 { + let cave_value = cave_noise.get([ + world_x as f64 * 0.05, + world_y as f64 * 0.05, + z as f64 * 0.05, + ]); + if cave_value < -0.75 { + commands.command_scope(|mut cmd| { + FloorTilePrefab::air(position).spawn(&mut cmd); + }); + local_tilemap_updates + .insert(pos_ivec, (0, true, false, true, 0, [0; 8])); + // Air tile + } else if cave_value < 0.8 { + commands.command_scope(|mut cmd| { + FloorTilePrefab::rock(position).spawn(&mut cmd); + }); + local_tilemap_updates + .insert(pos_ivec, (2, false, true, false, 50, [0; 8])); + // Rock tile + } else { + commands.command_scope(|mut cmd| { + FloorTilePrefab::dirt(position).spawn(&mut cmd); + }); + local_tilemap_updates + .insert(pos_ivec, (1, false, true, false, 85, [0; 8])); + // Dirt tile + } + } else if noise_position.z > position.z { + if (generate_surface_terrain(world_x, world_y) * TILE_SIZE).round() + <= position.z + TILE_SIZE + { + commands.command_scope(|mut cmd| { + FloorTilePrefab::grass(position).spawn(&mut cmd); + }); + local_tilemap_updates + .insert(pos_ivec, (1, false, true, false, 100, [0; 8])); // Dirt tile (grass) + surface_positions.push((position, ("grass").to_string())); + } else { + commands.command_scope(|mut cmd| { + FloorTilePrefab::dirt(position).spawn(&mut cmd); + }); + local_tilemap_updates + .insert(pos_ivec, (1, false, true, false, 85, [0; 8])); + // Dirt tile + } + } else { + commands.command_scope(|mut cmd| { + FloorTilePrefab::air(position).spawn(&mut cmd); + }); + local_tilemap_updates.insert(pos_ivec, (0, true, false, true, 0, [0; 8])); + // Air tile + } + } + } + } + + // Add our local updates to the global mutexes + { + let mut tilemap_guard = tilemap_updates.lock().unwrap(); + for (pos, data) in local_tilemap_updates { + tilemap_guard.insert(pos, data); + } + } + + // Store forrestry event for this chunk + forrestry_events.lock().unwrap().push(ChunkForrestryEvent { + chunk_position: chunk_pos, + floor_tiles: surface_positions, + }); + }); + + for (pos, data) in tilemap_updates.into_inner().unwrap() { + tilemap.floor_tiles.insert(pos, data); + occlusion_event_writer.write(TileOcclusionEvent { tile_position: pos }); + } + + // Send all forrestry events + for event in forrestry_events.into_inner().unwrap() { + forrestry_event_writer.write(event); + } + + if !is_empty { + println!("{} terrain chunks loaded in {:.2?}", count, start.elapsed()); + } +} + +pub fn generate_chunk_weathering_and_precipitation(// mut commands: Commands, + // mut events: EventReader, + // mut chunk_map: ResMut, + // mut tilemap: ResMut, +) { + // TODO: Generate weathering and precipitation + // Temperature and humidity + // Erosion + // Hadley lines/cells etc + // Biomes +} diff --git a/src/world/mod.rs b/src/world/mod.rs new file mode 100644 index 0000000..01106c6 --- /dev/null +++ b/src/world/mod.rs @@ -0,0 +1,78 @@ +use crate::{ + camera, + world::generation::{ + generate_chunk_fauna, generate_chunk_foliage, generate_chunk_forrestry, + generate_chunk_terrain, generate_chunk_weathering_and_precipitation, + }, +}; +use bevy::prelude::*; + +pub mod chunks; +pub mod generation; +pub mod textures; +pub mod tiles; + +// Re-export commonly used items +pub use chunks::management::*; +pub use textures::management::*; +pub use tiles::{prefabs::*, rendering::*, visibility::*}; + +// Plugin +pub struct WorldPlugin; + +impl Plugin for WorldPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_systems(Startup, (setup_chunk_system, setup_initial_chunks).chain()) + .add_systems( + FixedUpdate, + ( + ( + handle_chunk_events, + generate_chunk_terrain, + generate_chunk_weathering_and_precipitation, + generate_chunk_forrestry, + generate_chunk_foliage, + generate_chunk_fauna, + ) + .chain(), + chunkmap_despawn_timer_system, + ), + ) + .add_systems( + Update, + ( + compute_visibility_of_game_entities, + ( + handle_tile_occlusion_updates, + build_quilted_terrain_sprites, + update_tile_visibility.run_if( + |cwss: Res, + camera_moved: Res| { + cwss.state == tiles::TerrainSpriteState::RenderReady + || camera_moved.0 + }, + ), + ) + .chain(), + ), + ); + } +} + +fn setup_initial_chunks(mut event_writer: EventWriter) { + for x in -5..=5 { + for y in -5..=5 { + event_writer.write(GenerateChunkEvent { + chunk_position: IVec2::new(x, y), + }); + } + } +} diff --git a/src/world/textures/management.rs b/src/world/textures/management.rs new file mode 100644 index 0000000..37eb195 --- /dev/null +++ b/src/world/textures/management.rs @@ -0,0 +1,85 @@ +use bevy::prelude::*; +use bevy_platform::collections::HashMap; +use bevy_platform::time::Instant; + +pub const FLOOR_ID_OFFSET: u32 = 0; +pub const FIXTURE_ID_OFFSET: u32 = 500000; + +pub const DEFAULT_TEXTURE: &str = "default.png"; + +#[derive(Resource)] +pub struct Textures { + pub handles: HashMap>, +} + +#[derive(Resource)] +pub struct TextureIDs { + pub refs: HashMap, +} + +const GRASS_FLOOR_PATH: &str = "grass_floor.png"; +const DIRT_FLOOR_PATH: &str = "dirt_floor.png"; +const ROCK_FLOOR_PATH: &str = "rock_floor.png"; +const BEDROCK_FLOOR_PATH: &str = "bedrock_floor.png"; +const SKY_PATH: &str = "sky.png"; +const DIRT_WALL_PATH: &str = "dirt_wall.png"; +const ROCK_WALL_PATH: &str = "rock_wall.png"; +const BEDROCK_WALL_PATH: &str = "bedrock_wall.png"; +const LOG_PATH: &str = "log.png"; +const LEAVES_PATH: &str = "leaves.png"; + +pub fn initialize_textures(mut commands: Commands, asset_server: Res) { + println!(""); + println!("####################"); + let start = Instant::now(); + let mut textures: HashMap> = HashMap::new(); + let mut texture_ids: HashMap = HashMap::new(); + texture_ids.insert(u32::MAX, DEFAULT_TEXTURE.to_string()); + + texture_ids.insert(FLOOR_ID_OFFSET, SKY_PATH.to_string()); + textures.insert(SKY_PATH.to_string(), asset_server.load(SKY_PATH)); + texture_ids.insert(FLOOR_ID_OFFSET + 1, GRASS_FLOOR_PATH.to_string()); + textures.insert( + GRASS_FLOOR_PATH.to_string(), + asset_server.load(GRASS_FLOOR_PATH), + ); + texture_ids.insert(FLOOR_ID_OFFSET + 2, DIRT_FLOOR_PATH.to_string()); + textures.insert( + DIRT_FLOOR_PATH.to_string(), + asset_server.load(DIRT_FLOOR_PATH), + ); + texture_ids.insert(FLOOR_ID_OFFSET + 3, ROCK_FLOOR_PATH.to_string()); + textures.insert( + ROCK_FLOOR_PATH.to_string(), + asset_server.load(ROCK_FLOOR_PATH), + ); + texture_ids.insert(FLOOR_ID_OFFSET + 4, BEDROCK_FLOOR_PATH.to_string()); + textures.insert( + BEDROCK_FLOOR_PATH.to_string(), + asset_server.load(BEDROCK_FLOOR_PATH), + ); + + texture_ids.insert(FIXTURE_ID_OFFSET + 1, DIRT_WALL_PATH.to_string()); + textures.insert( + DIRT_WALL_PATH.to_string(), + asset_server.load(DIRT_WALL_PATH), + ); + texture_ids.insert(FIXTURE_ID_OFFSET + 2, ROCK_WALL_PATH.to_string()); + textures.insert( + ROCK_WALL_PATH.to_string(), + asset_server.load(ROCK_WALL_PATH), + ); + texture_ids.insert(FIXTURE_ID_OFFSET + 3, BEDROCK_WALL_PATH.to_string()); + textures.insert( + BEDROCK_WALL_PATH.to_string(), + asset_server.load(BEDROCK_WALL_PATH), + ); + texture_ids.insert(FIXTURE_ID_OFFSET + 4, LOG_PATH.to_string()); + textures.insert(LOG_PATH.to_string(), asset_server.load(LOG_PATH)); + texture_ids.insert(FIXTURE_ID_OFFSET + 5, LEAVES_PATH.to_string()); + textures.insert(LEAVES_PATH.to_string(), asset_server.load(LEAVES_PATH)); + + commands.insert_resource(Textures { handles: textures }); + commands.insert_resource(TextureIDs { refs: texture_ids }); + println!("Textures initialized in {:.2?}", start.elapsed()); +} diff --git a/src/world/textures/mod.rs b/src/world/textures/mod.rs new file mode 100644 index 0000000..5679e09 --- /dev/null +++ b/src/world/textures/mod.rs @@ -0,0 +1,3 @@ +pub mod management; + +pub use management::*; diff --git a/src/world/tiles/components.rs b/src/world/tiles/components.rs new file mode 100644 index 0000000..2a1b114 --- /dev/null +++ b/src/world/tiles/components.rs @@ -0,0 +1,44 @@ +use bevy::prelude::*; + +#[derive(Component, Clone)] +pub struct FloorTile { + pub id: u32, + pub opaque: bool, + pub walkable: bool, + pub astar_weight: u8, + pub visible_range: [u32; 8], +} + +impl Default for FloorTile { + fn default() -> Self { + Self { + id: 0, + opaque: true, + walkable: true, + astar_weight: 0, + visible_range: [0; 8], + } + } +} + +#[derive(Component, Clone)] +pub struct FixtureTile { + pub id: u32, + pub solid: bool, + pub visible_range: [u32; 8], +} + +impl Default for FixtureTile { + fn default() -> Self { + Self { + id: 0, + solid: true, + visible_range: [0; 8], + } + } +} + +#[derive(Component)] +pub struct TileState { + pub timer: Timer, +} diff --git a/src/world/tiles/mod.rs b/src/world/tiles/mod.rs new file mode 100644 index 0000000..74b63cc --- /dev/null +++ b/src/world/tiles/mod.rs @@ -0,0 +1,11 @@ +pub mod components; +pub mod prefabs; +pub mod rendering; +pub mod tilemap; +pub mod visibility; + +pub use components::*; +pub use prefabs::*; +pub use rendering::*; +pub use tilemap::*; +pub use visibility::*; diff --git a/src/world/tiles/prefabs.rs b/src/world/tiles/prefabs.rs new file mode 100644 index 0000000..36fbd6e --- /dev/null +++ b/src/world/tiles/prefabs.rs @@ -0,0 +1,166 @@ +use bevy::prelude::*; + +use crate::world::{ + tiles::{FixtureTile, FloorTile, TileState}, + FIXTURE_ID_OFFSET, +}; + +#[derive(Bundle)] +pub struct FloorTilePrefab { + transform: Transform, + tile: FloorTile, + tile_state: TileState, + visibility: Visibility, +} + +impl FloorTilePrefab { + pub fn grass(position: Vec3) -> Self { + FloorTilePrefab { + transform: Transform::from_translation(position), + tile: FloorTile { + id: 1, + astar_weight: 100, + ..Default::default() + }, + tile_state: TileState { + timer: Timer::from_seconds(1.0, TimerMode::Repeating), + }, + visibility: Visibility::Hidden, + } + } + + pub fn dirt(position: Vec3) -> Self { + FloorTilePrefab { + transform: Transform::from_translation(position), + tile: FloorTile { + id: 2, + astar_weight: 85, + ..Default::default() + }, + tile_state: TileState { + timer: Timer::from_seconds(1.0, TimerMode::Repeating), + }, + visibility: Visibility::Hidden, + } + } + + pub fn rock(position: Vec3) -> Self { + FloorTilePrefab { + transform: Transform::from_translation(position), + tile: FloorTile { + id: 3, + astar_weight: 50, + ..Default::default() + }, + tile_state: TileState { + timer: Timer::from_seconds(1.0, TimerMode::Repeating), + }, + visibility: Visibility::Hidden, + } + } + + pub fn air(position: Vec3) -> Self { + FloorTilePrefab { + transform: Transform::from_translation(position), + tile: FloorTile { + id: 0, + opaque: false, + walkable: false, + ..Default::default() + }, + tile_state: TileState { + timer: Timer::from_seconds(1.0, TimerMode::Repeating), + }, + visibility: Visibility::Hidden, + } + } + + pub fn bedrock(position: Vec3) -> Self { + FloorTilePrefab { + transform: Transform::from_translation(position), + tile: FloorTile { + id: 4, + astar_weight: 150, + ..Default::default() + }, + tile_state: TileState { + timer: Timer::from_seconds(1.0, TimerMode::Repeating), + }, + visibility: Visibility::Hidden, + } + } + + pub fn spawn(self, commands: &mut Commands) { + commands.spawn((self.tile, self.transform, self.tile_state, self.visibility)); + } +} + +#[derive(Bundle)] +pub struct FixtureTilePrefab { + transform: Transform, + tile: FixtureTile, + visibility: Visibility, +} + +impl FixtureTilePrefab { + pub fn dirt_wall(position: Vec3) -> Self { + FixtureTilePrefab { + transform: Transform::from_translation(position), + tile: FixtureTile { + id: FIXTURE_ID_OFFSET + 1, + ..Default::default() + }, + visibility: Visibility::Hidden, + } + } + + pub fn rock_wall(position: Vec3) -> Self { + FixtureTilePrefab { + transform: Transform::from_translation(position), + tile: FixtureTile { + id: FIXTURE_ID_OFFSET + 2, + ..Default::default() + }, + visibility: Visibility::Hidden, + } + } + + pub fn log(position: Vec3) -> Self { + FixtureTilePrefab { + transform: Transform::from_translation(position), + tile: FixtureTile { + id: FIXTURE_ID_OFFSET + 4, + ..Default::default() + }, + visibility: Visibility::Hidden, + } + } + + pub fn leaves(position: Vec3) -> Self { + FixtureTilePrefab { + transform: Transform::from_translation(position), + tile: FixtureTile { + id: FIXTURE_ID_OFFSET + 5, + ..Default::default() + }, + visibility: Visibility::Hidden, + } + } + + pub fn bedrock_wall(position: Vec3) -> Self { + FixtureTilePrefab { + transform: Transform::from_translation(position), + tile: FixtureTile { + id: 0, + ..Default::default() + }, + visibility: Visibility::Hidden, + } + } + + pub fn spawn(self, commands: &mut Commands) -> Entity { + commands + .spawn((self.tile, self.transform, self.visibility)) + .id() + } +} diff --git a/src/tiles.rs b/src/world/tiles/rendering.rs similarity index 53% rename from src/tiles.rs rename to src/world/tiles/rendering.rs index b9de973..a00db20 100644 --- a/src/tiles.rs +++ b/src/world/tiles/rendering.rs @@ -1,93 +1,13 @@ -use crate::tile::{FixtureTile, FloorTile, TileState}; -use crate::tilemap::Z_BELOW; -use crate::{constants::*, tilemap}; -use bevy::asset::RenderAssetUsages; -use bevy::prelude::*; -use bevy::render::render_resource; -use bevy_platform::collections::hash_map::HashMap; +use bevy::{asset::RenderAssetUsages, prelude::*, render::render_resource}; +use bevy_platform::collections::HashMap; +use bevy_platform::sync::Mutex; +use bevy_platform::time::Instant; use rayon::prelude::*; -#[derive(Resource)] -pub struct Textures { - pub handles: HashMap>, -} - -#[derive(Resource)] -pub struct TextureIDs { - pub refs: HashMap, -} - -const FLOOR_ID_OFFSET: u32 = 0; -const FIXTURE_ID_OFFSET: u32 = 500000; - -pub const DEFAULT_TEXTURE: &str = "default.png"; - -const GRASS_FLOOR_PATH: &str = "grass_floor.png"; -const DIRT_FLOOR_PATH: &str = "dirt_floor.png"; -const ROCK_FLOOR_PATH: &str = "rock_floor.png"; -const BEDROCK_FLOOR_PATH: &str = "bedrock_floor.png"; -const SKY_PATH: &str = "sky.png"; -const DIRT_WALL_PATH: &str = "dirt_wall.png"; -const ROCK_WALL_PATH: &str = "rock_wall.png"; -const BEDROCK_WALL_PATH: &str = "bedrock_wall.png"; -const LOG_PATH: &str = "log.png"; -const LEAVES_PATH: &str = "leaves.png"; - -pub fn initialize_textures(mut commands: Commands, asset_server: Res) { - println!(""); - println!("####################"); - let start = Instant::now(); - let mut textures: HashMap> = HashMap::new(); - let mut texture_ids: HashMap = HashMap::new(); - texture_ids.insert(u32::MAX, DEFAULT_TEXTURE.to_string()); - - texture_ids.insert(FLOOR_ID_OFFSET, SKY_PATH.to_string()); - textures.insert(SKY_PATH.to_string(), asset_server.load(SKY_PATH)); - texture_ids.insert(FLOOR_ID_OFFSET + 1, GRASS_FLOOR_PATH.to_string()); - textures.insert( - GRASS_FLOOR_PATH.to_string(), - asset_server.load(GRASS_FLOOR_PATH), - ); - texture_ids.insert(FLOOR_ID_OFFSET + 2, DIRT_FLOOR_PATH.to_string()); - textures.insert( - DIRT_FLOOR_PATH.to_string(), - asset_server.load(DIRT_FLOOR_PATH), - ); - texture_ids.insert(FLOOR_ID_OFFSET + 3, ROCK_FLOOR_PATH.to_string()); - textures.insert( - ROCK_FLOOR_PATH.to_string(), - asset_server.load(ROCK_FLOOR_PATH), - ); - texture_ids.insert(FLOOR_ID_OFFSET + 4, BEDROCK_FLOOR_PATH.to_string()); - textures.insert( - BEDROCK_FLOOR_PATH.to_string(), - asset_server.load(BEDROCK_FLOOR_PATH), - ); - - texture_ids.insert(FIXTURE_ID_OFFSET + 1, DIRT_WALL_PATH.to_string()); - textures.insert( - DIRT_WALL_PATH.to_string(), - asset_server.load(DIRT_WALL_PATH), - ); - texture_ids.insert(FIXTURE_ID_OFFSET + 2, ROCK_WALL_PATH.to_string()); - textures.insert( - ROCK_WALL_PATH.to_string(), - asset_server.load(ROCK_WALL_PATH), - ); - texture_ids.insert(FIXTURE_ID_OFFSET + 3, BEDROCK_WALL_PATH.to_string()); - textures.insert( - BEDROCK_WALL_PATH.to_string(), - asset_server.load(BEDROCK_WALL_PATH), - ); - texture_ids.insert(FIXTURE_ID_OFFSET + 4, LOG_PATH.to_string()); - textures.insert(LOG_PATH.to_string(), asset_server.load(LOG_PATH)); - texture_ids.insert(FIXTURE_ID_OFFSET + 5, LEAVES_PATH.to_string()); - textures.insert(LEAVES_PATH.to_string(), asset_server.load(LEAVES_PATH)); - - commands.insert_resource(Textures { handles: textures }); - commands.insert_resource(TextureIDs { refs: texture_ids }); - println!("Textures initialized in {:.2?}", start.elapsed()); -} +use crate::{ + constants::{PIXEL_RATIO, TILE_PIXELS, TILE_SIZE}, + world::{tiles::FloorTile, TextureIDs, Textures, Z_BELOW, Z_TOTAL}, +}; #[derive(Debug, PartialEq)] pub enum TerrainSpriteState { @@ -102,10 +22,6 @@ pub struct CurrentWorldSpriteState { pub state: TerrainSpriteState, } -use std::process::exit; -use std::sync::Mutex; -use std::time::Instant; - #[derive(Component)] pub struct TerrainSprite { pub z_index: usize, // Store the z-index this sprite belongs to (as world_Z) @@ -158,7 +74,7 @@ pub fn build_quilted_terrain_sprites( for (floortile, transform) in query_tiles.iter() { let position = Vec2::new(transform.translation.x, transform.translation.y); - for z_index in 0..=tilemap::Z_TOTAL as usize { + for z_index in 0..=Z_TOTAL as usize { if (floortile.visible_range[z_index / 32] & (1 << (z_index % 32) as u32)) != 0 { tiles_by_z .entry(z_index) @@ -384,185 +300,3 @@ fn blit_texture_with_alpha( } } } - -// This system handles updating quilted sprites when the world changes -// pub fn update_quilts_on_world_change( -// mut commands: Commands, -// mut quilt_cache: ResMut, -// mut cwss: ResMut, -// Add any resources or queries that indicate world changes -// For example, if you have a WorldChangeEvent: -// mut world_changes: EventReader, -// ) { -// Example: Check for world changes -// if !world_changes.is_empty() { -// for event in world_changes.iter() { -// quilt_cache.dirty_indices.push(event.z_index); -// } -// cwss.state = WorldSpriteState::WaitingForRender; -// } - -// Alternatively, if you have specific systems that modify the world, -// you could have them set cwss.state = WorldSpriteState::WaitingForRender -// and add affected z-indices to quilt_cache.dirty_indices -// } - -#[derive(Bundle)] -pub struct FloorTilePrefab { - transform: Transform, - tile: FloorTile, - tile_state: TileState, - visibility: Visibility, -} - -impl FloorTilePrefab { - pub fn grass(position: Vec3) -> Self { - FloorTilePrefab { - transform: Transform::from_translation(position), - tile: FloorTile { - id: 1, - astar_weight: 100, - ..Default::default() - }, - tile_state: TileState { - timer: Timer::from_seconds(1.0, TimerMode::Repeating), - }, - visibility: Visibility::Hidden, - } - } - - pub fn dirt(position: Vec3) -> Self { - FloorTilePrefab { - transform: Transform::from_translation(position), - tile: FloorTile { - id: 2, - astar_weight: 85, - ..Default::default() - }, - tile_state: TileState { - timer: Timer::from_seconds(1.0, TimerMode::Repeating), - }, - visibility: Visibility::Hidden, - } - } - - pub fn rock(position: Vec3) -> Self { - FloorTilePrefab { - transform: Transform::from_translation(position), - tile: FloorTile { - id: 3, - astar_weight: 50, - ..Default::default() - }, - tile_state: TileState { - timer: Timer::from_seconds(1.0, TimerMode::Repeating), - }, - visibility: Visibility::Hidden, - } - } - - pub fn air(position: Vec3) -> Self { - FloorTilePrefab { - transform: Transform::from_translation(position), - tile: FloorTile { - id: 0, - opaque: false, - walkable: false, - ..Default::default() - }, - tile_state: TileState { - timer: Timer::from_seconds(1.0, TimerMode::Repeating), - }, - visibility: Visibility::Hidden, - } - } - - pub fn bedrock(position: Vec3) -> Self { - FloorTilePrefab { - transform: Transform::from_translation(position), - tile: FloorTile { - id: 4, - astar_weight: 150, - ..Default::default() - }, - tile_state: TileState { - timer: Timer::from_seconds(1.0, TimerMode::Repeating), - }, - visibility: Visibility::Hidden, - } - } - - pub fn spawn(self, commands: &mut Commands) { - commands.spawn((self.tile, self.transform, self.tile_state, self.visibility)); - } -} - -#[derive(Bundle)] -pub struct FixtureTilePrefab { - transform: Transform, - tile: FixtureTile, - visibility: Visibility, -} - -impl FixtureTilePrefab { - pub fn dirt_wall(position: Vec3) -> Self { - FixtureTilePrefab { - transform: Transform::from_translation(position), - tile: FixtureTile { - id: FIXTURE_ID_OFFSET + 1, - ..Default::default() - }, - visibility: Visibility::Hidden, - } - } - - pub fn rock_wall(position: Vec3) -> Self { - FixtureTilePrefab { - transform: Transform::from_translation(position), - tile: FixtureTile { - id: FIXTURE_ID_OFFSET + 2, - ..Default::default() - }, - visibility: Visibility::Hidden, - } - } - - pub fn log(position: Vec3) -> Self { - FixtureTilePrefab { - transform: Transform::from_translation(position), - tile: FixtureTile { - id: FIXTURE_ID_OFFSET + 4, - ..Default::default() - }, - visibility: Visibility::Hidden, - } - } - - pub fn leaves(position: Vec3) -> Self { - FixtureTilePrefab { - transform: Transform::from_translation(position), - tile: FixtureTile { - id: FIXTURE_ID_OFFSET + 5, - ..Default::default() - }, - visibility: Visibility::Hidden, - } - } - - pub fn bedrock_wall(position: Vec3) -> Self { - FixtureTilePrefab { - transform: Transform::from_translation(position), - tile: FixtureTile { - id: 0, - ..Default::default() - }, - visibility: Visibility::Hidden, - } - } - - pub fn spawn(self, commands: &mut Commands) -> Entity { - commands - .spawn((self.tile, self.transform, self.visibility)) - .id() - } -} diff --git a/src/world/tiles/tilemap.rs b/src/world/tiles/tilemap.rs new file mode 100644 index 0000000..b35b8a9 --- /dev/null +++ b/src/world/tiles/tilemap.rs @@ -0,0 +1,8 @@ +use bevy::prelude::*; +use bevy_platform::collections::hash_map::HashMap; + +#[derive(Resource, Default, Clone)] +pub struct TileMap { + pub floor_tiles: HashMap, //id, canStandIn, canStandOn, visiblyTransparent, astar_weight, visible_range + pub fixture_tiles: HashMap, // id, canStandIn, canStandOn, visible_range +} diff --git a/src/world/tiles/visibility.rs b/src/world/tiles/visibility.rs new file mode 100644 index 0000000..849f529 --- /dev/null +++ b/src/world/tiles/visibility.rs @@ -0,0 +1,181 @@ +use crate::{ + constants::{ITILE_SIZE, TILE_SIZE}, + game, + world::{ + chunks::{Z_ABOVE, Z_BELOW, Z_TOTAL}, + tiles::{CurrentWorldSpriteState, FloorTile, TerrainSprite, TerrainSpriteState, TileMap}, + }, +}; +use bevy::prelude::*; +use bevy_platform::collections::HashMap; +use bevy_platform::time::Instant; + +#[derive(Component)] +pub struct VisibleGameEntity; + +pub fn compute_visibility_of_game_entities( + mut query: Query<(&Transform, &mut Visibility, &mut Sprite), With>, + z_index: Res, +) { + query + .par_iter_mut() + .for_each(|(transform, mut visibility, mut sprite)| { + let entity_z = (transform.translation.z / TILE_SIZE) - 1.0; + // if same z-level, always visible + if z_index.0 == entity_z { + sprite.color = Color::WHITE; + sprite.color.set_alpha(1.0); + *visibility = Visibility::Visible; + return; + }; + + // if entity too low, saturation does not matter + if z_index.0 - entity_z > 8.0 { + *visibility = Visibility::Hidden; + return; + } + // if entity too high, saturation does not matter + if entity_z > z_index.0 { + *visibility = Visibility::Hidden; + return; + } + + // if visible, calculate saturation + *visibility = Visibility::Visible; + let saturation = + ((z_index.0 - (transform.translation.z / TILE_SIZE) + 1.) / 8.0).clamp(0.0, 1.0); + + sprite.color = Color::hsv(194.7, saturation, 1.0 - (saturation / 2.0)); + + sprite.color.set_alpha(1.0 - saturation); + }); +} + +#[derive(Event)] +pub struct TileOcclusionEvent { + pub tile_position: IVec3, +} + +pub fn handle_tile_occlusion_updates( + mut tilemap: ResMut, + mut floor_tiles: Query<(Entity, &mut FloorTile, &Transform)>, + mut cwss: ResMut, + mut events: EventReader, +) { + let start = Instant::now(); + + let count = events.len(); + + // Process events in parallel + let updates: Vec<(IVec3, [u32; 8])> = events + .par_read() + .into_iter() + .map(|event| { + let pos = event.0.tile_position; + let visibility = calculate_visibility(pos, &tilemap); + (pos, visibility) + }) + .collect(); + + // Map updates for efficient lookup + let update_map: HashMap = updates.into_iter().collect(); + + // Update only the relevant FloorTile components + for (_, mut tile, pos) in floor_tiles.iter_mut() { + if let Some(visibility) = update_map.get(&pos.translation.as_ivec3()) { + tile.visible_range = *visibility; + if let Some(tile_data) = tilemap.floor_tiles.get_mut(&pos.translation.as_ivec3()) { + tile_data.5 = *visibility; + } + } + } + if count > 0 { + cwss.state = TerrainSpriteState::WaitingForRender; + println!( + "Tile occlusion calculated for {} tiles in {:.2?}", + count, + start.elapsed() + ); + } +} + +pub fn calculate_visibility(pos: IVec3, tilemap: &TileMap) -> [u32; 8] { + let mut visible_range = [0u32; 8]; + + for mut camera_z in -Z_BELOW as i32..=Z_ABOVE as i32 { + camera_z *= ITILE_SIZE; + let mut is_visible = false; + + if pos.z > camera_z { + continue; + } + + let mut is_occluded = false; + + let v_check_height: i32 = Z_TOTAL as i32 - (camera_z / ITILE_SIZE) + Z_BELOW as i32 + 1; + 'vertical_check: for z_offset in 1..v_check_height { + let above_pos = IVec3::new(pos.x, pos.y, pos.z + (z_offset * ITILE_SIZE)); + if above_pos.z <= camera_z { + if let Some(&(_, _, _, visibly_transparent, _, _)) = + tilemap.floor_tiles.get(&above_pos) + { + if !visibly_transparent { + is_occluded = true; + break 'vertical_check; + } + } + } else { + break 'vertical_check; + } + } + if !is_occluded { + 'neighbor_check: for x_offset in -1..=1 { + for y_offset in -1..=1 { + for z_offset in 0..=1 { + if x_offset == 0 && y_offset == 0 && z_offset == 0 { + continue; + } + let neighbor_pos = IVec3::new( + pos.x + x_offset * ITILE_SIZE, + pos.y + y_offset * ITILE_SIZE, + pos.z + z_offset * ITILE_SIZE, + ); + + if let Some(&(id, _, _, _, _, _)) = tilemap.floor_tiles.get(&neighbor_pos) { + if id == 0 { + // id 0 = air tile + is_visible = true; + break 'neighbor_check; + } + } + } + } + } + } + + if is_visible { + let z2 = ((camera_z / ITILE_SIZE) + Z_BELOW as i32) as usize; + visible_range[z2 / 32] |= 1 << ((z2 % 32) as u32); + } + } + + visible_range +} + +pub fn update_tile_visibility( + z_index: Res, + mut query: Query<(&TerrainSprite, &mut Visibility)>, + mut cwss: ResMut, +) { + let now = Instant::now(); + + for (terrain_sprite, mut visibility) in query.iter_mut() { + *visibility = if terrain_sprite.z_index == ((z_index.0 + Z_BELOW) as usize) { + Visibility::Visible + } else { + Visibility::Hidden + }; + } + cwss.state = TerrainSpriteState::Inactive; + println!("Visibility update: {:.2?}", now.elapsed()); +}