mirror of
https://github.com/JHDev2006/Super-Mario-Bros.-Remastered-Public.git
synced 2025-10-22 07:28:14 +00:00
1160 lines
36 KiB
GDScript3
1160 lines
36 KiB
GDScript3
@tool
|
|
extends Node
|
|
|
|
## A [TileMapLayer] terrain / auto-tiling system.
|
|
##
|
|
## This is a drop-in replacement for Godot 4's tilemap terrain system, offering
|
|
## more versatile and straightforward autotiling. It can be used with any
|
|
## existing [TileMapLayer] or [TileSet], either through the editor plugin, or
|
|
## directly via code.
|
|
## [br][br]
|
|
## The [b]BetterTerrain[/b] class contains only static functions, each of which
|
|
## either takes a [TileMapLayer], a [TileSet], and sometimes a [TileData].
|
|
## Meta-data is embedded inside the [TileSet] and the [TileData] types to store
|
|
## the terrain information. See [method Object.get_meta] for information.
|
|
## [br][br]
|
|
## Once terrain is set up, it can be written to the tilemap using [method set_cells].
|
|
## Similar to Godot 3.x, setting the cells does not run the terrain solver, so once
|
|
## the cells have been set, you need to call an update function such as [method update_terrain_cells].
|
|
|
|
|
|
## The meta-data key used to store terrain information.
|
|
const TERRAIN_META = &"_better_terrain"
|
|
|
|
## The current version. Used to handle future upgrades.
|
|
const TERRAIN_SYSTEM_VERSION = "0.2"
|
|
|
|
var _tile_cache = {}
|
|
var rng = RandomNumberGenerator.new()
|
|
var use_seed := true
|
|
|
|
## A helper class that provides functions detailing valid peering bits and
|
|
## polygons for different tile types.
|
|
var data := load("res://addons/better-terrain/BetterTerrainData.gd"):
|
|
get:
|
|
return data
|
|
|
|
enum TerrainType {
|
|
MATCH_TILES, ## Selects tiles by matching against adjacent tiles.
|
|
MATCH_VERTICES, ## Select tiles by analysing vertices, similar to wang-style tiles.
|
|
CATEGORY, ## Declares a matching type for more sophisticated rules.
|
|
DECORATION, ## Fills empty tiles by matching adjacent tiles
|
|
MAX,
|
|
}
|
|
|
|
enum TileCategory {
|
|
EMPTY = -1, ## An empty cell, or a tile marked as decoration
|
|
NON_TERRAIN = -2, ## A non-empty cell that does not contain a terrain tile
|
|
ERROR = -3
|
|
}
|
|
|
|
enum SymmetryType {
|
|
NONE,
|
|
MIRROR, ## Horizontally mirror
|
|
FLIP, ## Vertically flip
|
|
REFLECT, ## All four reflections
|
|
ROTATE_CLOCKWISE,
|
|
ROTATE_COUNTER_CLOCKWISE,
|
|
ROTATE_180,
|
|
ROTATE_ALL, ## All four rotated forms
|
|
ALL ## All rotated and reflected forms
|
|
}
|
|
|
|
|
|
func _intersect(first: Array, second: Array) -> bool:
|
|
if first.size() > second.size():
|
|
return _intersect(second, first) # Array 'has' is fast compared to gdscript loop
|
|
for f in first:
|
|
if second.has(f):
|
|
return true
|
|
return false
|
|
|
|
|
|
# Meta-data functions
|
|
|
|
func _get_terrain_meta(ts: TileSet) -> Dictionary:
|
|
return ts.get_meta(TERRAIN_META) if ts and ts.has_meta(TERRAIN_META) else {
|
|
terrains = [],
|
|
decoration = ["Decoration", Color.DIM_GRAY, TerrainType.DECORATION, [], {path = "res://addons/better-terrain/icons/Decoration.svg"}],
|
|
version = TERRAIN_SYSTEM_VERSION
|
|
}
|
|
|
|
|
|
func _set_terrain_meta(ts: TileSet, meta : Dictionary) -> void:
|
|
ts.set_meta(TERRAIN_META, meta)
|
|
ts.emit_changed()
|
|
|
|
|
|
func _get_tile_meta(td: TileData) -> Dictionary:
|
|
return td.get_meta(TERRAIN_META) if td.has_meta(TERRAIN_META) else {
|
|
type = TileCategory.NON_TERRAIN
|
|
}
|
|
|
|
|
|
func _set_tile_meta(ts: TileSet, td: TileData, meta) -> void:
|
|
td.set_meta(TERRAIN_META, meta)
|
|
ts.emit_changed()
|
|
|
|
|
|
func _get_cache(ts: TileSet) -> Array:
|
|
if _tile_cache.has(ts):
|
|
return _tile_cache[ts]
|
|
|
|
var cache := []
|
|
if !ts:
|
|
return cache
|
|
_tile_cache[ts] = cache
|
|
|
|
var watcher = Node.new()
|
|
watcher.set_script(load("res://addons/better-terrain/Watcher.gd"))
|
|
watcher.tileset = ts
|
|
watcher.trigger.connect(_purge_cache.bind(ts))
|
|
add_child(watcher)
|
|
ts.changed.connect(watcher.activate)
|
|
|
|
var types = {}
|
|
|
|
var ts_meta := _get_terrain_meta(ts)
|
|
for t in ts_meta.terrains.size():
|
|
var terrain = ts_meta.terrains[t]
|
|
var bits = terrain[3].duplicate()
|
|
bits.push_back(t)
|
|
types[t] = bits
|
|
cache.push_back([])
|
|
|
|
# Decoration
|
|
types[-1] = [TileCategory.EMPTY]
|
|
cache.push_back([[-1, Vector2.ZERO, -1, {}, 1.0]])
|
|
|
|
for s in ts.get_source_count():
|
|
var source_id := ts.get_source_id(s)
|
|
var source := ts.get_source(source_id) as TileSetAtlasSource
|
|
if !source:
|
|
continue
|
|
source.changed.connect(watcher.activate)
|
|
for c in source.get_tiles_count():
|
|
var coord := source.get_tile_id(c)
|
|
for a in source.get_alternative_tiles_count(coord):
|
|
var alternate := source.get_alternative_tile_id(coord, a)
|
|
var td := source.get_tile_data(coord, alternate)
|
|
var td_meta := _get_tile_meta(td)
|
|
if td_meta.type < TileCategory.EMPTY or td_meta.type >= cache.size():
|
|
continue
|
|
|
|
td.changed.connect(watcher.activate)
|
|
var peering := {}
|
|
for key in td_meta.keys():
|
|
if !(key is int):
|
|
continue
|
|
|
|
var targets := []
|
|
for k in types:
|
|
if _intersect(types[k], td_meta[key]):
|
|
targets.push_back(k)
|
|
|
|
peering[key] = targets
|
|
|
|
# Decoration tiles without peering are skipped
|
|
if td_meta.type == TileCategory.EMPTY and !peering:
|
|
continue
|
|
|
|
var symmetry = td_meta.get("symmetry", SymmetryType.NONE)
|
|
# Branch out no symmetry tiles early
|
|
if symmetry == SymmetryType.NONE:
|
|
cache[td_meta.type].push_back([source_id, coord, alternate, peering, td.probability])
|
|
continue
|
|
|
|
# calculate the symmetry order for this tile
|
|
var symmetry_order := 0
|
|
for flags in data.symmetry_mapping[symmetry]:
|
|
var symmetric_peering = data.peering_bits_after_symmetry(peering, flags)
|
|
if symmetric_peering == peering:
|
|
symmetry_order += 1
|
|
|
|
var adjusted_probability = td.probability / symmetry_order
|
|
for flags in data.symmetry_mapping[symmetry]:
|
|
var symmetric_peering = data.peering_bits_after_symmetry(peering, flags)
|
|
cache[td_meta.type].push_back([source_id, coord, alternate | flags, symmetric_peering, adjusted_probability])
|
|
|
|
return cache
|
|
|
|
|
|
func _get_cache_terrain(ts_meta : Dictionary, index: int) -> Array:
|
|
# the cache and the terrains in ts_meta don't line up because
|
|
# decorations are cached too
|
|
if index < 0 or index >= ts_meta.terrains.size():
|
|
return ts_meta.decoration
|
|
return ts_meta.terrains[index]
|
|
|
|
|
|
func _purge_cache(ts: TileSet) -> void:
|
|
_tile_cache.erase(ts)
|
|
for c in get_children():
|
|
if c.tileset == ts:
|
|
c.tidy()
|
|
break
|
|
|
|
|
|
func _clear_invalid_peering_types(ts: TileSet) -> void:
|
|
var ts_meta := _get_terrain_meta(ts)
|
|
|
|
var cache := _get_cache(ts)
|
|
for t in cache.size():
|
|
var type = _get_cache_terrain(ts_meta, t)[2]
|
|
var valid_peering_types = data.get_terrain_peering_cells(ts, type)
|
|
|
|
for c in cache[t]:
|
|
if c[0] < 0:
|
|
continue
|
|
var source := ts.get_source(c[0]) as TileSetAtlasSource
|
|
if !source:
|
|
continue
|
|
var td := source.get_tile_data(c[1], c[2])
|
|
var td_meta := _get_tile_meta(td)
|
|
|
|
for peering in c[3].keys():
|
|
if valid_peering_types.has(peering):
|
|
continue
|
|
td_meta.erase(peering)
|
|
|
|
_set_tile_meta(ts, td, td_meta)
|
|
|
|
# Not strictly necessary
|
|
_purge_cache(ts)
|
|
|
|
|
|
func _has_invalid_peering_types(ts: TileSet) -> bool:
|
|
var ts_meta := _get_terrain_meta(ts)
|
|
|
|
var cache := _get_cache(ts)
|
|
for t in cache.size():
|
|
var type = _get_cache_terrain(ts_meta, t)[2]
|
|
var valid_peering_types = data.get_terrain_peering_cells(ts, type)
|
|
|
|
for c in cache[t]:
|
|
for peering in c[3].keys():
|
|
if !valid_peering_types.has(peering):
|
|
return true
|
|
|
|
return false
|
|
|
|
|
|
func _update_terrain_data(ts: TileSet) -> void:
|
|
var ts_meta = _get_terrain_meta(ts)
|
|
var previous_version = ts_meta.get("version")
|
|
|
|
# First release: no version info
|
|
if !ts_meta.has("version"):
|
|
ts_meta["version"] = "0.0"
|
|
|
|
# 0.0 -> 0.1: add categories
|
|
if ts_meta.version == "0.0":
|
|
for t in ts_meta.terrains:
|
|
if t.size() == 3:
|
|
t.push_back([])
|
|
ts_meta.version = "0.1"
|
|
|
|
# 0.1 -> 0.2: add decoration tiles and terrain icons
|
|
if ts_meta.version == "0.1":
|
|
# Add terrain icon containers
|
|
for t in ts_meta.terrains:
|
|
if t.size() == 4:
|
|
t.push_back({})
|
|
|
|
# Add default decoration data
|
|
ts_meta["decoration"] = ["Decoration", Color.DIM_GRAY, TerrainType.DECORATION, [], {path = "res://addons/better-terrain/icons/Decoration.svg"}]
|
|
ts_meta.version = "0.2"
|
|
|
|
if previous_version != ts_meta.version:
|
|
_set_terrain_meta(ts, ts_meta)
|
|
|
|
|
|
func _weighted_selection(choices: Array, apply_empty_probability: bool):
|
|
if choices.is_empty():
|
|
return null
|
|
|
|
var weight = choices.reduce(func(a, c): return a + c[4], 0.0)
|
|
|
|
if apply_empty_probability and weight < 1.0 and rng.randf() > weight:
|
|
return [-1, Vector2.ZERO, -1, null, 1.0]
|
|
|
|
if choices.size() == 1:
|
|
return choices[0]
|
|
|
|
if weight == 0.0:
|
|
return choices[rng.randi() % choices.size()]
|
|
|
|
var pick = rng.randf() * weight
|
|
for c in choices:
|
|
if pick < c[4]:
|
|
return c
|
|
pick -= c[4]
|
|
return choices.back()
|
|
|
|
|
|
func _weighted_selection_seeded(choices: Array, coord: Vector2i, apply_empty_probability: bool):
|
|
if use_seed:
|
|
rng.seed = hash(coord)
|
|
return _weighted_selection(choices, apply_empty_probability)
|
|
|
|
|
|
func _update_tile_tiles(tm: TileMapLayer, coord: Vector2i, types: Dictionary, cache: Array, apply_empty_probability: bool):
|
|
var type = types[coord]
|
|
|
|
const reward := 3
|
|
var penalty := -2000 if apply_empty_probability else -10
|
|
|
|
var best_score := -1000 # Impossibly bad score
|
|
var best := []
|
|
for t in cache[type]:
|
|
var score := 0
|
|
for peering in t[3]:
|
|
score += reward if t[3][peering].has(types[tm.get_neighbor_cell(coord, peering)]) else penalty
|
|
if score > best_score:
|
|
best_score = score
|
|
best = [t]
|
|
elif score == best_score:
|
|
best.append(t)
|
|
|
|
return _weighted_selection_seeded(best, coord, apply_empty_probability)
|
|
|
|
|
|
func _probe(tm: TileMapLayer, coord: Vector2i, peering: int, type: int, types: Dictionary) -> int:
|
|
var targets = data.associated_vertex_cells(tm, coord, peering)
|
|
targets = targets.map(func(c): return types[c])
|
|
|
|
var first = targets[0]
|
|
if targets.all(func(t): return t == first):
|
|
return first
|
|
|
|
# if different, use the lowest non-same
|
|
targets = targets.filter(func(t): return t != type)
|
|
return targets.reduce(func(a, t): return min(a, t))
|
|
|
|
|
|
func _update_tile_vertices(tm: TileMapLayer, coord: Vector2i, types: Dictionary, cache: Array):
|
|
var type = types[coord]
|
|
|
|
const reward := 3
|
|
const penalty := -10
|
|
|
|
var best_score := -1000 # Impossibly bad score
|
|
var best := []
|
|
for t in cache[type]:
|
|
var score := 0
|
|
for peering in t[3]:
|
|
score += reward if _probe(tm, coord, peering, type, types) in t[3][peering] else penalty
|
|
|
|
if score > best_score:
|
|
best_score = score
|
|
best = [t]
|
|
elif score == best_score:
|
|
best.append(t)
|
|
|
|
return _weighted_selection_seeded(best, coord, false)
|
|
|
|
|
|
func _update_tile_immediate(tm: TileMapLayer, coord: Vector2i, ts_meta: Dictionary, types: Dictionary, cache: Array) -> void:
|
|
var type = types[coord]
|
|
if type < TileCategory.EMPTY or type >= ts_meta.terrains.size():
|
|
return
|
|
|
|
var placement
|
|
var terrain = _get_cache_terrain(ts_meta, type)
|
|
if terrain[2] in [TerrainType.MATCH_TILES, TerrainType.DECORATION]:
|
|
placement = _update_tile_tiles(tm, coord, types, cache, true)
|
|
elif terrain[2] == TerrainType.MATCH_VERTICES:
|
|
placement = _update_tile_vertices(tm, coord, types, cache)
|
|
else:
|
|
return
|
|
|
|
if placement:
|
|
tm.set_cell(coord, placement[0], placement[1], placement[2])
|
|
|
|
|
|
func _update_tile_deferred(tm: TileMapLayer, coord: Vector2i, ts_meta: Dictionary, types: Dictionary, cache: Array):
|
|
var type = types[coord]
|
|
if type >= TileCategory.EMPTY and type < ts_meta.terrains.size():
|
|
var terrain = _get_cache_terrain(ts_meta, type)
|
|
if terrain[2] in [TerrainType.MATCH_TILES, TerrainType.DECORATION]:
|
|
return _update_tile_tiles(tm, coord, types, cache, terrain[2] == TerrainType.DECORATION)
|
|
elif terrain[2] == TerrainType.MATCH_VERTICES:
|
|
return _update_tile_vertices(tm, coord, types, cache)
|
|
return null
|
|
|
|
|
|
func _widen(tm: TileMapLayer, coords: Array) -> Array:
|
|
var result := {}
|
|
var peering_neighbors = data.get_terrain_peering_cells(tm.tile_set, TerrainType.MATCH_TILES)
|
|
for c in coords:
|
|
result[c] = true
|
|
var neighbors = data.neighboring_coords(tm, c, peering_neighbors)
|
|
for t in neighbors:
|
|
result[t] = true
|
|
return result.keys()
|
|
|
|
|
|
func _widen_with_exclusion(tm: TileMapLayer, coords: Array, exclusion: Rect2i) -> Array:
|
|
var result := {}
|
|
var peering_neighbors = data.get_terrain_peering_cells(tm.tile_set, TerrainType.MATCH_TILES)
|
|
for c in coords:
|
|
if !exclusion.has_point(c):
|
|
result[c] = true
|
|
var neighbors = data.neighboring_coords(tm, c, peering_neighbors)
|
|
for t in neighbors:
|
|
if !exclusion.has_point(t):
|
|
result[t] = true
|
|
return result.keys()
|
|
|
|
# Terrains
|
|
|
|
## Returns an [Array] of categories. These are the terrains in the [TileSet] which
|
|
## are marked with [enum TerrainType] of [code]CATEGORY[/code]. Each entry in the
|
|
## array is a [Dictionary] with [code]name[/code], [code]color[/code], and [code]id[/code].
|
|
func get_terrain_categories(ts: TileSet) -> Array:
|
|
var result := []
|
|
if !ts:
|
|
return result
|
|
|
|
var ts_meta := _get_terrain_meta(ts)
|
|
for id in ts_meta.terrains.size():
|
|
var t = ts_meta.terrains[id]
|
|
if t[2] == TerrainType.CATEGORY:
|
|
result.push_back({name = t[0], color = t[1], id = id})
|
|
|
|
return result
|
|
|
|
|
|
## Adds a new terrain to the [TileSet]. Returns [code]true[/code] if this is successful.
|
|
## [br][br]
|
|
## [code]type[/code] must be one of [enum TerrainType].[br]
|
|
## [code]categories[/code] is an indexed list of terrain categories that this terrain
|
|
## can match as. The indexes must be valid terrains of the CATEGORY type.
|
|
## [code]icon[/code] is a [Dictionary] with either a [code]path[/code] string pointing
|
|
## to a resource, or a [code]source_id[/code] [int] and a [code]coord[/code] [Vector2i].
|
|
## The former takes priority if both are present.
|
|
func add_terrain(ts: TileSet, name: String, color: Color, type: int, categories: Array = [], icon: Dictionary = {}) -> bool:
|
|
if !ts or name.is_empty() or type < 0 or type == TerrainType.DECORATION or type >= TerrainType.MAX:
|
|
return false
|
|
|
|
var ts_meta := _get_terrain_meta(ts)
|
|
|
|
# check categories
|
|
if type == TerrainType.CATEGORY and !categories.is_empty():
|
|
return false
|
|
for c in categories:
|
|
if c < 0 or c >= ts_meta.terrains.size() or ts_meta.terrains[c][2] != TerrainType.CATEGORY:
|
|
return false
|
|
|
|
if icon and not (icon.has("path") or (icon.has("source_id") and icon.has("coord"))):
|
|
return false
|
|
|
|
ts_meta.terrains.push_back([name, color, type, categories, icon])
|
|
_set_terrain_meta(ts, ts_meta)
|
|
_purge_cache(ts)
|
|
return true
|
|
|
|
|
|
## Removes the terrain at [code]index[/code] from the [TileSet]. Returns [code]true[/code]
|
|
## if the deletion is successful.
|
|
func remove_terrain(ts: TileSet, index: int) -> bool:
|
|
if !ts or index < 0:
|
|
return false
|
|
|
|
var ts_meta := _get_terrain_meta(ts)
|
|
if index >= ts_meta.terrains.size():
|
|
return false
|
|
|
|
if ts_meta.terrains[index][2] == TerrainType.CATEGORY:
|
|
for t in ts_meta.terrains:
|
|
t[3].erase(index)
|
|
|
|
for s in ts.get_source_count():
|
|
var source := ts.get_source(ts.get_source_id(s)) as TileSetAtlasSource
|
|
if !source:
|
|
continue
|
|
for t in source.get_tiles_count():
|
|
var coord := source.get_tile_id(t)
|
|
for a in source.get_alternative_tiles_count(coord):
|
|
var alternate := source.get_alternative_tile_id(coord, a)
|
|
var td := source.get_tile_data(coord, alternate)
|
|
|
|
var td_meta := _get_tile_meta(td)
|
|
if td_meta.type == TileCategory.NON_TERRAIN:
|
|
continue
|
|
|
|
if td_meta.type == index:
|
|
_set_tile_meta(ts, td, null)
|
|
continue
|
|
|
|
if td_meta.type > index:
|
|
td_meta.type -= 1
|
|
|
|
for peering in td_meta.keys():
|
|
if !(peering is int):
|
|
continue
|
|
|
|
var fixed_peering = []
|
|
for p in td_meta[peering]:
|
|
if p < index:
|
|
fixed_peering.append(p)
|
|
elif p > index:
|
|
fixed_peering.append(p - 1)
|
|
|
|
if fixed_peering.is_empty():
|
|
td_meta.erase(peering)
|
|
else:
|
|
td_meta[peering] = fixed_peering
|
|
|
|
_set_tile_meta(ts, td, td_meta)
|
|
|
|
ts_meta.terrains.remove_at(index)
|
|
_set_terrain_meta(ts, ts_meta)
|
|
|
|
_purge_cache(ts)
|
|
return true
|
|
|
|
|
|
## Returns the number of terrains in the [TileSet].
|
|
func terrain_count(ts: TileSet) -> int:
|
|
if !ts:
|
|
return 0
|
|
|
|
var ts_meta := _get_terrain_meta(ts)
|
|
return ts_meta.terrains.size()
|
|
|
|
|
|
## Retrieves information about the terrain at [code]index[/code] in the [TileSet].
|
|
## [br][br]
|
|
## Returns a [Dictionary] describing the terrain. If it succeeds, the key [code]valid[/code]
|
|
## will be set to [code]true[/code]. Other keys are [code]name[/code], [code]color[/code],
|
|
## [code]type[/code] (a [enum TerrainType]), [code]categories[/code] which is
|
|
## an [Array] of category type terrains that this terrain matches as, and
|
|
## [code]icon[/code] which is a [Dictionary] with a [code]path[/code] [String] or
|
|
## a [code]source_id[/code] [int] and [code]coord[/code] [Vector2i]
|
|
func get_terrain(ts: TileSet, index: int) -> Dictionary:
|
|
if !ts or index < TileCategory.EMPTY:
|
|
return {valid = false}
|
|
|
|
var ts_meta := _get_terrain_meta(ts)
|
|
if index >= ts_meta.terrains.size():
|
|
return {valid = false}
|
|
|
|
var terrain := _get_cache_terrain(ts_meta, index)
|
|
return {
|
|
id = index,
|
|
name = terrain[0],
|
|
color = terrain[1],
|
|
type = terrain[2],
|
|
categories = terrain[3].duplicate(),
|
|
icon = terrain[4].duplicate(),
|
|
valid = true
|
|
}
|
|
|
|
|
|
## Updates the details of the terrain at [code]index[/code] in [TileSet]. Returns
|
|
## [code]true[/code] if this succeeds.
|
|
## [br][br]
|
|
## If supplied, the [code]categories[/code] must be a list of indexes to other [code]CATEGORY[/code]
|
|
## type terrains.
|
|
## [code]icon[/code] is a [Dictionary] with either a [code]path[/code] string pointing
|
|
## to a resource, or a [code]source_id[/code] [int] and a [code]coord[/code] [Vector2i].
|
|
func set_terrain(ts: TileSet, index: int, name: String, color: Color, type: int, categories: Array = [], icon: Dictionary = {valid = false}) -> bool:
|
|
if !ts or name.is_empty() or index < 0 or type < 0 or type == TerrainType.DECORATION or type >= TerrainType.MAX:
|
|
return false
|
|
|
|
var ts_meta := _get_terrain_meta(ts)
|
|
if index >= ts_meta.terrains.size():
|
|
return false
|
|
|
|
if type == TerrainType.CATEGORY and !categories.is_empty():
|
|
return false
|
|
for c in categories:
|
|
if c < 0 or c == index or c >= ts_meta.terrains.size() or ts_meta.terrains[c][2] != TerrainType.CATEGORY:
|
|
return false
|
|
|
|
var icon_valid = icon.get("valid", "true")
|
|
if icon_valid:
|
|
match icon:
|
|
{}, {"path"}, {"source_id", "coord"}: pass
|
|
_: return false
|
|
|
|
if type != TerrainType.CATEGORY:
|
|
for t in ts_meta.terrains:
|
|
t[3].erase(index)
|
|
|
|
ts_meta.terrains[index] = [name, color, type, categories, icon]
|
|
_set_terrain_meta(ts, ts_meta)
|
|
|
|
_clear_invalid_peering_types(ts)
|
|
_purge_cache(ts)
|
|
return true
|
|
|
|
|
|
## Swaps the terrains at [code]index1[/code] and [code]index2[/code] in [TileSet].
|
|
func swap_terrains(ts: TileSet, index1: int, index2: int) -> bool:
|
|
if !ts or index1 < 0 or index2 < 0 or index1 == index2:
|
|
return false
|
|
|
|
var ts_meta := _get_terrain_meta(ts)
|
|
if index1 >= ts_meta.terrains.size() or index2 >= ts_meta.terrains.size():
|
|
return false
|
|
|
|
for t in ts_meta.terrains:
|
|
var has1 = t[3].has(index1)
|
|
var has2 = t[3].has(index2)
|
|
|
|
if has1 and !has2:
|
|
t[3].erase(index1)
|
|
t[3].push_back(index2)
|
|
elif has2 and !has1:
|
|
t[3].erase(index2)
|
|
t[3].push_back(index1)
|
|
|
|
for s in ts.get_source_count():
|
|
var source := ts.get_source(ts.get_source_id(s)) as TileSetAtlasSource
|
|
if !source:
|
|
continue
|
|
for t in source.get_tiles_count():
|
|
var coord := source.get_tile_id(t)
|
|
for a in source.get_alternative_tiles_count(coord):
|
|
var alternate := source.get_alternative_tile_id(coord, a)
|
|
var td := source.get_tile_data(coord, alternate)
|
|
|
|
var td_meta := _get_tile_meta(td)
|
|
if td_meta.type == TileCategory.NON_TERRAIN:
|
|
continue
|
|
|
|
if td_meta.type == index1:
|
|
td_meta.type = index2
|
|
elif td_meta.type == index2:
|
|
td_meta.type = index1
|
|
|
|
for peering in td_meta.keys():
|
|
if !(peering is int):
|
|
continue
|
|
|
|
var fixed_peering = []
|
|
for p in td_meta[peering]:
|
|
if p == index1:
|
|
fixed_peering.append(index2)
|
|
elif p == index2:
|
|
fixed_peering.append(index1)
|
|
else:
|
|
fixed_peering.append(p)
|
|
td_meta[peering] = fixed_peering
|
|
|
|
_set_tile_meta(ts, td, td_meta)
|
|
|
|
var temp = ts_meta.terrains[index1]
|
|
ts_meta.terrains[index1] = ts_meta.terrains[index2]
|
|
ts_meta.terrains[index2] = temp
|
|
_set_terrain_meta(ts, ts_meta)
|
|
|
|
_purge_cache(ts)
|
|
return true
|
|
|
|
|
|
# Terrain tile data
|
|
|
|
## For a tile in a [TileSet] as specified by [TileData], set the terrain associated
|
|
## with that tile to [code]type[/code], which is an index of an existing terrain.
|
|
## Returns [code]true[/code] on success.
|
|
func set_tile_terrain_type(ts: TileSet, td: TileData, type: int) -> bool:
|
|
if !ts or !td or type < TileCategory.NON_TERRAIN:
|
|
return false
|
|
|
|
var td_meta = _get_tile_meta(td)
|
|
td_meta.type = type
|
|
if type == TileCategory.NON_TERRAIN:
|
|
td_meta = null
|
|
_set_tile_meta(ts, td, td_meta)
|
|
|
|
_clear_invalid_peering_types(ts)
|
|
_purge_cache(ts)
|
|
return true
|
|
|
|
|
|
## Returns the terrain type associated with tile specified by [TileData]. Returns
|
|
## -1 if the tile has no associated terrain.
|
|
func get_tile_terrain_type(td: TileData) -> int:
|
|
if !td:
|
|
return TileCategory.ERROR
|
|
var td_meta := _get_tile_meta(td)
|
|
return td_meta.type
|
|
|
|
|
|
## For a tile represented by [TileData] [code]td[/code] in [TileSet]
|
|
## [code]ts[/code], sets [enum SymmetryType] [code]type[/code]. This controls
|
|
## how the tile is rotated/mirrored during placement.
|
|
func set_tile_symmetry_type(ts: TileSet, td: TileData, type: int) -> bool:
|
|
if !ts or !td or type < SymmetryType.NONE or type > SymmetryType.ALL:
|
|
return false
|
|
|
|
var td_meta := _get_tile_meta(td)
|
|
if td_meta.type == TileCategory.NON_TERRAIN:
|
|
return false
|
|
|
|
td_meta.symmetry = type
|
|
_set_tile_meta(ts, td, td_meta)
|
|
_purge_cache(ts)
|
|
return true
|
|
|
|
|
|
## For a tile [code]td[/code], returns the [enum SymmetryType] which that
|
|
## tile uses.
|
|
func get_tile_symmetry_type(td: TileData) -> int:
|
|
if !td:
|
|
return SymmetryType.NONE
|
|
|
|
var td_meta := _get_tile_meta(td)
|
|
return td_meta.get("symmetry", SymmetryType.NONE)
|
|
|
|
|
|
## Returns an Array of all [TileData] tiles included in the specified
|
|
## terrain [code]type[/code] for the [TileSet] [code]ts[/code]
|
|
func get_tiles_in_terrain(ts: TileSet, type: int) -> Array[TileData]:
|
|
var result:Array[TileData] = []
|
|
if !ts or type < TileCategory.EMPTY:
|
|
return result
|
|
|
|
var cache := _get_cache(ts)
|
|
if type > cache.size():
|
|
return result
|
|
|
|
var tiles = cache[type]
|
|
if !tiles:
|
|
return result
|
|
for c in tiles:
|
|
if c[0] < 0:
|
|
continue
|
|
var source := ts.get_source(c[0]) as TileSetAtlasSource
|
|
var td := source.get_tile_data(c[1], c[2])
|
|
result.push_back(td)
|
|
|
|
return result
|
|
|
|
|
|
## Returns an [Array] of [Dictionary] items including information about each
|
|
## tile included in the specified terrain [code]type[/code] for
|
|
## the [TileSet] [code]ts[/code]. Each Dictionary item includes
|
|
## [TileSetAtlasSource] [code]source[/code], [TileData] [code]td[/code],
|
|
## [Vector2i] [code]coord[/code], and [int] [code]alt_id[/code].
|
|
func get_tile_sources_in_terrain(ts: TileSet, type: int) -> Array[Dictionary]:
|
|
var result:Array[Dictionary] = []
|
|
|
|
var cache := _get_cache(ts)
|
|
var tiles = cache[type]
|
|
if !tiles:
|
|
return result
|
|
for c in tiles:
|
|
if c[0] < 0:
|
|
continue
|
|
var source := ts.get_source(c[0]) as TileSetAtlasSource
|
|
if not source:
|
|
continue
|
|
var td := source.get_tile_data(c[1], c[2])
|
|
result.push_back({
|
|
source = source,
|
|
td = td,
|
|
coord = c[1],
|
|
alt_id = c[2]
|
|
})
|
|
|
|
return result
|
|
|
|
|
|
## For a [TileSet]'s tile, specified by [TileData], add terrain [code]type[/code]
|
|
## (an index of a terrain) to match this tile in direction [code]peering[/code],
|
|
## which is of type [enum TileSet.CellNeighbor]. Returns [code]true[/code] on success.
|
|
func add_tile_peering_type(ts: TileSet, td: TileData, peering: int, type: int) -> bool:
|
|
if !ts or !td or peering < 0 or peering > 15 or type < TileCategory.EMPTY:
|
|
return false
|
|
|
|
var ts_meta := _get_terrain_meta(ts)
|
|
var td_meta := _get_tile_meta(td)
|
|
if td_meta.type < TileCategory.EMPTY or td_meta.type >= ts_meta.terrains.size():
|
|
return false
|
|
|
|
if !td_meta.has(peering):
|
|
td_meta[peering] = [type]
|
|
elif !td_meta[peering].has(type):
|
|
td_meta[peering].append(type)
|
|
else:
|
|
return false
|
|
_set_tile_meta(ts, td, td_meta)
|
|
_purge_cache(ts)
|
|
return true
|
|
|
|
|
|
## For a [TileSet]'s tile, specified by [TileData], remove terrain [code]type[/code]
|
|
## from matching in direction [code]peering[/code], which is of type [enum TileSet.CellNeighbor].
|
|
## Returns [code]true[/code] on success.
|
|
func remove_tile_peering_type(ts: TileSet, td: TileData, peering: int, type: int) -> bool:
|
|
if !ts or !td or peering < 0 or peering > 15 or type < TileCategory.EMPTY:
|
|
return false
|
|
|
|
var td_meta := _get_tile_meta(td)
|
|
if !td_meta.has(peering):
|
|
return false
|
|
if !td_meta[peering].has(type):
|
|
return false
|
|
td_meta[peering].erase(type)
|
|
if td_meta[peering].is_empty():
|
|
td_meta.erase(peering)
|
|
_set_tile_meta(ts, td, td_meta)
|
|
_purge_cache(ts)
|
|
return true
|
|
|
|
|
|
## For the tile specified by [TileData], return an [Array] of peering directions
|
|
## for which terrain matching is set up. These will be of type [enum TileSet.CellNeighbor].
|
|
func tile_peering_keys(td: TileData) -> Array:
|
|
if !td:
|
|
return []
|
|
|
|
var td_meta := _get_tile_meta(td)
|
|
var result := []
|
|
for k in td_meta:
|
|
if k is int:
|
|
result.append(k)
|
|
return result
|
|
|
|
|
|
## For the tile specified by [TileData], return the [Array] of terrains that match
|
|
## for the direction [code]peering[/code] which should be of type [enum TileSet.CellNeighbor].
|
|
func tile_peering_types(td: TileData, peering: int) -> Array:
|
|
if !td or peering < 0 or peering > 15:
|
|
return []
|
|
|
|
var td_meta := _get_tile_meta(td)
|
|
return td_meta[peering].duplicate() if td_meta.has(peering) else []
|
|
|
|
|
|
## For the tile specified by [TileData], return the [Array] of peering directions
|
|
## for the specified terrain type [code]type[/code].
|
|
func tile_peering_for_type(td: TileData, type: int) -> Array:
|
|
if !td:
|
|
return []
|
|
|
|
var td_meta := _get_tile_meta(td)
|
|
var result := []
|
|
var sides := tile_peering_keys(td)
|
|
for side in sides:
|
|
if td_meta[side].has(type):
|
|
result.push_back(side)
|
|
|
|
result.sort()
|
|
return result
|
|
|
|
|
|
# Painting
|
|
|
|
## Applies the terrain [code]type[/code] to the [TileMapLayer] for the [Vector2i]
|
|
## [code]coord[/code]. Returns [code]true[/code] if it succeeds. Use [method set_cells]
|
|
## to change multiple tiles at once.
|
|
## [br][br]
|
|
## Use terrain type -1 to erase cells.
|
|
func set_cell(tm: TileMapLayer, coord: Vector2i, type: int) -> bool:
|
|
if !tm or !tm.tile_set or type < TileCategory.EMPTY:
|
|
return false
|
|
|
|
if type == TileCategory.EMPTY:
|
|
tm.erase_cell(coord)
|
|
return true
|
|
|
|
var cache := _get_cache(tm.tile_set)
|
|
if type >= cache.size():
|
|
return false
|
|
|
|
if cache[type].is_empty():
|
|
return false
|
|
|
|
var tile = cache[type].front()
|
|
tm.set_cell(coord, tile[0], tile[1], tile[2])
|
|
return true
|
|
|
|
|
|
## Applies the terrain [code]type[/code] to the [TileMapLayer] for the
|
|
## [Vector2i] [code]coords[/code]. Returns [code]true[/code] if it succeeds.
|
|
## [br][br]
|
|
## Note that this does not cause the terrain solver to run, so this will just place
|
|
## an arbitrary terrain-associated tile in the given position. To run the solver,
|
|
## you must set the require cells, and then call either [method update_terrain_cell],
|
|
## [method update_terrain_cels], or [method update_terrain_area].
|
|
## [br][br]
|
|
## If you want to prepare changes to the tiles in advance, you can use [method create_terrain_changeset]
|
|
## and the associated functions.
|
|
## [br][br]
|
|
## Use terrain type -1 to erase cells.
|
|
func set_cells(tm: TileMapLayer, coords: Array, type: int) -> bool:
|
|
if !tm or !tm.tile_set or type < TileCategory.EMPTY:
|
|
return false
|
|
|
|
if type == TileCategory.EMPTY:
|
|
for c in coords:
|
|
tm.erase_cell(c)
|
|
return true
|
|
|
|
var cache := _get_cache(tm.tile_set)
|
|
if type >= cache.size():
|
|
return false
|
|
|
|
if cache[type].is_empty():
|
|
return false
|
|
|
|
var tile = cache[type].front()
|
|
for c in coords:
|
|
tm.set_cell(c, tile[0], tile[1], tile[2])
|
|
return true
|
|
|
|
|
|
## Replaces an existing tile on the [TileMapLayer] for the [Vector2i]
|
|
## [code]coord[/code] with a new tile in the provided terrain [code]type[/code]
|
|
## *only if* there is a tile with a matching set of peering sides in this terrain.
|
|
## Returns [code]true[/code] if any tiles were changed. Use [method replace_cells]
|
|
## to replace multiple tiles at once.
|
|
func replace_cell(tm: TileMapLayer, coord: Vector2i, type: int) -> bool:
|
|
if !tm or !tm.tile_set or type < 0:
|
|
return false
|
|
|
|
var cache := _get_cache(tm.tile_set)
|
|
if type >= cache.size():
|
|
return false
|
|
|
|
if cache[type].is_empty():
|
|
return false
|
|
|
|
var td = tm.get_cell_tile_data(coord)
|
|
if !td:
|
|
return false
|
|
|
|
var ts_meta := _get_terrain_meta(tm.tile_set)
|
|
var categories = ts_meta.terrains[type][3]
|
|
var check_types = [type] + categories
|
|
|
|
for check_type in check_types:
|
|
var placed_peering = tile_peering_for_type(td, check_type)
|
|
for pt in get_tiles_in_terrain(tm.tile_set, type):
|
|
var check_peering := tile_peering_for_type(pt, check_type)
|
|
if placed_peering == check_peering:
|
|
var tile = cache[type].front()
|
|
tm.set_cell(coord, tile[0], tile[1], tile[2])
|
|
return true
|
|
|
|
return false
|
|
|
|
|
|
## Replaces existing tiles on the [TileMapLayer] for the [Vector2i]
|
|
## [code]coords[/code] with new tiles in the provided terrain [code]type[/code]
|
|
## *only if* there is a tile with a matching set of peering sides in this terrain
|
|
## for each tile.
|
|
## Returns [code]true[/code] if any tiles were changed.
|
|
func replace_cells(tm: TileMapLayer, coords: Array, type: int) -> bool:
|
|
if !tm or !tm.tile_set or type < 0:
|
|
return false
|
|
|
|
var cache := _get_cache(tm.tile_set)
|
|
if type >= cache.size():
|
|
return false
|
|
|
|
if cache[type].is_empty():
|
|
return false
|
|
|
|
var ts_meta := _get_terrain_meta(tm.tile_set)
|
|
var categories = ts_meta.terrains[type][3]
|
|
var check_types = [type] + categories
|
|
|
|
var changed = false
|
|
var potential_tiles = get_tiles_in_terrain(tm.tile_set, type)
|
|
for c in coords:
|
|
var found = false
|
|
var td = tm.get_cell_tile_data(c)
|
|
if !td:
|
|
continue
|
|
for check_type in check_types:
|
|
var placed_peering = tile_peering_for_type(td, check_type)
|
|
for pt in potential_tiles:
|
|
var check_peering = tile_peering_for_type(pt, check_type)
|
|
if placed_peering == check_peering:
|
|
var tile = cache[type].front()
|
|
tm.set_cell(c, tile[0], tile[1], tile[2])
|
|
changed = true
|
|
found = true
|
|
break
|
|
|
|
if found:
|
|
break
|
|
|
|
return changed
|
|
|
|
|
|
## Returns the terrain type detected in the [TileMapLayer] at specified [Vector2i]
|
|
## [code]coord[/code]. Returns -1 if tile is not valid or does not contain a
|
|
## tile associated with a terrain.
|
|
func get_cell(tm: TileMapLayer, coord: Vector2i) -> int:
|
|
if !tm or !tm.tile_set:
|
|
return TileCategory.ERROR
|
|
|
|
if tm.get_cell_source_id(coord) == -1:
|
|
return TileCategory.EMPTY
|
|
|
|
var t := tm.get_cell_tile_data(coord)
|
|
if !t:
|
|
return TileCategory.NON_TERRAIN
|
|
|
|
return _get_tile_meta(t).type
|
|
|
|
|
|
## Runs the tile solving algorithm on the [TileMapLayer] for the given
|
|
## [Vector2i] coordinates in the [code]cells[/code] parameter. By default,
|
|
## the surrounding cells are also solved, but this can be adjusted by passing [code]false[/code]
|
|
## to the [code]and_surrounding_cells[/code] parameter.
|
|
## [br][br]
|
|
## See also [method update_terrain_area] and [method update_terrain_cell].
|
|
func update_terrain_cells(tm: TileMapLayer, cells: Array, and_surrounding_cells := true) -> void:
|
|
if !tm or !tm.tile_set:
|
|
return
|
|
|
|
if and_surrounding_cells:
|
|
cells = _widen(tm, cells)
|
|
var needed_cells := _widen(tm, cells)
|
|
|
|
var types := {}
|
|
for c in needed_cells:
|
|
types[c] = get_cell(tm, c)
|
|
|
|
var ts_meta := _get_terrain_meta(tm.tile_set)
|
|
var cache := _get_cache(tm.tile_set)
|
|
for c in cells:
|
|
_update_tile_immediate(tm, c, ts_meta, types, cache)
|
|
|
|
|
|
## Runs the tile solving algorithm on the [TileMapLayer] for the given [Vector2i]
|
|
## [code]cell[/code]. By default, the surrounding cells are also solved, but
|
|
## this can be adjusted by passing [code]false[/code] to the [code]and_surrounding_cells[/code]
|
|
## parameter. This calls through to [method update_terrain_cells].
|
|
func update_terrain_cell(tm: TileMapLayer, cell: Vector2i, and_surrounding_cells := true) -> void:
|
|
update_terrain_cells(tm, [cell], and_surrounding_cells)
|
|
|
|
|
|
## Runs the tile solving algorithm on the [TileMapLayer] for the given [Rect2i]
|
|
## [code]area[/code]. By default, the surrounding cells are also solved, but
|
|
## this can be adjusted by passing [code]false[/code] to the [code]and_surrounding_cells[/code]
|
|
## parameter.
|
|
## [br][br]
|
|
## See also [method update_terrain_cells].
|
|
func update_terrain_area(tm: TileMapLayer, area: Rect2i, and_surrounding_cells := true) -> void:
|
|
if !tm or !tm.tile_set:
|
|
return
|
|
|
|
# Normalize area and extend so tiles cover inclusive space
|
|
area = area.abs()
|
|
area.size += Vector2i.ONE
|
|
|
|
var edges = []
|
|
for x in range(area.position.x, area.end.x):
|
|
edges.append(Vector2i(x, area.position.y))
|
|
edges.append(Vector2i(x, area.end.y - 1))
|
|
for y in range(area.position.y + 1, area.end.y - 1):
|
|
edges.append(Vector2i(area.position.x, y))
|
|
edges.append(Vector2i(area.end.x - 1, y))
|
|
|
|
var additional_cells := []
|
|
var needed_cells := _widen_with_exclusion(tm, edges, area)
|
|
|
|
if and_surrounding_cells:
|
|
additional_cells = needed_cells
|
|
needed_cells = _widen_with_exclusion(tm, needed_cells, area)
|
|
|
|
var types := {}
|
|
for y in range(area.position.y, area.end.y):
|
|
for x in range(area.position.x, area.end.x):
|
|
var coord = Vector2i(x, y)
|
|
types[coord] = get_cell(tm, coord)
|
|
for c in needed_cells:
|
|
types[c] = get_cell(tm, c)
|
|
|
|
var ts_meta := _get_terrain_meta(tm.tile_set)
|
|
var cache := _get_cache(tm.tile_set)
|
|
for y in range(area.position.y, area.end.y):
|
|
for x in range(area.position.x, area.end.x):
|
|
var coord := Vector2i(x, y)
|
|
_update_tile_immediate(tm, coord, ts_meta, types, cache)
|
|
for c in additional_cells:
|
|
_update_tile_immediate(tm, c, ts_meta, types, cache)
|
|
|
|
|
|
## For a [TileMapLayer], create a changeset that will
|
|
## be calculated via a [WorkerThreadPool], so it will not delay processing the current
|
|
## frame or affect the framerate.
|
|
## [br][br]
|
|
## The [code]paint[/code] parameter must be a [Dictionary] with keys of type [Vector2i]
|
|
## representing map coordinates, and integer values representing terrain types.
|
|
## [br][br]
|
|
## Returns a [Dictionary] with internal details. See also [method is_terrain_changeset_ready],
|
|
## [method apply_terrain_changeset], and [method wait_for_terrain_changeset].
|
|
func create_terrain_changeset(tm: TileMapLayer, paint: Dictionary) -> Dictionary:
|
|
# Force cache rebuild if required
|
|
var _cache := _get_cache(tm.tile_set)
|
|
|
|
var cells := paint.keys()
|
|
var needed_cells := _widen(tm, cells)
|
|
|
|
var types := {}
|
|
for c in needed_cells:
|
|
types[c] = paint[c] if paint.has(c) else get_cell(tm, c)
|
|
|
|
var placements := []
|
|
placements.resize(cells.size())
|
|
|
|
var ts_meta := _get_terrain_meta(tm.tile_set)
|
|
var work := func(n: int):
|
|
placements[n] = _update_tile_deferred(tm, cells[n], ts_meta, types, _cache)
|
|
|
|
return {
|
|
"valid": true,
|
|
"tilemap": tm,
|
|
"cells": cells,
|
|
"placements": placements,
|
|
"group_id": WorkerThreadPool.add_group_task(work, cells.size(), -1, false, "BetterTerrain")
|
|
}
|
|
|
|
|
|
## Returns [code]true[/code] if a changeset created by [method create_terrain_changeset]
|
|
## has finished the threaded calculation and is ready to be applied by [method apply_terrain_changeset].
|
|
## See also [method wait_for_terrain_changeset].
|
|
func is_terrain_changeset_ready(change: Dictionary) -> bool:
|
|
if !change.has("group_id"):
|
|
return false
|
|
|
|
return WorkerThreadPool.is_group_task_completed(change.group_id)
|
|
|
|
|
|
## Blocks until a changeset created by [method create_terrain_changeset] finishes.
|
|
## This is useful to tidy up threaded work in the event that a node is to be removed
|
|
## whilst still waiting on threads.
|
|
## [br][br]
|
|
## Usage example:
|
|
## [codeblock]
|
|
## func _exit_tree():
|
|
## if changeset.valid:
|
|
## BetterTerrain.wait_for_terrain_changeset(changeset)
|
|
## [/codeblock]
|
|
func wait_for_terrain_changeset(change: Dictionary) -> void:
|
|
if change.has("group_id"):
|
|
WorkerThreadPool.wait_for_group_task_completion(change.group_id)
|
|
|
|
|
|
## Apply the changes in a changeset created by [method create_terrain_changeset]
|
|
## once it is confirmed by [method is_terrain_changeset_ready]. The changes will
|
|
## be applied to the [TileMapLayer] that the changeset was initialized with.
|
|
## [br][br]
|
|
## Completed changesets can be applied multiple times, and stored for as long as
|
|
## needed once calculated.
|
|
func apply_terrain_changeset(change: Dictionary) -> void:
|
|
for n in change.cells.size():
|
|
var placement = change.placements[n]
|
|
if placement:
|
|
change.tilemap.set_cell(change.cells[n], placement[0], placement[1], placement[2])
|