2026-05-29 19:54:56 +08:00

279 lines
13 KiB
GDScript

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)))