Files
stickman/tests/phase20b_capture_overlay_visual_verify.gd
T

280 lines
15 KiB
GDScript

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)