//! Minimal Tiled-CSV map loader. //! //! Reads a bare comma-separated tile grid (Tiled's "CSV" layer export, stripped of the //! surrounding TMX/XML). Dimensions are derived from the content: columns from the first //! row, height from the row count. Tiled encodes per-tile flip/rotation in the top bits of //! each 32-bit GID, which makes flipped tiles show up as large negative decimals — those //! bits are masked off here, leaving the bare tile_id. Flip orientation is discarded for now. use crate::chunk::TileFlags; /// Top three bits hold horizontal / vertical / diagonal flip flags; the rest is the tile id. const GID_FLIP_MASK: u32 = 0x1FFF_FFFF; pub struct TileMap { pub width: u16, pub height: u16, pub tiles: Vec, // row-major, len == width * height } impl TileMap { pub fn from_csv(path: &str) -> Self { let text = std::fs::read_to_string(path) .unwrap_or_else(|e| panic!("failed to read map {path}: {e}")); let mut tiles = Vec::new(); let mut width = 0usize; let mut height = 0usize; for line in text.lines() { if line.trim().is_empty() { continue; } let mut cols = 0usize; for field in line.split(',') { let field = field.trim(); if field.is_empty() { continue; // tolerate a trailing comma } // Parse as i64 so the "negative" flipped GIDs fit, then reinterpret the low // 32 bits as u32 and strip the flip flags. let raw = field.parse::() .unwrap_or_else(|_| panic!("invalid tile value {field:?} in {path}")); let gid = (raw as u32) & GID_FLIP_MASK; tiles.push(gid as u16); cols += 1; } if width == 0 { width = cols; } else if cols != width { panic!("ragged map row in {path}: expected {width} cols, got {cols}"); } height += 1; } TileMap { width: width as u16, height: height as u16, tiles } } /// Tile id at map coordinates, or 0 (empty) when outside the map bounds. pub fn tile(&self, x: i16, y: i16) -> u16 { if x < 0 || y < 0 || x >= self.width as i16 || y >= self.height as i16 { return 0; } self.tiles[y as usize * self.width as usize + x as usize] } } /// Gameplay flags for a global tile id. Collision comes from the shared movement /// vocabulary (`shared::tile_collidable`) so the client can predict it identically; /// other flags (opaque, …) stay server-side. Tile ids index into `overworld.tga`. pub fn tile_flags(tile_id: u16) -> TileFlags { let mut bits = 0u8; if shared::tile_collidable(tile_id) { bits |= TileFlags::COLLIDABLE; } TileFlags(bits) }