class_name ShopPanel extends RefCounted # 商店面板: # - 负责普通道具、神秘商店、背景三类商品展示和购买入口。 # - 商品配置来自 GameConfigCatalog,购买后通过 callbacks 扣货币/刷新仓库/刷新顶部资源。 # - 该面板只持有自己的列表节点,不直接访问 game_scene 的土地或其它弹窗状态。 const NumberFormatterScript := preload("res://scripts/core/number_formatter.gd") var _ui var _texture_provider var _game_config var _shop_api var _get_item_icon_texture: Callable var _context var _callbacks: Dictionary = {} var _tab_index := 0 var _panel: Control var _list: GridContainer func setup(options: Dictionary) -> void: _ui = options.get("ui_builder") _texture_provider = options.get("texture_provider") _game_config = options.get("game_config") _shop_api = options.get("shop_api") _get_item_icon_texture = options.get("get_item_icon_texture", Callable()) _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() func create(tab_index: int) -> Control: _tab_index = tab_index _panel = _ui.create_common_panel_shell("ShopPanel", Vector2(620.0, 754.0), "panel_icon_shop", _texture_provider.atlas_path("common"), "panel_title_shop", _texture_provider.atlas_path("common")) _ui.add_content_panel_background(_panel, "InnerPage", Vector2(67.0, 248.0), Vector2(510.0, 538.0)) _ui.create_tab_bar(_panel, "ShopTabs", ["普通道具", "神秘商店", "背景"], Vector2(89.0, 201.0), _tab_index, Callable(self, "_on_tab_pressed")) var scroll := ScrollContainer.new() scroll.name = "ShopScroll" scroll.position = Vector2(71.0, 251.0) scroll.size = Vector2(500.0, 531.0) scroll.clip_contents = true scroll.follow_focus = true _ui.configure_vertical_scroll(scroll) _panel.add_child(scroll) var holder := MarginContainer.new() holder.name = "ShopListHolder" holder.custom_minimum_size = Vector2(500.0, 0.0) holder.add_theme_constant_override("margin_left", 16) holder.add_theme_constant_override("margin_top", 8) holder.add_theme_constant_override("margin_right", 4) holder.add_theme_constant_override("margin_bottom", 8) scroll.add_child(holder) _list = GridContainer.new() _list.name = "ShopList" _list.columns = 2 _list.custom_minimum_size = Vector2(464.0, 0.0) _list.add_theme_constant_override("h_separation", 16) _list.add_theme_constant_override("v_separation", 12) holder.add_child(_list) render() return _panel func list() -> GridContainer: return _list func render() -> void: if _list == null: return _clear_children(_list) var shop_type := 1 if _tab_index == 1: shop_type = 2 elif _tab_index == 2: shop_type = 3 var items: Array[Dictionary] = _game_config.get_shop_config_by_type(shop_type) for item in items: _list.add_child(_create_item_card(item)) func _on_tab_pressed(index: int) -> void: _tab_index = index var tab_bar := _panel.get_node_or_null("ShopTabs") if _panel != null else null _ui.update_tab_bar(tab_bar as Control, _tab_index) render() func _create_item_card(shop_item: Dictionary) -> Control: var holder := Control.new() holder.name = "ShopItem%s" % int(shop_item.get("id", 0)) holder.custom_minimum_size = Vector2(224.0, 163.0) holder.size = Vector2(224.0, 163.0) _ui.add_nine_patch(holder, "Bg", _texture_provider.common("tipbg"), Vector2.ZERO, holder.size, Vector4(45.0, 21.0, 57.0, 69.0)) _ui.add_nine_patch(holder, "PriceBg", _texture_provider.common("blue_rect"), Vector2(116.0, 7.0), Vector2(89.0, 21.0), Vector4(22.0, 10.0, 13.0, 1.0)) _ui.add_panel_label(holder, "PriceTitle", "单价", Vector2(146.0, 10.0), Vector2(48.0, 18.0), 16, Color.WHITE, 0, Color.TRANSPARENT) var item_config: Dictionary = shop_item.get("item", {}) _ui.add_panel_label(holder, "Name", str(item_config.get("name", "物品")), Vector2(13.0, 121.0), Vector2(193.0, 28.0), 24, Color.html("#753f24"), 0, Color.TRANSPARENT) var group_icon := Control.new() group_icon.name = "IconGroup" group_icon.position = Vector2(14.0, 22.0) group_icon.size = Vector2(86.0, 91.0) holder.add_child(group_icon) _ui.add_texture(group_icon, "ItemBg", _texture_provider.common("item_bg"), Vector2.ZERO, Vector2(90.0, 90.0)) _ui.add_texture(group_icon, "Icon", _item_icon_texture(int(item_config.get("icon", shop_item.get("item_id", 0)))), Vector2(8.0, 8.0), Vector2(74.0, 70.0), TextureRect.STRETCH_KEEP_ASPECT_CENTERED) _ui.add_nine_patch(group_icon, "LvBg", _texture_provider.common("blue_rect"), Vector2(-1.0, -1.0), Vector2(52.0, 21.0), Vector4(0.0, 2.0, 5.0, 17.0)) var level_required := int(shop_item.get("playerLevel", shop_item.get("player_level", 1))) _ui.add_panel_label(group_icon, "Lv", "Lv.%s" % level_required, Vector2(6.0, 1.0), Vector2(50.0, 20.0), 16, Color.WHITE, 0, Color.TRANSPARENT, HORIZONTAL_ALIGNMENT_LEFT) var is_locked := int(_player_data().get("level", 1)) < level_required var lock: TextureRect = _ui.add_texture(group_icon, "Lock", _texture_provider.common("img_lock"), Vector2(53.0, 49.0), Vector2(35.0, 40.0)) lock.visible = is_locked var price_icon_key := "sicon_gold" if int(shop_item.get("gold", 0)) > 0 else "sicon_diamond" _ui.add_texture(holder, "PriceIcon", _texture_provider.common(price_icon_key), Vector2(125.0, 42.0), Vector2(35.0, 35.0), TextureRect.STRETCH_KEEP_ASPECT_CENTERED) var price := int(shop_item.get("gold", 0)) if int(shop_item.get("gold", 0)) > 0 else int(shop_item.get("diamond", shop_item.get("gem", 0))) _ui.add_panel_label(holder, "Price", NumberFormatterScript.compact(price), Vector2(150.0, 43.0), Vector2(68.0, 28.0), 22, Color.html("#fffef7"), 2, Color.html("#685036"), HORIZONTAL_ALIGNMENT_LEFT) var buy_button: Control = _ui.add_texture_button(holder, "Buy", _texture_provider.common("c_btn2"), Vector2(123.0, 70.0), Vector2(88.0, 44.0), "购买", Callable(self, "_buy_item").bind(shop_item.duplicate(true)), 22) if is_locked: buy_button.modulate = Color(0.72, 0.72, 0.72, 1.0) var hit := buy_button.get_node_or_null("Hit") as Button if hit != null: hit.disabled = true return holder func _buy_item(shop_item: Dictionary) -> void: if _is_busy(): return var token := _token() if token.is_empty(): _show_toast("登录信息失效") return var player_data := _player_data() var level_required := int(shop_item.get("playerLevel", shop_item.get("player_level", 1))) if int(player_data.get("level", 1)) < level_required: _show_toast("购买失败:等级不足") return var gold_cost := int(shop_item.get("gold", 0)) var gem_cost := int(shop_item.get("diamond", shop_item.get("gem", 0))) if gold_cost > int(player_data.get("gold", 0)): _show_toast("购买失败:金豆不足") return if gem_cost > int(player_data.get("gem", 0)): _show_toast("购买失败:资源不足,请充值!") return _set_busy(true) var count := 100 if int(shop_item.get("id", 0)) == 99 else 1 var result: Dictionary = await _shop_api.buy(token, int(shop_item.get("id", 0)), count) _set_busy(false) if not result.get("ok", false): _show_toast("购买失败:%s" % result.get("game_status", result.get("status", 0))) return _call("decrease_player_currency", [gold_cost, gem_cost]) _call("render_player") _show_toast("购买成功") await _call_async("refresh_store_house") render() func _item_icon_texture(icon_id: int) -> Texture2D: if _texture_provider != null and _texture_provider.has_method("item_icon"): return _texture_provider.item_icon(icon_id) if _get_item_icon_texture.is_valid(): return _get_item_icon_texture.call(icon_id) as Texture2D return _texture_provider.common("item_bg") func _player_data() -> Dictionary: var result = _call("player_data", [], {}) return result if result is Dictionary else {} func _token() -> String: return str(_call("token", [], "")) func _is_busy() -> bool: return bool(_call("is_operation_in_progress", [], false)) func _set_busy(value: bool) -> void: _call("set_operation_in_progress", [value]) func _show_toast(message: String) -> void: _call("show_toast", [message]) 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()