class_name MarketPanel extends PanelModule const NumberFormatterScript := preload("res://scripts/core/number_formatter.gd") const UiMotionScript := preload("res://scripts/ui/common/ui_motion.gd") const MARKET_ICON_PATH := "res://assets/replacement/menu2/market.png" const PANEL_SIZE := Vector2(600.0, 760.0) const PANEL_POS := Vector2(20.0, 100.0) const CONTENT_POS := Vector2(58.0, 238.0) const CONTENT_SIZE := Vector2(524.0, 548.0) const ROW_SIZE := Vector2(500.0, 104.0) var _deal_api var _panel: Control var _list: VBoxContainer var _status_label: Label var _trade_rows: Array[Dictionary] = [] var _crop_output_item_ids: Array[int] = [] func setup(options: Dictionary) -> void: super.setup(options) _deal_api = options.get("deal_api") if _game_config != null and _game_config.has_method("get_crop_output_item_ids"): _crop_output_item_ids = _game_config.get_crop_output_item_ids() func open() -> void: _close_modal(false) _panel = _create_panel() _open_modal(_panel) await _refresh_prices() func _create_panel() -> Control: var root: Control = _ui.create_modal_root("MarketPanel") as Control _ui.add_panel_shell_background(root, "PanelBg", PANEL_POS, PANEL_SIZE) var icon: Texture2D = _texture_provider.standalone(MARKET_ICON_PATH, _texture_provider.common("item_bg")) _ui.add_texture(root, "PanelIcon", icon, Vector2(68.0, 102.0), Vector2(110.0, 110.0), TextureRect.STRETCH_KEEP_ASPECT_CENTERED) _ui.add_panel_label(root, "PanelTitle", "Market", Vector2(165.0, 104.0), Vector2(315.0, 74.0), 54, Color.html("#fff100"), 5, Color.html("#9a3c09")) _ui.add_panel_label(root, "PanelSubTitle", "Live Fruit Prices", Vector2(177.0, 174.0), Vector2(288.0, 34.0), 25, Color.html("#fff6ca"), 3, Color.html("#8d4318")) _ui.add_content_panel_background(root, "InnerPage", CONTENT_POS, CONTENT_SIZE) _ui.add_texture_button(root, "PanelClose", _texture_provider.common("c_btn1"), Vector2(220.0, 797.0), Vector2(200.0, 70.0), "离开", Callable(self, "_close_modal"), 36) _status_label = _ui.add_panel_label(root, "Status", "Loading live prices...", Vector2(92.0, 260.0), Vector2(456.0, 36.0), 24, Color.html("#764d24"), 0, Color.TRANSPARENT) var scroll := ScrollContainer.new() scroll.name = "MarketScroll" scroll.position = CONTENT_POS + Vector2(11.0, 14.0) scroll.size = CONTENT_SIZE - Vector2(22.0, 28.0) scroll.clip_contents = true scroll.follow_focus = true scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_SHOW_NEVER root.add_child(scroll) var holder := MarginContainer.new() holder.name = "MarketListHolder" holder.custom_minimum_size = Vector2(502.0, 0.0) holder.add_theme_constant_override("margin_left", 4) holder.add_theme_constant_override("margin_top", 6) holder.add_theme_constant_override("margin_right", 4) holder.add_theme_constant_override("margin_bottom", 12) scroll.add_child(holder) _list = VBoxContainer.new() _list.name = "MarketList" _list.custom_minimum_size = Vector2(500.0, 0.0) _list.add_theme_constant_override("separation", 10) holder.add_child(_list) _render_loading() return root func _refresh_prices() -> void: var token := _token() var result: Dictionary = {} if _deal_api != null and not token.is_empty(): result = await _deal_api.product_list(token) if result.get("ok", false): _trade_rows = _normalize_trade_rows(_response_data(result, [])) else: _trade_rows = _normalize_trade_rows(_local_trade_rows()) if _status_label != null: _status_label.text = "Live prices failed; showing start prices" if not token.is_empty() and not result.is_empty(): _show_toast(_response_status_text(result, "实时价格加载失败")) _render() func _render_loading() -> void: if _list == null: return _clear_children(_list) func _render() -> void: if _list == null: return _clear_children(_list) if _status_label != null: _status_label.visible = _trade_rows.is_empty() for row in _trade_rows: _list.add_child(_create_row(row)) func _create_row(row: Dictionary) -> Control: var holder := Control.new() holder.name = "MarketItem%s" % int(row.get("id", 0)) holder.custom_minimum_size = ROW_SIZE holder.size = ROW_SIZE holder.mouse_filter = Control.MOUSE_FILTER_IGNORE _ui.add_nine_patch(holder, "Bg", _texture_provider.common("tipbg"), Vector2.ZERO, ROW_SIZE, Vector4(45.0, 21.0, 57.0, 69.0)) var item_id := int(row.get("item_id", 0)) var icon_id: int = _game_config.get_item_icon_id(item_id) if _game_config != null else item_id _ui.add_texture(holder, "ItemBg", _texture_provider.common("item_bg"), Vector2(14.0, 13.0), Vector2(78.0, 78.0)) _ui.add_texture(holder, "Icon", _get_item_icon_texture(icon_id), Vector2(20.0, 18.0), Vector2(66.0, 62.0), TextureRect.STRETCH_KEEP_ASPECT_CENTERED) _ui.add_panel_label(holder, "Name", str(row.get("name", "果实")), Vector2(104.0, 13.0), Vector2(145.0, 26.0), 24, Color.html("#753f24"), 0, Color.TRANSPARENT, HORIZONTAL_ALIGNMENT_LEFT) _ui.add_panel_label(holder, "Owned", "Owned:%s" % _owned_amount(item_id), Vector2(104.0, 42.0), Vector2(145.0, 24.0), 18, Color.html("#8b5a2b"), 0, Color.TRANSPARENT, HORIZONTAL_ALIGNMENT_LEFT) _ui.add_panel_label(holder, "Deals", "Deals:%s" % int(row.get("success", 0)), Vector2(104.0, 66.0), Vector2(145.0, 24.0), 18, Color.html("#8b5a2b"), 0, Color.TRANSPARENT, HORIZONTAL_ALIGNMENT_LEFT) _ui.add_texture(holder, "GoldIcon", _texture_provider.common("sicon_gold"), Vector2(252.0, 31.0), Vector2(36.0, 36.0), TextureRect.STRETCH_KEEP_ASPECT_CENTERED) _ui.add_panel_label(holder, "Price", NumberFormatterScript.compact(int(row.get("price", 0))), Vector2(288.0, 30.0), Vector2(76.0, 36.0), 24, Color.html("#fffef7"), 2, Color.html("#685036"), HORIZONTAL_ALIGNMENT_LEFT) _ui.add_panel_label(holder, "Range", "High:%s Low:%s" % [int(row.get("high", 0)), int(row.get("low", 0))], Vector2(252.0, 65.0), Vector2(122.0, 24.0), 17, Color.html("#8b5a2b"), 0, Color.TRANSPARENT, HORIZONTAL_ALIGNMENT_LEFT) _ui.add_texture_button(holder, "Buy", _texture_provider.common("c_btn2"), Vector2(376.0, 17.0), Vector2(94.0, 40.0), "Buy", Callable(self, "_open_quantity_popup").bind(row.duplicate(true), "buy"), 21) _ui.add_texture_button(holder, "Sell", _texture_provider.common("c_btn2"), Vector2(376.0, 60.0), Vector2(94.0, 40.0), "Sell", Callable(self, "_open_quantity_popup").bind(row.duplicate(true), "sell"), 21) return holder func _open_quantity_popup(row: Dictionary, action: String) -> void: if _panel == null: return var max_count := _max_count(row, action) if max_count <= 0: _show_toast("金豆不足" if action == "buy" else "没有可售卖的果实") return var popup := Control.new() popup.name = "MarketQuantityPopup" popup.size = _panel_screen_size popup.mouse_filter = Control.MOUSE_FILTER_STOP _panel.add_child(popup) var mask := Button.new() mask.name = "QuantityMask" mask.set_anchors_preset(Control.PRESET_FULL_RECT) mask.focus_mode = Control.FOCUS_NONE mask.text = "" UiMotionScript.apply_empty_button_style(mask) mask.pressed.connect(func() -> void: popup.queue_free()) popup.add_child(mask) var verb := "Buy" if action == "buy" else "Sell" var price := int(row.get("price", 0)) _ui.add_nine_patch(popup, "PopupBg", _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_button(popup, "Close", _texture_provider.common("btn_close"), Vector2(480.0, 281.0), Vector2(80.0, 74.0), "", func() -> void: popup.queue_free(), 20) _ui.add_panel_label(popup, "Title", "%s %s" % [verb, str(row.get("name", "Fruit"))], Vector2(146.0, 323.0), Vector2(348.0, 40.0), 29, Color.html("#fff100"), 4, Color.html("#8d4318")) _ui.add_panel_label(popup, "PriceTitle", "Price:%s" % price, Vector2(166.0, 374.0), Vector2(120.0, 30.0), 22, Color.html("#290202"), 0, Color.TRANSPARENT, HORIZONTAL_ALIGNMENT_LEFT) _ui.add_panel_label(popup, "MaxCount", "Max:%s" % NumberFormatterScript.compact(max_count), Vector2(322.0, 374.0), Vector2(142.0, 30.0), 22, Color.html("#290202"), 0, Color.TRANSPARENT, HORIZONTAL_ALIGNMENT_LEFT) _ui.add_nine_patch(popup, "Line", _texture_provider.common("line"), Vector2(129.0, 424.0), Vector2(367.0, 4.0), Vector4.ZERO) _ui.add_nine_patch(popup, "Input", _texture_provider.common("inp_mid"), Vector2(244.0, 454.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, 458.0), Vector2(125.0, 34.0), 30, Color.WHITE, 0, Color.TRANSPARENT) var total_label: Label = _ui.add_panel_label(popup, "Total", "Total:%s" % price, Vector2(218.0, 508.0), Vector2(205.0, 32.0), 24, Color.html("#fffef7"), 2, Color.html("#685036")) var state := {"count": 1, "max": max_count} _ui.add_texture_button(popup, "Sub", _texture_provider.common("btn_sub"), Vector2(168.0, 447.0), Vector2(49.0, 32.0), "", Callable(self, "_change_count").bind(row.duplicate(true), state, count_label, total_label, -1), 20) _ui.add_texture_button(popup, "Add", _texture_provider.common("btn_add"), Vector2(424.0, 447.0), Vector2(49.0, 32.0), "", Callable(self, "_change_count").bind(row.duplicate(true), state, count_label, total_label, 1), 20) _ui.add_texture_button(popup, "Confirm", _texture_provider.common("c_btn3"), Vector2(237.0, 554.0), Vector2(166.0, 58.0), verb, Callable(self, "_submit_order").bind(row.duplicate(true), action, 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_count(row: Dictionary, state: Dictionary, count_label: Label, total_label: Label, delta: int) -> void: var count := clampi(int(state.get("count", 1)) + delta, 1, int(state.get("max", 1))) state["count"] = count count_label.text = str(count) total_label.text = "Total:%s" % (int(row.get("price", 0)) * count) func _submit_order(row: Dictionary, action: String, state: Dictionary, popup: Control) -> void: if _is_busy(): return if _deal_api == null: _show_toast("市场接口未配置") return var token := _token() if token.is_empty(): _show_toast("登录信息失效") return var count := int(state.get("count", 1)) var price := int(row.get("price", 0)) var item_id := int(row.get("item_id", 0)) if action == "buy" and price * count > int(_player_data().get("gold", 0)): _show_toast("金豆不足") return if action == "sell" and count > _owned_amount(item_id): _show_toast("果实数量不足") return _set_busy(true) var result: Dictionary = await _deal_api.order(token, int(row.get("id", 0)), price, count, action) _set_busy(false) if not result.get("ok", false): _show_toast(_response_status_text(result, "交易失败")) return if action == "buy": _call("decrease_player_currency", [price * count, 0]) else: _decrease_warehouse_item(item_id, count) _render_player() if popup != null and is_instance_valid(popup): popup.queue_free() _show_toast("%s委托已提交" % ("买入" if action == "buy" else "卖出")) await _refresh_store_house() await _refresh_prices() func _normalize_trade_rows(raw: Variant) -> Array[Dictionary]: var source: Array = [] if raw is Array: source = raw elif raw is Dictionary: source = raw.values() var rows: Array[Dictionary] = [] for raw_row in source: if not raw_row is Dictionary: continue var row: Dictionary = raw_row var item_id := int(row.get("itemid", row.get("item_id", 0))) if item_id <= 0: continue if not _crop_output_item_ids.is_empty() and not _crop_output_item_ids.has(item_id): continue var name := str(row.get("itemname", "")) if name.is_empty() and _game_config != null: name = _game_config.get_item_name(item_id) rows.append({ "id": int(row.get("id", 0)), "item_id": item_id, "name": name, "price": _row_price(row, item_id), "high": int(row.get("HighestPrice", row.get("high", 0))), "low": int(row.get("LowestPrice", row.get("low", 0))), "success": int(row.get("successNum", row.get("success", 0))), }) rows.sort_custom(func(left: Dictionary, right: Dictionary) -> bool: return int(left.get("id", 0)) < int(right.get("id", 0)) ) return rows func _local_trade_rows() -> Array[Dictionary]: if _game_config == null or not _game_config.has_method("get_trade_config_list"): return [] return _game_config.get_trade_config_list() func _row_price(row: Dictionary, item_id: int) -> int: for key in ["nowOpen", "buyprice", "price", "startprice"]: var price := int(row.get(key, 0)) if price > 0: return price return _game_config.get_item_sell_price(item_id) if _game_config != null else 0 func _max_count(row: Dictionary, action: String) -> int: var price := maxi(int(row.get("price", 0)), 1) if action == "buy": return maxi(int(float(int(_player_data().get("gold", 0))) / float(price)), 0) return _owned_amount(int(row.get("item_id", 0)))