extends SceneTree const SANDBOX_STAGE: PackedScene = preload("res://scenes/stage/sandbox_stage.tscn") const CAPTURE_DIR: String = "res://tests/phase20b_captures/capture_overlay_review" var _stage: SandboxStage var _failures: int = 0 func _initialize() -> void: call_deferred("_run") func _run() -> void: var capture_width: int = int(OS.get_environment("PHASE20B_CAPTURE_WIDTH")) root.size = Vector2i(capture_width if capture_width > 0 else 1280, 720) root.gui_embed_subwindows = true var make_dir_error: Error = DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(CAPTURE_DIR)) if make_dir_error != OK and make_dir_error != ERR_ALREADY_EXISTS: _fail("could not create capture output directory") quit(1) return _stage = SANDBOX_STAGE.instantiate() as SandboxStage root.add_child(_stage) await _wait_frames(8) var recovery: ConfirmationDialog = _stage.find_child("AutosaveRecoveryDialog", true, false) as ConfirmationDialog if recovery != null: recovery.hide() var spawner: StageSpawner = _stage.get("_spawner") as StageSpawner var actor: StickmanRig = spawner.spawn_stickman_asset("res://content/stickmen/break.stk", Vector2(-120.0, -170.0)) var prop: PropBlock = spawner.spawn_prop_asset("crate", Vector2(150.0, 0.0)) _check(actor != null and prop != null, "production spawner creates capture fixtures") if actor == null or prop == null: await _finish() return _stage.call("_finalize_placed_node", actor) _stage.call("_finalize_placed_node", prop) var selection: StageSelection = _stage.get("_selection") as StageSelection _stage.set_mode(SandboxStage.StageMode.DIRECT) selection.select_only(actor) _stage.call("_refresh_character_settings_card") await _wait_frames(6) var character_view: StageCharacterSettingsView = _stage.get("_character_settings_view") as StageCharacterSettingsView var character_card: Control = character_view.get_card() var quick_actions: Control = character_view.find_child("QuickActionButtons", true, false) as Control var wait_button: Button = _find_button_with_text(quick_actions, "⏳ Wait") _check(character_card != null and character_card.is_visible_in_tree() and wait_button != null, "selected character inspector and quick action controls are visible") var visual_scenario: String = OS.get_environment("PHASE20B_CAPTURE_SCENARIO") if visual_scenario.is_empty(): visual_scenario = "plan" if visual_scenario == "set_down": await _send_mouse_motion(Vector2(350.0, 520.0)) _stage.call("_begin_set_down_position_capture", actor, "Click where to let go", Callable(), Callable()) await _wait_frames(3) var set_down_overlay: StageInspectorCaptureOverlay = _stage.get("_inspector_capture_overlay") as StageInspectorCaptureOverlay _check(set_down_overlay.has_capture() and set_down_overlay.get_inspector_rect().is_equal_approx(character_card.get_global_rect()), "set-down capture attaches the strip to the selected character inspector") var set_down_prompt: Label = set_down_overlay.find_child("CapturePrompt", true, false) as Label var set_down_strip: Control = set_down_overlay.find_child("CaptureCancelStrip", true, false) as Control var set_down_cancel: Button = _find_button_with_text(set_down_overlay, "Cancel") _check(set_down_prompt != null and set_down_prompt.text == "Set down prop", "set-down capture caption only names the action") _check(set_down_cancel != null and set_down_strip != null and set_down_cancel.get_global_rect().end.x <= set_down_strip.get_global_rect().end.x + 1.0, "set-down Cancel stays inside the inspector strip") var set_down_tooltip: Control = _stage.find_child("ActionTooltip", true, false) as Control _check(set_down_tooltip == null or not set_down_tooltip.visible, "set-down capture keeps instructions out of the inspector prompt") await _save_capture("set_down_capture_attached_cancel") await _send_escape() _check(int(_stage.get("_capture_kind")) == SandboxStage.CaptureKind.NONE and not set_down_overlay.has_capture(), "Escape cancels set-down capture and removes the dim layer") print("PHASE20B_CAPTURE_OVERLAY_RESULT failures=%d captures=%s" % [_failures, CAPTURE_DIR]) await _finish() return if visual_scenario == "use_spot": _stage.set_mode(SandboxStage.StageMode.EDIT) selection.select_only(prop) _stage.call("_refresh_prop_settings_card") await _wait_frames(3) _stage.call("_begin_spot_edit", prop, "") await _wait_frames(3) var spot_overlay: StageInspectorCaptureOverlay = _stage.get("_inspector_capture_overlay") as StageInspectorCaptureOverlay var prop_card: Control = (_stage.get("_prop_settings_view") as StagePropSettingsView).get_card() _check(spot_overlay.has_capture() and spot_overlay.get_inspector_rect().is_equal_approx(prop_card.get_global_rect()), "Use-spot capture attaches the strip to the selected prop inspector") var spot_instruction: String = String((_stage.get("_use_spot_overlay") as StageUseSpotOverlay).get("_instruction")) _check(spot_instruction.contains("Press Escape or right-click to cancel"), "Use-spot cursor pill includes cancellation instructions") _check(not (_stage.get("_chrome") as StageChrome).get_tooltip().visible, "Use-spot capture avoids a duplicate Stage tooltip") await _save_capture("use_spot_capture_attached_cancel") var spot_scrim: Control = spot_overlay.find_child("InspectorInputBlocker", true, false) as Control if spot_scrim != null: await _send_right_click(spot_scrim.get_global_rect().get_center()) await _wait_frames(3) _check(not spot_overlay.has_capture() and not bool(_stage.get("_spot_edit_active")), "right-click cancels Use-spot placement") print("PHASE20B_CAPTURE_OVERLAY_RESULT failures=%d captures=%s" % [_failures, CAPTURE_DIR]) await _finish() return if visual_scenario == "prop_target": await _send_mouse_motion(Vector2(350.0, 520.0)) _stage.call("_begin_capture", SandboxStage.CaptureKind.PROP, "Choose a prop for break", Callable(), Callable()) await _wait_frames(3) var prop_overlay: StageInspectorCaptureOverlay = _stage.get("_inspector_capture_overlay") as StageInspectorCaptureOverlay _check(prop_overlay.has_capture() and prop_overlay.get_inspector_rect().is_equal_approx(character_card.get_global_rect()), "prop-target capture attaches Cancel to the character inspector") _check_capture_tooltip_layout(prop_overlay, _stage.get("_chrome") as StageChrome) await _save_capture("prop_target_capture_attached_cancel") await _send_escape() _check(int(_stage.get("_capture_kind")) == SandboxStage.CaptureKind.NONE and not prop_overlay.has_capture(), "Escape cancels prop-target capture") print("PHASE20B_CAPTURE_OVERLAY_RESULT failures=%d captures=%s" % [_failures, CAPTURE_DIR]) await _finish() return if visual_scenario == "use_target": var empty_preview_prefix: Array[Dictionary] = [] _stage.call("_begin_use_target_capture", "direct", actor, empty_preview_prefix, true) _stage.call("_select_use_target_prop", prop, Vector2.ZERO) await _wait_frames(3) var use_overlay: StageInspectorCaptureOverlay = _stage.get("_inspector_capture_overlay") as StageInspectorCaptureOverlay var use_prompt: Label = use_overlay.find_child("CapturePrompt", true, false) as Label var use_strip: Control = use_overlay.find_child("CaptureCancelStrip", true, false) as Control var use_actions: Control = use_overlay.find_child("CaptureActions", true, false) as Control var use_cancel: Button = _find_button_with_text(use_overlay, "Cancel") _check(use_prompt != null and use_prompt.text == "Use", "Use target capture keeps a short action caption") _check(use_strip != null and use_strip.size.y >= 112.0 and use_actions != null and use_actions.get_global_rect().position.y >= use_prompt.get_global_rect().position.y, "multiple capture actions stack below the title") _check(use_cancel != null and use_cancel.visible and use_cancel.custom_minimum_size.y >= 44.0 and use_cancel.get_global_rect().end.x <= use_strip.get_global_rect().end.x + 1.0, "stacked Cancel remains visible and touch-sized") _check_capture_tooltip_layout(use_overlay, _stage.get("_chrome") as StageChrome) await _save_capture("use_target_capture_stacked_actions") _stage.call("_cancel_capture") _check(not use_overlay.has_capture(), "Cancel closes the stacked Use-target capture") print("PHASE20B_CAPTURE_OVERLAY_RESULT failures=%d captures=%s" % [_failures, CAPTURE_DIR]) await _finish() return await _send_mouse_motion(Vector2(350.0, 520.0)) _stage.call("_begin_capture", SandboxStage.CaptureKind.PLAN_PLACE, "Choose a place for break", Callable(), Callable()) await _wait_frames(3) var overlay: StageInspectorCaptureOverlay = _stage.get("_inspector_capture_overlay") as StageInspectorCaptureOverlay var cancel_button: Button = overlay.find_child("Capture_cancel_capture", true, false) as Button _check(overlay.has_capture() and overlay.visible and not (_stage.get("_use_target_capture_bar") as StageUseTargetCaptureBar).visible, "place capture shows the attached inspector strip instead of a detached capture bar") _check(overlay.get_inspector_rect().is_equal_approx(character_card.get_global_rect()), "capture dimmer aligns with the selected character inspector") _check(cancel_button != null and cancel_button.visible and cancel_button.custom_minimum_size.y >= 44.0 and cancel_button.has_focus(), "Cancel is visible, touch-sized, and owns keyboard focus") var tooltip: Control = _stage.find_child("ActionTooltip", true, false) as Control var chrome: StageChrome = _stage.get("_chrome") as StageChrome _check(tooltip != null and tooltip.visible and String(tooltip.get("text")).contains("Press Escape or right-click to cancel"), "cursor hint includes the full cancel shortcuts") if tooltip != null and chrome != null: _check(not tooltip.get_global_rect().intersects(overlay.get_inspector_rect()), "cursor hint avoids the active inspector") _check(not tooltip.get_global_rect().intersects(chrome.get_timestamp_badge().get_global_rect()), "cursor hint avoids the timer badge") await _save_capture("place_capture_attached_cancel") if wait_button != null: await _send_click(wait_button.get_global_rect().get_center()) _check(int(_stage.get("_capture_kind")) == SandboxStage.CaptureKind.PLAN_PLACE and not (_stage.get("_wait_dialog") as Window).visible, "dimmed inspector controls cannot be activated by clicking through the scrim") if cancel_button != null: await _send_click(cancel_button.get_global_rect().get_center()) await _wait_frames(2) _check(int(_stage.get("_capture_kind")) == SandboxStage.CaptureKind.NONE and not overlay.has_capture(), "attached Cancel resolves through the production capture cancel path") if visual_scenario == "plan": print("PHASE20B_CAPTURE_OVERLAY_RESULT failures=%d captures=%s" % [_failures, CAPTURE_DIR]) await _finish() return _stage.call("_begin_set_down_position_capture", actor, "Click where to let go", Callable(), Callable()) await _wait_frames(2) _check(overlay.has_capture(), "set-down target capture uses the same inspector cancel strip") await _save_capture("set_down_capture_attached_cancel") var escape: InputEventKey = InputEventKey.new() escape.keycode = KEY_ESCAPE escape.pressed = true Input.parse_input_event(escape) await _wait_frames(3) _check(int(_stage.get("_capture_kind")) == SandboxStage.CaptureKind.NONE and not overlay.has_capture(), "Escape cancels set-down capture and removes the dim layer") _stage.set_mode(SandboxStage.StageMode.EDIT) selection.select_only(prop) _stage.call("_refresh_prop_settings_card") await _wait_frames(3) _stage.call("_begin_spot_edit", prop, "") await _wait_frames(3) _check(overlay.has_capture() and overlay.get_inspector_rect().is_equal_approx((_stage.get("_prop_settings_view") as StagePropSettingsView).get_card().get_global_rect()), "Use-spot placement attaches Cancel to and dims the prop inspector") await _save_capture("use_spot_capture_attached_cancel") var scrim: Control = overlay.find_child("InspectorInputBlocker", true, false) as Control if scrim != null: await _send_right_click(scrim.get_global_rect().get_center()) await _wait_frames(3) _check(not overlay.has_capture() and not bool(_stage.get("_spot_edit_active")), "right-clicking the dimmed inspector cancels spot placement") print("PHASE20B_CAPTURE_OVERLAY_RESULT failures=%d captures=%s" % [_failures, CAPTURE_DIR]) await _finish() func _find_button_with_text(root_node: Node, text: String) -> Button: if root_node == null: return null for node: Node in root_node.find_children("*", "Button", true, false): var button: Button = node as Button if button != null and button.text == text: return button return null func _check_capture_tooltip_layout(overlay: StageInspectorCaptureOverlay, chrome: StageChrome) -> void: var tooltip: Control = _stage.find_child("ActionTooltip", true, false) as Control _check(tooltip != null and tooltip.visible and String(tooltip.get("text")).contains("Press Escape or right-click to cancel"), "capture tooltip includes the Escape and right-click cancel hint") if tooltip == null or chrome == null or overlay == null: return _check(not tooltip.get_global_rect().intersects(overlay.get_inspector_rect()), "capture tooltip avoids the active inspector") _check(not tooltip.get_global_rect().intersects(chrome.get_timestamp_badge().get_global_rect()), "capture tooltip avoids the timer badge") func _send_click(position: Vector2) -> void: var motion: InputEventMouseMotion = InputEventMouseMotion.new() motion.position = position motion.global_position = position Input.parse_input_event(motion) await process_frame var down: InputEventMouseButton = InputEventMouseButton.new() down.button_index = MOUSE_BUTTON_LEFT down.pressed = true down.position = position down.global_position = position Input.parse_input_event(down) await process_frame var up: InputEventMouseButton = InputEventMouseButton.new() up.button_index = MOUSE_BUTTON_LEFT up.pressed = false up.position = position up.global_position = position Input.parse_input_event(up) await _wait_frames(2) func _send_mouse_motion(position: Vector2) -> void: var motion: InputEventMouseMotion = InputEventMouseMotion.new() motion.position = position motion.global_position = position Input.parse_input_event(motion) await _wait_frames(2) func _send_right_click(position: Vector2) -> void: var event: InputEventMouseButton = InputEventMouseButton.new() event.button_index = MOUSE_BUTTON_RIGHT event.pressed = true event.position = position event.global_position = position Input.parse_input_event(event) await _wait_frames(3) func _send_escape() -> void: var escape: InputEventKey = InputEventKey.new() escape.keycode = KEY_ESCAPE escape.pressed = true Input.parse_input_event(escape) await _wait_frames(3) func _save_capture(file_name: String) -> void: await RenderingServer.frame_post_draw var image: Image = root.get_texture().get_image() var error: Error = image.save_png(ProjectSettings.globalize_path(CAPTURE_DIR.path_join(file_name + ".png"))) _check(error == OK and image.get_size() == root.size, "rendered %s capture saves at viewport size" % file_name) func _wait_frames(count: int) -> void: for _index: int in count: await process_frame await RenderingServer.frame_post_draw func _check(condition: bool, message: String) -> void: if condition: print("PHASE20B_CAPTURE_OVERLAY_CHECK PASS: %s" % message) else: _fail(message) func _fail(message: String) -> void: _failures += 1 push_error("PHASE20B_CAPTURE_OVERLAY_CHECK FAIL: %s" % message) func _finish() -> void: if _stage != null and is_instance_valid(_stage): _stage.queue_free() await process_frame quit(1 if _failures > 0 else 0)