class_name FriendPanel extends RefCounted # 好友/排行面板: # - 负责全国排行、好友排行、翻页和好友行渲染。 # - 排行接口返回的数据只在本面板分页状态中保存,不参与全局玩家状态。 # - 行按钮后续扩展访问好友时,应继续通过 callbacks 发回上层路由。 const NumberFormatterScript := preload("res://scripts/core/number_formatter.gd") const I18nScript := preload("res://scripts/core/i18n.gd") var _ui var _texture_provider var _friend_api var _context var _callbacks: Dictionary = {} var _page_size := 7 var _rank_type := 0 var _show_type := 1 var _page_index := 1 var _page_max := 1 var _panel: Control var _list: Control var _page_label: Label var _title_lv_label: Label func setup(options: Dictionary) -> void: _ui = options.get("ui_builder") _texture_provider = options.get("texture_provider") _friend_api = options.get("friend_api") _context = options.get("context") _callbacks = options.get("callbacks", {}) if _callbacks.is_empty() and _context != null and _context.has_method("legacy_callbacks"): _callbacks = _context.legacy_callbacks() _page_size = int(options.get("page_size", _page_size)) func create(rank_type: int) -> Control: _rank_type = rank_type _show_type = 1 _page_index = 1 _page_max = 1 _panel = _ui.create_common_panel_shell("FriendPanel", Vector2(594.0, 862.0), "panel_icon_friend", _texture_provider.atlas_path("friend"), "panel_title_friend", _texture_provider.atlas_path("friend")) _ui.add_content_panel_background(_panel, "InnerPage", Vector2(88.0, 206.0), Vector2(464.0, 638.0)) _ui.create_tab_bar(_panel, "FriendTabs", ["全国排名", "好友排名"], Vector2(88.0, 159.0), _rank_type, Callable(self, "_on_tab_pressed"), 50.0) _ui.add_panel_label(_panel, "TitleRank", "排名", Vector2(120.0, 233.0), Vector2(60.0, 32.0), 26, Color.html("#ffcc00"), 3, Color.html("#9c460b")) _ui.add_panel_label(_panel, "TitleName", "游戏昵称", Vector2(201.0, 233.0), Vector2(108.0, 32.0), 26, Color.html("#ffcc00"), 3, Color.html("#9c460b")) _title_lv_label = _ui.add_panel_label(_panel, "TitleLv", "等级", Vector2(335.0, 233.0), Vector2(72.0, 32.0), 26, Color.html("#ffcc00"), 3, Color.html("#9c460b")) _ui.add_panel_label(_panel, "TitleHome", "家园", Vector2(413.0, 233.0), Vector2(64.0, 32.0), 26, Color.html("#ffcc00"), 3, Color.html("#9c460b")) var scroll := ScrollContainer.new() scroll.name = "FriendScroll" scroll.position = Vector2(102.0, 278.0) scroll.size = Vector2(446.0, 487.0) scroll.clip_contents = true _ui.configure_vertical_scroll(scroll) _panel.add_child(scroll) _list = Control.new() _list.name = "FriendList" _list.custom_minimum_size = Vector2(450.0, float(_page_size) * 70.0) _list.size = Vector2(450.0, float(_page_size) * 70.0) scroll.add_child(_list) _ui.add_texture_button(_panel, "FriendPrev", _texture_provider.common("btn_page_prev"), Vector2(364.0, 791.0), Vector2(46.0, 41.0), "", Callable(self, "_on_prev_pressed"), 20) _ui.add_texture_button(_panel, "FriendNext", _texture_provider.common("btn_page_next"), Vector2(481.0, 791.0), Vector2(46.0, 41.0), "", Callable(self, "_on_next_pressed"), 20) _page_label = _ui.add_panel_label(_panel, "FriendPage", "1/1", Vector2(409.0, 800.0), Vector2(72.0, 28.0), 24, Color.html("#753f24"), 0, Color.TRANSPARENT) _ui.add_texture_button(_panel, "FriendGold", _texture_provider.friend("friend_btn_caifu"), Vector2(94.0, 773.0), Vector2(72.0, 65.0), "", Callable(self, "_on_gold_pressed"), 20) _ui.add_texture_button(_panel, "FriendLv", _texture_provider.friend("friend_btn_lv"), Vector2(173.0, 760.0), Vector2(70.0, 80.0), "", Callable(self, "_on_lv_pressed"), 20) _ui.add_texture_button(_panel, "FriendPower", _texture_provider.friend("friend_btn_zhanli"), Vector2(253.0, 773.0), Vector2(66.0, 67.0), "", Callable(self, "_on_power_pressed"), 20) return _panel func list() -> Control: return _list func page_label() -> Label: return _page_label func title_lv_label() -> Label: return _title_lv_label func update_page() -> void: var list := _list if _friend_api == null or not _is_live_node(list): return _render_empty_rows(list) _update_page_label() var token := _token() if token.is_empty(): return var result: Dictionary = await _friend_api.fetch_list(token, _rank_type, _show_type, _page_index, _page_size) if not _is_current_node(list, _list): return if not result.get("ok", false): _show_toast("排行榜加载失败:%s" % result.get("game_status", result.get("status", 0))) return var body = result.get("body", {}) var data = body.get("data", {}) if body is Dictionary else {} if not data is Dictionary: return _page_max = maxi(int(ceil(float(int(data.get("total_count", 0))) / float(_page_size))), 1) _update_page_label() _clear_children(list) var rows = data.get("list", []) var row_index := 0 if rows is Array: for raw_row in rows: if raw_row is Dictionary: var row: Dictionary = raw_row.duplicate(true) if not row.has("ranking"): row["ranking"] = (_page_index - 1) * _page_size + row_index + 1 _add_row_to_list(list, _create_row(row), row_index) row_index += 1 for index in range(row_index, _page_size): _add_row_to_list(list, _create_row({}), index) func _on_tab_pressed(index: int) -> void: _rank_type = index _page_index = 1 var tab_bar := _panel.get_node_or_null("FriendTabs") if _panel != null else null _ui.update_tab_bar(tab_bar as Control, _rank_type) update_page() func _on_prev_pressed() -> void: if _page_index <= 1: return _page_index -= 1 update_page() func _on_next_pressed() -> void: if _page_index >= _page_max: return _page_index += 1 update_page() func _on_gold_pressed() -> void: _show_type = 0 if _title_lv_label != null: _title_lv_label.text = I18nScript.t("财富") _page_index = 1 update_page() func _on_lv_pressed() -> void: _show_type = 1 if _title_lv_label != null: _title_lv_label.text = I18nScript.t("等级") _page_index = 1 update_page() func _on_power_pressed() -> void: _show_toast("战力排行暂未开放") func _render_empty_rows(parent: Control) -> void: if not _clear_children_if_live(parent): return for index in range(_page_size): _add_row_to_list(parent, _create_row({}), index) func _update_page_label() -> void: if _is_live_node(_page_label): _page_label.text = "%s/%s" % [_page_index, _page_max] func _add_row_to_list(parent: Control, row: Control, index: int) -> void: if not _is_live_node(parent): return row.position = Vector2(0.0, float(index) * 70.0) parent.add_child(row) func _create_row(row: Dictionary) -> Control: var holder := Control.new() holder.name = "FriendRow%s" % int(row.get("user_id", 0)) holder.custom_minimum_size = Vector2(450.0, 70.0) holder.size = Vector2(450.0, 70.0) _ui.add_texture(holder, "Bg", _texture_provider.rank("friend_item"), Vector2(0.0, 8.0), Vector2(430.0, 56.0)) if row.is_empty(): return holder var rank := int(row.get("ranking", row.get("rank", 0))) if rank >= 1 and rank <= 3: _ui.add_texture(holder, "RankIcon", _texture_provider.rank("rank%s" % rank), Vector2(24.0, 15.0), Vector2(31.0, 40.0), TextureRect.STRETCH_KEEP_ASPECT_CENTERED) else: _ui.add_panel_label(holder, "Rank", str(rank), Vector2(19.0, 27.0), Vector2(39.0, 24.0), 24, Color.html("#753f24"), 0, Color.TRANSPARENT) _ui.add_panel_label(holder, "Name", str(row.get("nickname", row.get("name", ""))).left(8), Vector2(46.0, 27.0), Vector2(193.0, 24.0), 20, Color.html("#753f24"), 0, Color.TRANSPARENT) var value := int(row.get("level", row.get("lv", 0))) if _show_type == 1 else int(row.get("gold", 0)) _ui.add_panel_label(holder, "Value", NumberFormatterScript.compact(value), Vector2(200.0, 27.0), Vector2(113.0, 24.0), 20, Color.html("#753f24"), 0, Color.TRANSPARENT) _ui.add_texture_button(holder, "Home", _texture_provider.rank("btn_friend_home"), Vector2(308.0, 6.0), Vector2(63.0, 59.0), "", Callable(self, "_on_home_pressed").bind(row.duplicate(true)), 20) return holder func _on_home_pressed(row: Dictionary) -> void: await _visit_friend(row) func _token() -> String: return str(_call("token", [], "")) func _show_toast(message: String) -> void: _call("show_toast", [message]) func _visit_friend(row: Dictionary) -> void: var target_id := _target_user_id(row) if target_id <= 0: _show_toast("好友数据异常") return var own_id := int(_call("player_id", [], 0)) if own_id > 0 and own_id == target_id: _show_toast("这是你的农场,不用去啦!") return await _call_async("visit_friend_home", [row]) func _target_user_id(row: Dictionary) -> int: for key in ["uid", "user_id", "id"]: var value := int(row.get(key, 0)) if value > 0: return value return 0 func _call(name: String, args: Array = [], fallback: Variant = null) -> Variant: if _context != null and _context.has_method("call_action"): return _context.call_action(name, args, fallback) var callable: Callable = _callbacks.get(name, Callable()) if not callable.is_valid(): return fallback return callable.callv(args) func _call_async(name: String, args: Array = []) -> Variant: if _context != null and _context.has_method("call_action_async"): return await _context.call_action_async(name, args) var callable: Callable = _callbacks.get(name, Callable()) if not callable.is_valid(): return null return await callable.callv(args) func _clear_children(parent: Node) -> void: for child in parent.get_children(): parent.remove_child(child) child.queue_free() func _is_live_node(node: Variant) -> bool: return node != null and is_instance_valid(node) and node is Node func _is_current_node(candidate: Variant, current: Variant) -> bool: return _is_live_node(candidate) and _is_live_node(current) and candidate == current func _clear_children_if_live(parent: Variant) -> bool: if not _is_live_node(parent): return false _clear_children(parent as Node) return true