use hecs::Entity;
use rapier2d::na::Vector2;
use serde::{Deserialize, Serialize};
use crate::{
texture::TextureKey,
tilemap::{get_tilemap_index, TileId, Tilemap, TilesetResource},
AssetLoader, Emerald, EmeraldError, World,
};
#[derive(Deserialize, Serialize)]
struct AutoTileRulesetsResource {
#[serde(default)]
pub rulesets: Vec<AutoTileRulesetSchema>,
}
pub fn load_autotile_rulesets_from_resource<T: Into<String>>(
loader: &mut AssetLoader,
resource_path: T,
) -> Result<Vec<AutoTileRuleset>, EmeraldError> {
let data = loader.string(resource_path.into())?;
let resource = crate::toml::from_str::<AutoTileRulesetsResource>(&data)?;
let rulesets = resource
.rulesets
.into_iter()
.map(|schema| schema.to_ruleset())
.collect::<Result<Vec<AutoTileRuleset>, EmeraldError>>()?;
Ok(rulesets)
}
#[derive(Clone, Copy, Debug, PartialEq, Hash, Deserialize, Serialize)]
pub enum AutoTileRulesetValue {
Any,
None,
Tile,
}
const AUTOTILE_RULESET_GRID_SIZE: usize = 5;
#[derive(Deserialize, Serialize)]
struct AutoTileRulesetSchema {
pub x: usize,
pub y: usize,
#[serde(default)]
pub rules: Vec<AutoTileRulesetSchemaTile>,
}
impl AutoTileRulesetSchema {
fn to_ruleset(self) -> Result<AutoTileRuleset, EmeraldError> {
let mut grid = default_ruleset_grid();
let max_offset = (AUTOTILE_RULESET_GRID_SIZE / 2) as i8;
for tile in self.rules {
if tile.x < -max_offset
|| tile.x > max_offset
|| tile.y < -max_offset
|| tile.y > max_offset
{
return Err(EmeraldError::new(format!(
"Tile {:?} does not fit inside of the 5x5 ruleset grid.",
tile
)));
}
let position = Vector2::new(
(AUTOTILE_RULESET_GRID_SIZE / 2) as i8 + tile.x,
(AUTOTILE_RULESET_GRID_SIZE / 2) as i8 + tile.y,
);
grid[position.x as usize][position.y as usize] = tile.value;
}
grid[2][2] = AutoTileRulesetValue::Tile;
Ok(AutoTileRuleset {
x: self.x,
y: self.y,
grid,
})
}
}
#[derive(Deserialize, Serialize, Debug)]
struct AutoTileRulesetSchemaTile {
pub x: i8,
pub y: i8,
pub value: AutoTileRulesetValue,
}
fn default_ruleset_grid(
) -> [[AutoTileRulesetValue; AUTOTILE_RULESET_GRID_SIZE]; AUTOTILE_RULESET_GRID_SIZE] {
let default_row = [
AutoTileRulesetValue::Any,
AutoTileRulesetValue::Any,
AutoTileRulesetValue::Any,
AutoTileRulesetValue::Any,
AutoTileRulesetValue::Any,
];
[
default_row,
default_row,
default_row,
default_row,
default_row,
]
}
#[derive(Deserialize, Serialize)]
pub struct AutoTileRuleset {
pub x: usize,
pub y: usize,
pub grid: [[AutoTileRulesetValue; AUTOTILE_RULESET_GRID_SIZE]; AUTOTILE_RULESET_GRID_SIZE],
}
impl AutoTileRuleset {
pub(crate) fn matches(
&self,
autotiles: &Vec<AutoTile>,
autotilemap_width: usize,
autotilemap_height: usize,
x: usize,
y: usize,
) -> bool {
match get_tilemap_index(x, y, autotilemap_width, autotilemap_height) {
Err(_) => {
return false;
}
Ok(index) => {
if autotiles[index] != AutoTile::Tile {
return false;
}
}
}
for ruleset_x in 0..AUTOTILE_RULESET_GRID_SIZE {
for ruleset_y in 0..AUTOTILE_RULESET_GRID_SIZE {
if (ruleset_x == (AUTOTILE_RULESET_GRID_SIZE / 2)
&& ruleset_y == (AUTOTILE_RULESET_GRID_SIZE / 2))
|| self.grid[ruleset_x][ruleset_y] == AutoTileRulesetValue::Any
{
continue;
}
let autotile_ruleset_value = self.get_autotile_ruleset_value(
autotiles,
autotilemap_width,
autotilemap_height,
x as isize - (AUTOTILE_RULESET_GRID_SIZE / 2) as isize + ruleset_x as isize,
y as isize - (AUTOTILE_RULESET_GRID_SIZE / 2) as isize + ruleset_y as isize,
);
if self.grid[ruleset_x][ruleset_y] != autotile_ruleset_value {
return false;
}
}
}
true
}
fn get_autotile_ruleset_value(
&self,
autotiles: &Vec<AutoTile>,
autotilemap_width: usize,
autotilemap_height: usize,
x: isize,
y: isize,
) -> AutoTileRulesetValue {
if x < 0 || y < 0 {
return AutoTileRulesetValue::Any;
}
match get_tilemap_index(
x as usize,
y as usize,
autotilemap_width,
autotilemap_height,
) {
Ok(index) => match autotiles[index] {
AutoTile::None => AutoTileRulesetValue::None,
AutoTile::Tile => AutoTileRulesetValue::Tile,
},
Err(_) => AutoTileRulesetValue::Any,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Hash)]
pub enum AutoTile {
None = 0,
Tile = 1,
}
fn default_visibility() -> bool {
true
}
#[derive(Deserialize, Serialize)]
struct AutoTileSchema {
x: usize,
y: usize,
}
#[derive(Deserialize, Serialize)]
struct AutoTileMapSchema {
#[serde(default)]
pub tileset: Option<TilesetResource>,
#[serde(default)]
pub tileset_resource: Option<String>,
#[serde(default)]
pub rulesets_resource: Option<String>,
#[serde(default)]
pub rulesets: Vec<AutoTileRulesetSchema>,
pub width: usize,
pub height: usize,
#[serde(default)]
pub tiles: Vec<AutoTileSchema>,
#[serde(default)]
pub z_index: f32,
#[serde(default = "default_visibility")]
pub visible: bool,
}
impl AutoTileMapSchema {
pub fn to_autotilemap(self, loader: &mut AssetLoader) -> Result<AutoTilemap, EmeraldError> {
let mut ruleset_schemas = Vec::new();
if let Some(resource) = &self.rulesets_resource {
let toml = loader.string(resource.clone())?;
let resource = crate::toml::from_str::<AutoTileRulesetsResource>(&toml)?;
ruleset_schemas = resource.rulesets;
}
let tileset_resource = if let Some(resource) = self.tileset {
resource
} else {
let data = loader.string(&self.tileset_resource.unwrap())?;
crate::toml::from_str::<TilesetResource>(&data)?
};
ruleset_schemas.extend(self.rulesets);
let rulesets = ruleset_schemas
.into_iter()
.map(|ruleset_schema| ruleset_schema.to_ruleset())
.collect::<Result<Vec<AutoTileRuleset>, EmeraldError>>()?;
let texture = loader.texture(tileset_resource.texture)?;
let tile_size = Vector2::new(
texture.size().0 as usize / tileset_resource.width,
texture.size().1 as usize / tileset_resource.height,
);
let mut autotilemap = AutoTilemap::new(
texture,
tile_size,
tileset_resource.width,
tileset_resource.height,
self.width,
self.height,
rulesets,
);
for tile in self.tiles {
autotilemap.set_tile(tile.x, tile.y)?;
}
autotilemap.set_z_index(self.z_index);
autotilemap.set_visible(self.visible);
Ok(autotilemap)
}
}
pub struct AutoTilemap {
pub(crate) tilemap: Tilemap,
rulesets: Vec<AutoTileRuleset>,
autotiles: Vec<AutoTile>,
}
impl AutoTilemap {
pub fn new(
tilesheet: TextureKey,
tile_size: Vector2<usize>,
tilesheet_width: usize,
tilesheet_height: usize,
map_width: usize,
map_height: usize,
rulesets: Vec<AutoTileRuleset>,
) -> Self {
let tilemap = Tilemap::new(
tilesheet,
tile_size,
tilesheet_width,
tilesheet_height,
map_width,
map_height,
);
let tile_count = map_width * map_height;
let mut autotiles = Vec::with_capacity(tile_count);
for _ in 0..tile_count {
autotiles.push(AutoTile::None);
}
Self {
tilemap,
rulesets,
autotiles,
}
}
pub fn bake(&mut self) -> Result<(), EmeraldError> {
for x in 0..self.width() {
for y in 0..self.height() {
let id = self.compute_tileset_tile_id(x, y)?;
self.tilemap.set_tile(x, y, id)?;
}
}
Ok(())
}
pub fn set_z_index(&mut self, z_index: f32) {
self.tilemap.z_index = z_index;
}
pub fn set_visible(&mut self, visible: bool) {
self.tilemap.visible = visible;
}
pub fn width(&self) -> usize {
self.tilemap.width
}
pub fn height(&self) -> usize {
self.tilemap.height
}
pub fn tilesheet(&self) -> TextureKey {
self.tilemap.tilesheet.clone()
}
pub fn tile_size(&self) -> Vector2<usize> {
self.tilemap.tile_size.clone()
}
pub fn add_ruleset(&mut self, ruleset: AutoTileRuleset) {
self.rulesets.push(ruleset);
}
pub fn get_autotile(&mut self, x: usize, y: usize) -> Result<AutoTile, EmeraldError> {
let index = get_tilemap_index(x, y, self.width(), self.height())?;
Ok(self.autotiles[index])
}
pub fn set_tile(&mut self, x: usize, y: usize) -> Result<(), EmeraldError> {
self.set_autotile(x, y, AutoTile::Tile)
}
pub fn set_none(&mut self, x: usize, y: usize) -> Result<(), EmeraldError> {
self.set_autotile(x, y, AutoTile::None)
}
pub fn set_autotile(
&mut self,
x: usize,
y: usize,
new_tile_id: AutoTile,
) -> Result<(), EmeraldError> {
let index = get_tilemap_index(x, y, self.width(), self.height())?;
self.autotiles[index] = new_tile_id;
Ok(())
}
pub fn tiles(&self) -> &Vec<Option<TileId>> {
&self.tilemap.tiles
}
pub fn get_ruleset(&self, tile_id: TileId) -> Option<&AutoTileRuleset> {
let width = self.width();
let height = self.height();
self.rulesets.iter().find(|ruleset| {
if let Ok(id) = get_tilemap_index(ruleset.x, ruleset.y, width, height) {
id == tile_id
} else {
false
}
})
}
pub fn remove_ruleset(&mut self, tile_id: TileId) -> Option<AutoTileRuleset> {
let width = self.width();
let height = self.height();
if let Some(index) = self.rulesets.iter().position(|ruleset| {
if let Ok(id) = get_tilemap_index(ruleset.x, ruleset.y, width, height) {
id == tile_id
} else {
false
}
}) {
return Some(self.rulesets.remove(index));
}
None
}
pub fn compute_tileset_tile_id(
&self,
x: usize,
y: usize,
) -> Result<Option<TileId>, EmeraldError> {
let width = self.width();
let height = self.height();
if let Some(ruleset) = self
.rulesets
.iter()
.find(|ruleset| ruleset.matches(&self.autotiles, width, height, x, y))
{
let id = get_tilemap_index(
ruleset.x,
ruleset.y,
self.tilemap.tilesheet_width,
self.tilemap.tilesheet_height,
)?;
return Ok(Some(id));
}
Ok(None)
}
pub fn get_tile_id(&self, x: usize, y: usize) -> Result<Option<TileId>, EmeraldError> {
self.tilemap.get_tile(x, y)
}
}
pub(crate) fn load_ent_autotilemap<'a>(
loader: &mut AssetLoader<'a>,
entity: Entity,
world: &mut World,
toml: &toml::Value,
) -> Result<(), EmeraldError> {
let schema: AutoTileMapSchema = toml::from_str(&toml.to_string())?;
let mut autotilemap = schema.to_autotilemap(loader)?;
autotilemap.bake()?;
world.insert_one(entity, autotilemap)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::{AutoTileMapSchema, AutoTileRulesetSchema, AutoTileRulesetValue};
#[test]
fn deser_ruleset() {
let ruleset_toml = r#"
x = 10
y = 11
[[rules]]
x = -1
y = -1
value = "None"
[[rules]]
x = -1
y = 0
value = "None"
[[rules]]
x = 1
y = 1
value = "Tile"
"#;
let schema: AutoTileRulesetSchema = crate::toml::from_str(ruleset_toml).unwrap();
let ruleset = schema.to_ruleset().unwrap();
assert_eq!(ruleset.x, 10);
assert_eq!(ruleset.y, 11);
assert_eq!(ruleset.grid[1][1], AutoTileRulesetValue::None);
assert_eq!(ruleset.grid[1][2], AutoTileRulesetValue::None);
assert_eq!(ruleset.grid[3][3], AutoTileRulesetValue::Tile);
assert_eq!(ruleset.grid[2][2], AutoTileRulesetValue::Tile);
let out_of_bounds_ruleset = r#"
x = 10
y = 11
[[rules]]
x = -3
y = 0
value = "None"
"#;
let schema: AutoTileRulesetSchema = crate::toml::from_str(out_of_bounds_ruleset).unwrap();
assert!(schema.to_ruleset().is_err());
}
#[test]
fn deser_autotilemap() {
let autotilemap_toml = r#"
width = 10
height = 11
[tileset]
texture = "test"
width = 1
height = 2
"#;
let schema: AutoTileMapSchema = crate::toml::from_str(&autotilemap_toml).unwrap();
assert_eq!(schema.width, 10);
assert_eq!(schema.height, 11);
assert_eq!(&schema.tileset.as_ref().unwrap().texture, "test");
assert_eq!(schema.tileset.as_ref().unwrap().width, 1);
assert_eq!(schema.tileset.as_ref().unwrap().height, 2);
let missing_map_size = r#"
tile_width = 32
tile_height = 32
"#;
let schema = crate::toml::from_str::<AutoTileMapSchema>(&missing_map_size);
assert!(schema.is_err());
}
}