221 lines
8.7 KiB
GDScript
221 lines
8.7 KiB
GDScript
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()
|