164 lines
5.9 KiB
GDScript
164 lines
5.9 KiB
GDScript
@tool
|
|
extends RefCounted
|
|
|
|
## TileMap / TileMapLayer authoring — set, fill, clear, and read tile cells
|
|
## directly in the editor scene with full undo/redo support.
|
|
##
|
|
## All ops target TileMapLayer nodes in the currently edited scene by
|
|
## scene-relative path (e.g. "/LavaLake20x20/Ground").
|
|
|
|
const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
|
|
const MAX_RECT_FILL_CELLS := 4096
|
|
|
|
var _undo_redo: EditorUndoRedoManager
|
|
|
|
|
|
func _init(undo_redo: EditorUndoRedoManager) -> void:
|
|
_undo_redo = undo_redo
|
|
|
|
|
|
## Set a single tile cell.
|
|
## params: {path, source_id, atlas_col, atlas_row, map_x, map_y}
|
|
## Returns: {map_x, map_y, source_id, atlas_col, atlas_row}
|
|
func set_cell(params: Dictionary) -> Dictionary:
|
|
var layer := _resolve_layer(params)
|
|
if layer.has("error"): return layer
|
|
var node: TileMapLayer = layer.node
|
|
var pos := Vector2i(params.get("map_x", 0), params.get("map_y", 0))
|
|
var src := int(params.get("source_id", 0))
|
|
var atlas := Vector2i(params.get("atlas_col", 0), params.get("atlas_row", 0))
|
|
var prev := _capture_cell_state(node, pos)
|
|
_undo_redo.create_action("MCP: TileMap set_cell")
|
|
_undo_redo.add_do_method(node, "set_cell", pos, src, atlas)
|
|
_undo_redo.add_undo_method(self, "_restore_cell_state", node, pos, prev)
|
|
_undo_redo.commit_action()
|
|
return {"data": {"map_x": pos.x, "map_y": pos.y, "source_id": src,
|
|
"atlas_col": atlas.x, "atlas_row": atlas.y, "undoable": true}}
|
|
|
|
|
|
## Fill a rectangular region with one tile type in a single undo action.
|
|
## params: {path, source_id, atlas_col, atlas_row, rect_x, rect_y, rect_w, rect_h}
|
|
## Returns: {cells_filled, rect: {x, y, w, h}}
|
|
func set_cells_rect(params: Dictionary) -> Dictionary:
|
|
var layer := _resolve_layer(params)
|
|
if layer.has("error"): return layer
|
|
var node: TileMapLayer = layer.node
|
|
var src := int(params.get("source_id", 0))
|
|
var atlas := Vector2i(params.get("atlas_col", 0), params.get("atlas_row", 0))
|
|
var rx := int(params.get("rect_x", 0)); var ry := int(params.get("rect_y", 0))
|
|
var rw := int(params.get("rect_w", 1)); var rh := int(params.get("rect_h", 1))
|
|
if rw <= 0 or rh <= 0:
|
|
return ErrorCodes.make(
|
|
ErrorCodes.VALUE_OUT_OF_RANGE,
|
|
"rect_w and rect_h must be > 0 (got %d x %d)" % [rw, rh]
|
|
)
|
|
var cell_count := rw * rh
|
|
if cell_count > MAX_RECT_FILL_CELLS:
|
|
return ErrorCodes.make(
|
|
ErrorCodes.VALUE_OUT_OF_RANGE,
|
|
"Rect too large: %d cells exceeds max %d" % [cell_count, MAX_RECT_FILL_CELLS]
|
|
)
|
|
var cells: Array[Vector2i] = []
|
|
var snapshot: Array[Dictionary] = []
|
|
for x in range(rx, rx + rw):
|
|
for y in range(ry, ry + rh):
|
|
var pos := Vector2i(x, y)
|
|
cells.append(pos)
|
|
snapshot.append({"pos": pos, "state": _capture_cell_state(node, pos)})
|
|
_undo_redo.create_action("MCP: TileMap set_cells_rect %dx%d" % [rw, rh])
|
|
for pos in cells:
|
|
_undo_redo.add_do_method(node, "set_cell", pos, src, atlas)
|
|
_undo_redo.add_undo_method(self, "_restore_rect_snapshot", node, snapshot)
|
|
_undo_redo.commit_action()
|
|
return {"data": {"cells_filled": cells.size(),
|
|
"rect": {"x": rx, "y": ry, "w": rw, "h": rh}, "undoable": true}}
|
|
|
|
|
|
## Remove all tiles from a TileMapLayer.
|
|
## params: {path}
|
|
## Returns: {cleared: true}
|
|
func clear_layer(params: Dictionary) -> Dictionary:
|
|
var layer := _resolve_layer(params)
|
|
if layer.has("error"): return layer
|
|
var node: TileMapLayer = layer.node
|
|
var snapshot := _capture_used_cells_snapshot(node)
|
|
_undo_redo.create_action("MCP: TileMap clear")
|
|
_undo_redo.add_do_method(node, "clear")
|
|
_undo_redo.add_undo_method(self, "_restore_cells_snapshot", node, snapshot)
|
|
_undo_redo.commit_action()
|
|
return {"data": {"cleared": true, "undoable": true}}
|
|
|
|
|
|
## Return all used cell coordinates.
|
|
## params: {path}
|
|
## Returns: {cells: [{x, y}, ...], count: int}
|
|
func get_used_cells(params: Dictionary) -> Dictionary:
|
|
var layer := _resolve_layer(params)
|
|
if layer.has("error"): return layer
|
|
var node: TileMapLayer = layer.node
|
|
var cells := node.get_used_cells()
|
|
var result: Array = []
|
|
for c in cells:
|
|
result.append({"x": c.x, "y": c.y})
|
|
return {"data": {"cells": result, "count": result.size()}}
|
|
|
|
|
|
## Resolve a TileMapLayer node from params["path"] in the currently edited
|
|
## scene. Returns {"node": TileMapLayer} on success, or an error dict.
|
|
func _resolve_layer(params: Dictionary) -> Dictionary:
|
|
var path: String = params.get("path", "")
|
|
var scene_file: String = params.get("scene_file", "")
|
|
var resolved := McpNodeValidator.resolve_or_error(path, "path", scene_file)
|
|
if resolved.has("error"):
|
|
return resolved
|
|
var node: Node = resolved.node
|
|
if not node is TileMapLayer:
|
|
return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
|
|
"Node is not a TileMapLayer: %s" % path)
|
|
return {"node": node}
|
|
|
|
|
|
func _capture_cell_state(node: TileMapLayer, pos: Vector2i) -> Dictionary:
|
|
var source_id := node.get_cell_source_id(pos)
|
|
if source_id == -1:
|
|
return {"has_tile": false}
|
|
var atlas: Vector2i = node.get_cell_atlas_coords(pos)
|
|
var alternative := node.get_cell_alternative_tile(pos)
|
|
return {
|
|
"has_tile": true,
|
|
"source_id": source_id,
|
|
"atlas_col": atlas.x,
|
|
"atlas_row": atlas.y,
|
|
"alternative": alternative,
|
|
}
|
|
|
|
|
|
func _capture_used_cells_snapshot(node: TileMapLayer) -> Array[Dictionary]:
|
|
var snapshot: Array[Dictionary] = []
|
|
for pos in node.get_used_cells():
|
|
snapshot.append({"pos": pos, "state": _capture_cell_state(node, pos)})
|
|
return snapshot
|
|
|
|
|
|
func _restore_cells_snapshot(node: TileMapLayer, snapshot: Array[Dictionary]) -> void:
|
|
node.clear()
|
|
for entry in snapshot:
|
|
_restore_cell_state(node, entry.pos, entry.state)
|
|
|
|
|
|
func _restore_rect_snapshot(node: TileMapLayer, snapshot: Array[Dictionary]) -> void:
|
|
for entry in snapshot:
|
|
_restore_cell_state(node, entry.pos, entry.state)
|
|
|
|
|
|
func _restore_cell_state(node: TileMapLayer, pos: Vector2i, state: Dictionary) -> void:
|
|
if not state.get("has_tile", false):
|
|
node.erase_cell(pos)
|
|
return
|
|
node.set_cell(
|
|
pos,
|
|
int(state.get("source_id", -1)),
|
|
Vector2i(int(state.get("atlas_col", -1)), int(state.get("atlas_row", -1))),
|
|
int(state.get("alternative", 0))
|
|
)
|