added the game

This commit is contained in:
JHDev2006
2025-09-13 16:30:32 +01:00
parent 5ef689109b
commit 3773bdaf64
3616 changed files with 263702 additions and 0 deletions

View File

@@ -0,0 +1,939 @@
@tool
extends Control
signal update_overlay
signal force_show_terrains
# The maximum individual tiles the overlay will draw before shortcutting the display
# To prevent editor lag when drawing large rectangles or filling large areas
const MAX_CANVAS_RENDER_TILES = 1500
const TERRAIN_PROPERTIES_SCENE := preload("res://addons/better-terrain/editor/TerrainProperties.tscn")
const TERRAIN_ENTRY_SCENE := preload("res://addons/better-terrain/editor/TerrainEntry.tscn")
const MIN_ZOOM_SETTING := "editor/better_terrain/min_zoom_amount"
const MAX_ZOOM_SETTING := "editor/better_terrain/max_zoom_amount"
# Buttons
@onready var draw_button: Button = $VBox/Toolbar/Draw
@onready var line_button: Button = $VBox/Toolbar/Line
@onready var rectangle_button: Button = $VBox/Toolbar/Rectangle
@onready var fill_button: Button = $VBox/Toolbar/Fill
@onready var replace_button: Button = $VBox/Toolbar/Replace
@onready var paint_type: Button = $VBox/Toolbar/PaintType
@onready var paint_terrain: Button = $VBox/Toolbar/PaintTerrain
@onready var select_tiles: Button = $VBox/Toolbar/SelectTiles
@onready var paint_symmetry: Button = $VBox/Toolbar/PaintSymmetry
@onready var symmetry_options: OptionButton = $VBox/Toolbar/SymmetryOptions
@onready var shuffle_random: Button = $VBox/Toolbar/ShuffleRandom
@onready var zoom_slider_container: VBoxContainer = $VBox/Toolbar/ZoomContainer
@onready var source_selector: MenuBar = $VBox/Toolbar/Sources
@onready var source_selector_popup: PopupMenu = $VBox/Toolbar/Sources/Sources
@onready var clean_button: Button = $VBox/Toolbar/Clean
@onready var layer_up: Button = $VBox/Toolbar/LayerUp
@onready var layer_down: Button = $VBox/Toolbar/LayerDown
@onready var layer_highlight: Button = $VBox/Toolbar/LayerHighlight
@onready var layer_grid: Button = $VBox/Toolbar/LayerGrid
@onready var grid_mode_button: Button = $VBox/HSplit/Terrains/LowerToolbar/GridMode
@onready var quick_mode_button: Button = $VBox/HSplit/Terrains/LowerToolbar/QuickMode
@onready var edit_tool_buttons: HBoxContainer = $VBox/HSplit/Terrains/LowerToolbar/EditTools
@onready var add_terrain_button: Button = $VBox/HSplit/Terrains/LowerToolbar/EditTools/AddTerrain
@onready var edit_terrain_button: Button = $VBox/HSplit/Terrains/LowerToolbar/EditTools/EditTerrain
@onready var pick_icon_button: Button = $VBox/HSplit/Terrains/LowerToolbar/EditTools/PickIcon
@onready var move_up_button: Button = $VBox/HSplit/Terrains/LowerToolbar/EditTools/MoveUp
@onready var move_down_button: Button = $VBox/HSplit/Terrains/LowerToolbar/EditTools/MoveDown
@onready var remove_terrain_button: Button = $VBox/HSplit/Terrains/LowerToolbar/EditTools/RemoveTerrain
@onready var scroll_container: ScrollContainer = $VBox/HSplit/Terrains/Panel/ScrollContainer
@onready var terrain_list: HFlowContainer = $VBox/HSplit/Terrains/Panel/ScrollContainer/TerrainList
@onready var tile_view: Control = $VBox/HSplit/Panel/ScrollArea/TileView
var selected_entry := -2
var tilemap : TileMapLayer
var tileset : TileSet
var undo_manager : EditorUndoRedoManager
var terrain_undo
var draw_overlay := false
var initial_click : Vector2i
var prev_position : Vector2i
var current_position : Vector2i
var tileset_dirty := false
var zoom_slider : HSlider
enum PaintMode {
NO_PAINT,
PAINT,
ERASE
}
enum PaintAction {
NO_ACTION,
LINE,
RECT
}
enum SourceSelectors {
ALL = 1000000,
NONE = 1000001,
}
var paint_mode := PaintMode.NO_PAINT
var paint_action := PaintAction.NO_ACTION
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
draw_button.icon = get_theme_icon("Edit", "EditorIcons")
line_button.icon = get_theme_icon("Line", "EditorIcons")
rectangle_button.icon = get_theme_icon("Rectangle", "EditorIcons")
fill_button.icon = get_theme_icon("Bucket", "EditorIcons")
select_tiles.icon = get_theme_icon("ToolSelect", "EditorIcons")
add_terrain_button.icon = get_theme_icon("Add", "EditorIcons")
edit_terrain_button.icon = get_theme_icon("Tools", "EditorIcons")
pick_icon_button.icon = get_theme_icon("ColorPick", "EditorIcons")
move_up_button.icon = get_theme_icon("ArrowUp", "EditorIcons")
move_down_button.icon = get_theme_icon("ArrowDown", "EditorIcons")
remove_terrain_button.icon = get_theme_icon("Remove", "EditorIcons")
grid_mode_button.icon = get_theme_icon("FileThumbnail", "EditorIcons")
quick_mode_button.icon = get_theme_icon("GuiVisibilityVisible", "EditorIcons")
layer_up.icon = get_theme_icon("MoveUp", "EditorIcons")
layer_down.icon = get_theme_icon("MoveDown", "EditorIcons")
layer_highlight.icon = get_theme_icon("TileMapHighlightSelected", "EditorIcons")
layer_grid.icon = get_theme_icon("Grid", "EditorIcons")
select_tiles.button_group.pressed.connect(_on_bit_button_pressed)
terrain_undo = load("res://addons/better-terrain/editor/TerrainUndo.gd").new()
add_child(terrain_undo)
tile_view.undo_manager = undo_manager
tile_view.terrain_undo = terrain_undo
tile_view.paste_occurred.connect(_on_paste_occurred)
tile_view.change_zoom_level.connect(_on_change_zoom_level)
tile_view.terrain_updated.connect(_on_terrain_updated)
# Zoom slider is manipulated by settings, make it at runtime
zoom_slider = HSlider.new()
zoom_slider.custom_minimum_size = Vector2(100, 0)
zoom_slider.value_changed.connect(tile_view._on_zoom_value_changed)
zoom_slider_container.add_child(zoom_slider)
# Init settings if needed
if !ProjectSettings.has_setting(MIN_ZOOM_SETTING):
ProjectSettings.set(MIN_ZOOM_SETTING, 1.0)
ProjectSettings.add_property_info({
"name": MIN_ZOOM_SETTING,
"type": TYPE_FLOAT,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "0.1,1.0,0.1"
})
ProjectSettings.set_initial_value(MIN_ZOOM_SETTING, 1.0)
ProjectSettings.set_as_basic(MIN_ZOOM_SETTING, true)
if !ProjectSettings.has_setting(MAX_ZOOM_SETTING):
ProjectSettings.set(MAX_ZOOM_SETTING, 8.0)
ProjectSettings.add_property_info({
"name": MAX_ZOOM_SETTING,
"type": TYPE_FLOAT,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "2.0,32.0,1.0"
})
ProjectSettings.set_initial_value(MAX_ZOOM_SETTING, 8.0)
ProjectSettings.set_as_basic(MAX_ZOOM_SETTING, true)
ProjectSettings.set_order(MAX_ZOOM_SETTING, ProjectSettings.get_order(MIN_ZOOM_SETTING) + 1)
ProjectSettings.settings_changed.connect(_on_adjust_settings)
_on_adjust_settings()
zoom_slider.value = 1.0
func _process(delta):
scroll_container.scroll_horizontal = 0
func _on_adjust_settings():
zoom_slider.min_value = ProjectSettings.get_setting(MIN_ZOOM_SETTING, 1.0)
zoom_slider.max_value = ProjectSettings.get_setting(MAX_ZOOM_SETTING, 8.0)
zoom_slider.step = (zoom_slider.max_value - zoom_slider.min_value) / 100.0
func _get_fill_cells(target: Vector2i) -> Array:
var pick := BetterTerrain.get_cell(tilemap, target)
var bounds := tilemap.get_used_rect()
var neighbors = BetterTerrain.data.cells_adjacent_for_fill(tileset)
# No sets yet, so use a dictionary
var checked := {}
var pending := [target]
var goal := []
while !pending.is_empty():
var p = pending.pop_front()
if checked.has(p):
continue
checked[p] = true
if !bounds.has_point(p) or BetterTerrain.get_cell(tilemap, p) != pick:
continue
goal.append(p)
pending.append_array(BetterTerrain.data.neighboring_coords(tilemap, p, neighbors))
return goal
func tiles_about_to_change() -> void:
if tileset and tileset.changed.is_connected(queue_tiles_changed):
tileset.changed.disconnect(queue_tiles_changed)
func tiles_changed() -> void:
# ensure up to date
BetterTerrain._update_terrain_data(tileset)
# clear terrains
for c in terrain_list.get_children():
terrain_list.remove_child(c)
c.queue_free()
# load terrains from tileset
var terrain_count := BetterTerrain.terrain_count(tileset)
var item_count = terrain_count + 1
for i in terrain_count:
var terrain := BetterTerrain.get_terrain(tileset, i)
if i >= terrain_list.get_child_count():
add_terrain_entry(terrain, i)
if item_count > terrain_list.get_child_count():
var terrain := BetterTerrain.get_terrain(tileset, BetterTerrain.TileCategory.EMPTY)
if terrain.valid:
add_terrain_entry(terrain, item_count - 1)
while item_count < terrain_list.get_child_count():
var child = terrain_list.get_child(terrain_list.get_child_count() - 1)
terrain_list.remove_child(child)
child.free()
source_selector_popup.clear()
source_selector_popup.add_item("All", SourceSelectors.ALL)
source_selector_popup.add_item("None", SourceSelectors.NONE)
var source_count = tileset.get_source_count() if tileset else 0
for s in source_count:
var source_id = tileset.get_source_id(s)
var source := tileset.get_source(source_id)
if !(source is TileSetAtlasSource):
continue
var name := source.resource_name
if name.is_empty():
var texture := (source as TileSetAtlasSource).texture
var texture_name := texture.resource_name if texture else ""
if !texture_name.is_empty():
name = texture_name
else:
var texture_path := texture.resource_path if texture else ""
if !texture_path.is_empty():
name = texture_path.get_file()
if !name.is_empty():
name += " "
name += " (ID: %d)" % source_id
source_selector_popup.add_check_item(name, source_id)
source_selector_popup.set_item_checked(source_selector_popup.get_item_index(source_id), true)
source_selector.visible = source_selector_popup.item_count > 3 # All, None and more than one source
update_tile_view_paint()
tile_view.refresh_tileset(tileset)
if tileset and !tileset.changed.is_connected(queue_tiles_changed):
tileset.changed.connect(queue_tiles_changed)
clean_button.visible = BetterTerrain._has_invalid_peering_types(tileset)
tileset_dirty = false
_on_grid_mode_pressed()
_on_quick_mode_pressed()
func about_to_be_visible(visible: bool) -> void:
if !visible:
return
if tileset != tilemap.tile_set:
tiles_about_to_change()
tileset = tilemap.tile_set
tiles_changed()
var settings := EditorInterface.get_editor_settings()
layer_highlight.set_pressed_no_signal(settings.get_setting("editors/tiles_editor/highlight_selected_layer"))
layer_grid.set_pressed_no_signal(settings.get_setting("editors/tiles_editor/display_grid"))
func queue_tiles_changed() -> void:
# Bring terrain data up to date with complex tileset changes
if !tileset or tileset_dirty:
return
tileset_dirty = true
tiles_changed.call_deferred()
func _on_entry_select(index:int):
selected_entry = index
if selected_entry >= BetterTerrain.terrain_count(tileset):
selected_entry = BetterTerrain.TileCategory.EMPTY
for i in range(terrain_list.get_child_count()):
if i != index:
terrain_list.get_child(i).set_selected(false)
update_tile_view_paint()
func _on_clean_pressed() -> void:
var confirmed := [false]
var popup := ConfirmationDialog.new()
popup.dialog_text = tr("Tile set changes have caused terrain to become invalid. Remove invalid terrain data?")
popup.dialog_hide_on_ok = false
popup.confirmed.connect(func():
confirmed[0] = true
popup.hide()
)
EditorInterface.popup_dialog_centered(popup)
await popup.visibility_changed
popup.queue_free()
if confirmed[0]:
undo_manager.create_action("Clean invalid terrain peering data", UndoRedo.MERGE_DISABLE, tileset)
undo_manager.add_do_method(BetterTerrain, &"_clear_invalid_peering_types", tileset)
undo_manager.add_do_method(self, &"tiles_changed")
terrain_undo.create_peering_restore_point(undo_manager, tileset)
undo_manager.add_undo_method(self, &"tiles_changed")
undo_manager.commit_action()
func _on_grid_mode_pressed() -> void:
for c in terrain_list.get_children():
c.grid_mode = grid_mode_button.button_pressed
c.update_style()
func _on_quick_mode_pressed() -> void:
edit_tool_buttons.visible = !quick_mode_button.button_pressed
for c in terrain_list.get_children():
c.visible = !quick_mode_button.button_pressed or c.terrain.type in [BetterTerrain.TerrainType.MATCH_TILES, BetterTerrain.TerrainType.MATCH_VERTICES]
func update_tile_view_paint() -> void:
tile_view.paint = selected_entry
tile_view.queue_redraw()
var editable = tile_view.paint != BetterTerrain.TileCategory.EMPTY
edit_terrain_button.disabled = !editable
move_up_button.disabled = !editable or tile_view.paint == 0
move_down_button.disabled = !editable or tile_view.paint == BetterTerrain.terrain_count(tileset) - 1
remove_terrain_button.disabled = !editable
pick_icon_button.disabled = !editable
func _on_add_terrain_pressed() -> void:
if !tileset:
return
var popup := TERRAIN_PROPERTIES_SCENE.instantiate()
popup.set_category_data(BetterTerrain.get_terrain_categories(tileset))
popup.terrain_name = "New terrain"
popup.terrain_color = Color.from_hsv(randf(), 0.3 + 0.7 * randf(), 0.6 + 0.4 * randf())
popup.terrain_icon = ""
popup.terrain_type = 0
EditorInterface.popup_dialog_centered(popup)
await popup.visibility_changed
if popup.accepted:
undo_manager.create_action("Add terrain type", UndoRedo.MERGE_DISABLE, tileset)
undo_manager.add_do_method(self, &"perform_add_terrain", popup.terrain_name, popup.terrain_color, popup.terrain_type, popup.terrain_categories, {path = popup.terrain_icon})
undo_manager.add_undo_method(self, &"perform_remove_terrain", terrain_list.get_child_count() - 1)
undo_manager.commit_action()
popup.queue_free()
func _on_edit_terrain_pressed() -> void:
if !tileset:
return
if selected_entry < 0:
return
var t := BetterTerrain.get_terrain(tileset, selected_entry)
var categories = BetterTerrain.get_terrain_categories(tileset)
categories = categories.filter(func(x): return x.id != selected_entry)
var popup := TERRAIN_PROPERTIES_SCENE.instantiate()
popup.set_category_data(categories)
t.icon = t.icon.duplicate()
popup.terrain_name = t.name
popup.terrain_type = t.type
popup.terrain_color = t.color
if t.has("icon") and t.icon.has("path"):
popup.terrain_icon = t.icon.path
popup.terrain_categories = t.categories
EditorInterface.popup_dialog_centered(popup)
await popup.visibility_changed
if popup.accepted:
undo_manager.create_action("Edit terrain details", UndoRedo.MERGE_DISABLE, tileset)
undo_manager.add_do_method(self, &"perform_edit_terrain", selected_entry, popup.terrain_name, popup.terrain_color, popup.terrain_type, popup.terrain_categories, {path = popup.terrain_icon})
undo_manager.add_undo_method(self, &"perform_edit_terrain", selected_entry, t.name, t.color, t.type, t.categories, t.icon)
if t.type != popup.terrain_type:
terrain_undo.create_terrain_type_restore_point(undo_manager, tileset)
terrain_undo.create_peering_restore_point_specific(undo_manager, tileset, selected_entry)
undo_manager.commit_action()
popup.queue_free()
func _on_pick_icon_pressed():
if selected_entry < 0:
return
tile_view.pick_icon_terrain = selected_entry
func _on_pick_icon_focus_exited():
tile_view.pick_icon_terrain_cancel = true
pick_icon_button.button_pressed = false
func _on_move_pressed(down: bool) -> void:
if !tileset:
return
if selected_entry < 0:
return
var index1 = selected_entry
var index2 = index1 + (1 if down else -1)
if index2 < 0 or index2 >= terrain_list.get_child_count():
return
undo_manager.create_action("Reorder terrains", UndoRedo.MERGE_DISABLE, tileset)
undo_manager.add_do_method(self, &"perform_swap_terrain", index1, index2)
undo_manager.add_undo_method(self, &"perform_swap_terrain", index1, index2)
undo_manager.commit_action()
func _on_remove_terrain_pressed() -> void:
if !tileset:
return
if selected_entry < 0:
return
# store confirmation in array to pass by ref
var t := BetterTerrain.get_terrain(tileset, selected_entry)
var confirmed := [false]
var popup := ConfirmationDialog.new()
popup.dialog_text = tr("Are you sure you want to remove {0}?").format([t.name])
popup.dialog_hide_on_ok = false
popup.confirmed.connect(func():
confirmed[0] = true
popup.hide()
)
EditorInterface.popup_dialog_centered(popup)
await popup.visibility_changed
popup.queue_free()
if confirmed[0]:
undo_manager.create_action("Remove terrain type", UndoRedo.MERGE_DISABLE, tileset)
undo_manager.add_do_method(self, &"perform_remove_terrain", selected_entry)
undo_manager.add_undo_method(self, &"perform_add_terrain", t.name, t.color, t.type, t.categories, t.icon)
for n in range(terrain_list.get_child_count() - 2, selected_entry, -1):
undo_manager.add_undo_method(self, &"perform_swap_terrain", n, n - 1)
if t.type == BetterTerrain.TerrainType.CATEGORY:
terrain_undo.create_terrain_type_restore_point(undo_manager, tileset)
terrain_undo.create_peering_restore_point_specific(undo_manager, tileset, selected_entry)
undo_manager.commit_action()
func add_terrain_entry(terrain:Dictionary, index:int = -1):
if index < 0:
index = terrain_list.get_child_count()
var entry = TERRAIN_ENTRY_SCENE.instantiate()
entry.tileset = tileset
entry.terrain = terrain
entry.grid_mode = grid_mode_button.button_pressed
entry.select.connect(_on_entry_select)
terrain_list.add_child(entry)
terrain_list.move_child(entry, index)
func remove_terrain_entry(index: int):
terrain_list.get_child(index).free()
for i in range(index, terrain_list.get_child_count()):
var child = terrain_list.get_child(i)
child.terrain = BetterTerrain.get_terrain(tileset, i)
child.update()
func perform_add_terrain(name: String, color: Color, type: int, categories: Array, icon:Dictionary = {}) -> void:
if BetterTerrain.add_terrain(tileset, name, color, type, categories, icon):
var index = BetterTerrain.terrain_count(tileset) - 1
var terrain = BetterTerrain.get_terrain(tileset, index)
add_terrain_entry(terrain, index)
func perform_remove_terrain(index: int) -> void:
if index >= BetterTerrain.terrain_count(tileset):
return
if BetterTerrain.remove_terrain(tileset, index):
remove_terrain_entry(index)
update_tile_view_paint()
func perform_swap_terrain(index1: int, index2: int) -> void:
var lower := min(index1, index2)
var higher := max(index1, index2)
if lower >= terrain_list.get_child_count() or higher >= terrain_list.get_child_count():
return
var item1 = terrain_list.get_child(lower)
var item2 = terrain_list.get_child(higher)
if BetterTerrain.swap_terrains(tileset, lower, higher):
terrain_list.move_child(item1, higher)
item1.terrain = BetterTerrain.get_terrain(tileset, higher)
item1.update()
item2.terrain = BetterTerrain.get_terrain(tileset, lower)
item2.update()
selected_entry = index2
terrain_list.get_child(index2).set_selected(true)
update_tile_view_paint()
func perform_edit_terrain(index: int, name: String, color: Color, type: int, categories: Array, icon: Dictionary = {}) -> void:
if index >= terrain_list.get_child_count():
return
var entry = terrain_list.get_child(index)
# don't overwrite empty icon
var valid_icon = icon
if icon.has("path") and icon.path.is_empty():
var terrain = BetterTerrain.get_terrain(tileset, index)
valid_icon = terrain.icon
if BetterTerrain.set_terrain(tileset, index, name, color, type, categories, valid_icon):
entry.terrain = BetterTerrain.get_terrain(tileset, index)
entry.update()
tile_view.queue_redraw()
func _on_shuffle_random_pressed():
BetterTerrain.use_seed = !shuffle_random.button_pressed
func _on_bit_button_pressed(button: BaseButton) -> void:
match select_tiles.button_group.get_pressed_button():
select_tiles: tile_view.paint_mode = tile_view.PaintMode.SELECT
paint_type: tile_view.paint_mode = tile_view.PaintMode.PAINT_TYPE
paint_terrain: tile_view.paint_mode = tile_view.PaintMode.PAINT_PEERING
paint_symmetry: tile_view.paint_mode = tile_view.PaintMode.PAINT_SYMMETRY
_: tile_view.paint_mode = tile_view.PaintMode.NO_PAINT
tile_view.queue_redraw()
symmetry_options.visible = paint_symmetry.button_pressed
func _on_symmetry_selected(index):
tile_view.paint_symmetry = index
func _on_paste_occurred():
select_tiles.button_pressed = true
func _on_change_zoom_level(value):
zoom_slider.value = value
func _on_terrain_updated(index):
var entry = terrain_list.get_child(index)
entry.terrain = BetterTerrain.get_terrain(tileset, index)
entry.update()
func canvas_tilemap_transform() -> Transform2D:
var transform := tilemap.get_viewport_transform() * tilemap.global_transform
# Handle subviewport
var editor_viewport := EditorInterface.get_editor_viewport_2d()
if tilemap.get_viewport() != editor_viewport:
var container = tilemap.get_viewport().get_parent() as SubViewportContainer
if container:
transform = editor_viewport.global_canvas_transform * container.get_transform() * transform
return transform
func canvas_draw(overlay: Control) -> void:
if !draw_overlay:
return
if selected_entry < 0:
return
var type = selected_entry
var terrain := BetterTerrain.get_terrain(tileset, type)
if !terrain.valid:
return
var tiles := []
var transform := canvas_tilemap_transform()
if paint_action == PaintAction.RECT and paint_mode != PaintMode.NO_PAINT:
var area := Rect2i(initial_click, current_position - initial_click).abs()
# Shortcut fill for large areas
if area.size.x > 1 and area.size.y > 1 and area.size.x * area.size.y > MAX_CANVAS_RENDER_TILES:
var shortcut := PackedVector2Array([
tilemap.map_to_local(area.position),
tilemap.map_to_local(Vector2i(area.end.x, area.position.y)),
tilemap.map_to_local(area.end),
tilemap.map_to_local(Vector2i(area.position.x, area.end.y))
])
overlay.draw_colored_polygon(transform * shortcut, Color(terrain.color, 0.5))
return
for y in range(area.position.y, area.end.y + 1):
for x in range(area.position.x, area.end.x + 1):
tiles.append(Vector2i(x, y))
elif paint_action == PaintAction.LINE and paint_mode != PaintMode.NO_PAINT:
var cells := _get_tileset_line(initial_click, current_position, tileset)
var shape = BetterTerrain.data.cell_polygon(tileset)
for c in cells:
var tile_transform := Transform2D(0.0, tilemap.tile_set.tile_size, 0.0, tilemap.map_to_local(c))
overlay.draw_colored_polygon(transform * tile_transform * shape, Color(terrain.color, 0.5))
elif fill_button.button_pressed:
tiles = _get_fill_cells(current_position)
if tiles.size() > MAX_CANVAS_RENDER_TILES:
tiles.resize(MAX_CANVAS_RENDER_TILES)
else:
tiles.append(current_position)
var shape = BetterTerrain.data.cell_polygon(tileset)
for t in tiles:
var tile_transform := Transform2D(0.0, tilemap.tile_set.tile_size, 0.0, tilemap.map_to_local(t))
overlay.draw_colored_polygon(transform * tile_transform * shape, Color(terrain.color, 0.5))
func canvas_input(event: InputEvent) -> bool:
if selected_entry < 0:
return false
draw_overlay = true
if event is InputEventMouseMotion:
var tr := canvas_tilemap_transform()
var pos := tr.affine_inverse() * Vector2(event.position)
var event_position := tilemap.local_to_map(pos)
prev_position = current_position
if event_position == current_position:
return false
current_position = event_position
update_overlay.emit()
var replace_mode = replace_button.button_pressed
var released : bool = event is InputEventMouseButton and !event.pressed
if released:
terrain_undo.finish_action()
var type = selected_entry
if paint_action == PaintAction.RECT and paint_mode != PaintMode.NO_PAINT:
var area := Rect2i(initial_click, current_position - initial_click).abs()
# Fill from initial_target to target
undo_manager.create_action(tr("Draw terrain rectangle"), UndoRedo.MERGE_DISABLE, tilemap)
for y in range(area.position.y, area.end.y + 1):
for x in range(area.position.x, area.end.x + 1):
var coord := Vector2i(x, y)
if paint_mode == PaintMode.PAINT:
if replace_mode:
undo_manager.add_do_method(BetterTerrain, &"replace_cell", tilemap, coord, type)
else:
undo_manager.add_do_method(BetterTerrain, &"set_cell", tilemap, coord, type)
else:
undo_manager.add_do_method(tilemap, &"erase_cell", coord)
undo_manager.add_do_method(BetterTerrain, &"update_terrain_area", tilemap, area)
terrain_undo.create_tile_restore_point_area(undo_manager, tilemap, area)
undo_manager.commit_action()
update_overlay.emit()
elif paint_action == PaintAction.LINE and paint_mode != PaintMode.NO_PAINT:
undo_manager.create_action(tr("Draw terrain line"), UndoRedo.MERGE_DISABLE, tilemap)
var cells := _get_tileset_line(initial_click, current_position, tileset)
if paint_mode == PaintMode.PAINT:
if replace_mode:
undo_manager.add_do_method(BetterTerrain, &"replace_cells", tilemap, cells, type)
else:
undo_manager.add_do_method(BetterTerrain, &"set_cells", tilemap, cells, type)
elif paint_mode == PaintMode.ERASE:
for c in cells:
undo_manager.add_do_method(tilemap, &"erase_cell", c)
undo_manager.add_do_method(BetterTerrain, &"update_terrain_cells", tilemap, cells)
terrain_undo.create_tile_restore_point(undo_manager, tilemap, cells)
undo_manager.commit_action()
update_overlay.emit()
paint_mode = PaintMode.NO_PAINT
return true
var clicked : bool = event is InputEventMouseButton and event.pressed
if clicked:
paint_mode = PaintMode.NO_PAINT
if (event.is_command_or_control_pressed() and !event.shift_pressed):
var pick = BetterTerrain.get_cell(tilemap, current_position)
if pick >= 0:
terrain_list.get_children()[pick]._on_focus_entered()
#_on_entry_select(pick)
return true
paint_action = PaintAction.NO_ACTION
if rectangle_button.button_pressed:
paint_action = PaintAction.RECT
elif line_button.button_pressed:
paint_action = PaintAction.LINE
elif draw_button.button_pressed:
if event.shift_pressed:
paint_action = PaintAction.LINE
if event.is_command_or_control_pressed():
paint_action = PaintAction.RECT
if event.button_index == MOUSE_BUTTON_LEFT:
paint_mode = PaintMode.PAINT
elif event.button_index == MOUSE_BUTTON_RIGHT:
paint_mode = PaintMode.ERASE
else:
return false
if (clicked or event is InputEventMouseMotion) and paint_mode != PaintMode.NO_PAINT:
if clicked:
initial_click = current_position
terrain_undo.action_index += 1
terrain_undo.action_count = 0
var type = selected_entry
if paint_action == PaintAction.LINE or paint_action == PaintAction.RECT:
# if painting as line, execution happens on release.
# prevent other painting actions from running.
pass
elif draw_button.button_pressed:
undo_manager.create_action(tr("Draw terrain") + str(terrain_undo.action_index), UndoRedo.MERGE_ALL, tilemap, true)
var cells := _get_tileset_line(prev_position, current_position, tileset)
if paint_mode == PaintMode.PAINT:
if replace_mode:
terrain_undo.add_do_method(undo_manager, BetterTerrain, &"replace_cells", [tilemap, cells, type])
else:
terrain_undo.add_do_method(undo_manager, BetterTerrain, &"set_cells", [tilemap, cells, type])
elif paint_mode == PaintMode.ERASE:
for c in cells:
terrain_undo.add_do_method(undo_manager, tilemap, &"erase_cell", [c])
terrain_undo.add_do_method(undo_manager, BetterTerrain, &"update_terrain_cells", [tilemap, cells])
terrain_undo.create_tile_restore_point(undo_manager, tilemap, cells)
undo_manager.commit_action()
terrain_undo.action_count += 1
elif fill_button.button_pressed:
var cells := _get_fill_cells(current_position)
undo_manager.create_action(tr("Fill terrain"), UndoRedo.MERGE_DISABLE, tilemap)
if paint_mode == PaintMode.PAINT:
if replace_mode:
undo_manager.add_do_method(BetterTerrain, &"replace_cells", tilemap, cells, type)
else:
undo_manager.add_do_method(BetterTerrain, &"set_cells", tilemap, cells, type)
elif paint_mode == PaintMode.ERASE:
for c in cells:
undo_manager.add_do_method(tilemap, &"erase_cell", c)
undo_manager.add_do_method(BetterTerrain, &"update_terrain_cells", tilemap, cells)
terrain_undo.create_tile_restore_point(undo_manager, tilemap, cells)
undo_manager.commit_action()
update_overlay.emit()
return true
return false
func canvas_mouse_exit() -> void:
draw_overlay = false
update_overlay.emit()
func _shortcut_input(event) -> void:
if event is InputEventKey:
if event.keycode == KEY_C and (event.is_command_or_control_pressed() and not event.echo):
get_viewport().set_input_as_handled()
tile_view.copy_selection()
if event.keycode == KEY_V and (event.is_command_or_control_pressed() and not event.echo):
get_viewport().set_input_as_handled()
tile_view.paste_selection()
## bresenham alg ported from Geometry2D::bresenham_line()
func _get_line(from:Vector2i, to:Vector2i) -> Array[Vector2i]:
if from == to:
return [to]
var points:Array[Vector2i] = []
var delta := (to - from).abs() * 2
var step := (to - from).sign()
var current := from
if delta.x > delta.y:
var err:int = delta.x / 2
while current.x != to.x:
points.push_back(current);
err -= delta.y
if err < 0:
current.y += step.y
err += delta.x
current.x += step.x
else:
var err:int = delta.y / 2
while current.y != to.y:
points.push_back(current)
err -= delta.x
if err < 0:
current.x += step.x
err += delta.y
current.y += step.y
points.push_back(current);
return points;
## half-offset bresenham alg ported from TileMapEditor::get_line
func _get_tileset_line(from:Vector2i, to:Vector2i, tileset:TileSet) -> Array[Vector2i]:
if tileset.tile_shape == TileSet.TILE_SHAPE_SQUARE:
return _get_line(from, to)
var points:Array[Vector2i] = []
var transposed := tileset.get_tile_offset_axis() == TileSet.TILE_OFFSET_AXIS_VERTICAL
if transposed:
from = Vector2i(from.y, from.x)
to = Vector2i(to.y, to.x)
var delta:Vector2i = to - from
delta = Vector2i(2 * delta.x + abs(posmod(to.y, 2)) - abs(posmod(from.y, 2)), delta.y)
var sign:Vector2i = delta.sign()
var current := from;
points.push_back(Vector2i(current.y, current.x) if transposed else current)
var err := 0
if abs(delta.y) < abs(delta.x):
var err_step:Vector2i = 3 * delta.abs()
while current != to:
err += err_step.y
if err > abs(delta.x):
if sign.x == 0:
current += Vector2i(sign.y, 0)
else:
current += Vector2i(sign.x if bool(current.y % 2) != (sign.x < 0) else 0, sign.y)
err -= err_step.x
else:
current += Vector2i(sign.x, 0)
err += err_step.y
points.push_back(Vector2i(current.y, current.x) if transposed else current)
else:
var err_step:Vector2i = delta.abs()
while current != to:
err += err_step.x
if err > 0:
if sign.x == 0:
current += Vector2i(0, sign.y)
else:
current += Vector2i(sign.x if bool(current.y % 2) != (sign.x < 0) else 0, sign.y)
err -= err_step.y;
else:
if sign.x == 0:
current += Vector2i(0, sign.y)
else:
current += Vector2i(-sign.x if bool(current.y % 2) != (sign.x > 0) else 0, sign.y)
err += err_step.y
points.push_back(Vector2i(current.y, current.x) if transposed else current)
return points
func _on_terrain_enable_id_pressed(id):
if id in [SourceSelectors.ALL, SourceSelectors.NONE]:
for i in source_selector_popup.item_count:
if source_selector_popup.is_item_checkable(i):
source_selector_popup.set_item_checked(i, id == SourceSelectors.ALL)
else:
var index = source_selector_popup.get_item_index(id)
var checked = source_selector_popup.is_item_checked(index)
source_selector_popup.set_item_checked(index, !checked)
var disabled_sources : Array[int]
for i in source_selector_popup.item_count:
if source_selector_popup.is_item_checkable(i) and !source_selector_popup.is_item_checked(i):
disabled_sources.append(source_selector_popup.get_item_id(i))
tile_view.disabled_sources = disabled_sources
func corresponding_tilemap_editor_button(similar: Button) -> Button:
var editors = EditorInterface.get_base_control().find_children("*", "TileMapLayerEditor", true, false)
var tile_map_layer_editor = editors[0]
var buttons = tile_map_layer_editor.find_children("*", "Button", true, false)
for button: Button in buttons:
if button.icon == similar.icon:
return button
return null
func _on_layer_up_or_down_pressed(button: Button) -> void:
var matching_button = corresponding_tilemap_editor_button(button)
if !matching_button:
return
# Major hack, to reduce flicker hide the tileset editor briefly
var editors = EditorInterface.get_base_control().find_children("*", "TileSetEditor", true, false)
var tile_set_editor = editors[0]
matching_button.pressed.emit()
tile_set_editor.modulate = Color.TRANSPARENT
await get_tree().process_frame
await get_tree().process_frame
force_show_terrains.emit()
tile_set_editor.modulate = Color.WHITE
func _on_layer_up_pressed() -> void:
_on_layer_up_or_down_pressed(layer_up)
func _on_layer_down_pressed() -> void:
_on_layer_up_or_down_pressed(layer_down)
func _on_layer_highlight_toggled(toggled: bool) -> void:
var settings = EditorInterface.get_editor_settings()
settings.set_setting("editors/tiles_editor/highlight_selected_layer", toggled)
var highlight = corresponding_tilemap_editor_button(layer_highlight)
if highlight:
highlight.toggled.emit(toggled)
func _on_layer_grid_toggled(toggled: bool) -> void:
var settings = EditorInterface.get_editor_settings()
settings.set_setting("editors/tiles_editor/display_grid", toggled)
var grid = corresponding_tilemap_editor_button(layer_grid)
if grid:
grid.toggled.emit(toggled)

