use bevy::prelude::*; use rustc_hash::FxHashMap; use crate::constants::ITILE_SIZE; /// Packed floor tile data for efficient storage. ~35 bytes vs 76 bytes tuple. #[derive(Clone, Copy, Debug)] pub struct FloorTileData { pub id: u8, /// bit0=can_stand_in, bit1=can_stand_on, bit2=visibly_transparent pub flags: u8, pub astar_weight: u8, pub visible_range: [u32; 8], } impl Default for FloorTileData { fn default() -> Self { Self { id: 0, flags: 0b001, astar_weight: 0, visible_range: [0; 8], } } } impl FloorTileData { pub fn new( id: u8, can_stand_in: bool, can_stand_on: bool, visibly_transparent: bool, astar_weight: u8, visible_range: [u32; 8], ) -> Self { let mut flags = 0u8; if can_stand_in { flags |= 0b001; } if can_stand_on { flags |= 0b010; } if visibly_transparent { flags |= 0b100; } Self { id, flags, astar_weight, visible_range, } } #[inline] pub fn can_stand_in(&self) -> bool { self.flags & 0b001 != 0 } #[inline] pub fn can_stand_on(&self) -> bool { self.flags & 0b010 != 0 } #[inline] pub fn visibly_transparent(&self) -> bool { self.flags & 0b100 != 0 } #[inline] pub fn set_can_stand_in(&mut self, value: bool) { if value { self.flags |= 0b001; } else { self.flags &= !0b001; } } #[inline] pub fn set_can_stand_on(&mut self, value: bool) { if value { self.flags |= 0b010; } else { self.flags &= !0b010; } } #[inline] pub fn set_visibly_transparent(&mut self, value: bool) { if value { self.flags |= 0b100; } else { self.flags &= !0b100; } } } /// Packed fixture tile data. ~18 bytes vs 48 bytes tuple. #[derive(Clone, Copy, Debug)] pub struct FixtureTileData { pub id: u8, /// bit0=can_stand_in, bit1=can_stand_on pub flags: u8, pub visible_range: [u32; 8], } impl Default for FixtureTileData { fn default() -> Self { Self { id: 0, flags: 0, visible_range: [0; 8], } } } impl FixtureTileData { pub fn new(id: u8, can_stand_in: bool, can_stand_on: bool, visible_range: [u32; 8]) -> Self { let mut flags = 0u8; if can_stand_in { flags |= 0b001; } if can_stand_on { flags |= 0b010; } Self { id, flags, visible_range, } } #[inline] pub fn can_stand_in(&self) -> bool { self.flags & 0b001 != 0 } #[inline] pub fn can_stand_on(&self) -> bool { self.flags & 0b010 != 0 } } /// Tile map using FxHashMap for fast lookups. No Arc wrapper - single-threaded access. #[derive(Resource, Default)] pub struct TileMap { pub floor_tiles: FxHashMap, pub fixture_tiles: FxHashMap, pub item_tiles: FxHashMap>, } impl TileMap { pub fn new() -> Self { Self::default() } #[inline] pub fn get_floor(&self, pos: &IVec3) -> Option<&FloorTileData> { self.floor_tiles.get(pos) } #[inline] pub fn get_fixture(&self, pos: &IVec3) -> Option<&FixtureTileData> { self.fixture_tiles.get(pos) } #[inline] pub fn has_floor(&self, pos: &IVec3) -> bool { self.floor_tiles.contains_key(pos) } #[inline] pub fn has_fixture(&self, pos: &IVec3) -> bool { self.fixture_tiles.contains_key(pos) } #[inline] pub fn insert_floor(&mut self, pos: IVec3, tile: FloorTileData) { self.floor_tiles.insert(pos, tile); } #[inline] pub fn insert_fixture(&mut self, pos: IVec3, tile: FixtureTileData) { self.fixture_tiles.insert(pos, tile); } #[inline] pub fn insert_item(&mut self, pos: IVec3, entity_id: u32) { self.item_tiles.entry(pos).or_default().push(entity_id); } #[inline] pub fn remove_item(&mut self, pos: &IVec3) -> Option> { self.item_tiles.remove(pos) } #[inline] pub fn get_floor_mut(&mut self, pos: &IVec3) -> Option<&mut FloorTileData> { self.floor_tiles.get_mut(pos) } } /// Bit-packed bounding-box snapshot for async pathfinding. /// 1 bit per tile = ~6KB for 50,000 tiles vs HashMap overhead. /// Must be Send+Sync — no RefCell, no Arc. #[derive(Clone, Debug)] pub struct StandableBitGrid { pub origin: IVec3, pub size: UVec3, pub bits: Vec, } impl StandableBitGrid { /// Create a bit-grid snapshot of all standable tiles within bounding box. /// origin: min corner (inclusive), snapped to ITILE_SIZE /// size: dimensions in tiles (not pixels) pub fn new(origin: IVec3, size: UVec3, tilemap: &TileMap) -> Self { let total_bits = (size.x * size.y * size.z) as usize; let words = (total_bits + 63) / 64; let mut bits = vec![0u64; words]; for bz in 0..size.z { for by in 0..size.y { for bx in 0..size.x { let pos = IVec3::new( origin.x + (bx as i32) * ITILE_SIZE, origin.y + (by as i32) * ITILE_SIZE, origin.z + (bz as i32) * ITILE_SIZE, ); if Self::tile_is_standable(tilemap, pos) { let idx = ((bz * size.y * size.x) + (by * size.x) + bx) as usize; bits[idx / 64] |= 1u64 << (idx % 64); } } } } Self { origin, size, bits } } /// Standable check using TileMap (mirrors pathfinding.rs::is_standable_tile) #[inline] fn tile_is_standable(tilemap: &TileMap, pos: IVec3) -> bool { let can_stand_in_tile = tilemap .floor_tiles .get(&pos) .map(|t| t.can_stand_in()) .unwrap_or(false); let can_stand_in_fixture = tilemap .fixture_tiles .get(&pos) .map(|t| t.can_stand_in()) .unwrap_or(false); let pos_below = pos - IVec3::new(0, 0, ITILE_SIZE); let can_stand_on_tile_below = tilemap .floor_tiles .get(&pos_below) .map(|t| t.can_stand_on()) .unwrap_or(false); let can_stand_on_fixture_below = tilemap .fixture_tiles .get(&pos_below) .map(|t| t.can_stand_on()) .unwrap_or(false); (can_stand_in_tile || can_stand_in_fixture) && (can_stand_on_tile_below || can_stand_on_fixture_below) } /// O(1) standable check using bit-grid coordinates. #[inline] pub fn is_standable_at(&self, bx: u32, by: u32, bz: u32) -> bool { if bx >= self.size.x || by >= self.size.y || bz >= self.size.z { return false; } let idx = ((bz * self.size.y * self.size.x) + (by * self.size.x) + bx) as usize; self.bits[idx / 64] & (1u64 << (idx % 64)) != 0 } /// Convert IVec3 world position to bit-grid coordinates. /// Returns None if position is outside the grid bounds. #[inline] pub fn to_bit_coords(&self, pos: IVec3) -> Option<(u32, u32, u32)> { let local = pos - self.origin; if local.x < 0 || local.y < 0 || local.z < 0 { return None; } let bx = (local.x / ITILE_SIZE) as u32; let by = (local.y / ITILE_SIZE) as u32; let bz = (local.z / ITILE_SIZE) as u32; if bx >= self.size.x || by >= self.size.y || bz >= self.size.z { return None; } Some((bx, by, bz)) } }