From 2915f19c4b87d4d92495512d693541004eed91d6 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Tue, 13 Oct 2026 00:21:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=BF=AD=E4=BB=A323-T6):=20=E8=90=A5?= =?UTF-8?q?=E5=9C=B0=E9=94=BB=E9=80=A0=E5=B7=A5=E5=9D=8A=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E9=97=B4=E2=80=94=E2=80=94=E4=B8=A4=E5=8C=BA=E5=85=AD=E8=B7=AF?= =?UTF-8?q?=E7=BA=BF=E8=A1=8C=E6=9D=BF=E5=AE=9E=E8=A3=85=EF=BC=88=E5=8D=A0?= =?UTF-8?q?=E4=BD=8D=E9=9D=A2=E6=9D=BF=E6=9B=BF=E6=8D=A2=EF=BC=89+?= =?UTF-8?q?=E4=B8=89=E6=84=8F=E5=9B=BE=E4=BF=A1=E5=8F=B7=E6=8E=A5=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MetaProgression.forge_shop_status():只读投影(等级/cap/解锁态/选中态/下级几何价/解锁价/前置通道进度——纯结构面,余额禁用归视图) - CampOverlay:_forge_workbench 占位面板→武器锻造区/护甲锻造区两区六路线行板(件名+Lv+解锁态文案+锻造/解锁/选中钮);_rebuild_forge 随 open/refresh 重建;默认不可见+门控重建=锁定态零行板零购买路径(占位纪律延续);三信号 forge_buy/unlock/select_requested(契约 9 UI 只发意图) - camp_shop_presenter:camp_data +forge 投影;on_camp_forge_buy/unlock/select 三处理器(写口唯一归 MetaProgression 锻造写口族;拒绝可见 pattern 1 toast) - main.gd:三信号接 presenter - 新单测 forge_workbench_test 12 断言(锁定态纪律/两区结构/钮态/余额禁用/满级/派生解锁出钮/三意图外发);run_all 23→24 套 - 轻闸:冒烟 938/0+run_all 24 套+probe --click 全绿(presentation 触碰加跑) --- game/core/meta_progression.gd | 37 ++++++ game/presentation/camp_shop_presenter.gd | 45 ++++++- game/presentation/main.gd | 3 + game/presentation/views/camp_overlay_view.gd | 119 +++++++++++++++--- game/tests/run_all.gd | 1 + .../unit/presentation/forge_workbench_test.gd | 109 ++++++++++++++++ .../presentation/forge_workbench_test.gd.uid | 1 + 7 files changed, 298 insertions(+), 17 deletions(-) create mode 100644 game/tests/unit/presentation/forge_workbench_test.gd create mode 100644 game/tests/unit/presentation/forge_workbench_test.gd.uid diff --git a/game/core/meta_progression.gd b/game/core/meta_progression.gd index a9139a7a..dbc001f4 100644 --- a/game/core/meta_progression.gd +++ b/game/core/meta_progression.gd @@ -232,6 +232,43 @@ func hero_armor_type() -> String: return String(row.get("type_axis", "")) +# ---- 锻造工坊柜台只读投影(迭代 23 T6——呈现层数据源:两区六路线行板;余额联动禁用归视图)---- +# 每行 {route_id, display_name, kind, type_axis, level, cap, unlocked, initial_owned, selected, +# next_price(未满级已解锁=下级几何价/否则 0), unlock_price, prereq_route, prereq_name, +# prereq_level, prereq_cap, prereq_maxed}——纯结构面,禁嵌余额判断(视图按 balance 禁用,同秘宝匣先例)。 +func forge_shop_status() -> Array: + var out: Array[Dictionary] = [] + for row in db.forge_routes(): + var rid := String(row.get("route_id", "")) + var lvl := save.forge_route_level(rid) + var cap_v := int(row.get("cap", 0)) + var unlocked := forge_route_unlocked(rid) + var prereq := String(row.get("prereq_route", "")) + var plvl := save.forge_route_level(prereq) if prereq != "" else 0 + var pcap := int(db.forge_route(prereq).get("cap", 0)) if prereq != "" else 0 + var selected := (String(row.get("kind", "")) == "weapon" and save.forge_weapon() == rid) \ + or (String(row.get("kind", "")) == "armor" and save.forge_armor() == rid) + out.append({ + "route_id": rid, + "display_name": String(row.get("display_name", "")), + "kind": String(row.get("kind", "")), + "type_axis": String(row.get("type_axis", "")), + "level": lvl, + "cap": cap_v, + "unlocked": unlocked, + "initial_owned": bool(row.get("initial_owned", false)), + "selected": selected, + "next_price": ForgeTable.level_price(row, lvl) if (unlocked and lvl < cap_v) else 0, + "unlock_price": int(row.get("unlock_price", 0)), + "prereq_route": prereq, + "prereq_name": String(db.forge_route(prereq).get("display_name", "")) if prereq != "" else "", + "prereq_level": plvl, + "prereq_cap": pcap, + "prereq_maxed": prereq != "" and plvl >= pcap, + }) + return out + + # ---- Boss 两档与收束入账(结算屏文案数据源)---- # 明细 {big_reward, boss_drops, total, new_unlocks};入账与标记经 save.apply_adventure_summary # (keystone_drops 副本随摘要入 recent_summaries)。 diff --git a/game/presentation/camp_shop_presenter.gd b/game/presentation/camp_shop_presenter.gd index b64c6857..945a6dbf 100644 --- a/game/presentation/camp_shop_presenter.gd +++ b/game/presentation/camp_shop_presenter.gd @@ -61,13 +61,15 @@ func on_camp_closed() -> void: _refresh_presentations.call() # 关闭后刷新顶栏(工坊解锁可能变了余额侧读数面) -# 营地快照投影(纯读——秘宝匣 12 件/同调装配/树面/余额/最近冒险;拼句归 CampOverlay,账面归 -# MetaProgression。属性锻造三行投影已删——迭代 21 T4 柜台退役;工坊六行换代=迭代 22 T2 秘宝匣) +# 营地快照投影(纯读——秘宝匣 12 件/同调装配/树面/锻造工坊两区六路线/余额/最近冒险;拼句归 +# CampOverlay,账面归 MetaProgression。属性锻造三行投影已删——迭代 21 T4 柜台退役;工坊六行换代 +# = 迭代 22 T2 秘宝匣;forge 键=迭代 23 T6 锻造工坊路线行板投影) func camp_data() -> Dictionary: var meta = _meta_get.call() return { "balance": meta.keystone_balance() if meta != null else 0, "workshop": meta.treasure_shop_status() if meta != null else [], # 键名沿 workshop(视图消费键不变——行内容=秘宝匣 12 件解锁投影) + "forge": meta.forge_shop_status() if meta != null else [], # 迭代 23 T6——锻造工坊两区六路线投影 "attune": meta.attune_camp_status() if meta != null else {}, # 迭代 11 第①批 T4——同调装配面投影 "tree": meta.tree_camp_rows() if meta != null else [], # 树面投影(含 forge_workshop/ks_workshop 钥匙门判据行) "fork_groups": meta.tree_fork_groups() if meta != null else [], @@ -133,6 +135,45 @@ func on_camp_fork_toggle(group: String, side: String) -> void: _refresh_presentations.call() +# ---- 锻造工坊三意图(迭代 23 T6——契约 9:写口唯一归 MetaProgression 锻造写口族;UI 只发意图)---- +# 路线购买(几何价;余额不足/未解锁/满级=核心拒绝零副作用,兜底拒绝可见 pattern 1) +func on_camp_forge_buy(route_id: String) -> void: + var meta = _meta_get.call() + if meta == null: + return + if not meta.forge_buy_level(route_id): + toast.show_toast("锻造被拒——钥石不足或路线不可锻造", "warn") + return + camp_overlay.refresh_camp(camp_data()) + _refresh_presentations.call() + + +# 一次性解锁价直购(并联双通道②;已解锁=拒绝零收费) +func on_camp_forge_unlock(route_id: String) -> void: + var meta = _meta_get.call() + if meta == null: + return + if not meta.forge_buy_unlock(route_id): + toast.show_toast("解锁被拒——钥石不足或已解锁", "warn") + return + camp_overlay.refresh_camp(camp_data()) + _refresh_presentations.call() + + +# 当前武器/甲选中切换(免费;未解锁拒选) +func on_camp_forge_select(route_id: String) -> void: + var meta = _meta_get.call() + if meta == null: + return + var ok: bool = meta.forge_select_weapon(route_id) if String(db.forge_route(route_id).get("kind", "")) == "weapon" \ + else meta.forge_select_armor(route_id) + if not ok: + toast.show_toast("切换被拒——路线未解锁", "warn") + return + camp_overlay.refresh_camp(camp_data()) + _refresh_presentations.call() + + func on_shop_buy(product_id: String) -> void: var runner = _runner_get.call() if runner == null: diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 4851e8b0..a59f046f 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -317,6 +317,9 @@ func _build_ui() -> void: camp_overlay.attune_toggled.connect(_camp_pres.on_camp_attune_toggle) camp_overlay.tree_buy_requested.connect(_camp_pres.on_camp_tree_buy) camp_overlay.tree_fork_toggled.connect(_camp_pres.on_camp_fork_toggle) + camp_overlay.forge_buy_requested.connect(_camp_pres.on_camp_forge_buy) # 迭代 23 T6——锻造三意图(写口唯一归 MetaProgression) + camp_overlay.forge_unlock_requested.connect(_camp_pres.on_camp_forge_unlock) + camp_overlay.forge_select_requested.connect(_camp_pres.on_camp_forge_select) camp_overlay.closed.connect(_camp_pres.on_camp_closed) start.campbtn.pressed.connect(_camp_pres.open_camp) start.careerbtn.pressed.connect(_open_career) # 迭代 18 卡④——生涯页一级入口(营地同组) diff --git a/game/presentation/views/camp_overlay_view.gd b/game/presentation/views/camp_overlay_view.gd index 5b62eecf..40338066 100644 --- a/game/presentation/views/camp_overlay_view.gd +++ b/game/presentation/views/camp_overlay_view.gd @@ -1,14 +1,16 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # Date: 2026-09-21(元进度实现批——营地覆盖层呈现;2026-10-12 迭代 21 T6 营地统一重排—— # 四柜台并排〔2026-10-11 锻造工坊裁决会话 A② 裁〕:树淬炼柜台=树脊柱界面形态=主界面 / -# 锻造工坊柜台〔钥匙门:forge_workshop 点亮前锁定态,点亮=工作间占位面板〕/ 秘宝匣柜台 -# 〔钥匙门:ks_workshop 点亮前锁定态,点亮=现工坊内容整体迁入——六行+同调装配段+Y 升级行〕/ +# 锻造工坊柜台〔钥匙门:forge_workshop 点亮前锁定态,点亮=工作间两区六路线行板——迭代 23 T6 实装〕/ +# 秘宝匣柜台〔钥匙门:ks_workshop 点亮前锁定态,点亮=现工坊内容整体迁入——六行+同调装配段+Y 升级行〕/ # 奖励格图鉴柜台常开独立位;宽度不足横向滚动条。旧属性锻造柜台退役〔攻防所有权切分—— # 生命唯一入口=树脊柱、攻/AC 唯一入口=锻造工坊路线〕)。 -# 占位面板硬要求(2026-10-11 用户令):锻造工坊工作间占位面板打开/关闭零副作用——零写档、 -# 零报错、面板内无任何可交互购买路径(纯文案标签,视觉终稿归 HUD 营地视觉批)。 +# 锁定态零副作用纪律(2026-10-11 用户令,延续):锻造工坊工作间锁定态(钥匙未点亮)开/关零副作用 +# ——零写档、零报错、面板内无任何可交互购买路径(锁定行纯文案;点亮后交互面归 T6 行板意图信号)。 # 信号面/投影契约:buy_requested(attr)〔现仅 Y 刷新升级行消费——装配段双入口之一 Q1 裁乙〕/closed、 -# attune_toggled/tree_buy_requested/tree_fork_toggled、open_camp(data)/refresh_camp(data)。 +# attune_toggled/tree_buy_requested/tree_fork_toggled、forge_buy_requested/forge_unlock_requested/ +# forge_select_requested(迭代 23 T6——锻造三意图;写口唯一归 MetaProgression 锻造写口族)、 +# open_camp(data)/refresh_camp(data)。 # 挂接方式:开合与开始层互斥仿设置面先例(开始覆盖层非模态且树序在后——打开本层前由组合根 # 隐藏开始层,规范 2.3 遮挡纪律)。 # 高内聚边界:纯视图 setter 面——账本快照经组合根喂入(MetaProgression 只读投影),购买/关闭 @@ -21,6 +23,9 @@ signal closed signal attune_toggled(item_id: String) # 同调装配切换意图(迭代 11 第①批 T4——写口唯一归 MetaProgression.toggle_attunement) signal tree_buy_requested(node_id: String) # 天赋树购买意图(迭代 12 T9——写口唯一归 MetaProgression.tree_buy) signal tree_fork_toggled(group: String, side: String) # 双态切换意图(一节点双态——零币切态) +signal forge_buy_requested(route_id: String) # 锻造路线购买意图(迭代 23 T6——写口唯一归 MetaProgression.forge_buy_level) +signal forge_unlock_requested(route_id: String) # 锻造路线解锁意图(迭代 23 T6——写口唯一归 MetaProgression.forge_buy_unlock) +signal forge_select_requested(route_id: String) # 锻造路线选中意图(迭代 23 T6——写口唯一归 MetaProgression.forge_select_weapon/armor) # 方向稿取色(柜台头带深棕/行板暖米;双色墨=亮板配深墨)——值单点收 UiKit(T4 裸色收口批) const HEADER_BAND_COL := UiKit.CAMP_HEADER_BAND @@ -31,7 +36,10 @@ var _tree_holder: VBoxContainer # 树淬炼柜台内容(主界面—— var _tree_buy_btns: Array[Dictionary] = [] # [{node_id, btn}]——树购买/解锁钮登记面(open/refresh 重建时刷新;探针消费) var _tree_fork_btns: Array[Dictionary] = [] # [{group, side, btn}]——Fork 双态切换钮登记面(探针消费) var _forge_lock_l: Label # 锻造工坊柜台锁定态行(钥匙未点亮) -var _forge_workbench: VBoxContainer # 锻造工坊工作间占位面板(点亮后展开——零交互纯文案) +var _forge_workbench: VBoxContainer # 锻造工坊工作间(钥匙点亮后展开——两区六路线行板,迭代 23 T6 实装) +var _forge_zone_weapon: VBoxContainer # 武器锻造区行板容器(三路线) +var _forge_zone_armor: VBoxContainer # 护甲锻造区行板容器(三路线) +var _forge_rows: Array[Dictionary] = [] # [{route_id, name_l, lvl_l, status_l, buy_btn, unlock_btn, select_btn, next_price, unlock_price}](open/refresh 重建;探针消费) var _ws_scroll: ScrollContainer # 秘宝匣柜台内容容器(钥匙点亮后可见) var _ws_lock_l: Label # 秘宝匣柜台锁定态行 var _workshop_holder: VBoxContainer # 秘宝匣柜台行板(六行+同调装配段+Y 升级行——迭代 11 第①批 T4 结构随迁) @@ -112,20 +120,23 @@ func _init() -> void: tree_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL mid.add_child(tree_panel) - # 柜二·锻造工坊(钥匙门——forge_workshop 点亮前锁定态;点亮=工作间占位面板,零交互) + # 柜二·锻造工坊(钥匙门——forge_workshop 点亮前锁定态;点亮=工作间两区六路线行板,迭代 23 T6 实装) _forge_lock_l = UiKit.label("🔒 锻造工坊未开放\n\n点亮树上「锻造工坊」钥匙节点后展开工作间。\n(钥匙免费、无前置——树上直接点亮)",UiKit.FS_HINT, false, UiKit.STONE_DEEP) _forge_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _forge_lock_l.custom_minimum_size = Vector2(280, 0) _forge_workbench = VBoxContainer.new() _forge_workbench.add_theme_constant_override("separation", 6) - var wb_hint := UiKit.label("工作间两区并列:武器锻造区 / 护甲锻造区",UiKit.FS_HINT, true, UiKit.INK) - wb_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - wb_hint.custom_minimum_size = Vector2(280, 0) - _forge_workbench.add_child(wb_hint) - var wb_body := UiKit.label("武器/护甲路线升级随「锻造工坊实装批」开放——本批为占位面板,无任何购买入口。",UiKit.FS_HINT, false, UiKit.STONE_DEEP) - wb_body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - wb_body.custom_minimum_size = Vector2(280, 0) - _forge_workbench.add_child(wb_body) + _forge_workbench.visible = false # 默认锁定态(钥匙门未判前零行板——门开才建,迭代 23 T6 占位纪律延续) + _forge_zone_weapon = VBoxContainer.new() + _forge_zone_weapon.add_theme_constant_override("separation", 4) + _forge_zone_armor = VBoxContainer.new() + _forge_zone_armor.add_theme_constant_override("separation", 4) + var wb_wh := UiKit.label("── 武器锻造区 ──",UiKit.FS_HINT, true, UiKit.GOLD_INK) # 尾裁微批a 同款深金墨(对柜台板 5.21:1) + _forge_workbench.add_child(wb_wh) + _forge_workbench.add_child(_forge_zone_weapon) + var wb_ah := UiKit.label("── 护甲锻造区 ──",UiKit.FS_HINT, true, UiKit.GOLD_INK) + _forge_workbench.add_child(wb_ah) + _forge_workbench.add_child(_forge_zone_armor) var forge_holder := VBoxContainer.new() forge_holder.add_theme_constant_override("separation", 6) forge_holder.add_child(_forge_lock_l) @@ -284,10 +295,87 @@ func _refresh_key_gates(data: Dictionary) -> void: _ws_scroll.visible = ws_open +# 锻造工坊行板重建(迭代 23 T6——两区六路线;open_camp/refresh_camp 调用。行结构仿秘宝匣行板: +# 件名+等级+解锁态+购买/解锁/选中钮。UI 只发意图信号〔契约 9——零核心写调用、零 MetaProgression +# 持有〕;钮禁用=常态防线(余额/未解锁/满级/已选中),兜底拒绝可见归 presenter pattern 1) +func _rebuild_forge(data: Dictionary) -> void: + for zone in [_forge_zone_weapon, _forge_zone_armor]: + for c in zone.get_children(): + c.queue_free() + _forge_rows.clear() + if not _forge_workbench.visible: + return # 锁定态(钥匙未点亮):零行板零购买路径——占位纪律延续(2026-10-11 用户令) + var bal := int(data.get("balance", 0)) + for row in data.get("forge", []): + var rid := String(row.get("route_id", "")) + var kind := String(row.get("kind", "")) + var zone: VBoxContainer = _forge_zone_weapon if kind == "weapon" else _forge_zone_armor + var unlocked := bool(row.get("unlocked", false)) + var initial := bool(row.get("initial_owned", false)) + var lvl := int(row.get("level", 0)) + var cap_v := int(row.get("cap", 0)) + var next_price := int(row.get("next_price", 0)) + var unlock_price := int(row.get("unlock_price", 0)) + var card := PanelContainer.new() + card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4))) + var line := HBoxContainer.new() + line.add_theme_constant_override("separation", 6) + card.add_child(line) + var name_l := UiKit.label(String(row.get("display_name", "")), UiKit.FS_HINT, true, UiKit.INK) + name_l.custom_minimum_size = Vector2(104, 0) + name_l.clip_text = true + name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + line.add_child(name_l) + var lvl_l := UiKit.label("Lv%d/%d" % [lvl, cap_v], 13, false, UiKit.INK_LABEL) + lvl_l.custom_minimum_size = Vector2(44, 0) + line.add_child(lvl_l) + # 解锁态/解锁通道呈现(并联双通道:练满前一档 or 一次性解锁价直购) + var st_text := "已解锁" + if not unlocked: + if initial: + st_text = "未解锁" + else: + st_text = "未解锁 · 练满%s 或 %d 钥石直购" % [String(row.get("prereq_name", "")), unlock_price] + elif lvl >= cap_v: + st_text = "已满级" + var status_l := UiKit.label(st_text, UiKit.FS_HINT, false, UiKit.STONE_DEEP) + status_l.clip_text = true + status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL + line.add_child(status_l) + # 购买钮(已解锁未满级——几何价显示;余额不足禁用) + var buy_btn: Button = null + if unlocked and lvl < cap_v: + buy_btn = UiKit.button("锻造 %d" % next_price, 0.0) # 行板紧凑钮豁免 + buy_btn.pressed.connect(forge_buy_requested.emit.bind(rid)) + buy_btn.disabled = bal < next_price + line.add_child(buy_btn) + # 解锁钮(待解锁非初始——一次性解锁价) + var unlock_btn: Button = null + if not unlocked and not initial: + unlock_btn = UiKit.button("解锁 %d" % unlock_price, 0.0) # 行板紧凑钮豁免 + unlock_btn.pressed.connect(forge_unlock_requested.emit.bind(rid)) + unlock_btn.disabled = bal < unlock_price + line.add_child(unlock_btn) + # 选中钮(已解锁——免费切换;当前=禁用态标记) + var select_btn: Button = null + if unlocked: + var sel := bool(row.get("selected", false)) + select_btn = UiKit.button("当前" if sel else "选中", 0.0) # 行板紧凑钮豁免 + select_btn.pressed.connect(forge_select_requested.emit.bind(rid)) + select_btn.disabled = sel + line.add_child(select_btn) + zone.add_child(card) + _forge_rows.append({"route_id": rid, "name_l": name_l, "lvl_l": lvl_l, "status_l": status_l, + "buy_btn": buy_btn, "unlock_btn": unlock_btn, "select_btn": select_btn, + "next_price": next_price, "unlock_price": unlock_price}) + + # 打开营地(全量重建四柜台行 + 同调装配段 + 最近冒险;树段与钥匙门随 refresh_camp 重建—— # 数据 = 组合根 _camp_data() 投影) func open_camp(data: Dictionary) -> void: _rebuild_workshop(data) # 段尾链 _rebuild_attune(同调装配段+Y 升级行随秘宝匣柜台内容) + _rebuild_forge(data) # 迭代 23 T6——锻造工坊两区六路线行板 _rebuild_recent(data) refresh_camp(data) visible = true @@ -300,6 +388,7 @@ func refresh_camp(data: Dictionary) -> void: _balance_label.text = "钥石 %d" % balance _rebuild_tree(data) _refresh_key_gates(data) + _rebuild_forge(data) # 迭代 23 T6——行板随刷重建(购买/解锁/选中后等级/价/禁用态同步) var ws_by_id := {} for row in data.get("workshop", []): ws_by_id[String(row.get("item_id", ""))] = row diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 278b6079..7299f26d 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -38,6 +38,7 @@ func _suites() -> Array: {"path": "res://tests/unit/core/curio_refresh_paid_test.gd", "expect": 0}, # 2026-10-06——三选一刷新免费双源+GP 付费档(对齐商店 MC-1:价曲线/优先序/停点清零/钱包单口) {"path": "res://tests/unit/presentation/pacing_queue_test.gd", "expect": 0}, # 迭代 8 卡⑦ AC⑤/T9——排空零分叉/零丢行/延迟双态 {"path": "res://tests/unit/presentation/reward_overlay_test.gd", "expect": 0}, # 迭代 7 hotfix——奖励节点 UI 直验(盲区补漏) + {"path": "res://tests/unit/presentation/forge_workbench_test.gd", "expect": 0}, # 迭代 23 锻造工坊批 T6——工作间两区六路线 UI 直验(V13 自动化前置面) {"path": "res://tests/unit/battle_settlement/damage_math_test.gd", "expect": 0}, {"path": "res://tests/unit/battle_settlement/damage_controller_test.gd", "expect": 0}, {"path": "res://tests/unit/battle_settlement/skill_unlock_test.gd", "expect": 0}, # 迭代14④ T6——Boss 半血解锁技·容器机制(门谓词/闩不可逆/双面过滤/保底不逼锁定/零扰动) diff --git a/game/tests/unit/presentation/forge_workbench_test.gd b/game/tests/unit/presentation/forge_workbench_test.gd new file mode 100644 index 00000000..b1b95e38 --- /dev/null +++ b/game/tests/unit/presentation/forge_workbench_test.gd @@ -0,0 +1,109 @@ +extends SceneTree +# 锻造工坊工作间 UI 直验套件(迭代 23 T6——V13 人闸的自动化前置面:两区结构/钮态/意图信号/锁定态纪律) +# 覆盖:锁定态零行板零购买路径(占位纪律延续)· 两区各三路线 · 初始持有行购买钮(几何价 20)· +# 待解锁行解锁钮(200)+无购买钮 · 选中钮当前态禁用 · 余额不足禁用态 · 满级无购买钮 · +# 三意图信号外发(UI 只发意图——契约 9,写口归 MetaProgression 由冒烟侧覆盖)。 +# 运行:godot --headless --path game --script res://tests/unit/presentation/forge_workbench_test.gd(expect 0) +const Kit := preload("res://tests/unit/test_kit.gd") + +const _TEST_SAVE := "user://test_forge_workbench.json" + + +func _initialize() -> void: + call_deferred("_run") + + +func _row_by_id(ov) -> Dictionary: + var out := {} + for r in ov._forge_rows: + out[String(r.route_id)] = r + return out + + +func _run() -> void: + var db = load("res://core/config_db.gd").new() + var load_ok: bool = db.load_tables("res://data/tables", true) + if not load_ok: + Kit.check("ConfigDb 加载(工作间测试夹具前提)", false) + quit(Kit.finish("forge_workbench_test")) + return + var sv = load("res://core/save_manager.gd").new(_TEST_SAVE) + sv.load_from_disk() + var mp = load("res://core/meta_progression.gd").new(db, sv) + var ov = load("res://presentation/views/camp_overlay_view.gd").new() + root.add_child(ov) + var buys := [] + var unlocks := [] + var selects := [] + ov.forge_buy_requested.connect(func(rid): buys.append(rid)) + ov.forge_unlock_requested.connect(func(rid): unlocks.append(rid)) + ov.forge_select_requested.connect(func(rid): selects.append(rid)) + + # ① 锁定态:树钥匙未点亮 → 工作间不可见、两区零行板(无任何可交互购买路径) + ov.open_camp({"balance": 999, "forge": mp.forge_shop_status(), "tree": []}) + Kit.check("锁定态:工作间不可见(forge_workshop 钥匙未点亮)〔锻造工坊批 V13〕", + not ov._forge_workbench.visible and ov._forge_lock_l.visible) + Kit.check("锁定态:两区零行板(占位纪律——零购买路径)〔锻造工坊批 V13〕", + ov._forge_zone_weapon.get_child_count() == 0 and ov._forge_zone_armor.get_child_count() == 0) + + # ② 解锁态:钥匙点亮 → 两区各三路线 + ov.open_camp({"balance": 500, "forge": mp.forge_shop_status(), + "tree": [{"node_id": "forge_workshop", "level": 1}]}) + Kit.check("解锁态:工作间可见(钥匙点亮展开)〔锻造工坊批 V13〕", ov._forge_workbench.visible) + Kit.check("两区各三路线(武器锻造区 3 + 护甲锻造区 3)〔锻造工坊批 V13〕", + ov._forge_zone_weapon.get_child_count() == 3 and ov._forge_zone_armor.get_child_count() == 3) + var rows: Dictionary = _row_by_id(ov) + Kit.check("六路线行册恰全(sword/spear/staff/light/heavy/ward)〔锻造工坊批 V13〕", + rows.has("sword") and rows.has("spear") and rows.has("staff") + and rows.has("light") and rows.has("heavy") and rows.has("ward")) + + # ③ 初始持有行(长剑/轻甲):购买钮现形带几何价、选中钮=当前禁用 + var rsw = rows.get("sword", {}) + var rli = rows.get("light", {}) + Kit.check("长剑行:购买钮「锻造 20」(0 级位几何价)+选中钮「当前」禁用〔锻造工坊批 V13〕", + rsw.get("buy_btn") != null and String(rsw.buy_btn.text) == "锻造 20" and not rsw.buy_btn.disabled + and rsw.get("select_btn") != null and String(rsw.select_btn.text) == "当前" and rsw.select_btn.disabled) + Kit.check("轻甲行:购买钮「锻造 20」+选中钮「当前」禁用〔锻造工坊批 V13〕", + rli.get("buy_btn") != null and String(rli.buy_btn.text) == "锻造 20" + and String(rli.select_btn.text) == "当前" and rli.select_btn.disabled) + + # ④ 待解锁行(长枪/法杖/重甲/魔法护甲):解锁钮 200、无购买钮、无选中钮、状态含双通道文案 + var rsp = rows.get("spear", {}) + var rwa = rows.get("ward", {}) + Kit.check("长枪行:解锁钮「解锁 200」+无购买钮+无选中钮〔锻造工坊批 V13〕", + rsp.get("unlock_btn") != null and String(rsp.unlock_btn.text) == "解锁 200" and not rsp.unlock_btn.disabled + and rsp.get("buy_btn") == null and rsp.get("select_btn") == null) + Kit.check("魔法护甲行:解锁钮「解锁 200」+状态含并联双通道文案(练满轻甲锻造 或 200 钥石直购)〔锻造工坊批 V13〕", + rwa.get("unlock_btn") != null and String(rwa.status_l.text).contains("练满轻甲锻造") + and String(rwa.status_l.text).contains("200")) + + # ⑤ 余额不足禁用态(常态防线) + ov.refresh_camp({"balance": 0, "forge": mp.forge_shop_status(), + "tree": [{"node_id": "forge_workshop", "level": 1}]}) + rows = _row_by_id(ov) + Kit.check("余额 0:长剑购买钮禁用+长枪解锁钮禁用(余额联动常态防线)〔锻造工坊批 V13〕", + rows.sword.buy_btn.disabled and rows.spear.unlock_btn.disabled) + + # ⑥ 满级行:无购买钮、状态=已满级(直置存档域构造——两分法调试口径) + sv.set_forge_route_level("sword", 5) + ov.refresh_camp({"balance": 500, "forge": mp.forge_shop_status(), + "tree": [{"node_id": "forge_workshop", "level": 1}]}) + rows = _row_by_id(ov) + Kit.check("长剑满级:无购买钮+状态「已满级」+长枪派生解锁出解锁钮消失(练满通道)〔锻造工坊批 V13〕", + rows.sword.get("buy_btn") == null and String(rows.sword.status_l.text) == "已满级" + and rows.spear.get("unlock_btn") == null and rows.spear.get("buy_btn") != null) + + # ⑦ 三意图信号外发(UI 只发意图——按真实按钮路径;选中钮直发信号面) + buys.clear(); unlocks.clear(); selects.clear() + rows.spear.buy_btn.pressed.emit() # 长枪已派生解锁——真实按钮路径 + rows.heavy.unlock_btn.pressed.emit() # 重甲待解锁——真实按钮路径 + ov.forge_select_requested.emit("spear") # 选中意图(当前行选中钮禁用态——信号面直发) + Kit.check("三意图信号外发(buy=spear/unlock=heavy/select=spear——UI 只发意图)〔锻造工坊批 V13〕", + buys == ["spear"] and unlocks == ["heavy"] and selects == ["spear"]) + + # 尾清(测试文件绝不残留) + ov.queue_free() + for suffix in ["", ".tmp", ".corrupt.bak"]: + if FileAccess.file_exists(_TEST_SAVE + suffix): + DirAccess.remove_absolute(_TEST_SAVE + suffix) + quit(Kit.finish("forge_workbench_test")) diff --git a/game/tests/unit/presentation/forge_workbench_test.gd.uid b/game/tests/unit/presentation/forge_workbench_test.gd.uid new file mode 100644 index 00000000..b32cca60 --- /dev/null +++ b/game/tests/unit/presentation/forge_workbench_test.gd.uid @@ -0,0 +1 @@ +uid://f8k2m4qx7wvtn