View File

@@ -0,0 +1 @@
uid://ynajlxcomlkc

View File

@@ -0,0 +1,399 @@
[gd_scene load_steps=32 format=3 uid="uid://de8b6h6ieal7r"]
[ext_resource type="Script" uid="uid://ynajlxcomlkc" path="res://addons/better-terrain/editor/Dock.gd" id="1_raoha"]
[ext_resource type="Texture2D" uid="uid://c6lxq2y7mpb18" path="res://addons/better-terrain/icons/EditType.svg" id="2_cpm2t"]
[ext_resource type="Texture2D" uid="uid://y3xy6qdckht6" path="res://addons/better-terrain/icons/Replace.svg" id="2_fvmt6"]
[ext_resource type="Texture2D" uid="uid://bo2cjv08jkvf8" path="res://addons/better-terrain/icons/EditTerrain.svg" id="3_pqb1p"]
[ext_resource type="Texture2D" uid="uid://b0es228gfcykd" path="res://addons/better-terrain/icons/Warning.svg" id="4_6ahwe"]
[ext_resource type="Script" uid="uid://cpm7dq6r0n0sn" path="res://addons/better-terrain/editor/TileView.gd" id="4_nqppq"]
[ext_resource type="Texture2D" uid="uid://co6gwwmog0pjy" path="res://addons/better-terrain/icons/EditSymmetry.svg" id="5_kfjwu"]
[ext_resource type="Texture2D" uid="uid://cs4mdmluiydj6" path="res://addons/better-terrain/icons/ShuffleRandom.svg" id="5_n3owo"]
[ext_resource type="Texture2D" uid="uid://5hm3bfj3dvej" path="res://addons/better-terrain/icons/SymmetryMirror.svg" id="6_mofuh"]
[ext_resource type="Texture2D" uid="uid://dqmc1jp56or8m" path="res://addons/better-terrain/icons/SymmetryFlip.svg" id="7_ojxs0"]
[ext_resource type="Texture2D" uid="uid://cxoewno1cefua" path="res://addons/better-terrain/icons/SymmetryReflect.svg" id="8_8dhyg"]
[ext_resource type="Texture2D" uid="uid://baxhjy28r1iqj" path="res://addons/better-terrain/icons/SymmetryRotateClockwise.svg" id="9_tq76a"]
[ext_resource type="Texture2D" uid="uid://csbwdkr6bc2db" path="res://addons/better-terrain/icons/SymmetryRotateCounterClockwise.svg" id="10_o5h1f"]
[ext_resource type="Texture2D" uid="uid://8mcycyl3e66r" path="res://addons/better-terrain/icons/SymmetryRotate180.svg" id="11_m6syp"]
[ext_resource type="Texture2D" uid="uid://b7fx4mk18lmls" path="res://addons/better-terrain/icons/SymmetryRotateAll.svg" id="12_11vru"]
[ext_resource type="Texture2D" uid="uid://cyjra4g05dwh" path="res://addons/better-terrain/icons/SymmetryAll.svg" id="13_lp5m2"]
[sub_resource type="ButtonGroup" id="ButtonGroup_aon7c"]
[sub_resource type="InputEventKey" id="InputEventKey_saph6"]
device = -1
keycode = 68
unicode = 100
[sub_resource type="Shortcut" id="Shortcut_3k2al"]
events = [SubResource("InputEventKey_saph6")]
[sub_resource type="SVGTexture" id="SVGTexture_nkf6h"]
_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#ff5d5d\" d=\"M2 1v8.586l1.293-1.293a1 1 0 0 1 1.414 0L7 10.587l2.293-2.293a1 1 0 0 1 1.414 0L13 10.586l1-1V6H9V1H2zm8 0v4h4zm-6 9.414-2 2V15h12v-2.586l-.293.293a1 1 0 0 1-1.414 0L10 10.414l-2.293 2.293a1 1 0 0 1-1.414 0L4 10.414z\"/></svg>
"
[sub_resource type="InputEventKey" id="InputEventKey_q1v0d"]
device = -1
keycode = 76
unicode = 108
[sub_resource type="Shortcut" id="Shortcut_wc6bu"]
events = [SubResource("InputEventKey_q1v0d")]
[sub_resource type="InputEventKey" id="InputEventKey_68n3h"]
device = -1
keycode = 82
unicode = 114
[sub_resource type="InputEventKey" id="InputEventKey_qcu1e"]
device = -1
keycode = 67
unicode = 99
[sub_resource type="Shortcut" id="Shortcut_tcjet"]
events = [SubResource("InputEventKey_68n3h"), SubResource("InputEventKey_qcu1e")]
[sub_resource type="InputEventKey" id="InputEventKey_grxy4"]
device = -1
keycode = 66
unicode = 98
[sub_resource type="Shortcut" id="Shortcut_46fac"]
events = [SubResource("InputEventKey_grxy4")]
[sub_resource type="InputEventKey" id="InputEventKey_xd61m"]
device = -1
keycode = 80
unicode = 112
[sub_resource type="Shortcut" id="Shortcut_uwwa1"]
events = [SubResource("InputEventKey_xd61m")]
[sub_resource type="ButtonGroup" id="ButtonGroup_3wrxn"]
allow_unpress = true
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_mpeb7"]
bg_color = Color(0, 0, 0, 0.4)
[node name="Dock" type="Control" node_paths=PackedStringArray("shortcut_context")]
custom_minimum_size = Vector2(0, 100)
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
focus_mode = 2
shortcut_context = NodePath(".")
script = ExtResource("1_raoha")
[node name="VBox" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Toolbar" type="HBoxContainer" parent="VBox"]
layout_mode = 2
[node name="Draw" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
tooltip_text = "Draw terrain
Shift: Draw line.
Ctrl/Cmd+Shift: Draw rectangle."
toggle_mode = true
button_pressed = true
button_group = SubResource("ButtonGroup_aon7c")
shortcut = SubResource("Shortcut_3k2al")
icon = SubResource("SVGTexture_nkf6h")
flat = true
[node name="Line" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
tooltip_text = "Draw line"
toggle_mode = true
button_group = SubResource("ButtonGroup_aon7c")
shortcut = SubResource("Shortcut_wc6bu")
icon = SubResource("SVGTexture_nkf6h")
flat = true
[node name="Rectangle" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
tooltip_text = "Fill a rectangle of terrain"
toggle_mode = true
button_group = SubResource("ButtonGroup_aon7c")
shortcut = SubResource("Shortcut_tcjet")
icon = SubResource("SVGTexture_nkf6h")
flat = true
[node name="Fill" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
tooltip_text = "Bucket fill terrain"
toggle_mode = true
button_group = SubResource("ButtonGroup_aon7c")
shortcut = SubResource("Shortcut_46fac")
icon = SubResource("SVGTexture_nkf6h")
flat = true
[node name="Replace" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
tooltip_text = "Toggle replace mode"
toggle_mode = true
shortcut = SubResource("Shortcut_uwwa1")
icon = ExtResource("2_fvmt6")
[node name="VSeparator" type="VSeparator" parent="VBox/Toolbar"]
layout_mode = 2
[node name="SelectTiles" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
tooltip_text = "Select"
toggle_mode = true
button_group = SubResource("ButtonGroup_3wrxn")
icon = SubResource("SVGTexture_nkf6h")
flat = true
[node name="PaintType" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
tooltip_text = "Paint terrain types"
toggle_mode = true
button_group = SubResource("ButtonGroup_3wrxn")
icon = ExtResource("2_cpm2t")
flat = true
[node name="PaintTerrain" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
tooltip_text = "Paint terrain connecting types"
toggle_mode = true
button_group = SubResource("ButtonGroup_3wrxn")
icon = ExtResource("3_pqb1p")
flat = true
[node name="PaintSymmetry" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
tooltip_text = "Paint tile symmetry"
toggle_mode = true
button_group = SubResource("ButtonGroup_3wrxn")
icon = ExtResource("5_kfjwu")
flat = true
[node name="SymmetryOptions" type="OptionButton" parent="VBox/Toolbar"]
visible = false
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
selected = 0
item_count = 9
popup/item_0/text = "No symmetry"
popup/item_0/id = 8
popup/item_1/text = "Mirror"
popup/item_1/icon = ExtResource("6_mofuh")
popup/item_1/id = 1
popup/item_2/text = "Flip"
popup/item_2/icon = ExtResource("7_ojxs0")
popup/item_2/id = 1
popup/item_3/text = "Reflect"
popup/item_3/icon = ExtResource("8_8dhyg")
popup/item_3/id = 2
popup/item_4/text = "Rotate clockwise"
popup/item_4/icon = ExtResource("9_tq76a")
popup/item_4/id = 3
popup/item_5/text = "Rotate counter-clockwise"
popup/item_5/icon = ExtResource("10_o5h1f")
popup/item_5/id = 4
popup/item_6/text = "Rotate 180"
popup/item_6/icon = ExtResource("11_m6syp")
popup/item_6/id = 5
popup/item_7/text = "All rotations"
popup/item_7/icon = ExtResource("12_11vru")
popup/item_7/id = 6
popup/item_8/text = "All reflections & rotations"
popup/item_8/icon = ExtResource("13_lp5m2")
popup/item_8/id = 7
[node name="VSeparator3" type="VSeparator" parent="VBox/Toolbar"]
layout_mode = 2
[node name="ZoomContainer" type="VBoxContainer" parent="VBox/Toolbar"]
layout_mode = 2
alignment = 1
[node name="Sources" type="MenuBar" parent="VBox/Toolbar"]
layout_mode = 2
[node name="Sources" type="PopupMenu" parent="VBox/Toolbar/Sources"]
auto_translate_mode = 2
auto_translate = false
hide_on_item_selection = false
hide_on_checkable_item_selection = false
[node name="Spacer" type="Control" parent="VBox/Toolbar"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShuffleRandom" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
tooltip_text = "Shuffle random tiles each update"
toggle_mode = true
icon = ExtResource("5_n3owo")
flat = true
[node name="Clean" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
text = "Clean tile data"
icon = ExtResource("4_6ahwe")
[node name="VSeparator2" type="VSeparator" parent="VBox/Toolbar"]
layout_mode = 2
[node name="LayerUp" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
tooltip_text = "Select previous layer"
icon = SubResource("SVGTexture_nkf6h")
flat = true
[node name="LayerDown" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
tooltip_text = "Select next layer"
icon = SubResource("SVGTexture_nkf6h")
flat = true
[node name="LayerHighlight" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
tooltip_text = "Highlight selected layer"
toggle_mode = true
icon = SubResource("SVGTexture_nkf6h")
flat = true
[node name="LayerGrid" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
tooltip_text = "Toggle grid visibility"
toggle_mode = true
icon = SubResource("SVGTexture_nkf6h")
flat = true
[node name="HSplit" type="HSplitContainer" parent="VBox"]
layout_mode = 2
size_flags_vertical = 3
split_offset = 325
[node name="Terrains" type="VBoxContainer" parent="VBox/HSplit"]
layout_mode = 2
[node name="Panel" type="PanelContainer" parent="VBox/HSplit/Terrains"]
layout_mode = 2
size_flags_vertical = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_mpeb7")
[node name="ScrollContainer" type="ScrollContainer" parent="VBox/HSplit/Terrains/Panel"]
layout_mode = 2
horizontal_scroll_mode = 3
[node name="TerrainList" type="HFlowContainer" parent="VBox/HSplit/Terrains/Panel/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="LowerToolbar" type="HBoxContainer" parent="VBox/HSplit/Terrains"]
layout_mode = 2
[node name="GridMode" type="Button" parent="VBox/HSplit/Terrains/LowerToolbar"]
layout_mode = 2
tooltip_text = "Toggle grid view"
toggle_mode = true
icon = SubResource("SVGTexture_nkf6h")
flat = true
[node name="QuickMode" type="Button" parent="VBox/HSplit/Terrains/LowerToolbar"]
auto_translate_mode = 1
layout_mode = 2
tooltip_text = "Toggle quick mode. Only shows paintable terrain types."
toggle_mode = true
icon = SubResource("SVGTexture_nkf6h")
flat = true
[node name="VSeparator" type="VSeparator" parent="VBox/HSplit/Terrains/LowerToolbar"]
layout_mode = 2
[node name="EditTools" type="HBoxContainer" parent="VBox/HSplit/Terrains/LowerToolbar"]
layout_mode = 2
size_flags_horizontal = 3
alignment = 2
[node name="AddTerrain" type="Button" parent="VBox/HSplit/Terrains/LowerToolbar/EditTools"]
layout_mode = 2
tooltip_text = "Add terrain type"
icon = SubResource("SVGTexture_nkf6h")
flat = true
[node name="EditTerrain" type="Button" parent="VBox/HSplit/Terrains/LowerToolbar/EditTools"]
layout_mode = 2
tooltip_text = "Edit terrain type"
icon = SubResource("SVGTexture_nkf6h")
flat = true
[node name="PickIcon" type="Button" parent="VBox/HSplit/Terrains/LowerToolbar/EditTools"]
layout_mode = 2
tooltip_text = "Pick terrain icon from tileset"
toggle_mode = true
icon = SubResource("SVGTexture_nkf6h")
flat = true
[node name="MoveUp" type="Button" parent="VBox/HSplit/Terrains/LowerToolbar/EditTools"]
layout_mode = 2
tooltip_text = "Move selected terrain up"
icon = SubResource("SVGTexture_nkf6h")
flat = true
[node name="MoveDown" type="Button" parent="VBox/HSplit/Terrains/LowerToolbar/EditTools"]
layout_mode = 2
tooltip_text = "Move selected terrain down"
icon = SubResource("SVGTexture_nkf6h")
flat = true
[node name="RemoveTerrain" type="Button" parent="VBox/HSplit/Terrains/LowerToolbar/EditTools"]
layout_mode = 2
tooltip_text = "Remove selected terrain type(s)"
icon = SubResource("SVGTexture_nkf6h")
flat = true
[node name="Panel" type="Panel" parent="VBox/HSplit"]
custom_minimum_size = Vector2(0, 80)
layout_mode = 2
[node name="ScrollArea" type="ScrollContainer" parent="VBox/HSplit/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
[node name="TileView" type="Control" parent="VBox/HSplit/Panel/ScrollArea"]
texture_filter = 1
texture_repeat = 1
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
focus_mode = 2
script = ExtResource("4_nqppq")
[connection signal="item_selected" from="VBox/Toolbar/SymmetryOptions" to="." method="_on_symmetry_selected"]
[connection signal="id_pressed" from="VBox/Toolbar/Sources/Sources" to="." method="_on_terrain_enable_id_pressed"]
[connection signal="pressed" from="VBox/Toolbar/ShuffleRandom" to="." method="_on_shuffle_random_pressed"]
[connection signal="pressed" from="VBox/Toolbar/Clean" to="." method="_on_clean_pressed"]
[connection signal="pressed" from="VBox/Toolbar/LayerUp" to="." method="_on_layer_up_pressed"]
[connection signal="pressed" from="VBox/Toolbar/LayerDown" to="." method="_on_layer_down_pressed"]
[connection signal="toggled" from="VBox/Toolbar/LayerHighlight" to="." method="_on_layer_highlight_toggled"]
[connection signal="toggled" from="VBox/Toolbar/LayerGrid" to="." method="_on_layer_grid_toggled"]
[connection signal="pressed" from="VBox/HSplit/Terrains/LowerToolbar/GridMode" to="." method="_on_grid_mode_pressed"]
[connection signal="pressed" from="VBox/HSplit/Terrains/LowerToolbar/QuickMode" to="." method="_on_quick_mode_pressed"]
[connection signal="pressed" from="VBox/HSplit/Terrains/LowerToolbar/EditTools/AddTerrain" to="." method="_on_add_terrain_pressed"]
[connection signal="pressed" from="VBox/HSplit/Terrains/LowerToolbar/EditTools/EditTerrain" to="." method="_on_edit_terrain_pressed"]
[connection signal="focus_exited" from="VBox/HSplit/Terrains/LowerToolbar/EditTools/PickIcon" to="." method="_on_pick_icon_focus_exited"]
[connection signal="pressed" from="VBox/HSplit/Terrains/LowerToolbar/EditTools/PickIcon" to="." method="_on_pick_icon_pressed"]
[connection signal="pressed" from="VBox/HSplit/Terrains/LowerToolbar/EditTools/MoveUp" to="." method="_on_move_pressed" binds= [false]]
[connection signal="pressed" from="VBox/HSplit/Terrains/LowerToolbar/EditTools/MoveDown" to="." method="_on_move_pressed" binds= [true]]
[connection signal="pressed" from="VBox/HSplit/Terrains/LowerToolbar/EditTools/RemoveTerrain" to="." method="_on_remove_terrain_pressed"]
[connection signal="mouse_exited" from="VBox/HSplit/Panel/ScrollArea/TileView" to="VBox/HSplit/Panel/ScrollArea/TileView" method="clear_highlighted_tile"]

View File

@@ -0,0 +1,185 @@
@tool
extends PanelContainer
signal select(index)
@onready var color_panel := %Color
@onready var terrain_icon_slot := %TerrainIcon
@onready var type_icon_slot := %TypeIcon
@onready var type_icon_panel := %TerrainIconPanel
@onready var name_label := %Name
@onready var layout_container := %Layout
@onready var icon_layout_container := %IconLayout
var selected := false
var tileset:TileSet
var terrain:Dictionary
var grid_mode := false
var color_style_list:StyleBoxFlat
var color_style_grid:StyleBoxFlat
var color_style_decoration:StyleBoxFlat
var _terrain_texture:Texture2D
var _terrain_texture_rect:Rect2i
var _icon_draw_connected := false
func _ready():
update()
func update():
if !terrain or !terrain.valid:
return
if !tileset:
return
name_label.text = terrain.name
tooltip_text = "%s (%d)" % [terrain.name, terrain.id]
color_style_list = color_panel.get_theme_stylebox("panel").duplicate()
color_style_grid = color_panel.get_theme_stylebox("panel").duplicate()
color_style_decoration = color_panel.get_theme_stylebox("panel").duplicate()
color_style_list.bg_color = terrain.color
color_style_list.corner_radius_top_left = 8
color_style_list.corner_radius_bottom_left = 8
color_style_list.corner_radius_top_right = 0
color_style_list.corner_radius_bottom_right = 0
color_style_list.content_margin_left = -1
color_style_list.content_margin_right = -1
color_style_list.border_width_left = 0
color_style_list.border_width_right = 0
color_style_list.border_width_top = 0
color_style_list.border_width_bottom = 0
color_style_grid.bg_color = terrain.color
color_style_grid.corner_radius_top_left = 6
color_style_grid.corner_radius_bottom_left = 6
color_style_grid.corner_radius_top_right = 6
color_style_grid.corner_radius_bottom_right = 6
color_style_grid.content_margin_left = -1
color_style_grid.content_margin_right = -1
color_style_grid.border_width_left = 0
color_style_grid.border_width_right = 0
color_style_grid.border_width_top = 0
color_style_grid.border_width_bottom = 0
color_style_decoration.bg_color = terrain.color
color_style_decoration.corner_radius_top_left = 8
color_style_decoration.corner_radius_bottom_left = 8
color_style_decoration.corner_radius_top_right = 8
color_style_decoration.corner_radius_bottom_right = 8
color_style_decoration.content_margin_left = -1
color_style_decoration.content_margin_right = -1
color_style_decoration.border_width_left = 4
color_style_decoration.border_width_right = 4
color_style_decoration.border_width_top = 4
color_style_decoration.border_width_bottom = 4
match terrain.type:
BetterTerrain.TerrainType.MATCH_TILES:
type_icon_slot.texture = load("res://addons/better-terrain/icons/MatchTiles.svg")
BetterTerrain.TerrainType.MATCH_VERTICES:
type_icon_slot.texture = load("res://addons/better-terrain/icons/MatchVertices.svg")
BetterTerrain.TerrainType.CATEGORY:
type_icon_slot.texture = load("res://addons/better-terrain/icons/NonModifying.svg")
BetterTerrain.TerrainType.DECORATION:
type_icon_slot.texture = load("res://addons/better-terrain/icons/Decoration.svg")
var has_icon = false
if terrain.has("icon"):
if terrain.icon.has("path") and not terrain.icon.path.is_empty():
terrain_icon_slot.texture = load(terrain.icon.path)
_terrain_texture = null
terrain_icon_slot.queue_redraw()
has_icon = true
elif terrain.icon.has("source_id") and tileset.has_source(terrain.icon.source_id):
var source := tileset.get_source(terrain.icon.source_id) as TileSetAtlasSource
var coord := terrain.icon.coord as Vector2i
var rect := source.get_tile_texture_region(coord, 0)
_terrain_texture = source.texture
_terrain_texture_rect = rect
terrain_icon_slot.queue_redraw()
has_icon = true
if not has_icon:
var tiles = BetterTerrain.get_tile_sources_in_terrain(tileset, get_index())
if tiles.size() > 0:
var source := tiles[0].source as TileSetAtlasSource
var coord := tiles[0].coord as Vector2i
var rect := source.get_tile_texture_region(coord, 0)
_terrain_texture = source.texture
_terrain_texture_rect = rect
terrain_icon_slot.queue_redraw()
if _terrain_texture:
terrain_icon_slot.texture = null
if not _icon_draw_connected:
terrain_icon_slot.connect("draw", func():
if _terrain_texture:
terrain_icon_slot.draw_texture_rect_region(_terrain_texture, Rect2i(0,0, 44, 44), _terrain_texture_rect)
)
_icon_draw_connected = true
update_style()
func update_style():
if terrain.type == BetterTerrain.TerrainType.DECORATION:
type_icon_panel.visible = false
color_panel.custom_minimum_size = Vector2i(52,52)
else:
type_icon_panel.visible = true
color_panel.custom_minimum_size = Vector2i(24,24)
if grid_mode:
if terrain.type == BetterTerrain.TerrainType.DECORATION:
color_panel.add_theme_stylebox_override("panel", color_style_decoration)
color_panel.size_flags_vertical = Control.SIZE_FILL
icon_layout_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
else:
color_panel.add_theme_stylebox_override("panel", color_style_grid)
color_panel.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
icon_layout_container.size_flags_vertical = Control.SIZE_FILL
custom_minimum_size = Vector2(0, 60)
size_flags_horizontal = Control.SIZE_FILL
layout_container.vertical = true
name_label.visible = false
icon_layout_container.add_theme_constant_override("separation", -24)
else:
if terrain.type == BetterTerrain.TerrainType.DECORATION:
color_panel.add_theme_stylebox_override("panel", color_style_decoration)
else:
color_panel.add_theme_stylebox_override("panel", color_style_list)
icon_layout_container.size_flags_vertical = Control.SIZE_FILL
custom_minimum_size = Vector2(2000, 60)
size_flags_horizontal = Control.SIZE_EXPAND_FILL
layout_container.vertical = false
name_label.visible = true
color_panel.size_flags_vertical = Control.SIZE_FILL
icon_layout_container.add_theme_constant_override("separation", 4)
func set_selected(value:bool = true):
selected = value
if value:
select.emit(get_index())
queue_redraw()
func _draw():
if selected:
draw_rect(Rect2(Vector2.ZERO, get_rect().size), Color(0.15, 0.70, 1, 0.3))
func _on_focus_entered():
queue_redraw()
selected = true
select.emit(get_index())
func _on_focus_exited():
queue_redraw()

View File

@@ -0,0 +1 @@
uid://c2qfovpuj58b7

View File

@@ -0,0 +1,114 @@
[gd_scene load_steps=8 format=3 uid="uid://u2y444hj182c"]
[ext_resource type="Script" uid="uid://c2qfovpuj58b7" path="res://addons/better-terrain/editor/TerrainEntry.gd" id="1_o2na3"]
[ext_resource type="Texture2D" uid="uid://kmypxsqhynyv" path="res://addons/better-terrain/icons/Decoration.svg" id="2_ossyj"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_3pdcc"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
draw_center = false
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dqhir"]
bg_color = Color(0.243, 0.816, 0.518, 1)
border_color = Color(0, 0, 0, 0.439216)
corner_radius_top_left = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rohyw"]
content_margin_left = 2.0
content_margin_top = 2.0
content_margin_right = 2.0
content_margin_bottom = 2.0
bg_color = Color(0, 0, 0, 0.439216)
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xa0fl"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0, 0, 0, 0.439216)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_b4rkm"]
content_margin_left = 3.0
bg_color = Color(0, 0, 0, 0.439216)
draw_center = false
[node name="TerrainEntry" type="PanelContainer"]
custom_minimum_size = Vector2(60, 60)
offset_right = 200.0
offset_bottom = 60.0
size_flags_vertical = 3
focus_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_3pdcc")
script = ExtResource("1_o2na3")
[node name="Layout" type="BoxContainer" parent="."]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 4
[node name="IconLayout" type="HBoxContainer" parent="Layout"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 4
[node name="Color" type="PanelContainer" parent="Layout/IconLayout"]
unique_name_in_owner = true
z_index = 1
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
size_flags_horizontal = 0
mouse_filter = 1
theme_override_styles/panel = SubResource("StyleBoxFlat_dqhir")
[node name="PanelContainer" type="PanelContainer" parent="Layout/IconLayout/Color"]
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
mouse_filter = 1
theme_override_styles/panel = SubResource("StyleBoxFlat_rohyw")
[node name="TypeIcon" type="TextureRect" parent="Layout/IconLayout/Color/PanelContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
texture = ExtResource("2_ossyj")
[node name="TerrainIconPanel" type="PanelContainer" parent="Layout/IconLayout"]
unique_name_in_owner = true
custom_minimum_size = Vector2(52, 52)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
mouse_filter = 1
theme_override_styles/panel = SubResource("StyleBoxFlat_xa0fl")
[node name="TerrainIcon" type="TextureRect" parent="Layout/IconLayout/TerrainIconPanel"]
unique_name_in_owner = true
texture_filter = 1
custom_minimum_size = Vector2(40, 40)
layout_mode = 2
expand_mode = 4
stretch_mode = 5
[node name="Name" type="Label" parent="Layout"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 0
theme_override_styles/normal = SubResource("StyleBoxFlat_b4rkm")
text = "New Terrain"
vertical_alignment = 1
text_overrun_behavior = 3
[connection signal="focus_entered" from="." to="." method="_on_focus_entered"]
[connection signal="focus_exited" from="." to="." method="_on_focus_exited"]

View File

@@ -0,0 +1,85 @@
@tool
extends ConfirmationDialog
var category_icon := load("res://addons/better-terrain/icons/NonModifying.svg")
const CATEGORY_CHECK_ID = &"category_check_id"
var accepted := false
var terrain_name : String:
set(value): %NameEdit.text = value
get: return %NameEdit.text
var terrain_color : Color:
set(value): %ColorPicker.color = value
get: return %ColorPicker.color
var terrain_icon : String:
set(value): %IconEdit.text = value
get: return %IconEdit.text
var terrain_type : int:
set(value):
%TypeOption.selected = value
_on_type_option_item_selected(value)
get: return %TypeOption.selected
var terrain_categories : Array: set = set_categories, get = get_categories
# category is name, color, id
func set_category_data(options: Array) -> void:
if !options.is_empty():
%CategoryLabel.show()
%CategoryContainer.show()
for o in options:
var c = CheckBox.new()
c.text = o.name
c.icon = category_icon
c.add_theme_color_override(&"icon_normal_color", o.color)
c.add_theme_color_override(&"icon_disabled_color", Color(o.color, 0.4))
c.add_theme_color_override(&"icon_focus_color", o.color)
c.add_theme_color_override(&"icon_hover_color", o.color)
c.add_theme_color_override(&"icon_hover_pressed_color", o.color)
c.add_theme_color_override(&"icon_normal_color", o.color)
c.add_theme_color_override(&"icon_pressed_color", o.color)
c.set_meta(CATEGORY_CHECK_ID, o.id)
%CategoryLayout.add_child(c)
func set_categories(ids : Array):
for c in %CategoryLayout.get_children():
c.button_pressed = c.get_meta(CATEGORY_CHECK_ID) in ids
func get_categories() -> Array:
var result := []
if terrain_type == BetterTerrain.TerrainType.CATEGORY:
return result
for c in %CategoryLayout.get_children():
if c.button_pressed:
result.push_back(c.get_meta(CATEGORY_CHECK_ID))
return result
func _on_confirmed() -> void:
# confirm valid name
if terrain_name.is_empty():
var dialog := AcceptDialog.new()
dialog.dialog_text = "Name cannot be empty"
EditorInterface.popup_dialog_centered(dialog)
await dialog.visibility_changed
dialog.queue_free()
return
accepted = true
hide()
func _on_type_option_item_selected(index: int) -> void:
var categories_available = (index != BetterTerrain.TerrainType.CATEGORY)
for c in %CategoryLayout.get_children():
c.disabled = !categories_available

View File

@@ -0,0 +1 @@
uid://j81f0xo4p36y

View File

@@ -0,0 +1,96 @@
[gd_scene load_steps=5 format=3 uid="uid://fdjybw6e7whr"]
[ext_resource type="Script" uid="uid://j81f0xo4p36y" path="res://addons/better-terrain/editor/TerrainProperties.gd" id="1_52nx8"]
[ext_resource type="Texture2D" uid="uid://d1h1p7pcwdnjk" path="res://addons/better-terrain/icons/MatchTiles.svg" id="2_ncc5p"]
[ext_resource type="Texture2D" uid="uid://dfemy1g6okwlv" path="res://addons/better-terrain/icons/MatchVertices.svg" id="3_0nvmi"]
[ext_resource type="Texture2D" uid="uid://1yr6yruwl63u" path="res://addons/better-terrain/icons/NonModifying.svg" id="5_awp83"]
[node name="TerrainProperties" type="ConfirmationDialog"]
auto_translate_mode = 2
oversampling_override = 1.0
title = "Edit terrain properties"
initial_position = 2
size = Vector2i(317, 257)
visible = true
dialog_hide_on_ok = false
script = ExtResource("1_52nx8")
[node name="GridContainer" type="GridContainer" parent="."]
offset_left = 8.0
offset_top = 8.0
offset_right = 309.0
offset_bottom = 212.0
columns = 2
[node name="NameLabel" type="Label" parent="GridContainer"]
layout_mode = 2
text = "Name"
[node name="NameEdit" type="LineEdit" parent="GridContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Terrain name"
[node name="ColorLabel" type="Label" parent="GridContainer"]
layout_mode = 2
text = "Color"
[node name="ColorPicker" type="ColorPickerButton" parent="GridContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
color = Color(1, 0.262745, 0.498039, 1)
edit_alpha = false
[node name="IconLabel" type="Label" parent="GridContainer"]
layout_mode = 2
text = "Icon"
[node name="IconEdit" type="LineEdit" parent="GridContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Icon path (optional)"
[node name="TypeLabel" type="Label" parent="GridContainer"]
layout_mode = 2
text = "Mode"
[node name="TypeOption" type="OptionButton" parent="GridContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
item_count = 3
popup/item_0/text = "Match tiles"
popup/item_0/icon = ExtResource("2_ncc5p")
popup/item_0/id = 0
popup/item_1/text = "Match vertices"
popup/item_1/icon = ExtResource("3_0nvmi")
popup/item_1/id = 1
popup/item_2/text = "Category"
popup/item_2/icon = ExtResource("5_awp83")
popup/item_2/id = 2
[node name="CategoryLabel" type="Label" parent="GridContainer"]
unique_name_in_owner = true
visible = false
layout_mode = 2
size_flags_vertical = 1
text = "Categories"
[node name="CategoryContainer" type="ScrollContainer" parent="GridContainer"]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(0, 100)
layout_mode = 2
size_flags_vertical = 3
[node name="CategoryLayout" type="VBoxContainer" parent="GridContainer/CategoryContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 100)
layout_mode = 2
size_flags_vertical = 3
[connection signal="confirmed" from="." to="." method="_on_confirmed"]
[connection signal="item_selected" from="GridContainer/TypeOption" to="." method="_on_type_option_item_selected"]

View File

@@ -0,0 +1,190 @@
@tool
extends Node
var action_index := 0
var action_count := 0
var _current_action_index := 0
var _current_action_count := 0
func create_tile_restore_point(undo_manager: EditorUndoRedoManager, tm: TileMapLayer, cells: Array, and_surrounding_cells: bool = true) -> void:
if and_surrounding_cells:
cells = BetterTerrain._widen(tm, cells)
var restore := []
for c in cells:
restore.append([
c,
tm.get_cell_source_id(c),
tm.get_cell_atlas_coords(c),
tm.get_cell_alternative_tile(c)
])
undo_manager.add_undo_method(self, &"restore_tiles", tm, restore)
func create_tile_restore_point_area(undo_manager: EditorUndoRedoManager, tm: TileMapLayer, area: Rect2i, and_surrounding_cells: bool = true) -> void:
area.end += Vector2i.ONE
var restore := []
for y in range(area.position.y, area.end.y):
for x in range(area.position.x, area.end.x):
var c := Vector2i(x, y)
restore.append([
c,
tm.get_cell_source_id(c),
tm.get_cell_atlas_coords(c),
tm.get_cell_alternative_tile(c)
])
undo_manager.add_undo_method(self, &"restore_tiles", tm, restore)
if !and_surrounding_cells:
return
var edges := []
for x in range(area.position.x, area.end.x):
edges.append(Vector2i(x, area.position.y))
edges.append(Vector2i(x, area.end.y))
for y in range(area.position.y + 1, area.end.y - 1):
edges.append(Vector2i(area.position.x, y))
edges.append(Vector2i(area.end.x, y))
edges = BetterTerrain._widen_with_exclusion(tm, edges, area)
create_tile_restore_point(undo_manager, tm, edges, false)
func restore_tiles(tm: TileMapLayer, restore: Array) -> void:
for r in restore:
tm.set_cell(r[0], r[1], r[2], r[3])
func create_peering_restore_point(undo_manager: EditorUndoRedoManager, ts: TileSet) -> void:
var restore := []
for s in ts.get_source_count():
var source_id := ts.get_source_id(s)
var source := ts.get_source(source_id) as TileSetAtlasSource
if !source:
continue
for t in source.get_tiles_count():
var coord := source.get_tile_id(t)
for a in source.get_alternative_tiles_count(coord):
var alternate := source.get_alternative_tile_id(coord, a)
var td := source.get_tile_data(coord, alternate)
var tile_type := BetterTerrain.get_tile_terrain_type(td)
if tile_type == BetterTerrain.TileCategory.NON_TERRAIN:
continue
var peering_dict := {}
for c in BetterTerrain.tile_peering_keys(td):
peering_dict[c] = BetterTerrain.tile_peering_types(td, c)
var symmetry = BetterTerrain.get_tile_symmetry_type(td)
restore.append([source_id, coord, alternate, tile_type, peering_dict, symmetry])
undo_manager.add_undo_method(self, &"restore_peering", ts, restore)
func create_peering_restore_point_specific(undo_manager: EditorUndoRedoManager, ts: TileSet, protect: int) -> void:
var restore := []
for s in ts.get_source_count():
var source_id := ts.get_source_id(s)
var source := ts.get_source(source_id) as TileSetAtlasSource
if !source:
continue
for t in source.get_tiles_count():
var coord := source.get_tile_id(t)
for a in source.get_alternative_tiles_count(coord):
var alternate := source.get_alternative_tile_id(coord, a)
var td := source.get_tile_data(coord, alternate)
var tile_type := BetterTerrain.get_tile_terrain_type(td)
if tile_type == BetterTerrain.TileCategory.NON_TERRAIN:
continue
var to_restore : bool = tile_type == protect
var terrain := BetterTerrain.get_terrain(ts, tile_type)
var cells = BetterTerrain.data.get_terrain_peering_cells(ts, terrain.type)
for c in cells:
if protect in BetterTerrain.tile_peering_types(td, c):
to_restore = true
break
if !to_restore:
continue
var peering_dict := {}
for c in cells:
peering_dict[c] = BetterTerrain.tile_peering_types(td, c)
var symmetry = BetterTerrain.get_tile_symmetry_type(td)
restore.append([source_id, coord, alternate, tile_type, peering_dict, symmetry])
undo_manager.add_undo_method(self, &"restore_peering", ts, restore)
func create_peering_restore_point_tile(undo_manager: EditorUndoRedoManager, ts: TileSet, source_id: int, coord: Vector2i, alternate: int) -> void:
var source := ts.get_source(source_id) as TileSetAtlasSource
var td := source.get_tile_data(coord, alternate)
var tile_type := BetterTerrain.get_tile_terrain_type(td)
var restore := []
var peering_dict := {}
for c in BetterTerrain.tile_peering_keys(td):
peering_dict[c] = BetterTerrain.tile_peering_types(td, c)
var symmetry = BetterTerrain.get_tile_symmetry_type(td)
restore.append([source_id, coord, alternate, tile_type, peering_dict, symmetry])
undo_manager.add_undo_method(self, &"restore_peering", ts, restore)
func restore_peering(ts: TileSet, restore: Array) -> void:
for r in restore:
var source := ts.get_source(r[0]) as TileSetAtlasSource
var td := source.get_tile_data(r[1], r[2])
BetterTerrain.set_tile_terrain_type(ts, td, r[3])
var peering_types = r[4]
for peering in peering_types:
var types := BetterTerrain.tile_peering_types(td, peering)
for t in types:
BetterTerrain.remove_tile_peering_type(ts, td, peering, t)
for t in peering_types[peering]:
BetterTerrain.add_tile_peering_type(ts, td, peering, t)
var symmetry = r[5]
BetterTerrain.set_tile_symmetry_type(ts, td, symmetry)
func create_terrain_type_restore_point(undo_manager: EditorUndoRedoManager, ts: TileSet) -> void:
var count = BetterTerrain.terrain_count(ts)
var restore = []
for i in count:
restore.push_back(BetterTerrain.get_terrain(ts, i))
undo_manager.add_undo_method(self, &"restore_terrain", ts, restore)
func restore_terrain(ts: TileSet, restore: Array) -> void:
for i in restore.size():
var r = restore[i]
BetterTerrain.set_terrain(ts, i, r.name, r.color, r.type, r.categories, r.icon)
func add_do_method(undo_manager: EditorUndoRedoManager, object:Object, method:StringName, args:Array):
if action_index > _current_action_index:
_current_action_index = action_index
_current_action_count = action_count
if action_count > _current_action_count:
_current_action_count = action_count
undo_manager.add_do_method(self, "_do_method", object, method, args, action_count)
func _do_method(object:Object, method:StringName, args:Array, this_action_count:int):
if this_action_count >= _current_action_count:
object.callv(method, args)
func finish_action():
_current_action_count = 0

View File

@@ -0,0 +1 @@
uid://ds1kcnrnbywo6

View File

@@ -0,0 +1,896 @@
@tool
extends Control
signal paste_occurred
signal change_zoom_level(value)
signal terrain_updated(index)
@onready var checkerboard := get_theme_icon("Checkerboard", "EditorIcons")
@onready var paint_symmetry_icons := [
null,
preload("res://addons/better-terrain/icons/paint-symmetry/SymmetryMirror.svg"),
preload("res://addons/better-terrain/icons/paint-symmetry/SymmetryFlip.svg"),
preload("res://addons/better-terrain/icons/paint-symmetry/SymmetryReflect.svg"),
preload("res://addons/better-terrain/icons/paint-symmetry/SymmetryRotateClockwise.svg"),
preload("res://addons/better-terrain/icons/paint-symmetry/SymmetryRotateCounterClockwise.svg"),
preload("res://addons/better-terrain/icons/paint-symmetry/SymmetryRotate180.svg"),
preload("res://addons/better-terrain/icons/paint-symmetry/SymmetryRotateAll.svg"),
preload("res://addons/better-terrain/icons/paint-symmetry/SymmetryAll.svg"),
]
# Draw checkerboard and tiles with specific materials in
# individual canvas items via rendering server
var _canvas_item_map = {}
var _canvas_item_background : RID
var tileset: TileSet
var disabled_sources: Array[int] = []: set = set_disabled_sources
var paint := BetterTerrain.TileCategory.NON_TERRAIN
var paint_symmetry := BetterTerrain.SymmetryType.NONE
var highlighted_tile_part := { valid = false }
var zoom_level := 1.0
var tiles_size : Vector2
var tile_size : Vector2i
var tile_part_size : Vector2
var alternate_size : Vector2
var alternate_lookup := []
var initial_click : Vector2i
var prev_position : Vector2i
var current_position : Vector2i
var selection_start : Vector2i
var selection_end : Vector2i
var selection_rect : Rect2i
var selected_tile_states : Array[Dictionary] = []
var copied_tile_states : Array[Dictionary] = []
var staged_paste_tile_states : Array[Dictionary] = []
var pick_icon_terrain : int = -1
var pick_icon_terrain_cancel := false
var undo_manager : EditorUndoRedoManager
var terrain_undo
# Modes for painting
enum PaintMode {
NO_PAINT,
PAINT_TYPE,
PAINT_PEERING,
PAINT_SYMMETRY,
SELECT,
PASTE
}
var paint_mode := PaintMode.NO_PAINT
# Actual interactions for painting
enum PaintAction {
NO_ACTION,
DRAW_TYPE,
ERASE_TYPE,
DRAW_PEERING,
ERASE_PEERING,
DRAW_SYMMETRY,
ERASE_SYMMETRY,
SELECT,
PASTE
}
var paint_action := PaintAction.NO_ACTION
const ALTERNATE_TILE_MARGIN := 18
func _enter_tree() -> void:
_canvas_item_background = RenderingServer.canvas_item_create()
RenderingServer.canvas_item_set_parent(_canvas_item_background, get_canvas_item())
RenderingServer.canvas_item_set_draw_behind_parent(_canvas_item_background, true)
func _exit_tree() -> void:
RenderingServer.free_rid(_canvas_item_background)
for p in _canvas_item_map:
RenderingServer.free_rid(_canvas_item_map[p])
_canvas_item_map.clear()
func refresh_tileset(ts: TileSet) -> void:
tileset = ts
tiles_size = Vector2.ZERO
alternate_size = Vector2.ZERO
alternate_lookup = []
disabled_sources = []
if !tileset:
return
for s in tileset.get_source_count():
var source_id := tileset.get_source_id(s)
var source := tileset.get_source(source_id) as TileSetAtlasSource
if !source or !source.texture:
continue
tiles_size.x = max(tiles_size.x, source.texture.get_width())
tiles_size.y += source.texture.get_height()
tile_size = source.texture_region_size
tile_part_size = Vector2(tile_size) / 3.0
for t in source.get_tiles_count():
var coord := source.get_tile_id(t)
var alt_count := source.get_alternative_tiles_count(coord)
if alt_count <= 1:
continue
var rect := source.get_tile_texture_region(coord, 0)
alternate_lookup.append([rect.size, source_id, coord])
alternate_size.x = max(alternate_size.x, rect.size.x * (alt_count - 1))
alternate_size.y += rect.size.y
_on_zoom_value_changed(zoom_level)
func is_tile_in_source(source: TileSetAtlasSource, coord: Vector2i) -> bool:
var origin := source.get_tile_at_coords(coord)
if origin == Vector2i(-1, -1):
return false
# Animation frames are not needed
var size := source.get_tile_size_in_atlas(origin)
return coord.x < origin.x + size.x and coord.y < origin.y + size.y
func _build_tile_part_from_position(result: Dictionary, position: Vector2i, rect: Rect2) -> void:
result.rect = rect
var type := BetterTerrain.get_tile_terrain_type(result.data)
if type == BetterTerrain.TileCategory.NON_TERRAIN:
return
result.terrain_type = type
var normalize_position := (Vector2(position) - rect.position) / rect.size
var terrain := BetterTerrain.get_terrain(tileset, type)
if !terrain.valid:
return
for p in BetterTerrain.data.get_terrain_peering_cells(tileset, terrain.type):
var side_polygon = BetterTerrain.data.peering_polygon(tileset, terrain.type, p)
if Geometry2D.is_point_in_polygon(normalize_position, side_polygon):
result.peering = p
result.polygon = side_polygon
break
func tile_part_from_position(position: Vector2i) -> Dictionary:
if !tileset:
return { valid = false }
var offset := Vector2.ZERO
var alt_offset := Vector2.RIGHT * (zoom_level * tiles_size.x + ALTERNATE_TILE_MARGIN)
if Rect2(alt_offset, zoom_level * alternate_size).has_point(position):
for a in alternate_lookup:
if a[1] in disabled_sources:
continue
var next_offset_y = alt_offset.y + zoom_level * a[0].y
if position.y > next_offset_y:
alt_offset.y = next_offset_y
continue
var source := tileset.get_source(a[1]) as TileSetAtlasSource
if !source:
break
var count := source.get_alternative_tiles_count(a[2])
var index := int((position.x - alt_offset.x) / (zoom_level * a[0].x)) + 1
if index < count:
var alt_id := source.get_alternative_tile_id(a[2], index)
var target_rect := Rect2(
alt_offset + Vector2.RIGHT * (index - 1) * zoom_level * a[0].x,
zoom_level * a[0]
)
var result := {
valid = true,
source_id = a[1],
coord = a[2],
alternate = alt_id,
data = source.get_tile_data(a[2], alt_id)
}
_build_tile_part_from_position(result, position, target_rect)
return result
else:
for s in tileset.get_source_count():
var source_id := tileset.get_source_id(s)
if source_id in disabled_sources:
continue
var source := tileset.get_source(source_id) as TileSetAtlasSource
if !source || !source.texture:
continue
for t in source.get_tiles_count():
var coord := source.get_tile_id(t)
var rect := source.get_tile_texture_region(coord, 0)
var target_rect := Rect2(offset + zoom_level * rect.position, zoom_level * rect.size)
if !target_rect.has_point(position):
continue
var result := {
valid = true,
source_id = source_id,
coord = coord,
alternate = 0,
data = source.get_tile_data(coord, 0)
}
_build_tile_part_from_position(result, position, target_rect)
return result
offset.y += zoom_level * source.texture.get_height()
return { valid = false }
func tile_rect_from_position(position: Vector2i) -> Rect2:
if !tileset:
return Rect2(-1,-1,0,0)
var offset := Vector2.ZERO
var alt_offset := Vector2.RIGHT * (zoom_level * tiles_size.x + ALTERNATE_TILE_MARGIN)
if Rect2(alt_offset, zoom_level * alternate_size).has_point(position):
for a in alternate_lookup:
if a[1] in disabled_sources:
continue
var next_offset_y = alt_offset.y + zoom_level * a[0].y
if position.y > next_offset_y:
alt_offset.y = next_offset_y
continue
var source := tileset.get_source(a[1]) as TileSetAtlasSource
if !source:
break
var count := source.get_alternative_tiles_count(a[2])
var index := int((position.x - alt_offset.x) / (zoom_level * a[0].x)) + 1
if index < count:
var target_rect := Rect2(
alt_offset + Vector2.RIGHT * (index - 1) * zoom_level * a[0].x,
zoom_level * a[0]
)
return target_rect
else:
for s in tileset.get_source_count():
var source_id := tileset.get_source_id(s)
if source_id in disabled_sources:
continue
var source := tileset.get_source(source_id) as TileSetAtlasSource
if !source:
continue
for t in source.get_tiles_count():
var coord := source.get_tile_id(t)
var rect := source.get_tile_texture_region(coord, 0)
var target_rect := Rect2(offset + zoom_level * rect.position, zoom_level * rect.size)
if target_rect.has_point(position):
return target_rect
offset.y += zoom_level * source.texture.get_height()
return Rect2(-1,-1,0,0)
func tile_parts_from_rect(rect:Rect2) -> Array[Dictionary]:
if !tileset:
return []
var tiles:Array[Dictionary] = []
var offset := Vector2.ZERO
var alt_offset := Vector2.RIGHT * (zoom_level * tiles_size.x + ALTERNATE_TILE_MARGIN)
for s in tileset.get_source_count():
var source_id := tileset.get_source_id(s)
if source_id in disabled_sources:
continue
var source := tileset.get_source(source_id) as TileSetAtlasSource
if !source:
continue
for t in source.get_tiles_count():
var coord := source.get_tile_id(t)
var tile_rect := source.get_tile_texture_region(coord, 0)
var target_rect := Rect2(offset + zoom_level * tile_rect.position, zoom_level * tile_rect.size)
if target_rect.intersects(rect):
var result := {
valid = true,
source_id = source_id,
coord = coord,
alternate = 0,
data = source.get_tile_data(coord, 0)
}
var pos = target_rect.position + target_rect.size/2
_build_tile_part_from_position(result, pos, target_rect)
tiles.push_back(result)
var alt_count := source.get_alternative_tiles_count(coord)
for a in alt_count:
var alt_id := 0
if a == 0:
continue
target_rect = Rect2(alt_offset + zoom_level * (a - 1) * tile_rect.size.x * Vector2.RIGHT, zoom_level * tile_rect.size)
alt_id = source.get_alternative_tile_id(coord, a)
if target_rect.intersects(rect):
var td := source.get_tile_data(coord, alt_id)
var result := {
valid = true,
source_id = source_id,
coord = coord,
alternate = alt_id,
data = td
}
var pos = target_rect.position + target_rect.size/2
_build_tile_part_from_position(result, pos, target_rect)
tiles.push_back(result)
if alt_count > 1:
alt_offset.y += zoom_level * tile_rect.size.y
offset.y += zoom_level * source.texture.get_height()
return tiles
func _get_canvas_item(td: TileData) -> RID:
if !td.material:
return self.get_canvas_item()
if _canvas_item_map.has(td.material):
return _canvas_item_map[td.material]
var rid = RenderingServer.canvas_item_create()
RenderingServer.canvas_item_set_material(rid, td.material.get_rid())
RenderingServer.canvas_item_set_parent(rid, get_canvas_item())
RenderingServer.canvas_item_set_draw_behind_parent(rid, true)
RenderingServer.canvas_item_set_default_texture_filter(rid, RenderingServer.CANVAS_ITEM_TEXTURE_FILTER_NEAREST)
_canvas_item_map[td.material] = rid
return rid
func _draw_tile_data(texture: Texture2D, rect: Rect2, src_rect: Rect2, td: TileData, draw_sides: bool = true) -> void:
var flipped_rect := rect
if td.flip_h:
flipped_rect.size.x = -rect.size.x
if td.flip_v:
flipped_rect.size.y = -rect.size.y
RenderingServer.canvas_item_add_texture_rect_region(
_get_canvas_item(td),
flipped_rect,
texture.get_rid(),
src_rect,
td.modulate,
td.transpose
)
var type := BetterTerrain.get_tile_terrain_type(td)
if type == BetterTerrain.TileCategory.NON_TERRAIN:
draw_rect(rect, Color(0.1, 0.1, 0.1, 0.5), true)
return
var terrain := BetterTerrain.get_terrain(tileset, type)
if !terrain.valid:
return
var transform := Transform2D(0.0, rect.size, 0.0, rect.position)
var center_polygon = transform * BetterTerrain.data.peering_polygon(tileset, terrain.type, -1)
draw_colored_polygon(center_polygon, Color(terrain.color, 0.6))
if terrain.type == BetterTerrain.TerrainType.DECORATION:
center_polygon.append(center_polygon[0])
draw_polyline(center_polygon, Color.BLACK)
if paint < BetterTerrain.TileCategory.EMPTY or paint >= BetterTerrain.terrain_count(tileset):
return
if not draw_sides:
return
var paint_terrain := BetterTerrain.get_terrain(tileset, paint)
for p in BetterTerrain.data.get_terrain_peering_cells(tileset, terrain.type):
if paint in BetterTerrain.tile_peering_types(td, p):
var side_polygon = transform * BetterTerrain.data.peering_polygon(tileset, terrain.type, p)
draw_colored_polygon(side_polygon, Color(paint_terrain.color, 0.6))
if paint_terrain.type == BetterTerrain.TerrainType.DECORATION:
side_polygon.append(side_polygon[0])
draw_polyline(side_polygon, Color.BLACK)
func _draw_tile_symmetry(texture: Texture2D, rect: Rect2, src_rect: Rect2, td: TileData, draw_icon: bool = true) -> void:
var flipped_rect := rect
if td.flip_h:
flipped_rect.size.x = -rect.size.x
if td.flip_v:
flipped_rect.size.y = -rect.size.y
RenderingServer.canvas_item_add_texture_rect_region(
_get_canvas_item(td),
flipped_rect,
texture.get_rid(),
src_rect,
td.modulate,
td.transpose
)
if not draw_icon:
return
var symmetry_type = BetterTerrain.get_tile_symmetry_type(td)
if symmetry_type == 0:
return
var symmetry_icon = paint_symmetry_icons[symmetry_type]
RenderingServer.canvas_item_add_texture_rect_region(
_get_canvas_item(td),
rect,
symmetry_icon.get_rid(),
Rect2(Vector2.ZERO, symmetry_icon.get_size()),
Color(1,1,1,0.5)
)
func _draw() -> void:
if !tileset:
return
# Clear material-based render targets
RenderingServer.canvas_item_clear(_canvas_item_background)
for p in _canvas_item_map:
RenderingServer.canvas_item_clear(_canvas_item_map[p])
var offset := Vector2.ZERO
var alt_offset := Vector2.RIGHT * (zoom_level * tiles_size.x + ALTERNATE_TILE_MARGIN)
RenderingServer.canvas_item_add_texture_rect(
_canvas_item_background,
Rect2(alt_offset, zoom_level * alternate_size),
checkerboard.get_rid(),
true
)
for s in tileset.get_source_count():
var source_id := tileset.get_source_id(s)
if source_id in disabled_sources:
continue
var source := tileset.get_source(source_id) as TileSetAtlasSource
if !source or !source.texture:
continue
RenderingServer.canvas_item_add_texture_rect(
_canvas_item_background,
Rect2(offset, zoom_level * source.texture.get_size()),
checkerboard.get_rid(),
true
)
for t in source.get_tiles_count():
var coord := source.get_tile_id(t)
var rect := source.get_tile_texture_region(coord, 0)
var alt_count := source.get_alternative_tiles_count(coord)
var target_rect : Rect2
for a in alt_count:
var alt_id := 0
if a == 0:
target_rect = Rect2(offset + zoom_level * rect.position, zoom_level * rect.size)
else:
target_rect = Rect2(alt_offset + zoom_level * (a - 1) * rect.size.x * Vector2.RIGHT, zoom_level * rect.size)
alt_id = source.get_alternative_tile_id(coord, a)
var td := source.get_tile_data(coord, alt_id)
var drawing_current = BetterTerrain.get_tile_terrain_type(td) == paint
if paint_mode == PaintMode.PAINT_SYMMETRY:
_draw_tile_symmetry(source.texture, target_rect, rect, td, drawing_current)
else:
_draw_tile_data(source.texture, target_rect, rect, td)
if drawing_current:
draw_rect(target_rect.grow(-1), Color(0,0,0, 0.75), false, 1)
draw_rect(target_rect, Color(1,1,1, 0.75), false, 1)
if paint_mode == PaintMode.SELECT:
if selected_tile_states.any(func(v):
return v.part.data == td
):
draw_rect(target_rect.grow(-1), Color.DEEP_SKY_BLUE, false, 2)
if alt_count > 1:
alt_offset.y += zoom_level * rect.size.y
# Blank out unused or uninteresting tiles
var size := source.get_atlas_grid_size()
for y in size.y:
for x in size.x:
var pos := Vector2i(x, y)
if !is_tile_in_source(source, pos):
var atlas_pos := source.margins + pos * (source.separation + source.texture_region_size)
draw_rect(Rect2(offset + zoom_level * atlas_pos, zoom_level * source.texture_region_size), Color(0.0, 0.0, 0.0, 0.8), true)
offset.y += zoom_level * source.texture.get_height()
# Blank out unused alternate tile sections
alt_offset = Vector2.RIGHT * (zoom_level * tiles_size.x + ALTERNATE_TILE_MARGIN)
for a in alternate_lookup:
if a[1] in disabled_sources:
continue
var source := tileset.get_source(a[1]) as TileSetAtlasSource
if source:
var count := source.get_alternative_tiles_count(a[2]) - 1
var occupied_width = count * zoom_level * a[0].x
var area := Rect2(
alt_offset.x + occupied_width,
alt_offset.y,
zoom_level * alternate_size.x - occupied_width,
zoom_level * a[0].y
)
draw_rect(area, Color(0.0, 0.0, 0.0, 0.8), true)
alt_offset.y += zoom_level * a[0].y
if highlighted_tile_part.valid:
if paint_mode == PaintMode.PAINT_PEERING and highlighted_tile_part.has("polygon"):
var transform := Transform2D(0.0, highlighted_tile_part.rect.size - 2 * Vector2.ONE, 0.0, highlighted_tile_part.rect.position + Vector2.ONE)
draw_colored_polygon(transform * highlighted_tile_part.polygon, Color(Color.WHITE, 0.2))
if paint_mode != PaintMode.NO_PAINT:
var inner_rect := Rect2(highlighted_tile_part.rect.position + Vector2.ONE, highlighted_tile_part.rect.size - 2 * Vector2.ONE)
draw_rect(inner_rect, Color.WHITE, false)
if paint_mode == PaintMode.PAINT_SYMMETRY:
if paint_symmetry > 0:
var symmetry_icon = paint_symmetry_icons[paint_symmetry]
draw_texture_rect(symmetry_icon, highlighted_tile_part.rect, false, Color(0.5,0.75,1,0.5))
if paint_mode == PaintMode.SELECT:
draw_rect(selection_rect, Color.WHITE, false)
if paint_mode == PaintMode.PASTE:
if staged_paste_tile_states.size() > 0:
var base_rect = staged_paste_tile_states[0].base_rect
var paint_terrain := BetterTerrain.get_terrain(tileset, paint)
var paint_terrain_type = paint_terrain.type
if paint_terrain_type == BetterTerrain.TerrainType.CATEGORY:
paint_terrain_type = 0
for state in staged_paste_tile_states:
var staged_rect:Rect2 = state.base_rect
staged_rect.position -= base_rect.position + base_rect.size / 2
staged_rect.position *= zoom_level
staged_rect.size *= zoom_level
staged_rect.position += Vector2(current_position)
var real_rect = tile_rect_from_position(staged_rect.get_center())
if real_rect.position.x >= 0:
draw_rect(real_rect, Color(0,0,0, 0.3), true)
var transform := Transform2D(0.0, real_rect.size, 0.0, real_rect.position)
var tile_sides = BetterTerrain.data.get_terrain_peering_cells(tileset, paint_terrain_type)
for p in tile_sides:
if state.paint in BetterTerrain.tile_peering_types(state.part.data, p):
var side_polygon = BetterTerrain.data.peering_polygon(tileset, paint_terrain_type, p)
var color = Color(paint_terrain.color, 0.6)
draw_colored_polygon(transform * side_polygon, color)
draw_rect(staged_rect, Color.DEEP_PINK, false)
func delete_selection():
undo_manager.create_action("Delete tile terrain peering types", UndoRedo.MERGE_DISABLE, tileset)
for t in selected_tile_states:
for side in range(16):
var old_peering = BetterTerrain.tile_peering_types(t.part.data, side)
if old_peering.has(paint):
undo_manager.add_do_method(BetterTerrain, &"remove_tile_peering_type", tileset, t.part.data, side, paint)
undo_manager.add_undo_method(BetterTerrain, &"add_tile_peering_type", tileset, t.part.data, side, paint)
undo_manager.add_do_method(self, &"queue_redraw")
undo_manager.add_undo_method(self, &"queue_redraw")
undo_manager.commit_action()
func toggle_selection():
undo_manager.create_action("Toggle tile terrain", UndoRedo.MERGE_DISABLE, tileset, true)
for t in selected_tile_states:
var type := BetterTerrain.get_tile_terrain_type(t.part.data)
var goal := paint if paint != type else BetterTerrain.TileCategory.NON_TERRAIN
terrain_undo.add_do_method(undo_manager, BetterTerrain, &"set_tile_terrain_type", [tileset, t.part.data, goal])
if goal == BetterTerrain.TileCategory.NON_TERRAIN:
terrain_undo.create_peering_restore_point_tile(
undo_manager,
tileset,
t.part.source_id,
t.part.coord,
t.part.alternate
)
else:
undo_manager.add_undo_method(BetterTerrain, &"set_tile_terrain_type", tileset, t.part.data, type)
terrain_undo.add_do_method(undo_manager, self, &"queue_redraw", [])
undo_manager.add_undo_method(self, &"queue_redraw")
undo_manager.commit_action()
terrain_undo.action_count += 1
func copy_selection():
copied_tile_states = selected_tile_states
func paste_selection():
staged_paste_tile_states = copied_tile_states
selected_tile_states = []
paint_mode = PaintMode.PASTE
paint_action = PaintAction.PASTE
paste_occurred.emit()
queue_redraw()
func set_disabled_sources(list):
disabled_sources = list
queue_redraw()
func emit_terrain_updated(index):
terrain_updated.emit(index)
func _gui_input(event) -> void:
if event is InputEventKey and event.is_pressed():
if event.keycode == KEY_DELETE and not event.echo:
accept_event()
delete_selection()
if event.keycode == KEY_ENTER and not event.echo:
accept_event()
toggle_selection()
if event.keycode == KEY_ESCAPE and not event.echo:
accept_event()
if paint_action == PaintAction.PASTE:
staged_paste_tile_states = []
paint_mode = PaintMode.SELECT
paint_action = PaintAction.NO_ACTION
selection_start = Vector2i(-1,-1)
if event.keycode == KEY_C and (event.ctrl_pressed or event.meta_pressed) and not event.echo:
accept_event()
copy_selection()
if event.keycode == KEY_X and (event.ctrl_pressed or event.meta_pressed) and not event.echo:
accept_event()
copy_selection()
delete_selection()
if event.keycode == KEY_V and (event.ctrl_pressed or event.meta_pressed) and not event.echo:
accept_event()
paste_selection()
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_WHEEL_UP and (event.ctrl_pressed or event.meta_pressed):
accept_event()
change_zoom_level.emit(zoom_level * 1.1)
if event.button_index == MOUSE_BUTTON_WHEEL_DOWN and (event.ctrl_pressed or event.meta_pressed):
accept_event()
change_zoom_level.emit(zoom_level / 1.1)
var released : bool = event is InputEventMouseButton and (not event.pressed and (event.button_index == MOUSE_BUTTON_LEFT or event.button_index == MOUSE_BUTTON_RIGHT))
if released:
paint_action = PaintAction.NO_ACTION
if event is InputEventMouseMotion:
prev_position = current_position
current_position = event.position
var tile := tile_part_from_position(event.position)
if tile.valid != highlighted_tile_part.valid or\
(tile.valid and tile.data != highlighted_tile_part.data) or\
(tile.valid and tile.get("peering") != highlighted_tile_part.get("peering")) or\
event.button_mask & MOUSE_BUTTON_LEFT and paint_action == PaintAction.SELECT:
queue_redraw()
highlighted_tile_part = tile
var clicked : bool = event is InputEventMouseButton and (event.pressed and (event.button_index == MOUSE_BUTTON_LEFT or event.button_index == MOUSE_BUTTON_RIGHT))
if clicked:
initial_click = current_position
selection_start = Vector2i(-1,-1)
terrain_undo.action_index += 1
terrain_undo.action_count = 0
if released:
terrain_undo.finish_action()
selection_rect = Rect2i(0,0,0,0)
queue_redraw()
if paint_action == PaintAction.PASTE:
if event is InputEventMouseMotion:
queue_redraw()
if clicked:
if event.button_index == MOUSE_BUTTON_LEFT and staged_paste_tile_states.size() > 0:
undo_manager.create_action("Paste tile terrain peering types", UndoRedo.MERGE_DISABLE, tileset)
var base_rect = staged_paste_tile_states[0].base_rect
for p in staged_paste_tile_states:
var staged_rect:Rect2 = p.base_rect
staged_rect.position -= base_rect.position + base_rect.size / 2
staged_rect.position *= zoom_level
staged_rect.size *= zoom_level
staged_rect.position += Vector2(current_position)
var old_tile_part = tile_part_from_position(staged_rect.get_center())
var new_tile_state = p
if (not old_tile_part.valid) or (not new_tile_state.part.valid):
continue
for side in range(16):
var old_peering = BetterTerrain.tile_peering_types(old_tile_part.data, side)
var new_sides = new_tile_state.sides
if new_sides.has(side) and not old_peering.has(paint):
undo_manager.add_do_method(BetterTerrain, &"add_tile_peering_type", tileset, old_tile_part.data, side, paint)
undo_manager.add_undo_method(BetterTerrain, &"remove_tile_peering_type", tileset, old_tile_part.data, side, paint)
elif old_peering.has(paint) and not new_sides.has(side):
undo_manager.add_do_method(BetterTerrain, &"remove_tile_peering_type", tileset, old_tile_part.data, side, paint)
undo_manager.add_undo_method(BetterTerrain, &"add_tile_peering_type", tileset, old_tile_part.data, side, paint)
var old_symmetry = BetterTerrain.get_tile_symmetry_type(old_tile_part.data)
var new_symmetry = new_tile_state.symmetry
if new_symmetry != old_symmetry:
undo_manager.add_do_method(BetterTerrain, &"set_tile_symmetry_type", tileset, old_tile_part.data, new_symmetry)
undo_manager.add_undo_method(BetterTerrain, &"set_tile_symmetry_type", tileset, old_tile_part.data, old_symmetry)
undo_manager.add_do_method(self, &"queue_redraw")
undo_manager.add_undo_method(self, &"queue_redraw")
undo_manager.commit_action()
staged_paste_tile_states = []
paint_mode = PaintMode.SELECT
paint_action = PaintAction.SELECT
return
if clicked and pick_icon_terrain >= 0:
highlighted_tile_part = tile_part_from_position(current_position)
if !highlighted_tile_part.valid:
return
var t = BetterTerrain.get_terrain(tileset, paint)
var prev_icon = t.icon.duplicate()
var icon = {
source_id = highlighted_tile_part.source_id,
coord = highlighted_tile_part.coord
}
undo_manager.create_action("Edit terrain details", UndoRedo.MERGE_DISABLE, tileset)
undo_manager.add_do_method(BetterTerrain, &"set_terrain", tileset, paint, t.name, t.color, t.type, t.categories, icon)
undo_manager.add_do_method(self, &"emit_terrain_updated", paint)
undo_manager.add_undo_method(BetterTerrain, &"set_terrain", tileset, paint, t.name, t.color, t.type, t.categories, prev_icon)
undo_manager.add_undo_method(self, &"emit_terrain_updated", paint)
undo_manager.commit_action()
pick_icon_terrain = -1
return
if pick_icon_terrain_cancel:
pick_icon_terrain = -1
pick_icon_terrain_cancel = false
if paint != BetterTerrain.TileCategory.NON_TERRAIN and clicked:
paint_action = PaintAction.NO_ACTION
if highlighted_tile_part.valid:
match [paint_mode, event.button_index]:
[PaintMode.PAINT_TYPE, MOUSE_BUTTON_LEFT]: paint_action = PaintAction.DRAW_TYPE
[PaintMode.PAINT_TYPE, MOUSE_BUTTON_RIGHT]: paint_action = PaintAction.ERASE_TYPE
[PaintMode.PAINT_PEERING, MOUSE_BUTTON_LEFT]: paint_action = PaintAction.DRAW_PEERING
[PaintMode.PAINT_PEERING, MOUSE_BUTTON_RIGHT]: paint_action = PaintAction.ERASE_PEERING
[PaintMode.PAINT_SYMMETRY, MOUSE_BUTTON_LEFT]: paint_action = PaintAction.DRAW_SYMMETRY
[PaintMode.PAINT_SYMMETRY, MOUSE_BUTTON_RIGHT]: paint_action = PaintAction.ERASE_SYMMETRY
[PaintMode.SELECT, MOUSE_BUTTON_LEFT]: paint_action = PaintAction.SELECT
else:
match [paint_mode, event.button_index]:
[PaintMode.SELECT, MOUSE_BUTTON_LEFT]: paint_action = PaintAction.SELECT
if (clicked or event is InputEventMouseMotion) and paint_action != PaintAction.NO_ACTION:
if paint_action == PaintAction.SELECT:
if clicked:
selection_start = Vector2i(-1,-1)
queue_redraw()
if selection_start.x < 0:
selection_start = current_position
selection_end = current_position
selection_rect = Rect2i(selection_start, selection_end - selection_start).abs()
var selected_tile_parts = tile_parts_from_rect(selection_rect)
selected_tile_states = []
for t in selected_tile_parts:
var state := {
part = t,
base_rect = Rect2(t.rect.position / zoom_level, t.rect.size / zoom_level),
paint = paint,
sides = BetterTerrain.tile_peering_for_type(t.data, paint),
symmetry = BetterTerrain.get_tile_symmetry_type(t.data)
}
selected_tile_states.push_back(state)
else:
if !highlighted_tile_part.valid:
return
#slightly crude and non-optimal but way simpler than the "correct" solution
var current_position_vec2 = Vector2(current_position)
var prev_position_vec2 = Vector2(prev_position)
var mouse_dist = current_position_vec2.distance_to(prev_position_vec2)
var step_size = (tile_part_size.x * zoom_level)
var steps = ceil(mouse_dist / step_size) + 1
for i in range(steps):
var t = float(i) / steps
var check_position = prev_position_vec2.lerp(current_position_vec2, t)
highlighted_tile_part = tile_part_from_position(check_position)
if !highlighted_tile_part.valid:
continue
if paint_action == PaintAction.DRAW_TYPE or paint_action == PaintAction.ERASE_TYPE:
var type := BetterTerrain.get_tile_terrain_type(highlighted_tile_part.data)
var goal := paint if paint_action == PaintAction.DRAW_TYPE else BetterTerrain.TileCategory.NON_TERRAIN
if type != goal:
undo_manager.create_action("Set tile terrain type " + str(terrain_undo.action_index), UndoRedo.MERGE_ALL, tileset, true)
terrain_undo.add_do_method(undo_manager, BetterTerrain, &"set_tile_terrain_type", [tileset, highlighted_tile_part.data, goal])
terrain_undo.add_do_method(undo_manager, self, &"queue_redraw", [])
if goal == BetterTerrain.TileCategory.NON_TERRAIN:
terrain_undo.create_peering_restore_point_tile(
undo_manager,
tileset,
highlighted_tile_part.source_id,
highlighted_tile_part.coord,
highlighted_tile_part.alternate
)
else:
undo_manager.add_undo_method(BetterTerrain, &"set_tile_terrain_type", tileset, highlighted_tile_part.data, type)
undo_manager.add_undo_method(self, &"queue_redraw")
undo_manager.commit_action()
terrain_undo.action_count += 1
elif paint_action == PaintAction.DRAW_PEERING:
if highlighted_tile_part.has("peering"):
if !(paint in BetterTerrain.tile_peering_types(highlighted_tile_part.data, highlighted_tile_part.peering)):
undo_manager.create_action("Set tile terrain peering type " + str(terrain_undo.action_index), UndoRedo.MERGE_ALL, tileset, true)
terrain_undo.add_do_method(undo_manager, BetterTerrain, &"add_tile_peering_type", [tileset, highlighted_tile_part.data, highlighted_tile_part.peering, paint])
terrain_undo.add_do_method(undo_manager, self, &"queue_redraw", [])
undo_manager.add_undo_method(BetterTerrain, &"remove_tile_peering_type", tileset, highlighted_tile_part.data, highlighted_tile_part.peering, paint)
undo_manager.add_undo_method(self, &"queue_redraw")
undo_manager.commit_action()
terrain_undo.action_count += 1
elif paint_action == PaintAction.ERASE_PEERING:
if highlighted_tile_part.has("peering"):
if paint in BetterTerrain.tile_peering_types(highlighted_tile_part.data, highlighted_tile_part.peering):
undo_manager.create_action("Remove tile terrain peering type " + str(terrain_undo.action_index), UndoRedo.MERGE_ALL, tileset, true)
terrain_undo.add_do_method(undo_manager, BetterTerrain, &"remove_tile_peering_type", [tileset, highlighted_tile_part.data, highlighted_tile_part.peering, paint])
terrain_undo.add_do_method(undo_manager, self, &"queue_redraw", [])
undo_manager.add_undo_method(BetterTerrain, &"add_tile_peering_type", tileset, highlighted_tile_part.data, highlighted_tile_part.peering, paint)
undo_manager.add_undo_method(self, &"queue_redraw")
undo_manager.commit_action()
terrain_undo.action_count += 1
elif paint_action == PaintAction.DRAW_SYMMETRY:
if paint == BetterTerrain.get_tile_terrain_type(highlighted_tile_part.data):
undo_manager.create_action("Set tile symmetry type " + str(terrain_undo.action_index), UndoRedo.MERGE_ALL, tileset, true)
var old_symmetry = BetterTerrain.get_tile_symmetry_type(highlighted_tile_part.data)
terrain_undo.add_do_method(undo_manager, BetterTerrain, &"set_tile_symmetry_type", [tileset, highlighted_tile_part.data, paint_symmetry])
terrain_undo.add_do_method(undo_manager, self, &"queue_redraw", [])
undo_manager.add_undo_method(BetterTerrain, &"set_tile_symmetry_type", tileset, highlighted_tile_part.data, old_symmetry)
undo_manager.add_undo_method(self, &"queue_redraw")
undo_manager.commit_action()
terrain_undo.action_count += 1
elif paint_action == PaintAction.ERASE_SYMMETRY:
if paint == BetterTerrain.get_tile_terrain_type(highlighted_tile_part.data):
undo_manager.create_action("Remove tile symmetry type " + str(terrain_undo.action_index), UndoRedo.MERGE_ALL, tileset, true)
var old_symmetry = BetterTerrain.get_tile_symmetry_type(highlighted_tile_part.data)
terrain_undo.add_do_method(undo_manager, BetterTerrain, &"set_tile_symmetry_type", [tileset, highlighted_tile_part.data, BetterTerrain.SymmetryType.NONE])
terrain_undo.add_do_method(undo_manager, self, &"queue_redraw", [])
undo_manager.add_undo_method(BetterTerrain, &"set_tile_symmetry_type", tileset, highlighted_tile_part.data, old_symmetry)
undo_manager.add_undo_method(self, &"queue_redraw")
undo_manager.commit_action()
terrain_undo.action_count += 1
func _on_zoom_value_changed(value) -> void:
zoom_level = value
custom_minimum_size.x = zoom_level * tiles_size.x
if alternate_size.x > 0:
custom_minimum_size.x += ALTERNATE_TILE_MARGIN + zoom_level * alternate_size.x
custom_minimum_size.y = zoom_level * max(tiles_size.y, alternate_size.y)
queue_redraw()
func clear_highlighted_tile() -> void:
highlighted_tile_part = { valid = false }
queue_redraw()

View File

@@ -0,0 +1 @@
uid://cpm7dq6r0n0sn