77 lines
2.8 KiB
Rust
77 lines
2.8 KiB
Rust
//! 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<u16>, // 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::<i64>()
|
|
.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)
|
|
}
|