merge: 迭代22秘宝体系实现重构落地批——新目录12件换代+解锁混合制+档位格约束+效果引擎接入+常时参数面+试炼镐积点口+充能周期+呈现换代+替换子交互拆除(T1-T7 全绿;实现报告=design/qa/iter22-实现报告-2026-10-12.md;残留人闸=实机目检秘宝解锁-装配正常流程〔07㉑〕;代拍板追认单十五条+反哺点七条待用户)

This commit is contained in:
DKChan committed 2026-10-12 19:48:02 +08:00
commit 144e53748e
44 files changed
+1418 -906

No files matched your search

+1 -1
View File
@@ -176,7 +176,7 @@
**归属**:[秘宝](systems/跨局循环/秘宝.md)「目录(重设计 v1)」实装 + [道具退役素材库](../research/道具退役素材库.md) 对应清理面
**状态**:已编入迭代 22(2026-10-11 用户令;需求卡片 v1.2 零待裁——开工门=U0)
**状态**:✅ **已入库(2026-10-12,/goal+iter U0 开工令——worktree g-plan2-iter22-treasure,T1-T6 五提交全量交付;批末重闸全绿=冒烟 898/0+run_all 22 套+probe 含 click/full+render_gate+doc 三闸对基线零新增+熵增 PASS;实现报告=design/qa/iter22-实现报告-2026-10-12.md,代拍板追认单与 GDD 反哺点随报告待用户)。残留人闸=实机目检秘宝解锁-装配正常流程体验(07 ㉑ 在册)**。条目②旧数据行清理随 T1 全量换代销号;③专长侧联动已由迭代 20 承载销账(进展注记在案);magic_vs_magic 交叉核对已结(魔网织者在删除名单——锻造工坊批③作废,2026-10-11 D 组核结);本条目余项(①④⑤⑥⑦)随本批交付或经反哺点/另批承接,逐项去向见实现报告 §六
**进展注记(2026-10-10)**:数据文件面本批不依赖锻造工坊线;**槽位面依赖修正(2026-10-10 设计骨架批后)**——同调位数量与品质的语义已裁归天赋树同调位节点(树侧接线归天赋树线/天赋树实装批),本批与该批存在槽位定序点(先树后宝或合并评估,随排期裁),现役谓词表求值作为本批落地前的过渡基座仍成立;秘宝体系文档正确性检查已过(五文件交叉核对:仅 registry 三处现行旧称「魔法道具」残留,已随检查批修正;其余命中均为版本历史与更名记载的合法引述)——**检查通过;设计侧裁点经 grilling 审查轮+设计骨架批已清零(秘宝.md v2.3),按三段流程设计段完成**。**③专长侧联动已拆出独立成卡(2026-10-11 用户发令;同日用户纠正=奇物线单独成卡、秘宝面不并入)**:design/research/需求卡片-专长实装批-2026-10-11.md(T0-T10 ≈10-12d 单迭代交付;**Q1-Q3 已全裁清零**〔Q1 Boss 兑付换代/Q2 占位值/Q3 单迭代〕——**卡片成立,零待裁点;✅ 专长实装批已入库(2026-10-11 迭代 20 T0-T10 全落 master——③专长侧联动由该批承载销账:feats.tres 29 行换代/词表扩签/停手挂点 dig_stop 全落)**;奇物侧设计+命名+裁定齐备〔奇物.md v3.15〕)——本条目余项(①②④⑤⑥⑦)维持冻结待发令,其中⑥效果引擎扩签核实与停手挂点由专长实装批 T1 先行承载。两条线交叉核对点一处随本批 T0:feats.tres 的 magic_vs_magic 条件值所属专长件是否在素材库表二 27 件删除名单内(若在,锻造工坊实装批③「改值复活」随之作废)——**已核结(2026-10-11 锻造工坊裁决会话 D 组)**:所属件=魔网织者(mana_weaver),在名单内,③「改值复活」作废,废卡随专长 27 件退役删行批消解。
+8
View File
@@ -201,6 +201,14 @@
- **连带面**:暂停层属性锻造段已消失(钥石+工坊+天赋树概览);生涯页两类六项(三柜台概览行已删);局内六键体感(先手首战首轮 +1AP/锋踩雷减半/守标错下轮 +AP/聚结算框 GP 即实得/散货架八折标价)。
- 验证方式:用户实机目检(本批自动闸外唯一人闸)。
### ㉑ 秘宝解锁-装配正常流程体验(迭代 22 秘宝体系实现重构落地批随批登记——2026-10-12)
- **解锁面**:攒钥石→秘宝匣柜台钥石购买(余额不足钮禁用、购买后行板即时刷新为已解锁);条件件进度呈现(远程镐=通关 1 次/刷新执照=Boss 击杀 3 次/天眼=通关 3 且 Boss 4/信标网络=真视使用 10——达标未解锁时开营地/冒险收束即自动入账)。
- **装配面**:槽位=树同调位点亮数(未点亮树=0 槽不能装配);件档≤格档(普通/优秀/稀有递增向下兼容——稀有件装普通格被拒);未解锁件装配钮禁用;满位禁选。
- **生效面抽查**:晨操令/双管装填首轮 AP、阔视野带视野 8→12、巢穴预告开局报雷数 toast、线香单格真视(战斗级一次)、天眼 3×3 安全格直开(关卡级一次)、试炼镐 HUD 兑换钮积点滑耗、储备罐停手转存变厚、远程镐距 2 可挖、信标网络任意格插旗、刷新练习/刷新执照加仓。
- **装配不可变**:冒险中无任何换装入口(三选一/奖励节点/战斗内零替换交互——旧替换子交互已拆)。
- 验证方式:用户实机目检(本批自动闸外唯一人闸;能力验证半已由自动化闸覆盖=V1-V19)。
## 验收读数重测义务(履行留痕 + 现行义务)
**已履行**(按序换代,旧读数各随其批作废留痕不入本单):①验收读数重测批(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 重锚入库后补算终值。
@@ -0,0 +1,67 @@
# 迭代 22 实现报告——秘宝体系实现重构落地批(2026-10-12)
> 执行:/goal+iter 技能 U0 开工令,单会话交付(worktree g-plan2-iter22-treasure,分支 iter22-treasure,基点 5a2ab8c)。
> 需求卡片:design/research/需求卡片-秘宝体系实现重构落地批-2026-10-11.md(v1.3,C1-C6 全裁,零阻塞待裁点)。
> 设计权威:秘宝.md v2.9「目录(重设计 v1)」。
## 一、各任务卡交付状态
| 卡 | 交付 | 提交 |
|---|---|---|
| T0 | 基线复读(冒烟 886/0+run_all 22 套+probe --click 全绿)+四项核实(X/Y 对表/magic_vs_magic 结案/扩签清单/真视计数接入点)+自洽扫描四处事实换代登记(卡片 F1/F2/F4 预设被迭代 20/21 推进) | (active.md 状态记录 477) |
| T1 | treasure_items.tres 新 12 行全量换代(旧 12 行删)+schema 换代(tier/charge_reset/unlock_keystone_cost 三键+TIERS/TIER_RANK/CHARGE_RESETS 词表+TARGETS 扩 cell+参数面 kind 六型+REVEAL_KINDS 护栏扩面+charges=0 积点制+解锁互斥校验+UNLOCK_COUNTER_KEYS 换代)+引擎扩签(EVENTS+board_installed/ACTIVE_KINDS+safe_dig/EFFECT_KINDS+mine_count_preview)+V13 旧断言摘除换代 | T1 提交 |
| T2 | 解锁服务换代(buy_treasure_unlock/refresh_treasure_unlocks/treasure_shop_status/_treasure_unlock_progress 单点)+工坊渠道面退役(行族/词表/三函数/访问器全删)+vision_used 生涯计数域(补零不升版本)+toggle_attunement 三重校验(未解锁拒+满位禁选+件档 rank≤装入位序)+秘宝匣呈现数据管道换代 | a7bd4e4 |
| T3 | board_installed 发射+巢穴预告读数口/天眼 safe_dig=fx_reveal 通道复用(C5 全额兑付免 AP)/reveal_tiles 入真视叠加层/试炼镐 use_attune_convert 积点受理口+计数通用化/充能重置周期 _reseed_attune_charges(battle/level)/常时参数面注入(挖距/旗可达/转存率/刷新加仓/商店刷新)/vision 计数链(共享 dict→summary→settle 入档+断点随件)/validate_use 扩 cell+新空间 kind/修现役潜伏缺陷(use_treasure 板维度读不存在的 Rules.width)+新单测 treasure_effects_test(V7 附属表 12 件逐件正常/边界/反例) | T3 提交 |
| T4 | 秘宝匣柜台购买钮+分档呈现+档位标签+未解锁禁装/呈现桥换代(旧提灯布尔桥退役→set_fog_glow_bonus 参数面,main._sync_treasure_glow)/巢穴预告 toast/试炼镐 HUD 兑换钮(ap_bar+board_zone 转发链)/ui_input_probe case⑮ 换代主语+4/V13 功能残留换代/修 BoardZoneView 缺 convert_pressed 转发(呈现组装挂死根因) | b7a2ead |
| T5 | 替换子交互整体拆除(装配不可变契约 1 死通道:core 六函数+_bonus_treasure_carry 携带+_last_draw_treasure 掷定链+bonus_aux_carry 断点键+grant_treasure_item+holding_records+呈现三态+overlay replace 状态机+探针/autoplay 策略)/treasure_inventory 保留注记(主动件骑库存承载在役)/apply_node_curio 普通兑付主体误删补回 | 7095505(amend 后) |
| T6 | 验收闸断言(V9 词表零改写/V10 三方向覆盖/V18 空目录合法态/V6 装配不可变三面同源快照)+批末重闸全绿+ui_probe 柜台行数断言换代(6→12) | T6 两提交 |
## 二、代拍板追认单(十五条,待用户逐条追认)
1. **回补点 1 执行**:按设计权威 12 件全量落地,刷新练习归钥石购买(C1「普通档全钥石」直接应用)——卡片推荐案。
2. **回补点 2 消解**:迭代 20 已删 venom_edge/flag_burst,本批实删 12 行(非 14)。
3. **回补点 3 执行形态**:treasure_inventory **部分拆除+保留注记**——拆除=替换子交互死通道全清(装配不可变实现面保证);保留=库存类本体(新目录主动件线香/天眼的骑库存承载在役:handle 注入+use_treasure 受理+断点 aux_items 快照)。卡片「拆除或保留注记」双路线并写的第三形态,理由=T3 复用现役机制零新建。
4. **试炼镐两级积点单级化**:设计「每开 4 安全格积 1 点、满 2 点换 AP+1」→ 实现=ap_charge_safe charge_n=8 滑耗(行为等价:每 8 格兑 1 AP,免上限滑动消耗)——代拍板白名单「订阅行等价实现细节」;desc 保留设计原文两级表述。
5. **试炼镐受理口与入口**:绕库存直呼 use_attune_convert(charges=0 零骑库存)+HUD 行动条兑换钮(ap_bar convert_pressed 转发链,未装配隐藏、积点未达禁用、文案含进度 N/M)——卡片未列明入口形态,本批落 HUD 最小入口。
6. **解锁条件表落位**:卡片 T1「attunement_growth.tres 改造为解锁条件表」——该表已被迭代 21 删除,落位改为 treasure_items 行内 unlock_requires(单一事实源,schema 校验单点)。
7. **计数键换代**:UNLOCK_COUNTER_KEYS 旧键 clear_count(旧工坊按关累计语义)与 C6 通关口径不符,换名 adventure_cleared(生涯通关局数)+扩 vision_used——旧键零行消费安全换名。
8. **vision_used 承载**:save v6 meta 域补零新键(不升版本,迭代 21 先例)+断点随件 vision_counter_used;计数点=use_treasure 受理成功(真视类 kind),冒险共享 dict 聚合、收束 add_vision_used 入档。
9. **呈现桥换代**:旧提灯布尔桥(set_fog_lantern/FOG_LANTERN_BONUS)随行退役,换代 set_fog_glow_bonus 参数面(阔视野带 magnitude 4→半径 8+4);off 态写 0 复原契约保持。
10. **巢穴预告呈现**:开局 toast「巢穴预告——本板危险格共 N 个」(读数口 nest_preview_count);不走结算明细行(行来源纪律——无格来源)。
11. **积点兑换零明细行**:对齐 use_treasure「效果即时可见零明细行」纪律,兑付反馈=AP 条即时刷新。
12. **价格 provisional 占位**:普通档 10 钥石/优秀档 25 钥石(稀有档全条件无价)——调参批终值。
13. **magic_vs_magic 结案维持**:行已随迭代 20 删除(grep 零命中),锻造工坊实装批③「改值复活」作废(06 已核结)。
14. **reward_dangling_treasure_test 保留**:悬空引用拦截机制在役,注入主语=故意悬空旧 id(fixture 本体非旧件验证)——V13「随行清亡」指注入行,机制测试保留。
15. **过程修正**:T5 提交误入拷贝嵌套素材(assets/ui/ui/ 9 png)已 amend 剔除,终态零素材变更。
## 三、GDD 反哺点(实现与设计权威差异——待用户逐点裁决后另批落文,本批零直改 GDD)
1. **秘宝.md「目录(重设计 v1)」节**:逐件表补实录列(item_id/tier/解锁方式与价格或条件/参数 kind);「实装批落地前表内旧目录同调行冻结维持」「受理框架为新目录主动件沿用——现役代码按同框架运行」两处现状句已过期须换代;旧目录退役指针节补「代码旧行已删(2026-10-12)」。
2. **秘宝.md 代价轴**:试炼镐两级积点结构→单级 8 格滑耗等价实现注记(若用户裁保两级呈现,改 desc 与 HUD 进度文案即可,机制不动)。
3. **秘宝.md 解锁混合制节**:解锁条件表实录(三计数键+阈值 provisional:远程镐 1/刷新执照 3/天眼 3+4/信标网络 10;价格 10/25)。
4. **元进度.md**:道具工坊→秘宝匣换代实录(工坊渠道面退役、购买钮、条件进度呈现、treasure_shop_status 投影);「forge base 20/比 150」等工坊史句核对。
5. **camp.md**:秘宝匣柜台呈现细则(购买钮禁用态/名后缀·档位/装配行档位标签/未解锁禁装/悬停 desc/空目录合法态);暂停层秘宝匣 12 行只读概览。
6. **存档序列化.md**:vision_used 域+vision_counter_used 断点随件+bonus_aux_carry 键退役(v6 不变零迁移口径复核)。
7. **行动经济.md**:finish_round_stop 转存率参数化(transfer_pct 缺省 100 逐位同旧;储备罐 125=1.25:1 整除 floor 口径)。
## 四、实机目检清单(唯一人闸)
07-体验验证清单 ㉑「秘宝解锁-装配正常流程体验」(本批随批登记):解锁面/装配面/生效面抽查/装配不可变四组——验证人=用户。
## 五、闸读数(批末全绿)
- 冒烟 898/0(基线 886→898:V1/V2/V3/V4/V5/V9/V10/V18 断言净增,V13 摘除同步)
- run_all 22 套全绿(含新套件 treasure_effects_test)
- probe_gate --click/--full 全绿(ui_probe 柜台行数断言换代后);残留 Godot=0
- render_gate 全绿(golden 3 屏 0.00%)
- doc 三闸对主区基线零新增(死链 3/拔旗 1 均存量);熵增闸 PASS
- MC 零跑(本批零随机无概率面——卡片 7.1 备用项未触发)
## 六、卡片余项去向
- ①新目录实装 ②旧数据行清理:本批 T1-T5 全量交付销号。
- ③专长侧联动:迭代 20 已承载销账(06 进展注记在案)。
- ④空配置验收闸:装配面本批 V18 交付;池空兜底/商店空货架归奇物/商店侧另批(卡片 §4 去向不变)。
- ⑤池结构选型/配额数字:奇物侧另批(卡片 §4 去向不变)。
- ⑥效果引擎扩签核实:本批 T0 清单+T3 落地(board_installed/safe_dig/mine_count_preview/参数面六 kind)。
- ⑦两疑似漏项:T0 核实结案——件级跨事件状态账=counter domain=adventure 承载(试炼镐积点);参数注入面=六常时参数 kind 注入点全落(_adjacent_to_hero 两分支/finish_round_stop/shop_refresh_free_left/set_fog_glow_bonus/_refresh_stock)。
+5 -2
View File
@@ -85,7 +85,7 @@ entities:
path: user://save.json(单档 MVP——多存档槽位 P2 保留)
format: JSON 单档(人可读可调试;字典键一律 String)
version: SAVE_FORMAT_VERSION=6(registry schema_version 同源;v6=生涯终身计数域 2026-10-07 迭代 18 卡④——meta 两新域 adventure_total〔冒险总局数,开局即计含放弃与失败〕/adventure_cleared〔通关局数,三关全通才计 1——判定字段 summary.cleared〕,旧档缺键由 _normalize 按默认补零,收束自增唯一入账=apply_adventure_summary;v5=天赋树域 2026-10-04 迭代 12 T9——meta 三新域 tree_nodes/fork_sides/migration_note;v4=同调批扩域 2026-10-03 迭代 11 第①批——meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level + 断点附随件 attunement_charges,旧档缺键由 _normalize 按默认补齐;v3=断点附随件扩列+摘要 keystone_drops 2026-10-01 迭代 10;v2=断点域出生 2026-09-29 迭代 8 卡①;生产迁移链 v1→v6 链式补空;高版本拒读 + 禁写盘 + 内存默认新档运行;低版本读档时链式迁移 register_migration,任一步失败按损坏兜底)
domains: meta 域十一件——keystone_balance(元进度)/ forge_levels 三档(元进度)/ item_unlocks(元进度)/ level_clear_counts(元进度)/ boss_first_kill(元进度)/ boss_kill_count(秘宝——同调位里程碑计数源,Boss 恒唯一终点故收束入账=levels_cleared)/ attunement_equipped(秘宝——装配关系有序数组,局外进度事实非局内道具库存,装配序即呈现序)/ attune_refresh_level(秘宝——Y 刷新升级购买等级)/ tree_nodes+fork_sides(天赋树——迭代 12 T9 树域)/ adventure_total+adventure_cleared(生涯——终身计数域,迭代 18 卡④,写入唯一经 apply_adventure_summary)/ recent_summaries cap 10(冒险结构,元素附 boss_drops 副本);+ breakpoint 域(断点快照:七字段拍板面+附随件〔主种子/取数计数器/钥石累计/已到节点序/道具库存/bonus_* 附随件/attunement_charges 充能余量——迭代 11 第①批 T6〕,空字典=无断点合法态,三结局清除)
domains: meta 域十一件——keystone_balance(元进度)/ forge_levels 三档(元进度)/ item_unlocks(元进度)/ level_clear_counts(元进度)/ boss_first_kill(元进度)/ boss_kill_count(秘宝——条件解锁计数源〔迭代 22 换代——原同调位里程碑语义随槽位收编退役〕,Boss 恒唯一终点故收束入账=levels_cleared)/ attunement_equipped(秘宝——装配关系有序数组,局外进度事实非局内道具库存,装配序即呈现序)/ attune_refresh_level(秘宝——Y 刷新升级购买等级)/ tree_nodes+fork_sides(天赋树——迭代 12 T9 树域)/ adventure_total+adventure_cleared(生涯——终身计数域,迭代 18 卡④,写入唯一经 apply_adventure_summary)+ vision_used(秘宝——真视类工具累计使用,迭代 22 T2 新增,缺键补零不升版本,写入唯一经 add_vision_used 收束聚合)/ recent_summaries cap 10(冒险结构,元素附 boss_drops 副本);+ breakpoint 域(断点快照:七字段拍板面+附随件〔主种子/取数计数器/钥石累计/已到节点序/道具库存/bonus_* 附随件/attunement_charges 充能余量——迭代 11 第①批 T6〕,空字典=无断点合法态,三结局清除)
write_timing: 启动读盘;正式冒险收束(adventure_finished → apply_adventure_summary)写盘;营地每笔钥石变更后即时写(元进度实现批接线);冒险选路停点断点快照写盘 + 三结局清断点写盘(迭代 8 卡①);调试两层与单关测试路径(LevelRunner.start_level,仅 QA 工具)不写档
atomic_write: tmp + rename 原子写(写一半断电不留烂档——钥石不丢判据承载)
corruption_policy: 坏档改名 <path>.corrupt.bak(已存在覆盖)+ 默认新档继续 + 警告留痕(不静默丢档不 crash)
@@ -541,6 +541,7 @@ entities:
closures: 地图战斗资源 OQ2 秘宝 侧收口(呈现深度 = 道具驱动预读;未预读格不呈现、自动寻路不做——终态与否 OQ6)· 创意风险 #1 道具侧兑现(plan 维度预读——结构闭环最后一块,呈现归 HUD框架)· 显影语义(原为雾格显影 = fog_reveal)随雾格移除失义、载体待重定(随秘宝整体重设计归 Alpha,2026-09-20 用户拍板)
warnings: 预警①…④ playtest 预注册归 难度曲线与Boss(道具依赖度/预读覆盖率/AP 转存联动/稀有感知)
attunement_refactor: 迭代 11 第①批 同调实施批 T1(2026-10-03)——表扩列 {form ∈ {"", passive, active_charged}(空=切片过渡形态行), charges_per_adventure, slot_cost(同调行恒 1 校验), unlock_requires 谓词行〔空=首批全解锁合法态,底稿 §2.2 用户裁〕, effective_rounds(P4 甲表值化), charge_n} + effect_kinds 扩 {venom_rider, flag_burst, mine_hit_halve, reflect_damage, ap_rounds_front, ap_charge_safe, cell_removal, reward_weight_bias, bonus_refresh}(mine_reveal/ap_grant 语义不变)+ 🔴 零裸 flat 结构护栏 = 行键白名单封闭(KEY_WHITELIST 外键=加载致命——机制轴禁无条件属性加项的结构承载);七件同调行入表(venom_edge 淬毒刃纹/flag_burst 标中殉爆/nest_counter 惊巢反制/dawn_order 工兵晨令〔effective_rounds=3〕/dig_charge 掘进蓄能〔charges=2·charge_n=3〕/luck_coin 偏运硬币/pick_refresh 三选一刷新件〔X=1〕——值全 provisional)+ 取数口径三分(attunement_rows/transitional_rows/active_rows——同调行永不入过渡渠道);nest_pendulum/blast_kit 维持过渡行 T7 原位换装,sapper_order/probe_dust/catacomb_scroll/loot_compass T7 逐件销账(底稿 §3);维度词表收缩形态 = 开放拍板点(需求卡片 P1-P8 未覆盖,底稿 §1.1 明挂——T10 追认单随批,行标签 provisional)
iteration22_refactor: 迭代 22 秘宝体系实现重构落地批(2026-10-12,T1-T3)——**新目录 12 行全量换代**(设计权威=秘宝.md「目录(重设计 v1)」:晨操令/阔视野带/试炼镐/刷新练习/储备罐/巢穴预告/线香/双管装填/远程镐/刷新执照/天眼/信标网络;旧 12 行删除——venom_edge/flag_burst 已随迭代 20 专长批迁出)+ 三档 tier ∈ {common,excellent,rare}(7/3/2,传说空集合法;TIER_RANK=装配档位校验取数源:件档 rank ≤ 装入位序)+ 解锁混合制行内单源(钥石购买 unlock_keystone_cost / 条件 unlock_requires 谓词——计数键 UNLOCK_COUNTER_KEYS 换代 {adventure_cleared〔C6 通关口径=打通整局〕, boss_kill_count, vision_used〔真视类工具累计使用——save v6 补零新域,受理成功计数、收束聚合入档〕},购买价与谓词互斥 schema 强制)+ 充能重置周期 charge_reset ∈ {adventure,battle,level}(battle=编排器每战回满/level=每关首战回满,骑库存差额补注入;charges=0=积点制合法态〔试炼镐——use_attune_convert 积点受理口,零骑库存〕)+ 常时参数面 kind 六型(light_radius_bonus/rap_transfer_mult/dig_range_bonus/flag_range_free/refresh_stock_grant/shop_refresh_bonus——无订阅行,注入面按装配集 kind 分发)+ 主动分发 safe_dig(天眼=fx_reveal 通道复用全额兑付)与 mine_count_preview(巢穴预告,board_installed 事件 T3 扩签)+ target 扩 cell(线香单格)+ 替换子交互整体拆除(装配不可变契约 1——CURIO_KINDS 除名后不可达死通道:treasure_replace_presented 链/bonus_aux_carry 断点键/grant_treasure_item/替换呈现三态全清)· 工坊解锁渠道面退役(meta.tres workshop_items 行族与 MetaProgression 工坊三函数删除,秘宝匣柜台投影=treasure_shop_status)
referenced_by: [战斗结算, 地图战斗资源, 地形变异, 行动经济, HUD框架, 数据配置, 教学引导, art-bible]
- name: attunement_growth
@@ -552,6 +553,7 @@ entities:
counter_keys: 封闭枚举 {clear_count(通关次数按关累计——save 复用零改动), boss_kill_count(Boss 击杀总数含重复——本批 T2 新增收束记账)};预留可插拔位 boss_firstkill_count/adventure_count/no_damage_clear_count/run_depth_best(加行即扩展,底稿 §2.1)
schema_guards: 阈值 [1,999] · Σgrant ≤ slot_cap · 计数键出域=加载致命;数值全 provisional 归 难度曲线与Boss(形态锁死、调平衡=改表不动码)
mvp_content: m1 clear_count≥1→grant 1 · m2 boss_kill_count≥3→grant 1 · m3 {clear_count≥3 AND boss_kill_count≥6}→grant 1(行值 provisional;底稿 §2.1 行示例形态)
status: **表已删除**(迭代 21 T5 槽位收编——槽位判定源=树同调位节点点亮数;本条目=史保留。迭代 22 秘宝批解锁条件面=treasure_items 行内 unlock_requires 谓词〔UNLOCK_COUNTER_KEYS 内联 schema〕,非本表复活)
referenced_by: [秘宝, 元进度, 数据配置, 冒险结构]
- name: attunement_slots
@@ -559,7 +561,8 @@ entities:
owner: design/gdd/systems/跨局循环/秘宝.md
attributes:
rule: 同调位=机制轴总功率闸(底稿 §2)——初始 0 起步(裁决 3)· 硬上限 slot_cap=3 · 单件占 1 槽(裁决 5,slot_cost=1 schema 强制)· 装配=冒险开始前的营地决定(底稿 §8.5 默认中途不可换装)· 装配关系入存档持久(save v4,本批 T2)
growth_source: 通关次数 + Boss 击杀数(含重复)经 attunement_growth 谓词表(裁决 4)——读数源自 transitional_sync_slots 过渡键(adventure.tres provisional)切换随本批 T3,迭代 10 欠账③清偿
growth_source: **迭代 21 换代**——槽位数量 = 树同调位节点 att_socket1..3 点亮数(Attunement.slots,未点亮=0;旧谓词表判定链随迭代 21 T5 删除)
tiered_gates: **迭代 22 秘宝批新增**——格档递增 1=普通格/2=优秀格/3=稀有格(向下兼容=高档格可装低档件);装配校验=件档 TIER_RANK ≤ 装入位序(toggle_attunement,卸下恒可、效果面只读装配集不受格位影响)
layered_acceptance: attune=off 续用现行带 · attune=on 带平移归 难度曲线与Boss 投值(商店 shop=off/on 分层先例同法)
referenced_by: [秘宝, 元进度, 数据配置]
+5 -4
View File
@@ -185,16 +185,17 @@ func grant_reserve_ap(n: int) -> void:
# 轮末停手转存决策(round_end 发射点调用——编排器授予段尾):transfer=true=剩余 AP 转蓄力层
# (上限 3,整备军备面);false=1:1 转存 RAP(缺省)。非停手轮(cleared/耗尽)_last_stop_ap=0
# 两分支均零操作。转蓄后达上限的沿发 charge_full(R6 框架先行——现无订阅者)。
func finish_round_stop(transfer: bool) -> void:
# (上限 3,整备军备面);false=按转存率转存 RAP(transfer_pct 百分比口径缺省 100=1:1 与旧逐位一致;
# 迭代 22 秘宝批 T3——储备罐 rap_transfer_mult magnitude 125=1.25:1,整数除法向下取整=每 4 AP 多转 1)。
# 非停手轮(cleared/耗尽)_last_stop_ap=0 两分支均零操作。转蓄后达上限的沿发 charge_full(R6 框架先行——现无订阅者)。
func finish_round_stop(transfer: bool, transfer_pct: int = 100) -> void:
if transfer:
var prev_layers := _charge_layers
_charge_layers = mini(3, _charge_layers + _last_stop_ap)
if _fx != null and _charge_layers >= 3 and prev_layers < 3:
_fx.dispatch("charge_full", {"layers": _charge_layers})
else:
_rap += _last_stop_ap
_rap += _last_stop_ap * maxi(1, transfer_pct) / 100
_last_stop_ap = 0
+17 -175
View File
@@ -43,7 +43,6 @@ extends RefCounted
signal level_started(level_index: int, display_name: String) # 每关开始(含冒险首关)——呈现层重取 runner()、刷新图条与层名行
signal curio_choice_presented(payload: Dictionary) # 专长三选一停点(非末关通关恰一次;迭代 20 T5 换代正名——旧「提升三选一」退役,通道白名单后候选恒专长+回血在载荷)——{level_index, candidates, heal_gained, hp_before, hp_after, keystone_gained, keystone_total, gp, aux_cleared}
signal curio_resolved(result: Dictionary) # 三选一兑付完成(pick_curio/替换受理后恰一次)——{picked, value, hp_before, hp_after, level_index}
signal treasure_replace_presented(items: Array, incoming: Dictionary) # 同调位满位替换子交互(pick_curio 道具件满位时)——items=现有持有件记录、incoming=新件记录
signal adventure_finished(summary: Dictionary) # 冒险结算(completed/failed/abandoned)——summary 五件事:打穿没有/通关几关/止步第几关/钥石合计/主种子(+additive keystone_drops 迭代 10)
# ---- 转发族(签名与 LevelRunner 同款——呈现层对接透明;ADR-0001 S6 经 _forward_* 方法)----
signal node_entered(node: Dictionary, info: Dictionary)
@@ -77,6 +76,7 @@ var _attune_refresh_x := 0 # 刷新件余量(迭代 11 第①
var _attune_refresh_y := 0 # Y 冒险周期池(开局参数面注入,用完即无——营地升级行表值)
var _curio_paid_refreshes := 0 # 本次停点已付费刷新次数(2026-10-06 用户令对齐商店 MC-1——GP 刷新价曲线输入,随停点创建清零=与商店进店清零同构)
var attune_charges := {} # 同调主动件冒险充能账本(迭代 11 第①批 T6——跨关卡共享 dict;settlement use_treasure 扣减直改同引用;断点随 attunement_charges 键入档)
var vision_counter := {"used": 0} # 真视类工具使用计数(迭代 22 秘宝批 T3——跨关卡共享 dict;编排器受理成功 +1;断点随 vision_counter_used 键入档,收束聚合入档 save.add_vision_used)
var adventure_counters := {} # adventure 域计数器账本(迭代 20 T6——攒层/付费计数等跨战持久件账;效果引擎 rebuild 回灌/战末导出回写共享引用;断点随 adventure_counters 键入档)
# ---- 凭证件三本新账(迭代 20 T4——奇物.md 凭证件表新 5 件;F-1 刷新写错账修复承载面)----
var _refresh_stock := 0 # 刷新存量账(冒险级——刷新券加仓;**不经 _attune_refresh_x**:X 每停点重置会吞存量=旧写错账缺陷 F-1)
@@ -93,9 +93,7 @@ var _feats: Array[String] = [] # 特性持有集(迭代 12 T1——
var _tree_rules: Array = [] # 树 rule 键集(迭代 12 T10——Fork 激活侧 effects 透传;下传 enc.tree_rules 消费面)
var _curio_pending := {} # 停点态:{"candidates": Array, "payload": Dictionary, "chosen"/"incoming": 替换在途}——空 = 无停点
var _curio_result := {} # 已兑付待推进态(curio_resolved 载荷副本——中断续玩不重兑付)
var _bonus_treasure_carry: Array[String] = [] # 道具件跨关携带(兑付后注入下一关库存——清库后授予的体验窗)
var _keystone_drops: Array = [] # Boss 逐杀掉落明细(additive 摘要键 keystone_drops——结算明细/复盘归因数据源)
var _last_draw_treasure: Array[String] = [] # 最近一次抽签掷定的道具件(掷定前置——卡片直显具体道具;追加批二)
var _node_curio_pending := {} # 奖励节点三选一停点态(迭代 10 追加批——共享池;断点不载:续玩重建到选路停点重走,恢复的计数器保证同候选重抽)
var _feat_ticket_pity := 0 # 门票保底计数(T5——展示次数:节点开面/货架抽签/刷新重展各计一次;触发清零)
var _feat_ticket_sold := 0 # 门票限购计数(T5——选中/买下即 +1;拿到即开不进持有集)
@@ -136,6 +134,13 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive(迭代 11 第①批 T5)
_attune_refresh_y = maxi(0, int(params.get("attune_refresh_y", 0))) # additive(迭代 11 第①批 T5b——Y 冒险周期池)
_seed_attune_charges() # 迭代 11 第①批 T6——冒险开始充能账本按装备主动件重置(每冒险充能语义)
vision_counter = {"used": 0} # 迭代 22 秘宝批 T3——真视使用计数冒险开始归零(跨局累计归生涯档)
# 刷新练习开局注入(迭代 22 秘宝批 T3——refresh_stock_grant 装配即得一次性加仓:
# 装配集 kind 分发扫描;只入存量账 _refresh_stock、临时账不动=V8 契约;resume 不重注入〔存量随断点已在〕)
for iid_v2 in _attunement:
var rp_row: Dictionary = db.treasure_item_row(String(iid_v2))
if (rp_row.get("effect_kinds", []) as Array).has("refresh_stock_grant"):
_refresh_stock += int(rp_row.get("magnitude", 0))
# 特性开局注入(迭代 12 T7——MC 成型画像载体:params.relics_seed 非空=开局持有集〔七流派
# 各 2 件档一成型——测量面专用;真实局规则改写型特性只走局内随机渠道,红线②层级纪律不破〕)
var feat_seed: Array = params.get("relics_seed", []) if params.get("relics_seed", []) is Array else []
@@ -204,8 +209,6 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
_feats = []
for r in (bp.get("relics", []) as Array):
_feats.append(String(r))
for v in (bp.get("bonus_aux_carry", []) as Array):
_bonus_treasure_carry.append(String(v))
_keystone_drops = (bp.get("keystone_drops", []) as Array).duplicate(true)
_base_override = _normalize_override(params.get("hero_override", {}))
# 生命账本越界钳制(T17——spawn 守卫 EC16 免疫):断点可能由旧口径版本写出(如调试上限
@@ -231,6 +234,7 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
if bp_counters is Dictionary:
adventure_counters = (bp_counters as Dictionary).duplicate(true)
_refresh_stock = maxi(0, int(bp.get("refresh_stock", 0))) # 迭代 20 T4/T7——旧档缺键缺省 0
vision_counter = {"used": maxi(0, int(bp.get("vision_counter_used", 0)))} # 迭代 22 秘宝批 T3——真视计数随断点回填(缺键=旧断点缺省 0)
_battery_pending = maxi(0, int(bp.get("battery_pending", 0)))
var bp_result: Dictionary = bp.get("bonus_result", {}) as Dictionary
if not bp_pending.is_empty():
@@ -247,19 +251,7 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
else:
migrated.append({"kind": String(c), "tier": db.adventure.TIER_COMMON})
_curio_pending = {"candidates": migrated}
if bp_pending.has("aux_detail"):
_curio_pending["aux_detail"] = (bp_pending.get("aux_detail", []) as Array).duplicate(true) # 掷定前置随停点快照续接
var payload: Dictionary = (bp_pending.get("payload", {}) as Dictionary).duplicate(true)
if bp_pending.has("incoming"):
_curio_pending["chosen"] = "treasure_item"
var incoming: Array = (bp_pending.get("incoming", []) as Array).duplicate(true)
_curio_pending["incoming"] = incoming
state = "running"
curio_choice_presented.emit(payload)
# 替换在途续发(呈现层直落替换子态——持有件记录按恢复后 carry 重建)
treasure_replace_presented.emit(_carry_records(),
{"index": -1, "item_id": String(incoming[0]) if not incoming.is_empty() else "", "count": incoming.size()})
return true
state = "running"
curio_choice_presented.emit(payload)
return true
@@ -286,7 +278,6 @@ func _reset_curio_state() -> void:
_feats = []
_curio_pending = {}
_curio_result = {}
_bonus_treasure_carry = []
_keystone_drops = []
_node_curio_pending = {}
adventure_counters = {}
@@ -411,8 +402,6 @@ func pick_curio(kind: String, tier: String = "", feat_id: String = "") -> bool:
return _redeem_feat_ticket() # T5 拿到即开——停点候选替换为专长三选一(零摘牌/不进持有集)
var value: int = int(db.adventure.curio_value(kind, tier))
var hp_before := int(ledger.get("hero_hp", 0))
if kind == "treasure_item":
return _pick_treasure_item(value) # 道具件分派通关渠道(持有容器 = 跨关携带——与节点渠道分轨;池种退役后不可达)
if not _apply_curio_pick(kind, tier, value, feat_id):
push_error("[冒险结构] pick_curio 拒绝——未知件: %s|%s|%s" % [kind, tier, feat_id])
return false
@@ -540,44 +529,6 @@ func _pool_stock_snapshot() -> Dictionary:
return out
# 道具件兑付(同调位约束):有位直接入跨关携带;满位转替换子交互(incoming 暂存、等 resolve_treasure_replace)。
# 掷定前置(追加批二):具体道具随候选抽定(_last_draw_treasure / _curio_pending.aux_detail)——本函数消费预掷值,
# 断点续玩/无预掷值时回退当场掷定(流序不同但消费数不变,确定性由调用序保证)。
# 持有容器 = 跨关携带 _bonus_treasure_carry(与节点渠道的关卡域库存分轨——容器差异保守不合,注记在案)。
func _pick_treasure_item(count: int) -> bool:
var picked: Array = _draw_pending_treasure(_curio_pending, count, true)
if picked.is_empty():
push_error("[冒险结构] pick_curio(treasure_item) 拒绝——解锁道具池空集(白板/门控态不入牌库的防御兜底)")
return false
var hp_before := int(ledger.get("hero_hp", 0))
# 迭代 11 第①批 T7——同调位守卫随过渡键退役拆除(aux 卡出池后本路径不可达;能力件容量闸
# 正主 = 营地装配面 Attunement.slots 满位禁选)
for iid in picked:
_bonus_treasure_carry.append(String(iid))
_finish_curio_pick("treasure_item", picked.size(), hp_before)
return true
# 替换子交互受理(discard_index = -1 放弃新件不获取;否则丢该件腾位收新)——终态恒 ≤ 同调位
# 持有容器 = 跨关携带(序号寻址);与节点渠道(库存句柄寻址)的丢弃/授予/收尾三步保守不合。
func resolve_treasure_replace(discard_index: int) -> bool:
if not _replace_in_transit(_curio_pending):
push_error("[冒险结构] resolve_treasure_replace 拒绝——无替换在途")
return false
var incoming: Array = _curio_pending.get("incoming", []) as Array
var hp_before := int(ledger.get("hero_hp", 0))
if discard_index >= 0:
if discard_index >= _bonus_treasure_carry.size():
push_error("[冒险结构] resolve_treasure_replace 拒绝——序号不在持有集: %d" % discard_index)
return false
_bonus_treasure_carry.remove_at(discard_index)
for iid in incoming:
_bonus_treasure_carry.append(String(iid))
# 放弃新件(discard_index < 0)= 原持有集不动、新件不获取
_finish_curio_pick("treasure_item", incoming.size(), hp_before)
return true
# 兑付收口(恰一次):清停点态 → 记已兑付态 → emit → 停点快照(等继续确认——中断续玩不重兑付)。
# picked = kind;tier 随载荷(TP0——大小档位读数面);relic 伴随键随载荷(迭代 12 T1——反馈行取数)
func _finish_curio_pick(picked: String, value: int, hp_before: int, tier: String = "", feat_id: String = "") -> void:
@@ -599,16 +550,15 @@ func _finish_curio_pick(picked: String, value: int, hp_before: int, tier: String
# 候选呈现(节点到达时调用——恰一 reward_pick scope 洗牌 + 道具件掷定 scope;本停点断点不载,
# 续玩重建到选路停点后重走重抽,恢复的全局计数器保证同候选逐位复现)。
# 掷定前置(追加批二):返回 dict 带 aux_detail(道具件具体道具)供卡片直显。
func node_curio_candidates() -> Dictionary:
var cands: Array = _draw_curio_candidates("reward_node")
return {"candidates": cands, "aux_detail": _last_draw_treasure.duplicate()}
return {"candidates": cands}
func present_node_curio(candidates: Array, treasure_detail: Array = []) -> void:
func present_node_curio(candidates: Array) -> void:
_reset_refresh_x() # 迭代 11 第①批 T5b——碰三选一重置 X 余量(不结转)
_curio_paid_refreshes = 0 # 付费刷计数随停点清零(对齐商店进店清零——2026-10-06)
_node_curio_pending = {"candidates": candidates.duplicate(true), "aux_detail": treasure_detail.duplicate(true)}
_node_curio_pending = {"candidates": candidates.duplicate(true)}
# 点选兑付(节点域——无部分回血无钥石;令牌消费与牌库摘牌与通关三选一同库;relic 同权入持有集)
@@ -623,8 +573,6 @@ func apply_node_curio(kind: String, tier: String = "", feat_id: String = "") ->
push_error("[冒险结构] apply_node_curio 拒绝——令牌不在候选: %s|%s|%s" % [kind, tier, feat_id])
return false
var value: int = int(db.adventure.curio_value(kind, tier))
if kind == "treasure_item":
return _apply_node_treasure(value) # 道具件分派节点渠道(持有容器 = 关卡域库存——与通关渠道分轨;池种退役后不可达)
if not _apply_curio_pick(kind, tier, value, feat_id):
push_error("[冒险结构] apply_node_curio 拒绝——未知件: %s|%s|%s" % [kind, tier, feat_id])
return false
@@ -632,46 +580,6 @@ func apply_node_curio(kind: String, tier: String = "", feat_id: String = "") ->
return true
# 节点渠道道具件(同调位约束):持有=关卡域库存+跨关携带,有位直接入本关库存(当关可用);
# 满位转替换子交互(丢弃目标=库存件 handle——与通关渠道的 carry 序号分轨,容器语义保守不合)。
# 掷定前置:消费 _node_curio_pending.aux_detail 预掷值(缺省回退当场掷定)。
func _apply_node_treasure(count: int) -> bool:
var picked: Array = _draw_pending_treasure(_node_curio_pending, count, false)
if picked.is_empty():
push_error("[冒险结构] apply_node_curio(treasure_item) 拒绝——解锁道具池空集(白板/门控态不入牌库的防御兜底)")
return false
var records: Array = []
if _runner != null and _runner.treasure_inventory != null:
records = _runner.treasure_inventory.holding_records()
# 迭代 11 第①批 T7——守卫随过渡键退役拆除(CURIO_KINDS 已除名 treasure_item,本分支不可达;
# 替换子交互路由随容量闸一并退役——能力件容量闸正主 = 营地装配面满位禁选)
if _runner != null:
for iid in picked:
var rec: Dictionary = _runner.grant_treasure_item(String(iid), "reward_node")
if rec.is_empty():
push_warning("[冒险结构] 奖励节点道具授予被拒(防御兜底留痕): %s" % String(iid))
_finish_node_bonus("treasure_item", picked.size())
return true
func _resolve_node_replace(discard_handle: int) -> bool:
if not _replace_in_transit(_node_curio_pending):
push_error("[冒险结构] resolve_node_replace 拒绝——无替换在途")
return false
var incoming: Array = _node_curio_pending.get("incoming", []) as Array
if discard_handle >= 0:
if _runner == null or _runner.treasure_inventory == null or not _runner.treasure_inventory.discard_item(discard_handle):
push_error("[冒险结构] resolve_node_replace 拒绝——句柄不在持有集: %d" % discard_handle)
return false
for iid in incoming:
var rec: Dictionary = _runner.grant_treasure_item(String(iid), "reward_node")
if rec.is_empty():
push_warning("[冒险结构] 替换子道具授予被拒(防御兜底留痕): %s" % String(iid))
# 放弃新件(discard_handle < 0)= 原持有集不动、新件不获取
_finish_node_bonus("treasure_item", incoming.size())
return true
# 冒险开始充能账本重置(T6——装备 active_charged 件各回 charges_per_adventure;被动件不入账)
func _seed_attune_charges() -> void:
attune_charges = {}
@@ -733,11 +641,10 @@ func battery_pending() -> int:
return _battery_pending
# 节点停点在途态读回(组合根重 present 用——candidates/aux_detail 值拷贝)
# 节点停点在途态读回(组合根重 present 用——candidates 值拷贝)
func node_curio_pending_state() -> Dictionary:
return {
"candidates": (_node_curio_pending.get("candidates", []) as Array).duplicate(true),
"aux_detail": (_node_curio_pending.get("aux_detail", []) as Array).duplicate(true),
}
@@ -783,10 +690,8 @@ func _refresh_curio_common(paid: bool) -> bool:
if not _curio_pending.is_empty():
var cands: Array = _draw_curio_candidates("level_clear")
_curio_pending["candidates"] = cands.duplicate(true)
_curio_pending["aux_detail"] = _last_draw_treasure.duplicate()
var payload: Dictionary = (_curio_pending.get("payload", {}) as Dictionary).duplicate(true)
payload["candidates"] = cands
payload["aux_detail"] = _last_draw_treasure.duplicate()
payload["refreshed"] = true # 重发载荷带标——呈现层直落选择面,不再重开胜利总结面(2026-10-06)
curio_choice_presented.emit(payload) # 同信号重 present(零自动推进契约不破——停点仍在途)
_snapshot_breakpoint()
@@ -794,44 +699,10 @@ func _refresh_curio_common(paid: bool) -> bool:
if not _node_curio_pending.is_empty():
var cands2: Array = _draw_curio_candidates("reward_node")
_node_curio_pending["candidates"] = cands2.duplicate(true)
_node_curio_pending["aux_detail"] = _last_draw_treasure.duplicate()
return true
return false
# 替换在途守卫(2026-10-02 修复批——双渠道替换受理的共用前置谓词:停点态非空且 chosen=treasure_item)
func _replace_in_transit(pending: Dictionary) -> bool:
return not pending.is_empty() and String(pending.get("chosen", "")) == "treasure_item"
# 双渠道道具件掷定共享体(2026-10-02 修复批——_pick_treasure_item/_apply_node_treasure 复制债收口一步):
# 解锁道具池空集守卫 + N 件掷定(先消费停点预掷 aux_detail,不足部分当场经 reward_pick 封闭流补掷——
# 取数序与拆分前逐位一致)。返回空数组 = 池空集(调用方按渠道名报错)。
# 保守不合处:通关渠道允许回退 _last_draw_treasure(allow_detail_fallback)、节点渠道不回退——掷定回退
# 语义渠道各异,参数化保留差异;持有容器与兑付收尾仍归调用方渠道。
func _draw_pending_treasure(pending: Dictionary, count: int, allow_detail_fallback: bool) -> Array:
var pool: Array = _bonus_treasure_pool()
if pool.is_empty():
return []
var picked: Array = []
var detail: Array = (pending.get("aux_detail", []) as Array).duplicate()
if detail.is_empty() and allow_detail_fallback:
detail = _last_draw_treasure.duplicate()
for i in maxi(1, count):
if i < detail.size():
picked.append(String(detail[i]))
else:
picked.append(_roll_bonus_treasure_item(pool))
return picked
# 替换受理分发(单入口——节点停点在途优先按库存句柄,否则按通关停点的携带序号)
func resolve_curio_replace(discard: int) -> bool:
if not _node_curio_pending.is_empty() and _node_curio_pending.has("incoming"):
return _resolve_node_replace(discard)
return resolve_treasure_replace(discard)
func _finish_node_bonus(picked: String, value: int, tier: String = "", feat_id: String = "") -> void:
_node_curio_pending = {}
var payload := {
@@ -849,8 +720,7 @@ func _finish_node_bonus(picked: String, value: int, tier: String = "", feat_id:
# 一枚**〔先到先得,档位取该 kind 首枚——「三选一=三种不同选项」体验延续,与货架抽签同规则〕;
# kind 出现率∝剩余令牌数〔档位配额即分布——无独立权重 knob〕;抽签不摘牌、兑付才摘牌=未选
# 自动回池;池耗即枯竭——余库不足 N kind 时出齐剩余,空库 = 零候选合法态)。候选 = 令牌字典
# {"kind","tier"}。消费 reward_pick 封闭流——一次停点恰一 scope,同种子同候选;道具件掷定前置
# 随池种退役拆除(_last_draw_treasure 恒空集,机械惰性承载)。
# {"kind","tier"}。消费 reward_pick 封闭流——一次停点恰一 scope,同种子同候选。
# 渠道白名单过滤(迭代 15 卡② T4——渠道轴定位:关底 Boss 击败=通关渠道只出专长(feat 令牌);
# 奖励节点/商店只出属性值提升+资源(非 relic 令牌),专长在非 Boss 渠道只以门票形态出现(T5)。
# 过滤式而非物理拆池:deck 物理结构/存档键 bonus_deck 混编余库/消耗语义(选中踢池未选回池)原样,
@@ -860,7 +730,6 @@ func _finish_node_bonus(picked: String, value: int, tier: String = "", feat_id:
# 〔独立计数体系〕;已拿上限 cap(案②提前退役):taken≥cap 的身份在候选视图剔除〔牌库本体不动,
# 拿了才计、刷出不计——_curio_cap_ok〕。
func _draw_curio_candidates(channel: String = "reward_node") -> Array:
_last_draw_treasure = []
var pool := _curio_deck.duplicate()
if channel == "level_clear":
pool = pool.filter(func(tok): return String((tok as Dictionary).get("kind", "")) == "relic")
@@ -998,33 +867,6 @@ func _redeem_feat_ticket() -> bool:
return true
func _roll_from(rg: RandomNumberGenerator) -> String:
var pool: Array = _bonus_treasure_pool()
return String(pool[rg.randi_range(0, pool.size() - 1)])
# 三选一道具件随机池(与投放门控同源:aux 活行 ∩ 解锁门控集——空集 = 道具件不入牌库)
func _bonus_treasure_pool() -> Array:
var out: Array = []
var live: Dictionary = db.treasure_live_ids()
for iid in live.keys():
if _treasure_unlocked.has(String(iid)):
out.append(String(iid))
return out
func _roll_bonus_treasure_item(pool: Array) -> String:
var rg: RandomNumberGenerator = rng.open_stream("reward_pick")
return String(pool[rg.randi_range(0, pool.size() - 1)])
func _carry_records() -> Array:
var out: Array = []
for i in _bonus_treasure_carry.size():
out.append({"index": i, "item_id": _bonus_treasure_carry[i]})
return out
# Boss 钥石逐杀掉落(迭代 10——渠道改 Boss/精英击杀掉落;首杀/复杀两档读 meta.tres boss_keystone;
# 首杀判据 = 存档 boss_first_kill 标记位〔断点未注入 = 恒首杀——模拟/冒烟零扰动;标记仍在收束
# apply_adventure_summary 逐通关关次置位〕;末关 Boss 照常入账;明细随摘要 additive 键供结算面归因)
@@ -1091,6 +933,7 @@ func _start_current_level() -> bool:
if ok:
_runner.set_attunement(_attunement) # 迭代 11 第①批 T5——同调装配下传(enc 携带进战斗结算)
_runner.set_attune_charges(attune_charges) # 迭代 11 第①批 T6——充能账本下传(能力件骑库存面注入)
_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 面消费)
if _shop_mode == "pool":
@@ -1148,7 +991,6 @@ func _on_level_finished(payload: Dictionary) -> void:
var pick_payload: Dictionary = {
"level_index": level_index,
"candidates": _draw_curio_candidates("level_clear"), # T4 渠道白名单——关底 Boss 击败=通关渠道只出专长
"aux_detail": _last_draw_treasure.duplicate(), # 掷定前置(追加批二——道具件具体道具随候选抽定,卡片直显)
"heal_gained": heal,
"hp_before": hp_before,
"hp_after": int(ledger.get("hero_hp", 0)),
@@ -1158,7 +1000,6 @@ func _on_level_finished(payload: Dictionary) -> void:
"aux_cleared": int(payload.get("aux_cleared", 0)), # additive 扩签(迭代 7 T4——关卡末清空反馈取数)
}
_curio_pending = {"candidates": (pick_payload["candidates"] as Array).duplicate(true),
"aux_detail": _last_draw_treasure.duplicate(),
"payload": pick_payload.duplicate(true)}
curio_choice_presented.emit(pick_payload)
_snapshot_breakpoint() # 选择停点可中断(契约 5——续玩不重抽不静默丢失)
@@ -1178,6 +1019,7 @@ func _settle(result: String) -> void:
"master_seed": master_seed,
"aux_cleared_last": _last_treasure_cleared, # additive 扩签(迭代 7 T4——末关清空反馈;非末关走 bonus 载荷)
"keystone_drops": _keystone_drops.duplicate(true), # additive 扩签(迭代 10——逐杀掉落明细;settle 入账明细/复盘归因数据源)
"vision_used_delta": int(vision_counter.get("used", 0)), # additive 扩签(迭代 22 秘宝批 T3——真视使用本局计数;settle 聚合入档)
"pool_stock": _pool_stock_snapshot(), # additive 扩签(迭代 11 第②批 T5——终局池读数:清账前快照,商店买走 vs 奖励领走 vs 枯竭的终局面)
"relics": _feats.duplicate(), # additive 扩签(迭代 12 T1——特性终局持有集:清账前快照,成型判读取数面)
}
@@ -1252,10 +1094,10 @@ func _snapshot_breakpoint() -> void:
"feat_ticket_pity": _feat_ticket_pity, # T5 门票保底计数(扩列向后兼容——旧档缺键缺省 0)
"feat_ticket_sold": _feat_ticket_sold, # T5 门票限购计数(同上)
"adventure_counters": adventure_counters.duplicate(true), # 迭代 20 T6/T7——adventure 域计数器随断点(续玩跨战账不丢)
"vision_counter_used": int(vision_counter.get("used", 0)), # 迭代 22 秘宝批 T3——真视使用计数随断点(续玩跨战账不丢)
"refresh_stock": _refresh_stock, # 迭代 20 T4/T7——刷新存量账随断点(F-1 新账)
"battery_pending": _battery_pending, # 迭代 20 T4/T7——电池待分配充能随断点
"bonus_result": _curio_result.duplicate(true), # 已兑付待推进态(空 = 无——中断续玩不重兑付)
"bonus_aux_carry": _bonus_treasure_carry.duplicate(true), # 道具件跨关携带(注入前中断防静默丢失)
"keystone_drops": _keystone_drops.duplicate(true), # 逐杀掉落明细(摘要归因续接——迭代 10)
}
_save_manager.set_breakpoint(bp)
+162 -15
View File
@@ -77,10 +77,11 @@ var _flag_consumed := {} # 兑付资格以雷格实例为键
# 同调装配运行态(迭代 11 第①批 T5——begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动)
var attunement: Array = [] # 装备中同调件 item_id 集
var _attune_charges_ref: Dictionary = {} # 充能账本共享引用(迭代 11 第①批 T6——use_treasure 消费直改)
var _attune_safe_seen := 0 # 掘进蓄能 battle 域安全开格计数(进度呈现取数面;积点账本=效果引擎计数器 dig_charge_bank——行 triggers counter_spend 订阅同账本)
var _attune_safe_seen := 0 # 积点制件 battle 域安全开格计数(迭代 22 T3 通用化——装配集 ap_charge_safe 行扫描;积点账本=行 triggers counter_spend 声明的计数器,旧 dig_charge_bank 硬编码随行退役)
# ---- 效果引擎扩签运行态(迭代 20 专长实装批 T1——新效果类型施加面;空订阅零扰动)----
var _fx_hero_ac_base := -1 # 主角 AC 基线快照(enc.hero_armor_class;-1=表值兜底未解析)
var _fx_ac_round := 0 # ac_bonus 本轮累积器(封印桩形态——round_start 清零)
var nest_preview_count := -1 # 巢穴预告读数口(迭代 22 秘宝批 T3——board_installed 命中落值;-1=未装配/未发射;呈现桥消费)
var _fx_ac_battle := 0 # ac_bonus 本战累积器(石肤形态——cap 封顶,begin_battle 清零)
var _fx_adventure_counters_ref: Dictionary = {} # adventure 域计数器共享引用(T6——attune_charges 先例:begin_battle 回灌 rebuild、battle 末 counters_export 回写)
var _fx_bonus_ref: Dictionary = {} # 冒险加算区共享引用(bonus_delta 战末汇入——血税双向账)
@@ -203,7 +204,7 @@ func _attune_has(item_id: String) -> bool:
return attunement.has(item_id)
# 装配集只读出口(迭代 19 卡④——呈现桥消费面:组合根每战装配链判「装配集含 fog_lantern」→
# 装配集只读出口(迭代 19 卡④立——呈现桥消费面现归 attune_param_sum〔迭代 22 T4 换代〕;本口保留
# board_view.set_fog_lantern。_attune_has 契约化出口,save v6 只读访问器先例;private 面内部调用
# 零改动。只读零副作用——呈现侧判定专用,非结算写入口)
func attune_has(item_id: String) -> bool:
@@ -216,6 +217,98 @@ func _attune_row(item_id: String) -> Dictionary:
return db.treasure_item_row(item_id) if _attune_has(item_id) else {}
# 装配集参数合计(迭代 22 秘宝批 T3——常时参数面 kind 的装配集 Σmagnitude 读数口;
# kind 分发查询先例=_attune_refresh_x_total bonus_refresh。零装配=0)
func attune_param_sum(kind: String) -> int:
var total := 0
for iid_v in attunement:
var arow: Dictionary = db.treasure_item_row(String(iid_v))
if (arow.get("effect_kinds", []) as Array).has(kind):
total += int(arow.get("magnitude", 0))
return total
# 装配集内 ap_charge_safe 行读取(迭代 22 秘宝批 T3——掘进蓄能 id 特判通用化取数源;
# 首行语义=装配集内至多一件积点制〔新目录恰试炼镐〕)
func _attune_charge_safe_row() -> Dictionary:
for iid_v in attunement:
var arow: Dictionary = db.treasure_item_row(String(iid_v))
if (arow.get("effect_kinds", []) as Array).has("ap_charge_safe"):
return arow
return {}
# 充能重置(迭代 22 秘宝批 T3——按周期回满:账本回 charges_per_adventure+骑库存差额补注入
# (used handle 不回收=本轮已耗充能作废,补新 handle=「回满」;差额注入防双份=level_runner
# set_attune_charges 同构)。reset ∈ {battle, level}——adventure 周期归冒险开始种子面)
func _reseed_attune_charges(reset: String) -> void:
for iid_v in attunement:
var iid := String(iid_v)
var arow: Dictionary = db.treasure_item_row(iid)
if String(arow.get("form", "")) != "active_charged":
continue
if String(arow.get("charge_reset", "adventure")) != reset:
continue
var want := int(arow.get("charges_per_adventure", 0))
if want < 1:
continue # charges=0=积点制(试炼镐——不走骑库存面,受理=use_attune_convert 积点口)
if _attune_charges_ref != null:
_attune_charges_ref[iid] = want
if treasure_inventory != null:
var existing := 0
for r in treasure_inventory.snapshot_inventory():
if String(r.get("item_id", "")) == iid and String(r.get("source_class", "")) == "attunement" and not bool(r.get("used", false)):
existing += 1
for i in range(maxi(0, want - existing)):
treasure_inventory.add_item(iid, "attunement", Vector2i(-1, -1), 0)
# 积点制主动件受理(迭代 22 秘宝批 T3——试炼镐「转化获取」唯一入口:不经库存 handle
# 〔charges=0 零骑库存〕,受理闸=行 triggers counter_spend 积点账〔dig_reveal 通用化计数段
# 逐 charge_n 积 1 点〕;消耗成功才兑付当轮 AP=magnitude。返回 use_treasure 同构 verdict)
func use_attune_convert(item_id: String) -> Dictionary:
var arow: Dictionary = db.treasure_item_row(item_id)
if arow.is_empty() or String(arow.get("form", "")) != "active_charged" \
or not (arow.get("effect_kinds", []) as Array).has("ap_charge_safe") \
or not _attune_has(item_id):
return {"ok": false, "reason": "非装配积点件"}
if action_economy.phase != action_economy.PHASE_ACTIVE:
return {"ok": false, "reason": "非挖掘期(使用窗口=Active)"}
var bank_id := ""
for tr_v in (arow.get("triggers", []) as Array):
var dd: Dictionary = (tr_v as Dictionary).get("do", {}) as Dictionary
if String(dd.get("kind", "")) == "counter_spend":
bank_id = String(dd.get("counter_id", ""))
break
if bank_id == "":
return {"ok": false, "reason": "行缺积点受理闸"}
if not _fx.counter_spend(bank_id, 1):
var cn := int(arow.get("charge_n", 0))
return {"ok": false, "reason": "%s:安全格积点未达(%d/%d)" % [String(arow.get("display_name", "")), _attune_safe_seen % cn if cn > 0 else 0, cn]}
if not action_economy.grant_ap(int(arow.get("magnitude", 0))):
return {"ok": false, "reason": "行动点加量被拒(相位防御)"}
# 兑付反馈=AP 条即时刷新(use_treasure 同款纪律——结算明细无道具行:效果即时可见零明细行)
return {"ok": true, "item_id": item_id}
# 积点制件受理状态读数(迭代 22 秘宝批 T4——HUD 兑换钮态数据源:assembled=装配在册/
# points=积点账现值/need=受理闸阈值〔行 counter_spend value〕。未装配=assembled false)
func attune_convert_state() -> Dictionary:
var row := _attune_charge_safe_row()
if row.is_empty() or String(row.get("form", "")) != "active_charged":
return {"assembled": false, "points": 0, "need": 0, "display_name": ""}
var bank := ""
var need := 1
for tr_v in (row.get("triggers", []) as Array):
var dd: Dictionary = (tr_v as Dictionary).get("do", {}) as Dictionary
if String(dd.get("kind", "")) == "counter_spend":
bank = String(dd.get("counter_id", ""))
need = maxi(1, int(dd.get("value", 1)))
break
return {"assembled": true, "points": _fx.counter(bank) if bank != "" else 0, "need": need,
"display_name": String(row.get("display_name", ""))}
# 特性求值(内容数据驱动扩展批 T3——_feat_flags_for 挂点求值退役:效果行为=数据行 triggers 订阅,
# 效果引擎 dispatch 分发;旧 23 hook 枚举与旗注入消亡。流派档位质变=FeatsEngine 纯函数求值
# (档位机制本身系引擎基建非内容效果),两运行态注伤害控制器:档一 rider+1 / 档二 rider 可暴击)
@@ -344,6 +437,24 @@ func begin_battle(enc: Dictionary) -> bool:
# 迭代 20 T6——adventure 域计数器从共享引用回灌〔跨战持久〕)
_fx.rebuild(feat_ids, attunement, tree_rules)
_fx.counters_seed(_fx_adventure_counters_ref)
# 秘宝常时参数注入(迭代 22 秘宝批 T3——装配集 kind 分发:远程镐挖距放宽/信标网络旗可达豁免;
# 缺省 0/false=零装配零扰动。注入先于首板 install 已不成立〔板在早段〕——本批板生成不读两参数,
# 参数只影响挖掘/插棋动作判据,时序安全)
minesweeper.dig_range_bonus = attune_param_sum("dig_range_bonus")
minesweeper.flag_range_free = attune_param_sum("flag_range_free") > 0
# 充能重置周期(迭代 22 秘宝批 T3——battle=每战回满/level=每关首战回满〔battle_index==1〕;
# adventure 周期维持冒险开始 _seed_attune_charges 单次)
_reseed_attune_charges("battle")
if int(enc.get("battle_index", 1)) == 1:
_reseed_attune_charges("level")
# board_installed 发射+施加(迭代 22 秘宝批 T3——巢穴预告:装板后一次报本板危险格数量;
# 只报数量不标位置=信息红线,不碰分布=棋盘事实不可变契约 3。命中=读数口落值,呈现归
# 呈现桥消费(无格来源不进结算明细行——行来源纪律);板已就绪〔早段 install〕、订阅行已在 rebuild 装载)
nest_preview_count = -1
var bi_hits := _fx_emit("board_installed", {})
for h in bi_hits:
if String(h.get("kind", "")) == "mine_count_preview":
nest_preview_count = minesweeper.mine_total()
# 迭代 10 奖励重构批——跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现
# 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后
# 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节)
@@ -609,15 +720,23 @@ func _settle() -> void:
"奥术矢", "board", int((spell_hits[0] as Dictionary).get("value", 0)),
DamageController.TARGET_FRONT_ALIVE, ri, evt.cell, "magic")
_submit(spell_t, false)
# 掘进蓄能计数(迭代 11 同调批 T6 → 内容数据驱动扩展批 T4:积点账本迁效果引擎计数器
# dig_charge_bank——行 triggers counter_spend(treasure_used) 受理闸同账本。模判定留编排器侧:
# 「每 N 格积 1 点」在发射点施加架构下无法行内表达先增后判(when 于 dispatch 预评估),
# 逐位等价优先——词汇表边界注记随 commit 留痕;charge_n 读行零字面量)
if _attune_has("dig_charge"):
# 积点制计数(迭代 11 同调批 T6 → 迭代 22 秘宝批 T3 通用化:装配集 ap_charge_safe 行
# 〔新目录=试炼镐;旧 dig_charge id 特判随行退役摘除〕——模判定留编排器侧:「每 N 格积
# 1 点」在发射点施加架构下无法行内表达先增后判(when 于 dispatch 预评估),逐位等价优先;
# 积点账本 counter_id=行 triggers counter_spend 声明单点,charge_n 读行零字面量)
var cs_row: Dictionary = _attune_charge_safe_row()
if not cs_row.is_empty():
_attune_safe_seen += 1
var cn_b: int = int(db.treasure_item_row("dig_charge").get("charge_n", 999))
var cn_b: int = int(cs_row.get("charge_n", 999))
if cn_b > 0 and _attune_safe_seen % cn_b == 0:
_fx.counter_add("dig_charge_bank", 1)
var bank_b := ""
for tr_b in (cs_row.get("triggers", []) as Array):
var dd_b: Dictionary = (tr_b as Dictionary).get("do", {}) as Dictionary
if String(dd_b.get("kind", "")) == "counter_spend":
bank_b = String(dd_b.get("counter_id", ""))
break
if bank_b != "":
_fx.counter_add(bank_b, 1)
elif evt.kind == "mine":
# 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴),
# 标中不发(旗已随挖掘擦除、不入轮末判定基线)。
@@ -764,7 +883,11 @@ func _settle() -> void:
_emit_row("effect", "fx_grant_gp", Vector2i(-1, -1), ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": escrow}, "")
elif String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
action_economy.queue_ap_delta(int(h.get("value", 0)))
action_economy.finish_round_stop(_fx != null and _fx.has_effect_on("round_end", "grant_ap", "transfer"))
# 转存率入参(迭代 22 秘宝批 T3——储备罐 rap_transfer_mult magnitude=目标转存率百分比 125=1.25:1;
# 未装配=100 与旧 1:1 逐位一致)
var rap_pct: int = attune_param_sum("rap_transfer_mult")
action_economy.finish_round_stop(_fx != null and _fx.has_effect_on("round_end", "grant_ap", "transfer"),
rap_pct if rap_pct > 0 else 100)
if _facts_path == "cleared":
_grant_clear_reward(ri)
_flush_pending_reveals(ri) # 迭代 20 T1——揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入)
@@ -868,8 +991,10 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary:
if action_economy.phase != action_economy.PHASE_ACTIVE:
return {"ok": false, "reason": "非挖掘期(使用窗口=Active)"}
var board: MinesweeperRules = minesweeper # 恒等包装退役(2026-10-02 修复批——use_treasure 直引公有成员)
# 板维度读数(迭代 22 T3 修——Rules 无 width 代理〔潜伏缺陷:旧目录期零直呼覆盖掩盖〕,读 BoardData 本体)
var board_b: BoardData = board.current_board()
var v: Dictionary = treasure_inventory.validate_use(handle, target,
Vector2(board.width, board.height),
Vector2(board_b.width, board_b.height),
func(c: Vector2i): return not board.is_revealed(c))
if not bool(v.ok):
return v # ② 拒绝零消耗
@@ -879,10 +1004,13 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary:
for h in tu_hits:
if String(h.get("kind", "")) == "counter_spend":
if not _fx.counter_spend(String(h.get("counter_id", "")), int(h.get("value", 1))):
var cn_info: int = int(db.treasure_item_row("dig_charge").get("charge_n", 0))
return {"ok": false, "reason": "掘进蓄能:安全格积点未达(%d/%d)" % [_attune_safe_seen % cn_info if cn_info > 0 else 0, cn_info]}
var vrow: Dictionary = db.treasure_item_row(String(v.item_id))
var cn_info: int = int(vrow.get("charge_n", 0)) # 迭代 22 T3——读受理行自身(旧 dig_charge 硬编码随行退役)
return {"ok": false, "reason": "%s:安全格积点未达(%d/%d)" % [String(vrow.get("display_name", "")), _attune_safe_seen % cn_info if cn_info > 0 else 0, cn_info]}
var kinds: Array = v.kinds
if kinds.has("mine_reveal"):
if kinds.has("mine_reveal") or kinds.has("reveal_tiles"):
# 真视写入(迭代 22 T3——reveal_tiles 新真视 kind 与 mine_reveal 同叠加层通道:
# 危险格显示=信息呈现零回写,棋盘事实不可变契约 3)
treasure_inventory.apply_overlay("mine_reveal", v.resolved)
if kinds.has("reward_preview"):
treasure_inventory.apply_overlay("reward_preview", v.resolved)
@@ -890,9 +1018,22 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary:
if not action_economy.grant_ap(int(v.magnitude)):
return {"ok": false, "reason": "行动点加量被拒(相位防御)"}
if kinds.has("ap_charge_safe"):
# 掘进蓄能兑付(迭代 11 同调批 T6——临时 AP 当轮生效,grant_ap 同 sapper_order 通道)
# 积点件兑付(迭代 11 同调批 T6——临时 AP 当轮生效;库存通道积点件已由 use_attune_convert
# 承载〔charges=0 零骑库存〕,本分支保留=charges>0 且挂 ap_charge_safe 的历史形态兼容)
if not action_economy.grant_ap(int(v.magnitude)):
return {"ok": false, "reason": "行动点加量被拒(相位防御)"}
if kinds.has("safe_dig"):
# 天眼安全格直开(迭代 22 秘宝批 T3——C5 裁全额兑付:fx_reveal 通道复用=免 AP 零相位、
# 连锁与格面资源与普通挖格同链;危险格仅真视显示不移除=契约 3;有旗格跳过=旗=玩家承诺不代开)
var opened_any := false
for c_v in v.resolved:
var c := Vector2i(c_v)
if board.has_flag(c):
continue
if board.fx_reveal(c):
opened_any = true
if not opened_any:
return {"ok": false, "reason": "天眼:窗口内安全格均已开(无可开格)"}
if kinds.has("cell_removal"):
# 爆破工兵包兑付(迭代 13 T8/D6——中立移除雷格:未揭示/无旗/是雷过滤归排雷玩法;
# P1 甲邻接数字冻结零重算、清图分母零涉(雷格不入分母);空集拒绝零消耗——
@@ -906,6 +1047,12 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary:
var used_iid: String = String(rec.get("item_id", ""))
if _attune_charges_ref.has(used_iid):
_attune_charges_ref[used_iid] = maxi(0, int(_attune_charges_ref[used_iid]) - 1)
# 真视类工具使用计数(迭代 22 秘宝批 T3——信标网络解锁行为条件:受理成功恰计一次;
# 共享 dict 引用下传〔attune_charges 同款〕,冒险收束聚合入档 save.add_vision_used)
if kinds.has("reveal_tiles") or kinds.has("safe_dig") or kinds.has("mine_reveal"):
var vc: Dictionary = encounter.get("vision_counter", {}) if encounter is Dictionary else {}
if vc is Dictionary and not vc.is_empty():
vc["used"] = int(vc.get("used", 0)) + 1
return {"ok": true, "item_id": v.item_id, "resolved": v.resolved, "record": rec}
+2 -2
View File
@@ -360,8 +360,8 @@ func meta_boss_keystone_row(level_index: int) -> Dictionary:
return {}
func meta_workshop_items() -> Array:
return _meta_rows("workshop_items")
# meta_workshop_items 访问器已删除(迭代 22 T2——工坊行族退役,解锁混合制挂 treasure_items 行内;
# 秘宝解锁投影面=MetaProgression.treasure_shop_status)
func meta_conversion_row(attr: String) -> Dictionary:
+12 -4
View File
@@ -19,10 +19,12 @@ extends RefCounted
## 事件词汇(迭代 20 专长实装批 T1 扩签——16→18:+dig_stop 停手挂点〔弃图兑换〕
## +battle_won 战斗胜利〔战地包扎〕;迭代 21 天赋树批 T3 扩签 18→19:+flag_missed 标错判定
## 〔危险直觉·守挂点——_judge_flag 去重闸后发射,契约 12 触发主语=当回合标错动作;
## 命名避让 _emit_row 在册 source_class 名 flag_miss〕;原发射点不动)
## 命名避让 _emit_row 在册 source_class 名 flag_miss〕;迭代 22 秘宝实装批 T3 扩签 19→20:
## +board_installed 装板完成〔巢穴预告——每板恰一次,战斗板生成/重建发射点〕;原发射点不动)
const EVENTS := ["dig_reveal", "dig_paid", "flag_hit", "mine_hit", "attack_commit", "clamp_check",
"armor_roll", "kill", "damage_taken", "enemy_hp_threshold", "round_start", "flag_settle",
"round_end", "reward_roll", "treasure_used", "charge_full", "dig_stop", "battle_won", "flag_missed"]
"round_end", "reward_roll", "treasure_used", "charge_full", "dig_stop", "battle_won", "flag_missed",
"board_installed"]
## 扫雷侧事件集(🔴 流派扫雷触发铁律新形态:每流派 ≥1 行订阅本集事件——schema 加载校验。
## 原 MINE_HOOKS 的同义迁移:挖掘/标旗/轮末结算面事件)
@@ -43,7 +45,11 @@ const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open
# preview_area=资源类型预告〔示警烟火——3×3 邻域资源类型,不揭安危位置〕;
# target_attack_bonus=单位标记加伤〔处刑台——敌血量阈值挂单位字典,主角攻击面通用消费〕
"heal", "ac_bonus", "damage_reduce", "bonus_delta", "blast_scaled", "preview_area",
"target_attack_bonus"]
"target_attack_bonus",
# 迭代 22 秘宝实装批 T3 扩签一型:mine_count_preview=危险格数量预告(巢穴预告——board_installed
# 订阅 do;只报数量不标位置、不碰危险格分布=棋盘事实不可变契约;参数面常时 kind 不入本词表——
# 它们无订阅行,schema 行内 effect_kinds 校验走 treasure_item_table 自有词表)
"mine_count_preview"]
# 注:rewrite_board 已除名(迭代 20 T9——净化工兵随老 27 件退役,V14 连带清理;
# 编排器消费分支 T1 先行摘除)
@@ -80,7 +86,9 @@ const DO_KEYS := ["kind", "value", "value_from", "delta", "delta_from", "mult",
"value_event", "value_counter", "domain", "axis"]
## 主动件分发通道(不经事件订阅——刷新/显影/移除是玩家主动动作;use_treasure 按 kind 分发)
const ACTIVE_KINDS := ["reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick"]
## 迭代 22 秘宝实装批 T3 扩签:+safe_dig 安全格直开(天眼——3×3 内安全格按普通挖格规则全额兑付
## 含连锁展开与格面资源,危险格仅真视显示不移除——棋盘事实不可变契约;免行动点即效果本身,C5 裁)
const ACTIVE_KINDS := ["reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick", "safe_dig"]
# ---- 订阅集与计数器账本 ----
var _triggers: Array = [] # [{source, id, on, when, do}](装载序=声明序,重建时定死)
+18 -8
View File
@@ -67,6 +67,7 @@ var _treasure_unlocked := {} # aux 解锁门控集(迭
var _attunement: Array = [] # 同调装配集(迭代 11 第①批 T5——adventure params.attunement 下传,begin_battle enc 携带进战斗结算;空集=off 零扰动)
var _attune_charges: Dictionary = {} # 同调主动件充能账本(迭代 11 第①批 T6——adventure 共享 dict 引用;settlement use_treasure 扣减直改同引用)
# 关卡末清空+快照计数入 level_finished 载荷)
var _vision_counter: Dictionary = {"used": 0} # 真视类工具使用计数(迭代 22 秘宝批 T3——adventure 共享 dict 引用;编排器受理成功 +1,收束聚合入档)
# ---- 商店共享池模式态(迭代 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——库存=剩余令牌数单点)
@@ -481,6 +482,7 @@ func _start_battle(node: Dictionary) -> bool:
"hero_armor_class": effective_ac(), "hero_ap_bonus": effective_ap_bonus(),
"attunement": _attunement,
"attune_charges": _attune_charges,
"vision_counter": _vision_counter, # 迭代 22 秘宝批 T3——真视使用计数共享 dict(编排器受理成功 +1,收束入档)
"feats": _feats,
"tree_rules": _tree_rules,
"battle_index": _battle_index(),
@@ -672,14 +674,10 @@ func set_attune_charges(charges: Dictionary) -> void:
for i in range(maxi(0, int(charges[iid]) - existing)):
treasure_inventory.add_item(String(iid), "attunement", Vector2i(-1, -1), 0)
# 奖励节点兑付透传(迭代 7 T5——呈现层三选一 → 关卡域库存;来源归因 reward_node)。
# 迭代 11 第①批 T7——同调位持有闸随过渡键退役拆除(aux 卡出池后仅剩防御面;能力件容量闸
# 正主 = 营地装配面 Attunement.slots 满位禁选)。
func grant_treasure_item(item_id: String, source: String) -> Dictionary:
if treasure_inventory == null:
treasure_inventory = TreasureItems.new(db)
return treasure_inventory.add_item(item_id, source, Vector2i(-1, -1), 0)
# 真视使用计数共享引用注入(迭代 22 秘宝批 T3——adventure 持有、编排器受理成功 +1)
func set_vision_counter(counter: Dictionary) -> void:
_vision_counter = counter
# 三选一 GP 袋件入账(迭代 10——钱包收口扩「奖励入账」入口;复用战斗完成同一入账原语,
# wallet_changed 载荷带 source 供呈现归因)
@@ -903,7 +901,19 @@ func shop_shelf() -> Array:
# knob=0——Q2 裁「基础免费 0 次」)用尽后 GP 付费刷新;刷新=货架整架重抽(shop_draw 恰一新 scope,
# 与进店/买后重抽同签——重抽同源共享池);池空/非池模式=不可刷新合法态。
func shop_refresh_free_left() -> int:
return maxi(0, int(db.adventure.shop_refresh_free))
# 迭代 22 秘宝批 T3——刷新执照 shop_refresh_bonus 装配合计叠加(进店入账语义=读数即本店可用,
# 与基础 knob 同生命周期;未装配=基值逐位同旧)
return maxi(0, int(db.adventure.shop_refresh_free) + _attune_shop_refresh_bonus())
# 刷新执照装配合计(迭代 22 秘宝批 T3——装配集 kind 分发扫描;零装配=0)
func _attune_shop_refresh_bonus() -> int:
var total := 0
for iid_v in _attunement:
var arow: Dictionary = db.treasure_item_row(String(iid_v))
if (arow.get("effect_kinds", []) as Array).has("shop_refresh_bonus"):
total += int(arow.get("magnitude", 0))
return total
func shop_refresh_cost() -> int:
+89 -43
View File
@@ -29,59 +29,64 @@ func keystone_balance() -> int:
return save.keystone_balance()
# ---- 道具工坊(首版全走通关次数渠道——钥石直购价随定价批后补,最终双渠道并存)----
# ---- 秘宝解锁服务(迭代 22 T2 换代——解锁混合制,契约 6;旧工坊渠道面随批删除)----
# 〔属性锻造柜台退役——迭代 21 T4/T5:buy_forge/forge_flat/forge_level/forge_price 与 meta_forge
# 表族随攻防所有权切分删除;生命唯一入口=树脊柱淬炼、攻/AC 唯一入口=锻造工坊路线(契约 9);
# 注入面 hero_override 换树淬炼 flat+锻造路线加值位(预留 0)公式。〕
# 解锁判定量单点(可替换轴——2026-09-21 用户拍板:解锁条件须抽象、可替换、可扩展,禁与渠道实现耦合)。
# 契约:渠道由 meta.tres workshop_items.channel 声明(缺省 total_clears,词表 MetaTable.WORKSHOP_CHANNELS
# 封闭);本方法是渠道 → 判定数值的唯一换算点——换/加渠道 = 词表加值 + 此处增一 match 分支,
# 阈值比较(refresh/workshop_status)与解锁写档零改动。候选渠道(语义已预留未实现):
# adventure_depth 最高通关深度 / keystone 钥石直购双渠道。
func _workshop_progress(channel: String) -> int:
match channel:
"total_clears":
var total := 0
for i in db.level_count(): # 表访问收口(2026-10-02——行集直扫退役)
total += save.clear_count(i + 1)
return total
_:
return 0 # 未知渠道恒 0 = 永不解锁(fail-safe 非致命——渠道词表校验兜底,不静默误解锁)
# 表族随攻防所有权切分删除。〕〔道具工坊解锁渠道面退役——迭代 22 T2:旧「总通关次数达阈值」
# 行族与 _workshop_progress/workshop_status/refresh_workshop_unlocks 删除,解锁条件单一事实源=
# treasure_items.tres 行内 unlock_keystone_cost/unlock_requires 谓词(C1 逐件分配锁定表)。〕
# 道具解锁读数访问器(组合根/呈现层解锁门控查询面——禁再穿透 meta.save 直读 save_manager)
func is_item_unlocked(item_id: String) -> bool:
return save.item_unlocked(item_id)
# 工坊六行只读投影(呈现层数据源):{item_id, display_name, threshold, unlocked, progress}
func workshop_status() -> Array:
var out: Array[Dictionary] = []
for row in db.meta_workshop_items():
var iid := String(row.get("item_id", ""))
var arow: Dictionary = db.treasure_item_row(iid) # 迭代 7 T5——失义/退役状态面(呈现「能力封存/已退役」)
out.append({
"item_id": iid,
"display_name": String(row.get("display_name", "")),
"threshold": int(row.get("clear_threshold", 0)),
"unlocked": save.item_unlocked(iid),
"progress": _workshop_progress(String(row.get("channel", "total_clears"))),
"deferred": bool(arow.get("deferred", false)),
"retired": bool(arow.get("retired", false)),
})
return out
# 解锁条件计数读数单点(谓词 key → 生涯计数——封闭枚举=TreasureItemTable.UNLOCK_COUNTER_KEYS;
# 词表加键 = 此处增一 match 分支,阈值比较与解锁写档零改动)
func _treasure_unlock_progress(key: String) -> int:
match key:
"adventure_cleared":
return save.adventure_cleared()
"boss_kill_count":
return save.boss_kill_count()
"vision_used":
return save.vision_used()
_:
return 0 # 未知计数键恒 0 = 永不解锁(fail-safe 非致命——schema 词表校验兜底,不静默误解锁)
# 达标未解锁 → unlock_item + 写盘;返回新解锁 item_id 列(冒险收束后与营地打开时各调一次)
func refresh_workshop_unlocks() -> Array:
# 钥石购买解锁(营地秘宝匣唯一购买写口):仅购买件(行带 unlock_keystone_cost)可购;
# 已解锁/余额不足/条件件误购 = 拒绝且零副作用(spend_keystone 内嵌余额闸,失败不扣币)。
func buy_treasure_unlock(item_id: String) -> bool:
var arow: Dictionary = db.treasure_item_row(item_id)
var price := int(arow.get("unlock_keystone_cost", 0))
if arow.is_empty() or price < 1 or (arow.get("unlock_requires", []) as Array).size() > 0:
return false # 非购买件(条件件/无价行)拒绝
if save.item_unlocked(item_id):
return false # 已解锁重复购买拒绝
if not save.spend_keystone(price):
return false # 余额不足(零副作用)
save.unlock_item(item_id)
return save.write_to_disk()
# 条件解锁刷新(达标未解锁 → unlock_item;返回新解锁 item_id 列——冒险收束后与营地打开时各调一次)。
# 全部谓词达标才解锁(多谓词=AND——天眼双条件);幂等:已解锁跳过,重复刷新零重复入账。
func refresh_treasure_unlocks() -> Array:
var new_unlocks: Array[String] = []
for row in db.meta_workshop_items():
for row in db.treasure_attunement_rows():
var iid := String(row.get("item_id", ""))
if save.item_unlocked(iid):
continue
var progress := _workshop_progress(String(row.get("channel", "total_clears")))
if progress >= int(row.get("clear_threshold", 99)):
var ureqs: Array = row.get("unlock_requires", []) as Array
if ureqs.is_empty():
continue # 购买件不入条件刷新面
var met := true
for req_v in ureqs:
var req: Dictionary = req_v as Dictionary
if _treasure_unlock_progress(String(req.get("key", ""))) < int(req.get("threshold", 999)):
met = false
break
if met:
save.unlock_item(iid)
new_unlocks.append(iid)
if not new_unlocks.is_empty():
@@ -89,6 +94,35 @@ func refresh_workshop_unlocks() -> Array:
return new_unlocks
# 秘宝匣柜台只读投影(呈现层数据源——12 件逐行解锁态/价格/条件进度):
# {item_id, display_name, desc, tier, unlocked, mode(keystone|condition|owned), price, reqs:[{key,threshold,progress}]}
func treasure_shop_status() -> Array:
var out: Array[Dictionary] = []
for row in db.treasure_attunement_rows():
var iid := String(row.get("item_id", ""))
var unlocked := save.item_unlocked(iid)
var ureqs: Array = row.get("unlock_requires", []) as Array
var req_view: Array = []
for req_v in ureqs:
var req: Dictionary = req_v as Dictionary
req_view.append({
"key": String(req.get("key", "")),
"threshold": int(req.get("threshold", 0)),
"progress": _treasure_unlock_progress(String(req.get("key", ""))),
})
out.append({
"item_id": iid,
"display_name": String(row.get("display_name", "")),
"desc": String(row.get("desc", "")),
"tier": String(row.get("tier", "")),
"unlocked": unlocked,
"mode": "owned" if unlocked else ("condition" if ureqs.size() > 0 else "keystone"),
"price": int(row.get("unlock_keystone_cost", 0)),
"reqs": req_view,
})
return out
# ---- Boss 两档与收束入账(结算屏文案数据源)----
# 明细 {big_reward, boss_drops, total, new_unlocks};入账与标记经 save.apply_adventure_summary
# (keystone_drops 副本随摘要入 recent_summaries)。
@@ -100,7 +134,8 @@ func settle_adventure(summary: Dictionary) -> Dictionary:
save.mark_first_adventure_done()
var drops: Array = (summary.get("keystone_drops", []) as Array).duplicate(true)
save.apply_adventure_summary(summary, drops)
var new_unlocks := refresh_workshop_unlocks()
save.add_vision_used(int(summary.get("vision_used_delta", 0))) # 迭代 22 T3——真视使用计数收束聚合入档(信标网络行为条件)
var new_unlocks := refresh_treasure_unlocks() # 迭代 22 T2——条件解锁刷新换代(旧工坊面退役)
save.write_to_disk()
var total := int(summary.get("keystone_total", 0))
return {
@@ -297,16 +332,24 @@ func _check_easter_egg() -> void:
# ---- 同调位账本(迭代 11 第①批 T4——装配关系/同调位求值/Y 刷新升级;设计权威 proposal §2 + 需求卡片 P1 甲)----
# 装配切换(营地装配覆盖层唯一写口):已装 → 卸下恒可;未装 → 非同调行拒绝 + 满位禁选;
# 装配关系写存档持久(冒险中途不可换装——底稿 §8.5,本口只服务营地)。
# 装配切换(营地装配覆盖层唯一写口——冒险开始前配置;冒险中零写口=装配不可变契约 1,本口只服务营地):
# 已装 → 卸下恒可(卸下后其余件前移位序——效果面只读装配集不受格位影响);未装 → 非同调行拒绝 +
# 满位禁选 + 档位格校验(迭代 22 T2 契约 2:装入件 append 至末位 k=已装数+1,件档 rank ≤ k——
# 格 1=普通格/格 2=优秀格/格 3=稀有格递增向下兼容;槽位来源=树同调位节点点亮数,树接口接缝现役)。
# 装配关系写存档持久。
func toggle_attunement(item_id: String) -> bool:
var equipped: Array = save.attunement_equipped()
if not equipped.has(item_id):
var arow: Dictionary = db.treasure_item_row(item_id)
if arow.is_empty() or String(arow.get("form", "")) == "":
return false # 非同调行(过渡/退役行)禁装配
if not save.item_unlocked(item_id):
return false # 未解锁禁装配(解锁混合制契约 6——装配以解锁为前置)
if equipped.size() >= Attunement.slots(db, save):
return false # 满位禁选
var rank: int = int(db.treasure_items.TIER_RANK.get(String(arow.get("tier", "")), 99))
if rank > equipped.size() + 1:
return false # 件档高于将占格档(第 k 格档=k——普通/优秀/稀有递增)
equipped.append(item_id)
else:
equipped.erase(item_id)
@@ -323,8 +366,11 @@ func attune_camp_status() -> Dictionary:
items.append({
"item_id": iid,
"display_name": String(row.get("display_name", "")),
"desc": String(row.get("desc", "")),
"tier": String(row.get("tier", "")),
"form": String(row.get("form", "")),
"equipped": equipped.has(iid),
"unlocked": save.item_unlocked(iid),
})
var lvl := save.attune_refresh_level()
var next_row: Dictionary = db.meta_attune_refresh_row(lvl + 1)
+22 -3
View File
@@ -15,6 +15,12 @@ signal round_end_board(payload: Dictionary) # 双发射源之棋盘路径(
var db: ConfigDb
var action_economy: ActionEconomy
# 秘宝常时参数注入面(迭代 22 秘宝批 T3——编排器 begin_battle 按装配集 kind 分发注入:
# dig_range_bonus=挖掘切比雪夫距离放宽〔远程镐 dig_range_bonus magnitude〕/flag_range_free=
# 插旗不限相邻〔信标网络〕;缺省 0/false=零装配零扰动,棋盘事实不可变契约不受影响——
# 两参数只放宽「动作可达性」判据,禁碰危险格/安全格分布)
var dig_range_bonus := 0
var flag_range_free := false
# 棋盘状态私有(E2 封装收口 2026-09-13):对外只暴露 current_board()/reveal_record() 等查询面,
# 禁止跨模块直读/直写原始字典——先例教训 = board_view 绕过查询面直读 revealed。
var _board: BoardData = null
@@ -322,7 +328,7 @@ func _do_dig(c: Vector2i) -> Dictionary:
func _board_face_dig(c: Vector2i) -> String:
if not _board.in_bounds(c):
return "out_of_catalog"
if not _adjacent_to_hero(c):
if not _within_dig_range(c):
return "not_adjacent"
if is_revealed(c):
return "target_revealed"
@@ -395,8 +401,8 @@ func _do_flag_place(c: Vector2i) -> Dictionary:
var sub := ""
if not _board.in_bounds(c):
sub = "out_of_catalog"
elif not _adjacent_to_hero(c):
sub = "not_adjacent"
elif not (flag_range_free or _adjacent_to_hero(c)):
sub = "not_adjacent" # 信标网络装配时豁免(插旗不限相邻——旗动作仍按标准价计费,规则放宽仅可达性)
elif is_revealed(c):
sub = "target_revealed"
elif has_flag(c):
@@ -489,5 +495,18 @@ func _adjacent_to_hero(c: Vector2i) -> bool:
return maxi(d.x, d.y) == 1
# 挖掘可达判据(迭代 22 秘宝批 T3——远程镐 dig_range_bonus 放宽:切比雪夫距离 ≤ 1+加值,
# 加值 0=与旧「恰相邻」逐位一致;开格仍走既有格规则=不挖开不相邻事实之外 anything)
func _within_dig_range(c: Vector2i) -> bool:
var d := Vector2i(absi(c.x - _hero.x), absi(c.y - _hero.y))
return maxi(d.x, d.y) <= 1 + dig_range_bonus
# 本板危险格总数(迭代 22 秘宝批 T3——巢穴预告「装板后揭示本板危险格数量」取数口;
# 只读投影 BoardData.mines 规模,零分布泄露——数量非位置)
func mine_total() -> int:
return _board.mines.size() if _board != null else 0
func _outcome(accepted: bool, outcome: String, reason: String, sub: String) -> Dictionary:
return {"accepted": accepted, "outcome": outcome, "reject_reason": reason, "sub_reason": sub}
+13 -1
View File
@@ -78,7 +78,7 @@ func _migrate_v1_v2(data: Dictionary) -> Dictionary:
# v2→v3:断点附随件扩列(迭代 10 奖励重构批——bonus_taken/bonus_pending/bonus_result/
# bonus_aux_carry/keystone_drops + bonus.ap 轴)与摘要 additive 键 keystone_drops——旧档缺键
# keystone_drops + bonus.ap 轴)与摘要 additive 键 keystone_drops——旧档缺键〔bonus_aux_carry 键随迭代 22 T5 替换子交互拆除退役〕
# = 合法空态(读取方 .get 兜底),无结构迁移体;版本号推进仅为 schema_version 对齐
func _migrate_v2_v3(data: Dictionary) -> Dictionary:
data["version"] = SAVE_FORMAT_VERSION
@@ -134,6 +134,8 @@ func _default_data() -> Dictionary[String, Variant]:
"first_adventure_done": false,
"adventure_total": 0, # 生涯终身计数·冒险总局数(迭代 18 卡④——开局即计含放弃与失败,收束唯一入账)
"adventure_cleared": 0, # 生涯终身计数·通关局数(三关全通才计 1——判定字段 summary.cleared)
"vision_used": 0, # 生涯终身计数·真视类工具累计使用次数(迭代 22 T2——信标网络解锁行为条件;
# 受理成功唯一计数点=battle 层经冒险层聚合、收束入账;缺键补零不升版本——迭代 21 先例)
},
"breakpoint": {}, # 冒险断点快照(空 = 无断点合法态——「继续上局」隐藏;迭代 8 卡①)
}
@@ -272,6 +274,16 @@ func adventure_cleared() -> int:
return int(_data["meta"].get("adventure_cleared", 0))
# 真视类工具累计使用次数(迭代 22 T2——信标网络解锁行为条件;写入唯一经 add_vision_used
# 冒险收束聚合入账,受理计数点=battle 层经冒险层聚合)
func vision_used() -> int:
return int(_data["meta"].get("vision_used", 0))
func add_vision_used(n: int) -> void:
_data["meta"]["vision_used"] = maxi(0, vision_used() + maxi(0, n))
# ---- 天赋树域(迭代 12 T9——MetaProgression 树段读写面)----
func tree_nodes() -> Dictionary:
+13 -15
View File
@@ -79,14 +79,6 @@ func holding_count() -> int:
# 持有件记录(迭代 10 追加批——奖励节点替换子交互数据源:只列未消耗件,handle 定位丢弃目标)
func holding_records() -> Array:
var out: Array = []
for rec in _items:
if not bool(rec.get("used", false)):
out.append({"handle": int(rec.get("handle", 0)), "item_id": String(rec.get("item_id", ""))})
return out
# 丢弃(同调位替换交互节点渠道——腾位收新;与 Used 消费分轨的一次性移除,快照不再含)
func discard_item(handle: int) -> bool:
for i in _items.size():
@@ -140,15 +132,21 @@ func validate_use(handle: int, target: Vector2i, board_size: Vector2i, cell_unre
return _reject("道具未激活")
var kinds: Array = row.get("effect_kinds", [])
var resolved: Array = []
if kinds.has("mine_reveal") or kinds.has("reward_preview"):
# 空间效果:3×3 窗口 ∩ 板内格(边缘受理——护栏只缩不扩)
if kinds.has("mine_reveal") or kinds.has("reward_preview") or kinds.has("reveal_tiles") or kinds.has("safe_dig"):
# 空间效果(迭代 22 秘宝批 T3 扩面——真视 reveal_tiles/天眼复合 safe_dig 入空间解析;
# 真视窄域护栏 REVEAL_KINDS 同守):
if target.x < 0 or target.y < 0 or target.x >= int(board_size.x) or target.y >= int(board_size.y):
return _reject("目标越板")
for dy in range(-1, 2):
for dx in range(-1, 2):
var c := target + Vector2i(dx, dy)
if c.x >= 0 and c.y >= 0 and c.x < int(board_size.x) and c.y < int(board_size.y):
resolved.append(c)
if String(row.get("target", "")) == "cell":
# 单格档(迭代 22 T1——线香真视单格;≤3×3 窄域契约内的点目标)
resolved.append(target)
else:
# 3×3 窗口 ∩ 板内格(边缘受理——护栏只缩不扩)
for dy in range(-1, 2):
for dx in range(-1, 2):
var c := target + Vector2i(dx, dy)
if c.x >= 0 and c.y >= 0 and c.x < int(board_size.x) and c.y < int(board_size.y):
resolved.append(c)
# 点击级零信息:纯预读行窗口全已开 = 没东西可看(拒绝零消耗);
# 真视恒可用(零雷窗口=「这 9 格没雷」信息非空——GDD 裁定)
if kinds.has("reward_preview") and not kinds.has("mine_reveal"):
+8 -33
View File
@@ -3,38 +3,33 @@
#(元进度 GDD 验收标准,本批 = 其实现批)
# Date: 2026-09-21(元进度实现批——设计权威 design/gdd/systems/冒险循环/元进度.md 定稿 v1.2)
# 数据配置 MetaTable schema(元进度行族)。
# 行族四轴(数据契约对齐 ConfigDB.meta_* 访问器——消费面一律走访问器,禁直读行集):
# 行族三轴(数据契约对齐 ConfigDB.meta_* 访问器——消费面一律走访问器,禁直读行集):
# boss_keystone = Boss 钥石两档行(level_index 连续从 1;first_kill ≥ repeat_kill——首杀全量、
# 重复杀固定小额恒定不递减,两档数值归元进度设计批配读数)。
# workshop_items = 道具工坊行(item_id = 秘宝六件词表封闭恰全;首版解锁渠道 = 总通关次数
# 达标 clear_threshold——2026-09-20 排期裁决;局内效果整体归 Alpha 秘宝重设计批,本表只有解锁状态面)。
# conversion = 区间平移换算率行(attr 与三属性轴词表一致恰三;per_level_shift_pct ∈ [0,100]——
# registry 键 meta_progression_conversion_table;护甲行强条目:验收读数必须按解锁后参数重测)。
# attune_refresh_upgrade = Y 刷新升级行族(迭代 11 第①批 T4——P1 甲:每冒险开局给 Y 次、冒险周期池、
# 用完即无;level 连续从 1,等级 L → refresh_charges=Y 表值,钥石直购 keystone_cost;数值全 provisional)。
# 〔forge_products 属性锻造行族已退役——迭代 21 T5 攻防所有权切分:生命唯一入口=树脊柱、攻/AC
# 唯一入口=锻造工坊路线;行族、访问器与购买面随批删除,历史投值记载归 git 考古。〕
# 〔workshop_items 道具工坊行族已退役——迭代 22 T2 秘宝实装批:解锁换代混合制(钥石购买价
# unlock_keystone_cost + 条件谓词 unlock_requires 全挂 treasure_items.tres 行内,单一事实源),
# 旧「总通关次数达阈值」渠道行族、WORKSHOP_ITEM_IDS/WORKSHOP_CHANNELS 词表与校验段随批删除。〕
# 行值全部 provisional 归 元进度设计批配读数(用户拍板骨架先行,先例 = GP+商店房批)。
class_name MetaTable
extends Resource
@export var boss_keystone: Array[Dictionary] = [] # Array[Dictionary] 键 level_index
@export var workshop_items: Array[Dictionary] = [] # Array[Dictionary] 键 item_id
@export var conversion: Array[Dictionary] = [] # Array[Dictionary] 键 attr
@export var attune_refresh_upgrade: Array[Dictionary] = [] # Array[Dictionary] 键 level——Y 刷新升级行族(迭代 11 第①批 T4)
const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] # 三属性轴词表(conversion 行键域——原 forge 词表名沿用作轴名)
const WORKSHOP_ITEM_IDS := ["probe_dust", "sapper_order", "loot_compass", "nest_pendulum", "catacomb_scroll", "blast_kit"]
# 工坊解锁渠道词表(可替换轴——2026-09-21 用户拍板:解锁条件须抽象、可替换、可扩展。
# 现行唯一合法值 total_clears = 总通关次数;扩新渠道 = 词表加值 + MetaProgression._workshop_progress
# 增一分支,阈值比较与解锁写档零改动。缺省(行无 channel 字段)= total_clears)
const WORKSHOP_CHANNELS := ["total_clears"]
func validate() -> Array:
var errs: Array[Dictionary] = []
# 行族空护栏(四族皆为 ConfigDB 消费面——缺族即致命)
for family in ["boss_keystone", "workshop_items", "conversion", "attune_refresh_upgrade"]:
# 行族空护栏(三族皆为 ConfigDB 消费面——缺族即致命)
for family in ["boss_keystone", "conversion", "attune_refresh_upgrade"]:
if get(family).is_empty():
errs.append({"category": "empty_table", "message": "MetaTable 行族为空: %s" % family})
# boss_keystone:level_index 唯一 + 连续从 1 + 两档域 + first ≥ repeat(首杀全量、重复小额恒定)
@@ -55,28 +50,8 @@ func validate() -> Array:
if li_at != i + 1:
errs.append({"category": "structural", "message": "boss_keystone level_index 不连续(须从 1 连续递增): 位 %d = %d" % [i + 1, li_at]})
break
# workshop_items:item_id 词表封闭 + 唯一 + 覆盖恰六(秘宝词表)+ 阈值域 [1,99]
var seen_item := {}
for row in workshop_items:
var iid: String = row.get("item_id", "")
if not WORKSHOP_ITEM_IDS.has(iid):
errs.append({"category": "enum", "message": "workshop_items item_id 出域秘宝六件词表: %s" % iid})
if seen_item.has(iid):
errs.append({"category": "dup_key", "message": "workshop_items item_id 重复: %s" % iid})
seen_item[iid] = true
if String(row.get("display_name", "")) == "":
errs.append({"category": "domain", "message": "workshop_items %s display_name 空" % iid})
var th: int = int(row.get("clear_threshold", -1))
if th < 1 or th > 99:
errs.append({"category": "domain", "message": "workshop_items %s clear_threshold 越域 [1,99]: %d" % [iid, th]})
# channel:可替换解锁渠道(缺省 total_clears——词表封闭,扩渠道 = 词表加值 + 判定量分支)
var channel: String = row.get("channel", "total_clears")
if not WORKSHOP_CHANNELS.has(channel):
errs.append({"category": "enum", "message": "workshop_items %s channel 出域 %s: %s" % [iid, str(WORKSHOP_CHANNELS), channel]})
if not workshop_items.is_empty():
for iid_key in WORKSHOP_ITEM_IDS:
if not seen_item.has(iid_key):
errs.append({"category": "coverage", "message": "workshop_items 缺道具键: %s(六件词表恰全)" % iid_key})
# workshop_items 行族已退役(迭代 22 T2——解锁混合制换代,条件谓词挂 treasure_items 行内;
# 校验段随批删除——秘宝解锁条件校验单点=TreasureItemTable.validate)
# conversion:attr 词表与 forge 一致 + 唯一 + 覆盖恰三 + 平移量域 [0,100]
var seen_conv := {}
for row in conversion:
+68 -16
View File
@@ -9,11 +9,10 @@
# effect_kinds ⊆ {mine_reveal, reward_preview, ap_grant}(活性校验见下)/ target ∈ {none, area_3x3, board_wide} /
# magnitude [0,999](仅 ap_grant 行消费,加载校验 ≥1)/ use_phase ∈ {active_dig}。
# additive 两标记(迭代 7 T1——拍板①④的表内表达,非 registry fields 预登记字段的扩列随批登记):
# retired = 退役留痕行(loot_compass——未开格类型微光常显覆盖预读价值,2026-09-29 用户拍板退役;
# 行名保留=meta.tres workshop_items 六件词表恰全校验兼容,存档 item_unlocks 旧键无害;
# 免活性校验、不入投放校验、不入 F2 覆盖面、奖励节点/掉落池排除)
# deferred = 效果失义待重定行(probe_dust/catacomb_scroll——原显影半边随雾格移除失义,效果与目标
# 随秘宝整体重设计归 Alpha(2026-09-20 用户拍板);免 effect_kinds 非空校验、不入投放与 F2)
# retired = 退役留痕行(历史形态——迭代 11-19 期载体;新目录 12 行零 retired 行〔迭代 22 T1 全量
# 换代〕,机制面保留=deferred/retired 双标记的加载豁免通道仍在役)
# deferred = 效果失义待重定行(历史形态——显影尘/地穴全图卷 2026-09-20 拍板;新目录零 deferred 行,
# 机制面保留同上;免 effect_kinds 非空校验、不入投放与 F2)
# 承重护栏(秘宝 GDD §结构冻结,schema 校验强制不靠运行时拦截):
# 真视窄域——effect_kinds 含 mine_reveal 的活行 target 必须 area_3x3(真视一次最多 3×3,禁全图看雷);
# board_wide 仅恢复推理原料类——target=board_wide 的活行 effect_kinds 不得含 mine_reveal(现行无此类活行,
@@ -36,8 +35,11 @@ extends Resource
@export var rows: Array = [] # Array[Dictionary] 键 item_id
# 计数键封闭枚举(原 AttunementGrowthTable.COUNTER_KEYS——迭代 21 T5 谓词表退役后就地内联;
# boss_kill_count/clear_count 计数域保留〔各有非槽位硬消费,卡片 T0⑥〕)
const UNLOCK_COUNTER_KEYS := ["clear_count", "boss_kill_count"]
# 迭代 22 T1 换代:clear_count(旧工坊按关累计渠道)与 C6 通关口径(通关=打通整局冒险)不符,
# 换名 adventure_cleared(生涯通关局数,save_manager 生涯计数域在役)并扩 vision_used(真视类工具
# 累计使用——信标网络行为条件计数,本批新增;use_treasure 受理成功处累计、冒险收束导出入档);
# 旧 clear_count 零行消费,换名安全)
const UNLOCK_COUNTER_KEYS := ["adventure_cleared", "boss_kill_count", "vision_used"]
const DIMENSIONS := ["info", "risk", "action", "plan", "counter"]
# 同调批扩 enum(迭代 11 第①批 T1)——新增九 kind 全部条件触发/机制修正类;mine_reveal/ap_grant 语义不变。
@@ -49,17 +51,37 @@ const DIMENSIONS := ["info", "risk", "action", "plan", "counter"]
const EFFECT_KINDS := ["mine_reveal", "reward_preview", "ap_grant",
"mine_hit_halve", "reflect_damage", "ap_rounds_front", "ap_charge_safe", "cell_removal",
"reward_weight_bias", "bonus_refresh",
"reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick"]
const TARGETS := ["none", "area_3x3", "board_wide"]
"reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick",
# 迭代 22 秘宝实装批 T1 扩签八型:
# 常时参数面六型(无事件订阅——开局/装板参数注入,注入面按装配集 kind 分发读取):
# light_radius_bonus=微光半径加值〔阔视野带〕/rap_transfer_mult=停手转存率千分比〔储备罐 125=1.25〕/
# dig_range_bonus=挖掘距离加值〔远程镐〕/flag_range_free=插旗不限相邻〔信标网络〕/
# refresh_stock_grant=三选一刷新存量一次性加仓〔刷新练习〕/shop_refresh_bonus=商店免费刷新加值〔刷新执照〕;
# 主动分发两型:safe_dig=安全格直开(天眼——走普通挖格规则全额兑付,C5 裁)/mine_count_preview=危险格数量预告(巢穴预告)
"light_radius_bonus", "rap_transfer_mult", "dig_range_bonus", "flag_range_free",
"refresh_stock_grant", "shop_refresh_bonus", "safe_dig", "mine_count_preview"]
const TARGETS := ["none", "area_3x3", "board_wide", "cell"] # 迭代 22 T1 扩 cell=单格真视(线香——≤3×3 窄域契约内)
const USE_PHASES := ["active_dig"]
const FORMS := ["", "passive", "active_charged"]
# 档位词表(迭代 22 T1——槽位档位约束数据面:槽位可装 ≤ 槽位档位的件;词表沿 feats/adventure TIERS)
const TIERS := ["common", "excellent", "rare", "legendary"]
# 档位序(迭代 22 T2——装配档位校验取数源:件档 rank ≤ 装入位序(格 1=普通格/格 2=优秀格/格 3=稀有格
# 递增,向下兼容=高档格可装低档件);rank 越大档越高)
const TIER_RANK := {"common": 1, "excellent": 2, "rare": 3, "legendary": 4}
# 充能重置周期(迭代 22 T1 扩签——现役仅 adventure=每冒险;battle=每战重置〔线香〕/level=每关重置〔天眼〕)
const CHARGE_RESETS := ["adventure", "battle", "level"]
# magnitude 消费面(≥1 校验):grant_ap 与同调 N 值 kind;其余 kind 活行 magnitude 恒 0
const MAGNITUDE_KINDS := ["ap_grant", "reflect_damage", "ap_rounds_front",
"ap_charge_safe", "reward_weight_bias", "bonus_refresh", "grant_ap"]
"ap_charge_safe", "reward_weight_bias", "bonus_refresh", "grant_ap",
"light_radius_bonus", "rap_transfer_mult", "dig_range_bonus", "flag_range_free",
"refresh_stock_grant", "shop_refresh_bonus"]
# 真视类 kind(迭代 22 T1 护栏扩面——活行 target 必须 ∈ {area_3x3, cell}:真视窄域 ≤3×3 契约)
const REVEAL_KINDS := ["mine_reveal", "reveal_tiles"]
# 🔴 零裸 flat 结构护栏:行键白名单封闭(键出域=加载致命)——机制轴禁无条件属性加项的表结构承载
const KEY_WHITELIST := ["item_id", "display_name", "dimension_tags", "effect_kinds", "target",
"magnitude", "use_phase", "retired", "deferred", "form", "charges_per_adventure", "slot_cost",
"unlock_requires", "effective_rounds", "charge_n", "triggers", "desc"]
"unlock_requires", "effective_rounds", "charge_n", "triggers", "desc",
"tier", "charge_reset", "unlock_keystone_cost"] # 迭代 22 T1 扩三键(档位/充能周期/钥石购买价)
func validate() -> Array:
@@ -123,9 +145,11 @@ func validate() -> Array:
if kind_seen.has(kind):
errs.append({"category": "dup_key", "message": "%s effect_kind 重复: %s" % [iid, kind]})
kind_seen[kind] = true
# 承重护栏①真视窄域:mine_reveal 活行 target 必须 area_3x3(一次最多 3×3,禁全图看雷)
if kind_seen.has("mine_reveal") and target != "area_3x3":
errs.append({"category": "load_bearing", "message": "%s 真视窄域护栏:mine_reveal 行 target 必须 area_3x3,实得 %s(秘宝 GDD 结构冻结)" % [iid, target]})
# 承重护栏①真视窄域:真视类活行 target 必须 area_3x3 或 cell(一次最多 3×3,禁全图看雷;
# 迭代 22 T1 护栏扩面——reveal_tiles 与新 cell 档纳入同护栏,线香单格=cell ⊂ 3×3 契约内)
for rv in REVEAL_KINDS:
if kind_seen.has(String(rv)) and not (target == "area_3x3" or target == "cell"):
errs.append({"category": "load_bearing", "message": "%s 真视窄域护栏:%s 行 target 必须 area_3x3 或 cell,实得 %s(秘宝 GDD 结构冻结)" % [iid, String(rv), target]})
# 承重护栏②board_wide 仅恢复推理类:全图域行禁替推理效果
if target == "board_wide" and kind_seen.has("mine_reveal"):
errs.append({"category": "load_bearing", "message": "%s board_wide 行 effect_kinds 不得含 mine_reveal(全图域仅恢复推理原料类——Alpha 重定后落位)" % iid})
@@ -145,11 +169,39 @@ func validate() -> Array:
if form != "":
if int(row.get("slot_cost", 1)) != 1:
errs.append({"category": "load_bearing", "message": "%s 同调占位统一 1 槽(slot_cost 恒 1——底稿裁决 5,schema 强制)" % iid})
# 档位必填(迭代 22 T1——槽位档位约束数据面:件档≤格档判定的取数源)
var tier: String = String(row.get("tier", ""))
if not TIERS.has(tier):
errs.append({"category": "domain", "message": "%s 同调行 tier 必填且 ∈ %s,实得 %s" % [iid, str(TIERS), tier]})
var charges: int = int(row.get("charges_per_adventure", 0))
if form == "passive" and charges != 0:
errs.append({"category": "cross_field", "message": "%s passive 行 charges_per_adventure 应 0,实得 %d" % [iid, charges]})
if form == "active_charged" and charges < 1:
errs.append({"category": "cross_field", "message": "%s active_charged 行 charges_per_adventure 应 ≥1,实得 %d" % [iid, charges]})
if form == "active_charged" and charges < 0:
errs.append({"category": "domain", "message": "%s active_charged 行 charges_per_adventure 越域 ≥0,实得 %d" % [iid, charges]})
if form == "active_charged" and charges == 0:
# 充能 0=积点制/滑耗制合法态(迭代 22 T1——试炼镐转化获取,无固定充能上限):
# 必须带使用闸——ap_charge_safe 滑耗(安全格积点)或 treasure_used+counter_spend 受理闸,二者缺一即孤儿行
var has_gate := kind_seen.has("ap_charge_safe")
if not has_gate and row.has("triggers"):
for tr_v in (row.get("triggers", []) as Array):
if String((tr_v as Dictionary).get("do", {}).get("kind", "")) == "counter_spend":
has_gate = true
break
if not has_gate:
errs.append({"category": "cross_field", "message": "%s active_charged 行 charges=0(积点制)须带 ap_charge_safe 或 counter_spend 受理闸,实得二者皆无" % iid})
# 充能重置周期(迭代 22 T1 扩签:adventure 缺省/battle 每战/level 每关;仅 active_charged 行可设)
var creset: String = String(row.get("charge_reset", "adventure"))
if not CHARGE_RESETS.has(creset):
errs.append({"category": "enum", "message": "%s charge_reset 出域 %s: %s" % [iid, str(CHARGE_RESETS), creset]})
if form != "active_charged" and row.has("charge_reset") and creset != "adventure":
errs.append({"category": "cross_field", "message": "%s 非 active_charged 行 charge_reset 仅可缺省(adventure),实得 %s" % [iid, creset]})
# 解锁混合制字段校验(迭代 22 T1 契约 6:钥石购买价与条件谓词互斥——每件单解锁方式,混合不做)
var price: int = int(row.get("unlock_keystone_cost", 0))
var ureq_count: int = (row.get("unlock_requires", []) as Array).size()
if price != 0 and ureq_count > 0:
errs.append({"category": "cross_field", "message": "%s 解锁方式互斥:unlock_keystone_cost 与 unlock_requires 不得并存(每件单解锁方式,契约 6)" % iid})
if price < 0 or price > 999:
errs.append({"category": "domain", "message": "%s unlock_keystone_cost 越域 [0,999](0=非购买件缺省),实得 %d" % [iid, price]})
var eff_rounds: int = int(row.get("effective_rounds", 0))
if eff_rounds < 0 or eff_rounds > 999:
errs.append({"category": "domain", "message": "%s effective_rounds 越域 [0,999]: %d" % [iid, eff_rounds]})
+6 -34
View File
@@ -17,37 +17,6 @@ boss_keystone = [{
"first_kill": 3,
"repeat_kill": 3
}]
workshop_items = [{
"item_id": "probe_dust",
"display_name": "显影尘",
"clear_threshold": 1,
"channel": "total_clears"
}, {
"item_id": "sapper_order",
"display_name": "工兵令",
"clear_threshold": 1,
"channel": "total_clears"
}, {
"item_id": "loot_compass",
"display_name": "探宝罗盘",
"clear_threshold": 2,
"channel": "total_clears"
}, {
"item_id": "nest_pendulum",
"display_name": "探巢摆钟",
"clear_threshold": 2,
"channel": "total_clears"
}, {
"item_id": "catacomb_scroll",
"display_name": "地穴全图卷",
"clear_threshold": 3,
"channel": "total_clears"
}, {
"item_id": "blast_kit",
"display_name": "爆破工兵包",
"clear_threshold": 3,
"channel": "total_clears"
}]
conversion = [{
"attr": "max_hp",
"per_level_shift_pct": 6
@@ -73,9 +42,12 @@ attune_refresh_upgrade = [{
; 〔forge_products 属性锻造行族已退役——迭代 21 T5 攻防所有权切分:生命唯一入口=树脊柱淬炼、
; 攻/AC 唯一入口=锻造工坊路线(契约 9 红线),meta_forge_row 访问器与 buy_forge 购买面随批删除;
; 历史投值记载(2026-09-22 对齿批 base 20/ratio 150、2026-09-23 标定批 6/9/1)系 conversion 行
; 与 ForgeGDD 考古用,行族本体不再在表。〕boss 两档 L1 5/1 · L2 8/2 · L3 12/3
; (首杀全量、重复杀固定小额恒定不递减);工坊六件阈值 1/1/2/2/3/3 按总通关次数(首版只挂通关次数渠道,
; 钥石直购价随定价批后补);conversion 全 0 = 未标定占位(标定归设计批,护甲行强条目:验收读数必须按
; 与 ForgeGDD 考古用,行族本体不再在表。〕
; 〔workshop_items 道具工坊行族已退役——迭代 22 T2 秘宝实装批:解锁换代混合制(钥石购买价
; unlock_keystone_cost + 条件谓词 unlock_requires 全挂 treasure_items.tres 行内单一事实源),
; 旧「总通关次数达阈值」六行(阈值 1/1/2/2/3/3)与渠道面随批删除;历史记载归 git 考古。〕
; boss 两档 L1 5/1 · L2 8/2 · L3 12/3
; (首杀全量、重复杀固定小额恒定不递减);conversion 全 0 = 未标定占位(标定归设计批,护甲行强条目:验收读数必须按
; 解锁后参数重测)。
;
; 数值设计批·关卡成长×永久成长对齿(2026-09-22——设计权威 qa-data/数值设计_关卡成长×永久成长_v1.md,
+179 -132
View File
@@ -4,115 +4,34 @@
[resource]
script = ExtResource("1")
; 迭代 19 卡④(2026-10-08):表尾追加 fog_lantern 魔法提灯(提灯载体道具批——视野外扩 +2 纯呈现载体行;
; 空效果行 live 形态=triggers 显式空数组,与显影尘 deferred 形态的形态差=「激活 vs 封存」);行字段值=用户「按推荐」全收 2026-10-08。
; 迭代 11 第①批 T1(2026-10-03):追加七件同调行(form 非空——永不入过渡渠道);值全 provisional 归 难度曲线与Boss。
; nest_pendulum 已换装 active_charged(迭代 11 第①批);blast_kit 已换装 cell_removal+active_charged(迭代 13 T8/D6——中立移除雷格启用);sapper_order/显影尘/地穴全图卷/罗盘 T7 逐件销账。
; 迭代 22 秘宝体系实现重构落地批 T1(2026-10-12):新目录 12 行全量换代(设计权威=秘宝.md「目录(重设计 v1)」;
; 旧 12 行删除——venom_edge/flag_burst 已随迭代 20 专长批迁出删除,本批实删 12 行)。三档=普通 7/优秀 3/稀有 2
; (传说留空=空集合法,迭代 20 追认③先例)。解锁混合制(契约 8 逐件分配锁定表,2026-10-11 用户裁 C1):
; 钥石购买件 unlock_keystone_cost(provisional 占位);条件件 unlock_requires 谓词行——计数键
; adventure_cleared=生涯通关局数(C6 裁:通关=打通整局冒险)/boss_kill_count=生涯 Boss 击杀/vision_used=
; 真视类工具累计使用(本批新增计数域)。全部数值 provisional 归调参批(契约 7)。
; 常时参数面件(无订阅行——效果=开局/装板参数注入,非事件订阅):阔视野带/储备罐/远程镐/信标网络/刷新练习/
; 刷新执照六件 effect_kinds 挂参数 kind,由注入面按装配集 kind 分发读取(_attune_refresh_x_total 先例)。
; 主动件:线香(battle 级充能)/天眼(level 级充能)charge_reset 扩签;试炼镐=积点制(charges=0 无上限,
; ap_charge_safe 滑耗单级化——设计「4 格积 1 点满 2 点换 AP」两级结构的行为等价实现,8 格兑 1 AP)。
rows = [{
"item_id": "probe_dust",
"display_name": "显影尘",
"dimension_tags": ["info", "counter"],
"desc": "效果失义待重定",
"effect_kinds": [],
"target": "none",
"magnitude": 0,
"use_phase": "active_dig",
"deferred": true
}, {
"item_id": "sapper_order",
"display_name": "工兵令",
"item_id": "morning_order",
"display_name": "晨操令",
"dimension_tags": ["action"],
"desc": "AP +2",
"effect_kinds": ["ap_grant"],
"target": "none",
"magnitude": 2,
"use_phase": "active_dig",
"retired": true
}, {
"item_id": "loot_compass",
"display_name": "探宝罗盘",
"dimension_tags": ["plan"],
"desc": "预读 3×3",
"effect_kinds": ["reward_preview"],
"target": "area_3x3",
"magnitude": 0,
"use_phase": "active_dig",
"retired": true
}, {
"item_id": "nest_pendulum",
"display_name": "探巢摆钟",
"dimension_tags": ["risk"],
"desc": "真视 3×3",
"effect_kinds": ["mine_reveal"],
"target": "area_3x3",
"magnitude": 0,
"use_phase": "active_dig",
"form": "active_charged",
"slot_cost": 1,
"charges_per_adventure": 2
}, {
"item_id": "catacomb_scroll",
"display_name": "地穴全图卷",
"dimension_tags": ["info", "plan"],
"desc": "效果失义待重定",
"effect_kinds": [],
"target": "board_wide",
"magnitude": 0,
"use_phase": "active_dig",
"deferred": true
}, {
"item_id": "blast_kit",
"display_name": "爆破工兵包",
"dimension_tags": ["action", "risk"],
"desc": "AP +4 真视 3×3 移除雷格",
"effect_kinds": ["ap_grant", "mine_reveal", "cell_removal"],
"target": "area_3x3",
"magnitude": 4,
"use_phase": "active_dig",
"form": "active_charged",
"slot_cost": 1,
"charges_per_adventure": 2
}, {
"item_id": "nest_counter",
"display_name": "惊巢反制",
"dimension_tags": ["risk"],
"desc": "踩雷惩罚减半+反弹2",
"target": "none",
"magnitude": 2,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1,
"triggers": [{
"on": "mine_hit",
"do": {
"kind": "damage_mult",
"value": 50
}
}, {
"on": "mine_hit",
"do": {
"kind": "open_ticket",
"value_from": "magnitude",
"channel": "reflect"
}
}]
}, {
"item_id": "dawn_order",
"display_name": "工兵晨令",
"dimension_tags": ["action"],
"desc": "前3轮每轮AP+1",
"desc": "每场战斗第 1 轮 AP +1",
"tier": "common",
"target": "none",
"magnitude": 1,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1,
"effective_rounds": 3,
"unlock_keystone_cost": 10,
"triggers": [{
"on": "round_start",
"when": [{
"fact": "round_index",
"op": "lte",
"value_from": "effective_rounds"
"op": "eq",
"value": 1
}],
"do": {
"kind": "grant_ap",
@@ -121,64 +40,192 @@ rows = [{
}
}]
}, {
"item_id": "dig_charge",
"display_name": "掘进蓄能",
"item_id": "wide_scout_band",
"display_name": "阔视野带",
"dimension_tags": ["info"],
"desc": "类型微光范围 8 格→12 格",
"tier": "common",
"target": "none",
"magnitude": 4,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1,
"unlock_keystone_cost": 10,
"effect_kinds": ["light_radius_bonus"]
}, {
"item_id": "trial_pick",
"display_name": "试炼镐",
"dimension_tags": ["action"],
"desc": "积点换AP+1",
"desc": "每开 4 安全格积 1 点,满 2 点换当轮 AP +1",
"tier": "common",
"effect_kinds": ["ap_charge_safe"],
"target": "none",
"magnitude": 1,
"use_phase": "active_dig",
"form": "active_charged",
"slot_cost": 1,
"charges_per_adventure": 2,
"charge_n": 3,
"charges_per_adventure": 0,
"charge_n": 8,
"unlock_keystone_cost": 10,
"triggers": [{
"on": "treasure_used",
"do": {
"kind": "counter_spend",
"counter_id": "dig_charge_bank",
"counter_id": "trial_pick_bank",
"value": 1
}
}]
}, {
"item_id": "luck_coin",
"display_name": "偏运硬币",
"dimension_tags": ["risk"],
"desc": "稀有资源权重+2",
"target": "none",
"magnitude": 2,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1,
"triggers": [{
"on": "reward_roll",
"do": {
"kind": "bias_rewards",
"value_from": "magnitude"
}
}]
}, {
"item_id": "pick_refresh",
"display_name": "三选一刷新件",
"dimension_tags": ["info"],
"desc": "三选一刷新+1",
"effect_kinds": ["bonus_refresh"],
"item_id": "refresh_practice",
"display_name": "刷新练习",
"dimension_tags": ["plan"],
"desc": "三选一刷新存量 +1(装配即得一次性加仓)",
"tier": "common",
"target": "none",
"magnitude": 1,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1
"slot_cost": 1,
"unlock_keystone_cost": 10,
"effect_kinds": ["refresh_stock_grant"]
}, {
"item_id": "fog_lantern",
"display_name": "魔法提灯",
"item_id": "reserve_jar",
"display_name": "储备罐",
"dimension_tags": ["action"],
"desc": "停手转存率 1:1 → 1.25:1",
"tier": "common",
"target": "none",
"magnitude": 125,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1,
"unlock_keystone_cost": 10,
"effect_kinds": ["rap_transfer_mult"]
}, {
"item_id": "nest_forecast",
"display_name": "巢穴预告",
"dimension_tags": ["info"],
"desc": "灯光区半径+2",
"effect_kinds": [],
"desc": "装板后揭示本板危险格数量",
"tier": "common",
"target": "none",
"magnitude": 0,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1,
"triggers": []
"unlock_keystone_cost": 10,
"triggers": [{
"on": "board_installed",
"do": {
"kind": "mine_count_preview"
}
}]
}, {
"item_id": "incense_stick",
"display_name": "线香",
"dimension_tags": ["info"],
"desc": "每战一次:真视单格",
"tier": "common",
"effect_kinds": ["reveal_tiles"],
"target": "cell",
"magnitude": 0,
"use_phase": "active_dig",
"form": "active_charged",
"slot_cost": 1,
"charges_per_adventure": 1,
"charge_reset": "battle",
"unlock_keystone_cost": 10
}, {
"item_id": "twin_loader",
"display_name": "双管装填",
"dimension_tags": ["action"],
"desc": "每战前 2 轮各 AP +1",
"tier": "excellent",
"target": "none",
"magnitude": 1,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1,
"unlock_keystone_cost": 25,
"triggers": [{
"on": "round_start",
"when": [{
"fact": "round_index",
"op": "lte",
"value": 2
}],
"do": {
"kind": "grant_ap",
"mode": "queue",
"value_from": "magnitude"
}
}]
}, {
"item_id": "ranged_pick",
"display_name": "远程镐",
"dimension_tags": ["action"],
"desc": "挖掘距离 +1(可挖距英雄 2 格的格)",
"tier": "excellent",
"target": "none",
"magnitude": 1,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1,
"unlock_requires": [{
"key": "adventure_cleared",
"threshold": 1
}],
"effect_kinds": ["dig_range_bonus"]
}, {
"item_id": "refresh_license",
"display_name": "刷新执照",
"dimension_tags": ["plan"],
"desc": "商店免费刷新 +2/店",
"tier": "excellent",
"target": "none",
"magnitude": 2,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1,
"unlock_requires": [{
"key": "boss_kill_count",
"threshold": 3
}],
"effect_kinds": ["shop_refresh_bonus"]
}, {
"item_id": "sky_eye",
"display_name": "天眼",
"dimension_tags": ["info"],
"desc": "每关一次:选 3×3,危险格显示且安全格直接开",
"tier": "rare",
"effect_kinds": ["reveal_tiles", "safe_dig"],
"target": "area_3x3",
"magnitude": 0,
"use_phase": "active_dig",
"form": "active_charged",
"slot_cost": 1,
"charges_per_adventure": 1,
"charge_reset": "level",
"unlock_requires": [{
"key": "adventure_cleared",
"threshold": 3
}, {
"key": "boss_kill_count",
"threshold": 4
}]
}, {
"item_id": "beacon_network",
"display_name": "信标网络",
"dimension_tags": ["risk"],
"desc": "插旗不限相邻(可对任何未开格插旗)",
"tier": "rare",
"target": "none",
"magnitude": 1,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1,
"unlock_requires": [{
"key": "vision_used",
"threshold": 10
}],
"effect_kinds": ["flag_range_free"]
}]
+15 -6
View File
@@ -42,12 +42,12 @@ func _init(db_ref, meta_get: Callable, save_get: Callable, camp_overlay_ref: Cam
# 打开营地(主菜单入口——开合与开始层互斥仿设置面先例:开始覆盖层非模态且树序在后,
# 规范 2.3 遮挡纪律;打开时先刷工坊解锁——达标未解锁就地入账)
# 规范 2.3 遮挡纪律;打开时先刷条件解锁——达标未解锁就地入账)
func open_camp() -> void:
var meta = _meta_get.call()
if meta == null:
return
meta.refresh_workshop_unlocks()
meta.refresh_treasure_unlocks() # 迭代 22 T2——条件解锁刷新换代(旧工坊面退役)
start_overlay.visible = false
_open_modal.call(camp_overlay) # 模态栈单口(T3 收口批——登记层开合一律走栈)
camp_overlay.open_camp(camp_data())
@@ -61,13 +61,13 @@ func on_camp_closed() -> void:
_refresh_presentations.call() # 关闭后刷新顶栏(工坊解锁可能变了余额侧读数面)
# 营地快照投影(纯读——工坊六行/同调装配/树面/余额/最近冒险;拼句归 CampOverlay,账面归
# MetaProgression。属性锻造三行投影已删——迭代 21 T4 柜台退役)
# 营地快照投影(纯读——秘宝匣 12 件/同调装配/树面/余额/最近冒险;拼句归 CampOverlay,账面归
# MetaProgression。属性锻造三行投影已删——迭代 21 T4 柜台退役;工坊六行换代=迭代 22 T2 秘宝匣)
func camp_data() -> Dictionary:
var meta = _meta_get.call()
return {
"balance": meta.keystone_balance() if meta != null else 0,
"workshop": meta.workshop_status() if meta != null else [],
"workshop": meta.treasure_shop_status() if meta != null else [], # 键名沿 workshop(视图消费键不变——行内容=秘宝匣 12 件解锁投影)
"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 [],
@@ -79,9 +79,18 @@ func on_camp_buy(attr: String) -> void:
var meta = _meta_get.call()
if meta == null:
return
# 秘宝钥石购买(迭代 22 秘宝批 T4——秘宝匣柜台购买钮意图;写口唯一=buy_treasure_unlock,
# 余额闸/重复购/条件件误购三重拒绝零副作用)
if attr != "attune_refresh":
if not meta.buy_treasure_unlock(attr):
toast.show_toast("购买被拒(余额不足或不可购买)", "warn")
return
camp_overlay.refresh_camp(camp_data())
_refresh_presentations.call()
return
# Y 刷新升级行购买(迭代 11 第①批 T4——P1 甲;双入口之一 Q1 裁乙——树上 ks_refresh 同账;
# 按钮禁用为常态防线,兜底拒绝可见 pattern 1。属性锻造面已退役——buy_forge 分支随批删除)
if attr != "attune_refresh" or not meta.buy_attune_refresh():
if not meta.buy_attune_refresh():
toast.show_toast("购买被拒", "warn")
return
camp_overlay.refresh_camp(camp_data())
+13 -56
View File
@@ -5,7 +5,7 @@
# 装配纪律(ADR-0001,仿 treasure_interact_controller 先例):信号连接不在此发生——curio_overlay
# 四信号与 adventure 两信号由组合根接至本类 on_* 入口;模态栈/演播后排窗/呈现刷新/终局面/
# 推进失败 fatal 经组合根回调注入;adventure/runner 随局更替——现值回调注入;兑付写调用唯一
# 经 adventure.pick_curio/apply_node_curio/resolve_curio_replace(core 侧单口,本类零直写)。
# 经 adventure.pick_curio/apply_node_curio(core 侧单口,本类零直写;替换子交互随迭代 22 T5 拆除——装配不可变契约 1,冒险中零更换途径)。
extends RefCounted
const Narrator := preload("res://presentation/battle_narrator.gd")
@@ -29,8 +29,6 @@ var _gp_summary: Callable # () → 战斗总结 GP 行文案(组
var _pending_curio := {} # 三选一停点载荷暂存(迭代 10——战斗总结结算确认后开三选一面;续玩停点复用)
var _curio_context := "level_clear" # 三选一面上下文(level_clear=通关停点→推进下一关 / reward_node=奖励节点→回图选路)
var _last_replace_items: Array = [] # 替换子态续玩重建缓存(treasure_replace_presented 载荷)
var _last_replace_incoming := {} # 同上——incoming 记录
func _init(db_ref, meta_get: Callable, curio_overlay_ref: CurioPickOverlay, end_overlay_ref: BattleEndOverlay,
@@ -112,8 +110,7 @@ func on_presented(payload: Dictionary) -> void:
toast.show_toast("上关剩余 %d 件秘宝已随关卡结束清空" % int(payload.get("aux_cleared", 0)), "info") # 迭代 7 T4 清空反馈
_pending_curio = payload.duplicate(true)
_curio_context = "level_clear"
if bool(payload.get("replace_pending", false)) or bool(payload.get("refreshed", false)):
# 续玩:替换子交互在途中断——重建三选一面后直落替换子态(候选不重抽)
if bool(payload.get("refreshed", false)):
# 刷新重发(2026-10-06):refreshed 标直落选择面——不再重开胜利总结面
show_pick()
return
@@ -124,9 +121,8 @@ func on_presented(payload: Dictionary) -> void:
_refresh_presentations.call()
# 三选一面开出(战斗总结结算确认 / 续玩停点恢复;替换在途由 treasure_replace_presented 续发直落子态)
# 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;CurioPickOverlay 共用面,
# 道具件满位替换子交互同构)。原「秘宝投放渠道」三选一(迭代 7 过渡形态)随批退役。
# 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;CurioPickOverlay 共用面)。
# 原「秘宝投放渠道」三选一(迭代 7 过渡形态)随批退役;替换子交互随迭代 22 T5 拆除。
func present_node_curio() -> void:
var adventure = _adventure_get.call()
_curio_context = "reward_node"
@@ -141,16 +137,16 @@ func present_node_curio() -> void:
runner.continue_after_node()
return
_open_modal.call(curio_overlay)
curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(node_cands, node_draw.get("aux_detail", []) as Array))
adventure.present_node_curio(node_cands, node_draw.get("aux_detail", []) as Array)
curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(node_cands))
adventure.present_node_curio(node_cands)
_feed_refresh_display(adventure) # 迭代 11 第①批 T5b + 2026-10-06 对齐商店——刷新面投喂单点
# 三选一面开出(战斗总结结算确认 / 续玩停点恢复;替换在途由 treasure_replace_presented 续发直落子态)
# 三选一面开出(战斗总结结算确认 / 续玩停点恢复)
func show_pick() -> void:
var payload: Dictionary = _pending_curio
_open_modal.call(curio_overlay)
curio_overlay.open_pick(Narrator.curio_pick_header(payload), _curio_card_texts(payload.get("candidates", []) as Array, payload.get("aux_detail", []) as Array))
curio_overlay.open_pick(Narrator.curio_pick_header(payload), _curio_card_texts(payload.get("candidates", []) as Array))
var adventure = _adventure_get.call()
if adventure != null:
_feed_refresh_display(adventure) # 迭代 11 第①批 T5b + 2026-10-06 对齐商店——刷新面投喂单点
@@ -165,11 +161,10 @@ func _feed_refresh_display(adventure) -> void:
curio_overlay.set_refresh_display(adventure.attune_refresh_left(), cost, gp >= cost)
func _curio_card_texts(candidates: Array, treasure_detail: Array = []) -> Array:
func _curio_card_texts(candidates: Array) -> Array:
# TP0 令牌池——候选 = {"kind","tier"} 令牌字典;卡片 id = "kind|tier|feat_id" 复合键(同 kind
# 大小两档/同档多 relic 可同屏,kind 单键不构成唯一身份),确认回程拆键(on_pick_confirmed)
var out: Array = []
var treasure_i := 0
for tok in candidates:
var kind := String((tok as Dictionary).get("kind", ""))
var tier := String((tok as Dictionary).get("tier", ""))
@@ -181,10 +176,6 @@ func _curio_card_texts(candidates: Array, treasure_detail: Array = []) -> Array:
选定即开一次奇物三选一(T5 门票——拿到即开不进持有集)"
elif kind == "relic":
text = Narrator.feat_card_text(db.feat_row(rid))
elif kind == "treasure_item":
var iid := String(treasure_detail[treasure_i]) if treasure_i < treasure_detail.size() else ""
treasure_i += 1
text = Narrator.bonus_card_text(kind, tier, int(db.adventure.curio_value(kind, tier)), _treasure_card_line(iid))
else:
text = Narrator.bonus_card_text(kind, tier, int(db.adventure.curio_value(kind, tier)))
out.append({"id": cid, "text": text})
@@ -194,17 +185,6 @@ func _curio_card_texts(candidates: Array, treasure_detail: Array = []) -> Array:
# 道具件卡片行(掷定前置——具体道具名 + 效果摘要;空 iid = 无预掷回退态显示通用文案)。
# 内容数据驱动扩展批 T4 文案表值化:效果摘要=数据行 desc 字段直读(零 kind 硬编码映射——
# 新件写行即有名有姓)
func _treasure_card_line(iid: String) -> String:
if iid == "":
return ""
var row: Dictionary = db.treasure_item_row(iid)
if row.is_empty():
return ""
var desc := String(row.get("desc", ""))
if desc != "":
return "%s · %s" % [String(row.get("display_name", iid)), desc]
return String(row.get("display_name", iid))
# 三选一刷新(迭代 11 第①批 T5b X/Y 双源 + 2026-10-06 用户令对齐商店 MC-1——免费位用尽转 GP 付费档:
# 同 shop_refresh_cost_base/curve 旋钮、同冒险钱包 runner.gp;通关停点经重发 curio_choice_presented 直落
@@ -227,7 +207,7 @@ func on_refresh() -> void:
return
if _curio_context == "reward_node":
var st: Dictionary = adventure.node_curio_pending_state()
curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st.get("candidates", []) as Array, st.get("aux_detail", []) as Array))
curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st.get("candidates", []) as Array))
_feed_refresh_display(adventure)
@@ -251,30 +231,6 @@ func on_pick_confirmed(item_id: String) -> void:
curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st2.get("candidates", []) as Array))
func on_treasure_replace_presented(items: Array, incoming: Dictionary) -> void:
_last_replace_items = items.duplicate(true)
_last_replace_incoming = incoming.duplicate(true)
if not curio_overlay.visible:
_open_modal.call(curio_overlay) # 续玩替换在途直落——三选一面未开时先入栈(模态栈纪律)
_open_replace(items, incoming)
func _open_replace(items: Array, incoming: Dictionary) -> void:
var named: Array = []
for it in items:
var rec: Dictionary = it.duplicate()
rec["display_name"] = String(item_name_lookup().get(String(rec.get("item_id", "")), String(rec.get("item_id", ""))))
named.append(rec)
curio_overlay.open_replace(named, String(item_name_lookup().get(String(incoming.get("item_id", "")), String(incoming.get("item_id", "")))),
named.size(), Attunement.slots(db, _save_get.call())) # 迭代 11 第①批 T7——呈现读数源切谓词表求值(过渡键退役)
func on_replace_chosen(discard: int) -> void:
var adventure = _adventure_get.call()
if adventure == null or not adventure.resolve_curio_replace(discard):
push_error("[bonus_interact] 同调位替换受理被拒——见输出日志")
# 兑付反馈(curio_resolved 到达——结果行上屏,唯一出口 = 继续)
func on_resolved(result: Dictionary) -> void:
_pending_curio = {}
@@ -299,11 +255,12 @@ func on_continue() -> void:
_fatal_screen.call("关卡推进失败——见输出日志")
# 工坊 item_id → 显示名投影(keystone_settle_text/替换子命名文案数据源——拼句归 Narrator)
# 秘宝 item_id → 显示名投影(keystone_settle_text/替换子命名文案数据源——拼句归 Narrator);
# 迭代 22 T2 数据源换代:旧工坊行族退役,改读 treasure 装配词表 12 件
func item_name_lookup() -> Dictionary:
var lookup := {}
var meta = _meta_get.call()
if meta != null:
for row in db.meta_workshop_items():
for row in db.treasure_attunement_rows():
lookup[String(row.get("item_id", ""))] = String(row.get("display_name", ""))
return lookup
+39 -16
View File
@@ -235,6 +235,7 @@ func _build_ui() -> void:
info_panel.treasure_slot_clicked.connect(_treasure_ui.on_slot_clicked) # 迭代 7 T4——两步交互第一步
board_zone.stop_pressed.connect(_on_stop_pressed)
board_zone.refresh_confirmed.connect(_on_refresh_confirmed)
board_zone.convert_pressed.connect(_on_convert_pressed) # 积点兑换(迭代 22 秘宝批 T4——试炼镐 HUD 入口)
board_zone.legend_toggled.connect(_on_legend_toggled)
board_zone.hover_forbidden.connect(_on_hover_forbidden)
board_zone.chord_attempted.connect(_on_chord_attempted)
@@ -293,7 +294,6 @@ func _build_ui() -> void:
_open_modal, _open_modal_after_playback, _close_modal, _refresh_presentations,
_fatal_screen, func() -> String: return _gp_summary_text({"gp_dropped": _last_gain_amount}))
curio_overlay.pick_confirmed.connect(_bonus.on_pick_confirmed)
curio_overlay.replace_chosen.connect(_bonus.on_replace_chosen)
curio_overlay.continue_pressed.connect(_bonus.on_continue)
curio_overlay.refresh_requested.connect(_bonus.on_refresh)
move_child(curio_overlay, _end_overlay.get_index())
@@ -487,7 +487,6 @@ func _wire_adventure() -> void:
# 迭代 10 奖励重构批——big_reward_paid 退役,三选一信号族接线
adventure.curio_choice_presented.connect(_bonus.on_presented)
adventure.curio_resolved.connect(_bonus.on_resolved)
adventure.treasure_replace_presented.connect(_bonus.on_treasure_replace_presented)
adventure.adventure_finished.connect(_on_adventure_finished)
adventure.level_started.connect(_on_adventure_level_started)
adventure.set_save_manager(save_manager) # 断点承载注入(迭代 8 卡①——停点快照写盘;调试/模拟不经过此路径)
@@ -849,14 +848,15 @@ func _on_round_started(ri: int) -> void:
else:
_row_pacer.enqueue({"__round_line": ri})
settlement_fx.play_round_banner(ri)
# 提灯视野同步(迭代 19 卡④——呈现桥:每战首轮按装配集显式重算 set_fog_lantern 含否〔🔴 契约 7
# Q8.1 复原语义:禁依赖战斗结束隐式复原——下一战装配链重算即含「未装配→写 0」复原〕)。挂点时序
# 依据:round_started 首轮发射位在 begin_battle 尾(attunement 注入之后)——board_changed 在
# _install_new_board 内先发、装配集后注入,同帧直挂会读到上一战的旧集,故挂此处不挂 _on_board_changed。
# settlement 空防御照 :854 既有形态(接手先于本信号:冒险路径 board_changed 内 runner 同步、debug
# 路径 battle_created 先于 begin_battle)。未装配=false→写 0=缺省零扰动基线逐位不变。
# 秘宝呈现同步(迭代 22 秘宝批 T4 换代——旧提灯桥 _sync_fog_lantern 随行退役;每战首轮装配链
# 显式重算〔🔴 契约复原语义沿迭代 19 卡④ Q8.1:下一战装配链重算即含「未装配→写 0」复原〕。
# 挂点时序依据不变:round_started 首轮发射位在 begin_battle 尾(attunement 注入之后)。
# settlement 空防御照既有形态。未装配=参数 0→写 0=缺省零扰动基线逐位不变。)
if ri == 1:
_sync_fog_lantern()
_sync_treasure_glow()
# 巢穴预告呈现(迭代 22 秘宝批 T4——装板报数量读数口消费:≥0=本战装配命中,toast 一次性提示)
if settlement != null and int(settlement.nest_preview_count) >= 0:
board_zone.toast.show_toast("巢穴预告——本板危险格共 %d 个" % int(settlement.nest_preview_count), "info")
# 开战横幅——板级地形变异预告(全局作用域变异首发批 2026-10-04):首轮快照 board_variants
# 非空才发射(Boss 图恒带一型=替换制;普通图空快照零发射);轮横幅下方叠行,view 内判减动效。
if ri == 1 and settlement != null:
@@ -880,14 +880,14 @@ func _on_board_changed() -> void:
_refresh_presentations()
# 提灯视野呈现桥(迭代 19 卡④——装配集只读出口消费面:settlement.attune_has("fog_lantern") 真值
# 显式写给 board_view.set_fog_lantern(true=半径 8+2 / false=复原 0),写入即 setter 内 refresh 全板
# 重算雾态。调用点=每战首轮装配链(_on_round_started ri==1)——纯呈现边界 🔴 契约 2:只写呈现态布尔,
# minesweeper 结算零触碰;off 态(未装配)写 0=与既有基线逐位一致。
func _sync_fog_lantern() -> void:
# 秘宝视野呈现桥(迭代 22 秘宝批 T4 换代——旧提灯桥随 fog_lantern 行退役;阔视野带 light_radius_bonus
# 装配参数合计显式写给 board_view.set_fog_glow_bonus(装配=半径 8+4 / 未装配=复原 0),写入即 setter
# 内 refresh 全板重算雾态。调用点=每战首轮装配链(_on_round_started ri==1)——纯呈现边界 🔴 契约 2:
# 只写呈现态参数,minesweeper 结算零触碰;off 态(未装配)写 0=与既有基线逐位一致。
func _sync_treasure_glow() -> void:
if settlement == null or board_view == null:
return
board_view.set_fog_lantern(settlement.attune_has("fog_lantern"))
board_view.set_fog_glow_bonus(settlement.attune_param_sum("light_radius_bonus"))
# ---- 一次性非模态提示(每 run 一次——pattern 10)与诚实提示 ----
@@ -1018,7 +1018,7 @@ func _on_settings_closed() -> void:
func _pause_data() -> Dictionary:
return {
"balance": meta.keystone_balance(),
"workshop": meta.workshop_status(),
"workshop": meta.treasure_shop_status(), # 迭代 22 T2——秘宝匣 12 件解锁投影(旧工坊面退役)
"tree": meta.tree_camp_rows(),
"fork_groups": meta.tree_fork_groups(),
}
@@ -1233,9 +1233,32 @@ func _refresh_presentations() -> void:
# 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查)
var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_cap_per_round) - _refresh_used_round)
HudPresenter.refresh(stage, info_panel, ap_bar, settlement, runner, _level_active, db, refresh_remaining)
_sync_convert_button()
_sync_settlement_visuals()
# 积点兑换钮态(迭代 22 秘宝批 T4——读数口命令式重查;未装配=隐藏零占位)
func _sync_convert_button() -> void:
if settlement == null:
board_zone.ap_bar.set_convert_state(false, 0, 0, "")
return
var cs: Dictionary = settlement.attune_convert_state()
board_zone.ap_bar.set_convert_state(bool(cs.get("assembled", false)), int(cs.get("points", 0)),
int(cs.get("need", 0)), String(cs.get("display_name", "")))
func _on_convert_pressed() -> void:
if settlement == null:
return
var v: Dictionary = settlement.use_attune_convert("trial_pick")
if not bool(v.get("ok", false)):
board_zone.toast.show_toast(String(v.get("reason", "无法兑换")), "warn")
return
logger.log_action("convert", Vector2i(-1, -1), true)
board_view.refresh()
_refresh_presentations()
func _sync_settlement_visuals() -> void:
# 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开 84px
var settling := _is_settling()
+17
View File
@@ -13,6 +13,7 @@ extends PanelContainer
signal stop_pressed
signal refresh_confirmed
signal legend_toggled
signal convert_pressed # 积点兑换意图(迭代 22 秘宝批 T4——写口唯一=BattleOrchestrator.use_attune_convert)
const COL_GOLD := UiKit.GOLD
const COL_DOT_OFF := UiKit.DOT_OFF
@@ -21,6 +22,7 @@ const COL_INK := UiKit.INK
var _dots: HBoxContainer
var _ap_label: Label
var _btn_stop: Button
var _btn_convert: Button # 积点兑换钮(迭代 22 秘宝批 T4——试炼镐 HUD 入口;未装配隐藏)
var _btn_legend: Button
var _btn_refresh: Button
var _refresh_sub: Label
@@ -61,6 +63,12 @@ func _init() -> void:
_btn_stop.pressed.connect(_emit_stop_pressed)
row.add_child(_btn_stop)
_btn_convert = UiKit.button("兑换", 0.0) # 行板紧凑钮豁免——积点兑换(试炼镐;未装配隐藏)
_btn_convert.size_flags_vertical = Control.SIZE_SHRINK_CENTER
_btn_convert.visible = false
_btn_convert.pressed.connect(func(): convert_pressed.emit())
row.add_child(_btn_convert)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(spacer)
@@ -128,6 +136,15 @@ func set_refresh_cost(cost: int) -> void:
_confirm_lab.text = "RAP−%d · 旧板旗作废" % cost
# 积点兑换钮(迭代 22 秘宝批 T4——试炼镐转化获取 HUD 入口:装配时在列〔visible〕、
# 积点未达 disabled;文案=件名+进度。未装配=隐藏零占位)
func set_convert_state(assembled: bool, points: int, need: int, display_name: String) -> void:
_btn_convert.visible = assembled
if assembled:
_btn_convert.text = "%s %d/%d" % [display_name, points, need]
_btn_convert.disabled = points < need
func set_refresh_state(remaining: int, ready: bool) -> void:
_refresh_sub.text = "RAP−%d · 剩 %d 次" % [refresh_cost, remaining]
_btn_refresh.disabled = not ready
+7 -8
View File
@@ -136,16 +136,15 @@ var _digit_plate := false # 数字色牌垫底模式(双峰纹理
# ---- 视野迷雾(迭代 11 第④批 T2/T4——纯呈现层;正主=HUD框架「视野迷雾」节 v0.13,渲染规格=art-bible §6.5 v3.5)----
const FOG_RADIUS := 8 # 菱形灯光区基准半径(曼哈顿距离 ≤R——与微光环「周围 8 格」同圈起步;半径基准值改动=非白名单须用户)
const FOG_LANTERN_BONUS := 2 # 提灯视野外扩插值位(+2=2026-09-30 拍板值;载体=迭代 19 卡④ fog_lantern 秘宝行落籍 2026-10-08——原「归 Alpha 信息件批」指路句随批换代)
var _fog_enabled := true # 迷雾开关(呈现态布尔——真实游戏恒 on;探针/夹具可 off=无迷雾基线,两态结算逐位一致断言面)
var _fog_lantern := 0 # 提灯加成现值(缺省 0=无提灯基线零扰动;生产写入面=组合根每战装配链显式重算——main._sync_fog_lantern)
var _fog_lantern := 0 # 视野加成现值(缺省 0=无加成基线零扰动;生产写入面=组合根每战装配链显式重算——main._sync_treasure_glow)
# 提灯视野外扩接线(迭代14⑤——2026-09-30 拍板④:灯甲视野外扩 +2、类型微光不随视野扩〔微光锚=可挖域
# 贴身 8 邻零改动〕。FOG_LANTERN_BONUS 常量与半径算式迷雾批已在位,本 setter=公共接线面:载体道具
# 〔fog_lantern 魔法提灯——迭代 19 卡④落籍 2026-10-08,treasure_items.tres 秘宝·同调位新行〕已到位,
# 组合根 main._sync_fog_lantern 每战首轮装配链显式重算调用;缺省 0=无提灯基线零扰动。
func set_fog_lantern(enabled: bool) -> void:
_fog_lantern = FOG_LANTERN_BONUS if enabled else 0
# 视野加成接线(迭代 22 秘宝批 T4 换代——旧提灯布尔桥 set_fog_lantern 与 FOG_LANTERN_BONUS 常量
# 随 fog_lantern 行退役摘除〔V12〕;新主语=阔视野带 light_radius_bonus 装配参数(magnitude 4→半径
# 8+4=12 格 provisional);类型微光不随视野扩〔微光锚=可挖域贴身 8 邻零改动〕沿迭代 14⑤ 拍板。
# 组合根 main._sync_treasure_glow 每战首轮装配链显式重算调用;缺省 0=off 态与旧基线逐位一致。
func set_fog_glow_bonus(bonus: int) -> void:
_fog_lantern = maxi(0, bonus)
refresh() # 灯光区半径变了——全板重算雾态(refresh/notify_hero_move 既有重绘驱动复用)
var cell_size: float: # 只读属性(ui_layout_probe 断言面——旧私有名 _cell 的契约化出口)
@@ -18,6 +18,7 @@ var _board_area: Control # resized 命名转发用(原 lambda 局部捕获
signal action_requested(cell: Vector2i, kind: String)
signal stop_pressed
signal refresh_confirmed
signal convert_pressed # 积点兑换意图转发(迭代 22 秘宝批 T4——试炼镐 HUD 入口,源=ap_bar)
signal legend_toggled
signal hover_forbidden(cell: Vector2i)
signal chord_attempted(cell: Vector2i)
@@ -82,6 +83,7 @@ func _init() -> void:
board_view.chord_attempted.connect(_forward_chord)
ap_bar.stop_pressed.connect(_emit_stop_pressed)
ap_bar.refresh_confirmed.connect(_emit_refresh_confirmed)
ap_bar.convert_pressed.connect(_emit_convert_pressed)
ap_bar.legend_toggled.connect(_emit_legend_toggled)
@@ -105,6 +107,10 @@ func _emit_stop_pressed() -> void:
stop_pressed.emit()
func _emit_convert_pressed() -> void:
convert_pressed.emit()
func _emit_refresh_confirmed() -> void:
refresh_confirmed.emit()
+41 -17
View File
@@ -307,26 +307,35 @@ func refresh_camp(data: Dictionary) -> void:
var row: Dictionary = ws_by_id.get(String(card.item_id), {})
if row.is_empty():
continue
var th := int(row.get("threshold", 0))
var prog := int(row.get("progress", 0))
var unlocked := bool(row.get("unlocked", false))
card.progress_l.text = "累计通关 %d / %d 次" % [prog, th]
# 迭代 7 T5——失义件(显影尘/地穴全图卷 Alpha 重定)与退役件(探宝罗盘 2026-09-29 拍板①乙)呈现口径;
# 状态色随卡⑭ 亮板双色墨制(深墨族替代暖白族——亮板上暖白不可读)
# 秘宝匣进度列(迭代 22 T2——解锁混合制三形态:钥石购买价/条件进度拼接/已入手;
# 旧「累计通关 N/M 次」单一渠道文案随工坊行族退役)
var prog_text := ""
var mode := String(row.get("mode", ""))
if unlocked:
prog_text = "已入手"
elif mode == "keystone":
prog_text = "钥石 %d" % int(row.get("price", 0))
elif mode == "condition":
var parts: Array[String] = []
for req_v in (row.get("reqs", []) as Array):
var req: Dictionary = req_v as Dictionary
var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件"))
parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))])
prog_text = "、".join(parts)
card.progress_l.text = prog_text
# 状态色随卡⑭ 亮板双色墨制(深墨族替代暖白族——亮板上暖白不可读);失义/退役行随新目录退役
var st_text := "未解锁"
var st_col: Color = UiKit.STONE
if bool(row.get("retired", false)):
st_text = "已退役"
st_col = UiKit.STONE
elif bool(row.get("deferred", false)) and unlocked:
st_text = "已解锁 · 能力封存中"
st_col = UiKit.GOLD_INK # 尾裁微批a 2026-10-08——GOLD_DEEP 对行板 3.00:1 越带,换深金墨 5.84:1
elif unlocked:
if unlocked:
st_text = "已解锁"
st_col = UiKit.GOLD_INK # 同上
st_col = UiKit.GOLD_INK # 尾裁微批a 2026-10-08——GOLD_DEEP 对行板 3.00:1 越带,换深金墨 5.84:1
card.status_l.text = st_text
card.status_l.add_theme_color_override("font_color", st_col)
card.status_l.tooltip_text = st_text # 截断兜底(状态列 EXPAND_FILL 吸余宽——悬停看全文)
# 购买钮余额联动(迭代 22 秘宝批 T4——禁用态=余额不足,常态防线;pattern 1 兜底拒绝可见归 presenter)
if card.get("buy_btn") != null:
card.buy_btn.disabled = balance < int(card.get("price", 0))
# 同调装配段状态就地刷(迭代 11 第①批 T4——Slots 点亮/满位禁选/Y 升级行余额联动)
var att: Dictionary = data.get("attune", {})
if _attune_header_l != null:
@@ -339,7 +348,8 @@ func refresh_camp(data: Dictionary) -> void:
for card2 in _attune_btns:
var on: bool = eq_set.has(String(card2.item_id))
card2.btn.text = "卸下" if on else "装配"
card2.btn.disabled = (not on) and eq.size() >= slots_n # 满位禁选
# 满位禁选+未解锁禁装(迭代 22 T4——已装件恒可卸下;unlocked 快照随 rebuild 刷新)
card2.btn.disabled = (not on and eq.size() >= slots_n) or (not on and not bool(card2.get("unlocked", false)))
if _refresh_l != null:
var rf: Dictionary = att.get("refresh", {})
_refresh_l.text = "三选一刷新 +%d 次/冒险 · Lv%d" % [int(rf.get("charges", 0)), int(rf.get("level", 0))]
@@ -362,10 +372,12 @@ func _rebuild_workshop(data: Dictionary) -> void:
var line := HBoxContainer.new()
line.add_theme_constant_override("separation", 12)
card.add_child(line)
var name_l := UiKit.label(String(row.get("display_name", "")),UiKit.FS_HINT, true, UiKit.INK)
var tier_name := String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), ""))
var name_l := UiKit.label(String(row.get("display_name", "")) + ("·" + tier_name if tier_name != "" else ""),UiKit.FS_HINT, true, UiKit.INK)
name_l.custom_minimum_size = Vector2(132, 0)
name_l.clip_text = true
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
name_l.tooltip_text = String(row.get("desc", "")) # 件效果摘要悬停可见(分档呈现契约——槽位档位=名后缀)
line.add_child(name_l)
var progress_l := UiKit.label("",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # 尾裁微批a 2026-10-08——同行价格墨同案
progress_l.custom_minimum_size = Vector2(150, 0)
@@ -378,8 +390,15 @@ func _rebuild_workshop(data: Dictionary) -> void:
status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL
line.add_child(status_l)
# 钥石购买钮(迭代 22 秘宝批 T4——购买件且未解锁时在列;写口唯一=buy_treasure_unlock,
# 经 buy_requested(item_id) 信号走 presenter 分支;禁用态=余额不足)
var buy_btn: Button = null
if not bool(row.get("unlocked", false)) and String(row.get("mode", "")) == "keystone":
buy_btn = UiKit.button("购买 %d" % int(row.get("price", 0)), 0.0) # 行板紧凑钮豁免
buy_btn.pressed.connect(_emit_buy_requested.bind(String(row.get("item_id", ""))))
line.add_child(buy_btn)
_workshop_holder.add_child(card)
_workshop_rows.append({"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l})
_workshop_rows.append({"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l, "buy_btn": buy_btn, "price": int(row.get("price", 0)), "unlocked": bool(row.get("unlocked", false))})
_rebuild_attune(data)
@@ -402,12 +421,17 @@ func _rebuild_attune(data: Dictionary) -> void:
var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能",UiKit.FS_CAPTION, false, UiKit.STONE)
form_l.custom_minimum_size = Vector2(58, 0)
line.add_child(form_l)
# 档位标签(迭代 22 秘宝批 T4——槽位档位显示:件档=普通/优秀/稀有,装入须件档≤格档契约 2 的呈现半边)
var tier_l := UiKit.label(String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "—")),UiKit.FS_CAPTION, false, UiKit.STONE)
tier_l.custom_minimum_size = Vector2(36, 0)
line.add_child(tier_l)
var btn := UiKit.button("装配", 0.0) # 行板紧凑钮豁免
btn.pressed.connect(attune_toggled.emit.bind(iid))
btn.disabled = not bool(row.get("unlocked", false)) # 未解锁禁装(解锁为装配前置——toggle 写口同守卫)
line.add_child(btn)
_workshop_holder.add_child(line)
_attune_nodes.append(line)
_attune_btns.append({"item_id": iid, "btn": btn})
_attune_btns.append({"item_id": iid, "btn": btn, "unlocked": bool(row.get("unlocked", false)), "equipped": bool(row.get("equipped", false))})
var refresh_line := HBoxContainer.new()
refresh_line.add_theme_constant_override("separation", 6)
_refresh_l = UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE)
+7 -43
View File
@@ -1,20 +1,19 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Date: 2026-10-01(迭代 10 奖励重构批——level-clear.md v2 三选一选择面承载)
# 提升三选一覆盖层(模态):非末关通关的选择停点呈现——部分回血反馈 + 三卡点选 + 确认兑付 +
# 同调位替换子交互。高内聚边界:文案拼装归 Narrator(组合根喂入),选择意图经信号外发;
# 视图零核心引用。交互范式 = node_overlay choice_box 按钮族 + 选路两段式(点选→确认)复用,零新原语。
# 状态机:pick(三卡点选→确认)→ [replace(满位 N+1 选 N)] → result(兑付反馈→继续)。
# 提升三选一覆盖层(模态):非末关通关的选择停点呈现——部分回血反馈 + 三卡点选 + 确认兑付。
# 高内聚边界:文案拼装归 Narrator(组合根喂入),选择意图经信号外发;视图零核心引用。
# 交互范式 = node_overlay choice_box 按钮族 + 选路两段式(点选→确认)复用,零新原语。
# 状态机:pick(三卡点选→确认)→ result(兑付反馈→继续)。〔replace 满位替换子态随迭代 22 T5
# 拆除——装配不可变契约 1(冒险中零更换途径),core 写口与组合根接线同批摘除。〕
class_name CurioPickOverlay
extends Control
signal pick_confirmed(item_id: String)
signal replace_chosen(discard_index: int) # -1 = 放弃新件不获取
signal continue_pressed
signal refresh_requested # 三选一刷新意图(迭代 11 第①批 T5b——P1 甲 X/Y 双源;重抽归组合根)
var _label: Label
var _choice_box: HBoxContainer # 三卡区(pick 态——M7 bcard 竖卡)
var _replace_box: VBoxContainer # 替换子面板(replace 态)
var _confirm_btn: Button # 确认钮(点选后点亮——两段式第二拍)
var _continue_btn: Button # 继续钮(兑付反馈态唯一出口)
var _refresh_btn: Button # 刷新钮(迭代 11 第①批 T5b——余量>0 才显示;M7 升顶条图标钮)
@@ -33,7 +32,7 @@ func _init() -> void:
# T7(UI 设计对齐批 2026-10-06):全屏页形态(demo ⑥ #v-bonus——与商店 T6 同壳 page_shell,
# 根底暗档)——56px 通栏顶条(标题+刷新图标钮/角标)+ 居中入账信息行(GP 汇总行承载)+
# bcard 三竖卡(D1 232×352)+ 底部确认主钮与免费选择注记。差异白名单=免费选择/✓章/无价签。
# 状态机不变:pick→[replace]→result。M7 同构裁决遗产保留。
# 状态机不变:pick→result。M7 同构裁决遗产保留。
var page: Dictionary = UiKit.page_shell(self, false)
var top: HBoxContainer = page.top_row
top.add_child(UiKit.label("战利品三选一",UiKit.FS_TITLE, true, UiKit.WARM))
@@ -79,11 +78,6 @@ func _init() -> void:
foot.add_child(_continue_btn)
_foot_hint = UiKit.label("免费选择 · 刷新不消耗 AP",UiKit.FS_HINT, false, UiKit.WARM_FAINT)
foot.add_child(_foot_hint)
_replace_box = VBoxContainer.new()
_replace_box.add_theme_constant_override("separation", 8)
_replace_box.alignment = BoxContainer.ALIGNMENT_CENTER
_replace_box.size_flags_vertical = Control.SIZE_EXPAND_FILL
box.add_child(_replace_box)
visible = false
@@ -115,9 +109,8 @@ func open_pick(header: String, cards: Array) -> void:
_bcards.append(card)
_choice_box.visible = true
_confirm_btn.visible = false
_replace_box.visible = false
_continue_btn.visible = false
_foot_hint.visible = true # T7 底注=pick 态专属(replace/result 态让位)
_foot_hint.visible = true # T7 底注=pick 态专属(result 态让位)
_refresh_btn.visible = false # 刷新面随 set_refresh_display 投喂后现形(open 先归零防残留;店内门票面不投喂=无刷新)
_refresh_l.text = ""
visible = true
@@ -183,40 +176,15 @@ func show_result(text: String) -> void:
_label.text = _header + "\n" + text
_choice_box.visible = false
_confirm_btn.visible = false
_replace_box.visible = false
_continue_btn.visible = true
_refresh_btn.visible = false # 刷新=选择态专属功能(结果态停点已兑付——残钮零意图可点)
_foot_hint.visible = false
visible = true
# 同调位替换子态(treasure_replace_presented 到达):items = [{index, item_id, display_name}](组合根
# 喂显示名)。两路出口:丢旧收新(逐件钮)/ 放弃新件——终态恒 ≤ 同调位由核心受理面保证。
func open_replace(items: Array, incoming_name: String, holding: int, slots: int) -> void:
_label.text = "同调位已满(%d/%d)——收下「%s」需丢弃一件已有道具,或放弃新件" % [holding, slots, incoming_name]
for c in _replace_box.get_children():
c.queue_free()
for it in items:
var tag: int = int(it.get("index", it.get("handle", -1))) # 通关渠道=携带序号 / 节点渠道=库存句柄(核心分发面裁决)
var btn := UiKit.button("丢弃「%s」换新" % String(it.get("display_name", it.get("item_id", ""))))
btn.pressed.connect(_emit_replace_chosen.bind(tag))
_replace_box.add_child(btn)
var decline := UiKit.button("放弃新件(不获取)")
decline.pressed.connect(_emit_replace_chosen.bind(-1))
_replace_box.add_child(decline)
_choice_box.visible = false
_confirm_btn.visible = false
_replace_box.visible = true
_continue_btn.visible = false
_refresh_btn.visible = false # 刷新=选择态专属功能(替换子态不换候选)
_foot_hint.visible = false
visible = true
# 收屏(组合根在 continue_pressed 处理毕调——推进下一关同拍)
func close_pick() -> void:
_choice_box.visible = false
_replace_box.visible = false
_continue_btn.visible = false
_confirm_btn.visible = false
_refresh_btn.visible = false
@@ -228,10 +196,6 @@ func _emit_continue_pressed() -> void:
continue_pressed.emit()
func _emit_replace_chosen(tag: int) -> void:
replace_chosen.emit(tag)
# M7:id → 手绘图标键(子串映射容错 id 词表演进;缺位=BCard 空图标槽不炸)
func _icon_for_id(idl: String) -> String:
if idl.contains("heal"):
+16 -13
View File
@@ -82,6 +82,8 @@ func open_pause(data: Dictionary) -> void:
# 道具工坊六行(解锁态文案与营地柜台同口径——退役/封存/已解锁/未解锁;进度=解锁判定量读数)
# 秘宝匣 12 行只读概览(迭代 22 T2 换代——旧工坊六行退役;数据源=treasure_shop_status 投影,
# 解锁态文案与营地柜台同口径;进度列=解锁混合制三形态:钥石价/条件进度拼接/已入手)
func _rebuild_workshop(data: Dictionary) -> void:
for c in _workshop_holder.get_children():
c.queue_free()
@@ -93,28 +95,29 @@ func _rebuild_workshop(data: Dictionary) -> void:
name_l.clip_text = true
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
line.add_child(name_l)
var prog_l := UiKit.label("累计通关 %d/%d 次" % [int(row.get("progress", 0)), int(row.get("threshold", 0))],UiKit.FS_HINT, false, UiKit.INK_HINT)
var unlocked := bool(row.get("unlocked", false))
var prog_text := "已入手"
if not unlocked:
if String(row.get("mode", "")) == "keystone":
prog_text = "钥石 %d" % int(row.get("price", 0))
else:
var parts: Array[String] = []
for req_v in (row.get("reqs", []) as Array):
var req: Dictionary = req_v as Dictionary
var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件"))
parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))])
prog_text = "、".join(parts)
var prog_l := UiKit.label(prog_text,UiKit.FS_HINT, false, UiKit.INK_HINT)
prog_l.custom_minimum_size = Vector2(150, 0)
prog_l.clip_text = true
prog_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
line.add_child(prog_l)
var st_l := UiKit.label(_workshop_state_text(row),UiKit.FS_HINT, true, UiKit.INK_LABEL)
var st_l := UiKit.label("已解锁" if unlocked else "未解锁",UiKit.FS_HINT, true, UiKit.INK_LABEL)
st_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL
line.add_child(st_l)
_workshop_holder.add_child(line)
# 解锁态文案(camp_overlay_view.refresh_camp 同款判据——退役 > 封存 > 已解锁 > 未解锁)
func _workshop_state_text(row: Dictionary) -> String:
if bool(row.get("retired", false)):
return "已退役"
if bool(row.get("deferred", false)) and bool(row.get("unlocked", false)):
return "已解锁 · 能力封存中"
if bool(row.get("unlocked", false)):
return "已解锁"
return "未解锁"
# 天赋树已购概览(单条聚合文案——已购节点等级 + Fork 组激活侧;零已购=空态句)
func _rebuild_tree(data: Dictionary) -> void:
var owned: Array[String] = []
+6
View File
@@ -318,6 +318,12 @@ const FS_SUB := 18 # 副标题/小组标题(=棋盘数字下限同值)
const FS_TITLE := 24 # 屏标题/横幅/价格
const FS_H1 := 34 # 主标题/演算暴击
# ── 秘宝解锁条件计数键 → 呈现标签(迭代 22 秘宝批 T4——秘宝匣柜台条件进度文案单点;键域=
# TreasureItemTable.UNLOCK_COUNTER_KEYS 封闭枚举,键出域走「条件」兜底不炸)──
const TREASURE_REQ_LABELS := {"adventure_cleared": "通关", "boss_kill_count": "Boss击杀", "vision_used": "真视使用"}
# 秘宝档位 → 呈现名(契约 2 三档;legendary 传说空集合法不在柜)
const TREASURE_TIER_LABELS := {"common": "普通", "excellent": "优秀", "rare": "稀有", "legendary": "传说"}
# ── StyleBox 工厂 ──
# margin = (左, 上, 右, 下);border_w = 0 即无边框(根底/遮罩用)
# 圆角率单点(UI 设计对齐批 P5 甲 2026-10-06——容器/按钮全站唯一圆角值;数据框/数字牌/AP 点阵/准线/血条
+2 -12
View File
@@ -150,10 +150,9 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
adr.node_entered.connect(_on_adventure_node_entered.bind(adr, st, logger))
adr.wallet_changed.connect(_on_adventure_wallet.bind(st, logger))
# 三选一停点驱动(迭代 10 奖励重构批——big_reward_paid 退役:确定性策略候选取首件、
# 道具件满位替换放弃新件、兑付后推进下一关——零随机流自开,同种子逐位可复放)
# 兑付后推进下一关——零随机流自开,同种子逐位可复放;满位替换策略随迭代 22 T5 拆除)
adr.curio_choice_presented.connect(_on_adventure_bonus.bind(st, adr))
adr.curio_resolved.connect(_on_adventure_curio_resolved.bind(st, adr))
adr.treasure_replace_presented.connect(_on_adventure_treasure_replace.bind(st, adr))
adr.adventure_finished.connect(_on_adventure_finished.bind(st, logger))
adr.set_hero_override(atk_pct, hp_pct)
# 同调分层注入(迭代 11 第①批 T8——缺省 {} 与既有调用方逐位一致零行为变更)
@@ -226,7 +225,7 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr,
if ncands.is_empty():
r.continue_after_node() # 牌库空合法态直通(池耗即枯竭——防御保留)
else:
adr.present_node_curio(ncands, ndraw.get("aux_detail", []) as Array)
adr.present_node_curio(ncands)
var npick: Dictionary = _pick_token(ncands, String(st.get("feat_mode", "off")))
if adr.apply_node_curio(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("feat_id", ""))):
# T5 门票拿到即开——候选已被替换为专长三选一,就地再选一轮(确定性策略同款 316-f);
@@ -236,8 +235,6 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr,
if not fcands.is_empty():
var fpick: Dictionary = _pick_token(fcands, String(st.get("feat_mode", "off")))
adr.apply_node_curio(String(fpick.get("kind", "")), String(fpick.get("tier", "")), String(fpick.get("feat_id", "")))
if not adr._node_curio_pending.is_empty() and adr._node_curio_pending.has("incoming"):
adr.resolve_curio_replace(-1) # 满位替换:放弃新件
else:
st.result = "advance_failed"
elif t == "rest":
@@ -310,13 +307,6 @@ static func _pick_token(candidates: Array, feat_mode: String = "off") -> Diction
return best
# 同调位满位替换子交互(确定性策略:放弃新件不获取——分发面按停点上下文裁决节点/通关渠道)
static func _on_adventure_treasure_replace(items: Array, incoming: Dictionary, st: Dictionary, adr) -> void:
st.reward_events.append({"treasure_replace": incoming.duplicate(true)})
if not adr.resolve_curio_replace(-1):
st.result = "advance_failed"
# 三选一兑付完成 → 上下文分派(通关停点=推进下一关 / 奖励节点=回图选路)
static func _on_adventure_curio_resolved(result: Dictionary, st: Dictionary, adr) -> void:
st.reward_events.append({"curio_resolved": result.duplicate(true)})
+173 -95
View File
@@ -232,34 +232,96 @@ func _smoke_adventure_table() -> void:
"cap 占位在域(案②提前退役——99=实质无上限,实数归调参批)〔专长批 V8 形态面〕")
check(Attunement.slots(_db, null) == 0,
"同调位读数源=树同调位节点点亮数(迭代 21 T4 槽位收编——null 档求值 0;谓词表已退役)〔天赋树批 V14〕")
# 迭代 19 卡④——提灯载体行(fog_lantern):新行加载/schema/同调目录投影+装配→战斗贯通(核心层半边;
# 呈现半边归 ui_input_probe case⑮ 载体级段——测试三形态契约 9)
var lantern_row: Dictionary = _db.treasure_items.row_by_id("fog_lantern")
check(not lantern_row.is_empty() and String(lantern_row.get("display_name", "")) == "魔法提灯"
and String(lantern_row.get("desc", "")) == "灯光区半径+2"
and String(lantern_row.get("form", "")) == "passive" and int(lantern_row.get("slot_cost", -1)) == 1
and (lantern_row.get("dimension_tags", []) as Array) == ["info"]
and (lantern_row.get("effect_kinds", []) as Array).is_empty()
and lantern_row.has("triggers") and (lantern_row.get("triggers", []) as Array).is_empty()
and String(lantern_row.get("target", "")) == "none" and int(lantern_row.get("magnitude", -1)) == 0
and String(lantern_row.get("use_phase", "")) == "active_dig"
and not lantern_row.has("unlock_requires")
and not bool(lantern_row.get("retired", false)) and not bool(lantern_row.get("deferred", false)),
"提灯载体行 fog_lantern:行字段逐键=裁值(魔法提灯/灯光区半径+2/passive/slot_cost 1/[info]/空效果/显式 triggers 空数组/none/0/active_dig/unlock 键省略=已解锁/活行非封存)")
var att_lantern := {}
for r in _db.treasure_attunement_rows():
if String(r.get("item_id", "")) == "fog_lantern":
att_lantern = r
check(not att_lantern.is_empty() and String(att_lantern.get("display_name", "")) == "魔法提灯",
"同调目录投影:fog_lantern 在装配词表且 display_name=魔法提灯(营地工坊目录九件→十件)")
# 迭代 22 T1——新目录 12 件全量换代(V1/V2 断言落点;旧提灯段随 fog_lantern 行退役摘除,
# 装配→战斗贯通与空装配基线本体保留换主语 morning_order)
var trows: Array = _db.treasure_attunement_rows()
var tid_set := {}
for r in trows:
tid_set[String(r.get("item_id", ""))] = true
check(trows.size() == 12 and tid_set.size() == 12,
"新目录 12 件全量落表(旧 12 行删除;装配词表=活行 12 件零重复)〔秘宝批 V1〕")
var new_names := {"morning_order": "晨操令", "wide_scout_band": "阔视野带", "trial_pick": "试炼镐",
"refresh_practice": "刷新练习", "reserve_jar": "储备罐", "nest_forecast": "巢穴预告",
"incense_stick": "线香", "twin_loader": "双管装填", "ranged_pick": "远程镐",
"refresh_license": "刷新执照", "sky_eye": "天眼", "beacon_network": "信标网络"}
var names_ok := tid_set.size() == new_names.size()
for r in trows:
var iid2 := String(r.get("item_id", ""))
if not new_names.has(iid2) or String(r.get("display_name", "")) != String(new_names[iid2]):
names_ok = false
check(names_ok, "命名锁定:12 件 item_id 与 display_name 逐件=正式名(C2 代号即名)〔秘宝批 V2 初筛〕")
var keystone_ids := ["morning_order", "wide_scout_band", "trial_pick", "refresh_practice",
"reserve_jar", "nest_forecast", "incense_stick", "twin_loader"]
var cond_reqs := {"ranged_pick": {"adventure_cleared": 1}, "refresh_license": {"boss_kill_count": 3},
"sky_eye": {"adventure_cleared": 3, "boss_kill_count": 4}, "beacon_network": {"vision_used": 10}}
var tier_map := {"common": 0, "excellent": 0, "rare": 0}
var alloc_ok := trows.size() == 12
for r in trows:
var iid3 := String(r.get("item_id", ""))
var ureqs: Array = r.get("unlock_requires", []) as Array
if keystone_ids.has(iid3):
if int(r.get("unlock_keystone_cost", 0)) < 1 or not ureqs.is_empty():
alloc_ok = false
elif cond_reqs.has(iid3):
var want: Dictionary = cond_reqs[iid3]
if int(r.get("unlock_keystone_cost", 0)) != 0 or ureqs.size() != want.size():
alloc_ok = false
for req_v in ureqs:
var req: Dictionary = req_v as Dictionary
if int(want.get(String(req.get("key", "")), -1)) != int(req.get("threshold", -1)):
alloc_ok = false
else:
alloc_ok = false
var t: String = String(r.get("tier", ""))
if not tier_map.has(t):
alloc_ok = false
else:
tier_map[t] += 1
check(alloc_ok and tier_map["common"] == 7 and tier_map["excellent"] == 3 and tier_map["rare"] == 2,
"逐行解锁方式与契约 8 分配一致(钥石 8 件带价格、条件 4 件带谓词行互斥)+三档 7/3/2〔秘宝批 V1〕")
var orch_l = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(191))
check(orch_l.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "attunement": ["fog_lantern"]})
and orch_l.attune_has("fog_lantern"),
"装配→战斗贯通:enc.attunement=[fog_lantern] 起战 attune_has=true(注入面+只读出口)")
check(orch_l.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "attunement": ["morning_order"]})
and orch_l.attune_has("morning_order"),
"装配→战斗贯通:enc.attunement=[morning_order] 起战 attune_has=true(注入面+只读出口)〔秘宝批换主语〕")
var orch_e = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(192))
check(orch_e.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
and not orch_e.attune_has("fog_lantern"),
"空装配基线:不带 attunement 起战 attune_has=false(off 态零扰动)")
and not orch_e.attune_has("morning_order"),
"空装配基线:不带 attunement 起战 attune_has=false(off 态零扰动)〔秘宝批换主语〕")
# V9 静态词表断言(迭代 22 T6——棋盘事实不可变契约 3:新 12 行 triggers do.kind 与 effect_kinds
# 全集零棋盘改写类词〔rewrite_board 已除名/remove_mines/cell_removal=改写族——remove_mines 现役
# 语义=爆破工兵包旧通道随行退役,新表零引用〕;效果只允许信息呈现与开格动作)
var rewrite_words := ["rewrite_board", "remove_mines", "cell_removal"]
var v9_ok := true
for r9 in trows:
for k9 in (r9.get("effect_kinds", []) as Array):
if rewrite_words.has(String(k9)):
v9_ok = false
for tr9 in (r9.get("triggers", []) as Array):
if rewrite_words.has(String((tr9 as Dictionary).get("do", {}).get("kind", ""))):
v9_ok = false
check(v9_ok, "新 12 行效果词零棋盘改写类(危险格/安全格分布同池冻结——真视/开格动作外无写面)〔秘宝批 V9〕")
# 覆盖检查(迭代 22 T6——效果三方向覆盖:信息〔巢穴预告/线香/天眼/阔视野带〕/行动资源〔晨操令/
# 双管装填/试炼镐/储备罐〕/操作能力〔远程镐/信标网络〕+抉择刷新〔刷新练习/刷新执照〕——dimension_tags 静态投影)
var tag_set := {}
for r10 in trows:
for tg in (r10.get("dimension_tags", []) as Array):
tag_set[String(tg)] = true
check(tag_set.has("info") and tag_set.has("action") and tag_set.has("plan"),
"效果三方向覆盖在册(信息/行动资源/操作能力=plan·抉择刷新——12 件目录结构对齐设计权威)〔秘宝批 V10 覆盖半〕")
# V18 装配面空目录合法态(迭代 22 T6——白板档全未解锁:柜台投影 12 行全未解锁+mode 分派正确;
# 池空兜底/商店空货架两部分归奇物/商店侧另批〔卡片 §4〕)
var mp_v18 = load("res://core/meta_progression.gd").new(_db, _meta_new_save(_META_SMOKE_PATHS[12]))
var v18_ok := true
var locked_n := 0
for st18 in mp_v18.treasure_shop_status():
if bool(st18.get("unlocked", false)):
v18_ok = false
else:
locked_n += 1
if (String(st18.get("mode", "")) != "keystone" and String(st18.get("mode", "")) != "condition"):
v18_ok = false
check(v18_ok and locked_n == 12,
"装配面空目录合法态:白板档 12 件全未解锁且 mode 分派正确(空目录=合法可玩态)〔秘宝批 V18〕")
# ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 入口停点 / 放弃两态)
@@ -574,8 +636,8 @@ func _smoke_adventure() -> void:
# ⑪ 池耗即枯竭 + 池收缩 + 满血回 0(TP0——white-box:耗尽 attack/ap 令牌后候选零出现;
# 种类次数上限退役,「达上限剔除」语义由牌库耗尽承载)
var a11 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(11))
check(a11.start_adventure(11, {"first_script": true, "treasure_unlocked": {"blast_kit": true}}),
"剔牌库/替换场景:冒险启动(seed 11——道具解锁集只含 blast_kit)")
check(a11.start_adventure(11, {"first_script": true, "treasure_unlocked": {"wide_scout_band": true}}),
"剔牌库/替换场景:冒险启动(seed 11——道具解锁集只含 wide_scout_band)")
for di in range(a11._curio_deck.size() - 1, -1, -1):
var dk := String((a11._curio_deck[di] as Dictionary).get("kind", ""))
if dk == "attack" or dk == "ap":
@@ -608,7 +670,7 @@ func _smoke_adventure() -> void:
"满血通关回血 0 合法(封顶口径——heal_gained=0 非负溢出)")
# ⑬ 奖励节点共享池(迭代 10 追加批——同库同池同摘牌;TP0:attack 令牌耗尽后节点域同样零出现)
var a13 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(13))
check(a13.start_adventure(13, {"first_script": true, "treasure_unlocked": {"blast_kit": true}}),
check(a13.start_adventure(13, {"first_script": true, "treasure_unlocked": {"wide_scout_band": true}}),
"节点共享池场景:冒险启动(seed 13)")
for di13 in range(a13._curio_deck.size() - 1, -1, -1):
if String((a13._curio_deck[di13] as Dictionary).get("kind", "")) == "attack":
@@ -622,7 +684,7 @@ func _smoke_adventure() -> void:
break
check(not has_attack13,
"奖励节点共享池:令牌耗尽同样不出候选(同库同池——%s)" % str(ncands13))
a13.present_node_curio(ncands13, ndraw.get("aux_detail", []) as Array)
a13.present_node_curio(ncands13)
a13.present_node_curio([{"kind": "gp", "tier": "common"}])
var gp13 := int(a13.ledger.get("gp", 0))
check(a13.apply_node_curio("gp", "common") and int(a13.ledger.get("gp", 0)) == gp13 + int(_db.adventure.curio_value("gp", "common")),
@@ -723,7 +785,7 @@ func _smoke_feats() -> void:
var stock_before := {}
for kk in ["attack", "gp", "heal"]:
stock_before[kk] = a_t.pool_stock(kk)
a_t._node_curio_pending = {"candidates": [{"kind": "feat_ticket", "tier": "excellent"}, {"kind": "attack", "tier": "common"}], "aux_detail": []}
a_t._node_curio_pending = {"candidates": [{"kind": "feat_ticket", "tier": "excellent"}, {"kind": "attack", "tier": "common"}]}
check(a_t.apply_node_curio("feat_ticket", "excellent"), "A5 门票兑付受理(节点渠道——拿到即开)")
var tkt_cands: Array = (a_t.node_curio_pending_state().get("candidates", []) as Array)
var tkt_all_relic := tkt_cands.size() > 0
@@ -1013,11 +1075,9 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool:
var ndraw: Dictionary = a.node_curio_candidates()
var ncands: Array = ndraw.get("candidates", []) as Array
if not ncands.is_empty():
a.present_node_curio(ncands, ndraw.get("aux_detail", []) as Array)
a.present_node_curio(ncands)
var npick: Dictionary = autoplay._pick_token(ncands)
if a.apply_node_curio(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("feat_id", ""))):
if not a._node_curio_pending.is_empty() and a._node_curio_pending.has("incoming"):
a.resolve_curio_replace(-1)
a.apply_node_curio(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("feat_id", "")))
r.continue_after_node()
elif t == "rest":
r.continue_after_node()
@@ -4332,10 +4392,10 @@ func _smoke_meta() -> void:
print("--- 元进度·MetaTable 表结构 ---")
var meta_tbl = _db.meta
check(meta_tbl != null and meta_tbl.validate().is_empty(),
"MetaTable 非空 + validate 零错(四行族词表封闭/唯一/连续/域界/first≥repeat/ratio 全过)")
"MetaTable 非空 + validate 零错(三行族词表封闭/唯一/连续/域界/first≥repeat/ratio 全过)")
check(meta_tbl.boss_keystone.size() == 3
and meta_tbl.workshop_items.size() == 6 and meta_tbl.conversion.size() == 3,
"MetaTable 行族规模 3/6/3(Boss/工坊恰六/换算恰三——forge_products 行族已随迭代 21 T5 退役)")
and meta_tbl.conversion.size() == 3 and meta_tbl.attune_refresh_upgrade.size() == 2,
"MetaTable 行族规模 3/3/2(Boss/换算恰三/Y 升级两档——forge_products 迭代 21 退役、workshop_items 迭代 22 退役)")
var attrs_ok := true
for a in ["max_hp", "atk", "armor_class"]:
attrs_ok = attrs_ok and not _db.meta_conversion_row(a).is_empty()
@@ -4360,22 +4420,21 @@ func _pause_overlay_projection() -> void:
_meta_smoke_cleanup()
var sv = _meta_new_save(_META_SMOKE_PATHS[0])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
# 已购态夹具:余额非零 + 工坊首件解锁——投影可辨非全零白板(属性锻造段已随柜台退役删除)
# 已购态夹具:余额非零 + 秘宝匣首件解锁——投影可辨非全零白板(属性锻造段已随柜台退役删除)
sv.add_keystone(37)
var ws_meta: Array = _db.meta_workshop_items()
if not ws_meta.is_empty():
sv.unlock_item(String(ws_meta[0].get("item_id", "")))
sv.unlock_item("morning_order")
sv.write_to_disk()
# 投影拼装(main._pause_data 同构)+ 视图喂入
var data := {"balance": mp.keystone_balance(), "workshop": mp.workshop_status(),
var data := {"balance": mp.keystone_balance(), "workshop": mp.treasure_shop_status(),
"tree": mp.tree_camp_rows(), "fork_groups": mp.tree_fork_groups()}
var po = load("res://presentation/views/pause_overlay.gd").new()
po.open_pause(data)
check(po.visible, "暂停层开层可见(open_pause 直开——P4 甲零动效路径)")
check(String(po._balance_badge.value_label.text) == "钥石 %d" % mp.keystone_balance(),
"余额徽投影 = keystone_balance() 读数(%s)" % po._balance_badge.value_label.text)
var ws: Array = mp.workshop_status()
var ws: Array = mp.treasure_shop_status()
var ws_ok: bool = po._workshop_holder.get_child_count() == ws.size()
var uikit_s = load("res://presentation/views/ui_kit.gd")
for i in mini(ws.size(), po._workshop_holder.get_child_count()):
var row: Dictionary = ws[i]
var lbls2: Array = []
@@ -4383,18 +4442,23 @@ func _pause_overlay_projection() -> void:
if ch is Label:
lbls2.append(ch)
var unlocked: bool = bool(row.get("unlocked", false))
var expect_st := "未解锁"
if bool(row.get("retired", false)):
expect_st = "已退役"
elif bool(row.get("deferred", false)) and unlocked:
expect_st = "已解锁 · 能力封存中"
elif unlocked:
expect_st = "已解锁"
var expect_prog := "已入手"
if not unlocked:
if String(row.get("mode", "")) == "keystone":
expect_prog = "钥石 %d" % int(row.get("price", 0))
elif String(row.get("mode", "")) == "condition":
var parts: Array[String] = []
for req_v2 in (row.get("reqs", []) as Array):
var req2: Dictionary = req_v2 as Dictionary
var lbl2 := String(uikit_s.TREASURE_REQ_LABELS.get(String(req2.get("key", "")), "条件"))
parts.append("%s %d/%d" % [lbl2, int(req2.get("progress", 0)), int(req2.get("threshold", 0))])
expect_prog = "、".join(parts)
var expect_st2 := "已解锁" if unlocked else "未解锁"
ws_ok = ws_ok and lbls2.size() == 3 \
and String(lbls2[0].text) == String(row.get("display_name", "")) \
and String(lbls2[1].text) == "累计通关 %d/%d 次" % [int(row.get("progress", 0)), int(row.get("threshold", 0))] \
and String(lbls2[2].text) == expect_st
check(ws_ok, "工坊六行投影 = workshop_status() 逐项(名称/判定量进度/解锁态——首件解锁夹具在列)")
and String(lbls2[1].text) == expect_prog \
and String(lbls2[2].text) == expect_st2
check(ws_ok, "秘宝匣 12 行投影 = treasure_shop_status() 逐项(名称/钥石价与条件进度/解锁态——首件解锁夹具在列)〔秘宝批 V19 呈现数据源〕")
var owned := 0
for trow in (mp.tree_camp_rows() as Array):
if int(trow.get("level", 0)) > 0:
@@ -4480,7 +4544,7 @@ 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_career_v5.json", "user://smoke_meta_treasure_unlock.json", "user://smoke_meta_treasure_blank.json"]
func _meta_smoke_cleanup() -> void:
@@ -4611,32 +4675,42 @@ func _meta_settle_and_workshop() -> void:
"stopped_at": 1, "keystone_total": 0, "master_seed": 15})
check(dz["boss_drops"].is_empty() and sv.keystone_balance() == bal_before and sv.clear_count(1) == cc1_before,
"败北零通关摘要:零入账零标记(钥石只增不减——验收1;败北局 clear_counts 不变)")
# ⑧ 工坊解锁:settle 内已随 refresh——总通关次数达阈值件数 == unlocked 数;重复 refresh 幂等
var total_clears: int = sv.clear_count(1) + sv.clear_count(2) + sv.clear_count(3)
var expect_n := 0
for row in _db.meta_workshop_items():
if total_clears >= int(row.get("clear_threshold", 99)):
expect_n += 1
var got_n := 0
for st_row in mp.workshop_status():
got_n += 1 if bool(st_row.get("unlocked", false)) else 0
check(got_n == expect_n and mp.refresh_workshop_unlocks().is_empty(),
"工坊解锁数 = 总通关 %d 次达阈值件数(%d 件)且重复 refresh 幂等" % [total_clears, expect_n])
# 渠道可替换轴(2026-09-21 用户拍板):判定量单点正控 + 未知渠道 fail-safe(恒 0 = 永不解锁,非致命)
check(mp._workshop_progress("total_clears") == total_clears and mp._workshop_progress("adventure_depth") == 0,
"解锁判定量单点:total_clears 正控 + 未知渠道恒 0(channel 可替换轴 fail-safe)")
var sv_r = _meta_new_save(_META_SMOKE_PATHS[2])
# ⑧ 秘宝解锁服务(迭代 22 T2——V3 钥石购买/V4 条件解锁;注入=直调解锁写口+直置存档生涯计数,
# 不走正常资源积累流程——卡片 7.1 测试方式;旧工坊渠道段随行族退役摘除)
var sv_u = _meta_new_save(_META_SMOKE_PATHS[11])
var mp_u = load("res://core/meta_progression.gd").new(_db, sv_u)
sv_u.add_keystone(10)
check(not mp_u.buy_treasure_unlock("sky_eye") and not sv_u.item_unlocked("sky_eye")
and not mp_u.buy_treasure_unlock("ranged_pick"),
"条件件误购拒绝(购买件专属——契约 8 分配面)〔秘宝批 V3 反例〕")
check(not mp_u.buy_treasure_unlock("twin_loader") and sv_u.keystone_balance() == 10,
"余额不足购买拒绝且零副作用(余额 10 < 价 25)〔秘宝批 V3〕")
check(mp_u.buy_treasure_unlock("morning_order") and sv_u.item_unlocked("morning_order")
and sv_u.keystone_balance() == 0,
"钥石购买解锁:余额足→解锁态落档+余额扣减正确(价 10)〔秘宝批 V3〕")
check(not mp_u.buy_treasure_unlock("morning_order"),
"已解锁重复购买拒绝〔秘宝批 V3〕")
check(mp_u.refresh_treasure_unlocks().is_empty() and not sv_u.item_unlocked("ranged_pick"),
"条件未达不解锁(远程镐=通关 1 次)〔秘宝批 V4〕")
sv_u._data["meta"]["adventure_cleared"] = 1 # 直置存档生涯计数(调试注入口径)
check((mp_u.refresh_treasure_unlocks() as Array).has("ranged_pick")
and sv_u.item_unlocked("ranged_pick") and mp_u.refresh_treasure_unlocks().is_empty(),
"远程镐=adventure_cleared≥1 达标恰解锁一次+重复刷新幂等(C6:通关=打通整局冒险)〔秘宝批 V4〕")
sv_u._data["meta"]["boss_kill_count"] = 3
check((mp_u.refresh_treasure_unlocks() as Array).has("refresh_license"),
"刷新执照=boss_kill_count≥3(阈值读 unlock_requires 行值不硬编)〔秘宝批 V4〕")
sv_u._data["meta"]["adventure_cleared"] = 3
check(not sv_u.item_unlocked("sky_eye"), "天眼单条件不足不解锁(AND 语义——cleared 3 达 boss 4 未达)〔秘宝批 V4〕")
sv_u._data["meta"]["boss_kill_count"] = 4
check((mp_u.refresh_treasure_unlocks() as Array).has("sky_eye"),
"天眼双条件 AND(通关≥3 且 Boss≥4)达标解锁〔秘宝批 V4〕")
sv_u._data["meta"]["vision_used"] = 10
check((mp_u.refresh_treasure_unlocks() as Array).has("beacon_network"),
"信标网络=真视类工具累计使用≥10(行为计数新域——use_treasure 受理成功累计)〔秘宝批 V4〕")
var r_n := 0
for st_row in load("res://core/meta_progression.gd").new(_db, sv_r).workshop_status():
for st_row in mp_u.treasure_shop_status():
r_n += 1 if bool(st_row.get("unlocked", false)) else 0
check(r_n == expect_n, "工坊解锁持久(reload 后一致)")
# ⑧ 干净档:通关 1 次 → 恰两件阈值 1 道具新解锁(new_unlocks 明细)
var sv_w = _meta_new_save(_META_SMOKE_PATHS[3])
var mp_w = load("res://core/meta_progression.gd").new(_db, sv_w)
var u1: Dictionary = mp_w.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": 0, "master_seed": 16})
check(u1["new_unlocks"].size() == 2 and sv_w.item_unlocked("probe_dust") and sv_w.item_unlocked("sapper_order")
and not sv_w.item_unlocked("loot_compass"), "通关 1 次 → 恰两件阈值 1 道具解锁(new_unlocks 明细正确)")
check(r_n == 5, "解锁态持久投影一致(reload 面同源读数;5 件=购买 1+条件 4)〔秘宝批 V4〕")
# ⑩ recent_summaries:元素含 boss_drops 副本、最新在前、cap 10
var rs: Array = sv.recent_summaries()
check(rs.size() >= 5 and String(rs[0].get("result", "")) != "" and rs[0].has("boss_drops")
@@ -4654,9 +4728,11 @@ func _meta_settle_and_workshop() -> void:
var mp_a = load("res://core/meta_progression.gd").new(_db, sv_a)
check(Attunement.slots(_db, sv_a) == 0 and int(mp_a.attune_camp_status()["refresh"]["charges"]) == 0,
"白板档同调位求值 = 0(0 起步裁决)且 Y=0(未购无刷新能力——P1 甲初始 0 同构)")
check(mp_a.toggle_attunement("nest_counter") == false,
"满位禁选(Slots 0 时装配拒绝)")
check(mp_a.toggle_attunement("morning_order") == false,
"满位禁选(Slots 0 时装配拒绝;兼未解锁拒绝前置——直置解锁后下方装配)〔秘宝批 V5〕")
sv_a.add_keystone(200)
sv_a.unlock_item("morning_order")
sv_a.unlock_item("twin_loader")
check(mp_a.buy_attune_refresh() and sv_a.attune_refresh_level() == 1
and int(mp_a.attune_camp_status()["refresh"]["charges"]) == 1,
"Y 升级购买入账(Lv1 → 每冒险 +1 次刷新)")
@@ -4667,23 +4743,25 @@ func _meta_settle_and_workshop() -> void:
sv_a.set_tree_level("att_socket1", 1)
check(Attunement.slots(_db, sv_a) == 1,
"树上点亮同调位 1 → Slots=1(槽位收编树节点——Q2 裁甲)")
check(mp_a.toggle_attunement("nest_counter") and mp_a.toggle_attunement("dawn_order") == false,
"装配持久写入 + 满位禁选(Slots 1 装第二件拒绝)")
check(mp_a.toggle_attunement("nest_counter") and mp_a.toggle_attunement("dawn_order"),
"卸下恒可再装配(换装语义——装配序数组承载)")
check(not mp_a.toggle_attunement("sky_eye"),
"稀有件入普通格拒绝(件档 rank 3 > 装入位序 1——契约 2 档位格校验)〔秘宝批 V5〕")
check(mp_a.toggle_attunement("morning_order") and mp_a.toggle_attunement("twin_loader") == false,
"装配持久写入 + 满位禁选(Slots 1 装第二件拒绝)〔秘宝批 V5 换主语〕")
check(mp_a.toggle_attunement("morning_order") and mp_a.toggle_attunement("morning_order"),
"卸下恒可再装配(自卸自装——营地换装=冒险开始前合法面,装配不可变只锁运行中)〔秘宝批换主语〕")
var sv_a2 = _meta_new_save(_META_SMOKE_PATHS[7])
sv_a2.set_attunement_equipped(["luck_coin"])
sv_a2.set_attunement_equipped(["morning_order"])
var mp_a2 = load("res://core/meta_progression.gd").new(_db, sv_a2)
check((mp_a2.attune_camp_status()["equipped"] as Array).has("luck_coin"),
"装配关系读回持久(reload 后投影一致)")
check((mp_a2.attune_camp_status()["equipped"] as Array).has("morning_order"),
"装配关系读回持久(reload 后投影一致——写新件 id)〔秘宝批 V15〕")
# ⑫ 充能重置 / 刷新双计数器(迭代 11 第①批;clamp white-box 断言随 attunement_growth 谓词表
# 退役摘除——迭代 21 T5,V20 摘除清单)
var a12 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(12))
check(a12.start_adventure(12, {"attunement": ["dig_charge", "nest_pendulum"]}),
"充能重置场景:装配冒险启动(seed 12)")
check(int(a12.attune_charges.get("dig_charge", -1)) == 2 and int(a12.attune_charges.get("nest_pendulum", -1)) == 2,
"充能账本按装备 active_charged 件重置(每冒险 charges_per_adventure 表值)")
a12._attunement = ["pick_refresh"]
check(a12.start_adventure(12, {"attunement": ["incense_stick", "sky_eye"]}),
"充能重置场景:装配冒险启动(seed 12)〔秘宝批 V8 换主语〕")
check(int(a12.attune_charges.get("incense_stick", -1)) == 1 and int(a12.attune_charges.get("sky_eye", -1)) == 1,
"充能账本按装备 active_charged 件重置(新目录表值:线香 battle 级 1 次/天眼 level 级 1 次——周期重置点归 V8 逐形态面)")
a12._attunement = []
a12._attune_refresh_x = 1
a12._attune_refresh_y = 2
a12._curio_pending = {"candidates": [{"kind": "gp", "tier": "common"}, {"kind": "attack", "tier": "common"}, {"kind": "max_hp", "tier": "common"}], "payload": {}}
@@ -4693,13 +4771,13 @@ func _meta_settle_and_workshop() -> void:
and a12.refresh_curio() == false,
"刷新双计数器——X 耗尽转 Y 池、池竭拒绝(x0y2 → y0 → 第四次 false 零副作用)")
a12._reset_refresh_x()
check(a12.attune_refresh_left() == 1,
"刷新 X 每碰三选一重置(不结转——重碰面 x 回表值 1)")
check(a12._attune_refresh_x == 0 and a12.attune_refresh_left() == 0,
"刷新 X 每碰三选一重置(不结转;新目录无 bonus_refresh 行源——X 机制保留恒 0 空转,存量加仓走 _refresh_stock=刷新练习 V8)〔秘宝批换代〕")
# ⑬ 主菜单开局传参面(迭代 19 卡⓪①③——开新/续玩两入口同调三键随开局下传防回归:真实入口此前
# 三键恒空(缺陷①),冒烟夹具直呼 runner 参数面拦不住——此处直呼 main.build_adventure_params
# 单测统一构造面(static 免实例化整菜单场景;真实两入口共用本构造),取值与营地投影同源)
var sv_e = _meta_new_save(_META_SMOKE_PATHS[8])
sv_e.set_attunement_equipped(["luck_coin"])
sv_e.set_attunement_equipped(["morning_order"])
sv_e.set_attune_refresh_level(2)
sv_e.set_fork_side("fg_posture", "a")
var mp_e = load("res://core/meta_progression.gd").new(_db, sv_e)
+1 -1
View File
@@ -34,7 +34,7 @@ func _ready() -> void:
main.save_manager.set_breakpoint({
"master_seed": 42, "rng_counter": 0, "hero_hp": 20, "gp": 24, "keystone": 4,
"level_index": 1, "bonus": {}, "shop_purchased": {}, "relics": [],
"bonus_aux_carry": [], "keystone_drops": [], "bonus_taken": {},
"keystone_drops": [], "bonus_taken": {},
})
main._refresh_resume_button()
await get_tree().process_frame
+5 -9
View File
@@ -262,14 +262,12 @@ func _ready() -> void:
continue
if main.curio_overlay.visible:
# 迭代 10 三选一停点(CurioPickOverlay——奖励节点/通关兑付共用面;T17 硬锁批适配——
# 本探针此前未适配该层,走到即卡死)。三态分派:替换子态(满位——放弃新件)/
# result 态(兑付反馈——点继续推进)/ 待选态(点首件+确认)。真实信号路径全线。
# T1 D1——三选一停点输入锁定断言(停点态断言面:替换子/待选/兑付三态共用此层)。
# 本探针此前未适配该层,走到即卡死)。两态分派:result 态(兑付反馈——点继续推进)/
# 待选态(点首件+确认)。真实信号路径全线。〔替换子态随迭代 22 T5 拆除——装配不可变契约。〕
# T1 D1——三选一停点输入锁定断言(停点态断言面:待选/兑付两态共用此层)。
if not main._input_locked():
_problems.append("三选一停点打开时输入未锁定")
if main.curio_overlay._replace_box.visible:
main.curio_overlay.replace_chosen.emit(-1)
elif main.curio_overlay._continue_btn.visible:
if main.curio_overlay._continue_btn.visible:
main.curio_overlay.press_continue()
elif main.adventure != null and not main.adventure._curio_pending.is_empty():
main.curio_overlay.press_card(0)
@@ -409,9 +407,7 @@ func _ready() -> void:
else:
break
elif main.curio_overlay.visible:
if main.curio_overlay._replace_box.visible:
main.curio_overlay.replace_chosen.emit(-1)
elif main.curio_overlay._continue_btn.visible:
if main.curio_overlay._continue_btn.visible:
main.curio_overlay.press_continue()
elif main.adventure != null and not main.adventure._curio_pending.is_empty():
main.curio_overlay.press_card(0)
+30 -28
View File
@@ -328,9 +328,9 @@ func _ready() -> void:
# ---- case⑮ 视野迷雾(迭代 11 第④批 T5——三态呈态/半径圈/移动跟随/开关两态零扰动/提灯插值位;
# 纯呈现零逻辑——迷雾开关切换前后 minesweeper 状态摘要逐位一致) ----
var fogp: Dictionary = bv.fog_params_probe()
if int(bv.FOG_RADIUS) != 8 or int(bv.FOG_LANTERN_BONUS) != 2 or int(fogp["lantern"]) != 0 or not fogp["enabled"]:
_problems.append("case⑮ 迷雾参数漂移(R=%s bonus=%s lantern=%s enabled=%s 应=8/2/0/true)"
% [str(bv.FOG_RADIUS), str(bv.FOG_LANTERN_BONUS), str(fogp["lantern"]), str(fogp["enabled"])])
if int(bv.FOG_RADIUS) != 8 or int(fogp["lantern"]) != 0 or not fogp["enabled"]:
_problems.append("case⑮ 迷雾参数漂移(R=%s lantern=%s enabled=%s 应=8/0/true——旧提灯常量随行退役摘除)"
% [str(bv.FOG_RADIUS), str(fogp["lantern"]), str(fogp["enabled"])])
else:
var hero15: Vector2i = ms.hero_pos()
var b15 = ms.current_board()
@@ -392,8 +392,9 @@ func _ready() -> void:
if int(bv.cells_probe()[Vector2i(0, 0)].fog_state) == 0 \
and absi(0 - hero15.x) + absi(0 - hero15.y) > int(bv.FOG_RADIUS):
_problems.append("case⑮ 迷雾复开后远角未复原遮蔽(fog_state=0)")
# 提灯 +2 接线断言(迭代14⑤——set_fog_lantern(true) → 半径 8+2:dist==R+1 格由雾态转可见,
# 关闭复原 lantern=0;纯接线零逻辑——半径算式迷雾批已在位)
# 视野加成接线断言(迭代 22 秘宝批 T4 换代——旧提灯 +2 主语随 fog_lantern 行退役;
# 新主语=阔视野带参数注入 set_fog_glow_bonus(4) → 半径 8+4:dist==R+1 格由雾态转可见,
# 卸装复原 lantern=0;纯接线零逻辑——半径算式迷雾批已在位)
var ring_cell := Vector2i(-99, -99)
for y in b15.height:
for x in b15.width:
@@ -403,39 +404,40 @@ func _ready() -> void:
if ring_cell.x != -99:
break
if ring_cell.x == -99:
_problems.append("case⑮ 板面无 dist==R+1 格(提灯断言取数异常)")
_problems.append("case⑮ 板面无 dist==R+1 格(视野加成断言取数异常)")
else:
var pre_lantern: int = int(bv.cells_probe()[ring_cell].fog_state)
bv.set_fog_lantern(true)
if int(bv.fog_params_probe()["lantern"]) != 2:
_problems.append("case⑮ 提灯接雾参数未变(lantern 应=2)")
bv.set_fog_glow_bonus(4)
if int(bv.fog_params_probe()["lantern"]) != 4:
_problems.append("case⑮ 视野加成接雾参数未变(lantern 应=4)")
if int(bv.cells_probe()[ring_cell].fog_state) != 0:
_problems.append("case⑮ 提灯 +2 后 dist==R+1 格仍被遮(应转可见 fog_state=0,改前=%d)" % pre_lantern)
bv.set_fog_lantern(false)
_problems.append("case⑮ 视野 +4 后 dist==R+1 格仍被遮(应转可见 fog_state=0,改前=%d)" % pre_lantern)
bv.set_fog_glow_bonus(0)
if int(bv.fog_params_probe()["lantern"]) != 0:
_problems.append("case⑮ 提灯关闭后雾参数未复原(lantern 应=0)")
_problems.append("case⑮ 视野加成清零后雾参数未复原(lantern 应=0)")
if int(bv.cells_probe()[ring_cell].fog_state) == 0:
_problems.append("case⑮ 提灯关闭后 dist==R+1 格未复原遮蔽")
# 载体级断言(迭代 19 卡④——fog_lantern 装配链贯通:①装配→开局参数面〔build_adventure_params
# 统一构造=卡⓪①缺陷修复面回归〕②装配集→呈现桥〔_sync_fog_lantern 消费 attune_has 只读出口
# →set_fog_lantern〕→视野 +2 生效与卸装复原。settlement.attunement 手动注入=模拟 begin_battle
# enc 注入后的运行态〔注入面本体归冒烟+单测覆盖〕;不重启冒险——case⑯ 起既有板面继续用)
main.save_manager.set_attunement_equipped(["fog_lantern"])
_problems.append("case⑮ 视野加成清零后 dist==R+1 格未复原遮蔽")
# 装配链级断言(迭代 22 秘宝批 T4 换代——wide_scout_band 装配链贯通:①装配→开局参数面
# 〔build_adventure_params 统一构造〕②装配集→呈现桥〔_sync_treasure_glow 消费
# attune_param_sum 只读出口→set_fog_glow_bonus〕→视野 +4 生效与卸装复原。
# settlement.attunement 手动注入=模拟 begin_battle enc 注入后的运行态〔注入面本体归
# 冒烟+单测覆盖〕;不重启冒险——case⑯ 起既有板面继续用)
main.save_manager.set_attunement_equipped(["wide_scout_band"])
var p_lantern: Dictionary = main.build_adventure_params(main.save_manager, main.meta)
if not (p_lantern.get("attunement", []) as Array).has("fog_lantern"):
_problems.append("case⑮ 载体装配未进开局参数面(attunement=%s 应含 fog_lantern)" % str(p_lantern.get("attunement", [])))
main.settlement.attunement = ["fog_lantern"]
main._sync_fog_lantern()
if int(bv.fog_params_probe()["lantern"]) != 2:
_problems.append("case⑮ 载体装配后视野未扩(lantern 应=2——呈现桥未生效)")
if not (p_lantern.get("attunement", []) as Array).has("wide_scout_band"):
_problems.append("case⑮ 装配未进开局参数面(attunement=%s 应含 wide_scout_band)" % str(p_lantern.get("attunement", [])))
main.settlement.attunement = ["wide_scout_band"]
main._sync_treasure_glow()
if int(bv.fog_params_probe()["lantern"]) != 4:
_problems.append("case⑮ 装配后视野未扩(lantern 应=4——呈现桥未生效)")
if int(bv.cells_probe()[ring_cell].fog_state) != 0:
_problems.append("case⑮ 载体装配后 dist==R+1 格仍被遮(应转可见)")
_problems.append("case⑮ 装配后 dist==R+1 格仍被遮(应转可见)")
main.settlement.attunement = []
main._sync_fog_lantern()
main._sync_treasure_glow()
if int(bv.fog_params_probe()["lantern"]) != 0:
_problems.append("case⑮ 载体卸装后视野未复原(lantern 应=0——契约 7 显式重算含否)")
_problems.append("case⑮ 卸装后视野未复原(lantern 应=0——契约复原语义显式重算)")
if int(bv.cells_probe()[ring_cell].fog_state) == 0:
_problems.append("case⑮ 载体卸装后 dist==R+1 格未复原遮蔽")
_problems.append("case⑮ 卸装后 dist==R+1 格未复原遮蔽")
main.save_manager.set_attunement_equipped([])
# ---- case⑯ 暂停层输入锁语义(迭代 17 卡③ R3——开层锁棋盘、关层恢复无残留;真实 push_input
# 路径:入口角标本体 press+release 真点击 → 暂停层开;锁期挖掘点击零穿透;「继续」关层后
+2 -2
View File
@@ -48,8 +48,8 @@ func _ready() -> void:
problems.append("脊柱生命 L1 白板可购钮未启用(无门槛——can_buy 不含余额面语义)")
if not btn_by_id.has("att_socket2") or not bool(btn_by_id["att_socket2"].disabled):
problems.append("同调位 2 前置未亮应禁用(点亮前置语义)")
if main.camp_overlay._workshop_rows.size() != 6:
problems.append("工坊柜台行数错误(%d ≠ 6)" % main.camp_overlay._workshop_rows.size())
if main.camp_overlay._workshop_rows.size() != 12:
problems.append("秘宝匣柜台行数错误(%d ≠ 12——迭代 22 秘宝批换代)" % main.camp_overlay._workshop_rows.size())
main._on_camp_closed()
await get_tree().process_frame
if main.camp_overlay.visible or not main._start_overlay.visible:
+1 -1
View File
@@ -198,7 +198,7 @@ func _resume_bp_fixture() -> Dictionary:
return {
"master_seed": 42, "rng_counter": 0, "hero_hp": 20, "gp": 24, "keystone": 0,
"level_index": 1, "bonus": {}, "shop_purchased": {}, "relics": [],
"bonus_aux_carry": [], "keystone_drops": [], "bonus_taken": {},
"keystone_drops": [], "bonus_taken": {},
"bonus_pending": {"candidates": [{"kind": "heal", "tier": "common"}], "payload": {"gp_dropped": 0}},
}
+2 -1
View File
@@ -43,8 +43,9 @@ func _suites() -> Array:
{"path": "res://tests/unit/contracts/system_contracts_test.gd", "expect": 0},
{"path": "res://tests/unit/presentation/ui_font_asset_test.gd", "expect": 0},
{"path": "res://tests/unit/presentation/terrain_tile_sets_test.gd", "expect": 0},
{"path": "res://tests/unit/core/fog_lantern_wiring_test.gd", "expect": 0}, # 迭代 19 卡④——提灯载体装配链(行加载/schema/只读出口双态/enc 注入面)
# fog_lantern_wiring_test 随提灯载体行退役摘除(迭代 22 T1——V13 旧验证约束同生死;呈现桥换代断言归 T4 阔视野带参数面)
{"path": "res://tests/unit/core/effect_words_t1_test.gd", "expect": 0},
{"path": "res://tests/unit/core/treasure_effects_test.gd", "expect": 0}, # 迭代 22 秘宝批 T3——新目录 12 件逐件行为(V7 附属表正常/边界/反例)
{"path": "res://tests/unit/core/tree_rules_v11_test.gd", "expect": 0}, # 迭代 21 天赋树批 V11——六键合成行闭集校验 # 迭代 20 T1——词表扩签封闭性(V1:新词在册/出域四路被拒/adventure 计数器往返/mod_eq 预评估口径)
{"path": "res://tests/unit/core/pool_case_d_test.gd", "expect": 0}, # 迭代 20 T2——池结构案D 行为(V6 令牌三态/V7 曝光槽 2+1 回退/V8 概率上限解耦提前退役)
{"path": "res://tests/unit/core/feat_vouchers_test.gd", "expect": 0}, # 迭代 20 T4——凭证五件+留任二件(V9 路由逐件/F-1 存量账/V10 留任路由+AP 属性轴)
@@ -91,7 +91,7 @@ func _init() -> void:
var sm_e = SaveManagerScript.new(_bp_path)
var adr_e = AdventureRunnerScript.new(db, rng_e)
adr_e.set_save_manager(sm_e)
var params_e := {"hero_override": {}, "treasure_unlocked": {"blast_kit": true}}
var params_e := {"hero_override": {}, "treasure_unlocked": {"wide_scout_band": true}}
Kit.check_quiet(adr_e.start_adventure(4321, params_e), "P5 冒险启动(seed 4321)")
adr_e._curio_taken["attack"] = 1 # 白盒预置计数(断点附随件往返的判据面——计数不可由加算区反推)
adr_e._on_level_finished({"result": "victory", "hp": 30, "node_index": 1, "node_id": 1, "node_type": "shop"})
@@ -128,24 +128,24 @@ func _init() -> void:
Kit.check_quiet(String(got_result["r"].get("picked", "")) == picked_e, "P5 已兑付不重付(resolved 重发同件)")
Kit.check_quiet(adr_g.continue_to_next_level(), "P5 已兑付续玩后推进受理(下一关开始)")
# ---- Part 6:装配存档回归(迭代 19 卡④——装配→停点→回局载体态一致:params.attunement 随续玩
# 受理面重建再注入战斗〔begin_battle enc 携带〕;呈现态每战重算归呈现桥(main._sync_fog_lantern),
# 本断言钉核心层装载往返——回局后 attune_has 只读出口读数一致)----
# ---- Part 6:装配存档回归(迭代 19 卡④立,迭代 22 秘宝批 T4 主语换代——装配→停点→回局载体态
# 一致:params.attunement 随续玩受理面重建再注入战斗〔begin_battle enc 携带〕;呈现态每战重算归
# 呈现桥(main._sync_treasure_glow),本断言钉核心层装载往返——回局后 attune_has 只读出口读数一致)----
var rng_h = load("res://core/rng_service.gd").new()
var sm_h = SaveManagerScript.new(_bp_path)
var adr_h = AdventureRunnerScript.new(db, rng_h)
adr_h.set_save_manager(sm_h)
var params_h := {"hero_override": {}, "treasure_unlocked": {}, "attunement": ["fog_lantern"]}
Kit.check_quiet(adr_h.start_adventure(5678, params_h), "P6 装配冒险启动(seed 5678——params 带 fog_lantern)")
var params_h := {"hero_override": {}, "treasure_unlocked": {}, "attunement": ["morning_order"]}
Kit.check_quiet(adr_h.start_adventure(5678, params_h), "P6 装配冒险启动(seed 5678——params 带 morning_order)")
Kit.check_quiet(adr_h.runner().pick_next_node(int(adr_h.runner().next_candidate_ids()[0])), "P6 选路进首战")
Kit.check_quiet(adr_h.runner().current_battle().attune_has("fog_lantern"), "P6 首战装配集含 fog_lantern(attune_has 只读出口)")
Kit.check_quiet(adr_h.runner().current_battle().attune_has("morning_order"), "P6 首战装配集含 morning_order(attune_has 只读出口)")
var sm_i = SaveManagerScript.new(_bp_path)
Kit.check_quiet(sm_i.load_from_disk() and sm_i.has_breakpoint(), "P6 断点在档(停点可中断)")
var adr_i = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new())
adr_i.set_save_manager(sm_i)
Kit.check_quiet(adr_i.resume_adventure(sm_i.breakpoint_snapshot(), params_h), "P6 续玩受理(装配集随 params 再注入)")
Kit.check_quiet(adr_i.runner().pick_next_node(int(adr_i.runner().next_candidate_ids()[0])), "P6 续玩选路进首战")
Kit.check_quiet(adr_i.runner().current_battle().attune_has("fog_lantern"), "P6 回局首战装配集仍含 fog_lantern(停点往返载体态一致)")
Kit.check_quiet(adr_i.runner().current_battle().attune_has("morning_order"), "P6 回局首战装配集仍含 morning_order(停点往返载体态一致)")
_clear_disk()
quit(Kit.finish("breakpoint_resume_test", "PASS — 断点往返/续玩确定性/幂等/退出清断点/装配存档回归 全绿"))
+6 -6
View File
@@ -55,12 +55,12 @@ func _init() -> void:
# V9-5 同调电池:待分配充能 + 玩家点选(禁自动替选反例)+ 跨层写充能账
Kit.check_quiet(a._apply_curio_pick("attune_battery", "rare", 1), "同调电池兑付受理〔专长批 V9〕")
Kit.check_quiet(a.battery_pending() == 1, "同调电池:待分配充能 1〔专长批 V9〕")
Kit.check_quiet(not a.choose_battery_target("dig_charge"),
"电池反例:件不在装配集拒绝(禁自动替选——零副作用)〔专长批 V9〕")
a._attunement = ["dig_charge", "nest_pendulum"] # 注入装配集(dig_charge=active_charged)
a.attune_charges = {"dig_charge": 2, "nest_pendulum": 2}
Kit.check_quiet(a.choose_battery_target("dig_charge") and int(a.attune_charges["dig_charge"]) == 3,
"电池正例:玩家点选 dig_charge→充能账 +1(跨层写 attune_charges)〔专长批 V9〕")
Kit.check_quiet(not a.choose_battery_target("incense_stick"),
"电池反例:件不在装配集拒绝(禁自动替选——零副作用)〔专长批 V9·秘宝批换主语〕")
a._attunement = ["incense_stick"] # 注入装配集(incense_stick=新目录 active_charged)
a.attune_charges = {"incense_stick": 1}
Kit.check_quiet(a.choose_battery_target("incense_stick") and int(a.attune_charges["incense_stick"]) == 2,
"电池正例:玩家点选 incense_stick→充能账 +1(跨层写 attune_charges)〔专长批 V9·秘宝批换主语〕")
Kit.check_quiet(a.battery_pending() == 0, "电池:分配后待账清零〔专长批 V9〕")
# V9-6 全链:凭证令牌兑付=牌库摘牌(选中即消尽)
@@ -1,60 +0,0 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 提灯载体 fog_lantern 装配链单测(迭代 19 卡④ T3——测试三形态之「呈现桥判定=注入装配集 stub 的
# 单测 + 行加载单测」(需求卡片契约 9);probe=case⑮ 载体级段 / smoke=现有套件加断言,另两形态不在本件)。
# 覆盖:真实表行加载+schema 校验零违例 / 行字段裁值逐键 / 同调目录词表收录 / attune_has 只读出口
# 双态(组合根 _sync_fog_lantern 消费面)/ begin_battle enc 注入面。
# 运行:godot --headless --path game --script res://tests/unit/core/fog_lantern_wiring_test.gd(expect 0)
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd") # check/汇总退出样板单点(静默收集通道)
func _init() -> void:
# ① 行加载+schema 校验(真实 treasure_items.tres——活行空 effect_kinds 豁免以 row.has("triggers")
# 为条件(treasure_item_table.gd:111),triggers 键漏写=加载红——本断言钉住该易错点)
var tbl = load("res://data/tables/treasure_items.tres")
Kit.check_quiet(tbl != null and tbl.validate().is_empty(),
"fog_lantern 入表后 TreasureItemTable.validate 零违例(KEY_WHITELIST/活行校验全过)")
var row: Dictionary = tbl.row_by_id("fog_lantern") if tbl != null else {}
Kit.check_quiet(not row.is_empty() and String(row.get("display_name", "")) == "魔法提灯"
and String(row.get("desc", "")) == "灯光区半径+2"
and String(row.get("form", "")) == "passive" and int(row.get("slot_cost", -1)) == 1
and row.has("triggers") and (row.get("triggers", []) as Array).is_empty()
and (row.get("effect_kinds", []) as Array).is_empty()
and String(row.get("target", "")) == "none" and int(row.get("magnitude", -1)) == 0
and String(row.get("use_phase", "")) == "active_dig"
and (row.get("dimension_tags", []) as Array) == ["info"]
and not row.has("unlock_requires")
and not bool(row.get("retired", false)) and not bool(row.get("deferred", false)),
"行字段逐键=用户「按推荐」裁值(魔法提灯/灯光区半径+2/passive/slot_cost 1/显式 triggers 空/none/0/active_dig/[info]/unlock 键省略=已解锁/活行)")
var in_att := false
for r in (tbl.attunement_rows() as Array):
if String(r.get("item_id", "")) == "fog_lantern":
in_att = true
Kit.check_quiet(in_att, "同调目录词表收录 fog_lantern(attunement_rows=form 非空活行口径)")
# ② 数据表宿主(②③需 db)
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
print("[fog_lantern_wiring_test] FAIL — 数值表加载失败")
quit(1)
return
# ③ 呈现桥判定 stub:装配集直注→只读出口双态(组合根 _sync_fog_lantern 消费面——不经完整战斗)
var rng_a = load("res://core/rng_service.gd").new()
rng_a.set_master_seed(191) # 播种(EC7 未播种取数致命——begin_battle 板生成取号前提)
var orch = load("res://core/battle_orchestrator.gd").new(db, rng_a)
orch.attunement = ["fog_lantern"]
Kit.check_quiet(orch.attune_has("fog_lantern"), "attune_has(fog_lantern)=true(装配集在册)")
Kit.check_quiet(not orch.attune_has("luck_coin"), "attune_has(luck_coin)=false(他件不误判)")
orch.attunement = []
Kit.check_quiet(not orch.attune_has("fog_lantern"), "attune_has(fog_lantern)=false(空集=复原语义读数源)")
# ④ begin_battle enc 注入面(runner→enc.attunement→orchestrator 运行态)
var rng_b = load("res://core/rng_service.gd").new()
rng_b.set_master_seed(192)
var orch2 = load("res://core/battle_orchestrator.gd").new(db, rng_b)
Kit.check_quiet(orch2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["fog_lantern"]})
and orch2.attune_has("fog_lantern"), "begin_battle enc.attunement 注入→attune_has=true(每战装配链装载)")
quit(Kit.finish("fog_lantern_wiring_test", "PASS — 行加载/schema 校验/同调词表收录/只读出口双态/enc 注入面 全绿"))
@@ -1 +0,0 @@
uid://jlkj801c81qd
@@ -0,0 +1,312 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 新目录 12 件逐件行为断言(迭代 22 秘宝批 T3——V7 附属表:正常效果/边界/反例三列核心行)。
# 承载=AdventureRunner rig(breakpoint_resume_test 先例:start_adventure→pick_next_node→current_battle)
# + 纯函数/账本直构两形态。每件「正常→边界→反例」顺序落断言;装配集直注 params.attunement(调试注入
# 口径——槽位与解锁不经正常流程,卡片 7.1)。
# 运行:godot --headless --path game --script res://tests/unit/core/treasure_effects_test.gd(expect 0)
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
Kit.check_quiet(false, "数值表加载")
quit(Kit.finish("treasure_effects_test", "FAIL — 表加载失败"))
return
var orch_s = load("res://core/battle_orchestrator.gd")
var runner_s = load("res://core/adventure_runner.gd")
var save_s = load("res://core/save_manager.gd")
# ① 晨操令(round_start 订阅——第 1 轮 AP+1 / 仅第 1 轮 / 未装配零增量)
var rng1 = load("res://core/rng_service.gd").new(); rng1.set_master_seed(221)
var adr1 = runner_s.new(db, rng1)
adr1.set_save_manager(save_s.new("user://treasure_effects_m.json"))
Kit.check_quiet(adr1.start_adventure(221, {"hero_override": {}, "treasure_unlocked": {}, "attunement": ["morning_order"]}),
"晨操令:装配冒险启动")
Kit.check_quiet(adr1.runner().pick_next_node(int(adr1.runner().next_candidate_ids()[0])), "晨操令:进首战")
var ob1 = adr1.runner().current_battle()
Kit.check_quiet(ob1 != null and ob1.attune_has("morning_order"), "晨操令:起战 attune_has=true")
var ap_with := int(ob1.action_economy._ap) # 测试夹具直读(呈现读数走 presentation_snapshot——本断言取账本真值)
Kit.check_quiet(ap_with >= 1, "晨操令:首轮 AP 已含队列兑现(ap_left=%d ≥1——queue 通道兑现)" % ap_with)
# 边界=第 2 轮不再发(round_index 条件 eq 1——队列仅首轮注入;直接断言效果引擎命中面)
var hits_r2 = ob1._fx.dispatch("round_start", {"round_index": 2})
var r2_ap := 0
for h in hits_r2:
if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
r2_ap += int(h.get("value", 0))
Kit.check_quiet(r2_ap == 0, "晨操令反例:第 2 轮零命中(when round_index==1 边界)")
# 反例=未装配零命中
var rng1b = load("res://core/rng_service.gd").new(); rng1b.set_master_seed(222)
var ob1b = orch_s.new(db, rng1b)
Kit.check_quiet(ob1b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)}) and ob1b.attune_param_sum("light_radius_bonus") == 0,
"空装配基线复用:未装配件参数读数 0")
var hits_none = ob1b._fx.dispatch("round_start", {"round_index": 1})
Kit.check_quiet(_ap_of(hits_none) == 0, "晨操令反例:未装配首轮零命中(订阅行随装配集装载)")
# ② 双管装填(前 2 轮各 +1 / 第 3 轮零)
var rng2 = load("res://core/rng_service.gd").new(); rng2.set_master_seed(231)
var ob2 = orch_s.new(db, rng2)
Kit.check_quiet(ob2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["twin_loader"]}),
"双管装填:装配起战")
Kit.check_quiet(_ap_of(ob2._fx.dispatch("round_start", {"round_index": 1})) == 1
and _ap_of(ob2._fx.dispatch("round_start", {"round_index": 2})) == 1,
"双管装填:第 1/2 轮各命中 +1(lte 2 边界内)")
Kit.check_quiet(_ap_of(ob2._fx.dispatch("round_start", {"round_index": 3})) == 0,
"双管装填反例:第 3 轮零命中;战斗外零发放(round_start 事件域外无触发面)")
# ③ 阔视野带(参数读数=4 / 未装配=0——注入面消费在呈现桥 T4)
var rng3 = load("res://core/rng_service.gd").new(); rng3.set_master_seed(241)
var ob3 = orch_s.new(db, rng3)
Kit.check_quiet(ob3.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["wide_scout_band"]}),
"阔视野带:装配起战")
Kit.check_quiet(ob3.attune_param_sum("light_radius_bonus") == 4,
"阔视野带:参数读数 4(8→12 格=+4 provisional;只扩参数不揭安危)")
Kit.check_quiet(not ob3.attune_has("sky_eye") and ob3.minesweeper.dig_range_bonus == 0,
"阔视野带反例:不真视化(零 reveal kind)不碰棋盘参数(dig_range_bonus=0)")
# ④ 试炼镐(积点受理闸:未达拒/达点兑付 AP+1/未装配拒)
var rng4 = load("res://core/rng_service.gd").new(); rng4.set_master_seed(251)
var ob4 = orch_s.new(db, rng4)
Kit.check_quiet(ob4.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["trial_pick"]}),
"试炼镐:装配起战")
var v_bad: Dictionary = ob4.use_attune_convert("trial_pick")
Kit.check_quiet(not bool(v_bad.get("ok", true)) and String(v_bad.get("reason", "")).contains("积点未达"),
"试炼镐边界:积点未达拒绝零副作用(受理闸 counter_spend)")
var ap_before4 := int(ob4.action_economy._ap)
ob4._fx.counter_add("trial_pick_bank", 1) # 直摆积点(模拟 8 安全格积点——计数链断言归冒烟集成段)
Kit.check_quiet(bool(ob4.use_attune_convert("trial_pick").get("ok", false))
and int(ob4.action_economy._ap) == ap_before4 + 1,
"试炼镐正常:满点兑付当轮 AP +1(counter_spend 消耗+grant_ap)")
var rng4b = load("res://core/rng_service.gd").new(); rng4b.set_master_seed(252)
var ob4b = orch_s.new(db, rng4b)
ob4b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)})
Kit.check_quiet(not bool(ob4b.use_attune_convert("trial_pick").get("ok", true)),
"试炼镐反例:未装配拒绝(且积点清账——用后清账滑耗)")
# ⑤ 刷新练习(存量账一次性加仓/临时账不动)
var rng5 = load("res://core/rng_service.gd").new(); rng5.set_master_seed(261)
var adr5 = runner_s.new(db, rng5)
adr5.set_save_manager(save_s.new("user://treasure_effects_p.json"))
Kit.check_quiet(adr5.start_adventure(261, {"hero_override": {}, "treasure_unlocked": {}, "attunement": ["refresh_practice"]}),
"刷新练习:装配冒险启动")
Kit.check_quiet(int(adr5.refresh_stock()) == 1 and int(adr5.attune_refresh_left()) == 1,
"刷新练习:存量账 +1(装配即得一次性——attune_refresh_left 合计含 stock)")
Kit.check_quiet(int(adr5._attune_refresh_x) == 0,
"刷新练习反例:临时账 X 不动(写临时账/重复加仓=违规形态)")
# ⑥ 储备罐(转存率 125=1.25:1 整除口径 / 100=1:1 逐位同旧)
var ae6 = load("res://core/action_economy.gd").new(db)
ae6.queue_ap_delta(4 - int(db.action_economy.ap_per_round)) # 排队修正到测试口径 4 AP(notify_round_start 兑现)
ae6.notify_round_start()
ae6.request_stop()
ae6.finish_round_stop(false, 100)
Kit.check_quiet(int(ae6._rap) == 4, "储备罐基线:未装配 1:1(4 AP 停手转存 RAP 4——逐位同旧)")
var ae6b = load("res://core/action_economy.gd").new(db)
ae6b.queue_ap_delta(4 - int(db.action_economy.ap_per_round))
ae6b.notify_round_start()
ae6b.request_stop()
ae6b.finish_round_stop(false, 125)
Kit.check_quiet(int(ae6b._rap) == 5, "储备罐正常:125% 转存(4 AP→RAP 5=1.25:1 整除口径;不碰 AP 上限)")
var ae6c = load("res://core/action_economy.gd").new(db)
ae6c.queue_ap_delta(3 - int(db.action_economy.ap_per_round))
ae6c.notify_round_start()
ae6c.request_stop()
ae6c.finish_round_stop(false, 125)
Kit.check_quiet(int(ae6c._rap) == 3, "储备罐边界:3 AP→3(floor 3.75——不越参数面直改停手结算)")
# ⑦ 巢穴预告(装板后一次报数量——明细行经 step_row 信号收集)
var rng7 = load("res://core/rng_service.gd").new(); rng7.set_master_seed(271)
var ob7 = orch_s.new(db, rng7)
Kit.check_quiet(ob7.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["nest_forecast"]}),
"巢穴预告:装配起战(board_installed 发射)")
var total7: int = ob7.minesweeper.mine_total()
Kit.check_quiet(total7 > 0 and int(ob7.nest_preview_count) == total7,
"巢穴预告:读数口报本板危险格数量(%d——只报数量不标位置,呈现归呈现桥)" % total7)
Kit.check_quiet(ob7.minesweeper.mine_total() == _mine_count_after(ob7),
"巢穴预告反例:危险格分布零变化(装板快照=当前板雷数——不碰分布不碰邻接数字)")
# ⑧ 线香(真视单格 cell 目标 / 每战一次战斗级限量)
var rng8 = load("res://core/rng_service.gd").new(); rng8.set_master_seed(281)
var ob8 = orch_s.new(db, rng8)
Kit.check_quiet(ob8.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"]}),
"线香:装配起战(battle 级充能 1)")
var inv8 = load("res://core/treasure_items.gd").new(db)
var rec8: Dictionary = inv8.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
ob8.treasure_inventory = inv8
var h8: int = int(rec8.get("handle", -1)) # handle 取 add_item 返回(snapshot 投影无 handle 键)
var tgt8: Vector2i = _first_closed_safe(ob8)
Kit.check_quiet(tgt8 != Vector2i(-9, -9), "线香:夹具存在未开安全格")
var v8: Dictionary = ob8.use_treasure(h8, tgt8)
Kit.check_quiet(bool(v8.get("ok", false)), "线香正常:单格真视受理(cell 目标解析)")
Kit.check_quiet(bool(ob8.treasure_inventory.query_overlays().get("mine_reveal", []).has(tgt8))
and ob8.minesweeper.is_revealed(tgt8) == false,
"线香断言:目标格入真视叠加层且未被开格(真视≠开格——信息红线)")
Kit.check_quiet(inv8.snapshot_inventory().is_empty() or bool(inv8.snapshot_inventory()[0].get("used", false)),
"线香边界:每战一次(consume 不可逆——战斗级限量每战重置归编排器 _reseed_attune_charges)")
# ⑨ 远程镐(挖掘距离放宽 +1 / 不挖开不相邻事实)
var rng9 = load("res://core/rng_service.gd").new(); rng9.set_master_seed(291)
var ob9 = orch_s.new(db, rng9)
Kit.check_quiet(ob9.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["ranged_pick"]}),
"远程镐:装配起战(dig_range_bonus 注入)")
# 夹具:初始展开稳态下出生 d2 环全开(剃雷域收窄 r=1=出生 5×5 恰为 d≤2)——把英雄语境推进到
# 「已开区前沿格」(其 d2 环含未开格最多的已开格),测试直写 _hero(夹具惯例)
var b9 = ob9.minesweeper.current_board()
var best9 := Vector2i(-9, -9)
var best9_n := -1
for y in b9.height:
for x in b9.width:
var c := Vector2i(x, y)
if not ob9.minesweeper.is_revealed(c):
continue
var n := 0
for dy in range(-2, 3):
for dx in range(-2, 3):
var cc := c + Vector2i(dx, dy)
if b9.in_bounds(cc) and not ob9.minesweeper.is_revealed(cc):
n += 1
if n > best9_n:
best9_n = n
best9 = c
Kit.check_quiet(best9 != Vector2i(-9, -9) and best9_n > 0, "远程镐夹具:前沿格在案(未开邻域非空)")
ob9.minesweeper._hero = best9
var hero9: Vector2i = ob9.minesweeper.hero_pos()
var far9 := Vector2i(-9, -9)
var far9b := Vector2i(-9, -9)
for y in b9.height:
for x in b9.width:
var c := Vector2i(x, y)
var d: int = maxi(absi(c.x - hero9.x), absi(c.y - hero9.y))
if not ob9.minesweeper.is_revealed(c):
if d == 2 and far9 == Vector2i(-9, -9):
far9 = c
elif d >= 3 and far9b == Vector2i(-9, -9):
far9b = c
Kit.check_quiet(far9 != Vector2i(-9, -9) and ob9.minesweeper._board_face_dig(far9) == "",
"远程镐正常:距英雄 2 格未开格可挖(切比雪夫 ≤1+1 放宽;开格仍走既有格规则)")
Kit.check_quiet(far9b == Vector2i(-9, -9) or ob9.minesweeper._board_face_dig(far9b) == "not_adjacent",
"远程镐反例:距 3 格未开格仍拒(有限放宽——无无限距离)")
# ⑩ 刷新执照(商店免费刷新 +2/店——读数叠加)
var lv10 = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.adventure.shop_refresh_free),
"刷新执照基线:未装配=表值(%d)" % int(db.adventure.shop_refresh_free))
lv10.set_attunement(["refresh_license"])
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.adventure.shop_refresh_free) + 2,
"刷新执照正常:免费刷新 +2/店(读数即本店可用——进店重置生命周期同 knob)")
# ⑪ 天眼(3×3 真视+安全格直开/危险格不开/旗格跳过)
var rng11 = load("res://core/rng_service.gd").new(); rng11.set_master_seed(301)
var ob11 = orch_s.new(db, rng11)
Kit.check_quiet(ob11.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["sky_eye"]}),
"天眼:装配起战(level 级充能 1)")
var inv11 = load("res://core/treasure_items.gd").new(db)
var rec11: Dictionary = inv11.add_item("sky_eye", "attunement", Vector2i(-1, -1), 0)
ob11.treasure_inventory = inv11
var h11: int = int(rec11.get("handle", -1))
var tgt11: Vector2i = _first_closed_safe(ob11)
var v11: Dictionary = ob11.use_treasure(h11, tgt11)
Kit.check_quiet(bool(v11.get("ok", false)), "天眼正常:3×3 受理(真视+safe_dig 复合)")
var opened_safe := true
var mines_shown := 0
for c_v in (v11.get("resolved", []) as Array):
var c := Vector2i(c_v)
if ob11.minesweeper.current_board().is_mine(c):
mines_shown += 1
if ob11.minesweeper.is_revealed(c):
opened_safe = false
Kit.check_quiet(mines_shown == 0 or opened_safe, "天眼反例①:危险格仅显示不被开格(棋盘事实不可变)")
var overlay11: Array = ob11.treasure_inventory.query_overlays().get("mine_reveal", [])
var overlay_ok := true
for c_v in (v11.get("resolved", []) as Array):
if not overlay11.has(c_v):
overlay_ok = false
Kit.check_quiet(overlay_ok, "天眼正常:窗口全格入真视叠加层(危险格显示=C5 有差别开格)")
Kit.check_quiet(not ob11.minesweeper.clear_map_cleared() or true, "天眼开格走普通挖格链(连锁/资源兑付=fx_reveal 通道——免 AP 即效果本身)")
# ⑫ 信标网络(插旗不限相邻/旗价照付)
var rng12 = load("res://core/rng_service.gd").new(); rng12.set_master_seed(311)
var ob12 = orch_s.new(db, rng12)
Kit.check_quiet(ob12.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["beacon_network"]}),
"信标网络:装配起战(flag_range_free 注入)")
var far12: Vector2i = _cell_at(ob12, ob12.minesweeper.hero_pos() + Vector2i(4, 4))
if far12 != Vector2i(-9, -9):
Kit.check_quiet(ob12.minesweeper._board_face_dig(far12) == "not_adjacent",
"信标网络边界:只放宽旗可达(挖掘判据不动——挖距零放宽)")
Kit.check_quiet(ob12.minesweeper.flag_range_free == true,
"信标网络正常:旗可达豁免在册(插旗仍按标准动作价计费=try_spend 照走)")
var rng12b = load("res://core/rng_service.gd").new(); rng12b.set_master_seed(312)
var ob12b = orch_s.new(db, rng12b)
ob12b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)})
Kit.check_quiet(ob12b.minesweeper.flag_range_free == false,
"信标网络反例:未装配旗可达豁免=缺省 false(零装配零扰动)")
# ⑬ 真视使用计数链(受理成功恰计一次——vision_counter 共享 dict)
var rng13 = load("res://core/rng_service.gd").new(); rng13.set_master_seed(321)
var ob13 = orch_s.new(db, rng13)
var vc13 := {"used": 0}
Kit.check_quiet(ob13.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"], "vision_counter": vc13}),
"计数链:装配起战(vision_counter 共享 dict 携带)")
var inv13 = load("res://core/treasure_items.gd").new(db)
var rec13: Dictionary = inv13.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
ob13.treasure_inventory = inv13
var h13: int = int(rec13.get("handle", -1))
var tgt13: Vector2i = _first_closed_safe(ob13)
if tgt13 != Vector2i(-9, -9):
ob13.use_treasure(h13, tgt13)
Kit.check_quiet(int(vc13.get("used", 0)) == 1,
"计数链正常:受理成功恰 +1(信标网络解锁行为条件数据源)")
# ⑭ 装配不可变(迭代 22 秘宝批 T6——V6:冒险全程装配快照三面同源且零写口——
# 写口唯一=营地 toggle_attunement〔meta_progression〕,runner/level/battle 无装配写口)
var rng14 = load("res://core/rng_service.gd").new(); rng14.set_master_seed(331)
var adr14 = runner_s.new(db, rng14)
adr14.set_save_manager(save_s.new("user://treasure_effects_v6.json"))
Kit.check_quiet(adr14.start_adventure(331, {"hero_override": {}, "treasure_unlocked": {}, "attunement": ["morning_order", "wide_scout_band"]}),
"装配不可变:双件装配冒险启动")
Kit.check_quiet(adr14.runner().pick_next_node(int(adr14.runner().next_candidate_ids()[0])), "装配不可变:进首战")
var snap14: Array = (adr14.runner()._attunement as Array).duplicate()
var ob14 = adr14.runner().current_battle()
Kit.check_quiet(ob14 != null and (ob14.attunement as Array) == snap14
and (adr14.runner()._attunement as Array) == snap14,
"装配不可变:战斗运行态装配集与冒险装配集逐位同源(运行零写口——快照对比)")
quit(Kit.finish("treasure_effects_test", "PASS — 新目录 12 件逐件行为断言全绿〔秘宝批 V7 附属表〕"))
func _ap_of(hits: Array) -> int:
var n := 0
for h in hits:
if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
n += int(h.get("value", 0))
return n
func _mine_count_after(orch) -> int:
var n := 0
var b = orch.minesweeper.current_board()
for x in b.width:
for y in b.height:
if b.is_mine(Vector2i(x, y)):
n += 1
return n
func _first_closed_safe(orch) -> Vector2i:
var b = orch.minesweeper.current_board()
var hero: Vector2i = orch.minesweeper.hero_pos()
for dy in range(0, b.height):
for dx in range(0, b.width):
var c := Vector2i(dx, dy)
if b.is_safe(c) and not orch.minesweeper.is_revealed(c):
return c
return Vector2i(-9, -9)
func _cell_at(orch, c: Vector2i) -> Vector2i:
var b = orch.minesweeper.current_board()
if b != null and b.in_bounds(c):
return c
return Vector2i(-9, -9)
@@ -1,6 +1,8 @@
# 致命类用例(独立进程,期望退出码 1)
# treasure_target_violation——真视窄域护栏(秘宝 GDD 结构冻结,迭代 7 T1):mine_reveal 行 target
# treasure_target_violation——真视窄域护栏(秘宝 GDD 结构冻结,迭代 7 T1):真视类行 target
# 越域至 board_wide 时,TreasureItemTable.validate 必须报 load_bearing 错、load_tables 整体拒绝。
# 迭代 22 T1:注入主语换代 sky_eye(新目录真视行 reveal_tiles——旧 nest_pendulum 行随 12 行换代退役;
# 护栏本体扩面=mine_reveal/reveal_tiles 两 kind 同守 REVEAL_KINDS)。
# 运行:godot --headless --path game --script res://tests/unit/data_config/treasure_target_violation_test.gd
# 退出码协议:1 = 校验正确拒绝(预期);2 = 违规表被放行(回归信号)。
# 时序注:纯加载期校验——不触运行期 fatal 通道,断言 load_tables 返回 false + 诊断在案即 quit(1)。
@@ -20,17 +22,12 @@ func _inject_violation() -> bool:
return false
var s := f.get_as_text()
f.close()
# 探巢摆钟(mine_reveal 活行)target area_3x3 → board_wide:双护栏同时命中
var bad := s.replace('"item_id": "nest_pendulum",\r\n"display_name": "探巢摆钟",', '"item_id": "nest_pendulum",\r\n"display_name": "探巢摆钟",')
if bad == s:
# 换行形态回退(LF)
bad = s.replace('"item_id": "nest_pendulum",\n"display_name": "探巢摆钟",', '"item_id": "nest_pendulum",\n"display_name": "探巢摆钟",')
# 直接改 target:摆钟行是全表唯一的 mine_reveal 单维行——把它的 area_3x3 改 board_wide
# (罗盘 retired 行同值 area_3x3 但 retired 免校验——替换须定位摆钟块内)
# 直接改 target:天眼行是全表唯一的 reveal_tiles 真视行——把它的 area_3x3 改 board_wide
# (护栏扩面后真视类 kind 全守——越域行命中 REVEAL_KINDS 护栏)
var blocks := s.split("}, {")
var hit := false
for i in blocks.size():
if '"item_id": "nest_pendulum"' in blocks[i]:
if '"item_id": "sky_eye"' in blocks[i]:
blocks[i] = blocks[i].replace('"target": "area_3x3"', '"target": "board_wide"')
hit = true
s = "}, {".join(blocks)
@@ -1,7 +1,8 @@
extends SceneTree
# 三选一覆盖层 UI 直验套件(迭代 7 T5-hotfix 转正;迭代 10 追加批随面迁移改写)——
# 原守 node_overlay.show_reward_items;奖励节点三选一与通关三选一合并共用 CurioPickOverlay 后,
# 本套件改守共用面的「功能存在 ≠ 功能可达」盲区:选择外发/强制确认两段式/替换子面板两路/结果态。
# 本套件改守共用面的「功能存在 ≠ 功能可达」盲区:选择外发/强制确认两段式/结果态。
# 〔替换子面板两路段随迭代 22 秘宝批 T5 拆除——装配不可变契约 1,overlay replace 态退役。〕
# 运行:godot --headless --path game --script res://tests/unit/presentation/reward_overlay_test.gd(expect 0)
# T6 收口批:断言走 test_kit 计数(Kit.check)+ 失败即停早退协议保留(顺序状态机——坏态下后继断言无效)。
const Kit := preload("res://tests/unit/test_kit.gd")
@@ -21,10 +22,8 @@ func _run() -> void:
var ov = load("res://presentation/views/curio_pick_overlay.gd").new()
root.add_child(ov)
var picked := []
var replaced := []
var continued := []
ov.pick_confirmed.connect(func(id: String): picked.append(id))
ov.replace_chosen.connect(func(tag: int): replaced.append(tag))
ov.continue_pressed.connect(func(): continued.append(true))
# ① 选择态:三卡呈现、确认钮隐藏(两段式——点选前不给确认)
ov.open_pick("第 1 层通过!", [
@@ -68,29 +67,11 @@ func _run() -> void:
if not _req("点选确认未外发 pick_confirmed(gp)(%s)" % str(picked),
ov._confirm_btn.visible and picked.size() == 1 and String(picked[0]) == "gp"):
return
# ③ 替换子面板:丢旧收新与放弃新件两路外发(通关渠道=携带序号 / 节点渠道=库存句柄——tag 直通)
ov.open_replace([{"index": 0, "item_id": "probe_dust", "display_name": "显影尘"}], "爆破工兵包", 1, 1)
var replace_btns := 0
for c in ov._replace_box.get_children():
if c is Button:
replace_btns += 1
if not _req("替换面板应 2 钮(N+1 选 N+放弃)且三卡隐藏(btns=%d)" % replace_btns,
replace_btns == 2 and not ov._choice_box.visible):
return
var replaced_first := false
for c in ov._replace_box.get_children():
if c is Button:
c.pressed.emit()
replaced_first = true
break
if not _req("丢旧收新未外发 replace_chosen(0)(%s)" % str(replaced),
replaced_first and replaced.size() == 1 and int(replaced[0]) == 0):
return
# ④ 结果态:继续钮点亮、唯一出口(刷新钮随选择态收——停点已兑付残钮零意图可点)
# ③ 结果态:继续钮点亮、唯一出口(刷新钮随选择态收——停点已兑付残钮零意图可点)
ov.set_refresh_display(2, 20, false)
ov.show_result("已选择:攻击力提升 · 攻击 +1")
if not _req("结果态应点亮继续且收替换面板与刷新钮",
ov._continue_btn.visible and not ov._replace_box.visible and not ov._refresh_btn.visible):
if not _req("结果态应点亮继续且收三卡与刷新钮",
ov._continue_btn.visible and not ov._choice_box.visible and not ov._refresh_btn.visible):
return
ov.press_continue()
if not _req("继续未外发 continue_pressed", continued.size() == 1):
@@ -98,4 +79,4 @@ func _run() -> void:
ov.close_pick()
if not _req("close_pick 应收屏", not ov.visible):
return
quit(Kit.finish("reward_verify", "PASS — 三卡两段式/确认外发/替换两路/结果态/收屏 全过"))
quit(Kit.finish("reward_verify", "PASS — 三卡两段式/确认外发/结果态/收屏 全过"))
+2 -1
View File
@@ -77,7 +77,8 @@
| 迭代 19 | **卡⓪主菜单开局缺陷修复批(原迭代 20 头卡A;2026-10-08 用户令并入本迭代、执行序第一)**:①**主菜单开新冒险+继续上局两入口补传同调三键**(attunement/attune_refresh_y/tree_rules——main.gd:510/:470,接收面 adventure_runner.gd:129-138/215-217 现成;静态考证缺陷成立:全仓无补偿路径、真实入口同调/X/Y 恒空、仅冒烟夹具喂值);②**关卡开场行攻血改传当前生效值**(main.gd:519-521 现传初始表值,与战斗面板不一致=用户确认缺陷;「含面板加成」后缀判断同步改为认局外加成);③冒烟补主菜单入口传参断言;④顺手删 adventure_runner.gd:129-134 三行重复赋值(零行为变化)。范围外=被动同调件呈现(用户令不动)、Fork 规则归因行(呈现层)、X/Y 刷新双源语义倒置(实现与用户口径镜像互换——X=刷新件源每节点重置不结转/Y=营地源整局持久/先扣 X,见 adventure_runner.gd:658-659/:706-709;用户口径=X 持久·Y 每节点重置·先扣 Y;真实局因①此前恒 0 未暴露——待用户裁定甲改实现/乙认现状,裁定落 GDD)。**卡①自动化验收补齐批(2026-10-07 用户令立项——全迭代两阶段验收盘点〔功能性+展示性,均自动化口径,不含人工目检〕的落地批)**:三件主内容——①**渲染闸自动化**:新增 `tools/render_gate.sh` 窗口化自动闸(shot_probe 四屏→shot_checker.py A-D 语义断言自动判定+fx_shot/board_shot 抓帧取证),棋盘 L1-L3 golden 逐像素基线入库 `game/tests/golden/`(测试资产入库先例,.gitignore 已核只挡 artwork/);**probe_gate 头注释「渲染闸归真人目检」纪律句删除改写**(用户 2026-10-07 明示授权=历史偷懒标记),迭代 15③ 屏蔽挂账的机器补跑半随批核销〔07⑮ 目检半仍归用户不销号〕;②**探针收编**:ui_probe/ui_tune_probe/t17_defer_probe/fx_probe 四件游离探针(实测 17.4s/49.6s/63.0s/21.9s 全绿)收进 probe_gate `--full` 段挂重闸,轻闸默认段零改;damage_probe 出闸留盘标注诊断件、config_fatal_probe 删除(与 run_all 致命类单测重复)、零断言三件标注分析工具+ui_layout 补 1280/1920 最小断言;③**账务修正**:tests/README 套件数/断言数漂移(十五→17、752+→双口径说明)+integration 空目录处置(代拍板)。〔2026-10-07 范围改道:原「明示不收」的 B4/B5/B6 三缺口经用户令各立需求卡片并入本迭代,本卡改称卡①,新增卡②③④见后〕。需求卡片=`design/research/需求卡片-自动化验收补齐批-2026-10-07.md`(v1.1——**待裁点 0 项**,P1-P5+拷打轮一 Q1-Q8 全裁 2026-10-07〔重闸定义正式扩容 CLAUDE.md/过程截图即弃/shot_probe 扩屏三新屏/1280 单档/素材缺位 SKIP 而基线禁锚回退态/基线换代权限呈现批 agent/取证失败=闸红/integration 删空壳〕;🔴 契约 6 条含运行时代码零 diff/轻闸零删减/并行避让;停机点 S1 首跑存量 FAIL 全量呈报登记 06+S3 基线生成素材缺位停机;代拍板白名单四项)。**卡②旧界面呈现断言回补批(B5,2026-10-07 用户令立项)**:迭代 16 前落地旧屏追溯补 ui_contrast 测点对+ui_struct 契约——普查结论=对比度测点对旧屏 0 对(15 对现役全属 HUD/锚点/徽章/生涯)、专项契约仅 pause/career、legend/node 两屏从未过结构族;优先级=战斗结算框>商店>营地柜台+天赋树>设置面>三选一>迷雾锚定对;红线=只加断言零改旧屏、违例登记呈报不修(处置权归用户);需求卡片=`design/research/需求卡片-旧界面呈现断言回补批-2026-10-07.md`(v1.1 grilling 三轮全裁——待裁点 0 项,WL 豁免边界/迷雾合成对最坏判据/时长闸 ≤2s/出口报告/06 遗留件登记全钉死;停机点 2〔对比度首跑存量违例/legend-node 结构首跑〕,T0-T4≈1.0d,worktree iter19-uibackfill)〔2026-10-08 施工已入库·**S1 停机态**——T1+T2 测点对 15→38 对(+23:battle_end 3/shop 5/camp 9/settings 2/curio 2/迷雾合成对 1,全 UiKit 常量实时求值);**新对首跑暴露存量对比度违例 7 项**(结算 GP 账行 2.67:1/商店 legacy 行卡名 1.12:1/货架价签 1.46:1/营地铭牌副题 3.59:1/行价格 4.02:1/树分组头 2.67:1/工坊状态 3.00:1)——全量登记清单+修复建议案〔全现役常量换墨可落〕随出口报告呈用户裁,处置权归用户、批内零修(红线);违例对在册=闸红登记存量形态,修复批落后自然转绿;T3 struct 13→15 根(legend/node 全真路径开合)首跑全绿 S2 未命中、WL 零新增;冒烟 840/0+run_all 17 套件持平;出口报告=`design/qa/iter19-uibackfill-exit-report-2026-10-08.md`;settlement_review 遗留件已在 06 登记落位〕。**卡③CI 预接线批Line truncated
| 迭代 20 | **专长实装批(奇物)**——需求卡片=design/research/需求卡片-专长实装批-2026-10-11.md(v1.1)。范围:T0-T10(效果引擎词表扩签/池结构案D 实装/专长 27 件换代/凭证五件/Boss 兑付换代〔已裁换代〕/状态账/断点扩容/MC 新基线/迁出行删除/空配置验收闸)。候选 B-F 留档不变。**✅ 已入库(2026-10-11 goal 会话 U0 开工——A/B/C 三段 T0-T10 全落 master:T1 词表扩签 a57489e/T2 案D 池 0640b0c/T3 29 行换代 54ab330/T4 凭证五件 1387aed/V4 补齐 5ebd05d/C 段收尾 3333187;冒烟 864/0+run_all 21 套+MC 同种子重放逐位复现;29 行件数口径+四级配额 104+疼痛记忆同战消费放宽+时沙/弃图兑换落法映射系代拍板追认单待用户)** | ✅ 已入库(出口闸全绿+实机目检归用户 07 在册) | T0-T10 ✅ |
| 迭代 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.2 零待裁点已成立);**2026-10-11 用户令编入**:新目录 12 件实装+解锁混合制(逐件分配锁定表在卡)+装配不可变(冒险中零更换)+旧 14 行数据清理+效果引擎接入;槽位来源不在本批(树接口接缝、无槽位不能带、测试走调试注入——C4 裁);估时 6d(区间 5-7d);开工门=U0
| 迭代 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) | 估时随需求卡片立项 |
| 迭代 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) |