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,62 @@
|
||||
class_name TransformTool
|
||||
extends EditorTool
|
||||
## Moves, rotates, or scales an existing object.
|
||||
##
|
||||
## Not one of the four tools literally named in docs/architecture/editor.md
|
||||
## §7.1's `ToolRegistry` list ("brush / object-place / select / erase,
|
||||
## etc.") but required by this task's brief ("place / move / rotate / scale
|
||||
## / delete") -- the doc's own "etc." leaves room, so this is a fifth
|
||||
## registered tool rather than overloading `ObjectPlaceTool` (placing a new
|
||||
## object and re-transforming an existing one are different undo units)
|
||||
## or `SelectTool` (selection is deliberately not chunk-mutating).
|
||||
##
|
||||
## `args`: `object_id` (String), `op` ("move" | "rotate" | "scale"), `value`
|
||||
## (Vector3 for move/scale, Vector4 quaternion for rotate).
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
tool_id = "transform"
|
||||
|
||||
|
||||
func apply(core: EditorCore, args: Dictionary) -> Variant:
|
||||
var object_id: String = args.get("object_id", "")
|
||||
var op: String = args.get("op", "")
|
||||
var obj := core.model.find_object(object_id)
|
||||
if obj == null:
|
||||
return false
|
||||
|
||||
match op:
|
||||
"move":
|
||||
return _apply_vector3(core, object_id, args.get("value", obj.position),
|
||||
obj.position, "Move", core.model.set_object_position_raw)
|
||||
"rotate":
|
||||
return _apply_vector4(core, object_id, args.get("value", obj.rotation),
|
||||
obj.rotation, "Rotate", core.model.set_object_rotation_raw)
|
||||
"scale":
|
||||
return _apply_vector3(core, object_id, args.get("value", obj.scale),
|
||||
obj.scale, "Scale", core.model.set_object_scale_raw)
|
||||
_:
|
||||
push_error("TransformTool: unknown op '%s'" % op)
|
||||
return false
|
||||
|
||||
|
||||
func _apply_vector3(core: EditorCore, object_id: String, new_value: Vector3,
|
||||
old_value: Vector3, label: String, setter: Callable) -> bool:
|
||||
if new_value.is_equal_approx(old_value):
|
||||
return false
|
||||
core.undo_redo.begin("%s '%s'" % [label, object_id])
|
||||
core.undo_redo.add_do(setter.bind(object_id, new_value))
|
||||
core.undo_redo.add_undo(setter.bind(object_id, old_value))
|
||||
core.undo_redo.commit()
|
||||
return true
|
||||
|
||||
|
||||
func _apply_vector4(core: EditorCore, object_id: String, new_value: Vector4,
|
||||
old_value: Vector4, label: String, setter: Callable) -> bool:
|
||||
if new_value.is_equal_approx(old_value):
|
||||
return false
|
||||
core.undo_redo.begin("%s '%s'" % [label, object_id])
|
||||
core.undo_redo.add_do(setter.bind(object_id, new_value))
|
||||
core.undo_redo.add_undo(setter.bind(object_id, old_value))
|
||||
core.undo_redo.commit()
|
||||
return true
|
||||
Reference in New Issue
Block a user