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

228 lines
11 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
## 商店覆盖层(模态):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()