merge: 迭代23锻造工坊实装批——新表 forge.tres 六路线行族+两根类型轴所有权移交(hero.tres 两轴删除)+写口族+存档 v7 forge_routes+注入链+营地工作间两区 UI(T0-T7 全量交付;worktree g-plan2-iter23-forge 十提交含 master 并入解冲突;四闸全绿=冒烟 938/0+run_all 24 套+probe click/full+MC 同种子配对 80 局零漂移;实现报告=design/qa/iter23-实现报告-2026-10-12.md;残留人闸=实机目检工作间形态与购买交互〔07㉒〕;代拍板追认单九条+GDD 反哺点三条待用户)

This commit is contained in:
DKChan committed 2026-10-13 01:41:33 +08:00
commit e8b3432cc1
29 files changed
+1361 -83

No files matched your search

+13 -9
View File
@@ -17,21 +17,21 @@
## 当前状态快照(收口批先更新本节,再追加状态记录;≤10 行)
- **更新于**:2026-10-12(active.md 状态记录清理批——完结条目 462-468 与锻造澄清 469 共八条删除,结论均在快照/06/git 史;迭代 21 开工条保留在飞。快照事实面仍反映迭代 20 总收口后现状)。
- **批末闸全绿**:冒烟 864/0+run_all 21 套+probe --click/--full+render_gate+doc 三闸零新增+熵增 PASS;合并后主区复跑同绿。
- **代拍板追认单十条已追认(2026-10-12 用户逐裁,06 在册)**:全数通过+连带裁定(血税保留/疼痛记忆「下场战斗」系笔误改「下一次攻击」/血税 desc 补生效时点修正案/流派=对外不显示对内保留事件族划分);**V20/V21/V22 人工核对半+实机目检五件仍待用户**(07 在册;V23 配额数字已随②③追认闭环)。
- **GDD 反哺点①-④全部落文(2026-10-12 用户逐裁令)**:①奇物.md v3.16+血税 desc fe0970d(冒烟 864/0);②冒险结构.md v0.20(关卡大奖励=专长三选一按档配比加权+回血,提升池机制段移交奇物.md);③难度曲线与Boss.md v0.28(投放读数只测不判,数值不动);④02-系统清单.md v1.24(奇物行换代);⑤归奇物呈现条(06 在册)。
- **回补带出四条待裁点**(秘宝卡两条+天赋树卡两条不阻塞;专长卡 §10.4 已随追认闭环)。
- **现行文档事实源**:秘宝.md v2.9+奇物.md v3.16+**天赋树.md v0.8(迭代 21 已按此实装入库)**+锻造工坊.md v0.5+术语表 v1.8;迭代粒度规矩=迭代周期 ≈ 一周左右。
- **更新于**:2026-10-12(迭代 23 锻造工坊实装批总收口——worktree g-plan2-iter23-forge 八提交,T0-T7 全量交付)。
- **批末闸全绿**:冒烟 938/0+run_all 24 套(含 forge_table_test/forge_workbench_test 两新套件)+probe --click/--full 八探针+render_gate(golden 0.00%)+doc 三闸对基线零新增+熵增 PASS;MC 同种子配对 80 局逐位零漂移(锻造满配档方向性补充 30/80 局差)。
- **现行文档事实源**:秘宝.md v2.9(目录 v1=12 件已实装)+奇物.md v3.16+天赋树.md v0.8+锻造工坊.md v0.6+术语表 v1.8。
- **代拍板追认单九条+GDD 反哺点三条**待用户(design/qa/iter23-实现报告-2026-10-12.md);**残留人闸=实机目检锻造工坊工作间形态与购买交互(07 ㉒)**。
- **迭代 21/22 残留人闸不变**:实机目检营地四柜台〔07 ⑳〕+秘宝解锁-装配流程〔07 ㉑〕;迭代 20 残留人闸=实机目检五件+V20/V21/V22 人工核对半(07 在册)。
- **已裁在册**:M4 数值批顺延迭代 24+ 未排期(锻造工坊 provisional 终值归其统一标定)。
## 项目当前位置(2026-10-12 刷新;实时细节以根目录 `roadmap.md` 与 06-待办清单 为准)
- **里程碑**:M3=Alpha 版本的完整实现(2026-10-09 立规:用户未明令宣布 M3 完成前停留 M3,迭代只排 M3 需求,M4 一律不排期)
- **迭代**:迭代 19 五卡已入库(2026-10-08);迭代 20 专长实装批已入库(2026-10-11 总收口,反哺①-④已落文);**迭代 21 天赋树实装批已入库(2026-10-12,goal 会话 U0 开工总收口)**;迭代 22=秘宝实装批已编入待 U0 发令;M4=迭代 23+ 未排期
- **近期主战场**:**迭代 21 残留人闸(实机目检营地四柜台〔V22〕+实现报告反哺点六条/追认单十二条待用户)**;迭代 22 待 U0 发令;迭代 20 残留人闸=实机目检五件+V20/V21/V22 人工核对半;文档治理连续批详情见 git 提交史
- **迭代**:迭代 19 五卡已入库(2026-10-08);迭代 20 专长实装批已入库(2026-10-11 总收口,反哺①-④已落文);**迭代 21 天赋树实装批已入库(2026-10-12,goal 会话 U0 开工总收口)**;**迭代 22 秘宝体系实现重构落地批已入库(2026-10-12,T1-T7 全量交付)**;**迭代 23 锻造工坊实装批已入库(2026-10-12,/goal+iter U0 开工单会话总收口)**;M4=迭代 24+ 未排期
- **近期主战场**:迭代 20/21/22/23 四批残留人闸(实机目检:专长五件 / 营地四柜台〔07 ⑳〕/ 秘宝解锁-装配〔07 ㉑〕/ 锻造工坊工作间〔07 ㉒〕)+ 各批实现报告代拍板追认单与 GDD 反哺点待用户;**下一前沿=M4 数值批,待 U0 发令**
## 未决面(进行中/待人闸/待裁点指针;现行事实源登记处=`design/gdd/06-待办清单.md`)
- 待用户裁决:美术审计两项 P0(art-bible 死样张指涉+主角像素管线重产;06 在册);去毒残留词扫否(魔法道具约 30 处/罕见约 18 处,呈报在案);喂雷流 R5 甲重启条件回落文(一句话件);「通关 vs 通过第 N 关」全库口径统一 273 处(待裁是否补登记 06);迭代 20 实机目检五件+V20/V21/V22 人工核对半(07 在册)
- 待发令:迭代 22 秘宝实装批开工(U0);锻造工坊实装批(17 裁点清零、06 在册,与天赋树批排期合并评估)
- 待发令:M4 数值批(迭代 24+,未排期——锻造工坊 provisional 终值/钥石产出重标/削弱档终值/元进度定价余面归其统一执行)
- 进行中:黑话清扫第二波(GDD 正文流程动词族约三千处,随时可派)
## 状态记录(滚动窗口:仅当前迭代;退役规则见 doc-conventions §10)
@@ -51,4 +51,8 @@
- **475(2026-10-12,奇物呈现修复批——僵尸呈现整改落地,呈现改法=甲逐件行)**:用户令「解决呈现问题」,按呈报推荐案甲落地(改乙/丙一句话另调)。代码两文件:`hud_presenter.gd` feat_holdings_lines 聚合行(新 29 行无 archetype 下空键兜底「流派 ×N · 档一」僵尸文案)→ 逐件行「件名 ×N」;`battle_narrator.gd` feat_card_text 摘「件名 · 流派」族名行(卡片=件名+效果摘要两行,兑付反馈行随之「件名 · 摘要」)+payload 死键 archetype 摘除;注释面五处换代(hud_presenter/hero_info_panel_view/level_map_screen)。断言面预查:smoke:982 只钉 desc 零族名断言,免改。文档同步:battle.md C 区两行(归 06 指针摘除改已整改事实)/07 HUD 条(验修复后逐件行)/报告 §五.5(同)/README 段(473 勘误句再换代为已修复)/06 奇物呈现条**收口移除**(三处违规+档位三路+呈现改法三面全清,去向=本条+git)/反哺点⑤✅销账(①-⑤全出清;反哺点条与追认单条随下次收口清理批一并移除)。FeatsEngine.tiers_for 呈现侧调用摘除后全仓零调用方(引擎库保留不动)。(worktree presentfix-20261012;**编号注记:474 已被并行改名批占用,本条顺延 475**)
- **481(2026-10-12,迭代 23 锻造工坊实装批总收口〔编号顺延——480 已被并行 06 清理批占用,主区未提交遗产撞号注记〕)**:T0-T7 八提交全量交付(worktree g-plan2-iter23-forge;T1 行族 df666dd/T2 两轴移交 00d98b6/T5 存档 v7 6def354〔序调先行——写口硬依赖〕/T3 写口族 a6270ba/T4 注入链 ed06292/T6 工作间 UI 2915f19/T7 收口笔)。T1 新表 forge.tres(ForgeTable 默认两轴+六路线行族,ConfigDB forge_* 六访问器)+单测 41 断言;T2 hero.tres 两字段删除+spawn 注入缝(空串=锻造配置默认值回退,行为逐位连续);T5 SAVE_FORMAT_VERSION 6→7+forge_routes 域{levels/unlocked/weapon/armor}(无特效域)+v6→v7 空迁移缺键补齐=初始持有态;T3 写口族(forge_buy_level 几何价 20/30/45/67/101/forge_buy_unlock 一次性 200/forge_select_* 免费/forge_route_unlocked 派生单点);T4 hero_override 公式换代(FORGE_ROUTE_BONUS 退役)+build_adventure_params 两键+参数面快照锁死+effective_ac 三层(甲型基础 AC+路线加值+局内)+enc→spawn 末端;T6 占位面板→两区六路线行板+三意图信号+presenter 处理器+新单测 12 断言。验证:冒烟 938/0(基线 898+40 锻造断言)+run_all 24 套+probe --click/--full 全绿残留 0+render_gate golden 0.00%+doc 三闸/熵增对基线零新增(3 个 gdd_lint 存量红同款;master 第 10 个扫描文件=并行会话未跟踪件 08-Alpha版本标准.md,禁碰);**MC 同种子配对 80 局逐位零漂移**(na=40 seed0=9000 双画像:normal 0/40↔0/40、perfect 28/40↔28/40)+方向性补充(锻造满配档 atk+5/ac+5 同种子 30/80 局结局差:perfect 28→33/40)——默认态行为连续铁证+注入面生效证据。机械面文档七件同步(roadmap/entities.yaml 回填实态/06 状态+①② v0.6 措辞订正/存档序列化 v1.6/07 ㉒+game/README 批次段);实现报告=design/qa/iter23-实现报告-2026-10-12.md(代拍板追认单九条+GDD 反哺点三条待用户);残留人闸=实机目检工作间形态与购买交互(07 ㉒)。环境注记:render_gate 需 gamedev venv(PIL)。
- **479(2026-10-12,迭代 23 锻造工坊实装批开工)**:/goal+iter 技能调用=U0 开工令(用户目标=总收口全绿:冒烟/单测/探针/读数对拍四闸全过+实现报告落档)。开工检查全过:卡片 v1.2 零阻塞待裁点(Q1=0 级起步+锻造第一级=首个 +1 已裁;Q2=provisional 沿锚表投值已裁);V1-V14 验收清单三要素齐(V9 静态 grep 人工核对+V13 用户目检=纪律类人工项,余自动闸)。自洽扫描两处 L2 记录继续(不触红线):①卡片 F8「meta 域 13 件」清单滞后——现行实态 15 键(迭代 22 T2 同批新增 vision_used 未及登卡),对 forge_routes 新域零影响;②06 条目①「forge_products 两行改造」系 v0.5 旧文——设计 v0.6/卡片 F1 已订正为「行族已退役、新建行族」,卡片优先,T7 机械面同步时订正 06 措辞。事实抽查全符(F1 三处退役登记在册/F3 FORGE_ROUTE_BONUS 与 hero_override 在役行号漂移至 :155/:157/F5 两轴在役/F8 v6+_default_data 15 键/F9 forge_workshop 钥匙节点+营地占位面板接缝在册/F7 敌方轴未开)。T0 实查结案:①基线复读=冒烟 898/0+run_all 22 套全绿+probe_gate --click 全绿残留 0(存量 fx_reveal 已随 iter21 修复,零存量红);②buy_forge/forge_products 断言已随 iter21 T5 全摘(余件皆注释/退役登记——smoke:4398「MetaTable 行族规模 3/3/2」断言不受影响:新行族落新表);③hero 两轴读取面全清单=运行时独一处 unit_container.gd:64(db.hero 两轴快照)+冒烟断言 smoke_all.gd:1901(_db.hero.attack_type)+battle_orchestrator.gd:1410(消费 hero_unit 快照非表直读)+README 文档面,测试层零直读;④meta.tres 现役三行族+boss_keystone/conversion/attune_refresh_upgrade,FORGE_ATTRS 常驻;⑤树钥匙+占位面板接缝在册(camp_overlay_view.gd:275-284 钥匙门);⑥设计 v0.6 两节(裁决结果清单+工作间内容清单)复核无冲突。注入链已探明:main.build_adventure_params(:501)→ adventure params → _runner.set_* 族(:934-945 模式)→ level_runner enc(:480-494)→ begin_battle(battle_orchestrator.gd:344/:396 spawn)。代拍板立项(白名单内):新表 forge.tres(ForgeTable:默认两轴+六路线行族);路线 id=sword/spear/staff/light/heavy/ward(甲路线 id=甲型值本身);存档域 forge_routes={levels 六键+unlocked 直购清单+weapon/armor 选中}(初始持有+练满前一档=派生解锁、直购=入档)。定义面已入库(b9cca33,卡片单笔);主区残留脏件全属他批遗产零碰(.zcode/active.md/锻造工坊.md v0.6 续裁行/playnote 等)。worktree g-plan2-iter23-forge 开工,T1 起。
- **476(2026-10-12,迭代 21 天赋树实装批总收口〔编号顺延——470-475 系并行批次占用,471/472 并行撞号已在彼条注记〕)**:T1-T7 全量交付(worktree g-plan2-iter21-tree 基于开工批 43f7230;master 并行十八笔已并入分支解 active 冲突后回合)。T1 schema 换代(FORK_GROUPS 4→3/RULE_KEYS 2→6/fork 单行双态 effects_a·b/row 退役门槛换轴/R1 裁撤案执行)+33→12 行重排(easter 摘攻防=契约 9);T2 购买面(easter 拒购/Fork 解锁默认 A/零币切态/彩蛋特化判定);T3 六键接线(EVENTS+flag_missed/FACTS+battle_index=L2 扩签两处/posture 每关首轮窗/sense 限次 counter 闸 domain:adventure 按关重置/守侧去重闸后发射=契约12/聚入账前×1.5/散双计价点×80%——数值全 provisional 归 M4);T4 注入换代(树淬炼 flat+锻造加值位预留 0/槽位判定源切树点亮数/ks_refresh 同账路由);T5 删除批(migrate_tree/buy_forge 家族/forge_products 行族/forge_levels 域/attunement_growth 谓词表/生涯三投影全删;tree_struct=2 标记废弃重置禁迁移);T6 四柜台并排(树=主界面/两钥匙门+零交互占位面板/图鉴常开/暂停层锻造段连带删)。存量红修复=fx_reveal 三处空 source_class(a57489e9 落地件,基线 51 行脚本错误清零——474 条 FAIL 注记随之销账)。验证:冒烟 886/0(V1-V10/V16 断言随批)+run_all 22 套(新增 V11 六键闭集单测)+probe --click --full 八探针全绿+render_gate golden 0.00%+doc 三闸/熵增闸对基线零新增(worktree 缺 ignored 素材两坑照 463 先例拷 artwork+game/assets/art 后绿)+V19 MC 三组同种子配对×60 局 perfect+max 档全部 61.7%、配对差 0.0pp 在带(normal 档基率地板读数换档记验证记录);守侧五场景=编排 rig 直注+引擎直构双承载。追认单十二条+反哺点六条+欠账三条落实现报告 design/qa/iter21-实现报告-2026-10-12.md 待用户;残留人闸=实机目检四柜台(V22)。
+2 -2
View File
@@ -57,11 +57,11 @@
#### 锻造工坊实装批 — 待办(冻结:迭代规划冻结中,重启=用户发令)(2026-10-09 随成文批登记,时名「武器锻造系统实装批」;2026-10-11 随系统更名换代)
**内容**:按 [锻造工坊](systems/跨局循环/锻造工坊.md) **v0.5** 定案口径实装(需求卡片输入=其「裁决结果清单」+「工作间内容清单」两节)——迁移面八项:① `meta.tres` forge_products 攻击/护甲两行改造为武器与护甲独立等级路线(生命行清退归「天赋树实装批」旧柜台退役——生命唯一入口=树脊柱生命线,2026-10-11 A① 裁;同表其余四行族零触碰);② `hero.tres` 攻击类型/甲型两休眠轴启用(营地选中即写轴,既有预留位启用;一次冒险一个武器类型+一个甲型,营地免费切换、冒险开始后锁死);③ `feats.tres` magic_vs_magic 死条件行——复活案作废(2026-10-11 核结:所属件魔网织者在素材库表二 27 件退役删除名单),随专长退役删行批消解,本批零触碰;④ 存档域新增三武器+三甲型等级与当前选中标记(**不设特效域**——2026-10-12 裁 6;开发阶段无迁移——旧档废弃口径随批登记存档序列化文档与 registry);⑤ **满级特效本期不实装**(2026-10-12 裁 6:不开三条新参数位、不加效果类型词,扩展位留空,启用需另立项);⑥ **敌方攻击类型轴填值(3b 裁连带,跨系统汇合点)**——敌方攻击单据 `attack_type` 按「未设置=默认 normal」填值使其参与克制矩阵查表(现役为 `""` 恒不查矩阵;`enemy.tres:527` 轴未开、全部 normal 档):**执行面归敌人系统/战斗结算侧**,本批只登记需求与数值影响(全局平移主角受伤点数:穿重甲 −1、穿魔法护甲 +1,须进⑧配对读数验证);排期与敌人系统/战斗结算侧协商(可同批做,也可先行);敌方 attack_type 为敌方定义数据(与敌种名字/血量同批同时读写、无独立时序),填值归敌人系统/战斗结算侧自然推进;本批只按 3b 裁落结构面(矩阵查表现役已支持)(2026-10-12 订正:原「两批排期协商(可同批做,也可先行)」句删除——用户裁:时序问题不存在);⑦ 术语登记——三型甲统一命名(轻甲/重甲/魔法护甲;**「魔法甲」系笔误不登记别名**)+ 工作间两区定名(武器锻造区/护甲锻造区)登记 registry entities.yaml;⑧ 数值验证义务=改前/改后同随机种子配对读数验证难度影响,统一调整归 M4 数值批。**解锁通道**:四条待解锁路线(长枪/法杖/重甲/魔法护甲)均走并联双通道(练满前一档 OR 一次性解锁价,互不影响、玩家自选),不设串联门槛。**前置**:17 裁点用户逐点裁决——**已清零(2026-10-12 B 组 15 点逐裁落文 v0.5;2c/5a 于 v0.4 结案)**。**关联(2026-10-11 换代)**:锻造工坊钥匙节点改无前置直接可点亮(2026-10-11 用户裁,原等级阈值前置作废);树脊柱砍攻/护两线(攻防所有权切分——攻击唯一入口=武器路线、AC 唯一入口=甲路线,树侧落文归天赋树会话交接批)后本批=攻防数值唯一实装面;树结构换代(33→12)已随迭代 21 入库——本批④存档域 forge_routes 独立成域(已预登记存档序列化.md v7 计划行),无共域冲突(2026-10-12 订正:原「排期两批合并评估」语境随树批入库消失——用户裁:非裁点)。**前置信息(2026-10-12 用户裁定:不在本批考虑)**:3b 后现行数据三型成纯序(重甲正向 > 轻甲中性 > 魔法护甲负向,敌方现行无魔法攻击故暂无优势窗口)——系**后续开发改造的前置事实**,敌方攻击类型差异化与魔法护甲的正当性归后续批次(含 M4 在基础 AC / 解锁价上的取值);本批只按统一伤害轴落结构与字段。
**内容**:按 [锻造工坊](systems/跨局循环/锻造工坊.md) **v0.6** 定案口径实装(需求卡片输入=其「裁决结果清单」+「工作间内容清单」两节)——迁移面八项:① **新建锻造路线行族**(forge_products 已随迭代 21 T5 整体退役、无改造对象——v0.6 事实订正:行族/访问器/购买面三处退役登记在册,本批新建而非改造;落位=新表 forge.tres〔ForgeTable〕,钥石定价历史考古 20/150%、provisional 投值归 M4 终值);② **两根类型轴所有权移交**(默认值归锻造工坊配置常驻持有、hero.tres 静态表两轴删除、选中态写存档域 forge_routes+冒险开始参数面注入;一次冒险一个武器类型+一个甲型,营地免费切换、冒险开始后锁死);③ `feats.tres` magic_vs_magic 死条件行——复活案作废(2026-10-11 核结:所属件魔网织者在素材库表二 27 件退役删除名单),随专长退役删行批消解,本批零触碰;④ 存档域新增三武器+三甲型等级与当前选中标记(**不设特效域**——2026-10-12 裁 6;开发阶段无迁移——旧档缺键补齐=初始持有态,v6→v7 空迁移;存档序列化文档与 registry 随批登记);⑤ **满级特效本期不实装**(2026-10-12 裁 6:不开三条新参数位、不加效果类型词,扩展位留空,启用需另立项);⑥ **敌方攻击类型轴填值(3b 裁连带,跨系统汇合点)**——敌方攻击单据 `attack_type` 按「未设置=默认 normal」填值使其参与克制矩阵查表(现役为 `""` 恒不查矩阵;`enemy.tres:527` 轴未开、全部 normal 档):**执行面归敌人系统/战斗结算侧**,本批只登记需求与数值影响(全局平移主角受伤点数:穿重甲 −1、穿魔法护甲 +1,须进⑧配对读数验证);敌方 attack_type 为敌方定义数据(与敌种名字/血量同批同时读写、无独立时序),填值归敌人系统/战斗结算侧自然推进;本批只按 3b 裁落结构面(矩阵查表现役已支持)(2026-10-12 订正:原「两批排期协商(可同批做,也可先行)」句删除——用户裁:时序问题不存在);⑦ 术语登记——三型甲统一命名(轻甲/重甲/魔法护甲;**「魔法甲」系笔误不登记别名**)+ 工作间两区定名(武器锻造区/护甲锻造区)登记 registry entities.yaml;⑧ 数值验证义务=改前/改后同随机种子配对读数验证难度影响,统一调整归 M4 数值批。**解锁通道**:四条待解锁路线(长枪/法杖/重甲/魔法护甲)均走并联双通道(练满前一档 OR 一次性解锁价,互不影响、玩家自选),不设串联门槛。**前置**:17 裁点用户逐点裁决——**已清零(2026-10-12 B 组 15 点逐裁落文 v0.5;2c/5a 于 v0.4 结案)**。**关联(2026-10-11 换代)**:锻造工坊钥匙节点改无前置直接可点亮(2026-10-11 用户裁,原等级阈值前置作废);树脊柱砍攻/护两线(攻防所有权切分——攻击唯一入口=武器路线、AC 唯一入口=甲路线,树侧落文归天赋树会话交接批)后本批=攻防数值唯一实装面;树结构换代(33→12)已随迭代 21 入库——本批④存档域 forge_routes 独立成域(已预登记存档序列化.md v7 计划行),无共域冲突(2026-10-12 订正:原「排期两批合并评估」语境随树批入库消失——用户裁:非裁点)。**前置信息(2026-10-12 用户裁定:不在本批考虑)**:3b 后现行数据三型成纯序(重甲正向 > 轻甲中性 > 魔法护甲负向,敌方现行无魔法攻击故暂无优势窗口)——系**后续开发改造的前置事实**,敌方攻击类型差异化与魔法护甲的正当性归后续批次(含 M4 在基础 AC / 解锁价上的取值);本批只按统一伤害轴落结构与字段。
**归属**:[锻造工坊](systems/跨局循环/锻造工坊.md) 实装(跨 meta/hero/feats/存档/战斗开场参数五面)
**状态**:**已编入迭代 23**(2026-10-12 用户令——开工门=U0 发令;设计面 v0.6:17 裁点清零+完备性检查批〔现役基线换代/两轴所有权移交/术语登记完毕〕,commit f5215c7)
**状态**:✅ **已入库(2026-10-12,/goal+iter U0 开工单会话交付——T1 行族/T2 两轴移交/T3 写口族/T5 存档 v7〔序调先行:写口硬依赖〕/T4 注入链/T6 工作间 UI/T7 收口七提交;冒烟 938/0+run_all 24 套+重闸全绿+MC 同种子配对零漂移;实现报告=design/qa/iter23-实现报告-2026-10-12.md;残留人闸=实机目检工作间形态与购买交互〔07 在册〕;代拍板追认单+GDD 反哺点待用户)**
#### 玩家向显示名改名小批(局内三轴 + 临时 AC 状态)——跨循环名称冲突清偿(2026-10-12 用户逐裁,锻造工坊裁决会话续 8a 连带)
+10
View File
@@ -209,6 +209,15 @@
- **装配不可变**:冒险中无任何换装入口(三选一/奖励节点/战斗内零替换交互——旧替换子交互已拆)。
- 验证方式:用户实机目检(本批自动闸外唯一人闸;能力验证半已由自动化闸覆盖=V1-V19)。
### ㉒ 锻造工坊工作间形态与购买交互(迭代 23 锻造工坊实装批随批登记——2026-10-12)
- **门与形态**:树上「锻造工坊」钥匙未点亮=锁定态(纯文案、无任何可交互购买路径);点亮后工作间展开=武器锻造区/护甲锻造区两区并列,六路线行板(行名=件名+锻造:长剑锻造/长枪锻造/法杖锻造/轻甲锻造/重甲锻造/魔法护甲锻造;每行件名+Lv+解锁态+钮组)。
- **购买面**:已解锁路线「锻造 N 钥石」钮(几何价 20/30/45/67/101 逐级涨);余额不足钮禁用;买后等级+1、行板即时刷新、钥石余额扣减。
- **解锁面**:待解锁路线双通道呈现——「练满长剑锻造 或 200 钥石直购」:长剑练满→长枪/法杖解锁钮消失可直接锻造(零收费);或 200 钥石直购开;两通道互不影响(未练满可直购、直购后练满不重复收)。
- **选中面**:已解锁路线「选中」钮免费切换(当前路线=「当前」禁用态);一次冒险只带一个武器类型+一个甲型;冒险开始后无切换入口(锁死)。
- **注入抽查**:营地选长枪→开局攻击类型=穿刺(对敌护甲查克制矩阵生效);选重甲→入战 AC=12+路线加值;旧路线练度保留(换回即生效)。
- 验证方式:用户实机目检(本批自动闸外唯一人闸;能力验证半已由自动化闸覆盖=V1-V14)。
## 验收读数重测义务(履行留痕 + 现行义务)
**已履行**(按序换代,旧读数各随其批作废留痕不入本单):①验收读数重测批(2026-09-19——Boss 单独出列/承伤带/通关率基线全量重测,硬化翻倍期·乏力死效果期·充能期旧读数作废);②迭代 5 基线重测(统一随机流后 MC+verifier 双窗);③**数值修复批新尺重测(2026-10-01)**——模拟档位重构后全量重测,新锚 `sim-data/runsum_numfix_final_n200_20261001.jsonl`,全旧带作废只测不判(numfix 报告(已退役,git 历史可查));④**迭代 10 奖励重构批重锚(2026-10-01)**——四代锚 77000/78000/79000/80000(改前/重构后/共享池后/掷定前置后,各 400 局),普通 L1 41.8%/L2 10.2% 带内(迭代10通关率读数(已退役,git 历史可查));⑤**T19 真人标定局收口(2026-10-03 用户裁)**——有效局 2(1 通关 1 死于 L3 首战,T19真人试玩读数(已退役,git 历史可查)),用户裁定**佐证级收口不再补局**;k 预期带同日立(**正常 >0.5、预警 <0.3**),字段随 81000 重锚入库后补算终值。
@@ -226,3 +235,4 @@
| v0.3 | 2026-10-09 | 版本历史表补建——本文件此前无版本表(2026-10-09 审查批呈报在案),本卫生批补建本表并补登上两行 |
| v0.4 | 2026-10-09 | **文档正确性审查批·独立复核(worktree docaudit-2026-10-09)**:①三处「链接语法但目标非路径」伪死链去链接改纯文本(numfix 报告/迭代10通关率读数/T19真人试玩读数——三份读数均已退役,git 历史可查口径不变);②「难度修正批」批次名全替「数值批(Beta 阶段,未排期)」——与 06 现行归属称呼对齐(同批=M4 数值批需求卡片,未排期态不变) |
| v0.5 | 2026-10-10 | 生涯页呈现行柜台名扫改(2026-10-10 天赋树设计会话裁「道具工坊」改名「秘宝匣」):「道具工坊已解锁 X/6 件」改「秘宝匣已解锁 X/6 件」;计数语义(旧六件→新目录)随实装批换代,见 06「天赋树结构重构批」改名连带面 |
| v0.6 | 2026-10-12 | 新增 ㉒ 锻造工坊工作间形态与购买交互目检节(迭代 23 锻造工坊实装批随批登记——V13 人闸观测项:门与形态/购买/解锁双通道/选中与锁死/注入抽查) |
@@ -102,7 +102,7 @@ SaveManager 不依赖任何玩法系统就能工作:它是被动的承载方
| `adventure_cleared` | 通关局数终身计数(三关全通才计 1,v6;秘宝条件解锁计数键) | [生涯](../跨局循环/生涯.md) |
| `vision_used` | 真视类工具累计使用次数终身计数(迭代 22 新增——信标网络解锁行为条件;**v6 域后扩列补零、不升版本**;写入唯一经 `add_vision_used`,冒险收束聚合入账;断点随件键=`vision_counter_used`,续玩回填缺省 0) | [秘宝](../跨局循环/秘宝.md) |
| ~~`forge_levels`~~ | ~~属性锻造三档等级~~ | **已退役**(迭代 21 T5:行族/访问器/域随攻防所有权切分删除——`save_manager.gd:12-14/:399-401` 白名单收口;继任 = 锻造工坊路线等级域,见下行计划登记) |
| `forge_routes`(**计划新增**——锻造工坊实装批,v7;域名以实装批定名为准) | 三武器等级 + 三甲等级 + 当前武器/甲选中标记(各 1;无特效域;旧档破坏性废弃口径归该批) | [锻造工坊](../跨局循环/锻造工坊.md) |
| `forge_routes`(**已落地 v7——锻造工坊实装批 T5,迭代 23**) | {levels: 六路线等级(sword/spear/staff/light/heavy/ward), unlocked: 直购解锁路线 id 清单, weapon: 当前武器路线 id, armor: 当前甲路线 id};无特效域;旧档缺键补齐 = 初始持有态 | [锻造工坊](../跨局循环/锻造工坊.md) |
| `breakpoint` | 冒险断点快照(停点粒度续玩域;空字典 = 无断点合法态——「继续上局」隐藏) | [冒险结构](../冒险循环/冒险结构.md) |
v4/v5/v6 各域缺键由读取方按默认值补齐(空集合/0),旧档零迁移损失;收束自增域(通关次数/Boss 计数/终身计数/真视使用计数)的唯一入账点 = `apply_adventure_summary`(vision_used 走 `add_vision_used` 收束)。**v6 域后扩列不升版先例**:`vision_used`(迭代 22,2026-10-12——迭代 21 同款口径;断点随件 `vision_counter_used` 同批新增;旧断点随件键 `bonus_aux_carry` 已随替换子交互拆除退役,迭代 22 T5)。
@@ -129,7 +129,7 @@ v4/v5/v6 各域缺键由读取方按默认值补齐(空集合/0),旧档零
### 版本与迁移
存档带版本字段(`version`,当前 = **6**;版本沿革:v2 = 断点域出生〔迭代 8 卡①〕→ v3 = 断点附随件扩列 + 摘要 keystone_drops〔迭代 10 奖励重构〕→ v4 = 同调批扩域〔boss_kill_count / attunement_equipped / attune_refresh_level,迭代 11 第①批〕→ v5 = 天赋树域〔tree_nodes / fork_sides,迭代 12〕→ v6 = 生涯终身计数域〔adventure_total / adventure_cleared,迭代 18 卡④〕;与 registry `schema_version` 同源)。**(计划)v7 = 锻造路线域**〔三武器/三甲等级 + 当前选中标记——锻造工坊实装批,2026-10-12 v1.5 预登记;届时旧档破坏性废弃口径归该批,禁迁移/折算〕。三条走向:
存档带版本字段(`version`,当前 = **7**;版本沿革:v2 = 断点域出生〔迭代 8 卡①〕→ v3 = 断点附随件扩列 + 摘要 keystone_drops〔迭代 10 奖励重构〕→ v4 = 同调批扩域〔boss_kill_count / attunement_equipped / attune_refresh_level,迭代 11 第①批〕→ v5 = 天赋树域〔tree_nodes / fork_sides,迭代 12〕→ v6 = 生涯终身计数域〔adventure_total / adventure_cleared,迭代 18 卡④〕→ **v7 = 锻造工坊域〔forge_routes,迭代 23 T5——已落地:{levels 六路线等级 + unlocked 直购解锁清单 + weapon/armor 当前选中标记},无特效域;旧档缺键由 _normalize 按默认补齐 = 初始持有态(sword/light 在档、余四锁、选中=初始路线),v6→v7 空迁移 = 缺键补齐模式(v3→v4 先例);开发阶段禁迁移禁折算——契约 4〕**;与 registry `schema_version` 同源)。三条走向:
| 情形 | 处置 |
|---|---|
@@ -137,7 +137,7 @@ v4/v5/v6 各域缺键由读取方按默认值补齐(空集合/0),旧档零
| 版本更高(未来版本的档) | **拒读且禁写**:不加载、后续写盘一律拒绝、内存以默认新档运行——保护未来版本的档不被当前版本覆写 |
| 版本更低(旧档) | 读档时逐级迁移:从旧版本起逐个调用注册的迁移函数升到当前版本;任一步失败按损坏兜底处理 |
迁移函数经 `register_migration(旧版本号, 函数)` 注册:函数把数据从「旧版本」改写成「旧版本 + 1」。生产迁移链现行五件(v2 起自注册):v1→v2 = 断点域补空;v2→v3 = 断点附随件与摘要 keystone_drops 补键(读取方 .get 兜底);v3→v4 = 同调三域补键;v4→v5 = 树域两键补齐(原挂接的「三体系迁移」体已随迭代 21 T5 攻防所有权切分删除——`save_manager.gd:96-98`、`meta_progression.gd:294-295` 实证;forge_levels 旧域同批清退,加载时按默认模型键集白名单收口,`save_manager.gd:399-401`);v5→v6 = 生涯两终身计数域补零。任一步失败按损坏兜底处理。
迁移函数经 `register_migration(旧版本号, 函数)` 注册:函数把数据从「旧版本」改写成「旧版本 + 1」。生产迁移链现行六件(v2 起自注册):v1→v2 = 断点域补空;v2→v3 = 断点附随件与摘要 keystone_drops 补键(读取方 .get 兜底);v3→v4 = 同调三域补键;v4→v5 = 树域两键补齐(原挂接的「三体系迁移」体已随迭代 21 T5 攻防所有权切分删除——`save_manager.gd:96-98`、`meta_progression.gd:294-295` 实证;forge_levels 旧域同批清退,加载时按默认模型键集白名单收口,`save_manager.gd:399-401`);v5→v6 = 生涯两终身计数域补零;**v6→v7 = 锻造工坊域 forge_routes 缺键补齐(迭代 23 T5——无结构迁移体,版本号推进仅为 schema_version 对齐;解锁态派生不落档:初始持有〔表 initial_owned〕∨ 直购〔unlocked 清单〕∨ 练满前一档〔levels[prereq]≥cap〕)**。任一步失败按损坏兜底处理。
### 损坏兜底
@@ -194,3 +194,4 @@ MVP 不要求本系统的闭环([不做清单](../../05-不做清单.md)把单
| v1.4 | 2026-10-09 | **文档正确性审查批·独立复核(worktree docaudit-2026-10-09——版本滞后四代换代,全部代码实证 save_manager.gd SAVE_FORMAT_VERSION=6)**:①数据模型 JSON 与域清单表换代 v2→v6——补 v4 同调三域(boss_kill_count/attunement_equipped/attune_refresh_level,迭代 11①)、v5 树域(tree_nodes/fork_sides,迭代 12)、v6 生涯两终身计数域(adventure_total/adventure_cleared,迭代 18 卡④)逐域 owner 行;②版本与迁移节换代——当前=6、迁移链五件(v1→v2…v5→v6,含 v4→v5 三体系迁移指路天赋树);③调参表版本常量 2→6;④缺键补齐纪律句补记(读取方默认补零/空集合,收束自增唯一入账=apply_adventure_summary)。注:v3/v4/v5/v6 四次升版当时均未回写本文,本批一次性清偿 |
| v1.5 | 2026-10-12 | **锻造工坊完备性检查批随迁(用户令:新加内容现在补、不等实装批)**:①**预登记锻造路线域**(计划 v7):域清单表 + 版本沿革补 `forge_routes`(三武器等级+三甲等级+当前选中标记、无特效域、旧档破坏性废弃归锻造工坊实装批;域名以实装批定名为准);②**清偿 forge_levels 残留**——JSON 示例、域清单表删该域(代码侧已随迭代 21 T5 删除:`save_manager.gd:12-14/:399-401` 白名单收口);迁移链 v4→v5 的「三体系迁移」体改述为已删除(`save_manager.gd:96-98`、`meta_progression.gd:294-295`);③**补登两漏域**(代码实证 `save_manager.gd:119-139`):`tree_struct`(树结构版本标记,迭代 21 T5,缺失=换代前结构加载废弃重置,owner 天赋树)、`first_adventure_done`(首冒险完成标记,新档默认 false、旧档缺省按已完成兜底,`meta_progression.gd:100` 置位,owner 元进度);④消费面纪律句「读写锻造等级」→「读写树等级」(访问器已随行族退役删除)。版本号常量仍 =6(v7 未落地) |
| v1.6 | 2026-10-12 | **迭代 22 反哺落文批(实现报告 §三⑥,用户裁「落文」)**:域清单表补 `vision_used` 行(真视使用终身计数,迭代 22 新增,v6 域后扩列补零不升版本——迭代 21 先例;断点随件 `vision_counter_used` 同批新增、旧随件键 `bonus_aux_carry` 随替换子交互拆除退役);缺键补齐纪律句补 v6 扩列注;零迁移口径复核=SAVE_FORMAT_VERSION 仍 6(`save_manager.gd:49`),无迁移分支新增 |
| v1.7 | 2026-10-12 | **锻造工坊实装批随迁(迭代 23 T5——v7 落地;承 v1.6 的 vision_used 登记)**:①域清单表 `forge_routes` 计划行→实际落地态({levels 六路线等级 + unlocked 直购解锁清单 + weapon/armor 当前选中标记},域内键名以实装批定名);②版本与迁移节——当前 =7、迁移链补 v6→v7(空迁移=缺键补齐模式,解锁态派生不落档三通道);③旧档口径订正——「破坏性废弃」→「缺键补齐 = 初始持有态」(开发阶段禁迁移禁折算纪律不变,契约 4);④域界注记——forge_routes 属 meta 永久进度域(契约 2 反面清单不涉) |
@@ -0,0 +1,66 @@
# 迭代 23 实现报告——锻造工坊实装批
> 日期:2026-10-12 | 迭代:23(M3=Alpha 完整实现面)| 开工令:/goal + iter 技能 U0(用户目标=总收口全绿:冒烟/单测/探针/读数对拍四闸全过+实现报告落档)
> 需求卡片:`design/research/需求卡片-锻造工坊实装批-2026-10-12.md`(v1.2,Q1/Q2 已裁、零待裁点)
> 设计权威:`design/gdd/systems/跨局循环/锻造工坊.md` v0.6(17 裁点清零+完备性检查批,commit f5215c7)
> Worktree:`g-plan2-iter23-forge`(分支 iter23-forge-20261012,基点 b9cca33)| 批内提交:T0 定义入库 b9cca33 + T1 df666dd / T2 00d98b6 / T5 6def354 / T3 a6270ba / T4 ed06292 / T6 2915f19 / T7 收口笔
> 状态:**✅ 已全批入库(单会话交付)**——四闸全绿+重闸全绿;残留人闸=实机目检工作间形态与购买交互(07 ㉒)+代拍板追认单九项+GDD 反哺点三条待用户
## 一、开工检查结论(第 2 步)
- **门**:roadmap 迭代 23 行「开工门=U0 发令」——本会话 /goal 调用即 U0,门解。
- **待裁点**:卡片 v1.2 零阻塞待裁点(Q1=等级 0 起步+锻造第一级=首个 +1;Q2=provisional 沿锚表投值——均已裁)。
- **验收清单**:V1-V14 逐条三要素(测什么/怎么测/谁验)齐;V9(静态 grep 人工核对)与 V13(用户目检)为纪律类人工项,余自动闸。
- **自洽扫描两处 L2(记录继续,不触红线)**:①卡片 F8「meta 域 13 件」清单滞后——现行 15 键(vision_used 系迭代 22 T2 同日新增未及登卡),对 forge_routes 新域零影响;②卡片 F11「效果类型闭集 24 种」系 2026-10-07 设计文档事实快照——现行 31(迭代 20 T1 +7、迭代 22 T3 +1),本批零扩签,断言钉 31。另:06 条目①「forge_products 两行改造」系 v0.5 旧文(v0.6/卡片 F1 已订正为「行族已退役、新建行族」)——T7 机械面同步时订正。
- **T0 实查**:基线复读=冒烟 898/0+run_all 22 套+probe --click 全绿残留 0(存量 fx_reveal 已随 iter21 修复,零存量红);buy_forge/forge_products 断言已随迭代 21 T5 全摘;hero 两轴读取面全清单(运行时 unit_container.gd:64 + 冒烟 smoke_all.gd:1901 + battle_orchestrator 间接消费 + README 文档面);注入链探明(build_adventure_params → adventure params → set_* 族 → enc → spawn)。
## 二、任务卡状态(T0-T7)
| 卡 | 状态 | 交付要点 | 提交 |
|---|---|---|---|
| T0 前置实查 | ✅ | 基线三件套+F1-F13 事实核对+断言摘除清单+两轴读取面清单+注入链探明;定义面入库(b9cca33) | b9cca33 |
| T1 路线行族 | ✅ | 新表 forge.tres(ForgeTable:默认两轴+六路线行族)+ConfigDB 六访问器+单测 41 断言(V1 正反 22 类/V11 行字段闭集+词表 31 钉现/V2 价格梯) | df666dd |
| T2 两轴移交 | ✅ | hero.tres 两字段删除(schema 同步)+spawn 注入缝(空串=锻造配置默认值回退,行为逐位连续)+冒烟 4 断言(V5 前半) | 00d98b6 |
| T5 存档 v7 | ✅ **(序调先行——T3 写口硬依赖存档域)** | SAVE_FORMAT_VERSION 6→7+_migrate_v6_v7 空迁移+forge_routes 域(levels/unlocked/weapon/armor,无特效域)+访问器组+冒烟 V8 六断言(往返/v6 旧档补齐/键集/值拷贝) | 6def354 |
| T3 写口族 | ✅ | MetaProgression 锻造写口(forge_buy_level 几何价/forge_buy_unlock 一次性解锁价/forge_select_weapon·armor 免费选中/forge_route_unlocked 派生单点)+冒烟 V2/V3/V4/V6 共 15 断言 | a6270ba |
| T4 注入接线 | ✅ | hero_override 公式换代(FORGE_ROUTE_BONUS 退役)+build_adventure_params 两键+参数面快照(契约 8 锁死)+effective_ac 三层(甲型基础 AC+路线加值+局内)+enc→spawn 末端;冒烟 V5 后半/V6/V7/V10 共 15 断言 | ed06292 |
| T6 工作间 UI | ✅ | 占位面板→两区六路线行板(锻造/解锁/选中三钮+余额联动+锁定态零行板零购买路径)+presenter 三意图处理器+main 接线+新单测 forge_workbench_test 12 断言(V13 自动化前置面) | 2915f19 |
| T7 收口 | ✅ | 重闸全绿+MC 配对+机械面文档七件+本报告 | 收口笔 |
## 三、验证闸总账(批末重闸)
| 闸 | 结果 |
|---|---|
| 冒烟(run_smoke 三闸:退出码/摘要/脚本错误扫描) | **938 项检查 0 项失败**(基线 898/0 + 本批 40 断言:T2×4/T3×15/T4×15/T5×6) |
| 单元(run_all) | **24 套全绿**(基线 22 + forge_table_test 41 断言 + forge_workbench_test 12 断言) |
| 探针(probe_gate) | **--click + --full 全绿、残留 Godot=0**(ui_input/ui_struct/ui_contrast/click/ui_probe/ui_tune/t17_defer/fx_probe 八件) |
| 读数对拍(MC 同种子配对,verifier_batch adventure 段 na=40 seed0=9000 双画像) | **A) 改前 vs 改后白板:80 局同种子逐位零差异**(normal 0/40↔0/40 均关 0.10;perfect 28/40↔28/40 均关 2.42)——默认态行为连续零漂移铁证。**B) 方向性补充**:白板 vs 锻造满配档(atk+5/ac+5 同种子)80 局中 30 局结局变化(normal 0/40→1/40、perfect 28/40→33/40)——锻造加值注入面生效且方向正确(数值 provisional 归 M4) |
| render_gate(窗口化渲染闸) | **全绿**——shot_checker A-D 断言过 + 棋盘 golden 三屏 changed=0.00%(容差内) |
| doc 三闸(gdd_lint/doc_link/doc_term) | **对基线零新增**——gdd_lint 3 个存量红同款(06/07 表格占比、术语表,master 同);doc_link 已知死链 1(存量);doc_term 单 hit 存量(06:37 拔旗,非本批编辑区) |
| 熵增闸(repo_structure_audit) | **PASS**(四查全过) |
> 环境注记:render_gate 的 PIL 依赖需 gamedev venv(`PATH=/d/dev/python/envs/gamedev/Scripts:$PATH`)——WorkBuddy 裸 python 无 PIL;本机 Godot 4.7.2 路径 `D:\code\godot\Godot_v4.7.2-stable_win64_console.exe`。
## 四、代拍板追认单(白名单内,待用户追认)
1. **行族落位选型**:新表 `forge.tres`(ForgeTable)而非 meta.tres 内新行族——T2「锻造工坊配置表新建」与 T1 行族同表承载(默认两轴+六路线);访问器连续性沿 treasure_items 先例(`db.forge_*`)。meta.tres 三行族零触碰。
2. **路线 id 命名**:sword/spear/staff/light/heavy/ward——甲路线 id=甲型值本身(ward=魔法护甲,3a 裁统一命名);存档域 levels 六键同。
3. **存档域键名**:`forge_routes = {levels:{六路线→等级}, unlocked:[直购路线 id], weapon/armor:选中路线 id}`——unlocked 只记直购;初始持有(表 initial_owned)与练满前一档(levels[prereq]≥cap)为派生解锁不落档(解锁态派生单点=MetaProgression.forge_route_unlocked)。
4. **provisional 行值**:沿 T1 锚表投值(cap 5/每级增量统一 +1/基础价 20×150%^n=20/30/45/67/101/一次性解锁价 200/甲型基础 AC 10-12-9/初始持有 sword+light/前置 spear·staff←sword、heavy·ward←light)——Q2 已裁沿锚,终值归 M4。
5. **v6→v7 空迁移形态**:缺键补齐模式(v3→v4 先例)——无结构迁移体,_normalize 按默认模型补 forge_routes=初始持有态;幂等零漂移(冒烟断言)。
6. **执行序调整**:T5 存档域先于 T3 写口施工(写口族硬依赖存档域——不满足则 T3 无法落盘),台账记录。
7. **工作间行板布局**(程序占位):件名+Lv+解锁态双通道文案+锻造/解锁/选中三钮横排;锁定态(钥匙未点亮)默认不可见+门控重建=零行板零购买路径(占位纪律延续,2026-10-11 用户令);视觉终稿归 HUD 营地视觉批。
8. **V11 词表钉 31**:卡片 F11「效果类型闭集 24 种」系 2026-10-07 旧快照(现行 31)——L2 事实订正,断言钉现行数(将来加词即转红提示复核)。
9. **F8 域计数订正**:卡片「meta 域 13 件」实为 15 键(vision_used 迭代 22 T2 同日新增)——L2 事实订正,零需求影响。
## 五、GDD 反哺点(设计权威文档不动,逐点待用户裁决后另批落文)
1. **锻造工坊.md**:①「每条路线的内容·等级:1 → 上限」正文宜补 Q1「0=未练合法态」衔接表述(Q1 已在版本历史续裁行落文,正文未同步);②「现役代码事实」表「效果引擎效果类型闭集 24 种」滞后(现行 31);③实装边界 1「行族命名、字段结构与落表位置归实装批定义」已兑现(新表 forge.tres/ROUTE_IDS 六 id/12 字段闭集)——可回填实态句。
2. **元进度.md**:①「跨局注入」节四层公式宜补「甲型基础 AC=选中甲路线行 base_ac(effective_ac 第一层)」终态句(树 flat+锻造路线加值已实装);②营地钥石入口族宜登记锻造三写口(forge_buy_level/forge_buy_unlock/forge_select_*——buy_forge 退役后的新族,契约 9 写口唯一)。
3. **术语表.md**:实现引入路线 id 与存档域内键名(levels/unlocked/weapon/armor)——非玩家可见术语,预计零新词;若需登记补登待裁。
## 六、残留人闸与遗留
- **残留人闸(07 ㉒,用户实机目检)**:工作间门与形态(锁定态零购买路径/点亮后两区六路线)/购买面(几何价逐级涨、余额禁用、即时刷新)/解锁面(双通道:练满派生零收费 vs 200 直购、互不重复收)/选中面(免费切换、当前态、冒险锁死)/注入抽查(选长枪=穿刺生效、选重甲 AC=12+加值、旧路线练度保留)。
- **遗留(在册不阻)**:①敌方 attack_type 填值(3b 裁连带)归敌人系统/战斗结算侧——本批只落结构面(矩阵查表现役已支持),06 在册;②全部数值 provisional 终值归 M4 数值批(未排期);③满级特效本期不做(六路线扩展位留空——不开参数位/不加效果类型词/存档无特效域);④工作间视觉终稿归 HUD 营地视觉批。
- **本批零触碰面**:meta.tres 三行族、克制矩阵九格、局内货架三行(奇物属性件)、天赋树、效果引擎词表(零扩签)。
+3 -2
View File
@@ -717,10 +717,11 @@ entities:
kind: 营地四柜台之三(树「锻造工坊」钥匙节点 free 无前置、点亮展开;未点亮 = 锁定态但配置常驻可读——默认值与解锁状态解耦)
routes: 六条独立等级路线——武器区 长剑锻造(普通/初始)/长枪锻造(穿刺/待解锁)/法杖锻造(魔法/待解锁);护甲区 轻甲锻造(基础 AC10/初始)/重甲锻造(AC12/待解锁)/魔法护甲锻造(ward AC9/待解锁);行名统一「件名+锻造」(8a 裁)
unlock_channels: 并联双通道——练满前一档 OR 一次性解锁价,互不影响、不设串联门槛(4b-1 裁)
currency: 纯钥石单币(5b 裁;几何递增定价历史考古参数 = 20 钥石/150% 公比——forge_products 行族已随迭代 21 T5 退役、无现役计价字段可沿,实装投值归 M4)
currency: 纯钥石单币(5b 裁;几何递增定价 provisional = 基础 20 × 150%^已购级数〔20/30/45/67/101〕/一次性解锁价 200/甲型基础 AC 10-12-9——迭代 23 T1 沿锚表投值,终值归 M4)
selection: 一次冒险 = 一个武器类型 + 一个甲型(1a 裁);营地内免费切换、冒险开始后锁死(1b/1c 裁)
axis_ownership: attack_type/armor_type 默认值由锻造工坊配置常驻持有(默认值 = normal/light)、hero.tres 静态表两轴随实装删除;营地选中态写存档域、冒险开始经参数面注入(v0.6 订正——与现役「表运行期只读」纪律 meta_progression.gd:12-13/119 一致)
save_domain: forge_routes(计划 v7——三武器等级 + 三甲等级 + 当前选中标记;无特效域;旧档破坏性废弃归实装批;已预登记 存档序列化.md)
save_domain: forge_routes(已落地 v7——迭代 23 T5:{levels 六路线等级 + unlocked 直购解锁清单 + weapon/armor 当前选中标记};无特效域;_normalize 缺键补齐 = 初始持有态(sword/light 在档余四锁),v6→v7 空迁移缺键补齐模式;旧档禁迁移禁折算——契约 4)
impl: 实装落位(迭代 23 实装批)——data/schemas/forge_table.gd + data/tables/forge.tres(ForgeTable:默认两轴常驻持有 + 六路线行族,ConfigDB.forge_* 访问器)· core/meta_progression.gd 锻造写口族(forge_buy_level 几何价/forge_buy_unlock 一次性解锁价/forge_select_weapon/armor 免费选中/forge_route_unlocked 派生解锁单点/forge_shop_status 投影)· presentation/views/camp_overlay_view.gd 工作间两区六路线行板(forge_buy/unlock/select_requested 三意图信号,契约 9 UI 只发意图)· core/save_manager.gd forge_routes 域读写访问器
red_lines: 攻防所有权切分——攻击唯一入口 = 武器路线、AC 唯一入口 = 甲路线、树零攻防来源(契约 9 红线);局内无武器/护甲提升(货架三行 = 奇物属性件,现役显示名 攻击祝福/生命祝福/护甲祝福)
max_effect: 满级特效本期不做、只留扩展位(6 裁——不设计清单/不定值/不实装/不开参数位/存档不设特效域)
referenced_by: [元进度, 天赋树, 存档序列化, 术语表]
+14
View File
@@ -577,3 +577,17 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/a
**营地统一**:四柜台并排(树淬炼=树脊柱界面形态主界面/锻造工坊柜台钥匙门+零交互占位面板/秘宝匣柜台钥匙门+工坊六行+同调装配段+Y 升级行迁入/图鉴常开);刷新升级双入口同账(树上 ks_refresh 路由 buy_attune_refresh)。
**验证**:冒烟 886/0(基线 864/0+存量红修复——fx_reveal 三处空 source_class 补标签,a57489e9 落地件 51 行脚本错误清零)+ run_all 22 套(新增 tree_rules_v11 六键闭集校验)+ probe --click 全绿(树购买/双态切换点击路径换代);V10 行为段=引擎直构+编排授予段直注(守侧五场景/聚散同种子配对/限次生命周期)。存量红修复与帐目细节见 design/qa/iter21-实现报告-2026-10-12.md。
## 2026-10-12 迭代 23 锻造工坊实装批(U0——需求卡片 v1.2;设计权威 = 锻造工坊.md v0.6)
**六路线行族新建**:新表 `forge.tres`(schema `forge_table.gd`/ForgeTable——默认两轴常驻持有 + routes 六行;ConfigDB `forge_*` 访问器:forge_route/forge_routes/forge_default_attack_type/forge_default_armor_type/forge_weapon_route_by_type/forge_armor_route_by_type)。路线 id=sword/spear/staff/light/heavy/ward(甲路线 id=甲型值本身);provisional 沿锚表:cap 5/每级增量统一 +1/基础价 20×150%^n(20/30/45/67/101)/一次性解锁价 200/甲型基础 AC 10-12-9/初始持有 sword+light/并联双通道前置 spear·staff←sword、heavy·ward←light。
**两根类型轴所有权移交**:`hero.tres` armor_type/attack_type 两字段删除(schema 同步删)——默认值归 forge.tres 常驻持有(normal/light);选中态写存档域 forge_routes、冒险开始经参数面注入。单位快照读取面=unit_container.spawn 新两参(空串=锻造配置默认值回退,行为逐位连续);冒烟 hero_attack 行类型键主语换代为 forge_default_attack_type()。
**写口族与注入链**:MetaProgression 锻造写口(契约 9 唯一)——forge_buy_level(几何价扣钥石即盘)/forge_buy_unlock(一次性解锁价,已解锁含派生=拒绝零收费)/forge_select_weapon·armor(免费切换只受理已解锁)/forge_route_unlocked(派生单点:初始持有∨直购∨练满前一档)。hero_override 公式换代(FORGE_ROUTE_BONUS 预留常量退役):atk=当前选中武器攻击加值+树 flat、armor_class=当前甲 AC 加值+树 flat、max_hp=树 flat;main.build_adventure_params +attack_type/armor_type 两键 → AdventureRunner 参数面快照(契约 8 冒险开始锁死)→ LevelRunner.set_hero_axes → enc → spawn;入战 AC 三层=甲型基础 AC(选中甲路线 base_ac)+甲路线等级加值+局内加算区。
**存档 v7**:SAVE_FORMAT_VERSION 6→7 + 新域 forge_routes({levels 六键/unlocked 直购清单/weapon/armor 选中},无特效域)+ _migrate_v6_v7 空迁移(缺键补齐=初始持有态)+ 访问器组;旧档缺键补齐零折算。
**营地工作间**:锻造工坊柜台占位面板→两区六路线行板(武器锻造区/护甲锻造区;件名+Lv+解锁态双通道文案+锻造/解锁/选中钮;余额联动禁用;锁定态零行板零购买路径——占位纪律延续);三意图信号 forge_buy/unlock/select_requested 经 camp_shop_presenter 走写口唯一路径。
**验证**:冒烟 938/0(基线 898/0+40 锻造断言)+ run_all 24 套(新增 forge_table_test 41 断言+forge_workbench_test 12 断言)+ probe --click/--full 全绿 + render_gate + doc 三闸/熵增对基线零新增 + MC 同种子配对零漂移。实现报告=design/qa/iter23-实现报告-2026-10-12.md。
+9
View File
@@ -62,6 +62,10 @@ var level_index := 0 # 当前关卡序号(1 起)
var levels_total := 0 # 序列总关数(读 level 表行数)
var keystone := 0 # 钥石入账累计(摘要第三件事的账面)
var master_seed := 0 # 本局主种子(摘要第五件事)
# 两根类型轴冒险开始快照(迭代 23 T4——契约 8:营地选中态经参数面注入、冒险开始后锁死;
# 公有=参数面快照的可断言面〔契约 9 写口唯一,本字段只读不改〕。空串=未注入→消费面回退锻造配置默认值)
var hero_attack_type := ""
var hero_armor_type := ""
var summary := {} # 最近一次结算的冒险摘要(idle 后保留——重开才新生成)
var _runner: LevelRunner # 当前关卡行进器(每关新建——关卡域状态天然隔离)
var _pending_next_level := false # 大奖励已兑付、等 continue_to_next_level 确认
@@ -128,6 +132,8 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
level_index = 1
_pending_next_level = false
_base_override = _normalize_override(params.get("hero_override", {}))
hero_attack_type = String(params.get("attack_type", "")) # 迭代 23 T4——两根类型轴快照(契约 8 锁死:开局后不随存档选中变化)
hero_armor_type = String(params.get("armor_type", ""))
_first_script = bool(params.get("first_script", false)) # 缺省 = 非首次(抽签路径;真实侧由呈现层读存档布尔位传入)
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 迭代 11 第②批 T3——缺省 legacy(off 对照零扰动)
_treasure_unlocked = params.get("treasure_unlocked", {}) if params.get("treasure_unlocked", {}) is Dictionary else {}
@@ -211,6 +217,8 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
_feats.append(String(r))
_keystone_drops = (bp.get("keystone_drops", []) as Array).duplicate(true)
_base_override = _normalize_override(params.get("hero_override", {}))
hero_attack_type = String(params.get("attack_type", "")) # 迭代 23 T4——续玩参数面同 start_adventure(账号层选中态随 resume 重注入)
hero_armor_type = String(params.get("armor_type", ""))
# 生命账本越界钳制(T17——spawn 守卫 EC16 免疫):断点可能由旧口径版本写出(如调试上限
# 调高期带出的账本值 > 新口径生效上限,实例:hero_hp=28 > 基础 26)——恢复后钳到生效上限,
# 否则首战 spawn「hero_hp 入参越界」拒收 → 「敌人系统 spawn 被拒」fatal。上限 = 表值+永久
@@ -936,6 +944,7 @@ func _start_current_level() -> bool:
_runner.set_vision_counter(vision_counter) # 迭代 22 秘宝批 T3——真视使用计数共享 dict 下传(编排器受理成功 +1)
_runner.set_feats(_feats) # 迭代 12 T1——特性持有集下传(begin_battle enc.feats 求值面)
_runner.set_tree_rules(_tree_rules) # 迭代 12 T10——树 rule 键集下传(A/B 面消费)
_runner.set_hero_axes(hero_attack_type, hero_armor_type) # 迭代 23 T4——两根类型轴快照下传(effective_ac 甲型基础 AC 层+enc 携带进 spawn)
if _shop_mode == "pool":
# 商店共享池注入(迭代 11 第②批 T3——库存账本引用+消费 handler 双下传;legacy 模式零注入零扰动。
# 迭代 20 T2——cap 检查回调四参注入)
+2 -1
View File
@@ -360,7 +360,8 @@ func begin_battle(enc: Dictionary) -> bool:
# E4 收口:加成经 encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径)
_hero_atk = int(enc.get("hero_atk", int(db.hero.atk)))
if not _enemy_system.spawn(int(enc.level_index), int(enc.node_depth), int(enc.hero_hp), count_overrides,
_hero_atk, int(enc.get("hero_max_hp", -1)), atk_overrides):
_hero_atk, int(enc.get("hero_max_hp", -1)), atk_overrides,
String(enc.get("attack_type", "")), String(enc.get("armor_type", ""))): # 迭代 23 T4——两根类型轴(空串=锻造配置默认值回退,行为连续)
_fatal("敌人系统 spawn 被拒")
return false
if not _install_new_board():
+45
View File
@@ -27,6 +27,7 @@ const TABLE_MANIFEST := {
"treasure_items": "treasure_items.tres", # 迭代 7 T1 秘宝内容批(TreasureItemTable 六行——定义表 Rule 4 豁免;罗盘 retired/失义件 deferred 标记见 schema 头注)
"adventure": "adventure.tres", # 冒险结构实现批 2026-09-21(AdventureTable 建表——大奖励与钥石行值单点)
"feats": "feats.tres", # 迭代 12 T1 双层 BD 规则改写型特性基建(FeatTable 定义表 Rule 4 豁免——TreasureItemTable 先例;空表合法态=主时间线零扰动)
"forge": "forge.tres", # 迭代 23 T1 锻造工坊实装批(ForgeTable——两根类型轴默认值+六路线行族;攻防所有权切分唯一数据面)
}
var action_economy: ActionEconomyTable
@@ -49,6 +50,7 @@ var meta_tree: MetaTreeTable # 迭代 12 T8 类型化持用(消费面走 tre
var treasure_items: TreasureItemTable # 迭代 7 T1 类型化持用(消费面走 treasure_* 访问器)
var adventure: AdventureTable
var feats: FeatTable # 迭代 12 T1 类型化持用(消费面走 feat_* 访问器)
var forge: ForgeTable # 迭代 23 T1 锻造工坊配置(默认两轴+六路线行族——消费面走 forge_* 访问器)
var is_ready := false
var _diagnostics: Array[Dictionary] = [] # {severity, category, message}
@@ -140,6 +142,7 @@ func load_tables(root: String, collect_only: bool) -> bool:
treasure_items = loaded.get("treasure_items")
adventure = loaded.get("adventure")
feats = loaded.get("feats")
forge = loaded.get("forge")
meta_tree = loaded.get("meta_tree")
is_ready = not has_errors
return not has_errors
@@ -387,6 +390,48 @@ func _meta_rows(family: String) -> Array:
return arr if arr is Array else []
# ---- 锻造工坊(迭代 23 T1——ForgeTable 消费面:两根类型轴默认值+六路线行族;禁自扫行集)----
# 设计权威 design/gdd/systems/跨局循环/锻造工坊.md v0.6:默认值归本表常驻持有(hero.tres 两轴已删——
# 契约 2);路线行族=攻防数值成长唯一数据面(契约 1)。数值全 provisional 归 M4。
# 两根类型轴默认值(表未落籍=裸体态兜底 normal/light——与 ForgeTable 缺省值同口径)
func forge_default_attack_type() -> String:
return String(forge.default_attack_type) if forge != null else "normal"
func forge_default_armor_type() -> String:
return String(forge.default_armor_type) if forge != null else "light"
# 六路线行族(副本——调用方禁改)
func forge_routes() -> Array:
return forge.routes.duplicate() if forge != null else []
# 路线行单点(键 route_id;缺行=空字典)
func forge_route(route_id: String) -> Dictionary:
for row in forge_routes():
if String(row.get("route_id", "")) == route_id:
return row
return {}
# 按类型轴反查武器路线(attack_type → 路线行;甲路线 id=甲型同值的显式化入口,避免消费侧依赖该巧合)
func forge_weapon_route_by_type(attack_type: String) -> Dictionary:
for row in forge_routes():
if String(row.get("kind", "")) == "weapon" and String(row.get("type_axis", "")) == attack_type:
return row
return {}
# 按类型轴反查护甲路线(armor_type → 路线行——入战 AC 三层之「甲型基础 AC」取数口)
func forge_armor_route_by_type(armor_type: String) -> Dictionary:
for row in forge_routes():
if String(row.get("kind", "")) == "armor" and String(row.get("type_axis", "")) == armor_type:
return row
return {}
# 技能行单点读取(敌人技能池批——单位容器执行上报 / 战斗结算兑现的消费面)
func skill_row(skill_id: String) -> Dictionary:
return skill.row_by_id(skill_id)
+26 -2
View File
@@ -68,6 +68,10 @@ var _attunement: Array = [] # 同调装配集(迭代 11 第
var _attune_charges: Dictionary = {} # 同调主动件充能账本(迭代 11 第①批 T6——adventure 共享 dict 引用;settlement use_treasure 扣减直改同引用)
# 关卡末清空+快照计数入 level_finished 载荷)
var _vision_counter: Dictionary = {"used": 0} # 真视类工具使用计数(迭代 22 秘宝批 T3——adventure 共享 dict 引用;编排器受理成功 +1,收束聚合入档)
# 两根类型轴(迭代 23 T4——adventure 参数面快照经 set_hero_axes 下传;空串=未注入→锻造配置默认值回退。
# 消费面:effective_ac 甲型基础 AC 层 + begin_battle enc 携带进 unit_container.spawn 英雄快照)
var _hero_attack_type := ""
var _hero_armor_type := ""
# ---- 商店共享池模式态(迭代 11 第②批 T3——缺省 legacy=固定三件加定价 off 对照承载,零 RNG 零池消费;TP0 令牌池换代)----
var _shop_pool_mode := false # pool 模式旗(set_shop_pool 注入时置位——AdventureRunner 按 params.shop_mode 下传)
var _pool_deck: Array = [] # 共享池令牌库共享引用(AdventureRunner._curio_deck——库存=剩余令牌数单点)
@@ -120,9 +124,21 @@ func effective_max_hp() -> int:
return maxi(1, int(round(float(base) * (100.0 + float(hero_hp_pct)) / 100.0)))
# 护甲等级生效值(永久强化 flat + 加算区 armor_class 轴——无调试浮窗百分比通道,纯 flat)
# 护甲等级生效值(入战 AC 三层可加——迭代 23 T4:甲型基础 AC〔forge.tres 选中甲路线 base_ac〕+
# 甲路线等级加值〔base_override.armor_class——hero_override 锻造分量〕+ 加算区 armor_class 轴;
# 无调试浮窗百分比通道,纯 flat)
func effective_ac() -> int:
return maxi(0, int(db.hero.armor_class) + int(base_override.get("armor_class", 0)) + int(bonus.get("armor_class", 0)))
return maxi(0, _armor_base_ac() + int(base_override.get("armor_class", 0)) + int(bonus.get("armor_class", 0)))
# 甲型基础 AC(三层之第一层——选中甲路线行 base_ac;未注入/缺行=锻造配置默认值路线=轻甲 10,
# 与 hero.tres 两轴删除前 armor_class=10 行为连续)
func _armor_base_ac() -> int:
var at: String = _hero_armor_type if _hero_armor_type != "" else db.forge_default_armor_type()
var row: Dictionary = db.forge_armor_route_by_type(at)
if row.is_empty():
return int(db.hero.armor_class) # 表缺失防御兜底(现役基准)
return int(row.get("base_ac", 0))
# 行动点加成生效值(迭代 10 奖励重构批——统一冒险加算区第四轴 ap;经战斗开始入参 hero_ap_bonus
@@ -480,6 +496,7 @@ func _start_battle(node: Dictionary) -> bool:
if not _settlement.begin_battle({"level_index": int(level_row.level_index), "node_depth": int(node.get("node_depth", 0)),
"hero_hp": hero_hp, "hero_atk": effective_atk(), "hero_max_hp": effective_max_hp(),
"hero_armor_class": effective_ac(), "hero_ap_bonus": effective_ap_bonus(),
"attack_type": _hero_attack_type, "armor_type": _hero_armor_type, # 迭代 23 T4——两根类型轴(spawn 英雄快照;空串=锻造配置默认值回退)
"attunement": _attunement,
"attune_charges": _attune_charges,
"vision_counter": _vision_counter, # 迭代 22 秘宝批 T3——真视使用计数共享 dict(编排器受理成功 +1,收束入档)
@@ -637,6 +654,13 @@ func set_tree_rules(rules: Array) -> void:
_tree_rules = rules.duplicate()
# 两根类型轴注入(迭代 23 T4——adventure 参数面快照;空串=未注入→锻造配置默认值回退。
# 消费面:effective_ac 甲型基础 AC 层 + begin_battle enc 携带进 unit_container.spawn 英雄快照)
func set_hero_axes(attack_type: String, armor_type: String) -> void:
_hero_attack_type = attack_type
_hero_armor_type = armor_type
# adventure 域计数器共享引用注入(迭代 20 T6——AdventureRunner 冒险级账本引用下传;
# begin_battle enc.adventure_counters 携带进效果引擎 rebuild 回灌与战末 counters_export 回写——
# **共享引用不复制**,attune_charges 同款;缺省空 dict=零扰动)
+158 -10
View File
@@ -9,8 +9,9 @@
# 钥石直购价随定价批后补最终双渠道;六件局内效果归 Alpha 秘宝重设计批,本类只有解锁状态);
# ③ 冒险收束入账(Boss 两档 = 表值 first_kill/repeat_kill,首杀标记读存档判first;调 SaveManager
# apply_adventure_summary 入账并附 boss_drops 副本——复盘可归因,验收 5)。
# 注入面:hero_override() = 三档 forge flat(全零 = 白板合法态;override 不写表——表运行期只读,
# 消费方 AdventureRunner/LevelRunner 经冒险开始参数面显式传参)。
# 注入面:hero_override() = 树生命/命中淬炼 flat + 锻造路线加值(当前选中武器/甲——迭代 23 T4 换代,
# 全零 = 白板合法态;override 不写表——表运行期只读,消费方 AdventureRunner/LevelRunner 经冒险开始
# 参数面显式传参)。
# 纪律:数值零字面量——价格/档位/阈值/两档金额只读 db.meta_* 访问器;存档只经 SaveManager 访问器,
# 禁读内部结构;行集解析禁自扫表(一律走 ConfigDB 访问器)。
class_name MetaProgression
@@ -123,6 +124,151 @@ func treasure_shop_status() -> Array:
return out
# ---- 锻造工坊写口族(迭代 23 T3——契约 9:扣币/升级/解锁/选中/写盘只经本组;UI 只发意图信号)----
# 设计权威 design/gdd/systems/跨局循环/锻造工坊.md v0.6:钥石单币(契约 3——GP 局内一次性货币
# 永不互兑)、并联双通道解锁(4b-1——练满前一档 OR 一次性解锁价,互不影响不设串联)、
# 一次冒险一武器一甲(契约 8——营地免费切换、冒险开始锁死由参数面快照保证)。
# 等级语义(Q1):未解锁/未锻造=0 加值(裸体态);锻造第一级=「获得装备」=首个 +1;逐级递进。
# 路线解锁态派生(不落档——初始持有∨直购∨练满前一档;派生单点只此一处)
func forge_route_unlocked(route_id: String) -> bool:
var row: Dictionary = db.forge_route(route_id)
if row.is_empty():
return false
if bool(row.get("initial_owned", false)):
return true
if save.forge_unlocked_purchases().has(route_id):
return true
var prereq := String(row.get("prereq_route", ""))
if prereq != "":
var prow: Dictionary = db.forge_route(prereq)
if not prow.is_empty() and save.forge_route_level(prereq) >= int(prow.get("cap", 0)):
return true
return false
# 路线购买(几何价=基础价×150%^已购级数——锚表 20/30/45/67/101;等级+1 即时写盘)。
# 未解锁/已满级/余额不足/未知路线 = 拒绝且零副作用(spend_keystone 内嵌余额闸,失败不扣币不写盘)。
func forge_buy_level(route_id: String) -> bool:
var row: Dictionary = db.forge_route(route_id)
if row.is_empty() or not forge_route_unlocked(route_id):
return false
var lvl := save.forge_route_level(route_id)
if lvl >= int(row.get("cap", 0)):
return false
var price := ForgeTable.level_price(row, lvl)
if not save.spend_keystone(price):
return false
save.set_forge_route_level(route_id, lvl + 1)
return save.write_to_disk()
# 一次性解锁价直购(并联双通道②)。已解锁(含练满前一档派生)= 拒绝零收费——两通道互不影响:
# 未练满也可直购;直购后练满不重复收(练满通道本就是派生、零收费)。初始持有行无解锁面。
func forge_buy_unlock(route_id: String) -> bool:
var row: Dictionary = db.forge_route(route_id)
if row.is_empty() or bool(row.get("initial_owned", false)):
return false
if forge_route_unlocked(route_id):
return false
var price := int(row.get("unlock_price", 0))
if price < 1 or not save.spend_keystone(price):
return false
save.add_forge_unlock(route_id)
return save.write_to_disk()
# 当前武器/甲选中切换(免费——只受理已解锁路线;写存档域+即时写盘。冒险开始锁死归参数面快照,
# 契约 8;冒险中无切换入口=UI 面无路径可达本口)
func forge_select_weapon(route_id: String) -> bool:
var row: Dictionary = db.forge_route(route_id)
if row.is_empty() or String(row.get("kind", "")) != "weapon" or not forge_route_unlocked(route_id):
return false
save.set_forge_weapon(route_id)
return save.write_to_disk()
func forge_select_armor(route_id: String) -> bool:
var row: Dictionary = db.forge_route(route_id)
if row.is_empty() or String(row.get("kind", "")) != "armor" or not forge_route_unlocked(route_id):
return false
save.set_forge_armor(route_id)
return save.write_to_disk()
# 当前选中武器攻击加值 / 当前甲 AC 加值(路线等级×每级增量——0=裸体态/未锻造合法态)。
# 选中态异常(未解锁/表缺行)防御回退 0——注入面只认合法态,禁把脏选中带进冒险。
func forge_weapon_atk_bonus() -> int:
var rid := save.forge_weapon()
var row: Dictionary = db.forge_route(rid)
if row.is_empty() or String(row.get("kind", "")) != "weapon" or not forge_route_unlocked(rid):
return 0
return save.forge_route_level(rid) * int(row.get("per_level_gain", 0))
func forge_armor_ac_bonus() -> int:
var rid := save.forge_armor()
var row: Dictionary = db.forge_route(rid)
if row.is_empty() or String(row.get("kind", "")) != "armor" or not forge_route_unlocked(rid):
return 0
return save.forge_route_level(rid) * int(row.get("per_level_gain", 0))
# 两根类型轴当前生效值(选中路线类型轴;选中态异常/未解锁=锻造配置默认值——契约 2 回退面)。
# 默认值与柜台解锁状态解耦:树钥匙未点亮时本函数照常返回默认值(配置常驻可读)。
func hero_attack_type() -> String:
var rid := save.forge_weapon()
var row: Dictionary = db.forge_route(rid)
if row.is_empty() or String(row.get("kind", "")) != "weapon" or not forge_route_unlocked(rid):
return db.forge_default_attack_type()
return String(row.get("type_axis", ""))
func hero_armor_type() -> String:
var rid := save.forge_armor()
var row: Dictionary = db.forge_route(rid)
if row.is_empty() or String(row.get("kind", "")) != "armor" or not forge_route_unlocked(rid):
return db.forge_default_armor_type()
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)。
@@ -147,18 +293,20 @@ func settle_adventure(summary: Dictionary) -> Dictionary:
# ---- 跨局注入(冒险开始参数面)----
# 迭代 21 T4 注入公式换代:基础值 + 树生命/命中淬炼 flat(命中线在账未注入——契约 6,消费面归
# 数值批)+ 锻造路线加值位(本批预留 0——锻造工坊实装批按当前武器/甲路线等级填充)+ 局内加算区
# (bonus 区域不经本面)。旧属性柜台 forge_flat 项退役移除(V7/V12);攻/AC 树来源随所有权切分
# 砍除(契约 9——树零攻防来源,现表零 atk/armor_class 行,tree_stat_flat 二轴恒 0 保留通式)。
# 迭代 23 T4 注入公式换代(锻造工坊实装批——契约 1 攻防所有权切分落地):基础值 + 树生命/命中淬炼
# flat(命中线在账未注入——契约 6,消费面归数值批)+ **锻造路线加值**(当前选中武器攻击加值 + 当前甲
# AC 加值——读存档 forge_routes 域+路线表)+ 局内加算区(bonus 区域不经本面)。入战 AC 三层可加:
# 甲型基础 AC(forge.tres 行 base_ac)+ 甲路线等级加值(本面 armor_class 分量)+ 局内加算区。
# 旧属性柜台 forge_flat 项退役移除(V7/V12);攻/AC 树来源随所有权切分砍除(契约 9——树零攻防来源,
# 现表零 atk/armor_class 行,tree_stat_flat 二轴恒 0 保留通式)。原 FORGE_ROUTE_BONUS 预留常量随本批
# 退役(公式不再出现——V7/V12)。
# 全零 = 白板合法态(无解锁不得改变任何局内规则);override 禁写表(表运行期只读)。
const FORGE_ROUTE_BONUS := {"atk": 0, "max_hp": 0, "armor_class": 0} # 锻造路线加值位(预留 0——锻造工坊实装批填充)
func hero_override() -> Dictionary:
return {
"atk": int(FORGE_ROUTE_BONUS.get("atk", 0)) + tree_stat_flat("atk"),
"max_hp": int(FORGE_ROUTE_BONUS.get("max_hp", 0)) + tree_stat_flat("max_hp"),
"armor_class": int(FORGE_ROUTE_BONUS.get("armor_class", 0)) + tree_stat_flat("armor_class"),
"atk": forge_weapon_atk_bonus() + tree_stat_flat("atk"),
"max_hp": tree_stat_flat("max_hp"),
"armor_class": forge_armor_ac_bonus() + tree_stat_flat("armor_class"),
}
+79 -1
View File
@@ -21,6 +21,10 @@
# tree_nodes / fork_sides / tree_struct —— 内容 owner = 天赋树.md(迭代 12 T9 建域、迭代 21 T5
# 换代承载:tree_nodes=node_id→已购等级;fork_sides=fork_group→激活态;tree_struct=树结构
# 版本标记〔缺失=换代前结构→加载时树域破坏性废弃重置——开发阶段无迁移,Q3 用户裁〕)
# forge_routes —— 内容 owner = 锻造工坊.md(迭代 23 T5:levels 六路线等级+unlocked 直购解锁清单
# +weapon/armor 当前选中标记;**无特效域**(裁 6——满级特效本期不做);解锁态派生不落档:
# 初始持有〔表 initial_owned〕∨ 直购〔unlocked〕∨ 练满前一档〔levels[prereq]≥cap〕——派生单点
# 在 MetaProgression.forge_route_unlocked;旧档缺键补齐=初始持有态,开发阶段禁迁移禁折算)
# breakpoint.attunement_charges(主动件充能余量)——每冒险重置语义的断点瞬态附随件,内容装配归
# AdventureRunner(随 T6 接线入断点),本类照单承载;契约 2 域界注记同断点域豁免先例
# recent_summaries(冒险摘要五件事副本)—— 内容 owner = 冒险结构.md(摘要五件事口径)
@@ -46,7 +50,7 @@
class_name SaveManager
extends RefCounted
const SAVE_FORMAT_VERSION := 6 # registry schema_version:v6=生涯终身计数域(迭代 18 卡④——meta 两新域 adventure_total〔冒险总局数,开局即计含放弃与失败〕/adventure_cleared〔通关局数,三关全通才计 1——判定字段 summary.cleared〕,旧档缺键由 _normalize 按默认补零,收束自增唯一入账=apply_adventure_summary);v5=天赋树域(迭代 12 T9——meta 三新域 tree_nodes〔node_id→已购等级〕/fork_sides〔fork_group→a|b〕/migration_note〔三体系迁移补偿注记〕——M-4 迭代 12 批次二;需求卡片「v3→v4」系同调批未落时行文,v4 已被同调批占用顺延 v5 披露);v4=同调批扩域(meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level——M-4 迭代 11 第①批 T2;断点充能余量随 T6 接线,旧档缺键 = 合法空态);v3=断点附随件扩列+摘要 keystone_drops(M-3 迭代 10 奖励重构);v2=断点域出生(M-3 迭代 8 卡①);v1=首版;高版本拒读 owner=存档序列化
const SAVE_FORMAT_VERSION := 7 # registry schema_version:v7=锻造工坊域(迭代 23 T5——meta 新域 forge_routes〔六路线等级 levels+直购解锁清单 unlocked+当前武器/甲选中标记 weapon/armor;无特效域——裁 6 满级特效本期不做〕,旧档缺键由 _normalize 按默认补齐=初始持有态〔sword/light 在档余四锁、选中=初始路线〕,v6→v7 空迁移=缺键补齐模式——v3→v4 先例);v6=生涯终身计数域(迭代 18 卡④——meta 两新域 adventure_total〔冒险总局数,开局即计含放弃与失败〕/adventure_cleared〔通关局数,三关全通才计 1——判定字段 summary.cleared〕,旧档缺键由 _normalize 按默认补零,收束自增唯一入账=apply_adventure_summary);v5=天赋树域(迭代 12 T9——meta 三新域 tree_nodes〔node_id→已购等级〕/fork_sides〔fork_group→a|b〕/migration_note〔三体系迁移补偿注记〕——M-4 迭代 12 批次二;需求卡片「v3→v4」系同调批未落时行文,v4 已被同调批占用顺延 v5 披露);v4=同调批扩域(meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level——M-4 迭代 11 第①批 T2;断点充能余量随 T6 接线,旧档缺键 = 合法空态);v3=断点附随件扩列+摘要 keystone_drops(M-3 迭代 10 奖励重构);v2=断点域出生(M-3 迭代 8 卡①);v1=首版;高版本拒读 owner=存档序列化
const SUMMARY_CAP := 10 # 最近冒险摘要保留条数(最新在前)
const TREE_STRUCT_VERSION := 2 # 树结构版本标记(迭代 21 T5——12 节点 v0.8 结构;加载检测缺失
@@ -67,6 +71,7 @@ func _init(path: String = "user://save.json") -> void:
register_migration(3, _migrate_v3_v4)
register_migration(4, _migrate_v4_v5)
register_migration(5, _migrate_v5_v6)
register_migration(6, _migrate_v6_v7)
# v1→v2:断点域出生(旧档无断点 = 合法空态——「继续上局」隐藏,非损坏非债务)
@@ -114,6 +119,14 @@ func _migrate_v5_v6(data: Dictionary) -> Dictionary:
return data
# v6→v7:锻造工坊域(迭代 23 T5——meta 新域 forge_routes,旧档缺键由 _normalize 按默认补齐=
# 初始持有态〔开发阶段无迁移禁折算——契约 4〕,无结构迁移体;版本号推进仅为 schema_version 对齐
# ——v3→v4 先例。幂等性:重复迁移只覆写 version,既有值零改动;加载后 _normalize 补齐缺省——旧档可载可玩)
func _migrate_v6_v7(data: Dictionary) -> Dictionary:
data["version"] = SAVE_FORMAT_VERSION
return data
# ---- 默认数据模型(新档合法态——全零;白板存档 = 完整可玩〔元进度 单局循环零依赖〕)----
func _default_data() -> Dictionary[String, Variant]:
@@ -136,6 +149,12 @@ func _default_data() -> Dictionary[String, Variant]:
"adventure_cleared": 0, # 生涯终身计数·通关局数(三关全通才计 1——判定字段 summary.cleared)
"vision_used": 0, # 生涯终身计数·真视类工具累计使用次数(迭代 22 T2——信标网络解锁行为条件;
# 受理成功唯一计数点=battle 层经冒险层聚合、收束入账;缺键补零不升版本——迭代 21 先例)
"forge_routes": { # 锻造工坊域(迭代 23 T5——六路线等级+直购解锁清单+当前选中标记;无特效域)
"levels": {"sword": 0, "spear": 0, "staff": 0, "light": 0, "heavy": 0, "ward": 0},
"unlocked": [], # 直购解锁路线 id 清单(初始持有与练满前一档=派生解锁不落档)
"weapon": "sword", # 当前选中武器路线(初始持有=sword)
"armor": "light", # 当前选中甲路线(初始持有=light)
},
},
"breakpoint": {}, # 冒险断点快照(空 = 无断点合法态——「继续上局」隐藏;迭代 8 卡①)
}
@@ -314,6 +333,65 @@ func set_fork_side(group: String, side: String) -> void:
_data["meta"]["fork_sides"] = fs
# ---- 锻造工坊域 forge_routes(迭代 23 T5——MetaProgression 锻造写口读写面;无特效域)----
# 解锁态不落档(派生):初始持有=表 initial_owned、练满前一档=levels[prereq]≥cap——判定单点在
# MetaProgression.forge_route_unlocked(读表+读档);本类只承载直购清单 unlocked 与选中标记。
# 域结构:{levels:{六路线 id→等级}, unlocked:[直购路线 id], weapon:当前武器路线 id, armor:当前甲路线 id}
func _forge_domain() -> Dictionary:
return _data["meta"].get("forge_routes", {}) as Dictionary
func forge_routes_snapshot() -> Dictionary:
return _forge_domain().duplicate(true) # 值拷贝出户(本类契约「禁碰内部结构」——消费方直改不得渗入存档内存态)
func forge_route_level(route_id: String) -> int:
var lv: Dictionary = _forge_domain().get("levels", {}) as Dictionary
return int(lv.get(route_id, 0))
func set_forge_route_level(route_id: String, level: int) -> void:
var dom: Dictionary = _forge_domain()
var lv: Dictionary = dom.get("levels", {}) as Dictionary
lv[route_id] = maxi(0, level)
dom["levels"] = lv
_data["meta"]["forge_routes"] = dom
func forge_unlocked_purchases() -> Array:
return (_forge_domain().get("unlocked", []) as Array).duplicate()
func add_forge_unlock(route_id: String) -> void:
var dom: Dictionary = _forge_domain()
var un: Array = dom.get("unlocked", []) as Array
if not un.has(route_id):
un.append(route_id)
dom["unlocked"] = un
_data["meta"]["forge_routes"] = dom
func forge_weapon() -> String:
return String(_forge_domain().get("weapon", "sword"))
func set_forge_weapon(route_id: String) -> void:
var dom: Dictionary = _forge_domain()
dom["weapon"] = route_id
_data["meta"]["forge_routes"] = dom
func forge_armor() -> String:
return String(_forge_domain().get("armor", "light"))
func set_forge_armor(route_id: String) -> void:
var dom: Dictionary = _forge_domain()
dom["armor"] = route_id
_data["meta"]["forge_routes"] = dom
# 装配关系(同调目录 item_id 有序数组——装配序即呈现序;跨冒险持久,冒险中途不可换装〔底稿 §8.5〕)。
# 局外进度事实(元进度域同族),非契约 2 反面清单的「局内道具库存」——存档序列化.md 契约 2 随批注记。
func attunement_equipped() -> Array:
+5 -3
View File
@@ -10,7 +10,8 @@
# (pattern 环数组 + steps 指针 + heavy_multiplier)随技能池整体退役——拍结构保留(每结算每存活敌恰行动一次、
# 行序、先作用后推进),"轮到什么"改由开轮摇号决定(死亡即不动手:摇定后兑现前死亡 → 本拍作废、不 commit)。
# 切片登记接口漂移:heal() 为切片新增(战斗结算 治疗 clamp 需要写入口);spawn 签名 +3 参数(E4/区间批/
# 遭遇骰——正式侧 GDD/registry 修订随对应批)。
# 遭遇骰——正式侧 GDD/registry 修订随对应批);迭代 23 T2 +2 参数(hero_attack_type/hero_armor_type——
# 两根类型轴注入面:hero.tres 静态表两轴删除〔契约 2 所有权移交〕,空串=锻造工坊配置默认值回退)。
# 实现 GDD:design/gdd/systems/战斗循环/敌人系统.md(registry 追溯锚,命名规范见 CLAUDE.md)
class_name UnitContainer
extends RefCounted
@@ -44,7 +45,8 @@ func _init(db_ref: ConfigDb) -> void:
# 逐行实例化、生成序 = 文件行序(行序即契约,敌人系统 v0.15);行集实例总数(各行传入数量求和)
# ≤ ENEMY_ROWSET_INSTANCE_MAX 守卫 fail-fast。
func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides: Array = [],
atk_override: int = -1, max_hp_override: int = -1, enemy_atk_overrides: Array = []) -> bool:
atk_override: int = -1, max_hp_override: int = -1, enemy_atk_overrides: Array = [],
hero_attack_type: String = "", hero_armor_type: String = "") -> bool:
if _spawned:
push_warning("[敌人系统] Alive 相位重复 spawn(相位守卫)")
return false
@@ -61,7 +63,7 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides
return false
# max_hp = 生效上限(非入参 hero_hp)——2026-09-11 关卡扩展批修复:跨战斗携带 HP 时上限不得压扁
# (恢复点/治疗包的满血守卫与治疗上限随之失效——切片登记接口语义澄清)
_hero = {"hp": hero_hp, "max_hp": eff_max_hp, "atk": eff_atk, "is_hero": true, "alive": true, "display_name": "探险者", "enemy_id": "hero", "armor_type": String(db.hero.get("armor_type")) if db.hero.get("armor_type") else "light", "attack_type": String(db.hero.get("attack_type")) if db.hero.get("attack_type") else "normal"} # 护甲/攻击类型轴快照(2026-09-18 营地扩展前置批——可配置;甲效矩阵消费面)
_hero = {"hp": hero_hp, "max_hp": eff_max_hp, "atk": eff_atk, "is_hero": true, "alive": true, "display_name": "探险者", "enemy_id": "hero", "armor_type": hero_armor_type if hero_armor_type != "" else db.forge_default_armor_type(), "attack_type": hero_attack_type if hero_attack_type != "" else db.forge_default_attack_type()} # 护甲/攻击类型轴快照(迭代 23 T2——hero.tres 两轴删除后改读注入值,空串回退锻造工坊配置默认值 normal/light=行为连续;甲效矩阵消费面)
_enemies.clear() # 难度曲线与Boss DifficultyTable 修饰(2026-09-12 结构位兑现):施加于行值、行值快照之前——MVP 空表 = 恒 1.0 无行为(AC-10);混编行集内全部敌种同乘((L,D) 精确匹配一次取值——F9 同乘不改变异,敌人系统 v0.15)
var mod: Dictionary = db.resolve_difficulty_modifiers(level_index, node_depth)
var hp_mult: float = float(mod.get("hp_mult", 1.0))
+131
View File
@@ -0,0 +1,131 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Date: 2026-10-12(迭代 23 锻造工坊实装批 T1——设计权威 design/gdd/systems/跨局循环/锻造工坊.md v0.6)
# 锻造工坊配置表(ForgeTable):两根类型轴默认值(常驻持有——契约 2)+ 六条锻造路线行族(三武器+三甲)。
# 所有权切分(契约 1 红线):攻击唯一入口=武器路线、AC 唯一入口=甲路线——树零攻防来源,本表是攻防
# 数值成长的唯一数据面。等级语义(Q1 用户裁):未解锁/未锻造=0 加值(裸体态,类型=默认轴值);
# 解锁并锻造第一级=「获得装备」=1 级=首个 +1;逐级递进(2 级=+2 量级)。
# 数值全 provisional(需求卡片 T1 锚表投值——终值归 M4 数值批):cap 5/每级增量统一 +1(v1.2 用户裁)/
# 基础价 20/公比 150%(各级价格 20/30/45/67/101)/一次性解锁价 200/甲型基础 AC 10-12-9(现役结构值)。
# 表运行期只读(铁律):永久强化只写存档(forge_routes 域),本表零写面。
class_name ForgeTable
extends Resource
# 路线 id 词表(代拍板立项:甲路线 id=甲型值本身——ward=魔法护甲,3a 裁统一命名)
const WEAPON_IDS := ["sword", "spear", "staff"]
const ARMOR_IDS := ["light", "heavy", "ward"]
const ROUTE_IDS := ["sword", "spear", "staff", "light", "heavy", "ward"]
# 两根类型轴枚举域(hero.tres 静态表两轴随本批删除——词表归本表常驻持有)
const ATTACK_TYPES := ["normal", "pierce", "magic"]
const ARMOR_TYPES := ["light", "ward", "heavy"]
# 行字段闭集(V11:满级特效本期不做——行族无特效参数位;键集出域即拒)
const ROW_KEYS := ["route_id", "display_name", "kind", "type_axis", "cap", "per_level_gain",
"base_ac", "base_price", "price_ratio_pct", "initial_owned", "prereq_route", "unlock_price"]
@export var default_attack_type: String = "normal" # 攻击类型轴默认值(常驻持有——与柜台解锁状态解耦:未点亮=无交互入口但默认值照常注入)
@export var default_armor_type: String = "light" # 甲型轴默认值(常驻持有)
@export var routes: Array[Dictionary] = [] # 六路线行族(键 route_id)
# 各级价格(几何递增:基础价 × 150%^已购级数——锚表 20/30/45/67/101;1.5^n 二进制精确,无浮点尘)
static func level_price(row: Dictionary, owned_levels: int) -> int:
var base := int(row.get("base_price", 0))
var ratio := float(int(row.get("price_ratio_pct", 100))) / 100.0
return int(floor(float(base) * pow(ratio, maxi(0, owned_levels))))
func validate() -> Array:
var errs: Array[Dictionary] = []
# 默认两轴枚举域
if not ATTACK_TYPES.has(default_attack_type):
errs.append({"category": "enum", "message": "default_attack_type 出域 %s: %s" % [ATTACK_TYPES, default_attack_type]})
if not ARMOR_TYPES.has(default_armor_type):
errs.append({"category": "enum", "message": "default_armor_type 出域 %s: %s" % [ARMOR_TYPES, default_armor_type]})
# 行族规模恰六(三武器+三甲——需求卡片 §1①)
if routes.size() != 6:
errs.append({"category": "structural", "message": "锻造路线行族规模 %d ≠ 6(三武器+三甲)" % routes.size()})
var seen := {}
var type_seen := {}
var kind_by_id := {}
var initial_ids := {}
for row in routes: # 预扫:route_id→kind 全集(前置反查需前向引用——轻甲行在长枪行之后)
kind_by_id[String(row.get("route_id", ""))] = String(row.get("kind", ""))
for row in routes:
var rid := String(row.get("route_id", ""))
if not ROUTE_IDS.has(rid):
errs.append({"category": "enum", "message": "锻造路线 route_id 出域 %s: %s" % [ROUTE_IDS, rid]})
if seen.has(rid):
errs.append({"category": "dup_key", "message": "锻造路线 route_id 重复: %s" % rid})
seen[rid] = true
var kind := String(row.get("kind", ""))
if not ["weapon", "armor"].has(kind):
errs.append({"category": "enum", "message": "%s kind 出域 {weapon, armor}: %s" % [rid, kind]})
continue
# 类型轴:武器=attack_type 域、甲=armor_type 域;每型恰一路线(三武器三甲各一)
var axis := String(row.get("type_axis", ""))
var domain: Array = ATTACK_TYPES if kind == "weapon" else ARMOR_TYPES
if not domain.has(axis):
errs.append({"category": "enum", "message": "%s type_axis 出域 %s: %s" % [rid, domain, axis]})
var type_key := "%s|%s" % [kind, axis]
if type_seen.has(type_key):
errs.append({"category": "dup_key", "message": "%s 类型轴重复(每型恰一路线): %s %s" % [kind, axis, rid]})
type_seen[type_key] = true
# 等级/增量/价格/基础 AC 值域
var cap_v: int = int(row.get("cap", -1))
if cap_v < 1 or cap_v > 9:
errs.append({"category": "domain", "message": "%s cap 越域 [1,9]: %d" % [rid, cap_v]})
var gain: int = int(row.get("per_level_gain", -1))
if gain < 0 or gain > 9:
errs.append({"category": "domain", "message": "%s per_level_gain 越域 [0,9]: %d" % [rid, gain]})
var base_ac: int = int(row.get("base_ac", -1))
if kind == "armor":
if base_ac < 0 or base_ac > 999:
errs.append({"category": "domain", "message": "%s base_ac 越域 [0,999]: %d" % [rid, base_ac]})
elif base_ac != 0:
errs.append({"category": "cross_field", "message": "%s 武器行 base_ac 须为 0(甲型基础 AC 只属护甲路线): %d" % [rid, base_ac]})
var base_price: int = int(row.get("base_price", -1))
if base_price < 1 or base_price > 999:
errs.append({"category": "domain", "message": "%s base_price 越域 [1,999]: %d" % [rid, base_price]})
var ratio: int = int(row.get("price_ratio_pct", -1))
if ratio < 100 or ratio > 500:
errs.append({"category": "domain", "message": "%s price_ratio_pct 越域 [100,500]: %d" % [rid, ratio]})
var unlock_price: int = int(row.get("unlock_price", -1))
if unlock_price < 0 or unlock_price > 999:
errs.append({"category": "domain", "message": "%s unlock_price 越域 [0,999]: %d" % [rid, unlock_price]})
# 解锁通道(并联双通道):初始持有行无前置无解锁价;待解锁行须有一次性解锁价+合法前置
var initial: bool = bool(row.get("initial_owned", false))
var prereq := String(row.get("prereq_route", ""))
if initial:
if prereq != "":
errs.append({"category": "cross_field", "message": "%s 初始持有行不得带前置: %s" % [rid, prereq]})
if unlock_price != 0:
errs.append({"category": "cross_field", "message": "%s 初始持有行解锁价须为 0: %d" % [rid, unlock_price]})
initial_ids[rid] = kind
else:
if unlock_price < 1:
errs.append({"category": "cross_field", "message": "%s 待解锁行须有一次性解锁价(并联双通道②): %d" % [rid, unlock_price]})
if not ROUTE_IDS.has(prereq):
errs.append({"category": "enum", "message": "%s prereq_route 出域 %s: %s" % [rid, ROUTE_IDS, prereq]})
elif prereq == rid:
errs.append({"category": "cross_field", "message": "%s prereq_route 不得自指" % rid})
elif kind_by_id.has(prereq) and String(kind_by_id[prereq]) != kind:
errs.append({"category": "cross_field", "message": "%s 前置 %s 类型不符(武器←武器/甲←甲): %s vs %s" % [rid, prereq, kind, String(kind_by_id[prereq])]})
# 初始持有恰两行且一武器一甲(长剑/轻甲——语义继承自已退役属性锻造行)
var init_weapons := 0
var init_armors := 0
for k in initial_ids.keys():
if String(initial_ids[k]) == "weapon":
init_weapons += 1
elif String(initial_ids[k]) == "armor":
init_armors += 1
if init_weapons != 1 or init_armors != 1:
errs.append({"category": "coverage", "message": "初始持有须恰一武器一甲(现 sword/light): 武器 %d / 甲 %d" % [init_weapons, init_armors]})
# 默认轴=初始持有路线类型轴(Q1 裸体态语义:未锻造=0 加值且类型=普通/轻甲)
if init_weapons == 1 and init_armors == 1:
for row in routes:
if not bool(row.get("initial_owned", false)):
continue
if String(row.get("kind", "")) == "weapon" and String(row.get("type_axis", "")) != default_attack_type:
errs.append({"category": "cross_field", "message": "default_attack_type 须=初始武器路线类型轴(裸体态普通): %s vs %s" % [default_attack_type, String(row.get("type_axis", ""))]})
if String(row.get("kind", "")) == "armor" and String(row.get("type_axis", "")) != default_armor_type:
errs.append({"category": "cross_field", "message": "default_armor_type 须=初始甲路线类型轴(裸体态轻甲): %s vs %s" % [default_armor_type, String(row.get("type_axis", ""))]})
return errs
+1
View File
@@ -0,0 +1 @@
uid://08fuv4rzuos2h
+3 -6
View File
@@ -3,14 +3,15 @@
# Date: 2026-09-10
# 数据配置 HeroTable schema(敌人系统 接口漂移新增;单行全局,Rule 4 豁免)。
# atk = 基础攻击恒存在(2026-09-11 用户裁决):域 [1,999]、0 = 加载拒绝——零基线死锁 schema 层杜绝。
# 〔2026-10-12 迭代 23 锻造工坊实装批 T2:armor_type/attack_type 两根类型轴字段删除——契约 2
# 所有权移交,默认值归锻造工坊配置(forge.tres)常驻持有、选中态写存档经参数面注入;本表只剩
# 三数值轴 max_hp/atk/armor_class(armor_class=甲路线缺失时兜底基准,甲型基础 AC 归 forge.tres 行)。〕
class_name HeroTable
extends Resource
@export var max_hp: int = 50 # [1,999] knob(provisional,溯源 = 原型锚)
@export var atk: int = 6 # [1,999] knob——基础攻击(溯源 = 概念原型 HERO_BASE_ATK,provisional 归 难度曲线与Boss);消费面 = 战斗结算·战斗段先手攻击
@export var armor_class: int = 10 # [0,999] knob——防守方护甲等级基准(2026-09-16 护甲等级掷点批,暂定 10 归 难度曲线与Boss);消费面 = 战斗结算掷点判定阈值基数(ac_up 效果加算其上)
@export var armor_type: String = "light" # enum {light, ward, heavy}——主角护甲类型轴(2026-09-18 营地扩展前置批:可配置可修改;缺省 light 向后兼容);消费面 = 战斗结算甲效矩阵(现行敌方攻击恒 normal 档 = 数值中性的结构位)
@export var attack_type: String = "normal" # enum {normal, pierce, magic}——主角基础攻击类型轴(2026-09-18 营地扩展前置批:基础攻击量(atk)与攻击类型抽象分离——营地永久强化换攻击类型 = 改此字段,量轴不动);消费面 = 战斗结算·战斗段基础分量查甲效矩阵
func validate() -> Array:
@@ -21,8 +22,4 @@ func validate() -> Array:
errs.append({"category": "domain", "message": "atk 越域 [1,999]: %d" % atk})
if armor_class < 0 or armor_class > 999:
errs.append({"category": "domain", "message": "armor_class 越域 [0,999]: %d" % armor_class})
if not ["light", "ward", "heavy"].has(armor_type):
errs.append({"category": "enum", "message": "armor_type 出域 {light, ward, heavy}: %s" % armor_type})
if not ["normal", "pierce", "magic"].has(attack_type):
errs.append({"category": "enum", "message": "attack_type 出域 {normal, pierce, magic}: %s" % attack_type})
return errs
+98
View File
@@ -0,0 +1,98 @@
[gd_resource type="Resource" script_class="ForgeTable" load_steps=2 format=3]
[ext_resource type="Script" path="res://data/schemas/forge_table.gd" id="1"]
[resource]
script = ExtResource("1")
default_attack_type = "normal"
default_armor_type = "light"
routes = [{
"route_id": "sword",
"display_name": "长剑锻造",
"kind": "weapon",
"type_axis": "normal",
"cap": 5,
"per_level_gain": 1,
"base_ac": 0,
"base_price": 20,
"price_ratio_pct": 150,
"initial_owned": true,
"prereq_route": "",
"unlock_price": 0
}, {
"route_id": "spear",
"display_name": "长枪锻造",
"kind": "weapon",
"type_axis": "pierce",
"cap": 5,
"per_level_gain": 1,
"base_ac": 0,
"base_price": 20,
"price_ratio_pct": 150,
"initial_owned": false,
"prereq_route": "sword",
"unlock_price": 200
}, {
"route_id": "staff",
"display_name": "法杖锻造",
"kind": "weapon",
"type_axis": "magic",
"cap": 5,
"per_level_gain": 1,
"base_ac": 0,
"base_price": 20,
"price_ratio_pct": 150,
"initial_owned": false,
"prereq_route": "sword",
"unlock_price": 200
}, {
"route_id": "light",
"display_name": "轻甲锻造",
"kind": "armor",
"type_axis": "light",
"cap": 5,
"per_level_gain": 1,
"base_ac": 10,
"base_price": 20,
"price_ratio_pct": 150,
"initial_owned": true,
"prereq_route": "",
"unlock_price": 0
}, {
"route_id": "heavy",
"display_name": "重甲锻造",
"kind": "armor",
"type_axis": "heavy",
"cap": 5,
"per_level_gain": 1,
"base_ac": 12,
"base_price": 20,
"price_ratio_pct": 150,
"initial_owned": false,
"prereq_route": "light",
"unlock_price": 200
}, {
"route_id": "ward",
"display_name": "魔法护甲锻造",
"kind": "armor",
"type_axis": "ward",
"cap": 5,
"per_level_gain": 1,
"base_ac": 9,
"base_price": 20,
"price_ratio_pct": 150,
"initial_owned": false,
"prereq_route": "light",
"unlock_price": 200
}]
; 锻造工坊实装批 T1(2026-10-12——迭代 23;设计权威 design/gdd/systems/跨局循环/锻造工坊.md v0.6,
; 需求卡片 design/research/需求卡片-锻造工坊实装批-2026-10-12.md T1 锚表):
; 六路线 provisional 投值——cap 5/每级增量统一 +1(v1.2 用户裁:provisional 线性起步,4a 前缓后陡为
; 终值形状方向归 M4)/甲型基础 AC 10-12-9(现役结构值 settlement.tres 矩阵同行)/基础价 20/公比 150%
; (历史考古参数 meta.tres:82-84 退役批注)/各级价格 20/30/45/67/101/一次性解锁价 200(中段三级 213 九折)。
; 初始持有=长剑+轻甲(语义继承自已退役属性锻造行——迭代 21 T5 行族删除后的新建行族,非改造)。
; 并联双通道(4b-1 裁):长枪/法杖←长剑练满、重甲/魔法护甲←轻甲练满,或一次性解锁价直购,互不影响。
; 默认两轴(契约 2 所有权移交):hero.tres 静态表两轴随本批删除,默认值归本表常驻持有——normal/light
; = 裸体态类型(Q1:未解锁/未锻造=0 加值)。全部数值 provisional 终值归 M4 数值批。
; 表运行期只读(铁律):永久强化只写存档 forge_routes 域,本表零写面。
+9 -9
View File
@@ -7,8 +7,12 @@ script = ExtResource("1")
max_hp = 26
atk = 2
armor_class = 10
armor_type = "light"
attack_type = "normal"
; 〔2026-10-12 迭代 23 锻造工坊实装批 T2——两根类型轴删除(契约 2 所有权移交):armor_type/
; attack_type 两字段随本批从静态表删除,默认值归锻造工坊配置常驻持有(forge.tres
; default_attack_type=normal/default_armor_type=light),选中态写存档域 forge_routes、冒险开始
; 经参数面注入;单位快照读取面改读注入值(缺省回退锻造配置默认值——行为连续)。原 2026-09-18
; 营地扩展前置批「两根可配置轴」登记随字段删除退役,历史记载归 git 考古。〕
; 数值压缩批(2026-09-17 用户拍板暂定——整体绝对值轴 ÷3):
; max_hp 50→17(16.67 四舍五入)、atk 6→2。armor_class 10 不动(掷点阈值轴,相对量纲不压缩——
@@ -18,13 +22,9 @@ attack_type = "normal"
; 护甲等级掷点批(2026-09-16 用户拍板):armor_class = 10(暂定基准,归 难度曲线与Boss;
; EV 参考数按判定式复算 = 期望减免 21.25%);ac_up 增益 +1 AC/层(×2 层 = AC+2)在其上加算。
;
; 营地扩展前置批(2026-09-18 用户指令):新增两根可配置轴——
; armor_type = 主角护甲类型(light/ward/heavy,缺省 light):轴现行休眠——甲效矩阵消费面 = 主角侧,
; 敌方攻击不查矩阵(2026-09-18 甲效矩阵调参批归因改述,原「恒 normal 档故三甲全 100% 中性」随旧三档
; 结构退役),类型轴生效待敌方技能攻击类型批;
; attack_type = 主角基础攻击类型(normal/pierce/magic,缺省 normal):基础攻击量(atk)与攻击类型抽象分离——
; 营地永久强化"换成魔法/穿刺攻击"= 改此字段(atk 量轴零改动),战斗段基础分量即按此查甲效矩阵。
; 两轴全部 provisional,营地建设批(元进度侧)拍板正式投值。
; 〔营地扩展前置批(2026-09-18 用户指令)两根可配置轴 armor_type/attack_type 已于 2026-10-12
; 迭代 23 锻造工坊实装批 T2 删除——所有权移交锻造工坊配置(见顶部注记);甲型基础 AC 随甲路线
; 行承载(轻 10/重 12/魔法护甲 9),本表 armor_class=10 保留为甲路线缺失时的兜底基准。〕
;
; Package B 迭代·第 2 轮(2026-09-22——计划书 §6.2,用户拍板一B):max_hp 17→20。第 1 轮读数
; 触发 §6.2(L1 精英承伤 43-55% vs 靶 ≤20%、L1 净消耗 17.7/17=104% 破产、normal L1 通过率
+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:
+7
View File
@@ -316,6 +316,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 卡④——生涯页一级入口(营地同组)
@@ -495,11 +498,15 @@ func _wire_adventure() -> void:
# 取值口径与营地投影完全同源:装配集=save.attunement_equipped(save_manager.gd 访问器)、Y 刷新池=
# meta.attune_refresh_charges(当前等级行表值)、树 rule 键集=meta.tree_rule_keys(Fork 激活侧 effects);
# hero_override 沿用既有 meta 现算口径。static 形态=冒烟直呼单测本函数,免实例化整菜单场景)。
# 迭代 23 T4:两根类型轴随开局下传(attack_type/armor_type——契约 2 所有权移交+契约 8 冒险开始快照;
# 取值=meta.hero_attack_type/hero_armor_type=存档选中路线类型轴,未解锁/异常选中态回退锻造配置默认值)。
# 注:runner 侧 resume_adventure 受理面现收 attunement/attune_refresh_y/tree_rules 三键
# (2026-10-08 尾裁微批b 卡⓪受理闭环——tree_rules 受理补齐,传参/受理两侧对齐,披露句退役)。
static func build_adventure_params(save_manager_ref, meta_ref, extra: Dictionary = {}) -> Dictionary:
var params: Dictionary = {
"hero_override": meta_ref.hero_override() if meta_ref != null else {},
"attack_type": meta_ref.hero_attack_type() if meta_ref != null else "",
"armor_type": meta_ref.hero_armor_type() if meta_ref != null else "",
"attunement": save_manager_ref.attunement_equipped() if save_manager_ref != null else [],
"attune_refresh_y": meta_ref.attune_refresh_charges() if meta_ref != null else 0,
"tree_rules": meta_ref.tree_rule_keys() if meta_ref != null else [],
+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
+213 -15
View File
@@ -1462,8 +1462,8 @@ func _ac11_tier_samples() -> Array:
# 同种子停手轮恒同掷点(效果只改乘区、不动掷点流)→ 落地伤害精确关系:
# 增伤 = 基线 ×1.5、乏力 = ×0.75(hero_offense 池乘攻击合计——2026-09-19 充能退役后 = 单分量,直乘旁路退役);
# 硬化 = 基线 ×0.5 且 < 基线(enemy_defense 池负值折半——+100 翻倍符号 bug 随批纠正)。
# 基线构成(加法核 2026-09-18 + 充能退役 2026-09-19):基线 100——hero attack_type=normal 对轻甲中性 0 修正
# (目标 = L1 地精轻甲;AC 0 恒全额非暴击种子)。
# 基线构成(加法核 2026-09-18 + 充能退役 2026-09-19):基线 100——hero 攻击类型=normal(锻造工坊配置默认值,
# 迭代 23 T2 起 hero.tres 两轴删除)对轻甲中性 0 修正(目标 = L1 地精轻甲;AC 0 恒全额非暴击种子)。
func _smoke_pool_wiring() -> void:
print("--- 主角乘区接线 + 硬化折半方向 ---")
var base_row := _hero_attack_row_with({}, 100, 20260918)
@@ -1898,7 +1898,8 @@ func _smoke_full_battle(L: int, D: int, label: String) -> void:
if row.get("source_class", "") == "hero_attack":
hero_attacks += 1
# 单分量伤害构成行(2026-09-19 充能退役批):hero_attack 行必须携带实际攻击类型键 + 掷点读数(P2 归因)
var atk_t: String = String(_db.hero.attack_type)
# 迭代 23 T2:类型键主语换代——hero.tres 两轴删除,实际攻击类型=锻造工坊配置默认值(本夹具零注入)
var atk_t: String = String(_db.forge_default_attack_type())
if not (row.has(atk_t) and row.has("roll") and row.has("ac") and row.has("tier")):
hero_breakdown_ok = false
check(has_verdict, "%s 步骤流含判定行" % label)
@@ -1908,7 +1909,7 @@ func _smoke_full_battle(L: int, D: int, label: String) -> void:
check(hero_breakdown_ok, "%s 主角攻击行携带类型键与掷点读数(单分量——充能退役批)" % label)
func _run_battle(L: int, D: int) -> Dictionary:
func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
# 每遍全新 RngService(同 master 种子)——回放断言的隔离前提(地图战斗资源 AC"两套全新 RngService+地图战斗资源"同款)
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(42)
@@ -1922,7 +1923,9 @@ func _run_battle(L: int, D: int) -> Dictionary:
settlement.action_economy.round_end_stopped.connect(func(p): out.events.append({"ev": "round_end_stopped", "path_tag": p.path_tag}))
var minesweeper = settlement.minesweeper
var action_economy = settlement.action_economy
if not settlement.begin_battle({"level_index": L, "node_depth": D, "hero_hp": _db.hero.max_hp}):
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.hero.max_hp}
enc.merge(axes, true) # 迭代 23 T4——两根类型轴注入尾参(锻造工坊批对拍用;缺省零注入=回退面)
if not settlement.begin_battle(enc):
out.stream.append({"type": "verdict", "value": "begin_failed"})
return out
var max_rounds := 40
@@ -4408,10 +4411,167 @@ func _smoke_meta() -> void:
_meta_pricing_and_buy()
_meta_settle_and_workshop()
_meta_injection()
_smoke_forge_axes()
_pause_overlay_projection()
_career_overlay_projection()
# ---- 锻造工坊·两根类型轴所有权移交(迭代 23 T2——V5 前半:hero.tres 两轴删除+锻造配置默认值)----
# 后半(build_adventure_params 注入默认值/选中覆盖)随 T4 注入接线同批断言;写口/解锁面归 T3。
func _smoke_forge_axes() -> void:
print("--- 锻造工坊·类型轴所有权移交(T2)---")
# V5:hero.tres 静态表两轴已删(全表加载零违例——config_db_test 同款闸;此处钉字段不在位)
check(not ("attack_type" in _db.hero) and not ("armor_type" in _db.hero),
"hero.tres 无 attack_type/armor_type 字段(契约 2——静态表两轴删除)〔锻造工坊批 V5〕")
# 锻造配置默认值 = normal/light(常驻持有——与柜台解锁状态解耦,未点亮也照常可读)
check(_db.forge_default_attack_type() == "normal" and _db.forge_default_armor_type() == "light",
"锻造配置默认值 attack_type=normal / armor_type=light(常驻持有)〔锻造工坊批 V5〕")
# 单位快照回退面:spawn 三参形态(零注入)= 锻造配置默认值(行为连续——原 hero.tres normal/light)
var uc = load("res://core/unit_container.gd").new(_db)
check(uc.spawn(1, 0, int(_db.hero.max_hp)), "spawn 受理(三参形态——两轴缺省回退面)")
check(String(uc.hero_unit().get("attack_type", "")) == "normal"
and String(uc.hero_unit().get("armor_type", "")) == "light",
"主角单位快照两轴 = 锻造配置默认值 normal/light(零注入回退——行为连续)〔锻造工坊批 V5〕")
# ---- T3 写口族(V2 购买/几何价 · V3 并联双通道 · V4 两币分工 · V6 选中切换)----
# 注入口径(两分法):add_keystone 置余额/直写存档域构造状态 → 直调写口 → 断余额/等级/读回
var sv2 = _meta_new_save(_META_SMOKE_PATHS[13])
var mp2 = load("res://core/meta_progression.gd").new(_db, sv2)
# V2①:长剑首购=20(0 级位几何价)、等级 0→1、扣币即盘、reload 持久
sv2.add_keystone(100)
check(mp2.forge_buy_level("sword") and sv2.forge_route_level("sword") == 1
and sv2.keystone_balance() == 80,
"长剑锻造首购:扣 20 钥石、等级 0→1(Q1——第一级=获得装备=首个 +1)〔锻造工坊批 V2〕")
var sv2r = _meta_new_save(_META_SMOKE_PATHS[13])
check(sv2r.forge_route_level("sword") == 1 and sv2r.keystone_balance() == 80,
"购买持久(reload 后等级与余额不变——即时写盘)〔锻造工坊批 V2〕")
# V2②:二级=30(1 级位);六路线独立(买长剑不动轻甲)
check(mp2.forge_buy_level("sword") and sv2.forge_route_level("sword") == 2
and sv2.keystone_balance() == 50 and sv2.forge_route_level("light") == 0,
"长剑二级购:扣 30、等级→2;轻甲独立零扰动(六路线各练各的)〔锻造工坊批 V2〕")
# V2③:余额不足拒且零副作用(等级/选中/解锁三面零变化)
var sv2d = _meta_new_save(_META_SMOKE_PATHS[14])
var mp2d = load("res://core/meta_progression.gd").new(_db, sv2d)
var fr_dry: Dictionary = sv2d.forge_routes_snapshot()
check(not mp2d.forge_buy_level("sword") and sv2d.keystone_balance() == 0
and sv2d.forge_routes_snapshot() == fr_dry,
"余额不足购买拒绝且零副作用(0 余额——三域零变化)〔锻造工坊批 V2〕")
# V2④:已满级拒购(cap 闸);未解锁拒购
sv2.set_forge_route_level("sword", 5)
check(not mp2.forge_buy_level("sword") and not mp2d.forge_buy_level("spear"),
"已满级拒购/未解锁拒购(cap 闸+解锁闸——零副作用)〔锻造工坊批 V2〕")
# V3① 通道一:练满前一档(长剑 cap)→ 长枪/法杖派生解锁(零收费);轻甲未满→甲线仍锁
check(mp2.forge_route_unlocked("spear") and mp2.forge_route_unlocked("staff")
and not mp2.forge_route_unlocked("heavy") and not mp2.forge_route_unlocked("ward"),
"通道一:长剑练满→长枪/法杖派生解锁;轻甲未练满→重甲/魔法护甲仍锁〔锻造工坊批 V3〕")
check(mp2.forge_buy_level("spear") and sv2.forge_route_level("spear") == 1
and sv2.keystone_balance() == 30,
"派生解锁后长枪可购(首购 20——余额 50−20)〔锻造工坊批 V3〕")
# V3② 通道二:一次性解锁价直购(未练满也可买——两通道互不影响)
var sv3 = _meta_new_save(_META_SMOKE_PATHS[15])
var mp3 = load("res://core/meta_progression.gd").new(_db, sv3)
sv3.add_keystone(500)
check(not mp3.forge_route_unlocked("ward") and mp3.forge_buy_unlock("ward")
and sv3.keystone_balance() == 300 and mp3.forge_route_unlocked("ward"),
"通道二:魔法护甲一次性解锁价 200 直购开(轻甲未练满也可买)〔锻造工坊批 V3〕")
# V3③ 直购后练满不重复收(练满通道=派生零收费;已解锁再直购=拒绝)
sv3.set_forge_route_level("light", 5)
check(mp3.forge_route_unlocked("ward") and not mp3.forge_buy_unlock("ward")
and sv3.keystone_balance() == 300,
"直购后练满不重复收(已解锁=拒绝零收费——两通道互不影响)〔锻造工坊批 V3〕")
# V3④ 不设串联门槛(法杖直购开——长枪零状态不阻;dry 档充值走直购)
sv2d.add_keystone(500)
check(mp2d.forge_buy_unlock("staff") and mp2d.forge_route_unlocked("staff")
and sv2d.forge_route_level("spear") == 0 and not mp2d.forge_route_unlocked("spear"),
"不设串联门槛(法杖直购开——长枪零状态不阻)〔锻造工坊批 V3〕")
# V4:任一购买/解锁只扣钥石——GP 侧零触碰(meta 域无 GP 键=域界扫描同款;此处钉余额差=当级价)
var ks4: int = sv3.keystone_balance()
check(mp3.forge_buy_level("ward") and sv3.keystone_balance() == ks4 - 20
and sv3.forge_route_level("ward") == 1,
"两币分工:购买只扣钥石(余额差=当级价 20——GP 零触碰、无材料计数)〔锻造工坊批 V4〕")
# V6①:营地切换=写存档域+免费(余额零动);旧路线练度保留(只不生效)
var ks6: int = sv3.keystone_balance()
check(mp3.forge_select_armor("ward") and sv3.forge_armor() == "ward"
and sv3.keystone_balance() == ks6,
"选中切换免费(余额零动——写存档域即盘)〔锻造工坊批 V6〕")
check(sv3.forge_route_level("light") == 5,
"切换后旧路线练度保留(轻甲 5 级不丢——回家待命只不生效)〔锻造工坊批 V6〕")
# V6②:未解锁路线拒选零副作用(dry 档:长枪未解锁)
check(not mp2d.forge_select_weapon("spear") and sv2d.forge_weapon() == "sword",
"未解锁路线拒选零副作用〔锻造工坊批 V6〕")
# V6③:类型轴读数随选中走(切换后读 ward;未解锁选中态防御回退默认值)
check(mp3.hero_armor_type() == "ward" and mp2d.hero_attack_type() == "normal"
and mp2d.hero_armor_type() == "light",
"类型轴当前生效值随选中走(ward;未解锁档回退默认 normal/light)〔锻造工坊批 V5/V6〕")
# ---- T4 注入接线(V5 后半 · V6 锁死 · V7 公式对拍 · V10 树零攻防复核)----
# V5:build_adventure_params 注入默认值(新档=树钥匙未点亮)+点亮后读存档选中覆盖
var sv4 = _meta_new_save(_META_SMOKE_PATHS[16])
var mp4 = load("res://core/meta_progression.gd").new(_db, sv4)
var bp_params: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv4, mp4)
check(String(bp_params.get("attack_type", "")) == "normal"
and String(bp_params.get("armor_type", "")) == "light",
"build_adventure_params 注入默认值 normal/light(树钥匙未点亮——新档初始持有态)〔锻造工坊批 V5〕")
sv4.add_keystone(1000)
check(mp4.forge_buy_unlock("spear") and mp4.forge_buy_level("spear") and mp4.forge_select_weapon("spear"),
"长枪直购解锁+首购+选中(注入面夹具——200+20 钥石)")
var bp_sel: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv4, mp4)
check(String(bp_sel.get("attack_type", "")) == "pierce"
and String(bp_sel.get("armor_type", "")) == "light",
"build_adventure_params 读存档选中覆盖(选中长枪→pierce)〔锻造工坊批 V5〕")
# V7:hero_override 公式对拍=树 flat+锻造路线加值(旧路线练度待命不生效;切回即生效)
check(int(mp4.hero_override().get("atk", -1)) == 1
and int(mp4.hero_override().get("max_hp", -1)) == 0
and int(mp4.hero_override().get("armor_class", -1)) == 0
and not mp4.hero_override().has("hit"),
"hero_override=锻造路线加值(选中长枪 Lv1→atk +1;甲 0 级→AC +0;树零分量;无 hit 键)〔锻造工坊批 V7〕")
sv4.set_forge_route_level("sword", 3) # 长剑练度待命(未选中)
check(int(mp4.hero_override().get("atk", -1)) == 1,
"换当前武器→攻击加值随切换(长剑 3 级待命不生效——只认选中)〔锻造工坊批 V7〕")
check(mp4.forge_select_weapon("sword") and int(mp4.hero_override().get("atk", -1)) == 3,
"切回长剑→加值=3 级量(旧路线练度保留且生效)〔锻造工坊批 V7〕")
# V7 三层 AC:重甲(基础 12)+路线 Lv1 加值+局内 0——经参数面起冒险断 runner 生效值
var sv5 = _meta_new_save(_META_SMOKE_PATHS[17])
var mp5 = load("res://core/meta_progression.gd").new(_db, sv5)
sv5.add_keystone(1000)
check(mp5.forge_buy_unlock("heavy") and mp5.forge_buy_level("heavy") and mp5.forge_select_armor("heavy"),
"重甲直购解锁+首购+选中(三层 AC 夹具)")
var adr5 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(77))
var p5: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv5, mp5)
check(adr5.start_adventure(77, p5), "注入参数冒险启动(两轴+override 同面)")
var r5 = adr5.runner()
check(int(r5.effective_ac()) == 12 + 1,
"入战 AC 三层:甲型基础 12(重甲)+路线加值 1+局内 0〔锻造工坊批 V7〕")
check(adr5.hero_attack_type == "normal" and adr5.hero_armor_type == "heavy",
"参数面快照入 runner(attack_type=normal/armor_type=heavy)〔锻造工坊批 V6〕")
# V6 锁死:开冒险后改存档选中→参数面/注入值不变(快照冻结;light=初始持有已解锁可切)
check(mp5.forge_select_armor("light") and adr5.hero_armor_type == "heavy"
and int(r5.effective_ac()) == 12 + 1,
"冒险开始后锁死:改存档选中不影响参数面快照与生效值(契约 8)〔锻造工坊批 V6〕")
# V5/V7 末端:enc 两轴直达 spawn 英雄快照(hero_attack 行携带选中类型键)
var run_ax = _run_battle(1, 0, {"attack_type": "pierce", "armor_type": "ward"})
var pierce_rows := 0
for row in run_ax.stream:
if row.get("source_class", "") == "hero_attack" and row.has("pierce"):
pierce_rows += 1
check(pierce_rows >= 1,
"enc 两轴直达 spawn 英雄快照(hero_attack 行携带 pierce 键——注入链末端)〔锻造工坊批 V5〕")
# spawn 显式注入面(三参旧形态零注入已在上方覆盖——此处钉显式值)
var uc2 = load("res://core/unit_container.gd").new(_db)
check(uc2.spawn(1, 0, int(_db.hero.max_hp), [], -1, -1, [], "magic", "heavy")
and String(uc2.hero_unit().get("attack_type", "")) == "magic"
and String(uc2.hero_unit().get("armor_type", "")) == "heavy",
"spawn 显式两轴注入(magic/heavy 直入英雄快照)〔锻造工坊批 V5〕")
# V10 复核:树表 12 行 effects 键集无 atk/armor_class(契约 1——树零攻防来源)
var tree_atk_ac := false
for trow in _db.tree_rows():
var eff: Dictionary = trow.get("effects", {})
if eff.has("atk") or eff.has("armor_class"):
tree_atk_ac = true
check(not tree_atk_ac,
"树零攻防来源复核(12 行 effects 键集无 atk/armor_class——契约 1)〔锻造工坊批 V10〕")
# ---- 暂停层只读投影一致性(迭代 17 卡③——R4 数据只读同源:暂停层展示值逐项 == MetaProgression
# 访问器读数;投影拼装与 main._pause_data 同构(同访问器族同序)。只读性连接巡检与零动效
# 断言归 ui_struct_probe 呈现侧,本段只管数据面)----
@@ -4544,7 +4704,9 @@ const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry
"user://smoke_meta_cap.json", "user://smoke_meta_white.json",
"user://smoke_meta_attune.json", "user://smoke_meta_attune2.json",
"user://smoke_meta_tree.json", "user://smoke_meta_career.json",
"user://smoke_meta_career_v5.json", "user://smoke_meta_treasure_unlock.json", "user://smoke_meta_treasure_blank.json"]
"user://smoke_meta_career_v5.json", "user://smoke_meta_treasure_unlock.json", "user://smoke_meta_treasure_blank.json",
"user://smoke_meta_forge.json", "user://smoke_meta_forge2.json", "user://smoke_meta_forge3.json",
"user://smoke_meta_forge4.json", "user://smoke_meta_forge5.json"]
func _meta_smoke_cleanup() -> void:
@@ -5057,7 +5219,8 @@ func _smoke_save() -> void:
"user://smoke_save_domain.json", "user://smoke_save_cap.json",
"user://smoke_save_integration.json", "user://smoke_save_breakpoint.json",
"user://smoke_save_v2compat.json", "user://smoke_save_v5upgrade.json",
"user://smoke_save_v5legacy.json", "user://smoke_save_v16discard.json"]
"user://smoke_save_v5legacy.json", "user://smoke_save_v16discard.json",
"user://smoke_save_forge_v6legacy.json"]
var stale: Array = []
for p in paths:
stale.append(p)
@@ -5073,6 +5236,10 @@ func _smoke_save() -> void:
w.set_tree_level("sp_hp", 2)
w.set_tree_level("sp_hit", 1)
w.set_fork_side("fg_posture", "b") # 树域写入(属性锻造域已退役——forge_levels 域清退换代件)
w.set_forge_route_level("spear", 3) # 锻造工坊域写入(迭代 23 T5——等级+直购解锁+选中标记)
w.add_forge_unlock("spear")
w.set_forge_weapon("spear")
w.set_forge_armor("ward")
w.unlock_item("probe_compass")
w.apply_adventure_summary({"result": "failed", "cleared": false, "levels_cleared": 2,
"stopped_at": 3, "keystone_total": 3, "master_seed": 99})
@@ -5088,6 +5255,10 @@ func _smoke_save() -> void:
check(r.keystone_balance() == 10, "钥石余额往返相等(10)")
check(r.tree_level("sp_hp") == 2 and r.tree_level("sp_hit") == 1 and r.fork_side("fg_posture") == "b",
"树域往返相等(tree_nodes/fork_sides——迭代 21 换代承载)")
check(r.forge_route_level("spear") == 3 and r.forge_route_level("sword") == 0
and r.forge_unlocked_purchases() == ["spear"]
and r.forge_weapon() == "spear" and r.forge_armor() == "ward",
"锻造工坊域往返相等(levels/unlocked/weapon/armor——迭代 23 T5)〔锻造工坊批 V8〕")
check(r.item_unlocked("probe_compass") and not r.item_unlocked("rope"), "道具解锁往返相等")
check(r.clear_count(1) == 1 and r.clear_count(2) == 1 and r.clear_count(3) == 0, "通关次数表往返相等")
check(r.boss_first_kill_marked(1) and r.boss_first_kill_marked(2) and not r.boss_first_kill_marked(3),
@@ -5142,8 +5313,8 @@ func _smoke_save() -> void:
fm.store_string(JSON.stringify({"version": 0, "keystone_old": 12}))
fm.close()
check(f7.load_from_disk() and f7.keystone_balance() == 12, "v0 旧档经迁移链入账(keystone_old 12 → 余额 12)")
check(f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 6,
"迁移链 0→1(测试钩)→6(生产) 写盘 version == 6(v6=生涯终身计数域,迭代 18 卡④——v5=天赋树域迭代 12 T9)")
check(f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 7,
"迁移链 0→1(测试钩)→7(生产) 写盘 version == 7(v7=锻造工坊域,迭代 23 T5——v6=生涯终身计数域迭代 18 卡④)")
# ⑧ 域界干净:meta 永久进度域不含局内资源键(行动点/RAP/GP/道具库存/生命账本不入档——契约 2;
# 迭代 8 卡①修订:扫描收窄到 meta 域——断点域=冒险进行中瞬态快照,生命/GP 按拍板面入域豁免)
@@ -5239,9 +5410,9 @@ func _smoke_save() -> void:
fold.store_string("{\"version\": 1, \"meta\": {\"keystone_balance\": 4}}")
fold.close()
check(fo.load_from_disk() and fo.keystone_balance() == 4 and not fo.has_breakpoint(),
"v1 旧档迁移 v4:断点域补空(合法空态,无断点域不炸)")
check(fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 6,
"v1 旧档迁移后写盘 version == 6(v1→v6 链式——生涯终身计数域 2026-10-07)")
"v1 旧档迁移 v7:断点域补空(合法空态,无断点域不炸)")
check(fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 7,
"v1 旧档迁移后写盘 version == 7(v1→v7 链式——锻造工坊域 2026-10-12)")
# ⑫ v2 旧档→v3 兼容(迭代 10 g 裁——semver 向后兼容:旧断点零丢失、新代码可续玩)
var fv2 = sm_script.new(paths[10])
@@ -5268,11 +5439,11 @@ func _smoke_save() -> void:
fv5u.close()
check(f13.load_from_disk() and f13.adventure_total() == 0 and f13.adventure_cleared() == 0
and f13.keystone_balance() == 9,
"v5 旧档升 v6:计数域缺省补零且既有余额原样(迁移只增字段不改值)")
"v5 旧档升 v7:计数域缺省补零且既有余额原样(迁移只增字段不改值——v5→v6→v7 链式)")
check(f13.write_to_disk() and f13.load_from_disk()
and int(JSON.parse_string(FileAccess.open(paths[11], FileAccess.READ).get_as_text()).get("version", -1)) == 6
and int(JSON.parse_string(FileAccess.open(paths[11], FileAccess.READ).get_as_text()).get("version", -1)) == 7
and f13.adventure_total() == 0 and f13.adventure_cleared() == 0,
"升档幂等:v6 档写盘再载零漂移(重复迁移零副作用)")
"升档幂等:v7 档写盘再载零漂移(重复迁移零副作用——锻造工坊域同批补齐)")
var f13b = sm_script.new(paths[12])
f13b.load_from_disk()
f13b.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 3,
@@ -5299,6 +5470,33 @@ func _smoke_save() -> void:
f15.add_keystone(50)
check(mp16.tree_buy("sp_hp") and f15.tree_level("sp_hp") == 1, "废弃重置后后续购买正常受理(无残留脏节点)")
# ⑯ 锻造工坊域 forge_routes(迭代 23 T5——V8):v6 旧档缺键补齐=初始持有态+无特效域+域界干净
var f16 = sm_script.new(paths[14])
var fv6 := FileAccess.open(paths[14], FileAccess.WRITE)
fv6.store_string(JSON.stringify({"version": 6, "meta": {"keystone_balance": 21,
"tree_nodes": {"sp_hp": 1}, "tree_struct": 2, "adventure_total": 4, "adventure_cleared": 1}}))
fv6.close()
check(f16.load_from_disk() and f16.keystone_balance() == 21
and f16.tree_level("sp_hp") == 1 and f16.adventure_total() == 4 and f16.adventure_cleared() == 1,
"v6 旧档升 v7:既有域逐位不动(钥石/树/生涯计数零折算——迁移只增不改)〔锻造工坊批 V8〕")
check(f16.forge_route_level("sword") == 0 and f16.forge_route_level("spear") == 0
and f16.forge_route_level("light") == 0 and f16.forge_route_level("ward") == 0
and f16.forge_unlocked_purchases().is_empty()
and f16.forge_weapon() == "sword" and f16.forge_armor() == "light",
"v6 旧档缺键补齐=初始持有态(六路线 0 级/直购清单空/选中=sword+light)〔锻造工坊批 V8〕")
var fr_keys: Array = (f16.forge_routes_snapshot() as Dictionary).keys()
fr_keys.sort()
check(fr_keys == ["armor", "levels", "unlocked", "weapon"],
"forge_routes 域键集恰四项(无特效域——满级特效本期不做,契约 7)〔锻造工坊批 V8/V11〕")
var lv_keys: Array = ((f16.forge_routes_snapshot() as Dictionary).get("levels", {}) as Dictionary).keys()
lv_keys.sort()
check(lv_keys == ["heavy", "light", "spear", "staff", "sword", "ward"],
"levels 六路线键集恰全(sword/spear/staff/light/heavy/ward)〔锻造工坊批 V8〕")
# 值拷贝出户(改快照不渗档——本类契约)
var snap: Dictionary = f16.forge_routes_snapshot()
(snap.get("levels", {}) as Dictionary)["sword"] = 99
check(f16.forge_route_level("sword") == 0, "forge_routes_snapshot 值拷贝出户(改快照不渗档)〔锻造工坊批 V8〕")
# ⑭ 收束链入账(迭代 18 卡④ T2——真实收束挂点全链恰一:主流程 adventure_finished →
# MetaProgression.settle_adventure → apply_adventure_summary 单口;两夹具经同链入账)
var mp13 = load("res://core/meta_progression.gd").new(_db, f13b)
+2
View File
@@ -28,6 +28,7 @@ func _suites() -> Array:
return [
{"path": "res://tests/unit/example_test.gd", "expect": 0},
{"path": "res://tests/unit/data_config/config_db_test.gd", "expect": 0},
{"path": "res://tests/unit/data_config/forge_table_test.gd", "expect": 0}, # 迭代 23 锻造工坊批 T1——六路线 schema 正反(V1)+特效留空(V11)+几何价格梯(V2 半边)
{"path": "res://tests/unit/data_config/config_db_fatal_oob_test.gd", "expect": 1},
{"path": "res://tests/unit/data_config/treasure_target_violation_test.gd", "expect": 1}, # 迭代 7 T1——真视窄域护栏
{"path": "res://tests/unit/data_config/reward_dangling_treasure_test.gd", "expect": 1}, # 迭代 7 T1——秘宝双表校验
@@ -37,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,200 @@
# tests/unit/data_config/forge_table_test.gd — 锻造工坊配置表单测(迭代 23 T1)
# 覆盖(需求卡片 §7.2):V1 六路线 schema 零违例(id 唯一/类型轴枚举/值域——出域行逐类构造各一行全部被拒)
# + V11 满级特效留空(行字段闭集=无特效参数位 + 效果类型闭集 24 种零新增)+ 几何价格梯 20/30/45/67/101。
# 运行:godot --headless --path game --script res://tests/unit/data_config/forge_table_test.gd(expect 0)
# 协议(ADR-0004 + tests/README):extends SceneTree + quit(code);负向用例资源直构不碰生产表(enemy_table 先例)。
extends SceneTree
const ConfigDBScript := preload("res://core/config_db.gd")
const ForgeTableScript := preload("res://data/schemas/forge_table.gd")
const EffectEngineScript := preload("res://core/effect_engine.gd")
const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
var db = ConfigDBScript.new()
var load_ok: bool = db.load_tables("res://data/tables", true)
Kit.check("生产 forge.tres 随全表加载 + validate 零违例〔锻造工坊批 V1〕", load_ok and db.diagnostics_count("error") == 0)
if load_ok:
_test_production_rows(db)
_test_price_ladder(db)
_test_negative_cases()
_test_effect_kinds_closed()
quit(Kit.finish("forge_table_test"))
# ---- 生产表六行正向(V1 生产面 + V11 行字段闭集)----
func _test_production_rows(db) -> void:
var rows: Array = db.forge_routes()
Kit.check("六路线行族规模恰 6(三武器+三甲)〔锻造工坊批 V1〕", rows.size() == 6)
var ids := []
for row in rows:
ids.append(String(row.get("route_id", "")))
ids.sort()
Kit.check("路线 id 全集 = {sword, spear, staff, light, heavy, ward}〔锻造工坊批 V1〕",
ids == ["heavy", "light", "spear", "staff", "sword", "ward"])
# V11:行字段闭集——键集恰 12 项,无特效参数位(满级特效本期不做,契约 7)
var expect_keys := ForgeTableScript.ROW_KEYS.duplicate()
expect_keys.sort()
var keys_closed := true
for row in rows:
var ks := (row as Dictionary).keys()
ks.sort()
if ks != expect_keys:
keys_closed = false
Kit.check("六行字段键集恰 = ROW_KEYS 闭集(无特效参数位)〔锻造工坊批 V11〕", keys_closed)
# 初始持有恰长剑+轻甲(语义继承自已退役属性锻造行)
var initial := []
for row in rows:
if bool(row.get("initial_owned", false)):
initial.append(String(row.get("route_id", "")))
initial.sort()
Kit.check("初始持有恰 sword+light〔锻造工坊批 V1〕", initial == ["light", "sword"])
# 默认两轴(契约 2 所有权移交——裸体态 normal/light)
Kit.check("默认攻击类型轴 = normal(常驻持有)〔锻造工坊批 V1〕", db.forge_default_attack_type() == "normal")
Kit.check("默认甲型轴 = light(常驻持有)〔锻造工坊批 V1〕", db.forge_default_armor_type() == "light")
# 类型轴分族与并联双通道前置(4b-1 裁)
Kit.check("长枪/法杖 ← 长剑练满(并联双通道①)〔锻造工坊批 V1〕",
String(db.forge_route("spear").get("prereq_route", "")) == "sword"
and String(db.forge_route("staff").get("prereq_route", "")) == "sword")
Kit.check("重甲/魔法护甲 ← 轻甲练满(并联双通道①)〔锻造工坊批 V1〕",
String(db.forge_route("heavy").get("prereq_route", "")) == "light"
and String(db.forge_route("ward").get("prereq_route", "")) == "light")
Kit.check("四待解锁路线一次性解锁价 = 200〔锻造工坊批 V1〕",
int(db.forge_route("spear").get("unlock_price", 0)) == 200
and int(db.forge_route("staff").get("unlock_price", 0)) == 200
and int(db.forge_route("heavy").get("unlock_price", 0)) == 200
and int(db.forge_route("ward").get("unlock_price", 0)) == 200)
# 甲型基础 AC(provisional 现役结构值——settlement.tres 矩阵同行)
Kit.check("甲型基础 AC = 轻 10 / 重 12 / 魔法护甲 9〔锻造工坊批 V1〕",
int(db.forge_route("light").get("base_ac", 0)) == 10
and int(db.forge_route("heavy").get("base_ac", 0)) == 12
and int(db.forge_route("ward").get("base_ac", 0)) == 9)
# 按类型轴反查(甲路线 id=甲型同值的显式化入口)
Kit.check("forge_armor_route_by_type(ward) = 魔法护甲锻造〔锻造工坊批 V1〕",
String(db.forge_armor_route_by_type("ward").get("route_id", "")) == "ward")
Kit.check("forge_weapon_route_by_type(pierce) = 长枪锻造〔锻造工坊批 V1〕",
String(db.forge_weapon_route_by_type("pierce").get("route_id", "")) == "spear")
# 缺行/未知轴 = 空字典合法态
Kit.check("未知 route_id / 未知类型轴 = 空字典〔锻造工坊批 V1〕",
db.forge_route("blade").is_empty() and db.forge_armor_route_by_type("plate").is_empty())
# ---- 几何价格梯(V2 价格面单测半边——购买写在 T3 冒烟)----
func _test_price_ladder(db) -> void:
var row: Dictionary = db.forge_route("sword")
var prices := []
for owned in range(5):
prices.append(ForgeTableScript.level_price(row, owned))
Kit.check("长剑各级价格 = 20/30/45/67/101(基础 20×150%^已购级数)〔锻造工坊批 V2〕",
prices == [20, 30, 45, 67, 101])
Kit.check("已满级(owned=5)再购价照算 151(cap 闸归写口,价格函数纯几何)〔锻造工坊批 V2〕",
ForgeTableScript.level_price(row, 5) == 151)
# ---- 负向:出域行逐类构造各一行全部被拒(V1)----
func _base_rows() -> Array[Dictionary]:
var out: Array[Dictionary] = []
var mk := func(rid: String, kind: String, axis: String, initial: bool, prereq: String, unlock: int) -> Dictionary:
return {"route_id": rid, "display_name": rid + "锻造", "kind": kind, "type_axis": axis,
"cap": 5, "per_level_gain": 1, "base_ac": 10 if kind == "armor" else 0,
"base_price": 20, "price_ratio_pct": 150, "initial_owned": initial,
"prereq_route": prereq, "unlock_price": unlock}
out.append(mk.call("sword", "weapon", "normal", true, "", 0))
out.append(mk.call("spear", "weapon", "pierce", false, "sword", 200))
out.append(mk.call("staff", "weapon", "magic", false, "sword", 200))
out.append(mk.call("light", "armor", "light", true, "", 0))
out.append(mk.call("heavy", "armor", "heavy", false, "light", 200))
out.append(mk.call("ward", "armor", "ward", false, "light", 200))
return out
func _has_cat(errs: Array, cat: String) -> bool:
for e in errs:
if String(e.get("category", "")) == cat:
return true
return false
# 多字段变异器(lambda 单表达式装不下的用例——命名函数承载)
func _mut_spear_initial(t) -> void:
t.routes[1]["initial_owned"] = true
t.routes[1]["prereq_route"] = ""
t.routes[1]["unlock_price"] = 0
func _test_negative_cases() -> void:
# 夹具本身须零违例(否则负向判读失效)
var t_ok = ForgeTableScript.new()
t_ok.routes = _base_rows()
Kit.check("负向夹具(合法六行)validate 零违例〔锻造工坊批 V1〕", t_ok.validate().is_empty())
var cases := []
# route_id 出域
cases.append(["route_id 出域即拒", func(t): t.routes[0]["route_id"] = "blade", "enum"])
# route_id 重复
cases.append(["route_id 重复即拒", func(t): t.routes[1]["route_id"] = "sword", "dup_key"])
# kind 出域
cases.append(["kind 出域即拒", func(t): t.routes[1]["kind"] = "shield", "enum"])
# 武器类型轴出域
cases.append(["武器 type_axis 出域即拒", func(t): t.routes[1]["type_axis"] = "ward", "enum"])
# 同攻击类型两路线
cases.append(["同攻击类型第二路线即拒", func(t): t.routes[2]["type_axis"] = "normal", "dup_key"])
# cap 越域
cases.append(["cap 越域即拒", func(t): t.routes[0]["cap"] = 0, "domain"])
# 每级增量越域
cases.append(["per_level_gain 越域即拒", func(t): t.routes[0]["per_level_gain"] = -1, "domain"])
# 基础价越域
cases.append(["base_price 越域即拒", func(t): t.routes[0]["base_price"] = 0, "domain"])
# 公比越域
cases.append(["price_ratio_pct 越域即拒", func(t): t.routes[0]["price_ratio_pct"] = 99, "domain"])
# 武器行 base_ac 非零
cases.append(["武器行 base_ac 非零即拒", func(t): t.routes[0]["base_ac"] = 5, "cross_field"])
# 甲行 base_ac 越域
cases.append(["甲行 base_ac 越域即拒", func(t): t.routes[3]["base_ac"] = -1, "domain"])
# 待解锁行无解锁价
cases.append(["待解锁行无一次性解锁价即拒", func(t): t.routes[1]["unlock_price"] = 0, "cross_field"])
# 初始持有行带前置
cases.append(["初始持有行带前置即拒", func(t): t.routes[0]["prereq_route"] = "spear", "cross_field"])
# 初始持有行带解锁价
cases.append(["初始持有行带解锁价即拒", func(t): t.routes[3]["unlock_price"] = 100, "cross_field"])
# 前置出域
cases.append(["prereq_route 出域即拒", func(t): t.routes[1]["prereq_route"] = "blade", "enum"])
# 前置类型不符(武器←甲)
cases.append(["前置类型不符即拒(武器行前置甲路线)", func(t): t.routes[1]["prereq_route"] = "light", "cross_field"])
# 前置自指
cases.append(["前置自指即拒", func(t): t.routes[1]["prereq_route"] = "spear", "cross_field"])
# 规模非六
cases.append(["行族规模非六即拒", func(t): t.routes.pop_back(), "structural"])
# 初始持有非一武器一甲(spear 转初始且清前置/解锁价——隔离出 coverage 单类)
cases.append(["初始持有非一武器一甲即拒", Callable(self, "_mut_spear_initial"), "coverage"])
# 默认轴≠初始路线类型轴
cases.append(["默认攻击轴≠初始武器类型轴即拒", func(t): t.default_attack_type = "pierce", "cross_field"])
# 默认甲轴出域
cases.append(["默认甲轴出域即拒", func(t): t.default_armor_type = "plate", "enum"])
# 同甲型两路线
cases.append(["同甲型第二路线即拒", func(t): t.routes[4]["type_axis"] = "light", "dup_key"])
for c in cases:
var label: String = c[0]
var mutate: Callable = c[1]
var expect_cat: String = c[2]
var t = ForgeTableScript.new()
t.routes = _base_rows()
mutate.call(t)
var errs: Array = t.validate()
Kit.check("锻造 schema 负向:%s〔锻造工坊批 V1〕" % label, _has_cat(errs, expect_cat))
# ---- V11:效果类型闭集零新增(满级特效本期不开新词)----
# 实数订正(L2 在册):卡片 F11「24 种」系 2026-10-07 设计文档事实快照——迭代 20 T1 +7、迭代 22 T3 +1
# 后现行 31;本批零扩签,钉现行数(将来任何一批加词即转红提示复核)。
func _test_effect_kinds_closed() -> void:
Kit.check("效果引擎效果类型闭集 31 种零新增(锻造工坊批不开效果类型词)〔锻造工坊批 V11〕",
EffectEngineScript.EFFECT_KINDS.size() == 31)
Kit.check("效果引擎事件词表在册 20 种(本批零扩签参照面)〔锻造工坊批 V11〕",
EffectEngineScript.EVENTS.size() == 20)
@@ -0,0 +1 @@
uid://frunrpvqgocl3
@@ -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
+3 -3
View File
@@ -24,8 +24,8 @@
## 当前位置(2026-10-11 刷新)
- **里程碑语义(2026-10-09 立规换代,详见下方里程碑表的换代注)**:现行里程碑 = **M3「Alpha 版本的完整实现」**——用户未明令宣布完成前停留 M3,迭代只排 M3 需求;M4=Beta 版本的完整实现,一律不排期。下文里程碑表的旧编号(M-3/M-4)保留作历史序列,语义以本节为准。
- **迭代进度**:迭代 19 五卡全部已入库(2026-10-08);**迭代 20=专长实装批 ✅ 已入库**(2026-10-11,出口闸全绿;残留人闸=实机目检五件+V20/V21/V22 人工核对半,07 在册);**迭代 21=天赋树实装批 ✅ 已入库(2026-10-12 U0 开工单会话交付,T0-T7 全绿;残留人闸=实机目检四柜台)**;**迭代 22=秘宝体系实现重构落地批已编入**(卡片 v1.2 零待裁),**开工门=U0 发令**(原「规划冻结维持」句已由 2026-10-11 编入令取代);**迭代 23=锻造工坊实装批已编入**(2026-10-12 用户令——设计面 v0.6 就绪〔17 裁点清零+完备性检查批,commit f5215c7〕,开工门=U0 发令);**迭代 24+**=M4 数值批(Alpha 收官位·未排期——用户令 2026-10-07:后续新迭代一律不先做 M4)。
- **近期主战场**:迭代 20 残留人闸(实机目检五件+V20/V21/V22 人工核对半);**下一前沿=迭代 22 秘宝实装批、迭代 23 锻造工坊实装批,均待 U0 发令**;锻造工坊实装批已编入迭代 23(设计面 v0.6:17 裁点清零+完备性检查批〔现役基线换代/两轴所有权移交/术语登记完毕〕)。
- **迭代进度**:迭代 19 五卡全部已入库(2026-10-08);**迭代 20=专长实装批 ✅ 已入库**(2026-10-11,出口闸全绿;残留人闸=实机目检五件+V20/V21/V22 人工核对半,07 在册);**迭代 21=天赋树实装批 ✅ 已入库(2026-10-12 U0 开工单会话交付,T0-T7 全绿;残留人闸=实机目检四柜台)**;**迭代 22=秘宝体系实现重构落地批 ✅ 已入库(2026-10-12 单会话交付,T1-T7 全绿;残留人闸=实机目检秘宝解锁-装配流程)**;**迭代 23=锻造工坊实装批 ✅ 已入库(2026-10-12 单会话交付,T0-T7 全绿;残留人闸=实机目检工作间形态与购买交互)**;**迭代 24+**=M4 数值批(Alpha 收官位·未排期——用户令 2026-10-07:后续新迭代一律不先做 M4)。
- **近期主战场**:迭代 20/21/22/23 残留人闸(实机目检四批:专长五件+营地四柜台+秘宝解锁装配+锻造工作间,07 在册);**下一前沿=M4 数值批,待 U0 发令**(锻造工坊批 provisional 数值终值归其统一标定)。
- **实时状态**:以根目录 [active.md](active.md) 顶部「当前状态快照」节为准(`stage.txt` 已定性为旧阶段开关,仅存档)。
## 主线路线(里程碑 × 迭代序列)
@@ -79,7 +79,7 @@
| 迭代 21 | **天赋树实装批(2026-10-11 用户令编入——整卡不切分,迭代容量放宽容纳全卡)**:按 [天赋树](design/gdd/systems/跨局循环/天赋树.md) **v0.8** 设计权威文档实装——**33→12 节点**结构换代(三轮换代:攻防所有权切分 29→21〔树零攻防来源=红线,攻/AC 唯一入口=锻造工坊路线〕/一线一节点 21→15〔生命·命中淬炼各一节点×4 级、row 字段退役〕/Fork 一节点双态 15→12〔开局姿态·危险直觉·财富之道三组各一节点、购买解锁后 A/B 零币切换〕;schema=Fork 词表 4→3 组/规则键词表 2→6 键、「等级阈值前置」形态去留=R1 开放待裁点〔推荐裁撤,**不阻开工门**〕)+三组 Fork 真效果(开局姿态每关首轮窗/危险直觉与财富之道·限次计数随冒险持久按关重置)+营地统一案甲(树=营地主界面/属性升级唯一入口/工作间钥匙模型/图鉴常开/占位面板零副作用硬要求)+注入公式换代(基础值+树淬炼+锻造路线加值位预留+局内加算)+旧属性柜台退役+旧档废弃与旧逻辑删除(开发阶段无迁移,Q3 裁决)+同调位槽位判定源切树节点(旧条件删除随批清扫)+树上刷新升级接线(双入口并存写同账)。需求卡片=design/research/需求卡片-天赋树实装批-2026-10-11.md(**v1.7**——grilling 五裁+歧义扫描六裁+**2026-10-12 完备性/跨系统歧义审查十一裁**全落;零阻塞待裁点);锻造工坊(原「武器锻造」)**17 裁点已清零**(2026-10-12 逐裁落文 v0.5,06 在册),Q5 排期前置已满足、默认本批先行;T0 的「标错机制存在性」与「boss_kill_count 消费面」两项**已预查结案**(标错机制查实存在且有代价,守侧 contingency 作废) | ✅ **已入库(2026-10-12,/goal+iter U0 开工令——T0-T7 全量交付;R1 按既定默认裁撤案执行;实现报告=design/qa/iter21-实现报告-2026-10-12.md;残留人闸=实机目检营地四柜台〔07 在册〕)** | T0-T7≈7.0-8.0d(整卡单迭代——实际 2026-10-12 单会话交付) |
| 迭代 22 | **秘宝体系实现重构落地批**——需求卡片=design/research/需求卡片-秘宝体系实现重构落地批-2026-10-11.md(v1.3 C1-C6 全裁);**2026-10-11 用户令编入**:新目录 12 件实装+解锁混合制(逐件分配锁定表在卡)+装配不可变(冒险中零更换)+旧数据行清理+效果引擎接入;槽位来源不在本批(树接口接缝——迭代 21 已实装树点亮数槽位源,本批直接消费)+装配档位格约束;估时 6d(区间 5-7d)——**✅ 已入库(2026-10-12,/goal+iter U0 开工单会话交付:T1 数据层 12 行换代+schema 扩签/T2 解锁服务+工坊面退役+档位格约束/T3 效果引擎接入+扩签+常时参数面+试炼镐积点口+充能周期/T4 呈现换代+呈现桥+试炼镐 HUD 入口/T5 替换子交互拆除+库存保留注记/T6 验收闸断言;冒烟 898/0+run_all 22 套+重闸全绿;实现报告=design/qa/iter22-实现报告-2026-10-12.md;残留人闸=实机目检秘宝解锁-装配正常流程〔07 ㉑〕)**
| 迭代 23+ | **M4 数值批(**原 22+;迭代 21 已编入天赋树实装批、迭代 22 已编入秘宝实装批,均 2026-10-11 用户令**——**Alpha 收官位·未排期**〔2026-10-09 语义换代:M4=Beta 阶段、非 Alpha 收官位,M3 未由用户明令关闭前不排期维持〕——2026-10-05 用户令编入迭代 17 卡②;2026-10-06 用户令移出独立成批并定序「Alpha 阶段最后——内容补全先行、统一数值调整殿后」〔当日曾被编入迭代 18 为收官卡,2026-10-07 用户令撤回〕;**2026-10-07 用户令二次后移:后续新迭代(迭代 20 起)一律不先做 M4,除非用户明确要求启动 M4 批次**)**:四件数值欠账合一批——①削弱档位终值(weaken 现值 −65/duration 2/refresh 考证在册;叠层改续时语义已定案,差 duration 终值+当量锚复算)②调参批数值点评(06 条目现行登记五条——任务口径「八条」与条目五条不符,需求卡片 §2.2 已呈报差异以条目为准)③迭代 10 奖励重构联动欠账①②(钥石产出重标 boss_keystone 首杀 3/3/3·复杀 2/2/3 provisional + 通关率带重扫〔通关回血 20%+AP 件首轮 +1〕)④元进度定价剩余面(forge base 20/比 150 考证在册——06 条目文本「base 10」滞后于 2026-09-22 对齿批已呈报;Boss 两档终值)。读数义务随批=迭代 14 两件后置复算(T6 MC 四带复算+T7 技能池节奏重测,均挂 07);顺路修复 damage_probe A1 改前既有红灯(判式换代批出口报告 §四.7 在册)。**硬约束:一切 MC/verifier 跑批发起前须用户批准;调参联动禁独立调参网络(β 带 [1.2,2.5] 逐 u 检算)**。需求卡片=`design/research/需求卡片-M4数值批-2026-10-05.md`(**v1.2 2026-10-06 案C 增补+联审修订**:v1.1 三事实与第五推算件照旧;联审=P3 转正顺延至重扫读数后/检算工具 WEAKEN_PCT 入同步面/F11 两句死文本全刷/T3·T5 四带复算并账一次跑批双认账/R5 改前锚=本批新立案C 锚/估时段一勘误 4.25d;案C 增补内容照旧=§2.6 三事实〔行为换代/锚全作废/难度预判〕+T0 推算稿第五件=对拍锚按新序重推导+T6 四带复算按案C 新行为首采;T0-T8≈6.25d,天然切分点 T4 末;🔴 契约 5 条含跑批批准制;**✅ 裁点全裁(2026-10-06 用户令:P1甲=维持 2 场·magnitude [60,85] 带内当量锚复算、具体值随出口 provisional 追认/P2甲 不对称有意/P3甲 3-3-3·2-2-3 维持、转正顺延至重扫读数到账且带内(Q4 二裁)/P4甲 不立直购价);开工=U0**) | 迭代 18 全部内容卡(③④⑤⑦)出口+全部内容批行为变更落定+**用户发令(未排期)**——重锚窗口须收尽全部内容批行为变更 | T0-T8≈6.25d(超软盒预挂,段一 T0-T4≈4.25d〔v1.2 勘误,原 4.5 系加总错误〕/段二 T5-T8≈2.0d) |
| 迭代 23 | **锻造工坊实装批(2026-10-12 用户令编入——设计面 v0.6 就绪)**:按 [锻造工坊](design/gdd/systems/跨局循环/锻造工坊.md) **v0.6** 设计权威文档实装——迁移面:①新建锻造路线行族(现役 `meta.tres` forge_products 已随迭代 21 T5 退役、无改造对象;钥石定价历史考古 20/150%、投值归 M4)②两根类型轴所有权移交(默认值归锻造工坊配置常驻持有、hero.tres 静态表两轴删除、选中态写存档域+冒险开始参数面注入)③存档域 forge_routes(已预登记存档序列化.md v7 计划行)④满级特效本期不实装(只留扩展位)⑤敌方 attack_type 填值归敌人系统/战斗结算侧(非本批——敌方定义数据、与名字血量同时读写、无独立时序)⑥术语登记(v0.6 已做完:术语表+registry)⑦数值验证义务=改前/改后同种子配对读数。解锁通道=并联双通道;一次冒险=一武器类型+一甲型。开工门=U0 发令 | 已编入待 U0 发令(设计面 v0.6:17 裁点清零+完备性检查批,commit f5215c7) | 估时随需求卡片立项 |
| 迭代 23 | **锻造工坊实装批(2026-10-12 用户令编入——设计面 v0.6 就绪)**:按 [锻造工坊](design/gdd/systems/跨局循环/锻造工坊.md) **v0.6** 设计权威文档实装——迁移面:①新建锻造路线行族(现役 `meta.tres` forge_products 已随迭代 21 T5 退役、无改造对象;钥石定价历史考古 20/150%、投值归 M4)②两根类型轴所有权移交(默认值归锻造工坊配置常驻持有、hero.tres 静态表两轴删除、选中态写存档域+冒险开始参数面注入)③存档域 forge_routes(已预登记存档序列化.md v7 计划行)④满级特效本期不实装(只留扩展位)⑤敌方 attack_type 填值归敌人系统/战斗结算侧(非本批——敌方定义数据、与名字血量同时读写、无独立时序)⑥术语登记(v0.6 已做完:术语表+registry)⑦数值验证义务=改前/改后同种子配对读数。解锁通道=并联双通道;一次冒险=一武器类型+一甲型。**✅ 已全批入库(2026-10-12,/goal+iter U0 开工单会话交付——worktree g-plan2-iter23-forge 七提交;T1 行族+T2 两轴移交+T3 写口族+T5 存档 v7(序调先行)+T4 注入链+T6 工作间 UI+T7 收口;冒烟 938/0+run_all 24 套+probe click/full+render_gate+doc 三闸/熵增对基线零新增+MC 同种子配对零漂移;实现报告=design/qa/iter23-实现报告-2026-10-12.md;残留人闸=实机目检工作间形态与购买交互〔07 在册〕;代拍板追认单+GDD 反哺点待用户)** | ✅ 已入库(出口闸全绿+实机目检归用户 07 在册) | 估时随需求卡片立项(实际单会话) |
| 迭代 24+ | **M4 数值批(**原 22+;迭代 21 已编入天赋树实装批、迭代 22 已编入秘宝实装批(均 2026-10-11 用户令)、迭代 23 已编入锻造工坊实装批(2026-10-12 用户令)**——**Alpha 收官位·未排期**〔2026-10-09 语义换代:M4=Beta 阶段、非 Alpha 收官位,M3 未由用户明令关闭前不排期维持〕——2026-10-05 用户令编入迭代 17 卡②;2026-10-06 用户令移出独立成批并定序「Alpha 阶段最后——内容补全先行、统一数值调整殿后」〔当日曾被编入迭代 18 为收官卡,2026-10-07 用户令撤回〕;**2026-10-07 用户令二次后移:后续新迭代(迭代 20 起)一律不先做 M4,除非用户明确要求启动 M4 批次**)**:四件数值欠账合一批——①削弱档位终值(weaken 现值 −65/duration 2/refresh 考证在册;叠层改续时语义已定案,差 duration 终值+当量锚复算)②调参批数值点评(06 条目现行登记五条——任务口径「八条」与条目五条不符,需求卡片 §2.2 已呈报差异以条目为准)③迭代 10 奖励重构联动欠账①②(钥石产出重标 boss_keystone 首杀 3/3/3·复杀 2/2/3 provisional + 通关率带重扫〔通关回血 20%+AP 件首轮 +1〕)④元进度定价剩余面(forge base 20/比 150 考证在册——06 条目文本「base 10」滞后于 2026-09-22 对齿批已呈报;Boss 两档终值)。读数义务随批=迭代 14 两件后置复算(T6 MC 四带复算+T7 技能池节奏重测,均挂 07);顺路修复 damage_probe A1 改前既有红灯(判式换代批出口报告 §四.7 在册)。**硬约束:一切 MC/verifier 跑批发起前须用户批准;调参联动禁独立调参网络(β 带 [1.2,2.5] 逐 u 检算)**。需求卡片=`design/research/需求卡片-M4数值批-2026-10-05.md`(**v1.2 2026-10-06 案C 增补+联审修订**:v1.1 三事实与第五推算件照旧;联审=P3 转正顺延至重扫读数后/检算工具 WEAKEN_PCT 入同步面/F11 两句死文本全刷/T3·T5 四带复算并账一次跑批双认账/R5 改前锚=本批新立案C 锚/估时段一勘误 4.25d;案C 增补内容照旧=§2.6 三事实〔行为换代/锚全作废/难度预判〕+T0 推算稿第五件=对拍锚按新序重推导+T6 四带复算按案C 新行为首采;T0-T8≈6.25d,天然切分点 T4 末;🔴 契约 5 条含跑批批准制;**✅ 裁点全裁(2026-10-06 用户令:P1甲=维持 2 场·magnitude [60,85] 带内当量锚复算、具体值随出口 provisional 追认/P2甲 不对称有意/P3甲 3-3-3·2-2-3 维持、转正顺延至重扫读数到账且带内(Q4 二裁)/P4甲 不立直购价);开工=U0**) | 迭代 18 全部内容卡(③④⑤⑦)出口+全部内容批行为变更落定+**用户发令(未排期)**——重锚窗口须收尽全部内容批行为变更 | T0-T8≈6.25d(超软盒预挂,段一 T0-T4≈4.25d〔v1.2 勘误,原 4.5 系加总错误〕/段二 T5-T8≈2.0d) |
## 执行纪律(2026-09-27 用户立规)