extends SceneTree var failures: Array[String] = [] func _init() -> void: _run.call_deferred() func _run() -> void: root.size = Vector2i(1152, 648) await _check_main_ui_authored_layout() await _check_stage_x1_camera_composition() _finish() func _check_main_ui_authored_layout() -> void: var scene: PackedScene = load("res://scenes/ui/rhythm_track.tscn") if scene == null: failures.append("Could not load rhythm_track.tscn") return # Movers glide with the live rhythm clock; freeze it so this layout check # reads the authored start positions deterministically. var rhythm := root.get_node_or_null("RhythmManager") if rhythm != null and rhythm.has_method("stop_manager"): rhythm.call("stop_manager") var track := scene.instantiate() as Control root.add_child(track) await process_frame _expect_float(track.anchor_left, 0.5, "RhythmTrack should stay centered") _expect_float(track.anchor_right, 0.5, "RhythmTrack should stay centered") _expect_bool(track.has_node("JudgementLabel"), true, "RhythmTrack should own its judgement label") var track_rect := _control_rect(track) _expect_float(track_rect.size.x, 864.0, "RhythmTrack should keep the authored scene width") _expect_float(track_rect.size.y, 240.0, "RhythmTrack should keep the x1 authored scene height") _expect_float(track_rect.position.y, 54.0, "RhythmTrack should keep the x1 authored scene y") var readout_row := track.get_node_or_null("HudReadoutRow") as Control var buff_row := track.get_node_or_null("BuffPipRow") as Control var judgement_art := track.get_node_or_null("JudgementArt") as Control _expect_bool(readout_row != null, true, "RhythmTrack should own the combo / ATK readout row") _expect_bool(buff_row != null, true, "RhythmTrack should own the seven-slot buff row") _expect_bool(judgement_art != null, true, "RhythmTrack should own the judgement art node") if readout_row != null: _expect_float(_control_rect(readout_row).position.y, 97.2, "Combo and ATK readouts should use the x1 responsive layout") if buff_row != null: _expect_float(_control_rect(buff_row).position.y, 95.04, "Buff pips should use the x1 responsive layout") if judgement_art != null: _expect_float(_control_rect(judgement_art).position.y, 108.48, "Judgement art should use the x1 responsive layout") var line := track.get_node("TrackLine") as Control var center_base := track.get_node("CenterBase") as Control var center_flash := track.get_node("CenterFlash") as Control var left_mover := track.get_node("LeftMover") as Control var right_mover := track.get_node("RightMover") as Control var line_center_y := _control_rect(line).get_center().y _expect_float((_control_rect(center_base).get_center()).y, line_center_y, "Center anchor should sit on the authored rhythm track horizontal axis") _expect_float((_control_rect(left_mover).get_center()).y, line_center_y, "Left rhythm point should start on the authored horizontal axis") _expect_float((_control_rect(right_mover).get_center()).y, line_center_y, "Right rhythm point should start on the authored horizontal axis") _expect_float((_control_rect(left_mover).get_center()).x, 174.41, "Left rhythm point should start at the x1 responsive position") _expect_float((_control_rect(right_mover).get_center()).x, 689.59, "Right rhythm point should start at the x1 responsive position") _expect_float((track.get("track_center") as Vector2).y, line_center_y, "Rhythm point convergence should use the authored track horizontal axis") _expect_bool(center_flash.z_index > left_mover.z_index, true, "Center hit effect should render above moving rhythm points") _expect_bool(center_base.z_index > center_flash.z_index, true, "Center icon should render above the hit effect") _expect_bool(center_base.z_index > left_mover.z_index, true, "Center icon should cover rhythm points when they reach the middle") track.free() var ui_scene: PackedScene = load("res://scenes/ui/main_ui.tscn") if ui_scene == null: failures.append("Could not load main_ui.tscn") return var ui := ui_scene.instantiate() root.add_child(ui) await process_frame _expect_bool(ui is Control, true, "MainUI should stay as the x1 Control scene hosted by Main/UILayer") var status_node := ui.get_node("StatusBars") as Control var rhythm_node := ui.get_node("RhythmTrack") as Control var boss_node := ui.get_node("BossStatus") as Control var combo_node := ui.get_node("ComboWindow") as Control _expect_vector(status_node.scale, Vector2(1.125, 1.125), "Player status HUD runtime scale should match x1") _expect_vector(status_node.position, Vector2(30.6, 25.2), "Player status HUD runtime position should match x1") _expect_vector(rhythm_node.scale, Vector2(1.0, 1.0), "Rhythm HUD runtime scale should match x1") _expect_vector(rhythm_node.pivot_offset, Vector2(432.0, 0.0), "Rhythm HUD should keep its top-center pivot") _expect_vector(boss_node.scale, Vector2(1.062, 1.062), "Boss status HUD runtime scale should match x1") _expect_vector(boss_node.pivot_offset, Vector2.ZERO, "Boss status HUD should keep the x1 top-left pivot") _expect_vector(combo_node.scale, Vector2(1.152, 1.152), "Combo slot HUD runtime scale should match x1") _expect_vector(combo_node.position, Vector2(37.8, 369.0), "Combo slots runtime position should match x1") _expect_bool(not bool(ui.get_node("TimePhaseHud").visible), true, "TimePhaseHud should use the authored hidden default") _expect_bool(not bool(ui.get_node("DebugPanel").visible), true, "DebugPanel should use the authored hidden default") ui.free() func _check_stage_x1_camera_composition() -> void: var scene: PackedScene = load("res://scenes/stage/stage.tscn") if scene == null: failures.append("Could not load stage.tscn") return var stage := scene.instantiate() var initial_player_position := (stage.get_node("ActorsContainer/Player") as Node2D).position var initial_melee_position := (stage.get_node("ActorsContainer/JinZhanMinion") as Node2D).position var initial_boss_position := (stage.get_node("ActorsContainer/Boss") as Node2D).position root.add_child(stage) await process_frame _expect_bool(bool(stage.get("use_authored_camera_view")), false, "Stage should use the x1 follow-camera baseline") var player := stage.get_node("ActorsContainer/Player") as Node2D var melee := stage.get_node("ActorsContainer/JinZhanMinion") as Node2D var boss := stage.get_node("ActorsContainer/Boss") as Node2D var camera := stage.get_node("Camera2D") as Camera2D var gate := stage.get_node("BossRoomGate") _expect_vector(camera.global_position, Vector2(1180, 395), "Camera2D should start over the left-spawn player (new2)") _expect_vector(camera.zoom, Vector2(1.25, 1.25), "Camera2D should keep the x1 reference zoom") _expect_vector(initial_player_position, Vector2(1180, 560), "Player should spawn at the map's left side (new2)") _expect_vector(initial_melee_position, Vector2(2200, 560), "Initial minion should use the left-shifted safe authored anchor") _expect_vector(initial_boss_position, Vector2(2520, 560), "Boss should keep the x1 authored anchor") _expect_float(player.global_position.y, melee.global_position.y, "Player and minion anchors should share one horizontal line") _expect_float(player.global_position.y, boss.global_position.y, "Player and Boss anchors should share one horizontal line") player.global_position = Vector2(float(gate.get("gate_x")), player.global_position.y) stage.call("_update_camera_follow") var boss_entry_x := float(gate.get("gate_x")) + float(gate.get("wall_thickness")) var half_view_width := float(root.size.x) / (2.0 * camera.zoom.x) var sealed_camera_max_x := boss_entry_x - half_view_width _expect_float(camera.global_position.x, sealed_camera_max_x, "Sealed Boss room should clamp camera before the boss-room entry line") _expect_float(camera.global_position.x + half_view_width, boss_entry_x, "Sealed camera view should not reveal the boss-room entry area") gate.call("unseal") await process_frame stage.call("_update_camera_follow") _expect_vector(camera.global_position, player.global_position + Vector2(0.0, -165.0), "Camera2D should follow freely after the Boss room path unlocks") var projectile_screen_y := _world_to_screen(boss.call("projectile_spawn_position", null), camera.global_position, camera.zoom, Vector2(1152, 648)).y var boss_bounds := _visible_sprite_bounds_global(boss.get_node("Visual/CharacterSprite") as Sprite2D) var boss_top_screen_y := _world_to_screen(Vector2(boss.global_position.x, boss_bounds.position.y), camera.global_position, camera.zoom, Vector2(1152, 648)).y var boss_bottom_screen_y := _world_to_screen(Vector2(boss.global_position.x, boss_bounds.end.y), camera.global_position, camera.zoom, Vector2(1152, 648)).y var boss_height_screen := boss_bottom_screen_y - boss_top_screen_y _expect_ratio(projectile_screen_y - boss_top_screen_y, boss_height_screen, 0.30, 0.62, "Boss projectile should spawn from the upper body, not below the feet") _expect_float(absf(float(player.get_node("Visual").scale.x)), 1.0, "Player visual scale should match x1") _expect_float(float(player.get_node("Visual").scale.y), 1.0, "Player visual scale should match x1") _expect_float(absf(float(melee.get_node("Visual").scale.x)), 2.0, "Minion visual scale should match x1") _expect_float(float(melee.get_node("Visual").scale.y), 2.0, "Minion visual scale should match x1") _expect_float(absf(float(boss.get_node("Visual").scale.x)), 2.0, "Boss visual scale should match x1") _expect_float(float(boss.get_node("Visual").scale.y), 2.0, "Boss visual scale should match x1") for sprite_path: String in [ "Ground/ArtLayer/PastBackground", "Ground/ArtLayer/FutureBackground", "ActorsContainer/Player/Visual/CharacterSprite", "ActorsContainer/Player/Visual/FxOverlay", "ActorsContainer/Boss/Visual/CharacterSprite", "ActorsContainer/Boss/Visual/FxOverlay", ]: var sprite := stage.get_node_or_null(sprite_path) as Sprite2D _expect_bool(sprite != null, true, "%s should exist for texture filtering" % sprite_path) if sprite != null: _expect_bool(sprite.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR, true, "%s should use linear texture interpolation when scaled" % sprite_path) stage.free() func _control_rect(control: Control) -> Rect2: return Rect2( Vector2(control.offset_left, control.offset_top), Vector2(control.offset_right - control.offset_left, control.offset_bottom - control.offset_top) ) func _world_to_screen(world_position: Vector2, camera_position: Vector2, zoom: Vector2, viewport_size: Vector2) -> Vector2: return (world_position - camera_position) * zoom + viewport_size * 0.5 func _visible_sprite_bounds_global(sprite: Sprite2D) -> Rect2: if sprite == null or sprite.texture == null: return Rect2() var image := sprite.texture.get_image() if image == null or image.is_empty(): return Rect2() var hframes := maxi(1, sprite.hframes) var vframes := maxi(1, sprite.vframes) var frame_width := image.get_width() / hframes var frame_height := image.get_height() / vframes var frame_index := clampi(sprite.frame, 0, hframes * vframes - 1) var column := frame_index % hframes var row := int(frame_index / hframes) var left := frame_width var right := -1 var top := frame_height var bottom := -1 for y: int in range(frame_height): for x: int in range(frame_width): var pixel := image.get_pixel(column * frame_width + x, row * frame_height + y) if pixel.a > 0.01: left = mini(left, x) right = maxi(right, x) top = mini(top, y) bottom = maxi(bottom, y) if right < left or bottom < top: return Rect2() var top_left := sprite.to_global(sprite.offset + Vector2(left, top)) var bottom_right := sprite.to_global(sprite.offset + Vector2(right, bottom)) return Rect2( Vector2(minf(top_left.x, bottom_right.x), minf(top_left.y, bottom_right.y)), Vector2(absf(bottom_right.x - top_left.x), absf(bottom_right.y - top_left.y)) ) func _expect_ratio(actual: float, basis: float, minimum: float, maximum: float, label: String) -> void: var ratio := actual / maxf(1.0, basis) if ratio < minimum or ratio > maximum: failures.append("%s: expected ratio %.3f..%.3f, got %.3f" % [label, minimum, maximum, ratio]) func _expect_float(actual: float, expected: float, label: String) -> void: if absf(actual - expected) > 0.02: failures.append("%s: expected %.3f, got %.3f" % [label, expected, actual]) func _expect_vector(actual: Vector2, expected: Vector2, label: String) -> void: if actual.distance_to(expected) > 0.03: failures.append("%s: expected %s, got %s" % [label, expected, actual]) func _expect_bool(actual: bool, expected: bool, label: String) -> void: if actual != expected: failures.append("%s: expected %s, got %s" % [label, expected, actual]) func _finish() -> void: if failures.is_empty(): print("PASS rhythm ui layout") quit(0) else: for failure: String in failures: push_error(failure) quit(1)