Initial project sync
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user