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:
+61
-2
@@ -28,6 +28,14 @@ var _remotes: Dictionary = {} # peer_id -> Node3D
|
||||
var _remote_prev: Dictionary = {} # peer_id -> Vector3 (last snapshot position)
|
||||
var _status_label: Label = null
|
||||
|
||||
## Build-mode UI, gated behind toggle_build_mode (F2) -- only ever
|
||||
## instantiated once the player actually enters build mode
|
||||
## (docs/architecture/editor.md §7.2: "Instantiated only when the player
|
||||
## enters 'build mode'... so the runtime footprint is negligible during
|
||||
## normal play"). Editing is in-memory only here; the save path to the
|
||||
## backend is a sibling task (world_persistence_impl.md).
|
||||
var _build_editor: InGameEditor = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_show_build_stamp()
|
||||
@@ -36,13 +44,64 @@ func _ready() -> void:
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
# ESC leaves the world back to the browser. Handled here (the scene root
|
||||
if event.is_action_pressed("toggle_build_mode"):
|
||||
get_viewport().set_input_as_handled()
|
||||
_toggle_build_mode()
|
||||
return
|
||||
|
||||
# ESC leaves the world back to the browser (or, while build mode is
|
||||
# open, just closes build mode instead). Handled here (the scene root
|
||||
# that owns the connection) rather than in player.gd so the teardown is
|
||||
# a single ordered path -- doing it from two places raced _on_server_lost
|
||||
# against the scene change and looked like a crash.
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
get_viewport().set_input_as_handled()
|
||||
_leave_world()
|
||||
if _build_editor != null and is_instance_valid(_build_editor):
|
||||
_close_build_mode()
|
||||
else:
|
||||
_leave_world()
|
||||
|
||||
|
||||
## Entry point into the in-game editor (docs/architecture/editor.md §7.2).
|
||||
## Freezes the player and opens a fresh, empty in-memory chunk to edit --
|
||||
## loading a *real* chunk from the backend for editing is the sibling
|
||||
## save-path task's territory (it owns the HTTP round-trip); this task's
|
||||
## contract stops at "a correctly-editable in-memory ChunkEditorModel".
|
||||
func _toggle_build_mode() -> void:
|
||||
if _build_editor != null and is_instance_valid(_build_editor):
|
||||
_close_build_mode()
|
||||
else:
|
||||
_open_build_mode()
|
||||
|
||||
|
||||
func _open_build_mode() -> void:
|
||||
var model := ChunkEditorModel.create_empty(32, 32, str(Session.world_id), "main")
|
||||
var catalog := EditorObjectCatalog.new()
|
||||
var err := catalog.load_catalog()
|
||||
if err != OK:
|
||||
push_error("game.gd: failed to load editor object catalog (%d)" % err)
|
||||
var core := EditorCore.new(model, catalog)
|
||||
|
||||
_build_editor = InGameEditor.new(core)
|
||||
_build_editor.closed.connect(_close_build_mode)
|
||||
add_child(_build_editor)
|
||||
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
_player.set_physics_process(false)
|
||||
_player.set_process_unhandled_input(false)
|
||||
|
||||
|
||||
## Shared teardown for both close paths (F2 again, or the editor's own
|
||||
## Close button/`closed` signal) -- safe to call twice: `queue_free()` on an
|
||||
## already-freeing node and restoring already-restored player state are
|
||||
## both no-ops.
|
||||
func _close_build_mode() -> void:
|
||||
if _build_editor != null and is_instance_valid(_build_editor):
|
||||
_build_editor.queue_free()
|
||||
_build_editor = null
|
||||
_player.set_physics_process(true)
|
||||
_player.set_process_unhandled_input(true)
|
||||
_player._refresh_mouse_mode()
|
||||
|
||||
|
||||
func _leave_world() -> void:
|
||||
|
||||
Reference in New Issue
Block a user