class_name WarehousePanel extends RefCounted # 仓库面板: # - 负责果实、种子、道具三类库存展示,以及果实出售弹窗。 # - 数据源由 PlayerInventoryFacade/InventoryState 提供,本面板只负责当前分页和 UI 选择。 # - 出售成功后通过 callbacks 刷新仓库和玩家资源,避免直接改主场景字段。 const NumberFormatterScript := preload("res://scripts/core/number_formatter.gd") const UiMotionScript := preload("res://scripts/ui/common/ui_motion.gd") var _ui var _texture_provider var _owner: Node var _warehouse_api var _get_item_icon_texture: Callable var _context var _callbacks: Dictionary = {} var _panel_screen_size := Vector2(640.0, 960.0) var _capacity := 32 var _item_type_fruit := 2 var _item_type_seed := 1 var _item_type_props := 3 var _tab_index := 0 var _panel: Control var _list: GridContainer var _modal_layer: Control func setup(options: Dictionary) -> void: _ui = options.get("ui_builder") _texture_provider = options.get("texture_provider") _owner = options.get("owner") as Node _warehouse_api = options.get("warehouse_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() _panel_screen_size = options.get("panel_screen_size", _panel_screen_size) _capacity = int(options.get("capacity", _capacity)) _item_type_fruit = int(options.get("item_type_fruit", _item_type_fruit)) _item_type_seed = int(options.get("item_type_seed", _item_type_seed)) _item_type_props = int(options.get("item_type_props", _item_type_props)) _modal_layer = options.get("modal_layer") as Control func create(tab_index: int) -> Control: _tab_index = tab_index _panel = _ui.create_common_panel_shell("WareHousePanel", Vector2(540.0, 634.0), "panel_icon_warehouse", _texture_provider.atlas_path("common"), "panel_title_warehouse", _texture_provider.atlas_path("common")) _ui.add_content_panel_background(_panel, "InnerPage", Vector2(114.0, 310.0), Vector2(411.0, 415.0)) _ui.create_tab_bar(_panel, "WareHouseTabs", ["果实", "种子", "道具"], Vector2(114.0, 263.0), _tab_index, Callable(self, "_on_tab_pressed")) var scroll := ScrollContainer.new() scroll.name = "WareHouseScroll" scroll.position = Vector2(114.0, 313.0) scroll.size = Vector2(411.0, 408.0) scroll.clip_contents = true scroll.follow_focus = true _ui.configure_vertical_scroll(scroll) _panel.add_child(scroll) var holder := MarginContainer.new() holder.name = "WareHouseListHolder" holder.custom_minimum_size = Vector2(411.0, 0.0) holder.add_theme_constant_override("margin_left", 10) holder.add_theme_constant_override("margin_top", 10) holder.add_theme_constant_override("margin_right", 0) holder.add_theme_constant_override("margin_bottom", 10) scroll.add_child(holder) _list = GridContainer.new() _list.name = "WareHouseList" _list.columns = 4 _list.custom_minimum_size = Vector2(390.0, 0.0) _list.add_theme_constant_override("h_separation", 10) _list.add_theme_constant_override("v_separation", 10) holder.add_child(_list) render() return _panel func list() -> GridContainer: return _list func set_modal_layer(modal_layer: Control) -> void: _modal_layer = modal_layer func render() -> void: if _list == null: return _clear_children(_list) var items := _items_for_tab(_tab_index) items.sort_custom(func(left: Dictionary, right: Dictionary) -> bool: return int(left.get("item_id", 0)) < int(right.get("item_id", 0)) ) for item in items: _list.add_child(_create_item(item)) for _index in range(maxi(_capacity - items.size(), 0)): _list.add_child(_create_item({})) func _on_tab_pressed(index: int) -> void: _tab_index = index var tab_bar := _panel.get_node_or_null("WareHouseTabs") if _panel != null else null _ui.update_tab_bar(tab_bar as Control, _tab_index) render() func _items_for_tab(index: int) -> Array[Dictionary]: var wanted_type := _item_type_fruit if index == 1: wanted_type = _item_type_seed elif index == 2: wanted_type = _item_type_props var result: Array[Dictionary] = [] for item in _warehouse_items(): var config: Dictionary = item.get("config", {}) if int(config.get("type", item.get("type", 0))) == wanted_type: result.append(item.duplicate(true)) return result func _create_item(item: Dictionary) -> Control: var holder := Control.new() holder.name = "WareHouseItem%s" % int(item.get("item_id", 0)) holder.custom_minimum_size = Vector2(90.0, 90.0) holder.size = Vector2(90.0, 90.0) _ui.add_texture(holder, "ItemBg", _texture_provider.common("item_bg"), Vector2.ZERO, Vector2(90.0, 90.0)) if item.is_empty(): return holder var config: Dictionary = item.get("config", {}) _ui.add_texture(holder, "Icon", _item_icon_texture(int(config.get("icon", item.get("icon_id", item.get("item_id", 0))))), Vector2(8.0, 9.0), Vector2(74.0, 68.0), TextureRect.STRETCH_KEEP_ASPECT_CENTERED) _ui.add_nine_patch(holder, "LvBg", _texture_provider.common("blue_rect"), Vector2.ZERO, Vector2(48.0, 21.0), Vector4(0.0, 2.0, 5.0, 17.0)) _ui.add_panel_label(holder, "Lv", "Lv.%s" % int(config.get("lv", 1)), Vector2(3.0, 2.0), Vector2(46.0, 18.0), 16, Color.WHITE, 0, Color.TRANSPARENT, HORIZONTAL_ALIGNMENT_LEFT) _ui.add_panel_label(holder, "Num", "x%s" % int(item.get("num", item.get("number", 0))), Vector2(20.0, 62.0), Vector2(64.0, 24.0), 20, Color.html("#b96108"), 1, Color.html("#ffe59d"), HORIZONTAL_ALIGNMENT_RIGHT) var hit := Button.new() hit.name = "Hit" hit.position = Vector2.ZERO hit.size = holder.size hit.focus_mode = Control.FOCUS_NONE hit.text = "" UiMotionScript.apply_empty_button_style(hit) hit.pressed.connect(_open_sell_popup.bind(item.duplicate(true))) hit.button_down.connect(func() -> void: UiMotionScript.play_press_scale(_owner, holder)) hit.button_up.connect(func() -> void: UiMotionScript.play_release_scale(_owner, holder)) holder.add_child(hit) return holder func _open_sell_popup(item: Dictionary) -> void: var config: Dictionary = item.get("config", {}) if int(config.get("sellPrice", 0)) <= 0: _show_toast("该物品不能出售") return if _modal_layer == null: return var popup := Control.new() popup.name = "SellPopup" popup.size = _panel_screen_size popup.mouse_filter = Control.MOUSE_FILTER_STOP _modal_layer.add_child(popup) var mask := Button.new() mask.name = "SellMask" mask.set_anchors_preset(Control.PRESET_FULL_RECT) mask.focus_mode = Control.FOCUS_NONE mask.text = "" UiMotionScript.apply_empty_button_style(mask) popup.add_child(mask) _ui.add_nine_patch(popup, "SellBg", _texture_provider.common("c_panel2"), Vector2(88.0, 302.0), Vector2(464.0, 316.0), Vector4(136.0, 128.0, 4.0, 2.0)) _ui.add_texture(popup, "Trim1", _texture_provider.common("trimming1"), Vector2(127.0, 279.0), Vector2(70.0, 63.0)) _ui.add_texture(popup, "Trim2", _texture_provider.common("trimming2"), Vector2(441.0, 302.0), Vector2(35.0, 26.0)) var title: Texture2D = _texture_provider.common("panel_title_sell") _ui.add_texture(popup, "SellTitle", title, Vector2(320.0 - title.get_width() * 0.5, 263.0), title.get_size()) _ui.add_texture_button(popup, "SellClose", _texture_provider.common("btn_close"), Vector2(480.0, 281.0), Vector2(80.0, 74.0), "", func() -> void: popup.queue_free(), 20) _ui.add_texture(popup, "ItemBg", _texture_provider.common("item_bg"), Vector2(124.0, 348.0), Vector2(90.0, 90.0)) _ui.add_texture(popup, "Icon", _item_icon_texture(int(config.get("icon", item.get("item_id", 0)))), Vector2(132.0, 356.0), Vector2(74.0, 70.0), TextureRect.STRETCH_KEEP_ASPECT_CENTERED) _ui.add_panel_label(popup, "Name", str(config.get("name", "物品")), Vector2(226.0, 349.0), Vector2(210.0, 28.0), 24, Color.html("#290202"), 0, Color.TRANSPARENT, HORIZONTAL_ALIGNMENT_LEFT) _ui.add_nine_patch(popup, "PriceBg", _texture_provider.common("blue_rect"), Vector2(229.0, 374.0), Vector2(94.0, 21.0), Vector4(23.0, 9.0, 9.0, 2.0)) _ui.add_panel_label(popup, "PriceTitle", "单价", Vector2(257.0, 376.0), Vector2(44.0, 18.0), 18, Color.WHITE, 0, Color.TRANSPARENT) _ui.add_nine_patch(popup, "TotalBg", _texture_provider.common("blue_rect"), Vector2(349.0, 374.0), Vector2(94.0, 21.0), Vector4(23.0, 9.0, 9.0, 2.0)) _ui.add_panel_label(popup, "TotalTitle", "总计收入", Vector2(360.0, 376.0), Vector2(72.0, 18.0), 18, Color.WHITE, 0, Color.TRANSPARENT) _ui.add_texture(popup, "PriceGold", _texture_provider.common("sicon_gold"), Vector2(225.0, 401.0), Vector2(35.0, 35.0)) _ui.add_texture(popup, "TotalGold", _texture_provider.common("sicon_gold"), Vector2(345.0, 401.0), Vector2(35.0, 35.0)) var sell_price := int(config.get("sellPrice", 0)) _ui.add_panel_label(popup, "Price", str(sell_price), Vector2(262.0, 407.0), Vector2(70.0, 24.0), 20, Color.html("#fffbf2"), 2, Color.html("#836344"), HORIZONTAL_ALIGNMENT_LEFT) var total_label: Label = _ui.add_panel_label(popup, "TotalPrice", str(sell_price), Vector2(382.0, 407.0), Vector2(88.0, 24.0), 20, Color.html("#fffbf2"), 2, Color.html("#836344"), HORIZONTAL_ALIGNMENT_LEFT) _ui.add_nine_patch(popup, "Line", _texture_provider.common("line"), Vector2(129.0, 447.0), Vector2(367.0, 4.0), Vector4.ZERO) _ui.add_nine_patch(popup, "Input", _texture_provider.common("inp_mid"), Vector2(244.0, 471.0), Vector2(155.0, 44.0), Vector4(30.0, 20.0, 7.0, 3.0)) var count_label: Label = _ui.add_panel_label(popup, "Count", "1", Vector2(256.0, 475.0), Vector2(125.0, 34.0), 30, Color.WHITE, 0, Color.TRANSPARENT) var state := {"count": 1} _ui.add_texture_button(popup, "Sub", _texture_provider.common("btn_sub"), Vector2(168.0, 464.0), Vector2(49.0, 32.0), "", Callable(self, "_change_sell_count").bind(item.duplicate(true), state, count_label, total_label, -1), 20) _ui.add_texture_button(popup, "Add", _texture_provider.common("btn_add"), Vector2(424.0, 464.0), Vector2(49.0, 32.0), "", Callable(self, "_change_sell_count").bind(item.duplicate(true), state, count_label, total_label, 1), 20) _ui.add_texture_button(popup, "Sell", _texture_provider.common("c_btn3"), Vector2(237.0, 554.0), Vector2(166.0, 58.0), "卖出", Callable(self, "_sell_item").bind(item.duplicate(true), state, popup), 30) popup.pivot_offset = _panel_screen_size * 0.5 popup.scale = Vector2(0.9, 0.9) popup.modulate = Color(1.0, 1.0, 1.0, 0.0) if _owner != null: var tween := _owner.create_tween().set_parallel(true) tween.tween_property(popup, "scale", Vector2.ONE, 0.18).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) tween.tween_property(popup, "modulate:a", 1.0, 0.12) func _change_sell_count(item: Dictionary, state: Dictionary, count_label: Label, total_label: Label, delta: int) -> void: var current := int(state.get("count", 1)) var max_count := int(item.get("num", item.get("number", 1))) current = clampi(current + delta, 1, maxi(max_count, 1)) state["count"] = current count_label.text = str(current) var config: Dictionary = item.get("config", {}) total_label.text = str(int(config.get("sellPrice", 0)) * current) func _sell_item(item: Dictionary, state: Dictionary, popup: Control) -> void: if _is_busy(): return var token := _token() if token.is_empty(): _show_toast("登录信息失效") return var count := int(state.get("count", 1)) var config: Dictionary = item.get("config", {}) var sell_price := int(config.get("sellPrice", 0)) if sell_price <= 0: _show_toast("该物品不能出售") return _set_busy(true) var result: Dictionary = await _warehouse_api.sell(token, int(item.get("item_id", config.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("add_player_gold", [sell_price * count]) _call("decrease_warehouse_item", [int(item.get("item_id", config.get("id", 0))), count]) _call("render_player") render() if popup != null: popup.queue_free() _show_toast("出售成功,获得%s金豆" % (sell_price * count)) await _call_async("refresh_store_house") 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 _warehouse_items() -> Array[Dictionary]: var result = _call("warehouse_items", [], []) var items: Array[Dictionary] = [] if result is Array: for item in result: if item is Dictionary: items.append(item) return items 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()