迭代11第①批:魔法道具同调实施批 T0-T10 全落(重闸 645/0 + MC off 逐位 PASS)

- T1 表与schema:attunement_growth.tres 谓词表(m1/m2/m3+Σgrant≤slot_cap 校验+slots() 求值口径)+道具表扩列(form/charges/slot_cost/unlock_requires/effective_rounds/charge_n+effect_kinds 九kind+KEY_WHITELIST 零裸flat护栏)+七同调行
- T2 save v4(boss_kill_count 收束入账+attunement_equipped+attune_refresh_level+断点 attunement_charges+迁移链 v3→v4)
- T3 Attunement 求值服务(纯函数零 await 零 RNG)
- T4 营地装配覆盖层(工坊扩容程序占位+Y 升级行 attune_refresh_upgrade 行族)
- T5 被动六件接线+装配注入链+刷新 X/Y 双源(BonusPickOverlay 刷新钮,autoplay 默认不刷)
- T6 掘进蓄能全链+摆钟能力面(骑库存面差额注入+断点充能键);炸格 P5 甲影响评估破面大→用户裁拆后批,blast_kit deferred 留位
- T7 退役面五件(BONUS_IDS 除名 aux_item+transitional_sync_slots 过渡键退役=迭代10欠账③清偿+授予路/四容量闸拆除+nest_pendulum 换装 active_charged)
- T8 冒烟六新断言(645/0)+MC attune 开关+贪心画像分层 off/on 各 N=60 双策略(on 画像功率显著带平移归调参批;off 新锚=对拍正主留盘主区 sim-data,81000 降批入口基准)
- T9 连带修订九件(魔法道具定位重写/元进度v1.6/地图战斗资源v0.16/行动经济v0.7/05不做v1.3/关卡节点地图v1.4/node-reward v2.2/registry 三处/06 收口+炸格拆后批新条)
- T10 重闸全绿:冒烟 645/0+熵增 PASS+doc 三闸零新增+MC off 复跑同种子逐位一致 PASS;出口人闸后移迭代 12(用户 2026-10-03 令)
This commit is contained in:
DKChan committed 2026-10-03 15:41:30 +08:00
1 parent 02cb92a972
commit a6a2bf885e
37 files changed
+1075 -173

No files matched your search

