Initial project sync
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
class_name ComboWindowHud
|
||||
extends Control
|
||||
|
||||
@export var slot_count := 4
|
||||
|
||||
const FRAME_TEXTURE := preload("res://assets/ui/combo/four_slot_judgement_frame.png")
|
||||
const FRAME_SIZE := Vector2(255.0, 51.0)
|
||||
const SLOT_SIZE := Vector2(50.0, 51.0)
|
||||
const SLOT_X := [0.0, 68.0, 137.0, 205.0]
|
||||
|
||||
var frame: TextureRect
|
||||
var panels: Array[PanelContainer] = []
|
||||
var labels: Array[Label] = []
|
||||
var clear_tween: Tween
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
custom_minimum_size = FRAME_SIZE
|
||||
size = FRAME_SIZE
|
||||
_build_slots()
|
||||
var bus := _event_bus()
|
||||
bus.connect("combo_updated", refresh)
|
||||
bus.connect("combo_cleared", _on_combo_cleared)
|
||||
|
||||
|
||||
func refresh(inputs: Array) -> void:
|
||||
if labels.is_empty():
|
||||
_build_slots()
|
||||
for index: int in range(labels.size()):
|
||||
var filled := index < inputs.size()
|
||||
labels[index].text = _slot_text(inputs[index]) if filled else "∅"
|
||||
labels[index].modulate = Color(1.0, 1.0, 1.0, 1.0 if filled else 0.32)
|
||||
panels[index].modulate = Color(1.0, 1.0, 1.0, 1.0 if filled else 0.72)
|
||||
if filled:
|
||||
_pulse_slot(panels[index])
|
||||
|
||||
|
||||
func _on_combo_cleared(_reason: StringName) -> void:
|
||||
refresh([])
|
||||
_flash_clear()
|
||||
|
||||
|
||||
func _build_slots() -> void:
|
||||
if not labels.is_empty():
|
||||
return
|
||||
frame = TextureRect.new()
|
||||
frame.name = "SlotFrame"
|
||||
frame.texture = FRAME_TEXTURE
|
||||
frame.modulate = Color(1.0, 1.0, 1.0, 0.7)
|
||||
frame.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
frame.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
frame.stretch_mode = TextureRect.STRETCH_SCALE
|
||||
frame.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||
frame.position = Vector2.ZERO
|
||||
frame.size = FRAME_SIZE
|
||||
add_child(frame)
|
||||
for index: int in range(slot_count):
|
||||
var panel := PanelContainer.new()
|
||||
panel.name = "Slot%d" % (index + 1)
|
||||
panel.position = Vector2(float(SLOT_X[index]), 0.0)
|
||||
panel.size = SLOT_SIZE
|
||||
panel.custom_minimum_size = SLOT_SIZE
|
||||
panel.pivot_offset = SLOT_SIZE * 0.5
|
||||
panel.modulate = Color(1.0, 1.0, 1.0, 0.72)
|
||||
panel.add_theme_stylebox_override("panel", _make_slot_style())
|
||||
var label := Label.new()
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.text = "∅"
|
||||
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
label.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
label.add_theme_color_override("font_color", Color(0.94, 0.98, 1.0, 1.0))
|
||||
label.add_theme_color_override("font_shadow_color", Color(0.0, 0.0, 0.0, 0.9))
|
||||
label.add_theme_constant_override("shadow_offset_x", 2)
|
||||
label.add_theme_constant_override("shadow_offset_y", 2)
|
||||
label.add_theme_font_size_override("font_size", 25)
|
||||
panel.add_child(label)
|
||||
add_child(panel)
|
||||
panels.append(panel)
|
||||
labels.append(label)
|
||||
|
||||
|
||||
func _slot_text(value: Variant) -> String:
|
||||
var symbol := StringName(str(value))
|
||||
match symbol:
|
||||
&"SP":
|
||||
return "sp"
|
||||
&"Ø":
|
||||
return "∅"
|
||||
return str(symbol)
|
||||
|
||||
|
||||
func _pulse_slot(panel: PanelContainer) -> void:
|
||||
var tween := create_tween()
|
||||
panel.scale = Vector2(1.08, 1.08)
|
||||
tween.tween_property(panel, "scale", Vector2.ONE, 0.09)
|
||||
|
||||
|
||||
func _flash_clear() -> void:
|
||||
if clear_tween != null and clear_tween.is_valid():
|
||||
clear_tween.kill()
|
||||
clear_tween = create_tween()
|
||||
clear_tween.set_parallel(true)
|
||||
for panel: PanelContainer in panels:
|
||||
panel.scale = Vector2(1.16, 1.16)
|
||||
panel.modulate = Color(1.0, 1.0, 1.0, 1.0)
|
||||
clear_tween.tween_property(panel, "scale", Vector2.ONE, 0.20)
|
||||
clear_tween.tween_property(panel, "modulate", Color(1.0, 1.0, 1.0, 0.72), 0.20)
|
||||
|
||||
|
||||
func _make_slot_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.content_margin_left = 4.0
|
||||
style.content_margin_top = 2.0
|
||||
style.content_margin_right = 4.0
|
||||
style.content_margin_bottom = 2.0
|
||||
style.bg_color = Color.TRANSPARENT
|
||||
style.border_width_left = 0
|
||||
style.border_width_top = 0
|
||||
style.border_width_right = 0
|
||||
style.border_width_bottom = 0
|
||||
return style
|
||||
|
||||
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
uid://cm81sgp06lw8i
|
||||
@@ -0,0 +1,8 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/ui/combo_window_hud.gd" id="1"]
|
||||
|
||||
[node name="ComboWindowHud" type="Control"]
|
||||
custom_minimum_size = Vector2(255, 51)
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1")
|
||||
@@ -0,0 +1,22 @@
|
||||
class_name EnergyBar
|
||||
extends ProgressBar
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
show_percentage = false
|
||||
_event_bus().connect("player_energy_changed", refresh)
|
||||
|
||||
|
||||
func refresh(current: float, maximum: float) -> void:
|
||||
max_value = maxf(0.01, maximum)
|
||||
value = clampf(current, 0.0, maximum)
|
||||
|
||||
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
uid://ckjx6raiygarl
|
||||
@@ -0,0 +1,19 @@
|
||||
[gd_scene load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/ui/energy_bar.gd" id="1"]
|
||||
[ext_resource type="Texture2D" path="res://assets/ui/panels/player_blue_ui.png" id="2_blue_fill"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_energy_bg"]
|
||||
bg_color = Color(0.02, 0.04, 0.07, 0.55)
|
||||
|
||||
[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_energy_fill"]
|
||||
texture = ExtResource("2_blue_fill")
|
||||
|
||||
[node name="EnergyBar" type="ProgressBar"]
|
||||
custom_minimum_size = Vector2(128, 15)
|
||||
theme_override_styles/background = SubResource("StyleBoxFlat_energy_bg")
|
||||
theme_override_styles/fill = SubResource("StyleBoxTexture_energy_fill")
|
||||
max_value = 100.0
|
||||
value = 100.0
|
||||
show_percentage = false
|
||||
script = ExtResource("1")
|
||||
@@ -0,0 +1,771 @@
|
||||
class_name MainUI
|
||||
extends Control
|
||||
|
||||
const DefenseResolverScript := preload("res://scripts/resolvers/defense_resolver.gd")
|
||||
const ActionPatternExporterScript := preload("res://tools/export_action_patterns.gd")
|
||||
|
||||
@onready var rhythm_track: Control = $RhythmTrack
|
||||
@onready var status_bars: Control = $StatusBars
|
||||
@onready var boss_status: Control = $BossStatus
|
||||
@onready var combo_window: Control = $ComboWindow
|
||||
@onready var time_phase_hud: Control = $TimePhaseHud
|
||||
@onready var debug_panel: Control = $DebugPanel
|
||||
@onready var health_bar: ProgressBar = $StatusBars/HealthBar
|
||||
@onready var charge_bar: ProgressBar = $StatusBars/ChargeBar
|
||||
@onready var charge_level_label: Label = $StatusBars/ChargeLevelLabel
|
||||
@onready var boss_health_bar: ProgressBar = $BossStatus/BossHealthBar
|
||||
@onready var boss_name_label: Label = $BossStatus/BossNameLabel
|
||||
@onready var combo_skill_label: Label = $ComboSkillLabel
|
||||
@onready var state_axes_label: Label = $DebugPanel/StateAxesLabel
|
||||
@onready var action_label: Label = $DebugPanel/ActionLabel
|
||||
@onready var anchor_label: Label = $DebugPanel/AnchorLabel
|
||||
@onready var effects_label: Label = $DebugPanel/EffectsLabel
|
||||
@onready var defense_label: Label = $DebugPanel/DefenseLabel
|
||||
@onready var patterns_label: Label = $DebugPanel/PatternsLabel
|
||||
@onready var hit_log_label: Label = $DebugPanel/HitLogLabel
|
||||
@onready var calibration_label: Label = $DebugPanel/CalibrationLabel
|
||||
@onready var time_phase_label: Label = $TimePhaseHud/TimePhaseLabel
|
||||
@onready var attack_buff_label: Label = $TimePhaseHud/AttackBuffLabel
|
||||
@onready var streak_label: Label = $TimePhaseHud/StreakLabel
|
||||
@onready var time_phase_debug_label: Label = $DebugPanel/TimePhaseDebugLabel
|
||||
@onready var time_anchor_debug_label: Label = $DebugPanel/TimeAnchorDebugLabel
|
||||
|
||||
const DESIGN_SIZE := Vector2(1280.0, 720.0)
|
||||
const HUD_MIN_SCALE := 0.72
|
||||
const HUD_MAX_SCALE := 1.15
|
||||
const TIME_PHASE_PAST_COLOR := Color(1.0, 0.84, 0.4)
|
||||
const TIME_PHASE_FUTURE_COLOR := Color(0.45, 0.85, 1.0)
|
||||
const COMBO_SKILL_IDLE_SCALE := Vector2.ONE
|
||||
const COMBO_SKILL_POP_SCALE := Vector2(1.12, 1.12)
|
||||
const MOVE_LIST_ACTION := &"toggle_move_list"
|
||||
const MOVE_LIST_PANEL_SIZE := Vector2(430.0, 430.0)
|
||||
const MOVE_LIST_SECTIONS := [
|
||||
{
|
||||
"title": "基础",
|
||||
"rows": [
|
||||
["A / D", "左右地面三段斩"],
|
||||
["W", "上挑"],
|
||||
["S", "格挡 / 空中下劈"],
|
||||
["空中 A / D", "空中斩"],
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "SP 派生",
|
||||
"rows": [
|
||||
["A/D + SP", "突进斩"],
|
||||
["A/A 或 D/D + SP", "连段终结"],
|
||||
["A/A/A 或 D/D/D + SP", "震地击"],
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "蓄力",
|
||||
"rows": [
|
||||
["A/D 按住后松开", "剑雨 Lv1-3"],
|
||||
["S 按住 + SP", "斩波 Lv1-3"],
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
var charge_bar_ready := false
|
||||
var charge_flash := 0.0
|
||||
var debug_actor: Node
|
||||
var boss_actor: Node
|
||||
var _boss_health_component: Node
|
||||
var _patterns_debug_text := "Patterns -"
|
||||
var _hit_log_entries: Array[String] = []
|
||||
var _last_diff_ms := INF
|
||||
var _current_time_phase: StringName = &"past"
|
||||
var _streak_count := 0
|
||||
var _attack_buff_stacks := 0
|
||||
var _attack_buff_max := 20
|
||||
var _last_viewport_size := Vector2.ZERO
|
||||
var _boss_room_active := false
|
||||
var move_list_panel: PanelContainer
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_ensure_move_list_input_action()
|
||||
_build_move_list_panel()
|
||||
_apply_responsive_layout()
|
||||
var viewport := get_viewport()
|
||||
var resize_callback := Callable(self, "_apply_responsive_layout")
|
||||
if viewport != null and not viewport.size_changed.is_connected(resize_callback):
|
||||
viewport.size_changed.connect(resize_callback)
|
||||
_apply_bar_styles()
|
||||
_patterns_debug_text = _debug_patterns()
|
||||
var bus := _event_bus()
|
||||
bus.connect("player_health_changed", _on_health_changed)
|
||||
bus.connect("player_charge_changed", _on_charge_changed)
|
||||
bus.connect("skill_executed", _on_skill_executed)
|
||||
if bus.has_signal("judgement_made") and not bus.is_connected("judgement_made", _on_judgement_made):
|
||||
bus.connect("judgement_made", _on_judgement_made)
|
||||
if bus.has_signal("hit_confirmed") and not bus.is_connected("hit_confirmed", _on_hit_confirmed):
|
||||
bus.connect("hit_confirmed", _on_hit_confirmed)
|
||||
if bus.has_signal("time_phase_changed") and not bus.is_connected("time_phase_changed", _on_time_phase_changed):
|
||||
bus.connect("time_phase_changed", _on_time_phase_changed)
|
||||
if bus.has_signal("attack_buff_changed") and not bus.is_connected("attack_buff_changed", _on_attack_buff_changed):
|
||||
bus.connect("attack_buff_changed", _on_attack_buff_changed)
|
||||
if bus.has_signal("streak_changed") and not bus.is_connected("streak_changed", _on_streak_changed):
|
||||
bus.connect("streak_changed", _on_streak_changed)
|
||||
if bus.has_signal("flow_state_changed") and not bus.is_connected("flow_state_changed", _on_flow_state_changed):
|
||||
bus.connect("flow_state_changed", _on_flow_state_changed)
|
||||
var time_phase_manager := get_tree().root.get_node_or_null("TimePhaseManager")
|
||||
if time_phase_manager != null:
|
||||
_current_time_phase = StringName(str(time_phase_manager.get("current_time_phase")))
|
||||
_boss_room_active = _current_flow_state() == &"Gameplay_BossRoom"
|
||||
_update_boss_status_visibility()
|
||||
_refresh_time_phase_hud()
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
var key_event := event as InputEventKey
|
||||
if key_event != null and key_event.echo:
|
||||
return
|
||||
var pressed := event.is_action_pressed(MOVE_LIST_ACTION)
|
||||
if not pressed and key_event != null and key_event.pressed:
|
||||
pressed = key_event.keycode == KEY_E or key_event.physical_keycode == KEY_E
|
||||
if not pressed:
|
||||
return
|
||||
toggle_move_list()
|
||||
var viewport := get_viewport()
|
||||
if viewport != null:
|
||||
viewport.set_input_as_handled()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var viewport_size := get_viewport_rect().size
|
||||
if viewport_size != _last_viewport_size:
|
||||
_apply_responsive_layout()
|
||||
_update_charge_bar_flash(delta)
|
||||
refresh_debug_panel()
|
||||
|
||||
|
||||
func _apply_responsive_layout() -> void:
|
||||
var viewport_size := get_viewport_rect().size
|
||||
if viewport_size.x <= 0.0 or viewport_size.y <= 0.0:
|
||||
return
|
||||
_last_viewport_size = viewport_size
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
offset_left = 0.0
|
||||
offset_top = 0.0
|
||||
offset_right = 0.0
|
||||
offset_bottom = 0.0
|
||||
var s := _hud_scale(viewport_size)
|
||||
|
||||
if status_bars != null:
|
||||
status_bars.set_anchors_preset(Control.PRESET_TOP_LEFT, true)
|
||||
status_bars.pivot_offset = Vector2.ZERO
|
||||
status_bars.size = Vector2(261.0, 111.0)
|
||||
status_bars.position = Vector2(34.0, 28.0) * s
|
||||
status_bars.scale = Vector2.ONE * (1.25 * s)
|
||||
|
||||
if boss_status != null:
|
||||
var boss_scale := 1.18 * s
|
||||
var boss_x := maxf(16.0 * s, viewport_size.x - 298.0 * boss_scale - 28.0 * s)
|
||||
boss_status.set_anchors_preset(Control.PRESET_TOP_LEFT, true)
|
||||
boss_status.pivot_offset = Vector2.ZERO
|
||||
boss_status.size = Vector2(298.0, 79.0)
|
||||
boss_status.position = Vector2(boss_x, 34.0 * s)
|
||||
boss_status.scale = Vector2.ONE * boss_scale
|
||||
|
||||
if rhythm_track != null:
|
||||
rhythm_track.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
rhythm_track.offset_left = 0.0
|
||||
rhythm_track.offset_top = 0.0
|
||||
rhythm_track.offset_right = 0.0
|
||||
rhythm_track.offset_bottom = 0.0
|
||||
rhythm_track.scale = Vector2.ONE
|
||||
|
||||
_layout_combo_window(viewport_size, s)
|
||||
|
||||
if combo_skill_label != null:
|
||||
combo_skill_label.visible = false
|
||||
combo_skill_label.text = ""
|
||||
|
||||
if time_phase_hud != null:
|
||||
time_phase_hud.visible = false
|
||||
|
||||
if debug_panel != null:
|
||||
debug_panel.visible = false
|
||||
debug_panel.set_anchors_preset(Control.PRESET_TOP_RIGHT, true)
|
||||
debug_panel.offset_left = -368.0 * s
|
||||
debug_panel.offset_top = 172.0 * s
|
||||
debug_panel.offset_right = -24.0 * s
|
||||
debug_panel.offset_bottom = 348.0 * s
|
||||
|
||||
_layout_move_list_panel(viewport_size, s)
|
||||
|
||||
|
||||
func _layout_combo_window(viewport_size: Vector2, s: float) -> void:
|
||||
if combo_window == null:
|
||||
return
|
||||
var combo_scale := 1.28 * s
|
||||
var combo_size := Vector2(255.0, 51.0)
|
||||
var x := 42.0 * s
|
||||
var y := viewport_size.y - 310.0 * s
|
||||
x = clampf(x, 24.0 * s, maxf(24.0 * s, viewport_size.x - combo_size.x * combo_scale - 24.0 * s))
|
||||
y = clampf(y, 170.0 * s, maxf(170.0 * s, viewport_size.y - combo_size.y * combo_scale - 150.0 * s))
|
||||
combo_window.set_anchors_preset(Control.PRESET_TOP_LEFT, true)
|
||||
combo_window.pivot_offset = Vector2.ZERO
|
||||
combo_window.size = combo_size
|
||||
combo_window.scale = Vector2.ONE * combo_scale
|
||||
combo_window.position = Vector2(x, y)
|
||||
|
||||
|
||||
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 toggle_move_list() -> void:
|
||||
set_move_list_visible(move_list_panel == null or not move_list_panel.visible)
|
||||
|
||||
|
||||
func set_move_list_visible(visible: bool) -> void:
|
||||
if move_list_panel == null:
|
||||
_build_move_list_panel()
|
||||
move_list_panel.visible = visible
|
||||
|
||||
|
||||
func _ensure_move_list_input_action() -> void:
|
||||
if not InputMap.has_action(MOVE_LIST_ACTION):
|
||||
InputMap.add_action(MOVE_LIST_ACTION)
|
||||
if _input_action_has_key(MOVE_LIST_ACTION, KEY_E):
|
||||
return
|
||||
var event := InputEventKey.new()
|
||||
event.keycode = KEY_E
|
||||
event.physical_keycode = KEY_E
|
||||
InputMap.action_add_event(MOVE_LIST_ACTION, event)
|
||||
|
||||
|
||||
func _input_action_has_key(action_name: StringName, keycode: Key) -> bool:
|
||||
if not InputMap.has_action(action_name):
|
||||
return false
|
||||
for event: InputEvent in InputMap.action_get_events(action_name):
|
||||
var key_event := event as InputEventKey
|
||||
if key_event != null and (key_event.keycode == keycode or key_event.physical_keycode == keycode):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _build_move_list_panel() -> void:
|
||||
if move_list_panel != null:
|
||||
return
|
||||
move_list_panel = PanelContainer.new()
|
||||
move_list_panel.name = "MoveListPanel"
|
||||
move_list_panel.visible = false
|
||||
move_list_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
move_list_panel.z_index = 40
|
||||
move_list_panel.custom_minimum_size = MOVE_LIST_PANEL_SIZE
|
||||
move_list_panel.add_theme_stylebox_override(
|
||||
"panel",
|
||||
_make_style(Color(0.045, 0.055, 0.07, 0.88), Color(0.7, 0.95, 1.0, 0.55))
|
||||
)
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 18)
|
||||
margin.add_theme_constant_override("margin_top", 16)
|
||||
margin.add_theme_constant_override("margin_right", 18)
|
||||
margin.add_theme_constant_override("margin_bottom", 16)
|
||||
move_list_panel.add_child(margin)
|
||||
|
||||
var content := VBoxContainer.new()
|
||||
content.add_theme_constant_override("separation", 9)
|
||||
margin.add_child(content)
|
||||
|
||||
var title := _make_move_list_label("招式表", 27, Color(0.92, 0.98, 1.0), HORIZONTAL_ALIGNMENT_CENTER)
|
||||
title.custom_minimum_size = Vector2(0.0, 34.0)
|
||||
content.add_child(title)
|
||||
|
||||
for section: Dictionary in MOVE_LIST_SECTIONS:
|
||||
_add_move_list_section(content, section)
|
||||
|
||||
add_child(move_list_panel)
|
||||
|
||||
|
||||
func _add_move_list_section(content: VBoxContainer, section: Dictionary) -> void:
|
||||
var section_title := _make_move_list_label(str(section.get("title", "")), 16, Color(0.98, 0.78, 0.36), HORIZONTAL_ALIGNMENT_LEFT)
|
||||
section_title.custom_minimum_size = Vector2(0.0, 22.0)
|
||||
content.add_child(section_title)
|
||||
|
||||
var grid := GridContainer.new()
|
||||
grid.columns = 2
|
||||
grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
grid.add_theme_constant_override("h_separation", 12)
|
||||
grid.add_theme_constant_override("v_separation", 5)
|
||||
content.add_child(grid)
|
||||
|
||||
for row: Array in section.get("rows", []):
|
||||
_add_move_list_row(grid, str(row[0]), str(row[1]))
|
||||
|
||||
|
||||
func _add_move_list_row(grid: GridContainer, key_text: String, move_text: String) -> void:
|
||||
var key_cell := PanelContainer.new()
|
||||
key_cell.custom_minimum_size = Vector2(145.0, 27.0)
|
||||
key_cell.add_theme_stylebox_override(
|
||||
"panel",
|
||||
_make_style(Color(0.1, 0.13, 0.16, 0.95), Color(0.35, 0.72, 0.92, 0.65))
|
||||
)
|
||||
var key_label := _make_move_list_label(key_text, 14, Color(0.88, 0.96, 1.0), HORIZONTAL_ALIGNMENT_CENTER)
|
||||
key_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
key_cell.add_child(key_label)
|
||||
grid.add_child(key_cell)
|
||||
|
||||
var move_label := _make_move_list_label(move_text, 15, Color(0.86, 0.9, 0.92), HORIZONTAL_ALIGNMENT_LEFT)
|
||||
move_label.custom_minimum_size = Vector2(220.0, 27.0)
|
||||
move_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
move_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
move_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
grid.add_child(move_label)
|
||||
|
||||
|
||||
func _make_move_list_label(text: String, font_size: int, color: Color, alignment: HorizontalAlignment) -> Label:
|
||||
var label := Label.new()
|
||||
label.text = text
|
||||
label.horizontal_alignment = alignment
|
||||
label.add_theme_font_size_override("font_size", font_size)
|
||||
label.add_theme_color_override("font_color", color)
|
||||
return label
|
||||
|
||||
|
||||
func _layout_move_list_panel(viewport_size: Vector2, s: float) -> void:
|
||||
if move_list_panel == null:
|
||||
return
|
||||
var panel_scale := 0.96 * s
|
||||
var margin := 30.0 * s
|
||||
var min_y := 126.0 * s
|
||||
var desired_y := maxf(min_y, viewport_size.y * 0.2)
|
||||
var max_y := maxf(min_y, viewport_size.y - MOVE_LIST_PANEL_SIZE.y * panel_scale - 52.0 * s)
|
||||
move_list_panel.set_anchors_preset(Control.PRESET_TOP_LEFT, true)
|
||||
move_list_panel.pivot_offset = Vector2.ZERO
|
||||
move_list_panel.size = MOVE_LIST_PANEL_SIZE
|
||||
move_list_panel.scale = Vector2.ONE * panel_scale
|
||||
move_list_panel.position = Vector2(
|
||||
maxf(18.0 * s, viewport_size.x - MOVE_LIST_PANEL_SIZE.x * panel_scale - margin),
|
||||
clampf(desired_y, min_y, max_y)
|
||||
)
|
||||
|
||||
func _on_health_changed(current: int, maximum: int) -> void:
|
||||
health_bar.max_value = max(1, maximum)
|
||||
health_bar.value = clampi(current, 0, maximum)
|
||||
|
||||
|
||||
func _on_charge_changed(current: float, maximum: float, ready: bool, active: bool) -> void:
|
||||
charge_bar.max_value = maxf(0.01, maximum)
|
||||
charge_bar.value = clampf(current, 0.0, maximum)
|
||||
charge_bar_ready = ready and active
|
||||
_update_charge_level_display(current, maximum, ready, active)
|
||||
if charge_bar_ready:
|
||||
return
|
||||
charge_bar.modulate = Color(1.0, 1.0, 1.0, 1.0 if active or ready else 0.45)
|
||||
|
||||
|
||||
func _update_charge_level_display(current: float, maximum: float, ready: bool, active: bool) -> void:
|
||||
if charge_level_label == null:
|
||||
return
|
||||
if not active:
|
||||
charge_level_label.text = ""
|
||||
return
|
||||
var max_level := maxi(2, int(round(maximum)) + 1)
|
||||
var level := clampi(1 + int(floor(current + 0.0001)), 1, max_level)
|
||||
charge_level_label.text = "CHARGE LV%d / %d%s" % [level, max_level, " MAX" if ready else ""]
|
||||
var level_color := _charge_level_color(level)
|
||||
charge_level_label.add_theme_color_override("font_color", level_color)
|
||||
charge_bar.add_theme_stylebox_override(
|
||||
"fill",
|
||||
_make_style(level_color, Color.TRANSPARENT, false)
|
||||
)
|
||||
|
||||
|
||||
func _charge_level_color(level: int) -> Color:
|
||||
match level:
|
||||
1:
|
||||
return Color(0.92, 0.72, 0.25)
|
||||
2:
|
||||
return Color(1.0, 0.55, 0.2)
|
||||
return Color(1.0, 0.32, 0.28)
|
||||
|
||||
|
||||
func _on_skill_executed(skill: Resource, _judgement: StringName) -> void:
|
||||
if combo_skill_label == null or not combo_skill_label.visible:
|
||||
return
|
||||
var display_name := str(skill.get("display_name"))
|
||||
if display_name.is_empty():
|
||||
display_name = str(skill.get("id")).to_upper()
|
||||
combo_skill_label.text = display_name
|
||||
var tween := create_tween()
|
||||
combo_skill_label.scale = COMBO_SKILL_POP_SCALE
|
||||
tween.tween_property(combo_skill_label, "scale", COMBO_SKILL_IDLE_SCALE, 0.12)
|
||||
|
||||
|
||||
func bind_debug_actor(actor: Node) -> void:
|
||||
debug_actor = actor
|
||||
_apply_responsive_layout()
|
||||
refresh_debug_panel()
|
||||
|
||||
|
||||
func bind_boss_actor(actor: Node) -> void:
|
||||
boss_actor = actor
|
||||
_refresh_boss_name_label()
|
||||
_bind_boss_health_component()
|
||||
_update_boss_status_visibility()
|
||||
|
||||
|
||||
func _on_flow_state_changed(_previous: StringName, current: StringName) -> void:
|
||||
_boss_room_active = current == &"Gameplay_BossRoom"
|
||||
_update_boss_status_visibility()
|
||||
|
||||
|
||||
func _update_boss_status_visibility() -> void:
|
||||
if boss_status == null:
|
||||
return
|
||||
boss_status.visible = _boss_room_active and boss_actor != null and is_instance_valid(boss_actor)
|
||||
|
||||
|
||||
func _current_flow_state() -> StringName:
|
||||
var flow := get_tree().root.get_node_or_null("GameFlowManager")
|
||||
if flow == null:
|
||||
return &""
|
||||
return StringName(str(flow.get("state")))
|
||||
|
||||
|
||||
func _refresh_boss_name_label() -> void:
|
||||
if boss_name_label == null:
|
||||
return
|
||||
if boss_actor == null or not is_instance_valid(boss_actor):
|
||||
return
|
||||
var display_name: Variant = boss_actor.get("display_name")
|
||||
if display_name != null and not str(display_name).is_empty():
|
||||
boss_name_label.text = str(display_name)
|
||||
|
||||
|
||||
func refresh_debug_panel() -> void:
|
||||
if debug_actor == null or not is_instance_valid(debug_actor):
|
||||
_write_debug_defaults()
|
||||
return
|
||||
var state_machine := debug_actor.get_node_or_null("StateMachine")
|
||||
var context := {}
|
||||
if state_machine != null and state_machine.has_method("build_context"):
|
||||
context = state_machine.call("build_context")
|
||||
state_axes_label.text = "State %s / %s / %s / %s" % [
|
||||
context.get("life_state", &"Alive"),
|
||||
context.get("ground_state", &"Grounded"),
|
||||
context.get("action_phase", &"Neutral"),
|
||||
context.get("defense_state", &"Vulnerable"),
|
||||
]
|
||||
var action_controller := debug_actor.get_node_or_null("ActionController")
|
||||
action_label.text = "Action %s" % _debug_action_id(action_controller)
|
||||
anchor_label.text = "Anchor %s stretch %.3f" % [
|
||||
_debug_anchor_grid(action_controller),
|
||||
_debug_startup_stretch(action_controller),
|
||||
]
|
||||
var effect_container := debug_actor.get_node_or_null("EffectContainer")
|
||||
effects_label.text = "Effects %s" % _debug_effects(effect_container)
|
||||
defense_label.text = _debug_baseline_defense(context, effect_container)
|
||||
patterns_label.text = _patterns_debug_text
|
||||
calibration_label.text = _debug_calibration()
|
||||
_write_hit_log_label()
|
||||
_write_time_phase_debug()
|
||||
|
||||
|
||||
func _write_debug_defaults() -> void:
|
||||
state_axes_label.text = "State Alive / Grounded / Neutral / Vulnerable"
|
||||
action_label.text = "Action -"
|
||||
anchor_label.text = "Anchor - stretch 0.000"
|
||||
effects_label.text = "Effects -"
|
||||
defense_label.text = "Baseline Vulnerable damage 1.00 interrupt true"
|
||||
patterns_label.text = _patterns_debug_text
|
||||
calibration_label.text = _debug_calibration()
|
||||
_write_hit_log_label()
|
||||
_write_time_phase_debug()
|
||||
|
||||
|
||||
func _on_time_phase_changed(_previous: StringName, current: StringName, _reason: StringName) -> void:
|
||||
_current_time_phase = current
|
||||
_refresh_time_phase_hud()
|
||||
|
||||
|
||||
func _on_attack_buff_changed(stacks: int, max_stacks: int) -> void:
|
||||
_attack_buff_stacks = stacks
|
||||
_attack_buff_max = max_stacks
|
||||
_refresh_time_phase_hud()
|
||||
|
||||
|
||||
func _on_streak_changed(count: int) -> void:
|
||||
_streak_count = count
|
||||
_refresh_time_phase_hud()
|
||||
|
||||
|
||||
func _refresh_time_phase_hud() -> void:
|
||||
if time_phase_label == null:
|
||||
return
|
||||
var phase_color := TIME_PHASE_FUTURE_COLOR if _current_time_phase == &"future" else TIME_PHASE_PAST_COLOR
|
||||
time_phase_label.text = str(_current_time_phase).to_upper() + _next_time_anchor_hint()
|
||||
time_phase_label.add_theme_color_override("font_color", phase_color)
|
||||
attack_buff_label.text = "ATK BUFF %d/%d (+%d%%)" % [_attack_buff_stacks, _attack_buff_max, _attack_buff_stacks * 10]
|
||||
streak_label.text = "STREAK %d" % _streak_count
|
||||
|
||||
|
||||
func _next_time_anchor_hint() -> String:
|
||||
var time_anchor_system := get_tree().root.get_node_or_null("TimeAnchorSystem")
|
||||
var rhythm := get_tree().root.get_node_or_null("RhythmManager")
|
||||
if time_anchor_system == null or rhythm == null or not time_anchor_system.has_method("armed_anchor_summaries"):
|
||||
return ""
|
||||
var summaries: Array = time_anchor_system.call("armed_anchor_summaries")
|
||||
if summaries.is_empty():
|
||||
return ""
|
||||
var beat_time := maxf(0.001, float(rhythm.get("beat_time")))
|
||||
var current_beat := (float(rhythm.call("song_position")) + float(rhythm.get("beat_offset"))) / beat_time
|
||||
var next_beat := int((summaries[0] as Dictionary).get("beat", 0))
|
||||
var beats_left := maxf(0.0, float(next_beat) - current_beat)
|
||||
return " ANCHOR %db" % int(ceil(beats_left))
|
||||
|
||||
|
||||
func _write_time_phase_debug() -> void:
|
||||
if time_phase_debug_label == null:
|
||||
return
|
||||
time_phase_debug_label.text = "TimePhase %s streak %d buff %d/%d" % [
|
||||
_current_time_phase, _streak_count, _attack_buff_stacks, _attack_buff_max,
|
||||
]
|
||||
time_anchor_debug_label.text = "TimeAnchor %s" % _debug_time_anchor_summary()
|
||||
_refresh_time_phase_hud()
|
||||
|
||||
|
||||
func _debug_time_anchor_summary() -> String:
|
||||
var time_anchor_system := get_tree().root.get_node_or_null("TimeAnchorSystem")
|
||||
if time_anchor_system == null or not time_anchor_system.has_method("armed_anchor_summaries"):
|
||||
return "-"
|
||||
var parts: Array[String] = []
|
||||
var summaries: Array = time_anchor_system.call("armed_anchor_summaries")
|
||||
for summary: Variant in summaries:
|
||||
if summary is Dictionary:
|
||||
parts.append("b%d(%s)" % [int((summary as Dictionary).get("beat", 0)), (summary as Dictionary).get("source", &"-")])
|
||||
var last: Dictionary = time_anchor_system.call("last_resolved_summary") if time_anchor_system.has_method("last_resolved_summary") else {}
|
||||
var last_text := "-"
|
||||
if not last.is_empty():
|
||||
last_text = "b%d %s" % [int(last.get("beat", 0)), "held" if bool(last.get("held", false)) else "broken"]
|
||||
if parts.is_empty():
|
||||
return "armed - last %s" % last_text
|
||||
return "armed %s last %s" % [" ".join(parts), last_text]
|
||||
|
||||
|
||||
func _on_hit_confirmed(result: Dictionary) -> void:
|
||||
_hit_log_entries.push_front(_format_hit_result(result))
|
||||
_write_hit_log_label()
|
||||
|
||||
|
||||
func _on_judgement_made(_quality: StringName, offset_ms: float, _beat_index: int) -> void:
|
||||
_last_diff_ms = offset_ms
|
||||
calibration_label.text = _debug_calibration()
|
||||
|
||||
|
||||
func get_hit_log_entries() -> Array[String]:
|
||||
return _hit_log_entries.duplicate()
|
||||
|
||||
|
||||
func export_hit_log() -> String:
|
||||
return "\n".join(_hit_log_entries)
|
||||
|
||||
|
||||
func _debug_action_id(action_controller: Node) -> String:
|
||||
if action_controller == null:
|
||||
return "-"
|
||||
var action: Resource = action_controller.get("current_action") as Resource
|
||||
if action == null:
|
||||
return "-"
|
||||
var action_id := str(action.get("id"))
|
||||
return action_id if not action_id.is_empty() else "-"
|
||||
|
||||
|
||||
func _debug_anchor_grid(action_controller: Node) -> String:
|
||||
if action_controller == null:
|
||||
return "-"
|
||||
return str(action_controller.get("anchor_grid"))
|
||||
|
||||
|
||||
func _debug_startup_stretch(action_controller: Node) -> float:
|
||||
if action_controller == null:
|
||||
return 0.0
|
||||
return float(action_controller.get("startup_stretch_seconds"))
|
||||
|
||||
|
||||
func _debug_effects(effect_container: Node) -> String:
|
||||
if effect_container == null:
|
||||
return "-"
|
||||
if effect_container.has_method("active_effect_summaries"):
|
||||
var summaries: Array = effect_container.call("active_effect_summaries")
|
||||
if summaries.is_empty():
|
||||
return "-"
|
||||
var parts: Array[String] = []
|
||||
for summary: Variant in summaries:
|
||||
if summary is Dictionary:
|
||||
parts.append(_format_effect_summary(summary))
|
||||
return ", ".join(parts) if not parts.is_empty() else "-"
|
||||
if effect_container.has_method("active_effect_ids"):
|
||||
var ids: Array = effect_container.call("active_effect_ids")
|
||||
return "-" if ids.is_empty() else _join_string_names(ids)
|
||||
return "-"
|
||||
|
||||
|
||||
func _debug_baseline_defense(context: Dictionary, effect_container: Node) -> String:
|
||||
var defense_context := context.duplicate()
|
||||
if effect_container != null:
|
||||
defense_context["effect_container"] = effect_container
|
||||
var result: Dictionary = DefenseResolverScript.resolve_effective_defense(defense_context, [])
|
||||
return "Baseline %s damage %.2f interrupt %s" % [
|
||||
result.get("defense_state", &"Vulnerable"),
|
||||
float(result.get("damage_mult", 1.0)),
|
||||
str(bool(result.get("interrupts", true))).to_lower(),
|
||||
]
|
||||
|
||||
|
||||
func _debug_patterns() -> String:
|
||||
var patterns: Dictionary = ActionPatternExporterScript.export_patterns()
|
||||
if patterns.is_empty():
|
||||
return "Patterns -"
|
||||
var highlighted := []
|
||||
for key: String in ["A", "D", "S", "W"]:
|
||||
if patterns.has(key):
|
||||
highlighted.append("%s->%s" % [key, patterns[key]])
|
||||
return "Patterns %d total %s" % [
|
||||
patterns.size(),
|
||||
", ".join(highlighted),
|
||||
]
|
||||
|
||||
|
||||
func _format_hit_result(result: Dictionary) -> String:
|
||||
var action: Resource = result.get("action", null) as Resource
|
||||
var action_id := "-"
|
||||
if action != null:
|
||||
action_id = str(action.get("id"))
|
||||
var defense = result.get("defense", {})
|
||||
var defense_state := &"Vulnerable"
|
||||
if defense is Dictionary:
|
||||
defense_state = StringName(str(defense.get("defense_state", &"Vulnerable")))
|
||||
return "%s dmg %d %s interrupt %s" % [
|
||||
action_id,
|
||||
int(result.get("damage", 0)),
|
||||
defense_state,
|
||||
str(bool(result.get("interrupts", true))).to_lower(),
|
||||
]
|
||||
|
||||
|
||||
func _write_hit_log_label() -> void:
|
||||
if _hit_log_entries.is_empty():
|
||||
hit_log_label.text = "Hits -"
|
||||
return
|
||||
var compact := _hit_log_entries.duplicate()
|
||||
if compact.size() > 3:
|
||||
compact.resize(3)
|
||||
hit_log_label.text = "Hits %s" % " | ".join(compact)
|
||||
|
||||
|
||||
func _format_effect_summary(summary: Dictionary) -> String:
|
||||
return "%s %s x%d src:%s" % [
|
||||
summary.get("id", &"-"),
|
||||
_format_effect_remaining(StringName(str(summary.get("duration_type", &"infinite"))), float(summary.get("remaining", 0.0))),
|
||||
int(summary.get("stacks", 1)),
|
||||
summary.get("source", &"-"),
|
||||
]
|
||||
|
||||
|
||||
func _format_effect_remaining(duration_type: StringName, remaining: float) -> String:
|
||||
match duration_type:
|
||||
&"seconds":
|
||||
return "%.1fs" % remaining
|
||||
&"beats":
|
||||
return "%.1fb" % remaining
|
||||
&"actions":
|
||||
return "%da" % int(round(remaining))
|
||||
&"hits":
|
||||
return "%dhit" % int(round(remaining))
|
||||
&"hurts":
|
||||
return "%dhurt" % int(round(remaining))
|
||||
&"measures":
|
||||
return "%dmeasure" % int(round(remaining))
|
||||
&"until_event":
|
||||
return "until"
|
||||
return "inf"
|
||||
|
||||
|
||||
func _debug_calibration() -> String:
|
||||
if is_inf(_last_diff_ms):
|
||||
return "Diff -- ms"
|
||||
return "Diff %+.0f ms" % _last_diff_ms
|
||||
|
||||
|
||||
func _join_string_names(values: Array) -> String:
|
||||
var parts: Array[String] = []
|
||||
for value: Variant in values:
|
||||
parts.append(str(value))
|
||||
return ", ".join(parts)
|
||||
|
||||
|
||||
func _update_charge_bar_flash(delta: float) -> void:
|
||||
if not charge_bar_ready:
|
||||
charge_flash = 0.0
|
||||
return
|
||||
charge_flash = fmod(charge_flash + delta * 7.0, TAU)
|
||||
var alpha := 0.62 + 0.38 * absf(sin(charge_flash))
|
||||
charge_bar.modulate = Color(1.0, 1.0, 1.0, alpha)
|
||||
|
||||
|
||||
func _apply_bar_styles() -> void:
|
||||
# Health and boss health bars keep their editor-authored StyleBoxTexture
|
||||
# skins (playerui / boss_ui panel wells) so the editor view matches the
|
||||
# running game; only the flat charge bar is styled at runtime.
|
||||
charge_bar.add_theme_stylebox_override(
|
||||
"background",
|
||||
_make_style(Color(0.08, 0.07, 0.12, 0.86), Color(0.42, 0.36, 0.75, 0.9))
|
||||
)
|
||||
charge_bar.add_theme_stylebox_override(
|
||||
"fill",
|
||||
_make_style(Color(0.92, 0.72, 0.25, 1.0), Color.TRANSPARENT, false)
|
||||
)
|
||||
|
||||
|
||||
func _make_style(bg_color: Color, border_color: Color, has_border := true) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = bg_color
|
||||
if has_border:
|
||||
style.border_width_left = 1
|
||||
style.border_width_top = 1
|
||||
style.border_width_right = 1
|
||||
style.border_width_bottom = 1
|
||||
style.border_color = border_color
|
||||
return style
|
||||
|
||||
|
||||
func _bind_boss_health_component() -> void:
|
||||
if _boss_health_component != null and is_instance_valid(_boss_health_component):
|
||||
var old_callback := Callable(self, "_on_boss_health_changed")
|
||||
if _boss_health_component.is_connected("health_changed", old_callback):
|
||||
_boss_health_component.disconnect("health_changed", old_callback)
|
||||
_boss_health_component = null
|
||||
if boss_actor == null or not is_instance_valid(boss_actor):
|
||||
boss_health_bar.max_value = 1.0
|
||||
boss_health_bar.value = 0.0
|
||||
return
|
||||
_boss_health_component = boss_actor.get_node_or_null("HealthComponent")
|
||||
if _boss_health_component == null:
|
||||
boss_health_bar.max_value = 1.0
|
||||
boss_health_bar.value = 0.0
|
||||
return
|
||||
var callback := Callable(self, "_on_boss_health_changed")
|
||||
if not _boss_health_component.is_connected("health_changed", callback):
|
||||
_boss_health_component.connect("health_changed", callback)
|
||||
_on_boss_health_changed(int(_boss_health_component.get("current")), int(_boss_health_component.get("maximum")))
|
||||
|
||||
|
||||
func _on_boss_health_changed(current: int, maximum: int) -> void:
|
||||
boss_health_bar.max_value = max(1, maximum)
|
||||
boss_health_bar.value = clampi(current, 0, maximum)
|
||||
|
||||
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
uid://ckv47o2ktmwjg
|
||||
@@ -0,0 +1,313 @@
|
||||
[gd_scene format=3 uid="uid://d4fldy342eji2"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://ckv47o2ktmwjg" path="res://scenes/ui/main_ui.gd" id="1"]
|
||||
[ext_resource type="PackedScene" uid="uid://csydrlqpqyx3s" path="res://scenes/ui/rhythm_track.tscn" id="2"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/ui/combo_window_hud.tscn" id="3"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/ui/energy_bar.tscn" id="4"]
|
||||
[ext_resource type="Texture2D" uid="uid://28b848eqeh1m" path="res://assets/ui/panels/playerui.png" id="5_player_panel"]
|
||||
[ext_resource type="Texture2D" uid="uid://dvvfegdyyadmt" path="res://assets/ui/panels/player_red_ui.png" id="6_player_fill"]
|
||||
[ext_resource type="Texture2D" uid="uid://bra1w65idoypp" path="res://assets/ui/panels/boss_ui.png" id="7_boss_panel"]
|
||||
[ext_resource type="Texture2D" uid="uid://c2i4gxu07l8pd" path="res://assets/ui/panels/boss_red_ui.png" id="8_boss_fill"]
|
||||
[ext_resource type="Texture2D" uid="uid://co5xfbp12oxkm" path="res://assets/ui/panels/boss_portrait.png" id="9_boss_portrait"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hp_bg"]
|
||||
bg_color = Color(0.09, 0.06, 0.07, 0.55)
|
||||
|
||||
[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_hp_fill"]
|
||||
texture = ExtResource("6_player_fill")
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_boss_bg"]
|
||||
bg_color = Color(0.08, 0.05, 0.06, 0.55)
|
||||
|
||||
[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_boss_fill"]
|
||||
texture = ExtResource("8_boss_fill")
|
||||
|
||||
[node name="MainUI" type="Control" unique_id=1839627378]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="RhythmTrack" parent="." unique_id=1068155307 instance=ExtResource("2")]
|
||||
layout_mode = 1
|
||||
anchors_preset = 5
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -432.0
|
||||
offset_top = 0.0
|
||||
offset_right = 432.0
|
||||
offset_bottom = 0.0
|
||||
grow_horizontal = 2
|
||||
pivot_offset = Vector2(432, 0)
|
||||
scale = Vector2(1, 1)
|
||||
|
||||
[node name="StatusBars" type="Control" parent="." unique_id=988601372]
|
||||
anchors_preset = 0
|
||||
offset_left = 34.0
|
||||
offset_top = 28.0
|
||||
offset_right = 295.0
|
||||
offset_bottom = 139.0
|
||||
scale = Vector2(1.25, 1.25)
|
||||
[node name="PanelArt" type="TextureRect" parent="StatusBars" unique_id=471259199]
|
||||
layout_mode = 0
|
||||
offset_right = 261.0
|
||||
offset_bottom = 111.0
|
||||
texture = ExtResource("5_player_panel")
|
||||
expand_mode = 1
|
||||
[node name="HealthBar" type="ProgressBar" parent="StatusBars" unique_id=1224155284]
|
||||
layout_mode = 0
|
||||
offset_left = 118.0
|
||||
offset_top = 24.0
|
||||
offset_right = 242.0
|
||||
offset_bottom = 37.0
|
||||
theme_override_styles/background = SubResource("StyleBoxFlat_hp_bg")
|
||||
theme_override_styles/fill = SubResource("StyleBoxTexture_hp_fill")
|
||||
value = 100.0
|
||||
show_percentage = false
|
||||
[node name="EnergyBar" parent="StatusBars" unique_id=1355963744 instance=ExtResource("4")]
|
||||
layout_mode = 0
|
||||
offset_left = 116.0
|
||||
offset_top = 48.0
|
||||
offset_right = 244.0
|
||||
offset_bottom = 63.0
|
||||
[node name="ChargeBar" type="ProgressBar" parent="StatusBars" unique_id=720162692]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_left = 116.0
|
||||
offset_top = 74.0
|
||||
offset_right = 244.0
|
||||
offset_bottom = 87.0
|
||||
max_value = 1.1
|
||||
show_percentage = false
|
||||
[node name="ChargeLevelLabel" type="Label" parent="StatusBars" unique_id=360971327]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_left = 116.0
|
||||
offset_top = 70.0
|
||||
offset_right = 244.0
|
||||
offset_bottom = 92.0
|
||||
theme_override_colors/font_color = Color(0.92, 0.72, 0.25, 1)
|
||||
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.85)
|
||||
theme_override_constants/shadow_offset_x = 1
|
||||
theme_override_constants/shadow_offset_y = 1
|
||||
theme_override_font_sizes/font_size = 16
|
||||
[node name="BossStatus" type="Control" parent="." unique_id=529865591]
|
||||
visible = false
|
||||
anchors_preset = 0
|
||||
offset_left = 900.36
|
||||
offset_top = 34.0
|
||||
offset_right = 1198.36
|
||||
offset_bottom = 113.0
|
||||
scale = Vector2(1.18, 1.18)
|
||||
[node name="PanelArt" type="TextureRect" parent="BossStatus" unique_id=1398889507]
|
||||
layout_mode = 0
|
||||
offset_right = 298.0
|
||||
offset_bottom = 79.0
|
||||
texture = ExtResource("7_boss_panel")
|
||||
expand_mode = 1
|
||||
[node name="BossPortrait" type="TextureRect" parent="BossStatus"]
|
||||
layout_mode = 0
|
||||
offset_left = 236.0
|
||||
offset_top = 11.0
|
||||
offset_right = 293.0
|
||||
offset_bottom = 69.0
|
||||
texture = ExtResource("9_boss_portrait")
|
||||
[node name="BossHealthBar" type="ProgressBar" parent="BossStatus" unique_id=255410015]
|
||||
layout_mode = 0
|
||||
offset_left = 25.0
|
||||
offset_top = 35.0
|
||||
offset_right = 219.0
|
||||
offset_bottom = 50.0
|
||||
theme_override_styles/background = SubResource("StyleBoxFlat_boss_bg")
|
||||
theme_override_styles/fill = SubResource("StyleBoxTexture_boss_fill")
|
||||
max_value = 28000.0
|
||||
value = 28000.0
|
||||
show_percentage = false
|
||||
[node name="BossNameLabel" type="Label" parent="BossStatus" unique_id=1576490707]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_left = 25.0
|
||||
offset_top = 8.0
|
||||
offset_right = 219.0
|
||||
offset_bottom = 30.0
|
||||
theme_override_colors/font_color = Color(0.95, 0.86, 0.72, 1)
|
||||
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.85)
|
||||
theme_override_constants/shadow_offset_x = 2
|
||||
theme_override_constants/shadow_offset_y = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "DREAD HARBINGER"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
[node name="ComboWindow" parent="." unique_id=586844780 instance=ExtResource("3")]
|
||||
layout_mode = 0
|
||||
offset_left = 42.0
|
||||
offset_top = 410.0
|
||||
offset_right = 297.0
|
||||
offset_bottom = 461.0
|
||||
scale = Vector2(1.28, 1.28)
|
||||
[node name="ComboSkillLabel" type="Label" parent="." unique_id=1934463479]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
anchors_preset = 5
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -360.0
|
||||
offset_top = 548.0
|
||||
offset_right = 360.0
|
||||
offset_bottom = 590.0
|
||||
grow_horizontal = 2
|
||||
theme_override_colors/font_color = Color(1, 0.84, 0.26, 1)
|
||||
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.85)
|
||||
theme_override_constants/shadow_offset_x = 3
|
||||
theme_override_constants/shadow_offset_y = 3
|
||||
theme_override_font_sizes/font_size = 30
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
[node name="TimePhaseHud" type="VBoxContainer" parent="." unique_id=293359189]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_left = 40.0
|
||||
offset_top = 430.0
|
||||
offset_right = 420.0
|
||||
offset_bottom = 540.0
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="TimePhaseLabel" type="Label" parent="TimePhaseHud" unique_id=1712218313]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 0.84, 0.4, 1)
|
||||
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.85)
|
||||
theme_override_constants/shadow_offset_x = 2
|
||||
theme_override_constants/shadow_offset_y = 2
|
||||
theme_override_font_sizes/font_size = 30
|
||||
text = "PAST"
|
||||
|
||||
[node name="AttackBuffLabel" type="Label" parent="TimePhaseHud" unique_id=1432771975]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 0.62, 0.32, 1)
|
||||
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.85)
|
||||
theme_override_constants/shadow_offset_x = 2
|
||||
theme_override_constants/shadow_offset_y = 2
|
||||
theme_override_font_sizes/font_size = 20
|
||||
text = "ATK BUFF 0/20"
|
||||
|
||||
[node name="StreakLabel" type="Label" parent="TimePhaseHud" unique_id=920703172]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.84, 0.94, 1, 1)
|
||||
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.85)
|
||||
theme_override_constants/shadow_offset_x = 2
|
||||
theme_override_constants/shadow_offset_y = 2
|
||||
theme_override_font_sizes/font_size = 20
|
||||
text = "STREAK 0"
|
||||
|
||||
[node name="DebugPanel" type="VBoxContainer" parent="." unique_id=666041163]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_left = 912.0
|
||||
offset_top = 172.0
|
||||
offset_right = 1270.0
|
||||
offset_bottom = 348.0
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="StateAxesLabel" type="Label" parent="DebugPanel" unique_id=1326887607]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.84, 0.94, 1, 1)
|
||||
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.8)
|
||||
theme_override_constants/shadow_offset_x = 1
|
||||
theme_override_constants/shadow_offset_y = 1
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "State Alive / Grounded / Neutral / Vulnerable"
|
||||
horizontal_alignment = 2
|
||||
|
||||
[node name="ActionLabel" type="Label" parent="DebugPanel" unique_id=823075570]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.84, 0.94, 1, 1)
|
||||
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.8)
|
||||
theme_override_constants/shadow_offset_x = 1
|
||||
theme_override_constants/shadow_offset_y = 1
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "Action -"
|
||||
horizontal_alignment = 2
|
||||
|
||||
[node name="AnchorLabel" type="Label" parent="DebugPanel" unique_id=287138792]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.84, 0.94, 1, 1)
|
||||
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.8)
|
||||
theme_override_constants/shadow_offset_x = 1
|
||||
theme_override_constants/shadow_offset_y = 1
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "Anchor - stretch 0.000"
|
||||
horizontal_alignment = 2
|
||||
|
||||
[node name="EffectsLabel" type="Label" parent="DebugPanel" unique_id=850058697]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.84, 0.94, 1, 1)
|
||||
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.8)
|
||||
theme_override_constants/shadow_offset_x = 1
|
||||
theme_override_constants/shadow_offset_y = 1
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "Effects -"
|
||||
horizontal_alignment = 2
|
||||
|
||||
[node name="DefenseLabel" type="Label" parent="DebugPanel" unique_id=1580894607]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.84, 0.94, 1, 1)
|
||||
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.8)
|
||||
theme_override_constants/shadow_offset_x = 1
|
||||
theme_override_constants/shadow_offset_y = 1
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "Baseline Vulnerable damage 1.00 interrupt true"
|
||||
horizontal_alignment = 2
|
||||
|
||||
[node name="PatternsLabel" type="Label" parent="DebugPanel" unique_id=1764315844]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.84, 0.94, 1, 1)
|
||||
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.8)
|
||||
theme_override_constants/shadow_offset_x = 1
|
||||
theme_override_constants/shadow_offset_y = 1
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "Patterns -"
|
||||
horizontal_alignment = 2
|
||||
|
||||
[node name="HitLogLabel" type="Label" parent="DebugPanel" unique_id=137664462]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.84, 0.94, 1, 1)
|
||||
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.8)
|
||||
theme_override_constants/shadow_offset_x = 1
|
||||
theme_override_constants/shadow_offset_y = 1
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "Hits -"
|
||||
horizontal_alignment = 2
|
||||
|
||||
[node name="CalibrationLabel" type="Label" parent="DebugPanel" unique_id=1640694321]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.84, 0.94, 1, 1)
|
||||
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.8)
|
||||
theme_override_constants/shadow_offset_x = 1
|
||||
theme_override_constants/shadow_offset_y = 1
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "Diff -- ms"
|
||||
horizontal_alignment = 2
|
||||
|
||||
[node name="TimePhaseDebugLabel" type="Label" parent="DebugPanel" unique_id=1559421767]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.84, 0.94, 1, 1)
|
||||
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.8)
|
||||
theme_override_constants/shadow_offset_x = 1
|
||||
theme_override_constants/shadow_offset_y = 1
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "TimePhase past streak 0 buff 0"
|
||||
horizontal_alignment = 2
|
||||
|
||||
[node name="TimeAnchorDebugLabel" type="Label" parent="DebugPanel" unique_id=683859755]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.84, 0.94, 1, 1)
|
||||
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.8)
|
||||
theme_override_constants/shadow_offset_x = 1
|
||||
theme_override_constants/shadow_offset_y = 1
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "TimeAnchor -"
|
||||
horizontal_alignment = 2
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://cl8oghmhtwkl
|
||||
@@ -0,0 +1,204 @@
|
||||
[gd_scene format=3 uid="uid://csydrlqpqyx3s"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cl8oghmhtwkl" path="res://scenes/ui/rhythm_track.gd" id="1"]
|
||||
[ext_resource type="Texture2D" uid="uid://6qsuvcixdqmp" path="res://assets/ui/rhythm/b.png" id="2_track_bg"]
|
||||
[ext_resource type="Texture2D" uid="uid://dml5nkni6bpoq" path="res://assets/ui/rhythm/anchor01.png" id="4_center_idle"]
|
||||
[ext_resource type="Texture2D" uid="uid://cgswvlcqufim" path="res://assets/ui/rhythm/c01.png" id="5_center_flash"]
|
||||
[ext_resource type="Texture2D" uid="uid://rt631il3y6h7" path="res://assets/ui/rhythm/star.png" id="6_star"]
|
||||
[ext_resource type="Texture2D" path="res://assets/ui/buff/buff_off.png" id="7_buff_off"]
|
||||
[ext_resource type="Texture2D" path="res://assets/ui/judgements/perfect.png" id="8_judgement_perfect"]
|
||||
|
||||
[node name="RhythmTrack" type="Control" unique_id=1294325361]
|
||||
layout_mode = 3
|
||||
anchors_preset = 5
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -432.0
|
||||
offset_top = 54.0
|
||||
offset_right = 432.0
|
||||
offset_bottom = 294.0
|
||||
grow_horizontal = 2
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="TrackBackground" type="TextureRect" parent="." unique_id=325202199]
|
||||
layout_mode = 0
|
||||
offset_left = 16.0
|
||||
offset_top = 19.0
|
||||
offset_right = 848.0
|
||||
offset_bottom = 135.0
|
||||
texture = ExtResource("2_track_bg")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="TrackLine" type="TextureRect" parent="." unique_id=1123045159]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_left = 66.0
|
||||
offset_top = 40.0
|
||||
offset_right = 798.0
|
||||
offset_bottom = 87.0
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="LeftMover" type="TextureRect" parent="." unique_id=790581017]
|
||||
layout_mode = 0
|
||||
offset_left = 32.0
|
||||
offset_top = 32.5
|
||||
offset_right = 100.0
|
||||
offset_bottom = 94.5
|
||||
texture = ExtResource("6_star")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="RightMover" type="TextureRect" parent="." unique_id=46330219]
|
||||
layout_mode = 0
|
||||
offset_left = 764.0
|
||||
offset_top = 32.5
|
||||
offset_right = 832.0
|
||||
offset_bottom = 94.5
|
||||
texture = ExtResource("6_star")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="CenterBase" type="TextureRect" parent="." unique_id=652811094]
|
||||
z_index = 2
|
||||
layout_mode = 0
|
||||
offset_left = 389.0
|
||||
offset_top = -2.0
|
||||
offset_right = 475.0
|
||||
offset_bottom = 129.0
|
||||
texture = ExtResource("4_center_idle")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="CenterFlash" type="TextureRect" parent="." unique_id=1409206211]
|
||||
modulate = Color(1, 1, 1, 0)
|
||||
z_index = 1
|
||||
layout_mode = 0
|
||||
offset_left = 359.0
|
||||
offset_top = -12.0
|
||||
offset_right = 505.0
|
||||
offset_bottom = 139.0
|
||||
texture = ExtResource("5_center_flash")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="ChartMarkerContainer" type="Control" parent="." unique_id=92513456]
|
||||
anchors_preset = 0
|
||||
offset_right = 864.0
|
||||
offset_bottom = 106.0
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="HudReadoutRow" type="VBoxContainer" parent="."]
|
||||
layout_mode = 0
|
||||
offset_left = 214.0
|
||||
offset_top = 104.0
|
||||
offset_right = 650.0
|
||||
offset_bottom = 138.0
|
||||
theme_override_constants/separation = 2
|
||||
alignment = 1
|
||||
|
||||
[node name="ComboCountLabel" type="Label" parent="HudReadoutRow"]
|
||||
custom_minimum_size = Vector2(190, 34)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 0.88, 0.46, 1)
|
||||
theme_override_colors/font_shadow_color = Color(0.18, 0.03, 0.01, 0.95)
|
||||
theme_override_constants/shadow_offset_x = 2
|
||||
theme_override_constants/shadow_offset_y = 2
|
||||
theme_override_font_sizes/font_size = 28
|
||||
text = "combo 0"
|
||||
horizontal_alignment = 0
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="AttackBuffValueLabel" type="Label" parent="HudReadoutRow"]
|
||||
custom_minimum_size = Vector2(190, 34)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 0.88, 0.46, 1)
|
||||
theme_override_colors/font_shadow_color = Color(0.18, 0.03, 0.01, 0.95)
|
||||
theme_override_constants/shadow_offset_x = 2
|
||||
theme_override_constants/shadow_offset_y = 2
|
||||
theme_override_font_sizes/font_size = 28
|
||||
text = "ATK x 0%"
|
||||
horizontal_alignment = 0
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="BuffPipRow" type="HBoxContainer" parent="."]
|
||||
layout_mode = 0
|
||||
offset_left = 249.0
|
||||
offset_top = 132.0
|
||||
offset_right = 615.0
|
||||
offset_bottom = 190.0
|
||||
theme_override_constants/separation = 12
|
||||
alignment = 1
|
||||
|
||||
[node name="BuffPip1" type="TextureRect" parent="BuffPipRow"]
|
||||
custom_minimum_size = Vector2(42, 55)
|
||||
layout_mode = 2
|
||||
texture = ExtResource("7_buff_off")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="BuffPip2" type="TextureRect" parent="BuffPipRow"]
|
||||
custom_minimum_size = Vector2(42, 55)
|
||||
layout_mode = 2
|
||||
texture = ExtResource("7_buff_off")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="BuffPip3" type="TextureRect" parent="BuffPipRow"]
|
||||
custom_minimum_size = Vector2(42, 55)
|
||||
layout_mode = 2
|
||||
texture = ExtResource("7_buff_off")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="BuffPip4" type="TextureRect" parent="BuffPipRow"]
|
||||
custom_minimum_size = Vector2(42, 55)
|
||||
layout_mode = 2
|
||||
texture = ExtResource("7_buff_off")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="BuffPip5" type="TextureRect" parent="BuffPipRow"]
|
||||
custom_minimum_size = Vector2(42, 55)
|
||||
layout_mode = 2
|
||||
texture = ExtResource("7_buff_off")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="BuffPip6" type="TextureRect" parent="BuffPipRow"]
|
||||
custom_minimum_size = Vector2(42, 55)
|
||||
layout_mode = 2
|
||||
texture = ExtResource("7_buff_off")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="BuffPip7" type="TextureRect" parent="BuffPipRow"]
|
||||
custom_minimum_size = Vector2(42, 55)
|
||||
layout_mode = 2
|
||||
texture = ExtResource("7_buff_off")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="JudgementArt" type="TextureRect" parent="."]
|
||||
layout_mode = 0
|
||||
offset_left = 310.0
|
||||
offset_top = 222.0
|
||||
offset_right = 554.0
|
||||
offset_bottom = 282.0
|
||||
pivot_offset = Vector2(122, 30)
|
||||
texture = ExtResource("8_judgement_perfect")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="JudgementLabel" type="Label" parent="." unique_id=1712665799]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_left = 202.0
|
||||
offset_top = 222.0
|
||||
offset_right = 662.0
|
||||
offset_bottom = 282.0
|
||||
theme_override_font_sizes/font_size = 30
|
||||
text = "READY"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
Reference in New Issue
Block a user