228 lines
11 KiB
GDScript
228 lines
11 KiB
GDScript
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||
## 商店覆盖层(模态):legacy 固定三件面与 pool 共享池货架面双承载,购买/离开/刷新意图经信号外发。
|
||
## 商品/货架快照与余额经组合根投喂(现价=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": "行动点", "heal": "回复", "feat_ticket": "专长凭证"} # 货架件值文案轴(共享池 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 _visit_label: Label # T6 爬价规则行(demo .svisit——已购件数/现价倍数由 presenter 投喂)
|
||
var _cards_box: VBoxContainer
|
||
var _foot_hint: Label # T6 底注(demo .shop-foot .hint)
|
||
var _card_rows: Array[Dictionary] = [] # [{product_id, price, bought, btn}]——refresh_state 就地重刷(零重建回流)
|
||
var _shelf_rows: Array[Dictionary] = [] # [{kind_id, btn}]——pool 面卡片(买后整架重建=重抽反馈)
|
||
|
||
|
||
func _init() -> void:
|
||
# 全屏页形态(demo ⑤ #v-shop——06 在册 D2 商店全屏化/D1 竖卡
|
||
# 232×352 落地)——暖亮底+石纹弱质感+暗角(UiKit.page_shell)+56px 通栏顶条(标题+刷新图标钮/
|
||
# 角标+GP 框)+ 爬价规则行 + bcard 三竖卡居中 + 底部离开钮与池源注记。
|
||
# M6 遗产:库存余量退役(关卡节点地图 v1.5-a 用户裁);刷新钮呈现位照摆(机制=MC-1 投喂)。
|
||
# legacy 面(open_shop)行卡同壳呈现(shop=off 对照承载原样)。高内聚边界不变:纯视图 setter 面。
|
||
var page: Dictionary[Variant, Variant] = UiKit.page_shell(self, true)
|
||
var top: HBoxContainer = page.top_row
|
||
top.add_child(UiKit.label("商 店",UiKit.FS_TITLE, true, UiKit.WARM))
|
||
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",UiKit.FS_CAPTION, 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,UiKit.RADIUS_OBJECT, 1, Vector4(12, 4, 12, 4)))
|
||
top.add_child(gp_chip)
|
||
_gp_label = UiKit.label("GP 0",UiKit.FS_BODY, true, UiKit.INK_ON_GOLD)
|
||
gp_chip.add_child(_gp_label)
|
||
var box: VBoxContainer = page.box
|
||
_visit_label = UiKit.label("",UiKit.FS_BODY, false, UiKit.WARM_SOFT)
|
||
_visit_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
box.add_child(_visit_label)
|
||
_cards_box = VBoxContainer.new()
|
||
_cards_box.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
_cards_box.alignment = BoxContainer.ALIGNMENT_CENTER # 卡片区垂直居中
|
||
_cards_box.add_theme_constant_override("separation", 8)
|
||
box.add_child(_cards_box)
|
||
var foot := HBoxContainer.new()
|
||
foot.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
foot.add_theme_constant_override("separation", 16)
|
||
box.add_child(foot)
|
||
var leave_btn := UiKit.button("离开商店")
|
||
leave_btn.pressed.connect(_emit_leave_pressed)
|
||
foot.add_child(leave_btn)
|
||
_foot_hint = UiKit.label("货架与奖励三选一同一池 · 池空即空架关店",UiKit.FS_HINT, false, UiKit.WARM_FAINT)
|
||
foot.add_child(_foot_hint)
|
||
visible = false
|
||
|
||
|
||
# 爬价规则行投喂(demo .svisit——buys<0 = legacy 面隐藏行;mult=pow(price_a, buys) 由 presenter 算)
|
||
func set_visit_info(buys: int, price_a: float) -> void:
|
||
if buys < 0:
|
||
_visit_label.text = ""
|
||
return
|
||
var mult := pow(price_a, float(buys))
|
||
_visit_label.text = "本次进店已购 %d 件 · 现价 %s | 买 1 件 → 三件全换新 · 全场涨价 ×%s" % [
|
||
buys, _xnum(mult), _xnum(price_a)]
|
||
|
||
|
||
func _xnum(v: float) -> String:
|
||
return "×%d" % int(round(v)) if absf(v - roundf(v)) < 0.005 else "×%.2f" % v
|
||
|
||
|
||
# 打开商店(products = runner.shop.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 = "GP %d" % gp
|
||
var by_id := {}
|
||
for p in products:
|
||
by_id[String(p.product_id)] = p
|
||
for card in _card_rows:
|
||
var p: Dictionary[Variant, Variant] = by_id.get(String(card.product_id), {})
|
||
if p.is_empty():
|
||
continue
|
||
card.price.text = "现价 %d GP" % 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,UiKit.RADIUS_OBJECT, 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),UiKit.FS_BODY, true, UiKit.WARM_SOFT) # 显式传墨:bold 缺省 INK_LABEL 压暗底行卡 1.12:1 越带,换暖白次级 9.53:1
|
||
name_l.custom_minimum_size = Vector2(190, 0)
|
||
row.add_child(name_l)
|
||
var price_l := UiKit.label("",UiKit.FS_HINT, false, UiKit.WARM_SOFT)
|
||
price_l.custom_minimum_size = Vector2(110, 0)
|
||
row.add_child(price_l)
|
||
var bought_l := UiKit.label("",UiKit.FS_HINT, 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.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", 26) # demo .cards gap 26(T6 对齐)
|
||
_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 份)",UiKit.FS_HINT, 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 = "GP %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 GP(provisional)\n池源=共享池,与奖励三选一同源" % [free_left, gp_cost]
|
||
_refresh_btn.disabled = not can_afford and free_left <= 0
|
||
|
||
|
||
# 货架卡(bcard 竖卡:类型色带+图标+名+件值描述+价签+购买钮——M6 同构卡;demo ⑤)
|
||
# 库存余量不再呈现(关卡节点地图 v1.5-a)——数据层 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 body := ""
|
||
if kind == "feat_ticket":
|
||
body = "开一次专长三选一"
|
||
else:
|
||
body = "%s %+d(本冒险)" % [String(KIND_LABELS.get(kind, kind)), int(c.get("value", 0))]
|
||
var card := UiKit.bcard(String(c.get("display_name", kind)), body,
|
||
UiKit.tg_band(kl), icon_key, # 类型色带单点(N4c/Q4 甲——货架与三选一同源判定,原固定默认带)
|
||
{"price": "%d ◈" % int(c.get("price", 0)), "footnote": 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()
|