Initial project sync
This commit is contained in:
@@ -0,0 +1,561 @@
|
||||
class_name RhythmTrack
|
||||
extends Control
|
||||
|
||||
@onready var judgement_label: Label = $JudgementLabel
|
||||
@onready var track_background: TextureRect = $TrackBackground
|
||||
@onready var track_line: TextureRect = $TrackLine
|
||||
@onready var center_base: TextureRect = $CenterBase
|
||||
@onready var center_flash: TextureRect = $CenterFlash
|
||||
@onready var left_mover: TextureRect = $LeftMover
|
||||
@onready var right_mover: TextureRect = $RightMover
|
||||
@onready var chart_marker_container: Control = $ChartMarkerContainer
|
||||
@onready var combo_count_label: Label = $HudReadoutRow/ComboCountLabel
|
||||
@onready var attack_buff_value_label: Label = $HudReadoutRow/AttackBuffValueLabel
|
||||
@onready var buff_pip_row: HBoxContainer = $BuffPipRow
|
||||
@onready var judgement_art: TextureRect = $JudgementArt
|
||||
|
||||
@export var bpm := 80.0
|
||||
@export var responsive_layout := true
|
||||
|
||||
# Full track skin per time phase: map A (past) is the purple/gold set, map B
|
||||
# (future) the red "d" set. Time anchors never add screen elements — the mover
|
||||
# itself swaps to the special-beat texture. A held special beat shows the
|
||||
# OTHER phase's hit emblem (cross variant) while the flash stays own-phase.
|
||||
const TRACK_SKINS := {
|
||||
&"past": {
|
||||
&"background": preload("res://assets/ui/rhythm/b.png"),
|
||||
&"center_idle": preload("res://assets/ui/rhythm/anchor01.png"),
|
||||
&"center_hit_normal": preload("res://assets/ui/rhythm/anchor02.png"),
|
||||
&"center_hit_special": preload("res://assets/ui/rhythm/anchor02d.png"),
|
||||
&"flash_normal": preload("res://assets/ui/rhythm/c01.png"),
|
||||
&"flash_special": preload("res://assets/ui/rhythm/c02.png"),
|
||||
&"mover_normal": preload("res://assets/ui/rhythm/star.png"),
|
||||
&"mover_special": preload("res://assets/ui/rhythm/sanchor.png"),
|
||||
},
|
||||
&"future": {
|
||||
&"background": preload("res://assets/ui/rhythm/bd.png"),
|
||||
&"center_idle": preload("res://assets/ui/rhythm/anchor01d.png"),
|
||||
&"center_hit_normal": preload("res://assets/ui/rhythm/anchor02d.png"),
|
||||
&"center_hit_special": preload("res://assets/ui/rhythm/anchor02.png"),
|
||||
&"flash_normal": preload("res://assets/ui/rhythm/c01d.png"),
|
||||
&"flash_special": preload("res://assets/ui/rhythm/c02d.png"),
|
||||
&"mover_normal": preload("res://assets/ui/rhythm/stard.png"),
|
||||
&"mover_special": preload("res://assets/ui/rhythm/sanchord.png"),
|
||||
},
|
||||
}
|
||||
|
||||
const DESIGN_SIZE := Vector2(1280.0, 720.0)
|
||||
const HUD_MIN_SCALE := 0.72
|
||||
const HUD_MAX_SCALE := 1.15
|
||||
const CENTER_HIT_HOLD_SECONDS := 0.62
|
||||
const BEAT_TICK_FLASH_PEAK := 0.35
|
||||
const BEAT_FLASH_DECAY_RATE := 8.0
|
||||
const CENTER_HIT_FLASH_DECAY_RATE := 2.8
|
||||
const CENTER_HIT_FLASH_SCALE := 1.55
|
||||
const TIME_ANCHOR_HELD_COLOR := Color(0.45, 1.0, 0.75)
|
||||
const TIME_ANCHOR_BROKEN_COLOR := Color(1.0, 0.2, 0.9)
|
||||
const TIME_ANCHOR_MOVER_SCALE := 1.65
|
||||
const BUFF_SLOT_COUNT := 7
|
||||
const BUFF_ON_TEXTURE := preload("res://assets/ui/buff/buff_on.png")
|
||||
const BUFF_OFF_TEXTURE := preload("res://assets/ui/buff/buff_off.png")
|
||||
const JUDGEMENT_ARTS := {
|
||||
&"perfect": preload("res://assets/ui/judgements/perfect.png"),
|
||||
&"good": preload("res://assets/ui/judgements/good.png"),
|
||||
&"bad": preload("res://assets/ui/judgements/bad.png"),
|
||||
&"miss": preload("res://assets/ui/judgements/miss.png"),
|
||||
}
|
||||
## 2026-07-05 策划:判定字样显示 1 秒后自动消失;新判定顶替时重置计时。
|
||||
const JUDGEMENT_VISIBLE_SECONDS := 1.0
|
||||
|
||||
var chart_markers: Array[Control] = []
|
||||
var _time_anchor_beats: Dictionary = {}
|
||||
var _last_beat_index := -1
|
||||
var _last_upcoming_beat := -1
|
||||
var _time_phase: StringName = &"past"
|
||||
var _streak_count := 0
|
||||
var _attack_buff_stacks := 0
|
||||
var _attack_buff_max := BUFF_SLOT_COUNT
|
||||
var _center_hit_timer := 0.0
|
||||
var _judgement_visible_left := 0.0
|
||||
var _time_anchor_feedback_latched := false
|
||||
var track_center := Vector2.ZERO
|
||||
var left_mover_start := Vector2.ZERO
|
||||
var right_mover_start := Vector2.ZERO
|
||||
var mover_size := Vector2.ZERO
|
||||
var normal_mover_size := Vector2.ZERO
|
||||
var time_anchor_mover_size := Vector2.ZERO
|
||||
var center_flash_size := Vector2.ZERO
|
||||
var normal_center_flash_size := Vector2.ZERO
|
||||
var hit_center_flash_size := Vector2.ZERO
|
||||
var center_flash_decay_rate := BEAT_FLASH_DECAY_RATE
|
||||
var center_flash_color := Color.WHITE
|
||||
var center_flash_peak := 1.0
|
||||
var beat_flash := 0.0
|
||||
var beat_age := 0.0
|
||||
var feedback_flash := 0.0
|
||||
var _last_layout_size := Vector2.ZERO
|
||||
var _responsive_scale := 1.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_apply_responsive_layout()
|
||||
_cache_rhythm_track_layout()
|
||||
center_flash.modulate = Color(1.0, 1.0, 1.0, 0.0)
|
||||
_sync_bpm_with_rhythm_manager()
|
||||
_apply_time_phase_skin(_initial_time_phase())
|
||||
_refresh_combo_count()
|
||||
_refresh_attack_buff_display()
|
||||
var bus := _event_bus()
|
||||
bus.connect("beat_ticked", _on_beat_ticked)
|
||||
bus.connect("judgement_made", _on_judgement_made)
|
||||
bus.connect("chart_event_upcoming", _on_chart_event_upcoming)
|
||||
bus.connect("chart_event_triggered", _on_chart_event_triggered)
|
||||
if bus.has_signal("time_anchor_scheduled"):
|
||||
bus.connect("time_anchor_scheduled", _on_time_anchor_scheduled)
|
||||
if bus.has_signal("time_anchor_resolved"):
|
||||
bus.connect("time_anchor_resolved", _on_time_anchor_resolved)
|
||||
if bus.has_signal("time_phase_changed"):
|
||||
bus.connect("time_phase_changed", _on_time_phase_changed)
|
||||
if bus.has_signal("chart_reset"):
|
||||
bus.connect("chart_reset", _on_chart_reset_clear_time_anchor_markers)
|
||||
if bus.has_signal("streak_changed"):
|
||||
bus.connect("streak_changed", _on_streak_changed)
|
||||
if bus.has_signal("attack_buff_changed"):
|
||||
bus.connect("attack_buff_changed", _on_attack_buff_changed)
|
||||
|
||||
|
||||
func _apply_responsive_layout() -> void:
|
||||
if not responsive_layout:
|
||||
return
|
||||
var viewport_size := size
|
||||
if viewport_size.x <= 0.0 or viewport_size.y <= 0.0:
|
||||
viewport_size = get_viewport_rect().size
|
||||
if viewport_size.x <= 0.0 or viewport_size.y <= 0.0:
|
||||
return
|
||||
_last_layout_size = viewport_size
|
||||
_responsive_scale = _hud_scale(viewport_size)
|
||||
var s := _responsive_scale
|
||||
var center_x := viewport_size.x * 0.5
|
||||
var track_y := viewport_size.y - 94.0 * s
|
||||
var axis_y := track_y + 18.0 * s
|
||||
var track_max_w := maxf(320.0 * s, viewport_size.x - 96.0 * s)
|
||||
var track_w := minf(832.0 * s, track_max_w)
|
||||
|
||||
_set_control_rect(track_background, Vector2(center_x, track_y + 14.0 * s), Vector2(track_w, 116.0 * s))
|
||||
_set_control_rect(track_line, Vector2(center_x, axis_y), Vector2(maxf(120.0 * s, track_w - 100.0 * s), 47.0 * s))
|
||||
_set_control_rect(center_base, Vector2(center_x, axis_y), Vector2(112.0 * s, 131.0 * s))
|
||||
_set_control_rect(center_flash, Vector2(center_x, axis_y), Vector2(160.0 * s, 151.0 * s))
|
||||
normal_center_flash_size = center_flash.size
|
||||
hit_center_flash_size = normal_center_flash_size * CENTER_HIT_FLASH_SCALE
|
||||
_set_center_flash_profile(false)
|
||||
normal_mover_size = Vector2(68.0 * s, 62.0 * s)
|
||||
time_anchor_mover_size = normal_mover_size * TIME_ANCHOR_MOVER_SCALE
|
||||
_set_control_rect(left_mover, Vector2(center_x - track_w * 0.43, axis_y), normal_mover_size)
|
||||
_set_control_rect(right_mover, Vector2(center_x + track_w * 0.43, axis_y), normal_mover_size)
|
||||
|
||||
_set_control_top_left(chart_marker_container, Vector2.ZERO, viewport_size)
|
||||
_set_control_top_left($HudReadoutRow, Vector2(154.0 * s, 135.0 * s), Vector2(260.0 * s, 76.0 * s))
|
||||
combo_count_label.custom_minimum_size = Vector2(240.0 * s, 32.0 * s)
|
||||
attack_buff_value_label.custom_minimum_size = Vector2(240.0 * s, 32.0 * s)
|
||||
combo_count_label.add_theme_font_size_override("font_size", int(round(26.0 * s)))
|
||||
attack_buff_value_label.add_theme_font_size_override("font_size", int(round(26.0 * s)))
|
||||
|
||||
var buff_total_w := (42.0 * 7.0 + 12.0 * 6.0) * s
|
||||
_set_control_top_left(buff_pip_row, Vector2(center_x - buff_total_w * 0.5, 132.0 * s), Vector2(buff_total_w, 58.0 * s))
|
||||
buff_pip_row.add_theme_constant_override("separation", int(round(12.0 * s)))
|
||||
for child: Node in buff_pip_row.get_children():
|
||||
var pip := child as TextureRect
|
||||
if pip != null:
|
||||
pip.custom_minimum_size = Vector2(42.0 * s, 55.0 * s)
|
||||
|
||||
var judgement_center := Vector2(center_x, viewport_size.y * 0.56)
|
||||
_set_control_rect(judgement_art, judgement_center, Vector2(270.0 * s, 72.0 * s))
|
||||
_set_control_rect(judgement_label, judgement_center, Vector2(500.0 * s, 74.0 * s))
|
||||
judgement_label.add_theme_font_size_override("font_size", int(round(32.0 * s)))
|
||||
_cache_rhythm_track_layout()
|
||||
|
||||
|
||||
func _hud_scale(viewport_size: Vector2) -> float:
|
||||
return clampf(minf(viewport_size.x / DESIGN_SIZE.x, viewport_size.y / DESIGN_SIZE.y), HUD_MIN_SCALE, HUD_MAX_SCALE)
|
||||
|
||||
|
||||
func _set_control_rect(control: Control, center: Vector2, target_size: Vector2) -> void:
|
||||
control.offset_left = center.x - target_size.x * 0.5
|
||||
control.offset_top = center.y - target_size.y * 0.5
|
||||
control.offset_right = center.x + target_size.x * 0.5
|
||||
control.offset_bottom = center.y + target_size.y * 0.5
|
||||
|
||||
|
||||
func _set_control_top_left(control: Control, top_left: Vector2, target_size: Vector2) -> void:
|
||||
control.offset_left = top_left.x
|
||||
control.offset_top = top_left.y
|
||||
control.offset_right = top_left.x + target_size.x
|
||||
control.offset_bottom = top_left.y + target_size.y
|
||||
|
||||
func _sync_bpm_with_rhythm_manager() -> void:
|
||||
var rhythm := get_tree().root.get_node_or_null("RhythmManager")
|
||||
if rhythm != null:
|
||||
var manager_bpm := float(rhythm.get("bpm"))
|
||||
if manager_bpm > 0.0:
|
||||
bpm = manager_bpm
|
||||
|
||||
|
||||
func _initial_time_phase() -> StringName:
|
||||
var manager := get_tree().root.get_node_or_null("TimePhaseManager")
|
||||
if manager != null:
|
||||
return StringName(str(manager.get("current_time_phase")))
|
||||
return &"past"
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if responsive_layout and size != _last_layout_size:
|
||||
_apply_responsive_layout()
|
||||
var visual_delta := minf(delta, 1.0 / 30.0)
|
||||
beat_age += delta
|
||||
beat_flash = maxf(0.0, beat_flash - visual_delta * center_flash_decay_rate)
|
||||
if _center_hit_timer > 0.0:
|
||||
_center_hit_timer -= delta
|
||||
if _center_hit_timer <= 0.0:
|
||||
center_base.texture = _skin()[&"center_idle"]
|
||||
var upcoming := _upcoming_beat_index()
|
||||
if upcoming != _last_upcoming_beat:
|
||||
_refresh_mover_textures()
|
||||
_update_movers()
|
||||
if feedback_flash > 0.0:
|
||||
feedback_flash = maxf(0.0, feedback_flash - visual_delta * 4.0)
|
||||
var feedback_scale := Vector2.ONE * (1.0 + feedback_flash * 0.18)
|
||||
judgement_label.scale = feedback_scale
|
||||
judgement_art.scale = feedback_scale
|
||||
# 满 1 秒才隐藏,倒计时期间不动 modulate/text,避免与脉冲/判定色互相干扰。
|
||||
if _judgement_visible_left > 0.0:
|
||||
_judgement_visible_left -= visual_delta
|
||||
if _judgement_visible_left <= 0.0:
|
||||
_judgement_visible_left = 0.0
|
||||
judgement_label.visible = false
|
||||
judgement_art.visible = false
|
||||
|
||||
|
||||
func _on_beat_ticked(beat_index: int) -> void:
|
||||
_last_beat_index = beat_index
|
||||
_set_center_flash_profile(false)
|
||||
center_flash.texture = _skin()[&"flash_normal"]
|
||||
center_flash_color = Color.WHITE
|
||||
center_flash_peak = BEAT_TICK_FLASH_PEAK
|
||||
beat_flash = 1.0
|
||||
beat_age = 0.0
|
||||
_refresh_mover_textures()
|
||||
_update_movers()
|
||||
|
||||
|
||||
func _on_judgement_made(quality: StringName, offset_ms: float, _beat_index: int) -> void:
|
||||
if _time_anchor_feedback_latched:
|
||||
# time_anchor_resolved already drove the center emblem, flash and label
|
||||
# for this very judgement; don't overwrite the anchor feedback with the
|
||||
# plain grade readout.
|
||||
_time_anchor_feedback_latched = false
|
||||
return
|
||||
var color := _judgement_color(quality)
|
||||
judgement_label.text = "%s %s" % [
|
||||
str(quality).to_upper(),
|
||||
_format_signed_ms(offset_ms / 1000.0),
|
||||
]
|
||||
judgement_label.modulate = color
|
||||
_show_judgement_art(quality)
|
||||
feedback_flash = 1.0
|
||||
if quality != &"miss":
|
||||
_show_center_hit(false)
|
||||
|
||||
|
||||
func _on_chart_event_upcoming(_event: Resource, _time_to_event: float) -> void:
|
||||
return
|
||||
|
||||
|
||||
func _on_chart_event_triggered(event: Resource) -> void:
|
||||
_set_center_flash_profile(false)
|
||||
center_flash.texture = _skin()[&"flash_normal"]
|
||||
if StringName(str(event.get("event_type"))) == &"camera_pulse":
|
||||
center_flash_color = Color(1.0, 0.84, 0.26, 1.0)
|
||||
else:
|
||||
center_flash_color = _chart_marker_color(event)
|
||||
center_flash_peak = 1.0
|
||||
beat_flash = 1.0
|
||||
_update_movers()
|
||||
|
||||
|
||||
func _on_time_anchor_scheduled(anchor: Dictionary) -> void:
|
||||
_time_anchor_beats[int(anchor.get("beat", 0))] = true
|
||||
_refresh_mover_textures()
|
||||
|
||||
|
||||
func _on_time_anchor_resolved(anchor: Dictionary, held: bool, judgement: Dictionary) -> void:
|
||||
_time_anchor_beats.erase(int(anchor.get("beat", 0)))
|
||||
_refresh_mover_textures()
|
||||
# Held anchors resolve inside the judgement_made dispatch (judgement dict is
|
||||
# non-empty there); latch so the follow-up grade readout keeps this feedback.
|
||||
_time_anchor_feedback_latched = not judgement.is_empty()
|
||||
if held:
|
||||
_show_center_hit(true)
|
||||
judgement_label.text = "ANCHOR HELD"
|
||||
judgement_label.modulate = TIME_ANCHOR_HELD_COLOR
|
||||
_show_judgement_art(_judgement_from_payload(judgement))
|
||||
else:
|
||||
_set_center_flash_profile(false)
|
||||
center_flash.texture = _skin()[&"flash_special"]
|
||||
center_flash_color = TIME_ANCHOR_BROKEN_COLOR
|
||||
center_flash_peak = 1.0
|
||||
beat_flash = 1.0
|
||||
if judgement.is_empty():
|
||||
judgement_label.text = ""
|
||||
judgement_label.visible = false
|
||||
judgement_art.visible = false
|
||||
feedback_flash = 0.0
|
||||
_judgement_visible_left = 0.0
|
||||
else:
|
||||
judgement_label.text = "TIME SHIFT!"
|
||||
judgement_label.modulate = TIME_ANCHOR_BROKEN_COLOR
|
||||
_show_judgement_art(_judgement_from_payload(judgement, &"miss"))
|
||||
feedback_flash = 1.0
|
||||
if held:
|
||||
feedback_flash = 1.0
|
||||
_update_movers()
|
||||
|
||||
|
||||
func _show_center_hit(special: bool) -> void:
|
||||
# Normal hit: own-phase anchor02 variant + c01 variant flash. Special hit:
|
||||
# the cross-phase anchor02 variant + own-phase c02 variant flash.
|
||||
var skin := _skin()
|
||||
center_base.texture = skin[&"center_hit_special"] if special else skin[&"center_hit_normal"]
|
||||
_set_center_flash_profile(true)
|
||||
center_flash.texture = skin[&"flash_special"] if special else skin[&"flash_normal"]
|
||||
center_flash_color = Color.WHITE
|
||||
center_flash_peak = 1.0
|
||||
beat_flash = 1.0
|
||||
_center_hit_timer = CENTER_HIT_HOLD_SECONDS
|
||||
_update_movers()
|
||||
|
||||
|
||||
func _refresh_mover_textures() -> void:
|
||||
# The beat the movers are converging toward is the next integer beat; if it
|
||||
# is a time anchor the rhythm point itself upgrades to the special texture.
|
||||
var skin := _skin()
|
||||
var upcoming := _upcoming_beat_index()
|
||||
_last_upcoming_beat = upcoming
|
||||
var special := _time_anchor_beats.has(upcoming)
|
||||
var texture: Texture2D = skin[&"mover_special"] if special else skin[&"mover_normal"]
|
||||
mover_size = time_anchor_mover_size if special else normal_mover_size
|
||||
if left_mover != null:
|
||||
left_mover.texture = texture
|
||||
if right_mover != null:
|
||||
right_mover.texture = texture
|
||||
_update_movers()
|
||||
|
||||
|
||||
func _on_time_phase_changed(_previous: StringName, current: StringName, _reason: StringName) -> void:
|
||||
_set_center_flash_profile(false)
|
||||
_apply_time_phase_skin(current)
|
||||
center_flash_color = Color(0.45, 0.85, 1.0) if current == &"future" else Color(1.0, 0.84, 0.4)
|
||||
center_flash_peak = 1.0
|
||||
beat_flash = 1.0
|
||||
_update_movers()
|
||||
|
||||
|
||||
func _apply_time_phase_skin(time_phase: StringName) -> void:
|
||||
_time_phase = time_phase if TRACK_SKINS.has(time_phase) else &"past"
|
||||
var skin := _skin()
|
||||
track_background.texture = skin[&"background"]
|
||||
track_line.visible = false
|
||||
track_line.texture = null
|
||||
center_base.texture = skin[&"center_idle"]
|
||||
center_flash.texture = skin[&"flash_normal"]
|
||||
_set_center_flash_profile(false)
|
||||
_center_hit_timer = 0.0
|
||||
_refresh_mover_textures()
|
||||
|
||||
|
||||
func _skin() -> Dictionary:
|
||||
return TRACK_SKINS[_time_phase]
|
||||
|
||||
|
||||
func _on_chart_reset_clear_time_anchor_markers(_chart_id: StringName) -> void:
|
||||
_time_anchor_beats.clear()
|
||||
_refresh_mover_textures()
|
||||
|
||||
|
||||
func _on_streak_changed(count: int) -> void:
|
||||
_streak_count = maxi(0, count)
|
||||
_refresh_combo_count()
|
||||
|
||||
|
||||
func _on_attack_buff_changed(stacks: int, max_stacks: int) -> void:
|
||||
_attack_buff_max = maxi(1, max_stacks)
|
||||
_attack_buff_stacks = clampi(stacks, 0, BUFF_SLOT_COUNT)
|
||||
_refresh_attack_buff_display()
|
||||
|
||||
|
||||
func _refresh_combo_count() -> void:
|
||||
combo_count_label.text = "combo %d" % _streak_count
|
||||
|
||||
|
||||
func _refresh_attack_buff_display() -> void:
|
||||
attack_buff_value_label.text = "ATK x %d%%" % (_attack_buff_stacks * 100)
|
||||
for index: int in range(buff_pip_row.get_child_count()):
|
||||
var pip := buff_pip_row.get_child(index) as TextureRect
|
||||
if pip != null:
|
||||
pip.texture = BUFF_ON_TEXTURE if index < _attack_buff_stacks else BUFF_OFF_TEXTURE
|
||||
|
||||
|
||||
func _show_judgement_art(quality: StringName) -> void:
|
||||
var normalized := StringName(str(quality).to_lower())
|
||||
judgement_art.texture = JUDGEMENT_ARTS.get(normalized, JUDGEMENT_ARTS[&"miss"])
|
||||
judgement_art.visible = true
|
||||
_judgement_visible_left = JUDGEMENT_VISIBLE_SECONDS
|
||||
var feedback_scale := Vector2(1.18, 1.18)
|
||||
judgement_label.scale = feedback_scale
|
||||
judgement_art.scale = feedback_scale
|
||||
|
||||
|
||||
func _judgement_from_payload(judgement: Dictionary, fallback := &"miss") -> StringName:
|
||||
if judgement.has("label"):
|
||||
return StringName(str(judgement["label"]).to_lower())
|
||||
if judgement.has(&"label"):
|
||||
return StringName(str(judgement[&"label"]).to_lower())
|
||||
if judgement.has("quality"):
|
||||
return StringName(str(judgement["quality"]).to_lower())
|
||||
if judgement.has(&"quality"):
|
||||
return StringName(str(judgement[&"quality"]).to_lower())
|
||||
return fallback
|
||||
|
||||
|
||||
func _update_movers() -> void:
|
||||
var progress := _current_beat_progress()
|
||||
_set_control_center(left_mover, left_mover_start.lerp(track_center, progress), mover_size)
|
||||
_set_control_center(right_mover, right_mover_start.lerp(track_center, progress), mover_size)
|
||||
_set_control_center(center_flash, track_center, center_flash_size)
|
||||
center_flash.modulate = Color(center_flash_color.r, center_flash_color.g, center_flash_color.b, beat_flash * center_flash_peak)
|
||||
|
||||
|
||||
## Movers follow the musical clock itself so their glide is continuous — the
|
||||
## old frame-accumulated beat_age plus the beat_flash snap made every beat
|
||||
## teleport the points to the center and back out. The arrival at the center
|
||||
## coincides exactly with the beat (progress wraps 1 → 0 on the tick).
|
||||
func _current_beat_progress() -> float:
|
||||
var rhythm := _rhythm_manager_or_null()
|
||||
if rhythm != null and rhythm.has_method("get_current_beat_progress") and bool(rhythm.get("running")):
|
||||
return clampf(float(rhythm.call("get_current_beat_progress")), 0.0, 1.0)
|
||||
var seconds_per_beat := 60.0 / maxf(1.0, bpm)
|
||||
return clampf(beat_age / seconds_per_beat, 0.0, 1.0)
|
||||
|
||||
|
||||
## 节奏点正收束的目标拍:直接由音乐时钟推导,而不是等物理帧上报的
|
||||
## beat_ticked(会晚约一个物理帧),否则拍界处锚点纹理会闪一下普通纹理。
|
||||
func _upcoming_beat_index() -> int:
|
||||
var rhythm := _rhythm_manager_or_null()
|
||||
if rhythm != null and bool(rhythm.get("running")) and rhythm.has_method("song_position"):
|
||||
var beat_seconds := float(rhythm.get("beat_time"))
|
||||
var adjusted := float(rhythm.call("song_position")) + float(rhythm.get("beat_offset"))
|
||||
if beat_seconds > 0.0 and adjusted >= 0.0:
|
||||
return int(floor(adjusted / beat_seconds)) + 1
|
||||
return _last_beat_index + 1
|
||||
|
||||
|
||||
func _rhythm_manager_or_null() -> Node:
|
||||
if not is_inside_tree():
|
||||
return null
|
||||
return get_tree().root.get_node_or_null("RhythmManager")
|
||||
|
||||
|
||||
func _set_center_flash_profile(hit: bool) -> void:
|
||||
center_flash_size = hit_center_flash_size if hit else normal_center_flash_size
|
||||
center_flash_decay_rate = CENTER_HIT_FLASH_DECAY_RATE if hit else BEAT_FLASH_DECAY_RATE
|
||||
|
||||
|
||||
func _cache_rhythm_track_layout() -> void:
|
||||
track_center = _control_center(center_base)
|
||||
left_mover_start = _control_center(left_mover)
|
||||
right_mover_start = _control_center(right_mover)
|
||||
normal_mover_size = left_mover.size if normal_mover_size == Vector2.ZERO else normal_mover_size
|
||||
time_anchor_mover_size = normal_mover_size * TIME_ANCHOR_MOVER_SCALE
|
||||
mover_size = normal_mover_size
|
||||
normal_center_flash_size = center_flash.size if normal_center_flash_size == Vector2.ZERO else normal_center_flash_size
|
||||
hit_center_flash_size = normal_center_flash_size * CENTER_HIT_FLASH_SCALE
|
||||
center_flash_size = normal_center_flash_size
|
||||
|
||||
|
||||
func _control_center(control: Control) -> Vector2:
|
||||
return Vector2(
|
||||
(control.offset_left + control.offset_right) * 0.5,
|
||||
(control.offset_top + control.offset_bottom) * 0.5
|
||||
)
|
||||
|
||||
|
||||
func _set_control_center(control: Control, center: Vector2, size: Vector2) -> void:
|
||||
control.offset_left = center.x - size.x * 0.5
|
||||
control.offset_top = center.y - size.y * 0.5
|
||||
control.offset_right = center.x + size.x * 0.5
|
||||
control.offset_bottom = center.y + size.y * 0.5
|
||||
|
||||
|
||||
func _chart_marker_text(event: Resource) -> String:
|
||||
match StringName(str(event.get("event_type"))):
|
||||
&"show_accent_marker":
|
||||
return "ACC"
|
||||
&"enemy_action":
|
||||
return _enemy_action_marker_text(event)
|
||||
&"camera_pulse":
|
||||
return "CAM"
|
||||
return str(event.get("event_type")).to_upper()
|
||||
|
||||
|
||||
func _chart_marker_color(event: Resource) -> Color:
|
||||
match StringName(str(event.get("event_type"))):
|
||||
&"show_accent_marker":
|
||||
return Color("ffd84a")
|
||||
&"enemy_action":
|
||||
return Color("ff3355")
|
||||
&"camera_pulse":
|
||||
return Color("ffffff")
|
||||
return Color("00f2ff")
|
||||
|
||||
|
||||
func _enemy_action_marker_text(event: Resource) -> String:
|
||||
var action_id := str(event.get("action_id"))
|
||||
if action_id.is_empty():
|
||||
return "ACT"
|
||||
if action_id.begins_with("enemy_"):
|
||||
action_id = action_id.substr(6)
|
||||
return action_id.replace("_", " ").to_upper()
|
||||
|
||||
|
||||
func _judgement_color(quality: StringName) -> Color:
|
||||
match quality:
|
||||
&"perfect":
|
||||
return Color("00f2ff")
|
||||
&"good":
|
||||
return Color("ffffff")
|
||||
&"bad":
|
||||
return Color("ffaa00")
|
||||
_:
|
||||
return Color("ff0055")
|
||||
|
||||
|
||||
func _chart_marker_position(time_to_event: float) -> Vector2:
|
||||
var seconds_per_beat := 60.0 / maxf(1.0, bpm)
|
||||
var beat_distance := clampf(time_to_event / seconds_per_beat, 0.0, 4.0)
|
||||
var x := track_center.x + beat_distance * 92.0 * _responsive_scale
|
||||
return Vector2(x - 33.0 * _responsive_scale, track_center.y + 44.0 * _responsive_scale)
|
||||
|
||||
|
||||
func _format_signed_ms(seconds: float) -> String:
|
||||
if is_inf(seconds):
|
||||
return "-- ms"
|
||||
return "%+.0f ms" % (seconds * 1000.0)
|
||||
|
||||
|
||||
func _event_bus() -> Node:
|
||||
var root := get_tree().root
|
||||
var bus := root.get_node_or_null("EventBus")
|
||||
if bus == null:
|
||||
bus = load("res://autoload/event_bus.gd").new()
|
||||
bus.name = "EventBus"
|
||||
root.add_child(bus)
|
||||
return bus
|
||||
Reference in New Issue
Block a user