forked from xiangwang25/whale-town-front-v2
1212 lines
46 KiB
GDScript
1212 lines
46 KiB
GDScript
class_name MallPanel
|
||
extends Control
|
||
|
||
# ============================================================================
|
||
# MallPanel.gd - 鲸鱼商城页面
|
||
# ============================================================================
|
||
# 面板式商城 UI:分类、商品槽位、详情、购买确认和提示反馈。
|
||
# ============================================================================
|
||
|
||
const MOVEMENT_ACTIONS: Array[String] = ["move_left", "move_right", "move_up", "move_down"]
|
||
const PANEL_SIZE: Vector2 = Vector2(1640, 1160)
|
||
const MIN_MARGIN: Vector2 = Vector2(24, 24)
|
||
const TEXT_COLOR: Color = Color(0.102, 0.224, 0.376)
|
||
const MUTED_COLOR: Color = Color(0.480, 0.569, 0.678)
|
||
const ACCENT_COLOR: Color = Color(0.086, 0.608, 0.922)
|
||
const WARNING_COLOR: Color = Color(0.906, 0.392, 0.310)
|
||
const SOFT_SURFACE: Color = Color(0.945, 0.977, 1.0, 0.72)
|
||
|
||
const MALL_BADGE_TEXTURE: Texture2D = preload("res://assets/ui/mall/branding/branding_mall_badge.png")
|
||
const COIN_TEXTURE: Texture2D = preload("res://assets/ui/mall/branding/branding_whale_coin.png")
|
||
const CATALOG_SCRIPT: Script = preload("res://Config/MallCatalog.gd")
|
||
const ITEM_CARD_SCRIPT: Script = preload("res://scenes/ui/mall/MallItemCard.gd")
|
||
const NetworkConfig = preload("res://_Core/utils/NetworkConfig.gd")
|
||
|
||
var _overlay: ColorRect
|
||
var _panel: Control
|
||
var _categoryList: VBoxContainer
|
||
var _grid: GridContainer
|
||
var _detailIcon: TextureRect
|
||
var _detailName: Label
|
||
var _detailDescription: Label
|
||
var _detailStatus: Label
|
||
var _detailPrice: Label
|
||
var _tagRow: HBoxContainer
|
||
var _purchaseButton: Button
|
||
var _previewButton: Button
|
||
var _toast: Control
|
||
var _toastLabel: Label
|
||
var _confirmDialog: Control
|
||
var _confirmLabel: Label
|
||
var _currencyLabel: Label
|
||
var _purchaseRequest: HTTPRequest
|
||
var _catalogRequest: HTTPRequest
|
||
|
||
var _categoryButtons: Dictionary = {}
|
||
var _cards: Array[Button] = []
|
||
var _currentCategory: String = "recommended"
|
||
var _selectedItem: Dictionary = {}
|
||
var _catalogItems: Array[Dictionary] = []
|
||
var _balance: int = 0
|
||
var _walletLoaded: bool = false
|
||
var _walletLoading: bool = false
|
||
var _walletError: String = ""
|
||
var _catalogLoaded: bool = false
|
||
var _catalogLoading: bool = false
|
||
var _catalogError: String = ""
|
||
var _catalogRequestAccountGeneration: int = -1
|
||
var _purchaseRequestAccountGeneration: int = -1
|
||
var _accountGeneration: int = -1
|
||
var _isOpen: bool = false
|
||
var _transitionTween: Tween
|
||
var _toastTween: Tween
|
||
|
||
func _ready() -> void:
|
||
visible = false
|
||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
set_process(false)
|
||
_build_ui()
|
||
_connect_auth_state()
|
||
_render_categories()
|
||
_select_category(_currentCategory)
|
||
|
||
func _exit_tree() -> void:
|
||
var authManager := get_node_or_null("/root/AuthManager")
|
||
if authManager != null and authManager.has_signal("auth_state_changed"):
|
||
var callback := Callable(self, "_on_auth_state_changed")
|
||
if authManager.is_connected("auth_state_changed", callback):
|
||
authManager.disconnect("auth_state_changed", callback)
|
||
|
||
func _notification(what: int) -> void:
|
||
if what == NOTIFICATION_RESIZED and is_instance_valid(_panel):
|
||
_position_panel()
|
||
|
||
func _input(event: InputEvent) -> void:
|
||
if not _isOpen:
|
||
return
|
||
if event is InputEventKey:
|
||
var keyEvent := event as InputEventKey
|
||
if keyEvent.pressed and not keyEvent.echo and keyEvent.keycode == KEY_ESCAPE:
|
||
hide_panel()
|
||
get_viewport().set_input_as_handled()
|
||
|
||
func _process(_delta: float) -> void:
|
||
if _isOpen:
|
||
_release_movement_actions()
|
||
|
||
func show_panel() -> void:
|
||
if _isOpen:
|
||
return
|
||
_isOpen = true
|
||
visible = true
|
||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||
set_process(true)
|
||
_release_movement_input_state()
|
||
_prepare_wallet_state()
|
||
_prepare_catalog_state()
|
||
_fetch_catalog()
|
||
_emit_event(EventNames.MALL_OPENED, {})
|
||
_animate_panel(true)
|
||
|
||
func hide_panel() -> void:
|
||
if not _isOpen:
|
||
return
|
||
_isOpen = false
|
||
_release_movement_input_state()
|
||
_hide_confirm_dialog()
|
||
_emit_event(EventNames.MALL_CLOSED, {})
|
||
_animate_panel(false)
|
||
|
||
func is_panel_open() -> bool:
|
||
return _isOpen
|
||
|
||
func _build_ui() -> void:
|
||
_overlay = ColorRect.new()
|
||
_overlay.name = "mallDimOverlay"
|
||
_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
_overlay.color = Color(0.026, 0.052, 0.092, 0.66)
|
||
_overlay.mouse_filter = Control.MOUSE_FILTER_STOP
|
||
_overlay.gui_input.connect(_on_overlay_gui_input)
|
||
add_child(_overlay)
|
||
|
||
_panel = Control.new()
|
||
_panel.name = "mallPanel"
|
||
_panel.custom_minimum_size = PANEL_SIZE
|
||
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||
add_child(_panel)
|
||
_position_panel()
|
||
|
||
var frame := PanelContainer.new()
|
||
frame.name = "mainFrame"
|
||
frame.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
frame.add_theme_stylebox_override("panel", _create_panel_style(Color(0.972, 0.992, 1.0, 1.0), 34, true))
|
||
frame.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_panel.add_child(frame)
|
||
|
||
var contentMargin := MarginContainer.new()
|
||
contentMargin.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
contentMargin.add_theme_constant_override("margin_left", 38)
|
||
contentMargin.add_theme_constant_override("margin_top", 30)
|
||
contentMargin.add_theme_constant_override("margin_right", 38)
|
||
contentMargin.add_theme_constant_override("margin_bottom", 34)
|
||
_panel.add_child(contentMargin)
|
||
|
||
var root := VBoxContainer.new()
|
||
root.add_theme_constant_override("separation", 18)
|
||
contentMargin.add_child(root)
|
||
root.add_child(_build_header())
|
||
root.add_child(_build_body())
|
||
root.add_child(_build_bottom_bar())
|
||
|
||
_build_toast()
|
||
_build_confirm_dialog()
|
||
_build_purchase_request()
|
||
_build_catalog_request()
|
||
_update_panel_alpha(0.0, 0.0)
|
||
|
||
func _build_header() -> Control:
|
||
var header := PanelContainer.new()
|
||
header.custom_minimum_size = Vector2(0, 108)
|
||
header.add_theme_stylebox_override("panel", _create_header_style())
|
||
|
||
var row := HBoxContainer.new()
|
||
row.custom_minimum_size = Vector2(0, 96)
|
||
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
row.add_theme_constant_override("separation", 18)
|
||
var margin := MarginContainer.new()
|
||
margin.add_theme_constant_override("margin_left", 28)
|
||
margin.add_theme_constant_override("margin_top", 10)
|
||
margin.add_theme_constant_override("margin_right", 22)
|
||
margin.add_theme_constant_override("margin_bottom", 10)
|
||
header.add_child(margin)
|
||
margin.add_child(row)
|
||
|
||
var badge := TextureRect.new()
|
||
badge.texture = MALL_BADGE_TEXTURE
|
||
badge.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||
badge.custom_minimum_size = Vector2(92, 92)
|
||
badge.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
||
badge.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
row.add_child(badge)
|
||
|
||
var titleBox := VBoxContainer.new()
|
||
titleBox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
titleBox.add_theme_constant_override("separation", 2)
|
||
row.add_child(titleBox)
|
||
|
||
var title := Label.new()
|
||
title.text = "鲸鱼商城"
|
||
title.add_theme_font_size_override("font_size", 38)
|
||
title.add_theme_color_override("font_color", Color.WHITE)
|
||
title.add_theme_color_override("font_shadow_color", Color(0.0, 0.145, 0.318, 0.72))
|
||
title.add_theme_constant_override("shadow_offset_x", 0)
|
||
title.add_theme_constant_override("shadow_offset_y", 3)
|
||
titleBox.add_child(title)
|
||
|
||
var subtitle := Label.new()
|
||
subtitle.text = "当前开放角色皮肤,其他商品分类陆续筹备中"
|
||
subtitle.add_theme_font_size_override("font_size", 17)
|
||
subtitle.add_theme_color_override("font_color", Color(0.847, 0.949, 1.0, 0.94))
|
||
titleBox.add_child(subtitle)
|
||
|
||
row.add_child(_build_currency_bar())
|
||
row.add_child(_build_close_button())
|
||
return header
|
||
|
||
func _build_currency_bar() -> Control:
|
||
var holder := PanelContainer.new()
|
||
holder.custom_minimum_size = Vector2(276, 76)
|
||
holder.add_theme_stylebox_override("panel", _create_panel_style(Color(0.038, 0.254, 0.505, 0.92), 34, false))
|
||
|
||
var row := HBoxContainer.new()
|
||
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
row.add_theme_constant_override("separation", 8)
|
||
var margin := MarginContainer.new()
|
||
margin.add_theme_constant_override("margin_left", 18)
|
||
margin.add_theme_constant_override("margin_top", 8)
|
||
margin.add_theme_constant_override("margin_right", 18)
|
||
margin.add_theme_constant_override("margin_bottom", 8)
|
||
holder.add_child(margin)
|
||
margin.add_child(row)
|
||
|
||
var coin := TextureRect.new()
|
||
coin.texture = COIN_TEXTURE
|
||
coin.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||
coin.custom_minimum_size = Vector2(42, 42)
|
||
coin.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
||
coin.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
row.add_child(coin)
|
||
|
||
_currencyLabel = Label.new()
|
||
_currencyLabel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
_currencyLabel.add_theme_font_size_override("font_size", 24)
|
||
_currencyLabel.add_theme_color_override("font_color", Color.WHITE)
|
||
row.add_child(_currencyLabel)
|
||
|
||
var plus := Label.new()
|
||
plus.text = "+"
|
||
plus.add_theme_font_size_override("font_size", 30)
|
||
plus.add_theme_color_override("font_color", Color.WHITE)
|
||
row.add_child(plus)
|
||
return holder
|
||
|
||
func _build_close_button() -> Button:
|
||
var button := Button.new()
|
||
button.custom_minimum_size = Vector2(76, 76)
|
||
button.focus_mode = Control.FOCUS_NONE
|
||
button.tooltip_text = "关闭商城"
|
||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||
button.text = "×"
|
||
button.add_theme_font_size_override("font_size", 42)
|
||
button.add_theme_color_override("font_color", Color.WHITE)
|
||
button.add_theme_stylebox_override("normal", _create_panel_style(Color(0.150, 0.510, 0.925, 0.95), 28, false))
|
||
button.add_theme_stylebox_override("hover", _create_panel_style(Color(0.210, 0.650, 1.0, 1.0), 28, false))
|
||
button.add_theme_stylebox_override("pressed", _create_panel_style(Color(0.080, 0.380, 0.780, 1.0), 28, false))
|
||
button.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
|
||
button.pressed.connect(hide_panel)
|
||
return button
|
||
|
||
func _build_body() -> Control:
|
||
var body := HBoxContainer.new()
|
||
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
body.add_theme_constant_override("separation", 22)
|
||
body.add_child(_build_sidebar())
|
||
body.add_child(_build_grid_panel())
|
||
body.add_child(_build_detail_panel())
|
||
return body
|
||
|
||
func _build_sidebar() -> Control:
|
||
var sidebar := PanelContainer.new()
|
||
sidebar.custom_minimum_size = Vector2(176, 0)
|
||
sidebar.add_theme_stylebox_override("panel", _create_panel_style(Color(1, 1, 1, 0.96), 24, false))
|
||
|
||
var margin := MarginContainer.new()
|
||
margin.add_theme_constant_override("margin_left", 18)
|
||
margin.add_theme_constant_override("margin_top", 22)
|
||
margin.add_theme_constant_override("margin_right", 18)
|
||
margin.add_theme_constant_override("margin_bottom", 22)
|
||
sidebar.add_child(margin)
|
||
|
||
_categoryList = VBoxContainer.new()
|
||
_categoryList.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
_categoryList.add_theme_constant_override("separation", 14)
|
||
margin.add_child(_categoryList)
|
||
return sidebar
|
||
|
||
func _build_grid_panel() -> Control:
|
||
var gridPanel := PanelContainer.new()
|
||
gridPanel.custom_minimum_size = Vector2(760, 0)
|
||
gridPanel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
gridPanel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
gridPanel.add_theme_stylebox_override("panel", _create_panel_style(Color(0.955, 0.986, 1.0, 0.98), 28, false))
|
||
|
||
var margin := MarginContainer.new()
|
||
margin.add_theme_constant_override("margin_left", 32)
|
||
margin.add_theme_constant_override("margin_top", 34)
|
||
margin.add_theme_constant_override("margin_right", 32)
|
||
margin.add_theme_constant_override("margin_bottom", 34)
|
||
gridPanel.add_child(margin)
|
||
|
||
var scroll := ScrollContainer.new()
|
||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
margin.add_child(scroll)
|
||
|
||
_grid = GridContainer.new()
|
||
_grid.columns = 3
|
||
_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
_grid.add_theme_constant_override("h_separation", 26)
|
||
_grid.add_theme_constant_override("v_separation", 30)
|
||
scroll.add_child(_grid)
|
||
return gridPanel
|
||
|
||
func _build_detail_panel() -> Control:
|
||
var detail := PanelContainer.new()
|
||
detail.custom_minimum_size = Vector2(440, 0)
|
||
detail.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
detail.add_theme_stylebox_override("panel", _create_panel_style(Color(0.970, 0.992, 1.0, 0.99), 28, false))
|
||
|
||
var margin := MarginContainer.new()
|
||
margin.add_theme_constant_override("margin_left", 28)
|
||
margin.add_theme_constant_override("margin_top", 28)
|
||
margin.add_theme_constant_override("margin_right", 28)
|
||
margin.add_theme_constant_override("margin_bottom", 28)
|
||
detail.add_child(margin)
|
||
|
||
var box := VBoxContainer.new()
|
||
box.add_theme_constant_override("separation", 12)
|
||
margin.add_child(box)
|
||
|
||
_detailIcon = TextureRect.new()
|
||
_detailIcon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||
_detailIcon.custom_minimum_size = Vector2(0, 260)
|
||
_detailIcon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
||
_detailIcon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
box.add_child(_detailIcon)
|
||
|
||
_detailName = Label.new()
|
||
_detailName.add_theme_font_size_override("font_size", 28)
|
||
_detailName.add_theme_color_override("font_color", TEXT_COLOR)
|
||
_detailName.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
box.add_child(_detailName)
|
||
|
||
_detailStatus = Label.new()
|
||
_detailStatus.add_theme_font_size_override("font_size", 16)
|
||
_detailStatus.add_theme_color_override("font_color", ACCENT_COLOR)
|
||
box.add_child(_detailStatus)
|
||
|
||
_detailDescription = Label.new()
|
||
_detailDescription.custom_minimum_size = Vector2(0, 104)
|
||
_detailDescription.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
_detailDescription.add_theme_font_size_override("font_size", 17)
|
||
_detailDescription.add_theme_color_override("font_color", MUTED_COLOR)
|
||
box.add_child(_detailDescription)
|
||
|
||
_tagRow = HBoxContainer.new()
|
||
_tagRow.add_theme_constant_override("separation", 8)
|
||
box.add_child(_tagRow)
|
||
|
||
var priceBox := HBoxContainer.new()
|
||
priceBox.add_theme_constant_override("separation", 8)
|
||
priceBox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
box.add_child(priceBox)
|
||
|
||
var coin := TextureRect.new()
|
||
coin.texture = COIN_TEXTURE
|
||
coin.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||
coin.custom_minimum_size = Vector2(34, 34)
|
||
coin.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
||
coin.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
priceBox.add_child(coin)
|
||
|
||
_detailPrice = Label.new()
|
||
_detailPrice.add_theme_font_size_override("font_size", 24)
|
||
_detailPrice.add_theme_color_override("font_color", TEXT_COLOR)
|
||
priceBox.add_child(_detailPrice)
|
||
|
||
var spacer := Control.new()
|
||
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
box.add_child(spacer)
|
||
|
||
_previewButton = _create_action_button("预览", false)
|
||
_previewButton.pressed.connect(func() -> void:
|
||
_preview_selected_item()
|
||
)
|
||
box.add_child(_previewButton)
|
||
|
||
_purchaseButton = _create_action_button("购买", true)
|
||
_purchaseButton.pressed.connect(_on_purchase_pressed)
|
||
box.add_child(_purchaseButton)
|
||
return detail
|
||
|
||
func _build_bottom_bar() -> Control:
|
||
var row := HBoxContainer.new()
|
||
row.custom_minimum_size = Vector2(0, 62)
|
||
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
row.add_theme_constant_override("separation", 14)
|
||
for label in ["购买记录", "商城消息", "设置"]:
|
||
var button := _create_pill_button(label)
|
||
button.pressed.connect(func() -> void:
|
||
_show_toast("%s入口稍后接入" % label)
|
||
)
|
||
row.add_child(button)
|
||
return row
|
||
|
||
func _render_categories() -> void:
|
||
_clear_children(_categoryList)
|
||
_categoryButtons.clear()
|
||
for category in CATALOG_SCRIPT.get_categories():
|
||
var categoryId := str(category.get("id", ""))
|
||
var button := Button.new()
|
||
button.custom_minimum_size = Vector2(132, 116)
|
||
button.focus_mode = Control.FOCUS_NONE
|
||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||
button.add_theme_stylebox_override("normal", _create_category_button_style(false, false))
|
||
button.add_theme_stylebox_override("hover", _create_category_button_style(false, true))
|
||
button.add_theme_stylebox_override("pressed", _create_category_button_style(true, true))
|
||
button.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
|
||
button.pressed.connect(func() -> void:
|
||
_select_category(categoryId)
|
||
)
|
||
_categoryList.add_child(button)
|
||
_categoryButtons[categoryId] = button
|
||
|
||
var content := VBoxContainer.new()
|
||
content.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
content.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
content.add_theme_constant_override("separation", 4)
|
||
content.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
button.add_child(content)
|
||
|
||
var icon := TextureRect.new()
|
||
icon.texture = load(CATALOG_SCRIPT.get_category_icon_path(categoryId)) as Texture2D
|
||
icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||
icon.custom_minimum_size = Vector2(64, 64)
|
||
icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
||
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
content.add_child(icon)
|
||
|
||
var label := Label.new()
|
||
label.text = str(category.get("label", ""))
|
||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
label.add_theme_font_size_override("font_size", 17)
|
||
label.add_theme_color_override("font_color", MUTED_COLOR)
|
||
content.add_child(label)
|
||
|
||
func _select_category(categoryId: String) -> void:
|
||
_currentCategory = categoryId
|
||
_emit_event(EventNames.MALL_CATEGORY_CHANGED, {"categoryId": categoryId})
|
||
_update_category_buttons()
|
||
_render_grid(_get_catalog_items_for_category(categoryId))
|
||
|
||
func _update_category_buttons() -> void:
|
||
for categoryId in _categoryButtons.keys():
|
||
var button := _categoryButtons[categoryId] as Button
|
||
button.add_theme_stylebox_override("normal", _create_category_button_style(categoryId == _currentCategory, false))
|
||
|
||
func _render_grid(items: Array[Dictionary]) -> void:
|
||
_clear_children(_grid)
|
||
_cards.clear()
|
||
_selectedItem = {}
|
||
for index in range(items.size()):
|
||
var item := items[index]
|
||
var card := ITEM_CARD_SCRIPT.new() as Button
|
||
card.call("setup", item, index == 0)
|
||
card.get("item_selected").connect(_on_item_selected)
|
||
_grid.add_child(card)
|
||
_cards.append(card)
|
||
if index == 0:
|
||
_selectedItem = item.duplicate(true)
|
||
if _selectedItem.is_empty():
|
||
_render_empty_detail()
|
||
else:
|
||
_render_detail(_selectedItem)
|
||
|
||
func _on_item_selected(item: Dictionary) -> void:
|
||
_selectedItem = item.duplicate(true)
|
||
for card in _cards:
|
||
var cardData: Dictionary = card.get("itemData")
|
||
card.call("set_selected", str(cardData.get("id", "")) == str(_selectedItem.get("id", "")))
|
||
_emit_event(EventNames.MALL_ITEM_SELECTED, {"item": _selectedItem})
|
||
_render_detail(_selectedItem)
|
||
|
||
func _render_detail(item: Dictionary) -> void:
|
||
var iconPath := str(item.get("icon", ""))
|
||
_detailIcon.texture = _load_texture(iconPath, MALL_BADGE_TEXTURE)
|
||
_detailIcon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||
_detailName.text = str(item.get("name", "商品槽位"))
|
||
_detailDescription.text = str(item.get("description", ""))
|
||
var status := str(item.get("status", CATALOG_SCRIPT.STATUS_COMING_SOON))
|
||
_detailStatus.text = CATALOG_SCRIPT.get_status_label(status)
|
||
_detailStatus.add_theme_color_override("font_color", ACCENT_COLOR if status == CATALOG_SCRIPT.STATUS_AVAILABLE or status == CATALOG_SCRIPT.STATUS_OWNED else WARNING_COLOR)
|
||
_detailPrice.text = _format_item_price(item)
|
||
_render_tags(item)
|
||
_update_purchase_button(item)
|
||
|
||
func _render_empty_detail() -> void:
|
||
_detailIcon.texture = MALL_BADGE_TEXTURE
|
||
_detailIcon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||
if _catalogLoading:
|
||
_detailName.text = "读取商城"
|
||
_detailDescription.text = "正在从服务器读取当前账号的钱包和皮肤拥有状态。"
|
||
_detailStatus.text = "读取中"
|
||
elif not _catalogError.is_empty():
|
||
_detailName.text = "读取失败"
|
||
_detailDescription.text = _catalogError
|
||
_detailStatus.text = "不可用"
|
||
else:
|
||
_detailName.text = "暂未开放"
|
||
_detailDescription.text = "这个分类暂时没有商品。"
|
||
_detailStatus.text = "未开放"
|
||
_detailPrice.text = "-"
|
||
_clear_children(_tagRow)
|
||
_purchaseButton.text = "不可购买"
|
||
_purchaseButton.disabled = true
|
||
_previewButton.disabled = true
|
||
|
||
func _render_tags(item: Dictionary) -> void:
|
||
_clear_children(_tagRow)
|
||
var tags: Array = item.get("tags", [])
|
||
for tag in tags:
|
||
var label := Label.new()
|
||
label.text = str(tag)
|
||
label.add_theme_font_size_override("font_size", 14)
|
||
label.add_theme_color_override("font_color", ACCENT_COLOR)
|
||
label.add_theme_stylebox_override("normal", _create_panel_style(Color(0.896, 0.960, 1.0, 0.84), 13, false))
|
||
_tagRow.add_child(label)
|
||
|
||
func _update_purchase_button(item: Dictionary) -> void:
|
||
var status := str(item.get("status", CATALOG_SCRIPT.STATUS_COMING_SOON))
|
||
_previewButton.disabled = not _is_skin_item(item)
|
||
match status:
|
||
CATALOG_SCRIPT.STATUS_AVAILABLE:
|
||
if _walletLoading:
|
||
_purchaseButton.text = "读取数据"
|
||
_purchaseButton.disabled = true
|
||
elif not _walletLoaded:
|
||
_purchaseButton.text = "钱包不可用"
|
||
_purchaseButton.disabled = true
|
||
else:
|
||
_purchaseButton.disabled = not CATALOG_SCRIPT.can_purchase(item, _balance)
|
||
_purchaseButton.text = "购买" if _balance >= int(item.get("price", 0)) else "余额不足"
|
||
CATALOG_SCRIPT.STATUS_OWNED:
|
||
_purchaseButton.text = "已购买"
|
||
_purchaseButton.disabled = true
|
||
CATALOG_SCRIPT.STATUS_LOCKED:
|
||
_purchaseButton.text = "未解锁"
|
||
_purchaseButton.disabled = true
|
||
_:
|
||
_purchaseButton.text = "暂未开放"
|
||
_purchaseButton.disabled = true
|
||
|
||
func _preview_selected_item() -> void:
|
||
if _selectedItem.is_empty() or not _is_skin_item(_selectedItem):
|
||
_show_toast("当前商品暂不支持预览")
|
||
return
|
||
var skinId := str(_selectedItem.get("skinId", "")).strip_edges()
|
||
var appearanceManager := get_node_or_null("/root/AppearanceManager")
|
||
if appearanceManager == null or not appearanceManager.has_method("set_selected_skin"):
|
||
_show_toast("外观系统暂不可用")
|
||
return
|
||
if bool(appearanceManager.call("set_selected_skin", skinId)):
|
||
_emit_event(EventNames.MALL_PREVIEW_REQUESTED, {"item": _selectedItem, "skinId": skinId})
|
||
_show_toast("已预览「%s」" % str(_selectedItem.get("name", "")))
|
||
else:
|
||
_show_toast("皮肤预览失败")
|
||
|
||
func _on_purchase_pressed() -> void:
|
||
if _selectedItem.is_empty():
|
||
return
|
||
if _catalogLoading:
|
||
_show_toast("正在读取商城数据")
|
||
return
|
||
if not _catalogLoaded:
|
||
_show_toast(_catalogError if not _catalogError.is_empty() else "商城数据不可用")
|
||
return
|
||
if _walletLoading:
|
||
_show_toast("正在读取钱包")
|
||
return
|
||
if not _walletLoaded:
|
||
_show_toast(_walletError if not _walletError.is_empty() else "请先登录后再购买")
|
||
return
|
||
if not CATALOG_SCRIPT.can_purchase(_selectedItem, _balance):
|
||
_emit_event(EventNames.MALL_PURCHASE_FAILED, {"item": _selectedItem, "reason": "unavailable"})
|
||
_show_toast("当前商品暂不可购买")
|
||
return
|
||
_emit_event(EventNames.MALL_PURCHASE_REQUESTED, {"item": _selectedItem})
|
||
_show_confirm_dialog(_selectedItem)
|
||
|
||
func _show_confirm_dialog(item: Dictionary) -> void:
|
||
_confirmLabel.text = "确认购买「%s」?\n将消耗 %d 鲸币。" % [str(item.get("name", "")), int(item.get("price", 0))]
|
||
_confirmDialog.visible = true
|
||
_confirmDialog.modulate.a = 0.0
|
||
var tween := create_tween()
|
||
tween.tween_property(_confirmDialog, "modulate:a", 1.0, 0.12)
|
||
|
||
func _hide_confirm_dialog() -> void:
|
||
if is_instance_valid(_confirmDialog):
|
||
_confirmDialog.visible = false
|
||
|
||
func _confirm_purchase() -> void:
|
||
if _selectedItem.is_empty():
|
||
return
|
||
var price := int(_selectedItem.get("price", 0))
|
||
if not _is_backend_purchase_available():
|
||
_hide_confirm_dialog()
|
||
_emit_event(EventNames.MALL_PURCHASE_FAILED, {"item": _selectedItem, "reason": "unauthenticated"})
|
||
_show_toast("请先登录后再购买")
|
||
return
|
||
if price > _balance:
|
||
_hide_confirm_dialog()
|
||
_emit_event(EventNames.MALL_PURCHASE_FAILED, {"item": _selectedItem, "reason": "insufficient_balance"})
|
||
_show_toast("鲸币余额不足")
|
||
return
|
||
_submit_backend_purchase(_selectedItem)
|
||
|
||
func _get_catalog_items_for_category(categoryId: String) -> Array[Dictionary]:
|
||
var result: Array[Dictionary] = []
|
||
for item in _catalogItems:
|
||
var nextItem := item.duplicate(true)
|
||
var itemCategory := str(nextItem.get("category", ""))
|
||
if categoryId == "recommended" or itemCategory == categoryId:
|
||
result.append(nextItem)
|
||
result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||
return int(a.get("sortOrder", 0)) < int(b.get("sortOrder", 0))
|
||
)
|
||
return result
|
||
|
||
func _build_confirm_dialog() -> void:
|
||
_confirmDialog = Control.new()
|
||
_confirmDialog.name = "purchaseConfirmDialog"
|
||
_confirmDialog.set_anchors_preset(Control.PRESET_CENTER)
|
||
_confirmDialog.custom_minimum_size = Vector2(520, 320)
|
||
_confirmDialog.offset_left = -260
|
||
_confirmDialog.offset_top = -160
|
||
_confirmDialog.offset_right = 260
|
||
_confirmDialog.offset_bottom = 160
|
||
_confirmDialog.visible = false
|
||
_confirmDialog.mouse_filter = Control.MOUSE_FILTER_STOP
|
||
add_child(_confirmDialog)
|
||
|
||
var bg := PanelContainer.new()
|
||
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
bg.add_theme_stylebox_override("panel", _create_panel_style(Color(0.982, 0.995, 1.0, 0.98), 30, true))
|
||
bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_confirmDialog.add_child(bg)
|
||
|
||
var margin := MarginContainer.new()
|
||
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
margin.add_theme_constant_override("margin_left", 40)
|
||
margin.add_theme_constant_override("margin_top", 54)
|
||
margin.add_theme_constant_override("margin_right", 40)
|
||
margin.add_theme_constant_override("margin_bottom", 40)
|
||
_confirmDialog.add_child(margin)
|
||
|
||
var box := VBoxContainer.new()
|
||
box.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
box.add_theme_constant_override("separation", 22)
|
||
margin.add_child(box)
|
||
|
||
_confirmLabel = Label.new()
|
||
_confirmLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
_confirmLabel.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
_confirmLabel.add_theme_font_size_override("font_size", 22)
|
||
_confirmLabel.add_theme_color_override("font_color", TEXT_COLOR)
|
||
box.add_child(_confirmLabel)
|
||
|
||
var row := HBoxContainer.new()
|
||
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
row.add_theme_constant_override("separation", 14)
|
||
box.add_child(row)
|
||
|
||
var cancel := _create_action_button("取消", false)
|
||
cancel.custom_minimum_size = Vector2(150, 54)
|
||
cancel.pressed.connect(_hide_confirm_dialog)
|
||
row.add_child(cancel)
|
||
|
||
var confirm := _create_action_button("确认购买", true)
|
||
confirm.custom_minimum_size = Vector2(170, 54)
|
||
confirm.pressed.connect(_confirm_purchase)
|
||
row.add_child(confirm)
|
||
|
||
func _build_toast() -> void:
|
||
_toast = Control.new()
|
||
_toast.name = "mallToast"
|
||
_toast.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
|
||
_toast.offset_left = 560
|
||
_toast.offset_right = -560
|
||
_toast.offset_top = -122
|
||
_toast.offset_bottom = -52
|
||
_toast.visible = false
|
||
_toast.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
add_child(_toast)
|
||
|
||
var bg := PanelContainer.new()
|
||
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
bg.add_theme_stylebox_override("panel", _create_panel_style(Color(0.028, 0.160, 0.310, 0.92), 28, false))
|
||
bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_toast.add_child(bg)
|
||
|
||
_toastLabel = Label.new()
|
||
_toastLabel.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
_toastLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
_toastLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
_toastLabel.add_theme_font_size_override("font_size", 19)
|
||
_toastLabel.add_theme_color_override("font_color", Color.WHITE)
|
||
_toast.add_child(_toastLabel)
|
||
|
||
func _show_toast(message: String) -> void:
|
||
if is_instance_valid(_toastTween):
|
||
_toastTween.kill()
|
||
_toastLabel.text = message
|
||
_toast.visible = true
|
||
_toast.modulate.a = 0.0
|
||
_toastTween = create_tween()
|
||
_toastTween.tween_property(_toast, "modulate:a", 1.0, 0.12)
|
||
_toastTween.tween_interval(1.45)
|
||
_toastTween.tween_property(_toast, "modulate:a", 0.0, 0.18)
|
||
_toastTween.finished.connect(func() -> void:
|
||
_toast.visible = false
|
||
)
|
||
|
||
func _create_action_button(labelText: String, primary: bool) -> Button:
|
||
var button := Button.new()
|
||
button.text = labelText
|
||
button.custom_minimum_size = Vector2(0, 68)
|
||
button.focus_mode = Control.FOCUS_NONE
|
||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||
button.add_theme_font_size_override("font_size", 22)
|
||
button.add_theme_color_override("font_color", Color.WHITE if primary else TEXT_COLOR)
|
||
if not primary:
|
||
button.add_theme_color_override("font_hover_color", ACCENT_COLOR)
|
||
button.add_theme_color_override("font_pressed_color", Color(0.060, 0.270, 0.540, 1.0))
|
||
button.add_theme_color_override("font_disabled_color", Color(0.520, 0.600, 0.680, 0.55))
|
||
if primary:
|
||
button.add_theme_stylebox_override("normal", _create_panel_style(Color(0.960, 0.310, 0.245, 0.96), 28, false))
|
||
button.add_theme_stylebox_override("hover", _create_panel_style(Color(1.000, 0.390, 0.300, 1.0), 28, false))
|
||
button.add_theme_stylebox_override("pressed", _create_panel_style(Color(0.820, 0.220, 0.180, 1.0), 28, false))
|
||
else:
|
||
button.add_theme_stylebox_override("normal", _create_panel_style(Color(0.936, 0.982, 1.0, 0.94), 28, false))
|
||
button.add_theme_stylebox_override("hover", _create_panel_style(Color(0.860, 0.955, 1.0, 1.0), 28, false))
|
||
button.add_theme_stylebox_override("pressed", _create_panel_style(Color(0.760, 0.905, 0.980, 1.0), 28, false))
|
||
button.add_theme_stylebox_override("disabled", _create_panel_style(Color(0.580, 0.650, 0.730, 0.62), 28, false))
|
||
button.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
|
||
return button
|
||
|
||
func _create_pill_button(labelText: String) -> Button:
|
||
var button := Button.new()
|
||
button.text = labelText
|
||
button.custom_minimum_size = Vector2(154, 48)
|
||
button.focus_mode = Control.FOCUS_NONE
|
||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||
button.add_theme_font_size_override("font_size", 16)
|
||
button.add_theme_color_override("font_color", TEXT_COLOR)
|
||
button.add_theme_color_override("font_hover_color", ACCENT_COLOR)
|
||
button.add_theme_color_override("font_pressed_color", Color(0.060, 0.270, 0.540, 1.0))
|
||
button.add_theme_color_override("font_disabled_color", Color(0.520, 0.600, 0.680, 0.55))
|
||
button.add_theme_stylebox_override("normal", _create_panel_style(Color(1, 1, 1, 0.70), 22, false))
|
||
button.add_theme_stylebox_override("hover", _create_panel_style(Color(0.900, 0.965, 1.0, 0.94), 22, false))
|
||
button.add_theme_stylebox_override("pressed", _create_panel_style(Color(0.806, 0.925, 1.0, 1.0), 22, false))
|
||
button.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
|
||
return button
|
||
|
||
func _on_overlay_gui_input(event: InputEvent) -> void:
|
||
if event is InputEventMouseButton:
|
||
var mouseEvent := event as InputEventMouseButton
|
||
if mouseEvent.pressed and mouseEvent.button_index == MOUSE_BUTTON_LEFT:
|
||
hide_panel()
|
||
|
||
func _position_panel() -> void:
|
||
var viewportSize := get_viewport_rect().size
|
||
var width := minf(PANEL_SIZE.x, maxf(1080.0, viewportSize.x - MIN_MARGIN.x * 2.0))
|
||
var height := minf(PANEL_SIZE.y, maxf(820.0, viewportSize.y - MIN_MARGIN.y * 2.0))
|
||
_panel.position = (viewportSize - Vector2(width, height)) * 0.5
|
||
_panel.size = Vector2(width, height)
|
||
_panel.pivot_offset = _panel.size * 0.5
|
||
|
||
func _animate_panel(opening: bool) -> void:
|
||
if is_instance_valid(_transitionTween):
|
||
_transitionTween.kill()
|
||
_transitionTween = create_tween()
|
||
_transitionTween.set_parallel(true)
|
||
if opening:
|
||
_update_panel_alpha(0.0, 0.0)
|
||
_panel.scale = Vector2(0.985, 0.985)
|
||
_transitionTween.tween_property(_overlay, "modulate:a", 1.0, 0.16)
|
||
_transitionTween.tween_property(_panel, "modulate:a", 1.0, 0.18)
|
||
_transitionTween.tween_property(_panel, "scale", Vector2.ONE, 0.18).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||
return
|
||
_transitionTween.tween_property(_overlay, "modulate:a", 0.0, 0.14)
|
||
_transitionTween.tween_property(_panel, "modulate:a", 0.0, 0.14)
|
||
_transitionTween.tween_property(_panel, "scale", Vector2(0.985, 0.985), 0.14)
|
||
_transitionTween.finished.connect(func() -> void:
|
||
visible = false
|
||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
set_process(false)
|
||
)
|
||
|
||
func _update_panel_alpha(overlayAlpha: float, panelAlpha: float) -> void:
|
||
_overlay.modulate.a = overlayAlpha
|
||
_panel.modulate.a = panelAlpha
|
||
|
||
func _update_currency() -> void:
|
||
if is_instance_valid(_currencyLabel):
|
||
if _walletLoading:
|
||
_currencyLabel.text = "读取中"
|
||
elif not _walletLoaded:
|
||
_currencyLabel.text = "未登录" if _walletError == "请先登录后再购买" else "读取失败"
|
||
_currencyLabel.tooltip_text = _walletError
|
||
else:
|
||
_currencyLabel.text = _format_number(_balance)
|
||
_currencyLabel.tooltip_text = "鲸币余额"
|
||
|
||
func _format_number(value: int) -> String:
|
||
var text := str(value)
|
||
var result := ""
|
||
var count := 0
|
||
for i in range(text.length() - 1, -1, -1):
|
||
if count > 0 and count % 3 == 0:
|
||
result = "," + result
|
||
result = text[i] + result
|
||
count += 1
|
||
return result
|
||
|
||
func _format_item_price(item: Dictionary) -> String:
|
||
var status := str(item.get("status", CATALOG_SCRIPT.STATUS_COMING_SOON))
|
||
if status == CATALOG_SCRIPT.STATUS_OWNED:
|
||
return "已购买"
|
||
if status == CATALOG_SCRIPT.STATUS_COMING_SOON or status == CATALOG_SCRIPT.STATUS_LOCKED:
|
||
return "-"
|
||
return str(int(item.get("price", 0)))
|
||
|
||
func _is_skin_item(item: Dictionary) -> bool:
|
||
return not str(item.get("skinId", "")).strip_edges().is_empty()
|
||
|
||
func _build_purchase_request() -> void:
|
||
_purchaseRequest = HTTPRequest.new()
|
||
_purchaseRequest.name = "mallPurchaseRequest"
|
||
_purchaseRequest.timeout = 12.0
|
||
_purchaseRequest.request_completed.connect(_on_purchase_request_completed)
|
||
add_child(_purchaseRequest)
|
||
|
||
func _build_catalog_request() -> void:
|
||
_catalogRequest = HTTPRequest.new()
|
||
_catalogRequest.name = "mallCatalogRequest"
|
||
_catalogRequest.timeout = 12.0
|
||
_catalogRequest.request_completed.connect(_on_catalog_request_completed)
|
||
add_child(_catalogRequest)
|
||
|
||
func _connect_auth_state() -> void:
|
||
var authManager := get_node_or_null("/root/AuthManager")
|
||
if authManager == null or not authManager.has_signal("auth_state_changed"):
|
||
return
|
||
_accountGeneration = _current_account_generation()
|
||
var callback := Callable(self, "_on_auth_state_changed")
|
||
if not authManager.is_connected("auth_state_changed", callback):
|
||
authManager.connect("auth_state_changed", callback)
|
||
|
||
func _on_auth_state_changed(_isAuthenticated: bool, _user: Dictionary) -> void:
|
||
var currentGeneration := _current_account_generation()
|
||
if currentGeneration == _accountGeneration:
|
||
return
|
||
_accountGeneration = currentGeneration
|
||
_cancel_account_bound_requests()
|
||
_reset_account_bound_state()
|
||
if _isOpen:
|
||
_prepare_wallet_state()
|
||
_prepare_catalog_state()
|
||
_fetch_catalog()
|
||
else:
|
||
_update_currency()
|
||
|
||
func _is_backend_purchase_available() -> bool:
|
||
var authManager := get_node_or_null("/root/AuthManager")
|
||
return authManager != null and authManager.has_method("is_authenticated") and bool(authManager.call("is_authenticated")) and authManager.has_method("get_access_token")
|
||
|
||
func _current_account_generation() -> int:
|
||
var authManager := get_node_or_null("/root/AuthManager")
|
||
if authManager != null and authManager.has_method("get_account_generation"):
|
||
return int(authManager.call("get_account_generation"))
|
||
return -1
|
||
|
||
func _cancel_account_bound_requests() -> void:
|
||
if is_instance_valid(_catalogRequest):
|
||
_catalogRequest.cancel_request()
|
||
if is_instance_valid(_purchaseRequest):
|
||
_purchaseRequest.cancel_request()
|
||
_catalogRequestAccountGeneration = -1
|
||
_purchaseRequestAccountGeneration = -1
|
||
|
||
func _reset_account_bound_state() -> void:
|
||
_balance = 0
|
||
_walletLoaded = false
|
||
_walletLoading = false
|
||
_walletError = ""
|
||
_catalogLoaded = false
|
||
_catalogLoading = false
|
||
_catalogError = ""
|
||
_catalogItems = []
|
||
_selectedItem = {}
|
||
_hide_confirm_dialog()
|
||
|
||
func _auth_headers() -> PackedStringArray:
|
||
var authManager := get_node_or_null("/root/AuthManager")
|
||
var accessToken := str(authManager.call("get_access_token")).strip_edges() if authManager != null and authManager.has_method("get_access_token") else ""
|
||
return PackedStringArray([
|
||
"Content-Type: application/json",
|
||
"Authorization: Bearer %s" % accessToken,
|
||
])
|
||
|
||
func _submit_backend_purchase(item: Dictionary) -> void:
|
||
var authManager := get_node_or_null("/root/AuthManager")
|
||
var accessToken := str(authManager.call("get_access_token")).strip_edges() if authManager != null else ""
|
||
if accessToken.is_empty():
|
||
_hide_confirm_dialog()
|
||
_emit_event(EventNames.MALL_PURCHASE_FAILED, {"item": item, "reason": "unauthenticated"})
|
||
_show_toast("请先登录后再购买")
|
||
_update_purchase_button(_selectedItem)
|
||
return
|
||
_purchaseButton.disabled = true
|
||
_purchaseButton.text = "购买中"
|
||
var payload := {
|
||
"item_id": str(item.get("id", "")),
|
||
}
|
||
_purchaseRequestAccountGeneration = _current_account_generation()
|
||
var err := _purchaseRequest.request("%s/shop/purchases" % NetworkConfig.get_api_base_url(), _auth_headers(), HTTPClient.METHOD_POST, JSON.stringify(payload))
|
||
if err != OK:
|
||
_purchaseRequestAccountGeneration = -1
|
||
_hide_confirm_dialog()
|
||
_emit_event(EventNames.MALL_PURCHASE_FAILED, {"item": item, "reason": "request_failed"})
|
||
_show_toast("购买请求发送失败")
|
||
_update_purchase_button(_selectedItem)
|
||
|
||
func _on_purchase_request_completed(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||
if _purchaseRequestAccountGeneration != _current_account_generation():
|
||
return
|
||
_purchaseRequestAccountGeneration = -1
|
||
if result != HTTPRequest.RESULT_SUCCESS:
|
||
_hide_confirm_dialog()
|
||
_emit_event(EventNames.MALL_PURCHASE_FAILED, {"item": _selectedItem, "reason": "network_failed"})
|
||
_show_toast("购买失败,请稍后再试")
|
||
_update_purchase_button(_selectedItem)
|
||
return
|
||
var json := JSON.new()
|
||
if json.parse(body.get_string_from_utf8()) != OK:
|
||
_hide_confirm_dialog()
|
||
_emit_event(EventNames.MALL_PURCHASE_FAILED, {"item": _selectedItem, "reason": "invalid_response"})
|
||
_show_toast("购买响应解析失败")
|
||
_update_purchase_button(_selectedItem)
|
||
return
|
||
var responseVariant: Variant = json.data
|
||
if not (responseVariant is Dictionary):
|
||
_hide_confirm_dialog()
|
||
_emit_event(EventNames.MALL_PURCHASE_FAILED, {"item": _selectedItem, "reason": "invalid_response"})
|
||
_show_toast("购买响应格式错误")
|
||
_update_purchase_button(_selectedItem)
|
||
return
|
||
var response: Dictionary = responseVariant
|
||
if responseCode < 200 or responseCode >= 300 or not bool(response.get("success", true)):
|
||
_hide_confirm_dialog()
|
||
_emit_event(EventNames.MALL_PURCHASE_FAILED, {"item": _selectedItem, "reason": "backend_rejected"})
|
||
_show_toast(str(response.get("message", "购买失败")))
|
||
_update_purchase_button(_selectedItem)
|
||
return
|
||
var dataVariant: Variant = response.get("data", {})
|
||
var data: Dictionary = dataVariant as Dictionary if dataVariant is Dictionary else {}
|
||
if not data.is_empty():
|
||
var ownedVariant: Variant = data.get("owned_skin_ids", [])
|
||
var appearanceManager := get_node_or_null("/root/AppearanceManager")
|
||
if ownedVariant is Array and appearanceManager != null and appearanceManager.has_method("set_owned_skin_ids"):
|
||
appearanceManager.call("set_owned_skin_ids", ownedVariant)
|
||
var playerStateManager := get_node_or_null("/root/PlayerStateManager")
|
||
if playerStateManager != null and playerStateManager.has_method("apply_purchase_payload"):
|
||
playerStateManager.call("apply_purchase_payload", data)
|
||
_balance = int(data.get("balance", _balance))
|
||
_walletLoaded = true
|
||
_walletLoading = false
|
||
_walletError = ""
|
||
_update_currency()
|
||
_hide_confirm_dialog()
|
||
var successMessage := "购买成功,已加入家具背包" if _is_room_decor_item(_selectedItem) else "购买成功,已加入你的皮肤"
|
||
_show_toast(successMessage)
|
||
_emit_event(EventNames.MALL_PURCHASE_SUCCEEDED, {"item": _selectedItem, "balance": _balance})
|
||
_fetch_catalog()
|
||
|
||
func _fetch_catalog() -> void:
|
||
if not _is_backend_purchase_available():
|
||
_catalogLoading = false
|
||
_catalogLoaded = false
|
||
_catalogError = "请先登录后查看商城"
|
||
_walletLoading = false
|
||
_walletLoaded = false
|
||
_walletError = "请先登录后再购买"
|
||
_catalogItems = []
|
||
_catalogRequestAccountGeneration = -1
|
||
_update_currency()
|
||
_select_category(_currentCategory)
|
||
return
|
||
_catalogLoading = true
|
||
_catalogLoaded = false
|
||
_catalogError = ""
|
||
_walletLoading = true
|
||
_walletLoaded = false
|
||
_walletError = ""
|
||
_update_currency()
|
||
_catalogRequestAccountGeneration = _current_account_generation()
|
||
var err := _catalogRequest.request("%s/shop/catalog" % NetworkConfig.get_api_base_url(), _auth_headers(), HTTPClient.METHOD_GET, "")
|
||
if err != OK:
|
||
_catalogRequestAccountGeneration = -1
|
||
_catalogLoading = false
|
||
_catalogLoaded = false
|
||
_catalogError = "商城数据请求发送失败"
|
||
_walletLoading = false
|
||
_walletLoaded = false
|
||
_walletError = _catalogError
|
||
_update_currency()
|
||
_render_grid([])
|
||
push_warning("MallPanel: 商城数据请求发送失败: %s" % error_string(err))
|
||
|
||
func _on_catalog_request_completed(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||
if _catalogRequestAccountGeneration != _current_account_generation():
|
||
return
|
||
_catalogRequestAccountGeneration = -1
|
||
if result != HTTPRequest.RESULT_SUCCESS or responseCode < 200 or responseCode >= 300:
|
||
_apply_catalog_error("商城数据读取失败")
|
||
return
|
||
var json := JSON.new()
|
||
if json.parse(body.get_string_from_utf8()) != OK:
|
||
_apply_catalog_error("商城响应解析失败")
|
||
return
|
||
var responseVariant: Variant = json.data
|
||
if not (responseVariant is Dictionary):
|
||
_apply_catalog_error("商城响应格式错误")
|
||
return
|
||
var response: Dictionary = responseVariant
|
||
if not bool(response.get("success", true)):
|
||
_apply_catalog_error(str(response.get("message", "商城数据读取失败")))
|
||
return
|
||
var dataVariant: Variant = response.get("data", {})
|
||
if not (dataVariant is Dictionary):
|
||
_apply_catalog_error("商城数据格式错误")
|
||
return
|
||
_apply_catalog_payload(dataVariant as Dictionary)
|
||
|
||
func _apply_catalog_payload(data: Dictionary) -> void:
|
||
_balance = int(data.get("balance", 0))
|
||
_walletLoaded = true
|
||
_walletLoading = false
|
||
_walletError = ""
|
||
_catalogItems.clear()
|
||
var itemsVariant: Variant = data.get("items", [])
|
||
if itemsVariant is Array:
|
||
for itemVariant in itemsVariant:
|
||
if itemVariant is Dictionary:
|
||
var item: Dictionary = (itemVariant as Dictionary).duplicate(true)
|
||
var itemId := str(item.get("id", item.get("item_id", ""))).strip_edges()
|
||
if not item.has("id") and not itemId.is_empty():
|
||
item["id"] = itemId
|
||
item["icon"] = _icon_path_for_item(item)
|
||
_catalogItems.append(item)
|
||
var ownedVariant: Variant = data.get("owned_skin_ids", [])
|
||
var appearanceManager := get_node_or_null("/root/AppearanceManager")
|
||
if ownedVariant is Array and appearanceManager != null and appearanceManager.has_method("set_owned_skin_ids"):
|
||
appearanceManager.call("set_owned_skin_ids", ownedVariant)
|
||
var playerStateManager := get_node_or_null("/root/PlayerStateManager")
|
||
if playerStateManager != null and playerStateManager.has_method("apply_wallet"):
|
||
playerStateManager.call("apply_wallet", {
|
||
"balance": _balance,
|
||
"currency": str(data.get("currency", "whale_coin")),
|
||
})
|
||
_catalogLoaded = true
|
||
_catalogLoading = false
|
||
_catalogError = ""
|
||
_update_currency()
|
||
_select_category(_currentCategory)
|
||
|
||
func _apply_catalog_error(message: String) -> void:
|
||
_catalogItems = []
|
||
_catalogLoaded = false
|
||
_catalogLoading = false
|
||
_catalogError = message
|
||
_walletLoaded = false
|
||
_walletLoading = false
|
||
_walletError = message
|
||
_update_currency()
|
||
_show_toast(message)
|
||
_select_category(_currentCategory)
|
||
|
||
func _icon_path_for_item(item: Dictionary) -> String:
|
||
var itemId := str(item.get("id", ""))
|
||
var directIcon := str(item.get("icon", ""))
|
||
if not directIcon.is_empty():
|
||
return directIcon
|
||
match itemId:
|
||
"skin_classic_whale":
|
||
return "res://assets/ui/mall/skins/classic_whale_product.png"
|
||
"skin_human_whale_directional_v2_8x4":
|
||
return "res://assets/ui/mall/skins/human_whale_directional_v2_8x4_product.png"
|
||
"skin_girl_sailor_turnaround_v2_8x4":
|
||
return "res://assets/ui/mall/skins/girl_sailor_turnaround_v2_8x4_product.png"
|
||
"skin_panda_hero_8x4":
|
||
return "res://assets/ui/mall/skins/panda_hero_8x4_product.png"
|
||
"skin_ordinary_man_male_8x4":
|
||
return "res://assets/ui/mall/skins/ordinary_man_male_8x4_product.png"
|
||
_:
|
||
return ""
|
||
|
||
func _prepare_wallet_state() -> void:
|
||
_balance = 0
|
||
_walletLoaded = false
|
||
_walletError = ""
|
||
_walletLoading = _is_backend_purchase_available()
|
||
if not _walletLoading:
|
||
_walletError = "请先登录后再购买"
|
||
_update_currency()
|
||
|
||
func _prepare_catalog_state() -> void:
|
||
_catalogItems = []
|
||
_catalogLoaded = false
|
||
_catalogError = ""
|
||
_catalogLoading = _is_backend_purchase_available()
|
||
_select_category(_currentCategory)
|
||
|
||
func _is_room_decor_item(item: Dictionary) -> bool:
|
||
return str(item.get("itemType", "")) == CATALOG_SCRIPT.ITEM_TYPE_ROOM_DECOR or not str(item.get("decorId", "")).strip_edges().is_empty()
|
||
|
||
func _load_texture(path: String, fallback: Texture2D = null) -> Texture2D:
|
||
if path.is_empty():
|
||
return fallback
|
||
if FileAccess.file_exists("%s.import" % path):
|
||
var texture := load(path) as Texture2D
|
||
if texture != null:
|
||
return texture
|
||
var image := Image.load_from_file(ProjectSettings.globalize_path(path))
|
||
if image == null or image.is_empty():
|
||
return fallback
|
||
return ImageTexture.create_from_image(image)
|
||
|
||
func _release_movement_input_state() -> void:
|
||
Input.flush_buffered_events()
|
||
_release_movement_actions()
|
||
|
||
func _release_movement_actions() -> void:
|
||
for action in MOVEMENT_ACTIONS:
|
||
Input.action_release(action)
|
||
|
||
func _clear_children(node: Node) -> void:
|
||
for child in node.get_children():
|
||
child.queue_free()
|
||
|
||
func _create_panel_style(color: Color, radius: int, shadow: bool) -> StyleBoxFlat:
|
||
var style := StyleBoxFlat.new()
|
||
style.bg_color = color
|
||
style.corner_radius_top_left = radius
|
||
style.corner_radius_top_right = radius
|
||
style.corner_radius_bottom_left = radius
|
||
style.corner_radius_bottom_right = radius
|
||
style.content_margin_left = 12
|
||
style.content_margin_top = 8
|
||
style.content_margin_right = 12
|
||
style.content_margin_bottom = 8
|
||
if shadow:
|
||
style.shadow_color = Color(0.082, 0.243, 0.380, 0.18)
|
||
style.shadow_size = 16
|
||
style.shadow_offset = Vector2(0, 5)
|
||
return style
|
||
|
||
func _create_category_button_style(selected: bool, hover: bool) -> StyleBoxFlat:
|
||
var style := StyleBoxFlat.new()
|
||
style.bg_color = Color(0.818, 0.936, 0.982, 1.0) if selected else Color(1.0, 1.0, 1.0, 0.92)
|
||
if hover and not selected:
|
||
style.bg_color = Color(0.916, 0.976, 1.0, 1.0)
|
||
style.corner_radius_top_left = 20
|
||
style.corner_radius_top_right = 20
|
||
style.corner_radius_bottom_left = 20
|
||
style.corner_radius_bottom_right = 20
|
||
style.border_width_left = 2
|
||
style.border_width_top = 2
|
||
style.border_width_right = 2
|
||
style.border_width_bottom = 2
|
||
style.border_color = Color(0.386, 0.745, 0.925, 0.92) if selected else Color(0.742, 0.870, 0.940, 0.72)
|
||
style.content_margin_left = 10
|
||
style.content_margin_top = 8
|
||
style.content_margin_right = 10
|
||
style.content_margin_bottom = 8
|
||
style.shadow_color = Color(0.090, 0.290, 0.430, 0.12 if selected else 0.06)
|
||
style.shadow_size = 4 if selected else 2
|
||
style.shadow_offset = Vector2(0, 2)
|
||
return style
|
||
|
||
func _create_header_style() -> StyleBoxFlat:
|
||
var style := _create_panel_style(Color(0.020, 0.505, 0.895, 0.96), 30, false)
|
||
style.border_width_bottom = 3
|
||
style.border_color = Color(0.690, 0.920, 1.0, 0.86)
|
||
return style
|
||
|
||
func _emit_event(eventName: String, data: Dictionary) -> void:
|
||
var eventSystem := get_node_or_null("/root/EventSystem")
|
||
if eventSystem != null:
|
||
eventSystem.call("emit_event", eventName, data)
|