class_name MallItemCard extends Button # ============================================================================ # MallItemCard.gd - 商城商品卡片 # ============================================================================ # 展示商品槽位、状态、价格和选中态,由 MallPanel 统一驱动。 # ============================================================================ signal item_selected(item: Dictionary) const TEXT_COLOR: Color = Color(0.111, 0.231, 0.380) const MUTED_COLOR: Color = Color(0.486, 0.584, 0.694) const ACCENT_COLOR: Color = Color(0.086, 0.608, 0.922) const LOCKED_COLOR: Color = Color(0.455, 0.510, 0.584) const COIN_TEXTURE: Texture2D = preload("res://assets/ui/mall/branding/branding_whale_coin.png") const CATALOG_SCRIPT: Script = preload("res://Config/MallCatalog.gd") var itemData: Dictionary = {} var isSelected: bool = false var _background: PanelContainer var _icon: TextureRect var _nameLabel: Label var _statusLabel: Label var _priceLabel: Label var _lockedOverlay: ColorRect func _ready() -> void: focus_mode = Control.FOCUS_NONE mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND custom_minimum_size = Vector2(218, 288) add_theme_stylebox_override("normal", StyleBoxEmpty.new()) add_theme_stylebox_override("hover", StyleBoxEmpty.new()) add_theme_stylebox_override("pressed", StyleBoxEmpty.new()) add_theme_stylebox_override("focus", StyleBoxEmpty.new()) _build_ui() pressed.connect(_on_pressed) func setup(data: Dictionary, selected: bool) -> void: itemData = data.duplicate(true) isSelected = selected if not is_inside_tree(): return _render() func set_selected(selected: bool) -> void: isSelected = selected if is_inside_tree(): _render() func _build_ui() -> void: _background = PanelContainer.new() _background.name = "background" _background.set_anchors_preset(Control.PRESET_FULL_RECT) _background.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(_background) var margin := MarginContainer.new() margin.name = "contentMargin" margin.set_anchors_preset(Control.PRESET_FULL_RECT) margin.add_theme_constant_override("margin_left", 14) margin.add_theme_constant_override("margin_top", 14) margin.add_theme_constant_override("margin_right", 14) margin.add_theme_constant_override("margin_bottom", 14) margin.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(margin) var content := VBoxContainer.new() content.alignment = BoxContainer.ALIGNMENT_CENTER content.add_theme_constant_override("separation", 7) content.mouse_filter = Control.MOUSE_FILTER_IGNORE margin.add_child(content) _statusLabel = Label.new() _statusLabel.custom_minimum_size = Vector2(0, 24) _statusLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT _statusLabel.add_theme_font_size_override("font_size", 15) _statusLabel.add_theme_color_override("font_color", MUTED_COLOR) content.add_child(_statusLabel) _icon = TextureRect.new() _icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR _icon.custom_minimum_size = Vector2(152, 134) _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) _nameLabel = Label.new() _nameLabel.custom_minimum_size = Vector2(0, 48) _nameLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _nameLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _nameLabel.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _nameLabel.add_theme_font_size_override("font_size", 19) _nameLabel.add_theme_color_override("font_color", TEXT_COLOR) content.add_child(_nameLabel) var priceRow := HBoxContainer.new() priceRow.alignment = BoxContainer.ALIGNMENT_CENTER priceRow.add_theme_constant_override("separation", 4) priceRow.mouse_filter = Control.MOUSE_FILTER_IGNORE content.add_child(priceRow) var coin := TextureRect.new() coin.texture = COIN_TEXTURE coin.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR coin.custom_minimum_size = Vector2(28, 28) coin.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL coin.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED coin.mouse_filter = Control.MOUSE_FILTER_IGNORE priceRow.add_child(coin) _priceLabel = Label.new() _priceLabel.add_theme_font_size_override("font_size", 18) _priceLabel.add_theme_color_override("font_color", MUTED_COLOR) priceRow.add_child(_priceLabel) _lockedOverlay = ColorRect.new() _lockedOverlay.name = "lockedOverlay" _lockedOverlay.set_anchors_preset(Control.PRESET_FULL_RECT) _lockedOverlay.color = Color(0.350, 0.420, 0.510, 0.18) _lockedOverlay.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(_lockedOverlay) _render() func _render() -> void: _background.add_theme_stylebox_override("panel", _create_card_style(isSelected)) var status := str(itemData.get("status", CATALOG_SCRIPT.STATUS_COMING_SOON)) var iconPath := str(itemData.get("icon", "")) _icon.texture = _load_texture(iconPath) _nameLabel.text = str(itemData.get("name", "商品槽位")) _statusLabel.text = CATALOG_SCRIPT.get_status_label(status) _priceLabel.text = _format_price(itemData) _lockedOverlay.visible = status == CATALOG_SCRIPT.STATUS_COMING_SOON or status == CATALOG_SCRIPT.STATUS_LOCKED _statusLabel.add_theme_color_override("font_color", ACCENT_COLOR if status == CATALOG_SCRIPT.STATUS_AVAILABLE else LOCKED_COLOR) func _create_card_style(selected: bool) -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = Color(0.982, 0.995, 1.0, 1.0) style.corner_radius_top_left = 18 style.corner_radius_top_right = 18 style.corner_radius_bottom_left = 18 style.corner_radius_bottom_right = 18 style.border_width_left = 2 if not selected else 4 style.border_width_top = 2 if not selected else 4 style.border_width_right = 2 if not selected else 4 style.border_width_bottom = 2 if not selected else 4 style.border_color = Color(0.658, 0.835, 0.965, 0.86) if not selected else Color(0.086, 0.690, 0.960, 1.0) style.shadow_color = Color(0.086, 0.314, 0.520, 0.15 if selected else 0.08) style.shadow_size = 14 if selected else 8 style.shadow_offset = Vector2(0, 4) return style func _format_price(data: Dictionary) -> String: var status := str(data.get("status", "")) if status == CATALOG_SCRIPT.STATUS_OWNED: return "已购买" if status == CATALOG_SCRIPT.STATUS_COMING_SOON or status == CATALOG_SCRIPT.STATUS_LOCKED: return "暂未开放" return str(int(data.get("price", 0))) func _load_texture(path: String) -> Texture2D: if path.is_empty(): return null 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 null return ImageTexture.create_from_image(image) func _on_pressed() -> void: item_selected.emit(itemData)