+1
View File
@@ -33,3 +33,4 @@
| v1.0 | 2026-09-14 | 初版随 GDD 骨架建立:确定不做 / 现阶段不做两节 | | v1.0 | 2026-09-14 | 初版随 GDD 骨架建立:确定不做 / 现阶段不做两节 |
| v1.1 | 2026-09-21 | 元进度实现批:元进度行注首批已落地(营地三柜台 / 钥石账本 / 跨局注入),数值 provisional 归设计批标定 | | v1.1 | 2026-09-21 | 元进度实现批:元进度行注首批已落地(营地三柜台 / 钥石账本 / 跨局注入),数值 provisional 归设计批标定 |
| v1.2 | 2026-10-03 | R3 清账批(gdd_lint 分域随批):元进度行日期批注迁出至本表 v1.1 行;语义零变更 | | v1.2 | 2026-10-03 | R3 清账批(gdd_lint 分域随批):元进度行日期批注迁出至本表 v1.1 行;语义零变更 |
| v1.3 | 2026-10-03 | 道具替代推理边界锚复核(迭代 11 第①批同调实施批——任务书 §十连带):「允许恢复推理原料,不允许道具替代推理」边界锚**原样保留即通过**——同调目录九件逐件对表:真视/预读类=信息给付(探巢摆钟 3×3 窄域护栏承重),六被动=机制修正(禁裸 flat schema 强制),无一替代推理判定本体;复核零改动 |
+11 -3
View File
@@ -62,7 +62,15 @@
**归属**:魔法道具同调实施批(主责)+ 元进度/存档序列化/地图战斗资源/行动经济/关卡节点地图 随批连带 **归属**:魔法道具同调实施批(主责)+ 元进度/存档序列化/地图战斗资源/行动经济/关卡节点地图 随批连带
**状态**:**已编入迭代 11、序位第一(2026-10-03 用户裁)**;任务书 v1.1 全裁(2026-10-02),开工=用户发令→任务书升 loop 计划书 U0 批准即 T0。〔原登记:待立项(2026-09-30 用户裁决甲落定方向;开工=用户发任务书;排期不在此登记。⚠ 事故留痕:本提案 2026-09-22 落档后未登记未立项,迭代 7 裁决时不可见致两态并存七日——登记本条即堵此洞,教训已入 doc-conventions v8 概念落位条款) **状态**:**已实施落籍(2026-10-03,M-4 迭代 11 第①批,本条收口)**——worktree `m4-iter11a-attune-20261003`(攒 T10 一次提交):T0-T8 落 A 表与 schema(attunement_growth.tres 谓词表+道具表扩列+七同调行)/save v4(boss_kill_count/attunement_equipped/attune_refresh_level+断点充能余量)/求值服务/营地装配覆盖层(工坊扩容程序占位+Y 升级行)/被动六件+主动两件(掘进蓄能/探巢摆钟)接线/三选一刷新件 X/Y 双源/退役面五件(aux 卡出池+过渡键退役+授予路拆除,迭代 10 欠账③随之清偿);P5 甲炸格影响评估破面判定=大→用户裁**拆后批**(blast_kit 行标 deferred 留位,cell_removal kind 留 schema,后批立项再启——见下「爆破工兵包中立移除格态」条);锚语义判读追认(T8 起 off 换新锚为对拍正主)。遗留:装配 UI 视觉终稿归营地批;attune=on 带平移投值归调参批;维度词表收缩形态开放点(T10 追认单随批)。〔原登记:已编入迭代 11、序位第一(2026-10-03 用户裁);任务书 v1.1 全裁(2026-10-02),开工=用户发令→任务书升 loop 计划书 U0 批准即 T0。⚠ 事故留痕:本提案 2026-09-22 落档后未登记未立项,迭代 7 裁决时不可见致两态并存七日——登记本条即堵此洞,教训已入 doc-conventions v8 概念落位条款〕
#### 爆破工兵包中立移除格态(2026-10-03 同调批 T6 拆出——后批立项)
**现状**:同调批 T6 前置半日影响评估(P5 甲)破面判定=**大**:中立移除格态触排雷玩法(第三格态「已移除」——挖掘/插旗/和弦/邻接数字/清空判定全要问它)+战斗结算(完美排雷读数与旗判定基线需定义移除格语义)+清图判定(移除安全格改分母——BoardData 清空判定+MC 清图率读数口径)三面,且底稿欠规格两处口径(被移除雷格是否仍计入邻接数字/移除格数字呈现)实做需追加拍板。用户裁(2026-10-03):拆后批+blast_kit 行标 deferred 留位(effect_kinds cell_removal 已在 schema 枚举、行位保留,后批立项时换装 form=active_charged 即启)。后批开工前置=两处口径拍板+三面改动面任务书。
**归属**:后批(迭代 12 或独立 mini-loop,与反制批/双层 BD 批次排序待用户发令)
**状态**:待立项(拆后批)
## 战斗循环 ## 战斗循环
@@ -270,9 +278,9 @@
#### 迭代 10 奖励重构联动欠账(2026-10-01 登记) #### 迭代 10 奖励重构联动欠账(2026-10-01 登记)
**现状**:迭代 10 奖励重构批(worktree 分支 m3-iter10-20261001,不合入 master、渲染层验证冻结,待用户批准合并)落地面产生三条后续义务——①钥石产出重标:渠道改「Boss/精英击杀掉落」(大奖励 keystone_per_level 退役、boss_keystone 重锚首杀 3/复杀 2 provisional),现行元进度产出结构作废,标定义务重开;②通关率带重扫:通关回血 20%(血回满→部分回血,难度上压方向)+ 三选一件值/权重/次数上限 knob(AP 件 = 每场战斗首轮 +1 AP 强度敏感轴)——难度曲线与Boss P1 行已登记;③同调位读数源切换:本批挂过渡键 transitional_sync_slots(provisional 1),同调实施批落地后切换为 attunement_growth.tres 谓词表实时求值,切换注记随该批。 **现状**:迭代 10 奖励重构批(worktree 分支 m3-iter10-20261001,不合入 master、渲染层验证冻结,待用户批准合并)落地面产生三条后续义务——①钥石产出重标:渠道改「Boss/精英击杀掉落」(大奖励 keystone_per_level 退役、boss_keystone 重锚首杀 3/复杀 2 provisional),现行元进度产出结构作废,标定义务重开;②通关率带重扫:通关回血 20%(血回满→部分回血,难度上压方向)+ 三选一件值/权重/次数上限 knob(AP 件 = 每场战斗首轮 +1 AP 强度敏感轴)——难度曲线与Boss P1 行已登记;③同调位读数源切换:本批挂过渡键 transitional_sync_slots(provisional 1),同调实施批落地后切换为 attunement_growth.tres 谓词表实时求值,切换注记随该批。〔③已清偿(2026-10-03 迭代 11 第①批 T7:过渡键表/schema/断言三处退役,读取点归零,呈现与求值正主=Attunement.slots 谓词表实时求值);①②仍开放〕
**归属**:①元进度设计批 + 曲线调参批 ②曲线调参批(现行窗) ③魔法道具同调实施批 **归属**:①元进度设计批 + 曲线调参批 ②曲线调参批(现行窗) ③~~魔法道具同调实施批~~(已清偿 2026-10-03)
**状态**:已登记(2026-10-01,迭代 10 批随批) **状态**:已登记(2026-10-01,迭代 10 批随批)
@@ -189,3 +189,4 @@ Boss 兑付(胜利或英雄死亡)触发关卡收束点,此后不再有选
| v1.1 | 2026-09-20 | **商店房扩类(GP+商店房批,用户拍板——与冒险结构 / 战斗结算 / 地图战斗资源 / 难度曲线与Boss / registry / 切片同批)**:① 节点封闭枚举四类→五类(+商店房);② 编排规则定稿——每关恰 1、关底 Boss 前一位必经位(哈迪斯式"决战前最后一个补给点"定位);必经位由冒烟断言承载、图合法性六检零新增;③ 兑付定稿——打开商店覆盖层、购买即时生效写统一冒险加算区(与关卡大奖励同账,归冒险结构)、离开后兑付确认;商店房不携带战斗行;④ 节点兑付表 / 纪律表 / 依赖表 / P0 闭环 / 验收标准 / P2「事件类节点候选」注记同步 | | v1.1 | 2026-09-20 | **商店房扩类(GP+商店房批,用户拍板——与冒险结构 / 战斗结算 / 地图战斗资源 / 难度曲线与Boss / registry / 切片同批)**:① 节点封闭枚举四类→五类(+商店房);② 编排规则定稿——每关恰 1、关底 Boss 前一位必经位(哈迪斯式"决战前最后一个补给点"定位);必经位由冒烟断言承载、图合法性六检零新增;③ 兑付定稿——打开商店覆盖层、购买即时生效写统一冒险加算区(与关卡大奖励同账,归冒险结构)、离开后兑付确认;商店房不携带战斗行;④ 节点兑付表 / 纪律表 / 依赖表 / P0 闭环 / 验收标准 / P2「事件类节点候选」注记同步 |
| v1.2 | 2026-09-23 | **停点前移批(冒险入口流程倒置修复,用户 2026-09-23 拍板;上位裁决 `../../../dev/architecture/adr-0006-board-node2d-rendering.md` 与 `../../../dev/architecture/board-node2d-migration-plan.md` §1,代码实装另批并行)**:① 行进循环改停点前移口径——关卡开始即呈现节点图选路停点(关卡画面 = 选路覆盖层常驻化,入口节点不自动进入,第一次选路就是"出发"),选路确认进节点,兑付确认后返回关卡画面再选路(战后回图);兑付后确认、零自动推进两条纪律语义不变,只改承载位;② 呈现侧口径补进度承载条款——关卡画面本体即节点链进度呈现载体(节点态直接读图:已过暗金 / 当前金 / 候选下一跳通道亮金 / Boss 旗朱描边),不另设进度条组件(战斗屏 A 区进度条随批退役);③ P0 闭环两条、依赖表 HUD 框架行、验收标准「行进自主」条同步。**批内代拍板三项已追认(2026-09-24 用户确认)**:① B/C/D 字母保持不重编;② 28px 纵向空间划归 C/D 底行;③ 关卡画面进度呈现由选路覆盖层节点态承载(登记面见 HUD框架 v0.8) | | v1.2 | 2026-09-23 | **停点前移批(冒险入口流程倒置修复,用户 2026-09-23 拍板;上位裁决 `../../../dev/architecture/adr-0006-board-node2d-rendering.md` 与 `../../../dev/architecture/board-node2d-migration-plan.md` §1,代码实装另批并行)**:① 行进循环改停点前移口径——关卡开始即呈现节点图选路停点(关卡画面 = 选路覆盖层常驻化,入口节点不自动进入,第一次选路就是"出发"),选路确认进节点,兑付确认后返回关卡画面再选路(战后回图);兑付后确认、零自动推进两条纪律语义不变,只改承载位;② 呈现侧口径补进度承载条款——关卡画面本体即节点链进度呈现载体(节点态直接读图:已过暗金 / 当前金 / 候选下一跳通道亮金 / Boss 旗朱描边),不另设进度条组件(战斗屏 A 区进度条随批退役);③ P0 闭环两条、依赖表 HUD 框架行、验收标准「行进自主」条同步。**批内代拍板三项已追认(2026-09-24 用户确认)**:① B/C/D 字母保持不重编;② 28px 纵向空间划归 C/D 底行;③ 关卡画面进度呈现由选路覆盖层节点态承载(登记面见 HUD框架 v0.8) |
| v1.3 | 2026-10-01 | **图合法性第七检(T17 目检用户拍板「节点出度最大为 2」)**:`validate` 新增 `fanout_violation`(单节点出边 >2 加载致命);规则连带层宽上限 4(原 5 与出度约束不相容,废止)——生成侧规则与模板池重建归[关卡节点图生成](关卡节点图生成.md) v0.6 同批 | | v1.3 | 2026-10-01 | **图合法性第七检(T17 目检用户拍板「节点出度最大为 2」)**:`validate` 新增 `fanout_violation`(单节点出边 >2 加载致命);规则连带层宽上限 4(原 5 与出度约束不相容,废止)——生成侧规则与模板池重建归[关卡节点图生成](关卡节点图生成.md) v0.6 同批 |
| v1.4 | 2026-10-03 | **奖励节点道具渠道退役注记(迭代 11 第①批同调实施批——与 node-reward.md v2.2 / registry 同批)**:奖励节点的魔法道具件渠道随停点渠道退役拆除(BONUS_IDS 词表除名 aux_item,池五种;三选一刷新件面新增见 node-reward.md v2.2)——奖励节点兑付面=提升三选一(属性/GP/AP 五种)+刷新,零结构改动(节点类型词表/图合法性六检零新增) |
@@ -247,3 +247,4 @@
| v0.13 | 2026-09-20 | **伏击单轮上限删除批同步(与战斗结算 v0.19 / 敌人系统 v0.11 等同批)**:负向即时节 blast 负向的伏击上限护栏与溢出行协议措辞删除,改直授无上限 | | v0.13 | 2026-09-20 | **伏击单轮上限删除批同步(与战斗结算 v0.19 / 敌人系统 v0.11 等同批)**:负向即时节 blast 负向的伏击上限护栏与溢出行协议措辞删除,改直授无上限 |
| v0.14 | 2026-09-20 | **格面资源八型批(用户拍板——与敌人系统 / 战斗结算 / 行动经济 / registry / 切片代码同批)**:① 七型→八型(+action 行动点——正向 ap_up 下轮行动点 +1、负向 ap_drain 迟滞 −1,经 action_economy 排队、下一轮兑现);② blast 负向 ambush_double 退役换装 enemy_alert 警觉(敌方全体护甲等级 +1/层,`status_effects.tres` alert 行,side-wide);③ 极性矩阵/八型字典/负向即时节/候选集合/调参表/结构冻结/验收标准同步;正向权重合计 67→74(attack 14 / defense 11 / blast 16 / heal 2 / weaken 8 / pierce 8 / magic 8 / 清障炸药 1 / action 6) | | v0.14 | 2026-09-20 | **格面资源八型批(用户拍板——与敌人系统 / 战斗结算 / 行动经济 / registry / 切片代码同批)**:① 七型→八型(+action 行动点——正向 ap_up 下轮行动点 +1、负向 ap_drain 迟滞 −1,经 action_economy 排队、下一轮兑现);② blast 负向 ambush_double 退役换装 enemy_alert 警觉(敌方全体护甲等级 +1/层,`status_effects.tres` alert 行,side-wide);③ 极性矩阵/八型字典/负向即时节/候选集合/调参表/结构冻结/验收标准同步;正向权重合计 67→74(attack 14 / defense 11 / blast 16 / heal 2 / weaken 8 / pierce 8 / magic 8 / 清障炸药 1 / action 6) |
| v0.15 | 2026-09-20 | **格面资源九型批(GP+商店房批,用户拍板——格面 GP 获取途径推翻提案"格面池零修订"推荐:定位"低概率惊喜获取"、量与战斗掉落相当;与战斗结算 v0.23 / 冒险结构 v0.5 / 关卡节点地图 v1.1 / registry / 切片代码同批)**:① 八型→九型(+gp 金币——正向 gp_gain 金币袋 weight 2、负向 gp_loss 破财 weight=0 纯字典,power 语义 = 战斗内 GP 缓冲 ±,兑付编排归战斗结算、钱包账本归冒险结构);② 负向恒即时节补 gp 两向缓冲语义(战斗完成随 gp_dropped 统一到账,无库存语义不变);③ 候选集合/调参表/结构冻结(九型封闭枚举)/验收标准同步——正向权重合计 74→76;字典 16 格→18 格 | | v0.15 | 2026-09-20 | **格面资源九型批(GP+商店房批,用户拍板——格面 GP 获取途径推翻提案"格面池零修订"推荐:定位"低概率惊喜获取"、量与战斗掉落相当;与战斗结算 v0.23 / 冒险结构 v0.5 / 关卡节点地图 v1.1 / registry / 切片代码同批)**:① 八型→九型(+gp 金币——正向 gp_gain 金币袋 weight 2、负向 gp_loss 破财 weight=0 纯字典,power 语义 = 战斗内 GP 缓冲 ±,兑付编排归战斗结算、钱包账本归冒险结构);② 负向恒即时节补 gp 两向缓冲语义(战斗完成随 gp_dropped 统一到账,无库存语义不变);③ 候选集合/调参表/结构冻结(九型封闭枚举)/验收标准同步——正向权重合计 74→76;字典 16 格→18 格 |
| v0.16 | 2026-10-03 | **偏运硬币权重投影注记(迭代 11 第①批同调实施批——任务书 §十连带;池结构与冻结项零触碰)**:同调被动件「偏运硬币」装配时对装板摇号(roll_board 身份批,reward_roll 封闭流)注入池内权重投影——稀有正向 kinds {heal, gp, pierce, magic} 行 weight +N(表值 magnitude,provisional),雷安仍同池同分布、池成员集合与取号次数零变化(reward_economy._biased_cell_rows,luck_bias=0 时逐位返回原行集=off 态零扰动);摇号权重合计面 76 为九型基准值、投影后合计随 N 浮动(逐行归因不受影响——抽签按调整后池权重消费同一取号) |
@@ -233,3 +233,4 @@
| v0.4 | 2026-09-19 | 插旗不可逆批(用户拍板全套落地):拔旗动作退役——设计目的/统一价目/价目表/布尔入口清单/P0 闭环/调参表六处删"拔旗",flag_pull_ap_cost 字段随 schema 与 data 表同批删除;插旗行补"旗 = 一次性承诺、纠错 = 直接挖开"口径 | | v0.4 | 2026-09-19 | 插旗不可逆批(用户拍板全套落地):拔旗动作退役——设计目的/统一价目/价目表/布尔入口清单/P0 闭环/调参表六处删"拔旗",flag_pull_ap_cost 字段随 schema 与 data 表同批删除;插旗行补"旗 = 一次性承诺、纠错 = 直接挖开"口径 |
| v0.5 | 2026-09-20 | **格面资源八型批同步(用户拍板——与敌人系统 / 战斗结算 / 地图战斗资源 / registry / 切片代码同批)**:① F1 条款 3 改写——原"敌方伏击暴露不等式"随伏击机制退役作废,惩罚当量对账模型归难度曲线与Boss 调参批重立;② 新增「下轮行动点修正」节——action 型格面资源经 queue_ap_delta 排队、下一轮开始兑现(基线 ap_per_round + 净值、下限 0、兑现后清零),跨轮携带量口径改写(RAP 唯一显式携带资源 + 一次性修正队列);③ 节拍不变式(满停一轮 = 恰好一刷)补扰动注记 | | v0.5 | 2026-09-20 | **格面资源八型批同步(用户拍板——与敌人系统 / 战斗结算 / 地图战斗资源 / registry / 切片代码同批)**:① F1 条款 3 改写——原"敌方伏击暴露不等式"随伏击机制退役作废,惩罚当量对账模型归难度曲线与Boss 调参批重立;② 新增「下轮行动点修正」节——action 型格面资源经 queue_ap_delta 排队、下一轮开始兑现(基线 ap_per_round + 净值、下限 0、兑现后清零),跨轮携带量口径改写(RAP 唯一显式携带资源 + 一次性修正队列);③ 节拍不变式(满停一轮 = 恰好一刷)补扰动注记 |
| v0.6 | 2026-10-01 | **失同步清偿 + 跨冒险行动点加成轴节(迭代 10 奖励重构批——与冒险结构 / registry 同批)**:①失同步欠账清偿——ap_per_round 真实配置 = **8**(action_economy.tres,Package B 2026-09-22 批 6→8 回调,表内批注在案);v0.2 批 8→6 系最后一次文档面记录,此后配置面单独回调未回写本文,本批补记(rap_refresh_cost 与 ap_per_round 的「满预算停手 = 恰好一刷」节拍耦合以现行 8/8 维持);②新增「跨冒险行动点加成轴」节——统一冒险加算区扩第四轴(行动点),经战斗开始入参覆盖值通道、只在每场战斗开轮首轮兑现一次;价值锚 = 冒险表 bonus_ap_flat / bonus_cap_ap(迭代 10 三选一 AP 件,全冒险 2 次上限),调参表补跨表行 | | v0.6 | 2026-10-01 | **失同步清偿 + 跨冒险行动点加成轴节(迭代 10 奖励重构批——与冒险结构 / registry 同批)**:①失同步欠账清偿——ap_per_round 真实配置 = **8**(action_economy.tres,Package B 2026-09-22 批 6→8 回调,表内批注在案);v0.2 批 8→6 系最后一次文档面记录,此后配置面单独回调未回写本文,本批补记(rap_refresh_cost 与 ap_per_round 的「满预算停手 = 恰好一刷」节拍耦合以现行 8/8 维持);②新增「跨冒险行动点加成轴」节——统一冒险加算区扩第四轴(行动点),经战斗开始入参覆盖值通道、只在每场战斗开轮首轮兑现一次;价值锚 = 冒险表 bonus_ap_flat / bonus_cap_ap(迭代 10 三选一 AP 件,全冒险 2 次上限),调参表补跨表行 |
| v0.7 | 2026-10-03 | **两 AP 件兑现通道注记(迭代 11 第①批同调实施批——任务书 §十连带)**:同调目录两 AP 件兑现通道落位——①工兵晨令(被动·限时)=战斗开始首轮 queue_ap_delta 入队 + 轮始段续队(前 effective_rounds 轮每轮 +1,表值化 P4 甲),复用「跨冒险行动点加成轴」同族排队通道;②掘进蓄能(主动·充能)=grant_ap 当轮加量(同挖掘期 ap_grant 通道),受理闸=战斗域安全开格积点(charge_n 格/点);账本零新机制(queue_ap_delta/grant_ap 两既有原语),充能扣减经共享账本(attune_charges)同引用直改 |
@@ -1,18 +1,26 @@
# 魔法道具 # 魔法道具
> 一句话:扫雷侧魔法道具(显影、探测类) > 一句话:扫雷侧魔法道具(同调机制件——营地装配、机制轴唯一来源)
> 状态:已定稿(自旧体系迁移) > 状态:已定稿(2026-10-03 迭代 11 第①批同调实施批——终局形态落籍,过渡形态退役)
> 形态沿革:现行正文=切片期过渡形态;终局=局外同调件(2026-09-30 用户裁决甲)——见下节「定位与形态沿革」 > 形态沿革:~~过渡形态~~已退役(2026-10-03);现行=局外同调件——见下节「定位与形态沿革」
> 旧版参考:../../../archive/gdd-2026-09-14/a5-minesweeper-aux-items.md > 旧版参考:../../../archive/gdd-2026-09-14/a5-minesweeper-aux-items.md
## 定位与形态沿革(2026-09-30 用户裁决甲) ## 定位与形态沿革(2026-09-30 用户裁决甲;2026-10-03 同调实施批落籍终局形态)
魔法道具存在**两态**,关系=形态替换而非机制积累: 魔法道具存在**两态**,关系=形态替换而非机制积累。**替换已于 2026-10-03 完成**(M-4 迭代 11 第①批,worktree m4-iter11a-attune-20261003):
- **现行(游戏内已实装,2026-09-29 迭代 7 落籍)=切片期过渡形态**:道具作为掉落奖励类别进入掉落池(现行投放三件:工兵令/探巢摆钟/爆破工兵包)、奖励节点三选一构筑点取用、关卡域库存、挖掘期两步使用——即本文以下全部正文章节所述形态。 - ~~**过渡形态**(2026-09-29 迭代 7 落籍)~~ **已退役**:掉落投放(2026-10-01 已随「挖格不掉道具」退役)/奖励节点三选一道具卡(2026-10-03 T7 出池)/关卡域库存/挖掘期两步使用——本文以下正文章节「掉落池获取」「关卡域库存」「什么时候怎么用」诸节随之整体退役,保留为历史口径(本节横幅为准)。
- **终局=局外同调件**:魔法道具重定义为「开局前在营地装配、占同调位、带每冒险充能的机制增强件」。设计权威底稿 = [proposal-magic-item-attunement-2026-09-22.md](../../../research/proposal-magic-item-attunement-2026-09-22.md)(2026-09-22 用户八条拍板在案:局内不掉落机制道具、三轴 doctrine〔跨局数值轴=属性锻造/局内数值轴=GP 商店/局外装配机制轴=魔法道具〕、同调位 0 起步硬上限 3、增长=通关次数+Boss 击杀数走里程碑谓词表、真视窄域护栏等承重资产继承清单见提案 §1.1)。 - **终局形态(现行)= 局外同调件**:开局前在营地装配(营地·道具工坊柜台「同调装配」段)、占同调位(0 起步、硬上限 3,slot_cost 恒 1)、被动件整局生效/主动件每冒险充能(冒险开始重置)。设计权威底稿 = [proposal-magic-item-attunement-2026-09-22.md](../../../research/proposal-magic-item-attunement-2026-09-22.md)(2026-09-22 用户八条拍板)+ 任务书 v1.1(P1-P8 全裁)。
**衔接纪律**:过渡形态的各项现行拍板(探宝罗盘退役、工坊解锁门控、三选一投放、投放权重)在同调实施批替换前继续有效;同调实施批立项时按提案 §1.1/§6/§9 执行替换——本文「掉落池获取」「关卡域库存」「奖励节点投放」诸节整体退役改写。实施批登记见 [06-待办清单](../../06-待办清单.md)「魔法道具同调实施批」条。 **现行形态速览**(实现面 = `game/data/tables/attunement_growth.tres` + `aux_items.tres` + `game/core/attunement.gd`):
- **同调位增长**:通关次数 + Boss 击杀数(含重复,收束入账 save v4)经 `attunement_growth.tres` 里程碑谓词表实时求值(Σ达成 grant clamp slot_cap)——试玩调平衡=改表不动码(迭代 10 欠账③已清偿:过渡键 transitional_sync_slots 退役)。
- **目录九件**(值全 provisional 归难度曲线与Boss):被动六件=淬毒刃纹(克制面穿刺/魔法直伤 +N)/标中殉爆(每标中一旗全体存活敌吃 N 群伤)/惊巢反制(挖雷惩罚减半+反弹 N)/工兵晨令(前 effective_rounds 轮每轮 +1 AP,P4 甲表值化)/偏运硬币(装板摇号稀有正向 kinds 权重 +N,池结构零触碰)/三选一刷新件(X 次/每碰三选一);主动三件=掘进蓄能(每开够 N 安全格积 1 点发起临时 AP,charges 2/冒险)/探巢摆钟(3×3 真视窄域,charges 2/冒险)/爆破工兵包(**deferred 留位**——P5 甲影响评估破面大,2026-10-03 用户裁拆后批,见 06「爆破工兵包中立移除格态」条)。
- **三选一刷新双源**(P1 甲):X=刷新件每碰三选一重给(优先消耗、不结转)+ Y=营地刷新升级行(每冒险开局注入冒险周期池、用完即无;meta.tres attune_refresh_upgrade 行族);重抽消费 reward_pick 既有洗牌流,池规则全保留;autoplay 默认不刷(钓鱼归玩家)。
- **🔴 承重契约**(schema 加载期强制):机制轴禁裸 flat 数值(行键白名单封闭);真视窄域 ≤3×3;核心层零 await;零随机;排雷玩法零特判;雷安同池冻结(硬币只动权重投影)。
- **验收分层**:attune=off 续用现行带;attune=on 带平移归难度曲线与Boss 投值(商店 shop=off/on 分层先例同法)。
**衔接纪律**(历史):过渡形态各项拍板在实施批替换前有效——替换已完成,本节留作事故留痕与口径溯源。实施批执行记录见 06「魔法道具同调实施批」条(已收口)。
> ⚠ 事故留痕(防复发):同调提案 2026-09-22 落档后未登记正主、未立项、未进待办,后续规划会话与 2026-09-29 迭代 7 裁决时点均不可见,致两套均有用户拍板记录的形态无文档声明关系并存七日(2026-09-30 审计发现、用户裁决甲收口,全录见审计报告 git 提交 c095916 C29,报告已退役)。 > ⚠ 事故留痕(防复发):同调提案 2026-09-22 落档后未登记正主、未立项、未进待办,后续规划会话与 2026-09-29 迭代 7 裁决时点均不可见,致两套均有用户拍板记录的形态无文档声明关系并存七日(2026-09-30 审计发现、用户裁决甲收口,全录见审计报告 git 提交 c095916 C29,报告已退役)。
@@ -162,3 +162,4 @@ MVP 不要求本系统的闭环([不做清单](../../05-不做清单.md)整体
| v1.3 | 2026-09-21 | **元进度实现批(与切片代码 / registry / 02 / 03 / 05 / 06 同批)**:本批 = 本文的实现批——MetaTable 四行族建表(`data/tables/meta.tres`,数值全 provisional 归设计批配读数);MetaProgression 账本服务(余额/几何定价/购买/工坊解锁/Boss 两档入账/注入计算,Core 层零 UI 零信号);注入通道落地(start_adventure params.hero_override → LevelRunner base_override,叠加序 = 表值+永久强化 flat+加算区 flat × 测试面板 pct);呈现面(营地覆盖层三柜台+主菜单入口+A 区顶栏钥石余额+结算屏入账明细,ui_kit 程序占位——视觉终稿归 HUD 协作批)。正文零改动(实现面不回流正文——正文零历史叙述纪律) | | v1.3 | 2026-09-21 | **元进度实现批(与切片代码 / registry / 02 / 03 / 05 / 06 同批)**:本批 = 本文的实现批——MetaTable 四行族建表(`data/tables/meta.tres`,数值全 provisional 归设计批配读数);MetaProgression 账本服务(余额/几何定价/购买/工坊解锁/Boss 两档入账/注入计算,Core 层零 UI 零信号);注入通道落地(start_adventure params.hero_override → LevelRunner base_override,叠加序 = 表值+永久强化 flat+加算区 flat × 测试面板 pct);呈现面(营地覆盖层三柜台+主菜单入口+A 区顶栏钥石余额+结算屏入账明细,ui_kit 程序占位——视觉终稿归 HUD 协作批)。正文零改动(实现面不回流正文——正文零历史叙述纪律) |
| v1.4 | 2026-09-23 | **换算率标定批(用户指令「现在标定」;与切片代码 / registry / 06-待办清单 / 判定端工具同批)**:换算率节回填现行表值(生命 6 / 攻击 9 / 护甲 1 点每级——OFAT 冒险注入档与白板同种子配对的逐关条件通关率差实测,冒险条件口径;同日单关直入口口径退役使首轮直入口面读数作废重测);护甲行强条目重测履行并暴露修复护甲断链(begin_battle hero_armor_class 参数零消费→_defender_ac 接线);判定端带平移表值驱动接线(带重锚后随换算率平移判);排期表 P1 行销项 | | v1.4 | 2026-09-23 | **换算率标定批(用户指令「现在标定」;与切片代码 / registry / 06-待办清单 / 判定端工具同批)**:换算率节回填现行表值(生命 6 / 攻击 9 / 护甲 1 点每级——OFAT 冒险注入档与白板同种子配对的逐关条件通关率差实测,冒险条件口径;同日单关直入口口径退役使首轮直入口面读数作废重测);护甲行强条目重测履行并暴露修复护甲断链(begin_battle hero_armor_class 参数零消费→_defender_ac 接线);判定端带平移表值驱动接线(带重锚后随换算率平移判);排期表 P1 行销项 |
| v1.5 | 2026-10-01 | **钥石渠道改一批(迭代 10 奖励重构——用户 2026-09-30 拍板;与冒险结构 v0.12 / registry 同批)**:钥石渠道自二改一——「关卡大奖励入账(keystone_per_level)」随大奖励三件套退役,渠道 = Boss/精英怪击杀掉落(逐杀实时入账,行值读 meta.tres boss_keystone 首杀/复杀两档;精英掉 1 预留);首杀大额/复杀小额规则不变。**产出重标义务重开**:现行两关冒险产出结构从「大奖励 10×2 + Boss 首杀 5」改「Boss 掉落 ×N(精英未实装零贡献)」,标定随元进度设计批/调参批重开(06 登记) | | v1.5 | 2026-10-01 | **钥石渠道改一批(迭代 10 奖励重构——用户 2026-09-30 拍板;与冒险结构 v0.12 / registry 同批)**:钥石渠道自二改一——「关卡大奖励入账(keystone_per_level)」随大奖励三件套退役,渠道 = Boss/精英怪击杀掉落(逐杀实时入账,行值读 meta.tres boss_keystone 首杀/复杀两档;精英掉 1 预留);首杀大额/复杀小额规则不变。**产出重标义务重开**:现行两关冒险产出结构从「大奖励 10×2 + Boss 首杀 5」改「Boss 掉落 ×N(精英未实装零贡献)」,标定随元进度设计批/调参批重开(06 登记) |
| v1.6 | 2026-10-03 | **同调位账本批(迭代 11 第①批同调实施批——save v4 / meta.tres attune_refresh_upgrade 行族 / registry 同批)**:①道具工坊柜台职责改写——解锁→谓词表形态(unlock_requires 行,首批全解锁=空谓词合法态,底稿 §2.2 用户裁),柜台内新增「同调装配」段(装配/换装/Slots 点亮 + Y 刷新升级行购买——程序占位面,视觉终稿归营地批;满位禁选/写档持久 MetaProgression.toggle_attunement 唯一写口);②Boss 击杀总数计数登记(boss_kill_count 含重复,收束入账=apply_adventure_summary 逐通关关次 +1,attunement_growth 谓词表计数源);③Y 刷新升级行登记(attune_refresh_upgrade 行族 L1→Y1/L2→Y2,钥石直购,provisional);④装配关系入存档持久(attunement_equipped save v4 域,冒险中途不可换装——底稿 §8.5) |
+8 -8
View File
@@ -1,27 +1,26 @@
# 节点奖励屏 UX spec(node-reward) # 节点奖励屏 UX spec(node-reward)
> 状态:v2.0(2026-10-01 迭代 10 追加批重写——用户拍板:奖励节点三选一与通关三选一**共享同池**;原「魔法道具投放渠道」过渡形态退役) > 状态:v2.2(2026-10-03 迭代 11 第①批同调实施批——aux_item 池种退役出三选一牌库,池五种;新增三选一刷新件面)
> 事实源:`game/presentation/views/bonus_pick_overlay.gd`(三选一覆盖层共用面)+ `main.gd`(奖励节点分支——`adventure.node_bonus_candidates/apply_node_bonus`)+ `battle_narrator.gd`(node_bonus_header) > 事实源:`game/presentation/views/bonus_pick_overlay.gd`(三选一覆盖层共用面)+ `main.gd`(奖励节点分支——`adventure.node_bonus_candidates/apply_node_bonus`)+ `battle_narrator.gd`(node_bonus_header)
> 相邻:[level-clear](level-clear.md)(通关三选一面——同一覆盖层的另一上下文)/ [level-map](level-map.md)(关卡画面)/ [魔法道具](../systems/排雷循环/魔法道具.md)(道具件内容权威)/ [冒险结构](../systems/冒险循环/冒险结构.md)(提升池单例与次数上限) > 相邻:[level-clear](level-clear.md)(通关三选一面——同一覆盖层的另一上下文)/ [level-map](level-map.md)(关卡画面)/ [魔法道具](../systems/排雷循环/魔法道具.md)(道具件内容权威——本批起为局外同调件,不再入三选一牌库)/ [冒险结构](../systems/冒险循环/冒险结构.md)(提升池单例与次数上限)
## 定位 ## 定位
奖励节点(reward node)到达即开的**提升三选一选择面**:与通关三选一共享同一提升池(六种:攻击/生命上限/护甲/GP 袋/魔法道具/行动点件)——同权重、同全冒险次数上限、同 reward_pick 封闭流。与通关面的差异仅在上下文:无部分回血、无钥石增量行,兑付后「继续」回图选路(不推进关卡)。 奖励节点(reward node)到达即开的**提升三选一选择面**:与通关三选一共享同一提升池(**五种**:攻击/生命上限/护甲/GP 袋/行动点件——v2.2 起魔法道具件随同调批退役出池)——同权重、同全冒险次数上限、同 reward_pick 封闭流。与通关面的差异仅在上下文:无部分回血、无钥石增量行,兑付后「继续」回图选路(不推进关卡)。
## 布局与交互 ## 布局与交互
- 载体 = 提升三选一覆盖层(BonusPickOverlay,与 [level-clear](level-clear.md) 共用组件——零新增交互原语)。 - 载体 = 提升三选一覆盖层(BonusPickOverlay,与 [level-clear](level-clear.md) 共用组件——零新增交互原语)。
- 头部行:「关卡奖励点 / 从提升池抽三选一(与通关三选一同池同权重同上限):选择一件带走:」(Narrator.node_bonus_header)。 - 头部行:「关卡奖励点 / 从提升池抽三选一(与通关三选一同池同权重同上限):选择一件带走:」(Narrator.node_bonus_header)。
- 三卡点选(✓ 高亮,确认前可改选)→「确认选择」→ 兑付反馈(选中件效果 + 丢弃提示)→「继续」→ 回图选路(零自动推进同款)。 - 三卡点选(✓ 高亮,确认前可改选)→「确认选择」→ 兑付反馈(选中件效果 + 丢弃提示)→「继续」→ 回图选路(零自动推进同款)。
- **魔法道具件直显具体道具**(掷定前置,v2.1):候选抽定时即掷定具体道具,卡片显示「魔法道具 / 探巢摆钟 · 真视 3×3」——不再是「随机获得一件」盲选。 - ~~魔法道具件直显/满位替换~~(v2.1 两面随池种退役——迭代 11 第①批 T7;魔法道具改走营地同调装配面,见[魔法道具](../systems/排雷循环/魔法道具.md))。
- 魔法道具件满位:替换子面板(现有库存件 + 新件 N+1 选 N,丢旧收新 / 放弃新件两路——与通关面同构;丢弃目标 = 关卡域库存件句柄)。 - **三选一刷新**(v2.2 新增——P1 甲 X/Y 双源):装配「三选一刷新件」给 X 次(每次碰三选一重给、优先消耗、不结转)+ 营地「刷新升级」给 Y 次(每冒险开局注入冒险周期池、用完即无);刷新钮余量 >0 时现形,点按重抽三候选(reward_pick 既有洗牌流,池规则全保留:不放回/达上限剔牌/掷定前置直显/停点可中断)。autoplay 默认不刷(钓鱼归玩家)。
## 口径 ## 口径
- **同池同上限**:件获取次数计数为冒险级单例——奖励节点与通关三选一两渠道合计判上限(达上限件在两渠道同样剔牌库);权重与牌库口径读冒险表(registry adventure_table 为准)。 - **同池同上限**:件获取次数计数为冒险级单例——奖励节点与通关三选一两渠道合计判上限(达上限件在两渠道同样剔牌库);权重与牌库口径读冒险表(registry adventure_table 为准)。
- 道具件入**本关库存**(即时可用,来源归因 `reward_node`)——区别于通关三选一的跨关携带(清库后授予、下一关生效)。 - ~~道具件入本关库存~~(v2.2 退役——魔法道具局内不掉落机制道具,三轴 doctrine 锁死)。
- 「池空直通」合法态随共享池退役:池含无次数上限的 GP/道具件,牌库恒非空。 - 「池空直通」合法态随共享池退役:池含无次数上限的 GP 件,牌库恒非空。
- 退役件(探宝罗盘)/失义件不入道具件随机池(同调位约束口径不变,归[魔法道具](../systems/排雷循环/魔法道具.md))。
## 版本历史 ## 版本历史
@@ -30,4 +29,5 @@
| v1.0 | 2026-09-29 | 迭代 7 T5 随批出生(三选一构筑点形态——用户拍板②丙) | | v1.0 | 2026-09-29 | 迭代 7 T5 随批出生(三选一构筑点形态——用户拍板②丙) |
| v2.0 | 2026-10-01 | **共享池改写(迭代 10 追加批——用户拍板「奖励节点与通关三选一相同、共享同一个池子」)**:①定位改提升三选一选择面(六种共享池、同权重同次数上限、同 reward_pick 流);②载体改 BonusPickOverlay 共用面(原 node_overlay 道具三选一面退役);③道具件满位升级为替换子交互两路(原过渡形态=满位不授予);④「池空直通」合法态退役(牌库恒非空);⑤道具件入本关库存(即时可用),区别于通关渠道的跨关携带 | | v2.0 | 2026-10-01 | **共享池改写(迭代 10 追加批——用户拍板「奖励节点与通关三选一相同、共享同一个池子」)**:①定位改提升三选一选择面(六种共享池、同权重同次数上限、同 reward_pick 流);②载体改 BonusPickOverlay 共用面(原 node_overlay 道具三选一面退役);③道具件满位升级为替换子交互两路(原过渡形态=满位不授予);④「池空直通」合法态退役(牌库恒非空);⑤道具件入本关库存(即时可用),区别于通关渠道的跨关携带 |
| v2.1 | 2026-10-01 | **掷定前置(目检反馈批——用户反馈「抽到魔法道具不显示详情不直观」,推荐案获批)**:道具件具体道具随候选抽定(reward_pick 流序内挪,消费数不变),卡片直显「魔法道具 / 道具名 · 效果摘要」;断点停点快照随载 aux_detail(续玩卡片详情不丢);node_bonus_candidates 返回扩 dict {candidates, aux_detail} | | v2.1 | 2026-10-01 | **掷定前置(目检反馈批——用户反馈「抽到魔法道具不显示详情不直观」,推荐案获批)**:道具件具体道具随候选抽定(reward_pick 流序内挪,消费数不变),卡片直显「魔法道具 / 道具名 · 效果摘要」;断点停点快照随载 aux_detail(续玩卡片详情不丢);node_bonus_candidates 返回扩 dict {candidates, aux_detail} |
| v2.2 | 2026-10-03 | **池种退役+刷新件面(迭代 11 第①批同调实施批 T7/T5b——P1 甲)**:①aux_item 池种退役出三选一牌库(BONUS_IDS 除名+权重/件数/掷定前置字段随批退役)——魔法道具改走营地同调装配面(三轴 doctrine),v2.1 直显/满位替换两面随渠道退役(机械惰性承载零投放);②三选一刷新件面新增(X/Y 双源:刷新件每碰三选一重给优先消耗不结转+营地刷新升级冒险周期池;重抽消费 reward_pick 既有洗牌流池规则全保留;autoplay 默认不刷) |
| v2.2 | 2026-10-03 | **三档位令牌池(用户全裁——待 M-4 数值批实施,现行 v2.1 平铺池维持到实施)**:池改固定配额令牌池(普通 70/稀有 25/罕见 5=100 张;洗牌不放回抽 3、选中踢池未选回池、池耗即枯竭);档位构成=普通〔小GP 18/小增伤 18/小护甲 17/小生命 17〕+稀有〔大GP/大增伤/大护甲/大生命/恢复 各 5〕+罕见〔AP 3/道具 2〕;数值对 20/50、+1/+2、+1/+2、+5/+10、恢复+6、AP+1、道具 1 件;**种类次数上限退役**(池消耗即上限;同调位上限保留);恢复件=新入池种类;MC 策略改判 1 级最稀有/2 级最左(确定性)——设计全稿与执行口径见 [06 待办「奖励池三档位令牌池重设计」条](../06-待办清单.md);卡片呈现面(本 spec 交互节)零改动 | | v2.2 | 2026-10-03 | **三档位令牌池(用户全裁——待 M-4 数值批实施,现行 v2.1 平铺池维持到实施)**:池改固定配额令牌池(普通 70/稀有 25/罕见 5=100 张;洗牌不放回抽 3、选中踢池未选回池、池耗即枯竭);档位构成=普通〔小GP 18/小增伤 18/小护甲 17/小生命 17〕+稀有〔大GP/大增伤/大护甲/大生命/恢复 各 5〕+罕见〔AP 3/道具 2〕;数值对 20/50、+1/+2、+1/+2、+5/+10、恢复+6、AP+1、道具 1 件;**种类次数上限退役**(池消耗即上限;同调位上限保留);恢复件=新入池种类;MC 策略改判 1 级最稀有/2 级最左(确定性)——设计全稿与执行口径见 [06 待办「奖励池三档位令牌池重设计」条](../06-待办清单.md);卡片呈现面(本 spec 交互节)零改动 |
+23 -2
View File
@@ -69,8 +69,8 @@ entities:
attributes: attributes:
path: user://save.json(单档 MVP——多存档槽位 P2 保留) path: user://save.json(单档 MVP——多存档槽位 P2 保留)
format: JSON 单档(人可读可调试;字典键一律 String) format: JSON 单档(人可读可调试;字典键一律 String)
version: SAVE_FORMAT_VERSION=2(registry schema_version 同源;v2=断点域出生 2026-09-29 迭代 8 卡①,生产迁移链 v1→v2 补空;高版本拒读 + 禁写盘 + 内存默认新档运行;低版本读档时链式迁移 register_migration,任一步失败按损坏兜底) version: SAVE_FORMAT_VERSION=4(registry schema_version 同源;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→v4 链式补空;高版本拒读 + 禁写盘 + 内存默认新档运行;低版本读档时链式迁移 register_migration,任一步失败按损坏兜底)
domains: meta 域六件——keystone_balance(内容 owner=元进度)/ forge_levels 三档 max_hp·atk·armor_class(元进度)/ item_unlocks(元进度·道具工坊消费)/ level_clear_counts(元进度)/ boss_first_kill(元进度)/ recent_summaries cap 10 冒险摘要五件事副本(内容 owner=冒险结构;元素附 boss_drops 副本——Boss 两档金额入账随元进度实现批 2026-09-21 接通,apply_adventure_summary 第二参);+ breakpoint 域(v2 新增——冒险断点快照,内容 owner=冒险结构:七字段拍板面+附随件〔主种子/取数计数器/钥石累计/已到节点序/道具库存〕,空字典=无断点合法态,三结局清除) 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 刷新升级购买等级)/ recent_summaries cap 10(冒险结构,元素附 boss_drops 副本);+ breakpoint 域(断点快照:七字段拍板面+附随件〔主种子/取数计数器/钥石累计/已到节点序/道具库存/bonus_* 附随件/attunement_charges 充能余量——迭代 11 第①批 T6〕,空字典=无断点合法态,三结局清除)
write_timing: 启动读盘;正式冒险收束(adventure_finished → apply_adventure_summary)写盘;营地每笔钥石变更后即时写(元进度实现批接线);冒险选路停点断点快照写盘 + 三结局清断点写盘(迭代 8 卡①);调试两层与单关测试路径(LevelRunner.start_level,仅 QA 工具)不写档 write_timing: 启动读盘;正式冒险收束(adventure_finished → apply_adventure_summary)写盘;营地每笔钥石变更后即时写(元进度实现批接线);冒险选路停点断点快照写盘 + 三结局清断点写盘(迭代 8 卡①);调试两层与单关测试路径(LevelRunner.start_level,仅 QA 工具)不写档
atomic_write: tmp + rename 原子写(写一半断电不留烂档——钥石不丢判据承载) atomic_write: tmp + rename 原子写(写一半断电不留烂档——钥石不丢判据承载)
corruption_policy: 坏档改名 <path>.corrupt.bak(已存在覆盖)+ 默认新档继续 + 警告留痕(不静默丢档不 crash) corruption_policy: 坏档改名 <path>.corrupt.bak(已存在覆盖)+ 默认新档继续 + 警告留痕(不静默丢档不 crash)
@@ -505,8 +505,29 @@ entities:
ux_flag: 有 UI 贡献(库存条/使用交互/叠加层呈现)——Phase 4 前置 /ux-design;asset-spec 候选(art-bible 样张批后) ux_flag: 有 UI 贡献(库存条/使用交互/叠加层呈现)——Phase 4 前置 /ux-design;asset-spec 候选(art-bible 样张批后)
closures: 地图战斗资源 OQ2 魔法道具 侧收口(呈现深度 = 道具驱动预读;未预读格不呈现、自动寻路不做——终态与否 OQ6)· 创意风险 #1 道具侧兑现(plan 维度预读——结构闭环最后一块,呈现归 HUD框架)· 显影语义(原为雾格显影 = fog_reveal)随雾格移除失义、载体待重定(随魔法道具整体重设计归 Alpha,2026-09-20 用户拍板) closures: 地图战斗资源 OQ2 魔法道具 侧收口(呈现深度 = 道具驱动预读;未预读格不呈现、自动寻路不做——终态与否 OQ6)· 创意风险 #1 道具侧兑现(plan 维度预读——结构闭环最后一块,呈现归 HUD框架)· 显影语义(原为雾格显影 = fog_reveal)随雾格移除失义、载体待重定(随魔法道具整体重设计归 Alpha,2026-09-20 用户拍板)
warnings: 预警①…④ playtest 预注册归 难度曲线与Boss(道具依赖度/预读覆盖率/AP 转存联动/稀有感知) 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)
referenced_by: [战斗结算, 地图战斗资源, 地形变异, 行动经济, HUD框架, 数据配置, 教学引导, art-bible] referenced_by: [战斗结算, 地图战斗资源, 地形变异, 行动经济, HUD框架, 数据配置, 教学引导, art-bible]
- name: attunement_growth
type: schema_data
owner: design/gdd/systems/排雷循环/魔法道具.md
attributes:
table: AttunementGrowthTable(数据配置 承载,单行全局常量表——AdventureTable 单行先例 Rule 4 豁免;迭代 11 第①批 同调实施批 T1 落籍 2026-10-03;设计权威 = proposal-magic-item-attunement-2026-09-22 §2.1 用户点名要求)
fields: slot_cap int [0,9](硬上限 3——底稿裁决)· milestones 行族 {id 唯一, grant ∈ [1,slot_cap], requires = (计数键,阈值) AND 列表非空}——同调位读数唯一口径 = Σ达成 grant clamp slot_cap
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 行示例形态)
referenced_by: [魔法道具, 元进度, 数据配置, 冒险结构]
- name: attunement_slots
type: convention
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 欠账③清偿
layered_acceptance: attune=off 续用现行带 · attune=on 带平移归 难度曲线与Boss 投值(商店 shop=off/on 分层先例同法)
referenced_by: [魔法道具, 元进度, 数据配置]
- name: difficulty_table - name: difficulty_table
type: table type: table
owner: design/gdd/systems/冒险循环/难度曲线与Boss.md owner: design/gdd/systems/冒险循环/难度曲线与Boss.md
+105 -33
View File
@@ -72,6 +72,10 @@ var _base_override := {"atk": 0, "max_hp": 0, "armor_class": 0}
var _first_script := false # 首次脚本模式(关卡节点图生成 v0.4——true = 全关零抽签取池内模板 0;测量夹具恒 false) # 永久强化 flat 注入(元进度实现批——冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板) var _first_script := false # 首次脚本模式(关卡节点图生成 v0.4——true = 全关零抽签取池内模板 0;测量夹具恒 false) # 永久强化 flat 注入(元进度实现批——冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板)
var _last_aux_cleared := 0 # additive(迭代 7 T4——最近一次关卡末清空计数,summary 取数面) var _last_aux_cleared := 0 # additive(迭代 7 T4——最近一次关卡末清空计数,summary 取数面)
var _aux_unlocked := {} # aux 解锁门控集(迭代 7 T5——params.aux_unlocked 接收面) var _aux_unlocked := {} # aux 解锁门控集(迭代 7 T5——params.aux_unlocked 接收面)
var _attunement: Array = [] # 同调装配集(迭代 11 第①批 T5——params.attunement 接收面,start_level 外下传 LevelRunner)
var _attune_refresh_x := 0 # 刷新件余量(迭代 11 第①批 T5b P1 甲——每碰三选一重置=X 表值;优先消耗、不结转)
var _attune_refresh_y := 0 # Y 冒险周期池(开局参数面注入,用完即无——营地升级行表值)
var attune_charges := {} # 同调主动件冒险充能账本(迭代 11 第①批 T6——跨关卡共享 dict;settlement use_aux 扣减直改同引用;断点随 attunement_charges 键入档)
var _save_manager = null # 断点承载注入(迭代 8 卡①——null = 不写断点;模拟/冒烟缺省) var _save_manager = null # 断点承载注入(迭代 8 卡①——null = 不写断点;模拟/冒烟缺省)
var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递) var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递)
# ---- 三选一停点态(迭代 10 奖励重构批)---- # ---- 三选一停点态(迭代 10 奖励重构批)----
@@ -112,7 +116,13 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
_pending_next_level = false _pending_next_level = false
_base_override = _normalize_override(params.get("hero_override", {})) _base_override = _normalize_override(params.get("hero_override", {}))
_first_script = bool(params.get("first_script", false)) # 缺省 = 非首次(抽签路径;真实侧由呈现层读存档布尔位传入) _first_script = bool(params.get("first_script", false)) # 缺省 = 非首次(抽签路径;真实侧由呈现层读存档布尔位传入)
_aux_unlocked = params.get("aux_unlocked", {}) if params.get("aux_unlocked", {}) is Dictionary else {} # additive(迭代 7 T5) _aux_unlocked = params.get("aux_unlocked", {}) if params.get("aux_unlocked", {}) is Dictionary else {}
_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——冒险开始充能账本按装备主动件重置(每冒险充能语义) # additive(迭代 7 T5)
_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——冒险开始充能账本按装备主动件重置(每冒险充能语义)
ledger["hero_hp"] = effective_max_hp() # 生命账本置满(生效上限口径——T17 修复:含 TunePanel pct,与战斗内同源) ledger["hero_hp"] = effective_max_hp() # 生命账本置满(生效上限口径——T17 修复:含 TunePanel pct,与战斗内同源)
state = "running" state = "running"
return _start_current_level() return _start_current_level()
@@ -168,6 +178,9 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
int(round(float(int(db.hero.max_hp) + int(_base_override.get("max_hp", 0))) * (100.0 + float(_override[1])) / 100.0)))) int(round(float(int(db.hero.max_hp) + int(_base_override.get("max_hp", 0))) * (100.0 + float(_override[1])) / 100.0))))
_first_script = false # 续玩模板已随快照强制(已抽模板零重抽)——脚本模式不重放 _first_script = false # 续玩模板已随快照强制(已抽模板零重抽)——脚本模式不重放
_aux_unlocked = params.get("aux_unlocked", {}) if params.get("aux_unlocked", {}) is Dictionary else {} _aux_unlocked = params.get("aux_unlocked", {}) if params.get("aux_unlocked", {}) is Dictionary else {}
_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——冒险开始充能账本按装备主动件重置(每冒险充能语义)
# 三选一停点续玩分支(迭代 10——选择态/已兑付态不重跑关卡;奖励选择态不重抽、已兑付不重付) # 三选一停点续玩分支(迭代 10——选择态/已兑付态不重跑关卡;奖励选择态不重抽、已兑付不重付)
var bp_pending: Dictionary = bp.get("bonus_pending", {}) as Dictionary var bp_pending: Dictionary = bp.get("bonus_pending", {}) as Dictionary
var bp_result: Dictionary = bp.get("bonus_result", {}) as Dictionary var bp_result: Dictionary = bp.get("bonus_result", {}) as Dictionary
@@ -197,6 +210,10 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
bonus_resolved.emit(_bonus_result.duplicate(true)) bonus_resolved.emit(_bonus_result.duplicate(true))
return true return true
_resume_bp = bp.duplicate(true) _resume_bp = bp.duplicate(true)
# 迭代 11 第①批 T6——充能账本随断点回局(缺键 = 旧档/无断点,保持 _seed_attune_charges 种子态)
var bp_charges = bp.get("attunement_charges", null)
if bp_charges is Dictionary:
attune_charges = (bp_charges as Dictionary).duplicate(true)
state = "running" state = "running"
return _start_current_level() return _start_current_level()
@@ -315,15 +332,11 @@ func _pick_aux_item(count: int) -> bool:
push_error("[冒险结构] pick_bonus(aux_item) 拒绝——解锁道具池空集(白板/门控态不入牌库的防御兜底)") push_error("[冒险结构] pick_bonus(aux_item) 拒绝——解锁道具池空集(白板/门控态不入牌库的防御兜底)")
return false return false
var hp_before := int(ledger.get("hero_hp", 0)) var hp_before := int(ledger.get("hero_hp", 0))
if AuxItems.can_accept(picked.size(), _bonus_aux_carry.size(), db): # 同调位守卫单点(2026-10-02) # 迭代 11 第①批 T7——同调位守卫随过渡键退役拆除(aux 卡出池后本路径不可达;能力件容量闸
for iid in picked: # 正主 = 营地装配面 Attunement.slots 满位禁选)
_bonus_aux_carry.append(String(iid)) for iid in picked:
_finish_bonus_pick("aux_item", picked.size(), hp_before) _bonus_aux_carry.append(String(iid))
return true _finish_bonus_pick("aux_item", picked.size(), hp_before)
# 满位替换子交互:现有持有件 + 新件 N+1 选 N(丢旧收新 / 放弃新件两路,终态恒 ≤ 同调位)
_bonus_pending["chosen"] = "aux_item"
_bonus_pending["incoming"] = picked.duplicate()
aux_replace_presented.emit(_carry_records(), {"index": -1, "item_id": String(picked[0]), "count": picked.size()})
return true return true
@@ -372,6 +385,7 @@ func node_bonus_candidates() -> Dictionary:
func present_node_bonus(candidates: Array, aux_detail: Array = []) -> void: func present_node_bonus(candidates: Array, aux_detail: Array = []) -> void:
_reset_refresh_x() # 迭代 11 第①批 T5b——碰三选一重置 X 余量(不结转)
_node_bonus_pending = {"candidates": candidates.duplicate(true), "aux_detail": aux_detail.duplicate(true)} _node_bonus_pending = {"candidates": candidates.duplicate(true), "aux_detail": aux_detail.duplicate(true)}
@@ -405,18 +419,14 @@ func _apply_node_aux(count: int) -> bool:
var records: Array = [] var records: Array = []
if _runner != null and _runner.aux_inventory != null: if _runner != null and _runner.aux_inventory != null:
records = _runner.aux_inventory.holding_records() records = _runner.aux_inventory.holding_records()
var holding := records.size() + _bonus_aux_carry.size() # 迭代 11 第①批 T7——守卫随过渡键退役拆除(BONUS_IDS 已除名 aux_item,本分支不可达;
if AuxItems.can_accept(picked.size(), holding, db): # 同调位守卫单点(2026-10-02) # 替换子交互路由随容量闸一并退役——能力件容量闸正主 = 营地装配面满位禁选)
if _runner != null: if _runner != null:
for iid in picked: for iid in picked:
var rec: Dictionary = _runner.grant_aux_item(String(iid), "reward_node") var rec: Dictionary = _runner.grant_aux_item(String(iid), "reward_node")
if rec.is_empty(): if rec.is_empty():
push_warning("[冒险结构] 奖励节点道具授予被拒(防御兜底留痕): %s" % String(iid)) push_warning("[冒险结构] 奖励节点道具授予被拒(防御兜底留痕): %s" % String(iid))
_finish_node_bonus("aux_item", picked.size()) _finish_node_bonus("aux_item", picked.size())
return true
_node_bonus_pending["chosen"] = "aux_item"
_node_bonus_pending["incoming"] = picked.duplicate()
aux_replace_presented.emit(records, {"handle": -1, "item_id": String(picked[0]), "count": picked.size()})
return true return true
@@ -436,6 +446,71 @@ func _resolve_node_replace(discard_handle: int) -> bool:
return true return true
# 冒险开始充能账本重置(T6——装备 active_charged 件各回 charges_per_adventure;被动件不入账)
func _seed_attune_charges() -> void:
attune_charges = {}
for iid in _attunement:
var arow: Dictionary = db.aux_item_row(String(iid))
if String(arow.get("form", "")) == "active_charged":
attune_charges[String(iid)] = maxi(0, int(arow.get("charges_per_adventure", 0)))
# ---- 三选一刷新(迭代 11 第①批 T5b——P1 甲 X/Y 双源:X=刷新件每碰三选一重给〔优先消耗、不结转〕、
# Y=营地升级行冒险周期池〔用完即无〕;重抽消费 reward_pick 既有洗牌流,池规则全保留——不放回/
# 达上限剔牌/掷定前置直显;停点可中断)----
func _attune_refresh_x_total() -> int:
return int(db.aux_item_row("pick_refresh").get("magnitude", 0)) if _attunement.has("pick_refresh") else 0
func _reset_refresh_x() -> void:
_attune_refresh_x = _attune_refresh_x_total()
# 当前可刷新次数(X 优先 + Y 池——呈现层余量读数口)
func attune_refresh_left() -> int:
return _attune_refresh_x + _attune_refresh_y
# 节点停点在途态读回(组合根重 present 用——candidates/aux_detail 值拷贝)
func node_bonus_pending_state() -> Dictionary:
return {
"candidates": (_node_bonus_pending.get("candidates", []) as Array).duplicate(true),
"aux_detail": (_node_bonus_pending.get("aux_detail", []) as Array).duplicate(true),
}
# 三选一刷新(组合根唯一调用口;无停点/无余量 false 零副作用)。通关停点重抽后重发
# bonus_choice_presented(呈现层重建卡片+刷新余量);节点停点由组合根读 node_bonus_pending_state 重 present。
func refresh_bonus() -> bool:
if _attune_refresh_x <= 0 and _attune_refresh_y <= 0:
return false
if not _bonus_pending.is_empty():
if _attune_refresh_x > 0:
_attune_refresh_x -= 1
else:
_attune_refresh_y -= 1
var cands: Array = _draw_bonus_candidates()
_bonus_pending["candidates"] = cands.duplicate(true)
_bonus_pending["aux_detail"] = _last_draw_aux.duplicate()
var payload: Dictionary = (_bonus_pending.get("payload", {}) as Dictionary).duplicate(true)
payload["candidates"] = cands
payload["aux_detail"] = _last_draw_aux.duplicate()
bonus_choice_presented.emit(payload) # 同信号重 present(零自动推进契约不破——停点仍在途)
_snapshot_breakpoint()
return true
if not _node_bonus_pending.is_empty():
if _attune_refresh_x > 0:
_attune_refresh_x -= 1
else:
_attune_refresh_y -= 1
var cands2: Array = _draw_bonus_candidates()
_node_bonus_pending["candidates"] = cands2.duplicate(true)
_node_bonus_pending["aux_detail"] = _last_draw_aux.duplicate()
return true
return false
# 替换在途守卫(2026-10-02 修复批——双渠道替换受理的共用前置谓词:停点态非空且 chosen=aux_item) # 替换在途守卫(2026-10-02 修复批——双渠道替换受理的共用前置谓词:停点态非空且 chosen=aux_item)
func _replace_in_transit(pending: Dictionary) -> bool: func _replace_in_transit(pending: Dictionary) -> bool:
return not pending.is_empty() and String(pending.get("chosen", "")) == "aux_item" return not pending.is_empty() and String(pending.get("chosen", "")) == "aux_item"
@@ -501,12 +576,8 @@ func _draw_bonus_candidates() -> Array:
keyed.sort_custom(func(a, b): return float(a["key"]) > float(b["key"])) keyed.sort_custom(func(a, b): return float(a["key"]) > float(b["key"]))
var out: Array = [] var out: Array = []
for i in mini(int(db.adventure.bonus_candidate_count), keyed.size()): # 候选数读表(2026-10-02 魔法数结构化——默认 3 = 现行行为) for i in mini(int(db.adventure.bonus_candidate_count), keyed.size()): # 候选数读表(2026-10-02 魔法数结构化——默认 3 = 现行行为)
var cid := String(keyed[i]["id"]) out.append(String(keyed[i]["id"]))
out.append(cid) # 迭代 11 第①批 T7——aux_item 掷定前置分支随池种退役拆除(BONUS_IDS 已除名,候选恒无 aux 卡)
if cid == "aux_item":
var roll_rg: RandomNumberGenerator = rng.open_stream("reward_pick")
for j in maxi(1, int(db.adventure.bonus_aux_item_count)):
_last_draw_aux.append(_roll_from(roll_rg))
return out return out
@@ -592,11 +663,10 @@ func _start_current_level() -> bool:
var ok: bool = _runner.start_level_external(level_index, ledger, bonus, purchased, var ok: bool = _runner.start_level_external(level_index, ledger, bonus, purchased,
Callable(self, "apply_rest"), _base_override, true, template_pick, resume_state) Callable(self, "apply_rest"), _base_override, true, template_pick, resume_state)
if ok: if ok:
# 三选一道具件跨关携带注入(迭代 10——上关兑付的道具件进本关库存〔奖励只朝下一战生效〕; _runner.set_attunement(_attunement) # 迭代 11 第①批 T5——同调装配下传(enc 携带进战斗结算)
# 注入即移交——carry 清空,本关末随关卡域库存清空) _runner.set_attune_charges(attune_charges) # 迭代 11 第①批 T6——充能账本下传(能力件骑库存面注入)
for iid in _bonus_aux_carry: # 三选一道具件跨关携带注入(迭代 10)——迭代 11 第①批 T7 退役:aux 卡出池后 carry 恒空集,
_runner.grant_aux_item(iid, "bonus_pick") # 注入面随停点渠道退役拆除(容器字段保留=替换子交互机械惰性承载,零投放零行为)
_bonus_aux_carry = []
level_started.emit(level_index, String(_runner.level_row.get("display_name", ""))) level_started.emit(level_index, String(_runner.level_row.get("display_name", "")))
# 入口停点断点(node_id=-1)与 defer_entry 同态(node_index=-1+单候选)——走入口呈现; # 入口停点断点(node_id=-1)与 defer_entry 同态(node_index=-1+单候选)——走入口呈现;
# 中途停点(node_id>=0)走停点呈现(restore_aux_items 先于呈现=C 区库存就位)。 # 中途停点(node_id>=0)走停点呈现(restore_aux_items 先于呈现=C 区库存就位)。
@@ -635,6 +705,7 @@ func _on_level_finished(payload: Dictionary) -> void:
ledger["hero_hp"] = hp_before + heal ledger["hero_hp"] = hp_before + heal
# 置位先于 emit(信号同步派发——处理器内立即确认时 _pending_next_level 必须已就位) # 置位先于 emit(信号同步派发——处理器内立即确认时 _pending_next_level 必须已就位)
_pending_next_level = true _pending_next_level = true
_reset_refresh_x() # 迭代 11 第①批 T5b——碰三选一重置 X 余量(不结转)
var pick_payload: Dictionary = { var pick_payload: Dictionary = {
"level_index": level_index, "level_index": level_index,
"candidates": _draw_bonus_candidates(), "candidates": _draw_bonus_candidates(),
@@ -724,6 +795,7 @@ func _snapshot_breakpoint() -> void:
}, },
"gp": int(ledger.get("gp", 0)), "gp": int(ledger.get("gp", 0)),
"shop_purchased": purchased.duplicate(true), "shop_purchased": purchased.duplicate(true),
"attunement_charges": attune_charges.duplicate(true), # 迭代 11 第①批 T6——同调主动件充能余量随断点(save v4 域)
"template_pick": _runner.active_template_index if _runner != null else -1, "template_pick": _runner.active_template_index if _runner != null else -1,
"master_seed": master_seed, "master_seed": master_seed,
"rng_counter": rng.counter(), "rng_counter": rng.counter(),
+27
View File
@@ -0,0 +1,27 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 同调位读数是否由「通关次数 + Boss 击杀数」经 attunement_growth.tres 谓词表
# 实时求值(Σ达成 grant clamp slot_cap),迭代 10 过渡键 transitional_sync_slots 得以退役(P0)
# Date: 2026-10-03(迭代 11 第①批 魔法道具同调实施批 T3——任务书 v1.1 §六 T3;设计权威底稿 proposal §2)
# 同调位求值服务:计数源组装(save 账本 → 计数字典)与实时求值,纯函数零副作用。
# 读数源切换(迭代 10 欠账③):装配面消费方(营地装配覆盖层 T4/冒险开始装配注入 T6)一律经本服务取
# Slots;过渡键 adventure.transitional_sync_slots 的在途读取面(aux_items 持有上限 gate/战斗内沉没/
# 停点替换交互/main 同调位呈现)系过渡渠道承载——T7 退役面切换时随渠道一并消亡(该批读数源切换的
# 正主落点,06 欠账③销账注记随 T7/T9)。过渡期切早 = off 态 MC 对 81000 锚添新扰动(fresh 档计数全零
# → Slots=0 ≠ 过渡键 1),违「attune=off 逐位一致」🔴 契约——故本服务先立、在途面 T7 切。
# attune=off 纪律:零 await、零 RNG、零写副作用(🔴 契约 3 的服务侧承载)。
class_name Attunement
extends RefCounted
# 计数源组装(save = SaveManager 或 null——模拟/冒烟未注入档,按全零计数求值)
static func counters_from_save(save) -> Dictionary:
if save == null:
return {}
return save.attunement_counters()
# 同调位实时求值(营地进冒险前求值一次 + 装配覆盖层实时呈现共用同一口径)
static func slots(db, save) -> int:
if db == null or db.attunement_growth == null:
return 0
return db.attunement_growth.slots(counters_from_save(save))
+1
View File
@@ -0,0 +1 @@
uid://bwccv5x51gs23
+4 -8
View File
@@ -73,13 +73,9 @@ func holding_count() -> int:
return n return n
# 同调位守卫谓词(2026-10-02 修复批——四处手抄收口单点:战斗结算授予路由 / 关卡授予 / 三选一双渠道): # 同调位守卫谓词——迭代 11 第①批 T7 退役(读取源 transitional_sync_slots 过渡键已随表退役;
# holding = 调用方渠道的当前实际持有数(库存渠道传 holding_count()、通关渠道传跨关携带数、 # 容量闸正主 = 营地装配面 Attunement.slots 满位禁选。函数体保留空壳防四散引用误删遗漏——
# 节点渠道传库存+携带合计);读数源 = db.adventure.transitional_sync_slots(负值钳 0 口径同前)。 # T8 冒烟「过渡键读取点归零」断言覆盖)
# 返回再入 n 件是否仍在位内。挂 static:谓词纯算术不依赖实例态——通关渠道(跨关携带)没有
# AuxItems 实例可用,实例方法形态不成立;db 参数不标类型(同 _db 桩表注入纪律)。
static func can_accept(n: int, holding: int, db) -> bool:
return holding + n <= maxi(0, int(db.adventure.transitional_sync_slots))
# 持有件记录(迭代 10 追加批——奖励节点替换子交互数据源:只列未消耗件,handle 定位丢弃目标) # 持有件记录(迭代 10 追加批——奖励节点替换子交互数据源:只列未消耗件,handle 定位丢弃目标)
@@ -108,7 +104,7 @@ func _usable_now(item_id: String, board_has_preview_info: Callable) -> bool:
if row.is_empty(): if row.is_empty():
return false return false
var kinds: Array = row.get("effect_kinds", []) var kinds: Array = row.get("effect_kinds", [])
if kinds.has("mine_reveal") or kinds.has("ap_grant"): if kinds.has("mine_reveal") or kinds.has("ap_grant") or kinds.has("ap_charge_safe"):
return true return true
return bool(board_has_preview_info.call()) if board_has_preview_info.is_valid() else true return bool(board_has_preview_info.call()) if board_has_preview_info.is_valid() else true
+74 -29
View File
@@ -67,6 +67,24 @@ var _refresh_used_this_round := 0 # 每轮刷新计数(Active 相位
var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用) var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用)
var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换来的图 = true(清图奖励守卫——刷新图不发清图奖励) var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换来的图 = true(清图奖励守卫——刷新图不发清图奖励)
var _flag_consumed := {} # 兑付资格以雷格实例为键(板级;换图重置) var _flag_consumed := {} # 兑付资格以雷格实例为键(板级;换图重置)
# 同调装配运行态(迭代 11 第①批 T5——begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动)
var attunement: Array = [] # 装备中同调件 item_id 集
var _attune_charges_ref: Dictionary = {} # 充能账本共享引用(迭代 11 第①批 T6——use_aux 消费直改)
var _attune_bank := 0 # 掘进蓄能积点(battle 域——每 charge_n 安全格 +1 点)
var _attune_safe_seen := 0 # 掘进蓄能 battle 域安全开格计数
func _attune_has(item_id: String) -> bool:
return attunement.has(item_id)
# 装备件表值读取(magnitude/effective_rounds 等——AuxItemTable 行;未装配不读表)
func _attune_row(item_id: String) -> Dictionary:
return db.aux_item_row(item_id) if _attune_has(item_id) else {}
func _attune_mag(item_id: String) -> int:
return int(_attune_row(item_id).get("magnitude", 0))
var _flag_miss_seen := {} # 标旗未中行去重(每安全格实例一次) var _flag_miss_seen := {} # 标旗未中行去重(每安全格实例一次)
var _perfect_hit_true := 0 # 板级完美排雷计数(install 重置) var _perfect_hit_true := 0 # 板级完美排雷计数(install 重置)
var _perfect_hit_false := 0 var _perfect_hit_false := 0
@@ -129,12 +147,23 @@ func begin_battle(enc: Dictionary) -> bool:
if not _install_new_board(): if not _install_new_board():
return false return false
_reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险) _reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险)
# 同调装配运行态注入(迭代 11 第①批 T5——enc.attunement 携带;off=空集零扰动。淬毒 N 注入
# 伤害控制器、偏运 N 注入地图战斗资源——均须先于首板 install)
attunement = (enc.get("attunement", []) as Array).duplicate()
_attune_charges_ref = enc.get("attune_charges", {}) # 共享 dict 引用(level/adventure 同账本——use_aux 扣减直改)
_attune_bank = 0
_attune_safe_seen = 0
_damage.attune_venom = _attune_mag("venom_edge") if _attune_has("venom_edge") else 0
_reward_economy.luck_bias = _attune_mag("luck_coin") if _attune_has("luck_coin") else 0
# 迭代 10 奖励重构批——跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现 # 迭代 10 奖励重构批——跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现
# 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后 # 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后
# 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节) # 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节)
var ap_bonus: int = int(enc.get("hero_ap_bonus", 0)) var ap_bonus: int = int(enc.get("hero_ap_bonus", 0))
if ap_bonus > 0: if ap_bonus > 0:
action_economy.queue_ap_delta(ap_bonus) action_economy.queue_ap_delta(ap_bonus)
# 工兵晨令(迭代 11 同调批 T5——前 effective_rounds 轮每轮 +1 AP;首轮在此入队,后续轮在轮始段续队)
if _attune_has("dawn_order"):
action_economy.queue_ap_delta(_attune_mag("dawn_order"))
if not action_economy.notify_round_start(): if not action_economy.notify_round_start():
_fatal("行动经济 首轮 notify_round_start 被拒") _fatal("行动经济 首轮 notify_round_start 被拒")
return false return false
@@ -305,6 +334,12 @@ func _settle() -> void:
_emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(插旗格直接挖开——零惩罚零奖励)", 0, {}, "") _emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(插旗格直接挖开——零惩罚零奖励)", 0, {}, "")
continue continue
_grant_routed(snap, "positive", "safe_cell", evt.cell, ri, "") _grant_routed(snap, "positive", "safe_cell", evt.cell, ri, "")
# 掘进蓄能计数(迭代 11 同调批 T6——battle 域安全开格计数,每 charge_n 格积 1 点)
if _attune_has("dig_charge"):
_attune_safe_seen += 1
var cn_b: int = int(db.aux_item_row("dig_charge").get("charge_n", 999))
if cn_b > 0 and _attune_safe_seen % cn_b == 0:
_attune_bank += 1
elif evt.kind == "mine": elif evt.kind == "mine":
# 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴), # 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴),
# 标中不发(旗已随挖掘擦除、不入轮末判定基线)。 # 标中不发(旗已随挖掘擦除、不入轮末判定基线)。
@@ -360,30 +395,8 @@ func _settle() -> void:
# 「动作 × 格型」矩阵的执行面:snap = 缓冲期冻结身份快照(cell_type = 该格装板期摇定的类型身份), # 「动作 × 格型」矩阵的执行面:snap = 缓冲期冻结身份快照(cell_type = 该格装板期摇定的类型身份),
# polarity 由动作决定(开安全格/标中旗 = positive,开雷格/标错旗 = negative), # polarity 由动作决定(开安全格/标中旗 = positive,开雷格/标错旗 = negative),
# 兑付行 = 奖励字典(类型, 极性)行。返回 true = percent 行实际生效(flat 行恒 false——兑付结果由行协议承载)。 # 兑付行 = 奖励字典(类型, 极性)行。返回 true = percent 行实际生效(flat 行恒 false——兑付结果由行协议承载)。
# ---- 魔法道具(迭代 7 T2/T3——库存授予路由 + use_aux 四步受理编排;结构冻结见魔法道具 GDD)---- # ---- 魔法道具(use_aux 四步受理编排保留给同调主动件复用——迭代 11 第①批 T7:过渡授予路由
# _grant_aux 随掉落/停点渠道退役拆除;能力件注入归 LevelRunner.set_attune_charges 骑库存面)----
# 授予路由 aux 分支:正=入库存(发行「获得」行——逐行归因纪律);负=零兑付占位行。
func _grant_aux(snap: Dictionary, polarity: String, source_class: String, cell: Vector2i, ri: int) -> bool:
if polarity != "positive" or aux_inventory == null:
# 负路由占位行=effect 型零兑付(雷安同池下标错旗/雷格摇中 aux 身份的负向字典行——逐行归因纪律:
# 该格资源的负向兑付存在且为零;effect_id 空不入增益容器)
_emit_row("effect", source_class, cell, ri, "道具随雷沉没(惩罚格不入库存)", 0,
{"effect_id": "", "aux_sink": String(snap.get("effect_id", ""))}, "")
return false
var iid := String(snap.get("effect_id", ""))
# 同调位持有上限(迭代 10 奖励重构批过渡承载——transitional_sync_slots,provisional 1):
# 战斗内渠道遇满位沉没不入(战斗域零交互纪律——替换交互归停点渠道;正主 = 同调位谓词表,
# 全渠道统一收口随魔法道具同调实施批)。逐行归因纪律:沉没行照发(「获得」的零兑付形态)。
# 谓词单点 = AuxItems.can_accept(2026-10-02 修复批——四处手抄收口)。
if not AuxItems.can_accept(1, aux_inventory.holding_count(), db):
_emit_row("effect", source_class, cell, ri, "同调位已满,%s 沉没" % String(db.aux_item_row(iid).get("display_name", iid)), 0,
{"effect_id": "", "aux_sink": iid, "sink_reason": "sync_slots_full"}, "")
return false
var arow: Dictionary = db.aux_item_row(iid)
var rec: Dictionary = aux_inventory.add_item(iid, source_class, cell, ri)
_emit_row("effect", source_class, cell, ri, "获得 %s" % String(arow.get("display_name", iid)), 1,
{"aux_item": iid, "handle": int(rec.get("handle", 0))}, "")
return true
# use_aux 四步受理(registry use_orchestration 规范序——唯一调用权归本类;序即原子: # use_aux 四步受理(registry use_orchestration 规范序——唯一调用权归本类;序即原子:
@@ -402,6 +415,12 @@ func use_aux(handle: int, target: Vector2i) -> Dictionary:
func(c: Vector2i): return not board.is_revealed(c)) func(c: Vector2i): return not board.is_revealed(c))
if not bool(v.ok): if not bool(v.ok):
return v # ② 拒绝零消耗 return v # ② 拒绝零消耗
# 掘进蓄能受理闸(迭代 11 同调批 T6——bank>0 才可发起,一行一点;积点不足拒绝零消耗)
if String(v.item_id) == "dig_charge" and _attune_has("dig_charge"):
if _attune_bank <= 0:
var cn_info: int = int(db.aux_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]}
_attune_bank -= 1
var kinds: Array = v.kinds var kinds: Array = v.kinds
if kinds.has("mine_reveal"): if kinds.has("mine_reveal"):
aux_inventory.apply_overlay("mine_reveal", v.resolved) aux_inventory.apply_overlay("mine_reveal", v.resolved)
@@ -410,7 +429,16 @@ func use_aux(handle: int, target: Vector2i) -> Dictionary:
if kinds.has("ap_grant"): if kinds.has("ap_grant"):
if not action_economy.grant_ap(int(v.magnitude)): if not action_economy.grant_ap(int(v.magnitude)):
return {"ok": false, "reason": "行动点加量被拒(相位防御)"} return {"ok": false, "reason": "行动点加量被拒(相位防御)"}
if kinds.has("ap_charge_safe"):
# 掘进蓄能兑付(迭代 11 同调批 T6——临时 AP 当轮生效,grant_ap 同 sapper_order 通道)
if not action_economy.grant_ap(int(v.magnitude)):
return {"ok": false, "reason": "行动点加量被拒(相位防御)"}
var rec: Dictionary = aux_inventory.consume(handle) var rec: Dictionary = aux_inventory.consume(handle)
# 同调主动件充能扣减(迭代 11 第①批 T6——共享账本直改:adventure/level/settlement 同一字典引用)
if not rec.is_empty() and String(rec.get("source_class", "")) == "attunement":
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)
return {"ok": true, "item_id": v.item_id, "resolved": v.resolved, "record": rec} return {"ok": true, "item_id": v.item_id, "resolved": v.resolved, "record": rec}
@@ -420,17 +448,26 @@ func set_aux_unlocked(ids: Dictionary) -> void:
func _grant_routed(snap: Dictionary, polarity: String, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "") -> bool: func _grant_routed(snap: Dictionary, polarity: String, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "") -> bool:
# 迭代 7 T3——aux 投放行授予路由(魔法道具 GDD:授予路由唯一调用方=战斗结算):正路由(挖开/标中) # 迭代 11 第①批 T7——aux 授予分支随掉落/停点渠道退役拆除(reward 词表第十型 aux_item 保留但零投放:
# → 道具入关卡域库存(来源可归因);负路由(雷格/标错旗摇中 aux 身份——雷安同池)→ 零兑付 # 无 aux 行入池、BONUS_IDS 词表已除名);剩余类型走极性字典路由。
# 「道具随雷沉没」(雷格奖励沉没既有语义);清图批追加份同路由(清图翻倍语义对 aux 同样成立)。
if String(snap.get("cell_type", "")) == RewardTable.AUX_ITEM_TYPE:
return _grant_aux(snap, polarity, source_class, cell, ri)
var row: Dictionary = _reward_economy.polarity_row(String(snap.get("cell_type", "")), polarity) var row: Dictionary = _reward_economy.polarity_row(String(snap.get("cell_type", "")), polarity)
if row.is_empty(): if row.is_empty():
_fatal("极性字典缺行: %s %s" % [String(snap.get("cell_type", "")), polarity]) _fatal("极性字典缺行: %s %s" % [String(snap.get("cell_type", "")), polarity])
return false return false
if polarity == "positive": if polarity == "positive":
row["power"] = int(snap.get("power", int(row.power))) # 区间行快照 power(身份批 roll 值)覆盖表值——授予期零随机口径(E14) row["power"] = int(snap.get("power", int(row.power))) # 区间行快照 power(身份批 roll 值)覆盖表值——授予期零随机口径(E14)
# 惊巢反制(迭代 11 同调批 T5——底稿 §3:挖开雷格该次惩罚减半 + 反弹 N;duplicate 隔离——减半
# 不回写极性字典;仅 source_class=mine_hit 受理,标错旗/负向字典行走原通道)
if polarity == "negative" and source_class == "mine_hit" and _attune_has("nest_counter"):
row = row.duplicate()
var half: int = int(ceil(float(int(row.power)) / 2.0))
_emit_row("effect", source_class, cell, ri, "惊巢反制——挖雷惩罚减半(%d→%d)" % [int(row.power), half], 0, {"effect_id": "nest_counter"}, highlight)
row.power = half
var reflect: int = _attune_mag("nest_counter")
if reflect > 0:
var nct := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_counter", "惊巢反制·反弹", "board",
reflect, DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, highlight, {})
_submit(nct, false)
return _dispatch_row(row, source_class, cell, ri, highlight, label_suffix) return _dispatch_row(row, source_class, cell, ri, highlight, label_suffix)
@@ -537,6 +574,11 @@ func _judge_flag(evt: Dictionary, ri: int) -> void:
return # 去重跳过(资格以雷格实例为键——排雷玩法 诚实重复上报的对侧);零明细行 return # 去重跳过(资格以雷格实例为键——排雷玩法 诚实重复上报的对侧);零明细行
_flag_consumed[cell] = true _flag_consumed[cell] = true
_perfect_hit_true += 1 _perfect_hit_true += 1
# 标中殉爆(迭代 11 同调批 T5——底稿 §3:每标中一面旗,全体存活敌吃 N 群伤;无类型走 blast 通道)
if _attune_has("flag_burst"):
var fbt := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_flag_burst", "标中殉爆", "board",
_attune_mag("flag_burst"), DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, "highlight", {})
_submit(fbt, false)
var snap = _reward_economy.grant(cell) var snap = _reward_economy.grant(cell)
if not (snap is Dictionary): if not (snap is Dictionary):
return return
@@ -747,6 +789,9 @@ func _exit_distribution() -> void:
if not _install_new_board(): if not _install_new_board():
_fatal("换图编排失败") _fatal("换图编排失败")
return return
# 工兵晨令续队(迭代 11 同调批 T5——upcoming 轮号 = round_index+1 ≤ effective_rounds 才续,P4 甲表值化)
if _attune_has("dawn_order") and round_index + 1 <= int(db.aux_item_row("dawn_order").get("effective_rounds", 0)):
action_economy.queue_ap_delta(_attune_mag("dawn_order"))
action_economy.notify_settlement_done(mode) action_economy.notify_settlement_done(mode)
action_economy.notify_round_start() action_economy.notify_round_start()
_reset_round_facts() _reset_round_facts()
+33 -2
View File
@@ -25,6 +25,7 @@ const TABLE_MANIFEST := {
"meta": "meta.tres", # 元进度实现批 2026-09-21(MetaTable 四行族——schema 与表同批落籍) "meta": "meta.tres", # 元进度实现批 2026-09-21(MetaTable 四行族——schema 与表同批落籍)
"aux_items": "aux_items.tres", # 迭代 7 T1 魔法道具内容批(AuxItemTable 六行——定义表 Rule 4 豁免;罗盘 retired/失义件 deferred 标记见 schema 头注) "aux_items": "aux_items.tres", # 迭代 7 T1 魔法道具内容批(AuxItemTable 六行——定义表 Rule 4 豁免;罗盘 retired/失义件 deferred 标记见 schema 头注)
"adventure": "adventure.tres", # 冒险结构实现批 2026-09-21(AdventureTable 建表——大奖励与钥石行值单点) "adventure": "adventure.tres", # 冒险结构实现批 2026-09-21(AdventureTable 建表——大奖励与钥石行值单点)
"attunement_growth": "attunement_growth.tres", # 迭代 11 第①批 同调实施批 T1(AttunementGrowthTable 单行全局——同调位里程碑谓词表;单行先例 Rule 4 豁免)
} }
var action_economy: ActionEconomyTable var action_economy: ActionEconomyTable
@@ -45,6 +46,7 @@ var shop: ShopTable = null
var meta: MetaTable var meta: MetaTable
var aux_items: AuxItemTable # 迭代 7 T1 类型化持用(消费面走 aux_* 访问器) var aux_items: AuxItemTable # 迭代 7 T1 类型化持用(消费面走 aux_* 访问器)
var adventure: AdventureTable var adventure: AdventureTable
var attunement_growth: AttunementGrowthTable # 迭代 11 第①批 T1 类型化持用(消费面走 attunement_* 访问器)
var is_ready := false var is_ready := false
var _diagnostics: Array[Dictionary] = [] # {severity, category, message} var _diagnostics: Array[Dictionary] = [] # {severity, category, message}
@@ -135,6 +137,7 @@ func load_tables(root: String, collect_only: bool) -> bool:
meta = loaded.get("meta") meta = loaded.get("meta")
aux_items = loaded.get("aux_items") aux_items = loaded.get("aux_items")
adventure = loaded.get("adventure") adventure = loaded.get("adventure")
attunement_growth = loaded.get("attunement_growth")
is_ready = not has_errors is_ready = not has_errors
return not has_errors return not has_errors
@@ -211,14 +214,34 @@ func aux_item_row(item_id: String) -> Dictionary:
return aux_items.row_by_id(item_id) if aux_items != null else {} return aux_items.row_by_id(item_id) if aux_items != null else {}
# 活行词表(掉落投放 effect_id 白名单) # 过渡渠道活行(form 空 = 切片期过渡形态)——掉落投放/停点池/工坊解锁取数口径;同调行(form 非空)
# 永不入过渡渠道(迭代 11 第①批 T1;T7 退役面切换后本口径随渠道消亡)
func aux_transitional_rows() -> Array:
return aux_items.transitional_rows() if aux_items != null else []
# 同调行集(form 非空——装配覆盖层/效果接线的取数口径,迭代 11 第①批 T1/T4)
func aux_attunement_rows() -> Array:
return aux_items.attunement_rows() if aux_items != null else []
# 活行词表(掉落投放 effect_id 白名单——过渡渠道口径)
func aux_live_ids() -> Dictionary: func aux_live_ids() -> Dictionary:
var out := {} var out := {}
for row in aux_active_rows(): for row in aux_transitional_rows():
out[String(row.get("item_id", ""))] = true out[String(row.get("item_id", ""))] = true
return out return out
# ── 同调轴访问器(迭代 11 第①批 T1——消费面一律走访问器,禁直读行集)──
func attunement_slot_cap() -> int:
return attunement_growth.slot_cap if attunement_growth != null else 0
func attunement_milestones() -> Array:
return attunement_growth.milestones if attunement_growth != null else []
# 行族四轴(数据契约,行集归 shop.tres):gp_drops {tier, mean, roll_min, roll_max} · # 行族四轴(数据契约,行集归 shop.tres):gp_drops {tier, mean, roll_min, roll_max} ·
# level_gp_bonus {level_index, bonus} · products {product_id, display_name, attr, flat_value, # level_gp_bonus {level_index, bonus} · products {product_id, display_name, attr, flat_value,
# base_price, price_increment} · price_level_bonus {level_index, bonus}。 # base_price, price_increment} · price_level_bonus {level_index, bonus}。
@@ -296,6 +319,14 @@ func meta_conversion_row(attr: String) -> Dictionary:
return {} return {}
# Y 刷新升级行单点读取(迭代 11 第①批 T4——键 = 等级,缺行返回空字典=未购/已满级合法态)
func meta_attune_refresh_row(level: int) -> Dictionary:
for row in _meta_rows("attune_refresh_upgrade"):
if int(row.get("level", -1)) == level:
return row
return {}
# 行族数组读取(MetaTable 属性名 = 行族名;表缺失/属性缺失 = 空集非错误态) # 行族数组读取(MetaTable 属性名 = 行族名;表缺失/属性缺失 = 空集非错误态)
func _meta_rows(family: String) -> Array: func _meta_rows(family: String) -> Array:
if meta == null: if meta == null:
+9 -1
View File
@@ -196,7 +196,12 @@ func _resolve_hero_attack(t: Dictionary) -> void:
var threshold: int = defender_ac(target) + int(db.settlement.attack_bonus) var threshold: int = defender_ac(target) + int(db.settlement.attack_bonus)
var crit: bool = bool(roll_read.crit) var crit: bool = bool(roll_read.crit)
var discount: int = _roll_discount(int(roll_read.roll), threshold) var discount: int = _roll_discount(int(roll_read.roll), threshold)
var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.settlement.crit_multiplier), _counter_delta(atk_type, armor)) var cdelta: int = _counter_delta(atk_type, armor)
# 淬毒刃纹(迭代 11 同调批 T5——底稿 §3:对被克制敌人的穿刺/魔法直伤 +N;条件 = 克制成立 ×
# 攻击类型限定,主角攻击面专属——棋盘克制直伤走 _resolve_board_typed 不吃本加成)
if attune_venom > 0 and (atk_type == "pierce" or atk_type == "magic") and cdelta > 0:
cdelta += attune_venom
var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.settlement.crit_multiplier), cdelta)
var payload: Dictionary = {"target": String(target.display_name)} var payload: Dictionary = {"target": String(target.display_name)}
payload[atk_type] = rolled # 分量键 = 实际攻击类型(归因字段,单分量模型) payload[atk_type] = rolled # 分量键 = 实际攻击类型(归因字段,单分量模型)
payload["roll"] = int(roll_read.roll) payload["roll"] = int(roll_read.roll)
@@ -326,6 +331,9 @@ func _counter_delta(attack_type: String, armor: String) -> int:
return int(row.get(armor, 0)) return int(row.get(armor, 0))
# 淬毒刃纹 N(迭代 11 同调批 T5——战斗结算 begin_battle 注入;0 = 未装配零扰动)
var attune_venom: int = 0
# 池内加算 → clamp [-100,+100](池间乘算归 DamageMath.pool_damage 调用侧) # 池内加算 → clamp [-100,+100](池间乘算归 DamageMath.pool_damage 调用侧)
func clamped_pool(pool: String) -> int: func clamped_pool(pool: String) -> int:
var total := 0 var total := 0
+26 -5
View File
@@ -64,6 +64,8 @@ var _shop_purchased := {} # product_id → 已购档数(现价
var _settlement: BattleOrchestrator = null # 当前战斗编排器(关卡结束置 null) var _settlement: BattleOrchestrator = null # 当前战斗编排器(关卡结束置 null)
var aux_inventory: AuxItems = null # 魔法道具 关卡域库存(迭代 7 T3——AuxItems;跨战斗延续, var aux_inventory: AuxItems = null # 魔法道具 关卡域库存(迭代 7 T3——AuxItems;跨战斗延续,
var _aux_unlocked := {} # aux 解锁门控集(迭代 7 T5——拍板③甲;空集=零投放) var _aux_unlocked := {} # aux 解锁门控集(迭代 7 T5——拍板③甲;空集=零投放)
var _attunement: Array = [] # 同调装配集(迭代 11 第①批 T5——adventure params.attunement 下传,begin_battle enc 携带进战斗结算;空集=off 零扰动)
var _attune_charges: Dictionary = {} # 同调主动件充能账本(迭代 11 第①批 T6——adventure 共享 dict 引用;settlement use_aux 扣减直改同引用)
# 关卡末清空+快照计数入 level_finished 载荷) # 关卡末清空+快照计数入 level_finished 载荷)
var hero_atk_pct := 0 # 测试面板覆盖值(切片专用) var hero_atk_pct := 0 # 测试面板覆盖值(切片专用)
@@ -464,6 +466,8 @@ func _start_battle(node: Dictionary) -> bool:
if not _settlement.begin_battle({"level_index": int(level_row.level_index), "node_depth": int(node.get("node_depth", 0)), if not _settlement.begin_battle({"level_index": int(level_row.level_index), "node_depth": int(node.get("node_depth", 0)),
"hero_hp": hero_hp, "hero_atk": effective_atk(), "hero_max_hp": effective_max_hp(), "hero_hp": hero_hp, "hero_atk": effective_atk(), "hero_max_hp": effective_max_hp(),
"hero_armor_class": effective_ac(), "hero_ap_bonus": effective_ap_bonus(), "hero_armor_class": effective_ac(), "hero_ap_bonus": effective_ap_bonus(),
"attunement": _attunement,
"attune_charges": _attune_charges,
"node_type": String(node.get("type", ""))}): "node_type": String(node.get("type", ""))}):
push_error("[关卡节点地图] 战斗启动失败(depth %s)" % str(node.get("node_depth", -1))) push_error("[关卡节点地图] 战斗启动失败(depth %s)" % str(node.get("node_depth", -1)))
return false return false
@@ -567,15 +571,32 @@ func set_aux_unlocked(ids: Dictionary) -> void:
if _settlement != null: if _settlement != null:
_settlement.set_aux_unlocked(ids) _settlement.set_aux_unlocked(ids)
# 同调装配集注入(迭代 11 第①批 T5——冒险开始参数面;AdventureRunner 唯一调用方)
func set_attunement(ids: Array) -> void:
_attunement = ids.duplicate()
# 同调主动件充能账本注入(迭代 11 第①批 T6——共享 dict 引用直改;能力件骑库存面注入:
# 差额注入防续玩双份——resume 路径 restore_inventory 已含断点内 attune 记录)
func set_attune_charges(charges: Dictionary) -> void:
_attune_charges = charges
if aux_inventory == null:
aux_inventory = AuxItems.new(db)
for iid in charges:
var existing := 0
for r in aux_inventory.snapshot_inventory():
if String(r.item_id) == String(iid) and String(r.source_class) == "attunement" and not bool(r.used):
existing += 1
for i in range(maxi(0, int(charges[iid]) - existing)):
aux_inventory.add_item(String(iid), "attunement", Vector2i(-1, -1), 0)
# 奖励节点兑付透传(迭代 7 T5——呈现层三选一 → 关卡域库存;来源归因 reward_node)。 # 奖励节点兑付透传(迭代 7 T5——呈现层三选一 → 关卡域库存;来源归因 reward_node)。
# 同调位持有上限(迭代 10 奖励重构批过渡承载):满位拒绝授予(奖励节点渠道过渡态 = 满位候选 # 迭代 11 第①批 T7——同调位持有闸随过渡键退役拆除(aux 卡出池后仅剩防御面;能力件容量闸
# 由呈现侧过滤不出;此处防御兜底)——替换交互归三选一渠道,全渠道统一收口随同调实施批。 # 正主 = 营地装配面 Attunement.slots 满位禁选)。
func grant_aux_item(item_id: String, source: String) -> Dictionary: func grant_aux_item(item_id: String, source: String) -> Dictionary:
if aux_inventory == null: if aux_inventory == null:
aux_inventory = AuxItems.new(db) aux_inventory = AuxItems.new(db)
if not AuxItems.can_accept(1, aux_inventory.holding_count(), db): # 同调位守卫单点(2026-10-02)
push_warning("[关卡节点地图] 同调位已满,授予拒绝: %s(%s)" % [item_id, source])
return {}
return aux_inventory.add_item(item_id, source, Vector2i(-1, -1), 0) return aux_inventory.add_item(item_id, source, Vector2i(-1, -1), 0)
+62
View File
@@ -150,3 +150,65 @@ func hero_override() -> Dictionary:
"max_hp": forge_flat("max_hp"), "max_hp": forge_flat("max_hp"),
"armor_class": forge_flat("armor_class"), "armor_class": forge_flat("armor_class"),
} }
# ---- 同调位账本(迭代 11 第①批 T4——装配关系/同调位求值/Y 刷新升级;设计权威 proposal §2 + 任务书 P1 甲)----
# 装配切换(营地装配覆盖层唯一写口):已装 → 卸下恒可;未装 → 非同调行拒绝 + 满位禁选;
# 装配关系写存档持久(冒险中途不可换装——底稿 §8.5,本口只服务营地)。
func toggle_attunement(item_id: String) -> bool:
var equipped: Array = save.attunement_equipped()
if not equipped.has(item_id):
var arow: Dictionary = db.aux_item_row(item_id)
if arow.is_empty() or String(arow.get("form", "")) == "":
return false # 非同调行(过渡/退役行)禁装配
if equipped.size() >= Attunement.slots(db, save):
return false # 满位禁选
equipped.append(item_id)
else:
equipped.erase(item_id)
save.set_attunement_equipped(equipped)
return save.write_to_disk()
# 营地装配面投影(账面归本类、拼句归 CampOverlay——_camp_data 同款分工)
func attune_camp_status() -> Dictionary:
var equipped: Array = save.attunement_equipped()
var items: Array[Dictionary] = []
for row in db.aux_attunement_rows():
var iid := String(row.get("item_id", ""))
items.append({
"item_id": iid,
"display_name": String(row.get("display_name", "")),
"form": String(row.get("form", "")),
"equipped": equipped.has(iid),
})
var lvl := save.attune_refresh_level()
var next_row: Dictionary = db.meta_attune_refresh_row(lvl + 1)
return {
"slots": Attunement.slots(db, save),
"equipped": equipped,
"items": items,
"refresh": {
"level": lvl,
"charges": attune_refresh_charges(),
"price": int(next_row.get("keystone_cost", 0)),
"maxed": next_row.is_empty(),
},
}
# 每冒险开局刷新次数 Y(当前等级行表值;0 级 = 0 次即无刷新能力——P1 甲初始 0 同构)
func attune_refresh_charges() -> int:
return int(db.meta_attune_refresh_row(save.attune_refresh_level()).get("refresh_charges", 0))
# Y 升级购买(直购价 = 下一级行表值 keystone_cost;已满级/余额不足 false 零副作用——buy_forge 同款)
func buy_attune_refresh() -> bool:
var next_row: Dictionary = db.meta_attune_refresh_row(save.attune_refresh_level() + 1)
if next_row.is_empty():
return false
if not save.spend_keystone(int(next_row.get("keystone_cost", 0))):
return false
save.set_attune_refresh_level(save.attune_refresh_level() + 1)
return save.write_to_disk()
+22 -1
View File
@@ -59,7 +59,7 @@ func roll_board(eligible: Array) -> bool:
_clear_granted = false _clear_granted = false
_clear_snapshot = {} _clear_snapshot = {}
var stream: RandomNumberGenerator = rng.open_stream("reward_roll") # 恰一 scope 批(格批 + 清图批尾 + 区间行追加取号) var stream: RandomNumberGenerator = rng.open_stream("reward_roll") # 恰一 scope 批(格批 + 清图批尾 + 区间行追加取号)
var cell_rows: Array = _rollable_rows() # cell 池正向行 weight>0(类型频次) var cell_rows: Array = _biased_cell_rows() # cell 池正向行 weight>0(偏运硬币装配时稀有正向 kinds 权重 +N——池结构零触碰)
var clear_rows: Array = _pool_rows("clear_default") var clear_rows: Array = _pool_rows("clear_default")
# ① 身份批:每非封锁格恰一类型身份(雷安同池——同一行集同一权重分布) # ① 身份批:每非封锁格恰一类型身份(雷安同池——同一行集同一权重分布)
var draft := {} var draft := {}
@@ -174,6 +174,27 @@ func polarity_row(cell_type: String, polarity: String) -> Dictionary:
# ---- F1 选行算子纯函数 ---- # ---- F1 选行算子纯函数 ----
# 偏运硬币 N(迭代 11 同调批 T5——战斗结算 begin_battle 注入;0 = 未装配零扰动)
var luck_bias: int = 0
# 稀有正向资源 kinds(底稿 §3——权重投影面,池结构零触碰)
const LUCK_BIAS_KINDS := ["heal", "gp", "pierce", "magic"]
# 偏运硬币权重投影(底稿 §3:只动池内权重、雷安仍同池同分布;luck_bias=0 逐位返回原行集——off 态零扰动)
func _biased_cell_rows() -> Array:
if luck_bias <= 0:
return _rollable_rows()
var out: Array = []
for row in _rollable_rows():
if LUCK_BIAS_KINDS.has(String(row.get("cell_type", ""))):
var biased: Dictionary = row.duplicate()
biased["weight"] = int(row.weight) + luck_bias
out.append(biased)
else:
out.append(row)
return out
func weighted_pick(rows: Array, r: int) -> String: func weighted_pick(rows: Array, r: int) -> String:
var total := _pool_weight(rows) var total := _pool_weight(rows)
assert(r >= 0 and r < total, "weighted_pick 取号越域(纯函数域守卫)") assert(r >= 0 and r < total, "weighted_pick 取号越域(纯函数域守卫)")
+63 -4
View File
@@ -10,6 +10,12 @@
# 域清单与 owner 分工(内容 owner 定口径,本类照单承载、不替任何人定义进度): # 域清单与 owner 分工(内容 owner 定口径,本类照单承载、不替任何人定义进度):
# keystone_balance / forge_levels / item_unlocks / level_clear_counts / boss_first_kill # keystone_balance / forge_levels / item_unlocks / level_clear_counts / boss_first_kill
# —— 内容 owner = 元进度.md(营地三柜台:钥石余额/属性锻造/道具工坊/通关次数/首杀标记) # —— 内容 owner = 元进度.md(营地三柜台:钥石余额/属性锻造/道具工坊/通关次数/首杀标记)
# boss_kill_count / attunement_equipped / attune_refresh_level
# —— 内容 owner = 魔法道具.md(同调位账本,M-4 迭代 11 第①批 T2):Boss 击杀总数〔含重复〕=
# attunement_growth 谓词表计数源;装配关系 = 局外进度事实(元进度域同族——非契约 2 反面清单的
# 「局内道具库存」,后者=冒险进行中瞬态);attune_refresh_level = Y 刷新升级购买等级
# breakpoint.attunement_charges(主动件充能余量)——每冒险重置语义的断点瞬态附随件,内容装配归
# AdventureRunner(随 T6 接线入断点),本类照单承载;契约 2 域界注记同断点域豁免先例
# recent_summaries(冒险摘要五件事副本)—— 内容 owner = 冒险结构.md(摘要五件事口径) # recent_summaries(冒险摘要五件事副本)—— 内容 owner = 冒险结构.md(摘要五件事口径)
# breakpoint(冒险断点快照——停点粒度续玩域)—— 内容 owner = 冒险结构.md(七字段拍板面 # breakpoint(冒险断点快照——停点粒度续玩域)—— 内容 owner = 冒险结构.md(七字段拍板面
# +恢复必需附随件:主种子/取数计数器/钥石累计/已到节点序/道具库存);契约 2 域界反面清单 # +恢复必需附随件:主种子/取数计数器/钥石累计/已到节点序/道具库存);契约 2 域界反面清单
@@ -30,8 +36,7 @@
class_name SaveManager class_name SaveManager
extends RefCounted extends RefCounted
const SAVE_FORMAT_VERSION := 3 # registry schema_version:v3=断点附随件扩列+摘要 keystone_drops(M-3 迭代 10 奖励重构; const SAVE_FORMAT_VERSION := 4 # registry schema_version:v4=同调批扩域(meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level——M-4 迭代 11 第①批 T2;断点充能余量随 T6 接线,旧档缺键 = 合法空态);v3=断点附随件扩列+摘要 keystone_drops(M-3 迭代 10 奖励重构);v2=断点域出生(M-3 迭代 8 卡①);v1=首版;高版本拒读 owner=存档序列化
# 旧档缺键 = 合法空态);v2=断点域出生(M-3 迭代 8 卡①);v1=首版;高版本拒读 owner=存档序列化
const SUMMARY_CAP := 10 # 最近冒险摘要保留条数(最新在前) const SUMMARY_CAP := 10 # 最近冒险摘要保留条数(最新在前)
const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] # 属性锻造三档合法键(元进度.md 属性锻造柜台) const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] # 属性锻造三档合法键(元进度.md 属性锻造柜台)
@@ -48,6 +53,7 @@ func _init(path: String = "user://save.json") -> void:
# 生产迁移链自注册(v2 起——测试钩 register_migration 仍可叠挂更早版本步骤) # 生产迁移链自注册(v2 起——测试钩 register_migration 仍可叠挂更早版本步骤)
register_migration(1, _migrate_v1_v2) register_migration(1, _migrate_v1_v2)
register_migration(2, _migrate_v2_v3) register_migration(2, _migrate_v2_v3)
register_migration(3, _migrate_v3_v4)
# v1→v2:断点域出生(旧档无断点 = 合法空态——「继续上局」隐藏,非损坏非债务) # v1→v2:断点域出生(旧档无断点 = 合法空态——「继续上局」隐藏,非损坏非债务)
@@ -66,6 +72,14 @@ func _migrate_v2_v3(data: Dictionary) -> Dictionary:
return data return data
# v3→v4:同调批扩域(迭代 11 第①批 T2——meta 三新域 boss_kill_count/attunement_equipped/
# attune_refresh_level,旧档缺键由 _normalize 按默认补齐,无结构迁移体;版本号推进仅为
# schema_version 对齐——v2→v3 先例)
func _migrate_v3_v4(data: Dictionary) -> Dictionary:
data["version"] = SAVE_FORMAT_VERSION
return data
# ---- 默认数据模型(新档合法态——全零;白板存档 = 完整可玩〔元进度 单局循环零依赖〕)---- # ---- 默认数据模型(新档合法态——全零;白板存档 = 完整可玩〔元进度 单局循环零依赖〕)----
func _default_data() -> Dictionary[String, Variant]: func _default_data() -> Dictionary[String, Variant]:
@@ -77,6 +91,9 @@ func _default_data() -> Dictionary[String, Variant]:
"item_unlocks": {}, "item_unlocks": {},
"level_clear_counts": {"1": 0, "2": 0, "3": 0}, "level_clear_counts": {"1": 0, "2": 0, "3": 0},
"boss_first_kill": {}, "boss_first_kill": {},
"boss_kill_count": 0,
"attunement_equipped": [],
"attune_refresh_level": 0,
"recent_summaries": [], "recent_summaries": [],
"first_adventure_done": false, "first_adventure_done": false,
}, },
@@ -207,14 +224,55 @@ func boss_first_kill_marked(level_index: int) -> bool:
return _data["meta"]["boss_first_kill"].has(str(level_index)) return _data["meta"]["boss_first_kill"].has(str(level_index))
# ---- 同调位账本域(迭代 11 第①批 T2——里程碑谓词表计数源 + 装配关系 + Y 升级态)----
# Boss 击杀总数(含重复——底稿裁决 4 计数源;收束入账 = apply_adventure_summary 逐通关关次 +1,
# Boss 恒唯一终点故 levels_cleared 即本次冒险 Boss 击杀数)
func boss_kill_count() -> int:
return int(_data["meta"].get("boss_kill_count", 0))
# 装配关系(同调目录 item_id 有序数组——装配序即呈现序;跨冒险持久,冒险中途不可换装〔底稿 §8.5〕)。
# 局外进度事实(元进度域同族),非契约 2 反面清单的「局内道具库存」——存档序列化.md 契约 2 随批注记。
func attunement_equipped() -> Array:
return (_data["meta"].get("attunement_equipped", []) as Array).duplicate()
func set_attunement_equipped(item_ids: Array) -> void:
var out: Array = []
for v in item_ids:
out.append(String(v))
_data["meta"]["attunement_equipped"] = out
# Y 刷新升级购买等级(0 = 未购 → 每冒险刷新次数 0;等级行值归营地升级行族——T4 接线)
func attune_refresh_level() -> int:
return int(_data["meta"].get("attune_refresh_level", 0))
func set_attune_refresh_level(n: int) -> void:
_data["meta"]["attune_refresh_level"] = maxi(0, n)
# 同调位计数源组装(Attunement 服务取数口——clear_count = 逐关通关次数合计〔含重复关,底稿 §2.1 口径〕;
# clear_count(level_index) 逐关访问器零改动复用)
func attunement_counters() -> Dictionary:
var total := 0
var counts: Dictionary = _data["meta"]["level_clear_counts"]
for k in counts.keys():
total += int(counts[k])
return {"clear_count": total, "boss_kill_count": boss_kill_count()}
func recent_summaries() -> Array: func recent_summaries() -> Array:
return _data["meta"]["recent_summaries"].duplicate(true) # 值拷贝出户(本类契约「禁碰内部结构」——消费方直改不得渗入存档内存态) return _data["meta"]["recent_summaries"].duplicate(true) # 值拷贝出户(本类契约「禁碰内部结构」——消费方直改不得渗入存档内存态)
# 域更新原语(正式冒险收束唯一入账口——组合根经本函数入账,禁直改内部结构): # 域更新原语(正式冒险收束唯一入账口——组合根经本函数入账,禁直改内部结构):
# 钥石入账(summary.keystone_total——迭代 10 起逐杀掉落已在冒险内计入,boss_drops 参数降级为 # 钥石入账(summary.keystone_total——迭代 10 起逐杀掉落已在冒险内计入,boss_drops 参数降级为
# 摘要归档副本不重复入账)+ 逐通关关次 clear_count +1 与 Boss 首杀标记 + 摘要头部插入 # 摘要归档副本不重复入账)+ 逐通关关次 clear_count +1 与 Boss 首杀标记 + Boss 击杀总数累计
# (cap 10,最新在前,元素附 keystone_drops 副本——复盘可归因)。 # (迭代 11 第①批 T2——同调位里程碑计数源)+ 摘要头部插入(cap 10,最新在前,元素附
# keystone_drops 副本——复盘可归因)。
func apply_adventure_summary(summary: Dictionary, boss_drops: Array = []) -> void: func apply_adventure_summary(summary: Dictionary, boss_drops: Array = []) -> void:
add_keystone(int(summary.get("keystone_total", 0))) add_keystone(int(summary.get("keystone_total", 0)))
var cleared := int(summary.get("levels_cleared", 0)) var cleared := int(summary.get("levels_cleared", 0))
@@ -222,6 +280,7 @@ func apply_adventure_summary(summary: Dictionary, boss_drops: Array = []) -> voi
var key := str(li) var key := str(li)
_data["meta"]["level_clear_counts"][key] = int(_data["meta"]["level_clear_counts"].get(key, 0)) + 1 _data["meta"]["level_clear_counts"][key] = int(_data["meta"]["level_clear_counts"].get(key, 0)) + 1
_data["meta"]["boss_first_kill"][key] = true _data["meta"]["boss_first_kill"][key] = true
_data["meta"]["boss_kill_count"] = boss_kill_count() + cleared # 迭代 11 T2——Boss 恒唯一终点:levels_cleared 即本次冒险 Boss 击杀数(含重复)
var entry: Dictionary = summary.duplicate(true) var entry: Dictionary = summary.duplicate(true)
entry["boss_drops"] = boss_drops.duplicate(true) entry["boss_drops"] = boss_drops.duplicate(true)
var arr: Array = _data["meta"]["recent_summaries"] var arr: Array = _data["meta"]["recent_summaries"]
+5 -15
View File
@@ -10,9 +10,10 @@
class_name AdventureTable class_name AdventureTable
extends Resource extends Resource
## 三选一提升池件 id 词表(封闭——断点附随件与牌库剔除态依赖它;扩件走本表+registry 修订) ## 三选一提升池件 id 词表(封闭——断点附随件与牌库剔除态依赖它;扩件走本表+registry 修订)。
const BONUS_IDS: Array[String] = ["attack", "max_hp", "armor", "gp", "aux_item", "ap"] ## aux_item 池种退役(迭代 11 第①批 T7——同调批出池:魔法道具件改走营地同调装配面,不入三选一牌库)
## 无次数上限件的 cap 哨兵值(gp/aux_item——牌库恒非空的结构保证) const BONUS_IDS: Array[String] = ["attack", "max_hp", "armor", "gp", "ap"]
## 无次数上限件的 cap 哨兵值(gp——牌库恒非空的结构保证)
const BONUS_CAP_UNLIMITED := -1 const BONUS_CAP_UNLIMITED := -1
# —— 通关部分回血(固定型,非末关通关兑付)—— # —— 通关部分回血(固定型,非末关通关兑付)——
@@ -28,14 +29,12 @@ const BONUS_CAP_UNLIMITED := -1
@export var bonus_armor_flat: int = 1 # [0,999] knob——护甲轴累加 @export var bonus_armor_flat: int = 1 # [0,999] knob——护甲轴累加
@export var bonus_gp_amount: int = 20 # [0,999] knob——GP 袋定额入钱包 @export var bonus_gp_amount: int = 20 # [0,999] knob——GP 袋定额入钱包
@export var bonus_ap_flat: int = 1 # [0,10] knob——AP 件每场战斗首轮行动点基线增量 @export var bonus_ap_flat: int = 1 # [0,10] knob——AP 件每场战斗首轮行动点基线增量
@export var bonus_aux_item_count: int = 1 # [1,9] knob——魔法道具件授予件数(受同调位约束)
# —— 三选一权重(加权洗牌不放回,E-S weight-key)—— # —— 三选一权重(加权洗牌不放回,E-S weight-key)——
@export var bonus_weight_attack: int = 3 # [1,99] knob @export var bonus_weight_attack: int = 3 # [1,99] knob
@export var bonus_weight_max_hp: int = 3 # [1,99] knob @export var bonus_weight_max_hp: int = 3 # [1,99] knob
@export var bonus_weight_armor: int = 3 # [1,99] knob @export var bonus_weight_armor: int = 3 # [1,99] knob
@export var bonus_weight_gp: int = 3 # [1,99] knob @export var bonus_weight_gp: int = 3 # [1,99] knob
@export var bonus_weight_aux: int = 1 # [1,99] knob——魔法道具件(受同调位约束)
@export var bonus_weight_ap: int = 1 # [1,99] knob——AP 件(全冒险最紧上限) @export var bonus_weight_ap: int = 1 # [1,99] knob——AP 件(全冒险最紧上限)
# —— 全冒险获取次数上限(达上限件先剔出牌库;gp/aux_item 无上限)—— # —— 全冒险获取次数上限(达上限件先剔出牌库;gp/aux_item 无上限)——
@@ -44,9 +43,6 @@ const BONUS_CAP_UNLIMITED := -1
@export var bonus_cap_armor: int = 3 # [0,99] knob @export var bonus_cap_armor: int = 3 # [0,99] knob
@export var bonus_cap_ap: int = 2 # [0,99] knob——全冒险最紧 @export var bonus_cap_ap: int = 2 # [0,99] knob——全冒险最紧
# —— 同调位过渡读数源(正主 = attunement_growth.tres 谓词表,同调实施批落地后切换)——
@export var transitional_sync_slots: int = 1 # [0,9] knob——道具持有上限
func bonus_value(id: String) -> int: func bonus_value(id: String) -> int:
match id: match id:
@@ -55,7 +51,6 @@ func bonus_value(id: String) -> int:
"armor": return bonus_armor_flat "armor": return bonus_armor_flat
"gp": return bonus_gp_amount "gp": return bonus_gp_amount
"ap": return bonus_ap_flat "ap": return bonus_ap_flat
"aux_item": return bonus_aux_item_count
return 0 return 0
@@ -65,12 +60,11 @@ func bonus_weight(id: String) -> int:
"max_hp": return bonus_weight_max_hp "max_hp": return bonus_weight_max_hp
"armor": return bonus_weight_armor "armor": return bonus_weight_armor
"gp": return bonus_weight_gp "gp": return bonus_weight_gp
"aux_item": return bonus_weight_aux
"ap": return bonus_weight_ap "ap": return bonus_weight_ap
return 0 return 0
## 次数上限(gp/aux_item 返回 BONUS_CAP_UNLIMITED = 无上限,牌库恒非空的结构保证) ## 次数上限(gp 返回 BONUS_CAP_UNLIMITED = 无上限,牌库恒非空的结构保证)
func bonus_cap(id: String) -> int: func bonus_cap(id: String) -> int:
match id: match id:
"attack": return bonus_cap_attack "attack": return bonus_cap_attack
@@ -95,10 +89,6 @@ func validate() -> Array:
var c := bonus_cap(id) var c := bonus_cap(id)
if c < 0 or c > 99: if c < 0 or c > 99:
errs.append({"category": "domain", "message": "bonus_cap(%s) 越域 [0,99]: %d" % [id, c]}) errs.append({"category": "domain", "message": "bonus_cap(%s) 越域 [0,99]: %d" % [id, c]})
if bonus_aux_item_count < 1 or bonus_aux_item_count > 9:
errs.append({"category": "domain", "message": "bonus_aux_item_count 越域 [1,9]: %d" % bonus_aux_item_count})
if bonus_candidate_count < 1 or bonus_candidate_count > BONUS_IDS.size(): if bonus_candidate_count < 1 or bonus_candidate_count > BONUS_IDS.size():
errs.append({"category": "domain", "message": "bonus_candidate_count 越域 [1,%d]: %d" % [BONUS_IDS.size(), bonus_candidate_count]}) errs.append({"category": "domain", "message": "bonus_candidate_count 越域 [1,%d]: %d" % [BONUS_IDS.size(), bonus_candidate_count]})
if transitional_sync_slots < 0 or transitional_sync_slots > 9:
errs.append({"category": "domain", "message": "transitional_sync_slots 越域 [0,9]: %d" % transitional_sync_slots})
return errs return errs
@@ -0,0 +1,81 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 同调位增长是否由「通关次数 + Boss 击杀数」经数据驱动的里程碑谓词表实时求值,
# 试玩后调增长曲线 = 改表不动码(proposal-magic-item-attunement-2026-09-22 §2.1 用户点名要求)
# Date: 2026-10-03(迭代 11 第①批 魔法道具同调实施批 T1——任务书 v1.1 §六 T1;设计权威底稿 proposal §2)
# 数据配置 AttunementGrowthTable schema(魔法道具 回填 owner)。
# 单行全局常量表(slot_cap + milestones 行族——AdventureTable 单行全局先例,Rule 4 豁免)。
# 行 = Dictionary:{id: String 唯一, grant: int, requires: [{key: String, threshold: int}]}——
# requires 为 (计数键, 阈值) 对的 AND 列表,全部满足即授予 grant 槽;
# 同调位读数唯一口径 = Σ已达成 grant clamp slot_cap(slots())。
# 承重契约(任务书 §三 7 + 底稿 §2.1):
# 计数键封闭枚举(COUNTER_KEYS——预留可插拔位 COUNTER_KEYS_RESERVED 本期不实现、加行即扩展不破坏
# schema);阈值 [1,999];Σgrant ≤ slot_cap(超出部分永不可达,禁);数值全 provisional 归
# 难度曲线与Boss——本表只锁形态,试玩调平衡的全部动作落在表上(改阈值/换计数键/slot_cap 加行)。
# OR/复合需求出现时升级谓词形态走任务书修订(底稿 §2.1 同款约束)。
class_name AttunementGrowthTable
extends Resource
@export var slot_cap: int = 3
@export var milestones: Array = [] # Array[Dictionary] 键 id/grant/requires
# 计数键封闭枚举(底稿 §2.1——save_file 逐域记账的键词表;T2 落 boss_kill_count 记账)
const COUNTER_KEYS := ["clear_count", "boss_kill_count"]
# 预留可插拔位(本期不实现,schema 枚举不收——底稿 §2.1 明列四件)
const COUNTER_KEYS_RESERVED := ["boss_firstkill_count", "adventure_count", "no_damage_clear_count", "run_depth_best"]
func validate() -> Array:
var errs: Array = []
if slot_cap < 0 or slot_cap > 9:
errs.append({"category": "domain", "message": "slot_cap 越域 [0,9]: %d" % slot_cap})
var seen := {}
var grant_sum := 0
for row in milestones:
var mid: String = String(row.get("id", ""))
if mid == "":
errs.append({"category": "domain", "message": "milestone id 空"})
continue
if seen.has(mid):
errs.append({"category": "dup_key", "message": "milestone id 重复: %s" % mid})
continue
seen[mid] = true
var grant: int = int(row.get("grant", 0))
if grant < 1 or grant > maxi(1, slot_cap):
errs.append({"category": "domain", "message": "%s grant 越域 [1,slot_cap=%d]: %d" % [mid, slot_cap, grant]})
grant_sum += maxi(0, grant)
var reqs: Array = row.get("requires", [])
if reqs.is_empty():
errs.append({"category": "domain", "message": "%s requires 空(无条件授予非法——里程碑必有谓词)" % mid})
var key_seen := {}
for req in reqs:
var key: String = String(req.get("key", ""))
if not COUNTER_KEYS.has(key):
errs.append({"category": "enum", "message": "%s 计数键出域 %s: %s" % [mid, str(COUNTER_KEYS), key]})
if key_seen.has(key):
errs.append({"category": "dup_key", "message": "%s 计数键重复: %s" % [mid, key]})
key_seen[key] = true
var th: int = int(req.get("threshold", 0))
if th < 1 or th > 999:
errs.append({"category": "domain", "message": "%s 阈值越域 [1,999]: %d" % [mid, th]})
if grant_sum > slot_cap:
errs.append({"category": "cross_field", "message": "Σgrant(%d) > slot_cap(%d)——超出部分永不可达,schema 禁" % [grant_sum, slot_cap]})
return errs
# 实时求值:Σ已达成 grant(谓词 = requires 全键达标;counters = {计数键: int},缺键按 0 计)
func granted_slots(counters: Dictionary) -> int:
var total := 0
for row in milestones:
var ok := true
for req in row.get("requires", []):
if int(counters.get(String(req.get("key", "")), 0)) < int(req.get("threshold", 0)):
ok = false
break
if ok:
total += maxi(0, int(row.get("grant", 0)))
return total
# 同调位读数唯一口径:Σ达成 grant clamp slot_cap(求值服务/装配覆盖层/运行期上限共读此口径)
func slots(counters: Dictionary) -> int:
return mini(granted_slots(counters), slot_cap)
@@ -0,0 +1 @@
uid://tfgsbs87xakg
+96 -6
View File
@@ -18,15 +18,39 @@
# 真视窄域——effect_kinds 含 mine_reveal 的活行 target 必须 area_3x3(真视一次最多 3×3,禁全图看雷); # 真视窄域——effect_kinds 含 mine_reveal 的活行 target 必须 area_3x3(真视一次最多 3×3,禁全图看雷);
# board_wide 仅恢复推理原料类——target=board_wide 的活行 effect_kinds 不得含 mine_reveal(现行无此类活行, # board_wide 仅恢复推理原料类——target=board_wide 的活行 effect_kinds 不得含 mine_reveal(现行无此类活行,
# 显影载体待 Alpha 重定后落位)。 # 显影载体待 Alpha 重定后落位)。
# 同调实施批扩列(迭代 11 第①批 T1,2026-10-03——任务书 v1.1;设计权威 proposal-magic-item-attunement-2026-09-22):
# form ∈ {"", "passive", "active_charged"}(空 = 切片期过渡形态行——掉落/停点渠道现行承载,T7 退役;
# passive = 装配整局生效;active_charged = 每冒险充能、挖掘期发起);
# charges_per_adventure(active_charged ≥1 / passive 恒 0);slot_cost(同调行恒 1——底稿裁决 5,schema 强制);
# unlock_requires 谓词行 [{key ∈ AttunementGrowthTable.COUNTER_KEYS, threshold ∈ [1,999]}](空 = 已解锁——
# 首批全解锁合法态,底稿 §2.2 用户裁;正式解锁渠道归 元进度 道具工坊柜台);
# effective_rounds(前 N 轮限时件轮数——P4 甲表值化;仅 ap_rounds_front 行消费);
# charge_n(充能进度阈值 N——仅 ap_charge_safe 行消费)。
# 🔴 零裸 flat 结构护栏(任务书 §三 1——机制轴禁无条件属性加项):行键白名单封闭(KEY_WHITELIST 外键 =
# 加载致命)+ effect_kinds 封闭枚举只收条件触发/机制修正类 kind——任何「无条件属性加项」类效果不得以
# 新 kind 或新键入表(返工红线)。
class_name AuxItemTable class_name AuxItemTable
extends Resource extends Resource
@export var rows: Array = [] # Array[Dictionary] 键 item_id @export var rows: Array = [] # Array[Dictionary] 键 item_id
const DIMENSIONS := ["info", "risk", "action", "plan", "counter"] const DIMENSIONS := ["info", "risk", "action", "plan", "counter"]
const EFFECT_KINDS := ["mine_reveal", "reward_preview", "ap_grant"] # 同调批扩 enum(迭代 11 第①批 T1)——新增九 kind 全部条件触发/机制修正类;mine_reveal/ap_grant 语义不变。
# cell_removal = 爆破工兵包中立移除格(T7 原位换装时启用——T6 影响评估前置);mine_hit_halve 系固定
# 「减半」语义不消费 magnitude(反弹 N 由 reflect_damage 行 magnitude 承载)。
const EFFECT_KINDS := ["mine_reveal", "reward_preview", "ap_grant", "venom_rider", "flag_burst",
"mine_hit_halve", "reflect_damage", "ap_rounds_front", "ap_charge_safe", "cell_removal",
"reward_weight_bias", "bonus_refresh"]
const TARGETS := ["none", "area_3x3", "board_wide"] const TARGETS := ["none", "area_3x3", "board_wide"]
const USE_PHASES := ["active_dig"] const USE_PHASES := ["active_dig"]
const FORMS := ["", "passive", "active_charged"]
# magnitude 消费面(≥1 校验):ap_grant 与同调 N 值 kind;其余 kind 活行 magnitude 恒 0
const MAGNITUDE_KINDS := ["ap_grant", "venom_rider", "flag_burst", "reflect_damage", "ap_rounds_front",
"ap_charge_safe", "reward_weight_bias", "bonus_refresh"]
# 🔴 零裸 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"]
func validate() -> Array: func validate() -> Array:
@@ -62,6 +86,13 @@ func validate() -> Array:
var phase: String = String(row.get("use_phase", "active_dig")) var phase: String = String(row.get("use_phase", "active_dig"))
if not USE_PHASES.has(phase): if not USE_PHASES.has(phase):
errs.append({"category": "enum", "message": "%s use_phase 出域 %s: %s" % [iid, str(USE_PHASES), phase]}) errs.append({"category": "enum", "message": "%s use_phase 出域 %s: %s" % [iid, str(USE_PHASES), phase]})
# 🔴 零裸 flat 结构护栏:键白名单封闭(同调批 T1——机制轴禁无条件属性加项)
for k in row.keys():
if not KEY_WHITELIST.has(String(k)):
errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s(KEY_WHITELIST 外键禁入)" % [iid, String(k)]})
var form: String = String(row.get("form", ""))
if not FORMS.has(form):
errs.append({"category": "enum", "message": "%s form 出域 %s: %s" % [iid, str(FORMS), form]})
if retired or deferred: if retired or deferred:
# 退役/失义行:免 effect_kinds 非空与承重护栏校验(登记面保留,不入投放与 F2) # 退役/失义行:免 effect_kinds 非空与承重护栏校验(登记面保留,不入投放与 F2)
continue continue
@@ -82,17 +113,76 @@ func validate() -> Array:
# 承重护栏②board_wide 仅恢复推理类:全图域行禁替推理效果 # 承重护栏②board_wide 仅恢复推理类:全图域行禁替推理效果
if target == "board_wide" and kind_seen.has("mine_reveal"): 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}) errs.append({"category": "load_bearing", "message": "%s board_wide 行 effect_kinds 不得含 mine_reveal(全图域仅恢复推理原料类——Alpha 重定后落位)" % iid})
# magnitude:仅 ap_grant 行消费(≥1);非 ap_grant 活行恒 0 # magnitude:MAGNITUDE_KINDS 行消费(≥1);其余活行恒 0(同调批 T1——消费面自 ap_grant 扩至 N 值 kind)
var mag: int = int(row.get("magnitude", 0)) var mag: int = int(row.get("magnitude", 0))
if mag < 0 or mag > 999: if mag < 0 or mag > 999:
errs.append({"category": "domain", "message": "%s magnitude 越域 [0,999]: %d" % [iid, mag]}) errs.append({"category": "domain", "message": "%s magnitude 越域 [0,999]: %d" % [iid, mag]})
if kind_seen.has("ap_grant") and mag < 1: var mag_consuming := false
errs.append({"category": "cross_field", "message": "%s ap_grant 行 magnitude 应 ≥1,实得 %d" % [iid, mag]}) for k in kind_seen.keys():
if not kind_seen.has("ap_grant") and mag != 0: if MAGNITUDE_KINDS.has(String(k)):
errs.append({"category": "cross_field", "message": "%s 非 ap_grant 活行 magnitude 应 0,实得 %d" % [iid, mag]}) mag_consuming = true
if mag_consuming and mag < 1:
errs.append({"category": "cross_field", "message": "%s N 值 kind 行 magnitude 应 ≥1,实得 %d" % [iid, mag]})
if not mag_consuming and mag != 0:
errs.append({"category": "cross_field", "message": "%s 非 N 值 kind 活行 magnitude 应 0,实得 %d" % [iid, mag]})
# ---- 同调行约束(form 非空——装配面;过渡行 form="" 不校验)----
if form != "":
if int(row.get("slot_cost", 1)) != 1:
errs.append({"category": "load_bearing", "message": "%s 同调占位统一 1 槽(slot_cost 恒 1——底稿裁决 5,schema 强制)" % iid})
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]})
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]})
if kind_seen.has("ap_rounds_front") and eff_rounds < 1:
errs.append({"category": "cross_field", "message": "%s ap_rounds_front 行 effective_rounds 应 ≥1(P4 甲表值化),实得 %d" % [iid, eff_rounds]})
if not kind_seen.has("ap_rounds_front") and eff_rounds != 0:
errs.append({"category": "cross_field", "message": "%s 非 ap_rounds_front 行 effective_rounds 应 0,实得 %d" % [iid, eff_rounds]})
var charge_n: int = int(row.get("charge_n", 0))
if charge_n < 0 or charge_n > 999:
errs.append({"category": "domain", "message": "%s charge_n 越域 [0,999]: %d" % [iid, charge_n]})
if kind_seen.has("ap_charge_safe") and charge_n < 1:
errs.append({"category": "cross_field", "message": "%s ap_charge_safe 行 charge_n 应 ≥1(每次充能需 N 安全格),实得 %d" % [iid, charge_n]})
if not kind_seen.has("ap_charge_safe") and charge_n != 0:
errs.append({"category": "cross_field", "message": "%s 非 ap_charge_safe 行 charge_n 应 0,实得 %d" % [iid, charge_n]})
# unlock_requires 谓词行:空 = 已解锁(首批全解锁合法态,底稿 §2.2);非空键 ∈ 计数键封闭枚举
var ureqs: Array = row.get("unlock_requires", [])
var ukeys := {}
for req in ureqs:
var ukey: String = String(req.get("key", ""))
if not AttunementGrowthTable.COUNTER_KEYS.has(ukey):
errs.append({"category": "enum", "message": "%s unlock_requires 计数键出域 %s: %s" % [iid, str(AttunementGrowthTable.COUNTER_KEYS), ukey]})
if ukeys.has(ukey):
errs.append({"category": "dup_key", "message": "%s unlock_requires 计数键重复: %s" % [iid, ukey]})
ukeys[ukey] = true
var uth: int = int(req.get("threshold", 0))
if uth < 1 or uth > 999:
errs.append({"category": "domain", "message": "%s unlock_requires 阈值越域 [1,999]: %d" % [iid, uth]})
return errs return errs
# 同调行词表(form 非空)——装配覆盖层/效果接线/同调位上限的唯一取数口径(迭代 11 第①批 T1)
func attunement_rows() -> Array:
var out: Array = []
for row in rows:
if String(row.get("form", "")) != "" and not bool(row.get("retired", false)) and not bool(row.get("deferred", false)):
out.append(row)
return out
# 过渡行词表(form 空 = 切片期过渡形态活行)——掉落投放/停点池/过渡覆盖面的取数口径;
# 同调行永不入过渡渠道。T7 退役面切换后本口径随渠道消亡。
func transitional_rows() -> Array:
var out: Array = []
for row in rows:
if String(row.get("form", "")) == "" and not bool(row.get("retired", false)) and not bool(row.get("deferred", false)):
out.append(row)
return out
# 活行词表(非 retired 非 deferred)——投放校验/奖励节点池/F2 覆盖面的唯一取数口径 # 活行词表(非 retired 非 deferred)——投放校验/奖励节点池/F2 覆盖面的唯一取数口径
func active_rows() -> Array: func active_rows() -> Array:
var out: Array = [] var out: Array = []
+23 -2
View File
@@ -12,6 +12,8 @@
# 达标 clear_threshold——2026-09-20 排期裁决;局内效果整体归 Alpha 魔法道具重设计批,本表只有解锁状态面)。 # 达标 clear_threshold——2026-09-20 排期裁决;局内效果整体归 Alpha 魔法道具重设计批,本表只有解锁状态面)。
# conversion = 区间平移换算率行(attr 与 forge 词表一致恰三;per_level_shift_pct ∈ [0,100]—— # conversion = 区间平移换算率行(attr 与 forge 词表一致恰三;per_level_shift_pct ∈ [0,100]——
# registry 键 meta_progression_conversion_table;护甲行强条目:验收读数必须按解锁后参数重测)。 # registry 键 meta_progression_conversion_table;护甲行强条目:验收读数必须按解锁后参数重测)。
# attune_refresh_upgrade = Y 刷新升级行族(迭代 11 第①批 T4——P1 甲:每冒险开局给 Y 次、冒险周期池、
# 用完即无;level 连续从 1,等级 L → refresh_charges=Y 表值,钥石直购 keystone_cost;数值全 provisional)。
# 行值全部 provisional 归 元进度设计批配读数(用户拍板骨架先行,先例 = GP+商店房批)。 # 行值全部 provisional 归 元进度设计批配读数(用户拍板骨架先行,先例 = GP+商店房批)。
class_name MetaTable class_name MetaTable
extends Resource extends Resource
@@ -20,6 +22,7 @@ extends Resource
@export var boss_keystone: Array[Dictionary] = [] # Array[Dictionary] 键 level_index @export var boss_keystone: Array[Dictionary] = [] # Array[Dictionary] 键 level_index
@export var workshop_items: Array[Dictionary] = [] # Array[Dictionary] 键 item_id @export var workshop_items: Array[Dictionary] = [] # Array[Dictionary] 键 item_id
@export var conversion: Array[Dictionary] = [] # Array[Dictionary] 键 attr @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"] const FORGE_ATTRS := ["max_hp", "atk", "armor_class"]
const WORKSHOP_ITEM_IDS := ["probe_dust", "sapper_order", "loot_compass", "nest_pendulum", "catacomb_scroll", "blast_kit"] const WORKSHOP_ITEM_IDS := ["probe_dust", "sapper_order", "loot_compass", "nest_pendulum", "catacomb_scroll", "blast_kit"]
@@ -31,8 +34,8 @@ const WORKSHOP_CHANNELS := ["total_clears"]
func validate() -> Array: func validate() -> Array:
var errs: Array[Dictionary] = [] var errs: Array[Dictionary] = []
# 行族空护栏(四族皆为 ConfigDB 消费面——缺族即致命) # 行族空护栏(五族皆为 ConfigDB 消费面——缺族即致命)
for family in ["forge_products", "boss_keystone", "workshop_items", "conversion"]: for family in ["forge_products", "boss_keystone", "workshop_items", "conversion", "attune_refresh_upgrade"]:
if get(family).is_empty(): if get(family).is_empty():
errs.append({"category": "empty_table", "message": "MetaTable 行族为空: %s" % family}) errs.append({"category": "empty_table", "message": "MetaTable 行族为空: %s" % family})
# forge_products:attr 词表封闭 + 唯一 + 覆盖恰三(注入契约三字段必齐)+ flat/价格域 + 公比域 [100,400] # forge_products:attr 词表封闭 + 唯一 + 覆盖恰三(注入契约三字段必齐)+ flat/价格域 + 公比域 [100,400]
@@ -115,4 +118,22 @@ func validate() -> Array:
for conv_key in FORGE_ATTRS: for conv_key in FORGE_ATTRS:
if not seen_conv.has(conv_key): if not seen_conv.has(conv_key):
errs.append({"category": "coverage", "message": "conversion 缺属性行: %s(三行与 forge 词表一致)" % conv_key}) errs.append({"category": "coverage", "message": "conversion 缺属性行: %s(三行与 forge 词表一致)" % conv_key})
# attune_refresh_upgrade:level 唯一 + 连续从 1 + charges/cost 域(迭代 11 第①批 T4)
var seen_al := {}
for row in attune_refresh_upgrade:
var al: int = int(row.get("level", -1))
if seen_al.has(al):
errs.append({"category": "dup_key", "message": "attune_refresh_upgrade level 重复: %d" % al})
seen_al[al] = true
var ach: int = int(row.get("refresh_charges", -1))
if ach < 1 or ach > 9:
errs.append({"category": "domain", "message": "attune_refresh_upgrade L%d refresh_charges 越域 [1,9]: %d" % [al, ach]})
var acost: int = int(row.get("keystone_cost", -1))
if acost < 1 or acost > 999:
errs.append({"category": "domain", "message": "attune_refresh_upgrade L%d keystone_cost 越域 [1,999]: %d" % [al, acost]})
for i in attune_refresh_upgrade.size():
var al_at: int = int(attune_refresh_upgrade[i].get("level", 0))
if al_at != i + 1:
errs.append({"category": "structural", "message": "attune_refresh_upgrade level 不连续(须从 1 连续递增): 位 %d = %d" % [i + 1, al_at]})
break
return errs return errs
+6 -6
View File
@@ -10,24 +10,24 @@ bonus_max_hp_flat = 5
bonus_armor_flat = 1 bonus_armor_flat = 1
bonus_gp_amount = 20 bonus_gp_amount = 20
bonus_ap_flat = 1 bonus_ap_flat = 1
bonus_aux_item_count = 1
bonus_weight_attack = 3 bonus_weight_attack = 3
bonus_weight_max_hp = 3 bonus_weight_max_hp = 3
bonus_weight_armor = 3 bonus_weight_armor = 3
bonus_weight_gp = 3 bonus_weight_gp = 3
bonus_weight_aux = 1
bonus_weight_ap = 1 bonus_weight_ap = 1
bonus_cap_attack = 3 bonus_cap_attack = 3
bonus_cap_max_hp = 3 bonus_cap_max_hp = 3
bonus_cap_armor = 3 bonus_cap_armor = 3
bonus_cap_ap = 2 bonus_cap_ap = 2
transitional_sync_slots = 1
; 迭代 10 奖励重构批(2026-10-01 扩列——registry adventure_table 条目同批改写): ; 迭代 10 奖励重构批(2026-10-01 扩列——registry adventure_table 条目同批改写):
; 大奖励 = 非末关通关恰一次:部分回血(clear_heal_pct,封顶上限不回满、满血回 0 合法) ; 大奖励 = 非末关通关恰一次:部分回血(clear_heal_pct,封顶上限不回满、满血回 0 合法)
; + 提升三选一(池六种加权洗牌不放回 E-S weight-key 掷 3,玩家点选兑付恰一件;消费 reward_pick 封闭流)。 ; + 提升三选一(池六种加权洗牌不放回 E-S weight-key 掷 3,玩家点选兑付恰一件;消费 reward_pick 封闭流)。
; 权重:属性/GP 各 3、道具/AP 各 1;次数上限:属性件各 3、AP 件 2(达上限件先剔牌库)——gp/道具无次数上限。 ; 权重:属性/GP 各 3、AP 1;次数上限:属性件各 3、AP 件 2(达上限件先剔牌库)——gp 无次数上限。
; 同调位过渡键 transitional_sync_slots=1(道具持有上限;正主 = attunement_growth.tres 谓词表,同调实施批切换)。 ; 迭代 11 第①批 同调实施批 T7(2026-10-03):aux_item 池种退役出三选一牌库(bonus_weight_aux/
; bonus_aux_item_count 两字段随批退役);transitional_sync_slots 过渡键退役(正主 = attunement_growth.tres
; 谓词表求值,Attunement.slots——迭代 10 欠账③清偿)。
; 末关通关不发三选一与回血(奖励只朝下一战生效)。 ; 末关通关不发三选一与回血(奖励只朝下一战生效)。
; 退役字段:level_clear_hp_bonus(血回满+上限提升件)、keystone_per_level(钥石改 Boss/精英击杀掉落,行值归 meta.tres boss_keystone)。 ; 退役字段:level_clear_hp_bonus(血回满+上限提升件)、keystone_per_level(钥石改 Boss/精英击杀掉落,行值归 meta.tres boss_keystone)、
; transitional_sync_slots / bonus_weight_aux / bonus_aux_item_count(本批 T7 退役三件)。
; 全部 provisional-pending-曲线调参批(值锚与上调约束归 难度曲线与Boss;回血 20% 系难度上压方向,带重扫随批呈报)。 ; 全部 provisional-pending-曲线调参批(值锚与上调约束归 难度曲线与Boss;回血 20% 系难度上压方向,带重扫随批呈报)。
+34
View File
@@ -0,0 +1,34 @@
[gd_resource type="Resource" script_class="AttunementGrowthTable" load_steps=2 format=3]
[ext_resource type="Script" path="res://data/schemas/attunement_growth_table.gd" id="1"]
; 迭代 11 第①批 同调实施批 T1(2026-10-03)——同调位里程碑谓词表。
; 数值全 provisional 归 难度曲线与Boss(任务书 §三 7);行形态 = 底稿 §2.1 示意形态。
[resource]
script = ExtResource("1")
slot_cap = 3
milestones = [{
"id": "m1",
"grant": 1,
"requires": [{
"key": "clear_count",
"threshold": 1
}]
}, {
"id": "m2",
"grant": 1,
"requires": [{
"key": "boss_kill_count",
"threshold": 3
}]
}, {
"id": "m3",
"grant": 1,
"requires": [{
"key": "clear_count",
"threshold": 3
}, {
"key": "boss_kill_count",
"threshold": 6
}]
}]
+83 -3
View File
@@ -4,6 +4,8 @@
[resource] [resource]
script = ExtResource("1") script = ExtResource("1")
; 迭代 11 第①批 T1(2026-10-03):追加七件同调行(form 非空——永不入过渡渠道);值全 provisional 归 难度曲线与Boss。
; nest_pendulum/blast_kit 维持过渡行,T7 渠道退役时原位换装 form=active_charged;sapper_order/显影尘/地穴全图卷/罗盘 T7 逐件销账。
rows = [{ rows = [{
"item_id": "probe_dust", "item_id": "probe_dust",
"display_name": "显影尘", "display_name": "显影尘",
@@ -20,7 +22,8 @@ rows = [{
"effect_kinds": ["ap_grant"], "effect_kinds": ["ap_grant"],
"target": "none", "target": "none",
"magnitude": 2, "magnitude": 2,
"use_phase": "active_dig" "use_phase": "active_dig",
"retired": true
}, { }, {
"item_id": "loot_compass", "item_id": "loot_compass",
"display_name": "探宝罗盘", "display_name": "探宝罗盘",
@@ -37,7 +40,10 @@ rows = [{
"effect_kinds": ["mine_reveal"], "effect_kinds": ["mine_reveal"],
"target": "area_3x3", "target": "area_3x3",
"magnitude": 0, "magnitude": 0,
"use_phase": "active_dig" "use_phase": "active_dig",
"form": "active_charged",
"slot_cost": 1,
"charges_per_adventure": 2
}, { }, {
"item_id": "catacomb_scroll", "item_id": "catacomb_scroll",
"display_name": "地穴全图卷", "display_name": "地穴全图卷",
@@ -54,5 +60,79 @@ rows = [{
"effect_kinds": ["ap_grant", "mine_reveal"], "effect_kinds": ["ap_grant", "mine_reveal"],
"target": "area_3x3", "target": "area_3x3",
"magnitude": 4, "magnitude": 4,
"use_phase": "active_dig" "use_phase": "active_dig",
"deferred": true
}, {
"item_id": "venom_edge",
"display_name": "淬毒刃纹",
"dimension_tags": ["counter"],
"effect_kinds": ["venom_rider"],
"target": "none",
"magnitude": 2,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1
}, {
"item_id": "flag_burst",
"display_name": "标中殉爆",
"dimension_tags": ["action"],
"effect_kinds": ["flag_burst"],
"target": "none",
"magnitude": 2,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1
}, {
"item_id": "nest_counter",
"display_name": "惊巢反制",
"dimension_tags": ["risk"],
"effect_kinds": ["mine_hit_halve", "reflect_damage"],
"target": "none",
"magnitude": 2,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1
}, {
"item_id": "dawn_order",
"display_name": "工兵晨令",
"dimension_tags": ["action"],
"effect_kinds": ["ap_rounds_front"],
"target": "none",
"magnitude": 1,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1,
"effective_rounds": 3
}, {
"item_id": "dig_charge",
"display_name": "掘进蓄能",
"dimension_tags": ["action"],
"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
}, {
"item_id": "luck_coin",
"display_name": "偏运硬币",
"dimension_tags": ["risk"],
"effect_kinds": ["reward_weight_bias"],
"target": "none",
"magnitude": 2,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1
}, {
"item_id": "pick_refresh",
"display_name": "三选一刷新件",
"dimension_tags": ["info"],
"effect_kinds": ["bonus_refresh"],
"target": "none",
"magnitude": 1,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1
}] }]
+13
View File
@@ -77,6 +77,15 @@ conversion = [{
"attr": "armor_class", "attr": "armor_class",
"per_level_shift_pct": 1 "per_level_shift_pct": 1
}] }]
attune_refresh_upgrade = [{
"level": 1,
"refresh_charges": 1,
"keystone_cost": 30
}, {
"level": 2,
"refresh_charges": 2,
"keystone_cost": 60
}]
; 元进度实现批(2026-09-21,设计权威 design/gdd/systems/冒险循环/元进度.md 定稿 v1.2): ; 元进度实现批(2026-09-21,设计权威 design/gdd/systems/冒险循环/元进度.md 定稿 v1.2):
; 数值全 provisional 归元进度设计批配读数(骨架先行——元进度实现批 2026-09-21,先例 = GP+商店房批)。 ; 数值全 provisional 归元进度设计批配读数(骨架先行——元进度实现批 2026-09-21,先例 = GP+商店房批)。
@@ -107,3 +116,7 @@ conversion = [{
; 钥石渠道自本批起 = ; 钥石渠道自本批起 =
; Boss/精英击杀逐杀掉落(大奖励 keystone_per_level 渠道退役;精英掉 1 预留行随怪物批实装落)。 ; Boss/精英击杀逐杀掉落(大奖励 keystone_per_level 渠道退役;精英掉 1 预留行随怪物批实装落)。
; 产出结构变化 → 元进度标定义务重开(06-待办清单 2026-10-01 登记条)。provisional 归曲线调参批。 ; 产出结构变化 → 元进度标定义务重开(06-待办清单 2026-10-01 登记条)。provisional 归曲线调参批。
;
; 迭代 11 第①批 同调实施批 T4(2026-10-03):attune_refresh_upgrade 行族 = Y 刷新升级行(任务书 P1 甲——
; 每冒险开局给 Y 次、冒险周期池、用完即无;等级 L → Y=L 次刷新,钥石直购)。数值全 provisional 归
; 难度曲线与Boss(任务书 §三 7)。
+3 -2
View File
@@ -34,12 +34,13 @@ func _init(db_ref, meta_ref, info_panel_ref: HeroInfoPanelView, toast_ref: Toast
_runner_get = runner_get _runner_get = runner_get
# aux 解锁门控集组装(迭代 7 T5——拍板③甲:工坊解锁 ∩ 活行词表;罗盘退役/失义件不入集) # aux 解锁门控集组装(迭代 7 T5——拍板③甲:工坊解锁 ∩ 过渡活行词表;罗盘退役/失义件/同调行不入集
# ——迭代 11 第①批 T1 切 aux_transitional_rows 口径,同调行永不入过渡渠道)
func unlocked_set() -> Dictionary: func unlocked_set() -> Dictionary:
var out := {} var out := {}
if meta == null: if meta == null:
return out return out
for row in db.aux_active_rows(): for row in db.aux_transitional_rows():
var iid := String(row.get("item_id", "")) var iid := String(row.get("item_id", ""))
if meta.is_item_unlocked(iid): if meta.is_item_unlocked(iid):
out[iid] = true out[iid] = true
+37 -1
View File
@@ -254,6 +254,7 @@ func _build_ui() -> void:
shop_overlay.buy_requested.connect(_on_shop_buy) shop_overlay.buy_requested.connect(_on_shop_buy)
shop_overlay.leave_pressed.connect(_on_shop_leave) shop_overlay.leave_pressed.connect(_on_shop_leave)
camp_overlay.buy_requested.connect(_on_camp_buy) camp_overlay.buy_requested.connect(_on_camp_buy)
camp_overlay.attune_toggled.connect(_on_camp_attune_toggle)
camp_overlay.closed.connect(_on_camp_closed) camp_overlay.closed.connect(_on_camp_closed)
_end_overlay.review_pressed.connect(_on_end_review) _end_overlay.review_pressed.connect(_on_end_review)
_end_overlay.continue_pressed.connect(_on_battle_end_continue) _end_overlay.continue_pressed.connect(_on_battle_end_continue)
@@ -269,6 +270,7 @@ func _build_ui() -> void:
bonus_overlay.pick_confirmed.connect(_on_bonus_pick_confirmed) bonus_overlay.pick_confirmed.connect(_on_bonus_pick_confirmed)
bonus_overlay.replace_chosen.connect(_on_bonus_replace_chosen) bonus_overlay.replace_chosen.connect(_on_bonus_replace_chosen)
bonus_overlay.continue_pressed.connect(_on_bonus_continue) bonus_overlay.continue_pressed.connect(_on_bonus_continue)
bonus_overlay.refresh_requested.connect(_on_bonus_refresh)
move_child(bonus_overlay, _end_overlay.get_index()) move_child(bonus_overlay, _end_overlay.get_index())
var start: Dictionary = UiAssembler.build_start_overlay(self, db) var start: Dictionary = UiAssembler.build_start_overlay(self, db)
@@ -486,6 +488,7 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void:
_open_modal(bonus_overlay) _open_modal(bonus_overlay)
bonus_overlay.open_pick(Narrator.node_bonus_header(), _bonus_card_texts(node_cands, node_draw.get("aux_detail", []) as Array)) bonus_overlay.open_pick(Narrator.node_bonus_header(), _bonus_card_texts(node_cands, node_draw.get("aux_detail", []) as Array))
adventure.present_node_bonus(node_cands, node_draw.get("aux_detail", []) as Array) adventure.present_node_bonus(node_cands, node_draw.get("aux_detail", []) as Array)
bonus_overlay.set_refresh_info(adventure.attune_refresh_left()) # 迭代 11 第①批 T5b——刷新余量投喂
log_view.append_bb(Narrator.reward_line()) log_view.append_bb(Narrator.reward_line())
elif t == "rest": elif t == "rest":
var healed: int = int(info.get("healed", 0)) var healed: int = int(info.get("healed", 0))
@@ -527,6 +530,7 @@ func _show_bonus_pick() -> void:
var payload: Dictionary = _pending_bonus var payload: Dictionary = _pending_bonus
_open_modal(bonus_overlay) _open_modal(bonus_overlay)
bonus_overlay.open_pick(Narrator.bonus_pick_header(payload), _bonus_card_texts(payload.get("candidates", []) as Array, payload.get("aux_detail", []) as Array)) bonus_overlay.open_pick(Narrator.bonus_pick_header(payload), _bonus_card_texts(payload.get("candidates", []) as Array, payload.get("aux_detail", []) as Array))
bonus_overlay.set_refresh_info(adventure.attune_refresh_left() if adventure != null else 0) # 迭代 11 第①批 T5b——刷新余量投喂
_refresh_presentations() _refresh_presentations()
var _last_replace_items: Array = [] # 替换子态续玩重建缓存(aux_replace_presented 载荷) var _last_replace_items: Array = [] # 替换子态续玩重建缓存(aux_replace_presented 载荷)
@@ -572,6 +576,19 @@ func _aux_card_line(iid: String) -> String:
return "%s · %s" % [String(row.get("display_name", iid)), " ".join(parts)] return "%s · %s" % [String(row.get("display_name", iid)), " ".join(parts)]
# 三选一刷新(迭代 11 第①批 T5b——P1 甲 X/Y 双源;通关停点经重发 bonus_choice_presented 自然重建,
# 节点停点在此读在途态重 present——零自动推进契约不破,停点仍在途)
func _on_bonus_refresh() -> void:
if adventure == null:
return
if not adventure.refresh_bonus():
return
if _bonus_context == "reward_node":
var st: Dictionary = adventure.node_bonus_pending_state()
bonus_overlay.open_pick(Narrator.node_bonus_header(), _bonus_card_texts(st.get("candidates", []) as Array, st.get("aux_detail", []) as Array))
bonus_overlay.set_refresh_info(adventure.attune_refresh_left())
func _on_bonus_pick_confirmed(item_id: String) -> void: func _on_bonus_pick_confirmed(item_id: String) -> void:
if adventure == null: if adventure == null:
return return
@@ -595,7 +612,7 @@ func _open_bonus_replace(items: Array, incoming: Dictionary) -> void:
rec["display_name"] = String(_item_name_lookup().get(String(rec.get("item_id", "")), String(rec.get("item_id", "")))) rec["display_name"] = String(_item_name_lookup().get(String(rec.get("item_id", "")), String(rec.get("item_id", ""))))
named.append(rec) named.append(rec)
bonus_overlay.open_replace(named, String(_item_name_lookup().get(String(incoming.get("item_id", "")), String(incoming.get("item_id", "")))), bonus_overlay.open_replace(named, String(_item_name_lookup().get(String(incoming.get("item_id", "")), String(incoming.get("item_id", "")))),
named.size(), int(db.adventure.transitional_sync_slots)) named.size(), Attunement.slots(db, save_manager)) # 迭代 11 第①批 T7——呈现读数源切谓词表求值(过渡键退役)
func _on_bonus_replace_chosen(discard: int) -> void: func _on_bonus_replace_chosen(discard: int) -> void:
@@ -1028,6 +1045,7 @@ func _camp_data() -> Dictionary:
"balance": meta.keystone_balance() if meta != null else 0, "balance": meta.keystone_balance() if meta != null else 0,
"forge": forge_rows, "forge": forge_rows,
"workshop": meta.workshop_status() if meta != null else [], "workshop": meta.workshop_status() if meta != null else [],
"attune": meta.attune_camp_status() if meta != null else {}, # 迭代 11 第①批 T4——同调装配面投影
"recent": save_manager.recent_summaries() if save_manager != null else [], "recent": save_manager.recent_summaries() if save_manager != null else [],
} }
@@ -1035,6 +1053,14 @@ func _camp_data() -> Dictionary:
func _on_camp_buy(attr: String) -> void: func _on_camp_buy(attr: String) -> void:
if meta == null: if meta == null:
return return
if attr == "attune_refresh":
# Y 刷新升级行购买(迭代 11 第①批 T4——P1 甲;按钮禁用为常态防线,兜底拒绝可见 pattern 1)
if not meta.buy_attune_refresh():
board_zone.toast.show_toast("钥石不足——买不起这次升级", "warn")
return
camp_overlay.refresh_camp(_camp_data())
_refresh_presentations()
return
if not meta.buy_forge(attr): if not meta.buy_forge(attr):
# 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1) # 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1)
board_zone.toast.show_toast("钥石不足——买不起这件强化", "warn") board_zone.toast.show_toast("钥石不足——买不起这件强化", "warn")
@@ -1043,6 +1069,16 @@ func _on_camp_buy(attr: String) -> void:
_refresh_presentations() _refresh_presentations()
# 同调装配切换(迭代 11 第①批 T4——装配关系唯一写口经 MetaProgression.toggle_attunement;满位兜底拒绝可见)
func _on_camp_attune_toggle(item_id: String) -> void:
if meta == null:
return
if not meta.toggle_attunement(item_id):
board_zone.toast.show_toast("同调位已满——先卸下再装配", "warn")
return
camp_overlay.refresh_camp(_camp_data())
# ---- 商店覆盖层与 GP 钱包(GP+商店房实施批——写调用唯一归组合根)---- # ---- 商店覆盖层与 GP 钱包(GP+商店房实施批——写调用唯一归组合根)----
func _on_shop_buy(product_id: String) -> void: func _on_shop_buy(product_id: String) -> void:
@@ -10,12 +10,16 @@ extends Control
signal pick_confirmed(item_id: String) signal pick_confirmed(item_id: String)
signal replace_chosen(discard_index: int) # -1 = 放弃新件不获取 signal replace_chosen(discard_index: int) # -1 = 放弃新件不获取
signal continue_pressed signal continue_pressed
signal refresh_requested # 三选一刷新意图(迭代 11 第①批 T5b——P1 甲 X/Y 双源;重抽归组合根)
var _label: Label var _label: Label
var _choice_box: HBoxContainer # 三卡按钮区(pick 态) var _choice_box: HBoxContainer # 三卡按钮区(pick 态)
var _replace_box: VBoxContainer # 替换子面板(replace 态) var _replace_box: VBoxContainer # 替换子面板(replace 态)
var _confirm_btn: Button # 确认钮(点选后点亮——两段式第二拍) var _confirm_btn: Button # 确认钮(点选后点亮——两段式第二拍)
var _continue_btn: Button # 继续钮(兑付反馈态唯一出口) var _continue_btn: Button # 继续钮(兑付反馈态唯一出口)
var _refresh_btn: Button # 刷新钮(迭代 11 第①批 T5b——余量>0 才显示)
var _refresh_l: Label # 刷新余量读数(X 优先 + Y 冒险池)
var _refresh_allowance := 0 # 组合根投喂余量(本视图零核心引用)
var _selected := "" # 点选暂存(确认前可改选) var _selected := "" # 点选暂存(确认前可改选)
var _header := "" # 停点头部文案(选择态保持显示) var _header := "" # 停点头部文案(选择态保持显示)
var _card_meta: Array = [] # open_pick 缓存 [{id, text}]——高亮还原读面 var _card_meta: Array = [] # open_pick 缓存 [{id, text}]——高亮还原读面
@@ -34,6 +38,13 @@ func _init() -> void:
_confirm_btn.pressed.connect(_on_confirm_pressed) _confirm_btn.pressed.connect(_on_confirm_pressed)
_confirm_btn.visible = false _confirm_btn.visible = false
box.add_child(_confirm_btn) box.add_child(_confirm_btn)
# 三选一刷新面(迭代 11 第①批 T5b——程序占位用 UiKit 既有件;余量 0 = 隐藏)
_refresh_btn = UiKit.button("刷新候选")
_refresh_btn.pressed.connect(_emit_refresh_requested)
_refresh_btn.visible = false
box.add_child(_refresh_btn)
_refresh_l = UiKit.label("", 13, false, UiKit.STONE)
box.add_child(_refresh_l)
_replace_box = VBoxContainer.new() _replace_box = VBoxContainer.new()
_replace_box.add_theme_constant_override("separation", 8) _replace_box.add_theme_constant_override("separation", 8)
_replace_box.alignment = BoxContainer.ALIGNMENT_CENTER _replace_box.alignment = BoxContainer.ALIGNMENT_CENTER
@@ -64,9 +75,22 @@ func open_pick(header: String, cards: Array) -> void:
_confirm_btn.visible = false _confirm_btn.visible = false
_replace_box.visible = false _replace_box.visible = false
_continue_btn.visible = false _continue_btn.visible = false
_refresh_btn.visible = false # 刷新余量随 set_refresh_info 投喂后现形(open 先归零防残留)
_refresh_l.text = ""
visible = true visible = true
# 三选一刷新余量投喂(组合根读 adventure.attune_refresh_left()——0 = 刷新面隐藏)
func set_refresh_info(allowance: int) -> void:
_refresh_allowance = maxi(0, int(allowance))
_refresh_btn.visible = _refresh_allowance > 0
_refresh_l.text = "可刷新 %d 次(装配刷新件 X 优先消耗,Y 为冒险池)" % _refresh_allowance if _refresh_allowance > 0 else ""
func _emit_refresh_requested() -> void:
refresh_requested.emit()
func _select_card(id: String) -> void: func _select_card(id: String) -> void:
_selected = id _selected = id
for i in _choice_box.get_child_count(): for i in _choice_box.get_child_count():
+71 -3
View File
@@ -13,6 +13,7 @@ extends Control
signal buy_requested(attr: String) signal buy_requested(attr: String)
signal closed signal closed
signal attune_toggled(item_id: String) # 同调装配切换意图(迭代 11 第①批 T4——写口唯一归 MetaProgression.toggle_attunement)
const ATTR_LABELS := {"atk": "攻击", "max_hp": "生命上限", "armor_class": "护甲"} # 档位文案轴(数据契约 attr 词表,与 ShopOverlay 同款) const ATTR_LABELS := {"atk": "攻击", "max_hp": "生命上限", "armor_class": "护甲"} # 档位文案轴(数据契约 attr 词表,与 ShopOverlay 同款)
# 方向稿取色(4-camp.png——柜台头带深棕/行板暖米;双色墨=亮板配深墨)——值单点收 UiKit(T4 裸色收口批) # 方向稿取色(4-camp.png——柜台头带深棕/行板暖米;双色墨=亮板配深墨)——值单点收 UiKit(T4 裸色收口批)
@@ -25,6 +26,11 @@ var _forge_holder: VBoxContainer
var _workshop_holder: VBoxContainer var _workshop_holder: VBoxContainer
var _forge_rows: Array[Dictionary] = [] # [{attr, level_l, price_l, btn}]——refresh_camp 就地重刷(零重建回流) var _forge_rows: Array[Dictionary] = [] # [{attr, level_l, price_l, btn}]——refresh_camp 就地重刷(零重建回流)
var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l}] var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l}]
var _attune_header_l: Label # 同调装配段(迭代 11 第①批 T4 工坊扩容——Slots 点亮呈现)
var _attune_btns: Array[Dictionary] = [] # [{item_id, btn}]——装配/卸下切换钮(满位禁选随刷)
var _attune_nodes: Array[Control] = [] # 段内节点册(open_camp 重建时定向清理,不扰工坊六行)
var _refresh_l: Label # Y 刷新升级行(P1 甲——每冒险开局给 Y 次、冒险周期池)
var _refresh_btn: Button
var _recent_list: Label # 最近冒险(逐行拼接 label——只读账面,随 open_camp 重建) var _recent_list: Label # 最近冒险(逐行拼接 label——只读账面,随 open_camp 重建)
@@ -91,9 +97,15 @@ func _init() -> void:
_forge_holder = VBoxContainer.new() _forge_holder = VBoxContainer.new()
_forge_holder.add_theme_constant_override("separation", 6) _forge_holder.add_theme_constant_override("separation", 6)
mid.add_child(_counter_panel("属性锻造", UiKit.TEX_ICON_FORGE, _forge_holder, 356)) mid.add_child(_counter_panel("属性锻造", UiKit.TEX_ICON_FORGE, _forge_holder, 356))
# 迭代 11 第①批 T4 工坊扩容(P2 甲程序占位——同调装配段+Y 升级行入工坊柜台,行板复用;行数增长以
# 柜台内滚承载,冻结三柜台版式与另两柜台零触碰,视觉终稿归营地批)
var ws_scroll := ScrollContainer.new()
ws_scroll.custom_minimum_size = Vector2(340, 336)
ws_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
_workshop_holder = VBoxContainer.new() _workshop_holder = VBoxContainer.new()
_workshop_holder.add_theme_constant_override("separation", 6) _workshop_holder.add_theme_constant_override("separation", 6)
mid.add_child(_counter_panel("道具工坊", UiKit.TEX_ICON_WORKSHOP, _workshop_holder, 356)) ws_scroll.add_child(_workshop_holder)
mid.add_child(_counter_panel("道具工坊", UiKit.TEX_ICON_WORKSHOP, ws_scroll, 356))
var atlas_holder := VBoxContainer.new() var atlas_holder := VBoxContainer.new()
mid.add_child(_counter_panel("奖励格图鉴", UiKit.TEX_ICON_ATLAS, atlas_holder, 356)) mid.add_child(_counter_panel("奖励格图鉴", UiKit.TEX_ICON_ATLAS, atlas_holder, 356))
var lock_l := UiKit.label(" —— 柜台结构位预留:内容排 Alpha(地图战斗资源差异化批)", 13, false, UiKit.STONE) var lock_l := UiKit.label(" —— 柜台结构位预留:内容排 Alpha(地图战斗资源差异化批)", 13, false, UiKit.STONE)
@@ -121,7 +133,7 @@ func _init() -> void:
# 柜台列面板(方向稿同构):PanelContainer 米板身 + 头带深棕(图标+标题暖白) # 柜台列面板(方向稿同构):PanelContainer 米板身 + 头带深棕(图标+标题暖白)
func _counter_panel(title_text: String, icon_path: String, holder: VBoxContainer, min_w: int) -> PanelContainer: func _counter_panel(title_text: String, icon_path: String, holder: Control, min_w: int) -> PanelContainer:
var panel := PanelContainer.new() var panel := PanelContainer.new()
panel.custom_minimum_size = Vector2(min_w, 0) panel.custom_minimum_size = Vector2(min_w, 0)
panel.add_theme_stylebox_override("panel", panel.add_theme_stylebox_override("panel",
@@ -147,10 +159,11 @@ func _counter_panel(title_text: String, icon_path: String, holder: VBoxContainer
# ---- 柜台行构建(open_camp 重建;行结构仿 ShopOverlay 商品卡——亮板双色墨制)---- # ---- 柜台行构建(open_camp 重建;行结构仿 ShopOverlay 商品卡——亮板双色墨制)----
# 打开营地(全量重建三柜台行 + 最近冒险——数据 = 组合根 _camp_data() 投影) # 打开营地(全量重建三柜台行 + 同调装配段 + 最近冒险——数据 = 组合根 _camp_data() 投影)
func open_camp(data: Dictionary) -> void: func open_camp(data: Dictionary) -> void:
_rebuild_forge(data) _rebuild_forge(data)
_rebuild_workshop(data) _rebuild_workshop(data)
_rebuild_attune(data)
_rebuild_recent(data) _rebuild_recent(data)
refresh_camp(data) refresh_camp(data)
visible = true visible = true
@@ -196,6 +209,24 @@ func refresh_camp(data: Dictionary) -> void:
st_col = UiKit.GOLD_DEEP st_col = UiKit.GOLD_DEEP
card.status_l.text = st_text card.status_l.text = st_text
card.status_l.add_theme_color_override("font_color", st_col) card.status_l.add_theme_color_override("font_color", st_col)
# 同调装配段状态就地刷(迭代 11 第①批 T4——Slots 点亮/满位禁选/Y 升级行余额联动)
var att: Dictionary = data.get("attune", {})
if _attune_header_l != null:
var slots_n := int(att.get("slots", 0))
var eq: Array = att.get("equipped", [])
_attune_header_l.text = "── 同调装配 Slots %d/%d ──" % [eq.size(), slots_n]
var eq_set := {}
for v in eq:
eq_set[String(v)] = true
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 # 满位禁选
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))]
_refresh_btn.text = "已满级" if bool(rf.get("maxed", false)) else "升级 %d 钥石" % int(rf.get("price", 0))
_refresh_btn.disabled = bool(rf.get("maxed", false)) or balance < int(rf.get("price", 0))
func _rebuild_forge(data: Dictionary) -> void: func _rebuild_forge(data: Dictionary) -> void:
@@ -252,6 +283,43 @@ func _rebuild_workshop(data: Dictionary) -> void:
_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})
# 同调装配段重建(迭代 11 第①批 T4——工坊柜台尾部追加;Slots 点亮+九件行板+Y 升级行,程序占位面)
func _rebuild_attune(data: Dictionary) -> void:
for n in _attune_nodes:
n.queue_free()
_attune_nodes.clear()
_attune_btns.clear()
var att: Dictionary = data.get("attune", {})
_attune_header_l = UiKit.label("", 13, true, UiKit.INK)
_workshop_holder.add_child(_attune_header_l)
_attune_nodes.append(_attune_header_l)
for row in att.get("items", []):
var iid := String(row.get("item_id", ""))
var line := HBoxContainer.new()
line.add_theme_constant_override("separation", 6)
var name_l := UiKit.label(String(row.get("display_name", "")), 13, true, UiKit.INK)
name_l.custom_minimum_size = Vector2(96, 0)
line.add_child(name_l)
var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能", 12, false, UiKit.STONE)
form_l.custom_minimum_size = Vector2(58, 0)
line.add_child(form_l)
var btn := UiKit.button("装配")
btn.pressed.connect(attune_toggled.emit.bind(iid))
line.add_child(btn)
_workshop_holder.add_child(line)
_attune_nodes.append(line)
_attune_btns.append({"item_id": iid, "btn": btn})
var refresh_line := HBoxContainer.new()
refresh_line.add_theme_constant_override("separation", 6)
_refresh_l = UiKit.label("", 12, false, UiKit.STONE)
refresh_line.add_child(_refresh_l)
_refresh_btn = UiKit.button("升级")
_refresh_btn.pressed.connect(_emit_buy_requested.bind("attune_refresh"))
refresh_line.add_child(_refresh_btn)
_workshop_holder.add_child(refresh_line)
_attune_nodes.append(refresh_line)
func _rebuild_recent(data: Dictionary) -> void: func _rebuild_recent(data: Dictionary) -> void:
var lines: Array[String] = [] var lines: Array[String] = []
var recent: Array = data.get("recent", []) var recent: Array = data.get("recent", [])
+32 -1
View File
@@ -34,11 +34,41 @@ func _initialize() -> void:
var route := "min" var route := "min"
if args.size() >= 4 and args[3] in ["min", "max"]: if args.size() >= 4 and args[3] in ["min", "max"]:
route = args[3] route = args[3]
# 同调分层开关(迭代 11 第①批 T8——off=现行基线零装配零扰动;on=贪心装配画像,确定性同种子可复放)
var attune_mode := "off"
if args.size() >= 5 and args[4] in ["off", "on"]:
attune_mode = args[4]
var db = load("res://core/config_db.gd").new() var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false): if not db.load_tables("res://data/tables", false):
print("表加载失败") print("表加载失败")
quit(1) quit(1)
return return
var attune_profile := {}
if attune_mode == "on":
# 贪心画像:中间进度计数(通关 3/Boss 击杀 6)经谓词表求值 Slots;装配序 = 表序 passive 优先至
# Slots-1 + 首个 active(slots=3 → [淬毒刃纹, 标中殉爆, 掘进蓄能],provisional 画像归调参批);
# Y = 营地升级 Lv1 表值 1。autoplay 默认不刷——Y 仅注入容量,刷新消费归玩家策略面(钓鱼归玩家)。
var slots: int = db.attunement_growth.slots({"clear_count": 3, "boss_kill_count": 6})
var passives: Array = []
var actives: Array = []
for r in db.aux_attunement_rows():
if String(r.get("form", "")) == "passive":
passives.append(String(r.get("item_id", "")))
elif String(r.get("form", "")) == "active_charged":
actives.append(String(r.get("item_id", "")))
var equipped: Array = []
for p in passives:
if equipped.size() >= maxi(0, slots - 1):
break
equipped.append(p)
if slots > equipped.size() and not actives.is_empty():
equipped.append(actives[0])
for p in passives:
if equipped.size() >= slots:
break
if not equipped.has(p):
equipped.append(p)
attune_profile = {"equipped": equipped, "refresh_y": 1}
var autoplay = load("res://smoke/level_autoplay.gd") var autoplay = load("res://smoke/level_autoplay.gd")
var t0 := Time.get_ticks_msec() var t0 := Time.get_ticks_msec()
print("=== 冒险全流程扫描:三关连探 · N=%d/策略 · 种子 %d 起逐局独立 · 策略=%s · atk+0%%/hp+0%% 零加成 · route=%s ===" % [n, seed0, policy_arg, route]) print("=== 冒险全流程扫描:三关连探 · N=%d/策略 · 种子 %d 起逐局独立 · 策略=%s · atk+0%%/hp+0%% 零加成 · route=%s ===" % [n, seed0, policy_arg, route])
@@ -51,10 +81,11 @@ func _initialize() -> void:
for i in range(n): for i in range(n):
var seed_val := seed0 + _run_cursor var seed_val := seed0 + _run_cursor
_run_cursor += 1 _run_cursor += 1
var st = autoplay.run_adventure(db, seed_val, 0, 0, false, logger, policy, route) var st = autoplay.run_adventure(db, seed_val, 0, 0, false, logger, policy, route, {}, attune_profile)
var sum: Dictionary = st["summary"] var sum: Dictionary = st["summary"]
var rec := { var rec := {
"policy": policy, "seed": seed_val, "atk_pct": 0, "hp_pct": 0, "route": route, "policy": policy, "seed": seed_val, "atk_pct": 0, "hp_pct": 0, "route": route,
"attune": attune_mode, "equipped": attune_profile.get("equipped", []),
"result": st["result"], "result": st["result"],
"cleared": bool(sum.get("cleared", false)), "cleared": bool(sum.get("cleared", false)),
"levels_cleared": int(sum.get("levels_cleared", -1)), "levels_cleared": int(sum.get("levels_cleared", -1)),
+7 -2
View File
@@ -114,7 +114,7 @@ static func _drive_level(runner, st: Dictionary, logger, policy: String, route:
# (last_battle_enemies——末场战斗敌方显示名序列,失败局即死亡遭遇)。 # (last_battle_enemies——末场战斗敌方显示名序列,失败局即死亡遭遇)。
static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_rows: bool = false, static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_rows: bool = false,
logger = null, policy: String = "normal", route: String = "min", logger = null, policy: String = "normal", route: String = "min",
hero_override: Dictionary = {}) -> Dictionary: hero_override: Dictionary = {}, attune: Dictionary = {}) -> Dictionary:
var own_session := logger == null var own_session := logger == null
if own_session: if own_session:
logger = load("res://presentation/playtest_logger.gd").new() logger = load("res://presentation/playtest_logger.gd").new()
@@ -149,7 +149,12 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
adr.aux_replace_presented.connect(_on_adventure_aux_replace.bind(st, adr)) adr.aux_replace_presented.connect(_on_adventure_aux_replace.bind(st, adr))
adr.adventure_finished.connect(_on_adventure_finished.bind(st, logger)) adr.adventure_finished.connect(_on_adventure_finished.bind(st, logger))
adr.set_hero_override(atk_pct, hp_pct) adr.set_hero_override(atk_pct, hp_pct)
if not adr.start_adventure(seed_val, {"hero_override": hero_override, "first_script": false}): # 测量夹具恒抽签(v0.4——首次脚本分支不进任何读数) # 同调分层注入(迭代 11 第①批 T8——缺省 {} 与既有调用方逐位一致零行为变更)
var adv_params := {"hero_override": hero_override, "first_script": false}
if not attune.is_empty():
adv_params["attunement"] = attune.get("equipped", [])
adv_params["attune_refresh_y"] = int(attune.get("refresh_y", 0))
if not adr.start_adventure(seed_val, adv_params): # 测量夹具恒抽签(v0.4——首次脚本分支不进任何读数)
st.result = "start_failed" st.result = "start_failed"
if own_session: if own_session:
logger.close_session() logger.close_session()
+69 -28
View File
@@ -210,11 +210,11 @@ func _smoke_adventure_table() -> void:
check(int(_db.adventure.bonus_cap_ap) == 2 and int(_db.adventure.bonus_cap_attack) == int(_db.adventure.bonus_cap_max_hp) check(int(_db.adventure.bonus_cap_ap) == 2 and int(_db.adventure.bonus_cap_attack) == int(_db.adventure.bonus_cap_max_hp)
and int(_db.adventure.bonus_cap_max_hp) == int(_db.adventure.bonus_cap_armor), and int(_db.adventure.bonus_cap_max_hp) == int(_db.adventure.bonus_cap_armor),
"次数上限口径:AP 件=%d(全冒险最紧)· 属性件=%d(三件同)" % [int(_db.adventure.bonus_cap_ap), int(_db.adventure.bonus_cap_attack)]) "次数上限口径:AP 件=%d(全冒险最紧)· 属性件=%d(三件同)" % [int(_db.adventure.bonus_cap_ap), int(_db.adventure.bonus_cap_attack)])
check(int(_db.adventure.bonus_cap("gp")) == _db.adventure.BONUS_CAP_UNLIMITED check(int(_db.adventure.bonus_cap("gp")) == _db.adventure.BONUS_CAP_UNLIMITED,
and int(_db.adventure.bonus_cap("aux_item")) == _db.adventure.BONUS_CAP_UNLIMITED, "GP 件无次数上限(牌库恒非空的结构保证;aux_item 池种已随同调批 T7 除名)")
"GP/道具件无次数上限(牌库恒非空的结构保证)") check(_db.attunement_growth != null and int(_db.attunement_slot_cap()) == 3
check(int(_db.adventure.transitional_sync_slots) >= 0 and int(_db.adventure.transitional_sync_slots) <= 9, and Attunement.slots(_db, null) == 0,
"transitional_sync_slots ∈ [0,9](%d——同调位过渡读数源)" % int(_db.adventure.transitional_sync_slots)) "同调位读数源=attunement_growth 谓词表(slot_cap 3;未注入档求值 0——transitional_sync_slots 过渡键已退役,迭代 11 第①批 T7)")
# ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 入口停点 / 放弃两态) # ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 入口停点 / 放弃两态)
@@ -527,23 +527,15 @@ func _smoke_adventure() -> void:
var draw: Array = a11._draw_bonus_candidates() var draw: Array = a11._draw_bonus_candidates()
check(not (draw as Array).has("attack") and not (draw as Array).has("ap"), check(not (draw as Array).has("attack") and not (draw as Array).has("ap"),
"达上限件剔牌库(attack 计满 %d / ap 计满 %d 不出现在候选——%s)" % [int(_db.adventure.bonus_cap_attack), int(_db.adventure.bonus_cap_ap), str(draw)]) "达上限件剔牌库(attack 计满 %d / ap 计满 %d 不出现在候选——%s)" % [int(_db.adventure.bonus_cap_attack), int(_db.adventure.bonus_cap_ap), str(draw)])
check((draw as Array).size() >= 2, "剔牌库后候选恒 ≥2(GP/道具件无次数上限——防「三选项全不可选」死角)") check((draw as Array).size() >= 2, "剔牌库后候选恒 ≥2(GP 件无次数上限——防「三选项全不可选」死角)")
# 同调位替换·丢旧收新路(slots 过渡键=1,carry 满 1 件再取道具件 → 满位转替换) # 池收缩(迭代 11 同调批 T7——aux 卡退役出三选一牌库:重复抽样零 aux 候选;同调位替换两路
var slots := int(_db.adventure.transitional_sync_slots) # white-box 测试随停点渠道退役拆除——机械惰性承载,零投放零行为)
a11._bonus_pending = {"candidates": ["aux_item", "gp", "attack"], "payload": {}} var aux_seen := false
a11._bonus_aux_carry.assign(["probe_dust"]) for i in 40:
check(a11.pick_bonus("aux_item") and a11._bonus_pending.has("incoming"), if (a11._draw_bonus_candidates() as Array).has("aux_item"):
"满位道具件转替换子交互(持有 %d/%d)" % [a11._bonus_aux_carry.size(), slots]) aux_seen = true
check(a11.resolve_aux_replace(0) and (a11._bonus_aux_carry as Array).has("blast_kit") \ break
and (a11._bonus_aux_carry as Array).size() <= slots, check(not aux_seen, "池收缩——40 次重抽零 aux 候选(停点渠道 aux 卡退役,迭代 11 第①批 T7)")
"替换受理·丢旧收新(终态 ≤ 同调位 %d——%s)" % [slots, str(a11._bonus_aux_carry)])
# 同调位替换·放弃新件路(原持有集不动、新件不获取)
a11._bonus_pending = {"candidates": ["aux_item"], "payload": {}}
a11._bonus_result = {}
a11._bonus_aux_carry.assign(["probe_dust"])
check(a11.pick_bonus("aux_item"), "替换场景二:满位再转替换子交互")
check(a11.resolve_aux_replace(-1) and a11._bonus_aux_carry.size() == 1 and String(a11._bonus_aux_carry[0]) == "probe_dust",
"替换受理·放弃新件(原持有集不动、新件不获取——两路终态恒 ≤ 同调位)")
# 满血回 0 合法(通关回血封顶口径) # 满血回 0 合法(通关回血封顶口径)
var a12 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(12)) var a12 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(12))
a12.start_adventure(12, {"first_script": true}) a12.start_adventure(12, {"first_script": true})
@@ -3456,7 +3448,8 @@ func _smoke_meta() -> void:
# 冒烟隔离存档名册(段首清陈旧 + 段尾清理——契约:测试不污染 user://save.json 真实档) # 冒烟隔离存档名册(段首清陈旧 + 段尾清理——契约:测试不污染 user://save.json 真实档)
const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry.json", const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry.json",
"user://smoke_meta_settle.json", "user://smoke_meta_workshop.json", "user://smoke_meta_settle.json", "user://smoke_meta_workshop.json",
"user://smoke_meta_cap.json", "user://smoke_meta_white.json"] "user://smoke_meta_cap.json", "user://smoke_meta_white.json",
"user://smoke_meta_attune.json", "user://smoke_meta_attune2.json"]
func _meta_smoke_cleanup() -> void: func _meta_smoke_cleanup() -> void:
@@ -3632,6 +3625,54 @@ func _meta_settle_and_workshop() -> void:
"stopped_at": 1, "keystone_total": 0, "master_seed": i}) "stopped_at": 1, "keystone_total": 0, "master_seed": i})
check(sv_c.recent_summaries().size() == 10 and int(sv_c.recent_summaries()[0].get("master_seed", -1)) == 11, check(sv_c.recent_summaries().size() == 10 and int(sv_c.recent_summaries()[0].get("master_seed", -1)) == 11,
"摘要 cap 10 且最新在前(12 连 settle 截断后 seed 11 居首)") "摘要 cap 10 且最新在前(12 连 settle 截断后 seed 11 居首)")
# ⑪ 同调位账本(迭代 11 第①批 T4——装配持久/满位禁选/Y 升级语义;写口 = MetaProgression 同调段)
var sv_a = _meta_new_save(_META_SMOKE_PATHS[6])
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("venom_edge") == false,
"满位禁选(Slots 0 时装配拒绝)")
sv_a.add_keystone(200)
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 次刷新)")
mp_a.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": 0, "master_seed": 21})
check(Attunement.slots(_db, sv_a) == 1 and sv_a.boss_kill_count() == 1,
"通关 1 关 → m1 达成 Slots=1 且 Boss 击杀总数=1(计数源收束入账)")
check(mp_a.toggle_attunement("venom_edge") and mp_a.toggle_attunement("flag_burst") == false,
"装配持久写入 + 满位禁选(Slots 1 装第二件拒绝)")
check(mp_a.toggle_attunement("venom_edge") and mp_a.toggle_attunement("flag_burst"),
"卸下恒可再装配(换装语义——装配序数组承载)")
var sv_a2 = _meta_new_save(_META_SMOKE_PATHS[7])
sv_a2.set_attunement_equipped(["luck_coin"])
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 后投影一致)")
# ⑫ 同调批 T8 断言面:clamp / 充能重置 / 刷新双计数器(white-box——迭代 11 第①批)
var tg := AttunementGrowthTable.new()
tg.slot_cap = 1
tg.milestones = [{"id": "x1", "grant": 1, "requires": [{"key": "clear_count", "threshold": 1}]},
{"id": "x2", "grant": 1, "requires": [{"key": "boss_kill_count", "threshold": 1}]}]
check(tg.slots({"clear_count": 5, "boss_kill_count": 5}) == 1,
"同调位 clamp(Σ达成 grant 2 → slots 1 = slot_cap——谓词表口径)")
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"]
a12._attune_refresh_x = 1
a12._attune_refresh_y = 2
a12._bonus_pending = {"candidates": ["gp", "attack", "max_hp"], "payload": {}}
check(a12.refresh_bonus() and a12._attune_refresh_x == 0 and a12._attune_refresh_y == 2,
"刷新双计数器——X 优先消耗(x1y2 → x0y2)")
check(a12.refresh_bonus() and a12.refresh_bonus() and a12._attune_refresh_y == 0
and a12.refresh_bonus() == false,
"刷新双计数器——X 耗尽转 Y 池、池竭拒绝(x0y2 → y0 → 第四次 false 零副作用)")
a12._reset_refresh_x()
check(a12.attune_refresh_left() == 1,
"刷新 X 每碰三选一重置(不结转——重碰面 x 回表值 1)")
_meta_smoke_cleanup() _meta_smoke_cleanup()
@@ -3771,8 +3812,8 @@ func _smoke_save() -> void:
fm.store_string(JSON.stringify({"version": 0, "keystone_old": 12})) fm.store_string(JSON.stringify({"version": 0, "keystone_old": 12}))
fm.close() fm.close()
check(f7.load_from_disk() and f7.keystone_balance() == 12, "v0 旧档经迁移链入账(keystone_old 12 → 余额 12)") check(f7.load_from_disk() and f7.keystone_balance() == 12, "v0 旧档经迁移链入账(keystone_old 12 → 余额 12)")
check(f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 3, check(f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 4,
"迁移链 0→1(测试钩)→3(生产) 写盘 version == 3(v3=断点附随件扩列,迭代 10 奖励重构)") "迁移链 0→1(测试钩)→4(生产) 写盘 version == 4(v4=同调批扩域,迭代 11 第①批)")
# ⑧ 域界干净:meta 永久进度域不含局内资源键(行动点/RAP/GP/道具库存/生命账本不入档——契约 2; # ⑧ 域界干净:meta 永久进度域不含局内资源键(行动点/RAP/GP/道具库存/生命账本不入档——契约 2;
# 迭代 8 卡①修订:扫描收窄到 meta 域——断点域=冒险进行中瞬态快照,生命/GP 按拍板面入域豁免) # 迭代 8 卡①修订:扫描收窄到 meta 域——断点域=冒险进行中瞬态快照,生命/GP 按拍板面入域豁免)
@@ -3868,9 +3909,9 @@ func _smoke_save() -> void:
fold.store_string("{\"version\": 1, \"meta\": {\"keystone_balance\": 4}}") fold.store_string("{\"version\": 1, \"meta\": {\"keystone_balance\": 4}}")
fold.close() fold.close()
check(fo.load_from_disk() and fo.keystone_balance() == 4 and not fo.has_breakpoint(), check(fo.load_from_disk() and fo.keystone_balance() == 4 and not fo.has_breakpoint(),
"v1 旧档迁移 v3:断点域补空(合法空态,无断点域不炸)") "v1 旧档迁移 v4:断点域补空(合法空态,无断点域不炸)")
check(fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 3, check(fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 4,
"v1 旧档迁移后写盘 version == 3") "v1 旧档迁移后写盘 version == 4(v1→v4 链式——同调批扩域 2026-10-03)")
# ⑫ v2 旧档→v3 兼容(迭代 10 g 裁——semver 向后兼容:旧断点零丢失、新代码可续玩) # ⑫ v2 旧档→v3 兼容(迭代 10 g 裁——semver 向后兼容:旧断点零丢失、新代码可续玩)
var fv2 = sm_script.new(paths[10]) var fv2 = sm_script.new(paths[10])
+1
View File
@@ -0,0 +1 @@
uid://7nn2b0crxvas