Add in-game editor MVP: EditorCore + 2D tile/object editing
Implements the in-game editor's shared, data-native core and a Control-based UI on top of it, per docs/architecture/editor.md §7.1/§7.2, scoped to 2D per §8's own risk mitigation (the default world is flat, CHUNK_SIZE_Z=1 -- no 3D gizmos here). EditorCore (scripts/editor/): - ChunkEditorModel wraps a ChunkDataDecoder.ChunkPayload directly (no parallel data model) and adds a TileLayerData.set_tile() mutator alongside the existing get_tile(). - EditorUndoRedoStack wraps Godot's built-in UndoRedo (game-usable, not the editor-only EditorUndoRedoManager) so every mutation is a real, revertible do/undo Callable pair over ChunkEditorModel's raw setters. - SelectionManager tracks the selected object id as UI state, deliberately outside the undo/redo stack. - ToolRegistry + 5 tools (BrushTool, ObjectPlaceTool, SelectTool, EraseTool, TransformTool -- the 5th covers move/rotate/scale, which the doc's tool list doesn't literally name but the task requires) is the single choke point every mutation goes through. - EditorObjectCatalog (assets/editor/object_catalog.json) is a new, small "placeable object" catalog -- AvatarCatalog and SpawnCatalog were checked first and are both scoped to other concerns, so this mirrors their loading convention rather than reusing either. InGameEditor UI (scripts/editor/ui/): EditorViewport (2D top-down tile grid + object dots), EditorToolbar, PalettePanel, InspectorPanel, wired together in InGameEditor. Built from Control nodes in code (matching game.gd's existing dynamic-UI convention) rather than a hand-authored .tscn. Entry point: game.gd gates build mode behind a new toggle_build_mode input action (F2), freezing the player and opening a fresh empty ChunkEditorModel. Loading a *real* chunk from the backend for editing, and saving edits back, are explicitly out of scope -- a sibling task owns the batch-PUT/conflict-handling save path (world_persistence_impl.md). Verification: scripts/smoke_test_editor_core.gd decodes a real backend fixture (backend/tests/fixtures/chunks/simple_tile_chunk.bin), drives place/move/rotate/scale/delete/paint/erase entirely through EditorCore.use_tool()/ToolRegistry (never touching ChunkEditorModel's raw setters directly), undoes and redoes every step, and round-trips the result through encode_binary()/decode_binary() to prove the edited model stays a valid, byte-compatible ChunkPayload. 69/69 assertions pass. Full existing Godot smoke suite (16 other scripts/smoke_test_*.gd, backend + dedicated-server-dependent ones included) still passes with no regressions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y2FESCaMXeJpC9gSctiEeD
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
class_name EditorToolbar
|
||||
extends HBoxContainer
|
||||
## Tool-mode switcher for the in-game editor (docs/architecture/editor.md
|
||||
## §7.2 `Toolbar`). Pure UI: it only emits signals: `InGameEditor` decides
|
||||
## what to do with them (calling into `EditorCore`/`ToolRegistry`).
|
||||
|
||||
signal tool_selected(tool_id: String)
|
||||
signal undo_pressed
|
||||
signal redo_pressed
|
||||
|
||||
const TOOL_LABELS := {
|
||||
"select": "Select",
|
||||
"brush": "Brush",
|
||||
"object_place": "Place",
|
||||
"erase": "Erase",
|
||||
}
|
||||
|
||||
var _tool_buttons: Dictionary = {}
|
||||
var _undo_button: Button
|
||||
var _redo_button: Button
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "EditorToolbar"
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
for tool_id in TOOL_LABELS:
|
||||
var btn := Button.new()
|
||||
btn.text = TOOL_LABELS[tool_id]
|
||||
btn.toggle_mode = true
|
||||
btn.pressed.connect(_on_tool_button_pressed.bind(tool_id))
|
||||
add_child(btn)
|
||||
_tool_buttons[tool_id] = btn
|
||||
|
||||
add_child(VSeparator.new())
|
||||
|
||||
_undo_button = Button.new()
|
||||
_undo_button.text = "Undo"
|
||||
_undo_button.pressed.connect(func(): undo_pressed.emit())
|
||||
add_child(_undo_button)
|
||||
|
||||
_redo_button = Button.new()
|
||||
_redo_button.text = "Redo"
|
||||
_redo_button.pressed.connect(func(): redo_pressed.emit())
|
||||
add_child(_redo_button)
|
||||
|
||||
|
||||
func _on_tool_button_pressed(tool_id: String) -> void:
|
||||
set_active_tool(tool_id)
|
||||
tool_selected.emit(tool_id)
|
||||
|
||||
|
||||
## Reflect the active tool in the button states without re-emitting the signal.
|
||||
func set_active_tool(tool_id: String) -> void:
|
||||
for id in _tool_buttons:
|
||||
_tool_buttons[id].button_pressed = (id == tool_id)
|
||||
|
||||
|
||||
## Reflect whether undo/redo currently have anything to do.
|
||||
func set_history_state(can_undo: bool, can_redo: bool) -> void:
|
||||
if _undo_button:
|
||||
_undo_button.disabled = not can_undo
|
||||
if _redo_button:
|
||||
_redo_button.disabled = not can_redo
|
||||
Reference in New Issue
Block a user