extends SceneTree ## Difficulty judgement windows + the dynamic bad-window cap ## (AnchorV1.0 §9.5 / §21.8). var failures: Array[String] = [] func _init() -> void: _run.call_deferred() func _run() -> void: var settings: Node = load("res://autoload/game_settings.gd").new() root.add_child(settings) var rhythm: Node = load("res://autoload/rhythm_manager.gd").new() rhythm.set("starts_on_ready", false) root.add_child(rhythm) await process_frame # --- §21.8 window tables --- _expect_vector(settings.call("difficulty_windows", &"easy"), Vector3(0.090, 0.160, 0.260), "easy windows") _expect_vector(settings.call("difficulty_windows", &"normal"), Vector3(0.060, 0.115, 0.215), "normal windows") _expect_vector(settings.call("difficulty_windows", &"hard"), Vector3(0.030, 0.070, 0.170), "hard windows") # --- applying a difficulty rewrites the rhythm manager windows --- var windows: Vector3 = settings.call("difficulty_windows", &"hard") rhythm.call("apply_judgement_windows", windows.x, windows.y, windows.z) _expect_float(float(rhythm.get("perfect_window")), 0.030, "hard perfect window applied") _expect_float(float(rhythm.get("good_window")), 0.070, "hard good window applied") _expect_float(float(rhythm.get("bad_window")), 0.170, "hard bad window applied") # --- default (normal) values live on the manager itself --- var fresh: Node = load("res://autoload/rhythm_manager.gd").new() fresh.set("starts_on_ready", false) root.add_child(fresh) await process_frame _expect_float(float(fresh.get("perfect_window")), 0.060, "default perfect window is Normal") _expect_float(float(fresh.get("good_window")), 0.115, "default good window is Normal") _expect_float(float(fresh.get("bad_window")), 0.215, "default bad window is Normal") # --- §9.5 dynamic cap: bad <= beat_interval * 0.48, cascading --- fresh.set("bpm", 240.0) # beat_time 0.25s → cap 0.12 var effective: Vector3 = fresh.call("effective_windows") _expect_float(effective.z, 0.12, "bad window capped at 48% of the beat") _expect_float(effective.y, 0.115, "good window stays when under the cap") _expect_float(effective.x, 0.060, "perfect window stays when under the cap") fresh.set("bpm", 600.0) # beat_time 0.1s → cap 0.048: everything collapses effective = fresh.call("effective_windows") _expect_float(effective.z, 0.048, "extreme BPM bad cap") _expect_float(effective.y, 0.048, "good window can never exceed the capped bad window") _expect_float(effective.x, 0.048, "perfect window can never exceed the capped good window") # --- the cap is what judgement actually uses --- fresh.set("bpm", 240.0) fresh.set("running", true) var rating: Dictionary = fresh.call("get_rating_for_time", 0.25 + 0.13) _expect_string(str(rating.get("label")), "miss", "an offset outside the capped bad window judges miss") rating = fresh.call("get_rating_for_time", 0.25 + 0.118) _expect_string(str(rating.get("label")), "bad", "an offset between good and the capped bad window judges bad") settings.free() rhythm.free() fresh.free() _finish() func _expect_vector(actual: Vector3, expected: Vector3, label: String) -> void: if (actual - expected).length() > 0.0005: failures.append("%s: expected %s, got %s" % [label, expected, actual]) func _expect_float(actual: float, expected: float, label: String) -> void: if absf(actual - expected) > 0.0005: 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 difficulty windows") quit(0) else: for failure: String in failures: push_error(failure) quit(1)