extends SceneTree var failures: Array[String] = [] func _init() -> void: _run.call_deferred() func _run() -> void: await process_frame _check_resources_load() await _check_runner_upcoming_and_triggered_once() await _check_runner_broadcasts_empty_action_and_unknown_events() _check_stage7_chart_resource_shape() _check_stage9_chart_resource_shape() _finish() func _check_resources_load() -> void: for path: String in [ "res://resources/chart_event.gd", "res://resources/chart_track.gd", "res://resources/beat_chart.gd", "res://scenes/chart/chart_runner.gd", ]: _expect(load(path) != null, "%s should load" % path) func _check_runner_upcoming_and_triggered_once() -> void: var runner := _make_runner(_make_chart()) var upcoming: Array[StringName] = [] var triggered: Array[StringName] = [] runner.connect("chart_event_upcoming", func(event: Resource, _time_to_event: float) -> void: upcoming.append(_event_identity(event)) ) runner.connect("chart_event_triggered", func(event: Resource) -> void: triggered.append(_event_identity(event)) ) runner.call("update_for_song_time", 0.49) _expect_array(upcoming, [&"boss_combo_1"], "Boss action upcoming should expose action_id at lead window") _expect(triggered.is_empty(), "No event should trigger before event time") runner.call("update_for_song_time", 1.0) runner.call("update_for_song_time", 1.1) _expect_array(triggered, [&"boss_combo_1"], "Boss action should trigger once") runner.call("update_for_song_time", 2.99) runner.call("update_for_song_time", 3.0) runner.call("update_for_song_time", 3.2) _expect(upcoming.count(&"boss_lunging_stab") == 1, "Special boss action upcoming should fire once") _expect(triggered.count(&"boss_lunging_stab") == 1, "Special boss action should trigger once") runner.queue_free() await process_frame func _check_runner_broadcasts_empty_action_and_unknown_events() -> void: var bus := _ensure_event_bus() var mirrored: Array[StringName] = [] bus.connect("chart_event_triggered", func(event: Resource) -> void: mirrored.append(_event_identity(event)) ) var chart_script: Script = load("res://resources/beat_chart.gd") var track_script: Script = load("res://resources/chart_track.gd") var chart: Resource = chart_script.new() var track: Resource = track_script.new() chart.set("chart_id", &"wide_channel_chart") track.set("track_id", &"boss_special") track.set("track_type", &"boss") track.set("events", [ _make_event(1, &"time_anchor", &"Boss", 0.0, &""), _make_event(2, &"phase_warning", &"", 0.0, &""), ]) chart.set("tracks", [track]) var runner := _make_runner(chart) runner.call("update_for_song_time", 0.5) _expect_array(mirrored, [&"time_anchor"], "ChartRunner should broadcast unknown empty-action events") runner.call("update_for_song_time", 1.0) _expect_array(mirrored, [&"time_anchor", &"phase_warning"], "ChartRunner should keep the future event wide channel") runner.queue_free() await process_frame func _check_stage7_chart_resource_shape() -> void: var chart: Resource = load("res://resources/charts/stage7_boss_opening.tres") _expect(chart != null, "Stage-7 boss opening chart should load") if chart == null: return var track_ids: Array[StringName] = [] for track: Resource in chart.get("tracks"): track_ids.append(StringName(str(track.get("track_id")))) for event: Resource in track.get("events"): if StringName(str(event.get("event_type"))) == &"enemy_action": _expect_equal(event.get("target_id"), &"Boss", "Boss chart action target_id should match ActorsContainer/Boss") _expect(track_ids.has(&"track_boss_melee"), "Boss chart should split melee events into their own track") _expect(track_ids.has(&"track_boss_ranged"), "Boss chart should split ranged events into their own track") _expect(track_ids.has(&"track_boss_special"), "Boss chart should reserve a special track") func _check_stage9_chart_resource_shape() -> void: var chart: Resource = load("res://resources/charts/stage9_boss_duel.tres") _expect(chart != null, "Stage-9 boss duel chart should load") if chart == null: return _expect_equal(chart.get("chart_id"), &"stage9_boss_duel", "Stage-9 chart id") _expect(int(chart.get("total_beats")) >= 64, "Stage-9 chart should cover a full 16-bar duel at 4/4") var track_ids: Array[StringName] = [] var action_ids: Array[StringName] = [] for track: Resource in chart.get("tracks"): track_ids.append(StringName(str(track.get("track_id")))) for event: Resource in track.get("events"): if StringName(str(event.get("event_type"))) != &"enemy_action": continue _expect_equal(event.get("target_id"), &"Boss", "Stage-9 Boss event target_id should match ActorsContainer/Boss") action_ids.append(StringName(str(event.get("action_id")))) _expect(track_ids.has(&"track_boss_melee"), "Stage-9 chart should keep melee track") _expect(track_ids.has(&"track_boss_ranged"), "Stage-9 chart should keep ranged track") _expect(track_ids.has(&"track_boss_special"), "Stage-9 chart should keep special track") for required_action: StringName in [&"boss_combo_1", &"boss_combo_2", &"boss_combo_3", &"boss_lunging_stab", &"boss_shoot_1", &"boss_shoot_2", &"boss_2way_shoot", &"boss_retreat_dash"]: _expect(action_ids.has(required_action), "Stage-9 chart should include %s" % required_action) func _make_event(beat: int, event_type: StringName, target_id := &"", lead_beats := 1.0, action_id := &"") -> Resource: var event_script: Script = load("res://resources/chart_event.gd") var event: Resource = event_script.new() event.set("beat_index", beat) event.set("event_type", event_type) event.set("target_id", target_id) event.set("lead_beats", lead_beats) event.set("action_id", action_id) return event func _make_chart() -> Resource: var chart_script: Script = load("res://resources/beat_chart.gd") var track_script: Script = load("res://resources/chart_track.gd") var chart: Resource = chart_script.new() var melee: Resource = track_script.new() var special: Resource = track_script.new() chart.set("chart_id", &"test_boss_chart") melee.set("track_id", &"track_boss_melee") melee.set("track_type", &"boss") melee.set("events", [ _make_event(2, &"enemy_action", &"Boss", 1.0, &"boss_combo_1"), ]) special.set("track_id", &"track_boss_special") special.set("track_type", &"boss") special.set("events", [ _make_event(6, &"enemy_action", &"Boss", 2.0, &"boss_lunging_stab"), ]) chart.set("tracks", [melee, special]) return chart func _make_runner(chart: Resource) -> Node: var runner_script: Script = load("res://scenes/chart/chart_runner.gd") var runner: Node = runner_script.new() runner.set("beat_time_override", 0.5) runner.call("set_chart", chart) root.add_child(runner) return runner func _ensure_event_bus() -> Node: var existing := root.get_node_or_null("EventBus") if existing != null: return existing var bus: Node = load("res://autoload/event_bus.gd").new() bus.name = "EventBus" root.add_child(bus) return bus func _event_identity(event: Resource) -> StringName: var action_id := StringName(str(event.get("action_id"))) if not action_id.is_empty(): return action_id return StringName(str(event.get("event_type"))) func _expect(condition: bool, label: String) -> void: if not condition: failures.append(label) func _expect_equal(actual: Variant, expected: Variant, label: String) -> void: if actual != expected: failures.append("%s: expected %s, got %s" % [label, expected, actual]) func _expect_array(actual: Array, expected: Array, 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 chart layer") quit(0) else: for failure: String in failures: push_error(failure) quit(1)