Files
gplan-2/game/presentation/views/shop_overlay_view.gd
T

204 lines
9.6 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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("", 0.0) # 图标钮自设 44×44——豁免宽度基准
_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("购买", 0.0) # 行卡紧凑钮豁免宽度基准
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()