Initial project sync

This commit is contained in:
wxm
2026-07-06 22:55:14 -07:00
commit 8cca00d0da
1066 changed files with 58585 additions and 0 deletions
+771
View File
@@ -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