Files
gplan-2/game/presentation/views/shop_overlay_view.gd
T
DKChan 32fdf957fd refactor(代码术语): 卡① flag→seal 全族换代+game/ 禁用词清扫(迭代 31 卡①)
标识符面:flag_*→seal_ 族(含 Godot 信号 flag_judged→seal_judged 与连接/回调同名、
FACTS 两键 flags_count→seals_count/dug_under_flag→dug_under_seal 全链、
.tres 键名与 schema .gd 同批同步);显式豁免零改动=size_flags_*(Godot 布局属性)+_fatal_flag。
中文面:金币→GP(金币袋→GP 袋)/插旗→封印/雷格→危险格/惩罚格→危险格/旗→封印·封印印记族,
game/ 内 *.gd/.tres/.md/.json 四类禁用词与 flag 双双零命中(V1/V4)。
守卫字面量不动(被检查的禁字不是使用):smoke_all V21 零「雷/旗」检查面、
ui_font_asset_test 字形集攻/雷/旗/封 保持原断言逻辑。
代拍板:板面单字槽 glyph「旗」→「封」(禁字纪律+单字位宽,消费面四处同名随动)。
素材换代(2026-10-18 用户裁案甲):assets/art/board/flag_seal.png→seal_mark.png(素材不入 git,
盘上改名+导入重刷),代码两处 res:// 字符串+COL_FLAG/CURSOR_FLAG 常量同步,
闸件四处随批(run_all_gates.sh/render_gate.sh/golden_baseline_refresh.sh/path_check.py)。
anchor.json 键名手改,重锚与改前改后 diff 随本卡收口(同裁案甲)。

基线读数(改前,主区 ed2dfaf6):冒烟 1151 检查 0 失败/单元 34 套全绿/探针 --click
存量红仅 screen_flow_probe(user:// 断点污染在册已知项)。
改后同批读数(本 worktree):冒烟 1151 检查 0 失败/探针 --click 同一存量红、其余全绿。
2026-10-19 02:52:38 +08:00

224 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": "行动点"} # 货架件值文案轴(共享池 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:
# T6(UI 设计对齐批 2026-10-06):全屏页形态(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 # 迭代 26 T5③ 卡片区垂直居中(旧默认顶对齐=卡片堆上半、下半空)
_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_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) # 尾裁微批a 2026-10-08——显式传墨: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_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 用户裁(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))],
UiKit.tg_band(kl), icon_key, # 类型色带单点(N4c/Q4 甲——货架与三选一同源判定,原固定默认带)
"%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()