772 lines
27 KiB
GDScript
772 lines
27 KiB
GDScript
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
|