129 lines
4.6 KiB
GDScript
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)
|