280 lines
15 KiB
GDScript
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)
|