use bevy_ecs::prelude::Resource; use glam::UVec2; use image::GenericImageView; use serde::{Deserialize, Serialize}; use std::fs; use tracing::{debug, info}; use crate::game::territory::get_idx; use crate::game::tilemap::TileMap; /// Calculate terrain color using pastel theme formulas fn calculate_theme_color(color_base: &str, color_variant: u8) -> [u8; 3] { let i = color_variant as i32; match color_base { "grass" => { // rgb(238 - 2 * i, 238 - 2 * i, 190 - i) [(238 - 2 * i).clamp(0, 255) as u8, (238 - 2 * i).clamp(0, 255) as u8, (190 - i).clamp(0, 255) as u8] } "mountain" => { // rgb(250 - 2 * i, 250 - 2 * i, 220 - i) [(250 - 2 * i).clamp(0, 255) as u8, (250 - 2 * i).clamp(0, 255) as u8, (220 - i).clamp(0, 255) as u8] } "water" => { // rgb(172 - 2 * i, 225 - 2 * i, 249 - 3 * i) [(172 - 2 * i).clamp(0, 255) as u8, (225 - 2 * i).clamp(0, 255) as u8, (249 - 3 * i).clamp(0, 255) as u8] } _ => { // Default fallback color (gray) [128, 128, 128] } } } /// Helper structs for loading World.json format #[derive(Deserialize)] struct WorldMapJson { tiles: Vec, } #[derive(Deserialize)] struct WorldTileDef { color: String, name: String, #[serde(default, rename = "colorBase")] color_base: Option, #[serde(default, rename = "colorVariant")] color_variant: Option, conquerable: bool, navigable: bool, #[serde(default, rename = "expansionCost")] expansion_cost: Option, #[serde(default, rename = "expansionTime")] expansion_time: Option, } /// Parse hex color string (#RRGGBB) to RGB bytes fn parse_hex_rgb(s: &str) -> Option<[u8; 3]> { let s = s.trim_start_matches('#'); if s.len() != 6 { return None; } let r = u8::from_str_radix(&s[0..2], 16).ok()?; let g = u8::from_str_radix(&s[2..4], 16).ok()?; let b = u8::from_str_radix(&s[4..6], 16).ok()?; Some([r, g, b]) } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TileType { pub name: String, pub color_base: String, pub color_variant: u8, pub conquerable: bool, pub navigable: bool, pub expansion_time: u8, pub expansion_cost: u8, } /// Map manifest structure #[derive(Debug, Clone, Deserialize, Serialize)] pub struct MapManifest { pub map: MapMetadata, pub name: String, pub nations: Vec, } /// Map size metadata #[derive(Debug, Clone, Deserialize, Serialize)] pub struct MapMetadata { pub width: usize, pub height: usize, pub num_land_tiles: usize, } /// Nation spawn point #[derive(Debug, Clone, Deserialize, Serialize)] pub struct NationSpawn { pub coordinates: [usize; 2], pub flag: String, pub name: String, pub strength: u32, } /// Loaded map data #[derive(Debug, Clone, Resource)] pub struct TerrainData { pub _manifest: MapManifest, /// Legacy terrain data (for backward compatibility) pub terrain_data: TileMap, /// Tile type indices (new format) pub tiles: Vec, /// Tile type definitions pub tile_types: Vec, } impl TerrainData { /// Load the World map from embedded assets pub fn load_world_map() -> Result> { const MAP_JSON: &[u8] = include_bytes!("../../assets/maps/World.json"); const MAP_PNG: &[u8] = include_bytes!("../../assets/maps/World.png"); // Parse JSON tile definitions let map_json: WorldMapJson = serde_json::from_slice(MAP_JSON)?; // Load PNG image let png = image::load_from_memory(MAP_PNG)?; let (width, height) = png.dimensions(); info!("Loading World map: {}x{}", width, height); // Build color-to-index lookup table let color_to_index: Vec<([u8; 3], usize)> = map_json.tiles.iter().enumerate().filter_map(|(idx, t)| parse_hex_rgb(&t.color).map(|rgb| (rgb, idx))).collect(); let mut tiles = vec![0u8; (width * height) as usize]; let mut terrain_data_raw = vec![0u8; (width * height) as usize]; // Match each pixel to nearest tile type by color for y in 0..height { for x in 0..width { let pixel = png.get_pixel(x, y).0; let rgb = [pixel[0], pixel[1], pixel[2]]; // Find nearest tile by RGB distance let (tile_idx, _) = color_to_index .iter() .map(|(c, idx)| { let dr = rgb[0] as i32 - c[0] as i32; let dg = rgb[1] as i32 - c[1] as i32; let db = rgb[2] as i32 - c[2] as i32; let dist = (dr * dr + dg * dg + db * db) as u32; (idx, dist) }) .min_by_key(|(_, d)| *d) .unwrap(); let i = (y * width + x) as usize; tiles[i] = *tile_idx as u8; // Set bit 7 if conquerable (land) if map_json.tiles[*tile_idx].conquerable { terrain_data_raw[i] |= 0x80; } // Lower 5 bits for terrain magnitude (unused for World map) } } // Convert to TileType format let tile_types = map_json.tiles.into_iter().map(|t| TileType { name: t.name, color_base: t.color_base.unwrap_or_default(), color_variant: t.color_variant.unwrap_or(0) as u8, conquerable: t.conquerable, navigable: t.navigable, expansion_cost: t.expansion_cost.unwrap_or(50) as u8, expansion_time: t.expansion_time.unwrap_or(50) as u8 }).collect(); let num_land_tiles = terrain_data_raw.iter().filter(|&&b| b & 0x80 != 0).count(); info!("World map loaded: {} land tiles", num_land_tiles); Ok(Self { _manifest: MapManifest { name: "World".to_string(), map: MapMetadata { width: width as usize, height: height as usize, num_land_tiles }, nations: vec![] }, terrain_data: TileMap::from_vec(width, height, terrain_data_raw), tiles, tile_types }) } /// Load a map from the resources directory pub fn load(map_name: &str) -> Result> { let base_path = format!("resources/maps/{}", map_name); // Load manifest let manifest_path = format!("{}/manifest.json", base_path); let manifest_json = fs::read_to_string(&manifest_path)?; let manifest: MapManifest = serde_json::from_str(&manifest_json)?; // Load binary map data let map_path = format!("{}/map.bin", base_path); let terrain_data_raw = fs::read(&map_path)?; let width = manifest.map.width as u32; let height = manifest.map.height as u32; // Verify data size if terrain_data_raw.len() != (width * height) as usize { return Err(format!("Map data size mismatch: expected {} bytes, got {}", width * height, terrain_data_raw.len()).into()); } info!("Loaded map '{}' ({}x{})", manifest.name, width, height); debug!("Land tiles: {}/{}", manifest.map.num_land_tiles, width * height); // Create default tile types for legacy format let tile_types = vec![TileType { name: "water".to_string(), color_base: "water".to_string(), color_variant: 0, conquerable: false, navigable: true, expansion_time: 255, expansion_cost: 255 }, TileType { name: "land".to_string(), color_base: "grass".to_string(), color_variant: 0, conquerable: true, navigable: false, expansion_time: 50, expansion_cost: 50 }]; // Convert legacy format to tile indices let tiles: Vec = terrain_data_raw.iter().map(|&byte| if byte & 0x80 != 0 { 1 } else { 0 }).collect(); // Create TileMap from terrain data let terrain_data = TileMap::from_vec(width, height, terrain_data_raw); Ok(Self { _manifest: manifest, terrain_data, tiles, tile_types }) } /// Get the size of the map pub fn size(&self) -> UVec2 { UVec2::new(self.terrain_data.width(), self.terrain_data.height()) } pub fn get_value>(&self, pos: T) -> u8 { self.terrain_data[get_idx(pos, self.terrain_data.width())] } /// Check if a tile is land (bit 7 set) pub fn is_land>(&self, pos: T) -> bool { self.get_value(pos) & 0x80 != 0 } /// Get terrain magnitude (bits 0-4) pub fn terrain_magnitude>(&self, pos: T) -> u8 { self.get_value(pos) & 0b00011111 } /// Get tile type at position pub fn get_tile_type>(&self, pos: T) -> &TileType { let idx = get_idx(pos, self.terrain_data.width()); &self.tile_types[self.tiles[idx] as usize] } /// Check if a tile is conquerable pub fn is_conquerable>(&self, pos: T) -> bool { self.get_tile_type(pos).conquerable } /// Check if a tile is navigable (water) pub fn is_navigable>(&self, pos: T) -> bool { self.get_tile_type(pos).navigable } /// Get expansion time for a tile pub fn get_expansion_time>(&self, pos: T) -> u8 { self.get_tile_type(pos).expansion_time } /// Get expansion cost for a tile pub fn get_expansion_cost>(&self, pos: T) -> u8 { self.get_tile_type(pos).expansion_cost } /// Get tile type IDs for rendering (each position maps to a tile type) pub fn get_tile_ids(&self) -> &[u8] { &self.tiles } /// Get terrain palette colors from tile types (for rendering) /// Returns a vec where index = tile type ID, value = RGB color /// Colors are calculated using theme formulas based on colorBase and colorVariant pub fn get_terrain_palette_colors(&self) -> Vec<[u8; 3]> { self.tile_types.iter().map(|tile_type| calculate_theme_color(&tile_type.color_base, tile_type.color_variant)).collect() } }