Initial project sync
This commit is contained in:
@@ -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")
|
||||
Reference in New Issue
Block a user