Files
project-hood/addons/TileMapDual/TerrainLayer.gd

129 lines
4.6 KiB
GDScript

## A set of _rules usable by a single DisplayLayer.
class_name TerrainLayer
extends Resource
## A list of which CellNeighbors to care about during terrain checking.
var terrain_neighborhood: Array = []
##[br] When a cell in a DisplayLayer needs to be recomputed,
## the TerrainLayer needs to know which tiles surround it.
##[br] This Array stores the paths from the affected cell to the neighboring world cells.
var display_to_world_neighborhood: Array
# TODO: change Mapping to support https://github.com/pablogila/TileMapDual/issues/13
##[br] The rules that dictate which tile matches a given set of neighbors.
##[br] Used by register_rule() and apply_rule()
##[codeblock]
## _rules: TrieNode = # The actual type of _rules
##
## TrieNode: Dictionary{
## key: int = # The terrain value of this neighbor
## value: TrieNode | TrieLeaf = # The next branch of this trie
## }
##
## TrieLeaf: Dictionary{
## 'mapping': Mapping = # The tile that this should now become
## }
##
## Mapping: Dictionary{
## 'sid': int = # The source_id of this tile
## 'tile': Vector2i = # The Atlas Coordinates of this tile
## }
##[/codeblock]
##[br] Internally a decision "trie":
##[br] - each node branch represents a terrain neighbor
##[br] - each leaf node represents the terrain that a tile
## with the given terrain neighborhood should become
##[br]
##[br] How the trie is searched:
##[br] - check the next neighbor in terrain_neighbors
##[br] - check if there is a branch corresponding to the terrain of that neighbor
##[br] - if there is a branch, search again under that branch
##[br] - else pretend that neighbor is empty and try again
##[br] - if there really isn't a branch, no rules exist so just return empty
##[br] - once at a leaf node, its mapping should tell us what terrain to become
##[br]
##[br] See apply_rule() for more details.
var _rules: Dictionary = {}
## Generator for random placement of tiles
var rand : RandomNumberGenerator
## Random seed used to generate tiles deterministically
var global_seed : int = 707
func _init(fields: Dictionary) -> void:
self.terrain_neighborhood = fields.terrain_neighborhood
self.display_to_world_neighborhood = fields.display_to_world_neighborhood
self.rand = RandomNumberGenerator.new()
## Register a new rule for a specific tile in an atlas.
func _register_tile(data: TileData, mapping: Dictionary) -> void:
if data.terrain_set != 0:
# This was already handled as an error in the parent TerrainDual
return
var terrain_neighbors := terrain_neighborhood.map(data.get_terrain_peering_bit)
# Skip tiles with no peering bits in this filter
# They might be used for a different layer,
# or may have no peering bits at all, which will just be ignored by all layers
if terrain_neighbors.any(func(neighbor): return neighbor == -1):
if terrain_neighbors.any(func(neighbor): return neighbor != -1):
push_warning(
"Invalid Tile Neighborhood at %s.\n" % [mapping] +
"Expected neighborhood: %s" % [terrain_neighborhood.map(Util.neighbor_name)]
)
return
mapping["prob"] = data.probability
_register_rule(terrain_neighbors, mapping)
## Register a new rule for a set of surrounding terrain neighbors
func _register_rule(terrain_neighbors: Array, mapping: Dictionary) -> void:
var node := _rules
for terrain in terrain_neighbors:
if terrain not in node:
node[terrain] = {}
node = node[terrain]
if not 'mappings' in node:
node.mappings = []
node.mappings.append(mapping)
const TILE_EMPTY: Dictionary = {'sid': - 1, 'tile': Vector2i(-1, -1)}
## Returns the tile that should be used based on the surrounding terrain neighbors
func apply_rule(terrain_neighbors: Array, cell: Vector2i) -> Dictionary:
var is_empty := terrain_neighbors.all(func(terrain): return terrain == -1)
if is_empty:
return TILE_EMPTY
var normalized_neighbors = terrain_neighbors.map(normalize_terrain)
var node := _rules
for terrain in normalized_neighbors:
if terrain not in node:
terrain = 0
if terrain not in node:
return TILE_EMPTY
node = node[terrain]
if 'mappings' not in node:
return TILE_EMPTY
#random tile selection
var weights: Array= []
var index: int = 0
for mapping in node.mappings:
weights.append(mapping.prob)
rand.seed = hash(str(cell)+str(global_seed))
index = rand.rand_weighted(weights)
return node.mappings[index]
## Coerces all empty tiles to have a terrain of 0.
static func normalize_terrain(terrain):
return terrain if terrain != -1 else 0
## Utility function for easier printing
func _neighbors_to_dict(terrain_neighbors: Array) -> Dictionary:
return Util.arrays_to_dict(terrain_neighborhood.map(Util.neighbor_name), terrain_neighbors)