feat(迭代23-T6): 营地锻造工坊工作间——两区六路线行板实装(占位面板替换)+三意图信号接线

- 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 触碰加跑)
This commit is contained in:
DKChan committed 2026-10-13 00:21:41 +08:00
1 parent ed0629236a
commit 2915f19c4b
7 files changed
+298 -17

No files matched your search

+37
View File
@@ -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)。
+43 -2
View File
@@ -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:
+3
View File
@@ -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 卡④——生涯页一级入口(营地同组)
+104 -15
View File
@@ -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
+1
View File
@@ -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 半血解锁技·容器机制(门谓词/闩不可逆/双面过滤/保底不逼锁定/零扰动)
@@ -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"))
@@ -0,0 +1 @@
uid://f8k2m4qx7wvtn