extends SceneTree const SANDBOX_STAGE: PackedScene = preload("res://scenes/stage/sandbox_stage.tscn") const PROP_STAGE_ROLES := preload("res://scripts/props/prop_stage_roles.gd") const CAPTURE_DIR := "res://tests/phase20b_captures/set_down_surface_highlights" var _stage: SandboxStage var _failures: int = 0 func _initialize() -> void: call_deferred("_run") func _run() -> void: root.size = Vector2i(1280, 720) root.gui_embed_subwindows = true var make_dir_error: Error = DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(CAPTURE_DIR)) _check(make_dir_error == OK or make_dir_error == ERR_ALREADY_EXISTS, "capture folder is available") _stage = SANDBOX_STAGE.instantiate() as SandboxStage root.add_child(_stage) await _wait_frames(10) var recovery: ConfirmationDialog = _stage.find_child("AutosaveRecoveryDialog", true, false) as ConfirmationDialog if recovery != null: recovery.hide() _stage.call("_on_grid_toggled", false) var spawner: StageSpawner = _stage.get("_spawner") as StageSpawner var actor: StickmanRig = spawner.spawn_stickman_asset("res://content/stickmen/break.stk", Vector2(-160.0, -190.0)) var supported_prop: PropBlock = spawner.spawn_prop_asset("crate", Vector2(110.0, -10.0)) var unsupported_prop: PropBlock = spawner.spawn_prop_asset("crate", Vector2(300.0, -10.0)) _check(actor != null and supported_prop != null and unsupported_prop != null, "stage fixtures spawn") if actor == null or supported_prop == null or unsupported_prop == null: await _finish() quit(1) return _stage.call("_finalize_placed_node", actor) _stage.call("_finalize_placed_node", supported_prop) _stage.call("_finalize_placed_node", unsupported_prop) var supported_roles: Dictionary = PROP_STAGE_ROLES.new_placement(supported_prop.has_usable_grip()) supported_roles["holds_other_props"] = true supported_prop.set_stage_roles(supported_roles) var unsupported_roles: Dictionary = PROP_STAGE_ROLES.new_placement(unsupported_prop.has_usable_grip()) unsupported_roles["holds_other_props"] = false unsupported_prop.set_stage_roles(unsupported_roles) var set_down_action: Dictionary = { "action_id": "set-down-highlight-step-one", "type": "set_down_prop", "target_position": Vector2(110.0, -10.0), } _check(actor.set_action_plan([{"actions": [set_down_action]}]), "Step 1 contains the Set Down Prop action") _stage.call("set_mode", SandboxStage.StageMode.DIRECT) var selection: StageSelection = _stage.get("_selection") as StageSelection selection.select_only(actor) _stage.call("_refresh_character_settings_card") await _wait_frames(8) var overlay: StagePlacementOverlay = _stage.get("_placement_overlay") as StagePlacementOverlay _check(overlay != null and overlay.surface_world == _stage.get_node("World"), "set-down overlay reads the live stage world") var support_points: PackedVector2Array = overlay.call("_prop_collision_world_points", supported_prop) var excluded_points: PackedVector2Array = overlay.call("_prop_collision_world_points", unsupported_prop) var support_edges: Array[PackedVector2Array] = overlay.call("_top_facing_edges", support_points) var excluded_edges: Array[PackedVector2Array] = overlay.call("_top_facing_edges", excluded_points) _check(not support_edges.is_empty() and not excluded_edges.is_empty(), "both props expose a top edge for role filtering") if support_edges.is_empty() or excluded_edges.is_empty(): await _finish() quit(1) return var target_world: Vector2 = (support_edges[0][0] + support_edges[0][1]) * 0.5 var screen_target: Vector2 = _stage.get_viewport().get_canvas_transform() * target_world await _send_mouse_motion(screen_target) _stage.call("_begin_set_down_position_capture", actor, "Click where to let go", Callable(), Callable()) await _wait_frames(6) var capture_overlay: StageInspectorCaptureOverlay = _stage.get("_inspector_capture_overlay") as StageInspectorCaptureOverlay var capture_prompt: Label = capture_overlay.find_child("CapturePrompt", true, false) as Label if capture_overlay != null else null var cancel_button: Button = capture_overlay.find_child("Capture_cancel_capture", true, false) as Button if capture_overlay != null else null _check(capture_overlay != null and capture_overlay.has_capture(), "Set Down capture shows the inspector capture row") _check(capture_prompt != null and capture_prompt.text == "Set Down Prop", "inspector row names the action without a long hint") _check(cancel_button != null and cancel_button.visible, "the inspector Cancel button stays visible") var action_tooltip: Control = _stage.find_child("ActionTooltip", true, false) as Control _check(action_tooltip == null or not action_tooltip.visible, "set-down capture has no extra floating instruction text") _check(overlay.action_visible and overlay.set_down_preview_visible and not overlay.action_show_trajectory, "set-down capture shows surfaces without the old trajectory") var capture_path: String = CAPTURE_DIR.path_join("set_down_support_surface_filter.png") await RenderingServer.frame_post_draw var image: Image = root.get_texture().get_image() var save_error: Error = image.save_png(ProjectSettings.globalize_path(capture_path)) if image != null and not image.is_empty() else ERR_CANT_CREATE _check(save_error == OK, "focused Godot run saves the set-down surface capture") var canvas_transform: Transform2D = _stage.get_viewport().get_canvas_transform() var supported_mid: Vector2 = canvas_transform * ((support_edges[0][0] + support_edges[0][1]) * 0.5) var unsupported_mid: Vector2 = canvas_transform * ((excluded_edges[0][0] + excluded_edges[0][1]) * 0.5) _check(_has_cyan_near(image, supported_mid), "Props can sit on top surface receives a cyan highlight") _check(not _has_cyan_near(image, unsupported_mid), "prop with Props can sit on top off receives no cyan highlight") print("SET_DOWN_SURFACE_CAPTURE path=%s size=%s supported=%s unsupported=%s" % [capture_path, str(image.get_size()) if image != null else "missing", str(supported_mid), str(unsupported_mid)]) await _send_escape() _check(not capture_overlay.has_capture(), "Escape cancels set-down capture and removes the inspector strip") print("PHASE20B_SET_DOWN_SURFACE_RESULT failures=%d" % _failures) await _finish() quit(1 if _failures > 0 else 0) func _has_cyan_near(image: Image, position: Vector2) -> bool: if image == null or image.is_empty(): return false var center: Vector2i = Vector2i(roundi(position.x), roundi(position.y)) for y: int in range(center.y - 3, center.y + 4): for x: int in range(center.x - 3, center.x + 4): if x < 0 or y < 0 or x >= image.get_width() or y >= image.get_height(): continue var pixel: Color = image.get_pixel(x, y) if pixel.g > pixel.r + 0.08 and pixel.b > pixel.r + 0.05: return true return false func _send_mouse_motion(position: Vector2) -> void: var motion: InputEventMouseMotion = InputEventMouseMotion.new() motion.position = position motion.global_position = position Input.parse_input_event(motion) await _wait_frames(2) func _send_escape() -> void: var escape: InputEventKey = InputEventKey.new() escape.keycode = KEY_ESCAPE escape.pressed = true Input.parse_input_event(escape) await _wait_frames(3) func _wait_frames(count: int) -> void: for _index: int in count: await process_frame await RenderingServer.frame_post_draw func _check(condition: bool, message: String) -> void: if condition: print("PASS: %s" % message) else: _failures += 1 push_error("FAIL: " + message) func _finish() -> void: if _stage != null and is_instance_valid(_stage): _stage.queue_free() await process_frame