Files
stickman/tests/test_phase19_surface_followup.gd
ryan 59f1817063 Add StageBackgroundView and related tests for plain background functionality
- Implemented StageBackgroundView class for managing sky and plain backgrounds in the stage.
- Added functionality to set background type and color, bind camera, and apply themes.
- Created test_phase19_plain_background.gd to validate plain background behavior and migration.
- Developed test_phase19_starter_chooser_layout.gd to ensure proper layout and functionality of the starter chooser.
- Included visual capture tests for UI elements and interactions.
2026-10-22 16:18:25 -04:00

544 lines
29 KiB
GDScript

extends SceneTree
const SANDBOX_STAGE: PackedScene = preload("res://scenes/stage/sandbox_stage.tscn")
const STAGE_SCENE_IO: Script = preload("res://scripts/stage/stage_scene_io.gd")
const STAGE_SCENE_ADAPTER: Script = preload("res://scripts/editors/stage/stage_scene_adapter.gd")
const STAGE_SURFACE_RENDERER: Script = preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
const STAGE_BACKGROUND_GROUND: Script = preload("res://scripts/stage/scenery/stage_background_ground.gd")
const STAGE_WALL_PANEL: Script = preload("res://scripts/stage/scenery/stage_wall_panel.gd")
const STAGE_SURFACE_PREVIEW: Script = preload("res://scripts/stage/scenery/stage_surface_preview.gd")
var _failures: Array[String] = []
var _stage: SandboxStage
var _initial_window_size: Vector2i = Vector2i(1280, 720)
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_set_window_geometry(Vector2i(1280, 720))
_initial_window_size = root.size
if DisplayServer.get_name() != "headless":
print("GUI capture window: usable=%s position=%s size=%s" % [str(DisplayServer.screen_get_usable_rect(DisplayServer.window_get_current_screen())), str(root.position), str(root.size)])
_test_v15_migration()
await _test_grass_render()
_stage = SANDBOX_STAGE.instantiate() as SandboxStage
root.add_child(_stage)
await process_frame
await process_frame
var world: Node2D = _stage.get_node("World") as Node2D
var controller: StageSceneryController = _stage.get("_scenery_controller") as StageSceneryController
var selected_ground: StageBackgroundGround = _place_background_ground(world, controller)
_test_background_ground_model_and_serialization(world, selected_ground)
await _test_surface_inspector_and_color_transactions(selected_ground)
await _test_surface_erase_confirmation(world, selected_ground)
await _test_selector_setup_routes(controller)
if _stage != null and is_instance_valid(_stage):
_stage.queue_free()
await process_frame
await process_frame
if _failures.is_empty():
print("PASS: Phase 19 background ground, color, migration, and erase behavior")
else:
for failure: String in _failures:
push_error("FAIL: " + failure)
quit(0 if _failures.is_empty() else 1)
func _test_v15_migration() -> void:
var source: Dictionary = {
"version": "1.5",
"sky_id": "sky_cloudy_day",
"objects": [
{"file_id": "ground_1", "type": "terrain", "asset_id": "ground", "position": {"x": 0.0, "y": 0.0}, "finish_id": "finish_ground_sidewalk"},
{"file_id": "wall_1", "type": "wall_panel", "position": {"x": 0.0, "y": 0.0}, "size": {"x": 320.0, "y": 240.0}, "finish_id": "finish_wall_brick", "color": "#5A9D70FF"},
{"file_id": "wall_2", "type": "wall_panel", "position": {"x": 400.0, "y": 0.0}, "size": {"x": 320.0, "y": 240.0}, "finish_id": "finish_wall_wood", "color": "#4477AAFF"}
]
}
var migrated: Dictionary = STAGE_SCENE_IO._migrate_to_current(source)
_expect(String(migrated.get("version", "")) == "1.7", "v1.5 scenes migrate to v1.7")
_expect(String(migrated.get("sky_id", "")) == "sky_cloudy_day", "v1.5 migration preserves the saved Sky")
_expect(String(migrated.get("background_kind", "")) == "sky" and String(migrated.get("plain_color", "")) == STAGE_SCENE_IO.DEFAULT_PLAIN_COLOR, "v1.5 migration adds the remembered Plain Background defaults")
var objects: Array = migrated.get("objects", []) as Array
_expect(String((objects[0] as Dictionary).get("finish_id", "")) == "finish_ground_sidewalk", "v1.5 migration preserves walkable terrain finishes")
var migrated_brick_color: String = String((objects[1] as Dictionary).get("color", "")).to_upper()
var expected_brick_color: String = "#" + STAGE_SURFACE_RENDERER.default_tint(&"finish_wall_brick").to_html(true).to_upper()
_expect(migrated_brick_color == expected_brick_color, "old placeholder brick tint becomes the natural brick color")
_expect(String((objects[2] as Dictionary).get("color", "")) == "#4477AAFF", "intentional v1.5 wood color is preserved")
var current: Dictionary = source.duplicate(true)
current["version"] = "1.6"
var current_objects: Array = current["objects"] as Array
(current_objects[1] as Dictionary)["color"] = "#5A9D70FF"
var current_migrated: Dictionary = STAGE_SCENE_IO._migrate_to_current(current)
_expect(String(((current_migrated["objects"] as Array)[1] as Dictionary).get("color", "")) == "#5A9D70FF", "v1.6 brick colors remain authoritative")
func _test_grass_render() -> void:
if DisplayServer.get_name() == "headless":
print("SKIP: headless dummy renderer provides no visual surface capture; run this fixture in the GUI renderer")
return
var viewport := SubViewport.new()
viewport.size = Vector2i(640, 320)
viewport.transparent_bg = true
viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
root.add_child(viewport)
var surface: StageBackgroundGround = STAGE_BACKGROUND_GROUND.new() as StageBackgroundGround
surface.configure(Vector2(10000.0, 6000.0), &"finish_background_grass", Color("#72A84C"))
surface.position = Vector2(320.0, 160.0)
viewport.add_child(surface)
await process_frame
await process_frame
var texture: Texture2D = viewport.get_texture()
if texture == null:
print("SKIP: headless dummy renderer provides no SubViewport texture; visual grass capture requires the GUI renderer")
viewport.queue_free()
await process_frame
return
if not texture.get_rid().is_valid():
print("SKIP: headless dummy renderer has no texture RID; visual grass capture requires the GUI renderer")
viewport.queue_free()
await process_frame
return
var image: Image = texture.get_image()
if image == null:
print("SKIP: headless dummy renderer cannot read a SubViewport image; visual capture requires the GUI renderer")
viewport.queue_free()
await process_frame
return
var distinct: Dictionary = {}
var detail_samples: int = 0
var base := Color("#72A84C")
for y: int in range(12, 308, 2):
for x: int in range(12, 628, 2):
var sample: Color = image.get_pixel(x, y)
distinct[sample.to_html(true)] = true
var color_distance: float = absf(sample.r - base.r) + absf(sample.g - base.g) + absf(sample.b - base.b)
if color_distance > 0.08:
detail_samples += 1
_expect(distinct.size() >= 5 and detail_samples >= 1100, "large grass ground renders dense, visible interior grass texture at normal Stage scale (samples=%d)" % detail_samples)
image.save_png("/private/tmp/phase19_background_grass.png")
viewport.queue_free()
await process_frame
func _place_background_ground(world: Node2D, controller: StageSceneryController) -> StageBackgroundGround:
controller.set_grid_snap(false, 15.0)
controller.begin_background_ground(&"finish_background_grass", Vector2(300.0, 100.0), Color("#72A84C"))
var press := InputEventMouseButton.new()
press.button_index = MOUSE_BUTTON_LEFT
press.pressed = true
controller.handle_mouse_button(press, Vector2(300.0, 100.0), false)
var release := InputEventMouseButton.new()
release.button_index = MOUSE_BUTTON_LEFT
release.pressed = false
controller.handle_mouse_button(release, Vector2(700.0, 340.0), false)
var placed: StageBackgroundGround
for child: Node in world.get_children():
if child.get_script() == STAGE_BACKGROUND_GROUND:
placed = child as StageBackgroundGround
_expect(placed != null, "production rectangle placement commits a background ground node")
var placed_node: Node = placed
_expect(placed != null and not (placed_node is CollisionObject2D), "background ground remains non-physical and non-walkable")
return placed
func _test_background_ground_model_and_serialization(world: Node2D, surface: StageBackgroundGround) -> void:
if surface == null:
return
print("[background ground size] %s" % str(surface.panel_size))
_expect(surface.panel_size.is_equal_approx(Vector2(400.0, 240.0)), "background ground size follows the drawn rectangle")
var sample_wall: StageWallPanel = STAGE_WALL_PANEL.new() as StageWallPanel
sample_wall.configure(Vector2(100.0, 100.0), &"finish_wall_paint", Color.WHITE)
_expect(surface.z_index < sample_wall.z_index, "background ground draws behind wall panels")
sample_wall.free()
var selection: StageSelection = _stage.get("_selection") as StageSelection
selection.set_scope(StageSelection.SelectionScope.SCENERY)
_expect(selection.hit_test(surface.global_position) == surface, "Edit Scenery hit-testing can select background ground")
var data: Dictionary = _stage.call("_collect_scene_data", false)
var validation: Dictionary = STAGE_SCENE_IO._validate_root(data)
_expect(String(data.get("version", "")) == STAGE_SCENE_IO.FILE_VERSION and bool(validation.get("ok", false)), "background ground serializes and validates in the current scene schema")
var found: Dictionary = {}
for entry_value: Variant in data.get("objects", []):
if entry_value is Dictionary and String((entry_value as Dictionary).get("type", "")) == "background_ground":
found = entry_value as Dictionary
_expect(String(found.get("finish_id", "")) == "finish_background_grass", "scene adapter saves the distinct background finish")
var restored_world := Node2D.new()
root.add_child(restored_world)
var adapter: StageSceneAdapter = _stage.get("_scene_adapter") as StageSceneAdapter
var isolated: Dictionary = data.duplicate(true)
isolated["objects"] = [found]
var result: Dictionary = adapter.apply(isolated, restored_world, _stage.get("_spawner") as StageSpawner)
var restored: StageBackgroundGround = null
for child: Node in restored_world.get_children():
if child.get_script() == STAGE_BACKGROUND_GROUND:
restored = child as StageBackgroundGround
_expect(restored != null and restored.panel_size == surface.panel_size and restored.tint.is_equal_approx(surface.tint), "background ground round-trips through the production adapter")
_expect((result.get("loaded_nodes", []) as Array).size() == 1, "background ground reload is a separate object")
restored_world.queue_free()
func _test_surface_inspector_and_color_transactions(surface: StageBackgroundGround) -> void:
if surface == null:
return
_stage.call("_enter_scenery_edit_mode")
var selection: StageSelection = _stage.get("_selection") as StageSelection
selection.select_only(surface)
var inspector: StageInspectorPanel = _stage.get("_scenery_inspector") as StageInspectorPanel
var colors: StageSurfaceColorControls = inspector.find_child("ScenerySurfaceColors", true, false) as StageSurfaceColorControls
var ground_looks: StagePanelChoiceGroup = inspector.find_child("BackgroundGroundLooks", true, false) as StagePanelChoiceGroup
_expect(inspector.visible and surface.get_parent() == _stage.get_node("World"), "selected background ground opens its normal scenery inspector")
_expect(not colors.visible and ground_looks.visible, "Grass hides Color while showing its Look choices")
_set_window_geometry(Vector2i(640, 480))
_stage.call("_refresh_scenery_inspector_layout")
inspector.set_safe_rect(_stage.call("_prop_settings_safe_rect"))
inspector.request_fit()
await process_frame
await process_frame
await process_frame
var inspector_rect: Rect2 = inspector.get_global_rect()
_expect(inspector_rect.position.x >= 0.0 and inspector_rect.position.y >= 0.0 and inspector_rect.end.x <= 640.0 and inspector_rect.end.y <= 480.0, "selected-piece inspector fits a narrow display")
var capture_bar: StageUseTargetCaptureBar = _stage.get("_use_target_capture_bar") as StageUseTargetCaptureBar
var capture_bar_rect: Rect2 = capture_bar.get_global_rect() if capture_bar != null else Rect2()
_expect(capture_bar != null and (not capture_bar.visible or capture_bar_rect.end.x <= inspector_rect.position.x + 1.0), "narrow scenery actions stay beside the selected-piece inspector")
var chrome: CanvasLayer = _stage.get("_chrome") as CanvasLayer
var mode_buttons: Dictionary = chrome.call("get_mode_buttons") as Dictionary if chrome != null else {}
var grid_check: CheckBox = chrome.call("get_grid_check") as CheckBox if chrome != null else null
var snap_check: CheckBox = chrome.call("get_snap_check") as CheckBox if chrome != null else null
var grid_size: SpinBox = chrome.call("get_grid_size_spin") as SpinBox if chrome != null else null
for mode_button_value: Variant in mode_buttons.values():
var mode_button: Button = mode_button_value as Button
for right_control: Control in [grid_check, snap_check, grid_size]:
if right_control != null and right_control.visible:
_expect(not mode_button.get_global_rect().intersects(right_control.get_global_rect()), "narrow Stage mode and Grid/Snap controls do not overlap")
_capture_root_viewport("phase19_inspector_narrow.png")
_set_window_geometry(_initial_window_size)
await process_frame
var original: Color = surface.tint
_stage.call("_set_scene_dirty", false)
_stage.call("_on_selected_surface_look_choice", &"finish_background_carpet")
_expect(surface.finish_id == &"finish_background_carpet" and surface.tint.is_equal_approx(original), "switching to Carpet preserves the selected piece's stored color")
_expect(colors.visible, "Carpet exposes the shared Color control")
_stage.call("_set_scene_dirty", false)
var dialog: StageSurfaceColorDialog = _stage.get("_surface_color_dialog") as StageSurfaceColorDialog
colors.custom_color_requested.emit()
var tentative := Color("#3F72B5")
var picker: ColorPicker = dialog.find_child("ColorPickerControl", true, false) as ColorPicker
if picker != null:
picker.color = tentative
if not surface.get_render_tint().is_equal_approx(tentative):
dialog.call("_on_picker_color_changed", tentative)
else:
dialog.preview_color_changed.emit(tentative)
_expect(not surface.tint.is_equal_approx(tentative) and surface.get_render_tint().is_equal_approx(tentative), "custom-color picker changes only the temporary render tint")
var dialog_preview: StageSurfacePreview = dialog.get("_surface_preview") as StageSurfacePreview
_expect(dialog_preview != null and dialog_preview.finish_id == surface.finish_id and dialog_preview.tint.is_equal_approx(tentative), "custom-color dialog preview follows the selected look and tentative color")
await process_frame
await create_timer(0.2).timeout
_capture_gui_screen("phase19_color_preview.png")
_expect(not bool((_stage.get("_document") as StageDocumentController).dirty), "tentative color does not dirty the Stage document")
var saved: Dictionary = _stage.call("_collect_scene_data", true)
var saved_surface: Dictionary = _find_object(saved, "background_ground")
_expect(String(saved_surface.get("color", "")).to_upper() == "#" + original.to_html(true).to_upper(), "autosave snapshot cannot capture a tentative color")
_stage.call("_on_surface_color_dialog_cancelled")
_expect(surface.get_render_tint().is_equal_approx(original), "Cancel restores the selected piece's prior color")
_expect(not bool((_stage.get("_document") as StageDocumentController).dirty), "Cancel leaves the Stage clean")
colors.custom_color_requested.emit()
if picker != null:
picker.color = tentative
dialog.call("_confirm_color")
await process_frame
_expect(surface.tint.is_equal_approx(tentative), "Use color commits the chosen tint to the selected piece")
_expect(bool((_stage.get("_document") as StageDocumentController).dirty), "confirmed color marks the Stage document dirty")
var recent: Array[Color] = _stage.get("_recent_surface_colors")
_expect(not recent.is_empty() and recent[0].is_equal_approx(tentative), "confirmed color enters the shared recent-color history")
for index: int in range(10):
_stage.call("_record_recent_surface_color", Color.from_hsv(float(index) / 10.0, 0.78, 0.92))
recent = _stage.get("_recent_surface_colors")
var recent_keys: Array[String] = _stage.call("_surface_color_history_strings")
_expect(recent.size() == 8 and recent_keys.size() == 8 and recent_keys[0] == _stage.call("_canonical_surface_color", Color.from_hsv(0.9, 0.78, 0.92)), "shared recent-color history deduplicates and keeps the newest eight colors")
_stage.call("_on_selected_surface_look_choice", &"finish_background_grass")
_stage.call("_on_selected_surface_look_choice", &"finish_background_carpet")
_expect(surface.tint.is_equal_approx(tentative), "Carpet keeps its color when switching to Grass and back")
func _test_surface_erase_confirmation(world: Node2D, background: StageBackgroundGround) -> void:
if background == null:
return
var wall: StageWallPanel = STAGE_WALL_PANEL.new() as StageWallPanel
wall.configure(Vector2(120.0, 120.0), &"finish_wall_wood", Color("#C87137"))
wall.position = Vector2(900.0, 400.0)
world.add_child(wall)
var selection: StageSelection = _stage.get("_selection") as StageSelection
selection.select_only(background)
_stage.call("_set_scene_dirty", false)
_stage.call("_begin_selected_wall_erase")
_expect(not (_stage.get("_scenery_controller") as StageSceneryController).is_active(), "wall-area eraser cannot start from a background ground selection")
_stage.call("_request_erase_selected_scenery")
var dialog: ConfirmationDialog = _stage.get("_scenery_erase_piece_dialog") as ConfirmationDialog
_expect(dialog.visible and dialog.dialog_text.contains("Erase this piece?"), "Erase background ground asks for confirmation")
dialog.canceled.emit()
dialog.hide()
_expect(is_instance_valid(background) and background.get_parent() == world, "Keep it leaves background ground in place")
_expect(not bool((_stage.get("_document") as StageDocumentController).dirty), "Keep it leaves the Stage clean")
selection.select_only(background)
_stage.call("_request_erase_selected_scenery")
dialog.confirmed.emit()
dialog.hide()
await process_frame
_expect(not is_instance_valid(background), "confirmed Erase removes only the captured background ground")
_expect(bool((_stage.get("_document") as StageDocumentController).dirty), "confirmed Erase dirties the Stage once")
_stage.call("_set_scene_dirty", false)
# Use a fresh background node so the Delete-key group route can verify its count.
var second_ground: StageBackgroundGround = STAGE_BACKGROUND_GROUND.new() as StageBackgroundGround
second_ground.configure(Vector2(80.0, 80.0), &"finish_background_concrete", Color("#A6AAA3"))
world.add_child(second_ground)
selection.clear_selection()
selection.select_only(wall)
selection.add_to_selection(second_ground)
var delete_event := InputEventKey.new()
delete_event.keycode = KEY_DELETE
delete_event.pressed = true
_stage.call("_unhandled_key_input", delete_event)
_expect(dialog.visible and dialog.dialog_text.contains("2 scenery pieces"), "Delete uses a counted confirmation for a scenery group")
dialog.canceled.emit()
dialog.hide()
_expect(is_instance_valid(wall) and is_instance_valid(second_ground), "Cancel leaves every group member intact")
_stage.call("_request_erase_selected_scenery")
dialog.confirmed.emit()
dialog.hide()
await process_frame
_expect(not is_instance_valid(wall) and not is_instance_valid(second_ground), "confirmed group erase removes only its captured scenery members")
func _test_selector_setup_routes(controller: StageSceneryController) -> void:
var browser: StageAssetBrowser = _stage.get("_asset_browser") as StageAssetBrowser
var selector: AssetSelector = browser.get_selector()
_expect(is_instance_valid(selector), "Scenery chooser instance is live")
if not is_instance_valid(selector):
return
var gui_render: bool = DisplayServer.get_name() != "headless"
if gui_render:
_stage.call("_open_selector", "scenery")
await process_frame
await process_frame
await process_frame
await create_timer(0.2).timeout
_capture_gui_screen("phase19_chooser.png")
else:
# Avoid PopupPanel.popup_centered() under the macOS dummy renderer while
# still exercising the production selector signal and StageAssetBrowser routing.
browser.set_selector_kind("scenery")
selector.kind = "scenery"
var entries: Array[Dictionary] = browser.scan_entries("scenery")
entries.append_array(browser.call("_scenery_background_entries") as Array[Dictionary])
selector.set_entries(entries)
var wall_card: AssetCard = _find_card(selector.find_child("GridContainer", true, false) as GridContainer, "wall_panel_setup")
_expect(wall_card != null, "Scenery gallery offers a Wall panel setup card")
if wall_card != null:
selector.call("_on_cell_pressed", wall_card.payload())
if gui_render:
await process_frame
await process_frame
await create_timer(0.2).timeout
_capture_gui_screen("phase19_wall_setup.png")
var setup: Control = selector.find_child("SurfaceSetupView", true, false) as Control
var draw: Button = selector.find_child("DrawSurfaceButton", true, false) as Button
var cancel_setup: Button = selector.find_child("CancelSurfaceSetupButton", true, false) as Button
var chooser_colors: StageSurfaceColorControls = selector.find_child("SurfaceColorControls", true, false) as StageSurfaceColorControls
_expect(setup != null and setup.visible and draw != null, "Wall panel setup shows a reachable preview and Draw action")
_expect(draw != null and draw.get_parent() == selector.find_child("Footer", true, false), "Wall panel Draw action stays fixed in the chooser footer")
_expect(cancel_setup != null and cancel_setup.visible and cancel_setup.get_parent() == selector.find_child("Footer", true, false), "Wall panel setup keeps an explicit Cancel action in the chooser footer")
_expect(browser.selector_dim != null and browser.selector_dim.mouse_filter == Control.MOUSE_FILTER_STOP, "Clicks outside the scenery chooser are blocked from the Stage canvas")
if gui_render:
selector.hide()
await process_frame
await process_frame
_expect(browser.selector_open and selector.visible, "Unexpected popup dismissal restores the chooser without cancelling setup")
if not gui_render:
browser.selector_open = true
if cancel_setup != null:
cancel_setup.pressed.emit()
_expect(not browser.selector_open and String(controller.get_active_tool()).is_empty(), "Cancel exits the scenery chooser without starting placement")
if gui_render:
_stage.call("_open_selector", "scenery")
await process_frame
await process_frame
await create_timer(0.2).timeout
else:
browser.selector_open = true
browser.set_selector_kind("scenery")
selector.kind = "scenery"
var reopened_entries: Array[Dictionary] = browser.scan_entries("scenery")
reopened_entries.append_array(browser.call("_scenery_background_entries") as Array[Dictionary])
selector.set_entries(reopened_entries)
wall_card = _find_card(selector.find_child("GridContainer", true, false) as GridContainer, "wall_panel_setup")
_expect(wall_card != null, "Cancel returns to a fresh scenery gallery")
if wall_card != null:
selector.call("_on_cell_pressed", wall_card.payload())
if gui_render:
await process_frame
await process_frame
setup = selector.find_child("SurfaceSetupView", true, false) as Control
draw = selector.find_child("DrawSurfaceButton", true, false) as Button
chooser_colors = selector.find_child("SurfaceColorControls", true, false) as StageSurfaceColorControls
_expect(setup != null and setup.visible and draw != null and draw.visible, "Wall setup reopens after Cancel with Draw available")
var wall_looks: StagePanelChoiceGroup = selector.get_meta("wall_look_group") as StagePanelChoiceGroup
if wall_looks != null:
wall_looks.choice_selected.emit(&"finish_wall_wood")
var selected_color := Color("#734E31")
if chooser_colors != null:
chooser_colors.color_selected.emit(selected_color)
if draw != null:
draw.pressed.emit()
_expect(controller.get_active_tool() == &"wall", "Draw starts wall placement from the chooser")
var selected_wall_tint: Color = controller.get("_wall_tint")
_expect(String(controller.get_active_tool()) == "wall" and String(controller.get("_wall_finish")) == "finish_wall_wood" and selected_wall_tint.is_equal_approx(selected_color), "wall placement receives the chosen material and color")
if gui_render:
Input.warp_mouse(Vector2i(640, 260))
var wall_motion := InputEventMouseMotion.new()
wall_motion.position = Vector2(640.0, 260.0)
wall_motion.global_position = wall_motion.position
_stage.get_viewport().push_input(wall_motion)
await process_frame
await process_frame
await create_timer(0.2).timeout
var wall_ghost: StageRectSurface = controller.get("_ghost") as StageRectSurface
_expect(wall_ghost != null and wall_ghost.panel_size.is_equal_approx(Vector2(240.0, 280.0)), "wall placement displays its patterned cursor ghost after production mouse motion")
_capture_root_viewport("phase19_wall_placement.png")
controller.exit_tool()
selector.hide()
if gui_render:
_stage.call("_open_selector", "scenery")
await process_frame
await create_timer(0.2).timeout
else:
browser.set_selector_kind("scenery")
selector.kind = "scenery"
var ground_card: AssetCard = _find_card(selector.find_child("GridContainer", true, false) as GridContainer, "background_ground_setup")
_expect(ground_card != null, "Scenery gallery offers a Background ground setup card")
var ground_entry_description: String = String(ground_card.payload().get("description", "")) if ground_card != null else ""
_expect(ground_entry_description.contains("Decorative") and ground_entry_description.contains("No walking"), "Background ground card explains that it is decorative and non-walkable")
if ground_card != null:
selector.call("_on_cell_pressed", ground_card.payload())
if gui_render:
await process_frame
await create_timer(0.2).timeout
_capture_gui_screen("phase19_ground_setup.png")
var ground_looks: StagePanelChoiceGroup = selector.get_meta("ground_look_group") as StagePanelChoiceGroup
if ground_looks != null:
ground_looks.choice_selected.emit(&"finish_background_carpet")
var carpet_color := Color("#9B5C83")
if chooser_colors != null:
chooser_colors.color_selected.emit(carpet_color)
var ground_draw: Button = selector.find_child("DrawSurfaceButton", true, false) as Button
if ground_draw != null:
ground_draw.pressed.emit()
_expect(controller.get_active_tool() == &"background_ground", "Draw starts background-ground placement from the chooser")
var chosen_ground_tint: Color = controller.get("_wall_tint")
_expect(String(controller.get("_wall_finish")) == "finish_background_carpet" and chosen_ground_tint.is_equal_approx(carpet_color), "background-ground placement receives the chosen material and color")
if gui_render:
Input.warp_mouse(Vector2i(500, 260))
var ground_motion := InputEventMouseMotion.new()
ground_motion.position = Vector2(500.0, 260.0)
ground_motion.global_position = ground_motion.position
_stage.get_viewport().push_input(ground_motion)
await process_frame
await process_frame
await create_timer(0.2).timeout
var ground_ghost: StageRectSurface = controller.get("_ghost") as StageRectSurface
_expect(ground_ghost != null and ground_ghost.panel_size.is_equal_approx(Vector2(240.0, 280.0)), "background-ground placement starts with a cursor ghost the same size as a wall panel")
_capture_root_viewport("phase19_ground_placement.png")
controller.exit_tool()
selector.hide()
func _capture_gui_screen(file_name: String) -> void:
if DisplayServer.get_name() == "headless":
return
var image: Image = DisplayServer.screen_get_image()
if image == null or image.is_empty():
print("SKIP: could not capture GUI screen for %s" % file_name)
return
var path: String = "/private/tmp/" + file_name
var error: Error = image.save_png(path)
print("CAPTURE: %s size=%s result=%s" % [path, str(image.get_size()), str(error)])
func _capture_root_viewport(file_name: String) -> void:
if DisplayServer.get_name() == "headless":
return
var texture: Texture2D = root.get_texture()
if texture == null:
print("SKIP: root viewport has no capture texture for %s" % file_name)
return
var image: Image = texture.get_image()
if image == null or image.is_empty():
print("SKIP: root viewport image is empty for %s" % file_name)
return
var path: String = "/private/tmp/" + file_name
var error: Error = image.save_png(path)
print("CAPTURE: %s size=%s result=%s" % [path, str(image.get_size()), str(error)])
func _set_window_geometry(requested_size: Vector2i) -> void:
var next_size: Vector2i = requested_size
if DisplayServer.get_name() != "headless":
var usable_rect: Rect2i = DisplayServer.screen_get_usable_rect(DisplayServer.window_get_current_screen())
if usable_rect.size.x > 0 and usable_rect.size.y > 0:
next_size = Vector2i(
mini(requested_size.x, maxi(1, usable_rect.size.x - 32)),
mini(requested_size.y, maxi(1, usable_rect.size.y - 32))
)
root.size = next_size
root.position = usable_rect.position + (usable_rect.size - next_size) / 2
return
root.size = next_size
func _click_control(control: Control) -> void:
if control == null or not control.is_visible_in_tree():
return
var viewport: Viewport = control.get_viewport()
var point: Vector2 = control.get_global_rect().get_center()
var press := InputEventMouseButton.new()
press.button_index = MOUSE_BUTTON_LEFT
press.position = point
press.global_position = point
press.pressed = true
viewport.push_input(press)
await process_frame
var release := InputEventMouseButton.new()
release.button_index = MOUSE_BUTTON_LEFT
release.position = point
release.global_position = point
release.pressed = false
viewport.push_input(release)
await process_frame
func _find_card(grid: GridContainer, id: String) -> AssetCard:
if grid == null:
return null
for child: Node in grid.get_children():
var card: AssetCard = child as AssetCard
if card != null and String(card.payload().get("placement_id", "")) == id:
return card
return null
func _find_object(data: Dictionary, object_type: String) -> Dictionary:
for entry_value: Variant in data.get("objects", []):
if entry_value is Dictionary and String((entry_value as Dictionary).get("type", "")) == object_type:
return entry_value as Dictionary
return {}
func _expect(condition: bool, message: String) -> void:
if condition:
print("PASS: " + message)
else:
_failures.append(message)
push_error("FAIL: " + message)