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)