| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159 |
- @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])
|