extends SceneTree var failures: Array[String] = [] func _init() -> void: _run.call_deferred() func _run() -> void: await process_frame _check_schema_defaults_and_track_propagation() await _check_runner_filtering_and_initial_phase() _check_validation_tool() _finish() func _check_schema_defaults_and_track_propagation() -> void: var event: Resource = load("res://resources/chart_event.gd").new() _expect_equal(event.get("time_phase_mask"), &"both", "ChartEvent default mask should be both") _expect_bool(bool(event.call("matches_time_phase", &"past")), true, "mask both should match past") _expect_bool(bool(event.call("matches_time_phase", &"future")), true, "mask both should match future") event.set("time_phase_mask", &"past") _expect_bool(bool(event.call("matches_time_phase", &"future")), false, "mask past should reject future") var track: Resource = load("res://resources/chart_track.gd").new() track.set("time_phase_mask", &"future") var default_event: Resource = load("res://resources/chart_event.gd").new() var explicit_event: Resource = load("res://resources/chart_event.gd").new() explicit_event.set("time_phase_mask", &"past") track.set("events", [default_event, explicit_event]) track.call("sorted_events") _expect_equal(default_event.get("time_phase_mask"), &"future", "track mask should flow into default events") _expect_equal(explicit_event.get("time_phase_mask"), &"past", "explicitly masked events keep their own mask") var chart: Resource = load("res://resources/beat_chart.gd").new() _expect_equal(chart.get("initial_time_phase"), &"past", "BeatChart initial_time_phase should default to past") func _check_runner_filtering_and_initial_phase() -> void: var bus: Node = load("res://autoload/event_bus.gd").new() bus.name = "EventBus" root.add_child(bus) var manager: Node = load("res://autoload/time_phase_manager.gd").new() manager.name = "TimePhaseManager" root.add_child(manager) var system: Node = load("res://autoload/time_anchor_system.gd").new() system.name = "TimeAnchorSystem" system.set("periodic_scheduling_enabled", false) root.add_child(system) await process_frame var past_event: Resource = load("res://resources/chart_event.gd").new() past_event.set("event_id", &"mask_past_event") past_event.set("beat_index", 1) past_event.set("event_type", &"phase_warning") past_event.set("time_phase_mask", &"past") var future_event: Resource = load("res://resources/chart_event.gd").new() future_event.set("event_id", &"mask_future_event") future_event.set("beat_index", 1) future_event.set("event_type", &"phase_warning") future_event.set("time_phase_mask", &"future") var track: Resource = load("res://resources/chart_track.gd").new() track.set("track_id", &"mask_track") track.set("events", [past_event, future_event]) var chart: Resource = load("res://resources/beat_chart.gd").new() chart.set("chart_id", &"mask_chart") chart.set("tracks", [track]) chart.set("initial_time_phase", &"future") var runner: Node = load("res://scenes/chart/chart_runner.gd").new() runner.name = "ChartRunner" runner.set("chart", chart) runner.set("beat_time_override", 0.5) root.add_child(runner) await process_frame _expect_equal(manager.get("current_time_phase"), &"future", "runner should apply the chart's initial_time_phase") var triggered: Array = [] bus.connect("chart_event_triggered", func(event: Resource) -> void: triggered.append(StringName(str(event.get("event_id")))) ) # Trigger-time final ruling: only the mask that matches the live phase dispatches. runner.call("update_for_song_time", 1.0) _expect_bool(triggered.has(&"mask_future_event"), true, "matching mask should dispatch") _expect_bool(triggered.has(&"mask_past_event"), false, "mismatching mask should be skipped") # Skipped events are never re-dispatched after the phase flips back. manager.call("set_time_phase", &"past", &"debug") runner.call("update_for_song_time", 2.0) _expect_bool(triggered.has(&"mask_past_event"), false, "skipped events must not be re-dispatched") # force_time_phase chart events route through TimeAnchorSystem to the manager. var reasons: Array = [] bus.connect("time_phase_changed", func(_previous: StringName, _current: StringName, reason: StringName) -> void: reasons.append(reason) ) var force_event: Resource = load("res://resources/chart_event.gd").new() force_event.set("event_id", &"force_future") force_event.set("event_type", &"force_time_phase") force_event.set("payload", {"target_time_phase": &"future"}) bus.emit_signal("chart_event_triggered", force_event) _expect_equal(manager.get("current_time_phase"), &"future", "force_time_phase should switch the phase") _expect_bool(reasons.has(&"chart_forced"), true, "forced switch should carry the chart_forced reason") # Same-phase force does not broadcast a fact. var reason_count := reasons.size() bus.emit_signal("chart_event_triggered", force_event) _expect_int(reasons.size(), reason_count, "forcing the current phase must not broadcast") # runner.reset discards armed anchors and reapplies the initial phase. system.call("schedule_time_anchor", 30, &"chart") runner.call("reset") _expect_int(int(system.call("anchored_beat_count")), 0, "chart reset should discard armed anchors") _expect_equal(manager.get("current_time_phase"), &"future", "chart reset should restore the chart initial phase") func _check_validation_tool() -> void: var tool_script: Script = load("res://tools/time_anchor_chart_tool.gd") var chart: Resource = load("res://resources/beat_chart.gd").new() var track: Resource = load("res://resources/chart_track.gd").new() var half_beat_anchor: Resource = load("res://resources/chart_event.gd").new() half_beat_anchor.set("event_id", &"bad_half_beat") half_beat_anchor.set("event_type", &"time_anchor") half_beat_anchor.set("beat_index", 3) half_beat_anchor.set("subdivision", 1) half_beat_anchor.set("subdivisions_per_beat", 2) var duplicate_a: Resource = load("res://resources/chart_event.gd").new() duplicate_a.set("event_id", &"dup_a") duplicate_a.set("event_type", &"time_anchor") duplicate_a.set("beat_index", 8) var duplicate_b: Resource = load("res://resources/chart_event.gd").new() duplicate_b.set("event_id", &"dup_b") duplicate_b.set("event_type", &"time_anchor") duplicate_b.set("beat_index", 8) var bad_mask: Resource = load("res://resources/chart_event.gd").new() bad_mask.set("event_id", &"bad_mask") bad_mask.set("event_type", &"phase_warning") bad_mask.set("beat_index", 10) bad_mask.set("time_phase_mask", &"yesterday") var bad_force: Resource = load("res://resources/chart_event.gd").new() bad_force.set("event_id", &"bad_force") bad_force.set("event_type", &"force_time_phase") bad_force.set("beat_index", 12) track.set("events", [half_beat_anchor, duplicate_a, duplicate_b, bad_mask, bad_force]) chart.set("tracks", [track]) var errors: Array = tool_script.call("validate_chart", chart) _expect_int(errors.size(), 4, "validator should flag half-beat anchor, duplicate anchors, bad mask and bad force payload") var clean_errors: Array = tool_script.call("validate_chart", load("res://resources/charts/stage9_boss_duel.tres")) _expect_int(clean_errors.size(), 0, "shipping chart should validate cleanly") var profile_errors: Array = tool_script.call("validate_time_phase_profile", load("res://resources/time_phase/profile_past_strong_enemy.tres")) _expect_int(profile_errors.size(), 0, "shipping time phase profile should validate cleanly") var generated: Array = tool_script.call("generate_periodic_time_anchor_events", 16, 64) _expect_int(generated.size(), 3, "generator should emit anchors at 16/32/48 for 64 beats") var simulated: Array = tool_script.call("simulate_periodic_schedule", load("res://resources/time_anchor_frequency_default.tres"), 0, 28, [18] as Array[int]) _expect_equal(str(simulated), str([4, 8, 12, 16, 22, 26]), "simulation should restart the 4-beat window at the explicit beat") 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_int(actual: int, expected: int, label: String) -> void: if actual != expected: failures.append("%s: expected %d, got %d" % [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 chart time phase mask") quit(0) else: for failure: String in failures: push_error(failure) quit(1)