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,73 @@
|
||||
class_name EditorUndoRedoStack
|
||||
extends RefCounted
|
||||
## Command pattern over ChunkEditorModel mutations
|
||||
## (docs/architecture/editor.md §7.1 `UndoRedoStack`).
|
||||
##
|
||||
## Thin wrapper around Godot's built-in `UndoRedo` -- the general-purpose
|
||||
## class meant for use in game code (unlike the editor-only
|
||||
## `EditorUndoRedoManager`, which only exists inside `EditorPlugin`s and
|
||||
## isn't available at runtime). Every EditorCore tool (scripts/editor/tools/
|
||||
## *.gd) opens exactly one action per user operation and adds a do/undo
|
||||
## Callable pair that calls the *same* ChunkEditorModel raw setter in both
|
||||
## directions (e.g. `set_tile_raw` with the new id, then again with the old
|
||||
## one) -- so undo genuinely reverts the mutation instead of re-deriving it.
|
||||
##
|
||||
## `UndoRedo` extends `Object`, not `RefCounted`, so it is not automatically
|
||||
## freed; `_notification(NOTIFICATION_PREDELETE)` frees it when this wrapper
|
||||
## (a RefCounted) goes out of scope.
|
||||
|
||||
var _ur := UndoRedo.new()
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_PREDELETE and _ur != null:
|
||||
_ur.free()
|
||||
_ur = null
|
||||
|
||||
|
||||
## Begin a new undoable action. Must be paired with `commit()`.
|
||||
func begin(action_name: String) -> void:
|
||||
_ur.create_action(action_name)
|
||||
|
||||
|
||||
## Register the forward step of the current action. Called immediately by
|
||||
## `commit()`, and again on every future `redo()`.
|
||||
func add_do(callable: Callable) -> void:
|
||||
_ur.add_do_method(callable)
|
||||
|
||||
|
||||
## Register the reverse step of the current action. Called on every
|
||||
## `undo()`.
|
||||
func add_undo(callable: Callable) -> void:
|
||||
_ur.add_undo_method(callable)
|
||||
|
||||
|
||||
## Close the current action and execute its do-step once.
|
||||
func commit() -> void:
|
||||
_ur.commit_action()
|
||||
|
||||
|
||||
## Close the current action without executing anything (used when a tool
|
||||
## decides mid-way that there is nothing worth recording).
|
||||
func cancel() -> void:
|
||||
_ur.commit_action(false)
|
||||
|
||||
|
||||
func undo() -> bool:
|
||||
return _ur.undo()
|
||||
|
||||
|
||||
func redo() -> bool:
|
||||
return _ur.redo()
|
||||
|
||||
|
||||
func can_undo() -> bool:
|
||||
return _ur.has_undo()
|
||||
|
||||
|
||||
func can_redo() -> bool:
|
||||
return _ur.has_redo()
|
||||
|
||||
|
||||
func clear_history() -> void:
|
||||
_ur.clear_history()
|
||||
Reference in New Issue
Block a user