mirror of
https://github.com/Xevion/smart-rgb.git
synced 2025-12-06 01:16:24 -06:00
271 lines
9.8 KiB
Rust
271 lines
9.8 KiB
Rust
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<WorldTileDef>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct WorldTileDef {
|
|
color: String,
|
|
name: String,
|
|
#[serde(default, rename = "colorBase")]
|
|
color_base: Option<String>,
|
|
#[serde(default, rename = "colorVariant")]
|
|
color_variant: Option<u32>,
|
|
conquerable: bool,
|
|
navigable: bool,
|
|
#[serde(default, rename = "expansionCost")]
|
|
expansion_cost: Option<u32>,
|
|
#[serde(default, rename = "expansionTime")]
|
|
expansion_time: Option<u32>,
|
|
}
|
|
|
|
/// 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<NationSpawn>,
|
|
}
|
|
|
|
/// 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<u8>,
|
|
/// Tile type indices (new format)
|
|
pub tiles: Vec<u8>,
|
|
/// Tile type definitions
|
|
pub tile_types: Vec<TileType>,
|
|
}
|
|
|
|
impl TerrainData {
|
|
/// Load the World map from embedded assets
|
|
pub fn load_world_map() -> Result<Self, Box<dyn std::error::Error>> {
|
|
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<Self, Box<dyn std::error::Error>> {
|
|
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<u8> = 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<T: Into<UVec2>>(&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<T: Into<UVec2>>(&self, pos: T) -> bool {
|
|
self.get_value(pos) & 0x80 != 0
|
|
}
|
|
|
|
/// Get terrain magnitude (bits 0-4)
|
|
pub fn terrain_magnitude<T: Into<UVec2>>(&self, pos: T) -> u8 {
|
|
self.get_value(pos) & 0b00011111
|
|
}
|
|
|
|
/// Get tile type at position
|
|
pub fn get_tile_type<T: Into<UVec2>>(&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<T: Into<UVec2>>(&self, pos: T) -> bool {
|
|
self.get_tile_type(pos).conquerable
|
|
}
|
|
|
|
/// Check if a tile is navigable (water)
|
|
pub fn is_navigable<T: Into<UVec2>>(&self, pos: T) -> bool {
|
|
self.get_tile_type(pos).navigable
|
|
}
|
|
|
|
/// Get expansion time for a tile
|
|
pub fn get_expansion_time<T: Into<UVec2>>(&self, pos: T) -> u8 {
|
|
self.get_tile_type(pos).expansion_time
|
|
}
|
|
|
|
/// Get expansion cost for a tile
|
|
pub fn get_expansion_cost<T: Into<UVec2>>(&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()
|
|
}
|
|
}
|