Skip to main content

wde_terrain_grid/core/
grid.rs

1use bevy::prelude::*;
2use std::collections::HashMap;
3use wde_terrain::prelude::{CHUNK_SIZE, ChunkPos};
4
5use crate::prelude::GridEntity;
6
7/// The number of subdivisions per chunk in the grid system (number of grid cells per chunk).
8pub const CHUNK_GRID_SUBDIVISIONS: u32 = CHUNK_SIZE as u32 / 2;
9/// The size of a single tile in the grid system.
10pub const TILE_SIZE: f32 = CHUNK_SIZE / CHUNK_GRID_SUBDIVISIONS as f32;
11
12/// The position of a chunk in the grid, represented as (x, z) coordinates.
13pub type GridChunkPos = IVec2;
14/// The local position of a tile within a chunk, represented as (x, z) coordinates.
15pub type GridLocalPos = (u32, u32);
16/// The full position in the grid
17pub type GridTilePos = (GridChunkPos, GridLocalPos);
18
19// Event when a grid entity is placed, moved, or removed
20#[derive(Debug, Clone, Message)]
21pub enum GridEntityEvent {
22    Placed {
23        entity: Entity,
24        grid_entity: GridEntity
25    },
26    Removed {
27        entity: Entity
28    }
29}
30
31/// A terrain chunk that contains a list of terrain tiles.
32pub struct Chunk {
33    /// List of terrain tiles that belong to this chunk.
34    tiles: Vec<Option<Entity>>,
35    /// Pointers from the entities to their position in the grid.
36    entity_to_tile: HashMap<Entity, Vec<GridLocalPos>>
37}
38impl Default for Chunk {
39    fn default() -> Self {
40        Chunk {
41            tiles: vec![None; (CHUNK_GRID_SUBDIVISIONS * CHUNK_GRID_SUBDIVISIONS * 4) as usize],
42            entity_to_tile: HashMap::new()
43        }
44    }
45}
46impl Chunk {
47    // Entity management methods
48    /// Sets the entity at the specified local tile position within this chunk.
49    pub fn set_entity_at(&mut self, local_pos: GridLocalPos, entity: Entity) {
50        let index = Self::local_pos_to_index(local_pos);
51        if let Some(tile) = self.tiles.get_mut(index) {
52            *tile = Some(entity);
53            self.entity_to_tile
54                .entry(entity)
55                .or_default()
56                .push(local_pos);
57        }
58    }
59    /// Gets the entity at the specified local tile position within this chunk, if it exists.
60    pub fn get_entity_at(&self, local_pos: GridLocalPos) -> Option<Entity> {
61        let index = Self::local_pos_to_index(local_pos);
62        self.tiles.get(index).and_then(|tile| *tile)
63    }
64    /// Removes the entity from all tiles it occupies in this chunk.
65    pub fn remove_entity(&mut self, entity: Entity) {
66        if let Some(local_positions) = self.entity_to_tile.remove(&entity) {
67            for local_pos in local_positions {
68                let index = Self::local_pos_to_index(local_pos);
69                if let Some(tile) = self.tiles.get_mut(index)
70                    && tile.as_ref() == Some(&entity)
71                {
72                    *tile = None;
73                }
74            }
75        }
76    }
77
78    // Coordinate conversion methods
79    /// Converts a world position to a local tile position within this chunk.
80    pub fn get_nearest_local_tile(world_pos: Vec2) -> GridLocalPos {
81        let half_chunk = CHUNK_SIZE * 0.5;
82        let cell_size = CHUNK_SIZE / CHUNK_GRID_SUBDIVISIONS as f32;
83
84        let local_x = (world_pos.x + half_chunk).rem_euclid(CHUNK_SIZE) / cell_size;
85        let local_y = (world_pos.y + half_chunk).rem_euclid(CHUNK_SIZE) / cell_size;
86
87        (local_x.floor() as u32, local_y.floor() as u32)
88    }
89    /// Gets the center position of a tile.
90    pub fn get_tile_world_pos(pos: GridTilePos) -> Vec2 {
91        let (chunk_pos, local_pos) = pos;
92        chunk_pos.as_vec2() * CHUNK_SIZE
93            + Vec2::new(local_pos.0 as f32, local_pos.1 as f32) * TILE_SIZE
94            + TILE_SIZE / 2.0
95            - CHUNK_SIZE / 2.0
96    }
97    /// Converts a local tile position to an index in the tiles vector.
98    pub fn local_pos_to_index(local_pos: GridLocalPos) -> usize {
99        let (local_x, local_y) = local_pos;
100        (local_y * CHUNK_GRID_SUBDIVISIONS + local_x) as usize * 4
101    }
102}
103
104/// The main terrain grid resource that holds all the terrain chunks and their respective tiles.
105#[derive(Resource, Default)]
106pub struct Grid {
107    chunks: HashMap<GridChunkPos, Chunk>,
108    parent: Option<Entity>
109}
110impl Grid {
111    /// Gets the entity at the specified chunk and local tile position in the grid, if it exists.
112    pub fn get_entity(&self, chunk_pos: GridChunkPos, local_pos: GridLocalPos) -> Option<Entity> {
113        self.chunks
114            .get(&chunk_pos)
115            .and_then(|chunk| chunk.get_entity_at(local_pos))
116    }
117    /// Adds the specified entity to the grid at the positions occupied by the entity footprint.
118    pub fn set_entity(&mut self, grid_entity: &GridEntity, entity: Entity) {
119        for pos in grid_entity.footprint() {
120            let chunk = self.chunks.entry(pos.0).or_default();
121            chunk.set_entity_at(pos.1, entity);
122        }
123    }
124    /// Removes the specified entity from the grid, freeing up the tiles it occupied.
125    pub fn remove_entity(&mut self, entity: Entity) {
126        for chunk in self.chunks.values_mut() {
127            chunk.remove_entity(entity);
128        }
129    }
130    /// Check if the position and extent are valid for placement (no existing entities in the area).
131    pub fn is_area_free(&self, entity: &GridEntity) -> bool {
132        let footprint = entity.footprint();
133        for pos in footprint {
134            if self.get_entity(pos.0, pos.1).is_some() {
135                return false;
136            }
137        }
138        true
139    }
140
141    // Methods to convert between world positions and chunk/local positions
142    /// Gets the nearest chunk and tile position (without subtile direction) for a given world position.
143    pub fn get_nearest_tile(world_pos: Vec2) -> GridTilePos {
144        let half_chunk = CHUNK_SIZE * 0.5;
145        let chunk_pos = ChunkPos::new(
146            (world_pos.x + half_chunk).div_euclid(CHUNK_SIZE) as i32,
147            (world_pos.y + half_chunk).div_euclid(CHUNK_SIZE) as i32
148        );
149        let local_pos = Chunk::get_nearest_local_tile(world_pos);
150        (chunk_pos, local_pos)
151    }
152    /// Gets the center world position of a given chunk.
153    pub fn get_chunk_world_pos(chunk_pos: GridChunkPos) -> Vec2 {
154        Vec2::new(
155            chunk_pos.x as f32 * CHUNK_SIZE * 2.0,
156            chunk_pos.y as f32 * CHUNK_SIZE * 2.0
157        )
158    }
159    /// Gets the center world position for a given chunk and local tile position.
160    pub fn get_tile_world_pos(pos: GridTilePos) -> Vec2 {
161        Chunk::get_tile_world_pos(pos)
162    }
163
164    // Helper methods
165    pub fn set_parent(&mut self, parent: Entity) {
166        self.parent = Some(parent);
167    }
168    pub fn get_parent(&self) -> Option<Entity> {
169        self.parent
170    }
171}