279 lines
13 KiB
GDScript
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)))
|