Initial project sync

This commit is contained in:
wxm
2026-07-06 22:55:14 -07:00
commit 8cca00d0da
1066 changed files with 58585 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
extends Node
## Per-run battle statistics for the result screen (AnchorV1.0 §19.6 / §21.10).
## Pure fact subscriber: judgement facts, anchor resolutions and phase shifts
## flow in from the EventBus; the rank formula reads only this node.
signal stats_changed
const RANK_THRESHOLDS := [
{"rank": "S", "min": 90.0},
{"rank": "A", "min": 80.0},
{"rank": "B", "min": 65.0},
{"rank": "C", "min": 50.0},
{"rank": "D", "min": 0.0},
]
const COMBO_TARGET := 80.0
const ACCURACY_WEIGHT := 65.0
const ANCHOR_WEIGHT := 20.0
const COMBO_WEIGHT := 15.0
var perfect_count := 0
var good_count := 0
var bad_count := 0
var miss_count := 0
var max_combo := 0
var anchor_success_count := 0
var phase_shift_count := 0
var _tracking := false
func _ready() -> void:
var bus := _event_bus_or_null()
if bus == null:
return
_connect_once(bus, "judgement_made", _on_judgement_made)
_connect_once(bus, "time_anchor_resolved", _on_time_anchor_resolved)
_connect_once(bus, "time_phase_changed", _on_time_phase_changed)
_connect_once(bus, "streak_changed", _on_streak_changed)
func start_run() -> void:
reset()
_tracking = true
func stop_tracking() -> void:
_tracking = false
func reset() -> void:
perfect_count = 0
good_count = 0
bad_count = 0
miss_count = 0
max_combo = 0
anchor_success_count = 0
phase_shift_count = 0
stats_changed.emit()
func summary() -> Dictionary:
return {
"perfect": perfect_count,
"good": good_count,
"bad": bad_count,
"miss": miss_count,
"max_combo": max_combo,
"anchor_success": anchor_success_count,
"phase_shift_count": phase_shift_count,
}
## §19.6: 总分 = 节奏准确分 65 + 锚点控制分 20 + 连击稳定分 15.
func total_score() -> float:
return accuracy_score() + anchor_score() + combo_score()
func accuracy_score() -> float:
var total := perfect_count + good_count + bad_count + miss_count
if total <= 0:
return 0.0
var accuracy_raw := (float(perfect_count) * 1.00 + float(good_count) * 0.85 + float(bad_count) * 0.55) / float(total)
return accuracy_raw * ACCURACY_WEIGHT
func anchor_score() -> float:
var anchor_total := anchor_success_count + phase_shift_count
if anchor_total <= 0:
return ANCHOR_WEIGHT
return float(anchor_success_count) / float(anchor_total) * ANCHOR_WEIGHT
func combo_score() -> float:
return minf(float(max_combo) / COMBO_TARGET, 1.0) * COMBO_WEIGHT
func rank_for_score(score: float) -> String:
for entry: Dictionary in RANK_THRESHOLDS:
if score >= float(entry.get("min", 0.0)):
return str(entry.get("rank", "D"))
return "D"
func current_rank() -> String:
return rank_for_score(total_score())
func _on_judgement_made(quality: StringName, _offset_ms: float, _beat_index: int) -> void:
if not _tracking:
return
match quality:
&"perfect":
perfect_count += 1
&"good":
good_count += 1
&"bad":
bad_count += 1
&"miss":
miss_count += 1
stats_changed.emit()
func _on_time_anchor_resolved(_anchor: Dictionary, held: bool, _judgement: Dictionary) -> void:
if not _tracking:
return
if held:
anchor_success_count += 1
stats_changed.emit()
func _on_time_phase_changed(_previous: StringName, _current: StringName, reason: StringName) -> void:
if not _tracking:
return
# Chart-driven initialisation is not a player-caused shift.
if reason == &"chart_init":
return
phase_shift_count += 1
stats_changed.emit()
func _on_streak_changed(count: int) -> void:
if not _tracking:
return
if count > max_combo:
max_combo = count
stats_changed.emit()
func _connect_once(bus: Node, signal_name: StringName, callback: Callable) -> void:
if bus.has_signal(signal_name) and not bus.is_connected(signal_name, callback):
bus.connect(signal_name, callback)
func _event_bus_or_null() -> Node:
if not is_inside_tree():
return null
return get_tree().root.get_node_or_null("EventBus")