# VERTICAL SLICE - NOT FOR PRODUCTION # Date: 2026-09-21(GP+商店房实施批——商店房覆盖层呈现) # 商店覆盖层(模态):标题「商店」/ 金币余额 / 商品卡陈列 / 购买按钮(余额不足禁用置灰)/ 离开按钮。 # 模态挂接方式仿既有覆盖层(legend_overlay 现成参照——dim + CenterContainer + box,组合根 # _register_modal 收口层级)。 # 双面承载(M-4 迭代 11 第②批 T4):legacy 面 = 三商品卡全陈列(open_shop/refresh_state——shop=off # 对照承载原样保留);pool 面 = 共享池货架三选一同构卡(open_shelf——进店抽 3 + 买 1 全重抽反馈 + # 库存余量就近呈现,买后组合根重投喂货架整架重建)。高内聚边界:纯视图 setter 面——商品/货架快照 # 与余额经组合根喂入(runner 投影现算现价,视图零复算定价公式),购买/离开意图经信号外发—— # 零核心写调用、零 runner 持有。 class_name ShopOverlay extends Control signal buy_requested(product_id: String) # legacy=product_id / pool=kind_id(组合根按模式分发) signal leave_pressed const ATTR_LABELS := {"atk": "攻击", "max_hp": "生命上限", "armor_class": "护甲"} # 档位文案轴(数据契约 attr 词表) const KIND_LABELS := {"attack": "攻击", "max_hp": "生命上限", "armor": "护甲", "ap": "行动点"} # 货架件值文案轴(共享池 kind 词表——迭代 11 第②批) var _gp_label: Label var _refresh_btn: Button # M6 呈现位——MC-1 起由 set_refresh_display 投喂启用 signal refresh_requested # MC-1:刷新意图(pool 面;组合根接 presenter.on_shop_refresh) var _cards_box: VBoxContainer var _card_rows: Array[Dictionary] = [] # [{product_id, price, bought, btn}]——refresh_state 就地重刷(零重建回流) var _shelf_rows: Array[Dictionary] = [] # [{kind_id, btn}]——pool 面卡片(买后整架重建=重抽反馈) func _init() -> void: # M6(UI 落码批):pool 面卡面 bcard 三竖卡同构化(demo ⑤/2026-10-02 同构裁决呈现侧闭环)—— # 库存余量退役(关卡节点地图 v1.5-a 用户裁);刷新钮呈现位照摆(机制=MC-1 核心批投喂,占位禁用); # 爬价总览行不摆=已购次数无成文出口(随 MC-1 shop_draw 投影补)。legacy 面(open_shop)原样保留。 var box: VBoxContainer = UiKit.modal_shell(self, Color(0, 0, 0, 0.6)).box var top := HBoxContainer.new() top.add_theme_constant_override("separation", 12) box.add_child(top) var title := UiKit.label("商 店", 26, true, UiKit.WARM) top.add_child(title) var top_sp := Control.new() top_sp.size_flags_horizontal = Control.SIZE_EXPAND_FILL top.add_child(top_sp) _refresh_btn = UiKit.button("") _refresh_btn.icon = UiKit.load_tex(UiKit.TEX_ICON_REFRESH) _refresh_btn.custom_minimum_size = Vector2(44, 44) _refresh_btn.disabled = true # MC-1 投喂后启用(set_refresh_display) _refresh_btn.pressed.connect(_emit_refresh_requested) top.add_child(_refresh_btn) var stock_badge := UiKit.label("0", 12, true, UiKit.INK_ON_GOLD) stock_badge.mouse_filter = Control.MOUSE_FILTER_IGNORE stock_badge.set_anchors_preset(Control.PRESET_TOP_RIGHT) stock_badge.offset_left = -22.0 stock_badge.offset_top = -6.0 _refresh_btn.add_child(stock_badge) var gp_chip := PanelContainer.new() gp_chip.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.CANDLE, UiKit.CAMP_HEADER_BAND, 2, 1, Vector4(12, 4, 12, 4))) top.add_child(gp_chip) _gp_label = UiKit.label("金币 0", 16, true, UiKit.INK_ON_GOLD) gp_chip.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, UiKit.WARM_SOFT) price_l.custom_minimum_size = Vector2(110, 0) row.add_child(price_l) var bought_l := UiKit.label("", 14, false, 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)) # ---- 共享池货架面(M-4 迭代 11 第②批 T4——P3 甲程序占位,三选一同构卡;UiKit 既有件)---- # 打开/重刷货架(cards = runner.shop_shelf() 投影——[{kind_id, display_name, value, price, stock_left}])。 # 买后组合根重投喂新货架 = 整架重建呈现(三件全重抽反馈——未买不保留);空架 = 池空硬闸合法态。 func open_shelf(cards: Array, gp: int) -> void: for child in _cards_box.get_children(): child.queue_free() _shelf_rows.clear() var row := HBoxContainer.new() row.alignment = BoxContainer.ALIGNMENT_CENTER row.add_theme_constant_override("separation", 20) _cards_box.add_child(row) for c in cards: _shelf_rows.append(_build_shelf_card(row, c)) if cards.is_empty(): var empty := UiKit.label("货架售罄——共享池已买空(属性件全冒险共 3+3+3 份 / AP 件 2 份)", 14, false, UiKit.WARM_FAINT) empty.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER row.add_child(empty) refresh_shelf_state(gp) visible = true # 货架余额重刷(就地改禁用态——买后重建路径经 open_shelf 覆盖,本口供 wallet 变化联动) func refresh_shelf_state(gp: int) -> void: _gp_label.text = "金币 %d" % gp for card in _shelf_rows: card.btn.disabled = gp < int(card.price) card.card.modulate = Color(1, 1, 1, 1) if gp >= int(card.price) else Color(1, 1, 1, 0.55) if _refresh_btn != null and not _refresh_btn.disabled: _refresh_btn.disabled = gp < _refresh_gp_cost # MC-1 刷新呈现投喂(free_left=天赋位免费剩余[基础 0]/gp_cost=本次 GP 刷新价/can_afford=余额可付) var _refresh_gp_cost := 0 func set_refresh_display(free_left: int, gp_cost: int, can_afford: bool) -> void: _refresh_gp_cost = gp_cost for lab in _refresh_btn.get_children(): if lab is Label: (lab as Label).text = str(free_left) _refresh_btn.tooltip_text = "刷新货架——换一批商品\n免费 %d 次(天赋增益位,基础 0)\nGP 刷新 %d 金币(provisional)\n池源=共享池,与奖励三选一同源" % [free_left, gp_cost] _refresh_btn.disabled = not can_afford and free_left <= 0 # 货架卡(bcard 竖卡:类型色带+图标+名+件值描述+价签+购买钮——M6 同构卡;demo ⑤) # 库存余量退役=关卡节点地图 v1.5-a 用户裁(2026-10-04)——数据层 stock_left 仍投喂断点,仅呈现不摆 func _build_shelf_card(holder: Control, c: Dictionary) -> Dictionary: var kind := String(c.get("kind_id", "")) var icon_key := UiKit.TEX_ICON_SWORD var kl := kind.to_lower() if kl.contains("hp") or kl.contains("heal"): icon_key = UiKit.TEX_ICON_HEART elif kl.contains("armor") or kl.contains("shield"): icon_key = UiKit.TEX_ICON_SHIELD elif kl.contains("ap") or kl.contains("action") or kl.contains("pouch"): icon_key = UiKit.TEX_ICON_POUCH var card := UiKit.bcard(String(c.get("display_name", kind)), "%s %+d(本冒险)" % [String(KIND_LABELS.get(kind, kind)), int(c.get("value", 0))], Color8(194, 178, 100), icon_key, "%d ◈" % int(c.get("price", 0)), String(KIND_LABELS.get(kind, kind))) var btn := UiKit.button("购买") btn.pressed.connect(_emit_shelf_buy.bind(kind)) card.add_footer(btn) holder.add_child(card) return {"kind_id": kind, "price": int(c.get("price", 0)), "btn": btn, "card": card} func _emit_shelf_buy(kind: String) -> void: buy_requested.emit(kind) func _emit_refresh_requested() -> void: refresh_requested.emit()