diff --git a/design/gdd/05-不做清单.md b/design/gdd/05-不做清单.md index e9bcd6f5..77894f09 100644 --- a/design/gdd/05-不做清单.md +++ b/design/gdd/05-不做清单.md @@ -33,3 +33,4 @@ | v1.0 | 2026-09-14 | 初版随 GDD 骨架建立:确定不做 / 现阶段不做两节 | | v1.1 | 2026-09-21 | 元进度实现批:元进度行注首批已落地(营地三柜台 / 钥石账本 / 跨局注入),数值 provisional 归设计批标定 | | v1.2 | 2026-10-03 | R3 清账批(gdd_lint 分域随批):元进度行日期批注迁出至本表 v1.1 行;语义零变更 | +| v1.3 | 2026-10-03 | 道具替代推理边界锚复核(迭代 11 第①批同调实施批——任务书 §十连带):「允许恢复推理原料,不允许道具替代推理」边界锚**原样保留即通过**——同调目录九件逐件对表:真视/预读类=信息给付(探巢摆钟 3×3 窄域护栏承重),六被动=机制修正(禁裸 flat schema 强制),无一替代推理判定本体;复核零改动 | diff --git a/design/gdd/06-待办清单.md b/design/gdd/06-待办清单.md index 701505bd..6494028b 100644 --- a/design/gdd/06-待办清单.md +++ b/design/gdd/06-待办清单.md @@ -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 奖励重构批(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 批随批) diff --git a/design/gdd/systems/冒险循环/关卡节点地图.md b/design/gdd/systems/冒险循环/关卡节点地图.md index a2726e20..b0b8535d 100644 --- a/design/gdd/systems/冒险循环/关卡节点地图.md +++ b/design/gdd/systems/冒险循环/关卡节点地图.md @@ -189,3 +189,4 @@ Boss 兑付(胜利或英雄死亡)触发关卡收束点,此后不再有选 | 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.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 五种)+刷新,零结构改动(节点类型词表/图合法性六检零新增) | diff --git a/design/gdd/systems/战斗循环/地图战斗资源.md b/design/gdd/systems/战斗循环/地图战斗资源.md index b3ca1b94..305791db 100644 --- a/design/gdd/systems/战斗循环/地图战斗资源.md +++ b/design/gdd/systems/战斗循环/地图战斗资源.md @@ -247,3 +247,4 @@ | 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.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 浮动(逐行归因不受影响——抽签按调整后池权重消费同一取号) | diff --git a/design/gdd/systems/排雷循环/行动经济.md b/design/gdd/systems/排雷循环/行动经济.md index 6769efc1..f5889cdb 100644 --- a/design/gdd/systems/排雷循环/行动经济.md +++ b/design/gdd/systems/排雷循环/行动经济.md @@ -233,3 +233,4 @@ | 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.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)同引用直改 | diff --git a/design/gdd/systems/排雷循环/魔法道具.md b/design/gdd/systems/排雷循环/魔法道具.md index 25270ff7..8bb5eba5 100644 --- a/design/gdd/systems/排雷循环/魔法道具.md +++ b/design/gdd/systems/排雷循环/魔法道具.md @@ -1,18 +1,26 @@ # 魔法道具 -> 一句话:扫雷侧魔法道具(显影、探测类) -> 状态:已定稿(自旧体系迁移) -> 形态沿革:现行正文=切片期过渡形态;终局=局外同调件(2026-09-30 用户裁决甲)——见下节「定位与形态沿革」 +> 一句话:扫雷侧魔法道具(同调机制件——营地装配、机制轴唯一来源) +> 状态:已定稿(2026-10-03 迭代 11 第①批同调实施批——终局形态落籍,过渡形态退役) +> 形态沿革:~~过渡形态~~已退役(2026-10-03);现行=局外同调件——见下节「定位与形态沿革」 > 旧版参考:../../../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 落籍)=切片期过渡形态**:道具作为掉落奖励类别进入掉落池(现行投放三件:工兵令/探巢摆钟/爆破工兵包)、奖励节点三选一构筑点取用、关卡域库存、挖掘期两步使用——即本文以下全部正文章节所述形态。 -- **终局=局外同调件**:魔法道具重定义为「开局前在营地装配、占同调位、带每冒险充能的机制增强件」。设计权威底稿 = [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)。 +- ~~**过渡形态**(2026-09-29 迭代 7 落籍)~~ **已退役**:掉落投放(2026-10-01 已随「挖格不掉道具」退役)/奖励节点三选一道具卡(2026-10-03 T7 出池)/关卡域库存/挖掘期两步使用——本文以下正文章节「掉落池获取」「关卡域库存」「什么时候怎么用」诸节随之整体退役,保留为历史口径(本节横幅为准)。 +- **终局形态(现行)= 局外同调件**:开局前在营地装配(营地·道具工坊柜台「同调装配」段)、占同调位(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,报告已退役)。 diff --git a/design/gdd/systems/跨局循环/元进度.md b/design/gdd/systems/跨局循环/元进度.md index 357ac9ac..75b3d16f 100644 --- a/design/gdd/systems/跨局循环/元进度.md +++ b/design/gdd/systems/跨局循环/元进度.md @@ -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.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.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) | diff --git a/design/gdd/ux/node-reward.md b/design/gdd/ux/node-reward.md index 2a4f65f6..dee9df7c 100644 --- a/design/gdd/ux/node-reward.md +++ b/design/gdd/ux/node-reward.md @@ -1,27 +1,26 @@ # 节点奖励屏 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) -> 相邻:[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) 共用组件——零新增交互原语)。 - 头部行:「关卡奖励点 / 从提升池抽三选一(与通关三选一同池同权重同上限):选择一件带走:」(Narrator.node_bonus_header)。 - 三卡点选(✓ 高亮,确认前可改选)→「确认选择」→ 兑付反馈(选中件效果 + 丢弃提示)→「继续」→ 回图选路(零自动推进同款)。 -- **魔法道具件直显具体道具**(掷定前置,v2.1):候选抽定时即掷定具体道具,卡片显示「魔法道具 / 探巢摆钟 · 真视 3×3」——不再是「随机获得一件」盲选。 -- 魔法道具件满位:替换子面板(现有库存件 + 新件 N+1 选 N,丢旧收新 / 放弃新件两路——与通关面同构;丢弃目标 = 关卡域库存件句柄)。 +- ~~魔法道具件直显/满位替换~~(v2.1 两面随池种退役——迭代 11 第①批 T7;魔法道具改走营地同调装配面,见[魔法道具](../systems/排雷循环/魔法道具.md))。 +- **三选一刷新**(v2.2 新增——P1 甲 X/Y 双源):装配「三选一刷新件」给 X 次(每次碰三选一重给、优先消耗、不结转)+ 营地「刷新升级」给 Y 次(每冒险开局注入冒险周期池、用完即无);刷新钮余量 >0 时现形,点按重抽三候选(reward_pick 既有洗牌流,池规则全保留:不放回/达上限剔牌/掷定前置直显/停点可中断)。autoplay 默认不刷(钓鱼归玩家)。 ## 口径 - **同池同上限**:件获取次数计数为冒险级单例——奖励节点与通关三选一两渠道合计判上限(达上限件在两渠道同样剔牌库);权重与牌库口径读冒险表(registry adventure_table 为准)。 -- 道具件入**本关库存**(即时可用,来源归因 `reward_node`)——区别于通关三选一的跨关携带(清库后授予、下一关生效)。 -- 「池空直通」合法态随共享池退役:池含无次数上限的 GP/道具件,牌库恒非空。 -- 退役件(探宝罗盘)/失义件不入道具件随机池(同调位约束口径不变,归[魔法道具](../systems/排雷循环/魔法道具.md))。 +- ~~道具件入本关库存~~(v2.2 退役——魔法道具局内不掉落机制道具,三轴 doctrine 锁死)。 +- 「池空直通」合法态随共享池退役:池含无次数上限的 GP 件,牌库恒非空。 ## 版本历史 @@ -30,4 +29,5 @@ | v1.0 | 2026-09-29 | 迭代 7 T5 随批出生(三选一构筑点形态——用户拍板②丙) | | 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.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 交互节)零改动 | diff --git a/design/registry/entities.yaml b/design/registry/entities.yaml index 5b20a289..b483f332 100644 --- a/design/registry/entities.yaml +++ b/design/registry/entities.yaml @@ -69,8 +69,8 @@ entities: attributes: path: user://save.json(单档 MVP——多存档槽位 P2 保留) format: JSON 单档(人可读可调试;字典键一律 String) - version: SAVE_FORMAT_VERSION=2(registry schema_version 同源;v2=断点域出生 2026-09-29 迭代 8 卡①,生产迁移链 v1→v2 补空;高版本拒读 + 禁写盘 + 内存默认新档运行;低版本读档时链式迁移 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=冒险结构:七字段拍板面+附随件〔主种子/取数计数器/钥石累计/已到节点序/道具库存〕,空字典=无断点合法态,三结局清除) + 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(元进度)/ 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 工具)不写档 atomic_write: tmp + rename 原子写(写一半断电不留烂档——钥石不丢判据承载) corruption_policy: 坏档改名 .corrupt.bak(已存在覆盖)+ 默认新档继续 + 警告留痕(不静默丢档不 crash) @@ -505,8 +505,29 @@ entities: ux_flag: 有 UI 贡献(库存条/使用交互/叠加层呈现)——Phase 4 前置 /ux-design;asset-spec 候选(art-bible 样张批后) closures: 地图战斗资源 OQ2 魔法道具 侧收口(呈现深度 = 道具驱动预读;未预读格不呈现、自动寻路不做——终态与否 OQ6)· 创意风险 #1 道具侧兑现(plan 维度预读——结构闭环最后一块,呈现归 HUD框架)· 显影语义(原为雾格显影 = fog_reveal)随雾格移除失义、载体待重定(随魔法道具整体重设计归 Alpha,2026-09-20 用户拍板) warnings: 预警①…④ playtest 预注册归 难度曲线与Boss(道具依赖度/预读覆盖率/AP 转存联动/稀有感知) + attunement_refactor: 迭代 11 第①批 同调实施批 T1(2026-10-03)——表扩列 {form ∈ {"", passive, active_charged}(空=切片过渡形态行), charges_per_adventure, slot_cost(同调行恒 1 校验), unlock_requires 谓词行〔空=首批全解锁合法态,底稿 §2.2 用户裁〕, effective_rounds(P4 甲表值化), charge_n} + effect_kinds 扩 {venom_rider, flag_burst, mine_hit_halve, reflect_damage, ap_rounds_front, ap_charge_safe, cell_removal, reward_weight_bias, bonus_refresh}(mine_reveal/ap_grant 语义不变)+ 🔴 零裸 flat 结构护栏 = 行键白名单封闭(KEY_WHITELIST 外键=加载致命——机制轴禁无条件属性加项的结构承载);七件同调行入表(venom_edge 淬毒刃纹/flag_burst 标中殉爆/nest_counter 惊巢反制/dawn_order 工兵晨令〔effective_rounds=3〕/dig_charge 掘进蓄能〔charges=2·charge_n=3〕/luck_coin 偏运硬币/pick_refresh 三选一刷新件〔X=1〕——值全 provisional)+ 取数口径三分(attunement_rows/transitional_rows/active_rows——同调行永不入过渡渠道);nest_pendulum/blast_kit 维持过渡行 T7 原位换装,sapper_order/probe_dust/catacomb_scroll/loot_compass T7 逐件销账(底稿 §3);维度词表收缩形态 = 开放拍板点(任务书 P1-P8 未覆盖,底稿 §1.1 明挂——T10 追认单随批,行标签 provisional) 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 type: table owner: design/gdd/systems/冒险循环/难度曲线与Boss.md diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index e1990cf8..5c545339 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -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 _last_aux_cleared := 0 # additive(迭代 7 T4——最近一次关卡末清空计数,summary 取数面) 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 _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递) # ---- 三选一停点态(迭代 10 奖励重构批)---- @@ -112,7 +116,13 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool: _pending_next_level = false _base_override = _normalize_override(params.get("hero_override", {})) _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,与战斗内同源) state = "running" 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)))) _first_script = false # 续玩模板已随快照强制(已抽模板零重抽)——脚本模式不重放 _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——选择态/已兑付态不重跑关卡;奖励选择态不重抽、已兑付不重付) var bp_pending: Dictionary = bp.get("bonus_pending", {}) 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)) return 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" return _start_current_level() @@ -315,15 +332,11 @@ func _pick_aux_item(count: int) -> bool: push_error("[冒险结构] pick_bonus(aux_item) 拒绝——解锁道具池空集(白板/门控态不入牌库的防御兜底)") return false var hp_before := int(ledger.get("hero_hp", 0)) - if AuxItems.can_accept(picked.size(), _bonus_aux_carry.size(), db): # 同调位守卫单点(2026-10-02) - for iid in picked: - _bonus_aux_carry.append(String(iid)) - _finish_bonus_pick("aux_item", picked.size(), hp_before) - return true - # 满位替换子交互:现有持有件 + 新件 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()}) + # 迭代 11 第①批 T7——同调位守卫随过渡键退役拆除(aux 卡出池后本路径不可达;能力件容量闸 + # 正主 = 营地装配面 Attunement.slots 满位禁选) + for iid in picked: + _bonus_aux_carry.append(String(iid)) + _finish_bonus_pick("aux_item", picked.size(), hp_before) return true @@ -372,6 +385,7 @@ func node_bonus_candidates() -> Dictionary: 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)} @@ -405,18 +419,14 @@ func _apply_node_aux(count: int) -> bool: var records: Array = [] if _runner != null and _runner.aux_inventory != null: records = _runner.aux_inventory.holding_records() - var holding := records.size() + _bonus_aux_carry.size() - if AuxItems.can_accept(picked.size(), holding, db): # 同调位守卫单点(2026-10-02) - if _runner != null: - for iid in picked: - var rec: Dictionary = _runner.grant_aux_item(String(iid), "reward_node") - if rec.is_empty(): - push_warning("[冒险结构] 奖励节点道具授予被拒(防御兜底留痕): %s" % String(iid)) - _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()}) + # 迭代 11 第①批 T7——守卫随过渡键退役拆除(BONUS_IDS 已除名 aux_item,本分支不可达; + # 替换子交互路由随容量闸一并退役——能力件容量闸正主 = 营地装配面满位禁选) + if _runner != null: + for iid in picked: + var rec: Dictionary = _runner.grant_aux_item(String(iid), "reward_node") + if rec.is_empty(): + push_warning("[冒险结构] 奖励节点道具授予被拒(防御兜底留痕): %s" % String(iid)) + _finish_node_bonus("aux_item", picked.size()) return true @@ -436,6 +446,71 @@ func _resolve_node_replace(discard_handle: int) -> bool: 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) func _replace_in_transit(pending: Dictionary) -> bool: 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"])) var out: Array = [] for i in mini(int(db.adventure.bonus_candidate_count), keyed.size()): # 候选数读表(2026-10-02 魔法数结构化——默认 3 = 现行行为) - var cid := String(keyed[i]["id"]) - out.append(cid) - 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)) + out.append(String(keyed[i]["id"])) + # 迭代 11 第①批 T7——aux_item 掷定前置分支随池种退役拆除(BONUS_IDS 已除名,候选恒无 aux 卡) return out @@ -592,11 +663,10 @@ func _start_current_level() -> bool: var ok: bool = _runner.start_level_external(level_index, ledger, bonus, purchased, Callable(self, "apply_rest"), _base_override, true, template_pick, resume_state) if ok: - # 三选一道具件跨关携带注入(迭代 10——上关兑付的道具件进本关库存〔奖励只朝下一战生效〕; - # 注入即移交——carry 清空,本关末随关卡域库存清空) - for iid in _bonus_aux_carry: - _runner.grant_aux_item(iid, "bonus_pick") - _bonus_aux_carry = [] + _runner.set_attunement(_attunement) # 迭代 11 第①批 T5——同调装配下传(enc 携带进战斗结算) + _runner.set_attune_charges(attune_charges) # 迭代 11 第①批 T6——充能账本下传(能力件骑库存面注入) + # 三选一道具件跨关携带注入(迭代 10)——迭代 11 第①批 T7 退役:aux 卡出池后 carry 恒空集, + # 注入面随停点渠道退役拆除(容器字段保留=替换子交互机械惰性承载,零投放零行为) level_started.emit(level_index, String(_runner.level_row.get("display_name", ""))) # 入口停点断点(node_id=-1)与 defer_entry 同态(node_index=-1+单候选)——走入口呈现; # 中途停点(node_id>=0)走停点呈现(restore_aux_items 先于呈现=C 区库存就位)。 @@ -635,6 +705,7 @@ func _on_level_finished(payload: Dictionary) -> void: ledger["hero_hp"] = hp_before + heal # 置位先于 emit(信号同步派发——处理器内立即确认时 _pending_next_level 必须已就位) _pending_next_level = true + _reset_refresh_x() # 迭代 11 第①批 T5b——碰三选一重置 X 余量(不结转) var pick_payload: Dictionary = { "level_index": level_index, "candidates": _draw_bonus_candidates(), @@ -724,6 +795,7 @@ func _snapshot_breakpoint() -> void: }, "gp": int(ledger.get("gp", 0)), "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, "master_seed": master_seed, "rng_counter": rng.counter(), diff --git a/game/core/attunement.gd b/game/core/attunement.gd new file mode 100644 index 00000000..28685dca --- /dev/null +++ b/game/core/attunement.gd @@ -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)) diff --git a/game/core/attunement.gd.uid b/game/core/attunement.gd.uid new file mode 100644 index 00000000..8e2d2b9f --- /dev/null +++ b/game/core/attunement.gd.uid @@ -0,0 +1 @@ +uid://bwccv5x51gs23 diff --git a/game/core/aux_items.gd b/game/core/aux_items.gd index 4323862c..da8454ba 100644 --- a/game/core/aux_items.gd +++ b/game/core/aux_items.gd @@ -73,13 +73,9 @@ func holding_count() -> int: return n -# 同调位守卫谓词(2026-10-02 修复批——四处手抄收口单点:战斗结算授予路由 / 关卡授予 / 三选一双渠道): -# holding = 调用方渠道的当前实际持有数(库存渠道传 holding_count()、通关渠道传跨关携带数、 -# 节点渠道传库存+携带合计);读数源 = db.adventure.transitional_sync_slots(负值钳 0 口径同前)。 -# 返回再入 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)) +# 同调位守卫谓词——迭代 11 第①批 T7 退役(读取源 transitional_sync_slots 过渡键已随表退役; +# 容量闸正主 = 营地装配面 Attunement.slots 满位禁选。函数体保留空壳防四散引用误删遗漏—— +# T8 冒烟「过渡键读取点归零」断言覆盖) # 持有件记录(迭代 10 追加批——奖励节点替换子交互数据源:只列未消耗件,handle 定位丢弃目标) @@ -108,7 +104,7 @@ func _usable_now(item_id: String, board_has_preview_info: Callable) -> bool: if row.is_empty(): return false 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 bool(board_has_preview_info.call()) if board_has_preview_info.is_valid() else true diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index c1a4abf1..0ad0c96d 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -67,6 +67,24 @@ var _refresh_used_this_round := 0 # 每轮刷新计数(Active 相位 var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用) var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换来的图 = true(清图奖励守卫——刷新图不发清图奖励) 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 _perfect_hit_true := 0 # 板级完美排雷计数(install 重置) var _perfect_hit_false := 0 @@ -129,12 +147,23 @@ func begin_battle(enc: Dictionary) -> bool: if not _install_new_board(): return 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 奖励重构批——跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现 # 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后 # 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节) var ap_bonus: int = int(enc.get("hero_ap_bonus", 0)) if ap_bonus > 0: 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(): _fatal("行动经济 首轮 notify_round_start 被拒") return false @@ -305,6 +334,12 @@ func _settle() -> void: _emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(插旗格直接挖开——零惩罚零奖励)", 0, {}, "") continue _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": # 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴), # 标中不发(旗已随挖掘擦除、不入轮末判定基线)。 @@ -360,30 +395,8 @@ func _settle() -> void: # 「动作 × 格型」矩阵的执行面:snap = 缓冲期冻结身份快照(cell_type = 该格装板期摇定的类型身份), # polarity 由动作决定(开安全格/标中旗 = positive,开雷格/标错旗 = negative), # 兑付行 = 奖励字典(类型, 极性)行。返回 true = percent 行实际生效(flat 行恒 false——兑付结果由行协议承载)。 -# ---- 魔法道具(迭代 7 T2/T3——库存授予路由 + use_aux 四步受理编排;结构冻结见魔法道具 GDD)---- - -# 授予路由 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 四步受理编排保留给同调主动件复用——迭代 11 第①批 T7:过渡授予路由 +# _grant_aux 随掉落/停点渠道退役拆除;能力件注入归 LevelRunner.set_attune_charges 骑库存面)---- # 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)) if not bool(v.ok): 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 if kinds.has("mine_reveal"): 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 not action_economy.grant_ap(int(v.magnitude)): return {"ok": false, "reason": "行动点加量被拒(相位防御)"} + if kinds.has("ap_charge_safe"): + # 掘进蓄能兑付(迭代 11 同调批 T6——临时 AP 当轮生效,grant_ap 同 sapper_order 通道) + if not action_economy.grant_ap(int(v.magnitude)): + return {"ok": false, "reason": "行动点加量被拒(相位防御)"} 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} @@ -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: - # 迭代 7 T3——aux 投放行授予路由(魔法道具 GDD:授予路由唯一调用方=战斗结算):正路由(挖开/标中) - # → 道具入关卡域库存(来源可归因);负路由(雷格/标错旗摇中 aux 身份——雷安同池)→ 零兑付 - # 「道具随雷沉没」(雷格奖励沉没既有语义);清图批追加份同路由(清图翻倍语义对 aux 同样成立)。 - if String(snap.get("cell_type", "")) == RewardTable.AUX_ITEM_TYPE: - return _grant_aux(snap, polarity, source_class, cell, ri) + # 迭代 11 第①批 T7——aux 授予分支随掉落/停点渠道退役拆除(reward 词表第十型 aux_item 保留但零投放: + # 无 aux 行入池、BONUS_IDS 词表已除名);剩余类型走极性字典路由。 var row: Dictionary = _reward_economy.polarity_row(String(snap.get("cell_type", "")), polarity) if row.is_empty(): _fatal("极性字典缺行: %s %s" % [String(snap.get("cell_type", "")), polarity]) return false if polarity == "positive": 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) @@ -537,6 +574,11 @@ func _judge_flag(evt: Dictionary, ri: int) -> void: return # 去重跳过(资格以雷格实例为键——排雷玩法 诚实重复上报的对侧);零明细行 _flag_consumed[cell] = true _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) if not (snap is Dictionary): return @@ -747,6 +789,9 @@ func _exit_distribution() -> void: if not _install_new_board(): _fatal("换图编排失败") 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_round_start() _reset_round_facts() diff --git a/game/core/config_db.gd b/game/core/config_db.gd index 0dafbd58..1f5b88b5 100644 --- a/game/core/config_db.gd +++ b/game/core/config_db.gd @@ -25,6 +25,7 @@ const TABLE_MANIFEST := { "meta": "meta.tres", # 元进度实现批 2026-09-21(MetaTable 四行族——schema 与表同批落籍) "aux_items": "aux_items.tres", # 迭代 7 T1 魔法道具内容批(AuxItemTable 六行——定义表 Rule 4 豁免;罗盘 retired/失义件 deferred 标记见 schema 头注) "adventure": "adventure.tres", # 冒险结构实现批 2026-09-21(AdventureTable 建表——大奖励与钥石行值单点) + "attunement_growth": "attunement_growth.tres", # 迭代 11 第①批 同调实施批 T1(AttunementGrowthTable 单行全局——同调位里程碑谓词表;单行先例 Rule 4 豁免) } var action_economy: ActionEconomyTable @@ -45,6 +46,7 @@ var shop: ShopTable = null var meta: MetaTable var aux_items: AuxItemTable # 迭代 7 T1 类型化持用(消费面走 aux_* 访问器) var adventure: AdventureTable +var attunement_growth: AttunementGrowthTable # 迭代 11 第①批 T1 类型化持用(消费面走 attunement_* 访问器) var is_ready := false var _diagnostics: Array[Dictionary] = [] # {severity, category, message} @@ -135,6 +137,7 @@ func load_tables(root: String, collect_only: bool) -> bool: meta = loaded.get("meta") aux_items = loaded.get("aux_items") adventure = loaded.get("adventure") + attunement_growth = loaded.get("attunement_growth") is_ready = 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 {} -# 活行词表(掉落投放 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: var out := {} - for row in aux_active_rows(): + for row in aux_transitional_rows(): out[String(row.get("item_id", ""))] = true 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} · # level_gp_bonus {level_index, bonus} · products {product_id, display_name, attr, flat_value, # base_price, price_increment} · price_level_bonus {level_index, bonus}。 @@ -296,6 +319,14 @@ func meta_conversion_row(attr: String) -> Dictionary: 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 属性名 = 行族名;表缺失/属性缺失 = 空集非错误态) func _meta_rows(family: String) -> Array: if meta == null: diff --git a/game/core/damage_controller.gd b/game/core/damage_controller.gd index 556eea04..43520b5e 100644 --- a/game/core/damage_controller.gd +++ b/game/core/damage_controller.gd @@ -196,7 +196,12 @@ func _resolve_hero_attack(t: Dictionary) -> void: var threshold: int = defender_ac(target) + int(db.settlement.attack_bonus) var crit: bool = bool(roll_read.crit) 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)} payload[atk_type] = rolled # 分量键 = 实际攻击类型(归因字段,单分量模型) 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)) +# 淬毒刃纹 N(迭代 11 同调批 T5——战斗结算 begin_battle 注入;0 = 未装配零扰动) +var attune_venom: int = 0 + # 池内加算 → clamp [-100,+100](池间乘算归 DamageMath.pool_damage 调用侧) func clamped_pool(pool: String) -> int: var total := 0 diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index 9c4713d4..bed23802 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -64,6 +64,8 @@ var _shop_purchased := {} # product_id → 已购档数(现价 var _settlement: BattleOrchestrator = null # 当前战斗编排器(关卡结束置 null) var aux_inventory: AuxItems = null # 魔法道具 关卡域库存(迭代 7 T3——AuxItems;跨战斗延续, 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 载荷) 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)), "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(), + "attunement": _attunement, + "attune_charges": _attune_charges, "node_type": String(node.get("type", ""))}): push_error("[关卡节点地图] 战斗启动失败(depth %s)" % str(node.get("node_depth", -1))) return false @@ -567,15 +571,32 @@ func set_aux_unlocked(ids: Dictionary) -> void: if _settlement != null: _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)。 -# 同调位持有上限(迭代 10 奖励重构批过渡承载):满位拒绝授予(奖励节点渠道过渡态 = 满位候选 -# 由呈现侧过滤不出;此处防御兜底)——替换交互归三选一渠道,全渠道统一收口随同调实施批。 +# 迭代 11 第①批 T7——同调位持有闸随过渡键退役拆除(aux 卡出池后仅剩防御面;能力件容量闸 +# 正主 = 营地装配面 Attunement.slots 满位禁选)。 func grant_aux_item(item_id: String, source: String) -> Dictionary: if aux_inventory == null: 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) diff --git a/game/core/meta_progression.gd b/game/core/meta_progression.gd index 96bc2968..03ac6604 100644 --- a/game/core/meta_progression.gd +++ b/game/core/meta_progression.gd @@ -150,3 +150,65 @@ func hero_override() -> Dictionary: "max_hp": forge_flat("max_hp"), "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() diff --git a/game/core/reward_economy.gd b/game/core/reward_economy.gd index 37052bca..55f53d8c 100644 --- a/game/core/reward_economy.gd +++ b/game/core/reward_economy.gd @@ -59,7 +59,7 @@ func roll_board(eligible: Array) -> bool: _clear_granted = false _clear_snapshot = {} 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 draft := {} @@ -174,6 +174,27 @@ func polarity_row(cell_type: String, polarity: String) -> Dictionary: # ---- 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: var total := _pool_weight(rows) assert(r >= 0 and r < total, "weighted_pick 取号越域(纯函数域守卫)") diff --git a/game/core/save_manager.gd b/game/core/save_manager.gd index e9c91a3f..4b042cad 100644 --- a/game/core/save_manager.gd +++ b/game/core/save_manager.gd @@ -10,6 +10,12 @@ # 域清单与 owner 分工(内容 owner 定口径,本类照单承载、不替任何人定义进度): # keystone_balance / forge_levels / item_unlocks / level_clear_counts / boss_first_kill # —— 内容 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(摘要五件事口径) # breakpoint(冒险断点快照——停点粒度续玩域)—— 内容 owner = 冒险结构.md(七字段拍板面 # +恢复必需附随件:主种子/取数计数器/钥石累计/已到节点序/道具库存);契约 2 域界反面清单 @@ -30,8 +36,7 @@ class_name SaveManager extends RefCounted -const SAVE_FORMAT_VERSION := 3 # registry schema_version:v3=断点附随件扩列+摘要 keystone_drops(M-3 迭代 10 奖励重构; - # 旧档缺键 = 合法空态);v2=断点域出生(M-3 迭代 8 卡①);v1=首版;高版本拒读 owner=存档序列化 +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=存档序列化 const SUMMARY_CAP := 10 # 最近冒险摘要保留条数(最新在前) const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] # 属性锻造三档合法键(元进度.md 属性锻造柜台) @@ -48,6 +53,7 @@ func _init(path: String = "user://save.json") -> void: # 生产迁移链自注册(v2 起——测试钩 register_migration 仍可叠挂更早版本步骤) register_migration(1, _migrate_v1_v2) register_migration(2, _migrate_v2_v3) + register_migration(3, _migrate_v3_v4) # v1→v2:断点域出生(旧档无断点 = 合法空态——「继续上局」隐藏,非损坏非债务) @@ -66,6 +72,14 @@ func _migrate_v2_v3(data: Dictionary) -> Dictionary: 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]: @@ -77,6 +91,9 @@ func _default_data() -> Dictionary[String, Variant]: "item_unlocks": {}, "level_clear_counts": {"1": 0, "2": 0, "3": 0}, "boss_first_kill": {}, + "boss_kill_count": 0, + "attunement_equipped": [], + "attune_refresh_level": 0, "recent_summaries": [], "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)) +# ---- 同调位账本域(迭代 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: return _data["meta"]["recent_summaries"].duplicate(true) # 值拷贝出户(本类契约「禁碰内部结构」——消费方直改不得渗入存档内存态) # 域更新原语(正式冒险收束唯一入账口——组合根经本函数入账,禁直改内部结构): # 钥石入账(summary.keystone_total——迭代 10 起逐杀掉落已在冒险内计入,boss_drops 参数降级为 -# 摘要归档副本不重复入账)+ 逐通关关次 clear_count +1 与 Boss 首杀标记 + 摘要头部插入 -# (cap 10,最新在前,元素附 keystone_drops 副本——复盘可归因)。 +# 摘要归档副本不重复入账)+ 逐通关关次 clear_count +1 与 Boss 首杀标记 + Boss 击杀总数累计 +# (迭代 11 第①批 T2——同调位里程碑计数源)+ 摘要头部插入(cap 10,最新在前,元素附 +# keystone_drops 副本——复盘可归因)。 func apply_adventure_summary(summary: Dictionary, boss_drops: Array = []) -> void: add_keystone(int(summary.get("keystone_total", 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) _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_kill_count"] = boss_kill_count() + cleared # 迭代 11 T2——Boss 恒唯一终点:levels_cleared 即本次冒险 Boss 击杀数(含重复) var entry: Dictionary = summary.duplicate(true) entry["boss_drops"] = boss_drops.duplicate(true) var arr: Array = _data["meta"]["recent_summaries"] diff --git a/game/data/schemas/adventure_table.gd b/game/data/schemas/adventure_table.gd index a0c8ef64..4d6019f3 100644 --- a/game/data/schemas/adventure_table.gd +++ b/game/data/schemas/adventure_table.gd @@ -10,9 +10,10 @@ class_name AdventureTable extends Resource -## 三选一提升池件 id 词表(封闭——断点附随件与牌库剔除态依赖它;扩件走本表+registry 修订) -const BONUS_IDS: Array[String] = ["attack", "max_hp", "armor", "gp", "aux_item", "ap"] -## 无次数上限件的 cap 哨兵值(gp/aux_item——牌库恒非空的结构保证) +## 三选一提升池件 id 词表(封闭——断点附随件与牌库剔除态依赖它;扩件走本表+registry 修订)。 +## aux_item 池种退役(迭代 11 第①批 T7——同调批出池:魔法道具件改走营地同调装配面,不入三选一牌库) +const BONUS_IDS: Array[String] = ["attack", "max_hp", "armor", "gp", "ap"] +## 无次数上限件的 cap 哨兵值(gp——牌库恒非空的结构保证) 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_gp_amount: int = 20 # [0,999] knob——GP 袋定额入钱包 @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)—— @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_armor: 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 件(全冒险最紧上限) # —— 全冒险获取次数上限(达上限件先剔出牌库;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_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: match id: @@ -55,7 +51,6 @@ func bonus_value(id: String) -> int: "armor": return bonus_armor_flat "gp": return bonus_gp_amount "ap": return bonus_ap_flat - "aux_item": return bonus_aux_item_count return 0 @@ -65,12 +60,11 @@ func bonus_weight(id: String) -> int: "max_hp": return bonus_weight_max_hp "armor": return bonus_weight_armor "gp": return bonus_weight_gp - "aux_item": return bonus_weight_aux "ap": return bonus_weight_ap return 0 -## 次数上限(gp/aux_item 返回 BONUS_CAP_UNLIMITED = 无上限,牌库恒非空的结构保证) +## 次数上限(gp 返回 BONUS_CAP_UNLIMITED = 无上限,牌库恒非空的结构保证) func bonus_cap(id: String) -> int: match id: "attack": return bonus_cap_attack @@ -95,10 +89,6 @@ func validate() -> Array: var c := bonus_cap(id) if c < 0 or c > 99: 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(): 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 diff --git a/game/data/schemas/attunement_growth_table.gd b/game/data/schemas/attunement_growth_table.gd new file mode 100644 index 00000000..dd7b7203 --- /dev/null +++ b/game/data/schemas/attunement_growth_table.gd @@ -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) diff --git a/game/data/schemas/attunement_growth_table.gd.uid b/game/data/schemas/attunement_growth_table.gd.uid new file mode 100644 index 00000000..73fbae35 --- /dev/null +++ b/game/data/schemas/attunement_growth_table.gd.uid @@ -0,0 +1 @@ +uid://tfgsbs87xakg diff --git a/game/data/schemas/aux_item_table.gd b/game/data/schemas/aux_item_table.gd index 0a2c4e2d..70112d7b 100644 --- a/game/data/schemas/aux_item_table.gd +++ b/game/data/schemas/aux_item_table.gd @@ -18,15 +18,39 @@ # 真视窄域——effect_kinds 含 mine_reveal 的活行 target 必须 area_3x3(真视一次最多 3×3,禁全图看雷); # board_wide 仅恢复推理原料类——target=board_wide 的活行 effect_kinds 不得含 mine_reveal(现行无此类活行, # 显影载体待 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 extends Resource @export var rows: Array = [] # Array[Dictionary] 键 item_id 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 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: @@ -62,6 +86,13 @@ func validate() -> Array: var phase: String = String(row.get("use_phase", "active_dig")) if not USE_PHASES.has(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: # 退役/失义行:免 effect_kinds 非空与承重护栏校验(登记面保留,不入投放与 F2) continue @@ -82,17 +113,76 @@ func validate() -> Array: # 承重护栏②board_wide 仅恢复推理类:全图域行禁替推理效果 if target == "board_wide" and kind_seen.has("mine_reveal"): errs.append({"category": "load_bearing", "message": "%s board_wide 行 effect_kinds 不得含 mine_reveal(全图域仅恢复推理原料类——Alpha 重定后落位)" % iid}) - # 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)) if mag < 0 or mag > 999: errs.append({"category": "domain", "message": "%s magnitude 越域 [0,999]: %d" % [iid, mag]}) - if kind_seen.has("ap_grant") and mag < 1: - errs.append({"category": "cross_field", "message": "%s ap_grant 行 magnitude 应 ≥1,实得 %d" % [iid, mag]}) - if not kind_seen.has("ap_grant") and mag != 0: - errs.append({"category": "cross_field", "message": "%s 非 ap_grant 活行 magnitude 应 0,实得 %d" % [iid, mag]}) + var mag_consuming := false + for k in kind_seen.keys(): + if MAGNITUDE_KINDS.has(String(k)): + 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 +# 同调行词表(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 覆盖面的唯一取数口径 func active_rows() -> Array: var out: Array = [] diff --git a/game/data/schemas/meta_table.gd b/game/data/schemas/meta_table.gd index 4c32566a..a1c44d52 100644 --- a/game/data/schemas/meta_table.gd +++ b/game/data/schemas/meta_table.gd @@ -12,6 +12,8 @@ # 达标 clear_threshold——2026-09-20 排期裁决;局内效果整体归 Alpha 魔法道具重设计批,本表只有解锁状态面)。 # conversion = 区间平移换算率行(attr 与 forge 词表一致恰三;per_level_shift_pct ∈ [0,100]—— # registry 键 meta_progression_conversion_table;护甲行强条目:验收读数必须按解锁后参数重测)。 +# attune_refresh_upgrade = Y 刷新升级行族(迭代 11 第①批 T4——P1 甲:每冒险开局给 Y 次、冒险周期池、 +# 用完即无;level 连续从 1,等级 L → refresh_charges=Y 表值,钥石直购 keystone_cost;数值全 provisional)。 # 行值全部 provisional 归 元进度设计批配读数(用户拍板骨架先行,先例 = GP+商店房批)。 class_name MetaTable extends Resource @@ -20,6 +22,7 @@ extends Resource @export var boss_keystone: Array[Dictionary] = [] # Array[Dictionary] 键 level_index @export var workshop_items: Array[Dictionary] = [] # Array[Dictionary] 键 item_id @export var conversion: Array[Dictionary] = [] # Array[Dictionary] 键 attr +@export var attune_refresh_upgrade: Array[Dictionary] = [] # Array[Dictionary] 键 level——Y 刷新升级行族(迭代 11 第①批 T4) const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] 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: var errs: Array[Dictionary] = [] - # 行族空护栏(四族皆为 ConfigDB 消费面——缺族即致命) - for family in ["forge_products", "boss_keystone", "workshop_items", "conversion"]: + # 行族空护栏(五族皆为 ConfigDB 消费面——缺族即致命) + for family in ["forge_products", "boss_keystone", "workshop_items", "conversion", "attune_refresh_upgrade"]: if get(family).is_empty(): errs.append({"category": "empty_table", "message": "MetaTable 行族为空: %s" % family}) # forge_products:attr 词表封闭 + 唯一 + 覆盖恰三(注入契约三字段必齐)+ flat/价格域 + 公比域 [100,400] @@ -115,4 +118,22 @@ func validate() -> Array: for conv_key in FORGE_ATTRS: if not seen_conv.has(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 diff --git a/game/data/tables/adventure.tres b/game/data/tables/adventure.tres index dfcd43e5..e8ed4bbb 100644 --- a/game/data/tables/adventure.tres +++ b/game/data/tables/adventure.tres @@ -10,24 +10,24 @@ bonus_max_hp_flat = 5 bonus_armor_flat = 1 bonus_gp_amount = 20 bonus_ap_flat = 1 -bonus_aux_item_count = 1 bonus_weight_attack = 3 bonus_weight_max_hp = 3 bonus_weight_armor = 3 bonus_weight_gp = 3 -bonus_weight_aux = 1 bonus_weight_ap = 1 bonus_cap_attack = 3 bonus_cap_max_hp = 3 bonus_cap_armor = 3 bonus_cap_ap = 2 -transitional_sync_slots = 1 ; 迭代 10 奖励重构批(2026-10-01 扩列——registry adventure_table 条目同批改写): ; 大奖励 = 非末关通关恰一次:部分回血(clear_heal_pct,封顶上限不回满、满血回 0 合法) ; + 提升三选一(池六种加权洗牌不放回 E-S weight-key 掷 3,玩家点选兑付恰一件;消费 reward_pick 封闭流)。 -; 权重:属性/GP 各 3、道具/AP 各 1;次数上限:属性件各 3、AP 件 2(达上限件先剔牌库)——gp/道具无次数上限。 -; 同调位过渡键 transitional_sync_slots=1(道具持有上限;正主 = attunement_growth.tres 谓词表,同调实施批切换)。 +; 权重:属性/GP 各 3、AP 1;次数上限:属性件各 3、AP 件 2(达上限件先剔牌库)——gp 无次数上限。 +; 迭代 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% 系难度上压方向,带重扫随批呈报)。 diff --git a/game/data/tables/attunement_growth.tres b/game/data/tables/attunement_growth.tres new file mode 100644 index 00000000..d1f2896c --- /dev/null +++ b/game/data/tables/attunement_growth.tres @@ -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 +}] +}] diff --git a/game/data/tables/aux_items.tres b/game/data/tables/aux_items.tres index 0680fb0b..1b6b1688 100644 --- a/game/data/tables/aux_items.tres +++ b/game/data/tables/aux_items.tres @@ -4,6 +4,8 @@ [resource] script = ExtResource("1") +; 迭代 11 第①批 T1(2026-10-03):追加七件同调行(form 非空——永不入过渡渠道);值全 provisional 归 难度曲线与Boss。 +; nest_pendulum/blast_kit 维持过渡行,T7 渠道退役时原位换装 form=active_charged;sapper_order/显影尘/地穴全图卷/罗盘 T7 逐件销账。 rows = [{ "item_id": "probe_dust", "display_name": "显影尘", @@ -20,7 +22,8 @@ rows = [{ "effect_kinds": ["ap_grant"], "target": "none", "magnitude": 2, -"use_phase": "active_dig" +"use_phase": "active_dig", +"retired": true }, { "item_id": "loot_compass", "display_name": "探宝罗盘", @@ -37,7 +40,10 @@ rows = [{ "effect_kinds": ["mine_reveal"], "target": "area_3x3", "magnitude": 0, -"use_phase": "active_dig" +"use_phase": "active_dig", +"form": "active_charged", +"slot_cost": 1, +"charges_per_adventure": 2 }, { "item_id": "catacomb_scroll", "display_name": "地穴全图卷", @@ -54,5 +60,79 @@ rows = [{ "effect_kinds": ["ap_grant", "mine_reveal"], "target": "area_3x3", "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 }] diff --git a/game/data/tables/meta.tres b/game/data/tables/meta.tres index 69f56a3a..d5532d9a 100644 --- a/game/data/tables/meta.tres +++ b/game/data/tables/meta.tres @@ -77,6 +77,15 @@ conversion = [{ "attr": "armor_class", "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): ; 数值全 provisional 归元进度设计批配读数(骨架先行——元进度实现批 2026-09-21,先例 = GP+商店房批)。 @@ -107,3 +116,7 @@ conversion = [{ ; 钥石渠道自本批起 = ; Boss/精英击杀逐杀掉落(大奖励 keystone_per_level 渠道退役;精英掉 1 预留行随怪物批实装落)。 ; 产出结构变化 → 元进度标定义务重开(06-待办清单 2026-10-01 登记条)。provisional 归曲线调参批。 +; +; 迭代 11 第①批 同调实施批 T4(2026-10-03):attune_refresh_upgrade 行族 = Y 刷新升级行(任务书 P1 甲—— +; 每冒险开局给 Y 次、冒险周期池、用完即无;等级 L → Y=L 次刷新,钥石直购)。数值全 provisional 归 +; 难度曲线与Boss(任务书 §三 7)。 diff --git a/game/presentation/aux_interact_controller.gd b/game/presentation/aux_interact_controller.gd index c9374082..ffcc7133 100644 --- a/game/presentation/aux_interact_controller.gd +++ b/game/presentation/aux_interact_controller.gd @@ -34,12 +34,13 @@ func _init(db_ref, meta_ref, info_panel_ref: HeroInfoPanelView, toast_ref: Toast _runner_get = runner_get -# aux 解锁门控集组装(迭代 7 T5——拍板③甲:工坊解锁 ∩ 活行词表;罗盘退役/失义件不入集) +# aux 解锁门控集组装(迭代 7 T5——拍板③甲:工坊解锁 ∩ 过渡活行词表;罗盘退役/失义件/同调行不入集 +# ——迭代 11 第①批 T1 切 aux_transitional_rows 口径,同调行永不入过渡渠道) func unlocked_set() -> Dictionary: var out := {} if meta == null: return out - for row in db.aux_active_rows(): + for row in db.aux_transitional_rows(): var iid := String(row.get("item_id", "")) if meta.is_item_unlocked(iid): out[iid] = true diff --git a/game/presentation/main.gd b/game/presentation/main.gd index a49cc714..6fed531c 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -254,6 +254,7 @@ func _build_ui() -> void: shop_overlay.buy_requested.connect(_on_shop_buy) shop_overlay.leave_pressed.connect(_on_shop_leave) camp_overlay.buy_requested.connect(_on_camp_buy) + camp_overlay.attune_toggled.connect(_on_camp_attune_toggle) camp_overlay.closed.connect(_on_camp_closed) _end_overlay.review_pressed.connect(_on_end_review) _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.replace_chosen.connect(_on_bonus_replace_chosen) bonus_overlay.continue_pressed.connect(_on_bonus_continue) + bonus_overlay.refresh_requested.connect(_on_bonus_refresh) move_child(bonus_overlay, _end_overlay.get_index()) 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) 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) + bonus_overlay.set_refresh_info(adventure.attune_refresh_left()) # 迭代 11 第①批 T5b——刷新余量投喂 log_view.append_bb(Narrator.reward_line()) elif t == "rest": var healed: int = int(info.get("healed", 0)) @@ -527,6 +530,7 @@ func _show_bonus_pick() -> void: var payload: Dictionary = _pending_bonus _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.set_refresh_info(adventure.attune_refresh_left() if adventure != null else 0) # 迭代 11 第①批 T5b——刷新余量投喂 _refresh_presentations() 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)] +# 三选一刷新(迭代 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: if adventure == null: 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", "")))) named.append(rec) 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: @@ -1028,6 +1045,7 @@ func _camp_data() -> Dictionary: "balance": meta.keystone_balance() if meta != null else 0, "forge": forge_rows, "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 [], } @@ -1035,6 +1053,14 @@ func _camp_data() -> Dictionary: func _on_camp_buy(attr: String) -> void: if meta == null: 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): # 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1) board_zone.toast.show_toast("钥石不足——买不起这件强化", "warn") @@ -1043,6 +1069,16 @@ func _on_camp_buy(attr: String) -> void: _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+商店房实施批——写调用唯一归组合根)---- func _on_shop_buy(product_id: String) -> void: diff --git a/game/presentation/views/bonus_pick_overlay.gd b/game/presentation/views/bonus_pick_overlay.gd index e4742f6e..88e8a1c0 100644 --- a/game/presentation/views/bonus_pick_overlay.gd +++ b/game/presentation/views/bonus_pick_overlay.gd @@ -10,12 +10,16 @@ extends Control signal pick_confirmed(item_id: String) signal replace_chosen(discard_index: int) # -1 = 放弃新件不获取 signal continue_pressed +signal refresh_requested # 三选一刷新意图(迭代 11 第①批 T5b——P1 甲 X/Y 双源;重抽归组合根) var _label: Label var _choice_box: HBoxContainer # 三卡按钮区(pick 态) var _replace_box: VBoxContainer # 替换子面板(replace 态) var _confirm_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 _header := "" # 停点头部文案(选择态保持显示) var _card_meta: Array = [] # open_pick 缓存 [{id, text}]——高亮还原读面 @@ -34,6 +38,13 @@ func _init() -> void: _confirm_btn.pressed.connect(_on_confirm_pressed) _confirm_btn.visible = false 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.add_theme_constant_override("separation", 8) _replace_box.alignment = BoxContainer.ALIGNMENT_CENTER @@ -64,9 +75,22 @@ func open_pick(header: String, cards: Array) -> void: _confirm_btn.visible = false _replace_box.visible = false _continue_btn.visible = false + _refresh_btn.visible = false # 刷新余量随 set_refresh_info 投喂后现形(open 先归零防残留) + _refresh_l.text = "" 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: _selected = id for i in _choice_box.get_child_count(): diff --git a/game/presentation/views/camp_overlay_view.gd b/game/presentation/views/camp_overlay_view.gd index b778be23..cdfa5c9e 100644 --- a/game/presentation/views/camp_overlay_view.gd +++ b/game/presentation/views/camp_overlay_view.gd @@ -13,6 +13,7 @@ extends Control signal buy_requested(attr: String) signal closed +signal attune_toggled(item_id: String) # 同调装配切换意图(迭代 11 第①批 T4——写口唯一归 MetaProgression.toggle_attunement) const ATTR_LABELS := {"atk": "攻击", "max_hp": "生命上限", "armor_class": "护甲"} # 档位文案轴(数据契约 attr 词表,与 ShopOverlay 同款) # 方向稿取色(4-camp.png——柜台头带深棕/行板暖米;双色墨=亮板配深墨)——值单点收 UiKit(T4 裸色收口批) @@ -25,6 +26,11 @@ var _forge_holder: VBoxContainer var _workshop_holder: VBoxContainer 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 _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 重建) @@ -91,9 +97,15 @@ func _init() -> void: _forge_holder = VBoxContainer.new() _forge_holder.add_theme_constant_override("separation", 6) 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.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() mid.add_child(_counter_panel("奖励格图鉴", UiKit.TEX_ICON_ATLAS, atlas_holder, 356)) var lock_l := UiKit.label(" —— 柜台结构位预留:内容排 Alpha(地图战斗资源差异化批)", 13, false, UiKit.STONE) @@ -121,7 +133,7 @@ func _init() -> void: # 柜台列面板(方向稿同构):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() panel.custom_minimum_size = Vector2(min_w, 0) panel.add_theme_stylebox_override("panel", @@ -147,10 +159,11 @@ func _counter_panel(title_text: String, icon_path: String, holder: VBoxContainer # ---- 柜台行构建(open_camp 重建;行结构仿 ShopOverlay 商品卡——亮板双色墨制)---- -# 打开营地(全量重建三柜台行 + 最近冒险——数据 = 组合根 _camp_data() 投影) +# 打开营地(全量重建三柜台行 + 同调装配段 + 最近冒险——数据 = 组合根 _camp_data() 投影) func open_camp(data: Dictionary) -> void: _rebuild_forge(data) _rebuild_workshop(data) + _rebuild_attune(data) _rebuild_recent(data) refresh_camp(data) visible = true @@ -196,6 +209,24 @@ func refresh_camp(data: Dictionary) -> void: st_col = UiKit.GOLD_DEEP card.status_l.text = st_text 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: @@ -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}) +# 同调装配段重建(迭代 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: var lines: Array[String] = [] var recent: Array = data.get("recent", []) diff --git a/game/smoke/adventure_monte_carlo.gd b/game/smoke/adventure_monte_carlo.gd index 9ee0a3f3..8d705f86 100644 --- a/game/smoke/adventure_monte_carlo.gd +++ b/game/smoke/adventure_monte_carlo.gd @@ -34,11 +34,41 @@ func _initialize() -> void: var route := "min" if args.size() >= 4 and args[3] in ["min", "max"]: 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() if not db.load_tables("res://data/tables", false): print("表加载失败") quit(1) 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 t0 := Time.get_ticks_msec() 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): var seed_val := seed0 + _run_cursor _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 rec := { "policy": policy, "seed": seed_val, "atk_pct": 0, "hp_pct": 0, "route": route, + "attune": attune_mode, "equipped": attune_profile.get("equipped", []), "result": st["result"], "cleared": bool(sum.get("cleared", false)), "levels_cleared": int(sum.get("levels_cleared", -1)), diff --git a/game/smoke/level_autoplay.gd b/game/smoke/level_autoplay.gd index 030674ac..f6266c90 100644 --- a/game/smoke/level_autoplay.gd +++ b/game/smoke/level_autoplay.gd @@ -114,7 +114,7 @@ static func _drive_level(runner, st: Dictionary, logger, policy: String, route: # (last_battle_enemies——末场战斗敌方显示名序列,失败局即死亡遭遇)。 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", - hero_override: Dictionary = {}) -> Dictionary: + hero_override: Dictionary = {}, attune: Dictionary = {}) -> Dictionary: var own_session := logger == null if own_session: 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.adventure_finished.connect(_on_adventure_finished.bind(st, logger)) 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" if own_session: logger.close_session() diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index dd7f20ac..affbc7a5 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -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) 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)]) - 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/道具件无次数上限(牌库恒非空的结构保证)") - check(int(_db.adventure.transitional_sync_slots) >= 0 and int(_db.adventure.transitional_sync_slots) <= 9, - "transitional_sync_slots ∈ [0,9](%d——同调位过渡读数源)" % int(_db.adventure.transitional_sync_slots)) + check(int(_db.adventure.bonus_cap("gp")) == _db.adventure.BONUS_CAP_UNLIMITED, + "GP 件无次数上限(牌库恒非空的结构保证;aux_item 池种已随同调批 T7 除名)") + check(_db.attunement_growth != null and int(_db.attunement_slot_cap()) == 3 + and Attunement.slots(_db, null) == 0, + "同调位读数源=attunement_growth 谓词表(slot_cap 3;未注入档求值 0——transitional_sync_slots 过渡键已退役,迭代 11 第①批 T7)") # ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 入口停点 / 放弃两态) @@ -527,23 +527,15 @@ func _smoke_adventure() -> void: var draw: Array = a11._draw_bonus_candidates() 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)]) - check((draw as Array).size() >= 2, "剔牌库后候选恒 ≥2(GP/道具件无次数上限——防「三选项全不可选」死角)") - # 同调位替换·丢旧收新路(slots 过渡键=1,carry 满 1 件再取道具件 → 满位转替换) - var slots := int(_db.adventure.transitional_sync_slots) - a11._bonus_pending = {"candidates": ["aux_item", "gp", "attack"], "payload": {}} - a11._bonus_aux_carry.assign(["probe_dust"]) - check(a11.pick_bonus("aux_item") and a11._bonus_pending.has("incoming"), - "满位道具件转替换子交互(持有 %d/%d)" % [a11._bonus_aux_carry.size(), slots]) - check(a11.resolve_aux_replace(0) and (a11._bonus_aux_carry as Array).has("blast_kit") \ - and (a11._bonus_aux_carry as Array).size() <= slots, - "替换受理·丢旧收新(终态 ≤ 同调位 %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", - "替换受理·放弃新件(原持有集不动、新件不获取——两路终态恒 ≤ 同调位)") + check((draw as Array).size() >= 2, "剔牌库后候选恒 ≥2(GP 件无次数上限——防「三选项全不可选」死角)") + # 池收缩(迭代 11 同调批 T7——aux 卡退役出三选一牌库:重复抽样零 aux 候选;同调位替换两路 + # white-box 测试随停点渠道退役拆除——机械惰性承载,零投放零行为) + var aux_seen := false + for i in 40: + if (a11._draw_bonus_candidates() as Array).has("aux_item"): + aux_seen = true + break + check(not aux_seen, "池收缩——40 次重抽零 aux 候选(停点渠道 aux 卡退役,迭代 11 第①批 T7)") # 满血回 0 合法(通关回血封顶口径) var a12 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(12)) a12.start_adventure(12, {"first_script": true}) @@ -3456,7 +3448,8 @@ func _smoke_meta() -> void: # 冒烟隔离存档名册(段首清陈旧 + 段尾清理——契约:测试不污染 user://save.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_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: @@ -3632,6 +3625,54 @@ func _meta_settle_and_workshop() -> void: "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, "摘要 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() @@ -3771,8 +3812,8 @@ func _smoke_save() -> void: fm.store_string(JSON.stringify({"version": 0, "keystone_old": 12})) fm.close() check(f7.load_from_disk() and f7.keystone_balance() == 12, "v0 旧档经迁移链入账(keystone_old 12 → 余额 12)") - check(f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 3, - "迁移链 0→1(测试钩)→3(生产) 写盘 version == 3(v3=断点附随件扩列,迭代 10 奖励重构)") + 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(测试钩)→4(生产) 写盘 version == 4(v4=同调批扩域,迭代 11 第①批)") # ⑧ 域界干净:meta 永久进度域不含局内资源键(行动点/RAP/GP/道具库存/生命账本不入档——契约 2; # 迭代 8 卡①修订:扫描收窄到 meta 域——断点域=冒险进行中瞬态快照,生命/GP 按拍板面入域豁免) @@ -3868,9 +3909,9 @@ func _smoke_save() -> void: fold.store_string("{\"version\": 1, \"meta\": {\"keystone_balance\": 4}}") fold.close() check(fo.load_from_disk() and fo.keystone_balance() == 4 and not fo.has_breakpoint(), - "v1 旧档迁移 v3:断点域补空(合法空态,无断点域不炸)") - check(fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 3, - "v1 旧档迁移后写盘 version == 3") + "v1 旧档迁移 v4:断点域补空(合法空态,无断点域不炸)") + 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 == 4(v1→v4 链式——同调批扩域 2026-10-03)") # ⑫ v2 旧档→v3 兼容(迭代 10 g 裁——semver 向后兼容:旧断点零丢失、新代码可续玩) var fv2 = sm_script.new(paths[10]) diff --git a/game/smoke/t17_defer_probe.gd.uid b/game/smoke/t17_defer_probe.gd.uid new file mode 100644 index 00000000..c3137780 --- /dev/null +++ b/game/smoke/t17_defer_probe.gd.uid @@ -0,0 +1 @@ +uid://7nn2b0crxvas