2024-11-16 03:41:32 +02:00
|
|
|
class_name CelTileMap
|
|
|
|
extends PixelCel
|
|
|
|
|
2024-11-25 01:44:37 +02:00
|
|
|
var tileset: TileSetCustom:
|
|
|
|
set(value):
|
|
|
|
tileset = value
|
|
|
|
if is_instance_valid(tileset):
|
|
|
|
indices_x = ceili(float(get_image().get_width()) / tileset.tile_size.x)
|
|
|
|
indices_y = ceili(float(get_image().get_height()) / tileset.tile_size.y)
|
|
|
|
indices.resize(indices_x * indices_y)
|
2024-11-16 03:41:32 +02:00
|
|
|
var indices := PackedInt32Array()
|
|
|
|
var indices_x: int
|
|
|
|
var indices_y: int
|
2024-11-25 14:17:01 +02:00
|
|
|
## Dictionary of [int] and an [Array] of [bool] ([member TileSetPanel.placing_tiles])
|
|
|
|
## and [enum TileSetPanel.TileEditingMode].
|
|
|
|
var undo_redo_modes := {}
|
2024-11-25 19:02:05 +02:00
|
|
|
## Dictionary of [int] and [Array].
|
|
|
|
## The key is the index of the tile in the tileset,
|
|
|
|
## and the value is the index of the tilemap tile that changed first, along with
|
|
|
|
## its image that is being changed when manual mode is enabled.
|
|
|
|
## Gets reset on [method update_tileset].
|
|
|
|
var editing_images := {}
|
2024-11-16 03:41:32 +02:00
|
|
|
|
|
|
|
|
2024-11-20 14:53:21 +02:00
|
|
|
func _init(_tileset: TileSetCustom, _image: ImageExtended, _opacity := 1.0) -> void:
|
2024-11-16 03:41:32 +02:00
|
|
|
super._init(_image, _opacity)
|
|
|
|
tileset = _tileset
|
|
|
|
|
|
|
|
|
2024-11-24 21:36:27 +02:00
|
|
|
func set_index(tile_position: int, index: int) -> void:
|
|
|
|
index = clampi(index, 0, tileset.tiles.size() - 1)
|
|
|
|
tileset.tiles[index].times_used += 1
|
|
|
|
indices[tile_position] = index
|
|
|
|
update_cel_portion(tile_position)
|
|
|
|
Global.canvas.queue_redraw()
|
|
|
|
|
|
|
|
|
2024-11-24 22:23:05 +02:00
|
|
|
func update_tileset(undo: bool) -> void:
|
2024-11-25 19:02:05 +02:00
|
|
|
editing_images.clear()
|
2024-11-25 14:17:01 +02:00
|
|
|
var undos := tileset.project.undos
|
|
|
|
if not undo and not _is_redo():
|
|
|
|
undo_redo_modes[undos] = [TileSetPanel.placing_tiles, TileSetPanel.tile_editing_mode]
|
|
|
|
if undo:
|
|
|
|
undos += 1
|
|
|
|
var tile_editing_mode := _get_tile_editing_mode(undos)
|
2024-11-16 03:41:32 +02:00
|
|
|
for i in indices.size():
|
2024-11-24 16:04:13 +02:00
|
|
|
var coords := get_tile_coords(i)
|
|
|
|
var rect := Rect2i(coords, tileset.tile_size)
|
2024-11-16 03:41:32 +02:00
|
|
|
var image_portion := image.get_region(rect)
|
|
|
|
var index := indices[i]
|
2024-11-25 00:15:06 +02:00
|
|
|
if index >= tileset.tiles.size():
|
2024-11-25 19:02:05 +02:00
|
|
|
printerr("Tile at position ", i, ", mapped to ", index, " is out of bounds!")
|
2024-11-25 00:15:06 +02:00
|
|
|
index = 0
|
|
|
|
indices[i] = 0
|
2024-11-24 02:19:04 +02:00
|
|
|
var current_tile := tileset.tiles[index]
|
2024-11-25 14:17:01 +02:00
|
|
|
if tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL:
|
2024-11-24 16:04:13 +02:00
|
|
|
if image_portion.is_invisible():
|
|
|
|
continue
|
2024-11-24 16:28:21 +02:00
|
|
|
if index == 0:
|
2024-11-24 16:04:13 +02:00
|
|
|
# If the tileset is empty, only then add a new tile.
|
2024-11-21 17:19:06 +02:00
|
|
|
if tileset.tiles.size() <= 1:
|
2024-11-25 14:17:01 +02:00
|
|
|
tileset.add_tile(image_portion, tile_editing_mode)
|
2024-11-21 17:19:06 +02:00
|
|
|
indices[i] = tileset.tiles.size() - 1
|
2024-11-16 03:41:32 +02:00
|
|
|
continue
|
2024-11-24 16:04:13 +02:00
|
|
|
if image_portion.get_data() != current_tile.image.get_data():
|
2024-11-18 03:09:46 +02:00
|
|
|
tileset.replace_tile_at(image_portion, index)
|
2024-11-25 14:17:01 +02:00
|
|
|
elif tile_editing_mode == TileSetPanel.TileEditingMode.AUTO:
|
2024-11-24 02:19:04 +02:00
|
|
|
handle_auto_editing_mode(i, image_portion)
|
|
|
|
else: # Stack
|
2024-11-24 16:04:13 +02:00
|
|
|
if image_portion.is_invisible():
|
|
|
|
continue
|
2024-11-16 15:38:06 +02:00
|
|
|
var found_tile := false
|
|
|
|
for j in range(1, tileset.tiles.size()):
|
|
|
|
var tile := tileset.tiles[j]
|
2024-11-22 01:43:07 +02:00
|
|
|
if image_portion.get_data() == tile.image.get_data():
|
2024-11-16 15:38:06 +02:00
|
|
|
indices[i] = j
|
|
|
|
found_tile = true
|
|
|
|
break
|
|
|
|
if not found_tile:
|
2024-11-25 14:17:01 +02:00
|
|
|
tileset.add_tile(image_portion, tile_editing_mode)
|
2024-11-24 02:19:04 +02:00
|
|
|
indices[i] = tileset.tiles.size() - 1
|
2024-11-24 22:23:05 +02:00
|
|
|
if undo:
|
|
|
|
var tile_removed := tileset.remove_unused_tiles()
|
|
|
|
if tile_removed:
|
2024-11-25 00:37:33 +02:00
|
|
|
re_index_all_tiles()
|
2024-11-24 02:19:04 +02:00
|
|
|
|
|
|
|
|
|
|
|
## Cases:[br]
|
|
|
|
## 0) Portion is transparent. Set its index to 0.
|
|
|
|
## [br]
|
|
|
|
## 0.5) Portion is transparent and mapped.
|
|
|
|
## Set its index to 0 and unuse the mapped tile.
|
|
|
|
## If the mapped tile is removed, educe the index of all portions that have indices greater or equal
|
|
|
|
## than the existing tile's index.
|
|
|
|
## [br]
|
|
|
|
## 1) Portion not mapped, exists in the tileset.
|
|
|
|
## Map the portion to the existing tile and increase its times_used by one.
|
|
|
|
## [br]
|
|
|
|
## 2) Portion not mapped, does not exist in the tileset.
|
|
|
|
## Add the portion as a tile in the tileset, set its index to be the tileset's tile size - 1.
|
|
|
|
## [br]
|
|
|
|
## 3) Portion mapped, tile did not change. Do nothing.
|
|
|
|
## [br]
|
|
|
|
## 4) Portion mapped, exists in the tileset.
|
|
|
|
## The mapped tile still exists in the tileset.
|
|
|
|
## Map the portion to the existing tile, increase its times_used by one,
|
|
|
|
## and reduce the previously mapped tile's times_used by 1.
|
|
|
|
## [br]
|
|
|
|
## 5) Portion mapped, exists in the tileset.
|
|
|
|
## The mapped tile does not exist in the tileset anymore.
|
|
|
|
## Map the portion to the existing tile and increase its times_used by one.
|
|
|
|
## Remove the previously mapped tile,
|
|
|
|
## and reduce the index of all portions that have indices greater or equal
|
|
|
|
## than the existing tile's index.
|
|
|
|
## [br]
|
|
|
|
## 6) Portion mapped, does not exist in the tileset.
|
|
|
|
## The mapped tile still exists in the tileset.
|
|
|
|
## Add the portion as a tile in the tileset, set its index to be the tileset's tile size - 1.
|
|
|
|
## Reduce the previously mapped tile's times_used by 1.
|
|
|
|
## [br]
|
|
|
|
## 7) Portion mapped, does not exist in the tileset.
|
|
|
|
## The mapped tile does not exist in the tileset anymore.
|
|
|
|
## Simply replace the old tile with the new one, do not change its index.
|
|
|
|
func handle_auto_editing_mode(i: int, image_portion: Image) -> void:
|
|
|
|
var index := indices[i]
|
|
|
|
var current_tile := tileset.tiles[index]
|
|
|
|
if image_portion.is_invisible():
|
|
|
|
# Case 0: The portion is transparent.
|
|
|
|
indices[i] = 0
|
|
|
|
if index > 0:
|
|
|
|
# Case 0.5: The portion is transparent and mapped to a tile.
|
|
|
|
var is_removed := tileset.unuse_tile_at_index(index)
|
|
|
|
if is_removed:
|
|
|
|
# Re-index all indices that are after the deleted one.
|
|
|
|
re_index_tiles_after_index(index)
|
|
|
|
return
|
|
|
|
var index_in_tileset := tileset.find_tile(image_portion)
|
|
|
|
if index == 0: # If the portion is not mapped to a tile.
|
|
|
|
if index_in_tileset > -1:
|
|
|
|
# Case 1: The portion is not mapped already,
|
|
|
|
# and it exists in the tileset as a tile.
|
|
|
|
tileset.tiles[index_in_tileset].times_used += 1
|
|
|
|
indices[i] = index_in_tileset
|
|
|
|
else:
|
|
|
|
# Case 2: The portion is not mapped already,
|
|
|
|
# and it does not exist in the tileset.
|
2024-11-25 14:17:01 +02:00
|
|
|
tileset.add_tile(image_portion, TileSetPanel.TileEditingMode.AUTO)
|
2024-11-24 02:19:04 +02:00
|
|
|
indices[i] = tileset.tiles.size() - 1
|
|
|
|
else: # If the portion is already mapped.
|
|
|
|
if image_portion.get_data() == current_tile.image.get_data():
|
|
|
|
# Case 3: The portion is mapped and it did not change.
|
|
|
|
# Do nothing and move on to the next portion.
|
|
|
|
return
|
|
|
|
if index_in_tileset > -1: # If the portion exists in the tileset as a tile.
|
|
|
|
if current_tile.times_used > 1:
|
|
|
|
# Case 4: The portion is mapped and it exists in the tileset as a tile,
|
|
|
|
# and the currently mapped tile still exists in the tileset.
|
|
|
|
tileset.tiles[index_in_tileset].times_used += 1
|
|
|
|
indices[i] = index_in_tileset
|
|
|
|
tileset.unuse_tile_at_index(index)
|
|
|
|
else:
|
|
|
|
# Case 5: The portion is mapped and it exists in the tileset as a tile,
|
|
|
|
# and the currently mapped tile no longer exists in the tileset.
|
|
|
|
tileset.tiles[index_in_tileset].times_used += 1
|
|
|
|
indices[i] = index_in_tileset
|
|
|
|
tileset.remove_tile_at_index(index)
|
|
|
|
# Re-index all indices that are after the deleted one.
|
|
|
|
re_index_tiles_after_index(index)
|
|
|
|
else: # If the portion does not exist in the tileset as a tile.
|
|
|
|
if current_tile.times_used > 1:
|
|
|
|
# Case 6: The portion is mapped and it does not
|
|
|
|
# exist in the tileset as a tile,
|
|
|
|
# and the currently mapped tile still exists in the tileset.
|
|
|
|
tileset.unuse_tile_at_index(index)
|
2024-11-25 14:17:01 +02:00
|
|
|
tileset.add_tile(image_portion, TileSetPanel.TileEditingMode.AUTO)
|
2024-11-24 02:19:04 +02:00
|
|
|
indices[i] = tileset.tiles.size() - 1
|
|
|
|
else:
|
|
|
|
# Case 7: The portion is mapped and it does not
|
|
|
|
# exist in the tileset as a tile,
|
|
|
|
# and the currently mapped tile no longer exists in the tileset.
|
|
|
|
tileset.replace_tile_at(image_portion, index)
|
|
|
|
|
|
|
|
|
|
|
|
## Re-indexes all [member indices] that are larger or equal to [param index],
|
|
|
|
## by reducing their value by one.
|
|
|
|
func re_index_tiles_after_index(index: int) -> void:
|
|
|
|
for i in indices.size():
|
|
|
|
var tmp_index := indices[i]
|
|
|
|
if tmp_index >= index:
|
|
|
|
indices[i] -= 1
|
|
|
|
|
|
|
|
|
2024-11-24 21:36:27 +02:00
|
|
|
func update_cel_portion(tile_position: int) -> void:
|
|
|
|
var coords := get_tile_coords(tile_position)
|
|
|
|
var rect := Rect2i(coords, tileset.tile_size)
|
|
|
|
var image_portion := image.get_region(rect)
|
|
|
|
var index := indices[tile_position]
|
|
|
|
var current_tile := tileset.tiles[index]
|
|
|
|
if image_portion.get_data() != current_tile.image.get_data():
|
|
|
|
var tile_size := current_tile.image.get_size()
|
|
|
|
image.blit_rect(current_tile.image, Rect2i(Vector2i.ZERO, tile_size), coords)
|
|
|
|
|
|
|
|
|
2024-11-25 19:02:05 +02:00
|
|
|
## Unused, should delete.
|
2024-11-24 16:04:13 +02:00
|
|
|
func update_cel_portions() -> void:
|
|
|
|
for i in indices.size():
|
2024-11-24 21:36:27 +02:00
|
|
|
update_cel_portion(i)
|
2024-11-24 16:04:13 +02:00
|
|
|
|
|
|
|
|
2024-11-24 21:36:27 +02:00
|
|
|
func get_tile_coords(portion_position: int) -> Vector2i:
|
|
|
|
var x_coord := float(tileset.tile_size.x) * (portion_position % indices_x)
|
2024-11-25 00:37:33 +02:00
|
|
|
@warning_ignore("integer_division")
|
2024-11-24 21:36:27 +02:00
|
|
|
var y_coord := float(tileset.tile_size.y) * (portion_position / indices_x)
|
2024-11-24 16:04:13 +02:00
|
|
|
return Vector2i(x_coord, y_coord)
|
|
|
|
|
|
|
|
|
2024-11-24 21:36:27 +02:00
|
|
|
func get_tile_position(coords: Vector2i) -> int:
|
2024-11-25 00:37:33 +02:00
|
|
|
@warning_ignore("integer_division")
|
|
|
|
var x := coords.x / tileset.tile_size.x
|
|
|
|
x = clampi(x, 0, indices_x - 1)
|
|
|
|
@warning_ignore("integer_division")
|
|
|
|
var y := coords.y / tileset.tile_size.y
|
|
|
|
y = clampi(y, 0, indices_y - 1)
|
|
|
|
y *= indices_x
|
2024-11-24 21:36:27 +02:00
|
|
|
return x + y
|
|
|
|
|
|
|
|
|
2024-11-25 00:37:33 +02:00
|
|
|
func re_index_all_tiles() -> void:
|
2024-11-24 02:19:04 +02:00
|
|
|
for i in indices.size():
|
2024-11-25 00:37:33 +02:00
|
|
|
var coords := get_tile_coords(i)
|
|
|
|
var rect := Rect2i(coords, tileset.tile_size)
|
2024-11-24 02:19:04 +02:00
|
|
|
var image_portion := image.get_region(rect)
|
|
|
|
if image_portion.is_invisible():
|
|
|
|
indices[i] = 0
|
|
|
|
continue
|
|
|
|
for j in range(1, tileset.tiles.size()):
|
|
|
|
var tile := tileset.tiles[j]
|
|
|
|
if image_portion.get_data() == tile.image.get_data():
|
|
|
|
indices[i] = j
|
|
|
|
break
|
2024-11-16 03:41:32 +02:00
|
|
|
|
|
|
|
|
2024-11-25 14:17:01 +02:00
|
|
|
func _is_redo() -> bool:
|
|
|
|
return Global.control.redone
|
|
|
|
|
|
|
|
|
|
|
|
func _get_tile_editing_mode(undos: int) -> TileSetPanel.TileEditingMode:
|
|
|
|
var tile_editing_mode: TileSetPanel.TileEditingMode
|
|
|
|
if undo_redo_modes.has(undos):
|
|
|
|
tile_editing_mode = undo_redo_modes[undos][1]
|
|
|
|
else:
|
|
|
|
tile_editing_mode = TileSetPanel.tile_editing_mode
|
|
|
|
return tile_editing_mode
|
|
|
|
|
|
|
|
|
2024-11-25 01:44:37 +02:00
|
|
|
# Overridden Methods:
|
2024-11-25 19:02:05 +02:00
|
|
|
func update_texture(undo := false) -> void:
|
|
|
|
var tile_editing_mode := TileSetPanel.tile_editing_mode
|
|
|
|
if undo or _is_redo() or tile_editing_mode != TileSetPanel.TileEditingMode.MANUAL:
|
|
|
|
super.update_texture(undo)
|
|
|
|
return
|
|
|
|
|
|
|
|
for i in indices.size():
|
|
|
|
var index := indices[i]
|
|
|
|
var coords := get_tile_coords(i)
|
|
|
|
var rect := Rect2i(coords, tileset.tile_size)
|
|
|
|
var image_portion := image.get_region(rect)
|
|
|
|
var current_tile := tileset.tiles[index]
|
|
|
|
if index == 0 and tileset.tiles.size() > 1:
|
2024-11-25 01:44:37 +02:00
|
|
|
# Prevent from drawing on empty image portions.
|
2024-11-25 19:02:05 +02:00
|
|
|
var tile_size := current_tile.image.get_size()
|
|
|
|
image.blit_rect(current_tile.image, Rect2i(Vector2i.ZERO, tile_size), coords)
|
|
|
|
continue
|
|
|
|
if editing_images.has(index):
|
|
|
|
var editing_portion := editing_images[index][0] as int
|
|
|
|
if i == editing_portion:
|
|
|
|
editing_images[index] = [i, image_portion]
|
|
|
|
var editing_image := editing_images[index][1] as Image
|
|
|
|
if editing_image.get_data() != image_portion.get_data():
|
|
|
|
var tile_size := image_portion.get_size()
|
|
|
|
image.blit_rect(editing_image, Rect2i(Vector2i.ZERO, tile_size), coords)
|
|
|
|
else:
|
|
|
|
if image_portion.get_data() != current_tile.image.get_data():
|
|
|
|
editing_images[index] = [i, image_portion]
|
|
|
|
super.update_texture(undo)
|
2024-11-25 01:44:37 +02:00
|
|
|
|
|
|
|
|
2024-11-25 02:05:49 +02:00
|
|
|
func size_changed(new_size: Vector2i) -> void:
|
|
|
|
indices_x = ceili(float(new_size.x) / tileset.tile_size.x)
|
|
|
|
indices_y = ceili(float(new_size.y) / tileset.tile_size.y)
|
|
|
|
indices.resize(indices_x * indices_y)
|
|
|
|
re_index_all_tiles()
|
|
|
|
|
|
|
|
|
2024-11-25 01:44:37 +02:00
|
|
|
func on_undo_redo(undo: bool) -> void:
|
2024-11-25 14:17:01 +02:00
|
|
|
var undos := tileset.project.undos
|
|
|
|
if undo:
|
|
|
|
undos += 1
|
|
|
|
if (undo or _is_redo()) and undo_redo_modes.has(undos):
|
|
|
|
var placing_tiles: bool = undo_redo_modes[undos][0]
|
|
|
|
if placing_tiles:
|
|
|
|
re_index_all_tiles()
|
|
|
|
return
|
2024-11-25 01:44:37 +02:00
|
|
|
update_tileset(undo)
|
|
|
|
|
|
|
|
|
|
|
|
func serialize() -> Dictionary:
|
|
|
|
var dict := super.serialize()
|
|
|
|
dict["tile_indices"] = indices
|
|
|
|
return dict
|
|
|
|
|
|
|
|
|
|
|
|
func deserialize(dict: Dictionary) -> void:
|
|
|
|
super.deserialize(dict)
|
|
|
|
indices = dict.get("tile_indices")
|
|
|
|
|
|
|
|
|
2024-11-16 03:41:32 +02:00
|
|
|
func get_class_name() -> String:
|
|
|
|
return "CelTileMap"
|