Implement Phase 19 updates and user/editor verification

- Updated architectural specifications for Phase 19 scenery, backgrounds, and surface finishes to reflect implementation status and user verification results.
- Revised UI/UX specifications for background ground and wall color, plain background, and starter scene composition to indicate successful implementation and verification.
- Enhanced scenery controller and sandbox stage scripts to improve eraser functionality and user interactions.
- Adjusted test cases to align with the latest changes in scenery and background implementations, ensuring all checks pass successfully.
- Added new buttons for wall area erasure in the scenery capture bar and updated layout handling for better user experience.
This commit is contained in:
2026-10-22 17:02:39 -04:00
parent 59f1817063
commit c6f3930dcb
21 changed files with 179 additions and 183 deletions
+13 -2
View File
@@ -5722,7 +5722,7 @@ func _refresh_scenery_capture_bar() -> void:
if _scenery_controller.get_active_tool() == &"scenery_eraser":
erase_prompt = "Drag over wall or ground to erase"
if _scenery_controller.get_surface_areas_erased() > 0:
erase_prompt = "Erase another area · Cancel undoes"
erase_prompt = "Erase another area · earlier openings stay"
actions.append({"id": "done_tool", "label": "Done", "primary": true})
actions.append({"id": "cancel_tool", "label": "Cancel"})
_use_target_capture_bar.set_scenery_capture_actions(erase_prompt, actions)
@@ -5753,7 +5753,12 @@ func _refresh_scenery_capture_bar() -> void:
_use_target_capture_bar.set_scenery_capture_actions(group_prompt, preview_actions)
return
if _scenery_controller != null and _scenery_controller.is_single_transform_active():
_hide_scenery_capture_bar()
var kind: StringName = _scenery_controller.get_single_transform_kind()
var prompt: String = "Move scenery · release to keep" if kind == &"move" else "Resize scenery · release to keep"
_set_scenery_selection_capture_bounds(true)
_use_target_capture_bar.set_scenery_capture_actions(prompt, [
{"id": "cancel_single", "label": "Cancel"},
])
return
if _scenery_edit_mode:
var count: int = _selection.get_selected().size()
@@ -5804,6 +5809,12 @@ func _on_capture_action_requested(action_id: StringName) -> void:
&"cancel_tool":
_scenery_controller.cancel_active_tool()
_hide_scenery_capture_bar()
&"cancel_single":
if _scenery_controller != null and _scenery_controller.is_single_transform_active():
_scenery_controller.cancel_single_transform()
_scenery_single_dragging = false
_refresh_scenery_inspector()
_refresh_scenery_capture_bar()
&"select_several":
_scenery_multi_select = true
_show_toast("Tap scenery pieces to add them. Shift-click also works.")
+3 -2
View File
@@ -259,10 +259,11 @@ func clear_object_state(node: Node2D) -> void:
func restore_authored_state(stage_mode_name: String, run_state_name: String, camera_zoom: Vector2) -> void:
for id: int in authored_state.keys():
var entry: Dictionary = authored_state[id]
var node: Node2D = entry.get("node") as Node2D
if node == null or not is_instance_valid(node):
var node_value: Variant = entry.get("node")
if not is_instance_valid(node_value) or not node_value is Node2D:
authored_state.erase(id)
continue
var node: Node2D = node_value as Node2D
if node is STAGE_SCENERY_ARTWORK:
authored_state.erase(id)
continue
@@ -40,8 +40,7 @@ var _wall_erase_dragging: bool = false
var _wall_erase_pointer: Vector2 = Vector2.ZERO
var _surface_erase_kind: StringName = &"wall"
var _surface_areas_erased: int = 0
# Keep original surfaces detached during an eraser session so Cancel can restore them.
var _surface_erase_originals: Array[Dictionary] = []
# Keep cut originals detached until each released eraser stroke is committed.
var _surface_erase_detached: Array[Node2D] = []
var _surface_erase_changed: bool = false
var _snap_enabled: bool = false
@@ -244,10 +243,8 @@ func begin_surface_eraser(initial_world_position: Vector2, surface_kind: StringN
_wall_erase_start = Vector2.INF
_wall_erase_dragging = false
_surface_areas_erased = 0
_surface_erase_originals.clear()
_surface_erase_detached.clear()
_surface_erase_changed = false
_capture_surface_eraser_snapshot(surface_kind)
_wall_erase_pointer = initial_world_position
var preview := Node2D.new()
preview.name = "BackgroundGroundEraserPreview" if surface_kind == &"background_ground" else "WallEraserPreview"
@@ -365,6 +362,7 @@ func handle_mouse_button(event: InputEventMouseButton, world_position: Vector2,
if changed > 0:
_surface_areas_erased += 1
_surface_erase_changed = true
_commit_surface_eraser_cut()
update_pointer(world_position)
return true
return false
@@ -442,14 +440,16 @@ func _rollback_placement_session() -> void:
func cancel_surface_eraser() -> void:
if not _is_surface_eraser():
return
if _surface_erase_changed:
_restore_surface_eraser_snapshot()
_surface_areas_erased = 0
_clear_surface_eraser_transaction()
exit_tool()
func _commit_surface_eraser() -> void:
_commit_surface_eraser_cut()
func _commit_surface_eraser_cut() -> void:
if _surface_erase_changed:
document_change_requested.emit()
for detached: Node2D in _surface_erase_detached:
@@ -459,63 +459,10 @@ func _commit_surface_eraser() -> void:
func _clear_surface_eraser_transaction() -> void:
_surface_erase_originals.clear()
_surface_erase_detached.clear()
_surface_erase_changed = false
func _capture_surface_eraser_snapshot(surface_kind: StringName) -> void:
if _world == null or not is_instance_valid(_world):
return
var target_scripts: Array[Script] = _surface_erase_target_scripts(surface_kind)
for child: Node in _world.get_children():
if not target_scripts.has(child.get_script()):
continue
var surface: Node2D = child as Node2D
if surface == null:
continue
_surface_erase_originals.append({
"node": surface,
"parent": surface.get_parent(),
"index": surface.get_index(),
})
func _restore_surface_eraser_snapshot() -> void:
if _world == null or not is_instance_valid(_world):
return
var original_ids: Dictionary = {}
for snapshot: Dictionary in _surface_erase_originals:
var original: Node2D = snapshot.get("node") as Node2D
if original != null and is_instance_valid(original):
original_ids[original.get_instance_id()] = true
var target_scripts: Array[Script] = _surface_erase_target_scripts(_surface_erase_kind)
for child: Node in _world.get_children().duplicate():
if not target_scripts.has(child.get_script()):
continue
_world.remove_child(child)
if not original_ids.has(child.get_instance_id()):
child.queue_free()
for detached: Node2D in _surface_erase_detached:
if detached == null or not is_instance_valid(detached):
continue
if not original_ids.has(detached.get_instance_id()):
detached.queue_free()
var restore_order: Array[Dictionary] = _surface_erase_originals.duplicate()
restore_order.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return int(a.get("index", 0)) < int(b.get("index", 0)))
for snapshot: Dictionary in restore_order:
var original: Node2D = snapshot.get("node") as Node2D
var parent: Node = snapshot.get("parent") as Node
if original == null or not is_instance_valid(original):
continue
if parent == null or not is_instance_valid(parent):
parent = _world
if original.get_parent() != null:
original.get_parent().remove_child(original)
parent.add_child(original)
parent.move_child(original, clampi(int(snapshot.get("index", 0)), 0, parent.get_child_count() - 1))
func begin_group_transform(kind: StringName, members: Array[Node2D]) -> bool:
if _single_active or members.is_empty() or kind not in [&"move", &"resize"]:
return false
@@ -34,14 +34,16 @@ var _scenery_edit_prompt: Label
var _scenery_edit_count: Label
var _scenery_edit_count_badge: PanelContainer
var _scenery_selection_group: PanelContainer
var _scenery_edit_actions: HBoxContainer
var _scenery_selection_actions: HBoxContainer
var _scenery_edit_header: BoxContainer
var _scenery_edit_actions: BoxContainer
var _scenery_selection_actions: BoxContainer
var _scenery_select_several_button: Button
var _scenery_select_all_button: Button
var _scenery_move_group_button: Button
var _scenery_resize_group_button: Button
var _scenery_clear_selection_button: Button
var _scenery_erase_area_button: Button
var _scenery_erase_wall_area_button: Button
var _scenery_done_button: Button
var _change_prop_button: Button
var _cancel_button: Button
@@ -175,6 +177,7 @@ func set_scenery_edit_state(selected_count: int) -> void:
_scenery_move_group_button.visible = has_group
_scenery_resize_group_button.visible = has_group
_scenery_erase_area_button.visible = selected_count == 0
_scenery_erase_wall_area_button.visible = selected_count == 0
_scenery_clear_selection_button.visible = selected_count > 0
visible = true
apply_theme()
@@ -273,7 +276,7 @@ func _apply_scenery_edit_theme() -> void:
if font != null:
_scenery_edit_prompt.add_theme_font_override("font", font)
_scenery_edit_count.add_theme_font_override("font", font)
for button: Button in [_scenery_select_several_button, _scenery_select_all_button, _scenery_move_group_button, _scenery_resize_group_button, _scenery_clear_selection_button, _scenery_erase_area_button, _scenery_done_button]:
for button: Button in [_scenery_select_several_button, _scenery_select_all_button, _scenery_move_group_button, _scenery_resize_group_button, _scenery_clear_selection_button, _scenery_erase_area_button, _scenery_erase_wall_area_button, _scenery_done_button]:
if font != null:
button.add_theme_font_override("font", font)
button.add_theme_font_size_override("font_size", 19)
@@ -506,6 +509,7 @@ func _build_scenery_edit_controls() -> void:
_scenery_edit_layout.visible = false
_layout_stash.add_child(_scenery_edit_layout)
var header_row: HBoxContainer = HBoxContainer.new()
_scenery_edit_header = header_row
header_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header_row.add_theme_constant_override("separation", 12)
_scenery_edit_layout.add_child(header_row)
@@ -560,6 +564,9 @@ func _build_scenery_edit_controls() -> void:
_scenery_erase_area_button = _make_scenery_action_button("EraseArea", "Erase Area", &"erase_area")
_scenery_erase_area_button.custom_minimum_size.x = 152.0
_scenery_edit_actions.add_child(_scenery_erase_area_button)
_scenery_erase_wall_area_button = _make_scenery_action_button("Action_erase_wall_area", "Erase wall area", &"erase_wall_area")
_scenery_erase_wall_area_button.custom_minimum_size.x = 160.0
_scenery_edit_actions.add_child(_scenery_erase_wall_area_button)
_scenery_done_button = _make_scenery_action_button("DoneEditing", "Done editing", &"done_edit")
_scenery_done_button.custom_minimum_size.x = 160.0
_scenery_edit_actions.add_child(_scenery_done_button)
@@ -688,12 +695,16 @@ func _layout_scenery_edit_capture_bar(viewport_size: Vector2) -> void:
_scenery_edit_prompt.add_theme_font_size_override("font_size", 27 if width >= 700.0 else (21 if width >= 520.0 else 18))
if narrow and _scenery_erase_area_button.get_parent() == _scenery_edit_actions:
_scenery_edit_actions.remove_child(_scenery_erase_area_button)
_scenery_edit_layout.get_child(0).add_child(_scenery_erase_area_button)
_scenery_edit_header.add_child(_scenery_erase_area_button)
elif not narrow and _scenery_erase_area_button.get_parent() != _scenery_edit_actions:
var header_row: HBoxContainer = _scenery_edit_layout.get_child(0) as HBoxContainer
header_row.remove_child(_scenery_erase_area_button)
_scenery_edit_header.remove_child(_scenery_erase_area_button)
_scenery_edit_actions.add_child(_scenery_erase_area_button)
_scenery_edit_actions.move_child(_scenery_erase_area_button, 1)
_scenery_edit_header = _replace_box_orientation(_scenery_edit_header, narrow)
_scenery_edit_actions = _replace_box_orientation(_scenery_edit_actions, narrow)
_scenery_selection_actions = _replace_box_orientation(_scenery_selection_actions, narrow)
_scenery_edit_actions.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_scenery_selection_group.size_flags_horizontal = Control.SIZE_EXPAND_FILL if narrow else Control.SIZE_SHRINK_BEGIN
_scenery_selection_actions.add_theme_constant_override("separation", 0)
_scenery_select_several_button.custom_minimum_size.x = 134.0 if narrow else 158.0
_scenery_select_all_button.custom_minimum_size.x = 124.0 if narrow else 148.0
@@ -701,8 +712,9 @@ func _layout_scenery_edit_capture_bar(viewport_size: Vector2) -> void:
_scenery_resize_group_button.custom_minimum_size.x = 158.0 if narrow else 182.0
_scenery_done_button.custom_minimum_size.x = 142.0 if narrow else 160.0
_scenery_erase_area_button.custom_minimum_size.x = 136.0 if narrow else 152.0
_scenery_erase_wall_area_button.custom_minimum_size.x = 144.0 if narrow else 160.0
_scenery_clear_selection_button.custom_minimum_size.x = 128.0 if narrow else 150.0
for button: Button in [_scenery_select_several_button, _scenery_select_all_button, _scenery_move_group_button, _scenery_resize_group_button, _scenery_clear_selection_button, _scenery_erase_area_button, _scenery_done_button]:
for button: Button in [_scenery_select_several_button, _scenery_select_all_button, _scenery_move_group_button, _scenery_resize_group_button, _scenery_clear_selection_button, _scenery_erase_area_button, _scenery_erase_wall_area_button, _scenery_done_button]:
button.add_theme_font_size_override("font_size", 19 if width >= 700.0 else 16)
button.custom_minimum_size.y = 52.0 if narrow else SCENERY_EDIT_BUTTON_HEIGHT
button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
@@ -710,9 +722,38 @@ func _layout_scenery_edit_capture_bar(viewport_size: Vector2) -> void:
call_deferred("_fit_scenery_edit_height")
func _replace_box_orientation(box: BoxContainer, vertical: bool) -> BoxContainer:
if box == null or (vertical and box is VBoxContainer) or (not vertical and box is HBoxContainer):
return box
var parent: Node = box.get_parent()
if parent == null:
return box
var child_index: int = box.get_index()
var replacement: BoxContainer = VBoxContainer.new() if vertical else HBoxContainer.new()
replacement.name = box.name
replacement.alignment = box.alignment
replacement.size_flags_horizontal = box.size_flags_horizontal
replacement.size_flags_vertical = box.size_flags_vertical
replacement.visible = box.visible
replacement.add_theme_constant_override("separation", box.get_theme_constant("separation"))
for child: Node in box.get_children():
box.remove_child(child)
replacement.add_child(child)
parent.remove_child(box)
parent.add_child(replacement)
parent.move_child(replacement, child_index)
box.queue_free()
return replacement
func _layout_scenery_capture_bar(viewport_size: Vector2) -> void:
var inset: float = float(_integer("stage_panels.shell.edge_inset", 16, 12, 48))
var available_width: float = maxf(1.0, viewport_size.x - inset * 2.0)
var safe_left: float = inset
var safe_right: float = viewport_size.x - inset
if _has_horizontal_safe_bounds:
safe_left = clampf(_safe_left, inset, maxf(inset, viewport_size.x - inset))
safe_right = clampf(_safe_right, safe_left + 1.0, maxf(safe_left + 1.0, viewport_size.x - inset))
var available_width: float = maxf(1.0, safe_right - safe_left)
var width: float = minf(SCENERY_CAPTURE_MAX_WIDTH, available_width)
var button_count: int = maxi(1, _custom_action_buttons.size())
var has_depth_choices: bool = button_count >= 5
@@ -724,7 +765,7 @@ func _layout_scenery_capture_bar(viewport_size: Vector2) -> void:
size.x = width
size_flags_horizontal = Control.SIZE_SHRINK_CENTER
clip_contents = false
position = Vector2((viewport_size.x - width) * 0.5, _top_bar_height + 56.0)
position = Vector2(safe_left + (available_width - width) * 0.5, _top_bar_height + 56.0)
_prompt.add_theme_font_size_override("font_size", 25 if width >= 520.0 else 19)
_set_layout(true, stack_actions)
_actions_row.alignment = BoxContainer.ALIGNMENT_END