working keyboard and plugin for better tilemaps
This commit is contained in:
70
addons/TileMapDual/AtlasWatcher.gd
Normal file
70
addons/TileMapDual/AtlasWatcher.gd
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
##[br] Watches a TileSetAtlasSource for changes.
|
||||||
|
##[br] Causes its 'parent' TileSetWatcher to emit terrains_changed when the atlas changes.
|
||||||
|
##[br] Also emits parent.atlas_autotiled when it thinks the user auto-generated atlas tiles.
|
||||||
|
class_name AtlasWatcher
|
||||||
|
|
||||||
|
## Prevents the number of seen atlases from extending to infinity.
|
||||||
|
const UNDO_LIMIT = 1024
|
||||||
|
## Stores all of the atlas instance id's that have been seen before, to prevent autogen on redo.
|
||||||
|
static var _registered_atlases := []
|
||||||
|
|
||||||
|
## The TileSetWatcher that created this AtlasWatcher. Used to send signals back.
|
||||||
|
var parent: TileSetWatcher
|
||||||
|
|
||||||
|
## The Source ID of `self.atlas`.
|
||||||
|
var sid: int
|
||||||
|
|
||||||
|
## The atlas to be watched for changes.
|
||||||
|
var atlas: TileSetAtlasSource
|
||||||
|
|
||||||
|
func _init(parent: TileSetWatcher, sid: int, atlas: TileSetAtlasSource) -> void:
|
||||||
|
self.parent = parent
|
||||||
|
self.sid = sid
|
||||||
|
self.atlas = atlas
|
||||||
|
atlas.changed.connect(_atlas_changed, ConnectFlags.CONNECT_DEFERRED)
|
||||||
|
var id := atlas.get_instance_id()
|
||||||
|
# should not autogen if atlas was created through redo, i.e. its instance id already existed
|
||||||
|
if _atlas_is_empty() and id not in _registered_atlases:
|
||||||
|
_registered_atlases.push_back(id)
|
||||||
|
if _registered_atlases.size() > UNDO_LIMIT:
|
||||||
|
_registered_atlases.pop_front()
|
||||||
|
atlas.changed.connect(_detect_autogen, ConnectFlags.CONNECT_DEFERRED | ConnectFlags.CONNECT_ONE_SHOT)
|
||||||
|
|
||||||
|
|
||||||
|
func _atlas_is_empty() -> bool:
|
||||||
|
return atlas.get_tiles_count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
## Returns true if the texture has any opaque pixels in the specified tile coordinates.
|
||||||
|
func _is_opaque_tile(image: Image, tile: Vector2i, p_threshold: float = 0.1) -> bool:
|
||||||
|
# We cannot use atlas.get_tile_texture_region(tile) as it fails on unregistered tiles.
|
||||||
|
var region := Rect2i(tile * atlas.texture_region_size, atlas.texture_region_size)
|
||||||
|
var sprite := image.get_region(region)
|
||||||
|
if sprite.is_invisible():
|
||||||
|
return false
|
||||||
|
# We're still not sure if the tile is empty or not.
|
||||||
|
# Godot's auto-gen considers 0.1 opacity as "transparent" but not "invisible".
|
||||||
|
for y in range(region.position.y, region.end.y):
|
||||||
|
for x in range(region.position.x, region.end.x):
|
||||||
|
if image.get_pixel(x, y).a > p_threshold:
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
##[br] HACK: literally just tries to guess which tiles the terrain autogen system would make
|
||||||
|
##[br] Called once, and only once, at the end of the first frame that a texture is created.
|
||||||
|
func _detect_autogen() -> void:
|
||||||
|
var size := Vector2i(atlas.texture.get_size()) / atlas.texture_region_size
|
||||||
|
var image := atlas.texture.get_image()
|
||||||
|
var expected_tiles := []
|
||||||
|
for y in size.y:
|
||||||
|
for x in size.x:
|
||||||
|
var tile := Vector2i(x, y)
|
||||||
|
if atlas.has_tile(tile) != _is_opaque_tile(image, tile):
|
||||||
|
return
|
||||||
|
parent.atlas_autotiled.emit(sid, atlas)
|
||||||
|
|
||||||
|
|
||||||
|
## Called every time the atlas changes. Simply flags that terrains have changed.
|
||||||
|
func _atlas_changed() -> void:
|
||||||
|
parent._flag_terrains_changed = true
|
||||||
1
addons/TileMapDual/AtlasWatcher.gd.uid
Normal file
1
addons/TileMapDual/AtlasWatcher.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cfhiw77a85x8w
|
||||||
41
addons/TileMapDual/CursorDual.gd
Normal file
41
addons/TileMapDual/CursorDual.gd
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
@icon('CursorDual.svg')
|
||||||
|
class_name CursorDual
|
||||||
|
extends Sprite2D
|
||||||
|
|
||||||
|
# TODO: instead of setting the target tilemap manually,
|
||||||
|
# just add the CursorDual as a child of the target tilemap
|
||||||
|
@export var tilemap_dual: TileMapDual = null
|
||||||
|
|
||||||
|
var cell: Vector2i
|
||||||
|
var tile_size: Vector2
|
||||||
|
var sprite_size: Vector2
|
||||||
|
var terrain := 1
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if tilemap_dual != null:
|
||||||
|
tile_size = tilemap_dual.tile_set.tile_size
|
||||||
|
sprite_size = self.texture.get_size()
|
||||||
|
scale = Vector2(tile_size.y, tile_size.y) / sprite_size
|
||||||
|
self.set_scale(scale)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if tilemap_dual == null:
|
||||||
|
return
|
||||||
|
cell = tilemap_dual.local_to_map(tilemap_dual.get_local_mouse_position())
|
||||||
|
global_position = tilemap_dual.to_global(tilemap_dual.map_to_local(cell))
|
||||||
|
# Clicking the 1 key activates the first terrain
|
||||||
|
if Input.is_action_pressed("quick_action_1"):
|
||||||
|
terrain = 1
|
||||||
|
# Clicking the 2 key activates the second terrain
|
||||||
|
if Input.is_action_pressed("quick_action_2"):
|
||||||
|
terrain = 2
|
||||||
|
# Clicking the 0 key activates the background terrain
|
||||||
|
if Input.is_action_pressed("quick_action_0"):
|
||||||
|
terrain = 0
|
||||||
|
|
||||||
|
if Input.is_action_pressed("left_click"):
|
||||||
|
tilemap_dual.draw_cell(cell, terrain)
|
||||||
|
elif Input.is_action_pressed("right_click"):
|
||||||
|
tilemap_dual.draw_cell(cell, 0)
|
||||||
1
addons/TileMapDual/CursorDual.gd.uid
Normal file
1
addons/TileMapDual/CursorDual.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c6m630v880okx
|
||||||
49
addons/TileMapDual/CursorDual.svg
Normal file
49
addons/TileMapDual/CursorDual.svg
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="CursorDual.svg"
|
||||||
|
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#999999"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="35.5"
|
||||||
|
inkscape:cx="9.0845069"
|
||||||
|
inkscape:cy="8.112676"
|
||||||
|
inkscape:window-width="1366"
|
||||||
|
inkscape:window-height="736"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<path
|
||||||
|
d="M 8.9882813,2.0703125 V 4.046875 h 2.9648437 v 2.9648437 h 1.976563 V 2.0703125 Z"
|
||||||
|
style="fill:#ffb273;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round"
|
||||||
|
id="path10" />
|
||||||
|
<path
|
||||||
|
d="m 8.9882813,11.953125 v 1.976563 H 13.929688 V 8.9882813 h -1.976563 v 2.9648437 z"
|
||||||
|
style="fill:#ffb273;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round"
|
||||||
|
id="path9" />
|
||||||
|
<path
|
||||||
|
d="M 7.0117187,13.929688 V 11.953125 H 4.046875 V 8.9882813 H 2.0703125 v 4.9414067 z"
|
||||||
|
style="fill:#ffb273;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round"
|
||||||
|
id="path8" />
|
||||||
|
<path
|
||||||
|
d="M 2.0703125,7.0117187 H 4.046875 V 4.046875 H 7.0117187 V 2.0703125 H 2.0703125 Z"
|
||||||
|
style="fill:#ffb273;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round"
|
||||||
|
id="path7" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
43
addons/TileMapDual/CursorDual.svg.import
Normal file
43
addons/TileMapDual/CursorDual.svg.import
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bth8306ui4dcx"
|
||||||
|
path="res://.godot/imported/CursorDual.svg-af24e4bad2d22f0cc5627c83d35ae307.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://addons/TileMapDual/CursorDual.svg"
|
||||||
|
dest_files=["res://.godot/imported/CursorDual.svg-af24e4bad2d22f0cc5627c83d35ae307.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
||||||
151
addons/TileMapDual/Display.gd
Normal file
151
addons/TileMapDual/Display.gd
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
##[br] A Node designed to hold and manage up to 2 DisplayLayer children.
|
||||||
|
##[br] See DisplayLayer.gd for details.
|
||||||
|
class_name Display
|
||||||
|
extends Node2D
|
||||||
|
|
||||||
|
|
||||||
|
## See TerrainDual.gd
|
||||||
|
var terrain: TerrainDual
|
||||||
|
## See TileSetWatcher.gd
|
||||||
|
var _tileset_watcher: TileSetWatcher
|
||||||
|
## The parent TileMapDual to base the terrains off of.
|
||||||
|
@export var world: TileMapDual
|
||||||
|
## Creates a new Display that updates when the TileSet updates.
|
||||||
|
func _init(world: TileMapDual, tileset_watcher: TileSetWatcher) -> void:
|
||||||
|
#print('initializing Display...')
|
||||||
|
self.world = world
|
||||||
|
_tileset_watcher = tileset_watcher
|
||||||
|
terrain = TerrainDual.new(tileset_watcher)
|
||||||
|
terrain.changed.connect(_terrain_changed, 1)
|
||||||
|
world_tiles_changed.connect(_world_tiles_changed, 1)
|
||||||
|
# let parent materal through to the displaylayers
|
||||||
|
use_parent_material = true
|
||||||
|
|
||||||
|
|
||||||
|
## Activates when the TerrainDual changes.
|
||||||
|
func _terrain_changed() -> void:
|
||||||
|
cached_cells.update(world)
|
||||||
|
_delete_layers()
|
||||||
|
if _tileset_watcher.tile_set != null:
|
||||||
|
_create_layers()
|
||||||
|
|
||||||
|
|
||||||
|
## Emitted when the tiles in the map have been edited.
|
||||||
|
signal world_tiles_changed(changed: Array)
|
||||||
|
func _world_tiles_changed(changed: Array) -> void:
|
||||||
|
#print('SIGNAL EMITTED: world_tiles_changed(%s)' % {'changed': changed})
|
||||||
|
for child in get_children(true):
|
||||||
|
child.update_tiles(cached_cells, changed)
|
||||||
|
|
||||||
|
|
||||||
|
## Initializes and configures new DisplayLayers according to the grid shape.
|
||||||
|
func _create_layers() -> void:
|
||||||
|
#print('GRID SHAPE: %s' % _tileset_watcher.grid_shape)
|
||||||
|
var grid: Array = GRIDS[_tileset_watcher.grid_shape]
|
||||||
|
for i in grid.size():
|
||||||
|
var layer_config: Dictionary = grid[i]
|
||||||
|
#print('layer_config: %s' % layer_config)
|
||||||
|
var layer := DisplayLayer.new(world, _tileset_watcher, layer_config, terrain.layers[i])
|
||||||
|
add_child(layer)
|
||||||
|
layer.update_tiles_all(cached_cells)
|
||||||
|
|
||||||
|
|
||||||
|
## Deletes all of the DisplayLayers.
|
||||||
|
func _delete_layers() -> void:
|
||||||
|
for child in get_children(true):
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
## The TileCache computed from the last time update() was called.
|
||||||
|
var cached_cells := TileCache.new()
|
||||||
|
## Updates the display based on the cells changed in the world TileMapDual.
|
||||||
|
func update(updated: Array) -> void:
|
||||||
|
if _tileset_watcher.tile_set == null:
|
||||||
|
return
|
||||||
|
_update_properties()
|
||||||
|
if not updated.is_empty():
|
||||||
|
cached_cells.update(world, updated)
|
||||||
|
world_tiles_changed.emit(updated)
|
||||||
|
|
||||||
|
|
||||||
|
## Activates when the properties of the parent TileMapDual have been edited.
|
||||||
|
func _update_properties() -> void:
|
||||||
|
for child in get_children(true):
|
||||||
|
child.update_properties(world)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: phase out GridShape and simply transpose everything when the offset axis is vertical
|
||||||
|
##[br] Returns what kind of grid a TileSet is.
|
||||||
|
##[br] Will default to SQUARE if Godot decides to add a new TileShape.
|
||||||
|
static func tileset_gridshape(tile_set: TileSet) -> GridShape:
|
||||||
|
var hori: bool = tile_set.tile_offset_axis == TileSet.TILE_OFFSET_AXIS_HORIZONTAL
|
||||||
|
match tile_set.tile_shape:
|
||||||
|
TileSet.TileShape.TILE_SHAPE_SQUARE:
|
||||||
|
return GridShape.SQUARE
|
||||||
|
TileSet.TileShape.TILE_SHAPE_ISOMETRIC:
|
||||||
|
return GridShape.ISO
|
||||||
|
TileSet.TileShape.TILE_SHAPE_HALF_OFFSET_SQUARE:
|
||||||
|
return GridShape.HALF_OFF_HORI if hori else GridShape.HALF_OFF_VERT
|
||||||
|
TileSet.TileShape.TILE_SHAPE_HEXAGON:
|
||||||
|
return GridShape.HEX_HORI if hori else GridShape.HEX_VERT
|
||||||
|
_:
|
||||||
|
return GridShape.SQUARE
|
||||||
|
|
||||||
|
|
||||||
|
## Every meaningfully different TileSet.tile_shape * TileSet.tile_offset_axis combination.
|
||||||
|
enum GridShape {
|
||||||
|
SQUARE,
|
||||||
|
ISO,
|
||||||
|
HALF_OFF_HORI,
|
||||||
|
HALF_OFF_VERT,
|
||||||
|
HEX_HORI,
|
||||||
|
HEX_VERT,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
##[br] How to deal with every available GridShape.
|
||||||
|
##[br] See DisplayLayer.gd for more information about these fields.
|
||||||
|
const GRIDS: Dictionary = {
|
||||||
|
GridShape.SQUARE: [
|
||||||
|
{ # []
|
||||||
|
'offset': Vector2(-0.5, -0.5),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
GridShape.ISO: [
|
||||||
|
{ # <>
|
||||||
|
'offset': Vector2(0, -0.5),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
GridShape.HALF_OFF_HORI: [
|
||||||
|
{ # v
|
||||||
|
'offset': Vector2(0.0, -0.5),
|
||||||
|
},
|
||||||
|
{ # ^
|
||||||
|
'offset': Vector2(-0.5, -0.5),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
GridShape.HALF_OFF_VERT: [
|
||||||
|
{ # >
|
||||||
|
'offset': Vector2(-0.5, 0.0),
|
||||||
|
},
|
||||||
|
{ # <
|
||||||
|
'offset': Vector2(-0.5, -0.5),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
GridShape.HEX_HORI: [
|
||||||
|
{ # v
|
||||||
|
'offset': Vector2(0.0, -3.0 / 8.0),
|
||||||
|
},
|
||||||
|
{ # ^
|
||||||
|
'offset': Vector2(-0.5, -3.0 / 8.0),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
GridShape.HEX_VERT: [
|
||||||
|
{ # >
|
||||||
|
'offset': Vector2(-3.0 / 8.0, 0.0),
|
||||||
|
},
|
||||||
|
{ # <
|
||||||
|
'offset': Vector2(-3.0 / 8.0, -0.5),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
1
addons/TileMapDual/Display.gd.uid
Normal file
1
addons/TileMapDual/Display.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cacp8xe0jaxln
|
||||||
102
addons/TileMapDual/DisplayLayer.gd
Normal file
102
addons/TileMapDual/DisplayLayer.gd
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
##[br] A single TileMapLayer whose purpose is to display tiles to maintain the Dual Grid illusion.
|
||||||
|
##[br] Its contents are automatically computed and updated based on:
|
||||||
|
##[br] - the contents of the parent TileMapDual
|
||||||
|
##[br] - the rules set in its assigned TerrainLayer
|
||||||
|
class_name DisplayLayer
|
||||||
|
extends TileMapLayer
|
||||||
|
|
||||||
|
|
||||||
|
##[br] How much to offset this DisplayLayer relative to the main TileMapDual grid.
|
||||||
|
##[br] This is independent of tile size.
|
||||||
|
var offset: Vector2
|
||||||
|
|
||||||
|
## See TileSetWatcher.gd
|
||||||
|
var _tileset_watcher: TileSetWatcher
|
||||||
|
|
||||||
|
## See TerrainDual.gd
|
||||||
|
var _terrain: TerrainLayer
|
||||||
|
|
||||||
|
func _init(
|
||||||
|
world: TileMapDual,
|
||||||
|
tileset_watcher: TileSetWatcher,
|
||||||
|
fields: Dictionary,
|
||||||
|
layer: TerrainLayer
|
||||||
|
) -> void:
|
||||||
|
#print('initializing Layer...')
|
||||||
|
update_properties(world)
|
||||||
|
offset = fields.offset
|
||||||
|
_tileset_watcher = tileset_watcher
|
||||||
|
_terrain = layer
|
||||||
|
tile_set = tileset_watcher.tile_set
|
||||||
|
tileset_watcher.tileset_resized.connect(reposition, 1)
|
||||||
|
reposition()
|
||||||
|
|
||||||
|
|
||||||
|
## Adjusts the position of this DisplayLayer based on the tile set's tile_size
|
||||||
|
func reposition() -> void:
|
||||||
|
position = offset * Vector2(_tileset_watcher.tile_size)
|
||||||
|
|
||||||
|
|
||||||
|
## Copies properties from parent TileMapDual to child display tilemap
|
||||||
|
func update_properties(parent: TileMapDual) -> void:
|
||||||
|
# Both tilemaps must be the same, so we copy all relevant properties
|
||||||
|
# Tilemap
|
||||||
|
# already covered by parent._tileset_watcher
|
||||||
|
# Rendering
|
||||||
|
self.y_sort_origin = parent.y_sort_origin
|
||||||
|
self.x_draw_order_reversed = parent.x_draw_order_reversed
|
||||||
|
self.rendering_quadrant_size = parent.rendering_quadrant_size
|
||||||
|
# Physics
|
||||||
|
self.collision_enabled = parent.collision_enabled
|
||||||
|
self.use_kinematic_bodies = parent.use_kinematic_bodies
|
||||||
|
self.collision_visibility_mode = parent.collision_visibility_mode
|
||||||
|
# Navigation
|
||||||
|
self.navigation_enabled = parent.navigation_enabled
|
||||||
|
self.navigation_visibility_mode = parent.navigation_visibility_mode
|
||||||
|
# Canvas item properties
|
||||||
|
self.show_behind_parent = parent.show_behind_parent
|
||||||
|
self.top_level = parent.top_level
|
||||||
|
self.light_mask = parent.light_mask
|
||||||
|
self.visibility_layer = parent.visibility_layer
|
||||||
|
self.y_sort_enabled = parent.y_sort_enabled
|
||||||
|
self.modulate = parent.modulate
|
||||||
|
self.self_modulate = parent.self_modulate
|
||||||
|
# NOTE: parent material takes priority over the current shaders, causing the world tiles to show up
|
||||||
|
self.use_parent_material = parent.use_parent_material
|
||||||
|
|
||||||
|
# Save any manually introduced Material change:
|
||||||
|
self.material = parent.display_material
|
||||||
|
|
||||||
|
|
||||||
|
## Updates all display tiles to reflect the current changes.
|
||||||
|
func update_tiles_all(cache: TileCache) -> void:
|
||||||
|
update_tiles(cache, cache.cells.keys())
|
||||||
|
|
||||||
|
|
||||||
|
## Update all display tiles affected by the world cells
|
||||||
|
func update_tiles(cache: TileCache, updated_world_cells: Array) -> void:
|
||||||
|
#push_warning('updating tiles')
|
||||||
|
var already_updated := Set.new()
|
||||||
|
for path: Array in _terrain.display_to_world_neighborhood:
|
||||||
|
path = path.map(Util.reverse_neighbor)
|
||||||
|
for world_cell: Vector2i in updated_world_cells:
|
||||||
|
var display_cell := follow_path(world_cell, path)
|
||||||
|
if already_updated.insert(display_cell):
|
||||||
|
update_tile(cache, display_cell)
|
||||||
|
|
||||||
|
|
||||||
|
## Updates a specific world cell.
|
||||||
|
func update_tile(cache: TileCache, cell: Vector2i) -> void:
|
||||||
|
var get_cell_at_path := func(path): return cache.get_terrain_at(follow_path(cell, path))
|
||||||
|
var terrain_neighbors := _terrain.display_to_world_neighborhood.map(get_cell_at_path)
|
||||||
|
var mapping: Dictionary = _terrain.apply_rule(terrain_neighbors, cell)
|
||||||
|
var sid: int = mapping.sid
|
||||||
|
var tile: Vector2i = mapping.tile
|
||||||
|
set_cell(cell, sid, tile)
|
||||||
|
|
||||||
|
|
||||||
|
## Finds the neighbor of a given cell by following a path of CellNeighbors
|
||||||
|
func follow_path(cell: Vector2i, path: Array) -> Vector2i:
|
||||||
|
for neighbor: TileSet.CellNeighbor in path:
|
||||||
|
cell = get_neighbor_cell(cell, neighbor)
|
||||||
|
return cell
|
||||||
1
addons/TileMapDual/DisplayLayer.gd.uid
Normal file
1
addons/TileMapDual/DisplayLayer.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bon5inags0mqy
|
||||||
76
addons/TileMapDual/Set.gd
Normal file
76
addons/TileMapDual/Set.gd
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
##[br] Real sets don't exist yet.
|
||||||
|
##[br] https://github.com/godotengine/godot/pull/94399
|
||||||
|
class_name Set
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
## The internal Dictionary that holds this Set's items as keys.
|
||||||
|
var data: Dictionary = {}
|
||||||
|
|
||||||
|
func _init(initial_data: Variant = []) -> void:
|
||||||
|
union_in_place(initial_data)
|
||||||
|
|
||||||
|
## Returns true if the item exists in this Set.
|
||||||
|
func has(item: Variant) -> bool:
|
||||||
|
return item in data
|
||||||
|
|
||||||
|
|
||||||
|
## A dummy value to put in a slot.
|
||||||
|
const DUMMY = null
|
||||||
|
## Returns true if the item was not previously in the Set.
|
||||||
|
func insert(item: Variant) -> bool:
|
||||||
|
var out := not has(item)
|
||||||
|
data[item] = DUMMY
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Returns true if the item was previously in the Set.
|
||||||
|
func remove(item: Variant) -> bool:
|
||||||
|
return data.erase(item)
|
||||||
|
|
||||||
|
|
||||||
|
## Deletes all items in this Set.
|
||||||
|
func clear() -> void:
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
|
||||||
|
## Merges an Array's items or Dict's keys into the Set.
|
||||||
|
func union_in_place(other: Variant):
|
||||||
|
for item in other:
|
||||||
|
insert(item)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns a new Set with the items of both self and other.
|
||||||
|
func union(other: Set) -> Set:
|
||||||
|
var out = self.duplicate()
|
||||||
|
out.union_in_place(other.data)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Removes an Array's items or Dict's keys from the Set.
|
||||||
|
func diff_in_place(other: Variant):
|
||||||
|
for item in other:
|
||||||
|
remove(item)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns a new Set with all items in self that are not present in other.
|
||||||
|
func diff(other: Set) -> Set:
|
||||||
|
var out = self.duplicate()
|
||||||
|
out.diff_in_place(other.data)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Inserts elements that are in other but not in self, and removes elements found in both.
|
||||||
|
func xor_in_place(other: Variant):
|
||||||
|
for item in other:
|
||||||
|
if has(item):
|
||||||
|
remove(item)
|
||||||
|
else:
|
||||||
|
insert(item)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns a new Set where each item is either in self or other, but not both.
|
||||||
|
func xor(other: Set) -> Set:
|
||||||
|
var out = self.duplicate()
|
||||||
|
out.xor_in_place(other.data)
|
||||||
|
return out
|
||||||
1
addons/TileMapDual/Set.gd.uid
Normal file
1
addons/TileMapDual/Set.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ds8j47h4vi6gx
|
||||||
194
addons/TileMapDual/TerrainDual.gd
Normal file
194
addons/TileMapDual/TerrainDual.gd
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
##[br] Reads a TileSet and dictates which tiles in the display map
|
||||||
|
##[br] match up with its neighbors in the world map
|
||||||
|
class_name TerrainDual
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
# Functions are ordered top to bottom in the transformation pipeline
|
||||||
|
|
||||||
|
## Maps a TileSet to a Neighborhood.
|
||||||
|
static func tileset_neighborhood(tile_set: TileSet) -> Neighborhood:
|
||||||
|
return GRID_NEIGHBORHOODS[Display.tileset_gridshape(tile_set)]
|
||||||
|
|
||||||
|
|
||||||
|
## Maps a GridShape to a Neighborhood.
|
||||||
|
const GRID_NEIGHBORHOODS = {
|
||||||
|
Display.GridShape.SQUARE: Neighborhood.SQUARE,
|
||||||
|
Display.GridShape.ISO: Neighborhood.ISOMETRIC,
|
||||||
|
Display.GridShape.HALF_OFF_HORI: Neighborhood.TRIANGLE_HORIZONTAL,
|
||||||
|
Display.GridShape.HALF_OFF_VERT: Neighborhood.TRIANGLE_VERTICAL,
|
||||||
|
Display.GridShape.HEX_HORI: Neighborhood.TRIANGLE_HORIZONTAL,
|
||||||
|
Display.GridShape.HEX_VERT: Neighborhood.TRIANGLE_VERTICAL,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
## A specific neighborhood that the Display tiles will look at.
|
||||||
|
enum Neighborhood {
|
||||||
|
SQUARE,
|
||||||
|
ISOMETRIC,
|
||||||
|
TRIANGLE_HORIZONTAL,
|
||||||
|
TRIANGLE_VERTICAL,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
## Maps a Neighborhood to a set of atlas terrain neighbors.
|
||||||
|
const NEIGHBORHOOD_LAYERS := {
|
||||||
|
Neighborhood.SQUARE: [
|
||||||
|
{ # []
|
||||||
|
'terrain_neighborhood': [
|
||||||
|
TileSet.CELL_NEIGHBOR_TOP_LEFT_CORNER,
|
||||||
|
TileSet.CELL_NEIGHBOR_TOP_RIGHT_CORNER,
|
||||||
|
TileSet.CELL_NEIGHBOR_BOTTOM_LEFT_CORNER,
|
||||||
|
TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER,
|
||||||
|
],
|
||||||
|
'display_to_world_neighborhood': [
|
||||||
|
[TileSet.CELL_NEIGHBOR_TOP_LEFT_CORNER],
|
||||||
|
[TileSet.CELL_NEIGHBOR_TOP_SIDE],
|
||||||
|
[TileSet.CELL_NEIGHBOR_LEFT_SIDE],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Neighborhood.ISOMETRIC: [
|
||||||
|
{ # <>
|
||||||
|
'terrain_neighborhood': [
|
||||||
|
TileSet.CELL_NEIGHBOR_TOP_CORNER,
|
||||||
|
TileSet.CELL_NEIGHBOR_RIGHT_CORNER,
|
||||||
|
TileSet.CELL_NEIGHBOR_LEFT_CORNER,
|
||||||
|
TileSet.CELL_NEIGHBOR_BOTTOM_CORNER,
|
||||||
|
],
|
||||||
|
'display_to_world_neighborhood': [
|
||||||
|
[TileSet.CELL_NEIGHBOR_TOP_CORNER],
|
||||||
|
[TileSet.CELL_NEIGHBOR_TOP_RIGHT_SIDE],
|
||||||
|
[TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Neighborhood.TRIANGLE_HORIZONTAL: [
|
||||||
|
{ # v
|
||||||
|
'terrain_neighborhood': [
|
||||||
|
TileSet.CELL_NEIGHBOR_BOTTOM_CORNER,
|
||||||
|
TileSet.CELL_NEIGHBOR_TOP_LEFT_CORNER,
|
||||||
|
TileSet.CELL_NEIGHBOR_TOP_RIGHT_CORNER,
|
||||||
|
],
|
||||||
|
'display_to_world_neighborhood': [
|
||||||
|
[],
|
||||||
|
[TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE],
|
||||||
|
[TileSet.CELL_NEIGHBOR_TOP_RIGHT_SIDE],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ # ^
|
||||||
|
'terrain_neighborhood': [
|
||||||
|
TileSet.CELL_NEIGHBOR_TOP_CORNER,
|
||||||
|
TileSet.CELL_NEIGHBOR_BOTTOM_LEFT_CORNER,
|
||||||
|
TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER,
|
||||||
|
],
|
||||||
|
'display_to_world_neighborhood': [
|
||||||
|
[TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE],
|
||||||
|
[TileSet.CELL_NEIGHBOR_LEFT_SIDE],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
# TODO: this is just TRIANGLE_HORIZONTAL but transposed. this can be refactored.
|
||||||
|
Neighborhood.TRIANGLE_VERTICAL: [
|
||||||
|
{ # >
|
||||||
|
'terrain_neighborhood': [
|
||||||
|
TileSet.CELL_NEIGHBOR_RIGHT_CORNER,
|
||||||
|
TileSet.CELL_NEIGHBOR_TOP_LEFT_CORNER,
|
||||||
|
TileSet.CELL_NEIGHBOR_BOTTOM_LEFT_CORNER,
|
||||||
|
],
|
||||||
|
'display_to_world_neighborhood': [
|
||||||
|
[],
|
||||||
|
[TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE],
|
||||||
|
[TileSet.CELL_NEIGHBOR_BOTTOM_LEFT_SIDE],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ # <
|
||||||
|
'terrain_neighborhood': [
|
||||||
|
TileSet.CELL_NEIGHBOR_LEFT_CORNER,
|
||||||
|
TileSet.CELL_NEIGHBOR_TOP_RIGHT_CORNER,
|
||||||
|
TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER,
|
||||||
|
],
|
||||||
|
'display_to_world_neighborhood': [
|
||||||
|
[TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE],
|
||||||
|
[TileSet.CELL_NEIGHBOR_TOP_SIDE],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
## The Neighborhood type of this TerrainDual.
|
||||||
|
var neighborhood: Neighborhood
|
||||||
|
|
||||||
|
## Maps a terrain type to its sprite as registered in the TerrainDual.
|
||||||
|
var terrains: Dictionary
|
||||||
|
|
||||||
|
## The TerrainLayers for this TerrainDual.
|
||||||
|
var layers: Array
|
||||||
|
var _tileset_watcher: TileSetWatcher
|
||||||
|
func _init(tileset_watcher: TileSetWatcher) -> void:
|
||||||
|
_tileset_watcher = tileset_watcher
|
||||||
|
_tileset_watcher.terrains_changed.connect(_changed, 1)
|
||||||
|
_changed()
|
||||||
|
|
||||||
|
|
||||||
|
##[br] Emitted when any of the terrains change.
|
||||||
|
##[br] NOTE: Prefer connecting to TerrainDual.changed instead of TileSetWatcher.terrains_changed.
|
||||||
|
func _changed() -> void:
|
||||||
|
#print('SIGNAL EMITTED: changed(%s)' % {})
|
||||||
|
read_tileset(_tileset_watcher.tile_set)
|
||||||
|
emit_changed()
|
||||||
|
|
||||||
|
|
||||||
|
## Create rules for every atlas in a TileSet.
|
||||||
|
func read_tileset(tile_set: TileSet) -> void:
|
||||||
|
terrains = {}
|
||||||
|
layers = []
|
||||||
|
neighborhood = Neighborhood.SQUARE # default
|
||||||
|
if tile_set == null:
|
||||||
|
return
|
||||||
|
neighborhood = tileset_neighborhood(tile_set)
|
||||||
|
layers = NEIGHBORHOOD_LAYERS[neighborhood].map(TerrainLayer.new)
|
||||||
|
for i in tile_set.get_source_count():
|
||||||
|
var sid := tile_set.get_source_id(i)
|
||||||
|
var src := tile_set.get_source(sid)
|
||||||
|
if src is not TileSetAtlasSource:
|
||||||
|
continue
|
||||||
|
read_atlas(src, sid)
|
||||||
|
|
||||||
|
|
||||||
|
## Create rules for every tile in an atlas.
|
||||||
|
func read_atlas(atlas: TileSetAtlasSource, sid: int) -> void:
|
||||||
|
var size = atlas.get_atlas_grid_size()
|
||||||
|
for y in size.y:
|
||||||
|
for x in size.x:
|
||||||
|
var tile := Vector2i(x, y)
|
||||||
|
# Take only existing tiles
|
||||||
|
if not atlas.has_tile(tile):
|
||||||
|
continue
|
||||||
|
read_tile(atlas, sid, tile)
|
||||||
|
|
||||||
|
|
||||||
|
## Add a new rule for a specific tile in an atlas.
|
||||||
|
func read_tile(atlas: TileSetAtlasSource, sid: int, tile: Vector2i) -> void:
|
||||||
|
var data := atlas.get_tile_data(tile, 0)
|
||||||
|
var mapping := {'sid': sid, 'tile': tile}
|
||||||
|
var terrain_set := data.terrain_set
|
||||||
|
if terrain_set != 0:
|
||||||
|
push_warning(
|
||||||
|
"The tile at %s has a terrain set of %d. Only terrain set 0 is supported." % [mapping, terrain_set]
|
||||||
|
)
|
||||||
|
return
|
||||||
|
var terrain := data.terrain
|
||||||
|
if terrain != -1:
|
||||||
|
if not terrain in terrains:
|
||||||
|
terrains[terrain] = mapping
|
||||||
|
|
||||||
|
var filters = NEIGHBORHOOD_LAYERS[neighborhood]
|
||||||
|
for i in layers.size():
|
||||||
|
var layer: TerrainLayer = layers[i]
|
||||||
|
layer._register_tile(data, mapping)
|
||||||
1
addons/TileMapDual/TerrainDual.gd.uid
Normal file
1
addons/TileMapDual/TerrainDual.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://qa8xnqphuk68
|
||||||
128
addons/TileMapDual/TerrainLayer.gd
Normal file
128
addons/TileMapDual/TerrainLayer.gd
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
## 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)
|
||||||
1
addons/TileMapDual/TerrainLayer.gd.uid
Normal file
1
addons/TileMapDual/TerrainLayer.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ks3fxwahkm6j
|
||||||
322
addons/TileMapDual/TerrainPreset.gd
Normal file
322
addons/TileMapDual/TerrainPreset.gd
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
## Functions for automatically generating terrains for an atlas.
|
||||||
|
class_name TerrainPreset
|
||||||
|
|
||||||
|
|
||||||
|
## Maps a Neighborhood to a Topology.
|
||||||
|
const NEIGHBORHOOD_TOPOLOGIES := {
|
||||||
|
TerrainDual.Neighborhood.SQUARE: Topology.SQUARE,
|
||||||
|
TerrainDual.Neighborhood.ISOMETRIC: Topology.SQUARE,
|
||||||
|
TerrainDual.Neighborhood.TRIANGLE_HORIZONTAL: Topology.TRIANGLE,
|
||||||
|
TerrainDual.Neighborhood.TRIANGLE_VERTICAL: Topology.TRIANGLE,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
## Determines the available Terrain presets for a certain Atlas.
|
||||||
|
enum Topology {
|
||||||
|
SQUARE,
|
||||||
|
TRIANGLE,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
## Maps a Neighborhood to a preset of the specified name.
|
||||||
|
static func neighborhood_preset(
|
||||||
|
neighborhood: TerrainDual.Neighborhood,
|
||||||
|
preset_name: String = 'Standard'
|
||||||
|
) -> Dictionary:
|
||||||
|
var topology: Topology = NEIGHBORHOOD_TOPOLOGIES[neighborhood]
|
||||||
|
# TODO: test when the preset doesn't exist
|
||||||
|
var available_presets = PRESETS[topology]
|
||||||
|
if preset_name not in available_presets:
|
||||||
|
return {'size': Vector2i.ONE, 'layers': []}
|
||||||
|
var out: Dictionary = available_presets[preset_name].duplicate(true)
|
||||||
|
# All Horizontal neighborhoods can be transposed to Vertical
|
||||||
|
if neighborhood == TerrainDual.Neighborhood.TRIANGLE_VERTICAL:
|
||||||
|
out.size = Util.transpose_vec(out.size)
|
||||||
|
out.fg = Util.transpose_vec(out.fg)
|
||||||
|
out.bg = Util.transpose_vec(out.bg)
|
||||||
|
for seq in out.layers:
|
||||||
|
for i in seq.size():
|
||||||
|
seq[i] = Util.transpose_vec(seq[i])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Contains all of the builtin Terrain presets for each topology
|
||||||
|
const PRESETS := {
|
||||||
|
Topology.SQUARE: {
|
||||||
|
'Standard': {
|
||||||
|
'size': Vector2i(4, 4),
|
||||||
|
'bg': Vector2i(0, 3),
|
||||||
|
'fg': Vector2i(2, 1),
|
||||||
|
'layers': [
|
||||||
|
[ # []
|
||||||
|
Vector2i(0, 3),
|
||||||
|
Vector2i(3, 3),
|
||||||
|
Vector2i(0, 2),
|
||||||
|
Vector2i(1, 2),
|
||||||
|
Vector2i(0, 0),
|
||||||
|
Vector2i(3, 2),
|
||||||
|
Vector2i(2, 3),
|
||||||
|
Vector2i(3, 1),
|
||||||
|
Vector2i(1, 3),
|
||||||
|
Vector2i(0, 1),
|
||||||
|
Vector2i(1, 0),
|
||||||
|
Vector2i(2, 2),
|
||||||
|
Vector2i(3, 0),
|
||||||
|
Vector2i(2, 0),
|
||||||
|
Vector2i(1, 1),
|
||||||
|
Vector2i(2, 1),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Topology.TRIANGLE: {
|
||||||
|
'Standard': {
|
||||||
|
'size': Vector2i(4, 4),
|
||||||
|
'bg': Vector2i(0, 0),
|
||||||
|
'fg': Vector2i(0, 2),
|
||||||
|
'layers': [
|
||||||
|
[ # v
|
||||||
|
Vector2i(0, 1),
|
||||||
|
Vector2i(2, 3),
|
||||||
|
Vector2i(3, 1),
|
||||||
|
Vector2i(1, 3),
|
||||||
|
Vector2i(1, 1),
|
||||||
|
Vector2i(3, 3),
|
||||||
|
Vector2i(2, 1),
|
||||||
|
Vector2i(0, 3),
|
||||||
|
],
|
||||||
|
[ # ^
|
||||||
|
Vector2i(0, 0),
|
||||||
|
Vector2i(2, 2),
|
||||||
|
Vector2i(3, 0),
|
||||||
|
Vector2i(1, 2),
|
||||||
|
Vector2i(1, 0),
|
||||||
|
Vector2i(3, 2),
|
||||||
|
Vector2i(2, 0),
|
||||||
|
Vector2i(0, 2),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
# Old template.
|
||||||
|
# a bit inconvenient to use for Brick (Half-Off Square) tilesets.
|
||||||
|
'Winged': {
|
||||||
|
'size': Vector2i(4, 4),
|
||||||
|
'bg': Vector2i(0, 0),
|
||||||
|
'fg': Vector2i(0, 2),
|
||||||
|
'layers': [
|
||||||
|
[ # v
|
||||||
|
Vector2i(0, 1),
|
||||||
|
Vector2i(2, 1),
|
||||||
|
Vector2i(3, 1),
|
||||||
|
Vector2i(1, 3),
|
||||||
|
Vector2i(1, 1),
|
||||||
|
Vector2i(3, 3),
|
||||||
|
Vector2i(2, 3),
|
||||||
|
Vector2i(0, 3),
|
||||||
|
],
|
||||||
|
[ # ^
|
||||||
|
Vector2i(0, 0),
|
||||||
|
Vector2i(2, 0),
|
||||||
|
Vector2i(3, 0),
|
||||||
|
Vector2i(1, 2),
|
||||||
|
Vector2i(1, 0),
|
||||||
|
Vector2i(3, 2),
|
||||||
|
Vector2i(2, 2),
|
||||||
|
Vector2i(0, 2),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
# Old template.
|
||||||
|
# The gaps between triangles made them harder to align.
|
||||||
|
'Alternating': {
|
||||||
|
'size': Vector2i(4, 4),
|
||||||
|
'bg': Vector2i(0, 0),
|
||||||
|
'fg': Vector2i(0, 2),
|
||||||
|
'layers': [
|
||||||
|
[ # v
|
||||||
|
Vector2i(0, 0),
|
||||||
|
Vector2i(2, 0),
|
||||||
|
Vector2i(3, 1),
|
||||||
|
Vector2i(1, 3),
|
||||||
|
Vector2i(1, 1),
|
||||||
|
Vector2i(3, 3),
|
||||||
|
Vector2i(2, 2),
|
||||||
|
Vector2i(0, 2),
|
||||||
|
],
|
||||||
|
[ # ^
|
||||||
|
Vector2i(0, 1),
|
||||||
|
Vector2i(2, 1),
|
||||||
|
Vector2i(3, 0),
|
||||||
|
Vector2i(1, 2),
|
||||||
|
Vector2i(1, 0),
|
||||||
|
Vector2i(3, 2),
|
||||||
|
Vector2i(2, 3),
|
||||||
|
Vector2i(0, 3),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
##[br] Would you like to automatically create tiles in the atlas?
|
||||||
|
##[br]
|
||||||
|
##[br] NOTE: Assumes urm.create_action() was called. Does not actually do anything until urm.commit_action() is called.
|
||||||
|
##[br] NOTE: Assumes atlas only has auto-generated tiles. Does not save peering bit information or anything else for undo/redo.
|
||||||
|
static func write_default_preset(urm: EditorUndoRedoManager, tile_set: TileSet, atlas: TileSetAtlasSource) -> void:
|
||||||
|
#print('writing default')
|
||||||
|
var neighborhood := TerrainDual.tileset_neighborhood(tile_set)
|
||||||
|
var terrain := new_terrain(
|
||||||
|
urm,
|
||||||
|
tile_set,
|
||||||
|
atlas.texture.resource_path.get_file()
|
||||||
|
)
|
||||||
|
write_preset(
|
||||||
|
urm,
|
||||||
|
atlas,
|
||||||
|
neighborhood,
|
||||||
|
terrain,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
##[br] Creates terrain set 0 (the primary terrain set) and terrain 0 (the 'any' terrain)
|
||||||
|
##[br]
|
||||||
|
##[br] NOTE: Assumes urm.create_action() was called. Does not actually do anything until urm.commit_action() is called.
|
||||||
|
static func init_terrains(urm: EditorUndoRedoManager, tile_set: TileSet) -> void:
|
||||||
|
urm.add_do_method(TerrainPreset, "_do_init_terrains", tile_set)
|
||||||
|
urm.add_undo_method(TerrainPreset, "_undo_init_terrains", tile_set)
|
||||||
|
|
||||||
|
static func _do_init_terrains(tile_set: TileSet) -> void:
|
||||||
|
tile_set.add_terrain_set()
|
||||||
|
tile_set.set_terrain_set_mode(0, TileSet.TERRAIN_MODE_MATCH_CORNERS)
|
||||||
|
tile_set.add_terrain(0)
|
||||||
|
tile_set.set_terrain_name(0, 0, "<any>")
|
||||||
|
tile_set.set_terrain_color(0, 0, Color.VIOLET)
|
||||||
|
|
||||||
|
static func _undo_init_terrains(tile_set: TileSet) -> void:
|
||||||
|
tile_set.remove_terrain_set(0)
|
||||||
|
|
||||||
|
|
||||||
|
##[br] Adds a new terrain type to terrain set 0 for the sprites to use.
|
||||||
|
##[br]
|
||||||
|
##[br] NOTE: Assumes urm.create_action() was called. Does not actually do anything until urm.commit_action() is called.
|
||||||
|
static func new_terrain(urm: EditorUndoRedoManager, tile_set: TileSet, terrain_name: String) -> int:
|
||||||
|
var terrain: int
|
||||||
|
if tile_set.get_terrain_sets_count() == 0:
|
||||||
|
init_terrains(urm, tile_set)
|
||||||
|
terrain = 1
|
||||||
|
else:
|
||||||
|
terrain = tile_set.get_terrains_count(0)
|
||||||
|
urm.add_do_method(TerrainPreset, "_do_new_terrain", tile_set, terrain_name)
|
||||||
|
urm.add_undo_method(TerrainPreset, "_undo_new_terrain", tile_set)
|
||||||
|
return terrain
|
||||||
|
|
||||||
|
static func _do_new_terrain(tile_set: TileSet, terrain_name: String) -> void:
|
||||||
|
tile_set.add_terrain(0)
|
||||||
|
var terrain := tile_set.get_terrains_count(0) - 1
|
||||||
|
tile_set.set_terrain_name(0, terrain, "FG -%s" % terrain_name)
|
||||||
|
|
||||||
|
static func _undo_new_terrain(tile_set: TileSet) -> void:
|
||||||
|
var terrain := tile_set.get_terrains_count(0) - 1
|
||||||
|
tile_set.remove_terrain(0, terrain)
|
||||||
|
|
||||||
|
|
||||||
|
##[br] Takes a preset and writes it onto the given atlas, replacing the previous configuration.
|
||||||
|
##[br] ARGUMENTS:
|
||||||
|
##[br] - atlas: the atlas source to apply the preset to.
|
||||||
|
##[br] - filters: the neighborhood filter
|
||||||
|
##[br]
|
||||||
|
##[br] NOTE: Assumes urm.create_action() was called. Does not actually do anything until urm.commit_action() is called.
|
||||||
|
##[br] NOTE: Assumes atlas only has auto-generated tiles. Does not save peering bit information or anything else for undo/redo.
|
||||||
|
static func write_preset(
|
||||||
|
urm: EditorUndoRedoManager,
|
||||||
|
atlas: TileSetAtlasSource,
|
||||||
|
neighborhood: TerrainDual.Neighborhood,
|
||||||
|
terrain_foreground: int,
|
||||||
|
terrain_background: int = 0,
|
||||||
|
preset: Dictionary = neighborhood_preset(neighborhood),
|
||||||
|
) -> void:
|
||||||
|
clear_and_divide_atlas(urm, atlas, preset.size)
|
||||||
|
urm.add_do_method(TerrainPreset, '_do_write_preset', atlas, neighborhood, terrain_foreground, terrain_background, preset)
|
||||||
|
|
||||||
|
static func _do_write_preset(
|
||||||
|
atlas: TileSetAtlasSource,
|
||||||
|
neighborhood: TerrainDual.Neighborhood,
|
||||||
|
terrain_foreground: int,
|
||||||
|
terrain_background: int,
|
||||||
|
preset: Dictionary,
|
||||||
|
) -> void:
|
||||||
|
var layers: Array = TerrainDual.NEIGHBORHOOD_LAYERS[neighborhood]
|
||||||
|
# Set peering bits
|
||||||
|
var sequences: Array = preset.layers
|
||||||
|
for j in layers.size():
|
||||||
|
var terrain_neighborhood = layers[j].terrain_neighborhood
|
||||||
|
var sequence: Array = sequences[j]
|
||||||
|
for i in sequence.size():
|
||||||
|
var tile: Vector2i = sequence[i]
|
||||||
|
atlas.create_tile(tile)
|
||||||
|
var data := atlas.get_tile_data(tile, 0)
|
||||||
|
data.terrain_set = 0
|
||||||
|
for neighbor in terrain_neighborhood:
|
||||||
|
data.set_terrain_peering_bit(
|
||||||
|
neighbor,
|
||||||
|
[terrain_background, terrain_foreground][i & 1]
|
||||||
|
)
|
||||||
|
i >>= 1
|
||||||
|
# Set terrains
|
||||||
|
atlas.get_tile_data(preset.bg, 0).terrain = terrain_background
|
||||||
|
atlas.get_tile_data(preset.fg, 0).terrain = terrain_foreground
|
||||||
|
|
||||||
|
##[br] Unregisters all the tiles in an atlas and changes the size of the individual sprites.
|
||||||
|
##[br]
|
||||||
|
##[br] NOTE: Assumes urm.create_action() was called. Does not actually do anything until urm.commit_action() is called.
|
||||||
|
##[br] NOTE: Assumes atlas only has auto-generated tiles. Does not save peering bit information or anything else for undo/redo.
|
||||||
|
static func clear_and_resize_atlas(urm: EditorUndoRedoManager, atlas: TileSetAtlasSource, size: Vector2) -> void:
|
||||||
|
var atlas_data := _save_atlas_data(atlas)
|
||||||
|
urm.add_do_method(TerrainPreset, '_do_clear_and_resize_atlas', atlas, size)
|
||||||
|
urm.add_undo_method(TerrainPreset, '_undo_clear_and_resize_atlas', atlas, atlas_data)
|
||||||
|
|
||||||
|
static func _do_clear_and_resize_atlas(atlas: TileSetAtlasSource, size: Vector2) -> void:
|
||||||
|
# Clear all tiles
|
||||||
|
atlas.texture_region_size = atlas.texture.get_size() + Vector2.ONE
|
||||||
|
atlas.clear_tiles_outside_texture()
|
||||||
|
# Resize the tiles
|
||||||
|
atlas.texture_region_size = size
|
||||||
|
|
||||||
|
static func _undo_clear_and_resize_atlas(atlas: TileSetAtlasSource, atlas_data: Dictionary) -> void:
|
||||||
|
_load_atlas_data(atlas, atlas_data)
|
||||||
|
|
||||||
|
## NOTE: Assumes atlas only has auto-generated tiles. Does not save peering bit information or anything else.
|
||||||
|
static func _save_atlas_data(atlas: TileSetAtlasSource) -> Dictionary:
|
||||||
|
var size_img := atlas.texture.get_size()
|
||||||
|
var size_sprite := atlas.texture_region_size
|
||||||
|
var size_dims := Vector2i(size_img) / size_sprite
|
||||||
|
var tiles := []
|
||||||
|
for y in size_dims.y:
|
||||||
|
var row := []
|
||||||
|
for x in size_dims.x:
|
||||||
|
var tile := Vector2i(x, y)
|
||||||
|
var exists := atlas.has_tile(tile)
|
||||||
|
row.push_back(exists)
|
||||||
|
tiles.push_back(row)
|
||||||
|
return {
|
||||||
|
'size_sprite': size_sprite,
|
||||||
|
'size_dims': size_dims,
|
||||||
|
'tiles': tiles,
|
||||||
|
}
|
||||||
|
|
||||||
|
static func _load_atlas_data(atlas: TileSetAtlasSource, atlas_data: Dictionary) -> void:
|
||||||
|
_do_clear_and_resize_atlas(atlas, atlas_data.size_sprite)
|
||||||
|
for y in atlas_data.size_dims.y:
|
||||||
|
for x in atlas_data.size_dims.x:
|
||||||
|
if atlas_data.tiles[y][x]:
|
||||||
|
var tile := Vector2i(x, y)
|
||||||
|
atlas.create_tile(tile)
|
||||||
|
|
||||||
|
|
||||||
|
##[br] Unregisters all the tiles in an atlas and changes the size of the
|
||||||
|
## individual sprites to accomodate a divisions.x by divisions.y grid of sprites.
|
||||||
|
##[br]
|
||||||
|
##[br] NOTE: Assumes urm.create_action() was called. Does not actually do anything until urm.commit_action() is called.
|
||||||
|
##[br] NOTE: Assumes atlas only has auto-generated tiles. Does not save peering bit information or anything else for undo/redo.
|
||||||
|
static func clear_and_divide_atlas(urm: EditorUndoRedoManager, atlas: TileSetAtlasSource, divisions: Vector2i) -> void:
|
||||||
|
clear_and_resize_atlas(urm, atlas, atlas.texture.get_size() / Vector2(divisions))
|
||||||
1
addons/TileMapDual/TerrainPreset.gd.uid
Normal file
1
addons/TileMapDual/TerrainPreset.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cdsxbtu57wews
|
||||||
74
addons/TileMapDual/TileCache.gd
Normal file
74
addons/TileMapDual/TileCache.gd
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
## Caches the sprite location and terrains of each tile in the TileMapDual world grid.
|
||||||
|
class_name TileCache
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
##[br] Maps a cell coordinate to the stored tile data.
|
||||||
|
##[codeblock]
|
||||||
|
## Dictionary{
|
||||||
|
## key: Vector2i = # The coordinates of this tile in the world grid.
|
||||||
|
## value: Dictionary{
|
||||||
|
## 'sid': int = # The Source ID of this tile.
|
||||||
|
## 'tile': Vector2i = # The coordinates of this tile in its Atlas.
|
||||||
|
## 'terrain': int = # The terrain assigned to this tile.
|
||||||
|
## } = # The data stored at this tile.
|
||||||
|
## }
|
||||||
|
##[/codeblock]
|
||||||
|
var cells := {}
|
||||||
|
func _init() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
##[br] Updates specific cells of the TileCache based on the current layer data at those points.
|
||||||
|
##[br] Makes corrections in case the user accidentally places invalid tiles.
|
||||||
|
func update(world: TileMapLayer, edited: Array = cells.keys()) -> void:
|
||||||
|
var tile_set := world.tile_set
|
||||||
|
if tile_set == null:
|
||||||
|
push_error('Attempted to update TileCache while tile set was null')
|
||||||
|
return
|
||||||
|
for cell in edited:
|
||||||
|
# Invalid cells will be treated as empty and ignored
|
||||||
|
var sid := world.get_cell_source_id(cell)
|
||||||
|
if sid == -1:
|
||||||
|
cells.erase(cell)
|
||||||
|
continue
|
||||||
|
if not tile_set.has_source(sid):
|
||||||
|
continue
|
||||||
|
var src = tile_set.get_source(sid)
|
||||||
|
var tile := world.get_cell_atlas_coords(cell)
|
||||||
|
if not src.has_tile(tile):
|
||||||
|
continue
|
||||||
|
var data := world.get_cell_tile_data(cell)
|
||||||
|
if data == null:
|
||||||
|
continue
|
||||||
|
# Accidental cells should be reset to their previous value
|
||||||
|
# They will be treated as unchanged
|
||||||
|
if data.terrain == -1 or data.terrain_set != 0:
|
||||||
|
if cell not in cells:
|
||||||
|
world.erase_cell(cell)
|
||||||
|
continue
|
||||||
|
var cached: Dictionary = cells[cell]
|
||||||
|
sid = cached.sid
|
||||||
|
tile = cached.tile
|
||||||
|
world.set_cell(cell, sid, tile)
|
||||||
|
data = world.get_cell_tile_data(cell)
|
||||||
|
cells[cell] = {'sid': sid, 'tile': tile, 'terrain': data.terrain}
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the symmetric difference (xor) of two tile caches.
|
||||||
|
func xor(other: TileCache) -> Array[Vector2i]:
|
||||||
|
var out: Array[Vector2i] = []
|
||||||
|
for key in cells:
|
||||||
|
if key not in other.cells or cells[key].terrain != other.cells[key].terrain:
|
||||||
|
out.push_back(key)
|
||||||
|
for key in other.cells:
|
||||||
|
if key not in cells:
|
||||||
|
out.push_back(key)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
##[br] Returns the terrain value of the tile at the given cell coordinates.
|
||||||
|
##[br] Empty cells have a terrain of -1.
|
||||||
|
func get_terrain_at(cell: Vector2i) -> int:
|
||||||
|
if cell not in cells:
|
||||||
|
return -1
|
||||||
|
return cells[cell].terrain
|
||||||
1
addons/TileMapDual/TileCache.gd.uid
Normal file
1
addons/TileMapDual/TileCache.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dpuc70ypq7wf8
|
||||||
160
addons/TileMapDual/TileMapDual.gd
Normal file
160
addons/TileMapDual/TileMapDual.gd
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
@tool
|
||||||
|
@icon('TileMapDual.svg')
|
||||||
|
class_name TileMapDual
|
||||||
|
extends TileMapLayer
|
||||||
|
|
||||||
|
|
||||||
|
## An invisible material used to hide the world grid so only the display layers show up.
|
||||||
|
## Currently implemented as a shader that sets all pixels to 0 alpha.
|
||||||
|
var _ghost_material: Material = preload("res://addons/TileMapDual/ghost_material.tres")
|
||||||
|
|
||||||
|
|
||||||
|
# === External functions that don't exist once exported ===
|
||||||
|
# HACK: this uses some sort of "Dynamic Linking" technique because these features don't exist right now
|
||||||
|
# - conditional compilation
|
||||||
|
# - static signals
|
||||||
|
static func _editor_only(name: String):
|
||||||
|
push_error('Attempt to call Editor-Only function "' + name + '"')
|
||||||
|
static var autotile: Callable = _editor_only.bind('autotile').unbind(3)
|
||||||
|
static var popup: Callable = _editor_only.bind('popup').unbind(2)
|
||||||
|
|
||||||
|
|
||||||
|
## Material for the display tilemap.
|
||||||
|
@export_custom(PROPERTY_HINT_RESOURCE_TYPE, "ShaderMaterial, CanvasItemMaterial")
|
||||||
|
var display_material: Material:
|
||||||
|
get:
|
||||||
|
return display_material
|
||||||
|
set(new_material): # Custom setter so that it gets copied
|
||||||
|
display_material = new_material
|
||||||
|
changed.emit()
|
||||||
|
|
||||||
|
var _tileset_watcher: TileSetWatcher
|
||||||
|
var _display: Display
|
||||||
|
func _ready() -> void:
|
||||||
|
_tileset_watcher = TileSetWatcher.new(tile_set)
|
||||||
|
_display = Display.new(self, _tileset_watcher)
|
||||||
|
add_child(_display)
|
||||||
|
_make_self_invisible(true)
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
_tileset_watcher.atlas_autotiled.connect(_atlas_autotiled)
|
||||||
|
set_process(true)
|
||||||
|
else: # Run in-game using signals for better performance
|
||||||
|
changed.connect(_changed, 1)
|
||||||
|
set_process(false)
|
||||||
|
# Update full tileset on first instance
|
||||||
|
await get_tree().process_frame
|
||||||
|
_changed()
|
||||||
|
|
||||||
|
|
||||||
|
## Automatically generate terrains when the atlas is initialized.
|
||||||
|
func _atlas_autotiled(source_id: int, atlas: TileSetAtlasSource):
|
||||||
|
autotile.call(source_id, atlas, tile_set)
|
||||||
|
|
||||||
|
|
||||||
|
## Keeps track of use_parent_material to see when it turns on or off.
|
||||||
|
var _cached_use_parent_material = null
|
||||||
|
##[br] Makes the main world grid invisible.
|
||||||
|
##[br] The main tiles don't need to be seen. Only the DisplayLayers should be visible.
|
||||||
|
##[br] Called every frame, and functions a lot like TileSetWatcher.
|
||||||
|
func _make_self_invisible(startup: bool = false) -> void:
|
||||||
|
# If user has set a material in the original slot, inform the user
|
||||||
|
if material != _ghost_material:
|
||||||
|
if not startup and Engine.is_editor_hint():
|
||||||
|
popup.call(
|
||||||
|
"Warning! Direct material edit detected.",
|
||||||
|
"Don't manually edit the real material in the editor! Instead edit the custom 'Display Material' property.\n" +
|
||||||
|
"(Resetting the material to an invisible shader material... this is to keep the 'World Layer' invisible)\n" +
|
||||||
|
"* This warning is only given when the material is set in the Godot editor.\n" +
|
||||||
|
"* In-game scripts may set the material directly. It will be copied over to display_material automatically."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# copy over the material if it was edited by script
|
||||||
|
display_material = material
|
||||||
|
material = _ghost_material # Force TileMapDual's material to become invisible
|
||||||
|
|
||||||
|
# check if use_parent_material is set
|
||||||
|
if (
|
||||||
|
Engine.is_editor_hint()
|
||||||
|
and use_parent_material != _cached_use_parent_material
|
||||||
|
and _cached_use_parent_material == false # cache may be null
|
||||||
|
):
|
||||||
|
popup.call(
|
||||||
|
"Warning: Using Parent Material.",
|
||||||
|
"The parent material will override any other materials used by the TileMapDual,\n" +
|
||||||
|
"including the 'ghost shader' that the world tiles use to hide themselves.\n" +
|
||||||
|
"This will cause the world tiles to show themselves in-game.\n" +
|
||||||
|
"\n" +
|
||||||
|
"* Recommendation: Turn this setting off. Don't use parent material.\n" +
|
||||||
|
"* Workaround: Set your world tiles to custom sprites that are entirely transparent.\n" +
|
||||||
|
"(see 'res://addons/TileMapDual/docs/custom_drawing_sprites.mp4' for a non-transparent example)"
|
||||||
|
)
|
||||||
|
_cached_use_parent_material = use_parent_material
|
||||||
|
|
||||||
|
|
||||||
|
## HACK: How long to wait before processing another "frame".
|
||||||
|
## Mainly matters when [godot_4_3_compatibility] is active.
|
||||||
|
@export_range(0.0, 0.1) var refresh_time: float = 0.02
|
||||||
|
var _timer: float = 0.0
|
||||||
|
func _process(delta: float) -> void: # Only used inside the editor
|
||||||
|
if refresh_time < 0.0:
|
||||||
|
return
|
||||||
|
if _timer > 0:
|
||||||
|
_timer -= delta
|
||||||
|
return
|
||||||
|
_timer = refresh_time
|
||||||
|
call_deferred('_changed')
|
||||||
|
|
||||||
|
## When toggled on, double-checks ALL cells in the grid every change.
|
||||||
|
## Only use this when running Godot 4.3 and below,
|
||||||
|
## where TileMapLayer could not detect changes properly.
|
||||||
|
@export var godot_4_3_compatibility: bool = _godot_is_below_4_4()
|
||||||
|
|
||||||
|
## Detects if godot is below v4.4.
|
||||||
|
## Only used to detect whether the _update_cells() function is usable.
|
||||||
|
func _godot_is_below_4_4():
|
||||||
|
var version := Engine.get_version_info()
|
||||||
|
return version.major < 4 or version.major == 4 and version.minor < 4
|
||||||
|
|
||||||
|
## Called by signals when the tileset changes,
|
||||||
|
## or by _process inside the editor.
|
||||||
|
func _changed() -> void:
|
||||||
|
_tileset_watcher.update(tile_set)
|
||||||
|
|
||||||
|
var updated_cells := []
|
||||||
|
# HACK: double check all tiles every refresh
|
||||||
|
if godot_4_3_compatibility and tile_set != null:
|
||||||
|
var current_cells := TileCache.new()
|
||||||
|
current_cells.update(self, get_used_cells())
|
||||||
|
updated_cells = current_cells.xor(_display.cached_cells)
|
||||||
|
|
||||||
|
_display.update(updated_cells)
|
||||||
|
_make_self_invisible()
|
||||||
|
|
||||||
|
|
||||||
|
## Called when the user draws on the map or presses undo/redo.
|
||||||
|
func _update_cells(coords: Array[Vector2i], forced_cleanup: bool) -> void:
|
||||||
|
if is_instance_valid(_display):
|
||||||
|
_display.update(coords)
|
||||||
|
|
||||||
|
|
||||||
|
##[br] Public method to add and remove tiles.
|
||||||
|
##[br]
|
||||||
|
##[br] - 'cell' is a vector with the cell position.
|
||||||
|
##[br] - 'terrain' is which terrain type to draw.
|
||||||
|
##[br] - terrain -1 completely removes the tile,
|
||||||
|
##[br] - and by default, terrain 0 is the empty tile.
|
||||||
|
func draw_cell(cell: Vector2i, terrain: int = 1) -> void:
|
||||||
|
var terrains := _display.terrain.terrains
|
||||||
|
if terrain not in terrains:
|
||||||
|
erase_cell(cell)
|
||||||
|
changed.emit()
|
||||||
|
return
|
||||||
|
var tile_to_use: Dictionary = terrains[terrain]
|
||||||
|
var sid: int = tile_to_use.sid
|
||||||
|
var tile: Vector2i = tile_to_use.tile
|
||||||
|
set_cell(cell, sid, tile)
|
||||||
|
changed.emit()
|
||||||
|
|
||||||
|
## Public method to get the terrain at a specific coordinate.
|
||||||
|
func get_cell(cell: Vector2i) -> int:
|
||||||
|
return get_cell_tile_data(cell).terrain
|
||||||
1
addons/TileMapDual/TileMapDual.gd.uid
Normal file
1
addons/TileMapDual/TileMapDual.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cjk8nronimk5r
|
||||||
38
addons/TileMapDual/TileMapDual.svg
Normal file
38
addons/TileMapDual/TileMapDual.svg
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="TileMapDual.svg"
|
||||||
|
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#999999"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="35.5"
|
||||||
|
inkscape:cx="8"
|
||||||
|
inkscape:cy="8.028169"
|
||||||
|
inkscape:window-width="1366"
|
||||||
|
inkscape:window-height="736"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<path
|
||||||
|
fill="#8da5f3"
|
||||||
|
d="M8 2 6.25 3.375 8 4.75l1.75-1.375zm2.624 2.062-1.75 1.375 1.75 1.375 1.75-1.375Zm2.626 2.063L11.5 7.5l1.75 1.375L15 7.5ZM5.376 4.062l-1.75 1.375 1.75 1.375 1.75-1.375zM8 6.124 6.25 7.499 8 8.874l1.75-1.375zm2.626 2.063-1.75 1.375 1.75 1.375 1.75-1.375ZM2.75 6.125 1 7.5l1.75 1.375L4.5 7.5Zm2.624 2.062-1.75 1.375 1.75 1.375 1.75-1.375ZM8 10.25l-1.75 1.375L8 13l1.75-1.375z"
|
||||||
|
id="path1"
|
||||||
|
style="fill:#ffb273;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
43
addons/TileMapDual/TileMapDual.svg.import
Normal file
43
addons/TileMapDual/TileMapDual.svg.import
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bqu55yxko3ofm"
|
||||||
|
path="res://.godot/imported/TileMapDual.svg-74ed91a3d196dbe51dce1133c9b2d6f7.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://addons/TileMapDual/TileMapDual.svg"
|
||||||
|
dest_files=["res://.godot/imported/TileMapDual.svg-74ed91a3d196dbe51dce1133c9b2d6f7.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
||||||
333
addons/TileMapDual/TileMapDualLegacy.gd
Normal file
333
addons/TileMapDual/TileMapDualLegacy.gd
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
@tool
|
||||||
|
@icon('TileMapDual.svg')
|
||||||
|
class_name TileMapDualLegacy
|
||||||
|
extends TileMapLayer
|
||||||
|
|
||||||
|
## Material for the display tilemap.
|
||||||
|
@export_custom(PROPERTY_HINT_RESOURCE_TYPE, "ShaderMaterial,CanvasItemMaterial")
|
||||||
|
var display_material: Material:
|
||||||
|
get:
|
||||||
|
return display_material
|
||||||
|
set(new_material): # Custom setter so that it gets copied
|
||||||
|
display_material = new_material
|
||||||
|
if display_tilemap: # Copy over, only if display tilemap is initiated
|
||||||
|
display_tilemap.material = display_material
|
||||||
|
|
||||||
|
var display_tilemap: TileMapLayer = null
|
||||||
|
var _filled_cells: Dictionary = {}
|
||||||
|
var _emptied_cells: Dictionary = {}
|
||||||
|
var _tile_shape: TileSet.TileShape = TileSet.TileShape.TILE_SHAPE_SQUARE
|
||||||
|
var _tile_size: Vector2i = Vector2i(16, 16)
|
||||||
|
## Coordinates for the fully-filled tile in the Atlas that
|
||||||
|
## will be used to sketch in the World grid.
|
||||||
|
## Only this tile will be considered for autotiling.
|
||||||
|
var full_tile: Vector2i = Vector2i(2,1)
|
||||||
|
## The opposed of full_tile. Used to erase sketched tiles.
|
||||||
|
var empty_tile: Vector2i = Vector2i(0,3)
|
||||||
|
var _should_check_cells: bool = false
|
||||||
|
## Prevents checking the cells more than once when the entire tileset
|
||||||
|
## is being updated, which is indicated by `_should_check_cells`.
|
||||||
|
var _checked_cells: Dictionary = {}
|
||||||
|
var is_isometric: bool = false
|
||||||
|
var _atlas_id: int
|
||||||
|
|
||||||
|
## We will use a bit-wise logic, so that a summation over all sketched
|
||||||
|
## neighbours provides a unique key, assigned to the corresponding
|
||||||
|
## tile from the Atlas through the NEIGHBOURS_TO_ATLAS dictionary.
|
||||||
|
enum _location {
|
||||||
|
TOP_LEFT = 1,
|
||||||
|
LOW_LEFT = 2,
|
||||||
|
TOP_RIGHT = 4,
|
||||||
|
LOW_RIGHT = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _direction {
|
||||||
|
TOP,
|
||||||
|
LEFT,
|
||||||
|
BOTTOM,
|
||||||
|
RIGHT,
|
||||||
|
BOTTOM_LEFT,
|
||||||
|
BOTTOM_RIGHT,
|
||||||
|
TOP_LEFT,
|
||||||
|
TOP_RIGHT,
|
||||||
|
}
|
||||||
|
|
||||||
|
## Overlapping tiles from the World grid
|
||||||
|
## that a tile from the Dual grid has.
|
||||||
|
const _NEIGHBORS := {
|
||||||
|
_direction.TOP : TileSet.CellNeighbor.CELL_NEIGHBOR_TOP_SIDE,
|
||||||
|
_direction.LEFT : TileSet.CellNeighbor.CELL_NEIGHBOR_LEFT_SIDE,
|
||||||
|
_direction.RIGHT : TileSet.CellNeighbor.CELL_NEIGHBOR_RIGHT_SIDE,
|
||||||
|
_direction.BOTTOM : TileSet.CellNeighbor.CELL_NEIGHBOR_BOTTOM_SIDE,
|
||||||
|
_direction.TOP_LEFT : TileSet.CellNeighbor.CELL_NEIGHBOR_TOP_LEFT_CORNER,
|
||||||
|
_direction.TOP_RIGHT : TileSet.CellNeighbor.CELL_NEIGHBOR_TOP_RIGHT_CORNER,
|
||||||
|
_direction.BOTTOM_LEFT : TileSet.CellNeighbor.CELL_NEIGHBOR_BOTTOM_LEFT_CORNER,
|
||||||
|
_direction.BOTTOM_RIGHT : TileSet.CellNeighbor.CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER
|
||||||
|
}
|
||||||
|
|
||||||
|
## Overlapping tiles from the World grid
|
||||||
|
## that a tile from the Dual grid has.
|
||||||
|
## To be used ONLY with isometric tilesets.
|
||||||
|
## CellNighbors are literal, even for Isometric
|
||||||
|
const _NEIGHBORS_ISOMETRIC := {
|
||||||
|
_direction.TOP : TileSet.CellNeighbor.CELL_NEIGHBOR_TOP_RIGHT_SIDE,
|
||||||
|
_direction.LEFT : TileSet.CellNeighbor.CELL_NEIGHBOR_TOP_LEFT_SIDE,
|
||||||
|
_direction.RIGHT : TileSet.CellNeighbor.CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE,
|
||||||
|
_direction.BOTTOM : TileSet.CellNeighbor.CELL_NEIGHBOR_BOTTOM_LEFT_SIDE,
|
||||||
|
_direction.TOP_LEFT : TileSet.CellNeighbor.CELL_NEIGHBOR_TOP_CORNER,
|
||||||
|
_direction.TOP_RIGHT : TileSet.CellNeighbor.CELL_NEIGHBOR_RIGHT_CORNER,
|
||||||
|
_direction.BOTTOM_LEFT : TileSet.CellNeighbor.CELL_NEIGHBOR_LEFT_CORNER,
|
||||||
|
_direction.BOTTOM_RIGHT : TileSet.CellNeighbor.CELL_NEIGHBOR_BOTTOM_CORNER
|
||||||
|
}
|
||||||
|
|
||||||
|
## Dict to assign the Atlas coordinates from the
|
||||||
|
## summation over all sketched NEIGHBOURS.
|
||||||
|
## Follows the official 2x2 template.
|
||||||
|
## Works for isometric as well.
|
||||||
|
const _NEIGHBORS_TO_ATLAS: Dictionary = {
|
||||||
|
0: Vector2i(0,3),
|
||||||
|
1: Vector2i(3,3),
|
||||||
|
2: Vector2i(0,0),
|
||||||
|
3: Vector2i(3,2),
|
||||||
|
4: Vector2i(0,2),
|
||||||
|
5: Vector2i(1,2),
|
||||||
|
6: Vector2i(2,3),
|
||||||
|
7: Vector2i(3,1),
|
||||||
|
8: Vector2i(1,3),
|
||||||
|
9: Vector2i(0,1),
|
||||||
|
10: Vector2i(3,0),
|
||||||
|
11: Vector2i(2,0),
|
||||||
|
12: Vector2i(1,0),
|
||||||
|
13: Vector2i(2,2),
|
||||||
|
14: Vector2i(1,1),
|
||||||
|
15: Vector2i(2,1)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
set_process(true)
|
||||||
|
else: # Run in-game using signals for better performance
|
||||||
|
set_process(false)
|
||||||
|
self.changed.connect(_update_tileset, 1)
|
||||||
|
update_full_tileset()
|
||||||
|
# Fire copy upon any property change. More specific than _update_tileset
|
||||||
|
self.changed.connect(copy_properties, 1)
|
||||||
|
|
||||||
|
func _process(_delta): # Only used inside the editor
|
||||||
|
if not self.tile_set:
|
||||||
|
return
|
||||||
|
call_deferred('_update_tileset')
|
||||||
|
|
||||||
|
## Copies properties from parent TileMapDual to child display tilemap
|
||||||
|
func copy_properties() -> void:
|
||||||
|
# Both tilemaps must be the same, so we copy all relevant properties
|
||||||
|
# Tilemap
|
||||||
|
display_tilemap.tile_set = self.tile_set
|
||||||
|
# Rendering
|
||||||
|
display_tilemap.y_sort_origin = self.y_sort_origin
|
||||||
|
display_tilemap.x_draw_order_reversed = self.x_draw_order_reversed
|
||||||
|
display_tilemap.rendering_quadrant_size = self.rendering_quadrant_size
|
||||||
|
# Physics
|
||||||
|
display_tilemap.collision_enabled = self.collision_enabled
|
||||||
|
display_tilemap.use_kinematic_bodies = self.use_kinematic_bodies
|
||||||
|
display_tilemap.collision_visibility_mode = self.collision_visibility_mode
|
||||||
|
# Navigation
|
||||||
|
display_tilemap.navigation_enabled = self.navigation_enabled
|
||||||
|
display_tilemap.navigation_visibility_mode = self.navigation_visibility_mode
|
||||||
|
# Canvas item properties
|
||||||
|
display_tilemap.show_behind_parent = self.show_behind_parent
|
||||||
|
display_tilemap.top_level = self.top_level
|
||||||
|
display_tilemap.light_mask = self.light_mask
|
||||||
|
display_tilemap.visibility_layer = self.visibility_layer
|
||||||
|
display_tilemap.y_sort_enabled = self.y_sort_enabled
|
||||||
|
display_tilemap.modulate = self.modulate
|
||||||
|
|
||||||
|
# If user has set a material in the original slot, copy it over for redundancy
|
||||||
|
# Helps both migration to new version, and prevents user mistakes
|
||||||
|
if self.material:
|
||||||
|
display_material = self.material
|
||||||
|
|
||||||
|
# Set material for first time
|
||||||
|
display_tilemap.material = display_material
|
||||||
|
|
||||||
|
# Save any manually introduced alpha modulation:
|
||||||
|
if self.self_modulate.a != 0.0:
|
||||||
|
display_tilemap.self_modulate = self.self_modulate
|
||||||
|
self.self_modulate.a = 0.0 # Override modulation to prevent render bugs with certain shaders
|
||||||
|
|
||||||
|
self.material = null # Unset TileMapDual's material, to prevent render of it
|
||||||
|
|
||||||
|
## Set the dual grid as a child of TileMapDual.
|
||||||
|
func _set_display_tilemap() -> void:
|
||||||
|
if not self.tile_set:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add the display TileMapLayer, if it doesn't already exist
|
||||||
|
if not get_node_or_null("WorldTileMap"):
|
||||||
|
display_tilemap = TileMapLayer.new()
|
||||||
|
display_tilemap.name = "WorldTileMap"
|
||||||
|
add_child(display_tilemap)
|
||||||
|
|
||||||
|
copy_properties() # Copy properties from TileMapDual to displayed tilemap
|
||||||
|
|
||||||
|
update_geometry()
|
||||||
|
display_tilemap.clear()
|
||||||
|
|
||||||
|
## Update the size and shape of the tileset, displacing the display TileMapLayer accordingly.
|
||||||
|
func update_geometry() -> void:
|
||||||
|
is_isometric = self.tile_set.tile_shape == TileSet.TileShape.TILE_SHAPE_ISOMETRIC
|
||||||
|
var offset := Vector2(self.tile_set.tile_size) * -0.5
|
||||||
|
if is_isometric:
|
||||||
|
offset.x = 0
|
||||||
|
display_tilemap.position = offset
|
||||||
|
_tile_size = self.tile_set.tile_size
|
||||||
|
_tile_shape = self.tile_set.tile_shape
|
||||||
|
|
||||||
|
|
||||||
|
## Update the entire tileset, processing all the cells in the map.
|
||||||
|
func update_full_tileset() -> void:
|
||||||
|
if display_tilemap == null:
|
||||||
|
_set_display_tilemap()
|
||||||
|
elif display_tilemap.tile_set != self.tile_set: # TO-DO: merge with the above
|
||||||
|
_set_display_tilemap()
|
||||||
|
_should_check_cells = true
|
||||||
|
for _cell in self.get_used_cells():
|
||||||
|
if _is_world_tile_sketched(_cell) == 1 or _is_world_tile_sketched(_cell) == 0:
|
||||||
|
update_tile(_cell)
|
||||||
|
_should_check_cells = false
|
||||||
|
_checked_cells = {}
|
||||||
|
# _checked_cells is only used when updating
|
||||||
|
# the whole tilemap to avoid repeating checks.
|
||||||
|
# This is skipped when updating tiles individually.
|
||||||
|
|
||||||
|
## Update only the very specific tiles that have changed.
|
||||||
|
## Much more efficient than update_full_tileset.
|
||||||
|
## Called by signals when the tileset changes,
|
||||||
|
## or by _process inside the editor.
|
||||||
|
func _update_tileset() -> void:
|
||||||
|
if display_tilemap == null:
|
||||||
|
update_full_tileset()
|
||||||
|
return
|
||||||
|
elif display_tilemap.tile_set != self.tile_set: # TO-DO: merge with the above
|
||||||
|
update_full_tileset()
|
||||||
|
return
|
||||||
|
elif _tile_size != self.tile_set.tile_size or _tile_shape != self.tile_set.tile_shape:
|
||||||
|
update_geometry()
|
||||||
|
return
|
||||||
|
|
||||||
|
var _new_emptied_cells: Dictionary = array_to_dict(get_used_cells_by_id(-1, empty_tile))
|
||||||
|
var _new_filled_cells: Dictionary = array_to_dict(get_used_cells_by_id(-1, full_tile))
|
||||||
|
var _changed_cells: Dictionary = exclude_dicts(_emptied_cells, _new_emptied_cells).merged(exclude_dicts(_filled_cells, _new_filled_cells))
|
||||||
|
|
||||||
|
_emptied_cells = _new_emptied_cells
|
||||||
|
_filled_cells = _new_filled_cells
|
||||||
|
for _cell in _changed_cells:
|
||||||
|
update_tile(_cell)
|
||||||
|
|
||||||
|
func array_to_dict(array: Array) -> Dictionary:
|
||||||
|
var dict: Dictionary = {}
|
||||||
|
for item in array:
|
||||||
|
dict[item] = true
|
||||||
|
return dict
|
||||||
|
|
||||||
|
## Return the values that are not shared between the arrays
|
||||||
|
func exclude_dicts(a: Dictionary, b: Dictionary) -> Dictionary:
|
||||||
|
var result = a.duplicate()
|
||||||
|
for item in b:
|
||||||
|
if result.has(item):
|
||||||
|
result.erase(item)
|
||||||
|
else:
|
||||||
|
result[item] = true
|
||||||
|
return result
|
||||||
|
|
||||||
|
## Takes a cell, and updates the overlapping tiles from the dual grid accordingly.
|
||||||
|
func update_tile(world_cell: Vector2i, recurse: bool = true) -> void:
|
||||||
|
_atlas_id = self.get_cell_source_id(world_cell)
|
||||||
|
|
||||||
|
# to not fall in a recursive loop because of a large space of emptiness in the map
|
||||||
|
if (!recurse and _atlas_id == -1):
|
||||||
|
return
|
||||||
|
|
||||||
|
var __NEIGHBORS = _NEIGHBORS_ISOMETRIC if is_isometric else _NEIGHBORS
|
||||||
|
var _top_left = world_cell
|
||||||
|
var _low_left = display_tilemap.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.BOTTOM])
|
||||||
|
var _top_right = display_tilemap.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.RIGHT])
|
||||||
|
var _low_right = display_tilemap.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.BOTTOM_RIGHT])
|
||||||
|
_update_displayed_tile(_top_left)
|
||||||
|
_update_displayed_tile(_low_left)
|
||||||
|
_update_displayed_tile(_top_right)
|
||||||
|
_update_displayed_tile(_low_right)
|
||||||
|
|
||||||
|
# if atlas id is -1 the tile is empty, so to have a good rendering we need to update surroundings
|
||||||
|
if (_atlas_id == -1):
|
||||||
|
update_tile(self.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.LEFT]), false)
|
||||||
|
update_tile(self.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.TOP_LEFT]), false)
|
||||||
|
update_tile(self.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.TOP]), false)
|
||||||
|
update_tile(self.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.TOP_RIGHT]), false)
|
||||||
|
update_tile(self.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.RIGHT]), false)
|
||||||
|
update_tile(self.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.BOTTOM_RIGHT]), false)
|
||||||
|
update_tile(self.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.BOTTOM]), false)
|
||||||
|
update_tile(self.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.BOTTOM_LEFT]), false)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_displayed_tile(_display_cell: Vector2i) -> void:
|
||||||
|
# Avoid updating cells more than necessary
|
||||||
|
if _should_check_cells:
|
||||||
|
if _display_cell in _checked_cells:
|
||||||
|
return
|
||||||
|
_checked_cells[_display_cell] = true
|
||||||
|
|
||||||
|
var __NEIGHBORS = _NEIGHBORS_ISOMETRIC if is_isometric else _NEIGHBORS
|
||||||
|
var _top_left = display_tilemap.get_neighbor_cell(_display_cell, __NEIGHBORS[_direction.TOP_LEFT])
|
||||||
|
var _low_left = display_tilemap.get_neighbor_cell(_display_cell, __NEIGHBORS[_direction.LEFT])
|
||||||
|
var _top_right = display_tilemap.get_neighbor_cell(_display_cell, __NEIGHBORS[_direction.TOP])
|
||||||
|
var _low_right = _display_cell
|
||||||
|
|
||||||
|
# We perform a bitwise summation over the sketched neighbours
|
||||||
|
var _tile_key: int = 0
|
||||||
|
if _is_world_tile_sketched(_top_left) == 1:
|
||||||
|
_tile_key += _location.TOP_LEFT
|
||||||
|
if _is_world_tile_sketched(_low_left) == 1:
|
||||||
|
_tile_key += _location.LOW_LEFT
|
||||||
|
if _is_world_tile_sketched(_top_right) == 1:
|
||||||
|
_tile_key += _location.TOP_RIGHT
|
||||||
|
if _is_world_tile_sketched(_low_right) == 1:
|
||||||
|
_tile_key += _location.LOW_RIGHT
|
||||||
|
|
||||||
|
var _coords_atlas: Vector2i = _NEIGHBORS_TO_ATLAS[_tile_key]
|
||||||
|
display_tilemap.set_cell(_display_cell, _atlas_id, _coords_atlas)
|
||||||
|
|
||||||
|
|
||||||
|
## Return -1 if the cell is empty, 0 if sketched with the empty tile,
|
||||||
|
## and 1 if it is sketched with the fully-filled tile.
|
||||||
|
func _is_world_tile_sketched(_world_cell: Vector2i):
|
||||||
|
var _atlas_coords = get_cell_atlas_coords(_world_cell)
|
||||||
|
if _atlas_coords == full_tile:
|
||||||
|
return 1
|
||||||
|
elif _atlas_coords == empty_tile:
|
||||||
|
return 0
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
## Public method to add and remove tiles, as
|
||||||
|
## TileMapDual.draw(cell, tile, atlas_id).
|
||||||
|
## 'cell' is a vector with the cell position.
|
||||||
|
## 'tile' is 1 to draw the full tile (default), 0 to draw the empty tile,
|
||||||
|
## and -1 to completely remove the tile.
|
||||||
|
## 'atlas_id' is the atlas id of the tileset to modify, 0 by default.
|
||||||
|
## This method replaces the deprecated 'fill_tile' and 'erase_tile' methods.
|
||||||
|
func draw(cell: Vector2i, tile: int = 1, atlas_id: int = 0) -> void:
|
||||||
|
# Prevents a crash if this is called on the first frame
|
||||||
|
if display_tilemap == null:
|
||||||
|
update_full_tileset()
|
||||||
|
var tile_to_use: Vector2i
|
||||||
|
if tile == 1:
|
||||||
|
tile_to_use = full_tile
|
||||||
|
if tile == 0:
|
||||||
|
tile_to_use = empty_tile
|
||||||
|
if tile == -1:
|
||||||
|
tile_to_use = Vector2i(-1, -1)
|
||||||
|
atlas_id = -1
|
||||||
|
set_cell(cell, atlas_id, tile_to_use)
|
||||||
|
update_tile(cell)
|
||||||
1
addons/TileMapDual/TileMapDualLegacy.gd.uid
Normal file
1
addons/TileMapDual/TileMapDualLegacy.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dlyqj0u8ckhb0
|
||||||
181
addons/TileMapDual/TileSetWatcher.gd
Normal file
181
addons/TileMapDual/TileSetWatcher.gd
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
## Provides information about a TileSet and sends signals when it changes.
|
||||||
|
class_name TileSetWatcher
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
## Caches the previous tile_set to see when it changes.
|
||||||
|
var tile_set: TileSet
|
||||||
|
## Caches the previous tile_size to see when it changes.
|
||||||
|
var tile_size: Vector2i
|
||||||
|
## caches the previous result of display.tileset_grid_shape(tile_set) to see when it changes.
|
||||||
|
var grid_shape: Display.GridShape
|
||||||
|
|
||||||
|
func _init(tile_set: TileSet) -> void:
|
||||||
|
# tileset_deleted.connect(func(): print('tileset_deleted'), 1)
|
||||||
|
# tileset_created.connect(func(): print('tileset_created'), 1)
|
||||||
|
# tileset_resized.connect(func(): print('tileset_resized'), 1)
|
||||||
|
# tileset_reshaped.connect(func(): print('tileset_reshaped'), 1)
|
||||||
|
atlas_added.connect(_atlas_added, 1)
|
||||||
|
update(tile_set)
|
||||||
|
|
||||||
|
|
||||||
|
var _flag_tileset_deleted := false
|
||||||
|
## Emitted when the parent TileMapDual's tile_set is cleared or replaced.
|
||||||
|
signal tileset_deleted
|
||||||
|
|
||||||
|
var _flag_tileset_created := false
|
||||||
|
## Emitted when the parent TileMapDual's tile_set is created or replaced.
|
||||||
|
signal tileset_created
|
||||||
|
|
||||||
|
var _flag_tileset_resized := false
|
||||||
|
## Emitted when tile_set.tile_size is changed.
|
||||||
|
signal tileset_resized
|
||||||
|
|
||||||
|
var _flag_tileset_reshaped := false
|
||||||
|
## Emitted when the GridShape of the TileSet would be different.
|
||||||
|
signal tileset_reshaped
|
||||||
|
|
||||||
|
var _flag_atlas_added := false
|
||||||
|
## Emitted when a new Atlas is added to this TileSet.
|
||||||
|
## Does not react to Scenes being added to the TileSet.
|
||||||
|
signal atlas_added(source_id: int, atlas: TileSetAtlasSource)
|
||||||
|
func _atlas_added(source_id: int, atlas: TileSetAtlasSource) -> void:
|
||||||
|
_flag_atlas_added = true
|
||||||
|
#print('SIGNAL EMITTED: atlas_added(%s)' % {'source_id': source_id, 'atlas': atlas})
|
||||||
|
|
||||||
|
|
||||||
|
## Emitted when the watcher thinks that "Yes" was clicked for:
|
||||||
|
## 'Would you like to automatically create tiles in the atlas?'
|
||||||
|
signal atlas_autotiled(source_id: int, atlas: TileSetAtlasSource)
|
||||||
|
|
||||||
|
|
||||||
|
var _flag_terrains_changed := false
|
||||||
|
## Emitted when an atlas is added or removed,
|
||||||
|
## or when the terrains change in one of the Atlases.
|
||||||
|
## NOTE: Prefer connecting to TerrainDual.changed instead of TileSetWatcher.terrains_changed.
|
||||||
|
signal terrains_changed
|
||||||
|
|
||||||
|
|
||||||
|
## Checks if anything about the concerned TileMapDual's tile_set changed.
|
||||||
|
## Must be called by the TileMapDual every frame.
|
||||||
|
func update(tile_set: TileSet) -> void:
|
||||||
|
check_tile_set(tile_set)
|
||||||
|
check_flags()
|
||||||
|
|
||||||
|
|
||||||
|
## Emit update signals if the corresponding flags were set.
|
||||||
|
## Must only be run once per frame.
|
||||||
|
func check_flags() -> void:
|
||||||
|
if _flag_tileset_changed:
|
||||||
|
_flag_tileset_changed = false
|
||||||
|
_update_tileset()
|
||||||
|
if _flag_tileset_deleted:
|
||||||
|
_flag_tileset_deleted = false
|
||||||
|
_flag_tileset_reshaped = true
|
||||||
|
tileset_deleted.emit()
|
||||||
|
if _flag_tileset_created:
|
||||||
|
_flag_tileset_created = false
|
||||||
|
_flag_tileset_reshaped = true
|
||||||
|
tileset_created.emit()
|
||||||
|
if _flag_tileset_resized:
|
||||||
|
_flag_tileset_resized = false
|
||||||
|
tileset_resized.emit()
|
||||||
|
if _flag_tileset_reshaped:
|
||||||
|
_flag_tileset_reshaped = false
|
||||||
|
_flag_terrains_changed = true
|
||||||
|
tileset_reshaped.emit()
|
||||||
|
if _flag_atlas_added:
|
||||||
|
_flag_atlas_added = false
|
||||||
|
_flag_terrains_changed = true
|
||||||
|
if _flag_terrains_changed:
|
||||||
|
_flag_terrains_changed = false
|
||||||
|
terrains_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
## Check if tile_set has been added, replaced, or deleted.
|
||||||
|
func check_tile_set(tile_set: TileSet) -> void:
|
||||||
|
if tile_set == self.tile_set:
|
||||||
|
return
|
||||||
|
if self.tile_set != null:
|
||||||
|
self.tile_set.changed.disconnect(_set_tileset_changed)
|
||||||
|
_cached_source_count = 0
|
||||||
|
_cached_sids.clear()
|
||||||
|
_flag_tileset_deleted = true
|
||||||
|
self.tile_set = tile_set
|
||||||
|
if self.tile_set != null:
|
||||||
|
self.tile_set.changed.connect(_set_tileset_changed, 1)
|
||||||
|
self.tile_set.emit_changed()
|
||||||
|
_flag_tileset_created = true
|
||||||
|
emit_changed()
|
||||||
|
|
||||||
|
|
||||||
|
var _flag_tileset_changed := false
|
||||||
|
## Helper method to be called when the tile_set detects a change.
|
||||||
|
## Must be disconnected when the tile_set is changed.
|
||||||
|
func _set_tileset_changed() -> void:
|
||||||
|
_flag_tileset_changed = true
|
||||||
|
|
||||||
|
|
||||||
|
## Called when _flag_tileset_changed.
|
||||||
|
## Provides more detail about what changed.
|
||||||
|
func _update_tileset() -> void:
|
||||||
|
var tile_size = tile_set.tile_size
|
||||||
|
if self.tile_size != tile_size:
|
||||||
|
self.tile_size = tile_size
|
||||||
|
_flag_tileset_resized = true
|
||||||
|
var grid_shape = Display.tileset_gridshape(tile_set)
|
||||||
|
if self.grid_shape != grid_shape:
|
||||||
|
self.grid_shape = grid_shape
|
||||||
|
_flag_tileset_reshaped = true
|
||||||
|
_update_tileset_atlases()
|
||||||
|
|
||||||
|
|
||||||
|
# Cached variables from the previous frame
|
||||||
|
# These are used to compare what changed between frames
|
||||||
|
var _cached_source_count: int = 0
|
||||||
|
var _cached_sids := {}
|
||||||
|
# TODO: detect automatic tile creation
|
||||||
|
## Checks if new atlases have been added.
|
||||||
|
## Does not check which ones were deleted.
|
||||||
|
func _update_tileset_atlases() -> void:
|
||||||
|
# Update all tileset sources
|
||||||
|
var source_count := tile_set.get_source_count()
|
||||||
|
|
||||||
|
# Only if an asset was added or removed
|
||||||
|
# FIXME?: may break on add+remove in 1 frame
|
||||||
|
if _cached_source_count == source_count:
|
||||||
|
return
|
||||||
|
_cached_source_count = source_count
|
||||||
|
|
||||||
|
# Process the new atlases in the TileSet
|
||||||
|
var sids := {}
|
||||||
|
for i in source_count:
|
||||||
|
var sid: int = tile_set.get_source_id(i)
|
||||||
|
if sid in _cached_sids:
|
||||||
|
sids[sid] = _cached_sids[sid]
|
||||||
|
continue
|
||||||
|
var source: TileSetSource = tile_set.get_source(sid)
|
||||||
|
if source is not TileSetAtlasSource:
|
||||||
|
push_warning(
|
||||||
|
"Non-Atlas TileSet found at index %i, source id %i.\n" % [i, source] +
|
||||||
|
"Dual Grids only support Atlas TileSets."
|
||||||
|
)
|
||||||
|
sids[sid] = null
|
||||||
|
continue
|
||||||
|
var atlas: TileSetAtlasSource = source
|
||||||
|
# FIXME?: check if this needs to be disconnected
|
||||||
|
# SETUP:
|
||||||
|
# - add logging to check which Watcher's flag was changed
|
||||||
|
# - add a TileSet with an atlas to 2 TileMapDuals
|
||||||
|
# - remove the TileSet
|
||||||
|
# - modify the terrains on one of the atlases
|
||||||
|
# - check how many watchers were flagged:
|
||||||
|
# - if 2 watchers were flagged, this is bad.
|
||||||
|
# try to repeatedly add and remove the tileset.
|
||||||
|
# this could either cause the flag to happen multiple times,
|
||||||
|
# or it could stay at 2 watchers.
|
||||||
|
# - if 1 watcher was flagged, that is ok
|
||||||
|
sids[sid] = AtlasWatcher.new(self, sid, atlas)
|
||||||
|
atlas_added.emit(sid, atlas)
|
||||||
|
_flag_terrains_changed = true
|
||||||
|
# FIXME?: find which sid's were deleted
|
||||||
|
_cached_sids = sids
|
||||||
1
addons/TileMapDual/TileSetWatcher.gd.uid
Normal file
1
addons/TileMapDual/TileSetWatcher.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bjmo6oy8a4k2k
|
||||||
62
addons/TileMapDual/Util.gd
Normal file
62
addons/TileMapDual/Util.gd
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
## Utility functions.
|
||||||
|
class_name Util
|
||||||
|
|
||||||
|
|
||||||
|
## Merges an Array of keys and an Array of values into a Dictionary.
|
||||||
|
static func arrays_to_dict(keys: Array, values: Array) -> Dictionary:
|
||||||
|
var out := {}
|
||||||
|
for i in keys.size():
|
||||||
|
out[keys[i]] = values[i]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Swaps the X and Y axes of a Vector2i.
|
||||||
|
static func transpose_vec(v: Vector2i) -> Vector2i:
|
||||||
|
return Vector2i(v.y, v.x)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: transposed(TileSet.CellNeighbor) -> Tileset.CellNeighbor
|
||||||
|
|
||||||
|
|
||||||
|
## Reverses the direction of a CellNeighbor.
|
||||||
|
static func reverse_neighbor(neighbor: TileSet.CellNeighbor) -> TileSet.CellNeighbor:
|
||||||
|
return (neighbor + 8) % 16
|
||||||
|
|
||||||
|
|
||||||
|
## Returns a shorthand name for a CellNeighbor.
|
||||||
|
static func neighbor_name(neighbor: TileSet.CellNeighbor) -> String:
|
||||||
|
const DIRECTIONS := ['E', 'SE', 'S', 'SW', 'W', 'NW', 'N', 'NE']
|
||||||
|
return DIRECTIONS[neighbor >> 1]
|
||||||
|
|
||||||
|
|
||||||
|
## Returns a pretty-printable neighborhood.
|
||||||
|
static func neighborhood_str(neighborhood: Array) -> String:
|
||||||
|
var neighbors := array_of(-1, 16)
|
||||||
|
for i in neighborhood.size():
|
||||||
|
neighbors[neighborhood[i]] = i
|
||||||
|
|
||||||
|
var get := func(neighbor: TileSet.CellNeighbor) -> String:
|
||||||
|
var terrain = neighbors[neighbor]
|
||||||
|
return '-' if terrain == -1 else str(terrain)
|
||||||
|
|
||||||
|
var nw = get.call(TileSet.CELL_NEIGHBOR_TOP_LEFT_CORNER)
|
||||||
|
var n = get.call(TileSet.CELL_NEIGHBOR_TOP_CORNER)
|
||||||
|
var ne = get.call(TileSet.CELL_NEIGHBOR_TOP_RIGHT_CORNER)
|
||||||
|
var w = get.call(TileSet.CELL_NEIGHBOR_LEFT_CORNER)
|
||||||
|
var e = get.call(TileSet.CELL_NEIGHBOR_RIGHT_CORNER)
|
||||||
|
var sw = get.call(TileSet.CELL_NEIGHBOR_BOTTOM_LEFT_CORNER)
|
||||||
|
var s = get.call(TileSet.CELL_NEIGHBOR_BOTTOM_CORNER)
|
||||||
|
var se = get.call(TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER)
|
||||||
|
|
||||||
|
return (
|
||||||
|
"%2s %2s %2s\n" % [nw, n, ne] +
|
||||||
|
"%2s C %2s\n" % [w, e] +
|
||||||
|
"%2s %2s %2s\n" % [sw, s, se]
|
||||||
|
)
|
||||||
|
|
||||||
|
## Returns an Array of the given size, all filled with the given value.
|
||||||
|
static func array_of(value: Variant, size: int) -> Array[Variant]:
|
||||||
|
var out := []
|
||||||
|
out.resize(size)
|
||||||
|
out.fill(value)
|
||||||
|
return out
|
||||||
1
addons/TileMapDual/Util.gd.uid
Normal file
1
addons/TileMapDual/Util.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bfbksxcjuwdjt
|
||||||
6
addons/TileMapDual/ghost.gdshader
Normal file
6
addons/TileMapDual/ghost.gdshader
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// A shader that sets all pixels to 0 alpha, making the object invisible.
|
||||||
|
|
||||||
|
shader_type canvas_item;
|
||||||
|
void fragment() {
|
||||||
|
COLOR = vec4(0);
|
||||||
|
}
|
||||||
1
addons/TileMapDual/ghost.gdshader.uid
Normal file
1
addons/TileMapDual/ghost.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://44652e0wv1ve
|
||||||
6
addons/TileMapDual/ghost_material.tres
Normal file
6
addons/TileMapDual/ghost_material.tres
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://cmbcfxlkxxnwq"]
|
||||||
|
|
||||||
|
[ext_resource type="Shader" uid="uid://44652e0wv1ve" path="res://addons/TileMapDual/ghost.gdshader" id="1_gvngp"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
shader = ExtResource("1_gvngp")
|
||||||
7
addons/TileMapDual/plugin.cfg
Normal file
7
addons/TileMapDual/plugin.cfg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[plugin]
|
||||||
|
|
||||||
|
name="TileMapDual"
|
||||||
|
description="An automatic, real-time dual-grid tileset system for Godot!"
|
||||||
|
author="@GilaPixel"
|
||||||
|
version="5.0.0rc3"
|
||||||
|
script="plugin.gd"
|
||||||
63
addons/TileMapDual/plugin.gd
Normal file
63
addons/TileMapDual/plugin.gd
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
@tool
|
||||||
|
class_name TileMapDualEditorPlugin
|
||||||
|
extends EditorPlugin
|
||||||
|
|
||||||
|
static var instance: TileMapDualEditorPlugin = null
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: create a message queue that groups warnings, errors, and messages into categories
|
||||||
|
# so that we don't get 300 lines of the same warnings pushed to console every time we undo/redo
|
||||||
|
|
||||||
|
|
||||||
|
func _enter_tree() -> void:
|
||||||
|
# assign singleton instance
|
||||||
|
instance = self
|
||||||
|
# register custom nodes
|
||||||
|
add_custom_type("TileMapDual", "TileMapLayer", preload("TileMapDual.gd"), preload("TileMapDual.svg"))
|
||||||
|
add_custom_type("CursorDual", "Sprite2D", preload("CursorDual.gd"), preload("CursorDual.svg"))
|
||||||
|
add_custom_type("TileMapDualLegacy", "TileMapLayer", preload("TileMapDualLegacy.gd"), preload("TileMapDual.svg"))
|
||||||
|
# load editor-only functions
|
||||||
|
TileMapDual.autotile = autotile
|
||||||
|
TileMapDual.popup = popup
|
||||||
|
# finish
|
||||||
|
print("plugin TileMapDual loaded")
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
# disable editor-only functions
|
||||||
|
TileMapDual.popup = TileMapDual._editor_only.bind('popup').unbind(2)
|
||||||
|
TileMapDual.autotile = TileMapDual._editor_only.bind('autotile').unbind(3)
|
||||||
|
# remove custom nodes
|
||||||
|
remove_custom_type("TileMapDualLegacy")
|
||||||
|
remove_custom_type("CursorDual")
|
||||||
|
remove_custom_type("TileMapDual")
|
||||||
|
# unassign singleton instance
|
||||||
|
instance = null
|
||||||
|
# finish
|
||||||
|
print("plugin TileMapDual unloaded")
|
||||||
|
|
||||||
|
|
||||||
|
# HACK: functions that reference EditorPlugin, directly or indirectly, cannot be in the publicly exported scripts
|
||||||
|
# or else they simply won't work when exported
|
||||||
|
|
||||||
|
## Shows a popup with a title bar, a message, and an "Ok" button in the middle of the screen.
|
||||||
|
func popup(title: String, message: String) -> void:
|
||||||
|
var popup := AcceptDialog.new()
|
||||||
|
get_editor_interface().get_base_control().add_child(popup)
|
||||||
|
popup.name = 'TileMapDualPopup'
|
||||||
|
popup.title = title
|
||||||
|
popup.dialog_text = message
|
||||||
|
popup.popup_centered()
|
||||||
|
await popup.confirmed
|
||||||
|
popup.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
## Automatically generate terrains when the atlas is initialized.
|
||||||
|
func autotile(source_id: int, atlas: TileSetAtlasSource, tile_set: TileSet):
|
||||||
|
print_stack()
|
||||||
|
var urm := get_undo_redo()
|
||||||
|
urm.create_action("Create tiles in non-transparent texture regions", UndoRedo.MergeMode.MERGE_ALL, self, true)
|
||||||
|
# NOTE: commit_action() is called immediately after.
|
||||||
|
# NOTE: Atlas is guaranteed to have only been auto-generated with no extra peering bit information.
|
||||||
|
TerrainPreset.write_default_preset(urm, tile_set, atlas)
|
||||||
|
urm.commit_action()
|
||||||
1
addons/TileMapDual/plugin.gd.uid
Normal file
1
addons/TileMapDual/plugin.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://irrn0ous8tet
|
||||||
BIN
assets/textures/1bit 16px icons part-2.png
Normal file
BIN
assets/textures/1bit 16px icons part-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
40
assets/textures/1bit 16px icons part-2.png.import
Normal file
40
assets/textures/1bit 16px icons part-2.png.import
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cfsvkp6w82tgh"
|
||||||
|
path="res://.godot/imported/1bit 16px icons part-2.png-964581ea1d15a6f7999a5835abf4d89f.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/textures/1bit 16px icons part-2.png"
|
||||||
|
dest_files=["res://.godot/imported/1bit 16px icons part-2.png-964581ea1d15a6f7999a5835abf4d89f.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -40,13 +40,13 @@ window/size/mode.release=4
|
|||||||
|
|
||||||
[editor_plugins]
|
[editor_plugins]
|
||||||
|
|
||||||
enabled=PackedStringArray("res://addons/dialogue_manager/plugin.cfg")
|
enabled=PackedStringArray("res://addons/TileMapDual/plugin.cfg", "res://addons/dialogue_manager/plugin.cfg")
|
||||||
|
|
||||||
[gui]
|
[gui]
|
||||||
|
|
||||||
fonts/dynamic_fonts/use_oversampling=false
|
|
||||||
theme/default_font_antialiasing=0
|
theme/default_font_antialiasing=0
|
||||||
theme/default_font_subpixel_positioning=0
|
theme/default_font_subpixel_positioning=0
|
||||||
|
theme/lcd_subpixel_layout=0
|
||||||
theme/custom_font="uid://s0mghd0bccm0"
|
theme/custom_font="uid://s0mghd0bccm0"
|
||||||
|
|
||||||
[input]
|
[input]
|
||||||
@@ -110,10 +110,6 @@ common/physics_interpolation=true
|
|||||||
|
|
||||||
textures/canvas_textures/default_texture_filter=0
|
textures/canvas_textures/default_texture_filter=0
|
||||||
textures/default_filters/use_nearest_mipmap_filter=true
|
textures/default_filters/use_nearest_mipmap_filter=true
|
||||||
anti_aliasing/quality/msaa_2d=3
|
|
||||||
anti_aliasing/quality/screen_space_aa=2
|
|
||||||
anti_aliasing/quality/use_taa=true
|
|
||||||
anti_aliasing/quality/use_debanding=true
|
|
||||||
2d/snap/snap_2d_transforms_to_pixel=true
|
2d/snap/snap_2d_transforms_to_pixel=true
|
||||||
2d/snap/snap_2d_vertices_to_pixel=true
|
2d/snap/snap_2d_vertices_to_pixel=true
|
||||||
environment/defaults/default_clear_color.release=Color(0, 0, 0, 1)
|
environment/defaults/default_clear_color.release=Color(0, 0, 0, 1)
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ grow_vertical = 2
|
|||||||
|
|
||||||
[node name="Button" type="Button" parent="TabContainer/Main/VBoxContainer"]
|
[node name="Button" type="Button" parent="TabContainer/Main/VBoxContainer"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_font_sizes/font_size = 8
|
|
||||||
text = "Start"
|
text = "Start"
|
||||||
|
|
||||||
[node name="ProfileCreator" parent="TabContainer" instance=ExtResource("2_ovrgc")]
|
[node name="ProfileCreator" parent="TabContainer" instance=ExtResource("2_ovrgc")]
|
||||||
|
|||||||
@@ -1,18 +1,40 @@
|
|||||||
[gd_scene load_steps=2 format=3 uid="uid://dn0lr1sbir3d3"]
|
[gd_scene load_steps=4 format=3 uid="uid://dn0lr1sbir3d3"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://bxquk4wo56r22" path="res://scripts/menus/util/keyboard.gd" id="1_xrtsm"]
|
[ext_resource type="Script" uid="uid://bxquk4wo56r22" path="res://scripts/menus/util/keyboard.gd" id="1_xrtsm"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://cfsvkp6w82tgh" path="res://assets/textures/1bit 16px icons part-2.png" id="2_v4hro"]
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_l3m3g"]
|
||||||
|
atlas = ExtResource("2_v4hro")
|
||||||
|
region = Rect2(296.5, 82, 13, 13)
|
||||||
|
|
||||||
[node name="Keyboard" type="Control"]
|
[node name="Keyboard" type="Control"]
|
||||||
layout_mode = 3
|
layout_mode = 3
|
||||||
anchors_preset = 15
|
anchors_preset = 15
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 3.0
|
||||||
|
offset_top = 1.0
|
||||||
|
offset_right = 3.0
|
||||||
|
offset_bottom = 1.0
|
||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
size_flags_horizontal = 4
|
size_flags_horizontal = 4
|
||||||
size_flags_vertical = 4
|
size_flags_vertical = 4
|
||||||
script = ExtResource("1_xrtsm")
|
script = ExtResource("1_xrtsm")
|
||||||
|
|
||||||
|
[node name="TextLabel" type="Label" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = -1
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.202
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.202
|
||||||
|
offset_left = -125.0
|
||||||
|
offset_top = -6.4000015
|
||||||
|
offset_right = 126.0
|
||||||
|
offset_bottom = 6.5999985
|
||||||
|
grow_horizontal = 2
|
||||||
|
|
||||||
[node name="MainKeys" type="HFlowContainer" parent="."]
|
[node name="MainKeys" type="HFlowContainer" parent="."]
|
||||||
custom_minimum_size = Vector2(230, 100)
|
custom_minimum_size = Vector2(230, 100)
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
@@ -22,9 +44,9 @@ anchor_top = 0.5
|
|||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
anchor_bottom = 0.5
|
anchor_bottom = 0.5
|
||||||
offset_left = -115.0
|
offset_left = -115.0
|
||||||
offset_top = -50.0
|
offset_top = -50.5
|
||||||
offset_right = 115.0
|
offset_right = 154.0
|
||||||
offset_bottom = 50.0
|
offset_bottom = 50.5
|
||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
|
|
||||||
@@ -73,6 +95,11 @@ custom_minimum_size = Vector2(22, 22)
|
|||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "I"
|
text = "I"
|
||||||
|
|
||||||
|
[node name="UShift" type="Button" parent="MainKeys"]
|
||||||
|
custom_minimum_size = Vector2(22, 22)
|
||||||
|
layout_mode = 2
|
||||||
|
icon = SubResource("AtlasTexture_l3m3g")
|
||||||
|
|
||||||
[node name="Button10" type="Button" parent="MainKeys"]
|
[node name="Button10" type="Button" parent="MainKeys"]
|
||||||
custom_minimum_size = Vector2(22, 22)
|
custom_minimum_size = Vector2(22, 22)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
@@ -143,6 +170,11 @@ custom_minimum_size = Vector2(22, 22)
|
|||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "W"
|
text = "W"
|
||||||
|
|
||||||
|
[node name="USpace" type="Button" parent="MainKeys"]
|
||||||
|
custom_minimum_size = Vector2(74, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Space"
|
||||||
|
|
||||||
[node name="Button24" type="Button" parent="MainKeys"]
|
[node name="Button24" type="Button" parent="MainKeys"]
|
||||||
custom_minimum_size = Vector2(22, 22)
|
custom_minimum_size = Vector2(22, 22)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|||||||
@@ -1 +1,33 @@
|
|||||||
extends Control
|
extends Control
|
||||||
|
|
||||||
|
signal text_changed
|
||||||
|
signal key_pressed
|
||||||
|
|
||||||
|
@onready var main_keys: HFlowContainer = $MainKeys
|
||||||
|
@onready var text_label: Label = $TextLabel
|
||||||
|
|
||||||
|
@export var text: String = "":
|
||||||
|
set(value):
|
||||||
|
text = value
|
||||||
|
text_label.text = text
|
||||||
|
text_changed.emit()
|
||||||
|
|
||||||
|
var shifting: bool = true
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
for key: Button in main_keys.get_children():
|
||||||
|
key.connect("pressed", func():
|
||||||
|
key_pressed.emit()
|
||||||
|
match key.name:
|
||||||
|
"UShift":
|
||||||
|
shifting = !shifting
|
||||||
|
for i_key: Button in main_keys.get_children():
|
||||||
|
if shifting:
|
||||||
|
i_key.text = i_key.text.to_upper()
|
||||||
|
else:
|
||||||
|
i_key.text = i_key.text.to_lower()
|
||||||
|
"USpace":
|
||||||
|
text += " "
|
||||||
|
_:
|
||||||
|
text += key.text
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user