extends SceneTree ## Result-screen statistics + Rank formula (AnchorV1.0 §19.6 / §21.10). var failures: Array[String] = [] func _init() -> void: _run.call_deferred() func _run() -> void: var stats: Node = load("res://autoload/game_stats.gd").new() root.add_child(stats) await process_frame stats.call("start_run") # --- fact subscription through a synthetic bus feed --- stats.call("_on_judgement_made", &"perfect", 0.0, 1) stats.call("_on_judgement_made", &"good", 20.0, 2) stats.call("_on_judgement_made", &"bad", 90.0, 3) stats.call("_on_judgement_made", &"miss", 300.0, 4) stats.call("_on_time_anchor_resolved", {"beat": 4}, true, {"label": "perfect"}) stats.call("_on_time_anchor_resolved", {"beat": 8}, false, {}) stats.call("_on_time_phase_changed", &"past", &"future", &"time_anchor_broken") stats.call("_on_time_phase_changed", &"past", &"future", &"chart_init") stats.call("_on_streak_changed", 12) stats.call("_on_streak_changed", 3) _expect_int(int(stats.get("perfect_count")), 1, "perfect count") _expect_int(int(stats.get("good_count")), 1, "good count") _expect_int(int(stats.get("bad_count")), 1, "bad count") _expect_int(int(stats.get("miss_count")), 1, "miss count") _expect_int(int(stats.get("anchor_success_count")), 1, "anchor success only on held") _expect_int(int(stats.get("phase_shift_count")), 1, "chart_init phase change must not count as a shift") _expect_int(int(stats.get("max_combo")), 12, "max combo keeps the highest streak") # --- accuracy: (1*1.00 + 1*0.85 + 1*0.55 + 0) / 4 * 65 --- _expect_float(float(stats.call("accuracy_score")), (1.0 + 0.85 + 0.55) / 4.0 * 65.0, "accuracy score formula") # --- anchor: 1 success / (1 + 1 shift) * 20 --- _expect_float(float(stats.call("anchor_score")), 10.0, "anchor control score formula") # --- combo: min(12/80, 1) * 15 --- _expect_float(float(stats.call("combo_score")), 12.0 / 80.0 * 15.0, "combo stability score formula") # --- rank bands (§19.6) --- _expect_string(str(stats.call("rank_for_score", 95.0)), "S", "rank S from 90") _expect_string(str(stats.call("rank_for_score", 90.0)), "S", "rank S boundary") _expect_string(str(stats.call("rank_for_score", 85.0)), "A", "rank A band") _expect_string(str(stats.call("rank_for_score", 70.0)), "B", "rank B band") _expect_string(str(stats.call("rank_for_score", 55.0)), "C", "rank C band") _expect_string(str(stats.call("rank_for_score", 20.0)), "D", "rank D band") # --- anchor_total = 0 → full anchor score (§19.6) --- stats.call("reset") _expect_float(float(stats.call("anchor_score")), 20.0, "no anchors at all should give the full 20 anchor points") # --- perfect run: full 100 / S --- stats.call("start_run") for index: int in range(80): stats.call("_on_judgement_made", &"perfect", 0.0, index) stats.call("_on_streak_changed", 80) stats.call("_on_time_anchor_resolved", {"beat": 4}, true, {"label": "perfect"}) _expect_float(float(stats.call("total_score")), 100.0, "perfect run should score 100") _expect_string(str(stats.call("current_rank")), "S", "perfect run rank") # --- tracking stops after stop_tracking --- stats.call("stop_tracking") stats.call("_on_judgement_made", &"miss", 0.0, 99) _expect_int(int(stats.get("miss_count")), 0, "no counting after stop_tracking") stats.free() _finish() 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_float(actual: float, expected: float, label: String) -> void: if absf(actual - expected) > 0.001: failures.append("%s: expected %f, got %f" % [label, expected, actual]) func _expect_string(actual: String, expected: String, 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 game stats rank") quit(0) else: for failure: String in failures: push_error(failure) quit(1)