fix(editor): move build stamp to bottom, expand palette list, add hover feedback and instructions
checkpoint / checkpoint (push) Successful in 6m5s

This commit is contained in:
2026-09-16 12:21:39 +00:00
parent 42520b33d5
commit 73172b1e28
6 changed files with 195 additions and 28 deletions
+24 -10
View File
@@ -8,11 +8,11 @@ signal tool_selected(tool_id: String)
signal undo_pressed signal undo_pressed
signal redo_pressed signal redo_pressed
const TOOL_LABELS := { const TOOL_CONFIG := {
"select": "Select", "brush": {"label": "Brush (B)", "tooltip": "Paint solid ground tiles (drag to paint; 0-9 for tile types)"},
"brush": "Brush", "select": {"label": "Select (V)", "tooltip": "Select and inspect placed objects"},
"object_place": "Place", "object_place": {"label": "Place (P)", "tooltip": "Place objects from the Palette onto the grid"},
"erase": "Erase", "erase": {"label": "Erase (X)", "tooltip": "Erase tiles or objects (drag to erase)"},
} }
var _tool_buttons: Dictionary = {} var _tool_buttons: Dictionary = {}
@@ -25,9 +25,11 @@ func _init() -> void:
func _ready() -> void: func _ready() -> void:
for tool_id in TOOL_LABELS: for tool_id in TOOL_CONFIG:
var btn := Button.new() var btn := Button.new()
btn.text = TOOL_LABELS[tool_id] var cfg: Dictionary = TOOL_CONFIG[tool_id]
btn.text = cfg["label"]
btn.tooltip_text = cfg["tooltip"]
btn.toggle_mode = true btn.toggle_mode = true
btn.pressed.connect(_on_tool_button_pressed.bind(tool_id)) btn.pressed.connect(_on_tool_button_pressed.bind(tool_id))
add_child(btn) add_child(btn)
@@ -36,12 +38,16 @@ func _ready() -> void:
add_child(VSeparator.new()) add_child(VSeparator.new())
_undo_button = Button.new() _undo_button = Button.new()
_undo_button.text = "Undo" _undo_button.text = "Undo (Ctrl+Z)"
_undo_button.tooltip_text = "Undo last action"
_undo_button.disabled = true
_undo_button.pressed.connect(func(): undo_pressed.emit()) _undo_button.pressed.connect(func(): undo_pressed.emit())
add_child(_undo_button) add_child(_undo_button)
_redo_button = Button.new() _redo_button = Button.new()
_redo_button.text = "Redo" _redo_button.text = "Redo (Ctrl+Y)"
_redo_button.tooltip_text = "Redo last undone action"
_redo_button.disabled = true
_redo_button.pressed.connect(func(): redo_pressed.emit()) _redo_button.pressed.connect(func(): redo_pressed.emit())
add_child(_redo_button) add_child(_redo_button)
@@ -54,7 +60,15 @@ func _on_tool_button_pressed(tool_id: String) -> void:
## Reflect the active tool in the button states without re-emitting the signal. ## Reflect the active tool in the button states without re-emitting the signal.
func set_active_tool(tool_id: String) -> void: func set_active_tool(tool_id: String) -> void:
for id in _tool_buttons: for id in _tool_buttons:
_tool_buttons[id].button_pressed = (id == tool_id) var btn: Button = _tool_buttons[id]
var is_active: bool = (str(id) == tool_id)
btn.button_pressed = is_active
if is_active:
btn.add_theme_color_override("font_color", Color(1.0, 0.95, 0.3))
btn.add_theme_color_override("font_pressed_color", Color(1.0, 0.95, 0.3))
else:
btn.remove_theme_color_override("font_color")
btn.remove_theme_color_override("font_pressed_color")
## Reflect whether undo/redo currently have anything to do. ## Reflect whether undo/redo currently have anything to do.
+60 -2
View File
@@ -26,6 +26,12 @@ var pending_catalog_id: String = ""
## Tile id the next "brush" click paints. Cycled with number keys 0-9. ## Tile id the next "brush" click paints. Cycled with number keys 0-9.
var current_tile_id: int = 1 var current_tile_id: int = 1
var _hover_tx: int = -1
var _hover_ty: int = -1
var _mouse_down: bool = false
var _last_drag_tx: int = -1
var _last_drag_ty: int = -1
const TILE_EMPTY_COLOR := Color(0.15, 0.15, 0.17) const TILE_EMPTY_COLOR := Color(0.15, 0.15, 0.17)
const TILE_SOLID_COLOR := Color(0.35, 0.55, 0.35) const TILE_SOLID_COLOR := Color(0.35, 0.55, 0.35)
const GRID_COLOR := Color(0, 0, 0, 0.25) const GRID_COLOR := Color(0, 0, 0, 0.25)
@@ -39,6 +45,16 @@ func _init() -> void:
focus_mode = Control.FOCUS_ALL focus_mode = Control.FOCUS_ALL
func _notification(what: int) -> void:
if what == NOTIFICATION_MOUSE_EXIT:
_hover_tx = -1
_hover_ty = -1
_mouse_down = false
_last_drag_tx = -1
_last_drag_ty = -1
queue_redraw()
func setup(p_core: EditorCore, p_layer_idx: int = 0) -> void: func setup(p_core: EditorCore, p_layer_idx: int = 0) -> void:
core = p_core core = p_core
layer_idx = p_layer_idx layer_idx = p_layer_idx
@@ -76,11 +92,32 @@ func _draw() -> void:
for y in range(layer.height + 1): for y in range(layer.height + 1):
draw_line(Vector2(0, y * cell_size), Vector2(layer.width * cell_size, y * cell_size), GRID_COLOR) draw_line(Vector2(0, y * cell_size), Vector2(layer.width * cell_size, y * cell_size), GRID_COLOR)
# Hover highlight over current cell
if _hover_tx >= 0 and _hover_tx < layer.width and _hover_ty >= 0 and _hover_ty < layer.height:
var h_rect := Rect2(_hover_tx * cell_size, _hover_ty * cell_size, cell_size, cell_size)
draw_rect(h_rect, Color(1.0, 1.0, 1.0, 0.2), true)
draw_rect(h_rect, Color(1.0, 0.95, 0.3, 0.85), false, 2.0)
if core.tools.get_active_tool_id() == "object_place" and pending_catalog_id != "":
var ghost_pos := Vector2((_hover_tx + 0.5) * cell_size, (_hover_ty + 0.5) * cell_size)
draw_circle(ghost_pos, cell_size * 0.35, Color(1.0, 0.8, 0.2, 0.5))
var default_font := ThemeDB.fallback_font
for obj in core.model.payload.objects: for obj in core.model.payload.objects:
var center := Vector2(obj.position.x * cell_size, obj.position.y * cell_size) var center := Vector2(obj.position.x * cell_size, obj.position.y * cell_size)
var is_selected: bool = core.selection.get_selected() == obj.id var is_selected: bool = core.selection.get_selected() == obj.id
var color := OBJECT_SELECTED_COLOR if is_selected else OBJECT_COLOR var color := OBJECT_SELECTED_COLOR if is_selected else OBJECT_COLOR
draw_circle(center, cell_size * 0.3, color) var r := cell_size * 0.38
draw_circle(center, r, color)
draw_arc(center, r, 0.0, TAU, 16, Color(0, 0, 0, 0.85), 1.5)
if is_selected:
draw_arc(center, r + 3.0, 0.0, TAU, 16, Color(1, 1, 1, 0.95), 2.0)
var display_name: String = ""
if obj.properties is Dictionary and obj.properties.has("display_name"):
display_name = str(obj.properties["display_name"])
if display_name == "":
display_name = obj.id
draw_string(default_font, center + Vector2(-r * 1.5, -r - 2), display_name.left(6), HORIZONTAL_ALIGNMENT_CENTER, int(r * 3.0), 10, Color(1, 1, 1, 0.9))
func _gui_input(event: InputEvent) -> void: func _gui_input(event: InputEvent) -> void:
@@ -88,9 +125,27 @@ func _gui_input(event: InputEvent) -> void:
return return
if event is InputEventMouseButton: if event is InputEventMouseButton:
var mb := event as InputEventMouseButton var mb := event as InputEventMouseButton
if mb.pressed and mb.button_index == MOUSE_BUTTON_LEFT: if mb.button_index == MOUSE_BUTTON_LEFT:
_mouse_down = mb.pressed
if mb.pressed:
_handle_click(mb.position) _handle_click(mb.position)
accept_event() accept_event()
else:
_last_drag_tx = -1
_last_drag_ty = -1
elif event is InputEventMouseMotion:
var mm := event as InputEventMouseMotion
var tx := int(floor(mm.position.x / cell_size))
var ty := int(floor(mm.position.y / cell_size))
if tx != _hover_tx or ty != _hover_ty:
_hover_tx = tx
_hover_ty = ty
queue_redraw()
if _mouse_down and (tx != _last_drag_tx or ty != _last_drag_ty):
var active_tool := core.tools.get_active_tool_id()
if active_tool in ["brush", "erase"]:
_handle_click(mm.position)
accept_event()
elif event is InputEventKey: elif event is InputEventKey:
var key := event as InputEventKey var key := event as InputEventKey
if key.pressed and not key.echo: if key.pressed and not key.echo:
@@ -105,6 +160,8 @@ func _handle_key(key: InputEventKey) -> void:
func _handle_click(local_pos: Vector2) -> void: func _handle_click(local_pos: Vector2) -> void:
var tx := int(floor(local_pos.x / cell_size)) var tx := int(floor(local_pos.x / cell_size))
var ty := int(floor(local_pos.y / cell_size)) var ty := int(floor(local_pos.y / cell_size))
_last_drag_tx = tx
_last_drag_ty = ty
match core.tools.get_active_tool_id(): match core.tools.get_active_tool_id():
"brush": "brush":
@@ -125,6 +182,7 @@ func _handle_click(local_pos: Vector2) -> void:
queue_redraw() queue_redraw()
"select": "select":
core.select_object(_object_at(tx, ty)) core.select_object(_object_at(tx, ty))
edit_applied.emit()
queue_redraw() queue_redraw()
+70 -2
View File
@@ -34,6 +34,7 @@ var _viewport: EditorViewport
var _close_button: Button var _close_button: Button
var _save_button: Button var _save_button: Button
var _save_status_label: Label var _save_status_label: Label
var _instruction_label: Label
## Save-path state (world_persistence_impl.md §8.1/§8.2), set via ## Save-path state (world_persistence_impl.md §8.1/§8.2), set via
## `setup_persistence()`. `_save_service == null` means there is nothing to ## `setup_persistence()`. `_save_service == null` means there is nothing to
@@ -66,11 +67,16 @@ func setup_persistence(save_service: ChunkSaveService, world_id: String, room_sl
_base_revision = base_revision _base_revision = base_revision
if _save_button != null: if _save_button != null:
_save_button.disabled = false _save_button.disabled = false
_save_button.tooltip_text = "Save chunk changes to backend (Ctrl+S)"
func _ready() -> void: func _ready() -> void:
_build_ui() _build_ui()
_refresh_history_buttons() _refresh_history_buttons()
if core.tools.get_active_tool_id() == "select" and core.model.payload.objects.is_empty():
core.tools.set_active_tool("brush")
_toolbar.set_active_tool("brush")
_update_instruction()
func _build_ui() -> void: func _build_ui() -> void:
@@ -98,14 +104,21 @@ func _build_ui() -> void:
_save_button = Button.new() _save_button = Button.new()
_save_button.text = "Save (Ctrl+S)" _save_button.text = "Save (Ctrl+S)"
_save_button.disabled = (_save_service == null) _save_button.disabled = (_save_service == null)
_save_button.tooltip_text = "Save changes to server (Ctrl+S)" if _save_service != null else "Saving not available in scratch mode"
_save_button.pressed.connect(_on_save_pressed) _save_button.pressed.connect(_on_save_pressed)
top_bar.add_child(_save_button) top_bar.add_child(_save_button)
_close_button = Button.new() _close_button = Button.new()
_close_button.text = "Close (F2)" _close_button.text = "Close (F2 / Esc)"
_close_button.tooltip_text = "Exit build mode and return to game"
_close_button.pressed.connect(_on_close_pressed) _close_button.pressed.connect(_on_close_pressed)
top_bar.add_child(_close_button) top_bar.add_child(_close_button)
_instruction_label = Label.new()
_instruction_label.add_theme_font_size_override("font_size", 13)
_instruction_label.add_theme_color_override("font_color", Color(0.75, 0.92, 1.0))
root_vbox.add_child(_instruction_label)
_save_status_label = Label.new() _save_status_label = Label.new()
_save_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD _save_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD
_save_status_label.visible = false _save_status_label.visible = false
@@ -124,13 +137,15 @@ func _build_ui() -> void:
viewport_scroll.add_child(_viewport) viewport_scroll.add_child(_viewport)
var sidebar := VBoxContainer.new() var sidebar := VBoxContainer.new()
sidebar.custom_minimum_size = Vector2(240, 0) sidebar.custom_minimum_size = Vector2(260, 0)
body.add_child(sidebar) body.add_child(sidebar)
_palette = PalettePanel.new() _palette = PalettePanel.new()
_palette.size_flags_vertical = Control.SIZE_EXPAND_FILL
sidebar.add_child(_palette) sidebar.add_child(_palette)
_inspector = InspectorPanel.new() _inspector = InspectorPanel.new()
_inspector.size_flags_vertical = Control.SIZE_EXPAND_FILL
sidebar.add_child(_inspector) sidebar.add_child(_inspector)
_viewport.setup(core, 0) _viewport.setup(core, 0)
@@ -166,16 +181,34 @@ func _unhandled_key_input(event: InputEvent) -> void:
core.delete_object(core.selection.get_selected()) core.delete_object(core.selection.get_selected())
_on_edit_applied() _on_edit_applied()
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
elif key.keycode == KEY_B and not key.ctrl_pressed:
_on_tool_selected("brush")
_toolbar.set_active_tool("brush")
get_viewport().set_input_as_handled()
elif key.keycode == KEY_V and not key.ctrl_pressed:
_on_tool_selected("select")
_toolbar.set_active_tool("select")
get_viewport().set_input_as_handled()
elif key.keycode == KEY_P and not key.ctrl_pressed:
_on_tool_selected("object_place")
_toolbar.set_active_tool("object_place")
get_viewport().set_input_as_handled()
elif key.keycode == KEY_X and not key.ctrl_pressed:
_on_tool_selected("erase")
_toolbar.set_active_tool("erase")
get_viewport().set_input_as_handled()
func _on_tool_selected(tool_id: String) -> void: func _on_tool_selected(tool_id: String) -> void:
core.tools.set_active_tool(tool_id) core.tools.set_active_tool(tool_id)
_update_instruction()
func _on_palette_object_selected(catalog_id: String) -> void: func _on_palette_object_selected(catalog_id: String) -> void:
_viewport.pending_catalog_id = catalog_id _viewport.pending_catalog_id = catalog_id
core.tools.set_active_tool("object_place") core.tools.set_active_tool("object_place")
_toolbar.set_active_tool("object_place") _toolbar.set_active_tool("object_place")
_update_instruction()
func _on_inspector_delete_requested(object_id: String) -> void: func _on_inspector_delete_requested(object_id: String) -> void:
@@ -197,10 +230,45 @@ func _on_edit_applied() -> void:
_viewport.queue_redraw() _viewport.queue_redraw()
_inspector.show_object(core.selection.get_selected()) _inspector.show_object(core.selection.get_selected())
_refresh_history_buttons() _refresh_history_buttons()
_update_instruction()
func _on_selection_changed(object_id: String) -> void: func _on_selection_changed(object_id: String) -> void:
_inspector.show_object(object_id) _inspector.show_object(object_id)
_update_instruction()
func _update_instruction() -> void:
if _instruction_label == null:
return
var active_id := core.tools.get_active_tool_id()
match active_id:
"brush":
_instruction_label.text = "Tool: [ Brush ] — Left-click and drag on grid squares to paint ground tiles. (Keys 0-9 pick tile type)"
"object_place":
var name := _viewport.pending_catalog_id
if core.catalog != null:
var e := core.catalog.resolve(name)
if e.has("display_name"):
name = str(e["display_name"])
if name == "":
_instruction_label.text = "Tool: [ Place ] — Select an item in the Palette on the right, then click on the grid squares to place."
else:
_instruction_label.text = "Tool: [ Place: %s ] — Click any grid square to place '%s'." % [name, name]
"select":
if core.selection.has_selection():
var sel_id := core.selection.get_selected()
var obj := core.model.find_object(sel_id)
var dname := sel_id
if obj != null and obj.properties is Dictionary and obj.properties.has("display_name"):
dname = "%s (%s)" % [obj.properties["display_name"], sel_id]
_instruction_label.text = "Tool: [ Select ] — Selected '%s'. Edit in the Inspector on the right, or press Delete to remove." % dname
else:
_instruction_label.text = "Tool: [ Select ] — Click on a placed object on the grid to inspect it, or switch to Brush/Place to build."
"erase":
_instruction_label.text = "Tool: [ Erase ] — Left-click and drag on grid squares to erase painted tiles or delete objects."
_:
_instruction_label.text = "Tool: [ %s ]" % active_id
func _refresh_history_buttons() -> void: func _refresh_history_buttons() -> void:
+3
View File
@@ -37,11 +37,14 @@ var _fields_box: VBoxContainer
func _init() -> void: func _init() -> void:
name = "InspectorPanel" name = "InspectorPanel"
size_flags_vertical = Control.SIZE_EXPAND_FILL
func _ready() -> void: func _ready() -> void:
var header := Label.new() var header := Label.new()
header.text = "Inspector" header.text = "Inspector"
header.add_theme_font_size_override("font_size", 15)
header.add_theme_color_override("font_color", Color(1, 0.95, 0.4))
add_child(header) add_child(header)
_empty_label = Label.new() _empty_label = Label.new()
+11
View File
@@ -14,15 +14,26 @@ var _ids: Array[String] = []
func _init() -> void: func _init() -> void:
name = "PalettePanel" name = "PalettePanel"
size_flags_vertical = Control.SIZE_EXPAND_FILL
func _ready() -> void: func _ready() -> void:
var header := Label.new() var header := Label.new()
header.text = "Palette" header.text = "Palette"
header.add_theme_font_size_override("font_size", 15)
header.add_theme_color_override("font_color", Color(1, 0.95, 0.4))
add_child(header) add_child(header)
var hint := Label.new()
hint.text = "Click an item to place on the grid:"
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
hint.add_theme_font_size_override("font_size", 11)
hint.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
add_child(hint)
_list = ItemList.new() _list = ItemList.new()
_list.size_flags_vertical = Control.SIZE_EXPAND_FILL _list.size_flags_vertical = Control.SIZE_EXPAND_FILL
_list.custom_minimum_size = Vector2(0, 180)
_list.item_selected.connect(_on_item_selected) _list.item_selected.connect(_on_item_selected)
add_child(_list) add_child(_list)
+25 -12
View File
@@ -85,7 +85,11 @@ func _toggle_build_mode() -> void:
func _open_build_mode() -> void: func _open_build_mode() -> void:
var model := ChunkEditorModel.create_empty(32, 32, str(Session.world_id), "main") var world_id_str := str(Session.world_id) if Session.world_id != null else "0"
var room_slug := "main"
var coord := Vector3i.ZERO
var model := ChunkEditorModel.create_empty(32, 32, world_id_str, room_slug, coord)
var catalog := EditorObjectCatalog.new() var catalog := EditorObjectCatalog.new()
var err := catalog.load_catalog() var err := catalog.load_catalog()
if err != OK: if err != OK:
@@ -93,6 +97,13 @@ func _open_build_mode() -> void:
var core := EditorCore.new(model, catalog) var core := EditorCore.new(model, catalog)
_build_editor = InGameEditor.new(core) _build_editor = InGameEditor.new(core)
if int(world_id_str) > 0:
var save_service := ChunkSaveService.new()
save_service.world_id = world_id_str
save_service.room_slug = room_slug
add_child(save_service)
_build_editor.setup_persistence(save_service, world_id_str, room_slug, coord, 0)
_build_editor.closed.connect(_close_build_mode) _build_editor.closed.connect(_close_build_mode)
add_child(_build_editor) add_child(_build_editor)
@@ -109,6 +120,9 @@ func _close_build_mode() -> void:
if _build_editor != null and is_instance_valid(_build_editor): if _build_editor != null and is_instance_valid(_build_editor):
_build_editor.queue_free() _build_editor.queue_free()
_build_editor = null _build_editor = null
for child in get_children():
if child is ChunkSaveService:
child.queue_free()
_player.set_physics_process(true) _player.set_physics_process(true)
_player.set_process_unhandled_input(true) _player.set_process_unhandled_input(true)
_player._refresh_mouse_mode() _player._refresh_mouse_mode()
@@ -210,19 +224,18 @@ func _show_build_stamp() -> void:
add_child(layer) add_child(layer)
var lbl := Label.new() var lbl := Label.new()
lbl.text = "build %s" % _build_label() lbl.text = "build %s" % _build_label()
lbl.add_theme_color_override("font_color", Color(1, 1, 0.35)) lbl.add_theme_color_override("font_color", Color(1, 1, 0.35, 0.85))
lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 1)) lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 1))
lbl.add_theme_constant_override("outline_size", 6) lbl.add_theme_constant_override("outline_size", 4)
lbl.add_theme_font_size_override("font_size", 20) lbl.add_theme_font_size_override("font_size", 14)
# Full-width strip pinned to the TOP-left. (A bare preset + .position on a # Pinned to the BOTTOM-right so it never blocks top menu or editor toolbars.
# zero-size Label collapses the rect to negative width and nothing draws -- lbl.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
# which is why this never showed; explicit offsets fix it.) lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
lbl.set_anchors_preset(Control.PRESET_TOP_WIDE) lbl.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
lbl.offset_left = 12 lbl.offset_left = 12
lbl.offset_right = -12 lbl.offset_right = -16
lbl.offset_top = 8 lbl.offset_top = -32
lbl.offset_bottom = 40 lbl.offset_bottom = -8
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
layer.add_child(lbl) layer.add_child(lbl)