Skip to content
Snippets Groups Projects
lib.rs 6.36 KiB
Newer Older
Louis's avatar
Louis committed
//! `micro_autotile` provides an implementation of the LDTK auto-tiling algorithm, for use in
//! programs at runtime. The representation is compatible with that saved by LDTK, meaning that
//! definitions can be loaded directly from LDTK JSON exports.
//!
//! Creating a single rule works like this:
//!
//! 1. Create a `TileMatcher` out of `TileStatus` entries.
//!     * Tile Matchers are squares represented as fixed size flat arrays
//!     * Currently only 1x1 and 3x3 matchers are supported, 5x5 matchers are incompatible
//!     * Since a Tile Matcher is a rule, they are usually created statically or loaded as
//!        an asset that will not change much / at all
//! 2. Create a `TileOutput` that represents the value produces by this rule when it matches
//!     * An output value of `Skip` will cause the rule to be a noop. This has utility when combined
//!       with a rule's `chance` value, as part of a set of rules
//!     * A `Single` output will always produce the same value
//!     * A `Random` output will produce one of the provided values at random
//! 3. Combine these into an `AutoTileRule`
//!     * There are a number of convenience methods for doing this process without mistakes in a single function call
//!
//! To utilise your matcher, you'll need to provide a specifically formatted slice of your input data (typically a sub-grid
//! of a tile map). If you're matching a single tile, you can use the convenience method `TileLayout::single`, otherwise
//! you will need to provide a 9 element array that represents 3 rows and 3 columns of data, in the following format:
//!
//! ```text
//! Flat array data
//! [1, 2, 3, 4, 5, 6, 7, 8, 9]
//! ```
//!
//! ```text
//! Formatted in the way it would appear if laid out in a tile map
//! [
//!   1, 2, 3, # First row, all three columns
//!   4, 5, 6, # Second row, all three columns
//!   7, 8, 9, # Third row, all three columns
//! ]
//! ```
//!
//! As we can see, the fifth element of the array is the centre tile of our matching grid. In fact, `TileLayout::single` constructs
//! a 9 element array where the fifth element is `Some(your_value)`, and the rest are simply `None`. This is possible because the
//! actual data represents each element as an `Option` (not as the simple numbers above), which allows matching up against edges of
//! data arrays, or against non-regular shapes. Putting this together to match against data from our tile map, we have the following:
//!
//! e.g.
//! ```rust
Louis's avatar
Louis committed
//! # use micro_autotile::{TileLayout, TileMatcher};
//! use micro_autotile::{AutoTileRule, AutoRuleSet};
//!
Louis's avatar
Louis committed
//! # fn main() {
Louis's avatar
Louis committed
//! const WALL_TILE: i32 = 0;
//! const GROUND_TILE: i32 = 1;
Louis's avatar
Louis committed
//!
//! // Match a 1x1 ground tile, output the index within the spritesheet that we'll use for rendering
//! let match_1_x_1 = AutoTileRule::exact(GROUND_TILE, 57);
//!
//! assert_eq!(
//!   match_1_x_1.resolve_match(&TileLayout::single(GROUND_TILE)),
//!   Some(57)
//! );
//!
//! // More realistically, we might have some tile data for a ground tile with other data arround it.
//! // When we match against a rule, we're always trying to produce a value for the _central_ value in
//! // our `TileLayout` (the fifth element)
//! let enclosed_ground = TileLayout([
//!   Some(WALL_TILE), Some(WALL_TILE), Some(WALL_TILE),
//!   Some(WALL_TILE), Some(GROUND_TILE), Some(WALL_TILE),
//!   Some(WALL_TILE), Some(WALL_TILE), Some(WALL_TILE),
//! ]);
//!
//! assert_eq!(
//!   match_1_x_1.resolve_match(&enclosed_ground),
//!   Some(57)
//! );
//!
//! // There may also be situations in which you just want to know that a given layout matches a rule, without
//! // concern for producing a value for that layout. You can directly use a `TileMatcher` for this
//! assert!(TileMatcher::single(GROUND_TILE).matches(&enclosed_ground));
//! # }
//! ```
//!
//! There's already a lot of utility to these structures, but we still need to manually run a set of
//! rules against our maps and do some work with the `AutoTileRule::chance` property to figure out
//! what the final output should be for a given layout.
//!
//! Introducing the `AutoRuleSet` struct, that represents a sequence of rules that should be evaluated
//! to produce an output. It provides similar methods to the individual AutoTileRule, but will execute
//! against a set of rules at once. There are also convenience methods for combining AutoRuleSet
//! instances
//!
//! ```rust
Louis's avatar
Louis committed
//! # use micro_autotile::{TileLayout, TileMatcher};
//! use micro_autotile::{AutoTileRule, AutoRuleSet};
//!
Louis's avatar
Louis committed
//! # fn main() {
//! use micro_autotile::{TileMatcher, TileStatus};
Louis's avatar
Louis committed
//! const WALL_TILE: i32 = 0;
//! const GROUND_TILE: i32 = 1;
//! const OTHER_TILE: i32 = 342;
Louis's avatar
Louis committed
//!
//! let wall_rules = AutoRuleSet(vec![
//! 	AutoTileRule::single_when(TileMatcher([ // Top Left Corner
//! 		TileStatus::IsNot(WALL_TILE), TileStatus::IsNot(WALL_TILE), TileStatus::IsNot(WALL_TILE),
//! 		TileStatus::IsNot(WALL_TILE), TileStatus::Is(WALL_TILE), TileStatus::Is(WALL_TILE),
//! 		TileStatus::IsNot(WALL_TILE), TileStatus::Is(WALL_TILE), TileStatus::Is(WALL_TILE),
//! 	]), 54),
//! 	AutoTileRule::single_when(TileMatcher([ // Top Right Corner
//! 		TileStatus::IsNot(WALL_TILE), TileStatus::IsNot(WALL_TILE), TileStatus::IsNot(WALL_TILE),
//! 		TileStatus::Is(WALL_TILE), TileStatus::Is(WALL_TILE), TileStatus::IsNot(WALL_TILE),
//! 		TileStatus::Is(WALL_TILE), TileStatus::Is(WALL_TILE), TileStatus::IsNot(WALL_TILE),
//! 	]), 55),
//! 	// ... Etc
//! ]);
//!
//! let ground_rules = AutoRuleSet(vec![
//! 	// Use decorated tiles in 10% of cases
//! 	AutoTileRule::single_any_chance(GROUND_TILE, vec![45, 46, 47], 0.1),
//! 	// Fall back to the basic tile if we don't match previously
//! 	AutoTileRule::exact(GROUND_TILE, 44),
//! ]);
//!
//! // Easily merge rule sets in an ordered way
//! let combined_rules = wall_rules + ground_rules;
//!
//! let sublayout = TileLayout([
//! 	Some(OTHER_TILE), Some(GROUND_TILE), Some(GROUND_TILE),
//! 	Some(WALL_TILE), Some(WALL_TILE), Some(OTHER_TILE),
//! 	Some(WALL_TILE), Some(WALL_TILE), Some(GROUND_TILE),
//! ]);
//!
//! // We've got a layout that represents the top right corner of a wall, the second rule in our
//! // set - the value of the tiles that match "IsNot(WALL_TILE)" are irrelevant, as long as they
//! // exist (Option::Some)
//! let output = combined_rules.resolve_match(&sublayout);
//! assert_eq!(output, Some(55));
//! # }
//! ```

Louis's avatar
Louis committed
mod layout;
mod autotile;
mod output;
mod utils;
Louis's avatar
Louis committed
pub use layout::*;
pub use autotile::*;
pub use output::*;