extends SceneTree const STAGE_SELECTION := preload("res://scripts/stage/stage_selection.gd") const PROP_BLOCK := preload("res://scripts/props/prop_block.gd") var _checks: int = 0 var _failures: int = 0 func _initialize() -> void: call_deferred("_run") func _run() -> void: var world := Node2D.new() root.add_child(world) var camera := Camera2D.new() root.add_child(camera) var selection: StageSelection = STAGE_SELECTION.new(world, camera) as StageSelection var triangle: PropBlock = _make_prop("Triangle") world.add_child(triangle) var triangle_points := PackedVector2Array([Vector2(-50.0, -50.0), Vector2(50.0, -50.0), Vector2(0.0, 50.0)]) triangle.set_custom_shapes([{ "shape_type": "polygon", "points": triangle_points, "closed": true, "fill_color": "#55AAFFFF", "outline_color": "#111111FF", "outline_width": 2.0, }], triangle_points) await process_frame _check(selection.hit_test_all(Vector2.ZERO).has(triangle), "visible polygon interior is selectable") _check(not selection.hit_test_all(Vector2(40.0, 40.0)).has(triangle), "empty point inside polygon AABB is rejected") var front: PropBlock = _make_prop("Front") front.position = Vector2.ZERO front.z_index = 2 world.add_child(front) front.set_custom_shapes([{ "shape_type": "polygon", "points": triangle_points, "closed": true, "fill_color": "#FFAA55FF", "outline_color": "#111111FF", "outline_width": 2.0, }], triangle_points) await process_frame var overlap_hits: Array[Node2D] = selection.hit_test_all(Vector2.ZERO) _check(overlap_hits.size() == 2 and overlap_hits[0] == front and overlap_hits[1] == triangle, "overlap candidates preserve front-to-back draw order") _check(not selection.hit_test_all(Vector2(40.0, 40.0)).has(front), "nearby overlapping prop is excluded outside its visible shape") triangle.position = Vector2(200.0, 0.0) triangle.rotation = PI * 0.5 triangle.scale = Vector2(2.0, 1.0) await process_frame var transformed_center: Vector2 = triangle.to_global(Vector2.ZERO) var transformed_empty: Vector2 = triangle.to_global(Vector2(40.0, 40.0)) _check(selection.hit_test_all(transformed_center).has(triangle), "polygon hit testing follows object translation, rotation, and scale") _check(not selection.hit_test_all(transformed_empty).has(triangle), "transformed empty polygon area remains unselectable") var line_prop: PropBlock = PROP_BLOCK.new() as PropBlock line_prop.name = "Thin Line" line_prop.position = Vector2(400.0, 100.0) world.add_child(line_prop) var line_points := PackedVector2Array([Vector2(-20.0, 0.0), Vector2(20.0, 0.0)]) var line_shape: Array[Dictionary] = [{ "shape_type": "line", "points": line_points, "closed": false, "fill_color": "#00000000", "outline_color": "#FFFFFFFF", "outline_width": 2.0, }] line_prop.set_custom_shapes(line_shape, PackedVector2Array()) await process_frame _check(selection.hit_test_all(Vector2(400.0, 105.0)).has(line_prop), "thin visible line remains selectable within the screen-space touch tolerance") _check(not selection.hit_test_all(Vector2(400.0, 109.0)).has(line_prop), "thin line rejects points beyond the touch tolerance") camera.zoom = Vector2(2.0, 2.0) _check(selection.hit_test_all(Vector2(400.0, 104.0)).has(line_prop), "line touch tolerance scales with camera zoom") _check(not selection.hit_test_all(Vector2(400.0, 105.0)).has(line_prop), "zoomed line does not retain an oversized world-space tolerance") camera.zoom = Vector2.ONE line_prop.modulate.a = 0.0 _check(not selection.hit_test_all(Vector2(400.0, 100.0)).has(line_prop), "fully transparent line is not selectable") line_prop.modulate.a = 1.0 line_prop.visible = false _check(not selection.hit_test_all(Vector2(400.0, 100.0)).has(line_prop), "hidden line is not selectable") print("PHASE 20A HIT TESTING: %d checks, %d failures" % [_checks, _failures]) world.queue_free() camera.queue_free() await process_frame quit(1 if _failures > 0 else 0) func _make_prop(prop_name: String) -> PropBlock: var prop := PROP_BLOCK.new() as PropBlock prop.name = prop_name return prop func _check(condition: bool, message: String) -> void: _checks += 1 if condition: print("PASS: " + message) else: _failures += 1 push_error("FAIL: " + message)