# gameplay.gd (更新版) extends NinePatchRect # UI Node References @onready var list_container = $Option/Info @onready var previous_button = $Title/previous/Button @onready var next_button = $Title/next/Button @onready var title_label = $Title/Title # Configuration var items_per_page: int = 7 # 每页显示的选项数量 # Data Storage var gameplay_list_nodes: Array[VBoxContainer] = [] # 存储 List_1, List_2 等节点 var option_nodes_per_list: Array = [] # 存储每个 List 对应的 option_X 按钮数组 var all_enabled_gameplays: Array = [] # 存储筛选后的玩法数据 { "name": String, "data": Dictionary } # State var total_pages: int = 0 var current_page_index: int = 0 var selected_gameplay_key: String = "" # 当前逻辑上选中的项的 Key (用于确认) var currently_selected_button: Button = null # 当前高亮/聚焦的按钮节点引用 var initial_key_to_highlight: String = "" # 弹窗打开时需要高亮的 Key (由父节点设置) func _ready(): # 1. 获取 List 节点 (与原版一致) for i in range(1, 4): # 检查 List_1, List_2, List_3 var list_node = list_container.get_node_or_null("List_" + str(i)) if list_node: gameplay_list_nodes.append(list_node) else: break if gameplay_list_nodes.is_empty(): printerr("错误:在 gameplay.tscn 中未找到 List 节点!无法显示选项。") set_process(false) return # 2. 获取每个 List 内的 Option 节点 (与原版一致) for list_node in gameplay_list_nodes: var current_list_options: Array = [] for i in range(1, items_per_page + 1): var option_node = list_node.get_node_or_null("option_" + str(i)) if option_node and option_node is Button: current_list_options.append(option_node) else: push_warning("警告:在 %s 中未能找到 option_%d 按钮" % [list_node.name, i]) option_nodes_per_list.append(current_list_options) # 3. 连接按钮信号 (与原版一致) if previous_button: if not previous_button.pressed.is_connected(_on_previous_pressed): previous_button.pressed.connect(_on_previous_pressed) else: printerr("错误:在 gameplay.tscn 中未找到 Previous 按钮") if next_button: if not next_button.pressed.is_connected(_on_next_pressed): next_button.pressed.connect(_on_next_pressed) else: printerr("错误:在 gameplay.tscn 中未找到 Next 按钮") # 4. 连接 visibility_changed 信号 (与原版一致) if not is_connected("visibility_changed", _on_visibility_changed): visibility_changed.connect(_on_visibility_changed) # 5. 移除 _ready 中的初始数据加载和显示更新 # 这些逻辑现在完全由 _on_visibility_changed -> reset_display 处理 # --- Visibility Change Handler --- func _on_visibility_changed(): if is_visible_in_tree(): print("Gameplay: 弹窗变为可见,调用 reset_display()") reset_display() # 当变为可见时调用 reset # else: # 可选:隐藏时的清理逻辑 # print("Gameplay: 弹窗变为隐藏。") # --- 新增:用于父节点设置初始选中项 --- func set_initial_selection(key: String): """ 由父节点 (task_development) 在显示此弹窗前调用, 传递当前在 task_info 中选中的玩法 Key。 """ initial_key_to_highlight = key print("Gameplay: 父节点设置初始高亮 Key 为: '%s'" % initial_key_to_highlight) # --- Data Processing and Display Logic --- func _process_gameplay_data(all_gameplays_data: Dictionary): """【修改】根据从 GameState 获取的玩法数据,筛选出启用的玩法,并计算总页数。""" all_enabled_gameplays.clear() if all_gameplays_data.is_empty(): push_warning("警告:接收到的玩法数据为空。") total_pages = 0 return # 遍历从 GameState 获取的玩法字典 for gameplay_name in all_gameplays_data: var gameplay_info = all_gameplays_data[gameplay_name] # 检查是否为字典且 "enabled" 字段为 true if typeof(gameplay_info) == TYPE_DICTIONARY and gameplay_info.get("enabled", false) == true: all_enabled_gameplays.append({"name": gameplay_name, "data": gameplay_info}) # else: # Debugging: 可以取消注释来看哪些被过滤了 # print("Gameplay: 过滤掉未启用或格式错误的玩法: ", gameplay_name) # 根据筛选结果计算总页数 if items_per_page > 0: total_pages = ceil(float(all_enabled_gameplays.size()) / items_per_page) else: total_pages = 0 printerr("错误:items_per_page 为零,无法计算页数。") print("Gameplay: 处理完成,找到 %d 个启用的玩法,共 %d 页。" % [all_enabled_gameplays.size(), total_pages]) func reset_display(): """【修改】重置状态,从 GameState 重新加载数据,并准备更新显示。""" print("Gameplay: 重置显示状态 (reset_display)。") # 1. 重置本地状态变量 currently_selected_button = null selected_gameplay_key = "" # initial_key_to_highlight 由 set_initial_selection 设置,这里不清空 # 2. 【修改】从 GameState 重新加载并处理数据 if not GameState: printerr("错误:GameState 在 reset_display 期间不可用。") total_pages = 0 all_enabled_gameplays.clear() _update_display() # 即使没有数据也要更新显示(显示空状态) return var task_dev_data = GameState.get_value("task_development", {}) if task_dev_data.is_empty(): printerr("错误:从 GameState 获取 'task_development' 数据失败。") total_pages = 0 all_enabled_gameplays.clear() else: var all_gameplays_data = task_dev_data.get("gameplays", {}) _process_gameplay_data(all_gameplays_data) # 重新处理数据,计算 total_pages # 3. 【修改】根据 initial_key_to_highlight 确定初始页面 current_page_index = 0 # 默认为第一页 var found_initial_key = false if total_pages > 0 and not initial_key_to_highlight.is_empty(): # 查找 initial_key_to_highlight 所在的索引和页面 for i in range(all_enabled_gameplays.size()): if all_enabled_gameplays[i].name == initial_key_to_highlight: current_page_index = int(floor(float(i) / items_per_page)) found_initial_key = true print("Gameplay: 找到需要初始高亮的 Key '%s' 在索引 %d, 目标页面 %d" % [initial_key_to_highlight, i, current_page_index]) break # 找到了,停止循环 if not found_initial_key: print("Gameplay: 需要初始高亮的 Key '%s' 未在启用的玩法中找到,将显示第一页。" % initial_key_to_highlight) # 保持 current_page_index 为 0 initial_key_to_highlight = "" # 清空,避免后续 _update_display 尝试高亮不存在的项 # 确保页面索引有效 if total_pages == 0: current_page_index = 0 elif current_page_index >= total_pages: current_page_index = total_pages - 1 elif current_page_index < 0: current_page_index = 0 # 4. 触发显示更新 _update_display() func _update_display(): """【更新版】更新列表的可见性并填充当前页面的内容。 现在将“经验”和“成本”显示为整数。 """ # --- 处理无数据情况 --- if gameplay_list_nodes.is_empty(): print("Gameplay: 无可用的 List 节点。") if title_label: title_label.text = "玩法选择" return if total_pages == 0: for list_node in gameplay_list_nodes: list_node.visible = false print("Gameplay: 没有启用的玩法选项可供显示。") if title_label: title_label.text = "玩法选择 (无可用)" # 确保按钮也禁用或隐藏 if previous_button: previous_button.disabled = true if next_button: next_button.disabled = true return else: # 确保按钮状态正确 (启用/禁用基于页数) if previous_button: previous_button.disabled = (total_pages <= 1) if next_button: next_button.disabled = (total_pages <= 1) # --- 更新页面索引 (循环) --- # 注意: reset_display 设置了初始页, 取模主要用于翻页 if total_pages > 0 : # 避免除以零 current_page_index = current_page_index % total_pages if current_page_index < 0: current_page_index += total_pages else: current_page_index = 0 # --- 更新标题 --- if title_label: var display_page_number = current_page_index + 1 # 显示给用户的页码从1开始 title_label.text = "玩法选择 %d/%d" % [display_page_number, total_pages] # --- 更新 List 可见性 --- # 确保只有一个 List 节点是可见的,即当前页对应的 List for i in range(gameplay_list_nodes.size()): gameplay_list_nodes[i].visible = (i == current_page_index) # --- 填充可见列表并查找要高亮的按钮 --- var button_to_highlight: Button = null var first_enabled_button_on_page: Button = null # 如果 initial_key 不在本页,则使用此按钮 # 检查当前页索引是否在有效范围内 if current_page_index < option_nodes_per_list.size(): var current_options = option_nodes_per_list[current_page_index] # 获取当前页的所有按钮节点 var start_index = current_page_index * items_per_page # 计算当前页数据在 all_enabled_gameplays 中的起始索引 # 遍历当前页的按钮槽位 for i in range(current_options.size()): var option_button = current_options[i] # 获取具体的按钮节点 var item_index = start_index + i # 计算此按钮对应的数据索引 # --- 断开旧信号 --- # 确保在连接新信号前断开旧的,防止重复连接 if option_button.pressed.is_connected(_on_option_selected): option_button.pressed.disconnect(_on_option_selected) # 检查数据索引是否在有效范围内 if item_index < all_enabled_gameplays.size(): # 获取对应的玩法数据 var gameplay_entry = all_enabled_gameplays[item_index] var gameplay_name = gameplay_entry.name var gameplay_data = gameplay_entry.data # 这是包含 Cost, Experience, Popularity 等信息的字典 # 获取按钮内部用于显示信息的 Row 节点 var row = option_button.get_node_or_null("Row") if row: # 填充标签文本 var title_label_in_row = row.get_node_or_null("Title") var exp_label = row.get_node_or_null("Experience") var pop_label = row.get_node_or_null("Popularity") # 查找成本标签,兼容 "Cost" 和 "cost" 两种可能的节点名 var cost_label = row.get_node_or_null("Cost") if row.has_node("Cost") else row.get_node_or_null("cost") # 设置标题 if title_label_in_row: title_label_in_row.text = gameplay_name # 【修改】设置经验值,显示为整数 if exp_label: var experience_value = gameplay_data.get("Experience", 0.0) # 获取原始值 exp_label.text = str(int(experience_value)) # 转为整数再转为字符串 # 【保持】设置流行度,保持原始字符串显示 if pop_label: var popularity_value = gameplay_data.get("Popularity", "未知") # 获取原始值 pop_label.text = str(popularity_value) # 直接转为字符串 # 【修改】设置成本,显示为整数 if cost_label: var cost_value = gameplay_data.get("Cost", 0.0) # 获取原始值 cost_label.text = str(int(cost_value)) # 转为整数再转为字符串 # 设置按钮可见、可用,并存储玩法名称到元数据 option_button.visible = true option_button.disabled = false option_button.set_meta("gameplay_name", gameplay_name) # --- 连接新信号 (使用 bind) --- # 传递玩法名称和按钮节点本身给处理函数 option_button.pressed.connect(_on_option_selected.bind(gameplay_name, option_button)) # 记录本页第一个可用的按钮,作为备选高亮目标 if first_enabled_button_on_page == null: first_enabled_button_on_page = option_button # 检查此按钮是否是需要初始高亮的按钮 if not initial_key_to_highlight.is_empty() and gameplay_name == initial_key_to_highlight: button_to_highlight = option_button # print("Gameplay: 找到需要初始高亮的按钮: ", button_to_highlight.name) # Debug else: # 未找到 Row 节点 push_warning("警告:在按钮 %s 中未找到 Row 节点" % option_button.get_path()) option_button.visible = false # 隐藏按钮以避免显示不完整 option_button.disabled = true else: # 此槽位没有对应的启用玩法数据 (例如最后一页不满) option_button.visible = false # 隐藏按钮 option_button.disabled = true else: # 这种情况理论上不应发生,除非页面计算或节点结构有问题 printerr("错误:当前页面索引 %d 超出 option_nodes_per_list 的范围 (大小 %d)" % [current_page_index, option_nodes_per_list.size()]) # --- 设置初始高亮和焦点 --- # 优先高亮 initial_key_to_highlight 对应的按钮 if button_to_highlight: # print("Gameplay: 尝试高亮指定按钮: ", button_to_highlight.name) # Debug _set_initial_highlight(button_to_highlight) # 如果没有指定按钮或指定按钮不在当前页,则高亮当前页的第一个可用按钮 elif first_enabled_button_on_page: print("Gameplay: 初始 Key '%s' 不在本页或为空。高亮本页第一个按钮: %s" % [initial_key_to_highlight, first_enabled_button_on_page.name]) _set_initial_highlight(first_enabled_button_on_page) # 如果当前页没有任何可用按钮 else: print("Gameplay: 本页没有可供高亮的按钮。") currently_selected_button = null # 确保没有按钮被标记为选中 selected_gameplay_key = "" # 确保没有 Key 被标记为选中 # (Helper) 设置初始高亮和焦点 (与原版类似,增加日志) func _set_initial_highlight(button_to_highlight: Button): if not is_instance_valid(button_to_highlight): printerr("错误:传递给 _set_initial_highlight 的按钮无效。") currently_selected_button = null selected_gameplay_key = "" return # print("Gameplay: 设置高亮按钮: ", button_to_highlight.name) # Debug currently_selected_button = button_to_highlight selected_gameplay_key = button_to_highlight.get_meta("gameplay_name", "") # 同时设置 Key # print("Gameplay: currently_selected_button 设置为: ", currently_selected_button.name) # Debug # print("Gameplay: selected_gameplay_key 设置为: '", selected_gameplay_key, "'") # Debug # 延迟调用 grab_focus 确保按钮已准备好接收焦点 if currently_selected_button and currently_selected_button.is_inside_tree(): # print("Gameplay: 尝试为按钮 call_deferred('grab_focus'): ", currently_selected_button.name) # Debug currently_selected_button.call_deferred("grab_focus") else: print("Gameplay: 按钮无效或不在场景树中,无法获取焦点。") # --- Signal Handlers --- func _on_previous_pressed(): if total_pages > 1: # 仅当有多页时才翻页 current_page_index -= 1 # 循环由 _update_display 处理 _update_display() func _on_next_pressed(): if total_pages > 1: # 仅当有多页时才翻页 current_page_index += 1 # 循环由 _update_display 处理 _update_display() # 【修改】处理选项按钮按下事件 (与原版类似,增加日志) func _on_option_selected(gameplay_name: String, button_node: Button): """当选项按钮被按下时调用。""" if not is_instance_valid(button_node): printerr("错误:在 _on_option_selected 中收到无效按钮节点,Key: " + gameplay_name) return if button_node == currently_selected_button: # --- 双击 (或点击已选中的按钮) --- print("Gameplay: 检测到双击或确认点击: ", gameplay_name) _confirm_and_close(gameplay_name) # 确认此选择 else: # --- 首次点击或点击不同按钮 --- print("Gameplay: 选中 (高亮) 玩法: ", gameplay_name) currently_selected_button = button_node selected_gameplay_key = gameplay_name # 更新 Key,以便后续可能的确认 # 更新视觉焦点 button_node.grab_focus() # 【修改】确认选择并关闭弹窗的逻辑 func _confirm_and_close(key_to_confirm: String): """更新父节点并关闭此弹窗。""" if key_to_confirm.is_empty(): printerr("错误:尝试确认一个空的玩法 Key。") return print("Gameplay: 确认选择: ", key_to_confirm) # --- 更新父节点 (Task Development Popup) --- var parent_node = get_parent() if parent_node and parent_node.has_method("update_task_options"): # 通知父节点更新其内部的 task_info parent_node.update_task_options({"玩法": key_to_confirm}) print("Gameplay: 已更新父节点的 task_options: 玩法 = ", key_to_confirm) else: printerr("错误:Gameplay: 父节点未找到或缺少 'update_task_options' 方法。") # 考虑是否应该在此处停止或仅记录错误后继续 # --- 关闭此弹窗 --- if parent_node and parent_node.has_method("_close_child_popup_and_return"): # 调用父节点的标准方法来关闭子弹窗并返回 parent_node._close_child_popup_and_return(self) else: printerr("错误:Gameplay: 父节点未找到或缺少 '_close_child_popup_and_return' 方法。") self.hide() # 作为备选方案,直接隐藏自身