2543 lines
189 KiB
GDScript
2543 lines
189 KiB
GDScript
extends SceneTree
|
||
|
||
const SANDBOX_STAGE: PackedScene = preload("res://scenes/stage/sandbox_stage.tscn")
|
||
const STAGE_DIALOG_STYLER := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
|
||
const ACTION_REGISTRY := preload("res://scripts/stage/action_registry.gd")
|
||
const CAPTURE_DIR: String = "res://tests/phase20b_captures/review_round43"
|
||
const PLAN_PLACE: int = 8
|
||
const PLAN_PROP: int = 9
|
||
|
||
var _failures: int = 0
|
||
var _active_capture_dir: String = CAPTURE_DIR
|
||
var _debug_stage: SandboxStage
|
||
var _capture_after_next_click: bool = false
|
||
var _skip_capture_hover: bool = false
|
||
var _trace_next_click: bool = false
|
||
var _last_click_label: String = ""
|
||
var _last_injected_mouse_position: Vector2 = Vector2.ZERO
|
||
var _watchdog: Timer
|
||
|
||
|
||
func _initialize() -> void:
|
||
call_deferred("_run_scenarios")
|
||
|
||
|
||
func _run_scenarios() -> void:
|
||
_skip_capture_hover = OS.get_environment("PHASE20B_SKIP_CAPTURE_HOVER") == "1"
|
||
var scenario: String = OS.get_environment("PHASE20B_GUI_SCENARIO")
|
||
var requested_capture_dir: String = OS.get_environment("PHASE20B_GUI_CAPTURE_DIR")
|
||
_active_capture_dir = requested_capture_dir if not requested_capture_dir.is_empty() else ("res://tests/phase20b_captures/review_round44" if scenario == "busy_scene_badge_stability" else CAPTURE_DIR)
|
||
_watchdog = Timer.new()
|
||
_watchdog.one_shot = true
|
||
_watchdog.wait_time = 45.0
|
||
_watchdog.timeout.connect(_on_gui_watchdog_timeout)
|
||
root.add_child(_watchdog)
|
||
_watchdog.start()
|
||
root.size = Vector2i(1280, 900) if scenario == "action_drawer" else Vector2i(1280, 720)
|
||
root.position = Vector2i.ZERO
|
||
root.gui_embed_subwindows = true
|
||
root.grab_focus()
|
||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(_active_capture_dir))
|
||
var stage: SandboxStage = SANDBOX_STAGE.instantiate() as SandboxStage
|
||
_debug_stage = stage
|
||
root.add_child(stage)
|
||
await _wait_frames(10)
|
||
var recovery_dialog: ConfirmationDialog = stage.find_child("AutosaveRecoveryDialog", true, false) as ConfirmationDialog
|
||
if recovery_dialog != null and STAGE_DIALOG_STYLER.is_open(recovery_dialog):
|
||
# Keep the user's autosave intact while allowing this isolated GUI capture
|
||
# to exercise the production inspector underneath the startup prompt.
|
||
STAGE_DIALOG_STYLER.hide(recovery_dialog)
|
||
if scenario == "busy_scene_badge_stability":
|
||
await _run_busy_scene_badge_stability(stage)
|
||
print("PHASE20B_GUI_SLICE_RESULT scenario=%s failures=%d captures=%s" % [scenario, _failures, _active_capture_dir])
|
||
await _finish(stage)
|
||
quit(1 if _failures > 0 else 0)
|
||
return
|
||
if scenario == "menu_focused_stack":
|
||
await _run_menu_focused_stack_slice(stage)
|
||
print("PHASE20B_GUI_SLICE_RESULT scenario=%s failures=%d captures=%s" % [scenario, _failures, _active_capture_dir])
|
||
await _finish(stage)
|
||
quit(1 if _failures > 0 else 0)
|
||
return
|
||
stage.call("_on_grid_toggled", false)
|
||
var spawner: StageSpawner = stage.get("_spawner") as StageSpawner
|
||
var world: Node2D = stage.get_node("World") as Node2D
|
||
var character: 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(character != null and prop != null, "production spawner creates the actor and named-prop fixtures")
|
||
if character == null or prop == null:
|
||
await _finish(stage)
|
||
quit(1)
|
||
return
|
||
stage.call("_finalize_placed_node", character)
|
||
stage.call("_finalize_placed_node", prop)
|
||
var identity: StageObjectIdentity = stage.get("_object_identity") as StageObjectIdentity
|
||
var renamed_character: Dictionary = identity.rename(character, "Milo the Magnificent Fetch Director", world)
|
||
var renamed_prop: Dictionary = identity.rename(prop, "Blue Fetch Ball", world)
|
||
_check(bool(renamed_character.get("ok", false)) and bool(renamed_prop.get("ok", false)), "fixture actors receive long and distinct stage names through production identity")
|
||
_make_small_prop(prop)
|
||
await _wait_frames(5)
|
||
|
||
# Mode and selection are driven through the same visible controls and viewport
|
||
# pointer routes used by the editor.
|
||
var direct_button: Button = _find_button_containing(stage, "Direct")
|
||
_check(direct_button != null, "DIRECT mode button is present in the Stage chrome")
|
||
if direct_button != null:
|
||
await _click_control(direct_button)
|
||
var selection: StageSelection = stage.get("_selection") as StageSelection
|
||
await _send_click(_screen_at(stage, character.global_position))
|
||
await _wait_frames(4)
|
||
_check(int(stage.get("current_mode")) == SandboxStage.StageMode.DIRECT, "pointer activation switches the production Stage to DIRECT")
|
||
_check(selection.get_selected().size() == 1 and selection.get_selected()[0] == character, "viewport pointer selects the actor in DIRECT")
|
||
var character_view: StageCharacterSettingsView = stage.get("_character_settings_view") as StageCharacterSettingsView
|
||
var card: StageInspectorPanel = character_view.get_card() as StageInspectorPanel
|
||
var stack: Control = character_view.find_child("ActionStepStack", true, false) as Control
|
||
var empty_label: Label = character_view.find_child("EmptyActionPlan", true, false) as Label
|
||
var quick_row: Control = character_view.find_child("QuickActionButtons", true, false) as Control
|
||
var empty_prompt_rect: Rect2 = empty_label.get_global_rect() if empty_label != null else Rect2()
|
||
_check(stack != null and stack.visible and empty_label != null and empty_label.visible and empty_label.text == "Choose an action to start %s’s plan." % character.stage_name and empty_prompt_rect.size.x > 0.0 and stack.get_global_rect().encloses(empty_prompt_rect), "approved empty-plan prompt is visible and fits inside the Step Stack")
|
||
_check(quick_row != null and quick_row.visible and _find_button_with_text(quick_row, "🚶 Move") != null and _find_button_with_text(quick_row, "💬 Say") != null and _find_button_with_text(quick_row, "⏳ Wait") != null, "pinned Move, Say, and Wait quick actions are visible")
|
||
await _save_capture("wide_empty_step_stack")
|
||
if not scenario.is_empty():
|
||
await _run_visual_slice(scenario, stage, character, prop, character_view, card, quick_row)
|
||
print("PHASE20B_GUI_SLICE_RESULT scenario=%s failures=%d" % [scenario, _failures])
|
||
await _finish(stage)
|
||
quit(1 if _failures > 0 else 0)
|
||
return
|
||
|
||
# Author a place Move via the Quick Move button, the Action Editor, and the
|
||
# transient stage capture bar. The click is sent through SandboxStage input.
|
||
stage.call("_set_scene_dirty", false)
|
||
var move_button: Button = _find_button_with_text(quick_row, "🚶 Move")
|
||
await _click_control(move_button)
|
||
var action_editor: ActionEditor = stage.get("_action_editor") as ActionEditor
|
||
var place_button: Button = _find_button_containing(action_editor, "Choose a place")
|
||
_check(action_editor != null and action_editor.visible and place_button != null, "Quick Move opens the production Action Editor with a place route")
|
||
if place_button == null:
|
||
await _finish(stage)
|
||
quit(1)
|
||
return
|
||
var place_press_events: Array[bool] = []
|
||
place_button.pressed.connect(func() -> void: place_press_events.append(true))
|
||
_capture_after_next_click = true
|
||
await _click_control(place_button)
|
||
_check(place_press_events.size() == 1, "Choose a place button receives the actual injected mouse click")
|
||
_check(int(stage.get("_capture_kind")) == PLAN_PLACE, "place button enters the production temporary capture state")
|
||
var first_place: Vector2 = Vector2(-20.0, 0.0)
|
||
_trace_next_click = true
|
||
await _send_click(_screen_at(stage, first_place))
|
||
await _wait_frames(3)
|
||
_check(action_editor.visible, "captured place returns to the Action Editor for confirmation")
|
||
var move_done: Button = _find_button_with_text(action_editor, "Done")
|
||
await _click_control(move_done)
|
||
await _wait_frames(4)
|
||
var plan: Array[Dictionary] = character.get_action_plan()
|
||
if plan.is_empty():
|
||
_check(false, "valid Move is added before continuing GUI scenarios")
|
||
await _finish(stage)
|
||
quit(1)
|
||
return
|
||
_check(plan.size() == 1 and (plan[0]["actions"] as Array).size() == 1, "Done adds one ordered Move step through the UI")
|
||
var first_move: Dictionary = (plan[0]["actions"] as Array)[0]
|
||
var move_id: String = String(first_move.get("action_id", ""))
|
||
var move_summary: Button = _find_button_containing(character_view.find_child("ActionStep_1", true, false), "Place 1")
|
||
_check(move_summary != null and not move_summary.text.contains("(-"), "free-place action summary uses its derived step label without raw coordinates")
|
||
var move_row_text: Label = character_view.find_child("ActionSummaryText_%s" % move_id, true, false) as Label
|
||
var move_row_rail: Control = character_view.find_child("ActionAccentRail_%s" % move_id, true, false) as Control
|
||
var move_icon_tile: Control = character_view.find_child("ActionIconTile", true, false) as Control
|
||
var move_row_options: Label = character_view.find_child("ActionSummaryOptions_%s" % move_id, true, false) as Label
|
||
var move_row_style: StyleBoxFlat = move_summary.get_theme_stylebox("normal") as StyleBoxFlat if move_summary != null else null
|
||
_check(move_row_text != null and move_row_text.text == "Walk to Place 1" and move_row_text.get_theme_font_size("font_size") == 18, "Move summary uses the mockup's bold 18 px action text without a redundant type chip")
|
||
_check(move_row_rail != null and move_row_rail.custom_minimum_size.x == 4.0 and move_icon_tile != null and move_icon_tile.custom_minimum_size == Vector2(32.0, 32.0) and move_row_options != null and move_row_options.text == "..." and move_summary.custom_minimum_size.y >= 48.0, "Move row has the twin rail, 32 px icon tile, 48 px target, and trailing options cue")
|
||
_check(move_row_style != null and move_row_style.bg_color.is_equal_approx(Color("#17313D")), "Move row uses the themed muted cyan tint from the approved mockup")
|
||
await RenderingServer.frame_post_draw
|
||
var move_row_image: Image = root.get_texture().get_image()
|
||
var move_rail_rect: Rect2 = move_row_rail.get_global_rect() if move_row_rail != null else Rect2()
|
||
var move_rail_pixel: Color = move_row_image.get_pixelv(Vector2i(roundi(move_rail_rect.get_center().x), roundi(move_rail_rect.get_center().y))) if move_row_image != null and not move_row_image.is_empty() and move_row_rail != null else Color.TRANSPARENT
|
||
_check(move_rail_pixel.is_equal_approx(Color("#8BD9FF")), "rendered Move accent rail uses the cyan route color")
|
||
_check(String((first_move.get("destination", {}) as Dictionary).get("kind", "")) == "place", "captured free destination is persisted as kind place")
|
||
_check(bool(stage.get("_dirty")), "valid Move commit marks the document dirty")
|
||
await _save_capture("wide_step_1_place")
|
||
|
||
# Click the rendered numbered flag. It must select/focus the same stable action
|
||
# without making a document edit.
|
||
stage.call("_set_scene_dirty", false)
|
||
await _send_click(_screen_at(stage, first_place))
|
||
await _wait_frames(4)
|
||
var move_options: Control = character_view.find_child("ActionOptions_%s" % move_id, true, false) as Control
|
||
_check(move_options != null and move_options.visible, "numbered route flag opens the matching action options by stable ID")
|
||
var expanded_card_rect: Rect2 = card.get_global_rect()
|
||
var pinned_move_rect: Rect2 = move_button.get_global_rect()
|
||
_check(expanded_card_rect.position.x >= 0.0 and expanded_card_rect.end.x <= root.size.x and expanded_card_rect.size.x <= 380.0, "expanded action options stay inside the 380 px inspector shell")
|
||
_check(move_button.visible and pinned_move_rect.position.y >= expanded_card_rect.position.y and pinned_move_rect.end.y <= expanded_card_rect.end.y, "quick actions stay visible in the fixed inspector area after expanding action options")
|
||
var route_entry: Dictionary = {}
|
||
for projection: Dictionary in (stage.get("_director_visuals") as StageDirectorVisuals)._plan_projection(character):
|
||
if String(projection.get("action_id", "")) == move_id:
|
||
route_entry = projection
|
||
break
|
||
var route_hit_rect: Rect2 = route_entry.get("hit_rect", Rect2())
|
||
var route_hit_size: Vector2 = route_hit_rect.size * (stage.get("_director_visuals") as StageDirectorVisuals)._zoom()
|
||
_check(route_hit_size.x >= 44.0 and route_hit_size.y >= 44.0, "numbered movement flag hit target is at least 44×44 screen pixels")
|
||
_check(not bool(stage.get("_dirty")), "opening route flag options does not dirty the scene")
|
||
await _save_capture("wide_flag_focused_action")
|
||
|
||
# Edit the Move action to choose an exact prop, then a nearby free place.
|
||
# The action options should match Say's stacked Edit/Erase layout.
|
||
var move_edit_button: Button = _find_button_with_text(move_options, "Edit action")
|
||
var move_erase_button: Button = _find_button_with_text(move_options, "Erase action")
|
||
var duplicate_change_place: Button = _find_button_with_text(move_options, "Change place")
|
||
_check(duplicate_change_place == null and move_edit_button != null and move_erase_button != null, "Move options match Say with only Edit action and Erase action")
|
||
_check(move_edit_button != null and move_erase_button != null and move_edit_button.get_parent() == move_options and move_erase_button.get_parent() == move_options and move_edit_button.get_global_rect().size.x >= 44.0 and move_edit_button.get_global_rect().size.x == move_erase_button.get_global_rect().size.x and move_edit_button.get_global_rect().end.y <= move_erase_button.get_global_rect().position.y, "Move Edit and Erase actions are full-width controls stacked vertically")
|
||
if move_edit_button != null:
|
||
await _click_control(move_edit_button)
|
||
var choose_prop_button: Button = _find_button_containing(action_editor, "Choose a prop")
|
||
_check(action_editor.visible and choose_prop_button != null, "Edit action opens the Move destination editor")
|
||
if choose_prop_button != null:
|
||
await _click_control(choose_prop_button)
|
||
_check(int(stage.get("_capture_kind")) == PLAN_PROP, "prop choice enters the production prop-only capture route")
|
||
await _send_click(_screen_at(stage, prop.global_position))
|
||
await _wait_frames(4)
|
||
await _save_capture("wide_prop_candidate_route")
|
||
var done_prop: Button = _find_button_with_text(action_editor, "Done")
|
||
await _click_control(done_prop)
|
||
await _wait_frames(4)
|
||
plan = character.get_action_plan()
|
||
first_move = (plan[0]["actions"] as Array)[0]
|
||
var prop_destination: Dictionary = first_move.get("destination", {})
|
||
_check(String(first_move.get("action_id", "")) == move_id, "retargeting preserves the stable Move action ID")
|
||
_check(String(prop_destination.get("kind", "")) == "prop" and String(prop_destination.get("target_prop_id", "")) == prop.stage_object_id, "named-prop target stores the exact selected stage ID")
|
||
_check(prop_destination.get("approach_offset", Vector2.ZERO) is Vector2, "captured prop approach retains a world-space offset")
|
||
await _save_capture("wide_step_1_exact_prop")
|
||
|
||
# Retarget to a free place near the prop, then cancel a second capture with
|
||
# Escape and prove the draft and dirty state remain unchanged.
|
||
move_options = character_view.find_child("ActionOptions_%s" % move_id, true, false) as Control
|
||
move_edit_button = _find_button_with_text(move_options, "Edit action")
|
||
await _click_control(move_edit_button)
|
||
place_button = _find_button_containing(action_editor, "place")
|
||
await _click_control(place_button)
|
||
var nearby_place: Vector2 = prop.global_position + Vector2(54.0, -42.0)
|
||
await _send_click(_screen_at(stage, nearby_place))
|
||
await _wait_frames(3)
|
||
await _click_control(_find_button_with_text(action_editor, "Done"))
|
||
await _wait_frames(4)
|
||
plan = character.get_action_plan()
|
||
first_move = (plan[0]["actions"] as Array)[0]
|
||
var free_destination: Dictionary = first_move.get("destination", {})
|
||
_check(String(first_move.get("action_id", "")) == move_id and String(free_destination.get("kind", "")) == "place", "nearby free-place intent remains a place and keeps the same action ID")
|
||
stage.call("_set_scene_dirty", false)
|
||
move_options = character_view.find_child("ActionOptions_%s" % move_id, true, false) as Control
|
||
await _click_control(_find_button_with_text(move_options, "Edit action"))
|
||
await _click_control(_find_button_containing(action_editor, "place"))
|
||
_check(int(stage.get("_capture_kind")) == PLAN_PLACE, "second target edit enters capture before cancellation")
|
||
await _save_capture("wide_candidate_route_before_cancel")
|
||
await _send_key(KEY_ESCAPE)
|
||
await _wait_frames(4)
|
||
_check(int(stage.get("_capture_kind")) == 0 and not action_editor.visible, "Escape cancels the active destination capture through Stage input")
|
||
_check(not bool(stage.get("_dirty")) and character.get_action_plan()[0]["actions"] == plan[0]["actions"], "cancel leaves saved plan content and dirty state unchanged")
|
||
await _save_capture("wide_capture_cancelled")
|
||
|
||
# The Add together popup is exercised with keyboard input; a Move step offers
|
||
# Say and excludes other primary actions. The text is entered through the focused composer.
|
||
var together_button: Button = _find_button_with_text(character_view.find_child("ActionStep_1", true, false), "+ Add together")
|
||
_check(together_button != null, "singleton Move step offers Add together")
|
||
await _click_control(together_button)
|
||
var together_popup: Panel = character_view.find_child("AddTogetherChoices", true, false) as Panel
|
||
var together_choices: VBoxContainer = together_popup.find_child("TogetherChoiceButtons", true, false) as VBoxContainer if together_popup != null else null
|
||
var together_helper: Label = together_popup.find_child("TogetherCompatibilityHint", true, false) as Label if together_popup != null else null
|
||
var together_cancel: Button = _find_button_with_text(together_popup, "Cancel") if together_popup != null else null
|
||
_check(together_popup != null and together_popup.visible and together_choices != null and together_choices.get_child_count() == 1 and (together_choices.get_child(0) as Button).text.contains("Say"), "Move compatibility picker offers only Say")
|
||
_check(together_choices != null and _find_button_containing(together_choices, "Wait") == null, "Move compatibility picker excludes Wait")
|
||
_check(together_helper == null and together_cancel != null and together_cancel.visible and together_cancel.size.x >= 44.0 and together_cancel.size.y >= 44.0, "Add together picker omits redundant compatibility copy and keeps a visible touch-sized Cancel")
|
||
await _save_capture("wide_add_together_picker")
|
||
await _send_key(KEY_ENTER)
|
||
await _wait_frames(5)
|
||
var composer: Control = stage.get("_speech_composer") as Control
|
||
var message_editor: TextEdit = null
|
||
var speech_toolbar: Control = null
|
||
var done_speech: Button = null
|
||
_check(composer != null and composer.visible, "keyboard selection opens the production speech composer")
|
||
if composer != null and composer.visible:
|
||
message_editor = composer.find_child("ActiveBubbleText", true, false) as TextEdit
|
||
_check(message_editor != null and message_editor.has_focus(), "speech composer gives its message editor keyboard focus")
|
||
if message_editor != null:
|
||
await _click_control(message_editor)
|
||
await _send_key(KEY_A, false, true)
|
||
await _type_text("Fetch this ball, Milo! Fetch it now!")
|
||
speech_toolbar = stage.get("_speech_show_toolbar") as Control
|
||
done_speech = _find_button_with_text(speech_toolbar, "✓ Done speech")
|
||
_check(done_speech != null and done_speech.visible, "speech composer exposes its labeled Done speech action")
|
||
await _save_capture("wide_grouped_say_composer")
|
||
if done_speech != null:
|
||
await _click_control(done_speech)
|
||
await _wait_frames(5)
|
||
plan = character.get_action_plan()
|
||
_check((plan[0]["actions"] as Array).size() == 2, "speech Done adds Say to the selected Move step")
|
||
_check(_step_has_action_type(plan[0], "speak"), "authored together speech is a real grouped action")
|
||
await _save_capture("wide_step_1_together")
|
||
|
||
# Add Wait as the next step, group Say with that Wait, and add more future
|
||
# steps via the pinned quick action. This also builds the compact-scroll case.
|
||
var wait_button: Button = _find_button_with_text(quick_row, "⏳ Wait")
|
||
await _click_control(wait_button)
|
||
await _wait_frames(2)
|
||
await _click_control(_find_button_with_text(action_editor, "Done"))
|
||
await _wait_frames(4)
|
||
plan = character.get_action_plan()
|
||
_check(plan.size() == 2 and String(((plan[1]["actions"] as Array)[0] as Dictionary).get("type", "")) == "wait", "quick Wait appends a separate second step")
|
||
var wait_together: Button = _find_button_with_text(character_view.find_child("ActionStep_2", true, false), "+ Add together")
|
||
await _click_control(wait_together)
|
||
together_popup = character_view.find_child("AddTogetherChoices", true, false) as Panel
|
||
together_choices = together_popup.find_child("TogetherChoiceButtons", true, false) as VBoxContainer if together_popup != null else null
|
||
_check(together_popup != null and together_popup.visible and together_choices != null and together_choices.get_child_count() == 1 and (together_choices.get_child(0) as Button).text.contains("Say"), "Wait compatibility picker offers Say as its only companion")
|
||
await _send_key(KEY_ENTER)
|
||
await _wait_frames(5)
|
||
composer = stage.get("_speech_composer") as Control
|
||
if composer != null and composer.visible:
|
||
message_editor = composer.find_child("ActiveBubbleText", true, false) as TextEdit
|
||
if message_editor != null:
|
||
await _click_control(message_editor)
|
||
await _send_key(KEY_A, false, true)
|
||
await _type_text("Fetch fetch fetch fetch fetch fetch fetch fetch fetch fetch fetch fetch")
|
||
speech_toolbar = stage.get("_speech_show_toolbar") as Control
|
||
done_speech = _find_button_with_text(speech_toolbar, "✓ Done speech")
|
||
if done_speech != null:
|
||
await _click_control(done_speech)
|
||
await _wait_frames(4)
|
||
plan = character.get_action_plan()
|
||
_check((plan[1]["actions"] as Array).size() == 2 and _step_has_action_type(plan[1], "speak"), "Wait+Say group commits to one concurrent step")
|
||
|
||
# More singleton waits are added through the quick action and Done button.
|
||
for _index: int in range(6):
|
||
await _click_control(_find_button_with_text(quick_row, "⏳ Wait"))
|
||
await _wait_frames(2)
|
||
await _click_control(_find_button_with_text(action_editor, "Done"))
|
||
await _wait_frames(3)
|
||
plan = character.get_action_plan()
|
||
_check(plan.size() == 8, "quick actions create eight ordered steps without bypassing the production UI")
|
||
|
||
# Reorder the future second step through its up arrow and back again;
|
||
# stable action IDs remain intact. Keyboard focus is checked on the step row.
|
||
var original_second_id: String = String(((plan[1]["actions"] as Array)[0] as Dictionary).get("action_id", ""))
|
||
var second_card: Control = character_view.find_child("ActionStep_2", true, false) as Control
|
||
var earlier: Button = _find_button_with_tooltip(second_card, "Move Step 2 earlier")
|
||
_check(earlier != null and earlier.text == "↑" and earlier.size.x >= 48.0 and earlier.size.y >= 48.0 and not earlier.disabled, "future step shows a touch-sized up arrow with a descriptive tooltip")
|
||
if earlier != null:
|
||
await _click_control(earlier)
|
||
await _wait_frames(4)
|
||
plan = character.get_action_plan()
|
||
_check(String(((plan[0]["actions"] as Array)[0] as Dictionary).get("action_id", "")) == original_second_id, "up arrow changes authored order while retaining stable action identity")
|
||
var first_card: Control = character_view.find_child("ActionStep_1", true, false) as Control
|
||
var later: Button = _find_button_with_tooltip(first_card, "Move Step 1 later")
|
||
if later != null:
|
||
await _click_control(later)
|
||
await _wait_frames(4)
|
||
plan = character.get_action_plan()
|
||
_check(String(((plan[1]["actions"] as Array)[0] as Dictionary).get("action_id", "")) == original_second_id, "down arrow restores the original future-step order")
|
||
|
||
# At 920x560, the card keeps its header and pinned quick actions in view and
|
||
# scrolls the long plan body. The long actor name exercises header fitting.
|
||
root.size = Vector2i(920, 560)
|
||
await _wait_frames(8)
|
||
var body_scroll: ScrollContainer = card.find_child("InspectorBodyScroll", true, false) as ScrollContainer
|
||
_check(card.visible and card.get_global_rect().position.x >= 0.0 and card.get_global_rect().end.x <= 920.0, "small viewport keeps the character card inside the horizontal safe area")
|
||
_check(quick_row.visible and quick_row.get_global_rect().position.y >= 0.0 and quick_row.get_global_rect().end.y <= 560.0, "small viewport preserves visible pinned quick actions")
|
||
_check(body_scroll != null and body_scroll.get_v_scroll_bar().max_value > 0.0, "eight-step plan scrolls inside the inspector body on the compact viewport")
|
||
await _save_capture("compact_920x560_eight_steps")
|
||
|
||
# Cancel a speech draft with the visible labeled control; it must not append
|
||
# an action or mark an otherwise-clean plan dirty.
|
||
stage.call("_set_scene_dirty", false)
|
||
await _click_control(_find_button_with_text(quick_row, "💬 Say"))
|
||
await _wait_frames(4)
|
||
composer = stage.get("_speech_composer") as Control
|
||
var speech_cancel: Button = _find_button_with_text(stage.get("_speech_show_toolbar") as Control, "Cancel speech")
|
||
_check(composer != null and composer.visible and speech_cancel != null, "new Say draft has a visible Cancel speech action")
|
||
if speech_cancel != null:
|
||
await _click_control(speech_cancel)
|
||
await _wait_frames(4)
|
||
_check(not composer.visible and not bool(stage.get("_dirty")) and character.get_action_plan().size() == 8, "Cancel speech closes the draft without a plan mutation")
|
||
await _save_capture("compact_speech_cancelled")
|
||
|
||
# Playback goes through the real Space shortcut and the runner emits precise
|
||
# physics-frame observations for the grouped barrier.
|
||
var started: Array[Dictionary] = []
|
||
var ended: Array[Dictionary] = []
|
||
character.planned_action_started.connect(func(action_id: String, action: Dictionary, step_index: int) -> void:
|
||
started.append({"id": action_id, "type": String(action.get("type", "")), "step": step_index, "frame": Engine.get_physics_frames()})
|
||
)
|
||
character.planned_action_ended.connect(func(action_id: String, outcome: StringName, step_index: int) -> void:
|
||
ended.append({"id": action_id, "outcome": String(outcome), "step": step_index, "frame": Engine.get_physics_frames()})
|
||
)
|
||
await _send_key(KEY_SPACE)
|
||
await _wait_frames(4)
|
||
_check(int(stage.get("current_mode")) == SandboxStage.StageMode.PLAY, "Space starts playback from DIRECT through the production input path")
|
||
await _save_capture("compact_playback_started")
|
||
await _wait_until_actions_end(stage, ended, 8.0)
|
||
var step_two_wait_id: String = String(((character.get_action_plan()[1]["actions"] as Array)[0] as Dictionary).get("action_id", ""))
|
||
var step_two_say_id: String = _action_id_for_type(character.get_action_plan()[1], "speak")
|
||
var step_three_start_frame: int = _start_frame_for_step(started, 2)
|
||
var group_end_frame: int = maxi(_end_frame_for_id(ended, step_two_wait_id), _end_frame_for_id(ended, step_two_say_id))
|
||
_check(_same_start_frame(started, 0), "Move and Say in the first authored step start on the same runner physics frame")
|
||
_check(_same_start_frame(started, 1), "Wait and Say in the second authored step start on the same runner physics frame")
|
||
_check(group_end_frame >= 0 and step_three_start_frame >= group_end_frame, "third step waits until both concurrent second-step actions finish")
|
||
print("PHASE20B_TIMING second_wait=%d second_say=%d third_start=%d frames" % [_end_frame_for_id(ended, step_two_wait_id), _end_frame_for_id(ended, step_two_say_id), step_three_start_frame])
|
||
await _save_capture("compact_playback_group_barrier")
|
||
|
||
# Pause through the visible DIRECT mode segment, verify the live future-only
|
||
# structural gate, then resume with Space. Completed plan rows stay erasable.
|
||
var direct_button_small: Button = _find_button_containing(stage, "Direct")
|
||
if direct_button_small != null:
|
||
await _click_control(direct_button_small)
|
||
await _wait_frames(4)
|
||
_check(int(stage.get("_run_state")) == SandboxStage.RunState.PAUSED, "DIRECT mode switch pauses an active run")
|
||
character_view.refresh_plan()
|
||
await _save_capture("compact_paused_plan")
|
||
var third_card: Control = character_view.find_child("ActionStep_3", true, false) as Control
|
||
var third_earlier: Button = _find_button_with_tooltip(third_card, "Move Step 3 earlier")
|
||
_check(third_earlier != null and third_earlier.disabled, "consumed run history prevents moving a completed/future-barrier step")
|
||
await _send_key(KEY_SPACE)
|
||
await _wait_frames(3)
|
||
_check(int(stage.get("current_mode")) == SandboxStage.StageMode.PLAY, "Space resumes playback from paused DIRECT")
|
||
|
||
print("PHASE20B_GUI_CAPTURE %s; failures=%d" % [_active_capture_dir, _failures])
|
||
await _finish(stage)
|
||
quit(1 if _failures > 0 else 0)
|
||
|
||
|
||
func _run_visual_slice(scenario: String, stage: SandboxStage, character: StickmanRig, prop: PropBlock, character_view: StageCharacterSettingsView, card: StageInspectorPanel, quick_row: Control) -> void:
|
||
match scenario:
|
||
"move_dialog":
|
||
await _run_move_dialog_slice(stage, character, prop, quick_row)
|
||
"wait_dialog":
|
||
await _run_wait_dialog_slice(stage, character, quick_row)
|
||
"move_action_options":
|
||
await _run_move_action_options_slice(stage, character, character_view)
|
||
"empty":
|
||
pass
|
||
"say":
|
||
await _run_say_slice(stage, character, character_view, quick_row)
|
||
"grouping":
|
||
await _run_grouping_slice(stage, character, character_view, quick_row)
|
||
"capture_entry":
|
||
await _run_capture_entry_slice(stage, quick_row)
|
||
"flag":
|
||
await _run_flag_slice(stage, character, character_view, card)
|
||
"step_spotlight":
|
||
await _run_step_spotlight_slice(stage, character, character_view)
|
||
"mode_overlay_and_names":
|
||
await _run_mode_overlay_and_names_slice(stage, character, prop, character_view)
|
||
"compact":
|
||
await _run_compact_slice(stage, character, character_view, card)
|
||
"action_drawer", "action_drawer_short":
|
||
await _run_action_drawer_slice(stage, character, character_view, card, quick_row)
|
||
"place_capture":
|
||
await _run_place_capture_slice(stage, character, prop, character_view, quick_row)
|
||
"prop_capture":
|
||
await _run_prop_capture_slice(stage, character, prop, character_view, quick_row)
|
||
_:
|
||
_check(false, "requested GUI scenario name is supported")
|
||
|
||
|
||
func _run_menu_focused_stack_slice(stage: SandboxStage) -> void:
|
||
root.size = Vector2i(1280, 900)
|
||
stage.call("_on_grid_toggled", false)
|
||
_check(bool(stage.call("_load_scene_from_path", "res://content/stages/menu.scene")), "the worktree menu.scene loads through the production stage loader")
|
||
await _wait_process_frames(8)
|
||
var world: Node2D = stage.get_node("World") as Node2D
|
||
var bob: StickmanRig
|
||
for child: Node in world.get_children():
|
||
if child is StickmanRig and (child as StickmanRig).stage_name == "Bob":
|
||
bob = child as StickmanRig
|
||
break
|
||
_check(bob != null and bob.get_action_plan().size() == 9, "the loaded menu.scene has Bob's nine authored steps")
|
||
if bob == null:
|
||
return
|
||
var direct_button: Button = _find_button_containing(stage, "Direct")
|
||
if direct_button != null:
|
||
await _click_control(direct_button)
|
||
var selection: StageSelection = stage.get("_selection") as StageSelection
|
||
selection.select_only(bob)
|
||
await _wait_process_frames(6)
|
||
var view: StageCharacterSettingsView = stage.get("_character_settings_view") as StageCharacterSettingsView
|
||
var card: StageInspectorPanel = view.get_card() as StageInspectorPanel
|
||
var body_scroll: ScrollContainer = card.find_child("InspectorBodyScroll", true, false) as ScrollContainer
|
||
var heading: Label = view.find_child("ActionPlanHeadingLabel", true, false) as Label
|
||
_check(heading != null and heading.text == "Plan · 9 steps", "the focused-stack header shows the plan step count")
|
||
var step_one: Control = view.find_child("ActionStep_1", true, false) as Control
|
||
var step_two: Control = view.find_child("ActionStep_2", true, false) as Control
|
||
var step_three: Control = view.find_child("ActionStep_3", true, false) as Control
|
||
var step_four: Control = view.find_child("ActionStep_4", true, false) as Control
|
||
var step_five: Control = view.find_child("ActionStep_5", true, false) as Control
|
||
var step_nine: Control = view.find_child("ActionStep_9", true, false) as Control
|
||
var use_action: Dictionary = (bob.get_action_plan()[0]["actions"] as Array)[0] as Dictionary
|
||
var use_summary: Button = view.find_child("ActionSummary_%s" % String(use_action.get("action_id", "")), true, false) as Button
|
||
var step_one_style: StyleBoxFlat = step_one.get_theme_stylebox("panel") as StyleBoxFlat if step_one != null else null
|
||
var step_one_expanded: Control = step_one.find_child("ExpandedStepContent", true, false) as Control if step_one != null else null
|
||
var use_summary_label: Label = view.find_child("ActionSummaryText_%s" % String(use_action.get("action_id", "")), true, false) as Label
|
||
_check(step_one != null and step_one_style != null and step_one_style.border_color.is_equal_approx(Color("#FFB300")) and step_one_expanded != null and step_one_expanded.visible, "selected Step 1 remains expanded with the DIRECT amber border")
|
||
_check(use_summary != null and use_summary_label != null and use_summary_label.text == "Use Wall Clock", "Use summary shows the current prop name instead of prop and spot IDs")
|
||
var step_two_compact_row: Control = step_two.find_child("CompactStepRow", true, false) as Control if step_two != null else null
|
||
_check(step_two != null and step_two_compact_row != null and step_two_compact_row.visible and not step_two.find_child("ExpandedStepContent", true, false).visible, "unselected Step 2 is a compact tappable row")
|
||
var step_two_label: Label = step_two.find_child("ActionSummaryText_Step2", true, false) as Label if step_two != null else null
|
||
var step_two_summary: Button = step_two.find_child("CompactStepSummary_2", true, false) as Button if step_two != null else null
|
||
var step_four_label: Label = step_four.find_child("ActionSummaryText_Step4", true, false) as Label if step_four != null else null
|
||
var step_nine_label: Label = step_nine.find_child("ActionSummaryText_Step9", true, false) as Label if step_nine != null else null
|
||
_check(step_two_label != null and step_two_label.text == "Brown Briefcase" and step_two_summary != null and step_two_summary.tooltip_text == "Pick up Brown Briefcase", "compact pickup summary names the prop while its tooltip states the full action")
|
||
var step_three_label: Label = step_three.find_child("ActionSummaryText_Step3", true, false) as Label if step_three != null else null
|
||
_check(step_three_label != null and step_three_label.text == "Wait 1 second", "Wait keeps its readable duration")
|
||
_check(step_four_label != null and step_four_label.text == "Set down prop", "Set Down summary omits the raw world coordinates")
|
||
_check(step_five.find_child("ActionSummaryText_Step5", true, false).text == "Run to Place 5", "Move keeps its friendly destination label")
|
||
_check(step_nine_label != null and step_nine_label.text == "Throw prop + Say", "compact Together row names both actions without target coordinates")
|
||
var visible_add_together_count: int = 0
|
||
for step_index: int in view.get("_step_cards").size():
|
||
var step_card: Control = (view.get("_step_cards") as Array)[step_index] as Control
|
||
var add_together: Control = step_card.find_child("AddTogetherRow", true, false) as Control
|
||
if add_together != null and add_together.visible:
|
||
visible_add_together_count += 1
|
||
_check(visible_add_together_count == 1, "only the selected step shows Add together")
|
||
await _save_capture_now("menu_focused_stack_step1")
|
||
if body_scroll != null and step_four != null:
|
||
card.scroll_to_control(step_four)
|
||
await _wait_process_frames(3)
|
||
var compact_step_four: Button = step_four.find_child("CompactStepSummary_4", true, false) as Button if step_four != null else null
|
||
var row_y_before: float = step_four.get_global_position().y if step_four != null else -1.0
|
||
if compact_step_four != null:
|
||
await _click_control(compact_step_four)
|
||
await _wait_process_frames(5)
|
||
step_one = view.find_child("ActionStep_1", true, false) as Control
|
||
step_four = view.find_child("ActionStep_4", true, false) as Control
|
||
var step_four_expanded: Control = step_four.find_child("ExpandedStepContent", true, false) as Control if step_four != null else null
|
||
var step_four_summary: Button = step_four.find_child("CompactStepSummary_4", true, false) as Button if step_four != null else null
|
||
var step_four_compact_row: Control = step_four.find_child("CompactStepRow", true, false) as Control if step_four != null else null
|
||
var row_y_after: float = step_four.get_global_position().y if step_four != null else -1.0
|
||
var set_down_action: Dictionary = (bob.get_action_plan()[3]["actions"] as Array)[0] as Dictionary
|
||
_check(int(view.get("_selected_step_index")) == 3 and step_four_expanded != null and step_four_expanded.visible and step_four_summary != null and step_four_compact_row != null and not step_four_compact_row.visible, "tapping a compact step expands and selects it")
|
||
_check(step_one != null and not step_one.find_child("ExpandedStepContent", true, false).visible, "the previous step collapses when another step is selected")
|
||
_check(absf(row_y_after - row_y_before) <= 2.0, "expanding a compact step keeps its row near the same screen position")
|
||
var selected_add_together: Control = step_four.find_child("AddTogetherRow", true, false) as Control if step_four != null else null
|
||
_check(selected_add_together != null and selected_add_together.visible, "the selected step exposes its compatible Add together control")
|
||
await _save_capture_now("menu_focused_stack_step4_selected")
|
||
var set_down_summary: Button = step_four.find_child("ActionSummary_%s" % String(set_down_action.get("action_id", "")), true, false) as Button if step_four != null else null
|
||
if set_down_summary != null:
|
||
await _click_control(set_down_summary)
|
||
await _wait_process_frames(3)
|
||
step_four = view.find_child("ActionStep_4", true, false) as Control
|
||
var set_down_options: Control = step_four.find_child("ActionOptions_%s" % String(set_down_action.get("action_id", "")), true, false) as Control if step_four != null else null
|
||
_check(set_down_options != null and set_down_options.visible, "action options open after the expanded step's action is tapped")
|
||
_check(not bool(stage.get("_dirty")), "selecting and expanding steps does not dirty the scene")
|
||
|
||
|
||
func _run_action_drawer_slice(stage: SandboxStage, character: StickmanRig, character_view: StageCharacterSettingsView, card: StageInspectorPanel, _quick_row: Control) -> void:
|
||
stage.call("_set_scene_dirty", false)
|
||
var add_step: Button = _find_button_with_text(character_view, "+ Add step")
|
||
_check(add_step != null and add_step.visible, "selected actor exposes the pinned Add step control")
|
||
if add_step == null:
|
||
return
|
||
await _click_control(add_step)
|
||
var drawer: Panel = character_view.find_child("AddTogetherChoices", true, false) as Panel
|
||
var dimmer: Panel = character_view.find_child("AddStepDrawerDimmer", true, false) as Panel
|
||
var opening_rect: Rect2 = drawer.get_global_rect() if drawer != null else Rect2()
|
||
_check(drawer != null and drawer.visible and opening_rect.position.x > card.get_global_rect().position.x - drawer.size.x - 5.0 and drawer.modulate.a > 0.0 and drawer.modulate.a < 1.0, "drawer visibly slides left from the inspector while fading in")
|
||
await _save_capture_now("action_drawer_opening")
|
||
await _wait_process_frames(14)
|
||
var title: Label = drawer.find_child("TogetherChoiceTitle", true, false) as Label if drawer != null else null
|
||
var choices: VBoxContainer = drawer.find_child("TogetherChoiceButtons", true, false) as VBoxContainer if drawer != null else null
|
||
var choices_scroll: ScrollContainer = drawer.find_child("TogetherChoiceScroll", true, false) as ScrollContainer if drawer != null else null
|
||
var cancel: Button = _find_button_with_text(drawer, "Cancel") if drawer != null else null
|
||
var drawer_rect: Rect2 = drawer.get_global_rect() if drawer != null else Rect2()
|
||
var card_rect: Rect2 = card.get_global_rect()
|
||
var dimmer_rect: Rect2 = dimmer.get_global_rect() if dimmer != null else Rect2()
|
||
var drawer_style: StyleBoxFlat = drawer.get_theme_stylebox("panel") as StyleBoxFlat if drawer != null else null
|
||
var dimmer_style: StyleBoxFlat = dimmer.get_theme_stylebox("panel") as StyleBoxFlat if dimmer != null else null
|
||
var first_choice_rect: Rect2 = choices.get_child(0).get_global_rect() if choices != null and choices.get_child_count() > 0 else Rect2()
|
||
var last_choice_rect: Rect2 = choices.get_child(choices.get_child_count() - 1).get_global_rect() if choices != null and choices.get_child_count() > 0 else Rect2()
|
||
var choices_scroll_rect: Rect2 = choices_scroll.get_global_rect() if choices_scroll != null else Rect2()
|
||
var all_choices_visible: bool = choices_scroll != null and choices_scroll.scroll_vertical == 0 and choices_scroll_rect.encloses(first_choice_rect) and choices_scroll_rect.encloses(last_choice_rect)
|
||
_check(drawer != null and drawer.visible and title != null and title.text == "Choose an action", "Add step opens the production Choose an action drawer")
|
||
_check(choices != null and choices.get_child_count() >= 8 and cancel != null and cancel.visible and choices.get_global_rect().position.y >= drawer_rect.position.y, "drawer keeps the scrollable action list and pinned Cancel visible")
|
||
_check(drawer_rect.size.y > card_rect.size.y and cancel != null and choices_scroll_rect.end.y <= cancel.get_global_rect().position.y, "drawer extends beyond the inspector while keeping Cancel pinned below the list")
|
||
if root.size.y >= 900:
|
||
_check(all_choices_visible, "tall drawer shows every action without scrolling")
|
||
else:
|
||
_check(choices_scroll != null and last_choice_rect.end.y > choices_scroll_rect.end.y and cancel != null and cancel.get_global_rect().end.y <= drawer_rect.end.y, "short drawer scrolls the action list while keeping Cancel visible")
|
||
_check(drawer_rect.end.y <= float(root.size.y - 50), "drawer stays above the Stage status bar and safe bottom inset")
|
||
_check(absf(drawer_rect.position.y - card_rect.position.y) <= 1.0 and absf(drawer_rect.end.x - card_rect.position.x) <= 10.0, "drawer top aligns with inspector and opens from its left edge")
|
||
_check(dimmer != null and dimmer.visible and dimmer_rect.position.distance_to(card_rect.position) <= 1.0 and dimmer_rect.size.distance_to(card_rect.size) <= 1.0, "scrim covers the inspector bounds only")
|
||
_check(dimmer_style != null and is_equal_approx(dimmer_style.bg_color.a, 0.32) and drawer_style != null and drawer_style.bg_color.a >= 0.95, "inspector scrim is a light 32 percent shade while the drawer stays opaque")
|
||
_check(root.gui_get_focus_owner() == (choices.get_child(0) as Button), "opening the drawer moves keyboard focus to the first action choice")
|
||
await _save_capture_now("action_drawer_open")
|
||
if cancel != null:
|
||
await _click_control(cancel)
|
||
await _wait_process_frames(6)
|
||
var cancel_motion_rect: Rect2 = drawer.get_global_rect() if drawer != null else Rect2()
|
||
_check(drawer != null and drawer.visible and cancel_motion_rect.position.x > drawer_rect.position.x + 8.0 and drawer.modulate.a > 0.0 and drawer.modulate.a < 1.0, "Cancel reverses the drawer slide and fades the inspector scrim")
|
||
await _save_capture_now("action_drawer_canceling")
|
||
await _wait_process_frames(12)
|
||
_check(drawer != null and not drawer.visible and dimmer != null and not dimmer.visible, "Cancel slides the drawer and inspector scrim away")
|
||
_check(root.gui_get_focus_owner() == add_step, "Cancel returns keyboard focus to Add step")
|
||
await _save_capture_now("action_drawer_cancelled")
|
||
|
||
await _click_control(add_step)
|
||
await _wait_process_frames(4)
|
||
await _send_click(Vector2(300.0, 300.0))
|
||
await _wait_process_frames(4)
|
||
var outside_motion_rect: Rect2 = drawer.get_global_rect() if drawer != null else Rect2()
|
||
_check(drawer.visible and outside_motion_rect.position.x > drawer_rect.position.x + 8.0 and bool(character_view.get("_plan_popup_closing")), "outside click starts the same reverse slide and fade")
|
||
await _save_capture_now("action_drawer_outside_closing")
|
||
await _wait_process_frames(12)
|
||
var selection: StageSelection = stage.get("_selection") as StageSelection
|
||
_check(not drawer.visible and not dimmer.visible, "left click outside slides the drawer closed")
|
||
_check(selection.get_selected().size() == 1 and selection.get_selected()[0] == character, "outside click is consumed without clearing the actor selection on stage")
|
||
_check(root.gui_get_focus_owner() == add_step, "outside click returns keyboard focus to Add step")
|
||
await _save_capture_now("action_drawer_outside_clicked")
|
||
|
||
await _click_control(add_step)
|
||
await _wait_process_frames(16)
|
||
await _send_click(add_step.get_global_rect().get_center())
|
||
await _wait_process_frames(16)
|
||
_check(not drawer.visible and not dimmer.visible, "clicking the dimmed inspector closes the drawer without reopening it")
|
||
_check(selection.get_selected().size() == 1 and selection.get_selected()[0] == character, "inspector click is consumed while the drawer is open")
|
||
await _save_capture_now("action_drawer_inspector_clicked")
|
||
|
||
await _click_control(add_step)
|
||
await _wait_process_frames(4)
|
||
await _send_key(KEY_ESCAPE)
|
||
await _wait_process_frames(4)
|
||
var escape_motion_rect: Rect2 = drawer.get_global_rect() if drawer != null else Rect2()
|
||
_check(drawer.visible and escape_motion_rect.position.x > drawer_rect.position.x + 8.0 and bool(character_view.get("_plan_popup_closing")), "Escape starts the same reverse slide and fade")
|
||
await _save_capture_now("action_drawer_escape_closing")
|
||
await _wait_process_frames(12)
|
||
_check(not drawer.visible and not dimmer.visible, "Escape slides the drawer closed and removes the scrim")
|
||
_check(root.gui_get_focus_owner() == add_step, "Escape returns keyboard focus to Add step")
|
||
await _save_capture_now("action_drawer_escape_closed")
|
||
|
||
await _click_control(add_step)
|
||
await _wait_process_frames(16)
|
||
var move_choice: Button = _find_button_containing(drawer, "Move")
|
||
_check(move_choice != null, "drawer retains working action choices after each dismissal path")
|
||
if move_choice != null:
|
||
await _click_control(move_choice)
|
||
await _wait_process_frames(16)
|
||
var action_editor: ActionEditor = stage.get("_action_editor") as ActionEditor
|
||
_check(not drawer.visible and action_editor != null and action_editor.visible, "choosing an action waits for the drawer to close before opening its editor")
|
||
if action_editor != null and action_editor.visible:
|
||
await _click_control(_find_button_with_text(action_editor, "Cancel"))
|
||
await _wait_process_frames(4)
|
||
_check(not bool(stage.get("_dirty")), "canceling the opened action draft leaves the stage unchanged")
|
||
await _save_capture_now("action_drawer_action_selected")
|
||
|
||
var fixture_say: Dictionary = {"action_id": "drawer-fixture-say", "type": "speak", "text": "We are just walking around and seeing what is going on? Is there anything going on?", "duration": 6.3}
|
||
var fixture_plan: Array[Dictionary] = [{"actions": [fixture_say]}]
|
||
_check(character.set_action_plan(fixture_plan), "Say-only fixture provides compatible action choices for Add together")
|
||
character_view.refresh_plan()
|
||
stage.call("_set_scene_dirty", false)
|
||
await _wait_process_frames(4)
|
||
var say_step: Control = character_view.find_child("ActionStep_1", true, false) as Control
|
||
var together_button: Button = _find_button_with_text(say_step, "+ Add together") if say_step != null else null
|
||
_check(together_button != null, "Say step exposes its Add together control")
|
||
if together_button == null:
|
||
return
|
||
await _click_control(together_button)
|
||
await _wait_process_frames(1)
|
||
var together_entering_rect: Rect2 = drawer.get_global_rect() if drawer != null else Rect2()
|
||
var entering_target_x: float = card.get_global_rect().position.x - drawer.size.x - 8.0 if drawer != null else 0.0
|
||
_check(drawer.visible and together_entering_rect.position.x > entering_target_x + 4.0 and drawer.modulate.a > 0.0 and drawer.modulate.a < 1.0, "Add together uses the same slide-out and fade as Add step")
|
||
await _save_capture_now("add_together_drawer_opening")
|
||
await _wait_process_frames(14)
|
||
var together_title: Label = drawer.find_child("TogetherChoiceTitle", true, false) as Label if drawer != null else null
|
||
var together_source: Label = drawer.find_child("TogetherSourceAction", true, false) as Label if drawer != null else null
|
||
var together_choices: VBoxContainer = drawer.find_child("TogetherChoiceButtons", true, false) as VBoxContainer if drawer != null else null
|
||
var together_rect: Rect2 = drawer.get_global_rect() if drawer != null else Rect2()
|
||
var say_step_rect: Rect2 = card.get_global_rect()
|
||
_check(drawer.visible and together_title != null and together_title.text == "Add to Step 1", "Add together opens the shared drawer with its step title")
|
||
_check(together_source != null and together_source.visible and together_source.text.begins_with("With: Say") and together_source.text.contains("We are just walking around"), "drawer preserves the source Say summary and duration context")
|
||
_check(together_source != null and together_source.max_lines_visible == 2 and together_source.tooltip_text == together_source.text, "long source summary is limited to two lines and remains available in its tooltip")
|
||
var expected_say_companions: Array[String] = ACTION_REGISTRY.types()
|
||
expected_say_companions.erase("speak")
|
||
var every_say_companion_visible: bool = together_choices != null and together_choices.get_child_count() == expected_say_companions.size()
|
||
for action_type: String in expected_say_companions:
|
||
var expected_label: String = "Move" if action_type == ACTION_REGISTRY.MOVE_TO else ACTION_REGISTRY.label(action_type)
|
||
if together_choices == null or _find_button_containing(together_choices, expected_label) == null:
|
||
every_say_companion_visible = false
|
||
_check(every_say_companion_visible and _find_button_containing(together_choices, "Say") == null, "Say Add together picker lists every other registered action and omits duplicate Say")
|
||
_check(absf(together_rect.position.y - say_step_rect.position.y) <= 1.0 and absf(together_rect.end.x - say_step_rect.position.x) <= 10.0 and dimmer.visible, "Add together drawer aligns with the inspector and dims its shell")
|
||
_check(root.gui_get_focus_owner() == together_choices.get_child(0), "Add together drawer gives focus to its first compatible choice")
|
||
await _save_capture_now("add_together_say_all_actions")
|
||
await _click_control(_find_button_with_text(drawer, "Cancel"))
|
||
await _wait_process_frames(6)
|
||
var together_cancel_motion_rect: Rect2 = drawer.get_global_rect() if drawer != null else Rect2()
|
||
_check(drawer.visible and together_cancel_motion_rect.position.x > together_rect.position.x + 8.0 and drawer.modulate.a > 0.0 and drawer.modulate.a < 1.0, "Add together Cancel slides the shared drawer and scrim back")
|
||
await _save_capture_now("add_together_drawer_canceling")
|
||
await _wait_process_frames(10)
|
||
_check(not drawer.visible and not dimmer.visible and root.gui_get_focus_owner() == together_button, "Add together Cancel slides in and restores focus to its trigger")
|
||
await _save_capture_now("add_together_drawer_cancelled")
|
||
|
||
await _click_control(together_button)
|
||
await _wait_process_frames(16)
|
||
await _send_click(Vector2(300.0, 300.0))
|
||
await _wait_process_frames(16)
|
||
_check(not drawer.visible and not dimmer.visible and root.gui_get_focus_owner() == together_button, "Add together closes on outside click and restores focus")
|
||
_check(selection.get_selected().size() == 1 and selection.get_selected()[0] == character, "Add together outside click is consumed by the drawer")
|
||
|
||
await _click_control(together_button)
|
||
await _wait_process_frames(16)
|
||
await _send_key(KEY_ESCAPE)
|
||
await _wait_process_frames(16)
|
||
_check(not drawer.visible and not dimmer.visible and root.gui_get_focus_owner() == together_button, "Add together closes on Escape and restores focus")
|
||
|
||
await _click_control(together_button)
|
||
await _wait_process_frames(16)
|
||
await _click_control(_find_button_containing(together_choices, "Move"))
|
||
await _wait_process_frames(16)
|
||
action_editor = stage.get("_action_editor") as ActionEditor
|
||
_check(action_editor != null and action_editor.visible and int(stage.get("_plan_step_index")) == 0 and bool(stage.get("_plan_add_together")), "compatible choice opens its editor for the same step after the drawer closes")
|
||
if action_editor != null and action_editor.visible:
|
||
await _click_control(_find_button_with_text(action_editor, "Cancel"))
|
||
await _wait_process_frames(4)
|
||
_check(character.get_action_plan().size() == 1 and (character.get_action_plan()[0]["actions"] as Array).size() == 1 and not bool(stage.get("_dirty")), "Canceling a together action draft leaves the source plan unchanged")
|
||
|
||
await _click_control(together_button)
|
||
await _wait_process_frames(16)
|
||
var ragdoll_choice: Button = _find_button_containing(drawer, "Ragdoll")
|
||
_check(ragdoll_choice != null, "Say step offers Ragdoll as a together action")
|
||
if ragdoll_choice != null:
|
||
await _click_control(ragdoll_choice)
|
||
await _wait_process_frames(16)
|
||
var say_ragdoll_plan: Array[Dictionary] = character.get_action_plan()
|
||
var say_ragdoll_actions: Array = say_ragdoll_plan[0].get("actions", []) if not say_ragdoll_plan.is_empty() else []
|
||
_check(say_ragdoll_actions.size() == 2 and String((say_ragdoll_actions[0] as Dictionary).get("type", "")) == "speak" and String((say_ragdoll_actions[1] as Dictionary).get("type", "")) == "ragdoll", "choosing Ragdoll commits it alongside the existing Say")
|
||
stage.call("_refresh_character_settings_card")
|
||
await _wait_process_frames(4)
|
||
await _save_capture_now("say_ragdoll_together_pills")
|
||
print("PHASE20B_ACTION_DRAWER rect=%s card=%s dimmer=%s focus=%s" % [str(drawer_rect), str(card_rect), str(dimmer_rect), str(root.gui_get_focus_owner())])
|
||
|
||
|
||
func _run_move_dialog_slice(stage: SandboxStage, character: StickmanRig, prop_fixture: PropBlock, quick_row: Control) -> void:
|
||
stage.call("_set_scene_dirty", false)
|
||
var move_button: Button = _find_button_with_text(quick_row, "🚶 Move")
|
||
await _click_control(move_button)
|
||
var editor: ActionEditor = stage.get("_action_editor") as ActionEditor
|
||
var title: Label = editor.find_child("ActionEditorTitle", true, false) as Label if editor != null else null
|
||
var type_row: Control = editor.find_child("ActionTypeRow", true, false) as Control if editor != null else null
|
||
var move_heading: Label = _find_label_exact_text(editor, "How should Milo move?") if editor != null else null
|
||
var destination_heading: Label = _find_label_exact_text(editor, "Where should Milo go?") if editor != null else null
|
||
var walk: Button = _find_button_with_text(editor, "🚶 Walk") if editor != null else null
|
||
var run: Button = _find_button_with_text(editor, "🏃 Run") if editor != null else null
|
||
var place: Button = editor.find_child("ChoosePlanPlace", true, false) as Button if editor != null else null
|
||
var choose_prop: Button = editor.find_child("ChoosePlanProp", true, false) as Button if editor != null else null
|
||
var done: Button = editor.find_child("ActionEditorDone", true, false) as Button if editor != null else null
|
||
var cancel: Button = editor.find_child("ActionEditorCancel", true, false) as Button if editor != null else null
|
||
var backdrop: ColorRect = stage.find_child("ActionEditorBackdrop", true, false) as ColorRect
|
||
_check(editor != null and editor.visible and title != null and title.text.begins_with("Move ") and title.text.contains("Milo"), "Quick Move opens a titled production Move dialog for the selected character")
|
||
_check(type_row != null and not type_row.visible, "fixed Quick Move hides the redundant action-type row")
|
||
_check(move_heading != null and destination_heading != null, "movement and destination use separate labeled sections")
|
||
_check(walk != null and run != null and walk.text == "🚶 Walk" and run.text == "🏃 Run" and walk.custom_minimum_size.y >= 56.0 and run.custom_minimum_size.y >= 56.0, "Walk and Run are equal large choices with stable labels")
|
||
if walk != null:
|
||
var selected_walk_color: Color = walk.get_theme_color("font_pressed_color")
|
||
var walk_focus: StyleBoxFlat = walk.get_theme_stylebox("focus") as StyleBoxFlat
|
||
_check(selected_walk_color.is_equal_approx(Color("#161616")), "selected Walk label stays dark and readable on the amber surface")
|
||
_check(walk_focus != null and walk_focus.border_color.is_equal_approx(Color("#8BD9FF")) and walk_focus.border_width_left >= 3, "keyboard focus remains a distinct cyan ring on the selected Walk tile")
|
||
_check(place != null and choose_prop != null and place.custom_minimum_size.y >= 52.0 and choose_prop.custom_minimum_size.y >= 52.0, "place and prop destinations use large separate controls")
|
||
_check(done != null and done.text == "Done" and done.disabled and cancel != null and cancel.text == "Cancel" and cancel.custom_minimum_size.y >= 52.0, "Done is the disabled primary action until a destination is chosen, beside visible Cancel")
|
||
var panel_style: StyleBoxFlat = editor.get_theme_stylebox("panel") as StyleBoxFlat if editor != null else null
|
||
_check(panel_style != null and panel_style.corner_radius_top_left >= 16 and panel_style.border_width_left >= 2 and panel_style.bg_color.a >= 0.95, "Move dialog uses the shared opaque rounded shell and clear border")
|
||
_check(backdrop != null and backdrop.visible and backdrop.color.a >= 0.5, "Move dialog dims the stage behind its modal card")
|
||
if done != null and cancel != null:
|
||
var done_style: StyleBoxFlat = done.get_theme_stylebox("normal") as StyleBoxFlat
|
||
var cancel_style: StyleBoxFlat = cancel.get_theme_stylebox("normal") as StyleBoxFlat
|
||
_check(done_style != null and done_style.bg_color.is_equal_approx(Color("#087DBA")) and cancel_style != null and cancel_style.bg_color != done_style.bg_color, "Done uses the shared blue primary role and Cancel stays secondary")
|
||
var viewport_size: Vector2i = root.size
|
||
_check(editor != null and editor.size.x <= viewport_size.x - 48 and editor.size.y <= viewport_size.y - 48, "Move dialog fits within the safe viewport")
|
||
var destination_hint: Label = _find_label_exact_text(editor, "Choose a place or prop, then tap Done.") if editor != null else null
|
||
_check(destination_hint != null, "destination guidance tells the child what to do next")
|
||
if walk != null:
|
||
walk.grab_focus()
|
||
await _wait_process_frames(2)
|
||
_check(walk.has_focus(), "Walk focus is visible before choosing a destination")
|
||
await _save_capture_now("phase20b_final_move_dialog_standard")
|
||
if editor != null and done != null and prop_fixture != null:
|
||
editor.set_plan_destination({"kind": "prop", "target_prop_id": prop_fixture.stage_object_id, "approach_offset": Vector2.ZERO, "last_known_approach": prop_fixture.global_position})
|
||
editor.set_plan_destination_summary(prop_fixture.stage_name)
|
||
await _wait_process_frames(2)
|
||
var selected_destination: Label = _find_label_exact_text(editor, "Destination: %s" % prop_fixture.stage_name)
|
||
var primary_style: StyleBoxFlat = done.get_theme_stylebox("normal") as StyleBoxFlat
|
||
_check(not done.disabled and selected_destination != null, "selected prop summary enables Done with a readable destination")
|
||
_check(primary_style != null and primary_style.bg_color.is_equal_approx(Color("#087DBA")), "enabled Done keeps the shared blue primary fill")
|
||
done.grab_focus()
|
||
await _wait_process_frames(2)
|
||
var done_focus: StyleBoxFlat = done.get_theme_stylebox("focus") as StyleBoxFlat
|
||
_check(done.has_focus() and done_focus != null and done_focus.border_color.is_equal_approx(Color("#8BD9FF")), "focused primary action keeps a separate visible focus ring")
|
||
await _save_capture_now("phase20b_final_move_dialog_destination_selected")
|
||
var anchor_before_return: Vector2i = editor.position
|
||
editor.hide_for_plan_target_capture()
|
||
await _wait_process_frames(2)
|
||
editor.resume_after_plan_target_capture()
|
||
await _wait_process_frames(2)
|
||
_check(editor.visible and editor.position == anchor_before_return, "returning from destination capture preserves the dialog anchor")
|
||
if editor != null and cancel != null:
|
||
var cancel_clicks: Array[bool] = []
|
||
cancel.pressed.connect(func() -> void: cancel_clicks.append(true))
|
||
await _click_control(cancel)
|
||
await _wait_process_frames(3)
|
||
_check(cancel_clicks.size() == 1 and not editor.visible and not bool(stage.get("_dirty")), "visible Cancel closes the Move dialog without changing the plan")
|
||
|
||
|
||
func _run_wait_dialog_slice(stage: SandboxStage, character: StickmanRig, quick_row: Control) -> void:
|
||
stage.call("_set_scene_dirty", false)
|
||
var wait_quick_action: Button = _find_button_with_text(quick_row, "⏳ Wait")
|
||
_check(wait_quick_action != null, "pinned Wait action is available to open the production dialog")
|
||
if wait_quick_action == null:
|
||
return
|
||
await _click_control(wait_quick_action)
|
||
var editor: ActionEditor = stage.get("_action_editor") as ActionEditor
|
||
var value_edit: LineEdit = editor.find_child("WaitDurationValue", true, false) as LineEdit if editor != null else null
|
||
var duration_spin: SpinBox = editor.get("_duration_spin") as SpinBox if editor != null else null
|
||
var unit_label: Label = editor.find_child("WaitDurationUnit", true, false) as Label if editor != null else null
|
||
var minus: Button = editor.find_child("WaitDurationMinus", true, false) as Button if editor != null else null
|
||
var plus: Button = editor.find_child("WaitDurationPlus", true, false) as Button if editor != null else null
|
||
var one_second: Button = editor.find_child("WaitQuickPick1", true, false) as Button if editor != null else null
|
||
var three_seconds: Button = editor.find_child("WaitQuickPick3", true, false) as Button if editor != null else null
|
||
var five_seconds: Button = editor.find_child("WaitQuickPick5", true, false) as Button if editor != null else null
|
||
var done: Button = editor.find_child("ActionEditorDone", true, false) as Button if editor != null else null
|
||
var cancel: Button = editor.find_child("ActionEditorCancel", true, false) as Button if editor != null else null
|
||
await _wait_process_frames(2)
|
||
_check(editor != null and editor.visible and editor.current_action_type() == "wait", "pinned Wait opens the real plan Wait dialog")
|
||
_check(value_edit != null and duration_spin != null and minus != null and plus != null and unit_label != null and one_second != null and three_seconds != null and five_seconds != null, "Wait dialog shows the editable value, large stepper, unit, and three quick picks")
|
||
_check(value_edit != null and value_edit.text == "1" and unit_label != null and unit_label.text == "second" and one_second != null and one_second.button_pressed, "default one second is shown without a decimal and selects the matching singular preset")
|
||
var value_field: PanelContainer = editor.find_child("WaitDurationValueField", true, false) as PanelContainer if editor != null else null
|
||
var value_field_style: StyleBoxFlat = value_field.get_theme_stylebox("panel") as StyleBoxFlat if value_field != null else null
|
||
var active_theme: SandboxTheme = stage.get("_sandbox_theme") as SandboxTheme
|
||
var expected_focus_color: Color = active_theme.color("popups.confirmation.accent_color", Color("#39BFFF")) if active_theme != null else Color("#39BFFF")
|
||
_check(value_edit != null and value_edit.has_focus() and value_field_style != null and value_field_style.border_width_left >= 3 and value_field_style.border_color.is_equal_approx(expected_focus_color), "editable seconds field starts focused with a visible cyan outline")
|
||
_check(minus != null and plus != null and minus.custom_minimum_size.x >= 64.0 and minus.custom_minimum_size.y >= 64.0 and plus.custom_minimum_size.x >= 64.0 and plus.custom_minimum_size.y >= 64.0, "plus and minus controls are large touch targets")
|
||
_check(editor != null and editor.size.y <= 400 and done != null and cancel != null and done.text == "Done" and cancel.text == "Cancel", "redesigned dialog is compact and keeps visible Done and Cancel actions")
|
||
if editor == null or value_edit == null or duration_spin == null or minus == null or plus == null or one_second == null or three_seconds == null or five_seconds == null or done == null or cancel == null:
|
||
return
|
||
await _save_capture_now("wait_dialog_initial")
|
||
|
||
# Cancel a changed draft and confirm that no wait was added to the plan.
|
||
await _click_control(five_seconds)
|
||
_check(is_equal_approx(duration_spin.value, 5.0) and five_seconds.button_pressed and not one_second.button_pressed and unit_label.text == "seconds", "five-second quick pick updates the value, selected chip, and plural unit")
|
||
await _click_control(cancel)
|
||
await _wait_process_frames(3)
|
||
_check(not editor.visible and character.get_action_plan().is_empty() and not bool(stage.get("_dirty")), "Cancel discards the changed Wait draft without changing the plan")
|
||
|
||
await _click_control(wait_quick_action)
|
||
await _wait_process_frames(3)
|
||
value_edit = editor.find_child("WaitDurationValue", true, false) as LineEdit
|
||
duration_spin = editor.get("_duration_spin") as SpinBox
|
||
unit_label = editor.find_child("WaitDurationUnit", true, false) as Label
|
||
minus = editor.find_child("WaitDurationMinus", true, false) as Button
|
||
plus = editor.find_child("WaitDurationPlus", true, false) as Button
|
||
one_second = editor.find_child("WaitQuickPick1", true, false) as Button
|
||
three_seconds = editor.find_child("WaitQuickPick3", true, false) as Button
|
||
five_seconds = editor.find_child("WaitQuickPick5", true, false) as Button
|
||
done = editor.find_child("ActionEditorDone", true, false) as Button
|
||
if value_edit == null or duration_spin == null or unit_label == null or minus == null or plus == null or one_second == null or three_seconds == null or five_seconds == null or done == null:
|
||
_check(false, "Wait controls are rebuilt after cancelling the first draft")
|
||
return
|
||
|
||
await _click_control(minus)
|
||
_check(is_equal_approx(duration_spin.value, 0.5) and value_edit.text == "0.5" and unit_label.text == "seconds", "minus steps down by half a second")
|
||
await _click_control(plus)
|
||
_check(is_equal_approx(duration_spin.value, 1.0) and value_edit.text == "1" and unit_label.text == "second", "plus steps up by half a second and restores the singular unit")
|
||
|
||
await _click_control(value_edit)
|
||
await _send_key(KEY_A, false, true)
|
||
await _type_text("1.2")
|
||
_check(value_edit.text == "1.2", "duration field accepts typed decimal seconds")
|
||
await _click_control(plus)
|
||
_check(is_equal_approx(duration_spin.value, 1.7) and value_edit.text == "1.7", "stepper adds 0.5 to a typed decimal without snapping it first")
|
||
await _click_control(minus)
|
||
_check(is_equal_approx(duration_spin.value, 1.2) and value_edit.text == "1.2", "stepper subtracts 0.5 and preserves tenths")
|
||
|
||
await _click_control(three_seconds)
|
||
_check(is_equal_approx(duration_spin.value, 3.0) and three_seconds.button_pressed and not one_second.button_pressed and unit_label.text == "seconds", "three-second preset selects itself and updates the value")
|
||
await _save_capture_now("wait_dialog_preset_three_seconds")
|
||
await _click_control(one_second)
|
||
_check(is_equal_approx(duration_spin.value, 1.0) and one_second.button_pressed and not three_seconds.button_pressed and unit_label.text == "second", "one-second preset returns to singular wording and updates the selected chip")
|
||
|
||
await _click_control(value_edit)
|
||
await _send_key(KEY_A, false, true)
|
||
await _type_text("3600")
|
||
await _send_key(KEY_ENTER)
|
||
_check(editor.visible and is_equal_approx(duration_spin.value, 3600.0) and value_edit.text == "3600" and plus.disabled, "Enter commits typed maximum seconds without accepting or closing the dialog")
|
||
await _click_control(value_edit)
|
||
await _send_key(KEY_A, false, true)
|
||
await _type_text("0.1")
|
||
await _send_key(KEY_ENTER)
|
||
_check(is_equal_approx(duration_spin.value, 0.1) and minus.disabled and unit_label.text == "seconds", "typed minimum is clamped to one tenth and disables the lower step")
|
||
await _click_control(plus)
|
||
_check(is_equal_approx(duration_spin.value, 0.6) and not plus.disabled and not minus.disabled, "plus steps up from the minimum by half a second")
|
||
await _click_control(five_seconds)
|
||
await _click_control(one_second)
|
||
_check(is_equal_approx(duration_spin.value, 1.0) and one_second.button_pressed and not five_seconds.button_pressed, "preset selection follows each choice instead of leaving stale highlights")
|
||
await _save_capture_now("wait_dialog_final_one_second")
|
||
await _click_control(done)
|
||
await _wait_process_frames(4)
|
||
var plan: Array[Dictionary] = character.get_action_plan()
|
||
var committed_wait: Dictionary = {}
|
||
if plan.size() == 1 and plan[0].get("actions", []) is Array and not (plan[0]["actions"] as Array).is_empty():
|
||
committed_wait = (plan[0]["actions"] as Array)[0] as Dictionary
|
||
_check(not editor.visible and plan.size() == 1 and String(committed_wait.get("type", "")) == "wait" and is_equal_approx(float(committed_wait.get("duration", -1.0)), 1.0), "Done commits the selected duration as the Wait action in the plan")
|
||
|
||
|
||
func _run_move_action_options_slice(stage: SandboxStage, character: StickmanRig, character_view: StageCharacterSettingsView) -> void:
|
||
var move_action: Dictionary = {
|
||
"action_id": "visual-options-move",
|
||
"type": "move_to",
|
||
"style": "walk",
|
||
"destination": {"kind": "place", "position": Vector2(-20.0, 0.0)},
|
||
}
|
||
var say_action: Dictionary = {"action_id": "visual-options-say", "type": "speak", "text": "Hello there!", "duration": 2.0}
|
||
var fixture_plan: Array[Dictionary] = [{"actions": [move_action]}, {"actions": [say_action]}]
|
||
_check(character.set_action_plan(fixture_plan), "visual comparison plan contains one Move and one Say action")
|
||
stage.call("_set_scene_dirty", false)
|
||
character_view.refresh_plan()
|
||
var move_summary: Button = _find_button_containing(character_view.find_child("ActionStep_1", true, false), "Walk to Place 1")
|
||
_check(move_summary != null, "Move action summary is visible for the options comparison")
|
||
if move_summary == null:
|
||
return
|
||
await _click_control(move_summary)
|
||
await _wait_process_frames(2)
|
||
var move_options: Control = character_view.find_child("ActionOptions_visual-options-move", true, false) as Control
|
||
var move_edit: Button = _find_button_with_text(move_options, "Edit action")
|
||
var move_change: Button = _find_button_with_text(move_options, "Change action")
|
||
var move_erase: Button = _find_button_with_text(move_options, "Erase action")
|
||
var move_edit_rect: Rect2 = move_edit.get_global_rect() if move_edit != null else Rect2()
|
||
var move_change_rect: Rect2 = move_change.get_global_rect() if move_change != null else Rect2()
|
||
var move_erase_rect: Rect2 = move_erase.get_global_rect() if move_erase != null else Rect2()
|
||
var move_layout_valid: bool = move_options is VBoxContainer and move_options.visible and move_edit != null and move_change != null and move_erase != null and move_edit.get_parent() == move_options and move_change.get_parent() == move_options and move_erase.get_parent() == move_options and is_equal_approx(move_edit_rect.size.x, move_change_rect.size.x) and is_equal_approx(move_edit_rect.size.x, move_erase_rect.size.x) and move_edit_rect.end.y <= move_change_rect.position.y and move_change_rect.end.y <= move_erase_rect.position.y and move_edit_rect.size.y >= 44.0 and move_change_rect.size.y >= 44.0 and move_erase_rect.size.y >= 44.0
|
||
_check(move_layout_valid, "Move has stacked, full-width Edit action, Change action, and Erase action controls in that order")
|
||
if move_layout_valid:
|
||
await _save_capture_now("move_action_options_stacked")
|
||
await _click_control(move_edit)
|
||
var action_editor: ActionEditor = stage.get("_action_editor") as ActionEditor
|
||
var choose_place: Button = _find_button_containing(action_editor, "place")
|
||
var choose_prop: Button = _find_button_containing(action_editor, "prop")
|
||
var action_type_row: Control = action_editor.find_child("ActionTypeRow", true, false) as Control if action_editor != null else null
|
||
_check(action_editor != null and action_editor.visible and action_editor.current_action_type() == "move_to" and choose_place != null and choose_prop != null and action_type_row != null and not action_type_row.visible, "Edit action opens the Move destination editor without an action-type dropdown")
|
||
var cancel_edit: Button = _find_button_with_text(action_editor, "Cancel")
|
||
if cancel_edit != null:
|
||
await _click_control(cancel_edit)
|
||
await _wait_process_frames(2)
|
||
await _click_control(move_change)
|
||
await _wait_process_frames(16)
|
||
var drawer: Panel = character_view.find_child("AddTogetherChoices", true, false) as Panel
|
||
var dimmer: Panel = character_view.find_child("AddStepDrawerDimmer", true, false) as Panel
|
||
var drawer_title: Label = drawer.find_child("TogetherChoiceTitle", true, false) as Label if drawer != null else null
|
||
var drawer_source: Label = drawer.find_child("TogetherSourceAction", true, false) as Label if drawer != null else null
|
||
var drawer_choices: VBoxContainer = drawer.find_child("TogetherChoiceButtons", true, false) as VBoxContainer if drawer != null else null
|
||
var change_drawer_rect: Rect2 = drawer.get_global_rect() if drawer != null else Rect2()
|
||
_check(drawer != null and drawer.visible and drawer_title != null and drawer_title.text == "Change action" and drawer_source != null and drawer_source.text == "Replace: Walk to Place 1", "Change action opens the shared drawer with the original action called out")
|
||
_check(drawer_choices != null and drawer_choices.get_child_count() == ACTION_REGISTRY.types().size() - 1 and _find_button_containing(drawer_choices, "Move") == null and dimmer != null and dimmer.visible, "standalone replacement picker lists all other action types and dims the inspector")
|
||
_check(absf(change_drawer_rect.position.y - character_view.get_card().get_global_rect().position.y) <= 1.0, "Change action drawer aligns to the inspector top")
|
||
await _save_capture_now("move_change_action_drawer_open")
|
||
var cancel_change: Button = _find_button_with_text(drawer, "Cancel") if drawer != null else null
|
||
if cancel_change != null:
|
||
await _click_control(cancel_change)
|
||
await _wait_process_frames(16)
|
||
var after_drawer_cancel: Array[Dictionary] = character.get_action_plan()
|
||
var drawer_cancel_preserved: bool = after_drawer_cancel.size() == 2 and String((after_drawer_cancel[0]["actions"] as Array)[0].get("type", "")) == "move_to" and String((after_drawer_cancel[0]["actions"] as Array)[0].get("action_id", "")) == "visual-options-move"
|
||
_check(drawer != null and not drawer.visible and dimmer != null and not dimmer.visible and drawer_cancel_preserved and not bool(stage.get("_dirty")), "Cancel closes the Change action drawer without modifying the plan")
|
||
|
||
await _click_control(move_change)
|
||
await _wait_process_frames(16)
|
||
var wait_choice: Button = _find_button_containing(drawer_choices, "Wait") if drawer_choices != null else null
|
||
if wait_choice != null:
|
||
await _click_control(wait_choice)
|
||
await _wait_process_frames(16)
|
||
action_editor = stage.get("_action_editor") as ActionEditor
|
||
action_type_row = action_editor.find_child("ActionTypeRow", true, false) as Control if action_editor != null else null
|
||
_check(action_editor != null and action_editor.visible and action_editor.current_action_type() == "wait" and action_type_row != null and not action_type_row.visible and String((character.get_action_plan()[0]["actions"] as Array)[0].get("action_id", "")) == "visual-options-move", "Choosing Wait opens its editor while the original Move and action ID remain until Done")
|
||
await _save_capture_now("change_action_wait_editor_open")
|
||
var cancel_replacement: Button = _find_button_with_text(action_editor, "Cancel") if action_editor != null else null
|
||
if cancel_replacement != null:
|
||
await _click_control(cancel_replacement)
|
||
await _wait_process_frames(4)
|
||
var after_editor_cancel: Array[Dictionary] = character.get_action_plan()
|
||
var editor_cancel_preserved: bool = after_editor_cancel.size() == 2 and String((after_editor_cancel[0]["actions"] as Array)[0].get("type", "")) == "move_to" and String((after_editor_cancel[0]["actions"] as Array)[0].get("action_id", "")) == "visual-options-move"
|
||
_check(editor_cancel_preserved and not bool(stage.get("_dirty")), "Canceling a type replacement leaves the original action and clean scene intact")
|
||
|
||
await _click_control(move_change)
|
||
await _wait_process_frames(16)
|
||
wait_choice = _find_button_containing(drawer_choices, "Wait") if drawer_choices != null else null
|
||
if wait_choice != null:
|
||
await _click_control(wait_choice)
|
||
await _wait_process_frames(16)
|
||
action_editor = stage.get("_action_editor") as ActionEditor
|
||
var replace_done: Button = _find_button_with_text(action_editor, "Done") if action_editor != null else null
|
||
if replace_done != null:
|
||
await _click_control(replace_done)
|
||
await _wait_process_frames(5)
|
||
var replaced_plan: Array[Dictionary] = character.get_action_plan()
|
||
var replaced_action: Dictionary = (replaced_plan[0]["actions"] as Array)[0] as Dictionary if not replaced_plan.is_empty() else {}
|
||
_check(String(replaced_action.get("type", "")) == "wait" and String(replaced_action.get("action_id", "")) == "visual-options-move" and bool(stage.get("_dirty")), "Done replaces the Move with Wait under the same action ID and dirties the scene once")
|
||
await _save_capture_now("move_replaced_by_wait")
|
||
|
||
var wait_for_say: Dictionary = replaced_action.duplicate(true)
|
||
var change_to_say_plan: Array[Dictionary] = [{"actions": [wait_for_say]}]
|
||
_check(character.set_action_plan(change_to_say_plan), "standalone Wait fixture is ready for changing to Say")
|
||
stage.call("_set_scene_dirty", false)
|
||
character_view.refresh_plan()
|
||
await _wait_process_frames(8)
|
||
var wait_to_say_summary: Button = _find_button_containing(character_view.find_child("ActionStep_1", true, false), "Wait")
|
||
var wait_to_say_options: Control = character_view.find_child("ActionOptions_visual-options-move", true, false) as Control
|
||
if wait_to_say_summary != null and (wait_to_say_options == null or not wait_to_say_options.visible):
|
||
await _click_control(wait_to_say_summary)
|
||
await _wait_process_frames(8)
|
||
wait_to_say_options = character_view.find_child("ActionOptions_visual-options-move", true, false) as Control
|
||
var wait_to_say_change: Button = _find_button_with_text(wait_to_say_options, "Change action")
|
||
if wait_to_say_change != null:
|
||
var inspector_for_say_change: StageInspectorPanel = character_view.get_card() as StageInspectorPanel
|
||
if inspector_for_say_change != null:
|
||
inspector_for_say_change.scroll_to_control(wait_to_say_change)
|
||
await _wait_process_frames(4)
|
||
await _click_control(wait_to_say_change)
|
||
await _wait_process_frames(16)
|
||
var say_change_drawer: Panel = character_view.find_child("AddTogetherChoices", true, false) as Panel
|
||
var say_change_choices: VBoxContainer = say_change_drawer.find_child("TogetherChoiceButtons", true, false) as VBoxContainer if say_change_drawer != null else null
|
||
var say_choice: Button = _find_button_containing(say_change_choices, "Say") if say_change_choices != null else null
|
||
_check(say_change_drawer != null and say_change_drawer.visible and say_choice != null, "standalone Wait can be changed to Say from the same action drawer")
|
||
if say_choice != null:
|
||
await _click_control(say_choice)
|
||
await _wait_process_frames(16)
|
||
var replacement_speech_composer: Control = stage.get("_speech_composer") as Control
|
||
_check(replacement_speech_composer != null and replacement_speech_composer.visible and String(stage.get("_plan_action_id")) == "visual-options-move", "choosing Say opens the production speech composer for the existing action ID")
|
||
if replacement_speech_composer != null and replacement_speech_composer.visible:
|
||
await _save_capture_now("change_action_say_composer_open")
|
||
var replacement_speech_ready: bool = await _focus_speech_for_message(stage, "change_action_say_message_editor")
|
||
var replacement_text: TextEdit = replacement_speech_composer.find_child("ActiveBubbleText", true, false) as TextEdit
|
||
if replacement_speech_ready and replacement_text != null and replacement_text.has_focus():
|
||
await _send_key(KEY_A, false, true)
|
||
await _type_text("Changed into speech!")
|
||
await _click_control(_find_button_with_text(replacement_speech_composer.find_child("MessageDialog", true, false), "✓ Done editing"))
|
||
await _wait_process_frames(3)
|
||
await _click_control(_find_button_with_text(stage.get("_speech_style_toolbar") as Control, "✓ Done styling"))
|
||
await _wait_process_frames(3)
|
||
await _click_control(_find_button_with_text(stage.get("_speech_show_toolbar") as Control, "✓ Done speech"))
|
||
await _wait_process_frames(6)
|
||
var changed_to_say_plan: Array[Dictionary] = character.get_action_plan()
|
||
var changed_to_say: Dictionary = (changed_to_say_plan[0]["actions"] as Array)[0] as Dictionary if not changed_to_say_plan.is_empty() else {}
|
||
_check(String(changed_to_say.get("type", "")) == "speak" and String(changed_to_say.get("action_id", "")) == "visual-options-move" and _first_speech_text(changed_to_say) == "Changed into speech!", "Done speech replaces Wait with Say, saves its text, and preserves the action ID")
|
||
|
||
var grouped_move: Dictionary = move_action.duplicate(true)
|
||
var grouped_say: Dictionary = say_action.duplicate(true)
|
||
grouped_move["action_id"] = "visual-options-group-move"
|
||
grouped_say["action_id"] = "visual-options-group-say"
|
||
var grouped_plan: Array[Dictionary] = [{"actions": [grouped_move, grouped_say]}]
|
||
_check(character.set_action_plan(grouped_plan), "Together compatibility fixture contains Move plus Say")
|
||
stage.call("_set_scene_dirty", false)
|
||
character_view.refresh_plan()
|
||
await _wait_process_frames(8)
|
||
var grouped_move_summary: Button = _find_button_containing(character_view.find_child("ActionStep_1", true, false), "Walk to Place 1")
|
||
if grouped_move_summary != null:
|
||
await _click_control(grouped_move_summary)
|
||
await _wait_process_frames(8)
|
||
var grouped_move_options: Control = character_view.find_child("ActionOptions_visual-options-group-move", true, false) as Control
|
||
var grouped_change: Button = _find_button_with_text(grouped_move_options, "Change action")
|
||
_check(grouped_change != null, "Move in a Together block has a compatible Change action option")
|
||
if grouped_change != null:
|
||
await _wait_process_frames(8)
|
||
var inspector_for_grouped_move: StageInspectorPanel = character_view.get_card() as StageInspectorPanel
|
||
if inspector_for_grouped_move != null:
|
||
inspector_for_grouped_move.scroll_to_control(grouped_change)
|
||
await _wait_process_frames(8)
|
||
await _save_capture_now("together_move_action_options_open")
|
||
await _click_control(grouped_change)
|
||
await _wait_process_frames(16)
|
||
var grouped_drawer: Panel = character_view.find_child("AddTogetherChoices", true, false) as Panel
|
||
var grouped_choices: VBoxContainer = grouped_drawer.find_child("TogetherChoiceButtons", true, false) as VBoxContainer if grouped_drawer != null else null
|
||
_check(grouped_drawer != null and grouped_drawer.visible and grouped_choices != null and grouped_choices.get_child_count() == 1 and _find_button_containing(grouped_choices, "Wait") != null and _find_button_containing(grouped_choices, "Move") == null and _find_button_containing(grouped_choices, "Say") == null, "Move plus Say offers only Wait as a valid replacement")
|
||
await _save_capture_now("together_move_change_action_compatible_only")
|
||
var grouped_cancel: Button = _find_button_with_text(grouped_drawer, "Cancel") if grouped_drawer != null else null
|
||
if grouped_cancel != null:
|
||
await _click_control(grouped_cancel)
|
||
await _wait_process_frames(16)
|
||
var say_summary: Button = _find_button_containing(character_view.find_child("ActionStep_1", true, false), "Say")
|
||
_check(say_summary != null, "Say action summary is visible for comparison")
|
||
if say_summary == null:
|
||
return
|
||
var inspector: StageInspectorPanel = character_view.get_card() as StageInspectorPanel
|
||
if inspector != null:
|
||
inspector.scroll_to_control(say_summary)
|
||
await _wait_process_frames(8)
|
||
await _click_control(say_summary)
|
||
await _wait_process_frames(8)
|
||
var say_options: Control = character_view.find_child("ActionOptions_visual-options-group-say", true, false) as Control
|
||
if inspector != null and say_options != null:
|
||
inspector.scroll_to_control(say_options)
|
||
await _wait_process_frames(8)
|
||
var say_edit: Button = _find_button_with_text(say_options, "Edit action")
|
||
var say_erase: Button = _find_button_with_text(say_options, "Erase action")
|
||
var say_edit_rect: Rect2 = say_edit.get_global_rect() if say_edit != null else Rect2()
|
||
var say_erase_rect: Rect2 = say_erase.get_global_rect() if say_erase != null else Rect2()
|
||
var say_change: Button = _find_button_with_text(say_options, "Change action")
|
||
var say_layout_valid: bool = say_options is VBoxContainer and say_options.visible and say_edit != null and say_erase != null and say_change == null and is_equal_approx(say_edit_rect.size.x, say_erase_rect.size.x) and say_edit_rect.end.y <= say_erase_rect.position.y
|
||
_check(say_layout_valid and character_view.get_selected_step_index() == 0, "Say inside Move plus Say keeps Edit and Erase because no other type fits the Together block")
|
||
if say_layout_valid:
|
||
await _save_capture_now("together_say_has_no_change_option")
|
||
|
||
var grouped_wait: Dictionary = {"action_id": "visual-options-group-wait", "type": "wait", "duration": 1.0}
|
||
var wait_say_plan: Array[Dictionary] = [{"actions": [grouped_wait, grouped_say]}]
|
||
_check(character.set_action_plan(wait_say_plan), "Together compatibility fixture contains Wait plus Say")
|
||
stage.call("_set_scene_dirty", false)
|
||
character_view.refresh_plan()
|
||
await _wait_process_frames(8)
|
||
var wait_summary: Button = _find_button_containing(character_view.find_child("ActionStep_1", true, false), "Wait")
|
||
if wait_summary != null:
|
||
await _click_control(wait_summary)
|
||
await _wait_process_frames(8)
|
||
var wait_options: Control = character_view.find_child("ActionOptions_visual-options-group-wait", true, false) as Control
|
||
var wait_change: Button = _find_button_with_text(wait_options, "Change action")
|
||
_check(wait_change != null, "Wait in a Together block has a compatible Change action option")
|
||
if wait_change != null:
|
||
await _wait_process_frames(8)
|
||
var inspector_for_wait: StageInspectorPanel = character_view.get_card() as StageInspectorPanel
|
||
if inspector_for_wait != null:
|
||
inspector_for_wait.scroll_to_control(wait_change)
|
||
await _wait_process_frames(8)
|
||
await _click_control(wait_change)
|
||
await _wait_process_frames(16)
|
||
var wait_change_drawer: Panel = character_view.find_child("AddTogetherChoices", true, false) as Panel
|
||
var wait_change_choices: VBoxContainer = wait_change_drawer.find_child("TogetherChoiceButtons", true, false) as VBoxContainer if wait_change_drawer != null else null
|
||
_check(wait_change_drawer != null and wait_change_drawer.visible and wait_change_choices != null and wait_change_choices.get_child_count() == 1 and _find_button_containing(wait_change_choices, "Move") != null and _find_button_containing(wait_change_choices, "Wait") == null and _find_button_containing(wait_change_choices, "Say") == null, "Wait plus Say offers only Move as a valid replacement")
|
||
await _save_capture_now("together_wait_change_action_compatible_only")
|
||
var wait_drawer_cancel: Button = _find_button_with_text(wait_change_drawer, "Cancel") if wait_change_drawer != null else null
|
||
if wait_drawer_cancel != null:
|
||
await _click_control(wait_drawer_cancel)
|
||
await _wait_process_frames(16)
|
||
var say_summary_in_wait_group: Button = _find_button_containing(character_view.find_child("ActionStep_1", true, false), "Say")
|
||
if say_summary_in_wait_group != null:
|
||
await _click_control(say_summary_in_wait_group)
|
||
await _wait_process_frames(8)
|
||
var say_options_in_wait_group: Control = character_view.find_child("ActionOptions_visual-options-group-say", true, false) as Control
|
||
_check(_find_button_with_text(say_options_in_wait_group, "Change action") == null, "Say in Wait plus Say also has no incompatible type replacement")
|
||
|
||
|
||
func _find_label_exact_text(parent: Node, expected: String) -> Label:
|
||
if parent == null:
|
||
return null
|
||
for child: Node in parent.find_children("*", "Label", true, false):
|
||
var label: Label = child as Label
|
||
if label != null and label.text == expected:
|
||
return label
|
||
return null
|
||
|
||
|
||
func _run_grouping_slice(stage: SandboxStage, character: StickmanRig, character_view: StageCharacterSettingsView, quick_row: Control) -> void:
|
||
var move_action: Dictionary = {"action_id": "visual-fixture-fetch-move", "type": "move_to", "style": "walk", "destination": {"kind": "place", "position": Vector2(-20.0, 0.0)}}
|
||
var fixture_plan: Array[Dictionary] = [{"actions": [move_action]}]
|
||
_check(character.set_action_plan(fixture_plan), "visual-only fixture supplies one committed Walk step for Add together authoring")
|
||
character_view.refresh_plan()
|
||
if character.get_action_plan().is_empty():
|
||
return
|
||
await _save_capture_now("phase20b_visual_fixture_move_step")
|
||
var step_card: Control = character_view.find_child("ActionStep_1", true, false) as Control
|
||
var step_heading: Label = _find_label_exact_text(step_card, "Step 1") if step_card != null else null
|
||
_check(step_heading != null and step_heading.name == "StepHeading" and step_heading.tooltip_text.is_empty() and step_heading.mouse_filter == Control.MOUSE_FILTER_PASS and step_heading.custom_minimum_size.y >= 44.0, "fixture Move shows plain Step 1 text with no tooltip")
|
||
_check(step_card != null and step_card.focus_mode == Control.FOCUS_ALL, "whole step card is keyboard-focusable")
|
||
if step_card == null or step_heading == null:
|
||
return
|
||
step_card.grab_focus()
|
||
await _send_key(KEY_ENTER)
|
||
await _wait_process_frames(3)
|
||
_check(character_view.get_selected_step_index() == 0 and step_card.has_focus(), "Enter activates the focused step card")
|
||
step_card = character_view.find_child("ActionStep_1", true, false) as Control
|
||
var together_button: Button = _find_button_with_text(step_card, "+ Add together")
|
||
await _click_control(together_button)
|
||
var popup: Panel = character_view.find_child("AddTogetherChoices", true, false) as Panel
|
||
var helper: Label = popup.find_child("TogetherCompatibilityHint", true, false) as Label if popup != null else null
|
||
var choice_column: VBoxContainer = popup.find_child("TogetherChoiceButtons", true, false) as VBoxContainer if popup != null else null
|
||
var cancel: Button = _find_button_with_text(popup, "Cancel") if popup != null else null
|
||
var picker_checks_before: int = _failures
|
||
_check(popup != null and popup.visible and helper == null, "Add together chooser omits the compatibility helper copy")
|
||
var popup_title: Label = popup.find_child("TogetherChoiceTitle", true, false) as Label if popup != null else null
|
||
var source_context: Label = popup.find_child("TogetherSourceAction", true, false) as Label if popup != null else null
|
||
var popup_scroll: ScrollContainer = popup.find_child("TogetherChoiceScroll", true, false) as ScrollContainer if popup != null else null
|
||
var picker_rect: Rect2 = popup.get_global_rect() if popup != null else Rect2()
|
||
var trigger_rect: Rect2 = together_button.get_global_rect() if together_button != null else Rect2()
|
||
var pinned_quick_row: Control = character_view.find_child("QuickActionButtons", true, false) as Control
|
||
print("PHASE20B_GUI_SLICE together_popup rect=%s trigger=%s custom=%s title=%s scroll=%s choices=%d cancel=%s focus=%s" % [str(picker_rect), str(trigger_rect), str(popup.custom_minimum_size) if popup != null else "missing", popup_title.text if popup_title != null else "missing", str(popup_scroll.get_global_rect()) if popup_scroll != null else "missing", choice_column.get_child_count() if choice_column != null else -1, str(cancel.size) if cancel != null else "missing", str(root.gui_get_focus_owner())])
|
||
var selection: StageSelection = stage.get("_selection") as StageSelection
|
||
_check(selection.get_selected().size() == 1 and selection.get_selected()[0] == character, "Add together interaction keeps the plan actor selected")
|
||
_check(choice_column != null and choice_column.get_child_count() == 1 and (choice_column.get_child(0) as Button).text.contains("Say") and cancel != null and cancel.visible and cancel.size.x >= 44.0 and cancel.size.y >= 44.0, "Move step offers one Say choice and a touch-sized visible Cancel")
|
||
_check(source_context != null and source_context.visible and source_context.text == "With: Walk to Place 1", "Add together chooser repeats the source action while covering the Step Stack row")
|
||
_check(popup_title != null and popup_title.text == "Add to Step 1" and popup != null and popup.size.y <= 288.0 and picker_rect.end.x <= root.size.x - 8.0 and picker_rect.end.y <= root.size.y - 38.0, "Add together chooser names its step and stays inside the safe viewport")
|
||
_check(together_button != null and popup != null and picker_rect.intersects(trigger_rect.grow(12.0)) and pinned_quick_row != null and not picker_rect.intersects(pinned_quick_row.get_global_rect()), "Add together chooser stays visually attached to its trigger without covering the pinned actions")
|
||
_check(root.gui_get_focus_owner() == choice_column.get_child(0), "keyboard focus moves to the compatible Say choice")
|
||
if _failures == picker_checks_before:
|
||
await _save_capture_now("phase20b_final_add_together_picker")
|
||
else:
|
||
print("PHASE20B_CAPTURE_SKIPPED name=phase20b_final_add_together_picker reason=picker_state_assertions_failed")
|
||
return
|
||
await _click_control(choice_column.get_child(0) as Button)
|
||
await _wait_process_frames(3)
|
||
var composer: Control = stage.get("_speech_composer") as Control
|
||
_check(composer != null and composer.visible, "visible Say choice opens the production speech composer")
|
||
if composer == null or not composer.visible:
|
||
return
|
||
var style_ready: bool = await _focus_speech_for_message(stage, "phase20b_final_grouped_say_message_dialog")
|
||
var message_button: Button = _find_button_with_text(stage.get("_speech_style_toolbar") as Control, "✏ Message")
|
||
_check(message_button != null and message_button.is_visible_in_tree() and message_button.get_global_rect().size.x > 0.0, "validation focus exposes the production Message control")
|
||
if not style_ready or message_button == null or not message_button.is_visible_in_tree() or message_button.get_global_rect().size.x <= 0.0:
|
||
await _click_control(_find_button_with_text(stage.get("_speech_show_toolbar") as Control, "Cancel speech"))
|
||
return
|
||
var text_edit: TextEdit = composer.find_child("ActiveBubbleText", true, false) as TextEdit if composer != null else null
|
||
_check(text_edit != null and text_edit.visible and text_edit.has_focus(), "production validation opens and focuses the speech text editor")
|
||
if text_edit != null and text_edit.has_focus():
|
||
await _send_key(KEY_A, false, true)
|
||
await _type_text("Fetch the ball, Milo!")
|
||
_check(text_edit.text == "Fetch the ball, Milo!", "Message text is entered through the focused production TextEdit")
|
||
await _save_capture_now("phase20b_final_grouped_say_composer")
|
||
await _click_control(_find_button_with_text(composer.find_child("MessageDialog", true, false), "✓ Done editing"))
|
||
await _wait_process_frames(3)
|
||
await _click_control(_find_button_with_text(stage.get("_speech_style_toolbar") as Control, "✓ Done styling"))
|
||
await _wait_process_frames(3)
|
||
var done: Button = _find_button_with_text(stage.get("_speech_show_toolbar") as Control, "✓ Done speech")
|
||
await _click_control(done)
|
||
await _wait_process_frames(6)
|
||
var plan: Array[Dictionary] = character.get_action_plan()
|
||
var grouped_commit_valid: bool = plan.size() == 1 and (plan[0]["actions"] as Array).size() == 2 and _step_has_action_type(plan[0], "speak")
|
||
_check(grouped_commit_valid, "real Add together and Done speech interactions commit exactly one Say alongside the Move fixture")
|
||
if not grouped_commit_valid:
|
||
return
|
||
var say_action: Dictionary = _find_action_type(plan[0], "speak")
|
||
_check(_first_speech_text(say_action) == "Fetch the ball, Milo!", "committed grouped Say preserves the entered message")
|
||
character_view.refresh_plan()
|
||
var grouped_step_card: Control = character_view.find_child("ActionStep_1", true, false) as Control
|
||
var grouped_say_summary: Button = _find_button_containing(grouped_step_card, "Say") if grouped_step_card != null else null
|
||
_check(grouped_say_summary != null and grouped_say_summary.text.begins_with("Say ") and grouped_say_summary.tooltip_text.contains("Fetch the ball, Milo!") and grouped_say_summary.tooltip_text.contains("seconds)"), "Say action keeps a concise control label and exposes the full message and duration by tooltip")
|
||
var say_id: String = String(say_action.get("action_id", ""))
|
||
var say_row_text: Label = character_view.find_child("ActionSummaryText_%s" % say_id, true, false) as Label
|
||
var say_duration: Label = character_view.find_child("ActionSummaryDuration_%s" % say_id, true, false) as Label
|
||
var say_rail: Control = character_view.find_child("ActionAccentRail_%s" % say_id, true, false) as Control
|
||
var say_row_options: Label = character_view.find_child("ActionSummaryOptions_%s" % say_id, true, false) as Label
|
||
var say_row_style: StyleBoxFlat = grouped_say_summary.get_theme_stylebox("normal") as StyleBoxFlat if grouped_say_summary != null else null
|
||
_check(say_row_text != null and say_row_text.text == "Say “Fetch the ball, Milo!”" and say_duration != null and say_duration.text.ends_with("s"), "Say row separates its spoken text and secondary duration like the mockup")
|
||
_check(say_rail != null and say_rail.custom_minimum_size.x == 4.0 and say_row_options != null and say_row_options.text == "..." and grouped_say_summary.custom_minimum_size.y >= 48.0, "Say row uses the same 48 px twin-rail layout and options cue as Move")
|
||
_check(say_row_style != null and say_row_style.bg_color.is_equal_approx(Color("#2C281C")), "Say row uses the matching warm tint from the approved mockup")
|
||
var projections: Array[Dictionary] = (stage.get("_director_visuals") as StageDirectorVisuals)._plan_projection(character)
|
||
var move_projection: Dictionary = _projection_for_action(projections, String(move_action.get("action_id", "")))
|
||
var say_projection: Dictionary = _projection_for_action(projections, String(say_action.get("action_id", "")))
|
||
var grouped_projection_valid: bool = say_projection.get("kind", "") == "say" and move_projection.get("kind", "") == "move" and say_projection.get("start", Vector2.INF) == move_projection.get("start", Vector2.ZERO)
|
||
_check(grouped_projection_valid, "grouped Say marker projects once at the same starting point as Move")
|
||
if not grouped_projection_valid:
|
||
return
|
||
await _wait_for_status_toast_clear(stage)
|
||
await _wait_process_frames(2)
|
||
await RenderingServer.frame_post_draw
|
||
var visuals: StageDirectorVisuals = stage.get("_director_visuals") as StageDirectorVisuals
|
||
var marker_image: Image = root.get_texture().get_image()
|
||
var say_marker_visible: bool = _say_marker_has_rendered_pixels(visuals, say_projection, marker_image)
|
||
if say_marker_visible:
|
||
await _save_capture_now("phase20b_final_walk_say_committed")
|
||
else:
|
||
await _save_capture_now("phase20b_diagnostics_say_marker")
|
||
_check(say_marker_visible, "grouped Say marker is visibly rendered at the projected Step 1 start")
|
||
await _click_control(_find_button_with_text(quick_row, "⏳ Wait"))
|
||
await _wait_process_frames(2)
|
||
var wait_editor: ActionEditor = stage.get("_action_editor") as ActionEditor
|
||
var wait_done_button: Button = _find_button_with_text(wait_editor, "Done") if wait_editor != null else null
|
||
var wait_editor_rect: Rect2 = Rect2(Vector2(wait_editor.position), Vector2(wait_editor.size)) if wait_editor != null else Rect2()
|
||
var wait_done_rect: Rect2 = wait_done_button.get_global_rect() if wait_done_button != null else Rect2()
|
||
var wait_editor_local_rect: Rect2 = Rect2(Vector2.ZERO, Vector2(wait_editor.size)) if wait_editor != null else Rect2()
|
||
_check(wait_editor != null and wait_done_button != null and wait_editor_rect.position.x >= 0.0 and wait_editor_rect.position.y >= 0.0 and wait_editor_rect.end.x <= root.size.x and wait_editor_rect.end.y <= root.size.y and wait_editor_local_rect.encloses(wait_done_rect), "Wait dialog and Done footer remain inside the production popup and viewport")
|
||
if wait_editor != null and wait_done_button != null:
|
||
await _save_capture_now("phase20b_wait_dialog_footer_visible")
|
||
await _click_control(wait_done_button)
|
||
await _wait_process_frames(4)
|
||
plan = character.get_action_plan()
|
||
_check(plan.size() == 2 and _step_has_action_type(plan[1], "wait"), "quick Wait appends a separate second step after the committed Walk+Say")
|
||
character_view.refresh_plan()
|
||
var wait_step_card: Control = character_view.find_child("ActionStep_2", true, false) as Control
|
||
var wait_summary: Button = _find_button_containing(wait_step_card, "Wait") if wait_step_card != null else null
|
||
_check(wait_summary != null and wait_summary.text == "Wait 1 second", "one-second Wait summary uses singular grammar")
|
||
var wait_id: String = String(_find_action_type(plan[1], "wait").get("action_id", "")) if plan.size() > 1 else ""
|
||
var wait_options: Label = character_view.find_child("ActionSummaryOptions_%s" % wait_id, true, false) as Label
|
||
_check(wait_options != null and wait_options.text == "...", "Step 2 uses the same trailing action-options cue as Step 1")
|
||
await _save_capture_now("phase20b_final_walk_say_then_wait")
|
||
_check(bool(stage.get("_dirty")), "valid production commits mark the scene dirty")
|
||
|
||
|
||
func _run_capture_entry_slice(stage: SandboxStage, quick_row: Control) -> void:
|
||
await _click_control(_find_button_with_text(quick_row, "🚶 Move"))
|
||
var action_editor: ActionEditor = stage.get("_action_editor") as ActionEditor
|
||
var choose_place: Button = _find_button_containing(action_editor, "Choose a place")
|
||
_check(action_editor != null and action_editor.visible and choose_place != null, "Quick Move opens its production draft before place capture")
|
||
if choose_place == null:
|
||
return
|
||
if action_editor.visible and choose_place.is_visible_in_tree():
|
||
await _save_capture_now("action_editor_choose_place_before_click")
|
||
else:
|
||
print("PHASE20B_CAPTURE_SKIPPED name=action_editor_choose_place_before_click reason=draft_or_choice_not_visible")
|
||
_capture_after_next_click = true
|
||
_trace_next_click = true
|
||
await _click_control(choose_place)
|
||
_check(int(stage.get("_capture_kind")) == PLAN_PLACE and not action_editor.visible, "Choose a place activates capture and closes the production draft")
|
||
var capture_rig: StickmanRig = stage.get("_action_editor_rig") as StickmanRig
|
||
_check(capture_rig != null and capture_rig.get_action_plan().is_empty(), "entering place capture leaves the Move draft uncommitted until Done")
|
||
var bar: StageUseTargetCaptureBar = stage.find_child("UseTargetCaptureBar", true, false) as StageUseTargetCaptureBar
|
||
_check(bar != null and bar.visible, "place capture prompt is rendered after the production click")
|
||
|
||
|
||
func _run_say_slice(stage: SandboxStage, character: StickmanRig, _character_view: StageCharacterSettingsView, quick_row: Control) -> void:
|
||
await _click_control(_find_button_with_text(quick_row, "💬 Say"))
|
||
await _wait_process_frames(4)
|
||
var composer: Control = stage.get("_speech_composer") as Control
|
||
_check(composer != null and composer.visible, "quick Say opens the production speech composer")
|
||
if composer != null and composer.visible:
|
||
await _save_capture_now("phase20b_final_standalone_say_toolbar")
|
||
else:
|
||
print("PHASE20B_CAPTURE_SKIPPED name=phase20b_final_standalone_say_toolbar reason=composer_not_open")
|
||
var style_ready: bool = await _focus_speech_for_message(stage, "phase20b_final_standalone_say_message_dialog")
|
||
var message_button: Button = _find_button_with_text(stage.get("_speech_style_toolbar") as Control, "✏ Message")
|
||
_check(message_button != null and message_button.is_visible_in_tree() and message_button.get_global_rect().size.x > 0.0, "focused speech preview exposes the production Message control")
|
||
if not style_ready or message_button == null or not message_button.is_visible_in_tree() or message_button.get_global_rect().size.x <= 0.0:
|
||
await _click_control(_find_button_with_text(stage.get("_speech_show_toolbar") as Control, "✕ Cancel speech"))
|
||
return
|
||
var text_edit: TextEdit = composer.find_child("ActiveBubbleText", true, false) as TextEdit if composer != null else null
|
||
var message_dialog: Control = composer.find_child("MessageDialog", true, false) as Control if composer != null else null
|
||
_check(message_button != null and message_dialog != null and message_dialog.visible and text_edit != null and text_edit.visible, "production validation opens the message editor for the empty Say bubble")
|
||
if text_edit != null and text_edit.visible and not text_edit.has_focus():
|
||
await _click_control(text_edit)
|
||
await _wait_process_frames(2)
|
||
_check(text_edit != null and text_edit.visible and text_edit.has_focus(), "production Message editor accepts pointer focus")
|
||
if text_edit != null and text_edit.has_focus():
|
||
await _send_key(KEY_A, false, true)
|
||
await _type_text("Hello from the stage!")
|
||
await _click_control(_find_button_with_text(composer.find_child("MessageDialog", true, false), "✓ Done editing"))
|
||
await _wait_process_frames(3)
|
||
await _click_control(_find_button_with_text(stage.get("_speech_style_toolbar") as Control, "✓ Done styling"))
|
||
await _wait_process_frames(3)
|
||
var done: Button = _find_button_with_text(stage.get("_speech_show_toolbar") as Control, "✓ Done speech")
|
||
_check(done != null and done.is_visible_in_tree() and done.get_global_rect().size.x > 0.0, "speech action exposes its labeled Done speech commit after styling closes")
|
||
if done != null:
|
||
await _click_control(done)
|
||
await _wait_process_frames(6)
|
||
var plan: Array[Dictionary] = character.get_action_plan()
|
||
_check(plan.size() == 1 and _step_has_action_type(plan[0], "speak"), "quick Say commits as its own first plan step through the production composer")
|
||
if plan.is_empty():
|
||
return
|
||
var say_action: Dictionary = _find_action_type(plan[0], "speak")
|
||
_check(_first_speech_text(say_action) == "Hello from the stage!", "standalone Say retains the committed message")
|
||
var visuals: StageDirectorVisuals = stage.get("_director_visuals") as StageDirectorVisuals
|
||
var say_projection: Dictionary = _projection_for_action(visuals._plan_projection(character), String(say_action.get("action_id", "")))
|
||
_check(say_projection.get("kind", "") == "say", "standalone Say has one projected preview marker")
|
||
await _wait_for_status_toast_clear(stage)
|
||
await _wait_process_frames(2)
|
||
await RenderingServer.frame_post_draw
|
||
var marker_image: Image = root.get_texture().get_image()
|
||
var say_marker_visible: bool = _say_marker_has_rendered_pixels(visuals, say_projection, marker_image)
|
||
if say_marker_visible:
|
||
await _save_capture_now("phase20b_final_standalone_say_committed")
|
||
else:
|
||
await _save_capture_now("phase20b_diagnostics_standalone_say_marker")
|
||
_check(say_marker_visible, "standalone Say marker is visibly rendered at the Step 1 start")
|
||
|
||
|
||
func _focus_speech_for_message(stage: SandboxStage, capture_name: String) -> bool:
|
||
var preview: SpeechBubble = stage.get("_speech_preview") as SpeechBubble
|
||
_check(preview != null and preview.visible, "speech draft exposes its live bubble preview")
|
||
if preview == null or not preview.visible:
|
||
return false
|
||
var composer: SpeechComposer = stage.get("_speech_composer") as SpeechComposer
|
||
var show_toolbar: Control = stage.get("_speech_show_toolbar") as Control
|
||
var done_speech: Button = _find_button_with_text(show_toolbar, "✓ Done speech")
|
||
var validation_events: Array[Dictionary] = []
|
||
if composer != null:
|
||
composer.validation_failed.connect(func(message: String, bubble_index: int) -> void: validation_events.append({"message": message, "bubble_index": bubble_index}), CONNECT_ONE_SHOT)
|
||
_check(done_speech != null and done_speech.is_visible_in_tree(), "speech draft exposes its labeled Done speech control")
|
||
if done_speech == null or not done_speech.is_visible_in_tree():
|
||
return false
|
||
await _click_control(done_speech)
|
||
await _wait_process_frames(4)
|
||
var style_toolbar: Control = stage.get("_speech_style_toolbar") as Control
|
||
var message_button: Button = _find_button_with_text(style_toolbar, "✏ Message")
|
||
var focus_index: int = int(composer.call("stage_focus_bubble_index")) if composer != null else -1
|
||
var controls_ready: bool = focus_index == 0 and message_button != null and message_button.is_visible_in_tree() and message_button.get_global_rect().size.x > 0.0 and message_button.get_global_rect().size.y > 0.0
|
||
var message_dialog: Control = composer.find_child("MessageDialog", true, false) as Control if composer != null else null
|
||
var text_edit: TextEdit = composer.find_child("ActiveBubbleText", true, false) as TextEdit if composer != null else null
|
||
print("PHASE20B_GUI_SPEECH_VALIDATION focus=%d events=%s message_rect=%s" % [focus_index, str(validation_events), str(message_button.get_global_rect()) if message_button != null else "missing"])
|
||
_check(validation_events.size() == 1 and int(validation_events[0].get("bubble_index", -1)) == 0, "Done speech validation guides the author to the first empty bubble")
|
||
_check(controls_ready, "validation focus exposes the production Message control for the bubble")
|
||
_check(composer != null and bool(composer.call("is_message_dialog_open")) and message_dialog != null and message_dialog.visible and text_edit != null and text_edit.visible and text_edit.has_focus(), "production validation opens and focuses the bubble's Message editor")
|
||
if controls_ready:
|
||
await _save_capture_now(capture_name)
|
||
return controls_ready
|
||
|
||
|
||
func _run_flag_slice(stage: SandboxStage, character: StickmanRig, character_view: StageCharacterSettingsView, card: StageInspectorPanel) -> void:
|
||
var quick_row: Control = character_view.find_child("QuickActionButtons", true, false) as Control
|
||
var move: Dictionary = {"action_id": "gui-flag-move", "type": "move_to", "style": "walk", "destination": {"kind": "place", "position": Vector2(-20.0, 0.0)}}
|
||
var wait_action: Dictionary = {"action_id": "gui-flag-wait", "type": "wait", "duration": 0.5}
|
||
var fixture_plan: Array[Dictionary] = [{"actions": [move]}, {"actions": [wait_action]}]
|
||
_check(character.set_action_plan(fixture_plan), "visual-only flag fixture seeds ordered Move and Wait steps through the rig plan API")
|
||
character_view.refresh_plan()
|
||
var move_id: String = String(move.get("action_id", ""))
|
||
stage.call("_set_scene_dirty", false)
|
||
var visuals: StageDirectorVisuals = stage.get("_director_visuals") as StageDirectorVisuals
|
||
var projection: Dictionary = {}
|
||
for entry: Dictionary in visuals._plan_projection(character):
|
||
if String(entry.get("action_id", "")) == move_id:
|
||
projection = entry
|
||
break
|
||
var hit_rect: Rect2 = projection.get("hit_rect", Rect2())
|
||
var screen_hit_size: Vector2 = hit_rect.size * visuals._zoom()
|
||
_check(String(projection.get("label", "")) == "Place 1" and screen_hit_size.x >= 44.0 and screen_hit_size.y >= 44.0, "projection carries Place 1 and a 44×44 screen hit rect")
|
||
var place_button: Button = _find_button_containing(character_view.find_child("ActionStep_1", true, false), "Place 1")
|
||
_check(place_button != null and not place_button.text.contains("(-"), "action row uses Place 1 without exposing raw coordinates")
|
||
await _send_click(_screen_at(stage, Vector2(-20.0, 0.0)))
|
||
await _wait_process_frames(2)
|
||
var options: Control = character_view.find_child("ActionOptions_%s" % move_id, true, false) as Control
|
||
_check(options != null and options.visible and not bool(stage.get("_dirty")), "production route flag click selects and opens options without dirtying the scene")
|
||
var card_rect: Rect2 = card.get_global_rect()
|
||
_check(card_rect.position.x >= 0.0 and card_rect.end.x <= root.size.x and quick_row != null and quick_row.visible and _find_button_with_text(quick_row, "⏳ Wait") != null, "expanded action inspector retains its shell and pinned actions")
|
||
var later: Button = _find_button_with_tooltip(character_view.find_child("ActionStep_1", true, false), "Move Step 1 later")
|
||
var earlier: Button = _find_button_with_tooltip(character_view.find_child("ActionStep_1", true, false), "Move Step 1 earlier")
|
||
_check(later != null and later.text == "↓" and later.size.x >= 48.0 and later.size.y >= 48.0 and earlier != null and earlier.text == "↑" and earlier.size.x >= 48.0 and earlier.size.y >= 48.0, "reorder controls use touch-sized directional arrows with descriptive tooltips")
|
||
await _save_capture_now("phase20b_final_flag_options")
|
||
_check(not bool(stage.get("_dirty")), "flag selection and opening its options do not dirty the scene")
|
||
character_view.call("_select_plan_step", 1)
|
||
if earlier != null:
|
||
await _click_control(earlier)
|
||
await _wait_process_frames(2)
|
||
var first_step_style: StyleBoxFlat = (character_view.find_child("ActionStep_1", true, false) as Control).get_theme_stylebox("panel") as StyleBoxFlat
|
||
_check(character_view.get_selected_step_index() == 0 and first_step_style.border_color.is_equal_approx(Color("#FFB300")), "clicking the disabled boundary arrow still selects its card")
|
||
if later != null:
|
||
await _click_control(later)
|
||
await _wait_process_frames(2)
|
||
_check(String(((character.get_action_plan()[0]["actions"] as Array)[0] as Dictionary).get("action_id", "")) == String(wait_action.get("action_id", "")), "real down arrow reorders only the future step")
|
||
var moved_move_card: Control = character_view.find_child("ActionStep_2", true, false) as Control
|
||
var restore_move_earlier: Button = _find_button_with_tooltip(moved_move_card, "Move Step 2 earlier") if moved_move_card != null else null
|
||
_check(restore_move_earlier != null and restore_move_earlier.text == "↑" and not restore_move_earlier.disabled, "reordered Move exposes an enabled up arrow on Step 2")
|
||
await _click_control(restore_move_earlier)
|
||
await _wait_process_frames(2)
|
||
_check(String(((character.get_action_plan()[0]["actions"] as Array)[0] as Dictionary).get("action_id", "")) == move_id, "real up arrow restores the initial order")
|
||
|
||
|
||
func _run_step_spotlight_slice(stage: SandboxStage, character: StickmanRig, character_view: StageCharacterSettingsView) -> void:
|
||
var first_move: Dictionary = {
|
||
"action_id": "spotlight-milo-step-1",
|
||
"type": "move_to",
|
||
"style": "walk",
|
||
"destination": {"kind": "place", "position": Vector2(80.0, 0.0)},
|
||
}
|
||
var first_say: Dictionary = {"action_id": "spotlight-milo-say", "type": "speak", "text": "Ready!", "duration": 1.0}
|
||
var second_move: Dictionary = {
|
||
"action_id": "spotlight-milo-step-2",
|
||
"type": "move_to",
|
||
"style": "run",
|
||
"destination": {"kind": "place", "position": Vector2(-300.0, -100.0)},
|
||
}
|
||
var fixture_plan: Array[Dictionary] = [
|
||
{"actions": [first_move, first_say]},
|
||
{"actions": [second_move]},
|
||
]
|
||
_check(character.set_action_plan(fixture_plan), "visual fixture seeds a grouped Walk+Say step followed by a Run step")
|
||
character_view.refresh_plan()
|
||
var spawner: StageSpawner = stage.get("_spawner") as StageSpawner
|
||
var ava: StickmanRig = spawner.spawn_stickman_asset("res://content/stickmen/break.stk", Vector2(420.0, -170.0))
|
||
_check(ava != null, "production spawner creates a second actor for Step Spotlight visibility checks")
|
||
if ava != null:
|
||
stage.call("_finalize_placed_node", ava)
|
||
var identity: StageObjectIdentity = stage.get("_object_identity") as StageObjectIdentity
|
||
var renamed: Dictionary = identity.rename(ava, "Ava", stage.get_node("World"))
|
||
_check(bool(renamed.get("ok", false)), "second actor receives its distinct stage name through production identity")
|
||
var ava_move: Dictionary = {
|
||
"action_id": "spotlight-ava-step-1",
|
||
"type": "move_to",
|
||
"style": "run",
|
||
"destination": {"kind": "place", "position": Vector2(500.0, 100.0)},
|
||
}
|
||
_check(ava.set_action_plan([{"actions": [ava_move]}]), "second actor receives its own visual-only Run route")
|
||
var selection: StageSelection = stage.get("_selection") as StageSelection
|
||
if selection != null and (selection.get_selected().size() != 1 or selection.get_selected()[0] != character):
|
||
selection.select_only(character)
|
||
character_view.refresh_plan()
|
||
stage.call("_set_scene_dirty", false)
|
||
await _wait_process_frames(4)
|
||
var visuals: StageDirectorVisuals = stage.get("_director_visuals") as StageDirectorVisuals
|
||
_check(character_view.get_selected_step_index() == 0 and visuals.get("_plan_spotlight_rig") == character and int(visuals.get("_plan_spotlight_step_index")) == 0, "DIRECT defaults Step Spotlight to the selected actor's first pending step")
|
||
_check(visuals.spotlight_route_width_px >= 4.0 and visuals.spotlight_route_shadow_width_px >= 6.0 and visuals.spotlight_dash_length_px >= 12.0 and visuals.spotlight_dash_gap_px >= 7.0 and visuals.spotlight_route_alpha >= 0.9, "theme loads the thicker, brighter route and contrast edge")
|
||
_check(visuals.action_badge_font_size_px >= 18.0 and visuals.action_badge_radius_px >= 10.0 and visuals.actor_name_font_size_px >= 20.0 and visuals.actor_name_radius_px >= 12.0 and visuals.actor_name_offset_px >= 24.0, "theme loads prominent rounded action badges and clear headwear clearance")
|
||
var name_layouts: Array = visuals.get("_last_actor_name_badge_layouts")
|
||
_check(name_layouts.size() == 1 and (name_layouts[0] as Dictionary).get("rig") == character, "DIRECT Step Spotlight draws the selected actor's name badge")
|
||
if name_layouts.size() == 1:
|
||
var name_layout: Dictionary = name_layouts[0]
|
||
var name_rect: Rect2 = name_layout.get("screen_rect", Rect2())
|
||
var head_rect: Rect2 = name_layout.get("head_bounds_screen", Rect2())
|
||
_check(String(name_layout.get("side", "")) == "above" and not name_rect.intersects(head_rect) and head_rect.position.y - name_rect.end.y >= 24.0, "DIRECT name badge stays above visible headwear with at least 24 screen pixels of clearance")
|
||
await _save_capture_now("phase20b_direct_name_badge_clearance")
|
||
var ava_target: Vector2 = Vector2(500.0, 100.0)
|
||
_check(ava != null and visuals.hit_test_plan_action(ava_target).is_empty(), "unselected actors' hidden routes do not retain invisible hit targets")
|
||
await _save_capture_now("phase20b_step_spotlight_step1_walk_say")
|
||
var step_two_card: Control = character_view.find_child("ActionStep_2", true, false) as Control
|
||
var step_two_action: Button = _find_button_with_text(step_two_card, "Run to Place 2")
|
||
var step_one_card: Control = character_view.find_child("ActionStep_1", true, false) as Control
|
||
var step_one: Label = _find_label_exact_text(step_one_card, "Step 1")
|
||
_check(step_two_action != null and step_one != null and step_one.tooltip_text.is_empty(), "action summary and plain Step 1 title are selectable without a redundant tooltip")
|
||
if step_two_action == null or step_one == null or step_one_card == null:
|
||
return
|
||
var heading_rect_before_hover: Rect2 = step_one.get_global_rect()
|
||
await _send_mouse_motion(step_one.get_screen_position() + step_one.size * 0.5)
|
||
await _wait_process_frames(3)
|
||
var heading_rect_after_hover: Rect2 = step_one.get_global_rect()
|
||
_check(heading_rect_before_hover.position.is_equal_approx(heading_rect_after_hover.position) and heading_rect_before_hover.size.is_equal_approx(heading_rect_after_hover.size), "hovering the plain step title does not shift or resize it")
|
||
var second_card_rect: Rect2 = step_two_card.get_global_rect()
|
||
await _send_click(Vector2(second_card_rect.position.x + 5.0, second_card_rect.get_center().y))
|
||
await _wait_process_frames(2)
|
||
var second_card_style: StyleBoxFlat = step_two_card.get_theme_stylebox("panel") as StyleBoxFlat
|
||
_check(character_view.get_selected_step_index() == 1 and second_card_style != null and second_card_style.border_color.is_equal_approx(Color("#FFB300")), "clicking blank Step 2 card space selects it with the amber border")
|
||
var step_two_add_together: Button = _find_button_with_text(step_two_card, "+ Add together")
|
||
character_view.call("_select_plan_step", 0)
|
||
var inspector: StageInspectorPanel = character_view.get_card() as StageInspectorPanel
|
||
if inspector != null and step_two_add_together != null:
|
||
inspector.scroll_to_control(step_two_add_together)
|
||
await _wait_process_frames(3)
|
||
await _click_control(step_two_add_together)
|
||
var together_popup: Panel = character_view.find_child("AddTogetherChoices", true, false) as Panel
|
||
_check(character_view.get_selected_step_index() == 1 and together_popup != null and together_popup.visible, "Add together selects its parent step while opening the choice picker")
|
||
var together_cancel: Button = _find_button_with_text(together_popup, "Cancel") if together_popup != null else null
|
||
await _click_control(together_cancel)
|
||
await _wait_process_frames(2)
|
||
await _click_control(step_two_action)
|
||
await _wait_process_frames(3)
|
||
var second_options: Control = character_view.find_child("ActionOptions_%s" % second_move.get("action_id", ""), true, false) as Control
|
||
var expanded_summary: Button = character_view.find_child("ActionSummary_%s" % second_move.get("action_id", ""), true, false) as Button
|
||
var expanded_style: StyleBoxFlat = expanded_summary.get_theme_stylebox("normal") as StyleBoxFlat if expanded_summary != null else null
|
||
_check(second_options != null and second_options.visible and expanded_style != null and expanded_style.border_color.is_equal_approx(Color("#FFB300")), "selecting Step 2 action opens its options and amber action highlight")
|
||
var step_one_heading: Label = _find_label_exact_text(character_view.find_child("ActionStep_1", true, false), "Step 1")
|
||
if inspector != null and step_one_heading != null:
|
||
inspector.scroll_to_control(step_one_heading)
|
||
await _wait_process_frames(3)
|
||
step_one_heading = _find_label_exact_text(character_view.find_child("ActionStep_1", true, false), "Step 1")
|
||
await _click_control(step_one_heading)
|
||
await _wait_process_frames(3)
|
||
var closed_options: Control = character_view.find_child("ActionOptions_%s" % second_move.get("action_id", ""), true, false) as Control
|
||
var collapsed_summary: Button = character_view.find_child("ActionSummary_%s" % second_move.get("action_id", ""), true, false) as Button
|
||
var collapsed_style: StyleBoxFlat = collapsed_summary.get_theme_stylebox("normal") as StyleBoxFlat if collapsed_summary != null else null
|
||
var selected_step_style: StyleBoxFlat = (character_view.find_child("ActionStep_1", true, false) as Control).get_theme_stylebox("panel") as StyleBoxFlat
|
||
step_one_heading = _find_label_exact_text(character_view.find_child("ActionStep_1", true, false), "Step 1")
|
||
_check(character_view.get_selected_step_index() == 0 and String(character_view.get("_expanded_action_id")) == "" and closed_options != null and not closed_options.visible, "selecting Step 1 heading closes the previously expanded Step 2 action")
|
||
_check(collapsed_style != null and not collapsed_style.border_color.is_equal_approx(Color("#FFB300")) and selected_step_style != null and selected_step_style.border_color.is_equal_approx(Color("#FFB300")), "step selection clears the old action highlight and leaves the amber border on its card")
|
||
_check(step_one_heading != null and step_one_heading.name == "StepHeading" and step_one_heading.tooltip_text.is_empty(), "selected step title remains plain text without a tooltip")
|
||
await _save_capture_now("phase20b_step_spotlight_step1_heading_selected")
|
||
var step_two: Label = _find_label_exact_text(character_view.find_child("ActionStep_2", true, false), "Step 2")
|
||
await _click_control(step_two)
|
||
await _wait_process_frames(3)
|
||
_check(character_view.get_selected_step_index() == 1 and int(visuals.get("_plan_spotlight_step_index")) == 1, "clicking the Step 2 heading switches the Step Spotlight")
|
||
var ghost_hit: Dictionary = visuals.hit_test_plan_action(Vector2(80.0, 0.0))
|
||
_check(String(ghost_hit.get("action_id", "")) == String(first_move.get("action_id", "")) and int(ghost_hit.get("step_index", -1)) == 0, "faint prior-step flag keeps its action-ID hit target")
|
||
await _save_capture_now("phase20b_step_spotlight_step2_run_ghost_flag")
|
||
await _send_click(_screen_at(stage, Vector2(80.0, 0.0)))
|
||
await _wait_process_frames(3)
|
||
_check(character_view.get_selected_step_index() == 0 and int(visuals.get("_plan_spotlight_step_index")) == 0, "clicking a ghost flag returns focus to its authored step")
|
||
_check(not bool(stage.get("_dirty")), "switching Step Spotlight by card or flag does not dirty the scene")
|
||
await _save_capture_now("phase20b_step_spotlight_ghost_flag_focus")
|
||
var original_character_position: Vector2 = character.global_position
|
||
var head_geometry: Dictionary = character.speech_head_geometry()
|
||
var edge_head_center: Vector2 = character.to_global(head_geometry.get("center", Vector2.ZERO))
|
||
var edge_head_radius: float = float(head_geometry.get("radius", 100.0)) * visuals._zoom()
|
||
var initial_head_top: float = (stage.get_viewport().get_canvas_transform() * edge_head_center).y - edge_head_radius
|
||
character.global_position.y += (84.0 - initial_head_top) / visuals._zoom()
|
||
visuals.mark_dirty()
|
||
await _wait_process_frames(4)
|
||
name_layouts = visuals.get("_last_actor_name_badge_layouts")
|
||
if name_layouts.size() == 1:
|
||
var edge_layout: Dictionary = name_layouts[0]
|
||
var edge_badge: Rect2 = edge_layout.get("screen_rect", Rect2())
|
||
var edge_head: Rect2 = edge_layout.get("head_bounds_screen", Rect2())
|
||
var safe_rect: Rect2 = Rect2(Vector2(8.0, 72.0), Vector2(float(root.size.x) - 16.0, float(root.size.y) - 112.0))
|
||
_check(String(edge_layout.get("side", "")) in ["left", "right"] and not edge_badge.intersects(edge_head), "DIRECT name badge moves beside a head near the top toolbar")
|
||
_check(safe_rect.encloses(edge_badge), "top-edge name badge stays inside the safe viewport")
|
||
else:
|
||
_check(false, "top-edge DIRECT state retains one actor-name badge layout")
|
||
await _save_capture_now("phase20b_direct_name_badge_top_edge")
|
||
character.global_position = original_character_position
|
||
visuals.mark_dirty()
|
||
await _wait_process_frames(3)
|
||
|
||
|
||
func _run_busy_scene_badge_stability(stage: SandboxStage) -> void:
|
||
var loaded: bool = bool(stage.call("_load_scene_from_path", "res://content/stages/busy.scene"))
|
||
_check(loaded, "production scene loader opens the user's busy.scene")
|
||
if not loaded:
|
||
return
|
||
await _wait_frames(12)
|
||
if int(stage.get("current_mode")) != SandboxStage.StageMode.EDIT:
|
||
await _click_control(_find_button_containing(stage, "Edit"))
|
||
var world: Node2D = stage.get_node("World") as Node2D
|
||
var target: StickmanRig
|
||
for child: Node in world.get_children():
|
||
if child is StickmanRig and String((child as StickmanRig).stage_name) == "break 2":
|
||
target = child as StickmanRig
|
||
break
|
||
_check(target != null, "busy.scene contains the named stickman shown in the user's report")
|
||
if target == null:
|
||
return
|
||
var camera: Camera2D = stage.get("_camera") as Camera2D
|
||
# Center only the test camera so the full production drag and badge remain in
|
||
# view; the scene itself is never saved or modified by this visual harness.
|
||
camera.position = target.global_position
|
||
var selection: StageSelection = stage.get("_selection") as StageSelection
|
||
var gizmos: StageGizmos = stage.get("_gizmos") as StageGizmos
|
||
var actor_screen: Vector2 = _screen_at(stage, target.global_position)
|
||
_check(Rect2(Vector2.ZERO, Vector2(root.size)).has_point(actor_screen), "reported stickman is visible in the rendered stage viewport")
|
||
await _send_click(actor_screen)
|
||
await _wait_frames(4)
|
||
var layouts: Array = gizmos.get("_last_selected_name_badge_layouts")
|
||
_check(selection.get_selected().size() == 1 and selection.get_selected()[0] == target, "production click selects break 2 in EDIT")
|
||
_check(layouts.size() == 1, "selected break 2 has one visible EDIT name badge")
|
||
if layouts.size() != 1:
|
||
return
|
||
var initial_slot: int = int((layouts[0] as Dictionary).get("anchor_slot", -1))
|
||
_check(initial_slot >= 0, "selected EDIT badge has a latched anchor slot")
|
||
await _save_capture_now("busy_scene_badge_selected")
|
||
|
||
# Move the actor through the production stage drag path and verify the badge
|
||
# stays on its chosen side both during the gesture and after release.
|
||
var initial_world_position: Vector2 = target.global_position
|
||
var drag_origin: Vector2 = _screen_at(stage, initial_world_position)
|
||
await _send_mouse_button(drag_origin, true, root.get_window_id(), drag_origin)
|
||
for delta: Vector2 in [Vector2(-16.0, 8.0), Vector2(-34.0, 14.0), Vector2(-52.0, 18.0)]:
|
||
var drag_position: Vector2 = drag_origin + delta
|
||
_send_mouse_motion(drag_position, root.get_window_id(), drag_position, MOUSE_BUTTON_MASK_LEFT)
|
||
await _wait_process_frames(2)
|
||
layouts = gizmos.get("_last_selected_name_badge_layouts")
|
||
_check(layouts.size() == 1 and int((layouts[0] as Dictionary).get("anchor_slot", -2)) == initial_slot, "EDIT name badge keeps its anchor while the busy.scene actor moves")
|
||
var drag_end: Vector2 = drag_origin + Vector2(-52.0, 18.0)
|
||
await _send_mouse_button(drag_end, false, root.get_window_id(), drag_end)
|
||
await _wait_frames(4)
|
||
layouts = gizmos.get("_last_selected_name_badge_layouts")
|
||
_check(gizmos.is_dragging() == false and layouts.size() == 1 and int((layouts[0] as Dictionary).get("anchor_slot", -2)) == initial_slot, "EDIT name badge keeps its anchor after releasing the actor")
|
||
_check(not target.global_position.is_equal_approx(initial_world_position), "production drag actually moves the busy.scene actor")
|
||
await _save_capture_now("busy_scene_badge_after_move_release")
|
||
|
||
# Exercise layout recomputation at another camera zoom while keeping the
|
||
# selected actor centered, then use a real middle-button drag to pan.
|
||
var original_zoom: Vector2 = camera.zoom
|
||
camera.zoom = original_zoom * 1.1
|
||
await _wait_frames(4)
|
||
layouts = gizmos.get("_last_selected_name_badge_layouts")
|
||
_check(layouts.size() == 1 and int((layouts[0] as Dictionary).get("anchor_slot", -2)) == initial_slot, "EDIT name badge anchor survives camera zoom")
|
||
_check(Rect2(Vector2.ZERO, Vector2(root.size)).has_point(_screen_at(stage, target.global_position)), "zoomed busy.scene actor stays in view")
|
||
camera.zoom = original_zoom
|
||
await _wait_frames(4)
|
||
await _save_capture_now("busy_scene_badge_after_zoom")
|
||
|
||
var camera_before_pan: Vector2 = camera.position
|
||
var pan_origin: Vector2 = Vector2(float(root.size.x) * 0.48, float(root.size.y) * 0.55)
|
||
var pan_end: Vector2 = pan_origin + Vector2(32.0, 16.0)
|
||
await _send_middle_button(pan_origin, true)
|
||
_send_mouse_motion(pan_end, root.get_window_id(), pan_end, MOUSE_BUTTON_MASK_MIDDLE)
|
||
await _wait_process_frames(3)
|
||
await _send_middle_button(pan_end, false)
|
||
await _wait_frames(4)
|
||
layouts = gizmos.get("_last_selected_name_badge_layouts")
|
||
_check(not camera.position.is_equal_approx(camera_before_pan), "middle-button drag pans the production Stage camera")
|
||
_check(layouts.size() == 1 and int((layouts[0] as Dictionary).get("anchor_slot", -2)) == initial_slot, "EDIT name badge keeps its anchor after camera pan")
|
||
_check(Rect2(Vector2.ZERO, Vector2(root.size)).has_point(_screen_at(stage, target.global_position)), "panned busy.scene actor stays in view")
|
||
await _save_capture_now("busy_scene_badge_after_pan")
|
||
|
||
|
||
func _run_mode_overlay_and_names_slice(stage: SandboxStage, character: StickmanRig, prop: PropBlock, character_view: StageCharacterSettingsView) -> void:
|
||
var move_action: Dictionary = {
|
||
"action_id": "edit-name-badge-move",
|
||
"type": "move_to",
|
||
"style": "walk",
|
||
"destination": {"kind": "place", "position": Vector2(260.0, -100.0)},
|
||
}
|
||
_check(character.set_action_plan([{"actions": [move_action]}]), "visual fixture seeds an authored Move route for mode visibility checks")
|
||
character_view.refresh_plan()
|
||
var visuals: StageDirectorVisuals = stage.get("_director_visuals") as StageDirectorVisuals
|
||
var gizmos: StageGizmos = stage.get("_gizmos") as StageGizmos
|
||
var selection: StageSelection = stage.get("_selection") as StageSelection
|
||
stage.call("_set_scene_dirty", false)
|
||
var edit_button: Button = _find_button_containing(stage, "Edit")
|
||
_check(edit_button != null, "EDIT mode control is available for the visibility slice")
|
||
if edit_button == null:
|
||
return
|
||
await _click_control(edit_button)
|
||
await _wait_frames(4)
|
||
_check(int(stage.get("current_mode")) == SandboxStage.StageMode.EDIT, "pointer activation switches from DIRECT to EDIT")
|
||
_check(not visuals.plan_overlay_visible and visuals.hit_test_plan_action(Vector2(260.0, -100.0)).is_empty(), "EDIT hides plan overlays and their hit targets when nothing is selected")
|
||
await _save_capture_now("phase20b_edit_empty_plan_hidden")
|
||
|
||
await _send_click(_screen_at(stage, character.global_position))
|
||
await _wait_frames(4)
|
||
var single_layouts: Array = gizmos.get("_last_selected_name_badge_layouts")
|
||
_check(selection.get_selected().size() == 1 and selection.get_selected()[0] == character, "production stage click selects the named stickman in EDIT")
|
||
_check(single_layouts.size() == 1 and (single_layouts[0] as Dictionary).get("node") == character, "one selected stickman receives one stage-name badge")
|
||
_check(not visuals.plan_overlay_visible and visuals.hit_test_plan_action(Vector2(260.0, -100.0)).is_empty(), "selected EDIT actor shows its name badge without revealing its plan route")
|
||
if single_layouts.size() == 1:
|
||
var character_badge: Dictionary = single_layouts[0]
|
||
var badge_rect: Rect2 = character_badge.get("screen_rect", Rect2())
|
||
var actor_bounds: Rect2 = character_badge.get("bounds_screen", Rect2())
|
||
_check(String(character_badge.get("stage_name", "")) == character.stage_name and String(character_badge.get("display_name", "")).ends_with("…"), "long actor stage name is elided while retaining its full source value")
|
||
_check(not badge_rect.intersects(actor_bounds), "actor badge stays clear of the actor's visible bounds")
|
||
await _save_capture_now("phase20b_edit_single_name_badge")
|
||
selection.clear_selection()
|
||
await _wait_frames(3)
|
||
var drag_origin: Vector2 = _screen_at(stage, character.global_position)
|
||
var drag_origin_world: Vector2 = character.global_position
|
||
await _send_mouse_button(drag_origin, true, root.get_window_id(), drag_origin)
|
||
_check(selection.get_selected().size() == 1 and selection.get_selected()[0] == character and gizmos.is_dragging(), "pressing an unselected actor selects it and begins the production translate gesture")
|
||
await _wait_frames(2)
|
||
var drag_layouts: Array = gizmos.get("_last_selected_name_badge_layouts")
|
||
var drag_offsets_stable: bool = drag_layouts.size() == 1
|
||
var initial_badge_offset: Vector2 = Vector2.INF
|
||
if drag_layouts.size() == 1:
|
||
var initial_badge_layout: Dictionary = drag_layouts[0]
|
||
var initial_badge_rect: Rect2 = initial_badge_layout.get("screen_rect", Rect2())
|
||
var initial_actor_bounds: Rect2 = initial_badge_layout.get("bounds_screen", Rect2())
|
||
initial_badge_offset = initial_badge_rect.position - initial_actor_bounds.position
|
||
for drag_delta: Vector2 in [Vector2(-24.0, 8.0), Vector2(-52.0, 16.0), Vector2(-80.0, 4.0), Vector2(-44.0, -8.0)]:
|
||
var drag_position: Vector2 = drag_origin + drag_delta
|
||
_send_mouse_motion(drag_position, root.get_window_id(), drag_position, MOUSE_BUTTON_MASK_LEFT)
|
||
await _wait_frames(2)
|
||
drag_layouts = gizmos.get("_last_selected_name_badge_layouts")
|
||
if drag_layouts.size() != 1:
|
||
drag_offsets_stable = false
|
||
continue
|
||
var drag_layout: Dictionary = drag_layouts[0]
|
||
var current_badge_rect: Rect2 = drag_layout.get("screen_rect", Rect2())
|
||
var current_actor_bounds: Rect2 = drag_layout.get("bounds_screen", Rect2())
|
||
var current_offset: Vector2 = current_badge_rect.position - current_actor_bounds.position
|
||
if not current_offset.is_equal_approx(initial_badge_offset):
|
||
drag_offsets_stable = false
|
||
_check(gizmos.is_dragging() and not character.global_position.is_equal_approx(drag_origin_world), "production pointer motion moves the actor while the gesture stays active")
|
||
_check(drag_offsets_stable, "EDIT name badge keeps its starting side and offset throughout the move")
|
||
await _save_capture_now("phase20b_edit_drag_name_badge_stable")
|
||
var drag_end: Vector2 = drag_origin + Vector2(-44.0, -8.0)
|
||
await _send_mouse_button(drag_end, false, root.get_window_id(), drag_end)
|
||
await _wait_frames(3)
|
||
_check(not gizmos.is_dragging(), "releasing the pointer ends the translate gesture")
|
||
character.global_position = drag_origin_world
|
||
stage.call("_rebuild_grid_cells")
|
||
await _wait_frames(2)
|
||
|
||
await _send_shift_click(_screen_at(stage, prop.global_position))
|
||
await _wait_frames(4)
|
||
var multi_layouts: Array = gizmos.get("_last_selected_name_badge_layouts")
|
||
_check(selection.get_selected().size() == 2 and selection.get_selected().has(character) and selection.get_selected().has(prop), "Shift-click creates a mixed stickman-and-prop selection")
|
||
_check(multi_layouts.size() == 2, "multi-selection shows a name badge for every selected named object")
|
||
if multi_layouts.size() == 2:
|
||
var first_badge: Rect2 = (multi_layouts[0] as Dictionary).get("screen_rect", Rect2())
|
||
var second_badge: Rect2 = (multi_layouts[1] as Dictionary).get("screen_rect", Rect2())
|
||
_check(not first_badge.intersects(second_badge), "multi-selection name badges move apart to avoid overlap")
|
||
var badge_obstacles: Array = gizmos.get("_selected_name_badge_obstacles")
|
||
var badges_clear_of_ui: bool = true
|
||
for layout_value: Variant in multi_layouts:
|
||
var layout: Dictionary = layout_value as Dictionary
|
||
var layout_rect: Rect2 = layout.get("screen_rect", Rect2())
|
||
for obstacle_value: Variant in badge_obstacles:
|
||
if layout_rect.intersects(obstacle_value as Rect2):
|
||
badges_clear_of_ui = false
|
||
_check(badges_clear_of_ui, "multi-selection badges stay clear of the inspector and Stage chrome")
|
||
_check(not visuals.plan_overlay_visible and visuals.hit_test_plan_action(Vector2(260.0, -100.0)).is_empty(), "selected EDIT objects still show no action-plan routes or hits")
|
||
await _save_capture_now("phase20b_edit_multi_name_badges")
|
||
|
||
gizmos.set_candidate_preview(prop)
|
||
await _wait_frames(4)
|
||
_check((gizmos.get("_last_selected_name_badge_layouts") as Array).is_empty(), "Objects here candidate preview suppresses persistent selection name badges")
|
||
await _save_capture_now("phase20b_edit_candidate_preview_badge_suppressed")
|
||
gizmos.set_candidate_preview(null)
|
||
await _wait_frames(3)
|
||
|
||
var direct_button: Button = _find_button_containing(stage, "Direct")
|
||
_check(direct_button != null, "DIRECT mode control is available after the EDIT badge checks")
|
||
if direct_button == null:
|
||
return
|
||
await _click_control(direct_button)
|
||
await _wait_frames(4)
|
||
_check(selection.get_selected().is_empty(), "switching to DIRECT clears EDIT multi-selection")
|
||
_check(not visuals.plan_overlay_visible and visuals.hit_test_plan_action(Vector2(260.0, -100.0)).is_empty(), "DIRECT with no actor selected hides plan markers and their hit targets")
|
||
await _save_capture_now("phase20b_direct_empty_plan_hidden")
|
||
|
||
await _send_click(_screen_at(stage, character.global_position))
|
||
await _wait_frames(4)
|
||
_check(visuals.plan_overlay_visible and visuals.get("_plan_spotlight_rig") == character, "selecting one actor in DIRECT restores its Step Spotlight")
|
||
var candidate_destination: Dictionary = {"kind": "place", "position": Vector2(360.0, -60.0), "label": "Place 2"}
|
||
visuals.set_plan_candidate(character, String(move_action.get("action_id", "")), candidate_destination, true, Vector2(360.0, -60.0))
|
||
selection.clear_selection()
|
||
await _wait_frames(4)
|
||
_check(not visuals.plan_overlay_visible and visuals.get("_plan_candidate_rig") == character, "clearing selection during retarget capture hides normal plans but keeps the candidate attached")
|
||
_check(visuals.hit_test_plan_action(Vector2(260.0, -100.0)).is_empty(), "faint capture routes remain non-interactive while selection is absent")
|
||
await _save_capture_now("phase20b_direct_capture_without_selection")
|
||
visuals.clear_plan_candidate()
|
||
|
||
|
||
func _run_compact_slice(stage: SandboxStage, character: StickmanRig, character_view: StageCharacterSettingsView, card: StageInspectorPanel) -> void:
|
||
var plan: Array[Dictionary] = []
|
||
for index: int in 8:
|
||
if index == 0:
|
||
plan.append({"actions": [
|
||
{"action_id": "fixture-move-0", "type": "move_to", "style": "walk", "destination": {"kind": "place", "position": Vector2(180.0, -50.0)}},
|
||
{"action_id": "fixture-say-0", "type": "speak", "text": "Fetch the ball!", "duration": 1.8}
|
||
]})
|
||
elif index == 2:
|
||
plan.append({"actions": [{"action_id": "fixture-say-long", "type": "speak", "text": "We are just walking around to see what happens. Who knows!?", "duration": 5.1}]})
|
||
else:
|
||
plan.append({"actions": [{"action_id": "fixture-wait-%d" % index, "type": "wait", "duration": 0.5}]})
|
||
_check(character.set_action_plan(plan), "compact visual-only fixture seeds eight plan steps through the rig plan API")
|
||
character_view.refresh_plan()
|
||
root.size = Vector2i(920, 560)
|
||
await _wait_process_frames(6)
|
||
var quick_row: Control = character_view.find_child("QuickActionButtons", true, false) as Control
|
||
var body_scroll: ScrollContainer = card.find_child("InspectorBodyScroll", true, false) as ScrollContainer
|
||
var card_rect: Rect2 = card.get_global_rect()
|
||
var selection: StageSelection = stage.get("_selection") as StageSelection
|
||
root.size = Vector2i(547, 626)
|
||
await _wait_process_frames(6)
|
||
var screenshot_size_rect: Rect2 = card.get_global_rect()
|
||
var stage_chrome: StageChrome = stage.get("_chrome") as StageChrome
|
||
var timestamp_badge: Control = stage_chrome.get_timestamp_badge() if stage_chrome != null else null
|
||
var timestamp_right: float = timestamp_badge.get_global_rect().end.x if timestamp_badge != null else -1.0
|
||
print("PHASE20B_GUI_LAYOUT_547 card=%s min=%s quick_min=%s title_min=%s" % [str(screenshot_size_rect), str(card.get_combined_minimum_size()), str(quick_row.get_combined_minimum_size()) if quick_row != null else "missing", str(card.find_child("InspectorTitle", true, false).get_combined_minimum_size()) if card.find_child("InspectorTitle", true, false) != null else "missing"])
|
||
_check(card.visible and screenshot_size_rect.position.x >= 0.0 and screenshot_size_rect.end.x <= 547.0 - 12.0 and screenshot_size_rect.size.x <= 380.0 and absf(screenshot_size_rect.end.x - timestamp_right) <= 1.0, "547 px resize aligns the inspector with the clock and leaves the 12 px frame margin")
|
||
await _save_capture_now("inspector_margin_at_547x626")
|
||
root.size = Vector2i(920, 560)
|
||
await _wait_process_frames(6)
|
||
card_rect = card.get_global_rect()
|
||
_check(card.visible and card_rect.position.x >= 0.0 and card_rect.end.x <= 920.0 and card_rect.end.y <= 560.0, "920×560 keeps the selected inspector within the viewport")
|
||
var resized_timestamp_right: float = timestamp_badge.get_global_rect().end.x if timestamp_badge != null else -1.0
|
||
_check(card_rect.size.x <= 380.0 and absf(card_rect.end.x - resized_timestamp_right) <= 1.0, "920×560 resize keeps the inspector's right edge aligned with the clock")
|
||
_check(quick_row != null and quick_row.get_global_rect().position.y >= card_rect.position.y and quick_row.get_global_rect().end.y < card_rect.end.y, "920×560 keeps pinned quick actions reachable")
|
||
_check(body_scroll != null and body_scroll.get_v_scroll_bar().max_value > 0.0, "eight-step plan scrolls within the inspector body")
|
||
var compact_step_one: Control = character_view.find_child("ActionStep_1", true, false) as Control
|
||
var compact_move: Button = _find_button_containing(compact_step_one, "Walk to Place 1")
|
||
var compact_say: Button = _find_button_containing(compact_step_one, "Say")
|
||
var compact_step_style: StyleBoxFlat = compact_step_one.get_theme_stylebox("panel") as StyleBoxFlat if compact_step_one != null else null
|
||
var compact_grouped_step_visible: bool = compact_step_one != null and body_scroll != null and body_scroll.get_global_rect().encloses(compact_step_one.get_global_rect()) and compact_move != null and compact_say != null and body_scroll.get_global_rect().encloses(compact_move.get_global_rect()) and body_scroll.get_global_rect().encloses(compact_say.get_global_rect()) and compact_step_style != null and compact_step_style.border_color.is_equal_approx(Color("#FFB300"))
|
||
_check(compact_grouped_step_visible, "compact DIRECT selection reveals the full selected grouped step with both twin-rail action rows")
|
||
_check(compact_move != null and compact_say != null and compact_move.custom_minimum_size.y == 48.0 and compact_say.custom_minimum_size.y == 48.0, "compact grouped action rows retain the mockup's 48 px targets")
|
||
var long_say_step: Control = character_view.find_child("ActionStep_3", true, false) as Control
|
||
var long_say_button: Button = character_view.find_child("ActionSummary_fixture-say-long", true, false) as Button
|
||
var long_say_label: Label = character_view.find_child("ActionSummaryText_fixture-say-long", true, false) as Label
|
||
_check(long_say_label != null and long_say_label.autowrap_mode == TextServer.AUTOWRAP_OFF and long_say_label.max_lines_visible == 1 and long_say_button != null and long_say_button.text.length() <= 24 and long_say_button.tooltip_text.contains("We are just walking around to see what happens. Who knows!?"), "long Say stays one line with ellipsis while its row tooltip retains the full speech")
|
||
_check(long_say_step != null and long_say_button != null and is_equal_approx(long_say_button.size.y, 48.0), "long Say does not make its action row taller")
|
||
var later_wait_summary: Button = _find_button_with_text(character_view.find_child("ActionStep_2", true, false), "Wait 0.5 seconds")
|
||
_check(later_wait_summary != null, "plan summaries spell out wait durations in seconds")
|
||
_check(selection.get_selected().size() == 1 and selection.get_selected()[0] == character, "viewport resize preserves the selected actor")
|
||
var compact_layout_valid: bool = card.visible and card_rect.position.x >= 0.0 and card_rect.end.x <= 920.0 and card_rect.end.y <= 560.0 and quick_row != null and quick_row.visible and quick_row.get_global_rect().position.y >= card_rect.position.y and quick_row.get_global_rect().end.y < card_rect.end.y and body_scroll != null and body_scroll.get_v_scroll_bar().max_value > 0.0 and compact_grouped_step_visible and later_wait_summary != null and selection.get_selected().size() == 1 and selection.get_selected()[0] == character
|
||
if compact_layout_valid:
|
||
await _save_capture_now("phase20b_final_compact_920x560_eight_steps")
|
||
await _save_capture_now("phase20b_final_compact_920x560_grouped_step_one")
|
||
else:
|
||
print("PHASE20B_CAPTURE_SKIPPED name=phase20b_final_compact_920x560_eight_steps reason=compact_layout_assertions_failed")
|
||
var fourth_step_heading: Label = _find_label_exact_text(character_view.find_child("ActionStep_4", true, false), "Step 4")
|
||
var fourth_step_card: Control = character_view.find_child("ActionStep_4", true, false) as Control
|
||
if body_scroll != null and fourth_step_heading != null and fourth_step_card != null:
|
||
for tick: int in 40:
|
||
var step_rect: Rect2 = fourth_step_card.get_global_rect()
|
||
var scroll_rect: Rect2 = body_scroll.get_global_rect()
|
||
if scroll_rect.encloses(step_rect):
|
||
break
|
||
await _send_wheel_down(scroll_rect.get_center())
|
||
await _wait_process_frames(1)
|
||
fourth_step_heading = _find_label_exact_text(character_view.find_child("ActionStep_4", true, false), "Step 4")
|
||
fourth_step_card = character_view.find_child("ActionStep_4", true, false) as Control
|
||
if tick % 4 == 3:
|
||
print("PHASE20B_GUI_SCROLL tick=%d value=%.1f max=%.1f step4_card=%s viewport=%s" % [tick + 1, body_scroll.scroll_vertical, body_scroll.get_v_scroll_bar().max_value, str(fourth_step_card.get_global_rect()) if fourth_step_card != null else "missing", str(body_scroll.get_global_rect())])
|
||
var final_step_rect: Rect2 = fourth_step_card.get_global_rect() if fourth_step_card != null else Rect2()
|
||
_check(fourth_step_heading != null and fourth_step_card != null and body_scroll.get_global_rect().encloses(final_step_rect), "production mouse-wheel scrolling brings the full Step 4 card into the 920×560 body viewport")
|
||
if fourth_step_heading != null:
|
||
root.grab_focus()
|
||
await _wait_process_frames(1)
|
||
var heading_rect_before_hover: Rect2 = fourth_step_heading.get_global_rect()
|
||
await _send_mouse_motion(fourth_step_heading.get_screen_position() + fourth_step_heading.size * 0.5)
|
||
await _wait_process_frames(2)
|
||
var hovered: Control = root.gui_get_hovered_control()
|
||
var heading_rect_after_hover: Rect2 = fourth_step_heading.get_global_rect()
|
||
print("PHASE20B_GUI_STEP4_HOVER target=%s hovered=%s" % [str(heading_rect_after_hover), hovered.name if hovered != null else "none"])
|
||
_check(fourth_step_heading.tooltip_text.is_empty() and heading_rect_before_hover.position.is_equal_approx(heading_rect_after_hover.position) and heading_rect_before_hover.size.is_equal_approx(heading_rect_after_hover.size), "hovering Step 4 text leaves it stationary and shows no tooltip")
|
||
await _wait_process_frames(4)
|
||
if fourth_step_card != null:
|
||
fourth_step_card.grab_focus()
|
||
await _send_key(KEY_ENTER)
|
||
await _wait_process_frames(4)
|
||
var selected_step_four: Label = _find_label_exact_text(character_view.find_child("ActionStep_4", true, false), "Step 4")
|
||
var selected_step_card: Control = character_view.find_child("ActionStep_4", true, false) as Control
|
||
var selected_step_rect: Rect2 = selected_step_card.get_global_rect() if selected_step_card != null else Rect2()
|
||
var selected_step_index: int = int(character_view.get("_selected_step_index"))
|
||
var focus_owner: Control = root.gui_get_focus_owner() as Control
|
||
var focus_text: String = focus_owner.name if focus_owner != null else "no focus"
|
||
var step_four_focused: bool = selected_step_card != null and selected_step_card.has_focus() and selected_step_index == 3
|
||
var selected_style: StyleBoxFlat = selected_step_card.get_theme_stylebox("panel") as StyleBoxFlat if selected_step_card != null else null
|
||
var direct_selected: bool = selected_style != null and selected_style.border_color.is_equal_approx(Color("#FFB300")) and selected_style.border_width_left == 3
|
||
var later_arrow: Button = selected_step_card.find_child("MoveStepLater_4", true, false) as Button if selected_step_card != null else null
|
||
var later_arrow_rect: Rect2 = later_arrow.get_global_rect() if later_arrow != null else Rect2()
|
||
print("PHASE20B_GUI_SCROLL selected_step=%s focused=%s selected_index=%d direct_amber=%s value=%.1f" % [str(selected_step_rect), focus_text, selected_step_index, str(direct_selected), body_scroll.scroll_vertical if body_scroll != null else -1.0])
|
||
_check(selected_step_four != null and selected_step_card != null and body_scroll.get_global_rect().encloses(selected_step_rect) and step_four_focused and direct_selected, "full Step 4 card stays visible with its DIRECT amber selection and keyboard focus")
|
||
_check(later_arrow != null and later_arrow_rect.end.x <= selected_step_rect.end.x - 8.0, "compact Step 4 down arrow stays inset from the selected card border")
|
||
_check(selection.get_selected().size() == 1 and selection.get_selected()[0] == character, "scrolling and selecting Step 4 do not dismiss the plan actor")
|
||
if step_four_focused and direct_selected and body_scroll.get_global_rect().encloses(selected_step_rect) and selection.get_selected().size() == 1 and selection.get_selected()[0] == character:
|
||
await _save_capture_now("phase20b_final_compact_920x560_step_four_selected")
|
||
else:
|
||
print("PHASE20B_CAPTURE_SKIPPED name=phase20b_final_compact_920x560_step_four_selected reason=step_focus_assertion_failed")
|
||
var focused_long_say_card: Control = character_view.find_child("ActionStep_3", true, false) as Control
|
||
var focused_long_say_button: Button = character_view.find_child("ActionSummary_fixture-say-long", true, false) as Button
|
||
if focused_long_say_card != null:
|
||
character_view.call("_select_plan_step", 2, true)
|
||
await _wait_process_frames(4)
|
||
var focused_long_say_row_visible: bool = focused_long_say_button != null and body_scroll != null and body_scroll.get_global_rect().encloses(focused_long_say_button.get_global_rect())
|
||
_check(int(character_view.get_selected_step_index()) == 2 and focused_long_say_row_visible and is_equal_approx(focused_long_say_button.size.y, 48.0), "selecting a long Say step keeps its row visible at one line")
|
||
if int(character_view.get_selected_step_index()) == 2 and focused_long_say_row_visible:
|
||
await _save_capture_now("phase20b_long_say_single_line")
|
||
else:
|
||
print("PHASE20B_CAPTURE_SKIPPED name=phase20b_long_say_single_line reason=selected_long_say_visibility_failed")
|
||
|
||
|
||
func _run_place_capture_slice(stage: SandboxStage, character: StickmanRig, prop: PropBlock, character_view: StageCharacterSettingsView, quick_row: Control) -> void:
|
||
var initial_place: Vector2 = Vector2(-500.0, 0.0)
|
||
var move: Dictionary = await _author_move_through_ui(stage, character, quick_row, "place", initial_place)
|
||
_check(not move.is_empty(), "place capture slice starts from a production-authored Move")
|
||
if move.is_empty():
|
||
return
|
||
var move_id: String = String(move.get("action_id", ""))
|
||
stage.call("_set_scene_dirty", false)
|
||
var visuals: StageDirectorVisuals = stage.get("_director_visuals") as StageDirectorVisuals
|
||
var committed_projection: Dictionary = _projection_for_action(visuals._plan_projection(character), move_id) if visuals != null else {}
|
||
var committed_flag_position: Vector2 = committed_projection.get("hit_position", Vector2.INF)
|
||
await _send_click(_screen_at(stage, committed_flag_position))
|
||
await _wait_process_frames(2)
|
||
var options: Control = character_view.find_child("ActionOptions_%s" % move_id, true, false) as Control
|
||
await _click_control(_find_button_with_text(options, "Edit action"))
|
||
var action_editor: ActionEditor = stage.get("_action_editor") as ActionEditor
|
||
await _click_control(_find_button_containing(action_editor, "place"))
|
||
var bar: StageUseTargetCaptureBar = stage.find_child("UseTargetCaptureBar", true, false) as StageUseTargetCaptureBar
|
||
var prompt: Label = bar.find_child("Prompt", true, false) as Label if bar != null else null
|
||
var cancel: Button = bar.find_child("Action_cancel_plan_capture", true, false) as Button if bar != null else null
|
||
var candidate_world: Vector2 = Vector2(-400.0, 0.0)
|
||
var candidate_screen: Vector2 = _screen_at(stage, candidate_world)
|
||
root.grab_focus()
|
||
_send_mouse_motion(candidate_screen)
|
||
await _wait_process_frames(3)
|
||
var cursor_prompt: Label = stage.get("_tooltip") as Label
|
||
var placement_overlay: StagePlacementOverlay = stage.get("_placement_overlay") as StagePlacementOverlay
|
||
var retarget_capture_valid: bool = int(stage.get("_capture_kind")) == PLAN_PLACE and bar != null and bar.visible and prompt != null and not prompt.visible and cancel != null and cancel.visible and cancel.size.x >= 44.0 and cancel.size.y >= 44.0 and action_editor != null and not action_editor.visible and cursor_prompt != null and cursor_prompt.visible and cursor_prompt.text == "Choose a new place or prop for Milo." and placement_overlay != null and placement_overlay.action_visible and not placement_overlay.action_show_trajectory
|
||
_check(retarget_capture_valid, "retarget capture keeps the cursor marker and instruction while showing a fixed Cancel-only control")
|
||
_check(prompt != null and not prompt.visible and cancel != null and cancel.visible and cancel.size.x >= 44.0 and cancel.size.y >= 44.0, "capture control removes the duplicate prompt and keeps Cancel touch-sized")
|
||
var capture_shell: StyleBoxFlat = bar.get_theme_stylebox("panel") as StyleBoxFlat if bar != null else null
|
||
_check(capture_shell != null and is_zero_approx(capture_shell.bg_color.a) and capture_shell.get_border_width(SIDE_LEFT) == 0, "Cancel button is shown without the old instruction-panel shell")
|
||
if retarget_capture_valid:
|
||
await _save_capture_marker_preview(stage, placement_overlay, cursor_prompt, candidate_world, candidate_screen, "phase20b_final_place_capture_preview")
|
||
else:
|
||
print("PHASE20B_CAPTURE_SKIPPED name=phase20b_final_place_capture_preview reason=production_capture_state_invalid")
|
||
var cancel_position_before: Vector2 = cancel.get_global_rect().position if cancel != null else Vector2.ZERO
|
||
var candidate_world_after_move: Vector2 = Vector2(-300.0, 0.0)
|
||
var candidate_screen_after_move: Vector2 = _screen_at(stage, candidate_world_after_move)
|
||
_send_mouse_motion(candidate_screen_after_move)
|
||
await _wait_process_frames(3)
|
||
var candidate_pointer: Vector2 = visuals.get("_plan_candidate_pointer")
|
||
var candidate_destination: Dictionary = visuals.get("_plan_candidate_destination")
|
||
var candidate_label: String = String(visuals.get("_plan_candidate_movement_label"))
|
||
var candidate_target: Variant = candidate_destination.get("position", Vector2.INF)
|
||
var candidate_anchor_data: Dictionary = visuals.call("_queue_anchor", character)
|
||
var candidate_anchor: Vector2 = candidate_anchor_data.get("ground", candidate_anchor_data.get("feet", Vector2.INF))
|
||
var grounded_candidate: Vector2 = visuals.call("_project_move_display_position", candidate_target, candidate_anchor.y) if candidate_target is Vector2 and candidate_anchor.is_finite() else Vector2.INF
|
||
var retarget_candidate_valid: bool = retarget_capture_valid and candidate_pointer.is_finite() and candidate_pointer.distance_to(candidate_world_after_move) < 0.1 and bool(visuals.get("_plan_candidate_valid"))
|
||
_check(retarget_candidate_valid, "production pointer motion previews a valid new place while the saved route remains intact")
|
||
_check(candidate_label == "Walk" and grounded_candidate.is_finite() and is_equal_approx(grounded_candidate.y, candidate_anchor.y), "candidate route uses the Walk label and a grounded horizontal endpoint")
|
||
_check(cancel != null and cancel.get_global_rect().position.is_equal_approx(cancel_position_before), "Cancel remains fixed while the cursor moves")
|
||
_check(String(((character.get_action_plan()[0]["actions"] as Array)[0] as Dictionary).get("action_id", "")) == move_id and not bool(stage.get("_dirty")), "candidate preview keeps the old action ID and does not dirty the authored destination")
|
||
if retarget_candidate_valid:
|
||
await _save_capture_marker_preview(stage, placement_overlay, cursor_prompt, candidate_world_after_move, candidate_screen_after_move, "phase20b_final_grounded_walk_candidate")
|
||
else:
|
||
print("PHASE20B_CAPTURE_SKIPPED name=phase20b_final_grounded_walk_candidate reason=valid_candidate_state_not_reached")
|
||
if cancel != null:
|
||
await _click_control(cancel)
|
||
await _wait_process_frames(3)
|
||
var current_action: Dictionary = (character.get_action_plan()[0]["actions"] as Array)[0] as Dictionary
|
||
_check(not bar.visible and not action_editor.visible and int(stage.get("_capture_kind")) == 0 and current_action == move and String(current_action.get("action_id", "")) == move_id and not bool(stage.get("_dirty")), "visible Cancel discards the retarget draft without changing the plan")
|
||
await _save_capture_now("phase20b_final_cancel_button_closed")
|
||
options = character_view.find_child("ActionOptions_%s" % move_id, true, false) as Control
|
||
await _click_control(_find_button_with_text(options, "Edit action"))
|
||
await _click_control(_find_button_containing(action_editor, "place"))
|
||
await _wait_process_frames(2)
|
||
await _send_key(KEY_ESCAPE)
|
||
await _wait_process_frames(2)
|
||
current_action = (character.get_action_plan()[0]["actions"] as Array)[0] as Dictionary
|
||
_check(not bar.visible and int(stage.get("_capture_kind")) == 0 and current_action == move and not bool(stage.get("_dirty")), "Escape remains a working shortcut for place-capture cancellation")
|
||
stage.set("_context_rig", character)
|
||
stage.call("_on_action_popup_id_pressed", SandboxStage.ACT_PICK_UP_PROP)
|
||
await _wait_process_frames(3)
|
||
var pickup_overlay: StagePlacementOverlay = stage.get("_placement_overlay") as StagePlacementOverlay
|
||
var pickup_target_preview_valid: bool = int(stage.get("_capture_kind")) == SandboxStage.CaptureKind.PROP and pickup_overlay != null and pickup_overlay.action_visible and not pickup_overlay.action_show_trajectory
|
||
_check(pickup_target_preview_valid, "Pick Up Prop capture shows only a cursor marker, without a movement-looking trajectory")
|
||
if pickup_target_preview_valid and prop != null:
|
||
var pickup_prompt: Label = stage.get("_tooltip") as Label
|
||
await _save_capture_marker_preview(stage, pickup_overlay, pickup_prompt, prop.global_position, _screen_at(stage, prop.global_position), "phase20b_final_pickup_prop_marker_only")
|
||
await _send_key(KEY_ESCAPE)
|
||
await _wait_process_frames(2)
|
||
_check(int(stage.get("_capture_kind")) == SandboxStage.CaptureKind.NONE and not pickup_overlay.action_visible, "Escape clears the Pick Up Prop target marker")
|
||
|
||
|
||
func _run_prop_capture_slice(stage: SandboxStage, character: StickmanRig, prop: PropBlock, character_view: StageCharacterSettingsView, quick_row: Control) -> void:
|
||
var spawner: StageSpawner = stage.get("_spawner") as StageSpawner
|
||
var second_prop: PropBlock = spawner.spawn_prop_asset("crate", prop.global_position + Vector2(130.0, -90.0))
|
||
_check(second_prop != null, "prop capture scenario places a second instance of the same prop asset")
|
||
if second_prop == null:
|
||
return
|
||
stage.call("_finalize_placed_node", second_prop)
|
||
_make_small_prop(second_prop)
|
||
var identity: StageObjectIdentity = stage.get("_object_identity") as StageObjectIdentity
|
||
var renamed: Dictionary = identity.rename(second_prop, "Green Fetch Ball", stage.get_node("World") as Node2D)
|
||
_check(bool(renamed.get("ok", false)), "second prop receives a distinct visible stage name")
|
||
await _wait_process_frames(4)
|
||
var move: Dictionary = await _author_move_through_ui(stage, character, quick_row, "prop", second_prop.global_position)
|
||
if move.is_empty():
|
||
return
|
||
var exact_prop_target: bool = String((move.get("destination", {}) as Dictionary).get("target_prop_id", "")) == second_prop.stage_object_id
|
||
_check(exact_prop_target, "production target capture stores the exact selected second prop ID")
|
||
var move_id: String = String(move.get("action_id", ""))
|
||
var summary: Button = _find_button_containing(character_view.find_child("ActionStep_1", true, false), "Green Fetch Ball")
|
||
var prop_move_valid: bool = exact_prop_target and summary != null and summary.is_visible_in_tree() and summary.text.contains("Green Fetch Ball")
|
||
_check(prop_move_valid, "Step Stack summary displays the selected prop's current stage name")
|
||
if prop_move_valid:
|
||
await _save_capture_now("phase20b_final_exact_prop_move")
|
||
else:
|
||
print("PHASE20B_CAPTURE_SKIPPED name=phase20b_final_exact_prop_move reason=prop_route_or_summary_invalid")
|
||
stage.call("_set_scene_dirty", false)
|
||
var visuals: StageDirectorVisuals = stage.get("_director_visuals") as StageDirectorVisuals
|
||
var projection: Dictionary = _projection_for_action(visuals._plan_projection(character), move_id)
|
||
_check(String(projection.get("label", "")) == "Green Fetch Ball", "route projection uses the exact selected prop's stage name")
|
||
var prop_endpoint: Vector2 = projection.get("end", second_prop.global_position)
|
||
await _send_click(_screen_at(stage, prop_endpoint))
|
||
await _wait_process_frames(3)
|
||
var options: Control = character_view.find_child("ActionOptions_%s" % move_id, true, false) as Control
|
||
await _click_control(_find_button_with_text(options, "Edit action"))
|
||
var action_editor: ActionEditor = stage.get("_action_editor") as ActionEditor
|
||
await _click_control(_find_button_containing(action_editor, "place"))
|
||
var bar: StageUseTargetCaptureBar = stage.find_child("UseTargetCaptureBar", true, false) as StageUseTargetCaptureBar
|
||
var prompt: Label = bar.find_child("Prompt", true, false) as Label if bar != null else null
|
||
var free_place: Vector2 = prop.global_position + Vector2(54.0, 0.0)
|
||
var free_place_screen: Vector2 = _screen_at(stage, free_place)
|
||
root.grab_focus()
|
||
_send_mouse_motion(free_place_screen)
|
||
await _wait_process_frames(3)
|
||
var free_place_prompt_valid: bool = int(stage.get("_capture_kind")) == PLAN_PLACE and bar != null and bar.visible and prompt != null and prompt.text == "Choose a new place or prop for Milo." and not action_editor.visible
|
||
_check(free_place_prompt_valid, "Edit action exposes the production place capture prompt")
|
||
var candidate: Vector2 = visuals.get("_plan_candidate_pointer")
|
||
var free_place_candidate_valid: bool = free_place_prompt_valid and candidate.is_finite() and candidate.distance_to(free_place) < 0.1 and bool(visuals.get("_plan_candidate_valid"))
|
||
_check(free_place_candidate_valid, "candidate pointer reaches a valid free place beside the named prop")
|
||
if free_place_candidate_valid:
|
||
await _save_capture_now("phase20b_final_free_place_near_prop_candidate")
|
||
else:
|
||
print("PHASE20B_CAPTURE_SKIPPED name=phase20b_final_free_place_near_prop_candidate reason=valid_candidate_state_not_reached")
|
||
await _send_click(free_place_screen)
|
||
await _wait_process_frames(4)
|
||
await _click_control(_find_button_with_text(action_editor, "Done"))
|
||
await _wait_process_frames(5)
|
||
var plan: Array[Dictionary] = character.get_action_plan()
|
||
var retargeted: Dictionary = (plan[0]["actions"] as Array)[0] as Dictionary
|
||
var destination: Dictionary = retargeted.get("destination", {})
|
||
_check(String(retargeted.get("action_id", "")) == move_id and String(destination.get("kind", "")) == "place", "Move Edit action to a nearby free point preserves the action ID and place intent")
|
||
var place_projection: Dictionary = _projection_for_action(visuals._plan_projection(character), move_id)
|
||
_check(String(place_projection.get("label", "")).contains("Place 1 · by Blue Fetch Ball"), "nearby free endpoint stays Place 1 with prop context in the route label")
|
||
await _save_capture_now("phase20b_final_free_place_by_prop_committed")
|
||
|
||
|
||
func _author_move_through_ui(stage: SandboxStage, character: StickmanRig, quick_row: Control, target_kind: String, target_world: Vector2) -> Dictionary:
|
||
stage.call("_set_scene_dirty", false)
|
||
var move_button: Button = _find_button_with_text(quick_row, "🚶 Move")
|
||
await _click_control(move_button)
|
||
var action_editor: ActionEditor = stage.get("_action_editor") as ActionEditor
|
||
var target_choice_text: String = "Choose a prop" if target_kind == "prop" else "Choose a place"
|
||
var target_choice: Button = _find_button_containing(action_editor, target_choice_text)
|
||
_check(action_editor != null and action_editor.visible and target_choice != null, "Quick Move opens the production action editor with %s" % target_choice_text)
|
||
if target_choice == null:
|
||
return {}
|
||
await _click_control(target_choice)
|
||
var expected_capture: int = PLAN_PROP if target_kind == "prop" else PLAN_PLACE
|
||
_check(int(stage.get("_capture_kind")) == expected_capture, "visible %s choice enters its production stage capture" % target_kind)
|
||
var target_screen: Vector2 = _screen_at(stage, target_world)
|
||
root.grab_focus()
|
||
_send_mouse_motion(target_screen)
|
||
await _wait_process_frames(2)
|
||
var capture_bar: StageUseTargetCaptureBar = stage.find_child("UseTargetCaptureBar", true, false) as StageUseTargetCaptureBar
|
||
var capture_prompt: Label = capture_bar.find_child("Prompt", true, false) as Label if capture_bar != null else null
|
||
var capture_cancel: Button = capture_bar.find_child("Action_cancel_plan_capture", true, false) as Button if capture_bar != null else null
|
||
var cursor_prompt: Label = stage.get("_tooltip") as Label
|
||
var expected_cursor_prompt: String = "Choose a place for Milo." if target_kind == "place" else "Choose the prop Milo will walk to."
|
||
var placement_overlay: StagePlacementOverlay = stage.get("_placement_overlay") as StagePlacementOverlay
|
||
var capture_layout_valid: bool = int(stage.get("_capture_kind")) == expected_capture and action_editor != null and not action_editor.visible and capture_bar != null and capture_bar.visible and capture_prompt != null and not capture_prompt.visible and capture_cancel != null and capture_cancel.visible and capture_cancel.size.x >= 44.0 and capture_cancel.size.y >= 44.0 and cursor_prompt != null and cursor_prompt.visible and cursor_prompt.text == expected_cursor_prompt and placement_overlay != null and placement_overlay.action_visible and not placement_overlay.action_show_trajectory
|
||
_check(capture_layout_valid, "production %s capture shows a cursor instruction, marker-only preview, and touch-sized Cancel" % target_kind)
|
||
if not capture_layout_valid:
|
||
return {}
|
||
var visuals: StageDirectorVisuals = stage.get("_director_visuals") as StageDirectorVisuals
|
||
var candidate_pointer: Vector2 = visuals.get("_plan_candidate_pointer") if visuals != null else Vector2.INF
|
||
var candidate_valid: bool = visuals != null and candidate_pointer.is_finite() and candidate_pointer.distance_to(target_world) < 0.1 and bool(visuals.get("_plan_candidate_valid"))
|
||
_check(candidate_valid, "actual pointer motion previews a valid requested %s target" % target_kind)
|
||
if candidate_valid:
|
||
await _save_capture_marker_preview(stage, placement_overlay, cursor_prompt, target_world, target_screen, "phase20b_final_%s_candidate_preview" % target_kind)
|
||
else:
|
||
print("PHASE20B_CAPTURE_SKIPPED name=phase20b_final_%s_candidate_preview reason=valid_candidate_state_not_reached" % target_kind)
|
||
await _send_click(target_screen)
|
||
await _wait_process_frames(5)
|
||
_check(action_editor.visible, "target selection returns to the production action editor for Done")
|
||
var done_button: Button = _find_button_with_text(action_editor, "Done")
|
||
_check(done_button != null and done_button.visible, "production Move draft exposes its labeled Done action")
|
||
if done_button == null:
|
||
return {}
|
||
await _click_control(done_button)
|
||
await _wait_process_frames(5)
|
||
var plan: Array[Dictionary] = character.get_action_plan()
|
||
if plan.is_empty():
|
||
_check(false, "production Move commit creates an ordered plan step")
|
||
return {}
|
||
var action_values: Array = plan[plan.size() - 1].get("actions", [])
|
||
if action_values.is_empty() or not action_values[0] is Dictionary:
|
||
_check(false, "production Move commit contains its action payload")
|
||
return {}
|
||
var action: Dictionary = (action_values[0] as Dictionary).duplicate(true)
|
||
_check(String(action.get("type", "")) == "move_to" and String((action.get("destination", {}) as Dictionary).get("kind", "")) == target_kind, "production Move commit preserves the selected %s destination kind" % target_kind)
|
||
_check(bool(stage.get("_dirty")), "valid production Move commit marks the scene dirty")
|
||
return action
|
||
|
||
|
||
func _make_small_prop(prop: PropBlock) -> void:
|
||
var points: PackedVector2Array = PackedVector2Array([Vector2(-20.0, -20.0), Vector2(20.0, -20.0), Vector2(20.0, 20.0), Vector2(-20.0, 20.0)])
|
||
var shapes: Array[Dictionary] = [{
|
||
"shape_type": "polygon",
|
||
"points": points,
|
||
"closed": true,
|
||
"fill_color": "#E6A73B",
|
||
"outline_color": "#3A2A11",
|
||
"outline_width": 3.0,
|
||
}]
|
||
prop.set_custom_shapes(shapes, points)
|
||
|
||
|
||
func _step_has_action_type(step: Dictionary, action_type: String) -> bool:
|
||
for action_value: Variant in step.get("actions", []):
|
||
if action_value is Dictionary and String((action_value as Dictionary).get("type", "")) == action_type:
|
||
return true
|
||
return false
|
||
|
||
|
||
func _find_action_type(step: Dictionary, action_type: String) -> Dictionary:
|
||
for action_value: Variant in step.get("actions", []):
|
||
if action_value is Dictionary and String((action_value as Dictionary).get("type", "")) == action_type:
|
||
return (action_value as Dictionary).duplicate(true)
|
||
return {}
|
||
|
||
|
||
func _first_speech_text(action: Dictionary) -> String:
|
||
var bubbles: Variant = action.get("bubbles", [])
|
||
if not bubbles is Array or (bubbles as Array).is_empty() or not (bubbles as Array)[0] is Dictionary:
|
||
return ""
|
||
return String(((bubbles as Array)[0] as Dictionary).get("text", ""))
|
||
|
||
|
||
func _projection_for_action(projections: Array[Dictionary], action_id: String) -> Dictionary:
|
||
for projection: Dictionary in projections:
|
||
if String(projection.get("action_id", "")) == action_id:
|
||
return projection
|
||
return {}
|
||
|
||
|
||
func _action_id_for_type(step: Dictionary, action_type: String) -> String:
|
||
for action_value: Variant in step.get("actions", []):
|
||
if action_value is Dictionary and String((action_value as Dictionary).get("type", "")) == action_type:
|
||
return String((action_value as Dictionary).get("action_id", ""))
|
||
return ""
|
||
|
||
|
||
func _same_start_frame(events: Array[Dictionary], step_index: int) -> bool:
|
||
var frames: Array[int] = []
|
||
for event: Dictionary in events:
|
||
if int(event.get("step", -1)) == step_index:
|
||
frames.append(int(event.get("frame", -1)))
|
||
return frames.size() == 2 and frames[0] == frames[1]
|
||
|
||
|
||
func _start_frame_for_step(events: Array[Dictionary], step_index: int) -> int:
|
||
for event: Dictionary in events:
|
||
if int(event.get("step", -1)) == step_index:
|
||
return int(event.get("frame", -1))
|
||
return -1
|
||
|
||
|
||
func _end_frame_for_id(events: Array[Dictionary], action_id: String) -> int:
|
||
for event: Dictionary in events:
|
||
if String(event.get("id", "")) == action_id:
|
||
return int(event.get("frame", -1))
|
||
return -1
|
||
|
||
|
||
func _wait_until_actions_end(stage: SandboxStage, events: Array[Dictionary], timeout: float) -> void:
|
||
var elapsed: float = 0.0
|
||
while elapsed < timeout and events.size() < 4 and int(stage.get("_run_state")) == SandboxStage.RunState.RUNNING:
|
||
await create_timer(0.05).timeout
|
||
elapsed += 0.05
|
||
await _wait_frames(3)
|
||
|
||
|
||
func _find_button_with_text(parent: Node, value: String) -> Button:
|
||
if parent == null:
|
||
return null
|
||
for child: Node in parent.find_children("*", "Button", true, false):
|
||
var button: Button = child as Button
|
||
if button != null and button.text == value:
|
||
return button
|
||
return null
|
||
|
||
|
||
func _find_button_containing(parent: Node, value: String) -> Button:
|
||
if parent == null:
|
||
return null
|
||
for child: Node in parent.find_children("*", "Button", true, false):
|
||
var button: Button = child as Button
|
||
if button != null and button.text.contains(value):
|
||
return button
|
||
return null
|
||
|
||
|
||
func _find_button_with_tooltip(parent: Node, value: String) -> Button:
|
||
if parent == null:
|
||
return null
|
||
for child: Node in parent.find_children("*", "Button", true, false):
|
||
var button: Button = child as Button
|
||
if button != null and button.tooltip_text == value:
|
||
return button
|
||
return null
|
||
|
||
|
||
func _screen_at(stage: SandboxStage, world_position: Vector2) -> Vector2:
|
||
return stage.get_viewport().get_canvas_transform() * world_position
|
||
|
||
|
||
func _click_control(control: Control) -> void:
|
||
if control == null:
|
||
_check(false, "requested production UI control exists for pointer click")
|
||
return
|
||
if not control.is_visible_in_tree() or control.get_global_rect().size.x <= 0.0 or control.get_global_rect().size.y <= 0.0:
|
||
_check(false, "requested production UI control is visible and has a rendered hit area")
|
||
return
|
||
var viewport_position: Vector2 = control.get_global_rect().get_center()
|
||
var owner_window: Window = control.get_window()
|
||
var window_id: int = owner_window.get_window_id()
|
||
var screen_position: Vector2 = control.get_screen_position() + control.size * 0.5
|
||
var input_position: Vector2 = screen_position if owner_window.position != Vector2i.ZERO else viewport_position
|
||
_last_click_label = control.text if control is Button else control.name
|
||
_send_mouse_motion(input_position, window_id, screen_position)
|
||
await _wait_process_frames(1)
|
||
var owner_viewport: Viewport = control.get_viewport()
|
||
var hovered_control: Control = owner_viewport.gui_get_hovered_control() as Control
|
||
print("PHASE20B_GUI_HOVER target=%s viewport_position=%s screen_position=%s window=%d window_position=%s hovered=%s" % [control.name, str(input_position), str(screen_position), window_id, str(owner_window.position), hovered_control.name if hovered_control != null else "none"])
|
||
if hovered_control != control:
|
||
print("PHASE20B_GUI_HOVER_MISMATCH target=%s hovered=%s; button activation is checked after press/release" % [control.name, hovered_control.name if hovered_control != null else "none"])
|
||
if control is Button and ((control as Button).text.contains("Message") or (control as Button).text.contains("Choose a place") or (control as Button).text == "Step 4"):
|
||
print("PHASE20B_GUI_CONTROL_CLICK text=%s rect=%s viewport_position=%s screen_position=%s window_id=%d window_pos=%s root_window_id=%d" % [(control as Button).text, str(control.get_global_rect()), str(viewport_position), str(screen_position), window_id, str(owner_window.position), root.get_window_id()])
|
||
print("PHASE20B_GUI_CONTROL_STATE visible=%s inside=%s focus=%s hovered=%s mouse_filter=%d disabled=%s" % [str(control.is_visible_in_tree()), str(control.is_inside_tree()), str(control.has_focus()), str(root.gui_get_hovered_control()), control.mouse_filter, str((control as Button).disabled)])
|
||
await _send_click(input_position, window_id, screen_position)
|
||
_last_click_label = ""
|
||
|
||
|
||
func _send_click(position: Vector2, window_id: int = 0, global_position: Vector2 = Vector2.INF) -> void:
|
||
var trace_this_click: bool = _trace_next_click
|
||
_trace_next_click = false
|
||
var event_global_position: Vector2 = global_position if global_position.is_finite() else position
|
||
await _send_mouse_button(position, true, window_id, event_global_position)
|
||
if trace_this_click:
|
||
print("PHASE20B_TARGET_CLICK after_press capture=%d speech_pending=%s speech_hits=%s hovered=%s focus=%s" % [int(_debug_stage.get("_capture_kind")), str(_debug_stage.get("_speech_bubble_pointer_pending")), str(_debug_stage.get("_speech_bubble_pointer_hits")), str(root.gui_get_hovered_control()), str(root.gui_get_focus_owner())])
|
||
await _wait_process_frames(1)
|
||
if _capture_after_next_click:
|
||
_capture_after_next_click = false
|
||
var target_viewport_position: Vector2 = _screen_at(_debug_stage, Vector2(-20.0, 0.0))
|
||
var stage_window: Window = _debug_stage.get_window()
|
||
var target_screen_position: Vector2 = target_viewport_position + Vector2(stage_window.position)
|
||
await _send_mouse_button(position, false, window_id, event_global_position, target_viewport_position, target_screen_position)
|
||
var frame_started_msec: int = Time.get_ticks_msec()
|
||
var editor: ActionEditor = _debug_stage.get("_action_editor") as ActionEditor
|
||
var capture_bar: StageUseTargetCaptureBar = _debug_stage.find_child("UseTargetCaptureBar", true, false) as StageUseTargetCaptureBar
|
||
var prompt: Label = capture_bar.find_child("Prompt", true, false) as Label if capture_bar != null else null
|
||
var cancel_button: Button = capture_bar.find_child("Action_cancel_plan_capture", true, false) as Button if capture_bar != null else null
|
||
var visuals: StageDirectorVisuals = _debug_stage.get("_director_visuals") as StageDirectorVisuals
|
||
var character_view: StageCharacterSettingsView = _debug_stage.get("_character_settings_view") as StageCharacterSettingsView
|
||
var inspector: StageInspectorPanel = character_view.get_card() as StageInspectorPanel if character_view != null else null
|
||
var capture_kind_matches: bool = not paused and int(_debug_stage.get("_capture_kind")) == PLAN_PLACE and editor != null and not editor.visible
|
||
_check(capture_kind_matches, "post-click state enters place capture and closes the draft editor")
|
||
_check(capture_bar != null and capture_bar.visible and prompt != null and prompt.text == "Choose a place for Milo.", "production capture bar shows the concise place prompt")
|
||
_check(cancel_button != null and cancel_button.visible and cancel_button.size.x >= 44.0 and cancel_button.size.y >= 44.0, "capture Cancel remains visible at a touch-sized target")
|
||
_check(visuals != null and visuals.get("_plan_candidate_rig") != null, "live route candidate remains attached to the selected actor")
|
||
var inspector_clear: bool = capture_bar != null and inspector != null and not capture_bar.get_global_rect().intersects(inspector.get_global_rect())
|
||
if capture_bar != null and inspector != null:
|
||
_check(inspector_clear, "capture bar stays clear of the selected Step Stack inspector")
|
||
var prompt_layout_valid: bool = capture_bar != null and capture_bar.visible and prompt != null and prompt.text == "Choose a place for Milo." and cancel_button != null and cancel_button.visible and cancel_button.size.x >= 44.0 and cancel_button.size.y >= 44.0 and capture_bar.size.x <= 440.0 and capture_bar.size.y <= 160.0 and capture_bar.get_global_rect().position.y >= 64.0 and inspector_clear
|
||
if capture_bar != null:
|
||
var capture_rect: Rect2 = capture_bar.get_global_rect()
|
||
print("PHASE20B_GUI_CAPTURE_BAR rect=%s size=%s prompt=%s" % [str(capture_rect), str(capture_rect.size), prompt.text if prompt != null else "missing"])
|
||
_check(capture_rect.size.x <= 440.0 and capture_rect.size.y <= 160.0 and capture_rect.position.y >= 64.0, "place capture prompt stays compact and below the top toolbar")
|
||
if capture_kind_matches and prompt_layout_valid:
|
||
if OS.get_environment("PHASE20B_SETTLE_CAPTURE_FRAME") == "1":
|
||
await _wait_process_frames(2)
|
||
_save_capture_current_frame("phase20b_place_capture_entry_current_frame")
|
||
else:
|
||
print("PHASE20B_CAPTURE_SKIPPED name=phase20b_place_capture_entry_current_frame reason=capture_ui_assertions_failed")
|
||
if _skip_capture_hover:
|
||
print("PHASE20B_GUI_CAPTURE_ENTRY_ONLY: capture state, draft closure, prompt, Cancel, and layout assertions complete; candidate pointer remains unverified")
|
||
return
|
||
await _wait_process_frames(2)
|
||
var release_candidate_pointer: Vector2 = visuals.get("_plan_candidate_pointer") if visuals != null else Vector2.INF
|
||
var candidate_matches: bool = release_candidate_pointer.is_finite() and release_candidate_pointer.distance_to(Vector2(-20.0, 0.0)) < 0.1 and visuals != null and bool(visuals.get("_plan_candidate_valid"))
|
||
_check(candidate_matches, "post-release viewport motion updates the valid open-ground candidate before preview")
|
||
if capture_kind_matches and prompt_layout_valid and candidate_matches and visuals != null and visuals.get("_plan_candidate_rig") != null:
|
||
_save_capture_current_frame("wide_place_capture_prompt")
|
||
else:
|
||
print("PHASE20B_CAPTURE_SKIPPED name=wide_place_capture_prompt reason=production_capture_state_invalid")
|
||
var candidate_screen_position: Vector2 = _screen_at(_debug_stage, Vector2(-20.0, 0.0))
|
||
root.grab_focus()
|
||
_send_mouse_motion(candidate_screen_position)
|
||
print("PHASE20B_GUI_FRAME mouse_motion_sent window_focused=%s" % root.has_focus())
|
||
await process_frame
|
||
print("PHASE20B_GUI_FRAME after_motion_process_frame")
|
||
await _wait_process_frames(2)
|
||
print("PHASE20B_GUI_FRAME after_motion_process_frames")
|
||
var candidate_pointer: Vector2 = visuals.get("_plan_candidate_pointer") if visuals != null else Vector2.INF
|
||
var candidate_position_valid: bool = candidate_pointer.is_finite() and candidate_pointer.distance_to(Vector2(-20.0, 0.0)) < 0.1
|
||
var candidate_is_valid: bool = visuals != null and bool(visuals.get("_plan_candidate_valid"))
|
||
_check(candidate_position_valid, "actual viewport motion updates the live destination candidate")
|
||
_check(candidate_is_valid, "free open ground previews as a valid destination")
|
||
print("PHASE20B_RENDERED_STATE elapsed_msec=%d capture=%d bar_rect=%s inspector_rect=%s candidate=%s valid=%s focus=%s" % [Time.get_ticks_msec() - frame_started_msec, int(_debug_stage.get("_capture_kind")), str(capture_bar.get_global_rect()) if capture_bar != null else "missing", str(inspector.get_global_rect()) if inspector != null else "missing", str(candidate_pointer), bool(visuals.get("_plan_candidate_valid")) if visuals != null else false, str(root.gui_get_focus_owner())])
|
||
if capture_kind_matches and prompt_layout_valid and candidate_position_valid and candidate_is_valid:
|
||
await _save_capture_after_process_frames("wide_place_capture_bar")
|
||
else:
|
||
print("PHASE20B_CAPTURE_SKIPPED name=wide_place_capture_bar reason=valid_candidate_state_not_reached")
|
||
else:
|
||
await _send_mouse_button(position, false, window_id, event_global_position)
|
||
await _wait_process_frames(2)
|
||
if trace_this_click:
|
||
print("PHASE20B_TARGET_CLICK after_release capture=%d speech_pending=%s speech_hits=%s focus=%s" % [int(_debug_stage.get("_capture_kind")), str(_debug_stage.get("_speech_bubble_pointer_pending")), str(_debug_stage.get("_speech_bubble_pointer_hits")), str(root.gui_get_focus_owner())])
|
||
|
||
|
||
func _send_shift_click(position: Vector2) -> void:
|
||
var event: InputEventMouseButton = InputEventMouseButton.new()
|
||
event.button_index = MOUSE_BUTTON_LEFT
|
||
event.position = position
|
||
event.global_position = position
|
||
event.window_id = root.get_window_id()
|
||
event.shift_pressed = true
|
||
event.pressed = true
|
||
Input.parse_input_event(event)
|
||
await process_frame
|
||
event.pressed = false
|
||
Input.parse_input_event(event)
|
||
await process_frame
|
||
await _wait_frames(2)
|
||
|
||
|
||
func _send_mouse_button(position: Vector2, pressed: bool, window_id: int = 0, global_position: Vector2 = Vector2.INF, post_release_viewport_position: Vector2 = Vector2.INF, post_release_global_position: Vector2 = Vector2.INF) -> void:
|
||
var event: InputEventMouseButton = InputEventMouseButton.new()
|
||
event.button_index = MOUSE_BUTTON_LEFT
|
||
event.position = position
|
||
event.global_position = global_position if global_position.is_finite() else position
|
||
event.window_id = window_id
|
||
event.pressed = pressed
|
||
var trace_input: bool = _last_click_label.contains("Choose a place")
|
||
if trace_input:
|
||
print("PHASE20B_INPUT before_parse label=%s pressed=%s position=%s window_id=%d" % [_last_click_label, str(pressed), str(position), window_id])
|
||
Input.parse_input_event(event)
|
||
_last_injected_mouse_position = position
|
||
if not pressed and post_release_viewport_position.is_finite():
|
||
var event_global_position: Vector2 = post_release_global_position if post_release_global_position.is_finite() else post_release_viewport_position
|
||
_send_mouse_motion(post_release_viewport_position, window_id, event_global_position)
|
||
if trace_input:
|
||
print("PHASE20B_INPUT post_release_viewport_motion label=%s position=%s global_position=%s window_position=%s" % [_last_click_label, str(post_release_viewport_position), str(event_global_position), str(_debug_stage.get_window().position)])
|
||
if trace_input:
|
||
print("PHASE20B_INPUT after_parse label=%s pressed=%s capture=%d" % [_last_click_label, str(pressed), int(_debug_stage.get("_capture_kind"))])
|
||
print("PHASE20B_INPUT before_process_frame label=%s pressed=%s" % [_last_click_label, str(pressed)])
|
||
await process_frame
|
||
if trace_input:
|
||
var bar: StageUseTargetCaptureBar = _debug_stage.find_child("UseTargetCaptureBar", true, false) as StageUseTargetCaptureBar
|
||
print("PHASE20B_INPUT after_process_frame label=%s pressed=%s capture=%d bar_visible=%s" % [_last_click_label, str(pressed), int(_debug_stage.get("_capture_kind")), str(bar != null and bar.visible)])
|
||
|
||
|
||
func _send_mouse_motion(position: Vector2, window_id: int = 0, global_position: Vector2 = Vector2.INF, button_mask: int = 0) -> void:
|
||
var motion: InputEventMouseMotion = InputEventMouseMotion.new()
|
||
motion.position = position
|
||
motion.global_position = global_position if global_position.is_finite() else position
|
||
motion.relative = position - _last_injected_mouse_position
|
||
motion.button_mask = button_mask
|
||
motion.window_id = window_id
|
||
if _last_click_label.contains("Choose a place"):
|
||
print("PHASE20B_INPUT motion_event position=%s global_position=%s relative=%s mask=%d window_id=%d" % [str(motion.position), str(motion.global_position), str(motion.relative), int(motion.button_mask), window_id])
|
||
Input.parse_input_event(motion)
|
||
_last_injected_mouse_position = position
|
||
|
||
|
||
func _send_wheel_down(position: Vector2) -> void:
|
||
var event: InputEventMouseButton = InputEventMouseButton.new()
|
||
event.button_index = MOUSE_BUTTON_WHEEL_DOWN
|
||
event.position = position
|
||
event.global_position = position
|
||
event.factor = 1.0
|
||
event.window_id = root.get_window_id()
|
||
event.pressed = true
|
||
Input.parse_input_event(event)
|
||
await process_frame
|
||
|
||
|
||
func _send_middle_button(position: Vector2, pressed: bool) -> void:
|
||
var event: InputEventMouseButton = InputEventMouseButton.new()
|
||
event.button_index = MOUSE_BUTTON_MIDDLE
|
||
event.position = position
|
||
event.global_position = position
|
||
event.window_id = root.get_window_id()
|
||
event.pressed = pressed
|
||
Input.parse_input_event(event)
|
||
_last_injected_mouse_position = position
|
||
await process_frame
|
||
|
||
|
||
func _send_key(keycode: Key, with_shift: bool = false, with_ctrl: bool = false) -> void:
|
||
var down: InputEventKey = InputEventKey.new()
|
||
down.keycode = keycode
|
||
down.shift_pressed = with_shift
|
||
down.ctrl_pressed = with_ctrl
|
||
down.pressed = true
|
||
Input.parse_input_event(down)
|
||
await process_frame
|
||
var up: InputEventKey = InputEventKey.new()
|
||
up.keycode = keycode
|
||
up.shift_pressed = with_shift
|
||
up.ctrl_pressed = with_ctrl
|
||
up.pressed = false
|
||
Input.parse_input_event(up)
|
||
await process_frame
|
||
|
||
|
||
func _type_text(value: String) -> void:
|
||
for index: int in value.length():
|
||
var character: String = value.substr(index, 1)
|
||
var event: InputEventKey = InputEventKey.new()
|
||
event.keycode = OS.find_keycode_from_string(character.to_upper())
|
||
event.unicode = value.unicode_at(index)
|
||
event.shift_pressed = character != character.to_lower()
|
||
event.pressed = true
|
||
Input.parse_input_event(event)
|
||
await process_frame
|
||
var release: InputEventKey = event.duplicate() as InputEventKey
|
||
release.pressed = false
|
||
Input.parse_input_event(release)
|
||
await process_frame
|
||
|
||
|
||
func _wait_frames(count: int) -> void:
|
||
for _index: int in count:
|
||
await process_frame
|
||
await RenderingServer.frame_post_draw
|
||
|
||
|
||
func _wait_process_frames(count: int) -> void:
|
||
for _index: int in count:
|
||
await process_frame
|
||
|
||
|
||
func _wait_for_status_toast_clear(stage: SandboxStage) -> void:
|
||
for _frame: int in 180:
|
||
if float(stage.get("_toast_timer")) <= 0.0:
|
||
break
|
||
await process_frame
|
||
_check(float(stage.get("_toast_timer")) <= 0.0, "transient validation status clears before the committed-plan capture")
|
||
|
||
|
||
func _say_marker_has_rendered_pixels(visuals: StageDirectorVisuals, projection: Dictionary, image: Image) -> bool:
|
||
if visuals == null or image == null or image.is_empty():
|
||
return false
|
||
var start: Vector2 = projection.get("start", Vector2.INF)
|
||
if not start.is_finite():
|
||
return false
|
||
var marker_center_world: Vector2 = visuals._say_marker_center(start, visuals._zoom())
|
||
var marker_center_screen: Vector2 = visuals.get_global_transform_with_canvas() * marker_center_world
|
||
var sample_rect: Rect2 = Rect2(marker_center_screen - Vector2(24.0, 20.0), Vector2(48.0, 40.0))
|
||
var stage_view_rect: Rect2 = Rect2(Vector2(0.0, 64.0), Vector2(float(root.size.x), float(root.size.y) - 104.0))
|
||
if not stage_view_rect.encloses(sample_rect):
|
||
print("PHASE20B_SAY_MARKER_OUTSIDE_STAGE center_screen=%s sample=%s stage_view=%s" % [str(marker_center_screen), str(sample_rect), str(stage_view_rect)])
|
||
return false
|
||
var yellow_pixels: int = 0
|
||
var start_x: int = maxi(0, int(floorf(sample_rect.position.x)))
|
||
var end_x: int = mini(image.get_width(), int(ceilf(sample_rect.end.x)))
|
||
var start_y: int = maxi(0, int(floorf(sample_rect.position.y)))
|
||
var end_y: int = mini(image.get_height(), int(ceilf(sample_rect.end.y)))
|
||
for y: int in range(start_y, end_y):
|
||
for x: int in range(start_x, end_x):
|
||
var pixel: Color = image.get_pixel(x, y)
|
||
if pixel.r > 0.72 and pixel.g > 0.48 and pixel.b < 0.52:
|
||
yellow_pixels += 1
|
||
print("PHASE20B_SAY_MARKER center_world=%s center_screen=%s sample=%s yellow_pixels=%d" % [str(marker_center_world), str(marker_center_screen), str(sample_rect), yellow_pixels])
|
||
return yellow_pixels >= 20
|
||
|
||
|
||
func _save_capture(name: String) -> void:
|
||
await RenderingServer.frame_post_draw
|
||
var image: Image = root.get_texture().get_image()
|
||
var path: String = _active_capture_dir.path_join("%s.png" % name)
|
||
var error: Error = image.save_png(ProjectSettings.globalize_path(path))
|
||
print("PHASE20B_CAPTURE path=%s size=%s error=%d" % [path, image.get_size(), error])
|
||
_check(error == OK and image.get_size() == root.size, "rendered capture %s is saved at viewport size" % name)
|
||
|
||
|
||
func _save_capture_now(name: String) -> void:
|
||
await process_frame
|
||
await RenderingServer.frame_post_draw
|
||
var image: Image = root.get_texture().get_image()
|
||
var path: String = _active_capture_dir.path_join("%s.png" % name)
|
||
var error: Error = image.save_png(ProjectSettings.globalize_path(path))
|
||
print("PHASE20B_CAPTURE_STABLE path=%s size=%s error=%d" % [path, image.get_size(), error])
|
||
_check(error == OK and image.get_size() == root.size, "settled rendered capture %s is saved at viewport size" % name)
|
||
|
||
|
||
func _save_capture_marker_preview(stage: SandboxStage, placement_overlay: StagePlacementOverlay, cursor_prompt: Label, world_position: Vector2, screen_position: Vector2, name: String) -> void:
|
||
# Synthetic pointer input updates the production capture state but does not
|
||
# move the desktop cursor. Freeze the Stage for this frame and place the
|
||
# cursor marker and its prompt at the injected pointer for a representative
|
||
# rendered screenshot.
|
||
stage.set_process(false)
|
||
placement_overlay.set_action_marker(world_position)
|
||
cursor_prompt.position = screen_position + Vector2(20.0, -20.0)
|
||
await _save_capture_now(name)
|
||
stage.set_process(true)
|
||
|
||
|
||
func _save_capture_after_process_frames(name: String) -> void:
|
||
await _wait_process_frames(2)
|
||
var image: Image = root.get_texture().get_image()
|
||
var path: String = _active_capture_dir.path_join("%s.png" % name)
|
||
var error: Error = image.save_png(ProjectSettings.globalize_path(path))
|
||
print("PHASE20B_CAPTURE_PROCESS_FRAME path=%s size=%s error=%d" % [path, image.get_size(), error])
|
||
_check(error == OK and image.get_size() == root.size, "process-settled capture %s is saved at viewport size" % name)
|
||
|
||
|
||
func _save_capture_current_frame(name: String) -> void:
|
||
var image: Image = root.get_texture().get_image()
|
||
var path: String = _active_capture_dir.path_join("%s.png" % name)
|
||
var error: Error = image.save_png(ProjectSettings.globalize_path(path))
|
||
print("PHASE20B_CAPTURE_CURRENT_FRAME path=%s size=%s error=%d" % [path, image.get_size(), error])
|
||
_check(error == OK and image.get_size() == root.size, "current rendered capture %s is saved at viewport size" % name)
|
||
|
||
|
||
func _check(condition: bool, label: String) -> void:
|
||
if condition:
|
||
print("PHASE20B_GUI_CHECK PASS: %s" % label)
|
||
else:
|
||
_failures += 1
|
||
push_error("PHASE20B_GUI_CHECK FAIL: %s" % label)
|
||
|
||
|
||
func _finish(stage: SandboxStage) -> void:
|
||
if _watchdog != null:
|
||
_watchdog.stop()
|
||
if stage != null and is_instance_valid(stage):
|
||
stage.queue_free()
|
||
await _wait_frames(2)
|
||
|
||
|
||
func _on_gui_watchdog_timeout() -> void:
|
||
push_error("Phase 20b GUI harness exceeded its 45-second frame watchdog")
|
||
print("PHASE20B_GUI_WATCHDOG mode=%s capture=%s paused=%s" % [str(_debug_stage.get("current_mode")) if _debug_stage != null and is_instance_valid(_debug_stage) else "missing", str(_debug_stage.get("_capture_kind")) if _debug_stage != null and is_instance_valid(_debug_stage) else "missing", paused])
|
||
quit(2)
|