use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Color {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Color {
pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
Self { r, g, b, a }
}
pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
Self { r, g, b, a: 1.0 }
}
pub const WHITE: Self = Self::rgb(1.0, 1.0, 1.0);
pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0);
pub const RED: Self = Self::rgb(1.0, 0.0, 0.0);
pub const GREEN: Self = Self::rgb(0.0, 1.0, 0.0);
pub const BLUE: Self = Self::rgb(0.0, 0.0, 1.0);
}
impl Default for Color {
fn default() -> Self {
Self::WHITE
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum TerrainSetType {
#[default]
Corner,
Edge,
Mixed,
}
impl TerrainSetType {
pub fn position_count(&self) -> usize {
match self {
TerrainSetType::Corner => 4,
TerrainSetType::Edge => 4,
TerrainSetType::Mixed => 8,
}
}
pub fn position_name(&self, index: usize) -> &'static str {
match self {
TerrainSetType::Corner => match index {
0 => "Top-Left",
1 => "Top-Right",
2 => "Bottom-Left",
3 => "Bottom-Right",
_ => "Unknown",
},
TerrainSetType::Edge => match index {
0 => "Top",
1 => "Right",
2 => "Bottom",
3 => "Left",
_ => "Unknown",
},
TerrainSetType::Mixed => match index {
0 => "Top-Left Corner",
1 => "Top Edge",
2 => "Top-Right Corner",
3 => "Right Edge",
4 => "Bottom-Right Corner",
5 => "Bottom Edge",
6 => "Bottom-Left Corner",
7 => "Left Edge",
_ => "Unknown",
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Terrain {
pub id: Uuid,
pub name: String,
pub color: Color,
pub icon_tile: Option<u32>,
}
impl Terrain {
pub fn new(name: String, color: Color) -> Self {
Self {
id: Uuid::new_v4(),
name,
color,
icon_tile: None,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TileTerrainData {
#[serde(deserialize_with = "deserialize_terrains")]
pub terrains: [Option<usize>; 9],
}
fn deserialize_terrains<'de, D>(deserializer: D) -> Result<[Option<usize>; 9], D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{SeqAccess, Visitor};
struct TerrainsVisitor;
impl<'de> Visitor<'de> for TerrainsVisitor {
type Value = [Option<usize>; 9];
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("an array of 8 or 9 optional terrain indices")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut terrains = [None; 9];
let mut i = 0;
while let Some(value) = seq.next_element()? {
if i < 9 {
terrains[i] = value;
}
i += 1;
}
Ok(terrains)
}
}
deserializer.deserialize_seq(TerrainsVisitor)
}
impl TileTerrainData {
pub fn new() -> Self {
Self {
terrains: [None; 9],
}
}
pub fn set(&mut self, position: usize, terrain_index: Option<usize>) {
if position < 9 {
self.terrains[position] = terrain_index;
}
}
pub fn get(&self, position: usize) -> Option<usize> {
self.terrains.get(position).copied().flatten()
}
pub fn has_any_terrain(&self) -> bool {
self.terrains.iter().any(|t| t.is_some())
}
pub fn is_uniform(&self, position_count: usize) -> Option<usize> {
let first = self.terrains[0]?;
for i in 1..position_count {
if self.terrains[i] != Some(first) {
return None;
}
}
Some(first)
}
}
#[derive(Debug, Clone, Default)]
pub struct TileConstraints {
pub desired: [Option<usize>; 8],
pub mask: [bool; 8],
}
impl TileConstraints {
pub fn new() -> Self {
Self {
desired: [None; 8],
mask: [false; 8],
}
}
pub fn set(&mut self, position: usize, terrain_index: usize) {
if position < 8 {
self.desired[position] = Some(terrain_index);
self.mask[position] = true;
}
}
pub fn set_desired(&mut self, position: usize, terrain_index: usize) {
if position < 8 {
self.desired[position] = Some(terrain_index);
}
}
pub fn is_constrained(&self, position: usize) -> bool {
position < 8 && self.mask[position]
}
#[allow(dead_code)]
pub fn required(&self) -> &[Option<usize>; 8] {
&self.desired
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TerrainSet {
pub id: Uuid,
pub name: String,
pub tileset_id: Uuid,
pub set_type: TerrainSetType,
pub terrains: Vec<Terrain>,
pub tile_terrains: HashMap<u32, TileTerrainData>,
#[serde(default)]
pub tile_probabilities: HashMap<u32, f32>,
}
impl TerrainSet {
pub fn new(name: String, tileset_id: Uuid, set_type: TerrainSetType) -> Self {
Self {
id: Uuid::new_v4(),
name,
tileset_id,
set_type,
terrains: Vec::new(),
tile_terrains: HashMap::new(),
tile_probabilities: HashMap::new(),
}
}
pub fn add_terrain(&mut self, name: String, color: Color) -> usize {
let terrain = Terrain::new(name, color);
self.terrains.push(terrain);
self.terrains.len() - 1
}
pub fn remove_terrain(&mut self, index: usize) -> Option<Terrain> {
if index < self.terrains.len() {
for tile_data in self.tile_terrains.values_mut() {
for pos in tile_data.terrains.iter_mut() {
if let Some(terrain_idx) = pos {
if *terrain_idx == index {
*pos = None;
} else if *terrain_idx > index {
*terrain_idx -= 1;
}
}
}
}
Some(self.terrains.remove(index))
} else {
None
}
}
pub fn get_terrain_index(&self, name: &str) -> Option<usize> {
self.terrains.iter().position(|t| t.name == name)
}
pub fn set_tile_terrain(
&mut self,
tile_index: u32,
position: usize,
terrain_index: Option<usize>,
) {
let data = self.tile_terrains.entry(tile_index).or_default();
data.set(position, terrain_index);
}
pub fn get_tile_terrain(&self, tile_index: u32) -> Option<&TileTerrainData> {
self.tile_terrains.get(&tile_index)
}
pub fn set_tile_probability(&mut self, tile_index: u32, probability: f32) {
if (probability - 1.0).abs() < f32::EPSILON {
self.tile_probabilities.remove(&tile_index);
} else {
self.tile_probabilities.insert(tile_index, probability);
}
}
pub fn get_tile_probability(&self, tile_index: u32) -> f32 {
self.tile_probabilities
.get(&tile_index)
.copied()
.unwrap_or(1.0)
}
pub fn find_matching_tile(&self, constraints: &TileConstraints) -> Option<u32> {
self.find_best_tile(constraints).map(|(tile, _score)| tile)
}
pub fn find_best_tile(&self, constraints: &TileConstraints) -> Option<(u32, f32)> {
let position_count = self.set_type.position_count();
let mut best_tile: Option<(u32, f32)> = None;
for (&tile_index, tile_data) in &self.tile_terrains {
if !tile_data.has_any_terrain() {
continue;
}
let mut penalty = 0.0f32;
let mut impossible = false;
for i in 0..position_count {
let desired = constraints.desired[i];
let actual = tile_data.terrains[i];
let is_constrained = constraints.mask[i];
match (desired, actual, is_constrained) {
(Some(d), Some(a), true) if d != a => {
impossible = true;
break;
}
(Some(_), Some(_), true) => {
}
(Some(_), None, true) => {
impossible = true;
break;
}
(Some(d), Some(a), false) if d != a => {
penalty += self.transition_penalty(d, a);
}
_ => {
}
}
}
if impossible {
continue;
}
match best_tile {
None => best_tile = Some((tile_index, penalty)),
Some((_, best_penalty)) if penalty < best_penalty => {
best_tile = Some((tile_index, penalty));
}
_ => {}
}
}
best_tile
}
pub fn transition_penalty(&self, from: usize, to: usize) -> f32 {
if from == to {
0.0
} else {
1.0
}
}
pub fn find_uniform_tiles(&self, terrain_index: usize) -> Vec<u32> {
let position_count = self.set_type.position_count();
self.tile_terrains
.iter()
.filter_map(|(&tile_index, tile_data)| {
if tile_data.is_uniform(position_count) == Some(terrain_index) {
Some(tile_index)
} else {
None
}
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_terrain_set_type_position_count() {
assert_eq!(TerrainSetType::Corner.position_count(), 4);
assert_eq!(TerrainSetType::Edge.position_count(), 4);
assert_eq!(TerrainSetType::Mixed.position_count(), 8);
}
#[test]
fn test_tile_terrain_data() {
let mut data = TileTerrainData::new();
assert!(!data.has_any_terrain());
data.set(0, Some(0));
assert!(data.has_any_terrain());
assert_eq!(data.get(0), Some(0));
assert_eq!(data.get(1), None);
}
#[test]
fn test_terrain_set_find_uniform() {
let mut set = TerrainSet::new("Test".to_string(), Uuid::new_v4(), TerrainSetType::Corner);
set.add_terrain("Grass".to_string(), Color::GREEN);
let mut tile_data = TileTerrainData::new();
for i in 0..4 {
tile_data.set(i, Some(0));
}
set.tile_terrains.insert(42, tile_data);
let uniform_tiles = set.find_uniform_tiles(0);
assert!(uniform_tiles.contains(&42));
}
}