# VERTICAL SLICE - NOT FOR PRODUCTION # Date: 2026-09-21(GP+商店房实施批——商店房覆盖层呈现) # 商店覆盖层(模态):标题「商店」/ 金币余额 / 三商品卡全陈列(显示名·档位文案·现价·已购档数)/ # 购买按钮(余额不足禁用置灰)/ 离开按钮。模态挂接方式仿既有覆盖层(route_overlay 现成参照—— # dim + CenterContainer + box,组合根 _register_modal 收口层级)。 # 高内聚边界:纯视图 setter 面——商品快照与余额经组合根喂入(runner.shop_products() 只读投影, # 现价/已购档数由 runner 现算),购买/离开意图经信号外发——零核心写调用、零 runner 持有。 class_name ShopOverlay extends Control signal buy_requested(product_id: String) signal leave_pressed const ATTR_LABELS := {"atk": "攻击", "max_hp": "生命上限", "armor_class": "护甲"} # 档位文案轴(数据契约 attr 词表) var _gp_label: Label var _cards_box: VBoxContainer var _card_rows: Array[Dictionary] = [] # [{product_id, price, bought, btn}]——refresh_state 就地重刷(零重建回流) func _init() -> void: var dim := ColorRect.new() dim.color = Color(0, 0, 0, 0.6) dim.set_anchors_preset(Control.PRESET_FULL_RECT) add_child(dim) var center := CenterContainer.new() center.set_anchors_preset(Control.PRESET_FULL_RECT) dim.add_child(center) var box := VBoxContainer.new() box.add_theme_constant_override("separation", 12) center.add_child(box) var title := UiKit.label("商店", 26, true) title.add_theme_color_override("font_color", UiKit.WARM) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(title) _gp_label = UiKit.label("金币 0", 18, true) _gp_label.add_theme_color_override("font_color", UiKit.GOLD) _gp_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(_gp_label) _cards_box = VBoxContainer.new() _cards_box.add_theme_constant_override("separation", 8) box.add_child(_cards_box) var leave_btn := UiKit.button("离开商店") leave_btn.pressed.connect(_emit_leave_pressed) box.add_child(leave_btn) visible = false # 打开商店(products = runner.shop_products() 投影——[{product_id, display_name, attr, flat_value, price, purchased}]) func open_shop(products: Array, gp: int) -> void: for child in _cards_box.get_children(): child.queue_free() _card_rows.clear() for p in products: _card_rows.append(_build_card(p)) refresh_state(products, gp) visible = true # 状态重刷(购买/钱包变化后调用——就地改文案与按钮禁用态,零重建) func refresh_state(products: Array, gp: int) -> void: _gp_label.text = "金币 %d" % gp var by_id := {} for p in products: by_id[String(p.product_id)] = p for card in _card_rows: var p: Dictionary = by_id.get(String(card.product_id), {}) if p.is_empty(): continue card.price.text = "现价 %d 金币" % int(p.price) card.bought.text = "已购 %d 档" % int(p.purchased) card.btn.disabled = gp < int(p.price) # 余额不足禁用置灰 # 商品卡(显示名 + 档位文案 | 现价 | 已购档数 | 购买按钮) func _build_card(p: Dictionary) -> Dictionary: var card := PanelContainer.new() card.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.BG_OVERLAY, UiKit.GOLD_DIM, 4, 1, Vector4(12, 6, 12, 6))) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 14) card.add_child(row) var name_l := UiKit.label(_card_title(p), 15, true) name_l.custom_minimum_size = Vector2(190, 0) row.add_child(name_l) var price_l := UiKit.label("", 14, false) price_l.add_theme_color_override("font_color", UiKit.WARM_SOFT) price_l.custom_minimum_size = Vector2(110, 0) row.add_child(price_l) var bought_l := UiKit.label("", 14, false) bought_l.add_theme_color_override("font_color", UiKit.WARM_FAINT) row.add_child(bought_l) var btn := UiKit.button("购买") btn.pressed.connect(_emit_buy_requested.bind(p)) row.add_child(btn) _cards_box.add_child(card) return {"product_id": String(p.product_id), "price": price_l, "bought": bought_l, "btn": btn} # 卡题 = 显示名 + 档位文案(attr →「攻击 +1」形) func _card_title(p: Dictionary) -> String: var attr := String(p.get("attr", "")) return "%s(%s %+d)" % [String(p.get("display_name", "")), String(ATTR_LABELS.get(attr, attr)), int(p.get("flat_value", 0))] func _emit_leave_pressed() -> void: leave_pressed.emit() func _emit_buy_requested(p: Dictionary) -> void: buy_requested.emit(String(p.product_id))